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