# 核心说明 (/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)` |
下一步 [#下一步]