登陆服

This commit is contained in:
liuxiaobo 2025-05-29 00:17:18 +08:00
parent ae28cbb0e8
commit e2f75e2db7
11 changed files with 550 additions and 11 deletions

View File

@ -7,6 +7,8 @@ type Common[T any] struct {
Etcd Etcd `json:"etcd"` Etcd Etcd `json:"etcd"`
Redis Redis `json:"redis"` Redis Redis `json:"redis"`
Nats Nats `json:"nats"` Nats Nats `json:"nats"`
Mysql Mysql `json:"mysql"`
MysqlLog Mysql `json:"mysql_log"`
Special *T `json:"special"` Special *T `json:"special"`
GitCommit string `json:"git_commit"` // 服务当前的hash值 GitCommit string `json:"git_commit"` // 服务当前的hash值
GitBranch string `json:"git_branch"` // 服务当前的版本分支 GitBranch string `json:"git_branch"` // 服务当前的版本分支
@ -25,11 +27,12 @@ type Rabbitmq struct {
VHost string `json:"v_host"` VHost string `json:"v_host"`
} }
type DB struct { type Mysql struct {
Host string `json:"host"` Host string `json:"host"`
Port string `json:"port"` Port string `json:"port"`
Username string `json:"username"` Username string `json:"username"`
Password string `json:"password"` Password string `json:"password"`
DbName string `json:"db_name"`
} }
type Redis struct { type Redis struct {

View File

@ -13,14 +13,21 @@ import (
const ( const (
//ModeDev = "dev" // 开发服 //ModeDev = "dev" // 开发服
etcdKey = "etcd_config" etcdKey = "etcd_config"
etcdAddress = "114.132.124.145:2379" etcdAddress = "114.132.124.145:2379"
natsKey = "nats_config" natsKey = "nats_config"
natsAddress = "nats://114.132.124.145:4222" natsAddress = "nats://114.132.124.145:4222"
redisKey = "redis_config" redisKey = "redis_config"
redisAddress = testHelper.Host redisAddress = testHelper.Host
redisPort = testHelper.RedisPort redisPort = testHelper.RedisPort
redisPassword = testHelper.RedisPassword 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 { 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 { } else {
err = json.Unmarshal([]byte(s), &comm.Redis) 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 return &comm, nil
} }

View File

@ -1,6 +1,7 @@
package serviceName package serviceName
const ( const (
Gate = "gate" Gate = "gate"
Chat = "chat" Chat = "chat"
Login = "login"
) )

36
docker.txt Normal file
View File

@ -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

31
server/login/cmd/cmd.go Normal file
View File

@ -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))
}

View File

@ -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")
}

24
server/login/main.go Normal file
View File

@ -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") })
}

57
server/login/model/db.go Normal file
View File

@ -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
}
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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)
}