feat: add venue booth templates for outdoor fairs

This commit is contained in:
2026-07-22 17:06:00 +08:00
parent 34ea4e5da3
commit 00609ff416
7 changed files with 250 additions and 10 deletions

View File

@@ -26,12 +26,14 @@ import com.ruoyi.cms.domain.OutdoorFairBoothBooking;
import com.ruoyi.cms.domain.OutdoorFairDevice;
import com.ruoyi.cms.domain.OutdoorFairDictDataRequest;
import com.ruoyi.cms.domain.OutdoorFairJob;
import com.ruoyi.cms.domain.VenueBooth;
import com.ruoyi.cms.domain.VenueInfo;
import com.ruoyi.cms.mapper.FairCompanyMapper;
import com.ruoyi.cms.mapper.OutdoorFairBoothMapper;
import com.ruoyi.cms.mapper.OutdoorFairBoothBookingMapper;
import com.ruoyi.cms.mapper.OutdoorFairDeviceMapper;
import com.ruoyi.cms.mapper.OutdoorFairJobMapper;
import com.ruoyi.cms.mapper.VenueBoothMapper;
import com.ruoyi.cms.mapper.CompanyMapper;
import com.ruoyi.cms.service.ICompanyService;
import com.ruoyi.cms.service.IJobService;
@@ -97,6 +99,9 @@ public class OutdoorFairController extends BaseController
@Autowired
private OutdoorFairBoothMapper outdoorFairBoothMapper;
@Autowired
private VenueBoothMapper venueBoothMapper;
@Autowired
private OutdoorFairJobMapper outdoorFairJobMapper;
@@ -120,6 +125,7 @@ public class OutdoorFairController extends BaseController
Company company = RoleUtils.isCompanyAdmin() ? getCurrentCompanyOrNull() : null;
startPage();
List<OutdoorFair> list = outdoorFairService.selectOutdoorFairList(outdoorFair);
fillReservedBoothCount(list);
fillCompanyApplyState(list, request, company);
return getDataTable(list);
}
@@ -373,7 +379,7 @@ public class OutdoorFairController extends BaseController
fillVenueFields(outdoorFair);
boolean saved = outdoorFairService.save(outdoorFair);
if (saved) {
resetBoothSnapshot(outdoorFair, true);
recreateBoothSnapshotFromVenueTemplate(outdoorFair, false);
}
return toAjax(saved);
}
@@ -393,10 +399,14 @@ public class OutdoorFairController extends BaseController
}
OutdoorFair oldFair = outdoorFair.getId() == null ? null : outdoorFairService.getById(outdoorFair.getId());
fillVenueFields(outdoorFair);
boolean venueChanged = oldFair == null || !java.util.Objects.equals(oldFair.getVenueId(), outdoorFair.getVenueId());
if (venueChanged && oldFair != null && hasFairBoothSnapshot(oldFair.getId())
&& !Boolean.TRUE.equals(outdoorFair.getResetBoothMap())) {
return AjaxResult.error("更换场地会清空原展位图和企业预定,请确认后重试");
}
boolean updated = outdoorFairService.updateById(outdoorFair);
if (updated) {
boolean venueChanged = oldFair == null || !java.util.Objects.equals(oldFair.getVenueId(), outdoorFair.getVenueId());
resetBoothSnapshot(outdoorFair, venueChanged);
if (updated && venueChanged) {
recreateBoothSnapshotFromVenueTemplate(outdoorFair, oldFair != null);
}
return toAjax(updated);
}
@@ -963,19 +973,80 @@ public class OutdoorFairController extends BaseController
}
}
private void resetBoothSnapshot(OutdoorFair outdoorFair, boolean forceRecreate)
private void fillReservedBoothCount(List<OutdoorFair> fairs)
{
if (fairs == null || fairs.isEmpty()) {
return;
}
List<Long> fairIds = fairs.stream()
.map(OutdoorFair::getId)
.filter(item -> item != null)
.collect(Collectors.toList());
if (fairIds.isEmpty()) {
return;
}
Map<Long, Integer> countMap = new HashMap<>();
for (Map<String, Object> row : outdoorFairBoothBookingMapper.countReservedBoothsByFairIds(fairIds)) {
Object fairId = row.get("fairId");
Object count = row.get("reservedBoothCount");
if (fairId instanceof Number && count instanceof Number) {
countMap.put(((Number) fairId).longValue(), ((Number) count).intValue());
}
}
for (OutdoorFair fair : fairs) {
fair.setReservedBoothCount(countMap.getOrDefault(fair.getId(), 0));
}
}
private boolean hasFairBoothSnapshot(Long fairId)
{
if (fairId == null) {
return false;
}
Long boothCount = outdoorFairBoothMapper.selectCount(new LambdaQueryWrapper<OutdoorFairBooth>()
.eq(OutdoorFairBooth::getFairId, fairId));
Long bookingCount = outdoorFairBoothBookingMapper.selectCount(new LambdaQueryWrapper<OutdoorFairBoothBooking>()
.eq(OutdoorFairBoothBooking::getFairId, fairId));
return (boothCount != null && boothCount > 0) || (bookingCount != null && bookingCount > 0);
}
/**
* 将当前场地模板克隆为招聘会自己的展位图。模板与副本只在这里发生单向复制,之后互不影响。
*/
private void recreateBoothSnapshotFromVenueTemplate(OutdoorFair outdoorFair, boolean clearExisting)
{
if (outdoorFair.getId() == null) {
return;
}
if (forceRecreate) {
if (clearExisting) {
outdoorFairBoothBookingMapper.physicalDeleteByFairId(outdoorFair.getId());
outdoorFairBoothMapper.physicalDeleteByFairId(outdoorFair.getId());
}
Long boothCount = outdoorFairBoothMapper.selectCount(new LambdaQueryWrapper<OutdoorFairBooth>()
.eq(OutdoorFairBooth::getFairId, outdoorFair.getId()));
outdoorFair.setBoothCount(boothCount == null ? 0 : boothCount.intValue());
outdoorFairService.updateById(outdoorFair);
int boothCount = 0;
if (outdoorFair.getVenueId() != null) {
List<VenueBooth> templateBooths = venueBoothMapper.selectList(new LambdaQueryWrapper<VenueBooth>()
.eq(VenueBooth::getVenueId, outdoorFair.getVenueId())
.orderByAsc(VenueBooth::getPositionY)
.orderByAsc(VenueBooth::getPositionX)
.orderByAsc(VenueBooth::getBoothNumber));
for (VenueBooth templateBooth : templateBooths) {
OutdoorFairBooth fairBooth = new OutdoorFairBooth();
fairBooth.setFairId(outdoorFair.getId());
fairBooth.setSourceVenueId(outdoorFair.getVenueId());
fairBooth.setSourceBoothId(templateBooth.getId());
fairBooth.setBoothNumber(templateBooth.getBoothNumber());
fairBooth.setPositionX(templateBooth.getPositionX() == null ? 0 : templateBooth.getPositionX());
fairBooth.setPositionY(templateBooth.getPositionY() == null ? 0 : templateBooth.getPositionY());
outdoorFairBoothMapper.insert(fairBooth);
boothCount++;
}
}
OutdoorFair boothCountUpdate = new OutdoorFair();
boothCountUpdate.setId(outdoorFair.getId());
boothCountUpdate.setBoothCount(boothCount);
outdoorFairService.updateById(boothCountUpdate);
outdoorFair.setBoothCount(boothCount);
}
private void fillVenueFields(OutdoorFair outdoorFair)

View File

@@ -11,6 +11,7 @@ import java.util.stream.Collectors;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.cms.domain.VenueInfo;
import com.ruoyi.cms.domain.VenueBooth;
import com.ruoyi.cms.domain.OutdoorFair;
import com.ruoyi.cms.domain.OutdoorFairBooth;
import com.ruoyi.cms.domain.OutdoorFairBoothBooking;
@@ -22,6 +23,7 @@ import com.ruoyi.cms.mapper.FairCompanyMapper;
import com.ruoyi.cms.mapper.OutdoorFairBoothMapper;
import com.ruoyi.cms.mapper.OutdoorFairBoothBookingMapper;
import com.ruoyi.cms.mapper.OutdoorFairMapper;
import com.ruoyi.cms.mapper.VenueBoothMapper;
import com.ruoyi.cms.service.IVenueInfoService;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
@@ -68,6 +70,9 @@ public class VenueInfoController extends BaseController
@Autowired
private OutdoorFairBoothMapper outdoorFairBoothMapper;
@Autowired
private VenueBoothMapper venueBoothMapper;
@Autowired
private CompanyMapper companyMapper;
@@ -148,6 +153,60 @@ public class VenueInfoController extends BaseController
return toAjax(venueInfoService.insertVenueInfoDictData(request.getDictType(), request.getDictLabel(), getUsername()));
}
@ApiOperation("查询场地展位图模板")
@PreAuthorize("@ss.hasPermi('cms:venueInfo:query')")
@GetMapping("/{venueId}/booth-map")
public AjaxResult venueBoothMap(@PathVariable Long venueId)
{
if (venueInfoService.getById(venueId) == null) {
return AjaxResult.error("场地不存在");
}
List<VenueBooth> booths = venueBoothMapper.selectList(new LambdaQueryWrapper<VenueBooth>()
.eq(VenueBooth::getVenueId, venueId)
.orderByAsc(VenueBooth::getPositionY)
.orderByAsc(VenueBooth::getPositionX)
.orderByAsc(VenueBooth::getBoothNumber));
return success(booths);
}
@ApiOperation("保存场地展位图模板")
@PreAuthorize("@ss.hasPermi('cms:venueInfo:edit')")
@Log(title = "场地展位图模板", businessType = BusinessType.UPDATE)
@Transactional(rollbackFor = Exception.class)
@PostMapping("/{venueId}/booths")
public AjaxResult saveVenueBooths(@PathVariable Long venueId, @RequestBody(required = false) List<VenueBooth> booths)
{
if (venueInfoService.getById(venueId) == null) {
return AjaxResult.error("场地不存在");
}
Set<String> boothNumbers = new HashSet<>();
List<VenueBooth> templateBooths = booths == null ? new ArrayList<>() : booths;
for (VenueBooth booth : templateBooths) {
String boothNumber = booth == null ? null : StringUtils.trim(booth.getBoothNumber());
if (StringUtils.isBlank(boothNumber)) {
return AjaxResult.error("展位号不能为空");
}
if (!boothNumbers.add(boothNumber)) {
return AjaxResult.error("展位号不能重复:" + boothNumber);
}
booth.setBoothNumber(boothNumber);
booth.setVenueId(venueId);
booth.setId(null);
booth.setPositionX(booth.getPositionX() == null ? 0 : booth.getPositionX());
booth.setPositionY(booth.getPositionY() == null ? 0 : booth.getPositionY());
}
venueBoothMapper.physicalDeleteByVenueId(venueId);
for (VenueBooth booth : templateBooths) {
venueBoothMapper.insert(booth);
}
VenueInfo venueInfo = new VenueInfo();
venueInfo.setId(venueId);
venueInfo.setFloorCount(templateBooths.size());
venueInfoService.updateById(venueInfo);
return success();
}
@ApiOperation("查询户外招聘会展位图")
@PreAuthorize("@ss.hasPermi('cms:outdoorFair:query')")
@GetMapping("/fair/{fairId}/booth-map")

