diff --git a/common/serialization/serialization.go b/common/serialization/serialization.go new file mode 100644 index 0000000..0e15579 --- /dev/null +++ b/common/serialization/serialization.go @@ -0,0 +1,21 @@ +package serialization + +import "reflect" + +func StructToMap(obj interface{}) map[string]interface{} { + out := make(map[string]interface{}) + v := reflect.ValueOf(obj) + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + t := v.Type() + for i := 0; i < v.NumField(); i++ { + field := t.Field(i) + key := field.Tag.Get("json") // 使用 json tag 作为字段名 + if key == "" { + key = field.Name + } + out[key] = v.Field(i).Interface() + } + return out +} diff --git a/server/client/server/chat.go b/server/client/server/chat.go new file mode 100644 index 0000000..c84f75e --- /dev/null +++ b/server/client/server/chat.go @@ -0,0 +1,28 @@ +package server + +import ( + "game/common/proto/pb" + "github.com/fox/fox/log" +) + +func (s *ClientService) chat() { + //const content = "hello world" + //s.SendMsg(pb.ServiceTypeId_STI_Chat, int32(pb.MsgId_C2SChatId), &pb.C2SChat{ + // SrcUser: s.userId, + // DstUser: nil, + // Type: 0, + // GameId: 0, + // Content: "", + //}) +} + +// 收到登陆成功消息,判断是否顶号 +func (s *ClientService) onChat(cMsg *pb.ClientMsg, msg *pb.S2CUserLogin) { + if msg.Code != pb.ErrCode_OK { + log.ErrorF("login error: %v", msg.Code) + return + } + _ = cMsg + s.userId = msg.UserId + log.DebugF("user:%v id:%v login success", s.username, msg.UserId) +} diff --git a/server/client/server/processor.go b/server/client/server/processor.go index 27db690..c2422db 100644 --- a/server/client/server/processor.go +++ b/server/client/server/processor.go @@ -10,21 +10,3 @@ func (s *ClientService) initProcessor() { pb.MsgId_S2CUserLoginId: {pb.S2CUserLogin{}, s.onLogin}, }) } - -// 收到登陆成功消息,判断是否顶号 -func (s *ClientService) onChat(uid int64, msg *pb.C2SChat) { - _ = uid - _ = msg - //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/db/cmd/cmd.go b/server/db/cmd/cmd.go new file mode 100644 index 0000000..1f027a5 --- /dev/null +++ b/server/db/cmd/cmd.go @@ -0,0 +1,32 @@ +package cmd + +import ( + "fmt" + "game/server/db/config" + "game/server/db/model" + "game/server/db/server" + "github.com/fox/fox/log" + "os" + "os/signal" + "syscall" +) + +func initRepo() { + model.InitRedis() + model.InitDb() +} + +func Run(GitCommit, GitBranch, BuildDate string) { + config.InitLog() + 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/db/config/config.go b/server/db/config/config.go new file mode 100644 index 0000000..ab93548 --- /dev/null +++ b/server/db/config/config.go @@ -0,0 +1,37 @@ +package config + +import ( + "game/common/config" + "github.com/fox/fox/db" + "github.com/fox/fox/log" +) + +var Command *config.Command +var Cfg *config.Common[DbConfig] + +type DbConfig struct { +} + +func InitLog() { + log.Open("./log/db.log", log.DebugL) + log.Info("") + log.Info("") + log.Info("") + log.Info("-----init log success-----") +} + +func LoadConfig(GitCommit, GitBranch, BuildDate string) { + Command = config.ParseCommand() + 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[DbConfig](rdb, GitCommit, GitBranch, BuildDate) + if err != nil { + log.Error(err.Error()) + return + } + log.DebugF("load common config success") +} diff --git a/server/db/main.go b/server/db/main.go new file mode 100644 index 0000000..49265f2 --- /dev/null +++ b/server/db/main.go @@ -0,0 +1,24 @@ +package main + +import ( + "game/server/db/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/db/model/db.go b/server/db/model/db.go new file mode 100644 index 0000000..d8a76e1 --- /dev/null +++ b/server/db/model/db.go @@ -0,0 +1,59 @@ +package model + +import ( + "game/server/db/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 + } +} diff --git a/server/db/model/tableOption.go b/server/db/model/tableOption.go new file mode 100644 index 0000000..06a8383 --- /dev/null +++ b/server/db/model/tableOption.go @@ -0,0 +1,148 @@ +package model + +import ( + "context" + "encoding/json" + "fmt" + "game/common/proto/pb" + "game/common/serialization" + "github.com/fox/fox/log" + "github.com/go-redis/redis/v8" + "gorm.io/gorm" + "time" +) + +const ( + tableExpire = 7 * 24 * time.Hour // 七天后过期 +) + +type resultT[T any] struct { + ret T + //err error +} + +type iTable interface { + GetId() uint +} + +/* +T:Table,如果不想操作redis则将rds设置为nil +*/ +type TableOp[T iTable] struct { + db *gorm.DB + rds *redis.Client +} + +func newTableOp[T iTable](db *gorm.DB, rds *redis.Client) *TableOp[T] { + return &TableOp[T]{db: db, rds: rds} +} + +func (s *TableOp[T]) tableName() string { + var result resultT[T] + return s.db.Model(&result.ret).Statement.Table +} + +func (s *TableOp[T]) redisKey(id uint) string { + return fmt.Sprintf("%s:%d", s.tableName(), id) +} + +func (s *TableOp[T]) findByRedis(id uint) *T { + if s.rds == nil { + return nil + } + maps, err := s.rds.HGetAll(context.Background(), s.redisKey(id)).Result() + if err != nil { + log.ErrorF("redis-key:%v HGetAll err: %v", s.redisKey(id), err) + return nil + } + if len(maps) == 0 { + return nil + } + jsonByte, _ := json.Marshal(maps) + var result resultT[T] + _ = json.Unmarshal(jsonByte, &result.ret) + return &result.ret +} + +func (s *TableOp[T]) writeRedis(id uint, t *T) { + if s.rds == nil { + return + } + maps := serialization.StructToMap(t) + if len(maps) == 0 { + log.ErrorF("table struct is empty:%v", s.tableName()) + } + s.updateRedis(id, maps) +} + +func (s *TableOp[T]) updateRedis(id uint, maps map[string]any) { + if s.rds == nil { + return + } + if err := s.rds.HMSet(context.Background(), s.redisKey(id), maps).Err(); err != nil { + log.ErrorF("redis-key:%v HMSet err: %v", s.redisKey(id), err) + } + _ = s.rds.Expire(context.Background(), s.redisKey(id), tableExpire).Err() +} + +func (s *TableOp[T]) deleteRedis(id uint) { + if s.rds == nil { + return + } + _ = s.rds.Del(context.Background(), s.redisKey(id)).Err() +} + +func (s *TableOp[T]) Create(t *T) (*T, pb.ErrCode) { + if err := s.db.Create(t).Error; err != nil { + log.ErrorF("create table:%v err:%v", s.tableName(), err) + return nil, pb.ErrCode_SystemErr + } + return t, pb.ErrCode_OK +} + +func (s *TableOp[T]) Find(id uint) (*T, pb.ErrCode) { + // 先从redis中查询,redis中没有则从mysql中查询 + if table := s.findByRedis(id); table != nil { + return table, pb.ErrCode_OK + } + var result resultT[T] + err := s.db.Where("id = ?", id).First(&result.ret).Error + if err != nil { + log.ErrorF("find table:%v id:%v err:%v", s.tableName(), id, err) + return nil, pb.ErrCode_SystemErr + } + return &result.ret, pb.ErrCode_OK +} + +//// 根据条件查询,只在mysql中查询,无法在redis中查询 +//func (s *TableOp[T]) FindCondition(condition map[string]any) (*T, error) { +// var result resultT[T] +// err := s.db.Where(condition).First(&result.ret).Error +// if err != nil { +// log.ErrorF("find table:%v condition:%v err:%v", s.tableName(), utils.JsonMarshal(condition), err) +// return nil, err +// } +// return &result.ret, nil +//} + +func (s *TableOp[T]) Update(id uint, updates map[string]any) (*T, pb.ErrCode) { + var result resultT[T] + err := s.db.Model(&result.ret).Where("id = ?", id).Updates(updates).Error + if err != nil { + log.ErrorF("update table:%v id:%v err:%v", s.tableName(), id, err) + return nil, pb.ErrCode_SystemErr + } + s.updateRedis(id, updates) + return &result.ret, pb.ErrCode_OK +} + +func (s *TableOp[T]) Delete(id uint) (*T, pb.ErrCode) { + var result resultT[T] + err := s.db.Delete(&result.ret, id).Error + if err != nil { + log.ErrorF("delete table:%v err:%v", s.tableName(), err) + return nil, pb.ErrCode_SystemErr + } + s.deleteRedis(id) + return &result.ret, pb.ErrCode_OK +} diff --git a/server/db/model/user.go b/server/db/model/user.go new file mode 100644 index 0000000..e7ac52b --- /dev/null +++ b/server/db/model/user.go @@ -0,0 +1,23 @@ +package model + +import ( + "gorm.io/gorm" +) + +// 玩家账户表 +type User struct { + gorm.Model + Nickname string `gorm:"type:varchar(32);uniqueIndex;not null"` // 用户名 + AvatarUrl string `gorm:"type:varchar(255)"` // 头像 + AvatarBorder string `gorm:"type:varchar(255)"` // 头像框 + Gold int64 `gorm:"type:bigint;default:0"` // 金币 + VipExp int32 `gorm:"type:int"` // vip经验值 +} + +func (u User) GetId() uint { + return u.ID +} + +func NewUserOp() *TableOp[User] { + return newTableOp[User](UserDB, UserRedis) +} diff --git a/server/db/model/userAccount.go b/server/db/model/userAccount.go new file mode 100644 index 0000000..cc25888 --- /dev/null +++ b/server/db/model/userAccount.go @@ -0,0 +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 +} diff --git a/server/db/server/processor.go b/server/db/server/processor.go new file mode 100644 index 0000000..3b46e96 --- /dev/null +++ b/server/db/server/processor.go @@ -0,0 +1,57 @@ +package server + +import ( + "errors" + "game/common/proto/pb" + "game/server/db/model" + "github.com/fox/fox/ipb" + "github.com/fox/fox/processor" + "github.com/fox/fox/service" + "gorm.io/gorm" +) + +func (s *DbService) initProcessor() { + s.processor.RegisterMessages(processor.RegisterMetas{ + pb.MsgId_C2SUserLoginId: {pb.C2SUserLogin{}, s.onLoginOrRegister}, + }) +} + +func (s *DbService) checkLoginOrRegister(req *pb.C2SUserLogin) (user *model.UserAccount, code pb.ErrCode) { + op := model.NewUserLoginOp() + var err error + user, err = op.Login(req.Username, req.Password, req.Ip, req.DeviceId) + 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 + } + } + return user, code +} + +// 登录或注册 +func (s *DbService) 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) +} diff --git a/server/db/server/service.go b/server/db/server/service.go new file mode 100644 index 0000000..6f54c24 --- /dev/null +++ b/server/db/server/service.go @@ -0,0 +1,116 @@ +package server + +import ( + "fmt" + "game/common/proto/pb" + "game/common/serviceName" + "game/common/userBindService" + "game/server/db/config" + "game/server/db/model" + "github.com/fox/fox/ipb" + "github.com/fox/fox/log" + "github.com/fox/fox/processor" + "github.com/fox/fox/service" + "github.com/golang/protobuf/proto" +) + +var DbSrv []*DbService + +type DbService struct { + *service.NatsService + processor *processor.Processor + bindService *userBindService.UserBindService +} + +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 { + DbSrv = append(DbSrv, srv) + } + } +} + +func Stop() { + for _, srv := range DbSrv { + log.DebugF("notify stop service %v", srv.Name()) + srv.NotifyStop() + } + for _, srv := range DbSrv { + srv.WaitStop() + } +} + +func newLoginService(serviceId int) *DbService { + var err error + s := new(DbService) + + 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 *DbService) 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 *DbService) CanStop() bool { + return true +} + +func (s *DbService) OnStop() { + s.NatsService.OnStop() + log.Debug("OnStop") +} + +// 处理其它服发送过来的消息 +func (s *DbService) 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, req) + } else { + log.Error(err.Error()) + } + //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) +}