基于券类型路由的渐进式重构:函数式选项模式与管道模式的完美结合

引言:复杂业务场景下的重构挑战

在现代电商系统中,优惠券订单处理是一个典型的复杂业务场景,涉及多种券类型、多级校验、库存管理、订单创建等环节。随着业务发展,原有的CreateCouponOrder函数已经膨胀到200+行,成为一个典型的"上帝函数",面临可维护性差、难以扩展、测试困难等问题。

本文将分享我们如何采用函数式选项模式管道模式相结合的策略,在保证业务连续性的前提下,对复杂业务逻辑进行渐进式重构的实践经验。通过按券类型路由,将高德券等新券类型走新逻辑,原有券类型保持旧逻辑,实现了平滑迁移和风险可控。

一、问题背景:优惠券订单处理的痛点

1.1 原有实现的核心问题

go 复制代码
func CreateCouponOrder(ctx context.Context, req coupon_define.CreateCouponOrderReq,
    shopInfo models.Shops, opts ...CouponOrderOptionFunc) (
    resp coupon_define.CreateCouponOrderResp,
    newOpts []CouponOrderOptionFunc,
    err error) {
    // 1. 查询商品信息
    // 2. 处理多种优惠券类型(美团券、品牌券等)
    // 3. 调用第三方接口核销
    // 4. 组装购物车
    // 5. 预扣库存
    // 6. 创建订单
    // ... 超过200行的复杂嵌套逻辑
}

主要痛点

  • 代码臃肿:单个函数承担过多职责
  • 难以测试:依赖复杂,难以编写有效的单元测试
  • 扩展困难:新增券类型需要修改大量代码
  • 重复查询:相同数据在不同地方重复查询
  • 维护困难:代码逻辑复杂,新人难以理解

1.2 重构目标与约束条件

  1. 业务连续性:不能影响线上原有券类型的处理
  2. 渐进式重构:支持平滑迁移,降低风险
  3. 数据复用:避免重复查询,提升性能
  4. 可测试性:提高代码的可测试性
  5. 可扩展性:便于新增业务逻辑和券类型

二、架构设计:按券类型路由的双逻辑并存

2.1 整体架构设计

复制代码
┌─────────────────────────────────────────────┐
│         路由层:CreateCouponOrderWithRoute   │
│       (识别券类型,路由到不同处理器)         │
└─────────────────┬───────────────────────────┘
                  │
          ┌───────▼──────────┐
          │  券类型识别逻辑   │
          │                  │
          └─────┬──────┬─────┘
                │      │
    ┌───────────▼      ▼────────────┐
    │ 高德券(新逻辑)     其他券(旧逻辑) │
    │  ┌────────────┐   ┌────────────┐│
    │  │ 管道模式   │   │ 原有函数   ││
    │  │ 处理器    │   │ 处理器     ││
    │  │ (返回选项) │   │ (返回选项) ││
    │  └────────────┘   └────────────┘│
    └─────────────────────────────────┘

2.2 路由层实现

go 复制代码
// 路由入口函数
func CreateCouponOrderWithRoute(ctx context.Context, req coupon_define.CreateCouponOrderReq,
    shopInfo models.Shops) (coupon_define.CreateCouponOrderResp, error) {
    
    // 构建选项函数,传递已查询的数据
    options := make([]CouponOrderOptionFunc, 0)
    
    // 如果已经查询过优惠券用户关系和规则,通过选项函数传递
    if couponUserRelation != nil {
        options = append(options, WithCouponOrderCouponUserRelation(couponUserRelation))
    }
    if couponRule != nil {
        options = append(options, WithCouponOrderCouponRule(couponRule))
    }
    
    log.InfoWithCtx(ctx, "创建团购券订单准备", zap.Any("req", req))
    
    // 第一步:调用原有方法处理所有券类型
    resp, newOpts, err := CreateCouponOrder(ctx, req, shopInfo, options...)
    if err != nil {
        return resp, err
    }
    
    // 第二步:高德券走新逻辑处理
    // 使用原有方法返回的选项函数(包含商品信息等)
    couponOrderPipeLineCtx := &CouponOrderPipelineContext{
        Ctx:      ctx,
        Req:      req,
        ShopInfo: shopInfo,
        Options:  newOpts,  // 使用原有方法查询的数据
    }
    
    // 调用新逻辑处理高德券
    couponOrderPipeLineCtx, err = orderService.CreateCouponOrder(couponOrderPipeLineCtx)
    if err != nil {
        // 如果失败,只需要回滚新逻辑的订单
        // 原有逻辑的订单保持不变
        log.ErrorWithCtx(ctx, "高德券订单处理失败", zap.Error(err))
        return resp, err
    }
    
    // 合并结果
    // ... 合并新旧逻辑的结果
    
    return resp, nil
}

