b 4 tháng trước cách đây
mục cha
commit
514f8e2741

+ 3 - 3
src/gin/jwtx/InitGroup.go

@@ -6,8 +6,8 @@ import (
 	"encoding/pem"
 	"time"
 
-	"git.ooo.ink/root/go-kit/gin/jwtx/db/dao/model"
-	"git.ooo.ink/root/go-kit/gin/jwtx/tool"
+	"git.ooo.ink/root/go-kit/src/gin/jwtx/db/dao/model"
+	"git.ooo.ink/root/go-kit/src/gin/jwtx/tool"
 	"git.ooo.ink/root/go-kit/src/gorm/mysqlx"
 	"git.ooo.ink/root/go-kit/src/utilx"
 	"github.com/5-say/go-tool/logx"
@@ -73,7 +73,7 @@ func InitGroup(group, configPath, privateKeyPath string) *SingletonT {
 	if err != nil {
 		panic(err)
 	}
-	s.PrivateKey[group] = privateKey
+	s.PrivateKey[group] = *privateKey
 
 	return s
 }

+ 2 - 0
src/gin/jwtx/db/Makefile

@@ -0,0 +1,2 @@
+run:
+	gentool -c "./gen.yml"

+ 29 - 0
src/gin/jwtx/db/dao/model/jwtx_tokens.gen.go

@@ -0,0 +1,29 @@
+// Code generated by gorm.io/gen. DO NOT EDIT.
+// Code generated by gorm.io/gen. DO NOT EDIT.
+// Code generated by gorm.io/gen. DO NOT EDIT.
+
+package model
+
+import (
+	"time"
+)
+
+const TableNameJwtxToken = "jwtx_tokens"
+
+// JwtxToken jwtx token 信息表
+type JwtxToken struct {
+	ID             uint64    `gorm:"column:id;type:bigint unsigned;primaryKey;autoIncrement:true;comment:token ID" json:"id"`                              // token ID
+	AccountID      uint64    `gorm:"column:account_id;type:bigint unsigned;not null;index:account_id,priority:1;comment:账户 ID" json:"account_id"`          // 账户 ID
+	LoginGroup     string    `gorm:"column:login_group;type:varchar(255);not null;index:login_group,priority:1;comment:登录的分组" json:"login_group"`          // 登录的分组
+	LoginTerminal  string    `gorm:"column:login_terminal;type:varchar(255);not null;index:login_terminal,priority:1;comment:登录的终端" json:"login_terminal"` // 登录的终端
+	MakeTokenIP    string    `gorm:"column:make_token_ip;type:varchar(50);not null;comment:首次请求生成 token 的 IP 地址" json:"make_token_ip"`                     // 首次请求生成 token 的 IP 地址
+	CreatedAt      time.Time `gorm:"column:created_at;type:datetime;not null;comment:创建时间" json:"created_at"`                                              // 创建时间
+	LastRefreshAt  time.Time `gorm:"column:last_refresh_at;type:datetime;not null;comment:上次的刷新时间" json:"last_refresh_at"`                                 // 上次的刷新时间
+	FinalRefreshAt time.Time `gorm:"column:final_refresh_at;type:datetime;not null;comment:最后的刷新时间" json:"final_refresh_at"`                               // 最后的刷新时间
+	ExpirationAt   time.Time `gorm:"column:expiration_at;type:datetime;not null;comment:过期时间" json:"expiration_at"`                                        // 过期时间
+}
+
+// TableName JwtxToken's table name
+func (*JwtxToken) TableName() string {
+	return TableNameJwtxToken
+}

+ 93 - 0
src/gin/jwtx/db/dao/query/gen.go

@@ -0,0 +1,93 @@
+// Code generated by gorm.io/gen. DO NOT EDIT.
+// Code generated by gorm.io/gen. DO NOT EDIT.
+// Code generated by gorm.io/gen. DO NOT EDIT.
+
+package query
+
+import (
+	"context"
+	"database/sql"
+
+	"gorm.io/gorm"
+
+	"gorm.io/gen"
+
+	"gorm.io/plugin/dbresolver"
+)
+
+func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
+	return &Query{
+		db:        db,
+		JwtxToken: newJwtxToken(db, opts...),
+	}
+}
+
+type Query struct {
+	db *gorm.DB
+
+	JwtxToken jwtxToken
+}
+
+func (q *Query) Available() bool { return q.db != nil }
+
+func (q *Query) clone(db *gorm.DB) *Query {
+	return &Query{
+		db:        db,
+		JwtxToken: q.JwtxToken.clone(db),
+	}
+}
+
+func (q *Query) ReadDB() *Query {
+	return q.ReplaceDB(q.db.Clauses(dbresolver.Read))
+}
+
+func (q *Query) WriteDB() *Query {
+	return q.ReplaceDB(q.db.Clauses(dbresolver.Write))
+}
+
+func (q *Query) ReplaceDB(db *gorm.DB) *Query {
+	return &Query{
+		db:        db,
+		JwtxToken: q.JwtxToken.replaceDB(db),
+	}
+}
+
+type queryCtx struct {
+	JwtxToken *jwtxTokenDo
+}
+
+func (q *Query) WithContext(ctx context.Context) *queryCtx {
+	return &queryCtx{
+		JwtxToken: q.JwtxToken.WithContext(ctx),
+	}
+}
+
+func (q *Query) Transaction(fc func(tx *Query) error, opts ...*sql.TxOptions) error {
+	return q.db.Transaction(func(tx *gorm.DB) error { return fc(q.clone(tx)) }, opts...)
+}
+
+func (q *Query) Begin(opts ...*sql.TxOptions) *QueryTx {
+	tx := q.db.Begin(opts...)
+	return &QueryTx{Query: q.clone(tx), Error: tx.Error}
+}
+
+type QueryTx struct {
+	*Query
+	Error error
+}
+
+func (q *QueryTx) Commit() error {
+	return q.db.Commit().Error
+}
+
+func (q *QueryTx) Rollback() error {
+	return q.db.Rollback().Error
+}
+
+func (q *QueryTx) SavePoint(name string) error {
+	return q.db.SavePoint(name).Error
+}
+
+func (q *QueryTx) RollbackTo(name string) error {
+	return q.db.RollbackTo(name).Error
+}

