import { PrismaClient, Prisma } from '@prisma/client'; import * as fs from 'fs'; import * as path from 'path'; const prisma = new PrismaClient(); const DUMP = path.resolve('/opt/import/prod/oa_clean.json'); type Row = 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 = 200) { const s = str(v); return s.length > n ? s.slice(0, n) : s || undefined; } function realPartyName(raw: unknown) { let name = str(raw).replace(/\s+/g, ' '); if (!name) return ''; const alias: Record = { '73047': '中国人民解放军73047部队保障部', '中国人民解放军 32272 部队': '中国人民解放军32272部队', }; name = alias[name] || name; if (/^未指定/.test(name)) return ''; if (/^\d+$/.test(name)) return ''; if (name.startsWith('/') || name.endsWith('.pdf')) return ''; if (name.length < 2) return ''; return name; } 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 mapBidStatus(row: Row) { const win = str(row.winning_status); const st = str(row.project_status); if (st === '中标' || (st === '' && win.includes('中标'))) return 'WON'; if (st === '未中' || (st === '' && win.includes('未中'))) return 'LOST'; if (st === '流标') return 'FAILED'; if (st === '弃标' || win.includes('弃标')) return 'TERMINATED'; return 'PENDING'; } function mapContractStatus(v: unknown) { const s = str(v); if (s === '已通过') return 'APPROVED'; if (s === '审批中') return 'PENDING'; return 'DRAFT'; } function mapProjectStatus(v: unknown) { const s = str(v); if (s === '验收通过') return 'ACCEPTED'; if (s === '已完成') return 'DONE'; if (s === '待验收') return 'INTERNAL_ACCEPTED'; if (s === '未开始') return 'NOT_STARTED'; return 'ACTIVE'; } function mapExpenseStatus(v: unknown) { const s = str(v); if (s === '2') return 'APPROVED'; if (s === '3') return 'REJECTED'; if (s === '1') return 'PENDING'; return 'DRAFT'; } function mapLoanStatus(row: Row) { const st = str(row.approval_status); const offset = num(row.offset_amount); if (st === '2') { if (str(row.offset_status) === '2' || offset > 0) return offset >= num(row.amount) ? 'OFFSET' : 'OPEN'; return 'OPEN'; } if (st === '1') return 'PENDING'; if (st === '3') return 'REJECTED'; return 'DRAFT'; } function mapSealStatus(v: unknown) { const s = str(v); if (s === '2') return 'APPROVED'; if (s === '3') return 'REJECTED'; return 'PENDING'; } function mapItemCategory(itemType: string) { if (itemType.includes('交通') || itemType.includes('差旅')) return 'travel'; if (itemType.includes('餐')) return 'meal'; if (itemType.includes('住宿')) return 'stay'; if (itemType.includes('办公')) return 'office'; if (itemType.includes('招待')) return 'entertain'; return 'other'; } function isBidExpense(itemType: string) { return itemType.includes('投标') || itemType.includes('制标'); } async function purgeBusiness() { await prisma.smsSendLog.deleteMany(); await prisma.operationLog.deleteMany(); await prisma.paymentRecord.deleteMany(); await prisma.projectChange.deleteMany(); await prisma.todoTask.deleteMany(); await prisma.approvalTask.deleteMany(); await prisma.workReport.deleteMany(); await prisma.fileAsset.deleteMany(); await prisma.expenseClaim.deleteMany(); await prisma.loanRecord.deleteMany(); await prisma.bondRecord.deleteMany(); await prisma.invoiceRecord.deleteMany(); await prisma.timesheet.deleteMany(); await prisma.projectAcceptance.deleteMany(); await prisma.projectBudget.deleteMany(); await prisma.projectTask.deleteMany(); await prisma.purchaseRequest.deleteMany(); await prisma.assetOccupancy.deleteMany(); await prisma.contractReviewer.deleteMany(); await prisma.contractActionLog.deleteMany(); await prisma.paymentMilestone.deleteMany(); await prisma.sealRequest.deleteMany(); await prisma.credentialBorrow.deleteMany(); await prisma.project.deleteMany(); await prisma.contract.deleteMany(); await prisma.bidDocVersion.deleteMany(); await prisma.bidAssignee.deleteMany(); await prisma.bidActionLog.deleteMany(); await prisma.bidReviewer.deleteMany(); await prisma.bidCase.deleteMany(); await prisma.partyContact.deleteMany(); await prisma.businessParty.deleteMany(); } async function main() { const raw = JSON.parse(fs.readFileSync(DUMP, 'utf8')) as Record; const admin = await prisma.user.findFirst({ where: { username: 'admin' } }); if (!admin) throw new Error('本地没有 admin'); await purgeBusiness(); await prisma.legalEntity.updateMany({ data: { isDefault: false } }); let entity = await prisma.legalEntity.findFirst({ where: { tenantId: '1', name: '江苏风影随行科技有限公司' }, }); if (!entity) { entity = await prisma.legalEntity.create({ data: { name: '江苏风影随行科技有限公司', shortName: '江苏风影', isDefault: true }, }); } else { entity = await prisma.legalEntity.update({ where: { id: entity.id }, data: { isDefault: true } }); } const tenders = (raw.oa_tender || []).filter((r) => /^TB-\d/.test(str(r.system_number)) && str(r.project_name)); const partyByName = new Map(); const partyNames = new Set(); for (const t of tenders) { const n = realPartyName(t.tender_dan_wei); if (n) partyNames.add(n); } for (const c of raw.oa_contract || []) { const n = realPartyName(c.party_a_name); if (n && n !== '江苏风影随行科技有限公司') partyNames.add(n); } for (const name of partyNames) { const row = await prisma.businessParty.create({ data: { name, remark: 'OA 导入', level: 'B' }, }); partyByName.set(name, row.id); } for (const t of tenders) { const n = realPartyName(t.tender_dan_wei); const person = str(t.bid_contact_person); const phone = str(t.bid_contact_phone); if (!n || (!person && !phone)) continue; const partyId = partyByName.get(n); if (!partyId) continue; const exists = await prisma.partyContact.findFirst({ where: { partyId, name: person || '招标联系人' } }); if (!exists) { await prisma.partyContact.create({ data: { partyId, name: person || '招标联系人', phone: phone || undefined }, }); } } const bidByProd = new Map(); const bidByNo = new Map(); const bidByName = new Map(); for (const t of tenders) { const bidNo = str(t.system_number); const partyName = realPartyName(t.tender_dan_wei); const resultBits = [ t.winning_unit ? `中标单位:${t.winning_unit}` : '', num(t.winning_amount) ? `中标金额:${num(t.winning_amount)}` : '', str(t.opening_remark), str(t.bid_summary), ].filter(Boolean); const row = await prisma.bidCase.create({ data: { bidNo, name: clip(t.project_name, 200)!, externalNo: clip(t.project_number, 120) || undefined, projectType: mapProjectType(t.project_type, t.project_name), source: clip(t.project_source, 50) || undefined, legalEntityId: entity.id, partyId: partyName ? partyByName.get(partyName) : undefined, tenderMethod: clip(t.tender_method, 50) || undefined, applyEndAt: when(t.file_apply_deadline) || undefined, openAt: when(t.bid_open_date) || undefined, openPlace: clip(t.tender_address, 200) || undefined, securityLevel: str(t.security_level) || 'INTERNAL', projectNature: clip(t.project_nature, 50) || undefined, contactName: clip(t.bid_contact_person, 64) || undefined, contactPhone: clip(t.bid_contact_phone, 32) || undefined, bondMethod: clip(t.deposit_pay_method, 20) || undefined, bondStatus: clip(t.deposit_status, 20) || undefined, bondPaidAt: when(t.deposit_pay_date) || undefined, delivererName: clip(t.bid_delivery_person, 64) || undefined, priceCap: t.price_limit == null ? undefined : String(t.price_limit), quoteAmount: t.bid_quotation == null ? undefined : String(t.bid_quotation), summary: clip(t.project_intro, 2000) || undefined, remark: clip(t.remark, 1000) || undefined, resultNote: resultBits.join(';') || undefined, openedResultAt: when(t.winning_date) || when(t.bid_open_date) || undefined, status: mapBidStatus(t), createdById: admin.id, createdAt: when(t.create_time) || undefined, }, }); bidByProd.set(String(t.id), row.id); bidByNo.set(bidNo, row.id); bidByName.set(str(t.project_name), row.id); const deposit = num(t.bid_deposit); if (deposit > 0) { const paid = Boolean(when(t.deposit_pay_date)); const returned = Boolean(when(t.deposit_refund_date)); await prisma.bondRecord.create({ data: { bondNo: `BZJ-${bidNo}`, bidCaseId: row.id, bondType: 'TENDER', amount: new Prisma.Decimal(deposit), paidAt: when(t.deposit_pay_date) || undefined, returnedAt: when(t.deposit_refund_date) || undefined, status: returned ? 'RETURNED' : paid ? 'PAID' : 'UNPAID', remark: 'OA 投标保证金', }, }); } } const contractByProd = new Map(); const contractByNo = new Map(); const contractByName = new Map(); for (const c of raw.oa_contract || []) { const contractNo = str(c.contract_no); if (!contractNo) continue; const tb = contractNo.match(/TB-\d+-\d+/); const bidCaseId = tb ? bidByNo.get(tb[0]) : undefined; if (str(c.approval_status) === 'pending_submit' && !c.sign_date && !num(c.amount)) { continue; } const partyA = realPartyName(c.party_a_name); const partyB = str(c.party_b_name); if (partyA && !partyByName.has(partyA) && partyA !== '江苏风影随行科技有限公司') { const p = await prisma.businessParty.create({ data: { name: partyA, remark: 'OA 合同甲方', level: 'B' } }); partyByName.set(partyA, p.id); } const remark = [ str(c.payment_method) ? `付款:${c.payment_method}` : '', str(c.contract_lb) ? `类别:${c.contract_lb}` : '', str(c.remark), ] .filter(Boolean) .join(' / '); const row = await prisma.contract.create({ data: { contractNo, name: clip(c.contract_name, 200)!, bidCaseId, legalEntityId: entity.id, amount: new Prisma.Decimal(num(c.amount)), status: mapContractStatus(c.approval_status), partyA: partyA || undefined, partyB: partyB || undefined, remark: remark || undefined, signedAt: when(c.sign_date) || undefined, createdById: admin.id, createdAt: when(c.create_time) || undefined, }, }); contractByProd.set(String(c.id), row.id); contractByNo.set(contractNo, row.id); contractByName.set(str(c.contract_name), row.id); } const projectByProd = new Map(); const projectByName = new Map(); for (const p of raw.project_list || []) { const projectNo = str(p.system_number) || str(p.project_code); if (!projectNo || !str(p.project_name)) continue; const contractId = (p.contract_id != null ? contractByProd.get(String(p.contract_id)) : undefined) || (str(p.contract_name) ? contractByName.get(str(p.contract_name)) : undefined); const row = await prisma.project.create({ data: { projectNo, name: clip(p.project_name, 200)!, contractId, status: mapProjectStatus(p.project_status), createdAt: when(p.create_time) || undefined, }, }); projectByProd.set(String(p.project_id), row.id); projectByName.set(str(p.project_name), row.id); const budget = num(p.project_budget); if (budget > 0) { await prisma.projectBudget.create({ data: { projectId: row.id, category: 'PURCHASE', amount: new Prisma.Decimal(budget) }, }); } const bzj = num(p.project_bzj); if (bzj > 0) { await prisma.bondRecord.create({ data: { bondNo: `BZJ-${projectNo}`, projectId: row.id, contractId, bondType: 'WARRANTY', amount: new Prisma.Decimal(bzj), dueBackAt: when(p.bzj_date) || undefined, status: 'UNPAID', remark: str(p.bzj_type) ? `OA 项目质保金 ${p.bzj_type}` : 'OA 项目质保金', }, }); } } const itemsByReimb = new Map(); for (const item of raw.reimbursement_item || []) { const key = String(item.reimbursement_id); if (!itemsByReimb.has(key)) itemsByReimb.set(key, []); itemsByReimb.get(key)!.push(item); } for (const r of raw.reimbursement || []) { const claimNo = str(r.reimburse_no); if (!claimNo) continue; const lines = itemsByReimb.get(String(r.id)) || []; const main = [...lines].sort((a, b) => num(b.amount) - num(a.amount))[0]; const itemType = str(main?.item_type); const cat = mapItemCategory(itemType); const projectId = r.project_id != null ? projectByProd.get(String(r.project_id)) : projectByName.get(str(r.project_name)); const bidCaseId = isBidExpense(itemType) ? bidByName.get(str(r.project_name)) : !projectId ? bidByName.get(str(r.project_name)) : undefined; const costType = isBidExpense(itemType) || bidCaseId ? 'BID' : projectId ? 'PROJECT' : 'DEPT'; await prisma.expenseClaim.create({ data: { claimNo, applicantId: admin.id, costType, budgetCategory: costType === 'PROJECT' ? (cat === 'office' ? 'PURCHASE' : 'TRAVEL') : undefined, projectId: costType === 'PROJECT' ? projectId : undefined, bidCaseId: costType === 'BID' ? bidCaseId : undefined, expenseCategory: cat, purpose: clip(r.subject || r.project_name, 200) || undefined, amount: new Prisma.Decimal(num(r.total_amount)), status: mapExpenseStatus(r.approval_status), remark: clip(r.remark, 1000) || undefined, submittedAt: when(r.created_time) || undefined, decidedAt: str(r.approval_status) === '2' ? when(r.created_time) || undefined : undefined, decidedById: str(r.approval_status) === '2' ? admin.id : undefined, }, }); } for (const r of raw.borrow_advance || []) { const loanNo = str(r.advance_no); if (!loanNo) continue; const projectId = r.project_id != null ? projectByProd.get(String(r.project_id)) : projectByName.get(str(r.project_name)); await prisma.loanRecord.create({ data: { loanNo, applicantId: admin.id, costType: projectId ? 'PROJECT' : 'DEPT', projectId, amount: new Prisma.Decimal(num(r.amount)), offsetAmount: new Prisma.Decimal(num(r.offset_amount)), purpose: clip(r.reason || r.subject, 200) || 'OA 借款', status: mapLoanStatus(r), remark: undefined, submittedAt: when(r.apply_time) || undefined, }, }); } for (const r of raw.oa_seal_apply || []) { const reason = clip(r.content || r.apply_subject, 500) || '用章'; const bidHit = tenders.find((t) => str(t.project_name) && reason.includes(str(t.project_name))); await prisma.sealRequest.create({ data: { sealType: clip(r.seal_type, 40) || '公章', reason, bidCaseId: bidHit ? bidByProd.get(String(bidHit.id)) : undefined, applicantId: admin.id, takeOut: str(r.is_borrow) === '是', returnAt: when(r.borrow_end) || undefined, status: mapSealStatus(r.approval_status), createdAt: when(r.create_time) || undefined, }, }); } for (const r of raw.oa_purchasing || []) { const title = [str(r.contract_no), str(r.contract_name)].filter(Boolean).join(' '); if (!title) continue; await prisma.purchaseRequest.create({ data: { title: clip(title, 200)!, amount: new Prisma.Decimal(num(r.amount)), status: str(r.approval_status) === '已通过' ? 'APPROVED' : str(r.approval_status) === '审批中' ? 'PENDING' : 'DRAFT', remark: [r.party_a_name, r.payment_method, r.sign_date, r.remark].map(str).filter(Boolean).join(' / ') || undefined, applicantId: admin.id, }, }); } const counts = { parties: partyByName.size, bids: tenders.length, contracts: (raw.oa_contract || []).length, projects: (raw.project_list || []).length, expenses: (raw.reimbursement || []).length, loans: (raw.borrow_advance || []).length, seals: (raw.oa_seal_apply || []).length, purchases: (raw.oa_purchasing || []).length, }; console.log(JSON.stringify(counts, null, 2)); } main() .catch((e) => { console.error(e); process.exit(1); }) .finally(() => prisma.$disconnect());