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