front/docs/揽收单与物流单串联方案.md
zzh 25445bc146 feat: 完善日夜间主题与揽收下单体验
统一暗色模式页面/顶栏/Handsontable 选中色,调整下单到仓方式文案与提示,CustomLeftMenu 卡片统一 60px;补充揽收单、问题件处理与 H5 相关页面。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-04 11:49:23 +08:00

518 lines
21 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 揽收单与物流单串联方案
> **文档状态**:产品 + 架构 + 表结构 + 接口(**先评审,再开发**)
> **编写日期**:2026-08-03
> **仓库**:前端 `E:\oms0414`,后端 `oms0414-back`
> **关联页面**:PC「我的订单」`/order/myOrders`、下单页 `/order/placeOrder`、揽收管理(新建)、H5 订单 / 发货指引
> **关联能力**:小包创建 `CreateParcelCoreAsync`、客户发件地址簿、订单支付、应收应付
> **关联文档**:`docs/oms系统设计-050801.md`(§4.4.4 旧揽收)、`docs/应收应付`、`docs/前端页面设计规范.md`、`docs/H5客户中心设计方案.md`
> **目标读者**:产品、前端、后端、运营、测试
---
## 0 阅读指引
| 角色 | 优先阅读 |
| --- | --- |
| 产品 / 运营 | §1~§3、§5、§6、§11 |
| 前端 | §5、§6、§8、§10 |
| 后端 / DBA | §4、§7~§9、§10、§12 |
| 测试 | §3、§7、§11、§13 |
**分期对照**
| 阶段 | 名称 | 交付物 |
| --- | --- | --- |
| **P1** | 主路径打通 | 表结构、创建/列表/详情、我的订单批量创建揽收、下单页到仓方式、对客揽收费支付(基础) |
| **P2** | 履约与改绑 | 追加/移除关联、改约/取消、承运商单号、管理端全操作、草稿补承运 |
| **P3** | 承运与结算 | 承运商 API 下发、结算批次与应付报表、H5 多选合并 |
> 本文默认按 **P1 → P2** 落地;P3 列边界。
> **旧能力**:`OrderSource=pickup` + `POST /order/pickup/create` 直接落 `t_order`(收件=仓库)退出主路径,见 §12。
---
## 1 背景与目标
### 1.1 现状与问题
| 现状 | 问题 |
| --- | --- |
| 无独立揽收实体;`t_order.OrderSource=pickup` 复用小包创建 | 揽收与跨境物流语义混在一张单上(收件被映射为仓库) |
| H5 / PC 提供「揽收订单」独立入口 | 用户以为是另一品类,与「物流订单」割裂 |
| 发货指引「叫上门揽收」再调 `pickup/create` | **另开一张送仓单**,与已下跨境单无关联 |
| 揽收列表有 `serviceNo` 但不能下钻;订单详情几乎不展示预约 | 无法「一单一号」跟完履约 |
| 无承运商侧应付与结算对象 | 无法管理揽收承运对账 |
### 1.2 建设目标
1. **物流单为主业务单**:跨境收件、申报、计费、支付、出库、轨迹仍在 `t_order`。
2. **揽收单为独立履约 / 结算单**:自有 `pickupNo`,可对接承运商、可对客收揽收运费、可对承运商结算。
3. **1 揽收 : N 物流**:多票货可合并一次上门取件。
4. **双入口汇入同一模型**:下单时声明到仓方式;我的订单勾选批量「创建揽收」。
5. **先物流后挂载**:主路径是先有物流单,再创建或挂载揽收单(下单勾选上门时同步建揽收亦可)。
### 1.3 已拍板决策
| # | 决策 | 口径 |
| --- | --- | --- |
| 1 | 合并约束 | **同一仓库 + 同一取件地址** 方可并入同一揽收单 |
| 2 | 改绑时机 | **已支付但未入库** 允许改绑/合并;**已取件 / 已到仓后禁止** |
| 3 | 揽收运费 | **客户支付**(揽收单对客应收);承运商成本在揽收单应付侧结算 |
| 4 | 承运商 | **可后补**:允许无承运商落 `draft`,补齐后再预约/下发 |
### 1.4 设计原则
1. **两本账分离**:跨境应收应付仍在 `t_order` / `t_order_fee`;上门取件费在 `t_pickup_order`,**不拆到各物流单费用行**(避免 N 票改绑费用乱飞)。
2. **列表可冗余**:`t_order.PickupOrderId` / `InboundMethod` 方便筛选,真源以揽收单 + 关联表为准。
3. **不与仓储 `C_parcel` 混表**(见仓储模块设计)。
4. **支付失败不阻断建运单**:下单同步建揽收失败时,物流单保留,提示客户到「我的订单」补建。
5. **默认先付再履约**:`draft → scheduled` 前须运费已确认且(默认)客户已支付揽收费;到付若需要另开 `PayMode`(P2+)。
### 1.5 非目标(P1 / P2 不做)
- 承运商实时轨迹深度对接(P3)
- 一揽收送多仓、多取件地址拼车
- 把「上门取件」做成销售产品附加服务勾选替代揽收单
- 旧 `OrderSource=pickup` 送仓单自动「升级」改海外收件人
- 改变跨境订单主状态机与现有小包计费主链路
---
## 2 概念与命名
| 术语 | 含义 |
| --- | --- |
| **物流订单 / 运单** | `t_order`;对外单号多为 `ServiceNo` |
| **揽收单** | `t_pickup_order`;独立单号 `PickupNo`;上门取件履约 + 对客收款 + 承运结算 |
| **到仓方式** | `InboundMethod`:`selfShip` 自寄仓 / `doorPickup` 上门揽收 |
| **挂载** | 物流单通过 `t_pickup_order_line` 关联到揽收单 |
| **改绑** | 未取件前将物流单从 A 揽收移到 B 揽收,或移出后新建 |
| **揽收运费(对客)** | 客户支付的上门取件费,记在揽收单 |
| **承运应付** | 付给揽收承运商的成本,记在揽收单,管理端结算 |
**命名注意**:对外文案优先用「上门取件 / 揽收单」;避免再让客户以为「揽收订单」是另一种跨境运单。
---
## 3 总览
```mermaid
flowchart TB
subgraph create [创建物流]
A[创建物流订单] --> B{到仓方式}
B -->|自寄| C[InboundMethod=selfShip<br/>不建揽收单]
B -->|上门揽收| D[发件地址 + 期望时间<br/>承运可空]
D --> E[落 t_order + 建 t_pickup_order<br/>1 行 line]
end
subgraph batch [我的订单]
F[勾选多票物流单] --> G[创建揽收]
G --> H{同仓且同取件地址?}
H -->|否| I[按行列出拒绝原因]
H -->|是| J[一张 PickupNo + N 行 line]
end
E --> K[t_pickup_order]
J --> K
K --> L[确认运费 → 客户支付]
L --> M[补承运商 → scheduled]
M --> N[取件 → 到仓]
N --> O[承运商结算 Settlement]
N --> P[物流单继续入库/支付跨境费/出库…]
```
**客户心智**:多票可共享一个揽收单号上门取件;每票跨境仍各有 `serviceNo`。
---
## 4 数据模型
### 4.1 揽收单头 `t_pickup_order`
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| Id | INT PK | 是 | 主键 |
| PickupNo | VARCHAR(32) | 是 | 揽收单号,唯一;建议 `PKyyyyMMddHHmmss####` |
| RequestId | VARCHAR(64) | 否 | 幂等键(对齐 `t_order.RequestId`) |
| CustomerId | INT | 是 | 客户 |
| OrganizationId / SecretLevel | CHAR(36) / TINYINT | 否/是 | 数据权限(对齐 `t_order`) |
| SenderAddressId | INT | 是 | 发件地址簿 Id |
| Sender* 快照 | 对齐 `t_order_sender` | 是/视情况 | Name/Company/Country/CountryCode/省市区邮编/地址1-3/门牌/Phone/TelePhone/Email |
| WarehouseId | INT | 是 | 送到哪仓;关联物流单仓必须一致 |
| ConcurrencyStamp | CHAR(36) | 是 | 并发戳(现表公共字段) |
| CarrierCode | VARCHAR(64) | 否 | 承运商编码;draft 可空 |
| CarrierService | VARCHAR(64) | 否 | 承运服务;draft 可空 |
| CarrierWaybillNo | VARCHAR(64) | 否 | 承运商回执运单号 |
| ExpectPickupTime | DATETIME | 是 | 期望揽收时间 |
| PickupWindowStart / End | DATETIME | 否 | 时间窗(P2) |
| ActualPickupTime | DATETIME | 否 | 实际取件时间 |
| Status | VARCHAR(32) | 是 | 见 §4.4 |
| ParcelCount | INT | 否 | 票数;可由 line 汇总 |
| TotalActualWeight / ChargeableWeight | DECIMAL | 否 | 汇总或人工 |
| Currency | VARCHAR(8) | 是 | 默认 CNY |
| FreightAmount | DECIMAL(18,2) | 否 | 对客揽收运费 |
| OtherFeeAmount | DECIMAL(18,2) | 否 | 对客其他费 |
| ReceivableAmount | DECIMAL(18,2) | 否 | 对客应收合计 |
| PayStatus | VARCHAR(16) | 是 | `unpaid` / `paid` |
| PayTime | DATETIME | 否 | |
| PayMode | VARCHAR(16) | 否 | 默认 `prepaid`;预留 `collect` |
| CarrierPayableAmount | DECIMAL(18,2) | 否 | 应付承运商 |
| SettlementStatus | VARCHAR(16) | 是 | `none` / `pending` / `settled` |
| SettlementBatchNo | VARCHAR(64) | 否 | |
| SettledAt | DATETIME | 否 | |
| Remark | VARCHAR(512) | 否 | |
| IsDeleted | TINYINT | 是 | 软删 |
| CreatedBy / UpdatedBy | | 否 | |
| CreateTime / UpdateTime | DATETIME | 是 | |
索引建议:`UK(PickupNo)`、`IX(CustomerId, Status, CreateTime)`、`IX(WarehouseId, ExpectPickupTime)`、`IX(SettlementStatus)`、`IX(PayStatus)`。
### 4.2 关联行 `t_pickup_order_line`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| Id | INT PK | |
| PickupOrderId / OrderId | INT | FK |
| ServiceNo / ClientNo | VARCHAR | 运单号/客户单号快照 |
| SortNo | INT | 展示序 |
| ConcurrencyStamp + 审计列 | | 对齐现表子表 |
约束:
- `UK(OrderId)`:**一票物流同时只能挂一个有效揽收**(取消关联后可再挂)。
- `UK(PickupOrderId, OrderId)`。
- 应用层保证同揽收下各 `Order.WarehouseId`、取件地址与头表一致。
### 4.3 物流单增量 `t_order`
| 字段 | 说明 |
| --- | --- |
| InboundMethod | `selfShip` \| `doorPickup` \| NULL(历史单) |
| PickupOrderId | 冗余 FK;解绑时清空 |
可保留现有 `PickupTime` / `CustomerSenderAddressId` 作展示缓存,**真源以揽收单为准**;P1 写入时与揽收单同步回写便于旧列表列配置过渡。
### 4.4 状态机
#### 履约 Status
```text
draft 已建单;承运商可空;运费可未确认
scheduled 已预约 / 已下发承运商(须承运已填;默认须已支付)
dispatched 司机已派出(可选,P2)
picked 已取件
inbound 已送达仓库(可与入库扫描联动)
cancelled 已取消
failed 取件失败
```
合法迁移(P1 最小集):
| 从 | 到 | 条件摘要 |
| --- | --- | --- |
| draft | scheduled | 承运已填、运费已确认、PayStatus=paid(PayMode=prepaid) |
| draft / scheduled | cancelled | 未取件 |
| scheduled | picked | 人工或承运回传 |
| picked | inbound | 到仓确认 / 入库联动 |
| scheduled | failed | 取件失败 |
#### 支付 PayStatus(对客)
`unpaid` → `paid`(揽收单维度一笔支付;多票共享不拆单)。
#### 结算 SettlementStatus(对承运)
`none` → `pending`(通常 picked/inbound 后)→ `settled`。
三者正交:例如 `Status=picked, PayStatus=paid, SettlementStatus=pending`。
---
## 5 业务规则
### 5.1 创建 / 追加挂载校验
全部满足方可写入 line:
1. 物流单 `CustomerId` = 揽收单 `CustomerId`
2. 物流单未删除、非不可发货终态(实现按现有订单状态枚举收紧)
3. **未绑定其他有效揽收**(line 唯一;或仅允许从已取消揽收解绑后的单)
4. **`WarehouseId` 与揽收头一致**
5. **取件地址与揽收头一致**(比 `SenderAddressId`,或比快照关键字段)
6. 货尚未在仓:默认允许 `draft / predeclared / pendingAllocation / pendingPayment / pendingShip / pendingOnline` 等;**已 `received` 及之后默认拒绝**
7. **已支付未入库允许**改绑/合并(决策 #2)
8. 揽收头 `Status ∈ {draft, scheduled}` 才允许追加;`picked/inbound/cancelled/failed` 禁止追加
失败时 API 返回明细数组:`{ orderId, serviceNo, reason }`,前端按行展示。
### 5.2 改绑 / 移除
- 原揽收 `draft|scheduled` 且未取件 → 可 `remove-orders` / 移入另一揽收。
- 移除后若 line 数为 0 → 自动将揽收置 `cancelled`(或保持空单由客服取消,P1 建议自动取消并要求 PayStatus 未支付;若已支付走退款策略见 §7.3)。
- `picked|inbound` → 拒绝改绑。
### 5.3 下单页同步建揽收
1. `CreateParcelCore` 成功。
2. `InboundMethod=doorPickup` 时内部创建揽收单(`orderIds=[newId]`)。
3. 承运商可空 → `Status=draft`。
4. 揽收创建失败:**不回滚**物流单;返回物流成功 + 揽收错误码/文案,引导我的订单补建。
### 5.4 费用归属
| 费用 | 记在哪 | 谁付 / 谁收 |
| --- | --- | --- |
| 跨境运费等 | `t_order` / `t_order_fee` receivable | 客户 → 平台(现有) |
| 上门揽收运费 | `t_pickup_order.FreightAmount` 等 | **客户 → 平台** |
| 揽收承运成本 | `t_pickup_order.CarrierPayableAmount` | 平台 → 承运商 |
报价:P1 允许人工录入或简单按重量×单价(配置表可后置);P2 再对接承运商询价。
---
## 6 交互方案
### 6.1 创建物流订单(PC / H5)
在基础信息增加 **到仓方式**:
- **自己寄到仓库**:只读展示仓址;不建揽收。
- **上门揽收**:展开
- 发件地址(地址簿,必填)
- 期望揽收时间(必填)
- 承运服务(选填,可后补)
- 提示:「多票合并取件请到我的订单勾选后点创建揽收」
提交成功页 / Toast:
- 运单号 `ServiceNo`
- 若已建揽收:揽收单号 `PickupNo`(可点详情)
- 若揽收失败:明确「运单已创建,揽收未建成功」+ 去补建入口
### 6.2 我的订单:创建揽收(主合并入口)
- 工具栏按钮 **创建揽收**(需勾选 ≥1 行;权限如 `Order.PickupOrder.Create`)。
- 弹窗:
1. 前端/后端预校验,不合格行红字原因。
2. 合格则填写:发件地址、期望时间、承运(可选)、备注。
3. 只读汇总:票数、仓库、运单号列表、预估重量。
- 成功:`揽收单 PKxxx 已创建,共 N 票` → 可跳转揽收详情。
- 列表列:到仓方式、揽收单号(链接)。
- 已绑定行:操作「查看揽收」;未绑定且可挂:可走创建或(P2)「加入已有揽收」。
### 6.3 物流订单详情
概览增加「到仓 / 揽收」块:
- 到仓方式、揽收单号、承运、期望时间、揽收状态、揽收支付状态
- 操作:查看揽收、去付揽收费(未付时)、改约(未取件)
### 6.4 揽收单管理(PC 菜单)
建议菜单:**订单 → 揽收单**(替换或降级原「揽收订单」页语义)。
| 区域 | 内容 |
| --- | --- |
| 列表 | PickupNo、客户、仓、承运、期望时间、状态、票数、应收、支付、结算 |
| 详情 | 抬头 + 关联物流表(ServiceNo、状态、跳转)+ 费用 + 日志 |
| 操作 | 确认运费、登记支付(或调支付)、补承运、下发、改约、取消、已取件、到仓、录入承运商单号、结算(P2/P3) |
客户门户:只读己方揽收 + 关联运单 + 支付;无结算。
### 6.5 H5
- 下单:同到仓方式。
- 订单详情:展示挂载揽收摘要;「预约上门」对未挂载单。
- 「我的 → 揽收」:改为揽收单列表(非假物流单);点进详情看关联运单。
- 发货指引:禁止再调旧 `pickup/create` 建第二张物流单;改为对本单创建/查看揽收。
### 6.6 冲突文案示例
> 以下运单不能合并:
> · SNxxx — 已绑定揽收单 PKyyy
> · SNzzz — 仓库不一致
> · SNaaa — 已入库
---
## 7 支付与结算
### 7.1 对客支付(揽收运费)
1. 管理端或系统确认 `FreightAmount` / `ReceivableAmount`。
2. 客户对 **`pickupNo`** 发起支付(复用余额支付能力;接口可 `POST /order/pickup-order/pay`)。
3. 成功写 `PayStatus=paid`、`PayTime`。
4. 默认 `PayMode=prepaid`:未支付不能 `scheduled`。
与物流待支付并列展示,避免客户只付跨境费误以为取件已安排。
### 7.2 承运商结算
- 对象:揽收单,不是物流单。
- 流程:取件/到仓后 `SettlementStatus=pending` → 对账录入/导入 → `settled` + `SettlementBatchNo`。
- 二期进入统一应付报表时,来源类型建议 `pickup_carrier`。
### 7.3 取消与退款(P1 口径)
| 场景 | 处理 |
| --- | --- |
| 未支付取消 | 直接 `cancelled` |
| 已支付未取件取消 | P1:标记取消 + 退款待办(人工退余额);P2:自动退回客户余额 |
| 已取件 | 不可取消;纠纷走客服 |
---
## 8 前端要点
遵循 `docs/前端页面设计规范.md`:
- 容器 `xxx-container`、`table-toolbar`、FormPro / TablePro / ModalPro / HandsontablePro。
- 权限建议:`Order.PickupOrder.Query` / `Create` / `Update` / `Pay` / `Settle`(Settle 仅管理端)。
- 路由建议:`/order/pickupOrder` 改为揽收单列表(或新 path `/order/pickupOrders`,旧 path 重定向)。
- 我的订单工具栏「创建揽收」与批量打印等并列,勾选态禁用规则与现有批量操作一致。
---
## 9 接口草案
基础路径前缀建议:`/order/pickup-order`(与旧 `/order/pickup/create` 区分)。
| 接口 | 方法 | 说明 |
| --- | --- | --- |
| 创建揽收单 | POST | `/order/pickup-order/create` |
| 追加物流单 | POST | `/order/pickup-order/add-orders` |
| 移除物流单 | POST | `/order/pickup-order/remove-orders` |
| 分页列表 | POST | `/order/pickup-order/list` |
| 详情 | GET | `/order/pickup-order/detail?id=` |
| 取消 | POST | `/order/pickup-order/cancel` |
| 改约 | POST | `/order/pickup-order/reschedule` |
| 确认运费 | POST | `/order/pickup-order/confirm-freight` |
| 支付 | POST | `/order/pickup-order/pay` |
| 补承运 / 预约下发 | POST | `/order/pickup-order/schedule` |
| 标记已取件 | POST | `/order/pickup-order/mark-picked` |
| 确认到仓 | POST | `/order/pickup-order/mark-inbound` |
| 结算 | POST | `/order/pickup-order/settle`(管理端,P2/P3) |
### 9.1 创建请求示例
```json
{
"senderAddressId": 10,
"warehouseId": 1,
"carrierCode": "",
"carrierService": "",
"expectPickupTime": "2026-08-04 10:00:00",
"orderIds": [1001, 1002, 1003],
"remark": "下午电联"
}
```
### 9.2 创建响应要点
```json
{
"id": 1,
"pickupNo": "PK202608031200000001",
"status": "draft",
"payStatus": "unpaid",
"orderCount": 3,
"rejected": []
}
```
部分失败策略:P1 建议 **全部校验通过才创建**(原子);若需尽最大成功,须产品另批。
### 9.3 小包创建扩展字段
`POST /order/parcel/create`(及下单 UI)增加:
```json
{
"inboundMethod": "doorPickup",
"senderAddressId": 10,
"expectPickupTime": "2026-08-04 10:00:00",
"carrierCode": null,
"carrierService": null
}
```
服务端在物流创建成功后按需调内部 `CreatePickupOrder`。
### 9.4 列表筛选(物流)
`POST /order/list` 增加:`inboundMethod`、`pickupOrderId`、`hasPickup`(bool)。
详情 `OrderOutput` 回显:`inboundMethod`、`pickupOrderId`、`pickupNo`(可 join)、揽收摘要可选嵌套。
---
## 10 权限与隔离
- 客户会话:强制 `CustomerId = effectiveCustomerId`(与 §4.4.0 一致)。
- 管理端代客:须显式 `CustomerId` + 数据权限。
- 关联表不可仅凭 `OrderId` 越权:改绑前校验订单归属与揽收归属一致。
---
## 11 分期与验收
### 11.1 P1 验收清单
- [ ] 表 `t_pickup_order` / `t_pickup_order_line` 及 `t_order` 两字段迁移成功
- [ ] 我的订单多选创建揽收:同仓同址成功;冲突按行提示
- [ ] 下单选上门揽收:物流单 + draft 揽收;承运可空
- [ ] 物流详情可见揽收单号与状态
- [ ] 客户可支付揽收运费;未支付不能 schedule(若 P1 含 schedule)
- [ ] 发货指引不再创建第二张 `OrderSource=pickup` 物流单
- [ ] 旧菜单入口有说明或重定向,不误导新建假送仓单为主路径
### 11.2 P2 / P3
见 §0 分期表;P3 含承运商 API 与结算报表对接。
---
## 12 旧揽收(OrderSource=pickup)处置
| 项 | 口径 |
| --- | --- |
| `POST /order/pickup/create` | 标记废弃;P1 起主路径停用或仅白名单兼容 |
| 存量 `OrderSource=pickup` 单 | 保留流转;可选迁移脚本:生成 `t_pickup_order` + line(收件为仓的历史单需运营确认是否迁移) |
| 文档 `oms系统设计-050801.md` §4.4.4 | 本文生效后以本文为准;旧节加「已由揽收单模型替代」脚注(另改) |
**禁止**:继续用「建一张收件=仓库的物流单」冒充揽收管理与承运结算对象。
---
## 13 测试要点
| 场景 | 期望 |
| --- | --- |
| 3 票同仓同址合并 | 1 个 PickupNo,3 行 line,订单冗余 PickupOrderId |
| 2 票不同仓 | 整单创建失败,原因含仓库不一致 |
| 已绑定再合并 | 拒绝,提示已绑定 PKxxx |
| 已支付未入库改绑 | 允许;原揽收 line=0 则取消 |
| 已取件改绑 | 拒绝 |
| 下单上门 + 揽收内部失败 | 物流单在,错误可补建 |
| 支付揽收费后 schedule | 成功;未支付 schedule 失败 |
| 客户 A 单不可挂进客户 B 揽收 | 鉴权失败 |
---
## 14 SQL 草案文件名
建议后端仓库脚本:
- `sql/20260803_pickup_order.sql` — 建 `t_pickup_order`、`t_pickup_order_line`
- `sql/20260803_order_inbound_pickup_fields.sql` — `t_order` 增加 `InboundMethod`、`PickupOrderId`
(评审通过后由开发落具体 DDL;本文不代替迁移脚本。)
---
## 15 修订记录
| 日期 | 说明 |
| --- | --- |
| 2026-08-03 | 首版:1 揽收 N 物流、双入口、对客付揽收费、draft 可后补承运;决策四条落地 |