76f266645d
含 Phase 1.1 IM seq 排序、断线重连、多端已读同步与微信式语音转文字 UI。 排除 node_modules、构建产物、安装包与 .env 密钥。 Co-authored-by: Cursor <cursoragent@cursor.com>
142 lines
4.6 KiB
TypeScript
142 lines
4.6 KiB
TypeScript
import { PrismaClient } from '@prisma/client';
|
|
import { readFileSync } from 'fs';
|
|
|
|
const prisma = new PrismaClient();
|
|
|
|
type OaExpense = {
|
|
name: string;
|
|
amount: number;
|
|
project: string;
|
|
date: string;
|
|
purpose: string;
|
|
remark: string | null;
|
|
expenseCategory: string;
|
|
};
|
|
|
|
type OaLoan = { name: string; amount: number; purpose: string; project: string };
|
|
|
|
const NAME_ALIAS: Record<string, string> = {
|
|
张靖妍: '张婧妍',
|
|
张婧妍: '张婧妍',
|
|
};
|
|
|
|
function normName(v?: string | null) {
|
|
const s = (v || '').trim();
|
|
return NAME_ALIAS[s] || s;
|
|
}
|
|
|
|
function amtEq(a: unknown, b: unknown) {
|
|
return Math.abs(Number(a) - Number(b)) < 0.021;
|
|
}
|
|
|
|
function sameProject(oa: string, locals: string[]) {
|
|
if (!oa) return locals.length === 0;
|
|
return locals.some((p) => p === oa || p.includes(oa) || oa.includes(p));
|
|
}
|
|
|
|
async function main() {
|
|
const oaExpenses = JSON.parse(readFileSync('/opt/apps/api/prisma/_oa_reimb_map.json', 'utf8')) as OaExpense[];
|
|
const oaLoans = JSON.parse(readFileSync('/opt/apps/api/prisma/_oa_loan_map.json', 'utf8')) as OaLoan[];
|
|
|
|
const expenses = await prisma.expenseClaim.findMany({
|
|
include: {
|
|
applicant: { select: { displayName: true, employee: { select: { name: true } } } },
|
|
project: { select: { name: true } },
|
|
bidCase: { select: { name: true } },
|
|
},
|
|
orderBy: { claimNo: 'asc' },
|
|
});
|
|
|
|
const used = new Set<number>();
|
|
let patchedExpenses = 0;
|
|
const unmatchedExpenses: string[] = [];
|
|
|
|
for (const row of expenses) {
|
|
const names = [normName(row.applicant?.displayName), normName(row.applicant?.employee?.name)].filter(Boolean);
|
|
const locals = [row.project?.name, row.bidCase?.name].filter((v): v is string => Boolean(v));
|
|
const date = String(row.submittedAt || '').slice(0, 10);
|
|
const idxs = oaExpenses
|
|
.map((o, i) => ({ o, i }))
|
|
.filter(({ o, i }) => !used.has(i) && amtEq(o.amount, row.amount) && names.includes(normName(o.name)));
|
|
const byProj = idxs.filter(({ o }) => sameProject(o.project, locals));
|
|
const byDate = idxs.filter(({ o }) => o.date === date);
|
|
const hit = (byProj.length === 1 ? byProj[0] : null) || (byDate.length === 1 ? byDate[0] : null) || (idxs.length === 1 ? idxs[0] : byProj[0] || byDate[0] || null);
|
|
if (!hit) {
|
|
unmatchedExpenses.push(`${row.claimNo} ${names[0] || '?'} ${row.amount}`);
|
|
continue;
|
|
}
|
|
used.add(hit.i);
|
|
await prisma.expenseClaim.update({
|
|
where: { id: row.id },
|
|
data: {
|
|
purpose: hit.o.purpose || null,
|
|
remark: hit.o.remark,
|
|
expenseCategory: hit.o.expenseCategory,
|
|
},
|
|
});
|
|
if (hit.o.purpose) {
|
|
await prisma.approvalTask.updateMany({
|
|
where: { bizId: row.id, bizType: 'EXPENSE_APPROVAL' },
|
|
data: { title: hit.o.purpose },
|
|
});
|
|
}
|
|
patchedExpenses += 1;
|
|
}
|
|
|
|
const loans = await prisma.loanRecord.findMany({
|
|
include: {
|
|
applicant: { select: { displayName: true, employee: { select: { name: true } } } },
|
|
},
|
|
orderBy: { loanNo: 'asc' },
|
|
});
|
|
const usedLoan = new Set<number>();
|
|
let patchedLoans = 0;
|
|
const unmatchedLoans: string[] = [];
|
|
for (const row of loans) {
|
|
const names = [normName(row.applicant?.displayName), normName(row.applicant?.employee?.name)].filter(Boolean);
|
|
const hit = oaLoans
|
|
.map((o, i) => ({ o, i }))
|
|
.find(({ o, i }) => !usedLoan.has(i) && amtEq(o.amount, row.amount) && names.includes(normName(o.name)) && (!row.purpose || row.purpose === o.purpose || o.purpose.includes(row.purpose) || row.purpose.includes(o.purpose)));
|
|
const fallback = oaLoans
|
|
.map((o, i) => ({ o, i }))
|
|
.find(({ o, i }) => !usedLoan.has(i) && amtEq(o.amount, row.amount) && names.includes(normName(o.name)));
|
|
const pick = hit || fallback;
|
|
if (!pick) {
|
|
unmatchedLoans.push(`${row.loanNo} ${names[0] || '?'}`);
|
|
await prisma.loanRecord.update({ where: { id: row.id }, data: { remark: null } });
|
|
if (row.purpose) {
|
|
await prisma.approvalTask.updateMany({
|
|
where: { bizId: row.id, bizType: 'LOAN_APPROVAL' },
|
|
data: { title: row.purpose },
|
|
});
|
|
}
|
|
continue;
|
|
}
|
|
usedLoan.add(pick.i);
|
|
await prisma.loanRecord.update({
|
|
where: { id: row.id },
|
|
data: { purpose: pick.o.purpose, remark: null },
|
|
});
|
|
await prisma.approvalTask.updateMany({
|
|
where: { bizId: row.id, bizType: 'LOAN_APPROVAL' },
|
|
data: { title: pick.o.purpose },
|
|
});
|
|
patchedLoans += 1;
|
|
}
|
|
|
|
console.log(
|
|
JSON.stringify(
|
|
{ patchedExpenses, unmatchedExpenses, patchedLoans, unmatchedLoans },
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
}
|
|
|
|
main()
|
|
.catch((e) => {
|
|
console.error(e);
|
|
process.exit(1);
|
|
})
|
|
.finally(() => prisma.$disconnect());
|