package luckybox_m import ( "context" "fmt" "git.hilo.cn/hilo-common/domain" "git.hilo.cn/hilo-common/mylogrus" "git.hilo.cn/hilo-common/resource/mysql" "git.hilo.cn/hilo-common/resource/redisCli" "git.hilo.cn/hilo-common/utils" redis2 "github.com/go-redis/redis/v8" "gorm.io/gorm" "gorm.io/gorm/clause" "hilo-group/_const/enum/luckybox_e" "hilo-group/_const/redis_key" "hilo-group/domain/model" "hilo-group/myerr" "math/rand" "strconv" "strings" "time" ) type LuckyboxCycleSum struct { mysql.Entity *domain.Model `gorm:"-"` DiamondNum mysql.Num UserId mysql.ID Cycle mysql.Str } // 幸运盒子 type Luckybox struct { mysql.Entity *domain.Model `gorm:"-"` DiamondNum mysql.Num } // 幸运盒子奖励 type LuckyboxAward struct { mysql.Entity *domain.Model `gorm:"-"` //可能是id,也可能是diamondNum AwardN uint64 AwardType luckybox_e.AwardTypeLuckyboxAward PicUrl string DayN mysql.Num DayTotalN mysql.Num DayStr mysql.Str Probability mysql.Num IsUse mysql.YesNo IsPublicScreen mysql.YesNo IsScrollScreen mysql.YesNo IntervalN mysql.Num //间隔次数才能再次获取 ProtectionSecond mysql.Num // 保护的时长 ProtectionSum mysql.Num //保护的金额 ProtectionProbability mysql.Num //变成的概率 } func LuckyboxAwardInit(model *domain.Model, awardN mysql.ID, awardType luckybox_e.AwardTypeLuckyboxAward, picUrl string, dayN mysql.Num, dayTotalN mysql.Num, probability mysql.Num, isUse mysql.YesNo, isPublicScreen mysql.YesNo, isScrollScreen mysql.YesNo, intervalN mysql.Num) *LuckyboxAward { return &LuckyboxAward{ Model: model, AwardN: awardN, AwardType: awardType, PicUrl: picUrl, DayN: dayN, DayTotalN: dayTotalN, DayStr: time.Now().Format(utils.COMPACT_DATE_FORMAT), Probability: probability, IsUse: isUse, IsPublicScreen: isPublicScreen, IsScrollScreen: isScrollScreen, IntervalN: intervalN, } } func GetLuckyboxAwardNilOrErr(model *domain.Model, id mysql.ID) (*LuckyboxAward, error) { luckyboxAward := LuckyboxAward{} if err := model.Db.Model(&LuckyboxAward{}).First(&luckyboxAward, id).Error; err != nil { return nil, myerr.WrapErr(err) } luckyboxAward.Model = model return &luckyboxAward, nil } func (luckyboxAward *LuckyboxAward) Edit(awardN mysql.ID, awardType luckybox_e.AwardTypeLuckyboxAward, picUrl string, dayN mysql.Num, dayTotalN mysql.Num, probability mysql.Num, isUse mysql.YesNo, isPublicScreen mysql.YesNo, isScrollScreen mysql.YesNo, intervalN mysql.Num) *LuckyboxAward { luckyboxAward.AwardN = awardN luckyboxAward.AwardType = awardType luckyboxAward.PicUrl = picUrl luckyboxAward.DayN = dayN luckyboxAward.DayTotalN = dayTotalN luckyboxAward.Probability = probability luckyboxAward.IsUse = isUse luckyboxAward.IsPublicScreen = isPublicScreen luckyboxAward.IsScrollScreen = isScrollScreen luckyboxAward.IntervalN = intervalN return luckyboxAward } // 幸运盒子用户抽奖记录 type LuckyboxUser struct { mysql.Entity *domain.Model `gorm:"-"` UserId mysql.ID // 0 代表没有中奖 LuckyboxAwardId mysql.ID //0 代表 没有中奖 AwardN uint64 //0 代表 没有中奖 AwardType luckybox_e.AwardTypeLuckyboxAward //购买幸运小盒子所需的钻石 BuyDiamond mysql.Num //奖励价值 AwardDiamond mysql.Num } // 获取最大的分数 func GetMaxLuckyboxDiamond(model *domain.Model, userId mysql.ID) (mysql.Num, error) { luckyboxUser := LuckyboxUser{} if err := model.Db.Model(&LuckyboxUser{}).Where(&LuckyboxUser{ UserId: userId, }).Order("award_diamond desc").First(&luckyboxUser).Error; err != nil { if err == gorm.ErrRecordNotFound { return 0, nil } else { return 0, myerr.WrapErr(err) } } return luckyboxUser.AwardDiamond, nil } func GetSumLuckyboxDiamond(model *domain.Model, userId mysql.ID) (uint64, error) { type summary struct { Sum uint64 } result := summary{} err := model.Db.Model(&LuckyboxUser{}). Select("SUM(award_diamond) AS sum"). Where(&LuckyboxUser{ UserId: userId, }). First(&result).Error if err != nil { return 0, err } return result.Sum, err } func GetSumLuckyboxDiamondV2(_model *domain.Model, userId mysql.ID) (uint64, error) { var luckyboxTotalUser LuckyboxTotalUser if err := _model.Db.Model(LuckyboxTotalUser{}).Where("user_id = ?", userId). First(&luckyboxTotalUser).Error; err != nil { if err != gorm.ErrRecordNotFound { return 0, err } else { // gorm.RecordNotFound luckyboxTotalUser.UserId = userId } } if luckyboxTotalUser.Sync == mysql.YES { return uint64(luckyboxTotalUser.AwardDiamond), nil } // sync before var awardDiamond, awardN, buyDiamond uint64 type summary struct { AwardN uint64 BuyDiamond uint64 AwardDiamond uint64 } result := summary{} err := _model.Db.Model(&LuckyboxUser{}). Select("SUM(award_diamond) AS award_diamond,SUM(award_n) as award_n,SUM(buy_diamond) as buy_diamond"). Where(&LuckyboxUser{ UserId: userId, }). First(&result).Error if err != nil { return 0, err } awardDiamond += result.AwardDiamond awardN += result.AwardN buyDiamond += result.BuyDiamond // 备份表 for i := 3; i <= 7; i++ { table := fmt.Sprintf("luckybox_user_20220%d", i) result := summary{} err := _model.Db.Table(table). Select("SUM(award_diamond) AS award_diamond,SUM(award_n) as award_n,SUM(buy_diamond) as buy_diamond"). Where(&LuckyboxUser{ UserId: userId, }). First(&result).Error if err != nil { return 0, err } awardDiamond += result.AwardDiamond awardN += result.AwardN buyDiamond += result.BuyDiamond } // 回写总表 luckyboxTotalUser.Sync = mysql.YES luckyboxTotalUser.BuyDiamond = mysql.Num(buyDiamond) luckyboxTotalUser.AwardDiamond = mysql.Num(awardDiamond) luckyboxTotalUser.AwardN = awardN if err := model.Persistent(mysql.Db, &luckyboxTotalUser); err != nil { mylogrus.MyLog.Errorf("luckyboxTotalUser fail:%v-%v", luckyboxTotalUser, err) } return awardDiamond, err } func GetLuckyboxNilOrErr(model *domain.Model) (*Luckybox, error) { luckybox := Luckybox{} if err := model.Db.Model(&Luckybox{}).First(&luckybox).Error; err != nil { return nil, myerr.WrapErr(err) } luckybox.Model = model return &luckybox, nil } func GetLucyboxDiamond(model *domain.Model) (uint32, error) { if luckybox, err := GetLuckyboxNilOrErr(model); err != nil { return 0, err } else { return luckybox.DiamondNum, nil } } // 幸运盒子用户抽奖汇总(周) type LuckyboxWeekUser struct { Date string UserId mysql.ID BuyDiamond mysql.Num //购买幸运小盒子所需的钻石 AwardDiamond mysql.Num //奖励价值 AwardN uint64 } func (lwu *LuckyboxWeekUser) AddDiamond(db *gorm.DB) error { return db.Clauses(clause.OnConflict{ DoUpdates: clause.Assignments(map[string]interface{}{ "buy_diamond": gorm.Expr("buy_diamond + ?", lwu.BuyDiamond), "award_diamond": gorm.Expr("award_diamond + ?", lwu.AwardDiamond), "award_n": gorm.Expr("award_n + ?", lwu.AwardN), }), }).Create(lwu).Error } // 乐透抽奖 func (luckybox *Luckybox) Lottery(userId mysql.ID) (*LuckyboxUser, string, mysql.YesNo, error) { luckyboxAwards, err := luckybox.getAllValidAward() if err != nil { return nil, "", mysql.NO, err } //打乱 rand.Shuffle(len(luckyboxAwards), func(i, j int) { t := luckyboxAwards[i] luckyboxAwards[i] = luckyboxAwards[j] luckyboxAwards[j] = t }) var winLuckyboxAward *LuckyboxAward = nil var picUrl string = "" var isPublicScreen mysql.YesNo = mysql.NO //循环判断 for i, r := range luckyboxAwards { random := luckybox.getRandomN() luckybox.Model.Log.Infof("luckybox lottery r.Probability :%v, random :%v, flag:%v userId:%v", r.Probability, random, r.Probability > random, userId) //间隔了多少, 产品认为只要是大奖,都间隔,同页面显示,数据库存储不太一样 var intervalSum int64 = -1 //产品的逻辑:无论抽多少次,都不能连续抽中大奖。 //产品条件: 不能连续抽中大奖 && 满足概率膨胀 && 每日放出的次数 //实现: 满足概率膨胀 && 不能连续抽中大奖 && 每日放出的次数 //当用户在最近48小时内: //下注金额-获奖金额≥100,000 //则用户进入抽奖保护期,满足这一条件的中奖概率调整为 //100,000大奖的中奖概率调整为100 if r.ProtectionProbability > 0 { /* s := luckySum{} if err := luckybox.Db.Model(&LuckyboxUser{}).Select("SUM(Buy_Diamond - Award_Diamond) AS money").Where(&LuckyboxUser{ UserId: userId, }).Where("created_time > ?", time.Now().Add(-time.Second * time.Duration(r.ProtectionSecond))).First(&s).Error; err != nil { return nil, "", mysql.NO, myerr.WrapErr(err) }*/ //只是移除一周,因为不知道,不同的奖励周期不一样 redisCli.GetRedis().ZRemRangeByScore(context.Background(), redis_key.GetLuckyboxBuyAward(userId), "0", strconv.FormatInt(time.Now().AddDate(0, 0, -7).Unix(), 10)) // var sumMoney int64 = 0 if zList, err := redisCli.GetRedis().ZRevRangeByScore(context.Background(), redis_key.GetLuckyboxBuyAward(userId), &redis2.ZRangeBy{ Min: strconv.FormatInt(time.Now().Add(-time.Second*time.Duration(r.ProtectionSecond)).Unix(), 10), Max: "+inf", }).Result(); err != nil { return nil, "", mysql.NO, myerr.WrapErr(err) } else { for i, _ := range zList { zs := strings.Split(zList[i], "_") if len(zs) > 1 { if money, err := strconv.ParseInt(zs[1], 10, 64); err != nil { return nil, "", mysql.NO, myerr.WrapErr(err) } else { sumMoney = sumMoney + money } } } } luckybox.Model.Log.Infof("luckybox lottery sumMoney:%v > ProtectionSum:%v, userId:%v", sumMoney, r.ProtectionSum, userId) if sumMoney >= int64(r.ProtectionSum) { r.Probability = r.ProtectionProbability luckybox.Model.Log.Infof("luckybox lottery sumMoney:%v > ProtectionSum:%v, userId:%v r.Probability:%v", sumMoney, r.ProtectionSum, userId, r.Probability) } } if r.Probability > random { //是否符合间隔多少次 if r.IntervalN > 0 { // if intervalSum == -1 { // luckyboxUserMax := LuckyboxUser{} if err := luckybox.Db.Model(&LuckyboxUser{}).Where(&LuckyboxUser{ UserId: userId, }).Where("luckybox_award_id in (select a.id from luckybox_award a where a.is_use = ? and a.interval_n > 0)", mysql.YES).Order("id desc").First(&luckyboxUserMax).Error; err != nil { /* if err == gorm.ErrRecordNotFound { intervalSum = 0 } else { return nil, "", mysql.NO, myerr.WrapErr(err) }*/ if err != gorm.ErrRecordNotFound { return nil, "", mysql.NO, myerr.WrapErr(err) } } /* else { //判断数量 var c int64 if err := luckybox.Db.Model(&LuckyboxUser{}).Where(&LuckyboxUser{ UserId: userId, }).Where("id > ?", luckyboxUserMax.ID).Count(&c).Error; err != nil { return nil, "", mysql.NO, myerr.WrapErr(err) } else { intervalSum = c } }*/ //luckyboxUserMax 可能为0 //判断数量 var c int64 if err := luckybox.Db.Model(&LuckyboxUser{}).Where(&LuckyboxUser{ UserId: userId, }).Where("id > ?", luckyboxUserMax.ID).Count(&c).Error; err != nil { return nil, "", mysql.NO, myerr.WrapErr(err) } else { intervalSum = c } } if int64(r.IntervalN) > intervalSum { luckybox.Model.Log.Infof("luckybox lottery IntervalN:%v, intervalSum:%v userId:%v", r.IntervalN, intervalSum, userId) continue } } dayStr := time.Now().Format(utils.COMPACT_DATE_FORMAT) //预先判断,拦截了部分。查询的时候,已经过滤了, //if luckyboxAwards[i].DayStr == dayStr && luckyboxAwards[i].DayN >= luckyboxAwards[i].DayTotalN { // continue //} // txWinLuckyboxAward := luckybox.Db.Model(&luckyboxAwards[i]) if err := txWinLuckyboxAward.Where("is_use = ?", mysql.YES).Where("day_total_n > 0").Where("day_n < ? or day_str <> ? ", luckyboxAwards[i].DayTotalN, dayStr).Updates(map[string]interface{}{"day_n": gorm.Expr("case when day_str = ? then day_n + 1 else 1 end", dayStr), "day_str": gorm.Expr(dayStr)}).Error; err != nil { //可能形成死锁,单做没抽中 luckybox.Log.Errorf("txWinLuckyboxAward.Where sqlErr:%+v", myerr.WrapErr(err)) continue //return nil, "", mysql.NO, myerr.WrapErr(err) } if txWinLuckyboxAward.RowsAffected == 0 { print(0) //没有更新任何数据,则不算中奖 } else { //已更新了数据,中奖,跳出 winLuckyboxAward = &luckyboxAwards[i] winLuckyboxAward.Model = luckybox.Model picUrl = luckyboxAwards[i].PicUrl isPublicScreen = luckyboxAwards[i].IsPublicScreen // luckybox.Model.Log.Infof("luckybox lottery win Probability:%v, random:%v, userId:%v", r.Probability, random, userId) // break } //严格控制数量,不允许并发超过日临界值值 /* if dayStr == luckyboxAwards[i].DayStr { // txWinLuckyboxAward := luckybox.Db.Model(&luckyboxAwards[i]) if err := txWinLuckyboxAward.Where("day_n < ?", luckyboxAwards[i].DayTotalN).UpdateColumn("day_n", gorm.Expr("day_n + 1")).Error; err != nil { return nil, "", mysql.NO, myerr.WrapErr(err) } if txWinLuckyboxAward.RowsAffected == 0 { //没有更新任何数据,则不算中奖 } else { //已更新了数据,中奖,跳出 winLuckyboxAward = &luckyboxAwards[i] winLuckyboxAward.Model = luckybox.Model picUrl = luckyboxAwards[i].PicUrl isPublicScreen = luckyboxAwards[i].IsPublicScreen break } //winLuckyboxAward.DayN = winLuckyboxAward.DayN + 1 } else { txWinLuckyboxAward := luckybox.Db.Model(&luckyboxAwards[i]) //这里存在并发的问题, 方案1:加上where day_str = dayStr 解决并发,问题在于,对计算概率产生了影响, 方案2:容错并发,day_n=1冲刷了数据。低概率,可接受,产品都接受改数据。 //情况1:先更新 day_n + 1,再更新day_n=1 没有问题。 情况2:先更新day_n=1,再更新day_n+1 没有问题,情况3:先更新day_n=1,再更新day_n=1 fixme:有问题, 解决方案:set case when if err := txWinLuckyboxAward.Where("day_n < ?", luckyboxAwards[i].DayTotalN).UpdateColumn("day_n", gorm.Expr("1")).UpdateColumn("day_str", gorm.Expr(dayStr)).Error; err != nil { return nil, "", mysql.NO, myerr.WrapErr(err) } if txWinLuckyboxAward.RowsAffected == 0 { //没有更新任何数据,则不算中奖 } else { //已更新了数据,中奖,跳出 winLuckyboxAward = &luckyboxAwards[i] winLuckyboxAward.Model = luckybox.Model picUrl = luckyboxAwards[i].PicUrl isPublicScreen = luckyboxAwards[i].IsPublicScreen break } }*/ } } luckyboxUser := LuckyboxUser{ Model: luckybox.Model, UserId: userId, LuckyboxAwardId: 0, AwardN: 0, AwardType: 0, BuyDiamond: luckybox.DiamondNum, AwardDiamond: 0, } //判断是否中奖 if winLuckyboxAward != nil { if winLuckyboxAward.AwardType == luckybox_e.Diamond { luckyboxUser.LuckyboxAwardId = winLuckyboxAward.ID luckyboxUser.AwardType = winLuckyboxAward.AwardType luckyboxUser.AwardN = winLuckyboxAward.AwardN luckyboxUser.AwardDiamond = mysql.Num(winLuckyboxAward.AwardN) } else { luckybox.Log.Errorf("Lottery AwardType:%v err", winLuckyboxAward.AwardType) } } return &luckyboxUser, picUrl, isPublicScreen, nil } // 获取全部的奖励规则 func (luckybox *Luckybox) getAllValidAward() ([]LuckyboxAward, error) { dateStr := time.Now().Format(utils.COMPACT_DATE_FORMAT) luckyboxAwards := []LuckyboxAward{} if err := luckybox.Db.Model(&LuckyboxAward{}).Where(&LuckyboxAward{ IsUse: mysql.YES, }).Where("(day_total_n > day_n and day_str = ?) or (day_str <> ?)", dateStr, dateStr).Find(&luckyboxAwards).Error; err != nil { return nil, myerr.WrapErr(err) } return luckyboxAwards, nil } // 获取随机数 func (luckybox *Luckybox) getRandomN() uint32 { return uint32(rand.Intn(100000)) } // 幸运盒子用户抽奖汇总(总) type LuckyboxTotalUser struct { mysql.Entity UserId mysql.ID BuyDiamond mysql.Num //购买幸运小盒子所需的钻石 AwardDiamond mysql.Num //奖励价值 AwardN uint64 Sync mysql.YesNo }