diff --git a/code/base-project/base-go-v2/cmd/app/main.go b/code/base-project/base-go-v2/cmd/app/main.go index 4879f7a4..43a3412f 100644 --- a/code/base-project/base-go-v2/cmd/app/main.go +++ b/code/base-project/base-go-v2/cmd/app/main.go @@ -1 +1,25 @@ -package app +package main + +import ( + "base-go-v2/internal/config" + "base-go-v2/internal/database" + "base-go-v2/internal/logx" + "log" +) + +func main() { + // 初始化配置 + err := config.InitAppConfig("./configs/app.yaml") + if err != nil { + log.Fatalf("加载配置失败: %v", err) + return + } + // 初始化数据库 + err = database.InitDb() + if err != nil { + log.Fatalf("加载数据库配置失败: %v", err) + return + } + // 初始化日志 + logx.InitLogger() +} diff --git a/code/base-project/base-go-v2/configs/app.yaml b/code/base-project/base-go-v2/configs/app.yaml index 00d895c5..eb10e709 100644 --- a/code/base-project/base-go-v2/configs/app.yaml +++ b/code/base-project/base-go-v2/configs/app.yaml @@ -14,4 +14,14 @@ db: jwt: secret: 3Bde3BGEbYqtqyEUzW3ry8jKFcaPH17fRmTmqE7MDr05Lwj95uruRKrrkb44TJ4s - expiry: 168h \ No newline at end of file + expiry: -1 + +log: + dir: "logs" # 日志根目录 + level: "debug" # 全局最低日志级别 + format: "json" # 输出格式,可选 json 或 console + daily: true # 是否按天滚动 + maxDays: 30 # 保留天数,0表示无限 + caller: true # 是否记录调用文件行号 + levelDirs: false # 是否按级别目录 + console: true # 是否打印到控制台 diff --git a/code/base-project/base-go-v2/go.mod b/code/base-project/base-go-v2/go.mod index 925615ca..3758b50a 100644 --- a/code/base-project/base-go-v2/go.mod +++ b/code/base-project/base-go-v2/go.mod @@ -2,20 +2,23 @@ module base-go-v2 go 1.25.0 +require ( + github.com/golang-jwt/jwt/v5 v5.3.0 + github.com/lib/pq v1.10.9 + github.com/spf13/viper v1.21.0 +) + require ( github.com/andybalholm/brotli v1.2.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect - github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/klauspost/compress v1.18.1 // indirect - github.com/lib/pq v1.10.9 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect - github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/sagikazarmark/locafero v0.12.0 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect - github.com/spf13/viper v1.21.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.68.0 // indirect diff --git a/code/base-project/base-go-v2/go.sum b/code/base-project/base-go-v2/go.sum index 7e50566d..82fd7216 100644 --- a/code/base-project/base-go-v2/go.sum +++ b/code/base-project/base-go-v2/go.sum @@ -14,6 +14,8 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0 github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= +github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= diff --git a/code/base-project/base-go-v2/internal/config/app.go b/code/base-project/base-go-v2/internal/config/app.go index 7017768f..b9b777a0 100644 --- a/code/base-project/base-go-v2/internal/config/app.go +++ b/code/base-project/base-go-v2/internal/config/app.go @@ -2,35 +2,54 @@ package config import ( "base-go-v2/internal/types" - "github.com/spf13/viper" + "fmt" "time" + + "github.com/spf13/viper" ) -// App 全局唯一导出配置实例,初始化默认值 -var App = &types.AppConfig{ - Server: types.AppConfig{}.Server, - JWT: types.AppConfig{}.JWT, - Db: types.AppConfig{}.Db, - Snowflake: types.AppConfig{}.Snowflake, -} +// App 全局唯一配置实例 +var App *types.AppConfig -// 给部分字段设置默认值 +// init 初始化默认值 func init() { - App.Server.Port = 8080 - App.JWT.Secret = "default_secret" - App.JWT.Expiry = time.Hour - - // 数据库只给连接池默认值 - App.Db.MaxOpenConns = 10 - App.Db.MaxIdleConns = 5 - App.Db.ConnMaxLifetime = 30 * time.Minute - - App.Snowflake.DatacenterID = 1 - App.Snowflake.WorkerID = 1 + App = &types.AppConfig{ + Server: types.ServerConfig{ + Port: 8080, + }, + JWT: types.JWTConfig{ + Secret: "default_secret", + Expiry: time.Hour, + }, + Db: types.DbConfig{ + MaxOpenConns: 10, + MaxIdleConns: 5, + ConnMaxLifetime: 30 * time.Minute, + }, + Snowflake: types.SnowflakeConfig{ + DatacenterID: 1, + WorkerID: 1, + }, + Log: types.LogConfig{ + Dir: "logs", + Level: "debug", + Format: "json", + Daily: true, + MaxDays: 30, + Caller: true, + LevelDirs: true, + Console: true, + }, + } } -// InitApplicationConfig 初始化配置 -func InitApplicationConfig(configPath string) error { +// InitAppConfig 初始化配置 +func InitAppConfig(configPath string) error { + if configPath == "" { + fmt.Println("⚠️ 使用默认配置") + return nil + } + v := viper.New() v.SetConfigFile(configPath) v.SetConfigType("yaml") @@ -39,21 +58,23 @@ func InitApplicationConfig(configPath string) error { return err } + // 逐字段解码,缺失字段保留默认值 if err := v.UnmarshalKey("server", &App.Server); err != nil { return err } - if err := v.UnmarshalKey("jwt", &App.JWT); err != nil { return err } - if err := v.UnmarshalKey("snowflake", &App.Snowflake); err != nil { return err } - if err := v.UnmarshalKey("db", &App.Db); err != nil { return err } + if err := v.UnmarshalKey("log", &App.Log); err != nil { + return err + } + fmt.Printf("✅ 初始化 %s 配置成功\n", configPath) return nil } diff --git a/code/base-project/base-go-v2/internal/database/db.go b/code/base-project/base-go-v2/internal/database/db.go index 4ca69719..3c452e2b 100644 --- a/code/base-project/base-go-v2/internal/database/db.go +++ b/code/base-project/base-go-v2/internal/database/db.go @@ -22,7 +22,7 @@ var defaultPoolConfig = struct { } func InitDb() error { - d := config.Db + d := config.App.Db // 构造 DSN dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", @@ -57,6 +57,8 @@ func InitDb() error { return fmt.Errorf("数据库连接测试失败: %w", err) } + fmt.Printf("✅ 数据库连接成功:%s:%d/%s\n", d.Host, d.Port, d.Dbname) + DB = db return nil } diff --git a/code/base-project/base-go-v2/internal/logx/field.go b/code/base-project/base-go-v2/internal/logx/field.go new file mode 100644 index 00000000..f4000214 --- /dev/null +++ b/code/base-project/base-go-v2/internal/logx/field.go @@ -0,0 +1,86 @@ +package logx + +import ( + "time" + + "go.uber.org/zap" +) + +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/base-project/base-go-v2/internal/logx/log.go b/code/base-project/base-go-v2/internal/logx/log.go new file mode 100644 index 00000000..986718de --- /dev/null +++ b/code/base-project/base-go-v2/internal/logx/log.go @@ -0,0 +1,221 @@ +package logx + +import ( + "base-go-v2/internal/config" + "base-go-v2/internal/routinex" + "base-go-v2/internal/utils/strutil" + "base-go-v2/internal/utils/uid" + "fmt" + "os" + "path/filepath" + "sync" + "time" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +var ( + Logger *zap.Logger + TrackID = "trackId" + 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", +} + +// ----------------- trackCore ----------------- +// 在写入日志时自动从 routinex 获取 log_id 并加入字段 +type trackCore struct { + zapcore.Core +} + +func (c *trackCore) With(fields []zapcore.Field) zapcore.Core { + return &trackCore{Core: c.Core.With(fields)} +} + +func (c *trackCore) Check(entry zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry { + if c.Enabled(entry.Level) { + return ce.AddCore(entry, c) + } + return ce +} + +func (c *trackCore) Write(entry zapcore.Entry, fields []zapcore.Field) error { + val := routinex.Get(TrackID) + if val == nil { + val = uid.NextID() + routinex.Set(TrackID, val) // 保证同一请求复用同一 trackID + } + + fields = append(fields, zap.String(TrackID, strutil.ToString(val))) + return c.Core.Write(entry, fields) +} + +// InitLogger ----------------- 初始化日志 ----------------- +func InitLogger() { + once.Do(func() { + createDirIfNotExist(config.App.Log.Dir) + switchDate(time.Now().Format("2006-01-02")) + + fmt.Printf("✅ 日志初始化成功,日志目录:%s\n", config.App.Log.Dir) + }) +} + +// ----------------- 初始化核心 ----------------- +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.App.Log.Dir, config.App.Log.Dir, config.App.Log.Dir + if config.App.Log.LevelDirs { + infoPath = filepath.Join(config.App.Log.Dir, "info") + warnPath = filepath.Join(config.App.Log.Dir, "warn") + errorPath = filepath.Join(config.App.Log.Dir, "error") + createDirIfNotExist(infoPath) + createDirIfNotExist(warnPath) + createDirIfNotExist(errorPath) + } else { + createDirIfNotExist(config.App.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.App.Log.Format == "console" { + fileEncoder = newConsoleEncoder(false) + } else { + fileEncoder = newJSONEncoder() + } + + infoCore = &trackCore{zapcore.NewCore(fileEncoder, zapcore.AddSync(infoFile), zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { + return lvl <= zapcore.InfoLevel + }))} + + warnCore = &trackCore{zapcore.NewCore(fileEncoder, zapcore.AddSync(warnFile), zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { + return lvl == zapcore.WarnLevel + }))} + + errorCore = &trackCore{zapcore.NewCore(fileEncoder, zapcore.AddSync(errorFile), zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { + return lvl >= zapcore.ErrorLevel + }))} + + cores := []zapcore.Core{infoCore, warnCore, errorCore} + + if config.App.Log.Console { + consoleCore := &trackCore{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.App.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.App.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] [%d] %s", pid, routinex.CurGID(), 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/base-project/base-go-v2/internal/routinex/routinex.go b/code/base-project/base-go-v2/internal/routinex/routinex.go new file mode 100644 index 00000000..6793a035 --- /dev/null +++ b/code/base-project/base-go-v2/internal/routinex/routinex.go @@ -0,0 +1,77 @@ +/** +保证多routine共用同一数据 +*/ + +package routinex + +import ( + "runtime" + "strconv" + "strings" + "sync" +) + +// CurGID 获取 routine 的id +func CurGID() uint64 { + var buf [64]byte + n := runtime.Stack(buf[:], false) + line := strings.Fields(strings.TrimPrefix(string(buf[:n]), "goroutine "))[0] + gid, _ := strconv.ParseUint(line, 10, 64) + return gid +} + +type RoutineLocal struct { + data sync.Map // gid -> map[key]any +} + +var ( + instance *RoutineLocal + instanceOnce sync.Once +) + +// GetInstance 获取当前示例 +func GetInstance() *RoutineLocal { + instanceOnce.Do(func() { + instance = &RoutineLocal{} + }) + return instance +} + +// Set 存储单条数据 +func Set(key string, value any) { + gid := CurGID() + v, _ := GetInstance().data.LoadOrStore(gid, &sync.Map{}) + m := v.(*sync.Map) + m.Store(key, value) +} + +// Get 获取单条数据 +func Get(key string) any { + gid := CurGID() + if v, ok := GetInstance().data.Load(gid); ok { + m := v.(*sync.Map) + if val, ok := m.Load(key); ok { + return val + } + } + return nil +} + +// Clear 删除当前 goroutine 所有数据 +func Clear() { + GetInstance().data.Delete(CurGID()) +} + +// Go 自动继承父 goroutine 数据,多 routine 的时候需要使用 +func Go(f func()) { + parentGID := CurGID() + parentData, _ := GetInstance().data.Load(parentGID) + + go func() { + if parentData != nil { + GetInstance().data.Store(CurGID(), parentData) + defer Clear() + } + f() + }() +} diff --git a/code/base-project/base-go-v2/internal/types/appConfig.go b/code/base-project/base-go-v2/internal/types/appConfig.go index 8b5821fd..89e6f171 100644 --- a/code/base-project/base-go-v2/internal/types/appConfig.go +++ b/code/base-project/base-go-v2/internal/types/appConfig.go @@ -2,27 +2,47 @@ package types import "time" -// AppConfig 统一配置结构体 +// AppConfig 统一配置结构体(非指针版) type AppConfig struct { - Server struct { - Port int - } - JWT struct { - Secret string - Expiry time.Duration - } - Db struct { - Host string - Port int - User string - Password string - Dbname string - MaxOpenConns int - MaxIdleConns int - ConnMaxLifetime time.Duration - } - Snowflake struct { - DatacenterID uint64 - WorkerID uint64 - } + Server ServerConfig + JWT JWTConfig + Db DbConfig + Snowflake SnowflakeConfig + Log LogConfig +} + +type ServerConfig struct { + Port int +} + +type JWTConfig struct { + Secret string + Expiry time.Duration +} + +type DbConfig struct { + Host string + Port int + User string + Password string + Dbname string + MaxOpenConns int + MaxIdleConns int + ConnMaxLifetime time.Duration +} + +type SnowflakeConfig struct { + DatacenterID uint64 + WorkerID uint64 +} + +type LogConfig struct { + Dir string // 日志根目录 + Level string // 全局最低日志级别 + Format string // 输出格式,可选 json 或 console + Daily bool // 是否按天滚动 + MaxDays int // 保留天数,0表示无限 + Caller bool // 是否记录调用文件行号 + LevelDirs bool // 是否按级别目录 + Console bool } diff --git a/code/base-project/base-go-v2/internal/utils/jwt/jwt.go b/code/base-project/base-go-v2/internal/utils/jwt/jwt.go index 60c751a3..784651e4 100644 --- a/code/base-project/base-go-v2/internal/utils/jwt/jwt.go +++ b/code/base-project/base-go-v2/internal/utils/jwt/jwt.go @@ -3,8 +3,9 @@ package jwt import ( "base-go-v2/internal/config" "errors" - "github.com/golang-jwt/jwt/v5" "time" + + "github.com/golang-jwt/jwt/v5" ) var ( @@ -29,6 +30,7 @@ func CreateToken(orgID, userID string) (string, error) { } expiry := config.App.JWT.Expiry + switch { case expiry > 0: expireAt := time.Now().Add(expiry) diff --git a/code/base-project/base-go-v2/internal/utils/strutil/strutil.go b/code/base-project/base-go-v2/internal/utils/strutil/strutil.go new file mode 100644 index 00000000..e7acc302 --- /dev/null +++ b/code/base-project/base-go-v2/internal/utils/strutil/strutil.go @@ -0,0 +1,53 @@ +package strutil + +import ( + "fmt" + "strconv" +) + +// ToString 通用高效转换为 string +func ToString(v any) string { + switch n := v.(type) { + // 整数类型 + case int: + return strconv.FormatInt(int64(n), 10) + case int8: + return strconv.FormatInt(int64(n), 10) + case int16: + return strconv.FormatInt(int64(n), 10) + case int32: + return strconv.FormatInt(int64(n), 10) + case int64: + return strconv.FormatInt(n, 10) + + // 无符号整数 + case uint: + return strconv.FormatUint(uint64(n), 10) + case uint8: + return strconv.FormatUint(uint64(n), 10) + case uint16: + return strconv.FormatUint(uint64(n), 10) + case uint32: + return strconv.FormatUint(uint64(n), 10) + case uint64: + return strconv.FormatUint(n, 10) + + // 浮点数 + case float32: + return strconv.FormatFloat(float64(n), 'f', -1, 32) + case float64: + return strconv.FormatFloat(n, 'f', -1, 64) + + // 字符串 + case string: + return n + + // 布尔 + case bool: + return strconv.FormatBool(n) + + default: + // fallback + return fmt.Sprintf("%v", v) + } +} diff --git a/code/base-project/base-go-v2/internal/utils/uid/snowflake.go b/code/base-project/base-go-v2/internal/utils/uid/snowflake.go new file mode 100644 index 00000000..795bb491 --- /dev/null +++ b/code/base-project/base-go-v2/internal/utils/uid/snowflake.go @@ -0,0 +1,90 @@ +package uid + +import ( + "base-go-v2/internal/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 + + dcID = config.App.Snowflake.DatacenterID + wID = config.App.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 +} diff --git a/code/base-project/base-go/cmd/app/main.go b/code/base-project/base-go/cmd/app/main.go index 92ad622e..13e75524 100644 --- a/code/base-project/base-go/cmd/app/main.go +++ b/code/base-project/base-go/cmd/app/main.go @@ -4,9 +4,11 @@ import ( "base-framework/internal/app" "base-framework/pkg/config" "base-framework/pkg/logx" - "log" + "net/http" "strconv" + + "log" ) func main() {