2.3 原有函数改造:支持选项函数传递

go 复制代码
// 原有函数增加选项函数返回
func CreateCouponOrder(ctx context.Context, req coupon_define.CreateCouponOrderReq,
    shopInfo models.Shops, opts ...CouponOrderOptionFunc) (
    resp coupon_define.CreateCouponOrderResp,
    newOpts []CouponOrderOptionFunc,
    err error) {
    
    log.InfoWithCtx(ctx, "原有创建券订单方法开始")
    
    // 应用传入的选项函数
    options := NewCreateCouponOrderOptions()
    ApplyCouponOrderOptions(options, opts...)
    
    // 如果选项中没有商品信息,则查询
    if options.GoodsView == nil {
        goodsView, err := categorys_service.GoodsView(ctx, goods_define.GoodsViewReq{
            ShopId:  req.ShopID,
            DineWay: e.DineWay.EatIn,
        })
        if err != nil {
            return resp, newOpts, err
        }
        options.GoodsView = &goodsView
        newOpts = append(newOpts, WithCouponOrderGoodsView(&goodsView))
    }
    
    // 原有的复杂处理逻辑
    // ... 处理美团券、品牌券等
    
    // 将其他查询结果也封装为选项函数返回
    // newOpts = append(newOpts, WithCouponOrderCouponRule(couponRule))
    
    log.InfoWithCtx(ctx, "原有创建券订单方法结束")
    return resp, newOpts, nil
}

三、核心实现:管道模式处理高德券

这一步的设计思路参考我的上一篇文章《解耦与提效:券订单系统重构全方案(策略+工厂+管道)

3.1 管道上下文设计

go 复制代码
// 管道上下文结构
type CouponOrderPipelineContext struct {
    Ctx          context.Context
    Tx           *gorm.DB
    Req          coupon_define.CreateCouponOrderReq
    ShopInfo     models.Shops
    Options      []CouponOrderOptionFunc           // 选项函数
    Resp         coupon_define.CreateCouponOrderResp
    UserCart     map[string]cart_define.CartResp
    CouponUserRelation *sync.Map                   // 优惠券用户关系
    CouponRule         *sync.Map                   // 优惠券规则
    ThirdPartyRedeemSuccess *sync.Map              // 第三方核销成功标记
}

// 管道步骤接口
type CouponOrderStep interface {
    Name() string
    Execute(ctx *CouponOrderPipelineContext) error
    Rollback(ctx *CouponOrderPipelineContext) error
}

3.2 高德券处理管道

go 复制代码
// 注册步骤到管道
func (p *CouponOrderPipeline) RegisterStep(step CouponOrderStep) *CouponOrderPipeline {
	p.steps = append(p.steps, step)
	return p
}

// 运行优惠券订单流程
func (p *CouponOrderPipeline) Run(ctx *CouponOrderPipelineContext) error {
	log.InfoWithCtx(ctx.Ctx, "产品券订单流程开始")
	// 初始化数据库事务
	tx := models.Db.Begin()
	var executedSteps []CouponOrderStep
	defer func() {
		tx.RollbackUnlessCommitted()
		// 如果出现panic或者错误,执行回滚
		if r := recover(); r != nil {
			log.ErrorWithCtx(ctx.Ctx, "产品券订单流程发生panic",
				zap.Any("panic", r),
				zap.String("stack", string(debug.Stack())))
			p.rollbackSteps(ctx, executedSteps)
		}
	}()
	ctx.Tx = tx
	// 记录当前步骤
	for _, step := range p.steps {
		// 执行步骤前,先记录当前步骤,用于回滚
		executedSteps = append(executedSteps, step)
		log.InfoWithCtx(ctx.Ctx, "执行步骤", zap.String("step", step.Name()))
		err := step.Execute(ctx)
		if err != nil {
			// 执行失败,回滚事务和已执行步骤
			tx.Rollback()
			p.rollbackSteps(ctx, executedSteps)

			log.WarnWithCtx(ctx.Ctx, "流程终止",
				zap.String("failed_step", step.Name()),
				zap.Error(err),
				zap.Any("customer_id", ctx.Req.CustomerID),
			)
			return fmt.Errorf("%s失败: %w", step.Name(), err)
		}
	}

	err := tx.Commit().Error
	if err != nil {
		// 提交失败,回滚已执行步骤
		p.rollbackSteps(ctx, executedSteps)

		log.ErrorWithCtx(ctx.Ctx, "事务提交失败",
			zap.Error(err),
			zap.Any("customer_id", ctx.Req.CustomerID),
		)
		return fmt.Errorf("事务提交失败: %w", err)
	}

	log.InfoWithCtx(ctx.Ctx, "产品券订单流程完成")
	return nil
}

