diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrPracticeDto.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrPracticeDto.java index f82792c6..4a292829 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrPracticeDto.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrPracticeDto.java @@ -162,6 +162,17 @@ public final class AihrPracticeDto { ) { } + public record MistakeAggregateResponse( + String scenarioId, + String scenarioName, + String cause, + Integer count, + Integer affectedPeople, + Integer averageScore, + String latestTime + ) { + } + public record DialogueResponse(String role, String label, String text, String audioUrl, Long audioOssId, Integer emotion, Integer trust, Boolean redFlag, String coachHint) { public DialogueResponse(String role, String label, String text) { this(role, label, text, "", null, null, null, null, ""); @@ -260,7 +271,8 @@ public final class AihrPracticeDto { public record PracticeTeamResponse( List members, List records, - List assignments + List assignments, + List mistakes ) { } 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 fca1354d..4fd694e8 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 @@ -26,6 +26,7 @@ import org.dromara.aihr.domain.AihrPracticeDto.PromptTemplateResponse; import org.dromara.aihr.domain.AihrPracticeDto.PromotionEvidenceResponse; import org.dromara.aihr.domain.AihrPracticeDto.RecordResponse; import org.dromara.aihr.domain.AihrPracticeDto.MistakeResponse; +import org.dromara.aihr.domain.AihrPracticeDto.MistakeAggregateResponse; import org.dromara.aihr.domain.AihrPracticeDto.ReviewAdviceRequest; import org.dromara.aihr.domain.AihrPracticeDto.ReviewDetailResponse; import org.dromara.aihr.domain.AihrPracticeDto.RubricDimensionResponse; @@ -799,12 +800,13 @@ public class AihrPracticeSeedService { TeamScope scope = teamScope(supervisorExtPartyId); int safeLimit = normalizeTeamLimit(limit); if (scope.scoped() && scope.extPartyIds().isEmpty()) { - return new PracticeTeamResponse(List.of(), List.of(), List.of()); + return new PracticeTeamResponse(List.of(), List.of(), List.of(), List.of()); } return new PracticeTeamResponse( practiceTeamMembers(scope), practiceTeamRecords(scope, safeLimit), - practiceTeamAssignments(scope, safeLimit) + practiceTeamAssignments(scope, safeLimit), + practiceTeamMistakes(scope, safeLimit) ); } @@ -920,6 +922,57 @@ public class AihrPracticeSeedService { return jdbcTemplate.query(sql, this::mapAssignment, args.toArray()); } + private List practiceTeamMistakes(TeamScope scope, int limit) { + ensurePracticeTable(); + List args = new ArrayList<>(); + args.add(TENANT_ID); + StringBuilder where = new StringBuilder("WHERE tenant_id = ? AND mode = 'mobile' AND finished_time IS NOT NULL"); + if (scope.scoped()) { + where.append(" AND ext_party_id IN (").append(inClause(scope.extPartyIds().size())).append(")"); + args.addAll(scope.extPartyIds()); + } + args.add(limit); + String sql = """ + SELECT scenario_id, + scenario_name, + cause, + COUNT(*) AS mistake_count, + COUNT(DISTINCT NULLIF(ext_party_id, '')) AS affected_people, + ROUND(AVG(COALESCE(total_score, 0))) AS average_score, + MAX(finished_time) AS latest_time + FROM ( + SELECT scenario_id, + COALESCE(NULLIF(scenario_name, ''), '场景训练') AS scenario_name, + ext_party_id, + total_score, + finished_time, + CASE + WHEN dialogue_json LIKE '%%"redFlag":true%%' THEN '红线话术' + WHEN COALESCE(dim_emotion, 100) < 60 THEN '情绪承接' + WHEN COALESCE(dim_compliance, 100) < 60 THEN '流程合规' + ELSE '综合得分不足' + END AS cause + FROM aihr_practice_session + %s + AND (total_score < 80 OR dialogue_json LIKE '%%"redFlag":true%%') + ) mistake_rows + GROUP BY scenario_id, scenario_name, cause + ORDER BY mistake_count DESC, latest_time DESC, scenario_name, cause + LIMIT ? + """.formatted(where); + return jdbcTemplate.query(sql, (rs, rowNum) -> new MistakeAggregateResponse( + rs.getString("scenario_id"), + firstNonBlank(rs.getString("scenario_name"), "场景训练"), + firstNonBlank(rs.getString("cause"), "综合得分不足"), + rs.getInt("mistake_count"), + rs.getInt("affected_people"), + rs.getInt("average_score"), + rs.getTimestamp("latest_time") == null + ? "" + : rs.getTimestamp("latest_time").toLocalDateTime().format(TIME_FORMATTER) + ), args.toArray()); + } + public List pendingReviewRecords(int limit) { return pendingReviewRecords("", "", "", limit); } diff --git a/docs/API_INTEGRATION.md b/docs/API_INTEGRATION.md index e68be582..aa37f922 100644 --- a/docs/API_INTEGRATION.md +++ b/docs/API_INTEGRATION.md @@ -17,7 +17,7 @@ | 移动端手机号登录 | `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 保演示 | | 移动端登录后角色识别 | `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,不伪造错题结论。主管接口以后端当前登录手机号映射在职组织快照,仅允许岗位为“主管/项目经理”的账号,并按租户和项目范围返回真实成员、全状态训练记录及非 daily 专项;普通员工返回“无主管权限”。复盘标记只允许首次 `待复盘 -> 已复盘` 创建后续专项,并发重复提交幂等;可带 `incentivePoint` 写入贡献度 | +| 移动端员工训练与主管复盘闭环 | 员工复用 `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 场景兼容参数;面试拉题/评分走真实模型优先;资料写 `sys_oss`/MinIO 和 `aihr_candidate_material`,HR 审核后进度页同步三态;岗前预习查询正式 SOP,不兜前端示例答案 | ## 后端落点 diff --git a/docs/BRD_IMPLEMENTATION_AUDIT.md b/docs/BRD_IMPLEMENTATION_AUDIT.md index 9e419ad4..0db03402 100644 --- a/docs/BRD_IMPLEMENTATION_AUDIT.md +++ b/docs/BRD_IMPLEMENTATION_AUDIT.md @@ -36,7 +36,7 @@ |---|---|---|---| | 岗位-SOP匹配与上岗资格 gating | 3.1、4.2、TechSpec P1 | 已补 `GET /api/knowledge/position-sop` 和员工学习页状态提示:一期只确认生活顾问内置学习导航;尚无 `sop_applicability`、`onboard_task`、`qualification_gate` 正式数据闭环,不能作为上岗资格判断 | 先接正式岗位/项目与 SOP 适用范围数据,再补培训任务状态;资格 gating 必须由 HR 明确规则和证据来源后实现 | | 认证等级与激励规则可配置 | 4.5、10.2-A、TechSpec M6 | 成长激励页已有初/中/高级证据包和学分口径展示,但规则仍是前端硬编码;原“排行榜”开关无实际业务作用,已移除并明确标记只读;尚无 `cert_rule`/`incentive_rule` 的持久化、版本、角色权限和生效记录 | 先由 HR 确认学分来源、分值、防刷和初/中/高级条件,再设计规则表与管理员 CRUD;在规则契约明确前,不把静态展示称为可配置 | -| 智能错题本 | 3.1、4.4、TechSpec P1 | 已补 `/api/aihr/mobile/practice/mistakes` 和员工“学”页错题本:按本人聚合低分/红线回合,展示原因、分数、再练状态,支持按现有归因筛选,并可跳已有 retry assignment;尚未做独立错题表和主管侧聚合 | 后续复用现有 session/assignment/annotation 补复盘详情关联和主管侧聚合,不新增独立错题事实表 | +| 智能错题本 | 3.1、4.4、TechSpec P1 | 已补员工本人错题本和主管 `/api/aihr/mobile/practice/team` 内的 `mistakes` 聚合:按当前项目权限统计场景、归因、次数、影响人数、平均分和最近发生时间,员工端仍可跳已有 retry assignment;不新增独立错题事实表 | 继续用正式组织快照和试点窗口验证主管只看到本项目数据;后续可在真实错题量上来后补趋势/导出,不把本地演示样本当正式指标 | | 案例学习/视频详情 | 3.1、4.6、矩阵第35行 | `AihrCaseService` 当前用内存 `CaseState` + `aihr_case_record` 保存转写/摘要,没有稳定 media URL、学习点、主管点评和案例详情 API;仓库也没有可用于一期验收的预渲染样片资产 | 先由业务提供样片或正式媒体 URL,并确定案例详情、学习点、主管点评字段,再扩持久化契约和权限;没有真实媒体 URL 时不造播放按钮 | | 案例库质量与数量 | L1-B、4.6/4.7 | 上传/整理/筛选路径已有;“至少20条脱敏案例”和每周每项目一条需要正式数据证明,当前也没有可用于一期样片验收的真实视频资产 | 先按项目窗口采集并脱敏检查,再补案例统计/学习详情导出;不使用空壳视频或演示 seed 冒充 | | RAG 可用率与知识治理 | 4.7、L1-C、G3 | 搜索、引用、反馈、知识缺口和人工评审入口已有;本轮已让员工反馈保存同一次搜索的 `reviewId`,但尚未保存模型/提示词答案版本;住宅核心流程覆盖率与可用率仍需正式评审数据,不能宣称完成逐问答全链路追溯 | 下一步由业务确认答案版本口径,再把 `prompt_version`/检索批次纳入评审批次;先用当前 `reviewId + query + fragment` 做可追溯统计,不伪造模型版本 | diff --git a/docs/FIGMA_IMPLEMENTATION_MATRIX.md b/docs/FIGMA_IMPLEMENTATION_MATRIX.md index a68b4c3d..78bd5862 100644 --- a/docs/FIGMA_IMPLEMENTATION_MATRIX.md +++ b/docs/FIGMA_IMPLEMENTATION_MATRIX.md @@ -96,7 +96,7 @@ | 44 | 结论/行动项结果 | `87:815` | — | 未实现 | 未修正,阶段二 | | 45 | 摘要/可编辑文字生成 | `87:865` | — | 未实现 | 未修正,阶段二 | | 46 | 员工工作量化看板 | `89:9` | `frontend/src/views/competency/growth.vue` | 按二期计划已实现;按 Figma 部分对齐 | 已有训练时长、贡献度、测评分、等级、证据包、预警和派发;完整三期量化指标暂不补 | -| 47 | 主管团队量化看板 | `89:776` | `mobile-uni/src/pages/supervisor/team/index.vue` | 按二期计划已实现;按 Figma 部分对齐 | 已有团队成员、风险、训练记录和成长证据;完整团队经营指标需正式组织数据 | +| 47 | 主管团队量化看板 | `89:776` | `mobile-uni/src/pages/supervisor/team/index.vue` | 按二期计划已实现;按 Figma 部分对齐 | 已有团队成员、风险、训练记录、成长证据和按项目范围聚合的团队错题本;完整团队经营指标仍需正式组织数据 | | 48 | HR/运营绩效看板 | `90:2` | — | 未实现 | 未修正,三期 | | 49 | 工作积分排行榜 | `90:91` | — | 未实现 | 未修正,三期 | | 50 | 等级/晋升条件/进度 | `90:867` | `frontend/src/views/competency/growth.vue` | 按二期计划已实现;按 Figma 部分对齐 | 已有初/中/高级成长路径和评级证据包;正式晋升申请/审核属于后续范围 | diff --git a/mobile-uni/src/pages/supervisor/team/index.vue b/mobile-uni/src/pages/supervisor/team/index.vue index b7671ee0..e4faf4b2 100644 --- a/mobile-uni/src/pages/supervisor/team/index.vue +++ b/mobile-uni/src/pages/supervisor/team/index.vue @@ -46,6 +46,27 @@ + + + 团队错题本 + 按项目范围聚合 + + + + + + {{ item.scenarioName }} + {{ item.count }} 次 + + {{ item.cause }} · {{ item.affectedPeople }} 名成员 · 平均 {{ item.averageScore }} 分 + 最近发生 {{ item.latestTime || '暂无时间' }} + + + + + 暂无团队错题记录 + + 成员列表 @@ -120,7 +141,7 @@