diff --git a/.gitignore b/.gitignore
index 0f3ddc9..a86bdd0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -45,4 +45,12 @@ nbdist/
!*/build/*.java
!*/build/*.html
!*/build/*.xml
-.DS_Store
\ No newline at end of file
+.DS_Store
+
+# Local credentials for project-scoped Codex skills
+.codex/skills/*/.password
+
+# Local application runtime files
+.local/
+.codex/
+local.sh
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index c685488..e7dff0b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -33,6 +33,7 @@
3.5.1
7.19.0
2.5.0
+ 1.18.34
5.5.9
@@ -129,6 +130,13 @@
${velocity.version}
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+
+
com.alibaba.fastjson2
@@ -228,7 +236,7 @@
org.apache.maven.plugins
maven-compiler-plugin
- 3.1
+ 3.8.1
${java.version}
${java.version}
diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/config/InterviewMenuMigration.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/config/InterviewMenuMigration.java
new file mode 100644
index 0000000..855235d
--- /dev/null
+++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/config/InterviewMenuMigration.java
@@ -0,0 +1,140 @@
+package com.ruoyi.cms.config;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.context.event.ApplicationReadyEvent;
+import org.springframework.context.event.EventListener;
+import org.springframework.stereotype.Component;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.Statement;
+
+/**
+ * 面试菜单权限自动迁移
+ * 应用启动后自动检查并添加面试列表菜单及按钮权限到 sys_menu 和 sys_role_menu 表
+ */
+@Slf4j
+@Component
+public class InterviewMenuMigration {
+
+ @Autowired
+ private DataSource dataSource;
+
+ @EventListener(ApplicationReadyEvent.class)
+ public void migrate() {
+ log.info("开始检查面试菜单权限...");
+ try (Connection conn = dataSource.getConnection()) {
+ // Step 1: 查找岗位管理目录的 menu_id
+ Long parentId = null;
+ try (Statement stmt = conn.createStatement();
+ ResultSet rs = stmt.executeQuery(
+ "SELECT menu_id FROM sys_menu WHERE path = 'management' AND menu_type = 'M' LIMIT 1")) {
+ if (rs.next()) {
+ parentId = rs.getLong("menu_id");
+ }
+ }
+
+ if (parentId == null) {
+ log.warn("未找到 management 目录菜单,跳过面试菜单迁移");
+ return;
+ }
+
+ log.info("找到 management 目录菜单,parent_id={}", parentId);
+
+ // Step 2: 插入面试列表菜单(幂等)
+ Long menuId = insertMenuIfNotExists(conn, 2100L, "面试列表", parentId, 10,
+ "interview-list", "Management/InterviewList", "InterviewList",
+ "C", "cms:interview:list", "message");
+
+ if (menuId == null) {
+ log.warn("面试列表菜单插入失败(可能已存在但 parent_id 不同)");
+ return;
+ }
+
+ // Step 3: 插入按钮权限
+ insertMenuIfNotExists(conn, 2101L, "面试查询", menuId, 1,
+ "", "", "", "F", "cms:interview:query", "#");
+ insertMenuIfNotExists(conn, 2102L, "面试新增", menuId, 2,
+ "", "", "", "F", "cms:interview:add", "#");
+ insertMenuIfNotExists(conn, 2103L, "面试修改", menuId, 3,
+ "", "", "", "F", "cms:interview:edit", "#");
+ insertMenuIfNotExists(conn, 2104L, "面试删除", menuId, 4,
+ "", "", "", "F", "cms:interview:remove", "#");
+
+ // Step 4: 将新菜单赋给所有非管理员角色
+ try (Statement stmt = conn.createStatement()) {
+ stmt.execute(
+ "INSERT INTO sys_role_menu (role_id, menu_id) " +
+ "SELECT r.role_id, " + menuId + " FROM sys_role r " +
+ "WHERE r.role_key != 'admin' " +
+ "AND NOT EXISTS (SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = r.role_id AND rm.menu_id = " + menuId + ")");
+ } catch (Exception e) {
+ log.debug("角色菜单关联已存在或插入失败: {}", e.getMessage());
+ }
+
+ // 按钮也赋给角色
+ for (long btnId : new long[]{2101L, 2102L, 2103L, 2104L}) {
+ try (Statement stmt = conn.createStatement()) {
+ stmt.execute(
+ "INSERT INTO sys_role_menu (role_id, menu_id) " +
+ "SELECT r.role_id, " + btnId + " FROM sys_role r " +
+ "WHERE r.role_key != 'admin' " +
+ "AND NOT EXISTS (SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = r.role_id AND rm.menu_id = " + btnId + ")");
+ } catch (Exception e) {
+ log.debug("按钮角色关联已存在: {}", e.getMessage());
+ }
+ }
+
+ log.info("面试菜单权限迁移完成");
+ } catch (Exception e) {
+ log.error("面试菜单迁移失败: {}", e.getMessage(), e);
+ }
+ }
+
+ /**
+ * 幂等插入菜单记录
+ */
+ private Long insertMenuIfNotExists(Connection conn, Long menuId, String menuName,
+ Long parentId, int orderNum, String path,
+ String component, String routeName,
+ String menuType, String perms, String icon) {
+ try {
+ // 检查是否已存在
+ try (PreparedStatement ps = conn.prepareStatement(
+ "SELECT 1 FROM sys_menu WHERE menu_id = ?")) {
+ ps.setLong(1, menuId);
+ try (ResultSet rs = ps.executeQuery()) {
+ if (rs.next()) {
+ log.debug("菜单已存在: {} (id={})", menuName, menuId);
+ return menuId;
+ }
+ }
+ }
+
+ // 插入新菜单
+ try (PreparedStatement ps = conn.prepareStatement(
+ "INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time) " +
+ "VALUES (?, ?, ?, ?, ?, ?, '', ?, 1, 0, ?, '0', '0', ?, ?, 'admin', CURRENT_TIMESTAMP)")) {
+ ps.setLong(1, menuId);
+ ps.setString(2, menuName);
+ ps.setLong(3, parentId);
+ ps.setInt(4, orderNum);
+ ps.setString(5, path);
+ ps.setString(6, component);
+ ps.setString(7, routeName);
+ ps.setString(8, menuType);
+ ps.setString(9, perms);
+ ps.setString(10, icon);
+ ps.executeUpdate();
+ log.info("菜单创建成功: {} (id={}, perms={})", menuName, menuId, perms);
+ return menuId;
+ }
+ } catch (Exception e) {
+ log.warn("菜单创建失败: {} (id={}), error={}", menuName, menuId, e.getMessage());
+ return null;
+ }
+ }
+}
diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/app/AppInterviewController.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/app/AppInterviewController.java
index 1fc40dd..5ec8fac 100644
--- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/app/AppInterviewController.java
+++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/app/AppInterviewController.java
@@ -12,6 +12,8 @@ import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
+import javax.servlet.http.HttpServletRequest;
+import java.io.BufferedReader;
import java.util.List;
@@ -67,7 +69,32 @@ public class AppInterviewController extends BaseController {
*/
@ApiOperation("更新面试邀约状态")
@PutMapping("/status/{id}")
- public AjaxResult updateStatus(@PathVariable Long id, @RequestParam String status){
+ public AjaxResult updateStatus(@PathVariable Long id, HttpServletRequest request) {
+ String status = request.getParameter("status");
+ // 如果 URL 参数没有,尝试从 JSON body 中读取
+ if (status == null || status.isEmpty()) {
+ try {
+ BufferedReader reader = request.getReader();
+ StringBuilder sb = new StringBuilder();
+ String line;
+ while ((line = reader.readLine()) != null) {
+ sb.append(line);
+ }
+ String body = sb.toString();
+ if (body != null && !body.isEmpty()) {
+ // 简单解析 JSON: {"status": "accepted"}
+ status = body.replaceAll(".*\"status\"\\s*:\\s*\"([^\"]+)\".*", "$1");
+ if (status.equals(body)) {
+ status = null;
+ }
+ }
+ } catch (Exception e) {
+ // ignore body read errors
+ }
+ }
+ if (status == null || status.isEmpty()) {
+ return AjaxResult.error("状态参数不能为空");
+ }
int rows = interviewInvitationService.updateInterviewStatus(id, status);
if (rows > 0 && "accepted".equals(status)) {
// 接受时返回面试官联系方式,供前端弹窗展示
diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/CmsInterviewController.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/CmsInterviewController.java
index 27239ff..5ad5191 100644
--- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/CmsInterviewController.java
+++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/CmsInterviewController.java
@@ -1,6 +1,7 @@
package com.ruoyi.cms.controller.cms;
import com.ruoyi.cms.domain.InterviewInvitation;
+import com.ruoyi.cms.domain.vo.InterviewInvitationVO;
import com.ruoyi.cms.service.InterviewInvitationService;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
@@ -38,7 +39,7 @@ public class CmsInterviewController extends BaseController {
@GetMapping("/list")
public TableDataInfo list(InterviewInvitation interviewInvitation){
startPage();
- List list = interviewInvitationService.getInterviewInvitationList(interviewInvitation);
+ List list = interviewInvitationService.getInterviewInvitationVOList(interviewInvitation);
return getDataTable(list);
}
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
new file mode 100644
index 0000000..e289dbe
--- /dev/null
+++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/OutdoorFairController.java
@@ -0,0 +1,134 @@
+package com.ruoyi.cms.controller.cms;
+
+import java.util.Arrays;
+import java.util.List;
+
+import com.ruoyi.cms.domain.OutdoorFair;
+import com.ruoyi.cms.domain.OutdoorFairDictDataRequest;
+import com.ruoyi.cms.domain.VenueInfo;
+import com.ruoyi.cms.service.IOutdoorFairService;
+import com.ruoyi.cms.service.IVenueInfoService;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.common.exception.ServiceException;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 户外招聘会Controller
+ *
+ * @author ruoyi
+ * @date 2026-06-25
+ */
+@RestController
+@RequestMapping("/cms/outdoor-fair")
+@Api(tags = "后台:户外招聘会")
+public class OutdoorFairController extends BaseController
+{
+ @Autowired
+ private IOutdoorFairService outdoorFairService;
+
+ @Autowired
+ private IVenueInfoService venueInfoService;
+
+ /**
+ * 查询户外招聘会列表
+ */
+ @ApiOperation("查询户外招聘会列表")
+ @PreAuthorize("@ss.hasPermi('cms:outdoorFair:list')")
+ @GetMapping("/list")
+ public TableDataInfo list(OutdoorFair outdoorFair)
+ {
+ startPage();
+ List list = outdoorFairService.selectOutdoorFairList(outdoorFair);
+ return getDataTable(list);
+ }
+
+ /**
+ * 获取户外招聘会详细信息
+ */
+ @ApiOperation("获取户外招聘会详细信息")
+ @PreAuthorize("@ss.hasPermi('cms:outdoorFair:query')")
+ @GetMapping(value = "/{id}")
+ public AjaxResult getInfo(@PathVariable("id") Long id)
+ {
+ return success(outdoorFairService.getById(id));
+ }
+
+ /**
+ * 新增户外招聘会
+ */
+ @ApiOperation("新增户外招聘会")
+ @PreAuthorize("@ss.hasPermi('cms:outdoorFair:add')")
+ @Log(title = "户外招聘会", businessType = BusinessType.INSERT)
+ @PostMapping
+ public AjaxResult add(@RequestBody OutdoorFair outdoorFair)
+ {
+ fillVenueFields(outdoorFair);
+ return toAjax(outdoorFairService.save(outdoorFair));
+ }
+
+ /**
+ * 修改户外招聘会
+ */
+ @ApiOperation("修改户外招聘会")
+ @PreAuthorize("@ss.hasPermi('cms:outdoorFair:edit')")
+ @Log(title = "户外招聘会", businessType = BusinessType.UPDATE)
+ @PutMapping
+ public AjaxResult edit(@RequestBody OutdoorFair outdoorFair)
+ {
+ fillVenueFields(outdoorFair);
+ return toAjax(outdoorFairService.updateById(outdoorFair));
+ }
+
+ /**
+ * 删除户外招聘会
+ */
+ @ApiOperation("删除户外招聘会")
+ @PreAuthorize("@ss.hasPermi('cms:outdoorFair:remove')")
+ @Log(title = "户外招聘会", businessType = BusinessType.DELETE)
+ @DeleteMapping("/{ids}")
+ public AjaxResult remove(@PathVariable Long[] ids)
+ {
+ return toAjax(outdoorFairService.removeByIds(Arrays.asList(ids)));
+ }
+
+ /**
+ * 新增户外招聘会页面使用的字典项
+ */
+ @ApiOperation("新增户外招聘会字典项")
+ @PreAuthorize("@ss.hasAnyPermi('cms:outdoorFair:add,cms:outdoorFair:edit')")
+ @Log(title = "户外招聘会字典项", businessType = BusinessType.INSERT)
+ @PostMapping("/dict-data")
+ public AjaxResult addDictData(@Validated @RequestBody OutdoorFairDictDataRequest request)
+ {
+ return toAjax(outdoorFairService.insertOutdoorFairDictData(request.getDictType(), request.getDictLabel(), getUsername()));
+ }
+
+ private void fillVenueFields(OutdoorFair outdoorFair)
+ {
+ if (outdoorFair.getVenueId() == null) {
+ return;
+ }
+ VenueInfo venueInfo = venueInfoService.getById(outdoorFair.getVenueId());
+ if (venueInfo == null) {
+ throw new ServiceException("绑定场地不存在");
+ }
+ outdoorFair.setVenueName(venueInfo.getVenueName());
+ outdoorFair.setBoothCount(venueInfo.getFloorCount() == null ? 0 : venueInfo.getFloorCount());
+ }
+}
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
new file mode 100644
index 0000000..c8e4414
--- /dev/null
+++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/VenueInfoController.java
@@ -0,0 +1,325 @@
+package com.ruoyi.cms.controller.cms;
+
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.ruoyi.cms.domain.VenueInfo;
+import com.ruoyi.cms.domain.OutdoorFair;
+import com.ruoyi.cms.domain.OutdoorFairBoothBooking;
+import com.ruoyi.cms.domain.VenueBooth;
+import com.ruoyi.cms.domain.VenueInfoDictDataRequest;
+import com.ruoyi.cms.domain.VenueBoothSwapRequest;
+import com.ruoyi.cms.mapper.CompanyMapper;
+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;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.domain.entity.Company;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.common.utils.StringUtils;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 场地信息Controller
+ *
+ * @author ruoyi
+ * @date 2026-06-25
+ */
+@RestController
+@RequestMapping("/cms/venue-info")
+@Api(tags = "后台:场地信息维护")
+public class VenueInfoController extends BaseController
+{
+ @Autowired
+ private IVenueInfoService venueInfoService;
+
+ @Autowired
+ private VenueBoothMapper venueBoothMapper;
+
+ @Autowired
+ private OutdoorFairMapper outdoorFairMapper;
+
+ @Autowired
+ private OutdoorFairBoothBookingMapper outdoorFairBoothBookingMapper;
+
+ @Autowired
+ private CompanyMapper companyMapper;
+
+ /**
+ * 查询场地信息列表
+ */
+ @ApiOperation("查询场地信息列表")
+ @PreAuthorize("@ss.hasPermi('cms:venueInfo:list')")
+ @GetMapping("/list")
+ public TableDataInfo list(VenueInfo venueInfo)
+ {
+ startPage();
+ List list = venueInfoService.selectVenueInfoList(venueInfo);
+ return getDataTable(list);
+ }
+
+ /**
+ * 获取场地信息详细信息
+ */
+ @ApiOperation("获取场地信息详细信息")
+ @PreAuthorize("@ss.hasPermi('cms:venueInfo:query')")
+ @GetMapping(value = "/{id}")
+ public AjaxResult getInfo(@PathVariable("id") Long id)
+ {
+ return success(venueInfoService.getById(id));
+ }
+
+ /**
+ * 新增场地信息
+ */
+ @ApiOperation("新增场地信息")
+ @PreAuthorize("@ss.hasPermi('cms:venueInfo:add')")
+ @Log(title = "场地信息", businessType = BusinessType.INSERT)
+ @PostMapping
+ public AjaxResult add(@RequestBody VenueInfo venueInfo)
+ {
+ fillHandleFields(venueInfo);
+ return toAjax(venueInfoService.save(venueInfo));
+ }
+
+ /**
+ * 修改场地信息
+ */
+ @ApiOperation("修改场地信息")
+ @PreAuthorize("@ss.hasPermi('cms:venueInfo:edit')")
+ @Log(title = "场地信息", businessType = BusinessType.UPDATE)
+ @PutMapping
+ public AjaxResult edit(@RequestBody VenueInfo venueInfo)
+ {
+ fillHandleFields(venueInfo);
+ return toAjax(venueInfoService.updateById(venueInfo));
+ }
+
+ /**
+ * 删除场地信息
+ */
+ @ApiOperation("删除场地信息")
+ @PreAuthorize("@ss.hasPermi('cms:venueInfo:remove')")
+ @Log(title = "场地信息", businessType = BusinessType.DELETE)
+ @DeleteMapping("/{ids}")
+ public AjaxResult remove(@PathVariable Long[] ids)
+ {
+ return toAjax(venueInfoService.removeByIds(Arrays.asList(ids)));
+ }
+
+ /**
+ * 新增场地信息维护页面使用的字典项
+ */
+ @ApiOperation("新增场地信息字典项")
+ @PreAuthorize("@ss.hasAnyPermi('cms:venueInfo:add,cms:venueInfo:edit')")
+ @Log(title = "场地信息字典项", businessType = BusinessType.INSERT)
+ @PostMapping("/dict-data")
+ public AjaxResult addDictData(@Validated @RequestBody VenueInfoDictDataRequest request)
+ {
+ return toAjax(venueInfoService.insertVenueInfoDictData(request.getDictType(), request.getDictLabel(), getUsername()));
+ }
+
+ @ApiOperation("查询场地展位图")
+ @PreAuthorize("@ss.hasAnyPermi('cms:venueInfo:query,cms:outdoorFair:query')")
+ @GetMapping("/{venueId}/booths")
+ public AjaxResult listBooths(@PathVariable Long venueId)
+ {
+ List list = venueBoothMapper.selectList(new LambdaQueryWrapper()
+ .eq(VenueBooth::getVenueId, venueId)
+ .orderByAsc(VenueBooth::getPositionY)
+ .orderByAsc(VenueBooth::getPositionX)
+ .orderByAsc(VenueBooth::getBoothNumber));
+ return success(list);
+ }
+
+ @ApiOperation("保存场地展位图")
+ @PreAuthorize("@ss.hasPermi('cms:venueInfo:edit')")
+ @Transactional(rollbackFor = Exception.class)
+ @PostMapping("/{venueId}/booths")
+ public AjaxResult saveBooths(@PathVariable Long venueId, @RequestBody List booths)
+ {
+ venueBoothMapper.physicalDeleteDeletedByVenueId(venueId);
+ List existingBooths = venueBoothMapper.selectList(new LambdaQueryWrapper()
+ .eq(VenueBooth::getVenueId, venueId));
+ Map existingById = existingBooths.stream()
+ .filter(item -> item.getId() != null)
+ .collect(Collectors.toMap(VenueBooth::getId, item -> item, (a, b) -> a));
+ Map existingByNumber = existingBooths.stream()
+ .filter(item -> StringUtils.isNotBlank(item.getBoothNumber()))
+ .collect(Collectors.toMap(VenueBooth::getBoothNumber, item -> item, (a, b) -> a));
+ Set boothNumbers = new HashSet<>();
+ Set savedIds = new HashSet<>();
+ if (booths != null) {
+ for (VenueBooth booth : booths) {
+ if (StringUtils.isBlank(booth.getBoothNumber())) {
+ return AjaxResult.error("展位号不能为空");
+ }
+ if (!boothNumbers.add(booth.getBoothNumber())) {
+ return AjaxResult.error("展位号不能重复:" + booth.getBoothNumber());
+ }
+ }
+ for (VenueBooth booth : booths) {
+ VenueBooth existing = booth.getId() == null ? null : existingById.get(booth.getId());
+ if (existing == null) {
+ existing = existingByNumber.get(booth.getBoothNumber());
+ }
+ booth.setVenueId(venueId);
+ if (existing == null) {
+ booth.setId(null);
+ venueBoothMapper.insert(booth);
+ } else {
+ booth.setId(existing.getId());
+ venueBoothMapper.updateById(booth);
+ }
+ if (booth.getId() != null) {
+ savedIds.add(booth.getId());
+ }
+ }
+ VenueInfo venueInfo = venueInfoService.getById(venueId);
+ if (venueInfo != null) {
+ venueInfo.setFloorCount(booths.size());
+ venueInfoService.updateById(venueInfo);
+ }
+ }
+ List removedIds = existingBooths.stream()
+ .map(VenueBooth::getId)
+ .filter(id -> id != null && !savedIds.contains(id))
+ .collect(Collectors.toList());
+ if (!removedIds.isEmpty()) {
+ venueBoothMapper.physicalDeleteByVenueIdAndIds(venueId, removedIds);
+ }
+ return success();
+ }
+
+ @ApiOperation("查询户外招聘会展位图")
+ @PreAuthorize("@ss.hasPermi('cms:outdoorFair:query')")
+ @GetMapping("/fair/{fairId}/booth-map")
+ public AjaxResult fairBoothMap(@PathVariable Long fairId)
+ {
+ OutdoorFair fair = outdoorFairMapper.selectById(fairId);
+ if (fair == null || fair.getVenueId() == null) {
+ return success(new ArrayList<>());
+ }
+ List booths = venueBoothMapper.selectList(new LambdaQueryWrapper()
+ .eq(VenueBooth::getVenueId, fair.getVenueId())
+ .orderByAsc(VenueBooth::getPositionY)
+ .orderByAsc(VenueBooth::getPositionX)
+ .orderByAsc(VenueBooth::getBoothNumber));
+ List bookings = outdoorFairBoothBookingMapper.selectList(new LambdaQueryWrapper()
+ .eq(OutdoorFairBoothBooking::getFairId, fairId));
+ Map bookingMap = bookings.stream().collect(Collectors.toMap(OutdoorFairBoothBooking::getBoothId, item -> item, (a, b) -> a));
+ List