👋 大家好,我是十三!
在上一篇 《活动架构的“第一性原理”》 中,我们回归本质,建立了任务、资格、奖品三大核心基石。它们如同三块坚固的“乐高积木”,为我们提供了稳固的原子能力。
但只有零件还不够。当业务方带着层出不穷的新玩法(“大转盘抽奖”、“签到领积分”、“玩游戏解锁皮肤”)冲过来时,我们如何快速、优雅地将这些“积木”拼装起来,满足需求?这,就是本篇要解决的核心问题:构建一个能驱动业务百变的“玩法编排引擎”。
1. 问题的提出:从“积木”到“成品”的挑战 🧩
我们已经有了三大独立的服务中心,现在,让我们直面第一个需求:“实现一个大转盘抽奖玩法”。
这个流程听起来很简单:
- 消耗用户一张“抽奖券”资格。
- 从奖池中随机抽取一个奖品。
- 为用户发放抽中的奖品。
1.1. 反模式警示:万恶的 if-else 地狱 🔥
最直观、最“朴素”的想法,就是创建一个ActivityLogic,然后用if-else来处理不同的活动类型。
// internal/logic/activity_logic.go
func (l *ActivityLogic) Execute(req *types.ActivityRequest) (*types.ActivityResponse, error) {
// 如果是抽奖活动
if req.ActivityType == "LOTTERY" {
// 1. 消耗资格
if err := l.svcCtx.QualificationService.Consume(l.ctx, req.UserId, "lottery_ticket", 1); err != nil {
return nil, err
}
// 2. 抽奖 & 发奖...
prize, err := l.svcCtx.PrizeService.DrawAndIssue(...)
// ...
return &types.ActivityResponse{Prize: prize}, nil
// 如果是签到领积分活动
} else if req.ActivityType == "SIGN_IN_REWARD" {
// 1. 检查是否已签到 (调用任务中心)
taskStatus, err := l.svcCtx.TaskService.GetTaskStatus(l.ctx, req.UserId, "daily_sign_in")
if !taskStatus.IsCompleted {
return nil, errors.New("任务未完成")
}
// 2. 发放固定积分 (调用奖品中心)
err := l.svcCtx.PrizeService.Issue(l.ctx, req.UserId, "points_10")
// ...
return &types.ActivityResponse{Message: "积分已发放"}, nil
// 如果未来还有N个活动...
} else if req.ActivityType == "..." {
// ... 无穷无尽的 else if
}
return nil, errors.New("不支持的活动类型")
}
这种写法的危害是毁灭性的:
- 违反开闭原则:每次新增一种玩法,都必须冒着风险修改这个巨大的
ActivityLogic文件。 - 职责不清晰:
ActivityLogic成了一个“上帝类”,它知道所有玩法的实现细节,耦合了所有依赖。 - 复杂度爆炸:随着
if-else分支的增多,代码的阅读、维护、测试成本呈指数级增长。
用不了多久,这里就会变成团队中人人避之不及的“代码禁区”。我们必须寻找一种更优雅的方案。
2. 核心设计:拥抱变化的“玩法编排引擎” 🚀
2.1. 架构原则:开闭原则 (Open/Closed Principle)
破局的指导思想,是软件设计中最核心的原则之一:开闭原则。
软件实体(类、模块、函数)应该对扩展开放,对修改关闭。
换言之,我们的目标是:当新增一种玩法时,我们应该通过增加新代码(扩展)来完成,而不是修改已有的旧代码(修改)。
2.2. 设计模式的选择:策略模式 (Strategy Pattern)
策略模式是实现开闭原则的绝佳武器。它的定义是:
定义一系列算法,把它们一个个封装起来,并且使它们可以相互替换。
在我们的场景中:
- 一系列算法:就是各种不同的活动玩法(抽奖、签到等)。
- 封装:将每种玩法的业务逻辑,封装到独立的
struct中。 - 相互替换:我们的引擎可以根据活动类型,自由地切换和执行不同的玩法
struct。
架构图 (UML):
classDiagram
class ActivityEngine {
-strategies: map
+Register(type, strategy)
+Execute(type, context)
}
class IActivityStrategy {
<<interface>>
+Execute(context) ActivityResponse
}
class LotteryStrategy {
+Execute(context) ActivityResponse
}
class SignInRewardStrategy {
+Execute(context) ActivityResponse
}
ActivityEngine o-- IActivityStrategy : uses
LotteryStrategy ..|> IActivityStrategy : implements
SignInRewardStrategy ..|> IActivityStrategy : implements
2.3. Go 实现:构建引擎骨架
1. 定义策略接口 IActivityStrategy:
// internal/engine/strategy.go
package engine
type ActivityContext struct {
UserId int64
ActivityId int64
// ... 其他上下文需要的信息
}
type ActivityResponse struct {
// ... 玩法执行结果
}
// IActivityStrategy a.k.a. 玩法策略接口
type IActivityStrategy interface {
Execute(ctx context.Context, activityCtx *ActivityContext) (*ActivityResponse, error)
}
2. 实现玩法引擎 ActivityEngine:
// internal/engine/engine.go
package engine
type ActivityEngine struct {
logx.Logger
svcCtx *svc.ServiceContext
strategies map[string]IActivityStrategy // 存储所有已注册的玩法策略
}
func NewActivityEngine(svcCtx *svc.ServiceContext) *ActivityEngine {
return &ActivityEngine{
svcCtx: svcCtx,
strategies: make(map[string]IActivityStrategy),
}
}
// Register 注册玩法策略
func (e *ActivityEngine) Register(activityType string, strategy IActivityStrategy) {
e.strategies[activityType] = strategy
}
// Execute 根据活动类型,执行对应的策略
func (e *ActivityEngine) Execute(ctx context.Context, activityType string, activityCtx *ActivityContext) (*ActivityResponse, error) {
strategy, ok := e.strategies[activityType]
if !ok {
return nil, errors.New("unsupported activity type")
}
return strategy.Execute(ctx, activityCtx)
}
通过这个设计,ActivityEngine本身变得非常稳定,它不关心任何具体玩法。它的唯一职责,就是作为一个“路由器”,根据activityType将请求转发给正确的strategy实现。
3. 实战演练:编排两种典型玩法 🛠️
现在,我们来实现两种具体的玩法策略。
3.1. 具体策略一:大转盘抽奖 (LotteryStrategy)
// internal/engine/lottery_strategy.go
package engine
// LotteryStrategy 实现了 IActivityStrategy 接口
type LotteryStrategy struct {
logx.Logger
svcCtx *svc.ServiceContext
}
func (s *LotteryStrategy) Execute(ctx context.Context, activityCtx *ActivityContext) (*ActivityResponse, error) {
// 1. 编排第一步:调用“资格中心”,消耗一张抽奖券
if err := s.svcCtx.QualificationService.Consume(ctx, activityCtx.UserId, "lottery_ticket", 1); err != nil {
return nil, err // 资格不足或消耗失败
}
// 2. 编排第二步:调用“奖品中心”,执行抽奖并扣减库存
prize, err := s.svcCtx.PrizeService.DrawAndIssue(ctx, activityCtx.ActivityId, activityCtx.UserId)
if err != nil {
// 注意:这里的失败可能需要补偿逻辑(如返还资格)
return nil, err
}
// 3. 封装并返回结果
return &ActivityResponse{Prize: prize}, nil
}
交互时序图:
sequenceDiagram
participant Client
participant Engine as ActivityEngine
participant Strategy as LotteryStrategy
participant QualSvc as QualificationService
participant PrizeSvc as PrizeService
Client->>Engine: Execute("LOTTERY", context)
Engine->>Strategy: Execute(context)
Strategy->>QualSvc: Consume(userId, "lottery_ticket")
QualSvc-->>Strategy: OK
Strategy->>PrizeSvc: DrawAndIssue(activityId, userId)
PrizeSvc-->>Strategy: Prize
Strategy-->>Engine: ActivityResponse{Prize}
Engine-->>Client: ActivityResponse{Prize}
3.2. 具体策略二:签到领积分 (SignInRewardStrategy)
这个玩法更简单,没有随机性,是确定性的奖励。
// internal/engine/signin_reward_strategy.go
package engine
type SignInRewardStrategy struct {
logx.Logger
svcCtx *svc.ServiceContext
}
func (s *SignInRewardStrategy) Execute(ctx context.Context, activityCtx *ActivityContext) (*ActivityResponse, error) {
// 1. 编排第一步:检查“每日签到”任务是否已完成
// (注:通常此玩法由任务中心完成事件触发,这里只做演示)
taskStatus, err := s.svcCtx.TaskService.GetTaskStatus(ctx, activityCtx.UserId, "daily_sign_in")
if err != nil || !taskStatus.IsCompleted {
return nil, errors.New("任务未完成,无法领奖")
}
// 2. 编排第二步:调用“奖品中心”发放固定奖励
if err := s.svcCtx.PrizeService.Issue(ctx, activityCtx.UserId, "points_10"); err != nil {
return nil, err
}
return &ActivityResponse{Message: "10积分已到账!"}, nil
}
现在,我们只需要在服务启动时,将这些策略注册到引擎中即可:
// main.go (或 service/servicecontext.go)
engine := engine.NewActivityEngine(svcCtx)
engine.Register("LOTTERY", &engine.LotteryStrategy{svcCtx: svcCtx})
engine.Register("SIGN_IN_REWARD", &engine.SignInRewardStrategy{svcCtx: svcCtx})
4. 总结与展望
通过引入策略模式,我们构建了一个高度可扩展的玩法编排引擎。
本篇成果:✅
- 拥抱了变化: 现在新增一个“玩游戏领奖励”的玩法,只需要新增一个
PlayGameRewardStrategy文件并注册即可,完全无需改动任何现有代码。 - 职责更清晰:
ActivityEngine负责“路由”,具体的Strategy实现负责“编排”,底层的三大中心负责“执行”。各司其职,互不干扰。
最终架构图:
graph TD
subgraph "玩法编排引擎 (本篇核心)"
direction LR
A[ActivityEngine] --> B{IActivityStrategy};
C[LotteryStrategy] -- implements --> B;
D[SignInRewardStrategy] -- implements --> B;
E[...] -- implements --> B;
end
subgraph "三大核心基石 (第一篇)"
F[任务中心];
G[资格中心];
H[奖品中心];
end
A -- "调度" --> F;
A -- "调度" --> G;
A -- "调度" --> H;
style A fill:#9cf,stroke:#333
👨💻 关于十三Tech
资深服务端研发工程师,AI编程实践者。
专注分享真实的技术实践经验,相信AI是程序员的最佳搭档。
希望能和大家一起写出更优雅的代码!
📧 联系方式:569893882@qq.com
🌟 GitHub:@TriTechAI
💬 微信:TriTechAI(备注:十三Tech)
点个关注不迷路