// 回滚已执行的步骤(倒序)
func (p *CouponOrderPipeline) rollbackSteps(ctx *CouponOrderPipelineContext, executedSteps []CouponOrderStep) {
	// 倒序回滚
	for i := len(executedSteps) - 1; i >= 0; i-- {
		step := executedSteps[i]
		if err := step.Rollback(ctx); err != nil {
			// 回滚失败记录日志,继续回滚其他步骤
			log.ErrorWithCtx(ctx.Ctx, "步骤回滚失败",
				zap.String("step", step.Name()),
				zap.Error(err),
				zap.Any("customer_id", ctx.Req.CustomerID),
			)
		}
	}
}
// 高德券处理服务
func (s *OrderService) CreateCouponOrder(ctx *CouponOrderPipelineContext) (*CouponOrderPipelineContext, error) {
    // 初始化优惠券订单管道
    pipeline := NewCouponOrderPipeline()
    
    // 注册优惠券订单步骤
    pipeline.RegisterStep(&ProductCouponValidateStep{})         // 校验优惠券
    pipeline.RegisterStep(&ProductCouponThirdPartyRedeemStep{}) // 调用第三方接口核销券
    pipeline.RegisterStep(&ProductCouponLocalRedeemStep{})      // 本地核销券
    pipeline.RegisterStep(&ProductCouponAssembleCartStep{})     // 组装购物车
    pipeline.RegisterStep(&ProductCouponPreDeductStockStep{})   // 预扣库存
    pipeline.RegisterStep(&ProductCouponCreateOrderStep{})      // 创建订单
    
    // 运行优惠券订单流程
    err := pipeline.Run(ctx)
    if err != nil {
        return ctx, err
    }
    return ctx, nil
}

3.3 步骤实现示例:优惠券校验

go 复制代码
// 优惠券校验步骤
type ProductCouponValidateStep struct {
    CouponOrderStepBase
}

func (s *ProductCouponValidateStep) Name() string {
    return "ProductCouponValidateStep"
}

func (s *ProductCouponValidateStep) Execute(ctx *CouponOrderPipelineContext) error {
    // 如果没有优惠券,跳过校验
    if len(ctx.Req.CouponCode) == 0 {
        return nil
    }
    
    // 从选项函数中获取预加载的数据
    options := &CreateCouponOrderOptions{}
    ApplyCouponOrderOptions(options, ctx.Options...)
    
    // 使用错误组进行并行校验
    eg, egCtx := errgroup.WithContext(ctx.Ctx)
    
    for _, couponCode := range ctx.Req.CouponCode {
        // 只处理高德券
        if !isAmapCoupon(couponCode) {
            continue
        }
        
        // 获取优惠券信息
        couponInfo, couponRule := getCouponInfoByCode(couponCode, 
            options.MapCouponRelation, options.MapCouponRule)
        if couponInfo == nil || couponRule == nil {
            continue
        }
        
        // 存储到上下文供后续步骤使用
        ctx.CouponUserRelation.Store(couponCode, couponInfo)
        ctx.CouponRule.Store(couponRule.BatchId, couponRule)
        
        // 使用工厂模式创建校验器
        validator := coupondomain.NewCouponValidator(couponRule.Type)
        if validator == nil {
            continue
        }
        
        // 创建校验请求
        validateReq := coupondomain.CouponValidateReq{
            CouponCode:         couponCode,
            CustomerId:         ctx.Req.CustomerID,
            ShopID:             ctx.Req.ShopID,
            CouponUserRelation: couponInfo,
            CouponRule:         couponRule,
        }
        
        // 并行执行校验
        localValidator := validator
        localReq := validateReq
        eg.Go(func() error {
            _, err := localValidator.Validate(egCtx, localReq)
            return err
        })
    }
    
    return eg.Wait()
}

