Files
Public/apps/api/prisma/backfill-contract-buckets.ts
daiyongkang 76f266645d Initial commit: 风影 OA 全栈源码(API/Web/Flutter/IM)
含 Phase 1.1 IM seq 排序、断线重连、多端已读同步与微信式语音转文字 UI。
排除 node_modules、构建产物、安装包与 .env 密钥。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-02 10:04:03 +00:00

122 lines
4.5 KiB
TypeScript

import { PrismaClient } from '@prisma/client';
import * as fs from 'fs';
import { parseOaJson } from './oa-json';
const prisma = new PrismaClient();
function str(v: unknown) {
if (v == null) return '';
return String(v).trim();
}
function compactName(name: string) {
return name.replace(/[\s()()【】\[\]《》·,,。.\-_/\\]/g, '').toLowerCase();
}
function namesOverlap(a: string, b: string) {
const na = compactName(a);
const nb = compactName(b);
if (!na || !nb) return false;
if (na === nb) return true;
const shorter = na.length <= nb.length ? na : nb;
const longer = na.length <= nb.length ? nb : na;
return shorter.length >= 8 && longer.includes(shorter);
}
async function linkContracts() {
const bids = await prisma.bidCase.findMany({
select: { id: true, bidNo: true, externalNo: true, name: true },
});
const contracts = await prisma.contract.findMany({
where: { bidCaseId: null },
select: { id: true, name: true, contractNo: true },
});
let linked = 0;
for (const c of contracts) {
const no = c.contractNo || '';
const tb = no.match(/TB-\d+-\d+/);
const hits = bids.filter((b) => {
if (tb && (b.bidNo === tb[0] || b.bidNo.includes(tb[0]) || (b.externalNo && b.externalNo.includes(tb[0])))) {
return true;
}
if (no && (b.bidNo && no.includes(b.bidNo))) return true;
return namesOverlap(c.name, b.name);
});
const unique = [...new Map(hits.map((h) => [h.id, h])).values()];
if (unique.length !== 1) continue;
await prisma.contract.update({ where: { id: c.id }, data: { bidCaseId: unique[0].id } });
linked += 1;
}
return { scanned: contracts.length, linked };
}
async function backfillSealApplicants() {
const dumps = ['/opt/import/prod/oa_fresh.json', '/opt/import/prod/oa_core.json', '/opt/import/prod/oa_clean.json'];
const dump = dumps.find((p) => fs.existsSync(p));
if (!dump) return { updated: 0, skipped: 'no dump' };
const raw = parseOaJson<Record<string, unknown>>(dump);
const hrPath = '/opt/import/prod/oa_hr.json';
const hr = fs.existsSync(hrPath) ? parseOaJson<{ users?: Record<string, unknown>[] }>(hrPath) : { users: [] };
const users = await prisma.user.findMany();
const byUsername = new Map(users.map((u) => [u.username, u]));
const byName = new Map(users.map((u) => [u.displayName, u]));
const admin = users.find((u) => u.displayName === '系统管理员');
const oaUser = new Map((hr.users || []).map((u) => [String(u.id), str(u.userName || u.username)]));
const userFromOaId = (id: unknown) => {
const uname = oaUser.get(str(id));
return uname ? byUsername.get(uname) : undefined;
};
const applies = (raw.oa_seal_apply || raw.oa_seal || []) as Record<string, unknown>[];
const rows = await prisma.sealRequest.findMany({
include: { applicant: { select: { displayName: true } } },
});
let updated = 0;
for (const r of rows) {
if (r.applicant?.displayName && r.applicant.displayName !== '系统管理员') continue;
const reason = r.reason || '';
const created = r.createdAt ? r.createdAt.getTime() : 0;
const hit = applies.find((a) => {
const content = str(a.content || a.apply_subject || a.reason);
if (content && reason && (content === reason || reason.includes(content) || content.includes(reason))) return true;
const t = str(a.create_time);
if (t && created) {
const d = new Date(t.includes('T') ? t : t.replace(' ', 'T'));
return Math.abs(d.getTime() - created) < 2000 && str(a.seal_type || '') === r.sealType;
}
return false;
});
if (!hit) continue;
const who =
byName.get(str(hit.applicant_name) === '张靖妍' ? '张婧妍' : str(hit.applicant_name)) ||
userFromOaId(hit.applicant_id) ||
userFromOaId(hit.create_by) ||
userFromOaId(hit.creator_id) ||
userFromOaId(hit.apply_user);
if (!who || who.id === admin?.id) continue;
await prisma.sealRequest.update({ where: { id: r.id }, data: { applicantId: who.id } });
updated += 1;
}
return { updated, scanned: rows.length };
}
async function main() {
const contracts = await linkContracts();
const seals = await backfillSealApplicants();
const [tender, incremental] = await Promise.all([
prisma.contract.count({ where: { bidCaseId: { not: null } } }),
prisma.contract.count({ where: { bidCaseId: null } }),
]);
console.log(JSON.stringify({ contracts, seals, tender, incremental }, null, 2));
}
main()
.then(async () => prisma.$disconnect())
.catch(async (e) => {
console.error(e);
await prisma.$disconnect();
process.exit(1);
});