1832 lines
47 KiB
Markdown
1832 lines
47 KiB
Markdown
# xlgo 框架使用指南
|
||
|
||
## 目录
|
||
|
||
1. [快速开始](#1-快速开始)
|
||
2. [配置管理](#2-配置管理)
|
||
3. [日志系统](#3-日志系统)
|
||
4. [数据库操作](#4-数据库操作)
|
||
5. [缓存与 Redis](#5-缓存与redis)
|
||
6. [请求处理](#6-请求处理)
|
||
7. [响应与错误处理](#7-响应与错误处理)
|
||
8. [中间件](#8-中间件)
|
||
9. [认证与授权](#9-认证与授权)
|
||
10. [文件存储](#10-文件存储)
|
||
11. [工具函数库](#11-工具函数库)
|
||
12. [实时通信](#12-实时通信)
|
||
13. [定时任务](#13-定时任务)
|
||
14. [链路追踪](#14-链路追踪)
|
||
15. [压缩解压](#15-压缩解压)
|
||
16. [测试工具](#16-测试工具)
|
||
17. [CLI 脚手架](#17-cli脚手架)
|
||
18. [最佳实践](#18-最佳实践)
|
||
|
||
---
|
||
|
||
## 1. 快速开始
|
||
|
||
> **环境要求**:xlgo 需要 **Go 1.25+**。本项目作为新框架不背负旧版本兼容包袱,可直接使用 Go 1.25 的新特性。
|
||
|
||
### 1.1 安装框架
|
||
|
||
```bash
|
||
# 创建新项目
|
||
xlgo new myproject
|
||
|
||
# 或手动安装
|
||
go get github.com/EthanCodeCraft/xlgo-core
|
||
```
|
||
|
||
### 1.2 项目结构
|
||
|
||
```
|
||
myproject/
|
||
├── config.yaml # 配置文件
|
||
├── main.go # 入口文件
|
||
├── handler/ # 请求处理器
|
||
├── model/ # 数据模型
|
||
├── repository/ # 数据仓库
|
||
├── service/ # 业务逻辑
|
||
├── middleware/ # 自定义中间件
|
||
├── public/ # 静态文件
|
||
├── logs/ # 日志目录
|
||
└── go.mod
|
||
```
|
||
|
||
### 1.3 最简示例
|
||
|
||
```go
|
||
package main
|
||
|
||
import (
|
||
xlgo "github.com/EthanCodeCraft/xlgo-core"
|
||
"github.com/EthanCodeCraft/xlgo-core/middleware"
|
||
"github.com/EthanCodeCraft/xlgo-core/response"
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
func main() {
|
||
// v1.0.2 起 xlgo.New 默认是轻量应用,不会自动初始化 MySQL/Redis/Storage,
|
||
// 也不会自动注册 /health 或 /swagger/* 路由。
|
||
// 通过 Option 显式启用所需组件即可。
|
||
app := xlgo.New(
|
||
xlgo.WithConfigPath("./config.yaml"),
|
||
xlgo.WithLogger(),
|
||
xlgo.WithDefaultRoutes(), // 同时启用 /health 与 /swagger/*
|
||
xlgo.WithMiddlewares(middleware.Logger(), middleware.CORS()),
|
||
)
|
||
|
||
// 通过 Engine 直接挂路由,或使用 xlgo.WithModules(...) 注册模块
|
||
app.GetRouter().GET("/hello", func(c *gin.Context) {
|
||
response.Success(c, gin.H{"message": "Hello World"})
|
||
})
|
||
|
||
if err := app.Run(); err != nil {
|
||
panic(err)
|
||
}
|
||
}
|
||
```
|
||
|
||
> 想要 batteries-included 体验,可改用 `xlgo.NewFullStack(...)` 或
|
||
> `xlgo.RunFullStack(...)`,它会一次性启用 Logger / MySQL / Redis / Storage /
|
||
> Wire / Health / Swagger / AutoMigrate 等组件。
|
||
|
||
**优雅关闭特性:**
|
||
- 监听系统信号(SIGINT、SIGTERM)
|
||
- 等待请求处理完成(最多 30 秒)
|
||
- 按顺序关闭各组件(限流器、数据库、Redis、日志)
|
||
|
||
---
|
||
|
||
## 2. 配置管理
|
||
|
||
### 2.1 配置文件结构
|
||
|
||
```yaml
|
||
# config.yaml
|
||
app:
|
||
name: "我的应用"
|
||
site_name: "my_app" # 站点别名(重要:多项目共用Redis时区分)
|
||
version: "1.0.0"
|
||
env: "dev" # dev/test/prod
|
||
debug: true
|
||
base_url: "http://localhost:8080"
|
||
|
||
server:
|
||
port: 8080
|
||
mode: development # development/production
|
||
|
||
database:
|
||
host: localhost
|
||
port: 3306
|
||
user: root
|
||
password: your_password
|
||
name: mydb
|
||
max_idle_conns: 10
|
||
max_open_conns: 100
|
||
|
||
redis:
|
||
host: localhost
|
||
port: 6379
|
||
password: ""
|
||
db: 0
|
||
|
||
jwt:
|
||
secret: your_jwt_secret_key
|
||
expire: "24h" # time.Duration,如 "24h"/"30m"
|
||
|
||
log:
|
||
dir: ./logs
|
||
max_size: 100 # MB
|
||
max_backups: 30
|
||
max_age: 30 # 天
|
||
compress: true
|
||
```
|
||
|
||
### 2.2 使用配置
|
||
|
||
```go
|
||
import "github.com/EthanCodeCraft/xlgo-core/config"
|
||
|
||
// 加载配置
|
||
cfg, err := config.Load("./config.yaml")
|
||
|
||
// 获取配置值
|
||
port := cfg.Server.Port
|
||
dbHost := cfg.Database.Host
|
||
|
||
// 获取站点别名(用于缓存键前缀)
|
||
siteName := cfg.GetSiteName()
|
||
|
||
// 判断环境
|
||
if cfg.IsProduction() {
|
||
gin.SetMode(gin.ReleaseMode)
|
||
}
|
||
|
||
// 热更新配置
|
||
config.LoadWithWatch("./config.yaml", func(newCfg *config.Config) {
|
||
// 配置变更时的回调
|
||
})
|
||
|
||
// 动态获取配置值
|
||
debug := config.GetBool("app.debug")
|
||
```
|
||
|
||
---
|
||
|
||
## 3. 日志系统
|
||
|
||
### 3.1 初始化日志
|
||
|
||
```go
|
||
import "github.com/EthanCodeCraft/xlgo-core/logger"
|
||
|
||
// 初始化
|
||
logger.Init(cfg)
|
||
|
||
// 程序退出前同步
|
||
// v1.0.2 起 Sync 返回 error,可按需处理
|
||
_ = logger.Sync()
|
||
```
|
||
|
||
### 3.2 日志级别
|
||
|
||
```go
|
||
// 基础日志
|
||
logger.Debug("调试信息")
|
||
logger.Info("普通信息")
|
||
logger.Warn("警告信息")
|
||
logger.Error("错误信息")
|
||
|
||
// 格式化日志
|
||
logger.Infof("用户 %s 登录成功", username)
|
||
logger.Warnf("请求失败: %v", err)
|
||
|
||
// 致命错误(会终止程序)—— 仅在业务进程入口(main 等)使用
|
||
// v1.0.2 起,框架内部已禁止使用 Fatal/Fatalf,初始化错误改为 error 返回
|
||
logger.Fatalf("数据库连接失败: %v", err)
|
||
|
||
// 结构化日志
|
||
logger.Info("用户登录",
|
||
zap.String("username", "admin"),
|
||
zap.Int("user_id", 123),
|
||
)
|
||
```
|
||
|
||
### 3.3 彩色控制台输出
|
||
|
||
`console` 包定位是**开发期彩色 stdout 工具**——跟 `fmt.Println` 同级,不写文件、不感知运行环境、不做任何隐式行为。
|
||
|
||
```go
|
||
import "github.com/EthanCodeCraft/xlgo-core/console"
|
||
|
||
console.Debug("调试信息") // 青色
|
||
console.Info("普通信息") // 白色
|
||
console.Success("成功信息") // 绿色
|
||
console.Warn("警告信息") // 黄色
|
||
console.Error("错误信息") // 红色
|
||
|
||
// 自定义配置
|
||
c := console.New(
|
||
console.WithColor(true),
|
||
console.WithTime(true),
|
||
console.WithCaller(true), // skip 可选,默认 2
|
||
console.WithLevel(console.LevelInfo),
|
||
)
|
||
c.Debug("此条不会输出,已被 LevelInfo 过滤")
|
||
c.Info("自定义输出")
|
||
```
|
||
|
||
#### console vs logger:怎么选?
|
||
|
||
| 维度 | `console` | `logger` |
|
||
|---|---|---|
|
||
| 定位 | 开发期肉眼调试 | 业务可观测性记录 |
|
||
| 输出目标 | stdout(彩色) | 文件 + stdout |
|
||
| 持久化 | ❌ | ✅ 滚动归档 |
|
||
| 结构化 | 文本 | JSON 字段 |
|
||
| 性能 | 一般 | zap 高性能 |
|
||
| 默认级别 | `LevelDebug`(全开) | dev=Debug / prod=Info |
|
||
| 适用场景 | 临时打印、开发联调 | 用户登录、订单事件、审计日志 |
|
||
|
||
**简单判断**:
|
||
|
||
- 这条信息上线后想留 → 用 `logger`
|
||
- 这条信息上线就该消失 → 用 `console`,并在 main 中显式切到高级别
|
||
|
||
#### 上线前显式收紧 console 输出
|
||
|
||
```go
|
||
func main() {
|
||
cfg, _ := config.Load("./config.yaml")
|
||
|
||
// 显式选择:生产期 console 只看 Warn / Error
|
||
if cfg.IsProduction() {
|
||
console.SetLevel(console.LevelWarn)
|
||
}
|
||
|
||
// 或者完全静默 console(业务全靠 logger)
|
||
// console.SetLevel(console.LevelSilent)
|
||
|
||
app := xlgo.New(...)
|
||
app.Run()
|
||
}
|
||
```
|
||
|
||
> **注意**:xlgo 不会自动根据 `app.env` 切换 console 级别——选择权完全在调用方。
|
||
> 我们认为隐式切换会带来"开发期看到的 / 生产期看到的"行为不一致,调试体验更糟。
|
||
|
||
---
|
||
|
||
## 4. 数据库操作
|
||
|
||
xlgo 基于 GORM,驱动由配置 `database.driver` 决定。v1.0.2 起 GORM 方言通过 **可插拔注册表** 管理:内置 `mysql`(默认)与 `postgres`(含 `postgresql`、`pg` 别名),应用可通过 `database.RegisterDialect` 接入任意 GORM 驱动;`database.dsn` 字段始终可用于手写连接串。
|
||
|
||
### 4.1 初始化数据库
|
||
|
||
```go
|
||
import "github.com/EthanCodeCraft/xlgo-core/database"
|
||
|
||
// 初始化数据库(驱动由配置决定,等价于 database.DefaultManager.Load().InitDB(cfg))
|
||
database.InitDB(cfg)
|
||
|
||
// 关闭全部连接(含从库)
|
||
defer database.CloseAll()
|
||
|
||
// 获取数据库实例
|
||
db := database.GetDB() // 主库
|
||
read := database.GetReadDB() // 从库(无从库时回退主库)
|
||
```
|
||
|
||
### 4.1.1 主从读写分离
|
||
|
||
```go
|
||
// 从库 DSN 列表需与主库驱动匹配
|
||
database.InitDBWithReplicas(cfg, []string{
|
||
"root:pass@tcp(slave1:3306)/db",
|
||
"root:pass@tcp(slave2:3306)/db",
|
||
})
|
||
|
||
// 选择策略:默认 Random,可换成 RoundRobin
|
||
database.SetReplicaPicker(&database.RoundRobinPicker{})
|
||
|
||
// 强制使用主库(事务、需要实时数据的场景)
|
||
ctx := database.UseMaster(context.Background())
|
||
database.GetDBFromContext(ctx).Find(&users)
|
||
|
||
// 强制使用从库(报表查询)
|
||
ctx = database.UseReplica(context.Background())
|
||
database.GetDBFromContext(ctx).Find(&reports)
|
||
```
|
||
|
||
### 4.1.2 实例化 Manager
|
||
|
||
需要多套数据库连接(如平台库 + 租户库)时,`database.NewManager(cfg)` 创建独立管理器,互不影响:
|
||
|
||
```go
|
||
mgr := database.NewManager(cfg)
|
||
if err := mgr.Open(context.Background()); err != nil {
|
||
return err
|
||
}
|
||
defer mgr.Close()
|
||
|
||
mgr.SetPicker(&database.RoundRobinPicker{})
|
||
db := mgr.FromContext(ctx)
|
||
```
|
||
|
||
`database.SetDefaultManager(m)` 会把新 manager 提升为全局默认,并关闭被替换的旧 manager,避免连接池泄漏。需要失败回滚或延迟释放旧资源时,请使用 `database.SwapDefaultManager(m)` 并自行关闭其返回的旧 manager。
|
||
|
||
### 4.1.3 注册自定义 GORM 驱动
|
||
|
||
`database.RegisterDialect` 一次注册即让 `database.driver: <name>` 生效,DSN 构建器同步登记到 `config` 包:
|
||
|
||
```go
|
||
import (
|
||
"github.com/EthanCodeCraft/xlgo-core/config"
|
||
"github.com/EthanCodeCraft/xlgo-core/database"
|
||
"gorm.io/driver/sqlite"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
func init() {
|
||
database.RegisterDialect(database.DialectSpec{
|
||
Name: "sqlite",
|
||
Aliases: []string{"sqlite3"},
|
||
Dialector: func(dsn string) gorm.Dialector { return sqlite.Open(dsn) },
|
||
DSN: func(c *config.DatabaseConfig) string { return c.Name }, // Name 当作文件路径
|
||
})
|
||
}
|
||
```
|
||
|
||
之后只需把配置改成 `database.driver: sqlite` 即可。SQL Server / ClickHouse / TiDB 等同理。
|
||
|
||
诊断接口:
|
||
|
||
- `database.RegisteredDialects()` - 列出已注册驱动(含别名)
|
||
- `database.LookupDialect(name)` - 检查某驱动是否可用
|
||
- `config.RegisteredDrivers()` - 列出已登记的 DSN 构建器
|
||
|
||
未注册的驱动会回退到 MySQL 以保持向后兼容。
|
||
|
||
### 4.2 定义模型
|
||
|
||
```go
|
||
import "github.com/EthanCodeCraft/xlgo-core/model"
|
||
|
||
type User struct {
|
||
model.BaseModel // 包含 ID, CreatedAt, UpdatedAt
|
||
Username string `gorm:"size:50;unique" json:"username"`
|
||
Email string `gorm:"size:100" json:"email"`
|
||
Password string `gorm:"size:255" json:"-"`
|
||
Status int `gorm:"default:1" json:"status"`
|
||
}
|
||
|
||
func (User) TableName() string {
|
||
return "users"
|
||
}
|
||
```
|
||
|
||
### 4.3 使用 Repository(扩展版)
|
||
|
||
```go
|
||
import "github.com/EthanCodeCraft/xlgo-core/repository"
|
||
|
||
// 创建仓库
|
||
userRepo := repository.NewBaseRepo[model.User](database.GetDB())
|
||
|
||
// 连接路由(v1.1.1+ 修复 H6c):
|
||
// - 读操作(FindByID/FindAll/FindPage/FindWhere/Count/Exists/...)默认路由到从库,
|
||
// 支持 database.UseMaster(ctx)/UseReplica(ctx) 显式路由;未配置从库时回退主库。
|
||
// - 写操作(Create/Update/Delete/*Batch/Restore/...)始终走主库,不路由到只读从库。
|
||
// - DefaultManager 未初始化(如单测注入 sqlite)时回退到 NewBaseRepo 传入的 db。
|
||
|
||
// 基础 CRUD
|
||
user, err := userRepo.FindByID(ctx, 1)
|
||
users, err := userRepo.FindAll(ctx)
|
||
users, err := userRepo.FindByIDs(ctx, []uint{1, 2, 3})
|
||
err := userRepo.Create(ctx, &user)
|
||
err := userRepo.Update(ctx, &user) // Save 全列覆写(含零值),见下方 UpdateFields
|
||
err := userRepo.Delete(ctx, 1) // T 含 gorm.Model 时软删除,否则硬删除
|
||
|
||
// 局部更新(推荐,H6a):避免 Save 全列覆写丢失更新/零值不可辨
|
||
err := userRepo.UpdateFields(ctx, &model.User{Name: "new"}, "id = ?", id) // struct 仅更新非零字段
|
||
err := userRepo.UpdateFields(ctx, map[string]any{"status": 0}, "id = ?", id) // map 可显式置零
|
||
|
||
// 统计数量
|
||
count, err := userRepo.Count(ctx)
|
||
count, err := userRepo.CountWhere(ctx, "status = ?", 1)
|
||
|
||
// 分页查询(内置)
|
||
result, err := userRepo.FindPage(ctx, 1, 20)
|
||
// result.Items - 数据列表
|
||
// result.Total - 总数
|
||
// result.Page - 当前页
|
||
// result.PageSize - 每页数量
|
||
|
||
// 条件分页查询
|
||
result, err := userRepo.FindPageWhere(ctx, 1, 20, "status = ?", 1)
|
||
|
||
// 分页并排序
|
||
result, err := userRepo.FindPageOrdered(ctx, 1, 20, "created_at DESC")
|
||
|
||
// 条件查询
|
||
user, err := userRepo.FindOne(ctx, "email = ?", "test@example.com")
|
||
users, err := userRepo.FindWhere(ctx, "status = ?", 1)
|
||
users, err := userRepo.FindWhereOrdered(ctx, "created_at DESC", "status = ?", 1)
|
||
|
||
// 排序查询
|
||
users, err := userRepo.FindOrdered(ctx, "created_at DESC", 10)
|
||
|
||
// 批量操作
|
||
err := userRepo.CreateBatch(ctx, users)
|
||
err := userRepo.UpdateBatch(ctx, []uint{1, 2}, "status", 2)
|
||
err := userRepo.DeleteBatch(ctx, []uint{1, 2, 3})
|
||
|
||
// 存在性检查
|
||
exists, err := userRepo.Exists(ctx, 1)
|
||
exists, err := userRepo.ExistsWhere(ctx, "email = ?", "test@example.com")
|
||
|
||
// 软删除恢复
|
||
err := userRepo.Restore(ctx, 1)
|
||
err := userRepo.RestoreBatch(ctx, []uint{1, 2})
|
||
|
||
// 查询已删除的记录
|
||
deletedUsers, err := userRepo.FindDeleted(ctx)
|
||
|
||
// 链式查询(灵活构建;QueryBuilder 为单次使用、非并发安全)
|
||
users, err := userRepo.NewQueryBuilder().
|
||
Where("status = ?", 1).
|
||
Where("role = ?", "admin").
|
||
Order("created_at DESC").
|
||
Limit(10).
|
||
Find(ctx)
|
||
|
||
// 链式分页查询
|
||
result, err := userRepo.NewQueryBuilder().
|
||
Where("status = ?", 1).
|
||
Order("created_at DESC").
|
||
Page(ctx, 1, 20)
|
||
|
||
// 事务支持
|
||
err := userRepo.WithTransaction(ctx, func(txRepo *repository.BaseRepo[model.User]) error {
|
||
// 创建用户
|
||
if err := txRepo.Create(ctx, &user); err != nil {
|
||
return err
|
||
}
|
||
// 更新关联数据
|
||
return txRepo.Update(ctx, &profile)
|
||
})
|
||
|
||
// 跨层/跨 repo 事务 join(H6c):把外层事务注入 ctx 后传给任意 repo 方法
|
||
err := database.TransactionWithContext(ctx, func(tx *gorm.DB) error {
|
||
ctx2 := database.WithTx(ctx, tx)
|
||
if err := userRepo.Create(ctx2, &user); err != nil {
|
||
return err
|
||
}
|
||
return otherRepo.Update(ctx2, &other) // 同一事务
|
||
})
|
||
```
|
||
|
||
### 4.4 Model 基础模型
|
||
|
||
```go
|
||
import "github.com/EthanCodeCraft/xlgo-core/model"
|
||
|
||
// BaseModel 包含 ID、CreatedAt、UpdatedAt、DeletedAt
|
||
type User struct {
|
||
model.BaseModel // 自动包含 ID、时间戳、软删除
|
||
Username string `gorm:"size:50;unique" json:"username"`
|
||
Email string `gorm:"size:100" json:"email"`
|
||
Status int `gorm:"default:1" json:"status"`
|
||
}
|
||
|
||
// BaseModel 字段说明
|
||
// ID uint - 主键
|
||
// CreatedAt time.Time - 创建时间
|
||
// UpdatedAt time.Time - 更新时间
|
||
// DeletedAt gorm.DeletedAt - 软删除时间(不返回给前端)
|
||
```
|
||
|
||
---
|
||
|
||
## 5. 缓存与 Redis
|
||
|
||
### 5.1 初始化缓存
|
||
|
||
```go
|
||
import "github.com/EthanCodeCraft/xlgo-core/cache"
|
||
|
||
// 初始化(通常在应用启动时)
|
||
cache.Init()
|
||
|
||
// 获取缓存实例
|
||
c := cache.GetCache()
|
||
```
|
||
|
||
### 5.2 缓存操作
|
||
|
||
```go
|
||
// 设置缓存
|
||
c.Set(ctx, "user:1", userData, 30*time.Minute)
|
||
|
||
// 获取缓存
|
||
var user User
|
||
if c.Get(ctx, "user:1", &user) {
|
||
// 缓存命中
|
||
}
|
||
|
||
// 删除缓存
|
||
c.Delete(ctx, "user:1")
|
||
|
||
// 批量删除(按模式)
|
||
c.DeleteByPattern(ctx, "user:*")
|
||
|
||
// 检查是否存在
|
||
exists := c.Exists(ctx, "user:1")
|
||
```
|
||
|
||
### 5.3 键名前缀管理(多站点共用 Redis)
|
||
|
||
```go
|
||
import "github.com/EthanCodeCraft/xlgo-core/cache"
|
||
|
||
// 自动从配置读取 site_name 作为前缀
|
||
// config.yaml: app.site_name: "my_app"
|
||
|
||
cache.K("user:1") // → "cache:my_app:user:1"
|
||
cache.KTemp("token") // → "temp:my_app:token"
|
||
cache.KPerm("config") // → "perm:my_app:config"
|
||
cache.KLock("order:123") // → "lock:my_app:order:123"
|
||
cache.KCounter("visit") // → "counter:my_app:visit"
|
||
cache.KSession("sid") // → "session:my_app:sid"
|
||
|
||
// 使用带前缀的缓存
|
||
c.Set(ctx, cache.K("user:1"), userData, ttl)
|
||
c.Get(ctx, cache.K("user:1"), &user)
|
||
```
|
||
|
||
### 5.4 分布式锁(安全增强版)
|
||
|
||
**使用 Lua 脚本 + UUID Token,只有锁的持有者才能释放锁:**
|
||
|
||
```go
|
||
// 安全加锁(返回 LockToken)
|
||
token, err := cache.NewLock(ctx, cache.KLock("pay:123"), 30*time.Second)
|
||
if token != nil {
|
||
defer cache.Unlock(ctx, token) // 只有持有者能释放
|
||
// 执行业务逻辑
|
||
}
|
||
|
||
// 带重试的锁
|
||
token, err := cache.TryLock(ctx, key, ttl, 100*time.Millisecond, 10)
|
||
if token != nil {
|
||
defer cache.Unlock(ctx, token)
|
||
}
|
||
|
||
// 自动管理锁(简单场景)
|
||
err := cache.WithLock(ctx, key, ttl, func(ctx context.Context) error {
|
||
return nil
|
||
})
|
||
|
||
// 自动续期锁(长任务场景)
|
||
err := cache.WithLockAutoExtend(ctx, key, 30*time.Second, 10*time.Second, func(ctx context.Context) error {
|
||
// 执行时间超过 TTL 时自动续期
|
||
return nil
|
||
})
|
||
|
||
// 续期锁(手动续期)
|
||
cache.ExtendLock(ctx, token, 30*time.Second)
|
||
|
||
// 检查锁是否被占用(Redis 不可用时返回 ErrRedisNotReady,可经
|
||
// errors.Is(err, cache.ErrRedisNotReady) 与"锁未占用"区分)
|
||
locked, err := cache.IsLocked(ctx, key)
|
||
|
||
// 强制释放锁(管理场景;Redis 不可用时返回 ErrRedisNotReady)
|
||
if err := cache.ForceUnlock(ctx, key); err != nil {
|
||
// 处理 Redis 不可用
|
||
}
|
||
```
|
||
|
||
**安全特性:**
|
||
- Lua 脚本保证原子性操作
|
||
- UUID Token 标识锁的持有者
|
||
- 只有持有者才能释放锁(防止误释放)
|
||
- 支持续期(长任务不会因锁过期而中断)
|
||
|
||
### 5.5 计数器
|
||
|
||
```go
|
||
// 自增
|
||
n, err := cache.Incr(ctx, cache.KCounter("page_view"))
|
||
|
||
// 指定增量
|
||
n, err := cache.IncrBy(ctx, key, 10)
|
||
|
||
// 自减
|
||
n, err := cache.Decr(ctx, key)
|
||
|
||
// 设置过期时间
|
||
cache.SetExpire(ctx, key, 1*time.Hour)
|
||
|
||
// 获取剩余时间
|
||
ttl, err := cache.GetTTL(ctx, key)
|
||
```
|
||
|
||
---
|
||
|
||
## 6. 请求处理
|
||
|
||
### 6.1 参数获取
|
||
|
||
```go
|
||
import "github.com/EthanCodeCraft/xlgo-core/handler"
|
||
|
||
// 类型安全的参数获取(推荐)
|
||
|
||
// Query参数
|
||
page := handler.QueryInt(c, "page", 1) // Query参数 → int
|
||
id := handler.QueryInt64(c, "id", 0) // Query参数 → int64
|
||
price := handler.QueryFloat64(c, "price", 0.0) // Query参数 → float64
|
||
enabled := handler.QueryBool(c, "enabled", false) // Query参数 → bool
|
||
|
||
// 路径参数(RESTful API)
|
||
id := handler.PathInt(c, "id", 0) // 路径参数 → int
|
||
id := handler.PathInt64(c, "id", 0) // 路径参数 → int64
|
||
id := handler.PathUint64(c, "id", 0) // 路径参数 → uint64
|
||
|
||
// 表单参数(POST提交)
|
||
count := handler.FormInt(c, "count", 0) // 表单参数 → int
|
||
id := handler.FormInt64(c, "id", 0) // 表单参数 → int64
|
||
id := handler.FormUint64(c, "id", 0) // 表单参数 → uint64
|
||
price := handler.FormFloat64(c, "price", 0.0) // 表单参数 → float64
|
||
enabled := handler.FormBool(c, "enabled", false) // 表单参数 → bool
|
||
name := handler.FormString(c, "name", "") // 表单参数 → string
|
||
|
||
// 分页参数
|
||
page, pageSize := handler.GetPage(c) // 默认 page=1, pageSize=20
|
||
|
||
// 路径ID
|
||
id, ok := handler.GetIDFromPath(c, "id")
|
||
|
||
// 绑定请求
|
||
var req LoginRequest
|
||
handler.BindJSON(c, &req)
|
||
```
|
||
|
||
### 6.2 请求验证
|
||
|
||
```go
|
||
import "github.com/EthanCodeCraft/xlgo-core/validation"
|
||
|
||
type RegisterRequest struct {
|
||
Username string `json:"username" label:"用户名" validate:"required,min=3,max=20" msg_required:"用户名不能为空"`
|
||
Password string `json:"password" label:"密码" validate:"required,password" msg_required:"密码不能为空"`
|
||
Email string `json:"email" label:"邮箱" validate:"required,email" msg_required:"邮箱不能为空"`
|
||
Phone string `json:"phone" label:"手机号" validate:"omitempty,phone"`
|
||
}
|
||
|
||
// 绑定并验证
|
||
errors, ok := validation.ShouldBindAndValidate(c, &req)
|
||
if !ok {
|
||
response.Fail(c, errors.Error())
|
||
return
|
||
}
|
||
```
|
||
|
||
### 6.3 验证规则
|
||
|
||
| 规则 | 说明 |
|
||
| ---------- | ------------------------------------- |
|
||
| `required` | 必填 |
|
||
| `min=3` | 最小长度 |
|
||
| `max=20` | 最大长度 |
|
||
| `email` | 邮箱格式 |
|
||
| `phone` | 手机号格式 |
|
||
| `phone_strict` | 手机号严格校验(验证运营商号段) |
|
||
| `password` | 密码强度(8-72 字节,需含大写+小写+数字) |
|
||
| `username` | 字母开头,3-20 位字母/数字/下划线 |
|
||
| `url` | URL 格式 |
|
||
| `ip` | IP 地址格式 |
|
||
|
||
---
|
||
|
||
## 7. 响应与错误处理
|
||
|
||
### 7.1 统一响应格式
|
||
|
||
```json
|
||
{
|
||
"code": 1,
|
||
"msg": "操作成功",
|
||
"data": {},
|
||
"request_id": "abc123"
|
||
}
|
||
```
|
||
|
||
### 7.2 成功响应
|
||
|
||
```go
|
||
import "github.com/EthanCodeCraft/xlgo-core/response"
|
||
|
||
// 基础成功响应
|
||
response.Success(c, gin.H{"user": user})
|
||
|
||
// 自定义消息
|
||
response.SuccessWithMsg(c, "注册成功", gin.H{"user_id": 123})
|
||
|
||
// 分页响应
|
||
response.Page(c, users, total, page, pageSize)
|
||
```
|
||
|
||
### 7.3 错误响应
|
||
|
||
```go
|
||
// 基础失败
|
||
response.Fail(c, "参数错误")
|
||
|
||
// 自定义错误码
|
||
response.FailWithCode(c, response.CodeUserNotFound, "用户不存在")
|
||
|
||
// 使用预定义错误
|
||
response.FailWithError(c, response.ErrUserNotFound)
|
||
|
||
// 带详细信息
|
||
response.FailWithDetail(c, response.ErrPasswordWrong, "连续错误3次将锁定")
|
||
```
|
||
|
||
### 7.4 预定义错误码
|
||
|
||
> 注:参数错误等业务化错误码请由业务项目自行定义,框架不再内置 `ErrInvalidParams`。
|
||
> 推荐使用 `response.NewError(40001, "参数错误")` 在业务侧统一管理。
|
||
|
||
| 错误 | 码 | 说明 |
|
||
| ------------------ | ------ | ---------- |
|
||
| `ErrUnauthorized` | 401 | 未授权 |
|
||
| `ErrForbidden` | 403 | 无权限访问 |
|
||
| `ErrNotFound` | 404 | 资源不存在 |
|
||
| `ErrRateLimit` | 429 | 请求过于频繁 |
|
||
| `ErrServerError` | 500 | 服务器错误 |
|
||
| `ErrUserNotFound` | 10001 | 用户不存在 |
|
||
| `ErrPasswordWrong` | 10004 | 密码错误 |
|
||
|
||
### 7.5 特殊响应
|
||
|
||
```go
|
||
// 文件下载
|
||
response.Download(c, "report.xlsx", fileData)
|
||
|
||
// HTML响应
|
||
response.HTML(c, "<html>...</html>")
|
||
|
||
// 页面跳转
|
||
response.Redirect(c, 302, "https://example.com")
|
||
```
|
||
|
||
---
|
||
|
||
## 8. 中间件
|
||
|
||
### 8.1 内置中间件
|
||
|
||
```go
|
||
import "github.com/EthanCodeCraft/xlgo-core/middleware"
|
||
|
||
// 日志中间件(默认配置)
|
||
r.Use(middleware.Logger())
|
||
|
||
// API 专用日志(记录请求体)
|
||
r.Use(middleware.LoggerForAPI())
|
||
|
||
// 调试日志(最详细,记录请求体和响应体)
|
||
r.Use(middleware.LoggerForDebug())
|
||
|
||
// 最简日志(只记录基本信息)
|
||
r.Use(middleware.LoggerMinimal())
|
||
|
||
// 自定义日志配置
|
||
r.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
|
||
LogRequestBody: true, // 记录请求体
|
||
LogResponseBody: false, // 不记录响应体
|
||
MaxBodyLength: 2048, // 最大记录长度
|
||
SkipPaths: []string{"/health"}, // 跳过路径
|
||
SlowRequestThreshold: 500 * time.Millisecond, // 慢请求阈值
|
||
}))
|
||
|
||
// CORS跨域
|
||
r.Use(middleware.CORS())
|
||
|
||
// 请求ID(追踪)
|
||
r.Use(middleware.RequestID())
|
||
|
||
// Panic恢复
|
||
r.Use(middleware.Recover())
|
||
|
||
// 认证中间件
|
||
r.Use(middleware.AuthRequired())
|
||
|
||
// 自定义用户类型权限
|
||
r.Use(middleware.RequireUserTypes("tenant_admin", "platform_admin"))
|
||
|
||
// 自定义角色权限
|
||
r.Use(middleware.RequireRoles("owner", "manager"))
|
||
|
||
// 默认快捷权限(super_admin/admin/staff 只是默认常量)
|
||
r.Use(middleware.AdminRequired())
|
||
|
||
// CSRF防护
|
||
r.Use(middleware.CSRF())
|
||
```
|
||
|
||
### 8.2 CORS 配置(支持配置文件)
|
||
|
||
```yaml
|
||
# config.yaml 添加 CORS 配置
|
||
cors:
|
||
allowed_origins:
|
||
- "https://example.com"
|
||
- "https://app.example.com"
|
||
- "*.example.com" # 支持通配符
|
||
allowed_methods:
|
||
- "GET"
|
||
- "POST"
|
||
- "PUT"
|
||
- "DELETE"
|
||
allowed_headers:
|
||
- "Content-Type"
|
||
- "Authorization"
|
||
allow_credentials: true
|
||
max_age: 86400 # 预检请求缓存时间(秒)
|
||
```
|
||
|
||
```go
|
||
// 使用配置文件的 CORS
|
||
r.Use(middleware.CORS())
|
||
|
||
// 指定域名列表
|
||
r.Use(middleware.CORSWithOrigins([]string{
|
||
"https://example.com",
|
||
"https://app.example.com",
|
||
}))
|
||
|
||
// 允许所有来源(仅开发环境)
|
||
r.Use(middleware.CORSWithWildcard())
|
||
|
||
// API 专用 CORS(不允许凭证)
|
||
r.Use(middleware.CORSForAPI())
|
||
```
|
||
|
||
**生产环境 CORS 配置建议:**
|
||
- 必须配置具体的 `allowed_origins`
|
||
- 不使用 `*` 通配符
|
||
- 根据需要配置 `allow_credentials`
|
||
|
||
### 8.3 CSRF 防护
|
||
|
||
```go
|
||
// Cookie模式(Web应用)
|
||
r.Use(middleware.CSRF())
|
||
|
||
// 双重提交Cookie模式(无状态)
|
||
r.Use(middleware.DoubleSubmitCookie())
|
||
|
||
// API模式
|
||
r.Use(middleware.CSRFForAPI())
|
||
|
||
// 获取Token
|
||
token := middleware.GetCSRFToken(c)
|
||
```
|
||
|
||
### 8.3 限流(支持 Redis 分布式限流)
|
||
|
||
```go
|
||
// 初始化限流器
|
||
middleware.InitRateLimiters()
|
||
|
||
// 内存限流(单实例)
|
||
r.POST("/login", middleware.LoginRateLimit(), handler.Login) // 每分钟10次
|
||
r.POST("/upload", middleware.UploadRateLimit(), handler.Upload) // 每分钟20次
|
||
r.Use(middleware.APIRateLimit()) // 每分钟100次
|
||
|
||
// 自定义内存限流
|
||
r.Use(middleware.CustomRateLimit(50, time.Minute)) // 每分钟50次
|
||
|
||
// Redis 分布式限流(多实例共享)
|
||
r.Use(middleware.RedisRateLimit("api_limit", 100)) // 每分钟100次(fail-open:Redis 故障时放行)
|
||
r.Use(middleware.LoginRedisRateLimit()) // 登录限流(fail-closed:Redis 故障时拒绝,防爆破)
|
||
r.Use(middleware.APIRedisRateLimit()) // API限流(fail-open)
|
||
r.Use(middleware.UploadRedisRateLimit()) // 上传限流(fail-open)
|
||
|
||
// 自定义 Redis 限流
|
||
r.Use(middleware.CustomRedisRateLimit("custom", 50, time.Minute)) // fail-open
|
||
r.Use(middleware.CustomRedisRateLimitFailClosed("sensitive", 50, time.Minute)) // fail-closed(安全场景)
|
||
|
||
// 自定义标识限流(如按用户ID)
|
||
r.Use(middleware.RedisRateLimitWithIdentifier("user_limit", 100, func(c *gin.Context) string {
|
||
return fmt.Sprintf("user:%d", middleware.GetUserID(c))
|
||
}))
|
||
|
||
// 停止限流器(应用关闭时)
|
||
defer middleware.StopRateLimiters()
|
||
```
|
||
|
||
> **fail-open vs fail-closed(H4c)**:Redis 限流器在 Redis 故障时有两种策略——
|
||
> - **fail-open**(`RedisRateLimit`/`APIRedisRateLimit`/`UploadRedisRateLimit`/`CustomRedisRateLimit`,默认):Redis 故障时放行,避免影响业务,但限流静默失效。
|
||
> - **fail-closed**(`RedisRateLimitFailClosed`/`LoginRedisRateLimit`/`CustomRedisRateLimitFailClosed`):Redis 故障时拒绝(HTTP 503),防限流静默失效。**安全敏感场景(登录防爆破、敏感操作)必须用 fail-closed**。
|
||
> `RedisRateLimiter` 可经 `NewRedisRateLimiterFailClosed` 构造或 `SetFailClosed(true)` 切换策略。
|
||
|
||
**内存限流 vs Redis 限流:**
|
||
- 内存限流:单实例使用,简单高效
|
||
- Redis 限流:多实例共享,滑动窗口算法,分布式场景必需
|
||
|
||
### 8.4 路由架构
|
||
|
||
框架提供灵活的路由系统,支持模块化、版本化 API 和中间件分组。
|
||
|
||
#### 8.4.1 模块化路由
|
||
|
||
```go
|
||
import "github.com/EthanCodeCraft/xlgo-core/router"
|
||
|
||
// 方式一:实现 Module 接口
|
||
type UserModule struct{}
|
||
|
||
func (m *UserModule) Name() string { return "user" }
|
||
func (m *UserModule) Register(r *gin.RouterGroup) {
|
||
r.GET("/users", listUsers)
|
||
r.POST("/users", createUser)
|
||
r.GET("/users/:id", getUser)
|
||
}
|
||
|
||
// 方式二:函数式注册
|
||
router.ModuleFunc(func(r *gin.RouterGroup) {
|
||
r.GET("/users", listUsers)
|
||
})
|
||
|
||
// 使用
|
||
app := xlgo.New(
|
||
xlgo.WithModules(&UserModule{}),
|
||
)
|
||
```
|
||
|
||
#### 8.4.2 版本化 API
|
||
|
||
```go
|
||
// 单版本
|
||
v1 := router.NewVersion("v1", "/api/v1")
|
||
v1.AddModuleFunc("user", func(r *gin.RouterGroup) {
|
||
r.GET("/users", listUsersV1)
|
||
})
|
||
|
||
// 多版本共存
|
||
v1 := router.NewVersion("v1", "/api/v1")
|
||
v1.AddModuleFunc("user", func(r *gin.RouterGroup) {
|
||
r.GET("/users", listUsersV1) // 旧版本
|
||
})
|
||
|
||
v2 := router.NewVersion("v2", "/api/v2")
|
||
v2.AddModuleFunc("user", func(r *gin.RouterGroup) {
|
||
r.GET("/users", listUsersV2) // 新版本
|
||
r.GET("/users/:id/profile", getProfile) // 新增功能
|
||
})
|
||
|
||
app := xlgo.New(xlgo.WithVersions(v1, v2))
|
||
|
||
// 结果:
|
||
// GET /api/v1/users -> listUsersV1
|
||
// GET /api/v2/users -> listUsersV2
|
||
```
|
||
|
||
#### 8.4.3 版本级中间件
|
||
|
||
```go
|
||
v1 := router.NewVersion("v1", "/api/v1",
|
||
middleware.CustomRateLimit(100, time.Minute), // 版本级限流
|
||
middleware.AuthRequired(), // 版本级认证
|
||
)
|
||
v1.AddModuleFunc("user", userRoutes)
|
||
|
||
// 该版本所有路由自动应用限流和认证
|
||
```
|
||
|
||
#### 8.4.4 中间件分组
|
||
|
||
```go
|
||
// 创建分组
|
||
authGroup := router.NewMiddlewareGroup("auth",
|
||
middleware.AuthRequired(),
|
||
middleware.RequireUserTypes("tenant_admin", "platform_admin"),
|
||
)
|
||
|
||
publicGroup := router.NewMiddlewareGroup("public",
|
||
middleware.CustomRateLimit(1000, time.Minute),
|
||
)
|
||
|
||
// 注册分组
|
||
registry := router.NewRegistry(engine)
|
||
registry.RegisterMiddlewareGroup(authGroup)
|
||
registry.RegisterMiddlewareGroup(publicGroup)
|
||
|
||
// 使用分组
|
||
userGroup := router.GroupWithMiddlewareGroup(engine, "/users", "auth")
|
||
userGroup.GET("/:id", getUser)
|
||
```
|
||
|
||
#### 8.4.5 RESTful CRUD
|
||
|
||
```go
|
||
registry.RegisterModuleFunc("product", func(r *gin.RouterGroup) {
|
||
rest := router.NewRESTful(r, "/products")
|
||
|
||
// 一行注册标准 CRUD
|
||
rest.CRUD(
|
||
listProducts, // GET /products
|
||
getProduct, // GET /products/:id
|
||
createProduct, // POST /products
|
||
updateProduct, // PUT /products/:id
|
||
deleteProduct, // DELETE /products/:id
|
||
)
|
||
})
|
||
|
||
// 部分 CRUD(只注册需要的)
|
||
rest := router.NewRESTful(r, "/articles")
|
||
rest.CRUD(listArticles, getArticle, createArticle, nil, nil)
|
||
```
|
||
|
||
#### 8.4.6 全局注册方式
|
||
|
||
```go
|
||
engine := gin.New()
|
||
router.Init(engine)
|
||
|
||
// 全局中间件
|
||
router.Use(middleware.CORS(), middleware.Logger())
|
||
|
||
// 注册模块
|
||
router.RegisterModule(&UserModule{})
|
||
router.RegisterModuleFunc("health", func(r *gin.RouterGroup) {
|
||
r.GET("/health", healthCheck)
|
||
})
|
||
|
||
// 注册版本
|
||
router.RegisterVersion(router.NewVersion("v1", "/api/v1"))
|
||
|
||
// 应用路由
|
||
router.Apply()
|
||
|
||
// 启动服务(如需优雅关闭与生命周期管理,改用 app := xlgo.New(...); app.Run())
|
||
engine.Run(":8080")
|
||
```
|
||
|
||
#### 8.4.7 默认路由
|
||
|
||
v1.0.2 起,`xlgo.New()` 默认是 **轻量应用**,不会自动注册任何默认路由。
|
||
按需通过下列 Option 显式启用:
|
||
|
||
```go
|
||
xlgo.New(
|
||
xlgo.WithHealthRoutes(), // 注册 /health
|
||
xlgo.WithSwaggerRoutes(), // 注册 /swagger/*any
|
||
// 或一步到位:
|
||
xlgo.WithDefaultRoutes(), // 同时注册 /health 与 /swagger/*any
|
||
)
|
||
```
|
||
|
||
也可以使用 `xlgo.NewFullStack(...)` / `xlgo.RunFullStack(...)` 启用全部默认组件。
|
||
|
||
> 生产环境建议关闭 Swagger,仅保留 `WithHealthRoutes()`,避免文档接口意外暴露。
|
||
|
||
---
|
||
|
||
## 9. 认证与授权
|
||
|
||
### 9.1 JWT 使用(黑名单优化版)
|
||
|
||
**使用 JTI(JWT ID)替代完整 Token 存储黑名单,大幅节省 Redis 内存:**
|
||
|
||
```go
|
||
import "github.com/EthanCodeCraft/xlgo-core/jwt"
|
||
|
||
// 生成Token(自动包含唯一 JTI)
|
||
token, err := jwt.GenerateToken(userID, username, "admin", "admin")
|
||
|
||
// 解析Token
|
||
claims, err := jwt.ParseToken(tokenString)
|
||
|
||
// 使Token失效(使用 JTI,内存占用约 30 字节)
|
||
jwt.InvalidateToken(tokenString)
|
||
|
||
// 获取 Token 的 JTI
|
||
jti, err := jwt.GetJTI(tokenString)
|
||
|
||
// 直接通过 JTI 撤销 Token
|
||
jwt.InvalidateTokenByID(jti, expiryTime)
|
||
|
||
// 自定义过期时间
|
||
token, err := jwt.GenerateTokenWithCustomExpiry(userID, username, "admin", "admin", 3600)
|
||
|
||
// 刷新Token(旧Token自动加入黑名单)
|
||
newToken, err := jwt.RefreshToken(oldToken)
|
||
|
||
// 获取已过期Token的信息(不验证过期)
|
||
claims, err := jwt.GetClaimsFromToken(tokenString)
|
||
```
|
||
|
||
**黑名单优化说明:**
|
||
- 每个Token自动生成唯一JTI(约24字节)
|
||
- 黑名单键名:`jwt_bl:{jti}`(而非完整Token)
|
||
- 内存节省:从数百字节降到约30字节每条记录
|
||
|
||
### 9.2 获取用户信息
|
||
|
||
```go
|
||
// 一次性取出全部认证信息(v1.0.2 推荐)
|
||
if user, ok := middleware.GetAuthUser(c); ok {
|
||
_ = user.UserID
|
||
_ = user.Username
|
||
_ = user.Role
|
||
_ = user.UserType
|
||
}
|
||
|
||
// 单字段便捷函数(与旧版兼容)
|
||
userID := middleware.GetUserID(c)
|
||
username := middleware.GetUsername(c)
|
||
userType := middleware.GetUserType(c) // super_admin/admin/staff 等,由业务定义
|
||
```
|
||
|
||
### 9.3 密码加密
|
||
|
||
```go
|
||
import "github.com/EthanCodeCraft/xlgo-core/validation"
|
||
|
||
// 加密密码
|
||
hash, err := validation.HashPassword("password123")
|
||
|
||
// 验证密码
|
||
if validation.CheckPassword(hash, "password123") {
|
||
// 密码正确
|
||
}
|
||
|
||
// 验证并自动升级成本
|
||
match, needUpgrade, newHash, err := validation.CheckPasswordAndUpgrade(hash, password, 12)
|
||
```
|
||
|
||
---
|
||
|
||
## 10. 文件存储
|
||
|
||
### 10.1 本地存储
|
||
|
||
```yaml
|
||
storage:
|
||
driver: local
|
||
local:
|
||
path: ./public
|
||
base_url: http://localhost:8080/public
|
||
```
|
||
|
||
### 10.2 阿里云 OSS
|
||
|
||
```yaml
|
||
storage:
|
||
driver: oss
|
||
oss:
|
||
endpoint: oss-cn-hangzhou.aliyuncs.com
|
||
bucket: my-bucket
|
||
access_key_id: your_key
|
||
access_key_secret: your_secret
|
||
base_url: https://my-bucket.oss-cn-hangzhou.aliyuncs.com
|
||
```
|
||
|
||
### 10.3 使用存储
|
||
|
||
```go
|
||
import "github.com/EthanCodeCraft/xlgo-core/storage"
|
||
|
||
// 初始化
|
||
if err := storage.Init(&cfg.Storage); err != nil {
|
||
return err
|
||
}
|
||
|
||
// 上传文件(从请求中获取)
|
||
file, _ := c.FormFile("file")
|
||
path, err := storage.Upload(file, "images")
|
||
|
||
// 获取访问 URL
|
||
url := storage.GetURL(path)
|
||
|
||
// 上传字节数据
|
||
path, err := storage.UploadFromBytes(data, "avatar.jpg", "images")
|
||
|
||
// 删除文件
|
||
err := storage.Delete(path)
|
||
|
||
// 获取文件内容
|
||
data, err := storage.Get(path)
|
||
|
||
// 检查文件是否存在
|
||
if storage.Exists(path) {
|
||
// 文件存在
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 11. 工具函数库
|
||
|
||
### 11.1 随机数生成
|
||
|
||
**安全选型**:字符串随机(token/OTP/验证码/会话 ID/nonce)用 `RandStringSecure`/`RandDigitSecure`
|
||
(基于 `crypto/rand`,不可预测)。`RandString`/`RandDigit`(math/rand 版本)已移除——
|
||
字符串随机的用途几乎都是安全场景,保留 math/rand 版本会诱导误用。
|
||
|
||
```go
|
||
import "github.com/EthanCodeCraft/xlgo-core/utils"
|
||
|
||
// 安全场景:token、会话 ID、API key(crypto/rand,不可预测)
|
||
token, err := utils.RandStringSecure(32)
|
||
if err != nil {
|
||
// 处理 crypto/rand 失败(极罕见,通常仅系统熵池耗尽)
|
||
}
|
||
|
||
// 安全场景:6 位 OTP 验证码、密码重置码(crypto/rand)
|
||
code, err := utils.RandDigitSecure(6)
|
||
|
||
// 安全场景:安全 nonce 范围、防猜抽奖、密钥分桶(crypto/rand 无偏)
|
||
idx, err := utils.RandIntSecure(0, 1000)
|
||
|
||
// 范围随机数(非密码学安全,仅用于负载均衡/游戏/A-B 分桶等非安全场景)
|
||
n := utils.RandInt(1, 100)
|
||
```
|
||
|
||
> 非安全场景需要高性能随机串时,直接用标准库 `math/rand` 即可,框架不再提供
|
||
> 易误用的 `RandString`/`RandDigit`。
|
||
|
||
### 11.2 字符串处理
|
||
|
||
```go
|
||
// 检查空白
|
||
if utils.IsBlank(str) { }
|
||
|
||
// 批量检查
|
||
if utils.IsAnyBlank(name, email, phone) { }
|
||
|
||
// 默认值
|
||
name := utils.DefaultIfBlank(name, "未知")
|
||
|
||
// Unicode长度(中文)
|
||
len := utils.StrLen("你好世界") // 4
|
||
|
||
// 截取子串(支持中文)
|
||
sub := utils.Substr("你好世界", 0, 2) // "你好"
|
||
|
||
// 不区分大小写比较
|
||
if utils.EqualsIgnoreCase("Admin", "admin") { }
|
||
```
|
||
|
||
### 11.3 时间日期
|
||
|
||
```go
|
||
// 时间戳
|
||
now := utils.NowUnix() // 秒
|
||
now := utils.NowTimestamp() // 毫秒
|
||
|
||
// 时间戳转时间
|
||
t := utils.FromUnix(unix)
|
||
t := utils.FromTimestamp(ms)
|
||
|
||
// 格式化
|
||
s := utils.FormatDateTime(t) // "2006-01-02 15:04:05"
|
||
s := utils.FormatDate(t) // "2006-01-02"
|
||
|
||
// 获取当天开始/结束
|
||
start := utils.StartOfDay(t)
|
||
end := utils.EndOfDay(t)
|
||
|
||
// 获取当月开始/结束
|
||
start := utils.StartOfMonth(t)
|
||
end := utils.EndOfMonth(t)
|
||
```
|
||
|
||
### 11.4 类型转换
|
||
|
||
```go
|
||
// 字符串转数字
|
||
n := utils.ToInt("123")
|
||
n := utils.ToIntDefault("abc", 0) // 失败返回默认值
|
||
|
||
n := utils.ToInt64("123")
|
||
f := utils.ToFloat64("3.14")
|
||
|
||
// 分页计算
|
||
totalPages := utils.CalcPageCount(100, 10) // 10页
|
||
offset := utils.CalcOffset(2, 20) // 20
|
||
```
|
||
|
||
### 11.5 文件操作
|
||
|
||
```go
|
||
// 检查存在
|
||
utils.FileExists(path)
|
||
utils.DirExists(path)
|
||
|
||
// 确保目录存在
|
||
utils.EnsureDir(path)
|
||
|
||
// 读写文件
|
||
data, err := utils.ReadFile(path)
|
||
err := utils.WriteFile(path, data)
|
||
err := utils.AppendFile(path, []byte("\nnew line"))
|
||
|
||
// 复制文件
|
||
err := utils.CopyFile(dst, src)
|
||
|
||
// 获取文件大小
|
||
size, err := utils.FileSize(path)
|
||
```
|
||
|
||
### 11.6 格式验证
|
||
|
||
```go
|
||
// 手机号
|
||
if utils.IsPhone("13812345678") { }
|
||
|
||
// 邮箱
|
||
if utils.IsEmail("test@example.com") { }
|
||
|
||
// IPv4
|
||
if utils.IsIPv4("192.168.1.1") { }
|
||
|
||
// 身份证
|
||
if utils.IsIDCard(id) { }
|
||
|
||
// 纯数字
|
||
if utils.IsNumeric(str) { }
|
||
|
||
// 字母数字
|
||
if utils.IsAlphanumeric(str) { }
|
||
```
|
||
|
||
### 11.7 HTTP 客户端(连接池优化版)
|
||
|
||
```go
|
||
import "github.com/EthanCodeCraft/xlgo-core/utils"
|
||
|
||
// 创建客户端(Transport 在初始化时创建,连接池可复用)
|
||
client := utils.NewHTTPClient()
|
||
|
||
// 自定义配置
|
||
client = utils.NewHTTPClientWithConfig(utils.HTTPClientConfig{
|
||
Timeout: 30 * time.Second,
|
||
MaxIdleConns: 100, // 最大空闲连接数
|
||
MaxIdleConnsPerHost: 10, // 每主机最大空闲连接
|
||
IdleConnTimeout: 90 * time.Second, // 空闲连接超时
|
||
})
|
||
|
||
// 链式配置
|
||
client.SetTimeout(30 * time.Second)
|
||
client.SetHeader("Authorization", "Bearer xxx")
|
||
client.SetSkipTLS(false) // 生产环境建议设为 false
|
||
|
||
// GET请求
|
||
data, err := client.Get(url, map[string]string{"page": "1"})
|
||
|
||
// POST表单
|
||
data, err := client.Post(url, map[string]string{"name": "test"})
|
||
|
||
// POST JSON
|
||
data, err := client.PostJSON(url, map[string]any{"name": "test"})
|
||
|
||
// PUT请求
|
||
data, err := client.Put(url, map[string]any{"id": 1})
|
||
|
||
// DELETE请求
|
||
data, err := client.Delete(url)
|
||
|
||
// 上传文件
|
||
files := []utils.UploadFile{
|
||
{FieldName: "file", FilePath: "/path/to/file"},
|
||
}
|
||
data, err := client.Upload(url, files, params)
|
||
|
||
// 从字节数据上传
|
||
data, err := client.UploadFromBytes(url, "file", "image.jpg", fileData, params)
|
||
|
||
// 自定义请求
|
||
data, err := client.Request("PATCH", url, bodyData)
|
||
|
||
// 关闭客户端(释放连接池资源)
|
||
client.Close()
|
||
```
|
||
|
||
**HTTPClient 特性:**
|
||
- Transport 初始化时创建,连接池可复用
|
||
- 支持连接池参数配置
|
||
- 全局默认客户端:`utils.DefaultHTTPClient()`
|
||
- 快捷函数:`utils.HTTPGet/HTTPPost/HTTPPostJSON`
|
||
|
||
### 11.8 UUID
|
||
|
||
```go
|
||
// UUID v4
|
||
uuid := utils.UUID() // "550e8400-e29b-41d4-a716-446655440000"
|
||
|
||
// 短UUID(无横线)
|
||
uuid := utils.UUIDShort() // "550e8400e29b41d4a716446655440000"
|
||
|
||
// 验证UUID
|
||
if utils.UUIDValid(uuid) { }
|
||
```
|
||
|
||
### 11.9 函数评估表
|
||
|
||
| 分类 | 高分函数(⭐⭐⭐⭐⭐) | 说明 |
|
||
|------|---------------------|------|
|
||
| **随机(安全)** | `RandStringSecure/RandDigitSecure/RandIntSecure/RandInt64Secure` | crypto/rand,token/OTP/nonce 用 |
|
||
| **随机(范围)** | `RandInt/RandInt64` | math/rand,负载均衡/游戏等非安全场景 |
|
||
| **字符串** | `IsBlank/DefaultIfBlank/StrLen` | 空值处理、Unicode支持 |
|
||
| **时间** | `FormatDateTime/StartOfDay/EndOfMonth` | 标准格式、边界计算 |
|
||
| **转换** | `ToIntDefault/CalcPageCount/CalcOffset` | 安全转换、分页计算 |
|
||
| **文件** | `FileExists/DirExists/EnsureDir/CopyFile` | 路径检查、目录创建 |
|
||
| **验证** | `IsIPv4/IsNumeric/IsAlphanumeric` | 格式验证 |
|
||
| **URL** | `ParseURL/URLBuilder` | 链式构建URL |
|
||
| **加密** | `SHA256/Base64Encode/Base64URLEncode` | 安全哈希、编码 |
|
||
| **HTTP** | `NewHTTPClient/Get/PostJSON/Upload` | 连接池、链式调用 |
|
||
| **UUID** | `UUID/UUIDShort` | UUID生成 |
|
||
|
||
**设计改进点:**
|
||
|
||
| 改进 | 说明 |
|
||
|------|------|
|
||
| 性能优化 | 非安全随机用 `RandInt/RandInt64`(math/rand,sync.Pool 复用源);安全场景用 `RandStringSecure/RandDigitSecure/RandIntSecure`(crypto/rand + big.Int 拒绝采样无偏) |
|
||
| 类型安全 | 移除使用反射的函数,保持类型安全 |
|
||
| 式调用 | `HTTPClient` 和 `URLBuilder` 支持链式调用 |
|
||
| 零依赖 | 仅依赖 `google/uuid`,其余使用标准库 |
|
||
|
||
---
|
||
|
||
## 12. 实时通信
|
||
|
||
### 12.1 SSE 流式响应
|
||
|
||
```go
|
||
import "github.com/EthanCodeCraft/xlgo-core/sse"
|
||
|
||
// AI对话场景
|
||
func ChatHandler(c *gin.Context) {
|
||
ch := make(chan string)
|
||
go func() {
|
||
defer close(ch)
|
||
for _, chunk := range aiResponse {
|
||
ch <- chunk
|
||
}
|
||
}()
|
||
sse.StreamChunks(c, ch)
|
||
}
|
||
|
||
// 手动写入
|
||
writer, _ := sse.NewSSEWriter(c)
|
||
writer.WriteEvent("message", "Hello")
|
||
writer.WriteMessage("World")
|
||
writer.WriteDone()
|
||
```
|
||
|
||
### 12.2 WebSocket
|
||
|
||
```go
|
||
import "github.com/EthanCodeCraft/xlgo-core/ws"
|
||
|
||
// 简单使用
|
||
r.GET("/ws", ws.HandleFunc(func(conn *ws.Connection, message []byte) {
|
||
conn.SendText("收到: " + string(message))
|
||
}))
|
||
|
||
// 广播模式
|
||
hub := ws.NewHub()
|
||
go hub.Run()
|
||
|
||
// 注册连接
|
||
hub.Register(conn)
|
||
|
||
// 广播消息
|
||
hub.Broadcast([]byte("广播消息"))
|
||
|
||
// 获取连接数
|
||
count := hub.Count()
|
||
```
|
||
|
||
---
|
||
|
||
## 13. 定时任务
|
||
|
||
### 13.1 添加任务
|
||
|
||
```go
|
||
import "github.com/EthanCodeCraft/xlgo-core/cron"
|
||
|
||
// 间隔执行(每5分钟)
|
||
cron.AddTask("cleanup", cron.Every(5*time.Minute), func(ctx context.Context) error {
|
||
return cleanupOldData()
|
||
})
|
||
|
||
// 每天固定时间(凌晨2点)
|
||
cron.AddTask("report", cron.Daily(2, 0), generateReport)
|
||
|
||
// 每周执行(周一上午10点)
|
||
cron.AddTask("weekly", cron.Weekly(time.Monday, 10, 0), weeklyTask)
|
||
|
||
// 简化 Cron(分钟+小时)
|
||
cron.AddTask("noon", cron.Cron("0", "12"), doSomething) // 每天12:00
|
||
|
||
// 完整 Cron 表达式(5字段)
|
||
cron.AddTask("complex", cron.ParseCron("*/15 * * * *"), doSomething) // 每15分钟
|
||
cron.AddTask("monthly", cron.ParseCron("0 0 1 * *"), doSomething) // 每月1号凌晨
|
||
cron.AddTask("workday", cron.ParseCron("0 9-17 * * 1-5"), doSomething) // 工作日9-17点
|
||
```
|
||
|
||
### 13.2 启动与停止
|
||
|
||
```go
|
||
// 启动调度器
|
||
cron.Start()
|
||
|
||
// 停止调度器
|
||
cron.Stop()
|
||
|
||
// 获取调度器实例进行更多操作
|
||
scheduler := cron.GetScheduler()
|
||
scheduler.RemoveTask("cleanup") // 移除任务
|
||
tasks := scheduler.ListTasks() // 查看任务列表
|
||
scheduler.RunTask("report") // 立即执行任务
|
||
scheduler.DisableTask("cleanup") // 禁用任务
|
||
scheduler.EnableTask("cleanup") // 启用任务
|
||
```
|
||
|
||
---
|
||
|
||
## 14. 链路追踪
|
||
|
||
### 14.1 初始化
|
||
|
||
```go
|
||
import "github.com/EthanCodeCraft/xlgo-core/trace"
|
||
|
||
trace.Init(trace.Config{
|
||
Enabled: true,
|
||
ServiceName: "my-service",
|
||
Endpoint: "localhost:4318",
|
||
ExporterType: "otlp-http",
|
||
// Insecure: true, // 明文 collector(localhost:4318 等本地 collector 默认无 TLS,需显式开启)
|
||
SampleRatio: 1.0,
|
||
// Propagator: "w3c", // 可选 "w3c"(默认) / "b3" / "jaeger"(映射 W3C)
|
||
})
|
||
defer trace.Close(ctx)
|
||
```
|
||
|
||
> **导出器类型**:`otlp-http` / `otlp-grpc` / `stdout`(写标准输出,便于调试)。未知类型 `Init` 返错。
|
||
> **Insecure**:默认 `false`(TLS);对无 TLS 的本地 collector(如 `localhost:4318`)需显式置 `true`,否则握手失败。
|
||
> **传播器**:`w3c`(默认,W3C TraceContext + Baggage)/ `b3`(同时支持单头与多头,兼容旧 B3 客户端)/ `jaeger`(映射为 W3C TraceContext——现代 Jaeger agent 透传 W3C;纯 Jaeger thrift 头协议请用 `b3`)。未知类型 `Init` 返错。
|
||
> **未 Init 也安全**:未调用 `Init` 或 `Init(Enabled:false)` 时为 Noop tracer,`Middleware`/`StartSpan` 等不 panic。
|
||
|
||
### 14.2 使用中间件
|
||
|
||
```go
|
||
// 自动追踪所有请求
|
||
r.Use(trace.Middleware("my-service"))
|
||
|
||
// 响应头自动添加 X-Trace-ID
|
||
```
|
||
|
||
### 14.3 业务追踪
|
||
|
||
```go
|
||
// 创建子Span
|
||
ctx, span := trace.StartSpan(c, "db_query")
|
||
defer span.End()
|
||
|
||
// 记录错误
|
||
trace.RecordError(c, err)
|
||
|
||
// 添加属性
|
||
trace.SetAttribute(c, "user_id", 123)
|
||
|
||
// 获取TraceID
|
||
traceID := trace.GetTraceID(c)
|
||
```
|
||
|
||
---
|
||
|
||
## 15. 压缩解压
|
||
|
||
### 15.1 Gzip
|
||
|
||
```go
|
||
import "github.com/EthanCodeCraft/xlgo-core/compress"
|
||
|
||
// 压缩数据
|
||
compressed, err := compress.GzipCompress(data)
|
||
|
||
// 解压数据
|
||
data, err := compress.GzipDecompress(compressed)
|
||
|
||
// 压缩文件
|
||
err := compress.GzipCompressFile("src.txt", "dst.gz")
|
||
|
||
// 解压文件
|
||
err := compress.GzipDecompressFile("src.gz", "dst.txt")
|
||
```
|
||
|
||
### 15.2 Zip
|
||
|
||
```go
|
||
// 打包文件/目录
|
||
err := compress.Zip("archive.zip", []string{"file1.txt", "dir/"})
|
||
|
||
// 解压到目录
|
||
err := compress.Unzip("archive.zip", "./output")
|
||
```
|
||
|
||
---
|
||
|
||
## 16. 测试工具
|
||
|
||
### 16.1 API 测试
|
||
|
||
```go
|
||
import "github.com/EthanCodeCraft/xlgo-core/test"
|
||
|
||
func TestUserAPI(t *testing.T) {
|
||
router := test.SetupRouter()
|
||
|
||
// POST请求
|
||
resp := test.POST(router, "/api/users").
|
||
WithJSON(map[string]any{"name": "test"}).
|
||
Execute()
|
||
resp.AssertOK(t)
|
||
resp.AssertCode(t, 1)
|
||
|
||
// GET请求
|
||
resp = test.GET(router, "/api/users/1").Execute()
|
||
resp.AssertOK(t)
|
||
resp.AssertJSONKeyExists(t, "data.id")
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 17. CLI 脚手架
|
||
|
||
### 17.1 创建项目
|
||
|
||
```bash
|
||
# 创建新项目
|
||
xlgo new myproject
|
||
|
||
# 指定模块名
|
||
xlgo new myproject --module github.com/company/myproject
|
||
```
|
||
|
||
### 17.2 生成代码
|
||
|
||
```bash
|
||
# 生成Handler
|
||
xlgo make handler user
|
||
|
||
# 生成Model
|
||
xlgo make model user
|
||
|
||
# 生成Repository
|
||
xlgo make repository user
|
||
|
||
# 生成Service
|
||
xlgo make service user
|
||
```
|
||
|
||
---
|
||
|
||
## 18. 最佳实践
|
||
|
||
### 18.1 项目分层
|
||
|
||
```
|
||
handler/ → 接收请求、参数验证、调用Service
|
||
service/ → 业务逻辑、事务管理
|
||
repository/ → 数据访问、CRUD操作
|
||
model/ → 数据模型定义
|
||
```
|
||
|
||
### 18.2 错误处理
|
||
|
||
```go
|
||
func GetUser(c *gin.Context) {
|
||
id := handler.PathInt64(c, "id", 0)
|
||
if id == 0 {
|
||
// 参数错误码由业务侧定义;这里直接返回通用失败 + 自定义消息
|
||
response.Fail(c, "参数错误")
|
||
return
|
||
}
|
||
|
||
user, err := userService.GetByID(id)
|
||
if err != nil {
|
||
trace.RecordError(c, err)
|
||
response.FailWithError(c, response.ErrUserNotFound)
|
||
return
|
||
}
|
||
|
||
response.Success(c, user)
|
||
}
|
||
```
|
||
|
||
### 18.3 多站点配置
|
||
|
||
```yaml
|
||
# A站点
|
||
app:
|
||
site_name: "site_a"
|
||
|
||
# B站点
|
||
app:
|
||
site_name: "site_b"
|
||
```
|
||
|
||
缓存自动隔离:
|
||
|
||
- site_a: `cache:site_a:user:1`
|
||
- site_b: `cache:site_b:user:1`
|
||
|
||
### 18.4 环境区分
|
||
|
||
```go
|
||
if cfg.IsProduction() {
|
||
gin.SetMode(gin.ReleaseMode)
|
||
// 生产环境收紧 console 输出(仅保留 Warn / Error)
|
||
// 业务事件请使用 logger 包记录
|
||
console.SetLevel(console.LevelWarn)
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 附录:完整示例
|
||
|
||
### 用户登录接口
|
||
|
||
```go
|
||
package handler
|
||
|
||
import (
|
||
"github.com/EthanCodeCraft/xlgo-core/cache"
|
||
"github.com/EthanCodeCraft/xlgo-core/response"
|
||
"github.com/EthanCodeCraft/xlgo-core/validation"
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
type LoginRequest struct {
|
||
Username string `json:"username" label:"用户名" validate:"required" msg_required:"用户名不能为空"`
|
||
Password string `json:"password" label:"密码" validate:"required,password" msg_required:"密码不能为空"`
|
||
}
|
||
|
||
func Login(c *gin.Context) {
|
||
var req LoginRequest
|
||
if !validation.ShouldBindAndValidate(c, &req) {
|
||
return
|
||
}
|
||
|
||
// 查询用户
|
||
user, err := userService.GetByUsername(req.Username)
|
||
if err != nil {
|
||
response.FailWithError(c, response.ErrUserNotFound)
|
||
return
|
||
}
|
||
|
||
// 验证密码
|
||
if !validation.CheckPassword(user.Password, req.Password) {
|
||
response.FailWithError(c, response.ErrPasswordWrong)
|
||
return
|
||
}
|
||
|
||
// 生成Token
|
||
token, err := jwt.GenerateToken(user.ID, user.Username, "admin", "admin")
|
||
if err != nil {
|
||
response.FailWithError(c, response.ErrServerError)
|
||
return
|
||
}
|
||
|
||
// 缓存用户信息
|
||
cache.GetCache().Set(c.Request.Context(),
|
||
cache.KSession(token),
|
||
user,
|
||
cfg.JWT.Expire)
|
||
|
||
response.Success(c, gin.H{
|
||
"token": token,
|
||
"user_id": user.ID,
|
||
"username": user.Username,
|
||
})
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
_文档版本: v1.2.0_
|
||
_最后更新: 2026-07-04_
|