package antiCheating import ( "context" "encoding/json" "errors" "fmt" "gadmin/config" "gadmin/internal/gorm/model" "gadmin/internal/gorm/query" "gadmin/package/gmdata" "gadmin/utility" "github.com/go-redis/redis" "github.com/sirupsen/logrus" "gorm.io/gen" "gorm.io/gorm" "io/ioutil" "os" "sort" "strconv" "strings" "sync" "time" ) type Combat struct { ctx context.Context sync.Mutex YearMonth string } type CombatOption func(*Combat) func WithYearMonth(yearMonth string) CombatOption { return func(combat *Combat) { combat.YearMonth = yearMonth } } func NewCombat(options ...CombatOption) *Combat { combat := &Combat{ ctx: context.Background(), } for _, f := range options { f(combat) } return combat } func (c *Combat) Run() { c.Lock() defer c.Unlock() logrus.Infof("异常数据筛选...") c.getJson() c.getCombatData() } // 读取角色json var RoleMap map[int64]map[int]gmdata.Role var SkillGroupMap map[int64][]gmdata.Exskill func (c *Combat) getJson() { if len(RoleMap) != 0 && len(SkillGroupMap) != 0 { return } confPath := os.Getenv("JSON_PATH") + "/" + os.Getenv("JSON_VERSION") err := gmdata.LoadItemData(confPath, "Role.json", &gmdata.Roles) if err != nil { logrus.Errorf("读取Role.json错误:%v", err) return } RoleMap = make(map[int64]map[int]gmdata.Role) for _, item := range gmdata.Roles { if RoleMap[item.Type] == nil { RoleMap[item.Type] = make(map[int]gmdata.Role) } RoleMap[item.Type][item.Evolution] = item } err = gmdata.LoadItemData(confPath, "exskill.json", &gmdata.Exskills) if err != nil { logrus.Errorf("读取exskill.json错误:%v", err) return } SkillGroupMap = make(map[int64][]gmdata.Exskill) for _, item := range gmdata.Exskills { SkillGroupMap[int64(item.SkillGroup)] = append(SkillGroupMap[int64(item.SkillGroup)], item) } } func (c *Combat) coverRoleInfo(id int64) map[string]int { roleInfo := make(map[string]int) roleInfo["type"] = c.coverIDToSlice(id, 8) % 100 roleInfo["evolution"] = c.coverIDToSlice(id, 4) % 1000 roleInfo["level"] = c.coverIDToSlice(id, 1) % 1000 roleInfo["break"] = int(id % 10) return roleInfo } func (c *Combat) coverIDToSlice(id int64, length int) int { idStr := strconv.FormatInt(id, 10) substring := idStr[:len(idStr)-length] result, _ := strconv.Atoi(substring) return result } func (c *Combat) getRoleSkillGroupIds(roleType int, evolution int, level int) []int { skillGroupIdArr := make([]int, 0) if role, ok := RoleMap[int64(roleType)][evolution]; ok { for i, item := range role.SkillUnlockLevel { if level >= item { skillGroupIdArr = append(skillGroupIdArr, role.Skill[i]) } } } return skillGroupIdArr } func (c *Combat) getRoleSkillToID(skillGroupId, evolution int) (gmdata.Exskill, error) { if skillGroup, ok := SkillGroupMap[int64(skillGroupId)]; ok { sort.Slice(skillGroup, func(i, j int) bool { return skillGroup[i].SkillUnlock < skillGroup[j].SkillUnlock }) exskill := gmdata.Exskill{} for _, skill := range skillGroup { if evolution >= 0 { exskill = skill } } return exskill, nil } return gmdata.Exskill{}, errors.New("未找到技能信息") } func (c *Combat) getMax(x, y float64) float64 { if x > y { return x } return y } // 获取当月战斗数据的player_id集合 func (c *Combat) getCombatData() { table := config.DB.Scopes(model.ChapterLogsUserDetailTableSetDate(c.YearMonth)) chapterModel := query.Use(table).ChapterLogsUserDetail.Table("") chapterQuery := chapterModel.WithContext(c.ctx) alarmModel := query.Use(config.DB).GameAlarmCombatLog alarmQuery := alarmModel.WithContext(c.ctx) //取lastId 先从redis,再从mysql lastIdMap, err := config.LogRedis.HGetAll("alarm:cheating:id").Result() if err != nil && !errors.Is(err, redis.Nil) { logrus.Errorf("redis获取作弊同步数据ID失败:%v", err) return } lastId, _ := strconv.ParseInt(lastIdMap["id"], 10, 64) yearMonth := lastIdMap["yearMonth"] if yearMonth != c.YearMonth { lastId = 0 } if lastId == 0 { alarmInfo, err := alarmQuery. Where(alarmModel.YearMonth.Eq(c.YearMonth)). Order(alarmModel.OriginTime.Desc(), alarmModel.OriginID.Desc()).First() if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { lastId = 0 } else { lastId = alarmInfo.OriginID } } chapterList := make([]*model.ChapterLogsUserDetail, 0) chapterQuery = chapterQuery.WithContext(c.ctx). Select(chapterModel.ID, chapterModel.UserID, chapterModel.ServerID, chapterModel.Extra, chapterModel.EventAtNs) if lastId != 0 { chapterQuery = chapterQuery.Where(chapterModel.ID.Gt(lastId)) } err = chapterQuery.FindInBatches(&chapterList, 1000, func(tx gen.Dao, batch int) error { // 批量从cos获取extra数据 extraPaths := make([]string, 0) for _, item := range chapterList { if !strings.Contains(item.Extra, "{") { extraPaths = append(extraPaths, item.Extra) } } extraPathMap := &sync.Map{} var wg sync.WaitGroup smallPaths := utility.SplitStringsBySize(extraPaths, 100) for _, paths := range smallPaths { wg.Add(1) var wg1 sync.WaitGroup for _, path := range paths { wg1.Add(1) go func(path string) { defer wg1.Done() if response, err := config.GetCOSClient().Object.Get(context.Background(), path, nil); err != nil { logrus.Errorf("从cos中获取extra发生错误: %v", err) return } else { extraByt, err := ioutil.ReadAll(response.Body) response.Body.Close() if err != nil { logrus.Errorf("读取cos数据出错: %v", err) return } else { extraPathMap.Store(path, string(extraByt)) } } }(path) } wg1.Wait() wg.Done() } wg.Wait() //遍历 for _, item := range chapterList { var extra Extra extraStr := "" if !strings.Contains(item.Extra, "{") { if val, ok := extraPathMap.Load(item.Extra); ok { extraStr = val.(string) } } else { extraStr = item.Extra } err := json.Unmarshal([]byte(extraStr), &extra) if err != nil { logrus.Errorf("战斗数据解析失败: %v", err) continue } if len(extra.Harm) == 0 { continue } var tempHarm float64 var data *model.GameAlarmCombatLog for _, harm := range extra.Harm { if harm[0] != 1 { continue } roleAttr := extra.Attr[harm[2]] roleInfo := c.coverRoleInfo(roleAttr.RoleId) if roleInfo["type"] != 1 { continue } skillGroupIds := c.getRoleSkillGroupIds(roleInfo["type"], roleInfo["evolution"], roleInfo["level"]) if len(skillGroupIds) == 0 { continue } for _, skillGroupId := range skillGroupIds { skillInfo, err := c.getRoleSkillToID(skillGroupId, roleInfo["evolution"]) if err != nil { return err } if skillInfo.SkillType == 1 { /* 倍率 */ var rate = skillInfo.SkillValue[2] /** 伤害估值 */ dd := float64(roleAttr.Attr.InitDps) * rate * 1.2 // 如果实际伤害大于估值 说明异常 if float64(harm[1]) > dd { //取最大值 if tempHarm < float64(harm[1]) { tempHarm = float64(harm[1]) ratio := fmt.Sprintf("%.2f", tempHarm/dd) ratioFloat, err := strconv.ParseFloat(ratio, 10) if err != nil { return err } roleInfoJson, err := json.Marshal(roleInfo) if err != nil { return err } data = &model.GameAlarmCombatLog{ OriginID: item.ID, OriginTime: strconv.FormatInt(item.EventAtNs, 10), YearMonth: c.YearMonth, PlayerID: item.UserID, ServerID: item.ServerID, Dps: float64(roleAttr.Attr.InitDps), //秒伤 Rate: rate, //技能倍率 Valuation: dd, //估值伤害 Damage: tempHarm, //实际伤害 Ratio: ratioFloat, //实际伤害/估值伤害 RoleInfo: string(roleInfoJson), AlarmData: item.Extra, CreatedAt: time.Now(), } } } } } } //循环harm完成 if data != nil { err = alarmQuery.Create(data) if err != nil { logrus.Errorf("异常数据插入失败: %v", err) return err } } } //保存lastId至redis id := chapterList[len(chapterList)-1].ID err := config.LogRedis.HMSet("alarm:cheating:id", map[string]interface{}{"id": id, "yearMonth": c.YearMonth}).Err() if err != nil { logrus.Errorf("redis保存作弊同步数据ID失败") return err } time.Sleep(time.Millisecond * 100) return nil }) if err != nil { logrus.Errorf("检测失败:%v", err) return } } type Extra struct { Attr []ExtraAttr `json:"attr"` Hp [][]int `json:"hp"` Harm [][]int64 `json:"harm"` } type ExtraAttr struct { RoleId int64 `json:"roleId"` State int `json:"state"` Attr ExtraAttrInfo `json:"attr"` } type ExtraAttrInfo struct { RoleId int `json:"roleId"` InitAtk int64 `json:"initAtk"` AtkUp float64 `json:"atkUp"` InitEleAtk int64 `json:"initEleAtk"` EleAtkUp float64 `json:"eleAtkUp"` BossUP float64 `json:"BossUP"` MonUp float64 `json:"MonUP"` InitDps int64 `json:"initDps"` }