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; 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) { 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; 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(); const byName = new Map(); const byCompact = new Map(); const add = (map: Map, 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(); 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());