This commit is contained in:
oneao committed 2025-08-15 17:27:23 +08:00
1 parent 428d370874
commit b408533fe7
15 files changed
+710 -97

No files matched your search

Whitespace-only changes.
+9 -31
View File
@@ -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)
@@ -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
+2 -1
View File
@@ -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
+4
View File
@@ -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=
@@ -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 测试错误
@@ -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...
Whitespace-only changes.
@@ -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
}
@@ -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
@@ -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) }
@@ -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
}
@@ -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 文件行(仅返回 <project>/<subpath>.go:<line>)
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 ""
}
// 将绝对路径裁剪为 "<project>/<subpath>.go:<line>"
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
}
}
@@ -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
}
@@ -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
}