/** * 权限点目录(A0-SH-01 骨架 → A2 定稿,口径见 H1 权限门) * * 命名规范:<模块>:<动作>,全小写;权限树一级模块(script / material / asset / delivery / * customer / customer_group / identity / industry / platform / audit)与 UI 原型一致, * 动作使用动词或名词短语。 * * 约束(AGENTS §10 / 开发计划 §14 附录 B): * - 权限点总共 30 个:保留 A0/A1 已有的 read / write 编码,只补齐 H1 确认的 5 个新编码; * - 新增 / 删除 / 改名权限点必须评审,`code` 一经发布不得变更(角色数据按 code 关联); * - 特殊权限 6 个编码(5 类):默认关闭、单独授予、不随角色复制带入; * - 本文件是唯一事实源:后端启动时同步 `permissions` 表,前端权限树由 `/permissions` 生成; * 本文件不依赖任何其他模块,保证 `@lingxi/shared` 零业务依赖。 */ // --------------------------------------------------------------------------- // 模块(权限树一级分组,共 10 个;与 design/a2-ui-v1/brief.md 对齐,顺序固定) // --------------------------------------------------------------------------- export const PERMISSION_MODULES = [ { module: 'script', moduleLabel: '脚本', sortOrder: 10 }, { module: 'material', moduleLabel: '素材', sortOrder: 20 }, { module: 'asset', moduleLabel: '资产', sortOrder: 30 }, { module: 'delivery', moduleLabel: '导出交付', sortOrder: 40 }, { module: 'customer', moduleLabel: '客户', sortOrder: 50 }, { module: 'customer_group', moduleLabel: '客户组', sortOrder: 60 }, { module: 'identity', moduleLabel: '组织与角色', sortOrder: 70 }, { module: 'industry', moduleLabel: '行业包', sortOrder: 80 }, { module: 'platform', moduleLabel: '平台配置', sortOrder: 90 }, { module: 'audit', moduleLabel: '审计', sortOrder: 100 }, ] as const; export type PermissionModule = (typeof PERMISSION_MODULES)[number]['module']; // --------------------------------------------------------------------------- // 权限点编码(30 个) // --------------------------------------------------------------------------- export const Permission = { // 脚本 SCRIPT_READ: 'script:read', SCRIPT_WRITE: 'script:write', SCRIPT_REVIEW: 'script:review', // 素材 MATERIAL_READ: 'material:read', MATERIAL_WRITE: 'material:write', MATERIAL_RENDER: 'material:render', MATERIAL_REVIEW: 'material:review', // 资产 ASSET_READ: 'asset:read', ASSET_WRITE: 'asset:write', ASSET_DELETE: 'asset:delete', // 交付导出 DELIVERY_READ: 'delivery:read', DELIVERY_EXPORT: 'delivery:export', // 客户 CUSTOMER_READ: 'customer:read', CUSTOMER_MANAGE: 'customer:manage', CUSTOMER_TRANSFER: 'customer:transfer', CUSTOMER_FIELD_APPLY: 'customer_field:apply', CUSTOMER_FIELD_APPROVE: 'customer_field:approve', // 客户组 CUSTOMER_GROUP_MANAGE: 'customer_group:manage', // 组织与角色 USER_VIEW: 'user:view', USER_MANAGE: 'user:manage', ROLE_VIEW: 'role:view', ROLE_MANAGE: 'role:manage', // 行业包 INDUSTRY_MANAGE: 'industry:manage', INDUSTRY_PUBLISH: 'industry:publish', // 平台配置 DICT_MANAGE: 'dict:manage', FORM_DESIGN: 'form:design', AI_CONFIGURE: 'ai:configure', SYSTEM_CONFIGURE: 'system:configure', // 审计 AUDIT_READ: 'audit:read', AUDIT_EXPORT: 'audit:export', } as const; export type PermissionCode = (typeof Permission)[keyof typeof Permission]; // --------------------------------------------------------------------------- // 特殊权限(6 编码 / 5 类,默认关闭、单独授予、不随角色复制带入) // --------------------------------------------------------------------------- /** 需要 SpecialPermissionGuard 单独校验的特殊权限(架构 §6.2 / PRD §5.4.3 五) */ export const SPECIAL_PERMISSIONS: readonly PermissionCode[] = [ Permission.DELIVERY_EXPORT, Permission.CUSTOMER_FIELD_APPROVE, Permission.INDUSTRY_PUBLISH, Permission.AI_CONFIGURE, Permission.AUDIT_READ, Permission.AUDIT_EXPORT, ]; const SPECIAL_PERMISSION_SET: ReadonlySet = new Set(SPECIAL_PERMISSIONS); export function isSpecialPermission(code: string): boolean { return SPECIAL_PERMISSION_SET.has(code); } // --------------------------------------------------------------------------- // 职责互斥三对(SoD;超管豁免,命中返回 40304) // --------------------------------------------------------------------------- export interface SodPair { id: string; label: string; description: string; permissions: readonly [PermissionCode, PermissionCode]; } export interface SodConflict { pairId: string; label: string; permissions: readonly [PermissionCode, PermissionCode]; } export const SOD_PAIRS: readonly SodPair[] = [ { id: 'script-write-review', label: '脚本生成 × 脚本审核', description: '自己写自己审会让审核形同虚设,同一角色不得同时拥有「脚本生成」与「脚本审核」。', permissions: [Permission.SCRIPT_WRITE, Permission.SCRIPT_REVIEW], }, { id: 'industry-manage-publish', label: '行业包编辑 × 行业包发布', description: '行业包改动影响所有客户,编辑与发布必须由两个角色分别承担。', permissions: [Permission.INDUSTRY_MANAGE, Permission.INDUSTRY_PUBLISH], }, { id: 'customer-field-apply-approve', label: '客户字段申请 × 客户字段审批', description: '字段申请与审批不得由同一角色完成,避免绕过审批直接进入生成。', permissions: [Permission.CUSTOMER_FIELD_APPLY, Permission.CUSTOMER_FIELD_APPROVE], }, ]; /** 命中任意一对 SoD 即返回该对明细(role 保存前校验,超管豁免) */ export function findSodConflicts(codes: readonly string[]): SodConflict[] { const codeSet = new Set(codes); return SOD_PAIRS.filter( (pair) => codeSet.has(pair.permissions[0]) && codeSet.has(pair.permissions[1]), ).map((pair) => ({ pairId: pair.id, label: pair.label, permissions: pair.permissions, })); } // --------------------------------------------------------------------------- // 软上限(建议值,仅提示不阻断;A4 迁 system_configs) // --------------------------------------------------------------------------- /** 角色数量建议上限(PRD §5.4.3 数量建议:角色 ≤ 20) */ export const ROLE_SOFT_LIMIT = 20; /** 超级管理员人数建议上限(PRD §5.4.1:建议 ≤ 2 人) */ export const SUPER_ADMIN_SOFT_LIMIT = 2; // --------------------------------------------------------------------------- // 权限点目录元数据 // --------------------------------------------------------------------------- export interface PermissionMeta { code: PermissionCode; module: PermissionModule; moduleLabel: string; name: string; description: string; isSpecial: boolean; sortOrder: number; } interface PermissionDefinition { code: PermissionCode; module: PermissionModule; name: string; description: string; } /** 目录定义(按模块 + 操作顺序),`PERMISSION_CATALOG` 由此生成 */ const PERMISSION_DEFINITIONS: readonly PermissionDefinition[] = [ // 脚本 { code: Permission.SCRIPT_READ, module: 'script', name: '查看脚本', description: '脚本库、模板与版本', }, { code: Permission.SCRIPT_WRITE, module: 'script', name: '编辑与生成脚本', description: '编辑脚本、发起生成', }, { code: Permission.SCRIPT_REVIEW, module: 'script', name: '审核脚本', description: '脚本入库审核', }, // 素材 { code: Permission.MATERIAL_READ, module: 'material', name: '查看素材任务', description: '素材与任务详情', }, { code: Permission.MATERIAL_WRITE, module: 'material', name: '编辑素材任务', description: '参数与素材信息', }, { code: Permission.MATERIAL_RENDER, module: 'material', name: '发起生成与渲染', description: '图片 / 视频生成', }, { code: Permission.MATERIAL_REVIEW, module: 'material', name: '审核素材', description: '素材入库审核', }, // 资产 { code: Permission.ASSET_READ, module: 'asset', name: '查看资产', description: '资产库与版本' }, { code: Permission.ASSET_WRITE, module: 'asset', name: '上传与编辑资产', description: '上传、编辑、启停', }, { code: Permission.ASSET_DELETE, module: 'asset', name: '删除资产', description: '受引用保护,仅未被引用可删', }, // 导出交付 { code: Permission.DELIVERY_READ, module: 'delivery', name: '查看交付记录', description: '交付清单与导出记录', }, { code: Permission.DELIVERY_EXPORT, module: 'delivery', name: '发起导出', description: '数据离开平台的唯一出口(特殊)', }, // 客户 { code: Permission.CUSTOMER_READ, module: 'customer', name: '查看客户', description: '客户资料与归属', }, { code: Permission.CUSTOMER_MANAGE, module: 'customer', name: '管理客户资料', description: '新建、编辑、停用', }, { code: Permission.CUSTOMER_TRANSFER, module: 'customer', name: '转交客户', description: '变更客户负责人', }, { code: Permission.CUSTOMER_FIELD_APPLY, module: 'customer', name: '申请客户字段', description: '提交扩展字段申请', }, { code: Permission.CUSTOMER_FIELD_APPROVE, module: 'customer', name: '审批客户字段', description: '审批扩展字段(特殊)', }, // 客户组 { code: Permission.CUSTOMER_GROUP_MANAGE, module: 'customer_group', name: '管理客户组', description: '客户组增改与成员', }, // 组织与角色 { code: Permission.USER_VIEW, module: 'identity', name: '查看用户', description: '用户与部门' }, { code: Permission.USER_MANAGE, module: 'identity', name: '管理用户', description: '新建、停用、转交', }, { code: Permission.ROLE_VIEW, module: 'identity', name: '查看角色', description: '角色、权限点与 SoD', }, { code: Permission.ROLE_MANAGE, module: 'identity', name: '管理角色与授权', description: '新建、编辑、复制、停用', }, // 行业包 { code: Permission.INDUSTRY_MANAGE, module: 'industry', name: '编辑行业包', description: '草稿、schema、规则集', }, { code: Permission.INDUSTRY_PUBLISH, module: 'industry', name: '发布行业包', description: '发布后影响全部客户(特殊)', }, // 平台配置 { code: Permission.DICT_MANAGE, module: 'platform', name: '管理字典', description: '字典与系统配置项', }, { code: Permission.FORM_DESIGN, module: 'platform', name: '设计表单', description: '表单设计器' }, { code: Permission.AI_CONFIGURE, module: 'platform', name: 'AI 配置', description: '模型、路由、额度(特殊)', }, { code: Permission.SYSTEM_CONFIGURE, module: 'platform', name: '系统配置', description: '平台参数', }, // 审计 { code: Permission.AUDIT_READ, module: 'audit', name: '查看审计', description: '审计日志检索(特殊)', }, { code: Permission.AUDIT_EXPORT, module: 'audit', name: '导出审计', description: '审计导出并留痕(特殊)', }, ]; export const PERMISSION_CATALOG: readonly PermissionMeta[] = PERMISSION_DEFINITIONS.map((item) => { const moduleMeta = PERMISSION_MODULES.find((module) => module.module === item.module); if (moduleMeta === undefined) { throw new Error(`权限点 ${item.code} 引用了未登记的模块:${item.module}`); } const indexInModule = PERMISSION_DEFINITIONS.filter( (candidate) => candidate.module === item.module, ).findIndex((candidate) => candidate.code === item.code); return { code: item.code, module: item.module, moduleLabel: moduleMeta.moduleLabel, name: item.name, description: item.description, isSpecial: isSpecialPermission(item.code), sortOrder: moduleMeta.sortOrder + indexInModule, }; }); export const PERMISSION_CODES: readonly PermissionCode[] = PERMISSION_CATALOG.map( (item) => item.code, ); const PERMISSION_CODE_SET: ReadonlySet = new Set(PERMISSION_CODES); export function isPermissionCode(value: string): value is PermissionCode { return PERMISSION_CODE_SET.has(value); } const PERMISSION_META_BY_CODE: ReadonlyMap = new Map( PERMISSION_CATALOG.map((item) => [item.code, item]), ); export function permissionMetaOf(code: string): PermissionMeta | undefined { return PERMISSION_META_BY_CODE.get(code); } /** 按模块分组(保持 PERMISSION_MODULES 顺序,空模块不返回) */ export function permissionsByModule(): readonly { module: PermissionModule; moduleLabel: string; sortOrder: number; permissions: readonly PermissionMeta[]; }[] { return PERMISSION_MODULES.map((moduleMeta) => ({ module: moduleMeta.module, moduleLabel: moduleMeta.moduleLabel, sortOrder: moduleMeta.sortOrder, permissions: PERMISSION_CATALOG.filter((item) => item.module === moduleMeta.module), })).filter((group) => group.permissions.length > 0); } // --------------------------------------------------------------------------- // 解析与校验 // --------------------------------------------------------------------------- export interface ParsedPermission { module: string; action: string; } /** 拆分权限点为 { module, action };格式不合法返回 null */ export function parsePermission(code: string): ParsedPermission | null { const separator = code.indexOf(':'); if (separator <= 0 || separator === code.length - 1) { return null; } const module = code.slice(0, separator); const action = code.slice(separator + 1); if (module.includes(':') || action.includes(':')) { return null; } return { module, action }; } /** * 校验并拆分角色保存入参的权限点:返回普通权限点与特殊权限点。 * 未登记编码抛 `INVALID_PERMISSION_CODE:`(由调用方映射为 40001)。 */ export function splitPermissions(codes: readonly string[]): { permissions: PermissionCode[]; specialPermissions: PermissionCode[]; } { const unique = [...new Set(codes)]; for (const code of unique) { if (!isPermissionCode(code)) { throw new Error(`INVALID_PERMISSION_CODE:${code}`); } } const permissions: PermissionCode[] = []; const specialPermissions: PermissionCode[] = []; for (const code of unique) { if (isSpecialPermission(code)) { specialPermissions.push(code as PermissionCode); } else { permissions.push(code as PermissionCode); } } return { permissions: permissions.sort(), specialPermissions: specialPermissions.sort(), }; }