From d44e9440ced3e8505d7f58c4e54ede705b3f09c3 Mon Sep 17 00:00:00 2001 From: let5sne Date: Wed, 15 Jul 2026 02:04:05 +0800 Subject: [PATCH] feat(aihr): add position qualification schema contract --- backend/script/sql/aihr_practice_mysql8.sql | 78 ++++++++++++++++++ ...0715_position_sop_qualification_mysql8.sql | 79 +++++++++++++++++++ docs/BRD_IMPLEMENTATION_AUDIT.md | 2 + docs/BRD_PRODUCTION_MIGRATION_RUNBOOK.md | 13 ++- scripts/demo-check.sh | 18 ++++- scripts/tests/aihr-schema-migrations.test.sh | 15 +++- scripts/tests/demo-check-pilot-gates.test.sh | 1 + 7 files changed, 200 insertions(+), 6 deletions(-) create mode 100644 backend/script/sql/update/aihr_20260715_position_sop_qualification_mysql8.sql diff --git a/backend/script/sql/aihr_practice_mysql8.sql b/backend/script/sql/aihr_practice_mysql8.sql index a673a5a3..11c240a4 100644 --- a/backend/script/sql/aihr_practice_mysql8.sql +++ b/backend/script/sql/aihr_practice_mysql8.sql @@ -463,3 +463,81 @@ ON DUPLICATE KEY UPDATE `enabled` = VALUES(`enabled`), `description` = VALUES(`description`), `update_time` = VALUES(`update_time`); + +-- 岗位职责、SOP 适用范围、入职任务和资格门禁正式结构。 +-- 不写入业务数据;与 update/aihr_20260715_position_sop_qualification_mysql8.sql 保持一致。 +CREATE TABLE IF NOT EXISTS `aihr_position_responsibility` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', + `tenant_id` varchar(20) NOT NULL DEFAULT '000000' COMMENT '租户编号', + `position_code` varchar(80) NOT NULL COMMENT '岗位编码', + `responsibility_text` varchar(1000) NOT NULL COMMENT '岗位职责', + `sop_id` bigint DEFAULT NULL COMMENT '关联SOP主键(跨模块引用)', + `sop_code` varchar(100) DEFAULT NULL COMMENT '关联SOP编码', + `enabled` tinyint NOT NULL DEFAULT 1 COMMENT '是否启用', + `effective_from` date DEFAULT NULL COMMENT '生效日期', + `effective_thru` date DEFAULT NULL COMMENT '失效日期', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_aihr_position_responsibility` (`tenant_id`, `position_code`, `sop_code`), + KEY `idx_aihr_position_responsibility_status` (`tenant_id`, `position_code`, `enabled`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI HR 岗位职责与SOP挂载'; + +CREATE TABLE IF NOT EXISTS `aihr_sop_applicability` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', + `tenant_id` varchar(20) NOT NULL DEFAULT '000000' COMMENT '租户编号', + `sop_id` bigint DEFAULT NULL COMMENT '关联SOP主键(跨模块引用)', + `sop_code` varchar(100) DEFAULT NULL COMMENT '关联SOP编码', + `project_type` varchar(80) DEFAULT NULL COMMENT '项目类型:住宅/公建/景区等', + `industry` varchar(80) DEFAULT NULL COMMENT '行业分类', + `org_scope` varchar(500) DEFAULT NULL COMMENT '适用组织/项目范围编码,不存展示姓名', + `enabled` tinyint NOT NULL DEFAULT 1 COMMENT '是否启用', + `effective_from` date DEFAULT NULL COMMENT '生效日期', + `effective_thru` date DEFAULT NULL COMMENT '失效日期', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `idx_aihr_sop_applicability_sop` (`tenant_id`, `sop_id`, `sop_code`), + KEY `idx_aihr_sop_applicability_scope` (`tenant_id`, `project_type`, `industry`, `enabled`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI HR SOP适用范围'; + +CREATE TABLE IF NOT EXISTS `aihr_onboard_task` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', + `tenant_id` varchar(20) NOT NULL DEFAULT '000000' COMMENT '租户编号', + `ext_party_id` varchar(100) NOT NULL COMMENT '员工外部主体ID,不复制姓名', + `project_code` varchar(100) DEFAULT NULL COMMENT '项目编码', + `position_code` varchar(80) DEFAULT NULL COMMENT '岗位编码', + `sop_id` bigint DEFAULT NULL COMMENT '关联SOP主键(跨模块引用)', + `course_id` bigint DEFAULT NULL COMMENT '关联课程主键(跨模块引用)', + `task_type` varchar(20) NOT NULL COMMENT '任务类型:岗前/入职', + `status` varchar(30) NOT NULL DEFAULT '待完成' COMMENT '待完成/进行中/已完成/已取消', + `assign_by` varchar(100) DEFAULT NULL COMMENT '派发人外部主体或系统标识', + `assigned_time` datetime DEFAULT NULL COMMENT '派发时间', + `due_time` datetime DEFAULT NULL COMMENT '截止时间', + `completed_time` datetime DEFAULT NULL COMMENT '完成时间', + `evidence_ref` varchar(500) DEFAULT NULL COMMENT '完成证据引用', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `idx_aihr_onboard_task_party` (`tenant_id`, `ext_party_id`, `status`, `create_time`), + KEY `idx_aihr_onboard_task_scope` (`tenant_id`, `project_code`, `position_code`, `task_type`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI HR 岗前/入职培训任务'; + +CREATE TABLE IF NOT EXISTS `aihr_qualification_gate` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', + `tenant_id` varchar(20) NOT NULL DEFAULT '000000' COMMENT '租户编号', + `ext_party_id` varchar(100) NOT NULL COMMENT '员工外部主体ID,不复制姓名', + `project_code` varchar(100) DEFAULT NULL COMMENT '项目编码', + `position_code` varchar(80) NOT NULL COMMENT '岗位编码', + `cert_id` varchar(100) NOT NULL COMMENT '资格/证书编码', + `passed` tinyint NOT NULL DEFAULT 0 COMMENT '是否通过', + `valid_thru` date DEFAULT NULL COMMENT '有效期至', + `evidence_ref` varchar(500) DEFAULT NULL COMMENT '资格证据引用', + `reviewer` varchar(100) DEFAULT NULL COMMENT '复核人外部主体或系统标识', + `reviewed_time` datetime DEFAULT NULL COMMENT '复核时间', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_aihr_qualification_gate` (`tenant_id`, `ext_party_id`, `project_code`, `position_code`, `cert_id`), + KEY `idx_aihr_qualification_gate_status` (`tenant_id`, `position_code`, `passed`, `valid_thru`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI HR 上岗资格门禁'; diff --git a/backend/script/sql/update/aihr_20260715_position_sop_qualification_mysql8.sql b/backend/script/sql/update/aihr_20260715_position_sop_qualification_mysql8.sql new file mode 100644 index 00000000..a344e8f1 --- /dev/null +++ b/backend/script/sql/update/aihr_20260715_position_sop_qualification_mysql8.sql @@ -0,0 +1,79 @@ +-- AIHR 岗位职责、SOP 适用范围、入职任务和上岗资格门禁结构迁移(MySQL 8.x) +-- 只补正式数据契约,不写入岗位、员工、课程或资格业务数据;可重复执行。 +-- sop_id/course_id 不加外键:正式 SOP/course 主表尚未纳入阶段一,先保留跨模块引用。 + +CREATE TABLE IF NOT EXISTS `aihr_position_responsibility` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', + `tenant_id` varchar(20) NOT NULL DEFAULT '000000' COMMENT '租户编号', + `position_code` varchar(80) NOT NULL COMMENT '岗位编码', + `responsibility_text` varchar(1000) NOT NULL COMMENT '岗位职责', + `sop_id` bigint DEFAULT NULL COMMENT '关联SOP主键(跨模块引用)', + `sop_code` varchar(100) DEFAULT NULL COMMENT '关联SOP编码', + `enabled` tinyint NOT NULL DEFAULT 1 COMMENT '是否启用', + `effective_from` date DEFAULT NULL COMMENT '生效日期', + `effective_thru` date DEFAULT NULL COMMENT '失效日期', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_aihr_position_responsibility` (`tenant_id`, `position_code`, `sop_code`), + KEY `idx_aihr_position_responsibility_status` (`tenant_id`, `position_code`, `enabled`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI HR 岗位职责与SOP挂载'; + +CREATE TABLE IF NOT EXISTS `aihr_sop_applicability` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', + `tenant_id` varchar(20) NOT NULL DEFAULT '000000' COMMENT '租户编号', + `sop_id` bigint DEFAULT NULL COMMENT '关联SOP主键(跨模块引用)', + `sop_code` varchar(100) DEFAULT NULL COMMENT '关联SOP编码', + `project_type` varchar(80) DEFAULT NULL COMMENT '项目类型:住宅/公建/景区等', + `industry` varchar(80) DEFAULT NULL COMMENT '行业分类', + `org_scope` varchar(500) DEFAULT NULL COMMENT '适用组织/项目范围编码,不存展示姓名', + `enabled` tinyint NOT NULL DEFAULT 1 COMMENT '是否启用', + `effective_from` date DEFAULT NULL COMMENT '生效日期', + `effective_thru` date DEFAULT NULL COMMENT '失效日期', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `idx_aihr_sop_applicability_sop` (`tenant_id`, `sop_id`, `sop_code`), + KEY `idx_aihr_sop_applicability_scope` (`tenant_id`, `project_type`, `industry`, `enabled`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI HR SOP适用范围'; + +CREATE TABLE IF NOT EXISTS `aihr_onboard_task` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', + `tenant_id` varchar(20) NOT NULL DEFAULT '000000' COMMENT '租户编号', + `ext_party_id` varchar(100) NOT NULL COMMENT '员工外部主体ID,不复制姓名', + `project_code` varchar(100) DEFAULT NULL COMMENT '项目编码', + `position_code` varchar(80) DEFAULT NULL COMMENT '岗位编码', + `sop_id` bigint DEFAULT NULL COMMENT '关联SOP主键(跨模块引用)', + `course_id` bigint DEFAULT NULL COMMENT '关联课程主键(跨模块引用)', + `task_type` varchar(20) NOT NULL COMMENT '任务类型:岗前/入职', + `status` varchar(30) NOT NULL DEFAULT '待完成' COMMENT '待完成/进行中/已完成/已取消', + `assign_by` varchar(100) DEFAULT NULL COMMENT '派发人外部主体或系统标识', + `assigned_time` datetime DEFAULT NULL COMMENT '派发时间', + `due_time` datetime DEFAULT NULL COMMENT '截止时间', + `completed_time` datetime DEFAULT NULL COMMENT '完成时间', + `evidence_ref` varchar(500) DEFAULT NULL COMMENT '完成证据引用', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `idx_aihr_onboard_task_party` (`tenant_id`, `ext_party_id`, `status`, `create_time`), + KEY `idx_aihr_onboard_task_scope` (`tenant_id`, `project_code`, `position_code`, `task_type`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI HR 岗前/入职培训任务'; + +CREATE TABLE IF NOT EXISTS `aihr_qualification_gate` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', + `tenant_id` varchar(20) NOT NULL DEFAULT '000000' COMMENT '租户编号', + `ext_party_id` varchar(100) NOT NULL COMMENT '员工外部主体ID,不复制姓名', + `project_code` varchar(100) DEFAULT NULL COMMENT '项目编码', + `position_code` varchar(80) NOT NULL COMMENT '岗位编码', + `cert_id` varchar(100) NOT NULL COMMENT '资格/证书编码', + `passed` tinyint NOT NULL DEFAULT 0 COMMENT '是否通过', + `valid_thru` date DEFAULT NULL COMMENT '有效期至', + `evidence_ref` varchar(500) DEFAULT NULL COMMENT '资格证据引用', + `reviewer` varchar(100) DEFAULT NULL COMMENT '复核人外部主体或系统标识', + `reviewed_time` datetime DEFAULT NULL COMMENT '复核时间', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_aihr_qualification_gate` (`tenant_id`, `ext_party_id`, `project_code`, `position_code`, `cert_id`), + KEY `idx_aihr_qualification_gate_status` (`tenant_id`, `position_code`, `passed`, `valid_thru`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI HR 上岗资格门禁'; diff --git a/docs/BRD_IMPLEMENTATION_AUDIT.md b/docs/BRD_IMPLEMENTATION_AUDIT.md index d93f3952..e7257e33 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-上岗资格数据契约补齐:复核发现 `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 生产迁移合同防回归:发现版本化 AIHR MySQL 迁移虽已全部写入 Runbook,但此前没有测试保证新增 `aihr_*_mysql8.sql` 必须同步进入执行清单;现将目录与 `BRD_PRODUCTION_MIGRATION_RUNBOOK.md` 的覆盖检查接入 schema migration 测试。该检查只验证发布合同,不执行生产迁移,也不替代备份、回滚和逐脚本核验。 - 2026-07-15 BRD 严格门禁旧案例结构兼容修复:`scripts/demo-check.sh` 原先在 `aihr_case_record` 整表或 `learning_points/supervisor_comment` 字段缺失时直接执行查询,可能以 Unknown column/table 中断,无法输出真正的 schema 门槛;现先探测表和字段,缺失时以 `stored_cases=0`、`case_media_schema=0/2`、`case_learning_schema=0/2` 继续判定,新增旧表/无表回归。该修复只改善迁移缺口可见性,不放松案例 `20` 条和字段 `2/2` 门槛。 diff --git a/docs/BRD_PRODUCTION_MIGRATION_RUNBOOK.md b/docs/BRD_PRODUCTION_MIGRATION_RUNBOOK.md index ce18b8fe..d5d760a2 100644 --- a/docs/BRD_PRODUCTION_MIGRATION_RUNBOOK.md +++ b/docs/BRD_PRODUCTION_MIGRATION_RUNBOOK.md @@ -39,9 +39,10 @@ mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aih mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260714_sop_requester_identity_mysql8.sql mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260715_knowledge_feedback_mysql8.sql mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260714_org_hire_date_mysql8.sql +mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260715_position_sop_qualification_mysql8.sql ``` -顺序原因:场景补充依赖三张场景/Rubric 基础表;五维迁移依赖场景和 Rubric;其余字段迁移依赖既有业务表但不写正式业务行。住宅 SOP 和 Prompt 迁移只补内置内容,不代表内容已完成业务复核。 +顺序原因:场景补充依赖三张场景/Rubric 基础表;五维迁移依赖场景和 Rubric;岗位/SOP/任务/资格迁移只补正式数据契约,不写业务行,也不代表岗位适用范围、任务规则或资格标准已经获得 HR 确认。住宅 SOP 和 Prompt 迁移只补内置内容,不代表内容已完成业务复核。 ## 迁移后只读核验 @@ -55,7 +56,9 @@ WHERE table_schema = DATABASE() 'aihr_practice_scenario', 'aihr_practice_rubric', 'aihr_practice_rubric_dimension', 'aihr_practice_audio', 'aihr_practice_audio_upload', 'aihr_practice_calibration', 'aihr_knowledge_gap', 'aihr_knowledge_answer_feedback', - 'aihr_candidate_material', 'aihr_interview_result', 'aihr_candidate_employee_link' + 'aihr_candidate_material', 'aihr_interview_result', 'aihr_candidate_employee_link', + 'aihr_position_responsibility', 'aihr_sop_applicability', + 'aihr_onboard_task', 'aihr_qualification_gate' ) ORDER BY table_name; @@ -70,6 +73,10 @@ WHERE table_schema = DATABASE() OR (table_name = 'aihr_org_snapshot' AND column_name = 'hire_date') OR (table_name = 'aihr_candidate_material' AND column_name IN ('reviewer', 'reviewed_time')) OR (table_name = 'aihr_interview_result' AND column_name IN ('questions_json', 'reviewed_score', 'review_status', 'reviewer', 'review_note', 'reviewed_time')) + OR (table_name = 'aihr_position_responsibility' AND column_name IN ('position_code', 'responsibility_text', 'sop_id', 'sop_code')) + OR (table_name = 'aihr_sop_applicability' AND column_name IN ('sop_id', 'sop_code', 'project_type', 'industry', 'org_scope')) + OR (table_name = 'aihr_onboard_task' AND column_name IN ('ext_party_id', 'position_code', 'task_type', 'status', 'assign_by')) + OR (table_name = 'aihr_qualification_gate' AND column_name IN ('ext_party_id', 'position_code', 'cert_id', 'passed', 'valid_thru')) ) ORDER BY table_name, column_name; @@ -79,7 +86,7 @@ WHERE tenant_id = '000000' AND code IN ('summary_card', 'sop_answer', 'mentor_rewrite', 'review_annotation', 'prep_card', 'supervision_image_analysis_maintenance'); ``` -预期是三张场景/Rubric 表、三张证据表、两张知识治理表和三张候选人/面试表均存在,会话证据字段为 `8/8`,候选资料审核字段为 `2/2`,面试复核字段为 `6/6`,字段查询覆盖所有列,内置 Prompt 数量为 `6`。这只证明 schema/seed 迁移完成,不证明正式试点完成。 +预期是三张场景/Rubric 表、三张证据表、两张知识治理表、三张候选人/面试表和四张岗位/SOP/任务/资格契约表均存在,会话证据字段为 `8/8`,候选资料审核字段为 `2/2`,面试复核字段为 `6/6`,字段查询覆盖所有列,内置 Prompt 数量为 `6`。岗位/SOP/任务/资格表为空是允许的;这只证明 schema/seed 迁移完成,不证明正式岗位规则、资格 gating 或试点完成。 ## 迁移后应用回归 diff --git a/scripts/demo-check.sh b/scripts/demo-check.sh index 7dd2aa1e..a73a7f1d 100755 --- a/scripts/demo-check.sh +++ b/scripts/demo-check.sh @@ -81,7 +81,7 @@ check_pilot_samples() { [[ "$(pilot_sql_scalar "SELECT DATE('$pilot_end') IS NOT NULL")" == "1" ]] || fail "invalid AIHR_PILOT_END_DATE: $pilot_end" [[ "$pilot_start" > "$pilot_end" ]] && fail "pilot start date must not be after end date" - local catalog_schema_count scenario_count scenario_service_promotion_count scenario_daily_service_count rubric_expected_count rubric_five_dimension_count completed_people pilot_people org_active org_phone_linked org_hire_date_linked org_pilot_people org_pilot_projects org_completed_people audio_table_count audio_upload_table_count case_table_exists case_media_schema_count case_learning_schema_count practice_session_schema_count daily_drill_schema_count prompt_template_count prompt_version_schema_count requester_identity_schema_count hire_date_schema_count sop_document_count case_stored_count case_unmasked_count + local catalog_schema_count scenario_count scenario_service_promotion_count scenario_daily_service_count rubric_expected_count rubric_five_dimension_count completed_people pilot_people org_active org_phone_linked org_hire_date_linked org_pilot_people org_pilot_projects org_completed_people audio_table_count audio_upload_table_count case_table_exists case_media_schema_count case_learning_schema_count practice_session_schema_count daily_drill_schema_count prompt_template_count prompt_version_schema_count requester_identity_schema_count hire_date_schema_count position_sop_schema_count formal_applicability_count onboard_task_count qualification_gate_count sop_document_count case_stored_count case_unmasked_count local calibration_stats calibration_count calibration_matched sop_total sop_usable sop_pending satisfaction_count satisfaction_average local org_identity_cte=" WITH active_org AS ( @@ -151,6 +151,15 @@ check_pilot_samples() { prompt_version_schema_count="$(pilot_sql_scalar "SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'aihr_sop_answer_review' AND column_name = 'prompt_version'")" requester_identity_schema_count="$(pilot_sql_scalar "SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'aihr_sop_answer_review' AND column_name = 'requester_ext_party_id'")" hire_date_schema_count="$(pilot_sql_scalar "SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'aihr_org_snapshot' AND column_name = 'hire_date'")" + position_sop_schema_count="$(pilot_sql_scalar "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name IN ('aihr_position_responsibility','aihr_sop_applicability','aihr_onboard_task','aihr_qualification_gate')")" + formal_applicability_count=0 + onboard_task_count=0 + qualification_gate_count=0 + if (( position_sop_schema_count == 4 )); then + formal_applicability_count="$(pilot_sql_scalar "SELECT COUNT(*) FROM aihr_sop_applicability WHERE tenant_id = '$pilot_tenant' AND enabled = 1")" + onboard_task_count="$(pilot_sql_scalar "SELECT COUNT(*) FROM aihr_onboard_task WHERE tenant_id = '$pilot_tenant'")" + qualification_gate_count="$(pilot_sql_scalar "SELECT COUNT(*) FROM aihr_qualification_gate WHERE tenant_id = '$pilot_tenant'")" + fi sop_document_count="$(pilot_sql_scalar "SELECT COUNT(*) FROM aihr_knowledge_attach WHERE tenant_id = '$pilot_tenant' AND type = 'sop' AND status = 2")" case_stored_count=0 case_unmasked_count=0 @@ -312,7 +321,7 @@ check_pilot_samples() { AND s.finished_time < DATE_ADD('${pilot_end}', INTERVAL 1 DAY) ) SELECT COALESCE(ROUND(AVG(satisfaction_score), 1), 0) FROM formal_sessions WHERE satisfaction_score IS NOT NULL")" - echo "OK: pilot gates window=${pilot_start}..${pilot_end} catalog_schema=${catalog_schema_count}/3 scenarios=${scenario_count}/12 rubric_five_dimensions=${rubric_five_dimension_count}/${rubric_expected_count} core_flow=service_promotion:${scenario_service_promotion_count} daily_service:${scenario_daily_service_count} sop_docs=${sop_document_count}/5 stored_cases=${case_stored_count}/20 case_unmasked=${case_unmasked_count} ten_sessions=${completed_people}/${pilot_people} org_identity=${org_phone_linked}/${org_active} org_hire_date=${org_hire_date_linked}/${org_active} org_pilot=${org_pilot_people}/20 projects=${org_pilot_projects}/1-2 org_ten_sessions=${org_completed_people}/${org_pilot_people} calibration=${calibration_count}/20 consistency=${calibration_matched}/${calibration_count} sop_usable=${sop_usable}/${sop_total} sop_pending=${sop_pending} satisfaction=${satisfaction_average}/5 responses=${satisfaction_count} audio_schema=${audio_table_count} audio_upload_schema=${audio_upload_table_count} case_media_schema=${case_media_schema_count}/2 case_learning_schema=${case_learning_schema_count}/2 practice_session_schema=${practice_session_schema_count}/8 daily_drill_schema=${daily_drill_schema_count}/6 prompt_templates=${prompt_template_count}/6 prompt_version_schema=${prompt_version_schema_count} requester_identity_schema=${requester_identity_schema_count} hire_date_schema=${hire_date_schema_count}" + echo "OK: pilot gates window=${pilot_start}..${pilot_end} catalog_schema=${catalog_schema_count}/3 scenarios=${scenario_count}/12 rubric_five_dimensions=${rubric_five_dimension_count}/${rubric_expected_count} core_flow=service_promotion:${scenario_service_promotion_count} daily_service:${scenario_daily_service_count} sop_docs=${sop_document_count}/5 stored_cases=${case_stored_count}/20 case_unmasked=${case_unmasked_count} ten_sessions=${completed_people}/${pilot_people} org_identity=${org_phone_linked}/${org_active} org_hire_date=${org_hire_date_linked}/${org_active} org_pilot=${org_pilot_people}/20 projects=${org_pilot_projects}/1-2 org_ten_sessions=${org_completed_people}/${org_pilot_people} calibration=${calibration_count}/20 consistency=${calibration_matched}/${calibration_count} sop_usable=${sop_usable}/${sop_total} sop_pending=${sop_pending} satisfaction=${satisfaction_average}/5 responses=${satisfaction_count} audio_schema=${audio_table_count} audio_upload_schema=${audio_upload_table_count} case_media_schema=${case_media_schema_count}/2 case_learning_schema=${case_learning_schema_count}/2 practice_session_schema=${practice_session_schema_count}/8 daily_drill_schema=${daily_drill_schema_count}/6 prompt_templates=${prompt_template_count}/6 prompt_version_schema=${prompt_version_schema_count} requester_identity_schema=${requester_identity_schema_count} hire_date_schema=${hire_date_schema_count} position_sop_schema=${position_sop_schema_count}/4 formal_applicability=${formal_applicability_count} onboard_tasks=${onboard_task_count} qualification_gates=${qualification_gate_count}" [[ "${AIHR_PILOT_STRICT:-false}" != "true" ]] || { (( catalog_schema_count == 3 )) || fail "pilot practice catalog schema incomplete (${catalog_schema_count}/3)" @@ -333,6 +342,7 @@ check_pilot_samples() { (( prompt_version_schema_count == 1 )) || fail "pilot RAG traceability schema unavailable (aihr_sop_answer_review.prompt_version missing)" (( requester_identity_schema_count == 1 )) || fail "pilot SOP requester identity schema unavailable (aihr_sop_answer_review.requester_ext_party_id missing)" (( hire_date_schema_count == 1 )) || fail "pilot org hire_date schema unavailable" + (( position_sop_schema_count == 4 )) || fail "pilot position/SOP/onboard/qualification schema incomplete (${position_sop_schema_count}/4)" (( org_phone_linked >= 20 )) || fail "pilot mobile-login org members ${org_phone_linked}/20 (active org ${org_active})" (( org_hire_date_linked >= 20 )) || fail "pilot org hire_date identities ${org_hire_date_linked}/20" (( org_pilot_people >= 20 )) || fail "pilot activity matched to active org ${org_pilot_people}/20" @@ -604,6 +614,10 @@ contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/control contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrSopDto.java "String position" contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSopController.java "/position-sop" contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSopController.java "@SaCheckLogin" +contains backend/script/sql/update/aihr_20260715_position_sop_qualification_mysql8.sql "aihr_position_responsibility" +contains backend/script/sql/update/aihr_20260715_position_sop_qualification_mysql8.sql "aihr_sop_applicability" +contains backend/script/sql/update/aihr_20260715_position_sop_qualification_mysql8.sql "aihr_onboard_task" +contains backend/script/sql/update/aihr_20260715_position_sop_qualification_mysql8.sql "aihr_qualification_gate" 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, "生活顾问")' diff --git a/scripts/tests/aihr-schema-migrations.test.sh b/scripts/tests/aihr-schema-migrations.test.sh index d0ec1acb..481b21d7 100755 --- a/scripts/tests/aihr-schema-migrations.test.sh +++ b/scripts/tests/aihr-schema-migrations.test.sh @@ -150,6 +150,19 @@ mysql "$DB" < "$ROOT_DIR/backend/script/sql/update/aihr_20260714_org_hire_date_m org_columns="$(mysql "$DB" -N -B -e "SELECT GROUP_CONCAT(column_name ORDER BY ordinal_position) FROM information_schema.columns WHERE table_schema='$DB' AND table_name='aihr_org_snapshot' AND column_name='hire_date';")" test "$org_columns" = 'hire_date' +mysql "$DB" < "$ROOT_DIR/backend/script/sql/update/aihr_20260715_position_sop_qualification_mysql8.sql" +mysql "$DB" < "$ROOT_DIR/backend/script/sql/update/aihr_20260715_position_sop_qualification_mysql8.sql" +position_sop_tables="$(mysql "$DB" -N -B -e "SELECT GROUP_CONCAT(table_name ORDER BY table_name) FROM information_schema.tables WHERE table_schema='$DB' AND table_name IN ('aihr_onboard_task','aihr_position_responsibility','aihr_qualification_gate','aihr_sop_applicability');")" +test "$position_sop_tables" = 'aihr_onboard_task,aihr_position_responsibility,aihr_qualification_gate,aihr_sop_applicability' +position_responsibility_columns="$(mysql "$DB" -N -B -e "SELECT COUNT(*) FROM information_schema.columns WHERE table_schema='$DB' AND table_name='aihr_position_responsibility' AND column_name IN ('position_code','responsibility_text','sop_id','sop_code');")" +test "$position_responsibility_columns" = '4' +sop_applicability_columns="$(mysql "$DB" -N -B -e "SELECT COUNT(*) FROM information_schema.columns WHERE table_schema='$DB' AND table_name='aihr_sop_applicability' AND column_name IN ('sop_id','sop_code','project_type','industry','org_scope');")" +test "$sop_applicability_columns" = '5' +onboard_task_columns="$(mysql "$DB" -N -B -e "SELECT COUNT(*) FROM information_schema.columns WHERE table_schema='$DB' AND table_name='aihr_onboard_task' AND column_name IN ('ext_party_id','position_code','task_type','status','assign_by');")" +test "$onboard_task_columns" = '5' +qualification_gate_columns="$(mysql "$DB" -N -B -e "SELECT COUNT(*) FROM information_schema.columns WHERE table_schema='$DB' AND table_name='aihr_qualification_gate' AND column_name IN ('ext_party_id','position_code','cert_id','passed','valid_thru');")" +test "$qualification_gate_columns" = '5' + mysql -e "CREATE DATABASE \`$LEGACY_DB\` CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;" mysql "$LEGACY_DB" <<'SQL' CREATE TABLE aihr_candidate_material ( @@ -172,4 +185,4 @@ test "$candidate_interview_tables" = 'aihr_candidate_employee_link,aihr_candidat candidate_interview_columns="$(mysql "$LEGACY_DB" -N -B -e "SELECT COUNT(*) FROM information_schema.columns WHERE table_schema='$LEGACY_DB' AND ((table_name = 'aihr_candidate_material' AND column_name IN ('reviewer','reviewed_time')) OR (table_name = 'aihr_interview_result' AND column_name IN ('questions_json','reviewed_score','review_status','reviewer','review_note','reviewed_time')));")" test "$candidate_interview_columns" = '8' -echo "PASS: AIHR legacy schema migrations are idempotent, including practice evidence, knowledge feedback, and candidate interview review fields" +echo "PASS: AIHR legacy schema migrations are idempotent, including practice evidence, knowledge feedback, candidate interview review, and position/SOP qualification contracts" diff --git a/scripts/tests/demo-check-pilot-gates.test.sh b/scripts/tests/demo-check-pilot-gates.test.sh index f5f95c8f..0d861bf8 100755 --- a/scripts/tests/demo-check-pilot-gates.test.sh +++ b/scripts/tests/demo-check-pilot-gates.test.sh @@ -177,6 +177,7 @@ export AIHR_PILOT_DB="$TEST_DB" pilot_output="$(AIHR_PILOT_START_DATE=2026-07-07 AIHR_PILOT_END_DATE=2026-07-10 check_pilot_samples)" [[ "$pilot_output" == *'ten_sessions=1/5'* ]] || { echo "FAIL: 9/10 boundary or raw window count: $pilot_output" >&2; exit 1; } [[ "$pilot_output" == *'catalog_schema=1/3'* ]] || { echo "FAIL: missing catalog schema was not visible: $pilot_output" >&2; exit 1; } +[[ "$pilot_output" == *'position_sop_schema=0/4'* ]] || { echo "FAIL: missing position/SOP qualification schema was not visible: $pilot_output" >&2; exit 1; } [[ "$pilot_output" == *'core_flow=service_promotion:1 daily_service:1'* ]] || { echo "FAIL: BRD core-flow coverage counts: $pilot_output" >&2; exit 1; } [[ "$pilot_output" == *'org_identity=2/6'* ]] || { echo "FAIL: unsafe org identities were not excluded: $pilot_output" >&2; exit 1; } [[ "$pilot_output" == *'org_pilot=2/20'* ]] || { echo "FAIL: collision or dual-key identity was miscounted: $pilot_output" >&2; exit 1; }