四、单元测试:重构带来的可测试性提升

4.1 步骤独立测试的优势

重构后的代码由于采用了管道模式和函数式选项模式,使得每个步骤都可以独立测试,大大提高了测试的便利性和覆盖率。

go 复制代码
// 测试ProductCouponValidateStep.Execute方法
func TestProductCouponValidateStep_Execute(t *testing.T) {
    // 测试场景1:没有优惠券,应该跳过校验
    t.Run("NoCouponCode", func(t *testing.T) {
        ctx := &CouponOrderPipelineContext{
            Req: coupon_define.CreateCouponOrderReq{CouponCode: []string{}},
        }
        step := &ProductCouponValidateStep{}
        err := step.Execute(ctx)
        assert.NoError(t, err)
        assert.Empty(t, ctx.CouponUserRelation)
    })
    
    // 测试场景2:校验成功
    t.Run("ValidateSuccess", func(t *testing.T) {
        // 使用mock工具模拟工厂函数
        patches := gomonkey.NewPatches()
        defer patches.Reset()
        
        // mock工厂函数返回mock校验器
        patches.ApplyFunc(coupondomain.NewCouponValidator, func(couponType int) coupondomain.CouponValidator {
            return &MockCouponValidator{success: true}
        })
        
        // 创建测试上下文
        couponUserRelationMap := sync.Map{}
        couponUserRelationMap.Store("valid_coupon", &models.CouponUserRelation{
            CouponCode: "valid_coupon",
            BatchId:    "valid_batch_id",
        })
        
        couponRuleMap := sync.Map{}
        couponRuleMap.Store("valid_batch_id", &coupon_rule_define.UpsertCouponRuleResp{
            Type:    e.CouponType.AmapProduct,
            BatchId: "valid_batch_id",
        })
        
        ctx := &CouponOrderPipelineContext{
            Req: coupon_define.CreateCouponOrderReq{
                CustomerID: 123456789,
                ShopID:     123,
                CouponCode: []string{"valid_coupon"},
            },
            Options: []CouponOrderOptionFunc{
                WithCouponOrderCouponUserRelation(couponUserRelationMap),
                WithCouponOrderCouponRule(couponRuleMap),
            },
            CouponUserRelation: &sync.Map{},
            CouponRule:         &sync.Map{},
        }
        
        step := &ProductCouponValidateStep{}
        err := step.Execute(ctx)
        assert.NoError(t, err)
        
        // 验证优惠券信息被正确存储
        _, couponExists := ctx.CouponUserRelation.Load("valid_coupon")
        _, ruleExists := ctx.CouponRule.Load("valid_batch_id")
        assert.True(t, couponExists)
        assert.True(t, ruleExists)
    })
    
    // 更多测试场景:校验失败、多个优惠券处理、部分失败等
    // ...
}

4.2 Mock工具的使用

这个github.com/agiledragon/gomonkey 工具提供了非常优雅的mock方案。详细用法参考这篇文章《优雅地Mock时间:gomonkey实现time.Now()替换的原理与实践

go 复制代码
// MockCouponValidator 模拟CouponValidator接口
type MockCouponValidator struct {
    success bool
    err     error
}

func (v *MockCouponValidator) Validate(ctx context.Context, req coupondomain.CouponValidateReq) (coupondomain.CouponInfo, error) {
    if !v.success && v.err != nil {
        return coupondomain.CouponInfo{}, v.err
    }
    return coupondomain.CouponInfo{
        CouponUserRelation: *req.CouponUserRelation,
        CouponRule:         *req.CouponRule,
    }, nil
}

func (v *MockCouponValidator) Name() string {
    return "MockCouponValidator"
}

4.3 测试覆盖率提升

