移除login直连数据库,将数据库操作交给db服处理

This commit is contained in:
liuxiaobo 2025-06-02 21:34:39 +08:00
parent a2ecd518aa
commit 78283ada73
24 changed files with 485 additions and 353 deletions

View File

@ -6,7 +6,7 @@ import (
) )
const ( const (
AccountNormal = 1 // 正常 //AccountNormal = 1 // 正常
AccountFrozen = 2 // 冻结 AccountFrozen = 2 // 冻结
AccountBanned = 3 // 封禁 AccountBanned = 3 // 封禁
) )
@ -33,7 +33,7 @@ func (u UserAccount) GetId() uint {
// 玩家登录记录表 // 玩家登录记录表
type UserLoginLog struct { type UserLoginLog struct {
gorm.Model gorm.Model
PlayerID uint `gorm:"index"` // 关联玩家ID UID uint `gorm:"index"` // 关联玩家ID
LoginIP string `gorm:"type:varchar(45);not null"` // 登录IP LoginIP string `gorm:"type:varchar(45);not null"` // 登录IP
LoginTime time.Time `gorm:"type:TIMESTAMP;default:CURRENT_TIMESTAMP"` // 登录时间 LoginTime time.Time `gorm:"type:TIMESTAMP;default:CURRENT_TIMESTAMP"` // 登录时间
DeviceInfo string `gorm:"type:varchar(255)"` // 设备信息(JSON格式) DeviceInfo string `gorm:"type:varchar(255)"` // 设备信息(JSON格式)

View File

@ -10,7 +10,8 @@ enum ErrCode
LoginUserOrPwdErr = 102; // LoginUserOrPwdErr = 102; //
AccountFrozen = 103; // AccountFrozen = 103; //
AccountBanned = 104; // AccountBanned = 104; //
RegisterUserExist = 120; // RegisterUserExist = 110; //
VersionTooLow = 115; //
} }

View File

@ -8,6 +8,7 @@ enum ServiceTypeId
STI_Gate = 100; // id STI_Gate = 100; // id
STI_Login = 101; // STI_Login = 101; //
STI_Chat = 102; // STI_Chat = 102; //
STI_DB = 103; // db服
} }

View File

