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