junboV2/docs/plans/2026-02-07-organization-import-verification-design.md

656 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 组织机构导入验证设计方案
**日期**: 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<DataConflict> conflicts; // 数据冲突列表
private List<String> 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<String> missing; // 数据库缺失的记录
private List<String> extra; // 数据库多余的记录
private List<FieldMismatch> mismatches; // 字段不匹配
// 时间点验证结果(仅关系类)
private List<TimePointIssue> 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<String> 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<LocalDate> criticalDates = timelineExtractor.extractCriticalDates(yamlData);
// 对每个时间点验证
List<String> 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