diff --git a/go.mod b/go.mod index 94ea1fb..70fd5d8 100644 --- a/go.mod +++ b/go.mod @@ -18,8 +18,10 @@ require ( github.com/howeyc/fsnotify v0.9.0 github.com/idealeak/goserver v0.0.0-20201014040547-b8f686262078 github.com/jinzhu/now v1.1.5 + github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 github.com/mojocn/base64Captcha v1.3.6 + github.com/sirupsen/logrus v1.9.0 github.com/spf13/cast v1.7.0 github.com/spf13/viper v1.19.0 github.com/tealeg/xlsx v1.0.5 @@ -33,10 +35,13 @@ require ( golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c google.golang.org/grpc v1.67.1 google.golang.org/protobuf v1.35.1 + gorm.io/driver/mysql v1.5.7 + gorm.io/gorm v1.25.12 mongo.games.com/goserver v0.0.0-00010101000000-000000000000 ) require ( + filippo.io/edwards25519 v1.1.0 // indirect github.com/360EntSecGroup-Skylar/excelize/v2 v2.3.1 // indirect github.com/containrrr/shoutrrr v0.6.1 // indirect github.com/coreos/go-semver v0.3.1 // indirect @@ -45,6 +50,7 @@ require ( github.com/dlclark/regexp2 v1.10.0 // indirect github.com/fatih/color v1.17.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/gocarina/gocsv v0.0.0-20221105105431-c8ef78125b99 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/protobuf v1.5.4 // indirect @@ -53,9 +59,11 @@ require ( github.com/gorilla/websocket v1.5.3 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/innopals/sls-logrus-hook v0.0.0-20190808032145-2fe1d6f7ce00 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect github.com/klauspost/compress v1.17.9 // indirect github.com/klauspost/cpuid/v2 v2.2.8 // indirect github.com/klauspost/reedsolomon v1.12.4 // indirect + github.com/lestrrat-go/strftime v1.1.0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -68,7 +76,6 @@ require ( github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/shopspring/decimal v1.3.1 // indirect - github.com/sirupsen/logrus v1.9.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/spf13/afero v1.11.0 // indirect diff --git a/go.sum b/go.sum index 0bc191c..ab41286 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/360EntSecGroup-Skylar/excelize/v2 v2.3.1 h1:j56fC19WoD3z+u+ZHxm2XwRGyS1XmdSMk7058BLhdsM= github.com/360EntSecGroup-Skylar/excelize/v2 v2.3.1/go.mod h1:gXEhMjm1VadSGjAzyDlBxmdYglP8eJpYWxpwJnmXRWw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -92,6 +94,9 @@ github.com/go-redis/redis v6.14.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8w github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gocarina/gocsv v0.0.0-20221105105431-c8ef78125b99 h1:qNAaZUnCulf2xIQc7rM6F3uGYr80h40rtilsVKyAHoM= github.com/gocarina/gocsv v0.0.0-20221105105431-c8ef78125b99/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI= @@ -159,8 +164,11 @@ github.com/innopals/sls-logrus-hook v0.0.0-20190808032145-2fe1d6f7ce00 h1:QfdUfo github.com/innopals/sls-logrus-hook v0.0.0-20190808032145-2fe1d6f7ce00/go.mod h1:Q24O6QMGImDU3WY71P4YAxNb36NNn5qaznCfMUoXVfc= github.com/jarcoal/httpmock v1.0.4 h1:jp+dy/+nonJE4g4xbVtl9QdrUNbn6/3hDT5R4nDIZnA= github.com/jarcoal/httpmock v1.0.4/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -186,6 +194,12 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/ledisdb/ledisdb v0.0.0-20200510135210-d35789ec47e6/go.mod h1:n931TsDuKuq+uX4v1fulaMbA/7ZLLhjc85h7chZGBCQ= +github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8= +github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is= +github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible h1:Y6sqxHMyB1D2YSzWkLibYKgg+SwmyFU9dF2hn6MdTj4= +github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible/go.mod h1:ZQnN8lSECaebrkQytbHj4xNgtg8CR7RYXnPok8e0EHA= +github.com/lestrrat-go/strftime v1.1.0 h1:gMESpZy44/4pXLO/m+sL0yBd1W6LjgjrrD4a68Gapyg= +github.com/lestrrat-go/strftime v1.1.0/go.mod h1:uzeIB52CeUJenCo1syghlugshMysrqUT51HlxphXVeI= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= @@ -581,6 +595,11 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= +gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= +gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= diff --git a/mongo/export.go b/mongo/export.go index aeac899..a86a949 100644 --- a/mongo/export.go +++ b/mongo/export.go @@ -25,17 +25,18 @@ type Collection = internal.Collection type Database = internal.Database var _manager *internal.Manager -var _conf *internal.Config // GetConfig 获取配置 func GetConfig() *Config { - return _conf + if _manager == nil { + return nil + } + return _manager.GetConfig() } // Init 初始化 func Init(conf *Config) { - _conf = conf - _manager = internal.NewManager(_conf) + _manager = internal.NewManager(conf) } // Restart 重启 @@ -44,7 +45,7 @@ func Restart() { logger.Logger.Error(NotInitError) return } - _manager.Restart(_conf) + _manager.Restart(_manager.GetConfig()) } // Close 关闭 diff --git a/mysql/export.go b/mysql/export.go new file mode 100644 index 0000000..229bcaf --- /dev/null +++ b/mysql/export.go @@ -0,0 +1,48 @@ +package mysql + +import ( + "errors" + + "mongo.games.com/game/mysql/internal" +) + +var NotInitError = errors.New("mysql manager is nil, please call Init() first") + +type Config = internal.Config +type DatabaseConfig = internal.DatabaseConfig +type Database = internal.Database + +var manager *internal.Manager + +func Init(conf *Config) error { + manager = internal.NewManager(conf) + return nil +} + +func SetAutoMigrateTables(tables []interface{}) { + if manager == nil { + return + } + manager.SetAutoMigrateTables(tables) +} + +func GetConfig() *Config { + if manager == nil { + return nil + } + return manager.GetConfig() +} + +func Close() { + if manager == nil { + return + } + manager.Close() +} + +func GetDatabase(platform string) (*Database, error) { + if manager == nil { + return nil, NotInitError + } + return manager.GetDatabase(platform) +} diff --git a/mysql/internal/mysql.go b/mysql/internal/mysql.go new file mode 100644 index 0000000..5aa82b5 --- /dev/null +++ b/mysql/internal/mysql.go @@ -0,0 +1,150 @@ +package internal + +import ( + "fmt" + "sync" + "time" + + "gorm.io/driver/mysql" + "gorm.io/gorm" + "mongo.games.com/goserver/core/logger" +) + +type Config struct { + Platforms map[string]*DatabaseConfig + MaxIdleConns int + MaxOpenConns int + ConnMaxLifetime int + ConnMaxIdletime int +} + +type DatabaseConfig struct { + HostName string + HostPort int32 + Database string + Username string + Password string + Options string +} + +type Database struct { + *Manager + *Config + *DatabaseConfig + *gorm.DB +} + +func (d *Database) Connect() error { + if d.DatabaseConfig == nil { + err := fmt.Errorf("mysql Connect error, DatabaseConifg not found") + logger.Logger.Error(err) + return err + } + + login := "" + if d.DatabaseConfig.Username != "" { + login = d.DatabaseConfig.Username + ":" + d.DatabaseConfig.Password + "@" + } + host := d.DatabaseConfig.HostName + if d.DatabaseConfig.HostName == "" { + host = "127.0.0.1" + } + port := d.DatabaseConfig.HostPort + if d.DatabaseConfig.HostPort == 0 { + port = 3306 + } + database := d.DatabaseConfig.Database + if database == "" { + database = "mysql" + } + myOptions := d.DatabaseConfig.Options + if myOptions != "" { + myOptions = "?" + myOptions + } + + // [username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN] + s := fmt.Sprintf("%stcp(%s:%d)/%s%s", login, host, port, "mysql", myOptions) + db, err := gorm.Open(mysql.Open(s), &gorm.Config{}) + if err != nil { + logger.Logger.Errorf("mysql Connect %v error: %v config:%+v", s, err, *d.DatabaseConfig) + return err + } + logger.Logger.Tracef("mysql connect success %+v", *d.DatabaseConfig) + + err = db.Exec(fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci", d.DatabaseConfig.Database)).Error + if err != nil { + logger.Logger.Errorf("mysql create database %s error: %v", d.DatabaseConfig.Database, err) + return err + } + + s = fmt.Sprintf("%stcp(%s:%d)/%s%s", login, host, port, d.DatabaseConfig.Database, myOptions) + db, err = gorm.Open(mysql.Open(s), &gorm.Config{SkipDefaultTransaction: true}) + if err != nil { + logger.Logger.Errorf("mysql Connect %v error: %v config:%+v", s, err, *d.DatabaseConfig) + return err + } + + sqlDB, err := db.DB() + if err != nil { + logger.Logger.Errorf("mysql get DB error: %v", err) + return err + } + + if len(d.tables) > 0 { + if err := db.AutoMigrate(d.tables...); err != nil { + logger.Logger.Warnf("mysql migrate error: %v", err) + } + } + + sqlDB.SetMaxIdleConns(d.MaxIdleConns) + sqlDB.SetMaxOpenConns(d.MaxOpenConns) + sqlDB.SetConnMaxLifetime(time.Duration(d.ConnMaxLifetime)) + sqlDB.SetConnMaxIdleTime(time.Duration(d.ConnMaxIdletime)) + + d.DB = db.Session(&gorm.Session{SkipDefaultTransaction: true}) + + return nil +} + +type Manager struct { + conf *Config + platforms sync.Map // 平台id:Database + tables []interface{} +} + +func (m *Manager) GetConfig() *Config { + return m.conf +} + +func (m *Manager) SetAutoMigrateTables(tables []interface{}) { + m.tables = tables +} + +func (m *Manager) GetDatabase(key string) (*Database, error) { + v, ok := m.platforms.Load(key) // 平台id + if !ok { + db := &Database{ + Manager: m, + Config: m.conf, + DatabaseConfig: m.conf.Platforms[key], + } + if err := db.Connect(); err != nil { + return nil, err + } + v = db + m.platforms.Store(key, v) + } + d, _ := v.(*Database) + return d, nil +} + +func (m *Manager) Close() { + +} + +func NewManager(conf *Config) *Manager { + return &Manager{ + conf: conf, + platforms: sync.Map{}, + } +} diff --git a/statistics/.gitignore b/statistics/.gitignore new file mode 100644 index 0000000..43aacb1 --- /dev/null +++ b/statistics/.gitignore @@ -0,0 +1,27 @@ +# ---> Go +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +.idea +.vscode +log + diff --git a/statistics/README.md b/statistics/README.md new file mode 100644 index 0000000..88cdc2b --- /dev/null +++ b/statistics/README.md @@ -0,0 +1,8 @@ +# statistics + +数据分析服务 + + * 定时查询注册表和登录日志获取玩家id,根据玩家id触发相关的数据统计 + +- [x] 新手离开记录 +- [ ] 在线时长 \ No newline at end of file diff --git a/statistics/build_linux.bat b/statistics/build_linux.bat new file mode 100644 index 0000000..7962122 --- /dev/null +++ b/statistics/build_linux.bat @@ -0,0 +1,8 @@ +set GOPATH=D:\godev +go env -w GO111MODULE=on + +set CGO_ENABLED=0 +set GOOS=linux +set GOARCH=amd64 +go build +pause \ No newline at end of file diff --git a/statistics/constant/constant.go b/statistics/constant/constant.go new file mode 100644 index 0000000..bf3f798 --- /dev/null +++ b/statistics/constant/constant.go @@ -0,0 +1,13 @@ +package constant + +const ( + User = "user" // 用户库内部名称 + Log = "log" // 日志库内部名称 +) + +const ( + InviteScoreTypeBind = 1 // 绑定邀请码 + InviteScoreTypePay = 2 // 充值返佣 + InviteScoreTypeRecharge = 3 // 充值完成 + InviteScoreTypePayMe = 4 // 充值(自己) +) diff --git a/statistics/etc/config.yaml b/statistics/etc/config.yaml new file mode 100644 index 0000000..ba82d5c --- /dev/null +++ b/statistics/etc/config.yaml @@ -0,0 +1,27 @@ +# 平台id +platforms: + - 1 + +# 几秒同步一次数据 +# 注册表,登录日志表 +update_second: 60 +# 注册表每次同步多少条数据 +update_account_num: 100 +# 登录日志每次同步多少条数据 +update_login_num: 100 +# 几秒读取一次玩家id列表 +update_second_snid: 30 +# 最多触发几个玩家数据更新 +update_snid_num: 100 + +# 邀请数据统计 +# 几秒读取一次邀请记录 +update_second_invite: 10 +# 一次最多读取多少条邀请记录 +update_invite_num: 30 + +# 道具获得数量统计 +# 几秒读取一次道具日志 +update_second_item: 10 +# 一次最多读取多少道具日志 +update_item_num: 100 \ No newline at end of file diff --git a/statistics/etc/mongo.yaml b/statistics/etc/mongo.yaml new file mode 100644 index 0000000..7f9e42d --- /dev/null +++ b/statistics/etc/mongo.yaml @@ -0,0 +1,53 @@ +global: + user: + HostName: 127.0.0.1 + HostPort: 27017 + Database: win88_global + Username: + Password: + Options: + log: + HostName: 127.0.0.1 + HostPort: 27017 + Database: win88_log + Username: + Password: + Options: + monitor: + HostName: 127.0.0.1 + HostPort: 27017 + Database: win88_monitor + Username: + Password: + Options: +platforms: + 0: + user: + HostName: 127.0.0.1 + HostPort: 27017 + Database: win88_user_plt_000 + Username: + Password: + Options: + log: + HostName: 127.0.0.1 + HostPort: 27017 + Database: win88_log_plt_000 + Username: + Password: + Options: + 1: + user: + HostName: 127.0.0.1 + HostPort: 27017 + Database: win88_user_plt_001 + Username: + Password: + Options: + log: + HostName: 127.0.0.1 + HostPort: 27017 + Database: win88_log_plt_001 + Username: + Password: + Options: \ No newline at end of file diff --git a/statistics/etc/mysql.yaml b/statistics/etc/mysql.yaml new file mode 100644 index 0000000..b639fbf --- /dev/null +++ b/statistics/etc/mysql.yaml @@ -0,0 +1,31 @@ +platforms: + global: + HostName: 127.0.0.1 + HostPort: 3306 + Database: win88_user + Username: root + Password: 123456 + Options: charset=utf8mb4&parseTime=True&loc=Local + 0: + HostName: 127.0.0.1 + HostPort: 3306 + Database: win88_plt_000 + Username: root + Password: 123456 + Options: charset=utf8mb4&parseTime=True&loc=Local + 1: + HostName: 127.0.0.1 + HostPort: 3306 + Database: win88_plt_001 + Username: root + Password: 123456 + Options: charset=utf8mb4&parseTime=True&loc=Local + +# 最大空闲连接数 +MaxIdleConns: 10 +# 最大连接数 +MaxOpenConns: 100 +# 连接可复用的最大时间 +ConnMaxLifetime: 3600 +# 连接最大空闲时间 +ConnMaxIdletime: 0 \ No newline at end of file diff --git a/statistics/local_test.go b/statistics/local_test.go new file mode 100644 index 0000000..9e66fc5 --- /dev/null +++ b/statistics/local_test.go @@ -0,0 +1,53 @@ +package main + +import ( + "context" + "fmt" + "testing" + "time" +) + +type A struct { +} + +func B() *A { + return nil +} + +func Test1(t *testing.T) { + var a interface{} + a = B() + fmt.Println(a == nil) // false +} + +func Test2(t *testing.T) { + c1 := context.Background() + c2, cancel2 := context.WithCancel(c1) + c3, _ := context.WithCancel(c2) + + go func() { + select { + case <-c3.Done(): + fmt.Println("c3 cancel") + } + }() + + time.Sleep(time.Second * 5) + cancel2() + fmt.Println("cancel2") + + //time.Sleep(time.Second * 5) + //cancel3() + //fmt.Println("cancel3") + + time.Sleep(time.Minute) +} + +func Test3(t *testing.T) { + n := time.Now() + y, m, d := n.Date() + n = time.Date(y, m, d, 0, 0, 0, 0, time.Local) + st := n.AddDate(0, 0, -int(n.Weekday())) + et := n.AddDate(0, 0, 7-int(n.Weekday())) + fmt.Println(st, et) +} diff --git a/statistics/logger.xml b/statistics/logger.xml new file mode 100644 index 0000000..f6eb37b --- /dev/null +++ b/statistics/logger.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/statistics/main.go b/statistics/main.go new file mode 100644 index 0000000..56d81aa --- /dev/null +++ b/statistics/main.go @@ -0,0 +1,239 @@ +package main + +import ( + "context" + "fmt" + "os" + "os/signal" + "sync" + "time" + + "github.com/spf13/viper" + "mongo.games.com/goserver/core/logger" + + "mongo.games.com/game/mongo" + "mongo.games.com/game/mysql" + mongomodel "mongo.games.com/game/statistics/mongo/model" + mysqlmodel "mongo.games.com/game/statistics/mysql/model" + "mongo.games.com/game/statistics/static" + "mongo.games.com/game/statistics/syn" + "mongo.games.com/game/statistics/tools" + "mongo.games.com/game/util" +) + +var VP *viper.Viper + +//func init() { +// // 日志 +// *log.StandardLogger() = *log.New() +// +// // 日志等级 +// level, err := log.ParseLevel(VP.GetString("log.level")) +// if err != nil { +// panic(err) +// } +// log.SetLevel(level) +// +// // 打印文件路径及行号 +// log.AddHook(tools.NewFileLineHook(log.ErrorLevel)) +// +// // 日志切分 +// for _, v := range VP.Get("log.rotate").([]interface{}) { +// conf := &tools.RotateLogConfig{} +// b, err := json.Marshal(v) +// if err != nil { +// panic(err) +// } +// if err = json.Unmarshal(b, conf); err != nil { +// panic(err) +// } +// log.AddHook(tools.NewRotateLogHook(conf)) +// } +//} + +// DoTick 定时执行 +func DoTick(ctx context.Context, wg *sync.WaitGroup, duration time.Duration, fu func(ctx context.Context)) { + wg.Add(1) + go func() { + defer wg.Done() + for { + select { + case <-ctx.Done(): + return + case <-time.After(duration): + tools.RecoverPanicFunc() // 捕获异常 + fu(ctx) + } + } + }() +} + +func DoTickPlatform(ctx context.Context, wg *sync.WaitGroup, duration time.Duration, batchSize int, + fu func(ctx context.Context, platform string, batchSize int)) { + wg.Add(1) + go func() { + defer wg.Done() + for { + select { + case <-ctx.Done(): + return + case <-time.After(duration): + tools.RecoverPanicFunc() // 捕获异常 + wg := new(sync.WaitGroup) + for _, v := range VP.GetStringSlice("platforms") { + platform := v + wg.Add(1) + go func() { + defer wg.Done() + fu(ctx, platform, batchSize) + }() + } + wg.Wait() + } + } + }() +} + +func main() { + VP = util.GetViper("config", "yaml") + // mongo + vp := util.GetViper("mongo", "yaml") + // mongo初始化 + conf := &mongo.Config{} + err := vp.Unmarshal(conf) + if err != nil { + panic(fmt.Errorf("mongo config error: %v", err)) + } + mongo.Init(conf) + defer mongo.Close() + + // mysql + vp = util.GetViper("mysql", "yaml") + myConf := &mysql.Config{} + err = vp.Unmarshal(myConf) + if err != nil { + panic(fmt.Errorf("mysql config error: %v", err)) + } + mysql.Init(myConf) + defer mysql.Close() + + mysql.SetAutoMigrateTables(mysqlmodel.Tables) + + wg := &sync.WaitGroup{} + ctx, cancel := context.WithCancel(context.Background()) + + DoTick(ctx, wg, time.Duration(VP.GetInt64("update_second"))*time.Second, SyncSnId) + + DoTick(ctx, wg, time.Duration(VP.GetInt64("update_second_snid"))*time.Second, func(ctx context.Context) { + wg := new(sync.WaitGroup) + for _, v := range VP.GetStringSlice("platforms") { + platform := v + wg.Add(1) + go func() { + defer wg.Done() + Static(platform) + }() + } + wg.Wait() + }) + + DoTick(ctx, wg, time.Duration(VP.GetInt64("update_second_invite"))*time.Second, SyncInvite) + + DoTickPlatform(ctx, wg, time.Duration(VP.GetInt64("update_second_item"))*time.Second, VP.GetInt("update_item_num"), + func(ctx context.Context, platform string, batchSize int) { + err := syn.ItemGainDone(&syn.Data[mongomodel.ItemLog]{ + Platform: platform, + BatchSize: batchSize, + }) + if err != nil { + logger.Logger.Errorf("SyncItem error:%v", err) + } + }) + + logger.Logger.Info("start") + + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, os.Kill) + sig := <-c + logger.Logger.Infof("closing down (signal: %v)", sig) + + // release + cancel() + wg.Wait() + + logger.Logger.Info("closed") +} + +// SyncSnId 同步注册和登录日志 +func SyncSnId(ctx context.Context) { + wg := new(sync.WaitGroup) + for _, v := range VP.GetStringSlice("platforms") { + platform := v + wg.Add(1) + go func() { + defer wg.Done() + _, err := syn.UserAccount(platform, VP.GetInt("update_account_num")) + if err != nil { + logger.Logger.Errorf("SyncUserAccount error: %v", err) + return + } + + _, err = syn.LogLogin(platform, VP.GetInt("update_login_num")) + if err != nil { + logger.Logger.Errorf("SyncLogLogin error: %v", err) + return + } + }() + } + wg.Wait() +} + +// Static 玩家id触发数据统计 +func Static(platform string) { + // 查询需要更新的玩家id + var ids []*mysqlmodel.UserID + db, err := mysql.GetDatabase(platform) + if err != nil { + logger.Logger.Errorf("GetDatabase error: %v", err) + return + } + if err := db.Limit(VP.GetInt("update_snid_num")).Find(&ids).Error; err != nil { + logger.Logger.Warnf("Get UserID error: %v", err) + return + } + + if len(ids) == 0 { + logger.Logger.Tracef("Static: no need to update") + return + } + + // 统计玩家跳出记录 + if err := static.UserLogin(platform, ids); err != nil { + logger.Logger.Errorf("StaticUserLogin error: %v", err) + return + } + + // 删除更新过的玩家id + if err := db.Delete(ids).Error; err != nil { + logger.Logger.Errorf("Delete error: %v", err) + return + } +} + +// SyncInvite 同步邀请数据 +func SyncInvite(ctx context.Context) { + wg := new(sync.WaitGroup) + for _, v := range VP.GetStringSlice("platforms") { + platform := v + wg.Add(1) + go func() { + defer wg.Done() + err := syn.SyncInviteScore(platform, VP.GetInt("update_invite_num")) + if err != nil { + logger.Logger.Errorf("SyncInviteScore error: %v", err) + return + } + }() + } + wg.Wait() +} diff --git a/statistics/mongo/model/log_gameplayerlistlog.go b/statistics/mongo/model/log_gameplayerlistlog.go new file mode 100644 index 0000000..4eeac20 --- /dev/null +++ b/statistics/mongo/model/log_gameplayerlistlog.go @@ -0,0 +1,47 @@ +package model + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const LogGamePlayerListLog = "log_gameplayerlistlog" + +type GamePlayerListLog struct { + LogId primitive.ObjectID `bson:"_id"` + SnId int32 //用户Id + Name string //名称 + GameId int32 //游戏id + BaseScore int32 //游戏底注 + ClubId int32 //俱乐部Id + ClubRoom string //俱乐部包间 + TaxCoin int64 //税收 + ClubPumpCoin int64 //俱乐部额外抽水 + Platform string //平台id + Channel string //渠道 + Promoter string //推广员 + PackageTag string //包标识 + SceneId int32 //场景ID + GameMode int32 //游戏类型 + GameFreeid int32 //游戏类型房间号 + GameDetailedLogId string //游戏记录Id + IsFirstGame bool //是否第一次游戏 + //对于拉霸类:BetAmount=100 WinAmountNoAnyTax=0 (表示投入多少、收益多少,值>=0) + //拉霸类小游戏会是:BetAmount=0 WinAmountNoAnyTax=100 (投入0、收益多少,值>=0) + //对战场:BetAmount=0 WinAmountNoAnyTax=100 (投入会有是0、收益有正负,WinAmountNoAnyTax=100则盈利,WinAmountNoAnyTax=-100则输100) + BetAmount int64 //下注金额 + WinAmountNoAnyTax int64 //盈利金额,不包含任何税 + TotalIn int64 //本局投入 + TotalOut int64 //本局产出 + Time time.Time //记录时间 + RoomType int32 //房间类型 + GameDif string //游戏标识 + GameClass int32 //游戏类型 1棋牌 2电子 3百人 4捕鱼 5视讯 6彩票 7体育 + MatchId int32 + MatchType int32 //0.普通场 1.锦标赛 2.冠军赛 3.vip专属 + Ts int32 + IsFree bool //拉霸专用 是否免费 + WinSmallGame int64 //拉霸专用 小游戏奖励 + WinTotal int64 //拉霸专用 输赢 +} diff --git a/statistics/mongo/model/log_invitescore.go b/statistics/mongo/model/log_invitescore.go new file mode 100644 index 0000000..4e6253c --- /dev/null +++ b/statistics/mongo/model/log_invitescore.go @@ -0,0 +1,19 @@ +package model + +import ( + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const LogInviteScore = "log_invitescore" + +type InviteScore struct { + Id primitive.ObjectID `bson:"_id"` + UpSnid int // 上级代理 + DownSnid int // 下级代理 + Level int // 代理层级 例如 1:DownSnid 是 UpSnid 的 1 级代理; 2: DownSnid 是 UpSnid 的 2 级代理 + Tp int // 返佣类型 + Rate int // 返佣比例 + Score int // 积分 + Money int // 充值金额 + Ts int // 时间戳 +} diff --git a/statistics/mongo/model/log_item.go b/statistics/mongo/model/log_item.go new file mode 100644 index 0000000..0c6bf87 --- /dev/null +++ b/statistics/mongo/model/log_item.go @@ -0,0 +1,27 @@ +package model + +import "go.mongodb.org/mongo-driver/bson/primitive" + +const LogItem = "log_itemlog" + +type ItemInfo struct { + ItemId int32 + ItemNum int64 +} + +type ItemLog struct { + LogId primitive.ObjectID `bson:"_id"` + Platform string //平台 + SnId int32 //玩家id + LogType int32 //记录类型 0.获取 1.消耗 + ItemId int32 //道具id + ItemName string //道具名称 + Count int64 //个数 + CreateTs int64 //记录时间 + Remark string //备注 + TypeId int32 // 变化类型 + GameId int32 // 游戏id,游戏中获得时有值 + GameFreeId int32 // 场次id,游戏中获得时有值 + Cost []*ItemInfo // 消耗的道具 + Id string // 撤销的id,兑换失败 +} diff --git a/statistics/mongo/model/log_login.go b/statistics/mongo/model/log_login.go new file mode 100644 index 0000000..7394d3e --- /dev/null +++ b/statistics/mongo/model/log_login.go @@ -0,0 +1,33 @@ +package model + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const LogLogin = "log_login" + +const ( + LogTypeLogin int32 = iota // 登录 + LogTypeLogout // 登出 + LogTypeRehold // 重连 + LogTypeDrop // 掉线 +) + +type LoginLog struct { + LogId primitive.ObjectID `bson:"_id"` + Platform string //平台id + SnId int32 + LogType int32 + Ts int64 + Time time.Time + GameId int // 玩家掉线时所在游戏id + LastGameID int // 玩家最后所在游戏id + ChannelId string // 推广渠道 + + DeviceName string + AppVersion string + BuildVersion string + AppChannel string +} diff --git a/statistics/mongo/model/user_account.go b/statistics/mongo/model/user_account.go new file mode 100644 index 0000000..f87bc7d --- /dev/null +++ b/statistics/mongo/model/user_account.go @@ -0,0 +1,24 @@ +package model + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const UserAccount = "user_account" + +type Account struct { + AccountId primitive.ObjectID `bson:"_id"` + SnId int32 // 玩家账号直接在这里生成 + Platform string // 平台 + RegisterTs int64 // 注册时间戳 + RegisteTime time.Time + ChannelId string // 推广渠道 + + Tel string `gorm:"index"` + DeviceName string `gorm:"index"` + AppVersion string `gorm:"index"` + BuildVersion string `gorm:"index"` + AppChannel string `gorm:"index"` +} diff --git a/statistics/mysql/model/init.go b/statistics/mysql/model/init.go new file mode 100644 index 0000000..976ac05 --- /dev/null +++ b/statistics/mysql/model/init.go @@ -0,0 +1,17 @@ +package model + +// 需要自动迁移的表添加在这里 Tables + +var Tables = []interface{}{ + &LogLogin{}, + &LogLoginMid{}, + &UserAccount{}, + &UserLogin{}, + &UserID{}, + &LogInviteScoreMid{}, + &LogInviteScore{}, + &LogInviteUser{}, + &LogMid{}, + &ItemGain{}, + &ItemTotalGain{}, +} diff --git a/statistics/mysql/model/log_invitescore.go b/statistics/mysql/model/log_invitescore.go new file mode 100644 index 0000000..177d219 --- /dev/null +++ b/statistics/mysql/model/log_invitescore.go @@ -0,0 +1,26 @@ +package model + +type LogInviteScoreMid struct { + ID uint `gorm:"primaryKey"` + MID string +} + +type LogInviteScore struct { + ID uint `gorm:"primaryKey"` + UpSnid int `gorm:"index"` // 上级代理 + DownSnid int `gorm:"index"` // 下级代理 + Level int `gorm:"index"` // 代理层级 例如 1:DownSnid 是 UpSnid 的 1 级代理; 2: DownSnid 是 UpSnid 的 2 级代理 + Tp int `gorm:"index"` // 返佣类型 + Rate int `gorm:"index"` // 返佣比例 + Score int `gorm:"index"` // 积分 + Money int `gorm:"index"` // 充值金额 + Ts int `gorm:"index"` // 时间戳 +} + +type LogInviteUser struct { + ID uint `gorm:"primaryKey"` + Psnid int `gorm:"index"` // 当前玩家 + Snid int `gorm:"index"` // 一级代理 + Level int `gorm:"index"` // 代理层级 例如 1:DownSnid 是 UpSnid 的 1 级代理; 2: DownSnid 是 UpSnid 的 2 级代理 + Ts int `gorm:"index"` // 绑定时间 +} diff --git a/statistics/mysql/model/log_itemgain.go b/statistics/mysql/model/log_itemgain.go new file mode 100644 index 0000000..b361104 --- /dev/null +++ b/statistics/mysql/model/log_itemgain.go @@ -0,0 +1,16 @@ +package model + +// ItemGain 道具获得数量,以小时,道具id,做主键 +type ItemGain struct { + ID uint `gorm:"primaryKey"` + Hour int64 `gorm:"index:idx_item"` // 小时时间戳,每小时统计一次 + ItemId int32 `gorm:"index:idx_item"` // 道具id + ItemNum int64 // 道具数量 +} + +// ItemTotalGain 道具获得总数 +type ItemTotalGain struct { + ID uint `gorm:"primaryKey"` + ItemId int32 `gorm:"index"` // 道具id + ItemNum int64 // 道具数量 +} diff --git a/statistics/mysql/model/log_login.go b/statistics/mysql/model/log_login.go new file mode 100644 index 0000000..3d1aea4 --- /dev/null +++ b/statistics/mysql/model/log_login.go @@ -0,0 +1,26 @@ +package model + +import "time" + +const ( + LogTypeLogin = 1 // 登录 + LogTypeRehold = 2 // 重连 + LogTypeOffline = 3 // 离线 +) + +type LogLogin struct { + ID uint `gorm:"primaryKey"` + Snid int `gorm:"index"` + OnlineType int `gorm:"index"` + //OnlineTs int `gorm:"index"` + OnlineTime time.Time `gorm:"index"` + OfflineType int `gorm:"index"` + //OfflineTs int `gorm:"index"` + OfflineTime time.Time `gorm:"index"` + ChannelId string `gorm:"index"` // 推广渠道 + + DeviceName string `gorm:"index"` + AppVersion string `gorm:"index"` + BuildVersion string `gorm:"index"` + AppChannel string `gorm:"index"` +} diff --git a/statistics/mysql/model/log_login_mid.go b/statistics/mysql/model/log_login_mid.go new file mode 100644 index 0000000..d46df2e --- /dev/null +++ b/statistics/mysql/model/log_login_mid.go @@ -0,0 +1,6 @@ +package model + +type LogLoginMid struct { + ID uint `gorm:"primaryKey"` + MID string +} diff --git a/statistics/mysql/model/log_mid.go b/statistics/mysql/model/log_mid.go new file mode 100644 index 0000000..047c56c --- /dev/null +++ b/statistics/mysql/model/log_mid.go @@ -0,0 +1,11 @@ +package model + +const ( + MidTypeItem = 1 // 道具记录 +) + +type LogMid struct { + ID uint `gorm:"primaryKey"` + Tp int `gorm:"index"` // 类型 + MID string +} diff --git a/statistics/mysql/model/user_account.go b/statistics/mysql/model/user_account.go new file mode 100644 index 0000000..d0c61d1 --- /dev/null +++ b/statistics/mysql/model/user_account.go @@ -0,0 +1,18 @@ +package model + +import "time" + +type UserAccount struct { + ID uint `gorm:"primaryKey"` + MID string + Snid int `gorm:"index"` + //RegisterTs int `gorm:"index"` + RegisterTime time.Time `gorm:"index"` + ChannelId string `gorm:"index"` // 推广渠道 + + DeviceName string `gorm:"index"` + AppVersion string `gorm:"index"` + BuildVersion string `gorm:"index"` + AppChannel string `gorm:"index"` + Tel string `gorm:"index"` +} diff --git a/statistics/mysql/model/user_id.go b/statistics/mysql/model/user_id.go new file mode 100644 index 0000000..a8c2360 --- /dev/null +++ b/statistics/mysql/model/user_id.go @@ -0,0 +1,9 @@ +package model + +/* + 服务定期查询注册和登录信息,然后获取玩家id,保存到这张表中;用于后续触发和玩家相关的数据统计 +*/ + +type UserID struct { + Snid int `gorm:"primaryKey"` +} diff --git a/statistics/mysql/model/user_login.go b/statistics/mysql/model/user_login.go new file mode 100644 index 0000000..a5235e2 --- /dev/null +++ b/statistics/mysql/model/user_login.go @@ -0,0 +1,29 @@ +package model + +import "time" + +const ( + OutTypRegister = 1 // 注册 + OutTypeLogin = 2 // 登录 + OutTypeGaming = 3 // 游戏中 + OutTypeGameOver = 4 // 游戏结束 +) + +type UserLogin struct { + ID uint `gorm:"primaryKey"` + Snid int `gorm:"uniqueIndex"` + //OnlineTs int `gorm:"index"` + OnlineTime time.Time `gorm:"index"` + //OfflineTs int `gorm:"index"` + OfflineTime time.Time `gorm:"index"` + OutType int `gorm:"index"` // 跳出类型 + GameID int `gorm:"index"` // 游戏id + Age int + Sex int + DeviceName string `gorm:"index"` + AppVersion string `gorm:"index"` + BuildVersion string `gorm:"index"` + AppChannel string `gorm:"index"` + Tel string `gorm:"index"` + ChannelId string `gorm:"index"` // 推广渠道 +} diff --git a/statistics/shell/close.sh b/statistics/shell/close.sh new file mode 100644 index 0000000..47f55d7 --- /dev/null +++ b/statistics/shell/close.sh @@ -0,0 +1,4 @@ +#!/bin/bash +pkill -2 statistics +echo "close ..." +tail -f log/all_log \ No newline at end of file diff --git a/statistics/shell/start.sh b/statistics/shell/start.sh new file mode 100644 index 0000000..980d151 --- /dev/null +++ b/statistics/shell/start.sh @@ -0,0 +1,2 @@ +#!/bin/bash +nohup ./statistics > /dev/null & diff --git a/statistics/static/init.go b/statistics/static/init.go new file mode 100644 index 0000000..4a9e036 --- /dev/null +++ b/statistics/static/init.go @@ -0,0 +1 @@ +package static diff --git a/statistics/static/user_login.go b/statistics/static/user_login.go new file mode 100644 index 0000000..ca7f8a8 --- /dev/null +++ b/statistics/static/user_login.go @@ -0,0 +1,372 @@ +package static + +import ( + "context" + "errors" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "mongo.games.com/goserver/core/logger" + + mymongo "mongo.games.com/game/mongo" + mymysql "mongo.games.com/game/mysql" + + "mongo.games.com/game/statistics/constant" + mongomodel "mongo.games.com/game/statistics/mongo/model" + mysqlmodel "mongo.games.com/game/statistics/mysql/model" +) + +func getAccountTel(platform string, id int) (string, error) { + acc := &mongomodel.Account{} + cc, err := mymongo.GetCollection(platform, constant.User, mongomodel.UserAccount) + if err != nil { + logger.Logger.Errorf("get collection %s %s error %v", constant.User, mongomodel.UserAccount, err) + return "", err + } + dd := cc.FindOne(context.TODO(), bson.M{"snid": id}, options.FindOne().SetProjection(bson.M{"tel": 1})) + err = dd.Err() + if err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + logger.Logger.Tracef("getAccountTel %v not found in user_account", id) + return "", nil + } + logger.Logger.Errorf("getAccountTel %v get user_account err: %v", id, err) + return "", err + } + if err := dd.Decode(acc); err != nil { + logger.Logger.Errorf("getAccountTel %v decode user_account err: %v", id, err) + return "", err + } + return acc.Tel, nil +} + +// 游戏结束离开 +func checkGameOver(db *mymysql.Database, login *mysqlmodel.UserLogin, platform string, id int) (bool, error) { + // 最早的一条掉线记录并且是游戏结束离开 + a := &mongomodel.LoginLog{} + c, err := mymongo.GetCollection(platform, constant.Log, mongomodel.LogLogin) + if err != nil { + logger.Logger.Errorf("get collection %s %s error %v", constant.Log, mongomodel.LogLogin, err) + return false, err + } + d := c.FindOne(context.TODO(), bson.M{"snid": id, "logtype": mongomodel.LogTypeDrop, "gameid": 0, "lastgameid": bson.D{{"$gt", 0}}}, + options.FindOne().SetSort(bson.D{{"time", 1}})) + err = d.Err() + if err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + logger.Logger.Tracef("checkGameOver %v not found in log_login", id) + return false, nil + } + logger.Logger.Errorf("checkGameOver %v get log_login err: %v", id, err) + return false, err + } + if err := d.Decode(a); err != nil { + logger.Logger.Errorf("checkGameOver %v decode log_login err: %v", id, err) + return false, err + } + + // account tel + tel, err := getAccountTel(platform, id) + if err != nil { + logger.Logger.Warnf("get account tel %v err: %v", id, err) + } + + update := &mysqlmodel.UserLogin{ + //OfflineTs: int(a.Ts), + OfflineTime: a.Time, + OutType: mysqlmodel.OutTypeGameOver, + GameID: a.LastGameID, + Tel: tel, + DeviceName: a.DeviceName, + AppVersion: a.AppVersion, + BuildVersion: a.BuildVersion, + AppChannel: a.AppChannel, + ChannelId: a.ChannelId, + } + + if err := db.Model(login).Select( + "OfflineTime", "OutType", "GameID", "DeviceName", "AppVersion", "BuildVersion", "AppChannel", "Tel", + ).Updates(update).Error; err != nil { + logger.Logger.Errorf("checkLogin %v update user_login err: %v", id, err) + return false, err + } + + return true, nil +} + +// 游戏中离开 +func checkGaming(db *mymysql.Database, login *mysqlmodel.UserLogin, platform string, id int) (bool, error) { + // 最早的一条掉线记录并且是游戏中掉线 + a := &mongomodel.LoginLog{} + c, err := mymongo.GetCollection(platform, constant.Log, mongomodel.LogLogin) + if err != nil { + logger.Logger.Errorf("get collection %s %s error %v", constant.Log, mongomodel.LogLogin, err) + return false, err + } + d := c.FindOne(context.TODO(), bson.M{"snid": id, "logtype": mongomodel.LogTypeDrop, "gameid": bson.D{{"$gt", 0}}}, + options.FindOne().SetSort(bson.D{{"time", 1}})) + err = d.Err() + if err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + logger.Logger.Tracef("checkGaming %v not found in log_login", id) + return false, nil + } + logger.Logger.Errorf("checkGaming %v get log_login err: %v", id, err) + return false, err + } + if err := d.Decode(a); err != nil { + logger.Logger.Errorf("checkGaming %v decode log_login err: %v", id, err) + return false, err + } + + // account tel + tel, err := getAccountTel(platform, id) + if err != nil { + logger.Logger.Warnf("get account tel %v err: %v", id, err) + } + + update := &mysqlmodel.UserLogin{ + //OfflineTs: int(a.Ts), + OfflineTime: a.Time, + OutType: mysqlmodel.OutTypeGaming, + GameID: a.GameId, + Tel: tel, + DeviceName: a.DeviceName, + AppVersion: a.AppVersion, + BuildVersion: a.BuildVersion, + AppChannel: a.AppChannel, + ChannelId: a.ChannelId, + } + + if err := db.Model(login).Select( + "OfflineTime", "OutType", "GameID", "DeviceName", "AppVersion", "BuildVersion", "AppChannel", "Tel", + ).Updates(update).Error; err != nil { + logger.Logger.Errorf("checkLogin %v update user_login err: %v", id, err) + return false, err + } + + return true, nil +} + +// 登录后离开 +func checkLogin(db *mymysql.Database, login *mysqlmodel.UserLogin, platform string, id int) (bool, error) { + // 最早的一条掉线记录 + a := &mongomodel.LoginLog{} + c, err := mymongo.GetCollection(platform, constant.Log, mongomodel.LogLogin) + if err != nil { + logger.Logger.Errorf("get collection %s %s error %v", constant.Log, mongomodel.LogLogin, err) + return false, err + } + d := c.FindOne(context.TODO(), bson.M{"snid": id, "logtype": mongomodel.LogTypeDrop}, options.FindOne().SetSort(bson.D{{"time", 1}})) + err = d.Err() + if err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + logger.Logger.Tracef("checkLogin %v not found in log_login", id) + return false, nil + } + logger.Logger.Errorf("checkLogin %v get log_login err: %v", id, err) + return false, err + } + if err := d.Decode(a); err != nil { + logger.Logger.Errorf("checkLogin %v decode log_login err: %v", id, err) + return false, err + } + + // account tel + tel, err := getAccountTel(platform, id) + if err != nil { + logger.Logger.Warnf("get account tel %v err: %v", id, err) + } + + update := &mysqlmodel.UserLogin{ + //OfflineTs: int(a.Ts), + OfflineTime: a.Time, + OutType: mysqlmodel.OutTypeLogin, + Tel: tel, + DeviceName: a.DeviceName, + AppVersion: a.AppVersion, + BuildVersion: a.BuildVersion, + AppChannel: a.AppChannel, + ChannelId: a.ChannelId, + } + + if err := db.Model(login).Select( + "OfflineTime", "OutType", "DeviceName", "AppVersion", "BuildVersion", "AppChannel", "Tel", + ).Updates(update).Error; err != nil { + logger.Logger.Errorf("checkLogin %v update user_login err: %v", id, err) + return false, err + } + + return true, nil +} + +// 注册后离开 +func checkRegister(db *mymysql.Database, login *mysqlmodel.UserLogin, platform string, id int) (bool, error) { + a := &mongomodel.Account{} + c, err := mymongo.GetCollection(platform, constant.User, mongomodel.UserAccount) + if err != nil { + logger.Logger.Errorf("get collection %s %s error %v", constant.Log, mongomodel.UserAccount, err) + return false, err + } + d := c.FindOne(context.TODO(), bson.M{"snid": id}) + err = d.Err() + if err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + logger.Logger.Warnf("checkRegister %v not found in user_account", id) + return false, nil + } + logger.Logger.Errorf("checkRegister %v get user_account err: %v", id, err) + return false, err + } + if err := d.Decode(a); err != nil { + logger.Logger.Errorf("checkRegister %v decode user_account err: %v", id, err) + return false, err + } + + // account tel + tel, err := getAccountTel(platform, id) + if err != nil { + logger.Logger.Warnf("get account tel %v err: %v", id, err) + } + + login.Snid = id + //login.OnlineTs = int(a.RegisterTs) + login.OnlineTime = a.RegisteTime + //login.OfflineTs = int(a.RegisterTs) + login.OfflineTime = a.RegisteTime + login.OutType = mysqlmodel.OutTypRegister + login.Tel = tel + login.DeviceName = a.DeviceName + login.AppVersion = a.AppVersion + login.BuildVersion = a.BuildVersion + login.AppChannel = a.AppChannel + login.ChannelId = a.ChannelId + + if err := db.Create(login).Error; err != nil { + logger.Logger.Errorf("checkRegister create err: %v", err) + return false, err + } + return true, nil +} + +// UserLogin 玩家跳出统计 +func UserLogin(platform string, ids []*mysqlmodel.UserID) error { + f := func(id int) error { + // 玩家是否已经统计结束,已经是游戏结束状态 + login := &mysqlmodel.UserLogin{} + db, err := mymysql.GetDatabase(platform) + if err != nil { + logger.Logger.Errorf("UserLogin get db err: %v", err) + return err + } + if err = db.Where("snid = ?", id).Find(login).Error; err != nil { + logger.Logger.Errorf("UserLogin find %v err: %v", id, err) + return err + } + + switch login.OutType { + case mysqlmodel.OutTypeGameOver: + return nil + + case mysqlmodel.OutTypeGaming: + _, err := checkGameOver(db, login, platform, id) + if err != nil { + logger.Logger.Errorf("UserLogin checkGameOver %v err: %v", id, err) + return err + } + return nil + + case mysqlmodel.OutTypeLogin: + ret, err := checkGameOver(db, login, platform, id) + if err != nil { + logger.Logger.Errorf("UserLogin checkGameOver %v err: %v", id, err) + return err + } + if ret { + return nil + } + ret, err = checkGaming(db, login, platform, id) + if err != nil { + logger.Logger.Errorf("UserLogin checkGaming %v err: %v", id, err) + return err + } + if ret { + return nil + } + + case mysqlmodel.OutTypRegister: + ret, err := checkGameOver(db, login, platform, id) + if err != nil { + logger.Logger.Errorf("UserLogin checkGameOver %v err: %v", id, err) + return err + } + if ret { + return nil + } + ret, err = checkGaming(db, login, platform, id) + if err != nil { + logger.Logger.Errorf("UserLogin checkGaming %v err: %v", id, err) + return err + } + if ret { + return nil + } + ret, err = checkLogin(db, login, platform, id) + if err != nil { + logger.Logger.Errorf("UserLogin checkLogin %v err: %v", id, err) + return err + } + if ret { + return nil + } + + default: + ret, err := checkRegister(db, login, platform, id) + if err != nil { + logger.Logger.Errorf("UserLogin checkRegister %v err: %v", id, err) + return err + } + if !ret { + logger.Logger.Warnf("UserLogin not found user_account checkRegister %v err: %v", id, err) + return nil + } + + ret, err = checkGameOver(db, login, platform, id) + if err != nil { + logger.Logger.Errorf("UserLogin checkGameOver %v err: %v", id, err) + return err + } + if ret { + return nil + } + ret, err = checkGaming(db, login, platform, id) + if err != nil { + logger.Logger.Errorf("UserLogin checkGaming %v err: %v", id, err) + return err + } + if ret { + return nil + } + ret, err = checkLogin(db, login, platform, id) + if err != nil { + logger.Logger.Errorf("UserLogin checkLogin %v err: %v", id, err) + return err + } + if ret { + return nil + } + return nil + } + + return nil + } + + for _, v := range ids { + if err := f(v.Snid); err != nil { + return err + } + } + + return nil +} diff --git a/statistics/syn/init.go b/statistics/syn/init.go new file mode 100644 index 0000000..b7247af --- /dev/null +++ b/statistics/syn/init.go @@ -0,0 +1,102 @@ +package syn + +import ( + "context" + "errors" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "gorm.io/gorm" + + mymongo "mongo.games.com/game/mongo" + mymysql "mongo.games.com/game/mysql" + mysqlmodel "mongo.games.com/game/statistics/mysql/model" + "mongo.games.com/goserver/core/logger" +) + +// Data 数据同步方法 +// T mongodb数据表结构 +// F mongodb中的每条数据的处理操作,自行实现 +type Data[T any] struct { + Platform string // 平台 + MidType int // 数据类型 例如 model.MidTypeItem + Database string // 库名称 + CollectionName string // 集合名称 + BatchSize int // 一次读取数量 + // F 自定义数据处理方法 + // data: mongodb中的一条日志 + F func(data *T, db *gorm.DB) (string, error) +} + +// CommonDone 数据获取方式,根据mongodb集合主键按时间顺序批量读取 +func (d *Data[T]) CommonDone() error { + db, err := mymysql.GetDatabase(d.Platform) + if err != nil { + logger.Logger.Errorf("mysql: failed to get database: %v", err) + return err + } + loginMID := &mysqlmodel.LogMid{Tp: d.MidType} + var n int64 + err = db.Model(&mysqlmodel.LogMid{}).Find(loginMID).Count(&n).Error + if err != nil { + logger.Logger.Errorf("mysql: failed to get log_mid: %v", err) + return err + } + if n == 0 { + if err = db.Create(loginMID).Error; err != nil { + logger.Logger.Errorf("mysql: failed to create log_mid: %v", err) + return err + } + } + + logger.Logger.Tracef("start log_mid tp:%v _id:%v", loginMID.Tp, loginMID.MID) + + _id, _ := primitive.ObjectIDFromHex(loginMID.MID) + filter := bson.M{"_id": bson.M{"$gt": _id}} + c, err := mymongo.GetCollection(d.Platform, mymongo.DatabaseType(d.Database), d.CollectionName) + if err != nil { + logger.Logger.Errorf("get collection %s %s error %v", d.Database, d.CollectionName, err) + return err + } + l, err := c.Find(context.TODO(), filter, + options.Find().SetSort(bson.D{primitive.E{Key: "_id", Value: 1}}), options.Find().SetLimit(int64(d.BatchSize))) + if err != nil && !errors.Is(err, mongo.ErrNoDocuments) { + logger.Logger.Errorf("mongo: failed to get %v: %v", d.CollectionName, err) + return err + } + + var logs []*T + if err = l.All(context.TODO(), &logs); err != nil { + l.Close(context.TODO()) + if errors.Is(err, mongo.ErrNoDocuments) { + return nil + } + + logger.Logger.Errorf("mongo: failed to get %v: %v", d.CollectionName, err) + return err + } + l.Close(context.TODO()) + if len(logs) == 0 { + logger.Logger.Infof("sync %v finished", d.CollectionName) + return nil + } + + err = db.Transaction(func(tx *gorm.DB) error { + for _, v := range logs { + loginMID.MID, err = d.F(v, tx) + if err != nil { + logger.Logger.Errorf("Process %v error:%v", d.CollectionName, err) + return err + } + if err = tx.Model(loginMID).Updates(loginMID).Error; err != nil { + logger.Logger.Errorf("mysql: failed to update %v log_mid: %v", d.CollectionName, err) + return err + } + } + return nil + }) + + return err +} diff --git a/statistics/syn/log_invitescore.go b/statistics/syn/log_invitescore.go new file mode 100644 index 0000000..fa17b14 --- /dev/null +++ b/statistics/syn/log_invitescore.go @@ -0,0 +1,291 @@ +package syn + +import ( + "context" + "errors" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "gorm.io/gorm" + + mymongo "mongo.games.com/game/mongo" + mymysql "mongo.games.com/game/mysql" + "mongo.games.com/game/statistics/constant" + mongomodel "mongo.games.com/game/statistics/mongo/model" + mysqlmodel "mongo.games.com/game/statistics/mysql/model" + "mongo.games.com/goserver/core/logger" +) + +func SyncInviteScore(platform string, batchSize int) error { + db, err := mymysql.GetDatabase(platform) + if err != nil { + logger.Logger.Errorf("mysql: SyncInviteScore failed to get database: %v", err) + return err + } + inviteMID := &mysqlmodel.LogInviteScoreMid{ID: 1} + var n int64 + err = db.Model(&mysqlmodel.LogInviteScoreMid{}).Find(inviteMID).Count(&n).Error + if err != nil { + logger.Logger.Errorf("mysql: SyncInviteScore failed to get log_invitescore_mid: %v", err) + return err + } + if n == 0 { + if err = db.Create(inviteMID).Error; err != nil { + logger.Logger.Errorf("mysql: SyncInviteScore failed to create log_invitescore_mid: %v", err) + return err + } + } + + logger.Logger.Tracef("start SyncInviteScore log_invitescore _id:%v", inviteMID.MID) + + _id, _ := primitive.ObjectIDFromHex(inviteMID.MID) + filter := bson.M{"_id": bson.M{"$gt": _id}} + c, err := mymongo.GetCollection(platform, constant.Log, mongomodel.LogInviteScore) + if err != nil { + logger.Logger.Errorf("get collection %s %s error %v", constant.Log, mongomodel.LogInviteScore, err) + return err + } + l, err := c.Find(context.TODO(), filter, + options.Find().SetSort(bson.D{primitive.E{Key: "_id", Value: 1}}), options.Find().SetLimit(int64(batchSize))) + if err != nil && !errors.Is(err, mongo.ErrNoDocuments) { + logger.Logger.Errorf("mongo: SyncInviteScore failed to get log_invitescore: %v", err) + return err + } + + var logs []*mongomodel.InviteScore + if err = l.All(context.TODO(), &logs); err != nil { + l.Close(context.TODO()) + if errors.Is(err, mongo.ErrNoDocuments) { + return err + } + + logger.Logger.Errorf("mongo: SyncInviteScore failed to get log_invitescore: %v", err) + return err + } + l.Close(context.TODO()) + + getPSnId := func(tx *gorm.DB, snid int) (int, error) { + if snid <= 0 { + return 0, nil + } + ret := new(mysqlmodel.LogInviteUser) + if err = tx.First(ret, "snid = ? and level = 1", snid).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + logger.Logger.Errorf("mysql: SyncInviteScore failed to getPSnId: %v", err) + return 0, err + } + return ret.Psnid, nil + } + + getDownSnId := func(tx *gorm.DB, snid []int) ([]int, error) { + if len(snid) == 0 { + return nil, nil + } + var ret []int + var us []*mysqlmodel.LogInviteUser + if err = tx.Select("snid").Where("psnid IN ? AND level = 1", snid).Find(&us).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + logger.Logger.Errorf("mysql: SyncInviteScore failed to getDownSnId: %v", err) + return ret, err + } + for _, v := range us { + ret = append(ret, v.Snid) + } + return ret, nil + } + + bind := func(tx *gorm.DB, psnid, snid int, ts int) ([]*mysqlmodel.LogInviteUser, error) { + var lu []*mysqlmodel.LogInviteUser + var a1, a2, a3, a4, b1 int + var b2, b3, b4 []int + a4 = psnid + a3, err = getPSnId(tx, a4) + if err != nil { + return nil, err + } + a2, err = getPSnId(tx, a3) + if err != nil { + return nil, err + } + a1, err = getPSnId(tx, a2) + if err != nil { + return nil, err + } + b1 = snid + b2, err = getDownSnId(tx, []int{b1}) + if err != nil { + return nil, err + } + b3, err = getDownSnId(tx, b2) + if err != nil { + return nil, err + } + b4, err = getDownSnId(tx, b3) + if err != nil { + return nil, err + } + logger.Logger.Tracef("a1:%d, a2:%d, a3:%d, a4:%d, b1:%d, b2:%v, b3:%v, b4:%v", a1, a2, a3, a4, b1, b2, b3, b4) + if a1 > 0 { + if b1 > 0 { + lu = append(lu, &mysqlmodel.LogInviteUser{ + Psnid: a1, + Snid: b1, + Level: 4, + Ts: ts, + }) + logger.Logger.Tracef("a1: %v %v %v", b1, 4, ts) + } + } + if a2 > 0 { + if b1 > 0 { + lu = append(lu, &mysqlmodel.LogInviteUser{ + Psnid: a2, + Snid: b1, + Level: 3, + Ts: ts, + }) + logger.Logger.Tracef("a2: %v %v %v", b1, 3, ts) + } + for _, v := range b2 { + if v <= 0 { + continue + } + lu = append(lu, &mysqlmodel.LogInviteUser{ + Psnid: a2, + Snid: v, + Level: 4, + Ts: ts, + }) + logger.Logger.Tracef("a2: %v %v %v", v, 4, ts) + } + } + if a3 > 0 { + if b1 > 0 { + lu = append(lu, &mysqlmodel.LogInviteUser{ + Psnid: a3, + Snid: b1, + Level: 2, + Ts: ts, + }) + logger.Logger.Tracef("a3: %v %v %v", b1, 2, ts) + } + for _, v := range b2 { + if v <= 0 { + continue + } + lu = append(lu, &mysqlmodel.LogInviteUser{ + Psnid: a3, + Snid: v, + Level: 3, + Ts: ts, + }) + logger.Logger.Tracef("a3: %v %v %v", v, 3, ts) + } + for _, v := range b3 { + if v <= 0 { + continue + } + lu = append(lu, &mysqlmodel.LogInviteUser{ + Psnid: a3, + Snid: v, + Level: 4, + Ts: ts, + }) + logger.Logger.Tracef("a3: %v %v %v", v, 4, ts) + } + } + if a4 > 0 { + if b1 > 0 { + lu = append(lu, &mysqlmodel.LogInviteUser{ + Psnid: a4, + Snid: b1, + Level: 1, + Ts: ts, + }) + logger.Logger.Tracef("a4: %v %v %v", b1, 1, ts) + } + for _, v := range b2 { + if v <= 0 { + continue + } + lu = append(lu, &mysqlmodel.LogInviteUser{ + Psnid: a4, + Snid: v, + Level: 2, + Ts: ts, + }) + logger.Logger.Tracef("a4: %v %v %v", v, 2, ts) + } + for _, v := range b3 { + if v <= 0 { + continue + } + lu = append(lu, &mysqlmodel.LogInviteUser{ + Psnid: a4, + Snid: v, + Level: 3, + Ts: ts, + }) + logger.Logger.Tracef("a4: %v %v %v", v, 3, ts) + } + for _, v := range b4 { + if v <= 0 { + continue + } + lu = append(lu, &mysqlmodel.LogInviteUser{ + Psnid: a4, + Snid: v, + Level: 4, + Ts: ts, + }) + logger.Logger.Tracef("a4: %v %v %v", v, 4, ts) + } + } + return lu, nil + } + + for _, v := range logs { + err = db.Transaction(func(tx *gorm.DB) error { + inviteMID.MID = v.Id.Hex() + if err = tx.Model(inviteMID).Updates(inviteMID).Error; err != nil { + logger.Logger.Errorf("mysql: SyncInviteScore failed to update log_invitescore_mid: %v", err) + return err + } + + err = tx.Save(&mysqlmodel.LogInviteScore{ + UpSnid: v.UpSnid, + DownSnid: v.DownSnid, + Level: v.Level, + Tp: v.Tp, + Rate: v.Rate, + Score: v.Score, + Money: v.Money, + Ts: v.Ts, + }).Error + if err != nil { + logger.Logger.Errorf("mysql: SyncInviteScore failed to insert: %v", err) + return err + } + + if v.Tp == constant.InviteScoreTypeBind && v.Level == 0 { + // 绑定关系 + lu, err := bind(tx, v.UpSnid, v.DownSnid, v.Ts) + if err != nil { + logger.Logger.Errorf("mysql: SyncInviteScore failed to bind: %v", err) + return err + } + if err = tx.CreateInBatches(lu, len(lu)).Error; err != nil { + logger.Logger.Errorf("mysql: SyncInviteScore failed to create log_invite_user: %v", err) + return err + } + } + + return nil + }) + if err != nil { + logger.Logger.Errorf("mysql: SyncInviteScore failed to transaction: %v", err) + return err + } + } + return nil +} diff --git a/statistics/syn/log_itemgain.go b/statistics/syn/log_itemgain.go new file mode 100644 index 0000000..80b828b --- /dev/null +++ b/statistics/syn/log_itemgain.go @@ -0,0 +1,61 @@ +package syn + +import ( + "errors" + "time" + + "gorm.io/gorm" + + "mongo.games.com/game/statistics/constant" + mongomodel "mongo.games.com/game/statistics/mongo/model" + mysqlmodel "mongo.games.com/game/statistics/mysql/model" +) + +func ItemGainDone(data *Data[mongomodel.ItemLog]) error { + data.MidType = mysqlmodel.MidTypeItem + data.Database = constant.Log + data.CollectionName = mongomodel.LogItem + data.F = func(data *mongomodel.ItemLog, db *gorm.DB) (string, error) { + if data == nil || data.LogId.Hex() == "" { + return "", errors.New("null") + } + if data.LogType != 0 || data.Id != "" { + return data.LogId.Hex(), nil + } + + hourTime := time.Unix(data.CreateTs, 0).Local() + hourTs := time.Date(hourTime.Year(), hourTime.Month(), hourTime.Day(), hourTime.Hour(), 0, 0, 0, time.Local).Unix() + + item := &mysqlmodel.ItemGain{} + err := db.Model(item).Where("hour = ? and item_id = ?", hourTs, data.ItemId).First(item).Error + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return "", err + } + item.Hour = hourTs + item.ItemId = data.ItemId + item.ItemNum += data.Count + if item.ID == 0 { + err = db.Create(item).Error + } else { + err = db.Model(item).Updates(item).Error + } + if err != nil { + return "", err + } + + itemTotal := &mysqlmodel.ItemTotalGain{} + err = db.Model(itemTotal).Where("item_id = ?", data.ItemId).First(itemTotal).Error + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return "", err + } + itemTotal.ItemId = data.ItemId + itemTotal.ItemNum += data.Count + if itemTotal.ID == 0 { + err = db.Create(itemTotal).Error + } else { + err = db.Model(itemTotal).Updates(itemTotal).Error + } + return data.LogId.Hex(), err + } + return data.CommonDone() +} diff --git a/statistics/syn/log_login.go b/statistics/syn/log_login.go new file mode 100644 index 0000000..01260e5 --- /dev/null +++ b/statistics/syn/log_login.go @@ -0,0 +1,182 @@ +package syn + +import ( + "context" + "errors" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "gorm.io/gorm" + + mymongo "mongo.games.com/game/mongo" + mymysql "mongo.games.com/game/mysql" + "mongo.games.com/game/statistics/constant" + mongomodel "mongo.games.com/game/statistics/mongo/model" + mysqlmodel "mongo.games.com/game/statistics/mysql/model" + "mongo.games.com/goserver/core/logger" +) + +/* + 登录日志同步使用了mongo的_id,从小到大每次同步n个 +*/ + +// LogLogin 同步登录日志 +func LogLogin(platform string, batchSize int) ([]*mysqlmodel.LogLogin, error) { + db, err := mymysql.GetDatabase(platform) + if err != nil { + logger.Logger.Errorf("mysql: SyncLogLogin failed to get database: %v", err) + return nil, err + } + loginMID := &mysqlmodel.LogLoginMid{ID: 1} + var n int64 + err = db.Model(&mysqlmodel.LogLoginMid{}).Find(loginMID).Count(&n).Error + if err != nil { + logger.Logger.Errorf("mysql: SyncLogLogin failed to get log_login_mid: %v", err) + return nil, err + } + if n == 0 { + if err = db.Create(loginMID).Error; err != nil { + logger.Logger.Errorf("mysql: SyncLogLogin failed to create log_login_mid: %v", err) + return nil, err + } + } + + logger.Logger.Tracef("start SyncLogLogin log_login _id:%v", loginMID.MID) + + _id, _ := primitive.ObjectIDFromHex(loginMID.MID) + filter := bson.M{"_id": bson.M{"$gt": _id}} + c, err := mymongo.GetCollection(platform, constant.Log, mongomodel.LogLogin) + if err != nil { + logger.Logger.Errorf("get collection %s %s error %v", constant.Log, mongomodel.LogLogin, err) + return nil, err + } + l, err := c.Find(context.TODO(), filter, + options.Find().SetSort(bson.D{primitive.E{Key: "_id", Value: 1}}), options.Find().SetLimit(int64(batchSize))) + if err != nil && !errors.Is(err, mongo.ErrNoDocuments) { + logger.Logger.Errorf("mongo: SyncLogLogin failed to get log_login: %v", err) + return nil, err + } + + var logs []*mongomodel.LoginLog + if err = l.All(context.TODO(), &logs); err != nil { + l.Close(context.TODO()) + if errors.Is(err, mongo.ErrNoDocuments) { + return nil, nil + } + + logger.Logger.Errorf("mongo: SyncLogLogin failed to get loginlog: %v", err) + return nil, err + } + l.Close(context.TODO()) + + var ls []*mysqlmodel.LogLogin + for _, v := range logs { + logger.Logger.Tracef("mongo SyncLogLogin log_login: %+v", *v) + var e *mysqlmodel.LogLogin + switch v.LogType { + case mongomodel.LogTypeLogin, mongomodel.LogTypeRehold: + onlineType := mysqlmodel.LogTypeLogin + if v.LogType == mongomodel.LogTypeRehold { + onlineType = mysqlmodel.LogTypeRehold + } + + // 创建数据 + var n int64 + if err = db.Model(&mysqlmodel.LogLogin{}).Where("snid = ? AND online_type = ? AND online_time = ?", + v.SnId, onlineType, v.Time).Count(&n).Error; err != nil { + logger.Logger.Errorf("mysql: SyncLogLogin failed to get log_login count: %v", err) + return ls, err + } + + if n == 0 { + e = &mysqlmodel.LogLogin{ + Snid: int(v.SnId), + OnlineType: onlineType, + //OnlineTs: int(v.Ts), + OnlineTime: v.Time, + OfflineType: 0, + //OfflineTs: 0, + OfflineTime: v.Time, + DeviceName: v.DeviceName, + AppVersion: v.AppVersion, + BuildVersion: v.BuildVersion, + AppChannel: v.AppChannel, + ChannelId: v.ChannelId, + } + if err = db.Create(e).Error; err != nil { + logger.Logger.Errorf("mysql: SyncLogLogin failed to create log_login: %v", err) + return ls, err + } + } else { + continue + } + + case mongomodel.LogTypeLogout, mongomodel.LogTypeDrop: + // 修改数据 + e = &mysqlmodel.LogLogin{} + err = db.Model(&mysqlmodel.LogLogin{}).Where("snid = ?", v.SnId).Order("online_time DESC").First(e).Error + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + logger.Logger.Errorf("mysql: SyncLogLogin failed to find log_login: %v", err) + return ls, err + } + + if errors.Is(err, gorm.ErrRecordNotFound) { + logger.Logger.Warnf("mysql: SyncLogLogin not found log_login: %v", v) + continue + } + + if e.OfflineType != 0 { + logger.Logger.Tracef("mysql: SyncLogLogin already offline: %+v", *e) + continue + } + + e.OfflineType = mysqlmodel.LogTypeOffline + //e.OfflineTs = int(v.Ts) + e.OfflineTime = v.Time + if err = db.Model(e).Select("OfflineType", "OfflineTime").Updates(e).Error; err != nil { + logger.Logger.Errorf("mysql: SyncLogLogin failed to update log_login: %v", err) + return ls, err + } + default: + continue + } + + if e != nil { + ls = append(ls, e) + } + } + + if len(logs) > 0 { + err = db.Transaction(func(tx *gorm.DB) error { + loginMID.MID = logs[len(logs)-1].LogId.Hex() + if err = tx.Model(loginMID).Updates(loginMID).Error; err != nil { + logger.Logger.Errorf("mysql: SyncLogLogin failed to update log_login_mid: %v", err) + return err + } + + for _, v := range ls { + if err = tx.First(&mysqlmodel.UserID{}, "snid = ?", v.Snid).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + logger.Logger.Errorf("mysql: SyncLogLogin failed to find user_id: %v", err) + return err + } + + if errors.Is(err, gorm.ErrRecordNotFound) { + if err = tx.Create(&mysqlmodel.UserID{Snid: v.Snid}).Error; err != nil { + logger.Logger.Errorf("mysql: SyncLogLogin failed to create user_id: %v", err) + return err + } + } + } + + return nil + }) + if err != nil { + logger.Logger.Errorf("mysql: SyncLogLogin failed to transaction: %v", err) + return nil, err + } + } + + return ls, nil +} diff --git a/statistics/syn/user_account.go b/statistics/syn/user_account.go new file mode 100644 index 0000000..e3f3756 --- /dev/null +++ b/statistics/syn/user_account.go @@ -0,0 +1,106 @@ +package syn + +import ( + "context" + "errors" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "gorm.io/gorm" + + mymongo "mongo.games.com/game/mongo" + mymysql "mongo.games.com/game/mysql" + "mongo.games.com/game/statistics/constant" + mongomodel "mongo.games.com/game/statistics/mongo/model" + mysqlmodel "mongo.games.com/game/statistics/mysql/model" + "mongo.games.com/goserver/core/logger" +) + +/* + 注册信息同步,使用mongo的_id,从小到大每次同步n个 +*/ + +// UserAccount 同步注册表 +func UserAccount(platform string, batchSize int) ([]*mysqlmodel.UserAccount, error) { + db, err := mymysql.GetDatabase(platform) + if err != nil { + logger.Logger.Errorf("mysql: UserAccount failed to get database: %v", err) + return nil, err + } + account := &mysqlmodel.UserAccount{} + err = db.Model(&mysqlmodel.UserAccount{}).Last(account).Error + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + logger.Logger.Errorf("mysql: UserAccount failed to get account: %v", err) + return nil, err + } + + logger.Logger.Tracef("start UserAccount account _id:%v", account.MID) + + _id, _ := primitive.ObjectIDFromHex(account.MID) + filter := bson.M{"_id": bson.M{"$gt": _id}} + c, err := mymongo.GetCollection(platform, constant.User, mongomodel.UserAccount) + if err != nil { + logger.Logger.Errorf("get collection %s %s error %v", constant.Log, mongomodel.UserAccount, err) + return nil, err + } + l, err := c.Find(context.TODO(), filter, + options.Find().SetSort(bson.D{primitive.E{Key: "_id", Value: 1}}), options.Find().SetLimit(int64(batchSize))) + if err != nil && !errors.Is(err, mongo.ErrNoDocuments) { + logger.Logger.Errorf("mongo: UserAccount failed to get account: %v", err) + return nil, err + } + + var accounts []*mongomodel.Account + if err = l.All(context.TODO(), &accounts); err != nil { + l.Close(context.TODO()) + if errors.Is(err, mongo.ErrNoDocuments) { + return nil, nil + } + + logger.Logger.Errorf("mongo: UserAccount failed to get account: %v", err) + return nil, err + } + l.Close(context.TODO()) + + var as []*mysqlmodel.UserAccount + err = db.Transaction(func(tx *gorm.DB) error { + for _, v := range accounts { + logger.Logger.Tracef("mongo account: %+v", *v) + a := &mysqlmodel.UserAccount{ + MID: v.AccountId.Hex(), + Snid: int(v.SnId), + //RegisterTs: int(v.RegisterTs), + RegisterTime: v.RegisteTime, + Tel: v.Tel, + ChannelId: v.ChannelId, + } + + if err = tx.Create(a).Error; err != nil { + logger.Logger.Errorf("mysql: UserAccount failed to create account: %v", err) + return err + } + + if err = tx.First(&mysqlmodel.UserID{}, "snid = ?", v.SnId).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + logger.Logger.Errorf("mysql: UserAccount failed to find user_id: %v", err) + return err + } + + if errors.Is(err, gorm.ErrRecordNotFound) { + if err = tx.Create(&mysqlmodel.UserID{Snid: int(v.SnId)}).Error; err != nil { + logger.Logger.Errorf("mysql: UserAccount failed to create user_id: %v", err) + return err + } + } + + as = append(as, a) + } + return nil + }) + if err != nil { + logger.Logger.Errorf("mysql: UserAccount failed to transaction: %v", err) + return as, err + } + return as, nil +} diff --git a/statistics/tools/logrus_hook.go b/statistics/tools/logrus_hook.go new file mode 100644 index 0000000..bdda57c --- /dev/null +++ b/statistics/tools/logrus_hook.go @@ -0,0 +1,95 @@ +package tools + +import ( + "fmt" + "runtime" + "strings" + "time" + + rotatelogs "github.com/lestrrat-go/file-rotatelogs" + "github.com/sirupsen/logrus" + "github.com/sirupsen/logrus/hooks/writer" +) + +// FileLineHook 新增一个字段用来打印文件路径及行号 +type FileLineHook struct { + LogLevels []logrus.Level // 需要打印的日志级别 + FieldName string // 字段名称 + Skip int // 跳过几层调用栈 + Num int // Skip后的查找范围 + Test bool // 打印所有调用栈信息,找出合适的 Skip 配置 + filename string // 文件名 + line int // 行号 +} + +func (e *FileLineHook) Levels() []logrus.Level { + return e.LogLevels +} + +func (e *FileLineHook) Fire(entry *logrus.Entry) error { + for i := 0; i < e.Num; i++ { + _, e.filename, e.line, _ = runtime.Caller(e.Skip + i) + if !strings.Contains(e.filename, "logrus") { + break + } + } + entry.Data[e.FieldName] = fmt.Sprintf("%s:%d", e.filename, e.line) + if e.Test { + buf := [4096]byte{} + n := runtime.Stack(buf[:], false) + fmt.Println(string(buf[:n])) + } + return nil +} + +// NewFileLineHook 打印文件路径及行号 +// levels 指定日志级别 +func NewFileLineHook(levels ...logrus.Level) logrus.Hook { + return &FileLineHook{ + LogLevels: levels, + FieldName: "source", + Skip: 8, + Num: 2, + } +} + +type RotateLogConfig struct { + Levels []string `json:"levels"` + Pattern string `json:"pattern"` + LinkName string `json:"link_name"` + MaxAge int `json:"max_age"` + RotationTime int `json:"rotation_time"` + RotationCount int `json:"rotation_count"` + RotationSize int `json:"rotation_size"` +} + +func NewRotateLogHook(config *RotateLogConfig) logrus.Hook { + var levels []logrus.Level + for _, v := range config.Levels { + level, err := logrus.ParseLevel(v) + if err != nil { + panic(err) + } + levels = append(levels, level) + } + + if len(levels) == 0 { + levels = logrus.AllLevels + } + + l, err := rotatelogs.New(config.Pattern, + rotatelogs.WithLinkName(config.LinkName), + rotatelogs.WithMaxAge(time.Duration(config.MaxAge)*time.Hour), + rotatelogs.WithRotationTime(time.Duration(config.RotationTime)*time.Hour), + rotatelogs.WithRotationCount(uint(config.RotationCount)), + rotatelogs.WithRotationSize(int64(config.RotationSize)), + ) + if err != nil { + panic(err) + } + + return &writer.Hook{ + Writer: l, + LogLevels: levels, + } +} diff --git a/statistics/tools/panic.go b/statistics/tools/panic.go new file mode 100644 index 0000000..b89d000 --- /dev/null +++ b/statistics/tools/panic.go @@ -0,0 +1,37 @@ +package tools + +import ( + "bytes" + "fmt" + "os" + "runtime" + "sync" +) + +var RecoverPanicFunc func(args ...interface{}) + +func init() { + bufPool := &sync.Pool{ + New: func() interface{} { + return &bytes.Buffer{} + }, + } + RecoverPanicFunc = func(args ...interface{}) { + if r := recover(); r != nil { + buf := bufPool.Get().(*bytes.Buffer) + defer bufPool.Put(buf) + buf.Reset() + buf.WriteString(fmt.Sprintf("panic: %v\n", r)) + for _, v := range args { + buf.WriteString(fmt.Sprintf("%v\n", v)) + } + pcs := make([]uintptr, 10) + n := runtime.Callers(3, pcs) + frames := runtime.CallersFrames(pcs[:n]) + for f, again := frames.Next(); again; f, again = frames.Next() { + buf.WriteString(fmt.Sprintf("%v:%v %v\n", f.File, f.Line, f.Function)) + } + fmt.Fprint(os.Stderr, buf.String()) + } + } +} diff --git a/util/viper.go b/util/viper.go new file mode 100644 index 0000000..3b695c3 --- /dev/null +++ b/util/viper.go @@ -0,0 +1,29 @@ +package util + +import ( + "fmt" + + "github.com/spf13/viper" +) + +var paths = []string{ + ".", + "./etc", + "./config", +} + +func GetViper(name, filetype string) *viper.Viper { + vp := viper.New() + // 配置文件 + vp.SetConfigName(name) + vp.SetConfigType(filetype) + for _, v := range paths { + vp.AddConfigPath(v) + } + + err := vp.ReadInConfig() + if err != nil { + panic(fmt.Errorf("fatal error config file: %w", err)) + } + return vp +} diff --git a/worldsrv/action_lottery.go b/worldsrv/action_lottery.go index d93237f..fb23ff2 100644 --- a/worldsrv/action_lottery.go +++ b/worldsrv/action_lottery.go @@ -5,6 +5,7 @@ import ( "mongo.games.com/goserver/core/logger" "mongo.games.com/goserver/core/netlib" "mongo.games.com/goserver/core/task" + "time" "mongo.games.com/game/common" "mongo.games.com/game/model" @@ -29,6 +30,7 @@ func CSLotteryInfoHandler(s *netlib.Session, packetid int, data interface{}, sid pack := &welfare.SCLotteryInfo{} + now := time.Now() var list []*welfare.LotteryInfo cfg := PlatformMgrSingleton.GetConfig(p.Platform).LotteryConfig if cfg != nil { @@ -42,8 +44,11 @@ func CSLotteryInfoHandler(s *netlib.Session, packetid int, data interface{}, sid for _, v := range list { playerLottery := info.Lottery[v.GetId()] if playerLottery != nil && playerLottery.StartTs == v.GetStartTs() { - v.CostRoomCard = playerLottery.CostCard - v.Codes = playerLottery.Code + // 活动开始和发奖期间显示 + if now.Unix() >= v.GetStartTs() && now.Unix() < v.GetWinTs() { + v.CostRoomCard = playerLottery.CostCard + v.Codes = playerLottery.Code + } } } }