@ -30,7 +30,8 @@ const (
ErrCode_LoginUserOrPwdErr ErrCode = 102 // 帐号或密码错误 ErrCode_LoginUserOrPwdErr ErrCode = 102 // 帐号或密码错误
ErrCode_AccountFrozen ErrCode = 103 // 帐号已冻结 ErrCode_AccountFrozen ErrCode = 103 // 帐号已冻结
ErrCode_AccountBanned ErrCode = 104 // 帐号已封禁 ErrCode_AccountBanned ErrCode = 104 // 帐号已封禁
ErrCode_RegisterUserExist ErrCode = 120 // 已有该帐号,无法注册 ErrCode_RegisterUserExist ErrCode = 110 // 已有该帐号,无法注册
ErrCode_VersionTooLow ErrCode = 115 // 版本太低,无法登陆
) )
// Enum value maps for ErrCode. // Enum value maps for ErrCode.
@ -42,7 +43,8 @@ var (
102: "LoginUserOrPwdErr", 102: "LoginUserOrPwdErr",
103: "AccountFrozen", 103: "AccountFrozen",
104: "AccountBanned", 104: "AccountBanned",
120: "RegisterUserExist", 110: "RegisterUserExist",
115: "VersionTooLow",
} }
ErrCode_value = map[string]int32{ ErrCode_value = map[string]int32{
"OK": 0, "OK": 0,
@ -51,7 +53,8 @@ var (
"LoginUserOrPwdErr": 102, "LoginUserOrPwdErr": 102,
"AccountFrozen": 103, "AccountFrozen": 103,
"AccountBanned": 104, "AccountBanned": 104,
"RegisterUserExist": 120, "RegisterUserExist": 110,
"VersionTooLow": 115,
} }
) )
@ -87,7 +90,7 @@ var File_code_proto protoreflect.FileDescriptor
const file_code_proto_rawDesc = "" + const file_code_proto_rawDesc = "" +
"\n" + "\n" +
"\n" + "\n" +
"code.proto\x12\x02pb*\x86\x01\n" + "code.proto\x12\x02pb*\x99\x01\n" +
"\aErrCode\x12\x06\n" + "\aErrCode\x12\x06\n" +
"\x02OK\x10\x00\x12\r\n" + "\x02OK\x10\x00\x12\r\n" +
"\tSystemErr\x10\x01\x12\x10\n" + "\tSystemErr\x10\x01\x12\x10\n" +
@ -95,7 +98,8 @@ const file_code_proto_rawDesc = "" +
"\x11LoginUserOrPwdErr\x10f\x12\x11\n" + "\x11LoginUserOrPwdErr\x10f\x12\x11\n" +
"\rAccountFrozen\x10g\x12\x11\n" + "\rAccountFrozen\x10g\x12\x11\n" +
"\rAccountBanned\x10h\x12\x15\n" + "\rAccountBanned\x10h\x12\x15\n" +
"\x11RegisterUserExist\x10xB\x11Z\x0fcommon/proto/pbb\x06proto3" "\x11RegisterUserExist\x10n\x12\x11\n" +
"\rVersionTooLow\x10sB\x11Z\x0fcommon/proto/pbb\x06proto3"
var ( var (
file_code_proto_rawDescOnce sync.Once file_code_proto_rawDescOnce sync.Once

View File

@ -22,7 +22,7 @@ const (
) )
// 命名规则: // 命名规则:
// 1. 所有游戏id都在msgId.proto的MsgId中定义前缀需有C2S,S2C,Ntf三种之一后缀统一为Id // 1. 所有消息id都在msgId.proto的MsgId中定义前缀需有C2S,S2C,Ntf三种之一后缀统一为Id
// 2. 所有错误码都在code.proto的ErrCode中定义 // 2. 所有错误码都在code.proto的ErrCode中定义
// 3. 所有消息名为对应消息id去掉后缀Id组成 // 3. 所有消息名为对应消息id去掉后缀Id组成
type MsgId int32 type MsgId int32

View File

@ -28,6 +28,7 @@ const (
ServiceTypeId_STI_Gate ServiceTypeId = 100 // 网关id ServiceTypeId_STI_Gate ServiceTypeId = 100 // 网关id
ServiceTypeId_STI_Login ServiceTypeId = 101 // 登陆服 ServiceTypeId_STI_Login ServiceTypeId = 101 // 登陆服
ServiceTypeId_STI_Chat ServiceTypeId = 102 // 聊天服 ServiceTypeId_STI_Chat ServiceTypeId = 102 // 聊天服
ServiceTypeId_STI_DB ServiceTypeId = 103 // db服
) )
// Enum value maps for ServiceTypeId. // Enum value maps for ServiceTypeId.
@ -37,12 +38,14 @@ var (
100: "STI_Gate", 100: "STI_Gate",
101: "STI_Login", 101: "STI_Login",
102: "STI_Chat", 102: "STI_Chat",
103: "STI_DB",
} }
ServiceTypeId_value = map[string]int32{ ServiceTypeId_value = map[string]int32{
"STI_Unknown": 0, "STI_Unknown": 0,
"STI_Gate": 100, "STI_Gate": 100,
"STI_Login": 101, "STI_Login": 101,
"STI_Chat": 102, "STI_Chat": 102,
"STI_DB": 103,
} }
) )
@ -77,12 +80,14 @@ var File_service_proto protoreflect.FileDescriptor
const file_service_proto_rawDesc = "" + const file_service_proto_rawDesc = "" +
"\n" + "\n" +
"\rservice.proto\x12\x02pb*K\n" + "\rservice.proto\x12\x02pb*W\n" +
"\rServiceTypeId\x12\x0f\n" + "\rServiceTypeId\x12\x0f\n" +
"\vSTI_Unknown\x10\x00\x12\f\n" + "\vSTI_Unknown\x10\x00\x12\f\n" +
"\bSTI_Gate\x10d\x12\r\n" + "\bSTI_Gate\x10d\x12\r\n" +
"\tSTI_Login\x10e\x12\f\n" + "\tSTI_Login\x10e\x12\f\n" +
"\bSTI_Chat\x10fB\x11Z\x0fcommon/proto/pbb\x06proto3" "\bSTI_Chat\x10f\x12\n" +
"\n" +
"\x06STI_DB\x10gB\x11Z\x0fcommon/proto/pbb\x06proto3"
var ( var (
file_service_proto_rawDescOnce sync.Once file_service_proto_rawDescOnce sync.Once

View File

@ -0,0 +1,8 @@
package rpcName
const (
GetUserAccount = "get.user.account.rpc"
CreateUserAccount = "create.user.account.rpc"
UpdateUserPassword = "update.user.password.rpc"
LogUserAccountLogin = "user.login.rpc"
)

View File

@ -1,7 +0,0 @@
package rpcName
const (
GetUserAccount = "get.user.account.rpc"
CreateUserAccount = "create.user.account.rpc"
UpdateUserPassword = "update.user.password.rpc"
)

View File

@ -4,4 +4,5 @@ const (
Gate = "gate" Gate = "gate"
Chat = "chat" Chat = "chat"
Login = "login" Login = "login"
Db = "db"
) )

23
common/utils/redis.go Normal file
View File

@ -0,0 +1,23 @@
package utils
import (
"github.com/fox/fox/ksync"
"github.com/go-redis/redis/v8"
"time"
)
// 动态调整redis池
func AutoSetRedisPool(rdb *redis.Client) {
// 连接池自动扩展
ksync.GoSafe(func() {
for {
stats := rdb.PoolStats()
usage := float64(stats.TotalConns-stats.IdleConns) / float64(stats.TotalConns)
if usage > 0.8 { // 使用率超过80%
// 动态调整PoolSize
rdb.Options().PoolSize = int(float64(rdb.Options().PoolSize) * 1.2)
}
time.Sleep(10 * time.Second)
}
}, nil)
}

8
common/utils/util.go Normal file
View File

@ -0,0 +1,8 @@
package utils
func Tie[T any](ret bool, v1, v2 T) T {
if ret {
return v1
}
return v2
}

View File

@ -2,6 +2,7 @@ package operation
import ( import (
"game/common/model/user" "game/common/model/user"
"game/common/utils"
"game/server/db/config" "game/server/db/config"
"github.com/fox/fox/db" "github.com/fox/fox/db"
"github.com/fox/fox/log" "github.com/fox/fox/log"
@ -25,11 +26,13 @@ func InitRedis() {
log.Fatal(err.Error()) log.Fatal(err.Error())
return return
} }
utils.AutoSetRedisPool(UserRedis)
AccountRedis, err = db.InitRedis(cfg.Password, cfg.Host, cfg.Port, 1) AccountRedis, err = db.InitRedis(cfg.Password, cfg.Host, cfg.Port, 1)
if err != nil { if err != nil {
log.Fatal(err.Error()) log.Fatal(err.Error())
return return
} }
utils.AutoSetRedisPool(AccountRedis)
} }
func InitDb() { func InitDb() {

View File

@ -7,6 +7,7 @@ import (
"game/common/model" "game/common/model"
"game/common/model/user" "game/common/model/user"
"game/common/proto/pb" "game/common/proto/pb"
"game/common/serialization"
"game/common/utils" "game/common/utils"
"github.com/fox/fox/log" "github.com/fox/fox/log"
"github.com/go-redis/redis/v8" "github.com/go-redis/redis/v8"
@ -15,6 +16,7 @@ import (
) )
type UserAccountOp struct { type UserAccountOp struct {
logDb *gorm.DB
db *gorm.DB db *gorm.DB
accountRedis *redis.Client accountRedis *redis.Client
accountOp *model.TableOp[user.UserAccount] accountOp *model.TableOp[user.UserAccount]
@ -22,6 +24,7 @@ type UserAccountOp struct {
func NewUserAccountOp() *UserAccountOp { func NewUserAccountOp() *UserAccountOp {
return &UserAccountOp{ return &UserAccountOp{
logDb: LogDB,
db: UserDB, db: UserDB,
accountRedis: AccountRedis, accountRedis: AccountRedis,
accountOp: model.NewTableOp[user.UserAccount](UserDB, AccountRedis), accountOp: model.NewTableOp[user.UserAccount](UserDB, AccountRedis),
@ -36,12 +39,18 @@ func (s *UserAccountOp) GetUserAccount(username string) (*user.UserAccount, pb.E
sUid, err := s.accountRedis.Get(context.Background(), s.redisKey(username)).Result() sUid, err := s.accountRedis.Get(context.Background(), s.redisKey(username)).Result()
if err != nil { if err != nil {
if errors.Is(err, redis.Nil) { if errors.Is(err, redis.Nil) {
var us user.UserAccount us := &user.UserAccount{}
err = s.db.Where("username = ?", username).First(&us).Error err = s.db.Where("username = ?", username).First(us).Error
if err != nil { if err != nil {
log.ErrorF("find user:%v err:%v", username, err) log.ErrorF("find user:%v err:%v", username, err)
return nil, pb.ErrCode_SystemErr return nil, pb.ErrCode_SystemErr
} }
// 从db中查到后写入redis并建立索引
if us.Username != "" && us.ID > 0 {
_, _ = s.accountOp.Update(us.ID, serialization.StructToMap(us))
_ = s.accountRedis.Set(context.Background(), s.redisKey(username), us.ID, model.TableExpire).Err()
}
return us, pb.ErrCode_OK
} else { } else {
log.ErrorF("find user:%v err:%v", username, err) log.ErrorF("find user:%v err:%v", username, err)
return nil, pb.ErrCode_SystemErr return nil, pb.ErrCode_SystemErr
@ -69,7 +78,8 @@ func (s *UserAccountOp) CreateUserAccount(us *user.UserAccount) (*user.UserAccou
if code != pb.ErrCode_OK { if code != pb.ErrCode_OK {
return nil, code return nil, code
} }
s.accountRedis.Set(context.Background(), s.redisKey(us.Username), us.ID, model.TableExpire) // 建立索引
_ = s.accountRedis.Set(context.Background(), s.redisKey(us.Username), us.ID, model.TableExpire).Err()
return us, pb.ErrCode_OK return us, pb.ErrCode_OK
} }
@ -84,25 +94,18 @@ func (s *UserAccountOp) UpdateUserPassword(us *user.UserAccount) (*user.UserAcco
var code pb.ErrCode var code pb.ErrCode
us, code = s.accountOp.Update(us.ID, map[string]any{"password": hashedPassword}) us, code = s.accountOp.Update(us.ID, map[string]any{"password": hashedPassword})
if code != pb.ErrCode_OK { if code != pb.ErrCode_OK {
s.accountRedis.Expire(context.Background(), s.redisKey(us.Username), model.TableExpire) _ = s.accountRedis.Expire(context.Background(), s.redisKey(us.Username), model.TableExpire).Err()
} }
return us, code return us, code
} }
//// 记录登录日志 // 记录登录日志
//func (s *UserAccountOp) recordLoginLog(userID uint, ip, deviceID string, success bool, failReason string) { func (s *UserAccountOp) RecordLoginLog(logEntry *user.UserLoginLog) {
// logEntry := user.UserLoginLog{ if err := s.logDb.Create(&logEntry).Error; err != nil {
// PlayerID: userID, log.ErrorF("记录登录日志失败: %v", err)
// LoginIP: ip, }
// DeviceInfo: deviceID, }
// LoginResult: success,
// FailReason: failReason,
// }
//
// if err := s.logDb.Create(&logEntry).Error; err != nil {
// log.ErrorF("记录登录日志失败: %v", err)
// }
//}
// //
//// 生成JWT令牌(简化版) //// 生成JWT令牌(简化版)
//func generateToken(userID uint, username string) (string, error) { //func generateToken(userID uint, username string) (string, error) {

View File

@ -0,0 +1,83 @@
package server
import (
"encoding/json"
"game/common/model/user"
"game/common/proto/pb"
"game/server/db/operation"
"github.com/fox/fox/ipb"
"github.com/fox/fox/log"
)
// 获取帐号
func (s *DbService) onGetUserAccount(iMsg *ipb.InternalMsg) *ipb.InternalMsg {
operationDb[user.UserAccount](iMsg, func(us *user.UserAccount) (*user.UserAccount, pb.ErrCode) {
return operation.NewUserAccountOp().GetUserAccount(us.Username)
})
return iMsg
}
// 创建帐号
func (s *DbService) onCreateUserAccount(iMsg *ipb.InternalMsg) *ipb.InternalMsg {
operationDb[user.UserAccount](iMsg, operation.NewUserAccountOp().CreateUserAccount)
return iMsg
}
type result[T any] struct {
ret T
}
// 装饰器
func operationDb[T any](iMsg *ipb.InternalMsg, operation func(table *T) (*T, pb.ErrCode)) {
t := result[T]{}
err := json.Unmarshal(iMsg.Msg, &t.ret)
if err != nil {
log.ErrorF("error unmarshalling user account %v", err)
return
}
table, code := operation(&t.ret)
if code != pb.ErrCode_OK {
return
}
iMsg.Msg, err = json.Marshal(table)
if err != nil {
log.ErrorF("error marshalling user account %v", err)
return
}
return
}
// 修改密码
func (s *DbService) onUpdateUserAccount(iMsg *ipb.InternalMsg) *ipb.InternalMsg {
operationDb[user.UserAccount](iMsg, operation.NewUserAccountOp().UpdateUserPassword)
return iMsg
}
// 日志操作无需返回值
func (s *DbService) onLogUserAccountLogin(iMsg *ipb.InternalMsg) *ipb.InternalMsg {
operationDb[user.UserLoginLog](iMsg, func(us *user.UserLoginLog) (*user.UserLoginLog, pb.ErrCode) {
operation.NewUserAccountOp().RecordLoginLog(us)
return nil, pb.ErrCode_OK
})
return iMsg
}
//func (s *DbService) operation(iMsg *ipb.InternalMsg, operation func(us *user.UserAccount) (*user.UserAccount, pb.ErrCode)) {
// us := &user.UserAccount{}
// err := json.Unmarshal(iMsg.Msg, us)
// if err != nil {
// log.ErrorF("error unmarshalling user account %v", err)
// return
// }
// var code pb.ErrCode
// us, code = operation(us)
// if code != pb.ErrCode_OK {
// return
// }
// iMsg.Msg, err = json.Marshal(us)
// if err != nil {
// log.ErrorF("error marshalling user account %v", err)
// return
// }
// return
//}

View File

@ -10,5 +10,6 @@ func (s *DbService) initRpcProcessor() {
rpcName.CreateUserAccount: s.onCreateUserAccount, rpcName.CreateUserAccount: s.onCreateUserAccount,
rpcName.GetUserAccount: s.onGetUserAccount, rpcName.GetUserAccount: s.onGetUserAccount,
rpcName.UpdateUserPassword: s.onUpdateUserAccount, rpcName.UpdateUserPassword: s.onUpdateUserAccount,
rpcName.LogUserAccountLogin: s.onLogUserAccountLogin,
}) })
} }

View File

@ -19,7 +19,7 @@ func Init() {
log.DebugF("init service begin id:%v, num:%v", config.Command.ServiceId, config.Command.ServiceNum) log.DebugF("init service begin id:%v, num:%v", config.Command.ServiceId, config.Command.ServiceNum)
for i := 0; i < config.Command.ServiceNum; i++ { for i := 0; i < config.Command.ServiceNum; i++ {
sid := config.Command.ServiceId + i sid := config.Command.ServiceId + i
if srv := newLoginService(sid); srv != nil { if srv := newDbService(sid); srv != nil {
DbSrv = append(DbSrv, srv) DbSrv = append(DbSrv, srv)
} }
} }
@ -35,20 +35,20 @@ func Stop() {
} }
} }
func newLoginService(serviceId int) *DbService { func newDbService(serviceId int) *DbService {
var err error var err error
s := new(DbService) s := new(DbService)
sName := fmt.Sprintf("%v-%d", serviceName.Login, serviceId) sName := fmt.Sprintf("%v-%d", serviceName.Db, serviceId)
if s.NatsService, err = service.NewNatsService(&service.InitNatsServiceParams{ if s.NatsService, err = service.NewNatsService(&service.InitNatsServiceParams{
EtcdAddress: config.Cfg.Etcd.Address, EtcdAddress: config.Cfg.Etcd.Address,
EtcdUsername: "", EtcdUsername: "",
EtcdPassword: "", EtcdPassword: "",
NatsAddress: config.Cfg.Nats.Address, NatsAddress: config.Cfg.Nats.Address,
ServiceType: serviceName.Login, ServiceType: serviceName.Db,
ServiceName: sName, ServiceName: sName,
OnFunc: s, OnFunc: s,
TypeId: int(pb.ServiceTypeId_STI_Login), TypeId: int(pb.ServiceTypeId_STI_DB),
Version: config.Cfg.BuildDate, Version: config.Cfg.BuildDate,
}); err != nil { }); err != nil {
log.Fatal(err.Error()) log.Fatal(err.Error())
@ -80,18 +80,5 @@ func (s *DbService) OnStop() {
// 处理其它服发送过来的消息 // 处理其它服发送过来的消息
func (s *DbService) OnMessage(data []byte) error { func (s *DbService) OnMessage(data []byte) error {
_ = data _ = data
//log.Debug(s.Log("received message:%v", iMsg.MsgId))
return nil return nil
} }
//// 向内部服务发送消息
//func (s *DbService) SendServiceData(topic string, connId uint32, userId int64, msgId int32, data []byte) {
// iMsg := ipb.MakeMsg(s.Name(), connId, userId, msgId, data)
// _ = s.Send(topic, iMsg)
//}
//
//// 向内部服务发送消息
//func (s *DbService) SendServiceMsg(topic string, connId uint32, userId int64, msgId int32, msg proto.Message) {
// data, _ := proto.Marshal(msg)
// s.SendServiceData(topic, connId, userId, msgId, data)
//}

View File

@ -1,51 +0,0 @@
package server
import (
"encoding/json"
"game/common/model/user"
"game/common/proto/pb"
"game/server/db/operation"
"github.com/fox/fox/ipb"
"github.com/fox/fox/log"
)
// 获取帐号
func (s *DbService) onGetUserAccount(iMsg *ipb.InternalMsg) *ipb.InternalMsg {
s.operation(iMsg, func(us *user.UserAccount) (*user.UserAccount, pb.ErrCode) {
return operation.NewUserAccountOp().GetUserAccount(us.Username)
})
return iMsg
}
// 创建帐号
func (s *DbService) onCreateUserAccount(iMsg *ipb.InternalMsg) *ipb.InternalMsg {
s.operation(iMsg, operation.NewUserAccountOp().CreateUserAccount)
return iMsg
}
// 修改密码
func (s *DbService) operation(iMsg *ipb.InternalMsg, operation func(us *user.UserAccount) (*user.UserAccount, pb.ErrCode)) {
us := &user.UserAccount{}
err := json.Unmarshal(iMsg.Msg, us)
if err != nil {
log.ErrorF("error unmarshalling user account %v", err)
return
}
var code pb.ErrCode
us, code = operation(us)
if code != pb.ErrCode_OK {
return
}
iMsg.Msg, err = json.Marshal(us)
if err != nil {
log.ErrorF("error marshalling user account %v", err)
return
}
return
}
// 修改密码
func (s *DbService) onUpdateUserAccount(iMsg *ipb.InternalMsg) *ipb.InternalMsg {
s.operation(iMsg, operation.NewUserAccountOp().UpdateUserPassword)
return iMsg
}

View File

@ -1,6 +1,7 @@
package model package model
import ( import (
"game/common/utils"
"game/server/gate/config" "game/server/gate/config"
"github.com/fox/fox/db" "github.com/fox/fox/db"
"github.com/fox/fox/log" "github.com/fox/fox/log"
@ -16,4 +17,5 @@ func InitRedis() {
log.Fatal(err.Error()) log.Fatal(err.Error())
return return
} }
utils.AutoSetRedisPool(UserRedis)
} }

View File

@ -192,7 +192,7 @@ func (s *GateService) WsOnMessage(conn ws.IConn, data []byte) {
} else { } else {
log.Error(s.Log("topic:%v not exist.user:%v", topic, conn.UserId())) log.Error(s.Log("topic:%v not exist.user:%v", topic, conn.UserId()))
} }
log.Debug(s.Log("received client:%d user:%v message:%v", conn.Id(), conn.UserId(), msg.MsgId)) log.Debug(s.Log("received conn:%d user:%v message:%v", conn.Id(), conn.UserId(), msg.MsgId))
} }
// 向内部服务发送消息 // 向内部服务发送消息

View File

@ -3,7 +3,6 @@ package cmd
import ( import (
"fmt" "fmt"
"game/server/login/config" "game/server/login/config"
"game/server/login/model"
"game/server/login/server" "game/server/login/server"
"github.com/fox/fox/log" "github.com/fox/fox/log"
"os" "os"
@ -12,8 +11,8 @@ import (
) )
func initRepo() { func initRepo() {
model.InitRedis() //model.InitRedis()
model.InitDb() //model.InitDb()
} }
func Run(GitCommit, GitBranch, BuildDate string) { func Run(GitCommit, GitBranch, BuildDate string) {

View File

@ -1,59 +1,59 @@
package model package model
import ( //import (
"game/server/login/config" // "game/server/login/config"
"github.com/fox/fox/db" // "github.com/fox/fox/db"
"github.com/fox/fox/log" // "github.com/fox/fox/log"
"github.com/go-redis/redis/v8" // "github.com/go-redis/redis/v8"
"gorm.io/gorm" // "gorm.io/gorm"
) //)
//
var ( //var (
UserRedis *redis.Client // UserRedis *redis.Client
UserDB *gorm.DB // UserDB *gorm.DB
LogDB *gorm.DB // LogDB *gorm.DB
) //)
//
func InitRedis() { //func InitRedis() {
log.Debug("init redis") // log.Debug("init redis")
var err error // var err error
cfg := &config.Cfg.Redis // cfg := &config.Cfg.Redis
UserRedis, err = db.InitRedis(cfg.Password, cfg.Host, cfg.Port, 0) // UserRedis, err = db.InitRedis(cfg.Password, cfg.Host, cfg.Port, 0)
if err != nil { // if err != nil {
log.Fatal(err.Error()) // log.Fatal(err.Error())
return // return
} // }
} //}
//
func InitDb() { //func InitDb() {
log.Debug("init db") // log.Debug("init db")
var err error // var err error
cfg := &config.Cfg.Mysql // cfg := &config.Cfg.Mysql
UserDB, err = db.InitMysql(cfg.Username, cfg.Password, cfg.Host, cfg.Port, cfg.DbName) // UserDB, err = db.InitMysql(cfg.Username, cfg.Password, cfg.Host, cfg.Port, cfg.DbName)
if err != nil { // if err != nil {
log.Fatal(err.Error()) // log.Fatal(err.Error())
return // return
} // }
cfg = &config.Cfg.MysqlLog // cfg = &config.Cfg.MysqlLog
LogDB, err = db.InitMysql(cfg.Username, cfg.Password, cfg.Host, cfg.Port, cfg.DbName) // LogDB, err = db.InitMysql(cfg.Username, cfg.Password, cfg.Host, cfg.Port, cfg.DbName)
if err != nil { // if err != nil {
log.Fatal(err.Error()) // log.Fatal(err.Error())
return // return
} // }
// 自动迁移game库表结构 // // 自动迁移game库表结构
err = UserDB.AutoMigrate( // err = UserDB.AutoMigrate(
&UserAccount{}, // &UserAccount{},
) // )
if err != nil { // if err != nil {
log.Fatal(err.Error()) // log.Fatal(err.Error())
return // return
} // }
// 自动迁移game_log库表结构 // // 自动迁移game_log库表结构
err = LogDB.AutoMigrate( // err = LogDB.AutoMigrate(
&UserLoginLog{}, // &UserLoginLog{},
) // )
if err != nil { // if err != nil {
log.Fatal(err.Error()) // log.Fatal(err.Error())
return // return
} // }
} //}

View File

@ -1,152 +1,152 @@
package model package model
import ( //import (
"errors" // "errors"
"github.com/fox/fox/log" // "github.com/fox/fox/log"
"golang.org/x/crypto/bcrypt" // "golang.org/x/crypto/bcrypt"
"gorm.io/gorm" // "gorm.io/gorm"
"time" // "time"
) //)
//
const ( //const (
AccountNormal = 1 // 正常 // AccountNormal = 1 // 正常
AccountFrozen = 2 // 冻结 // AccountFrozen = 2 // 冻结
AccountBanned = 3 // 封禁 // AccountBanned = 3 // 封禁
) //)
//
// 玩家账户表 //// 玩家账户表
type UserAccount struct { //type UserAccount struct {
gorm.Model // gorm.Model
Username string `gorm:"type:varchar(32);uniqueIndex;not null"` // 用户名 // Username string `gorm:"type:varchar(32);uniqueIndex;not null"` // 用户名
Password string `gorm:"type:varchar(255);not null"` // 密码哈希 // Password string `gorm:"type:varchar(255);not null"` // 密码哈希
Email string `gorm:"type:varchar(100)"` // 邮箱(可选) // Email string `gorm:"type:varchar(100)"` // 邮箱(可选)
Phone string `gorm:"type:varchar(20)"` // 手机号(可选) // Phone string `gorm:"type:varchar(20)"` // 手机号(可选)
DeviceID string `gorm:"type:varchar(64);index"` // 设备ID // DeviceID string `gorm:"type:varchar(64);index"` // 设备ID
LastLoginIP string `gorm:"type:varchar(45)"` // 最后登录IP(支持IPv6) // LastLoginIP string `gorm:"type:varchar(45)"` // 最后登录IP(支持IPv6)
LastLoginTime time.Time // 最后登录时间 // LastLoginTime time.Time // 最后登录时间
Status int `gorm:"type:tinyint;default:1"` // 账号状态 1-正常 2-冻结 3-封禁 // Status int `gorm:"type:tinyint;default:1"` // 账号状态 1-正常 2-冻结 3-封禁
RegisterIP string `gorm:"type:varchar(45)"` // 注册IP // RegisterIP string `gorm:"type:varchar(45)"` // 注册IP
RegisterTime time.Time `gorm:"type:TIMESTAMP;default:CURRENT_TIMESTAMP"` // 注册时间 // RegisterTime time.Time `gorm:"type:TIMESTAMP;default:CURRENT_TIMESTAMP"` // 注册时间
} //}
//
// 玩家登录记录表 //// 玩家登录记录表
type UserLoginLog struct { //type UserLoginLog struct {
gorm.Model // gorm.Model
PlayerID uint `gorm:"index"` // 关联玩家ID // PlayerID uint `gorm:"index"` // 关联玩家ID
LoginIP string `gorm:"type:varchar(45);not null"` // 登录IP // LoginIP string `gorm:"type:varchar(45);not null"` // 登录IP
LoginTime time.Time `gorm:"type:TIMESTAMP;default:CURRENT_TIMESTAMP"` // 登录时间 // LoginTime time.Time `gorm:"type:TIMESTAMP;default:CURRENT_TIMESTAMP"` // 登录时间
DeviceInfo string `gorm:"type:varchar(255)"` // 设备信息(JSON格式) // DeviceInfo string `gorm:"type:varchar(255)"` // 设备信息(JSON格式)
LoginResult bool // 登录结果 true-成功 false-失败 // LoginResult bool // 登录结果 true-成功 false-失败
FailReason string `gorm:"type:varchar(100)"` // 失败原因 // FailReason string `gorm:"type:varchar(100)"` // 失败原因
} //}
//
type UserLoginOp struct { //type UserLoginOp struct {
db *gorm.DB // db *gorm.DB
logDb *gorm.DB // logDb *gorm.DB
} //}
//
func NewUserLoginOp() *UserLoginOp { //func NewUserLoginOp() *UserLoginOp {
return &UserLoginOp{db: UserDB, logDb: LogDB} // return &UserLoginOp{db: UserDB, logDb: LogDB}
} //}
//
var ( //var (
ErrUserOrPassword = errors.New("user or password was error") // ErrUserOrPassword = errors.New("user or password was error")
ErrAccountFrozen = errors.New("account frozen") // ErrAccountFrozen = errors.New("account frozen")
ErrAccountBanned = errors.New("account banned") // ErrAccountBanned = errors.New("account banned")
) //)
//
func (s *UserLoginOp) Login(username, password, ip, deviceID string) (*UserAccount, error) { //func (s *UserLoginOp) Login(username, password, ip, deviceID string) (*UserAccount, error) {
var user UserAccount // var user UserAccount
err := s.db.Where("username = ?", username).First(&user).Error // err := s.db.Where("username = ?", username).First(&user).Error
if err != nil { // if err != nil {
return nil, err // return nil, err
} // }
// 验证密码 // // 验证密码
if err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil { // if err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
s.recordLoginLog(user.ID, ip, deviceID, false, ErrUserOrPassword.Error()) // s.recordLoginLog(user.ID, ip, deviceID, false, ErrUserOrPassword.Error())
return nil, ErrUserOrPassword // return nil, ErrUserOrPassword
} // }
// 检查账号状态 // // 检查账号状态
switch user.Status { // switch user.Status {
case AccountNormal: // case AccountNormal:
//
case AccountFrozen: // case AccountFrozen:
s.recordLoginLog(user.ID, ip, deviceID, false, ErrAccountFrozen.Error()) // s.recordLoginLog(user.ID, ip, deviceID, false, ErrAccountFrozen.Error())
return nil, ErrAccountFrozen // return nil, ErrAccountFrozen
case AccountBanned: // case AccountBanned:
s.recordLoginLog(user.ID, ip, deviceID, false, ErrAccountBanned.Error()) // s.recordLoginLog(user.ID, ip, deviceID, false, ErrAccountBanned.Error())
return nil, ErrAccountBanned // return nil, ErrAccountBanned
} // }
// 更新最后登录信息 // // 更新最后登录信息
user.LastLoginIP = ip // user.LastLoginIP = ip
user.LastLoginTime = time.Now() // user.LastLoginTime = time.Now()
_ = s.db.Save(&user).Error // _ = s.db.Save(&user).Error
//
// 记录成功登录日志 // // 记录成功登录日志
s.recordLoginLog(user.ID, ip, deviceID, true, "") // s.recordLoginLog(user.ID, ip, deviceID, true, "")
//
// 6. 生成访问令牌 // // 6. 生成访问令牌
token, err := generateToken(user.ID, user.Username) // token, err := generateToken(user.ID, user.Username)
if err != nil { // if err != nil {
return nil, err // return nil, err
} // }
user.Password = token // user.Password = token
return &user, err // return &user, err
} //}
//
// 注册新用户 //// 注册新用户
func (s *UserLoginOp) RegisterNewUser(username, password, ip, deviceID string) (*UserAccount, error) { //func (s *UserLoginOp) RegisterNewUser(username, password, ip, deviceID string) (*UserAccount, error) {
// 密码加密 // // 密码加密
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) // hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil { // if err != nil {
return nil, err // return nil, err
} // }
user := UserAccount{ // user := UserAccount{
Username: username, // Username: username,
Password: string(hashedPassword), // Password: string(hashedPassword),
DeviceID: deviceID, // DeviceID: deviceID,
RegisterIP: ip, // RegisterIP: ip,
Status: 1, // Status: 1,
LastLoginIP: ip, // LastLoginIP: ip,
LastLoginTime: time.Now(), // LastLoginTime: time.Now(),
} // }
//
if err := s.db.Create(&user).Error; err != nil { // if err := s.db.Create(&user).Error; err != nil {
return nil, err // return nil, err
} // }
//
s.recordLoginLog(user.ID, ip, deviceID, true, "") // s.recordLoginLog(user.ID, ip, deviceID, true, "")
//
// 生成访问令牌 // // 生成访问令牌
token, err := generateToken(user.ID, user.Username) // token, err := generateToken(user.ID, user.Username)
if err != nil { // if err != nil {
return nil, err // return nil, err
} // }
user.Password = token // user.Password = token
//
return &user, nil // return &user, nil
} //}
//
// 记录登录日志 //// 记录登录日志
func (s *UserLoginOp) recordLoginLog(userID uint, ip, deviceID string, success bool, failReason string) { //func (s *UserLoginOp) recordLoginLog(userID uint, ip, deviceID string, success bool, failReason string) {
logEntry := UserLoginLog{ // logEntry := UserLoginLog{
PlayerID: userID, // PlayerID: userID,
LoginIP: ip, // LoginIP: ip,
DeviceInfo: deviceID, // DeviceInfo: deviceID,
LoginResult: success, // LoginResult: success,
FailReason: failReason, // FailReason: failReason,
} // }
//
if err := s.logDb.Create(&logEntry).Error; err != nil { // if err := s.logDb.Create(&logEntry).Error; err != nil {
log.ErrorF("记录登录日志失败: %v", err) // log.ErrorF("记录登录日志失败: %v", err)
} // }
} //}
//
// 生成JWT令牌(简化版) //// 生成JWT令牌(简化版)
func generateToken(userID uint, username string) (string, error) { //func generateToken(userID uint, username string) (string, error) {
_ = userID // _ = userID
_ = username // _ = username
// 这里应该使用JWT库生成实际令牌 // // 这里应该使用JWT库生成实际令牌
// 简化实现实际项目中请使用安全的JWT实现 // // 简化实现实际项目中请使用安全的JWT实现
return "generated-token-placeholder", nil // return "generated-token-placeholder", nil
} //}

View File

@ -1,13 +1,22 @@
package server package server
import ( import (
"errors" "encoding/json"
"game/common/model/user"
"game/common/proto/pb" "game/common/proto/pb"
"game/server/login/model" "game/common/rpcName"
"game/common/utils"
"github.com/fox/fox/etcd"
"github.com/fox/fox/ipb" "github.com/fox/fox/ipb"
"github.com/fox/fox/ksync"
"github.com/fox/fox/log"
"github.com/fox/fox/processor" "github.com/fox/fox/processor"
"github.com/fox/fox/service" "github.com/fox/fox/service"
"gorm.io/gorm" "time"
)
const (
timeout = time.Second * 30
) )
func (s *LoginService) initProcessor() { func (s *LoginService) initProcessor() {
@ -16,42 +25,92 @@ func (s *LoginService) initProcessor() {
}) })
} }
func (s *LoginService) checkLoginOrRegister(req *pb.C2SUserLogin) (user *model.UserAccount, code pb.ErrCode) { func (s *LoginService) checkLoginOrRegister(req *pb.C2SUserLogin) (us *user.UserAccount, code pb.ErrCode, node *etcd.ServiceNode) {
op := model.NewUserLoginOp()
var err error var err error
user, err = op.Login(req.Username, req.Password, req.Ip, req.DeviceId) node, err = s.bindService.RandServiceNode(pb.ServiceTypeId_STI_DB)
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { log.ErrorF(s.Log("not find db service.err:%s ", err.Error()))
user, err = op.RegisterNewUser(req.Username, req.Password, req.Ip, req.DeviceId) return nil, pb.ErrCode_SystemErr, node
}
if req.Version < "20250601123030" {
return nil, pb.ErrCode_VersionTooLow, node
}
us = &user.UserAccount{
Username: req.Username,
Password: req.Password,
DeviceID: req.DeviceId,
LastLoginIP: req.Ip,
}
rpcMsg := ipb.MakeRpcMsg[user.UserAccount](rpcName.GetUserAccount, 0, us)
rspMsg, err := s.Call(service.RpcTopicEx(node.Name), timeout, rpcMsg)
if err != nil { if err != nil {
code = pb.ErrCode_RegisterUserExist log.ErrorF(s.Log("call rpc:%v err:%s ", rpcMsg.RpcMsgId, err.Error()))
return return nil, pb.ErrCode_SystemErr, node
} }
} else if errors.Is(err, model.ErrUserOrPassword) { _ = json.Unmarshal(rspMsg.Msg, us)
code = pb.ErrCode_LoginUserOrPwdErr if us.ID == 0 {
return // 没有帐号,创建帐号
} else if errors.Is(err, model.ErrAccountFrozen) { rpcMsg.RpcMsgId = rpcName.CreateUserAccount
code = pb.ErrCode_AccountFrozen rspMsg, err = s.Call(service.RpcTopicEx(node.Name), timeout, rpcMsg)
return if err != nil {
} else if errors.Is(err, model.ErrAccountBanned) { log.ErrorF(s.Log("call rpc:%v err:%s ", rpcMsg.RpcMsgId, err.Error()))
code = pb.ErrCode_AccountBanned return nil, pb.ErrCode_SystemErr, node
return }
} else { _ = json.Unmarshal(rspMsg.Msg, us)
code = pb.ErrCode_SystemErr if us.ID == 0 {
log.ErrorF(s.Log("call rpc:%v err", rpcMsg.RpcMsgId))
return nil, pb.ErrCode_SystemErr, node
} }
} }
return user, code if pwd, _ := utils.Password(req.Password); pwd != us.Password {
return nil, pb.ErrCode_LoginUserOrPwdErr, node
}
switch us.Status {
case user.AccountFrozen:
return nil, pb.ErrCode_AccountFrozen, node
case user.AccountBanned:
return nil, pb.ErrCode_AccountBanned, node
default:
return us, pb.ErrCode_OK, node
}
}
// 生成JWT令牌(简化版)
func generateToken(userID uint, username string) (string, error) {
_ = userID
_ = username
// 这里应该使用JWT库生成实际令牌
// 简化实现实际项目中请使用安全的JWT实现
return "generated-token-placeholder", nil
} }
// 登录或注册 // 登录或注册
func (s *LoginService) onLoginOrRegister(iMsg *ipb.InternalMsg, req *pb.C2SUserLogin) { func (s *LoginService) onLoginOrRegister(iMsg *ipb.InternalMsg, req *pb.C2SUserLogin) {
user, code := s.checkLoginOrRegister(req) ksync.GoSafe(func() {
us, code, node := s.checkLoginOrRegister(req)
userId := int64(0) userId := int64(0)
rsp := &pb.S2CUserLogin{Code: code} rsp := &pb.S2CUserLogin{Code: code}
if user != nil && code == pb.ErrCode_OK { if us != nil && code == pb.ErrCode_OK {
rsp.UserId = int64(user.ID) rsp.UserId = int64(us.ID)
rsp.Token = user.Password rsp.Token, _ = generateToken(us.ID, us.Username)
userId = rsp.UserId userId = rsp.UserId
} }
s.SendServiceMsg(service.TopicEx(iMsg.ServiceName), iMsg.ConnId, userId, int32(pb.MsgId_S2CUserLoginId), rsp) s.SendServiceMsg(service.TopicEx(iMsg.ServiceName), iMsg.ConnId, userId, int32(pb.MsgId_S2CUserLoginId), rsp)
if us != nil && us.ID > 0 {
switch code {
case pb.ErrCode_LoginUserOrPwdErr:
rpcMsg := ipb.MakeRpcMsg[user.UserLoginLog](rpcName.GetUserAccount, 0, &user.UserLoginLog{
UID: us.ID,
LoginIP: us.LastLoginIP,
LoginTime: time.Now(),
DeviceInfo: us.DeviceID,
LoginResult: code == pb.ErrCode_OK,
FailReason: code.String(),
})
_, _ = s.Call(service.RpcTopicEx(node.Name), timeout, rpcMsg)
}
}
}, nil)
} }

View File

@ -1,6 +1,8 @@
1.测试gate 1.测试gate
1.1 每10分钟新起1000个连接发送登陆然后关闭。检查是否有内存及协程泄漏。(有泄漏) 1.1 每10分钟新起1000个连接发送登陆然后关闭。检查是否有内存及协程泄漏。(已修复)
1.2 启动1000个链接每小时固定发送登陆消息一天后查看连接是否还在。检查心跳机制。(已修复) 1.2 启动1000个链接每小时固定发送登陆消息一天后查看连接是否还在。检查心跳机制。(已修复)
1.3 修改完后清除调试日志。(已完成)
1.4 客户端stop关闭连接触发服务端连接崩溃。(已修复)
2.编写db服 2.编写db服
2.1 login服向db服请求数据及向log db服写入日志。测试rpc机制。 2.1 login服向db服请求数据及向log db服写入日志。测试rpc机制。