656 lines
18 KiB
Markdown
656 lines
18 KiB
Markdown
# 组织机构导入验证设计方案
|
||
|
||
**日期**: 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
|