更多内容欢迎访问我的个人博客网站:www.zpf0000.com
在数据库中准备好以下数据表
lottery表
sql代码解读复制代码
DROP TABLE IF EXISTS `lottery`;
CREATE TABLE `lottery` (
`id` int NOT NULL AUTO_INCREMENT,
`user_id` int NOT NULL DEFAULT 0 COMMENT '发起抽奖用户ID',
`name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '默认取一等奖名称',
`thumb` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '默认取一等经配图',
`publish_time` datetime NULL DEFAULT NULL COMMENT '发布抽奖时间',
`join_number` int NOT NULL DEFAULT 0 COMMENT '自动开奖人数',
`introduce` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '抽奖说明',
`award_deadline` datetime NOT NULL COMMENT '领奖截止时间',
`is_selected` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否精选 1是 0否',
`announce_type` tinyint(1) NOT NULL DEFAULT 0 COMMENT '开奖设置:1按时间开奖 2按人数开奖 3即抽即中',
`announce_time` datetime NOT NULL DEFAULT NULL COMMENT '开奖时间',
`del_state` tinyint NOT NULL DEFAULT '0',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`delete_time` timestamp NULL DEFAULT NULL COMMENT '删除时间',
`is_announced` tinyint(1) NULL DEFAULT 0 COMMENT '是否开奖:0未开奖;1已经开奖',
`sponsor_id` int NOT NULL DEFAULT 0 COMMENT '发起抽奖赞助商ID',
`is_clocked` tinyint(1) NULL DEFAULT 0 COMMENT '是否开启打卡任务:0未开启;1已开启',
`clock_task_id` int NOT NULL DEFAULT 0 COMMENT '打卡任务任务ID',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 111 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '抽奖表' ROW_FORMAT = DYNAMIC;
SET FOREIGN_KEY_CHECKS = 1;
奖品表
sql代码解读复制代码
DROP TABLE IF EXISTS `prize`;
CREATE TABLE `prize` (
`id` int(0) NOT NULL AUTO_INCREMENT,
`lottery_id` int(0) NOT NULL DEFAULT 0 COMMENT '抽奖ID',
`type` tinyint(1) NOT NULL DEFAULT 0 COMMENT '奖品类型:1奖品 2优惠券 3兑换码 4商城 5微信红包封面 6红包',
`name` varchar(24) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '奖品名称',
`level` int(0) NOT NULL DEFAULT 1 COMMENT '几等奖 默认1',
`thumb` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '奖品图',
`count` int(0) NOT NULL DEFAULT 0 COMMENT '奖品份数',
`grant_type` tinyint(1) NOT NULL COMMENT '奖品发放方式:1快递邮寄 2让中奖者联系我 3中奖者填写信息 4跳转到其他小程序',
`create_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0),
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 25 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '奖品表' ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
参与抽奖表
sql代码解读复制代码
CREATE TABLE lottery_participation
(
id BIGINT AUTO_INCREMENT COMMENT '主键'
PRIMARY KEY,
lottery_id INT NOT NULL COMMENT '参与的抽奖的id',
user_id INT NOT NULL COMMENT '用户id',
is_won TINYINT NOT NULL COMMENT '中奖了吗?',
prize_id BIGINT NOT NULL COMMENT '中奖id',
del_state tinyint NOT NULL DEFAULT '0',
create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT index_lottery_user UNIQUE (lottery_id, user_id)
)
COMMENT '参与抽奖' COLLATE = utf8mb4_general_ci;
设计思路
我们的抽奖算法将基于两种策略:基于时间的抽奖策略和基于人数的抽奖策略。
在基于时间的抽奖策略中,抽奖的开奖时间将作为一个重要的参考因素;
而在基于人数的抽奖策略中,参与抽奖的人数将决定中奖的概率。
针对这些不同的策略,我们很容易联想到使用策略模式来实现不同开奖算法的调用,最终通过RPC调用与其他服务进行通信。
实现步骤
定义RPC服务
步骤一:定义RPC请求、回复、远程服务的函数名
可以看见,我们首先定义了Req和Resp
protobuf代码解读复制代码
message AnnounceLotteryReq {
int64 AnnounceType = 1;
}
message AnnounceLotteryResp {
}
rpc AnnounceLottery(AnnounceLotteryReq) returns (AnnounceLotteryResp);
步骤二:生成RPC服务模板
sh代码解读复制代码
goctl rpc protoc lottery.proto --go_out=./ --go-grpc_out=./ --zrpc_out=./ --style=goZero
策略模式
策略模式是一种行为设计模式,它允许在运行时选择算法的行为。它将一组算法封装在独立的类中,并使它们可以互换使用,从而使算法的变化独立于使用算法的客户端。
在策略模式中,通常有三个核心角色:
- 环境(Context):环境对象是使用策略的主体,它持有一个策略对象的引用,并在需要执行特定行为时将请求委派给策略对象。
- 策略(Strategy):策略是定义算法接口的共同接口或抽象类,它封装了具体的算法实现。
- 具体策略(Concrete Strategy):具体策略是策略的具体实现类,它实现了策略接口中定义的算法。
策略模式的核心思想是将可变的行为封装在独立的策略类中,使得这些策略类可以互换使用。这样,在需要变更行为时,只需要替换相应的策略对象,而不需要修改环境对象或其他客户端代码。
步骤一:定义抽奖策略接口
首先,在代码中定义了一个抽奖策略接口 LotteryStrategy
,该接口声明了一个 Run()
方法用于执行抽奖策略。
go代码解读复制代码
type LotteryStrategy interface {
Run() error
}
步骤二:定义具体抽奖策略类型
接下来,代码实现了两个基于不同策略的具体抽奖策略类型:TimeLotteryStrategy
和 PeopleLotteryStrategy
。
TimeLotteryStrategy
结构体实现了基于时间的抽奖策略。它包含了 AnnounceLotteryLogic
的引用,以及当前时间信息。
go代码解读复制代码
type TimeLotteryStrategy struct {
*AnnounceLotteryLogic
CurrentTime time.Time
}
PeopleLotteryStrategy
结构体实现了基于人数的抽奖策略。它也包含了 AnnounceLotteryLogic
的引用,以及当前时间信息。
go代码解读复制代码
type PeopleLotteryStrategy struct {
*AnnounceLotteryLogic
CurrentTime time.Time
}
步骤三:根据传入的抽奖类型选择相应的策略进行抽奖
在 AnnounceLotteryLogic
结构体中,根据传入的抽奖类型选择相应的策略进行抽奖。 AnnounceLotteryLogic
结构体拥有一个 AnnounceLottery()
方法,该方法接收一个 pb.AnnounceLotteryReq
参数,并返回一个 *pb.AnnounceLotteryResp
和一个错误。
go代码解读复制代码
func (l *AnnounceLotteryLogic) AnnounceLottery(in *pb.AnnounceLotteryReq) (*pb.AnnounceLotteryResp, error) {
var strategy LotteryStrategy
switch in.AnnounceType {
case constants.AnnounceTypeTimeLottery:
strategy = &TimeLotteryStrategy{
AnnounceLotteryLogic: l,
CurrentTime: time.Now(),
}
case constants.AnnounceTypePeopleLottery:
strategy = &PeopleLotteryStrategy{
AnnounceLotteryLogic: l,
CurrentTime: time.Now(),
}
}
err := strategy.Run()
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.AnnounceLottery_ERROR), "AnnounceStrategy run error: %v", err)
}
return &pb.AnnounceLotteryResp{}, nil
}
通过使用策略模式,可以根据不同的抽奖类型选择不同的策略进行开奖操作,而不需要在主逻辑中编写大量的条件语句来处理不同的策略。这样可以使代码结构更加清晰和可扩展,方便添加新的抽奖策略类型。
抽奖算法实现
步骤一:定义抽奖策略实现方法
现在,我们可以分别实现基于时间的抽奖策略和基于人数的抽奖策略的具体逻辑。在这些策略中,我们可以根据业务需求编写相应的代码,例如根据时间计算开奖结果或根据参与人数计算中奖概率。以下是部分代码示例:
go代码解读复制代码
type TimeLotteryStrategy struct {
*AnnounceLotteryLogic
CurrentTime time.Time
}
func (s *TimeLotteryStrategy) Run() error {
// 基于时间的抽奖逻辑实现
// TODO: 根据时间计算开奖结果
return nil
}
type PeopleLotteryStrategy struct {
*AnnounceLotteryLogic
CurrentTime time.Time
}
func (s *PeopleLotteryStrategy) Run() error {
// 基于人数的抽奖逻辑实现
// TODO: 根据参与人数计算中奖概率
return nil
}
步骤二:定义Winner结构体
目前我们的开奖业务有三个结构体,分别是AnnounceLotteryLogic、TimeLotteryStrategy、PeopleLotteryStrategy。分别表示业务逻辑结构体,基于时间的抽奖逻辑,基于人数的抽奖逻辑;我们将之后所有通用的业务方法挂名在AnnounceLotteryLogic下,方便后续调用。
定义Winner结构体用于存储中奖者的信息,包括抽奖id、用户id、奖品id。
go代码解读复制代码
type Winner struct {
LotteryId int64
UserId int64
PrizeId int64
}
步骤三:从参与者中随机选择中奖者,并分配奖品
该方法是一个公用方法,挂名在AnnounceLotteryLogic。输入参数包括抽奖id、奖品列表、参与者列表,输出参数包括中奖者列表。该方法首先计算每个参与者的中奖概率,然后根据中奖概率随机选择中奖者,最后分配奖品给中奖者。
go代码解读复制代码
func (l *AnnounceLotteryLogic) DrawLottery(ctx context.Context, lotteryId int64, prizes []*model.Prize, participantor []int64) ([]Winner, error) {
// 随机选择中奖者
rand.New(rand.NewSource(time.Now().UnixNano()))
// 获取奖品总数 = 中奖人数
var WinnersNum int64
for _, p := range prizes {
WinnersNum += p.Count
}
winners := make([]Winner, 0)
records, err := l.svcCtx.ClockTaskRecordModel.GetClockTaskRecordByLotteryIdAndUserIds(lotteryId, participantor)
if err != nil {
return nil, err
}
// 查出来可能有多条记录 每条记录就是完成的一次任务 increase_multiple就是那一次任务所增加的概率,一个用户可能有多条记录,我这边在业务里面再进行统计一次
// 所以用一个map来存储每个用户的中奖倍率
RationsMap := make(map[int64]int64)
for _, participant := range participantor {
RationsMap[participant] = 1
}
for _, record := range records {
RationsMap[record.UserId] += record.IncreaseMultiple
}
Ratios := make([]int64, len(participantor))
for i, participant := range participantor {
Ratios[i] = RationsMap[participant]
}
// 计算总的中奖概率
totalRatio := int64(0)
for _, ratio := range Ratios {
totalRatio += ratio
}
// 计算每个用户的最终中奖概率
FinalRatios := make([]float64, len(participantor))
for idx := range Ratios {
FinalRatios[idx] = float64(Ratios[idx]) / float64(totalRatio)
}
// 根据中奖总数量进行开奖
for i := 0; i < int(WinnersNum); i++ { // 中奖人数
var randomWinnerIndex int
var winnerUserId int64
//如果参与者少于预计中奖人数,就结束开奖。(参与人数 < 中奖人数)
if len(participantor) == 0 {
break
}
//生成一个0到1之间的随机数
randomProbability := rand.Float64()
// 根据随机数确定中奖用户
probabilitySum := 0.0
for idx := range participantor {
// 逐个累加中奖概率,直到大于随机数
probabilitySum += FinalRatios[idx]
// 如果随机数小于等于累加的概率,说明中奖
if randomProbability <= probabilitySum {
// 中奖者的uid
winnerUserId = participantor[idx]
// 中奖者的索引
randomWinnerIndex = idx
break
}
}
//fmt.Println("winnerUserId:", winnerUserId)
//如果没有中奖用户,则第一个参与者中奖
if winnerUserId == 0 {
winnerUserId = participantor[0]
//fmt.Println("没有中奖用户,默认第一个参与者中奖", winnerUserId)
}
// 对所有prizes按照type排序 // todo 获取的时候能保证type有序吗?有序则可以不用排序了
sort.Slice(prizes, func(i, j int) bool {
return prizes[i].Type < prizes[j].Type
})
// 如果当前等级的奖品的剩余数量都为0,去掉,获取下一等级的奖品。
if prizes[0].Count == 0 {
prizes = prizes[1:]
}
prizes[0].Count--
prizeId := prizes[0].Id
// 创建中奖者对象
winner := Winner{
LotteryId: lotteryId,
UserId: winnerUserId,
PrizeId: prizeId, // 使用选中的奖品名称
}
winners = append(winners, winner)
// 从参与者列表中移除已中奖的用户以及对应的中奖概率
participantor = append(participantor[:randomWinnerIndex], participantor[randomWinnerIndex+1:]...)
FinalRatios = append(FinalRatios[:randomWinnerIndex], FinalRatios[randomWinnerIndex+1:]...)
}
return winners, nil
}
步骤四:实现按时间开奖业务逻辑
- 首先,通过调用
s.svcCtx.LotteryModel.GetLotterysByLessThanCurrentTime
方法查询满足条件的抽奖活动。该方法返回了一组满足条件的抽奖活动列表,存储在lotteries
变量中。 - 对于每一个抽奖活动,进行以下操作:
- a. 创建一个空的
participators
数组,用于存储参与者的ID。 - b. 开启一个数据库事务,通过
s.svcCtx.LotteryModel.Trans
方法进行事务处理。 - c. 根据抽奖活动ID(
lotteryId
),调用s.svcCtx.PrizeModel.FindByLotteryId
方法获取该抽奖活动对应的所有奖品列表,存储在prizes
变量中。 - d. 调用
s.svcCtx.LotteryParticipationModel.GetParticipationUserIdsByLotteryId
方法获取参与该抽奖活动的用户ID列表,存储在participators
变量中。 - e. 调用
s.DrawLottery
方法进行抽奖操作,传入抽奖活动ID、奖品列表、参与者ID列表,并返回中奖者列表,存储在winners
变量中。 - f. 调用
s.svcCtx.LotteryModel.UpdateLotteryStatus
方法更新该抽奖活动的状态为"已开奖"。 - g. 调用
s.WriteWinnersToLotteryParticipation
方法将中奖者信息写入数据库。 - h. 提交事务,如果有任何错误发生,则回滚事务。
- i. 调用
s.NotifyParticipators
方法执行开奖结果通知任务,传入参与者ID列表和抽奖活动ID。 - 返回错误(如果有)。
go代码解读复制代码
// Run 按时间开奖业务逻辑
func (s *TimeLotteryStrategy) Run() error {
// 查询满足条件的抽奖
lotteries, err := s.svcCtx.LotteryModel.GetLotterysByLessThanCurrentTime(s.ctx, s.CurrentTime, constants.AnnounceTypeTimeLottery)
if err != nil {
return err
}
// 遍历每一个抽奖
for _, lotteryId := range lotteries {
var participators []int64
// 事务处理
err = s.svcCtx.LotteryModel.Trans(s.ctx, func(context context.Context, session sqlx.Session) error {
//根据抽奖id得到对应的所有奖品
prizes, err := s.svcCtx.PrizeModel.FindByLotteryId(s.ctx, lotteryId)
if err != nil {
return err
}
// 获取该lotteryId对应的所有参与者
participators, err = s.svcCtx.LotteryParticipationModel.GetParticipationUserIdsByLotteryId(s.ctx, lotteryId)
if err != nil {
return err
}
winners, err := s.DrawLottery(s.ctx, lotteryId, prizes, participators)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.AnnounceLottery_ERROR), "DrawLottery,lotteryId:%v,prizes:%v,participators:%v error: %v", lotteryId, prizes, participators, err)
}
//更新抽奖状态为"已开奖"
err = s.svcCtx.LotteryModel.UpdateLotteryStatus(s.ctx, lotteryId)
if err != nil {
return err
}
// 将得到的中奖信息,写入数据库participants
err = s.WriteWinnersToLotteryParticipation(winners)
if err != nil {
return err
}
return nil
})
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.AnnounceLottery_ERROR), "AnnounceLotteryTrans error: %v", err)
}
// 执行开奖结果通知任务
err := s.NotifyParticipators(participators, lotteryId)
if err != nil {
return err
}
}
return err
}
步骤五:实现按人数开奖业务逻辑
- 首先,通过调用
s.svcCtx.LotteryModel.GetTypeIs2AndIsNotAnnounceLotterys
方法查询开奖类型为2且尚未开奖的抽奖活动。该方法返回了一组满足条件的抽奖活动列表,存储在lotteries
变量中。 - 对查询到的抽奖活动列表进行检查,通过调用
s.CheckLottery
方法,传入抽奖活动列表,返回一个经过检查的抽奖活动列表CheckedLottery
,该列表可能会剔除一些不符合条件的抽奖活动。 - 对于每一个抽奖活动,进行以下操作:
- a. 创建一个空的
participators
数组,用于存储参与者的ID。 - b. 开启一个数据库事务,通过
s.svcCtx.LotteryModel.Trans
方法进行事务处理。 - c. 根据抽奖活动ID(
lottery.Id
),调用s.svcCtx.PrizeModel.FindByLotteryId
方法获取该抽奖活动对应的所有奖品列表,存储在prizes
变量中。 - d. 调用
s.svcCtx.LotteryParticipationModel.GetParticipationUserIdsByLotteryId
方法获取参与该抽奖活动的用户ID列表,存储在participators
变量中。 - e. 调用
s.DrawLottery
方法进行抽奖操作,传入抽奖活动ID、奖品列表、参与者ID列表,并返回中奖者列表,存储在winners
变量中。 - f. 调用
s.svcCtx.LotteryModel.UpdateLotteryStatus
方法更新该抽奖活动的状态为"已开奖"。 - g. 调用
s.WriteWinnersToLotteryParticipation
方法将中奖者信息写入数据库。 - h. 提交事务,如果有任何错误发生,则回滚事务。
- i. 调用
s.NotifyParticipators
方法执行开奖结果通知任务,传入参与者ID列表和抽奖活动ID。 - 返回错误(如果有)。
go代码解读复制代码
// Run 按人数开奖策略
func (s *PeopleLotteryStrategy) Run() error {
// 查询开奖类型为2并且没有开奖的所有抽奖
lotteries, err := s.svcCtx.LotteryModel.GetTypeIs2AndIsNotAnnounceLotterys(s.ctx, constants.AnnounceTypePeopleLottery)
if err != nil {
return err
}
CheckedLottery, err := s.CheckLottery(lotteries)
if err != nil {
return err
}
// 遍历每一个抽奖
for _, lottery := range CheckedLottery {
var participators []int64
// 事务处理
err = s.svcCtx.LotteryModel.Trans(s.ctx, func(context context.Context, session sqlx.Session) error {
//根据抽奖id得到对应的所有奖品
prizes, err := s.svcCtx.PrizeModel.FindByLotteryId(s.ctx, lottery.Id)
if err != nil {
return err
}
// 获取该lotteryId对应的所有参与者
participators, err = s.svcCtx.LotteryParticipationModel.GetParticipationUserIdsByLotteryId(s.ctx, lottery.Id)
if err != nil {
return err
}
winners, err := s.DrawLottery(s.ctx, lottery.Id, prizes, participators)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.AnnounceLottery_ERROR), "DrawLottery,lotteryId:%v,prizes:%v,participators:%v, error: %v", lottery.Id, prizes, participators, err)
}
//更新抽奖状态为"已开奖"
err = s.svcCtx.LotteryModel.UpdateLotteryStatus(s.ctx, lottery.Id)
if err != nil {
return err
}
// 将得到的中奖信息,写入数据库participants
err = s.WriteWinnersToLotteryParticipation(winners)
if err != nil {
return err
}
return nil
})
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.AnnounceLottery_ERROR), "AnnounceLotteryTrans error: %v", err)
}
// 执行开奖结果通知任务
err := s.NotifyParticipators(participators, lottery.Id)
if err != nil {
return err
}
}
return nil
}
go代码解读复制代码
func (s *PeopleLotteryStrategy) CheckLottery(lotteries []*model.Lottery) (CheckedLotterys []*model.Lottery, err error) {
// 筛选满足条件的抽奖
// 1. 超过当前时间的,即使没有满足人数条件也需要进行开奖
// 2. 当参与人数 >= 开奖人数,进行开奖
for _, l := range lotteries {
// l.AnnounceTime 小于等于 s.CurrentTime,即超过当前时间,需要开奖
if l.AnnounceTime.Before(s.CurrentTime) || l.AnnounceTime.Equal(s.CurrentTime) {
CheckedLotterys = append(CheckedLotterys, l)
} else {
ParticipatorCount, err := s.svcCtx.LotteryParticipationModel.GetParticipatorsCountByLotteryId(s.ctx, l.Id)
if err != nil {
return nil, err
}
// 检查参与人数是否达到指定人数
if ParticipatorCount >= l.JoinNumber {
CheckedLotterys = append(CheckedLotterys, l)
}
}
}
return
}
步骤六:实现根据中奖者列表,通知中奖者中奖的结果
该方法的输入参数包括中奖者列表、抽奖id,输出参数为空。通过调用notice服务的NoticeLotteryDraw方法,通知中奖者中奖的结果。
go代码解读复制代码
func (l *AnnounceLotteryLogic) NotifyParticipators(participators []int64, lotteryId int64) error {
fmt.Println("NotifyParticipators", participators, lotteryId)
_, err := l.svcCtx.NoticeRpc.NoticeLotteryDraw(l.ctx, ¬ice.NoticeLotteryDrawReq{
LotteryId: lotteryId,
UserIds: participators,
})
if err != nil {
return err
}
return nil
}
步骤七:更新参与抽奖表
该方法是更新中奖者的核心逻辑,根据中奖者列表,更新中奖者的中奖信息。该方法输入参数包括中奖者列表,输出参数为空。首先遍历中奖者列表,然后调用LotteryParticipationModel的UpdateWinners方法,更新中奖者的中奖信息。
go代码解读复制代码
func (l *AnnounceLotteryLogic) WriteWinnersToLotteryParticipation(winners []Winner) error {
for _, w := range winners {
err := l.svcCtx.LotteryParticipationModel.UpdateWinners(l.ctx, w.LotteryId, w.UserId, w.PrizeId)
if err != nil {
return err
}
}
return nil
}
总结
通过以上步骤,我们成功地使用Go-Zero框架实现了一个抽奖算法。
在这个过程中,我们利用了模板定制化功能来快速生成代码,并灵活地设计了抽奖策略接口和具体实现。我们还结合RPC调用和其他服务进行数据交互,使得抽奖算法更加完善和可扩展。
希望本文对你理解Go-Zero框架的使用和抽奖算法的设计有所帮助。通过这个实战案例,你可