feat: add venue booth templates for outdoor fairs
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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("举办时间")
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
27
sql/cms_venue_booth_template.sql
Normal file
27
sql/cms_venue_booth_template.sql
Normal 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");
|
||||
Reference in New Issue
Block a user