From 2cae83ff1b3e53d75f42a10899dbb0237fc54654 Mon Sep 17 00:00:00 2001 From: let5sne Date: Tue, 14 Jul 2026 10:23:56 +0800 Subject: [PATCH] fix(aihr): scope mobile home metrics by identity --- .../aihr/controller/AihrMobileController.java | 22 ++- .../aihr/service/AihrMobileSeedService.java | 75 ++++++++-- .../aihr/service/AihrPracticeSeedService.java | 133 ++++++++++++++++++ .../service/AihrMobileSeedServiceTest.java | 24 ++++ .../service/AihrPracticeSeedServiceTest.java | 5 + docs/API_INTEGRATION.md | 4 +- docs/BRD_IMPLEMENTATION_AUDIT.md | 1 + mobile-uni/src/services/home.ts | 3 +- scripts/demo-check.sh | 6 + 9 files changed, 254 insertions(+), 19 deletions(-) diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrMobileController.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrMobileController.java index 5f313e5c..27090845 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrMobileController.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrMobileController.java @@ -63,7 +63,17 @@ public class AihrMobileController { @SaIgnore @GetMapping("/home/{role}") public R home(@PathVariable String role) { - return R.ok(mobileSeedService.home(role)); + if (!LoginHelper.isLogin()) { + return R.ok(mobileSeedService.publicHome(role)); + } + LoginUser loginUser = LoginHelper.getLoginUser(); + String identity = currentAppUsername(); + if (loginUser != null + && UserType.APP_USER.getUserType().equals(loginUser.getUserType()) + && supervisorRoleRequested(role)) { + mobileSeedService.requireSupervisorIdentity(identity); + } + return R.ok(mobileSeedService.home(role, identity)); } /** @@ -226,6 +236,16 @@ public class AihrMobileController { return username == null ? "" : username.trim(); } + private static boolean supervisorRoleRequested(String role) { + if (role == null) { + return false; + } + return switch (role.toLowerCase()) { + case "supervisor", "manager", "admin" -> true; + default -> false; + }; + } + private static Map reviewDetailPayload(ReviewDetailResponse detail) { Map data = new LinkedHashMap<>(); data.put("id", detail.id()); diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrMobileSeedService.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrMobileSeedService.java index 0343ee4d..e4a908fe 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrMobileSeedService.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrMobileSeedService.java @@ -41,13 +41,43 @@ public class AihrMobileSeedService { } public HomeResponse home(String role) { + return home(role, ""); + } + + public HomeResponse home(String role, String mobileIdentity) { return switch (normalize(role)) { case "candidate" -> candidateHome(); - case "supervisor" -> supervisorHome(); - default -> userHome(); + case "supervisor" -> supervisorHomeForIdentity(mobileIdentity); + default -> userHome(mobileIdentity); }; } + /** + * Return a public first-screen seed without reading tenant business metrics. + * The authenticated home path remains available through {@link #home(String)}. + */ + public HomeResponse publicHome(String role) { + return switch (normalize(role)) { + case "candidate" -> candidateHome(); + case "supervisor" -> supervisorHome(false, ""); + default -> workerHome("user", "员工端", "avatar-woman", "早上好,王敏", "星河湾一期 · 客服管家", false, ""); + }; + } + + private HomeResponse supervisorHomeForIdentity(String supervisorIdentity) { + if (supervisorIdentity == null || supervisorIdentity.isBlank()) { + return supervisorHome(true, new AihrPracticeSeedService.MobileTeamMetrics( + practiceSeedService.mobileCompletedPeopleCount(), + practiceSeedService.mobilePilotPeopleCount(), + practiceSeedService.mobilePendingReviewCount(), + practiceSeedService.mobilePendingAssignmentCount(), + practiceSeedService.mobileLowScoreCount(), + practiceSeedService.mobileAverageScore() + )); + } + return supervisorHome(true, practiceSeedService.mobileTeamMetrics(supervisorIdentity)); + } + public List practiceHistory(String extPartyId) { return practiceSeedService.mobileHistory(extPartyId, 5); } @@ -159,8 +189,8 @@ public class AihrMobileSeedService { }; } - private HomeResponse userHome() { - return workerHome("user", "员工端", "avatar-woman", "早上好,王敏", "星河湾一期 · 客服管家"); + private HomeResponse userHome(String mobileIdentity) { + return workerHome("user", "员工端", "avatar-woman", "早上好,王敏", "星河湾一期 · 客服管家", true, mobileIdentity); } private static HomeResponse candidateHome() { @@ -208,10 +238,18 @@ public class AihrMobileSeedService { ); } - private HomeResponse workerHome(String role, String roleLabel, String avatarTone, String greeting, String location) { - int completed = practiceSeedService.mobileCompletedCount(); - int pendingReview = practiceSeedService.mobilePendingReviewCount(); - int averageScore = practiceSeedService.mobileAverageScore(); + private HomeResponse workerHome(String role, String roleLabel, String avatarTone, String greeting, String location, + boolean includeLiveMetrics, String mobileIdentity) { + boolean scoped = mobileIdentity != null && !mobileIdentity.isBlank(); + int completed = includeLiveMetrics + ? scoped ? practiceSeedService.mobileCompletedCount(mobileIdentity) : practiceSeedService.mobileCompletedCount() + : 0; + int pendingReview = includeLiveMetrics + ? scoped ? practiceSeedService.mobilePendingReviewCount(mobileIdentity) : practiceSeedService.mobilePendingReviewCount() + : 0; + int averageScore = includeLiveMetrics + ? scoped ? practiceSeedService.mobileAverageScore(mobileIdentity) : practiceSeedService.mobileAverageScore() + : 0; return new HomeResponse( role, roleLabel, @@ -256,14 +294,21 @@ public class AihrMobileSeedService { ); } - private HomeResponse supervisorHome() { - int completed = practiceSeedService.mobileCompletedPeopleCount(); - int total = practiceSeedService.mobilePilotPeopleCount(); + private HomeResponse supervisorHome(boolean includeLiveMetrics, String supervisorIdentity) { + AihrPracticeSeedService.MobileTeamMetrics metrics = includeLiveMetrics + ? practiceSeedService.mobileTeamMetrics(supervisorIdentity) + : AihrPracticeSeedService.MobileTeamMetrics.zero(); + return supervisorHome(includeLiveMetrics, metrics); + } + + private HomeResponse supervisorHome(boolean includeLiveMetrics, AihrPracticeSeedService.MobileTeamMetrics metrics) { + int completed = metrics.completedPeople(); + int total = metrics.pilotPeople(); int pending = Math.max(0, total - completed); - int pendingReview = practiceSeedService.mobilePendingReviewCount(); - int pendingAssignment = practiceSeedService.mobilePendingAssignmentCount(); - int lowScore = practiceSeedService.mobileLowScoreCount(); - int averageScore = practiceSeedService.mobileAverageScore(); + int pendingReview = metrics.pendingReview(); + int pendingAssignment = metrics.pendingAssignment(); + int lowScore = metrics.lowScore(); + int averageScore = metrics.averageScore(); int completionRate = total == 0 ? 0 : Math.round(completed * 100F / total); return new HomeResponse( "supervisor", diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java index e356c0b5..b650e871 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java @@ -694,10 +694,18 @@ public class AihrPracticeSeedService { return countMobileRecords(""); } + public int mobileCompletedCount(String extPartyId) { + return countMobileRecords(extPartyId); + } + public int mobilePendingReviewCount() { return countPendingReview(""); } + public int mobilePendingReviewCount(String extPartyId) { + return countPendingReview(extPartyId); + } + public int mobileCompletedPeopleCount() { return countCompletedPilotPeople(); } @@ -735,6 +743,59 @@ public class AihrPracticeSeedService { return value == null ? 0 : Math.round(value.floatValue()); } + public int mobileAverageScore(String extPartyId) { + if (isBlank(extPartyId)) { + return mobileAverageScore(); + } + ensurePracticeTable(); + Double value = jdbcTemplate.queryForObject(""" + SELECT COALESCE(AVG(total_score), 0) + FROM aihr_practice_session + WHERE tenant_id = ? AND mode = 'mobile' AND ext_party_id = ? + """, Double.class, tenantId(), extPartyId.trim()); + return value == null ? 0 : Math.round(value.floatValue()); + } + + public MobileTeamMetrics mobileTeamMetrics(String supervisorExtPartyId) { + TeamScope scope = teamScope(supervisorExtPartyId); + if (!scope.scoped()) { + return new MobileTeamMetrics( + mobileCompletedPeopleCount(), + mobilePilotPeopleCount(), + mobilePendingReviewCount(), + mobilePendingAssignmentCount(), + mobileLowScoreCount(), + mobileAverageScore() + ); + } + if (scope.extPartyIds().isEmpty()) { + return MobileTeamMetrics.zero(); + } + ensurePracticeTable(); + ensureAssignmentTable(); + return new MobileTeamMetrics( + countCompletedPilotPeople(scope), + countPilotPeople(scope), + countPendingReview(scope), + countPendingAssignment(scope), + countLowScore(scope), + averageScore(scope) + ); + } + + public record MobileTeamMetrics( + int completedPeople, + int pilotPeople, + int pendingReview, + int pendingAssignment, + int lowScore, + int averageScore + ) { + public static MobileTeamMetrics zero() { + return new MobileTeamMetrics(0, 0, 0, 0, 0, 0); + } + } + public List mobileHistory(String extPartyId, int limit) { ensurePracticeTable(); int safeLimit = normalizeLimit(limit); @@ -1197,6 +1258,23 @@ public class AihrPracticeSeedService { """, tenantId()); } + private int countCompletedPilotPeople(TeamScope scope) { + List args = new ArrayList<>(); + args.add(tenantId()); + args.addAll(scope.extPartyIds()); + return count(""" + SELECT COUNT(*) + FROM ( + SELECT ext_party_id + FROM aihr_practice_session + WHERE tenant_id = ? AND mode = 'mobile' + AND ext_party_id IN (%s) + GROUP BY ext_party_id + HAVING COUNT(*) >= 10 + ) completed_people + """.formatted(inClause(scope.extPartyIds().size())), args.toArray()); + } + private int countPilotPeople() { ensurePracticeTable(); ensureAssignmentTable(); @@ -1216,6 +1294,29 @@ public class AihrPracticeSeedService { """, tenantId(), tenantId()); } + private int countPilotPeople(TeamScope scope) { + List args = new ArrayList<>(); + args.add(tenantId()); + args.addAll(scope.extPartyIds()); + args.add(tenantId()); + args.addAll(scope.extPartyIds()); + return count(""" + SELECT COUNT(*) + FROM ( + SELECT ext_party_id + FROM aihr_practice_session + WHERE tenant_id = ? AND mode = 'mobile' + AND ext_party_id IN (%s) + UNION + SELECT ext_party_id + FROM aihr_practice_assignment + WHERE tenant_id = ? AND ext_party_id IN (%s) + AND status = '待训练' + AND (source <> 'daily' OR DATE(create_time) = CURRENT_DATE()) + ) pilot_people + """.formatted(inClause(scope.extPartyIds().size()), inClause(scope.extPartyIds().size())), args.toArray()); + } + public ReviewDetailResponse reviewDetail(Long id) { ensurePracticeTable(); if (id == null) { @@ -2072,6 +2173,22 @@ public class AihrPracticeSeedService { """.formatted(inClause(scope.extPartyIds().size())), args.toArray()); } + private int countPendingAssignment(TeamScope scope) { + if (scope.extPartyIds().isEmpty()) { + return 0; + } + List args = new ArrayList<>(); + args.add(tenantId()); + args.addAll(scope.extPartyIds()); + return count(""" + SELECT COUNT(*) + FROM aihr_practice_assignment + WHERE tenant_id = ? AND status = '待训练' + AND (source <> 'daily' OR DATE(create_time) = CURRENT_DATE()) + AND ext_party_id IN (%s) + """.formatted(inClause(scope.extPartyIds().size())), args.toArray()); + } + private int countLowScore(TeamScope scope) { if (scope.extPartyIds().isEmpty()) { return 0; @@ -2087,6 +2204,22 @@ public class AihrPracticeSeedService { """.formatted(inClause(scope.extPartyIds().size())), args.toArray()); } + private int averageScore(TeamScope scope) { + if (scope.extPartyIds().isEmpty()) { + return 0; + } + List args = new ArrayList<>(); + args.add(tenantId()); + args.addAll(scope.extPartyIds()); + Double value = jdbcTemplate.queryForObject(""" + SELECT COALESCE(AVG(total_score), 0) + FROM aihr_practice_session + WHERE tenant_id = ? AND mode = 'mobile' + AND ext_party_id IN (%s) + """.formatted(inClause(scope.extPartyIds().size())), Double.class, args.toArray()); + return value == null ? 0 : Math.round(value.floatValue()); + } + private boolean inTeamScope(String supervisorExtPartyId, String extPartyId) { if (isBlank(extPartyId)) { return false; diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrMobileSeedServiceTest.java b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrMobileSeedServiceTest.java index 57e4f307..9fee3709 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrMobileSeedServiceTest.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrMobileSeedServiceTest.java @@ -8,6 +8,7 @@ import java.util.stream.Collectors; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; @Tag("dev") @@ -38,5 +39,28 @@ public class AihrMobileSeedServiceTest { assertEquals(3, supervisor.todos().get(0).count()); assertEquals(2, supervisor.todos().get(1).count()); assertEquals("1", supervisor.team().stats().get(2).value()); + + AihrPracticeSeedService publicPractice = mock(AihrPracticeSeedService.class); + AihrMobileSeedService publicService = new AihrMobileSeedService(publicPractice); + HomeResponse publicUser = publicService.publicHome("user"); + HomeResponse publicSupervisor = publicService.publicHome("supervisor"); + assertEquals("0", publicUser.metrics().get(0).value()); + assertEquals("0人已完成 · 0人待跟进", publicSupervisor.team().progressText()); + assertEquals(0, publicSupervisor.todos().get(0).count()); + verifyNoInteractions(publicPractice); + + when(practice.mobileCompletedCount("employee-1")).thenReturn(2); + when(practice.mobilePendingReviewCount("employee-1")).thenReturn(1); + when(practice.mobileAverageScore("employee-1")).thenReturn(88); + when(practice.mobileTeamMetrics("supervisor-1")) + .thenReturn(new AihrPracticeSeedService.MobileTeamMetrics(3, 4, 2, 1, 1, 79)); + HomeResponse scopedUser = service.home("user", "employee-1"); + HomeResponse scopedSupervisor = service.home("supervisor", "supervisor-1"); + assertEquals("2", scopedUser.metrics().get(0).value()); + assertEquals("88", scopedUser.metrics().get(1).value()); + assertEquals("1", scopedUser.metrics().get(2).value()); + assertEquals("3人已完成 · 1人待跟进", scopedSupervisor.team().progressText()); + assertEquals(2, scopedSupervisor.todos().get(0).count()); + assertEquals(1, scopedSupervisor.todos().get(1).count()); } } diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrPracticeSeedServiceTest.java b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrPracticeSeedServiceTest.java index b7652b16..c4d2dded 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrPracticeSeedServiceTest.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrPracticeSeedServiceTest.java @@ -401,6 +401,11 @@ public class AihrPracticeSeedServiceTest { assertTrue(controllerSource.contains("mobileSeedService.requireSupervisorIdentity(currentAppUsername())")); assertTrue(controllerSource.contains("@SaCheckLogin\npublic class AihrMobileController")); assertTrue(controllerSource.contains("@SaIgnore\n @GetMapping(\"/home/{role}\")")); + assertTrue(controllerSource.contains("if (!LoginHelper.isLogin())")); + assertTrue(controllerSource.contains("return R.ok(mobileSeedService.publicHome(role))")); + assertTrue(controllerSource.contains("return R.ok(mobileSeedService.home(role, identity))")); + assertTrue(controllerSource.contains("mobileSeedService.requireSupervisorIdentity(identity)")); + assertTrue(controllerSource.contains("private static boolean supervisorRoleRequested(String role)")); assertTrue(controllerSource.contains("UserType.SYS_USER.getUserType().equals(loginUser.getUserType())")); assertTrue(controllerSource.contains("StpUtil.hasRoleOr(TenantConstants.SUPER_ADMIN_ROLE_KEY, \"hr_operator\")")); assertFalse(controllerSource.contains("private static String supervisorScopeExtPartyId()")); diff --git a/docs/API_INTEGRATION.md b/docs/API_INTEGRATION.md index 6f10cf74..158182e0 100644 --- a/docs/API_INTEGRATION.md +++ b/docs/API_INTEGRATION.md @@ -16,7 +16,7 @@ | 资料处理 `/knowledge/processing` | `GET /api/knowledge/processing/overview`、`POST /api/knowledge/doc/upload-async`、`GET /api/knowledge/doc/upload-items`、`POST /api/knowledge/doc/upload-items/{id}/retry`、`POST /api/knowledge/doc/import-local-task`、`GET /api/knowledge/doc/import-tasks`、`POST /api/knowledge/doc/import-tasks/{id}/cancel` | 已接入解析任务状态聚合;**批量上传走异步队列**:接口只暂存+入队即秒回,后台 worker(并发 2)逐条解析/归类/向量化;ZIP 在 worker 内安全解压后把支持的子文件继续入同一批次队列,页面按批次轮询进度、失败可单文件重试;服务端目录导入、进度轮询和任务取消保留,失败回退 seed | | 组织人员同步 | `POST /api/aihr/org/sync` | 从开放组织同步系统的 `/api/open/v1/sync/snapshot` 拉取 `company/department/employee` 快照,分页参数使用 `limit`;员工手机号只落 `person_phone` 用于移动端身份映射,不在组织列表响应暴露;岗位识别 `position/job_title/post/job_name/role/title` 等字段。默认 `replaceExisting=true`,写入前必须先用 `{"dryRun":true}`;dry-run 不访问本地快照表、不执行 DDL/写库,返回 `phoneLinked/maskedPhone/suspectText/warnings` 且 `syncedCount=0`。非 dry-run 覆盖写入遇到员工被跳过、手机号不完整、脱敏手机号或疑似乱码时默认拒绝,只有确认 dry-run 结果后显式传 `allowPartialReplace=true` 才允许覆盖;重复 `ext_party_id` 始终拒绝写入,因为数据库唯一键会折叠重复身份;`replaceExisting=false` 不触发不完整快照覆盖闸门,但仍拒绝重复身份。2026-07-10 源接口实测 3474 人仅 1 个可用手机号、3473 个脱敏手机号、8 条疑似乱码,因此未执行覆盖同步,正式试点需上游先开放至少 20 名试点人员手机号 | | 移动端手机号登录 | `GET /resource/sms/code`、`POST /auth/mobile/sms-login` | 已复用 sms4j 阿里云配置 `config1` 和 RuoYi `sms` 授权策略;短信发送成功后才写 Redis 验证码;手机号不存在时自动注册 `app_user`;`aihr.sms.dev-fixed-code` 非空时不真发短信、验证码固定(dev 默认 `123456`,prod profile 代码级强制失效) | -| 用户侧三端首页 `mobile-uni` hash 路由;旧 `/h5/user`、`/h5/candidate`、`/h5/supervisor` 兼容重定向 | `GET /api/aihr/mobile/home/{role}` | 已接入员工、候选人、主管首页公开只读 API;移动端本地 fallback 保演示 | +| 用户侧三端首页 `mobile-uni` hash 路由;旧 `/h5/user`、`/h5/candidate`、`/h5/supervisor` 兼容重定向 | `GET /api/aihr/mobile/home/{role}` | 未登录请求只返回不读取租户业务统计的公开首屏 seed;已登录移动端请求自动携带 `Authorization/clientid`,才返回员工/主管真实统计;移动端本地 fallback 保演示 | | 移动端登录后角色识别 | `GET /api/aihr/mobile/me` | 认证后按手机号匹配组织快照;`position_level` 为“主管/项目经理”时进入主管端,否则进入员工端;接口失败回退员工端 | | 移动端员工训练与主管复盘闭环 | 员工复用 `POST /api/train/practice/start`、`/turn`、`/finish`,查询 `GET /api/aihr/mobile/practice/history`、`/practice/mistakes`、`/profile`;训练完成后提交 `POST /api/aihr/mobile/practice/satisfaction`;主管查询 `GET /api/aihr/mobile/practice/team`、`/practice/alerts`、`/practice/reviews`、`/practice/reviews/{id}`,标记 `POST /api/aihr/mobile/practice/reviews/{id}/reviewed`,指派 `POST /api/aihr/mobile/practice/assignments` | 员工端登录后带 `Authorization` 与 `clientid` 调用;`mode=mobile` 完成后写入 `aihr_practice_session`。满意度接口只接受本人已完成训练的 1-5 分,意见脱敏后落库,未填写不补默认值。错题本按员工本人聚合低分/红线回合,并关联已有 `retry` assignment,不伪造错题结论。主管 `/practice/team` 在同一项目权限范围内额外返回 `mistakes` 聚合,按场景/归因统计次数、影响人数、平均分和最近发生时间;普通员工返回“无主管权限”。主管接口以后端当前登录手机号映射在职组织快照,仅允许岗位为“主管/项目经理”的账号,并按租户和项目范围返回真实成员、全状态训练记录及非 daily 专项。复盘标记只允许首次 `待复盘 -> 已复盘` 创建后续专项,并发重复提交幂等;可带 `incentivePoint` 写入贡献度 | | 移动端候选人闭环 | 页面拆为 `/pages/candidate/index/index`、`/interview/index`、`/materials/index`、`/progress/index`、`/study/index`;面试复用 `POST /api/recruit/interview/start`、`/answer`、`/finish` 和 `GET /records`;资料 `POST/GET /api/aihr/mobile/candidate/materials`;预习 `POST /api/knowledge/search`;HR 审核 `GET /api/aihr/hr/candidate/materials`、`POST /api/aihr/hr/candidate/materials/{id}/review` | 候选人端登录后带 `Authorization` 与 `clientid` 调用;首页只按当前手机号对应的真实面试记录和最新资料状态分流,不读取公开 home seed。APP 候选人身份以后端登录手机号为准,前端 `candidateId/candidateName` 只作非 APP 场景兼容参数;`/answer` 与 `/finish` 还会校验当前 APP 手机号与启动会话的候选人 ID 一致,未知或他人会话直接拒绝;面试拉题/评分走真实模型优先;资料写 `sys_oss`/MinIO 和 `aihr_candidate_material`,HR 审核后进度页同步三态;岗前预习查询正式 SOP,不兜前端示例答案 | @@ -31,7 +31,7 @@ 当前管理端 AI 面试和案例沉淀已改为真实模型/ASR 优先;移动端首页和部分演示态数据仍保留 fallback。知识库、模型能力、文档解析、RAG、chat 按 [ruoyi-ai 能力分片迁移计划](RUOYI_AI_INCREMENTAL_MIGRATION.md) 逐片引入;知识库 DDL 与住宅类 SOP seed 在 `backend/script/sql/aihr_knowledge_mysql8.sql`,模型 DDL 在 `backend/script/sql/aihr_model_mysql8.sql`,训练记录 DDL 在 `backend/script/sql/aihr_practice_mysql8.sql`,AI 面试结果 DDL 在 `backend/script/sql/aihr_interview_result_mysql8.sql`,候选人资料 DDL 在 `backend/script/sql/aihr_candidate_material_mysql8.sql`,组织人员快照表在 `backend/script/sql/aihr_org_snapshot_mysql8.sql`,本地 reset 带 2 个住宅项目 22 人 seed;配置开放组织同步系统后用 `POST /api/aihr/org/sync` 覆盖为外部快照。 -直接打后端 `/api/**` 通常需要登录后的 `Authorization: Bearer `;浏览器内通过已登录前端和 `/dev-api` 代理访问。移动端登录接口为 `POST /auth/mobile/sms-login`,请求 `{ phonenumber, smsCode, tenantId }`,内部固定使用 app 客户端 `428a8310cd442757ae699df5d894f051` 和 `sms` grant;验证码通过后若手机号不存在,会创建 `app_user`,用户名为手机号,备注为“移动端短信自动注册”。移动端 MVP 首页接口 `GET /api/aihr/mobile/home/{role}` 目前仍是 `@SaIgnore` 的公开只读 seed 接口,避免 H5 首屏被后台管理登录态阻断;后续接小程序登录后再收紧为移动端 token。 +直接打后端 `/api/**` 通常需要登录后的 `Authorization: Bearer `;浏览器内通过已登录前端和 `/dev-api` 代理访问。移动端登录接口为 `POST /auth/mobile/sms-login`,请求 `{ phonenumber, smsCode, tenantId }`,内部固定使用 app 客户端 `428a8310cd442757ae699df5d894f051` 和 `sms` grant;验证码通过后若手机号不存在,会创建 `app_user`,用户名为手机号,备注为“移动端短信自动注册”。移动端首页接口 `GET /api/aihr/mobile/home/{role}` 仍保留 `@SaIgnore` 以保证未登录首屏可用,但匿名分支只返回不读取业务统计的公开 seed;登录后的 `mobile-uni` 会自动携带移动端 token,服务端才返回真实员工/主管统计。 阿里云短信复用 RuoYi 的 `sms.blends.config1`。本地开发把真实短信参数放根目录 `.env.local` 或外部环境变量,`scripts/dev-backend.sh` 会自动加载;`application-dev.yml` / `application-prod.yml` 只保留占位,不写真实密钥。 diff --git a/docs/BRD_IMPLEMENTATION_AUDIT.md b/docs/BRD_IMPLEMENTATION_AUDIT.md index 892189bb..5360836d 100644 --- a/docs/BRD_IMPLEMENTATION_AUDIT.md +++ b/docs/BRD_IMPLEMENTATION_AUDIT.md @@ -167,3 +167,4 @@ - 2026-07-14 BRD G3 对练会话归属复核:发现三角色对练的 `start` 虽已绑定 APP 手机号,但 `turn/finish` 原先可用任意会话 ID继续提交,未知会话还会回退场景并落成训练记录;现由控制器把当前 APP 手机号传入服务,移动端回合与完成操作必须匹配启动会话的 `ext_party_id`,外部/未知会话统一拒绝;后台管理端调用保留原有演示兼容行为。新增越权/未知会话回归测试与 `demo-check` marker,未修改生产环境。 - 2026-07-14 BRD G3 主管身份多记录复核:同一手机号可能对应多条在职组织记录,主管身份检查和团队范围解析现从全部匹配记录中选择“主管/项目经理”,不再因第一条记录是一线岗位而误拒主管或选错项目范围;新增多岗位手机号回归测试,未修改生产环境。 - 2026-07-14 BRD G3 移动端角色降级复核:`/api/aihr/mobile/me` 组织快照查询异常时原先会直接返回 500,与“组织身份不可用时安全降级员工端”的口径不一致;现捕获组织查询运行时异常并返回员工端默认身份,同时仅记录固定错误提示,不泄露外部/数据库错误详情;新增源码契约 marker,未修改生产环境。 +- 2026-07-14 BRD G3 移动端公开首页复核:`/api/aihr/mobile/home/{role}` 保留未登录首屏能力,但匿名请求此前会读取员工训练统计和主管团队完训/低分/待复盘数据;现改为匿名只返回无租户业务统计的静态 seed,已登录 `mobile-uni` 自动携带 `Authorization/clientid` 后,员工只读取本人统计、主管只读取组织快照解析出的项目范围统计,普通员工请求主管角色直接拒绝;已补 service、controller、前端请求、源码契约和真实 HTTP 回归测试;未修改生产环境。 diff --git a/mobile-uni/src/services/home.ts b/mobile-uni/src/services/home.ts index 3f04acfb..753c12a8 100644 --- a/mobile-uni/src/services/home.ts +++ b/mobile-uni/src/services/home.ts @@ -1,10 +1,11 @@ import type { RoleHome, RoleKey } from '@/types/api'; import { apiRequest } from './api'; +import { isLoggedIn } from './auth'; export const getMobileHome = (role: RoleKey) => apiRequest({ url: `/api/aihr/mobile/home/${role}`, method: 'GET', - auth: false, + auth: isLoggedIn(), timeout: 15000 }); diff --git a/scripts/demo-check.sh b/scripts/demo-check.sh index 7399a224..6bbed633 100755 --- a/scripts/demo-check.sh +++ b/scripts/demo-check.sh @@ -332,6 +332,12 @@ contains mobile-uni/src/pages.json "pages/candidate/study/index" contains mobile-uni/src/pages/auth/login/index.vue "/resource/sms/code" contains mobile-uni/src/pages/auth/login/index.vue "/auth/mobile/sms-login" contains mobile-uni/src/services/home.ts "/api/aihr/mobile/home/" +contains mobile-uni/src/services/home.ts "auth: isLoggedIn()" +contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrMobileSeedService.java "public HomeResponse publicHome(String role)" +contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrMobileController.java "return R.ok(mobileSeedService.publicHome(role))" +contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrMobileController.java "mobileSeedService.home(role, identity)" +contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrMobileController.java "supervisorRoleRequested" +contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java "public MobileTeamMetrics mobileTeamMetrics(String supervisorExtPartyId)" contains mobile-uni/src/services/practice.ts "/api/train/practice/start" contains mobile-uni/src/services/practice.ts "/api/aihr/mobile/practice/history" contains mobile-uni/src/services/practice.ts "/api/aihr/mobile/profile/promotion-evidence"