diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrInterviewController.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrInterviewController.java index dcf9c6ac..edca3a60 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrInterviewController.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrInterviewController.java @@ -46,16 +46,25 @@ public class AihrInterviewController { @PostMapping("/start") public R start(@RequestBody StartRequest request) { + if (!canUseInterviewFlow()) { + return R.fail("无权进行面试"); + } return R.ok(interviewService.start(bindAppCandidate(request))); } @PostMapping("/answer") public R answer(@RequestBody AnswerRequest request) { + if (!canUseInterviewFlow()) { + return R.fail("无权进行面试"); + } return R.ok(interviewService.answer(request, currentAppUsername())); } @PostMapping("/finish") public R finish(@RequestBody FinishRequest request) { + if (!canUseInterviewFlow()) { + return R.fail("无权进行面试"); + } return R.ok(interviewService.finish(request, currentAppUsername())); } @@ -114,6 +123,18 @@ public class AihrInterviewController { return username == null ? "" : username.trim(); } + private static boolean canUseInterviewFlow() { + LoginUser loginUser = LoginHelper.getLoginUser(); + if (loginUser == null) { + return false; + } + if (UserType.APP_USER.getUserType().equals(loginUser.getUserType())) { + return true; + } + return UserType.SYS_USER.getUserType().equals(loginUser.getUserType()) + && StpUtil.hasRoleOr(TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE); + } + private static String appCandidateName(String username) { return username.matches("1[3-9]\\d{9}") ? "候选人" + username.substring(username.length() - 4) : username; } diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrInterviewServiceTest.java b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrInterviewServiceTest.java index bfa400bf..58d1c01e 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrInterviewServiceTest.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrInterviewServiceTest.java @@ -147,6 +147,24 @@ class AihrInterviewServiceTest { assertTrue(controllerSource.contains("return R.fail(\"无权查看面试记录\")")); } + @Test + @Tag("dev") + void interviewFlowRequiresCandidateOrHrRole() throws Exception { + Path source = Path.of("src/main/java/org/dromara/aihr/controller/AihrInterviewController.java"); + if (!Files.exists(source)) { + source = Path.of("ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrInterviewController.java"); + } + String controllerSource = Files.readString(source); + + assertTrue(controllerSource.contains("private static boolean canUseInterviewFlow()")); + assertTrue(controllerSource.contains("UserType.APP_USER.getUserType().equals(loginUser.getUserType())")); + assertTrue(controllerSource.contains("StpUtil.hasRoleOr(TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE)")); + assertTrue(controllerSource.contains("return R.fail(\"无权进行面试\")")); + String guard = "if (!canUseInterviewFlow())"; + int guardOccurrences = (controllerSource.length() - controllerSource.replace(guard, "").length()) / guard.length(); + assertEquals(3, guardOccurrences); + } + @Test @Tag("dev") void reviewResultPersistsHumanScoreWithoutChangingAiScore() { diff --git a/docs/BRD_IMPLEMENTATION_AUDIT.md b/docs/BRD_IMPLEMENTATION_AUDIT.md index ce527b1a..8b446729 100644 --- a/docs/BRD_IMPLEMENTATION_AUDIT.md +++ b/docs/BRD_IMPLEMENTATION_AUDIT.md @@ -405,3 +405,4 @@ - 2026-07-14 BRD 4.3.4 场景覆盖补齐:原生活顾问 12 条 seed 只有投诉、催费、报修、突发求助,缺少 BRD 明确要求的服务推介/增值与日常服务;现新增 `service-promotion-needs`、`daily-service-followup` 两条补充场景及五维 Rubric,保留原 12 条核心场景不变。提供幂等迁移 `aihr_20260714_practice_brd_scenarios_mysql8.sql` 和临时库回归脚本;该内容属于待业务复核的初始化素材,不等同于正式试点样本。 - 2026-07-14 BRD 4.3.4 场景 schema 发布补齐:只读核验发现生产缺少 `aihr_practice_scenario`、`aihr_practice_rubric`、`aihr_practice_rubric_dimension` 三张表;现新增只建结构的 `aihr_20260714_practice_catalog_schema_mysql8.sql`,并让补充场景迁移显式依赖该基础迁移。未写入生产数据,正式发布仍需按顺序执行基础 schema、场景 seed 与 SOP seed 迁移。 - 2026-07-14 BRD 4.7 住宅 SOP 内容补齐:原知识库只有 3 份已解析住宅 SOP;现新增增值服务需求识别、日常服务回访两份 SOP 及各 3 个检索片段,reset SQL 与版本化迁移同步,临时库连续执行两次保持幂等。严格门禁已从 `sop_docs=3/5` 推进到 `5/5`;案例库、正式组织身份和正式试点证据仍未完成。 +- 2026-07-15 BRD G3 候选人面试操作角色边界修复:`/api/recruit/interview/start`、`answer`、`finish` 此前只要求登录,普通后台系统用户可绕过 HR/管理员权限创建或完成面试;现统一限制为 APP 候选人本人或 `superadmin/hr_operator`,候选人会话仍按本人手机号绑定。新增源码契约测试和 `demo-check` marker;尚未发布生产。 diff --git a/scripts/demo-check.sh b/scripts/demo-check.sh index 35947fcd..0d2f1a79 100755 --- a/scripts/demo-check.sh +++ b/scripts/demo-check.sh @@ -504,6 +504,8 @@ contains frontend/src/api/aihr/interview.ts "/api/recruit/interview/start" contains frontend/src/api/aihr/interview.ts "/api/recruit/interview/candidate-links" contains frontend/src/api/aihr/interview.ts "/records/\${encodeURIComponent(sessionId)}/review" contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrInterviewController.java "/api/recruit/interview" +contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrInterviewController.java "private static boolean canUseInterviewFlow()" +contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrInterviewController.java "无权进行面试" contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrInterviewController.java "无权查看面试记录" contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrInterviewController.java "/records/{sessionId}/review" contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrInterviewService.java "reviewed_score"