@dalehkx/quote-cli 0.3.3 → 0.3.5
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.
- package/bin/quote.js +2 -9
- package/package.json +5 -2
- package/scripts/prepack.cjs +40 -0
- package/skill/SKILL.md +222 -0
- package/skill/agents/openai.yaml +7 -0
- package/skill/references/api-mapping.md +596 -0
- package/skill/references/data-schema.md +111 -0
- package/skill/references/inquiry-api-guide.md +749 -0
- package/skill/references/inquiry-flow.md +191 -0
- package/skill/references/workflow.md +78 -0
- package/src/adapter/api.mjs +529 -43
- package/src/auth.mjs +32 -6
- package/src/commands/inquiry.mjs +37 -8
- package/src/commands/install.mjs +162 -0
- package/src/commands/login.mjs +4 -14
- package/src/commands/order.mjs +224 -16
- package/src/constants.mjs +3 -2
package/src/adapter/api.mjs
CHANGED
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
* API 远程接口适配器
|
|
3
3
|
* 对接 terminal-api-v2 真实后端
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
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
|
|
36
|
-
|
|
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
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
683
|
+
id: orderId,
|
|
684
|
+
settleId,
|
|
338
685
|
inquiryId,
|
|
339
686
|
replyId,
|
|
340
|
-
status:
|
|
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
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
|
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:
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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 ||
|
|
842
|
+
id: item.orderId || '',
|
|
417
843
|
inquiryId: item.inquiryId || '',
|
|
418
|
-
status: item.statusId || '
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
// ─── 状态映射 ──────────────────────────────────────────────
|