解耦与提效:券订单系统重构全方案(策略+工厂+管道)
背景:从“业务堆砌”到“架构解耦”的重构之路
在餐饮O2O场景中,订单系统是业务核心,而“券”(产品兑换券、代金券)作为引流和转化的关键工具,往往需要对接多平台(抖音、美团、高德等)、支持多订单模式(无支付产品券、需支付普通订单)。但随着业务迭代,很多系统会陷入“堆砌式开发”的困境——我所在的项目也不例外。
原有系统的4大核心痛点
- 逻辑强耦合:券的校验、核销、状态修改逻辑与订单流程深度绑定在
service层,代码臃肿(单函数千行+),新增券类型(如高德券)需修改原有订单核心代码,风险极高; - 流程混乱:产品券订单(无支付)和普通支付订单(有支付)共用部分代码,券校验、核销步骤重复执行,边界模糊,排查问题时需跨多层追踪;
- 事务一致性难保障:订单创建、券状态修改、储值扣减强绑定在同一个事务中,一旦涉及外部平台(如抖音券核销接口)调用,极易导致事务超时或数据不一致;
- 扩展性差:新增多平台券(如高德代金券、支付宝兑换券)时,需在订单流程中硬编码判断逻辑,违反“开闭原则”,维护成本指数级增长。
为解决这些问题,我们启动了订单系统的重构项目,核心目标是:解耦券逻辑与订单流程、统一流程编排规范、保障事务一致性、提升系统扩展性。经过多轮技术选型和方案论证,最终形成了“策略模式+管道模式+事务策略”的全栈解决方案,下文将详细拆解实现思路与完整代码。
一、整体架构设计
1. 架构分层(清晰职责边界)
| 分层 | 核心职责 | 关键组件/接口 |
|---|---|---|
| API层 | 接收请求、参数校验、返回响应 | CreateCouponOrder(c *gin.Context)、CreatePayOrder(c *gin.Context)、PayCallback(c *gin.Context) |
| Service层 | 业务流程编排(管道模式)、事务协调、跨领域调用 | 管道(Pipeline)、步骤(Step)、流程上下文(Context) |
| Domain层 | 券核心逻辑封装(策略模式+工厂模式),不依赖外部层 | 券校验器(CouponValidator)、券操作器(CouponRedeemer)、券信息模型(CouponInfo) |
| Model层 | 纯数据持久化操作,拆分核心订单/券/储值接口,支持外部事务传入 | CreateCoreOrderWithTx(核心订单)、CouponModel(券操作)、StoreModel(储值) |
| Enum/Define层 | 枚举定义(订单状态、券类型、支付类型)、请求/响应结构体定义 | enum.OrderStatus、coupon_define.CouponValidateReq、orders_define.CreateOrderReq |
2. 核心设计模式组合
- 策略模式+工厂模式:封装不同类型券的校验/锁定/核销逻辑(美团、抖音、高德等),支持灵活扩展;
- 管道模式+上下文:编排业务流程(如“券校验→抵扣计算→创建订单→券操作”),步骤解耦、可维护;
- 事务策略模式:按订单类型(产品券/普通支付)选择事务方案,平衡“解耦”与“一致性”。
二、核心组件实现(完整代码)
1. 基础定义(Enum/Define)
1.1 枚举定义(enum/order.go、enum/coupon.go)
package enum
// 订单状态
type OrderStatus int
const (
WaitPay OrderStatus = 1 // 待支付
PaySuccess OrderStatus = 2 // 支付成功
PaySuccessButCouponFail OrderStatus = 3 // 支付成功但券核销失败
Canceled OrderStatus = 4 // 已取消
)
// 券用户关联状态
type CouponUserRelationStatus int
const (
StatusAvailable CouponUserRelationStatus = 1 // 可用
StatusLocked CouponUserRelationStatus = 2 // 锁定
StatusUsed CouponUserRelationStatus = 3 // 已用
)
// 券类型
type CouponType int
const (
MeiTuanExchangeCoupon CouponType = 1 // 美团兑换券(产品券)
MeiTuanCashCoupon CouponType = 2 // 美团代金券(支付券)
DouYinExchangeCoupon CouponType = 3 // 抖音兑换券
AmapCashCoupon CouponType = 4 // 高德代金券
)
// 订单类型
type OrderType int
const (
CouponOrder OrderType = 1 // 产品券订单
PayOrder OrderType = 2 // 普通支付订单
)
1.2 请求/响应结构体(define/coupon_define.go、define/orders_define.go)
package coupon_define
// 券校验请求
type CouponValidateReq struct {
CouponCode string `json:"coupon_code"`
CouponType enum.CouponType `json:"coupon_type"`
CustomerID int64 `json:"customer_id"`
ShopID int64 `json:"shop_id"`
OrderType enum.OrderType `json:"order_type"`
OrderAmount int64 `json:"order_amount,omitempty"` // 支付订单需传入订单金额
}
// 券校验响应(内部使用)
type CouponValidateResp struct {
CouponCode string `json:"coupon_code"`
CouponType enum.CouponType `json:"coupon_type"`
CustomerID int64 `json:"customer_id"`
ShopID int64 `json:"shop_id"`
ParValue int64 `json:"par_value"` // 券面值
}
// 产品券订单创建请求
type CreateCouponOrderReq struct {
CouponProductCode []CouponProduct `json:"coupon_product_code"`
CustomerID int64 `json:"customer_id"`
ShopID int64 `json:"shop_id"`
Platform int `json:"platform"`
DineWay int `json:"dine_way"`
Contact string `json:"contact"`
Note string `json:"note"`
}
type CouponProduct struct {
CouponCode string `json:"coupon_code"`
CouponType enum.CouponType `json:"coupon_type"`
}
// 产品券订单创建响应
type CreateCouponOrderResp struct {
OrderNo []string `json:"order_no"`
}
package orders_define
// 核心订单创建请求(传给Model层)
type CreateOrderReq struct {
OrderNo string `json:"order_no"`
ShopID int64 `json:"shop_id"`
CustomerID int64 `json:"customer_id"`
Platform int `json:"platform"`
DineWay int `json:"dine_way"`
PayType int `json:"pay_type"`
CouponType enum.CouponType `json:"coupon_type,omitempty"`
CouponCode string `json:"coupon_code,omitempty"`
CouponAmount int64 `json:"coupon_amount,omitempty"` // 券抵扣金额
TotalAmount int64 `json:"total_amount"` // 最终支付金额
OrderState enum.OrderStatus `json:"order_state"`
Contact string `json:"contact"`
Note string `json:"note"`
}
// 核心订单创建响应
type CreateOrderResp struct {
OrderNo string `json:"order_no"`
OrderID int64 `json:"order_id"`
State enum.OrderStatus `json:"state"`
}
// 普通支付订单创建请求
type CreatePayOrderReq struct {
CustomerID int64 `json:"customer_id"`
ShopID int64 `json:"shop_id"`
Platform int `json:"platform"`
DineWay int `json:"dine_way"`
PayType int `json:"pay_type"`
CouponCode string `json:"coupon_code,omitempty"`
CouponType enum.CouponType `json:"coupon_type,omitempty"`
Contact string `json:"contact"`
Note string `json:"note"`
}
// 普通支付订单创建响应
type CreatePayOrderResp struct {
OrderNo string `json:"order_no"`
OrderID int64 `json:"order_id"`
State enum.OrderStatus `json:"state"`
PayAmount int64 `json:"pay_amount"`
}
// 支付回调请求
type PayCallbackReq struct {
OrderNo string `json:"order_no"`
PayStatus int `json:"pay_status"` // 1-支付成功
PayAmount int64 `json:"pay_amount"`
Sign string `json:"sign"`
}
2. Domain层:券核心逻辑封装(策略+工厂)
2.1 券核心接口(coupon_domain/coupon.go)
package coupon_domain
import (
"context"
"your-project/enum"
"your-project/define/coupon_define"
"your-project/models"
)
// 券信息模型(Domain层内部传递)
type CouponInfo struct {
CouponCode string `json:"coupon_code"`
CouponType enum.CouponType `json:"coupon_type"`
CustomerID int64 `json:"customer_id"`
ShopID int64 `json:"shop_id"`
ParValue int64 `json:"par_value"` // 券面值
}
// 券校验器接口
type CouponValidator interface {
Name() string
Validate(ctx context.Context, req coupon_define.CouponValidateReq) (*CouponInfo, error)
}
// 券操作器接口(锁定/核销/解锁)
type CouponOperator interface {
Name() string
Lock(ctx context.Context, orderNo string, coupon *CouponInfo, tx *gorm.DB) error // 锁定券(待支付订单)
Redeem(ctx context.Context, orderNo string, coupon *CouponInfo, shop models.Shops, tx *gorm.DB) error // 核销券
Unlock(ctx context.Context, couponCode string, customerID int64) error // 解锁券(支付失败/超时)
}
2.2 具体券实现(以美团为例,coupon_domain/mei_tuan_coupon.go)
package coupon_domain
import (
"context"
"fmt"
"strings"
"time"
"your-project/enum"
"your-project/define/coupon_define"
"your-project/models"
"your-project/utils"
"go.uber.org/zap"
)
// 美团兑换券(产品券)
type MeiTuanExchangeCoupon struct {
couponModel *models.CouponModel
}
func NewMeiTuanExchangeCoupon() (CouponValidator, CouponOperator) {
return &MeiTuanExchangeCoupon{
couponModel: &models.CouponModel{},
}, &MeiTuanExchangeCoupon{
couponModel: &models.CouponModel{},
}
}
func (m *MeiTuanExchangeCoupon) Name() string {
return "美团兑换券"
}
// Validate 校验美团兑换券(产品券:无需订单金额,仅校验有效性)
func (m *MeiTuanExchangeCoupon) Validate(ctx context.Context, req coupon_define.CouponValidateReq) (*CouponInfo, error) {
// 1. 基础校验(券码非空、商户匹配)
if req.CouponCode == "" {
return nil, fmt.Errorf("券码不能为空")
}
if req.ShopID <= 0 {
return nil, fmt.Errorf("商户ID无效")
}
// 2. 调用美团开放平台校验接口(模拟)
meiTuanValid, parValue, err := m.callMeiTuanValidateAPI(ctx, req.CouponCode, req.ShopID)
if err != nil {
return nil, fmt.Errorf("美团平台校验失败:%w", err)
}
if !meiTuanValid {
return nil, fmt.Errorf("券码无效或已使用")
}
// 3. 校验本地关联关系(用户是否有权使用)
valid, err := m.couponModel.CheckCouponUserRelation(ctx, req.CustomerID, req.CouponCode)
if err != nil {
return nil, fmt.Errorf("本地券关联校验失败:%w", err)
}
if !valid {
return nil, fmt.Errorf("用户无此券使用权限")
}
return &CouponInfo{
CouponCode: req.CouponCode,
CouponType: req.CouponType,
CustomerID: req.CustomerID,
ShopID: req.ShopID,
ParValue: parValue,
}, nil
}
// Lock 美团兑换券无需锁定(产品券直接核销)
func (m *MeiTuanExchangeCoupon) Lock(ctx context.Context, orderNo string, coupon *CouponInfo, tx *gorm.DB) error {
return nil
}
// Redeem 核销美团兑换券(产品券:订单创建即核销)
func (m *MeiTuanExchangeCoupon) Redeem(ctx context.Context, orderNo string, coupon *CouponInfo, shop models.Shops, tx *gorm.DB) error {
zap.L().Info("核销美团兑换券", zap.String("order_no", orderNo), zap.String("coupon_code", coupon.CouponCode))
year, err := utils.GetYearByOrderNo("", orderNo)
if err != nil {
return err
}
// 1. 写入订单-券关联表(OrderCoupon)
orderCoupon := &models.OrderCoupon{
OrderNo: orderNo,
CouponCode: coupon.CouponCode,
CouponType: int(coupon.CouponType),
Amount: coupon.ParValue,
CustomerId: coupon.CustomerID,
ShopId: coupon.ShopID,
}
if err := m.couponModel.CreateOrderCoupon(ctx, tx, orderCoupon, year); err != nil {
if strings.Contains(err.Error(), "duplicate key") {
zap.L().Warn("订单-券关联表已存在", zap.String("order_no", orderNo))
} else {
return fmt.Errorf("写入OrderCoupon失败:%w", err)
}
}
// 2. 修改券状态为“已用”
if err := m.couponModel.UpdateCouponUserRelationStatus(ctx, tx, coupon.CustomerID, coupon.CouponCode, enum.CouponUserRelationStatus.StatusUsed); err != nil {
return fmt.Errorf("修改券状态失败:%w", err)
}
// 3. 调用美团平台核销接口(模拟)
if err := m.callMeiTuanRedeemAPI(ctx, coupon.CouponCode, orderNo); err != nil {
return fmt.Errorf("美团平台核销失败:%w", err)
}
return nil
}
// Unlock 美团兑换券无需解锁
func (m *MeiTuanExchangeCoupon) Unlock(ctx context.Context, couponCode string, customerID int64) error {
return nil
}
// 模拟美团开放平台接口
func (m *MeiTuanExchangeCoupon) callMeiTuanValidateAPI(ctx context.Context, couponCode string, shopID int64) (bool, int64, error) {
// 实际场景:http调用美团接口,返回校验结果和券面值
return true, 100, nil
}
func (m *MeiTuanExchangeCoupon) callMeiTuanRedeemAPI(ctx context.Context, couponCode string, orderNo string) error {
// 实际场景:http调用美团核销接口
return nil
}
// 美团代金券(支付券)实现类似,差异在于:
// 1. Validate需校验订单金额≥券面值;
// 2. Lock需锁定券(状态改为StatusLocked);
// 3. Redeem需先解锁再核销,或直接修改状态为StatusUsed。
2.3 券工厂(coupon_domain/factory.go)
package coupon_domain
import (
"your-project/enum"
)
// 校验器工厂
func NewCouponValidator(couponType enum.CouponType) CouponValidator {
switch couponType {
case enum.MeiTuanExchangeCoupon:
validator, _ := NewMeiTuanExchangeCoupon()
return validator
case enum.MeiTuanCashCoupon:
validator, _ := NewMeiTuanCashCoupon()
return validator
case enum.AmapCashCoupon:
validator, _ := NewAmapCashCoupon()
return validator
default:
return nil
}
}
// 操作器工厂
func NewCouponOperator(couponType enum.CouponType) CouponOperator {
switch couponType {
case enum.MeiTuanExchangeCoupon:
_, operator := NewMeiTuanExchangeCoupon()
return operator
case enum.MeiTuanCashCoupon:
_, operator := NewMeiTuanCashCoupon()
return operator
case enum.AmapCashCoupon:
_, operator := NewAmapCashCoupon()
return operator
default:
return nil
}
}
3. Model层:数据持久化接口拆分
3.1 订单Model(models/order.go)
package models
import (
"context"
"your-project/define/orders_define"
"your-project/enum"
"your-project/utils"
"go.uber.org/zap"
"gorm.io/gorm"
)
// 核心订单创建(支持外部事务)
func CreateCoreOrderWithTx(ctx context.Context, tx *gorm.DB, req orders_define.CreateOrderReq, cart cart_define.CartResp) (int64, error) {
// 1. 构造订单主表数据
order := &Orders{
OrderNo: req.OrderNo,
DeskNo: "",
DineWay: req.DineWay,
TotalCost: cart.TotalCost,
TotalAmount: req.TotalAmount,
GoodsCount: cart.GoodsCount,
Note: req.Note,
Contact: req.Contact,
CustomerId: req.CustomerID,
Platform: req.Platform,
PayType: req.PayType,
ShopId: req.ShopID,
State: int(req.OrderState),
CouponType: int(req.CouponType),
CouponCode: req.CouponCode,
CouponAmount: req.CouponAmount,
CreatedAt: &utils.XTime{Time: time.Now()},
UpdatedAt: &utils.XTime{Time: time.Now()},
}
// 2. 构造订单项数据
var goods []*OrderGoods
for _, item := range cart.GoodsDetail {
goods = append(goods, &OrderGoods{
OrderNo: req.OrderNo,
GoodsId: item.GoodsID,
GoodsName: item.GoodsName,
Price: item.Price,
Count: item.Count,
Total: item.Total,
})
}
// 3. 写入数据库(使用外部事务tx)
year, err := utils.GetYearByOrderNo("", req.OrderNo)
if err != nil {
return 0, err
}
orderTable := order.TableName(year)
// 写入订单主表
if err := tx.Model(&Orders{}).Table(orderTable).Create(order).Error; err != nil {
zap.L().Error("写入订单主表失败", zap.Error(err), zap.String("order_no", req.OrderNo))
return 0, err
}
// 批量写入订单项
orderGoodsDo := &OrderGoods{}
saveSql, args, err := orderGoodsDo.BatchSave(ctx, year, goods)
if err != nil {
zap.L().Error("构造订单项SQL失败", zap.Error(err), zap.String("order_no", req.OrderNo))
return 0, err
}
if err := tx.Exec(saveSql, args...).Error; err != nil {
zap.L().Error("写入订单项失败", zap.Error(err), zap.String("order_no", req.OrderNo))
return 0, err
}
return order.ID, nil
}
// 更新订单状态
func UpdateOrderState(ctx context.Context, orderNo string, state enum.OrderStatus) error {
year, err := utils.GetYearByOrderNo("", orderNo)
if err != nil {
return err
}
orderTable := new(Orders).TableName(year)
return Db.Table(orderTable).Where("order_no = ?", orderNo).Update("state", int(state)).Error
}
3.2 券Model(models/coupon.go)
package models
import (
"context"
"your-project/enum"
"gorm.io/gorm"
)
// CouponModel 券相关数据操作
type CouponModel struct{}
// 检查用户-券关联关系是否存在
func (m *CouponModel) CheckCouponUserRelation(ctx context.Context, customerID int64, couponCode string) (bool, error) {
var count int64
err := Db.Table(new(CouponUserRelation).TableName(0)).
Where("customer_id = ? AND coupon_code = ? AND status = ?", customerID, couponCode, enum.CouponUserRelationStatus.StatusAvailable).
Count(&count).Error
if err != nil {
return false, err
}
return count > 0, nil
}
// 写入订单-券关联表
func (m *CouponModel) CreateOrderCoupon(ctx context.Context, tx *gorm.DB, orderCoupon *OrderCoupon, year int) error {
return tx.Table(new(OrderCoupon).TableName(year)).Create(orderCoupon).Error
}
// 修改券用户关联状态
func (m *CouponModel) UpdateCouponUserRelationStatus(ctx context.Context, tx *gorm.DB, customerID int64, couponCode string, status enum.CouponUserRelationStatus) error {
updateMap := map[string]interface{}{"status": status}
if status == enum.CouponUserRelationStatus.StatusUsed {
updateMap["charge_time"] = time.Now().Unix()
}
return tx.Table(new(CouponUserRelation).TableName(0)).
Where("customer_id = ? AND coupon_code = ?", customerID, couponCode).
Updates(updateMap).Error
}
4. Service层:管道模式流程编排
4.1 产品券订单流程(无支付)
4.1.1 流程上下文(order_service/coupon_order_context.go)
package order_service
import (
"context"
"your-project/coupon_domain"
"your-project/define/cart_define"
"your-project/define/coupon_define"
"your-project/define/orders_define"
"your-project/models"
)
type CouponOrderPipelineContext struct {
// 输入
Req coupon_define.CreateCouponOrderReq
Cart cart_define.CartResp
ShopInfo models.Shops
Ctx context.Context
// 中间结果
ValidCoupons []*coupon_domain.CouponInfo
OrderNos []string
CreateOrderReqs []orders_define.CreateOrderReq
CreateOrderResps []orders_define.CreateOrderResp
// 输出
Resp coupon_define.CreateCouponOrderResp
Error error
}
func (c *CouponOrderPipelineContext) SetError(err error) {
c.Error = err
}
func (c *CouponOrderPipelineContext) HasError() bool {
return c.Error != nil
}
4.1.2 步骤实现(order_service/coupon_order_steps.go)
package order_service
import (
"your-project/coupon_domain"
"your-project/define/coupon_define"
"your-project/enum"
"go.uber.org/zap"
)
// 步骤1:券校验
type CouponOrderValidateStep struct{}
func (s *CouponOrderValidateStep) Name() string { return "券校验步骤" }
func (s *CouponOrderValidateStep) Execute(ctx *CouponOrderPipelineContext) {
if ctx.HasError() {
return
}
validCoupons := make([]*coupon_domain.CouponInfo, 0, len(ctx.Req.CouponProductCode))
eg := errgroup.Group{}
for _, cp := range ctx.Req.CouponProductCode {
cp := cp
eg.Go(func() error {
validator := coupon_domain.NewCouponValidator(cp.CouponType)
if validator == nil {
return fmt.Errorf("不支持的券类型:%d", cp.CouponType)
}
req := coupon_define.CouponValidateReq{
CouponCode: cp.CouponCode,
CouponType: cp.CouponType,
CustomerID: ctx.Req.CustomerID,
ShopID: ctx.Req.ShopID,
OrderType: enum.CouponOrder,
}
coupon, err := validator.Validate(ctx.Ctx, req)
if err != nil {
return fmt.Errorf("券%s校验失败:%w", cp.CouponCode, err)
}
validCoupons = append(validCoupons, coupon)
return nil
})
}
if err := eg.Wait(); err != nil {
ctx.SetError(err)
return
}
ctx.ValidCoupons = validCoupons
zap.L().Info("券校验完成", zap.Int("count", len(validCoupons)))
}
// 步骤2:构造购物车
type CouponOrderBuildCartStep struct{}
func (s *CouponOrderBuildCartStep) Name() string { return "构造购物车步骤" }
func (s *CouponOrderBuildCartStep) Execute(ctx *CouponOrderPipelineContext) {
if ctx.HasError() {
return
}
// 产品券:一个券对应一个商品(从券信息或商品库获取商品数据)
var goods []cart_define.GoodsDetail
totalCost := int64(0)
for _, coupon := range ctx.ValidCoupons {
// 模拟从商品库获取商品信息(实际需调用商品服务)
goods = append(goods, cart_define.GoodsDetail{
GoodsID: fmt.Sprintf("coupon_%s", coupon.CouponCode),
GoodsName: fmt.Sprintf("%s商品", coupon_domain.NewCouponValidator(coupon.CouponType).Name()),
Price: coupon.ParValue,
Count: 1,
Total: coupon.ParValue,
})
totalCost += coupon.ParValue
}
ctx.Cart = cart_define.CartResp{
GoodsDetail: goods,
TotalCost: totalCost,
GoodsCount: len(goods),
}
zap.L().Info("构造购物车完成", zap.Int("goods_count", len(goods)))
}
// 步骤3:生成订单号
type CouponOrderGenerateOrderNoStep struct{}
func (s *CouponOrderGenerateOrderNoStep) Name() string { return "生成订单号步骤" }
func (s *CouponOrderGenerateOrderNoStep) Execute(ctx *CouponOrderPipelineContext) {
if ctx.HasError() {
return
}
orderNos := make([]string, 0, len(ctx.ValidCoupons))
eg := errgroup.Group{}
for range ctx.ValidCoupons {
eg.Go(func() error {
// 调用订单号生成服务(模拟)
orderNo := utils.GenerateOrderNo(ctx.Req.CustomerID)
orderNos = append(orderNos, orderNo)
return nil
})
}
if err := eg.Wait(); err != nil {
ctx.SetError(fmt.Errorf("生成订单号失败:%w", err))
return
}
ctx.OrderNos = orderNos
zap.L().Info("生成订单号完成", zap.Strings("order_nos", orderNos))
}
// 步骤4:创建订单+核销券(同一事务)
type CouponOrderCreateAndRedeemStep struct{}
func (s *CouponOrderCreateAndRedeemStep) Name() string { return "创建订单+券核销步骤" }
func (s *CouponOrderCreateAndRedeemStep) Execute(ctx *CouponOrderPipelineContext) {
if ctx.HasError() {
return
}
eg := errgroup.Group{}
createResps := make([]orders_define.CreateOrderResp, 0, len(ctx.OrderNos))
for i, orderNo := range ctx.OrderNos {
i, orderNo := i, orderNo
coupon := ctx.ValidCoupons[i]
eg.Go(func() error {
// 开启事务:订单创建+券核销原子性
tx := models.Db.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
// 构造核心订单请求(产品券订单无支付,状态直接设为已支付)
createReq := orders_define.CreateOrderReq{
OrderNo: orderNo,
ShopID: ctx.Req.ShopID,
CustomerID: ctx.Req.CustomerID,
Platform: ctx.Req.Platform,
DineWay: ctx.Req.DineWay,
PayType: enum.PayType.XjdCouponPayType,
CouponType: coupon.CouponType,
CouponCode: coupon.CouponCode,
CouponAmount: coupon.ParValue,
TotalAmount: 0, // 全额抵扣
OrderState: enum.PaySuccess,
Contact: ctx.Req.Contact,
Note: ctx.Req.Note,
}
// 调用核心订单创建(传入事务tx)
orderID, err := models.CreateCoreOrderWithTx(ctx.Ctx, tx, createReq, ctx.Cart)
if err != nil {
tx.Rollback()
return fmt.Errorf("创建订单失败:%w", err)
}
// 调用券核销(传入同一事务tx)
operator := coupon_domain.NewCouponOperator(coupon.CouponType)
if err := operator.Redeem(ctx.Ctx, orderNo, coupon, ctx.ShopInfo, tx); err != nil {
tx.Rollback()
return fmt.Errorf("券核销失败:%w", err)
}
// 提交事务
tx.Commit()
createResps = append(createResps, orders_define.CreateOrderResp{
OrderNo: orderNo,
OrderID: orderID,
State: enum.PaySuccess,
})
return nil
})
}
if err := eg.Wait(); err != nil {
ctx.SetError(err)
return
}
ctx.CreateOrderResps = createResps
zap.L().Info("创建订单+券核销完成", zap.Int("success_count", len(createResps)))
}
4.1.3 管道与Service入口(order_service/coupon_order_service.go)
package order_service
import (
"context"
"your-project/define/coupon_define"
"your-project/models"
)
// 管道定义
type CouponOrderPipeline struct {
steps []CouponOrderStep
}
type CouponOrderStep interface {
Name() string
Execute(ctx *CouponOrderPipelineContext)
}
func NewCouponOrderPipeline() *CouponOrderPipeline {
return &CouponOrderPipeline{steps: make([]CouponOrderStep, 0)}
}
func (p *CouponOrderPipeline) RegisterStep(step CouponOrderStep) *CouponOrderPipeline {
p.steps = append(p.steps, step)
return p
}
func (p *CouponOrderPipeline) Run(ctx *CouponOrderPipelineContext) {
zap.L().Info("产品券订单流程开始", zap.Any("req", ctx.Req))
for _, step := range p.steps {
if ctx.HasError() {
zap.L().Warn("流程终止", zap.String("failed_step", step.Name()), zap.Error(ctx.Error))
return
}
zap.L().Info("执行步骤", zap.String("step", step.Name()))
step.Execute(ctx)
}
// 组装响应
if !ctx.HasError() {
orderNos := make([]string, 0, len(ctx.CreateOrderResps))
for _, resp := range ctx.CreateOrderResps {
orderNos = append(orderNos, resp.OrderNo)
}
ctx.Resp = coupon_define.CreateOrderCouponResp{OrderNo: orderNos}
}
}
// Service入口(供API层调用)
func CreateCouponOrder(ctx context.Context, req coupon_define.CreateCouponOrderReq) (coupon_define.CreateCouponOrderResp, error) {
// 查询商户信息
shopInfo, err := models.GetShopById(ctx, req.ShopID)
if err != nil {
return coupon_define.CreateCouponOrderResp{}, fmt.Errorf("查询商户失败:%w", err)
}
// 初始化上下文
pipelineCtx := &CouponOrderPipelineContext{
Req: req,
ShopInfo: shopInfo,
Ctx: ctx,
}
// 注册步骤并执行
pipeline := NewCouponOrderPipeline().
RegisterStep(&CouponOrderValidateStep{}).
RegisterStep(&CouponOrderBuildCartStep{}).
RegisterStep(&CouponOrderGenerateOrderNoStep{}).
RegisterStep(&CouponOrderCreateAndRedeemStep{})
pipeline.Run(pipelineCtx)
if pipelineCtx.HasError() {
return coupon_define.CreateCouponOrderResp{}, pipelineCtx.Error
}
return pipelineCtx.Resp, nil
}
4.2 普通支付订单流程(有支付)
4.2.1 流程上下文与步骤(类似产品券订单,核心差异在“券锁定”和“支付回调核销”)
// 流程上下文(order_service/pay_order_context.go)
type PayOrderPipelineContext struct {
// 输入
Req orders_define.CreatePayOrderReq
Cart cart_define.CartResp
Ctx context.Context
// 中间结果
ValidCoupon *coupon_domain.CouponInfo
OrderNo string
CreateOrderReq orders_define.CreateOrderReq
CreateOrderResp orders_define.CreateOrderResp
// 输出
Resp orders_define.CreatePayOrderResp
Error error
}
// 核心步骤:券锁定(order_service/pay_order_steps.go)
type PayOrderLockCouponStep struct{}
func (s *PayOrderLockCouponStep) Name() string { return "券锁定步骤" }
func (s *PayOrderLockCouponStep) Execute(ctx *PayOrderPipelineContext) {
if ctx.HasError() || ctx.ValidCoupon == nil {
return
}
// 开启事务:订单创建+券锁定原子性
tx := models.Db.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
// 1. 创建核心订单
orderID, err := models.CreateCoreOrderWithTx(ctx.Ctx, tx, ctx.CreateOrderReq, ctx.Cart)
if err != nil {
tx.Rollback()
ctx.SetError(fmt.Errorf("创建订单失败:%w", err))
return
}
// 2. 锁定券
operator := coupon_domain.NewCouponOperator(ctx.ValidCoupon.CouponType)
if err := operator.Lock(ctx.Ctx, ctx.OrderNo, ctx.ValidCoupon, tx); err != nil {
tx.Rollback()
ctx.SetError(fmt.Errorf("券锁定失败:%w", err))
return
}
// 提交事务
tx.Commit()
ctx.CreateOrderResp = orders_define.CreateOrderResp{
OrderNo: ctx.OrderNo,
OrderID: orderID,
State: enum.WaitPay,
}
}
// 支付回调核销步骤(order_service/pay_callback_steps.go)
type PayCallbackRedeemCouponStep struct{}
func (s *PayCallbackRedeemCouponStep) Name() string { return "券核销步骤" }
func (s *PayCallbackRedeemCouponStep) Execute(ctx *PayCallbackPipelineContext) {
if ctx.HasError() || ctx.Order.CouponCode == "" {
return
}
// 构造券信息
couponInfo := &coupon_domain.CouponInfo{
CouponCode: ctx.Order.CouponCode,
CouponType: enum.CouponType(ctx.Order.CouponType),
CustomerID: ctx.Order.CustomerId,
ShopID: ctx.Order.ShopId,
ParValue: ctx.Order.CouponAmount,
}
// 开启事务核销券
tx := models.Db.Begin()
defer func() {
if ctx.HasError() {
tx.Rollback()
} else {
tx.Commit()
}
}()
operator := coupon_domain.NewCouponOperator(couponInfo.CouponType)
if err := operator.Redeem(ctx.Ctx, ctx.Order.OrderNo, couponInfo, ctx.ShopInfo, tx); err != nil {
ctx.SetError(fmt.Errorf("券核销失败:%w", err))
// 标记订单状态
_ = models.UpdateOrderState(ctx.Ctx, ctx.Order.OrderNo, enum.PaySuccessButCouponFail)
return
}
// 更新订单状态为已支付
if err := models.UpdateOrderState(ctx.Ctx, ctx.Order.OrderNo, enum.PaySuccess); err != nil {
ctx.SetError(fmt.Errorf("更新订单状态失败:%w", err))
return
}
}
5. 补偿机制:定时任务解锁超时券(coupon_service/compensate.go)
package coupon_service
import (
"context"
"time"
"your-project/coupon_domain"
"your-project/enum"
"your-project/models"
"go.uber.org/zap"
)
// UnlockExpiredLockedCoupons 定时任务(每5分钟执行):解锁15分钟未支付的券
func UnlockExpiredLockedCoupons(ctx context.Context) {
zap.L().Info("开始执行超时券解锁任务")
expiredTime := time.Now().Add(-15 * time.Minute).Unix()
// 查询超时锁定的券
var orderCoupons []*models.OrderCoupon
err := models.Db.Table(new(models.OrderCoupon).TableName(0)).
Joins("LEFT JOIN orders ON orders.order_no = order_coupon.order_no").
Where("order_coupon.status = ?", enum.CouponUserRelationStatus.StatusLocked).
Where("orders.state = ?", enum.WaitPay).
Where("orders.created_at < ?", expiredTime).
Find(&orderCoupons).Error
if err != nil {
zap.L().Error("查询超时锁定券失败", zap.Error(err))
return
}
// 批量解锁
for _, oc := range orderCoupons {
operator := coupon_domain.NewCouponOperator(enum.CouponType(oc.CouponType))
if err := operator.Unlock(ctx, oc.CouponCode, oc.CustomerId); err != nil {
zap.L().Error("解锁券失败", zap.String("coupon_code", oc.CouponCode), zap.Error(err))
continue
}
// 更新订单状态为已取消
if err := models.UpdateOrderState(ctx, oc.OrderNo, enum.Canceled); err != nil {
zap.L().Error("更新订单状态为取消失败", zap.String("order_no", oc.OrderNo), zap.Error(err))
}
zap.L().Info("解锁超时券成功", zap.String("coupon_code", oc.CouponCode), zap.String("order_no", oc.OrderNo))
}
}
6. API层(api/order_api.go)
package api
import (
"net/http"
"your-project/define/orders_define"
"your-project/define/coupon_define"
"your-project/service/order_service"
"your-project/utils/resp"
"github.com/gin-gonic/gin"
)
// 创建产品券订单
func CreateCouponOrder(c *gin.Context) {
var req coupon_define.CreateCouponOrderReq
if err := c.ShouldBindJSON(&req); err != nil {
resp.Fail(c, http.StatusBadRequest, "参数校验失败", err.Error())
return
}
respData, err := order_service.CreateCouponOrder(c.Request.Context(), req)
if err != nil {
resp.Fail(c, http.StatusInternalServerError, "创建产品券订单失败", err.Error())
return
}
resp.Success(c, "创建成功", respData)
}
// 创建普通支付订单
func CreatePayOrder(c *gin.Context) {
var req orders_define.CreatePayOrderReq
if err := c.ShouldBindJSON(&req); err != nil {
resp.Fail(c, http.StatusBadRequest, "参数校验失败", err.Error())
return
}
respData, err := order_service.CreatePayOrder(c.Request.Context(), req)
if err != nil {
resp.Fail(c, http.StatusInternalServerError, "创建支付订单失败", err.Error())
return
}
resp.Success(c, "创建成功", respData)
}
// 支付回调
func PayCallback(c *gin.Context) {
var req orders_define.PayCallbackReq
if err := c.ShouldBindJSON(&req); err != nil {
resp.Fail(c, http.StatusBadRequest, "参数校验失败", err.Error())
return
}
err := order_service.PayCallback(c.Request.Context(), req)
if err != nil {
resp.Fail(c, http.StatusInternalServerError, "支付回调处理失败", err.Error())
return
}
resp.Success(c, "处理成功", nil)
}
三、关键特性与优势
1. 解耦彻底
- 券逻辑完全抽离到
coupon_domain,订单领域不再包含任何券校验/核销代码; - Model层按“订单/券/储值”拆分接口,职责单一,支持独立扩展。
2. 可扩展性强
- 新增券类型(如高德券):仅需实现
CouponValidator和CouponOperator接口,工厂注册即可,无需修改流程代码; - 新增流程步骤(如积分抵扣):仅需新增Step实现,管道中注册,不影响其他步骤。
3. 事务一致性保障
- 产品券订单:单一事务覆盖“订单创建+券核销”,原子性执行;
- 普通支付订单:“订单创建+券锁定”事务+“支付回调+券核销”事务+定时补偿,保证最终一致性。
4. 可维护性高
- 管道模式让流程可视化,步骤顺序清晰,便于排查问题;
- 每个组件职责单一,代码复用率高(如“生成订单号”步骤可复用)。
四、重构落地步骤
- 基础准备:定义枚举、请求/响应结构体,确保各层依赖清晰;
- Domain层实现:先完成券的策略接口和具体实现(美团/抖音/高德),编写单元测试;
- Model层改造:拆分核心订单、券、储值接口,保留原有表结构,仅修改数据操作逻辑;
- Service层编排:实现管道模式和步骤,先调试产品券订单流程(无支付,逻辑简单);
- 支付订单与回调:实现普通支付订单流程和支付回调,测试“锁定-核销-解锁”全链路;
- 补偿机制:部署定时任务,测试超时券解锁逻辑;
- 灰度发布:先灰度产品券订单,再灰度普通支付订单,监控日志和数据一致性。
五、风险与应对
| 风险点 | 应对方案 |
|---|---|
| 券核销与订单事务不一致 | 产品券订单用单一事务;普通支付订单加定时补偿+人工干预接口 |
| 外部券平台调用失败 | 增加重试机制(幂等性设计),失败后标记订单状态便于人工处理 |
| 重构后性能下降 | 批量写入订单项保留原有SQL拼接逻辑;核心接口加缓存(如商户信息) |
通过以上方案,可彻底解决原有业务中“券逻辑与订单耦合”“流程混乱”“事务不一致”的问题,同时具备极强的扩展性和可维护性。
总结:重构的核心价值与进阶方向(事件溯源的补充)
一、重构带来的4大核心收益
- 解耦彻底,职责清晰:通过
Domain层封装券核心逻辑,Service层专注流程编排,Model层聚焦数据持久化,各层边界明确,彻底解决了原有系统“牵一发而动全身”的问题; - 扩展性极强:新增券类型(如微信券、支付宝券)时,仅需实现
CouponValidator和CouponOperator接口,通过工厂模式注册,无需修改任何核心流程代码,完全符合“开闭原则”; - 一致性与性能平衡:针对不同订单类型设计差异化事务策略——产品券订单用“单一事务”保证原子性,普通支付订单用“锁定+核销+补偿”保证最终一致性,既避免了事务过大导致的超时问题,又保障了数据可靠性;
- 可维护性大幅提升:管道模式让业务流程可视化,步骤解耦后便于单元测试和问题排查,代码复用率提升(如“生成订单号”“构造购物车”等步骤可跨流程复用)。
二、进阶优化:事件溯源(Event Sourcing)实现更彻底的解耦
本次重构已经解决了核心痛点,但在复杂分布式场景下,仍可通过事件溯源进一步优化,实现领域间的“完全解耦”。
1. 事件溯源的核心思想
事件溯源的核心是:不存储领域对象的当前状态,而是存储导致状态变更的所有事件。通过重放事件,可以重建对象的任意历史状态。对于订单与券的业务场景,可将关键操作转化为“事件”:
- 订单领域事件:
OrderCreatedEvent(订单创建)、OrderPaidEvent(订单支付)、OrderCanceledEvent(订单取消); - 券领域事件:
CouponLockedEvent(券锁定)、CouponRedeemedEvent(券核销)、CouponUnlockedEvent(券解锁)。
2. 事件溯源与现有方案的结合
在本次重构的基础上,引入事件总线(如Kafka、RabbitMQ),让订单领域和券领域通过“事件”通信,而非直接调用:
- 普通支付订单流程优化:
- 订单创建后,不直接调用券领域的
Lock方法,而是发布OrderCreatedEvent; - 券领域订阅
OrderCreatedEvent,接收事件后执行券锁定逻辑,并发布CouponLockedEvent; - 若券锁定失败,发布
CouponLockFailedEvent,订单领域订阅后执行订单取消逻辑。
- 订单创建后,不直接调用券领域的
- 支付回调流程优化:
- 支付成功后,发布
OrderPaidEvent; - 券领域订阅
OrderPaidEvent,执行券核销逻辑,发布CouponRedeemedEvent; - 订单领域订阅
CouponRedeemedEvent,更新订单状态为“支付成功且券核销完成”。
- 支付成功后,发布
3. 事件溯源的核心优势
- 彻底解耦:订单领域和券领域不再有直接依赖,仅通过事件总线通信,新增业务或修改逻辑时互不影响;
- 可追溯性:所有状态变更都有事件记录,可完整追溯订单和券的流转过程,便于问题排查和审计;
- 容错性强:若某领域处理事件失败,可通过事件重放机制恢复状态,无需人工干预;
- 支持复杂业务扩展:如需新增“积分发放”“营销统计”等业务,仅需订阅相关事件即可,无需修改原有流程。
三、最终思考
架构设计没有“银弹”,本次重构选择“策略+管道+事务策略”的组合,是基于当前业务场景(单体架构、核心诉求是解耦和扩展)的最优解。而事件溯源作为进阶方向,更适合分布式场景下的复杂业务扩展。
核心原则是:架构设计需匹配业务阶段——早期业务快速迭代时,优先保证开发效率和稳定性;业务成熟后,可通过引入更先进的架构模式(如事件溯源、DDD)进一步提升系统弹性和扩展性。
本次重构不仅解决了现有系统的痛点,更建立了一套可复用的“业务编排+领域封装”模式,为后续业务扩展打下了坚实基础。希望本文的方案能为面临类似问题的开发者提供参考。
更多推荐


所有评论(0)