# 核心说明 (/docs/bamboo-base-go/architecture) import { TypeTable } from '@/components/type-table'; 核心架构 [#核心架构] 项目概述 [#项目概述] **模块名称**: `github.com/bamboo-services/bamboo-base-go` **Go 版本**: 1.24+ **项目定位**: Go 语言基础框架库,为 Bamboo 服务提供核心功能支持,包括日志、错误处理、雪花算法 ID 生成、验证器、中间件等基础设施。 包结构说明 [#包结构说明] ``` github.com/bamboo-services/bamboo-base-go/ │ ├── defined/ # 🏷️ 定义层 — 零依赖的常量与类型 │ ├── context/ # 上下文键常量 (xCtx) │ └── env/ # 环境变量常量 (xEnv) │ ├── common/ # 🧱 公共层 — 可独立使用的基础设施 │ ├── base_response.go # 统一响应结构体 (xBase) │ ├── error/ # 错误码和错误处理 (xError) │ ├── log/ # 日志系统 (xLog) │ ├── snowflake/ # 雪花算法 ID 生成 (xSnowflake) │ ├── utility/ # 工具函数 (xUtil) │ │ └── context/ # 上下文工具 (xCtxUtil) │ └── validator/ # 验证器 (xVaild) │ ├── major/ # 🚀 主框架层 — 依赖 Gin 的核心组件 │ ├── cache/ # 缓存抽象 (xCache) │ ├── helper/ # 核心中间件 (xHelper) │ ├── hook/ # 钩子函数 (xHook) │ ├── http/ # HTTP 常量 (xHttp) │ ├── main/ # 服务运行时 (xMain) │ ├── middleware/ # 业务中间件 (xMiddle) │ ├── models/ # 数据模型基类 (xModels) │ ├── register/ # 应用注册和初始化 (xReg) │ ├── result/ # 响应结果处理 (xResult) │ └── route/ # 路由处理 (xRoute) │ └── plugins/ # 🔌 插件层 — 可选扩展 └── grpc/ # gRPC 支持 ├── constant/ # gRPC 常量 ├── generate/ # Proto 生成辅助 ├── interceptor/ # 拦截器 ├── middleware/ # gRPC 中间件 ├── result/ # gRPC 响应处理 └── runner/ # gRPC 运行时 ``` 包导入别名 [#包导入别名] defined 层(零依赖) [#defined-层零依赖] common 层(基础设施) [#common-层基础设施] major 层(主框架) [#major-层主框架] plugins 层(可选扩展) [#plugins-层可选扩展] 模块依赖关系 [#模块依赖关系] 初始化流程 [#初始化流程] ```go title="main.go" package main import ( "context" xReg "github.com/bamboo-services/bamboo-base-go/major/register" ) func main() { // [!code highlight:2] // 一键初始化所有组件 reg := xReg.Register(context.Background(), nil) // 注册路由... // [!code highlight] reg.Serve.Run(":8080") } ``` 核心依赖 [#核心依赖] | 依赖 | 版本 | 用途 | | ------------------------- | ------- | --------- | | `gin-gonic/gin` | v1.10+ | Web 框架 | | `go-playground/validator` | v10.30+ | 验证器 | | `joho/godotenv` | v1.5+ | 环境变量加载 | | `redis/go-redis` | v9.14+ | Redis 客户端 | | `gorm.io/gorm` | v1.30+ | ORM 框架 | 设计原则 [#设计原则] 1. **分层解耦** - 四层架构(defined → common → major → plugins),按需引入 2. **单一职责** - 每个包只负责特定的功能领域 3. **统一命名** - 所有包使用 `x` 前缀别名,便于识别 4. **约定优于配置** - 提供合理的默认值,减少配置负担 5. **链路追踪** - 请求 ID 贯穿整个请求生命周期 下一步 [#下一步] # 筱工具(Golang) (/docs/bamboo-base-go) 筱工具(Golang) [#筱工具golang] **筱工具(Golang)** 是筱锋的 Go 语言基础组件库,基于 Gin 框架构建,为微服务提供标准化的基础设施。 特性 [#特性] * 🚀 **快速启动** - 基于 Gin 框架,一键启动 Web 服务 * 🏗️ **多模块分层** - 四层架构(defined / common / major / plugins),按需引入 * 📦 **开箱即用** - 内置日志、配置、数据库等常用组件 * 🆔 **分布式 ID** - 自定义雪花算法,支持业务基因 * 📝 **统一响应** - 标准化 API 响应格式 * 🔒 **类型安全** - 完整的类型定义和错误处理 * 📄 **分页标准化** - 内置 `PageRequest`/`PageResponse` 泛型分页模型 * ▶️ **运行时托管** - 内置 `xMain.Runner`,统一启动与优雅关闭 * 🔌 **gRPC 支持** - 提供 `xGrpcRunner`、追踪拦截器与错误包装能力 模块概览 [#模块概览] | 模块 | 层级 | 描述 | | ------------------------------------------------- | ------- | ------------------------- | | [注册系统](/docs/bamboo-base-go/core/init) | major | Reg 结构体、节点初始化与 Runner 运行时 | | [统一响应](/docs/bamboo-base-go/core/result) | common | BaseResponse 与响应处理中间件 | | [错误处理](/docs/bamboo-base-go/core/result/error) | common | IError 接口与错误构造函数 | | [gRPC(可选)](/docs/bamboo-base-go/grpc) | plugins | 可选扩展:gRPC 启动、服务注册与优雅关闭 | | [雪花算法](/docs/bamboo-base-go/components/snowflake) | common | 64 位分布式 ID 生成 | | [模型基类](/docs/bamboo-base-go/components/models) | major | GORM 实体基类、软删除与分页模型 | | [日志系统](/docs/bamboo-base-go/components/log) | common | 基于 slog 的彩色日志 | | [中间件](/docs/bamboo-base-go/components/middleware) | major | CORS、响应处理等 | | [验证器](/docs/bamboo-base-go/components/validator) | common | 自定义验证规则与中文翻译 | 快速开始 [#快速开始] ```bash go get github.com/bamboo-services/bamboo-base-go/major go get github.com/bamboo-services/bamboo-base-go/common go get github.com/bamboo-services/bamboo-base-go/defined go get github.com/bamboo-services/bamboo-base-go/plugins/grpc # 可选 go mod tidy ``` > 关于 `make test` / `make vet` / `make proto` 等维护命令,请查看 [快速开始](/docs/bamboo-base-go/quick-start) 的「工具链与命令」章节。 ```go package main import ( "context" "github.com/gin-gonic/gin" xLog "github.com/bamboo-services/bamboo-base-go/common/log" xMain "github.com/bamboo-services/bamboo-base-go/major/main" xReg "github.com/bamboo-services/bamboo-base-go/major/register" ) func main() { reg := xReg.Register(context.Background(), nil) log := xLog.WithName(xLog.NamedMAIN) xMain.Runner(reg, log, func(reg *xReg.Reg) { reg.Serve.GET("/ping", func(c *gin.Context) { c.JSON(200, gin.H{"message": "pong"}) }) }) } ``` 更多信息 [#更多信息] # 快速开始 (/docs/bamboo-base-go/quick-start) 快速开始 [#快速开始] 本指南将帮助你快速搭建一个基于 Bamboo Base Go 的 Web 服务。 安装 [#安装] ```bash go get github.com/bamboo-services/bamboo-base-go/major go get github.com/bamboo-services/bamboo-base-go/common go get github.com/bamboo-services/bamboo-base-go/defined go get github.com/bamboo-services/bamboo-base-go/plugins/grpc # 可选 go mod tidy ``` 工具链与命令(接入方 vs 维护方) [#工具链与命令接入方-vs-维护方] `bamboo-base-go` 同时面向 **业务项目接入** 与 **基础库仓库维护**,两类场景的命令不同: | 场景 | 推荐命令 | 说明 | | -------------------- | -------------------------------------- | ----------------- | | 业务项目接入 | `go get ...` + `go mod tidy` | 只安装你实际 import 的模块 | | 本地启动业务服务 | `go run .` | 启动当前业务项目 | | 业务项目测试 | `go test ./...` | 运行当前业务项目测试 | | 维护 bamboo-base-go 仓库 | `make tidy` / `make test` / `make vet` | 统一执行依赖整理、测试和静态检查 | | 生成 gRPC 代码 | `make proto` | 封装 Buf 生成流程 | > `plugins/grpc` 目录包含 `buf.yaml` 与 `buf.gen.yaml`。若在仓库根目录执行 `make proto` 提示找不到 Buf 配置,请切到 `plugins/grpc` 后执行 `buf generate`。 项目结构 [#项目结构] 推荐的项目结构: ``` your-project/ ├── main.go # 入口文件 ├── .env # 环境变量配置 ├── api/ # API 请求/响应结构定义 │ └── user/ │ └── user.go ├── internal/ │ ├── app/ │ │ ├── middleware/ # 中间件 │ │ ├── route/ # 路由注册 │ │ └── startup/ # 启动初始化 │ ├── entity/ # 数据库实体 │ ├── handler/ # HTTP 处理器 │ └── logic/ # 业务逻辑层 └── go.mod ``` 环境配置 [#环境配置] 创建 `.env` 文件: ```bash title=".env" # 调试模式 XLF_DEBUG=true # 服务配置 XLF_HOST=localhost XLF_PORT=8080 # gRPC 配置(可选) GRPC_PORT=1119 GRPC_REFLECTION=false # 数据库配置 DATABASE_HOST=localhost DATABASE_PORT=5432 DATABASE_USER=postgres DATABASE_PASS=your_password DATABASE_NAME=your_database DATABASE_PREFIX=app_ DATABASE_TIMEZONE=Asia/Shanghai # Redis 配置 NOSQL_HOST=localhost NOSQL_PORT=6379 NOSQL_USER= NOSQL_PASS= NOSQL_DATABASE=0 NOSQL_POOL_SIZE=10 NOSQL_PREFIX= # 雪花算法配置(可选,默认自动生成) SNOWFLAKE_DATACENTER_ID=1 SNOWFLAKE_NODE_ID=1 ``` 入口文件 [#入口文件] ```go title="main.go" package main import ( "context" xCtx "github.com/bamboo-services/bamboo-base-go/defined/context" xLog "github.com/bamboo-services/bamboo-base-go/common/log" xMain "github.com/bamboo-services/bamboo-base-go/major/main" xReg "github.com/bamboo-services/bamboo-base-go/major/register" xRegNode "github.com/bamboo-services/bamboo-base-go/major/register/node" "your-project/internal/app/startup" "your-project/internal/app/route" ) func main() { // [!code highlight:7] // 1. 初始化 bamboo-base-go 核心组件(传入自定义节点) reg := xReg.Register(context.Background(), []xRegNode.RegNodeList{ {Key: xCtx.DatabaseKey, Node: startup.InitDatabase}, {Key: xCtx.RedisClientKey, Node: startup.InitRedis}, }) log := xLog.WithName(xLog.NamedMAIN) // 2. 交由 Runner 托管服务生命周期(路由注册 + 启动 + 优雅关闭) xMain.Runner(reg, log, route.NewRoute) } ``` 可选:HTTP + gRPC 一体化启动 [#可选http--grpc-一体化启动] 当服务同时暴露 HTTP 与 gRPC 接口时,可将 gRPC 任务函数直接挂入 `xMain.Runner`: 需要额外引入 gRPC 运行时相关包: ```go import ( xGrpcInterface "github.com/bamboo-services/bamboo-base-go/plugins/grpc/interceptor" xGrpcRunner "github.com/bamboo-services/bamboo-base-go/plugins/grpc/runner" "google.golang.org/grpc" ) ``` ```go title="main.go" grpcTask := xGrpcRunner.New( xGrpcRunner.WithLogger(xLog.WithName(xLog.NamedGRPC)), xGrpcRunner.WithRegisterService(func(ctx context.Context, server grpc.ServiceRegistrar) { // 注册你的 gRPC Service }), // [!code highlight:5] xGrpcRunner.WithUnaryInterceptors( xGrpcInterface.Recover(), xGrpcInterface.InitContext(reg.Init.Ctx), xGrpcInterface.ResponseBuilder(), ), ) xMain.Runner(reg, log, route.NewRoute, grpcTask) ``` 完整说明参见:[gRPC 运行时](/docs/bamboo-base-go/grpc/runner) 业务初始化 [#业务初始化] 数据库和 Redis 通过**节点化系统**注入,通常放在 `startup` 包中实现: ```go title="internal/app/startup/init_database.go" package startup import ( "context" "log/slog" "strings" xEnv "github.com/bamboo-services/bamboo-base-go/defined/env" xLog "github.com/bamboo-services/bamboo-base-go/common/log" "your-project/internal/entity" "gorm.io/driver/postgres" "gorm.io/gorm" "gorm.io/gorm/schema" ) // [!code highlight:4] // 需要自动迁移的数据表 var migrateTables = []interface{}{ &entity.User{}, } // [!code highlight:2] // InitDatabase 数据库初始化节点 func InitDatabase(ctx context.Context) (any, error) { log := xLog.WithName(xLog.NamedINIT) log.Debug(ctx, "正在连接数据库...") // [!code highlight:12] // 构建 DSN dsn := strings.Builder{} dsn.WriteString("host=") dsn.WriteString(xEnv.GetEnvString(xEnv.DatabaseHost, "localhost")) dsn.WriteString(" user=") dsn.WriteString(xEnv.GetEnvString(xEnv.DatabaseUser, "postgres")) dsn.WriteString(" password=") dsn.WriteString(xEnv.GetEnvString(xEnv.DatabasePass, "")) dsn.WriteString(" dbname=") dsn.WriteString(xEnv.GetEnvString(xEnv.DatabaseName, "postgres")) dsn.WriteString(" port=") dsn.WriteString(xEnv.GetEnvString(xEnv.DatabasePort, "5432")) dsn.WriteString(" sslmode=disable") // [!code highlight:11] db, err := gorm.Open(postgres.Open(dsn.String()), &gorm.Config{ NamingStrategy: schema.NamingStrategy{ TablePrefix: xEnv.GetEnvString(xEnv.DatabasePrefix, "app_"), SingularTable: true, }, Logger: xLog.NewSlogLogger(slog.Default().WithGroup(xLog.NamedREPO), xLog.GormLoggerConfig{ SlowThreshold: 200, LogLevel: xLog.LevelInfo, IgnoreRecordNotFoundError: true, }), }) if err != nil { return nil, err } // [!code highlight:2] // 自动迁移 _ = db.AutoMigrate(migrateTables...) log.Info(ctx, "数据库连接成功") // [!code highlight:2] // 返回值将被存入上下文 return db, nil } ``` Redis 初始化 [#redis-初始化] ```go title="internal/app/startup/init_redis.go" package startup import ( "context" xEnv "github.com/bamboo-services/bamboo-base-go/defined/env" xLog "github.com/bamboo-services/bamboo-base-go/common/log" "github.com/redis/go-redis/v9" ) // [!code highlight:2] // InitRedis Redis 初始化节点 func InitRedis(ctx context.Context) (any, error) { log := xLog.WithName(xLog.NamedINIT) log.Debug(ctx, "正在连接 Redis...") // [!code highlight:7] rdb := redis.NewClient(&redis.Options{ Addr: xEnv.GetEnvString(xEnv.NoSqlHost, "localhost") + ":" + xEnv.GetEnvString(xEnv.NoSqlPort, "6379"), Password: xEnv.GetEnvString(xEnv.NoSqlPass, ""), DB: xEnv.GetEnvInt(xEnv.NoSqlDatabase, 0), PoolSize: xEnv.GetEnvInt(xEnv.NoSqlPoolSize, 10), }) log.Info(ctx, "Redis 连接成功") // [!code highlight:2] // 返回值将被存入上下文 return rdb, nil } ``` 实体定义 [#实体定义] ```go title="internal/entity/user.go" package entity import ( xModels "github.com/bamboo-services/bamboo-base-go/major/models" xSnowflake "github.com/bamboo-services/bamboo-base-go/common/snowflake" ) // [!code highlight:7] // User 用户实体 type User struct { xModels.BaseEntity // 继承基础实体(ID、CreatedAt、UpdatedAt) Username string `gorm:"type:varchar(64);uniqueIndex" json:"username"` Email string `gorm:"type:varchar(128);uniqueIndex" json:"email"` Password string `gorm:"type:varchar(256)" json:"-"` } // [!code highlight:4] // GetGene 返回用户基因,用于雪花 ID 生成 func (u *User) GetGene() xSnowflake.Gene { return xSnowflake.GeneUser } ``` 路由注册 [#路由注册] ```go title="internal/app/route/route.go" package route import ( xMiddle "github.com/bamboo-services/bamboo-base-go/major/middleware" xReg "github.com/bamboo-services/bamboo-base-go/major/register" xRoute "github.com/bamboo-services/bamboo-base-go/major/route" "your-project/internal/handler" ) func NewRoute(xReg *xReg.Reg) { engine := xReg.Serve // [!code highlight:3] // 全局异常处理 engine.NoMethod(xRoute.NoMethod) engine.NoRoute(xRoute.NoRoute) // [!code highlight:4] // 全局中间件 engine.Use(xMiddle.ResponseMiddleware) engine.Use(xMiddle.ReleaseAllCors) engine.Use(xMiddle.AllowOption) // [!code highlight:2] // API 路由组 api := engine.Group("/api/v1") { userHandler := handler.NewUserHandler() // [!code highlight:4] // 用户路由 user := api.Group("/user") user.POST("/register", userHandler.Register) user.POST("/login", userHandler.Login) } } ``` Handler 编写 [#handler-编写] ```go title="internal/handler/user.go" package handler import ( xError "github.com/bamboo-services/bamboo-base-go/common/error" xLog "github.com/bamboo-services/bamboo-base-go/common/log" xResult "github.com/bamboo-services/bamboo-base-go/major/result" xUtil "github.com/bamboo-services/bamboo-base-go/common/utility" "github.com/gin-gonic/gin" apiUser "your-project/api/user" "your-project/internal/logic" ) type UserHandler struct { log *xLog.LogNamedLogger } func NewUserHandler() *UserHandler { return &UserHandler{ log: xLog.WithName(xLog.NamedCONT), } } // [!code highlight] // Register 用户注册 func (h *UserHandler) Register(c *gin.Context) { h.log.Info(c, "开始处理用户注册请求") // [!code highlight:4] // 1. 验证并绑定数据 req := xUtil.Bind(c, &apiUser.RegisterRequest{}).Data() if req == nil { return } // [!code highlight:5] // 2. 调用业务逻辑(通过上下文获取资源) user, xErr := logic.NewUserLogic().CreateUser(c, req.Username, req.Email, req.Password) if xErr != nil { _ = c.Error(xErr) // 传递给错误中间件处理 return } // [!code highlight:2] // 3. 返回成功响应 xResult.SuccessHasData(c, "注册成功", user) } // [!code highlight] // Login 用户登录 func (h *UserHandler) Login(c *gin.Context) { h.log.Info(c, "开始处理用户登录请求") req := xUtil.Bind(c, &apiUser.LoginRequest{}).Data() if req == nil { return } // 查找用户 user, xErr := logic.NewUserLogic().GetUserByUsername(c, req.Username) if xErr != nil { _ = c.Error(xErr) return } // 验证密码 if !logic.NewUserLogic().VerifyPassword(user.Password, req.Password) { // [!code highlight:2] // 创建错误并传递给中间件 _ = c.Error(xError.NewError(c.Request.Context(), xError.Unauthorized, "用户名或密码错误", false)) return } xResult.SuccessHasData(c, "登录成功", user) } ``` Logic 层编写 [#logic-层编写] ```go title="internal/logic/user.go" package logic import ( xError "github.com/bamboo-services/bamboo-base-go/common/error" xLog "github.com/bamboo-services/bamboo-base-go/common/log" xUtil "github.com/bamboo-services/bamboo-base-go/common/utility" xCtxUtil "github.com/bamboo-services/bamboo-base-go/common/utility/context" "github.com/gin-gonic/gin" "gorm.io/gorm" "your-project/internal/entity" ) type UserLogic struct { log *xLog.LogNamedLogger } func NewUserLogic() *UserLogic { return &UserLogic{ log: xLog.WithName(xLog.NamedLOGC), } } // [!code highlight] // CreateUser 创建用户 func (l *UserLogic) CreateUser(c *gin.Context, username, email, password string) (*entity.User, *xError.Error) { l.log.Info(c, "开始创建用户") // [!code highlight:2] // 从上下文获取数据库连接 ctx := c.Request.Context() db := xCtxUtil.MustGetDB(ctx) // 检查用户名是否存在 var count int64 db.Model(&entity.User{}).Where("username = ?", username).Count(&count) if count > 0 { // [!code highlight] return nil, xError.NewError(c.Request.Context(), xError.Existed, "用户名已存在", false) } // 加密密码 hashedPassword, err := xUtil.Password().EncryptString(password) if err != nil { // [!code highlight] return nil, xError.NewInternalServerError(c.Request.Context(), "密码加密失败", err) } // [!code highlight:2] // 生成雪花 ID(从上下文获取节点) userID := xCtxUtil.MustGenerateGeneSnowflakeID(ctx, entity.User{}.GetGene()) user := &entity.User{ Username: username, Email: email, Password: hashedPassword, } user.ID = userID if err := db.Create(user).Error; err != nil { // [!code highlight] return nil, xError.NewInternalServerError(c.Request.Context(), "创建用户失败", err) } return user, nil } // [!code highlight] // GetUserByUsername 根据用户名获取用户 func (l *UserLogic) GetUserByUsername(c *gin.Context, username string) (*entity.User, *xError.Error) { // [!code highlight:2] // 从上下文获取数据库连接 db := xCtxUtil.MustGetDB(c.Request.Context()) var user entity.User if err := db.Where("username = ?", username).First(&user).Error; err != nil { if err == gorm.ErrRecordNotFound { // [!code highlight] return nil, xError.NewError(c.Request.Context(), xError.UserNotFound, "用户不存在", false, err) } // [!code highlight] return nil, xError.NewInternalServerError(c.Request.Context(), "查询用户失败", err) } return &user, nil } // VerifyPassword 验证密码 func (l *UserLogic) VerifyPassword(hashedPassword, password string) bool { return xUtil.Password().IsValid(password, hashedPassword) } ``` API 结构定义 [#api-结构定义] ```go title="api/user/user.go" package user import "your-project/internal/entity" // RegisterRequest 注册请求 type RegisterRequest struct { Username string `json:"username" binding:"required,min=3,max=32"` Email string `json:"email" binding:"required,email"` Password string `json:"password" binding:"required,min=6"` } // LoginRequest 登录请求 type LoginRequest struct { Username string `json:"username" binding:"required"` Password string `json:"password" binding:"required"` } // UserResponse 用户响应 type UserResponse struct { *entity.User Token string `json:"token,omitempty"` } ``` 运行项目 [#运行项目] ```bash # 启动服务 go run . ``` 启动成功后,你将看到类似输出: ``` 2024-01-15 10:00:00 [INFO] [INIT] 初始化系统上下文 2024-01-15 10:00:00 [INFO] [INIT] 数据库连接成功 2024-01-15 10:00:00 [INFO] [INIT] Redis 连接成功 2024-01-15 10:00:00 [INFO] [MAIN] 服务器启动成功 addr=http://localhost:8080 ``` 测试接口 [#测试接口] ```bash # 注册用户 curl -X POST http://localhost:8080/api/v1/user/register \ -H "Content-Type: application/json" \ -d '{"username":"test","email":"test@example.com","password":"123456"}' # 用户登录 curl -X POST http://localhost:8080/api/v1/user/login \ -H "Content-Type: application/json" \ -d '{"username":"test","password":"123456"}' ``` 下一步 [#下一步] # 模块架构 (/docs/bamboo-base-java/architecture) 模块架构 [#模块架构] 筱工具(Java) 采用多模块架构,各模块职责明确、可独立引入。 模块依赖关系 [#模块依赖关系] ``` bamboo-base (核心库,零框架依赖) ├── bamboo-mvc (Spring MVC 集成) ├── bamboo-webflux (Spring WebFlux 集成) ├── bamboo-notify (通知服务) └── bamboo-triple (Dubbo Triple RPC) ``` 所有业务模块均依赖 `bamboo-base`,但彼此之间互不依赖,可按需组合使用。 模块职责 [#模块职责] | 模块 | 定位 | 核心能力 | | ------------------ | ----------- | ----------------------- | | **bamboo-base** | 基础层 | 通用响应体、错误码、雪花算法 | | **bamboo-mvc** | MVC 集成层 | 异常处理器、过滤器、AOP 切面、上下文管理 | | **bamboo-webflux** | WebFlux 集成层 | 响应式异常处理、WebFilter、切面 | | **bamboo-notify** | 通知层 | 邮件通知、Webhook 回调、消息模板 | | **bamboo-triple** | RPC 层 | Dubbo Triple 请求校验、上下文传播 | Maven 坐标 [#maven-坐标] 所有模块共享相同的 groupId 和版本号: ```xml title="pom.xml" 2.0.0 com.x-lf.utility bamboo-base ${general-utils.version} com.x-lf.utility bamboo-mvc ${general-utils.version} com.x-lf.utility bamboo-webflux ${general-utils.version} com.x-lf.utility bamboo-notify ${general-utils.version} com.x-lf.utility bamboo-triple ${general-utils.version} ``` 技术栈 [#技术栈] | 技术 | 版本 | 说明 | | ------------ | ----- | ----------------- | | Java | 17 | 最低要求 JDK 17 | | Spring Boot | 3.5.x | 基础框架 | | MyBatis-Plus | 3.5.x | ORM 框架(MVC 模块可选) | | HuTool | 5.8.x | 工具库 | | Dubbo | 3.3.x | RPC 框架(Triple 模块) | 推荐用法 [#推荐用法] * **传统 Web 服务**:`bamboo-base` + `bamboo-mvc` * **响应式 Web 服务**:`bamboo-base` + `bamboo-webflux` * **微服务 + RPC**:`bamboo-base` + `bamboo-mvc` + `bamboo-triple` * **带通知功能**:在上述基础上追加 `bamboo-notify` 下一步 [#下一步] # 筱工具(Java) (/docs/bamboo-base-java) 筱工具(Java) [#筱工具java] **筱工具(Java)** 是筱锋的 Java 语言多模块基础组件库,基于 Spring Boot 3 构建,为 Web 服务与微服务提供标准化的基础设施。 特性 [#特性] * 🚀 **Spring Boot 3 原生** - 基于 Java 17 + Spring Boot 3.5,完整自动配置支持 * 📦 **多模块设计** - 五大模块(base / mvc / webflux / notify / triple),按需引入 * 📝 **统一响应** - `BaseResponse` 泛型响应体,标准化 API 输出格式 * 🔢 **70+ 错误码** - `ErrorCode` 枚举覆盖通用、客户端、服务端全场景 * 🆔 **分布式 ID** - 雪花算法 ID 生成、解析与验证 * 🛡️ **异常兜底** - 多层 `@ControllerAdvice` 异常处理器,覆盖 Java / Spring / MySQL / PostgreSQL 异常 * 🔍 **AOP 日志** - 日志切面、调试切面、业务日志切面 * 🔒 **过滤器链** - CORS、权限、上下文注入等开箱即用过滤器 * ⚡ **双框架** - 同时支持 Spring MVC 与 Spring WebFlux * 📧 **通知服务** - 邮件 + Webhook 多渠道通知 * 🔌 **Dubbo Triple** - Dubbo Triple RPC 请求校验与上下文传播 模块概览 [#模块概览] | 模块 | Maven Artifact | 描述 | | ------------------------------------------------ | ---------------- | --------------------------- | | [核心库](/docs/bamboo-base-java/base) | `bamboo-base` | BaseResponse、ErrorCode、雪花算法 | | [Spring MVC](/docs/bamboo-base-java/mvc) | `bamboo-mvc` | 异常处理、过滤器、AOP 切面、上下文管理 | | [Spring WebFlux](/docs/bamboo-base-java/webflux) | `bamboo-webflux` | 响应式版本的异常处理、WebFilter、切面 | | [通知服务](/docs/bamboo-base-java/notify) | `bamboo-notify` | 邮件通知、Webhook 通知、模板引擎 | | [Dubbo Triple](/docs/bamboo-base-java/triple) | `bamboo-triple` | Triple RPC 请求/响应模型、切面、注解 | 快速开始 [#快速开始] ```xml title="pom.xml" com.x-lf.utility bamboo-base 2.0.0 com.x-lf.utility bamboo-mvc 2.0.0 ``` ```java title="DemoController.java" import com.xlf.utility.BaseResponse; import com.xlf.utility.ErrorCode; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api") public class DemoController { @GetMapping("/ping") public BaseResponse ping() { // [!code highlight:2] // 返回标准化成功响应 return new BaseResponse<>(ErrorCode.SUCCESS, "pong"); } } ``` 更多信息 [#更多信息] # 快速开始 (/docs/bamboo-base-java/quick-start) import { TypeTable } from '@/components/type-table'; 快速开始 [#快速开始] 本指南将帮助你快速搭建一个基于筱工具(Java) 的 Spring Boot Web 服务。 安装 [#安装] 在 `pom.xml` 中添加依赖: ```xml title="pom.xml" com.x-lf.utility bamboo-base 2.0.0 com.x-lf.utility bamboo-mvc 2.0.0 ``` 项目结构 [#项目结构] 推荐的项目结构: ``` your-project/ ├── src/main/java/com/example/demo/ │ ├── DemoApplication.java # 启动类 │ ├── controller/ # 控制器 │ │ └── UserController.java │ ├── service/ # 业务逻辑 │ │ └── UserService.java │ ├── mapper/ # 数据访问层 │ │ └── UserMapper.java │ ├── model/ │ │ ├── entity/ # 数据库实体 │ │ └── dto/ # 数据传输对象 │ └── exception/ # 自定义异常处理 │ └── GlobalExceptionHandler.java ├── src/main/resources/ │ └── application.yml # 配置文件 └── pom.xml ``` 配置文件 [#配置文件] ```yaml title="application.yml" # 服务配置 server: port: 8080 # 筱工具上下文配置 bamboo: context: enable-input: true # 允许外部传入 Context UUID exclude-urls: # 排除的 URL(不注入上下文) - /actuator/** # 数据库配置(可选,使用 MyBatis-Plus 时) spring: datasource: url: jdbc:postgresql://localhost:5432/your_database username: postgres password: your_password driver-class-name: org.postgresql.Driver ``` 启动类 [#启动类] ```java title="DemoApplication.java" package com.example.demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class DemoApplication { public static void main(String[] args) { // [!code highlight:2] // bamboo-mvc 的自动配置会自动注册过滤器、异常处理器和 AOP 切面 SpringApplication.run(DemoApplication.class, args); } } ``` 控制器编写 [#控制器编写] ```java title="controller/UserController.java" package com.example.demo.controller; import com.xlf.utility.BaseResponse; import com.xlf.utility.ErrorCode; import com.xlf.utility.mvc.ResultUtil; import com.example.demo.model.dto.UserDTO; import com.example.demo.service.UserService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/v1/user") @RequiredArgsConstructor public class UserController { private final UserService userService; // [!code highlight] // 获取用户信息 @GetMapping("/{id}") public ResponseEntity> getUser(@PathVariable Long id) { // [!code highlight:2] // 使用 ResultUtil 返回标准化响应 UserDTO user = userService.getUserById(id); return ResultUtil.success("获取成功", user); } // [!code highlight] // 创建用户 @PostMapping public ResponseEntity> createUser(@RequestBody UserDTO userDTO) { userService.createUser(userDTO); return ResultUtil.success("创建成功"); } } ``` 业务逻辑层 [#业务逻辑层] ```java title="service/UserService.java" package com.example.demo.service; import com.xlf.utility.ErrorCode; import com.xlf.utility.exception.library.BusinessException; import com.example.demo.model.dto.UserDTO; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor public class UserService { // [!code highlight] // 获取用户 public UserDTO getUserById(Long id) { // 业务逻辑... if (id == null) { // [!code highlight:2] // 抛出业务异常,会被全局异常处理器捕获并转换为标准响应 throw new BusinessException("用户 ID 不能为空", ErrorCode.PARAMETER_ERROR); } return new UserDTO(id, "test_user", "test@example.com"); } public void createUser(UserDTO userDTO) { // 业务逻辑... } } ``` 自定义异常处理 [#自定义异常处理] 如果需要扩展异常处理,继承 `SystemExceptionHandler`: ```java title="exception/GlobalExceptionHandler.java" package com.example.demo.exception; import com.xlf.utility.BaseResponse; import com.xlf.utility.ErrorCode; import com.xlf.utility.mvc.ResultUtil; import com.xlf.utility.mvc.exception.SystemExceptionHandler; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; // [!code highlight:2] // 继承 SystemExceptionHandler 以获得内置异常处理能力 @RestControllerAdvice public class GlobalExceptionHandler extends SystemExceptionHandler { // [!code highlight:2] // 添加自定义异常处理 @ExceptionHandler(IllegalStateException.class) public ResponseEntity> handleIllegalState(IllegalStateException e) { return ResultUtil.error(ErrorCode.SERVER_INTERNAL_ERROR, e.getMessage(), null); } } ``` 运行项目 [#运行项目] ```bash # 启动服务 mvn spring-boot:run ``` 启动成功后,你将看到类似输出: ``` . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ 2026-01-15 10:00:00 [INFO] [MAIN] Started DemoApplication in 2.5 seconds ``` 测试接口 [#测试接口] ```bash # 获取用户 curl http://localhost:8080/api/v1/user/1 # 创建用户 curl -X POST http://localhost:8080/api/v1/user \ -H "Content-Type: application/json" \ -d '{"username":"test","email":"test@example.com"}' ``` 成功响应示例: ```json title="response.json" { "context": "550e8400-e29b-41d4-a716-446655440000", "output": "Success", "code": 200, "message": "获取成功", "errorMessage": null, "duration": 15, "data": { "id": 1, "username": "test_user", "email": "test@example.com" } } ``` 下一步 [#下一步] # AI 集成 (/docs/guide/ai-integration) AI 集成 [#ai-集成] 本站原生支持 LLM(大语言模型)友好协议,你可以让 AI 助手直接读取文档内容,获得更精准的技术支持。 本文档站所有 LLM 可访问资源的根路径为: ``` https://doc.x-lf.com ``` 后续所有路径示例均为**相对路径**,使用时请拼接上方根路径。 使用方式 [#使用方式] 方式一:文档索引 [#方式一文档索引] 访问 `/llms.txt` 获取所有文档页面的索引列表,然后按需请求单页内容。 ``` https://doc.x-lf.com/llms.txt ``` 方式二:单页 MDX 模式 [#方式二单页-mdx-模式] 每个文档页面都支持 MDX 原始内容获取,在路径后加 `.mdx` 前缀即可: ``` https://doc.x-lf.com/llms.mdx/docs/bamboo-base-go/quick-start https://doc.x-lf.com/llms.mdx/docs/bamboo-base-java/quick-start ``` 方式三:MCP 服务(推荐) [#方式三mcp-服务推荐] 本站提供兼容 MCP(Model Context Protocol)Streamable HTTP 协议的服务端点,支持任何 MCP 客户端直接接入,无需安装任何二进制文件: ``` https://doc.x-lf.com/api/mcp ``` MCP 客户端连接配置示例: ```json { "mcpServers": { "bamboo-document": { "type": "streamableHttp", "url": "https://doc.x-lf.com/api/mcp" } } } ``` MCP 方式无需手动拼接 URL,AI 助手可通过工具调用自动获取所需的文档内容,是最便捷的集成方式。适用于 Claude Code、Cursor、Windsurf 等支持 MCP 的工具。 方式四:索引 + 单页结合 [#方式四索引--单页结合] 如果没有 MCP 支持,推荐**先读索引,再按需拉取单页内容**: 1. 访问 `/llms.txt` 获取所有页面的索引列表 2. 从索引中定位与问题相关的页面路径 3. 访问 `/llms.mdx/` 获取该页面的完整内容 4. 基于内容回答问题 这样可以避免一次性加载全量文档,让 AI 只获取真正需要的内容。 Claude Code 集成 [#claude-code-集成] 如果你使用 Claude Code 命令行工具,可以通过 MCP 服务直接集成竹简文档,让 Claude 自动获取所需文档内容。 Agent Prompt 模板 [#agent-prompt-模板] 以下 prompt 可直接复制到任意支持网络请求的 AI Agent 中使用: ```markdown 请按以下步骤获取竹简文档并回答我的问题: 1. 获取文档索引:https://doc.x-lf.com/llms.txt 2. 从索引中找到与我的问题相关的页面路径 3. 获取该页面内容:https://doc.x-lf.com/llms.mdx/<找到的路径> 4. 基于文档内容回答:[你的问题] ``` 将上述 prompt 复制到 Claude、ChatGPT、Cursor 或其他支持网络请求的 AI 工具中,替换最后一行为你的具体问题,即可自动获取并分析文档内容。 相关链接 [#相关链接] * [LLM 协议文件](/llms.txt) - 文档索引 * [完整文档](/llms-full.txt) - 所有内容合并 * [页面操作](./navigation) - 每页可复制 MDX 源码 参考资料 [#参考资料] * **bamboo-base-go 全局文档**: [https://doc.x-lf.com/llms.txt](https://doc.x-lf.com/llms.txt) * **具体路径查询**: `https://doc.x-lf.com/llms.mdx/` # Claude Code 集成 (/docs/guide/claude-code) import { Callout } from 'fumadocs-ui/components/callout'; Claude Code 集成 [#claude-code-集成] Claude Code 是 Anthropic 官方的命令行 AI 助手。通过集成竹简文档 MCP 服务,可以让 Claude Code 直接访问项目文档,获得更精准的代码辅助。 MCP(Model Context Protocol)是 Anthropic 提出的模型上下文协议,允许 AI 助手通过标准化接口访问外部数据源和工具。 配置步骤 [#配置步骤] 通过 Streamable HTTP 端点连接,无需安装任何软件。 编辑 Claude Code 的 MCP 配置文件(项目级 `.claude/settings.json` 或全局 `~/.claude/settings.json`),添加以下内容: ```json { "mcpServers": { "bamboo-document": { "type": "streamableHttp", "url": "https://doc.x-lf.com/api/mcp" } } } ``` 配置完成后重启 Claude Code,MCP 服务将自动加载。 无需安装、无需编译、跨平台支持。只要有网络连接即可使用,服务端自动维护文档内容。 *** 可用 MCP 工具 [#可用-mcp-工具] | 工具名 | 功能 | 参数 | | -------- | ------------- | ----------------------------------- | | `sector` | 获取所有可用的文档板块列表 | 无 | | `list` | 查看指定板块的文档目录列表 | `sector`(必填)、`search`(可选关键词) | | `detail` | 获取指定文档的完整内容 | `sector`(板块标识)、`path`(文档路径) | | `search` | 在文档内容中搜索关键词 | `query`(必填)、`sector`(可选)、`path`(可选) | 可用板块 [#可用板块] 当前支持的文档板块: * `bamboo-base-go` - Go 语言基础框架文档 * `bamboo-base-java` - Java 语言基础框架文档 * `guide` - 使用指南 使用 `sector` 工具可随时获取最新的板块列表,新板块会持续添加。 使用示例 [#使用示例] 在 Claude Code 中,直接提问即可。Claude 会自动调用 MCP 工具获取相关文档内容: ``` 帮我查一下 bamboo-base-go 的快速开始指南 ``` ``` 搜索 bamboo-base-java 中的业务异常处理方式 ``` 让 Claude 知道你在使用哪个板块(如 `bamboo-base-go`),可以获得更精准的文档匹配结果。 相关链接 [#相关链接] * [AI 集成](./ai-integration) - 通用 AI 集成方式 * [文档导航](./navigation) - 文档站使用指南 * **MCP 端点**: [https://doc.x-lf.com/api/mcp](https://doc.x-lf.com/api/mcp) # 入门指南 (/docs/guide) 入门指南 [#入门指南] 欢迎使用竹简文档!本指南将帮助你快速了解如何使用本站获取所需信息。 关于竹简库 [#关于竹简库] 竹简库是一套跨语言的后端服务组件库,旨在提供统一风格的 API 设计和开发体验。目前支持: 快速开始 [#快速开始] 选择你的语言 [#选择你的语言] 根据你的技术栈选择对应的文档: * **Go 开发者**:前往 [Go 快速开始](../bamboo-base-go/quick-start) 了解如何在 Gin 项目中集成 * **Java 开发者**:前往 [Java 快速开始](../bamboo-base-java/quick-start) 了解如何在 Spring Boot 项目中集成 核心概念 [#核心概念] 竹简库在两种语言中提供了相似的核心功能: | 功能 | 说明 | | --------- | --------------------------- | | **统一响应** | `BaseResponse` 标准化 API 响应格式 | | **错误码** | 统一的错误码体系,便于前后端协作 | | **雪花算法** | 分布式 ID 生成器 | | **上下文管理** | 请求上下文与链路追踪 | | **异常处理** | 分层异常处理器,优雅处理各类异常 | 反馈与贡献 [#反馈与贡献] 如果你发现文档有误或有改进建议: * **GitHub**:[bamboo-services](https://github.com/bamboo-services) 提交 Issue 或 PR * **网站**:[筱锋的博客](https://www.x-lf.com) 留言 *** # 文档导航 (/docs/guide/navigation) 文档导航 [#文档导航] 了解如何高效浏览和使用竹简文档站。 侧边栏 [#侧边栏] 左侧侧边栏提供了完整的文档目录结构,你可以: * 点击展开/折叠分类 * 使用搜索功能快速定位(按 `Ctrl+K` 或 `Cmd+K`) 侧边栏特性 [#侧边栏特性] | 特性 | 说明 | | -------- | ---------------------------------- | | **折叠记忆** | 侧边栏会记住你的折叠状态,下次访问时保持一致 | | **快速跳转** | 使用键盘快捷键 `Ctrl+K` / `Cmd+K` 快速搜索并跳转 | | **层级导航** | 支持多级目录结构,清晰展示文档层次 | 页面内导航 [#页面内导航] 每个文档页面右侧会显示当前页面的目录大纲(TOC),方便跳转到感兴趣的章节。 大纲特性 [#大纲特性] * **自动生成**:根据标题层级自动生成 * **高亮当前**:滚动时自动高亮当前阅读的章节 * **点击跳转**:点击任意条目快速定位 页面操作 [#页面操作] 每个文档页面顶部提供了便捷操作按钮: 复制原始内容 [#复制原始内容] 点击复制按钮将当前页面的 MDX 源码复制到剪贴板,方便: * 粘贴到 AI 对话中 * 保存到本地笔记 * 分享给他人 在 AI 平台打开 [#在-ai-平台打开] 一键跳转到 AI 平台并自动带上文档链接,支持: * **ChatGPT**:跳转到 ChatGPT 并附带文档链接 * **Claude**:跳转到 Claude 并附带文档链接 * **其他平台**:更多 AI 平台持续添加中 如果你想在 AI 平台中讨论某个具体页面,直接点击「在 AI 平台打开」按钮,比手动复制链接更方便! 搜索功能 [#搜索功能] 全局搜索 [#全局搜索] 按 `Ctrl+K`(Windows/Linux)或 `Cmd+K`(macOS)打开搜索面板: * 支持模糊搜索 * 搜索标题和内容 * 显示匹配结果预览 搜索技巧 [#搜索技巧] | 技巧 | 示例 | | ---- | ---------------- | | 精确匹配 | `"BaseResponse"` | | 模块搜索 | `bamboo-base-go` | | 功能搜索 | `雪花算法` | *** # gRPC 常量 (/docs/bamboo-base-go/grpc/constants) import { TypeTable } from '@/components/type-table'; gRPC 常量 [#grpc-常量] `bamboo-base-go` 提供类型安全的 gRPC 元数据常量,区分 **Metadata**(请求头)和 **Trailer**(响应尾)两种语义。 ```go import xGrpcConst "github.com/xiaolfeng/bamboo-base-go/plugins/grpc/constant" ``` Metadata 常量 [#metadata-常量] Metadata 用于传递**请求头元数据**,如认证信息、请求标识等。 ```go title="grpc/constant/metadata.go" type Metadata string const ( MetadataAppAccessID Metadata = "app_access_id" // 应用访问标识符 MetadataAppSecretKey Metadata = "app_secret_key" // 应用密钥 MetadataRequestUUID Metadata = "x_request_uuid" // 请求唯一标识符 ) ``` **类型方法**: ```go func (md Metadata) String() string ``` > `Metadata` 实现了 `fmt.Stringer` 接口,可直接转换为字符串用于 gRPC 元数据操作。 Trailer 常量 [#trailer-常量] Trailer 用于传递**响应尾元数据**,如请求追踪标识的回传。 ```go title="grpc/constant/trailer.go" type Trailer string const ( TrailerRequestUUID Trailer = "x_request_uuid" // 请求唯一标识符(回传) ) ``` **类型方法**: ```go func (md Trailer) String() string ``` 语义边界 [#语义边界] | 类型 | 方向 | 用途 | | ------------ | --------- | --------------- | | **Metadata** | 客户端 → 服务端 | 请求头元数据(认证、标识传递) | | **Trailer** | 服务端 → 客户端 | 响应尾元数据(追踪标识回传) | > `Trace` 拦截器会从 Metadata 读取 `x_request_uuid`,处理完成后写入 Trailer 供客户端追踪。 ExtractMetadata 工具函数 [#extractmetadata-工具函数] `utility` 包提供从 gRPC 上下文中提取元数据的工具函数。 ```go title="plugins/grpc/utility/grpc.go" import xGrpcUtil "github.com/xiaolfeng/bamboo-base-go/plugins/grpc/utility" func ExtractMetadata(ctx context.Context, key xGrpcConst.Metadata) (string, *xError.Error) ``` **功能说明**: * 从 incoming metadata 中提取指定键的第一个非空白值 * 若上下文中不存在元数据或指定键无有效值,返回 `xError.NotExist` 错误 **使用示例**: ```go import ( xGrpcConst "github.com/xiaolfeng/bamboo-base-go/plugins/grpc/constant" xGrpcUtil "github.com/xiaolfeng/bamboo-base-go/plugins/grpc/utility" ) func (s *Server) Handle(ctx context.Context, req *pb.Request) (*pb.Response, error) { // [!code highlight:2] appID, xErr := xGrpcUtil.ExtractMetadata(ctx, xGrpcConst.MetadataAppAccessID) secretKey, xErr := xGrpcUtil.ExtractMetadata(ctx, xGrpcConst.MetadataAppSecretKey) if xErr != nil { return nil, xErr } // 使用提取的认证信息... } ``` 完整中间件示例 [#完整中间件示例] 以下是一个完整的 gRPC 认证中间件实现,展示 Metadata 常量的实际使用: ```go title="middleware/app_verify.go" import ( "context" xError "github.com/xiaolfeng/bamboo-base-go/common/error" xGrpcConst "github.com/xiaolfeng/bamboo-base-go/plugins/grpc/constant" xGrpcUtil "github.com/xiaolfeng/bamboo-base-go/plugins/grpc/utility" xLog "github.com/xiaolfeng/bamboo-base-go/common/log" xSnowflake "github.com/xiaolfeng/bamboo-base-go/common/snowflake" "google.golang.org/grpc" ) func UnaryAppVerify(mainCtx context.Context) grpc.UnaryServerInterceptor { log := xLog.WithName(xLog.NamedMIDE, "UnaryAppVerify") appLogic := logic.NewAppLogic(mainCtx) return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { log.Info(ctx, "验证应用身份") // [!code highlight:2] appIDStr, xErr := xGrpcUtil.ExtractMetadata(ctx, xGrpcConst.MetadataAppAccessID) secretKey, xErr := xGrpcUtil.ExtractMetadata(ctx, xGrpcConst.MetadataAppSecretKey) if xErr != nil { return nil, xErr } appID, err := xSnowflake.ParseSnowflakeID(appIDStr) if err != nil { return nil, xError.NewError(ctx, xError.ValidationError, "应用 ID 格式无效", false, err) } _, xErr = appLogic.Authenticate(ctx, appID, secretKey) if xErr != nil { return nil, xErr } return handler(ctx, req) } } ``` 下一步 [#下一步] # gRPC(可选) (/docs/bamboo-base-go/grpc) gRPC(可选) [#grpc可选] `bamboo-base-go` 的核心是 HTTP + 节点化初始化;gRPC 属于**可选扩展能力**,按需接入。 适用场景: * 需要同时暴露 HTTP 与 gRPC 接口。 * 希望与 `xMain.Runner` 共享同一套信号处理与优雅关闭流程。 * 需要统一的 trace、错误码映射与结构化响应(由 `ResponseBuilder` 拦截器统一处理)。 快速入口 [#快速入口] # 内置拦截器 (/docs/bamboo-base-go/grpc/interceptors) 内置拦截器 [#内置拦截器] `bamboo-base-go` 将拦截器按类型拆分到独立包,并提供服务级中间件分发机制。 ```go // 一元拦截器 import xGrpcIUnary "github.com/bamboo-services/bamboo-base-go/plugins/grpc/interceptor/unary" // 流式拦截器 import xGrpcIStream "github.com/bamboo-services/bamboo-base-go/plugins/grpc/interceptor/stream" ``` 一元拦截器(Unary) [#一元拦截器unary] 1) Trace(默认启用) [#1-trace默认启用] ```go title="grpc/interceptor/unary/trace.go" func Trace() grpc.UnaryServerInterceptor ``` 作用: * 从 incoming metadata 读取 `x_request_uuid`;若不存在则自动生成 UUID。 * 将请求 ID 写入 `context`(`xCtx.RequestKey`)。 * 记录请求开始时间(`xCtx.UserStartTimeKey`)。 * 将 `x_request_uuid` 写入 gRPC **Trailer**,便于全链路追踪。 > `xGrpcRunner.New(...)` 默认会把 `Trace()` 放到拦截器链首位。 2) Recover(可选挂载) [#2-recover可选挂载] ```go title="grpc/interceptor/unary/recover.go" func Recover() grpc.UnaryServerInterceptor ``` 作用: * 捕获 handler 中的 panic,避免服务进程崩溃。 * 将 panic 转换为统一 gRPC 错误响应(内部使用 `ServerInternalError`)。 * 记录方法名与 panic 内容,便于排障。 3) InitContext(可选挂载) [#3-initcontext可选挂载] ```go title="grpc/interceptor/unary/init_context.go" func InitContext(mainCtx context.Context) grpc.UnaryServerInterceptor ``` 作用: * 从主上下文提取 `xCtx.RegNodeKey`(节点初始化结果集合)。 * 注入到每个 gRPC 请求上下文,便于业务逻辑复用已注册组件(如数据库、Redis 等)。 4) ResponseBuilder(可选挂载) [#4-responsebuilder可选挂载] ```go title="grpc/interceptor/unary/response_builder.go" func ResponseBuilder() grpc.UnaryServerInterceptor ``` 作用: * **统一处理 handler 返回值**,自动完成错误转换与响应元数据注入。 * 应放在拦截器链**末端**(最接近 handler),以捕获所有上游行为。 三种处理路径 [#三种处理路径] | 场景 | 条件 | 行为 | | --------- | ------------------ | ----------------------------------------------------- | | **错误转换** | handler 返回 `error` | 提取 `*xError.Error`,映射为 gRPC status error | | **成功注入** | handler 返回响应对象 | 提取 `BaseResponse`,注入 `context`(追踪 ID)和 `overhead`(耗时) | | **防御性处理** | 既无响应也无错误 | 返回 `DeveloperError`,提示检查代码逻辑 | HTTP → gRPC 状态码映射 [#http--grpc-状态码映射] `ResponseBuilder` 内部通过 `xGrpc.ToGrpcStatusCode` 将业务错误码映射为 gRPC 状态码: | HTTP 状态码 | gRPC Status Code | | -------- | -------------------- | | 400 | `InvalidArgument` | | 401 | `Unauthenticated` | | 403 | `PermissionDenied` | | 404 | `NotFound` | | 405 | `Unimplemented` | | 406 | `FailedPrecondition` | | 408 | `DeadlineExceeded` | | 409 | `Aborted` | | 410 | `NotFound` | | 413 | `ResourceExhausted` | | 415 | `InvalidArgument` | | 422 | `FailedPrecondition` | | 429 | `ResourceExhausted` | | 500 | `Internal` | | 502 | `Unavailable` | | 503 | `Unavailable` | | 504 | `DeadlineExceeded` | | 其他 | `Unknown` | 5) Middleware(服务级中间件分发) [#5-middleware服务级中间件分发] ```go title="grpc/interceptor/unary/middleware.go" func Middleware() grpc.UnaryServerInterceptor ``` 作用: * 根据 `FullMethod` 解析出服务名,在全局注册表中查找对应的中间件链并依次执行。 * 若服务未注册中间件,直接透传到下一个 handler。 * 中间件链执行遵循**洋葱模型**。 *** 流式拦截器(Stream) [#流式拦截器stream] 流式拦截器与一元拦截器功能对应,用于处理 gRPC 流式请求。 1) Trace [#1-trace] ```go title="grpc/interceptor/stream/trace.go" func Trace() grpc.StreamServerInterceptor ``` 2) Recover [#2-recover] ```go title="grpc/interceptor/stream/recover.go" func Recover() grpc.StreamServerInterceptor ``` 3) InitContext [#3-initcontext] ```go title="grpc/interceptor/stream/init_context.go" func InitContext(mainCtx context.Context) grpc.StreamServerInterceptor ``` 4) Middleware [#4-middleware] ```go title="grpc/interceptor/stream/middleware.go" func Middleware() grpc.StreamServerInterceptor ``` > 流式拦截器**没有** `ResponseBuilder`,因为流式响应由业务代码直接控制。 *** 服务级中间件分发 [#服务级中间件分发] 概述 [#概述] `xGrpcMiddle` 包提供**按服务注册中间件**的能力,支持将中间件精确绑定到特定 gRPC 服务。 ```go import xGrpcMiddle "github.com/bamboo-services/bamboo-base-go/plugins/grpc/middleware" ``` 注册函数 [#注册函数] ```go // 注册一元中间件 func UseUnary(desc grpc.ServiceDesc, middlewares ...UnaryMiddlewareFunc) // 注册流式中间件 func UseStream(desc grpc.ServiceDesc, middlewares ...StreamMiddlewareFunc) ``` 查找函数 [#查找函数] ```go // 查找一元中间件链 func LookupUnary(serviceName string) []UnaryMiddlewareFunc // 查找流式中间件链 func LookupStream(serviceName string) []StreamMiddlewareFunc // 从 FullMethod 提取服务名 func ExtractServiceName(fullMethod string) string ``` 洋葱模型执行顺序 [#洋葱模型执行顺序] 注册顺序 `[A, B, C]` 的执行顺序为: ``` A-enter → B-enter → C-enter → handler → C-exit → B-exit → A-exit ``` 使用示例 [#使用示例] **1. 定义中间件**: ```go title="middleware/app_verify.go" func UnaryAppVerify(mainCtx context.Context) grpc.UnaryServerInterceptor { log := xLog.WithName(xLog.NamedMIDE, "UnaryAppVerify") appLogic := logic.NewAppLogic(mainCtx) return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { // 从 metadata 提取认证信息 appIDStr, xErr := xGrpcUtil.ExtractMetadata(ctx, xGrpcConst.MetadataAppAccessID) secretKey, xErr := xGrpcUtil.ExtractMetadata(ctx, xGrpcConst.MetadataAppSecretKey) if xErr != nil { return nil, xErr } // 验证逻辑... return handler(ctx, req) } } ``` **2. 注册到服务**: ```go title="proto/normal_upload.go" func NewNormalUploadProto(ctx context.Context, server grpc.ServiceRegistrar) *NormalUploadProto { handler := &NormalUploadProto{ log: xLog.WithName(xLog.NamedGRPC, "NormalUploadProto"), // ... } // 注册 gRPC 服务 bGrpcApi.RegisterNormalUploadServiceServer(server, handler) // [!code highlight] // 绑定服务级中间件(只对此服务生效) xGrpcMiddle.UseUnary(bGrpcApi.NormalUploadService_ServiceDesc, middleware.UnaryAppVerify(ctx)) return handler } ``` **3. 启用分发拦截器**: ```go title="main.go" grpcTask := xGrpcRunner.New( xGrpcRunner.WithRegisterService(registerGrpcService), xGrpcRunner.WithUnaryInterceptors( xGrpcIUnary.InitContext(reg.Init.Ctx), xGrpcIUnary.Recover(), // [!code highlight] xGrpcIUnary.Middleware(), // 服务级中间件分发 xGrpcIUnary.ResponseBuilder(), // 放在最后 ), ) ``` *** 推荐挂载顺序 [#推荐挂载顺序] ```go xGrpcRunner.WithUnaryInterceptors( xGrpcIUnary.InitContext(reg.Init.Ctx), // 1. 注入上下文 xGrpcIUnary.Recover(), // 2. Panic 捕获 xGrpcIUnary.Middleware(), // 3. 服务级中间件分发 xGrpcIUnary.ResponseBuilder(), // 4. 响应构建(最后) ) ``` > `Trace()` 由 Runner 默认添加,位于链首。 完整链路顺序: 1. `Trace()`(Runner 默认) 2. `InitContext(...)`(你追加) 3. `Recover()`(你追加) 4. `Middleware()`(你追加) 5. 服务级中间件链(若有) 6. `ResponseBuilder()`(你追加) 7. 业务 Handler *** 下一步 [#下一步] # 内置 Proto (/docs/bamboo-base-go/grpc/proto) 内置 Proto [#内置-proto] 脚手架内置了 `base.proto` 基础响应结构,业务端可直接复用,避免重复定义。 base.proto [#baseproto] ```proto syntax = "proto3"; package xBase; option go_package = ".;xGrpcGenerate"; // BaseResponse 表示 gRPC 统一响应的元信息部分。 message BaseResponse { // 请求追踪标识(对应 HTTP 的 X-Request-UUID)。 string context = 1; // 输出标识(如 "Success"、"PARAMETER_ERROR")。 string output = 2; // 业务状态码(200 表示成功,其余与 xError.ErrorCode.Code 对应)。 uint64 code = 3; // 人类可读的描述信息。 string message = 4; // 补充性错误详情,仅在错误场景下填充。 optional string error_message = 5; // 请求处理耗时(微秒),仅在调试模式下填充。 optional int64 overhead = 6; } ``` 字段说明 [#字段说明] | 字段 | 类型 | 说明 | | --------------- | ----------------- | -------------------------------------------- | | `context` | `string` | 请求追踪 ID,由 `Trace` 拦截器生成,`ResponseBuilder` 注入 | | `output` | `string` | 输出标识,如 `"Success"`、`"PARAMETER_ERROR"` | | `code` | `uint64` | 业务状态码,`200` 表示成功 | | `message` | `string` | 人类可读的描述信息 | | `error_message` | `optional string` | 补充性错误详情,仅错误场景填充 | | `overhead` | `optional int64` | 请求处理耗时(微秒),仅调试模式填充 | > `BaseResponse` 是纯元信息结构,**不包含 `data` 字段**。业务数据通过自定义响应 proto 嵌入 `BaseResponse`,由 [`SuccessWith[T]`](/docs/bamboo-base-go/grpc/result) 泛型注入。 Buf 管理 [#buf-管理] Proto 生成基于 [Buf](https://buf.build/) 管理,仓库层通常通过 `make proto` 统一执行。 buf.yaml [#bufyaml] ```yaml title="buf.yaml" version: v2 modules: - path: proto deps: - buf.build/protocolbuffers/wellknowntypes ``` buf.gen.yaml [#bufgenyaml] ```yaml title="buf.gen.yaml" version: v2 plugins: - protoc_builtin: go out: grpc/generate opt: - paths=source_relative ``` 生成命令 [#生成命令] ```bash # 在 bamboo-base 仓库根目录执行(推荐) make proto # 或在 grpc 模块目录直接执行 Buf cd plugins/grpc buf generate ``` 生成结果输出到 `plugins/grpc/generate/` 目录。 业务端引用方式 [#业务端引用方式] 业务端**不需要自己创建 proto 文件来引用 `BaseResponse`**,直接在 Go 代码中 import 即可: ```go import xGrpcGenerate "github.com/bamboo-services/bamboo-base-go/plugins/grpc/generate" ``` 如需携带业务数据,在业务 proto 中嵌入 `BaseResponse` 字段: ```proto title="proto/your_service.proto" syntax = "proto3"; package yourService; import "proto/base.proto"; option go_package = "./generate;pb"; message YourResponse { xBase.BaseResponse base_response = 1; // 你的业务字段... string name = 2; } ``` 下一步 [#下一步] # gRPC 结果处理 (/docs/bamboo-base-go/grpc/result) xGrpcResult 包 [#xgrpcresult-包] `xGrpcResult` 包提供统一的 gRPC 响应构建函数,基于内置 `BaseResponse` proto 消息封装。 ```go import xGrpcResult "github.com/bamboo-services/bamboo-base-go/plugins/grpc/result" ``` Success [#success] 返回无数据的成功响应,填充 `Output`、`Code`、`Message` 三个业务语义字段。 ```go func Success(ctx context.Context, message string) *xGrpcGenerate.BaseResponse ``` > 请求追踪 ID(`context`)和耗时(`overhead`)由 `ResponseBuilder` 拦截器统一注入,无需手动填写。 **示例:** ```go func (s *UserServer) DeleteUser(ctx context.Context, req *pb.DeleteUserRequest) (*xGrpcGenerate.BaseResponse, error) { xErr := s.service.Delete(ctx, req.Id) if xErr != nil { return nil, xErr } // [!code highlight] return xGrpcResult.Success(ctx, "删除成功"), nil } ``` SuccessWith[T] [#successwitht] 泛型函数,创建包含业务数据的成功响应,通过反射自动注入 `BaseResponse` 字段。 ```go func SuccessWith[T any](ctx context.Context, message string) T ``` 类型约束 [#类型约束] * `T` 必须是**指针类型**(如 `*pb.YourResponse`)。 * `T` 指向的结构体中必须包含名为 `BaseResponse`、类型为 `*xGrpcGenerate.BaseResponse` 的字段。 * 若不满足条件,函数会 **panic** 以在开发阶段尽早暴露错误。 工作原理 [#工作原理] ``` SuccessWith[*pb.YourResponse](ctx, "操作成功") ↓ reflect.New(YourResponse{}) ↓ 设置 BaseResponse 字段 = &BaseResponse{Output: "Success", Code: 200, Message: "操作成功"} ↓ 返回 *pb.YourResponse(调用者继续填充业务字段) ``` Proto 定义示例 [#proto-定义示例] ```proto title="proto/upload.proto" syntax = "proto3"; package yourService; import "link/base.proto"; option go_package = "./internal/proto/api;bGrpcApi"; message UploadResponse { // [!code highlight] xBase.BaseResponse base_response = 1; string file_id = 2; string file_url = 3; int64 file_size = 4; } ``` Handler 使用示例 [#handler-使用示例] ```go func (s *FileServer) Upload(ctx context.Context, req *pb.UploadRequest) (*pb.UploadResponse, error) { result, xErr := s.service.Upload(ctx, req.FileName, req.Content) if xErr != nil { return nil, xErr } // [!code highlight:5] resp := xGrpcResult.SuccessWith[*pb.UploadResponse](ctx, "上传成功") resp.FileId = result.ID resp.FileUrl = result.URL resp.FileSize = result.Size return resp, nil } ``` 完整 Handler 示例 [#完整-handler-示例] 展示新版错误处理模式:业务 handler 直接返回 `*xError.Error`,由 `ResponseBuilder` 拦截器统一转换。 ```go func (s *OrderServer) CreateOrder(ctx context.Context, req *pb.CreateOrderRequest) (*xGrpcGenerate.BaseResponse, error) { // 参数校验 if req.UserId == 0 { // [!code highlight] return nil, xError.NewError(ctx, xError.BadRequest, "用户 ID 不能为空", false) } // 调用 service(错误直接返回,ResponseBuilder 负责转换) xErr := s.service.CreateOrder(ctx, req.UserId, req.Amount) if xErr != nil { // [!code highlight] return nil, xErr } // 成功响应 // [!code highlight] return xGrpcResult.Success(ctx, "订单创建成功"), nil } ``` > **重要**:错误处理由 [`ResponseBuilder`](/docs/bamboo-base-go/grpc/interceptors) 拦截器统一负责。handler 中只需将 `*xError.Error` 作为 `error` 返回,拦截器会自动完成错误码映射和 gRPC status 构建。 下一步 [#下一步] # gRPC 运行时 (/docs/bamboo-base-go/grpc/runner) import { TypeTable } from '@/components/type-table'; gRPC 运行时 [#grpc-运行时] `xGrpcRunner` 提供 gRPC 服务的启动与优雅退出能力,可直接作为 `xMain.Runner` 的附加协程使用。 ```go import xGrpcRunner "github.com/bamboo-services/bamboo-base-go/plugins/grpc/runner" ``` 核心入口 [#核心入口] ```go title="grpc/runner/runner.go" func New(options ...Option) func(ctx context.Context, option ...any) ``` 返回值可直接传给: ```go xMain.Runner(reg, log, registerRoute, grpcTask) ``` 常用 Option [#常用-option] 集成示例 [#集成示例] ```go title="main.go" package main import ( "context" "time" xLog "github.com/bamboo-services/bamboo-base-go/common/log" xMain "github.com/bamboo-services/bamboo-base-go/major/main" xReg "github.com/bamboo-services/bamboo-base-go/major/register" xGrpcIUnary "github.com/bamboo-services/bamboo-base-go/plugins/grpc/interceptor/unary" xGrpcRunner "github.com/bamboo-services/bamboo-base-go/plugins/grpc/runner" "google.golang.org/grpc" ) func main() { reg := xReg.Register(context.Background(), nil) log := xLog.WithName(xLog.NamedMAIN) grpcTask := xGrpcRunner.New( xGrpcRunner.WithLogger(xLog.WithName(xLog.NamedGRPC)), xGrpcRunner.WithGracefulStopTimeout(30*time.Second), xGrpcRunner.WithRegisterService(func(ctx context.Context, server grpc.ServiceRegistrar) { // 在这里注册你的服务: // pb.RegisterYourServiceServer(server, yourHandler) }), // [!code highlight:4] xGrpcRunner.WithUnaryInterceptors( xGrpcIUnary.InitContext(reg.Init.Ctx), xGrpcIUnary.ResponseBuilder(), ), ) xMain.Runner(reg, log, registerRoute, grpcTask) } ``` 内置行为 [#内置行为] * 默认使用 `GRPC_PORT`(默认 `1119`)作为监听端口。 * 默认启用追踪拦截器 `Trace`(复用或生成 `x_request_uuid`)。 * 由 `xMain.Runner` 统一管理退出信号:`SIGINT`/`SIGTERM`。 * `GRPC_REFLECTION=true` 时启用反射。 拦截器链路 [#拦截器链路] 默认链路中已包含 `Trace()`。你可以通过 `WithUnaryInterceptors(...)` 追加内置拦截器: ```go xGrpcRunner.WithUnaryInterceptors( xGrpcIUnary.InitContext(reg.Init.Ctx), xGrpcIUnary.Recover(), // [!code highlight] xGrpcIUnary.Middleware(), // 服务级中间件分发 xGrpcIUnary.ResponseBuilder(), // 放在最后 ) ``` 完整链路顺序: 1. `Trace()`(Runner 默认) 2. `InitContext(...)`(你追加) 3. `Recover()`(你追加) 4. `Middleware()`(你追加) 5. 服务级中间件链(若有) 6. `ResponseBuilder()`(你追加) 7. 业务 Handler 流式拦截器 [#流式拦截器] 如需使用流式 RPC,可通过 `WithStreamInterceptors` 追加流式拦截器: ```go import xGrpcIStream "github.com/bamboo-services/bamboo-base-go/plugins/grpc/interceptor/stream" xGrpcRunner.WithStreamInterceptors( xGrpcIStream.InitContext(reg.Init.Ctx), xGrpcIStream.Recover(), xGrpcIStream.Middleware(), ) ``` > 流式拦截器**没有** `ResponseBuilder`,因为流式响应由业务代码直接控制。 错误处理 [#错误处理] `xGrpcRunner` 配合 `ResponseBuilder` 拦截器与 `xError` 使用。业务 handler 直接返回 `*xError.Error`,`ResponseBuilder` 自动将其映射为标准 gRPC status 返回给客户端。 详见 [内置拦截器](/docs/bamboo-base-go/grpc/interceptors) 中的 ResponseBuilder 章节。 下一步 [#下一步] # Buf 工具配置 (/docs/bamboo-base-go/grpc-best-practices/buf-config) import { TypeTable } from '@/components/type-table'; Buf 工具配置 [#buf-工具配置] [Buf](https://buf.build/) 是现代化的 Proto 管理工具,提供 lint、breaking change 检测和代码生成能力。本章介绍如何配置 Buf 与 `bamboo-base-go` 集成。 安装 Buf [#安装-buf] ```bash # macOS brew install bufbuild/buf/buf # Linux/WSL curl -sSL "https://github.com/bufbuild/buf/releases/latest/download/buf-$(uname)-$(uname -m)" \ -o /usr/local/bin/buf && chmod +x /usr/local/bin/buf # Go install go install github.com/bufbuild/buf/cmd/buf@latest ``` buf.yaml 配置 [#bufyaml-配置] 在 `proto/` 目录下创建 `buf.yaml`: ```yaml title="proto/buf.yaml" version: v2 modules: - path: . # 当前目录作为模块根目录 lint: use: - MINIMAL # 使用最小规则集,减少约束 except: # 以下规则与 link/base.proto 符号链接冲突,需要忽略 - PACKAGE_DIRECTORY_MATCH - PACKAGE_SAME_DIRECTORY - DIRECTORY_SAME_PACKAGE - PACKAGE_LOWER_SNAKE_CASE - PACKAGE_VERSION_SUFFIX - PACKAGE_DEFINED - SYNTAX_SPECIFIED ignore: - link/base.proto # 忽略符号链接的 lint 检查 breaking: use: - FILE # 文件级别的破坏性变更检测 ``` Lint 规则说明 [#lint-规则说明] | 规则 | 说明 | | --------- | -------------- | | `MINIMAL` | 最小规则集,包含最基本的检查 | | `BASIC` | 基础规则集,增加命名规范检查 | | `DEFAULT` | 默认规则集,最严格 | 为什么需要 except [#为什么需要-except] 符号链接模式会导致以下问题: 1. `link/base.proto` 的 package 是 `xBase`,与目录结构不匹配 2. 符号链接文件不在 `proto/` 的目录层级下 因此需要排除相关 lint 规则。 buf.gen.yaml 配置 [#bufgenyaml-配置] 代码生成配置文件通常放在 `proto/` 目录下: ```yaml title="proto/buf.gen.yaml" version: v2 plugins: # Go 消息生成器 - local: protoc-gen-go out: internal/grpc/gen # 输出目录 opt: - paths=source_relative # 使用相对路径生成文件 # M 参数:将 link/base.proto 映射到 bamboo-base-go 的包路径 - Mlink/base.proto=github.com/bamboo-services/bamboo-base-go/plugins/grpc/generate # Go gRPC 服务生成器 - local: protoc-gen-go-grpc out: internal/grpc/gen opt: - paths=source_relative - Mlink/base.proto=github.com/bamboo-services/bamboo-base-go/plugins/grpc/generate ``` M 参数映射 [#m-参数映射] `M` 参数告诉 `protoc-gen-go`,当遇到 `import "link/base.proto"` 时,生成的 Go 代码应该导入哪个包: ```proto import "link/base.proto"; message MyResponse { xBase.BaseResponse base_response = 1; // 类型引用 } ``` 生成的 Go 代码: ```go import xGrpcGenerate "github.com/bamboo-services/bamboo-base-go/plugins/grpc/generate" type MyResponse struct { BaseResponse *xGrpcGenerate.BaseResponse `protobuf:"bytes,1,opt,name=base_response,json=baseResponse,embedded=base_response,req=is=BaseResponse"` } ``` 输出目录结构 [#输出目录结构] 执行 `buf generate` 后的目录结构: ``` internal/grpc/gen/ ├── beacon/ │ └── sso/ │ └── v1/ │ ├── auth.pb.go # 消息定义 │ ├── auth_grpc.pb.go # 服务定义 │ ├── public.pb.go │ └── public_grpc.pb.go └── link/ └── base.pb.go # 实际不会生成(映射到外部包) ``` > 由于 `M` 参数映射,`link/base.proto` 不会在本地生成代码,而是直接引用 `bamboo-base-go` 的包。 Makefile 集成 [#makefile-集成] 将 Buf 命令集成到 Makefile 中,提供统一的开发体验: ```makefile title="Makefile" # 变量定义 MAIN_FILE = main.go PROTO_FILE ?= beacon/sso/v1/auth.proto # 默认生成的文件 BASE_GO_MODULE_DIR := $(shell go list -m -f '{{.Dir}}' github.com/bamboo-services/bamboo-base-go/plugins/grpc) XBASE_LINK := proto/link/base.proto .PHONY: proto proto-init # 初始化 proto 符号链接 proto-init: @mkdir -p $(dir $(XBASE_LINK)) @if [ ! -d "$(BASE_GO_MODULE_DIR)" ]; then \ echo "错误: 找不到 bamboo-base-go 模块,请先运行 go mod download"; \ exit 1; \ fi @ln -sf $(BASE_GO_MODULE_DIR)/proto/base.proto $(XBASE_LINK) @echo "符号链接已创建: $(XBASE_LINK)" # 生成 proto(自动初始化符号链接) proto: proto-init cd proto && buf generate --path $(PROTO_FILE) ``` 使用方式 [#使用方式] ```bash # 初始化符号链接 make proto-init # 生成指定 proto 文件 make proto PROTO_FILE=beacon/sso/v1/auth.proto # 生成所有 proto 文件(不指定 --path) cd proto && buf generate ``` 按需生成 vs 全量生成 [#按需生成-vs-全量生成] | 模式 | 命令 | 适用场景 | | ---- | ------------------------------- | --------------- | | 按需生成 | `buf generate --path xxx.proto` | 开发阶段,只修改了单个文件 | | 全量生成 | `buf generate` | CI/CD、首次克隆、多人协作 | Lint 检查 [#lint-检查] 在 CI 中添加 lint 检查: ```bash # 检查 lint 问题 buf lint # 检查破坏性变更(对比远程版本) buf breaking proto --against '.git#branch=main' ``` GitHub Actions 集成 [#github-actions-集成] ```yaml title=".github/workflows/proto-lint.yml" name: Proto Lint on: push: paths: - 'proto/**' jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: bufbuild/buf-setup-action@v1 - uses: bufbuild/buf-lint-action@v1 with: input: proto ``` 常见问题 [#常见问题] 1. 符号链接指向错误的版本 [#1-符号链接指向错误的版本] **问题**:升级 `bamboo-base-go` 后,符号链接仍指向旧版本。 **解决**:重新执行 `make proto-init` 或手动删除符号链接后重建。 ```bash rm proto/link/base.proto make proto-init ``` 2. 找不到 protoc-gen-go [#2-找不到-protoc-gen-go] **问题**:执行 `buf generate` 报错找不到插件。 **解决**:确保 protoc 插件已安装并在 PATH 中。 ```bash go install google.golang.org/protobuf/cmd/protoc-gen-go@latest go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest ``` 3. import cycle detected [#3-import-cycle-detected] **问题**:Proto 文件之间存在循环引用。 **解决**:重构 Proto 文件,将共享定义提取到独立的 `common.proto` 中。 下一步 [#下一步] # Handler 实现模式 (/docs/bamboo-base-go/grpc-best-practices/handler-pattern) Handler 实现模式 [#handler-实现模式] 本章介绍如何编写符合 `bamboo-base-go` 规范的 gRPC Handler,包括基础结构、响应构建和依赖注入。 基础结构 [#基础结构] 每个 gRPC 服务对应一个 Handler 结构体,实现生成的 Server 接口: ```go title="internal/grpc/handler/auth.go" package handler import ( "context" xError "github.com/bamboo-services/bamboo-base-go/common/error" xLog "github.com/bamboo-services/bamboo-base-go/common/log" xGrpcMiddle "github.com/bamboo-services/bamboo-base-go/plugins/grpc/middleware" xGrpcResult "github.com/bamboo-services/bamboo-base-go/plugins/grpc/result" "github.com/your-org/beacon-sso/internal/grpc/gen" "github.com/your-org/beacon-sso/internal/logic" "google.golang.org/grpc" ) // AuthHandler 认证服务 Handler type AuthHandler struct { log *xLog.LogNamedLogger userLogic *logic.UserLogic tokenLogic *logic.TokenLogic // 必须嵌入 Unimplemented 服务 gen.UnimplementedAuthServiceServer } // NewAuthHandler 创建认证服务 Handler // // 构造函数负责三件事: // 1. 初始化依赖 // 2. 注册 gRPC 服务 // 3. 绑定服务级中间件 func NewAuthHandler(ctx context.Context, server grpc.ServiceRegistrar) *AuthHandler { log := xLog.WithName(xLog.NamedGRPC, "AuthHandler") log.Info(ctx, "初始化认证服务 gRPC Handler") handler := &AuthHandler{ log: log, userLogic: logic.NewUserLogic(ctx), tokenLogic: logic.NewTokenLogic(ctx), } // 1. 注册 gRPC 服务到 Server gen.RegisterAuthServiceServer(server, handler) // 2. 绑定服务级中间件(只对此服务生效) // xGrpcMiddle.UseUnary(gen.AuthService_ServiceDesc, middleware.UnaryAppVerify(ctx)) return handler } ``` 结构体设计要点 [#结构体设计要点] 1. **命名日志器**:使用 `xLog.WithName` 创建带有服务标识的日志器 2. **Logic 依赖**:通过组合 Logic 层实现业务逻辑 3. **Unimplemented 嵌入**:确保向前兼容,新增 RPC 方法时不会编译报错 xGrpcResult 使用 [#xgrpcresult-使用] `xGrpcResult` 提供统一的响应构建函数,自动填充 `BaseResponse` 字段。 Success - 无数据响应 [#success---无数据响应] 用于不需要返回业务数据的操作: ```go title="internal/grpc/handler/auth.go" func (h *AuthHandler) Logout(ctx context.Context, req *gen.LogoutRequest) (*xGrpcGenerate.BaseResponse, error) { xErr := h.tokenLogic.Invalidate(ctx, req.Token) if xErr != nil { return nil, xErr } // 返回无数据的成功响应 return xGrpcResult.Success(ctx, "退出登录成功"), nil } ``` SuccessWith[T] - 有数据响应 [#successwitht---有数据响应] 泛型函数,创建包含业务数据的成功响应: ```go title="internal/grpc/handler/auth.go" func (h *AuthHandler) RegisterByEmail(ctx context.Context, req *gen.RegisterByEmailRequest) (*gen.RegisterByEmailResponse, error) { // 调用 Logic 层执行业务逻辑 result, xErr := h.userLogic.RegisterByEmail(ctx, req.Email, req.Code, req.Password, req.Username, req.Nickname) if xErr != nil { return nil, xErr } // 构建响应(自动填充 BaseResponse) resp := xGrpcResult.SuccessWith[*gen.RegisterByEmailResponse](ctx, "注册成功") resp.UserId = result.UserID.String() resp.Token = result.Token return resp, nil } ``` 类型约束 [#类型约束] `SuccessWith[T]` 要求: 1. `T` 必须是指针类型(如 `*pb.YourResponse`) 2. `T` 指向的结构体必须包含 `BaseResponse` 字段(类型为 `*xGrpcGenerate.BaseResponse`) 如果类型不满足条件,函数会 **panic** 以在开发阶段暴露错误。 错误处理 [#错误处理] 业务 Handler 直接返回 `*xError.Error`,由 `ResponseBuilder` 拦截器统一转换为 gRPC status。 标准错误返回 [#标准错误返回] ```go func (h *AuthHandler) Login(ctx context.Context, req *gen.LoginRequest) (*gen.LoginResponse, error) { // 参数校验 if req.Email == "" { return nil, xError.NewError(ctx, xError.BadRequest, "邮箱不能为空", false) } // 业务逻辑 result, xErr := h.userLogic.LoginByEmail(ctx, req.Email, req.Password) if xErr != nil { return nil, xErr // 直接返回,拦截器处理转换 } resp := xGrpcResult.SuccessWith[*gen.LoginResponse](ctx, "登录成功") resp.UserId = result.UserID.String() resp.Token = result.Token return resp, nil } ``` 错误码到 gRPC Status 映射 [#错误码到-grpc-status-映射] `ResponseBuilder` 拦截器自动将业务错误码映射为 gRPC 状态码: | HTTP 状态码 | gRPC Status Code | | -------- | ------------------ | | 400 | `InvalidArgument` | | 401 | `Unauthenticated` | | 403 | `PermissionDenied` | | 404 | `NotFound` | | 500 | `Internal` | 依赖注入 [#依赖注入] Handler 通过构造函数注入 Logic 层依赖,Logic 层再从 context 获取基础设施。 Handler 层 [#handler-层] ```go func NewAuthHandler(ctx context.Context, server grpc.ServiceRegistrar) *AuthHandler { return &AuthHandler{ log: xLog.WithName(xLog.NamedGRPC, "AuthHandler"), userLogic: logic.NewUserLogic(ctx), tokenLogic: logic.NewTokenLogic(ctx), } } ``` Logic 层 [#logic-层] Logic 层从 context 获取数据库、Redis 等基础设施: ```go title="internal/logic/user.go" type UserLogic struct { log *xLog.LogNamedLogger } func NewUserLogic(ctx context.Context) *UserLogic { return &UserLogic{ log: xLog.WithName(xLog.NamedLOGC, "UserLogic"), } } func (l *UserLogic) GetByID(ctx context.Context, userID int64) (*entity.User, *xError.Error) { // 从 context 获取数据库连接 db := xCtxUtil.MustGetDB(ctx) var user entity.User if err := db.First(&user, userID).Error; err != nil { if err == gorm.ErrRecordNotFound { return nil, xError.NewError(ctx, xError.NotExist, "用户不存在", false) } return nil, xError.NewInternalServerError(ctx, "查询用户失败", err) } return &user, nil } ``` 完整示例 [#完整示例] 以下是一个完整的 Handler 实现示例: ```go title="internal/grpc/handler/upload.go" package handler import ( "context" xError "github.com/bamboo-services/bamboo-base-go/common/error" xLog "github.com/bamboo-services/bamboo-base-go/common/log" xSnowflake "github.com/bamboo-services/bamboo-base-go/common/snowflake" xGrpcMiddle "github.com/bamboo-services/bamboo-base-go/plugins/grpc/middleware" xGrpcResult "github.com/bamboo-services/bamboo-base-go/plugins/grpc/result" "github.com/your-org/beacon-bucket/internal/grpc/gen" "github.com/your-org/beacon-bucket/internal/logic" "github.com/your-org/beacon-bucket/internal/proto/middleware" "google.golang.org/grpc" "google.golang.org/protobuf/types/known/timestamppb" ) type UploadHandler struct { log *xLog.LogNamedLogger bucketLogic *logic.BucketLogic bucketFileLogic *logic.BucketFileLogic fileLogic *logic.FileLogic gen.UnimplementedNormalUploadServiceServer } func NewUploadHandler(ctx context.Context, server grpc.ServiceRegistrar) *UploadHandler { log := xLog.WithName(xLog.NamedGRPC, "UploadHandler") log.Info(ctx, "初始化上传服务 gRPC Handler") handler := &UploadHandler{ log: log, bucketLogic: logic.NewBucketLogic(ctx), bucketFileLogic: logic.NewBucketFileLogic(ctx), fileLogic: logic.NewFileLogic(ctx), } // 注册 gRPC 服务 gen.RegisterNormalUploadServiceServer(server, handler) // 绑定服务级中间件(App 认证) xGrpcMiddle.UseUnary(gen.NormalUploadService_ServiceDesc, middleware.UnaryAppVerify(ctx)) return handler } // Upload 上传文件 func (h *UploadHandler) Upload(ctx context.Context, req *gen.UploadRequest) (*gen.UploadResponse, error) { h.log.Info(ctx, "处理普通上传请求") // 流程一:解析存储桶 ID bucketID, err := xSnowflake.ParseSnowflakeID(req.GetBucketId()) if err != nil { return nil, xError.NewError(ctx, xError.BadRequest, "存储桶 ID 格式无效", false, err) } // 流程二:获取存储桶 bucket, xErr := h.bucketLogic.Get(ctx, bucketID) if xErr != nil { return nil, xErr } // 流程三:解析文件内容 decodedFile, contentType, xErr := h.fileLogic.ParseMIMEBase64File(ctx, req.GetContentBase64()) if xErr != nil { return nil, xErr } // 流程四:上传到存储 xErr = h.fileLogic.UploadObject(ctx, bucket, "path/file", decodedFile, contentType) if xErr != nil { return nil, xErr } // 流程五:创建数据库记录 bucketFile, xErr := h.bucketFileLogic.Create(ctx, bucketID, "file", len(decodedFile)) if xErr != nil { return nil, xErr } // 构建响应 resp := xGrpcResult.SuccessWith[*gen.UploadResponse](ctx, "文件上传成功") resp.FileId = bucketFile.ID.String() resp.Size = bucketFile.Size resp.UploadedAt = timestamppb.New(bucketFile.CreatedAt) return resp, nil } // Delete 删除文件 func (h *UploadHandler) Delete(ctx context.Context, req *gen.DeleteRequest) (*gen.DeleteResponse, error) { h.log.Info(ctx, "处理文件删除请求") fileID, err := xSnowflake.ParseSnowflakeID(req.GetFileId()) if err != nil { return nil, xError.NewError(ctx, xError.BadRequest, "文件 ID 格式无效", false, err) } xErr := h.bucketFileLogic.Delete(ctx, fileID) if xErr != nil { return nil, xErr } return xGrpcResult.SuccessWith[*gen.DeleteResponse](ctx, "文件删除成功"), nil } ``` 最佳实践总结 [#最佳实践总结] | 要点 | 说明 | | ----- | ------------------------------------------- | | 日志命名 | 使用 `xLog.NamedGRPC` + 服务名创建日志器 | | 错误返回 | 直接返回 `*xError.Error`,不手动转换 | | 响应构建 | 使用 `xGrpcResult.Success` 或 `SuccessWith[T]` | | 依赖注入 | 在构造函数中创建 Logic 实例 | | 中间件绑定 | 在构造函数中调用 `xGrpcMiddle.UseUnary` | 下一步 [#下一步] # gRPC 最佳实践 (/docs/bamboo-base-go/grpc-best-practices) gRPC 最佳实践 [#grpc-最佳实践] 本指南总结了基于 `bamboo-base-go` 框架进行 gRPC 开发的最佳实践,涵盖 Proto 文件组织、代码生成、Handler 实现模式、中间件设计等核心主题。 适用场景 [#适用场景] * 需要同时暴露 HTTP 与 gRPC 接口的服务 * 微服务间高性能通信 * 需要强类型接口定义的内部 API 快速入口 [#快速入口] 核心原则 [#核心原则] 1. 统一响应结构 [#1-统一响应结构] 所有 gRPC 响应都应包含 `BaseResponse` 元信息,由 `ResponseBuilder` 拦截器自动注入追踪 ID 和耗时: ```proto message YourResponse { xBase.BaseResponse base_response = 1; // 必须放在第一位 // 业务字段... } ``` 2. 错误处理模式 [#2-错误处理模式] 业务 Handler 直接返回 `*xError.Error`,由 `ResponseBuilder` 统一转换为 gRPC status: ```go if xErr != nil { return nil, xErr // 直接返回,拦截器处理转换 } ``` 3. 拦截器链顺序 [#3-拦截器链顺序] 遵循从外到内的洋葱模型: ``` Trace → InitContext → Recover → Middleware → ResponseBuilder → Handler ``` 架构概览 [#架构概览] ``` ┌─────────────────────────────────────────────────────────────┐ │ gRPC 请求 │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Trace 拦截器(生成/传递 x_request_uuid) │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ InitContext 拦截器(注入数据库、Redis 等依赖) │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Recover 拦截器(捕获 panic) │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ 服务级中间件(如 App 认证) │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ ResponseBuilder 拦截器(错误转换 & 响应注入) │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ 业务 Handler │ └─────────────────────────────────────────────────────────────┘ ``` # 中间件模式 (/docs/bamboo-base-go/grpc-best-practices/middleware-pattern) 中间件模式 [#中间件模式] `bamboo-base-go` 提供了灵活的服务级中间件机制,允许为不同的 gRPC 服务绑定不同的中间件链。 服务级中间件分发 [#服务级中间件分发] 工作原理 [#工作原理] 全局拦截器 `xGrpcIUnary.Middleware()` 作为分发器,根据请求的 `FullMethod` 解析服务名,查找并执行对应的中间件链。 ``` 请求进入 → Middleware 拦截器 → 解析服务名 → 查找中间件链 → 执行中间件 → Handler ``` 注册 API [#注册-api] ```go import xGrpcMiddle "github.com/bamboo-services/bamboo-base-go/plugins/grpc/middleware" // 注册一元中间件 func UseUnary(desc grpc.ServiceDesc, middlewares ...UnaryMiddlewareFunc) // 注册流式中间件 func UseStream(desc grpc.ServiceDesc, middlewares ...StreamMiddlewareFunc) ``` 洋葱模型 [#洋葱模型] 中间件链遵循**洋葱模型**执行顺序: ``` 注册顺序 [A, B, C] 执行顺序: A-enter → B-enter → C-enter → handler → C-exit → B-exit → A-exit ``` App 认证中间件示例 [#app-认证中间件示例] 以下是一个完整的 App 认证中间件实现,验证调用方的应用身份: ```go title="internal/grpc/middleware/app_verify.go" package middleware import ( "context" xError "github.com/bamboo-services/bamboo-base-go/common/error" xLog "github.com/bamboo-services/bamboo-base-go/common/log" xSnowflake "github.com/bamboo-services/bamboo-base-go/common/snowflake" xGrpcConst "github.com/bamboo-services/bamboo-base-go/plugins/grpc/constant" xGrpcUtil "github.com/bamboo-services/bamboo-base-go/plugins/grpc/utility" "github.com/your-org/beacon-bucket/internal/logic" "google.golang.org/grpc" ) // UnaryAppVerify 创建用于验证应用身份的 gRPC 一元服务端拦截器 // // 它从传入请求的 gRPC 元数据中提取应用 ID 和密钥, // 并调用 Logic 层进行身份认证。认证失败将直接返回错误响应。 // // 参数: // - mainCtx: 用于初始化内部 AppLogic 和 Logger 的上下文 // // 返回值: // - 返回一个 grpc.UnaryServerInterceptor,用于拦截请求执行认证逻辑 func UnaryAppVerify(mainCtx context.Context) grpc.UnaryServerInterceptor { log := xLog.WithName(xLog.NamedMIDE, "UnaryAppVerify") appLogic := logic.NewAppLogic(mainCtx) return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { log.Info(ctx, "验证应用身份") // 从 metadata 提取认证信息 appIDStr, xErr := xGrpcUtil.ExtractMetadata(ctx, xGrpcConst.MetadataAppAccessID) if xErr != nil { return nil, xErr } secretKey, xErr := xGrpcUtil.ExtractMetadata(ctx, xGrpcConst.MetadataAppSecretKey) if xErr != nil { return nil, xErr } // 校验必填性 if appIDStr == "" || secretKey == "" { log.Warn(ctx, "缺少应用认证信息") return nil, xError.NewError(ctx, xError.Unauthorized, "缺少应用认证信息", false) } // 解析雪花 ID appID, err := xSnowflake.ParseSnowflakeID(appIDStr) if err != nil { log.Warn(ctx, "应用 ID 格式无效") return nil, xError.NewError(ctx, xError.ValidationError, "应用 ID 格式无效", false, err) } // 调用 Logic 层认证 _, xErr = appLogic.Authenticate(ctx, appID, secretKey) if xErr != nil { log.Warn(ctx, "应用认证失败") return nil, xErr } log.Debug(ctx, "应用认证成功") // 将 AppID 存入 context,供后续逻辑使用 ctx = context.WithValue(ctx, "app_id", appID) return handler(ctx, req) } } ``` 流式中间件版本 [#流式中间件版本] 对于流式 RPC,需要实现流式拦截器: ```go title="internal/grpc/middleware/app_verify.go" // StreamAppVerify 用于 gRPC 流式请求的应用认证拦截器 func StreamAppVerify(mainCtx context.Context) grpc.StreamServerInterceptor { log := xLog.WithName(xLog.NamedMIDE, "StreamAppVerify") appLogic := logic.NewAppLogic(mainCtx) return func(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { ctx := ss.Context() // 从 metadata 提取认证信息 appIDStr, xErr := xGrpcUtil.ExtractMetadata(ctx, xGrpcConst.MetadataAppAccessID) if xErr != nil { return xErr } secretKey, xErr := xGrpcUtil.ExtractMetadata(ctx, xGrpcConst.MetadataAppSecretKey) if xErr != nil { return xErr } if appIDStr == "" || secretKey == "" { log.Warn(ctx, "缺少应用认证信息") return xError.NewError(ctx, xError.Unauthorized, "缺少应用认证信息", false) } appID, err := xSnowflake.ParseSnowflakeID(appIDStr) if err != nil { return xError.NewError(ctx, xError.ValidationError, "应用 ID 格式无效", false, err) } _, xErr = appLogic.Authenticate(ctx, appID, secretKey) if xErr != nil { return xErr } return handler(srv, ss) } } ``` 绑定中间件到服务 [#绑定中间件到服务] 中间件在 Handler 构造函数中绑定: ```go title="internal/grpc/handler/upload.go" func NewUploadHandler(ctx context.Context, server grpc.ServiceRegistrar) *UploadHandler { handler := &UploadHandler{ // ... } // 1. 注册 gRPC 服务 gen.RegisterNormalUploadServiceServer(server, handler) // 2. 绑定服务级中间件(只对此服务生效) xGrpcMiddle.UseUnary( gen.NormalUploadService_ServiceDesc, middleware.UnaryAppVerify(ctx), ) return handler } ``` 多中间件链 [#多中间件链] 可以绑定多个中间件,按注册顺序执行: ```go xGrpcMiddle.UseUnary( gen.YourService_ServiceDesc, middleware.UnaryRateLimit(ctx), // 1. 限流 middleware.UnaryAppVerify(ctx), // 2. App 认证 middleware.UnaryAccessLog(ctx), // 3. 访问日志 ) ``` 自定义中间件模板 [#自定义中间件模板] 基础模板 [#基础模板] ```go title="internal/grpc/middleware/custom.go" package middleware import ( "context" xLog "github.com/bamboo-services/bamboo-base-go/common/log" "google.golang.org/grpc" ) // UnaryCustom 自定义一元中间件 func UnaryCustom(mainCtx context.Context) grpc.UnaryServerInterceptor { log := xLog.WithName(xLog.NamedMIDE, "UnaryCustom") return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { // === 前置处理 === log.Debug(ctx, "中间件前置处理", "method", info.FullMethod) // === 调用下一个处理器 === resp, err := handler(ctx, req) // === 后置处理 === if err != nil { log.Warn(ctx, "请求处理失败", "error", err) } return resp, err } } ``` 请求/响应拦截 [#请求响应拦截] ```go title="internal/grpc/middleware/access_log.go" func UnaryAccessLog(mainCtx context.Context) grpc.UnaryServerInterceptor { log := xLog.WithName(xLog.NamedMIDE, "AccessLog") return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { start := time.Now() // 记录请求 log.Info(ctx, "gRPC 请求开始", "method", info.FullMethod, "request_type", reflect.TypeOf(req).String(), ) // 调用处理器 resp, err := handler(ctx, req) // 记录响应 duration := time.Since(start) if err != nil { log.Warn(ctx, "gRPC 请求失败", "method", info.FullMethod, "duration", duration, "error", err, ) } else { log.Info(ctx, "gRPC 请求成功", "method", info.FullMethod, "duration", duration, ) } return resp, err } } ``` 限流中间件 [#限流中间件] ```go title="internal/grpc/middleware/rate_limit.go" func UnaryRateLimit(mainCtx context.Context, limit int) grpc.UnaryServerInterceptor { limiter := rate.NewLimiter(rate.Limit(limit), limit*2) log := xLog.WithName(xLog.NamedMIDE, "RateLimit") return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { if !limiter.Allow() { log.Warn(ctx, "请求被限流", "method", info.FullMethod) return nil, xError.NewError(ctx, xError.TooManyRequests, "请求过于频繁,请稍后重试", false) } return handler(ctx, req) } } ``` 全局拦截器配置 [#全局拦截器配置] 在 `main.go` 中配置全局拦截器链: ```go title="main.go" grpcTask := xGrpcRunner.New( xGrpcRunner.WithLogger(xLog.WithName(xLog.NamedGRPC)), xGrpcRunner.WithRegisterService(registerGrpcService), xGrpcRunner.WithUnaryInterceptors( xGrpcIUnary.InitContext(reg.Init.Ctx), // 1. 注入上下文 xGrpcIUnary.Recover(), // 2. Panic 捕获 xGrpcIUnary.Middleware(), // 3. 服务级中间件分发 xGrpcIUnary.ResponseBuilder(), // 4. 响应构建(最后) ), ) ``` 拦截器链顺序 [#拦截器链顺序] | 顺序 | 拦截器 | 作用 | | -- | --------------- | ------------------ | | 1 | Trace | 请求追踪 ID(Runner 默认) | | 2 | InitContext | 注入主上下文到请求上下文 | | 3 | Recover | Panic 恢复,防止服务崩溃 | | 4 | Middleware | 服务级中间件分发 | | 5 | 服务中间件 | App 认证、限流等 | | 6 | ResponseBuilder | 错误转换、响应注入 | | 7 | Handler | 业务处理 | 最佳实践 [#最佳实践] 1. **中间件粒度**:每个中间件只做一件事 2. **日志规范**:使用命名日志器,便于过滤 3. **错误处理**:返回 `*xError.Error`,由 ResponseBuilder 统一处理 4. **Context 传递**:通过 context 传递中间件提取的信息 下一步 [#下一步] # Proto 文件组织 (/docs/bamboo-base-go/grpc-best-practices/proto-organization) Proto 文件组织 [#proto-文件组织] 良好的 Proto 文件组织是 gRPC 项目可维护性的基础。本章介绍推荐的目录结构、命名规范以及如何复用 `bamboo-base-go` 的基础定义。 目录结构 [#目录结构] 推荐采用**按服务/领域分层**的目录结构: ``` project-root/ ├── proto/ # Proto 源文件根目录 │ ├── link/ # 符号链接目录(外部依赖) │ │ └── base.proto -> ... # 链接到 bamboo-base-go │ │ │ ├── beacon/ # 按项目/命名空间分组 │ │ └── sso/ # 按服务分组 │ │ └── v1/ # 版本化 │ │ ├── auth.proto # 认证服务 │ │ └── public.proto # 公共服务 │ │ │ ├── buf.yaml # Buf 模块配置 │ └── buf.gen.yaml # 代码生成配置 │ ├── internal/ │ └── grpc/ │ ├── gen/ # 生成的 Go 代码(不要手动编辑) │ │ ├── beacon/sso/v1/ │ │ │ ├── auth.pb.go │ │ │ ├── auth_grpc.pb.go │ │ │ └── ... │ │ └── link/ │ │ └── base.pb.go # 来自 bamboo-base-go │ │ │ ├── handler/ # gRPC Handler 实现 │ ├── middleware/ # 服务级中间件 │ └── register/ # 服务注册入口 │ └── Makefile # 包含 proto 生成命令 ``` 包命名规范 [#包命名规范] Package 名称 [#package-名称] 采用**逆向域名 + 服务名 + 版本**的格式: ```proto syntax = "proto3"; // 推荐:命名空间.服务名.版本 package beacon.sso.v1; // 或者简单格式 package proto; ``` Go Package 映射 [#go-package-映射] 通过 `option go_package` 指定生成的 Go 代码路径: ```proto // 完整路径格式(推荐) option go_package = "github.com/your-org/your-project/internal/grpc/gen;pb"; // 相对路径格式(适用于单体项目) option go_package = "./internal/proto/api;bGrpcApi"; ``` 命名约定 [#命名约定] | 元素 | 规范 | 示例 | | ------- | -------------------------------- | -------------------------------- | | Service | PascalCase + Service 后缀 | `AuthService`, `UserService` | | RPC 方法 | PascalCase 动词短语 | `RegisterByEmail`, `GetUserInfo` | | Message | PascalCase + Request/Response 后缀 | `LoginRequest`, `LoginResponse` | | 字段 | snake\_case | `user_id`, `created_at` | | 枚举 | SCREAMING\_SNAKE\_CASE | `USER_STATUS_ACTIVE` | 嵌入 BaseResponse [#嵌入-baseresponse] 所有业务响应消息都应该嵌入 `xBase.BaseResponse`,以便获得统一的元信息: ```proto title="proto/beacon/sso/v1/auth.proto" syntax = "proto3"; package beacon.sso.v1; import "link/base.proto"; // 引入 bamboo-base-go 的基础定义 option go_package = "github.com/your-org/beacon-sso/internal/grpc/gen;pb"; // RegisterByEmailResponse 邮箱注册响应 message RegisterByEmailResponse { // 基础响应信息(必须放在第一个字段) xBase.BaseResponse base_response = 1; // 业务字段从 11 开始,预留 2-10 供 BaseResponse 扩展 string user_id = 11; string token = 12; } ``` 字段编号分配建议 [#字段编号分配建议] | 范围 | 用途 | | ---- | ------------------ | | 1-10 | BaseResponse 及未来扩展 | | 11+ | 业务数据字段 | 这样设计的好处是未来可以在业务消息和 BaseResponse 之间添加新字段,而不会破坏兼容性。 符号链接模式 [#符号链接模式] 为什么使用符号链接 [#为什么使用符号链接] `bamboo-base-go` 提供了 `BaseResponse` 的标准定义,业务端不应该复制粘贴,而应该通过符号链接引用: 1. **保持一致性**:所有服务使用相同版本的 BaseResponse 2. **简化更新**:升级 `bamboo-base-go` 后自动获得最新定义 3. **避免冲突**:通过 M 参数映射到正确的 Go 包路径 创建符号链接 [#创建符号链接] 在 Makefile 中定义初始化命令: ```makefile title="Makefile" # 变量定义 PROTO_FILE ?= beacon/sso/v1/auth.proto BASE_GO_MODULE_DIR := $(shell go list -m -f '{{.Dir}}' github.com/bamboo-services/bamboo-base-go/plugins/grpc) XBASE_LINK := proto/link/base.proto # 初始化 proto 符号链接 proto-init: @mkdir -p $(dir $(XBASE_LINK)) @if [ ! -d "$(BASE_GO_MODULE_DIR)" ]; then \ echo "错误: 找不到 bamboo-base-go 模块,请先运行 go mod download"; \ exit 1; \ fi @ln -sf $(BASE_GO_MODULE_DIR)/proto/base.proto $(XBASE_LINK) @echo "符号链接已创建: $(XBASE_LINK) -> $(BASE_GO_MODULE_DIR)/proto/base.proto" ``` 符号链接目录结构 [#符号链接目录结构] ``` proto/ ├── link/ │ └── base.proto -> /path/to/go/pkg/mod/.../bamboo-base-go@v1.0.0/proto/base.proto └── ... ``` > 符号链接的目标是 Go modules 缓存中的文件,因此必须先执行 `go mod download` 确保依赖已下载。 Message 设计最佳实践 [#message-设计最佳实践] Request Message [#request-message] ```proto message RegisterByEmailRequest { // 必填字段,使用清晰的注释说明 string email = 1; // 邮箱地址(必填) string code = 2; // 验证码(必填) string password = 3; // 密码(必填,至少 6 位且包含字母和数字) // 可选字段使用 optional optional string username = 4; // 用户名(可选,不填则自动生成) optional string nickname = 5; // 昵称(可选) } ``` Response Message [#response-message] ```proto message GetUserResponse { xBase.BaseResponse base_response = 1; // 业务数据 string user_id = 11; string username = 12; string email = 13; // 可选数据 optional string avatar = 14; optional string bio = 15; // 嵌套对象 message Profile { string display_name = 1; int64 created_at = 2; } Profile profile = 16; } ``` 使用 google.protobuf.Timestamp [#使用-googleprotobuftimestamp] 对于时间字段,推荐使用标准类型: ```proto import "google/protobuf/timestamp.proto"; message FileInfo { string file_id = 1; string name = 2; google.protobuf.Timestamp uploaded_at = 3; optional google.protobuf.Timestamp deleted_at = 4; } ``` Service 定义规范 [#service-定义规范] 方法注释 [#方法注释] 每个 RPC 方法都应该添加详细的注释,说明认证要求、请求流程和返回值: ```proto // AuthService 认证服务(需要 App 认证) // // 该服务的所有方法都需要在 metadata 中提供有效的 App 凭证: // - app-access-id: App 的 Access ID // - app-secret-key: App 的 Secret Key service AuthService { // RegisterByEmail 通过邮箱注册 // // 该接口用于通过邮箱验证码完成用户注册,注册成功后自动生成登录 Token。 // 注册流程: // 1. 验证邮箱验证码 // 2. 检查邮箱是否已注册 // 3. 验证密码强度 // 4. 创建用户账号 // 5. 绑定邮箱并标记为已验证 // 6. 生成登录 Token rpc RegisterByEmail(RegisterByEmailRequest) returns (RegisterByEmailResponse); } ``` 公共服务 vs 认证服务 [#公共服务-vs-认证服务] 根据是否需要认证,将服务分组到不同的 proto 文件: ``` proto/beacon/sso/v1/ ├── public.proto # 无需 App 认证的公共服务 ├── auth.proto # 需要 App 认证的认证服务 └── user.proto # 需要用户认证的用户服务 ``` 下一步 [#下一步] # 服务注册 (/docs/bamboo-base-go/grpc-best-practices/service-registration) 服务注册 [#服务注册] 本章介绍如何组织 gRPC 服务的注册流程,以及如何与 `xGrpcRunner` 集成实现统一的生命周期管理。 集中注册入口 [#集中注册入口] 推荐创建一个集中的注册函数,统一管理所有 gRPC 服务的初始化: ```go title="internal/grpc/register/register.go" package register import ( "context" "github.com/your-org/beacon-sso/internal/grpc/handler" "google.golang.org/grpc" ) // RegisterGRPCServices 注册所有 gRPC 服务 // // 该函数作为 xGrpcRunner.WithRegisterService 的回调, // 负责初始化所有 Handler 并注册到 gRPC Server。 func RegisterGRPCServices(ctx context.Context, server grpc.ServiceRegistrar) { // 公共服务(无需 App 认证) handler.NewPublicHandler(ctx, server) // 认证服务(需要 App 认证) handler.NewAuthHandler(ctx, server) // 用户服务(需要用户认证) handler.NewUserHandler(ctx, server) } ``` 目录结构 [#目录结构] ``` internal/grpc/ ├── gen/ # 生成的代码 │ └── beacon/sso/v1/ │ ├── auth.pb.go │ ├── auth_grpc.pb.go │ └── ... ├── handler/ # Handler 实现 │ ├── auth.go │ ├── public.go │ └── user.go ├── middleware/ # 服务级中间件 │ └── app_verify.go └── register/ # 注册入口 └── register.go ``` 与 gRPC Runner 集成 [#与-grpc-runner-集成] 基本集成 [#基本集成] 在 `main.go` 中,将注册函数传给 `xGrpcRunner`: ```go title="main.go" package main import ( "context" "time" xLog "github.com/bamboo-services/bamboo-base-go/common/log" xMain "github.com/bamboo-services/bamboo-base-go/major/main" xReg "github.com/bamboo-services/bamboo-base-go/major/register" xGrpcIUnary "github.com/bamboo-services/bamboo-base-go/plugins/grpc/interceptor/unary" xGrpcRunner "github.com/bamboo-services/bamboo-base-go/plugins/grpc/runner" "github.com/your-org/beacon-sso/internal/app/route" "github.com/your-org/beacon-sso/internal/app/startup" "github.com/your-org/beacon-sso/internal/grpc/register" "google.golang.org/grpc" ) func main() { // 1. 初始化框架 reg := xReg.Register(startup.Init()) log := xLog.WithName(xLog.NamedMAIN) // 2. 定义 gRPC 服务注册函数 registerGrpcService := func(ctx context.Context, server grpc.ServiceRegistrar) { register.RegisterGRPCServices(ctx, server) } // 3. 创建 gRPC 任务 grpcTask := xGrpcRunner.New( xGrpcRunner.WithLogger(xLog.WithName(xLog.NamedGRPC)), xGrpcRunner.WithGracefulStopTimeout(30*time.Second), xGrpcRunner.WithRegisterService(registerGrpcService), xGrpcRunner.WithUnaryInterceptors( xGrpcIUnary.InitContext(reg.Init.Ctx), xGrpcIUnary.Recover(), xGrpcIUnary.Middleware(), xGrpcIUnary.ResponseBuilder(), ), ) // 4. 启动服务(HTTP + gRPC 并行) xMain.Runner(reg, log, route.NewRoute, grpcTask) } ``` 配置选项 [#配置选项] ```go xGrpcRunner.New( // 日志器 xGrpcRunner.WithLogger(xLog.WithName(xLog.NamedGRPC)), // 优雅停止超时 xGrpcRunner.WithGracefulStopTimeout(30*time.Second), // 服务注册回调 xGrpcRunner.WithRegisterService(func(ctx context.Context, server grpc.ServiceRegistrar) { // 注册你的服务... }), // 一元拦截器 xGrpcRunner.WithUnaryInterceptors( xGrpcIUnary.InitContext(reg.Init.Ctx), xGrpcIUnary.Recover(), xGrpcIUnary.Middleware(), xGrpcIUnary.ResponseBuilder(), ), // 流式拦截器(如需流式 RPC) xGrpcRunner.WithStreamInterceptors( xGrpcIStream.InitContext(reg.Init.Ctx), xGrpcIStream.Recover(), xGrpcIStream.Middleware(), ), ) ``` 启动流程 [#启动流程] 完整启动顺序 [#完整启动顺序] ``` main() │ ├── 1. xReg.Register() │ ├── 环境变量初始化 │ ├── 日志系统初始化 │ └── 节点化初始化(数据库、Redis 等) │ ├── 2. xGrpcRunner.New() → 返回任务函数 │ └── 3. xMain.Runner() ├── Gin 引擎创建 ├── 路由注册 ├── gRPC Server 启动(协程) │ ├── 创建 gRPC Listener │ ├── 执行服务注册回调 │ └── 开始接受连接 ├── HTTP Server 启动(协程) └── 信号监听(优雅关闭) ├── SIGINT/SIGTERM ├── HTTP 优雅关闭 └── gRPC 优雅关闭 ``` 环境变量 [#环境变量] | 变量 | 默认值 | 说明 | | ----------------- | ------- | ------------ | | `GRPC_PORT` | `1119` | gRPC 监听端口 | | `GRPC_REFLECTION` | `false` | 是否启用 gRPC 反射 | 启用 gRPC 反射 [#启用-grpc-反射] 开发调试时可启用反射,支持 `grpcurl` 等工具: ```bash # .env GRPC_REFLECTION=true ``` ```bash # 使用 grpcurl 查看服务 grpcurl -plaintext localhost:1119 list grpcurl -plaintext localhost:1119 describe beacon.sso.v1.AuthService ``` 服务隔离注册 [#服务隔离注册] 当项目规模较大时,可以按领域拆分注册函数: ```go title="internal/grpc/register/register.go" package register import ( "context" "google.golang.org/grpc" ) // RegisterGRPCServices 注册所有 gRPC 服务 func RegisterGRPCServices(ctx context.Context, server grpc.ServiceRegistrar) { // 按领域分组注册 registerPublicServices(ctx, server) registerAuthServices(ctx, server) registerUserServices(ctx, server) } // registerPublicServices 注册公共服务(无需认证) func registerPublicServices(ctx context.Context, server grpc.ServiceRegistrar) { handler.NewPublicHandler(ctx, server) } // registerAuthServices 注册认证服务(需要 App 认证) func registerAuthServices(ctx context.Context, server grpc.ServiceRegistrar) { handler.NewAuthHandler(ctx, server) } // registerUserServices 注册用户服务(需要用户认证) func registerUserServices(ctx context.Context, server grpc.ServiceRegistrar) { handler.NewUserHandler(ctx, server) } ``` 多 gRPC 服务场景 [#多-grpc-服务场景] 某些复杂项目可能需要多个 gRPC Server(如内网/外网隔离): ```go title="main.go" func main() { reg := xReg.Register(startup.Init()) log := xLog.WithName(xLog.NamedMAIN) // 内网 gRPC 服务 internalGrpc := xGrpcRunner.New( xGrpcRunner.WithLogger(xLog.WithName(xLog.NamedGRPC, "internal")), xGrpcRunner.WithRegisterService(register.RegisterInternalServices), // ... ) // 外网 gRPC 服务 externalGrpc := xGrpcRunner.New( xGrpcRunner.WithLogger(xLog.WithName(xLog.NamedGRPC, "external")), xGrpcRunner.WithRegisterService(register.RegisterExternalServices), // ... ) xMain.Runner(reg, log, route.NewRoute, internalGrpc, externalGrpc) } ``` 健康检查 [#健康检查] gRPC 健康检查协议 [#grpc-健康检查协议] 实现标准的 gRPC 健康检查协议: ```go title="internal/grpc/handler/health.go" package handler import ( "context" "google.golang.org/grpc/health" "google.golang.org/grpc/health/grpc_health_v1" ) func RegisterHealthServer(server grpc.ServiceRegistrar) { healthServer := health.NewServer() // 设置服务状态 healthServer.SetServingStatus("your-service", grpc_health_v1.HealthCheckResponse_SERVING) grpc_health_v1.RegisterHealthServer(server, healthServer) } ``` Kubernetes 探针集成 [#kubernetes-探针集成] ```yaml title="k8s/deployment.yaml" livenessProbe: exec: command: ["grpc_health_probe", "-addr=:1119"] initialDelaySeconds: 10 readinessProbe: exec: command: ["grpc_health_probe", "-addr=:1119"] initialDelaySeconds: 5 ``` 最佳实践 [#最佳实践] | 要点 | 说明 | | ---------- | ----------------------- | | 集中注册 | 使用统一入口管理所有服务注册 | | Handler 职责 | 构造函数负责注册服务 + 绑定中间件 | | 命名规范 | 服务名与 proto package 保持一致 | | 健康检查 | 实现标准健康检查协议 | 下一步 [#下一步] # 请求绑定 (/docs/bamboo-base-go/utils/binding) 请求绑定 [#请求绑定] `xUtil` 通过统一入口 `Bind` 提供 4 种绑定方法,分别覆盖请求体、查询参数、路径参数与请求头。 ```go import xUtil "github.com/bamboo-services/bamboo-base-go/common/utility" ``` 统一行为 [#统一行为] 四种绑定方法都遵循同一套行为: * 绑定成功:返回传入的 `data` 指针。 * 绑定失败:调用 `xVaild.HandleValidationError(ctx, err)`,执行 `ctx.Abort()`,并返回 `nil`。 * 调用方拿到 `nil` 时,应立即 `return`,避免重复写响应。 API 结构 [#api-结构] ```go title="binding.go" func Bind[T any](ctx *gin.Context, data *T) *pack.Binding[T] func (u *Binding[T]) Data() *T func (u *Binding[T]) Query() *T func (u *Binding[T]) URI() *T func (u *Binding[T]) Header() *T ``` 使用示例 [#使用示例] 1) JSON 请求体:Data [#1-json-请求体data] ```go title="handler/user.go" type CreateUserRequest struct { Username string `json:"username" binding:"required,min=3,max=32"` Email string `json:"email" binding:"required,email"` Password string `json:"password" binding:"required,min=8"` } func CreateUser(ctx *gin.Context) { req := xUtil.Bind(ctx, &CreateUserRequest{}).Data() if req == nil { return } // req 已完成绑定和校验 xResult.Success(ctx, "创建成功") } ``` 2) Query 参数:Query [#2-query-参数query] ```go type ListUserQuery struct { Page int64 `form:"page" binding:"omitempty,min=1"` Size int64 `form:"size" binding:"omitempty,min=1,max=200"` } func ListUsers(ctx *gin.Context) { query := xUtil.Bind(ctx, &ListUserQuery{}).Query() if query == nil { return } xResult.SuccessHasData(ctx, "查询成功", query) } ``` 3) URI 参数:URI [#3-uri-参数uri] ```go type UserURI struct { UserID int64 `uri:"user_id" binding:"required,min=1"` } func GetUser(ctx *gin.Context) { uri := xUtil.Bind(ctx, &UserURI{}).URI() if uri == nil { return } xResult.SuccessHasData(ctx, "查询成功", gin.H{"user_id": uri.UserID}) } ``` 4) Header 参数:Header [#4-header-参数header] ```go type TraceHeader struct { RequestID string `header:"X-Request-UUID" binding:"required"` } func GetProfile(ctx *gin.Context) { header := xUtil.Bind(ctx, &TraceHeader{}).Header() if header == nil { return } xResult.SuccessHasData(ctx, "ok", gin.H{"request_id": header.RequestID}) } ``` 与手动绑定对比(以 Query 为例) [#与手动绑定对比以-query-为例] 手动写法: ```go var query ListUserQuery if err := ctx.ShouldBindQuery(&query); err != nil { xVaild.HandleValidationError(ctx, err) ctx.Abort() return } ``` 使用 `Bind(...).Query()`: ```go query := xUtil.Bind(ctx, &ListUserQuery{}).Query() if query == nil { return } ``` 注意事项 [#注意事项] * `Data` 使用 JSON 绑定,适用于请求体场景。 * `Query` / `URI` / `Header` 分别用于 query、路径参数与 header。 * 返回 `nil` 代表请求已被中止,调用方应立即 `return`。 * 需要完全自定义错误处理时,可保留手动绑定流程。 下一步 [#下一步] # 通用工具 (/docs/bamboo-base-go/utils/common) import { TypeTable } from '@/components/type-table'; 通用工具 [#通用工具] `xUtil` 提供泛型指针操作、切片查找、布尔值转换等通用工具函数。 Ptr [#ptr] 返回给定值的指针: ```go title="common.go" // [!code highlight] func Ptr[T any](data T) *T ``` **示例:** ```go // [!code highlight:3] name := xUtil.Ptr("hello") // *string age := xUtil.Ptr(18) // *int enabled := xUtil.Ptr(true) // *bool // nil 值返回 nil var nilPtr *string = nil result := xUtil.Ptr(nilPtr) // nil ``` **使用场景:** ```go type User struct { Name string Nickname *string // 可选字段 } // [!code highlight:2] // 快速创建可选字段 user := User{ Name: "张三", Nickname: xUtil.Ptr("小张"), } ``` Val [#val] 从指针中安全地获取值,如果指针为 nil 则返回零值: ```go title="common.go" // [!code highlight] func Val[T any](ptr *T) T ``` **示例:** ```go name := xUtil.Ptr("hello") // [!code highlight] value := xUtil.Val(name) // "hello" var nilPtr *string = nil // [!code highlight] value2 := xUtil.Val(nilPtr) // ""(零值) var nilInt *int = nil // [!code highlight] value3 := xUtil.Val(nilInt) // 0(零值) ``` **使用场景:** ```go type Config struct { Timeout *int } func GetTimeout(cfg *Config) int { // [!code highlight:2] // 安全获取可选配置,避免空指针 return xUtil.Val(cfg.Timeout) } ``` Contains [#contains] 检查切片中是否包含指定元素: ```go title="common.go" // [!code highlight] func Contains[T comparable](slice []T, item T) bool ``` **示例:** ```go // [!code highlight:2] // 整数切片 xUtil.Contains([]int{1, 2, 3}, 2) // true xUtil.Contains([]int{1, 2, 3}, 4) // false // [!code highlight:2] // 字符串切片 xUtil.Contains([]string{"a", "b", "c"}, "b") // true xUtil.Contains([]string{"a", "b", "c"}, "d") // false ``` **使用场景:** ```go allowedRoles := []string{"admin", "user", "guest"} func CheckRole(role string) bool { // [!code highlight] return xUtil.Contains(allowedRoles, role) } ``` ToBool [#tobool] 将字符串转换为布尔值,支持多种格式: ```go title="common.go" // [!code highlight] func ToBool(str string, defaultValue bool) bool ``` **支持的格式:** | 值 | 结果 | | ------------------------------------- | -------------- | | `true`, `1`, `yes`, `on`, `enabled` | `true` | | `false`, `0`, `no`, `off`, `disabled` | `false` | | 其他 | `defaultValue` | **示例:** ```go // [!code highlight:4] xUtil.ToBool("true", false) // true xUtil.ToBool("1", false) // true xUtil.ToBool("yes", false) // true xUtil.ToBool("enabled", false) // true // [!code highlight:4] xUtil.ToBool("false", true) // false xUtil.ToBool("0", true) // false xUtil.ToBool("no", true) // false xUtil.ToBool("disabled", true) // false // [!code highlight:2] // 无法识别时返回默认值 xUtil.ToBool("invalid", true) // true xUtil.ToBool("", false) // false ``` **使用场景:** ```go func LoadConfig() { // [!code highlight:2] // 从环境变量读取布尔配置 debugMode := xUtil.ToBool(os.Getenv("DEBUG"), false) enableCache := xUtil.ToBool(os.Getenv("ENABLE_CACHE"), true) } ``` 下一步 [#下一步] # 加密哈希 (/docs/bamboo-base-go/utils/encryption) import { TypeTable } from '@/components/type-table'; 加密哈希 [#加密哈希] `xUtil.Encryption()` 提供 SHA-256 和 MD5 哈希计算函数。 ```go import xUtil "github.com/xiaolfeng/bamboo-base-go/common/utility" ``` 入口函数 [#入口函数] ```go title="utility.go" // [!code highlight] func Encryption() *pack.Encryption ``` SHA-256 哈希 [#sha-256-哈希] SHA256 [#sha256] 计算字符串的 SHA-256 哈希: ```go title="package/encryption.go" // [!code highlight] func (e *Encryption) SHA256(data string) string ``` **返回:** 64 位小写十六进制字符串。 ```go // [!code highlight] hash := xUtil.Encryption().SHA256("password123") // "ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f" ``` SHA256Bytes [#sha256bytes] 计算字节切片的 SHA-256 哈希: ```go title="package/encryption.go" // [!code highlight] func (e *Encryption) SHA256Bytes(data []byte) string ``` ```go data := []byte("password123") // [!code highlight] hash := xUtil.Encryption().SHA256Bytes(data) ``` MD5 哈希 [#md5-哈希] MD5 [#md5] 计算字符串的 MD5 哈希: ```go title="package/encryption.go" // [!code highlight] func (e *Encryption) MD5(str string) string ``` **返回:** 32 位小写十六进制字符串。 ```go // [!code highlight:2] xUtil.Encryption().MD5("password123") // "482c811da5d5b4bc632efdea819417a" ``` MD5Bytes [#md5bytes] 计算字节切片的 MD5 哈希: ```go title="package/encryption.go" // [!code highlight] func (e *Encryption) MD5Bytes(data []byte) string ``` ```go data := []byte("password123") // [!code highlight] hash := xUtil.Encryption().MD5Bytes(data) ``` 使用场景 [#使用场景] 文件完整性校验 [#文件完整性校验] ```go title="service/file.go" func (s *FileService) CheckIntegrity(filePath string) (bool, error) { data, err := os.ReadFile(filePath) if err != nil { return false, err } // [!code highlight] // 计算文件 SHA-256 actual := xUtil.Encryption().SHA256Bytes(data) // 从数据库获取预期的哈希值... expected := "ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f" return actual == expected, nil } ``` 密码存储(不推荐) [#密码存储不推荐] ```go // [!code warning] // 警告:MD5 不适合用于密码存储,请使用 xUtil.Password() 进行密码加密 hash := xUtil.Encryption().MD5("password123") // 仅用于演示 ``` 安全说明 [#安全说明] | 算法 | 输出长度 | 用途 | 安全性 | | ----------- | ----- | ---------- | ------ | | **SHA-256** | 64 字符 | 数据签名、文件校验 | 高 | | **MD5** | 32 字符 | 非安全场景的快速校验 | 低(易碰撞) | > **重要:** MD5 不适合用于密码存储或安全签名,请使用 `xUtil.Password()` 进行密码加密。 下一步 [#下一步] # 函数工具 (/docs/bamboo-base-go/utils/function) import { TypeTable } from '@/components/type-table'; 函数工具 [#函数工具] `xUtil.Function()` 提供与函数反射相关的工具方法,用于获取函数名称、解析方法名等场景。 ```go import xUtil "github.com/xiaolfeng/bamboo-base-go/common/utility" ``` 入口函数 [#入口函数] ```go title="utility.go" // [!code highlight] func Function() *pack.Function ``` 获取函数名 [#获取函数名] GetFunctionName [#getfunctionname] 获取函数的完整名称(包含包路径): ```go title="package/function.go" // [!code highlight] func (f *Function) GetFunctionName(fn interface{}) string ``` 该方法通过反射机制获取传入函数的底层指针,并利用 `runtime.FuncForPC` 查询其运行时信息,从而返回函数的完整标识符。 ```go // [!code highlight:4] xUtil.Function().GetFunctionName(myFunc) // "main.myFunc" xUtil.Function().GetFunctionName(http.Get) // "net/http.Get" xUtil.Function().GetFunctionName(42) // ""(非函数类型) xUtil.Function().GetFunctionName(nil) // ""(nil 输入) ``` 完整示例 [#完整示例] ```go package main import ( "fmt" xUtil "github.com/xiaolfeng/bamboo-base-go/common/utility" ) func myFunction() {} func main() { // [!code highlight] name := xUtil.Function().GetFunctionName(myFunction) fmt.Println(name) // "main.myFunction" // 匿名函数 // [!code highlight] closure := func() {} closureName := xUtil.Function().GetFunctionName(closure) fmt.Println(closureName) // 包含 "func" 字样的特殊格式 } ``` 获取方法名 [#获取方法名] GetMethodName [#getmethodname] 获取方法的方法名(不包含包路径和接收者类型): ```go title="package/function.go" // [!code highlight] func (f *Function) GetMethodName(method interface{}) string ``` 该方法通过反射获取传入方法的运行时信息,并对其完整名称进行解析处理。它会去除包路径、接收者类型(如 `(*Receiver)` 或 `Receiver`)以及闭包方法特有的 `-fm` 后缀,仅返回纯粹的方法名称字符串。 ```go type UserService struct{} func (s *UserService) GetUser() {} func (s *UserService) DeleteUser() {} func main() { service := &UserService{} // [!code highlight:3] xUtil.Function().GetMethodName(service.GetUser) // "GetUser" xUtil.Function().GetMethodName(service.DeleteUser) // "DeleteUser" } ``` 完整示例 [#完整示例-1] ```go package main import ( "fmt" xUtil "github.com/xiaolfeng/bamboo-base-go/common/utility" ) type Handler struct{} func (h *Handler) HandleRequest() {} func (h *Handler) validateInput() {} func main() { handler := &Handler{} // [!code highlight] name := xUtil.Function().GetMethodName(handler.HandleRequest) fmt.Println(name) // "HandleRequest" // 私有方法同样适用 // [!code highlight] privateName := xUtil.Function().GetMethodName(handler.validateInput) fmt.Println(privateName) // "validateInput" } ``` 使用场景 [#使用场景] 日志记录 [#日志记录] 在日志中记录当前调用的方法名,便于调试和追踪: ```go func (s *UserService) CreateUser(ctx *gin.Context, req *CreateUserReq) { // 记录当前方法名 // [!code highlight] methodName := xUtil.Function().GetMethodName(s.CreateUser) s.logger.Info(fmt.Sprintf("[%s] 开始创建用户", methodName)) // 业务逻辑... } ``` 中间件追踪 [#中间件追踪] 在中间件中自动获取 Handler 方法名用于监控: ```go func TraceMiddleware() gin.HandlerFunc { return func(ctx *gin.Context) { // 获取 Handler 方法名(需要特殊处理) // [!code highlight] // 可以在 Handler 中注入方法名用于追踪 ctx.Next() } } ``` 注意事项 [#注意事项] `GetMethodName` 方法不会验证传入参数是否为有效的方法,若传入非法参数可能会导致意外结果。 * 对于非函数类型的输入,`GetFunctionName` 会返回空字符串 * 匿名函数(闭包)的名称格式可能因 Go 版本不同而有所差异 * 私有方法同样可以获取名称 下一步 [#下一步] # 随机生成 (/docs/bamboo-base-go/utils/generate) import { TypeTable } from '@/components/type-table'; 随机生成 [#随机生成] `xUtil.Generate()` 提供各种格式的随机字符串生成函数。 ```go import xUtil "github.com/xiaolfeng/bamboo-base-go/common/utility" ``` 入口函数 [#入口函数] ```go title="utility.go" // [!code highlight] func Generate() *pack.Generate ``` RandomString [#randomstring] 生成包含大小写字母和数字的随机字符串: ```go title="package/generate.go" // [!code highlight] func (g *Generate) RandomString(length int) string ``` **字符集:** `a-z` `A-Z` `0-9` ```go // [!code highlight:3] xUtil.Generate().RandomString(16) // "aB3xY9kLmN2pQ5wR" xUtil.Generate().RandomString(8) // "Kj7mNp2Q" xUtil.Generate().RandomString(32) // "aB3xY9kLmN2pQ5wRaB3xY9kLmN2pQ5wR" ``` RandomUpperString [#randomupperstring] 生成仅包含大写字母和数字的随机字符串: ```go title="package/generate.go" // [!code highlight] func (g *Generate) RandomUpperString(length int) string ``` **字符集:** `A-Z` `0-9` ```go // [!code highlight:2] xUtil.Generate().RandomUpperString(8) // "A3X9K2N5" xUtil.Generate().RandomUpperString(6) // "K7M2N9" ``` RandomNumberString [#randomnumberstring] 生成仅包含数字的随机字符串: ```go title="package/generate.go" // [!code highlight] func (g *Generate) RandomNumberString(length int) string ``` **字符集:** `0-9` ```go // [!code highlight:2] xUtil.Generate().RandomNumberString(6) // "384729" xUtil.Generate().RandomNumberString(4) // "9527" ``` 使用场景 [#使用场景] 验证码 [#验证码] ```go title="service/captcha.go" func (s *CaptchaService) Generate() string { // [!code highlight] // 6 位数字验证码 return xUtil.Generate().RandomNumberString(6) } ``` 邀请码 [#邀请码] ```go title="service/invite.go" func (s *InviteService) GenerateCode() string { // [!code highlight] // 8 位大写字母数字邀请码 return xUtil.Generate().RandomUpperString(8) } ``` 临时密码 [#临时密码] ```go title="service/user.go" func (s *UserService) ResetPassword(ctx context.Context, userID int64) (string, error) { // [!code highlight] // 生成 12 位临时密码 tempPassword := xUtil.Generate().RandomString(12) // [!code highlight] hash, err := xUtil.Password().Encrypt(tempPassword) if err != nil { return "", err } err = s.repo.UpdatePassword(ctx, userID, hash) if err != nil { return "", err } return tempPassword, nil } ``` 文件名 [#文件名] ```go title="service/upload.go" func (s *UploadService) GenerateFileName(ext string) string { // [!code highlight] // 生成唯一文件名 return xUtil.Generate().RandomString(16) + ext } ``` 订单号 [#订单号] ```go title="service/order.go" func (s *OrderService) GenerateOrderNo() string { // [!code highlight:2] // 日期 + 随机数字 date := xUtil.Timer().FormatNow("20060102") random := xUtil.Generate().RandomNumberString(8) return "ORD" + date + random } ``` 下一步 [#下一步] # 概述 (/docs/bamboo-base-go/utils) import { TypeTable } from '@/components/type-table'; 工具函数 [#工具函数] `xUtil` 提供丰富的工具函数,涵盖字符串处理、时间操作、数据验证、加密哈希、密码处理等常用场景。 ```go import xUtil "github.com/xiaolfeng/bamboo-base-go/common/utility" ``` 功能分类 [#功能分类] 调用方式 [#调用方式] 所有工具函数采用**链式调用**方式: ```go // [!code highlight] // 字符串工具 xUtil.Str().IsBlank(" ") // true xUtil.Str().DefaultIfBlank("", "default") // "default" // [!code highlight] // 时间工具 xUtil.Timer().FormatNow("2006-01-02") // "2026-02-02" xUtil.Timer().StartOfDay(time.Now()) // 今天 00:00:00 // [!code highlight] // 验证工具 xUtil.Valid().IsPhone("13812345678") // true xUtil.Valid().IsEmail("test@example.com") // true // [!code highlight] // 密码工具 hash, _ := xUtil.Password().Encrypt("myPassword123") xUtil.Password().IsValid("myPassword123", hash) // true // [!code highlight] // 生成工具 code := xUtil.Generate().RandomString(16) // "aB3xY9kLmN2pQ5wR" numCode := xUtil.Generate().RandomNumberString(6) // "384729" // [!code highlight] // 安全密钥工具 key := xUtil.Security().GenerateKey() // "cs_a1b2c3d4..." ``` 快速示例 [#快速示例] 字符串处理 [#字符串处理] ```go // 空白检查 // [!code highlight:2] xUtil.Str().IsBlank(" ") // true xUtil.Str().DefaultIfBlank("", "default") // "default" // 字符串脱敏 // [!code highlight] xUtil.Str().Mask("13812345678", 3, 4, "*") // "138****5678" // 命名转换 // [!code highlight] xUtil.Str().CamelToSnake("userName") // "user_name" ``` 时间处理 [#时间处理] ```go // 格式化 // [!code highlight] dateStr := xUtil.Timer().FormatNow("2006-01-02") // "2026-02-02" // 时间范围 // [!code highlight:2] start := xUtil.Timer().StartOfDay(time.Now()) // 今天 00:00:00 end := xUtil.Timer().EndOfMonth(time.Now()) // 本月最后一天 23:59:59 // 年龄计算 // [!code highlight] age := xUtil.Timer().Age(birthday) // 26 ``` 数据验证 [#数据验证] ```go // 格式验证 // [!code highlight:2] xUtil.Valid().IsPhone("13812345678") // true xUtil.Valid().IsEmail("test@example.com") // true xUtil.Valid().IsUUID("550e8400-...") // true // 密码强度 // [!code highlight] xUtil.Valid().IsStrongPassword("Abc123!@#") // true ``` 密码加密 [#密码加密] ```go // 加密密码 // [!code highlight] hash, _ := xUtil.Password().Encrypt("myPassword123") // 验证密码 // [!code highlight] isValid := xUtil.Password().IsValid("myPassword123", hash) // true ``` 随机生成 [#随机生成] ```go // 随机字符串 // [!code highlight:2] code := xUtil.Generate().RandomString(16) // "aB3xY9kLmN2pQ5wR" numCode := xUtil.Generate().RandomNumberString(6) // "384729" // 安全密钥 // [!code highlight] key := xUtil.Security().GenerateKey() // "cs_a1b2c3d4..." ``` 下一步 [#下一步] # 类型解析 (/docs/bamboo-base-go/utils/parse) import { TypeTable } from '@/components/type-table'; 类型解析 [#类型解析] `xUtil.Parse()` 提供统一的类型解析入口,覆盖整数、浮点数、布尔与字符串的常见转换场景。 ```go import xUtil "github.com/bamboo-services/bamboo-base-go/common/utility" ``` 快速使用 [#快速使用] ```go parser := xUtil.Parse() // [!code highlight:2] // 整数解析 age, ok := parser.Int("18") // [!code highlight:2] // 布尔解析 enabled, ok := parser.Bool("yes") ``` 解析规则 [#解析规则] * 所有方法返回 `(value, ok)`,`ok` 为 `false` 表示类型不支持或解析失败 * `string` 输入会先 `TrimSpace` 再解析 * 浮点输入解析整数时会**截断小数部分** * `Uint*` 在负数或超出范围时返回失败 方法一览 [#方法一览] 整数解析 [#整数解析] ```go value, ok := xUtil.Parse().Int("123") ``` **支持类型:** 所有整数、浮点数、`string` **规则:** * 字符串按十进制解析 * 浮点数会截断小数部分 * 超出范围或类型不支持返回失败 无符号整数解析 [#无符号整数解析] ```go value, ok := xUtil.Parse().Uint("123") ``` **支持类型:** 所有整数、浮点数、`string` **规则:** * 负数或超出范围返回失败 * 浮点数会截断小数部分 浮点解析 [#浮点解析] ```go value32, ok := xUtil.Parse().Float32("3.14") value64, ok := xUtil.Parse().Float64(3.14) ``` **支持类型:** 所有整数、浮点数、`string` **规则:** * `Float32` 超出可表示范围会失败 * `Float64` 解析失败或类型不支持会失败 布尔解析 [#布尔解析] ```go value, ok := xUtil.Parse().Bool("enabled") ``` **支持类型:** `bool`、所有整数、浮点数、`string` **字符串识别:** * `true`, `1`, `yes`, `on`, `enabled` → `true` * `false`, `0`, `no`, `off`, `disabled` → `false` **数值规则:** * 非 0 为 `true`,0 为 `false` 字符串解析 [#字符串解析] ```go value, ok := xUtil.Parse().String([]byte("hello")) ``` **支持类型:** `string`、`[]byte`、`fmt.Stringer`、`error`、基础数值类型 **规则:** * 数值类型按十进制格式化 * 浮点数使用 Go 默认格式规则 失败语义 [#失败语义] * `Int*`/`Uint*`/`Float*`:返回 `0` 和 `false` * `Bool`:返回 `false` 和 `false` * `String`:返回空字符串和 `false` 下一步 [#下一步] # 密码处理 (/docs/bamboo-base-go/utils/password) import { TypeTable } from '@/components/type-table'; 密码处理 [#密码处理] `xUtil.Password()` 提供基于 bcrypt 的密码加密和验证函数,额外使用 Base64 编码增强安全性。 ```go import xUtil "github.com/xiaolfeng/bamboo-base-go/common/utility" ``` 入口函数 [#入口函数] ```go title="utility.go" // [!code highlight] func Password() *pack.Password ``` 加密流程 [#加密流程] ``` 明文密码 → Base64 编码 → bcrypt 哈希 → 存储 ``` Encrypt [#encrypt] 加密密码并返回字节切片: ```go title="package/password.go" // [!code highlight] func (p *Password) Encrypt(pass string) ([]byte, error) ``` MustEncrypt [#mustencrypt] 加密密码,失败时 panic: ```go title="package/password.go" // [!code highlight] func (p *Password) MustEncrypt(pass string) []byte ``` EncryptString [#encryptstring] 加密密码并返回字符串: ```go title="package/password.go" // [!code highlight] func (p *Password) EncryptString(pass string) (string, error) ``` MustEncryptString [#mustencryptstring] 加密密码返回字符串,失败时 panic: ```go title="package/password.go" // [!code highlight] func (p *Password) MustEncryptString(pass string) string ``` **示例:** ```go // [!code highlight:2] // 推荐:返回字符串,便于存储 hash, err := xUtil.Password().EncryptString("myPassword123") if err != nil { log.Fatal(err) } // [!code highlight:2] // 简便方式(失败时 panic) hash := xUtil.Password().MustEncryptString("myPassword123") ``` Verify [#verify] 验证密码是否匹配: ```go title="package/password.go" // [!code highlight] func (p *Password) Verify(inputPass, hashPass string) error ``` **返回:** 匹配返回 `nil`,不匹配返回错误。 IsValid [#isvalid] 验证密码是否匹配(布尔值): ```go title="package/password.go" // [!code highlight] func (p *Password) IsValid(inputPass, hashPass string) bool ``` **示例:** ```go // [!code highlight] hash := xUtil.Password().MustEncryptString("myPassword123") // [!code highlight:2] // 方式一:检查错误 err := xUtil.Password().Verify("myPassword123", hash) if err != nil { fmt.Println("密码错误") } // [!code highlight:2] // 方式二:布尔值 if xUtil.Password().IsValid("myPassword123", hash) { fmt.Println("密码正确") } ``` 使用示例 [#使用示例] 用户注册 [#用户注册] ```go title="service/user.go" func (s *UserService) Register(ctx context.Context, req *dto.RegisterRequest) error { // [!code highlight:2] // 加密密码 hash, err := xUtil.Password().EncryptString(req.Password) if err != nil { return err } user := &entity.User{ Username: req.Username, // [!code highlight] Password: hash, } return s.repo.Create(ctx, user) } ``` 用户登录 [#用户登录] ```go title="service/user.go" func (s *UserService) Login(ctx context.Context, req *dto.LoginRequest) (*entity.User, error) { user, err := s.repo.FindByUsername(ctx, req.Username) if err != nil { return nil, err } // [!code highlight:3] // 验证密码 if !xUtil.Password().IsValid(req.Password, user.Password) { return nil, errors.New("密码错误") } return user, nil } ``` 修改密码 [#修改密码] ```go title="service/user.go" func (s *UserService) ChangePassword(ctx context.Context, userID int64, oldPass, newPass string) error { user, err := s.repo.FindByID(ctx, userID) if err != nil { return err } // [!code highlight:3] // 验证旧密码 if !xUtil.Password().IsValid(oldPass, user.Password) { return errors.New("原密码错误") } // [!code highlight:2] // 加密新密码 hash, err := xUtil.Password().EncryptString(newPass) if err != nil { return err } return s.repo.UpdatePassword(ctx, userID, hash) } ``` 安全说明 [#安全说明] * **bcrypt**: 使用 bcrypt 算法,自动加盐,抗彩虹表攻击 * **Base64 预处理**: 先进行 Base64 编码,增加一层保护 * **默认成本**: 使用 `bcrypt.DefaultCost`(10),平衡安全性和性能 * **不可逆**: 哈希后的密码无法还原为明文 下一步 [#下一步] # 安全密钥 (/docs/bamboo-base-go/utils/security) import { TypeTable } from '@/components/type-table'; 安全密钥 [#安全密钥] `xUtil.Security()` 提供安全密钥的生成和验证函数,密钥以 `cs_` 为前缀。 ```go import xUtil "github.com/xiaolfeng/bamboo-base-go/common/utility" ``` 入口函数 [#入口函数] ```go title="utility.go" // [!code highlight] func Security() *pack.Security ``` 密钥格式 [#密钥格式] | 类型 | 格式 | 长度 | | -------- | ---------------- | ----- | | **长密钥** | `cs_` + 64 位十六进制 | 67 字符 | | **标准密钥** | `cs_` + 32 位十六进制 | 35 字符 | 示例: ``` // 长密钥 cs_a1b2c3d4e5f6789012345678901234567890123456789012345678901234 // 标准密钥 cs_a1b2c3d4e5f67890123456789012345678 ``` GenerateLongKey [#generatelongkey] 生成长安全密钥(67 字符): ```go title="package/security.go" // [!code highlight] func (s *Security) GenerateLongKey() string ``` ```go // [!code highlight] key := xUtil.Security().GenerateLongKey() // "cs_a1b2c3d4e5f6789012345678901234567890123456789012345678901234" ``` GenerateKey [#generatekey] 生成标准安全密钥(35 字符): ```go title="package/security.go" // [!code highlight] func (s *Security) GenerateKey() string ``` ```go // [!code highlight] key := xUtil.Security().GenerateKey() // "cs_a1b2c3d4e5f6789012345678901234" ``` VerifyKey [#verifykey] 验证密钥格式是否正确: ```go title="package/security.go" // [!code highlight] func (s *Security) VerifyKey(input string) bool ``` **验证规则:** 1. 以 `cs_` 前缀开头 2. 后面是 32 或 64 位十六进制字符 ```go // [!code highlight:4] xUtil.Security().VerifyKey("cs_a1b2c3d4e5f6789012345678901234") // true(32位) xUtil.Security().VerifyKey("cs_a1b2c3d4e5f678901234567890123456789012345678901234") // true(64位) xUtil.Security().VerifyKey("550e8400e29b41d4a716446655440000") // false(缺少 cs_ 前缀) xUtil.Security().VerifyKey("cs_invalid") // false(长度不对) xUtil.Security().VerifyKey("cs_GGGG8400e29b41d4a716446655440000") // false(非十六进制) ``` 使用场景 [#使用场景] API 密钥 [#api-密钥] ```go title="service/api_key.go" func (s *APIKeyService) Create(ctx context.Context, userID int64) (*entity.APIKey, error) { apiKey := &entity.APIKey{ UserID: userID, // [!code highlight] Key: xUtil.Security().GenerateLongKey(), Status: 1, } return s.repo.Create(ctx, apiKey) } ``` 访问令牌 [#访问令牌] ```go title="service/token.go" func (s *TokenService) GenerateAccessToken(userID int64) string { // [!code highlight] return xUtil.Security().GenerateKey() } ``` 密钥验证中间件 [#密钥验证中间件] ```go title="middleware/api_key.go" func APIKeyMiddleware() gin.HandlerFunc { return func(ctx *gin.Context) { apiKey := ctx.GetHeader("X-API-Key") // [!code highlight:4] // 验证密钥格式 if !xUtil.Security().VerifyKey(apiKey) { ctx.Error(xError.NewError(ctx.Request.Context(), xError.Unauthorized, "无效的 API 密钥", true)) return } // 验证密钥是否存在于数据库... ctx.Next() } } ``` 安全建议 [#安全建议] 1. **使用长密钥**: 对于高安全性场景,建议使用 `GenerateLongKey()` 2. **验证密钥**: 接收密钥时始终调用 `VerifyKey()` 验证格式 3. **存储哈希**: 数据库中存储密钥的哈希值而非明文 4. **定期轮换**: 建议定期轮换密钥 下一步 [#下一步] # 字符串处理 (/docs/bamboo-base-go/utils/string) import { TypeTable } from '@/components/type-table'; 字符串处理 [#字符串处理] `xUtil.Str()` 提供丰富的字符串处理函数,包括空白检查、截断、命名转换、脱敏等。 ```go import xUtil "github.com/xiaolfeng/bamboo-base-go/common/utility" ``` 入口函数 [#入口函数] ```go title="utility.go" // [!code highlight] func Str() *pack.Str ``` 空白检查 [#空白检查] IsBlank [#isblank] 检查字符串是否为空白(空字符串或只包含空白字符): ```go title="package/string.go" // [!code highlight] func (s *Str) IsBlank(str string) bool ``` ```go // [!code highlight:4] xUtil.Str().IsBlank("") // true xUtil.Str().IsBlank(" ") // true xUtil.Str().IsBlank("\t\n") // true xUtil.Str().IsBlank("hello") // false ``` IsNotBlank [#isnotblank] 检查字符串是否不为空白: ```go title="package/string.go" // [!code highlight] func (s *Str) IsNotBlank(str string) bool ``` ```go // [!code highlight:2] xUtil.Str().IsNotBlank("hello") // true xUtil.Str().IsNotBlank("") // false ``` DefaultIfBlank [#defaultifblank] 如果字符串为空白则返回默认值: ```go title="package/string.go" // [!code highlight] func (s *Str) DefaultIfBlank(str, defaultStr string) string ``` ```go // [!code highlight:3] xUtil.Str().DefaultIfBlank("", "default") // "default" xUtil.Str().DefaultIfBlank(" ", "default") // "default" xUtil.Str().DefaultIfBlank("hello", "default") // "hello" ``` 字符串截断 [#字符串截断] Truncate [#truncate] 截断字符串到指定长度: ```go title="package/string.go" // [!code highlight] func (s *Str) Truncate(str string, maxLen int) string ``` ```go // [!code highlight:2] xUtil.Str().Truncate("Hello World", 5) // "Hello" xUtil.Str().Truncate("Hi", 10) // "Hi" ``` TruncateWithSuffix [#truncatewithsuffix] 截断字符串并添加后缀: ```go title="package/string.go" // [!code highlight] func (s *Str) TruncateWithSuffix(str string, maxLen int, suffix string) string ``` ```go // [!code highlight:3] xUtil.Str().TruncateWithSuffix("Hello World", 8, "...") // "Hello..." xUtil.Str().TruncateWithSuffix("Hi", 10, "...") // "Hi" xUtil.Str().TruncateWithSuffix("Hello", 8, "") // "Hello"(默认后缀 "...") ``` 命名转换 [#命名转换] CamelToSnake [#cameltosnake] 驼峰命名转蛇形命名: ```go title="package/string.go" // [!code highlight] func (s *Str) CamelToSnake(str string) string ``` ```go // [!code highlight:4] xUtil.Str().CamelToSnake("userName") // "user_name" xUtil.Str().CamelToSnake("UserName") // "user_name" xUtil.Str().CamelToSnake("userID") // "user_i_d" xUtil.Str().CamelToSnake("HTTPServer") // "h_t_t_p_server" ``` SnakeToCamel [#snaketocamel] 蛇形命名转驼峰命名: ```go title="package/string.go" // [!code highlight] func (s *Str) SnakeToCamel(str string) string ``` ```go // [!code highlight:3] xUtil.Str().SnakeToCamel("user_name") // "userName" xUtil.Str().SnakeToCamel("user_id") // "userId" xUtil.Str().SnakeToCamel("http_server") // "httpServer" ``` 字符串脱敏 [#字符串脱敏] Mask [#mask] 对字符串进行脱敏处理: ```go title="package/string.go" // [!code highlight] func (s *Str) Mask(str string, start, end int, mask string) string ``` **参数说明:** * `start`: 开始保留的字符数 * `end`: 结尾保留的字符数 * `mask`: 用于替换的字符 ```go // [!code highlight:4] // 手机号脱敏 xUtil.Str().Mask("13812345678", 3, 4, "*") // "138****5678" // 邮箱脱敏 xUtil.Str().Mask("hello@example.com", 2, 11, "*") // "he***********.com" // 身份证脱敏 xUtil.Str().Mask("110101199001011234", 3, 4, "*") // "110***********1234" ``` 其他工具 [#其他工具] RemoveSpaces [#removespaces] 移除所有空白字符: ```go title="package/string.go" // [!code highlight] func (s *Str) RemoveSpaces(str string) string ``` ```go // [!code highlight:2] xUtil.Str().RemoveSpaces("Hello World") // "HelloWorld" xUtil.Str().RemoveSpaces("a b c") // "abc" ``` CountWords [#countwords] 统计单词数量: ```go title="package/string.go" // [!code highlight] func (s *Str) CountWords(str string) int ``` ```go // [!code highlight:2] xUtil.Str().CountWords("Hello World") // 2 xUtil.Str().CountWords("你好 世界") // 2 ``` IsValidEmail [#isvalidemail] 验证邮箱格式: ```go title="package/string.go" // [!code highlight] func (s *Str) IsValidEmail(email string) bool ``` ```go // [!code highlight:3] xUtil.Str().IsValidEmail("test@example.com") // true xUtil.Str().IsValidEmail("invalid") // false xUtil.Str().IsValidEmail("test@.com") // false ``` 下一步 [#下一步] # 时间处理 (/docs/bamboo-base-go/utils/time) import { TypeTable } from '@/components/type-table'; 时间处理 [#时间处理] `xUtil.Timer()` 提供常用的时间处理函数,包括格式化、解析、范围计算、年龄计算等。 ```go import xUtil "github.com/xiaolfeng/bamboo-base-go/common/utility" ``` 入口函数 [#入口函数] ```go title="utility.go" // [!code highlight] func Timer() *pack.Timer ``` 预定义格式 [#预定义格式] ```go title="package/time.go" const ( TimeFormatDate = "2006-01-02" // 日期格式 TimeFormatDateTime = "2006-01-02 15:04:05" // 日期时间格式 TimeFormatTime = "15:04:05" // 时间格式 TimeFormatISO8601 = "2006-01-02T15:04:05Z07:00" // ISO8601 格式 TimeFormatUnix = "1136239445" // Unix 时间戳格式 ) ``` 获取当前时间 [#获取当前时间] Now [#now] 获取当前时间: ```go title="package/time.go" // [!code highlight] func (t *Timer) Now() time.Time ``` NowUnix [#nowunix] 获取当前 Unix 时间戳(秒): ```go title="package/time.go" // [!code highlight] func (t *Timer) NowUnix() int64 ``` NowUnixMilli [#nowunixmilli] 获取当前 Unix 时间戳(毫秒): ```go title="package/time.go" // [!code highlight] func (t *Timer) NowUnixMilli() int64 ``` ```go // [!code highlight:3] now := xUtil.Timer().Now() // time.Time unix := xUtil.Timer().NowUnix() // 1738387200 milli := xUtil.Timer().NowUnixMilli() // 1738387200000 ``` 格式化 [#格式化] Format [#format] 格式化时间为字符串: ```go title="package/time.go" // [!code highlight] func (t *Timer) Format(time time.Time, layout string) string ``` FormatNow [#formatnow] 格式化当前时间: ```go title="package/time.go" // [!code highlight] func (t *Timer) FormatNow(layout string) string ``` ```go // [!code highlight:4] xUtil.Timer().Format(time.Now(), xUtil.TimeFormatDate) // "2026-02-02" xUtil.Timer().Format(time.Now(), xUtil.TimeFormatDateTime) // "2026-02-02 15:04:05" xUtil.Timer().FormatNow(xUtil.TimeFormatDate) // "2026-02-02" xUtil.Timer().FormatNow("20060102") // "20260202" ``` 解析 [#解析] Parse [#parse] 解析时间字符串: ```go title="package/time.go" // [!code highlight] func (t *Timer) Parse(layout, value string) (time.Time, error) ``` MustParse [#mustparse] 解析时间字符串(失败时 panic): ```go title="package/time.go" // [!code highlight] func (t *Timer) MustParse(layout, value string) time.Time ``` ```go // [!code highlight:3] t, err := xUtil.Timer().Parse(xUtil.TimeFormatDate, "2026-02-02") t := xUtil.Timer().MustParse(xUtil.TimeFormatDate, "2026-02-02") t := xUtil.Timer().MustParse("20060102", "20260202") // 使用自定义格式 ``` FromUnix [#fromunix] 从 Unix 时间戳(秒)解析: ```go title="package/time.go" // [!code highlight] func (t *Timer) FromUnix(unix int64) time.Time ``` FromUnixMilli [#fromunixmilli] 从 Unix 时间戳(毫秒)解析: ```go title="package/time.go" // [!code highlight] func (t *Timer) FromUnixMilli(unixMilli int64) time.Time ``` ```go // [!code highlight:2] t1 := xUtil.Timer().FromUnix(1738387200) // time.Time t2 := xUtil.Timer().FromUnixMilli(1738387200000) // time.Time ``` 时间判断 [#时间判断] IsToday [#istoday] 判断是否为今天: ```go title="package/time.go" // [!code highlight] func (t *Timer) IsToday(t time.Time) bool ``` IsYesterday [#isyesterday] 判断是否为昨天: ```go title="package/time.go" // [!code highlight] func (t *Timer) IsYesterday(t time.Time) bool ``` IsWeekend [#isweekend] 判断是否为周末: ```go title="package/time.go" // [!code highlight] func (t *Timer) IsWeekend(t time.Time) bool ``` ```go // [!code highlight:3] xUtil.Timer().IsToday(time.Now()) // true xUtil.Timer().IsYesterday(yesterday) // true xUtil.Timer().IsWeekend(saturday) // true ``` 时间范围 [#时间范围] StartOfDay / EndOfDay [#startofday--endofday] 获取当天的开始和结束时间: ```go title="package/time.go" // [!code highlight:2] func (t *Timer) StartOfDay(t time.Time) time.Time // 00:00:00 func (t *Timer) EndOfDay(t time.Time) time.Time // 23:59:59.999999999 ``` StartOfWeek / EndOfWeek [#startofweek--endofweek] 获取本周的开始和结束时间(周一为一周开始): ```go title="package/time.go" // [!code highlight:2] func (t *Timer) StartOfWeek(t time.Time) time.Time // 周一 00:00:00 func (t *Timer) EndOfWeek(t time.Time) time.Time // 周日 23:59:59.999999999 ``` StartOfMonth / EndOfMonth [#startofmonth--endofmonth] 获取本月的开始和结束时间: ```go title="package/time.go" // [!code highlight:2] func (t *Timer) StartOfMonth(t time.Time) time.Time // 1日 00:00:00 func (t *Timer) EndOfMonth(t time.Time) time.Time // 月末 23:59:59.999999999 ``` **示例:** ```go now := time.Now() // [!code highlight:2] // 今天的时间范围 start := xUtil.Timer().StartOfDay(now) // 2026-02-02 00:00:00 end := xUtil.Timer().EndOfDay(now) // 2026-02-02 23:59:59.999999999 // [!code highlight:2] // 本周的时间范围 weekStart := xUtil.Timer().StartOfWeek(now) // 2026-02-02 00:00:00(周一) weekEnd := xUtil.Timer().EndOfWeek(now) // 2026-02-08 23:59:59.999999999(周日) // [!code highlight:2] // 本月的时间范围 monthStart := xUtil.Timer().StartOfMonth(now) // 2026-02-01 00:00:00 monthEnd := xUtil.Timer().EndOfMonth(now) // 2026-02-28 23:59:59.999999999 ``` 时间差计算 [#时间差计算] DiffDays [#diffdays] 计算天数差: ```go title="package/time.go" // [!code highlight] func (t *Timer) DiffDays(t1, t2 time.Time) int ``` DiffHours [#diffhours] 计算小时差: ```go title="package/time.go" // [!code highlight] func (t *Timer) DiffHours(t1, t2 time.Time) float64 ``` DiffMinutes [#diffminutes] 计算分钟差: ```go title="package/time.go" // [!code highlight] func (t *Timer) DiffMinutes(t1, t2 time.Time) float64 ``` ```go t1 := time.Now() t2 := t1.AddDate(0, 0, -7) // [!code highlight:3] xUtil.Timer().DiffDays(t1, t2) // 7 xUtil.Timer().DiffHours(t1, t2) // 168.0 xUtil.Timer().DiffMinutes(t1, t2) // 10080.0 ``` 年龄计算 [#年龄计算] Age [#age] 根据生日计算年龄: ```go title="package/time.go" // [!code highlight] func (t *Timer) Age(birthday time.Time) int ``` ```go birthday := xUtil.Timer().MustParse(xUtil.TimeFormatDate, "2000-01-01") // [!code highlight] age := xUtil.Timer().Age(birthday) // 26(假设当前是 2026 年) ``` 下一步 [#下一步] # 数据验证 (/docs/bamboo-base-go/utils/validate) import { TypeTable } from '@/components/type-table'; 数据验证 [#数据验证] `xUtil.Valid()` 提供各种数据格式的验证函数,用于快速校验用户输入。 ```go import xUtil "github.com/xiaolfeng/bamboo-base-go/common/utility" ``` 入口函数 [#入口函数] ```go title="utility.go" // [!code highlight] func Valid() *pack.Valid ``` 手机号验证 [#手机号验证] IsPhone [#isphone] 验证中国大陆手机号(1\[3-9] 开头,11 位): ```go title="package/validate.go" // [!code highlight] func (v *Valid) IsPhone(phone string) bool ``` ```go // [!code highlight:3] xUtil.Valid().IsPhone("13812345678") // true xUtil.Valid().IsPhone("12345678901") // false(第二位不是 3-9) xUtil.Valid().IsPhone("1381234567") // false(不足 11 位) ``` 身份证验证 [#身份证验证] IsIDCard [#isidcard] 验证中国大陆身份证号(18 位): ```go title="package/validate.go" // [!code highlight] func (v *Valid) IsIDCard(idCard string) bool ``` ```go // [!code highlight:3] xUtil.Valid().IsIDCard("110101199001011234") // true xUtil.Valid().IsIDCard("11010119900101123X") // true(X 结尾) xUtil.Valid().IsIDCard("123456789012345678") // false ``` URL 验证 [#url-验证] IsURI [#isuri] 验证 URL 格式: ```go title="package/validate.go" // [!code highlight] func (v *Valid) IsURI(url string) bool ``` ```go // [!code highlight:4] xUtil.Valid().IsURI("https://example.com") // true xUtil.Valid().IsURI("http://localhost:8080/path") // true xUtil.Valid().IsURI("ftp://example.com") // false xUtil.Valid().IsURI("example.com") // false ``` IP 地址验证 [#ip-地址验证] IsIP [#isip] 验证 IPv4 地址: ```go title="package/validate.go" // [!code highlight] func (v *Valid) IsIP(ip string) bool ``` ```go // [!code highlight:4] xUtil.Valid().IsIP("192.168.1.1") // true xUtil.Valid().IsIP("255.255.255.255") // true xUtil.Valid().IsIP("256.1.1.1") // false xUtil.Valid().IsIP("192.168.1") // false ``` UUID 验证 [#uuid-验证] IsUUID [#isuuid] 验证 UUID 格式: ```go title="package/validate.go" // [!code highlight] func (v *Valid) IsUUID(uuid string) bool ``` ```go // [!code highlight:3] xUtil.Valid().IsUUID("550e8400-e29b-41d4-a716-446655440000") // true xUtil.Valid().IsUUID("550e8400e29b41d4a716446655440000") // false(缺少连字符) xUtil.Valid().IsUUID("invalid-uuid") // false ``` 字符类型验证 [#字符类型验证] IsNumeric [#isnumeric] 检查是否只包含数字: ```go title="package/validate.go" // [!code highlight] func (v *Valid) IsNumeric(str string) bool ``` ```go // [!code highlight:3] xUtil.Valid().IsNumeric("123456") // true xUtil.Valid().IsNumeric("123abc") // false xUtil.Valid().IsNumeric("") // false ``` IsAlpha [#isalpha] 检查是否只包含字母: ```go title="package/validate.go" // [!code highlight] func (v *Valid) IsAlpha(str string) bool ``` ```go // [!code highlight:3] xUtil.Valid().IsAlpha("Hello") // true xUtil.Valid().IsAlpha("Hello123") // false xUtil.Valid().IsAlpha("") // false ``` IsAlphaNumeric [#isalphanumeric] 检查是否只包含字母和数字: ```go title="package/validate.go" // [!code highlight] func (v *Valid) IsAlphaNumeric(str string) bool ``` ```go // [!code highlight:3] xUtil.Valid().IsAlphaNumeric("Hello123") // true xUtil.Valid().IsAlphaNumeric("Hello_123") // false(包含下划线) xUtil.Valid().IsAlphaNumeric("") // false ``` 用户名验证 [#用户名验证] IsUsername [#isusername] 验证用户名(4-20 位,字母开头,只能包含字母、数字、下划线): ```go title="package/validate.go" // [!code highlight] func (v *Valid) IsUsername(username string) bool ``` ```go // [!code highlight:4] xUtil.Valid().IsUsername("xiao_lfeng") // true xUtil.Valid().IsUsername("user123") // true xUtil.Valid().IsUsername("123user") // false(数字开头) xUtil.Valid().IsUsername("ab") // false(少于 4 位) ``` 密码强度验证 [#密码强度验证] IsStrongPassword [#isstrongpassword] 验证强密码(至少 8 位,包含大写、小写、数字、特殊字符): ```go title="package/validate.go" // [!code highlight] func IsStrongPassword(password string) bool ``` ```go // [!code highlight:4] xUtil.IsStrongPassword("Abc123!@#") // true xUtil.IsStrongPassword("abc123!@#") // false(缺少大写) xUtil.IsStrongPassword("Abc12345") // false(缺少特殊字符) xUtil.IsStrongPassword("Abc!@#") // false(少于 8 位) ``` 范围验证 [#范围验证] InRange [#inrange] 检查数值是否在指定范围内: ```go title="package/validate.go" // [!code highlight] func (v *Valid) InRange(value, min, max float64) bool ``` ```go // [!code highlight:3] xUtil.Valid().InRange(5.5, 1.0, 10.0) // true xUtil.Valid().InRange(0.5, 1.0, 10.0) // false xUtil.Valid().InRange(10.5, 1.0, 10.0) // false ``` IsLength [#islength] 检查字符串长度是否在指定范围内: ```go title="package/validate.go" // [!code highlight] func (v *Valid) IsLength(str string, minLen, maxLen int) bool ``` ```go // [!code highlight:3] xUtil.Valid().IsLength("hello", 1, 10) // true xUtil.Valid().IsLength("hi", 3, 10) // false xUtil.Valid().IsLength("hello world", 1, 5) // false ``` JSON 格式验证 [#json-格式验证] IsJSON [#isjson] 简单检查 JSON 格式: ```go title="package/validate.go" // [!code highlight] func (v *Valid) IsJSON(jsonStr string) bool ``` ```go // [!code highlight:4] xUtil.Valid().IsJSON(`{"name":"test"}`) // true xUtil.Valid().IsJSON(`[1, 2, 3]`) // true xUtil.Valid().IsJSON(`invalid`) // false xUtil.Valid().IsJSON(``) // false ``` 下一步 [#下一步] # 核心库 (/docs/bamboo-base-java/base) 核心库 [#核心库] `bamboo-base` 是筱工具(Java) 的基础层模块,为 `bamboo-mvc`、`bamboo-webflux`、`bamboo-triple` 等模块提供可复用的底层能力: * **响应模型**:`BaseResponse`、`ErrorCode` * **异常体系**:`BusinessException` 与扩展异常类型 * **分页模型**:`PageDTO` * **分布式 ID**:`SnowflakeIdGenerator`、`SnowflakeUtil`、`GeneSnowflakeUtil` * **UUID 能力**:`UuidUtil`、`UuidV7Generator` Maven 依赖 [#maven-依赖] ```xml title="pom.xml" com.x-lf.utility bamboo-base 2.0.0 ``` 包结构 [#包结构] | 包路径 | 说明 | | ----------------------------------- | ------------------------------------------------------------ | | `com.xlf.utility` | `BaseResponse`、`ErrorCode` | | `com.xlf.utility.exception.library` | 业务异常与公共异常 | | `com.xlf.utility.models.dto` | `PageDTO`、`SnowflakeInfoDTO`、`GeneSnowflakeInfoDTO` | | `com.xlf.utility.incrementer` | `SnowflakeIdGenerator`、`UuidV7Generator`、`OrdinaryGenerator` | | `com.xlf.utility.utility` | `SnowflakeUtil`、`GeneSnowflakeUtil`、`UuidUtil` 等工具类 | | `com.xlf.utility.constant` | 常量定义 | 下一步 [#下一步] # 自动配置 (/docs/bamboo-base-java/framework/auto-configuration) import { TypeTable } from '@/components/type-table'; 自动配置 [#自动配置] `bamboo-webflux` 通过 `WebFluxSdkAutoConfiguration` 自动注册核心组件。\ 仅在响应式应用环境(REACTIVE)下生效。 WebFluxSdkAutoConfiguration [#webfluxsdkautoconfiguration] ```java title="WebFluxSdkAutoConfiguration.java" @AutoConfiguration(before = ErrorWebFluxAutoConfiguration.class) @EnableAspectJAutoProxy @EnableConfigurationProperties(ContextWebFluxProperties.class) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) public class WebFluxSdkAutoConfiguration { // ... } ``` 自动注册组件 [#自动注册组件] | Bean | 说明 | | -------------------------- | ------------------------------ | | `contextFilter` | 上下文过滤器(注入 context 与 startTime) | | `logAspectHandler` | 日志切面 | | `debugAspectHandler` | 调试切面 | | `globalErrorController` | 全局错误处理器 | | `mybatisPlusConfigHandler` | MyBatis-Plus 配置(条件注册) | `mybatisPlusConfigHandler` 的注册条件: ```java title="WebFluxSdkAutoConfiguration.java" @Bean @ConditionalOnProperty( prefix = "utility.context.webflux", name = "gateway-mode", havingValue = "false", matchIfMissing = true ) @ConditionalOnMissingBean public MybatisPlusConfigHandler mybatisPlusConfigHandler(UtilityBaseProperties properties) { return new MybatisPlusConfigHandler(properties); } ``` ContextWebFluxProperties [#contextwebfluxproperties] `ContextWebFluxProperties` 绑定前缀 `utility.context.webflux`: ```yaml title="application.yml" utility: context: webflux: enable-input: true enable-output: true gateway-mode: false exclude-urls: - /actuator/** - /health ``` ', default: '[]', }, }} /> 下一步 [#下一步] # ContextHolder (/docs/bamboo-base-java/framework/context-holder) import { TypeTable } from '@/components/type-table'; ContextHolder [#contextholder] `ContextHolder` 是请求级别的上下文管理器,用于存储链路追踪标识(contextId)和请求开始时间(startTime)。根据使用的框架不同,有两种实现方式: | 框架 | 实现机制 | 包路径 | | ------------------ | ----------------- | ------------------------- | | **Spring MVC** | `ThreadLocal` | `com.xlf.utility.mvc` | | **Spring WebFlux** | `Reactor Context` | `com.xlf.utility.webflux` | 实现差异 [#实现差异] 为何 MVC 和 WebFlux 实现不同? [#为何-mvc-和-webflux-实现不同] 在 Spring MVC 的同步阻塞模型中,一个请求始终在同一个线程中处理,因此使用 `ThreadLocal` 可以安全地存储上下文。 在 Spring WebFlux 的响应式模型中,一个请求可能在多个线程之间切换执行。`ThreadLocal` 绑定于特定线程,在线程切换时上下文信息会丢失。Reactor Context 沿着响应式信号链传播,不依赖于执行线程。 ``` ThreadLocal 方式(MVC): Thread-1: [请求开始] → [业务处理] → [响应构建] → [请求结束] ↑ contextId 绑定在 Thread-1 上 Reactor Context 方式(WebFlux): Signal Chain: [subscribe] ← [operator] ← [operator] ← [source] ↑ contextId 沿信号链传播,与线程无关 ``` Spring MVC(ThreadLocal 实现) [#spring-mvcthreadlocal-实现] 内部结构 [#内部结构] ```java title="ContextHolder.java" // 私有记录类,封装上下文标识与起始时间 private record RequestContext(String contextId, long startTime) {} ``` 方法列表 [#方法列表] 使用示例(MVC) [#使用示例mvc] ```java title="ManualContextExample.java" // 初始化上下文(通常由 ContextFilter 自动完成) ContextHolder.initContext(); try { // 获取上下文标识 String contextId = ContextHolder.getContextId(); log.info("当前请求上下文: {}", contextId); // 执行业务逻辑... // 获取请求耗时 Long duration = ContextHolder.getDuration(); log.info("请求处理耗时: {}ms", duration); } finally { // 必须在 finally 块中清理 ContextHolder.clear(); } ``` Spring WebFlux(Reactor Context 实现) [#spring-webfluxreactor-context-实现] 响应式 API [#响应式-api] 以下方法返回 `Mono` 类型,必须在 Reactor 管道中被订阅: ```java title="ContextHolder.java" // 返回 Mono,从 Reactor Context 中读取 UUID public static Mono getContextId() // 返回 Mono,判断上下文是否已注入 public static Mono hasContext() // 返回 Mono,毫秒级时间戳 public static Mono getStartTime() // 返回 Mono,单位为毫秒 public static Mono getDuration() ``` 同步 API [#同步-api] 以下方法接受 `ContextView` 参数,适用于 `Mono.deferContextual` 内部: ```java title="ContextHolder.java" // 同步方式从 ContextView 中读取链路追踪标识 public static String getContextId(ContextView contextView) // 同步方式判断 ContextView 中是否存在上下文 public static boolean hasContext(ContextView contextView) // 同步方式计算请求耗时 public static Long getDuration(ContextView contextView) ``` 使用示例(WebFlux) [#使用示例webflux] **响应式 API:** ```java title="SomeService.java" public Mono doSomething() { return ContextHolder.getContextId() .doOnNext(contextId -> log.info("当前链路: {}", contextId)) .then(); } ``` **同步 API(在 deferContextual 中使用):** ```java title="CustomHandler.java" public Mono buildResponse() { // 在 deferContextual 中使用同步 API,避免嵌套 Mono return Mono.deferContextual(ctx -> { String contextId = ContextHolder.getContextId(ctx); Long duration = ContextHolder.getDuration(ctx); return Mono.just("Context: " + contextId + ", Duration: " + duration + "ms"); }); } ``` 生命周期 [#生命周期] MVC 生命周期 [#mvc-生命周期] ``` 请求进入 → ContextFilter → initContext() → 业务处理 → ResultUtil(读取上下文)→ clear() ``` WebFlux 生命周期 [#webflux-生命周期] ``` 请求进入 → ContextFilter → contextWrite() → 业务处理 → ResultUtil(读取上下文) ``` 注意事项 [#注意事项] MVC 注意事项 [#mvc-注意事项] * **线程安全**:基于 `ThreadLocal` 实现,每个线程拥有独立的上下文副本 * **资源清理**:必须在请求结束时调用 `clear()` 方法。`ContextFilter` 已在内部处理此逻辑 * **异步场景**:若业务代码使用 `@Async` 或线程池,上下文不会自动传播至新线程 WebFlux 注意事项 [#webflux-注意事项] * **不可在非响应式代码中使用 `Mono` API**:必须在 Reactor 管道中被订阅 * **上下文传播方向**:Reactor Context 从下游(订阅者)向上游传播 * **依赖 ContextFilter**:若未注册 `ContextFilter`,所有读取操作将返回默认值或空值 下一步 [#下一步] # GlobalErrorController (/docs/bamboo-base-java/framework/global-error) import { TypeTable } from '@/components/type-table'; GlobalErrorController [#globalerrorcontroller] `GlobalErrorController` 用于统一处理所有未被 `@ExceptionHandler` 捕获的异常和 HTTP 错误状态(如 404、500 等)。根据使用的框架不同,实现方式有所差异。 框架实现对比 [#框架实现对比] | 对比项 | MVC 版本 | WebFlux 版本 | | ----- | -------------------------- | ----------------------------- | | 实现接口 | `ErrorController` | `ErrorWebExceptionHandler` | | 处理方法 | 返回 `ResponseEntity` | 返回 `Mono` | | 请求对象 | `HttpServletRequest` | `ServerWebExchange` | | 响应写入 | 框架自动处理 | 手动写入 `DataBuffer` | | 自动配置类 | `BaseSdkAutoConfiguration` | `WebFluxSdkAutoConfiguration` | 工作原理 [#工作原理] MVC 版本 [#mvc-版本] ``` HTTP 请求 │ ▼ Spring DispatcherServlet │ ├── 正常处理 → Controller → 返回响应 │ └── 异常/错误(未匹配任何 @ExceptionHandler) │ ▼ 转发至 /error │ ▼ GlobalErrorController │ ▼ 构造 BaseResponse 响应 ``` WebFlux 版本 [#webflux-版本] ``` HTTP 请求 │ ▼ WebFilter Chain │ ├── 正常处理 → Handler → 返回 Mono │ └── 异常/错误(未匹配任何 @ExceptionHandler) │ ▼ GlobalErrorController.handle() │ ▼ 构造 BaseResponse 并写入响应 │ ▼ Mono 完成 ``` 错误码映射 [#错误码映射] `GlobalErrorController` 将 HTTP 状态码自动映射到对应的 `ErrorCode`: | HTTP 状态码 | ErrorCode | 说明 | | -------- | ----------------------- | ------------------ | | 400 | `BAD_REQUEST` | 错误请求 | | 401 | `UNAUTHORIZED` | 未授权 | | 403 | `FORBIDDEN` | 禁止访问 | | 404 | `PAGE_NOT_FOUND` | 页面未找到 | | 405 | `METHOD_NOT_ALLOWED` | 方法不允许 | | 406 | `NOT_ACCEPTABLE` | 不可接受 | | 408 | `TIMEOUT` | 请求超时 | | 429 | `TOO_MANY_REQUESTS` | 请求过多 | | 500 | `SERVER_INTERNAL_ERROR` | 服务器内部错误(仅 WebFlux) | | 502 | `GATEWAY_ERROR` | 网关错误 | | 503 | `SERVICE_UNAVAILABLE` | 服务不可用 | | 其他 | `SERVER_INTERNAL_ERROR` | 服务器内部错误(仅 MVC) | 响应格式 [#响应格式] 当发生错误时,`GlobalErrorController` 返回如下格式的响应: ```json title="response.json (404 示例)" { "context": "550e8400-e29b-41d4-a716-446655440000", "output": "PageNotFound", "code": 40401, "message": "页面未找到", "errorMessage": "No static resource found for /api/v1/unknown", "duration": 5, "data": { "path": "/api/v1/unknown", "status": 404 } } ``` ', required: false, }, }} /> 常见错误场景 [#常见错误场景] 404 Not Found [#404-not-found] 当访问不存在的路径时: ```json title="response.json" { "context": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "output": "PageNotFound", "code": 40401, "message": "页面未找到", "errorMessage": null, "duration": 2, "data": { "path": "/api/v1/users/999", "status": 404 } } ``` 405 Method Not Allowed [#405-method-not-allowed] 当使用错误的 HTTP 方法时: ```json title="response.json" { "context": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "output": "MethodNotAllowed", "code": 40501, "message": "方法不允许", "errorMessage": "Request method 'DELETE' not supported", "duration": 1, "data": { "path": "/api/v1/user/profile", "status": 405 } } ``` 500 Internal Server Error [#500-internal-server-error] 当发生未捕获的异常时: ```json title="response.json" { "context": "123e4567-e89b-12d3-a456-426614174000", "output": "ServerInternalError", "code": 50001, "message": "服务器内部错误", "errorMessage": "NullPointerException: ...", "duration": 150, "data": { "path": "/api/v1/data/process", "status": 500 } } ``` WebFlux 特有功能 [#webflux-特有功能] 处理器结构(仅 WebFlux) [#处理器结构仅-webflux] ```java title="GlobalErrorController.java" public class GlobalErrorController implements ErrorWebExceptionHandler { private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); @Override public @NotNull Mono handle(ServerWebExchange exchange, @NotNull Throwable ex) { // [!code highlight:2] // 确定状态码 HttpStatus status = this.determineHttpStatus(ex); String requestPath = exchange.getRequest().getPath().value(); // [!code highlight:2] // 映射状态码到 ErrorCode ErrorCode errorCode = this.mapStatusCodeToErrorCode(status.value()); // [!code highlight:5] // 构建错误详情 Map errorDetails = new HashMap<>(); errorDetails.put("path", requestPath); errorDetails.put("status", status.value()); // 使用 ResultUtil 构造响应并写入 return ResultUtil.error(errorCode, errorMessage, errorDetails) .flatMap(response -> this.writeResponse(exchange, response)); } } ``` 特殊异常处理(仅 WebFlux) [#特殊异常处理仅-webflux] ResponseStatusException [#responsestatusexception] WebFlux 中常见的 `ResponseStatusException` 会被正确处理: ```java title="示例" // 在 Handler 中抛出 throw new ResponseStatusException(HttpStatus.NOT_FOUND, "User not found"); // GlobalErrorController 会提取状态码 404,映射到 PAGE_NOT_FOUND ``` IllegalArgumentException [#illegalargumentexception] 对于 `IllegalArgumentException`,返回 400 状态码: ```java title="GlobalErrorController.java" private HttpStatus determineHttpStatus(Throwable ex) { // [!code highlight:3] if (ex instanceof ResponseStatusException rse) { return HttpStatus.valueOf(rse.getStatusCode().value()); } if (ex instanceof IllegalArgumentException) { return HttpStatus.BAD_REQUEST; } return HttpStatus.INTERNAL_SERVER_ERROR; } ``` 响应写入机制(仅 WebFlux) [#响应写入机制仅-webflux] WebFlux 版本需要手动将响应写入 `ServerHttpResponse`: ```java title="GlobalErrorController.java" private Mono writeResponse( ServerWebExchange exchange, ResponseEntity>> response ) { ServerHttpResponse serverResponse = exchange.getResponse(); // [!code highlight:2] // 设置状态码 serverResponse.setStatusCode(response.getStatusCode()); // [!code highlight:2] // 设置内容类型 serverResponse.getHeaders().setContentType(MediaType.APPLICATION_JSON); // [!code highlight:4] // 序列化响应体 try { String jsonBody = OBJECT_MAPPER.writeValueAsString(response.getBody()); DataBuffer buffer = serverResponse.bufferFactory() .wrap(jsonBody.getBytes(StandardCharsets.UTF_8)); return serverResponse.writeWith(Mono.just(buffer)); } catch (JsonProcessingException e) { // 序列化失败时的降级处理 String fallbackBody = "{\"output\":\"UnknownError\",\"code\":50999}"; DataBuffer buffer = serverResponse.bufferFactory() .wrap(fallbackBody.getBytes(StandardCharsets.UTF_8)); return serverResponse.writeWith(Mono.just(buffer)); } } ``` MVC 特有功能 [#mvc-特有功能] 自定义错误路径(仅 MVC) [#自定义错误路径仅-mvc] 默认错误路径为 `/error`,可通过配置修改: ```yaml title="application.yml" server: error: path: /custom-error ``` 生产环境配置(仅 MVC) [#生产环境配置仅-mvc] 生产环境建议关闭详细的错误信息输出: ```yaml title="application.yml" server: error: include-message: never ``` 与异常处理器的关系 [#与异常处理器的关系] `GlobalErrorController` 是异常处理的最后一道防线: ``` 异常抛出 │ ▼ @ExceptionHandler 匹配? │ ├── 是 → 对应的 ExceptionHandler 处理 │ └── 否 → Spring 转发至 /error(MVC)或传播至 ErrorWebExceptionHandler(WebFlux) │ ▼ GlobalErrorController 处理 ``` 注意事项 [#注意事项] * `GlobalErrorController` 由对应的自动配置类自动注册,无需手动声明 * 该控制器仅处理未被 `@ExceptionHandler` 捕获的错误 * 响应格式与 `ResultUtil.error()` 保持一致,确保前端处理逻辑统一 * WebFlux 版本在序列化失败时会返回降级响应,避免级联错误 下一步 [#下一步] # 框架集成 (/docs/bamboo-base-java/framework) 框架集成 [#框架集成] `bamboo-mvc` 与 `bamboo-webflux` 是竹简框架对 Spring 生态的两种集成方案,分别适用于传统的阻塞式架构和响应式架构。两个模块共享相同的设计理念,但在具体实现上针对各自的编程模型进行了优化。 选择指南 [#选择指南] | 场景 | 推荐模块 | 说明 | | ------------------ | ---------------- | ------------------------- | | 传统 Spring Boot 应用 | `bamboo-mvc` | 基于 Servlet API,同步阻塞模型 | | 响应式 Spring Boot 应用 | `bamboo-webflux` | 基于 Reactive Streams,非阻塞模型 | | 高并发、低延迟场景 | `bamboo-webflux` | 充分利用异步非阻塞优势 | | 快速开发、团队熟悉度优先 | `bamboo-mvc` | 更广泛的社区支持和资料 | 两个模块都依赖 `bamboo-base` 核心库,共享 `BaseResponse`、`ErrorCode`、`BusinessException` 等基础类型。详见 [核心库](/docs/bamboo-base-java/base)。 Maven 依赖 [#maven-依赖] bamboo-mvc [#bamboo-mvc] ```xml title="pom.xml" com.x-lf.utility bamboo-mvc 2.0.0 ``` bamboo-webflux [#bamboo-webflux] ```xml title="pom.xml" com.x-lf.utility bamboo-webflux 2.0.0 ``` 核心功能对比 [#核心功能对比] | 功能 | MVC 版本 | WebFlux 版本 | | ----- | ---------------------------------- | ---------------------------------------- | | 响应构建 | `ResultUtil` → `ResponseEntity` | `ResultUtil` → `Mono>` | | 上下文持有 | `ContextHolder` (ThreadLocal) | `ContextHolder` (Reactor Context) | | 过滤器 | `javax.servlet.Filter` | `WebFilter` | | 异常处理 | `@RestControllerAdvice` | `@RestControllerAdvice` | | 全局错误 | `ErrorController` | `ErrorWebExceptionHandler` | 模块结构 [#模块结构] ``` framework/ ├── result-util.mdx # 响应构建工具 ├── context-holder.mdx # 上下文持有器 ├── global-error.mdx # 全局错误处理 ├── filter/ # 过滤器 │ ├── cors.mdx # CORS 跨域 │ ├── options.mdx # OPTIONS 预检 │ ├── permission.mdx # 权限校验 │ └── context.mdx # 上下文注入 ├── aspect/ # 切面 │ ├── log.mdx # 日志切面 │ └── debug.mdx # 调试切面 └── util/ # 工具类 ├── http-servlet-util.mdx # HttpServletRequest 工具(仅 MVC) └── server-web-exchange-util.mdx # ServerWebExchange 工具(仅 WebFlux) ``` 异常处理 [#异常处理] 异常处理架构已归纳到核心库,MVC 和 WebFlux 共用相同的异常处理器继承链。详见 [异常处理架构](/docs/bamboo-base-java/base/exception)。 框架特有的异常处理器: * [Spring MVC 异常](/docs/bamboo-base-java/framework/exception/spring-boot) * [Spring WebFlux 异常](/docs/bamboo-base-java/framework/exception/webflux-boot) 快速开始 [#快速开始] MVC 项目 [#mvc-项目] ```java title="UserController.java" @RestController @RequestMapping("/api/v1/user") public class UserController { @GetMapping("/{id}") public ResponseEntity> getUser(@PathVariable Long id) { UserVO user = userService.getUserById(id); return ResultUtil.success("查询成功", user); } } ``` WebFlux 项目 [#webflux-项目] ```java title="UserController.java" @RestController @RequestMapping("/api/v1/user") public class UserController { @GetMapping("/{id}") public Mono>> getUser(@PathVariable Long id) { return userService.getUserById(id) .flatMap(user -> ResultUtil.success("查询成功", user)) .switchIfEmpty(ResultUtil.error(ErrorCode.RESOURCE_NOT_FOUND, "用户不存在", null)); } } ``` 下一步 [#下一步] # MyBatis-Plus 配置 (/docs/bamboo-base-java/framework/mybatis-plus) import { TypeTable } from '@/components/type-table'; import { Callout } from 'fumadocs-ui/components/callout'; MyBatis-Plus 配置 [#mybatis-plus-配置] `bamboo-mvc` 不直接定义分页配置类,而是在自动配置中注册\ `com.xlf.utility.app.config.MybatisPlusConfigHandler`(来自 `bamboo-base`)。 自动注册入口 [#自动注册入口] ```java title="BaseSdkAutoConfiguration.java" @Bean @ConditionalOnMissingBean @ConditionalOnClass(name = "com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor") public MybatisPlusConfigHandler mybatisPlusConfigHandler(UtilityBaseProperties properties) { return new MybatisPlusConfigHandler(properties); } ``` MybatisPlusConfigHandler 提供的能力 [#mybatisplusconfighandler-提供的能力] * 注册 `MybatisPlusInterceptor` + `PaginationInnerInterceptor` * 从 `UtilityBaseProperties` 读取数据库类型和分页上限 * 注册 `MybatisPlusPropertiesCustomizer`,默认使用 `OrdinaryGenerator` 配置项 [#配置项] ```yaml title="application.yml" bamboo: base: datasource: db-type: MYSQL page: max-limit: 500 default-limit: 20 ``` 使用示例 [#使用示例] ```java title="UserService.java" public IPage getUserList(int current, int size) { Page page = new Page<>(current, size); return userMapper.selectPage(page, null); } ``` 扩展配置 [#扩展配置] 业务项目可以继承 `MybatisPlusConfigHandler` 并实现 `MetaObjectHandler` 接口, 以扩展乐观锁、自定义 ID 生成器和元数据自动填充功能。 乐观锁插件 [#乐观锁插件] 通过重写 `mybatisPlusInterceptor()` 方法,在父类分页拦截器的基础上添加乐观锁插件: ```java title="MybatisPlusConfig.java" @Override public MybatisPlusInterceptor mybatisPlusInterceptor() { return Optional.of(super.mybatisPlusInterceptor()) .map(interceptor -> { // 添加乐观锁插件 interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return interceptor; }) .orElseThrow(() -> new ServerInternalErrorException("初始化 MybatisPlusInterceptor 失败")); } ``` 在实体类中使用 `@Version` 注解标识乐观锁字段: ```java title="UserEntity.java" @Data @TableName("t_user") public class UserEntity { @Version private Long version; // 其他字段... } ``` 1. 更新时 MyBatis-Plus 自动在 WHERE 条件中添加 `version = 旧值` 2. 同时 SET 中自动 `version = 旧值 + 1` 3. 如果受影响行数为 0,说明被其他事务修改过 自定义 ID 生成器 [#自定义-id-生成器] 默认使用 `OrdinaryGenerator` 生成 ID(生成6位随机整数),可以通过重写 `plusPropertiesCustomizer()` 方法替换为雪花算法: 默认使用 `OrdinaryGenerator`,生成6位随机整数。如需分布式唯一 ID,建议替换为 `SnowflakeIdGenerator`。 ```java title="MybatisPlusConfig.java" @Override public MybatisPlusPropertiesCustomizer plusPropertiesCustomizer() { return mybatisProperties -> mybatisProperties.getGlobalConfig() .setIdentifierGenerator(new SnowflakeIdGenerator( properties.getSnowflake().getDatacenterId(), properties.getSnowflake().getMachineId(), properties.getSnowflake().getEpoch() )); } ``` 雪花算法 ID 结构 [#雪花算法-id-结构] `SnowflakeIdGenerator` 生成64位长整型 ID,结构如下: ``` ┌─────────────┬──────────┬──────────┬─────────────┐ │ 时间戳位 │ 数据中心 │ 机器ID位 │ 序列号位 │ │ (41位) │ (5位) │ (5位) │ (12位) │ └─────────────┴──────────┴──────────┴─────────────┘ 63 22 21 17 16 12 11 0 ``` * **时间戳位**:41位,可支持约69年的时间范围 * **数据中心位**:5位,支持32个数据中心 * **机器ID位**:5位,每个数据中心支持32台机器 * **序列号位**:12位,每毫秒可生成4096个ID 元数据自动填充 [#元数据自动填充] 实现 `MetaObjectHandler` 接口,在插入和更新时自动填充公共字段: ```java title="MybatisPlusConfig.java" @Slf4j @Configuration public class MybatisPlusConfig extends MybatisPlusConfigHandler implements MetaObjectHandler { public MybatisPlusConfig(UtilityBaseProperties properties) { super(properties); } @Override public void insertFill(@NotNull MetaObject metaObject) { log.debug("自定义插入填充 {}", metaObject.getOriginalObject().getClass().getName()); this.strictInsertFill(metaObject, "createdAt", Timestamp.class, new Timestamp(System.currentTimeMillis())); this.strictInsertFill(metaObject, "updatedAt", Timestamp.class, new Timestamp(System.currentTimeMillis())); this.strictInsertFill(metaObject, "version", Long.class, 0L); this.strictInsertFill(metaObject, "delete", Boolean.class, false); } @Override public void updateFill(@NotNull MetaObject metaObject) { log.debug("自定义更新填充 {}", metaObject.getOriginalObject().getClass().getName()); this.strictUpdateFill(metaObject, "updatedAt", Timestamp.class, new Timestamp(System.currentTimeMillis())); this.strictUpdateFill(metaObject, "delete", Boolean.class, false); } } ``` 自动填充字段规则 [#自动填充字段规则] | 字段 | 插入时填充 | 更新时填充 | 类型 | 默认值 | | ----------- | :---: | :---: | --------- | ----- | | `createdAt` | ✅ | ❌ | Timestamp | 当前时间 | | `updatedAt` | ✅ | ✅ | Timestamp | 当前时间 | | `version` | ✅ | ❌ | Long | 0L | | `delete` | ✅ | ✅ | Boolean | false | 使用 `strictInsertFill` / `strictUpdateFill` 进行严格填充,仅在字段为 null 时才会填充值。 如果字段已有值,则不会覆盖。这样可以保护业务代码中手动设置的值。 实体类注解配置 [#实体类注解配置] 在实体类中使用 `@TableField` 注解指定字段的填充策略: ```java title="UserEntity.java" @Data @TableName("t_user") public class UserEntity { @TableId(type = IdType.ASSIGN_ID) private Long id; private String username; @TableField(fill = FieldFill.INSERT) private Timestamp createdAt; @TableField(fill = FieldFill.INSERT_UPDATE) private Timestamp updatedAt; @Version private Long version; @TableLogic @TableField(fill = FieldFill.INSERT_UPDATE) private Boolean delete; } ``` 完整配置示例 [#完整配置示例] 以下是一个完整的 MyBatis-Plus 扩展配置类,集成了乐观锁、雪花算法 ID 生成器和元数据自动填充: ```java title="MybatisPlusConfig.java" @Slf4j @Configuration public class MybatisPlusConfig extends MybatisPlusConfigHandler implements MetaObjectHandler { public MybatisPlusConfig(UtilityBaseProperties properties) { super(properties); } @Override public MybatisPlusInterceptor mybatisPlusInterceptor() { return Optional.of(super.mybatisPlusInterceptor()) .map(interceptor -> { interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return interceptor; }) .orElseThrow(() -> new ServerInternalErrorException("初始化 MybatisPlusInterceptor 失败")); } @Override public MybatisPlusPropertiesCustomizer plusPropertiesCustomizer() { return mybatisProperties -> mybatisProperties.getGlobalConfig() .setIdentifierGenerator(new SnowflakeIdGenerator( properties.getSnowflake().getDatacenterId(), properties.getSnowflake().getMachineId(), properties.getSnowflake().getEpoch() )); } @Override public void insertFill(@NotNull MetaObject metaObject) { log.debug("自定义插入填充 {}", metaObject.getOriginalObject().getClass().getName()); this.strictInsertFill(metaObject, "createdAt", Timestamp.class, new Timestamp(System.currentTimeMillis())); this.strictInsertFill(metaObject, "updatedAt", Timestamp.class, new Timestamp(System.currentTimeMillis())); this.strictInsertFill(metaObject, "version", Long.class, 0L); this.strictInsertFill(metaObject, "delete", Boolean.class, false); } @Override public void updateFill(@NotNull MetaObject metaObject) { log.debug("自定义更新填充 {}", metaObject.getOriginalObject().getClass().getName()); this.strictUpdateFill(metaObject, "updatedAt", Timestamp.class, new Timestamp(System.currentTimeMillis())); this.strictUpdateFill(metaObject, "delete", Boolean.class, false); } } ``` 注意事项 [#注意事项] * 未引入 MyBatis-Plus 时,该 Bean 不会注册。 * 分页能力来自 `bamboo-base`,`mvc` 与 `webflux` 共用同一套配置实现。 * 乐观锁插件仅支持 `Integer`、`Long`、`Date`、`Timestamp` 类型的版本字段。 * 自定义配置类需要继承 `MybatisPlusConfigHandler` 并添加 `@Configuration` 注解。 下一步 [#下一步] # ResultUtil (/docs/bamboo-base-java/framework/result-util) import { TypeTable } from '@/components/type-table'; ResultUtil [#resultutil] `ResultUtil` 是统一响应构建工具类,用于构建符合 `BaseResponse` 规范的响应。根据使用的框架不同,返回类型有所差异: | 框架 | 包路径 | 返回类型 | | ------------------ | ------------------------- | --------------------------------------- | | **Spring MVC** | `com.xlf.utility.mvc` | `ResponseEntity>` | | **Spring WebFlux** | `com.xlf.utility.webflux` | `Mono>>` | 方法签名 [#方法签名] 两种框架的方法签名完全相同,仅返回类型不同: ```java title="ResultUtil.java" public static XxxResponse success(String message) public static XxxResponse success(String message, T data) public static XxxResponse error(ErrorCode errorCode, String errorMessage, T data) ``` ::: tip 返回类型说明 * **MVC**: `XxxResponse` = `ResponseEntity>` * **WebFlux**: `XxxResponse` = `Mono>>` ::: 行为说明 [#行为说明] ', required: true, }, 'error(errorCode, errorMessage, data)': { description: '返回错误响应,HTTP 状态码按 errorCode.getCode()/100 计算。', type: 'XxxResponse', required: true, }, }} /> 响应示例 [#响应示例] 成功响应 [#成功响应] ```json title="response.json" { "context": "550e8400-e29b-41d4-a716-446655440000", "output": "Success", "code": 200, "message": "操作成功", "duration": 15, "data": null } ``` 错误响应 [#错误响应] ```json title="response.json" { "context": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "output": "Unauthorized", "code": 40101, "message": "未授权", "errorMessage": "令牌已过期", "duration": 5 } ``` 使用示例 [#使用示例] Spring MVC(同步) [#spring-mvc同步] ```java title="UserController.java" @GetMapping("/{id}") public ResponseEntity> getUser(@PathVariable Long id) { UserVO user = userService.getUserById(id); return ResultUtil.success("查询成功", user); } ``` Spring WebFlux(响应式) [#spring-webflux响应式] ```java title="UserController.java" @GetMapping("/{id}") public Mono>> getUser(@PathVariable Long id) { return userService.getUserById(id) .flatMap(user -> ResultUtil.success("获取成功", user)) .switchIfEmpty(ResultUtil.error(ErrorCode.RESOURCE_NOT_FOUND, "用户不存在", null)); } ``` 注意事项 [#注意事项] * `success` 系列方法中的 `code` 使用 `200`,不是 `ErrorCode` 中的业务码。 * `error` 推荐在全局异常处理器中调用,业务层优先抛异常而不是直接返回错误响应。 * 若请求未经过 `ContextFilter`,`context` 与 `duration` 可能为 `null`。 下一步 [#下一步] # 系统初始化 (/docs/bamboo-base-java/framework/system-init) import { TypeTable } from '@/components/type-table'; import { Callout } from 'fumadocs-ui/components/callout'; 系统初始化 [#系统初始化] `bamboo-base` 提供 `InitSystemStartupHandler` 抽象类,用于在 Spring Boot 启动时执行系统初始化逻辑。 支持数据库表结构检查、初始数据填充、数据库迁移等功能,并确保初始化过程的幂等性。 初始化流程 [#初始化流程] ``` 1. Spring Boot 启动 ↓ 2. ApplicationContextAware.setApplicationContext() - SnowflakeUtilInitializer 执行(雪花算法初始化) ↓ 3. @PostConstruct 方法执行 - 子类的 init() 方法执行 - 数据库表结构检查 - 初始数据填充 ↓ 4. CommandLineRunner.run() 执行 - initFinal() 打印启动完成 Banner ``` InitSystemStartupHandler [#initsystemstartuphandler] `InitSystemStartupHandler` 是系统初始化的入口抽象类,定义了初始化的生命周期方法: ```java title="InitSystemStartupHandler.java" public abstract class InitSystemStartupHandler { /** * 抽象初始化方法(子类必须实现) */ public abstract void init(); /** * 初始化结束标志 */ @Bean public CommandLineRunner initFinal() { return args -> { log.info("=========== End of Initialization ==========="); // 打印 Banner }; } /** * 数据库准备(子类可覆盖) */ public void prepareDatabase() { log.debug("准备数据库「当前无数据库需要检查」"); } /** * 数据库迁移 */ public void databaseMigrate(String schema, @NotNull InitPrepareAlgorithmHandler prepareAlgorithmHandler) { // 扫描 migrate/*.sql 文件并执行 } } ``` 最佳实践 [#最佳实践] 主入口类 [#主入口类] 创建 `SystemInit` 类继承 `InitSystemStartupHandler`,作为系统初始化的主入口: ```java title="SystemInit.java" @Slf4j @Configuration @RequiredArgsConstructor public class SystemInit extends InitSystemStartupHandler { // 依赖注入(框架提供) private final JdbcTemplate jdbcTemplate; private final MigrateHandlerDAO migrateHandlerDAO; private final TransactionTemplate transactionTemplate; private final SqlDialectStrategyFactory strategyFactory; private final UtilityBaseProperties properties; // 依赖注入(业务 DAO) private final PermissionDAO permissionDAO; private final RoleDAO roleDAO; private final SystemDAO systemDAO; private final UserDAO userDAO; private final UserRoleDAO userRoleDAO; private InitAlgorithm prepare; @Override @PostConstruct public void init() { log.info("系统开始进行初始化"); // 1. 创建算法处理器实例 prepare = new InitAlgorithm(jdbcTemplate, migrateHandlerDAO, transactionTemplate, strategyFactory, properties, permissionDAO, roleDAO, systemDAO); // 2. 检查数据库表结构 this.prepareDatabase(); // 3. 按顺序初始化数据(注意依赖顺序) new SystemPrepare(prepare); // 系统配置 new PermissionPrepare(prepare); // 权限数据 new RolePrepare(prepare); // 角色数据 new UserPrepare(prepare, userDAO, systemDAO, userRoleDAO); // 超级管理员 // 4. 执行数据库迁移 this.databaseMigrate("your_schema", prepare); } @Override public CommandLineRunner initFinal() { return args -> { log.info("=========== End of Initialization ==========="); // 打印 ASCII Art Banner }; } @Override public void prepareDatabase() { // 按依赖关系分层创建表:基础表 → 一级依赖表 → 二级依赖表 prepare.checkTable("your_schema", "t_user"); prepare.checkTable("your_schema", "t_system"); prepare.checkTable("your_schema", "t_permission"); prepare.checkTable("your_schema", "t_role"); prepare.checkTable("your_schema", "t_user_role"); } } ``` InitAlgorithm 算法处理器 [#initalgorithm-算法处理器] `InitAlgorithm` 继承 `InitPrepareAlgorithmHandler`,封装数据库操作和幂等性检查逻辑: ```java title="InitAlgorithm.java" public class InitAlgorithm extends InitPrepareAlgorithmHandler { private final PermissionDAO permissionDAO; private final RoleDAO roleDAO; private final SystemDAO systemDAO; public InitAlgorithm(JdbcTemplate jdbcTemplate, MigrateHandlerDAO migrateHandlerDAO, TransactionTemplate transactionTemplate, SqlDialectStrategyFactory strategyFactory, UtilityBaseProperties properties, PermissionDAO permissionDAO, RoleDAO roleDAO, SystemDAO systemDAO) { super(jdbcTemplate, migrateHandlerDAO, transactionTemplate, strategyFactory, properties); this.permissionDAO = permissionDAO; this.roleDAO = roleDAO; this.systemDAO = systemDAO; } /** * 幂等性设计:存在则返回已有ID,不存在则创建 */ public String prepareDatabaseForPermission(@NotNull PermissionEntity entity) { return permissionDAO.lambdaQuery() .eq(PermissionEntity::getCode, entity.getCode()) .last("limit 1") .oneOpt() .map(e -> String.valueOf(e.getId())) .orElseGet(() -> { permissionDAO.save(entity); return String.valueOf(entity.getId()); }); } public String prepareDatabaseForRole(@NotNull RoleEntity entity) { return roleDAO.lambdaQuery() .eq(RoleEntity::getCode, entity.getCode()) .last("limit 1") .oneOpt() .map(e -> String.valueOf(e.getId())) .orElseGet(() -> { roleDAO.save(entity); return String.valueOf(entity.getId()); }); } } ``` Prepare 类模式 [#prepare-类模式] 使用「构造函数即执行」模式,每个 Prepare 类负责一类数据的初始化: ```java title="PermissionPrepare.java" @Slf4j public class PermissionPrepare { public PermissionPrepare(@NotNull InitAlgorithm prepare) { log.debug("开始初始化权限数据"); // 一级权限(顶级) String systemPermissionId = prepare.prepareDatabaseForPermission( PermissionEntity.builder() .code("system") .name("系统管理") .description("系统级管理权限") .build() ); // 二级权限(带父级ID) prepare.prepareDatabaseForPermission( PermissionEntity.builder() .parentId(systemPermissionId) .code("system:user") .name("用户管理") .description("用户管理权限") .build() ); prepare.prepareDatabaseForPermission( PermissionEntity.builder() .parentId(systemPermissionId) .code("system:role") .name("角色管理") .description("角色管理权限") .build() ); } } ``` ```java title="UserPrepare.java" @Slf4j public class UserPrepare { public UserPrepare(@NotNull InitAlgorithm prepare, UserDAO userDAO, SystemDAO systemDAO, UserRoleDAO userRoleDAO) { log.debug("开始初始化用户数据"); // 检查超级管理员是否存在 UserEntity admin = userDAO.lambdaQuery() .eq(UserEntity::getUsername, "admin") .one(); if (admin == null) { // 创建超级管理员 admin = UserEntity.builder() .username("admin") .password("encrypted_password") .build(); userDAO.save(admin); } // 关联默认角色 // ... } } ``` 数据库表分层创建 [#数据库表分层创建] 按照外键依赖关系分层创建表,确保依赖表先创建: ```java title="SystemInit.java" @Override public void prepareDatabase() { // 第一层:无外键依赖的基础表 prepare.checkTable("your_schema", "t_system"); // 第二层:依赖基础表的表 prepare.checkTable("your_schema", "t_user"); prepare.checkTable("your_schema", "t_permission"); prepare.checkTable("your_schema", "t_role"); // 第三层:多对多关联表(依赖第二层) prepare.checkTable("your_schema", "t_user_role"); prepare.checkTable("your_schema", "t_role_permission"); } ``` 数据库自动迁移 [#数据库自动迁移] 系统初始化支持两类数据库操作:表结构定义(`database/`)和数据迁移(`migrate/`)。 目录结构 [#目录结构] ``` src/main/resources/ ├── database/ # 表结构定义 │ ├── t_user.sql # 用户表 │ ├── t_role.sql # 角色表 │ └── t_order.sql # 订单表 │ └── migrate/ # 数据库迁移脚本 ├── 2025_01_15_10_00_add_user_status.sql └── 2025_02_20_14_30_modify_order_type.sql ``` 表结构定义(database 目录) [#表结构定义database-目录] `database/` 目录存放建表 SQL 文件,按依赖关系分层创建表。 **文件命名规范**:`t_{表名}.sql` ```sql title="database/t_user.sql" -- ==================== -- 表名:用户表 -- 时间:2025-01-15 -- 说明:存储系统用户信息 -- ==================== CREATE TABLE IF NOT EXISTS `t_user` ( `id` BIGINT UNSIGNED NOT NULL PRIMARY KEY COMMENT '用户ID', `nickname` VARCHAR(64) NOT NULL COMMENT '用户昵称', `role_id` BIGINT UNSIGNED NOT NULL COMMENT '角色ID', `status` BOOLEAN NOT NULL DEFAULT TRUE COMMENT '用户状态', `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `version` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', `delete` BOOLEAN NOT NULL DEFAULT FALSE COMMENT '删除标志' ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT ='用户表'; -- 索引 ALTER TABLE `t_user` ADD INDEX `idx_user_role_id` (`role_id`); ``` **在代码中调用**: ```java title="SystemInit.java" @Override public void prepareDatabase() { // 按依赖关系分层创建表 // 基础表(无外部外键依赖) prepare.checkTable("your_schema", "t_user"); prepare.checkTable("your_schema", "t_role"); prepare.checkTable("your_schema", "t_system"); // 一级依赖表(依赖基础表) prepare.checkTable("your_schema", "t_order"); prepare.checkTable("your_schema", "t_permission"); } ``` **checkTable 工作原理**: 1. 检查表是否存在(查询 `information_schema.TABLES`) 2. 不存在时从 `classpath:/database/` 读取对应的 SQL 文件 3. 在事务中执行 SQL 创建表 数据迁移(migrate 目录) [#数据迁移migrate-目录] `migrate/` 目录存放增量迁移脚本,用于修改现有表结构或数据。 **文件命名规范**:`YYYY_MM_DD_HH_mm_{描述}.sql` 示例:`2025_01_15_10_30_add_user_status.sql` ```sql title="migrate/2025_01_15_10_30_add_user_status.sql" -- ============================================================================ -- 迁移脚本:为 t_user 表添加 status 字段 -- 日期:2025-01-15 10:30 -- ============================================================================ -- 添加新字段 ALTER TABLE `t_user` ADD COLUMN `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态: 1-正常 2-禁用' AFTER `role_id`; -- 更新现有数据 UPDATE `t_user` SET `status` = 1 WHERE `status` IS NULL; ``` **在代码中调用**: ```java title="SystemInit.java" @Override @PostConstruct public void init() { // ... 其他初始化 ... // 执行数据库迁移 this.databaseMigrate("your_schema", prepare); } ``` **databaseMigrate 工作原理**: 1. 扫描 `classpath*:migrate/*.sql` 路径 2. 按文件名自然排序(确保执行顺序) 3. 检查迁移记录表,跳过已执行的脚本 4. 逐条执行 SQL 语句,支持断点续传 5. 记录执行状态(SUCCESS/PARTIAL/FAILED) 迁移记录表 [#迁移记录表] 系统自动创建迁移记录表,用于追踪执行状态: ```sql CREATE TABLE IF NOT EXISTS `bamboo_migrate` ( migrate_id BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, migrate_name VARCHAR(255) NOT NULL UNIQUE COMMENT '迁移文件名', migrate_hash VARCHAR(64) NOT NULL COMMENT '文件 SHA-256 哈希', migrate_status VARCHAR(20) NOT NULL DEFAULT 'SUCCESS' COMMENT '状态', error_message TEXT DEFAULT NULL COMMENT '错误信息', last_executed_line INT DEFAULT NULL COMMENT '最后执行的行号', total_lines INT DEFAULT NULL COMMENT '总行数', applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '执行时间' ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT ='迁移记录表'; ``` 迁移状态 [#迁移状态] | 状态 | 说明 | | --------- | --------------- | | `SUCCESS` | 迁移成功完成 | | `PARTIAL` | 部分执行,下次启动会从断点继续 | | `FAILED` | 执行失败,系统会退出 | SQL 方言支持 [#sql-方言支持] 支持多种数据库的 SQL 方言: | 数据库 | 策略类 | | --------------- | --------------------------- | | MySQL / MariaDB | `MySqlDialectStrategy` | | PostgreSQL | `PostgreSqlDialectStrategy` | 根据 `bamboo.base.datasource.db-type` 配置自动选择。 * 迁移文件一旦执行成功,如果文件内容被修改(哈希值变化),系统会拒绝启动 * 迁移失败时系统会退出(`System.exit(1)`),防止数据不一致 * 迁移脚本使用时间戳前缀确保执行顺序 * 每个迁移脚本只做一件事,便于回滚 * 使用事务确保原子性 幂等性设计 [#幂等性设计] 所有初始化方法都应该设计为幂等的,即多次执行不会产生副作用: ```java /** * 幂等性设计模式 * 1. 先查询是否存在 * 2. 存在则返回已有数据 * 3. 不存在则创建新数据 */ public String prepareDatabaseForData(@NotNull SomeEntity entity) { return someDAO.lambdaQuery() .eq(SomeEntity::getUniqueKey, entity.getUniqueKey()) .last("limit 1") .oneOpt() .map(e -> String.valueOf(e.getId())) // 存在:返回已有ID .orElseGet(() -> { someDAO.save(entity); // 不存在:创建新记录 return String.valueOf(entity.getId()); }); } ``` * 使用唯一键(如 `code`)作为查询条件 * 使用 `Optional` 链式调用优雅处理存在/不存在两种情况 * 返回 ID 供后续创建关联数据使用 注意事项 [#注意事项] * `init()` 方法必须添加 `@PostConstruct` 注解,确保在 Spring Bean 初始化后执行 * 初始化顺序很重要,按数据依赖关系排列 Prepare 类 * 数据库表检查按外键依赖分层,基础表先创建 * 所有数据初始化方法应设计为幂等的 * 使用 `@Slf4j` 记录初始化日志,便于调试 * 复杂初始化逻辑拆分到独立的 Prepare 类中 下一步 [#下一步] # 邮件通知 (/docs/bamboo-base-java/notify/email) 邮件通知 [#邮件通知] `EmailNotify` 是 `NotifyService` 接口的邮件通知实现,基于 SMTP 协议完成邮件发送。该实现封装了 SMTP 连接管理与邮件构建逻辑,业务层仅需通过 `NotifyMessage` 传入必要参数即可完成邮件投递。 类定义 [#类定义] ```java title="EmailNotify.java" package com.xlf.utility.notify; // [!code highlight:2] // 邮件通知实现,需配合 SMTP 配置使用 public class EmailNotify implements NotifyService { @Override public NotifyResult send(NotifyMessage message) { ... } @Override public void sendAsync(NotifyMessage message) { ... } } ``` SMTP 配置 [#smtp-配置] 使用邮件通知前,需在 `application.yml` 中配置 SMTP 服务参数: ```yaml title="application.yml" spring: mail: host: smtp.example.com port: 465 username: noreply@example.com password: your-smtp-password properties: mail: smtp: auth: true ssl: enable: true starttls: enable: true ``` > 生产环境中,建议将 SMTP 密码存储于环境变量或密钥管理服务中,避免明文写入配置文件。 使用示例 [#使用示例] 发送简单邮件 [#发送简单邮件] ```java title="AlertService.java" import com.xlf.utility.notify.EmailNotify; import com.xlf.utility.notify.NotifyMessage; import com.xlf.utility.notify.NotifyResult; import com.xlf.utility.notify.NotifyType; @Service public class AlertService { private final NotifyService emailNotify; public AlertService(EmailNotify emailNotify) { this.emailNotify = emailNotify; } public void sendAlert(String recipient, String subject, String content) { // [!code highlight:6] // 构建邮件消息并发送 NotifyMessage message = NotifyMessage.builder() .to(recipient) .subject(subject) .content(content) .type(NotifyType.EMAIL) .build(); NotifyResult result = emailNotify.send(message); if (!result.isSuccess()) { log.error("邮件发送失败: {}", result.getMessage()); } } } ``` 使用模板变量 [#使用模板变量] ```java title="WelcomeService.java" import java.util.Map; public void sendWelcomeEmail(String email, String username) { // [!code highlight:7] // 通过 variables 传递模板变量 NotifyMessage message = NotifyMessage.builder() .to(email) .subject("欢迎注册") .content("welcome-template") .type(NotifyType.EMAIL) .variables(Map.of("username", username, "year", 2026)) .build(); emailNotify.sendAsync(message); } ``` 设置优先级 [#设置优先级] ```java title="EmailPriorityExample.java" // [!code highlight:3] // priority 值越小优先级越高,默认为 3 NotifyMessage urgentMessage = NotifyMessage.builder() .to("ops@example.com") .subject("紧急:数据库连接异常") .content("主库连接池耗尽,请立即排查。") .type(NotifyType.EMAIL) .priority(1) .build(); ``` 错误处理 [#错误处理] 当邮件发送失败时,`NotifyResult` 将携带错误信息与错误码: ```java title="ErrorHandlingExample.java" NotifyResult result = emailNotify.send(message); if (!result.isSuccess()) { // [!code highlight:3] // 获取错误码与详细信息 String errorCode = result.getErrorCode(); String errorMessage = result.getMessage(); log.error("邮件发送失败 [{}]: {}", errorCode, errorMessage); } ``` 下一步 [#下一步] # 通知服务 (/docs/bamboo-base-java/notify) 通知服务 [#通知服务] **bamboo-notify** 是筱工具(Java) 的通知层模块,提供统一的多渠道消息通知能力。通过 `NotifyService` 接口抽象,实现邮件、Webhook 等通知渠道的标准化接入。 核心架构 [#核心架构] ``` NotifyService (统一通知接口) ├── EmailNotify (邮件通知实现) └── WebhookNotify (Webhook 通知实现) TemplateEngine (模板引擎接口) └── 自定义实现 NotifyMessage ──→ NotifyService ──→ NotifyResult (消息构建) (发送处理) (结果反馈) ``` 所有通知渠道均实现 `NotifyService` 接口,业务层通过统一的 `send()` 或 `sendAsync()` 方法发送通知,无需关注底层渠道差异。 Maven 依赖 [#maven-依赖] ```xml title="pom.xml" com.x-lf.utility bamboo-notify 2.0.0 ``` 通知类型 [#通知类型] `NotifyType` 枚举定义了当前支持的通知渠道: | 枚举值 | 说明 | | --------- | ----------------------- | | `EMAIL` | 邮件通知,基于 SMTP 协议发送 | | `WEBHOOK` | Webhook 通知,通过 HTTP 回调推送 | | `SMS` | 短信通知,对接第三方短信服务 | 基本用法 [#基本用法] ```java title="NotifyExample.java" import com.xlf.utility.notify.NotifyMessage; import com.xlf.utility.notify.NotifyResult; import com.xlf.utility.notify.NotifyService; import com.xlf.utility.notify.NotifyType; // [!code highlight:7] // 构建通知消息 NotifyMessage message = NotifyMessage.builder() .to("admin@example.com") .subject("系统告警") .content("服务器 CPU 使用率超过 90%") .type(NotifyType.EMAIL) .priority(1) .build(); // 同步发送 NotifyResult result = notifyService.send(message); // 异步发送(无返回值) notifyService.sendAsync(message); ``` 模块结构 [#模块结构] | 类 / 接口 | 说明 | | ---------------- | ------------------------------------- | | `NotifyService` | 通知服务接口,定义 `send()` 与 `sendAsync()` 方法 | | `NotifyMessage` | 通知消息模型,采用 Builder 模式构建 | | `NotifyResult` | 通知结果模型,包含成功状态与错误信息 | | `NotifyType` | 通知类型枚举 | | `TemplateEngine` | 模板引擎接口,支持变量替换渲染 | | `EmailNotify` | 邮件通知实现 | | `WebhookNotify` | Webhook 通知实现 | 下一步 [#下一步] # 数据模型 (/docs/bamboo-base-java/notify/model) import { TypeTable } from '@/components/type-table'; 数据模型 [#数据模型] bamboo-notify 模块通过 `NotifyMessage` 和 `NotifyResult` 两个数据模型,分别定义通知请求的输入参数与发送结果的输出结构。两者均采用 Lombok `@Builder` 模式,支持链式构建。 NotifyMessage [#notifymessage] `NotifyMessage` 是通知消息的载体,封装了接收方、内容、类型等全部发送参数。 类定义 [#类定义] ```java title="NotifyMessage.java" package com.xlf.utility.notify; import lombok.Builder; // [!code highlight:2] // 通知消息模型,使用 Builder 模式构建 @Builder public class NotifyMessage { private String to; private String subject; private String content; private NotifyType type; private Map variables; private int priority; } ``` 字段说明 [#字段说明] ', required: false, }, priority: { description: '消息优先级。值越小优先级越高,默认值为 3。可用于通知队列的优先排序。', type: 'int', default: '3', }, }} /> 构建示例 [#构建示例] ```java title="NotifyMessageExample.java" // [!code highlight:8] // 完整参数构建 NotifyMessage message = NotifyMessage.builder() .to("admin@example.com") .subject("系统告警") .content("服务 ${service} 在 ${time} 出现异常:${error}") .type(NotifyType.EMAIL) .variables(Map.of( "service", "order-service", "time", "2026-03-02 14:30:00", "error", "数据库连接超时" )) .priority(1) .build(); ``` ```java title="NotifyMessageSimple.java" // 最简构建(仅必填字段) NotifyMessage message = NotifyMessage.builder() .to("https://hooks.example.com/notify") .subject("部署完成") .content("{\"status\":\"success\"}") .type(NotifyType.WEBHOOK) .build(); ``` NotifyResult [#notifyresult] `NotifyResult` 是通知发送结果的封装,包含发送状态、结果消息与时间戳等信息。提供静态工厂方法简化实例创建。 类定义 [#类定义-1] ```java title="NotifyResult.java" package com.xlf.utility.notify; import lombok.Builder; import java.time.LocalDateTime; // [!code highlight:2] // 通知结果模型,提供静态工厂方法 @Builder public class NotifyResult { private boolean success; private String message; private LocalDateTime timestamp; private String errorCode; public static NotifyResult success(String message) { ... } public static NotifyResult failure(String message) { ... } public static NotifyResult failure(String message, String errorCode) { ... } } ``` 字段说明 [#字段说明-1] 静态工厂方法 [#静态工厂方法] | 方法签名 | 说明 | | ------------------------------------------- | ------------------------------- | | `success(String message)` | 创建成功结果,`success` 为 `true` | | `failure(String message)` | 创建失败结果,`success` 为 `false`,无错误码 | | `failure(String message, String errorCode)` | 创建失败结果,携带错误码 | 使用示例 [#使用示例] ```java title="NotifyResultExample.java" // 成功结果 NotifyResult success = NotifyResult.success("邮件发送成功"); // [!code highlight:3] // 失败结果(携带错误码) NotifyResult failure = NotifyResult.failure( "SMTP 连接超时", "SMTP_TIMEOUT"); // 判断发送结果 if (result.isSuccess()) { log.info("通知发送成功: {}", result.getMessage()); } else { log.error("通知发送失败 [{}]: {}", result.getErrorCode(), result.getMessage()); } ``` 下一步 [#下一步] # 模板引擎 (/docs/bamboo-base-java/notify/template) 模板引擎 [#模板引擎] `TemplateEngine` 是 bamboo-notify 模块的模板渲染接口,用于将通知内容中的占位变量替换为实际值。通过实现该接口,可对接 Thymeleaf、FreeMarker 等模板框架,或自行编写轻量级的变量替换逻辑。 接口定义 [#接口定义] ```java title="TemplateEngine.java" package com.xlf.utility.notify; public interface TemplateEngine { // [!code highlight:3] /** * 渲染模板,将变量替换为实际值 */ String render(String templateName, Map variables); } ``` 参数说明 [#参数说明] | 参数 | 类型 | 说明 | | -------------- | --------------------- | ----------- | | `templateName` | `String` | 模板名称或模板内容标识 | | `variables` | `Map` | 模板变量键值对 | 返回值 [#返回值] 返回渲染完成后的完整字符串内容,可直接作为 `NotifyMessage` 的 `content` 使用。 自定义实现 [#自定义实现] 简单变量替换 [#简单变量替换] 以下示例演示了一个基于占位符的轻量级模板引擎实现: ```java title="SimpleTemplateEngine.java" import com.xlf.utility.notify.TemplateEngine; import java.util.Map; @Component public class SimpleTemplateEngine implements TemplateEngine { // [!code highlight:9] // 基于 ${key} 占位符的简单替换实现 @Override public String render(String templateName, Map variables) { String template = this.loadTemplate(templateName); for (Map.Entry entry : variables.entrySet()) { template = template.replace( "${" + entry.getKey() + "}", String.valueOf(entry.getValue()) ); } return template; } private String loadTemplate(String templateName) { // 从文件系统、数据库或资源目录加载模板内容 return "尊敬的 ${username},您的验证码为 ${code},有效期 ${expiry} 分钟。"; } } ``` 对接 Thymeleaf [#对接-thymeleaf] ```java title="ThymeleafTemplateEngine.java" import com.xlf.utility.notify.TemplateEngine; import org.thymeleaf.context.Context; import org.thymeleaf.spring6.SpringTemplateEngine; import java.util.Map; @Component public class ThymeleafTemplateEngine implements TemplateEngine { private final SpringTemplateEngine springTemplateEngine; public ThymeleafTemplateEngine(SpringTemplateEngine springTemplateEngine) { this.springTemplateEngine = springTemplateEngine; } // [!code highlight:6] // 委托 Thymeleaf 引擎处理模板渲染 @Override public String render(String templateName, Map variables) { Context context = new Context(); context.setVariables(variables); return springTemplateEngine.process(templateName, context); } } ``` 配合 NotifyMessage 使用 [#配合-notifymessage-使用] `NotifyMessage` 的 `variables` 字段可直接传递模板变量,在通知服务内部调用 `TemplateEngine.render()` 完成内容渲染: ```java title="TemplateNotifyExample.java" import java.util.Map; // [!code highlight:7] // 构建携带模板变量的通知消息 NotifyMessage message = NotifyMessage.builder() .to("user@example.com") .subject("验证码通知") .content("verification-code") .type(NotifyType.EMAIL) .variables(Map.of( "username", "筱锋", "code", "839201", "expiry", 5 )) .build(); notifyService.send(message); ``` 在上述示例中,`content` 字段值 `"verification-code"` 将作为 `templateName` 传入 `TemplateEngine.render()` 方法,配合 `variables` 完成变量替换后生成最终通知内容。 设计建议 [#设计建议] | 建议 | 说明 | | ------- | ----------------------------------------- | | 模板与代码分离 | 将模板文件存放于 `resources/templates/` 目录下,避免硬编码 | | 缓存机制 | 对频繁使用的模板启用缓存,减少 I/O 开销 | | 异常处理 | 模板渲染失败时应抛出明确异常,避免发送空内容通知 | | 变量校验 | 渲染前校验必要变量是否齐全,防止占位符残留 | 下一步 [#下一步] # Webhook 通知 (/docs/bamboo-base-java/notify/webhook) Webhook 通知 [#webhook-通知] `WebhookNotify` 是 `NotifyService` 接口的 Webhook 通知实现,通过向指定 URL 发起 HTTP 回调完成消息推送。适用于对接企业通讯工具、CI/CD 流水线通知、第三方告警平台等场景。 类定义 [#类定义] ```java title="WebhookNotify.java" package com.xlf.utility.notify; // [!code highlight:2] // Webhook 通知实现,向目标 URL 发送 HTTP POST 请求 public class WebhookNotify implements NotifyService { @Override public NotifyResult send(NotifyMessage message) { ... } @Override public void sendAsync(NotifyMessage message) { ... } } ``` 使用方式 [#使用方式] Webhook 通知中,`to` 字段用于指定回调 URL,`content` 字段为 HTTP 请求体内容。 基本发送 [#基本发送] ```java title="WebhookExample.java" import com.xlf.utility.notify.WebhookNotify; import com.xlf.utility.notify.NotifyMessage; import com.xlf.utility.notify.NotifyResult; import com.xlf.utility.notify.NotifyType; @Service public class DeployNotifyService { private final NotifyService webhookNotify; public DeployNotifyService(WebhookNotify webhookNotify) { this.webhookNotify = webhookNotify; } public void notifyDeploySuccess(String service, String version) { // [!code highlight:7] // to 字段填写 Webhook 回调 URL NotifyMessage message = NotifyMessage.builder() .to("https://hooks.example.com/services/deploy") .subject("部署通知") .content("{\"service\":\"" + service + "\",\"version\":\"" + version + "\"}") .type(NotifyType.WEBHOOK) .priority(2) .build(); NotifyResult result = webhookNotify.send(message); if (!result.isSuccess()) { log.warn("Webhook 推送失败: {}", result.getMessage()); } } } ``` 携带模板变量 [#携带模板变量] ```java title="WebhookTemplateExample.java" import java.util.Map; // [!code highlight:7] // 使用 variables 传递结构化数据 NotifyMessage message = NotifyMessage.builder() .to("https://hooks.example.com/services/alert") .subject("告警通知") .content("alert-template") .type(NotifyType.WEBHOOK) .variables(Map.of( "level", "WARNING", "service", "user-service", "message", "响应时间超过阈值" )) .build(); webhookNotify.sendAsync(message); ``` 异步推送 [#异步推送] 对于非关键性通知,推荐使用异步发送避免阻塞主线程: ```java title="AsyncWebhookExample.java" // [!code highlight:2] // 异步发送,不阻塞当前线程 webhookNotify.sendAsync(message); ``` 适用场景 [#适用场景] | 场景 | 说明 | | --------- | ---------------------------------- | | 企业通讯集成 | 对接钉钉、飞书、企业微信等 Incoming Webhook | | CI/CD 流水线 | 部署结果通知、构建状态回调 | | 告警平台 | 向 PagerDuty、Grafana OnCall 等平台推送告警 | | 事件驱动 | 业务事件触发第三方系统处理 | 错误处理 [#错误处理] Webhook 发送失败时,`NotifyResult` 将包含 HTTP 响应状态或连接异常信息: ```java title="WebhookErrorExample.java" NotifyResult result = webhookNotify.send(message); if (!result.isSuccess()) { // [!code highlight:2] // errorCode 可能包含 HTTP 状态码或异常类型 log.error("Webhook 推送失败 [{}]: {}", result.getErrorCode(), result.getMessage()); } ``` 下一步 [#下一步] # 注解 (/docs/bamboo-base-java/triple/annotation) import { TypeTable } from '@/components/type-table'; 注解 [#注解] bamboo-triple 提供两个核心注解,分别用于声明式的上下文传播与请求参数校验。通过在 Dubbo 服务方法上标注注解,即可自动触发对应的 AOP 切面逻辑。 @DubboPersistentContext [#dubbopersistentcontext] `@DubboPersistentContext` 用于声明 Dubbo Triple 服务方法需要进行上下文传播。标注该注解后,`DubboContextAspect` 将自动从请求参数中提取 `ctx` 并初始化 `ContextHolder` 与 `MDC`。 定义 [#定义] ```java title="DubboPersistentContext.java" package com.xlf.utility.triple.annotations; import java.lang.annotation.*; // [!code highlight:4] // 可标注于方法或类型上 @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface DubboPersistentContext { } ``` 作用范围 [#作用范围] | 标注位置 | 效果 | | ---- | -------------- | | 方法级别 | 仅该方法触发上下文传播 | | 类级别 | 该类所有方法均触发上下文传播 | 使用示例 [#使用示例] 方法级别标注 [#方法级别标注] ```java title="UserTripleServiceImpl.java" @DubboService public class UserTripleServiceImpl implements UserTripleService { // [!code highlight:2] // 仅 queryUser 方法触发上下文传播 @DubboPersistentContext @Override public TripleResponse queryUser(UserQueryRequest request) { UserDTO user = userService.getById(request.getUserId()); return TripleResult.success(user); } // 此方法不触发上下文传播 @Override public TripleResponse ping() { return TripleResult.success(); } } ``` 类级别标注 [#类级别标注] ```java title="OrderTripleServiceImpl.java" // [!code highlight:2] // 类级别标注,所有方法均触发上下文传播 @DubboPersistentContext @DubboService public class OrderTripleServiceImpl implements OrderTripleService { @Override public TripleResponse queryOrder(OrderQueryRequest request) { return TripleResult.success(orderService.getById(request.getOrderId())); } @Override public TripleResponse cancelOrder(OrderCancelRequest request) { orderService.cancel(request.getOrderId()); return TripleResult.success(); } } ``` 前置条件 [#前置条件] 使用 `@DubboPersistentContext` 时,方法参数需满足以下条件: * 至少有一个参数对象提供 `getCtx()` 方法 * 通常该参数为 `TripleRequest` 的子类 * `ctx` 字段需在调用方通过 `init()` 方法初始化 @TripleRequestCheck [#triplerequestcheck] `@TripleRequestCheck` 用于声明 Dubbo Triple 服务方法需要进行请求参数校验。标注该注解后,`TripleRequestCheckAspect` 将自动提取 `TripleRequest` 参数并调用其 `validate()` 方法。 定义 [#定义-1] ```java title="TripleRequestCheck.java" package com.xlf.utility.triple.annotations; import com.xlf.utility.ErrorCode; import java.lang.annotation.*; // [!code highlight:3] // 可标注于方法或类上 @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface TripleRequestCheck { String message() default ""; ErrorCode errorCode() default ErrorCode.PARAMETER_INVALID; } ``` 属性说明 [#属性说明] 使用示例 [#使用示例-1] 基本用法 [#基本用法] ```java title="BasicCheckExample.java" @DubboPersistentContext // [!code highlight:2] // 使用默认错误消息与错误码 @TripleRequestCheck @Override public TripleResponse queryUser(UserQueryRequest request) { return TripleResult.success(userService.getById(request.getUserId())); } ``` 自定义消息与错误码 [#自定义消息与错误码] ```java title="CustomCheckExample.java" @DubboPersistentContext // [!code highlight:2] // 自定义校验失败的错误消息与错误码 @TripleRequestCheck(message = "订单创建参数无效", errorCode = ErrorCode.PARAMETER_INVALID) @Override public TripleResponse createOrder(OrderCreateRequest request) { OrderDTO order = orderService.create(request); return TripleResult.success(order, "订单创建成功"); } ``` 校验流程 [#校验流程] ``` 方法调用 │ ▼ TripleRequestCheckAspect 拦截 │ ├── 遍历方法参数,查找 TripleRequest 类型 │ ├── 未找到 → 直接放行 │ ├── 找到 → 调用 validate() │ │ │ ├── true → 放行,执行目标方法 │ │ │ └── false → 抛出 BusinessException │ ├── message = @TripleRequestCheck.message()(为空则使用默认消息) │ └── errorCode = @TripleRequestCheck.errorCode() │ ▼ 目标方法执行或异常抛出 ``` 组合使用 [#组合使用] 在实际业务中,通常将两个注解组合使用,确保上下文传播与参数校验同时生效: ```java title="CombinedExample.java" @DubboService public class ProductTripleServiceImpl implements ProductTripleService { // [!code highlight:3] // 组合使用:先传播上下文,再校验请求参数 @DubboPersistentContext @TripleRequestCheck(message = "商品查询参数无效", errorCode = ErrorCode.PARAMETER_INVALID) @Override public TripleResponse queryProduct(ProductQueryRequest request) { ProductDTO product = productService.getById(request.getProductId()); if (product == null) { return TripleResult.error(ErrorCode.RESOURCE_NOT_FOUND, "商品不存在"); } return TripleResult.success(product, "查询成功"); } } ``` 调用方构建请求: ```java title="ProductClient.java" // [!code highlight:5] // 调用方需通过 init() 初始化请求元数据 ProductQueryRequest request = ProductQueryRequest.builder() .productId(20001L) .build() .init("gateway-service"); TripleResponse response = productTripleService.queryProduct(request); ``` 下一步 [#下一步] # AOP 切面 (/docs/bamboo-base-java/triple/aspect) AOP 切面 [#aop-切面] bamboo-triple 提供两个核心 AOP 切面,分别负责 Dubbo Triple 调用链的上下文传播与请求参数校验。两者均以 `HIGHEST_PRECEDENCE` 优先级执行,确保在业务逻辑之前完成基础设施初始化。 DubboContextAspect [#dubbocontextaspect] `DubboContextAspect` 是上下文传播切面,拦截标注 `@DubboPersistentContext` 的方法,从请求参数中提取链路追踪 ID 并初始化上下文环境。 工作流程 [#工作流程] ``` 客户端调用 Dubbo 服务方法 │ ▼ DubboContextAspect (@Around) │ ├── 1. 遍历所有参数,通过反射尝试调用 getCtx() ├── 2. 提取 ctx 值(链路追踪 ID) ├── 3. 初始化 ContextHolder(设置 contextId 与起始时间) ├── 4. 设置 MDC(日志链路追踪) ├── 5. 执行目标方法 └── 6. 清理 ContextHolder 与 MDC ``` 类定义 [#类定义] ```java title="DubboContextAspect.java" package com.xlf.utility.triple.aspect; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Around; import org.springframework.core.annotation.Order; import org.springframework.core.Ordered; import org.springframework.stereotype.Component; // [!code highlight:3] // 最高优先级执行,确保上下文在所有切面之前初始化 @Aspect @Component @Order(Ordered.HIGHEST_PRECEDENCE) public class DubboContextAspect { @Around("@within(com.xlf.utility.triple.annotations.DubboPersistentContext) " + "|| @annotation(com.xlf.utility.triple.annotations.DubboPersistentContext)") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { // 1. 反射提取 ctx // 2. 初始化 ContextHolder + MDC // 3. 执行目标方法 // 4. 清理上下文 } } ``` 上下文提取机制 [#上下文提取机制] 切面通过反射机制从方法参数中提取上下文 ID: ```java title="ContextExtraction.java" // [!code highlight:5] // 反射调用 getCtx() 获取链路追踪 ID Object[] args = joinPoint.getArgs(); for (Object arg : args) { if (arg == null) { continue; } try { Method getCtx = arg.getClass().getMethod("getCtx"); String contextId = (String) getCtx.invoke(arg); // 找到第一个非空 ctx 后立即使用 } catch (NoSuchMethodException ignored) { // 非上下文参数,继续遍历 } } ``` 该机制要求方法的请求参数中至少有一个对象提供 `getCtx()` 方法(通常为 `TripleRequest` 子类)。 ContextHolder 与 MDC [#contextholder-与-mdc] 初始化完成后,以下信息可在整个调用链中使用: | 存储位置 | 键 | 值 | 用途 | | --------------- | ------------ | ---- | --------------------------------- | | `ContextHolder` | contextId | UUID | `TripleResult` 自动填充 `context` 字段 | | `ContextHolder` | startTime | 时间戳 | `TripleResult` 自动计算 `duration` 字段 | | `MDC` | `CONTEXT_ID` | UUID | 日志框架自动输出链路追踪 ID | TripleRequestCheckAspect [#triplerequestcheckaspect] `TripleRequestCheckAspect` 是请求校验切面,拦截标注 `@TripleRequestCheck` 的方法,调用请求参数的 `validate()` 方法进行参数校验。 工作流程 [#工作流程-1] ``` Dubbo 服务方法调用 │ ▼ TripleRequestCheckAspect (@Around) │ ├── 1. 从方法参数中查找 TripleRequest 类型的参数 ├── 2. 调用 validate() 方法 ├── 3a. validate() 返回 true → 继续执行目标方法 └── 3b. validate() 返回 false → 抛出 BusinessException ``` 类定义 [#类定义-1] ```java title="TripleRequestCheckAspect.java" package com.xlf.utility.triple.aspect; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Around; import org.springframework.core.annotation.Order; import org.springframework.core.Ordered; import org.springframework.stereotype.Component; // [!code highlight:3] // 最高优先级执行,在业务逻辑之前完成参数校验 @Aspect @Component @Order(Ordered.HIGHEST_PRECEDENCE) public class TripleRequestCheckAspect { @Around("@within(tripleRequestCheck) || @annotation(tripleRequestCheck)") public Object around(ProceedingJoinPoint joinPoint, TripleRequestCheck tripleRequestCheck) throws Throwable { // 1. 遍历参数提取第一个 TripleRequest 参数 // 2. 调用 validate() // 3. 校验失败则抛出 BusinessException } } ``` 校验失败处理 [#校验失败处理] 当 `validate()` 返回 `false` 时,切面将抛出 `BusinessException`,异常消息与错误码来源于 `@TripleRequestCheck` 注解的属性: ```java title="ValidationFailure.java" // [!code highlight:4] // 校验失败时抛出异常,消息与错误码来自注解属性 if (!request.validate()) { String message = tripleRequestCheck.message().isEmpty() ? "TripleRequest参数校验失败" : tripleRequestCheck.message(); throw new BusinessException( message, tripleRequestCheck.errorCode(), request.getSummary()); } ``` 切面执行顺序 [#切面执行顺序] 当方法同时标注 `@DubboPersistentContext` 与 `@TripleRequestCheck` 时,执行顺序如下: ``` 请求进入 │ ▼ DubboContextAspect (HIGHEST_PRECEDENCE) │ → 初始化上下文 ▼ TripleRequestCheckAspect (HIGHEST_PRECEDENCE) │ → 参数校验 ▼ 目标方法执行 │ ▼ 返回响应 ``` > 两个切面均为 `HIGHEST_PRECEDENCE`,实际执行顺序由 Spring 容器的注册顺序决定。`TripleAutoConfiguration` 确保 `DubboContextAspect` 优先注册。 下一步 [#下一步] # 异常处理 (/docs/bamboo-base-java/triple/exception) import { TypeTable } from '@/components/type-table'; 异常处理 [#异常处理] bamboo-triple 提供两个异常处理器,用于统一处理 Dubbo RPC 调用过程中产生的异常。这些处理器将 RPC 异常转换为标准化的 `TripleResponse` 响应,确保调用方能够获得一致的错误信息。 异常处理器概览 [#异常处理器概览] | 处理器 | 处理的异常类型 | 说明 | | -------------------------- | -------------------- | ----------------------------------- | | `DubboException` | `RpcException` | Dubbo RPC 调用直接抛出的异常 | | `CustomExecutionException` | `ExecutionException` | 异步调用包装的异常(内部含 `StatusRpcException`) | DubboException [#dubboexception] `DubboException` 处理 Dubbo RPC 框架直接抛出的 `RpcException` 异常。 类定义 [#类定义] ```java title="DubboException.java" package com.xlf.utility.triple.exception; import org.apache.dubbo.rpc.RpcException; @SuppressWarnings("unused") public class DubboException { private static final Logger log = LoggerFactory.getLogger(DubboException.class); public TripleResponse handleRpcException(RpcException exception) { log.error("RPC 异常 | [{}]{} ", exception.getCode(), exception.getMessage(), exception); return TripleResult.error(ErrorCode.SERVER_INTERNAL_ERROR, exception.getMessage(), null); } } ``` 处理的异常场景 [#处理的异常场景] | RpcException 错误码 | 场景 | 说明 | | ---------------- | ----- | ----------- | | `0` | 未知异常 | 网络层面或框架内部异常 | | `1` | 网络异常 | 连接失败、超时等 | | `2` | 服务端异常 | 服务提供者返回异常 | | `3` | 客户端异常 | 服务消费者端异常 | | `4` | 线程池异常 | 线程池满、拒绝执行 | | `5` | 序列化异常 | 请求/响应序列化失败 | 响应示例 [#响应示例] ```json title="response.json" { "context": "550e8400-e29b-41d4-a716-446655440000", "success": false, "code": "50001", "message": "Failed to invoke the method queryUser in the service com.example.UserService...", "data": null, "duration": 2003, "timestamp": 1772620800000 } ``` CustomExecutionException [#customexecutionexception] `CustomExecutionException` 处理异步调用场景下的 `ExecutionException`,该异常通常包装了 Dubbo 的 `StatusRpcException`。 类定义 [#类定义-1] ```java title="CustomExecutionException.java" package com.xlf.utility.triple.exception; import org.apache.dubbo.rpc.StatusRpcException; import java.util.concurrent.ExecutionException; @SuppressWarnings("unused") public class CustomExecutionException { private static final Logger log = LoggerFactory.getLogger(CustomExecutionException.class); public TripleResponse handleRpcException(ExecutionException exception) { if (exception.getCause() != null) { // [!code highlight:3] // 如果原因是 StatusRpcException,提取 RPC 异常信息 if (exception.getCause() instanceof StatusRpcException rpcException) { log.error("RPC 异常 | [{}]{} ", rpcException.getCode(), rpcException.getMessage(), rpcException); return TripleResult.error(ErrorCode.SERVER_INTERNAL_ERROR, rpcException.getMessage(), null); } } // 其他类型的 ExecutionException log.error("执行异常 | {} ", exception.getMessage(), exception); return TripleResult.error(ErrorCode.SERVER_INTERNAL_ERROR, exception.getMessage(), null); } } ``` 处理流程 [#处理流程] ``` 异步 Dubbo 调用 │ ▼ CompletableFuture.get() 抛出 ExecutionException │ ▼ CustomExecutionException.handleRpcException() │ ├── getCause() 是 StatusRpcException? │ │ │ ├── 是 → 提取 RPC 异常信息,返回 TripleResponse │ │ │ └── 否 → 使用 ExecutionException 消息,返回 TripleResponse │ ▼ TripleResponse 错误响应 ``` 自动配置 [#自动配置] 这两个异常处理器由 `TripleAutoConfiguration` 自动注册: ```java title="TripleAutoConfiguration.java" @Configuration public class TripleAutoConfiguration { @Bean @ConditionalOnClass(RpcException.class) public DubboException dubboException() { return new DubboException(); } @Bean @ConditionalOnClass(ExecutionException.class) public CustomExecutionException customExecutionException() { return new CustomExecutionException(); } } ``` 使用场景 [#使用场景] 同步调用异常 [#同步调用异常] ```java title="SyncExample.java" @DubboReference private UserService userService; public TripleResponse getUser(Long userId) { try { return userService.queryUser(request); } catch (RpcException e) { // [!code highlight:2] // RpcException 由 DubboException 处理 throw e; // 或直接返回 dubboException.handleRpcException(e) } } ``` 异步调用异常 [#异步调用异常] ```java title="AsyncExample.java" @DubboReference(async = true) private UserService userService; public CompletableFuture> getUserAsync(Long userId) { return userService.queryUserAsync(request) .thenApply(response -> response) .exceptionally(ex -> { // [!code highlight:3] // ExecutionException 由 CustomExecutionException 处理 if (ex instanceof ExecutionException ee) { return customExecutionException.handleRpcException(ee); } return TripleResult.error(ErrorCode.SERVER_INTERNAL_ERROR, ex.getMessage(), null); }); } ``` 注意事项 [#注意事项] 1. **异常信息暴露**:异常处理器直接使用异常消息作为响应消息,生产环境可能需要脱敏处理 2. **日志级别**:所有 RPC 异常均以 ERROR 级别记录,便于问题排查 3. **错误码统一**:所有 RPC 异常统一映射为 `SERVER_INTERNAL_ERROR`(50001) 4. **上下文传播**:异常发生时,上下文信息(`context` 和 `duration`)仍会被正确填充 下一步 [#下一步] # Dubbo Triple (/docs/bamboo-base-java/triple) Dubbo Triple [#dubbo-triple] **bamboo-triple** 是筱工具(Java) 的 RPC 层模块,专为 Apache Dubbo Triple 协议设计。该模块提供了请求/响应模型、AOP 切面、注解驱动的校验机制与异常处理器,实现 Dubbo 微服务间的标准化通信。 核心能力 [#核心能力] | 能力 | 说明 | | --------- | -------------------------------------------------------- | | **请求模型** | `TripleRequest` 抽象类,定义请求基础结构与校验规范 | | **响应模型** | `TripleResponse` 泛型响应体,统一 RPC 返回格式 | | **结果工厂** | `TripleResult` 静态工厂,快速构建成功/失败响应 | | **上下文传播** | `@DubboPersistentContext` 注解 + `DubboContextAspect` 切面 | | **请求校验** | `@TripleRequestCheck` 注解 + `TripleRequestCheckAspect` 切面 | | **异常兜底** | `DubboException` 与 `CustomExecutionException` 异常处理器 | 适用场景 [#适用场景] 当项目满足以下条件时,推荐引入 bamboo-triple 模块: * 使用 Apache Dubbo 3.x 作为 RPC 框架 * 采用 Triple 协议进行服务间通信 * 需要统一 RPC 请求/响应格式 * 需要在 Dubbo 调用链中传播上下文信息(如链路追踪 ID) Maven 依赖 [#maven-依赖] ```xml title="pom.xml" com.x-lf.utility bamboo-triple 2.0.0 ``` > bamboo-triple 依赖 `bamboo-base` 核心库,引入时将自动传递依赖。 自动配置 [#自动配置] bamboo-triple 通过 `TripleAutoConfiguration` 实现 Spring Boot 自动配置。当 classpath 中存在 Dubbo 相关类时,以下组件将自动注册: ```java title="TripleAutoConfiguration.java" // [!code highlight:5] // 条件化自动注册,仅在 Dubbo 环境下生效 @Configuration public class TripleAutoConfiguration { // DubboContextAspect - 上下文传播切面 // DubboException - RpcException 异常处理器 // CustomExecutionException - ExecutionException 异常处理器 } ``` | 自动注册组件 | 条件 | 职责 | | -------------------------- | -------------------- | ------------------------------------------------ | | `DubboContextAspect` | Dubbo 类存在于 classpath | 上下文初始化与传播 | | `DubboException` | Dubbo 类存在于 classpath | 处理 `RpcException` | | `CustomExecutionException` | Dubbo 类存在于 classpath | 处理 `ExecutionException`(包装 `StatusRpcException`) | 模块结构 [#模块结构] ``` bamboo-triple └── com/xlf/utility/triple ├── TripleRequest (请求抽象基类) ├── TripleResponse (响应泛型封装) ├── TripleResult (响应静态工厂) ├── annotations │ ├── DubboPersistentContext (上下文注解) │ └── TripleRequestCheck (校验注解) ├── aspect │ ├── DubboContextAspect (上下文传播切面) │ └── TripleRequestCheckAspect (请求校验切面) ├── exception │ ├── DubboException (RPC 异常处理) │ └── CustomExecutionException (执行异常处理) └── auto └── TripleAutoConfiguration (自动配置) ``` 下一步 [#下一步] # TripleRequest (/docs/bamboo-base-java/triple/request) import { TypeTable } from '@/components/type-table'; TripleRequest [#triplerequest] `TripleRequest` 是 Dubbo Triple RPC 请求的抽象基类,定义了请求元数据结构与校验规范。所有 Triple 服务的请求参数均应继承该类,实现 `validate()` 与 `getSummary()` 抽象方法。 类定义 [#类定义] ```java title="TripleRequest.java" package com.xlf.utility.triple; import lombok.experimental.SuperBuilder; import java.io.Serializable; // [!code highlight:3] // 抽象请求基类,使用 @SuperBuilder 支持子类继承 Builder @SuperBuilder public abstract class TripleRequest implements Serializable { private String ctx; private Long timestamp; private String source; private String version; public T init(String source) { ... } public abstract boolean validate(); public abstract String getSummary(); } ``` 字段说明 [#字段说明] 抽象方法 [#抽象方法] validate() [#validate] 校验请求参数的合法性。由 `TripleRequestCheckAspect` 在方法执行前自动调用。 ```java title="TripleRequest.java" // [!code highlight:3] // 返回 true 表示校验通过,false 将触发 BusinessException public abstract boolean validate(); ``` getSummary() [#getsummary] 返回请求的摘要描述,可用于日志记录与调试输出。 ```java title="TripleRequest.java" public abstract String getSummary(); ``` init() 方法 [#init-方法] `init()` 方法用于初始化请求元数据,并返回当前实例(泛型 `T extends TripleRequest`)以支持链式调用。其行为与源码保持一致: * `timestamp`:设置为 `System.currentTimeMillis()` * `ctx`:读取 `com.xlf.utility.mvc.holder.ContextHolder.getContextId()` * `source`:设置为调用方传入值 * `version`:固定设置为 `"1.0.0"` ```java title="InitExample.java" // [!code highlight:2] // 初始化请求元数据 UserQueryRequest request = UserQueryRequest.builder() .userId(10001L) .build() .init("order-service"); ``` 自定义请求类 [#自定义请求类] 继承 `TripleRequest` 并实现两个抽象方法: ```java title="UserQueryRequest.java" import com.xlf.utility.triple.TripleRequest; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.experimental.SuperBuilder; @Data @SuperBuilder @EqualsAndHashCode(callSuper = true) public class UserQueryRequest extends TripleRequest { private Long userId; private String username; // [!code highlight:8] // 实现参数校验逻辑 @Override public boolean validate() { return userId != null && userId > 0; } @Override public String getSummary() { return "查询用户 [userId=" + userId + ", username=" + username + "]"; } } ``` 完整使用示例 [#完整使用示例] ```java title="UserTripleService.java" import com.xlf.utility.ErrorCode; import org.apache.dubbo.config.annotation.DubboService; @DubboService public class UserTripleServiceImpl implements UserTripleService { @DubboPersistentContext @TripleRequestCheck(message = "用户查询参数无效", errorCode = ErrorCode.PARAMETER_INVALID) @Override public TripleResponse queryUser(UserQueryRequest request) { // [!code highlight:3] // 此处 request 已通过 validate() 校验 // ctx 已由 DubboContextAspect 提取并注入上下文 UserDTO user = userService.getById(request.getUserId()); return TripleResult.success(user, "查询成功"); } } ``` 下一步 [#下一步] # TripleResponse (/docs/bamboo-base-java/triple/response) import { TypeTable } from '@/components/type-table'; TripleResponse [#tripleresponse] `TripleResponse` 是 Dubbo Triple RPC 的统一响应封装,采用泛型设计承载业务数据。所有 Triple 服务方法均应返回该类型,确保微服务间通信格式一致。 类定义 [#类定义] ```java title="TripleResponse.java" package com.xlf.utility.triple; import lombok.experimental.SuperBuilder; import java.io.Serializable; // [!code highlight:2] // 泛型响应体,T 为业务数据类型 @SuperBuilder public class TripleResponse implements Serializable { private String context; private Boolean success; private String code; private String message; private T data; private Long duration; private Long timestamp; } ``` 字段说明 [#字段说明] 响应示例 [#响应示例] 成功响应 [#成功响应] ```json title="success-response.json" { "context": "550e8400-e29b-41d4-a716-446655440000", "success": true, "code": "200", "message": "查询成功", "data": { "userId": 10001, "username": "xiao_lfeng", "email": "test@example.com" }, "duration": 23, "timestamp": 1772620800000 } ``` 失败响应 [#失败响应] ```json title="error-response.json" { "context": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "success": false, "code": "40004", "message": "参数无效: 用户 ID 不能为空", "data": null, "duration": 2, "timestamp": 1772620800000 } ``` 异常响应 [#异常响应] ```json title="exception-response.json" { "context": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "success": false, "code": "500", "message": "系统异常: NullPointerException", "data": null, "duration": 156, "timestamp": 1772620800000 } ``` 与 BaseResponse 的区别 [#与-baseresponse-的区别] | 维度 | `BaseResponse` | `TripleResponse` | | ----- | ----------------- | ------------------------------ | | 适用场景 | HTTP REST API | Dubbo Triple RPC | | 状态码类型 | `Integer`(5 位数字码) | `String`(数字码字符串) | | 序列化 | Jackson JSON | Dubbo 序列化(Protobuf / Hessian2) | | 上下文来源 | MVC ContextHolder | Dubbo ContextHolder | | 成功标志 | 依据 code 判断 | 显式 `success` 布尔值 | 构建方式 [#构建方式] 通常不直接构建 `TripleResponse`,而是通过 `TripleResult` 静态工厂方法创建: ```java title="TripleResponseBuild.java" // [!code highlight:2] // 推荐:使用 TripleResult 工厂方法 TripleResponse response = TripleResult.success(userDTO, "查询成功"); // 直接 Builder 构建(不推荐,需手动填充上下文信息) TripleResponse response = TripleResponse.builder() .context(ContextHolder.getContextId()) .success(true) .code("200") .message("查询成功") .data(userDTO) .duration(ContextHolder.getDuration()) .timestamp(System.currentTimeMillis()) .build(); ``` 下一步 [#下一步] # TripleResult (/docs/bamboo-base-java/triple/result) TripleResult [#tripleresult] `TripleResult` 是 `TripleResponse` 的静态工厂类,提供一组便捷方法用于快速构建标准化的成功与错误响应。该类自动从 MVC `ContextHolder` 获取链路追踪 ID 与请求耗时,无需手动填充上下文信息。成功响应固定为 `success=true`、`code="200"`;失败响应会映射 `ErrorCode.getCode()`(如 `"40403"`、`"50001"`)。 成功响应 [#成功响应] success() [#success] 构建不携带数据的成功响应: ```java title="TripleResult.java" // [!code highlight:2] // 无数据成功响应 TripleResponse response = TripleResult.success(); ``` success(T data) [#successt-data] 构建携带数据的成功响应: ```java title="TripleResult.java" UserDTO user = userService.getById(userId); // [!code highlight:2] // 携带业务数据 TripleResponse response = TripleResult.success(user); ``` success(T data, String message) [#successt-data-string-message] 构建携带数据与自定义消息的成功响应: ```java title="TripleResult.java" // [!code highlight:2] // 携带数据与自定义消息 TripleResponse response = TripleResult.success(user, "用户查询成功"); ``` 错误响应 [#错误响应] error(ErrorCode) [#errorerrorcode] 基于 ErrorCode 枚举构建错误响应: ```java title="TripleResult.java" // [!code highlight:2] // 使用预定义错误码 TripleResponse response = TripleResult.error(ErrorCode.PARAMETER_ERROR); ``` error(ErrorCode, String) [#errorerrorcode-string] 基于 ErrorCode 并附带自定义消息: ```java title="TripleResult.java" // [!code highlight:3] // 自定义错误消息 TripleResponse response = TripleResult.error( ErrorCode.PARAMETER_ERROR, "用户 ID 不能为空"); ``` error(ErrorCode, String, T) [#errorerrorcode-string-t] 基于 ErrorCode、自定义消息并携带错误详情数据: ```java title="TripleResult.java" // [!code highlight:4] // 携带错误详情数据 ValidationResult detail = new ValidationResult("userId", "不能为空"); TripleResponse response = TripleResult.error( ErrorCode.PARAMETER_ERROR, "参数校验失败", detail); ``` error(Exception) [#errorexception] 基于异常对象构建错误响应: ```java title="TripleResult.java" try { // 业务逻辑 } catch (Exception e) { // [!code highlight:2] // 异常转错误响应 return TripleResult.error(e); } ``` 方法汇总 [#方法汇总] | 方法签名 | 返回类型 | 说明 | | --------------------------------- | ------------------- | -------------------------- | | `success()` | `TripleResponse` | 无数据成功响应(通常以 `T=Void` 使用) | | `success(T data)` | `TripleResponse` | 携带数据的成功响应 | | `success(T data, String message)` | `TripleResponse` | 携带数据与消息的成功响应 | | `error(ErrorCode)` | `TripleResponse` | 基于错误码的失败响应 | | `error(ErrorCode, String)` | `TripleResponse` | 基于错误码与消息的失败响应 | | `error(ErrorCode, String, T)` | `TripleResponse` | 携带错误详情的失败响应 | | `error(Exception)` | `TripleResponse` | 基于异常的失败响应(通常以 `T=Void` 使用) | 上下文自动填充 [#上下文自动填充] `TripleResult` 内部通过 MVC `ContextHolder` 自动填充以下字段: * **context** - 从 `ContextHolder.getContextId()` 获取链路追踪 ID * **duration** - 从 `ContextHolder.getDuration()` 计算请求耗时 * **timestamp** - 使用 `System.currentTimeMillis()` 获取当前时间 因此,使用 `TripleResult` 前需确保 `DubboContextAspect` 已正确初始化上下文。 完整使用示例 [#完整使用示例] ```java title="OrderTripleServiceImpl.java" import com.xlf.utility.ErrorCode; import com.xlf.utility.triple.TripleResponse; import com.xlf.utility.triple.TripleResult; import org.apache.dubbo.config.annotation.DubboService; @DubboService public class OrderTripleServiceImpl implements OrderTripleService { @DubboPersistentContext @TripleRequestCheck(message = "订单查询参数无效", errorCode = ErrorCode.PARAMETER_INVALID) @Override public TripleResponse queryOrder(OrderQueryRequest request) { OrderDTO order = orderRepository.findById(request.getOrderId()); if (order == null) { // [!code highlight:3] // 业务错误:订单不存在 return TripleResult.error( ErrorCode.RESOURCE_NOT_FOUND, "订单不存在"); } return TripleResult.success(order, "查询成功"); } @DubboPersistentContext @Override public TripleResponse cancelOrder(OrderCancelRequest request) { try { orderService.cancel(request.getOrderId()); return TripleResult.success(); } catch (Exception e) { // [!code highlight:2] // 异常兜底处理 return TripleResult.error(e); } } } ``` 下一步 [#下一步] # HashCache (/docs/bamboo-base-go/components/cache/hash-cache) import { TypeTable } from '@/components/type-table'; HashCache [#hashcache] `HashCache` 定义了基于哈希(Hash)数据结构的缓存操作接口,用于管理二维键值对数据。 接口定义 [#接口定义] ```go type HashCache[K any, F comparable, V any, S any] interface { Get(ctx context.Context, key K, field F) (*V, bool, error) Set(ctx context.Context, key K, field F, value *V) error GetAll(ctx context.Context, key K) (map[F]V, error) GetAllStruct(ctx context.Context, key K) (S, error) SetAll(ctx context.Context, key K, fields map[F]*V) error SetAllStruct(ctx context.Context, key K, value S) error Exists(ctx context.Context, key K, field F) (bool, error) Remove(ctx context.Context, key K, fields ...F) error Delete(ctx context.Context, key K) error } ``` 泛型参数 [#泛型参数] **注意:** 字段键类型 `F` 必须是可比较的(`comparable`),因为需要作为 map 的键。 数据结构 [#数据结构] 哈希结构为:`key → field → value` ``` user:123 → { "name": "张三", "email": "zhangsan@example.com", "age": "25" } ``` 方法说明 [#方法说明] Get [#get] 获取指定字段的值。 ```go Get(ctx context.Context, key K, field F) (*V, bool, error) ``` **参数:** * `ctx` - context.Context 上下文 * `key` - 哈希键 * `field` - 字段键 **返回值:** * `*V` - 指向值的指针(如果存在) * `bool` - 字段是否存在 * `error` - 错误信息 Set [#set] 设置单个字段的值。 ```go Set(ctx context.Context, key K, field F, value *V) error ``` **参数:** * `ctx` - context.Context 上下文 * `key` - 哈希键 * `field` - 字段键 * `value` - 指向值的指针 **返回值:** * `error` - 错误信息 GetAll [#getall] 获取哈希表中的所有字段和值。 ```go GetAll(ctx context.Context, key K) (map[F]V, error) ``` **参数:** * `ctx` - context.Context 上下文 * `key` - 哈希键 **返回值:** * `map[F]V` - 所有字段和值的映射 * `error` - 错误信息 GetAllStruct [#getallstruct] 获取哈希表中的所有字段和值,直接映射到指定结构体。 ```go GetAllStruct(ctx context.Context, key K) (S, error) ``` **参数:** * `ctx` - context.Context 上下文 * `key` - 哈希键 **返回值:** * `S` - 结构体映射结果 * `error` - 错误信息 SetAll [#setall] 批量设置多个字段的值。 ```go SetAll(ctx context.Context, key K, fields map[F]*V) error ``` **参数:** * `ctx` - context.Context 上下文 * `key` - 哈希键 * `fields` - 字段和值的映射 **返回值:** * `error` - 错误信息 SetAllStruct [#setallstruct] 批量设置多个字段的值,使用指定结构体进行写入。 ```go SetAllStruct(ctx context.Context, key K, value S) error ``` **参数:** * `ctx` - context.Context 上下文 * `key` - 哈希键 * `value` - 结构体数据 **返回值:** * `error` - 错误信息 Exists [#exists] 检查指定字段是否存在。 ```go Exists(ctx context.Context, key K, field F) (bool, error) ``` **参数:** * `ctx` - context.Context 上下文 * `key` - 哈希键 * `field` - 字段键 **返回值:** * `bool` - 字段是否存在 * `error` - 错误信息 Remove [#remove] 从哈希表中移除指定的字段。 ```go Remove(ctx context.Context, key K, fields ...F) error ``` **参数:** * `ctx` - context.Context 上下文 * `key` - 哈希键 * `fields` - 要移除的字段列表(可变参数) **返回值:** * `error` - 错误信息 Delete [#delete] 删除整个哈希表。 ```go Delete(ctx context.Context, key K) error ``` **参数:** * `ctx` - context.Context 上下文 * `key` - 哈希键 **返回值:** * `error` - 错误信息 实现示例 [#实现示例] 用户配置缓存 [#用户配置缓存] ```go title="cache/user_config.go" import ( "context" "encoding/json" "github.com/redis/go-redis/v9" xCache "github.com/bamboo-services/bamboo-base-go/major/cache" ) type UserConfig struct { Theme string `json:"theme"` Language string `json:"language"` Timezone string `json:"timezone"` } type UserConfigCache struct { *xCache.Cache } // [!code highlight:17] // Get 获取单个配置项 func (c *UserConfigCache) Get(ctx context.Context, userID string, field string) (*string, bool, error) { key := "user:config:" + userID val, err := c.RDB.HGet(ctx, key, field).Result() if err == redis.Nil { return nil, false, nil } if err != nil { return nil, false, err } return &val, true, nil } // [!code highlight:5] // Set 设置单个配置项 func (c *UserConfigCache) Set(ctx context.Context, userID string, field string, value *string) error { key := "user:config:" + userID return c.RDB.HSet(ctx, key, field, *value).Err() } // [!code highlight:13] // GetAll 获取所有配置 func (c *UserConfigCache) GetAll(ctx context.Context, userID string) (map[string]string, error) { key := "user:config:" + userID result, err := c.RDB.HGetAll(ctx, key).Result() if err != nil { return nil, err } return result, nil } // [!code highlight:13] // SetAll 批量设置配置 func (c *UserConfigCache) SetAll(ctx context.Context, userID string, fields map[string]*string) error { key := "user:config:" + userID data := make(map[string]interface{}) for field, value := range fields { if value != nil { data[field] = *value } } return c.RDB.HSet(ctx, key, data).Err() } // [!code highlight:5] // Exists 检查配置项是否存在 func (c *UserConfigCache) Exists(ctx context.Context, userID string, field string) (bool, error) { key := "user:config:" + userID return c.RDB.HExists(ctx, key, field).Result() } // [!code highlight:5] // Remove 删除配置项 func (c *UserConfigCache) Remove(ctx context.Context, userID string, fields ...string) error { key := "user:config:" + userID return c.RDB.HDel(ctx, key, fields...).Err() } // [!code highlight:4] // Delete 删除所有配置 func (c *UserConfigCache) Delete(ctx context.Context, userID string) error { key := "user:config:" + userID return c.RDB.Del(ctx, key).Err() } ``` 使用配置缓存 [#使用配置缓存] ```go title="service/user_config.go" type UserConfigService struct { cache *UserConfigCache } func NewUserConfigService(cache *xCache.Cache) *UserConfigService { return &UserConfigService{ cache: &UserConfigCache{Cache: cache}, } } // [!code highlight:11] // GetTheme 获取用户主题 func (s *UserConfigService) GetTheme(ctx context.Context, userID string) (string, error) { theme, exists, err := s.cache.Get(ctx, userID, "theme") if err != nil { return "", err } if !exists { return "default", nil } return *theme, nil } // [!code highlight:6] // UpdateTheme 更新用户主题 func (s *UserConfigService) UpdateTheme(ctx context.Context, userID string, theme string) error { // 更新数据库 if err := s.updateThemeInDB(userID, theme); err != nil { return err } // [!code highlight:2] // 更新缓存 return s.cache.Set(ctx, userID, "theme", &theme) } // [!code highlight:5] // GetAllConfig 获取所有配置 func (s *UserConfigService) GetAllConfig(ctx context.Context, userID string) (map[string]string, error) { config, err := s.cache.GetAll(ctx, userID) if err != nil { return nil, err } // 如果缓存为空,从数据库加载 if len(config) == 0 { config, err = s.loadConfigFromDB(userID) if err != nil { return nil, err } // [!code highlight:7] // 写入缓存 fields := make(map[string]*string) for k, v := range config { val := v fields[k] = &val } _ = s.cache.SetAll(ctx, userID, fields) } return config, nil } // [!code highlight:2] // ResetConfig 重置配置 func (s *UserConfigService) ResetConfig(ctx context.Context, userID string) error { // 删除数据库配置 if err := s.deleteConfigFromDB(userID); err != nil { return err } // [!code highlight:2] // 删除缓存 return s.cache.Delete(ctx, userID) } ``` 使用场景 [#使用场景] 用户配置 [#用户配置] ```go type UserConfigCache interface { xCache.HashCache[string, string, string] } ``` 适用于: * 用户偏好设置 * 界面配置 * 通知设置 商品详情 [#商品详情] ```go type ProductCache interface { xCache.HashCache[string, string, interface{}] } ``` 适用于: * 商品价格 * 库存数量 * 商品属性 会话数据 [#会话数据] ```go type SessionCache interface { xCache.HashCache[string, string, string] } ``` 适用于: * 用户会话 * 临时数据 * 表单状态 最佳实践 [#最佳实践] 1. 字段命名规范 [#1-字段命名规范] 使用清晰的字段名: ```go // ✅ 好的命名 fields := map[string]*string{ "theme": &theme, "language": &language, "timezone": &timezone, } // ❌ 避免的命名 fields := map[string]*string{ "t": &theme, "l": &language, "z": &timezone, } ``` 2. 批量操作优化 [#2-批量操作优化] 优先使用 `GetAll` 和 `SetAll`: ```go // ✅ 批量获取 config, _ := cache.GetAll(ctx, userID) // ❌ 逐个获取 theme, _ := cache.Get(ctx, userID, "theme") language, _ := cache.Get(ctx, userID, "language") timezone, _ := cache.Get(ctx, userID, "timezone") ``` 3. 部分更新 [#3-部分更新] 只更新变化的字段: ```go func (s *UserConfigService) UpdatePartial(ctx context.Context, userID string, updates map[string]string) error { fields := make(map[string]*string) for k, v := range updates { val := v fields[k] = &val } return s.cache.SetAll(ctx, userID, fields) } ``` 4. 字段删除 [#4-字段删除] 使用 `Remove` 而不是 `Delete`: ```go // ✅ 删除特定字段 cache.Remove(ctx, userID, "theme", "language") // ❌ 删除整个哈希表(除非确实需要) cache.Delete(ctx, userID) ``` 性能优化 [#性能优化] 使用 Pipeline [#使用-pipeline] 批量操作多个哈希表: ```go func (c *UserConfigCache) SetMultipleUsers(ctx context.Context, configs map[string]map[string]*string) error { pipe := c.RDB.Pipeline() for userID, fields := range configs { key := "user:config:" + userID data := make(map[string]interface{}) for field, value := range fields { if value != nil { data[field] = *value } } pipe.HSet(ctx, key, data) } _, err := pipe.Exec(ctx) return err } ``` 字段数量控制 [#字段数量控制] 避免单个哈希表字段过多: ```go // ✅ 合理的字段数量(< 100) user:config:123 → { "theme": "dark", "language": "zh-CN", "timezone": "Asia/Shanghai" } // ❌ 字段过多(> 1000) user:data:123 → { "field1": "value1", "field2": "value2", // ... 1000+ fields } ``` 注意事项 [#注意事项] 1. **字段类型限制**:字段键必须是 `comparable` 类型 2. **内存占用**:大量字段会占用较多内存,考虑分片 3. **原子性**:单个字段操作是原子的,但多字段操作不保证原子性 4. **过期时间**:哈希表整体过期,无法为单个字段设置 TTL 5. **序列化**:复杂值类型需要序列化为字符串存储 # 概述 (/docs/bamboo-base-go/components/cache) import { TypeTable } from '@/components/type-table'; 缓存 [#缓存] `xCache` 基于 Redis 提供了四种常用数据结构的缓存接口,支持泛型操作,适用于各种缓存场景。 ```go import xCache "github.com/bamboo-services/bamboo-base-go/major/cache" ``` 核心结构 [#核心结构] Cache 结构体 [#cache-结构体] ```go type Cache struct { RDB *redis.Client // Redis 客户端 TTL time.Duration // 默认过期时间 } ``` 缓存接口 [#缓存接口] 快速使用 [#快速使用] 初始化缓存 [#初始化缓存] ```go title="main.go" import ( "time" "github.com/redis/go-redis/v9" xCache "github.com/bamboo-services/bamboo-base-go/major/cache" ) func main() { // [!code highlight:5] // 创建 Redis 客户端 rdb := redis.NewClient(&redis.Options{ Addr: "localhost:6379", DB: 0, }) // [!code highlight:4] // 初始化缓存实例 cache := &xCache.Cache{ RDB: rdb, TTL: 24 * time.Hour, // 默认 24 小时过期 } } ``` 使用键值对缓存 [#使用键值对缓存] ```go title="service/user.go" type UserCache struct { *xCache.Cache } // 实现 KeyCache 接口 func (c *UserCache) Get(ctx context.Context, userID string) (*User, bool, error) { // 实现获取逻辑 } func (c *UserCache) Set(ctx context.Context, userID string, user *User) error { // 实现设置逻辑 } ``` 使用场景 [#使用场景] KeyCache - 键值对缓存 [#keycache---键值对缓存] 适用于存储单一对象: * 用户信息缓存 * 配置项缓存 * Token 缓存 HashCache - 哈希缓存 [#hashcache---哈希缓存] 适用于存储对象属性: * 用户配置(多个字段) * 商品详情(价格、库存等) * 会话数据 SetCache - 集合缓存 [#setcache---集合缓存] 适用于存储唯一元素: * 用户标签 * 权限列表 * 在线用户集合 ListCache - 列表缓存 [#listcache---列表缓存] 适用于存储有序数据: * 消息队列 * 操作历史 * 排行榜 设计特点 [#设计特点] 泛型支持 [#泛型支持] 所有接口都支持泛型,提供类型安全: ```go // 字符串键,User 值 type UserCache interface { xCache.KeyCache[string, User] } // 字符串键,字符串字段,配置值 type ConfigCache interface { xCache.HashCache[string, string, ConfigValue] } ``` 统一上下文 [#统一上下文] 所有方法都接受 `*gin.Context`,便于: * 请求追踪 * 日志记录 * 超时控制 灵活的键类型 [#灵活的键类型] 键类型支持任意类型(`any`),可以使用: * 字符串:`"user:123"` * 结构体:`UserKey{ID: 123}` * 自定义类型:`type CacheKey string` 下一步 [#下一步] # KeyCache (/docs/bamboo-base-go/components/cache/key-cache) import { TypeTable } from '@/components/type-table'; KeyCache [#keycache] `KeyCache` 定义了基于字符串(String)数据结构的缓存操作接口,用于管理单一键值对数据。 接口定义 [#接口定义] ```go type KeyCache[K any, V any] interface { Get(ctx context.Context, key K) (*V, bool, error) Set(ctx context.Context, key K, value *V) error Exists(ctx context.Context, key K) (bool, error) Delete(ctx context.Context, key K) error } ``` 泛型参数 [#泛型参数] 方法说明 [#方法说明] Get [#get] 根据键检索值。 ```go Get(ctx context.Context, key K) (*V, bool, error) ``` **参数:** * `ctx` - context.Context 上下文 * `key` - 缓存键 **返回值:** * `*V` - 指向值的指针(如果存在) * `bool` - 键是否存在 * `error` - 错误信息 Set [#set] 将键值对存入缓存。 ```go Set(ctx context.Context, key K, value *V) error ``` **参数:** * `ctx` - context.Context 上下文 * `key` - 缓存键 * `value` - 指向值的指针(可以为 nil) **返回值:** * `error` - 错误信息 **注意:** 需要处理 `value` 为 `nil` 的场景。 Exists [#exists] 检查指定键是否存在。 ```go Exists(ctx context.Context, key K) (bool, error) ``` **参数:** * `ctx` - context.Context 上下文 * `key` - 缓存键 **返回值:** * `bool` - 键是否存在 * `error` - 错误信息 Delete [#delete] 从缓存中移除指定的键。 ```go Delete(ctx context.Context, key K) error ``` **参数:** * `ctx` - context.Context 上下文 * `key` - 缓存键 **返回值:** * `error` - 错误信息 实现示例 [#实现示例] 用户缓存实现 [#用户缓存实现] ```go title="cache/user.go" import ( "context" "encoding/json" "time" "github.com/gin-gonic/gin" "github.com/redis/go-redis/v9" xCache "github.com/bamboo-services/bamboo-base-go/major/cache" ) type User struct { ID string `json:"id"` Username string `json:"username"` Email string `json:"email"` } type UserCache struct { *xCache.Cache } // [!code highlight:15] // Get 获取用户缓存 func (c *UserCache) Get(ctx context.Context, userID string) (*User, bool, error) { key := "user:" + userID val, err := c.RDB.Get(ctx, key).Result() if err == redis.Nil { return nil, false, nil } if err != nil { return nil, false, err } var user User if err := json.Unmarshal([]byte(val), &user); err != nil { return nil, false, err } return &user, true, nil } // [!code highlight:17] // Set 设置用户缓存 func (c *UserCache) Set(ctx context.Context, userID string, user *User) error { if user == nil { return nil } key := "user:" + userID data, err := json.Marshal(user) if err != nil { return err } return c.RDB.Set(ctx, key, data, c.TTL).Err() } // [!code highlight:5] // Exists 检查用户是否存在 func (c *UserCache) Exists(ctx context.Context, userID string) (bool, error) { key := "user:" + userID count, err := c.RDB.Exists(ctx, key).Result() return count > 0, err } // [!code highlight:4] // Delete 删除用户缓存 func (c *UserCache) Delete(ctx context.Context, userID string) error { key := "user:" + userID return c.RDB.Del(ctx, key).Err() } ``` 使用缓存 [#使用缓存] ```go title="service/user.go" type UserService struct { cache *UserCache } func NewUserService(cache *xCache.Cache) *UserService { return &UserService{ cache: &UserCache{Cache: cache}, } } // [!code highlight:11] // GetUser 获取用户(带缓存) func (s *UserService) GetUser(ctx context.Context, userID string) (*User, error) { // 先从缓存获取 user, exists, err := s.cache.Get(ctx, userID) if err != nil { return nil, err } if exists { return user, nil } // 缓存未命中,从数据库查询 user, err = s.getUserFromDB(userID) if err != nil { return nil, err } // [!code highlight:2] // 写入缓存 _ = s.cache.Set(ctx, userID, user) return user, nil } // [!code highlight:2] // DeleteUser 删除用户并清除缓存 func (s *UserService) DeleteUser(ctx context.Context, userID string) error { // 删除数据库记录 if err := s.deleteUserFromDB(userID); err != nil { return err } // [!code highlight:2] // 删除缓存 return s.cache.Delete(ctx, userID) } ``` 使用场景 [#使用场景] 用户信息缓存 [#用户信息缓存] ```go type UserCache interface { xCache.KeyCache[string, User] } ``` 适用于: * 用户基本信息 * 用户权限信息 * 用户配置 Token 缓存 [#token-缓存] ```go type TokenCache interface { xCache.KeyCache[string, TokenInfo] } ``` 适用于: * JWT Token * 刷新 Token * 临时访问凭证 配置缓存 [#配置缓存] ```go type ConfigCache interface { xCache.KeyCache[string, Config] } ``` 适用于: * 系统配置 * 功能开关 * 动态参数 最佳实践 [#最佳实践] 1. 统一键命名 [#1-统一键命名] 使用前缀区分不同类型的缓存: ```go func (c *UserCache) Get(ctx context.Context, userID string) (*User, bool, error) { // [!code highlight:2] // 使用 "user:" 前缀 key := "user:" + userID // ... } ``` 2. 处理 nil 值 [#2-处理-nil-值] 在 `Set` 方法中正确处理 `nil` 值: ```go func (c *UserCache) Set(ctx context.Context, userID string, user *User) error { // [!code highlight:3] // 如果值为 nil,直接返回 if user == nil { return nil } // ... } ``` 3. 错误处理 [#3-错误处理] 区分缓存未命中和真实错误: ```go func (c *UserCache) Get(ctx context.Context, userID string) (*User, bool, error) { val, err := c.RDB.Get(ctx, key).Result() // [!code highlight:3] // 缓存未命中不是错误 if err == redis.Nil { return nil, false, nil } // [!code highlight:3] // 其他错误需要返回 if err != nil { return nil, false, err } // ... } ``` 4. 序列化选择 [#4-序列化选择] 根据场景选择合适的序列化方式: ```go // JSON - 可读性好,兼容性强 data, _ := json.Marshal(user) // MessagePack - 性能更好,体积更小 data, _ := msgpack.Marshal(user) // Protocol Buffers - 强类型,跨语言 data, _ := proto.Marshal(user) ``` 性能优化 [#性能优化] 批量操作 [#批量操作] 虽然 `KeyCache` 接口不支持批量操作,但可以使用 Pipeline: ```go func (c *UserCache) SetBatch(ctx context.Context, users map[string]*User) error { pipe := c.RDB.Pipeline() for userID, user := range users { key := "user:" + userID data, _ := json.Marshal(user) pipe.Set(ctx, key, data, c.TTL) } _, err := pipe.Exec(ctx) return err } ``` 缓存预热 [#缓存预热] 在系统启动时预加载热点数据: ```go func (s *UserService) WarmupCache(ctx context.Context) error { // 获取热点用户列表 hotUsers, err := s.getHotUsers() if err != nil { return err } // 批量写入缓存 ginCtx := &gin.Context{} for _, user := range hotUsers { _ = s.cache.Set(ginCtx, user.ID, user) } return nil } ``` 注意事项 [#注意事项] 1. **TTL 管理**:合理设置过期时间,避免缓存雪崩 2. **内存占用**:大对象考虑压缩或分片存储 3. **并发安全**:Redis 操作本身是原子的,但业务逻辑需要注意 4. **缓存一致性**:更新数据库后及时更新或删除缓存 # ListCache (/docs/bamboo-base-go/components/cache/list-cache) import { TypeTable } from '@/components/type-table'; ListCache [#listcache] `ListCache` 定义了基于列表(List)数据结构的缓存操作接口,用于管理有序且允许重复元素的列表数据。 接口定义 [#接口定义] ```go type ListCache[K any, V any] interface { Prepend(ctx context.Context, key K, values ...V) error Append(ctx context.Context, key K, values ...V) error Range(ctx context.Context, key K, start int64, end int64) ([]V, error) Index(ctx context.Context, key K, index int64) (*V, error) Len(ctx context.Context, key K) (int64, error) Pop(ctx context.Context, key K) (*V, error) PopLast(ctx context.Context, key K) (*V, error) Remove(ctx context.Context, key K, count int64, value V) error Delete(ctx context.Context, key K) error } ``` 泛型参数 [#泛型参数] 数据特点 [#数据特点] * **有序性**:元素按插入顺序排列 * **可重复**:允许存储相同的元素 * **双端操作**:支持头尾插入和弹出 ``` message:queue → ["msg1", "msg2", "msg3", "msg2"] ↑ ↑ 头部 尾部 ``` 方法说明 [#方法说明] Prepend [#prepend] 将一个或多个值插入到列表头部(左侧)。 ```go Prepend(ctx context.Context, key K, values ...V) error ``` **参数:** * `ctx` - context.Context 上下文 * `key` - 列表键 * `values` - 要插入的值(可变参数) **返回值:** * `error` - 错误信息 **注意:** 多个值按顺序插入,最后一个值会在最前面。 Append [#append] 将一个或多个值追加到列表尾部(右侧)。 ```go Append(ctx context.Context, key K, values ...V) error ``` **参数:** * `ctx` - context.Context 上下文 * `key` - 列表键 * `values` - 要追加的值(可变参数) **返回值:** * `error` - 错误信息 Range [#range] 按索引范围获取列表元素。 ```go Range(ctx context.Context, key K, start int64, end int64) ([]V, error) ``` **参数:** * `ctx` - context.Context 上下文 * `key` - 列表键 * `start` - 起始索引(支持负数) * `end` - 结束索引(支持负数) **返回值:** * `[]V` - 元素切片 * `error` - 错误信息 **索引说明:** * `0` 表示第一个元素 * `-1` 表示最后一个元素 * `-2` 表示倒数第二个元素 Index [#index] 获取指定索引位置的元素。 ```go Index(ctx context.Context, key K, index int64) (*V, error) ``` **参数:** * `ctx` - context.Context 上下文 * `key` - 列表键 * `index` - 索引位置(支持负数) **返回值:** * `*V` - 指向元素的指针 * `error` - 错误信息 Len [#len] 获取列表的长度。 ```go Len(ctx context.Context, key K) (int64, error) ``` **参数:** * `ctx` - context.Context 上下文 * `key` - 列表键 **返回值:** * `int64` - 列表长度 * `error` - 错误信息 Pop [#pop] 从列表头部弹出一个元素并返回。 ```go Pop(ctx context.Context, key K) (*V, error) ``` **参数:** * `ctx` - context.Context 上下文 * `key` - 列表键 **返回值:** * `*V` - 指向弹出元素的指针 * `error` - 错误信息 PopLast [#poplast] 从列表尾部弹出一个元素并返回。 ```go PopLast(ctx context.Context, key K) (*V, error) ``` **参数:** * `ctx` - context.Context 上下文 * `key` - 列表键 **返回值:** * `*V` - 指向弹出元素的指针 * `error` - 错误信息 Remove [#remove] 从列表中移除指定数量的匹配元素。 ```go Remove(ctx context.Context, key K, count int64, value V) error ``` **参数:** * `ctx` - context.Context 上下文 * `key` - 列表键 * `count` - 移除数量 * `count > 0`:从头部开始移除 * `count < 0`:从尾部开始移除 * `count = 0`:移除所有匹配项 * `value` - 要移除的值 **返回值:** * `error` - 错误信息 Delete [#delete] 删除整个列表。 ```go Delete(ctx context.Context, key K) error ``` **参数:** * `ctx` - context.Context 上下文 * `key` - 列表键 **返回值:** * `error` - 错误信息 实现示例 [#实现示例] 消息队列缓存 [#消息队列缓存] ```go title="cache/message_queue.go" import ( "encoding/json" "github.com/gin-gonic/gin" "github.com/redis/go-redis/v9" xCache "github.com/bamboo-services/bamboo-base-go/major/cache" ) type Message struct { ID string `json:"id"` Content string `json:"content"` Time int64 `json:"time"` } type MessageQueueCache struct { *xCache.Cache } // [!code highlight:10] // Prepend 在队列头部插入消息 func (c *MessageQueueCache) Prepend(ctx context.Context, queueName string, messages ...string) error { key := "queue:" + queueName values := make([]interface{}, len(messages)) for i, msg := range messages { values[i] = msg } return c.RDB.LPush(ctx, key, values...).Err() } // [!code highlight:10] // Append 在队列尾部追加消息 func (c *MessageQueueCache) Append(ctx context.Context, queueName string, messages ...string) error { key := "queue:" + queueName values := make([]interface{}, len(messages)) for i, msg := range messages { values[i] = msg } return c.RDB.RPush(ctx, key, values...).Err() } // [!code highlight:5] // Range 获取指定范围的消息 func (c *MessageQueueCache) Range(ctx context.Context, queueName string, start, end int64) ([]string, error) { key := "queue:" + queueName return c.RDB.LRange(ctx, key, start, end).Result() } // [!code highlight:9] // Index 获取指定位置的消息 func (c *MessageQueueCache) Index(ctx context.Context, queueName string, index int64) (*string, error) { key := "queue:" + queueName val, err := c.RDB.LIndex(ctx, key, index).Result() if err == redis.Nil { return nil, nil } return &val, err } // [!code highlight:5] // Len 获取队列长度 func (c *MessageQueueCache) Len(ctx context.Context, queueName string) (int64, error) { key := "queue:" + queueName return c.RDB.LLen(ctx, key).Result() } // [!code highlight:9] // Pop 从队列头部弹出消息 func (c *MessageQueueCache) Pop(ctx context.Context, queueName string) (*string, error) { key := "queue:" + queueName val, err := c.RDB.LPop(ctx, key).Result() if err == redis.Nil { return nil, nil } return &val, err } // [!code highlight:9] // PopLast 从队列尾部弹出消息 func (c *MessageQueueCache) PopLast(ctx context.Context, queueName string) (*string, error) { key := "queue:" + queueName val, err := c.RDB.RPop(ctx, key).Result() if err == redis.Nil { return nil, nil } return &val, err } // [!code highlight:5] // Remove 移除指定消息 func (c *MessageQueueCache) Remove(ctx context.Context, queueName string, count int64, message string) error { key := "queue:" + queueName return c.RDB.LRem(ctx, key, count, message).Err() } // [!code highlight:4] // Delete 删除整个队列 func (c *MessageQueueCache) Delete(ctx context.Context, queueName string) error { key := "queue:" + queueName return c.RDB.Del(ctx, key).Err() } ``` 使用消息队列 [#使用消息队列] ```go title="service/message_queue.go" type MessageQueueService struct { cache *MessageQueueCache } func NewMessageQueueService(cache *xCache.Cache) *MessageQueueService { return &MessageQueueService{ cache: &MessageQueueCache{Cache: cache}, } } // [!code highlight:7] // Enqueue 入队(追加到尾部) func (s *MessageQueueService) Enqueue(ctx context.Context, queueName string, message string) error { return s.cache.Append(ctx, queueName, message) } // [!code highlight:5] // Dequeue 出队(从头部弹出) func (s *MessageQueueService) Dequeue(ctx context.Context, queueName string) (*string, error) { return s.cache.Pop(ctx, queueName) } // [!code highlight:5] // Peek 查看队列头部(不弹出) func (s *MessageQueueService) Peek(ctx context.Context, queueName string) (*string, error) { return s.cache.Index(ctx, queueName, 0) } // [!code highlight:5] // Size 获取队列大小 func (s *MessageQueueService) Size(ctx context.Context, queueName string) (int64, error) { return s.cache.Len(ctx, queueName) } // [!code highlight:5] // GetRecent 获取最近的 N 条消息 func (s *MessageQueueService) GetRecent(ctx context.Context, queueName string, count int64) ([]string, error) { return s.cache.Range(ctx, queueName, -count, -1) } ``` 使用场景 [#使用场景] 消息队列 [#消息队列] ```go type MessageQueueCache interface { xCache.ListCache[string, string] } ``` 适用于: * 任务队列 * 消息队列 * 事件队列 操作历史 [#操作历史] ```go type HistoryCache interface { xCache.ListCache[string, string] } ``` 适用于: * 用户操作历史 * 浏览历史 * 搜索历史 排行榜 [#排行榜] ```go type LeaderboardCache interface { xCache.ListCache[string, string] } ``` 适用于: * 实时排行榜 * 热门列表 * 推荐列表 栈结构 [#栈结构] ```go type StackCache interface { xCache.ListCache[string, string] } ``` 适用于: * 撤销/重做功能 * 状态栈 * 调用栈 最佳实践 [#最佳实践] 1. 队列模式 [#1-队列模式] 使用 `Append` + `Pop` 实现 FIFO 队列: ```go // 入队 cache.Append(ctx, "queue", "msg1", "msg2", "msg3") // 出队 msg, _ := cache.Pop(ctx, "queue") // 返回 "msg1" ``` 2. 栈模式 [#2-栈模式] 使用 `Prepend` + `Pop` 实现 LIFO 栈: ```go // 入栈 cache.Prepend(ctx, "stack", "item1", "item2", "item3") // 出栈 item, _ := cache.Pop(ctx, "stack") // 返回 "item3" ``` 3. 限制列表长度 [#3-限制列表长度] 使用 `LTrim` 保持固定长度: ```go func (c *MessageQueueCache) AppendWithLimit(ctx context.Context, queueName string, message string, maxLen int64) error { key := "queue:" + queueName // [!code highlight:2] // 追加消息 if err := c.RDB.RPush(ctx, key, message).Err(); err != nil { return err } // [!code highlight:2] // 保留最新的 maxLen 条 return c.RDB.LTrim(ctx, key, -maxLen, -1).Err() } ``` 4. 分页获取 [#4-分页获取] 使用 `Range` 实现分页: ```go func (s *MessageQueueService) GetPage(ctx context.Context, queueName string, page, pageSize int64) ([]string, error) { start := (page - 1) * pageSize end := start + pageSize - 1 return s.cache.Range(ctx, queueName, start, end) } ``` 5. 批量操作 [#5-批量操作] 使用可变参数批量插入: ```go // ✅ 批量追加 cache.Append(ctx, "queue", "msg1", "msg2", "msg3") // ❌ 逐个追加 cache.Append(ctx, "queue", "msg1") cache.Append(ctx, "queue", "msg2") cache.Append(ctx, "queue", "msg3") ``` 高级操作 [#高级操作] 阻塞弹出 [#阻塞弹出] 实现阻塞队列(等待元素): ```go // 阻塞弹出(等待 5 秒) func (c *MessageQueueCache) BlockingPop(ctx context.Context, queueName string, timeout time.Duration) (*string, error) { key := "queue:" + queueName result, err := c.RDB.BLPop(ctx, timeout, key).Result() if err == redis.Nil { return nil, nil } if err != nil { return nil, err } if len(result) < 2 { return nil, nil } return &result[1], nil } ``` 列表间移动 [#列表间移动] 将元素从一个列表移动到另一个: ```go // 从源队列弹出并推入目标队列 func (c *MessageQueueCache) Move(ctx context.Context, srcQueue, dstQueue string) error { srcKey := "queue:" + srcQueue dstKey := "queue:" + dstQueue _, err := c.RDB.RPopLPush(ctx, srcKey, dstKey).Result() return err } ``` 插入到指定位置 [#插入到指定位置] 在某个元素前后插入: ```go // 在 pivot 元素之前插入 func (c *MessageQueueCache) InsertBefore(ctx context.Context, queueName, pivot, value string) error { key := "queue:" + queueName return c.RDB.LInsertBefore(ctx, key, pivot, value).Err() } // 在 pivot 元素之后插入 func (c *MessageQueueCache) InsertAfter(ctx context.Context, queueName, pivot, value string) error { key := "queue:" + queueName return c.RDB.LInsertAfter(ctx, key, pivot, value).Err() } ``` 性能优化 [#性能优化] 使用 Pipeline [#使用-pipeline] 批量操作多个列表: ```go func (c *MessageQueueCache) EnqueueMultiple(ctx context.Context, messages map[string][]string) error { pipe := c.RDB.Pipeline() for queueName, msgs := range messages { key := "queue:" + queueName values := make([]interface{}, len(msgs)) for i, msg := range msgs { values[i] = msg } pipe.RPush(ctx, key, values...) } _, err := pipe.Exec(ctx) return err } ``` 避免大列表 [#避免大列表] 限制列表长度,避免性能问题: ```go const MaxQueueSize = 10000 func (c *MessageQueueCache) Append(ctx context.Context, queueName string, messages ...string) error { key := "queue:" + queueName // [!code highlight:5] // 检查长度 length, err := c.RDB.LLen(ctx, key).Result() if err != nil { return err } // [!code highlight:3] // 超过限制则拒绝 if length+int64(len(messages)) > MaxQueueSize { return errors.New("队列已满") } values := make([]interface{}, len(messages)) for i, msg := range messages { values[i] = msg } return c.RDB.RPush(ctx, key, values...).Err() } ``` 注意事项 [#注意事项] 1. **索引范围**:`Range` 的 `end` 是包含的,不同于 Go 切片 2. **负数索引**:`-1` 表示最后一个元素,`-2` 表示倒数第二个 3. **空列表**:弹出空列表返回 `nil`,不是错误 4. **内存占用**:大列表会占用较多内存,考虑分片 5. **性能**:头部插入/删除比尾部慢,优先使用尾部操作 6. **过期时间**:整个列表过期,无法为单个元素设置 TTL # SetCache (/docs/bamboo-base-go/components/cache/set-cache) import { TypeTable } from '@/components/type-table'; SetCache [#setcache] `SetCache` 定义了基于集合(Set)数据结构的缓存操作接口,用于管理无序且元素唯一的集合数据。 接口定义 [#接口定义] ```go type SetCache[K any, V any] interface { Add(ctx context.Context, key K, members ...V) error Members(ctx context.Context, key K) ([]V, error) IsMember(ctx context.Context, key K, member V) (bool, error) Count(ctx context.Context, key K) (int64, error) Remove(ctx context.Context, key K, members ...V) error Delete(ctx context.Context, key K) error } ``` 泛型参数 [#泛型参数] 数据特点 [#数据特点] * **无序性**:成员没有固定顺序 * **唯一性**:相同成员只会存储一次 * **高效性**:成员检查时间复杂度 O(1) ``` user:tags:123 → {"golang", "redis", "docker"} ``` 方法说明 [#方法说明] Add [#add] 将一个或多个成员添加到集合中。 ```go Add(ctx context.Context, key K, members ...V) error ``` **参数:** * `ctx` - context.Context 上下文 * `key` - 集合键 * `members` - 要添加的成员(可变参数) **返回值:** * `error` - 错误信息 **注意:** 已存在的成员会被忽略。 Members [#members] 获取集合中的所有成员。 ```go Members(ctx context.Context, key K) ([]V, error) ``` **参数:** * `ctx` - context.Context 上下文 * `key` - 集合键 **返回值:** * `[]V` - 所有成员的切片 * `error` - 错误信息 IsMember [#ismember] 检查指定成员是否存在于集合中。 ```go IsMember(ctx context.Context, key K, member V) (bool, error) ``` **参数:** * `ctx` - context.Context 上下文 * `key` - 集合键 * `member` - 要检查的成员 **返回值:** * `bool` - 成员是否存在 * `error` - 错误信息 Count [#count] 获取集合中的成员数量。 ```go Count(ctx context.Context, key K) (int64, error) ``` **参数:** * `ctx` - context.Context 上下文 * `key` - 集合键 **返回值:** * `int64` - 成员数量 * `error` - 错误信息 Remove [#remove] 从集合中移除指定的成员。 ```go Remove(ctx context.Context, key K, members ...V) error ``` **参数:** * `ctx` - context.Context 上下文 * `key` - 集合键 * `members` - 要移除的成员(可变参数) **返回值:** * `error` - 错误信息 Delete [#delete] 删除整个集合。 ```go Delete(ctx context.Context, key K) error ``` **参数:** * `ctx` - context.Context 上下文 * `key` - 集合键 **返回值:** * `error` - 错误信息 实现示例 [#实现示例] 用户标签缓存 [#用户标签缓存] ```go title="cache/user_tags.go" import ( "github.com/gin-gonic/gin" xCache "github.com/bamboo-services/bamboo-base-go/major/cache" ) type UserTagsCache struct { *xCache.Cache } // [!code highlight:5] // Add 添加用户标签 func (c *UserTagsCache) Add(ctx context.Context, userID string, tags ...string) error { key := "user:tags:" + userID return c.RDB.SAdd(ctx, key, tags).Err() } // [!code highlight:5] // Members 获取所有标签 func (c *UserTagsCache) Members(ctx context.Context, userID string) ([]string, error) { key := "user:tags:" + userID return c.RDB.SMembers(ctx, key).Result() } // [!code highlight:5] // IsMember 检查标签是否存在 func (c *UserTagsCache) IsMember(ctx context.Context, userID string, tag string) (bool, error) { key := "user:tags:" + userID return c.RDB.SIsMember(ctx, key, tag).Result() } // [!code highlight:5] // Count 获取标签数量 func (c *UserTagsCache) Count(ctx context.Context, userID string) (int64, error) { key := "user:tags:" + userID return c.RDB.SCard(ctx, key).Result() } // [!code highlight:5] // Remove 移除标签 func (c *UserTagsCache) Remove(ctx context.Context, userID string, tags ...string) error { key := "user:tags:" + userID return c.RDB.SRem(ctx, key, tags).Err() } // [!code highlight:4] // Delete 删除所有标签 func (c *UserTagsCache) Delete(ctx context.Context, userID string) error { key := "user:tags:" + userID return c.RDB.Del(ctx, key).Err() } ``` 使用标签缓存 [#使用标签缓存] ```go title="service/user_tags.go" type UserTagsService struct { cache *UserTagsCache } func NewUserTagsService(cache *xCache.Cache) *UserTagsService { return &UserTagsService{ cache: &UserTagsCache{Cache: cache}, } } // [!code highlight:6] // AddTags 添加用户标签 func (s *UserTagsService) AddTags(ctx context.Context, userID string, tags ...string) error { // 更新数据库 if err := s.addTagsToDB(userID, tags...); err != nil { return err } // [!code highlight:2] // 更新缓存 return s.cache.Add(ctx, userID, tags...) } // [!code highlight:11] // GetTags 获取用户标签 func (s *UserTagsService) GetTags(ctx context.Context, userID string) ([]string, error) { // 先从缓存获取 tags, err := s.cache.Members(ctx, userID) if err != nil { return nil, err } // 如果缓存为空,从数据库加载 if len(tags) == 0 { tags, err = s.getTagsFromDB(userID) if err != nil { return nil, err } // [!code highlight:4] // 写入缓存 if len(tags) > 0 { _ = s.cache.Add(ctx, userID, tags...) } } return tags, nil } // [!code highlight:5] // HasTag 检查用户是否有指定标签 func (s *UserTagsService) HasTag(ctx context.Context, userID string, tag string) (bool, error) { return s.cache.IsMember(ctx, userID, tag) } // [!code highlight:6] // RemoveTags 移除用户标签 func (s *UserTagsService) RemoveTags(ctx context.Context, userID string, tags ...string) error { // 更新数据库 if err := s.removeTagsFromDB(userID, tags...); err != nil { return err } // [!code highlight:2] // 更新缓存 return s.cache.Remove(ctx, userID, tags...) } ``` 使用场景 [#使用场景] 用户标签 [#用户标签] ```go type UserTagsCache interface { xCache.SetCache[string, string] } ``` 适用于: * 用户兴趣标签 * 技能标签 * 分类标签 权限管理 [#权限管理] ```go type PermissionCache interface { xCache.SetCache[string, string] } ``` 适用于: * 用户权限列表 * 角色权限 * 资源访问权限 在线用户 [#在线用户] ```go type OnlineUsersCache interface { xCache.SetCache[string, string] } ``` 适用于: * 在线用户集合 * 活跃用户统计 * 房间成员列表 去重场景 [#去重场景] ```go type UniqueItemsCache interface { xCache.SetCache[string, string] } ``` 适用于: * 已处理的任务 ID * 去重的消息 ID * 唯一的访客 IP 最佳实践 [#最佳实践] 1. 批量添加 [#1-批量添加] 使用可变参数一次添加多个成员: ```go // ✅ 批量添加 cache.Add(ctx, userID, "golang", "redis", "docker") // ❌ 逐个添加 cache.Add(ctx, userID, "golang") cache.Add(ctx, userID, "redis") cache.Add(ctx, userID, "docker") ``` 2. 成员检查 [#2-成员检查] 使用 `IsMember` 而不是 `Members`: ```go // ✅ 高效检查 exists, _ := cache.IsMember(ctx, userID, "golang") // ❌ 低效检查 members, _ := cache.Members(ctx, userID) for _, m := range members { if m == "golang" { exists = true break } } ``` 3. 数量统计 [#3-数量统计] 使用 `Count` 而不是 `Members`: ```go // ✅ 高效统计 count, _ := cache.Count(ctx, userID) // ❌ 低效统计 members, _ := cache.Members(ctx, userID) count := len(members) ``` 4. 空集合处理 [#4-空集合处理] 区分空集合和不存在的集合: ```go func (s *UserTagsService) GetTags(ctx context.Context, userID string) ([]string, error) { tags, err := s.cache.Members(ctx, userID) if err != nil { return nil, err } // [!code highlight:7] // 空切片表示集合存在但为空 // nil 表示集合不存在 if tags == nil { tags = []string{} } return tags, nil } ``` 高级操作 [#高级操作] 集合运算 [#集合运算] 虽然接口不直接支持,但可以使用 Redis 命令: ```go // 交集:获取两个用户的共同标签 func (c *UserTagsCache) CommonTags(ctx context.Context, userID1, userID2 string) ([]string, error) { key1 := "user:tags:" + userID1 key2 := "user:tags:" + userID2 return c.RDB.SInter(ctx, key1, key2).Result() } // 并集:获取两个用户的所有标签 func (c *UserTagsCache) AllTags(ctx context.Context, userID1, userID2 string) ([]string, error) { key1 := "user:tags:" + userID1 key2 := "user:tags:" + userID2 return c.RDB.SUnion(ctx, key1, key2).Result() } // 差集:获取用户1独有的标签 func (c *UserTagsCache) UniqueTags(ctx context.Context, userID1, userID2 string) ([]string, error) { key1 := "user:tags:" + userID1 key2 := "user:tags:" + userID2 return c.RDB.SDiff(ctx, key1, key2).Result() } ``` 随机获取 [#随机获取] 随机获取成员(用于推荐等场景): ```go // 随机获取一个标签 func (c *UserTagsCache) RandomTag(ctx context.Context, userID string) (string, error) { key := "user:tags:" + userID return c.RDB.SRandMember(ctx, key).Result() } // 随机获取多个标签 func (c *UserTagsCache) RandomTags(ctx context.Context, userID string, count int64) ([]string, error) { key := "user:tags:" + userID return c.RDB.SRandMemberN(ctx, key, count).Result() } ``` 移动成员 [#移动成员] 将成员从一个集合移动到另一个: ```go // 将标签从一个用户移动到另一个用户 func (c *UserTagsCache) MoveTag(ctx context.Context, fromUserID, toUserID, tag string) error { fromKey := "user:tags:" + fromUserID toKey := "user:tags:" + toUserID return c.RDB.SMove(ctx, fromKey, toKey, tag).Err() } ``` 性能优化 [#性能优化] 使用 Pipeline [#使用-pipeline] 批量操作多个集合: ```go func (c *UserTagsCache) AddTagsToMultipleUsers(ctx context.Context, userTags map[string][]string) error { pipe := c.RDB.Pipeline() for userID, tags := range userTags { key := "user:tags:" + userID pipe.SAdd(ctx, key, tags) } _, err := pipe.Exec(ctx) return err } ``` 成员数量限制 [#成员数量限制] 避免单个集合成员过多: ```go func (c *UserTagsCache) Add(ctx context.Context, userID string, tags ...string) error { key := "user:tags:" + userID // [!code highlight:5] // 检查成员数量 count, err := c.RDB.SCard(ctx, key).Result() if err != nil { return err } // [!code highlight:3] // 限制最多 100 个标签 if count+int64(len(tags)) > 100 { return errors.New("标签数量超过限制") } return c.RDB.SAdd(ctx, key, tags).Err() } ``` 注意事项 [#注意事项] 1. **无序性**:不要依赖成员的顺序 2. **唯一性**:重复添加相同成员不会报错,但只存储一次 3. **内存占用**:大量成员会占用较多内存 4. **序列化**:复杂类型需要序列化为字符串 5. **过期时间**:整个集合过期,无法为单个成员设置 TTL # 请求上下文 (/docs/bamboo-base-go/components/helper/context) import { TypeTable } from '@/components/type-table'; 请求上下文 [#请求上下文] `RequestContext` 为每个 HTTP 请求生成唯一标识符 (UUID) 并记录请求开始时间,用于请求追踪和性能监控。 RequestContext [#requestcontext] ```go title="context.go" // [!code highlight] func RequestContext() gin.HandlerFunc ``` **实现:** ```go title="context.go" func RequestContext() gin.HandlerFunc { return func(c *gin.Context) { // [!code highlight:2] // 1. 生成请求唯一 ID (UUID) requestID := uuid.NewString() // [!code highlight:2] // 2. 设置响应头 X-Request-UUID c.Writer.Header().Set(xHttp.HeaderRequestUUID.String(), requestID) // [!code highlight:3] // 3. 存储到 Gin Context c.Set(xCtx.RequestKey.String(), requestID) c.Set(xCtx.UserStartTimeKey.String(), time.Now()) // [!code highlight:3] // 4. 注入到标准 context (供 slog 日志使用) ctx := context.WithValue(c.Request.Context(), xCtx.RequestKey, requestID) c.Request = c.Request.WithContext(ctx) c.Next() } } ``` 注入的上下文键 [#注入的上下文键] 响应头 [#响应头] 中间件会在响应头中添加请求 UUID: ``` X-Request-UUID: 550e8400-e29b-41d4-a716-446655440000 ``` 使用示例 [#使用示例] 基础使用 [#基础使用] ```go title="main.go" func main() { router := gin.New() // [!code highlight:2] // 注册请求上下文中间件(应该最先注册) router.Use(xHelper.RequestContext()) router.GET("/api/users", getUsers) router.Run(":8080") } ``` 获取请求 UUID [#获取请求-uuid] ```go title="handler/user.go" func GetUser(ctx *gin.Context) { // [!code highlight:2] // 从 Gin Context 获取 requestID := ctx.GetString(xCtx.RequestKey.String()) // 用于日志记录 xLog.WithName(xLog.NamedCONT).SugarInfo(ctx, "处理用户请求", "request_id", requestID, ) } ``` 计算请求耗时 [#计算请求耗时] ```go title="middleware/timing.go" func TimingMiddleware() gin.HandlerFunc { return func(ctx *gin.Context) { ctx.Next() // [!code highlight:4] // 获取开始时间并计算耗时 if startTime, exists := ctx.Get(xCtx.UserStartTimeKey.String()); exists { elapsed := time.Since(startTime.(time.Time)) xLog.WithName(xLog.NamedMIDE).SugarInfo(ctx, "请求耗时", "elapsed_ms", elapsed.Milliseconds(), ) } } } ``` 传递到 Service 层 [#传递到-service-层] ```go title="handler/user.go" func GetUser(ctx *gin.Context) { // [!code highlight:2] // ctx 已包含 RequestKey,可直接传递 user, err := userService.FindByID(ctx, id) // ... } ``` ```go title="service/user.go" func (s *UserService) FindByID(ctx context.Context, id string) (*entity.User, error) { // [!code highlight:2] // 日志会自动提取 RequestKey 作为 Trace ID xLog.WithName(xLog.NamedSERV).Info(ctx, "查询用户") // [!code highlight:2] // GORM 操作也会继承 Trace ID return s.repo.FindByID(ctx, id) } ``` 工作流程 [#工作流程] 下一步 [#下一步] # HTTP 日志 (/docs/bamboo-base-go/components/helper/http-logger) import { TypeTable } from '@/components/type-table'; HTTP 日志 [#http-日志] `HttpLogger` 提供完整的 HTTP 请求/响应日志记录,支持调试模式下的详细信息记录和敏感数据脱敏。 HttpLogger [#httplogger] ```go title="http_logger.go" // [!code highlight] func HttpLogger() gin.HandlerFunc ``` 日志记录内容 [#日志记录内容] 请求开始阶段 [#请求开始阶段] **基础信息(始终记录):** **调试模式额外信息:** 请求完成阶段 [#请求完成阶段] 日志级别策略 [#日志级别策略] | 状态码范围 | 日志级别 | 说明 | | --------- | ----- | ----- | | 2xx / 3xx | INFO | 成功请求 | | 4xx | WARN | 客户端错误 | | 5xx | ERROR | 服务器错误 | 敏感信息脱敏 [#敏感信息脱敏] 请求头脱敏 [#请求头脱敏] | 处理方式 | 字段 | | ---- | ----------------------------------------------------------------------------- | | 完全移除 | `Set-Cookie` | | 中间隐藏 | `Authorization`, `Cookie`, `Proxy-Authorization`, `X-API-Key`, `Access-Token` | | 原样保留 | 其他字段 | 请求体脱敏 [#请求体脱敏] 自动脱敏的 JSON 字段: ```go sensitiveFields := map[string]bool{ // [!code highlight:6] "password": true, "passwd": true, "token": true, "secret": true, "apikey": true, "api_key": true, "x-api-key": true, "access_token": true, "refresh_token": true, "old_password": true, "new_password": true, "confirm_password": true, } ``` 脱敏规则 [#脱敏规则] ```go title="http_logger.go" func maskSensitive(value string) string { // [!code highlight:2] // 短值(≤6字符):全部隐藏 if len(value) <= 6 { return "******" } // [!code highlight:2] // 长值:保留首尾各3字符 return value[:3] + "..." + value[len(value)-3:] } ``` **示例:** | 原始值 | 脱敏后 | | -------------------------------- | ------------ | | `abc` | `******` | | `mySecretToken123` | `myS...123` | | `Bearer eyJhbGciOiJIUzI1NiIs...` | `Bea...s...` | 支持的 Content-Type [#支持的-content-type] 请求体日志仅记录以下类型: * `application/json` * `application/x-www-form-urlencoded` * `text/xml` * `application/xml` 使用示例 [#使用示例] 基础使用 [#基础使用] ```go title="main.go" func main() { router := gin.New() router.Use(xHelper.RequestContext()) router.Use(xHelper.PanicRecovery()) // [!code highlight:2] // 注册 HTTP 日志中间件 router.Use(xHelper.HttpLogger()) router.Run(":8080") } ``` 日志输出示例 [#日志输出示例] **请求开始:** ``` 2024-01-15 10:30:00.123 [INFO] [abc123] [MIDE] 请求开始 method=POST path=/api/users/login client_ip=192.168.1.100 query= headers={"Content-Type":"application/json","Authorization":"Bea...xyz"} body={"username":"zhangsan","password":"******"} ``` **请求完成:** ``` 2024-01-15 10:30:00.456 [INFO] [abc123] [MIDE] 请求完成 status=200 latency_ms=333.45 client_ip=192.168.1.100 ``` **错误请求:** ``` 2024-01-15 10:30:01.123 [WARN] [def456] [MIDE] 请求完成 status=400 latency_ms=12.34 client_ip=192.168.1.100 ``` 请求体缓存 [#请求体缓存] HttpLogger 会缓存请求体供后续中间件使用: ```go title="http_logger.go" // [!code highlight:2] // 缓存请求体到 Context c.Set("cached_request_body", bodyBytes) ``` **在验证器中使用:** ```go func ValidatorMiddleware() gin.HandlerFunc { return func(ctx *gin.Context) { // [!code highlight:2] // 获取缓存的请求体 if body, exists := ctx.Get("cached_request_body"); exists { // 使用缓存的请求体进行验证 } ctx.Next() } } ``` 下一步 [#下一步] # HTTP 工具 (/docs/bamboo-base-go/components/helper/http) import { TypeTable } from '@/components/type-table'; HTTP 工具 [#http-工具] `xHttp` 提供常见请求头常量与 Token 解析工具,便于在 Gin 请求中统一读取。 ```go import xHttp "github.com/bamboo-services/bamboo-base-go/major/http" ``` 快速使用 [#快速使用] ```go // [!code highlight:2] // 解析 Authorization authorization, err := xHttp.GetAuthorization(c) // [!code highlight:2] // 获取自定义 Token accessToken := xHttp.GetToken(c, xHttp.HeaderAuthorization) ``` Header 类型 [#header-类型] `Header` 是对 HTTP 头字段名称的类型封装,便于复用常用标头。 常用 Header 常量 [#常用-header-常量] 覆盖常见请求头、响应头与自定义头字段: ```go xHttp.HeaderAuthorization xHttp.HeaderRefreshToken xHttp.HeaderRequestUUID xHttp.HeaderContentType xHttp.HeaderXForwardedFor ``` Header 方法 [#header-方法] ```go xHttp.HeaderAuthorization.String() // "Authorization" xHttp.Header("X-Trace-Id").IsCustom() // true xHttp.Header("").IsEmpty() // true ``` * `String()`:返回标准字符串名称 * `IsEmpty()`:判断是否为空 * `IsCustom()`:判断是否为自定义标头(`X-` / `x-` 前缀) GetAuthorization [#getauthorization] 从请求头中解析 `Authorization`,要求为 `Bearer ` 格式: ```go // [!code highlight:3] token, err := xHttp.GetAuthorization(c) if err != nil { return err } ``` **错误场景:** * 缺少 `Authorization` 请求头 * 请求头格式不是 `Bearer ` GetToken [#gettoken] 根据指定的头字段读取 Token(支持 `Authorization` / `X-Refresh-Token`): ```go // [!code highlight:2] accessToken := xHttp.GetToken(c, xHttp.HeaderAuthorization) refreshToken := xHttp.GetToken(c, xHttp.HeaderRefreshToken) ``` **行为说明:** * 非 `HeaderAuthorization` / `HeaderRefreshToken` 会回退到 `HeaderAuthorization` * 识别并剥离 `Bearer ` 前缀 * 不做有效性校验,缺失时返回空字符串 # 概述 (/docs/bamboo-base-go/components/helper) import { TypeTable } from '@/components/type-table'; 辅助中间件 [#辅助中间件] `xHelper` 提供 Gin 框架的辅助中间件,包括请求上下文注入、HTTP 请求日志、Panic 恢复等。 ```go import xHelper "github.com/bamboo-services/bamboo-base-go/major/helper" ``` 中间件列表 [#中间件列表] 快速使用 [#快速使用] ```go title="main.go" func main() { router := gin.New() // [!code highlight:4] // 推荐的注册顺序 router.Use(xHelper.RequestContext()) // 1. 请求上下文(最先) router.Use(xHelper.PanicRecovery()) // 2. Panic 恢复 router.Use(xHelper.HttpLogger()) // 3. HTTP 日志 // 其他中间件... router.Use(xMiddle.ReleaseAllCors) router.Use(xMiddle.AllowOption) router.Use(xMiddle.ResponseMiddleware) router.Run(":8080") } ``` 中间件执行顺序 [#中间件执行顺序] 推荐配置 [#推荐配置] 完整配置 [#完整配置] ```go title="main.go" func main() { router := gin.New() // [!code highlight:4] // 辅助中间件 router.Use(xHelper.RequestContext()) router.Use(xHelper.PanicRecovery()) router.Use(xHelper.HttpLogger()) // [!code highlight:4] // 业务中间件 router.Use(xMiddle.ReleaseAllCors) router.Use(xMiddle.AllowOption) router.Use(xMiddle.ResponseMiddleware) // [!code highlight:2] // 资源注入 router.Use(InjectMiddleware(db, rdb, node)) router.Run(":8080") } ``` 最小配置 [#最小配置] ```go // [!code highlight:3] // 最小必要配置 router.Use(xHelper.RequestContext()) // 请求追踪必需 router.Use(xHelper.PanicRecovery()) // 防止服务崩溃 ``` 与其他组件配合 [#与其他组件配合] 配合 xLog 使用 [#配合-xlog-使用] 辅助中间件使用 `NamedMIDE` 日志标识: ```go // HttpLogger 中的日志 xLog.WithName(xLog.NamedMIDE).Info(ctx, "请求开始", ...) xLog.WithName(xLog.NamedMIDE).Info(ctx, "请求完成", ...) // PanicRecovery 中的日志 xLog.WithName(xLog.NamedMIDE).Error(ctx, "Panic 恢复", ...) ``` 配合 xResult 使用 [#配合-xresult-使用] PanicRecovery 使用 `xBase.BaseResponse` 返回统一格式: ```json { "context": "abc123-uuid", "output": "SERVER_INTERNAL_ERROR", "code": 50000, "message": "服务器内部错误", "error_message": "具体错误描述" } ``` 下一步 [#下一步] # Panic 恢复 (/docs/bamboo-base-go/components/helper/panic-recovery) import { TypeTable } from '@/components/type-table'; Panic 恢复 [#panic-恢复] `PanicRecovery` 全局 Panic 捕获与恢复中间件,防止服务因未处理的异常而崩溃,并返回统一的 JSON 错误响应。 PanicRecovery [#panicrecovery] ```go title="panic_recovery.go" // [!code highlight] func PanicRecovery() gin.HandlerFunc ``` **实现:** ```go title="panic_recovery.go" func PanicRecovery() gin.HandlerFunc { return gin.RecoveryWithWriter(io.Discard, func(c *gin.Context, recovered interface{}) { // [!code highlight:2] // 1. 从上下文提取错误码 value, exists := c.Get(xCtx.ErrorCodeKey.String()) getErrMessage, msgExist := c.Get(xCtx.ErrorMessageKey.String()) // [!code highlight:2] // 2. 默认错误码: ServerInternalError (50000) errorCode := xError.ServerInternalError if exists && value != nil { if ec, ok := value.(*xError.ErrorCode); ok && ec != nil { errorCode = ec } } // [!code highlight:2] // 3. 默认错误消息 if !msgExist { getErrMessage = "未知错误,请稍后再试" } // [!code highlight:2] // 4. 记录日志 xLog.WithName(xLog.NamedMIDE).Error(c.Request.Context(), "Panic 恢复", ...) // [!code highlight:2] // 5. 返回统一 JSON 响应 c.JSON(int(errorCode.Code/100), xBase.BaseResponse{...}) c.Abort() }) } ``` 错误码提取 [#错误码提取] 中间件会尝试从上下文获取错误信息: 响应格式 [#响应格式] ```json { "context": "abc123-uuid", "output": "SERVER_INTERNAL_ERROR", "code": 50000, "message": "服务器内部错误", "error_message": "具体错误描述" } ``` 响应字段说明 [#响应字段说明] 使用示例 [#使用示例] 基础使用 [#基础使用] ```go title="main.go" func main() { router := gin.New() router.Use(xHelper.RequestContext()) // [!code highlight:2] // 注册 Panic 恢复中间件 router.Use(xHelper.PanicRecovery()) router.Use(xHelper.HttpLogger()) router.Run(":8080") } ``` 测试 Panic 恢复 [#测试-panic-恢复] ```go title="handler/test.go" func TestPanic(ctx *gin.Context) { // [!code highlight:2] // 触发 Panic panic("测试 Panic 恢复") } ``` **响应:** ```json { "context": "550e8400-e29b-41d4-a716-446655440000", "output": "SERVER_INTERNAL_ERROR", "code": 50000, "message": "服务器内部错误", "error_message": "未知错误,请稍后再试" } ``` 带错误码的 Panic [#带错误码的-panic] ```go title="handler/user.go" func GetUser(ctx *gin.Context) { // [!code highlight:3] // 设置错误码后再 Panic ctx.Set(xCtx.ErrorCodeKey.String(), xError.NotFound) ctx.Set(xCtx.ErrorMessageKey.String(), "用户不存在") panic("user not found") } ``` **响应:** ```json { "context": "550e8400-e29b-41d4-a716-446655440000", "output": "NOT_EXIST", "code": 40004, "message": "数据不存在", "error_message": "用户不存在" } ``` 日志输出 [#日志输出] Panic 发生时会记录 ERROR 级别日志: ``` 2024-01-15 10:30:00.123 [ERRO] [abc123] [MIDE] Panic 恢复 code=50000 output=SERVER_INTERNAL_ERROR message=服务器内部错误 errorMessage=未知错误,请稍后再试 goroutine 1 [running]: ... ``` 工作流程 [#工作流程] 注意事项 [#注意事项] 中间件顺序 [#中间件顺序] ```go // [!code highlight:3] // PanicRecovery 应该在 HttpLogger 之前 router.Use(xHelper.RequestContext()) router.Use(xHelper.PanicRecovery()) // 先注册 router.Use(xHelper.HttpLogger()) // 后注册 ``` 与 xError 配合 [#与-xerror-配合] 推荐使用 `xError.NewError` 而不是直接 Panic: ```go // [!code highlight:3] // 推荐:使用 xError ctx.Error(xError.NewError(ctx.Request.Context(), xError.NotFound, "用户不存在", true)) return // [!code highlight:2] // 不推荐:直接 Panic panic("user not found") ``` 下一步 [#下一步] # GORM 集成 (/docs/bamboo-base-go/components/log/gorm) import { TypeTable } from '@/components/type-table'; GORM 集成 [#gorm-集成] `SlogLogger` 是 GORM 日志适配器,实现 `gorm.io/gorm/logger.Interface` 接口,将数据库操作日志输出到 slog。 SlogLogger [#sloglogger] ```go title="gorm.go" type SlogLogger struct { logger *slog.Logger config GormLoggerConfig slowThreshold time.Duration logLevel LogLevel ignoreRecordNotFoundError bool } ``` GormLoggerConfig [#gormloggerconfig] ```go title="gorm.go" // [!code highlight:6] type GormLoggerConfig struct { SlowThreshold int // 慢查询阈值(毫秒),超过此值以 WARN 级别记录 LogLevel LogLevel // 日志级别 IgnoreRecordNotFoundError bool // 是否忽略 ErrRecordNotFound 错误 Colorful bool // 是否启用彩色输出(预留字段) } ``` 配置说明 [#配置说明] 日志级别 [#日志级别] ```go title="gorm.go" type LogLevel int const ( // [!code highlight:4] LevelSilent LogLevel = iota + 1 // 静默模式,不输出任何日志 LevelError // 仅输出错误日志 LevelWarn // 输出错误和警告日志 LevelInfo // 输出所有级别日志(包括 SQL 语句) ) ``` NewSlogLogger [#newsloglogger] 创建 GORM slog 日志适配器: ```go title="gorm.go" // [!code highlight] func NewSlogLogger(slogger *slog.Logger, config GormLoggerConfig) logger.Interface ``` **参数说明:** * `slogger`: slog.Logger 实例,推荐使用 `slog.Default().WithGroup(xLog.NamedREPO)` * `config`: GORM Logger 配置 **示例:** ```go // [!code highlight:6] gormLogger := xLog.NewSlogLogger( slog.Default().WithGroup(xLog.NamedREPO), xLog.GormLoggerConfig{ SlowThreshold: 200, // 200ms 慢查询阈值 LogLevel: xLog.LevelInfo, // 日志级别 IgnoreRecordNotFoundError: true, // 忽略记录未找到错误 }, ) ``` 日志输出规则 [#日志输出规则] SQL 追踪 [#sql-追踪] `Trace` 方法根据执行情况输出不同级别的日志: ```go title="gorm.go" func (l *SlogLogger) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) { switch { // [!code highlight:2] // 执行出错 → ERROR 级别 case err != nil && l.logLevel >= LevelError: l.logger.LogAttrs(ctx, slog.LevelError, "执行SQL语句失败", ...) // [!code highlight:2] // 慢查询 → WARN 级别 case elapsed > l.slowThreshold && l.logLevel >= LevelWarn: l.logger.LogAttrs(ctx, slog.LevelWarn, "发现SQL慢查询", ...) // [!code highlight:2] // 普通查询 → DEBUG 级别 case l.logLevel >= LevelInfo: l.logger.LogAttrs(ctx, slog.LevelDebug, "成功执行SQL语句", ...) } } ``` 日志属性 [#日志属性] 每条 SQL 日志包含以下属性: 使用示例 [#使用示例] 基础集成 [#基础集成] ```go title="register_database.go" func (r *Reg) DatabaseInit() { dsn := "host=localhost user=postgres dbname=myapp" // [!code highlight:7] // 创建 GORM Logger gormLogger := xLog.NewSlogLogger( slog.Default().WithGroup(xLog.NamedREPO), xLog.GormLoggerConfig{ SlowThreshold: 200, LogLevel: xLog.LevelInfo, IgnoreRecordNotFoundError: true, }, ) // [!code highlight:4] // 连接数据库 db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ Logger: gormLogger, }) if err != nil { panic(err) } } ``` 环境区分 [#环境区分] ```go func createGormLogger(isDebug bool) logger.Interface { config := xLog.GormLoggerConfig{ SlowThreshold: 200, IgnoreRecordNotFoundError: true, } // [!code highlight:5] if isDebug { config.LogLevel = xLog.LevelInfo // 开发环境:输出所有 SQL } else { config.LogLevel = xLog.LevelWarn // 生产环境:仅输出警告和错误 } return xLog.NewSlogLogger( slog.Default().WithGroup(xLog.NamedREPO), config, ) } ``` 链路追踪 [#链路追踪] GORM 操作自动继承 Context 中的 Trace ID: ```go title="repository/user.go" func (r *UserRepo) FindByID(ctx context.Context, id int64) (*entity.User, error) { var user entity.User // [!code highlight:2] // WithContext 传递 Trace ID,日志会自动关联 err := r.db.WithContext(ctx).First(&user, id).Error return &user, err } ``` **日志输出:** ``` 2024-01-15 10:30:00.123 [DEBU] [abc123] [REPO] 成功执行SQL语句 elapsed_ms=1.234 rows=1 sql=SELECT * FROM users WHERE id = 12345 LIMIT 1 ``` 慢查询监控 [#慢查询监控] ```go // [!code highlight:4] // 设置较低的阈值以捕获更多慢查询 gormLogger := xLog.NewSlogLogger( slog.Default().WithGroup(xLog.NamedREPO), xLog.GormLoggerConfig{ SlowThreshold: 100, // 100ms LogLevel: xLog.LevelWarn, }, ) ``` **慢查询日志:** ``` 2024-01-15 10:30:00.123 [WARN] [abc123] [REPO] 发现SQL慢查询 elapsed_ms=156.789 rows=1000 sql=SELECT * FROM orders WHERE created_at > '2024-01-01' threshold=100ms ``` 完整示例 [#完整示例] ```go title="startup/startup_database.go" package startup import ( "log/slog" "os" xLog "github.com/bamboo-services/bamboo-base-go/common/log" "gorm.io/driver/postgres" "gorm.io/gorm" ) func InitDatabase() *gorm.DB { dsn := os.Getenv("DATABASE_URL") isDebug := os.Getenv("APP_ENV") == "development" // [!code highlight:2] // 根据环境配置日志级别 logLevel := xLog.LevelWarn if isDebug { logLevel = xLog.LevelInfo } // [!code highlight:7] gormLogger := xLog.NewSlogLogger( slog.Default().WithGroup(xLog.NamedREPO), xLog.GormLoggerConfig{ SlowThreshold: 200, LogLevel: logLevel, IgnoreRecordNotFoundError: true, }, ) // [!code highlight:3] db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ Logger: gormLogger, }) if err != nil { panic(err) } return db } ``` 下一步 [#下一步] # 自定义 Handler (/docs/bamboo-base-go/components/log/handler) import { TypeTable } from '@/components/type-table'; 自定义 Handler [#自定义-handler] `LogHandler` 是基于 `slog.Handler` 接口的自定义实现,支持控制台彩色输出和文件 JSON 输出双通道。 LogHandler [#loghandler] ```go title="handler.go" type LogHandler struct { opts slog.HandlerOptions mu *sync.Mutex console io.Writer file io.Writer group string // logger 名称(通过 WithGroup 设置) attrs []slog.Attr isDebugMode bool } ``` HandlerConfig [#handlerconfig] ```go title="handler.go" // [!code highlight:6] type HandlerConfig struct { Console io.Writer // 控制台输出(可选,默认 os.Stdout) File io.Writer // 文件输出(可选) Level slog.Level // 日志级别 IsDebugMode bool // 是否调试模式 } ``` 配置说明 [#配置说明] NewLogHandler [#newloghandler] 创建自定义 slog Handler: ```go title="handler.go" // [!code highlight] func NewLogHandler(config HandlerConfig) slog.Handler ``` **示例:** ```go // [!code highlight:7] handler := xLog.NewLogHandler(xLog.HandlerConfig{ Console: os.Stdout, File: rotator, // RotatingWriter 实例 Level: slog.LevelDebug, IsDebugMode: true, }) // [!code highlight] slog.SetDefault(slog.New(handler)) ``` 双通道输出 [#双通道输出] 控制台输出 [#控制台输出] 彩色格式,便于开发调试: ``` 2024-01-15 10:30:00.123 [INFO] [abc123] [CORE] [DB] [Redis] >> 服务启动成功 config=production port=8080 ``` **格式说明:** | 部分 | 说明 | 颜色 | | --------- | ----------------------------------- | ----------- | | 时间戳 | `2006-01-02 15:04:05.000` | 灰色 | | 日志级别 | `[DEBU]` `[INFO]` `[WARN]` `[ERRO]` | 见下表 | | Trace ID | 请求追踪标识 | 蓝色 | | Logger 名称 | 主模块标识 | 按类别着色 | | Option 名称 | 额外命名空间 | 青色(高亮) | | 消息 | 日志内容 | 默认色,前缀 `>>` | | 属性 | key=value 格式 | 棕色 | 文件输出 [#文件输出] JSON 格式,便于日志分析: ```json { "time": "2024-01-15T10:30:00.123456789+08:00", "level": "INFO", "message": "服务启动成功", "trace": "abc123", "logger": "CORE", "config": "production", "port": 8080 } ``` 日志级别颜色 [#日志级别颜色] Logger 名称颜色 [#logger-名称颜色] 不同类别的命名常量显示不同颜色: | 类别 | 颜色 | 包含常量 | | ----- | -- | ---------------------------------------------- | | 核心服务类 | 蓝色 | CONT, SERV, LOGC, REPO, CORE, BASE, MAIN | | 路由网络类 | 黄色 | ROUT, HTTP, GRPC, SOCK, CONN, LINK | | 安全认证类 | 红色 | AUTH, USER, PERM, ROLE, TOKN, SIGN | | 业务逻辑类 | 白色 | BUSI, PROC, FLOW, TASK, JOBS | | 其他已定义 | 橙色 | RECO, UTIL, FILT, MIDE, VALD, INIT, THOW, RESU | | 未定义 | 紫色 | 其他自定义名称 | Trace ID 提取 [#trace-id-提取] Handler 自动从 Context 中提取 Trace ID: ```go title="handler.go" func (h *LogHandler) extractContextUUID(ctx context.Context) string { // [!code highlight:3] // 1. 从 gin.Context 提取 if ginCtx, ok := ctx.(*gin.Context); ok { if contextUUID, exists := ginCtx.Get(string(xCtx.RequestKey)); exists { return contextUUID.(string) } } // [!code highlight:2] // 2. 从标准 context.Context 提取(包括 GORM 场景) if contextUUID := ctx.Value(xCtx.RequestKey); contextUUID != nil { return contextUUID.(string) } return "" } ``` **支持的 Context 类型:** * `*gin.Context` - HTTP 请求场景 * `context.Context` - 标准 Context(包括 GORM 数据库操作) 错误堆栈 [#错误堆栈] ERROR 级别日志自动附加堆栈信息: ```go // [!code highlight:4] // 错误级别添加堆栈 if r.Level >= slog.LevelError { buf.WriteString(h.getStack()) } ``` **输出示例:** ``` 2024-01-15 10:30:00.123 [ERRO] [abc123] [REPO] 数据库连接失败 error=connection refused goroutine 1 [running]: github.com/bamboo-services/bamboo-base-go/log.(*LogHandler).getStack(...) /path/to/handler.go:310 ... ``` 完整初始化示例 [#完整初始化示例] ```go title="register_logger.go" func (r *Reg) LoggerInit() { // [!code highlight:7] // 1. 创建日志切割器 rotator, err := xLog.NewRotatingWriter(xLog.RotatorConfig{ Dir: ".logs", BaseName: "log", Ext: ".log", MaxSize: 10 * 1024 * 1024, }) if err != nil { panic(err) } // [!code highlight:7] // 2. 创建自定义 Handler handler := xLog.NewLogHandler(xLog.HandlerConfig{ Console: os.Stdout, File: rotator, Level: slog.LevelDebug, IsDebugMode: true, }) // [!code highlight:2] // 3. 设置为全局默认 Logger slog.SetDefault(slog.New(handler)) } ``` 下一步 [#下一步] # 概述 (/docs/bamboo-base-go/components/log) import { TypeTable } from '@/components/type-table'; 日志系统 [#日志系统] `xLog` 是基于 Go 标准库 `log/slog` 构建的企业级日志库,提供彩色控制台输出、JSON 文件记录、自动切割归档、链路追踪等功能。 ```go import xLog "github.com/bamboo-services/bamboo-base-go/common/log" ``` 核心特性 [#核心特性] | 特性 | 说明 | | ------- | --------------- | | 双通道输出 | 控制台彩色 + 文件 JSON | | 命名日志器 | 按模块分类,便于过滤 | | 链路追踪 | 自动提取 Trace ID | | 自动切割 | 按大小切割,按天归档 | | GORM 集成 | 数据库日志适配器 | 快速使用 [#快速使用] 命名日志器 [#命名日志器] ```go // [!code highlight:2] // 创建带名称的日志器 log := xLog.WithName(xLog.NamedCORE) // [!code highlight] log.Info(ctx, "服务启动成功") log.Debug(ctx, "调试信息") log.Error(ctx, "发生错误") ``` 全局函数 [#全局函数] ```go // [!code highlight:2] // 标准方式 xLog.Info(ctx, "用户登录", slog.String("user_id", "12345")) // [!code highlight:2] // Sugar 语法糖(key-value 形式) xLog.SugarInfo(ctx, "用户登录", "user_id", "12345", "ip", "192.168.1.1") ``` 带属性的日志 [#带属性的日志] ```go log := xLog.WithName(xLog.NamedAUTH) // [!code highlight:4] // 使用 slog.Attr log.Info(ctx, "登录成功", slog.String("user_id", "12345"), slog.String("ip", "192.168.1.1"), ) // [!code highlight:2] // 使用 Sugar 语法糖 log.SugarInfo(ctx, "登录成功", "user_id", "12345", "ip", "192.168.1.1") ``` 日志级别 [#日志级别] | 级别 | 方法 | 说明 | | ------ | ------------------------ | ------------- | | DEBUG | `Debug` / `SugarDebug` | 调试信息,仅调试模式输出 | | INFO | `Info` / `SugarInfo` | 一般信息 | | NOTICE | `Notice` / `SugarNotice` | 重要信息 (INFO+1) | | WARN | `Warn` / `SugarWarn` | 警告信息 | | ERROR | `Error` / `SugarError` | 错误信息 | | PANIC | `Panic` / `SugarPanic` | 错误并触发 panic | 输出格式 [#输出格式] 控制台输出 [#控制台输出] 彩色格式,便于开发调试: ``` 2024-01-15 10:30:00 [INFO] [abc123] [CORE] 服务启动成功 2024-01-15 10:30:01 [DEBUG] [abc123] [AUTH] 验证用户令牌 2024-01-15 10:30:02 [ERROR] [abc123] [REPO] 数据库连接失败 ``` 文件输出 [#文件输出] JSON 格式,便于日志分析: ```json { "time": "2024-01-15T10:30:00.000+08:00", "level": "INFO", "message": "服务启动成功", "trace": "abc123", "logger": "CORE" } ``` 初始化示例 [#初始化示例] ```go title="register_logger.go" func (r *Reg) LoggerInit() { // [!code highlight:6] // 1. 创建日志切割器 rotator, err := xLog.NewRotatingWriter(xLog.RotatorConfig{ Dir: ".logs", BaseName: "log", Ext: ".log", MaxSize: 10 * 1024 * 1024, // 10MB }) if err != nil { panic(err) } // [!code highlight:7] // 2. 创建自定义 Handler handler := xLog.NewLogHandler(xLog.HandlerConfig{ Console: os.Stdout, File: rotator, Level: slog.LevelDebug, IsDebugMode: true, }) // [!code highlight:2] // 3. 设置为全局默认 Logger slog.SetDefault(slog.New(handler)) } ``` API 一览 [#api-一览] 下一步 [#下一步] # 命名常量 (/docs/bamboo-base-go/components/log/named) import { TypeTable } from '@/components/type-table'; 命名常量 [#命名常量] `xLog` 提供了丰富的预定义命名常量,用于按模块分类管理日志。所有常量均为 4 字符,便于日志输出对齐。 LogNamedLogger [#lognamedlogger] ```go title="command.go" type LogNamedLogger struct { logger *slog.Logger } ``` WithName [#withname] 创建带名称的日志器,支持追加多个命名空间: ```go title="command.go" // [!code highlight] // optionName 会按顺序追加到 logger 名称后显示,如 [CORE] [DB] func WithName(name string, optionName ...string) *LogNamedLogger ``` **示例:** ```go // [!code highlight] log := xLog.WithName(xLog.NamedCORE) log.Info(ctx, "服务启动成功") // 输出: 2024-01-15 10:30:00 [INFO] [abc123] [CORE] >> 服务启动成功 // [!code highlight:2] // 带多个命名空间 dbLog := xLog.WithName(xLog.NamedCORE, "DB", "Redis") dbLog.Info(ctx, "数据库连接成功") // 输出: 2024-01-15 10:30:00 [INFO] [abc123] [CORE] [DB] [Redis] >> 数据库连接成功 ``` 四层架构类 [#四层架构类] 用于标准分层架构: **示例:** ```go // [!code highlight:4] // Handler 层 handlerLog := xLog.WithName(xLog.NamedCONT) // Service 层 serviceLog := xLog.WithName(xLog.NamedSERV) // Repository 层 repoLog := xLog.WithName(xLog.NamedREPO) ``` 业务逻辑类 [#业务逻辑类] 用于业务处理模块: 核心服务类 [#核心服务类] 用于核心模块: 网络通信类 [#网络通信类] 用于网络相关模块: 安全认证类 [#安全认证类] 用于安全相关模块: 通用工具类 [#通用工具类] 用于工具和中间件: 控制台颜色 [#控制台颜色] 不同类别的命名常量在控制台显示不同颜色: | 类别 | 颜色 | 包含常量 | | ----- | -- | ---------------------------------------------- | | 核心服务类 | 蓝色 | CONT, SERV, LOGC, REPO, CORE, BASE, MAIN | | 路由网络类 | 黄色 | ROUT, HTTP, GRPC, SOCK, CONN, LINK | | 安全认证类 | 红色 | AUTH, USER, PERM, ROLE, TOKN, SIGN | | 业务逻辑类 | 白色 | BUSI, PROC, FLOW, TASK, JOBS | | 其他已定义 | 橙色 | RECO, UTIL, FILT, MIDE, VALD, INIT, THOW, RESU | | 未定义 | 紫色 | 其他自定义名称 | 使用示例 [#使用示例] ```go title="handler/user.go" package handler type UserHandler struct { // [!code highlight] log *xLog.LogNamedLogger } func NewUserHandler() *UserHandler { return &UserHandler{ // [!code highlight] log: xLog.WithName(xLog.NamedCONT), } } func (h *UserHandler) Login(ctx *gin.Context) { // [!code highlight] h.log.Info(ctx, "开始处理登录请求") // ... 业务逻辑 ... // [!code highlight] h.log.SugarInfo(ctx, "登录成功", "user_id", userID) } ``` ```go title="logic/user.go" package logic type UserLogic struct { // [!code highlight] log *xLog.LogNamedLogger } func NewUserLogic() *UserLogic { return &UserLogic{ // [!code highlight] log: xLog.WithName(xLog.NamedLOGC), } } ``` 下一步 [#下一步] # 日志切割 (/docs/bamboo-base-go/components/log/rotator) import { TypeTable } from '@/components/type-table'; 日志切割 [#日志切割] `RotatingWriter` 是支持自动切割的日志写入器,实现 `io.Writer` 接口,当文件大小超过阈值时自动切割,并在每天凌晨自动归档前一天的日志。 RotatingWriter [#rotatingwriter] ```go title="rotator.go" type RotatingWriter struct { mu sync.Mutex file *os.File // 当前写入的文件 dir string // 日志目录 baseName string // 基础文件名 ext string // 扩展名 maxSize int64 // 最大文件大小 currentSize int64 // 当前文件大小 currentDate string // 当前日期 } ``` RotatorConfig [#rotatorconfig] ```go title="rotator.go" // [!code highlight:6] type RotatorConfig struct { Dir string // 日志目录 BaseName string // 基础文件名 (如 "log") Ext string // 扩展名 (如 ".log") MaxSize int64 // 最大文件大小 (字节),默认 10MB } ``` 配置说明 [#配置说明] NewRotatingWriter [#newrotatingwriter] 创建日志切割写入器: ```go title="rotator.go" // [!code highlight] func NewRotatingWriter(config RotatorConfig) (*RotatingWriter, error) ``` **示例:** ```go // [!code highlight:6] rotator, err := xLog.NewRotatingWriter(xLog.RotatorConfig{ Dir: ".logs", BaseName: "log", Ext: ".log", MaxSize: 10 * 1024 * 1024, // 10MB }) if err != nil { panic(err) } ``` 文件命名规则 [#文件命名规则] 当前日志文件 [#当前日志文件] ``` .logs/log.log # 当前正在写入的文件 ``` 切割后的文件 [#切割后的文件] ``` .logs/log.0.log # 最早的切割文件 .logs/log.1.log # 较新的切割文件 .logs/log.2.log # 最新的切割文件 ``` **规则:** 索引数字越大,文件越新。 归档文件 [#归档文件] ``` .logs/logger-2024-01-14.tar.gz # 2024-01-14 的归档 .logs/logger-2024-01-15.tar.gz # 2024-01-15 的归档 ``` 切割流程 [#切割流程] **切割逻辑:** ```go title="rotator.go" func (w *RotatingWriter) rotate() error { // [!code highlight:2] // 1. 关闭当前文件 w.file.Close() // [!code highlight:2] // 2. 查找最大索引 maxIndex := w.findMaxRotatedIndex() nextIndex := maxIndex + 1 // [!code highlight:2] // 3. 重命名当前文件 os.Rename(w.currentFilePath(), w.rotatedFilePath(nextIndex)) // [!code highlight:2] // 4. 创建新文件 w.currentSize = 0 return w.openFile() } ``` 归档流程 [#归档流程] 每天 00:00:05 自动执行归档: ```mermaid flowchart TD A[00:00:05 触发] --> B{归档文件已存在?} B -->|是| C[跳过] B -->|否| D[收集切割文件] D --> E{有文件需要归档?} E -->|否| C E -->|是| F[创建 tar.gz] F --> G[删除已归档文件] ``` **归档逻辑:** ```go title="rotator.go" func (w *RotatingWriter) archiveYesterday() { // [!code highlight:2] // 归档文件名: logger-yyyy-MM-dd.tar.gz yesterday := time.Now().AddDate(0, 0, -1).Format("2006-01-02") archiveName := fmt.Sprintf("logger-%s.tar.gz", yesterday) // [!code highlight:2] // 收集需要归档的文件 (log.0.log, log.1.log, ...) files := w.collectFilesToArchive() // [!code highlight:2] // 创建 tar.gz 归档 w.createTarGz(archivePath, files) // [!code highlight:2] // 删除已归档的文件 for _, file := range files { os.Remove(file) } } ``` 目录结构示例 [#目录结构示例] ``` .logs/ ├── log.log # 当前日志文件 ├── log.0.log # 切割文件 (今天) ├── log.1.log # 切割文件 (今天) ├── logger-2024-01-13.tar.gz # 归档 (前天) └── logger-2024-01-14.tar.gz # 归档 (昨天) ``` 使用示例 [#使用示例] 基础使用 [#基础使用] ```go // [!code highlight:5] rotator, err := xLog.NewRotatingWriter(xLog.RotatorConfig{ Dir: ".logs", BaseName: "app", MaxSize: 5 * 1024 * 1024, // 5MB }) if err != nil { panic(err) } defer rotator.Close() // [!code highlight:2] // 配合 LogHandler 使用 handler := xLog.NewLogHandler(xLog.HandlerConfig{ Console: os.Stdout, File: rotator, Level: slog.LevelInfo, }) ``` 自定义配置 [#自定义配置] ```go // [!code highlight:7] // 大型应用:更大的文件和更频繁的切割 rotator, _ := xLog.NewRotatingWriter(xLog.RotatorConfig{ Dir: "/var/log/myapp", BaseName: "service", Ext: ".json", MaxSize: 50 * 1024 * 1024, // 50MB }) ``` ```go // [!code highlight:7] // 小型应用:较小的文件 rotator, _ := xLog.NewRotatingWriter(xLog.RotatorConfig{ Dir: ".logs", BaseName: "debug", Ext: ".log", MaxSize: 1 * 1024 * 1024, // 1MB }) ``` 线程安全 [#线程安全] `RotatingWriter` 使用 `sync.Mutex` 保证线程安全: ```go title="rotator.go" func (w *RotatingWriter) Write(p []byte) (n int, err error) { // [!code highlight:2] w.mu.Lock() defer w.mu.Unlock() // 检查是否需要切割 if w.currentSize+int64(len(p)) > w.maxSize { w.rotate() } // 写入数据 n, err = w.file.Write(p) w.currentSize += int64(n) return n, err } ``` 下一步 [#下一步] # CORS 跨域 (/docs/bamboo-base-go/components/middleware/cors) import { TypeTable } from '@/components/type-table'; CORS 跨域 [#cors-跨域] `xMiddle` 提供两个中间件处理跨域请求:`ReleaseAllCors` 设置 CORS 头部,`AllowOption` 处理预检请求。 ReleaseAllCors [#releaseallcors] 设置跨域请求的 HTTP 头部信息,允许所有来源: ```go title="cors.go" // [!code highlight] func ReleaseAllCors(ctx *gin.Context) ``` **实现:** ```go title="cors.go" func ReleaseAllCors(ctx *gin.Context) { // [!code highlight:3] ctx.Writer.Header().Set("Access-Control-Allow-Origin", "*") ctx.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") ctx.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") ctx.Next() } ``` 设置的头部 [#设置的头部] AllowOption [#allowoption] 处理 CORS 预检请求(OPTIONS 方法),直接返回 200 状态码并终止请求链: ```go title="option.go" // [!code highlight] func AllowOption(ctx *gin.Context) ``` **实现:** ```go title="option.go" func AllowOption(ctx *gin.Context) { // [!code highlight:4] if ctx.Request.Method == "OPTIONS" { xLog.WithName(xLog.NamedMIDE).Debug(ctx, "检测到 OPTIONS 请求,继续处理") ctx.AbortWithStatus(200) } } ``` **特性:** * 检测请求方法是否为 `OPTIONS` * 记录 Debug 级别日志(使用 `NamedMIDE` 标识) * 返回 HTTP 200 并终止后续中间件执行 使用示例 [#使用示例] 基础使用 [#基础使用] ```go title="main.go" func main() { router := gin.Default() // [!code highlight:3] // 先设置 CORS 头,再处理 OPTIONS router.Use(xMiddle.ReleaseAllCors) router.Use(xMiddle.AllowOption) router.GET("/api/users", getUsers) router.Run(":8080") } ``` 执行流程 [#执行流程] 自定义 CORS [#自定义-cors] 生产环境建议自定义 CORS 配置: ```go title="middleware/custom_cors.go" func CustomCors(allowedOrigins []string) gin.HandlerFunc { return func(ctx *gin.Context) { origin := ctx.Request.Header.Get("Origin") // [!code highlight:5] // 检查是否在允许列表中 for _, allowed := range allowedOrigins { if origin == allowed { ctx.Writer.Header().Set("Access-Control-Allow-Origin", origin) break } } ctx.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") ctx.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") // [!code highlight] ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true") ctx.Next() } } ``` **使用:** ```go // [!code highlight:4] router.Use(CustomCors([]string{ "https://example.com", "https://app.example.com", })) router.Use(xMiddle.AllowOption) ``` 注意事项 [#注意事项] 中间件顺序 [#中间件顺序] ```go // [!code highlight:3] // 正确顺序:先 CORS,后 OPTIONS router.Use(xMiddle.ReleaseAllCors) // 1. 设置头部 router.Use(xMiddle.AllowOption) // 2. 处理预检 ``` 开发 vs 生产 [#开发-vs-生产] | 环境 | 推荐配置 | | ---- | ------------------------ | | 开发环境 | `ReleaseAllCors`(允许所有来源) | | 生产环境 | 自定义 CORS(限制允许的来源) | 携带凭证 [#携带凭证] 如果需要携带 Cookie,需要自定义 CORS: ```go // [!code highlight:3] // ReleaseAllCors 使用 "*",不支持 credentials // 需要自定义中间件设置具体的 Origin ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true") ``` 下一步 [#下一步] # 概述 (/docs/bamboo-base-go/components/middleware) import { TypeTable } from '@/components/type-table'; 中间件 [#中间件] `xMiddle` 提供 Gin 框架常用的中间件,包括 CORS 跨域处理、OPTIONS 预检请求、统一响应处理等。 ```go import xMiddle "github.com/bamboo-services/bamboo-base-go/major/middleware" ``` 中间件列表 [#中间件列表] 快速使用 [#快速使用] ```go title="main.go" func main() { router := gin.Default() // [!code highlight:4] // 按顺序注册中间件 router.Use(xMiddle.ReleaseAllCors) // 1. 设置 CORS 头 router.Use(xMiddle.AllowOption) // 2. 处理 OPTIONS 请求 router.Use(xMiddle.ResponseMiddleware) // 3. 统一响应处理 // 路由定义... router.Run(":8080") } ``` 中间件执行顺序 [#中间件执行顺序] 推荐配置 [#推荐配置] 开发环境 [#开发环境] ```go // [!code highlight:4] // 开发环境:全开放 CORS router.Use(xMiddle.ReleaseAllCors) router.Use(xMiddle.AllowOption) router.Use(xMiddle.ResponseMiddleware) ``` 生产环境 [#生产环境] ```go // [!code highlight:2] // 生产环境:建议自定义 CORS 配置 router.Use(customCorsMiddleware()) // 自定义 CORS router.Use(xMiddle.AllowOption) router.Use(xMiddle.ResponseMiddleware) ``` 与其他组件配合 [#与其他组件配合] 配合 xResult 使用 [#配合-xresult-使用] ```go title="handler/user.go" func GetUser(ctx *gin.Context) { user, err := userService.FindByID(ctx, id) if err != nil { // [!code highlight:2] // 使用 xError 抛出错误,ResponseMiddleware 自动处理 ctx.Error(xError.NewError(ctx.Request.Context(), xError.NotFound, "用户不存在", true)) return } // [!code highlight:2] // 使用 xResult 输出成功响应 xResult.Success(ctx, user) } ``` 配合 xLog 使用 [#配合-xlog-使用] 中间件内部使用 `NamedMIDE` 日志标识: ```go // AllowOption 中的日志 xLog.WithName(xLog.NamedMIDE).Debug(ctx, "检测到 OPTIONS 请求,继续处理") ``` 下一步 [#下一步] # 统一响应 (/docs/bamboo-base-go/components/middleware/response) import { TypeTable } from '@/components/type-table'; 统一响应 [#统一响应] `ResponseMiddleware` 在请求处理完成后,统一检查和处理响应输出,确保所有请求都有正确的响应格式。 ResponseMiddleware [#responsemiddleware] ```go title="response.go" // [!code highlight] func ResponseMiddleware(ctx *gin.Context) ``` **实现:** ```go title="response.go" func ResponseMiddleware(ctx *gin.Context) { // [!code highlight] ctx.Next() // 先执行后续处理器 // [!code highlight:2] // 检查响应是否已写入 if !ctx.Writer.Written() { // 情况1: 存在错误 if ctx.Errors != nil && len(ctx.Errors) > 0 { var getErr *xError.Error // [!code highlight:3] // 尝试解析为自定义错误类型 if errors.As(ctx.Errors.Last(), &getErr) && getErr.ErrorCode != nil { xResult.Error(ctx, getErr.ErrorCode, getErr.ErrorMessage, getErr.Data) } else { // 通用服务器内部错误 xResult.Error(ctx, xError.ServerInternalError, ...) } ctx.Abort() } else { // [!code highlight:2] // 情况2: 无错误但也无响应(排除重定向) if ctx.Writer.Status() != 301 && ctx.Writer.Status() != 302 { xResult.Error(ctx, xError.DeveloperError, "没有正常输出信息或报错信息,请检查代码逻辑「开发者错误」", nil) ctx.Abort() } } } } ``` 处理流程 [#处理流程] 三种处理场景 [#三种处理场景] 场景一:正常响应 [#场景一正常响应] Handler 已调用 `xResult.Success()` 输出响应: ```go title="handler/user.go" func GetUser(ctx *gin.Context) { user := userService.FindByID(ctx, id) // [!code highlight:2] // 已输出响应,ResponseMiddleware 不做处理 xResult.Success(ctx, user) } ``` 场景二:错误响应 [#场景二错误响应] Handler 通过 `ctx.Error()` 添加错误: ```go title="handler/user.go" func GetUser(ctx *gin.Context) { user, err := userService.FindByID(ctx, id) if err != nil { // [!code highlight:2] // 添加错误到 ctx.Errors,ResponseMiddleware 自动处理 ctx.Error(xError.NewError(ctx.Request.Context(), xError.NotFound, "用户不存在", true)) return } xResult.Success(ctx, user) } ``` **输出:** ```json { "context": "abc123", "output": "NOT_EXIST", "code": 40004, "message": "数据不存在", "error_message": "用户不存在" } ``` 场景三:开发者错误 [#场景三开发者错误] Handler 既没有输出响应,也没有添加错误: ```go title="handler/user.go" func GetUser(ctx *gin.Context) { user := userService.FindByID(ctx, id) // [!code highlight:2] // 忘记输出响应!ResponseMiddleware 会返回 DeveloperError _ = user } ``` **输出:** ```json { "context": "abc123", "output": "DEVELOPER_ERROR", "code": 50001, "message": "开发者错误", "error_message": "没有正常输出信息或报错信息,请检查代码逻辑「开发者错误」" } ``` 错误类型处理 [#错误类型处理] xError.Error 类型 [#xerrorerror-类型] 如果错误是 `xError.Error` 类型,使用其错误码和消息: ```go // [!code highlight:2] // 创建带错误码的错误 err := xError.NewError(ctx.Request.Context(), xError.ParameterError, "参数格式错误", true) ctx.Error(err) ``` 其他错误类型 [#其他错误类型] 如果是普通 `error`,使用 `ServerInternalError`: ```go // [!code highlight:2] // 普通错误会被包装为 ServerInternalError ctx.Error(errors.New("database connection failed")) ``` 使用示例 [#使用示例] 基础配置 [#基础配置] ```go title="main.go" func main() { router := gin.Default() // [!code highlight:2] // 注册统一响应中间件 router.Use(xMiddle.ResponseMiddleware) router.GET("/api/users/:id", getUser) router.Run(":8080") } ``` 完整示例 [#完整示例] ```go title="handler/user.go" package handler import ( xError "github.com/bamboo-services/bamboo-base-go/common/error" xResult "github.com/bamboo-services/bamboo-base-go/major/result" "github.com/gin-gonic/gin" ) func GetUser(ctx *gin.Context) { id := ctx.Param("id") // [!code highlight:4] // 参数校验 if id == "" { ctx.Error(xError.NewError(ctx.Request.Context(), xError.ParameterError, "用户ID不能为空", true)) return } user, err := userService.FindByID(ctx, id) if err != nil { // [!code highlight:3] // 业务错误 ctx.Error(xError.NewError(ctx.Request.Context(), xError.NotFound, "用户不存在", true)) return } // [!code highlight:2] // 成功响应 xResult.Success(ctx, user) } ``` 带数据的错误 [#带数据的错误] ```go // [!code highlight:6] // 错误响应中携带额外数据 ctx.Error(xError.NewErrorHasData( ctx.Request.Context(), xError.ParameterError, "参数校验失败", true, nil, map[string]string{ "username": "长度必须在 3-20 之间", "email": "格式不正确", }, )) ``` **输出:** ```json { "context": "abc123", "output": "PARAMETER_ERROR", "code": 40000, "message": "参数错误", "error_message": "参数校验失败", "data": { "username": "长度必须在 3-20 之间", "email": "格式不正确" } } ``` 重定向处理 [#重定向处理] 重定向响应(301/302)不会被 ResponseMiddleware 处理: ```go func RedirectHandler(ctx *gin.Context) { // [!code highlight:2] // 重定向不会触发 DeveloperError ctx.Redirect(302, "/new-location") } ``` 下一步 [#下一步] # 基因提供者 (/docs/bamboo-base-go/components/models/gene-provider) import { TypeTable } from '@/components/type-table'; 基因提供者 [#基因提供者] `GeneProvider` 接口允许每个实体类型定义自己的业务基因,在创建记录时自动使用对应的基因生成雪花 ID。 GeneProvider 接口 [#geneprovider-接口] ```go title="provider.go" // [!code highlight:4] type GeneProvider interface { // GetGene 返回实体的基因类型,用于生成雪花 ID 时指定业务基因 GetGene() xSnowflake.Gene } ``` 工作原理 [#工作原理] 在 `BaseEntity.BeforeCreate` 钩子中,会检查实体是否实现了 `GeneProvider` 接口: ```go title="base_entity.go" func (e *BaseEntity) BeforeCreate(tx *gorm.DB) error { if e.ID.IsZero() { // [!code highlight:5] // 默认使用 GeneDefault gene := xSnowflake.GeneDefault // 检查实体是否实现了 GeneProvider 接口 if provider, ok := tx.Statement.Dest.(GeneProvider); ok { gene = provider.GetGene() } // [!code highlight] e.ID = xSnowflake.GenerateID(gene) } // ... } ``` 使用示例 [#使用示例] 实现接口 [#实现接口] ```go title="entity/user.go" package entity import ( xModels "github.com/bamboo-services/bamboo-base-go/major/models" xSnowflake "github.com/bamboo-services/bamboo-base-go/common/snowflake" ) type User struct { xModels.BaseEntity Username string `gorm:"type:varchar(64)"` } // [!code highlight:4] // 实现 GeneProvider 接口 func (u *User) GetGene() xSnowflake.Gene { return xSnowflake.GeneUser } ``` ```go title="entity/order.go" type Order struct { xModels.BaseEntityWithSoftDelete OrderNo string `gorm:"type:varchar(64)"` } // [!code highlight:3] func (o *Order) GetGene() xSnowflake.Gene { return xSnowflake.GeneOrder } ``` ```go title="entity/payment.go" type Payment struct { xModels.BaseEntity Amount int64 `gorm:"type:bigint"` } // [!code highlight:3] func (p *Payment) GetGene() xSnowflake.Gene { return xSnowflake.GenePayment } ``` 创建记录 [#创建记录] ```go // [!code highlight:2] // 创建用户,ID 会使用 GeneUser 基因 user := &entity.User{Username: "zhangsan"} db.Create(user) fmt.Println(user.ID.Gene()) // User // [!code highlight:2] // 创建订单,ID 会使用 GeneOrder 基因 order := &entity.Order{OrderNo: "ORD001"} db.Create(order) fmt.Println(order.ID.Gene()) // Order // [!code highlight:2] // 创建支付,ID 会使用 GenePayment 基因 payment := &entity.Payment{Amount: 9900} db.Create(payment) fmt.Println(payment.ID.Gene()) // Payment ``` 不实现接口 [#不实现接口] 如果实体不实现 `GeneProvider` 接口,会使用默认基因 `GeneDefault(0)`: ```go title="entity/log.go" type Log struct { xModels.BaseEntity Message string `gorm:"type:text"` } // [!code highlight] // 不实现 GeneProvider,使用默认基因 ``` ```go log := &entity.Log{Message: "操作日志"} db.Create(log) fmt.Println(log.ID.Gene()) // Default ``` 基因分片 [#基因分片] 可以使用 `GeneCalc` 基于关联 ID 计算基因,实现数据分片: ```go title="entity/order_item.go" type OrderItem struct { xModels.BaseEntity OrderID xSnowflake.SnowflakeID `gorm:"type:bigint;index"` ProductID xSnowflake.SnowflakeID `gorm:"type:bigint"` Quantity int `gorm:"type:int"` } // [!code highlight:4] // 基于订单 ID 计算基因,同一订单的商品项具有相同基因 func (o *OrderItem) GetGene() xSnowflake.Gene { return xSnowflake.CalcGene().Hash(o.OrderID) } ``` **效果:** * 同一订单的所有商品项具有相同的基因值 * 便于按订单维度进行数据分片 * 查询同一订单的商品项时可以定位到同一分片 预定义基因 [#预定义基因] 完整示例 [#完整示例] ```go title="entity/merchant.go" package entity import ( xModels "github.com/bamboo-services/bamboo-base-go/major/models" xSnowflake "github.com/bamboo-services/bamboo-base-go/common/snowflake" ) // [!code highlight:2] // 自定义基因常量 const GeneMerchant xSnowflake.Gene = 32 type Merchant struct { xModels.BaseEntityWithSoftDelete Name string `gorm:"type:varchar(128)" json:"name"` Contact string `gorm:"type:varchar(64)" json:"contact"` Status int `gorm:"type:smallint;default:1" json:"status"` } // [!code highlight:4] // 使用自定义基因 func (m *Merchant) GetGene() xSnowflake.Gene { return GeneMerchant } ``` ```go // 创建商户 merchant := &entity.Merchant{ Name: "测试商户", Contact: "13800138000", } db.Create(merchant) // [!code highlight:2] // 从 ID 识别数据类型 fmt.Println(merchant.ID.Gene()) // Custom(32) ``` 下一步 [#下一步] # 基础实体 (/docs/bamboo-base-go/components/models) import { TypeTable } from '@/components/type-table'; 模型基类 [#模型基类] `xModels` 包提供与 GORM 深度集成的基础实体类型与分页模型,自动处理主键生成、时间戳管理和分页响应构建。 ```go import xModels "github.com/bamboo-services/bamboo-base-go/major/models" ``` BaseEntity [#baseentity] 基础实体结构体,适用于不需要软删除的数据表。 ```go title="base_entity.go" type BaseEntity struct { // [!code highlight:3] ID xSnowflake.SnowflakeID `json:"id" gorm:"type:bigint;primaryKey;comment:主键"` CreatedAt time.Time `json:"-" gorm:"autoCreateTime:milli;not null;comment:创建时间"` UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime:milli;not null;comment:更新时间"` } ``` 字段说明 [#字段说明] GORM 标签说明 [#gorm-标签说明] | 字段 | JSON | GORM 标签 | | ----------- | -------------- | ------------------------------- | | `ID` | `"id"` | `type:bigint;primaryKey` | | `CreatedAt` | `"-"` (不输出) | `autoCreateTime:milli;not null` | | `UpdatedAt` | `"updated_at"` | `autoUpdateTime:milli;not null` | 使用示例 [#使用示例] 定义实体 [#定义实体] ```go title="entity/user.go" package entity import ( xModels "github.com/bamboo-services/bamboo-base-go/major/models" xSnowflake "github.com/bamboo-services/bamboo-base-go/common/snowflake" ) // [!code highlight:5] type User struct { xModels.BaseEntity // 继承基础实体 Username string `gorm:"type:varchar(64);uniqueIndex" json:"username"` Email string `gorm:"type:varchar(128);uniqueIndex" json:"email"` Password string `gorm:"type:varchar(256)" json:"-"` } // [!code highlight:4] // 实现 GeneProvider 接口(可选) func (u *User) GetGene() xSnowflake.Gene { return xSnowflake.GeneUser } ``` 创建记录 [#创建记录] ```go user := &entity.User{ Username: "zhangsan", Email: "zhangsan@example.com", Password: hashedPassword, } // [!code highlight:2] // ID 会自动生成,CreatedAt 和 UpdatedAt 会自动设置 db.Create(user) fmt.Println(user.ID) // 1234567890123456789 ``` 更新记录 [#更新记录] ```go // [!code highlight:2] // UpdatedAt 会自动更新 db.Model(&user).Update("username", "lisi") ``` 查询记录 [#查询记录] ```go var user entity.User // [!code highlight:2] // 按 ID 查询 db.First(&user, "id = ?", "1234567890123456789") // [!code highlight:2] // 按条件查询 db.Where("username = ?", "zhangsan").First(&user) ``` GORM 钩子 [#gorm-钩子] BeforeCreate [#beforecreate] 在插入数据前自动执行: ```go title="base_entity.go" func (e *BaseEntity) BeforeCreate(tx *gorm.DB) error { if e.ID.IsZero() { // [!code highlight:4] // 尝试从实体获取基因类型 gene := xSnowflake.GeneDefault if provider, ok := tx.Statement.Dest.(GeneProvider); ok { gene = provider.GetGene() } // [!code highlight] e.ID = xSnowflake.GenerateID(gene) } // [!code highlight:2] now := time.Now() e.CreatedAt = now e.UpdatedAt = now return nil } ``` **功能:** * 如果 ID 为零值,自动生成雪花 ID * 检查实体是否实现 `GeneProvider` 接口 * 实现了接口则使用自定义基因,否则使用默认基因 * 设置 `CreatedAt` 和 `UpdatedAt` 为当前时间 BeforeUpdate [#beforeupdate] 在更新数据前自动执行: ```go title="base_entity.go" func (e *BaseEntity) BeforeUpdate(tx *gorm.DB) error { // [!code highlight] e.UpdatedAt = time.Now() return nil } ``` **功能:** * 自动更新 `UpdatedAt` 时间戳 数据库表结构 [#数据库表结构] 使用 `BaseEntity` 的实体会生成如下表结构: ```sql CREATE TABLE users ( -- [!code highlight:3] id BIGINT PRIMARY KEY, created_at TIMESTAMP(3) NOT NULL, updated_at TIMESTAMP(3) NOT NULL, username VARCHAR(64) UNIQUE, email VARCHAR(128) UNIQUE, password VARCHAR(256) ); ``` JSON 序列化 [#json-序列化] ```go user := entity.User{ Username: "zhangsan", Email: "zhangsan@example.com", } db.Create(&user) // [!code highlight] data, _ := json.Marshal(user) ``` 输出: ```json { "id": "1234567890123456789", "updated_at": "2024-01-15T10:30:00.123Z", "username": "zhangsan", "email": "zhangsan@example.com" } ``` **注意:** * `ID` 序列化为字符串(避免 JavaScript 精度丢失) * `CreatedAt` 不输出(`json:"-"`) * `Password` 不输出(`json:"-"`) 下一步 [#下一步] # 分页模型 (/docs/bamboo-base-go/components/models/page) import { TypeTable } from '@/components/type-table'; 分页模型 [#分页模型] `xModels` 提供通用分页模型,统一分页请求参数、排序规则和分页响应结构。 ```go import xModels "github.com/bamboo-services/bamboo-base-go/major/models" ``` 核心类型 [#核心类型] PageRequest [#pagerequest] 分页请求参数: ```go title="page.go" type PageRequest struct { Page int64 `json:"page" form:"page" binding:"omitempty,min=1"` Size int64 `json:"size" form:"size" binding:"omitempty,min=1,max=200"` Sort PageSort `json:"sort" form:"sort" binding:"omitempty,enum_string=asc desc"` } ``` PageResponse [#pageresponse] 泛型分页响应结构: ```go title="page.go" type PageResponse[T any] struct { CurrentPage int64 `json:"current_page"` TotalPages int64 `json:"total_pages"` TotalItems int64 `json:"total_items"` Size int64 `json:"size"` Items T `json:"items"` } ``` PageSort [#pagesort] 排序方向枚举: ```go const ( SortAsc PageSort = "asc" SortDesc PageSort = "desc" ) ``` PageProvider [#pageprovider] 当业务请求结构体实现该接口时,可通过 `GetPageRequest` 统一提取分页参数: ```go type PageProvider interface { GetPageSettings() PageRequest } func GetPageRequest[T any](req T) PageRequest ``` 默认分页配置 [#默认分页配置] ```go const ( DefaultPageNumber int64 = 1 DefaultPageSize int64 = 20 DefaultPageMaxSize int64 = 200 ) var DefaultPageConfig = xModels.PageConfig{ DefaultPage: xModels.DefaultPageNumber, DefaultSize: xModels.DefaultPageSize, MaxSize: xModels.DefaultPageMaxSize, DefaultSort: xModels.SortAsc, } ``` 快速使用 [#快速使用] 解析分页请求 [#解析分页请求] ```go func ListUsers(ctx *gin.Context) { req := xModels.DefaultPageRequest() _ = ctx.ShouldBindQuery(&req) req = req.Normalize() offset := req.Offset() limit := req.Limit() order := "created_at " + req.OrderDirection() // 查询数据库... _ = offset _ = limit _ = order } ``` 构造分页响应 [#构造分页响应] ```go func ListUsers(ctx *gin.Context) { req := xModels.PageRequest{Page: 1, Size: 10}.Normalize() totalItems := int64(95) users := []UserDTO{{ID: 1, Username: "alice"}} page := xModels.NewPageFromRequest(req, totalItems, users) xResult.SuccessHasData(ctx, "查询成功", page) } ``` 从业务请求统一提取分页参数 [#从业务请求统一提取分页参数] ```go type ListUserRequest struct { Keyword string Page xModels.PageRequest } func (r ListUserRequest) GetPageSettings() xModels.PageRequest { return r.Page } func ListUsers(ctx *gin.Context) { req := ListUserRequest{} _ = ctx.ShouldBindQuery(&req) pageReq := xModels.GetPageRequest(req) page := xModels.NewPageFromRequest(pageReq, 95, []UserDTO{}) xResult.SuccessHasData(ctx, "查询成功", page) } ``` 响应示例: ```json { "current_page": 1, "total_pages": 10, "total_items": 95, "size": 10, "items": [ { "id": 1, "username": "alice" } ] } ``` 常用方法 [#常用方法] 下一步 [#下一步] # 软删除实体 (/docs/bamboo-base-go/components/models/soft-delete) import { TypeTable } from '@/components/type-table'; 软删除实体 [#软删除实体] `BaseEntityWithSoftDelete` 在 `BaseEntity` 基础上增加了软删除支持,删除操作不会物理删除数据,而是设置删除时间戳。 BaseEntityWithSoftDelete [#baseentitywithsoftdelete] ```go title="base_entity_soft_delete.go" type BaseEntityWithSoftDelete struct { // [!code highlight:4] ID xSnowflake.SnowflakeID `json:"id" gorm:"type:bigint;primaryKey;comment:主键"` CreatedAt time.Time `json:"-" gorm:"autoCreateTime:milli;not null;comment:创建时间"` UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime:milli;not null;comment:更新时间"` DeletedAt gorm.DeletedAt `json:"-" gorm:"type:timestamp;index;comment:删除时间"` } ``` 字段说明 [#字段说明] 与 BaseEntity 的区别 [#与-baseentity-的区别] | 特性 | BaseEntity | BaseEntityWithSoftDelete | | ------------ | ---------- | ------------------------ | | 软删除支持 | 无 | 有 | | DeletedAt 字段 | 无 | `gorm.DeletedAt` | | 删除行为 | 物理删除 | 逻辑删除 | | 查询行为 | 查询所有记录 | 自动过滤已删除记录 | 使用示例 [#使用示例] 定义实体 [#定义实体] ```go title="entity/order.go" package entity import ( xModels "github.com/bamboo-services/bamboo-base-go/major/models" xSnowflake "github.com/bamboo-services/bamboo-base-go/common/snowflake" ) // [!code highlight:5] type Order struct { xModels.BaseEntityWithSoftDelete // 继承软删除实体 UserID xSnowflake.SnowflakeID `json:"user_id" gorm:"type:bigint;index"` OrderNo string `json:"order_no" gorm:"type:varchar(64);uniqueIndex"` Amount int64 `json:"amount" gorm:"type:bigint"` } // [!code highlight:3] func (o *Order) GetGene() xSnowflake.Gene { return xSnowflake.GeneOrder } ``` 创建记录 [#创建记录] ```go order := &entity.Order{ UserID: userID, OrderNo: "ORD202401150001", Amount: 9900, } // [!code highlight] db.Create(order) ``` 软删除 [#软删除] ```go // [!code highlight:2] // 软删除:设置 DeletedAt 为当前时间,不物理删除 db.Delete(&order) // 数据库中: // deleted_at = '2024-01-15 10:30:00' ``` 查询记录 [#查询记录] ```go var orders []entity.Order // [!code highlight:2] // 普通查询:自动过滤已删除记录 db.Find(&orders) // [!code highlight:2] // 查询包含已删除的记录 db.Unscoped().Find(&orders) // [!code highlight:2] // 只查询已删除的记录 db.Unscoped().Where("deleted_at IS NOT NULL").Find(&orders) ``` 恢复数据 [#恢复数据] ```go // [!code highlight:2] // 恢复已删除的记录 db.Unscoped().Model(&order).Update("deleted_at", nil) ``` 物理删除 [#物理删除] ```go // [!code highlight:2] // 真正删除数据(慎用) db.Unscoped().Delete(&order) ``` 数据库表结构 [#数据库表结构] 使用 `BaseEntityWithSoftDelete` 的实体会生成如下表结构: ```sql CREATE TABLE orders ( -- [!code highlight:4] id BIGINT PRIMARY KEY, created_at TIMESTAMP(3) NOT NULL, updated_at TIMESTAMP(3) NOT NULL, deleted_at TIMESTAMP, -- 软删除字段 user_id BIGINT, order_no VARCHAR(64) UNIQUE, amount BIGINT ); -- [!code highlight:2] -- 软删除字段索引 CREATE INDEX idx_orders_deleted_at ON orders(deleted_at); ``` 软删除机制 [#软删除机制] 删除操作 [#删除操作] ```go // 调用 Delete db.Delete(&order) // 实际执行的 SQL // UPDATE orders SET deleted_at = '2024-01-15 10:30:00' WHERE id = ? ``` 查询操作 [#查询操作] ```go // 普通查询 db.Find(&orders) // 实际执行的 SQL // SELECT * FROM orders WHERE deleted_at IS NULL ``` Unscoped 查询 [#unscoped-查询] ```go // 忽略软删除条件 db.Unscoped().Find(&orders) // 实际执行的 SQL // SELECT * FROM orders ``` 最佳实践 [#最佳实践] 选择合适的基类 [#选择合适的基类] | 场景 | 推荐基类 | | ---- | -------------------------- | | 用户数据 | `BaseEntityWithSoftDelete` | | 订单数据 | `BaseEntityWithSoftDelete` | | 日志数据 | `BaseEntity` | | 配置数据 | `BaseEntity` | | 临时数据 | `BaseEntity` | 软删除注意事项 [#软删除注意事项] ```go // [!code highlight:2] // 1. 唯一索引问题:软删除后唯一索引仍然生效 // 解决方案:使用复合唯一索引包含 deleted_at type User struct { xModels.BaseEntityWithSoftDelete // [!code highlight:2] // 复合唯一索引,允许同名用户(一个已删除,一个未删除) Username string `gorm:"type:varchar(64);uniqueIndex:idx_username_deleted"` } // [!code highlight:2] // 2. 关联查询:关联表也需要考虑软删除 db.Preload("Orders", "deleted_at IS NULL").Find(&users) ``` 下一步 [#下一步] # 自定义验证器 (/docs/bamboo-base-go/components/validator/custom) import { TypeTable } from '@/components/type-table'; 自定义验证器 [#自定义验证器] `xVaild` 提供多个自定义验证器,扩展 `go-playground/validator` 的验证能力。 RegisterCustomValidators [#registercustomvalidators] 注册所有自定义验证器: ```go title="custom.go" // [!code highlight] func RegisterCustomValidators(validate *validator.Validate) error ``` **使用:** ```go validate := validator.New() // [!code highlight] if err := xVaild.RegisterCustomValidators(validate); err != nil { log.Fatal("验证器注册失败:", err) } ``` 验证器列表 [#验证器列表] strict_url [#strict_url] 严格的 URL 验证,仅允许 HTTP/HTTPS 协议: ```go type Request struct { // [!code highlight] Website string `json:"website" binding:"required,strict_url"` } ``` **有效值:** * `https://example.com` * `http://localhost:8080/path` **无效值:** * `ftp://example.com` * `example.com`(缺少协议) strict_uuid [#strict_uuid] 严格的 UUID 格式验证: ```go type Request struct { // [!code highlight] ID string `json:"id" binding:"required,strict_uuid"` } ``` **有效值:** * `550e8400-e29b-41d4-a716-446655440000` **无效值:** * `550e8400e29b41d4a716446655440000`(缺少连字符) * `invalid-uuid` alphanum_underscore [#alphanum_underscore] 字母、数字和下划线验证: ```go type Request struct { // [!code highlight] Username string `json:"username" binding:"required,alphanum_underscore"` } ``` **有效值:** * `user_name` * `User123` * `test_user_01` **无效值:** * `user-name`(包含连字符) * `user@name`(包含特殊字符) regexp [#regexp] 正则表达式验证: ```go type Request struct { // [!code highlight:2] // 中国大陆手机号 Phone string `json:"phone" binding:"required,regexp=^1[3-9]\\d{9}$"` // [!code highlight:2] // 邮政编码 ZipCode string `json:"zip_code" binding:"required,regexp=^\\d{6}$"` } ``` **注意:** 正则表达式中的 `\` 需要转义为 `\\`。 enum_int [#enum_int] 整数枚举值验证: ```go type Request struct { // [!code highlight:2] // 状态:1=启用, 2=禁用, 3=待审核 Status int `json:"status" binding:"required,enum_int=1 2 3"` } ``` **有效值:** `1`, `2`, `3` **无效值:** `0`, `4`, `100` enum_string [#enum_string] 字符串枚举值验证: ```go type Request struct { // [!code highlight:2] // 角色:admin, user, guest Role string `json:"role" binding:"required,enum_string=admin user guest"` } ``` **有效值:** `admin`, `user`, `guest` **无效值:** `Admin`(大小写敏感), `superadmin` enum_float [#enum_float] 浮点数枚举值验证: ```go type Request struct { // [!code highlight:2] // 折扣:0.5, 0.7, 0.8, 0.9, 1.0 Discount float64 `json:"discount" binding:"required,enum_float=0.5 0.7 0.8 0.9 1.0"` } ``` 完整示例 [#完整示例] ```go title="dto/product.go" type CreateProductRequest struct { // [!code highlight:2] // 商品名称:字母数字下划线 Code string `json:"code" label:"商品编码" binding:"required,alphanum_underscore,min=4,max=32"` // [!code highlight:2] // 商品链接:严格 URL Link string `json:"link" label:"商品链接" binding:"omitempty,strict_url"` // [!code highlight:2] // 商品状态:枚举值 Status int `json:"status" label:"商品状态" binding:"required,enum_int=1 2 3"` // [!code highlight:2] // SKU 编码:正则验证 SKU string `json:"sku" label:"SKU编码" binding:"required,regexp=^SKU-\\d{8}$"` } ``` ```go title="handler/product.go" func CreateProduct(ctx *gin.Context) { var req dto.CreateProductRequest if err := ctx.ShouldBindJSON(&req); err != nil { xVaild.HandleValidationError(ctx, err) return } // 业务逻辑... } ``` *** 编写自定义验证器 [#编写自定义验证器] 如果内置验证器不满足需求,可以编写自己的验证器。 验证器函数签名 [#验证器函数签名] ```go // [!code highlight] type Func func(fl validator.FieldLevel) bool ``` `validator.FieldLevel` 提供以下方法: 示例一:简单验证器 [#示例一简单验证器] 验证字符串是否以特定前缀开头: ```go title="validator/validator_prefix.go" package validator import ( "strings" "github.com/go-playground/validator/v10" ) // [!code highlight:2] // ValidatePrefix 验证字符串是否以指定前缀开头 // 使用: binding:"prefix=SKU-" func ValidatePrefix(fl validator.FieldLevel) bool { // [!code highlight:2] // 获取参数(前缀) prefix := fl.Param() if prefix == "" { return false } // [!code highlight:2] // 获取字段值 value := fl.Field().String() // [!code highlight] return strings.HasPrefix(value, prefix) } ``` 示例二:带参数的验证器 [#示例二带参数的验证器] 验证整数是否在指定范围内: ```go title="validator/validator_between.go" package validator import ( "strconv" "strings" "github.com/go-playground/validator/v10" ) // [!code highlight:2] // ValidateBetween 验证整数是否在指定范围内 // 使用: binding:"between=1-100" func ValidateBetween(fl validator.FieldLevel) bool { // [!code highlight:2] // 解析参数 "min-max" param := fl.Param() parts := strings.Split(param, "-") if len(parts) != 2 { return false } min, err1 := strconv.ParseInt(parts[0], 10, 64) max, err2 := strconv.ParseInt(parts[1], 10, 64) if err1 != nil || err2 != nil { return false } // [!code highlight:2] // 获取字段值 value := fl.Field().Int() // [!code highlight] return value >= min && value <= max } ``` 示例三:跨字段验证器 [#示例三跨字段验证器] 验证确认密码是否与密码一致: ```go title="validator/validator_confirm.go" package validator import ( "github.com/go-playground/validator/v10" ) // [!code highlight:2] // ValidateConfirmPassword 验证确认密码是否与密码一致 // 使用: binding:"confirm_password=Password" func ValidateConfirmPassword(fl validator.FieldLevel) bool { // [!code highlight:2] // 获取参数(要比较的字段名) fieldName := fl.Param() if fieldName == "" { return false } // [!code highlight:2] // 获取父结构体中的目标字段 parent := fl.Parent() targetField := parent.FieldByName(fieldName) if !targetField.IsValid() { return false } // [!code highlight:2] // 比较两个字段的值 return fl.Field().String() == targetField.String() } ``` **使用:** ```go type RegisterRequest struct { Password string `json:"password" label:"密码" binding:"required,min=8"` // [!code highlight] ConfirmPassword string `json:"confirm_password" label:"确认密码" binding:"required,confirm_password=Password"` } ``` 注册自定义验证器 [#注册自定义验证器] 由于 `xReg.EngineInit()` 已经注册了内置验证器,第三方项目需要在 **路由注册之前** 额外注册自己的验证器。 ```go title="startup/startup_validator.go" package startup import ( "github.com/gin-gonic/gin/binding" "github.com/go-playground/validator/v10" ) // [!code highlight:2] // RegisterCustomValidators 注册项目自定义验证器 // 在 xReg.Register() 之后、路由注册之前调用 func RegisterCustomValidators() { if v, ok := binding.Validator.Engine().(*validator.Validate); ok { // [!code highlight:4] // 注册自己的验证器 _ = v.RegisterValidation("prefix", ValidatePrefix) _ = v.RegisterValidation("between", ValidateBetween) _ = v.RegisterValidation("confirm_password", ValidateConfirmPassword) } } ``` ```go title="main.go" func main() { ctx := context.Background() // [!code highlight:2] // 1. 初始化基础库(已注册内置验证器) reg := xReg.Register(ctx) // [!code highlight:2] // 2. 注册项目自定义验证器 startup.RegisterCustomValidators() // [!code highlight:2] // 3. 注册路由 router.RegisterRouter(reg.Serve) // 4. 启动服务 _ = reg.Serve.Run(":8080") } ``` 完整的验证器文件结构 [#完整的验证器文件结构] ``` your-project/ ├── main.go ├── startup/ │ └── startup_validator.go # 自定义验证器注册 ├── validator/ │ ├── validator_prefix.go # 前缀验证器 │ ├── validator_between.go # 范围验证器 │ └── validator_confirm.go # 确认密码验证器 └── dto/ └── user.go # 使用验证器的 DTO ``` ```` ### 添加中文错误消息 在 `messages.go` 中添加自定义验证器的错误消息: ```go title="validator/messages.go" var CustomValidationMessages = map[string]string{ // [!code highlight:3] "prefix": "必须以 %s 开头", "between": "必须在 %s 范围内", "confirm_password": "与密码不一致", } ```` 或者在注册翻译器时添加: ```go // [!code highlight:6] // 注册自定义验证器的翻译 _ = v.RegisterTranslation("prefix", trans, func(ut ut.Translator) error { return ut.Add("prefix", "{0}必须以 {1} 开头", true) }, func(ut ut.Translator, fe validator.FieldError) string { t, _ := ut.T("prefix", fe.Field(), fe.Param()) return t }) ``` 下一步 [#下一步] # 概述 (/docs/bamboo-base-go/components/validator) import { TypeTable } from '@/components/type-table'; 验证器 [#验证器] `xVaild` 基于 `go-playground/validator` 提供自定义验证器、中文翻译支持和统一错误响应处理。 ```go import xVaild "github.com/bamboo-services/bamboo-base-go/common/validator" ``` 核心功能 [#核心功能] 自定义验证器 [#自定义验证器] 快速使用 [#快速使用] 初始化验证器 [#初始化验证器] ```go title="register_engine.go" func (r *Reg) EngineInit() { engine := gin.Default() // [!code highlight:2] // 获取 Gin 的验证器 if v, ok := binding.Validator.Engine().(*validator.Validate); ok { // [!code highlight:2] // 注册自定义验证器 _ = xVaild.RegisterCustomValidators(v) // [!code highlight:2] // 注册中文翻译器 _ = xVaild.RegisterTranslator(v) } } ``` 定义请求结构 [#定义请求结构] ```go title="dto/user.go" type CreateUserRequest struct { // [!code highlight:2] // 使用 label 标签定义中文字段名 Username string `json:"username" label:"用户名" binding:"required,min=4,max=20,alphanum_underscore"` Password string `json:"password" label:"密码" binding:"required,min=8"` Email string `json:"email" label:"邮箱" binding:"required,email"` // [!code highlight:2] // 使用枚举验证 Role string `json:"role" label:"角色" binding:"required,enum_string=admin user guest"` // [!code highlight:2] // 使用正则验证 Phone string `json:"phone" label:"手机号" binding:"required,regexp=^1[3-9]\\d{9}$"` } ``` 处理验证错误 [#处理验证错误] ```go title="handler/user.go" func CreateUser(ctx *gin.Context) { var req dto.CreateUserRequest if err := ctx.ShouldBindJSON(&req); err != nil { // [!code highlight:2] // 自动处理验证错误并返回中文响应 xVaild.HandleValidationError(ctx, err) return } // 业务逻辑... } ``` 错误响应格式 [#错误响应格式] ```json { "context": "abc123", "output": "REQUEST_BODY_ERROR", "code": 40014, "message": "请求体错误", "error_message": "用户名格式不正确", "data": [ { "field": "username", "tag": "alphanum_underscore", "message": "用户名只能包含字母、数字和下划线", "value": "user@name" } ] } ``` 下一步 [#下一步] # 错误响应 (/docs/bamboo-base-go/components/validator/response) import { TypeTable } from '@/components/type-table'; 错误响应 [#错误响应] `xVaild` 提供统一的验证错误响应处理,将验证错误转换为友好的 JSON 格式。 HandleValidationError [#handlevalidationerror] 处理验证错误并返回响应: ```go title="response.go" // [!code highlight] func HandleValidationError(ctx *gin.Context, bindErr error) ``` **功能:** * 解析验证错误 * 翻译为中文消息 * 返回统一格式的 JSON 响应 ValidationErrorDetail [#validationerrordetail] 错误详情结构: ```go title="response.go" type ValidationErrorDetail struct { // [!code highlight:4] Field string `json:"field"` // 字段名(英文) Tag string `json:"tag"` // 验证标签 Message string `json:"message"` // 错误消息(中文) Value interface{} `json:"value"` // 字段值 } ``` 响应格式 [#响应格式] 单个错误 [#单个错误] ```json { "context": "abc123", "output": "REQUEST_BODY_ERROR", "code": 40014, "message": "请求体错误", "error_message": "用户名为必填项", "data": [ { "field": "username", "tag": "required", "message": "用户名为必填项", "value": "" } ] } ``` 多个错误 [#多个错误] ```json { "context": "abc123", "output": "REQUEST_BODY_ERROR", "code": 40014, "message": "请求体错误", "error_message": "用户名为必填项", "data": [ { "field": "username", "tag": "required", "message": "用户名为必填项", "value": "" }, { "field": "password", "tag": "min", "message": "密码长度不能少于 8 个字符", "value": "123" }, { "field": "email", "tag": "email", "message": "邮箱必须是有效的邮箱地址", "value": "invalid-email" } ] } ``` 使用示例 [#使用示例] 基础使用 [#基础使用] ```go title="handler/user.go" func CreateUser(ctx *gin.Context) { var req dto.CreateUserRequest if err := ctx.ShouldBindJSON(&req); err != nil { // [!code highlight:2] // 自动处理验证错误 xVaild.HandleValidationError(ctx, err) return } // 业务逻辑... xResult.Success(ctx, user) } ``` 完整示例 [#完整示例] ```go title="dto/user.go" type CreateUserRequest struct { Username string `json:"username" label:"用户名" binding:"required,min=4,max=20,alphanum_underscore"` Password string `json:"password" label:"密码" binding:"required,min=8"` Email string `json:"email" label:"邮箱" binding:"required,email"` Phone string `json:"phone" label:"手机号" binding:"required,regexp=^1[3-9]\\d{9}$"` Role string `json:"role" label:"角色" binding:"required,enum_string=admin user guest"` } ``` ```go title="handler/user.go" func CreateUser(ctx *gin.Context) { var req dto.CreateUserRequest // [!code highlight:4] // 绑定并验证请求 if err := ctx.ShouldBindJSON(&req); err != nil { xVaild.HandleValidationError(ctx, err) return } // 创建用户... user, err := userService.Create(ctx, &req) if err != nil { ctx.Error(err) return } xResult.Success(ctx, user) } ``` 请求示例 [#请求示例] **请求:** ```json { "username": "ab", "password": "123", "email": "invalid", "phone": "123456", "role": "superadmin" } ``` **响应:** ```json { "context": "550e8400-e29b-41d4-a716-446655440000", "output": "REQUEST_BODY_ERROR", "code": 40014, "message": "请求体错误", "error_message": "用户名长度不能少于 4 个字符", "data": [ { "field": "username", "tag": "min", "message": "用户名长度不能少于 4 个字符", "value": "ab" }, { "field": "password", "tag": "min", "message": "密码长度不能少于 8 个字符", "value": "123" }, { "field": "email", "tag": "email", "message": "邮箱必须是有效的邮箱地址", "value": "invalid" }, { "field": "phone", "tag": "regexp", "message": "手机号格式不正确", "value": "123456" }, { "field": "role", "tag": "enum_string", "message": "角色必须是以下值之一: admin user guest", "value": "superadmin" } ] } ``` 错误码 [#错误码] 验证错误使用 `RequestBodyError` 错误码: 下一步 [#下一步] # 中文翻译 (/docs/bamboo-base-go/components/validator/translator) import { TypeTable } from '@/components/type-table'; 中文翻译 [#中文翻译] `xVaild` 为 `go-playground/validator` 提供中文翻译支持,将验证错误消息转换为友好的中文提示。 RegisterTranslator [#registertranslator] 注册中文翻译器: ```go title="translator.go" // [!code highlight] func RegisterTranslator(validate *validator.Validate) error ``` **使用:** ```go validate := validator.New() xVaild.RegisterCustomValidators(validate) // [!code highlight] xVaild.RegisterTranslator(validate) ``` GetTranslator [#gettranslator] 获取翻译器实例: ```go title="translator.go" // [!code highlight] func GetTranslator() ut.Translator ``` TranslateError [#translateerror] 翻译验证错误为中文: ```go title="translator.go" // [!code highlight] func TranslateError(err error) map[string]string ``` **返回:** 字段名到错误消息的映射。 label 标签 [#label-标签] 使用 `label` 标签定义字段的中文名称: ```go type User struct { // [!code highlight:2] // label 优先于 json 作为字段名 Username string `json:"username" label:"用户名" binding:"required,min=4"` Password string `json:"password" label:"密码" binding:"required,min=8"` Email string `json:"email" label:"邮箱" binding:"required,email"` } ``` **错误消息示例:** * `用户名为必填项` * `密码长度不能少于 8 个字符` * `邮箱必须是有效的邮箱地址` 内置消息映射 [#内置消息映射] 自定义验证器消息 [#自定义验证器消息] 使用示例 [#使用示例] 基础使用 [#基础使用] ```go title="handler/user.go" func CreateUser(ctx *gin.Context) { var req dto.CreateUserRequest if err := ctx.ShouldBindJSON(&req); err != nil { // [!code highlight:2] // 获取翻译后的错误消息 errors := xVaild.TranslateError(err) for field, msg := range errors { fmt.Printf("%s: %s\n", field, msg) } return } } ``` 配合 HandleValidationError [#配合-handlevalidationerror] ```go title="handler/user.go" func CreateUser(ctx *gin.Context) { var req dto.CreateUserRequest if err := ctx.ShouldBindJSON(&req); err != nil { // [!code highlight:2] // 自动翻译并返回响应 xVaild.HandleValidationError(ctx, err) return } } ``` 完整初始化 [#完整初始化] ```go title="register_engine.go" func (r *Reg) EngineInit() { engine := gin.Default() if v, ok := binding.Validator.Engine().(*validator.Validate); ok { // [!code highlight:2] // 1. 注册自定义验证器 _ = xVaild.RegisterCustomValidators(v) // [!code highlight:2] // 2. 注册中文翻译器 _ = xVaild.RegisterTranslator(v) // [!code highlight:3] // 3. 使用 label 标签作为字段名 v.RegisterTagNameFunc(func(fld reflect.StructField) string { name := fld.Tag.Get("label") if name == "" { name = fld.Tag.Get("json") } return name }) } } ``` 下一步 [#下一步] # 自定义注入 (/docs/bamboo-base-go/core/context/custom) 自定义注入 [#自定义注入] 除了框架预定义的上下文键,你可以通过**节点化系统**扩展自定义的业务上下文键。 定义自定义键 [#定义自定义键] 在业务项目中创建自定义上下文键常量: ```go title="internal/constant/context/business.go" package bContext import xCtx "github.com/bamboo-services/bamboo-base-go/defined/context" const ( // [!code highlight:4] // 使用 xCtx.ContextKey 类型定义自定义键 TencentCloudForSmsKey xCtx.ContextKey = "business_tencent_cloud_sms" // 腾讯云短信 UserEntityKey xCtx.ContextKey = "business_user_entity" // 用户实体 MerchantEntityKey xCtx.ContextKey = "business_merchant_entity" // 商户实体 ) ``` **命名规范:** | 前缀 | 说明 | 示例 | | ----------- | ------ | ---------------------- | | `context_` | 框架预定义键 | `context_database` | | `business_` | 业务自定义键 | `business_user_entity` | 注入自定义数据 [#注入自定义数据] 方式一:节点化注入(推荐) [#方式一节点化注入推荐] 在 `Register` 时通过自定义节点注入全局资源: ```go title="main.go" package main import ( "context" xCtx "github.com/bamboo-services/bamboo-base-go/defined/context" xReg "github.com/bamboo-services/bamboo-base-go/major/register" xRegNode "github.com/bamboo-services/bamboo-base-go/major/register/node" bContext "your-project/internal/constant/context" ) func main() { // [!code highlight:8] reg := xReg.Register(context.Background(), []xRegNode.RegNodeList{ // 框架资源 {Key: xCtx.DatabaseKey, Node: initDatabase}, {Key: xCtx.RedisClientKey, Node: initRedis}, // 业务资源 {Key: bContext.TencentCloudForSmsKey, Node: initTencentSms}, }) reg.Serve.Run(":8080") } // [!code highlight:2] // 腾讯云短信初始化节点 func initTencentSms(ctx context.Context) (any, error) { credential := common.NewCredential(secretId, secretKey) client, err := sms.NewClient(credential, "ap-guangzhou", profile.NewClientProfile()) return client, err } ``` 方式二:中间件注入 [#方式二中间件注入] 在认证中间件中注入用户实体(请求级资源): ```go title="internal/app/middleware/auth_required.go" package middleware import ( "context" xError "github.com/bamboo-services/bamboo-base-go/common/error" xResult "github.com/bamboo-services/bamboo-base-go/major/result" xCtxUtil "github.com/bamboo-services/bamboo-base-go/common/utility/context" "github.com/gin-gonic/gin" bContext "your-project/internal/constant/context" "your-project/internal/logic" ) func AuthRequired(c *gin.Context) { // ... 验证 token 逻辑 ... // 获取用户信息 ctx := c.Request.Context() db := xCtxUtil.MustGetDB(ctx) user, xErr := logic.NewUser(db).GetUser(ctx, userID) if xErr != nil { xResult.AbortError(c, xErr.ErrorCode, xErr.ErrorMessage, nil) return } // [!code highlight:3] // 注入用户实体到上下文 ctx = context.WithValue(ctx, bContext.UserEntityKey, user) c.Request = c.Request.WithContext(ctx) c.Next() } ``` 获取自定义数据 [#获取自定义数据] 创建工具函数 [#创建工具函数] 为自定义键创建便捷的获取函数: ```go title="pkg/ctx/user.go" package bCtxUtil import ( "context" bContext "your-project/internal/constant/context" "your-project/internal/entity" ) // [!code highlight:2] // GetUserEntity 从上下文获取当前登录用户 func GetUserEntity(ctx context.Context) *entity.User { if value := ctx.Value(bContext.UserEntityKey); value != nil { // [!code highlight] return value.(*entity.User) } return nil } // [!code highlight:2] // MustGetUserEntity 从上下文获取当前登录用户(不存在则 panic) func MustGetUserEntity(ctx context.Context) *entity.User { user := GetUserEntity(ctx) if user == nil { panic("用户实体不存在于上下文中") } return user } ``` 在 Handler 中使用 [#在-handler-中使用] ```go title="internal/handler/user.go" package handler import ( xResult "github.com/bamboo-services/bamboo-base-go/major/result" "github.com/gin-gonic/gin" bCtxUtil "your-project/pkg/ctx" ) func (h *UserHandler) GetProfile(c *gin.Context) { // [!code highlight:2] // 获取当前登录用户 user := bCtxUtil.GetUserEntity(c.Request.Context()) xResult.SuccessHasData(c, "获取成功", user) } func (h *UserHandler) UpdateProfile(c *gin.Context) { // [!code highlight:2] // 获取当前登录用户 ID currentUser := bCtxUtil.MustGetUserEntity(c.Request.Context()) // 更新逻辑... h.service.UpdateUser(c.Request.Context(), currentUser.ID, req) xResult.Success(c, "更新成功") } ``` 完整示例 [#完整示例] 项目结构 [#项目结构] ``` your-project/ ├── main.go # 入口,注册节点 ├── internal/ │ ├── app/ │ │ └── middleware/ │ │ └── auth_required.go # 认证中间件 │ ├── constant/ │ │ └── context/ │ │ └── business.go # 自定义键定义 │ └── handler/ │ └── user.go └── pkg/ └── ctx/ └── user.go # 自定义获取函数 ``` 自定义键定义 [#自定义键定义] ```go title="internal/constant/context/business.go" package bContext import xCtx "github.com/bamboo-services/bamboo-base-go/defined/context" const ( // [!code highlight:5] // 用户相关 UserEntityKey xCtx.ContextKey = "business_user_entity" UserTokenKey xCtx.ContextKey = "business_user_token" UserPermissionKey xCtx.ContextKey = "business_user_permission" // [!code highlight:3] // 第三方服务 TencentSmsKey xCtx.ContextKey = "business_tencent_sms" AliyunOssKey xCtx.ContextKey = "business_aliyun_oss" ) ``` 节点化注入 [#节点化注入] ```go title="main.go" package main import ( "context" xCtx "github.com/bamboo-services/bamboo-base-go/defined/context" xReg "github.com/bamboo-services/bamboo-base-go/major/register" xRegNode "github.com/bamboo-services/bamboo-base-go/major/register/node" bContext "your-project/internal/constant/context" ) func main() { reg := xReg.Register(context.Background(), []xRegNode.RegNodeList{ // [!code highlight:3] // 框架资源 {Key: xCtx.DatabaseKey, Node: initDatabase}, {Key: xCtx.RedisClientKey, Node: initRedis}, // [!code highlight:3] // 业务资源 {Key: bContext.TencentSmsKey, Node: initTencentSms}, {Key: bContext.AliyunOssKey, Node: initAliyunOss}, }) reg.Serve.Run(":8080") } ``` 工具函数 [#工具函数] ```go title="pkg/ctx/business.go" package bCtxUtil import ( "context" sms "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms/v20210111" bContext "your-project/internal/constant/context" "your-project/internal/entity" ) // [!code highlight] func GetUserEntity(ctx context.Context) *entity.User { if v := ctx.Value(bContext.UserEntityKey); v != nil { return v.(*entity.User) } return nil } // [!code highlight] func MustGetTencentSmsClient(ctx context.Context) *sms.Client { if v := ctx.Value(bContext.TencentSmsKey); v != nil { return v.(*sms.Client) } panic("腾讯云短信客户端未注入") } ``` 下一步 [#下一步] # 概述 (/docs/bamboo-base-go/core/context) 上下文管理 [#上下文管理] Bamboo Base 提供了完整的上下文管理机制,基于标准 `context.Context` 实现**框架解耦**。 相关包 [#相关包] | 包 | 别名 | 说明 | | --------------------- | ---------- | -------- | | `.../context` | `xCtx` | 上下文键常量定义 | | `.../utility/ctxutil` | `xCtxUtil` | 上下文工具函数 | | `.../helper` | `xHelper` | 上下文中间件 | | `.../register/node` | `xRegNode` | 节点化注册系统 | 工作原理 [#工作原理] 上下文数据流 [#上下文数据流] ```go title="初始化流程" // [!code highlight:2] // 1. Register 创建节点管理器 xReg.Register(ctx, nodeList) │ ├── configInit() // 私有:加载配置 ├── loggerInit() // 私有:初始化日志 │ // [!code highlight:2] // 2. 内置节点注入雪花算法 ├── SnowflakeNodeKey → *xSnowflake.Node │ // [!code highlight:2] // 3. 自定义节点注入业务资源 ├── DatabaseKey → *gorm.DB ├── RedisClientKey → *redis.Client │ // [!code highlight:2] // 4. 引擎初始化,注入上下文到请求 └── engineInit() └── injectContext(ctx) ``` ```go title="请求流程" // [!code highlight:2] // 5. RequestContext 中间件注入请求级数据 xHelper.RequestContext() ├── RequestKey → UUID 请求标识 └── UserStartTimeKey → 请求开始时间 ``` 快速使用 [#快速使用] ```go import ( xCtxUtil "github.com/bamboo-services/bamboo-base-go/common/utility/context" ) func MyHandler(c *gin.Context) { // [!code highlight:2] // 获取标准上下文 ctx := c.Request.Context() // [!code highlight:2] // 获取数据库连接(Panic 版本) db := xCtxUtil.MustGetDB(ctx) // [!code highlight:2] // 获取数据库连接(错误返回版本) db, err := xCtxUtil.GetDB(ctx) // [!code highlight:2] // 获取 Redis 客户端 rdb := xCtxUtil.MustGetRDB(ctx) // [!code highlight:2] // 生成雪花 ID id := xCtxUtil.MustGenerateSnowflakeID(ctx) // [!code highlight:2] // 获取请求 ID requestID := xCtxUtil.GetRequestKey(ctx) } ``` 上下文节点 [#上下文节点] `ContextNode` 用于在初始化阶段保存上下文中的资源条目,配合 `ContextNodeList` 形成有序的键值链式列表。 ```go import xCtx "github.com/bamboo-services/bamboo-base-go/defined/context" ``` ContextNode 结构 [#contextnode-结构] ```go type ContextNode struct { Key xCtx.ContextKey Value any } ``` * `Key`:上下文键,用于标识资源类型 * `Value`:资源实例,支持任意类型 ContextNodeList 结构 [#contextnodelist-结构] ```go type ContextNodeList []ContextNode func NewCtxNodeList() ContextNodeList func (c ContextNodeList) GetList() []ContextNode func (c ContextNodeList) Get(key ContextKey) any func (c *ContextNodeList) Append(key ContextKey, value any) ``` **常用能力:** * `Get`:按键读取已注入资源 * `Append`:追加新的资源节点 * `GetList`:按顺序获取完整节点列表 使用示例 [#使用示例] ```go title="register.go" nodeList := xCtx.NewCtxNodeList() nodeList.Append(xCtx.DatabaseKey, initDatabase()) nodeList.Append(xCtx.RedisClientKey, initRedis()) reg := xReg.Register(ctx, []xRegNode.RegNodeList{ {Key: xCtx.RegNodeKey, Node: func(ctx context.Context) (any, error) { return nodeList, nil }}, }) ``` 注入上下文 [#注入上下文] 方式一:节点化注入(推荐) [#方式一节点化注入推荐] 在 `Register` 时通过自定义节点注入: ```go title="main.go" import ( "context" xCtx "github.com/bamboo-services/bamboo-base-go/defined/context" xReg "github.com/bamboo-services/bamboo-base-go/major/register" xRegNode "github.com/bamboo-services/bamboo-base-go/major/register/node" ) func main() { // [!code highlight:6] reg := xReg.Register(context.Background(), []xRegNode.RegNodeList{ {Key: xCtx.DatabaseKey, Node: initDatabase}, {Key: xCtx.RedisClientKey, Node: initRedis}, }) reg.Serve.Run(":8080") } // [!code highlight:2] // 数据库初始化节点 func initDatabase(ctx context.Context) (any, error) { db, err := gorm.Open(...) return db, err } // [!code highlight:2] // Redis 初始化节点 func initRedis(ctx context.Context) (any, error) { rdb := redis.NewClient(...) return rdb, nil } ``` 方式二:中间件注入 [#方式二中间件注入] 在业务中间件中注入(适用于请求级资源): ```go title="middleware.go" func InjectUserMiddleware() gin.HandlerFunc { return func(c *gin.Context) { // 获取用户信息... user := getUserFromToken(c) // [!code highlight:2] // 注入到 Gin 上下文 c.Set("user", user) c.Next() } } ``` 框架解耦 [#框架解耦] 新版本的核心改进是**框架解耦**: | 方面 | 旧设计 | 新设计 | | ------ | --------------------- | --------------------- | | 工具函数参数 | `*gin.Context` | `context.Context` | | 资源注入 | Gin 中间件 | 节点化系统 | | 上下文传递 | `c.Set()` / `c.Get()` | `context.WithValue()` | | 业务代码 | 依赖 Gin | 可独立测试 | **优势:** * ✅ 业务逻辑不再依赖 Gin 框架 * ✅ 更容易编写单元测试 * ✅ 支持在非 HTTP 场景使用(如定时任务、消息队列) 下一步 [#下一步] # 上下文键 (/docs/bamboo-base-go/core/context/keys) import { TypeTable } from '@/components/type-table'; 上下文键 [#上下文键] `xCtx` 包定义了用于在上下文中存储和获取数据的键常量。 ```go import xCtx "github.com/bamboo-services/bamboo-base-go/defined/context" ``` ContextKey 类型 [#contextkey-类型] ```go title="context.go" // [!code highlight] type ContextKey string func (s ContextKey) String() string { return string(s) } ``` 预定义键 [#预定义键] ```go title="context.go" const ( // [!code highlight:10] Nil ContextKey = "" // 空值 Exec ContextKey = "special_execution" // 特殊执行 RegNodeKey ContextKey = "context_reg_node" // 注册节点 RequestKey ContextKey = "context_request_key" // 请求唯一标识 ErrorCodeKey ContextKey = "context_error_code" // 错误码 ErrorMessageKey ContextKey = "context_error_message" // 错误描述 UserStartTimeKey ContextKey = "context_user_start_time" // 请求开始时间 DatabaseKey ContextKey = "context_database" // 数据库客户端 RedisClientKey ContextKey = "context_redis_client" // Redis 客户端 SnowflakeNodeKey ContextKey = "context_snowflake_node" // 雪花算法节点 ) ``` 键说明 [#键说明] 使用方式 [#使用方式] 节点化注入(推荐) [#节点化注入推荐] 在 `Register` 时通过节点注入资源: ```go import ( "context" xCtx "github.com/bamboo-services/bamboo-base-go/defined/context" xReg "github.com/bamboo-services/bamboo-base-go/major/register" xRegNode "github.com/bamboo-services/bamboo-base-go/major/register/node" ) // [!code highlight:8] // 注册时注入数据库和 Redis reg := xReg.Register(context.Background(), []xRegNode.RegNodeList{ {Key: xCtx.DatabaseKey, Node: initDatabase}, {Key: xCtx.RedisClientKey, Node: initRedis}, }) // [!code highlight:2] // 初始化节点函数 func initDatabase(ctx context.Context) (any, error) { db, err := gorm.Open(...) return db, err } ``` 从上下文获取值 [#从上下文获取值] 使用 `xCtxUtil` 工具函数获取注入的资源: ```go import xCtxUtil "github.com/bamboo-services/bamboo-base-go/common/utility/context" func MyHandler(c *gin.Context) { // [!code highlight:2] // 获取标准上下文 ctx := c.Request.Context() // [!code highlight:2] // 获取数据库连接 db := xCtxUtil.MustGetDB(ctx) // [!code highlight:2] // 获取 Redis 客户端 rdb := xCtxUtil.MustGetRDB(ctx) // [!code highlight:2] // 获取雪花算法节点 node := xCtxUtil.GetSnowflakeNode(ctx) } ``` 直接从上下文获取 [#直接从上下文获取] 也可以直接使用 `context.Value` 获取: ```go import xCtx "github.com/bamboo-services/bamboo-base-go/defined/context" func MyHandler(c *gin.Context) { ctx := c.Request.Context() // [!code highlight:3] // 直接获取并类型断言 if value := ctx.Value(xCtx.DatabaseKey); value != nil { db := value.(*gorm.DB) } } ``` 注入时机 [#注入时机] | 键 | 注入位置 | 说明 | | ------------------ | -------------------------- | ------- | | `RequestKey` | `xHelper.RequestContext()` | 框架自动注入 | | `UserStartTimeKey` | `xHelper.RequestContext()` | 框架自动注入 | | `SnowflakeNodeKey` | `xReg.Register()` 内置节点 | 框架自动注入 | | `DatabaseKey` | 自定义初始化节点 | 需手动注入 | | `RedisClientKey` | 自定义初始化节点 | 需手动注入 | | `ErrorCodeKey` | 错误处理中间件 | 错误时自动设置 | | `ErrorMessageKey` | 错误处理中间件 | 错误时自动设置 | 上下文流转 [#上下文流转] ``` Register(ctx, nodeList) │ ├── 内置节点注入 SnowflakeNodeKey │ ├── 自定义节点注入 DatabaseKey, RedisClientKey, ... │ └── engineInit() │ └── injectContext(ctx) 中间件 │ └── 每个 HTTP 请求 │ └── c.Request.Context() 包含所有注入的资源 ``` 下一步 [#下一步] # 工具函数 (/docs/bamboo-base-go/core/context/util) import { TypeTable } from '@/components/type-table'; 工具函数 [#工具函数] `xCtxUtil` 包提供便捷的上下文操作函数,使用标准 `context.Context` 实现**框架解耦**。 ```go import xCtxUtil "github.com/bamboo-services/bamboo-base-go/common/utility/context" ``` 设计理念 [#设计理念] 新版 `xCtxUtil` 采用**双版本 API** 设计: | 版本 | 前缀 | 行为 | 适用场景 | | -------- | ------- | ------------------ | ----------- | | Panic 版本 | `Must*` | 失败时 panic | 确定资源已注入的场景 | | 错误返回版本 | `Get*` | 返回 `*xError.Error` | 需要优雅处理错误的场景 | 通用函数 [#通用函数] IsDebugMode [#isdebugmode] 判断当前是否处于调试模式。 ```go title="common.go" // [!code highlight] func IsDebugMode() bool ``` **实现:** ```go title="common.go" func IsDebugMode() bool { // [!code highlight] return xEnv.GetEnvBool(xEnv.Debug, false) } ``` **示例:** ```go if xCtxUtil.IsDebugMode() { // [!code highlight] // 调试模式下的特殊处理 log.Debug(ctx, "调试信息") } ``` CalcOverheadTime [#calcoverheadtime] 计算当前请求的耗时(微秒级)。 ```go title="common.go" // [!code highlight] func CalcOverheadTime(ctx context.Context) int64 ``` **说明:** * 仅在调试模式下计算耗时 * 非调试模式始终返回 0 * 单位为微秒(microseconds) **示例:** ```go // [!code highlight] overhead := xCtxUtil.CalcOverheadTime(c.Request.Context()) // overhead = 1234 (微秒) ``` GetRequestKey [#getrequestkey] 获取请求唯一标识。 ```go title="common.go" // [!code highlight] func GetRequestKey(ctx context.Context) string ``` **示例:** ```go // [!code highlight] requestID := xCtxUtil.GetRequestKey(c.Request.Context()) // requestID = "550e8400-e29b-41d4-a716-446655440000" ``` GetErrorMessage [#geterrormessage] 获取上下文中的错误消息。 ```go title="common.go" // [!code highlight] func GetErrorMessage(ctx context.Context) string ``` 通用组件获取 [#通用组件获取] 当需要读取自定义注入的组件时,可使用泛型的 `MustGet` / `Get`: ```go import xCtx "github.com/bamboo-services/bamboo-base-go/defined/context" ``` MustGet [#mustget] 通用组件获取(Panic 版本)。 ```go title="custom.go" // [!code highlight] func MustGet[T any](ctx context.Context, key xCtx.ContextKey) T ``` **读取优先级:** 1. `RegNodeKey` 聚合的节点列表 2. `ctx.Value(key)` **说明:** * 若传入 `*gin.Context` 会自动转换为 `c.Request.Context()` * 未找到组件时会触发 `panic` **示例:** ```go // [!code highlight:3] // 直接从 Gin 上下文获取 cache := xCtxUtil.MustGet[*xCache.Cache](c, xCtx.CacheKey) // [!code highlight:3] // 或者先获取标准 context ctx := c.Request.Context() cache := xCtxUtil.MustGet[*xCache.Cache](ctx, xCtx.CacheKey) ``` Get [#get] 通用组件获取(错误返回版本)。 ```go title="custom.go" // [!code highlight] func Get[T any](ctx context.Context, key xCtx.ContextKey) (T, *xError.Error) ``` **示例:** ```go cache, err := xCtxUtil.Get[*xCache.Cache](ctx, xCtx.CacheKey) if err != nil { return nil, err } ``` MustGetDB [#mustgetdb] 从上下文获取 GORM 数据库连接(Panic 版本)。 ```go title="database.go" // [!code highlight] func MustGetDB(ctx context.Context) *gorm.DB ``` **注意:** * 如果上下文中未找到数据库连接,会触发 `panic` * 返回的连接已自动绑定当前上下文(`WithContext`) **示例:** ```go func GetUser(ctx *gin.Context, id string) (*entity.User, error) { // [!code highlight] db := xCtxUtil.MustGetDB(ctx.Request.Context()) var user entity.User if err := db.Where("id = ?", id).First(&user).Error; err != nil { return nil, err } return &user, nil } ``` GetDB [#getdb] 从上下文获取 GORM 数据库连接(错误返回版本)。 ```go title="database.go" // [!code highlight] func GetDB(ctx context.Context) (*gorm.DB, *xError.Error) ``` **示例:** ```go func GetUser(ctx *gin.Context, id string) (*entity.User, *xError.Error) { // [!code highlight] db, err := xCtxUtil.GetDB(ctx.Request.Context()) if err != nil { return nil, err } var user entity.User if dbErr := db.Where("id = ?", id).First(&user).Error; dbErr != nil { return nil, xError.NewInternalServerError(ctx.Request.Context(), "查询失败", dbErr) } return &user, nil } ``` Redis 函数 [#redis-函数] MustGetRDB [#mustgetrdb] 从上下文获取 Redis 客户端(Panic 版本)。 ```go title="nosql.go" // [!code highlight] func MustGetRDB(ctx context.Context) *redis.Client ``` **注意:** * 如果上下文中未找到 Redis 客户端,会触发 `panic` **示例:** ```go func GetCache(ctx *gin.Context, key string) (string, error) { // [!code highlight] rdb := xCtxUtil.MustGetRDB(ctx.Request.Context()) return rdb.Get(ctx, key).Result() } ``` GetRDB [#getrdb] 从上下文获取 Redis 客户端(错误返回版本)。 ```go title="nosql.go" // [!code highlight] func GetRDB(ctx context.Context) (*redis.Client, *xError.Error) ``` **示例:** ```go func GetCache(ctx *gin.Context, key string) (string, *xError.Error) { // [!code highlight] rdb, err := xCtxUtil.GetRDB(ctx.Request.Context()) if err != nil { return "", err } val, redisErr := rdb.Get(ctx, key).Result() if redisErr != nil { return "", xError.NewError(ctx.Request.Context(), xError.CacheError, "缓存读取失败", false, redisErr) } return val, nil } ``` 雪花算法函数 [#雪花算法函数] GetSnowflakeNode [#getsnowflakenode] 从上下文获取雪花算法节点。 ```go title="snowflake.go" // [!code highlight] func GetSnowflakeNode(ctx context.Context) *xSnowflake.Node ``` **说明:** * 如果上下文中不存在节点,会**回退到默认节点** * 确保即使在非 HTTP 请求上下文中也能正常生成 ID **示例:** ```go // [!code highlight] node := xCtxUtil.GetSnowflakeNode(ctx.Request.Context()) id := node.MustGenerate() ``` MustGenerateSnowflakeID [#mustgeneratesnowflakeid] 生成普通雪花 ID(Panic 版本)。 ```go title="snowflake.go" // [!code highlight] func MustGenerateSnowflakeID(ctx context.Context) xSnowflake.SnowflakeID ``` **示例:** ```go // [!code highlight] id := xCtxUtil.MustGenerateSnowflakeID(ctx.Request.Context()) // id = 1234567890123456789 ``` GenerateSnowflakeID [#generatesnowflakeid] 生成普通雪花 ID(错误返回版本)。 ```go title="snowflake.go" // [!code highlight] func GenerateSnowflakeID(ctx context.Context) (xSnowflake.SnowflakeID, error) ``` MustGenerateGeneSnowflakeID [#mustgenerategenesnowflakeid] 生成带业务基因的雪花 ID(Panic 版本)。 ```go title="snowflake.go" // [!code highlight] func MustGenerateGeneSnowflakeID(ctx context.Context, gene xSnowflake.Gene) xSnowflake.SnowflakeID ``` **示例:** ```go // [!code highlight:2] // 生成用户 ID userID := xCtxUtil.MustGenerateGeneSnowflakeID(ctx.Request.Context(), xSnowflake.GeneUser) // [!code highlight:2] // 生成订单 ID orderID := xCtxUtil.MustGenerateGeneSnowflakeID(ctx.Request.Context(), xSnowflake.GeneOrder) ``` GenerateGeneSnowflakeID [#generategenesnowflakeid] 生成带业务基因的雪花 ID(错误返回版本)。 ```go title="snowflake.go" // [!code highlight] func GenerateGeneSnowflakeID(ctx context.Context, gene xSnowflake.Gene) (xSnowflake.SnowflakeID, error) ``` 函数一览 [#函数一览] 完整示例 [#完整示例] ```go title="service.go" package logic import ( "context" xError "github.com/bamboo-services/bamboo-base-go/common/error" xSnowflake "github.com/bamboo-services/bamboo-base-go/common/snowflake" xCtxUtil "github.com/bamboo-services/bamboo-base-go/common/utility/context" "github.com/gin-gonic/gin" ) func (l *UserLogic) CreateUser(c *gin.Context, username string) (*entity.User, *xError.Error) { // [!code highlight:2] // 获取标准上下文 ctx := c.Request.Context() // [!code highlight:2] // 获取数据库连接(错误返回版本) db, err := xCtxUtil.GetDB(ctx) if err != nil { return nil, err } // [!code highlight:2] // 生成用户 ID(Panic 版本,确定雪花节点已注入) userID := xCtxUtil.MustGenerateGeneSnowflakeID(ctx, xSnowflake.GeneUser) user := &entity.User{ Username: username, } user.ID = userID if dbErr := db.Create(user).Error; dbErr != nil { return nil, xError.NewInternalServerError(ctx, "创建用户失败", dbErr) } // [!code highlight:2] // 缓存用户信息(Panic 版本) rdb := xCtxUtil.MustGetRDB(ctx) rdb.Set(ctx, "user:"+userID.String(), user, time.Hour) return user, nil } ``` 迁移指南 [#迁移指南] 从旧版本迁移到新版本: | 旧版本 | 新版本 | | ---------------------------------------------- | ----------------------------------------------------------------- | | `xCtxUtil.GetDB(c *gin.Context)` | `xCtxUtil.MustGetDB(c.Request.Context())` | | `xCtxUtil.GetRDB(c *gin.Context)` | `xCtxUtil.MustGetRDB(c.Request.Context())` | | `xCtxUtil.GetSnowflakeNode(c *gin.Context)` | `xCtxUtil.GetSnowflakeNode(c.Request.Context())` | | `xCtxUtil.GenerateSnowflakeID(c *gin.Context)` | `xCtxUtil.MustGenerateSnowflakeID(c.Request.Context())` | | `xCtxUtil.GenerateGeneSnowflakeID(c, gene)` | `xCtxUtil.MustGenerateGeneSnowflakeID(c.Request.Context(), gene)` | **关键变化:** 1. 参数从 `*gin.Context` 改为 `context.Context` 2. 在 Gin Handler 中使用 `c.Request.Context()` 获取上下文 3. 新增 `Get*` 系列函数返回错误而非 panic 下一步 [#下一步] # 配置初始化 (/docs/bamboo-base-go/core/init/config) 配置初始化 [#配置初始化] `ConfigInit` 方法从 `.env` 文件加载环境变量到系统环境中。 ConfigInit [#configinit] ```go title="register_config.go" // [!code highlight] func (r *Reg) ConfigInit() ``` **实现:** ```go title="register_config.go" func (r *Reg) ConfigInit() { // [!code highlight] // 加载 .env 文件到环境变量(忽略不存在的错误) _ = godotenv.Load() } ``` 配置文件 [#配置文件] 在项目根目录创建 `.env` 文件: ```bash title=".env" # 调试模式 XLF_DEBUG=true # 雪花算法配置 SNOWFLAKE_DATACENTER_ID=1 SNOWFLAKE_NODE_ID=1 # 服务配置 XLF_PORT=8080 ``` 读取配置 [#读取配置] 使用 `xEnv` 包读取环境变量: ```go import xEnv "github.com/bamboo-services/bamboo-base-go/defined/env" // [!code highlight:2] // 读取布尔值 debug := xEnv.GetEnvBool(xEnv.Debug, false) // [!code highlight:2] // 读取字符串 port := xEnv.GetEnvString("SERVER_PORT", "8080") // [!code highlight:2] // 读取整数 nodeID := xEnv.GetEnvInt("SNOWFLAKE_NODE_ID", 1) ``` 预定义环境变量 [#预定义环境变量] 系统配置 [#系统配置] | 变量名 | 类型 | 默认值 | 说明 | | ----------------- | ------ | ----------- | --------- | | `XLF_DEBUG` | bool | `false` | 调试模式开关 | | `XLF_HOST` | string | `localhost` | HTTP 监听地址 | | `XLF_PORT` | int | `1118` | HTTP 监听端口 | | `GRPC_PORT` | int | `1119` | gRPC 监听端口 | | `GRPC_REFLECTION` | bool | `false` | gRPC 反射开关 | 数据库配置 [#数据库配置] | 变量名 | 类型 | 默认值 | 说明 | | ------------------- | ------ | --------------- | ------ | | `DATABASE_HOST` | string | `localhost` | 数据库主机 | | `DATABASE_PORT` | int | `5432` | 数据库端口 | | `DATABASE_USER` | string | - | 数据库用户名 | | `DATABASE_PASS` | string | - | 数据库密码 | | `DATABASE_NAME` | string | - | 数据库名称 | | `DATABASE_CHARSET` | string | `utf8mb4` | 字符集 | | `DATABASE_TIMEZONE` | string | `Asia/Shanghai` | 时区 | | `DATABASE_PREFIX` | string | - | 表前缀 | Redis 配置 [#redis-配置] | 变量名 | 类型 | 默认值 | 说明 | | ----------------- | ------ | ----------- | ----------- | | `NOSQL_HOST` | string | `localhost` | Redis 主机 | | `NOSQL_PORT` | int | `6379` | Redis 端口 | | `NOSQL_PASS` | string | - | Redis 密码 | | `NOSQL_DATABASE` | int | `0` | Redis 数据库索引 | | `NOSQL_POOL_SIZE` | int | `10` | 连接池大小 | | `NOSQL_PREFIX` | string | - | 键前缀 | 雪花算法配置 [#雪花算法配置] | 变量名 | 类型 | 默认值 | 说明 | | ------------------------- | --- | ---- | -------------- | | `SNOWFLAKE_DATACENTER_ID` | int | 自动生成 | 数据中心 ID (0-31) | | `SNOWFLAKE_NODE_ID` | int | 自动生成 | 节点 ID (0-31) | 日志配置 [#日志配置] | 变量名 | 类型 | 默认值 | 说明 | | ----------------- | ------ | -------- | ------------ | | `LOG_LEVEL` | string | `info` | 日志级别 | | `LOG_PATH` | string | `./logs` | 日志目录 | | `LOG_MAX_SIZE` | int | `100` | 单文件最大尺寸 (MB) | | `LOG_MAX_AGE` | int | `30` | 文件保留天数 | | `LOG_MAX_BACKUPS` | int | `10` | 最大备份文件数 | | `LOG_COMPRESS` | bool | `true` | 是否压缩归档 | 运行环境配置 [#运行环境配置] | 变量名 | 类型 | 默认值 | 说明 | | ------------- | ------ | --------------- | ---- | | `ENV` | string | `development` | 运行环境 | | `APP_NAME` | string | - | 应用名称 | | `APP_VERSION` | string | - | 应用版本 | | `TIMEZONE` | string | `Asia/Shanghai` | 系统时区 | 注意事项 [#注意事项] * `.env` 文件不存在时不会报错,会静默忽略 * 环境变量优先级:系统环境变量 > `.env` 文件 * 建议将 `.env` 添加到 `.gitignore`,避免敏感信息泄露 下一步 [#下一步] # 上下文注入 (/docs/bamboo-base-go/core/init/context) 上下文注入 [#上下文注入] 新版本采用**节点化系统**实现上下文资源注入,取代了原有的 `SystemContextInit` 方法。 注入机制 [#注入机制] 节点化注入(初始化时) [#节点化注入初始化时] 在 `Register` 时通过自定义节点注入全局资源: ```go title="main.go" import ( "context" xCtx "github.com/bamboo-services/bamboo-base-go/defined/context" xReg "github.com/bamboo-services/bamboo-base-go/major/register" xRegNode "github.com/bamboo-services/bamboo-base-go/major/register/node" ) func main() { // [!code highlight:6] reg := xReg.Register(context.Background(), []xRegNode.RegNodeList{ {Key: xCtx.DatabaseKey, Node: initDatabase}, {Key: xCtx.RedisClientKey, Node: initRedis}, }) reg.Serve.Run(":8080") } ``` 中间件注入(请求时) [#中间件注入请求时] `xHelper.RequestContext()` 中间件为每个请求注入请求级数据: ```go title="helper/request_context.go" func RequestContext() gin.HandlerFunc { return func(c *gin.Context) { // [!code highlight:2] // 生成请求唯一标识 requestID := uuid.New().String() // [!code highlight:2] // 记录请求开始时间 startTime := time.Now() // 注入到上下文 ctx := c.Request.Context() ctx = context.WithValue(ctx, xCtx.RequestKey, requestID) ctx = context.WithValue(ctx, xCtx.UserStartTimeKey, startTime) c.Request = c.Request.WithContext(ctx) c.Next() } } ``` 上下文合并 [#上下文合并] `injectContext` 中间件将初始化上下文合并到每个请求: ```go title="register_gin.go" func injectContext(ctx context.Context) func(c *gin.Context) { return func(c *gin.Context) { // [!code highlight:2] // 将初始化上下文(包含 DB、Redis、Snowflake 等)注入到请求 c.Request = c.Request.WithContext(ctx) c.Next() } } ``` 注入的资源 [#注入的资源] | 资源 | Key | 类型 | 注入时机 | | --------- | ------------------ | ------------------ | -------- | | 雪花算法节点 | `SnowflakeNodeKey` | `*xSnowflake.Node` | 内置节点(自动) | | 数据库连接 | `DatabaseKey` | `*gorm.DB` | 自定义节点 | | Redis 客户端 | `RedisClientKey` | `*redis.Client` | 自定义节点 | | 请求 ID | `RequestKey` | `string` | 请求中间件 | | 开始时间 | `UserStartTimeKey` | `time.Time` | 请求中间件 | 获取上下文资源 [#获取上下文资源] 使用 `xCtxUtil` 包从上下文获取注入的资源: ```go import xCtxUtil "github.com/bamboo-services/bamboo-base-go/common/utility/context" func CreateOrder(c *gin.Context) { // [!code highlight:2] // 获取标准上下文 ctx := c.Request.Context() // [!code highlight:2] // 获取数据库连接 db := xCtxUtil.MustGetDB(ctx) // [!code highlight:2] // 获取 Redis 客户端 rdb := xCtxUtil.MustGetRDB(ctx) // [!code highlight:2] // 获取雪花算法节点 node := xCtxUtil.GetSnowflakeNode(ctx) // 生成订单 ID orderID := node.MustGenerate(xSnowflake.GeneOrder) } ``` 自定义初始化节点 [#自定义初始化节点] 创建自定义初始化节点的标准模式: ```go title="init_database.go" import ( "context" xLog "github.com/bamboo-services/bamboo-base-go/common/log" ) // [!code highlight:2] // 初始化节点函数签名:func(ctx context.Context) (any, error) func initDatabase(ctx context.Context) (any, error) { log := xLog.WithName(xLog.NamedINIT) log.Info(ctx, "初始化数据库连接") // 构建 DSN dsn := buildDSN() // [!code highlight:2] // 创建数据库连接 db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) if err != nil { return nil, err } log.Info(ctx, "数据库连接成功") // [!code highlight:2] // 返回值将被存入上下文 return db, nil } ``` 请求 ID [#请求-id] 每个请求都会自动生成唯一的请求 ID: ```go func GetRequestID(c *gin.Context) string { // [!code highlight] return xCtxUtil.GetRequestKey(c.Request.Context()) } ``` 请求 ID 会: * 存储在上下文中,通过 `xCtx.RequestKey` 访问 * 添加到响应头 `X-Request-ID` * 用于日志追踪和问题排查 迁移指南 [#迁移指南] 从旧版本迁移: | 旧版本 | 新版本 | | -------------------------- | ----------------------------------------- | | `xReg.SystemContextInit()` | 节点化系统自动处理 | | `c.Set(key, value)` | `context.WithValue()` | | `c.Get(key)` | `ctx.Value(key)` | | `xCtxUtil.GetDB(c)` | `xCtxUtil.MustGetDB(c.Request.Context())` | 下一步 [#下一步] # 引擎初始化 (/docs/bamboo-base-go/core/init/engine) 引擎初始化 [#引擎初始化] `engineInit` 方法创建并配置 Gin 引擎,注册内置中间件、自定义验证器,并将初始化上下文注入到每个 HTTP 请求。 engineInit [#engineinit] ```go title="register_gin.go" // [!code highlight:2] // 私有方法,在 Register 流程中自动调用 func (r *Reg) engineInit() ``` 实现细节 [#实现细节] ```go title="register_gin.go" func (r *Reg) engineInit() { log := xLog.WithName(xLog.NamedINIT) log.Debug(r.Init.Ctx, "初始化 GIN 引擎") // [!code highlight:3] if !xCtxUtil.IsDebugMode() { gin.SetMode(gin.ReleaseMode) } // [!code highlight:6] // 注册验证器和翻译器 if v, ok := binding.Validator.Engine().(*validator.Validate); ok { xVaild.RegisterTranslator(v) xVaild.RegisterCustomValidators(v) } // [!code highlight:7] // 创建 Gin 引擎并注册中间件 r.Serve = gin.New(func(engine *gin.Engine) { engine.Use(xHelper.RequestContext()) engine.Use(xHelper.PanicRecovery()) engine.Use(xHelper.HttpLogger()) engine.Use(injectContext(r.Init.Ctx)) // 注入初始化上下文 }) } ``` 上下文注入 [#上下文注入] 关键改进:将节点管理器的上下文注入到每个 HTTP 请求: ```go title="register_gin.go" // [!code highlight:2] // 注入初始化上下文到每个请求 func injectContext(ctx context.Context) func(c *gin.Context) { return func(c *gin.Context) { // [!code highlight:2] // 将初始化上下文合并到请求上下文 c.Request = c.Request.WithContext(ctx) c.Next() } } ``` **这意味着:** * 所有通过节点注入的资源(数据库、Redis 等)都可以通过 `c.Request.Context()` 获取 * 业务代码使用标准 `context.Context`,实现框架解耦 内置中间件 [#内置中间件] | 中间件 | 说明 | | -------------------------- | ----------------------- | | `xHelper.RequestContext()` | 请求上下文处理,生成请求 ID 等 | | `xHelper.PanicRecovery()` | Panic 恢复,防止单个请求崩溃影响整个服务 | | `xHelper.HttpLogger()` | HTTP 请求日志记录 | | `injectContext(ctx)` | 注入初始化上下文到请求 | 运行模式 [#运行模式] 根据 `DEBUG` 环境变量自动切换 Gin 运行模式: | 环境变量 | Gin 模式 | 说明 | | ------------------ | ------- | ----------- | | `DEBUG=true` | Debug | 输出详细调试信息 | | `DEBUG=false` 或未设置 | Release | 生产模式,关闭调试输出 | 验证器 [#验证器] 翻译器 [#翻译器] 注册中文翻译器,将验证错误信息翻译为中文: ```go type CreateUserReq struct { Name string `json:"name" binding:"required"` Email string `json:"email" binding:"required,email"` } // 验证失败时返回中文错误信息 // "Name 为必填字段" // "Email 必须是有效的邮箱地址" ``` 自定义验证器 [#自定义验证器] 框架预置了常用的自定义验证器: ```go type CreateUserReq struct { // [!code highlight] Phone string `json:"phone" binding:"required,phone"` // 手机号验证 // [!code highlight] IDCard string `json:"id_card" binding:"required,idcard"` // 身份证验证 } ``` 使用引擎 [#使用引擎] 初始化完成后,通过 `reg.Serve` 访问 Gin 引擎: ```go reg := xReg.Register(context.Background(), nodeList) // [!code highlight:4] // 注册路由 reg.Serve.GET("/api/users", userHandler.List) reg.Serve.POST("/api/users", userHandler.Create) reg.Serve.PUT("/api/users/:id", userHandler.Update) // [!code highlight:2] // 路由分组 api := reg.Serve.Group("/api/v1") { api.GET("/products", productHandler.List) api.POST("/orders", orderHandler.Create) } // [!code highlight:2] // 启动服务 reg.Serve.Run(":8080") ``` 在 Handler 中获取资源 [#在-handler-中获取资源] ```go func MyHandler(c *gin.Context) { // [!code highlight:2] // 获取标准上下文(包含所有注入的资源) ctx := c.Request.Context() // [!code highlight:2] // 使用 xCtxUtil 获取资源 db := xCtxUtil.MustGetDB(ctx) rdb := xCtxUtil.MustGetRDB(ctx) node := xCtxUtil.GetSnowflakeNode(ctx) } ``` 下一步 [#下一步] # 快速开始 (/docs/bamboo-base-go/core/init) import { TypeTable } from '@/components/type-table'; xReg 包 [#xreg-包] `xReg` 包是 Bamboo Base 的核心注册模块,采用**节点化模式**实现灵活的组件初始化和依赖注入。 ```go import xReg "github.com/bamboo-services/bamboo-base-go/major/register" import xRegNode "github.com/bamboo-services/bamboo-base-go/major/register/node" ``` Reg 结构体 [#reg-结构体] 应用程序的核心注册结构,包含所有初始化后的组件实例: ```go title="register.go" type Reg struct { // [!code highlight:2] Serve *gin.Engine // Gin 引擎实例 Init *xRegNode.RegNode // 初始化节点管理器 } ``` Register [#register] 注册并初始化应用的所有核心组件,支持自定义初始化节点。 ```go title="register.go" // [!code highlight] func Register(ctx context.Context, nodeList []xRegNode.RegNodeList) *Reg ``` **参数说明:** **初始化顺序:** ``` Register(ctx, nodeList) ├── configInit() // 1. 加载配置(私有) ├── loggerInit() // 2. 初始化日志(私有) ├── SnowflakeInit // 3. 内置雪花算法节点 ├── [自定义节点...] // 4. 用户自定义节点 └── engineInit() // 5. 初始化 Gin 引擎(私有) 启动阶段(推荐) └── xMain.Runner(...) // 6. 启动 HTTP 服务并托管优雅关闭 ``` **示例:** ```go title="main.go" package main import ( "context" xCtx "github.com/bamboo-services/bamboo-base-go/defined/context" xLog "github.com/bamboo-services/bamboo-base-go/common/log" xMain "github.com/bamboo-services/bamboo-base-go/major/main" xReg "github.com/bamboo-services/bamboo-base-go/major/register" xRegNode "github.com/bamboo-services/bamboo-base-go/major/register/node" "github.com/gin-gonic/gin" ) func main() { // [!code highlight:8] // 初始化,传入自定义节点 reg := xReg.Register(context.Background(), []xRegNode.RegNodeList{ { Key: xCtx.DatabaseKey, Node: initDatabase, // 自定义数据库初始化 }, }) log := xLog.WithName(xLog.NamedMAIN) xMain.Runner(reg, log, func(reg *xReg.Reg) { reg.Serve.GET("/ping", func(c *gin.Context) { c.String(200, "pong") }) }) } // [!code highlight:2] // 自定义初始化节点 func initDatabase(ctx context.Context) (any, error) { // 从上下文获取配置(前面节点的结果) // 初始化数据库连接 db, err := gorm.Open(...) return db, err } ``` 节点化系统 [#节点化系统] RegNode 结构 [#regnode-结构] ```go title="register/node/node.go" // [!code highlight:2] // Node 定义初始化函数签名 type Node func(ctx context.Context) (any, error) // [!code highlight:2] // RegNodeList 存储节点信息 type RegNodeList struct { Key xCtx.ContextKey // 上下文键 Node Node // 初始化函数 } // [!code highlight:2] // RegNode 节点管理器 type RegNode struct { list []RegNodeList Ctx context.Context } ``` 核心方法 [#核心方法] ```go title="register/node/node.go" // [!code highlight:2] // Use 注册初始化节点 func (rn *RegNode) Use(ctxKey xCtx.ContextKey, registerFunc Node) // [!code highlight:2] // Exec 按顺序执行所有节点 func (rn *RegNode) Exec() ``` 执行流程 [#执行流程] ```go title="register/node/node.go" func (rn *RegNode) Exec() { for i, node := range rn.list { // [!code highlight:2] // 执行节点初始化 val, err := node.Node(rn.Ctx) if err != nil { panic(fmt.Sprintf("执行注册节点失败: index=%d Key=%v err=%v", i, node.Key, err)) } // [!code highlight:2] // 将结果存入上下文,供后续节点使用 rn.Ctx = context.WithValue(rn.Ctx, node.Key, val) } rn.list = nil // 释放内存 } ``` **特点:** * 🔄 **依赖注入**:每个节点可访问前面节点的初始化结果 * 📊 **顺序执行**:按注册顺序依次初始化 * 🛡️ **错误处理**:任何节点失败都会 panic 并输出详细信息 内置初始化 [#内置初始化] 配置初始化(私有) [#配置初始化私有] 从 `.env` 文件加载环境变量配置。 详见:[配置初始化](/docs/bamboo-base-go/core/init/config) 日志初始化(私有) [#日志初始化私有] 初始化日志记录器,支持控制台彩色输出和文件 JSON 记录。 详见:[日志初始化](/docs/bamboo-base-go/core/init/logger) 雪花算法节点(内置) [#雪花算法节点内置] 作为内置节点自动注册,初始化雪花算法节点。 ```go title="register.go" // [!code highlight] reg.Init.Use(xCtx.SnowflakeNodeKey, xInit.SnowflakeInit) ``` 详见:[雪花算法初始化](/docs/bamboo-base-go/core/init/snowflake) 引擎初始化(私有) [#引擎初始化私有] 初始化 Gin 引擎,注册内置中间件和验证器,并注入初始化上下文。 详见:[引擎初始化](/docs/bamboo-base-go/core/init/engine) 服务运行时(公共) [#服务运行时公共] 推荐使用 `xMain.Runner` 启动服务,统一处理: * 信号监听(`SIGINT`/`SIGTERM`) * 优雅关闭 * 后台协程托管 详见:[服务运行时](/docs/bamboo-base-go/core/init/runner) > gRPC 为可选能力,已拆分到独立板块:[`gRPC(可选)`](/docs/bamboo-base-go/grpc) 上下文注入 [#上下文注入] 引擎初始化时会自动将节点管理器的上下文注入到每个 HTTP 请求: ```go title="register_gin.go" // [!code highlight:2] // 注入初始化上下文到每个请求 func injectContext(ctx context.Context) func(c *gin.Context) { return func(c *gin.Context) { c.Request = c.Request.WithContext(ctx) c.Next() } } ``` 这意味着在业务代码中可以通过 `c.Request.Context()` 获取初始化时注入的所有资源。 调试模式 [#调试模式] 通过环境变量 `XLF_DEBUG` 控制调试模式: | 环境变量 | 值 | 效果 | | ----------- | ------------ | ------------------ | | `XLF_DEBUG` | `true` | 启用调试模式,日志级别为 Debug | | `XLF_DEBUG` | `false` 或未设置 | 生产模式,日志级别为 Info | 完整示例 [#完整示例] ```go title="main.go" package main import ( "context" xCtx "github.com/bamboo-services/bamboo-base-go/defined/context" xReg "github.com/bamboo-services/bamboo-base-go/major/register" xRegNode "github.com/bamboo-services/bamboo-base-go/major/register/node" xResult "github.com/bamboo-services/bamboo-base-go/major/result" "github.com/gin-gonic/gin" ) func main() { // [!code highlight:9] // 初始化,注册自定义节点 reg := xReg.Register(context.Background(), []xRegNode.RegNodeList{ {Key: xCtx.DatabaseKey, Node: initDatabase}, {Key: xCtx.RedisClientKey, Node: initRedis}, }) // 注册路由 // [!code highlight:4] reg.Serve.GET("/api/status", func(c *gin.Context) { xResult.SuccessHasData(c, "服务正常", gin.H{ "status": "running", }) }) // 启动服务 // [!code highlight] reg.Serve.Run(":8080") } // [!code highlight:2] // 数据库初始化节点 func initDatabase(ctx context.Context) (any, error) { // 初始化数据库... return db, nil } // [!code highlight:2] // Redis 初始化节点 func initRedis(ctx context.Context) (any, error) { // 初始化 Redis... return rdb, nil } ``` 下一步 [#下一步] # 日志初始化 (/docs/bamboo-base-go/core/init/logger) import { TypeTable } from '@/components/type-table'; 日志初始化 [#日志初始化] `LoggerInit` 方法配置并初始化全局日志记录器,支持双输出模式。 LoggerInit [#loggerinit] ```go title="register_logger.go" // [!code highlight] func (r *Reg) LoggerInit() ``` **日志输出:** | 输出目标 | 格式 | 说明 | | ---- | ---- | ------------------------------------------ | | 控制台 | 彩色文本 | `<时间戳> [<日志等级>] [] [<日志类型>] <输出内容>` | | 文件 | JSON | Info 级别及以上,支持自动切割和归档 | 实现细节 [#实现细节] ```go title="register_logger.go" func (r *Reg) LoggerInit() { // [!code highlight:7] // 创建日志切割写入器 rotator, err := xLog.NewRotatingWriter(xLog.RotatorConfig{ Dir: ".logs", BaseName: "log", Ext: ".log", MaxSize: 10 * 1024 * 1024, // 10MB }) if err != nil { panic("[INIT] 日志切割器创建失败: " + err.Error()) } // [!code highlight:4] // 确定日志级别 logLevel := slog.LevelInfo debugMode := isDebugMode() if debugMode { logLevel = slog.LevelDebug } // [!code highlight:6] // 创建自定义 Handler handler := xLog.NewLogHandler(xLog.HandlerConfig{ Console: os.Stdout, File: rotator, Level: logLevel, IsDebugMode: debugMode, }) // [!code highlight:2] // 设置为全局默认 logger logger := slog.New(handler) slog.SetDefault(logger) } ``` 日志切割配置 [#日志切割配置] 日志切割规则 [#日志切割规则] 按大小切割 [#按大小切割] 单文件超过 10MB 自动切割: ``` .logs/ ├── log.log # 当前日志文件 ├── log.0.log # 切割后的历史文件 ├── log.1.log └── log.2.log ``` 按时间归档 [#按时间归档] 每天 00:00:05 将前一天日志打包: ``` .logs/ ├── log.log └── logger-2024-01-15.tar.gz # 归档文件 ``` 日志级别 [#日志级别] | 模式 | 日志级别 | 说明 | | ------------------- | ----- | -------------- | | 调试模式 (`DEBUG=true`) | Debug | 输出所有级别日志 | | 生产模式 | Info | 仅输出 Info 及以上级别 | 使用日志 [#使用日志] ```go import xLog "github.com/bamboo-services/bamboo-base-go/common/log" // [!code highlight:2] // 创建带名称的日志记录器 log := xLog.WithName(xLog.NamedINIT) // [!code highlight] log.Info(ctx, "服务启动成功") // [!code highlight] log.Debug(ctx, "调试信息") // [!code highlight] log.Error(ctx, "发生错误: " + err.Error()) // [!code highlight:2] // 带结构化参数 log.SugarDebug(ctx, "用户登录", "user_id", 123, "ip", "192.168.1.1") ``` 下一步 [#下一步] # 服务运行时 (/docs/bamboo-base-go/core/init/runner) import { TypeTable } from '@/components/type-table'; 服务运行时 [#服务运行时] `xMain.Runner` 用于统一启动 HTTP 服务、托管后台协程并在信号到达时优雅关闭。 ```go import xMain "github.com/bamboo-services/bamboo-base-go/major/main" ``` Runner [#runner] ```go title="runner.go" func Runner( reg *xReg.Reg, log *xLog.LogNamedLogger, routeFunc func(reg *xReg.Reg), goroutineFunc ...func(ctx context.Context, option ...any), ) ``` 功能 [#功能] * 从环境变量读取 `XLF_HOST` 与 `XLF_PORT` 作为监听地址,默认 `localhost:1118`。 * 自动监听 `SIGINT`/`SIGTERM`,触发优雅关闭。 * 支持注册路由函数。 * 支持启动多个后台协程并统一等待退出。 * HTTP 关闭超时时间为 30 秒。 参数说明 [#参数说明] 快速使用 [#快速使用] ```go title="main.go" package main import ( "context" xCtx "github.com/bamboo-services/bamboo-base-go/defined/context" xLog "github.com/bamboo-services/bamboo-base-go/common/log" xMain "github.com/bamboo-services/bamboo-base-go/major/main" xReg "github.com/bamboo-services/bamboo-base-go/major/register" xRegNode "github.com/bamboo-services/bamboo-base-go/major/register/node" ) func main() { reg := xReg.Register(context.Background(), []xRegNode.RegNodeList{ {Key: xCtx.DatabaseKey, Node: initDatabase}, {Key: xCtx.RedisClientKey, Node: initRedis}, }) log := xLog.WithName(xLog.NamedMAIN) xMain.Runner(reg, log, registerRoute) } ``` 带后台任务示例 [#带后台任务示例] ```go func runConsumer(ctx context.Context, option ...any) { for { select { case <-ctx.Done(): return default: // 消费任务 } } } func main() { // ... reg, log 初始化省略 xMain.Runner(reg, log, registerRoute, runConsumer) } ``` > 如需接入 gRPC(可选),请前往独立模块文档:[`gRPC 运行时`](/docs/bamboo-base-go/grpc/runner) 下一步 [#下一步] # 雪花算法初始化 (/docs/bamboo-base-go/core/init/snowflake) 雪花算法初始化 [#雪花算法初始化] `SnowflakeInit` 作为**内置初始化节点**,自动注册到节点管理器中,用于初始化雪花算法节点。 SnowflakeInit [#snowflakeinit] ```go title="register/init/init_snowflake.go" // [!code highlight:2] // 内置初始化节点函数 func SnowflakeInit(ctx context.Context) (any, error) ``` **环境变量配置:** | 变量名 | 说明 | 默认值 | | ------------------------- | ------- | ---------- | | `SNOWFLAKE_DATACENTER_ID` | 数据中心 ID | 根据机器特征自动生成 | | `SNOWFLAKE_NODE_ID` | 节点 ID | 根据机器特征自动生成 | 实现细节 [#实现细节] ```go title="register/init/init_snowflake.go" func SnowflakeInit(ctx context.Context) (any, error) { log := xLog.WithName(xLog.NamedINIT) log.Info(ctx, "初始化雪花算法节点") // [!code highlight:4] // 初始化默认节点 if err := xSnowflake.InitDefaultNode(); err != nil { return nil, err } // [!code highlight] node := xSnowflake.GetDefaultNode() // [!code highlight:3] // 生成测试 ID 验证节点正常工作 testID := node.MustGenerate() // 普通 ID(Gene=0) testGeneID := node.MustGenerate(xSnowflake.GeneSystem) // 基因 ID log.SugarDebug(ctx, "雪花算法节点初始化成功", "datacenter_id", node.DatacenterID(), "node_id", node.NodeID(), "test_id", testID.String(), "test_gene_id", testGeneID.String(), ) // [!code highlight:2] // 返回节点实例,将被存入上下文 return xSnowflake.GetDefaultNode(), nil } ``` 节点注册 [#节点注册] 在 `Register` 流程中,雪花算法作为内置节点自动注册: ```go title="register.go" func Register(ctx context.Context, nodeList []xRegNode.RegNodeList) *Reg { reg := newReg(ctx) reg.configInit() reg.loggerInit() // [!code highlight:2] // 内置节点:雪花算法 reg.Init.Use(xCtx.SnowflakeNodeKey, xInit.SnowflakeInit) // 用户自定义节点 for _, node := range nodeList { reg.Init.Use(node.Key, node.Node) } reg.Init.Exec() reg.engineInit() return reg } ``` 自动节点 ID 生成 [#自动节点-id-生成] 若未配置环境变量,系统会根据机器特征自动生成节点 ID: 1. **MAC 地址**:优先使用网卡 MAC 地址计算 2. **主机名**:MAC 地址不可用时使用主机名哈希 这确保了同一台机器在重启后仍能获得相同的节点 ID。 使用雪花算法 [#使用雪花算法] 初始化后,雪花算法节点会通过上下文注入到每个 HTTP 请求: ```go import ( xSnowflake "github.com/bamboo-services/bamboo-base-go/common/snowflake" xCtxUtil "github.com/bamboo-services/bamboo-base-go/common/utility/context" ) func CreateUser(c *gin.Context) { // [!code highlight:2] // 获取标准上下文 ctx := c.Request.Context() // [!code highlight:2] // 从上下文获取雪花算法节点 node := xCtxUtil.GetSnowflakeNode(ctx) // [!code highlight:2] // 生成普通 ID userID := node.MustGenerate() // [!code highlight:2] // 生成带基因的 ID orderID := node.MustGenerate(xSnowflake.GeneOrder) } ``` 便捷函数 [#便捷函数] `xCtxUtil` 提供了便捷的 ID 生成函数: ```go // [!code highlight:2] // Panic 版本 id := xCtxUtil.MustGenerateSnowflakeID(ctx) geneID := xCtxUtil.MustGenerateGeneSnowflakeID(ctx, xSnowflake.GeneUser) // [!code highlight:2] // 错误返回版本 id, err := xCtxUtil.GenerateSnowflakeID(ctx) geneID, err := xCtxUtil.GenerateGeneSnowflakeID(ctx, xSnowflake.GeneUser) ``` 回退机制 [#回退机制] `GetSnowflakeNode` 具有回退机制: ```go title="utility/ctxutil/snowflake.go" func GetSnowflakeNode(ctx context.Context) *xSnowflake.Node { value := ctx.Value(xCtx.SnowflakeNodeKey) if value != nil { if node, ok := value.(*xSnowflake.Node); ok { return node } } // [!code highlight:2] // 回退到默认节点(即使在非 HTTP 请求上下文中也能工作) return xSnowflake.GetDefaultNode() } ``` 这确保了即使在非 HTTP 请求上下文中(如定时任务、消息队列处理)也能正常生成 ID。 基因 ID [#基因-id] 基因 ID 在标准雪花 ID 基础上增加了业务类型标识: ```go // [!code highlight:5] // 预定义基因类型 xSnowflake.GeneSystem // 系统级 ID xSnowflake.GeneUser // 用户 ID xSnowflake.GeneOrder // 订单 ID // ... 更多基因类型 ``` **解析基因:** ```go id := node.MustGenerate(xSnowflake.GeneUser) // [!code highlight:2] // 获取基因类型 gene := id.Gene() fmt.Println(gene.String()) // "User" ``` 注意事项 [#注意事项] * 该节点在 `loggerInit` 之后执行,确保日志记录器可用 * 初始化失败会返回错误,导致 `Exec()` panic 并终止程序 * 在分布式环境中,确保不同节点配置不同的 `SNOWFLAKE_DATACENTER_ID` 和 `SNOWFLAKE_NODE_ID` 下一步 [#下一步] # 通用返回结构 (/docs/bamboo-base-go/core/result/base-response) import { TypeTable } from '@/components/type-table'; BaseResponse [#baseresponse] `BaseResponse` 是所有 API 响应的统一结构体,位于 `xBase` 包根目录。 ```go title="base_response.go" package xBase type BaseResponse struct { Context string `json:"context"` Output string `json:"output"` Code uint `json:"code"` Message string `json:"message"` ErrorMessage xError.ErrMessage `json:"error_message,omitempty"` Overhead int64 `json:"overhead,omitempty"` Data interface{} `json:"data,omitempty"` } ``` 字段说明 [#字段说明] 响应格式示例 [#响应格式示例] 成功响应(无数据) [#成功响应无数据] ```json { "context": "req_abc123", "output": "Success", "code": 200, "message": "操作成功", "overhead": 1234 } ``` 成功响应(有数据) [#成功响应有数据] ```json { "context": "req_abc123", "output": "Success", "code": 200, "message": "获取成功", "overhead": 2345, "data": { "id": 1, "name": "bamboo" } } ``` 错误响应 [#错误响应] ```json { "context": "req_abc123", "output": "NOT_FOUND", "code": 40400, "message": "未找到", "error_message": "用户不存在", "overhead": 567 } ``` 下一步 [#下一步] # 错误码 (/docs/bamboo-base-go/core/result/error-code) import { TypeTable } from '@/components/type-table'; 错误码规范 [#错误码规范] 错误码采用 5 位数字格式,前 3 位对应 HTTP 状态码: ``` XXXXX ├── XXX: HTTP 状态码(如 400、401、404、500) └── XX: 具体错误序号(00-99) ``` HTTP 状态码映射 [#http-状态码映射] | 错误码前缀 | HTTP 状态码 | 说明 | | ----- | -------- | --------------------- | | 400xx | 400 | Bad Request - 请求错误 | | 401xx | 401 | Unauthorized - 未授权 | | 403xx | 403 | Forbidden - 禁止访问 | | 404xx | 404 | Not Found - 未找到 | | 405xx | 405 | Method Not Allowed | | 408xx | 408 | Request Timeout | | 409xx | 409 | Conflict - 冲突 | | 410xx | 410 | Gone - 已删除 | | 413xx | 413 | Payload Too Large | | 422xx | 422 | Unprocessable Entity | | 429xx | 429 | Too Many Requests | | 500xx | 500 | Internal Server Error | | 502xx | 502 | Bad Gateway | | 503xx | 503 | Service Unavailable | | 504xx | 504 | Gateway Timeout | 错误码表 [#错误码表] 400 Bad Request [#400-bad-request] 401 Unauthorized [#401-unauthorized] 403 Forbidden [#403-forbidden] 404 Not Found [#404-not-found] 405 Method Not Allowed [#405-method-not-allowed] 406 Not Acceptable [#406-not-acceptable] 408 Request Timeout [#408-request-timeout] 409 Conflict [#409-conflict] 410 Gone [#410-gone] 413 Payload Too Large [#413-payload-too-large] 415 Unsupported Media Type [#415-unsupported-media-type] 422 Unprocessable Entity [#422-unprocessable-entity] 429 Too Many Requests [#429-too-many-requests] 500 Internal Server Error [#500-internal-server-error] 502 Bad Gateway [#502-bad-gateway] 503 Service Unavailable [#503-service-unavailable] 504 Gateway Timeout [#504-gateway-timeout] 下一步 [#下一步] # 错误处理 (/docs/bamboo-base-go/core/result/error) import { TypeTable } from '@/components/type-table'; xError 包 [#xerror-包] `xError` 包提供统一的错误处理机制,包含错误接口、错误结构体和错误创建函数。 ```go import xError "github.com/bamboo-services/bamboo-base-go/common/error" ``` 核心类型 [#核心类型] IError 接口 [#ierror-接口] 所有错误类型都实现 `IError` 接口: ```go title="error.go" type IError interface { // [!code highlight:4] Error() string GetErrorCode() *ErrorCode GetErrorMessage() ErrMessage GetData() interface{} } ``` ErrorCode 结构体 [#errorcode-结构体] 预定义错误码的结构: ```go title="error_code.go" type ErrorCode struct { // [!code highlight:3] Code uint // 错误码(如 40400) Output string // 输出标识(如 "NOT_FOUND") Message string // 错误信息(如 "未找到") } ``` Error 结构体 [#error-结构体] 实际的错误对象: ```go title="error.go" type Error struct { // [!code highlight:4] *ErrorCode // 嵌入错误码 error error // 原始错误 ErrorMessage ErrMessage // 自定义错误消息 Data interface{} // 附加数据 } ``` ErrMessage 类型 [#errmessage-类型] 自定义错误消息类型: ```go title="error_message.go" // [!code highlight] type ErrMessage string func (e *ErrMessage) String() string { return string(*e) } ``` 创建错误 [#创建错误] NewError [#newerror] 创建标准错误对象。 ```go title="error_new.go" func NewError( ctx context.Context, err *ErrorCode, errorMessage ErrMessage, // [!code highlight] throw bool, getErr ...error, ) *Error ``` **参数说明:** | 参数 | 类型 | 说明 | | ------------ | ----------------- | -------- | | ctx | `context.Context` | 标准上下文 | | err | `*ErrorCode` | 预定义错误码 | | errorMessage | `ErrMessage` | 自定义错误描述 | | throw | `bool` | 是否记录日志 | | getErr | `...error` | 原始错误(可选) | **示例:** ```go func GetUser(ctx *gin.Context, id string) (*User, error) { user, err := db.FindUser(id) if err != nil { // [!code highlight:6] return nil, xError.NewError( ctx.Request.Context(), xError.NotFound, "用户不存在", true, // 记录日志 err, // 原始错误 ) } return user, nil } ``` NewErrorHasData [#newerrorhasdata] 创建带附加数据的错误对象。 ```go title="error_new.go" func NewErrorHasData( ctx context.Context, err *ErrorCode, errorMessage ErrMessage, throw bool, getErr error, // [!code highlight] data ...interface{}, ) *Error ``` **示例:** ```go func ValidateUser(ctx *gin.Context, req *CreateUserReq) error { if req.Age < 18 { // [!code highlight:10] return xError.NewErrorHasData( ctx.Request.Context(), xError.ValidationError, "年龄不符合要求", true, nil, map[string]interface{}{ "field": "age", "required": 18, "actual": req.Age, }, ) } return nil } ``` NewInternalServerError [#newinternalservererror] 创建服务器内部错误,自动记录 ERROR 级别日志。 ```go title="error_library.go" func NewInternalServerError( ctx context.Context, errMessage ErrMessage, err error, ) *Error ``` **示例:** ```go func SaveUser(ctx *gin.Context, user *User) error { if err := db.Save(user); err != nil { // [!code highlight:4] return xError.NewInternalServerError( ctx.Request.Context(), "保存用户失败", err, ) } return nil } ``` 使用模式 [#使用模式] Handler 中的错误处理 [#handler-中的错误处理] 在 Handler 中,使用 `ctx.Error()` 将错误传递给错误处理中间件: ```go title="handler.go" func (h *AppHandler) CreateApp(ctx *gin.Context) { h.log.Info(ctx, "开始处理创建应用请求") // 验证并绑定数据 getReq := xUtil.Bind(ctx, &apiApp.CreateAppRequest{}).Data() if getReq == nil { return } // 验证商户 ID merchantID, snowflakeErr := xSnowflake.ParseSnowflakeID(getReq.MerchantID) if snowflakeErr != nil { // [!code highlight:2] // 创建错误并传递给中间件处理 _ = ctx.Error(xError.NewError(ctx.Request.Context(), xError.BadRequest, "商户 ID 非法", false)) return } // 检查用户权限 if xErr := h.service.merchantUser.CheckUserBelongsToMerchant(ctx, getUser.ID, merchantID); xErr != nil { // [!code highlight] _ = ctx.Error(xErr) return } // 创建应用 newApp, clientSecret, xErr := h.service.app.CreateApp(ctx, merchantID, getUser.ID, getReq.AppName, getReq.BaseURL, getReq.Description, getReq.Logo, getReq.Homepage) if xErr != nil { // [!code highlight] _ = ctx.Error(xErr) return } // 成功响应 response := apiApp.CreateAppResponse{ App: newApp, ClientSecret: *clientSecret, } // [!code highlight] xResult.SuccessHasData(ctx, "应用创建成功", response) } ``` 中间件中的错误处理 [#中间件中的错误处理] 在中间件中,使用 `xResult.AbortError()` 直接返回错误: ```go title="middleware.go" func AuthRequired(ctx *gin.Context) { // 获取用户 token getToken := ctx.GetHeader(xHttp.HeaderAuthorization.String()) if getToken == "" { // [!code highlight] xResult.AbortError(ctx, xError.NotAcceptable, "Authorization Token 不能为空", nil) return } // 验证 token 格式 if !strings.HasPrefix(getToken, "Bearer ") { // [!code highlight] xResult.AbortError(ctx, xError.NotAcceptable, "无效的 Authorization Token 格式", nil) return } // 验证 token 合法性 if !xUtil.VerifySecurityKey(getToken[7:]) { // [!code highlight] xResult.AbortError(ctx, xError.Unauthorized, "无效的 Authorization Token", nil) return } // 获取用户信息 getUser, xErr := logic.NewUser(db, rdb).GetUser(ctx, entity.SearchTypeID, tokenUser.UserID.String()) if xErr != nil { // [!code highlight] xResult.AbortError(ctx, xErr.ErrorCode, xErr.ErrorMessage, xErr.Error()) return } ctx.Set(bConstContext.UserEntityKey.String(), getUser) ctx.Next() } ``` Service 层的错误处理 [#service-层的错误处理] 在 Service 层,返回 `*xError.Error` 类型的错误: ```go title="service.go" func (s *UserService) GetUser(ctx *gin.Context, id string) (*entity.User, *xError.Error) { user, err := s.repo.FindByID(ctx, id) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { // [!code highlight] return nil, xError.NewError(ctx.Request.Context(), xError.UserNotFound, "用户不存在", true, err) } // [!code highlight] return nil, xError.NewInternalServerError(ctx.Request.Context(), "查询用户失败", err) } return user, nil } ``` 错误方法 [#错误方法] ```go // [!code highlight] // 获取错误字符串 err.Error() string // [!code highlight] // 获取错误码结构 err.GetErrorCode() *ErrorCode // [!code highlight] // 获取自定义错误消息 err.GetErrorMessage() ErrMessage // [!code highlight] // 获取附加数据 err.GetData() interface{} ``` 下一步 [#下一步] # 结果处理 (/docs/bamboo-base-go/core/result/result) xResult 包 [#xresult-包] `xResult` 包提供便捷的响应函数,基于 [BaseResponse](/docs/bamboo-base-go/core/result/base-response) 结构体封装,自动处理日志记录和响应格式化。 ```go import xResult "github.com/bamboo-services/bamboo-base-go/major/result" ``` Success [#success] 返回成功响应(无数据)。 ```go title="result.go" func Success(ctx *gin.Context, message string) ``` **示例:** ```go func Logout(ctx *gin.Context) { // 执行登出逻辑... // [!code highlight] xResult.Success(ctx, "登出成功") } ``` **响应:** ```json { "context": "req_abc123", "output": "Success", // [!code highlight] "code": 200, "message": "登出成功", "overhead": 1234 } ``` SuccessHasData [#successhasdata] 返回成功响应(有数据)。 ```go title="result.go" func SuccessHasData(ctx *gin.Context, message string, data interface{}) ``` **示例:** ```go func GetUser(ctx *gin.Context) { user := User{ ID: 1, Name: "Bamboo", } // [!code highlight] xResult.SuccessHasData(ctx, "获取成功", user) } ``` **响应:** ```json { "context": "req_abc123", "output": "Success", // [!code highlight] "code": 200, "message": "获取成功", "overhead": 2345, // [!code highlight:5] "data": { "id": 1, "name": "Bamboo" } } ``` Error [#error] 返回错误响应,请求继续执行后续中间件。适用于需要统一错误处理中间件的场景。 ```go title="result.go" func Error(ctx *gin.Context, errorCode *xError.ErrorCode, errorMessage xError.ErrMessage, data interface{}) ``` **参数说明:** | 参数 | 类型 | 说明 | | ------------ | ------------------- | ------------ | | ctx | `*gin.Context` | Gin 上下文 | | errorCode | `*xError.ErrorCode` | 预定义错误码 | | errorMessage | `xError.ErrMessage` | 详细错误描述 | | data | `interface{}` | 附加数据(可为 nil) | **响应:** ```json { "context": "req_abc123", // [!code highlight:3] "output": "NOT_FOUND", "code": 40400, "message": "未找到", // [!code highlight] "error_message": "用户不存在", "overhead": 567 } ``` AbortError [#aborterror] 返回错误响应并**终止请求**,不再执行后续中间件和处理函数。**主要用于中间件中的错误处理**。 ```go title="result.go" func AbortError(ctx *gin.Context, errorCode *xError.ErrorCode, errorMessage xError.ErrMessage, data interface{}) ``` **与 Error 的区别:** | 函数 | 使用场景 | 行为 | | ------------ | ---------------- | -------------- | | `Error` | Handler 中配合错误中间件 | 返回响应后继续执行后续中间件 | | `AbortError` | 中间件中直接返回 | 返回响应后立即终止请求链 | 使用模式 [#使用模式] Handler 中的错误处理 [#handler-中的错误处理] 在 Handler 中,推荐使用 `ctx.Error()` 配合错误处理中间件,而不是直接调用 `xResult.Error()`: ```go title="handler.go" func (h *AppHandler) CreateApp(ctx *gin.Context) { h.log.Info(ctx, "开始处理创建应用请求") // 验证并绑定数据 getReq := xUtil.Bind(ctx, &apiApp.CreateAppRequest{}).Data() if getReq == nil { return } // 验证商户 ID merchantID, snowflakeErr := xSnowflake.ParseSnowflakeID(getReq.MerchantID) if snowflakeErr != nil { // [!code highlight:2] // 使用 ctx.Error() 而不是直接返回 _ = ctx.Error(xError.NewError(ctx.Request.Context(), xError.BadRequest, "商户 ID 非法", false)) return } // 检查用户权限 if xErr := h.service.merchantUser.CheckUserBelongsToMerchant(ctx, getUser.ID, merchantID); xErr != nil { // [!code highlight] _ = ctx.Error(xErr) return } // 创建应用 newApp, clientSecret, xErr := h.service.app.CreateApp(ctx, merchantID, getUser.ID, getReq.AppName, getReq.BaseURL, getReq.Description, getReq.Logo, getReq.Homepage) if xErr != nil { // [!code highlight] _ = ctx.Error(xErr) return } // 成功响应 response := apiApp.CreateAppResponse{ App: newApp, ClientSecret: *clientSecret, } // [!code highlight] xResult.SuccessHasData(ctx, "应用创建成功", response) } ``` 中间件中的错误处理 [#中间件中的错误处理] 在中间件中,使用 `xResult.AbortError()` 直接返回错误并终止请求: ```go title="middleware.go" func AuthRequired(ctx *gin.Context) { xLog.WithName(xLog.NamedMIDE).Info(ctx, "开始验证用户登录状态") // 获取用户 token getToken := ctx.GetHeader(xHttp.HeaderAuthorization.String()) if getToken == "" { // [!code highlight] xResult.AbortError(ctx, xError.NotAcceptable, "Authorization Token 不能为空", nil) return } // 验证 token 格式 if !strings.HasPrefix(getToken, "Bearer ") { // [!code highlight] xResult.AbortError(ctx, xError.NotAcceptable, "无效的 Authorization Token 格式", nil) return } // 验证 token 合法性 if !xUtil.VerifySecurityKey(getToken[7:]) { // [!code highlight] xResult.AbortError(ctx, xError.Unauthorized, "无效的 Authorization Token", nil) return } // 获取缓存的登录态信息 tokenUser := new(bModelsCache.TokenUser) rdb := xCtxUtil.GetRDB(ctx) redisErr := rdb.HGetAll(ctx, bRedis.TokenUserKey.Get(getToken[7:]).String()).Scan(tokenUser) if redisErr != nil { // [!code highlight] xResult.AbortError(ctx, xError.CacheError, "缓存获取失败", redisErr.Error()) return } if tokenUser == nil || tokenUser.Token == "" { // [!code highlight] xResult.AbortError(ctx, xError.Unauthorized, "无效的用户令牌", nil) return } // 设置用户信息到上下文 db := xCtxUtil.GetDB(ctx) getUser, xErr := logic.NewUser(db, rdb).GetUser(ctx, entity.SearchTypeID, tokenUser.UserID.String()) if xErr != nil { // [!code highlight] xResult.AbortError(ctx, xErr.ErrorCode, xErr.ErrorMessage, xErr.Error()) return } ctx.Set(bConstContext.UserEntityKey.String(), getUser) // 继续放行 ctx.Next() } ``` HTTP 状态码映射 [#http-状态码映射] `xResult` 会根据错误码自动计算 HTTP 状态码: ```go // [!code highlight] httpStatus := errorCode.Code / 100 ``` | 错误码范围 | HTTP 状态码 | 说明 | | ----------- | -------- | --------------------- | | 40000-40099 | 400 | Bad Request | | 40100-40199 | 401 | Unauthorized | | 40300-40399 | 403 | Forbidden | | 40400-40499 | 404 | Not Found | | 50000-50099 | 500 | Internal Server Error | 下一步 [#下一步] # 业务基因 (/docs/bamboo-base-go/components/snowflake/gene) import { TypeTable } from '@/components/type-table'; 业务基因 [#业务基因] 业务基因(Gene)是雪花 ID 中嵌入的 6 位业务类型标识,支持 64 种业务分类(0-63)。 Gene 类型 [#gene-类型] ```go title="gene.go" // [!code highlight] type Gene int64 ``` 方法列表 [#方法列表] ```go title="gene.go" // [!code highlight] // 返回基因名称,未定义则返回 "Custom(n)" func (g Gene) String() string // [!code highlight] // 判断是否为系统级别 (0-15) func (g Gene) IsSystem() bool // [!code highlight] // 判断是否为业务级别 (16-63) func (g Gene) IsBusiness() bool // [!code highlight] // 判断是否在有效范围 (0-63) func (g Gene) IsValid() bool // [!code highlight] // 返回 int64 值 func (g Gene) Int64() int64 ``` 预定义基因 [#预定义基因] 系统级别 (0-15) [#系统级别-0-15] 用于框架和系统内部数据: 业务级别 (16-63) [#业务级别-16-63] 用于业务数据: 使用示例 [#使用示例] 生成带基因的 ID [#生成带基因的-id] ```go // [!code highlight:2] // 生成用户 ID userID := xSnowflake.GenerateID(xSnowflake.GeneUser) // [!code highlight:2] // 生成订单 ID orderID := xSnowflake.GenerateID(xSnowflake.GeneOrder) // [!code highlight:2] // 生成支付 ID paymentID := xSnowflake.GenerateID(xSnowflake.GenePayment) ``` 从 ID 提取基因 [#从-id-提取基因] ```go id := xSnowflake.GenerateID(xSnowflake.GeneOrder) // [!code highlight:2] gene := id.Gene() fmt.Println(gene.String()) // "Order" // [!code highlight:2] // 判断基因类型 if gene.IsBusiness() { fmt.Println("这是业务数据") } // [!code highlight:2] if gene == xSnowflake.GeneOrder { fmt.Println("这是订单数据") } ``` 在实体中使用 [#在实体中使用] ```go title="entity/user.go" package entity import ( xModels "github.com/bamboo-services/bamboo-base-go/major/models" xSnowflake "github.com/bamboo-services/bamboo-base-go/common/snowflake" ) type User struct { xModels.BaseEntity Username string `gorm:"type:varchar(64)"` } // [!code highlight:4] // 实现 GeneProvider 接口 func (u *User) GetGene() xSnowflake.Gene { return xSnowflake.GeneUser } ``` ```go title="entity/order.go" type Order struct { xModels.BaseEntity UserID xSnowflake.SnowflakeID OrderNo string } // [!code highlight:3] func (o *Order) GetGene() xSnowflake.Gene { return xSnowflake.GeneOrder } ``` 基因计算 [#基因计算] `GeneCalc` 提供基于哈希的基因计算,用于分片定位: ```go title="gene_calc.go" // [!code highlight] func CalcGene() GeneCalc ``` 计算方法 [#计算方法] ```go // [!code highlight:2] // 基于单个 ID 计算基因 gene := xSnowflake.CalcGene().Hash(userID) // [!code highlight:2] // 基于多个 ID 计算组合基因 gene := xSnowflake.CalcGene().HashMulti(userID, merchantID) // [!code highlight:2] // 基于字符串计算基因 gene := xSnowflake.CalcGene().HashString("user_123") ``` 分片定位示例 [#分片定位示例] 同一用户的所有订单使用相同基因,便于数据分片: ```go title="entity/order.go" type Order struct { xModels.BaseEntity UserID xSnowflake.SnowflakeID OrderNo string } // [!code highlight:4] // 基于用户 ID 计算基因,实现同用户数据分片 func (o *Order) GetGene() xSnowflake.Gene { return xSnowflake.CalcGene().Hash(o.UserID) } ``` **效果:** * 同一用户的所有订单具有相同的基因值 * 便于按用户维度进行数据分片 * 查询同一用户的订单时可以定位到同一分片 自定义基因 [#自定义基因] 可以使用 32-63 范围内未定义的值作为自定义基因: ```go const ( // [!code highlight:3] // 自定义业务基因 GeneMyBusiness xSnowflake.Gene = 32 GeneMyData xSnowflake.Gene = 33 ) // 使用自定义基因 id := xSnowflake.GenerateID(GeneMyBusiness) ``` 下一步 [#下一步] # 概述 (/docs/bamboo-base-go/components/snowflake) import { TypeTable } from '@/components/type-table'; 雪花算法 [#雪花算法] `xSnowflake` 包实现了带业务基因的雪花算法(Gene Snowflake ID),在标准雪花算法基础上嵌入 6 位业务基因,便于从 ID 直接识别数据类型。 ```go import xSnowflake "github.com/bamboo-services/bamboo-base-go/common/snowflake" ``` ID 结构 [#id-结构] 64 位 ID 的位分配: ``` ┌─────────┬──────────────────────────────────────────┬────────┬────────────┬────────┬────────────┐ │ 符号位 │ 时间戳 │ 基因 │ 数据中心ID │ 节点ID │ 序列号 │ │ 1 bit │ 41 bits │ 6 bits │ 3 bits │ 3 bits │ 10 bits │ │ (不用) │ (毫秒级,约69年) │ (0-63) │ (0-7) │ (0-7) │ (0-1023) │ └─────────┴──────────────────────────────────────────┴────────┴────────────┴────────┴────────────┘ 63 62 22 21-16 15-13 12-10 9-0 ``` 快速使用 [#快速使用] 生成 ID [#生成-id] ```go // [!code highlight:2] // 使用默认节点生成普通 ID id := xSnowflake.GenerateID() // [!code highlight:2] // 生成带业务基因的 ID userID := xSnowflake.GenerateID(xSnowflake.GeneUser) orderID := xSnowflake.GenerateID(xSnowflake.GeneOrder) ``` 解析 ID [#解析-id] ```go id := xSnowflake.GenerateID(xSnowflake.GeneUser) // [!code highlight:5] fmt.Println("时间:", id.Timestamp()) // 2024-01-15 10:30:00 fmt.Println("基因:", id.Gene()) // User fmt.Println("数据中心:", id.DatacenterID()) // 1 fmt.Println("节点:", id.NodeID()) // 2 fmt.Println("序列号:", id.Sequence()) // 0 ``` 字符串转换 [#字符串转换] ```go // [!code highlight:2] // ID 转字符串 str := id.String() // "1234567890123456789" // [!code highlight:2] // 字符串转 ID id, err := xSnowflake.ParseSnowflakeID("1234567890123456789") ``` SnowflakeID 类型 [#snowflakeid-类型] ```go title="snowflake.go" // [!code highlight] type SnowflakeID int64 ``` 方法列表 [#方法列表] 序列化支持 [#序列化支持] SnowflakeID 实现了多种序列化接口: ```go // [!code highlight:2] // JSON 序列化为字符串(避免 JavaScript 精度丢失) json.Marshal(id) // "1234567890123456789" // [!code highlight:2] // GORM 数据库读写 type User struct { ID xSnowflake.SnowflakeID `gorm:"type:bigint;primaryKey"` } // [!code highlight:2] // Redis 二进制存储 rdb.Set(ctx, "key", id, 0) ``` 性能与容量 [#性能与容量] | 指标 | 数值 | | ------ | ---------------- | | 每毫秒每节点 | 1,024 个 ID | | 每秒每节点 | 1,024,000 个 ID | | 集群节点数 | 8 × 8 = 64 个 | | 集群每秒总量 | 65,536,000 个 ID | | 时间有效期 | 约 69 年(至 2092 年) | 线程安全 [#线程安全] * 使用 `sync.Mutex` 保证并发安全 * 序列号用尽时自旋等待下一毫秒 * 支持高并发场景 下一步 [#下一步] # 节点管理 (/docs/bamboo-base-go/components/snowflake/node) import { TypeTable } from '@/components/type-table'; 节点管理 [#节点管理] `Node` 结构体是雪花算法的核心,负责生成唯一 ID。每个节点由数据中心 ID 和节点 ID 唯一标识。 Node 结构体 [#node-结构体] ```go title="snowflake.go" type Node struct { // [!code highlight:5] mu sync.Mutex // 互斥锁,保证线程安全 datacenterID int64 // 数据中心 ID (0-7) nodeID int64 // 节点 ID (0-7) lastTime int64 // 上次生成 ID 的时间戳 sequence int64 // 当前毫秒内的序列号 } ``` 创建节点 [#创建节点] 手动创建 [#手动创建] ```go title="snowflake.go" // [!code highlight] func NewNode(datacenterID, nodeID int64) (*Node, error) ``` **参数范围:** | 参数 | 范围 | 说明 | | -------------- | --- | ------- | | `datacenterID` | 0-7 | 数据中心 ID | | `nodeID` | 0-7 | 节点 ID | **示例:** ```go // [!code highlight:2] // 创建数据中心 1,节点 2 的节点 node, err := xSnowflake.NewNode(1, 2) if err != nil { panic(err) } // 使用节点生成 ID id := node.MustGenerate(xSnowflake.GeneUser) ``` 默认节点 [#默认节点] 框架提供全局默认节点,自动初始化: ```go title="global.go" // [!code highlight] // 初始化默认节点 func InitDefaultNode() error // [!code highlight] // 获取默认节点(自动初始化) func GetDefaultNode() *Node // [!code highlight] // 使用默认节点生成 ID func GenerateID(gene ...Gene) SnowflakeID ``` **示例:** ```go // [!code highlight:2] // 方式一:直接使用全局函数 id := xSnowflake.GenerateID(xSnowflake.GeneUser) // [!code highlight:2] // 方式二:获取默认节点后使用 node := xSnowflake.GetDefaultNode() id := node.MustGenerate(xSnowflake.GeneUser) ``` 节点 ID 获取逻辑 [#节点-id-获取逻辑] 默认节点的 ID 获取优先级: 1. 环境变量(优先) [#1-环境变量优先] ```bash title=".env" # [!code highlight:2] SNOWFLAKE_DATACENTER_ID=1 SNOWFLAKE_NODE_ID=2 ``` 2. 自动生成(回退) [#2-自动生成回退] 未配置环境变量时,自动生成节点 ID: ```go title="global.go" func autoGenerateIDs() (datacenterID, nodeID int64) { var hashInput []byte // [!code highlight:2] // 优先使用 MAC 地址 interfaces, err := net.Interfaces() if err == nil { for _, iface := range interfaces { if iface.Flags&net.FlagLoopback != 0 || len(iface.HardwareAddr) < 6 { continue } hashInput = iface.HardwareAddr break } } // [!code highlight:2] // 回退到主机名 if len(hashInput) == 0 { hostname, _ := os.Hostname() if hostname == "" { hostname = "unknown-host" } hashInput = []byte(hostname) } // [!code highlight:2] // 使用 FNV-1a 哈希算法 h := fnv.New64a() _, _ = h.Write(hashInput) hash := h.Sum64() // [!code highlight:2] // 从哈希值中提取 ID datacenterID = int64(hash % uint64(maxDatacenterID+1)) nodeID = int64((hash >> 3) % uint64(maxNodeID+1)) return } ``` **特点:** * 同一台机器重启后获得相同的节点 ID * 不同机器大概率获得不同的节点 ID * 无需手动配置,开箱即用 Node 方法 [#node-方法] Generate [#generate] 生成 ID,线程安全: ```go title="snowflake.go" // [!code highlight] func (n *Node) Generate(gene ...Gene) (SnowflakeID, error) ``` **示例:** ```go node, _ := xSnowflake.NewNode(1, 1) // [!code highlight:2] // 生成普通 ID id, err := node.Generate() // [!code highlight:2] // 生成带基因的 ID id, err := node.Generate(xSnowflake.GeneUser) ``` MustGenerate [#mustgenerate] 生成 ID,失败则 panic: ```go title="snowflake.go" // [!code highlight] func (n *Node) MustGenerate(gene ...Gene) SnowflakeID ``` **示例:** ```go // [!code highlight] id := node.MustGenerate(xSnowflake.GeneOrder) ``` 获取节点信息 [#获取节点信息] ```go title="snowflake.go" // [!code highlight] func (n *Node) DatacenterID() int64 // [!code highlight] func (n *Node) NodeID() int64 ``` **示例:** ```go node := xSnowflake.GetDefaultNode() // [!code highlight:2] fmt.Println("数据中心:", node.DatacenterID()) // 1 fmt.Println("节点:", node.NodeID()) // 2 ``` ID 生成核心逻辑 [#id-生成核心逻辑] ```go title="snowflake.go" func (n *Node) Generate(gene ...Gene) (SnowflakeID, error) { g := GeneDefault if len(gene) > 0 { g = gene[0] } // [!code highlight:3] if !g.IsValid() { return 0, fmt.Errorf("基因类型必须在 0-%d 之间", maxGene) } n.mu.Lock() defer n.mu.Unlock() now := time.Now().UnixMilli() if now == n.lastTime { // [!code highlight:2] // 同一毫秒内,序列号递增 n.sequence = (n.sequence + 1) & maxSequence if n.sequence == 0 { // [!code highlight:2] // 序列号用尽,等待下一毫秒 for now <= n.lastTime { now = time.Now().UnixMilli() } } } else { // [!code highlight:2] // 新的毫秒,序列号重置 n.sequence = 0 } n.lastTime = now // [!code highlight:6] // 组装 ID id := ((now - epoch) << timestampShift) | (int64(g) << geneShift) | (n.datacenterID << datacenterShift) | (n.nodeID << nodeShift) | n.sequence return SnowflakeID(id), nil } ``` 分布式部署 [#分布式部署] 在分布式环境中,确保不同节点配置不同的 ID: 方式一:环境变量 [#方式一环境变量] ```yaml title="docker-compose.yml" services: app-1: environment: # [!code highlight:2] - SNOWFLAKE_DATACENTER_ID=1 - SNOWFLAKE_NODE_ID=1 app-2: environment: # [!code highlight:2] - SNOWFLAKE_DATACENTER_ID=1 - SNOWFLAKE_NODE_ID=2 app-3: environment: # [!code highlight:2] - SNOWFLAKE_DATACENTER_ID=2 - SNOWFLAKE_NODE_ID=1 ``` 方式二:自动生成 [#方式二自动生成] 不配置环境变量,依赖 MAC 地址自动生成: ```go // [!code highlight:2] // 每台机器自动获得唯一的节点 ID reg := xReg.Register() ``` 下一步 [#下一步] # ID 解析 (/docs/bamboo-base-go/components/snowflake/parse) import { TypeTable } from '@/components/type-table'; ID 解析 [#id-解析] SnowflakeID 提供丰富的解析方法,可以从 ID 中提取时间戳、基因、数据中心、节点等信息。 字符串解析 [#字符串解析] ParseSnowflakeID [#parsesnowflakeid] 从字符串解析 ID: ```go title="global.go" // [!code highlight] func ParseSnowflakeID(s string) (SnowflakeID, error) ``` **示例:** ```go // [!code highlight] id, err := xSnowflake.ParseSnowflakeID("1234567890123456789") if err != nil { log.Error("解析失败:", err) return } fmt.Println(id.Gene()) // User ``` MustParseSnowflakeID [#mustparsesnowflakeid] 解析 ID,失败则 panic: ```go title="global.go" // [!code highlight] func MustParseSnowflakeID(s string) SnowflakeID ``` **示例:** ```go // [!code highlight] id := xSnowflake.MustParseSnowflakeID("1234567890123456789") ``` 信息提取 [#信息提取] Timestamp [#timestamp] 提取创建时间: ```go title="snowflake.go" // [!code highlight] func (s SnowflakeID) Timestamp() time.Time ``` **实现:** ```go title="snowflake.go" func (s SnowflakeID) Timestamp() time.Time { // [!code highlight:2] ms := (int64(s) >> timestampShift) + epoch return time.UnixMilli(ms) } ``` **示例:** ```go id := xSnowflake.GenerateID() // [!code highlight] createTime := id.Timestamp() fmt.Println(createTime) // 2024-01-15 10:30:00.123 +0800 CST ``` Gene [#gene] 提取业务基因: ```go title="snowflake.go" // [!code highlight] func (s SnowflakeID) Gene() Gene ``` **实现:** ```go title="snowflake.go" func (s SnowflakeID) Gene() Gene { // [!code highlight] return Gene((int64(s) >> geneShift) & maxGene) } ``` **示例:** ```go id := xSnowflake.GenerateID(xSnowflake.GeneOrder) // [!code highlight:3] gene := id.Gene() fmt.Println(gene) // 16 fmt.Println(gene.String()) // "Order" // [!code highlight:2] if gene == xSnowflake.GeneOrder { fmt.Println("这是订单 ID") } ``` DatacenterID [#datacenterid] 提取数据中心 ID: ```go title="snowflake.go" // [!code highlight] func (s SnowflakeID) DatacenterID() int64 ``` **实现:** ```go title="snowflake.go" func (s SnowflakeID) DatacenterID() int64 { // [!code highlight] return (int64(s) >> datacenterShift) & maxDatacenterID } ``` NodeID [#nodeid] 提取节点 ID: ```go title="snowflake.go" // [!code highlight] func (s SnowflakeID) NodeID() int64 ``` **实现:** ```go title="snowflake.go" func (s SnowflakeID) NodeID() int64 { // [!code highlight] return (int64(s) >> nodeShift) & maxNodeID } ``` Sequence [#sequence] 提取序列号: ```go title="snowflake.go" // [!code highlight] func (s SnowflakeID) Sequence() int64 ``` **实现:** ```go title="snowflake.go" func (s SnowflakeID) Sequence() int64 { // [!code highlight] return int64(s) & maxSequence } ``` 完整解析示例 [#完整解析示例] ```go id := xSnowflake.GenerateID(xSnowflake.GeneUser) // [!code highlight:6] fmt.Println("ID:", id.String()) fmt.Println("创建时间:", id.Timestamp()) fmt.Println("基因:", id.Gene().String()) fmt.Println("数据中心:", id.DatacenterID()) fmt.Println("节点:", id.NodeID()) fmt.Println("序列号:", id.Sequence()) // 输出: // ID: 1234567890123456789 // 创建时间: 2024-01-15 10:30:00.123 +0800 CST // 基因: User // 数据中心: 1 // 节点: 2 // 序列号: 0 ``` 类型转换 [#类型转换] Int64 [#int64] 返回 int64 值: ```go title="snowflake.go" // [!code highlight] func (s SnowflakeID) Int64() int64 ``` **示例:** ```go id := xSnowflake.GenerateID() // [!code highlight] num := id.Int64() fmt.Println(num) // 1234567890123456789 ``` String [#string] 返回字符串表示: ```go title="snowflake.go" // [!code highlight] func (s SnowflakeID) String() string ``` **示例:** ```go id := xSnowflake.GenerateID() // [!code highlight] str := id.String() fmt.Println(str) // "1234567890123456789" ``` IsZero [#iszero] 判断是否为零值: ```go title="snowflake.go" // [!code highlight] func (s SnowflakeID) IsZero() bool ``` **示例:** ```go var id xSnowflake.SnowflakeID // [!code highlight] if id.IsZero() { fmt.Println("ID 未初始化") } ``` 序列化支持 [#序列化支持] JSON [#json] 序列化为字符串,避免 JavaScript 精度丢失: ```go title="snowflake.go" // [!code highlight] func (s SnowflakeID) MarshalJSON() ([]byte, error) // [!code highlight] func (s *SnowflakeID) UnmarshalJSON(data []byte) error ``` **示例:** ```go type User struct { ID xSnowflake.SnowflakeID `json:"id"` Name string `json:"name"` } user := User{ ID: xSnowflake.GenerateID(xSnowflake.GeneUser), Name: "张三", } // [!code highlight:2] // JSON 序列化 data, _ := json.Marshal(user) // {"id":"1234567890123456789","name":"张三"} // [!code highlight:2] // JSON 反序列化(支持字符串和数字格式) json.Unmarshal([]byte(`{"id":"1234567890123456789"}`), &user) json.Unmarshal([]byte(`{"id":1234567890123456789}`), &user) ``` GORM [#gorm] 支持数据库读写: ```go title="snowflake.go" // [!code highlight] func (s SnowflakeID) Value() (driver.Value, error) // [!code highlight] func (s *SnowflakeID) Scan(value interface{}) error ``` **示例:** ```go type User struct { // [!code highlight] ID xSnowflake.SnowflakeID `gorm:"type:bigint;primaryKey"` Username string `gorm:"type:varchar(64)"` } // 写入数据库 db.Create(&User{ ID: xSnowflake.GenerateID(xSnowflake.GeneUser), Username: "zhangsan", }) // 从数据库读取 var user User db.First(&user, "id = ?", "1234567890123456789") ``` Redis [#redis] 支持二进制存储: ```go title="snowflake.go" // [!code highlight] func (s SnowflakeID) MarshalBinary() ([]byte, error) // [!code highlight] func (s *SnowflakeID) UnmarshalBinary(data []byte) error ``` **示例:** ```go id := xSnowflake.GenerateID() // [!code highlight:2] // 存储到 Redis rdb.Set(ctx, "user:id", id, time.Hour) // [!code highlight:2] // 从 Redis 读取 var readID xSnowflake.SnowflakeID rdb.Get(ctx, "user:id").Scan(&readID) ``` 实际应用 [#实际应用] 根据 ID 判断数据类型 [#根据-id-判断数据类型] ```go func HandleData(id xSnowflake.SnowflakeID) { // [!code highlight] switch id.Gene() { case xSnowflake.GeneUser: handleUser(id) case xSnowflake.GeneOrder: handleOrder(id) case xSnowflake.GenePayment: handlePayment(id) default: handleDefault(id) } } ``` 根据 ID 判断创建时间 [#根据-id-判断创建时间] ```go func IsExpired(id xSnowflake.SnowflakeID, duration time.Duration) bool { // [!code highlight] createTime := id.Timestamp() return time.Since(createTime) > duration } // 使用 if IsExpired(tokenID, 24*time.Hour) { fmt.Println("Token 已过期") } ``` 根据 ID 定位节点 [#根据-id-定位节点] ```go func GetNodeInfo(id xSnowflake.SnowflakeID) string { // [!code highlight:2] return fmt.Sprintf("数据中心 %d,节点 %d", id.DatacenterID(), id.NodeID()) } ``` 下一步 [#下一步] # Cron 定时任务(可选) (/docs/bamboo-base-go/plugins/cron) Cron 定时任务(可选) [#cron-定时任务可选] `bamboo-base-go` 的核心是 HTTP + 节点化初始化;Cron 属于**可选扩展能力**,按需接入。 适用场景: * 需要定时执行后台任务(如数据清理、报表生成、缓存刷新)。 * 希望与 `xMain.Runner` 共享同一套信号处理与优雅关闭流程。 * 需要统一的日志记录与错误处理。 快速入口 [#快速入口] 功能特性 [#功能特性] * **多种 Cron 表达式**: 支持标准 5 位表达式和 `@every` 语法 * **灵活的函数签名**: 支持 `func()` 和 `func(context.Context)` 两种签名 * **优雅关闭**: 与主程序共享信号处理,支持优雅关闭 * **可选秒级精度**: 支持秒级 Cron 表达式 * **时区支持**: 可配置自定义时区 * **日志集成**: 与 `xLog` 深度集成,统一日志格式 模块导入 [#模块导入] ```go import ( xCron "github.com/xiaolfeng/bamboo-base/plugins/cron" xCronRunner "github.com/xiaolfeng/bamboo-base/plugins/cron/runner" ) ``` 快速示例 [#快速示例] ```go package main import ( xCron "github.com/xiaolfeng/bamboo-base/plugins/cron" xCronRunner "github.com/xiaolfeng/bamboo-base/plugins/cron/runner" xLog "github.com/xiaolfeng/bamboo-base/common/log" xMain "github.com/xiaolfeng/bamboo-base/major/main" xReg "github.com/xiaolfeng/bamboo-base/major/register" ) func main() { // 创建注册中心 reg := xReg.Register(ctx, nil) log := xLog.WithName(xLog.NamedMAIN) // 定义定时任务 jobs := []xCron.Job{ xCron.NewJob("@every 1m", func() { log.Info(ctx, "每分钟执行一次的任务") }), xCron.NewJob("0 0 * * *", func(ctx context.Context) { log.Info(ctx, "每天凌晨执行的任务") }), } // 创建 Cron 运行时 cronTask := xCronRunner.New( xCronRunner.WithLogger(xLog.WithName(xLog.NamedCRON)), xCronRunner.WithRegister(jobs...), ) // 启动服务(HTTP + Cron) xMain.Runner(reg, log, registerRoute, cronTask) } ``` # Job 定义与适配 (/docs/bamboo-base-go/plugins/cron/job) Job 定义与适配 [#job-定义与适配] `Job` 结构体用于定义定时任务的基本信息,包括 Cron 表达式和任务函数。 结构体定义 [#结构体定义] ```go // Job 定义定时任务结构 type Job struct { Spec string // cron 表达式,如 "*/5 * * * *" 或 "@every 1m" Func any // 支持 func() 或 func(context.Context) } // NewJob 创建定时任务 func NewJob(spec string, fn any) Job ``` 字段说明 [#字段说明] | 字段 | 类型 | 说明 | | ------ | -------- | ----------------------------------------------------------------------------------------- | | `Spec` | `string` | Cron 表达式,支持两种格式:**标准 5 位表达式**(如 `*/5 * * * *`)和 **`@every` 语法**(如 `@every 1m`、 `@hourly`) | | `Func` | `any` | 任务函数,支持两种签名:`func()` 或 `func(context.Context)` | Cron 表达式格式 [#cron-表达式格式] 标准 5 位表达式 [#标准-5-位表达式] ``` * * * * * * │ │ │ │ │ │ │ │ │ │ └─ 星期几 (0-6, 分钟 (0-59) 小时 (0-23) 日 (1-31) 月 (1-12) ``` 常用示例: | 表达式 | 说明 | | ------------- | ---------- | | `*/5 * * * *` | 每 5 分钟执行 | | `0 * * * *` | 每小时整点执行 | | `0 0 * * *` | 每天午夜执行 | | `0 0 0 * *` | 每月 1 日午夜执行 | @every 语法 [#every-语法] ``` @every ``` 常用示例: | 表达式 | 说明 | | ------------ | --------- | | `@every 1m` | 每 1 分钟执行 | | `@every 5m` | 每 5 分钟执行 | | `@every 1h` | 每 1 小时执行 | | `@every 24h` | 每 24 小时执行 | 函数签名 [#函数签名] 无参数签名 [#无参数签名] ```go func() { // 无需上下文,执行简单任务 } ``` 吺Context签名 [#吺context签名] ```go func(ctx context.Context) { // 可以访问上下文中的资源(如数据库、Redis) db := xCtxUtil.MustGetDB(ctx) // 执行需要上下文的任务 } ``` AdaptJob 函数适配器 [#adaptjob-函数适配器] `AdaptJob` 函数用于将不同签名的函数适配为统一的 `func(context.Context)` 格式。 ```go // AdaptJob 适配任务函数,支持多种签名 func AdaptJob(fn any) (jobFunc, error) ``` 工作原理 [#工作原理] 1. 检查 `fn` 的类型 2. 如果是 `func()`,包装为调用 `func(context.Context)` 3. 如果是 `func(context.Context)`,直接使用 4. 其他类型返回错误 使用示例 [#使用示例] ```go // 创建无参数任务 job1 := xCron.NewJob("@every 1m", func() { log.Println("每分钟执行一次") }) // 创建带上下文的任务 job2 := xCron.NewJob("*/5 * * * *", func(ctx context.Context) { db := xCtxUtil.MustGetDB(ctx) // 执行数据库清理 db.Where("expired_at < ?", time.Now()).Delete(&models.Data{}) }) // 手动适配函数 adaptedFunc, err := xCron.AdaptJob(func() { log.Println("手动适配的任务") }) if err != nil { log.Fatalf("适配失败: %v", err) } ``` 完整示例 [#完整示例] ```go package main import ( xCron "github.com/xiaolfeng/bamboo-base/plugins/cron" xCronRunner "github.com/xiaolfeng/bamboo-base/plugins/cron/runner" ) // 定义任务列表 var jobs = []xCron.Job{ xCron.NewJob("@every 1m", func() { log.Println("[CRON] 每分钟任务执行") }), xCron.NewJob("0 * * * *", func(ctx context.Context) { db := xCtxUtil.MustGetDB(ctx) db.Where("created_at < ?", time.Now().AddDate(-24 * time.Hour).Delete(&models.Log{}) }), } func main() { // ... 其他初始化代码 } ``` # Cron 运行时 (/docs/bamboo-base-go/plugins/cron/runner) Cron 运行时 [#cron-运行时] `xCronRunner.New` 函数用于创建并启动 Cron 定时任务服务。它与 `xMain.Runner` 集成,可以统一管理 HTTP 服务和定时任务的信号处理与优雅关闭。 函数签名 [#函数签名] ```go // New 创建 Cron 运行时函数 func New(options ...Option) func(ctx context.Context, option ...any) // Option 配置函数 func WithGracefulStopTimeout(timeout time.Duration) Option func WithLogger(logger *xLog.LogNamedLogger) Option func WithRegister(jobs ...xCron.Job) Option func WithSeconds() Option func WithLocation(loc *time.Location) Option ``` Config 结构 [#config-结构] ```go // Config Cron 运行时配置 type Config struct { GracefulStopTimeout time.Duration // 优雅关闭超时 Logger *xLog.LogNamedLogger // 日志记录器 Jobs []xCron.Job // 任务列表 WithSeconds bool // 是否启用秒级精度 Location *time.Location // 时区设置 } ``` 配置选项 [#配置选项] WithGracefulStopTimeout [#withgracefulstoptimeout] 设置优雅关闭超时时间,默认 30 秒。 ```go runner := xCronRunner.New( xCronRunner.WithGracefulStopTimeout(60 * time.Second), ) ``` WithLogger [#withlogger] 设置日志记录器,使用 `xLog.WithName(xLog.NamedCRON)` 创建。 ```go runner := xCronRunner.New( xCronRunner.WithLogger(xLog.WithName(xLog.NamedCRON)), ) ``` WithRegister [#withregister] 注册任务列表,可以注册多个任务。 ```go runner := xCronRunner.New( xCronRunner.WithRegister(job1, job2, job3), ) ``` WithSeconds [#withseconds] 启用秒级精度,默认 Cron 表达式为 5 位(分钟级)。启用后支持 6 位(秒级)。 ```go runner := xCronRunner.New( xCronRunner.WithSeconds(), // 支持 "*/1 * * * * *" 格式 ) ``` WithLocation [#withlocation] 设置时区,默认使用本地时区。 ```go runner := xCronRunner.New( xCronRunner.WithLocation(time.UTC), ) ``` 与 xMain.Runner 集成 [#与-xmainrunner-集成] Cron 运行时通过 `xMain.Runner` 的 `goroutineFunc` 参数启动,实现与 HTTP 服务共享信号处理。 基本集成 [#基本集成] ```go package main import ( xCron "github.com/xiaolfeng/bamboo-base/plugins/cron" xCronRunner "github.com/xiaolfeng/bamboo-base/plugins/cron/runner" xLog "github.com/xiaolfeng/bamboo-base/common/log" xMain "github.com/xiaolfeng/bamboo-base/major/main" xReg "github.com/xiaolfeng/bamboo-base/major/register" ) func main() { ctx := context.Background() // 创建注册中心 reg := xReg.Register(ctx, nil) // 创建日志 log := xLog.WithName(xLog.NamedMAIN, "cron-example") // 创建任务 jobs := []xCron.Job{ xCron.NewJob("@every 1m", func() { log.Info(ctx, "每分钟任务执行") }), } // 创建 Cron 运行时 cronTask := xCronRunner.New( xCronRunner.WithLogger(xLog.WithName(xLog.NamedCRON)), xCronRunner.WithRegister(jobs...), ) // 启动服务(HTTP + Cron) xMain.Runner(reg, log, registerRoute, cronTask) } func registerRoute(reg *xReg.Reg) { // 注册 HTTP 路由 } ``` 多任务集成 [#多任务集成] ```go // 创建多个任务 jobs := []xCron.Job{ xCron.NewJob("@every 1m", cleanupTask), xCron.NewJob("@every 5m", refreshCacheTask), xCron.NewJob("0 0 * * *", dailyReportTask), // 每天凌晨执行 } // 创建 Cron 运行时 cronTask := xCronRunner.New( xCronRunner.WithLogger(xLog.WithName(xLog.NamedCRON)), xCronRunner.WithSeconds(), // 启用秒级精度 xCronRunner.WithRegister(jobs...), ) ``` 优雅关闭 [#优雅关闭] 当收到 `SIGINT` 或 `SIGTERM` 信号时: 1. Cron 调度器停止接受新任务 2. 等待当前执行的任务完成 3. 超时后强制关闭(默认 30 秒) 注意事项 [#注意事项] * **时区设置**: 确保服务器时区与 Cron 时区一致,避免任务执行时间偏差。 * **任务幂等性**: 定时任务应该设计为幂等的,避免重复执行导致数据问题。 * **错误处理**: 任务中的 panic 会被日志记录,但不会导致整个服务崩溃。 * **资源清理**: 在任务执行完毕后,及时释放资源(如数据库连接、文件句柄)。 # PageDTO (/docs/bamboo-base-java/base/dto/page-dto) import { TypeTable } from '@/components/type-table'; PageDTO [#pagedto] `PageDTO` 是筱工具(Java) 的分页数据传输对象,位于 `com.xlf.utility.models.dto` 包下。用于标准化分页查询的响应格式,支持泛型以适配任意类型的数据记录。 类定义 [#类定义] ```java title="PageDTO.java" package com.xlf.utility.models.dto; import lombok.Data; import lombok.AllArgsConstructor; import lombok.experimental.Accessors; import java.util.List; // [!code highlight:2] @Data @Accessors(chain = true) // 支持链式调用 @AllArgsConstructor public class PageDTO { private List records; // 数据记录列表 private Long total; // 总记录数 private Long size; // 每页大小 private Long current; // 当前页码 } ``` 字段说明 [#字段说明] ', required: true, }, total: { description: '总记录数,用于前端计算总页数和分页导航。', type: 'Long', required: true, }, size: { description: '每页大小,即当前分页的容量。', type: 'Long', required: true, }, current: { description: '当前页码。通常从 1 开始计数(由业务层约束)。', type: 'Long', required: true, }, }} /> 构造方法 [#构造方法] 空构造方法 [#空构造方法] ```java title="PageDTO.java" public PageDTO() { this.total = 0L; this.size = 0L; this.current = 0L; this.records = new ArrayList<>(); } ``` 创建空的分页对象,所有字段初始化为默认值。 指定总数和大小 [#指定总数和大小] ```java title="PageDTO.java" public PageDTO(long total, long size) { this.total = total; this.size = size; this.records = new ArrayList<>(); } ``` 创建指定总数和每页大小的分页对象,记录列表初始化为空。 全参构造 [#全参构造] ```java title="PageDTO.java" public PageDTO(List records, Long total, Long size, Long current) ``` 通过 Lombok `@AllArgsConstructor` 生成,指定所有字段值。 使用示例 [#使用示例] 在 Service 中构建 [#在-service-中构建] ```java title="UserService.java" import com.xlf.utility.models.dto.PageDTO; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; @Service public class UserService { public PageDTO getUserList(int pageNum, int pageSize) { // [!code highlight:5] // 使用 MyBatis-Plus 分页查询 Page page = userMapper.selectPage( new Page<>(pageNum, pageSize), new LambdaQueryWrapper() .eq(UserDO::getStatus, 1) .orderByDesc(UserDO::getCreatedAt) ); // 转换为 PageDTO PageDTO result = new PageDTO<>(); result.setRecords(convertToDTO(page.getRecords())); result.setTotal(page.getTotal()); result.setSize(page.getSize()); result.setCurrent(page.getCurrent()); return result; } } ``` 链式调用 [#链式调用] ```java title="UserService.java" import com.xlf.utility.models.dto.PageDTO; public PageDTO getUserList(int pageNum, int pageSize) { List users = userMapper.selectList(pageNum, pageSize); long total = userMapper.selectCount(); // [!code highlight:6] // 使用链式调用 return new PageDTO() .setRecords(users) .setTotal(total) .setSize((long) pageSize) .setCurrent((long) pageNum); } ``` 在 Controller 中返回 [#在-controller-中返回] ```java title="UserController.java" import com.xlf.utility.BaseResponse; import com.xlf.utility.models.dto.PageDTO; @RestController @RequestMapping("/api/v1/user") public class UserController { @GetMapping("/list") public BaseResponse> getUserList( @RequestParam(defaultValue = "1") int pageNum, @RequestParam(defaultValue = "10") int pageSize ) { PageDTO pageData = userService.getUserList(pageNum, pageSize); return new BaseResponse<>( null, "Success", 200, "获取成功", null, null, pageData ); } } ``` JSON 响应示例 [#json-响应示例] ```json title="response.json" { "context": "550e8400-e29b-41d4-a716-446655440000", "output": "Success", "code": 200, "message": "获取成功", "duration": 25, "data": { "records": [ { "id": 1, "username": "user1", "email": "user1@example.com" }, { "id": 2, "username": "user2", "email": "user2@example.com" } ], "total": 100, "size": 10, "current": 1 } } ``` 从 JSON 设置记录 [#从-json-设置记录] ```java title="Example.java" import com.xlf.utility.models.dto.PageDTO; // [!code highlight:2] // 从 JSON 字符串设置记录(依赖 hutool-json) String jsonRecords = "[{\"id\":1,\"name\":\"test\"}]"; PageDTO page = new PageDTO<>(100L, 10L); page.setRecords(jsonRecords, UserDTO.class); ``` 前端分页计算 [#前端分页计算] 前端可根据 PageDTO 的字段计算分页信息: ```javascript title="pagination.js" // 总页数 const totalPages = Math.ceil(pageData.total / pageData.size); // 是否有上一页 const hasPrevious = pageData.current > 1; // 是否有下一页 const hasNext = pageData.current < totalPages; // 起始记录号 const startRecord = (pageData.current - 1) * pageData.size + 1; // 结束记录号 const endRecord = Math.min(pageData.current * pageData.size, pageData.total); ``` 下一步 [#下一步] # BusinessException (/docs/bamboo-base-java/base/exception/business) import { TypeTable } from '@/components/type-table'; BusinessException [#businessexception] `BusinessException` 是筱工具(Java) 的自定义业务异常类,位于 `com.xlf.utility.exception.library` 包下。继承自 `RuntimeException`,用于在程序运行期间抛出非受检异常,主要描述特定业务场景中的错误。 类定义 [#类定义] ```java title="BusinessException.java" package com.xlf.utility.exception.library; import com.xlf.utility.ErrorCode; import lombok.Getter; @Getter public class BusinessException extends RuntimeException { // [!code highlight:4] private final String errorMessage; // 错误消息 private final ErrorCode errorCode; // 错误代码 private final Object data; // 附加数据 private final boolean errorOutput; // 是否输出详细错误 // 构造方法... } ``` 字段说明 [#字段说明] 构造方法 [#构造方法] 主构造方法 [#主构造方法] ```java title="BusinessException.java" public BusinessException( String errorMessage, ErrorCode errorCode, Object data, boolean errorOutput ) ``` 创建完整的业务异常,指定所有属性。构造时会自动打印警告日志。 常用构造方法 [#常用构造方法] ```java title="BusinessException.java" // [!code highlight:2] // 仅指定消息和错误码(最常用) public BusinessException(String message, ErrorCode errorCode) // [!code highlight:2] // 携带附加数据 public BusinessException(String message, ErrorCode errorCode, Object data) // [!code highlight:2] // 指定是否输出错误详情 public BusinessException(String message, ErrorCode errorCode, boolean errorOutput) ``` 使用示例 [#使用示例] 基本用法 [#基本用法] ```java title="UserService.java" import com.xlf.utility.ErrorCode; import com.xlf.utility.exception.library.BusinessException; public UserDTO getUserById(Long id) { if (id == null) { // [!code highlight:2] // 抛出简单的业务异常 throw new BusinessException("用户 ID 不能为空", ErrorCode.PARAMETER_MISSING); } UserDTO user = userMapper.selectById(id); if (user == null) { // [!code highlight:2] throw new BusinessException("用户不存在", ErrorCode.NOT_FOUND); } return user; } ``` 携带附加数据 [#携带附加数据] ```java title="ValidateService.java" import com.xlf.utility.ErrorCode; import com.xlf.utility.exception.library.BusinessException; import java.util.Map; public void validateUser(UserDTO dto) { Map errors = new HashMap<>(); if (dto.getUsername() == null) { errors.put("username", "用户名不能为空"); } if (dto.getEmail() == null) { errors.put("email", "邮箱不能为空"); } if (!errors.isEmpty()) { // [!code highlight:2] // 携带验证错误详情 throw new BusinessException("参数验证失败", ErrorCode.PARAMETER_INVALID, errors); } } ``` 输出错误详情 [#输出错误详情] ```java title="AuthService.java" import com.xlf.utility.ErrorCode; import com.xlf.utility.exception.library.BusinessException; public void login(LoginDTO dto) { User user = userMapper.selectByUsername(dto.getUsername()); if (user == null) { // [!code highlight:2] // errorOutput = true,将详细错误信息返回给客户端 throw new BusinessException( "用户名 [" + dto.getUsername() + "] 不存在", ErrorCode.LOGIN_FAILED, true ); } if (!passwordMatch(dto.getPassword(), user.getPassword())) { // [!code highlight:2] throw new BusinessException("密码错误", ErrorCode.LOGIN_FAILED, true); } } ``` 异常处理流程 [#异常处理流程] ``` ┌──────────────────┐ │ Service 层抛出 │ │ BusinessException │ └────────┬─────────┘ │ ▼ ┌──────────────────┐ │ 全局异常处理器 │ │ @ControllerAdvice │ └────────┬─────────┘ │ ▼ ┌──────────────────┐ │ 构造 BaseResponse │ │ - errorCode.code │ │ - errorCode.output│ │ - message │ │ - errorOutput ? │ │ errorMessage │ └────────┬─────────┘ │ ▼ ┌──────────────────┐ │ 返回 JSON 响应 │ └──────────────────┘ ``` 日志输出 [#日志输出] BusinessException 在构造时会自动打印 WARN 级别日志: ``` 业务异常 | 错误代码: PARAMETER_INVALID, 错误消息: 参数验证失败, 附加数据: {username=用户名不能为空}, 错误输出标识: true ``` 与其他异常的区别 [#与其他异常的区别] | 异常类 | 场景 | 特点 | | ----------------------- | ------ | ----------------- | | `BusinessException` | 通用业务异常 | 最常用,适用于大部分业务场景 | | `NoPermissionException` | 权限不足 | 专门用于权限校验失败 | | `RequestException` | 远程调用失败 | 用于微服务间 HTTP 调用错误 | | `DeveloperException` | 开发者错误 | 用于代码逻辑错误,生产环境不应出现 | 下一步 [#下一步] # 异常处理架构 (/docs/bamboo-base-java/base/exception) 异常处理架构 [#异常处理架构] 竹简框架提供分层式异常处理架构,通过继承链实现从底层 Java 异常到上层业务异常的全面捕获。所有异常处理器均标注 `@RestControllerAdvice`,返回符合 `BaseResponse` 规范的标准化响应。 本文档描述的异常处理架构在 MVC 和 WebFlux 中概念相同。主要差异是返回类型: * MVC: `ResponseEntity>` * WebFlux: `Mono>>` 继承层级 [#继承层级] ``` JavaBaseExceptionHandler ← Java 基础异常(IOException, NullPointerException 等) └── PublicExceptionHandler ← 公共业务异常(BusinessException, CheckFailureException 等) └── SystemExceptionHandler ← Spring 框架异常(MethodArgumentNotValidException 等) ``` 每一层处理器继承上一层的全部异常处理能力,并在此基础上扩展新的异常类型。项目中只需继承最顶层的 `SystemExceptionHandler` 即可获得完整的异常处理能力。 处理器职责 [#处理器职责] | 处理器 | 层级 | 职责 | 典型异常 | | ---------------------------- | ---- | --------------- | ------------------------------------------- | | `JavaBaseExceptionHandler` | 基础层 | Java 标准库异常 | `NullPointerException`、`IOException` | | `PublicExceptionHandler` | 业务层 | 框架定义的业务异常 | `BusinessException`、`CheckFailureException` | | `SystemExceptionHandler` | 框架层 | Spring 框架异常 | `MethodArgumentNotValidException` | | `MysqlExceptionHandler` | 数据库层 | MySQL 相关异常 | `SQLException`、`MysqlDataTruncation` | | `PostgreSqlExceptionHandler` | 数据库层 | PostgreSQL 相关异常 | `PSQLException`、`DataTruncation` | 数据库异常处理器(`MysqlExceptionHandler` / `PostgreSqlExceptionHandler`)不在继承链中,需根据项目实际使用的数据库单独引入。 异常处理流程 [#异常处理流程] ``` Controller 抛出异常 │ ▼ Spring 异常解析器 │ ▼ 匹配 @ExceptionHandler │ ├── 匹配 SystemExceptionHandler? → 处理 Spring 框架异常 ├── 匹配 PublicExceptionHandler? → 处理业务异常 ├── 匹配 JavaBaseExceptionHandler?→ 处理 Java 基础异常 └── 未匹配 → 返回 500 默认响应 │ ▼ ResultUtil.error() 构建标准响应 ``` MVC 与 WebFlux 的差异 [#mvc-与-webflux-的差异] 异常处理架构在 MVC 和 WebFlux 中概念相同,主要差异在于返回类型: | 框架 | 返回类型 | | ---------------- | --------------------------------------- | | `bamboo-mvc` | `ResponseEntity>` | | `bamboo-webflux` | `Mono>>` | * 查看 **MVC 版本** 的具体实现:[Spring MVC 异常处理](/docs/bamboo-base-java/mvc/exception/spring-boot) * 查看 **WebFlux 版本** 的具体实现:[Spring WebFlux 异常处理](/docs/bamboo-base-java/webflux/exception/spring-boot) 扩展方式 [#扩展方式] 在项目中创建自定义异常处理器,继承 `SystemExceptionHandler` 即可获得全部内置异常处理能力: ```java title="GlobalExceptionHandler.java" // [!code highlight:2] // 继承 SystemExceptionHandler 获得完整的异常处理链 @RestControllerAdvice public class GlobalExceptionHandler extends SystemExceptionHandler { // [!code highlight:2] // 添加项目特有的异常处理 @ExceptionHandler(CustomBusinessException.class) public ResponseEntity> handleCustom(CustomBusinessException e) { return ResultUtil.error(ErrorCode.OPERATION_FAILED, e.getMessage(), null); } } ``` 下一步 [#下一步] # Java 基础异常 (/docs/bamboo-base-java/base/exception/java-base) import { TypeTable } from '@/components/type-table'; Java 基础异常 [#java-基础异常] `JavaBaseExceptionHandler` 是异常处理链的最底层,负责捕获 Java 标准库中的常见异常。该处理器标注 `@RestControllerAdvice`,所有异常均转换为标准化的 `BaseResponse` 响应。 本文档描述的异常映射表在 MVC 和 WebFlux 中完全一致。主要差异是返回类型: * MVC: `ResponseEntity>` * WebFlux: `Mono>>` 处理的异常类型 [#处理的异常类型] 客户端错误(4xx) [#客户端错误4xx] 服务端错误(5xx) [#服务端错误5xx] 日志级别 [#日志级别] 不同类型的异常使用不同的日志级别: | 日志级别 | 异常类型 | 说明 | | ------- | ------------------------------------------------------------------- | ----------- | | `ERROR` | NullPointerException, IOException, ClassCastException 等 | 服务端错误,需要关注 | | `WARN` | IllegalArgumentException, TimeoutException, FileNotFoundException 等 | 客户端错误或预期内异常 | 响应示例 [#响应示例] NullPointerException [#nullpointerexception] ```json title="response.json" { "context": "550e8400-e29b-41d4-a716-446655440000", "output": "ServerInternalError", "code": 50001, "message": "服务器内部错误", "errorMessage": "空指针异常", "duration": 3, "data": { "message": "Cannot invoke method on null reference" } } ``` HTTP 状态码为 `500`(50001 / 100)。 TimeoutException [#timeoutexception] ```json title="response.json" { "context": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "output": "Timeout", "code": 40801, "message": "请求超时", "errorMessage": "操作超时: Connection timed out", "duration": 30000, "data": null } ``` HTTP 状态码为 `408`(40801 / 100)。 FileNotFoundException [#filenotfoundexception] ```json title="response.json" { "context": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "output": "NotExist", "code": 40000, "message": "内容不存在", "errorMessage": "文件未找到: /data/config.json", "duration": 2, "data": null } ``` HTTP 状态码为 `400`(40000 / 100)。 设计说明 [#设计说明] * `IllegalArgumentException`、`FileNotFoundException`、`TimeoutException` 等映射为对应的客户端错误码 * `NullPointerException`、`IOException`、`ClassCastException` 等映射为服务端错误码 * `Exception` 兜底处理器确保任何未预期的异常都不会泄露堆栈信息至客户端 * 所有处理方法均通过 `ResultUtil.error()` 构建响应,自动注入上下文标识与请求耗时 下一步 [#下一步] # MySQL 异常处理 (/docs/bamboo-base-java/base/exception/mysql) import { TypeTable } from '@/components/type-table'; MySQL 异常处理 [#mysql-异常处理] `MysqlExceptionHandler` 专门处理 MySQL 数据库操作过程中产生的异常。该处理器独立于主异常处理继承链,通过自动配置注册。 本文档描述的异常映射表在 MVC 和 WebFlux 中完全一致。主要差异是返回类型: * MVC: `ResponseEntity>` * WebFlux: `Mono>>` 处理的异常类型 [#处理的异常类型] 处理器结构 [#处理器结构] ```java title="MysqlExceptionHandler.java" // [!code highlight:2] @RestControllerAdvice public class MysqlExceptionHandler { @ExceptionHandler(SQLSyntaxErrorException.class) public ResponseEntity> handleSqlSyntaxError( SQLSyntaxErrorException e) { // [!code highlight:2] // SQL 语法错误映射为服务器内部错误,避免暴露 SQL 细节 return ResultUtil.error( ErrorCode.SERVER_INTERNAL_ERROR, e.getMessage(), null ); } @ExceptionHandler(SQLException.class) public ResponseEntity> handleSqlException( SQLException e) { return ResultUtil.error( ErrorCode.SERVER_INTERNAL_ERROR, e.getMessage(), null ); } // [!code highlight:3] // 数据截断视为参数错误(客户端提交的数据不符合规范) @ExceptionHandler(MysqlDataTruncation.class) public ResponseEntity> handleDataTruncation( MysqlDataTruncation e) { return ResultUtil.error( ErrorCode.PARAMETER_ERROR, e.getMessage(), null ); } } ``` 典型场景 [#典型场景] SQL 语法错误 [#sql-语法错误] 当 MyBatis 动态 SQL 生成了无效的 SQL 语句时: ```json title="response.json" { "context": "550e8400-e29b-41d4-a716-446655440000", "output": "SERVER_INTERNAL_ERROR", "code": 50002, "message": null, "errorMessage": "You have an error in your SQL syntax", "duration": 12, "data": null } ``` 数据截断 [#数据截断] 当插入的数据超出字段长度限制时(例如 `VARCHAR(50)` 字段写入 100 字符): ```json title="response.json" { "context": "550e8400-e29b-41d4-a716-446655440000", "output": "PARAMETER_ERROR", "code": 40000, "message": null, "errorMessage": "Data truncation: Data too long for column 'username'", "duration": 8, "data": null } ``` 适用条件 [#适用条件] 该异常处理器仅在项目使用 MySQL 数据库时生效。若项目使用 PostgreSQL,请参阅 [PostgreSQL 异常处理](/docs/bamboo-base-java/base/exception/postgresql)。 下一步 [#下一步] # 异常处理架构 (/docs/bamboo-base-java/base/exception/overview) 异常处理架构 [#异常处理架构] 竹简框架提供分层式异常处理架构,通过继承链实现从底层 Java 异常到上层业务异常的全面捕获。所有异常处理器均标注 `@RestControllerAdvice`,返回符合 `BaseResponse` 规范的标准化响应。 继承层级 [#继承层级] ``` JavaBaseExceptionHandler ← Java 基础异常(IOException, NullPointerException 等) └── PublicExceptionHandler ← 公共业务异常(BusinessException, CheckFailureException 等) └── SystemExceptionHandler ← Spring 框架异常(MethodArgumentNotValidException 等) ``` 每一层处理器继承上一层的全部异常处理能力,并在此基础上扩展新的异常类型。项目中只需继承最顶层的 `SystemExceptionHandler` 即可获得完整的异常处理能力。 处理器职责 [#处理器职责] | 处理器 | 层级 | 职责 | 典型异常 | | ---------------------------- | ---- | --------------- | ------------------------------------------- | | `JavaBaseExceptionHandler` | 基础层 | Java 标准库异常 | `NullPointerException`、`IOException` | | `PublicExceptionHandler` | 业务层 | 框架定义的业务异常 | `BusinessException`、`CheckFailureException` | | `SystemExceptionHandler` | 框架层 | Spring 框架异常 | `MethodArgumentNotValidException` | | `MysqlExceptionHandler` | 数据库层 | MySQL 相关异常 | `SQLException`、`MysqlDataTruncation` | | `PostgreSqlExceptionHandler` | 数据库层 | PostgreSQL 相关异常 | `PSQLException`、`DataTruncation` | 数据库异常处理器(`MysqlExceptionHandler` / `PostgreSqlExceptionHandler`)不在继承链中,需根据项目实际使用的数据库单独引入。 异常处理流程 [#异常处理流程] ``` Controller 抛出异常 │ ▼ Spring 异常解析器 │ ▼ 匹配 @ExceptionHandler │ ├── 匹配 SystemExceptionHandler? → 处理 Spring 框架异常 ├── 匹配 PublicExceptionHandler? → 处理业务异常 ├── 匹配 JavaBaseExceptionHandler?→ 处理 Java 基础异常 └── 未匹配 → 返回 500 默认响应 │ ▼ ResultUtil.error() 构建标准响应 ``` MVC 与 WebFlux 的差异 [#mvc-与-webflux-的差异] 异常处理架构在 MVC 和 WebFlux 中概念相同,主要差异在于返回类型: | 框架 | 返回类型 | | ---------------- | --------------------------------------- | | `bamboo-mvc` | `ResponseEntity>` | | `bamboo-webflux` | `Mono>>` | * 查看 **MVC 版本** 的具体实现:[Spring MVC 异常处理](/docs/bamboo-base-java/mvc/exception/spring-boot) * 查看 **WebFlux 版本** 的具体实现:[Spring WebFlux 异常处理](/docs/bamboo-base-java/webflux/exception/spring-boot) 扩展方式 [#扩展方式] 在项目中创建自定义异常处理器,继承 `SystemExceptionHandler` 即可获得全部内置异常处理能力: ```java title="GlobalExceptionHandler.java" // [!code highlight:2] // 继承 SystemExceptionHandler 获得完整的异常处理链 @RestControllerAdvice public class GlobalExceptionHandler extends SystemExceptionHandler { // [!code highlight:2] // 添加项目特有的异常处理 @ExceptionHandler(CustomBusinessException.class) public ResponseEntity> handleCustom(CustomBusinessException e) { return ResultUtil.error(ErrorCode.OPERATION_FAILED, e.getMessage(), null); } } ``` 下一步 [#下一步] # PostgreSQL 异常处理 (/docs/bamboo-base-java/base/exception/postgresql) import { TypeTable } from '@/components/type-table'; PostgreSQL 异常处理 [#postgresql-异常处理] `PostgreSqlExceptionHandler` 专门处理 PostgreSQL 数据库操作过程中产生的异常。该处理器独立于主异常处理继承链,通过自动配置注册。 本文档描述的异常映射表在 MVC 和 WebFlux 中完全一致。主要差异是返回类型: * MVC: `ResponseEntity>` * WebFlux: `Mono>>` 处理的异常类型 [#处理的异常类型] 处理器结构 [#处理器结构] ```java title="PostgreSqlExceptionHandler.java" // [!code highlight:2] @RestControllerAdvice public class PostgreSqlExceptionHandler { @ExceptionHandler(SQLSyntaxErrorException.class) public ResponseEntity> handleSqlSyntaxError( SQLSyntaxErrorException e) { return ResultUtil.error( ErrorCode.SERVER_INTERNAL_ERROR, e.getMessage(), null ); } @ExceptionHandler(SQLException.class) public ResponseEntity> handleSqlException( SQLException e) { return ResultUtil.error( ErrorCode.SERVER_INTERNAL_ERROR, e.getMessage(), null ); } // [!code highlight:3] // PSQLException 是 PostgreSQL JDBC 驱动的专有异常类 @ExceptionHandler(PSQLException.class) public ResponseEntity> handlePsqlException( PSQLException e) { return ResultUtil.error( ErrorCode.SERVER_INTERNAL_ERROR, e.getMessage(), null ); } // [!code highlight:3] // 数据截断视为参数错误 @ExceptionHandler(DataTruncation.class) public ResponseEntity> handleDataTruncation( DataTruncation e) { return ResultUtil.error( ErrorCode.PARAMETER_ERROR, e.getMessage(), null ); } } ``` 与 MySQL 处理器的差异 [#与-mysql-处理器的差异] | 对比项 | MySQL | PostgreSQL | | ------ | --------------------- | ------------------------- | | 驱动专有异常 | `MysqlDataTruncation` | `PSQLException` | | 数据截断异常 | `MysqlDataTruncation` | `DataTruncation`(JDBC 标准) | | 错误信息格式 | MySQL 错误码 + 消息 | PostgreSQL 状态码 + 详细消息 | 典型场景 [#典型场景] PSQLException [#psqlexception] 当 PostgreSQL 约束违反时(如唯一键冲突): ```json title="response.json" { "context": "550e8400-e29b-41d4-a716-446655440000", "output": "SERVER_INTERNAL_ERROR", "code": 50002, "message": null, "errorMessage": "ERROR: duplicate key value violates unique constraint \"user_email_key\"", "duration": 10, "data": null } ``` 数据截断 [#数据截断] 当插入的数据超出字段长度限制时: ```json title="response.json" { "context": "550e8400-e29b-41d4-a716-446655440000", "output": "PARAMETER_ERROR", "code": 40000, "message": null, "errorMessage": "ERROR: value too long for type character varying(50)", "duration": 6, "data": null } ``` 适用条件 [#适用条件] 该异常处理器仅在项目使用 PostgreSQL 数据库时生效。若项目使用 MySQL,请参阅 [MySQL 异常处理](/docs/bamboo-base-java/base/exception/mysql)。 下一步 [#下一步] # 公共业务异常 (/docs/bamboo-base-java/base/exception/public) import { TypeTable } from '@/components/type-table'; 公共业务异常 [#公共业务异常] `PublicExceptionHandler` 继承自 `JavaBaseExceptionHandler`,负责处理框架层面定义的业务异常。这些异常通常由业务代码主动抛出,用于表达特定的业务错误语义。 本文档描述的异常映射表在 MVC 和 WebFlux 中完全一致。主要差异是返回类型: * MVC: `ResponseEntity>` * WebFlux: `Mono>>` 继承关系 [#继承关系] ``` JavaBaseExceptionHandler └── PublicExceptionHandler ← 当前层级 └── SystemExceptionHandler ``` 处理的异常类型 [#处理的异常类型] BusinessException 使用 [#businessexception-使用] `BusinessException` 是最常用的业务异常,支持自定义 `ErrorCode` 和错误消息: ```java title="UserService.java" @Service public class UserService { public UserVO getUserById(Long id) { UserEntity user = userMapper.selectById(id); if (user == null) { // [!code highlight:3] // 抛出业务异常,指定错误码和错误消息 throw new BusinessException( "用户不存在", ErrorCode.PAGE_NOT_FOUND ); } return convertToVO(user); } public void updateEmail(Long id, String email) { if (!isValidEmail(email)) { // [!code highlight:2] throw new BusinessException( "邮箱格式不正确", ErrorCode.PARAMETER_ERROR ); } // 业务逻辑... } } ``` 响应示例 [#响应示例] 抛出 `BusinessException("用户不存在", ErrorCode.PAGE_NOT_FOUND)` 时: ```json title="response.json" { "context": "550e8400-e29b-41d4-a716-446655440000", "output": "PAGE_NOT_FOUND", "code": 40403, "message": null, "errorMessage": "用户不存在", "duration": 8, "data": null } ``` HTTP 状态码为 `404`(40403 / 100)。 异常选择指南 [#异常选择指南] | 场景 | 推荐异常 | | ---------- | ---------------------------------------------------------------------- | | 通用业务错误 | `BusinessException` | | 资源不存在 | `PageNotFoundedException` 或 `BusinessException("...", PAGE_NOT_FOUND)` | | 参数校验失败 | `CheckFailureException` | | 用户未登录/令牌过期 | `UserAuthenticationException` | | 权限不足 | `NoPermissionException` | | 开发阶段的逻辑标记 | `DeveloperException` | 下一步 [#下一步] # BaseResponse (/docs/bamboo-base-java/base/response/base-response) import { TypeTable } from '@/components/type-table'; BaseResponse [#baseresponse] `BaseResponse` 位于 `com.xlf.utility`,用于统一接口返回结构。\ 该类使用 `@JsonInclude(JsonInclude.Include.NON_NULL)`,字段值为 `null` 时默认不输出到 JSON。 类定义 [#类定义] ```java title="BaseResponse.java" package com.xlf.utility; @Data @Builder @NoArgsConstructor @AllArgsConstructor @JsonInclude(JsonInclude.Include.NON_NULL) public class BaseResponse implements Serializable { private String context; @NotNull private String output; @NotNull private Integer code; @NotNull private String message; private String errorMessage; private Long duration; private E data; // 兼容旧版本的五参构造 public BaseResponse(String output, Integer code, String message, String errorMessage, E data) { this(null, output, code, message, errorMessage, null, data); } } ``` 字段说明 [#字段说明] 构造方式 [#构造方式] 全参构造(推荐在工具层使用) [#全参构造推荐在工具层使用] ```java title="Example.java" BaseResponse response = new BaseResponse<>( "550e8400-e29b-41d4-a716-446655440000", // context "Success", // output 200, // code "获取成功", // message null, // errorMessage 12L, // duration userDTO // data ); ``` 兼容构造(旧风格) [#兼容构造旧风格] ```java title="Example.java" BaseResponse response = new BaseResponse<>( "Success", 200, "获取成功", null, userDTO ); ``` Builder 构造(复杂场景推荐) [#builder-构造复杂场景推荐] ```java title="Example.java" BaseResponse response = BaseResponse.builder() .context("550e8400-e29b-41d4-a716-446655440000") .output("Success") .code(200) .message("获取成功") .duration(12L) .data(userDTO) .build(); ``` > 当前实现中**没有** `BaseResponse(ErrorCode, ...)` 形式的构造函数。 响应示例 [#响应示例] 成功响应 [#成功响应] ```json title="response.json" { "context": "550e8400-e29b-41d4-a716-446655440000", "output": "Success", "code": 200, "message": "获取成功", "duration": 12, "data": { "id": 1, "username": "xiao_lfeng" } } ``` 错误响应 [#错误响应] ```json title="response.json" { "context": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "output": "ParameterMissing", "code": 40003, "message": "参数缺失", "errorMessage": "用户 ID 不能为空", "duration": 3 } ``` 使用建议 [#使用建议] * 在 `bamboo-mvc` 或 `bamboo-webflux` 中,优先使用各模块 `ResultUtil` 统一构造响应。 * 成功响应建议 `output="Success"`、`code=200`。 * 错误响应建议从 `ErrorCode` 读取 `output` 与 `code`,避免硬编码。 下一步 [#下一步] # ErrorCode (/docs/bamboo-base-java/base/response/error-code) import { TypeTable } from '@/components/type-table'; ErrorCode [#errorcode] `ErrorCode` 是筱工具(Java) 的错误码枚举类,位于 `com.xlf.utility` 包下。每个枚举值包含英文标识、5 位数字响应码与中文描述,用于标准化 API 响应状态。 枚举定义 [#枚举定义] ```java title="ErrorCode.java" package com.xlf.utility; public enum ErrorCode { NOT_EXIST("NotExist", 40000, "内容不存在"), EXISTED("Existed", 40001, "内容已存在"), PARAMETER_ERROR("ParameterError", 40002, "参数错误"), // ... 58 个预定义错误码 // [!code highlight:4] private final String output; // 英文标识 private final Integer code; // 5 位数字响应码 private final String message; // 中文描述 } ``` 枚举字段 [#枚举字段] 响应码规则 [#响应码规则] ErrorCode 的 5 位数字响应码遵循以下规则: ``` ┌─────────────┬───────────────┐ │ 前 3 位 │ 后 2 位 │ │ HTTP 状态码 │ 业务细分码 │ └─────────────┴───────────────┘ 示例:40024 → HTTP 400 + 细分码 24 50001 → HTTP 500 + 细分码 01 ``` HTTP 状态码可通过 `code / 100` 计算得出。例如 `40024 / 100 = 400`,`50001 / 100 = 500`。 完整错误码列表 [#完整错误码列表] 通用错误码(400xx) [#通用错误码400xx] | 枚举值 | output | code | 说明 | | ----------- | -------- | ----- | ----- | | `NOT_EXIST` | NotExist | 40000 | 内容不存在 | | `EXISTED` | Existed | 40001 | 内容已存在 | 400 请求错误 [#400-请求错误] | 枚举值 | output | code | 说明 | | ------------------------- | --------------------- | ----- | -------- | | `BAD_REQUEST` | BadRequest | 40024 | 错误请求 | | `PARAMETER_ERROR` | ParameterError | 40002 | 参数错误 | | `PARAMETER_MISSING` | ParameterMissing | 40003 | 参数缺失 | | `PARAMETER_INVALID` | ParameterInvalid | 40004 | 参数无效 | | `PARAMETER_ILLEGAL` | ParameterIllegal | 40005 | 参数非法 | | `PARAMETER_TYPE_ERROR` | ParameterTypeError | 40006 | 参数类型错误 | | `BODY_ERROR` | BodyError | 40007 | 请求体错误 | | `BODY_MISSING` | BodyMissing | 40008 | 请求体缺失 | | `BODY_INVALID` | BodyInvalid | 40009 | 请求体无效 | | `BODY_ILLEGAL` | BodyIllegal | 40010 | 请求体非法 | | `BODY_TYPE_ERROR` | BodyTypeError | 40011 | 请求体类型错误 | | `HEADER_ERROR` | HeaderError | 40012 | 请求头错误 | | `HEADER_MISSING` | HeaderMissing | 40013 | 请求头缺失 | | `HEADER_INVALID` | HeaderInvalid | 40014 | 请求头无效 | | `HEADER_ILLEGAL` | HeaderIllegal | 40015 | 请求头非法 | | `HEADER_TYPE_ERROR` | HeaderTypeError | 40016 | 请求头类型错误 | | `OPERATION_ERROR` | OperationError | 40017 | 操作错误 | | `OPERATION_FAILED` | OperationFailed | 40018 | 操作失败 | | `OPERATION_INVALID` | OperationInvalid | 40019 | 操作无效 | | `OPERATION_ILLEGAL` | OperationIllegal | 40020 | 操作非法 | | `OPERATION_DENIED` | OperationDenied | 40021 | 操作被拒绝 | | `OPERATION_NOT_ALLOWED` | OperationNotAllowed | 40022 | 操作不允许 | | `OPERATION_NOT_SUPPORTED` | OperationNotSupported | 40023 | 操作不支持 | | `OPERATION_TYPE_ERROR` | OperationTypeError | 40025 | 操作类型错误 | | `EXPIRED` | Expired | 40026 | 内容已过期 | | `REQUEST_ERROR` | RequestError | 40027 | 远程服务请求失败 | 401 未授权 [#401-未授权] | 枚举值 | output | code | 说明 | | -------------- | ------------ | ----- | ---- | | `UNAUTHORIZED` | Unauthorized | 40101 | 未授权 | | `LOGIN_FAILED` | LoginFailed | 40102 | 登录失败 | 403 禁止访问 [#403-禁止访问] | 枚举值 | output | code | 说明 | | ------------------- | ---------------- | ----- | ---- | | `FORBIDDEN` | Forbidden | 40301 | 禁止访问 | | `PERMISSION_DENIED` | PermissionDenied | 40302 | 权限拒绝 | | `ACCESS_LIMITED` | AccessLimited | 40303 | 访问受限 | 404 资源不存在 [#404-资源不存在] | 枚举值 | output | code | 说明 | | -------------------- | ---------------- | ----- | ----- | | `PAGE_NOT_FOUND` | PageNotFound | 40401 | 页面未找到 | | `NOT_FOUND` | NotFound | 40402 | 未找到 | | `RESOURCE_NOT_FOUND` | ResourceNotFound | 40403 | 资源未找到 | 405 方法不允许 [#405-方法不允许] | 枚举值 | output | code | 说明 | | -------------------- | ---------------- | ----- | ----- | | `METHOD_NOT_ALLOWED` | MethodNotAllowed | 40501 | 方法不允许 | 406 不可接受 [#406-不可接受] | 枚举值 | output | code | 说明 | | ---------------- | ------------- | ----- | ---- | | `NOT_ACCEPTABLE` | NotAcceptable | 40601 | 不可接受 | 408 请求超时 [#408-请求超时] | 枚举值 | output | code | 说明 | | -------------------- | ----------------- | ----- | ---- | | `TIMEOUT` | Timeout | 40801 | 请求超时 | | `CONNECTION_TIMEOUT` | ConnectionTimeout | 40802 | 连接超时 | | `READ_TIMEOUT` | ReadTimeout | 40803 | 读取超时 | | `WRITE_TIMEOUT` | WriteTimeout | 40804 | 写入超时 | 429 请求过多 [#429-请求过多] | 枚举值 | output | code | 说明 | | ----------------------- | ------------------ | ----- | ------ | | `TOO_MANY_REQUESTS` | TooManyRequests | 42901 | 请求过多 | | `REQUEST_RATE_TOO_HIGH` | RequestRateTooHigh | 42902 | 请求频率过高 | 500 服务端错误 [#500-服务端错误] | 枚举值 | output | code | 说明 | | ----------------------- | ------------------- | ----- | ------- | | `SERVER_INTERNAL_ERROR` | ServerInternalError | 50001 | 服务器内部错误 | | `DATABASE_ERROR` | DatabaseError | 50002 | 数据库错误 | | `CACHE_ERROR` | CacheError | 50003 | 缓存错误 | | `FILE_ERROR` | FileError | 50004 | 文件错误 | | `STORAGE_ERROR` | StorageError | 50005 | 存储错误 | | `REMOTE_CALL_ERROR` | RemoteCallError | 50006 | 远程调用错误 | | `CONFIGURATION_ERROR` | ConfigurationError | 50007 | 配置错误 | | `RESOURCE_EXHAUSTED` | ResourceExhausted | 50008 | 资源耗尽 | | `NETWORK_ERROR` | NetworkError | 50009 | 网络错误 | | `NETWORK_NOT_SUCCESS` | NetworkNotSuccess | 50010 | 网络请求不成功 | 502/503 网关与服务错误 [#502503-网关与服务错误] | 枚举值 | output | code | 说明 | | --------------------- | ------------------ | ----- | ----- | | `GATEWAY_ERROR` | GatewayError | 50201 | 网关错误 | | `SERVICE_UNAVAILABLE` | ServiceUnavailable | 50301 | 服务不可用 | | `SYSTEM_MAINTENANCE` | SystemMaintenance | 50302 | 系统维护 | 特殊错误码 [#特殊错误码] | 枚举值 | output | code | 说明 | | --------------- | ------------ | ----- | ---- | | `UNKNOWN_ERROR` | UnknownError | 50999 | 未知错误 | 使用方式 [#使用方式] 组合 BaseResponse [#组合-baseresponse] ```java title="Example.java" import com.xlf.utility.BaseResponse; import com.xlf.utility.ErrorCode; // [!code highlight:2] // 使用 ErrorCode 字段构造标准错误响应 BaseResponse response = new BaseResponse<>(null, ErrorCode.PARAMETER_ERROR.getOutput(), ErrorCode.PARAMETER_ERROR.getCode(), ErrorCode.PARAMETER_ERROR.getMessage(), "用户名不能为空", null, null); ``` 抛出业务异常 [#抛出业务异常] ```java title="UserService.java" import com.xlf.utility.ErrorCode; import com.xlf.utility.exception.library.BusinessException; public UserDTO getUserById(Long id) { if (id == null) { // [!code highlight:2] // 抛出携带 ErrorCode 的业务异常,由全局异常处理器统一捕获 throw new BusinessException("用户 ID 不能为空", ErrorCode.PARAMETER_MISSING); } // ... } ``` 条件判断 [#条件判断] ```java title="AuthService.java" public void validateToken(String token) { if (token == null || token.isEmpty()) { throw new BusinessException("令牌不能为空", ErrorCode.UNAUTHORIZED); } if (isTokenExpired(token)) { // [!code highlight:2] // 根据具体场景选择最精确的错误码 throw new BusinessException("令牌已过期,请重新登录", ErrorCode.EXPIRED); } } ``` 下一步 [#下一步] # GeneSnowflakeInfoDTO (/docs/bamboo-base-java/base/snowflake/gene-info-dto) import { TypeTable } from '@/components/type-table'; GeneSnowflakeInfoDTO [#genesnowflakeinfodto] `GeneSnowflakeInfoDTO` 位于 `com.xlf.utility.models.dto`,由 `GeneSnowflakeUtil.parseInfo(long id)` 返回。\ 用于展示带业务基因的雪花 ID 解析结果。 类定义 [#类定义] ```java title="GeneSnowflakeInfoDTO.java" package com.xlf.utility.models.dto; public class GeneSnowflakeInfoDTO { private Long id; private Long timestamp; private Long machineId; private Integer gene; private Long datacenterId; private Long sequence; private LocalDateTime generatedTime; } ``` 字段说明 [#字段说明] 使用示例 [#使用示例] ```java title="Example.java" import com.xlf.utility.models.dto.GeneSnowflakeInfoDTO; import com.xlf.utility.utility.GeneSnowflakeUtil; long id = GeneSnowflakeUtil.generateId("order"); GeneSnowflakeInfoDTO info = GeneSnowflakeUtil.parseInfo(id); System.out.println(info.getGene()); System.out.println(info.getDatacenterId()); System.out.println(info.getMachineId()); System.out.println(info.getGeneratedTime()); ``` 下一步 [#下一步] # GeneSnowflakeUtil (/docs/bamboo-base-java/base/snowflake/gene-util) import { TypeTable } from '@/components/type-table'; GeneSnowflakeUtil [#genesnowflakeutil] `GeneSnowflakeUtil` 位于 `com.xlf.utility.utility`。\ 它基于标准雪花 ID 重组位布局,嵌入 8 位业务基因(CRC8),让 ID 自带业务语义。 位结构 [#位结构] ``` [1-bit sign][41-bit timestamp][4-bit machine][8-bit gene][3-bit datacenter][8-bit sequence] ``` > 说明:机器位与数据中心位、序列位是从标准雪花结构压缩后得到,吞吐量相较标准模式更低(同毫秒最多 256)。 方法概览 [#方法概览] 生成方法 [#生成方法] 解析方法 [#解析方法] 基因与校验方法 [#基因与校验方法] 使用示例 [#使用示例] ```java title="Example.java" import com.xlf.utility.utility.GeneSnowflakeUtil; long userId = GeneSnowflakeUtil.generateId("user"); long orderId = GeneSnowflakeUtil.generateId("order"); int userGene = GeneSnowflakeUtil.parseGene(userId); boolean ok = GeneSnowflakeUtil.verifyGene(orderId, "order"); ``` 注意事项 [#注意事项] * CRC8 基因位存在碰撞概率,不能替代严格业务字段。 * 适合做“快速路由/粗粒度分类”,不适合做强一致主业务键判定。 * 基因模式吞吐上限低于标准雪花(序列号仅 8 位)。 下一步 [#下一步] # SnowflakeIdGenerator (/docs/bamboo-base-java/base/snowflake/generator) import { TypeTable } from '@/components/type-table'; SnowflakeIdGenerator [#snowflakeidgenerator] `SnowflakeIdGenerator` 位于 `com.xlf.utility.incrementer`,是标准雪花算法核心实现。\ 它实现了 MyBatis-Plus 的 `IdentifierGenerator` 接口,核心生成方法为 `nextId(Object entity)`。 类定义 [#类定义] ```java title="SnowflakeIdGenerator.java" package com.xlf.utility.incrementer; public class SnowflakeIdGenerator implements IdentifierGenerator { public SnowflakeIdGenerator() {} public SnowflakeIdGenerator(long datacenterId, long machineId) { configure(datacenterId, machineId); } public SnowflakeIdGenerator(long datacenterId, long machineId, long epoch) { this.epoch = epoch; configure(datacenterId, machineId); } public void configure(long datacenterId, long machineId) { ... } @Override public Number nextId(Object entity) { ... } @Override public String nextUUID(Object entity) { ... } } ``` 参数与范围 [#参数与范围] 关键行为 [#关键行为] * 生成方法使用内部 `lock` 同步,保证并发安全。 * 同毫秒内序列号自增,溢出后自旋等待下一毫秒。 * 检测到时钟回拨时,会抛出 `BusinessException`(`SERVER_INTERNAL_ERROR`)。 * 未调用 `configure(...)` 就生成 ID,也会抛 `BusinessException`。 使用示例 [#使用示例] 手动创建并生成 [#手动创建并生成] ```java title="Example.java" import com.xlf.utility.incrementer.SnowflakeIdGenerator; SnowflakeIdGenerator generator = new SnowflakeIdGenerator(1, 1, 1690214400000L); Long id = (Long) generator.nextId(null); String idString = generator.nextUUID(null); ``` 默认构造 + configure [#默认构造--configure] ```java title="Example.java" SnowflakeIdGenerator generator = new SnowflakeIdGenerator(); generator.configure(2, 10); Long id = (Long) generator.nextId(null); ``` 与 SnowflakeUtil 的关系 [#与-snowflakeutil-的关系] `SnowflakeUtil` 是该生成器的静态门面。\ 业务层通常直接用 `SnowflakeUtil.generateId()`;只有自定义初始化流程时才直接操作生成器。 下一步 [#下一步] # 雪花算法 (/docs/bamboo-base-java/base/snowflake) 雪花算法 [#雪花算法] `bamboo-base` 提供两套 ID 能力: * **标准雪花 ID**:`SnowflakeIdGenerator` + `SnowflakeUtil` * **基因雪花 ID**:`GeneSnowflakeUtil`(在标准雪花基础上嵌入 8 位业务基因) 标准雪花结构(64 位) [#标准雪花结构64-位] ``` [1-bit sign][41-bit timestamp][5-bit datacenter][5-bit machine][12-bit sequence] ``` | 段 | 位数 | 范围 | 说明 | | ---------- | -- | ------- | -------------- | | sign | 1 | 固定 0 | 保证正数 | | timestamp | 41 | 约 69 年 | 相对 epoch 的时间偏移 | | datacenter | 5 | 0\~31 | 数据中心 | | machine | 5 | 0\~31 | 机器编号 | | sequence | 12 | 0\~4095 | 同毫秒内序号 | 基因雪花结构(64 位) [#基因雪花结构64-位] `GeneSnowflakeUtil` 会把标准雪花重组为: ``` [1-bit sign][41-bit timestamp][4-bit machine][8-bit gene][3-bit datacenter][8-bit sequence] ``` 这让 ID 自带业务基因(CRC8 计算),可用于业务分片路由与快速归类。\ 注意:8 位基因存在碰撞概率(理论约 `1/256`)。 Spring Boot 配置 [#spring-boot-配置] 雪花算法通过 `bamboo.base.snowflake` 初始化: ```yaml title="application.yml" bamboo: base: snowflake: datacenter-id: 1 machine-id: 1 epoch: 1690214400000 ``` | 配置项 | 默认值 | 说明 | | --------------- | --------------- | -------- | | `datacenter-id` | `1` | 数据中心 ID | | `machine-id` | `1` | 机器 ID | | `epoch` | `1690214400000` | 起始纪元(毫秒) | 自动初始化 [#自动初始化] 在 Spring 环境下,`SnowflakeUtilInitializer` 会在启动时读取配置并调用 `SnowflakeUtil.initialize(...)`。\ 在非 Spring 环境下,`SnowflakeUtil` 会按默认值懒初始化生成器。 快速使用 [#快速使用] ```java title="Example.java" import com.xlf.utility.utility.SnowflakeUtil; import com.xlf.utility.utility.GeneSnowflakeUtil; long id = SnowflakeUtil.generateId(); String idStr = SnowflakeUtil.generateIdString(); long orderId = GeneSnowflakeUtil.generateId("order"); boolean isOrder = GeneSnowflakeUtil.verifyGene(orderId, "order"); ``` 下一步 [#下一步] # SnowflakeInfoDTO (/docs/bamboo-base-java/base/snowflake/info-dto) import { TypeTable } from '@/components/type-table'; SnowflakeInfoDTO [#snowflakeinfodto] `SnowflakeInfoDTO` 位于 `com.xlf.utility.models.dto`,由 `SnowflakeUtil.parseInfo(long id)` 返回。\ 用于展示标准雪花 ID 的结构化解析结果。 类定义 [#类定义] ```java title="SnowflakeInfoDTO.java" package com.xlf.utility.models.dto; public class SnowflakeInfoDTO { private Long id; private Long timestamp; private Long datacenterId; private Long machineId; private Long sequence; private LocalDateTime generatedTime; } ``` 字段说明 [#字段说明] 使用示例 [#使用示例] ```java title="Example.java" import com.xlf.utility.models.dto.SnowflakeInfoDTO; import com.xlf.utility.utility.SnowflakeUtil; long id = SnowflakeUtil.generateId(); SnowflakeInfoDTO info = SnowflakeUtil.parseInfo(id); System.out.println("id = " + info.getId()); System.out.println("timestamp(offset) = " + info.getTimestamp()); System.out.println("datacenterId = " + info.getDatacenterId()); System.out.println("machineId = " + info.getMachineId()); System.out.println("sequence = " + info.getSequence()); System.out.println("generatedTime = " + info.getGeneratedTime()); ``` 与基因版本的区别 [#与基因版本的区别] `SnowflakeInfoDTO` 仅对应**标准雪花**。\ 基因雪花请使用 `GeneSnowflakeInfoDTO`,包含 `gene` 字段和不同位布局。 下一步 [#下一步] # SnowflakeUtil (/docs/bamboo-base-java/base/snowflake/util) import { TypeTable } from '@/components/type-table'; SnowflakeUtil [#snowflakeutil] `SnowflakeUtil` 位于 `com.xlf.utility.utility`,内部持有单例 `SnowflakeIdGenerator`。\ 它是业务层最常用的标准雪花 ID 入口。 初始化机制 [#初始化机制] * Spring 环境:由 `SnowflakeUtilInitializer` 调用 `initialize(...)` 完成初始化。 * 非 Spring 环境:首次调用时按默认值懒初始化(`datacenterId=1`、`machineId=1`、`epoch=1729440000000`)。 方法列表 [#方法列表] 生成方法 [#生成方法] 解析方法 [#解析方法] 校验与配置 [#校验与配置] 使用示例 [#使用示例] 生成与解析 [#生成与解析] ```java title="Example.java" import com.xlf.utility.models.dto.SnowflakeInfoDTO; import com.xlf.utility.utility.SnowflakeUtil; long id = SnowflakeUtil.generateId(); String idString = SnowflakeUtil.generateIdString(); long realTimestamp = SnowflakeUtil.parseTimestamp(id); SnowflakeInfoDTO info = SnowflakeUtil.parseInfo(id); System.out.println(idString); System.out.println(realTimestamp); System.out.println(info.getGeneratedTime()); ``` 合法性校验 [#合法性校验] ```java title="Example.java" Long id = SnowflakeUtil.generateId(); boolean ok = SnowflakeUtil.isValid(id); boolean invalid = SnowflakeUtil.isValid(null); ``` 非 Spring 场景手动初始化 [#非-spring-场景手动初始化] ```java title="Example.java" import com.xlf.utility.incrementer.SnowflakeIdGenerator; import com.xlf.utility.utility.SnowflakeUtil; SnowflakeIdGenerator generator = new SnowflakeIdGenerator(1, 1, 1690214400000L); SnowflakeUtil.initialize(generator); ``` 下一步 [#下一步] # UuidUtil (/docs/bamboo-base-java/base/uuid/uuid-util) import { TypeTable } from '@/components/type-table'; UuidUtil [#uuidutil] `UuidUtil` 是筱工具(Java) 的 UUID 工具类,位于 `com.xlf.utility.utility` 包下。提供 UUID 生成、转换与格式化等静态方法。 类定义 [#类定义] ```java title="UuidUtil.java" package com.xlf.utility.utility; import java.util.UUID; public class UuidUtil { // 静态方法,无需实例化 } ``` 方法列表 [#方法列表] 生成方法 [#生成方法] 转换方法 [#转换方法] 从字符串生成 [#从字符串生成] 使用示例 [#使用示例] 生成随机 UUID [#生成随机-uuid] ```java title="Example.java" import com.xlf.utility.utility.UuidUtil; // [!code highlight:2] // 生成标准 UUID(带横杠) String uuid = UuidUtil.generateStringUuid(); // 输出: "550e8400-e29b-41d4-a716-446655440000" // [!code highlight:2] // 生成无横杠 UUID String uuidNoDash = UuidUtil.generateUuidNoDash(); // 输出: "550e8400e29b41d4a716446655440000" // [!code highlight:2] // 生成 UUID 对象 UUID uuidObj = UuidUtil.generateUuid(); ``` 生成链路追踪 ID [#生成链路追踪-id] ```java title="TraceService.java" import com.xlf.utility.utility.UuidUtil; public class TraceService { public String generateTraceId() { // [!code highlight:2] // 使用无横杠 UUID 作为链路追踪 ID return UuidUtil.generateUuidNoDash(); } } ``` 从字符串生成确定性 UUID [#从字符串生成确定性-uuid] ```java title="GravatarService.java" import com.xlf.utility.utility.UuidUtil; public class GravatarService { public String getGravatarUrl(String email) { // [!code highlight:2] // 根据邮箱生成确定性 UUID(相同邮箱始终生成相同结果) String hash = UuidUtil.makeUuidFromStringNoDash(email.toLowerCase()); return "https://www.gravatar.com/avatar/" + hash; } } ``` UUID 转换 [#uuid-转换] ```java title="Example.java" import com.xlf.utility.utility.UuidUtil; import java.util.UUID; // [!code highlight:2] // 字符串转 UUID 对象 UUID uuid = UuidUtil.convertToUuid("550e8400-e29b-41d4-a716-446655440000"); ``` UUID 格式说明 [#uuid-格式说明] ``` 标准格式(36 位): 550e8400-e29b-41d4-a716-446655440000 └─8位─┘ └─4位┘ └─4位┘ └─4位┘ └───12位───┘ 无横杠格式(32 位): 550e8400e29b41d4a716446655440000 ``` 与 UUID v7 的区别 [#与-uuid-v7-的区别] | 特性 | UUID v4 (UuidUtil) | UUID v7 (UuidV7Generator) | | ---- | ------------------ | ------------------------- | | 排序性 | 随机,无序 | 时间有序 | | 碰撞概率 | 理论上存在 | 极低 | | 适用场景 | 链路追踪、临时 ID | 数据库主键、分布式 ID | | 性能 | 高 | 略低(需获取时间戳) | 下一步 [#下一步] # UuidV7Generator (/docs/bamboo-base-java/base/uuid/uuid-v7-generator) import { TypeTable } from '@/components/type-table'; UuidV7Generator [#uuidv7generator] `UuidV7Generator` 是筱工具(Java) 的 UUID v7 生成器,位于 `com.xlf.utility.incrementer` 包下。\ 它实现了 MyBatis-Plus 的 `IdentifierGenerator` 接口: * `nextUUID(...)`:生成 UUID v7 字符串(推荐用于字符串主键) * `nextId(...)`:返回 6 位随机整数(用于数值主键场景的兜底) 类定义 [#类定义] ```java title="UuidV7Generator.java" package com.xlf.utility.incrementer; import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator; import com.github.f4b6a3.uuid.UuidCreator; public class UuidV7Generator implements IdentifierGenerator { @Override public Number nextId(Object entity) { return Integer.parseInt(RandomUtil.createRandomInt(6)); } @Override public String nextUUID(Object entity) { // [!code highlight:2] return UuidCreator.getTimeOrderedEpoch().toString(); } } ``` 方法行为说明 [#方法行为说明] | 方法 | 返回值 | 说明 | | ------------------------- | -------- | ------------------------------------------------- | | `nextUUID(Object entity)` | `String` | 基于 `UuidCreator.getTimeOrderedEpoch()` 生成 UUID v7 | | `nextId(Object entity)` | `Number` | 返回 6 位随机整数(`RandomUtil.createRandomInt(6)`) | UUID v7 特性 [#uuid-v7-特性] UUID v7 是一种时间有序的 UUID 格式,相较于传统的 UUID v4 具有以下优势: | 特性 | UUID v4 | UUID v7 | | --------- | -------- | ------------- | | **排序性** | 完全随机,无序 | 按时间顺序递增 | | **数据库友好** | 索引效率低 | 索引效率高(近似有序) | | **碰撞概率** | 理论存在 | 极低(时间戳 + 随机位) | | **可追溯** | 无法获取生成时间 | 可提取生成时间戳 | UUID v7 结构 [#uuid-v7-结构] ``` ┌──────────────┬────────────┬──────────────┬──────────────────┐ │ 时间戳 │ 版本位 │ 序列位 │ 随机位 │ │ (48 bits) │ (4 bits) │ (12 bits) │ (62 bits) │ │ 毫秒级时间戳 │ 固定为 7 │ 同毫秒内自增 │ 随机数 │ └──────────────┴────────────┴──────────────┴──────────────────┘ ``` 在 MyBatis-Plus 中使用 [#在-mybatis-plus-中使用] 配置生成器 [#配置生成器] ```java title="MybatisPlusConfig.java" import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import com.xlf.utility.incrementer.UuidV7Generator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MybatisPlusConfig { @Bean public UuidV7Generator uuidV7Generator() { // [!code highlight:2] return new UuidV7Generator(); } } ``` 实体类配置 [#实体类配置] ```java title="UserDO.java" import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; @Data @TableName("t_user") public class UserDO { // [!code highlight:3] // 使用 ASSIGN_UUID 策略,MyBatis-Plus 会调用 UuidV7Generator.nextUUID() @TableId(type = IdType.ASSIGN_UUID) private String id; private String username; private String email; private LocalDateTime createdAt; } ``` 插入时自动生成 [#插入时自动生成] ```java title="UserService.java" import org.springframework.stereotype.Service; @Service public class UserService { public void createUser(UserDTO dto) { UserDO user = new UserDO(); user.setUsername(dto.getUsername()); user.setEmail(dto.getEmail()); user.setCreatedAt(LocalDateTime.now()); // id 字段无需手动设置 // [!code highlight:2] // 插入时 MyBatis-Plus 自动调用 UuidV7Generator 生成 UUID v7 userMapper.insert(user); // 此时 user.getId() 已被赋值 System.out.println("生成的 ID: " + user.getId()); } } ``` 依赖说明 [#依赖说明] `UuidV7Generator` 依赖 `uuid-creator` 库: ```xml title="pom.xml" com.github.f4b6a3 uuid-creator 6.1.1 ``` 生成的 UUID 示例 [#生成的-uuid-示例] ```java title="Example.java" import com.xlf.utility.incrementer.UuidV7Generator; UuidV7Generator generator = new UuidV7Generator(); // [!code highlight:4] // 连续生成多个 UUID v7(时间有序) String id1 = generator.nextUUID(null); // 018f3b7a-1c2d-7d3e-8f4a-5b6c7d8e9f0a String id2 = generator.nextUUID(null); // 018f3b7a-1c2d-7d3e-8f4a-5b6c7d8e9f0b String id3 = generator.nextUUID(null); // 018f3b7a-1c2d-7d3e-8f4a-5b6c7d8e9f0c // UUID v7 通常按时间趋势有序,适合索引插入场景 ``` 与雪花算法的对比 [#与雪花算法的对比] | 特性 | UUID v7 | 雪花算法 | | -------------- | ---------------------- | ------------- | | **长度** | 36 字符字符串 | 19 位数字 | | **存储空间** | 36 字节(字符串)或 16 字节(二进制) | 8 字节 | | **排序性** | 时间有序 | 时间有序 | | **数据中心/机器 ID** | 无 | 支持 | | **基因位** | 无 | 支持 | | **适用场景** | 需要字符串 ID、跨系统兼容 | 高性能、短 ID、分库分表 | 选择建议 [#选择建议] * **使用 UUID v7**:需要字符串格式 ID、跨系统集成、对 ID 长度不敏感 * **使用雪花算法**:高性能要求、需要短数字 ID、分库分表场景 下一步 [#下一步] # 业务日志切面 (/docs/bamboo-base-java/framework/aspect/business-log) import { TypeTable } from '@/components/type-table'; BusinessLogAspect [#businesslogaspect] `BusinessLogAspect` 是一个独立的业务日志工具类,提供 Controller、Service 和 DAO 三层的日志前置处理方法。与 `LogAspectHandler` 基于 AOP 自动拦截不同,`BusinessLogAspect` 需要在业务代码中显式调用,适用于需要自定义日志行为的场景。 方法详解 [#方法详解] 使用示例 [#使用示例] Controller 层 [#controller-层] ```java title="OrderController.java" @RestController @RequestMapping("/api/v1/order") @RequiredArgsConstructor public class OrderController { private final BusinessLogAspect businessLogAspect; @PostMapping("/create") public ResponseEntity> createOrder( @RequestBody OrderDTO dto) { // [!code highlight:2] // 手动记录 Controller 层日志 businessLogAspect.beforeControllerLog(); OrderVO order = orderService.createOrder(dto); return ResultUtil.success("订单创建成功", order); } } ``` Service 层 [#service-层] ```java title="OrderService.java" @Service @RequiredArgsConstructor public class OrderService { private final BusinessLogAspect businessLogAspect; public OrderVO createOrder(OrderDTO dto) { // [!code highlight:2] // 手动记录 Service 层日志 businessLogAspect.beforeServiceLog(); // 业务逻辑... return orderVO; } } ``` DAO 层 [#dao-层] ```java title="OrderRepository.java" @Repository @RequiredArgsConstructor public class OrderRepository { private final BusinessLogAspect businessLogAspect; public OrderEntity findByOrderNo(String orderNo) { // [!code highlight:2] // 手动记录 DAO 层日志 businessLogAspect.beforeDaoLog(); return orderMapper.selectByOrderNo(orderNo); } } ``` 与 LogAspectHandler 的对比 [#与-logaspecthandler-的对比] | 特性 | LogAspectHandler | BusinessLogAspect | | ----- | ---------------- | ----------------- | | 触发方式 | AOP 自动拦截 | 手动显式调用 | | 覆盖范围 | 全局生效 | 按需使用 | | 自定义程度 | 固定格式 | 可结合业务逻辑 | | 适用场景 | 通用日志记录 | 特定业务流程的日志增强 | 注意事项 [#注意事项] * `BusinessLogAspect` 是一个独立的组件类,非 AOP 切面,不会自动拦截方法调用 * 若项目已启用 `LogAspectHandler`(默认启用),通常不需要额外使用 `BusinessLogAspect` * 适合在 `LogAspectHandler` 无法满足需求时作为补充手段使用 下一步 [#下一步] # 调试切面 (/docs/bamboo-base-java/framework/aspect/debug) import { TypeTable } from '@/components/type-table'; import { Callout } from 'fumadocs-ui/components/callout'; DebugAspectHandler [#debugaspecthandler] `DebugAspectHandler` 用于保护调试专用的 Controller 接口。当应用运行在非调试模式时,该切面将拦截对调试接口的访问并抛出 `BusinessException`,防止调试接口在生产环境中被意外暴露。 MVC vs WebFlux 对比 [#mvc-vs-webflux-对比] | 特性 | MVC 版本 | WebFlux 版本 | | ------ | -------------------- | ------------------------- | | 请求对象 | `HttpServletRequest` | `ServerWebExchange` | | 权限检查方式 | 仅同步 | 同步 + 响应式 | | 管道集成 | 不适用 | `debugCheckTransformer()` | 核心方法 [#核心方法] checkDebugController [#checkdebugcontroller] 同步方式检查调试接口的访问权限: ```java title="DebugAspectHandler.java" @Aspect @Component public class DebugAspectHandler { // [!code highlight:3] // 检查当前是否为调试模式 public void checkDebugController() { if (!isDebugMode()) { // [!code highlight:2] // 非调试模式下抛出未授权异常 throw new BusinessException("调试接口不可用", ErrorCode.UNAUTHORIZED); } } } ``` checkDebugControllerReactive(仅 WebFlux) [#checkdebugcontrollerreactive仅-webflux] 响应式方式检查调试接口的访问权限: ```java title="DebugAspectHandler.java" // [!code highlight:3] // 响应式检查调试接口权限 // 接受 ServerWebExchange 参数以获取请求信息 public void checkDebugControllerReactive(ServerWebExchange exchange) { // 从 exchange 中获取请求信息进行权限校验 } ``` debugCheckTransformer(仅 WebFlux) [#debugchecktransformer仅-webflux] 提供响应式流转换器,用于在 `Mono` / `Flux` 管道中插入调试权限检查逻辑: ```java title="DebugAspectHandler.java" // [!code highlight:3] // 返回响应式转换器,可嵌入 Mono/Flux 管道 public Function, Mono> debugCheckTransformer() { return mono -> mono.transformDeferredContextual((m, ctx) -> { // 在响应式管道中执行调试权限检查 return m; }); } ``` **使用示例:** ```java title="DebugController.java" @RestController @RequestMapping("/debug") public class DebugController { private final DebugAspectHandler debugAspect; @GetMapping("/info") public Mono>> getDebugInfo() { // [!code highlight:3] // 在响应式管道中使用 debugCheckTransformer 进行权限检查 return Mono.just(buildDebugInfo()) .transform(debugAspect.debugCheckTransformer()) .flatMap(info -> ResultUtil.success("调试信息", info)); } } ``` 使用方式 [#使用方式] 标记调试接口 [#标记调试接口] 在需要保护的调试 Controller 中调用 `checkDebugController()` 方法: ```java title="DebugController.java" @RestController @RequestMapping("/debug") @RequiredArgsConstructor public class DebugController { private final DebugAspectHandler debugAspectHandler; @GetMapping("/cache/clear") public ResponseEntity> clearCache() { // [!code highlight:2] // 非调试模式下将返回 401 Unauthorized debugAspectHandler.checkDebugController(); cacheService.clearAll(); return ResultUtil.success("缓存已清除"); } @GetMapping("/config/reload") public ResponseEntity> reloadConfig() { // [!code highlight] debugAspectHandler.checkDebugController(); configService.reload(); return ResultUtil.success("配置已重载"); } } ``` 非调试模式响应 [#非调试模式响应] 在生产环境中访问调试接口时,返回如下响应: ```json title="response.json" { "context": "550e8400-e29b-41d4-a716-446655440000", "output": "UNAUTHORIZED", "code": 40101, "message": null, "errorMessage": "调试接口不可用", "duration": 1, "data": null } ``` HTTP 状态码为 `401`。 调试模式配置 [#调试模式配置] 调试模式通常通过 Spring Profile 或应用属性控制: ```yaml title="application.yml" # 开发环境 spring: profiles: active: dev # 调试模式开关 bamboo: debug: enabled: true ``` 自动注册 [#自动注册] `DebugAspectHandler` 由 `BaseSdkAutoConfiguration` 自动注册。 `DebugAspectHandler` 由 `WebFluxSdkAutoConfiguration` 自动注册: ```java title="WebFluxSdkAutoConfiguration.java" @Bean public DebugAspectHandler debugAspectHandler() { return new DebugAspectHandler(); } ``` 注意事项 [#注意事项] * 调试接口应始终添加 `checkDebugController()` 保护,避免生产环境暴露 * 该切面由自动配置注册,开发者无需手动声明 Bean * 建议将所有调试接口集中在统一的 URL 前缀下(如 `/debug/**`),便于管理 下一步 [#下一步] # 切面概览 (/docs/bamboo-base-java/framework/aspect) import { Callout } from 'fumadocs-ui/components/callout'; 切面概览 [#切面概览] bamboo-base-java 提供基于 Spring AOP 的切面处理器,用于处理日志记录、调试保护等横切关注点。 切面列表 [#切面列表] | 切面 | 职责 | 自动注册 | | -------------------- | -------------------- | ---- | | `LogAspectHandler` | 日志切面,记录请求参数、返回值和执行耗时 | 是 | | `DebugAspectHandler` | 调试切面,控制调试接口的访问权限 | 是 | MVC vs WebFlux 对比 [#mvc-vs-webflux-对比] | 对比项 | MVC 版本 | WebFlux 版本 | | ------ | --------------------------------- | ----------------- | | 日志切点 | Controller + Service + Repository | 仅 Controller | | 响应式支持 | 无 | 支持 Mono/Flux 返回类型 | | 调试检查方式 | 仅同步 | 同步 + 响应式 | LogAspectHandler [#logaspecthandler] 日志切面拦截 Controller、Service 和 Repository 三层的方法调用,记录请求参数、返回值和执行耗时。 切点定义 [#切点定义] | 注解 | 说明 | 日志级别 | | --------------------------------- | ----------- | ----- | | `@Controller` / `@RestController` | HTTP 请求处理方法 | INFO | | `@Service` | 业务逻辑方法 | DEBUG | | `@Repository` | 数据访问方法 | INFO | 日志输出示例 [#日志输出示例] **Controller 层:** ``` [INFO] [HTTP] >> GET /api/v1/user/1 [INFO] [HTTP] >> Headers: {Authorization: Bearer ***} [INFO] [HTTP] >> Parameters: {id: 1} [INFO] [HTTP] << 200 OK (15ms) ``` **Service 层:** ``` [DEBUG] [SERVICE] UserService.getUserById(id=1) [DEBUG] [SERVICE] UserService.getUserById -> UserVO{id=1, username='test'} ``` **Repository 层:** ``` [INFO] [DAO] UserMapper.selectById(1) -> completed in 3ms ``` DebugAspectHandler [#debugaspecthandler] 调试切面用于保护调试专用的 Controller 接口。当应用运行在非调试模式时,该切面将拦截对调试接口的访问并抛出异常。 使用方式 [#使用方式] ```java title="DebugController.java" @RestController @RequestMapping("/debug") @RequiredArgsConstructor public class DebugController { private final DebugAspectHandler debugAspectHandler; @GetMapping("/cache/clear") public ResponseEntity> clearCache() { // [!code highlight:2] // 非调试模式下将返回 401 Unauthorized debugAspectHandler.checkDebugController(); cacheService.clearAll(); return ResultUtil.success("缓存已清除"); } } ``` 调试模式配置 [#调试模式配置] ```yaml title="application.yml" # 开发环境 spring: profiles: active: dev # 调试模式开关 bamboo: debug: enabled: true ``` 下一步 [#下一步] # 日志切面 (/docs/bamboo-base-java/framework/aspect/log) import { TypeTable } from '@/components/type-table'; import { Callout } from 'fumadocs-ui/components/callout'; LogAspectHandler [#logaspecthandler] `LogAspectHandler` 是基于 Spring AOP 的日志切面处理器,通过 `@Around` 切点自动拦截方法调用,记录请求参数、返回值和执行耗时。 MVC vs WebFlux 对比 [#mvc-vs-webflux-对比] | 对比项 | MVC 版本 | WebFlux 版本 | | ----- | --------------------------------- | ----------------------------- | | 切点范围 | Controller + Service + Repository | 仅 Controller | | 响应式支持 | 无 | 支持 Mono/Flux 返回类型 | | 自动配置类 | `BaseSdkAutoConfiguration` | `WebFluxSdkAutoConfiguration` | 切点定义 [#切点定义] 切面结构 [#切面结构] ```java title="LogAspectHandler.java" // [!code highlight:2] @Aspect @Component public class LogAspectHandler { // [!code highlight:3] // Controller 层 — 记录 HTTP 请求完整信息 @Around("@within(org.springframework.stereotype.Controller) || " + "@within(org.springframework.web.bind.annotation.RestController)") public Object aroundController(ProceedingJoinPoint joinPoint) throws Throwable { // 记录请求方法、路径、参数 // 执行目标方法 // 记录响应状态和耗时 } // [!code highlight:3] // Service 层 — DEBUG 级别调用链路记录 @Around("@within(org.springframework.stereotype.Service)") public Object aroundService(ProceedingJoinPoint joinPoint) throws Throwable { // 以 DEBUG 级别记录方法调用 // 执行目标方法 } // [!code highlight:3] // Repository 层 — DAO 执行耗时记录 @Around("@within(org.springframework.stereotype.Repository)") public Object aroundRepository(ProceedingJoinPoint joinPoint) throws Throwable { // 记录 DAO 方法执行耗时 // 支持 @IgnoreOutputDAO 跳过输出 } } ``` WebFlux 版本能够正确处理 `Mono` 与 `Flux` 响应式返回类型: ```java title="LogAspectHandler.java" // [!code highlight:2] // 响应式日志切面,由自动配置注册 @Aspect public class LogAspectHandler { // [!code highlight:2] // 匹配所有 @RestController 标注类的所有方法 @Around("@within(org.springframework.web.bind.annotation.RestController)") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { // ... } } ``` 响应式返回类型处理 [#响应式返回类型处理] ```java title="LogAspectHandler.java" // [!code highlight:6] // 对于 Mono 返回类型,使用 doOnSuccess / doOnError 记录日志 if (result instanceof Mono mono) { return mono .doOnSuccess(value -> logResponse(joinPoint, value)) .doOnError(error -> logError(joinPoint, error)); } // [!code highlight:5] // 对于 Flux 返回类型,使用 doOnComplete / doOnError 记录日志 if (result instanceof Flux flux) { return flux .doOnComplete(() -> logComplete(joinPoint)) .doOnError(error -> logError(joinPoint, error)); } ``` ServerWebExchange 提取 [#serverwebexchange-提取] ```java title="LogAspectHandler.java" // [!code highlight:5] // 遍历方法参数,查找 ServerWebExchange 实例 ServerWebExchange exchange = null; for (Object arg : joinPoint.getArgs()) { if (arg instanceof ServerWebExchange) { exchange = (ServerWebExchange) arg; break; } } ``` Controller 层日志 [#controller-层日志] Controller 层切面记录完整的 HTTP 请求信息: ``` [INFO] [HTTP] >> GET /api/v1/user/1 [INFO] [HTTP] >> Headers: {Authorization: Bearer ***} [INFO] [HTTP] >> Parameters: {id: 1} [INFO] [HTTP] << 200 OK (15ms) ``` 日志内容包括: * 请求方法(GET、POST 等) * 请求路径 * 请求头(敏感信息自动脱敏) * 请求参数 * 响应状态码 * 处理耗时 Service 层日志(仅 MVC) [#service-层日志仅-mvc] Service 层使用 DEBUG 级别记录,仅在开发和调试阶段可见: ``` [DEBUG] [SERVICE] UserService.getUserById(id=1) [DEBUG] [SERVICE] UserService.getUserById -> UserVO{id=1, username='test'} ``` Repository 层日志(仅 MVC) [#repository-层日志仅-mvc] Repository 层记录 DAO 方法的执行耗时: ``` [INFO] [DAO] UserMapper.selectById(1) -> completed in 3ms ``` @IgnoreOutputDAO [#ignoreoutputdao] 对于返回大量数据的 DAO 方法,可使用 `@IgnoreOutputDAO` 注解跳过返回值日志输出: ```java title="UserMapper.java" public interface UserMapper extends BaseMapper { // [!code highlight:3] // 该方法的返回值不会被日志切面输出 @IgnoreOutputDAO List selectAllUsers(); // 普通方法仍会输出返回值 UserEntity selectById(Long id); } ``` 日志输出内容(WebFlux) [#日志输出内容webflux] | 字段 | 说明 | | ------ | ------------------------- | | 请求路径 | HTTP 请求的 URI | | 请求方法 | GET、POST、PUT、DELETE 等 | | 客户端 IP | 从 `ServerWebExchange` 中提取 | | 方法签名 | Controller 类名与方法名 | | 执行耗时 | 方法执行时间(毫秒) | | 响应状态 | 成功或异常 | 注意事项 [#注意事项] * Controller 层日志始终以 INFO 级别输出,确保生产环境可见 * Service 层日志以 DEBUG 级别输出,生产环境默认不可见 * Repository 层支持 `@IgnoreOutputDAO` 注解,避免大数据量返回值污染日志 * 该切面由自动配置注册,无需手动声明 Bean 下一步 [#下一步] # 自定义异常处理 (/docs/bamboo-base-java/framework/exception/custom-system) 自定义异常处理 [#自定义异常处理] `bamboo-mvc` 的异常处理架构基于继承设计,开发者只需继承 `SystemExceptionHandler` 即可获得完整的内置异常处理能力,同时添加项目特有的异常处理逻辑。 基本用法 [#基本用法] 创建自定义异常处理器 [#创建自定义异常处理器] ```java title="GlobalExceptionHandler.java" package com.example.demo.exception; import com.xlf.utility.BaseResponse; import com.xlf.utility.ErrorCode; import com.xlf.utility.mvc.ResultUtil; import com.xlf.utility.mvc.exception.SystemExceptionHandler; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; // [!code highlight:3] // 继承 SystemExceptionHandler 获得完整的异常处理链 @RestControllerAdvice public class GlobalExceptionHandler extends SystemExceptionHandler { // [!code highlight:3] // 添加项目自定义异常处理 @ExceptionHandler(OrderNotFoundException.class) public ResponseEntity> handleOrderNotFound( OrderNotFoundException e) { return ResultUtil.error( ErrorCode.PAGE_NOT_FOUND, e.getMessage(), null ); } @ExceptionHandler(InsufficientBalanceException.class) public ResponseEntity> handleInsufficientBalance( InsufficientBalanceException e) { return ResultUtil.error( ErrorCode.OPERATION_FAILED, "余额不足: " + e.getMessage(), null ); } } ``` 定义自定义异常 [#定义自定义异常] ```java title="OrderNotFoundException.java" package com.example.demo.exception; public class OrderNotFoundException extends RuntimeException { public OrderNotFoundException(String orderId) { super("订单不存在: " + orderId); } } ``` ```java title="InsufficientBalanceException.java" package com.example.demo.exception; public class InsufficientBalanceException extends RuntimeException { public InsufficientBalanceException(String message) { super(message); } } ``` 携带数据的异常响应 [#携带数据的异常响应] 当异常响应需要附带额外信息时,可以使用 `ResultUtil.error()` 的泛型重载: ```java title="GlobalExceptionHandler.java" @ExceptionHandler(ValidationException.class) public ResponseEntity>> handleValidation( ValidationException e) { // [!code highlight:3] // 将校验失败的字段信息作为响应数据返回 Map errors = e.getFieldErrors(); return ResultUtil.error( ErrorCode.PARAMETER_ERROR, "参数校验失败", errors ); } ``` 覆盖内置处理器 [#覆盖内置处理器] 若需要修改内置异常的处理行为,可以在子类中覆盖父类的处理方法: ```java title="GlobalExceptionHandler.java" @RestControllerAdvice public class GlobalExceptionHandler extends SystemExceptionHandler { // [!code highlight:3] // 覆盖父类的 NullPointerException 处理,添加日志记录 @Override @ExceptionHandler(NullPointerException.class) public ResponseEntity> handleNullPointerException( NullPointerException e) { log.error("空指针异常发生", e); return ResultUtil.error( ErrorCode.SERVER_INTERNAL_ERROR, "系统内部错误,请稍后重试", null ); } } ``` 结合数据库异常处理器 [#结合数据库异常处理器] 若项目使用 MySQL 或 PostgreSQL,可以同时引入对应的数据库异常处理器。数据库异常处理器独立于主继承链,通过 Bean 注册方式引入: ```java title="GlobalExceptionHandler.java" @RestControllerAdvice public class GlobalExceptionHandler extends SystemExceptionHandler { // 项目自定义异常处理... } ``` 数据库异常处理器由自动配置注册,无需在自定义处理器中声明。 异常处理优先级 [#异常处理优先级] Spring MVC 在匹配 `@ExceptionHandler` 时遵循以下优先级规则: 1. **精确匹配**:优先匹配异常类型完全一致的处理器 2. **子类优先**:子类处理器优先于父类处理器 3. **就近原则**:`@RestControllerAdvice` 中定义的处理器优先于全局默认 因此,自定义异常处理器中声明的 `@ExceptionHandler` 方法会优先于 `SystemExceptionHandler` 中的同类型处理器。 下一步 [#下一步] # 异常处理概览 (/docs/bamboo-base-java/framework/exception) import { Callout } from 'fumadocs-ui/components/callout'; 异常处理概览 [#异常处理概览] bamboo-base-java 提供分层式异常处理架构,通过继承链实现从底层 Java 异常到上层业务异常的全面捕获。所有异常处理器均标注 `@RestControllerAdvice`,返回符合 `BaseResponse` 规范的标准化响应。 Java 基础异常、MySQL/PostgreSQL 异常、业务异常等内容已归纳到 [核心库异常处理文档](/docs/bamboo-base-java/base/exception),MVC 和 WebFlux 共用。 继承层级 [#继承层级] ``` JavaBaseExceptionHandler ← Java 基础异常(IOException, NullPointerException 等) └── PublicExceptionHandler ← 公共业务异常(BusinessException, CheckFailureException 等) └── SystemExceptionHandler ← Spring 框架异常(MethodArgumentNotValidException 等) ``` 每一层处理器继承上一层的全部异常处理能力,并在此基础上扩展新的异常类型。项目中只需继承最顶层的 `SystemExceptionHandler` 即可获得完整的异常处理能力。 MVC vs WebFlux 对比 [#mvc-vs-webflux-对比] | 对比项 | MVC 版本 | WebFlux 版本 | | ----- | --------------------------------- | --------------------------------------- | | 返回类型 | `ResponseEntity>` | `Mono>>` | | 特有处理器 | `UserAuthExceptionHandler` | 无 | | 自动配置类 | `BaseSdkAutoConfiguration` | `WebFluxSdkAutoConfiguration` | 响应式返回类型差异 [#响应式返回类型差异] WebFlux 的所有异常处理方法均返回 `Mono>>` 类型: ```java title="ExceptionHandler.java" // bamboo-mvc @ExceptionHandler(BusinessException.class) public ResponseEntity> handleBusinessException(BusinessException e) { return ResultUtil.error(e.getErrorCode(), e.getMessage(), null); } // [!code highlight:4] // bamboo-webflux(响应式版本) @ExceptionHandler(BusinessException.class) public Mono>> handleBusinessException(BusinessException e) { return ResultUtil.error(e.getErrorCode(), e.getMessage(), null); } ``` 扩展方式 [#扩展方式] 在项目中创建自定义异常处理器,继承 `SystemExceptionHandler` 即可获得全部内置异常处理能力: ```java title="GlobalExceptionHandler.java" // [!code highlight:2] // 继承 SystemExceptionHandler 获得完整的异常处理链 @RestControllerAdvice public class GlobalExceptionHandler extends SystemExceptionHandler { // [!code highlight:2] // 添加项目特有的异常处理 @ExceptionHandler(CustomBusinessException.class) public ResponseEntity> handleCustom(CustomBusinessException e) { return ResultUtil.error(ErrorCode.OPERATION_FAILED, e.getMessage(), null); } } ``` ```java title="GlobalExceptionHandler.java" @RestControllerAdvice public class GlobalExceptionHandler extends SystemExceptionHandler { @ExceptionHandler(CustomException.class) public Mono>> handleCustom(CustomException e) { return ResultUtil.error(ErrorCode.SERVER_INTERNAL_ERROR, e.getMessage(), null); } } ``` 框架特有处理器 [#框架特有处理器] | 处理器 | 框架 | 职责 | | -------------------------- | ----- | ---------------------------------------- | | `SystemExceptionHandler` | 两者 | Spring 框架异常(MethodNotAllowedException 等) | | `UserAuthExceptionHandler` | 仅 MVC | 用户认证异常(UserAuthenticationException) | 下一步 [#下一步] # Spring MVC 异常(仅 MVC) (/docs/bamboo-base-java/framework/exception/spring-boot) import { TypeTable } from '@/components/type-table'; SystemExceptionHandler [#systemexceptionhandler] `SystemExceptionHandler` 是异常处理继承链的最顶层,继承自 `PublicExceptionHandler`,负责处理 Spring MVC 框架在请求处理过程中抛出的异常。项目中自定义异常处理器应继承此类。 继承关系 [#继承关系] ``` JavaBaseExceptionHandler └── PublicExceptionHandler └── SystemExceptionHandler ← 当前层级(推荐继承) ``` 处理的异常类型 [#处理的异常类型] 处理器结构 [#处理器结构] ```java title="SystemExceptionHandler.java" // [!code highlight:2] @RestControllerAdvice public class SystemExceptionHandler extends PublicExceptionHandler { // [!code highlight:3] // 请求方法不支持 @ExceptionHandler(HttpRequestMethodNotSupportedException.class) public ResponseEntity> handleMethodNotSupported( HttpRequestMethodNotSupportedException e) { return ResultUtil.error( ErrorCode.REQUEST_METHOD_NOT_ALLOWED, e.getMessage(), null ); } // [!code highlight:3] // 参数校验失败(@Valid) @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity> handleValidationException( MethodArgumentNotValidException e) { return ResultUtil.error( ErrorCode.PARAMETER_ERROR, e.getMessage(), null ); } // [!code highlight:3] // 请求体无法解析 @ExceptionHandler(HttpMessageNotReadableException.class) public ResponseEntity> handleNotReadable( HttpMessageNotReadableException e) { return ResultUtil.error( ErrorCode.PARAMETER_ERROR, e.getMessage(), null ); } // ... 其他异常处理方法 } ``` 典型场景 [#典型场景] 参数校验失败 [#参数校验失败] 当使用 `@Valid` 进行参数校验时: ```java title="UserController.java" @PostMapping("/register") public ResponseEntity> register( // [!code highlight:2] // @Valid 触发 Bean Validation @Valid @RequestBody RegisterDTO dto) { userService.register(dto); return ResultUtil.success("注册成功"); } ``` ```java title="RegisterDTO.java" public record RegisterDTO( @NotBlank(message = "用户名不能为空") String username, @Email(message = "邮箱格式不正确") String email, @Size(min = 8, message = "密码长度不得少于 8 位") String password ) {} ``` 校验失败时自动返回: ```json title="response.json" { "context": "550e8400-e29b-41d4-a716-446655440000", "output": "PARAMETER_ERROR", "code": 40000, "message": null, "errorMessage": "用户名不能为空", "duration": 5, "data": null } ``` 请求方法不匹配 [#请求方法不匹配] 当 POST 接口收到 GET 请求时: ```json title="response.json" { "context": "550e8400-e29b-41d4-a716-446655440000", "output": "REQUEST_METHOD_NOT_ALLOWED", "code": 40500, "message": null, "errorMessage": "Request method 'GET' is not supported", "duration": 2, "data": null } ``` 资源未找到 [#资源未找到] 当请求的 URL 不存在时: ```json title="response.json" { "context": "550e8400-e29b-41d4-a716-446655440000", "output": "PAGE_NOT_FOUND", "code": 40403, "message": null, "errorMessage": "No static resource found", "duration": 1, "data": null } ``` 下一步 [#下一步] # 用户认证异常(仅 MVC) (/docs/bamboo-base-java/framework/exception/user-auth) import { TypeTable } from '@/components/type-table'; UserAuthenticationException [#userauthenticationexception] `UserAuthenticationException` 是专门用于用户认证与授权场景的异常类,继承自 `RuntimeException`。该异常携带 `ErrorType` 枚举标识具体的认证错误类型,并可选择性地附带 `UserInfo` 记录类提供用户上下文信息。 ErrorType 枚举 [#errortype-枚举] `ErrorType` 定义了所有可能的认证错误类型: UserInfo 记录类 [#userinfo-记录类] `UserInfo` 是一个内嵌的记录类,用于在异常中携带触发认证错误的用户信息: ```java title="UserAuthenticationException.java" // [!code highlight:2] // 记录类,不可变且自动生成 equals/hashCode/toString public record UserInfo( String userId, String username, String email ) {} ``` 异常结构 [#异常结构] ```java title="UserAuthenticationException.java" public class UserAuthenticationException extends RuntimeException { // [!code highlight:2] private final ErrorType errorType; private final UserInfo userInfo; public UserAuthenticationException(ErrorType errorType, String message) { super(message); this.errorType = errorType; this.userInfo = null; } public UserAuthenticationException( ErrorType errorType, String message, UserInfo userInfo) { super(message); this.errorType = errorType; this.userInfo = userInfo; } // getter 方法... } ``` 使用示例 [#使用示例] 抛出认证异常 [#抛出认证异常] ```java title="AuthService.java" @Service public class AuthService { public TokenVO login(String username, String password) { UserEntity user = userMapper.findByUsername(username); if (user == null) { // [!code highlight:3] // 用户不存在 throw new UserAuthenticationException( ErrorType.USER_NOT_EXIST, "用户不存在" ); } if (user.isBanned()) { // [!code highlight:4] // 用户已封禁,附带用户信息 throw new UserAuthenticationException( ErrorType.USER_BANNED, "账户已被封禁", new UserInfo(user.getId(), user.getUsername(), user.getEmail()) ); } if (!passwordEncoder.matches(password, user.getPassword())) { // [!code highlight:3] throw new UserAuthenticationException( ErrorType.WRONG_PASSWORD, "密码错误" ); } return generateToken(user); } } ``` 令牌校验 [#令牌校验] ```java title="TokenFilter.java" public class TokenFilter { public void validateToken(String token) { if (token == null || token.isEmpty()) { // [!code highlight:3] throw new UserAuthenticationException( ErrorType.USER_NOT_LOGIN, "请先登录" ); } if (tokenService.isExpired(token)) { // [!code highlight:3] throw new UserAuthenticationException( ErrorType.TOKEN_EXPIRED, "登录已过期,请重新登录" ); } } } ``` 权限校验 [#权限校验] ```java title="PermissionService.java" public void checkPermission(String userId, String resource) { if (!hasPermission(userId, resource)) { // [!code highlight:3] throw new UserAuthenticationException( ErrorType.PERMISSION_DENIED, "无权访问该资源" ); } } ``` 异常处理 [#异常处理] `UserAuthenticationException` 由异常处理器捕获并转换为标准响应: ```json title="response.json" { "context": "550e8400-e29b-41d4-a716-446655440000", "output": "UNAUTHORIZED", "code": 40101, "message": null, "errorMessage": "登录已过期,请重新登录", "duration": 3, "data": null } ``` HTTP 状态码为 `401`(40101 / 100)。 下一步 [#下一步] # 上下文过滤器 (/docs/bamboo-base-java/framework/filter/context) import { TypeTable } from '@/components/type-table'; import { Callout } from 'fumadocs-ui/components/callout'; ContextFilter [#contextfilter] `ContextFilter` 是优先级最高的过滤器(`@Order(HIGHEST_PRECEDENCE)`),负责在每个请求中注入链路追踪标识(UUID)与请求开始时间戳。这些信息将被 `ContextHolder`、`ResultUtil` 和日志切面等组件消费。 MVC vs WebFlux 对比 [#mvc-vs-webflux-对比] | 对比项 | MVC 版本 | WebFlux 版本 | | ----- | -------------------------- | ----------------------------- | | 接口 | `OncePerRequestFilter` | `WebFilter` | | 存储方式 | `ThreadLocal` | Reactor Context | | 请求对象 | `HttpServletRequest` | `ServerWebExchange` | | 清理方式 | `finally` 块手动清理 | Reactor 自动清理 | | 自动配置类 | `BaseSdkAutoConfiguration` | `WebFluxSdkAutoConfiguration` | 存储方式的本质区别 [#存储方式的本质区别] MVC 版本使用 `ThreadLocal` 存储上下文信息: ```java title="对比" // MVC 版本 - ThreadLocal public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { ThreadLocal contextId = new ThreadLocal<>(); contextId.set(UUID.randomUUID().toString()); chain.doFilter(request, response); contextId.remove(); } ``` WebFlux 版本使用 Reactor Context: ```java title="对比" // [!code highlight:6] // WebFlux 版本 - Reactor Context public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { String uuid = UUID.randomUUID().toString(); return chain.filter(exchange) .contextWrite(Context.of( ContextConstant.CONTEXT_KEY, uuid, ContextConstant.START_TIME_KEY, System.currentTimeMillis() )); } ``` 执行流程 [#执行流程] ``` HTTP 请求进入 │ ▼ 检查请求方法是否为 OPTIONS → 是 → 跳过,直接放行 │ 否 ▼ 检查 URL 是否在排除列表中 → 是 → 跳过,直接放行 │ 否 ▼ 检查 Handler 是否标注 @IgnoreContext(仅 MVC)→ 是 → 跳过 │ 否 ▼ 生成或提取 UUID │ ▼ 设置 MDC(日志追踪) │ ▼ 注入上下文(ThreadLocal / Reactor Context) │ ▼ 执行后续过滤器链和业务逻辑 │ ▼ 清理上下文(仅 MVC,WebFlux 自动处理) ``` 配置属性 [#配置属性] ```yaml title="application.yml" bamboo: context: enable-input: true exclude-urls: - /actuator/** - /health - /favicon.ico ``` ', default: '[]', }, }} /> WebFlux 版本额外支持以下配置: UUID 获取策略 [#uuid-获取策略] ``` enable-input = true ? │ ├── 是 → 检查请求头中是否存在 UUID │ │ │ ├── 存在 → 使用请求头中的 UUID │ └── 不存在 → 生成新的 UUID │ └── 否 → 始终生成新的 UUID ``` 当 `enable-input` 为 `true` 时,过滤器从请求头中提取 UUID;若请求头中无有效 UUID,则自动生成。这一机制适用于微服务场景中的链路追踪,上游服务可通过请求头传递上下文标识。 @IgnoreContext 注解(仅 MVC) [#ignorecontext-注解仅-mvc] 对于不需要上下文注入的 Handler 方法,可使用 `@IgnoreContext` 注解跳过: ```java title="HealthController.java" @RestController public class HealthController { // [!code highlight:3] // 该方法不会触发上下文初始化 @IgnoreContext @GetMapping("/health") public String health() { return "OK"; } } ``` 过滤器结构 [#过滤器结构] ```java title="ContextFilter.java" // [!code highlight:2] @Order(Ordered.HIGHEST_PRECEDENCE) public class ContextFilter extends OncePerRequestFilter { private final ContextProperties properties; private final HandlerMapping handlerMapping; @Override protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // [!code highlight:2] // 跳过 OPTIONS 请求 if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { filterChain.doFilter(request, response); return; } // [!code highlight:2] // 跳过排除的 URL if (isExcluded(request.getRequestURI())) { filterChain.doFilter(request, response); return; } // [!code highlight:2] // 检查 @IgnoreContext 注解 HandlerMethod handler = HttpServletUtil.getHandlerMethod( request, handlerMapping); if (handler != null && hasIgnoreContext(handler)) { filterChain.doFilter(request, response); return; } try { // [!code highlight:3] // 生成或提取 UUID String contextId = resolveContextId(request); MDC.put("contextId", contextId); ContextHolder.initContext(contextId); filterChain.doFilter(request, response); } finally { // [!code highlight:2] // 确保清理,防止线程池复用导致的上下文泄漏 ContextHolder.clear(); MDC.clear(); } } } ``` ```java title="ContextFilter.java" // [!code highlight:4] // 最高优先级的上下文注入过滤器 @Order(Ordered.HIGHEST_PRECEDENCE) public class ContextFilter implements WebFilter { @Override public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { // [!code highlight:5] // 检查请求头中是否携带外部 UUID String externalContextId = exchange.getRequest().getHeaders() .getFirst(ContextConstant.CONTEXT_KEY); String uuid = (enableInput && externalContextId != null) ? externalContextId : UUID.randomUUID().toString(); // [!code highlight:2] // 设置 MDC,使日志输出包含 contextId MDC.put("contextId", uuid); // [!code highlight:5] // 通过 contextWrite 注入 Reactor Context return chain.filter(exchange) .contextWrite(Context.of( ContextConstant.CONTEXT_KEY, uuid, ContextConstant.START_TIME_KEY, System.currentTimeMillis() )); } } ``` MDC 集成 [#mdc-集成] 过滤器将上下文标识写入 SLF4J 的 MDC,使日志框架自动携带追踪信息: ```xml title="logback-spring.xml" %d{yyyy-MM-dd HH:mm:ss} [%thread] [%X{contextId}] %-5level %logger - %msg%n ``` 日志输出示例: ``` 2026-01-15 10:00:00 [http-nio-8080-exec-1] [550e8400-e29b-41d4-a716-446655440000] INFO UserController - 处理用户请求 ``` 注意事项 [#注意事项] * 该过滤器以 `HIGHEST_PRECEDENCE` 注册,确保在所有其他过滤器之前执行 * MVC 版本:`finally` 块中的清理逻辑确保 ThreadLocal 和 MDC 不会泄漏至线程池中的其他请求 * WebFlux 版本:Reactor Context 随响应式流自动传播和清理 * 由自动配置类注册,无需手动声明 下一步 [#下一步] # CORS 跨域过滤器 (/docs/bamboo-base-java/framework/filter/cors) import { TypeTable } from '@/components/type-table'; import { Callout } from 'fumadocs-ui/components/callout'; CORS 过滤器 [#cors-过滤器] CORS(Cross-Origin Resource Sharing)过滤器通过校验请求的 `Host` 头部与预配置的域名白名单进行匹配,对授权域名设置 CORS 响应头。 MVC vs WebFlux 对比 [#mvc-vs-webflux-对比] | 特性 | MVC AllowCorsFilterHandler | WebFlux AllowCorsWebFilter | | ---- | -------------------------- | -------------------------- | | 接口 | `javax.servlet.Filter` | `WebFilter` | | 请求对象 | `HttpServletRequest` | `ServerWebExchange` | | 注册方式 | `FilterRegistrationBean` | `@Bean` 直接注册 | | 拒绝响应 | 不设置 CORS 头,直接放行 | 返回 `403 Forbidden` | | 异步支持 | 同步阻塞 | 返回 `Mono` | 工作流程 [#工作流程] ``` 请求进入 │ ▼ 提取 Host 请求头 │ ▼ 校验 Host 是否在 domains 白名单中 │ ├── 匹配 → 设置 CORS 响应头 → 放行 └── 不匹配 → 直接放行(MVC)或返回 403(WebFlux) ``` 设置的响应头 [#设置的响应头] 当 Host 校验通过时,过滤器设置以下 CORS 响应头: | 响应头 | 值 | 说明 | | ---------------------------------- | ---------------- | ----------- | | `Access-Control-Allow-Origin` | 请求的 Origin 或 `*` | 允许的请求来源 | | `Access-Control-Allow-Methods` | 配置的方法列表 | 允许的 HTTP 方法 | | `Access-Control-Allow-Headers` | 配置的请求头列表 | 允许的请求头 | | `Access-Control-Allow-Credentials` | `true` | 允许携带凭证 | 构造参数 [#构造参数] 注册示例 [#注册示例] ```java title="FilterConfig.java" @Configuration public class FilterConfig { @Bean public FilterRegistrationBean corsFilter() { FilterRegistrationBean registration = new FilterRegistrationBean<>(); // [!code highlight:5] registration.setFilter(new AllowCorsFilterHandler( new String[]{"example.com", "api.example.com", "localhost"}, new String[]{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, new String[]{"Authorization", "Content-Type", "X-Requested-With"} )); registration.addUrlPatterns("/*"); registration.setOrder(1); return registration; } } ``` ```java title="FilterConfig.java" @Configuration public class FilterConfig { @Bean public AllowCorsWebFilter corsWebFilter() { return new AllowCorsWebFilter( new String[]{"example.com", "api.example.com", "localhost"} ); } } ``` WebFlux 版本的过滤器实现: ```java title="AllowCorsWebFilter.java" @Override public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { String host = exchange.getRequest().getHeaders().getHost().getHostString(); // [!code highlight:4] // 校验域名是否在白名单中 if (!isAllowedDomain(host)) { exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN); return exchange.getResponse().setComplete(); } // 设置 CORS 响应头 ServerHttpResponse response = exchange.getResponse(); response.getHeaders().add("Access-Control-Allow-Origin", "*"); response.getHeaders().add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); response.getHeaders().add("Access-Control-Allow-Headers", "*"); return chain.filter(exchange); } ``` 与 OPTIONS 过滤器的配合 [#与-options-过滤器的配合] CORS 过滤器通常与 OPTIONS 过滤器配合使用: ``` 浏览器发送 OPTIONS 预检请求 │ ▼ AllowCorsFilter │ 设置 CORS 响应头 ▼ AllowOptionFilter │ 检测到 OPTIONS 方法 │ 直接返回 200 OK ▼ 响应返回浏览器(不进入 Controller) ``` > 建议将 CORS 过滤器的 `order` 设为 1,OPTIONS 过滤器设为 2,确保 CORS 响应头在 OPTIONS 快速响应前已设置。 注意事项 [#注意事项] * 域名校验基于请求的 `Host` 头,而非 `Origin` 头 * MVC 版本:若 Host 不在白名单中,过滤器不会阻止请求,仅不设置 CORS 响应头(浏览器将拒绝跨域响应) * WebFlux 版本:若 Host 不在白名单中,直接返回 `403 Forbidden` * 该过滤器需手动注册为 Bean,不在自动配置范围内 下一步 [#下一步] # 过滤器概览 (/docs/bamboo-base-java/framework/filter) import { Callout } from 'fumadocs-ui/components/callout'; 过滤器概览 [#过滤器概览] bamboo-base-java 提供一组 HTTP 请求预处理过滤器,覆盖上下文注入、跨域处理、安全校验和权限控制等场景。 MVC vs WebFlux 对比 [#mvc-vs-webflux-对比] | 对比项 | MVC 版本 | WebFlux 版本 | | ---- | ------------------------ | ------------------------- | | 接口 | `javax.servlet.Filter` | `WebFilter` | | 请求对象 | `HttpServletRequest` | `ServerWebExchange` | | 返回类型 | `void` | `Mono` | | 链路调用 | `FilterChain.doFilter()` | `WebFilterChain.filter()` | | 线程模型 | 阻塞式,一请求一线程 | 非阻塞式,事件循环 | 过滤器列表 [#过滤器列表] | 过滤器 | 优先级 | 职责 | 类型 | | ------------------- | -------------------- | -------------------------- | ------ | | `ContextFilter` | `HIGHEST_PRECEDENCE` | 请求上下文初始化,生成/提取 UUID,设置 MDC | 内置自动注册 | | `AllowCorsFilter` | 用户定义 | CORS 跨域请求处理 | 需手动配置 | | `AllowOptionFilter` | 用户定义 | OPTIONS 预检请求快速响应 | 需手动配置 | | `PermissionFilter` | 用户定义 | 权限校验(抽象类,需实现) | 需继承实现 | 过滤器执行顺序 [#过滤器执行顺序] ``` HTTP 请求 │ ▼ ContextFilter (HIGHEST_PRECEDENCE) │ 生成/提取上下文 UUID │ 设置 MDC 日志追踪 │ 初始化 ContextHolder ▼ AllowCorsFilter(可选) │ 校验 Origin/Host │ 设置 CORS 响应头 ▼ AllowOptionFilter(可选) │ OPTIONS 请求直接返回 200 ▼ PermissionFilter(可选) │ 检查 @NeedPermission 注解 │ 执行权限校验 ▼ Spring DispatcherServlet / WebFilter Chain │ ▼ Controller → Service → Repository ``` 注册方式 [#注册方式] 自动注册 [#自动注册] `ContextFilter` 由自动配置以最高优先级自动注册,无需任何配置。 手动注册 [#手动注册] 其他过滤器需要在配置类中手动注册为 `FilterRegistrationBean`: ```java title="FilterConfig.java" @Configuration public class FilterConfig { // [!code highlight:3] // 注册 CORS 过滤器 @Bean public FilterRegistrationBean corsFilter() { FilterRegistrationBean registration = new FilterRegistrationBean<>(); registration.setFilter(new AllowCorsFilterHandler( new String[]{"example.com", "api.example.com"}, new String[]{"GET", "POST", "PUT", "DELETE"}, new String[]{"Authorization", "Content-Type"} )); registration.addUrlPatterns("/*"); registration.setOrder(1); return registration; } } ``` WebFlux 过滤器直接注册为 `@Bean`: ```java title="FilterConfig.java" @Configuration public class FilterConfig { @Bean public AllowCorsWebFilter corsWebFilter() { return new AllowCorsWebFilter( new String[]{"example.com", "api.example.com"} ); } } ``` 配置属性 [#配置属性] 上下文过滤器支持通过 `application.yml` 配置: ```yaml title="application.yml" bamboo: context: enable-input: true exclude-urls: - /actuator/** - /health ``` MVC 与 WebFlux 过滤器对照 [#mvc-与-webflux-过滤器对照] | MVC 过滤器 | WebFlux 过滤器 | | ------------------------------------ | -------------------------------------- | | `ContextFilter` (implements Filter) | `ContextFilter` (implements WebFilter) | | `AllowCorsFilterHandler` | `AllowCorsWebFilter` | | `AllowOptionFilterHandler` | `AllowOptionWebFilter` | | `PermissionFilterHandler` (abstract) | `PermissionWebFilter` (abstract) | 两者在功能上完全对等,仅在接口实现与请求对象类型上有所不同。 下一步 [#下一步] # OPTIONS 过滤器 (/docs/bamboo-base-java/framework/filter/options) import { Callout } from 'fumadocs-ui/components/callout'; OPTIONS 过滤器 [#options-过滤器] OPTIONS 过滤器专门处理 HTTP OPTIONS 预检请求。当浏览器在发送跨域请求前发出 OPTIONS 预检请求时,该过滤器直接返回 `200 OK`,避免请求进入后续的 Controller 处理链。 MVC vs WebFlux 对比 [#mvc-vs-webflux-对比] | 对比项 | MVC 版本 | WebFlux 版本 | | -------- | -------------------------------------- | ---------------------------------------- | | 实现接口 | `Filter` | `WebFilter` | | 请求对象 | `ServletRequest` | `ServerWebExchange` | | 响应设置 | `HttpServletResponse.setStatus()` | `exchange.getResponse().setStatusCode()` | | 返回类型 | `void` | `Mono` | | 注册方式 | `FilterRegistrationBean` | `@Bean` 直接注册 | | Order 设置 | 通过 `FilterRegistrationBean.setOrder()` | `@Order(Ordered.HIGHEST_PRECEDENCE + 1)` | 工作原理 [#工作原理] ```java title="AllowOptionFilterHandler.java" public class AllowOptionFilterHandler implements Filter { @Override public void doFilter( ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; // [!code highlight:4] // 仅拦截 OPTIONS 方法,其他方法直接放行 if ("OPTIONS".equalsIgnoreCase(httpRequest.getMethod())) { HttpServletResponse httpResponse = (HttpServletResponse) response; httpResponse.setStatus(HttpServletResponse.SC_OK); return; } chain.doFilter(request, response); } } ``` ```java title="AllowOptionWebFilter.java" // [!code highlight:4] // OPTIONS 预检过滤器,优先级略低于 CORS 过滤器 @Order(Ordered.HIGHEST_PRECEDENCE + 1) public class AllowOptionWebFilter implements WebFilter { @Override public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { // [!code highlight:5] // 判断请求方法是否为 OPTIONS if (exchange.getRequest().getMethod() == HttpMethod.OPTIONS) { exchange.getResponse().setStatusCode(HttpStatus.OK); return exchange.getResponse().setComplete(); } // 非 OPTIONS 请求,继续传递至下一个过滤器 return chain.filter(exchange); } } ``` 执行流程 [#执行流程] ``` 浏览器跨域请求 │ ├── 预检请求 (OPTIONS /api/v1/user) │ │ │ ├── CORS 过滤器 → 设置 CORS 头 │ │ │ └── OPTIONS 过滤器 → 返回 200 OK(终止链路) │ └── 实际请求 (POST /api/v1/user) │ ├── CORS 过滤器 → 设置 CORS 头 │ ├── OPTIONS 过滤器 → 非 OPTIONS,跳过 │ └── 继续执行后续过滤器 → Controller ``` Order 说明 [#order-说明] WebFlux 版本的 Order 为 `HIGHEST_PRECEDENCE + 1`,比 CORS 过滤器(`HIGHEST_PRECEDENCE`)低一级。这确保了 CORS 响应头在 OPTIONS 请求返回之前已经被设置。 注册方式 [#注册方式] ```java title="FilterConfig.java" @Configuration public class FilterConfig { @Bean public FilterRegistrationBean optionFilter() { FilterRegistrationBean registration = new FilterRegistrationBean<>(); // [!code highlight] registration.setFilter(new AllowOptionFilterHandler()); registration.addUrlPatterns("/*"); registration.setOrder(2); return registration; } } ``` ```java title="FilterConfig.java" @Configuration public class FilterConfig { @Bean public AllowOptionWebFilter optionWebFilter() { return new AllowOptionWebFilter(); } } ``` 注意事项 [#注意事项] * 该过滤器仅处理 HTTP `OPTIONS` 方法,所有其他方法直接透传至后续过滤器链 * 响应不包含任何响应体,仅设置 HTTP 状态码 200 * 需配合 CORS 过滤器使用,否则浏览器无法获取 CORS 响应头 下一步 [#下一步] # 权限过滤器 (/docs/bamboo-base-java/framework/filter/permission) import { TypeTable } from '@/components/type-table'; import { Callout } from 'fumadocs-ui/components/callout'; 权限过滤器 [#权限过滤器] 权限过滤器是一个抽象过滤器类,提供基于 `@NeedPermission` 注解的权限校验框架。该过滤器在请求到达 Controller 之前检查目标方法是否标注了 `@NeedPermission` 注解,若标注则调用抽象方法执行实际的权限校验逻辑。 MVC vs WebFlux 对比 [#mvc-vs-webflux-对比] | 对比项 | MVC 版本 | WebFlux 版本 | | ---------- | ------------------------------------------------ | ----------------------------------------------- | | 实现接口 | `Filter` | `WebFilter` | | 类名 | `PermissionFilterHandler` | `PermissionWebFilter` | | 抽象方法 | `hasPermissionCheck(HttpServletRequest, String)` | `hasPermissionCheck(ServerWebExchange, String)` | | 返回类型 | `boolean` | `Mono` | | Handler 解析 | `HandlerMapping.getHandler()` | `ServerWebExchangeUtil.getHandlerMethod()` | 工作流程 [#工作流程] ``` HTTP 请求进入 │ ▼ 解析目标 Handler 方法 │ ▼ 检查是否标注 @NeedPermission │ ├── 未标注 → 直接放行 │ └── 已标注 → 调用 hasPermissionCheck() │ ├── 校验通过 → 放行 └── 校验失败 → 返回 403 Forbidden ``` @NeedPermission 注解 [#needpermission-注解] ```java title="NeedPermission.java" @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface NeedPermission { // [!code highlight:2] // 权限标识字符串 String value(); } ``` 抽象方法 [#抽象方法] ```java title="PermissionFilterHandler.java" // [!code highlight:3] // 子类必须实现此方法,完成实际的权限校验逻辑 protected abstract boolean hasPermissionCheck( HttpServletRequest request, String permission); ``` 返回 `true` 表示放行,返回 `false` 表示拒绝访问。 ```java title="PermissionWebFilter.java" // [!code highlight:3] // 抽象方法,由子类实现具体的权限校验逻辑 protected abstract Mono hasPermissionCheck( ServerWebExchange exchange, String permission); ``` 校验通过则正常完成 `Mono`,失败时应抛出异常或直接设置响应状态码。 实现示例 [#实现示例] ```java title="ApiPermissionFilter.java" public class ApiPermissionFilter extends PermissionFilterHandler { private final PermissionService permissionService; public ApiPermissionFilter( HandlerMapping handlerMapping, PermissionService permissionService) { super(handlerMapping); this.permissionService = permissionService; } // [!code highlight:3] @Override protected boolean hasPermissionCheck( HttpServletRequest request, String permission) { // 从请求头中获取用户身份 String token = request.getHeader("Authorization"); if (token == null) { return false; } // [!code highlight:2] // 委托给权限服务校验 String userId = tokenService.getUserId(token); return permissionService.hasPermission(userId, permission); } } ``` 注册过滤器: ```java title="FilterConfig.java" @Configuration @RequiredArgsConstructor public class FilterConfig { private final HandlerMapping handlerMapping; private final PermissionService permissionService; @Bean public FilterRegistrationBean permissionFilter() { FilterRegistrationBean registration = new FilterRegistrationBean<>(); // [!code highlight:2] registration.setFilter( new ApiPermissionFilter(handlerMapping, permissionService)); registration.addUrlPatterns("/api/*"); registration.setOrder(10); return registration; } } ``` ```java title="CustomPermissionFilter.java" // [!code highlight:2] // 继承 PermissionWebFilter 并实现 hasPermissionCheck 方法 @Component public class CustomPermissionFilter extends PermissionWebFilter { private final PermissionService permissionService; public CustomPermissionFilter(PermissionService permissionService, RequestMappingHandlerMapping handlerMapping) { super(handlerMapping); this.permissionService = permissionService; } @Override // [!code highlight:3] // 实现具体的权限校验逻辑 // 返回 Mono,校验通过则正常完成,失败则抛出异常 protected Mono hasPermissionCheck(ServerWebExchange exchange, String permission) { String token = exchange.getRequest().getHeaders().getFirst("Authorization"); return permissionService.checkPermission(token, permission) .flatMap(hasPermission -> { if (!hasPermission) { exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN); return exchange.getResponse().setComplete(); } return Mono.empty(); }); } } ``` 使用注解标记接口 [#使用注解标记接口] 在 Controller 方法上标注 `@NeedPermission` 注解,声明该接口所需的权限: ```java title="AdminController.java" @RestController @RequestMapping("/api/v1/admin") public class AdminController { // [!code highlight:2] // 需要 "admin:user:manage" 权限 @NeedPermission("admin:user:manage") @GetMapping("/users") public ResponseEntity>> listUsers() { return ResultUtil.success("查询成功", userService.listAll()); } // [!code highlight:2] // 需要 "admin:config:edit" 权限 @NeedPermission("admin:config:edit") @PutMapping("/config") public ResponseEntity> updateConfig( @RequestBody ConfigDTO dto) { configService.update(dto); return ResultUtil.success("更新成功"); } // 未标注 @NeedPermission,无需权限校验 @GetMapping("/public/info") public ResponseEntity> getPublicInfo() { return ResultUtil.success("查询成功", infoService.getPublic()); } } ``` 注意事项 [#注意事项] * 权限过滤器是抽象类,不能直接使用,必须实现抽象方法 * 过滤器需要 `HandlerMapping` 实例来解析请求对应的 Handler 方法及其注解 * 权限校验在 Filter 层执行,早于 Controller 的 AOP 切面 * 建议将权限标识设计为层级格式(如 `module:resource:action`),便于管理 * 该过滤器不由自动配置注册,需开发者自行将实现类注册为 Spring Bean 下一步 [#下一步] # Referer 拦截过滤器 (/docs/bamboo-base-java/framework/filter/referer-ban) BanEmptyReferer [#banemptyreferer] `BanEmptyReferer` 过滤器用于拦截未携带 `Referer` 请求头的 HTTP 请求。该过滤器主要用于防止 API 被非浏览器客户端(如爬虫、自动化脚本)直接调用,提供基础的请求来源校验能力。 工作原理 [#工作原理] ```java title="BanEmptyReferer.java" public class BanEmptyReferer implements Filter { @Override public void doFilter( ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; String referer = httpRequest.getHeader("Referer"); // [!code highlight:5] // 若 Referer 为空,返回 403 JSON 响应 if (referer == null || referer.isEmpty()) { HttpServletResponse httpResponse = (HttpServletResponse) response; httpResponse.setStatus(HttpServletResponse.SC_FORBIDDEN); httpResponse.setContentType("application/json;charset=UTF-8"); // 写入错误 JSON 响应体 return; } chain.doFilter(request, response); } } ``` 响应格式 [#响应格式] 当请求未携带 `Referer` 时,返回 HTTP 403 和 JSON 格式的错误信息: ```json title="response.json" { "context": null, "output": "FORBIDDEN", "code": 40300, "message": null, "errorMessage": "请求来源不合法", "duration": 0, "data": null } ``` 注册方式 [#注册方式] ```java title="FilterConfig.java" @Configuration public class FilterConfig { @Bean public FilterRegistrationBean refererFilter() { FilterRegistrationBean registration = new FilterRegistrationBean<>(); // [!code highlight] registration.setFilter(new BanEmptyReferer()); // [!code highlight:2] // 仅对 API 路径生效 registration.addUrlPatterns("/api/*"); registration.setOrder(3); return registration; } } ``` 适用场景 [#适用场景] * **Web 前端 API**:前端页面发出的请求通常携带 `Referer`,而直接调用 API 的工具不会 * **防爬虫**:简单的反爬策略,阻止未设置 `Referer` 的请求 * **内部 API 保护**:配合其他安全机制使用 注意事项 [#注意事项] * `Referer` 头可被伪造,该过滤器仅提供基础防护,不应作为唯一的安全手段 * 部分合法场景(如浏览器隐私设置、HTTPS 到 HTTP 的跳转)可能不携带 `Referer` * 建议仅在特定路径模式上注册(如 `/api/*`),避免影响静态资源和页面访问 * 该过滤器需手动注册为 Bean,不在自动配置范围内 下一步 [#下一步] # HeaderUtil (/docs/bamboo-base-java/framework/util/header-util) import { TypeTable } from '@/components/type-table'; HeaderUtil [#headerutil] `HeaderUtil` 是一个静态工具类,封装了 HTTP 请求头的常用提取和校验操作。所有方法均接收 `HttpServletRequest` 参数,返回对应请求头的解析结果。 方法列表 [#方法列表] 授权用户标识 [#授权用户标识] Referer [#referer] User-Agent [#user-agent] Host 与 Accept [#host-与-accept] 使用示例 [#使用示例] 获取授权用户 [#获取授权用户] ```java title="UserController.java" @GetMapping("/profile") public ResponseEntity> getProfile( HttpServletRequest request) { // [!code highlight:2] // 从请求头中提取用户 UUID UUID userId = HeaderUtil.getAuthorizeUserUuid(request); UserVO user = userService.getById(userId); return ResultUtil.success("查询成功", user); } ``` 获取字符串格式的用户标识 [#获取字符串格式的用户标识] ```java title="LogService.java" public void recordUserAction(HttpServletRequest request, String action) { // [!code highlight:2] // 使用字符串格式,避免 UUID 解析开销 String userId = HeaderUtil.getAuthorizeUserUuidString(request); log.info("用户 {} 执行操作: {}", userId, action); } ``` Referer 校验 [#referer-校验] ```java title="SecurityService.java" public void validateReferer(HttpServletRequest request) { // [!code highlight:2] if (!HeaderUtil.hasReferer(request)) { throw new BusinessException("缺少 Referer", ErrorCode.FORBIDDEN); } String referer = HeaderUtil.getReferer(request); if (!isAllowedReferer(referer)) { throw new BusinessException("非法来源", ErrorCode.FORBIDDEN); } } ``` User-Agent 检测 [#user-agent-检测] ```java title="AnalyticsService.java" public void logClientInfo(HttpServletRequest request) { // [!code highlight:3] if (HeaderUtil.hasUserAgent(request)) { String ua = HeaderUtil.getUserAgent(request); log.info("客户端: {}", ua); } } ``` 注意事项 [#注意事项] * 所有方法均为静态方法,无需实例化 * `getAuthorizeUserUuid()` 在解析失败时可能抛出异常,建议在调用前确认请求头存在 * `has*()` 系列方法适合在条件判断中使用,避免空指针异常 下一步 [#下一步] # HttpServletUtil (/docs/bamboo-base-java/framework/util/http-servlet-util) import { TypeTable } from '@/components/type-table'; HttpServletUtil [#httpservletutil] `HttpServletUtil` 是一个静态工具类,提供从 `HttpServletRequest` 解析 Spring MVC `HandlerMethod` 的能力。该工具类主要在过滤器层使用,用于在请求到达 Controller 之前获取目标处理方法的元信息(如注解、参数类型等)。 方法详解 [#方法详解] 参数说明 [#参数说明] 返回值 [#返回值] * 若请求 URL 匹配到有效的 Controller 方法,返回对应的 `HandlerMethod` 实例 * 若无法匹配(如请求静态资源或 URL 不存在),返回 `null` 使用场景 [#使用场景] 在过滤器中检查注解 [#在过滤器中检查注解] `HttpServletUtil` 的典型使用场景是在过滤器层判断目标方法是否标注了特定注解: ```java title="CustomFilter.java" public class CustomFilter extends OncePerRequestFilter { private final HandlerMapping handlerMapping; @Override protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // [!code highlight:3] // 解析目标 Handler 方法 HandlerMethod handlerMethod = HttpServletUtil.getHandlerMethod(request, handlerMapping); if (handlerMethod != null) { // [!code highlight:3] // 检查方法上是否标注了 @NeedPermission NeedPermission annotation = handlerMethod.getMethodAnnotation(NeedPermission.class); if (annotation != null) { // 执行权限校验逻辑... } } filterChain.doFilter(request, response); } } ``` 在 ContextFilter 中使用 [#在-contextfilter-中使用] `ContextFilter` 使用此工具检查 `@IgnoreContext` 注解: ```java title="ContextFilter.java" // [!code highlight:2] HandlerMethod handler = HttpServletUtil.getHandlerMethod(request, handlerMapping); if (handler != null) { // [!code highlight:2] // 检查是否标注 @IgnoreContext IgnoreContext ignore = handler.getMethodAnnotation(IgnoreContext.class); if (ignore != null) { filterChain.doFilter(request, response); return; } } ``` 获取方法元信息 [#获取方法元信息] ```java title="AuditFilter.java" HandlerMethod handlerMethod = HttpServletUtil.getHandlerMethod(request, handlerMapping); if (handlerMethod != null) { // [!code highlight:2] // 获取 Controller 类名和方法名 String className = handlerMethod.getBeanType().getSimpleName(); String methodName = handlerMethod.getMethod().getName(); log.info("请求目标: {}.{}", className, methodName); // [!code highlight:2] // 获取方法参数类型 Class[] parameterTypes = handlerMethod.getMethod().getParameterTypes(); } ``` 注意事项 [#注意事项] * 该方法可能返回 `null`,调用方必须进行空值判断 * `HandlerMapping` 实例可通过 Spring 注入获取 * 在高频调用场景中,`getHandlerMethod()` 的性能开销主要来自 `HandlerMapping` 的内部查找过程,Spring 内部已做缓存优化 * 该工具类仅适用于 Servlet 环境(Spring MVC),不适用于响应式环境(Spring WebFlux) 下一步 [#下一步] # 工具类概览 (/docs/bamboo-base-java/framework/util) 工具类概览 [#工具类概览] bamboo-base-java 提供一组 HTTP 请求工具类,用于在过滤器层和业务代码中获取请求信息。 工具类列表 [#工具类列表] | 工具类 | 框架 | 说明 | | ----------------------- | --------- | ---------------------------------- | | `HttpServletUtil` | 仅 MVC | Servlet 请求工具,提供 HandlerMethod 解析能力 | | `HeaderUtil` | 仅 MVC | HTTP 请求头工具,提供 Token、User-Agent 等提取 | | `ServerWebExchangeUtil` | 仅 WebFlux | 响应式请求工具,提供 Handler 解析与请求信息提取 | MVC vs WebFlux 对比 [#mvc-vs-webflux-对比] | 功能 | MVC 工具 | WebFlux 工具 | | ---------- | ------------------------------------ | ------------------------------------------ | | Handler 解析 | `HttpServletUtil.getHandlerMethod()` | `ServerWebExchangeUtil.getHandlerMethod()` | | 客户端 IP | `request.getRemoteAddr()` | `ServerWebExchangeUtil.getClientIp()` | | User-Agent | `request.getHeader()` | `ServerWebExchangeUtil.getUserAgent()` | | Token 提取 | `HeaderUtil.getAuthorizeUserUuid()` | 自行从 exchange 提取 | 使用场景 [#使用场景] 过滤器层 [#过滤器层] 在过滤器中检查目标方法的注解: ```java title="CustomFilter.java" // MVC HandlerMethod handler = HttpServletUtil.getHandlerMethod(request, handlerMapping); NeedPermission annotation = handler.getMethodAnnotation(NeedPermission.class); // WebFlux return ServerWebExchangeUtil.getHandlerMethod(exchange, handlerMapping) .flatMap(handler -> { NeedPermission annotation = handler.getMethodAnnotation(NeedPermission.class); // ... }); ``` 业务代码 [#业务代码] 在业务代码中提取请求信息: ```java title="AuditService.java" // MVC UUID userId = HeaderUtil.getAuthorizeUserUuid(request); String clientIp = request.getRemoteAddr(); // WebFlux String clientIp = ServerWebExchangeUtil.getClientIp(exchange); String userAgent = ServerWebExchangeUtil.getUserAgent(exchange); ``` 下一步 [#下一步] # ServerWebExchangeUtil (/docs/bamboo-base-java/framework/util/server-web-exchange-util) ServerWebExchangeUtil [#serverwebexchangeutil] `ServerWebExchangeUtil` 是 `bamboo-webflux` 模块提供的工具类,封装了从 `ServerWebExchange` 中提取常用信息的方法,包括 Handler Method 解析、客户端 IP 获取和 User-Agent 提取。 方法说明 [#方法说明] getHandlerMethod [#gethandlermethod] 根据当前请求解析出对应的 `HandlerMethod` 对象。该方法主要供 `PermissionWebFilter` 等内部组件使用,用于获取目标 Controller 方法上的注解信息。 ```java title="ServerWebExchangeUtil.java" // [!code highlight:4] // 通过 RequestMappingHandlerMapping 解析目标 HandlerMethod // 返回 Mono,若无法解析则返回空 Mono public static Mono getHandlerMethod( ServerWebExchange exchange, RequestMappingHandlerMapping handlerMapping) ``` **使用示例:** ```java title="CustomFilter.java" public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { return ServerWebExchangeUtil.getHandlerMethod(exchange, handlerMapping) .flatMap(handlerMethod -> { // [!code highlight:2] // 获取目标方法上的自定义注解 MyAnnotation annotation = handlerMethod.getMethodAnnotation(MyAnnotation.class); if (annotation != null) { // 执行自定义逻辑 } return chain.filter(exchange); }) .switchIfEmpty(chain.filter(exchange)); } ``` getClientIp [#getclientip] 从 `ServerWebExchange` 中提取客户端的真实 IP 地址。该方法会依次检查常见的代理头部(`X-Forwarded-For`、`X-Real-IP` 等),获取最终客户端 IP。 ```java title="ServerWebExchangeUtil.java" // [!code highlight:3] // 提取客户端真实 IP,支持代理头部解析 public static String getClientIp(ServerWebExchange exchange) ``` IP 提取的优先级顺序: 1. `X-Forwarded-For` 头部(取第一个非空值) 2. `X-Real-IP` 头部 3. `exchange.getRequest().getRemoteAddress()` 直接获取 **使用示例:** ```java title="AuditService.java" public Mono logAccess(ServerWebExchange exchange) { // [!code highlight:2] // 获取客户端 IP 用于审计日志 String clientIp = ServerWebExchangeUtil.getClientIp(exchange); log.info("访问来源 IP: {}", clientIp); return Mono.empty(); } ``` getUserAgent [#getuseragent] 从 `ServerWebExchange` 中提取客户端的 User-Agent 信息。 ```java title="ServerWebExchangeUtil.java" // [!code highlight:2] // 提取请求头中的 User-Agent 值 public static String getUserAgent(ServerWebExchange exchange) ``` **使用示例:** ```java title="SecurityService.java" public Mono checkDevice(ServerWebExchange exchange) { String userAgent = ServerWebExchangeUtil.getUserAgent(exchange); // [!code highlight:2] // 根据 User-Agent 判断设备类型 boolean isMobile = userAgent.contains("Mobile"); return Mono.just(isMobile); } ``` 与 MVC 版本的对应关系 [#与-mvc-版本的对应关系] 在 `bamboo-mvc` 中,类似的功能通常直接通过 `HttpServletRequest` 的方法实现。`ServerWebExchangeUtil` 提供了对等的能力: | MVC 方式 | WebFlux 工具方法 | | ------------------------------------ | ----------------------------------------------------------- | | `request.getRemoteAddr()` | `ServerWebExchangeUtil.getClientIp(exchange)` | | `request.getHeader("User-Agent")` | `ServerWebExchangeUtil.getUserAgent(exchange)` | | `HandlerMapping.getHandler(request)` | `ServerWebExchangeUtil.getHandlerMethod(exchange, mapping)` | 下一步 [#下一步]