diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/OutdoorFairController.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/OutdoorFairController.java index 5e86109..c9497ef 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/OutdoorFairController.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/OutdoorFairController.java @@ -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 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 fairs) + { + if (fairs == null || fairs.isEmpty()) { + return; + } + List fairIds = fairs.stream() + .map(OutdoorFair::getId) + .filter(item -> item != null) + .collect(Collectors.toList()); + if (fairIds.isEmpty()) { + return; + } + Map countMap = new HashMap<>(); + for (Map 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() + .eq(OutdoorFairBooth::getFairId, fairId)); + Long bookingCount = outdoorFairBoothBookingMapper.selectCount(new LambdaQueryWrapper() + .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() - .eq(OutdoorFairBooth::getFairId, outdoorFair.getId())); - outdoorFair.setBoothCount(boothCount == null ? 0 : boothCount.intValue()); - outdoorFairService.updateById(outdoorFair); + + int boothCount = 0; + if (outdoorFair.getVenueId() != null) { + List templateBooths = venueBoothMapper.selectList(new LambdaQueryWrapper() + .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) diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/VenueInfoController.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/VenueInfoController.java index d3ba639..7d6d0e5 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/VenueInfoController.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/VenueInfoController.java @@ -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 booths = venueBoothMapper.selectList(new LambdaQueryWrapper() + .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 booths) + { + if (venueInfoService.getById(venueId) == null) { + return AjaxResult.error("场地不存在"); + } + Set boothNumbers = new HashSet<>(); + List 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") diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/OutdoorFair.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/OutdoorFair.java index ea6f6d4..4405421 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/OutdoorFair.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/OutdoorFair.java @@ -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("举办时间") diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/VenueBooth.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/VenueBooth.java new file mode 100644 index 0000000..139254c --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/VenueBooth.java @@ -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; +} diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/OutdoorFairBoothBookingMapper.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/OutdoorFairBoothBookingMapper.java index 765fa1b..23cc37d 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/OutdoorFairBoothBookingMapper.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/OutdoorFairBoothBookingMapper.java @@ -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 { @@ -15,4 +19,18 @@ public interface OutdoorFairBoothBookingMapper extends BaseMapper", + "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", + "", + "#{fairId}", + "", + "GROUP BY fair_id", + "" + }) + List> countReservedBoothsByFairIds(@Param("fairIds") List fairIds); } diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/VenueBoothMapper.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/VenueBoothMapper.java new file mode 100644 index 0000000..feef548 --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/VenueBoothMapper.java @@ -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 +{ + /** + * 模板保存采用完整覆盖。这里物理删除可避免逻辑删除记录占用唯一展位号约束。 + */ + @Delete("DELETE FROM cms_venue_booth WHERE venue_id = #{venueId}") + int physicalDeleteByVenueId(@Param("venueId") Long venueId); +} diff --git a/sql/cms_venue_booth_template.sql b/sql/cms_venue_booth_template.sql new file mode 100644 index 0000000..a3f8338 --- /dev/null +++ b/sql/cms_venue_booth_template.sql @@ -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");