import { NextRequest, NextResponse } from 'next/server' import { Prisma } from '@prisma/client' import { getServerSession } from 'next-auth' import { authOptions } from '@/lib/auth' import { prisma } from '@/lib/prisma' import { getAllMaterialTypes, isSuperAdmin } from '@/lib/department-permission' import { filterSelectableMaterialTypes } from '@/lib/material-type-options' import { BINDING_STATUS, PERMISSION_PATH_PROFILE_SELECT } from '@/lib/employee-directory' import { selectVisibleOrganizationPeople } from '@/lib/auto-personnel-rules' import { collectOccupiedOrganizationPaths, lastOrganizationPathSegment, normalizeTaskDetailOnlyFlags, pruneOrganizationTree, resolveEmployeeOrganizationPaths, } from '@/lib/organization-material-permissions' import { findConflictingDepartmentGroup, makePermissionGroupSlug, loadPermissionGroupDepartments, } from '@/lib/permission-groups' import { packItemsToMap, savePermissionPackItems, } from '@/lib/user-permission-inherit' type OrgNode = { id: string name: string path: string level: number parentId: string | null children: OrgNode[] } async function requireSuperAdmin() { const session = await getServerSession(authOptions) const userId = (session?.user as { id?: string } | undefined)?.id if (!session || !(await isSuperAdmin(userId))) return null return session } async function requireDingTalkUser(userId: string) { const user = await prisma.user.findUnique({ where: { id: userId }, select: { id: true, dingtalkUserId: true }, }) if (!user) return { error: '没有这个账号', status: 400 as const } if (!user.dingtalkUserId) { return { error: '这个账号没有钉钉绑定,不能进权限组', status: 400 as const } } return { user } } export async function GET() { try { if (!(await requireSuperAdmin())) { return NextResponse.json({ error: '需要超级管理员权限' }, { status: 403 }) } const [packs, units, rosterProfiles, materialTypes, departmentRows] = await Promise.all([ prisma.materialPermissionPack.findMany({ orderBy: { name: 'asc' }, select: { id: true, slug: true, name: true, videoEarlyAccess: true, items: { select: { materialType: true, canView: true, canDownload: true, canDownloadSource: true, canShare: true, canEditAssets: true, canAccessPrivate: true, }, }, inheritors: { orderBy: { updatedAt: 'desc' }, select: { userId: true, user: { select: { username: true, realName: true, employeeBindings: { where: { status: BINDING_STATUS.ACTIVE }, take: 1, select: { employeeProfile: { select: { ...PERMISSION_PATH_PROFILE_SELECT, displayName: true }, }, }, }, }, }, }, }, }, }), prisma.organizationUnit.findMany({ where: { isActive: true }, select: { id: true, name: true, path: true, level: true, parentId: true, sourceDeptId: true }, orderBy: [{ level: 'asc' }, { name: 'asc' }], }), prisma.performanceSurveyEmployeeProfile.findMany({ select: { departmentPath: true, employmentState: true, sourcePayload: true, departmentMemberships: { select: { sourceDeptId: true, departmentPath: true }, }, }, }), getAllMaterialTypes().then(filterSelectableMaterialTypes), loadPermissionGroupDepartments(), ]) const nodeMap = new Map() for (const unit of units) { nodeMap.set(unit.id, { id: unit.id, name: unit.name, path: unit.path, level: unit.level, parentId: unit.parentId, children: [], }) } const roots: OrgNode[] = [] for (const node of Array.from(nodeMap.values())) { if (node.parentId && nodeMap.has(node.parentId)) { nodeMap.get(node.parentId)!.children.push(node) } else { roots.push(node) } } const pathBySourceDeptId = new Map(units.map((unit) => [String(unit.sourceDeptId), unit.path])) const occupiedPaths = collectOccupiedOrganizationPaths([ ...selectVisibleOrganizationPeople(rosterProfiles).flatMap((profile) => resolveEmployeeOrganizationPaths(profile, pathBySourceDeptId), ), ...departmentRows.flatMap((row) => row.organizationUnit?.isActive ? [row.organizationUnit.path] : []), ]) const tree = pruneOrganizationTree(roots, occupiedPaths) const departmentsByPack = new Map() for (const row of departmentRows) { const list = departmentsByPack.get(row.packId) || [] list.push(row) departmentsByPack.set(row.packId, list) } return NextResponse.json({ materialTypes, tree, groups: packs.map((pack) => ({ slug: pack.slug, name: pack.name, videoEarlyAccess: pack.videoEarlyAccess, permissions: packItemsToMap(pack.items), departments: (departmentsByPack.get(pack.id) || []).map((row) => ({ id: row.id, organizationUnitId: row.organizationUnitId, path: row.organizationUnit?.path || row.departmentPath, name: row.organizationUnit?.name || lastOrganizationPathSegment(row.departmentPath), status: row.organizationUnit?.isActive ? 'active' : 'invalid', })), members: pack.inheritors.map((row) => { const profile = row.user.employeeBindings[0]?.employeeProfile const currentPath = resolveEmployeeOrganizationPaths(profile, pathBySourceDeptId)[0] || null return { userId: row.userId, username: row.user.username, displayName: profile?.displayName || row.user.realName || row.user.username, departmentPath: currentPath, departmentName: currentPath ? lastOrganizationPathSegment(currentPath) : null, } }), })), }) } catch (error) { console.error('[permission-groups] GET failed', error) return NextResponse.json({ error: '获取权限组失败' }, { status: 500 }) } } export async function PUT(request: NextRequest) { try { if (!(await requireSuperAdmin())) { return NextResponse.json({ error: '需要超级管理员权限' }, { status: 403 }) } const body = await request.json() const action = String(body?.action || '').trim() if (action === 'create') { const name = String(body?.name || '').trim() if (!name) return NextResponse.json({ error: '请填写组名' }, { status: 400 }) const slug = makePermissionGroupSlug(name) await prisma.materialPermissionPack.create({ data: { slug, name }, }) return NextResponse.json({ ok: true, slug }) } const slug = String(body?.slug || '').trim() if (!slug) return NextResponse.json({ error: '缺少权限组' }, { status: 400 }) const pack = await prisma.materialPermissionPack.findUnique({ where: { slug }, select: { id: true, slug: true, name: true }, }) if (!pack) return NextResponse.json({ error: '没有这个权限组' }, { status: 400 }) if (action === 'rename') { const name = String(body?.name || '').trim() if (!name) return NextResponse.json({ error: '请填写组名' }, { status: 400 }) await prisma.materialPermissionPack.update({ where: { id: pack.id }, data: { name }, }) return NextResponse.json({ ok: true }) } if (action === 'set-video-early-access') { const enabled = body?.enabled === true await prisma.materialPermissionPack.update({ where: { id: pack.id }, data: { videoEarlyAccess: enabled }, }) return NextResponse.json({ ok: true, videoEarlyAccess: enabled }) } if (action === 'delete') { await prisma.$transaction([ prisma.userPermissionInherit.deleteMany({ where: { packId: pack.id } }), prisma.materialPermissionPack.delete({ where: { id: pack.id } }), ]) return NextResponse.json({ ok: true }) } if (action === 'save-pack') { const incoming: Array<{ materialType: string canView?: boolean canDownload?: boolean canDownloadSource?: boolean canShare?: boolean canEditAssets?: boolean canAccessPrivate?: boolean }> = Array.isArray(body?.permissions) ? body.permissions : [] if (incoming.length === 0) { return NextResponse.json({ error: '素材类型列表不能为空' }, { status: 400 }) } const permissions = incoming.filter((item) => item.materialType).map((item) => { const taskDetailOnlyFlags = normalizeTaskDetailOnlyFlags(item.materialType, item) return { materialType: item.materialType, canView: Boolean(item.canView), canDownload: Boolean(item.canDownload), canDownloadSource: Boolean(item.canDownloadSource), canShare: true, canEditAssets: taskDetailOnlyFlags.canEditAssets, canAccessPrivate: taskDetailOnlyFlags.canAccessPrivate, } }) await savePermissionPackItems(slug, permissions) // 在事务提交后返回实际标准化的权限;客户端无需再加载整棵组织树才能确认保存。 return NextResponse.json({ ok: true, permissions: packItemsToMap(permissions) }) } if (action === 'add-department' || action === 'remove-department') { const organizationUnitId = String(body?.organizationUnitId || '').trim() if (action === 'remove-department') { const associationId = String(body?.associationId || '').trim() if (!associationId && !organizationUnitId) return NextResponse.json({ error: '请刷新列表后选择部门' }, { status: 400 }) await prisma.materialPermissionPackDepartment.deleteMany({ where: { packId: pack.id, ...(associationId ? { id: associationId } : { organizationUnitId }) }, }) return NextResponse.json({ ok: true }) } if (!organizationUnitId) return NextResponse.json({ error: '请刷新列表后选择部门' }, { status: 400 }) const unit = await prisma.organizationUnit.findFirst({ where: { isActive: true, id: organizationUnitId }, select: { path: true }, }) if (!unit) return NextResponse.json({ error: '花名册里没有这个部门' }, { status: 400 }) const path = unit.path const conflict = await findConflictingDepartmentGroup(organizationUnitId, pack.id) if (conflict) { const name = lastOrganizationPathSegment(path) || path return NextResponse.json( { error: `「${name}」已经在权限组「${conflict.name}」里,一个部门只能进一组。先从那组拿掉,再加到这里。` }, { status: 409 }, ) } try { const association = await prisma.materialPermissionPackDepartment.upsert({ where: { organizationUnitId }, update: {}, create: { packId: pack.id, organizationUnitId, departmentPath: path }, select: { packId: true }, }) if (association.packId !== pack.id) { return NextResponse.json({ error: '这个部门已在别的权限组里,请刷新列表。' }, { status: 409 }) } } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002') { const name = lastOrganizationPathSegment(path) || path return NextResponse.json( { error: `「${name}」已经在别的权限组里,一个部门只能进一组。` }, { status: 409 }, ) } throw error } return NextResponse.json({ ok: true }) } if (action === 'add-member' || action === 'remove-member') { const userId = String(body?.userId || '').trim() if (!userId) return NextResponse.json({ error: '请选择人员' }, { status: 400 }) if (action === 'remove-member') { await prisma.userPermissionInherit.deleteMany({ where: { userId, packId: pack.id }, }) return NextResponse.json({ ok: true }) } const checked = await requireDingTalkUser(userId) if ('error' in checked) { return NextResponse.json({ error: checked.error }, { status: checked.status }) } await prisma.userPermissionInherit.upsert({ where: { userId_packId: { userId, packId: pack.id } }, update: {}, create: { userId, packId: pack.id }, }) return NextResponse.json({ ok: true }) } return NextResponse.json({ error: '不认识这个操作' }, { status: 400 }) } catch (error) { console.error('[permission-groups] PUT failed', error) return NextResponse.json({ error: '保存权限组失败' }, { status: 500 }) } }