@dalehkx/quote-cli 0.3.4 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,10 +2,14 @@
2
2
  * API 远程接口适配器
3
3
  * 对接 terminal-api-v2 真实后端
4
4
  */
5
- import { getValidToken } from '../auth.mjs';
5
+ import { randomUUID } from 'crypto';
6
+ import { getValidToken, refreshToken } from '../auth.mjs';
6
7
  import { APP_USER_AGENT } from '../constants.mjs';
7
8
  import { Store } from '../store.mjs';
8
9
 
10
+ // 服务端认证失败的 errorCode 集合(token 失效、过期、被踢等)
11
+ const AUTH_ERROR_CODES = new Set([401, 652, 653, 654]);
12
+
9
13
  export class ApiAdapter {
10
14
  constructor(config = {}) {
11
15
  this.baseUrl = config.apiBase || '';
@@ -32,19 +36,37 @@ export class ApiAdapter {
32
36
  if (v !== undefined && v !== null && v !== '') url.searchParams.set(k, v);
33
37
  }
34
38
 
35
- const opts = { method, headers: await this._headers() };
36
- if (body && method !== 'GET') opts.body = JSON.stringify(body);
39
+ const makeOpts = async () => {
40
+ const opts = { method, headers: await this._headers() };
41
+ if (body && method !== 'GET') opts.body = JSON.stringify(body);
42
+ return opts;
43
+ };
37
44
 
38
- const res = await fetch(url.toString(), opts);
45
+ const parseResponse = (json) => {
46
+ if (json.errorCode !== undefined && json.errorCode !== 0) {
47
+ const err = new Error(json.message || `API 错误 [${json.errorCode}]`);
48
+ err.errorCode = json.errorCode;
49
+ throw err;
50
+ }
51
+ return json.data !== undefined ? json.data : json;
52
+ };
53
+
54
+ // 首次请求
55
+ const res = await fetch(url.toString(), await makeOpts());
39
56
  const json = await res.json();
40
57
 
41
- if (json.errorCode !== undefined && json.errorCode !== 0) {
42
- const err = new Error(json.message || `API 错误 [${json.errorCode}]`);
43
- err.errorCode = json.errorCode;
44
- throw err;
58
+ // 服务端返回认证错误时,尝试刷新 token 后重试一次
59
+ if (
60
+ json.errorCode !== undefined &&
61
+ (AUTH_ERROR_CODES.has(json.errorCode) || res.status === 401)
62
+ ) {
63
+ await refreshToken(this.baseUrl); // 失败则向上抛出(提示重新登录)
64
+ const retryRes = await fetch(url.toString(), await makeOpts());
65
+ const retryJson = await retryRes.json();
66
+ return parseResponse(retryJson);
45
67
  }
46
68
 
47
- return json.data !== undefined ? json.data : json;
69
+ return parseResponse(json);
48
70
  }
49
71
 
50
72
  // ─── Inquiry ─────────────────────────────────────────────
@@ -253,6 +275,7 @@ export class ApiAdapter {
253
275
 
254
276
  /**
255
277
  * 从 detailV2 原始数据中提取报价列表(避免重复请求)
278
+ * 每条报价会带上 storeId / storeName,供后续加购物车使用
256
279
  */
257
280
  async _fetchRepliesFromDetail(inquiryId, detail) {
258
281
  const stores = detail.inquiryQuoteStores || [];
@@ -265,7 +288,9 @@ export class ApiAdapter {
265
288
  inquiryId,
266
289
  storeId: store.storeId,
267
290
  });
268
- return (result.consultingQuotationProducts || []).map(this._mapQuotationProduct);
291
+ return (result.consultingQuotationProducts || []).map(
292
+ item => this._mapQuotationProduct(item, store.storeId, store.storeName)
293
+ );
269
294
  } catch {
270
295
  return [];
271
296
  }
@@ -282,7 +307,9 @@ export class ApiAdapter {
282
307
  const detail = await this._request('GET', `/inquiries/${inquiryId}/detailV2`, null, {
283
308
  platform: 'ANDROID',
284
309
  });
285
- return await this._fetchRepliesFromDetail(inquiryId, detail);
310
+ const all = await this._fetchRepliesFromDetail(inquiryId, detail);
311
+ // 过滤掉 price=0 的占坑槽位,只返回供应商已填实价的报价
312
+ return all.filter(r => r.priced);
286
313
  } catch {
287
314
  return [];
288
315
  }
@@ -305,7 +332,7 @@ export class ApiAdapter {
305
332
  platform: 'ANDROID',
306
333
  });
307
334
  const inquiry = this._mapInquiryDetail(inquiryId, detail);
308
- const replies = await this._fetchRepliesFromDetail(inquiryId, detail);
335
+ const replies = (await this._fetchRepliesFromDetail(inquiryId, detail)).filter(r => r.priced);
309
336
 
310
337
  const sorted = [...replies].sort((a, b) => {
311
338
  if (sort === 'price') return a.price - b.price;
@@ -323,48 +350,440 @@ export class ApiAdapter {
323
350
  // ─── Order ───────────────────────────────────────────────
324
351
 
325
352
  /**
326
- * 确认下单(采购确认)
353
+ * 获取用户收货地址列表
354
+ * 先尝试 GET /address/proxy_order_bff/post_addresses/{userLoginId}(完整列表),
355
+ * 失败则回退到 GET /address(仅默认地址)。
327
356
  */
328
- async confirmOrder(inquiryId, replyId) {
357
+ async listAddresses() {
358
+ const config = this._store.getConfig();
359
+ const userLoginId = config.userLoginId || '';
360
+
361
+ try {
362
+ const result = await this._request(
363
+ 'GET', `/address/proxy_order_bff/post_addresses/${userLoginId}`
364
+ );
365
+ const items = Array.isArray(result) ? result : (result ? [result] : []);
366
+ if (items.length > 0) {
367
+ return items.map(addr => ({
368
+ id: addr.id || '',
369
+ receiverName: addr.receiverName || '',
370
+ address: [
371
+ addr.provinceGeoName, addr.cityGeoName,
372
+ addr.countyGeoName, addr.villageGeoName,
373
+ addr.address,
374
+ ].filter(Boolean).join(' '),
375
+ contactNumber: addr.contactNumber || addr.contactTel || '',
376
+ }));
377
+ }
378
+ } catch { /* fall through */ }
379
+
380
+ // 回退:只拿默认地址
381
+ try {
382
+ const addr = await this._request('GET', '/address');
383
+ if (addr) {
384
+ return [{
385
+ id: addr.addressId || addr.id || '',
386
+ receiverName: addr.receiverName || '',
387
+ address: [
388
+ addr.provinceGeoName, addr.cityGeoName,
389
+ addr.countyGeoName, addr.address,
390
+ ].filter(Boolean).join(' '),
391
+ contactNumber: addr.contactNumber || addr.contactTel || '',
392
+ }];
393
+ }
394
+ } catch { /* fall through */ }
395
+
396
+ return [];
397
+ }
398
+
399
+ /**
400
+ * 为"结算单预览"阶段暴露物流选项——在 tosettle + INIT 之后调用。
401
+ * 返回结构化的物流列表,供 CLI 展示给用户选择。
402
+ *
403
+ * @param {string} inquiryId
404
+ * @param {string} replyId
405
+ * @param {string} addressId
406
+ * @returns {{ settleId, totalPrice, settleProducts, logisticsOptions, validGroups }}
407
+ */
408
+ async previewSettle(inquiryId, replyId, addressId) {
409
+ const config = this._store.getConfig();
410
+
411
+ // Step 1: purchase_confirm
329
412
  await this._request('POST', '/inquiries/purchase_confirm', {
330
413
  inquiryId,
331
414
  quotationProductIds: [replyId],
332
415
  });
333
416
 
334
- const inquiry = await this.getInquiry(inquiryId);
417
+ // Step 2: 找报价
418
+ const detail = await this._request('GET', `/inquiries/${inquiryId}/detailV2`, null, {
419
+ platform: 'ANDROID',
420
+ });
421
+ const inquiry = this._mapInquiryDetail(inquiryId, detail);
422
+ const stores = detail.inquiryQuoteStores || [];
423
+
424
+ let matched = null;
425
+ for (const store of stores) {
426
+ const res = await this._request('GET', '/inquiries/store/quotation', null, {
427
+ inquiryId, storeId: store.storeId,
428
+ }).catch(() => null);
429
+ if (!res) continue;
430
+ const product = (res.consultingQuotationProducts || [])
431
+ .find(p => p.quotationProductId === replyId);
432
+ if (product) {
433
+ matched = this._mapQuotationProduct(product, store.storeId, store.storeName);
434
+ break;
435
+ }
436
+ }
437
+ if (!matched) throw new Error(`未找到报价 ${replyId}`);
438
+
439
+ // Step 3: tosettle
440
+ const settleRes = await this._request('POST', '/buy/proxy_order_bff/tosettle', {
441
+ application: 'ANDROID', businessGroup: 'INQUIRY', businessUnit: 'COMMON_INQUIRY',
442
+ originSource: 'INQUIRY_CONFIRM',
443
+ buyerUserLoginId: config.userLoginId || '',
444
+ buyerCompanyId: String(config.garageCompanyId || ''),
445
+ terminal: 'APP', postalAddressId: addressId,
446
+ toSettleItems: [{
447
+ productId: matched.id,
448
+ facilityId: matched.facilityId,
449
+ sellerStoreId: matched.storeId,
450
+ inquiryId,
451
+ quantity: inquiry.quantity || 1,
452
+ needInvoice: 'B',
453
+ itemInvoice: 'N',
454
+ }],
455
+ });
456
+ const settleId = settleRes.settleId;
457
+ if (!settleId) throw new Error('生成结算单失败:响应中未包含 settleId');
458
+
459
+ // Step 4: INIT
460
+ const settleDetail = await this._request('POST', '/buy/settle', {
461
+ type: 'INIT',
462
+ settlePayload: { settleId, application: 'ANDROID', terminal: 'APP' },
463
+ });
464
+
465
+ const totalPrice = settleDetail.totalAmount?.totalAmount
466
+ ?? settleDetail.totalAmount?.productTotalAmount ?? 0;
467
+
468
+ const validGroups = settleDetail.validGroups || [];
469
+ const settleProducts = [];
470
+ // 将每个 store 的所有物流选项扁平化,加上 storeId 标识
471
+ const logisticsOptions = [];
472
+
473
+ for (const group of validGroups) {
474
+ for (const inqItem of (group.inquiryItems || [])) {
475
+ for (const product of (inqItem.productItems || [])) {
476
+ settleProducts.push({
477
+ settleItemId: product.settleItemId,
478
+ productId: product.productId,
479
+ quantity: product.quantity,
480
+ storeId: group.storeId,
481
+ facilityId: product.facilityId || matched.facilityId,
482
+ });
483
+ }
484
+ }
485
+
486
+ for (const svc of (group.xiaomaLogisticsService || [])) {
487
+ // 默认推荐
488
+ const dl = svc.defaultLogisticsDTO;
489
+ if (dl) {
490
+ logisticsOptions.push({
491
+ storeId: svc.storeId,
492
+ facilityId: svc.facilityId,
493
+ code: dl.logisticsCompanyCode,
494
+ name: dl.logisticsCompanyName,
495
+ transport: dl.transportationName || '汽运',
496
+ location: dl.logisticsLocationName || '',
497
+ deliver: dl.deliverType === 'arrive_home' ? '送货上门' : `${dl.logisticsLocationName || ''}自提`,
498
+ shift: dl.displayShiftName ? `${dl.displayShiftName} ${dl.departureTime || ''}` : '',
499
+ _raw: dl,
500
+ isDefault: true,
501
+ });
502
+ }
503
+ // 常用物流(去重 code)
504
+ const seenCodes = new Set(dl ? [dl.logisticsCompanyCode] : []);
505
+ for (const c of (svc.commonlyUsedLogistics || [])) {
506
+ if (seenCodes.has(c.displayLogisticsCompaniesCode)) continue;
507
+ seenCodes.add(c.displayLogisticsCompaniesCode);
508
+ const way = (c.transportWayDTOS || [])[0];
509
+ const loc = (way?.logisticsLocationDTOS || [])[0];
510
+ logisticsOptions.push({
511
+ storeId: svc.storeId,
512
+ facilityId: svc.facilityId,
513
+ code: c.displayLogisticsCompaniesCode,
514
+ name: c.displayLogisticsCompaniesName,
515
+ transport: way?.transportationName || '汽运',
516
+ location: loc?.logisticsLocationName || '',
517
+ deliver: loc?.deliverType === 'arrive_home' ? '送货上门'
518
+ : `${loc?.logisticsLocationName || ''}自提`,
519
+ shift: '',
520
+ _raw: { storeId: svc.storeId, facilityId: svc.facilityId, ...c, way, loc },
521
+ isDefault: false,
522
+ });
523
+ }
524
+ }
525
+ }
526
+
527
+ if (settleProducts.length === 0) {
528
+ settleProducts.push({
529
+ settleItemId: matched.id, productId: matched.id,
530
+ quantity: inquiry.quantity || 1,
531
+ storeId: matched.storeId, facilityId: matched.facilityId,
532
+ });
533
+ }
534
+
535
+ return { settleId, totalPrice, settleProducts, logisticsOptions, matched, inquiry, validGroups };
536
+ }
537
+
538
+ /**
539
+ * 确认下单 — 完整下单流程(采购确认 → 生成结算单 → 提交结算)
540
+ *
541
+ * 流程:
542
+ * 0. 解析收货地址(优先 opts.addressId,否则取用户第一条地址)
543
+ * 1. POST /inquiries/purchase_confirm — 采购确认
544
+ * 2. 从 detailV2 + store/quotation 找目标报价的 storeId / facilityId
545
+ * 3. POST /buy/proxy_order_bff/tosettle — 生成结算单(需传 postalAddressId)
546
+ * 4. POST /buy/settle { type: INIT } — 拉结算详情(含 totalAmount / settleItemId)
547
+ * 5. POST /buy/proxy_order_bff/settle_submit — 提交下单,返回 orderIds
548
+ *
549
+ * @param {string} inquiryId
550
+ * @param {string} replyId quotationProductId
551
+ * @param {object} opts
552
+ * @param {string} opts.addressId 收货地址 ID
553
+ * @param {string} [opts.logisticsCode] 物流公司 code(不传则取推荐;'default' 同不传)
554
+ * @param {object} [opts._preview] 已有 previewSettle 结果时直接复用,跳过重复请求
555
+ */
556
+ async confirmOrder(inquiryId, replyId, opts = {}) {
557
+ const config = this._store.getConfig();
558
+
559
+ // ── Step 0: 解析收货地址 ─────────────────────────────────
560
+ const addressId = opts.addressId || '';
561
+ if (!addressId) throw new Error('addressId 不能为空');
562
+
563
+ // ── Steps 1-4: 复用 previewSettle,或重新计算 ────────────
564
+ const preview = opts._preview || await this.previewSettle(inquiryId, replyId, addressId);
565
+ const { settleId, totalPrice, settleProducts, logisticsOptions, matched } = preview;
566
+
567
+ // ── 选择物流 ────────────────────────────────────────────
568
+ let xiaomaLogistics = [];
569
+ const targetCode = opts.logisticsCode || '';
570
+
571
+ for (const opt of logisticsOptions) {
572
+ const pick = targetCode
573
+ ? opt.code === targetCode
574
+ : opt.isDefault;
575
+ if (!pick) continue;
576
+
577
+ const raw = opt._raw;
578
+ // raw 有两种形状:来自 defaultLogisticsDTO 的直接字段,或来自 commonlyUsedLogistics 的嵌套结构
579
+ if (raw.logisticsCompanyCode) {
580
+ // defaultLogisticsDTO 形状
581
+ xiaomaLogistics.push({
582
+ storeId: raw.storeId,
583
+ facilityId: raw.facilityId,
584
+ logisticsCompanyCode: raw.logisticsCompanyCode,
585
+ logisticsCompanyName: raw.logisticsCompanyName,
586
+ transportationCode: raw.transportationCode,
587
+ transportationName: raw.transportationName,
588
+ logisticsLocationCode: raw.logisticsLocationCode,
589
+ logisticsLocationName: raw.logisticsLocationName,
590
+ landingLogisticsLocationCode: raw.landingLogisticsLocationCode,
591
+ landingLogisticsLocationName: raw.landingLogisticsLocationName,
592
+ deliverType: raw.deliverType,
593
+ departureTime: raw.departureTime || '',
594
+ lineShiftCode: raw.lineShiftCode || '',
595
+ lineShiftName: raw.lineShiftName || '',
596
+ });
597
+ } else {
598
+ // commonlyUsedLogistics 形状(_raw = { storeId, facilityId, ...c, way, loc })
599
+ const way = raw.way;
600
+ const loc = raw.loc;
601
+ xiaomaLogistics.push({
602
+ storeId: opt.storeId,
603
+ facilityId: opt.facilityId,
604
+ logisticsCompanyCode: raw.displayLogisticsCompaniesCode,
605
+ logisticsCompanyName: raw.displayLogisticsCompaniesName,
606
+ transportationCode: way?.transportationCode || 'CAR_FREIGHT',
607
+ transportationName: way?.transportationName || '汽运',
608
+ logisticsLocationCode: loc?.logisticsLocationCode || 'arrive_home',
609
+ logisticsLocationName: loc?.logisticsLocationName || '送货上门',
610
+ landingLogisticsLocationCode: loc?.landingLogisticCompanyCode || raw.displayLogisticsCompaniesCode,
611
+ landingLogisticsLocationName: loc?.landingLogisticCompanyName || raw.displayLogisticsCompaniesName,
612
+ deliverType: loc?.deliverType || 'arrive_home',
613
+ departureTime: '',
614
+ lineShiftCode: '',
615
+ lineShiftName: '',
616
+ });
617
+ }
618
+ break;
619
+ }
620
+
621
+ // 兜底:没有任何物流选项时用 _pickLogisticsEntry 提取
622
+ if (xiaomaLogistics.length === 0) {
623
+ for (const group of (preview.validGroups || [])) {
624
+ for (const svc of (group.xiaomaLogisticsService || [])) {
625
+ const entry = _pickLogisticsEntry(svc);
626
+ if (entry) { xiaomaLogistics.push(entry); break; }
627
+ }
628
+ if (xiaomaLogistics.length) break;
629
+ }
630
+ }
631
+
632
+ // ── Step 5: 提交结算下单 ────────────────────────────────
633
+ const submitBody = {
634
+ settleId,
635
+ clientRequestId: randomUUID(),
636
+ application: 'ANDROID',
637
+ businessGroup: 'INQUIRY',
638
+ businessUnit: 'COMMON_INQUIRY',
639
+ buyerUserLoginId: config.userLoginId || '',
640
+ buyerCompanyId: String(config.garageCompanyId || ''),
641
+ postalAddressId: addressId,
642
+ terminal: 'APP',
643
+ goldCoinUsed: false,
644
+ totalAmount: totalPrice,
645
+ invoices: [{
646
+ storeId: matched.storeId,
647
+ inquiryId,
648
+ needInvoice: 'B',
649
+ }],
650
+ logistics: { xiaomaLogistics },
651
+ products: settleProducts,
652
+ };
653
+
654
+ let submitRes;
655
+ try {
656
+ submitRes = await this._request('POST', '/buy/proxy_order_bff/settle_submit', submitBody);
657
+ } catch (err) {
658
+ // beta 环境已知问题:订单成功入库但后续通知步骤异常,服务端返回 errorCode=999。
659
+ // 此时兜底查最新订单:若 30 秒内出现本 inquiryId 对应的新订单,视为下单成功。
660
+ process.stderr.write(`[DBG] settle_submit catch: errorCode=${err.errorCode} type=${typeof err.errorCode}\n`);
661
+ if (err.errorCode === 999) {
662
+ process.stderr.write(`[DBG] entering fallback poll\n`);
663
+ const fallback = await this._findRecentOrder(inquiryId, 30_000);
664
+ process.stderr.write(`[DBG] fallback result: ${fallback ? fallback.orderId : 'null'}\n`);
665
+ if (fallback) return this._buildConfirmResult(fallback, settleId, inquiryId, replyId, matched, preview);
666
+ }
667
+ throw err;
668
+ }
669
+
670
+ if (!submitRes.isSuccess) {
671
+ const msg = (typeof submitRes.message === 'object'
672
+ ? submitRes.message?.content
673
+ : submitRes.message) || submitRes.code || '未知错误';
674
+ throw new Error(`下单提交失败:${msg}`);
675
+ }
676
+
677
+ const orderId = Array.isArray(submitRes.orderIds) && submitRes.orderIds.length > 0
678
+ ? submitRes.orderIds[0]
679
+ : settleId;
680
+
335
681
  return {
336
682
  order: {
337
- id: `ORD-${Date.now()}`,
683
+ id: orderId,
684
+ settleId,
338
685
  inquiryId,
339
686
  replyId,
340
- status: 'confirmed',
687
+ status: 'pending_payment',
341
688
  confirmedAt: new Date().toISOString(),
689
+ totalPrice,
690
+ currency: 'CNY',
691
+ },
692
+ inquiry: preview.inquiry,
693
+ reply: {
694
+ id: matched.id,
695
+ supplier: matched.supplier,
696
+ price: matched.price,
697
+ currency: 'CNY',
698
+ partNum: matched.partNum || '',
699
+ brand: matched.brand || '',
700
+ location: matched.location || '',
342
701
  },
343
- inquiry,
344
- reply: { id: replyId, supplier: '平台供应商', price: 0 },
345
702
  };
346
703
  }
347
704
 
348
- async listOrders() {
705
+ async listOrders({ createdBy } = {}) {
349
706
  try {
350
- const result = await this._request('POST', '/inquiries/order/list', {}, {
351
- page: 1,
352
- size: 20,
353
- });
354
- const items = result.content || result || [];
355
- return Array.isArray(items) ? items.map(this._mapOrderItem) : [];
707
+ const body = { pageNumber: 1, pageSize: 20 };
708
+ if (createdBy) body.createdBy = createdBy;
709
+ const result = await this._request('POST', '/orders', body);
710
+ const items = result.orders || result.content || [];
711
+ return Array.isArray(items) ? items.map(i => this._mapOrderItem(i)) : [];
356
712
  } catch {
357
713
  return [];
358
714
  }
359
715
  }
360
716
 
717
+ /**
718
+ * settle_submit errorCode=999 兜底:轮询订单列表,找到 submitTime 之后出现的
719
+ * 最新订单(orderDate >= submitTime)。返回 raw order item 或 null。
720
+ * 原始订单列表不含 inquiryId 顶层字段,改用时间戳识别。
721
+ */
722
+ async _findRecentOrder(_inquiryId, windowMs = 30_000) {
723
+ const submitTime = Date.now();
724
+ const deadline = submitTime + windowMs;
725
+ let attempt = 0;
726
+ while (Date.now() < deadline) {
727
+ await new Promise(r => setTimeout(r, 2000));
728
+ attempt++;
729
+ try {
730
+ const result = await this._request('POST', '/orders', { pageNumber: 1, pageSize: 5 });
731
+ const items = result.orders || result.content || [];
732
+ process.stderr.write(`[DBG] poll#${attempt} got ${items.length} orders, submitTime=${submitTime}\n`);
733
+ for (const i of items.slice(0, 3)) {
734
+ process.stderr.write(` orderId=${i.orderId} orderDate=${i.orderDate} diff=${i.orderDate - submitTime}\n`);
735
+ }
736
+ // 找 orderDate 在本次提交之后(>= submitTime - 10s 容差)的最新条目
737
+ const found = items.find(i => (i.orderDate || 0) >= submitTime - 10_000);
738
+ if (found) return found;
739
+ } catch (e) {
740
+ process.stderr.write(`[DBG] poll#${attempt} error: ${e.message}\n`);
741
+ }
742
+ }
743
+ return null;
744
+ }
745
+
746
+ /** 从原始订单条目 + settle 上下文组装 confirmOrder 的返回结构 */
747
+ _buildConfirmResult(rawOrder, settleId, inquiryId, replyId, matched, preview) {
748
+ const mapped = this._mapOrderItem(rawOrder);
749
+ return {
750
+ order: {
751
+ id: mapped.id || settleId,
752
+ settleId,
753
+ inquiryId,
754
+ replyId,
755
+ status: 'pending_payment',
756
+ confirmedAt: mapped.confirmedAt || new Date().toISOString(),
757
+ totalPrice: rawOrder.actualCurrencyAmount ?? matched.price,
758
+ currency: 'CNY',
759
+ },
760
+ inquiry: preview.inquiry,
761
+ reply: {
762
+ id: matched.id,
763
+ supplier: rawOrder.productStoreName || matched.supplier,
764
+ price: rawOrder.actualCurrencyAmount ?? matched.price,
765
+ currency: 'CNY',
766
+ partNum: matched.partNum || '',
767
+ brand: matched.brand || '',
768
+ location: matched.location || '',
769
+ },
770
+ };
771
+ }
772
+
361
773
  // ─── 数据映射 ────────────────────────────────────────────
362
774
 
363
775
  _mapInquiryItem(item) {
776
+ const brand = item.carBrandName || '';
777
+ const model = item.carModelName || item.saleModelName || '';
778
+ // 合并成 "大众 朗逸" 格式;若只有其中一项则只显示一项
779
+ const vehicle = brand && model ? `${brand} ${model}` : (brand || model);
364
780
  return {
365
781
  id: item.inquiryId || item.id || '',
366
782
  product: item.userNeed || '询价单',
367
- vehicle: item.carModelName || item.saleModelName || '',
783
+ vehicle,
784
+ carBrand: brand,
785
+ carModel: model,
786
+ vin: item.vin || '',
368
787
  quantity: 1,
369
788
  status: mapStatus(item.statusId || ''),
370
789
  createdAt: item.createdStamp ? new Date(item.createdStamp).toISOString() : '',
@@ -396,31 +815,98 @@ export class ApiAdapter {
396
815
  };
397
816
  }
398
817
 
399
- _mapQuotationProduct(item) {
818
+ _mapQuotationProduct(item, storeId = '', storeName = '') {
400
819
  return {
401
- id: item.quotationProductId || '',
402
- supplier: item.displayName || item.brandName || '',
403
- price: parseFloat(item.displayPrice) || 0,
404
- currency: 'CNY',
405
- brand: item.brandName || '',
406
- partNum: item.partsNum || '',
407
- delivery: item.arrivalTime || null,
408
- note: item.remark || '',
409
- quality: item.qualityDescription || '',
410
- location: item.locationName || '',
820
+ id: item.quotationProductId || '',
821
+ storeId: storeId,
822
+ storeName: storeName,
823
+ supplier: item.displayName || item.brandName || '',
824
+ price: parseFloat(item.displayPrice) || 0,
825
+ priced: parseFloat(item.displayPrice) > 0, // false = 供应商占坑未报实价
826
+ currency: 'CNY',
827
+ brand: item.brandName || '',
828
+ partNum: item.partsNum || '',
829
+ delivery: item.arrivalTime || null,
830
+ note: item.remark || '',
831
+ quality: item.qualityDescription || '',
832
+ facilityId: item.location || '', // 仓库 ID(用于下单)
833
+ location: item.locationName || '', // 仓库名称(用于展示)
411
834
  };
412
835
  }
413
836
 
414
837
  _mapOrderItem(item) {
838
+ const brand = item.carBrandName || '';
839
+ const model = item.carModelInfo || item.carModelName || item.saleModelName || '';
840
+ const vehicle = brand && model ? `${brand} ${model}` : (brand || model);
415
841
  return {
416
- id: item.orderId || item.id || '',
842
+ id: item.orderId || '',
417
843
  inquiryId: item.inquiryId || '',
418
- status: item.statusId || 'confirmed',
419
- confirmedAt: item.createdStamp ? new Date(item.createdStamp).toISOString() : '',
420
- inquiry: { product: item.productName || '?' },
421
- reply: { supplier: item.storeName || '?', price: item.totalPrice || 0, currency: 'CNY' },
844
+ status: item.statusId || '',
845
+ statusDesc: item.statusIdDesc || '',
846
+ confirmedAt: item.orderDate ? new Date(item.orderDate).toISOString() : '',
847
+ inquiry: { product: item.orderName || item.userNeed || '?' },
848
+ reply: {
849
+ supplier: item.productStoreName || '',
850
+ price: item.actualCurrencyAmount || 0,
851
+ currency: 'CNY',
852
+ },
853
+ vehicle,
854
+ carBrand: brand,
855
+ };
856
+ }
857
+ }
858
+
859
+ // ─── 物流辅助 ──────────────────────────────────────────────
860
+
861
+ /**
862
+ * 从结算单返回的 xiaomaLogisticsService 条目中提取最优物流配置。
863
+ * 优先使用 defaultLogisticsDTO(平台推荐),否则从 commonlyUsedLogistics[0] 构造。
864
+ * departureTime / lineShiftCode / lineShiftName 无值时传空字符串(接口要求 string 类型)。
865
+ */
866
+ function _pickLogisticsEntry(svc) {
867
+ if (!svc) return null;
868
+
869
+ const dl = svc.defaultLogisticsDTO;
870
+ if (dl) {
871
+ return {
872
+ storeId: dl.storeId,
873
+ facilityId: dl.facilityId,
874
+ logisticsCompanyCode: dl.logisticsCompanyCode,
875
+ logisticsCompanyName: dl.logisticsCompanyName,
876
+ transportationCode: dl.transportationCode,
877
+ transportationName: dl.transportationName,
878
+ logisticsLocationCode: dl.logisticsLocationCode,
879
+ logisticsLocationName: dl.logisticsLocationName,
880
+ landingLogisticsLocationCode: dl.landingLogisticsLocationCode,
881
+ landingLogisticsLocationName: dl.landingLogisticsLocationName,
882
+ deliverType: dl.deliverType,
883
+ departureTime: dl.departureTime || '',
884
+ lineShiftCode: dl.lineShiftCode || '',
885
+ lineShiftName: dl.lineShiftName || '',
422
886
  };
423
887
  }
888
+
889
+ // 没有推荐物流,从常用物流第一条取最简配置
890
+ const common = (svc.commonlyUsedLogistics || [])[0];
891
+ if (!common) return null;
892
+ const way = (common.transportWayDTOS || [])[0];
893
+ const loc = (way?.logisticsLocationDTOS || [])[0];
894
+ return {
895
+ storeId: svc.storeId,
896
+ facilityId: svc.facilityId,
897
+ logisticsCompanyCode: common.displayLogisticsCompaniesCode,
898
+ logisticsCompanyName: common.displayLogisticsCompaniesName,
899
+ transportationCode: way?.transportationCode || 'CAR_FREIGHT',
900
+ transportationName: way?.transportationName || '汽运',
901
+ logisticsLocationCode: loc?.logisticsLocationCode || 'arrive_home',
902
+ logisticsLocationName: loc?.logisticsLocationName || '送货上门',
903
+ landingLogisticsLocationCode: loc?.landingLogisticCompanyCode || common.displayLogisticsCompaniesCode,
904
+ landingLogisticsLocationName: loc?.landingLogisticCompanyName || common.displayLogisticsCompaniesName,
905
+ deliverType: loc?.deliverType || 'arrive_home',
906
+ departureTime: '',
907
+ lineShiftCode: '',
908
+ lineShiftName: '',
909
+ };
424
910
  }
425
911
 
426
912
  // ─── 状态映射 ──────────────────────────────────────────────