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 1c24bb0b..fc4e3da2 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 @@ -1138,7 +1138,7 @@ public class AihrSopSeedService { return Optional.ofNullable(jdbcTemplate.queryForObject(""" select o.ext1, coalesce(k.name, '') as category from aihr_knowledge_attach a - join sys_oss o on o.oss_id = a.oss_id and o.tenant_id = a.tenant_id + join sys_oss o on o.oss_id = a.oss_id and binary o.tenant_id = binary a.tenant_id left join aihr_knowledge_info k on k.id = a.knowledge_id and k.tenant_id = a.tenant_id where a.tenant_id = ? and a.id = ? limit 1 @@ -1169,7 +1169,7 @@ public class AihrSopSeedService { else 'name-size' end as match_type from aihr_knowledge_attach a - join sys_oss o on o.oss_id = a.oss_id and o.tenant_id = a.tenant_id + join sys_oss o on o.oss_id = a.oss_id and binary o.tenant_id = binary a.tenant_id where a.tenant_id = ? and ( json_unquote(json_extract(o.ext1, '$.aihrFileSha256')) = ? @@ -1291,8 +1291,7 @@ public class AihrSopSeedService { } private List processingRows() { - try { - return jdbcTemplate.query(""" + return jdbcTemplate.query(""" select a.id, a.oss_id, i.name as category, @@ -1307,7 +1306,7 @@ public class AihrSopSeedService { from aihr_knowledge_attach a join aihr_knowledge_info i on i.id = a.knowledge_id and i.tenant_id = a.tenant_id left join aihr_knowledge_fragment f on f.tenant_id = a.tenant_id and f.knowledge_id = a.knowledge_id and f.doc_id = a.doc_id - left join sys_oss o on o.oss_id = a.oss_id and o.tenant_id = a.tenant_id + left join sys_oss o on o.oss_id = a.oss_id and binary o.tenant_id = binary a.tenant_id where a.tenant_id = ? group by a.id, a.oss_id, i.name, a.name, a.type, a.status, a.update_time, a.remark, o.ext1 order by a.update_time desc, a.id desc @@ -1332,9 +1331,6 @@ public class AihrSopSeedService { insight.reason() ); }, tenantId()); - } catch (DataAccessException e) { - return List.of(); - } } private ProcessingTaskResponse processingTask(ProcessingTaskRow row) { 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 f041dddc..d3ac99bc 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 @@ -5,6 +5,7 @@ import org.dromara.aihr.domain.AihrSopDto; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; +import org.springframework.dao.DataAccessResourceFailureException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; @@ -18,6 +19,7 @@ import java.util.Set; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; @@ -349,13 +351,27 @@ public class AihrSopSeedServiceTest { } String code = Files.readString(source); - assertTrue(code.contains("join sys_oss o on o.oss_id = a.oss_id and o.tenant_id = a.tenant_id")); - assertTrue(code.contains("left join sys_oss o on o.oss_id = a.oss_id and o.tenant_id = a.tenant_id")); + assertTrue(code.contains("join sys_oss o on o.oss_id = a.oss_id and binary o.tenant_id = binary a.tenant_id")); + assertTrue(code.contains("left join sys_oss o on o.oss_id = a.oss_id and binary o.tenant_id = binary a.tenant_id")); assertTrue(code.contains("left join aihr_knowledge_info k on k.id = a.knowledge_id and k.tenant_id = a.tenant_id")); assertTrue(code.contains("where tenant_id = ? and oss_id = ? limit 1")); assertTrue(code.contains("where tenant_id = ? and oss_id = ?\", objectMapper.writeValueAsString(node), tenantId(), ossId")); } + @Test + @Tag("dev") + @SuppressWarnings({"rawtypes", "unchecked"}) + public void processingOverviewPropagatesDatabaseFailures() { + JdbcTemplate jdbcTemplate = mock(JdbcTemplate.class); + when(jdbcTemplate.query(anyString(), any(RowMapper.class), any(Object[].class))) + .thenThrow(new DataAccessResourceFailureException("collation mismatch")); + AihrModelSeedService modelService = mock(AihrModelSeedService.class); + when(modelService.visionAllowed()).thenReturn(false); + AihrSopSeedService service = new AihrSopSeedService(new ObjectMapper(), jdbcTemplate, null, "", null, null, modelService); + + assertThrows(DataAccessResourceFailureException.class, service::processingOverview); + } + @Test @Tag("dev") public void qdrantRebuildKeepsOtherTenantsVectors() throws Exception { diff --git a/backend/script/sql/aihr_candidate_material_mysql8.sql b/backend/script/sql/aihr_candidate_material_mysql8.sql index e88e43d6..e08fc8fa 100644 --- a/backend/script/sql/aihr_candidate_material_mysql8.sql +++ b/backend/script/sql/aihr_candidate_material_mysql8.sql @@ -2,7 +2,7 @@ CREATE TABLE IF NOT EXISTS `aihr_candidate_material` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', - `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '000000' COMMENT '租户编号', `candidate_id` varchar(80) NOT NULL COMMENT '候选人外部ID', `candidate_name` varchar(100) DEFAULT NULL COMMENT '候选人姓名', `material_type` varchar(50) DEFAULT NULL COMMENT '资料类型', diff --git a/backend/script/sql/aihr_interview_result_mysql8.sql b/backend/script/sql/aihr_interview_result_mysql8.sql index 68bd77f8..5ebe4a72 100644 --- a/backend/script/sql/aihr_interview_result_mysql8.sql +++ b/backend/script/sql/aihr_interview_result_mysql8.sql @@ -3,7 +3,7 @@ CREATE TABLE IF NOT EXISTS `aihr_interview_result` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', - `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '000000' COMMENT '租户编号', `session_id` varchar(100) NOT NULL COMMENT '面试会话ID', `candidate_id` varchar(80) DEFAULT NULL COMMENT '候选人ID', `candidate_name` varchar(100) DEFAULT NULL COMMENT '候选人姓名', @@ -33,7 +33,7 @@ CREATE TABLE IF NOT EXISTS `aihr_interview_result` ( CREATE TABLE IF NOT EXISTS `aihr_candidate_employee_link` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', - `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '000000' COMMENT '租户编号', `candidate_id` varchar(80) NOT NULL COMMENT '本地候选人ID', `ext_party_id` varchar(100) NOT NULL COMMENT '外部员工主体ID', `status` varchar(20) DEFAULT '已关联' COMMENT '关联状态', diff --git a/backend/script/sql/aihr_knowledge_mysql8.sql b/backend/script/sql/aihr_knowledge_mysql8.sql index 4dfa9748..f3360c5f 100644 --- a/backend/script/sql/aihr_knowledge_mysql8.sql +++ b/backend/script/sql/aihr_knowledge_mysql8.sql @@ -3,7 +3,7 @@ CREATE TABLE IF NOT EXISTS `aihr_knowledge_info` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', - `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '000000' COMMENT '租户编号', `name` varchar(100) NOT NULL COMMENT '知识库名称', `description` varchar(1000) DEFAULT NULL COMMENT '知识库描述', `separator` varchar(255) DEFAULT NULL COMMENT '知识分隔符', @@ -28,7 +28,7 @@ CREATE TABLE IF NOT EXISTS `aihr_knowledge_info` ( CREATE TABLE IF NOT EXISTS `aihr_knowledge_attach` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', - `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '000000' COMMENT '租户编号', `knowledge_id` bigint NOT NULL COMMENT '知识库ID', `oss_id` bigint DEFAULT NULL COMMENT '对象存储ID', `doc_id` varchar(32) DEFAULT NULL COMMENT '文档ID', @@ -48,7 +48,7 @@ CREATE TABLE IF NOT EXISTS `aihr_knowledge_attach` ( CREATE TABLE IF NOT EXISTS `aihr_knowledge_fragment` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', - `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '000000' COMMENT '租户编号', `knowledge_id` bigint NOT NULL COMMENT '知识库ID', `idx` int NOT NULL COMMENT '片段序号', `doc_id` varchar(32) DEFAULT NULL COMMENT '文档ID', @@ -70,7 +70,7 @@ CREATE TABLE IF NOT EXISTS `aihr_knowledge_fragment` ( CREATE TABLE IF NOT EXISTS `aihr_knowledge_import_task` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', - `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '000000' COMMENT '租户编号', `directory` varchar(500) NOT NULL COMMENT '导入相对目录', `category` varchar(100) NOT NULL COMMENT '知识库分类', `status` tinyint DEFAULT 0 COMMENT '任务状态: 0等待, 1运行中, 2完成, 3失败, 4取消', @@ -138,7 +138,7 @@ ON DUPLICATE KEY UPDATE -- 知识库批量上传队列:上传秒回后由后台 worker 加工,支持单文件重试 CREATE TABLE IF NOT EXISTS `aihr_knowledge_upload_item` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', - `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '000000' COMMENT '租户编号', `batch_id` varchar(64) NOT NULL COMMENT '上传批次', `file_name` varchar(255) NOT NULL COMMENT '文件名', `category` varchar(100) DEFAULT '' COMMENT '目标分类', diff --git a/backend/script/sql/aihr_model_mysql8.sql b/backend/script/sql/aihr_model_mysql8.sql index 43bec1c2..02efecbe 100644 --- a/backend/script/sql/aihr_model_mysql8.sql +++ b/backend/script/sql/aihr_model_mysql8.sql @@ -3,7 +3,7 @@ CREATE TABLE IF NOT EXISTS `aihr_model_provider` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', - `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '000000' COMMENT '租户编号', `provider_name` varchar(100) NOT NULL COMMENT '供应商名称', `provider_code` varchar(50) NOT NULL COMMENT '供应商编码', `provider_desc` varchar(500) DEFAULT NULL COMMENT '供应商描述', @@ -24,7 +24,7 @@ CREATE TABLE IF NOT EXISTS `aihr_model_provider` ( CREATE TABLE IF NOT EXISTS `aihr_model_config` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', - `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '000000' COMMENT '租户编号', `category` varchar(30) NOT NULL COMMENT '模型类型: chat/vector/rerank', `model_name` varchar(100) NOT NULL COMMENT '模型名称', `provider_code` varchar(50) NOT NULL COMMENT '供应商编码', diff --git a/backend/script/sql/aihr_org_snapshot_mysql8.sql b/backend/script/sql/aihr_org_snapshot_mysql8.sql index 4548716a..55d54f7d 100644 --- a/backend/script/sql/aihr_org_snapshot_mysql8.sql +++ b/backend/script/sql/aihr_org_snapshot_mysql8.sql @@ -6,7 +6,7 @@ DROP TABLE IF EXISTS `aihr_org_snapshot`; CREATE TABLE `aihr_org_snapshot` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', - `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '000000' COMMENT '租户编号', `project_code` varchar(50) NOT NULL COMMENT '项目编码(数据权限范围主体,D1)', `project_name` varchar(100) NOT NULL COMMENT '项目名称', `dept_name` varchar(100) DEFAULT NULL COMMENT '部门/条线', diff --git a/backend/script/sql/aihr_practice_mysql8.sql b/backend/script/sql/aihr_practice_mysql8.sql index 11c240a4..07a9485a 100644 --- a/backend/script/sql/aihr_practice_mysql8.sql +++ b/backend/script/sql/aihr_practice_mysql8.sql @@ -3,7 +3,7 @@ CREATE TABLE IF NOT EXISTS `aihr_case_record` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', - `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '000000' COMMENT '租户编号', `case_id` varchar(80) NOT NULL COMMENT '案例ID', `file_name` varchar(255) DEFAULT NULL COMMENT '来源文件', `project_ext_org_id` varchar(100) DEFAULT NULL COMMENT '项目外部组织ID', @@ -26,7 +26,7 @@ CREATE TABLE IF NOT EXISTS `aihr_case_record` ( CREATE TABLE IF NOT EXISTS `aihr_practice_session` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', - `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '000000' COMMENT '租户编号', `session_id` varchar(100) NOT NULL COMMENT '训练会话ID', `ext_party_id` varchar(100) DEFAULT NULL COMMENT '外部主体或手机号', `trainee_name` varchar(100) DEFAULT NULL COMMENT '学员名称', @@ -64,7 +64,7 @@ CREATE TABLE IF NOT EXISTS `aihr_practice_session` ( CREATE TABLE IF NOT EXISTS `aihr_practice_annotation` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', - `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '000000' COMMENT '租户编号', `session_id` varchar(100) NOT NULL COMMENT '训练会话ID', `turn_index` int NOT NULL COMMENT '话术序号', `source_text` varchar(1000) DEFAULT NULL COMMENT '原话', @@ -80,7 +80,7 @@ CREATE TABLE IF NOT EXISTS `aihr_practice_annotation` ( CREATE TABLE IF NOT EXISTS `aihr_practice_audio` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', - `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '000000' COMMENT '租户编号', `session_id` varchar(100) NOT NULL COMMENT '训练会话ID', `turn_index` int NOT NULL COMMENT '话术序号', `role` varchar(30) NOT NULL COMMENT '音频角色', @@ -95,7 +95,7 @@ CREATE TABLE IF NOT EXISTS `aihr_practice_audio` ( CREATE TABLE IF NOT EXISTS `aihr_practice_audio_upload` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', - `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '000000' COMMENT '租户编号', `oss_id` bigint NOT NULL COMMENT 'OSS文件ID', `owner_user_id` bigint NOT NULL COMMENT '上传用户ID', `owner_identity` varchar(100) NOT NULL COMMENT '上传主体', @@ -111,7 +111,7 @@ CREATE TABLE IF NOT EXISTS `aihr_practice_audio_upload` ( CREATE TABLE IF NOT EXISTS `aihr_practice_assignment` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', - `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '000000' COMMENT '租户编号', `ext_party_id` varchar(100) NOT NULL COMMENT '员工外部主体或手机号', `trainee_name` varchar(100) DEFAULT NULL COMMENT '员工姓名', `scenario_id` varchar(80) NOT NULL COMMENT '场景ID', @@ -136,7 +136,7 @@ CREATE TABLE IF NOT EXISTS `aihr_practice_assignment` ( CREATE TABLE IF NOT EXISTS `aihr_practice_calibration` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', - `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '000000' COMMENT '租户编号', `session_id` varchar(100) NOT NULL COMMENT '训练会话ID', `reviewer` varchar(100) DEFAULT NULL COMMENT '校准人', `original_score` int DEFAULT NULL COMMENT 'AI原始总分', @@ -152,7 +152,7 @@ CREATE TABLE IF NOT EXISTS `aihr_practice_calibration` ( CREATE TABLE IF NOT EXISTS `aihr_practice_scenario` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', - `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '000000' COMMENT '租户编号', `scenario_code` varchar(80) NOT NULL COMMENT '场景编码', `scenario_name` varchar(100) NOT NULL COMMENT '场景名称', `position` varchar(80) NOT NULL COMMENT '岗位', @@ -184,7 +184,7 @@ CREATE TABLE IF NOT EXISTS `aihr_practice_scenario` ( CREATE TABLE IF NOT EXISTS `aihr_practice_rubric` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', - `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '000000' COMMENT '租户编号', `rubric_code` varchar(100) NOT NULL COMMENT 'Rubric编码', `scenario_code` varchar(80) NOT NULL COMMENT '场景编码', `rubric_name` varchar(100) NOT NULL COMMENT 'Rubric名称', @@ -200,7 +200,7 @@ CREATE TABLE IF NOT EXISTS `aihr_practice_rubric` ( CREATE TABLE IF NOT EXISTS `aihr_practice_rubric_dimension` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', - `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '000000' COMMENT '租户编号', `rubric_code` varchar(100) NOT NULL COMMENT 'Rubric编码', `dimension_code` varchar(50) NOT NULL COMMENT '维度编码', `dimension_name` varchar(50) NOT NULL COMMENT '维度名称', @@ -216,7 +216,7 @@ CREATE TABLE IF NOT EXISTS `aihr_practice_rubric_dimension` ( CREATE TABLE IF NOT EXISTS `aihr_prompt_template` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', - `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '000000' COMMENT '租户编号', `code` varchar(80) NOT NULL COMMENT '模板编码', `name` varchar(100) NOT NULL COMMENT '模板名称', `category` varchar(50) NOT NULL COMMENT '业务分类', @@ -236,7 +236,7 @@ CREATE TABLE IF NOT EXISTS `aihr_prompt_template` ( CREATE TABLE IF NOT EXISTS `aihr_knowledge_gap` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', - `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '000000' COMMENT '租户编号', `query_text` varchar(1000) NOT NULL COMMENT '未命中问题', `category` varchar(100) DEFAULT NULL COMMENT '知识分类', `position` varchar(80) DEFAULT NULL COMMENT '岗位', @@ -249,7 +249,7 @@ CREATE TABLE IF NOT EXISTS `aihr_knowledge_gap` ( CREATE TABLE IF NOT EXISTS `aihr_sop_answer_review` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', - `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '000000' COMMENT '租户编号', `query_text` varchar(1000) NOT NULL COMMENT '问题', `category` varchar(100) DEFAULT NULL COMMENT '知识分类', `answer_text` varchar(2000) DEFAULT NULL COMMENT '回答', @@ -271,7 +271,7 @@ CREATE TABLE IF NOT EXISTS `aihr_sop_answer_review` ( CREATE TABLE IF NOT EXISTS `aihr_knowledge_answer_feedback` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', - `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '000000' COMMENT '租户编号', `query_text` varchar(1000) NOT NULL COMMENT '问题原文', `query_norm` varchar(500) NOT NULL COMMENT '规范化问题', `fragment_id` bigint NOT NULL COMMENT '知识片段ID', @@ -468,7 +468,7 @@ ON DUPLICATE KEY UPDATE -- 不写入业务数据;与 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 '租户编号', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci 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主键(跨模块引用)', @@ -485,7 +485,7 @@ CREATE TABLE IF NOT EXISTS `aihr_position_responsibility` ( CREATE TABLE IF NOT EXISTS `aihr_sop_applicability` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', - `tenant_id` varchar(20) NOT NULL DEFAULT '000000' COMMENT '租户编号', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci 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 '项目类型:住宅/公建/景区等', @@ -503,7 +503,7 @@ CREATE TABLE IF NOT EXISTS `aihr_sop_applicability` ( CREATE TABLE IF NOT EXISTS `aihr_onboard_task` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', - `tenant_id` varchar(20) NOT NULL DEFAULT '000000' COMMENT '租户编号', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci 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 '岗位编码', @@ -525,7 +525,7 @@ CREATE TABLE IF NOT EXISTS `aihr_onboard_task` ( CREATE TABLE IF NOT EXISTS `aihr_qualification_gate` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', - `tenant_id` varchar(20) NOT NULL DEFAULT '000000' COMMENT '租户编号', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci 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 '岗位编码', diff --git a/backend/script/sql/update/aihr_20260714_practice_catalog_schema_mysql8.sql b/backend/script/sql/update/aihr_20260714_practice_catalog_schema_mysql8.sql index 3e0d7ec3..71232239 100644 --- a/backend/script/sql/update/aihr_20260714_practice_catalog_schema_mysql8.sql +++ b/backend/script/sql/update/aihr_20260714_practice_catalog_schema_mysql8.sql @@ -3,7 +3,7 @@ CREATE TABLE IF NOT EXISTS `aihr_practice_scenario` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', - `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '000000' COMMENT '租户编号', `scenario_code` varchar(80) NOT NULL COMMENT '场景编码', `scenario_name` varchar(100) NOT NULL COMMENT '场景名称', `position` varchar(80) NOT NULL COMMENT '岗位', @@ -35,7 +35,7 @@ CREATE TABLE IF NOT EXISTS `aihr_practice_scenario` ( CREATE TABLE IF NOT EXISTS `aihr_practice_rubric` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', - `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '000000' COMMENT '租户编号', `rubric_code` varchar(100) NOT NULL COMMENT 'Rubric编码', `scenario_code` varchar(80) NOT NULL COMMENT '场景编码', `rubric_name` varchar(100) NOT NULL COMMENT 'Rubric名称', @@ -51,7 +51,7 @@ CREATE TABLE IF NOT EXISTS `aihr_practice_rubric` ( CREATE TABLE IF NOT EXISTS `aihr_practice_rubric_dimension` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', - `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '000000' COMMENT '租户编号', `rubric_code` varchar(100) NOT NULL COMMENT 'Rubric编码', `dimension_code` varchar(50) NOT NULL COMMENT '维度编码', `dimension_name` varchar(50) NOT NULL COMMENT '维度名称', diff --git a/backend/script/sql/update/aihr_20260714_practice_evidence_mysql8.sql b/backend/script/sql/update/aihr_20260714_practice_evidence_mysql8.sql index 4d03cfba..24af9e65 100644 --- a/backend/script/sql/update/aihr_20260714_practice_evidence_mysql8.sql +++ b/backend/script/sql/update/aihr_20260714_practice_evidence_mysql8.sql @@ -3,7 +3,7 @@ CREATE TABLE IF NOT EXISTS `aihr_practice_audio` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', - `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '000000' COMMENT '租户编号', `session_id` varchar(100) NOT NULL COMMENT '训练会话ID', `turn_index` int NOT NULL COMMENT '话术序号', `role` varchar(30) NOT NULL COMMENT '音频角色', @@ -18,7 +18,7 @@ CREATE TABLE IF NOT EXISTS `aihr_practice_audio` ( CREATE TABLE IF NOT EXISTS `aihr_practice_audio_upload` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', - `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '000000' COMMENT '租户编号', `oss_id` bigint NOT NULL COMMENT 'OSS文件ID', `owner_user_id` bigint NOT NULL COMMENT '上传用户ID', `owner_identity` varchar(100) NOT NULL COMMENT '上传主体', @@ -34,7 +34,7 @@ CREATE TABLE IF NOT EXISTS `aihr_practice_audio_upload` ( CREATE TABLE IF NOT EXISTS `aihr_practice_calibration` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', - `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '000000' COMMENT '租户编号', `session_id` varchar(100) NOT NULL COMMENT '训练会话ID', `reviewer` varchar(100) DEFAULT NULL COMMENT '校准人', `original_score` int DEFAULT NULL COMMENT 'AI原始总分', diff --git a/backend/script/sql/update/aihr_20260714_practice_five_dimensions_mysql8.sql b/backend/script/sql/update/aihr_20260714_practice_five_dimensions_mysql8.sql index 1e9e944c..fbc73112 100644 --- a/backend/script/sql/update/aihr_20260714_practice_five_dimensions_mysql8.sql +++ b/backend/script/sql/update/aihr_20260714_practice_five_dimensions_mysql8.sql @@ -57,7 +57,7 @@ EXECUTE aihr_response_latency_stmt; DEALLOCATE PREPARE aihr_response_latency_stmt; CREATE TEMPORARY TABLE IF NOT EXISTS aihr_brd_rubric_codes ( - tenant_id varchar(20) NOT NULL, + tenant_id varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, rubric_code varchar(100) NOT NULL, PRIMARY KEY (tenant_id, rubric_code) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; diff --git a/backend/script/sql/update/aihr_20260715_candidate_interview_review_mysql8.sql b/backend/script/sql/update/aihr_20260715_candidate_interview_review_mysql8.sql index 06ec6ab0..807f5dd0 100644 --- a/backend/script/sql/update/aihr_20260715_candidate_interview_review_mysql8.sql +++ b/backend/script/sql/update/aihr_20260715_candidate_interview_review_mysql8.sql @@ -3,7 +3,7 @@ CREATE TABLE IF NOT EXISTS `aihr_candidate_material` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', - `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '000000' COMMENT '租户编号', `candidate_id` varchar(80) NOT NULL COMMENT '候选人外部ID', `candidate_name` varchar(100) DEFAULT NULL COMMENT '候选人姓名', `material_type` varchar(50) DEFAULT NULL COMMENT '资料类型', @@ -56,7 +56,7 @@ DEALLOCATE PREPARE aihr_candidate_material_reviewed_time_stmt; CREATE TABLE IF NOT EXISTS `aihr_interview_result` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', - `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '000000' COMMENT '租户编号', `session_id` varchar(100) NOT NULL COMMENT '面试会话ID', `candidate_id` varchar(80) DEFAULT NULL COMMENT '候选人ID', `candidate_name` varchar(100) DEFAULT NULL COMMENT '候选人姓名', @@ -182,7 +182,7 @@ DEALLOCATE PREPARE aihr_interview_reviewed_time_stmt; CREATE TABLE IF NOT EXISTS `aihr_candidate_employee_link` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', - `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '000000' COMMENT '租户编号', `candidate_id` varchar(80) NOT NULL COMMENT '本地候选人ID', `ext_party_id` varchar(100) NOT NULL COMMENT '外部员工主体ID', `status` varchar(20) DEFAULT '已关联' COMMENT '关联状态', diff --git a/backend/script/sql/update/aihr_20260715_knowledge_feedback_mysql8.sql b/backend/script/sql/update/aihr_20260715_knowledge_feedback_mysql8.sql index 715b6fa3..d958e8bd 100644 --- a/backend/script/sql/update/aihr_20260715_knowledge_feedback_mysql8.sql +++ b/backend/script/sql/update/aihr_20260715_knowledge_feedback_mysql8.sql @@ -3,7 +3,7 @@ CREATE TABLE IF NOT EXISTS `aihr_knowledge_gap` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', - `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '000000' COMMENT '租户编号', `query_text` varchar(1000) NOT NULL COMMENT '未命中问题', `category` varchar(100) DEFAULT NULL COMMENT '知识分类', `position` varchar(80) DEFAULT NULL COMMENT '岗位', @@ -16,7 +16,7 @@ CREATE TABLE IF NOT EXISTS `aihr_knowledge_gap` ( CREATE TABLE IF NOT EXISTS `aihr_knowledge_answer_feedback` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', - `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '000000' COMMENT '租户编号', `query_text` varchar(1000) NOT NULL COMMENT '问题原文', `query_norm` varchar(500) NOT NULL COMMENT '规范化问题', `fragment_id` bigint NOT NULL COMMENT '知识片段ID', 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 index a344e8f1..d17efab6 100644 --- a/backend/script/sql/update/aihr_20260715_position_sop_qualification_mysql8.sql +++ b/backend/script/sql/update/aihr_20260715_position_sop_qualification_mysql8.sql @@ -4,7 +4,7 @@ CREATE TABLE IF NOT EXISTS `aihr_position_responsibility` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', - `tenant_id` varchar(20) NOT NULL DEFAULT '000000' COMMENT '租户编号', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci 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主键(跨模块引用)', @@ -21,7 +21,7 @@ CREATE TABLE IF NOT EXISTS `aihr_position_responsibility` ( CREATE TABLE IF NOT EXISTS `aihr_sop_applicability` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', - `tenant_id` varchar(20) NOT NULL DEFAULT '000000' COMMENT '租户编号', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci 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 '项目类型:住宅/公建/景区等', @@ -39,7 +39,7 @@ CREATE TABLE IF NOT EXISTS `aihr_sop_applicability` ( CREATE TABLE IF NOT EXISTS `aihr_onboard_task` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', - `tenant_id` varchar(20) NOT NULL DEFAULT '000000' COMMENT '租户编号', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci 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 '岗位编码', @@ -61,7 +61,7 @@ CREATE TABLE IF NOT EXISTS `aihr_onboard_task` ( CREATE TABLE IF NOT EXISTS `aihr_qualification_gate` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', - `tenant_id` varchar(20) NOT NULL DEFAULT '000000' COMMENT '租户编号', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci 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 '岗位编码', diff --git a/backend/script/sql/update/aihr_20260715_tenant_collation_mysql8.sql b/backend/script/sql/update/aihr_20260715_tenant_collation_mysql8.sql new file mode 100644 index 00000000..c6c4e2ce --- /dev/null +++ b/backend/script/sql/update/aihr_20260715_tenant_collation_mysql8.sql @@ -0,0 +1,70 @@ +-- Keep AIHR tenant identifiers compatible with the framework tenant contract. +-- This migration derives the target charset/collation from sys_oss.tenant_id, +-- preserves each AIHR tenant_id column's nullability/default/comment, and is idempotent. + +DROP PROCEDURE IF EXISTS aihr_align_tenant_collation; + +DELIMITER $$ + +CREATE PROCEDURE aihr_align_tenant_collation() +BEGIN + DECLARE done int DEFAULT 0; + DECLARE target_charset varchar(64); + DECLARE target_collation varchar(64); + DECLARE current_table varchar(64); + DECLARE current_type text; + DECLARE current_nullable varchar(3); + DECLARE current_default text; + DECLARE current_comment text; + + DECLARE tenant_columns CURSOR FOR + SELECT table_name, column_type, is_nullable, column_default, column_comment + FROM information_schema.columns + WHERE table_schema = DATABASE() + AND LEFT(table_name, 5) = 'aihr_' + AND column_name = 'tenant_id' + AND collation_name <> target_collation + ORDER BY table_name; + DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1; + + SELECT character_set_name, collation_name + INTO target_charset, target_collation + FROM information_schema.columns + WHERE table_schema = DATABASE() + AND table_name = 'sys_oss' + AND column_name = 'tenant_id' + LIMIT 1; + + IF target_charset IS NULL OR target_collation IS NULL THEN + SIGNAL SQLSTATE '45000' + SET MESSAGE_TEXT = 'sys_oss.tenant_id charset/collation contract is missing'; + END IF; + + OPEN tenant_columns; + align_loop: LOOP + FETCH tenant_columns + INTO current_table, current_type, current_nullable, current_default, current_comment; + IF done = 1 THEN + LEAVE align_loop; + END IF; + + SET @aihr_tenant_ddl = CONCAT( + 'ALTER TABLE `', REPLACE(current_table, '`', '``'), + '` MODIFY COLUMN `tenant_id` ', current_type, + ' CHARACTER SET `', REPLACE(target_charset, '`', '``'), + '` COLLATE `', REPLACE(target_collation, '`', '``'), '`', + IF(current_nullable = 'YES', ' NULL', ' NOT NULL'), + IF(current_default IS NULL, '', CONCAT(' DEFAULT ', QUOTE(current_default))), + ' COMMENT ', QUOTE(COALESCE(current_comment, '')) + ); + PREPARE aihr_tenant_statement FROM @aihr_tenant_ddl; + EXECUTE aihr_tenant_statement; + DEALLOCATE PREPARE aihr_tenant_statement; + END LOOP; + CLOSE tenant_columns; +END$$ + +DELIMITER ; + +CALL aihr_align_tenant_collation(); +DROP PROCEDURE aihr_align_tenant_collation; diff --git a/docs/BRD_PRODUCTION_MIGRATION_RUNBOOK.md b/docs/BRD_PRODUCTION_MIGRATION_RUNBOOK.md index a6c1d515..cf00597b 100644 --- a/docs/BRD_PRODUCTION_MIGRATION_RUNBOOK.md +++ b/docs/BRD_PRODUCTION_MIGRATION_RUNBOOK.md @@ -11,12 +11,13 @@ - 对练五维、每日三题评分模式、SOP 提问主体/版本、组织入职日期字段尚未全部迁移; - `aihr_prompt_template` 表存在,但内置模板数量为 `0`; - `aihr_candidate_material` 缺少 `reviewer/reviewed_time`,`aihr_interview_result` 缺少人工复核字段和 `questions_json`,`aihr_candidate_employee_link` 尚不存在;这些候选人/HR 复核链路不能依赖首次请求触发懒迁移; +- AIHR 表的 `tenant_id` 曾使用 `utf8mb4_0900_ai_ci`,框架 `sys_oss.tenant_id` 使用 `utf8mb4_general_ci`;跨模块等值连接会报 `Illegal mix of collations`,不能靠查询吞错或单次 `binary` 兼容长期掩盖; - 这些事实不能由应用启动时的懒迁移或页面 fallback 代替。 ## 发布前置条件 1. 确认发布窗口、DB 备份和回滚负责人;迁移脚本包含 `ALTER TABLE`,不能在无备份状态执行。 -2. 先执行 `scripts/release-preflight.sh` 的只读检查;生产静态资源、后端包和当前提交必须一致。若要同时核对线上结构,追加 `RELEASE_VERIFY_REMOTE_SCHEMA=true`,该开关只读检查必需表,不执行迁移。 +2. 先执行 `scripts/release-preflight.sh` 的本地产物检查。生产迁移后、后端发布完成后,必须追加 `RELEASE_VERIFY_REMOTE_SCHEMA=true RELEASE_VERIFY_REMOTE_BACKEND=true` 再做线上核验;该门禁会检查必需表、所有 AIHR `tenant_id` 与 `sys_oss.tenant_id` 的排序规则,以及资料处理实际连接 SQL。 3. 确认迁移目标库、租户和字符集为 MySQL 8 / `utf8mb4`;脚本只允许在目标业务库执行。 4. 生产组织同步凭据、AI 供应商留存/费用口径和正式试点窗口仍未闭合时,不要把迁移完成宣称为 BRD 验收完成。 @@ -40,9 +41,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_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 +mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260715_tenant_collation_mysql8.sql ``` -顺序原因:场景补充依赖三张场景/Rubric 基础表;五维迁移依赖场景和 Rubric;岗位/SOP/任务/资格迁移只补正式数据契约,不写业务行,也不代表岗位适用范围、任务规则或资格标准已经获得 HR 确认。住宅 SOP 和 Prompt 迁移只补内置内容,不代表内容已完成业务复核。 +顺序原因:场景补充依赖三张场景/Rubric 基础表;五维迁移依赖场景和 Rubric;租户排序规则迁移必须最后执行,以覆盖本批次新建的所有 AIHR 表。岗位/SOP/任务/资格迁移只补正式数据契约,不写业务行,也不代表岗位适用范围、任务规则或资格标准已经获得 HR 确认。住宅 SOP 和 Prompt 迁移只补内置内容,不代表内容已完成业务复核。 ## 迁移后只读核验 @@ -84,9 +86,20 @@ SELECT COUNT(*) AS built_in_prompt_templates FROM aihr_prompt_template WHERE tenant_id = '000000' AND code IN ('summary_card', 'sop_answer', 'mentor_rewrite', 'review_annotation', 'prep_card', 'supervision_image_analysis_maintenance'); + +SELECT aihr.table_name, aihr.collation_name, core.collation_name AS expected_collation +FROM information_schema.columns aihr +JOIN information_schema.columns core + ON core.table_schema = aihr.table_schema + AND core.table_name = 'sys_oss' + AND core.column_name = 'tenant_id' +WHERE aihr.table_schema = DATABASE() + AND LEFT(aihr.table_name, 5) = 'aihr_' + AND aihr.column_name = 'tenant_id' + AND aihr.collation_name <> core.collation_name; ``` -预期是三张场景/Rubric 表、三张证据表、两张知识治理表、三张候选人/面试表和四张岗位/SOP/任务/资格契约表均存在,会话证据字段为 `8/8`,候选资料审核字段为 `2/2`,面试复核字段为 `6/6`,字段查询覆盖所有列,内置 Prompt 数量为 `6`。岗位/SOP/任务/资格表为空是允许的;这只证明 schema/seed 迁移完成,不证明正式岗位规则、资格 gating 或试点完成。 +预期是三张场景/Rubric 表、三张证据表、两张知识治理表、三张候选人/面试表和四张岗位/SOP/任务/资格契约表均存在,会话证据字段为 `8/8`,候选资料审核字段为 `2/2`,面试复核字段为 `6/6`,字段查询覆盖所有列,内置 Prompt 数量为 `6`,排序规则差异查询返回 `0` 行。岗位/SOP/任务/资格表为空是允许的;这只证明 schema/seed 迁移完成,不证明正式岗位规则、资格 gating 或试点完成。 ## 迁移后应用回归 diff --git a/docs/DEV_SETUP.md b/docs/DEV_SETUP.md index 84d92a31..ff11791d 100644 --- a/docs/DEV_SETUP.md +++ b/docs/DEV_SETUP.md @@ -169,11 +169,11 @@ curl -k -s https://peilian.njzhmj.top/h5/ | sed -n '1,20p' RELEASE_REMOTE_URL=https://peilian.njzhmj.top ./scripts/release-preflight.sh # 发布后核验线上主资源是否与当前本地产物一致 RELEASE_REMOTE_URL=https://peilian.njzhmj.top RELEASE_VERIFY_REMOTE_MATCH=true ./scripts/release-preflight.sh -# 同时通过 SSH 校验线上后端 jar;默认使用本机 SSH alias YCWY;比较 ruoyi-aihr 模块内容,避免 ZIP 打包时间戳造成误报 -RELEASE_REMOTE_URL=https://peilian.njzhmj.top RELEASE_VERIFY_REMOTE_MATCH=true RELEASE_VERIFY_REMOTE_BACKEND=true ./scripts/release-preflight.sh +# 同时校验线上 schema、资料处理连接 SQL 与后端 jar;后端发布验收不得省略 schema 门禁 +RELEASE_REMOTE_URL=https://peilian.njzhmj.top RELEASE_VERIFY_REMOTE_MATCH=true RELEASE_VERIFY_REMOTE_SCHEMA=true RELEASE_VERIFY_REMOTE_BACKEND=true ./scripts/release-preflight.sh ``` -`RELEASE_VERIFY_REMOTE_BACKEND=true` 必须与 `RELEASE_VERIFY_REMOTE_MATCH=true` 同时使用;预检会同时打印整包 jar SHA-256 和 `ruoyi-aihr` 模块内容 SHA-256,实际匹配以模块内容 hash 为准。远端后端默认核对 `/opt/wygj/app/ruoyi-admin.jar`,如发布路径不同可通过 `RELEASE_REMOTE_BACKEND_PATH` 覆盖;路径必须是安全的绝对路径。 +`RELEASE_VERIFY_REMOTE_BACKEND=true` 必须与 `RELEASE_VERIFY_REMOTE_MATCH=true`、`RELEASE_VERIFY_REMOTE_SCHEMA=true` 同时使用;预检会同时打印整包 jar SHA-256 和 `ruoyi-aihr` 模块内容 SHA-256,实际匹配以模块内容 hash 为准。schema 门禁会拒绝 AIHR 与框架租户字段排序规则不一致,并执行资料处理页依赖的 `aihr_knowledge_attach`/`sys_oss` 连接检查。远端后端默认核对 `/opt/wygj/app/ruoyi-admin.jar`,如发布路径不同可通过 `RELEASE_REMOTE_BACKEND_PATH` 覆盖;路径必须是安全的绝对路径。 带 `RELEASE_REMOTE_URL` 时,预检同时校验租户接口 JSON 的业务 `code=200`;HTTP 200 但业务返回 401/405 会判定失败。 diff --git a/scripts/release-preflight.sh b/scripts/release-preflight.sh index 8de90123..d04fac0a 100755 --- a/scripts/release-preflight.sh +++ b/scripts/release-preflight.sh @@ -68,6 +68,9 @@ grep -q '/h5/' "$mobile_index" || fail "mobile H5 index does not contain the /h5 if [[ "${RELEASE_VERIFY_REMOTE_BACKEND:-false}" == "true" && "${RELEASE_VERIFY_REMOTE_MATCH:-false}" != "true" ]]; then fail "RELEASE_VERIFY_REMOTE_BACKEND=true requires RELEASE_VERIFY_REMOTE_MATCH=true" fi +if [[ "${RELEASE_VERIFY_REMOTE_BACKEND:-false}" == "true" && "${RELEASE_VERIFY_REMOTE_SCHEMA:-false}" != "true" ]]; then + fail "RELEASE_VERIFY_REMOTE_BACKEND=true requires RELEASE_VERIFY_REMOTE_SCHEMA=true" +fi if [[ "${RELEASE_VERIFY_REMOTE_SCHEMA:-false}" == "true" && "${RELEASE_VERIFY_REMOTE_MATCH:-false}" != "true" ]]; then fail "RELEASE_VERIFY_REMOTE_SCHEMA=true requires RELEASE_VERIFY_REMOTE_MATCH=true" fi @@ -111,12 +114,12 @@ require_remote_schema() { local remote_db="${RELEASE_REMOTE_DB_NAME:-ry-vue}" [[ "$remote_db" =~ ^[A-Za-z0-9_-]+$ ]] || fail "remote database name is invalid: $remote_db" - local missing - missing="$(ssh -o BatchMode=yes -o ConnectTimeout=10 "$remote_ssh" bash -s -- "$remote_db" <<'REMOTE' + local issues + issues="$(ssh -o BatchMode=yes -o ConnectTimeout=10 "$remote_ssh" bash -s -- "$remote_db" <<'REMOTE' set -euo pipefail db="$1" mysql --batch --skip-column-names --connect-timeout=5 "$db" <<'SQL' -SELECT required.table_name +SELECT CONCAT('missing_table:', required.table_name) AS issue FROM ( SELECT 'aihr_prompt_template' AS table_name UNION ALL SELECT 'aihr_practice_scenario' @@ -138,13 +141,54 @@ FROM ( LEFT JOIN information_schema.tables actual ON actual.table_schema = DATABASE() AND actual.table_name = required.table_name WHERE actual.table_name IS NULL -ORDER BY required.table_name; +UNION ALL +SELECT 'missing_column:sys_oss.tenant_id' +WHERE NOT EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_schema = DATABASE() + AND table_name = 'sys_oss' + AND column_name = 'tenant_id' +) +UNION ALL +SELECT CONCAT( + 'tenant_collation:', aihr.table_name, '.', aihr.column_name, + '=', COALESCE(aihr.collation_name, 'NULL'), + ',expected=', core.collation_name +) +FROM information_schema.columns aihr +JOIN information_schema.columns core + ON core.table_schema = aihr.table_schema + AND core.table_name = 'sys_oss' + AND core.column_name = 'tenant_id' +WHERE aihr.table_schema = DATABASE() + AND LEFT(aihr.table_name, 5) = 'aihr_' + AND aihr.column_name = 'tenant_id' + AND aihr.collation_name <> core.collation_name +ORDER BY issue; SQL REMOTE )" || fail "remote schema check failed: $remote_ssh:$remote_db" - [[ -z "$missing" ]] || fail "remote schema missing required tables: $(printf '%s' "$missing" | tr '\n' ', ' | sed 's/, $//')" + [[ -z "$issues" ]] || fail "remote schema incompatible: $(printf '%s' "$issues" | tr '\n' ', ' | sed 's/, $//')" + + local processing_join_count + processing_join_count="$(ssh -o BatchMode=yes -o ConnectTimeout=10 "$remote_ssh" bash -s -- "$remote_db" <<'REMOTE' +set -euo pipefail +db="$1" +mysql --batch --skip-column-names --connect-timeout=5 "$db" <<'SQL' +SELECT COUNT(*) +FROM aihr_knowledge_attach a +LEFT JOIN sys_oss o + ON o.oss_id = a.oss_id + AND o.tenant_id = a.tenant_id; +SQL +REMOTE + )" || fail "remote processing join check failed: $remote_ssh:$remote_db" + [[ "$processing_join_count" =~ ^[0-9]+$ ]] || fail "remote processing join count is invalid: $processing_join_count" echo "remote_schema=16/16 $remote_ssh:$remote_db" + echo "remote_tenant_collation=aligned" + echo "remote_processing_join_count=$processing_join_count" } changed_files="$(git status --porcelain)" diff --git a/scripts/tests/aihr-schema-migrations.test.sh b/scripts/tests/aihr-schema-migrations.test.sh index 481b21d7..66b58cf8 100755 --- a/scripts/tests/aihr-schema-migrations.test.sh +++ b/scripts/tests/aihr-schema-migrations.test.sh @@ -11,6 +11,17 @@ MYSQL_CONTAINER="wygj-mysql" DB="wygj_aihr_schema_test_$$" LEGACY_DB="wygj_aihr_candidate_legacy_test_$$" +tenant_contract_violations="$({ + find "$ROOT_DIR/backend/script/sql" -maxdepth 2 -type f -name 'aihr_*_mysql8.sql' -print0 \ + | xargs -0 rg -n '^[[:space:]]*`?tenant_id`?[[:space:]]+varchar\(20\)' \ + | grep -v 'COLLATE utf8mb4_general_ci' +} || true)" +if [[ -n "$tenant_contract_violations" ]]; then + echo "FAIL: AIHR tenant_id DDL must use the framework utf8mb4_general_ci contract:" >&2 + printf '%s\n' "$tenant_contract_violations" >&2 + exit 1 +fi + while IFS= read -r migration; do migration_name="$(basename "$migration")" grep -Fq "backend/script/sql/update/$migration_name" "$ROOT_DIR/docs/BRD_PRODUCTION_MIGRATION_RUNBOOK.md" || { @@ -185,4 +196,30 @@ 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, candidate interview review, and position/SOP qualification contracts" +mysql "$LEGACY_DB" <<'SQL' +CREATE TABLE sys_oss ( + oss_id bigint NOT NULL, + tenant_id varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '000000', + PRIMARY KEY (oss_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; +CREATE TABLE aihr_collation_probe ( + id bigint NOT NULL, + oss_id bigint NOT NULL, + tenant_id varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT '000000', + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +INSERT INTO sys_oss (oss_id, tenant_id) VALUES (1, '000000'); +INSERT INTO aihr_collation_probe (id, oss_id, tenant_id) VALUES (1, 1, '000000'); +SQL +if mysql "$LEGACY_DB" -N -B -e "SELECT COUNT(*) FROM aihr_collation_probe a JOIN sys_oss o ON o.oss_id=a.oss_id AND o.tenant_id=a.tenant_id;" >/dev/null 2>&1; then + echo "FAIL: mixed-collation tenant join unexpectedly succeeded before migration" >&2 + exit 1 +fi +mysql "$LEGACY_DB" < "$ROOT_DIR/backend/script/sql/update/aihr_20260715_tenant_collation_mysql8.sql" +mysql "$LEGACY_DB" < "$ROOT_DIR/backend/script/sql/update/aihr_20260715_tenant_collation_mysql8.sql" +mismatched_tenant_columns="$(mysql "$LEGACY_DB" -N -B -e "SELECT COUNT(*) FROM information_schema.columns WHERE table_schema='$LEGACY_DB' AND table_name LIKE 'aihr\\_%' AND column_name='tenant_id' AND collation_name <> 'utf8mb4_general_ci';")" +test "$mismatched_tenant_columns" = '0' +collation_join_count="$(mysql "$LEGACY_DB" -N -B -e "SELECT COUNT(*) FROM aihr_collation_probe a JOIN sys_oss o ON o.oss_id=a.oss_id AND o.tenant_id=a.tenant_id;")" +test "$collation_join_count" = '1' + +echo "PASS: AIHR legacy schema migrations are idempotent, including tenant collation compatibility"