通过步骤独立测试,我们可以实现:

  1. 高覆盖率:每个步骤的多种场景都可以覆盖
  2. 快速反馈:测试运行速度快,开发效率高
  3. 精准定位:问题可以精准定位到具体步骤
  4. 回归保护:确保重构不影响原有功能

五、数据传递机制:函数式选项模式的应用

5.1 选项函数定义

go 复制代码
// 选项配置结构体
type CreateCouponOrderOptions struct {
    MapCouponRelation sync.Map // 优惠券用户关系
    MapCouponRule     sync.Map // 优惠券规则
    GoodsView         *category_define.CategoryResp // 商品详情
}

// 选项函数类型
type CouponOrderOptionFunc func(*CreateCouponOrderOptions)

// 商品详情选项函数
func WithCouponOrderGoodsView(goodsView *category_define.CategoryResp) CouponOrderOptionFunc {
    return func(o *CreateCouponOrderOptions) {
        o.GoodsView = goodsView
    }
}

// 优惠券用户关系选项函数
func WithCouponOrderCouponUserRelation(couponUserRelation sync.Map) CouponOrderOptionFunc {
    return func(o *CreateCouponOrderOptions) {
        o.MapCouponRelation = couponUserRelation
    }
}

// 优惠券规则选项函数
func WithCouponOrderCouponRule(couponRule sync.Map) CouponOrderOptionFunc {
    return func(o *CreateCouponOrderOptions) {
        o.MapCouponRule = couponRule
    }
}

// 选项应用函数
func ApplyCouponOrderOptions(options *CreateCouponOrderOptions, opts ...CouponOrderOptionFunc) {
    for _, opt := range opts {
        opt(options)
    }
}

5.2 数据复用机制

go 复制代码
// 路由入口函数
func CreateCouponOrderWithRoute(ctx context.Context, req coupon_define.CreateCouponOrderReq,
    shopInfo models.Shops) (coupon_define.CreateCouponOrderResp, error) {
    
    // 构建选项函数,传递已查询的数据
    options := make([]CouponOrderOptionFunc, 0)
    
    // 如果已经查询过优惠券用户关系和规则,通过选项函数传递
    if couponUserRelation != nil {
        options = append(options, WithCouponOrderCouponUserRelation(couponUserRelation))
    }
    if couponRule != nil {
        options = append(options, WithCouponOrderCouponRule(couponRule))
    }
    
    log.InfoWithCtx(ctx, "创建团购券订单准备", zap.Any("req", req))
    
    // 第一步:调用原有方法处理所有券类型
    resp, newOpts, err := CreateCouponOrder(ctx, req, shopInfo, options...)
    if err != nil {
        return resp, err
    }
    
    // 第二步:高德券走新逻辑处理
    // 使用原有方法返回的选项函数(包含商品信息等)
    couponOrderPipeLineCtx := &CouponOrderPipelineContext{
        Ctx:      ctx,
        Req:      req,
        ShopInfo: shopInfo,
        Options:  newOpts,  // 使用原有方法查询的数据
    }
    
    // 调用新逻辑处理高德券
    couponOrderPipeLineCtx, err = orderService.CreateCouponOrder(couponOrderPipeLineCtx)
    if err != nil {
        // 如果失败,只需要回滚新逻辑的订单
        // 原有逻辑的订单保持不变
        log.ErrorWithCtx(ctx, "高德券订单处理失败", zap.Error(err))
        return resp, err
    }
    
    // 合并结果
    // ... 合并新旧逻辑的结果
    
    return resp, nil
}

六、渐进式迁移策略

6.1 第一阶段:新旧逻辑并存