+ 362 - 0
src/gin/jwtx/db/dao/query/jwtx_tokens.gen.go

@@ -0,0 +1,362 @@
+// Code generated by gorm.io/gen. DO NOT EDIT.
+// Code generated by gorm.io/gen. DO NOT EDIT.
+// Code generated by gorm.io/gen. DO NOT EDIT.
+
+package query
+
+import (
+	"context"
+
+	"gorm.io/gorm"
+	"gorm.io/gorm/clause"
+	"gorm.io/gorm/schema"
+
+	"gorm.io/gen"
+	"gorm.io/gen/field"
+
+	"gorm.io/plugin/dbresolver"
+
+	"github.com/5-say/go-tool/gin/jwtx/db/dao/model"
+)
+
+func newJwtxToken(db *gorm.DB, opts ...gen.DOOption) jwtxToken {
+	_jwtxToken := jwtxToken{}
+
+	_jwtxToken.jwtxTokenDo.UseDB(db, opts...)
+	_jwtxToken.jwtxTokenDo.UseModel(&model.JwtxToken{})
+
+	tableName := _jwtxToken.jwtxTokenDo.TableName()
+	_jwtxToken.ALL = field.NewAsterisk(tableName)
+	_jwtxToken.ID = field.NewUint64(tableName, "id")
+	_jwtxToken.AccountID = field.NewUint64(tableName, "account_id")
+	_jwtxToken.LoginGroup = field.NewString(tableName, "login_group")
+	_jwtxToken.LoginTerminal = field.NewString(tableName, "login_terminal")
+	_jwtxToken.MakeTokenIP = field.NewString(tableName, "make_token_ip")
+	_jwtxToken.CreatedAt = field.NewTime(tableName, "created_at")
+	_jwtxToken.LastRefreshAt = field.NewTime(tableName, "last_refresh_at")
+	_jwtxToken.FinalRefreshAt = field.NewTime(tableName, "final_refresh_at")
+	_jwtxToken.ExpirationAt = field.NewTime(tableName, "expiration_at")
+
+	_jwtxToken.fillFieldMap()
+
+	return _jwtxToken
+}
+
+// jwtxToken jwtx token 信息表
+type jwtxToken struct {
+	jwtxTokenDo jwtxTokenDo
+
+	ALL            field.Asterisk
+	ID             field.Uint64 // token ID
+	AccountID      field.Uint64 // 账户 ID
+	LoginGroup     field.String // 登录的分组
+	LoginTerminal  field.String // 登录的终端
+	MakeTokenIP    field.String // 首次请求生成 token 的 IP 地址
+	CreatedAt      field.Time   // 创建时间
+	LastRefreshAt  field.Time   // 上次的刷新时间
+	FinalRefreshAt field.Time   // 最后的刷新时间
+	ExpirationAt   field.Time   // 过期时间
+
+	fieldMap map[string]field.Expr
+}
+
+func (j jwtxToken) Table(newTableName string) *jwtxToken {
+	j.jwtxTokenDo.UseTable(newTableName)
+	return j.updateTableName(newTableName)
+}
+
+func (j jwtxToken) As(alias string) *jwtxToken {
+	j.jwtxTokenDo.DO = *(j.jwtxTokenDo.As(alias).(*gen.DO))
+	return j.updateTableName(alias)
+}
+
+func (j *jwtxToken) updateTableName(table string) *jwtxToken {
+	j.ALL = field.NewAsterisk(table)
+	j.ID = field.NewUint64(table, "id")
+	j.AccountID = field.NewUint64(table, "account_id")
+	j.LoginGroup = field.NewString(table, "login_group")
+	j.LoginTerminal = field.NewString(table, "login_terminal")
+	j.MakeTokenIP = field.NewString(table, "make_token_ip")
+	j.CreatedAt = field.NewTime(table, "created_at")
+	j.LastRefreshAt = field.NewTime(table, "last_refresh_at")
+	j.FinalRefreshAt = field.NewTime(table, "final_refresh_at")
+	j.ExpirationAt = field.NewTime(table, "expiration_at")
+
+	j.fillFieldMap()
+
+	return j
+}
+
+func (j *jwtxToken) WithContext(ctx context.Context) *jwtxTokenDo {
+	return j.jwtxTokenDo.WithContext(ctx)
+}
+
+func (j jwtxToken) TableName() string { return j.jwtxTokenDo.TableName() }
+
+func (j jwtxToken) Alias() string { return j.jwtxTokenDo.Alias() }
+
+func (j jwtxToken) Columns(cols ...field.Expr) gen.Columns { return j.jwtxTokenDo.Columns(cols...) }
+
+func (j *jwtxToken) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
+	_f, ok := j.fieldMap[fieldName]
+	if !ok || _f == nil {
+		return nil, false
+	}
+	_oe, ok := _f.(field.OrderExpr)
+	return _oe, ok
+}
+
+func (j *jwtxToken) fillFieldMap() {
+	j.fieldMap = make(map[string]field.Expr, 9)
+	j.fieldMap["id"] = j.ID
+	j.fieldMap["account_id"] = j.AccountID
+	j.fieldMap["login_group"] = j.LoginGroup
+	j.fieldMap["login_terminal"] = j.LoginTerminal
+	j.fieldMap["make_token_ip"] = j.MakeTokenIP
+	j.fieldMap["created_at"] = j.CreatedAt
+	j.fieldMap["last_refresh_at"] = j.LastRefreshAt
+	j.fieldMap["final_refresh_at"] = j.FinalRefreshAt
+	j.fieldMap["expiration_at"] = j.ExpirationAt
+}
+
+func (j jwtxToken) clone(db *gorm.DB) jwtxToken {
+	j.jwtxTokenDo.ReplaceConnPool(db.Statement.ConnPool)
+	return j
+}
+
+func (j jwtxToken) replaceDB(db *gorm.DB) jwtxToken {
+	j.jwtxTokenDo.ReplaceDB(db)
+	return j
+}
+
+type jwtxTokenDo struct{ gen.DO }
+
+func (j jwtxTokenDo) Debug() *jwtxTokenDo {
+	return j.withDO(j.DO.Debug())
+}
+
+func (j jwtxTokenDo) WithContext(ctx context.Context) *jwtxTokenDo {
+	return j.withDO(j.DO.WithContext(ctx))
+}
+
+func (j jwtxTokenDo) ReadDB() *jwtxTokenDo {
+	return j.Clauses(dbresolver.Read)
+}
+
+func (j jwtxTokenDo) WriteDB() *jwtxTokenDo {
+	return j.Clauses(dbresolver.Write)
+}
+
+func (j jwtxTokenDo) Session(config *gorm.Session) *jwtxTokenDo {
+	return j.withDO(j.DO.Session(config))
+}
+
+func (j jwtxTokenDo) Clauses(conds ...clause.Expression) *jwtxTokenDo {
+	return j.withDO(j.DO.Clauses(conds...))
+}
+
+func (j jwtxTokenDo) Returning(value interface{}, columns ...string) *jwtxTokenDo {
+	return j.withDO(j.DO.Returning(value, columns...))
+}
+
+func (j jwtxTokenDo) Not(conds ...gen.Condition) *jwtxTokenDo {
+	return j.withDO(j.DO.Not(conds...))
+}
+
+func (j jwtxTokenDo) Or(conds ...gen.Condition) *jwtxTokenDo {
+	return j.withDO(j.DO.Or(conds...))
+}
+
+func (j jwtxTokenDo) Select(conds ...field.Expr) *jwtxTokenDo {
+	return j.withDO(j.DO.Select(conds...))
+}
+
+func (j jwtxTokenDo) Where(conds ...gen.Condition) *jwtxTokenDo {
+	return j.withDO(j.DO.Where(conds...))
+}
+
+func (j jwtxTokenDo) Order(conds ...field.Expr) *jwtxTokenDo {
+	return j.withDO(j.DO.Order(conds...))
+}
+
+func (j jwtxTokenDo) Distinct(cols ...field.Expr) *jwtxTokenDo {
+	return j.withDO(j.DO.Distinct(cols...))
+}
+
+func (j jwtxTokenDo) Omit(cols ...field.Expr) *jwtxTokenDo {
+	return j.withDO(j.DO.Omit(cols...))
+}
+
+func (j jwtxTokenDo) Join(table schema.Tabler, on ...field.Expr) *jwtxTokenDo {
+	return j.withDO(j.DO.Join(table, on...))
+}
+
+func (j jwtxTokenDo) LeftJoin(table schema.Tabler, on ...field.Expr) *jwtxTokenDo {
+	return j.withDO(j.DO.LeftJoin(table, on...))
+}
+
+func (j jwtxTokenDo) RightJoin(table schema.Tabler, on ...field.Expr) *jwtxTokenDo {
+	return j.withDO(j.DO.RightJoin(table, on...))
+}
+
+func (j jwtxTokenDo) Group(cols ...field.Expr) *jwtxTokenDo {
+	return j.withDO(j.DO.Group(cols...))
+}
+
+func (j jwtxTokenDo) Having(conds ...gen.Condition) *jwtxTokenDo {
+	return j.withDO(j.DO.Having(conds...))
+}
+
+func (j jwtxTokenDo) Limit(limit int) *jwtxTokenDo {
+	return j.withDO(j.DO.Limit(limit))
+}
+
+func (j jwtxTokenDo) Offset(offset int) *jwtxTokenDo {
+	return j.withDO(j.DO.Offset(offset))
+}
+
+func (j jwtxTokenDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *jwtxTokenDo {
+	return j.withDO(j.DO.Scopes(funcs...))
+}
+
+func (j jwtxTokenDo) Unscoped() *jwtxTokenDo {
+	return j.withDO(j.DO.Unscoped())
+}
+
+func (j jwtxTokenDo) Create(values ...*model.JwtxToken) error {
+	if len(values) == 0 {
+		return nil
+	}
+	return j.DO.Create(values)
+}
+
+func (j jwtxTokenDo) CreateInBatches(values []*model.JwtxToken, batchSize int) error {
+	return j.DO.CreateInBatches(values, batchSize)
+}
+
+// Save : !!! underlying implementation is different with GORM
+// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
+func (j jwtxTokenDo) Save(values ...*model.JwtxToken) error {
+	if len(values) == 0 {
+		return nil
+	}
+	return j.DO.Save(values)
+}
+
+func (j jwtxTokenDo) First() (*model.JwtxToken, error) {
+	if result, err := j.DO.First(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.JwtxToken), nil
+	}
+}
+
+func (j jwtxTokenDo) Take() (*model.JwtxToken, error) {
+	if result, err := j.DO.Take(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.JwtxToken), nil
+	}
+}
+
+func (j jwtxTokenDo) Last() (*model.JwtxToken, error) {
+	if result, err := j.DO.Last(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.JwtxToken), nil
+	}
+}
+
+func (j jwtxTokenDo) Find() ([]*model.JwtxToken, error) {
+	result, err := j.DO.Find()
+	return result.([]*model.JwtxToken), err
+}
+
+func (j jwtxTokenDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.JwtxToken, err error) {
+	buf := make([]*model.JwtxToken, 0, batchSize)
+	err = j.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
+		defer func() { results = append(results, buf...) }()
+		return fc(tx, batch)
+	})
+	return results, err
+}
+
+func (j jwtxTokenDo) FindInBatches(result *[]*model.JwtxToken, batchSize int, fc func(tx gen.Dao, batch int) error) error {
+	return j.DO.FindInBatches(result, batchSize, fc)
+}
+
+func (j jwtxTokenDo) Attrs(attrs ...field.AssignExpr) *jwtxTokenDo {
+	return j.withDO(j.DO.Attrs(attrs...))
+}
+
+func (j jwtxTokenDo) Assign(attrs ...field.AssignExpr) *jwtxTokenDo {
+	return j.withDO(j.DO.Assign(attrs...))
+}
+
+func (j jwtxTokenDo) Joins(fields ...field.RelationField) *jwtxTokenDo {
+	for _, _f := range fields {
+		j = *j.withDO(j.DO.Joins(_f))
+	}
+	return &j
+}
+
+func (j jwtxTokenDo) Preload(fields ...field.RelationField) *jwtxTokenDo {
+	for _, _f := range fields {
+		j = *j.withDO(j.DO.Preload(_f))
+	}
+	return &j
+}
+
+func (j jwtxTokenDo) FirstOrInit() (*model.JwtxToken, error) {
+	if result, err := j.DO.FirstOrInit(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.JwtxToken), nil
+	}
+}
+
+func (j jwtxTokenDo) FirstOrCreate() (*model.JwtxToken, error) {
+	if result, err := j.DO.FirstOrCreate(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.JwtxToken), nil
+	}
+}
+
+func (j jwtxTokenDo) FindByPage(offset int, limit int) (result []*model.JwtxToken, count int64, err error) {
+	result, err = j.Offset(offset).Limit(limit).Find()
+	if err != nil {
+		return
+	}
+
+	if size := len(result); 0 < limit && 0 < size && size < limit {
+		count = int64(size + offset)
+		return
+	}
+
+	count, err = j.Offset(-1).Limit(-1).Count()
+	return
+}
+
+func (j jwtxTokenDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
+	count, err = j.Count()
+	if err != nil {
+		return
+	}
+
+	err = j.Offset(offset).Limit(limit).Scan(result)
+	return
+}
+
+func (j jwtxTokenDo) Scan(result interface{}) (err error) {
+	return j.DO.Scan(result)
+}
+
+func (j jwtxTokenDo) Delete(models ...*model.JwtxToken) (result gen.ResultInfo, err error) {
+	return j.DO.Delete(models)
+}
+
+func (j *jwtxTokenDo) withDO(do gen.Dao) *jwtxTokenDo {
+	j.DO = *do.(*gen.DO)
+	return j
+}

