简介
本文件面向DouPHP的物流插件系统,聚焦“配送方式插件”的设计与实现。当前仓库已提供两类物流插件示例:快递(express)与邮政EMS(ems)。它们通过统一的Shipping插件接口暴露配送方式、费用与描述,并在订单结算时参与运费计算;在后台可录入物流公司与运单号,首次录入时会推进订单状态并解锁库存。本文基于仓库现有代码,梳理插件抽象、服务层、订单状态机与后台流程,并给出扩展主流物流公司(如快递100、顺丰等)的对接思路与最佳实践建议。
项目结构
- 插件目录 plugin 下以“插件ID”组织,每个插件包含 Provider、Service 与 manifest 声明。
- 订单模块位于 _'module/order,包含后台服务、模型、请求校验与状态机等。
- API 层提供结账与运费重算能力,供前端或小程序调用。
graph TB
subgraph "插件层"
EProv["ExpressProvider"]
ESvc["ExpressService"]
EMProv["EmsProvider"]
EMSvc["EmsService"]
MExp["express/manifest.php"]
end
subgraph "订单与状态"
OReq["OrderTrackingFormRequest"]
OSvc["OrderService"]
OSt["OrderStatusTransition"]
end
subgraph "API"
Ctl["CheckoutController"]
end
EProv --> ESvc
EMProv --> EMSvc
MExp --> EProv
Ctl --> ESvc
Ctl --> EMSvc
OReq --> OSvc
OSvc --> OSt
核心组件
- 配送插件提供者(Provider):对外暴露插件元数据(名称、描述、版本、分组、客户端限制)与配置项定义,并提供 methods() 返回可用配送方式列表。
- 配送业务服务(Service):读取插件配置,组装配送方式(id、name、price、desc),供结算与展示使用。
- 订单物流录入与状态推进:后台表单校验后,保存 shipping_id、tracking_no、shipped_at;首次填写时推进订单状态并解锁库存。
- 订单状态机:在无物流插件或非商品模块场景下,付款后可直接推进到完成;否则遵循状态机规则。
- 结账与运费重算:API 根据所选配送方式与优惠券重新计算运费与订单金额。
架构总览
下图展示了从“选择配送方式”到“结算计费”,再到“后台发货录入”的端到端流程。
sequenceDiagram
participant Client as "客户端"
participant API as "CheckoutController"
participant SExp as "ExpressService"
participant SEms as "EmsService"
participant Admin as "后台"
participant OrderSvc as "OrderService"
participant State as "OrderStatusTransition"
Client->>API : "获取结账信息/重算运费"
API->>SExp : "methods()"
API->>SEms : "methods()"
SExp-->>API : "配送方式(含费用)"
SEms-->>API : "配送方式(含费用)"
API-->>Client : "shipping_list, amount"
Admin->>OrderSvc : "提交物流(shipping_id, tracking_no)"
OrderSvc->>State : "changeStatus(COMPLETED)"
State-->>OrderSvc : "状态变更成功"
OrderSvc-->>Admin : "跳转详情页"
详细组件分析
配送插件提供者与服务
- ExpressProvider/EmsProvider:实现统一接口,声明插件分组为 shipping,提供配置字段 fee(配送费)、free(满额包邮阈值),并通过 Service 暴露 methods()。
- ExpressService/EmsService:读取插件配置,返回固定的一条配送方式,包含 id、name、price、desc。
classDiagram
class ExpressProvider {
+pluginId() string
+meta() array
+methods() array
}
class ExpressService {
+methods() array
}
class EmsProvider {
+pluginId() string
+meta() array
+methods() array
}
class EmsService {
+methods() array
}
ExpressProvider --> ExpressService : "委托"
EmsProvider --> EmsService : "委托"
订单物流录入与状态推进
- 后台表单校验:仅允许 order_id、shipping_id、tracking_no 等字段,长度与类型受控。
- 保存与推进:若订单已有运单号则更新物流信息;否则首次录入时推进订单至完成并解锁库存。
flowchart TD
Start(["开始"]) --> Validate["校验输入(order_id/shipping_id/tracking_no)"]
Validate --> Exists{"订单存在?"}
Exists -- 否 --> Err["抛出非法参数异常"]
Exists -- 是 --> First{"是否已有运单号?"}
First -- 否 --> Push["推进订单状态为完成<br/>写入shipping_id/tracking_no/shipped_at<br/>解锁库存"]
First -- 是 --> Update["更新物流信息与发货时间"]
Push --> End(["结束"])
Update --> End
订单状态机与无物流插件场景
- 当没有启用物流插件或非商品模块时,付款后会直接推进到完成,避免阻塞交易闭环。
- 状态变更具备合法性校验,非法转换会被拒绝并记录错误日志。
结账与运费重算
- 结账接口返回 shipping_list(由插件 methods() 聚合),并支持按所选配送方式与优惠券重算运费与订单金额。
- 前端/小程序据此展示运费与最终应付金额。
依赖关系分析
- 插件发现与注册:通过 manifest.php 声明插件分组与 Provider 类名,框架扫描并实例化 Provider。
- 插件分组:两个示例插件均声明为 shipping 分组,便于统一管理与筛选。
- 服务解耦:Provider 仅负责元数据与配置,具体逻辑下沉到 Service,便于测试与扩展。
graph LR
Manifest["express/manifest.php"] --> Registry["ConnectPluginRegistry"]
Registry --> Provider["ExpressProvider"]
Provider --> Service["ExpressService"]
性能与可靠性
- 缓存策略
- 插件配置与方法列表建议在应用启动或配置变更时缓存,减少重复解析与数据库查询。
- 对第三方物流查询结果可按“运单号+公司”做短期缓存,降低外部接口压力。
- 重试与降级
- 对第三方接口调用增加指数退避重试与熔断降级,失败时回退到本地缓存或提示用户稍后重试。
- 并发与一致性
- 订单状态变更与库存解锁在同一事务中执行,确保一致性。
- 可扩展性
- 新增物流公司只需新增插件目录、manifest、Provider 与 Service,并在 methods() 中返回对应方式与价格。
故障排查指南
- 后台录入物流无效
- 检查表单字段是否符合校验规则(order_id、shipping_id、tracking_no)。
- 确认订单是否存在且处于可推进状态。
- 运费未生效
- 核对插件配置中的 fee 与 free 是否设置正确。
- 确认结账接口是否正确传入 shipping_id 与 coupon_id。
- 状态机拒绝
- 查看状态转换日志,确认 from/to 状态是否合法。
- 在无物流插件或非商品模块时,付款会直接完成,属预期行为。
结论
DouPHP 的物流插件体系以 Provider/Service 模式解耦了“配送方式与计费”和“订单状态流转”。当前仓库提供了 express 与 ems 两个示例插件,覆盖结算计费与后台发货的核心路径。对于主流物流公司的API对接(如快递100、顺丰等),可在 Service 层封装SDK调用、认证签名、响应解析与缓存策略,并通过 Provider 暴露新的配送方式与价格规则。结合状态机与事务保障,可实现稳定可靠的物流跟踪与订单生命周期管理。
附录:开发示例与最佳实践
新增物流公司插件(以“快递100”为例)
- 创建目录 plugin/kd100,包含:
- manifest.php:声明 plugin_group=shipping、provider=Kd100Provider。
- Kd100Provider.php:实现 ShippingPluginProviderInterface,返回 meta 与 methods。
- Kd100Service.php:读取配置,返回配送方式(可动态计算运费或按重量/体积规则)。
- 在 Kd100Service::methods() 中返回 id/name/price/desc,供结账接口使用。
- 如需在线查询轨迹,可在 Service 内封装 SDK 调用,并对结果进行标准化映射。
接口认证与请求封装
- 认证:将密钥、签名算法等放入插件配置,由 Service 在构造或初始化时加载。
- 请求封装:统一封装 HTTP 客户端、重试、超时、签名生成与响应解码。
- 响应解析:将第三方响应转换为内部标准结构(状态、节点时间、地点、备注)。
数据映射与缓存策略
- 数据映射:建立“第三方状态码 -> 内部状态”的映射表,保证前端展示一致。
- 缓存策略:
- 短时效缓存:按运单号+公司缓存最近一次查询结果,TTL 建议 1-5 分钟。
- 长时效缓存:插件配置与方法列表在配置变更后失效重建。
- 去抖:同一运单号在短时间内合并查询,避免重复请求。
多公司支持与费用计算
- 多公司:在 methods() 中返回多个配送方式(例如“标准快递”“次日达”),前端可选择。
- 费用计算:
- 固定费用:直接读取配置的 fee。
- 条件包邮:根据购物车金额与 free 阈值判断是否免邮。
- 复杂计费:可在 Service 中接入重量/体积/目的地规则引擎。
地址校验与异常处理
- 地址校验:在下单前对收件人地址进行格式校验(省市区、邮编、手机号)。
- 异常处理:
- 网络异常:重试+降级(返回缓存或提示稍后重试)。
- 业务异常:记录错误上下文(订单号、运单号、公司、错误码),便于追踪。
- 状态机异常:非法状态转换时记录日志并拒绝操作。
性能优化建议
- 批量查询:对多运单号采用批量接口减少请求次数。
- 异步拉取:后台定时任务增量拉取轨迹,避免阻塞主流程。
- 索引优化:为运单号、物流公司、订单号建立合适索引,提升查询效率。