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 cda909bc..3b27ea84 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 @@ -174,6 +174,18 @@ public class AihrSopSeedService { public PositionSopResponse positionSop(String requestedPosition, String projectType) { String position = firstNonBlank(requestedPosition, "生活顾问"); String effectiveProjectType = firstNonBlank(projectType, "住宅"); + List formalTopics = formalPositionSopTopics(position, effectiveProjectType); + if (!formalTopics.isEmpty()) { + return new PositionSopResponse( + position, + effectiveProjectType, + "formal-applicability", + true, + "NOT_CONFIGURED", + "已按正式岗位/SOP适用范围加载学习主题;上岗资格仍需组织身份和资格规则配置。", + formalTopics + ); + } if (!"生活顾问".equals(position)) { return new PositionSopResponse( position, @@ -199,6 +211,46 @@ public class AihrSopSeedService { ); } + private List formalPositionSopTopics(String position, String projectType) { + if (jdbcTemplate == null) { + return List.of(); + } + try { + return jdbcTemplate.query(""" + SELECT r.position_code, r.responsibility_text, r.sop_code, r.sop_id + FROM aihr_position_responsibility r + JOIN aihr_sop_applicability a + ON a.tenant_id = r.tenant_id + AND ((r.sop_id IS NOT NULL AND r.sop_id = a.sop_id) + OR (NULLIF(r.sop_code, '') IS NOT NULL AND r.sop_code = a.sop_code)) + WHERE r.tenant_id = ? + AND r.position_code = ? + AND r.enabled = 1 + AND a.enabled = 1 + AND (a.project_type IS NULL OR a.project_type = '' OR a.project_type = ?) + AND (r.effective_from IS NULL OR r.effective_from <= CURRENT_DATE) + AND (r.effective_thru IS NULL OR r.effective_thru >= CURRENT_DATE) + AND (a.effective_from IS NULL OR a.effective_from <= CURRENT_DATE) + AND (a.effective_thru IS NULL OR a.effective_thru >= CURRENT_DATE) + ORDER BY r.id + LIMIT 50 + """, (rs, rowNum) -> { + String sopCode = rs.getString("sop_code"); + Object sopId = rs.getObject("sop_id"); + String sopReference = firstNonBlank(sopCode, sopId == null ? null : "sop#" + sopId); + return new PositionSopTopicResponse( + firstNonBlank(rs.getString("position_code"), position), + rs.getString("responsibility_text"), + firstNonBlank(sopReference, "待绑定SOP"), + "正式适用" + ); + }, tenantId(), position, projectType); + } catch (DataAccessException ex) { + log.debug("formal position SOP mapping unavailable, use pilot seed navigation", ex); + return List.of(); + } + } + public SopReviewResponse reviewSearch(Long id, SopReviewRequest request) { return reviewSearch(id, request, null); } diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrSopSeedServiceTest.java b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrSopSeedServiceTest.java index 6b6e5b91..53ca34b1 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrSopSeedServiceTest.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrSopSeedServiceTest.java @@ -244,6 +244,22 @@ public class AihrSopSeedServiceTest { assertFalse(code.contains("@SaIgnore\n @GetMapping(\"/position-sop\")")); } + @Test + @Tag("dev") + public void positionSopUsesFormalMappingBeforeSeedFallback() throws Exception { + Path source = Path.of("src/main/java/org/dromara/aihr/service/AihrSopSeedService.java"); + if (!Files.exists(source)) { + source = Path.of("ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java"); + } + String code = Files.readString(source); + + assertTrue(code.contains("formalPositionSopTopics(position, effectiveProjectType)")); + assertTrue(code.contains("FROM aihr_position_responsibility r")); + assertTrue(code.contains("JOIN aihr_sop_applicability a")); + assertTrue(code.contains("\"formal-applicability\"")); + assertTrue(code.contains("\"NOT_CONFIGURED\"")); + } + @Test @Tag("dev") public void answerFeedbackReviewUsesAuthenticatedOperatorForAuditTrail() throws Exception { diff --git a/docs/BRD_IMPLEMENTATION_AUDIT.md b/docs/BRD_IMPLEMENTATION_AUDIT.md index 1176be4d..952c45a7 100644 --- a/docs/BRD_IMPLEMENTATION_AUDIT.md +++ b/docs/BRD_IMPLEMENTATION_AUDIT.md @@ -1,5 +1,7 @@ # BRD 功能审查与未完成项记录 +- 2026-07-15 BRD 4.2/M4 正式岗位-SOP读取路径补齐:`/api/knowledge/position-sop` 现在优先读取已启用且在生效期内的 `position_responsibility + sop_applicability` 映射;没有正式映射或旧库尚未迁移时继续使用生活顾问 seed 导航。正式映射只改变学习主题来源,资格状态仍固定为 `NOT_CONFIGURED`,移动端继续显示资格提醒;未写入任何业务映射,待 HR 提供组织/项目/SOP规则后再实现资格判定和入职任务分派。 + - 2026-07-15 岗位资格契约提交后线上只读复核:生产根站、`/h5/` 和租户接口仍返回 `200`,`wygj-aihr.service` 为 `active`;线上 H5 仍引用 `/h5/assets/index-CDn6TrX8.js`,本轮新增岗位/SOP/任务/资格迁移尚未执行线上数据库写入。当前结论仍是代码与迁移可继续发布准备,线上未追平;生产迁移需正式窗口、备份/回滚责任和组织同步凭据。 - 2026-07-15 BRD 4.2/M4 岗位-SOP-上岗资格数据契约补齐:复核发现 `position_responsibility`、`sop_applicability`、`onboard_task`、`qualification_gate` 在代码库中只有 TechSpec 设计,没有正式 MySQL 结构;现新增幂等迁移和 reset DDL,并把四表纳入生产 Runbook、schema 回归和严格预检的结构可见性检查。迁移不写岗位、员工、课程或资格 seed,`position-sop` 仍保持 `NOT_CONFIGURED`;正式适用范围、入职任务规则和资格标准需 HR/组织系统提供后再接读取与 gating。 diff --git a/mobile-uni/src/pages/user/learning/index.vue b/mobile-uni/src/pages/user/learning/index.vue index 76c9ebdc..6cb2ab69 100644 --- a/mobile-uni/src/pages/user/learning/index.vue +++ b/mobile-uni/src/pages/user/learning/index.vue @@ -135,7 +135,7 @@ {{ positionSopSummary.position }} · {{ positionSopSummary.topics.length }} 个学习主题 {{ positionSopSummary.message }} - 当前摘要不代表正式上岗资格 + 当前摘要不代表正式上岗资格