package user_m import ( "encoding/json" "fmt" "github.com/pkg/errors" "hilo-user/_const/enum/robot_e" "hilo-user/_const/enum/user_e" "hilo-user/domain" "hilo-user/domain/cache/robot_c" "hilo-user/domain/model/group_m" "hilo-user/domain/model/user_m" "hilo-user/resource/config" "hilo-user/resource/mysql" "hilo-user/rpc" "time" ) // 机器人 type GameRobot struct { mysql.Entity UserId mysql.ID // 用户id State robot_e.RobotState // 机器人状态 OpStep robot_e.RobotOp // 当前所在步骤 GroupId string // 群组id MicIndex int // 麦位 GameUid mysql.ID // 游戏id MgId string // sdk游戏Id LastUseTime time.Time // 上次使用的时间 model *domain.Model `gorm:"-"` // db、ctx Game GameTiny `gorm:"-"` // 需要加入的游戏 User user_m.User `gorm:"-"` // 用户信息 Group *group_m.GroupInfo `gorm:"-"` // 群组信息 Token string `gorm:"-"` // jwt token Over bool `gorm:"-"` // 是否结束 Err error `gorm:"-"` // 错误日志 // service层的GameOpt GameOpt func(userId, userId uint64, code, ext, lang, userCode, txGroupId string, opt user_e.GameOpt, seatIdx int8, isAi bool) error `gorm:"-"` } // 机器人日志 type GameRobotLog struct { mysql.Entity RobotUid mysql.ID Op robot_e.RobotOp // 机器人操作 DataId string // 房间号 or 麦位 or 游戏id } // 标记正在使用中 func (robot *GameRobot) MarkUsing(model *domain.Model) error { updates := map[string]interface{}{ "state": robot_e.RobotStateUsing, "last_use_time": time.Now(), } return model.Db.Model(GameRobot{}).Where("id = ?", robot.ID). Updates(updates).Error } // 标记闲置 func (robot *GameRobot) MarkIdle(model *domain.Model) { updates := map[string]interface{}{ "state": robot_e.RobotStateIdle, "group_id": "", "mic_index": -1, "user_uid": 0, "mg_id": "", "op_step": robot_e.RobotOpNone, } if err := model.Db.Model(GameRobot{}).Where("id = ?", robot.ID). Updates(updates).Error; err != nil { robot.model.Log.Errorf("MarkIdle fail:%v", err) } } // 机器人干活了 func (robot *GameRobot) Run() { robot.model = domain.CreateModelNil() robot.model.Log = robot.model.Log.WithField("GameRobotRun", robot.ID) var err error defer func() { if err != nil { robot.model.Log.Errorf("Run fail,robotId:%v,uid:%v,err:%v", robot.ID, robot.UserId, err) // reset to idle robot.MarkIdle(robot.model) robot.Over = true robot.Err = err } }() // 检查游戏状态 if robot.Game.Status == user_e.GameStatusNoStart { // 未开始 go robot.heartbeat() // 心跳自愈 // 进入游戏流程 switch robot.OpStep { case robot_e.RobotOpNone: if err = robot.GroupIn(); err != nil { return } fallthrough case robot_e.RobotOpGroupIn: if err = robot.MicIn(); err != nil { return } fallthrough case robot_e.RobotOpMicIn: if err = robot.GameIn(); err != nil { return } fallthrough default: robot.model.Log.Infof("may in other step:%v", robot.OpStep) } } else { robot.model.Log.Warnf("user status not normal:%v", robot.Game) } } // 进程重启后的机器人自愈 // 如果走过进入游戏了,则继续走心跳逻辑 // 否则,直接走 func (robot *GameRobot) ReRun() { robot.model = domain.CreateModelNil() robot.model.Log = robot.model.Log.WithField("GameRobotReRun", robot.ID) if robot.GameUid > 0 { go robot.heartbeat() } else { robot.Leave("ReRun") } } // 进房 func (robot *GameRobot) GroupIn() error { url := config.GetUrlConfig().BIZ_HTTP + "/v1/imGroup/in" header := map[string]string{ "nonce": "hilo", "token": robot.Token, } form := map[string]string{ "groupId": robot.Game.GroupId, } // http to biz if resp, err := rpc.HttpPutForm(robot.model, url, header, form); err != nil { return err } else { type GroupInResp struct { Code int `json:"code"` } res := new(GroupInResp) if err = json.Unmarshal(resp, res); err != nil { return err } else { if res.Code != 200 { return errors.New(fmt.Sprintf("Group In Not 200:%v", res)) } else { // 复制群组id robot.GroupId = robot.Game.GroupId } } } // no error return robot.model.Transaction(func(txModel *domain.Model) error { updates := map[string]interface{}{ "group_id": robot.GroupId, "op_step": robot_e.RobotOpGroupIn, } err := txModel.Db.Model(GameRobot{}).Where("id = ?", robot.ID). Updates(updates).Error if err != nil { return err } return txModel.Db.Create(&GameRobotLog{ RobotUid: robot.ID, Op: robot_e.RobotOpGroupIn, DataId: robot.GroupId, }).Error }) } // 上麦 func (robot *GameRobot) MicIn() error { url := config.GetUrlConfig().BIZ_HTTP + "/v1/imGroup/mic/in" header := map[string]string{ "nonce": "hilo", "token": robot.Token, } form := map[string]string{ "groupUuid": robot.GroupId, "i": "", // 空则随意上一个空位置 } // http to biz if resp, err := rpc.HttpPostForm(robot.model, url, header, form); err != nil { robot.model.Log.Errorf("Mic In fail:%v", err) return err } else { type MicInResp struct { Code int `json:"code"` Data struct { MicIndex int `json:"micIndex"` } `json:"data"` } res := new(MicInResp) if err = json.Unmarshal(resp, res); err != nil { robot.model.Log.Errorf(fmt.Sprintf("Mic In json:%v", res)) return err } else { if res.Code != 200 || res.Data.MicIndex < 0 { robot.model.Log.Errorf(fmt.Sprintf("Mic In Not 200:%v,groupId:%v", res, robot.GroupId)) if res.Code == 12008 { // 群上麦失败,群先做cd,能上麦的情况太多 _ = robot_c.CDMicInGroup(robot.model, robot.GroupId) } return errors.New(fmt.Sprintf("Mic In Not 200:%v", res)) } else { robot.MicIndex = res.Data.MicIndex // 赋值麦位 } } } return robot.model.Transaction(func(txModel *domain.Model) error { updates := map[string]interface{}{ "mic_index": robot.MicIndex, "op_step": robot_e.RobotOpMicIn, } err := robot.model.Db.Model(GameRobot{}).Where("id = ?", robot.ID). Updates(updates).Error if err != nil { return err } return txModel.Db.Create(&GameRobotLog{ RobotUid: robot.ID, Op: robot_e.RobotOpMicIn, DataId: fmt.Sprintf("%s-%d", robot.GroupId, robot.MicIndex), }).Error }) } // 加入游戏 func (robot *GameRobot) GameIn() error { if err := robot.GameOpt(robot.UserId, robot.Game.GameUid, robot.User.Code, robot.User.ExternalId, "en", "", robot.GroupId, user_e.GameOptJoin, -1, true); err != nil { robot.model.Log.Errorf("Game In fail,userId:%v,err:%v", robot.UserId, err) return err } err := robot.model.Transaction(func(txModel *domain.Model) error { updates := map[string]interface{}{ "user_uid": robot.Game.GameUid, "mg_id": robot.Game.MgId, "op_step": robot_e.RobotOpGameIn, } err := robot.model.Db.Model(GameRobot{}).Where("id = ?", robot.ID). Updates(updates).Error if err != nil { return err } // 赋值游戏id robot.GameUid = robot.Game.GameUid return txModel.Db.Create(&GameRobotLog{ RobotUid: robot.ID, Op: robot_e.RobotOpGameIn, DataId: fmt.Sprintf("%d", robot.GameUid), }).Error }) if err != nil { return err } // GameIn成功之后腾讯云通知 robot.TxGroupInPush() return nil } // 机器人离开 // 下麦+离开房间+退出游戏 func (robot *GameRobot) Leave(reason string) { // 异常情况会直接leave,保护一下model未赋值 if robot.model == nil { robot.model = domain.CreateModelNil() } robot.model.Log.Infof("robot leave,id:%v,reason:%v", robot.ID, reason) robot.MicLeave() robot.GroupLeave() robot.GameOut() robot.MarkIdle(robot.model) } // 下麦 func (robot *GameRobot) MicLeave() { // 未上麦 if robot.MicIndex < 0 { return } url := config.GetUrlConfig().BIZ_HTTP + "/v1/imGroup/mic/leave" header := map[string]string{ "nonce": "hilo", "token": robot.Token, } form := map[string]string{ "groupUuid": robot.GroupId, "i": fmt.Sprintf("%d", robot.MicIndex), // } // http to biz if resp, err := rpc.HttpPostForm(robot.model, url, header, form); err != nil { robot.model.Log.Errorf("Mic Leave fail:%v", err) return } else { type MicLeaveResp struct { Code int `json:"code"` } res := new(MicLeaveResp) if err = json.Unmarshal(resp, res); err != nil { robot.model.Log.Errorf(fmt.Sprintf("Mic Leave json:%v", res)) return } else { if res.Code != 200 { robot.model.Log.Errorf(fmt.Sprintf("Mic Leave Not 200:%v", res)) } } } if err := robot.model.Db.Create(&GameRobotLog{ RobotUid: robot.ID, Op: robot_e.RobotOpMicLeave, DataId: fmt.Sprintf("%s-%d", robot.GroupId, robot.MicIndex), }).Error; err != nil { robot.model.Log.Errorf("log fail:%v", err) } } // 离房 func (robot *GameRobot) GroupLeave() { if len(robot.GroupId) <= 0 { return } url := config.GetUrlConfig().BIZ_HTTP + "/v1/imGroup/leave" header := map[string]string{ "nonce": "hilo", "token": robot.Token, } form := map[string]string{ "groupId": robot.GroupId, } // http to biz if resp, err := rpc.HttpPostForm(robot.model, url, header, form); err != nil { robot.model.Log.Errorf("GroupLeave fail:%v", err) return } else { type GroupLeaveResp struct { Code int `json:"code"` } res := new(GroupLeaveResp) if err = json.Unmarshal(resp, res); err != nil { robot.model.Log.Errorf(fmt.Sprintf("Group Leave json :%v", res)) return } else { if res.Code != 200 { robot.model.Log.Errorf(fmt.Sprintf("Group Leave Not 200:%v", res)) } } } if err := robot.model.Db.Create(&GameRobotLog{ RobotUid: robot.ID, Op: robot_e.RobotOpGroupLeave, DataId: robot.GroupId, }).Error; err != nil { robot.model.Log.Errorf("log fail:%v", err) } } // 离开游戏 func (robot *GameRobot) GameOut() { if robot.GameUid <= 0 || len(robot.GroupId) <= 0 { return } if err := robot.GameOpt(robot.UserId, robot.GameUid, robot.User.Code, robot.User.ExternalId, "en", "", robot.GroupId, user_e.GameOptExit, -1, true); err != nil { robot.model.Log.Errorf("Game Out fail,userId:%v,err:%v", robot.UserId, err) return } if err := robot.model.Db.Create(&GameRobotLog{ RobotUid: robot.ID, Op: robot_e.RobotOpGameOut, DataId: fmt.Sprintf("%d", robot.GameUid), }).Error; err != nil { robot.model.Log.Errorf("log fail:%v", err) } } // 腾讯云麦位通知 func (robot *GameRobot) TxGroupInPush() { if robot.Group == nil || robot.User.ID <= 0 { return } url := config.GetUrlConfig().BIZ_HTTP + "/inner/micAllRPush" header := map[string]string{ "nonce": "hilo", "token": robot.Token, } form := map[string]string{ "imGroupId": robot.Group.ImGroupId, "externalId": robot.User.ExternalId, } // http to biz if resp, err := rpc.HttpPostForm(robot.model, url, header, form); err != nil { robot.model.Log.Errorf("GroupLeave fail:%v", err) return } else { type Resp struct { Code int `json:"code"` } res := new(Resp) if err = json.Unmarshal(resp, res); err != nil { robot.model.Log.Errorf(fmt.Sprintf("TxGroupInPush json :%v", res)) return } else { if res.Code != 200 { robot.model.Log.Errorf(fmt.Sprintf("TxGroupInPush Not 200:%v", res)) } } } } // 机器人心跳自愈 func (robot *GameRobot) heartbeat() { // robot.GameUid是mysql/执行完加入游戏才有 userId := robot.GameUid if userId <= 0 { // 这里是未加入游戏前的userId userId = robot.Game.GameUid } ticker := time.NewTicker(time.Second) defer ticker.Stop() for { select { case <-ticker.C: user, err := getGameInfo(robot.model, userId) if robot.Over || err != nil || user_e.GameStatus(user.Status) == user_e.GameStatusEnd { robot.Leave("robot over") return } } } } // 根据id获取游戏信息 func getGameInfo(model *domain.Model, userUid mysql.ID) (*GameInfo, error) { user := new(GameInfo) err := model.DB().Model(user).Where("id = ?", userUid).First(user).Error return user, err }