go 复制代码
// 第一阶段:高德券走新逻辑,其他券走旧逻辑
// 路由入口函数
func CreateCouponOrderWithRoute(ctx context.Context, req coupon_define.CreateCouponOrderReq,
    shopInfo models.Shops) (coupon_define.CreateCouponOrderResp, error) {
    
    // 构建选项函数,传递已查询的数据
    options := make([]CouponOrderOptionFunc, 0)
    
    // 如果已经查询过优惠券用户关系和规则,通过选项函数传递
    if couponUserRelation != nil {
        options = append(options, WithCouponOrderCouponUserRelation(couponUserRelation))
    }
    if couponRule != nil {
        options = append(options, WithCouponOrderCouponRule(couponRule))
    }
    
    log.InfoWithCtx(ctx, "创建团购券订单准备", zap.Any("req", req))
    
    // 第一步:调用原有方法处理所有券类型
    resp, newOpts, err := CreateCouponOrder(ctx, req, shopInfo, options...)
    if err != nil {
        return resp, err
    }
    
    // 第二步:高德券走新逻辑处理
    // 使用原有方法返回的选项函数(包含商品信息等)
    couponOrderPipeLineCtx := &CouponOrderPipelineContext{
        Ctx:      ctx,
        Req:      req,
        ShopInfo: shopInfo,
        Options:  newOpts,  // 使用原有方法查询的数据
    }
    
    // 调用新逻辑处理高德券
    couponOrderPipeLineCtx, err = orderService.CreateCouponOrder(couponOrderPipeLineCtx)
    if err != nil {
        // 如果失败,只需要回滚新逻辑的订单
        // 原有逻辑的订单保持不变
        log.ErrorWithCtx(ctx, "高德券订单处理失败", zap.Error(err))
        return resp, err
    }
    
    // 合并结果
    // ... 合并新旧逻辑的结果
    
    return resp, nil
}

6.2 第二阶段:逐步迁移其他券类型

go 复制代码
// 第二阶段:通过功能开关控制
func CreateCouponOrderV2(ctx context.Context, req coupon_define.CreateCouponOrderReq,
   
}

6.3 第三阶段:统一架构

go 复制代码
// 第三阶段:所有券类型都支持新架构
func CreateCouponOrderV3(ctx context.Context, req coupon_define.CreateCouponOrderReq,
    shopInfo models.Shops) (coupon_define.CreateCouponOrderResp, error) {
    
     couponOrderPipeLineCtx, err = orderService.CreateCouponOrder(couponOrderPipeLineCtx)
    if err != nil {
        // 如果失败,只需要回滚新逻辑的订单
        // 原有逻辑的订单保持不变
        log.ErrorWithCtx(ctx, "高德券订单处理失败", zap.Error(err))
        return resp, err
    }
}

七、总结与收益

7.1 技术收益

  1. 架构清晰:按券类型路由,职责分离明确
  2. 渐进式重构:支持平滑迁移,降低风险
  3. 代码复用:公共数据通过选项函数复用
  4. 可测试性:管道步骤独立,易于单元测试
  5. 可扩展性:新增券类型只需添加新处理器

7.2 业务收益

  1. 快速上线:高德券可以快速上线,不影响原有业务
  2. 风险可控:新旧逻辑并行,可以快速回滚
  3. 性能优化:避免重复查询,提升处理效率
  4. 维护便利:代码结构清晰,新人上手快

7.3 关键成功因素

  1. 正确的路由策略:基于券类型的路由是架构核心
  2. 数据传递机制:选项函数模式实现数据复用
  3. 渐进式迁移:三个阶段平滑过渡
  4. 完备的测试:单元测试确保重构质量

通过这种按券类型路由的渐进式重构架构,我们既实现了新功能快速上线,又保证了系统整体稳定性,为后续的业务扩展奠定了良好的架构基础。这种模式不仅适用于优惠券订单场景,对于其他复杂的业务流程重构也具有很好的参考价值。

相关推荐
有味道的男人5 小时前
国内电商 API 深度赋能:从选品、库存到履约,重构电商运营效率新范式
大数据·重构
有一个好名字5 小时前
设计模式-单例模式
java·单例模式·设计模式
AI科技星5 小时前
质量定义方程的物理数学融合与求导验证
数据结构·人工智能·算法·机器学习·重构
顾安r5 小时前
12.17 脚本工具 自动化全局跳转
linux·前端·css·golang·html
赵得C6 小时前
2025下半年软件设计师考前几页纸
java·开发语言·分布式·设计模式·性能优化·软考·软件设计师
GEO AI搜索优化助手6 小时前
未来图景:信息传播链的生态重构与长期影响
人工智能·搜索引擎·重构·生成式引擎优化·ai优化·geo搜索优化
alibli6 小时前
Alibli深度理解设计模式系列教程
c++·设计模式
Henry_Wu0017 小时前
go与c# 及nats和rabbitmq交互
golang·c#·rabbitmq·grpc·nats
Asus.Blogs7 小时前
golang格式化打印json
javascript·golang·json