gongxiangp2/lib/department-permission.ts
chenpei 4e89b57798 feat(upload): optimize upload UI and performance
- Expand batch upload list area and optimize preview image layout
- Improve upload speed tracking and display (server-side & client-side)
- Reduce network detection log noise
- Optimize Docker build process (caching, env validation)
- Add shadow environment deployment support
2026-01-28 15:07:50 +08:00

550 lines
16 KiB
TypeScript
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.

import { prisma } from './prisma';
/**
* 判断是否是超级管理员(从数据库读取)
*/
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 = [
// 3D相关
'3D场景', // 注:3DMAX 已于 2026-01-19 迁移合并到此类型
'材质',
'标准模型',
'标准饰品',
// 图片相关
'详情模板',
'模特',
'实拍图片',
'买家秀图片',
// 视频相关
'主图视频',
'安装视频',
'种草视频',
// 模板和种草
'主图模版',
'SKU模版',
'小红书种草',
// 其他
'音乐',
'品牌VI灵感',
// 任务相关
'任务-详情',
'任务-3D原图',
'任务-AI原图',
'任务-实拍原图',
'任务-视频截图',
] as const;
/**
* 从数据库中获取所有素材类型(去重)
*/
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;
}
/**
* 获取部门的实际权限(考虑关联部门和权限继承)
*
* @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 prisma.departmentMaterialPermission.findUnique({
where: {
department_materialType: {
department,
materialType,
},
},
});
// 如果有关联部门,从关联部门读取权限
if (permission?.linkedToDepartment) {
const linkedPermission = await prisma.departmentMaterialPermission.findUnique({
where: {
department_materialType: {
department: 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 function isShareLinkAccess(request: Request): boolean {
const url = new URL(request.url);
return url.searchParams.has('share') || url.searchParams.has('shareId');
}
/**
* 为部门创建默认权限配置
* 使用数据库中实际存在的所有素材类型
*/
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 = await getAllMaterialTypes();
const newPermissions = allMaterialTypes
.filter((type) => !existingTypes.has(type))
.map((materialType) => ({
department,
materialType,
canView: false, // 默认不允许查看
canDownload: false, // 默认不允许下载
canShare: true, // 默认允许分享
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 permission = await prisma.departmentMaterialPermission.findUnique({
where: {
department_materialType: {
department,
materialType,
},
},
select: {
canDownloadSource: true,
linkedToDepartment: true,
},
});
// 如果有关联部门,从关联部门读取权限
if (permission?.linkedToDepartment) {
const linkedPermission = await prisma.departmentMaterialPermission.findUnique({
where: {
department_materialType: {
department: permission.linkedToDepartment,
materialType,
},
},
select: {
canDownloadSource: true,
},
});
return linkedPermission?.canDownloadSource ?? false;
}
return permission?.canDownloadSource ?? false;
}