# 部门隔离方案优化建议 ## 📋 当前实现分析 ### 现有功能 1. **部门权限配置** - 通过 `DepartmentMaterialPermission` 表配置部门对素材类型的权限 - 支持:`canView`(可见)、`canDownload`(可下载)、`canShare`(可分享) - 支持权限继承(从父部门继承) - 支持权限关联(关联到其他部门) 2. **部门层级结构** - 支持父子部门关系(`parentId`) - 支持部门层级(`level`) - 支持部门管理员(`managerId`) 3. **权限检查** - 基于素材类型(`materialType`)的权限过滤 - 用户只能看到有权限的素材类型 ### 当前限制 1. **数据隔离粒度** - 当前是基于**素材类型**的隔离,不是基于**素材本身**的隔离 - 同一素材类型下,所有有权限的部门都能看到所有素材 2. **素材归属** - 素材表(`Material`)没有直接的部门字段 - 只能通过 `userId` → `User.departmentId` 间接关联 3. **跨部门访问** - 没有明确的跨部门访问控制 - 共享功能可能绕过权限限制 ## 🎯 优化建议 ### 方案一:基于素材归属的严格隔离(推荐) **核心思想**:每个素材明确归属于一个部门,用户只能看到自己部门(及有权限的子部门)的素材。 #### 1.1 Schema 改进 ```prisma model Material { // ... 现有字段 ... // 新增:素材归属部门(明确归属) ownerDepartmentId String? ownerDepartment Department? @relation("MaterialOwnerDepartment", fields: [ownerDepartmentId], references: [id]) // 新增:可访问部门(白名单,可选) // 如果为空,则只有归属部门及其子部门可访问 accessibleDepartmentIds String[] // 部门ID数组(PostgreSQL 数组) @@index([ownerDepartmentId]) @@index([accessibleDepartmentIds]) } model Department { // ... 现有字段 ... // 新增关联 ownedMaterials Material[] @relation("MaterialOwnerDepartment") } ``` #### 1.2 权限检查逻辑 ```typescript // 伪代码 async function canUserAccessMaterial(userId: string, materialId: string): Promise { const user = await getUser(userId); const material = await getMaterial(materialId); // 1. 超级管理员可以访问所有 if (await isSuperAdmin(userId)) return true; // 2. 素材创建者可以访问自己的素材 if (material.userId === userId) return true; // 3. 检查素材归属部门 if (!material.ownerDepartmentId) { // 没有归属部门,使用创建者的部门 const creatorDept = await getUserDepartment(material.userId); material.ownerDepartmentId = creatorDept?.departmentId; } // 4. 用户部门是否在可访问范围内 const userDeptId = user.departmentId; if (!userDeptId) return false; // 5. 检查用户部门是否是归属部门或其子部门 if (isDepartmentOrDescendant(material.ownerDepartmentId, userDeptId)) { // 6. 检查素材类型的权限 return await checkMaterialTypePermission(userDeptId, material.materialType); } // 7. 检查是否在白名单中 if (material.accessibleDepartmentIds?.includes(userDeptId)) { return await checkMaterialTypePermission(userDeptId, material.materialType); } return false; } ``` #### 1.3 查询过滤 ```typescript // 在 GET /api/materials 中 async function getMaterialsForUser(userId: string, filters: any) { const user = await getUser(userId); if (await isSuperAdmin(userId)) { // 超级管理员看到所有 return getAllMaterials(filters); } const userDeptId = user.departmentId; if (!userDeptId) return []; // 获取用户部门及其所有子部门 const accessibleDeptIds = await getDepartmentAndDescendants(userDeptId); const where = { ...filters, OR: [ // 1. 素材归属于用户部门或其子部门 { ownerDepartmentId: { in: accessibleDeptIds } }, // 2. 素材在白名单中包含用户部门 { accessibleDepartmentIds: { has: userDeptId } }, // 3. 用户自己创建的素材(兜底) { userId: userId } ], // 同时检查素材类型权限 materialType: { in: await getUserViewableMaterialTypes(userDeptId) } }; return prisma.material.findMany({ where }); } ``` #### 优点 - ✅ 数据隔离清晰,每个素材有明确的归属 - ✅ 支持跨部门共享(通过白名单) - ✅ 支持部门层级权限继承 - ✅ 性能好(有索引支持) #### 缺点 - ⚠️ 需要数据迁移(为现有素材设置 `ownerDepartmentId`) - ⚠️ Schema 变更需要谨慎处理 --- ### 方案二:基于用户部门的动态隔离 **核心思想**:保持现有 Schema,通过查询时动态过滤来实现隔离。 #### 2.1 查询优化 ```typescript // 在 GET /api/materials 中 async function getMaterialsForUser(userId: string, filters: any) { const user = await getUserWithDepartment(userId); if (await isSuperAdmin(userId)) { return getAllMaterials(filters); } const userDeptId = user.departmentId; if (!userDeptId) return []; // 获取用户部门及其子部门的用户ID const accessibleDeptIds = await getDepartmentAndDescendants(userDeptId); const creatorUserIds = await prisma.user.findMany({ where: { departmentId: { in: accessibleDeptIds } }, select: { id: true } }).then(users => users.map(u => u.id)); const where = { ...filters, userId: { in: creatorUserIds }, // 只显示这些用户创建的素材 materialType: { in: await getUserViewableMaterialTypes(userDeptId) } }; return prisma.material.findMany({ where }); } ``` #### 优点 - ✅ 无需 Schema 变更 - ✅ 实现简单,改动小 - ✅ 向后兼容 #### 缺点 - ⚠️ 查询性能可能较差(需要通过用户表关联) - ⚠️ 跨部门共享不够灵活 - ⚠️ 逻辑不够直观 --- ### 方案三:混合方案(推荐用于渐进式迁移) **核心思想**:先使用方案二快速实现,逐步迁移到方案一。 #### 3.1 迁移步骤 1. **阶段一**:实施方案二(基于用户部门的动态过滤) - 无需 Schema 变更 - 快速上线 - 验证隔离效果 2. **阶段二**:添加 `ownerDepartmentId` 字段(可选) - 新上传的素材自动设置 - 现有素材逐步迁移 - 保持向后兼容 3. **阶段三**:完全迁移到方案一 - 所有素材都有 `ownerDepartmentId` - 优化查询性能 - 支持更灵活的权限控制 --- ## 🔐 权限控制细节 ### 1. 上传时的部门归属 ```typescript // 上传素材时 async function createMaterial(userId: string, materialData: any) { const user = await getUser(userId); return prisma.material.create({ data: { ...materialData, userId, // 自动设置归属部门 ownerDepartmentId: user.departmentId || null, } }); } ``` ### 2. 跨部门共享 ```typescript // 共享素材到其他部门 async function shareMaterialToDepartments( materialId: string, departmentIds: string[], userId: string // 操作人 ) { // 权限检查:只有素材创建者或部门管理员可以共享 const material = await getMaterial(materialId); const user = await getUser(userId); if (material.userId !== userId && !await isDepartmentManager(userId, material.ownerDepartmentId)) { throw new Error('无权限共享此素材'); } // 更新可访问部门列表 return prisma.material.update({ where: { id: materialId }, data: { accessibleDepartmentIds: departmentIds } }); } ``` ### 3. 下载权限检查 ```typescript // 下载时额外检查下载权限 async function canUserDownloadMaterial(userId: string, materialId: string): Promise { // 1. 先检查访问权限 if (!await canUserAccessMaterial(userId, materialId)) { return false; } // 2. 检查下载权限(素材类型级别) const material = await getMaterial(materialId); const user = await getUser(userId); const userDeptId = user.departmentId; const permission = await getDepartmentPermission(userDeptId, material.materialType); return permission?.canDownload ?? false; } ``` --- ## 📊 数据迁移建议 ### 迁移脚本 ```typescript // scripts/migrate-material-department.ts async function migrateMaterialDepartments() { const materials = await prisma.material.findMany({ include: { user: { select: { departmentId: true } } } }); for (const material of materials) { if (!material.ownerDepartmentId && material.user.departmentId) { await prisma.material.update({ where: { id: material.id }, data: { ownerDepartmentId: material.user.departmentId } }); } } } ``` --- ## 🎨 UI/UX 建议 ### 1. 部门筛选器 在素材列表页面添加部门筛选: - 显示用户有权限的部门 - 默认显示本部门及子部门 - 支持多选 ### 2. 素材详情页 显示素材归属信息: - 所属部门 - 可访问部门(如果已共享) - 共享按钮(如果有权限) ### 3. 上传表单 自动填充部门信息: - 显示当前用户部门 - 可选:允许选择归属部门(需权限) --- ## ⚠️ 注意事项 1. **性能优化** - 为 `ownerDepartmentId` 和 `accessibleDepartmentIds` 添加索引 - 考虑使用 Redis 缓存部门层级关系 - 批量查询时使用 `include` 避免 N+1 问题 2. **数据一致性** - 用户转部门时,需要考虑其创建的素材归属 - 部门删除时,需要处理素材归属(建议软删除) 3. **权限边界** - 明确区分"访问权限"和"下载权限" - 共享链接可能需要特殊处理(绕过部门隔离?) 4. **审计日志** - 记录跨部门访问 - 记录权限变更操作 --- ## 📝 总结 **推荐方案**:方案三(混合方案,渐进式迁移) 1. **短期**:实施方案二,快速实现部门隔离 2. **中期**:添加 `ownerDepartmentId` 字段,逐步迁移 3. **长期**:完善权限控制,支持更灵活的数据共享 这样可以在不影响现有功能的情况下,逐步完善部门隔离机制。