# 组织机构导入验证设计方案 **日期**: 2026-02-07 **状态**: 设计完成 **目标**: 确保 org.yml 数据完整、准确地导入到 SQLite,支持幂等性和完整性验证 ## 一、背景与目标 ### 问题描述 项目完成了从 YAML/Excel 到 SQLite 的数据模型迁移,但缺乏完善的验证机制,无法确保: 1. **数量完整性** - YAML 中的数据是否全部导入 2. **关系正确性** - 时间范围内的关系状态是否正确 3. **数据去重** - 多次导入不会产生重复数据 4. **原子性** - 导入过程遇到错误能正确回滚 ### 验证目标 - ✅ **数量验证**: 员工、部门、关系数量与 YAML 一致 - ✅ **关系验证**: 所有时间点的组织关系正确无误 - ✅ **幂等性**: 多次导入不会产生重复数据 - ✅ **冲突检测**: 数据不一致时报错而非静默覆盖 - ✅ **可视化报告**: 提供 Markdown + JSON 双格式报告 ## 二、整体架构 ### 核心组件 ``` ┌─────────────────────────────────────────────────────────────┐ │ org.yml │ └────────────────────────┬────────────────────────────────────┘ │ ▼ ┌───────────────────────────────┐ │ OrganizationImportService │ 【增强幂等性】 │ - 唯一性检查 │ │ - 冲突检测 │ │ - 事务保证 │ └───────────────┬───────────────┘ │ ▼ ┌───────────────┐ │ SQLite 数据库 │ └───────┬───────┘ │ ▼ ┌───────────────────────────────┐ │ OrganizationVerifier │ 【新增验证器】 │ - verifyEmployees() │ │ - verifyDepartments() │ │ - verifyDepartmentRelations()│ │ - verifyEmployeeDeptRelations()│ │ - verifyAtDate() │ └───────────────┬───────────────┘ │ ▼ ┌───────────────────────────────┐ │ VerificationReport │ 【新增报告类】 │ - CategoryResult × 4 │ │ - Summary │ └───────────────┬───────────────┘ │ ▼ ┌───────────────────────────────┐ │ ReportGenerator │ 【新增生成器】 │ - generateMarkdown() │ │ - generateJson() │ └───────────────────────────────┘ ``` ## 三、幂等性保证设计 ### 3.1 唯一性判断规则 #### 员工(EmployeeEntity) ``` 唯一性标识: 姓名 + 所有别名 判断逻辑: 如果姓名或任一别名匹配已有员工,视为同一人 示例: YAML: "潘力;潘总" 数据库已有: name="潘力", aliases=["潘总"] 结果: 视为同一人,跳过 ``` #### 部门(DepartmentEntity) ``` 唯一性标识: 部门名称 判断逻辑: 部门名称完全匹配 示例: YAML: "产品研发中心" 数据库已有: name="产品研发中心" 结果: 视为同一部门,跳过 ``` #### 部门关系(DepartmentRelationEntity) ``` 唯一性标识: 父部门 + 子部门 + 开始日期 判断逻辑: 三者都匹配视为同一关系 示例: YAML: "产品研发中心;测试部;2012-06-01" 数据库已有: parent="产品研发中心", child="测试部", startDate="2012-06-01" 结果: 视为同一关系 冲突检测: - 如果唯一性标识相同,但 endDate 不同 → 报错(数据冲突) ``` #### 人员部门关系(EmployeeDepartmentRelationEntity) ``` 唯一性标识: 员工 + 部门 + 开始日期 判断逻辑: 三者都匹配视为同一关系 冲突检测: - 如果唯一性标识相同,但 endDate 不同 → 报错(数据冲突) ``` ### 3.2 导入策略 ```java 对于每条记录: 1. 查询是否已存在(根据唯一性规则) 2. 如果存在且完全一致: ✅ 跳过(记录到 skipped 计数) 3. 如果存在但数据不一致: ❌ 报错(记录到 conflicts 列表) ❌ 事务回滚,停止导入 4. 如果不存在: ✅ 插入新记录(记录到 inserted 计数) 遇到任何冲突立即停止,保证数据一致性 ``` ### 3.3 ImportResult 扩展 ```java @Data @Builder public class ImportResult { private boolean success; // 是否成功 private int insertedCount; // 新插入的记录数 private int skippedCount; // 跳过的记录数(完全相同) private List conflicts; // 数据冲突列表 private List errors; // 其他错误信息 } @Data @Builder public class DataConflict { private String type; // 冲突类型: "DepartmentRelation" / "EmployeeDepartmentRelation" private String key; // 唯一标识: "产品研发中心 -> 测试部, 2012-06-01" private String yamlValue; // YAML 中的值 private String dbValue; // 数据库中的值 private String field; // 冲突字段: "endDate" } ``` ## 四、完整时间线验证设计 ### 4.1 时间点提取策略 **从 YAML 和数据库中提取所有关键时间点** ``` 关键时间点包括: 1. 所有关系的开始日期(startDate) 2. 所有关系的结束日期(endDate) 3. 开始日期的前一天(验证关系未生效) 4. 结束日期的后一天(验证关系已结束) ``` **示例**: ``` 关系1: 产品研发中心 -> 测试部, 2012-06-01 ~ 无结束 提取时间点: - 2012-05-31 (前一天,关系应不存在) - 2012-06-01 (开始日期,关系应存在) 关系2: 产品研发中心 -> 大数据平台开发部, 2012-01-01 ~ 2023-12-17 提取时间点: - 2011-12-31 (前一天,关系应不存在) - 2012-01-01 (开始,关系应存在) - 2023-12-17 (结束,关系应存在) - 2023-12-18 (后一天,关系应不存在) ``` ### 4.2 验证逻辑 ```java 对于每个时间点 T: 1. 查询 YAML: 哪些关系在 T 应该有效 规则: startDate <= T && (endDate == null || endDate >= T) 2. 查询数据库: 哪些关系在 T 实际有效 使用同样的查询规则 3. 对比结果: - 缺失 (Missing): YAML 有但数据库没有 - 多余 (Extra): 数据库有但 YAML 没有 - 匹配 (Match): 两边都有且一致 4. 记录差异到报告 ``` ### 4.3 验证覆盖范围 ``` 验证项目: ✅ 部门关系在所有关键时间点的正确性 ✅ 人员部门关系在所有关键时间点的正确性 ✅ 关系的开始日期和结束日期准确性 ✅ 边界条件(前一天、后一天)的正确性 ``` ## 五、验证报告设计 ### 5.1 报告数据结构 ```java @Data @Builder public class VerificationReport { private LocalDateTime verificationTime; // 验证时间 private boolean overallSuccess; // 总体是否通过 // 四大类验证结果 private CategoryResult employees; private CategoryResult departments; private CategoryResult departmentRelations; private CategoryResult employeeDepartmentRelations; // 汇总统计 private Summary summary; } @Data @Builder public class CategoryResult { private String category; // 类别名称 private boolean passed; // 是否通过 // 数量统计 private int yamlCount; // YAML 中的数量 private int dbCount; // 数据库中的数量 private boolean countMatch; // 数量是否匹配 // 差异明细 private List missing; // 数据库缺失的记录 private List extra; // 数据库多余的记录 private List mismatches; // 字段不匹配 // 时间点验证结果(仅关系类) private List timePointIssues; } @Data @Builder public class FieldMismatch { private String recordKey; // 记录标识 private String field; // 字段名 private String yamlValue; // YAML 中的值 private String dbValue; // 数据库中的值 } @Data @Builder public class TimePointIssue { private LocalDate date; // 时间点 private String issueType; // "MISSING" / "EXTRA" private String relation; // 关系描述 } @Data @Builder public class Summary { private int totalChecks; // 总检查项 private int passedChecks; // 通过的检查 private int failedChecks; // 失败的检查 private List criticalIssues; // 关键问题列表 } ``` ### 5.2 Markdown 报告格式 ```markdown # 组织机构导入验证报告 **验证时间**: 2026-02-07 15:30:00 **总体结果**: ✅ 通过 / ❌ 失败 --- ## 一、员工验证 - **YAML 数量**: 200 - **数据库数量**: 200 - **状态**: ✅ 通过 --- ## 二、部门验证 - **YAML 数量**: 40 - **数据库数量**: 40 - **状态**: ✅ 通过 --- ## 三、部门关系验证 - **YAML 数量**: 95 - **数据库数量**: 94 - **状态**: ❌ 失败 ### 缺失记录 - 敏捷团队 -> 风暴队, 2025-11-17 ~ null ### 时间点验证问题 - **2025-11-17**: ❌ 缺少关系 "敏捷团队 -> 风暴队" - **2025-11-18**: ❌ 缺少关系 "敏捷团队 -> 风暴队" --- ## 四、人员部门关系验证 - **YAML 数量**: 880 - **数据库数量**: 880 - **状态**: ✅ 通过 --- ## 汇总统计 - **总检查项**: 1215 - **通过**: 1214 - **失败**: 1 - **通过率**: 99.92% ### 关键问题 1. 部门关系缺失: "敏捷团队 -> 风暴队, 2025-11-17 ~ null" ``` ### 5.3 JSON 报告格式 ```json { "verificationTime": "2026-02-07T15:30:00", "overallSuccess": false, "employees": { "category": "员工", "passed": true, "yamlCount": 200, "dbCount": 200, "countMatch": true, "missing": [], "extra": [], "mismatches": [] }, "departmentRelations": { "category": "部门关系", "passed": false, "yamlCount": 95, "dbCount": 94, "countMatch": false, "missing": ["敏捷团队 -> 风暴队, 2025-11-17 ~ null"], "extra": [], "mismatches": [], "timePointIssues": [ { "date": "2025-11-17", "issueType": "MISSING", "relation": "敏捷团队 -> 风暴队" } ] }, "summary": { "totalChecks": 1215, "passedChecks": 1214, "failedChecks": 1, "criticalIssues": [ "部门关系缺失: 敏捷团队 -> 风暴队, 2025-11-17 ~ null" ] } } ``` ## 六、测试策略 ### 6.1 测试用例 #### 1. 基础导入测试 ```java @Test void shouldImportOrganizationDataSuccessfully() { // 清空数据库 cleanDatabase(); // 执行导入 ImportResult result = importService.importFromYaml("org.yml"); // 验证结果 assertThat(result.isSuccess()).isTrue(); assertThat(result.getInsertedCount()).isGreaterThan(0); assertThat(result.getConflicts()).isEmpty(); } ``` #### 2. 幂等性测试 ```java @Test void shouldBeIdempotentWhenImportingTwice() { // 第一次导入 ImportResult result1 = importService.importFromYaml("org.yml"); int firstInserted = result1.getInsertedCount(); // 第二次导入(相同数据) ImportResult result2 = importService.importFromYaml("org.yml"); // 验证幂等性 assertThat(result2.getInsertedCount()).isEqualTo(0); assertThat(result2.getSkippedCount()).isEqualTo(firstInserted); assertThat(result2.isSuccess()).isTrue(); // 验证数据库记录总数不变 long totalEmployees = employeeRepository.count(); assertThat(totalEmployees).isEqualTo(200); // 假设 YAML 有 200 个员工 } ``` #### 3. 数据冲突检测测试 ```java @Test void shouldDetectConflictWhenEndDateDiffers() { // 导入原始数据 importService.importFromYaml("org.yml"); // 修改某个关系的结束日期 String modifiedYaml = modifyRelationEndDate("org.yml", "产品研发中心;测试部;2012-06-01", "2023-12-31"); // 原本无结束日期 // 再次导入 ImportResult result = importService.importFromYaml(modifiedYaml); // 验证冲突检测 assertThat(result.isSuccess()).isFalse(); assertThat(result.getConflicts()).hasSize(1); DataConflict conflict = result.getConflicts().get(0); assertThat(conflict.getType()).isEqualTo("DepartmentRelation"); assertThat(conflict.getField()).isEqualTo("endDate"); assertThat(conflict.getYamlValue()).isEqualTo("2023-12-31"); assertThat(conflict.getDbValue()).isEqualTo("null"); } ``` #### 4. 完整性验证测试 ```java @Test void shouldVerifyAllDataCorrectness() { // 导入数据 ImportResult importResult = importService.importFromYaml("org.yml"); assertThat(importResult.isSuccess()).isTrue(); // 执行验证 VerificationReport report = verifier.verify("org.yml"); // 验证四大类 assertThat(report.getEmployees().isPassed()).isTrue(); assertThat(report.getDepartments().isPassed()).isTrue(); assertThat(report.getDepartmentRelations().isPassed()).isTrue(); assertThat(report.getEmployeeDepartmentRelations().isPassed()).isTrue(); // 验证总体结果 assertThat(report.isOverallSuccess()).isTrue(); // 生成报告文件 reportGenerator.generateMarkdown(report, "target/verification-report.md"); reportGenerator.generateJson(report, "target/verification-report.json"); } ``` #### 5. 时间点验证测试 ```java @Test void shouldVerifyRelationsAtAllCriticalTimepoints() { // 导入数据 importService.importFromYaml("org.yml"); // 加载 YAML 数据 OrganizationData yamlData = yamlDataLoader.load("org.yml"); // 提取所有关键时间点 Set criticalDates = timelineExtractor.extractCriticalDates(yamlData); // 对每个时间点验证 List failures = new ArrayList<>(); for (LocalDate date : criticalDates) { TimePointVerification result = verifier.verifyAtDate(date, yamlData); if (!result.isPassed()) { failures.add(String.format("%s: %s", date, result.getIssues())); } } // 验证所有时间点都通过 assertThat(failures) .withFailMessage("以下时间点验证失败:\n" + String.join("\n", failures)) .isEmpty(); } ``` ### 6.2 测试数据准备 ```java @SpringBootTest @Transactional @TestPropertySource(properties = { "spring.datasource.url=jdbc:sqlite::memory:", "spring.jpa.hibernate.ddl-auto=create-drop" }) class OrganizationImportTest { @Autowired private OrganizationImportService importService; @Autowired private OrganizationVerifier verifier; @Autowired private ReportGenerator reportGenerator; @BeforeEach void setup() { // 每个测试前清空数据库 cleanDatabase(); } private void cleanDatabase() { employeeDepartmentRelationRepository.deleteAll(); departmentRelationRepository.deleteAll(); employeeRepository.deleteAll(); departmentRepository.deleteAll(); } } ``` ## 七、实现计划 ### 阶段 1: 增强导入服务 - [ ] 实现唯一性检查逻辑 - [ ] 实现数据冲突检测 - [ ] 扩展 ImportResult 类 - [ ] 添加事务注解 ### 阶段 2: 创建验证器 - [ ] 创建 OrganizationVerifier 类 - [ ] 实现四大类验证方法 - [ ] 实现时间点提取逻辑 - [ ] 实现时间点验证方法 ### 阶段 3: 创建报告组件 - [ ] 创建报告数据类 - [ ] 创建 ReportGenerator 类 - [ ] 实现 Markdown 生成 - [ ] 实现 JSON 生成 ### 阶段 4: 编写测试 - [ ] 基础导入测试 - [ ] 幂等性测试 - [ ] 冲突检测测试 - [ ] 完整性验证测试 - [ ] 时间点验证测试 ## 八、使用流程 ### 开发阶段 ```bash # 1. 修改 org.yml # 2. 运行测试 ./gradlew test --tests OrganizationImportTest # 3. 查看报告 cat build/reports/verification-report.md ``` ### 手动验证 ```java @SpringBootTest public class ManualVerification { @Autowired private OrganizationImportService importService; @Autowired private OrganizationVerifier verifier; @Autowired private ReportGenerator reportGenerator; @Test void manualVerify() { // 1. 导入 ImportResult importResult = importService.importFromYaml("org.yml"); System.out.println("导入结果: " + importResult); // 2. 验证 VerificationReport report = verifier.verify("org.yml"); // 3. 生成报告 reportGenerator.generateMarkdown(report, "verification-report.md"); reportGenerator.generateJson(report, "verification-report.json"); // 4. 检查结果 if (!report.isOverallSuccess()) { System.err.println("验证失败,请查看报告: verification-report.md"); } } } ``` ## 九、成功标准 ✅ **功能完整性** - 支持 4 类数据验证(员工、部门、部门关系、人员部门关系) - 支持完整时间线验证(所有关键时间点) - 支持幂等性导入(多次导入不重复) - 支持冲突检测(数据不一致时报错) ✅ **报告质量** - Markdown 报告清晰易读 - JSON 报告结构完整 - 差异明细准确定位问题 ✅ **测试覆盖** - 单元测试覆盖所有核心逻辑 - 集成测试验证端到端流程 - 所有测试通过 ✅ **代码质量** - 符合项目编码规范 - 有完善的注释和文档 - 通过代码审查 --- **文档版本**: 1.0 **最后更新**: 2026-02-07 **维护者**: User & Claude