chenpei 887d911b76 fix: 权限组部门树改走花名册组织口径
跟数据口径同一套在职花名册和一人多部门,不再只按有素材库账号的人裁树。
2026-08-20 15:43:28 +08:00

343 lines
12 KiB
TypeScript

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,
items: {
select: {
materialType: true,
canView: true,
canDownload: true,
canDownloadSource: true,
canShare: 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<string, OrgNode>()
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<string, string[]>()
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,
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 === '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
}> = 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: item.canShare !== false,
})),
)
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 })
}
}