简介
本文件面向 DouPHP 的订单控制器及相关服务,系统性说明从购物车到下单、支付、状态流转、售后等完整生命周期。重点覆盖:
- 购物车管理(加购、改数量、删除)
- 结算页数据准备、运费重算、优惠券试算
- 下单事务(库存检查、价格防篡改、地址快照、优惠券核销、分销奖励)
- 收银台(余额+网关混合支付、线下凭证上传、货到付款)
- 订单状态机与支付回调联动
- 可扩展点:新增支付方式、物流服务、订单分析与自动化任务
项目结构
订单相关的前端入口集中在 front/controller/order 下,按职责拆分为:
- OrderController:路由薄入口,默认跳转到购物车
- CartController:购物车页面与操作
- CheckoutController:结算页、提交订单、运费与优惠券交互
- CashierController:收银台、发起支付、线下凭证、货到付款
业务逻辑下沉至 front/service/order 下的 CartService、CheckoutService、CashierService;通用能力由 core/service/order/OrderService 提供门面编排。
graph TB
A["OrderController<br/>路由入口"] --> B["CartController<br/>购物车"]
B --> C["CartService<br/>加购/改数量/删除"]
B --> D["OrderService<br/>购物车视图/清空"]
C --> E["PricingService<br/>定价"]
C --> F["OrderStockGuard<br/>库存校验"]
G["CheckoutController<br/>结算/下单"] --> H["CheckoutService<br/>结算数据/下单事务"]
H --> I["OrderService<br/>订单号/状态/列表"]
H --> J["WalletService<br/>积分扣减"]
H --> K["CouponEligibility<br/>券资格/抵扣"]
L["CashierController<br/>收银台/支付"] --> M["CashierService<br/>余额+网关/离线/货到付款"]
M --> N["PaymentService<br/>支付台账/回调处理"]
M --> O["OrderStatusTransition<br/>状态推进"]
核心组件
- 购物车控制器与服务:负责用户加购、规格属性校验、库存检查、数量更新、删除条目,并驱动"立即购买"或"进入结算"的跳转策略。
- 结算控制器与服务:负责结算页数据装配(购物车、配送方式、优惠券)、运费重算、优惠券试算、下单事务(库存二次校验、价格防篡改、地址快照、优惠券核销、分销奖励)。
- 收银台控制器与服务:负责收银台展示、发起支付(含余额+网关混合支付)、线下付款凭证上传、货到付款流程。
- 订单核心服务:统一对外暴露购物车、状态机、库存、定时任务、结算选项等能力,屏蔽内部子服务细节。
架构总览
订单链路采用"控制器薄 + 服务厚"的分层设计:
- 控制器仅做参数收集、鉴权、响应格式化
- 服务封装领域规则(库存、价格、优惠券、钱包、状态机)
- 核心服务作为门面聚合各子服务,保证对外接口稳定
sequenceDiagram
participant U as "用户"
participant CC as "CartController"
participant CS as "CartService"
participant OS as "OrderService"
participant PS as "PricingService"
participant SG as "OrderStockGuard"
U->>CC : 加入购物车
CC->>CS : addToCart(userId,module,item_id,number,mode,action,atts)
CS->>SG : checkStock(module,item_id,number)
SG-->>CS : 库存结果
CS->>PS : salePrice(...)
PS-->>CS : 售价信息
CS->>OS : clearCart()/getCart()
CS-->>CC : 结果(mode/module/action)
CC-->>U : 跳转(购物车/结算)
详细组件分析
购物车模块(CartController + CartService)
- 登录校验:未登录时 AJAX 返回 401,普通请求跳转登录
- 加购流程:
- 解析 module/itemid/number/mode/action/att* 参数
- 跨模块或积分兑换时清空购物车
- 非商品或立即购买/一键购买时清空购物车
- 规格属性校验(attribute_value 归属校验)
- 库存检查(不可超卖)
- 写入 order_cart(合并数量或新增)
- 根据 mode/module/action 决定跳转(积分→结算;商品→购物车或结算)
- 更新数量:
- 仅商品且非一键购买时允许
- 实时库存回退
- 重新计算小计与合计
- 删除条目:按 user_id 安全删除
flowchart TD
Start(["开始"]) --> Login["登录校验"]
Login --> |未登录| Redirect["跳转登录或返回401"]
Login --> |已登录| Parse["解析参数<br/>module/item_id/number/mode/action/atts"]
Parse --> ClearRule{"是否跨模块/积分/立即购买?"}
ClearRule --> |是| ClearCart["清空购物车"]
ClearRule --> |否| Next1["继续"]
ClearCart --> Next1
Next1 --> Attr["规格属性校验"]
Attr --> Stock["库存检查"]
Stock --> |失败| Err["返回错误"]
Stock --> |通过| Upsert["写入/合并购物车行"]
Upsert --> Jump{"跳转策略"}
Jump --> |积分| ToCheckout["跳转结算"]
Jump --> |商品| ToCartOrCheckout["跳转购物车或结算"]
Jump --> |其他| ToCheckout
ToCheckout --> End(["结束"])
ToCartOrCheckout --> End
Err --> End
结算模块(CheckoutController + CheckoutService)
- 结算页数据:
- 获取购物车视图
- 加载配送插件配置,计算运费(免邮阈值/积分模式/非商品)
- 初始化会话中的运费与优惠券金额
- 加载会员默认收货地址快照
- 加载可用优惠券列表
- 运费重算:根据选择的配送方式 slug 动态计算运费与订单总额
- 优惠券试算:
- 单券模式,校验归属与可用性
- 计算抵扣金额(百分比封顶、生效范围限制)
- 写回会话供前端展示
- 下单事务(createOrder):
- 再次库存校验
- 价格防篡改:基于定价服务与属性加价重算,与购物车缓存对比
- 生成订单号
- 事务内:
- 积分兑换:先扣积分
- 优惠券核销:标记 coupon_log 为已用,写入 order_coupon
- 运费计算与减免
- 写入 order、order_item、order_address 快照
- 可选同步更新会员默认收货地址
- 0元订单直接推进到 PAID
- 清理购物车
- 异常回滚并记录日志
更新 输入验证逻辑重构,引入中间变量提升安全性和可维护性
sequenceDiagram
participant UC as "用户"
participant CO as "CheckoutController"
participant COS as "CheckoutService"
participant OS as "OrderService"
participant WS as "WalletService"
participant CE as "CouponEligibility"
UC->>CO : 提交订单(contact_id,shipping_id,mode,coupon_id,...)
CO->>COS : createOrder(userId,form)
COS->>OS : getCart()/checkStock()
COS->>WS : createPoint(-order_point) // 积分模式
COS->>CE : canUse(coupon, lines)
CE-->>COS : 可用/不可用
COS->>COS : 计算抵扣/写coupon_log/order_coupon
COS->>COS : 计算运费/订单金额
COS->>OS : createOrderSn()
COS->>COS : DB事务(写入order/order_item/order_address)
alt 0元或积分
COS->>OS : changeStatus(PAID)
end
COS->>OS : clearCart()
COS-->>CO : {ok, order_sn, order_amount, mode}
CO-->>UC : 跳转(订单详情/收银台/我的订单)
收银台模块(CashierController + CashierService)
- 收银台展示:
- 校验待付款订单归属
- 计算余额+网关混合支付预览(可抵扣金额、网关剩余)
- 渲染支付方式列表
- 发起支付(charge):
- 校验 pay_id 合法性与订单归属
- 若勾选使用余额:调用混合支付,可能直接收尾或转入网关支付
- 创建 dou_order_payment(status=pending),设置过期时间
- 写订单 pay_id
- 调起支付插件 provider.start(),返回第三方 HTML 给浏览器
- 线下凭证上传:
- 校验订单归属
- 存储附件并关联 payment_sn
- 将订单状态推进到 awaiting_confirmation
- 货到付款:
- 立即标记支付成功(内部会推进状态)
- 写 pay_id/paid_at 审计字段
sequenceDiagram
participant U as "用户"
participant CA as "CashierController"
participant CAS as "CashierService"
participant PS as "PaymentService"
participant PR as "PluginRegistry"
participant ST as "OrderStatusTransition"
U->>CA : POST charge(order_sn,pay_id,use_wallet?)
CA->>CAS : getPayOrder()/previewWalletSplit()
alt 使用余额
CA->>CAS : payWithWalletAndGateway()
CAS->>PS : markSucceededByWallet(order_sn,wallet)
alt 余额已付满
CAS->>ST : changeStatus(PAID/COMPLETED)
CAS-->>CA : finalized=true
CA-->>U : 跳转订单详情
else 仍需网关
CAS-->>CA : gateway_amount
CA->>PS : createForOrder(...,gateway_amount)
CA->>PR : provider.start()
CA-->>U : 返回第三方支付HTML
end
else 纯网关
CA->>PS : createForOrder(...,order_amount)
CA->>PR : provider.start()
CA-->>U : 返回第三方支付HTML
end
订单状态流转与支付回调
- 下单后初始状态:PENDING
- 0元订单(积分兑换或优惠券打到0):直接推进到 PAID
- 支付成功后:
- 在线支付:由支付回调触发 PaymentService 标记成功,内部调用状态机推进
- 货到付款:CashierService.submitCod 标记成功并推进状态
- 线下凭证:保存凭证后推进到 AWAITING_CONFIRMATION,等待后台确认
- 完成态:无物流插件或非商品模块时,PAID 顺势推进到 COMPLETED
stateDiagram-v2
[*] --> PENDING : "下单成功"
PENDING --> PAID : "支付成功/0元订单"
PENDING --> AWAITING_CONFIRMATION : "线下凭证上传"
PAID --> COMPLETED : "无物流/非商品"
AWAITING_CONFIRMATION --> PAID : "后台确认"
PAID --> COMPLETED : "发货完成"
依赖关系分析
- 控制器依赖服务:
- CartController → CartService、OrderService
- CheckoutController → CheckoutService、OrderService
- CashierController → CashierService、OrderService、PaymentService
- 服务间协作:
- CartService 依赖 PricingService、OrderStockGuard、OrderService
- CheckoutService 依赖 WalletService、PricingService、CouponEligibility、OrderService
- CashierService 依赖 PaymentService、OrderStatusTransition、UserStatsService、OrderService
- 外部集成点:
- 支付方式插件:通过 Module::make('plugin.registry_aggregator') 获取 provider
- 配送方式插件:通过 plugin()->valueByGroup('shipping', 'config') 获取配置
- 优惠券模块:可选注入 CouponEligibility,未安装时为 null
graph LR
CC["CartController"] --> CS["CartService"]
CC --> OS["OrderService"]
CS --> PS["PricingService"]
CS --> SG["OrderStockGuard"]
CO["CheckoutController"] --> COS["CheckoutService"]
CO --> OS
COS --> WS["WalletService"]
COS --> CE["CouponEligibility"]
CA["CashierController"] --> CAS["CashierService"]
CA --> OS
CA --> PM["PaymentService"]
CAS --> OST["OrderStatusTransition"]
性能与并发考虑
- 库存检查:
- 加购与下单均调用 OrderStockGuard.checkStock,避免超卖
- 实时更新购物车数量时,若库存不足则回退到可售数量
- 价格防篡改:
- 下单前基于 PricingService 与属性加价重新计算最终价,与购物车缓存比对,差异过大拒绝下单
- 事务保护:
- 下单事务包含积分扣减、优惠券核销、订单/明细/地址写入,异常回滚并记录日志
- 会话缓存:
- 运费与优惠券金额暂存 Session,减少重复计算
- 支付超时:
- 支付记录设置过期时间,结合定时任务自动取消未付款订单
故障排查指南
- 登录态丢失:
- 控制器 assertLogin 对 AJAX 返回 401,普通请求跳转登录
- 购物车为空:
- 结算页 getCheckoutData 返回空时提示并返回首页
- 库存不足:
- addtocart/updateCartItem/checkStock 返回 stock_error,需调整数量或等待补货
- 价格被篡改:
- createOrder 中价格重算不一致返回 price_changed,需刷新页面重试
- 优惠券无效:
- applyCoupon 中资格判定不通过,返回 0 抵扣;检查 coupon_log 归属与状态
- 支付失败:
- charge 中 pay_id 非法或插件不可用,返回收银台;检查插件注册与配置
- 线下凭证上传失败:
- 附件存储抛出 DomainException,返回错误消息并停留在收银台
- 下单异常:
- 事务捕获异常并回滚,记录详细日志(通道、模块、负载、异常堆栈)
更新 输入验证改进后,非法的 shipping_id 和 slug 参数将被安全过滤,减少潜在的安全风险
结论
DouPHP 订单控制器以清晰的分层与职责划分,实现了从购物车到下单、支付、状态流转的完整闭环。通过服务化封装与核心门面编排,既保证了业务规则的集中治理,又提供了良好的扩展性。最近的输入验证重构进一步提升了系统的安全性和可维护性。建议在扩展新功能时遵循:
- 控制器保持薄,业务规则下沉到服务
- 关键路径使用事务与幂等设计
- 严格进行库存与价格校验
- 通过插件机制扩展支付与物流
- 利用状态机推进订单生命周期
- 采用安全的输入验证模式,如中间变量和正则表达式过滤
附录:扩展指引
- 添加新的支付方式:
- 实现支付插件 provider,并通过插件注册器暴露
- 在收银台 charge 中通过 pay_id 调起 provider.start()
- 确保回调能正确标记支付成功并推进状态
- 参考:CashierController.charge:127-234
- 集成物流服务:
- 在配送插件中提供 config(fee/free)与查询接口
- 结算页通过 plugin()->valueByGroup('shipping','config') 读取配置
- 参考:CheckoutService.getCheckoutData:92-106
- 实现订单分析:
- 基于 order、order_item、order_address、order_coupon 等表统计
- 结合 OrderItemQuery 与 OrderScheduledTasks 提供的能力
- 参考:OrderService.getOrderItem:183-193
- 扩展优惠券策略:
- 自定义 CouponEligibility 实现,控制 canUse 与 eligibleAmount
- 在 CheckoutService.applyCoupon 中复用
- 参考:CheckoutService.applyCoupon:170-211
更新 输入验证最佳实践示例:
- 使用中间变量存储原始值和验证后的值
- 应用严格的正则表达式验证
- 对非法输入提供安全的默认值