feat(aihr): read formal position SOP mappings

This commit is contained in:
2026-07-15 02:08:51 +08:00
parent c2a0611f50
commit 747112910c
5 changed files with 74 additions and 1 deletions
@@ -174,6 +174,18 @@ public class AihrSopSeedService {
public PositionSopResponse positionSop(String requestedPosition, String projectType) { public PositionSopResponse positionSop(String requestedPosition, String projectType) {
String position = firstNonBlank(requestedPosition, "生活顾问"); String position = firstNonBlank(requestedPosition, "生活顾问");
String effectiveProjectType = firstNonBlank(projectType, "住宅"); String effectiveProjectType = firstNonBlank(projectType, "住宅");
List<PositionSopTopicResponse> formalTopics = formalPositionSopTopics(position, effectiveProjectType);
if (!formalTopics.isEmpty()) {
return new PositionSopResponse(
position,
effectiveProjectType,
"formal-applicability",
true,
"NOT_CONFIGURED",
"已按正式岗位/SOP适用范围加载学习主题;上岗资格仍需组织身份和资格规则配置。",
formalTopics
);
}
if (!"生活顾问".equals(position)) { if (!"生活顾问".equals(position)) {
return new PositionSopResponse( return new PositionSopResponse(
position, position,
@@ -199,6 +211,46 @@ public class AihrSopSeedService {
); );
} }
private List<PositionSopTopicResponse> 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) { public SopReviewResponse reviewSearch(Long id, SopReviewRequest request) {
return reviewSearch(id, request, null); return reviewSearch(id, request, null);
} }
@@ -244,6 +244,22 @@ public class AihrSopSeedServiceTest {
assertFalse(code.contains("@SaIgnore\n @GetMapping(\"/position-sop\")")); 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 @Test
@Tag("dev") @Tag("dev")
public void answerFeedbackReviewUsesAuthenticatedOperatorForAuditTrail() throws Exception { public void answerFeedbackReviewUsesAuthenticatedOperatorForAuditTrail() throws Exception {
+2
View File
@@ -1,5 +1,7 @@
# BRD 功能审查与未完成项记录 # 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 岗位资格契约提交后线上只读复核:生产根站、`/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。 - 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。
+1 -1
View File
@@ -135,7 +135,7 @@
{{ positionSopSummary.position }} · {{ positionSopSummary.topics.length }} 个学习主题 {{ positionSopSummary.position }} · {{ positionSopSummary.topics.length }} 个学习主题
</text> </text>
<text class="position-sop-note">{{ positionSopSummary.message }}</text> <text class="position-sop-note">{{ positionSopSummary.message }}</text>
<text v-if="!positionSopSummary.formalApplicabilityConfigured" class="position-sop-warning">当前摘要不代表正式上岗资格</text> <text v-if="!positionSopSummary.formalApplicabilityConfigured || positionSopSummary.qualificationStatus === 'NOT_CONFIGURED'" class="position-sop-warning">当前摘要不代表正式上岗资格</text>
</view> </view>
<view class="chip-row topic-row"> <view class="chip-row topic-row">
<button <button
+3
View File
@@ -621,6 +621,9 @@ 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 "@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/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 '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 "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/service/AihrSopSeedService.java "normalizeSearchSource" 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 frontend/src/views/train/practice.vue "practice_help"
contains mobile-uni/src/pages/user/practice/index.vue "loadHelp" contains mobile-uni/src/pages/user/practice/index.vue "loadHelp"