# 阶段二实施总结 - 部门外键关联 > **实施日期**: 2025-12-27 > **状态**: 代码已完成,待测试 --- ## ✅ 已完成的工作 ### 1. Schema 更新 **文件**: `prisma/schema.prisma` **User 模型变更**: - ✅ 添加 `departmentId` 字段(可选,外键) - ✅ 添加 `departmentRef` 关联(`Department` 模型) - ✅ 保留 `department` 字段(向后兼容) - ✅ 添加 `departmentId` 索引 **Department 模型变更**: - ✅ 添加 `users` 关联(`User[]`) **影响评估**: - ✅ **低风险**: 只添加字段,不删除字段 - ✅ **向后兼容**: 所有现有代码继续工作 - ✅ **可选字段**: `departmentId` 可为空,不影响现有数据 --- ### 2. 代码更新(双写双读) #### 2.1 更新用户部门 API (`app/api/admin/users/update-department/route.ts`) **变更内容**: - ✅ 双写:同时更新 `department`(旧字段)和 `departmentId`(新字段) - ✅ 根据部门名称查找部门ID - ✅ 如果部门不存在,清除 `departmentId` **代码逻辑**: ```typescript // 如果提供了部门名称,查找对应的部门ID if (department && department.trim() !== '') { const dept = await prisma.department.findUnique({ where: { name: department.trim() } }); if (dept) { updateData.departmentId = dept.id; // 新字段 } else { updateData.departmentId = null; // 部门不存在 } } ``` --- #### 2.2 钉钉完善资料 API (`app/api/dingtalk/complete-profile/route.ts`) **变更内容**: - ✅ 双写:同时更新 `department` 和 `departmentId` - ✅ 根据部门名称查找部门ID - ✅ 返回数据包含 `departmentRef` 关联信息 --- #### 2.3 用户列表 API (`app/api/admin/users/route.ts`) **变更内容**: - ✅ 返回 `departmentId` 字段 - ✅ 返回 `departmentRef` 关联信息(包含部门ID和名称) - ✅ 保留 `department` 字段(向后兼容) --- #### 2.4 部门权限辅助函数 (`lib/department-permission.ts`) **新增函数**: - ✅ `getUserDepartmentName()`: 获取用户部门名称(双读) - 优先使用 `departmentRef.name` - 回退到 `department` 字段 **现有函数**: - ✅ 保持不变,继续使用 `department` 字符串参数 - ✅ 向后兼容,不影响现有功能 --- ### 3. 迁移脚本 **文件**: `scripts/migrate-department-to-id-safe.ts` **功能**: - ✅ 确保所有用户部门都在 Department 表中 - ✅ 为每个用户设置 `departmentId`(基于 `department` 名称匹配) - ✅ 验证数据一致性 - ✅ 幂等性:可以重复执行 --- ### 4. 验证脚本 **文件**: `scripts/verify-migration.ts` **阶段二验证内容**: - ✅ 检查 `departmentId` 字段是否存在 - ✅ 验证所有有部门名称的用户都有 `departmentId` - ✅ 验证 `departmentId` 指向的部门存在 - ✅ 验证 `department` 和 `departmentRef.name` 一致 --- ## 🔄 双写双读策略 ### 双写策略 **所有更新用户部门的操作都同时更新两个字段**: ```typescript // 示例:更新用户部门 const updateData = { department: departmentName, // 旧字段(向后兼容) departmentId: departmentId, // 新字段(逐步迁移) }; ``` ### 双读策略 **优先使用新字段,回退到旧字段**: ```typescript // 示例:获取用户部门 const departmentName = user.departmentRef?.name || user.department || null; ``` --- ## 📋 待测试项 ### 功能测试 - [ ] 更新用户部门(管理员操作) - [ ] 钉钉用户完善资料(选择部门) - [ ] 用户列表API返回新字段 - [ ] 数据迁移脚本执行 - [ ] 数据一致性验证 ### 兼容性测试 - [ ] 旧代码(只更新 `department`)仍然工作 - [ ] Session 中的 `department` 字段正常 - [ ] 权限检查逻辑正常(使用 `department` 字符串) ### 数据验证 - [ ] 所有有部门名称的用户都有 `departmentId` - [ ] `departmentId` 指向的部门存在 - [ ] `department` 和 `departmentRef.name` 一致 --- ## 🚨 注意事项 ### 1. Session 中的部门信息 **当前实现**: - Session 中仍然使用 `department` 字符串字段 - 权限检查函数使用 `department` 字符串参数 - **不需要修改**,因为权限检查函数已经支持字符串参数 **未来优化**(可选): - 可以在 Session 中添加 `departmentId` 字段 - 更新权限检查函数,优先使用 `departmentId` ### 2. 部门不存在的情况 **处理逻辑**: - 如果用户有部门名称,但 Department 表中不存在该部门 - `departmentId` 设置为 `null` - `department` 字段保留原值(向后兼容) **建议**: - 运行迁移脚本前,先运行 `migrate-departments-from-users.ts` - 确保所有用户部门都在 Department 表中 ### 3. 性能考虑 **当前实现**: - 更新用户部门时,需要查询 Department 表 - 用户列表查询时,包含 `departmentRef` 关联查询 **优化建议**(未来): - 添加缓存机制 - 批量查询优化 --- ## 📝 下一步 1. **在影子系统测试** - 运行 Schema 更新:`npx prisma db push` - 运行迁移脚本:`npx tsx scripts/migrate-department-to-id-safe.ts` - 运行验证脚本:`npx tsx scripts/verify-migration.ts 2` - 按照测试指南进行功能测试 2. **记录问题** - 发现问题时,记录到 `docs/shadow-testing-issues.md` - 记录解决方案和修复步骤 3. **验证通过后** - 可以考虑实施阶段三(部门层级结构) - 或者先部署阶段一和阶段二到正式系统 --- **文档版本**: 1.0 **创建日期**: 2025-12-27 **最后更新**: 2025-12-27