// @ts-nocheck import { PrismaClient, Prisma } from '@prisma/client'; import { createHash, randomUUID } from 'crypto'; import * as fs from 'fs'; import * as path from 'path'; const prisma = new PrismaClient(); const DUMP = '/opt/import/prod/oa_core.json'; const HR = '/opt/import/prod/oa_hr.json'; const OA_FILES = '/opt/import/prod/oa-files'; const UPLOAD = path.join(process.cwd(), 'uploads'); type Row = Record; const MIME: Record = { pdf: 'application/pdf', jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', gif: 'image/gif', webp: 'image/webp', doc: 'application/msword', docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', xls: 'application/vnd.ms-excel', xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', zip: 'application/zip', rar: 'application/x-rar-compressed', }; const NAME_ALIAS: Record = { 张靖妍: '张婧妍', }; function str(v: unknown) { if (v == null) return ''; return String(v).trim(); } function num(v: unknown) { if (v == null || v === '') return 0; const n = Number(v); return Number.isFinite(n) ? n : 0; } function when(v: unknown) { const s = str(v); if (!s || s.startsWith('0000')) return null; const d = new Date(s.includes('T') ? s : s.replace(' ', 'T')); return Number.isNaN(d.getTime()) ? null : d; } function clip(v: unknown, n = 2000) { const s = str(v); return s.length > n ? s.slice(0, n) : s || undefined; } function mapProjectType(v: unknown, projectName?: unknown) { const name = str(projectName); if (name.includes('三维')) return '三维'; if (/(视频.{0,12}(制作|摄制|拍摄)|摄制|拍摄|摄影|宣传片|专题片|纪录片|微电影|影视制作)/.test(name)) return '摄制'; const s = str(v); if (s.includes('三维')) return '三维'; if (s.includes('摄制') || s.includes('视频')) return '摄制'; if (s.includes('物资')) return '物资'; if (s.includes('集成')) return '集成'; return '软件'; } function mapQual(raw: unknown) { const s = str(raw); if (!s) return '无需资质'; try { const arr = JSON.parse(s) as { item?: string }[]; const item = str(arr?.[0]?.item); if (!item || item === '无') return '无需资质'; if (item.includes('军')) return '军二'; if (item.includes('涉')) return '涉乙'; return item.slice(0, 20); } catch { if (s.includes('军')) return '军二'; if (s.includes('涉')) return '涉乙'; return '其他'; } } function profilePaths(v: unknown) { const out: string[] = []; if (!v) return out; for (const chunk of str(v).split(/[,;]/)) { const piece = chunk.trim(); const first = piece.split('|')[0].trim(); const idx = first.indexOf('/profile/upload/'); const p = idx >= 0 ? first.slice(idx) : first; if (p.startsWith('/profile/upload/')) out.push(p); } return out; } function relOf(p: string) { return p.replace(/^\/profile\/upload\//, ''); } function displayName(p: string) { const pipe = str(p).split('|')[1]; if (pipe && !pipe.includes('/')) return path.basename(pipe); return path.basename(relOf(p.split('|')[0])); } function mimeOf(name: string) { const ext = name.split('.').pop()?.toLowerCase() || 'bin'; return MIME[ext] || 'application/octet-stream'; } function prettyName(name: string) { try { return decodeURIComponent(name); } catch { return name; } } async function attach( bizType: string, bizId: string, urls: unknown, uploadedById: string, seen: Set, ) { let n = 0; let firstNas: string | undefined; for (const raw of profilePaths(urls)) { const rel = relOf(raw); const src = path.join(OA_FILES, rel); if (!fs.existsSync(src)) continue; const buf = fs.readFileSync(src); const hash = createHash('sha1').update(buf).digest('hex').slice(0, 16); const key = `${bizId}:${hash}`; if (seen.has(key)) continue; seen.add(key); const id = randomUUID(); const ext = (path.extname(rel).replace('.', '') || 'bin').replace(/[^a-zA-Z0-9]/g, '').slice(0, 8); const storageKey = `OA/${bizType}/${bizId}/${id}.${ext || 'bin'}`; const dest = path.join(UPLOAD, storageKey); fs.mkdirSync(path.dirname(dest), { recursive: true }); fs.copyFileSync(src, dest); const name = prettyName(displayName(raw) || path.basename(rel)); await prisma.fileAsset.create({ data: { id, tenantId: '1', bizType, bizId, fileName: name.slice(0, 180), mimeType: mimeOf(name), size: buf.length, storageKey, uploadedById, }, }); n += 1; firstNas ||= raw; } return { n, firstNas }; } async function main() { const raw = JSON.parse(fs.readFileSync(DUMP, 'utf8')) as Record; const hr = JSON.parse(fs.readFileSync(HR, 'utf8')) as { users: { id: number; userName: string; nickName: string }[] }; const admin = await prisma.user.findFirst({ where: { username: 'admin' } }); if (!admin) throw new Error('没有 admin'); fs.mkdirSync(UPLOAD, { recursive: true }); 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])); for (const [a, b] of Object.entries(NAME_ALIAS)) { const u = byName.get(b); if (u) byName.set(a, u); } const oaUser = new Map(hr.users.map((u) => [String(u.id), u.userName])); const userFromOaId = (id: unknown) => { const uname = oaUser.get(str(id)); return uname ? byUsername.get(uname) : undefined; }; const userFromName = (name: unknown) => { const n = NAME_ALIAS[str(name)] || str(name); return byName.get(n); }; await prisma.fileAsset.deleteMany({ where: { storageKey: { startsWith: 'OA/' } } }); const seen = new Set(); const stats = { bids: 0, bidFiles: 0, contracts: 0, contractFiles: 0, expenses: 0, expenseFiles: 0, projects: 0, purchases: 0 }; const bids = await prisma.bidCase.findMany(); const bidByName = new Map(bids.map((b) => [b.name, b])); const tenders = (raw.oa_tender || []).filter((t) => /^TB-/.test(str(t.system_number)) && str(t.project_name)); for (const t of tenders) { const local = bidByName.get(str(t.project_name)); if (!local) { console.warn('no local bid', t.project_name); continue; } const creator = userFromOaId(t.create_by) || userFromName(t.bid_prepare_person) || admin; const drafter = userFromName(t.bid_prepare_person); const bidder = userFromOaId(t.bid_delivery_person_id) || userFromName(t.bid_delivery_person); const announce = await attach('BID_ANNOUNCE', local.id, t.announcement_file, creator.id, seen); const extra = await attach('BID_FILE', local.id, t.attachment, creator.id, seen); const formal = await attach('BID_FORMAL', local.id, t.formal_bid_file, creator.id, seen); const proof = await attach('BID_PROOF', local.id, t.submit_proof_file, creator.id, seen); const nas = announce.firstNas || extra.firstNas || formal.firstNas || proof.firstNas; await prisma.bidCase.update({ where: { id: local.id }, data: { externalNo: clip(t.project_number, 120) || local.externalNo, projectType: mapProjectType(t.project_type, t.project_name), source: clip(t.project_source, 50) || local.source, qualificationNeed: mapQual(t.bid_doc_requirement), tenderMethod: clip(t.tender_method, 50) || local.tenderMethod, applyStartAt: when(t.create_time) || local.applyStartAt, applyEndAt: when(t.file_apply_deadline) || local.applyEndAt, applyMethod: announce.firstNas || extra.firstNas ? '网上申领' : '其他', openAt: when(t.bid_open_date) || local.openAt, openMethod: str(t.tender_address) ? '线下开标' : '其他', openPlace: clip(t.tender_address, 200) || local.openPlace, projectNature: clip(t.project_nature, 50) || local.projectNature, securityLevel: str(t.security_level) || local.securityLevel, contactName: clip(t.bid_contact_person, 64) || local.contactName, contactPhone: clip(t.bid_contact_phone, 32) || local.contactPhone, bondMethod: clip(t.deposit_pay_method, 20) || local.bondMethod, bondStatus: clip(t.deposit_status, 20) || local.bondStatus, bondPaidAt: when(t.deposit_pay_date) || local.bondPaidAt, delivererName: clip(t.bid_delivery_person, 64) || local.delivererName, priceCap: t.price_limit == null || t.price_limit === '' ? local.priceCap : String(t.price_limit), quoteAmount: t.bid_quotation == null || t.bid_quotation === '' ? local.quoteAmount : String(t.bid_quotation), summary: clip(t.project_content || t.project_intro, 4000) || local.summary, nasPath: nas || local.nasPath, remark: clip(t.remark, 1000) || local.remark, createdById: creator.id, }, }); if (drafter) { await prisma.bidAssignee.upsert({ where: { bidCaseId_userId_role: { bidCaseId: local.id, userId: drafter.id, role: 'DRAFTER' } }, update: {}, create: { bidCaseId: local.id, userId: drafter.id, role: 'DRAFTER' }, }); } if (bidder) { await prisma.bidAssignee.upsert({ where: { bidCaseId_userId_role: { bidCaseId: local.id, userId: bidder.id, role: 'BIDDER' } }, update: {}, create: { bidCaseId: local.id, userId: bidder.id, role: 'BIDDER' }, }); } if (formal.firstNas) { const exists = await prisma.bidDocVersion.findFirst({ where: { bidCaseId: local.id, versionNo: 1 } }); if (!exists) { await prisma.bidDocVersion.create({ data: { bidCaseId: local.id, versionNo: 1, nasPath: formal.firstNas, note: 'OA 正式标书', submittedById: (drafter || creator).id, }, }); } else { await prisma.bidDocVersion.update({ where: { id: exists.id }, data: { nasPath: formal.firstNas }, }); } } stats.bids += 1; stats.bidFiles += announce.n + extra.n + formal.n + proof.n; } const contracts = await prisma.contract.findMany(); const contractByName = new Map(contracts.map((c) => [c.name, c])); for (const c of raw.oa_contract || []) { const local = contractByName.get(str(c.contract_name)); if (!local) { console.warn('no local contract', c.contract_name); continue; } const creator = userFromOaId(c.create_by) || userFromOaId(c.creator_id) || admin; const original = await attach('CONTRACT', local.id, c.contract_original_url, creator.id, seen); const accept = await attach('CONTRACT_ACCEPT', local.id, c.acceptance_url, creator.id, seen); const award = await attach('CONTRACT_AWARD', local.id, c.award_notice_url, creator.id, seen); await prisma.contract.update({ where: { id: local.id }, data: { nasPath: original.firstNas || accept.firstNas || award.firstNas || local.nasPath, signedAt: when(c.sign_date) || local.signedAt, createdById: creator.id, partyA: local.partyA || str(c.party_a_name) || undefined, partyB: local.partyB || str(c.party_b_name) || undefined, remark: local.remark || [str(c.payment_method) && `付款:${c.payment_method}`, str(c.remark)] .filter(Boolean) .join(' / ') || undefined, }, }); stats.contracts += 1; stats.contractFiles += original.n + accept.n + award.n; } const expenses = await prisma.expenseClaim.findMany(); for (const r of raw.reimbursement || []) { const amount = num(r.total_amount); const name = str(r.employee_name); const subject = str(r.subject); let hits = expenses.filter((e) => Math.abs(Number(e.amount) - amount) < 0.009 && (e.remark || '').includes(name)); if (subject) { const tighter = hits.filter((e) => (e.remark || '').includes(subject)); if (tighter.length) hits = tighter; } const stamp = when(r.created_time); if (stamp && hits.length > 1) { const day = stamp.toISOString().slice(0, 10); const byDay = hits.filter((e) => (e.submittedAt || e.createdAt).toISOString().slice(0, 10) === day); if (byDay.length) hits = byDay; } const local = hits[0]; if (!local) continue; const who = userFromName(name) || users.find((u) => u.id === local.applicantId) || admin; const items = (raw.reimbursement_item || []).filter((i) => String(i.reimbursement_id) === String(r.id)); let n = 0; for (const item of items) { n += (await attach('EXPENSE', local.id, item.url, who.id, seen)).n; } n += (await attach('EXPENSE', local.id, r.attachment_url, who.id, seen)).n; if (n) { stats.expenses += 1; stats.expenseFiles += n; } } const projects = await prisma.project.findMany(); const projectByName = new Map(projects.map((p) => [p.name, p])); for (const p of raw.project_list || []) { const local = projectByName.get(str(p.project_name)); if (!local) continue; const n = await attach('PROJECT', local.id, p.bzj_attachment, admin.id, seen); stats.projects += n.n ? 1 : 0; } const purchases = await prisma.purchaseRequest.findMany(); for (const r of raw.oa_purchasing || []) { const titleBits = [str(r.contract_no), str(r.contract_name)].filter(Boolean); const local = purchases.find((p) => titleBits.some((b) => p.title.includes(b))); if (!local) continue; const who = userFromOaId(r.create_by) || userFromOaId(r.creator_id) || admin; const a = await attach('PURCHASE', local.id, r.contract_original_url, who.id, seen); const b = await attach('PURCHASE_ACCEPT', local.id, r.acceptance_url, who.id, seen); if (a.n + b.n) stats.purchases += 1; } const fileCount = await prisma.fileAsset.count(); console.log(JSON.stringify({ ...stats, fileAssetTotal: fileCount }, null, 2)); } main() .catch((e) => { console.error(e); process.exit(1); }) .finally(() => prisma.$disconnect());