素材库与考核调研此前各有一套人事数据,彼此没有外键,靠姓名、邮箱在运行时 逐层软匹配。本次把身份判定收敛到钉钉唯一值,并让组织架构与人员状态跟随 上游 HR 系统自动更新。 人员身份 - 新增账号与权威人员档案的正式绑定表,活跃绑定双向唯一 - users.dingtalkUserId 补上唯一约束,数据库层面杜绝一人多号 - 移除钉钉登录的同名自动合并,同名冲突一律转人工核对 - 看板删除特判由比对用户名改为比对钉钉唯一值,避免同名越权 组织架构 - 新增 OrganizationUnit,直接镜像上游 HR 部门树 - 不再从员工部门路径倒推,同名部门不会再被合并 - 上游已删除的部门下线而非物理删除 人员状态 - 新增规范化的 employmentState,取代对混杂原始值做关键词匹配 - 接入上游离职表,离职判定不再依赖中文关键词 - 离职联动:归档账号并把素材托管给所在部门,默认只处理离职满 30 天者 素材归属 - 归属变更纳入正式转移,记录最初上传者、转移流水与理由 - 新增素材托管部门,归属与署名分离:托管只改维护责任,不动署名 - 编辑素材的权限判定统一到 canManageMaterial - 修复任务-详情上传把邮箱误存为作者名的问题 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
93 lines
2.8 KiB
TypeScript
93 lines
2.8 KiB
TypeScript
import { prisma } from '@/lib/prisma'
|
||
import {
|
||
isSuperAdmin,
|
||
isSystemAdmin,
|
||
} from '@/lib/department-permission'
|
||
|
||
export interface MaterialAuthorizationUser {
|
||
id: string
|
||
role?: string | null
|
||
isSuperAdmin?: boolean | null
|
||
/** 所属部门 ID。未提供时会在需要判定托管权限的场景下按需查库 */
|
||
departmentId?: string | null
|
||
}
|
||
|
||
export interface MaterialAuthorizationTarget {
|
||
/** 素材 ID。当调用方未查询 custodyDepartmentId 时,用它补查 */
|
||
id?: string
|
||
userId: string
|
||
/**
|
||
* 素材托管部门。
|
||
* `undefined` 表示调用方没有查这个字段,`null` 表示确认未处于托管状态。
|
||
*/
|
||
custodyDepartmentId?: string | null
|
||
}
|
||
|
||
/**
|
||
* 解析用户所属部门 ID。
|
||
*
|
||
* 优先取 departmentId 外键;部分历史账号只填了部门名称字符串而没有回填
|
||
* departmentId,这里按名称兜底反查,避免这些人无法接手本部门托管的素材。
|
||
*/
|
||
async function resolveUserDepartmentId(userId: string): Promise<string | null> {
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: userId },
|
||
select: { departmentId: true, department: true },
|
||
})
|
||
if (!user) return null
|
||
if (user.departmentId) return user.departmentId
|
||
|
||
const name = user.department?.trim()
|
||
if (!name) return null
|
||
|
||
const department = await prisma.department.findUnique({
|
||
where: { name },
|
||
select: { id: true },
|
||
})
|
||
return department?.id ?? null
|
||
}
|
||
|
||
/**
|
||
* 素材写操作的最小统一授权:
|
||
* - 素材所有者
|
||
* - 素材托管部门的成员(原归属人离职后由该部门接手维护)
|
||
* - 系统管理员(兼容 ADMIN / SYSTEM_ADMIN)
|
||
* - 超级管理员
|
||
*
|
||
* 部门查看权限不能自动升级为素材写权限。
|
||
*/
|
||
export async function canManageMaterial(
|
||
user: MaterialAuthorizationUser,
|
||
material: MaterialAuthorizationTarget,
|
||
): Promise<boolean> {
|
||
if (
|
||
material.userId === user.id ||
|
||
isSystemAdmin(user.role) ||
|
||
user.isSuperAdmin === true
|
||
) {
|
||
return true
|
||
}
|
||
|
||
// 多数调用方只 select 了 id / userId,没有查托管字段。此处按需补查,
|
||
// 避免因为某个调用点漏 select 就让托管权限静默失效——调用方若已带上
|
||
// custodyDepartmentId(包括显式的 null),这里不会产生任何额外查询。
|
||
let custodyDepartmentId = material.custodyDepartmentId
|
||
if (custodyDepartmentId === undefined && material.id) {
|
||
const row = await prisma.material.findUnique({
|
||
where: { id: material.id },
|
||
select: { custodyDepartmentId: true },
|
||
})
|
||
custodyDepartmentId = row?.custodyDepartmentId ?? null
|
||
}
|
||
|
||
if (custodyDepartmentId) {
|
||
const userDepartmentId =
|
||
user.departmentId ?? (await resolveUserDepartmentId(user.id))
|
||
if (userDepartmentId && userDepartmentId === custodyDepartmentId) {
|
||
return true
|
||
}
|
||
}
|
||
|
||
return isSuperAdmin(user.id)
|
||
}
|