76f266645d
含 Phase 1.1 IM seq 排序、断线重连、多端已读同步与微信式语音转文字 UI。 排除 node_modules、构建产物、安装包与 .env 密钥。 Co-authored-by: Cursor <cursoragent@cursor.com>
187 lines
5.4 KiB
TypeScript
187 lines
5.4 KiB
TypeScript
import { Prisma, PrismaClient } from '@prisma/client';
|
||
import * as fs from 'fs';
|
||
|
||
const prisma = new PrismaClient();
|
||
const DUMP = '/opt/import/prod/oa_flow_finance.json';
|
||
const APPLY = process.argv.includes('--apply');
|
||
|
||
type Row = Record<string, unknown>;
|
||
type Bid = {
|
||
id: string;
|
||
bidNo: string;
|
||
name: string;
|
||
externalNo: string | null;
|
||
status: string;
|
||
winnerName: string | null;
|
||
winnerAmount: Prisma.Decimal | null;
|
||
winnerNote: string | null;
|
||
};
|
||
|
||
function str(v: unknown) {
|
||
if (v == null) return '';
|
||
return String(v).trim();
|
||
}
|
||
|
||
function num(v: unknown) {
|
||
if (v == null || v === '') return null;
|
||
const n = Number(String(v).replace(/,/g, ''));
|
||
if (!Number.isFinite(n) || n === 0) return null;
|
||
return n;
|
||
}
|
||
|
||
function compactName(name: string) {
|
||
return str(name)
|
||
.replace(/\s+/g, '')
|
||
.replace(/[((].*?[))]/g, '')
|
||
.replace(/招标公告|采购公告|询价公告|竞争性谈判公告|谈判公告|比价公告|需求公示|意向公开/g, '');
|
||
}
|
||
|
||
function isOwnWinner(name: string) {
|
||
return /风影|随行科技|山东鲁兵/.test(name) || name === '隆创';
|
||
}
|
||
|
||
function officialNo(raw: unknown) {
|
||
const n = str(raw).replace(/[))]+$/, '').trim();
|
||
if (!n || n === '无' || n === '-' || n === '—') return '';
|
||
return n;
|
||
}
|
||
|
||
function pickUnused(cands: Bid[], used: Set<string>) {
|
||
const open = cands.filter((b) => !used.has(b.id));
|
||
if (!open.length) return null;
|
||
const yt = open.find((b) => b.bidNo.startsWith('YT-'));
|
||
return yt || open[0];
|
||
}
|
||
|
||
async function main() {
|
||
if (!fs.existsSync(DUMP)) {
|
||
throw new Error(`找不到 OA 备份 ${DUMP}`);
|
||
}
|
||
const raw = JSON.parse(fs.readFileSync(DUMP, 'utf8')) as Record<string, Row[]>;
|
||
const pres = raw.project_preinvest_apply || [];
|
||
const sources = pres.filter((p) => {
|
||
const deleted = str(p.deleted) === '1';
|
||
const winner = str(p.winning_unit);
|
||
const cat = str(p.screening_category) || str(p.project_type);
|
||
return deleted && winner && cat.includes('软件');
|
||
});
|
||
|
||
const bids = await prisma.bidCase.findMany({
|
||
where: { tenantId: '1', status: 'TERMINATED' },
|
||
select: {
|
||
id: true,
|
||
bidNo: true,
|
||
name: true,
|
||
externalNo: true,
|
||
status: true,
|
||
winnerName: true,
|
||
winnerAmount: true,
|
||
winnerNote: true,
|
||
},
|
||
});
|
||
|
||
const byNo = new Map<string, Bid[]>();
|
||
const byName = new Map<string, Bid[]>();
|
||
const byCompact = new Map<string, Bid[]>();
|
||
const add = (map: Map<string, Bid[]>, key: string, row: Bid) => {
|
||
if (!key) return;
|
||
const list = map.get(key) || [];
|
||
list.push(row);
|
||
map.set(key, list);
|
||
};
|
||
for (const b of bids) {
|
||
add(byNo, b.bidNo, b);
|
||
add(byNo, str(b.externalNo), b);
|
||
add(byName, b.name, b);
|
||
add(byCompact, compactName(b.name), b);
|
||
}
|
||
|
||
const used = new Set<string>();
|
||
const updated: { bidNo: string; name: string; winner: string; amount: number | null; via: string }[] = [];
|
||
const skippedSame: string[] = [];
|
||
const conflicts: { bidNo: string; have: string; want: string }[] = [];
|
||
const unmatched: { applyNo: string; name: string; winner: string }[] = [];
|
||
const ownSkipped: string[] = [];
|
||
|
||
for (const p of sources) {
|
||
const applyNo = str(p.apply_no);
|
||
const name = str(p.project_name);
|
||
const official = officialNo(p.project_number);
|
||
const winner = str(p.winning_unit);
|
||
const amount = num(p.winning_amount);
|
||
if (isOwnWinner(winner)) {
|
||
ownSkipped.push(`${applyNo} ${winner}`);
|
||
continue;
|
||
}
|
||
|
||
const hit =
|
||
pickUnused(byNo.get(applyNo) || [], used) ||
|
||
pickUnused(byNo.get(official) || [], used) ||
|
||
pickUnused(byName.get(name) || [], used) ||
|
||
pickUnused(byCompact.get(compactName(name)) || [], used);
|
||
|
||
if (!hit) {
|
||
unmatched.push({ applyNo, name, winner });
|
||
continue;
|
||
}
|
||
used.add(hit.id);
|
||
|
||
const haveName = str(hit.winnerName);
|
||
const haveAmt = hit.winnerAmount != null ? Number(hit.winnerAmount) : null;
|
||
if (haveName) {
|
||
const sameName = haveName === winner;
|
||
const sameAmt = amount == null || haveAmt === amount;
|
||
if (sameName && sameAmt) skippedSame.push(hit.bidNo);
|
||
else conflicts.push({ bidNo: hit.bidNo, have: `${haveName}/${haveAmt}`, want: `${winner}/${amount}` });
|
||
continue;
|
||
}
|
||
|
||
const note = str(hit.winnerNote) || 'OA 软件已删除迁入';
|
||
if (APPLY) {
|
||
await prisma.bidCase.update({
|
||
where: { id: hit.id },
|
||
data: {
|
||
winnerName: winner,
|
||
winnerAmount: amount == null ? undefined : new Prisma.Decimal(amount),
|
||
winnerNote: note,
|
||
},
|
||
});
|
||
}
|
||
updated.push({
|
||
bidNo: hit.bidNo,
|
||
name: hit.name,
|
||
winner,
|
||
amount,
|
||
via: hit.bidNo === applyNo ? 'bidNo' : str(hit.externalNo) === applyNo || hit.bidNo === official ? 'official' : 'name',
|
||
});
|
||
}
|
||
|
||
console.log(
|
||
JSON.stringify(
|
||
{
|
||
apply: APPLY,
|
||
dumpSources: sources.length,
|
||
terminated: bids.length,
|
||
filled: updated.length,
|
||
skippedSame: skippedSame.length,
|
||
conflicts: conflicts.length,
|
||
unmatched: unmatched.length,
|
||
ownSkipped: ownSkipped.length,
|
||
filledSample: updated.slice(0, 8),
|
||
conflictRows: conflicts,
|
||
unmatchedRows: unmatched.slice(0, 20),
|
||
tinyAmounts: updated.filter((u) => u.amount != null && u.amount < 100),
|
||
},
|
||
null,
|
||
2,
|
||
),
|
||
);
|
||
}
|
||
|
||
main()
|
||
.catch((e) => {
|
||
console.error(e);
|
||
process.exit(1);
|
||
})
|
||
.finally(() => prisma.$disconnect());
|