diff --git a/docs/oms系统设计-050801.md b/docs/oms系统设计-050801.md index bdd1b13..8b9c4e6 100644 --- a/docs/oms系统设计-050801.md +++ b/docs/oms系统设计-050801.md @@ -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 | 待发货 | diff --git a/src/api/order/index.ts b/src/api/order/index.ts index af9f41e..543810b 100644 --- a/src/api/order/index.ts +++ b/src/api/order/index.ts @@ -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 = { pay: '/order/batch/pay', 'pre-declare': '/order/batch/pre-declare', diff --git a/src/locale/en-US.ts b/src/locale/en-US.ts index 438e2cf..1b82724 100644 --- a/src/locale/en-US.ts +++ b/src/locale/en-US.ts @@ -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.', diff --git a/src/locale/zh-CN.ts b/src/locale/zh-CN.ts index 6930247..fc75063 100644 --- a/src/locale/zh-CN.ts +++ b/src/locale/zh-CN.ts @@ -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': '未填写包裹尺寸:计费重按实重估算,入库实测尺寸后价格可能变化。', diff --git a/src/views/order/batchJobs/components/BatchJobDetailDrawer.vue b/src/views/order/batchJobs/components/BatchJobDetailDrawer.vue index 4c53c89..d89782b 100644 --- a/src/views/order/batchJobs/components/BatchJobDetailDrawer.vue +++ b/src/views/order/batchJobs/components/BatchJobDetailDrawer.vue @@ -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 || '-' +} - +