引言:复杂业务场景下的重构挑战
在现代电商系统中,优惠券订单处理是一个典型的复杂业务场景,涉及多种券类型、多级校验、库存管理、订单创建等环节。随着业务发展,原有的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 重构目标与约束条件
- 业务连续性:不能影响线上原有券类型的处理
- 渐进式重构:支持平滑迁移,降低风险
- 数据复用:避免重复查询,提升性能
- 可测试性:提高代码的可测试性
- 可扩展性:便于新增业务逻辑和券类型
二、架构设计:按券类型路由的双逻辑并存
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 测试覆盖率提升
通过步骤独立测试,我们可以实现:
- 高覆盖率:每个步骤的多种场景都可以覆盖
- 快速反馈:测试运行速度快,开发效率高
- 精准定位:问题可以精准定位到具体步骤
- 回归保护:确保重构不影响原有功能
五、数据传递机制:函数式选项模式的应用
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 技术收益
- 架构清晰:按券类型路由,职责分离明确
- 渐进式重构:支持平滑迁移,降低风险
- 代码复用:公共数据通过选项函数复用
- 可测试性:管道步骤独立,易于单元测试
- 可扩展性:新增券类型只需添加新处理器
7.2 业务收益
- 快速上线:高德券可以快速上线,不影响原有业务
- 风险可控:新旧逻辑并行,可以快速回滚
- 性能优化:避免重复查询,提升处理效率
- 维护便利:代码结构清晰,新人上手快
7.3 关键成功因素
- 正确的路由策略:基于券类型的路由是架构核心
- 数据传递机制:选项函数模式实现数据复用
- 渐进式迁移:三个阶段平滑过渡
- 完备的测试:单元测试确保重构质量
通过这种按券类型路由的渐进式重构架构,我们既实现了新功能快速上线,又保证了系统整体稳定性,为后续的业务扩展奠定了良好的架构基础。这种模式不仅适用于优惠券订单场景,对于其他复杂的业务流程重构也具有很好的参考价值。