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
+239
View File
@@ -0,0 +1,239 @@
import { createReadStream } from 'fs';
import { createGunzip } from 'zlib';
import { PrismaClient } from '@prisma/client';
import { parseOaJson } from './oa-json';
const prisma = new PrismaClient();
const DUMP = process.env.OA_DUMP || '/opt/backup/cloud-20260830/oa.sql.gz';
const HR = '/opt/import/prod/oa_hr.json';
function n(v: unknown) {
if (v == null || v === '') return 0;
const x = Number(v);
return Number.isFinite(x) ? x : 0;
}
function parseMysqlTuples(s: string): unknown[][] {
const rows: unknown[][] = [];
let i = 0;
while (i < s.length) {
while (i < s.length && ' \n\r\t,'.includes(s[i])) i += 1;
if (i >= s.length) break;
if (s[i] !== '(') throw new Error(`expected ( at ${i}`);
i += 1;
const row: unknown[] = [];
while (true) {
while (i < s.length && ' \n\r\t'.includes(s[i])) i += 1;
if (s.startsWith('NULL', i) && (i + 4 === s.length || ',) \n\r\t'.includes(s[i + 4]))) {
row.push(null);
i += 4;
} else if (s[i] === "'" || s[i] === '"') {
const q = s[i];
i += 1;
let out = '';
while (i < s.length) {
const ch = s[i];
if (ch === '\\') {
out += s[i + 1];
i += 2;
continue;
}
if (ch === q) {
if (s[i + 1] === q) {
out += q;
i += 2;
continue;
}
i += 1;
break;
}
out += ch;
i += 1;
}
row.push(out);
} else {
const start = i;
while (i < s.length && s[i] !== ',' && s[i] !== ')') i += 1;
const raw = s.slice(start, i).trim();
row.push(raw === '' ? null : /^-?\d+(\.\d+)?$/.test(raw) ? Number(raw) : raw);
}
while (i < s.length && ' \n\r\t'.includes(s[i])) i += 1;
if (s[i] === ',') {
i += 1;
continue;
}
if (s[i] === ')') {
i += 1;
rows.push(row);
break;
}
throw new Error(`bad token at ${i}`);
}
}
return rows;
}
function insertsFor(dump: string, table: string) {
const rows: unknown[][] = [];
const needle = `INSERT INTO \`${table}\` VALUES `;
let from = 0;
while (true) {
const start = dump.indexOf(needle, from);
if (start < 0) break;
let i = start + needle.length;
while (i < dump.length) {
while (i < dump.length && ' \n\r\t'.includes(dump[i])) i += 1;
if (dump[i] === ';') {
i += 1;
break;
}
if (dump[i] === ',') {
i += 1;
continue;
}
if (dump[i] !== '(') break;
const sliceStart = i;
let depth = 0;
let inStr = false;
let q = '';
while (i < dump.length) {
const ch = dump[i];
if (inStr) {
if (ch === '\\') {
i += 2;
continue;
}
if (ch === q) inStr = false;
i += 1;
continue;
}
if (ch === "'" || ch === '"') {
inStr = true;
q = ch;
i += 1;
continue;
}
if (ch === '(') depth += 1;
if (ch === ')') {
depth -= 1;
if (depth === 0) {
i += 1;
break;
}
}
i += 1;
}
rows.push(...parseMysqlTuples(dump.slice(sliceStart, i)));
}
from = i;
}
return rows;
}
async function readDump() {
return new Promise<string>((resolve, reject) => {
const chunks: Buffer[] = [];
createReadStream(DUMP)
.pipe(createGunzip())
.on('data', (c: Buffer) => chunks.push(c))
.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')))
.on('error', reject);
});
}
async function main() {
const dump = await readDump();
const hr = parseOaJson(HR) as { users?: { id: number; nickName?: string; phone?: string }[] };
const employees = await prisma.employee.findMany({ select: { id: true, name: true, mobile: true } });
const byName = new Map(employees.map((e) => [e.name, e]));
const byMobile = new Map(employees.filter((e) => e.mobile).map((e) => [e.mobile as string, e]));
const oaUserToEmp = new Map<number, string>();
for (const u of hr.users || []) {
const hit = (u.phone && byMobile.get(String(u.phone))) || (u.nickName && byName.get(u.nickName));
if (hit) oaUserToEmp.set(Number(u.id), hit.id);
}
const months = insertsFor(dump, 'oa_salary_month');
const slips = insertsFor(dump, 'oa_salary_slip');
const admin = await prisma.user.findFirst({ where: { username: '18115604840' } });
await prisma.payrollItem.deleteMany();
await prisma.payrollRun.deleteMany();
const runByYm = new Map<string, string>();
for (const row of months) {
const ym = String(row[1] || '');
if (!/^\d{4}-\d{2}$/.test(ym)) continue;
const confirmed = Number(row[2]) === 1;
const run = await prisma.payrollRun.create({
data: {
tenantId: '1',
yearMonth: ym,
status: confirmed ? 'CONFIRMED' : 'DRAFT',
createdById: admin?.id,
confirmedAt: confirmed ? (row[7] ? new Date(String(row[7])) : new Date()) : null,
},
});
runByYm.set(ym, run.id);
}
let ok = 0;
let skip = 0;
for (const row of slips) {
const ym = String(row[2] || '');
const oaUserId = Number(row[3]);
const name = String(row[4] || '');
let empId = oaUserToEmp.get(oaUserId);
if (!empId && name) empId = byName.get(name)?.id;
if (!empId || !runByYm.has(ym)) {
skip += 1;
continue;
}
await prisma.payrollItem.upsert({
where: { runId_employeeId: { runId: runByYm.get(ym) as string, employeeId: empId } },
update: {
basePay: n(row[9]) + n(row[10]),
performancePay: n(row[18] || row[11]),
leaveDeduct: n(row[22]),
sickDeduct: n(row[23]),
socialPersonal: n(row[27]),
tax: n(row[28]),
grossPay: n(row[16]),
netPay: n(row[30]),
scheduledDays: n(row[31]),
prorationFactor: n(row[29]) || 1,
},
create: {
runId: runByYm.get(ym) as string,
employeeId: empId,
basePay: n(row[9]) + n(row[10]),
performancePay: n(row[18] || row[11]),
leaveDeduct: n(row[22]),
sickDeduct: n(row[23]),
socialPersonal: n(row[27]),
tax: n(row[28]),
grossPay: n(row[16]),
netPay: n(row[30]),
scheduledDays: n(row[31]),
prorationFactor: n(row[29]) || 1,
},
});
ok += 1;
}
console.log(
JSON.stringify({
months: runByYm.size,
slips: slips.length,
imported: ok,
skipped: skip,
}),
);
}
main()
.then(async () => prisma.$disconnect())
.catch(async (e) => {
console.error(e);
await prisma.$disconnect();
process.exit(1);
});