package players import ( "context" "joybuywork/internal/app" "joybuywork/internal/consts" "joybuywork/internal/consts/ckey" "joybuywork/internal/plugins/jsonm" "joybuywork/internal/repository/cache" "joybuywork/internal/repository/dao" "joybuywork/internal/repository/dbcheck" "joybuywork/internal/repository/model" "joybuywork/internal/repository/mtype" "joybuywork/internal/repository/withdefault" "joybuywork/internal/service/cmm" "joybuywork/internal/service/provider/award" "joybuywork/internal/service/provider/stocks" "joybuywork/internal/utils" "joybuywork/internal/utils/lockdo" "joybuywork/proto/go_proto/workerr" "math/rand" "time" "github.com/golang-module/carbon/v2" "gorm.io/datatypes" "gorm.io/gen" "gorm.io/gen/field" ) const ( cashFailTypeEnergyLack = iota + 1 // 能量不足 cashFailTypeRewardLack // 奖品库存不足 ) // Cash 现金转盘交互逻辑 type Cash struct { Player player *model.FissionCash conf *model.FissionCampaignCash blindLang string cashPercentKey string cashAgainKey string } func NewCash(ctx context.Context, user *model.FissionUser, opts ...Option) (player *Cash, err error) { player = &Cash{} err = player.Init(ctx, user, consts.IdCash, opts...) if err != nil { return } err = player.initPlayer() if err != nil { return } return } // EffectInvite 有效邀请追加进度 func (s *Cash) EffectInvite(helperId int64, add float64) (err error) { player := s.GetPlayer() // 一定要触发需要邀请进来的才算 if player.LotteryNum < 5 { return } percent, err := s._addPercent(player.ID, add) if err != nil { return } inviteData, _ := jsonm.MarshalToString(mtype.RecordTypeInvite{ UserId: helperId, Help: true, }) // 更新成有效助力 m := s.GetDao().FissionCashRecord _, err = m.WithContext(s.Ctx).Where( m.CampaignID.Eq(player.CampaignID), m.UserID.Eq(player.UserID), m.CashID.Eq(player.ID), m.Type.Eq(mtype.FissionCashRecordTypeInvite), ).Where(gen.Cond( datatypes.JSONQuery(m.Data.ColumnName().String()).Equals(helperId, "user_id"), )...).UpdateSimple(m.Data.Value(inviteData)) if err != nil { err = s.BE().MysqlError(err) return } energyData, _ := jsonm.MarshalToString(mtype.RecordTypeEnergy{ TitleMark: "record:share_reward", DecOrAdd: mtype.FissionCashRecordDataAdd, Energy: add, }) err = s._addRecord(withdefault.FissionCashRecord(&model.FissionCashRecord{ CampaignID: player.CampaignID, UserID: player.UserID, CashID: player.ID, Type: mtype.FissionCashRecordTypeEnergy, Data: energyData, })) if err != nil || percent < 1 { return } // 进度条满了 playerM := s.GetDao().FissionCash _, err = playerM.WithContext(s.Ctx).Where(playerM.ID.Eq(player.ID)).UpdateSimple(playerM.Energy.Add(1)) if err != nil { err = s.BE().MysqlError(err) return } err = app.Redis.Cache.IncrByFloat(s.Ctx, s.cashPercentKey, -1).Err() if err != nil { err = s.BE().CacheError(err) return } return } // InviteOne 邀请一个人 func (s *Cash) InviteOne(helperId int64) (err error) { player := s.GetPlayer() // 一定要触发需要邀请进来的才算 if player.LotteryNum < 5 { return } inviteData, _ := jsonm.MarshalToString(mtype.RecordTypeInvite{ UserId: helperId, Help: false, }) return s._addRecord(withdefault.FissionCashRecord(&model.FissionCashRecord{ CampaignID: player.CampaignID, UserID: player.UserID, CashID: player.ID, Type: mtype.FissionCashRecordTypeInvite, Data: inviteData, })) } // Spin 转转盘 func (s *Cash) Spin() (id int64, failType int32, err error) { cpn, user := s.cpn, s.user player, conf := s.GetPlayer(), s.GetConf() if player.Energy < consts.FissionCashFixEnergyUse { failType = cashFailTypeEnergyLack return } energyData, _ := jsonm.MarshalToString(mtype.RecordTypeEnergy{ TitleMark: "record:spin_reward", DecOrAdd: mtype.FissionCashRecordDataDec, Energy: consts.FissionCashFixEnergyUse, }) var records = []*model.FissionCashRecord{ withdefault.FissionCashRecord(&model.FissionCashRecord{ CampaignID: player.CampaignID, UserID: player.UserID, CashID: player.ID, Type: mtype.FissionCashRecordTypeEnergy, Data: energyData, }), } defer func() { if err != nil { return } err = s._addRecord(records...) }() id = consts.SpinGetId[int(player.LotteryNum)] var price float64 for _, v := range consts.FissionCashSpin { if id == v.Id { price = v.Cash } } cashData, _ := jsonm.MarshalToString(mtype.RecordTypeCash{ TitleMark: "record:spin_reward", DecOrAdd: mtype.FissionCashRecordDataAdd, Cash: price, }) records = append(records, withdefault.FissionCashRecord(&model.FissionCashRecord{ CampaignID: player.CampaignID, UserID: player.UserID, CashID: player.ID, Type: mtype.FissionCashRecordTypeCash, Data: cashData, })) m := s.GetDao().FissionCash var upData = []field.AssignExpr{ m.ShowPrice.Add(price), m.Energy.Sub(consts.FissionCashFixEnergyUse), m.LotteryNum.Add(1), } switch player.LotteryNum + 1 { case 4: upData = append(upData, m.NextStep.Value(mtype.FissionCashNextStepTriggerShare)) case 5: upData = append(upData, m.NextStep.Value(mtype.FissionCashNextStepLastSpin)) case 6: var stock *stocks.Campaign stock, err = stocks.NewCampaign(s.Ctx, cpn.ID) if err != nil { return } if conf.PriceNum > 0 { if conf.Lack == mtype.Yes { failType = cashFailTypeRewardLack return } var ok bool ok, err = stock.DecrPoolAndAdd(cpn.ID, 1) if err != nil { return } if !ok { if conf.Lack == mtype.No { confM := s.GetDao().FissionCampaignCash _, err = confM.WithContext(s.Ctx).Where(confM.ID.Eq(conf.ID)).UpdateSimple(confM.Lack.Value(mtype.Yes)) if err != nil { err = s.BE().MysqlError(err) return } _ = cache.NewCampaignCash(s.Ctx).Clear(conf) } failType = cashFailTypeRewardLack return } } else { err = stock.AddUseStock(1) if err != nil { return } } upData = append(upData, m.NextStep.Value(mtype.FissionCashNextStepShowReward)) cm := cmm.NewCMM(s.Ctx, s.GetDao()) var rewards []*model.FissionCampaignReward rewards, err = cm.UserAppReward(s.user) if err != nil { return } var addRewards = make([]*award.RewardOne, 0, len(rewards)) var ids []int64 for _, reward := range rewards { addRewards = append(addRewards, &award.RewardOne{Reward: reward}) } var rewardsRes []*award.RewardRes rewardsRes, _, err = award.NewAwardReward(s.Ctx, s.GetDao(), cpn.ProjectID, user).Batch(addRewards, true) if err != nil { return } for _, reward := range rewardsRes { ids = append(ids, reward.Record.ID) } data, _ := jsonm.MarshalToString(mtype.RecordTypeReward{ Ids: ids, }) records = append(records, withdefault.FissionCashRecord(&model.FissionCashRecord{ CampaignID: player.CampaignID, UserID: player.UserID, CashID: player.ID, Type: mtype.FissionCashRecordTypeReward, Data: data, })) } _, err = m.WithContext(s.Ctx).Where(m.ID.Eq(player.ID)).UpdateSimple(upData...) if err != nil { err = s.BE().MysqlError(err) return } return } // LastSpin 弹窗还差最后一次抽奖获得奖品动画 func (s *Cash) LastSpin() (err error) { player := s.player if player.NextStep != mtype.FissionCashNextStepLastSpin { return } m := s.GetDao().FissionCash _, err = m.WithContext(s.Ctx).Where(m.ID.Eq(player.ID)).UpdateSimple( m.NextStep.Value(mtype.FissionCashNextStepInviteSpin), ) if err != nil { err = s.BE().MysqlError(err) return } return } // ShowReward 播放展示奖品动画 func (s *Cash) ShowReward() (err error) { player := s.player if player.NextStep != mtype.FissionCashNextStepShowReward { return } m := s.GetDao().FissionCash _, err = m.WithContext(s.Ctx).Where(m.ID.Eq(player.ID)).UpdateSimple( m.NextStep.Value(mtype.FissionCashNextStepNone), ) if err != nil { err = s.BE().MysqlError(err) return } return } // TriggerShare 播放触发了分享动画 func (s *Cash) TriggerShare() (err error) { player := s.player if player.NextStep != mtype.FissionCashNextStepTriggerShare { return } m := s.GetDao().FissionCash _, err = m.WithContext(s.Ctx).Where(m.ID.Eq(player.ID)).UpdateSimple( m.ShowPrice.Add(consts.FissionCashFixShare), m.Energy.Add(consts.FissionCashFixEnergyUse), m.NextStep.Value(mtype.FissionCashNextStepNone), ) if err != nil { err = s.BE().MysqlError(err) return } cashRecordData, _ := jsonm.MarshalToString(mtype.RecordTypeCash{ TitleMark: "record:share_reward", DecOrAdd: "add", Cash: consts.FissionCashFixShare, }) energyRecordData, _ := jsonm.MarshalToString(mtype.RecordTypeEnergy{ TitleMark: "record:share_reward", DecOrAdd: "add", Energy: consts.FissionCashFixEnergyUse, }) err = s._addRecord(withdefault.FissionCashRecord(&model.FissionCashRecord{ CampaignID: player.CampaignID, UserID: player.UserID, CashID: player.ID, Type: mtype.FissionCashRecordTypeCash, Data: cashRecordData, }), withdefault.FissionCashRecord(&model.FissionCashRecord{ CampaignID: player.CampaignID, UserID: player.UserID, CashID: player.ID, Type: mtype.FissionCashRecordTypeEnergy, Data: energyRecordData, })) if err != nil { return } return } // OpenGift 播放开盒子动画 func (s *Cash) OpenGift() (openBox, leftBox, rightBox int32, err error) { player, cpn, conf := s.GetPlayer(), s.cpn, s.GetConf() if player.NextStep != mtype.FissionCashNextStepOpenGift { return } spl := cpn.GetLangMarks() var price = conf.Price if player.Lang != spl[0] { var lang *model.FissionCampaignLang lang, err = cache.NewCampaignLang(s.Ctx).FindByCampaignId(cpn.ID, player.Lang) if err != nil { err = s.BE().CacheError(err) return } if lang == nil { err = workerr.NotFound("not found cash conf") return } if c := lang.GetCash(); c != nil { price = c.Price } } openBox, leftBox, rightBox = s._boxVal(price) m := s.GetDao().FissionCash _, err = m.WithContext(s.Ctx).Where(m.ID.Eq(player.ID)).UpdateSimple( m.ShowPrice.Value(float64(openBox)), m.Energy.Value(consts.FissionCashFixEnergy), m.NextStep.Value(mtype.FissionCashNextStepNone), ) if err != nil { err = s.BE().MysqlError(err) return } cashRecordData, _ := jsonm.MarshalToString(mtype.RecordTypeCash{ TitleMark: "record:start_reward", DecOrAdd: mtype.FissionCashRecordDataAdd, Cash: float64(openBox), }) energyRecordData, _ := jsonm.MarshalToString(mtype.RecordTypeEnergy{ TitleMark: "record:start_reward", DecOrAdd: mtype.FissionCashRecordDataAdd, Energy: consts.FissionCashFixEnergy, }) err = s._addRecord(withdefault.FissionCashRecord(&model.FissionCashRecord{ CampaignID: player.CampaignID, UserID: player.UserID, CashID: player.ID, Type: mtype.FissionCashRecordTypeCash, Data: cashRecordData, }), withdefault.FissionCashRecord(&model.FissionCashRecord{ CampaignID: player.CampaignID, UserID: player.UserID, CashID: player.ID, Type: mtype.FissionCashRecordTypeEnergy, Data: energyRecordData, })) if err != nil { return } return } // Again 再玩一次 func (s *Cash) Again() (err error) { if !app.NewRedisLock(s.Ctx, s.cashAgainKey).SoftLock() { return workerr.OnceLockError } hasAgain, err := s.HasAgain() if err != nil || !hasAgain { return } _, err = s.createPlayer(s.user.LangMark) return } // HasAgain 是否有再玩一次 func (s *Cash) HasAgain() (ok bool, err error) { player, conf := s.GetPlayer(), s.GetConf() if conf.LoopNum == 1 || player.LotteryNum < 6 { return } if conf.LoopNum == 0 { return true, nil } rewardNum, err := s._rewardNum() if err != nil { return } if rewardNum >= int64(conf.LoopNum) { return } return true, nil } // RefreshView 刷新预览用户 func (s *Cash) RefreshView() (err error) { if !s.IsView() { err = s.BE().VerifyError("非预览用户无法重新体验") return } player := s.GetPlayer() m := s.GetDao().FissionCashRecord _, err = m.WithContext(s.Ctx).Where(m.CashID.Eq(player.ID)).Delete() if err != nil { err = s.BE().MysqlError(err) return } app.Redis.Cache.Del(s.Ctx, s.cashPercentKey) playerM := s.GetDao().FissionCash _, err = playerM.WithContext(s.Ctx).Where(playerM.ID.Eq(player.ID)).Delete() if err != nil { err = s.BE().MysqlError(err) return } return } // GetConf 获取玩法配置 func (s *Cash) GetConf() *model.FissionCampaignCash { if s.conf == nil { s.conf, _ = cache.NewCampaignCash(s.Ctx).FindByCampaignId(s.user.CampaignID) } return s.conf } // GetPlayer 获取玩家基础数据 func (s *Cash) GetPlayer() *model.FissionCash { return s.player } // 玩家 func (s *Cash) initPlayer() (err error) { user, sp := s.user, s.cpn.GetLangMarks() lang := utils.NotExistGetFirst(user.LangMark, sp) s.blindLang = lang player, err := s._dbFirst(lang) if err != nil { return } defer func() { if err != nil { return } s.player = player s.cashPercentKey = ckey.FissionUserCashPercent(player.ID) s.cashAgainKey = ckey.FissionUserCashAgain(player.UserID) }() if player != nil { // 没有过期,或者已经完成了任务 if player.ExpiredIn > time.Now().Unix() || player.LotteryNum >= int8(len(consts.SpinGetId)) { return } // 过期了 playerM := s.GetDao().FissionCash _, err = playerM.WithContext(s.Ctx).Where(playerM.ID.Eq(player.ID)).UpdateSimple(playerM.IsExpired.Value(mtype.Yes)) if err != nil { err = s.BE().MysqlError(err) return } } player, err = s.firstOrCreate(lang) if err != nil { return } return } // 创建玩家基础数据 func (s *Cash) firstOrCreate(lang string) (player *model.FissionCash, err error) { lc := app.NewFollower(s.Ctx, ckey.FissionUserCreatePlayerOnce(s.user.ID)).Lock() defer lc.Release() if lc.Error() != nil { err = lc.Error() return } player, err = s._dbFirst(lang) if err != nil { return } if player != nil { return } return s.createPlayer(lang) } func (s *Cash) createPlayer(lang string) (player *model.FissionCash, err error) { conf := s.GetConf() playerM := dao.FissionCash player = withdefault.FissionCash(&model.FissionCash{ CampaignID: s.user.CampaignID, UserID: s.user.ID, LotteryNum: 0, NextStep: mtype.FissionCashNextStepOpenGift, Energy: 0, ShowPrice: 0, ExpiredIn: carbon.Now().AddDays(int(conf.ExpiredDay)).Timestamp(), IsExpired: 0, Lang: lang, }) err = playerM.WithContext(s.Ctx).Create(player) if err != nil { err = s.BE().MysqlError(err) return } return } func (s *Cash) _dbFirst(lang string) (*model.FissionCash, error) { playerM := dao.FissionCash player, err := playerM.WithContext(s.Ctx).Where(playerM.UserID.Eq(s.user.ID), playerM.Lang.Value(lang), playerM.IsExpired.Eq(mtype.No)).Last() if err = dbcheck.DbError(err); err != nil { return nil, s.BE().MysqlError(err) } return player, nil } func (s *Cash) _addRecord(records ...*model.FissionCashRecord) (err error) { err = s.GetDao().FissionCashRecord.WithContext(s.Ctx).Create(records...) if err != nil { err = s.BE().MysqlError(err) return } return } // 追加进度 func (s *Cash) _addPercent(playerId int64, onePercent float64) (percent float64, err error) { key := s.cashPercentKey err = lockdo.ExistDo(s.Ctx, key, func(exist bool) (err error) { if exist { return } m := s.GetDao().FissionCashRecord efNum, err := m.WithContext(s.Ctx).Where( m.CashID.Eq(playerId), m.Type.Eq(mtype.FissionCashRecordTypeInvite), ).Where(gen.Cond( datatypes.JSONQuery(m.Data.ColumnName().String()).Equals(true, "help"), )...).Count() if err != nil { return } // 重新设置进度 p := float64(efNum) * onePercent for p > 1 { p -= 1 } err = app.Redis.Cache.IncrByFloat(s.Ctx, key, p).Err() if err != nil { return } return }) if err != nil { err = s.BE().CacheError(err) return } app.Redis.Cache.Expire(s.Ctx, key, consts.TimeDay) percent, err = app.Redis.Cache.IncrByFloat(s.Ctx, key, onePercent).Result() if err != nil { err = s.BE().CacheError(err) return } return } func (s *Cash) _boxVal(price int32) (openBox, leftBox, rightBox int32) { openBox = price - consts.FissionCashFixLeft if price <= 30 { leftBox = openBox - 1 rightBox = openBox - 2 return } if price <= 50 { leftBox = openBox - (rand.Int31n(5) + 15) rightBox = openBox - (rand.Int31n(5) + 10) return } leftBox = openBox - (rand.Int31n(10) + 15) rightBox = openBox - (rand.Int31n(10) + 10) return } func (s *Cash) _rewardNum() (count int64, err error) { player := s.GetPlayer() m := s.GetDao().FissionCashRecord count, err = m.WithContext(s.Ctx).Where( m.CampaignID.Eq(player.CampaignID), m.UserID.Eq(player.UserID), m.Type.Eq(mtype.FissionCashRecordTypeReward), ).Count() if err != nil { err = s.BE().MysqlError(err) return } return }