76f266645d
含 Phase 1.1 IM seq 排序、断线重连、多端已读同步与微信式语音转文字 UI。 排除 node_modules、构建产物、安装包与 .env 密钥。 Co-authored-by: Cursor <cursoragent@cursor.com>
240 lines
6.7 KiB
TypeScript
240 lines
6.7 KiB
TypeScript
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);
|
|
});
|