View File

@@ -62,6 +62,17 @@ public class OutdoorFair extends BaseEntity
@ApiModelProperty("展位数量")
private Integer boothCount;
@TableField(exist = false)
@ApiModelProperty("已预定摊位数")
private Integer reservedBoothCount;
/**
* 编辑时更换场地且已有展位副本时,必须由前端明确确认后才允许清空旧副本和预定。
*/
@TableField(exist = false)
@ApiModelProperty("确认按新场地模板重建展位图")
private Boolean resetBoothMap;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "举办时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty("举办时间")

View File

@@ -0,0 +1,38 @@
package com.ruoyi.cms.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.ruoyi.common.core.domain.BaseEntity;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* 场地展位图模板对象 cms_venue_booth。
*
* 模板只描述场地的固定布局;招聘会使用时会复制为 cms_outdoor_fair_booth 的独立副本。
*/
@Data
@ApiModel("场地展位图模板")
@TableName("cms_venue_booth")
public class VenueBooth extends BaseEntity
{
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
@ApiModelProperty("主键ID")
private Long id;
@ApiModelProperty("场地ID")
private Long venueId;
@ApiModelProperty("展位号")
private String boothNumber;
@ApiModelProperty("X坐标")
private Integer positionX;
@ApiModelProperty("Y坐标")
private Integer positionY;
}

