diff --git a/pkg/go.mod b/pkg/go.mod index b1b7b26..86432bb 100644 --- a/pkg/go.mod +++ b/pkg/go.mod @@ -4,8 +4,11 @@ go 1.24.3 require ( buf.build/go/protovalidate v0.14.0 + github.com/redis/go-redis/v9 v9.19.0 + go.uber.org/zap v1.24.0 google.golang.org/grpc v1.80.0 google.golang.org/protobuf v1.36.11 + gopkg.in/natefinch/lumberjack.v2 v2.2.1 gorm.io/driver/mysql v1.6.0 gorm.io/gorm v1.31.2 gorm.io/plugin/dbresolver v1.6.2 @@ -16,11 +19,14 @@ require ( cel.dev/expr v0.25.1 // indirect filippo.io/edwards25519 v1.1.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/google/cel-go v0.25.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/multierr v1.6.0 // indirect golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/sys v0.40.0 // indirect diff --git a/pkg/go.sum b/pkg/go.sum index 0b6dae3..fbd9614 100644 --- a/pkg/go.sum +++ b/pkg/go.sum @@ -8,6 +8,12 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -31,20 +37,29 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD 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/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/redis/go-redis/v9 v9.19.0 h1:XPVaaPSnG6RhYf7p+rmSa9zZfeVAnWsH5h3lxthOm/k= +github.com/redis/go-redis/v9 v9.19.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA= github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= +github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= @@ -57,6 +72,15 @@ go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2W go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw= golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= @@ -76,6 +100,8 @@ google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07 google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= 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= diff --git a/pkg/log/log.go b/pkg/log/log.go new file mode 100644 index 0000000..489d021 --- /dev/null +++ b/pkg/log/log.go @@ -0,0 +1,137 @@ +package log + +import ( + "fmt" + "os" + "time" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "gopkg.in/natefinch/lumberjack.v2" +) + +var sugar *zap.SugaredLogger + +type Config struct { + Path string + InfoFile string + ErrorFile string + FatalFile string + MaxSize int + MaxBackups int + MaxAge int +} + +func Init(c Config) error { + l, err := New(c) + if err != nil { + return err + } + sugar = l + return nil +} + +func Info(args ...any) { sugar.Info(args...) } +func Infof(template string, args ...any) { sugar.Infof(template, args...) } +func Warn(args ...any) { sugar.Warn(args...) } +func Warnf(template string, args ...any) { sugar.Warnf(template, args...) } +func Error(args ...any) { sugar.Error(args...) } +func Errorf(template string, args ...any) { sugar.Errorf(template, args...) } +func Fatal(args ...any) { sugar.Fatal(args...) } +func Fatalf(template string, args ...any) { sugar.Fatalf(template, args...) } + +func New(c Config) (*zap.SugaredLogger, error) { + if c.Path == "" { + c.Path = "logs" + } + if c.InfoFile == "" { + c.InfoFile = "info.log" + } + if c.ErrorFile == "" { + c.ErrorFile = "error.log" + } + if c.FatalFile == "" { + c.FatalFile = "fatal.log" + } + if c.MaxSize == 0 { + c.MaxSize = 100 + } + if c.MaxBackups == 0 { + c.MaxBackups = 10 + } + if c.MaxAge == 0 { + c.MaxAge = 30 + } + + if err := os.MkdirAll(c.Path, 0o755); err != nil { + return nil, fmt.Errorf("log mkdir: %w", err) + } + + consoleEncoder := zapcore.NewConsoleEncoder(zapcore.EncoderConfig{ + MessageKey: "msg", + LevelKey: "level", + EncodeLevel: zapcore.LowercaseColorLevelEncoder, + TimeKey: "ts", + EncodeTime: func(t time.Time, enc zapcore.PrimitiveArrayEncoder) { + enc.AppendString(t.Format("2006-01-02 15:04:05")) + }, + CallerKey: "file", + EncodeCaller: zapcore.ShortCallerEncoder, + EncodeDuration: func(d time.Duration, enc zapcore.PrimitiveArrayEncoder) { + enc.AppendInt64(int64(d) / 1e6) + }, + }) + + fileEncoder := zapcore.NewConsoleEncoder(zapcore.EncoderConfig{ + MessageKey: "msg", + LevelKey: "level", + EncodeLevel: zapcore.LowercaseLevelEncoder, + TimeKey: "ts", + EncodeTime: func(t time.Time, enc zapcore.PrimitiveArrayEncoder) { + enc.AppendString(t.Format("2006-01-02 15:04:05")) + }, + CallerKey: "file", + EncodeCaller: zapcore.ShortCallerEncoder, + EncodeDuration: func(d time.Duration, enc zapcore.PrimitiveArrayEncoder) { + enc.AppendInt64(int64(d) / 1e6) + }, + }) + + infoLevel := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { + return lvl == zapcore.InfoLevel + }) + errorLevel := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { + return lvl == zapcore.ErrorLevel + }) + fatalLevel := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { + return lvl == zapcore.FatalLevel + }) + + today := time.Now().Format("2006-01-02") + infoWriter := getWriter(c.Path+"/"+today+"-"+c.InfoFile, c) + errorWriter := getWriter(c.Path+"/"+today+"-"+c.ErrorFile, c) + fatalWriter := getWriter(c.Path+"/"+today+"-"+c.FatalFile, c) + + core := zapcore.NewTee( + zapcore.NewCore(consoleEncoder, zapcore.AddSync(os.Stdout), zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { + return lvl >= zapcore.InfoLevel + })), + zapcore.NewCore(fileEncoder, infoWriter, infoLevel), + zapcore.NewCore(fileEncoder, errorWriter, errorLevel), + zapcore.NewCore(fileEncoder, fatalWriter, fatalLevel), + ) + + // AddCallerSkip(1):经包级 Info/Errorf 转发时显示真实调用方 + return zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1)).Sugar(), nil +} + +func getWriter(filename string, c Config) zapcore.WriteSyncer { + return zapcore.AddSync(&lumberjack.Logger{ + Filename: filename, + MaxSize: c.MaxSize, + MaxBackups: c.MaxBackups, + MaxAge: c.MaxAge, + Compress: false, + LocalTime: true, + }) +} diff --git a/pkg/redis/redis.go b/pkg/redis/redis.go new file mode 100644 index 0000000..461623c --- /dev/null +++ b/pkg/redis/redis.go @@ -0,0 +1,58 @@ +package redis + +import ( + "context" + "fmt" + "time" + + goredis "github.com/redis/go-redis/v9" +) + +const Nil = goredis.Nil + +var Client *goredis.Client + +type Config struct { + Host string + Port int + Password string + DB int +} + +func Init(c Config) error { + client, err := New(c) + if err != nil { + return err + } + Client = client + return nil +} + +func New(c Config) (*goredis.Client, error) { + if c.Host == "" { + return nil, fmt.Errorf("redis: Host is empty") + } + if c.Port == 0 { + c.Port = 6379 + } + + client := goredis.NewClient(&goredis.Options{ + Addr: fmt.Sprintf("%s:%d", c.Host, c.Port), + Password: c.Password, + DB: c.DB, + }) + + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + if err := client.Ping(ctx).Err(); err != nil { + _ = client.Close() + return nil, fmt.Errorf("redis ping: %w", err) + } + + return client, nil +} + +func GetRedisKey(key string) string { + return key +} diff --git a/product/etc/product.yaml b/product/etc/product.yaml index 2289905..e9471dd 100644 --- a/product/etc/product.yaml +++ b/product/etc/product.yaml @@ -2,6 +2,18 @@ Name: product-service ListenOn: 0.0.0.0:10100 Mode: dev +Log: + ServiceName: product-service + Mode: file # console | file | volume + Encoding: plain + Level: info + Path: logs + KeepDays: 7 + Compress: false + # Rotation: daily + # MaxSize: 100 + # MaxBackups: 10 + Nacos: Hosts: - rnacos:8848 @@ -17,8 +29,23 @@ Mysql: Database: dms-product Charset: utf8mb4 Prefix: - # ReadHost: mysql-slave # 必填才会启用读写分离 - # ReadPort: 3306 # 可省略,默认用主库 Port - # ReadUser: root # 可省略,默认用主库 User - # ReadPassword: "123123" # 可省略,默认用主库 Password - # ReadDatabase: dms-product # 可省略,默认用主库 Database \ No newline at end of file + # ReadHost: mysql-slave + # ReadPort: 3306 + # ReadUser: root + # ReadPassword: "123123" + # ReadDatabase: dms-product + +BizRedis: + Host: redis + Port: 6379 + Password: "" + DB: 0 + +AppLog: + Path: logs + InfoFile: info.log + ErrorFile: error.log + FatalFile: fatal.log + MaxSize: 100 + MaxBackups: 10 + MaxAge: 30 diff --git a/product/go.mod b/product/go.mod index 5877881..739c658 100644 --- a/product/go.mod +++ b/product/go.mod @@ -4,7 +4,9 @@ go 1.24.3 require ( buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250717165733-d22d418d82d8.1 + github.com/redis/go-redis/v9 v9.19.0 github.com/zeromicro/go-zero v1.10.2 + go.uber.org/zap v1.24.0 google.golang.org/grpc v1.80.0 google.golang.org/protobuf v1.36.11 gorm.io/gorm v1.31.2 @@ -59,7 +61,6 @@ require ( github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.66.1 // indirect github.com/prometheus/procfs v0.16.1 // indirect - github.com/redis/go-redis/v9 v9.19.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect github.com/titanous/json5 v1.0.0 // indirect @@ -82,7 +83,6 @@ require ( go.uber.org/automaxprocs v1.6.0 // indirect go.uber.org/mock v0.6.0 // indirect go.uber.org/multierr v1.9.0 // indirect - go.uber.org/zap v1.24.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect @@ -96,6 +96,7 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gorm.io/driver/mysql v1.6.0 // indirect diff --git a/product/go.sum b/product/go.sum index 346ec60..fe78074 100644 --- a/product/go.sum +++ b/product/go.sum @@ -283,6 +283,8 @@ gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY= gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI= gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= diff --git a/product/internal/config/config.go b/product/internal/config/config.go index 4fb10e5..3de9421 100644 --- a/product/internal/config/config.go +++ b/product/internal/config/config.go @@ -4,8 +4,10 @@ import "github.com/zeromicro/go-zero/zrpc" type Config struct { zrpc.RpcServerConf - Nacos NacosConf - Mysql MysqlConf + Nacos NacosConf + Mysql MysqlConf + BizRedis RedisConf + AppLog LogConf } type NacosConf struct { @@ -30,3 +32,20 @@ type MysqlConf struct { ReadPassword string `json:",optional"` ReadDatabase string `json:",optional"` } + +type RedisConf struct { + Host string + Port int `json:",default=6379"` + Password string `json:",optional"` + DB int `json:",optional"` +} + +type LogConf struct { + Path string `json:",default=logs"` + InfoFile string `json:",default=info.log"` + ErrorFile string `json:",default=error.log"` + FatalFile string `json:",default=fatal.log"` + MaxSize int `json:",default=100"` + MaxBackups int `json:",default=10"` + MaxAge int `json:",default=30"` +} diff --git a/product/internal/logic/addLogic.go b/product/internal/logic/addLogic.go index 64aadad..9ef54f6 100644 --- a/product/internal/logic/addLogic.go +++ b/product/internal/logic/addLogic.go @@ -11,7 +11,9 @@ import ( "product/internal/svc" "product/product" + "pkg.local/log" "pkg.local/modelbase" + "pkg.local/redis" "github.com/zeromicro/go-zero/core/logx" "google.golang.org/grpc/codes" @@ -34,6 +36,11 @@ func NewAddLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AddLogic { } func (l *AddLogic) Add(in *product.AddReq) (*product.AddResp, error) { + l.Info("zero logx 方式 ... ") + log.Info("原始 zap 方式 ... ") + + redis.Client.Get(l.ctx, redis.GetRedisKey("product:test")) + if in.GetType() == dao.ProductTypeNormal { if in.GetAgentPrice() == 0 || in.GetStorePrice() == 0 || in.GetSalePrice() == 0 || in.GetSharePrice() == 0 || @@ -56,7 +63,7 @@ func (l *AddLogic) Add(in *product.AddReq) (*product.AddResp, error) { Eq: map[string]string{"name": in.GetName()}, }, &check) if err != nil { - l.Errorf("check product name: %v", err) + log.Errorf("check product name: %v", err) return nil, status.Error(codes.Internal, "查询失败") } if check.Id > 0 { @@ -150,10 +157,18 @@ func (l *AddLogic) Add(in *product.AddReq) (*product.AddResp, error) { return model.VerifyModel{}.Init().WithTX(tx).Create(&verify) }) if err != nil { - l.Errorf("create product: %v", err) + log.Errorf("create product: %v", err) return nil, status.Error(codes.Internal, "创建失败") } + // redis 用法示例 + key := redis.GetRedisKey("product:last_add") + if err := redis.Client.Set(l.ctx, key, in.GetName(), time.Hour).Err(); err != nil { + log.Errorf("redis set %s: %v", key, err) + } else { + log.Infof("add product success id=%d", data.Id) + } + return &product.AddResp{}, nil } diff --git a/product/product.go b/product/product.go index 46487f1..f287803 100644 --- a/product/product.go +++ b/product/product.go @@ -3,8 +3,8 @@ package main import ( "flag" "fmt" - "log" "net" + "os" "strconv" "product/internal/config" @@ -13,11 +13,14 @@ import ( "product/product" "pkg.local/discovery" + "pkg.local/log" "pkg.local/modelbase" - localmysql "pkg.local/mysql" + "pkg.local/mysql" + "pkg.local/redis" "pkg.local/validate" "github.com/zeromicro/go-zero/core/conf" + "github.com/zeromicro/go-zero/core/logx" "github.com/zeromicro/go-zero/core/service" "github.com/zeromicro/go-zero/zrpc" "google.golang.org/grpc" @@ -32,6 +35,19 @@ func main() { var c config.Config conf.MustLoad(*configFile, &c) + if err := log.Init(log.Config{ + Path: c.AppLog.Path, + InfoFile: c.AppLog.InfoFile, + ErrorFile: c.AppLog.ErrorFile, + FatalFile: c.AppLog.FatalFile, + MaxSize: c.AppLog.MaxSize, + MaxBackups: c.AppLog.MaxBackups, + MaxAge: c.AppLog.MaxAge, + }); err != nil { + fmt.Fprintf(os.Stderr, "log init: %v\n", err) + os.Exit(1) + } + if err := discovery.Init(discovery.Config{ Hosts: c.Nacos.Hosts, NamespaceId: c.Nacos.NamespaceId, @@ -55,11 +71,11 @@ func main() { } defer func() { if err := discovery.Deregister(); err != nil { - log.Printf("nacos deregister: %v", err) + log.Errorf("nacos deregister: %v", err) } }() - db, err := localmysql.New(localmysql.Config{ + db, err := mysql.New(mysql.Config{ Host: c.Mysql.Host, Port: c.Mysql.Port, User: c.Mysql.User, @@ -77,6 +93,15 @@ func main() { log.Fatalf("mysql init: %v", err) } + if err := redis.Init(redis.Config{ + Host: c.BizRedis.Host, + Port: c.BizRedis.Port, + Password: c.BizRedis.Password, + DB: c.BizRedis.DB, + }); err != nil { + log.Fatalf("redis init: %v", err) + } + // modelbase uses Table(prefix+name); keep gorm TablePrefix for Model(&T{}) paths. modelbase.Init(db, modelbase.Config{Prefix: c.Mysql.Prefix}) @@ -91,9 +116,11 @@ func main() { }) defer s.Stop() + logx.AddWriter(logx.NewWriter(os.Stdout)) + s.AddUnaryInterceptors(validate.UnaryServerInterceptor(validate.MustNew())) - fmt.Printf("Starting rpc server at %s...\n", c.ListenOn) + log.Infof("Starting rpc server at %s...", c.ListenOn) s.Start() } diff --git a/utils/cronLog.go b/utils/cronLog.go index 3557e10..c324e19 100644 --- a/utils/cronLog.go +++ b/utils/cronLog.go @@ -6,16 +6,19 @@ import ( "go.uber.org/zap" "go.uber.org/zap/zapcore" + "gopkg.in/natefinch/lumberjack.v2" ) +// Logger 保留给 utils 内旧代码使用;新代码请用 pkg.local/log。 +var Logger = zap.NewNop().Sugar() + var CronLogger *zap.SugaredLogger func InitCronLog() { - // 设置一些基本日志格式 具体含义还比较好理解,直接看zap源码也不难懂 encoder := zapcore.NewConsoleEncoder(zapcore.EncoderConfig{ MessageKey: "msg", LevelKey: "level", - EncodeLevel: zapcore.LowercaseColorLevelEncoder, //设置颜色 + EncodeLevel: zapcore.LowercaseColorLevelEncoder, TimeKey: "ts", EncodeTime: func(t time.Time, enc zapcore.PrimitiveArrayEncoder) { enc.AppendString(t.Format("2006-01-02 15:04:05")) @@ -25,7 +28,7 @@ func InitCronLog() { EncodeDuration: func(d time.Duration, enc zapcore.PrimitiveArrayEncoder) { enc.AppendInt64(int64(d) / 1000000) }, - }) // 实现两个判断日志等级的interface + }) fileEncoderConfig := zapcore.EncoderConfig{ TimeKey: "time", @@ -35,9 +38,9 @@ func InitCronLog() { EncodeTime: func(t time.Time, enc zapcore.PrimitiveArrayEncoder) { enc.AppendString(t.Format("2006-01-02 15:04:05")) }, - EncodeLevel: zapcore.LowercaseLevelEncoder, // 无颜色 + EncodeLevel: zapcore.LowercaseLevelEncoder, EncodeCaller: zapcore.ShortCallerEncoder, - ConsoleSeparator: " ", // tab分隔 + ConsoleSeparator: " ", } fileEncoder := zapcore.NewConsoleEncoder(fileEncoderConfig) @@ -51,21 +54,30 @@ func InitCronLog() { return lvl >= zapcore.FatalLevel }) - // 获取 error日志文件的io.Writer 抽象 getWriter() 在下方实现 path := GetConfigString("log.path") errorFile := GetConfigString("log.errorFile") - errorWriter := getWriter(path + "/cron-" + errorFile) // 最后创建具体的Logger - fatalWriter := getWriter(path + "/cron-" + errorFile) infoFile := GetConfigString("log.infoFile") - infoWriter := getWriter(path + "/cron-" + infoFile) + errorWriter := cronWriter(path + "/cron-" + errorFile) + fatalWriter := cronWriter(path + "/cron-" + errorFile) + infoWriter := cronWriter(path + "/cron-" + infoFile) core := zapcore.NewTee( - zapcore.NewCore(encoder, zapcore.AddSync(os.Stdout), infoLevel), //打印到控制台 + zapcore.NewCore(encoder, zapcore.AddSync(os.Stdout), infoLevel), zapcore.NewCore(fileEncoder, fatalWriter, fatalLevel), zapcore.NewCore(fileEncoder, errorWriter, errorLevel), zapcore.NewCore(fileEncoder, infoWriter, infoLevel), ) - log := zap.New(core, zap.AddCaller()) // 需要传入 zap.AddCaller() 才会显示打日志点的文件名和行数, 有点小坑 - CronLogger = log.Sugar() + CronLogger = zap.New(core, zap.AddCaller()).Sugar() +} + +func cronWriter(filename string) zapcore.WriteSyncer { + return zapcore.AddSync(&lumberjack.Logger{ + Filename: filename, + MaxSize: GetConfigInt("log.maxSize"), + MaxBackups: GetConfigInt("log.maxBackups"), + MaxAge: GetConfigInt("log.maxAge"), + Compress: false, + LocalTime: true, + }) } diff --git a/utils/log.go b/utils/log.go deleted file mode 100644 index da83e9b..0000000 --- a/utils/log.go +++ /dev/null @@ -1,94 +0,0 @@ -package utils - -import ( - "os" - "time" - - "go.uber.org/zap" - "go.uber.org/zap/zapcore" - "gopkg.in/natefinch/lumberjack.v2" -) - -var Logger *zap.SugaredLogger - -func InitLog() { - // 设置控制台编码器(带颜色) - consoleEncoder := zapcore.NewConsoleEncoder(zapcore.EncoderConfig{ - MessageKey: "msg", - LevelKey: "level", - EncodeLevel: zapcore.LowercaseColorLevelEncoder, //设置颜色 - TimeKey: "ts", - EncodeTime: func(t time.Time, enc zapcore.PrimitiveArrayEncoder) { - enc.AppendString(t.Format("2006-01-02 15:04:05")) - }, - CallerKey: "file", - EncodeCaller: zapcore.ShortCallerEncoder, - EncodeDuration: func(d time.Duration, enc zapcore.PrimitiveArrayEncoder) { - enc.AppendInt64(int64(d) / 1000000) - }, - }) - - // 设置文件编码器(无颜色) - fileEncoder := zapcore.NewConsoleEncoder(zapcore.EncoderConfig{ - MessageKey: "msg", - LevelKey: "level", - EncodeLevel: zapcore.LowercaseLevelEncoder, // 无颜色 - TimeKey: "ts", - EncodeTime: func(t time.Time, enc zapcore.PrimitiveArrayEncoder) { - enc.AppendString(t.Format("2006-01-02 15:04:05")) - }, - CallerKey: "file", - EncodeCaller: zapcore.ShortCallerEncoder, - EncodeDuration: func(d time.Duration, enc zapcore.PrimitiveArrayEncoder) { - enc.AppendInt64(int64(d) / 1000000) - }, - }) - - // 实现日志等级的interface - infoLevel := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { - return lvl == zapcore.InfoLevel - }) - errorLevel := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { - return lvl == zapcore.ErrorLevel - }) - fatalLevel := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { - return lvl == zapcore.FatalLevel - }) - - // 获取日志文件的io.Writer 抽象 - path := GetConfigString("log.path") - infoFile := GetConfigString("log.infoFile") - errorFile := GetConfigString("log.errorFile") - fatalFile := GetConfigString("log.fatalFile") - - // 生成带日期的文件名 - today := time.Now().Format("2006-01-02") - infoWriter := getWriter(path + "/" + today + "-" + infoFile) - errorWriter := getWriter(path + "/" + today + "-" + errorFile) - fatalWriter := getWriter(path + "/" + today + "-" + fatalFile) - // 创建核心 - core := zapcore.NewTee( - zapcore.NewCore(consoleEncoder, zapcore.AddSync(os.Stdout), zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { - return lvl >= zapcore.InfoLevel - })), // 控制台打印所有级别,带颜色 - zapcore.NewCore(fileEncoder, infoWriter, infoLevel), // info 只写入 info 文件 - zapcore.NewCore(fileEncoder, errorWriter, errorLevel), // error 只写入 error 文件 - zapcore.NewCore(fileEncoder, fatalWriter, fatalLevel), // fatal 只写入 fatal 文件 - ) - log := zap.New(core, zap.AddCaller()) // 需要传入 zap.AddCaller() 才会显示打日志点的文件名和行数 - - Logger = log.Sugar() -} - -func getWriter(filename string) zapcore.WriteSyncer { - - lumberJackLogger := &lumberjack.Logger{ - Filename: filename, - MaxSize: GetConfigInt("log.maxSize"), - MaxBackups: GetConfigInt("log.maxBackups"), - MaxAge: GetConfigInt("log.maxAge"), - Compress: false, - LocalTime: true, - } - return zapcore.AddSync(lumberJackLogger) -} diff --git a/utils/phpValue.go b/utils/phpValue.go deleted file mode 100644 index acd2bdc..0000000 --- a/utils/phpValue.go +++ /dev/null @@ -1,317 +0,0 @@ -package utils - -import ( - "fmt" - "strconv" - "strings" -) - -// PHPValue 定义 PHP 序列化值的类型(兼容字符串、整数、数组) -type PHPValue interface{} - -// PHPDecode 原生解析 PHP 序列化字符串为 map[string]PHPValue -func PHPDecode(serialized string) (map[string]PHPValue, error) { - // 预处理:去除首尾空白 - serialized = strings.TrimSpace(serialized) - - // 验证是否为 PHP 数组格式(a:N:{...}) - if !strings.HasPrefix(serialized, "a:") { - return nil, fmt.Errorf("不是 PHP 数组格式") - } - - // 提取数组内容(去掉外层 a:N:{ 和 末尾的 }) - colonIdx := strings.Index(serialized, ":") - if colonIdx == -1 { - return nil, fmt.Errorf("格式错误:缺少冒号") - } - braceOpenIdx := strings.Index(serialized, "{") - if braceOpenIdx == -1 { - return nil, fmt.Errorf("格式错误:缺少左大括号") - } - content := serialized[braceOpenIdx+1 : len(serialized)-1] - - // 递归解析数组内容 - result := make(map[string]PHPValue) - idx := 0 - contentLen := len(content) - - for idx < contentLen { - // 跳过空白字符 - for idx < contentLen && (content[idx] == ' ' || content[idx] == '\t' || content[idx] == '\n') { - idx++ - } - if idx >= contentLen { - break - } - - // 解析 Key - key, keyEndIdx, err := parsePHPValueAt(content, idx) - if err != nil { - return nil, fmt.Errorf("解析 Key 失败:%v", err) - } - idx = keyEndIdx + 1 // 跳过 Key 后的分号 - - // 解析 Value - val, valEndIdx, err := parsePHPValueAt(content, idx) - if err != nil { - return nil, fmt.Errorf("解析 Value 失败:%v", err) - } - idx = valEndIdx + 1 // 跳过 Value 后的分号 - - // 将 Key 转为字符串并存入结果 - var keyStr string - switch k := key.(type) { - case string: - keyStr = k - case int: - keyStr = strconv.Itoa(k) - default: - keyStr = fmt.Sprintf("%v", k) - } - result[keyStr] = val - } - - return result, nil -} - -// parsePHPValueAt 从指定索引解析单个 PHP 序列化值 -func parsePHPValueAt(s string, start int) (PHPValue, int, error) { - if start >= len(s) { - return nil, -1, fmt.Errorf("索引越界") - } - - // 根据类型前缀解析 - switch s[start] { - case 's': // 字符串类型:s:长度:"内容"; - return parsePHPString(s, start) - case 'i': // 整数类型:i:数值; - return parsePHPInt(s, start) - case 'a': // 数组类型:a:长度:{...}; - return parsePHPArray(s, start) - default: - return nil, -1, fmt.Errorf("不支持的类型:%c", s[start]) - } -} - -// parsePHPString 解析 PHP 字符串类型(s:len:"value") -func parsePHPString(s string, start int) (string, int, error) { - // 格式:s:3:"abc"; - // 1. 跳过 "s:" - if len(s) < start+2 || s[start:start+2] != "s:" { - return "", -1, fmt.Errorf("不是字符串类型") - } - idx := start + 2 - - // 2. 解析长度 - lenStr := "" - for idx < len(s) && s[idx] != ':' { - lenStr += string(s[idx]) - idx++ - } - if idx >= len(s) || s[idx] != ':' { - return "", -1, fmt.Errorf("字符串长度解析失败") - } - strLen, err := strconv.Atoi(lenStr) - if err != nil { - return "", -1, fmt.Errorf("长度不是数字:%v", err) - } - idx++ // 跳过冒号 - - // 3. 跳过引号 - if idx >= len(s) || s[idx] != '"' { - return "", -1, fmt.Errorf("缺少字符串起始引号") - } - idx++ - - // 4. 提取字符串内容 - endIdx := idx + strLen - if endIdx > len(s) { - return "", -1, fmt.Errorf("字符串长度不足") - } - strVal := s[idx:endIdx] - idx = endIdx - - // 5. 跳过结束引号和分号 - if idx >= len(s) || s[idx] != '"' { - return "", -1, fmt.Errorf("缺少字符串结束引号") - } - idx++ - if idx >= len(s) || s[idx] != ';' { - return "", -1, fmt.Errorf("缺少字符串结束分号") - } - - return strVal, idx, nil -} - -// parsePHPInt 解析 PHP 整数类型(i:123;) -func parsePHPInt(s string, start int) (int, int, error) { - // 格式:i:123; - // 1. 跳过 "i:" - if len(s) < start+2 || s[start:start+2] != "i:" { - return 0, -1, fmt.Errorf("不是整数类型") - } - idx := start + 2 - - // 2. 解析数字 - numStr := "" - for idx < len(s) && s[idx] != ';' { - numStr += string(s[idx]) - idx++ - } - if numStr == "" { - return 0, -1, fmt.Errorf("整数为空") - } - numVal, err := strconv.Atoi(numStr) - if err != nil { - return 0, -1, fmt.Errorf("整数解析失败:%v", err) - } - - return numVal, idx, nil -} - -// parsePHPArray 解析 PHP 数组类型(a:len:{...}) -func parsePHPArray(s string, start int) (map[string]PHPValue, int, error) { - // 格式:a:2:{s:1:"k";i:123;s:1:"v";s:3:"abc";}; - // 1. 跳过 "a:" - if len(s) < start+2 || s[start:start+2] != "a:" { - return nil, -1, fmt.Errorf("不是数组类型") - } - idx := start + 2 - - // 2. 解析数组长度(仅校验,实际按内容解析) - for idx < len(s) && s[idx] != ':' { - idx++ - } - if idx >= len(s) || s[idx] != ':' { - return nil, -1, fmt.Errorf("数组长度解析失败") - } - idx++ - - // 3. 找到数组起始大括号 - if idx >= len(s) || s[idx] != '{' { - return nil, -1, fmt.Errorf("缺少数组起始大括号") - } - idx++ - - // 4. 解析数组内容(递归调用 PHPDecode 的核心逻辑) - arrayContentStart := idx - depth := 1 - arrayContentEnd := -1 - - // 找到匹配的结束大括号(处理嵌套数组) - for idx < len(s) && depth > 0 { - switch s[idx] { - case '{': - depth++ - case '}': - depth-- - if depth == 0 { - arrayContentEnd = idx - } - } - idx++ - } - - if arrayContentEnd == -1 { - return nil, -1, fmt.Errorf("数组缺少结束大括号") - } - - // 5. 解析数组内容为 map - arrayContent := s[arrayContentStart:arrayContentEnd] - arrayResult := make(map[string]PHPValue) - innerIdx := 0 - contentLen := len(arrayContent) - - for innerIdx < contentLen { - // 跳过空白 - for innerIdx < contentLen && (arrayContent[innerIdx] == ' ' || arrayContent[innerIdx] == '\t') { - innerIdx++ - } - if innerIdx >= contentLen { - break - } - - // 解析 Key - key, keyEnd, err := parsePHPValueAt(arrayContent, innerIdx) - if err != nil { - return nil, -1, fmt.Errorf("解析数组 Key 失败:%v", err) - } - innerIdx = keyEnd + 1 // 跳过分号 - - // 解析 Value - val, valEnd, err := parsePHPValueAt(arrayContent, innerIdx) - if err != nil { - return nil, -1, fmt.Errorf("解析数组 Value 失败:%v", err) - } - innerIdx = valEnd + 1 // 跳过分号 - - // 转换 Key 为字符串 - var keyStr string - switch k := key.(type) { - case string: - keyStr = k - case int: - keyStr = strconv.Itoa(k) - default: - keyStr = fmt.Sprintf("%v", k) - } - arrayResult[keyStr] = val - } - - // 6. 跳过数组结束后的分号 - if idx < len(s) && s[idx] == ';' { - idx++ - } - - return arrayResult, idx - 1, nil // 返回数组结束的索引 -} - -// GetInt 从 map 中安全获取 int 类型值,失败返回默认值 -func GetInt(m map[string]PHPValue, key string, defaultValue int) int { - val, ok := m[key] - if !ok { - return defaultValue - } - // 类型断言:将 interface{} 转为 int - num, ok := val.(int) - if !ok { - fmt.Printf("警告:%s 不是 int 类型,使用默认值 %d\n", key, defaultValue) - return defaultValue - } - return num -} - -// GetString 从 map 中安全获取 string 类型值,失败返回默认值 -func GetString(m map[string]PHPValue, key string, defaultValue string) string { - val, ok := m[key] - if !ok { - return defaultValue - } - // 类型断言:将 interface{} 转为 string - str, ok := val.(string) - if !ok { - fmt.Printf("警告:%s 不是 string 类型,使用默认值 %s\n", key, defaultValue) - return defaultValue - } - return str -} - -// GetMap 从 map 中安全获取嵌套 map 类型值,失败返回空 map -func GetMap(m map[string]PHPValue, key string) map[string]interface{} { - val, ok := m[key] - if !ok { - return make(map[string]interface{}) - } - // 类型断言:将 interface{} 转为 map[string]interface{} - nestedMap, ok := val.(map[string]PHPValue) - if !ok { - fmt.Printf("警告:%s 不是数组类型\n", key) - return make(map[string]interface{}) - } - // 转换为通用 map[string]interface{} - result := make(map[string]interface{}) - for k, v := range nestedMap { - result[k] = v - } - return result -} diff --git a/utils/redis.go b/utils/redis.go deleted file mode 100644 index ba9b21c..0000000 --- a/utils/redis.go +++ /dev/null @@ -1,30 +0,0 @@ -package utils - -import ( - "base-service/pkg/queue" - "fmt" - - "github.com/go-redis/redis" -) - -var Redis *redis.Client - -const RedisNil = "redis: nil" - -func InitRedis() { - password := GetConfigString("redis.password") - host := GetConfigString("redis.host") - port := GetConfigInt("redis.port") - defaultDb := GetConfigInt("redis.default_db") - Redis = redis.NewClient(&redis.Options{ - Addr: fmt.Sprintf("%s:%d", host, port), - Password: password, // no password set - DB: defaultDb, // use default DB - }) - _, err := Redis.Ping().Result() - if err != nil { - Logger.Fatal("clint redis error ", err) - } - - queue.RegisterRedis(Redis) -}