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
+79
View File
@@ -0,0 +1,79 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const KEYS = ['ccDaily', 'ccWeekly', 'ccMonthly'] as const;
function rolesOf(emp: { users: { roles: { role: { code: string } }[] }[] }) {
return [...new Set(emp.users.flatMap((u) => u.roles.map((r) => r.role.code)))];
}
function isDirector(emp: { title?: string | null; jobRole?: string | null; users: { roles: { role: { code: string } }[] }[] }) {
if (/总监/.test(`${emp.title || ''}${emp.jobRole || ''}`)) return true;
return rolesOf(emp).some((r) => r.endsWith('_director') || r === 'biz_director' || r === 'owner');
}
function parseIds(raw?: string | null) {
if (!raw) return [];
return [...new Set(raw.split(/[,,、\s]+/).filter((s) => UUID_RE.test(s)))];
}
async function main() {
const depts = await prisma.department.findMany({ where: { tenantId: '1' }, select: { id: true, parentId: true } });
const byId = new Map(depts.map((d) => [d.id, d]));
const people = await prisma.employee.findMany({
where: { tenantId: '1' },
include: { users: { select: { roles: { select: { role: { select: { code: true } } } } } } },
});
const byDept = new Map<string, typeof people>();
for (const e of people) {
const list = byDept.get(e.departmentId) || [];
list.push(e);
byDept.set(e.departmentId, list);
}
function scopeDeptIds(start: string, selfId: string) {
const scope: string[] = [];
const seen = new Set<string>();
let cur: string | null | undefined = start;
while (cur && !seen.has(cur)) {
seen.add(cur);
scope.push(cur);
const hasDir = (byDept.get(cur) || []).some((e) => e.id !== selfId && isDirector(e));
if (hasDir) break;
cur = byId.get(cur)?.parentId;
}
return new Set(scope);
}
let n = 0;
for (const emp of people) {
const scope = scopeDeptIds(emp.departmentId, emp.id);
const dirs = people.filter((d) => scope.has(d.departmentId) && d.id !== emp.id && d.id !== emp.managerId && isDirector(d));
const dirIds = dirs.map((d) => d.id);
const allowed = new Set(dirIds);
const data: Partial<Record<(typeof KEYS)[number], string | null>> = {};
for (const key of KEYS) {
const cur = parseIds(emp[key]);
let next = cur.filter((id) => allowed.has(id));
if (!next.length && dirIds.length && cur.some((id) => id === emp.managerId || !allowed.has(id))) {
next = dirIds;
}
const joined = next.length ? next.join(',') : null;
if (joined !== (emp[key] || null)) data[key] = joined;
}
if (!Object.keys(data).length) continue;
await prisma.employee.update({ where: { id: emp.id }, data });
n += 1;
console.log(`${emp.name}: ${JSON.stringify(data)}`);
}
console.log(`updated ${n} / ${people.length} employee cc fields`);
}
main()
.then(async () => prisma.$disconnect())
.catch(async (e) => {
console.error(e);
await prisma.$disconnect();
process.exit(1);
});