diff --git a/internal/dao/user_dao.go b/internal/dao/user_dao.go index c642c21..2c31032 100644 --- a/internal/dao/user_dao.go +++ b/internal/dao/user_dao.go @@ -1,6 +1,7 @@ package dao import ( + "errors" "fmt" "github.com/gin-gonic/gin" "gorm.io/gorm" @@ -46,3 +47,25 @@ func (u *User) FindUserByName(username string) *entity.User { } return user } + +func (u *User) IncrementTokenVersion(userId int64) (int32, error) { + u1 := &entity.User{} + tx := u.DB.Select("token_version").First(u1, userId) + // fmt.Printf("current user: %v, %d\n", u1, tx.RowsAffected) + if tx.Error != nil { + return 0, tx.Error + } + newTokenVersion := u1.TokenVersion + 1 + tx.Model(&entity.User{}). + Where("id", userId). + Where("token_version", u1.TokenVersion). + Update("token_version", newTokenVersion) + // fmt.Printf("RowsAffected: %d\n\n", tx.RowsAffected) + if tx.Error != nil { + return 0, tx.Error + } + if tx.RowsAffected != 1 { + return 0, errors.New("登录异常") + } + return newTokenVersion, nil +} diff --git a/internal/model/entity/user.go b/internal/model/entity/user.go index cacfa4f..1f7df4c 100644 --- a/internal/model/entity/user.go +++ b/internal/model/entity/user.go @@ -8,7 +8,7 @@ type User struct { Balance int32 `gorm:"column:balance;default:0" json:"balance"` // comment:余额 Avatar string `gorm:"column:avatar" json:"avatar"` // comment:头像 Version int32 `gorm:"column:version;default:0" json:"version"` // comment:更新版本锁 - TokenVersion int32 `gorm:"column:token_version;default:0" json:"tokenVersion"` // comment:token版本,重新登录后之前的token失效,登录丢到缓存中和token过期时间相同 + TokenVersion int32 `gorm:"column:token_version;default:1" json:"tokenVersion"` // comment:token版本,重新登录后之前的token失效,登录丢到缓存中和token过期时间相同 } func (u *User) TableName() string { diff --git a/internal/service/auth.go b/internal/service/auth.go index 09de09b..bd9c056 100644 --- a/internal/service/auth.go +++ b/internal/service/auth.go @@ -72,11 +72,19 @@ func Authorize(ctx *gin.Context) { } sub := &session.Subject{ - Id: user.Id, - Name: user.Username, - Time: time.Now().UnixMilli(), - Avatar: user.Avatar, + Id: user.Id, + Name: user.Username, + Time: time.Now().UnixMilli(), + Avatar: user.Avatar, + TokenVersion: user.TokenVersion + 1, } + // dao.UserDao.UpdateTokenVersion(user.Id, sub.TokenVersion, user.TokenVersion) + tVersion, err := dao.UserDao.IncrementTokenVersion(user.Id) + if err != nil { + ctx.JSON(http.StatusUnauthorized, gin.H{"msg": err.Error()}) + return + } + store.TokenVersions.SetDefault(user.Id, tVersion) token, err := EncodeSubject(sub) if err != nil { @@ -103,18 +111,22 @@ func registerUser(username string, password string) (*entity.User, error) { // 生成头像 avatar := fmt.Sprintf("%d%d%s", time.Now().UnixMilli(), rand.Intn(90)+10, ".jpg") err := govatar.GenerateFile(govatar.MALE, conf.Conf.Game.AvatarPath+avatar) + // 保存用户信息到db if err != nil { return nil, err } user := &entity.User{ - Username: username, - Password: password, - Balance: conf.Conf.Game.GiftChip, - Avatar: avatar, - Version: 0, - } - // TODO error - dao.UserDao.DB.Save(user) + Username: username, + Password: password, + Balance: conf.Conf.Game.GiftChip, + Avatar: avatar, + Version: 0, + TokenVersion: 0, + } + tx := dao.UserDao.DB.Save(user) + if tx.Error != nil { + return nil, tx.Error + } return user, nil } @@ -152,10 +164,15 @@ func SubjectAuthFilter(ctx *gin.Context) { if err != nil { ctx.JSON(http.StatusUnauthorized, gin.H{"msg": "认证失败,请重新登录"}) ctx.Abort() - } else { - // 设置token用户到请求上下文 - ctx.Set(SubjectKey, subject) + return } + if subject.TokenVersion != store.TokenVersions.Get(subject.Id) { + ctx.JSON(http.StatusUnauthorized, gin.H{"msg": "登录已过期"}) + ctx.Abort() + return + } + // 设置token用户到请求上下文 + ctx.Set(SubjectKey, subject) } // EncodeSubject 对subject对象aesCBC加密并返回base64Std编码的 token @@ -206,6 +223,9 @@ func HandleReqIdentity(client *session.NetClient, msg *api.ReqIdentity) (proto.M // client.Close("authorize failed!") return &api.ResFail{Code: 401, Msg: err.Error()}, nil } + if subject.TokenVersion != store.TokenVersions.Get(subject.Id) { + return &api.ResFail{Code: 401, Msg: "登录已过期"}, nil + } account := &session.NetAccount{ Id: subject.Id, @@ -216,8 +236,19 @@ func HandleReqIdentity(client *session.NetClient, msg *api.ReqIdentity) (proto.M BalanceLock: &sync.RWMutex{}, Lock: &sync.Mutex{}, } - // TODO 查询 DB 账户余额 - account = store.SaveAndTryRecoverNetAccounts(account) + // TODO 查询 DB 账户余额,处理断线、重连问题 + current := store.NetAccounts.Get(account.Id) + if current == nil { + store.NetAccounts.SetDefault(account.Id, account) + } else { + // 掉线重连的用户, TODO 重复登录的用户 + current.Client.Write(&api.ResFail{Code: 403, Msg: "该账号在其他地方登录,您已下线"}) + current.Client = account.Client + if current.Player != nil { + current.Player.ProtoWriter = account.Client + } + account = current + } client.Account = account // response diff --git a/internal/service/online.go b/internal/service/online.go new file mode 100644 index 0000000..e8dc296 --- /dev/null +++ b/internal/service/online.go @@ -0,0 +1,37 @@ +package service + +import ( + "fmt" + "texas-poker-bk/internal/service/store" + "texas-poker-bk/internal/session" + "texas-poker-bk/tool/collect" + "time" +) + +func init() { + OfflineQueue := collect.NewDelayQueue(HandleUserOffline) + OfflineQueue.Add(1, nil) +} + +func HandleUserOffline(account *session.NetAccount, t time.Time) { + fmt.Printf("account %d, %s handle offline.\n", account.Id, account.UserName) + if account == nil || store.NetAccounts.Get(account.Id) == nil { + return + } + if account.Player == nil { + // 不在游戏中,直接下线 + removeOnlineUser(account) + return + } + if collect.In(account.Player.GameTable.Stage, 1, 9) { + // TODO account.Player.GameTable.RemovePlayer() + } + +} + +func removeOnlineUser(account *session.NetAccount) { + store.NetAccounts.Delete(account.Id) + store.TokenVersions.Delete(account.Id) + + // TODO 持久化账户金额 +} diff --git a/internal/service/store/store.go b/internal/service/store/store.go index 16c4d8d..d787b28 100644 --- a/internal/service/store/store.go +++ b/internal/service/store/store.go @@ -1,36 +1,23 @@ package store import ( - "errors" - "fmt" "github.com/patrickmn/go-cache" "strconv" - "sync" "sync/atomic" - "texas-poker-bk/api" "texas-poker-bk/internal/game" "texas-poker-bk/internal/session" "time" ) var ( - TableNo *atomic.Int32 - LobbyTables map[int32]*game.Table - lobbyLock *sync.Mutex + TableNo *atomic.Int32 + + LobbyTables *store[int32, *game.Table] // NetAccounts 存储一下在线用户, TODO 连接终端,未在牌局或牌局未开始中回收账号,机器人代打后回收账号 - NetAccounts map[int64]*session.NetAccount - accountLock *sync.Mutex - - // 存储登录 token version - //TokenVersionStore map[int64]int32 - //tokenVersionLock *sync.Mutex - - // 后面使用多个缓存实体,减少不同类型锁时间 - storeCache *cache.Cache - // a. cache in one -> type conv - // b. cache in many -> space/code more - TokenVStore *store[int64, int32] + NetAccounts *store[int64, *session.NetAccount] + + TokenVersions *store[int64, int32] ) const ( @@ -57,55 +44,46 @@ func (s *store[K, V]) Get(k K) V { if !found { return s.zeroValue } - return v + return v.(V) } func (s *store[K, V]) Delete(k K) { s.c.Delete(s.k2str(k)) } +func (s *store[K, V]) ForEach(f func(k string, v V)) { + for k, v := range s.c.Items() { + f(k, v.Object.(V)) + } +} + +func (s *store[K, V]) Count() int { + return s.c.ItemCount() +} + func init() { - TokenVStore = &store[int64, int32]{ + LobbyTables = &store[int32, *game.Table]{ + c: cache.New(cache.NoExpiration, cache.NoExpiration), + zeroValue: nil, + k2str: func(k int32) string { + return strconv.Itoa(int(k)) + }, + } + NetAccounts = &store[int64, *session.NetAccount]{ c: cache.New(cache.NoExpiration, cache.NoExpiration), - zeroValue: 0, + zeroValue: nil, k2str: func(k int64) string { return strconv.FormatInt(k, 10) }, } - TableNo = &atomic.Int32{} // 牌桌编号计数器 - - LobbyTables = make(map[int32]*game.Table, 16) - lobbyLock = &sync.Mutex{} - - NetAccounts = make(map[int64]*session.NetAccount) - accountLock = &sync.Mutex{} -} - -func SaveNewTable(table *game.Table) error { - lobbyLock.Lock() - defer lobbyLock.Unlock() - if LobbyTables[table.TableNo] != nil { - return errors.New(fmt.Sprintf("table no %d exists!", table.TableNo)) + TokenVersions = &store[int64, int32]{ + c: cache.New(cache.NoExpiration, cache.NoExpiration), + zeroValue: -1, + k2str: func(k int64) string { + return strconv.FormatInt(k, 10) + }, } - LobbyTables[table.TableNo] = table - return nil -} -// SaveAndTryRecoverNetAccounts 保存一下在线账户映射关系 TODO 处理断线、重连问题 -func SaveAndTryRecoverNetAccounts(account *session.NetAccount) *session.NetAccount { - accountLock.Lock() - defer accountLock.Unlock() - current := NetAccounts[account.Id] - if current == nil { - NetAccounts[account.Id] = account - return account - } - // 掉线重连的用户, TODO 重复登录的用户 - current.Client.Write(&api.ResFail{Code: 403, Msg: "该账号在其他地方登录,您已下线"}) - current.Client = account.Client - if current.Player != nil { - current.Player.ProtoWriter = account.Client - } - return current + TableNo = &atomic.Int32{} // 牌桌编号计数器 } diff --git a/internal/service/table.go b/internal/service/table.go index 7eb7850..d2ead7f 100644 --- a/internal/service/table.go +++ b/internal/service/table.go @@ -80,20 +80,21 @@ func HandleReqCreateTable(account *session.NetAccount, msg *api.ReqCreateTable) for i := 0; i < int(robotNum); i++ { table.Robots[i] = &game.Robot{} } - err := store.SaveNewTable(table) - if err != nil { - return &api.ResFail{Msg: err.Error()}, nil - } + store.LobbyTables.SetDefault(table.TableNo, table) + //err := store.SaveNewTable(table) + //if err != nil { + // return &api.ResFail{Msg: err.Error()}, nil + //} return &api.ResCreateTable{TableNo: table.TableNo}, nil } // HandleReqLobbyView 返回当前所有桌面和玩家数量 func HandleReqLobbyView(account *session.NetAccount, msg *api.ReqLobbyView) (proto.Message, error) { res := &api.ResLobbyView{} - if store.LobbyTables == nil || len(store.LobbyTables) == 0 { + if store.LobbyTables.Count() == 0 { return res, nil } - tables := make([]*api.LobbyTable, len(store.LobbyTables)) + tables := make([]*api.LobbyTable, store.LobbyTables.Count()) if account.Player != nil { res.CurTableNo = account.Player.GameTable.TableNo } @@ -101,7 +102,7 @@ func HandleReqLobbyView(account *session.NetAccount, msg *api.ReqLobbyView) (pro // 遍历 store tables 转换为视图层结构体 idx := 0 - for _, t := range store.LobbyTables { + store.LobbyTables.ForEach(func(k string, t *game.Table) { table := &api.LobbyTable{TableNo: t.TableNo, PlayerNum: t.PlayerNum, RobotNum: t.RobotNum} table.Players = make([]*api.LobbyPlayer, t.PlayerNum+t.RobotNum) @@ -126,7 +127,7 @@ func HandleReqLobbyView(account *session.NetAccount, msg *api.ReqLobbyView) (pro tables[idx] = table idx++ - } + }) // 牌桌号降序 sort.Slice(tables, func(i, j int) bool { return tables[i].TableNo > tables[j].TableNo @@ -142,7 +143,7 @@ func HandleReqJoinTable(account *session.NetAccount, msg *api.ReqJoinTable) (pro if account.Player != nil { return &api.ResFail{Msg: fmt.Sprintf("当前已加入#%d牌桌", account.Player.GameTable.TableNo)}, nil } - table := store.LobbyTables[msg.TableNo] + table := store.LobbyTables.Get(msg.TableNo) if table == nil { return &api.ResFail{Msg: "牌桌不存在"}, nil } @@ -209,7 +210,7 @@ func HandleReqKickOutTable(player *game.Player, msg *api.ReqKickOutTable) (proto // 从牌桌移除该玩家 player.GameTable.Players[i] = nil // 被对踢出人发送消息 - account := store.NetAccounts[p.Id] + account := store.NetAccounts.Get(p.Id) account.Player = nil // 解除账户绑定 account.IncrementBalance(player.Chip) // 筹码返还账户 p.ProtoWriter.Write(&api.ResKickOutTable{}) // 被踢玩家消息 @@ -242,7 +243,7 @@ func HandleReqLeaveTable(player *game.Player, msg *api.ReqLeaveTable) (proto.Mes break } } - account := store.NetAccounts[player.Id] + account := store.NetAccounts.Get(player.Id) account.Player = nil // 解除账户绑定 account.IncrementBalance(player.Chip) // 筹码返还账户 @@ -318,13 +319,13 @@ func HandleReqDismissGameTable(player *game.Player, msg *api.ReqDismissGameTable return &api.ResFail{Msg: fmt.Sprintf("#%d,牌局进行中", player.GameTable.Stage)}, nil } player.GameTable.Stage = 9 - delete(store.LobbyTables, player.GameTable.TableNo) + store.LobbyTables.Delete(player.GameTable.TableNo) // 通知所有玩家,解除账号绑定,结算玩家金额 for i, p := range player.GameTable.Players { if p == nil { continue } - account := store.NetAccounts[p.Id] + account := store.NetAccounts.Get(p.Id) account.IncrementBalance(p.Chip) account.Player = nil player.GameTable.Players[i] = nil diff --git a/internal/session/subject.go b/internal/session/subject.go index f3e0bfb..0e6811c 100644 --- a/internal/session/subject.go +++ b/internal/session/subject.go @@ -2,8 +2,9 @@ package session // Subject 认证对象 type Subject struct { - Id int64 `json:"id,omitempty"` // 用户id - Name string `json:"name,omitempty"` // 用户名 - Avatar string `json:"avatar,omitempty"` // 头像 TODO 缓存,不加到token - Time int64 `json:"time,omitempty"` // 生成时间戳 + Id int64 `json:"id,omitempty"` // 用户id + Name string `json:"name,omitempty"` // 用户名 + Avatar string `json:"avatar,omitempty"` // 头像 TODO 缓存,不加到token + Time int64 `json:"time,omitempty"` // 生成时间戳 + TokenVersion int32 `json:"tv,omitempty"` }