From ab54b04f769c1fe31e6e2d14f9debf580179b438 Mon Sep 17 00:00:00 2001 From: let5sne Date: Wed, 15 Jul 2026 02:17:04 +0800 Subject: [PATCH] fix(aihr): scope formal sop topics by app projects --- .../aihr/controller/AihrSopController.java | 26 ++++++++++++++- .../aihr/service/AihrSopSeedService.java | 33 ++++++++++++++----- .../aihr/service/AihrSopSeedServiceTest.java | 17 +++++++++- docs/BRD_IMPLEMENTATION_AUDIT.md | 2 ++ scripts/demo-check.sh | 4 ++- 5 files changed, 70 insertions(+), 12 deletions(-) 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 23a8bc99..b39905e1 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 @@ -25,7 +25,9 @@ import org.dromara.aihr.domain.AihrSopDto.UploadItemResponse; import org.dromara.aihr.domain.AihrSopDto.UploadResponse; import org.dromara.aihr.domain.AihrSopDto.VectorIndexStatusResponse; import org.dromara.aihr.domain.AihrSopDto.VectorizeResponse; +import org.dromara.aihr.domain.AihrOrgSyncDto.OrgPersonRow; import org.dromara.aihr.service.AihrSopSeedService; +import org.dromara.aihr.service.AihrOrgSyncService; import org.dromara.aihr.service.AihrUploadQueueService; import org.dromara.common.core.constant.TenantConstants; import org.dromara.common.core.domain.R; @@ -42,7 +44,9 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; /** * SOP 知识库 API。 @@ -56,6 +60,7 @@ public class AihrSopController { private static final String DEMO_VERIFY_SOURCE = "verify_demo_questions"; private final AihrSopSeedService sopSeedService; + private final AihrOrgSyncService orgSyncService; private final AihrUploadQueueService uploadQueueService; @SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR) @@ -105,7 +110,26 @@ public class AihrSopController { @GetMapping("/position-sop") public R positionSop(@RequestParam(required = false) String position, @RequestParam(required = false) String projectType) { - return R.ok(sopSeedService.positionSop(position, projectType)); + return R.ok(sopSeedService.positionSop(position, projectType, currentAppProjectCodes())); + } + + private Set currentAppProjectCodes() { + LoginUser loginUser = LoginHelper.getLoginUser(); + if (loginUser == null || !UserType.APP_USER.getUserType().equals(loginUser.getUserType())) { + return null; + } + String identity = currentRequesterExtPartyId(); + if (identity.isBlank()) { + return Set.of(); + } + try { + return orgSyncService.snapshot(identity, null, null, "active", 1, 500, 500).rows().stream() + .map(OrgPersonRow::projectCode) + .filter(code -> code != null && !code.isBlank()) + .collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new)); + } catch (RuntimeException ex) { + return Set.of(); + } } @SaCheckLogin 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 3b27ea84..1c24bb0b 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 @@ -168,13 +168,17 @@ public class AihrSopSeedService { } /** - * 一期只提供岗位学习导航摘要,不把它冒充成正式的 SOP 适用范围或上岗资格判断。 - * 正式匹配需要 position_responsibility/sop_applicability 和组织项目数据落库后再开启。 + * 提供岗位学习导航摘要,不把它冒充成正式的上岗资格判断。 + * 有正式岗位/SOP映射时按项目范围读取;没有映射或旧库尚未迁移时回退一期 seed。 */ public PositionSopResponse positionSop(String requestedPosition, String projectType) { + return positionSop(requestedPosition, projectType, null); + } + + public PositionSopResponse positionSop(String requestedPosition, String projectType, Set projectCodes) { String position = firstNonBlank(requestedPosition, "生活顾问"); String effectiveProjectType = firstNonBlank(projectType, "住宅"); - List formalTopics = formalPositionSopTopics(position, effectiveProjectType); + List formalTopics = formalPositionSopTopics(position, effectiveProjectType, projectCodes); if (!formalTopics.isEmpty()) { return new PositionSopResponse( position, @@ -211,12 +215,12 @@ public class AihrSopSeedService { ); } - private List formalPositionSopTopics(String position, String projectType) { + private List formalPositionSopTopics(String position, String projectType, Set projectCodes) { if (jdbcTemplate == null) { return List.of(); } try { - return jdbcTemplate.query(""" + StringBuilder sql = new StringBuilder(""" SELECT r.position_code, r.responsibility_text, r.sop_code, r.sop_id FROM aihr_position_responsibility r JOIN aihr_sop_applicability a @@ -232,9 +236,20 @@ public class AihrSopSeedService { 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) -> { + """); + List args = new ArrayList<>(List.of(tenantId(), position, projectType)); + if (projectCodes != null) { + sql.append(" AND (a.org_scope IS NULL OR a.org_scope = ''"); + for (String projectCode : projectCodes) { + if (projectCode != null && !projectCode.isBlank()) { + sql.append(" OR FIND_IN_SET(?, REPLACE(a.org_scope, ' ', '')) > 0"); + args.add(projectCode.trim()); + } + } + sql.append(")\n"); + } + sql.append(" ORDER BY r.id LIMIT 50"); + return jdbcTemplate.query(sql.toString(), (rs, rowNum) -> { String sopCode = rs.getString("sop_code"); Object sopId = rs.getObject("sop_id"); String sopReference = firstNonBlank(sopCode, sopId == null ? null : "sop#" + sopId); @@ -244,7 +259,7 @@ public class AihrSopSeedService { firstNonBlank(sopReference, "待绑定SOP"), "正式适用" ); - }, tenantId(), position, projectType); + }, args.toArray()); } catch (DataAccessException ex) { log.debug("formal position SOP mapping unavailable, use pilot seed navigation", ex); return List.of(); 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 53ca34b1..a5773ecd 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 @@ -253,13 +253,28 @@ public class AihrSopSeedServiceTest { } String code = Files.readString(source); - assertTrue(code.contains("formalPositionSopTopics(position, effectiveProjectType)")); + assertTrue(code.contains("formalPositionSopTopics(position, effectiveProjectType, projectCodes)")); assertTrue(code.contains("FROM aihr_position_responsibility r")); assertTrue(code.contains("JOIN aihr_sop_applicability a")); + assertTrue(code.contains("FIND_IN_SET(?, REPLACE(a.org_scope, ' ', '')) > 0")); assertTrue(code.contains("\"formal-applicability\"")); assertTrue(code.contains("\"NOT_CONFIGURED\"")); } + @Test + @Tag("dev") + public void positionSopBindsFormalMappingsToAuthenticatedAppProjects() throws Exception { + Path source = Path.of("src/main/java/org/dromara/aihr/controller/AihrSopController.java"); + if (!Files.exists(source)) { + source = Path.of("ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSopController.java"); + } + String code = Files.readString(source); + + assertTrue(code.contains("sopSeedService.positionSop(position, projectType, currentAppProjectCodes())")); + assertTrue(code.contains("orgSyncService.snapshot(identity, null, null, \"active\", 1, 500, 500)")); + assertTrue(code.contains("OrgPersonRow::projectCode")); + } + @Test @Tag("dev") public void answerFeedbackReviewUsesAuthenticatedOperatorForAuditTrail() throws Exception { diff --git a/docs/BRD_IMPLEMENTATION_AUDIT.md b/docs/BRD_IMPLEMENTATION_AUDIT.md index 4d75cb97..2242513e 100644 --- a/docs/BRD_IMPLEMENTATION_AUDIT.md +++ b/docs/BRD_IMPLEMENTATION_AUDIT.md @@ -1,5 +1,7 @@ # BRD 功能审查与未完成项记录 +- 2026-07-15 BRD 4.2/G3 正式岗位-SOP项目范围收口:`position-sop` 读取正式适用范围时此前只按岗位和项目类型过滤,未使用 `sop_applicability.org_scope`;现从认证 APP 用户对应的在职组织快照提取项目编码,并以参数化 `FIND_IN_SET` 仅返回全局或本人项目范围的映射。无组织映射时不扩大范围,继续回退一期 seed;系统用户读取保留管理侧不绑定 APP 项目的兼容语义。新增源码契约与 `demo-check` marker,未写入业务映射或生产数据。 + - 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/任务/资格迁移尚未执行线上数据库写入。当前结论仍是代码与迁移可继续发布准备,线上未追平;生产迁移需正式窗口、备份/回滚责任和组织同步凭据。 diff --git a/scripts/demo-check.sh b/scripts/demo-check.sh index d9cfa0b0..d82b3739 100755 --- a/scripts/demo-check.sh +++ b/scripts/demo-check.sh @@ -621,9 +621,11 @@ contains backend/script/sql/update/aihr_20260715_position_sop_qualification_mysq contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrDashboardController.java "@SaCheckLogin" contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrDashboardController.java "@SaCheckRole" contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java 'firstNonBlank(position, "生活顾问")' -contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java "formalPositionSopTopics(position, effectiveProjectType)" +contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java "formalPositionSopTopics(position, effectiveProjectType, projectCodes)" contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java "formal-applicability" contains mobile-uni/src/pages/user/learning/index.vue "qualificationStatus === 'NOT_CONFIGURED'" +contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSopController.java "currentAppProjectCodes()" +contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java "FIND_IN_SET(?, REPLACE(a.org_scope, ' ', '')) > 0" contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java "normalizeSearchSource" contains frontend/src/views/train/practice.vue "practice_help" contains mobile-uni/src/pages/user/practice/index.vue "loadHelp"