+ 56 - 0
src/gin/jwtx/db/gen.yml

@@ -0,0 +1,56 @@
+version: "0.1"
+database:
+
+  # consult[https://gorm.io/docs/connecting_to_the_database.html]"
+  dsn: "root:root@tcp(mysql:3306)/dev_local_platform?charset=utf8mb4&parseTime=True&loc=Local"
+
+  # input mysql or postgres or sqlite or sqlserver. consult[https://gorm.io/docs/connecting_to_the_database.html]
+  db: "mysql"
+
+  # enter the required data table or leave it blank.You can input :
+  # 指定要生成的表名称,默认所有表
+  # tables  :
+  #   - orders
+  #   - users
+  #   - goods
+  tables:
+    - jwtx_tokens
+
+  # only generate models (without query file)
+  onlyModel: false
+
+  # specify a directory for output
+  # 指定输出目录
+  outPath: "./dao/query"
+
+  # query code file name, default: gen.go
+  # Genrated 查询代码文件名称
+  outFile: ""
+
+  # generate unit test for query code
+  # 生成单元测试
+  withUnitTest: false
+
+  # generated model code's package name
+  # 生成模型代码包名称
+  modelPkgName: ""
+
+  # generate with pointer when field is nullable
+  # 当字段允许空时用指针生成
+  fieldNullable: true
+
+  # generate with pointer when field has default value
+  # 当字段具有默认值时,使用指针生成
+  fieldCoverable: true
+
+  # generate field with gorm index tag
+  # 生成带有 gorm index 标签的字段
+  fieldWithIndexTag: true
+
+  # generate field with gorm column type tag
+  # 生成带有 gorm type 标签的字段
+  fieldWithTypeTag: true
+
+  # detect integer field's unsigned type, adjust generated data type
+  # 检测整数字段的无符号类型,调整生成的数据类型
+  fieldSignable: true

