76f266645d
含 Phase 1.1 IM seq 排序、断线重连、多端已读同步与微信式语音转文字 UI。 排除 node_modules、构建产物、安装包与 .env 密钥。 Co-authored-by: Cursor <cursoragent@cursor.com>
166 lines
6.1 KiB
TypeScript
166 lines
6.1 KiB
TypeScript
import { PrismaClient } from '@prisma/client';
|
|
import { DIRECTOR_ROLES, WORK_ASSIGN_ROLES } from '../src/common/role-groups';
|
|
import { MENUS, isHrSettingCode, type MenuNode } from './menus';
|
|
|
|
const prisma = new PrismaClient();
|
|
|
|
async function upsertNode(node: MenuNode, parentId: string | null, sortNo: number) {
|
|
const existing = await prisma.permission.findUnique({ where: { code: node.code } });
|
|
const data = {
|
|
parentId,
|
|
name: node.name,
|
|
permType: 'MENU',
|
|
path: node.path,
|
|
icon: node.icon ?? null,
|
|
sortNo,
|
|
visible: true,
|
|
};
|
|
const row = existing
|
|
? await prisma.permission.update({ where: { id: existing.id }, data })
|
|
: await prisma.permission.create({ data: { ...data, code: node.code } });
|
|
return row;
|
|
}
|
|
|
|
const ACTION_DEFS = [
|
|
{ name: '查看', suffix: 'view' },
|
|
{ name: '新增', suffix: 'create' },
|
|
{ name: '编辑', suffix: 'edit' },
|
|
{ name: '删除', suffix: 'delete' },
|
|
{ name: '审批', suffix: 'approve' },
|
|
];
|
|
|
|
async function upsertActions(parentCode: string, parentId: string) {
|
|
const ids: string[] = [];
|
|
for (let i = 0; i < ACTION_DEFS.length; i++) {
|
|
const a = ACTION_DEFS[i];
|
|
const code = `${parentCode}:${a.suffix}`;
|
|
const existing = await prisma.permission.findUnique({ where: { code } });
|
|
const data = {
|
|
parentId,
|
|
name: a.name,
|
|
permType: 'ACTION',
|
|
path: null as string | null,
|
|
icon: null as string | null,
|
|
sortNo: i + 1,
|
|
visible: true,
|
|
};
|
|
const row = existing
|
|
? await prisma.permission.update({ where: { id: existing.id }, data })
|
|
: await prisma.permission.create({ data: { ...data, code } });
|
|
ids.push(row.id);
|
|
}
|
|
return ids;
|
|
}
|
|
|
|
async function walk(nodes: MenuNode[], parentId: string | null, root = true) {
|
|
const ids: string[] = [];
|
|
for (let i = 0; i < nodes.length; i++) {
|
|
const node = nodes[i];
|
|
const row = await upsertNode(node, parentId, root ? (i + 1) * 10 : i + 1);
|
|
ids.push(row.id);
|
|
if (node.children?.length) ids.push(...(await walk(node.children, row.id, false)));
|
|
else ids.push(...(await upsertActions(node.code, row.id)));
|
|
}
|
|
return ids;
|
|
}
|
|
|
|
async function main() {
|
|
const kept = await walk(MENUS, null);
|
|
const stale = await prisma.permission.findMany({
|
|
where: {
|
|
permType: 'MENU',
|
|
OR: [
|
|
{ id: { notIn: kept }, code: { in: ['system:org'] } },
|
|
{ code: { in: ['office:loan-approve', 'finance:expense-approve', 'finance:loan-approve', 'finance:bonds', 'contract:milestones', 'office:attendance', 'office:policies', 'project:budgets', 'project:changes'] } },
|
|
],
|
|
},
|
|
});
|
|
if (stale.length) {
|
|
await prisma.permission.updateMany({
|
|
where: { id: { in: stale.map((s) => s.id) } },
|
|
data: { visible: false },
|
|
});
|
|
}
|
|
|
|
const all = await prisma.permission.findMany({ where: { visible: true } });
|
|
const grantByCodes = (codes: string[]) => {
|
|
if (codes[0] === '*') return all;
|
|
return all.filter((p) =>
|
|
codes.some((c) => {
|
|
if (c.endsWith(':')) return p.code === c.slice(0, -1) || p.code.startsWith(c);
|
|
return p.code === c || p.code.startsWith(`${c}:`);
|
|
}),
|
|
);
|
|
};
|
|
const STAFF_OFFICE = [
|
|
'office:overview',
|
|
'office',
|
|
'office:todos',
|
|
'office:approvals',
|
|
'office:calendar',
|
|
'office:reports',
|
|
'office:expenses',
|
|
'office:flow',
|
|
'office:hr-apply',
|
|
'office:seal-apply',
|
|
'office:apply',
|
|
'office:notices',
|
|
'office:directory',
|
|
'office:im',
|
|
];
|
|
const ROLE_GRANT: Record<string, string[]> = {
|
|
admin: ['*'],
|
|
owner: ['*'],
|
|
biz_director: ['office:', 'party:', 'seal:', 'bid:', 'contract:', 'finance:', 'report:', 'hr:', 'asset:', 'system:hr', 'system:org', 'system:attendance-rules'],
|
|
biz_staff: ['office:', 'party:', 'seal:', 'bid:', 'contract:', 'finance:', 'asset:', 'hr:employees', 'hr:payroll', 'hr:performance', 'hr:attendance', 'hr:lifecycle'],
|
|
tech_director: ['office:', 'bid:', 'project:', 'report:', 'hr:performance', 'hr:notices'],
|
|
rd_director: ['*'],
|
|
'3d_director': ['office:', 'bid:', 'project:', 'report:', 'hr:performance', 'hr:notices'],
|
|
video_director: ['office:', 'bid:', 'project:', 'report:', 'hr:performance', 'hr:notices'],
|
|
material_director: ['office:', 'bid:', 'project:', 'report:', 'hr:performance', 'hr:notices'],
|
|
finance: ['office:', 'finance:', 'contract:', 'report:', 'bid:result', 'asset:'],
|
|
hr: ['office:', 'hr:', 'contract:hr', 'system:hr', 'system:org', 'system:attendance-rules', 'system:acl'],
|
|
employee: [...STAFF_OFFICE, 'project:timesheets'],
|
|
bid_screener: ['office', 'office:overview', 'bid', 'bid:screening'],
|
|
pm: [...STAFF_OFFICE, 'office:assign', 'project:'],
|
|
rd_staff: [...STAFF_OFFICE, 'project:timesheets'],
|
|
'3d_staff': [...STAFF_OFFICE, 'project:timesheets'],
|
|
video_staff: [...STAFF_OFFICE, 'project:timesheets'],
|
|
material_staff: [...STAFF_OFFICE, 'project:timesheets'],
|
|
tech: [...STAFF_OFFICE, 'project:timesheets'],
|
|
};
|
|
const roles = await prisma.role.findMany();
|
|
for (const role of roles) {
|
|
if (role.aclCustom) continue;
|
|
const codes = ROLE_GRANT[role.code];
|
|
if (!codes) continue;
|
|
const wanted = grantByCodes(codes);
|
|
const extra = role.code === 'hr' ? all.filter((p) => isHrSettingCode(p.code)) : [];
|
|
const smsRoles = ['admin', 'owner', 'hr', ...DIRECTOR_ROLES];
|
|
const merged = [...new Map([...wanted, ...extra].map((p) => [p.id, p])).values()].filter((p) => {
|
|
if (p.code === 'office:assign') return WORK_ASSIGN_ROLES.includes(role.code);
|
|
if (p.code === 'office:sms') return smsRoles.includes(role.code);
|
|
return true;
|
|
});
|
|
const wantedIds = merged.map((p) => p.id);
|
|
await prisma.rolePermission.deleteMany({
|
|
where: { roleId: role.id, permissionId: { notIn: wantedIds.length ? wantedIds : [''] } },
|
|
});
|
|
await prisma.rolePermission.createMany({
|
|
data: merged.map((p) => ({ roleId: role.id, permissionId: p.id })),
|
|
skipDuplicates: true,
|
|
});
|
|
}
|
|
console.log(`menus synced: ${kept.length} visible, hid ${stale.length}`);
|
|
}
|
|
|
|
main()
|
|
.then(async () => {
|
|
await prisma.$disconnect();
|
|
})
|
|
.catch(async (e) => {
|
|
console.error(e);
|
|
await prisma.$disconnect();
|
|
process.exit(1);
|
|
});
|