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

21 KiB
Raw Permalink Blame History

揽收单与物流单串联方案

文档状态:产品 + 架构 + 表结构 + 接口(先评审,再开发)
编写日期: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 总览

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

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 创建请求示例

{
  "senderAddressId": 10,
  "warehouseId": 1,
  "carrierCode": "",
  "carrierService": "",
  "expectPickupTime": "2026-08-04 10:00:00",
  "orderIds": [1001, 1002, 1003],
  "remark": "下午电联"
}

9.2 创建响应要点

{
  "id": 1,
  "pickupNo": "PK202608031200000001",
  "status": "draft",
  "payStatus": "unpaid",
  "orderCount": 3,
  "rejected": []
}

部分失败策略:P1 建议 全部校验通过才创建(原子);若需尽最大成功,须产品另批。

9.3 小包创建扩展字段

POST /order/parcel/create(及下单 UI)增加:

{
  "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 可后补承运;决策四条落地