背景:从“业务堆砌”到“架构解耦”的重构之路

在餐饮O2O场景中,订单系统是业务核心,而“券”(产品兑换券、代金券)作为引流和转化的关键工具,往往需要对接多平台(抖音、美团、高德等)、支持多订单模式(无支付产品券、需支付普通订单)。但随着业务迭代,很多系统会陷入“堆砌式开发”的困境——我所在的项目也不例外。

原有系统的4大核心痛点

  1. 逻辑强耦合:券的校验、核销、状态修改逻辑与订单流程深度绑定在service层,代码臃肿(单函数千行+),新增券类型(如高德券)需修改原有订单核心代码,风险极高;
  2. 流程混乱:产品券订单(无支付)和普通支付订单(有支付)共用部分代码,券校验、核销步骤重复执行,边界模糊,排查问题时需跨多层追踪;
  3. 事务一致性难保障:订单创建、券状态修改、储值扣减强绑定在同一个事务中,一旦涉及外部平台(如抖音券核销接口)调用,极易导致事务超时或数据不一致;
  4. 扩展性差:新增多平台券(如高德代金券、支付宝兑换券)时,需在订单流程中硬编码判断逻辑,违反“开闭原则”,维护成本指数级增长。

为解决这些问题,我们启动了订单系统的重构项目,核心目标是:解耦券逻辑与订单流程、统一流程编排规范、保障事务一致性、提升系统扩展性。经过多轮技术选型和方案论证,最终形成了“策略模式+管道模式+事务策略”的全栈解决方案,下文将详细拆解实现思路与完整代码。

一、整体架构设计

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.OrderStatuscoupon_define.CouponValidateReqorders_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. 可扩展性强

  • 新增券类型(如高德券):仅需实现CouponValidatorCouponOperator接口,工厂注册即可,无需修改流程代码;
  • 新增流程步骤(如积分抵扣):仅需新增Step实现,管道中注册,不影响其他步骤。

3. 事务一致性保障

  • 产品券订单:单一事务覆盖“订单创建+券核销”,原子性执行;
  • 普通支付订单:“订单创建+券锁定”事务+“支付回调+券核销”事务+定时补偿,保证最终一致性。

4. 可维护性高

  • 管道模式让流程可视化,步骤顺序清晰,便于排查问题;
  • 每个组件职责单一,代码复用率高(如“生成订单号”步骤可复用)。

四、重构落地步骤

  1. 基础准备:定义枚举、请求/响应结构体,确保各层依赖清晰;
  2. Domain层实现:先完成券的策略接口和具体实现(美团/抖音/高德),编写单元测试;
  3. Model层改造:拆分核心订单、券、储值接口,保留原有表结构,仅修改数据操作逻辑;
  4. Service层编排:实现管道模式和步骤,先调试产品券订单流程(无支付,逻辑简单);
  5. 支付订单与回调:实现普通支付订单流程和支付回调,测试“锁定-核销-解锁”全链路;
  6. 补偿机制:部署定时任务,测试超时券解锁逻辑;
  7. 灰度发布:先灰度产品券订单,再灰度普通支付订单,监控日志和数据一致性。

五、风险与应对

风险点 应对方案
券核销与订单事务不一致 产品券订单用单一事务;普通支付订单加定时补偿+人工干预接口
外部券平台调用失败 增加重试机制(幂等性设计),失败后标记订单状态便于人工处理
重构后性能下降 批量写入订单项保留原有SQL拼接逻辑;核心接口加缓存(如商户信息)

通过以上方案,可彻底解决原有业务中“券逻辑与订单耦合”“流程混乱”“事务不一致”的问题,同时具备极强的扩展性和可维护性。

总结:重构的核心价值与进阶方向(事件溯源的补充)

一、重构带来的4大核心收益

  1. 解耦彻底,职责清晰:通过Domain层封装券核心逻辑,Service层专注流程编排,Model层聚焦数据持久化,各层边界明确,彻底解决了原有系统“牵一发而动全身”的问题;
  2. 扩展性极强:新增券类型(如微信券、支付宝券)时,仅需实现CouponValidatorCouponOperator接口,通过工厂模式注册,无需修改任何核心流程代码,完全符合“开闭原则”;
  3. 一致性与性能平衡:针对不同订单类型设计差异化事务策略——产品券订单用“单一事务”保证原子性,普通支付订单用“锁定+核销+补偿”保证最终一致性,既避免了事务过大导致的超时问题,又保障了数据可靠性;
  4. 可维护性大幅提升:管道模式让业务流程可视化,步骤解耦后便于单元测试和问题排查,代码复用率提升(如“生成订单号”“构造购物车”等步骤可跨流程复用)。

二、进阶优化:事件溯源(Event Sourcing)实现更彻底的解耦

本次重构已经解决了核心痛点,但在复杂分布式场景下,仍可通过事件溯源进一步优化,实现领域间的“完全解耦”。

1. 事件溯源的核心思想

事件溯源的核心是:不存储领域对象的当前状态,而是存储导致状态变更的所有事件。通过重放事件,可以重建对象的任意历史状态。对于订单与券的业务场景,可将关键操作转化为“事件”:

  • 订单领域事件:OrderCreatedEvent(订单创建)、OrderPaidEvent(订单支付)、OrderCanceledEvent(订单取消);
  • 券领域事件:CouponLockedEvent(券锁定)、CouponRedeemedEvent(券核销)、CouponUnlockedEvent(券解锁)。
2. 事件溯源与现有方案的结合

在本次重构的基础上,引入事件总线(如Kafka、RabbitMQ),让订单领域和券领域通过“事件”通信,而非直接调用:

  1. 普通支付订单流程优化:
    • 订单创建后,不直接调用券领域的Lock方法,而是发布OrderCreatedEvent
    • 券领域订阅OrderCreatedEvent,接收事件后执行券锁定逻辑,并发布CouponLockedEvent
    • 若券锁定失败,发布CouponLockFailedEvent,订单领域订阅后执行订单取消逻辑。
  2. 支付回调流程优化:
    • 支付成功后,发布OrderPaidEvent
    • 券领域订阅OrderPaidEvent,执行券核销逻辑,发布CouponRedeemedEvent
    • 订单领域订阅CouponRedeemedEvent,更新订单状态为“支付成功且券核销完成”。
3. 事件溯源的核心优势
  • 彻底解耦:订单领域和券领域不再有直接依赖,仅通过事件总线通信,新增业务或修改逻辑时互不影响;
  • 可追溯性:所有状态变更都有事件记录,可完整追溯订单和券的流转过程,便于问题排查和审计;
  • 容错性强:若某领域处理事件失败,可通过事件重放机制恢复状态,无需人工干预;
  • 支持复杂业务扩展:如需新增“积分发放”“营销统计”等业务,仅需订阅相关事件即可,无需修改原有流程。

三、最终思考

架构设计没有“银弹”,本次重构选择“策略+管道+事务策略”的组合,是基于当前业务场景(单体架构、核心诉求是解耦和扩展)的最优解。而事件溯源作为进阶方向,更适合分布式场景下的复杂业务扩展。

核心原则是:架构设计需匹配业务阶段——早期业务快速迭代时,优先保证开发效率和稳定性;业务成熟后,可通过引入更先进的架构模式(如事件溯源、DDD)进一步提升系统弹性和扩展性。

本次重构不仅解决了现有系统的痛点,更建立了一套可复用的“业务编排+领域封装”模式,为后续业务扩展打下了坚实基础。希望本文的方案能为面临类似问题的开发者提供参考。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