u
This commit is contained in:
1 parent
91ae4cb474
commit
b116690769
14 files changed
+669
-56
No files matched your search
@@ -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()
|
||||||
|
}
|
||||||
@@ -14,4 +14,14 @@ db:
|
|||||||
|
|
||||||
jwt:
|
jwt:
|
||||||
secret: 3Bde3BGEbYqtqyEUzW3ry8jKFcaPH17fRmTmqE7MDr05Lwj95uruRKrrkb44TJ4s
|
secret: 3Bde3BGEbYqtqyEUzW3ry8jKFcaPH17fRmTmqE7MDr05Lwj95uruRKrrkb44TJ4s
|
||||||
expiry: 168h
|
expiry: -1
|
||||||
|
|
||||||
|
log:
|
||||||
|
dir: "logs" # 日志根目录
|
||||||
|
level: "debug" # 全局最低日志级别
|
||||||
|
format: "json" # 输出格式,可选 json 或 console
|
||||||
|
daily: true # 是否按天滚动
|
||||||
|
maxDays: 30 # 保留天数,0表示无限
|
||||||
|
caller: true # 是否记录调用文件行号
|
||||||
|
levelDirs: false # 是否按级别目录
|
||||||
|
console: true # 是否打印到控制台
|
||||||
@@ -2,20 +2,23 @@ module base-go-v2
|
|||||||
|
|
||||||
go 1.25.0
|
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 (
|
require (
|
||||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.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/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/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/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||||
github.com/spf13/afero v1.15.0 // indirect
|
github.com/spf13/afero v1.15.0 // indirect
|
||||||
github.com/spf13/cast v1.10.0 // indirect
|
github.com/spf13/cast v1.10.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.10 // 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/subosito/gotenv v1.6.0 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasthttp v1.68.0 // indirect
|
github.com/valyala/fasthttp v1.68.0 // indirect
|
||||||
|
|||||||
@@ -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/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 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
||||||
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
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 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
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=
|
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||||
|
|||||||
@@ -2,35 +2,54 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"base-go-v2/internal/types"
|
"base-go-v2/internal/types"
|
||||||
"github.com/spf13/viper"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
// App 全局唯一导出配置实例,初始化默认值
|
// App 全局唯一配置实例
|
||||||
var App = &types.AppConfig{
|
var App *types.AppConfig
|
||||||
Server: types.AppConfig{}.Server,
|
|
||||||
JWT: types.AppConfig{}.JWT,
|
|
||||||
Db: types.AppConfig{}.Db,
|
|
||||||
Snowflake: types.AppConfig{}.Snowflake,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 给部分字段设置默认值
|
// init 初始化默认值
|
||||||
func init() {
|
func init() {
|
||||||
App.Server.Port = 8080
|
App = &types.AppConfig{
|
||||||
App.JWT.Secret = "default_secret"
|
Server: types.ServerConfig{
|
||||||
App.JWT.Expiry = time.Hour
|
Port: 8080,
|
||||||
|
},
|
||||||
// 数据库只给连接池默认值
|
JWT: types.JWTConfig{
|
||||||
App.Db.MaxOpenConns = 10
|
Secret: "default_secret",
|
||||||
App.Db.MaxIdleConns = 5
|
Expiry: time.Hour,
|
||||||
App.Db.ConnMaxLifetime = 30 * time.Minute
|
},
|
||||||
|
Db: types.DbConfig{
|
||||||
App.Snowflake.DatacenterID = 1
|
MaxOpenConns: 10,
|
||||||
App.Snowflake.WorkerID = 1
|
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 初始化配置
|
// InitAppConfig 初始化配置
|
||||||
func InitApplicationConfig(configPath string) error {
|
func InitAppConfig(configPath string) error {
|
||||||
|
if configPath == "" {
|
||||||
|
fmt.Println("⚠️ 使用默认配置")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
v := viper.New()
|
v := viper.New()
|
||||||
v.SetConfigFile(configPath)
|
v.SetConfigFile(configPath)
|
||||||
v.SetConfigType("yaml")
|
v.SetConfigType("yaml")
|
||||||
@@ -39,21 +58,23 @@ func InitApplicationConfig(configPath string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 逐字段解码,缺失字段保留默认值
|
||||||
if err := v.UnmarshalKey("server", &App.Server); err != nil {
|
if err := v.UnmarshalKey("server", &App.Server); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := v.UnmarshalKey("jwt", &App.JWT); err != nil {
|
if err := v.UnmarshalKey("jwt", &App.JWT); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := v.UnmarshalKey("snowflake", &App.Snowflake); err != nil {
|
if err := v.UnmarshalKey("snowflake", &App.Snowflake); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := v.UnmarshalKey("db", &App.Db); err != nil {
|
if err := v.UnmarshalKey("db", &App.Db); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := v.UnmarshalKey("log", &App.Log); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("✅ 初始化 %s 配置成功\n", configPath)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -22,7 +22,7 @@ var defaultPoolConfig = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func InitDb() error {
|
func InitDb() error {
|
||||||
d := config.Db
|
d := config.App.Db
|
||||||
|
|
||||||
// 构造 DSN
|
// 构造 DSN
|
||||||
dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
|
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)
|
return fmt.Errorf("数据库连接测试失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fmt.Printf("✅ 数据库连接成功:%s:%d/%s\n", d.Host, d.Port, d.Dbname)
|
||||||
|
|
||||||
DB = db
|
DB = db
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -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) }
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}()
|
||||||
|
}
|
||||||
@@ -2,27 +2,47 @@ package types
|
|||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
// AppConfig 统一配置结构体
|
// AppConfig 统一配置结构体(非指针版)
|
||||||
type AppConfig struct {
|
type AppConfig struct {
|
||||||
Server struct {
|
Server ServerConfig
|
||||||
Port int
|
JWT JWTConfig
|
||||||
}
|
Db DbConfig
|
||||||
JWT struct {
|
Snowflake SnowflakeConfig
|
||||||
Secret string
|
Log LogConfig
|
||||||
Expiry time.Duration
|
}
|
||||||
}
|
|
||||||
Db struct {
|
type ServerConfig struct {
|
||||||
Host string
|
Port int
|
||||||
Port int
|
}
|
||||||
User string
|
|
||||||
Password string
|
type JWTConfig struct {
|
||||||
Dbname string
|
Secret string
|
||||||
MaxOpenConns int
|
Expiry time.Duration
|
||||||
MaxIdleConns int
|
}
|
||||||
ConnMaxLifetime time.Duration
|
|
||||||
}
|
type DbConfig struct {
|
||||||
Snowflake struct {
|
Host string
|
||||||
DatacenterID uint64
|
Port int
|
||||||
WorkerID uint64
|
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
|
||||||
}
|
}
|
||||||
@@ -3,8 +3,9 @@ package jwt
|
|||||||
import (
|
import (
|
||||||
"base-go-v2/internal/config"
|
"base-go-v2/internal/config"
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/golang-jwt/jwt/v5"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -29,6 +30,7 @@ func CreateToken(orgID, userID string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
expiry := config.App.JWT.Expiry
|
expiry := config.App.JWT.Expiry
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case expiry > 0:
|
case expiry > 0:
|
||||||
expireAt := time.Now().Add(expiry)
|
expireAt := time.Now().Add(expiry)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -4,9 +4,11 @@ import (
|
|||||||
"base-framework/internal/app"
|
"base-framework/internal/app"
|
||||||
"base-framework/pkg/config"
|
"base-framework/pkg/config"
|
||||||
"base-framework/pkg/logx"
|
"base-framework/pkg/logx"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
Reference in new issue
Block a user