View File

@@ -4,6 +4,10 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.cms.domain.OutdoorFairBoothBooking;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
import java.util.Map;
public interface OutdoorFairBoothBookingMapper extends BaseMapper<OutdoorFairBoothBooking>
{
@@ -15,4 +19,18 @@ public interface OutdoorFairBoothBookingMapper extends BaseMapper<OutdoorFairBoo
@Delete("DELETE FROM cms_outdoor_fair_booth_booking WHERE fair_id = #{fairId}")
int physicalDeleteByFairId(@Param("fairId") Long fairId);
/** 批量统计有效预定展位,供招聘会列表使用,避免逐条招聘会查询。 */
@Select({
"<script>",
"SELECT fair_id AS \\\"fairId\\\", COUNT(DISTINCT booth_id) AS \\\"reservedBoothCount\\\"",
"FROM cms_outdoor_fair_booth_booking",
"WHERE del_flag = '0' AND status = 'reserved' AND fair_id IN",
"<foreach collection='fairIds' item='fairId' open='(' separator=',' close=')'>",
"#{fairId}",
"</foreach>",
"GROUP BY fair_id",
"</script>"
})
List<Map<String, Object>> countReservedBoothsByFairIds(@Param("fairIds") List<Long> fairIds);
}

View File

@@ -0,0 +1,16 @@
package com.ruoyi.cms.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.cms.domain.VenueBooth;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Param;
/** 场地展位图模板 Mapper */
public interface VenueBoothMapper extends BaseMapper<VenueBooth>
{
/**
* 模板保存采用完整覆盖。这里物理删除可避免逻辑删除记录占用唯一展位号约束。
*/
@Delete("DELETE FROM cms_venue_booth WHERE venue_id = #{venueId}")
int physicalDeleteByVenueId(@Param("venueId") Long venueId);
}

