添加db服

This commit is contained in:
liuxiaobo 2025-05-31 23:34:58 +08:00
parent 02d1811d50
commit da1f14b7dd
12 changed files with 697 additions and 18 deletions

View File

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

View File

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

View File

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

32
server/db/cmd/cmd.go Normal file
View File

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

View File

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

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

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

59
server/db/model/db.go Normal file
View File

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

View File

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

23
server/db/model/user.go Normal file
View File

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

View File

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

View File

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

116
server/db/server/service.go Normal file
View File

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