电商结算核心是订单状态精确控制,需用Symfony/Workflow定义cart→confirmed→payment_pending→paid→shipped等状态及guard校验,库存扣减须行级锁,支付回调要幂等且异步处理,关键操作必须包裹Doctrine事务。

Symfony 本身不提供开箱即用的“电商结算流程”,必须手动组合组件、控制状态流转、严防并发与数据不一致——这是绝大多数失败项目的根源。
如何设计 Order 实体与状态机(state machine)
电商结算的核心不是支付,而是订单状态的精确控制。直接用布尔字段(is_paid、is_shipped)会导致状态爆炸和非法跃迁(比如跳过库存锁定直接发货)。
- 用
Symfony/Workflow定义明确状态:cart→confirmed→payment_pending→paid→shipped,每个 transition 绑定 guard 表达式(如is_stock_available()) - Order 实体中避免冗余字段,只存
state字符串和必要快照(如当时商品价格、库存版本号),不要在paid_at字段上做业务判断 - Workflow 配置必须启用
supports并指定实体类,否则$workflow->can($order, 'pay')永远返回 false
如何安全处理库存扣减与支付回调竞争
用户点击“支付”和支付平台异步回调几乎同时到达时,若不加控制,会重复扣减库存或重复确认订单。
- 库存扣减必须走数据库行级锁:
SELECT * FROM product_stock WHERE id = ? FOR UPDATE,不能靠应用层缓存计数 - 支付回调入口(如
/webhook/stripe)必须幂等:用支付平台传回的payment_intent.id做唯一索引,重复请求直接返回 200,不做二次处理 - 不要在回调中同步调用发货接口;改用
MessageBus发送PaymentConfirmedEvent,由独立 handler 异步执行后续动作
如何集成 Stripe / PayPal 而不耦合业务逻辑
把支付网关当“黑盒”,只关心它能否返回确定结果,不信任其任何中间状态。
- 前端调用
stripe.confirmPayment()前,先通过 Symfony API 创建一个PaymentIntent并关联到当前 Order,保存其client_secret和status(初始为requires_payment_method) - 后端接收 Stripe webhook 时,只验证签名 + 查询
PaymentIntent状态是否为succeeded,然后触发 workflow transition:$workflow->apply($order, 'confirm_payment') - PayPal 的
ORDER_CAPTURED事件同理,但注意其purchase_units[0].payments.captures[0].id才是真实资金流水 ID,别误用 order_id
为什么不能跳过 Doctrine 事务直接写 SQL
看似简单的“扣库存+改订单状态+记支付日志”三步,在高并发下极易出现部分成功。Doctrine 的 EntityManager::flush() 不等于事务提交,只是同步变更到数据库。
- 所有结算关键步骤必须包裹在
$entityManager->transactional()中,否则flush()失败后实体状态已脏,再试可能重复提交 - 不要在事务里调用外部 HTTP 请求(如调 PayPal API),会拖长锁持有时间;应拆成两阶段:先落库生成待支付订单,再由命令行任务轮询支付状态
- MySQL 默认隔离级别
REPEATABLE READ下,SELECT ... FOR UPDATE可能锁住间隙(gap lock),测试时务必用真实并发压测,而非单线程模拟
真正难的不是写完支付回调,而是当用户投诉“我付了两次钱”时,你能 5 分钟内从日志、数据库快照、消息队列消费记录里定位出是幂等键失效、还是 workflow guard 没生效、或是前端重复提交未拦截。这些点,文档不会告诉你,只有线上事故会。


















