Initial commit: 风影 OA 全栈源码(API/Web/Flutter/IM)
含 Phase 1.1 IM seq 排序、断线重连、多端已读同步与微信式语音转文字 UI。 排除 node_modules、构建产物、安装包与 .env 密钥。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user