Merge branch 'main' of ssh://124.243.245.42:2222/zkr/shz-backend

This commit is contained in:
2026-06-25 14:42:17 +08:00
12 changed files with 551 additions and 13 deletions

View File

@@ -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;
}
}
}

View File

@@ -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)) {
// 接受时返回面试官联系方式,供前端弹窗展示

View File

@@ -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<InterviewInvitation> list = interviewInvitationService.getInterviewInvitationList(interviewInvitation);
List<InterviewInvitationVO> list = interviewInvitationService.getInterviewInvitationVOList(interviewInvitation);
return getDataTable(list);
}

View File

@@ -77,6 +77,11 @@ public class InterviewInvitation extends BaseEntity {
*/
@ApiModelProperty("面试官联系方式")
private String contactPhone;
/**
* 面试官姓名
*/
@ApiModelProperty("面试官姓名")
private String interviewerName;
/**
* 公司名称冗余存储创建时从company表获取
*/

View File

@@ -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;
}

View File

@@ -11,4 +11,10 @@ public class InterviewInvitationVO extends InterviewInvitation {
@ApiModelProperty("岗位名称(来自关联查询的实时数据)")
private String jobTitle;
@ApiModelProperty("求职者姓名(来自关联查询的实时数据)")
private String applicantName;
@ApiModelProperty("求职者联系方式(来自关联查询的实时数据)")
private String applicantPhone;
}

View File

@@ -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());