Files
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

476 lines
17 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string, unknown>;
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<string, string> = {
'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<string, Row[]>;
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<string, string>();
const partyNames = new Set<string>();
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<string, string>();
const bidByNo = new Map<string, string>();
const bidByName = new Map<string, string>();
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<string, string>();
const contractByNo = new Map<string, string>();
const contractByName = new Map<string, string>();
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<string, string>();
const projectByName = new Map<string, string>();
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<string, Row[]>();
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());