View File

@@ -0,0 +1,27 @@
-- 场地展位图模板:只保存固定布局,不保存企业、预定状态或预定记录。
-- 瀚高数据库 / PostgreSQL 兼容;可重复执行。
CREATE SEQUENCE IF NOT EXISTS "cms_venue_booth_id_seq" START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1;
CREATE TABLE IF NOT EXISTS "cms_venue_booth" (
"id" BIGINT NOT NULL DEFAULT nextval('"cms_venue_booth_id_seq"') PRIMARY KEY,
"venue_id" BIGINT NOT NULL,
"booth_number" VARCHAR(50) NOT NULL,
"position_x" INTEGER NOT NULL DEFAULT 0,
"position_y" INTEGER NOT NULL DEFAULT 0,
"create_by" VARCHAR(64) DEFAULT '',
"create_time" TIMESTAMP(0),
"update_by" VARCHAR(64) DEFAULT '',
"update_time" TIMESTAMP(0),
"remark" VARCHAR(500),
"del_flag" CHAR(1) DEFAULT '0'
);
COMMENT ON TABLE "cms_venue_booth" IS '场地展位图模板';
COMMENT ON COLUMN "cms_venue_booth"."venue_id" IS '场地ID';
COMMENT ON COLUMN "cms_venue_booth"."booth_number" IS '展位号';
COMMENT ON COLUMN "cms_venue_booth"."position_x" IS 'X坐标';
COMMENT ON COLUMN "cms_venue_booth"."position_y" IS 'Y坐标';
CREATE UNIQUE INDEX IF NOT EXISTS "uk_cms_venue_booth_number" ON "cms_venue_booth" ("venue_id", "booth_number");
CREATE INDEX IF NOT EXISTS "idx_cms_venue_booth_venue" ON "cms_venue_booth" ("venue_id");