diff --git a/code/go-project/base-farmework/build/.gitkeep b/code/go-project/base-farmework/build/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/code/go-project/base-farmework/cmd/app/main.go b/code/go-project/base-farmework/cmd/app/main.go index 4cd0658a..6965727f 100644 --- a/code/go-project/base-farmework/cmd/app/main.go +++ b/code/go-project/base-farmework/cmd/app/main.go @@ -3,6 +3,8 @@ package main import ( "base-framework/internal/app" "base-framework/pkg/config" + "base-framework/pkg/logx" + "base-framework/pkg/utils/snowflake" "log" "net/http" "strconv" @@ -19,7 +21,14 @@ func main() { log.Fatalf("加载数据库配置失败: %v", err) return } + // 初始化日志 + logx.InitLogger() + logx.Logger.Info("项目启动成功...", logx.Uint64("id", snowflake.NextID())) + logx.Logger.Info("项目启动成功2...") + logx.Logger.Error("测试错误") + + // 初始化路由 r := app.InitAppRouter() err = http.ListenAndServe(":"+strconv.Itoa(config.Server.Port), r) @@ -27,34 +36,3 @@ func main() { log.Fatalf("server start failed: %v", err) } } - -// -//// 连接字符串格式 -//connStr := "host=117.72.182.135 port=5432 user=postgres password=zhang520.. dbname=test sslmode=disable" -// -//db, err := sql.Open("postgres", connStr) -//if err != nil { -// log.Fatal("打开数据库失败:", err) -//} -//defer db.Close() -// -//// 设置连接池参数(可选) -//db.SetMaxOpenConns(20) -//db.SetMaxIdleConns(5) -//db.SetConnMaxLifetime(0) -// -//// 测试连接 -//err = db.Ping() -//if err != nil { -// log.Fatal("数据库连接失败:", err) -//} -// -//fmt.Println("数据库连接成功!") -// -//// 查询示例 -//var version string -//err = db.QueryRow("SELECT version()").Scan(&version) -//if err != nil { -// log.Fatal(err) -//} -//fmt.Println("PostgreSQL version:", version) diff --git a/code/go-project/base-farmework/configs/app/application.yaml b/code/go-project/base-farmework/configs/app/application.yaml index a31a4afa..91710029 100644 --- a/code/go-project/base-farmework/configs/app/application.yaml +++ b/code/go-project/base-farmework/configs/app/application.yaml @@ -1,5 +1,20 @@ server: port: 8082 + jwt: secret: 3Bde3BGEbYqtqyEUzW3ry8jKFcaPH17fRmTmqE7MDr05Lwj95uruRKrrkb44TJ4s expiry: 168h + +log: + dir: "logs" # 日志根目录 + level: "debug" # 全局最低日志级别 + format: "console" # 输出格式,可选 json 或 console + daily: true # 是否按天滚动 + maxDays: 30 # 保留天数,0表示无限 + caller: true # 是否记录调用文件行号 + levelDirs: false # 是否按级别目录 + console: true # 是否打印到控制台 + +snowflake: + datacenterID: 0 + workerID: 0 diff --git a/code/go-project/base-farmework/go.mod b/code/go-project/base-farmework/go.mod index 95cd8aa6..86fd4699 100644 --- a/code/go-project/base-farmework/go.mod +++ b/code/go-project/base-farmework/go.mod @@ -19,7 +19,8 @@ require ( github.com/spf13/pflag v1.0.6 // indirect github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/atomic v1.9.0 // indirect - go.uber.org/multierr v1.9.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect golang.org/x/sys v0.29.0 // indirect golang.org/x/text v0.21.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/code/go-project/base-farmework/go.sum b/code/go-project/base-farmework/go.sum index c109c17d..0887e0ff 100644 --- a/code/go-project/base-farmework/go.sum +++ b/code/go-project/base-farmework/go.sum @@ -45,6 +45,10 @@ go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= diff --git a/code/go-project/base-farmework/logs/error-2025-08-15.log b/code/go-project/base-farmework/logs/error-2025-08-15.log new file mode 100644 index 00000000..47592e38 --- /dev/null +++ b/code/go-project/base-farmework/logs/error-2025-08-15.log @@ -0,0 +1,32 @@ +{"level":"ERROR","ts":"2025-08-15 14:58:22","caller":"app/main.go:29","msg":"测试错误..."} +2025-08-15 15:01:57 ERROR app/main.go:29 测试错误... +ERROR app/main.go:29 测试错误... +2025-08-15 15:21:51.058 ERROR app/main.go:29 测试错误... +ERROR app/main.go:29 测试错误... +ERROR app/main.go:29 测试错误... +ERROR app/main.go:29 测试错误... +2025-08-15 15:26:50 ERROR app/main.go:29 测试错误... +2025-08-15 15:27:34 ERROR app/main.go:29 测试错误... +2025-08-15 15:31:13.135 ERROR app/main.go:29 测试错误... +2025-08-15 15:34:43.374 ERROR 15832 --- app/main.go:29 测试错误... +2025-08-15 15:35:09.214 ERROR 3820 --- app/main.go:29 测试错误... +2025-08-15 15:44:29.342 ERROR 5068 --- app/main.go:29 : 测试错误... +2025-08-15 15:50:13.811 ERROR 10980 --- app/main.go:29 : 测试错误... +2025-08-15 15:50:21.047 ERROR 14588 --- app/main.go:29 : 测试错误... +2025-08-15 15:50:44.946 ERROR 4664 --- app/main.go:29 : 测试错误... +2025-08-15 15:50:53.440 ERROR 16332 --- app/main.go:29 : 测试错误... +2025-08-15 16:00:58.286 ERROR 5928 --- app/main.go:29 : 测试错误... +2025-08-15 16:04:36.926 ERROR 1616 --- app/main.go:29: 测试错误... +2025-08-15 16:15:11.023 ERROR 3956 --- app/main.go:29: 测试错误 +{"level":"ERROR","ts":"2025-08-15 16:15:30.845","caller":"app/main.go:29","msg":"测试错误"} +{"level":"ERROR","ts":"2025-08-15 17:16:36.736","caller":"app/main.go:29","msg":"测试错误"} +{"level":"ERROR","ts":"2025-08-15 17:22:15.508","caller":"app/main.go:29","msg":"测试错误"} +{"level":"ERROR","ts":"2025-08-15 17:22:27.021","caller":"app/main.go:29","msg":"测试错误"} +{"level":"ERROR","ts":"2025-08-15 17:22:46.606","caller":"app/main.go:29","msg":"测试错误"} +{"level":"ERROR","ts":"2025-08-15 17:22:54.954","caller":"app/main.go:29","msg":"测试错误"} +{"level":"ERROR","ts":"2025-08-15 17:24:12.726","caller":"app/main.go:29","msg":"测试错误"} +{"level":"ERROR","ts":"2025-08-15 17:24:34.956","caller":"app/main.go:29","msg":"测试错误"} +2025-08-15 17:24:58.277 ERROR 14832 app/main.go:29 测试错误 +2025-08-15 17:26:22.912 ERROR 6312 --- app/main.go:29 测试错误 +2025-08-15 17:26:33.441 ERROR 1184 --- app/main.go:29 测试错误 +2025-08-15 17:26:41.369 ERROR 7592 --- app/main.go:29 测试错误 diff --git a/code/go-project/base-farmework/logs/info-2025-08-15.log b/code/go-project/base-farmework/logs/info-2025-08-15.log new file mode 100644 index 00000000..b95f3db5 --- /dev/null +++ b/code/go-project/base-farmework/logs/info-2025-08-15.log @@ -0,0 +1,64 @@ +{"level":"INFO","ts":"2025-08-15 14:58:22","caller":"app/main.go:26","msg":"项目启动成功...","id":346910475188961280} +{"level":"INFO","ts":"2025-08-15 14:58:22","caller":"app/main.go:27","msg":"项目启动成功2..."} +2025-08-15 15:01:57 INFO app/main.go:26 项目启动成功... {"id": 346911374758117376} +2025-08-15 15:01:57 INFO app/main.go:27 项目启动成功2... +INFO app/main.go:26 项目启动成功... {"id": 346914331130068992} +INFO app/main.go:27 项目启动成功2... +2025-08-15 15:21:51.058 INFO app/main.go:26 项目启动成功... {"id": 346916382815813632} +2025-08-15 15:21:51.058 INFO app/main.go:27 项目启动成功2... +INFO app/main.go:26 项目启动成功... {"id": 346917491865288704} +INFO app/main.go:27 项目启动成功2... +INFO app/main.go:26 项目启动成功... {"id": 346917578448306176} +INFO app/main.go:27 项目启动成功2... +INFO app/main.go:26 项目启动成功... {"id": 346917609146417152} +INFO app/main.go:27 项目启动成功2... +2025-08-15 15:26:50 INFO app/main.go:26 项目启动成功... {"id": 346917637277614080} +2025-08-15 15:26:50 INFO app/main.go:27 项目启动成功2... +2025-08-15 15:27:34 INFO app/main.go:26 项目启动成功... {"id": 346917823945113600} +2025-08-15 15:27:34 INFO app/main.go:27 项目启动成功2... +2025-08-15 15:31:13.135 INFO app/main.go:26 项目启动成功... {"id": 346918740337623040} +2025-08-15 15:31:13.135 INFO app/main.go:27 项目启动成功2... +2025-08-15 15:34:43.373 INFO 15832 --- app/main.go:26 项目启动成功... {"id": 346919622139707392} +2025-08-15 15:34:43.374 INFO 15832 --- app/main.go:27 项目启动成功2... +2025-08-15 15:35:09.214 INFO 3820 --- app/main.go:26 项目启动成功... {"id": 346919730524717056} +2025-08-15 15:35:09.214 INFO 3820 --- app/main.go:27 项目启动成功2... +2025-08-15 15:44:29.342 INFO 5068 --- app/main.go:26 : 项目启动成功... {"id": 346922079871827968} +2025-08-15 15:44:29.342 INFO 5068 --- app/main.go:27 : 项目启动成功2... +2025-08-15 15:50:13.811 INFO 10980 --- app/main.go:26 : 项目启动成功... {"id": 346923524679532544} +2025-08-15 15:50:13.811 INFO 10980 --- app/main.go:27 : 项目启动成功2... +2025-08-15 15:50:21.046 INFO 14588 --- app/main.go:26 : 项目启动成功... {"id": 346923555025321984} +2025-08-15 15:50:21.047 INFO 14588 --- app/main.go:27 : 项目启动成功2... +2025-08-15 15:50:44.946 INFO 4664 --- app/main.go:26 : 项目启动成功... {"id": 346923655269187584} +2025-08-15 15:50:44.946 INFO 4664 --- app/main.go:27 : 项目启动成功2... +2025-08-15 15:50:53.440 INFO 16332 --- app/main.go:26 : 项目启动成功... {"id": 346923690895605760} +2025-08-15 15:50:53.440 INFO 16332 --- app/main.go:27 : 项目启动成功2... +2025-08-15 16:00:58.286 INFO 5928 --- app/main.go:26 : 项目启动成功... {"id": 346926227803602944} +2025-08-15 16:00:58.286 INFO 5928 --- app/main.go:27 : 项目启动成功2... +2025-08-15 16:04:36.925 INFO 1616 --- app/main.go:26: 项目启动成功... {"id": 346927144842035200} +2025-08-15 16:04:36.926 INFO 1616 --- app/main.go:27: 项目启动成功2... +2025-08-15 16:15:11.023 INFO 3956 --- app/main.go:26: 项目启动成功... {"id": 346929804441812992} +2025-08-15 16:15:11.023 INFO 3956 --- app/main.go:27: 项目启动成功2... +{"level":"INFO","ts":"2025-08-15 16:15:30.845","caller":"app/main.go:26","msg":"项目启动成功...","id":346929887581306880} +{"level":"INFO","ts":"2025-08-15 16:15:30.845","caller":"app/main.go:27","msg":"项目启动成功2..."} +{"level":"INFO","ts":"2025-08-15 17:16:36.735","caller":"app/main.go:26","msg":"项目启动成功...","id":346945263438397440} +{"level":"INFO","ts":"2025-08-15 17:16:36.735","caller":"app/main.go:27","msg":"项目启动成功2..."} +{"level":"INFO","ts":"2025-08-15 17:22:15.508","caller":"app/main.go:26","msg":"项目启动成功...","id":346946684351152128} +{"level":"INFO","ts":"2025-08-15 17:22:15.508","caller":"app/main.go:27","msg":"项目启动成功2..."} +{"level":"INFO","ts":"2025-08-15 17:22:27.021","caller":"app/main.go:26","msg":"项目启动成功...","id":346946732644368384} +{"level":"INFO","ts":"2025-08-15 17:22:27.021","caller":"app/main.go:27","msg":"项目启动成功2..."} +{"level":"INFO","ts":"2025-08-15 17:22:46.606","caller":"app/main.go:26","msg":"项目启动成功...","id":346946814789812224} +{"level":"INFO","ts":"2025-08-15 17:22:46.606","caller":"app/main.go:27","msg":"项目启动成功2..."} +{"level":"INFO","ts":"2025-08-15 17:22:54.953","caller":"app/main.go:26","msg":"项目启动成功...","id":346946849799667712} +{"level":"INFO","ts":"2025-08-15 17:22:54.954","caller":"app/main.go:27","msg":"项目启动成功2..."} +{"level":"INFO","ts":"2025-08-15 17:24:12.726","caller":"app/main.go:26","msg":"项目启动成功...","id":346947176003272704} +{"level":"INFO","ts":"2025-08-15 17:24:12.726","caller":"app/main.go:27","msg":"项目启动成功2..."} +{"level":"INFO","ts":"2025-08-15 17:24:34.956","caller":"app/main.go:26","msg":"项目启动成功...","id":346947269242650624} +{"level":"INFO","ts":"2025-08-15 17:24:34.956","caller":"app/main.go:27","msg":"项目启动成功2..."} +2025-08-15 17:24:58.277 INFO 14832 app/main.go:26 项目启动成功... {"id": 346947367058014208} +2025-08-15 17:24:58.277 INFO 14832 app/main.go:27 项目启动成功2... +2025-08-15 17:26:22.912 INFO 6312 --- app/main.go:26 项目启动成功... {"id": 346947722042933248} +2025-08-15 17:26:22.912 INFO 6312 --- app/main.go:27 项目启动成功2... +2025-08-15 17:26:33.441 INFO 1184 --- app/main.go:26 项目启动成功... {"id": 346947766204760064} +2025-08-15 17:26:33.441 INFO 1184 --- app/main.go:27 项目启动成功2... +2025-08-15 17:26:41.368 INFO 7592 --- app/main.go:26 项目启动成功... {"id": 346947799453007872} +2025-08-15 17:26:41.369 INFO 7592 --- app/main.go:27 项目启动成功2... diff --git a/code/go-project/base-farmework/logs/warn-2025-08-15.log b/code/go-project/base-farmework/logs/warn-2025-08-15.log new file mode 100644 index 00000000..e69de29b diff --git a/code/go-project/base-farmework/pkg/config/application.go b/code/go-project/base-farmework/pkg/config/application.go index 02c22c10..31b13c28 100644 --- a/code/go-project/base-farmework/pkg/config/application.go +++ b/code/go-project/base-farmework/pkg/config/application.go @@ -1,48 +1,56 @@ package config import ( + "base-framework/pkg/types" "github.com/spf13/viper" - "time" ) -// server 服务器配置结构体(包内私有) -type server struct { - Port int -} - -// jwt 配置结构体(包内私有) -type jwt struct { - Secret string - Expiry time.Duration -} - -// 全局导出变量,指向私有结构体实例 +// 全局导出变量,指向 types 包中的配置实例 var ( - Server *server - JWT *jwt + Server *types.Server + JWT *types.Jwt + Log *types.Log + Snowflake *types.Snowflake ) // InitApplicationConfig 初始化配置,传入配置文件路径 -func InitApplicationConfig(configPath string) (error, error) { +func InitApplicationConfig(configPath string) (err error, err2 error) { v := viper.New() v.SetConfigFile(configPath) v.SetConfigType("yaml") if err := v.ReadInConfig(); err != nil { - return err, nil + return err, err2 } - var s server - if err := v.UnmarshalKey("server", &s); err != nil { - return err, nil + // server 配置 + var server types.Server + if err := v.UnmarshalKey("server", &server); err != nil { + return err, err2 } - Server = &s - var j jwt - if err := v.UnmarshalKey("jwt", &j); err != nil { - return err, nil + // jwt 配置 + var jwt types.Jwt + if err := v.UnmarshalKey("jwt", &jwt); err != nil { + return err, err2 } - JWT = &j - return nil, nil + // 初始化日志模块 + var log types.Log + if err := v.UnmarshalKey("log", &log); err != nil { + return err, err2 + } + + // 初始化日志模块 + var snowflake types.Snowflake + if err := v.UnmarshalKey("snowflake", &snowflake); err != nil { + return err, err2 + } + + Server = &server + JWT = &jwt + Log = &log + Snowflake = &snowflake + + return nil, err2 } diff --git a/code/go-project/base-farmework/pkg/config/database.go b/code/go-project/base-farmework/pkg/config/database.go index 35028cdb..6088815e 100644 --- a/code/go-project/base-farmework/pkg/config/database.go +++ b/code/go-project/base-farmework/pkg/config/database.go @@ -1,6 +1,7 @@ package config import ( + "base-framework/pkg/types" "database/sql" "fmt" "log" @@ -13,18 +14,6 @@ import ( _ "github.com/lib/pq" // PostgreSQL 驱动,根据实际数据库替换 ) -// DBConfig 数据库配置结构体 -type DBConfig struct { - Host string - Port int - Username string - Password string - Dbname string - MaxOpenConns int // 最大打开连接数 - MaxIdleConns int // 最大空闲连接数 - ConnMaxLifetime time.Duration // 连接最大生命周期 -} - // 默认连接池参数 const ( defaultMaxOpenConns = 10 @@ -33,7 +22,7 @@ const ( ) var ( - dbConfigs map[string]DBConfig // 配置副本 + dbConfigs map[string]types.DBConfig // 配置副本 dataSources = make(map[string]*sql.DB) // 连接池 configsDSN = make(map[string]string) // key -> dsn mu sync.RWMutex @@ -83,7 +72,7 @@ func unmarshalConfigs() error { mu.Lock() defer mu.Unlock() - temp := make(map[string]DBConfig) + temp := make(map[string]types.DBConfig) if err := v.Unmarshal(&temp); err != nil { return fmt.Errorf("解析数据库配置失败: %w", err) } @@ -92,11 +81,11 @@ func unmarshalConfigs() error { } // GetDBConfigs 并发安全地返回当前数据库配置副本 -func GetDBConfigs() map[string]DBConfig { +func GetDBConfigs() map[string]types.DBConfig { mu.RLock() defer mu.RUnlock() - res := make(map[string]DBConfig, len(dbConfigs)) + res := make(map[string]types.DBConfig, len(dbConfigs)) for k, v := range dbConfigs { res[k] = v } @@ -104,13 +93,13 @@ func GetDBConfigs() map[string]DBConfig { } // buildDSN 构建 PostgreSQL DSN -func buildDSN(cfg DBConfig) string { +func buildDSN(cfg types.DBConfig) string { return fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", cfg.Host, cfg.Port, cfg.Username, cfg.Password, cfg.Dbname) } // ReloadDataSources 支持增量更新数据源,并验证数据库连接 -func ReloadDataSources(newConfigs map[string]DBConfig) error { +func ReloadDataSources(newConfigs map[string]types.DBConfig) error { mu.Lock() defer mu.Unlock() @@ -171,7 +160,7 @@ func ReloadDataSources(newConfigs map[string]DBConfig) error { } // applyDBConfig 设置 sql.DB 连接池参数,并应用默认值 -func applyDBConfig(db *sql.DB, cfg DBConfig) { +func applyDBConfig(db *sql.DB, cfg types.DBConfig) { maxOpen := cfg.MaxOpenConns if maxOpen <= 0 { maxOpen = defaultMaxOpenConns diff --git a/code/go-project/base-farmework/pkg/logx/field.go b/code/go-project/base-farmework/pkg/logx/field.go new file mode 100644 index 00000000..67d6995e --- /dev/null +++ b/code/go-project/base-farmework/pkg/logx/field.go @@ -0,0 +1,87 @@ +package logx + +import ( + "time" + + "go.uber.org/zap" +) + +// String 类型字段 +func String(key, val string) zap.Field { return zap.String(key, val) } +func Stringp(key string, val *string) zap.Field { return zap.Stringp(key, val) } +func Strings(key string, val []string) zap.Field { return zap.Strings(key, val) } + +func Bool(key string, val bool) zap.Field { return zap.Bool(key, val) } +func Boolp(key string, val *bool) zap.Field { return zap.Boolp(key, val) } +func Bools(key string, val []bool) zap.Field { return zap.Bools(key, val) } + +func Int(key string, val int) zap.Field { return zap.Int(key, val) } +func Intp(key string, val *int) zap.Field { return zap.Intp(key, val) } +func Ints(key string, val []int) zap.Field { return zap.Ints(key, val) } + +func Int64(key string, val int64) zap.Field { return zap.Int64(key, val) } +func Int64p(key string, val *int64) zap.Field { return zap.Int64p(key, val) } +func Int64s(key string, val []int64) zap.Field { return zap.Int64s(key, val) } + +func Int32(key string, val int32) zap.Field { return zap.Int32(key, val) } +func Int32p(key string, val *int32) zap.Field { return zap.Int32p(key, val) } +func Int32s(key string, val []int32) zap.Field { return zap.Int32s(key, val) } + +func Int16(key string, val int16) zap.Field { return zap.Int16(key, val) } +func Int16p(key string, val *int16) zap.Field { return zap.Int16p(key, val) } +func Int16s(key string, val []int16) zap.Field { return zap.Int16s(key, val) } + +func Int8(key string, val int8) zap.Field { return zap.Int8(key, val) } +func Int8p(key string, val *int8) zap.Field { return zap.Int8p(key, val) } +func Int8s(key string, val []int8) zap.Field { return zap.Int8s(key, val) } + +func Uint(key string, val uint) zap.Field { return zap.Uint(key, val) } +func Uintp(key string, val *uint) zap.Field { return zap.Uintp(key, val) } +func Uints(key string, val []uint) zap.Field { return zap.Uints(key, val) } + +func Uint64(key string, val uint64) zap.Field { return zap.Uint64(key, val) } +func Uint64p(key string, val *uint64) zap.Field { return zap.Uint64p(key, val) } +func Uint64s(key string, val []uint64) zap.Field { return zap.Uint64s(key, val) } + +func Uint32(key string, val uint32) zap.Field { return zap.Uint32(key, val) } +func Uint32p(key string, val *uint32) zap.Field { return zap.Uint32p(key, val) } +func Uint32s(key string, val []uint32) zap.Field { return zap.Uint32s(key, val) } + +func Uint16(key string, val uint16) zap.Field { return zap.Uint16(key, val) } +func Uint16p(key string, val *uint16) zap.Field { return zap.Uint16p(key, val) } +func Uint16s(key string, val []uint16) zap.Field { return zap.Uint16s(key, val) } + +func Uint8(key string, val uint8) zap.Field { return zap.Uint8(key, val) } +func Uint8p(key string, val *uint8) zap.Field { return zap.Uint8p(key, val) } + +func Uintptr(key string, val uintptr) zap.Field { return zap.Uintptr(key, val) } +func Uintptrp(key string, val *uintptr) zap.Field { return zap.Uintptrp(key, val) } +func Uintptrs(key string, val []uintptr) zap.Field { return zap.Uintptrs(key, val) } + +func Float32(key string, val float32) zap.Field { return zap.Float32(key, val) } +func Float32p(key string, val *float32) zap.Field { return zap.Float32p(key, val) } +func Float32s(key string, val []float32) zap.Field { return zap.Float32s(key, val) } + +func Float64(key string, val float64) zap.Field { return zap.Float64(key, val) } +func Float64p(key string, val *float64) zap.Field { return zap.Float64p(key, val) } +func Float64s(key string, val []float64) zap.Field { return zap.Float64s(key, val) } + +func Complex64(key string, val complex64) zap.Field { return zap.Complex64(key, val) } +func Complex64p(key string, val *complex64) zap.Field { return zap.Complex64p(key, val) } +func Complex64s(key string, val []complex64) zap.Field { return zap.Complex64s(key, val) } + +func Complex128(key string, val complex128) zap.Field { return zap.Complex128(key, val) } +func Complex128p(key string, val *complex128) zap.Field { return zap.Complex128p(key, val) } +func Complex128s(key string, val []complex128) zap.Field { return zap.Complex128s(key, val) } + +func Binary(key string, val []byte) zap.Field { return zap.Binary(key, val) } + +func Time(key string, val time.Time) zap.Field { return zap.Time(key, val) } +func Timep(key string, val *time.Time) zap.Field { return zap.Timep(key, val) } +func Times(key string, val []time.Time) zap.Field { return zap.Times(key, val) } + +func Duration(key string, val time.Duration) zap.Field { return zap.Duration(key, val) } +func Durationp(key string, val *time.Duration) zap.Field { return zap.Durationp(key, val) } +func Durations(key string, val []time.Duration) zap.Field { return zap.Durations(key, val) } + +func Any(key string, val any) zap.Field { return zap.Any(key, val) } diff --git a/code/go-project/base-farmework/pkg/logx/log.go b/code/go-project/base-farmework/pkg/logx/log.go new file mode 100644 index 00000000..d3e40b51 --- /dev/null +++ b/code/go-project/base-farmework/pkg/logx/log.go @@ -0,0 +1,212 @@ +package logx + +import ( + "fmt" + "os" + "path/filepath" + "sync" + "time" + + "base-framework/pkg/config" + "base-framework/pkg/types" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +var ( + Logger *zap.Logger + + once sync.Once + currentDate string + mu sync.Mutex + + infoFile *os.File + warnFile *os.File + errorFile *os.File + + infoCore zapcore.Core + warnCore zapcore.Core + errorCore zapcore.Core +) + +const logTmFmtWithMS = "2006-01-02 15:04:05.000" + +// 彩色日志级别 +var levelColor = map[zapcore.Level]string{ + zapcore.DebugLevel: "\033[36mDEBUG\033[0m", + zapcore.InfoLevel: "\033[32mINFO\033[0m", + zapcore.WarnLevel: "\033[33mWARN\033[0m", + zapcore.ErrorLevel: "\033[31mERROR\033[0m", + zapcore.DPanicLevel: "\033[35mDPANIC\033[0m", + zapcore.PanicLevel: "\033[35mPANIC\033[0m", + zapcore.FatalLevel: "\033[35mFATAL\033[0m", +} + +// defaultLogConfig 返回默认日志配置 +func defaultLogConfig() *types.Log { + return &types.Log{ + Dir: "logs", + Level: "debug", + Format: "json", + Daily: true, + MaxDays: 30, + Caller: true, + LevelDirs: true, + Console: true, + } +} + +// InitLogger 初始化全局 Logger +func InitLogger() { + once.Do(func() { + if config.Log == nil { + config.Log = defaultLogConfig() + } + createDirIfNotExist(config.Log.Dir) + switchDate(time.Now().Format("2006-01-02")) + }) +} + +// 切换日志文件 +func switchDate(today string) { + currentDate = today + + // 关闭旧文件 + if infoFile != nil { + _ = infoFile.Close() + } + if warnFile != nil { + _ = warnFile.Close() + } + if errorFile != nil { + _ = errorFile.Close() + } + + // 文件路径 + infoPath, warnPath, errorPath := config.Log.Dir, config.Log.Dir, config.Log.Dir + if config.Log.LevelDirs { + infoPath = filepath.Join(config.Log.Dir, "info") + warnPath = filepath.Join(config.Log.Dir, "warn") + errorPath = filepath.Join(config.Log.Dir, "error") + createDirIfNotExist(infoPath) + createDirIfNotExist(warnPath) + createDirIfNotExist(errorPath) + } else { + createDirIfNotExist(config.Log.Dir) + } + + // 打开文件 + infoFile = getOrCreateFile(filepath.Join(infoPath, fmt.Sprintf("info-%s.log", today))) + warnFile = getOrCreateFile(filepath.Join(warnPath, fmt.Sprintf("warn-%s.log", today))) + errorFile = getOrCreateFile(filepath.Join(errorPath, fmt.Sprintf("error-%s.log", today))) + + // 文件编码器 + var fileEncoder zapcore.Encoder + if config.Log.Format == "console" { + fileEncoder = newConsoleEncoder(false) + } else { + fileEncoder = newJSONEncoder() + } + + // 文件核心 + infoCore = zapcore.NewCore(fileEncoder, zapcore.AddSync(infoFile), zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { + return lvl <= zapcore.InfoLevel + })) + warnCore = zapcore.NewCore(fileEncoder, zapcore.AddSync(warnFile), zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { + return lvl == zapcore.WarnLevel + })) + errorCore = zapcore.NewCore(fileEncoder, zapcore.AddSync(errorFile), zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { + return lvl >= zapcore.ErrorLevel + })) + + cores := []zapcore.Core{infoCore, warnCore, errorCore} + + // 开启本地console输出 + if config.Log.Console { + consoleCore := zapcore.NewCore(newConsoleEncoder(true), zapcore.AddSync(os.Stdout), zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { + return lvl >= zapcore.DebugLevel + })) + cores = append(cores, consoleCore) + } + + Logger = zap.New(zapcore.NewTee(cores...)) + if config.Log.Caller { + Logger = Logger.WithOptions(zap.AddCaller()) + } else { + Logger = Logger.WithOptions(zap.AddCallerSkip(1)) + } + + Logger = Logger.WithOptions(zap.Hooks(func(entry zapcore.Entry) error { + if config.Log.Daily { + checkDateSwitch() + } + return nil + })) +} + +func newConsoleEncoder(enableColor bool) zapcore.Encoder { + cfg := zapcore.EncoderConfig{ + TimeKey: "ts", + LevelKey: "level", + CallerKey: "caller", + MessageKey: "msg", + LineEnding: zapcore.DefaultLineEnding, + EncodeTime: func(t time.Time, enc zapcore.PrimitiveArrayEncoder) { + enc.AppendString(t.Format(logTmFmtWithMS)) + }, + EncodeLevel: func(lvl zapcore.Level, enc zapcore.PrimitiveArrayEncoder) { + if enableColor { + color, ok := levelColor[lvl] + if !ok { + color = lvl.CapitalString() + } + enc.AppendString(color) + } else { + enc.AppendString(lvl.CapitalString()) + } + }, + EncodeCaller: func(caller zapcore.EntryCaller, enc zapcore.PrimitiveArrayEncoder) { + pid := os.Getpid() + enc.AppendString(fmt.Sprintf("%d --- %s", pid, caller.TrimmedPath())) + }, + } + + return zapcore.NewConsoleEncoder(cfg) +} + +func newJSONEncoder() zapcore.Encoder { + cfg := zap.NewProductionEncoderConfig() + cfg.TimeKey = "ts" + cfg.EncodeTime = func(t time.Time, enc zapcore.PrimitiveArrayEncoder) { + enc.AppendString(t.Format(logTmFmtWithMS)) + } + cfg.EncodeLevel = zapcore.CapitalLevelEncoder + cfg.EncodeCaller = zapcore.ShortCallerEncoder + return zapcore.NewJSONEncoder(cfg) +} + +func checkDateSwitch() { + today := time.Now().Format("2006-01-02") + if today != currentDate { + mu.Lock() + defer mu.Unlock() + if today != currentDate { + switchDate(today) + } + } +} + +func createDirIfNotExist(dir string) { + if _, err := os.Stat(dir); os.IsNotExist(err) { + _ = os.MkdirAll(dir, 0755) + } +} + +func getOrCreateFile(file string) *os.File { + f, err := os.OpenFile(file, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + panic(err) + } + return f +} diff --git a/code/go-project/base-farmework/pkg/middleware/error.go b/code/go-project/base-farmework/pkg/middleware/error.go index eabbd349..c9e5b4f4 100644 --- a/code/go-project/base-farmework/pkg/middleware/error.go +++ b/code/go-project/base-farmework/pkg/middleware/error.go @@ -2,15 +2,29 @@ package middleware import ( "base-framework/pkg/reponse" - "strings" - "base-framework/pkg/router" + "os" + "path/filepath" + "runtime" + "strings" + "sync" +) + +// 仅初始化一次的项目信息 +var ( + initOnce sync.Once + projectBase string // 例:base-farmework + projectAbsPrefix string // 例:/abs/path/to/base-farmework/ + projectAbsPrefixLower string // 小写版,用于不区分大小写前缀比较(Windows 安全) ) // Error 中间件:捕获 c.Errors() 并返回统一的 ErrorInfo func Error() router.HandlerFunc { + // 注册时预热(只做一次) + initOnce.Do(initProjectInfo) + return func(c *router.Context) { - c.Next() // 执行后续 Handler + c.Next() errs := c.Errors() if len(errs) == 0 { @@ -25,8 +39,8 @@ func Error() router.HandlerFunc { Method: c.Request.Method, Path: c.Request.URL.Path, Message: lastErr.Error(), - Stack: stack, // 完整堆栈 - Location: location, // 业务 Handler 文件行号 + Stack: stack, + Location: location, } reponse.Error(c).ErrorInfo(info).Send() @@ -34,34 +48,111 @@ func Error() router.HandlerFunc { } } -// extractBusinessLocation:获取 AddError 后的业务 Handler 文件行号 +// 仅在注册时执行:确定 go.mod 根目录并缓存前缀 +func initProjectInfo() { + // 以当前源文件为起点向上找 go.mod + _, file, _, _ := runtime.Caller(0) + if root, ok := findGoModRoot(file); ok { + projectBase = filepath.Base(root) + + p := filepath.ToSlash(root) + if !strings.HasSuffix(p, "/") { + p += "/" + } + projectAbsPrefix = p + projectAbsPrefixLower = strings.ToLower(p) + } +} + +// extractBusinessLocation:获取 AddError 后的业务 Handler 文件行(仅返回 /.go:) func extractBusinessLocation(stack string) string { lines := strings.Split(stack, "\n") foundAddError := false countGo := 0 - for _, line := range lines { - line = strings.TrimSpace(line) + for _, raw := range lines { + line := strings.TrimSpace(raw) + if strings.Contains(line, "AddError") { foundAddError = true continue } - if foundAddError && strings.Contains(line, ".go:") { + if !foundAddError { + continue + } + + if strings.Contains(line, ".go:") { countGo++ - if countGo == 2 { // 第2个 .go: 行就是业务 Handler - // 去掉 +0x偏移 - if idx := strings.Index(line, " +"); idx != -1 { - line = line[:idx] + if countGo == 2 { + // 去掉 " +0x..." 偏移 + if i := strings.Index(line, " +"); i != -1 { + line = line[:i] } - // 保留 base-farmework 开头的路径 - slash := strings.Index(line, "base-farmework") - if slash != -1 { - line = line[slash:] - } - return line + return trimToProject(line) + } + } + } + return "" +} + +// 将绝对路径裁剪为 "/.go:" +func trimToProject(goLine string) string { + // 统一斜杠 + line := strings.ReplaceAll(goLine, "\\", "/") + + // 用最后一个冒号分离(避免 Windows 盘符) + idx := strings.LastIndex(line, ":") + if idx == -1 || idx+1 >= len(line) { + return line + } + filePath, lineNo := line[:idx], line[idx+1:] + + // 优先:前缀裁剪(O(1)),比 filepath.Rel 更快 + if projectAbsPrefix != "" { + fp := filepath.ToSlash(filePath) + fpl := strings.ToLower(fp) + if strings.HasPrefix(fpl, projectAbsPrefixLower) { + rel := fp[len(projectAbsPrefix):] + return projectBase + "/" + rel + ":" + lineNo + } + } + + // 其次:internal/cmd/pkg 作为锚点 + segs := strings.Split(filePath, "/") + for i, s := range segs { + if s == "internal" || s == "cmd" || s == "pkg" { + if i > 0 { + return strings.Join(segs[i-1:], "/") + ":" + lineNo } } } - return "" + // 启发式:目录名包含 '-' 或 '_',当作仓库名 + for i, s := range segs { + if strings.ContainsAny(s, "-_") { + return strings.Join(segs[i:], "/") + ":" + lineNo + } + } + + // 兜底:末尾 4 级 + start := len(segs) - 4 + if start < 0 { + start = 0 + } + return strings.Join(segs[start:], "/") + ":" + lineNo +} + +// 从给定文件路径向上查找包含 go.mod 的目录(仅注册时调用一次) +func findGoModRoot(filePath string) (string, bool) { + dir := filepath.Clean(filepath.Dir(filepath.FromSlash(filePath))) + for { + if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { + return dir, true + } + parent := filepath.Dir(dir) + if parent == dir { + return "", false + } + dir = parent + } } diff --git a/code/go-project/base-farmework/pkg/types/config.go b/code/go-project/base-farmework/pkg/types/config.go new file mode 100644 index 00000000..fa4ce059 --- /dev/null +++ b/code/go-project/base-farmework/pkg/types/config.go @@ -0,0 +1,42 @@ +package types + +import "time" + +// Server 服务器配置结构体(包内私有) +type Server struct { + Port int +} + +// Jwt 配置结构体(包内私有) +type Jwt struct { + Secret string + Expiry time.Duration +} + +type Log struct { + Dir string // 日志根目录 + Level string // 全局最低日志级别 + Format string // 输出格式,可选 json 或 console + Daily bool // 是否按天滚动 + MaxDays int // 保留天数,0表示无限 + Caller bool // 是否记录调用文件行号 + LevelDirs bool // 是否按级别目录 + Console bool +} + +// DBConfig 数据库配置结构体 +type DBConfig struct { + Host string + Port int + Username string + Password string + Dbname string + MaxOpenConns int // 最大打开连接数 + MaxIdleConns int // 最大空闲连接数 + ConnMaxLifetime time.Duration // 连接最大生命周期 +} + +type Snowflake struct { + DatacenterID uint64 + WorkerID uint64 +} diff --git a/code/go-project/base-farmework/pkg/utils/snowflake/snowflake.go b/code/go-project/base-farmework/pkg/utils/snowflake/snowflake.go new file mode 100644 index 00000000..42bc91ff --- /dev/null +++ b/code/go-project/base-farmework/pkg/utils/snowflake/snowflake.go @@ -0,0 +1,90 @@ +package snowflake + +import ( + "base-framework/pkg/config" + "sync" + "sync/atomic" + "time" +) + +const ( + datacenterBits = uint64(5) + workerBits = uint64(5) + sequenceBits = uint64(12) + + maxSequence = -1 ^ (-1 << sequenceBits) + workerShift = sequenceBits + datacenterShift = sequenceBits + workerBits + timestampShift = sequenceBits + workerBits + datacenterBits + + epoch = int64(1672531200000) // 2023-01-01 +) + +type Snowflake struct { + lastStamp uint64 // 上一次时间戳 (毫秒) + sequence uint64 // 序列号 + dcID uint64 + workerID uint64 +} + +var ( + sfInstance *Snowflake + once sync.Once +) + +// lazy 初始化实例 +func initInstance() { + var dcID, wID uint64 + if config.Snowflake != nil { + dcID = config.Snowflake.DatacenterID + wID = config.Snowflake.WorkerID + } + sfInstance = &Snowflake{ + dcID: dcID, + workerID: wID, + } +} + +// NextID 生成全局唯一 ID,无锁版本 +func NextID() uint64 { + once.Do(initInstance) + + for { + now := uint64(time.Now().UnixNano() / 1e6) + + last := atomic.LoadUint64(&sfInstance.lastStamp) + seq := atomic.LoadUint64(&sfInstance.sequence) + + if now < last { + // 系统时钟回拨,等待 + now = waitNextMillis(last) + } + + if now == last { + seq = (seq + 1) & uint64(maxSequence) + if seq == 0 { + now = waitNextMillis(last) + } + } else { + seq = 0 + } + + if atomic.CompareAndSwapUint64(&sfInstance.lastStamp, last, now) { + atomic.StoreUint64(&sfInstance.sequence, seq) + id := ((now - uint64(epoch)) << timestampShift) | + (sfInstance.dcID << datacenterShift) | + (sfInstance.workerID << workerShift) | + seq + return id + } + } +} + +// 等待下一个毫秒 +func waitNextMillis(last uint64) uint64 { + now := uint64(time.Now().UnixNano() / 1e6) + for now <= last { + now = uint64(time.Now().UnixNano() / 1e6) + } + return now +}