76f266645d
含 Phase 1.1 IM seq 排序、断线重连、多端已读同步与微信式语音转文字 UI。 排除 node_modules、构建产物、安装包与 .env 密钥。 Co-authored-by: Cursor <cursoragent@cursor.com>
122 lines
4.5 KiB
TypeScript
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);
|
|
});
|