From e2f75e2db7afe2774c363b5b9421922afd6e8c77 Mon Sep 17 00:00:00 2001 From: liuxiaobo <1224730913@qq.com> Date: Thu, 29 May 2025 00:17:18 +0800 Subject: [PATCH] =?UTF-8?q?=E7=99=BB=E9=99=86=E6=9C=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- common/config/config.go | 5 +- common/config/loadConfig.go | 43 +++++++++-- common/serviceName/serviceName.go | 5 +- docker.txt | 36 +++++++++ server/login/cmd/cmd.go | 31 ++++++++ server/login/config/config.go | 35 +++++++++ server/login/main.go | 24 ++++++ server/login/model/db.go | 57 +++++++++++++++ server/login/model/userAccount.go | 117 ++++++++++++++++++++++++++++++ server/login/server/processor.go | 93 ++++++++++++++++++++++++ server/login/server/service.go | 115 +++++++++++++++++++++++++++++ 11 files changed, 550 insertions(+), 11 deletions(-) create mode 100644 docker.txt create mode 100644 server/login/cmd/cmd.go create mode 100644 server/login/config/config.go create mode 100644 server/login/main.go create mode 100644 server/login/model/db.go create mode 100644 server/login/model/userAccount.go create mode 100644 server/login/server/processor.go create mode 100644 server/login/server/service.go diff --git a/common/config/config.go b/common/config/config.go index c34c4c0..dd87234 100644 --- a/common/config/config.go +++ b/common/config/config.go @@ -7,6 +7,8 @@ type Common[T any] struct { Etcd Etcd `json:"etcd"` Redis Redis `json:"redis"` Nats Nats `json:"nats"` + Mysql Mysql `json:"mysql"` + MysqlLog Mysql `json:"mysql_log"` Special *T `json:"special"` GitCommit string `json:"git_commit"` // 服务当前的hash值 GitBranch string `json:"git_branch"` // 服务当前的版本分支 @@ -25,11 +27,12 @@ type Rabbitmq struct { VHost string `json:"v_host"` } -type DB struct { +type Mysql struct { Host string `json:"host"` Port string `json:"port"` Username string `json:"username"` Password string `json:"password"` + DbName string `json:"db_name"` } type Redis struct { diff --git a/common/config/loadConfig.go b/common/config/loadConfig.go index 7f33b8f..2977f62 100644 --- a/common/config/loadConfig.go +++ b/common/config/loadConfig.go @@ -13,14 +13,21 @@ import ( const ( //ModeDev = "dev" // 开发服 - etcdKey = "etcd_config" - etcdAddress = "114.132.124.145:2379" - natsKey = "nats_config" - natsAddress = "nats://114.132.124.145:4222" - redisKey = "redis_config" - redisAddress = testHelper.Host - redisPort = testHelper.RedisPort - redisPassword = testHelper.RedisPassword + etcdKey = "etcd_config" + etcdAddress = "114.132.124.145:2379" + natsKey = "nats_config" + natsAddress = "nats://114.132.124.145:4222" + redisKey = "redis_config" + redisAddress = testHelper.Host + redisPort = testHelper.RedisPort + redisPassword = testHelper.RedisPassword + mysqlKey = "mysql_config" + mysqlAddress = "114.132.124.145" + mysqlPort = "3306" + mysqlUser = "game" + mysqlPasswd = "fox379@@zyxi" + mysqlDBName = "game" + mysqlLogDBName = "game_log" ) func LoadSpecialConfig[T any](rd *redis.Client, specialKey string, comm *Common[T]) error { @@ -102,6 +109,26 @@ func LoadCommonConfig[T any](rd *redis.Client, GitCommit, GitBranch, BuildDate s } else { err = json.Unmarshal([]byte(s), &comm.Redis) } + + // 初始化mysql + s, err = rd.Get(context.Background(), mysqlKey).Result() + if err != nil && !errors.Is(err, redis.Nil) { + log.FatalF("init config:%v", err) + return nil, err + } + if s == "" { + log.DebugF("load config:empty mysql key") + comm.Mysql = Mysql{Host: mysqlAddress, Port: mysqlPort, Password: mysqlPasswd, Username: mysqlUser, DbName: mysqlDBName} + if bs, err := json.Marshal(&comm.Redis); err == nil { + err = rd.Set(context.Background(), mysqlKey, string(bs), 0).Err() + } + } else { + err = json.Unmarshal([]byte(s), &comm.Mysql) + } + + comm.MysqlLog = comm.Mysql + comm.MysqlLog.DbName = mysqlLogDBName + return &comm, nil } diff --git a/common/serviceName/serviceName.go b/common/serviceName/serviceName.go index 72f47cd..a4502d0 100644 --- a/common/serviceName/serviceName.go +++ b/common/serviceName/serviceName.go @@ -1,6 +1,7 @@ package serviceName const ( - Gate = "gate" - Chat = "chat" + Gate = "gate" + Chat = "chat" + Login = "login" ) diff --git a/docker.txt b/docker.txt new file mode 100644 index 0000000..d0abb95 --- /dev/null +++ b/docker.txt @@ -0,0 +1,36 @@ +docker run -d --name my-nats \ + -p 4222:4222 -p 8222:8222 \ + nats + +sudo docker run -d \ + --name my-etcd \ + -p 2379:2379 \ + -p 2380:2380 \ + quay.io/coreos/etcd:v3.6.0 \ + etcd \ + --name etcd-single \ + --data-dir /etcd-data \ + --initial-advertise-peer-urls http://0.0.0.0:2380 \ + --listen-peer-urls http://0.0.0.0:2380 \ + --advertise-client-urls http://0.0.0.0:2379 \ + --listen-client-urls http://0.0.0.0:2379 \ + --initial-cluster etcd-single=http://0.0.0.0:2380 + +docker run -d --name etcdkeeper -p 8080:8080 evildecay/etcdkeeper + +sudo docker run -d \ + --name my-redis \ + -p 6379:6379 \ + -e REDIS_PASSWORD=fox379@@zyxi \ + redis:latest \ + --requirepass fox379@@zyxi + + +sudo docker run -d \ + --name mysql-server \ + -e MYSQL_ROOT_PASSWORD=fox379@@zyxi \ + -e MYSQL_DATABASE=game \ + -e MYSQL_USER=game \ + -e MYSQL_PASSWORD=fox379@@zyxi \ + -p 3306:3306 \ + mysql:8.0 \ No newline at end of file diff --git a/server/login/cmd/cmd.go b/server/login/cmd/cmd.go new file mode 100644 index 0000000..28f5fd7 --- /dev/null +++ b/server/login/cmd/cmd.go @@ -0,0 +1,31 @@ +package cmd + +import ( + "fmt" + "game/server/login/config" + "game/server/login/model" + "game/server/login/server" + "github.com/fox/fox/log" + "os" + "os/signal" + "syscall" +) + +func initRepo() { + model.InitRedis() + model.InitDb() +} + +func Run(GitCommit, GitBranch, BuildDate string) { + config.LoadConfig(GitCommit, GitBranch, BuildDate) + log.Info(fmt.Sprintf("版本分支:%v,hash值:%v,编译时间:%v", GitBranch, GitCommit, BuildDate)) + initRepo() + + server.Init() + // 截获 SIGINT 和 SIGTERM 信号 + c := make(chan os.Signal, 1) + signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) + sig := <-c + server.Stop() + log.Info(fmt.Sprintf("received %s, initiating shutdown...", sig)) +} diff --git a/server/login/config/config.go b/server/login/config/config.go new file mode 100644 index 0000000..6d2ae88 --- /dev/null +++ b/server/login/config/config.go @@ -0,0 +1,35 @@ +package config + +import ( + "fmt" + "game/common/config" + "github.com/fox/fox/db" + "github.com/fox/fox/log" +) + +var Command *config.Command +var Cfg *config.Common[LoginConfig] + +type LoginConfig struct { +} + +func initLog() { + log.Open(fmt.Sprintf("login_%v.log", Command.VMod), log.DebugL) +} + +func LoadConfig(GitCommit, GitBranch, BuildDate string) { + Command = config.ParseCommand() + initLog() + rdb, err := db.InitRedis(Command.RedisPassword, Command.RedisHost, Command.RedisPort, 0) + if err != nil { + log.Error(err.Error()) + return + } + defer func() { _ = rdb.Close() }() + Cfg, err = config.LoadCommonConfig[LoginConfig](rdb, GitCommit, GitBranch, BuildDate) + if err != nil { + log.Error(err.Error()) + return + } + log.DebugF("load common config success") +} diff --git a/server/login/main.go b/server/login/main.go new file mode 100644 index 0000000..c8e35c4 --- /dev/null +++ b/server/login/main.go @@ -0,0 +1,24 @@ +package main + +import ( + "game/server/login/cmd" + "github.com/fox/fox/ksync" + "github.com/fox/fox/log" + "time" +) + +var ( + GitCommit = "unknown" + GitBranch = "unknown" + BuildDate = "unknown" +) + +func main() { + tm, err := time.Parse("20060102150405", BuildDate) + if err == nil { + BuildDate = tm.Format("2006-01-02 15:04:05") + } + ksync.RunSafe(func() { + cmd.Run(GitBranch, GitCommit, BuildDate) + }, func() { log.ErrorF("reset run") }) +} diff --git a/server/login/model/db.go b/server/login/model/db.go new file mode 100644 index 0000000..e8178fc --- /dev/null +++ b/server/login/model/db.go @@ -0,0 +1,57 @@ +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() { + 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() { + 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 + } +} diff --git a/server/login/model/userAccount.go b/server/login/model/userAccount.go new file mode 100644 index 0000000..597b32e --- /dev/null +++ b/server/login/model/userAccount.go @@ -0,0 +1,117 @@ +package model + +import ( + "errors" + "github.com/fox/fox/log" + "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" + "time" +) + +const ( + AccountNormal = 0 // 正常 + AccountFrozen = 1 // 冻结 + AccountBanned = 2 // 封禁 +) + +// 玩家账户表 +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);uniqueIndex"` // 邮箱(可选) + Phone string `gorm:"type:varchar(20);uniqueIndex"` // 手机号(可选) + 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:"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:"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) 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) { + // 这里应该使用JWT库生成实际令牌 + // 简化实现,实际项目中请使用安全的JWT实现 + return "generated-token-placeholder", nil +} diff --git a/server/login/server/processor.go b/server/login/server/processor.go new file mode 100644 index 0000000..832c942 --- /dev/null +++ b/server/login/server/processor.go @@ -0,0 +1,93 @@ +package server + +import ( + "game/common/proto/pb" + "game/common/topicName" + "github.com/fox/fox/log" + "github.com/fox/fox/processor" + "github.com/fox/fox/service" + "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" + "time" +) + +func (s *LoginService) initProcessor() { + s.processor.RegisterMessages(processor.RegisterMetas{ + //pb.MsgId_C2SChatId: {pb.C2SChat{}, s.onChat}, + }) +} + +// 登录或注册 +func (s *LoginService) LoginOrRegister(req *pb.C2SUserLogin) { + // 1. 尝试查找用户 + var user models.UserAccount + err := s.db.Where("username = ?", req.Username).First(&user).Error + + if err != nil { + // 如果是用户不存在错误,则注册新用户 + if errors.Is(err, gorm.ErrRecordNotFound) { + return s.registerNewUser(req) + } + // 其他数据库错误 + return nil, err + } + + // 2. 验证密码 + if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil { + // 记录失败登录日志 + s.recordLoginLog(user.ID, req.IP, req.DeviceID, false, "密码错误") + return nil, errors.New("用户名或密码错误") + } + + // 3. 检查账号状态 + if user.Status != 1 { + var reason string + switch user.Status { + case 2: + reason = "账号已冻结" + case 3: + reason = "账号已封禁" + } + s.recordLoginLog(user.ID, req.IP, req.DeviceID, false, reason) + return nil, errors.New(reason) + } + + // 4. 更新最后登录信息 + user.LastLoginIP = req.IP + user.LastLoginTime = time.Now() + if err := s.db.Save(&user).Error; err != nil { + return nil, err + } + + // 5. 记录成功登录日志 + s.recordLoginLog(user.ID, req.IP, req.DeviceID, true, "") + + // 6. 生成访问令牌 + token, err := generateToken(user.ID, user.Username) + if err != nil { + return nil, err + } + + return &models.LoginResponse{ + UserID: user.ID, + Username: user.Username, + Token: token, + }, nil +} + +// 收到登陆成功消息,判断是否顶号 +func (s *LoginService) onLogin(uid int64, msg *pb.C2SUserLogin) { + + switch msg.Type { + case pb.ChatType_CT_Private: + sName, err := s.bindService.FindServiceName(msg.DstUser.UserId, pb.ServiceTypeId_STI_Gate) + if err != nil { + log.DebugF("find user:%v in gate err: %v", uid, err) + return + } + s.SendServiceMsg(service.TopicEx(sName), msg.DstUser.UserId, int32(pb.MsgId_S2CChatId), msg) + default: + s.SendServiceMsg(service.TopicEx(topicName.WorldMessage), uid, int32(pb.MsgId_S2CChatId), msg) + } + +} diff --git a/server/login/server/service.go b/server/login/server/service.go new file mode 100644 index 0000000..d0fa908 --- /dev/null +++ b/server/login/server/service.go @@ -0,0 +1,115 @@ +package server + +import ( + "fmt" + "game/common/proto/ipb" + "game/common/proto/pb" + "game/common/serviceName" + "game/common/userBindService" + "game/server/login/config" + "game/server/login/model" + "github.com/fox/fox/log" + "github.com/fox/fox/processor" + "github.com/fox/fox/service" + "github.com/golang/protobuf/proto" +) + +var Login []*LoginService + +type LoginService struct { + *service.NatsService + processor *processor.Processor + bindService *userBindService.UserBindService +} + +func Init() { + for i := 0; i < config.Command.ServiceNum; i++ { + sid := config.Command.ServiceId + i + if srv := newLoginService(sid); srv != nil { + Login = append(Login, srv) + } + } +} + +func Stop() { + for _, srv := range Login { + srv.NotifyStop() + } + for _, srv := range Login { + srv.WaitStop() + } +} + +func newLoginService(serviceId int) *LoginService { + var err error + s := new(LoginService) + + sName := fmt.Sprintf("%v-%d", serviceName.Login, serviceId) + if s.NatsService, err = service.NewNatsService(&service.InitNatsServiceParams{ + EtcdAddress: config.Cfg.Etcd.Address, + EtcdUsername: "", + EtcdPassword: "", + NatsAddress: config.Cfg.Nats.Address, + ServiceType: serviceName.Login, + ServiceName: sName, + OnFunc: s, + TypeId: int(pb.ServiceTypeId_STI_Login), + Version: config.Cfg.BuildDate, + }); err != nil { + log.Fatal(err.Error()) + return nil + } + + s.bindService = userBindService.NewUserBindService(model.UserRedis, s.ServiceEtcd()) + s.processor = processor.NewProcessor() + s.initProcessor() + s.OnInit() + return s +} + +func (s *LoginService) OnInit() { + // if err := s.NatsService.QueueSubscribe(service.GroupTopic(s), service.GroupQueue(s)); err != nil { + // log.Error(err.Error()) + // } + s.NatsService.Run() + log.Debug("onInit") +} + +func (s *LoginService) CanStop() bool { + return true +} + +func (s *LoginService) OnStop() { + s.NatsService.OnStop() + log.Debug("OnStop") +} + +// 处理其它服发送过来的消息 +func (s *LoginService) OnMessage(data []byte) error { + var iMsg = &ipb.InternalMsg{} + var err error + if err = proto.Unmarshal(data, iMsg); err != nil { + log.Error(err.Error()) + return err + } + if req, err := s.processor.Unmarshal(iMsg.MsgId, iMsg.Msg); err == nil { + err = s.processor.Dispatch(iMsg.MsgId, iMsg.UserId, req) + } + //log.Debug(s.Log("on message:%v", string(msg))) + return nil +} + +// 向内部服务发送消息 +func (s *LoginService) SendServiceData(topic string, userId int64, msgId int32, data []byte) { + iMsg := &ipb.InternalMsg{ConnId: 0, UserId: userId, MsgId: msgId, Msg: data} + dMsg, _ := proto.Marshal(iMsg) + _ = s.Send(topic, dMsg) +} + +// 向内部服务发送消息 +func (s *LoginService) SendServiceMsg(topic string, userId int64, msgId int32, msg proto.Message) { + iMsg := &ipb.InternalMsg{ConnId: 0, UserId: userId, MsgId: msgId} + iMsg.Msg, _ = proto.Marshal(msg) + dMsg, _ := proto.Marshal(iMsg) + _ = s.Send(topic, dMsg) +}