package players import ( "context" "joybuywork/internal/app" "joybuywork/internal/consts" "joybuywork/internal/consts/ckey" "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" "joybuywork/internal/service/provider/award" "joybuywork/internal/service/provider/fissionaction" "joybuywork/internal/service/queue/producer" "joybuywork/internal/service/queue/types" "joybuywork/internal/utils" "joybuywork/internal/utils/lockdo" "joybuywork/proto/go_proto/workerr" "fmt" "strings" "time" "gits.branchcn.com/backend/microcode/plugins/derivative/lock" "gorm.io/gen" ) const ( chopHelpOk = iota chopHelpLimit chopHelpDone ) type helperInfo struct { ipAddress string // ip os string // 系统 osVersion string // 系统版本 deviceModel string // 设备类型 webId string // 浏览器id deviceID string // 设备号 actionTime int64 // 助力时间 helper *model.FissionUser // 用户数据 } // Chop 现金转盘交互逻辑 type Chop struct { Player player *model.FissionChop conf *model.FissionCampaignChop chopHelpUserKey string chopHelpUserRewardKey string canSelectReward int32 oldPercent float64 } func NewChop(ctx context.Context, user *model.FissionUser, opts ...Option) (player *Chop, err error) { player = &Chop{} err = player.Init(ctx, user, consts.IdChop, opts...) if err != nil { return } err = player.initPlayer() if err != nil { return } return } // HelpClue 助力提醒 func (s *Chop) HelpClue() (isFix bool, old, add float64) { player := s.GetPlayer() help := provider.NewFissionClue(s.Ctx, player.UserID).GetChopHelp() if help.ChopRewardId != player.ChopRewardID { return } isFix = help.IsFix old = help.OldPercent add = help.AddPercent return } // RegisterHelp 注册助力 func (s *Chop) RegisterHelp(actionTime int64, helper *model.FissionUser) (helpOk bool, err error) { player := s.GetPlayer() if player.ChopRewardID == 0 { return } conf, userMark, helpMark := s.GetConf(), fmt.Sprint(helper.ID), mtype.FissionCampaignTicketHelpEventRegister var canHelp bool switch conf.HelpRule { case mtype.FissionCampaignChopHelpRuleUniqReward: canHelp, err = s.rewardCanHelp(helpMark, userMark) if err != nil { return } case mtype.FissionCampaignChopHelpRuleUniqUser: canHelp, err = s.userCanHelp(helpMark, userMark) if err != nil { return } } // 无需助力 if !canHelp { return } // 任务数据 chopReward, task, err := s.getTask(mtype.FissionCampaignTaskTypeRegister) if err != nil || task == nil { return } // 助力处理 status, err := s._doHelp(chopReward, task, helpMark, userMark) if err != nil || status != chopHelpOk { return } info := &helperInfo{ actionTime: actionTime, helper: helper, } err = s.helpOkNotify(info, chopReward, helpMark, task.Percent, types.HelpSourceRegister) if err != nil { return } return true, nil } // InstallHelp 安装助力 func (s *Chop) InstallHelp(actionTime int64, helper *model.FissionUser) (helpOk bool, err error) { player := s.GetPlayer() if player.ChopRewardID == 0 { return } conf, userMark, helpMark := s.GetConf(), fmt.Sprint(helper.ID), mtype.FissionCampaignTicketHelpEventInstall var canHelp bool switch conf.HelpRule { case mtype.FissionCampaignChopHelpRuleUniqReward: canHelp, err = s.rewardCanHelp(helpMark, userMark) if err != nil { return } case mtype.FissionCampaignChopHelpRuleUniqUser: canHelp, err = s.userCanHelp(helpMark, userMark) if err != nil { return } } // 无需助力 if !canHelp { return } // 任务数据 chopReward, task, err := s.getTask(mtype.FissionCampaignTaskTypeInstall) if err != nil || task == nil { return } // 助力处理 status, err := s._doHelp(chopReward, task, helpMark, userMark) if err != nil || status != chopHelpOk { return } info := &helperInfo{ actionTime: actionTime, helper: helper, } err = s.helpOkNotify(info, chopReward, helpMark, task.Percent, types.HelpSourceInstall) if err != nil { return } return true, nil } // OpenHelp 打开助力 func (s *Chop) OpenHelp(actionTime int64, helper *model.FissionUser) (helpOk bool, err error) { player := s.GetPlayer() if player.ChopRewardID == 0 { return } conf, userMark, helpMark := s.GetConf(), fmt.Sprint(helper.ID), mtype.FissionCampaignTicketHelpEventOpen var canHelp bool switch conf.HelpRule { case mtype.FissionCampaignChopHelpRuleUniqReward: canHelp, err = s.rewardCanHelp(helpMark, userMark) if err != nil { return } case mtype.FissionCampaignChopHelpRuleUniqUser: canHelp, err = s.userCanHelp(helpMark, userMark) if err != nil { return } } // 无需助力 if !canHelp { return } // 任务数据 chopReward, task, err := s.getTask(mtype.FissionCampaignTaskTypeOpen) if err != nil || task == nil { return } // 助力处理 status, err := s._doHelp(chopReward, task, helpMark, userMark) if err != nil || status != chopHelpOk { return } info := &helperInfo{ actionTime: actionTime, helper: helper, } err = s.helpOkNotify(info, chopReward, helpMark, task.Percent, types.HelpSourceOpen) if err != nil { return } return true, nil } // ClickHelp 点击助力 func (s *Chop) ClickHelp(actionTime int64, userMark, IpAddress, os, osVersion, deviceModel, deviceId, webId string) (helpOk bool, err error) { player := s.GetPlayer() if player.ChopRewardID == 0 { return } conf, helpMark := s.GetConf(), mtype.FissionCampaignTicketHelpEventClick var canHelp bool switch conf.HelpRule { case mtype.FissionCampaignChopHelpRuleUniqReward: canHelp, err = s.rewardCanHelp(helpMark, userMark) if err != nil { return } case mtype.FissionCampaignChopHelpRuleUniqUser: canHelp, err = s.userCanHelp(helpMark, userMark) if err != nil { return } } // 无需助力 if !canHelp { return } // 任务数据 chopReward, task, err := s.getTask(mtype.FissionCampaignTaskTypeClick) if err != nil || task == nil { return } // 助力处理 status, err := s._doHelp(chopReward, task, helpMark, userMark) if err != nil || status != chopHelpOk { return } helper := &helperInfo{ ipAddress: IpAddress, os: os, osVersion: osVersion, deviceModel: deviceModel, deviceID: deviceId, webId: webId, actionTime: actionTime, } err = s.helpOkNotify(helper, chopReward, helpMark, task.Percent, types.HelpSourceClick) if err != nil { return } return true, nil } // Reselect 重选奖品 func (s *Chop) Reselect() (err error) { player := s.GetPlayer() if player.ChopRewardID == 0 { return } canSelect, err := s.ExistSelectReward() if err != nil { return } if !canSelect { return } m := s.GetDao().FissionChop _, err = m.WithContext(s.Ctx).Where(m.ID.Eq(player.ID)).UpdateSimple(m.ChopRewardID.Value(0)) if err != nil { err = s.BE().MysqlError(err) return } return } // SelectReward 选择奖品 func (s *Chop) SelectReward(rewardId int64) (err error) { player := s.GetPlayer() if player.ChopRewardID > 0 { err = s.BE().VerifyError("reward selected") return } rewardM := s.GetDao().FissionCampaignReward reward, err := rewardM.WithContext(s.Ctx).Where(rewardM.ID.Eq(rewardId)).Take() if err = dbcheck.DbError(err); err != nil { err = s.BE().MysqlError(err) return } if reward == nil { err = workerr.NotFound("not found reward") return } chopRewardM := s.GetDao().FissionChopReward if reward.HasRule == mtype.Yes { ruleM := s.GetDao().FissionCampaignRewardRule var rule *model.FissionCampaignRewardRule rule, err = ruleM.WithContext(s.Ctx).Where(ruleM.RewardID.Eq(reward.ID)).Take() if err = dbcheck.DbError(err); err != nil { err = s.BE().MysqlError(err) return } var count int64 count, err = chopRewardM.WithContext(s.Ctx).Where( chopRewardM.UserID.Eq(player.UserID), chopRewardM.RewardID.Eq(reward.ID), ).Count() if err != nil { err = s.BE().MysqlError(err) return } if count >= int64(rule.CampaignLimit) { err = s.BE().VerifyError("reward limit") return } } chopReward := withdefault.FissionChopReward(&model.FissionChopReward{ UserID: player.UserID, CampaignID: player.CampaignID, RewardID: reward.ID, InitPercent: reward.InitPercent, Percent: reward.InitPercent, RecordID: 0, }) if err = chopRewardM.WithContext(s.Ctx).Create(chopReward); err != nil { err = s.BE().MysqlError(err) return } m := s.GetDao().FissionChop _, err = m.WithContext(s.Ctx).Where(m.ID.Eq(player.ID)).UpdateSimple(m.ChopRewardID.Value(chopReward.ID)) if err != nil { err = s.BE().MysqlError(err) return } // 选择奖品埋点 fissionaction.NewMarkPoint(s.Ctx).InApp(s.user).SelectReward(time.Now().Unix(), reward.ID) // 有初始助力,要推送一下消息 if reward.InitPercent > 0 { provider.NewFissionClue(s.Ctx, s.user.ID).ChopHelp(chopReward.ID, 0, reward.InitPercent, true) } return } // ExistSelectReward 是否存在可选择的奖品 func (s *Chop) ExistSelectReward() (exist bool, err error) { if s.canSelectReward != 0 { return s.canSelectReward == 1, nil } exist, err = s._existSelectReward() if err != nil { return } if exist { s.canSelectReward = 1 } else { s.canSelectReward = -1 } return } // 是否选择奖品 func (s *Chop) _existSelectReward() (exist bool, err error) { rewards, err := cmm.NewCMM(s.Ctx, s.GetDao()).UserAppReward(s.user) if err != nil { return } var limitIds []int64 for _, reward := range rewards { if reward.HasRule == mtype.Yes { limitIds = append(limitIds, reward.ID) } } if len(limitIds) == 0 { return true, nil } ruleM := s.GetDao().FissionCampaignRewardRule rules, err := ruleM.WithContext(s.Ctx).Where(ruleM.CampaignID.Eq(s.user.CampaignID), ruleM.RewardID.In(limitIds...)).Find() if err != nil { err = s.BE().MysqlError(err) return } var ruleMap = make(map[int64]*model.FissionCampaignRewardRule, len(rules)) for _, rule := range rules { ruleMap[rule.RewardID] = rule } type rewardGet struct { RewardId int64 `json:"reward_id"` Num int32 `json:"num"` } var gets []*rewardGet chopRewardM := s.GetDao().FissionChopReward err = chopRewardM.WithContext(s.Ctx). Select(chopRewardM.ID.Count().As("num"), chopRewardM.RewardID). Where(chopRewardM.UserID.Eq(s.user.ID), chopRewardM.RecordID.Gt(0)). Group(chopRewardM.RewardID). Scan(&gets) if err != nil { err = s.BE().MysqlError(err) return } var filterMap = make(map[int64]struct{}) for _, v := range gets { if rule, ok := ruleMap[v.RewardId]; ok && rule.CampaignLimit <= v.Num { filterMap[v.RewardId] = struct{}{} } } return len(rewards) > len(filterMap), nil } // GetConf 获取玩法配置 func (s *Chop) GetConf() *model.FissionCampaignChop { if s.conf == nil { s.conf, _ = cache.NewCampaignChop(s.Ctx).FindByCampaignId(s.user.CampaignID) } return s.conf } // RefreshView 刷新预览用户 func (s *Chop) RefreshView() (err error) { if !s.IsView() { err = s.BE().VerifyError("非预览用户无法重新体验") return } player := s.GetPlayer() m := s.GetDao().FissionChop _, err = m.WithContext(s.Ctx).Where(m.UserID.Eq(player.UserID)).Delete() if err != nil { err = s.BE().MysqlError(err) return } app.Redis.Cache.Del(s.Ctx, s.chopHelpUserRewardKey, s.chopHelpUserKey) helpM := s.GetDao().FissionChopHelp _, err = helpM.WithContext(s.Ctx).Where(helpM.UserID.Eq(player.UserID)).Delete() if err != nil { err = s.BE().MysqlError(err) return } chopRewardM := s.GetDao().FissionChopReward _, err = chopRewardM.WithContext(s.Ctx).Where(chopRewardM.UserID.Eq(player.UserID)).Delete() if err != nil { err = s.BE().MysqlError(err) return } return } // GetPlayer 获取玩家基础数据 func (s *Chop) GetPlayer() *model.FissionChop { return s.player } // 玩家 func (s *Chop) initPlayer() (err error) { player, err := s._dbFirst() if err != nil { return } defer func() { if err != nil { return } s.player = player s.chopHelpUserKey = ckey.FissionChopHelpUser(player.UserID) s.chopHelpUserRewardKey = ckey.FissionChopHelpUserReward(player.ChopRewardID) }() if player != nil { return } player, err = s.firstOrCreate() if err != nil { return } return } // 创建玩家基础数据 func (s *Chop) firstOrCreate() (player *model.FissionChop, 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() if err != nil { return } if player != nil { return } return s.createPlayer() } func (s *Chop) createPlayer() (player *model.FissionChop, err error) { playerM := dao.FissionChop player = withdefault.FissionChop(&model.FissionChop{ CampaignID: s.user.CampaignID, UserID: s.user.ID, }) err = playerM.WithContext(s.Ctx).Create(player) if err != nil { err = s.BE().MysqlError(err) return } return } func (s *Chop) _dbFirst() (*model.FissionChop, error) { playerM := dao.FissionChop player, err := playerM.WithContext(s.Ctx).Where(playerM.UserID.Eq(s.user.ID)).Take() if err = dbcheck.DbError(err); err != nil { return nil, s.BE().MysqlError(err) } return player, nil } // 用户唯一助力 func (s *Chop) userCanHelp(mark string, uniqMark string) (canHelp bool, err error) { player, key := s.GetPlayer(), s.chopHelpUserKey // 这个人给它上级助力过事件了,就不能再助力了 yes, err := app.Redis.Cache.HSetNX(s.Ctx, key, fmt.Sprintf("%s_%s", mark, uniqMark), 1, ).Result() if err != nil { err = s.BE().RedisError(err) return } if !yes { return } // 设置成功就给一个过期时间 app.Redis.Cache.Expire(s.Ctx, key, consts.TimeDay) // 再查数据库看看是不是真的,怕只是缓存过期 helpM := s.GetDao().FissionChopHelp help, err := helpM.WithContext(s.Ctx). Where( helpM.CampaignID.Eq(player.CampaignID), helpM.Mark.Eq(mark), helpM.UserMark.Eq(uniqMark), ).Take() if err = dbcheck.DbError(err); err != nil { err = s.BE().MysqlError(err) return } if help != nil { return } return true, nil } // 奖品唯一助力 func (s *Chop) rewardCanHelp(mark string, uniqMark string) (canHelp bool, err error) { player, key := s.GetPlayer(), s.chopHelpUserRewardKey // 这个人给它上级助力过事件了,就不能再助力了 yes, err := app.Redis.Cache.HSetNX(s.Ctx, key, fmt.Sprintf("%s_%s", mark, uniqMark), 1, ).Result() if err != nil { err = s.BE().RedisError(err) return } if !yes { return } app.Redis.Cache.Expire(s.Ctx, key, consts.TimeDay) // 再查数据库看看助力过没有 helpM := s.GetDao().FissionChopHelp help, err := helpM.WithContext(s.Ctx). Where( helpM.ChopRewardID.Eq(player.ChopRewardID), helpM.Mark.Eq(mark), helpM.UserMark.Eq(uniqMark), ).Take() if err = dbcheck.DbError(err); err != nil { err = s.BE().MysqlError(err) return } if help != nil { return } return true, nil } func (s *Chop) getTask(taskType int8) (chopReward *model.FissionChopReward, task *model.FissionCampaignTask, err error) { player := s.GetPlayer() chopRewardM := s.GetDao().FissionChopReward chopReward, err = chopRewardM.WithContext(s.Ctx).Where(chopRewardM.ID.Eq(player.ChopRewardID)).Take() if err = dbcheck.DbError(err); err != nil { err = s.BE().MysqlError(err) return } // 没找到,或者助力完成 if chopReward == nil || chopReward.RecordID > 0 { return } taskM := s.GetDao().FissionCampaignTask task, err = taskM.WithContext(s.Ctx).Where( taskM.CampaignID.Eq(player.CampaignID), taskM.TaskType.Eq(taskType), taskM.TaskMark.Eq(utils.FissionCampaignTaskMarkRewardOneVal(chopReward.RewardID)), ).Take() if err = dbcheck.DbError(err); err != nil { err = s.BE().MysqlError(err) return } s.oldPercent = chopReward.Percent return } func (s *Chop) _doHelp(chopReward *model.FissionChopReward, task *model.FissionCampaignTask, helpMark, userMark string) (status int8, err error) { // 计算助力进度 percent, err := s._helpPercent(chopReward, task, helpMark) if err != nil { // 有错误,或者被限制了 return } if percent == 0 { status = chopHelpLimit return } if percent < 100 { err = s._chopHelpOk(chopReward, task, helpMark, userMark, percent) return } // 发奖要锁住,防止并发发奖 lc := app.NewRedisLock(s.Ctx, ckey.FissionChopHelpOnce(chopReward.ID), lock.SetExpire(10*time.Second)) if !lc.SoftLock() { return } defer lc.Release() // 重新找一遍,看看是不是没绑定奖励 chopRewardM := s.GetDao().FissionChopReward chopReward, err = chopRewardM.WithContext(s.Ctx).Where(chopRewardM.ID.Eq(chopReward.ID)).Take() if err = dbcheck.DbError(err); err != nil { err = s.BE().MysqlError(err) return } if chopReward.RecordID > 0 { status = chopHelpDone return } err = s._chopHelpOk(chopReward, task, helpMark, userMark, 100) if err != nil { return } return } func (s *Chop) _helpPercent(chopReward *model.FissionChopReward, task *model.FissionCampaignTask, helpMark string) (percent float64, err error) { // 有限制的助力 if task.PercentLimit != 0 { key := ckey.FissionChopHelpMarkPercent(chopReward.ID, helpMark) s._createPercentCache(chopReward.ID, key, helpMark, 0) num := app.Redis.Cache.IncrByFloat(s.Ctx, key, task.Percent).Val() // 超过限制不能助力 if num > task.PercentLimit { return } } // 提高助力进度 key := ckey.FissionChopHelpPercent(chopReward.ID) s._createPercentCache(chopReward.ID, key, "", chopReward.InitPercent) percent = app.Redis.Cache.IncrByFloat(s.Ctx, key, task.Percent).Val() return } // 创建缓存进度 func (s *Chop) _createPercentCache(chopRewardId int64, key, mark string, initPercent float64) { _ = lockdo.ExistDo(s.Ctx, key, func(exist bool) (err error) { if exist { return } m := s.GetDao().FissionChopHelp var cond = []gen.Condition{ m.ChopRewardID.Eq(chopRewardId), } if mark != "" { cond = append(cond, m.Mark.Eq(mark)) } var num struct { Percent float64 } err = m.WithContext(s.Ctx).Select(m.Percent.Sum().As("percent")).Where(cond...).Scan(&num) if err != nil { err = s.BE().MysqlError(err) return } app.Redis.Cache.IncrByFloat(s.Ctx, key, num.Percent+initPercent) app.Redis.Cache.Expire(s.Ctx, key, consts.TimeDay) return }) } // 助力成功 func (s *Chop) _chopHelpOk(chopReward *model.FissionChopReward, task *model.FissionCampaignTask, helpMark, userMark string, percent float64) (err error) { user := s.user m := s.GetDao().FissionChopHelp err = m.WithContext(s.Ctx).Create(withdefault.FissionChopHelp(&model.FissionChopHelp{ UserID: chopReward.UserID, CampaignID: chopReward.CampaignID, ChopRewardID: chopReward.ID, Mark: helpMark, Percent: task.Percent, UserMark: userMark, })) if err != nil { err = s.BE().MysqlError(err) return } chopRewardM := s.GetDao().FissionChopReward if percent != 100 { _, err = chopRewardM.WithContext(s.Ctx).Where(chopRewardM.ID.Eq(chopReward.ID)).UpdateSimple(chopRewardM.Percent.Add(task.Percent)) if err != nil { err = s.BE().MysqlError(err) return } return } rewardM := s.GetDao().FissionCampaignReward reward, err := rewardM.WithContext(s.Ctx).Where(rewardM.ID.Eq(chopReward.RewardID)).Take() if err = dbcheck.DbError(err); err != nil { err = s.BE().MysqlError(err) return } ok, err := award.NewAwardIssVerify(s.Ctx, user, reward).DeductStock().IsOK() if err != nil { return } var opts []award.RecordOption if !ok { opts = append(opts, award.RewardRecordStatus( mtype.FissionRewardRecordStatusFail, mtype.FissionRewardRecordFailTypeCli, mtype.FissionRewardRecordFailTypeCliLackMsg, )) } record, _, _, err := award.NewAwardReward(s.Ctx, s.GetDao(), s.cpn.ProjectID, user).One(reward, true, opts...) if err != nil { return } _, err = chopRewardM.WithContext(s.Ctx).Where(chopRewardM.ID.Eq(chopReward.ID)).UpdateSimple( chopRewardM.Percent.Value(100), chopRewardM.RecordID.Value(record.ID), ) if err != nil { err = s.BE().MysqlError(err) return } return } // 一些助力回调处理 func (s *Chop) helpOkNotify(info *helperInfo, chopReward *model.FissionChopReward, helpMark string, percent float64, source int32) (err error) { beHelper, cpn := s.user, s.cpn // 助力次数 + 1 statM := s.GetDao().FissionUserStat _, err = statM.WithContext(s.Ctx).Where(statM.UserID.Eq(beHelper.ID)).UpdateSimple(statM.HelpNum.Add(1)) if err != nil { err = s.BE().MysqlError(err) return } // 如果是点击事件 info 为nil var subUserId, subDeviceId string if source == types.HelpSourceClick { // 平台 platform, dm := strings.ToLower(info.os), strings.ToLower(info.deviceModel) if platform == "macos" { platform = consts.PlatformIOS } if platform == consts.PlatformIOS && dm == consts.PlatformIPad { platform = consts.PlatformIPad } subDeviceId = info.deviceID fissionaction.NewMarkPoint(s.Ctx).OutApp( beHelper.CampaignID, beHelper.ChannelID, beHelper.ID, info.webId, platform, beHelper.LangMark, ).Help( info.actionTime, helpMark, chopReward.RewardID, info.ipAddress, info.os, info.osVersion, info.deviceModel, ) } else { subUserId = info.helper.SubUserID subDeviceId = info.helper.DeviceID fissionaction.NewMarkPoint(s.Ctx).InApp(info.helper).Help(info.actionTime, helpMark, chopReward.RewardID) } langTitle, err := provider.NewLangTitle(s.Ctx, s.cpn, beHelper.LangMark) if err != nil { return } pageTitle, err := langTitle.GetPageTitle() if err != nil { return } // 助力通知 producer.NewOplatformOther(s.Ctx).OpoCampaignHelpOk(types.OpoCampaignHelpOk{ ProjectId: cpn.ProjectID, CampaignHashId: cpn.HashID, CampaignTitle: cpn.Title, PageTitle: pageTitle, HelperUserId: subUserId, HelperDeviceId: subDeviceId, BeHelperUserId: beHelper.SubUserID, BeHelperDeviceId: beHelper.DeviceID, Source: source, }, producer.SetHeaderLang(beHelper.LangMark)) provider.NewFissionClue(s.Ctx, beHelper.ID).ChopHelp(chopReward.ID, s.oldPercent, percent, false) return }