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:
2026-09-02 10:04:03 +00:00
commit 76f266645d
507 changed files with 99891 additions and 0 deletions
+165
View File
@@ -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);
});