# xlgo Web Framework

Go Version Gin Version License

> 基于 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 cache.SetDefaultCacheManager(&cache.CacheManager{}) // 测试注入自定义 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` 管理后台 goroutine,Shutdown 时 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: ` 生效,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 缓存 > **v1.4.0**:App 经 `WithRedis()` 启用时自动用 App 的 Redis 客户端初始化缓存(多 App 隔离,不串越)。下方 `cache.Init()` 用于 standalone。 ```go // 初始化缓存 cache.Init() // 使用缓存 ctx := context.Background() cacheService := cache.GetCache() // 设置缓存 cacheService.Set(ctx, "user:1", user, 10*time.Minute) // 获取缓存(命中返回 true,未命中返回 false,nil,后端错误返回 err) var user User if hit, err := cacheService.Get(ctx, "user:1", &user); err != nil { return err } else if hit { // 缓存命中 } if exists, err := cacheService.Exists(ctx, "user:1"); err != nil { return err } else if exists { // key 存在 } // 删除缓存 cacheService.Delete(ctx, "user:1") // 按模式删除(使用 SCAN 优化) cacheService.DeleteByPattern(ctx, "user:*") ``` ### JWT 认证 ```go // 生成 Token(自动包含 JTI) token, err := jwt.GenerateToken(userID, username, "admin", "admin") // 解析 Token // ParseToken 默认 fail-closed:黑名单后端不可检查时拒绝 Token(需 Redis) claims, err := jwt.ParseToken(tokenString) // 需显式 fail-open(仅无 Redis 或低安全场景)用: claims, err = jwt.ParseTokenWithBlacklistPolicy(tokenString, jwt.BlacklistFailOpen) // 使 Token 失效(使用 JTI,高效) jwt.InvalidateToken(tokenString) // 获取 Token 的 JTI jti, err := jwt.GetJTI(tokenString) ``` **JWT 黑名单优化**:使用 JTI(JWT 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 分布式限流 > **v1.4.0**:多 App 走各自 Redis 用 `middleware.NewRedisRateLimiter(..., middleware.WithRedisClient(app.RedisClient()))`。 ```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) // 检查文件是否存在(不存在 false,nil;后端错误返回 err) ok, err := storage.Exists(path) if err != nil { return err } ``` ### 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("广播消息")) ``` ### 定时任务 > **v1.4.0**:App 持专属调度器(实例隔离)。**App 管理的任务用 `xlgo.WithCronTask(name, schedule, handler)` > 注册**(蕴含启用 cron,`Init` 时注册+启动,`Shutdown` 时停止)。下方包级 `cron.AddTask`/`cron.Start` > 仅用于 standalone(无 App)或 `Init` 之后;**`Init` 之前**调包级 `cron.AddTask` 会注册到 init 默认 > 调度器、被 App swap 丢弃。`app.Scheduler()` 供动态注册与管理。 ```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号 // ParseCron 对非法表达式 fail-fast panic;动态输入请用 ParseCronStrict 处理 error。 // 如需旧版“非法表达式回退为每分钟”的兼容语义,请显式使用 ParseCronOrDefault。 // 启动调度器 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) ``` ### 限流中间件 > **v1.4.0**:App `Shutdown` 自动 `registry.Stop()`(不再调全局 `StopRateLimiters`,避免误停其他 App)。多 App per-App 计数隔离用 `app.RateLimitRegistry().LoginRateLimit()` 等 App-bound 中间件。 ```go // 登录限流(每分钟 10 次) r.POST("/login", middleware.LoginRateLimit(), func(c *gin.Context) { response.Success(c, gin.H{"token": "..."}) }) // 上传限流(每分钟 20 次) r.POST("/upload", middleware.UploadRateLimit(), func(c *gin.Context) { response.Success(c, gin.H{"file": "..."}) }) // 自定义限流 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 status(401/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.4.0 (2026-07-12) > **instance+facade 一致性收尾**:cron/storage/cache/ratelimit 实例化(App 持 per-App 实例 + `SwapDefault*` + 包级 facade 代理,照 `database.Manager`/`jwt.Manager` 模式),消除单进程多 App 互相污染;trace 纳入 App 生命周期(`WithTrace`);删除 `WithoutWire` 空函数。`go vet` + `go build` + `go test -race ./...` 全绿。完整变更见 [CHANGELOG.md](./CHANGELOG.md)。 主要破坏性变更(升级前必读): - **`xlgo.WithoutWire()` 删除**:v1.1.0 wire 包删除后遗留的空 no-op,直接删除调用即可(无行为变化)。 - **`cron.AddTask` 在 `App.Init` 之前注册失效**:App 持专属调度器,pre-Init 包级 `cron.AddTask` 注册到 init 默认调度器、被 App swap 丢弃。改用 `xlgo.WithCronTask(name, schedule, handler)`(或 Init 后 `app.Scheduler().AddTask`)。 - **`middleware.StopRateLimiters` 语义收窄**:仅停默认 Registry;App `Shutdown` 调 `app` 自己的 `registry.Stop()`,不再误停其他 App 限流器。 - **`commitReplacedResources` 不再关闭 `previousDB`/`previousRedis`/`loggerSnapshot`**:多 App 下 previous 可能是另一 App 的活跃资源(致 `redis: client is closed` / logger writer 误杀)。单 App 无影响(init 默认无资源)。 - **cache/ratelimit redis 注入**(jwt 模型):`cache.NewRedisCacheWithRedis`/`CacheManager.InitWithRedis`;`middleware.WithRedisClient`。 新增:`WithTrace`/`WithCronTask`;`app.Scheduler()`/`Cache()`/`RedisClient()`/`RateLimitRegistry()`;`cron/storage/cache/middleware` 各 `SwapDefault*`;`config.TraceConfig`;`middleware.RateLimitRegistry` + App-bound 限流中间件;`storage.StorageManager.Close()`。 升级:`go get github.com/EthanCodeCraft/xlgo-core@v1.4.0`(⚠️ 含破坏性变更,详见 CHANGELOG 迁移说明) ### 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** - 受管理的后台 goroutine,Shutdown 时 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)`(`-<8字节crypto/rand hex>.`),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: ` 生效,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) 文件