Files
xlgo-core/README.md
T
2026-07-08 19:11:37 +08:00

1071 lines
39 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# xlgo Web Framework
<p align="center">
<img src="https://img.shields.io/badge/Go-1.25+-00ADD8?style=for-the-badge&logo=go" alt="Go Version">
<img src="https://img.shields.io/badge/Gin-v1.9-00ADD8?style=for-the-badge" alt="Gin Version">
<img src="https://img.shields.io/badge/License-MIT-green?style=for-the-badge" alt="License">
</p>
> 基于 Go + Gin 的后端开发框架。**组件全部 Manager 化**——既能像脚本一样用包级函数简单调用,又能像正经框架一样注入实例、跑多套、塞 mock 做单测。
## 为什么是 xlgo
多数 Gin 脚手架把数据库、Redis、缓存、JWT、日志做成**包级全局单例**:写小项目顺手,但一旦要做单元测试、同进程跑多套实例、或替换实现,就动弹不得。xlgo 把这些组件全部抽象成 `XxxManager`,同时保留包级便捷函数做 facade——**简单用法零成本,复杂场景不封顶**:
```go
// 简单用法:包级函数,跟单例脚手架一样直接
database.InitDB(cfg)
db := database.GetDB()
// 同一份代码,也能注入实例 / 跑多套 / 塞 mock
myDB := database.NewManager(cfg)
myDB.Open(ctx) // 独立实例,不受全局影响
database.SetDefaultManager(myDB) // 提升为全局默认,并关闭旧默认 manager
mockCache := &fakeCacheSvc{}
cache.SetDefaultCacheManager(&cache.CacheManager{}) // 测试注入
```
### 区别于一般 Gin 脚手架的几点
- **组件可注入** — database / redis / cache / jwt / storage / logger 均为 `Manager` + 全局 facade 双轨,支持多实例与 mock 注入,而非写死的包级单例
- **生产就绪内置** — `/livez` `/readyz`、Prometheus `/metrics`、主库探活自愈、请求级超时、优雅关闭(等待 in-flight goroutine)开箱即用,不用自己搭
- **框架内零 Fatal** — 错误一律返回由调用方处理,框架代码不 `panic` 杀进程
- **默认轻量、按需启用** — `New()` 什么都不开,`NewFullStack()` 一键全开,中间任意组合;`Without*` 精细排除
- **可插拔数据库方言** — GORM 方言注册表,内置 MySQL / PostgreSQL,可扩展 SQLite / ClickHouse 等
### 30 秒上手
```go
package main
import (
xlgo "github.com/EthanCodeCraft/xlgo-core"
"github.com/EthanCodeCraft/xlgo-core/response"
"github.com/gin-gonic/gin"
)
func main() {
app := xlgo.New(
xlgo.WithConfigPath("./config.yaml"),
xlgo.WithLogger(),
xlgo.WithHealthRoutes(),
)
app.GetRouter().GET("/", func(c *gin.Context) {
response.Success(c, gin.H{"hello": "xlgo"})
})
_ = app.Run()
}
```
## 框架特性
**架构与可注入性**
- **组件 Manager 化** - database / redis / cache / jwt / storage / logger 均为 `XxxManager` + 全局 facade 双轨,支持多实例与测试注入 mock
- **可插拔数据库方言** - GORM 方言注册表,内置 MySQL / PostgreSQL,可注册 SQLite / ClickHouse 等任意驱动
- **读写分离** - 主库 + 多副本,`ReplicaPicker` 策略可插拔(轮询 / 随机 / 自定义),context 路由强制主库
- **Lifecycle Hooks** - `OnInit / OnStart / OnReady / OnStop` 生命周期钩子,可插拔启动/关闭逻辑
**生产就绪**
- **健康探针** - `/livez`(存活)+ `/readyz`(就绪)+ `/health`(兼容),K8s probe 友好
- **Prometheus 指标** - `/metrics` 端点 + 请求计数 / 耗时 / 在飞数采集中间件
- **依赖健康自愈** - 主库后台探活,连续失败标记不健康联动 503;从库失败临时剔除、恢复自动纳入
- **请求级超时** - `middleware.Timeout` 级联取消下游 GORM/Redis
- **优雅关闭** - `App.Go` 管理后台 goroutineShutdown 时 cancel + 等待退出(带超时)
- **配置校验** - 启动期 `Validate` 拦截非法配置(端口/密钥/必填字段),错误前置
**基础功能**
- **配置管理** - YAML + 环境变量覆盖 + 热更新,`time.Duration` 字符串解析(`"24h"`
- **缓存** - Redis 缓存,键前缀、TTL、SCAN 优化、分布式锁
- **认证** - JWT 认证,Token 黑名单、刷新机制,HMAC 算法可选
- **日志** - 分级日志(通用 / API / DB 三分流),lumberjack 轮转
- **中间件** - CORS、限流(内存 + Redis 版)、请求日志、认证、CSRF、RequestID、Recover
- **文件存储** - 本地 + 阿里云 OSS
- **实时通信** - SSE 流式响应 + WebSocket
- **定时任务** - 内置调度器(interval / daily / weekly / cron
- **验证器** - 请求参数验证,自定义错误消息
- **响应模式** - `ModeBusiness`(业务码,默认)/ `ModeREST`(按错误码映射 HTTP status)一键切换
- **CLI 工具** - 脚手架 + 代码生成(handler/repository/model/service
## 快速开始
> **环境要求**xlgo 基于 Go 1.25+ 构建,请确保本地已安装 Go 1.25 或更高版本。
### 1. 安装
```bash
# 安装脚手架工具
go install github.com/EthanCodeCraft/xlgo-core/cmd/xlgo@latest
# 创建新项目
xlgo new myproject
# 进入目录
cd myproject
# 安装依赖
go mod tidy
```
### 2. 创建配置文件 config.yaml
```yaml
server:
host: "" # 绑定地址,空=监听所有接口;127.0.0.1=仅本机
port: 8080
mode: development
read_timeout: 15s
write_timeout: 30s
idle_timeout: 60s
shutdown_timeout: 30s
response_mode: business # business(默认,全200+业务码) 或 rest(按错误码映射HTTP status)
database:
driver: mysql # mysql(默认)或 postgres
host: localhost
port: 3306
user: root
password: your_password
name: your_database
max_idle_conns: 10
max_open_conns: 100
# dsn: "自定义连接字符串,设置后优先于上面的字段"
redis:
host: localhost
port: 6379
password: ""
db: 0
jwt:
secret: your_jwt_secret_key_at_least_32_bytes # ≥32 字节,否则启动期被 Validate 拦截
expire: "24h" # time.Duration,支持 "24h"/"30m" 等
refresh_expire: "168h" # 刷新 token 过期时间
issuer: xlgo
algorithm: HS256 # HS256(默认)/HS384/HS512
storage:
driver: local
local:
path: ./public
base_url: http://localhost:8080/public
log:
dir: ./logs
max_size: 100
max_backups: 30
max_age: 30
compress: true
```
### 3. 运行项目
```bash
go run main.go
```
访问 http://localhost:8080/health 检查服务状态。
> v1.0.2 起,`xlgo.New()` 默认是轻量应用,不会自动初始化 MySQL、Redis、Storage 或 Swagger。需要完整基础设施时请显式使用 `WithMySQL()`、`WithRedis()`、`WithStorage()`、`WithSwaggerRoutes()`,或直接使用 `xlgo.NewFullStack()`。
>
> `Without*` 系列 Option(如 `WithoutSwaggerRoutes`)主要用于 `NewFullStack` / `RunFullStack` 启用全部组件后排除个别项,例如 `xlgo.NewFullStack(xlgo.WithoutSwaggerRoutes())` 全组件但关闭 Swagger。`xlgo.New()` 本身已全关,无需再 `Without`。
### 4. 模块路径与包名
xlgo 的 **模块路径**`github.com/EthanCodeCraft/xlgo-core`**包名** 是 `xlgo`(二者不同是 Go 惯例,如 `github.com/gin-gonic/gin` → 包名 `gin`)。import 时用别名 `xlgo` 即可:
```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() {
app := xlgo.New(
xlgo.WithConfigPath("./config.yaml"),
xlgo.WithLogger(),
xlgo.WithHealthRoutes(),
)
// 注册一个示例路由
app.GetRouter().GET("/", func(c *gin.Context) {
response.Success(c, gin.H{"message": "Hello xlgo!"})
})
// 启用 JWT 认证示例路由(需配合 WithMySQL/WithRedis 生成 token
_ = middleware.RequireUserTypes("admin")
_ = app.Run()
}
```
> 子包(`config` / `database` / `logger` / `middleware` ...)的包名与目录名一致,无需别名。
---
## 核心功能
### 配置管理
```go
// 加载配置
cfg, err := config.Load("./config.yaml")
// 获取配置
cfg := config.Get()
serverPort := cfg.Server.Port
// 配置热更新
cfg, err := config.LoadWithWatch("./config.yaml", func(newCfg *config.Config) {
log.Println("配置已更新")
})
// 注册配置变更回调
config.RegisterCallback(func(cfg *config.Config) {
// 处理配置变更
})
// 手动重新加载
config.Reload()
```
### 数据库操作
xlgo 基于 GORM,主库与从库的驱动由配置 `database.driver` 决定,内置支持 `mysql`(默认)与 `postgres`,也可通过 `database.dsn` 使用任意自定义连接字符串。v1.0.2 起 GORM 方言通过 **可插拔注册表** 管理,应用可自行接入 SQLite、SQL Server、ClickHouse 等任意 GORM 驱动而无需修改框架。
```go
// 初始化数据库(驱动由配置决定)
database.InitDB(cfg)
defer database.Close()
// 主从读写分离(从库 DSN 需与主库驱动匹配)
database.InitDBWithReplicas(cfg, []string{
"root:pass@tcp(slave1:3306)/db",
"root:pass@tcp(slave2:3306)/db",
})
// 读操作自动路由到从库
var users []model.User
database.GetReadDB().Find(&users)
// 强制使用主库
ctx := database.UseMaster(context.Background())
database.GetDBFromContext(ctx).Find(&users)
// 事务
database.Transaction(func(tx *gorm.DB) error {
return tx.Create(&user).Error
})
// 健康检查
status := database.HealthCheck()
```
v1.0.2 起数据库状态由实例化的 `database.Manager` 管理,全局函数(`GetDB``GetReadDB``CloseAll` 等)作为默认 `DefaultManager` 的 facade 保留。可通过 `database.NewManager(cfg)` 创建独立管理器,并通过 `ReplicaPicker` 自定义从库选择策略:
```go
// 独立管理器(不影响全局 DefaultManager
mgr := database.NewManager(cfg)
if err := mgr.Open(context.Background()); err != nil {
return err
}
defer mgr.Close()
// 从库选择策略:轮询(默认随机)
database.SetReplicaPicker(&database.RoundRobinPicker{})
```
`database.SetDefaultManager(m)` 表示把新 manager 接管为全局默认,并关闭被替换的旧 manager,避免连接池泄漏。需要失败回滚或延迟释放旧资源的初始化流程,请使用 `database.SwapDefaultManager(m)` 并自行决定何时关闭返回的旧 manager。
#### 注册自定义 GORM 方言
通过 `database.RegisterDialect` 一次注册即可让 `database.driver: <name>` 生效,DSN 构建器会同步登记到 `config` 包,因此 `cfg.Database.DSN()` 也会识别新驱动:
```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.RegisteredDialects()` 返回当前已注册的驱动名(含别名),`database.LookupDialect(name)` 用于检查某个驱动是否已就绪。未注册的驱动会回退到 MySQL 以保持向后兼容。
### Repository 泛型 CRUD
```go
// 创建仓库
userRepo := repository.NewBaseRepo[model.User](database.GetDB())
// 基础 CRUD
user, err := userRepo.FindByID(ctx, 1)
err := userRepo.Create(ctx, &user)
err := userRepo.Update(ctx, &user)
err := userRepo.Delete(ctx, 1)
// 分页查询
result, err := userRepo.FindPage(ctx, 1, 20)
// result.Items, result.Total, result.Page, result.PageSize
// 条件查询
users, err := userRepo.FindWhere(ctx, "status = ?", 1)
// 链式查询
users, err := userRepo.NewQueryBuilder().
Where("status = ?", 1).
Order("created_at DESC").
Limit(10).
Find(ctx)
// 批量操作
err := userRepo.CreateBatch(ctx, users)
err := userRepo.DeleteBatch(ctx, []uint{1, 2, 3})
// 事务支持
err := userRepo.WithTransaction(ctx, func(txRepo *repository.BaseRepo[model.User]) error {
return txRepo.Create(ctx, &user)
})
```
### Redis 缓存
```go
// 初始化缓存
cache.Init()
// 使用缓存
ctx := context.Background()
cacheService := cache.GetCache()
// 设置缓存
cacheService.Set(ctx, "user:1", user, 10*time.Minute)
// 获取缓存
var user User
if cacheService.Get(ctx, "user:1", &user) {
// 缓存命中
}
// 删除缓存
cacheService.Delete(ctx, "user:1")
// 按模式删除(使用 SCAN 优化)
cacheService.DeleteByPattern(ctx, "user:*")
```
### JWT 认证
```go
// 生成 Token(自动包含 JTI
token, err := jwt.GenerateToken(userID, username, "admin", "admin")
// 解析 Token
claims, err := jwt.ParseToken(tokenString)
// 使 Token 失效(使用 JTI,高效)
jwt.InvalidateToken(tokenString)
// 获取 Token 的 JTI
jti, err := jwt.GetJTI(tokenString)
```
**JWT 黑名单优化**:使用 JTIJWT ID)替代完整 Token 存储,大幅节省 Redis 内存。
### 分布式锁
```go
// 安全加锁(返回 LockToken
token, err := cache.NewLock(ctx, cache.KLock("order:123"), 30*time.Second)
if token != nil {
defer cache.Unlock(ctx, token) // 只有持有者能释放
// 执行业务逻辑
}
// 续期锁(长任务场景)
cache.ExtendLock(ctx, token, 30*time.Second)
// 自动续期执行
err := cache.WithLockAutoExtend(ctx, key, 30*time.Second, 10*time.Second, func(ctx context.Context) error {
// 长任务自动续期
return nil
})
```
**分布式锁安全特性**:使用 Lua 脚本 + UUID Token,只有锁的持有者才能释放。
### Redis 分布式限流
```go
// 内存限流(单实例)
r.Use(middleware.CustomRateLimit(100, time.Minute))
// Redis 分布式限流(多实例共享)
r.Use(middleware.RedisRateLimit("api_limit", 100))
r.Use(middleware.LoginRedisRateLimit()) // 登录限流
r.Use(middleware.APIRedisRateLimit()) // API 限流
```
### 请求验证
```go
type LoginRequest struct {
Username string `json:"username" label:"用户名" validate:"required,username" msg_required:"请输入用户名"`
Password string `json:"password" label:"密码" validate:"required,password" error:"密码格式不正确"`
Phone string `json:"phone" label:"手机号" validate:"omitempty,phone" msg_phone:"请输入正确的手机号"`
}
// 绑定并验证
errors, ok := validation.ShouldBindAndValidate(c, &req)
if !ok {
response.Fail(c, errors.FirstMessage())
return
}
```
**验证器支持的自定义标签**
- `label:"中文名"` - 字段显示名称
- `error:"通用错误消息"` - 所有验证失败时显示
- `msg_required:"必填项"` - 特定规则的错误消息
- `msg_min:"最少5个字符"` - 针对 min 规则的错误消息
### 统一错误码
```go
// 使用预定义错误
response.FailWithError(c, response.ErrUserNotFound)
// 带详细信息
response.FailWithDetail(c, response.ErrPasswordWrong, "连续错误3次将锁定账户")
// 自定义错误
err := response.NewError(10001, "自定义错误")
```
**预定义错误码**
- 用户模块:`ErrUserNotFound`, `ErrPasswordWrong`, `ErrTokenExpired`
- 文件模块:`ErrFileTooLarge`, `ErrFileTypeInvalid`
- 数据模块:`ErrDataNotFound`, `ErrDataConflict`
### 文件上传
```go
// 上传文件
file, err := c.FormFile("file")
path, err := storage.Upload(file, "images")
url := storage.GetURL(path)
// 上传字节数组
path, err := storage.UploadFromBytes(data, "filename.jpg", "images")
// 删除文件
err := storage.Delete(path)
// 获取文件内容
data, err := storage.Get(path)
// 检查文件是否存在
exists := storage.Exists(path)
```
### SSE 流式响应
```go
// 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)
}
// 带消息 ID 的流式响应
sse.StreamWithID(c, "msg_123", ch)
```
### WebSocket
```go
// 简单使用
r.GET("/ws", ws.HandleFunc(func(conn *ws.Connection, message []byte) {
conn.SendText("收到: " + string(message))
}))
// 广播模式
hub := ws.NewHub()
go hub.Run()
hub.Broadcast([]byte("广播消息"))
```
### 定时任务
```go
// 每隔 5 分钟执行
cron.AddTask("cleanup", cron.Every(5*time.Minute), func(ctx context.Context) error {
return cleanupOldData()
})
// 每天凌晨 2 点执行
cron.AddTask("daily_report", cron.Daily(2, 0), generateReport)
// 每周一上午 9 点执行
cron.AddTask("weekly", cron.Weekly(time.Monday, 9, 0), weeklyTask)
// 完整 Cron 表达式
cron.AddTask("every15min", cron.ParseCron("*/15 * * * *"), doSomething)
cron.AddTask("monthly", cron.ParseCron("0 0 1 * *"), doSomething) // 每月1号
// 启动调度器
cron.Start()
defer cron.Stop()
```
### CSRF 防护
```go
// 基本使用
r.Use(middleware.CSRF())
// 获取 Token(返回给前端)
token := middleware.GetCSRFToken(c)
// 跳过指定路径
r.Use(middleware.CSRFWithSkip([]string{"/api/webhook"}))
// API 模式(双重提交 Cookie
r.Use(middleware.DoubleSubmitCookie())
```
### 密码加密
```go
// 加密密码
hash, err := validation.HashPassword("password123")
// 验证密码
if validation.CheckPassword(hash, "password123") {
// 密码正确
}
// 带成本升级的验证
match, needUpgrade, newHash, err := validation.CheckPasswordAndUpgrade(hash, password, 12)
```
---
## 中间件
### 认证中间件
```go
// JWT 认证(必须登录)
r.Use(middleware.AuthRequired())
// 自定义用户类型权限
r.Use(middleware.RequireUserTypes("tenant_admin", "platform_admin"))
// 自定义角色权限
r.Use(middleware.RequireRoles("owner", "manager"))
// 自定义复杂权限判断
r.Use(middleware.RequireAuth(func(user middleware.AuthUser, c *gin.Context) bool {
return user.UserType == "merchant" && user.Role == "owner"
}))
// 默认快捷方法(super_admin/admin/staff 只是框架默认常量)
r.Use(middleware.AdminRequired())
r.Use(middleware.SuperAdminRequired())
r.Use(middleware.StaffRequired())
r.Use(middleware.AnyUserRequired())
// 获取用户信息
user, ok := middleware.GetAuthUser(c)
userID := middleware.GetUserID(c)
username := middleware.GetUsername(c)
role := middleware.GetRole(c)
userType := middleware.GetUserType(c)
```
### 限流中间件
```go
// 登录限流(每分钟 10 次)
r.POST("/login", middleware.LoginRateLimit(), handler.Login)
// 上传限流(每分钟 20 次)
r.POST("/upload", middleware.UploadRateLimit(), handler.Upload)
// 自定义限流
r.Use(middleware.CustomRateLimit(50, time.Minute))
// 停止限流器(应用关闭时)
defer middleware.StopRateLimiters()
```
---
## 响应格式
框架使用统一的响应格式:
```json
{
"code": 1,
"msg": "操作成功",
"data": {},
"request_id": "abc123"
}
```
```go
// 成功响应
response.Success(c, data)
response.SuccessWithMsg(c, "自定义消息", data)
// 失败响应
response.Fail(c, "错误消息")
// 分页响应
response.Page(c, list, total, page, pageSize)
// 错误响应
response.Unauthorized(c, "请先登录")
response.NotFound(c, "资源不存在")
response.ServerError(c, "服务器错误")
response.RateLimit(c)
// 显式指定 HTTP status(不受 Mode 影响)
response.Custom(c, http.StatusBadRequest, response.CodeFail, "参数错误", nil)
```
**响应模式**v1.1.0):默认 `ModeBusiness`,所有响应 HTTP 200 + 业务码(兼容存量)。切换 `ModeREST` 后,失败响应按错误码映射对应 HTTP status401/404/429/500...),body 仍带业务码,便于 APM / Prometheus / 网关按 status 区分异常:
```go
response.SetMode(response.ModeREST) // 代码切换
// 或在 config.yaml 配置:server.response_mode: rest
```
---
## CLI 工具
```bash
# 创建新项目
xlgo new myproject
# 创建新项目并指定模块路径
xlgo new myproject --module github.com/myorg/myproject
# 生成代码
xlgo make handler user # 创建 handler/user.go
xlgo make repository user # 创建 repository/user_repository.go
xlgo make model user # 创建 model/user.go
xlgo make service user # 创建 service/user_service.go
# 显示版本
xlgo version
```
---
## 测试
框架提供完整的测试工具包:
```go
func TestUserAPI(t *testing.T) {
router := test.SetupRouter()
// 创建请求
resp := test.POST(router, "/api/v1/users").
WithJSON(map[string]any{"name": "test"}).
WithToken("xxx").
Execute()
// 断言
resp.AssertOK(t)
// 解析响应
var result map[string]any
resp.ParseJSON(&result)
}
```
---
## 目录结构
```
xlgo/
├── app.go # 应用入口
├── cache/
│ ├── cache.go # 缓存服务
│ ├── keybuilder.go # 键名前缀管理
│ └── lock.go # 分布式锁
├── cmd/
│ └── xlgo/ # CLI 脚手架
├── compress/
│ └── compress.go # Gzip/Zip 压缩解压
├── config/
│ ├── config.go # 配置管理(支持热更新、time.Duration 解析)
│ └── validate.go # 配置校验(启动期拦截非法配置)
├── console/
│ └── console.go # 彩色控制台输出
├── cron/
│ └── cron.go # 定时任务调度
├── database/
│ ├── manager.go # 数据库管理器(主从、Picker、探活自愈、Init/Close/HealthCheck
│ ├── dialect.go # GORM 方言注册表(mysql / postgres,可扩展)
│ └── redis.go # Redis 连接管理器(RedisManager
├── handler/
│ └── handler.go # 基础处理器(类型安全参数获取)
├── jwt/
│ └── jwt.go # JWT 工具
├── logger/
│ ├── logger.go # 日志实现
│ └── field.go # 日志字段工具
├── middleware/
│ ├── auth.go # JWT 认证中间件
│ ├── cors.go # CORS 跨域中间件
│ ├── csrf.go # CSRF 防护中间件
│ ├── logger.go # 请求日志中间件
│ ├── metrics.go # Prometheus 指标中间件
│ ├── ratelimit.go # 限流中间件
│ ├── requestid.go # 请求ID中间件
│ ├── recover.go # Panic恢复中间件
│ └── timeout.go # 请求级超时中间件
├── model/
│ └── base.go # 基础模型
├── repository/
│ └── repository.go # 基础仓库(泛型CRUD
├── response/
│ ├── response.go # 统一响应格式
│ ├── mode.go # 响应模式开关(business / REST
│ └── error.go # 统一错误码
├── router/
│ ├── router.go # 路由注册中心(模块化/版本化、livez/readyz/health
│ └── metrics.go # Prometheus /metrics 端点
├── sse/
│ └── sse.go # SSE 流式响应
├── storage/
│ └── storage.go # 文件存储(本地+OSS)
├── test/
│ └── test.go # 测试工具包
├── trace/
│ └── trace.go # 链路追踪
├── utils/
│ ├── random.go # 随机数生成
│ ├── strings.go # 字符串处理
│ ├── datetime.go # 时间日期
│ ├── convert.go # 类型转换
│ ├── file.go # 文件操作
│ ├── url.go # URL处理
│ ├── validator.go # 格式验证
│ ├── crypto.go # 加密编码
│ ├── http.go # HTTP客户端
│ └── uuid.go # UUID生成
├── validation/
│ ├── validator.go # 请求验证器
│ ├── password.go # 密码强度验证
│ └── hash.go # 密码加密
└── ws/
└── ws.go # WebSocket 支持
```
---
## 部署指南
### Docker 部署
```dockerfile
FROM golang:1.25-alpine AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o server .
FROM alpine:latest
RUN apk --no-cache add ca-certificates tzdata
WORKDIR /app
COPY --from=builder /app/server .
COPY --from=builder /app/config.yaml .
RUN mkdir -p /app/public /app/logs
EXPOSE 8080
CMD ["./server", "-config", "./config.yaml"]
```
```bash
docker build -t xlgo-app:latest .
docker run -d -p 8080:8080 xlgo-app:latest
```
---
## 更新日志
> 完整变更历史见 [CHANGELOG.md](./CHANGELOG.md)。
### v1.2.0 (2026-07-04)
> **破坏性版本**4 轮对抗性评审(deepseek / GLM / Claude / 终审)收口的全部 CRITICAL/HIGH/MEDIUM 修复 + 主线A 包级可变全局并发治理统一。`go vet` + `go build` + `go test -race ./...` 全绿。完整变更见 [CHANGELOG.md](./CHANGELOG.md)。
主要破坏性变更(升级前必读):
- **包级可变全局一律 `atomic.Pointer`**(主线A):`database.DefaultRedis`/`database.DefaultManager`/`storage.DefaultStorage`/`validation.Validator` 由裸指针改为 `atomic.Pointer[T]`。直接读字段调方法需改用 facade(`InitRedis`/`GetDB`/`ValidateStruct` 等)或 `.Load()``database.DefaultManager = x``database.SetDefaultManager(x)`
- **`jwt.DefaultJWT` 包级变量删除** → `GetDefaultJWT()` / `SetDefaultJWTManager()`
- **`repository.FindWhereOrdered`/`FindPageWhereOrdered` 签名**`args []any``args ...any``order` 前置于 `query`
- **`response.Response``Data`/`RequestID``omitempty`**:两字段在所有响应中恒存在。
- **`cache.IsLocked`/`GetLockTTL`/`ForceUnlock`** Redis 不可用改返 `ErrRedisNotReady`(锁操作正确性相关)。
- **`config.Load()` 返回深拷贝**:新增 `(*Config).Clone()``Get()` 仍返只读指针(热路径零分配)。
- **`handler.GetPage``page` 上限 10000**`utils.EqualsIgnoreCase``strings.EqualFold``utils.ReadFile` 去 TOCTOU 前置检查(错误改 `errors.Is(err, os.ErrNotExist)`)。
升级:`go get github.com/EthanCodeCraft/xlgo-core@v1.2.0`(⚠️ 含破坏性变更,详见 CHANGELOG 迁移说明)
### v1.1.1 (2026-06-23)
> v1.1.0 的补丁发布:补 `ServerConfig.Host` 绑定地址、面向用户文案统一中文、修正 README 过时/错误描述(含会致启动失败的配置示例)。
- **ServerConfig.Host** - `server.host` 控制监听地址,空=所有接口(兼容),`127.0.0.1`=仅本机,内网 IP=绑定指定网卡
- **文案统一中文** - recover/logger/metrics 等面向用户文案统一中文(保留协议字段名、Prometheus metric Name、MySQL 错误串匹配等英文)
- **README 修正** - 删 wire 段、修配置示例(secret≥32 字节 + expire 改 Duration)、补 v1.1.0 新文件、重写首段介绍
升级:`go get github.com/EthanCodeCraft/xlgo-core@v1.1.1`(无破坏性变更)
### v1.1.0 (2026-06-23)
> 本版本定位为 **HA & Manager 化 release**:高可用与生产就绪改进 + 组件 Manager 化。含少量破坏性变更,升级前请阅读 [CHANGELOG 升级说明](./CHANGELOG.md#升级说明)。
- **组件 Manager 化** - storage/cache/redis/jwt/logger 五组件新增 `XxxManager` + `SetDefaultXxxManager`,包级 facade 保留兼容,支持多实例与测试注入
- **Lifecycle Hooks** - `WithHook(Hook{OnInit/OnStart/OnReady/OnStop})` 生命周期钩子
- **App.Go** - 受管理的后台 goroutineShutdown 时 cancel + 等待退出
- **Server 配置化** - `server` 新增 timeout/TLS/unix_socket/response_mode 等字段
- **JWTConfig time.Duration** - `jwt.expire``"24h"`,新增 issuer/algorithm
- **Config Validate** - 启动期校验配置(端口/密钥/必填字段),错误前置
- **response REST 模式** - `SetMode(ModeREST)` 按错误码映射 HTTP status
- **livez/readyz** - K8s 友好的存活性/就绪性探针
- **Prometheus metrics** - `/metrics` + `middleware.Metrics()` 采集请求指标
- **请求级 Timeout** - `middleware.Timeout(d)` 级联取消下游
- **依赖健康自愈** - 主库探活 + replica 健康剔除,`/readyz` 联动 503
- **RequestID 默认装入** - 每个响应/panic 日志都带 request_id
升级:`go get github.com/EthanCodeCraft/xlgo-core@v1.1.0`(⚠️ 含破坏性变更,见升级说明)
### v1.0.4 (2026-06-22)
> 本版本定位为 **DX & Docs release**:开发体验与文档改进,无破坏性 API 变更。
- **CLI 多模板** - `xlgo new --template minimal/api/fullstack`,支持轻量 HTTP / 标准业务 / 全组件三种脚手架
- **examples/ 目录** - 新增 `examples/minimal`(无依赖可跑)与 `examples/full`mysql+redis+jwt+user CRUD)可运行示例
- **模块路径文档** - 明确说明 module path `xlgo-core` 与 package name `xlgo` 的关系,补完整 import 示例(保留 `-core`xlgo 多产品系列)
- **Without* 定位** - 文档化 `Without*` Option 主要用于 `NewFullStack` 后排除单项
- **文档结构** - 规划文档归档到 `docs/plans/`,新增 `docs/README.md` 索引
升级:`go get github.com/EthanCodeCraft/xlgo-core@v1.0.4`(无破坏性变更)
### v1.0.3 (2026-06-22)
> 本版本定位为 **bug fix release**:收口 v1.0.2 引入的破坏性清理,并修复 4 个轻量 bug + 依赖复查。完整说明见 [CHANGELOG.md#unreleased](./CHANGELOG.md#unreleased)。
#### 🐛 Fixed — JWT JTI 生成忽略 `rand.Read` 错误
`generateJTI()` 丢弃 `crypto/rand.Read` 的 error,失败时会基于全零字节生成 JTI,导致所有 token 的 JTI 相同、黑名单机制失效。改为 `(string, error)` 并在 `GenerateToken` / `GenerateTokenWithCustomExpiry` 传播错误。
#### 🐛 Fixed — `QueryBuilder.Page` 统计行数被残留 Limit 截断
`Page()` 复制查询做 Count 时未清除残留 `Limit`/`Offset`,调用方先 `.Limit(n)``.Page(...)` 会让 Count 被包成子查询,返回 `total` 被截断为 ≤ n。countDB 改为 `.Limit(-1).Offset(-1)` 清除残留条件。
#### 🐛 Fixed — OSS / 本地存储文件名冲突
4 处上传路径仅用 `time.Now().UnixNano()` 命名,同纳秒并发上传会撞名覆盖。新增 `uniqueFilename(now, ext)``<unixNano>-<8字节crypto/rand hex>.<ext>`),4 处统一改用。
#### 🐛 Fixed — 数据库重试策略对不可恢复错误无效
`InitDB` 对认证失败(`Access denied`)、未知数据库(`Unknown database`)、非法 DSN、未注册驱动等配置类错误也退避重试 5 次,白白延迟 31 秒。新增 `isTransientDBError`,这类错误首次出现即返回;网络类错误仍正常重试。
#### 📦 Dependencies — `go mod tidy` + 安全补丁升级
- `go mod tidy` 补全 postgres 方言传递依赖(`jackc/pgx` 家族、`golang.org/x/sync`),`gorm.io/driver/postgres` 提升为直接依赖
- 安全补丁升级(无 API 变更):`golang.org/x/crypto` v0.49→v0.53、`golang-jwt/jwt/v5` v5.2.1→v5.3.1、`gorilla/websocket` v1.5.1→v1.5.3
- 暂缓升级(留待 v1.0.4 / v1.1):`gin` / `validator` / `gorm` / `aliyun-oss-go-sdk` v2→v3(major 破坏性)等跨版本升级
#### ⚠️ Breaking — 清理 v1.0.2 兼容别名(database 包)
```go
// ❌ 移除
database.InitMySQL(cfg)
database.InitMySQLWithReplicas(cfg, replicas)
(*Manager).InitMySQL / InitMySQLWithReplicas
// ✅ 改用(v1.0.2 起就是正式 API,驱动由 cfg.Database.Driver 决定)
database.InitDB(cfg)
database.InitDBWithReplicas(cfg, replicas)
```
xlgo 仍是早期框架,趁此一次彻底清理 v1.0.2 临时保留的别名,避免长期累积技术债。
#### 🗑️ Removed — 删除死代码 `database.DBResolver`
`database.DBResolver.BeforeQuery` 从未被注册到 GORM callback chain,属于纯死代码。文档曾暗示的"自动读写分离"实际从未生效——读写分离一直依赖业务侧显式调用 `database.UseMaster(ctx)` / `database.UseReplica(ctx)`
需要 callback 级自动路由的用户请直接接入官方 [`gorm.io/plugin/dbresolver`](https://github.com/go-gorm/dbresolver)。
#### 🔄 Changed — 文件重命名 `database/mysql.go → database/manager.go`
文件内容已与 MySQL 解耦(v1.0.2 引入可插拔方言注册表后),继续叫 `mysql.go` 误导。**导入路径无变化**,公开 API 全部保留。
#### ✨ Added — console 包显式 level 控制
`console` 包新增显式级别屏蔽能力:
```go
console.SetLevel(console.LevelWarn) // 只看 Warn / Error
console.SetLevel(console.LevelSilent) // 完全静默
```
**定位明确**console 是**开发期彩色 stdout 工具**(跟 `fmt.Println` 同级),**不写文件、不感知环境**。业务可观测信息请使用 `logger`。框架不会自动根据 `app.env` 切换级别——选择权完全在调用方,避免"dev / prod 行为不一致"的隐式陷阱。
完整对比表见 [GUIDE.md §3.3](./GUIDE.md#33-彩色控制台输出)。
#### 🐛 Fixed — Logger 重复写入修复
修复通用 `Logger``apiCore``dbCore` 都 Tee 进来导致**每条日志写三份**的 bug。
- `Logger`(通用)→ `logs/app.log` + console
- `APILog()``logs/api.log` + console
- `DBLog()``logs/database.log` + console
- 互不串扰,磁盘体积砍掉 2/3
**新增**`logger.Close()` 显式关闭文件句柄,`App.Shutdown` 已自动调用;`Init(nil)` 改为返回 error 而非 panic;生产默认级别从 `Warn` 调整为 `Info`
**升级注意**:日志目录会新增 `logs/app.log` 文件(之前通用日志被串写进了 `api.log`/`database.log`),运维采集脚本如有需要请补上。
#### 🔒 Security — CORS 中间件修复
修复 CORS 中间件多个安全与规范遵守问题:
- **`Access-Control-Allow-Credentials` 永远是 `true`** — 旧实现 `if/else` 两个分支都设了 `"true"`,导致即使配置 `AllowCredentials=false` 也会发送凭证头
- **`*` + `credentials: true` 的规范违规** — 同时发送会被浏览器拒绝;修复后此场景回显具体 Origin
- **缺失 `Vary: Origin`** — 防止 CDN / 网关把 A 用户的 CORS 响应缓存给 B 用户
- 非白名单 Origin 不再被回显,防反射型 CORS 漏洞
**升级影响**:如果你之前依赖"默认允许凭证"的隐式行为,需要在配置里显式启用:
```yaml
cors:
allowed_origins: ["https://your-frontend.example"]
allow_credentials: true
```
#### ⚠️ Breaking — 错误码体系重构
修复 `CodeSuccess``CodeInvalidParams` 撞码的生产级 bug(两者都等于 `1`,导致业务错误响应被前端误判为成功)。
**数值变更**
| 常量 | 旧值 | 新值 |
|---|---|---|
| `response.CodeSuccess` | `1` | **`0`** |
| `response.CodeFail` | `0` | **`1`** |
**移除**
- `response.CodeInvalidParams`(与 `CodeSuccess` 撞码,且参数错误码应由业务自定义)
- `response.ErrInvalidParams`
**迁移指南**
```go
// ❌ 旧代码(编译失败)
response.FailWithError(c, response.ErrInvalidParams)
// ✅ 推荐:业务侧自定义错误码
var ErrInvalidParams = response.NewError(40001, "参数错误")
response.FailWithError(c, ErrInvalidParams)
// ✅ 或直接使用通用失败响应
response.Fail(c, "用户名格式错误")
```
**前端**`if (resp.code === 1)``if (resp.code === 0)`
新增 `_errorCodeUniquenessGuard` 编译期防撞码 map,任何后续 `Code*` 常量重复都会在 `go build` 阶段直接报错。
详细迁移说明见 [CHANGELOG.md](./CHANGELOG.md)。
### v1.0.2 (2026-06-20)
#### 数据库
- **可插拔方言注册表** - 新增 `database.RegisterDialect` / `LookupDialect` / `RegisteredDialects`,应用可一次注册即让 `database.driver: <name>` 生效,DSN 构建器同步登记到 `config` 包;内置 `mysql``postgres`(含 `postgresql``pg` 别名),未注册驱动回退 MySQL
- **多数据库驱动** - `database.driver` 支持 `mysql`(默认)与 `postgres`,新增 `database.InitDB` / `InitDBWithReplicas`v1.0.3 起原 `InitMySQL*` 别名已移除)
- **数据库管理器** - 引入实例化 `database.Manager` 持有主从连接,提供 `Master/Replica/FromContext/HealthCheck` 等方法
- **从库选择策略** - 新增 `ReplicaPicker` 接口与 `RoundRobinPicker` / `RandomPicker` 实现,可通过 `SetReplicaPicker` 自定义
- **私有上下文键** - `db_mode` 字符串键替换为私有 `dbModeContextKey{}` 类型,避免上下文键名冲突
- **DSN 构建器注册表** - `config` 包新增 `RegisterDSNBuilder` / `LookupDSNBuilder` / `RegisteredDrivers``DatabaseConfig.DSN()` 改为查注册表,自定义 `CustomDSN` 优先
#### App 启动流程
- **轻量默认** - `xlgo.New()` 默认不再初始化 MySQL / Redis / Storage,也不注册 `/health``/swagger/*`;通过 Option 显式启用
- **组件 Option** - 新增 `WithLogger / WithMySQL / WithRedis / WithStorage / WithHealthRoutes / WithSwaggerRoutes / WithDefaultRoutes / WithAutoMigrate` 及对应 `WithoutXxx` 关闭项(注:`WithWire` 曾在此版本新增,已于 v1.1.0 随 wire 包删除而移除)
- **迁移控制** - 新增 `Migrator` 类型与 `WithMigrator / WithModels`,注册时自动开启 `WithAutoMigrate``WithoutAutoMigrate` 可显式关闭;不再强制调用空的 `database.AutoMigrate()`
- **batteries-included** - 新增 `WithFullStack` / `NewFullStack` / `RunFullStack`,一键启用全部默认组件
- **错误传播** - 框架初始化失败一律 `return error`,不再在框架内部直接 `Fatalf` 退出进程
#### 权限中间件
- **去业务化** - `super_admin/admin/staff` 调整为默认常量而非固定业务模型
- **通用能力** - 新增 `AuthUser` 结构体、`GetAuthUser``RequireUserTypes / RequireRoles / RequireAuth`,旧的 `AdminRequired/SuperAdminRequired/StaffRequired/AnyUserRequired` 改为基于通用能力实现
#### 配置管理
- **实例化 Manager** - 新增 `config.Manager`,提供 `Load / LoadWithWatch / Reload / RegisterCallback / Get / GetViper / Set` 等方法,支持多实例与重复加载
- **App 持有 Manager** - `WithConfigPath` 创建 App 私有 manager 并通过新增的 `config.SetDefaultManager` 推为全局默认,使 `config.Get / GetString` 等便捷函数仍然可用
- **修复** - 修复 `WithConfigPath` 此前的空实现问题
#### 健康检查与默认路由
- **拆分注册** - `router` 包新增 `RegisterHealthRoute` / `RegisterSwaggerRoutes`,原 `RegisterDefaultRoutes` 改为组合调用
- **检查项与状态** - `/health` 支持注册 `HealthCheck`,失败返回 HTTP 503 与 `{"status":"error", "checks":{...}}`
- **App 集成** - 启用 MySQL / Redis 时自动追加对应健康检查项,可通过 `WithHealthCheck` 注册自定义检查
#### 资源关闭
- **Shutdown 修复** - 改为 `database.CloseAll()` 关闭主从连接,并使用 `errors.Join` 聚合限流器、日志、Redis 等组件的关闭错误
- **Storage 可选化** - 未初始化 Storage 时返回 `ErrStorageNotInitialized`,不再 nil panic
#### 文档与 CLI
- **CLI 修复** - `xlgo version` 更新为 v1.0.2,脚手架模板改为依赖 `github.com/EthanCodeCraft/xlgo-core` 并在注释中提示 Swagger / MySQL / Redis / FullStack 的开启方式
- **GUIDE 同步** - 1.3 最简示例、3.2 日志注释、8.4.7 默认路由、9.2 用户信息获取均更新为 v1.0.2 行为
- **历史日志修正** - 修复此前 README 中错误的 v2.0.0 / v2.1.0 更新日志表述
### v1.0.1 (2026-04-30)
- 新增工具函数库、彩色控制台输出、压缩解压、RequestID、Recover 中间件
- 新增缓存键名前缀、分布式锁、计数器、Redis 分布式限流
- 增强 JWT 黑名单、Repository、CORS、日志中间件和优雅关闭能力
- 新增路由架构,支持模块化、版本化 API、中间件分组和 RESTful CRUD
- 完善配置热更新、数据库读写分离、CSRF、SSE、WebSocket、定时任务、CLI、测试工具和统一错误码
### v1.0.0 (2024-04)
- 初始版本发布
- 基础框架功能
- 完整示例代码
---
## 许可证
MIT License - 详见 [LICENSE](LICENSE) 文件