// 浏览器测试的 API 边界替身;绝不向真实业务接口发请求。 const pdfObjects = [ '<< /Type /Catalog /Pages 2 0 R >>', '<< /Type /Pages /Kids [3 0 R] /Count 1 >>', '<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Resources << >> /Contents 4 0 R >>', '<< /Length 0 >>\nstream\n\nendstream' ] let pdfFixture = '%PDF-1.4\n' const offsets = [0] pdfObjects.forEach((obj, i) => { offsets.push(Buffer.byteLength(pdfFixture)) pdfFixture += `${i + 1} 0 obj\n${obj}\nendobj\n` }) const xref = Buffer.byteLength(pdfFixture) pdfFixture += `xref\n0 5\n0000000000 65535 f \n${offsets .slice(1) .map((n) => `${String(n).padStart(10, '0')} 00000 n `) .join('\n')}\ntrailer\n<< /Size 5 /Root 1 0 R >>\nstartxref\n${xref}\n%%EOF` export function makePlan(index, overrides = {}) { const id = `plan-${index}` const toxic = { id: `${id}-toxic`, planId: id, productId: 'toxic', productNo: 'CP001', productName: '测试易制毒产品', brandName: '测试品牌', aliasName: '测试别名', model: '99%', unit: 'ton', quantity: 12, actualQty: 11, isToxic: 1, isExplosive: 0, purchaseCertUrl: '', transportCertUrl: '', legalUseCertUrl: '' } const explosive = { ...toxic, id: `${id}-explosive`, productId: 'explosive', productNo: 'CP002', productName: '测试易制爆产品', isToxic: 0, isExplosive: 1 } const stopProducts = [toxic, explosive].map((p) => ({ ...p, id: `stop-${p.id}`, planProductId: p.id, planQty: 12, inboundQty: 10, outboundQty: 9, contractId: 'contract-1', contractNo: 'HT-001', packageSpec: 'bulk', remark: '产品备注' })) return { id, planNo: `YS${String(index).padStart(3, '0')}`, planDate: '2026-10-17', createDate: '2026-10-17', createTime: '2026-10-17 09:15:00', belongsCompany: 'shanxia', planType: ['inbound', 'outbound', 'direct'][(index - 1) % 3], followStatus: 'pending_arrange', transportStatus: 'pending', archiveStatus: 'pending', completeStatus: 'pending', hasFreight: 1, freightAmount: 1234, freightRemark: '测试运费说明', totalAmount: 12345, hazardCertReady: 0, followUserName: '测试跟进人', licensePlate: '鄂E12345', trailerNo: '鄂E1234挂', driverName: '测试司机', escortName: '测试押运员', personnelSummary: '司机:测试司机;押运:测试押运员', deliveryTimes: ['2026-10-18 10:30:00', '2026-10-19 14:20:00'], remark: '证书迁移测试备注', products: [toxic, explosive], documentAttachments: [{ url: '/fixtures/plan.pdf', label: '计划附件.pdf' }], load: { id: `${id}-load`, customerName: '测试供应商', address: '湖北省宜昌市装货地址', loadTime: '2026-10-17 09:20:00', contact: '张师傅 13800000000', notes: '装货注意事项', contracts: [{ contractId: 'contract-1', contractNo: 'HT-001' }], products: stopProducts }, deliveries: [1, 2].map((seq) => ({ id: `${id}-delivery-${seq}`, seq, customerName: `测试需方${seq}`, address: `送货地址${seq}`, deliveryTime: `2026-10-${17 + seq} 10:30:00`, contact: '李师傅 13900000000', notes: `送货注意事项${seq}`, generateDibang: 0, contracts: [{ contractId: 'contract-1', contractNo: 'HT-001' }], products: stopProducts.map((p) => ({ ...p, id: `${p.id}-${seq}`, planQty: 6 })) })), ...overrides } } export async function installCertFixture(context) { const state = { plans: Array.from({ length: 24 }, (_, i) => makePlan(i + 1)), codes: [ 'mini:pl:plan-cert:list', 'pl:plan-cert:list', 'pl:plan-cert:upload', 'pl:plan-cert:ready' ], calls: [], unknown: [], failList: false, failSave: false, failReadyIds: [], uploadGate: null, listDelay: null, linkedListGate: null, failPermissions: false } state.plans[3].archiveStatus = 'archived' state.plans[4].followStatus = 'cancelled' state.plans[5].hazardCertReady = 1 state.plans[6].transportStatus = 'arranged' state.plans[6].followStatus = 'arranged' state.plans[6].transportId = 'transport-7' await context.addInitScript(() => { window.__copiedText = '' Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText: async (text) => { window.__copiedText = text } } }) window.wx = { miniProgram: { navigateBack() {}, reLaunch() {}, getEnv(cb) { cb({ miniprogram: true }) } } } }) await context.route('**/fixtures/**', (route) => route.fulfill( route.request().url().endsWith('.png') ? { contentType: 'image/png', body: Buffer.from( 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+cL1sAAAAASUVORK5CYII=', 'base64' ) } : { contentType: 'application/pdf', body: pdfFixture } ) ) await context.route('https://api.qrserver.com/**', (route) => route.fulfill({ contentType: 'image/svg+xml', body: 'Mock QR preview' }) ) await context.route('**/api/**', async (route) => { const request = route.request() const url = new URL(request.url()) if (!url.pathname.startsWith('/api/')) return route.continue() const path = url.pathname.slice(4) const method = request.method() let body = {} try { body = request.postDataJSON() || {} } catch { body = {} } const params = Object.fromEntries(url.searchParams) state.calls.push({ path, method, body, params }) let data let error = '' if (path === '/web/auth/mp-webview-exchange') { data = { token: 'fixture-token', refreshToken: 'fixture-refresh', userInfo: { id: 'user-1', userId: 'user-1', userName: '测试用户', role: 'admin', roles: ['admin'] } } } else if (path === '/web/auth/menu-codes') { data = { all: false, isSuperAdmin: false, menuCodes: state.codes } if (state.failPermissions) error = '权限加载失败' } else if (path === '/web/admin/dict-data/all') { data = { crm_product_unit: [{ value: 'ton', label: '吨' }], pl_package_spec: [{ value: 'bulk', label: '散装' }] } } else if (path === '/transport/plan/') { if (params.contractId === 'contract-1' && state.linkedListGate) await state.linkedListGate if (state.listDelay) await state.listDelay(params) if (state.failList) { state.failList = false error = '列表测试失败' } let rows = state.plans if (params.followStatus) rows = rows.filter((p) => params.followStatus.split(',').includes(p.followStatus)) if (params.planType) rows = rows.filter((p) => p.planType === params.planType) if (params.hazardCertReady !== undefined) rows = rows.filter((p) => p.hazardCertReady === Number(params.hazardCertReady)) if (params.q) rows = rows.filter( (p) => p.planNo.includes(params.q) || p.products.some((x) => x.productName.includes(params.q)) ) if (params.certUploadStatus === 'incomplete') rows = rows.filter((p) => p.products.some((x) => x.isToxic ? !(x.purchaseCertUrl && x.transportCertUrl) : x.isExplosive && !x.legalUseCertUrl ) ) const page = Number(params.page || 1), size = Number(params.pageSize || 20) data = { list: rows.slice((page - 1) * size, page * size), total: rows.length, page, pageSize: size } } else if (path === '/transport/plan/column-facet-values') { const values = { planNo: ['YS001'], licensePlate: ['鄂E12345'], productName: ['测试易制毒产品'], productSpec: ['99%'], productUnit: ['ton'], loadCustomer: ['测试供应商'], deliveryCustomer: ['测试需方1'], productContract: ['HT-001'] } data = { list: (values[body.field] || []).map((value) => ({ value, label: value, count: 1 })) } } else if (/^\/transport\/plan\/plan-\d+$/.test(path)) { data = state.plans.find((p) => p.id === path.split('/').pop()) } else if (/\/plan-\d+\/product-certs$/.test(path)) { const plan = state.plans.find((p) => p.id === path.split('/')[3]) if (state.failSave) { state.failSave = false error = '证书保存测试失败' } else for (const item of body.products) Object.assign( plan.products.find((p) => p.id === item.id), item ) data = plan } else if (/\/plan-\d+\/hazard-cert-ready$/.test(path)) { const plan = state.plans.find((p) => p.id === path.split('/')[3]) if (state.failReadyIds.includes(plan.id)) error = '齐备测试失败' else plan.hazardCertReady = body.ready ? 1 : 0 data = plan } else if (/\/plan-\d+\/arrange-info$/.test(path)) { data = { driverUrl: 'https://example.test/driver', licensePlate: '鄂E12345', siteRecords: [ { siteLinkId: 'site-1', siteUrl: 'https://example.test/site', planProductId: 'plan-7-toxic', productName: '测试易制毒产品', customerName: '测试需方1' } ], inventoryRecords: [ { dbInventoryId: 'inventory-7', dbPurchasePlanId: 'purchase-7', planProductId: 'plan-7-toxic', productName: '测试易制毒产品', businessType: '入库' } ] } } else if (path === '/transport/inbound/by-plan/plan-7') { data = { id: 'io-7', kind: 'inbound', orderNo: 'RK-007', status: 'in_progress', siteFeedbackRemark: '入库现场备注', siteFeedbackTime: '2026-10-17 16:20:00', siteFeedbackImages: ['/fixtures/inbound.pdf'], products: [] } } else if (path === '/transport/order/transport-7') { data = { id: 'transport-7', transportNo: 'TR007', licensePlate: '鄂E12345', driverName: '测试司机', driverPhone: '13800000000', escortName: '测试押运员', freightAmount: 1234, freightPrice: 50, freightType: 'by_weight', transportQty: 24, hasInvoice: 1, distanceKm: 100, unit: 'ton', transportStatus: 'arranged', approvalStatus: 'approved', createTime: '2026-10-17 11:00:00', feedbackTime: '2026-10-17 16:00:00', feedbackQty: 23, feedbackRemark: '司机回传测试备注', feedbackImages: '["/fixtures/driver.pdf"]', driverStopFeedbacks: [ { stopType: 'load', stopId: 'plan-7-load', label: '装货', customerName: '测试供应商', quantity: 23, remark: '司机装货回传', feedbackTime: '2026-10-17 16:00:00', attachmentUrls: ['/fixtures/driver.png'], products: [] } ], siteStopFeedbacks: [ { stopType: 'delivery', stopId: 'plan-7-delivery-1', label: '送货', customerName: '测试需方1', quantity: 12, remark: '现场送货回传', feedbackTime: '2026-10-18 10:00:00', attachmentUrls: ['/fixtures/site.pdf'], products: [] } ] } } else if (path === '/transport/freight/by-transport/transport-7') { data = null } else if (path === '/inventory/inventory-7/related') { data = { plan: null, weighing: { id: 'weighing-7', vehicleNo: '鄂E12345', netWeight: 11, grossWeight: 30, tareWeight: 19, grossWeightTime: '2026-10-17 15:30:00', tareWeightTime: '2026-10-17 16:30:00', supplier: '测试供应商', consignee: '测试需方1', remarks: '测试过磅备注' } } } else if (path === '/inventory/inventory-7') { data = { id: 'inventory-7', date: '2026-10-17', vehicleNo: '鄂E12345', partnerCompany: '测试供应商', product: '测试易制毒产品', quantity: 12, businessType: '入库' } } else if (/^\/web\/crm\/contract\/approval\/flow\/contract-[12]$/.test(path)) { data = [] } else if (/^\/web\/crm\/contract\/contract-[12]$/.test(path)) { data = { id: path.split('/').pop(), contractNo: path.endsWith('contract-1') ? 'HT-001' : 'HT-002', contractName: '危化品证书测试合同', opportunityId: 'opportunity-1', quoteId: 'quote-1', contractType: 'sales', customerName: '测试需方1', supplierName: '测试供应商', status: 'approved', startDate: '2026-01-01', endDate: '2026-12-31', remark: '合同说明', amount: 2400, partyA: '测试甲方', partyB: '测试乙方', partyAAccount: '测试账号001', contractFile: '[{"ossId":"/fixtures/contract.pdf","name":"合同原件.pdf"}]', attachmentFiles: '[{"ossId":"/fixtures/extra.pdf","name":"其他附件.pdf"}]', products: [ { id: 'contract-product-1', productId: 'toxic', productName: '测试别名', unit: 'ton', quantity: 24, price: 100, subtotal: 2400 } ], attachments: '/fixtures/contract.pdf' } } else if (path === '/web/crm/opportunity/detail') { data = { id: 'opportunity-1', opportunityName: '测试关联商机' } } else if (path === '/web/crm/quote/detail') { data = { id: 'quote-1', quoteNo: 'BJ-001' } } else if (path === '/web/crm/products/detail') { data = { id: body.id, productName: body.id === 'toxic' ? '测试易制毒产品' : '测试易制爆产品', aliases: [{ aliasName: '测试别名' }], isToxic: body.id === 'toxic' ? 1 : 0, isExplosive: body.id === 'explosive' ? 1 : 0 } } else if (path === '/web/admin/attachment/preview-url') { data = { previewUrl: params.fileUrl || body.fileUrl } } else if (path === '/web/admin/attachment/download-url') { data = { downloadUrl: params.fileUrl || body.fileUrl } } else if (path === '/web/admin/attachment/upload') { if (state.uploadGate) await state.uploadGate data = { id: 'file-1', fileName: '测试证书.pdf', fileUrl: '/fixtures/cert.pdf', fileType: 'application/pdf' } } else { state.unknown.push({ path, method, body, params }) error = `未定义测试接口:${method} ${path}` } if (error) return route.fulfill({ status: 400, contentType: 'application/json', body: JSON.stringify({ code: 400, msg: error, detail: error }) }) // 运输 API 是 FastAPI 直出;Web API 使用 code=200 的信封。 await route.fulfill({ contentType: 'application/json', body: JSON.stringify( path.startsWith('/transport/') || path.startsWith('/inventory/') ? data : { code: 200, msg: 'ok', data } ) }) }) return state }