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

View File

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

View File

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

View File

@ -30,7 +30,8 @@ const (
ErrCode_LoginUserOrPwdErr ErrCode = 102 // 帐号或密码错误
ErrCode_AccountFrozen ErrCode = 103 // 帐号已冻结
ErrCode_AccountBanned ErrCode = 104 // 帐号已封禁
ErrCode_RegisterUserExist ErrCode = 120 // 已有该帐号,无法注册
ErrCode_RegisterUserExist ErrCode = 110 // 已有该帐号,无法注册
ErrCode_VersionTooLow ErrCode = 115 // 版本太低,无法登陆
)
// Enum value maps for ErrCode.
@ -42,7 +43,8 @@ var (
102: "LoginUserOrPwdErr",
103: "AccountFrozen",
104: "AccountBanned",
120: "RegisterUserExist",
110: "RegisterUserExist",
115: "VersionTooLow",
}
ErrCode_value = map[string]int32{
"OK": 0,
@ -51,7 +53,8 @@ var (
"LoginUserOrPwdErr": 102,
"AccountFrozen": 103,
"AccountBanned": 104,
"RegisterUserExist": 120,
"RegisterUserExist": 110,
"VersionTooLow": 115,
}
)
@ -87,7 +90,7 @@ var File_code_proto protoreflect.FileDescriptor
const file_code_proto_rawDesc = "" +
"\n" +
"\n" +
"code.proto\x12\x02pb*\x86\x01\n" +
"code.proto\x12\x02pb*\x99\x01\n" +
"\aErrCode\x12\x06\n" +
"\x02OK\x10\x00\x12\r\n" +
"\tSystemErr\x10\x01\x12\x10\n" +
@ -95,7 +98,8 @@ const file_code_proto_rawDesc = "" +
"\x11LoginUserOrPwdErr\x10f\x12\x11\n" +
"\rAccountFrozen\x10g\x12\x11\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 (
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中定义
// 3. 所有消息名为对应消息id去掉后缀Id组成
type MsgId int32

View File

