510 lines
16 KiB
TypeScript
510 lines
16 KiB
TypeScript
import { prisma } from './prisma';
|
||
import { filterSelectableMaterialTypes, MATERIAL_TYPE_OPTIONS } from './material-type-options';
|
||
import { getCompatibleVideoMaterialTypes, isVideoMaterialType } from './video-material-types';
|
||
|
||
/**
|
||
* 判断是否是超级管理员(从数据库读取)
|
||
*/
|
||
export async function isSuperAdmin(userId: string | null | undefined): Promise<boolean> {
|
||
if (!userId) {
|
||
return false;
|
||
}
|
||
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: userId },
|
||
select: { isSuperAdmin: true },
|
||
});
|
||
|
||
return user?.isSuperAdmin ?? false;
|
||
}
|
||
|
||
/**
|
||
* 同步版本的超级管理员判断(用于已经获取到用户信息的情况)
|
||
*/
|
||
export function isSuperAdminSync(isSuperAdmin: boolean): boolean {
|
||
return isSuperAdmin;
|
||
}
|
||
|
||
/**
|
||
* 角色权限检查辅助函数(阶段四新增)
|
||
*/
|
||
|
||
/**
|
||
* 判断是否是系统管理员(包括ADMIN和SYSTEM_ADMIN,向后兼容)
|
||
*/
|
||
export function isSystemAdmin(role: string | null | undefined): boolean {
|
||
if (!role) return false;
|
||
// ADMIN 视为 SYSTEM_ADMIN(向后兼容)
|
||
return role === 'ADMIN' || role === 'SYSTEM_ADMIN';
|
||
}
|
||
|
||
/**
|
||
* 判断是否是部门管理员
|
||
*/
|
||
export function isDepartmentAdmin(role: string | null | undefined): boolean {
|
||
if (!role) return false;
|
||
return role === 'DEPT_ADMIN';
|
||
}
|
||
|
||
/**
|
||
* 判断是否是管理员(系统管理员或部门管理员)
|
||
*/
|
||
export function isAdmin(role: string | null | undefined): boolean {
|
||
if (!role) return false;
|
||
// 包括所有管理员角色:ADMIN(兼容)、SYSTEM_ADMIN、DEPT_ADMIN
|
||
return isSystemAdmin(role) || isDepartmentAdmin(role);
|
||
}
|
||
|
||
/**
|
||
* 判断是否有系统管理权限(系统管理员或超级管理员)
|
||
*/
|
||
export function hasSystemAdminPermission(
|
||
role: string | null | undefined,
|
||
isSuperAdminFlag: boolean = false
|
||
): boolean {
|
||
return isSuperAdminFlag || isSystemAdmin(role);
|
||
}
|
||
|
||
/**
|
||
* 判断是否有部门管理权限(部门管理员、系统管理员或超级管理员)
|
||
*/
|
||
export function hasDepartmentAdminPermission(
|
||
role: string | null | undefined,
|
||
isSuperAdminFlag: boolean = false
|
||
): boolean {
|
||
return isSuperAdminFlag || isSystemAdmin(role) || isDepartmentAdmin(role);
|
||
}
|
||
|
||
/**
|
||
* 获取当前超级管理员数量
|
||
*/
|
||
export async function getSuperAdminCount(): Promise<number> {
|
||
return await prisma.user.count({
|
||
where: { isSuperAdmin: true },
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 检查是否可以添加新的超级管理员(最多4个)
|
||
*/
|
||
export async function canAddSuperAdmin(): Promise<boolean> {
|
||
const count = await getSuperAdminCount();
|
||
return count < 4;
|
||
}
|
||
|
||
// 默认素材类型列表(作为后备,用于新部门创建默认权限配置时)
|
||
// 如果数据库中已有素材,会优先使用数据库中的素材类型
|
||
// 已按分类排序
|
||
export const DEFAULT_MATERIAL_TYPES = MATERIAL_TYPE_OPTIONS;
|
||
|
||
/**
|
||
* 从数据库中获取所有素材类型(去重)
|
||
*/
|
||
export async function getAllMaterialTypes(): Promise<string[]> {
|
||
try {
|
||
// 从materials表中查询所有不重复的materialType
|
||
const materials = await prisma.material.findMany({
|
||
select: {
|
||
materialType: true,
|
||
},
|
||
distinct: ['materialType'],
|
||
});
|
||
|
||
const dbTypes = materials.map((m) => m.materialType).filter(Boolean);
|
||
|
||
// 合并数据库中的类型和默认类型,去重
|
||
const allTypesSet = new Set([...dbTypes, ...DEFAULT_MATERIAL_TYPES]);
|
||
|
||
// 按照 DEFAULT_MATERIAL_TYPES 的顺序排序,未在列表中的类型放在最后
|
||
const sortedTypes = [
|
||
...DEFAULT_MATERIAL_TYPES.filter(type => allTypesSet.has(type)),
|
||
...Array.from(allTypesSet).filter(type => !DEFAULT_MATERIAL_TYPES.includes(type as any)).sort()
|
||
];
|
||
|
||
return sortedTypes;
|
||
} catch (error) {
|
||
console.error('[getAllMaterialTypes] Error:', error);
|
||
// 如果查询失败,返回默认类型列表
|
||
return [...DEFAULT_MATERIAL_TYPES];
|
||
}
|
||
}
|
||
|
||
export interface DepartmentPermission {
|
||
department: string;
|
||
materialType: string;
|
||
canView: boolean;
|
||
canDownload: boolean;
|
||
canShare: boolean;
|
||
canDownloadSource: boolean;
|
||
linkedToDepartment: string | null;
|
||
}
|
||
|
||
async function findDepartmentPermissionRecord(
|
||
department: string,
|
||
materialType: string
|
||
) {
|
||
const normalizedType = String(materialType || '').trim();
|
||
const lookupTypes = isVideoMaterialType(normalizedType)
|
||
? getCompatibleVideoMaterialTypes(normalizedType)
|
||
: [normalizedType];
|
||
|
||
for (const lookupType of lookupTypes) {
|
||
const permission = await prisma.departmentMaterialPermission.findUnique({
|
||
where: {
|
||
department_materialType: {
|
||
department,
|
||
materialType: lookupType,
|
||
},
|
||
},
|
||
});
|
||
|
||
if (permission) {
|
||
return permission;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 获取部门的实际权限(考虑关联部门和权限继承)
|
||
*
|
||
* @param department 部门名称
|
||
* @param materialType 素材类型
|
||
* @param departments 部门列表(用于权限继承检查,可选)
|
||
* @param descendantMap 部门后裔映射(可选,用于加速权限继承检查)
|
||
* @returns 部门权限配置
|
||
*/
|
||
export async function getDepartmentPermission(
|
||
department: string,
|
||
materialType: string,
|
||
departments?: Array<{ id: string; name: string; parentId?: string | null }>,
|
||
descendantMap?: Map<string, Set<string>>
|
||
): Promise<DepartmentPermission | null> {
|
||
// 先查询当前部门的权限配置
|
||
const permission = await findDepartmentPermissionRecord(department, materialType);
|
||
|
||
// 如果有关联部门,从关联部门读取权限
|
||
if (permission?.linkedToDepartment) {
|
||
const linkedPermission = await findDepartmentPermissionRecord(
|
||
permission.linkedToDepartment,
|
||
materialType
|
||
);
|
||
|
||
if (linkedPermission) {
|
||
return {
|
||
department,
|
||
materialType,
|
||
canView: linkedPermission.canView,
|
||
canDownload: linkedPermission.canDownload,
|
||
canShare: linkedPermission.canShare,
|
||
canDownloadSource: linkedPermission.canDownloadSource,
|
||
linkedToDepartment: permission.linkedToDepartment,
|
||
};
|
||
}
|
||
}
|
||
|
||
// 如果当前部门有明确的权限配置,直接返回
|
||
if (permission) {
|
||
return {
|
||
department: permission.department,
|
||
materialType: permission.materialType,
|
||
canView: permission.canView,
|
||
canDownload: permission.canDownload,
|
||
canShare: permission.canShare,
|
||
canDownloadSource: permission.canDownloadSource,
|
||
linkedToDepartment: permission.linkedToDepartment,
|
||
};
|
||
}
|
||
|
||
// 权限继承:如果没有找到当前部门的权限配置,尝试从父部门继承
|
||
if (departments && departments.length > 0) {
|
||
const { buildDepartmentDescendants, isDepartmentWithinScope } = await import('@/lib/departments');
|
||
|
||
// 构建部门映射
|
||
const deptMap = new Map<string, { id: string; name: string; parentId?: string | null }>();
|
||
departments.forEach(dept => {
|
||
deptMap.set(dept.name, dept);
|
||
});
|
||
|
||
// 获取当前部门的父部门链
|
||
const currentDept = deptMap.get(department);
|
||
if (currentDept?.parentId) {
|
||
// 查找父部门
|
||
const parentDept = departments.find(d => d.id === currentDept.parentId);
|
||
if (parentDept) {
|
||
console.log(`[Permission Inheritance] 部门 ${department} 无权限配置,尝试从父部门 ${parentDept.name} 继承 ${materialType} 权限`);
|
||
|
||
// 递归获取父部门的权限
|
||
const parentPermission = await getDepartmentPermission(
|
||
parentDept.name,
|
||
materialType,
|
||
departments,
|
||
descendantMap
|
||
);
|
||
|
||
// 如果父部门有权限配置,继承父部门的权限
|
||
if (parentPermission) {
|
||
console.log(`[Permission Inheritance] 成功从父部门 ${parentDept.name} 继承权限: canDownload=${parentPermission.canDownload}`);
|
||
return {
|
||
department,
|
||
materialType,
|
||
canView: parentPermission.canView,
|
||
canDownload: parentPermission.canDownload,
|
||
canShare: parentPermission.canShare,
|
||
canDownloadSource: parentPermission.canDownloadSource,
|
||
linkedToDepartment: parentPermission.linkedToDepartment || parentDept.name,
|
||
};
|
||
} else {
|
||
console.log(`[Permission Inheritance] 父部门 ${parentDept.name} 也没有 ${materialType} 权限配置`);
|
||
}
|
||
} else {
|
||
console.log(`[Permission Inheritance] 找不到父部门 (parentId=${currentDept.parentId})`);
|
||
}
|
||
} else {
|
||
console.log(`[Permission Inheritance] 部门 ${department} 没有父部门`);
|
||
}
|
||
}
|
||
|
||
// 如果都没有找到,返回 null(表示没有权限配置)
|
||
console.log(`[Permission Inheritance] 部门 ${department} 对于 ${materialType} 没有找到任何权限配置`);
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 获取部门所有素材类型的权限(支持权限继承)
|
||
*
|
||
* @param department 部门名称
|
||
* @param departments 部门列表(用于权限继承检查,可选)
|
||
* @param materialTypes 素材类型列表(可选,如果不提供则从数据库获取)
|
||
* @returns 部门所有素材类型的权限配置
|
||
*/
|
||
export async function getDepartmentPermissions(
|
||
department: string,
|
||
departments?: Array<{ id: string; name: string; parentId?: string | null }>,
|
||
materialTypes?: string[]
|
||
): Promise<Record<string, DepartmentPermission>> {
|
||
// 如果没有提供素材类型列表,从数据库获取所有素材类型
|
||
const allMaterialTypes = materialTypes || await getAllMaterialTypes();
|
||
|
||
// 获取部门后裔映射(用于加速权限继承检查)
|
||
const { buildDepartmentDescendants } = await import('@/lib/departments');
|
||
const descendantMap = departments && departments.length > 0
|
||
? buildDepartmentDescendants(departments)
|
||
: undefined;
|
||
|
||
const result: Record<string, DepartmentPermission> = {};
|
||
|
||
// 为每种素材类型获取权限(包括继承的权限)
|
||
for (const materialType of allMaterialTypes) {
|
||
const permission = await getDepartmentPermission(
|
||
department,
|
||
materialType,
|
||
departments,
|
||
descendantMap
|
||
);
|
||
|
||
if (permission) {
|
||
result[materialType] = permission;
|
||
}
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* 获取用户部门名称(双读:优先使用 departmentId,回退到 department)
|
||
* 阶段二新增:支持外键关联
|
||
*/
|
||
export async function getUserDepartmentName(
|
||
user: { department?: string | null; departmentId?: string | null; departmentRef?: { name: string } | null } | null | undefined
|
||
): Promise<string | null> {
|
||
if (!user) {
|
||
return null;
|
||
}
|
||
|
||
// 优先使用新字段(departmentId + departmentRef)
|
||
if (user.departmentRef?.name) {
|
||
return user.departmentRef.name;
|
||
}
|
||
|
||
// 回退到旧字段(department)
|
||
return user.department || null;
|
||
}
|
||
|
||
/**
|
||
* 获取用户有查看权限的素材类型列表(支持权限继承)
|
||
*/
|
||
export async function getUserViewableMaterialTypes(
|
||
userDepartment: string | null | undefined,
|
||
userRole: string,
|
||
managedDepartment: string | null | undefined,
|
||
userId?: string | null | undefined
|
||
): Promise<string[]> {
|
||
// 超级管理员可以看到所有素材类型
|
||
if (userId && await isSuperAdmin(userId)) {
|
||
return await getAllMaterialTypes();
|
||
}
|
||
|
||
// 如果没有部门信息,返回空数组
|
||
const department = managedDepartment || userDepartment;
|
||
if (!department) {
|
||
return [];
|
||
}
|
||
|
||
// 获取部门列表(用于权限继承检查)
|
||
const departments = await prisma.department.findMany({
|
||
select: { id: true, name: true, parentId: true },
|
||
});
|
||
|
||
// 获取该部门所有素材类型的权限(包括继承的权限)
|
||
const permissions = await getDepartmentPermissions(department, departments);
|
||
|
||
// 获取有查看权限的素材类型
|
||
const viewableTypes = Object.values(permissions)
|
||
.filter((p) => p.canView)
|
||
.map((p) => p.materialType);
|
||
|
||
// 按照 DEFAULT_MATERIAL_TYPES 的顺序排序
|
||
const sortedTypes = [
|
||
...DEFAULT_MATERIAL_TYPES.filter(type => viewableTypes.includes(type as string)),
|
||
...viewableTypes.filter(type => !DEFAULT_MATERIAL_TYPES.includes(type as any)).sort()
|
||
];
|
||
|
||
return sortedTypes;
|
||
}
|
||
|
||
/**
|
||
* 检查用户是否有查看权限(支持权限继承)
|
||
*/
|
||
export async function canUserViewMaterial(
|
||
userDepartment: string | null | undefined,
|
||
userRole: string,
|
||
managedDepartment: string | null | undefined,
|
||
materialType: string,
|
||
userId?: string | null | undefined
|
||
): Promise<boolean> {
|
||
// 超级管理员可以查看所有素材
|
||
if (userId && await isSuperAdmin(userId)) {
|
||
return true;
|
||
}
|
||
|
||
// 如果没有部门信息,不允许查看
|
||
const department = managedDepartment || userDepartment;
|
||
if (!department) {
|
||
return false;
|
||
}
|
||
|
||
// 获取部门列表(用于权限继承检查)
|
||
const departments = await prisma.department.findMany({
|
||
select: { id: true, name: true, parentId: true },
|
||
});
|
||
|
||
const permission = await getDepartmentPermission(department, materialType, departments);
|
||
return permission?.canView ?? false;
|
||
}
|
||
|
||
/**
|
||
* 检查用户是否有下载权限(支持权限继承)
|
||
*/
|
||
export async function canUserDownloadMaterial(
|
||
userDepartment: string | null | undefined,
|
||
userRole: string,
|
||
managedDepartment: string | null | undefined,
|
||
materialType: string,
|
||
userId?: string | null | undefined
|
||
): Promise<boolean> {
|
||
// 超级管理员可以下载所有素材
|
||
if (userId && await isSuperAdmin(userId)) {
|
||
console.log(`[Permission] 超级管理员 ${userId} 允许下载 ${materialType}`);
|
||
return true;
|
||
}
|
||
|
||
// 如果没有部门信息,不允许下载
|
||
const department = managedDepartment || userDepartment;
|
||
if (!department) {
|
||
console.log(`[Permission] 用户无部门信息,拒绝下载 ${materialType}`);
|
||
return false;
|
||
}
|
||
|
||
// 获取部门列表(用于权限继承检查)
|
||
const departments = await prisma.department.findMany({
|
||
select: { id: true, name: true, parentId: true },
|
||
});
|
||
|
||
const permission = await getDepartmentPermission(department, materialType, departments);
|
||
|
||
// 增强日志:记录权限检查结果
|
||
console.log(`[Permission] 用户部门: ${department}, 素材类型: ${materialType}, 权限: ${permission?.canDownload ?? false}, 继承自: ${permission?.linkedToDepartment || '无'}`);
|
||
|
||
return permission?.canDownload ?? false;
|
||
}
|
||
|
||
/**
|
||
* 为部门创建默认权限配置
|
||
* 使用数据库中实际存在的所有素材类型
|
||
*/
|
||
export async function createDefaultPermissionsForDepartment(
|
||
department: string
|
||
): Promise<void> {
|
||
const existingPermissions = await prisma.departmentMaterialPermission.findMany({
|
||
where: { department },
|
||
});
|
||
|
||
const existingTypes = new Set(existingPermissions.map((p: any) => p.materialType));
|
||
|
||
// 获取数据库中所有素材类型
|
||
const allMaterialTypes = filterSelectableMaterialTypes(await getAllMaterialTypes());
|
||
|
||
const newPermissions = allMaterialTypes
|
||
.filter((type) => !existingTypes.has(type))
|
||
.map((materialType) => ({
|
||
department,
|
||
materialType,
|
||
canView: false, // 默认不允许查看
|
||
canDownload: false, // 默认不允许下载
|
||
canShare: true, // 默认允许分享
|
||
canDownloadSource: false, // 默认不允许下载源文件
|
||
linkedToDepartment: null,
|
||
}));
|
||
|
||
if (newPermissions.length > 0) {
|
||
await prisma.departmentMaterialPermission.createMany({
|
||
data: newPermissions,
|
||
});
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* 检查用户是否有下载源文件(PSD)权限
|
||
* 用于任务-详情类型的 PSD 下载权限控制
|
||
*/
|
||
export async function canUserDownloadSource(
|
||
userDepartment: string | null | undefined,
|
||
userRole: string,
|
||
managedDepartment: string | null | undefined,
|
||
materialType: string,
|
||
userId?: string | null | undefined
|
||
): Promise<boolean> {
|
||
// 超级管理员可以下载所有源文件
|
||
if (userId && await isSuperAdmin(userId)) {
|
||
return true;
|
||
}
|
||
|
||
// 如果没有部门信息,不允许下载
|
||
const department = managedDepartment || userDepartment;
|
||
if (!department) {
|
||
return false;
|
||
}
|
||
|
||
// 获取部门列表(用于权限继承检查),保持和查看/下载权限同一套口径
|
||
const departments = await prisma.department.findMany({
|
||
select: { id: true, name: true, parentId: true },
|
||
});
|
||
|
||
const permission = await getDepartmentPermission(department, materialType, departments);
|
||
|
||
return permission?.canDownloadSource ?? false;
|
||
}
|