diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSopController.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSopController.java index e43260d7..86d025d9 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSopController.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSopController.java @@ -9,6 +9,7 @@ import org.dromara.aihr.domain.AihrSopDto.LocalImportRequest; import org.dromara.aihr.domain.AihrSopDto.LocalImportResponse; import org.dromara.aihr.domain.AihrSopDto.LocalImportTaskResponse; import org.dromara.aihr.domain.AihrSopDto.ProcessingOverviewResponse; +import org.dromara.aihr.domain.AihrSopDto.PositionSopResponse; import org.dromara.aihr.domain.AihrSopDto.SearchRequest; import org.dromara.aihr.domain.AihrSopDto.SearchResponse; import org.dromara.aihr.domain.AihrSopDto.SopReviewRequest; @@ -69,6 +70,12 @@ public class AihrSopController { return R.ok(sopSeedService.search(request)); } + @GetMapping("/position-sop") + public R positionSop(@RequestParam(required = false) String position, + @RequestParam(required = false) String projectType) { + return R.ok(sopSeedService.positionSop(position, projectType)); + } + @PostMapping("/answer-feedback") public R answerFeedback(@RequestBody AnswerFeedbackRequest request) { return R.ok(sopSeedService.answerFeedback(request)); diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrSopDto.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrSopDto.java index d0fb28be..207c0b3d 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrSopDto.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrSopDto.java @@ -10,6 +10,25 @@ public final class AihrSopDto { public record SearchRequest(String queryText, String category, String position, String source, Integer limit) { } + public record PositionSopResponse( + String position, + String projectType, + String routingMode, + Boolean formalApplicabilityConfigured, + String qualificationStatus, + String message, + List topics + ) { + } + + public record PositionSopTopicResponse( + String category, + String question, + String reference, + String status + ) { + } + public record SummaryCardRequest(String queryText, String category) { } diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java index 946df5fe..c70d84ee 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java @@ -19,6 +19,8 @@ import org.dromara.aihr.domain.AihrSopDto.LocalImportResponse; import org.dromara.aihr.domain.AihrSopDto.LocalImportTaskResponse; import org.dromara.aihr.domain.AihrSopDto.MetricResponse; import org.dromara.aihr.domain.AihrSopDto.PipelineStepResponse; +import org.dromara.aihr.domain.AihrSopDto.PositionSopResponse; +import org.dromara.aihr.domain.AihrSopDto.PositionSopTopicResponse; import org.dromara.aihr.domain.AihrSopDto.ProcessingEventResponse; import org.dromara.aihr.domain.AihrSopDto.ProcessingOverviewResponse; import org.dromara.aihr.domain.AihrSopDto.ProcessingRulesResponse; @@ -156,6 +158,38 @@ public class AihrSopSeedService { return withReviewId(noEvidenceResponse(queryText, category), source); } + /** + * 一期只提供岗位学习导航摘要,不把它冒充成正式的 SOP 适用范围或上岗资格判断。 + * 正式匹配需要 position_responsibility/sop_applicability 和组织项目数据落库后再开启。 + */ + public PositionSopResponse positionSop(String requestedPosition, String projectType) { + String position = firstNonBlank(requestedPosition, "生活顾问"); + String effectiveProjectType = firstNonBlank(projectType, "住宅"); + if (!"生活顾问".equals(position)) { + return new PositionSopResponse( + position, + effectiveProjectType, + "pilot-seed-navigation", + false, + "NOT_CONFIGURED", + "当前一期只开放生活顾问试点,当前岗位尚未配置正式 SOP 适用范围。", + List.of() + ); + } + List topics = seeds.values().stream() + .map(seed -> new PositionSopTopicResponse(seed.category(), seed.question(), seed.reference(), "可学习")) + .toList(); + return new PositionSopResponse( + position, + effectiveProjectType, + "pilot-seed-navigation", + false, + "NOT_CONFIGURED", + "当前为生活顾问一期学习导航;正式上岗资格仍需组织身份、SOP 适用范围和资格规则配置。", + topics + ); + } + public SopReviewResponse reviewSearch(Long id, SopReviewRequest request) { if (id == null) { return null; diff --git a/docs/API_INTEGRATION.md b/docs/API_INTEGRATION.md index 06096112..f5e383d8 100644 --- a/docs/API_INTEGRATION.md +++ b/docs/API_INTEGRATION.md @@ -11,7 +11,7 @@ | 正式试点数据导出 | `GET /api/train/practice/export?startDate=YYYY-MM-DD&endDate=YYYY-MM-DD` | 起止日期必填且包含结束日;只统计窗口内能通过唯一手机号或外部 ID 映射到在职组织快照的正式会话,排除重复手机号和身份碰撞。完训定义为每人至少 10 次,校准必须关联同一窗口内正式会话;CSV 同时给出校准命中数和 SOP 可用数,避免用四舍五入后的比率反推门禁状态;汇总和明细均携带正式人员及项目口径,不混入历史 seed/开发身份 | | 对练语音 | `POST /api/ai/asr`(multipart 字段 `file`,≤5MB)、`POST /api/ai/tts`(JSON `{text≤300字, voice}`,返回 `{audioUrl}` base64 dataURL) | 走 OpenAI-compatible audio 接口(如硅基流动 SenseVoice/CosyVoice2);模型管理需启用 `category=asr/tts` 配置;移动端优先用浏览器录音,`getUserMedia/MediaRecorder` 不可用或麦克风权限失败时,用 `audio/*` file input 选择/录制音频后继续调同一 ASR 接口;ASR/TTS 未配置或失败返回 fail,前端降级文本 | | 案例沉淀 `/knowledge/cases` | `POST /api/knowledge/case/upload`、`/organize`、`/curate` | `/upload` 改为 multipart 真实语音上传并走 ASR;`/organize` 用真实转写调 chat 模型整理案例,未配置模型时按真实 transcript 本地结构化;不再用固定样例转写冒充成功 | -| SOP知识库 `/knowledge/sop` | `POST /api/knowledge/search`、`POST /api/knowledge/doc/upload` | 已接入 MySQL Fulltext + Qdrant 混合召回、OSS-first 文档上传、txt/md/PDF/Word/Excel/PPT 解析和 embedding 写入,失败回退 seed | +| SOP知识库 `/knowledge/sop` | `POST /api/knowledge/search`、`GET /api/knowledge/position-sop`、`POST /api/knowledge/doc/upload` | 已接入 MySQL Fulltext + Qdrant 混合召回、岗位学习适配摘要、OSS-first 文档上传、txt/md/PDF/Word/Excel/PPT 解析和 embedding 写入,失败回退 seed;`position-sop` 目前只返回一期生活顾问学习导航,不代表正式上岗资格 | | 资料处理 `/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`。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 代码级强制失效) | diff --git a/docs/BRD_IMPLEMENTATION_AUDIT.md b/docs/BRD_IMPLEMENTATION_AUDIT.md index 4b4ce5e2..e4351c34 100644 --- a/docs/BRD_IMPLEMENTATION_AUDIT.md +++ b/docs/BRD_IMPLEMENTATION_AUDIT.md @@ -33,7 +33,7 @@ | 缺口 | BRD依据 | 当前事实 | 建议实现顺序 | |---|---|---|---| -| 岗位-SOP匹配与上岗资格 gating | 3.1、4.2、TechSpec P1 | 当前有岗位选择、SOP 搜索和训练任务,但未发现稳定的 `onboard_task` / `qualification_gate` 业务表与页面闭环 | 先建岗位适用 SOP 只读结果,再补培训任务状态,最后补“未通过不得标记上岗”的资格规则 | +| 岗位-SOP匹配与上岗资格 gating | 3.1、4.2、TechSpec P1 | 已补 `GET /api/knowledge/position-sop` 和员工学习页状态提示:一期只确认生活顾问内置学习导航;尚无 `sop_applicability`、`onboard_task`、`qualification_gate` 正式数据闭环,不能作为上岗资格判断 | 先接正式岗位/项目与 SOP 适用范围数据,再补培训任务状态;资格 gating 必须由 HR 明确规则和证据来源后实现 | | 智能错题本 | 3.1、4.4、TechSpec P1 | 已补 `/api/aihr/mobile/practice/mistakes` 和员工“学”页错题本:按本人聚合低分/红线回合,展示原因、分数、再练状态,并可跳已有 retry assignment;暂未做复杂原因筛选和独立错题表 | 下一步补原因筛选、复盘详情关联和主管侧聚合,仍复用现有 session/assignment/annotation | | 案例学习/视频详情 | 3.1、4.6、矩阵第35行 | `AihrCaseService` 当前用内存 `CaseState` + `aihr_case_record` 保存转写/摘要,没有稳定 media URL、学习点、主管点评和案例详情 API | 先扩持久化契约和权限,再接预渲染样片;没有真实媒体 URL 时不造播放按钮 | | 案例库质量与数量 | L1-B、4.6/4.7 | 上传/整理/筛选路径已有;“至少20条脱敏案例”和每周每项目一条需要正式数据证明 | 增加案例统计/脱敏检查导出,按项目窗口采集,不用演示 seed 冒充 | @@ -49,7 +49,7 @@ 1. 先完成正式组织快照 dry-run 与项目范围核对;不修改正式数据,不用手机号猜角色。 2. 在本地/测试环境实现“错题本只读聚合”,复用现有训练记录和 retry assignment,补 API、员工入口和主管查看边界。 -3. 再实现岗位-SOP匹配与培训任务状态;资格 gating 放在有真实资格规则后接入,避免先做一个无法运营的开关。 +3. 已先补岗位学习适配摘要,但它只负责把一期生活顾问导航状态说清楚;下一步接正式岗位/项目/SOP 适用范围,再实现培训任务状态;资格 gating 放在有真实资格规则后接入,避免先做一个无法运营的开关。 4. 单独设计案例详情数据契约,确定媒体来源、样片、学习点和主管点评后再开发详情页。 5. 用正式试点窗口跑 BRD L1/M5,而不是继续增加视觉页面。 diff --git a/mobile-uni/src/pages/user/learning/index.vue b/mobile-uni/src/pages/user/learning/index.vue index a162cd34..edaefa54 100644 --- a/mobile-uni/src/pages/user/learning/index.vue +++ b/mobile-uni/src/pages/user/learning/index.vue @@ -108,6 +108,13 @@ {{ knowledgeLoading ? '检索中' : '重查' }} + + + {{ positionSopSummary.position }} · {{ positionSopSummary.topics.length }} 个学习主题 + + {{ positionSopSummary.message }} + 当前摘要不代表正式上岗资格 +