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:
2026-09-02 10:04:03 +00:00
commit 76f266645d
507 changed files with 99891 additions and 0 deletions
+186
View File
@@ -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());