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,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);
|
||||
});
|
||||
Reference in New Issue
Block a user