+ 19 - 0
src/gin/jwtx/db/mysql/jwtx_tokens.sql

@@ -0,0 +1,19 @@
+-- Active: 1727011634447@@mysql@3306@dev_local_platform
+
+DROP TABLE IF EXISTS `jwtx_tokens`;
+CREATE TABLE `jwtx_tokens` (
+  `id`               bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'token ID',
+  `account_id`       bigint unsigned NOT NULL                COMMENT '账户 ID',
+  `login_group`      varchar(255)    NOT NULL                COMMENT '登录的分组',
+  `login_terminal`   varchar(255)    NOT NULL                COMMENT '登录的终端',
+  `make_token_ip`    varchar(50)     NOT NULL                COMMENT '首次请求生成 token 的 IP 地址',
+  `created_at`       datetime        NOT NULL                COMMENT '创建时间',
+  `last_refresh_at`  datetime        NOT NULL                COMMENT '上次的刷新时间',
+  `final_refresh_at` datetime        NOT NULL                COMMENT '最后的刷新时间',
+  `expiration_at`    datetime        NOT NULL                COMMENT '过期时间',
+  --
+  KEY `account_id` (`account_id`),
+  KEY `login_group` (`login_group`),
+  KEY `login_terminal` (`login_terminal`),
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='jwtx token 信息表';

+ 1 - 1
src/gin/jwtx/test/Tool_test.go

@@ -7,7 +7,7 @@ import (
 	"testing"
 	"time"
 
-	"git.ooo.ink/root/go-kit/gin/jwtx/tool"
+	"git.ooo.ink/root/go-kit/src/gin/jwtx/tool"
 	"github.com/golang-jwt/jwt/v5"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"

+ 83 - 9
src/logx/Debug.go

@@ -13,27 +13,101 @@ var (
 	debugLogger *zerolog.Logger
 )
 
-// InitDebugWriter
+// InitDebugWriter 初始化 Debug 级别的日志写入器
 //
-// e.g.
+// 该函数创建一个配置好的 zerolog 日志器实例,专门用于处理 Debug 级别的日志。
+// 日志器会使用控制台格式输出,包含时间戳、调用者信息,并支持自定义时区。
 //
-//	logx.InitDebugWriter(logx.DefaultWriter("debug.log", true))
+// 参数:
+//
+//	writer - 日志输出目标,可以是文件、控制台或其他实现了 io.Writer 接口的对象
+//
+// 功能特性:
+//   - 使用 ConsoleWriter 格式化输出,便于人类阅读
+//   - 禁用颜色输出(NoColor: true),适合文件记录
+//   - 自动包含时间戳,使用配置的时区格式化
+//   - 包含调用者信息(文件名和行号)
+//   - 实现单例模式,全局共享同一个日志器实例
+//
+// 使用示例:
+//
+//	// 使用默认配置创建异步文件写入器
+//	writer := logx.DefaultWriter("debug.log", true)
+//
+//	// 初始化 Debug 日志器
+//	logx.InitDebugWriter(writer)
+//
+// 注意事项:
+//   - 该函数应该在应用程序启动时调用一次
+//   - 如果多次调用,后一次的配置会覆盖前一次
+//   - 需要先调用 logx.InitDefault() 或设置 loggingLocationName 以确保时区正确
 func InitDebugWriter(writer io.Writer) {
+	// 创建 zerolog 日志器实例,配置如下:
+	// - ConsoleWriter: 使用控制台格式输出,便于阅读
+	// - Out: writer: 指定日志输出目标
+	// - NoColor: true: 禁用颜色输出(适合文件记录)
+	// - FormatTimestamp: 使用自定义时区格式化时间戳
+	// - With().Timestamp(): 自动包含时间戳字段
+	// - With().Caller(): 自动包含调用者信息(文件名和行号)
 	l := zerolog.New(zerolog.ConsoleWriter{Out: writer, NoColor: true, FormatTimestamp: tool.ZerologFormatTimestamp(loggingLocationName)}).With().Timestamp().Caller().Logger()
-	// 存储单例
+
+	// 存储单例实例,供全局使用
+	// 后续调用 logx.Debug() 时会使用这个预配置的日志器
 	debugLogger = &l
 }
 
-// Debug
+// Debug 返回一个 Debug 级别的日志事件,用于记录调试信息
+//
+// 该函数实现了智能的日志器选择机制:
+// 1. 如果已经通过 InitDebugWriter() 初始化了专用日志器,则使用该日志器
+// 2. 如果没有初始化,则创建一个临时的控制台输出日志器作为回退方案
 //
-// e.g.
+// 返回值:
 //
-//	logx.Debug().Msg("")
-//	logx.Debug().Any("data", data).Send()
+//	*zerolog.Event - 可用于链式调用的日志事件对象
+//
+// 使用方式:
+//  1. 直接记录消息: logx.Debug().Msg("调试信息")
+//  2. 添加结构化字段: logx.Debug().Str("key", "value").Int("count", 10).Send()
+//  3. 记录任意类型数据: logx.Debug().Any("data", complexStruct).Send()
+//
+// 使用示例:
+//
+//	// 简单消息记录
+//	logx.Debug().Msg("函数执行开始")
+//
+//	// 带结构化字段的记录
+//	logx.Debug().
+//	    Str("username", "john").
+//	    Int("attempt", 3).
+//	    Msg("用户登录尝试")
+//
+//	// 记录任意类型数据
+//	logx.Debug().
+//	    Any("request", req).
+//	    Any("response", resp).
+//	    Send()
+//
+// 最佳实践:
+//   - 在开发阶段大量使用 Debug 日志进行调试
+//   - 生产环境中可通过不初始化 Debug 日志器来禁用 Debug 日志
+//   - 使用结构化字段而不是字符串拼接,便于日志分析和查询
 func Debug() *zerolog.Event {
+	// 检查是否已经初始化了专用 Debug 日志器
+	// 如果已初始化,则使用预配置的日志器(通常指向文件或其他持久化存储)
 	if debugLogger != nil {
 		return debugLogger.Debug()
 	}
-	l := zerolog.New(zerolog.ConsoleWriter{Out: os.Stdout, NoColor: true, FormatTimestamp: tool.ZerologFormatTimestamp(loggingLocationName)}).With().Timestamp().Caller().Logger()
+
+	// 如果没有初始化专用日志器,创建临时控制台日志器作为回退
+	// 这种设计确保即使没有显式初始化,Debug 日志也能正常工作
+	// 控制台输出适合开发环境调试使用
+	l := zerolog.New(zerolog.ConsoleWriter{
+		Out:             os.Stdout,                                        // 输出到标准输出
+		NoColor:         true,                                             // 禁用颜色(适合文件记录)
+		FormatTimestamp: tool.ZerologFormatTimestamp(loggingLocationName), // 自定义时区时间戳
+	}).With().Timestamp().Caller().Logger() // 包含时间戳和调用者信息
+
+	// 返回 Debug 级别的日志事件,可以继续链式调用添加字段和消息
 	return l.Debug()
 }

+ 282 - 0
src/logx/README.md

@@ -0,0 +1,282 @@
+# Go-Kit Logx 日志库
+
+基于 [zerolog](https://github.com/rs/zerolog) 构建的高性能、结构化 Go 语言日志库,支持文件滚动、异步写入和多种日志类型。
+
+## 特性
+
+- 🚀 **高性能**:基于 zerolog 的零分配日志记录
+- 📁 **文件滚动**:自动日志文件轮转和备份管理
+- ⚡ **异步写入**:无锁、无阻塞的异步日志写入
+- 🌍 **时区支持**:自定义时区格式化时间戳
+- 🏷️ **多种日志类型**:Info、Error、Debug、Gin、Gorm、Nano 等
+- 🔧 **灵活配置**:支持自定义滚动和异步参数
+
+## 安装
+
+```bash
+go get github.com/5-say/go-tool/logx
+```
+
+## 快速开始
+
+### 基本使用
+
+```go
+package main
+
+import "github.com/5-say/go-tool/logx"
+
+func main() {
+    // 快速初始化(推荐)
+    logx.InitDefault("logs/app", "Asia/Shanghai", true, "info", "error", "debug")
+
+    // 记录日志
+    logx.Info().Msg("应用启动成功")
+    logx.Error().Msg("发生了一个错误")
+    logx.Debug().Any("data", map[string]interface{}{"key": "value"}).Send()
+}
+```
+
+### 自定义配置
+
+```go
+package main
+
+import (
+    "time"
+    "github.com/5-say/go-tool/logx"
+)
+
+func main() {
+    // 自定义写入器配置
+    config := logx.WriterConfig{
+        Rolling: logx.WriterRollingConfig{
+            MaxMegabytes: 10,    // 单文件最大 10MB
+            MaxDays:      30,    // 保留 30 天
+            MaxBackups:   50,    // 最多 50 个备份文件
+            Compress:     true,  // 压缩备份文件
+        },
+        Async: &logx.WriterAsyncConfig{
+            BufferSize:   5000,                    // 异步缓冲区大小
+            PollInterval: time.Millisecond * 100,  // 轮询间隔
+        },
+    }
+
+    // 创建写入器
+    writer := logx.NewWriter("logs/app.log", config)
+
+    // 初始化日志写入器
+    logx.InitInfoWriter(writer)
+    logx.InitErrorWriter(writer)
+
+    // 使用日志
+    logx.Info().Msg("自定义配置初始化完成")
+}
+```
+
+## API 文档
+
+### 初始化函数
+
+#### `InitDefault(filePath, locationName string, useAsync bool, tags ...string)`
+
+快速初始化日志系统。
+
+**参数:**
+- `filePath`:日志文件路径前缀
+- `locationName`:时区名称(如 "Asia/Shanghai")
+- `useAsync`:是否启用异步写入
+- `tags`:要启用的日志类型标签("info", "error", "debug", "gin", "gorm", "nano")
+
+**示例:**
+```go
+logx.InitDefault("logs/app", "Asia/Shanghai", true, "info", "error", "debug")
+```
+
+#### `InitInfoWriter(writer io.Writer)`
+初始化 Info 日志写入器。
+
+#### `InitErrorWriter(writer io.Writer)`
+初始化 Error 日志写入器(支持错误堆栈)。
+
+#### `InitDebugWriter(writer io.Writer)`
+初始化 Debug 日志写入器。
+
+#### `InitGinWriter(writer io.Writer)`
+初始化 Gin 框架专用日志写入器。
+
+#### `InitGormWriter(writer io.Writer)`
+初始化 GORM 数据库操作日志写入器。
+
+#### `InitNanoWriter(writer io.Writer)`
+初始化微服务专用日志写入器。
+
+### 日志记录函数
+
+#### `Info() *zerolog.Event`
+记录信息级别日志。
+
+**示例:**
+```go
+logx.Info().Msg("用户登录成功")
+logx.Info().Str("username", "john").Int("age", 25).Send()
+```
+
+#### `Error() *zerolog.Event`
+记录错误级别日志(自动包含错误堆栈)。
+
+**示例:**
+```go
+err := errors.New("数据库连接失败")
+logx.Error().Err(err).Msg("操作失败")
+```
+
+#### `Debug() *zerolog.Event`
+记录调试级别日志。
+
+**示例:**
+```go
+logx.Debug().Any("request", req).Msg("收到请求")
+```
+
+### 配置结构体
+
+#### `WriterConfig`
+日志写入器配置。
+
+```go
+type WriterConfig struct {
+    Rolling WriterRollingConfig  // 滚动配置
+    Async   *WriterAsyncConfig   // 异步配置(可选)
+}
+```
+
+#### `WriterRollingConfig`
+文件滚动配置。
+
+```go
+type WriterRollingConfig struct {
+    MaxMegabytes int  // 单文件最大存储容量(兆字节)
+    MaxDays      int  // 最大存储天数
+    MaxBackups   int  // 最大备份文件数量
+    Compress     bool // 备份文件是否压缩
+}
+```
+
+#### `WriterAsyncConfig`
+异步写入配置。
+
+```go
+type WriterAsyncConfig struct {
+    BufferSize   int           // 生产者缓冲区大小
+    PollInterval time.Duration // 消费者轮询间隔
+    Alerter      diode.Alerter // 丢弃告警函数
+}
+```
+
+### 工具函数
+
+#### `DefaultWriter(fileName string, useAsync bool) io.Writer`
+创建默认配置的写入器。
+
+**默认配置:**
+- 滚动:1MB/文件,保留10天,10个备份,不压缩
+- 异步:1000缓冲区(如果启用)
+
+#### `NewWriter(fileName string, c WriterConfig) io.Writer`
+根据配置创建新的写入器。
+
+## 日志文件结构
+
+使用 `InitDefault` 初始化后,会生成如下结构的日志文件:
+
+```
+logs/
+├── app.info.log      # Info 级别日志
+├── app.error.log     # Error 级别日志
+├── app.debug.log     # Debug 级别日志
+├── app.gin.log       # Gin 框架日志
+├── app.gorm.log      # GORM 数据库日志
+└── app.nano.log      # 微服务日志
+```
+
+## 日志格式
+
+日志采用结构化格式,包含:
+
+```
+<timestamp> <level> <caller> <message> <fields>
+```
+
+**示例输出:**
+```
+CST 2024-01-15 10:30:25 INFO main.go:15 用户登录成功 username=john age=25
+CST 2024-01-15 10:30:26 ERROR main.go:20 数据库操作失败 error="connection timeout"
+```
+
+## 性能优化
+
+### 异步写入
+启用异步写入可以显著提高日志记录性能,防止日志 I/O 阻塞主程序:
+
+```go
+logx.InitDefault("logs/app", "Asia/Shanghai", true, "info", "error")
+```
+
+### 合理的滚动配置
+根据应用需求调整滚动参数:
+
+```go
+config := logx.WriterConfig{
+    Rolling: logx.WriterRollingConfig{
+        MaxMegabytes: 100,  // 大文件减少文件数量
+        MaxDays:      7,    // 短期保留节省空间
+        MaxBackups:   10,   // 控制备份数量
+        Compress:     true, // 压缩节省磁盘空间
+    },
+}
+```
+
+## 最佳实践
+
+1. **生产环境**:启用异步写入和文件压缩
+2. **开发环境**:可以禁用异步以便实时查看日志
+3. **错误日志**:始终启用 Error 日志以捕获异常堆栈
+4. **敏感信息**:避免在日志中记录密码、密钥等敏感信息
+5. **日志级别**:根据环境调整日志级别(生产环境可关闭 Debug)
+
+## 故障排除
+
+### 常见问题
+
+**Q: 日志文件没有生成?**
+A: 检查文件路径权限,确保应用有写入权限。
+
+**Q: 异步写入丢失日志?**
+A: 增大 `BufferSize` 或减少 `PollInterval`。
+
+**Q: 时区显示不正确?**
+A: 确认 `locationName` 参数使用正确的时区名称。
+
+### 调试模式
+
+可以临时启用控制台输出进行调试:
+
+```go
+// 不初始化文件写入器,使用标准输出
+logx.Info().Msg("调试信息")
+```
+
+## 依赖项
+
+- [zerolog](https://github.com/rs/zerolog) - 高性能日志库
+- [lumberjack](https://github.com/natefinch/lumberjack) - 日志文件轮转
+- [zerolog/diode](https://pkg.go.dev/github.com/rs/zerolog/diode) - 异步写入器
+
+## 许可证
+
+MIT License
+
+## 贡献
+
+欢迎提交 Issue 和 Pull Request!

+ 38 - 11
src/logx/tool/FileRollingWriter.go

@@ -6,19 +6,46 @@ import (
 	"github.com/natefinch/lumberjack"
 )
 
-// 文件滚动写入器
+// FileRollingWriter 创建并返回一个支持文件滚动的日志写入器
 //
-//	fileName     string // 包含完整路径的文件名
-//	MaxMegabytes int    // 单文件最大存储容量(兆字节)
-//	MaxDays      int    // 最大存储天数
-//	MaxBackups   int    // 最大备份文件数量
-//	Compress     bool   // 备份文件是否压缩
+// 基于 lumberjack 库实现,提供自动的日志文件轮转功能,防止单个日志文件过大。
+//
+// 参数:
+//
+//	fileName     - 包含完整路径的日志文件名(例如:"logs/app.log")
+//	maxMegabytes - 单文件最大存储容量(单位:兆字节)
+//	maxDays      - 日志文件最大保留天数
+//	maxBackups   - 最大备份文件数量
+//	compress     - 备份文件是否启用压缩
+//
+// 返回值:
+//
+//	io.Writer - 符合标准 io.Writer 接口的日志写入器
+//
+// 功能特性:
+//  1. 自动文件轮转:当日志文件达到指定大小时自动创建新文件
+//  2. 备份管理:保留指定数量的备份文件,自动清理旧文件
+//  3. 压缩支持:可选启用备份文件压缩以节省磁盘空间
+//  4. 时间管理:自动清理超过指定天数的旧日志文件
+//
+// 使用示例:
+//
+//	writer := FileRollingWriter("logs/app.log", 10, 30, 50, true)
+//	// writer 可以传递给任何需要 io.Writer 接口的日志库
+//
+// 注意事项:
+//   - 文件轮转基于文件大小,当文件达到 MaxSize 时触发
+//   - 备份文件命名格式:原文件名 + 时间戳(如 app.log.2024-01-15T10-30-25.1)
+//   - 如果启用压缩,备份文件会添加 .gz 后缀
+//   - 建议在生产环境中启用压缩以节省磁盘空间
 func FileRollingWriter(fileName string, maxMegabytes, maxDays, maxBackups int, compress bool) io.Writer {
+	// 创建并配置 lumberjack 日志轮转器
+	// lumberjack 会自动处理文件轮转、备份管理和压缩等操作
 	return &lumberjack.Logger{
-		Filename:   fileName,
-		MaxSize:    maxMegabytes,
-		MaxAge:     maxDays,
-		MaxBackups: maxBackups,
-		Compress:   compress,
+		Filename:   fileName,     // 日志文件路径
+		MaxSize:    maxMegabytes, // 单文件最大大小(MB)
+		MaxAge:     maxDays,      // 文件最大保留天数
+		MaxBackups: maxBackups,   // 最大备份文件数量
+		Compress:   compress,     // 是否压缩备份文件
 	}
 }

+ 43 - 6
src/logx/tool/WrapAsyncWriter.go

@@ -7,13 +7,50 @@ import (
 	"github.com/rs/zerolog/diode"
 )
 
-// 包装异步写入器
+// WrapAsyncWriter 包装一个同步写入器,使其支持异步写入功能
 //
-//	writer       io.Writer
-//	bufferSize   int           // 生产者缓冲区大小
-//	pollInterval time.Duration // 消费者轮询间隔
-//	alerter      diode.Alerter // 丢弃告警函数
+// 基于 zerolog/diode 库实现,提供高性能的异步日志写入能力,避免日志 I/O 操作阻塞主程序。
+//
+// 参数:
+//
+//	writer       - 底层的同步写入器(如文件写入器、控制台写入器等)
+//	bufferSize   - 生产者缓冲区大小,决定可以缓存的日志条目数量
+//	pollInterval - 消费者轮询间隔,控制异步写入的频率
+//	alerter      - 丢弃告警函数,当缓冲区满时丢弃日志条目的回调函数
+//
+// 返回值:
+//
+//	io.Writer - 包装后的异步写入器,符合标准 io.Writer 接口
+//
+// 工作原理:
+//  1. 生产者(主程序)将日志写入缓冲区,立即返回,不等待 I/O 完成
+//  2. 消费者(后台线程)定期轮询缓冲区,将日志批量写入底层写入器
+//  3. 当缓冲区满时,根据配置决定是阻塞还是丢弃日志
+//
+// 性能优势:
+//   - 线程安全:多 goroutine 并发写入无需加锁
+//   - 无锁设计:使用无锁环形缓冲区,避免锁竞争
+//   - 无阻塞:主程序不会被日志 I/O 操作阻塞
+//
+// 使用示例:
+//
+//	// 创建基础的文件写入器
+//	fileWriter := FileRollingWriter("logs/app.log", 10, 30, 10, false)
+//
+//	// 包装为异步写入器
+//	asyncWriter := WrapAsyncWriter(fileWriter, 1000, time.Millisecond*100, nil)
+//
+//	// 将异步写入器传递给日志库使用
+//	logx.InitInfoWriter(asyncWriter)
+//
+// 配置建议:
+//   - 缓冲区大小:根据日志产生频率调整,通常 1000-10000 为宜
+//   - 轮询间隔:根据实时性要求调整,通常 10ms-1s
+//   - 告警函数:生产环境建议设置告警,监控日志丢弃情况
 func WrapAsyncWriter(writer io.Writer, bufferSize int, pollInterval time.Duration, alerter diode.Alerter) io.Writer {
-	// 线程安全、无锁、无阻塞
+	// 使用 diode.NewWriter 创建异步写入器
+	// diode 库提供了线程安全、无锁、无阻塞的异步写入实现
+	// 当主程序写入日志时,数据会被放入环形缓冲区并立即返回
+	// 后台线程会定期将缓冲区中的数据批量写入底层写入器
 	return diode.NewWriter(writer, bufferSize, pollInterval, alerter)
 }

+ 31 - 2
src/logx/tool/ZerologFormatTimestamp.go

@@ -6,18 +6,47 @@ import (
 	"github.com/rs/zerolog"
 )
 
-// ZerologFormatTimestamp ..
+// ZerologFormatTimestamp 创建一个 zerolog 时间戳格式化器,支持自定义时区
+//
+// 参数:
+//   loggingLocationName - 时区名称,例如 "Asia/Shanghai"、"America/New_York"
+//
+// 返回值:
+//   zerolog.Formatter - 符合 zerolog 格式化器接口的函数
+//
+// 功能说明:
+//   1. 将 zerolog 的默认时间戳格式转换为指定时区的本地时间
+//   2. 格式化输出为 "时区缩写 年-月-日 时:分:秒" 格式
+//   3. 在时间戳前添加回车换行符,使日志输出更美观
+//
+// 示例输出:
+//   "CST 2024-01-15 10:30:25"
+//   "EST 2024-01-14 21:30:25"
+//
+// 错误处理:
+//   - 如果时间解析失败,返回错误信息字符串
+//   - 如果时区加载失败,返回错误信息字符串
 func ZerologFormatTimestamp(loggingLocationName string) zerolog.Formatter {
 
 	return func(i interface{}) string {
+		// 解析 zerolog 的默认时间戳格式
 		t, err := time.Parse(zerolog.TimeFieldFormat, i.(string))
 		if err != nil {
 			return err.Error()
 		}
+
+		// 加载指定的时区
 		location, err := time.LoadLocation(loggingLocationName)
 		if err != nil {
 			return err.Error()
 		}
+
+		// 转换为指定时区的时间,并格式化为易读的字符串
+		// 格式说明:
+		//   "MST" - 时区缩写(如 CST、EST、PST)
+		//   "2006-01-02" - 年-月-日格式(Go 语言特殊日期格式)
+		//   "15:04:05" - 24小时制的时间格式
+		//   "\r\n" - 在时间戳前添加换行,使日志输出更清晰
 		return "\r\n" + t.In(location).Format("MST 2006-01-02 15:04:05")
 	}
-}
+}