@ -28,6 +28,7 @@ const (
ServiceTypeId_STI_Gate ServiceTypeId = 100 // 网关id
ServiceTypeId_STI_Login ServiceTypeId = 101 // 登陆服
ServiceTypeId_STI_Chat ServiceTypeId = 102 // 聊天服
ServiceTypeId_STI_DB ServiceTypeId = 103 // db服
)
// Enum value maps for ServiceTypeId.
@ -37,12 +38,14 @@ var (
100: "STI_Gate",
101: "STI_Login",
102: "STI_Chat",
103: "STI_DB",
}
ServiceTypeId_value = map[string]int32{
"STI_Unknown": 0,
"STI_Gate": 100,
"STI_Login": 101,
"STI_Chat": 102,
"STI_DB": 103,
}
)
@ -77,12 +80,14 @@ var File_service_proto protoreflect.FileDescriptor
const file_service_proto_rawDesc = "" +
"\n" +
"\rservice.proto\x12\x02pb*K\n" +
"\rservice.proto\x12\x02pb*W\n" +
"\rServiceTypeId\x12\x0f\n" +
"\vSTI_Unknown\x10\x00\x12\f\n" +
"\bSTI_Gate\x10d\x12\r\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 (
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"
Chat = "chat"
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 (
"game/common/model/user"
"game/common/utils"
"game/server/db/config"
"github.com/fox/fox/db"
"github.com/fox/fox/log"
@ -25,11 +26,13 @@ func InitRedis() {
log.Fatal(err.Error())
return
}
utils.AutoSetRedisPool(UserRedis)
AccountRedis, err = db.InitRedis(cfg.Password, cfg.Host, cfg.Port, 1)
if err != nil {
log.Fatal(err.Error())
return
}
utils.AutoSetRedisPool(AccountRedis)
}
func InitDb() {

View File

@ -7,6 +7,7 @@ import (
"game/common/model"
"game/common/model/user"
"game/common/proto/pb"
"game/common/serialization"
"game/common/utils"
"github.com/fox/fox/log"
"github.com/go-redis/redis/v8"
@ -15,6 +16,7 @@ import (
)
type UserAccountOp struct {
logDb *gorm.DB
db *gorm.DB
accountRedis *redis.Client
accountOp *model.TableOp[user.UserAccount]
@ -22,6 +24,7 @@ type UserAccountOp struct {
func NewUserAccountOp() *UserAccountOp {
return &UserAccountOp{
logDb: LogDB,
db: UserDB,
accountRedis: 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()
if err != nil {
if errors.Is(err, redis.Nil) {
var us user.UserAccount
err = s.db.Where("username = ?", username).First(&us).Error
us := &user.UserAccount{}
err = s.db.Where("username = ?", username).First(us).Error
if err != nil {
log.ErrorF("find user:%v err:%v", username, err)
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 {
log.ErrorF("find user:%v err:%v", username, err)
return nil, pb.ErrCode_SystemErr
@ -69,7 +78,8 @@ func (s *UserAccountOp) CreateUserAccount(us *user.UserAccount) (*user.UserAccou
if code != pb.ErrCode_OK {
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
}
@ -84,25 +94,18 @@ func (s *UserAccountOp) UpdateUserPassword(us *user.UserAccount) (*user.UserAcco
var code pb.ErrCode
us, code = s.accountOp.Update(us.ID, map[string]any{"password": hashedPassword})
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
}
//// 记录登录日志
//func (s *UserAccountOp) recordLoginLog(userID uint, ip, deviceID string, success bool, failReason string) {
// logEntry := user.UserLoginLog{
// PlayerID: userID,
// LoginIP: ip,
// DeviceInfo: deviceID,
// LoginResult: success,
// FailReason: failReason,
// }
//
// if err := s.logDb.Create(&logEntry).Error; err != nil {
// log.ErrorF("记录登录日志失败: %v", err)
// }
//}
// 记录登录日志
func (s *UserAccountOp) RecordLoginLog(logEntry *user.UserLoginLog) {
if err := s.logDb.Create(&logEntry).Error; err != nil {
log.ErrorF("记录登录日志失败: %v", err)
}
}
//
//// 生成JWT令牌(简化版)
//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

@ -7,8 +7,9 @@ import (
func (s *DbService) initRpcProcessor() {
s.RpcProcessor.RegisterMessages(map[string]processor.RpcHandler{
rpcName.CreateUserAccount: s.onCreateUserAccount,
rpcName.GetUserAccount: s.onGetUserAccount,
rpcName.UpdateUserPassword: s.onUpdateUserAccount,
rpcName.CreateUserAccount: s.onCreateUserAccount,
rpcName.GetUserAccount: s.onGetUserAccount,
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)
for i := 0; i < config.Command.ServiceNum; i++ {
sid := config.Command.ServiceId + i
if srv := newLoginService(sid); srv != nil {
if srv := newDbService(sid); srv != nil {
DbSrv = append(DbSrv, srv)
}
}
@ -35,20 +35,20 @@ func Stop() {
}
}
func newLoginService(serviceId int) *DbService {
func newDbService(serviceId int) *DbService {
var err error
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{
EtcdAddress: config.Cfg.Etcd.Address,
EtcdUsername: "",
EtcdPassword: "",
NatsAddress: config.Cfg.Nats.Address,
ServiceType: serviceName.Login,
ServiceType: serviceName.Db,
ServiceName: sName,
OnFunc: s,
TypeId: int(pb.ServiceTypeId_STI_Login),
TypeId: int(pb.ServiceTypeId_STI_DB),
Version: config.Cfg.BuildDate,
}); err != nil {
log.Fatal(err.Error())
@ -80,18 +80,5 @@ func (s *DbService) OnStop() {
// 处理其它服发送过来的消息
func (s *DbService) OnMessage(data []byte) error {
_ = data
//log.Debug(s.Log("received message:%v", iMsg.MsgId))
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
import (
"game/common/utils"
"game/server/gate/config"
"github.com/fox/fox/db"
"github.com/fox/fox/log"
@ -16,4 +17,5 @@ func InitRedis() {
log.Fatal(err.Error())
return
}
utils.AutoSetRedisPool(UserRedis)
}

View File

@ -192,7 +192,7 @@ func (s *GateService) WsOnMessage(conn ws.IConn, data []byte) {
} else {
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 (
"fmt"
"game/server/login/config"
"game/server/login/model"
"game/server/login/server"
"github.com/fox/fox/log"
"os"
@ -12,8 +11,8 @@ import (
)
func initRepo() {
model.InitRedis()
model.InitDb()
//model.InitRedis()
//model.InitDb()
}
func Run(GitCommit, GitBranch, BuildDate string) {

View File

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

View File

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

View File

@ -1,13 +1,22 @@
package server
import (
"errors"
"encoding/json"
"game/common/model/user"
"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/ksync"
"github.com/fox/fox/log"
"github.com/fox/fox/processor"
"github.com/fox/fox/service"
"gorm.io/gorm"
"time"
)
const (
timeout = time.Second * 30
)
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) {
op := model.NewUserLoginOp()
func (s *LoginService) checkLoginOrRegister(req *pb.C2SUserLogin) (us *user.UserAccount, code pb.ErrCode, node *etcd.ServiceNode) {
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 errors.Is(err, gorm.ErrRecordNotFound) {
user, err = op.RegisterNewUser(req.Username, req.Password, req.Ip, req.DeviceId)
if err != nil {
code = pb.ErrCode_RegisterUserExist
return
}
} else if errors.Is(err, model.ErrUserOrPassword) {
code = pb.ErrCode_LoginUserOrPwdErr
return
} else if errors.Is(err, model.ErrAccountFrozen) {
code = pb.ErrCode_AccountFrozen
return
} else if errors.Is(err, model.ErrAccountBanned) {
code = pb.ErrCode_AccountBanned
return
} else {
code = pb.ErrCode_SystemErr
log.ErrorF(s.Log("not find db service.err:%s ", err.Error()))
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 {
log.ErrorF(s.Log("call rpc:%v err:%s ", rpcMsg.RpcMsgId, err.Error()))
return nil, pb.ErrCode_SystemErr, node
}
_ = json.Unmarshal(rspMsg.Msg, us)
if us.ID == 0 {
// 没有帐号,创建帐号
rpcMsg.RpcMsgId = rpcName.CreateUserAccount
rspMsg, err = s.Call(service.RpcTopicEx(node.Name), timeout, rpcMsg)
if err != nil {
log.ErrorF(s.Log("call rpc:%v err:%s ", rpcMsg.RpcMsgId, err.Error()))
return nil, pb.ErrCode_SystemErr, node
}
_ = json.Unmarshal(rspMsg.Msg, us)
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) {
user, code := s.checkLoginOrRegister(req)
userId := int64(0)
rsp := &pb.S2CUserLogin{Code: code}
if user != nil && code == pb.ErrCode_OK {
rsp.UserId = int64(user.ID)
rsp.Token = user.Password
userId = rsp.UserId
}
s.SendServiceMsg(service.TopicEx(iMsg.ServiceName), iMsg.ConnId, userId, int32(pb.MsgId_S2CUserLoginId), rsp)
ksync.GoSafe(func() {
us, code, node := s.checkLoginOrRegister(req)
userId := int64(0)
rsp := &pb.S2CUserLogin{Code: code}
if us != nil && code == pb.ErrCode_OK {
rsp.UserId = int64(us.ID)
rsp.Token, _ = generateToken(us.ID, us.Username)
userId = rsp.UserId
}
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.1 每10分钟新起1000个连接发送登陆然后关闭。检查是否有内存及协程泄漏。(有泄漏)
1.1 每10分钟新起1000个连接发送登陆然后关闭。检查是否有内存及协程泄漏。(已修复)
1.2 启动1000个链接每小时固定发送登陆消息一天后查看连接是否还在。检查心跳机制。(已修复)
1.3 修改完后清除调试日志。(已完成)
1.4 客户端stop关闭连接触发服务端连接崩溃。(已修复)
2.编写db服
2.1 login服向db服请求数据及向log db服写入日志。测试rpc机制。