mp-pl-plan-cert-fixture.mjs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  1. // 浏览器测试的 API 边界替身;绝不向真实业务接口发请求。
  2. const pdfObjects = [
  3. '<< /Type /Catalog /Pages 2 0 R >>',
  4. '<< /Type /Pages /Kids [3 0 R] /Count 1 >>',
  5. '<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Resources << >> /Contents 4 0 R >>',
  6. '<< /Length 0 >>\nstream\n\nendstream'
  7. ]
  8. let pdfFixture = '%PDF-1.4\n'
  9. const offsets = [0]
  10. pdfObjects.forEach((obj, i) => {
  11. offsets.push(Buffer.byteLength(pdfFixture))
  12. pdfFixture += `${i + 1} 0 obj\n${obj}\nendobj\n`
  13. })
  14. const xref = Buffer.byteLength(pdfFixture)
  15. pdfFixture += `xref\n0 5\n0000000000 65535 f \n${offsets
  16. .slice(1)
  17. .map((n) => `${String(n).padStart(10, '0')} 00000 n `)
  18. .join('\n')}\ntrailer\n<< /Size 5 /Root 1 0 R >>\nstartxref\n${xref}\n%%EOF`
  19. export function makePlan(index, overrides = {}) {
  20. const id = `plan-${index}`
  21. const toxic = {
  22. id: `${id}-toxic`,
  23. planId: id,
  24. productId: 'toxic',
  25. productNo: 'CP001',
  26. productName: '测试易制毒产品',
  27. brandName: '测试品牌',
  28. aliasName: '测试别名',
  29. model: '99%',
  30. unit: 'ton',
  31. quantity: 12,
  32. actualQty: 11,
  33. isToxic: 1,
  34. isExplosive: 0,
  35. purchaseCertUrl: '',
  36. transportCertUrl: '',
  37. legalUseCertUrl: ''
  38. }
  39. const explosive = {
  40. ...toxic,
  41. id: `${id}-explosive`,
  42. productId: 'explosive',
  43. productNo: 'CP002',
  44. productName: '测试易制爆产品',
  45. isToxic: 0,
  46. isExplosive: 1
  47. }
  48. const stopProducts = [toxic, explosive].map((p) => ({
  49. ...p,
  50. id: `stop-${p.id}`,
  51. planProductId: p.id,
  52. planQty: 12,
  53. inboundQty: 10,
  54. outboundQty: 9,
  55. contractId: 'contract-1',
  56. contractNo: 'HT-001',
  57. packageSpec: 'bulk',
  58. remark: '产品备注'
  59. }))
  60. return {
  61. id,
  62. planNo: `YS${String(index).padStart(3, '0')}`,
  63. planDate: '2026-10-17',
  64. createDate: '2026-10-17',
  65. createTime: '2026-10-17 09:15:00',
  66. belongsCompany: 'shanxia',
  67. planType: ['inbound', 'outbound', 'direct'][(index - 1) % 3],
  68. followStatus: 'pending_arrange',
  69. transportStatus: 'pending',
  70. archiveStatus: 'pending',
  71. completeStatus: 'pending',
  72. hasFreight: 1,
  73. freightAmount: 1234,
  74. freightRemark: '测试运费说明',
  75. totalAmount: 12345,
  76. hazardCertReady: 0,
  77. followUserName: '测试跟进人',
  78. licensePlate: '鄂E12345',
  79. trailerNo: '鄂E1234挂',
  80. driverName: '测试司机',
  81. escortName: '测试押运员',
  82. personnelSummary: '司机:测试司机;押运:测试押运员',
  83. deliveryTimes: ['2026-10-18 10:30:00', '2026-10-19 14:20:00'],
  84. remark: '证书迁移测试备注',
  85. products: [toxic, explosive],
  86. documentAttachments: [{ url: '/fixtures/plan.pdf', label: '计划附件.pdf' }],
  87. load: {
  88. id: `${id}-load`,
  89. customerName: '测试供应商',
  90. address: '湖北省宜昌市装货地址',
  91. loadTime: '2026-10-17 09:20:00',
  92. contact: '张师傅 13800000000',
  93. notes: '装货注意事项',
  94. contracts: [{ contractId: 'contract-1', contractNo: 'HT-001' }],
  95. products: stopProducts
  96. },
  97. deliveries: [1, 2].map((seq) => ({
  98. id: `${id}-delivery-${seq}`,
  99. seq,
  100. customerName: `测试需方${seq}`,
  101. address: `送货地址${seq}`,
  102. deliveryTime: `2026-10-${17 + seq} 10:30:00`,
  103. contact: '李师傅 13900000000',
  104. notes: `送货注意事项${seq}`,
  105. generateDibang: 0,
  106. contracts: [{ contractId: 'contract-1', contractNo: 'HT-001' }],
  107. products: stopProducts.map((p) => ({ ...p, id: `${p.id}-${seq}`, planQty: 6 }))
  108. })),
  109. ...overrides
  110. }
  111. }
  112. export async function installCertFixture(context) {
  113. const state = {
  114. plans: Array.from({ length: 24 }, (_, i) => makePlan(i + 1)),
  115. codes: [
  116. 'mini:pl:plan-cert:list',
  117. 'pl:plan-cert:list',
  118. 'pl:plan-cert:upload',
  119. 'pl:plan-cert:ready'
  120. ],
  121. calls: [],
  122. unknown: [],
  123. failList: false,
  124. failSave: false,
  125. failReadyIds: [],
  126. uploadGate: null,
  127. listDelay: null,
  128. linkedListGate: null,
  129. failPermissions: false
  130. }
  131. state.plans[3].archiveStatus = 'archived'
  132. state.plans[4].followStatus = 'cancelled'
  133. state.plans[5].hazardCertReady = 1
  134. state.plans[6].transportStatus = 'arranged'
  135. state.plans[6].followStatus = 'arranged'
  136. state.plans[6].transportId = 'transport-7'
  137. await context.addInitScript(() => {
  138. window.__copiedText = ''
  139. Object.defineProperty(navigator, 'clipboard', {
  140. configurable: true,
  141. value: {
  142. writeText: async (text) => {
  143. window.__copiedText = text
  144. }
  145. }
  146. })
  147. window.wx = {
  148. miniProgram: {
  149. navigateBack() {},
  150. reLaunch() {},
  151. getEnv(cb) {
  152. cb({ miniprogram: true })
  153. }
  154. }
  155. }
  156. })
  157. await context.route('**/fixtures/**', (route) =>
  158. route.fulfill(
  159. route.request().url().endsWith('.png')
  160. ? {
  161. contentType: 'image/png',
  162. body: Buffer.from(
  163. 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+cL1sAAAAASUVORK5CYII=',
  164. 'base64'
  165. )
  166. }
  167. : { contentType: 'application/pdf', body: pdfFixture }
  168. )
  169. )
  170. await context.route('https://api.qrserver.com/**', (route) =>
  171. route.fulfill({
  172. contentType: 'image/svg+xml',
  173. body: '<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200"><rect width="200" height="200" fill="white"/><text x="10" y="100">Mock QR preview</text></svg>'
  174. })
  175. )
  176. await context.route('**/api/**', async (route) => {
  177. const request = route.request()
  178. const url = new URL(request.url())
  179. if (!url.pathname.startsWith('/api/')) return route.continue()
  180. const path = url.pathname.slice(4)
  181. const method = request.method()
  182. let body = {}
  183. try {
  184. body = request.postDataJSON() || {}
  185. } catch {
  186. body = {}
  187. }
  188. const params = Object.fromEntries(url.searchParams)
  189. state.calls.push({ path, method, body, params })
  190. let data
  191. let error = ''
  192. if (path === '/web/auth/mp-webview-exchange') {
  193. data = {
  194. token: 'fixture-token',
  195. refreshToken: 'fixture-refresh',
  196. userInfo: {
  197. id: 'user-1',
  198. userId: 'user-1',
  199. userName: '测试用户',
  200. role: 'admin',
  201. roles: ['admin']
  202. }
  203. }
  204. } else if (path === '/web/auth/menu-codes') {
  205. data = { all: false, isSuperAdmin: false, menuCodes: state.codes }
  206. if (state.failPermissions) error = '权限加载失败'
  207. } else if (path === '/web/admin/dict-data/all') {
  208. data = {
  209. crm_product_unit: [{ value: 'ton', label: '吨' }],
  210. pl_package_spec: [{ value: 'bulk', label: '散装' }]
  211. }
  212. } else if (path === '/transport/plan/') {
  213. if (params.contractId === 'contract-1' && state.linkedListGate) await state.linkedListGate
  214. if (state.listDelay) await state.listDelay(params)
  215. if (state.failList) {
  216. state.failList = false
  217. error = '列表测试失败'
  218. }
  219. let rows = state.plans
  220. if (params.followStatus)
  221. rows = rows.filter((p) => params.followStatus.split(',').includes(p.followStatus))
  222. if (params.planType) rows = rows.filter((p) => p.planType === params.planType)
  223. if (params.hazardCertReady !== undefined)
  224. rows = rows.filter((p) => p.hazardCertReady === Number(params.hazardCertReady))
  225. if (params.q)
  226. rows = rows.filter(
  227. (p) =>
  228. p.planNo.includes(params.q) || p.products.some((x) => x.productName.includes(params.q))
  229. )
  230. if (params.certUploadStatus === 'incomplete')
  231. rows = rows.filter((p) =>
  232. p.products.some((x) =>
  233. x.isToxic
  234. ? !(x.purchaseCertUrl && x.transportCertUrl)
  235. : x.isExplosive && !x.legalUseCertUrl
  236. )
  237. )
  238. const page = Number(params.page || 1),
  239. size = Number(params.pageSize || 20)
  240. data = {
  241. list: rows.slice((page - 1) * size, page * size),
  242. total: rows.length,
  243. page,
  244. pageSize: size
  245. }
  246. } else if (path === '/transport/plan/column-facet-values') {
  247. const values = {
  248. planNo: ['YS001'],
  249. licensePlate: ['鄂E12345'],
  250. productName: ['测试易制毒产品'],
  251. productSpec: ['99%'],
  252. productUnit: ['ton'],
  253. loadCustomer: ['测试供应商'],
  254. deliveryCustomer: ['测试需方1'],
  255. productContract: ['HT-001']
  256. }
  257. data = {
  258. list: (values[body.field] || []).map((value) => ({ value, label: value, count: 1 }))
  259. }
  260. } else if (/^\/transport\/plan\/plan-\d+$/.test(path)) {
  261. data = state.plans.find((p) => p.id === path.split('/').pop())
  262. } else if (/\/plan-\d+\/product-certs$/.test(path)) {
  263. const plan = state.plans.find((p) => p.id === path.split('/')[3])
  264. if (state.failSave) {
  265. state.failSave = false
  266. error = '证书保存测试失败'
  267. } else
  268. for (const item of body.products)
  269. Object.assign(
  270. plan.products.find((p) => p.id === item.id),
  271. item
  272. )
  273. data = plan
  274. } else if (/\/plan-\d+\/hazard-cert-ready$/.test(path)) {
  275. const plan = state.plans.find((p) => p.id === path.split('/')[3])
  276. if (state.failReadyIds.includes(plan.id)) error = '齐备测试失败'
  277. else plan.hazardCertReady = body.ready ? 1 : 0
  278. data = plan
  279. } else if (/\/plan-\d+\/arrange-info$/.test(path)) {
  280. data = {
  281. driverUrl: 'https://example.test/driver',
  282. licensePlate: '鄂E12345',
  283. siteRecords: [
  284. {
  285. siteLinkId: 'site-1',
  286. siteUrl: 'https://example.test/site',
  287. planProductId: 'plan-7-toxic',
  288. productName: '测试易制毒产品',
  289. customerName: '测试需方1'
  290. }
  291. ],
  292. inventoryRecords: [
  293. {
  294. dbInventoryId: 'inventory-7',
  295. dbPurchasePlanId: 'purchase-7',
  296. planProductId: 'plan-7-toxic',
  297. productName: '测试易制毒产品',
  298. businessType: '入库'
  299. }
  300. ]
  301. }
  302. } else if (path === '/transport/inbound/by-plan/plan-7') {
  303. data = {
  304. id: 'io-7',
  305. kind: 'inbound',
  306. orderNo: 'RK-007',
  307. status: 'in_progress',
  308. siteFeedbackRemark: '入库现场备注',
  309. siteFeedbackTime: '2026-10-17 16:20:00',
  310. siteFeedbackImages: ['/fixtures/inbound.pdf'],
  311. products: []
  312. }
  313. } else if (path === '/transport/order/transport-7') {
  314. data = {
  315. id: 'transport-7',
  316. transportNo: 'TR007',
  317. licensePlate: '鄂E12345',
  318. driverName: '测试司机',
  319. driverPhone: '13800000000',
  320. escortName: '测试押运员',
  321. freightAmount: 1234,
  322. freightPrice: 50,
  323. freightType: 'by_weight',
  324. transportQty: 24,
  325. hasInvoice: 1,
  326. distanceKm: 100,
  327. unit: 'ton',
  328. transportStatus: 'arranged',
  329. approvalStatus: 'approved',
  330. createTime: '2026-10-17 11:00:00',
  331. feedbackTime: '2026-10-17 16:00:00',
  332. feedbackQty: 23,
  333. feedbackRemark: '司机回传测试备注',
  334. feedbackImages: '["/fixtures/driver.pdf"]',
  335. driverStopFeedbacks: [
  336. {
  337. stopType: 'load',
  338. stopId: 'plan-7-load',
  339. label: '装货',
  340. customerName: '测试供应商',
  341. quantity: 23,
  342. remark: '司机装货回传',
  343. feedbackTime: '2026-10-17 16:00:00',
  344. attachmentUrls: ['/fixtures/driver.png'],
  345. products: []
  346. }
  347. ],
  348. siteStopFeedbacks: [
  349. {
  350. stopType: 'delivery',
  351. stopId: 'plan-7-delivery-1',
  352. label: '送货',
  353. customerName: '测试需方1',
  354. quantity: 12,
  355. remark: '现场送货回传',
  356. feedbackTime: '2026-10-18 10:00:00',
  357. attachmentUrls: ['/fixtures/site.pdf'],
  358. products: []
  359. }
  360. ]
  361. }
  362. } else if (path === '/transport/freight/by-transport/transport-7') {
  363. data = null
  364. } else if (path === '/inventory/inventory-7/related') {
  365. data = {
  366. plan: null,
  367. weighing: {
  368. id: 'weighing-7',
  369. vehicleNo: '鄂E12345',
  370. netWeight: 11,
  371. grossWeight: 30,
  372. tareWeight: 19,
  373. grossWeightTime: '2026-10-17 15:30:00',
  374. tareWeightTime: '2026-10-17 16:30:00',
  375. supplier: '测试供应商',
  376. consignee: '测试需方1',
  377. remarks: '测试过磅备注'
  378. }
  379. }
  380. } else if (path === '/inventory/inventory-7') {
  381. data = {
  382. id: 'inventory-7',
  383. date: '2026-10-17',
  384. vehicleNo: '鄂E12345',
  385. partnerCompany: '测试供应商',
  386. product: '测试易制毒产品',
  387. quantity: 12,
  388. businessType: '入库'
  389. }
  390. } else if (/^\/web\/crm\/contract\/approval\/flow\/contract-[12]$/.test(path)) {
  391. data = []
  392. } else if (/^\/web\/crm\/contract\/contract-[12]$/.test(path)) {
  393. data = {
  394. id: path.split('/').pop(),
  395. contractNo: path.endsWith('contract-1') ? 'HT-001' : 'HT-002',
  396. contractName: '危化品证书测试合同',
  397. opportunityId: 'opportunity-1',
  398. quoteId: 'quote-1',
  399. contractType: 'sales',
  400. customerName: '测试需方1',
  401. supplierName: '测试供应商',
  402. status: 'approved',
  403. startDate: '2026-01-01',
  404. endDate: '2026-12-31',
  405. remark: '合同说明',
  406. amount: 2400,
  407. partyA: '测试甲方',
  408. partyB: '测试乙方',
  409. partyAAccount: '测试账号001',
  410. contractFile: '[{"ossId":"/fixtures/contract.pdf","name":"合同原件.pdf"}]',
  411. attachmentFiles: '[{"ossId":"/fixtures/extra.pdf","name":"其他附件.pdf"}]',
  412. products: [
  413. {
  414. id: 'contract-product-1',
  415. productId: 'toxic',
  416. productName: '测试别名',
  417. unit: 'ton',
  418. quantity: 24,
  419. price: 100,
  420. subtotal: 2400
  421. }
  422. ],
  423. attachments: '/fixtures/contract.pdf'
  424. }
  425. } else if (path === '/web/crm/opportunity/detail') {
  426. data = { id: 'opportunity-1', opportunityName: '测试关联商机' }
  427. } else if (path === '/web/crm/quote/detail') {
  428. data = { id: 'quote-1', quoteNo: 'BJ-001' }
  429. } else if (path === '/web/crm/products/detail') {
  430. data = {
  431. id: body.id,
  432. productName: body.id === 'toxic' ? '测试易制毒产品' : '测试易制爆产品',
  433. aliases: [{ aliasName: '测试别名' }],
  434. isToxic: body.id === 'toxic' ? 1 : 0,
  435. isExplosive: body.id === 'explosive' ? 1 : 0
  436. }
  437. } else if (path === '/web/admin/attachment/preview-url') {
  438. data = { previewUrl: params.fileUrl || body.fileUrl }
  439. } else if (path === '/web/admin/attachment/download-url') {
  440. data = { downloadUrl: params.fileUrl || body.fileUrl }
  441. } else if (path === '/web/admin/attachment/upload') {
  442. if (state.uploadGate) await state.uploadGate
  443. data = {
  444. id: 'file-1',
  445. fileName: '测试证书.pdf',
  446. fileUrl: '/fixtures/cert.pdf',
  447. fileType: 'application/pdf'
  448. }
  449. } else {
  450. state.unknown.push({ path, method, body, params })
  451. error = `未定义测试接口:${method} ${path}`
  452. }
  453. if (error)
  454. return route.fulfill({
  455. status: 400,
  456. contentType: 'application/json',
  457. body: JSON.stringify({ code: 400, msg: error, detail: error })
  458. })
  459. // 运输 API 是 FastAPI 直出;Web API 使用 code=200 的信封。
  460. await route.fulfill({
  461. contentType: 'application/json',
  462. body: JSON.stringify(
  463. path.startsWith('/transport/') || path.startsWith('/inventory/')
  464. ? data
  465. : { code: 200, msg: 'ok', data }
  466. )
  467. })
  468. })
  469. return state
  470. }