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/domain/InterviewInvitation.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/InterviewInvitation.java index af095fb..c0f8281 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/InterviewInvitation.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/InterviewInvitation.java @@ -77,6 +77,11 @@ public class InterviewInvitation extends BaseEntity { */ @ApiModelProperty("面试官联系方式") private String contactPhone; + /** + * 面试官姓名 + */ + @ApiModelProperty("面试官姓名") + private String interviewerName; /** * 公司名称(冗余存储,创建时从company表获取) */ diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/CandidateVO.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/CandidateVO.java index 5ba7347..38ec0ec 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/CandidateVO.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/CandidateVO.java @@ -18,4 +18,6 @@ public class CandidateVO extends AppUser { private String companyName; @Excel(name = "岗位名称", sort = 1) private String jobName; + private String interviewStatus; + private Long interviewId; } diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/InterviewInvitationVO.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/InterviewInvitationVO.java index aaa8356..2b56d61 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/InterviewInvitationVO.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/InterviewInvitationVO.java @@ -11,4 +11,10 @@ public class InterviewInvitationVO extends InterviewInvitation { @ApiModelProperty("岗位名称(来自关联查询的实时数据)") private String jobTitle; + + @ApiModelProperty("求职者姓名(来自关联查询的实时数据)") + private String applicantName; + + @ApiModelProperty("求职者联系方式(来自关联查询的实时数据)") + private String applicantPhone; } diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/InterviewInvitationServiceImpl.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/InterviewInvitationServiceImpl.java index f8461c7..d4f955e 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/InterviewInvitationServiceImpl.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/InterviewInvitationServiceImpl.java @@ -17,12 +17,14 @@ import com.ruoyi.common.core.domain.entity.Company; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import javax.annotation.PostConstruct; import javax.sql.DataSource; import java.sql.Connection; import java.sql.Statement; import lombok.extern.slf4j.Slf4j; import java.util.Arrays; + +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; import java.util.List; @@ -43,8 +45,9 @@ public class InterviewInvitationServiceImpl implements InterviewInvitationServic @Autowired private DataSource dataSource; - // @PostConstruct + @EventListener(ApplicationReadyEvent.class) public void initTable() { + log.info("开始检查并初始化 interview_invitation 表..."); try (Connection conn = dataSource.getConnection(); Statement stmt = conn.createStatement()) { // Check if id column has auto-increment (BIGSERIAL or sequence default) @@ -80,6 +83,7 @@ public class InterviewInvitationServiceImpl implements InterviewInvitationServic + "meeting_password varchar(100) NULL," + "interview_location varchar(500) NULL," + "contact_phone varchar(50) NULL," + + "interviewer_name varchar(100) NULL," + "company_name varchar(200) NULL," + "job_name varchar(200) NULL," + "status varchar(20) DEFAULT 'pending' NULL," @@ -96,9 +100,16 @@ public class InterviewInvitationServiceImpl implements InterviewInvitationServic log.info("interview_invitation 表创建成功(序列自增)!"); } else { log.info("interview_invitation 表结构正常,检查新增列..."); - // 为已有数据库添加新列(幂等操作) - try { stmt.execute("ALTER TABLE interview_invitation ADD COLUMN IF NOT EXISTS company_name varchar(200) NULL"); } catch (Exception ignored) {} - try { stmt.execute("ALTER TABLE interview_invitation ADD COLUMN IF NOT EXISTS job_name varchar(200) NULL"); } catch (Exception ignored) {} + // 为已有数据库添加新列(幂等操作,使用 DO 块兼容不支持 IF NOT EXISTS 的数据库版本) + try { + stmt.execute("DO $$ BEGIN ALTER TABLE interview_invitation ADD COLUMN company_name varchar(200) NULL; EXCEPTION WHEN duplicate_column THEN END; $$;"); + } catch (Exception e) { log.warn("添加 company_name 列失败: {}", e.getMessage()); } + try { + stmt.execute("DO $$ BEGIN ALTER TABLE interview_invitation ADD COLUMN job_name varchar(200) NULL; EXCEPTION WHEN duplicate_column THEN END; $$;"); + } catch (Exception e) { log.warn("添加 job_name 列失败: {}", e.getMessage()); } + try { + stmt.execute("DO $$ BEGIN ALTER TABLE interview_invitation ADD COLUMN interviewer_name varchar(100) NULL; EXCEPTION WHEN duplicate_column THEN END; $$;"); + } catch (Exception e) { log.warn("添加 interviewer_name 列失败: {}", e.getMessage()); } } } catch (Exception e) { log.error("初始化表失败: {}", e.getMessage()); diff --git a/ruoyi-bussiness/src/main/resources/mapper/app/InterviewInvitationMapper.xml b/ruoyi-bussiness/src/main/resources/mapper/app/InterviewInvitationMapper.xml index 59e7c90..8afcdfa 100644 --- a/ruoyi-bussiness/src/main/resources/mapper/app/InterviewInvitationMapper.xml +++ b/ruoyi-bussiness/src/main/resources/mapper/app/InterviewInvitationMapper.xml @@ -16,6 +16,7 @@ + @@ -30,12 +31,14 @@ + + select id, company_id, job_id, user_id, apply_id, interview_time, interview_method, meeting_link, meeting_password, interview_location, - contact_phone, company_name, job_name, status, del_flag, + contact_phone, interviewer_name, company_name, job_name, status, del_flag, create_by, create_time, update_by, update_time, remark from interview_invitation @@ -56,10 +59,13 @@