feat: show batch track upsert responses and improve price check UX
Display track upsert request/response in batch job and order modals, relax price check dimensions with disclaimer UI, and update order guide and design docs.
This commit is contained in:
parent
76d8b473b6
commit
6441ca6543
@ -101,9 +101,9 @@ OMS 业务表中的 **`CustomerId`** 指向 **`t_customer.Id`**(客户业务
|
||||
| 能力 | 设计目标 | 当前实现(oms0414-back) |
|
||||
| --- | --- | --- |
|
||||
| 快照读取 | Redis + 进程内存为主,DB 降级 | **直接读** `t_sales_product_publish_record`;无发布记录时 **Live 组装**(`BuildLiveRulesSnapshotAsync` / `BuildLivePriceSnapshotAsync`),版本号写 **`LIVE-{salesProductId}`** |
|
||||
| 预报应付费用 | §4.3.4.2 排货确认渠道 + 渠道买价 | **已实现**:`/order/parcel/create` 预报成功后 **`ConfirmServiceChannelAndPayableAsync`**(`ChargingStage=forecast`,重量取 **`downstream`** 行) |
|
||||
| 入库触发应收/应付 | §6.3.1.1 收货计重后重算 | **已实现**:`/operation/inbound/create` → `ApplyReceiveBillingByWaybillAsync`(应收 `ChargingStage=inbound` + **重新排货确认渠道与应付**) |
|
||||
| 出库更新状态与发货件重尺 | §6.3.2.1 覆盖 `Type=ship` 并重算;**`OrderStatus=pendingOnline`** | **未实现**:`/operation/outbound/create` 当前仅写 `t_outbound_record`,不更新 `t_order` / `t_order_weight` |
|
||||
| 预报应付费用 | §4.3.4.2 排货确认渠道 + 渠道买价 | **创建时不排货**;**入库**时 `TryConfirmServiceChannelAndPayableAsync`(失败 → **`pendingAllocation`** + `AllocationPendingReason`,不阻断收货/inbound 应收) |
|
||||
| 入库触发应收/应付 | §6.3.1.1 收货计重后重算 | **已实现**:`/operation/inbound/create` → `ApplyReceiveBillingByWaybillAsync`(inbound 应收 + **可选排货**;成功 → `pendingPayment`,失败 → `pendingAllocation`) |
|
||||
| 出库更新状态与发货件重尺 | §6.3.2.1 覆盖 `Type=ship` 并重算;**`OrderStatus=pendingOnline`** | **已实现**:`/operation/outbound/create` → `ApplyShipBillingByWaybillAsync` + 写 `t_outbound_record` |
|
||||
| 批量 JSON 下单 | 异步任务 + `taskId` | **同步逐条**调用 `CreateParcelCoreAsync`,返回 **`batchNo` + `items[]`** |
|
||||
| Excel 导入 | 解析后等同批量创建 / 异步 | **统一走** `t_order_batch_job`(`action=import-row`);MQ 开启异步入队,关闭则同步逐条处理;返回 **`jobId`** |
|
||||
| 批量重算费用 | `POST /order/fee/batch-recalculate` | **未实现**(§4.4.1.10 为设计稿) |
|
||||
@ -145,7 +145,7 @@ OMS 业务表中的 **`CustomerId`** 指向 **`t_customer.Id`**(客户业务
|
||||
|
||||
1. **查价**(可选)→ **创建 / 预报** → 校验 → 草稿与包裹 → **预报计重完成后**将全部启用卖价项写入 **`t_order_fee`**(`ChargingStage=forecast`)。
|
||||
2. **契约固化**:预报等关键节点写入 **`SalesProductRulesVersion`、`SalesProductPriceVersion`**(及渠道侧应付相关版本,若有);非显式「改单重算」流程 **不得** 因后台新发布而改写已存版本号。
|
||||
3. **状态机**(概念顺序;枚举见 **`## 10 附录` → `### 10.3`**):如 草稿 → 已预报 → 已收货 → 待支付 → 待发货 → **待上网** → 运输中 → 派送成功/失败;另有拦截、**问题件**(`problematic`)、**理赔件**(`claimed`)、删除等分支。问题件/理赔件支持 **处理子状态**(`HandleStatus`:待处理/已处理),详见 **§4.4.1.13~§4.4.1.15**。
|
||||
3. **状态机**(概念顺序;枚举见 **`## 10 附录` → `### 10.3`**):如 草稿 → 已预报 → **待排货(可选)** → 待支付 → 待发货 → **待上网** → 运输中 → 派送成功/失败;另有拦截、**问题件**(`problematic`)、**理赔件**(`claimed`)、删除等分支。入库排货失败时进入 **`pendingAllocation`**,写入 **`AllocationPendingReason`**,仍保留收货与 inbound 应收。
|
||||
4. **支付**:OMS 侧订单状态与支付动作对接;**扣减 `t_customer.Balance`** 并 **写入 `t_customer_account_flow`(`BizType=orderPay`,金额为负)**;支付金额取订单 **`t_order_fee` 应收合计**。
|
||||
5. **分阶段计费(按件重尺类型)**:卖价/买价 **不再配置「收费时机」**;应收在 **`t_order_weight.Type=forecast` 或 `receive` 计重完成** 后触发,全部启用价格项参与;`t_order_fee.ChargingStage` 标记本次算价所用重量阶段(`forecast` / `inbound`)。出库阶段(`ship` 行)待 §6.3.2.1 接入。
|
||||
6. **批量下单**:Excel/模板 或 JSON 批量接口 → 异步任务;**单次最多受理 2000 条**。
|
||||
@ -386,6 +386,7 @@ JWT → mapId(user.userid INT)
|
||||
| IsIntercept | TINYINT | - | - | 否 | 是否拦截:0-否,1-是 |
|
||||
| IsRemote | TINYINT | - | - | 否 | 是否偏远:0-否,1-是 |
|
||||
| PredeclareFailReason | VARCHAR | 512 | - | 否 | 预报失败原因:草稿已落库后环节失败时记录;黑名单命中、快照未命中等不落库场景不写此字段 |
|
||||
| AllocationPendingReason | VARCHAR | 512 | - | 否 | 待排货原因:入库排货或渠道买价未匹配时记录(`OrderStatus=pendingAllocation`) |
|
||||
| SalesProductRulesVersion | VARCHAR | 32 | - | 否 | 下单绑定的规则版本号(与发布记录 `Version` 一致,`OperatorType=rules`) |
|
||||
| SalesProductPriceVersion | VARCHAR | 32 | - | 否 | 下单绑定的价格版本号(与发布记录 `Version` 一致,`OperatorType=price`) |
|
||||
| IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 |
|
||||
@ -394,7 +395,7 @@ JWT → mapId(user.userid INT)
|
||||
| UpdateUserId | INT | - | - | 否 | 更新人 |
|
||||
| UpdateTime | DATETIME | - | - | 否 | 更新时间 |
|
||||
|
||||
**OrderStatus 枚举**:draft, predeclared, received, pendingPayment, pendingShip, pendingOnline, shipping, deliverySuccess, deliveryFailed, problematic, deleted, claimed
|
||||
**OrderStatus 枚举**:draft, predeclared, pendingAllocation, received, pendingPayment, pendingShip, pendingOnline, shipping, deliverySuccess, deliveryFailed, problematic, deleted, claimed
|
||||
|
||||
**OrderSource 枚举**:见 **§10.3.9**(`manual` / `pickup` / `upload` / `batch` / `platform`)
|
||||
|
||||
@ -768,8 +769,8 @@ JWT → mapId(user.userid INT)
|
||||
|
||||
| 件重尺 `Type` | 典型触发节点 | 应收(`Type=receivable`) | 应付(`Type=payable`,设计) |
|
||||
| --- | --- | --- | --- |
|
||||
| `forecast` | 下单预报成功 | **§4.3.4.1**:全部启用卖价项;`ChargingStage=forecast` | **§4.3.4.2**:排货确认渠道 + 渠道买价;`ChargingStage=forecast`;重量取 **`downstream`** |
|
||||
| `receive` | 仓库收货 `/operation/inbound/create` | **§4.3.4.1**:全部启用卖价项;`ChargingStage=inbound` | **§4.3.4.2**:**重新**排货确认渠道 + 渠道买价;`ChargingStage=inbound`;重量取 **`receive`** |
|
||||
| `forecast` | 下单预报成功 | **§4.3.4.1**:全部启用卖价项;`ChargingStage=forecast`;**不排货、不写应付** |
|
||||
| `receive` | 仓库收货 `/operation/inbound/create` | **§4.3.4.1**:全部启用卖价项;`ChargingStage=inbound` | **§4.3.4.2**:**TryConfirm** 排货 + 渠道买价(失败 → `pendingAllocation`,不抛错);`ChargingStage=inbound`;重量取 **`receive`** |
|
||||
| `ship` | 仓库出库 §6.3.2.1 | **§4.3.4.1**(**未实现**) | **§4.3.4.2**(**未实现**) |
|
||||
|
||||
**规则小结**:先锁版本;按件重尺阶段触发算价;每次对 **全部启用** 价格项算价;后阶段仍读订单绑定快照。
|
||||
@ -826,10 +827,10 @@ JWT → mapId(user.userid INT)
|
||||
|
||||
| 场景 | 方法 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 小包创建 / 查价 | `CalculateReceivableFees` / `CalculateQuoteReceivableFees` | 全部启用卖价项;创建写 `ChargingStage=forecast` |
|
||||
| 入库收货 | `ApplyReceiveBillingByWaybillAsync` | 仅覆盖 **`Type=receive`** 并重算应收;**重新排货**确认渠道与应付(`ChargingStage=inbound`);**`OrderStatus=pendingPayment`**,写入 **`ReceiveTime`** |
|
||||
| 出库发货 | `ApplyShipBillingByWaybillAsync`(待新增) | 仅覆盖 **`Type=ship`** 并重算;写 `ChargingStage=outbound`(应收待接入);**`OrderStatus=pendingOnline`**,写入 **`ShipTime`** |
|
||||
| 应付 | `CalculatePayableFees` + `ConfirmServiceChannelAndPayableAsync` | 预报 / 入库 / 手动重算应付 |
|
||||
| 小包创建 / 查价 | `CalculateReceivableFees` / `CalculateQuoteReceivableFees` | 全部启用卖价项;创建写 `ChargingStage=forecast`(**仅应收**) |
|
||||
| 入库收货 | `ApplyReceiveBillingByWaybillAsync` | 仅覆盖 **`Type=receive`** 并重算应收;**TryConfirm** 排货/应付(`ChargingStage=inbound`);成功 → **`pendingPayment`**,失败 → **`pendingAllocation`** + `AllocationPendingReason`;写入 **`ReceiveTime`** |
|
||||
| 出库发货 | `ApplyShipBillingByWaybillAsync` | 仅覆盖 **`Type=ship`** 并重算;写 `ChargingStage=outbound`;**`OrderStatus=pendingOnline`**,写入 **`ShipTime`** |
|
||||
| 应付 | `TryConfirmServiceChannelAndPayableAsync` / 手动重算 | **入库**或运营重算;成功清 `AllocationPendingReason` 并 → `pendingPayment` |
|
||||
|
||||
**查价额外逻辑**(`OrderService.ParcelQuote.cs`):遍历 **全部已启用** 销售产品(`shippingMethod` 模糊匹配 `ProductCode` / `ProductName`);失败写入 **`failures[]`**(含 `productId`、`step`、`reason`);成功项带 **`rulesVersion`、`priceVersion`、`isRemote`**。
|
||||
|
||||
@ -1746,17 +1747,15 @@ JWT → mapId(user.userid INT)
|
||||
|
||||
###### 核心流程
|
||||
|
||||
1. **校验**:`destinationCountry` 必填(ISO 二字码);`cargoWeight`、`length`、`width`、`height` 须 **> 0**。
|
||||
1. **校验**:`destinationCountry` 必填(ISO 二字码);`cargoWeight` 须 **> 0**;**尺寸可选** — 全空则按实重估算,或长/宽/高三者齐全(部分填写 → 报错)。
|
||||
2. **绑定时刻**:`bindTime = DateTime.Now`;解析卖价最新生效快照(`ResolveSnapshotBindingAsync`)。
|
||||
3. **产品范围**:`shippingMethod` 按产品代码/名称过滤;未找到 → `failures[0].step=product`。
|
||||
4. **逐产品试算**(按快照明细匹配):
|
||||
- **分区**:用 **目的国 + 邮编**(及省州/城市若有)匹配产品 `PriceAreaId` 对应 `t_kpi_area_info`;未命中 → `step=zone`。
|
||||
- **计重**:按规则快照计重得到 `chargeableWeight`;失败 → `step=weight`。
|
||||
- **卖价明细**:在快照 `prices[].details` 中匹配 **分区 + 重量区间 + 货物属性**:
|
||||
- 重量因子存 **左开右闭区间** `(start, end]`,如 `0,0.5` 表示 `0 < 重量 ≤ 0.5`;单值 `0.5` 兼容为 `(0, 0.5]`。
|
||||
- `calculationFormula` 使用字典编码(如 `unit`、`total`、`continueUnit`),与渠道配置 `calculation_formula` 一致。
|
||||
3. **产品范围**:未传 `shippingMethod` 时遍历 **全部已启用** 销售产品;有值则按代码/名称过滤;未找到 → `failures[0].step=product`。
|
||||
4. **逐产品试算**(按快照明细匹配;**无邮编时跳过黑名单**;分区无匹配时可 **zone 兜底** 取最低价分区,标记 `quoteLevel=estimated`):
|
||||
- **分区**:用 **目的国 + 邮编**(及省州/城市若有)匹配;未命中且无兜底 → `step=zone`。
|
||||
- **计重**:无尺寸时按实重;有尺寸则体积重参与;失败 → `step=weight`。
|
||||
- **货物/仓库**:未筛选时展开 **general/electronics/sensitive × 全部仓库** 组合试算。
|
||||
- **算价**:`CalculateQuoteReceivableFees`;无明细命中 → `step=price`。
|
||||
5. **结果**:成功写入 `items`;失败写入 `failures`(含 `step` + `reason`),**禁止静默丢弃**。
|
||||
5. **结果**:成功写入 `items`(含 **`quoteLevel`、`disclaimers`**);顶层 **`queryHint`** 汇总缺失字段与免责声明;失败写入 `failures`(含 `step` + `reason`),**禁止静默丢弃**。
|
||||
|
||||
###### failures.step 说明
|
||||
|
||||
@ -1775,10 +1774,19 @@ JWT → mapId(user.userid INT)
|
||||
| --- | --- |
|
||||
| 目的国为空 | 目的国不能为空 |
|
||||
| 重量 ≤ 0 | 货物重量必须大于0 |
|
||||
| 长宽高无效 | 长宽高必须大于0 |
|
||||
| 尺寸部分填写 | 尺寸请填写完整的长、宽、高,或全部留空 |
|
||||
| 目的国不存在 | 目的国不存在 |
|
||||
| 全部产品未命中 | `200` + `items: []` + `failures` 逐条原因 |
|
||||
|
||||
**响应扩展(对客提示)**
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `queryHint.quoteLevel` | `exact` / `estimated`(有缺失字段时为估算) |
|
||||
| `queryHint.disclaimers` | 免责声明列表(如未填尺寸、未填邮编) |
|
||||
| `items[].quoteLevel` | 单行报价精度 |
|
||||
| `items[].disclaimers` | 单行补充说明 |
|
||||
|
||||
###### 3 当前实现(oms0414-back)
|
||||
|
||||
- `OrderService.PriceCheckAsync` → `BuildPriceCheckResultAsync`(`OrderService.ParcelQuote.cs`)。
|
||||
@ -1814,7 +1822,7 @@ JWT → mapId(user.userid INT)
|
||||
"requestId": "string", // 幂等键 t_order.RequestId//可选;重复则直接返回已有订单结果
|
||||
"clientNo": "string", // 客户单号 t_order.ClientNo//可选
|
||||
"trackNo": "string", // 跟踪号 t_order.TrackNo//可选
|
||||
"serviceNo": "string", // 运单号 t_order.ServiceNo//可选;未传时服务端自动生成(LP 前缀)
|
||||
"serviceNo": "string", // 运单号 t_order.ServiceNo//可选;未传时服务端自动生成(IDE 前缀)
|
||||
"productCode": "string", // 产品代码 t_order.ProductCode//必填
|
||||
"channelCode": "string", // 渠道代码 t_order.ChannelCode
|
||||
"cargoAttribute": "string", // 货物属性 t_order.CargoAttribute:general-普货,electronics-3c&带电,sensitive-敏感货//必填
|
||||
@ -1971,7 +1979,7 @@ JWT → mapId(user.userid INT)
|
||||
- 命中处理:**不创建订单**,直接返回失败(错误信息需明确「收件地址命中黑名单区域」),不进入草稿落库及后续流程。
|
||||
|
||||
5. 创建订单草稿
|
||||
- **`ServiceNo`**:请求体 `serviceNo` 为空时,服务端自动生成唯一运单号(`LP` + 时间戳 + 随机数);`trackNo` 为空则落库 `NULL`。
|
||||
- **`ServiceNo`**:请求体 `serviceNo` 为空时,服务端自动生成唯一运单号(**`IDE` + `yyMMddHHmmss` + 4 位随机数**,年份取后两位如 2026→26);`trackNo` 为空则落库 `NULL`。
|
||||
- 插入 `t_order`:
|
||||
- 写入字段:`ServiceNo`、`ClientNo`、`TrackNo`、`ProductCode`、`ChannelCode`、`ConsigneeCountry`、`CargoAttribute`、`ExportCustomsMode`、`OrderStatus`、`WarehouseId`、`CustomerId`、`IsCod`、`CodCurrency`、`CodSum`、`IsIntercept`、`IsRemote`、`PredeclareFailReason`、`SalesProductRulesVersion`、`SalesProductPriceVersion`、审计字段。
|
||||
- 处理规则:
|
||||
@ -2033,12 +2041,10 @@ JWT → mapId(user.userid INT)
|
||||
- 读取 `rulesSnapshot.content.areas.price.rules`,按收件国家/省市/邮编区间匹配得到 `zoneName`(如“美国西部”)。
|
||||
- 若未匹配到价格分区,更新 `t_order.PredeclareFailReason = '价格分区未匹配(规则快照)'`,订单状态保持 `draft`,返回失败。
|
||||
|
||||
10. 费用计算与费用落库(**拆分为应收 / 应付**,与 §4.3.4.1、§4.3.4.2 一致;本接口处于**预报**节点)
|
||||
- **1)重算应收(§4.3.4.1)**:全部启用卖价项;**重量**取 **`Type=forecast`**;**upsert** `ChargingStage=forecast`。入库由 `/operation/inbound/create` 再写 `ChargingStage=inbound`。
|
||||
- **2)重算应付(§4.3.4.2)**:排货确认服务渠道 → 渠道买价算价 → upsert `ChargingStage=forecast`;回写 **`ChannelCode`** 及下游对接四字段(§4.3.4.2 步骤 1)。
|
||||
- 写入字段(各行):`OrderId`、`Type`、`CostItem`、`Amount`、`Currency`、`PriceNo`、`PriceEffectiveTime`、`ChargingStage`;`PriceNo` 应收写 `SalesProductPriceVersion`,应付写渠道买价版本(若有)。
|
||||
10. 费用计算与费用落库(**预报节点仅应收**)
|
||||
- **重算应收(§4.3.4.1)**:全部启用卖价项;**重量**取 **`Type=forecast`**;**upsert** `ChargingStage=forecast`。
|
||||
- **不在创建阶段排货/写应付**;渠道与应付在 **入库** `ApplyReceiveBillingByWaybillAsync` 中 `TryConfirmServiceChannelAndPayableAsync` 处理(见 **§6.3.1.1**)。
|
||||
- **强校验**:存在启用卖价项但未匹配到任何明细时,预报失败。
|
||||
- 若应收应算项未匹配到卖价明细,更新 `t_order.PredeclareFailReason = '产品卖价快照未配置或未匹配到有效价格'`,订单状态保持 `draft`,返回失败。
|
||||
|
||||
11. 固定报价示例规则(某产品,来自 price 快照的 continuePricing)
|
||||
- 示例规则:
|
||||
@ -2097,10 +2103,10 @@ JWT → mapId(user.userid INT)
|
||||
| 会话客户 | `ResolveEffectiveCustomerAsync` → 写 `input.CustomerId = sessionCustomer.Id` |
|
||||
| 必填校验 | 另含 **`declaredCurrency`、`quantity`(>0)**、申报明细 **`enName/unitPrice/unitWeight/quantity`**;**`clientNo` 非必填** |
|
||||
| `channelCode` | 未传时默认 **`product.ProductCode`** |
|
||||
| `serviceNo` | 自定义时查重,冲突 → **「运单号已存在」**;否则 **`LP{yyyyMMddHHmmss}{4位随机}`** |
|
||||
| `serviceNo` | 自定义时查重,冲突 → **「运单号已存在」**;否则 **`IDE{yyMMddHHmmss}{4位随机}`** |
|
||||
| 快照 | 见 **§4.3.5**(DB 发布 + Live 降级,**无 Redis**) |
|
||||
| 应付 | 排货确认渠道 + **`Type=payable`**(`ChargingStage=forecast`) |
|
||||
| 成功 | `OrderStatus=predeclared`;**`labelUrl`** 同 print 逻辑生成 HTML |
|
||||
| 应付/排货 | **创建时不执行**;入库时 **TryConfirm**(失败 → `pendingAllocation`) |
|
||||
| 成功 | `OrderStatus=predeclared`;清空 `AllocationPendingReason`;**`labelUrl`** 同 print 逻辑生成 HTML |
|
||||
| 失败 | 草稿已建则 **`MarkParcelDraftFailedAsync`**(保持 `draft` + `PredeclareFailReason`)后 **仍抛业务异常** |
|
||||
|
||||
##### 4.4.3.2 批量创建小包(POST /order/parcel/batch-create)
|
||||
@ -6664,7 +6670,7 @@ ValidateParcelCreateInputAsync
|
||||
|
||||
```json
|
||||
{
|
||||
"waybillNo": "LP1234567890",
|
||||
"waybillNo": "IDE2607221859123456",
|
||||
"weight": 2.5,
|
||||
"length": 20,
|
||||
"width": 15,
|
||||
@ -6685,7 +6691,7 @@ ValidateParcelCreateInputAsync
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"id": 1,
|
||||
"waybillNo": "LP1234567890",
|
||||
"waybillNo": "IDE2607221859123456",
|
||||
"weight": 2.5,
|
||||
"length": 20,
|
||||
"width": 15,
|
||||
@ -6706,10 +6712,12 @@ ValidateParcelCreateInputAsync
|
||||
|
||||
- 解析 `waybillNo` 等参数,定位订单(及绑定销售产品、已落库的 `t_order_weight` 四行)。
|
||||
- 写入 **`t_inbound_record`**(实重、尺寸、销售产品、目的国、仓库、操作人、操作时间等)。
|
||||
- **仅覆盖 `t_order_weight` 中本单 `Type=receive` 行**:`Length`、`Width`、`Height`、`ActualWeight` 以本次入库实测为准(与请求体 `weight`、`length`、`width`、`height` 对齐),`Cubic = Length × Width × Height / 1_000_000`。
|
||||
- **对该 `receive` 行**按本单绑定 **`WeightRuleId` / `WeightRoundingId`**(及 rules 快照或子表明细)执行 **§4.4.3.1 步骤 7、8 相同算法**,重算 **`VolumetricWeight`、`ChargeableWeight`**(**不修改 `forecast` / `ship` / `downstream` 行**)。
|
||||
- 更新 **`t_order.OrderStatus = pendingPayment`**,写入 **`ReceiveTime`**。
|
||||
- 入库完成后:**§4.3.4.1** 重算应收(**`receive` 行**计费重,`ChargingStage=inbound`)。
|
||||
- **仅覆盖 `t_order_weight` 中本单 `Type=receive` 行**:实测件重尺重算计费重(**不修改 forecast/ship/downstream**)。
|
||||
- **§4.3.4.1** 重算应收(`receive` 计费重,`ChargingStage=inbound`)。
|
||||
- **TryConfirmServiceChannelAndPayableAsync** 尝试排货与应付:
|
||||
- 成功 → **`OrderStatus=pendingPayment`**,清空 **`AllocationPendingReason`**,写入 **`ReceiveTime`**。
|
||||
- 失败 → **`OrderStatus=pendingAllocation`**,写入 **`AllocationPendingReason`**(仍完成收货与 inbound 应收)。
|
||||
- **允许入库的源状态**:`predeclared`、`pendingAllocation`;已入库状态拒绝重复收货。
|
||||
|
||||
###### 校验与异常
|
||||
|
||||
@ -6717,7 +6725,7 @@ ValidateParcelCreateInputAsync
|
||||
|
||||
###### 3 当前实现(oms0414-back)
|
||||
|
||||
- `InboundRecordService.CreateAsync` → `ApplyReceiveBillingByWaybillAsync`:已联动订单状态与 **`receive` 行**计重、应收/应付 **`ChargingStage=inbound`**,并 **重新排货确认 `ChannelCode`**。
|
||||
- `InboundRecordService.CreateAsync` → `ApplyReceiveBillingByWaybillAsync`:联动 **`receive` 行**计重、inbound 应收、**可选排货**;状态 **`pendingPayment` / `pendingAllocation`**;`ParcelBillingValidators.EnsureInboundOrderStatus`。
|
||||
|
||||
##### 6.3.1.2 最近入库记录(GET /operation/inbound/recent-list)
|
||||
|
||||
@ -6739,7 +6747,7 @@ ValidateParcelCreateInputAsync
|
||||
"items": [
|
||||
{
|
||||
"id": 1,
|
||||
"waybillNo": "LP1234567890",
|
||||
"waybillNo": "IDE2607221859123456",
|
||||
"weight": 2.5,
|
||||
"length": 20,
|
||||
"width": 15,
|
||||
@ -6784,7 +6792,7 @@ ValidateParcelCreateInputAsync
|
||||
|
||||
```json
|
||||
{
|
||||
"waybillNo": "LP1234567890",
|
||||
"waybillNo": "IDE2607221859123456",
|
||||
"weight": 2.5,
|
||||
"length": 20,
|
||||
"width": 15,
|
||||
@ -6806,7 +6814,7 @@ ValidateParcelCreateInputAsync
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"id": 1,
|
||||
"waybillNo": "LP1234567890",
|
||||
"waybillNo": "IDE2607221859123456",
|
||||
"weight": 2.5,
|
||||
"length": 20,
|
||||
"width": 15,
|
||||
@ -6847,9 +6855,7 @@ ValidateParcelCreateInputAsync
|
||||
|
||||
###### 3 当前实现(oms0414-back)
|
||||
|
||||
- `OutboundRecordService.CreateAsync`:**仅**插入 **`t_outbound_record`**(`WaybillNo`、`WarehouseId`、`OperateTime`、`OperatorId`);**校验**订单须为 **`pendingShip`**。
|
||||
- **未**覆盖 **`t_order_weight.Type=ship`** 件重尺、**未**更新 **`t_order.OrderStatus` / `ShipTime`**、**未**触发 §4.3.4 算价(与上文设计目标 **§6.3.2.1 核心流程** 尚未对齐)。
|
||||
- **待实现**:抽取 **`OrderService.ApplyShipBillingByWaybillAsync`**(对称于 **`ApplyReceiveBillingByWaybillAsync`**),由 `CreateAsync` 在写 **`t_outbound_record`** 前/同一事务内调用。
|
||||
- `OutboundRecordService.CreateAsync` → **`ApplyShipBillingByWaybillAsync`** + 插入 **`t_outbound_record`**;已覆盖 **`ship` 行**、更新 **`OrderStatus=pendingOnline`** / **`ShipTime`**、写入 outbound 应收。
|
||||
|
||||
##### 6.3.2.2 最近出库记录(GET /operation/outbound/recent-list)
|
||||
|
||||
@ -6871,7 +6877,7 @@ ValidateParcelCreateInputAsync
|
||||
"items": [
|
||||
{
|
||||
"id": 1,
|
||||
"waybillNo": "LP1234567890",
|
||||
"waybillNo": "IDE2607221859123456",
|
||||
"weight": 2.5,
|
||||
"length": 20,
|
||||
"width": 15,
|
||||
@ -6921,7 +6927,7 @@ ValidateParcelCreateInputAsync
|
||||
|
||||
```json
|
||||
{
|
||||
"number": "LP1234567890",
|
||||
"number": "IDE2607221859123456",
|
||||
"numberType": "waybill",
|
||||
"labelTemplateId": 1
|
||||
}
|
||||
@ -6937,7 +6943,7 @@ ValidateParcelCreateInputAsync
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"id": 1,
|
||||
"waybillNo": "LP1234567890",
|
||||
"waybillNo": "IDE2607221859123456",
|
||||
"customerOrderNo": "",
|
||||
"trackingNo": "",
|
||||
"otherNumber": "",
|
||||
@ -6981,7 +6987,7 @@ ValidateParcelCreateInputAsync
|
||||
"items": [
|
||||
{
|
||||
"id": 1,
|
||||
"waybillNo": "LP1234567890",
|
||||
"waybillNo": "IDE2607221859123456",
|
||||
"customerOrderNo": "CUST001",
|
||||
"trackingNo": "TRK001",
|
||||
"otherNumber": "",
|
||||
@ -8418,6 +8424,7 @@ GET /customer/account-flow/list?page=1&pageSize=50&bizType=orderPay&startTime=20
|
||||
|
||||
| 迭代日期 | 迭代编号 | 迭代功能 | 变更模块 | 备注 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 2026-07-22 | 20260722-01 | 查价/下单/入库/出库链路加固;待排货;运单号 IDE | 订单 §2.3、§4.3.5、§4.4.2.1、§4.4.3.1、§6.3;附录 §10.3.1;新手指引 `/order/guide` | 查价可选尺寸+参考价;创建仅 forecast 应收;入库可选排货→`pendingAllocation`;运单号 `IDE{yyMMddHHmmss}{4位随机}` |
|
||||
| 2026-07-19 | 20260719-01 | 店铺订单创建物流 / 物流&代发 | 订单 §4.1、§2.3、§4.2.11、**§4.4.7**;附录权限 | 校对弹窗;`batch-create-logistics` / `batch-create-fulfill`;与匹配库存分流 |
|
||||
| 2026-07-15 | 20260715-01 | 客户发件地址 + 揽收订单 | 订单 §4.1、§4.2.1、§4.2.10、§4.4.1.1、§4.4.4、§4.4.5;附录 §10.3.9 | 地址簿 `t_customer_sender_address`;`OrderSource=pickup` 复用小包创建;菜单客户/订单 |
|
||||
| 2026-07-04 | 20260704-01 | 创建问题件 / 创建理赔件 | 订单 §4.2.1、§4.2.9、§4.4.1.13~§4.4.1.16;渠道 §5.3.0.7;附录 §10.3.6~§10.3.8 | 问题模板消费;`t_order_exception_event`;权限 `Order.Problematic.Create` 等 |
|
||||
@ -8806,6 +8813,7 @@ GET /customer/account-flow/list?page=1&pageSize=50&bizType=orderPay&startTime=20
|
||||
| --- | --- |
|
||||
| draft | 草稿 |
|
||||
| predeclared | 已预报 |
|
||||
| pendingAllocation | 待排货 |
|
||||
| received | 已收货 |
|
||||
| pendingPayment | 待支付 |
|
||||
| pendingShip | 待发货 |
|
||||
|
||||
@ -506,6 +506,120 @@ export function parseBatchJobPrintUrl(item: { resultJson?: string }): string {
|
||||
}
|
||||
}
|
||||
|
||||
export type ParsedBatchTrackUpsertPayload = {
|
||||
success?: boolean
|
||||
message?: string
|
||||
trackUpsert?: {
|
||||
skipped?: boolean
|
||||
skipReason?: string
|
||||
requestUrl?: string
|
||||
requestBody?: unknown
|
||||
responseStatusCode?: number
|
||||
responseBody?: unknown
|
||||
responseBodyRaw?: string
|
||||
}
|
||||
}
|
||||
|
||||
export function parseBatchJobTrackUpsertPayload(item: { resultJson?: string }): ParsedBatchTrackUpsertPayload | null {
|
||||
if (!item.resultJson) return null
|
||||
try {
|
||||
return JSON.parse(item.resultJson) as ParsedBatchTrackUpsertPayload
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function stringifyJsonBlock(value: unknown): string {
|
||||
if (value == null) return ''
|
||||
if (typeof value === 'string') return value
|
||||
try {
|
||||
return JSON.stringify(value, null, 2)
|
||||
} catch {
|
||||
return String(value)
|
||||
}
|
||||
}
|
||||
|
||||
/** 单条批量轨迹任务的 resultJson 展示文本 */
|
||||
export function formatBatchJobTrackUpsertItemText(label: string, item: ParsedOrderBatchJobItem): string {
|
||||
const parsed = parseBatchJobTrackUpsertPayload(item)
|
||||
const lines = [`【${label}】`]
|
||||
if (item.status === 'failed') {
|
||||
lines.push(`失败:${item.failReason || parsed?.message || '未知原因'}`)
|
||||
} else if (item.status === 'success') {
|
||||
lines.push(parsed?.message || item.resultMessage || '成功')
|
||||
} else {
|
||||
lines.push(item.resultMessage || item.status || '处理中')
|
||||
}
|
||||
|
||||
const track = parsed?.trackUpsert
|
||||
if (!track) {
|
||||
if (parsed) {
|
||||
lines.push(stringifyJsonBlock(parsed))
|
||||
} else if (item.resultJson) {
|
||||
lines.push(item.resultJson)
|
||||
}
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
if (track.skipped) {
|
||||
lines.push(`跳过:${track.skipReason || '未知原因'}`)
|
||||
}
|
||||
if (track.requestUrl) lines.push(`请求:${track.requestUrl}`)
|
||||
const requestBody = stringifyJsonBlock(track.requestBody)
|
||||
if (requestBody) lines.push(`请求体:\n${requestBody}`)
|
||||
if (track.responseStatusCode != null) lines.push(`HTTP:${track.responseStatusCode}`)
|
||||
const responseBody = stringifyJsonBlock(track.responseBody) || track.responseBodyRaw
|
||||
if (responseBody) lines.push(`响应:\n${responseBody}`)
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
/** 批量创建轨迹订单完成后的汇总展示(含 resultJson 报文) */
|
||||
export function formatBatchJobTrackUpsertResultText(
|
||||
title: string,
|
||||
job: { jobId: number; status: string; total: number; successCount: number; failCount: number },
|
||||
items: ParsedOrderBatchJobItem[],
|
||||
rows: any[],
|
||||
resolveLabel?: (row: any, item: ParsedOrderBatchJobItem) => string,
|
||||
): string {
|
||||
const resolve = resolveLabel ?? ((row, item) => resolveBatchJobItemRowLabel(row, item))
|
||||
const lines = [
|
||||
`【${title}】任务 #${job.jobId}`,
|
||||
`状态:${job.status},合计 ${job.total},成功 ${job.successCount},失败 ${job.failCount}`,
|
||||
'',
|
||||
]
|
||||
for (const item of items) {
|
||||
const row = rows.find((r) => {
|
||||
const rowId = Number(r?.id ?? r?.orderId ?? r?.Id ?? r?.OrderId ?? 0)
|
||||
return rowId === item.orderId
|
||||
})
|
||||
lines.push(formatBatchJobTrackUpsertItemText(resolve(row, item), item))
|
||||
lines.push('')
|
||||
}
|
||||
return lines.join('\n').trimEnd()
|
||||
}
|
||||
|
||||
/** 批量任务详情抽屉:轨迹响应摘要(单行) */
|
||||
export function getBatchJobTrackUpsertResultSummary(item: ParsedOrderBatchJobItem): string {
|
||||
const parsed = parseBatchJobTrackUpsertPayload(item)
|
||||
const track = parsed?.trackUpsert
|
||||
if (!track) {
|
||||
return item.resultMessage || parsed?.message || ''
|
||||
}
|
||||
if (track.skipped) {
|
||||
return `跳过:${track.skipReason || '未知原因'}`
|
||||
}
|
||||
const code = track.responseStatusCode != null ? `HTTP ${track.responseStatusCode}` : ''
|
||||
const body = stringifyJsonBlock(track.responseBody) || track.responseBodyRaw || ''
|
||||
const preview = body.replace(/\s+/g, ' ').slice(0, 120)
|
||||
return [code, preview].filter(Boolean).join(' · ') || item.resultMessage || '成功'
|
||||
}
|
||||
|
||||
/** 批量任务详情抽屉:轨迹响应详情(多行) */
|
||||
export function formatBatchJobTrackUpsertDetailText(item: ParsedOrderBatchJobItem): string {
|
||||
const label = getBatchJobItemDisplayNo(item)
|
||||
return formatBatchJobTrackUpsertItemText(label, item)
|
||||
}
|
||||
|
||||
const ORDER_BATCH_ACTION_ROUTES: Record<string, string> = {
|
||||
pay: '/order/batch/pay',
|
||||
'pre-declare': '/order/batch/pre-declare',
|
||||
|
||||
@ -742,6 +742,7 @@ export default {
|
||||
'order.batchJobs.items.rowNumber': 'Excel Row',
|
||||
'order.batchJobs.items.finishedAt': 'Finished',
|
||||
'order.batchJobs.items.retryCount': 'Retried {count} time(s)',
|
||||
'order.batchJobs.items.viewResponse': 'View response',
|
||||
'order.batchJobs.filter.allItems': 'All Items',
|
||||
'order.batchJobs.filter.itemStatus': 'Item Status',
|
||||
'order.batchJobs.message.running': 'Running: {progress}. Click Refresh for latest progress.',
|
||||
@ -1505,6 +1506,8 @@ export default {
|
||||
'order.priceCheck.tag.priceOptimal': 'Best price',
|
||||
'order.priceCheck.tag.timeOptimal': 'Fastest',
|
||||
'order.priceCheck.tag.estimated': 'Reference',
|
||||
'order.priceCheck.hint.summaryInline': 'Indicative only; final after inbound',
|
||||
'order.priceCheck.hint.dismiss': 'Dismiss',
|
||||
'order.priceCheck.hint.bannerTitle': 'Reference quote notice',
|
||||
'order.priceCheck.hint.bannerLead': 'Some details were not provided. Prices below are for reference only and may change after order or inbound weighing.',
|
||||
'order.priceCheck.hint.missing.dimensions': 'No package dimensions: chargeable weight uses actual weight only; price may change after inbound measurement.',
|
||||
|
||||
@ -791,6 +791,7 @@ export default {
|
||||
'order.batchJobs.items.rowNumber': 'Excel 行号',
|
||||
'order.batchJobs.items.finishedAt': '完成时间',
|
||||
'order.batchJobs.items.retryCount': '重试 {count} 次',
|
||||
'order.batchJobs.items.viewResponse': '查看报文',
|
||||
'order.batchJobs.filter.allItems': '全部子项',
|
||||
'order.batchJobs.filter.itemStatus': '子项状态',
|
||||
'order.batchJobs.message.running': '任务执行中:{progress},请点击刷新查看最新进度',
|
||||
@ -1392,6 +1393,8 @@ export default {
|
||||
'order.priceCheck.tag.priceOptimal': '价格最优',
|
||||
'order.priceCheck.tag.timeOptimal': '时效最优',
|
||||
'order.priceCheck.tag.estimated': '参考价',
|
||||
'order.priceCheck.hint.summaryInline': '参考价,入库实测为准',
|
||||
'order.priceCheck.hint.dismiss': '关闭提示',
|
||||
'order.priceCheck.hint.bannerTitle': '查价说明',
|
||||
'order.priceCheck.hint.bannerLead': '您未填写部分条件,以下报价仅供参考,下单或入库实测后可能调整。',
|
||||
'order.priceCheck.hint.missing.dimensions': '未填写包裹尺寸:计费重按实重估算,入库实测尺寸后价格可能变化。',
|
||||
|
||||
@ -9,6 +9,8 @@ import {
|
||||
getBatchJobItemDisplayNo,
|
||||
buildBatchJobProgressText,
|
||||
formatBatchJobSummaryMessage,
|
||||
formatBatchJobTrackUpsertDetailText,
|
||||
getBatchJobTrackUpsertResultSummary,
|
||||
postOrderBatchJobCancel,
|
||||
postOrderBatchJobRetryFailed,
|
||||
type ParsedOrderBatchJob,
|
||||
@ -47,6 +49,7 @@ const completionNotified = ref(false)
|
||||
|
||||
const refNoColumnTitle = computed(() => t('order.batchJobs.items.clientNo'))
|
||||
const showRowNumber = computed(() => isBatchCreateAction(job.value?.action ?? ''))
|
||||
const isTrackUpsertJob = computed(() => job.value?.action === 'track-upsert-small-bag')
|
||||
|
||||
const progressPercent = computed(() => {
|
||||
if (!job.value?.total) return 0
|
||||
@ -195,12 +198,22 @@ function handleCancelPending() {
|
||||
}
|
||||
|
||||
function getItemResultText(record: ParsedOrderBatchJobItem) {
|
||||
if (isTrackUpsertJob.value && record.resultJson) {
|
||||
return getBatchJobTrackUpsertResultSummary(record) || record.resultMessage || t('order.batchJobs.itemStatus.success')
|
||||
}
|
||||
if (record.resultMessage) return record.resultMessage
|
||||
if (record.status === 'success') return t('order.batchJobs.itemStatus.success')
|
||||
if (record.status === 'pending') return t('order.batchJobs.itemStatus.pending')
|
||||
if (record.status === 'running') return t('order.batchJobs.itemStatus.running')
|
||||
return record.failReason || '-'
|
||||
}
|
||||
|
||||
function getItemResultDetailText(record: ParsedOrderBatchJobItem) {
|
||||
if (isTrackUpsertJob.value && record.resultJson) {
|
||||
return formatBatchJobTrackUpsertDetailText(record)
|
||||
}
|
||||
return record.resultJson || record.failReason || record.resultMessage || '-'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -333,11 +346,21 @@ function getItemResultText(record: ParsedOrderBatchJobItem) {
|
||||
</a-tag>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column :title="t('order.batchJobs.items.result')" :min-width="180">
|
||||
<a-table-column :title="t('order.batchJobs.items.result')" :min-width="220">
|
||||
<template #cell="{ record }">
|
||||
<span :class="{ 'fail-text': record.status === 'failed' }">
|
||||
{{ getItemResultText(record) }}
|
||||
</span>
|
||||
<a-popover
|
||||
v-if="record.resultJson"
|
||||
position="left"
|
||||
:content-style="{ maxWidth: '560px', maxHeight: '360px', overflow: 'auto' }"
|
||||
>
|
||||
<a-link style="margin-left: 8px">{{ t('order.batchJobs.items.viewResponse') }}</a-link>
|
||||
<template #content>
|
||||
<pre class="result-json-pre">{{ getItemResultDetailText(record) }}</pre>
|
||||
</template>
|
||||
</a-popover>
|
||||
<span v-if="record.retryCount > 0" class="retry-hint">
|
||||
({{ t('order.batchJobs.items.retryCount', { count: record.retryCount }) }})
|
||||
</span>
|
||||
@ -381,4 +404,13 @@ function getItemResultText(record: ParsedOrderBatchJobItem) {
|
||||
.drawer-toolbar {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.result-json-pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
font-family: Consolas, Monaco, 'Courier New', monospace;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -62,10 +62,6 @@
|
||||
<span class="basic-chip-label">{{ $t('order.myOrders.drawer.trackNo') }}</span>
|
||||
<span class="basic-chip-value">{{ getOrderValue('trackNo') }}</span>
|
||||
</span>
|
||||
<span class="basic-chip">
|
||||
<span class="basic-chip-label">{{ $t('order.myOrders.drawer.channelName') }}</span>
|
||||
<span class="basic-chip-value">{{ getOrderValue('channelName') }}</span>
|
||||
</span>
|
||||
<span class="basic-chip">
|
||||
<span class="basic-chip-label">{{ $t('order.myOrders.drawer.consigneeCountryName') }}</span>
|
||||
<span class="basic-chip-value">{{ getOrderValue('consigneeCountryName') }}</span>
|
||||
@ -98,7 +94,11 @@
|
||||
|
||||
<template v-for="section in visibleBasicSections" :key="section.key">
|
||||
<div class="detail-section" :class="section.key">
|
||||
<div class="detail-section-header">
|
||||
<div
|
||||
class="detail-section-header"
|
||||
:class="{ 'is-collapsible': isCollapsibleSection(section.key) }"
|
||||
@click="toggleSectionCollapse(section.key)"
|
||||
>
|
||||
<div class="detail-section-title-wrap">
|
||||
<span class="detail-section-dot" />
|
||||
<span class="detail-section-title">{{ $t(section.labelKey) }}</span>
|
||||
@ -110,16 +110,21 @@
|
||||
{{ displayDeclarationRows.length }}
|
||||
</a-tag>
|
||||
</div>
|
||||
<a-button
|
||||
v-if="section.key === 'declaration' && basicEditing"
|
||||
size="mini"
|
||||
type="outline"
|
||||
@click="addDeclarationRow"
|
||||
>
|
||||
{{ $t('order.myOrders.drawer.declaration.addRow') }}
|
||||
</a-button>
|
||||
<div class="detail-section-header-actions">
|
||||
<icon-down v-if="isCollapsibleSection(section.key) && isSectionCollapsed(section.key)" />
|
||||
<icon-up v-else-if="isCollapsibleSection(section.key)" />
|
||||
<a-button
|
||||
v-if="section.key === 'declaration' && basicEditing"
|
||||
size="mini"
|
||||
type="outline"
|
||||
@click.stop="addDeclarationRow"
|
||||
>
|
||||
{{ $t('order.myOrders.drawer.declaration.addRow') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="!isSectionCollapsed(section.key)" class="detail-section-body">
|
||||
<!-- 订单基本信息 -->
|
||||
<template v-if="section.key === 'basic'">
|
||||
<div
|
||||
@ -333,6 +338,7 @@
|
||||
class="detail-section-empty"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@ -442,7 +448,7 @@ import { ref, computed, watch, reactive } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { IconRefresh, IconCheckCircleFill, IconLocation, IconRobot } from '@arco-design/web-vue/es/icon';
|
||||
import { IconRefresh, IconCheckCircleFill, IconLocation, IconRobot, IconDown, IconUp } from '@arco-design/web-vue/es/icon';
|
||||
import useDrawer from '@/components/DrawerPro/useDrawer';
|
||||
import { postGetDetailTimeline } from '@/api/TrackOrder/trackOrder';
|
||||
import {
|
||||
@ -505,6 +511,28 @@ const basicSectionDefs = [
|
||||
{ key: 'declaration' as const, labelKey: 'order.myOrders.drawer.declaration' },
|
||||
];
|
||||
|
||||
const COLLAPSIBLE_BASIC_SECTIONS = new Set<BasicViewMode>(['sender']);
|
||||
const sectionCollapsed = reactive<Partial<Record<BasicViewMode, boolean>>>({
|
||||
sender: true,
|
||||
});
|
||||
|
||||
function isCollapsibleSection(key: BasicViewMode) {
|
||||
return COLLAPSIBLE_BASIC_SECTIONS.has(key);
|
||||
}
|
||||
|
||||
function isSectionCollapsed(key: BasicViewMode) {
|
||||
return Boolean(sectionCollapsed[key]);
|
||||
}
|
||||
|
||||
function toggleSectionCollapse(key: BasicViewMode) {
|
||||
if (!isCollapsibleSection(key)) return;
|
||||
sectionCollapsed[key] = !sectionCollapsed[key];
|
||||
}
|
||||
|
||||
function resetSectionCollapse() {
|
||||
sectionCollapsed.sender = true;
|
||||
}
|
||||
|
||||
const visibleBasicSections = computed(() => {
|
||||
if (basicViewMode.value === 'all') return basicSectionDefs;
|
||||
return basicSectionDefs.filter((section) => section.key === basicViewMode.value);
|
||||
@ -528,7 +556,6 @@ interface FormFieldDef {
|
||||
const orderEditFieldDefs: FormFieldDef[] = [
|
||||
{ key: 'clientNo', labelKey: 'order.myOrders.drawer.clientNo' },
|
||||
{ key: 'trackNo', labelKey: 'order.myOrders.drawer.trackNo' },
|
||||
{ key: 'channelCode', labelKey: 'order.myOrders.drawer.channelCode' },
|
||||
{
|
||||
key: 'cargoAttribute',
|
||||
labelKey: 'order.myOrders.drawer.cargoAttribute',
|
||||
@ -579,7 +606,6 @@ function getRecipientFormKey(key: string) {
|
||||
const basicEditForm = reactive({
|
||||
clientNo: '',
|
||||
trackNo: '',
|
||||
channelCode: '',
|
||||
cargoAttribute: 'general',
|
||||
isCod: false,
|
||||
codSum: undefined as number | undefined,
|
||||
@ -771,8 +797,6 @@ const orderBasicFields = [
|
||||
{ key: 'trackNo', labelKey: 'order.myOrders.drawer.trackNo' },
|
||||
{ key: 'productCode', labelKey: 'order.myOrders.drawer.productCode' },
|
||||
{ key: 'productName', labelKey: 'order.myOrders.drawer.productName' },
|
||||
{ key: 'channelCode', labelKey: 'order.myOrders.drawer.channelCode' },
|
||||
{ key: 'channelName', labelKey: 'order.myOrders.drawer.channelName' },
|
||||
{ key: 'consigneeCountry', labelKey: 'order.myOrders.drawer.consigneeCountry' },
|
||||
{ key: 'consigneeCountryName', labelKey: 'order.myOrders.drawer.consigneeCountryName' },
|
||||
{ key: 'cargoAttribute', labelKey: 'order.myOrders.drawer.cargoAttribute' },
|
||||
@ -808,8 +832,6 @@ const orderBasicFieldGroups = [
|
||||
fields: [
|
||||
'productCode',
|
||||
'productName',
|
||||
'channelCode',
|
||||
'channelName',
|
||||
'warehouse',
|
||||
'consigneeCountry',
|
||||
'consigneeCountryName',
|
||||
@ -1440,7 +1462,6 @@ function fillBasicEditForm() {
|
||||
if (!o?.id) return;
|
||||
basicEditForm.clientNo = String(o.clientNo ?? o.customerOrderNo ?? '');
|
||||
basicEditForm.trackNo = String(o.trackNo ?? o.trackingNo ?? '');
|
||||
basicEditForm.channelCode = String(o.channelCode ?? '');
|
||||
basicEditForm.cargoAttribute = String(o.cargoAttribute ?? 'general');
|
||||
basicEditForm.isCod = !!(o.isCod ?? o.IsCod);
|
||||
basicEditForm.codSum = o.codSum != null ? Number(o.codSum) : undefined;
|
||||
@ -1522,7 +1543,6 @@ async function handleBasicEditSave() {
|
||||
id,
|
||||
clientNo: basicEditForm.clientNo,
|
||||
trackNo: basicEditForm.trackNo,
|
||||
channelCode: basicEditForm.channelCode,
|
||||
cargoAttribute: basicEditForm.cargoAttribute,
|
||||
isCod: basicEditForm.isCod,
|
||||
codSum: basicEditForm.codSum,
|
||||
@ -1674,6 +1694,7 @@ defineExpose({
|
||||
const tab = resolveDrawerTab(opts?.tab);
|
||||
activeTab.value = tab;
|
||||
basicViewMode.value = 'all';
|
||||
resetSectionCollapse();
|
||||
resetEditState();
|
||||
setDrawerProps({
|
||||
content: '',
|
||||
@ -1906,6 +1927,27 @@ defineExpose({
|
||||
padding: 10px 12px;
|
||||
background: #f7f8fa;
|
||||
border-bottom: 1px solid #e5e6eb;
|
||||
|
||||
&.is-collapsible {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
&:hover .detail-section-title {
|
||||
color: var(--app-primary-color, #2d8c8c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.detail-section-header-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--color-text-3);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.detail-section-body {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.detail-section-title-wrap {
|
||||
|
||||
@ -22,6 +22,7 @@
|
||||
postOrderDownstreamForwardCreateOne,
|
||||
postOrderBatchJob,
|
||||
waitOrderBatchJob,
|
||||
formatBatchJobTrackUpsertResultText,
|
||||
isOrderBatchQueueDisabledError,
|
||||
formatBatchJobFailureMessage,
|
||||
formatBatchJobSummaryMessage,
|
||||
@ -492,12 +493,12 @@
|
||||
const job = await postOrderBatchJob({ action: 'track-upsert-small-bag', orderIds });
|
||||
const result = await waitOrderBatchJob(job.jobId);
|
||||
if (result.timedOut) {
|
||||
trackUpsertResultText.value = `${formatBatchJobSummaryMessage(result.job, { timedOut: true, jobId: job.jobId })}\n\n${formatBatchJobResultText('创建轨迹', result.job, result.items, rows)}`;
|
||||
trackUpsertResultText.value = `${formatBatchJobSummaryMessage(result.job, { timedOut: true, jobId: job.jobId })}\n\n${formatBatchJobTrackUpsertResultText('创建轨迹', result.job, result.items, rows)}`;
|
||||
trackUpsertModalVisible.value = true;
|
||||
return;
|
||||
}
|
||||
const { job: finishedJob, items } = result;
|
||||
trackUpsertResultText.value = formatBatchJobResultText('创建轨迹', finishedJob, items, rows);
|
||||
trackUpsertResultText.value = formatBatchJobTrackUpsertResultText('创建轨迹', finishedJob, items, rows);
|
||||
trackUpsertModalVisible.value = true;
|
||||
return;
|
||||
} catch (e: any) {
|
||||
|
||||
@ -11,7 +11,9 @@
|
||||
|
||||
后端入口:`OrderService`(`Oms.Application.Services.Orders`),Swagger 分组 **OMS-Order**。
|
||||
|
||||
**心智模型:** 创建订单 = **小包预报流水线** — 绑快照 → 校验地址 → 计重 → 匹配分区 → 算应收 → 排货算应付 → 状态 `predeclared`。
|
||||
**心智模型:** 创建订单 = **预报应收流水线** — 绑快照 → 校验地址 → 计重 → 匹配分区 → **仅写应收(forecast)** → 状态 `predeclared`。**排货与渠道买价在入库环节处理**(失败则 `pendingAllocation` 待排货,不阻断收货与应收)。
|
||||
|
||||
**全链路:** 查价(参考)→ 下单(预报应收)→ 入库(实测 + 应收 + 可选排货)→ 付款 → 出库(发货实测 + outbound 应收)。
|
||||
|
||||
---
|
||||
|
||||
@ -27,13 +29,27 @@
|
||||
| **forecast 重量** | 预报阶段计重行,应收费用用它 |
|
||||
| **downstream 重量** | 下游计重行,**排货应付费用用它**(可与 forecast 相同) |
|
||||
| **ChargingStage** | `forecast` / `inbound` / `outbound`,费用按阶段区分 |
|
||||
| **AllocationPendingReason** | 入库排货/渠道买价未匹配时的原因(`pendingAllocation` 状态展示) |
|
||||
|
||||
### 订单状态(创建相关)
|
||||
### 订单状态(生命周期相关)
|
||||
|
||||
| 状态 | 含义 |
|
||||
|------|------|
|
||||
| `draft` | 草稿;预报计费失败时保留,并写 `PredeclareFailReason` |
|
||||
| `predeclared` | **创建成功**后的默认状态 |
|
||||
| `predeclared` | **创建成功**后的默认状态(仅预报应收,未排货) |
|
||||
| `pendingAllocation` | **待排货**:已入库、应收已算,但排货/渠道买价未成功 |
|
||||
| `pendingPayment` | 待支付(入库排货成功或手动重算应付成功后) |
|
||||
| `pendingShip` | 待发货(已付款) |
|
||||
| `pendingOnline` | 待上网(已出库) |
|
||||
|
||||
### 运单号(ServiceNo)
|
||||
|
||||
| 项 | 规则 |
|
||||
|----|------|
|
||||
| 前缀 | **`IDE`** |
|
||||
| 格式 | `IDE` + **`yyMMddHHmmss`**(年份取后两位,如 2026 → `26`)+ 4 位随机数 |
|
||||
| 示例 | `IDE2607221859123456` |
|
||||
| 自定义 | 请求体可传 `serviceNo`,须唯一;否则服务端自动生成 |
|
||||
|
||||
---
|
||||
|
||||
@ -77,16 +93,15 @@ flowchart TD
|
||||
G --> H[解析目的国 Country]
|
||||
H --> I{IsBlacklistHitAsync?}
|
||||
I -->|命中| X1
|
||||
I --> J[生成 ServiceNo NE*]
|
||||
I --> J[生成 ServiceNo IDE+yyMMddHHmmss+随机]
|
||||
J --> K[Insert t_order 草稿]
|
||||
K --> L[SaveParcelChildren<br/>收发件/申报/4行重量]
|
||||
L --> M{预报计费 try}
|
||||
M --> N[偏远判定 IsRemote]
|
||||
N --> O[ApplyChargeableWeightAsync 计重]
|
||||
O --> P[ResolvePriceZoneKeysAsync 分区]
|
||||
P --> Q[UpsertReceivableFeesAsync 应收]
|
||||
Q --> R[ConfirmServiceChannelAndPayableAsync<br/>排货+应付]
|
||||
R --> S[OrderStatus=predeclared]
|
||||
O --> P[ResolveOrderBillingContext 分区]
|
||||
P --> Q[UpsertReceivableFeesAsync 应收 forecast]
|
||||
Q --> S[OrderStatus=predeclared]
|
||||
S --> T[轨迹 Upsert + 自动触发]
|
||||
T --> Z2[返回 OrderParcelCreateOutput]
|
||||
M -->|异常| U[MarkParcelDraftFailedAsync<br/>draft + PredeclareFailReason]
|
||||
@ -207,8 +222,8 @@ flowchart TD
|
||||
|
||||
| 步骤 | 动作 | 写入表/字段 |
|
||||
|------|------|-------------|
|
||||
| 1 | `ResolveServiceNoAsync` | 运单号前缀 `NE` |
|
||||
| 2 | `BuildDraftOrderEntity` | `t_order`:产品、国家、快照版本、初始 `draft`;`ChannelCode` 暂用 `ProductCode`,排货成功后覆盖 |
|
||||
| 1 | `ResolveServiceNoAsync` | 运单号 **`IDE{yyMMddHHmmss}{4位随机}`** |
|
||||
| 2 | `BuildDraftOrderEntity` | `t_order`:产品、国家、快照版本、初始 `draft`;**创建阶段不写 ChannelCode 排货结果** |
|
||||
| 3 | `BuildInitialWeightRows` | `t_order_weight` 四行:`forecast` / `receive` / `ship` / `downstream` |
|
||||
| 4 | `SaveParcelChildren` | `t_order_sender`、`t_order_recipient`、`t_order_declaration` |
|
||||
|
||||
@ -227,11 +242,12 @@ flowchart TD
|
||||
T3 --> T4[ResolvePriceZoneKeysAsync]
|
||||
T4 --> T5{zoneKeys 非空?}
|
||||
T5 -->|否| E1[价格分区未匹配]
|
||||
T5 --> T6[UpsertReceivableFeesAsync<br/>用 forecast.ChargeableWeight]
|
||||
T6 --> T7[ConfirmServiceChannelAndPayableAsync<br/>用 downstream.ChargeableWeight]
|
||||
T7 --> T8[status=predeclared]
|
||||
T5 --> T6[UpsertReceivableFeesAsync<br/>ChargingStage=forecast]
|
||||
T6 --> T8[status=predeclared]
|
||||
```
|
||||
|
||||
> **创建阶段不算应付、不排货。** 渠道与买价在 **入库** `ApplyReceiveBillingByWaybillAsync` 中通过 `TryConfirmServiceChannelAndPayableAsync` 尝试;失败写入 `AllocationPendingReason`,订单进 **`pendingAllocation`**,仍保留入库实测与 inbound 应收。
|
||||
|
||||
---
|
||||
|
||||
#### 3.5.2 偏远判定 `IsRemoteHitAsync`
|
||||
@ -393,43 +409,27 @@ flowchart TD
|
||||
|
||||
---
|
||||
|
||||
#### 3.5.6 排货与应付 `ConfirmServiceChannelAndPayableAsync`
|
||||
#### 3.5.6 入库排货与应付(创建时不执行)
|
||||
|
||||
排货逻辑在 **入库收货** 时调用 `TryConfirmServiceChannelAndPayableAsync`(失败不抛异常):
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A1[ResolveAllocationProviders<br/>规则快照 providers] --> A2[FilterProviders<br/>须 ServiceChannelId>0]
|
||||
A2 --> A3[客户白名单过滤<br/>CustomerIds 空=全部]
|
||||
A3 --> A4{候选数>0?}
|
||||
A4 -->|否| E1[排货规则未匹配到可用服务渠道]
|
||||
A4 --> A5[遍历每个候选渠道]
|
||||
A5 --> A6{渠道启用?}
|
||||
A6 --> A7[ResolveChannelPriceBinding<br/>渠道买价快照]
|
||||
A7 --> A8[CalculatePayableFees 预览总成本]
|
||||
A8 --> A9{应付>0?}
|
||||
A9 -->|是| A10[加入 candidates]
|
||||
A9 -->|否| A5
|
||||
A10 --> A11{candidates>0?}
|
||||
A11 -->|否| E2[排货规则未匹配到有效渠道买价]
|
||||
A11 --> A12[AllocationResolver.SelectChannel]
|
||||
A12 --> A13{Scheme}
|
||||
A13 -->|costLowest| A14[应付总额最小]
|
||||
A13 -->|timeFastest| A15[MinTime 最小]
|
||||
A13 -->|overallBest| A16[Provider.Weight 最大]
|
||||
A14 --> A17[写 ChannelCode + 下游字段]
|
||||
A15 --> A17
|
||||
A16 --> A17
|
||||
A17 --> A18[重算应付并 UpsertFees payable]
|
||||
A1[入库实测件重尺 receive 行] --> A2[重算 inbound 应收]
|
||||
A2 --> A3[TryConfirmServiceChannelAndPayableAsync]
|
||||
A3 -->|成功| A4[OrderStatus=pendingPayment<br/>清空 AllocationPendingReason]
|
||||
A3 -->|失败| A5[OrderStatus=pendingAllocation<br/>写 AllocationPendingReason]
|
||||
```
|
||||
|
||||
| Scheme | 选渠规则 |
|
||||
|--------|----------|
|
||||
| `costLowest`(默认) | `TotalPayableCost` 最小,同价按序号 |
|
||||
| `costLowest`(默认) | 应付总额最小,同价按序号 |
|
||||
| `timeFastest` | 渠道 `MinTime` 最小 |
|
||||
| `overallBest` | 服务商 `Weight`(评分)最大 |
|
||||
|
||||
**注意:** 排货应付使用 **`downstream` 行的 ChargeableWeight**(若为空则回退 forecast),与应收使用的 forecast 行可能相同(创建时四行已同步)。
|
||||
**注意:** 入库排货应付使用 **`receive` 行**的 `ChargeableWeight`(实测后重算)。手动「重算应付」成功也会清原因并改为 `pendingPayment`。
|
||||
|
||||
**写入订单字段:** `ChannelCode`、`DownstreamSupplierCode`、`DownstreamAppKey`、`ChannelForecastCode` 等(`AllocationProviderSnapshotEnricher`)。
|
||||
**写入订单字段(排货成功时):** `ChannelCode`、`DownstreamSupplierCode`、`DownstreamAppKey`、`ChannelForecastCode` 等。
|
||||
|
||||
---
|
||||
|
||||
@ -506,21 +506,41 @@ flowchart LR
|
||||
| 未配置计重规则/进位 | 产品配置 + 规则发布快照 |
|
||||
| 计重规则条件均未命中 | 件重尺与规则 Condition |
|
||||
| 卖价未匹配到有效价格 | 卖价矩阵分区/重量段/货物属性 |
|
||||
| 排货无可用渠道 | 排货列表、客户白名单、渠道启用 |
|
||||
| 排货无有效渠道买价 | 渠道买价发布、分区/重量段匹配 |
|
||||
| 卖价未匹配到有效价格 | 卖价矩阵分区/重量段/货物属性 |
|
||||
| 入库排货失败 | 订单为 **待排货**,查 `AllocationPendingReason`;补排货规则/渠道买价后重算应付 |
|
||||
| 客户单号已存在 | 同客户重复下单 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 查价 vs 创建
|
||||
## 4. 查价 vs 创建 vs 入库 vs 出库
|
||||
|
||||
| 对比 | `POST /order/price-check` | `POST /order/parcel/create` |
|
||||
|------|---------------------------|------------------------------|
|
||||
| 落库 | 否 | 是 |
|
||||
| 校验 | 目的国/重量/尺寸 | 完整收发件/申报/客户单号唯一等 |
|
||||
| 黑名单 | 试算路径可失败记录 | **落库前硬拦截** |
|
||||
| 排货 | 不绑 ChannelCode | 自动排货写库 |
|
||||
| 算价引擎 | 同一 `ParcelBillingCalculator` | 同一套 |
|
||||
### 4.1 环节对比
|
||||
|
||||
| 对比 | 查价 | 下单(预报) | 入库 | 出库 |
|
||||
|------|------|--------------|------|------|
|
||||
| 接口 | `POST /order/price-check` | `POST /order/parcel/create` | `POST /operation/inbound/create` | `POST /operation/outbound/create` |
|
||||
| 落库 | 否 | 是 | 写入库记录 + 订单 | 写出库记录 + 订单 |
|
||||
| 尺寸 | **可选**(全空按实重估算) | **必填** 长/宽/高/实重 > 0 | 实测必填 | 实测必填 |
|
||||
| 邮编/黑名单 | 无邮编则**跳过**黑名单/偏远 | **必填邮编**,黑名单**硬拦截** | 用订单已有地址 | — |
|
||||
| 计费 | 参考价,`queryHint` + `quoteLevel` | 仅 **forecast 应收** | **inbound 应收** + 可选排货应付 | **outbound 应收** |
|
||||
| 排货 | 不执行 | **不执行** | 可选;失败 → `pendingAllocation` | 不执行 |
|
||||
| 状态 | — | `predeclared` | `pendingPayment` 或 `pendingAllocation` | `pendingOnline`(须已付款) |
|
||||
|
||||
### 4.2 查价宽松规则(对客)
|
||||
|
||||
- **必填**:目的国 + 实重 > 0。
|
||||
- **尺寸**:全空或三者齐全;部分填写会报错。
|
||||
- **未填邮编**:不校验黑名单;分区无匹配时可 **zone 兜底**(取最低价分区,标记为估算)。
|
||||
- **未选货物属性/仓库**:按 **全部启用产品 × 全部仓库 × 货物类型展开**(general/electronics/sensitive)试算。
|
||||
- 响应 **`queryHint.disclaimers`**、每行 **`quoteLevel`**(`exact` / `estimated`)提示「仅供参考,入库实测后可能调整」。
|
||||
|
||||
### 4.3 共用校验 `ParcelBillingValidators`
|
||||
|
||||
| 方法 | 用途 |
|
||||
|------|------|
|
||||
| `EnsurePriceCheckDimensions` | 查价:尺寸全空或齐全 |
|
||||
| `EnsurePositiveDimensions` | 下单/入库/出库:件重尺均 > 0 |
|
||||
| `EnsureInboundOrderStatus` | 入库:仅 `predeclared` / `pendingAllocation` |
|
||||
|
||||
---
|
||||
|
||||
@ -542,20 +562,22 @@ flowchart LR
|
||||
| 主题 | 文件 / 方法 |
|
||||
|------|-------------|
|
||||
| 创建主流程 | `OrderService.ParcelBilling.cs` → `CreateParcelCoreAsync` |
|
||||
| 入参校验 | `ValidateParcelCreateInputAsync` |
|
||||
| 快照绑定 | `ResolveSnapshotBindingAsync` |
|
||||
| 计重 | `ApplyChargeableWeightAsync` + `ParcelBillingCalculator.CalculateRawBillableWeight` |
|
||||
| 分区 | `ResolvePriceZoneKeysAsync` + `IsAreaMatched` |
|
||||
| 应收 | `UpsertReceivableFeesAsync` + `CalculateReceivableFees` |
|
||||
| 排货应付 | `ConfirmServiceChannelAndPayableAsync` + `AllocationResolver` |
|
||||
| 查价 | `OrderService.ParcelQuote.cs` |
|
||||
| 入参校验 | `ValidateParcelCreateInputAsync` + `ParcelBillingValidators` |
|
||||
| 快照绑定 | `ResolveSnapshotBindingAsync` / `ResolveOrderBillingContextAsync` |
|
||||
| 计重 | `ApplyChargeableWeightAsync` + `ParcelBillingCalculator` |
|
||||
| 应收 | `UpsertReceivableFeesAsync` |
|
||||
| 入库排货 | `ApplyReceiveBillingByWaybillAsync` → `TryConfirmServiceChannelAndPayableAsync` |
|
||||
| 出库 | `ApplyShipBillingByWaybillAsync` |
|
||||
| 查价 | `OrderService.ParcelQuote.cs` + `PriceCheckHintBuilder` |
|
||||
| 运单号 | `ResolveServiceNoAsync` → `IDE{yyMMddHHmmss}{随机}` |
|
||||
| API 路由 | `OrderService.cs` → `CreateParcelAsync` |
|
||||
|
||||
---
|
||||
|
||||
## 7. 推荐阅读顺序
|
||||
|
||||
1. 本文 **§3 流程图**(总览 → 校验 → 计重 → 分区 → 应收 → 排货);
|
||||
2. **渠道新手指引**(快照发布、排货配置);
|
||||
3. 用查价验证报价,再下一笔测试单,在「我的订单」看 `t_order_fee` 与 `ChannelCode`;
|
||||
4. 预报失败时对照 **§3.8** 与订单 `PredeclareFailReason`。
|
||||
1. 本文 **§3 创建流程** + **§4 全链路对比**;
|
||||
2. **渠道新手指引**(快照发布、排货配置、渠道买价);
|
||||
3. 用 **我要查价** 看参考价,再 **我要下单** 下测试单;
|
||||
4. 运营 **入库/出库** 扫描,在「我的订单」核对状态与费用;
|
||||
5. 预报失败对照 **§3.8** 与 `PredeclareFailReason`;待排货对照 `AllocationPendingReason`。
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue';
|
||||
import { IconSearch, IconRefresh, IconDown, IconUp } from '@arco-design/web-vue/es/icon';
|
||||
import { IconSearch, IconRefresh, IconDown, IconUp, IconInfoCircle } from '@arco-design/web-vue/es/icon';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
@ -62,6 +62,7 @@
|
||||
const recommendReason = ref('');
|
||||
const aiRecommendLoading = ref(false);
|
||||
const queryHint = ref<PriceCheckQueryHint | null>(null);
|
||||
const hintBannerDismissed = ref(false);
|
||||
|
||||
const advancedActiveCount = computed(() => {
|
||||
let n = 0;
|
||||
@ -85,6 +86,11 @@
|
||||
return codes.map((code) => t(`order.priceCheck.hint.missing.${code}`)).filter(Boolean);
|
||||
});
|
||||
|
||||
const queryHintSummary = computed(() => {
|
||||
if (!showQueryHintBanner.value) return '';
|
||||
return t('order.priceCheck.hint.summaryInline');
|
||||
});
|
||||
|
||||
function normalizeStringList(raw: unknown): string[] {
|
||||
if (!Array.isArray(raw)) return [];
|
||||
return raw.map((x) => String(x ?? '').trim()).filter(Boolean);
|
||||
@ -426,6 +432,7 @@
|
||||
queryLoading.value = true;
|
||||
queryFailureText.value = '';
|
||||
queryHint.value = null;
|
||||
hintBannerDismissed.value = false;
|
||||
try {
|
||||
const length = parseOptionalDimension(formData.length);
|
||||
const width = parseOptionalDimension(formData.width);
|
||||
@ -443,6 +450,7 @@
|
||||
postalCode: formData.postalCode?.trim() || '',
|
||||
});
|
||||
queryHint.value = hint;
|
||||
hintBannerDismissed.value = false;
|
||||
queryResults.value = processQueryResults(items || []);
|
||||
selectedGuideRecord.value = null;
|
||||
recommendReason.value = '';
|
||||
@ -479,6 +487,7 @@
|
||||
});
|
||||
queryResults.value = [];
|
||||
queryHint.value = null;
|
||||
hintBannerDismissed.value = false;
|
||||
selectedGuideRecord.value = null;
|
||||
recommendReason.value = '';
|
||||
currentPage.value = 1;
|
||||
@ -621,10 +630,7 @@
|
||||
<div class="query-form-layout liquid-panel__content">
|
||||
<div class="form-container">
|
||||
<div class="form-head">
|
||||
<div>
|
||||
<h2 class="form-head__title">{{ $t('order.priceCheck.form.title') }}</h2>
|
||||
<p class="form-head__desc">{{ $t('order.priceCheck.form.subtitle') }}</p>
|
||||
</div>
|
||||
<h2 class="form-head__title">{{ $t('order.priceCheck.form.title') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="form-row form-row-primary">
|
||||
@ -666,7 +672,9 @@
|
||||
</div>
|
||||
|
||||
<div class="form-item dimensions-group">
|
||||
<label class="form-label">{{ $t('order.priceCheck.form.dimensions') }}</label>
|
||||
<label class="form-label" :title="$t('order.priceCheck.form.dimensionsHintOptional')">
|
||||
{{ $t('order.priceCheck.form.dimensions') }}
|
||||
</label>
|
||||
<div class="dimensions-inputs">
|
||||
<a-input
|
||||
v-model="formData.length"
|
||||
@ -684,7 +692,6 @@
|
||||
class="dimension-input"
|
||||
/>
|
||||
</div>
|
||||
<p class="form-hint">{{ $t('order.priceCheck.form.dimensionsHintOptional') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="form-item button-group">
|
||||
@ -789,20 +796,6 @@
|
||||
|
||||
<section class="liquid-panel table-section">
|
||||
<div class="table-section__inner liquid-panel__content">
|
||||
<a-alert
|
||||
v-if="showQueryHintBanner"
|
||||
type="warning"
|
||||
class="price-check-hint-banner"
|
||||
:title="$t('order.priceCheck.hint.bannerTitle')"
|
||||
closable
|
||||
>
|
||||
<template v-if="queryHint?.quoteLevel === 'estimated'">
|
||||
<p class="hint-banner-lead">{{ $t('order.priceCheck.hint.bannerLead') }}</p>
|
||||
</template>
|
||||
<ul v-if="queryHintDisclaimers.length" class="hint-banner-list">
|
||||
<li v-for="(line, idx) in queryHintDisclaimers" :key="idx">{{ line }}</li>
|
||||
</ul>
|
||||
</a-alert>
|
||||
<div class="table-toolbar">
|
||||
<div class="toolbar-left">
|
||||
<div class="sort-tabs" role="tablist">
|
||||
@ -834,6 +827,36 @@
|
||||
<span v-if="totalCount" class="result-count">
|
||||
{{ t('order.priceCheck.resultCount', { count: totalCount }) }}
|
||||
</span>
|
||||
<template v-if="showQueryHintBanner && !hintBannerDismissed">
|
||||
<a-popover
|
||||
v-if="queryHintDisclaimers.length"
|
||||
position="bl"
|
||||
trigger="hover"
|
||||
:content-style="{ maxWidth: '320px', padding: '8px 12px' }"
|
||||
>
|
||||
<button type="button" class="hint-inline-chip">
|
||||
<icon-info-circle />
|
||||
<span>{{ queryHintSummary }}</span>
|
||||
</button>
|
||||
<template #content>
|
||||
<ul class="hint-popover-list">
|
||||
<li v-for="(line, idx) in queryHintDisclaimers" :key="idx">{{ line }}</li>
|
||||
</ul>
|
||||
</template>
|
||||
</a-popover>
|
||||
<span v-else class="hint-inline-chip hint-inline-chip--static">
|
||||
<icon-info-circle />
|
||||
<span>{{ queryHintSummary }}</span>
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
class="hint-inline-dismiss"
|
||||
:title="$t('order.priceCheck.hint.dismiss')"
|
||||
@click="hintBannerDismissed = true"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<div class="pagination-controls">
|
||||
@ -1099,9 +1122,8 @@
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
.form-head { margin-bottom: 14px; }
|
||||
.form-head { margin-bottom: 10px; }
|
||||
.form-head__title { margin: 0; color: #1d2129; font-size: 16px; font-weight: 650; line-height: 1.3; }
|
||||
.form-head__desc { margin: 4px 0 0; color: var(--color-text-3); font-size: 12px; line-height: 1.4; }
|
||||
.form-row { display: flex; gap: 16px; align-items: flex-start; flex-wrap: wrap; }
|
||||
.form-row-primary, .form-row-advanced { overflow-x: auto; padding-bottom: 2px; -webkit-overflow-scrolling: touch; }
|
||||
.form-row-advanced { margin-top: 4px; padding-top: 12px; border-top: 1px dashed rgba(45, 140, 140, 0.18); }
|
||||
@ -1161,16 +1183,62 @@
|
||||
.table-section { flex: 1; min-height: 0; display: flex; flex-direction: column; padding: 0; }
|
||||
.table-section__inner { flex: 1; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }
|
||||
.table-toolbar {
|
||||
padding: 14px 16px 0;
|
||||
padding: 12px 16px 0;
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.toolbar-left { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; min-width: 0; }
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
min-width: 0;
|
||||
}
|
||||
.hint-inline-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
max-width: 220px;
|
||||
height: 24px;
|
||||
padding: 0 8px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: rgba(255, 125, 0, 0.08);
|
||||
color: #d48806;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
cursor: default;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
:deep(svg) { flex-shrink: 0; font-size: 14px; }
|
||||
&--static { cursor: default; }
|
||||
}
|
||||
.hint-inline-dismiss {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: var(--color-text-3);
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
&:hover { color: var(--color-text-2); background: rgba(0, 0, 0, 0.04); }
|
||||
}
|
||||
.hint-popover-list {
|
||||
margin: 0;
|
||||
padding-left: 16px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--color-text-2);
|
||||
li + li { margin-top: 4px; }
|
||||
}
|
||||
.sort-tabs {
|
||||
display: inline-flex; padding: 3px; border-radius: 10px; border: 1px solid rgba(255, 255, 255, 0.7);
|
||||
background: rgba(255, 255, 255, 0.45); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8);
|
||||
@ -1185,23 +1253,6 @@
|
||||
}
|
||||
}
|
||||
.result-count { color: var(--color-text-3); font-size: 13px; }
|
||||
.price-check-hint-banner {
|
||||
margin: 0 12px 12px;
|
||||
:deep(.arco-alert-body) { padding-top: 4px; }
|
||||
}
|
||||
.hint-banner-lead {
|
||||
margin: 0 0 8px;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-2);
|
||||
line-height: 1.5;
|
||||
}
|
||||
.hint-banner-list {
|
||||
margin: 0;
|
||||
padding-left: 18px;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-2);
|
||||
line-height: 1.6;
|
||||
}
|
||||
.quote-disclaimer-popover {
|
||||
max-width: 320px;
|
||||
white-space: pre-wrap;
|
||||
|
||||
@ -22,6 +22,7 @@
|
||||
postOrderDownstreamForwardCreateOne,
|
||||
postOrderBatchJob,
|
||||
waitOrderBatchJob,
|
||||
formatBatchJobTrackUpsertResultText,
|
||||
isOrderBatchQueueDisabledError,
|
||||
formatBatchJobFailureMessage,
|
||||
formatBatchJobSummaryMessage,
|
||||
@ -552,12 +553,12 @@
|
||||
const job = await postOrderBatchJob({ action: 'track-upsert-small-bag', orderIds });
|
||||
const result = await waitOrderBatchJob(job.jobId);
|
||||
if (result.timedOut) {
|
||||
trackUpsertResultText.value = `${formatBatchJobSummaryMessage(result.job, { timedOut: true, jobId: job.jobId })}\n\n${formatBatchJobResultText('创建轨迹', result.job, result.items, rows)}`;
|
||||
trackUpsertResultText.value = `${formatBatchJobSummaryMessage(result.job, { timedOut: true, jobId: job.jobId })}\n\n${formatBatchJobTrackUpsertResultText('创建轨迹', result.job, result.items, rows)}`;
|
||||
trackUpsertModalVisible.value = true;
|
||||
return;
|
||||
}
|
||||
const { job: finishedJob, items } = result;
|
||||
trackUpsertResultText.value = formatBatchJobResultText('创建轨迹', finishedJob, items, rows);
|
||||
trackUpsertResultText.value = formatBatchJobTrackUpsertResultText('创建轨迹', finishedJob, items, rows);
|
||||
trackUpsertModalVisible.value = true;
|
||||
return;
|
||||
} catch (e: any) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user