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 } from '@/lib/employee-directory' import { selectVisibleOrganizationPeople } from '@/lib/auto-personnel-rules' import { collectOccupiedOrganizationPaths, lastOrganizationPathSegment, normalizeOrganizationPath, pruneOrganizationTree, resolveEmployeeOrganizationPaths, } from '@/lib/organization-material-permissions' import { findConflictingDepartmentGroup, makePermissionGroupSlug, seedDefaultGroupDepartments, } from '@/lib/permission-groups' import { ensureDefaultPermissionPacks, 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 }) } await ensureDefaultPermissionPacks() await seedDefaultGroupDepartments() 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, }, }, inheritors: { orderBy: { updatedAt: 'desc' }, select: { userId: true, user: { select: { username: true, realName: true, employeeBindings: { where: { status: BINDING_STATUS.ACTIVE }, take: 1, select: { employeeProfile: { select: { displayName: true, departmentPath: true, departmentName: 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), prisma.materialPermissionPackDepartment .findMany({ orderBy: { departmentPath: 'asc' }, select: { packId: true, departmentPath: true }, }) .catch((error) => { if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2021') { return [] as Array<{ packId: string; departmentPath: string }> } throw error }), ]) 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.map((row) => row.departmentPath), ]) const tree = pruneOrganizationTree(roots, occupiedPaths) const departmentsByPack = new Map() for (const row of departmentRows) { const list = departmentsByPack.get(row.packId) || [] list.push(row.departmentPath) 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((path) => ({ path, name: lastOrganizationPathSegment(path) || path, })), members: pack.inheritors.map((row) => { const profile = row.user.employeeBindings[0]?.employeeProfile return { userId: row.userId, username: row.user.username, displayName: profile?.displayName || row.user.realName || row.user.username, departmentPath: profile?.departmentPath || null, departmentName: profile?.departmentName || 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 }) } await ensureDefaultPermissionPacks() 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 }> = Array.isArray(body?.permissions) ? body.permissions : [] if (incoming.length === 0) { return NextResponse.json({ error: '素材类型列表不能为空' }, { status: 400 }) } await savePermissionPackItems( slug, incoming.map((item) => ({ materialType: item.materialType, canView: Boolean(item.canView), canDownload: Boolean(item.canDownload), canDownloadSource: Boolean(item.canDownloadSource), canShare: true, canEditAssets: item.canEditAssets === true, })), ) return NextResponse.json({ ok: true }) } if (action === 'add-department' || action === 'remove-department') { const path = normalizeOrganizationPath(body?.path) if (!path) return NextResponse.json({ error: '请选择部门' }, { status: 400 }) const unit = await prisma.organizationUnit.findFirst({ where: { isActive: true, path }, select: { path: true }, }) if (!unit) return NextResponse.json({ error: '花名册里没有这个部门' }, { status: 400 }) if (action === 'remove-department') { await prisma.materialPermissionPackDepartment.deleteMany({ where: { packId: pack.id, departmentPath: path }, }) return NextResponse.json({ ok: true }) } const conflict = await findConflictingDepartmentGroup(path, pack.id) if (conflict) { const name = lastOrganizationPathSegment(path) || path return NextResponse.json( { error: `「${name}」已经在权限组「${conflict.name}」里,一个部门只能进一组。先从那组拿掉,再加到这里。` }, { status: 409 }, ) } try { await prisma.materialPermissionPackDepartment.upsert({ where: { packId_departmentPath: { packId: pack.id, departmentPath: path }, }, update: {}, create: { packId: pack.id, departmentPath: path }, }) } 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 }) } }