README
¶
gocrux
gocrux 是一个 Go 语言通用 CRUD 后端框架,提供泛型化的 Handler → Service → Repository 三层架构,支持级联操作、版本管理和审计日志。
go get github.com/Huey1979/gocrux
目录
- 架构概述
- 快速开始
- 实体定义
- Service 层配置
- Handler 层配置
- 展开深度控制
- 忽略控制
- 自关联展开
- List 字段裁剪
- Entity → DTO 响应映射
- 路由注册
- 钩子系统
- 输入校验
- 级联机制
- 版本管理
- 身份认证与授权
- 幂等支持
- 操作日志与备份
- 运行时日志(Request/Response/Business)
- 列表查询条件
- 配置文件
- 代码生成器 gentity
- 项目结构
架构概述
HTTP 请求
│
▼
GenericHandler[M] ← HandlerConfig(路由前缀、级联关系、认证、权限)
│ before → do → after ← HandlerHooks(可覆盖任意环节)
▼
GenericService[M] ← Config(版本模式、唯一性校验、操作日志)
│ before → do → after ← Hooks(可覆盖任意环节)
▼
CRUDRepository[M] ← 泛型 GORM 仓储(自动推导主键)
│
▼
MySQL / MongoDB / Redis
核心设计原则:
- 泛型化:每种实体类型 = 一个
GenericHandler[M]→GenericService[M]→CRUDRepository[M]链条,编译时类型安全 - 管线模式:每个操作(Create/Update/Delete/Get/List/Activate/EditVersion)都遵循 before → do → after 三段管线
- 钩子覆盖:任意环节的钩子函数均可被外部替换,未替换时 fallback 到内置默认实现
- 事务透明:Handler 层通过
TxCoordinator编排事务,Service 层通过common.GetTx(ctx)自动感知事务上下文 - HTTP 与业务解耦:所有业务场景统一返回 HTTP 200,业务结果(成功/参数错误/数据不存在/内部错误)通过响应体中的
code字段区分,绝不使用 HTTP 状态码表达业务语义
快速开始
1. 定义实体
package entity
import "time"
type Site struct {
SiteULID string `gorm:"column:site_ulid;primaryKey;size:26" json:"site_ulid"`
SiteCode string `gorm:"column:site_code;size:64" json:"site_code"`
SiteName string `gorm:"column:site_name;size:128" json:"site_name"`
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
IsDeleted int8 `gorm:"column:is_deleted" json:"is_deleted"`
}
// 必须实现 service.Record 接口
func (s *Site) SetDefaults() {}
func (s *Site) SetID() { s.SiteULID = common.NewULID() }
func (s *Site) SetCreatedAt(t time.Time) { s.CreatedAt = t }
func (s *Site) SetCreatedBy(userID string) {}
func (s *Site) SetUpdatedAt(t time.Time) { s.UpdatedAt = t }
func (s *Site) SetUpdatedBy(userID string) {}
func (s *Site) SupportsDraft() bool { return false }
func (s *Site) SetDelete() bool { s.IsDeleted = 1; return true }
func (s *Site) PKField() string { return "site_ulid" }
2. 定义请求类型
type CreateSiteRequest struct {
SiteCode string `json:"site_code"`
SiteName string `json:"site_name"`
}
func (r *CreateSiteRequest) GetID() any { return nil }
func (r *CreateSiteRequest) Validate() error { return nil }
func (r *CreateSiteRequest) MergeTo(target any) error {
s := target.(*entity.Site)
s.SiteCode = r.SiteCode
s.SiteName = r.SiteName
return nil
}
3. 组装并注册路由
package main
import (
"github.com/Huey1979/gocrux/handler"
"github.com/Huey1979/gocrux/service"
"github.com/Huey1979/gocrux/repository"
"github.com/gin-gonic/gin"
)
func main() {
// 创建仓储
repo := repository.NewCRUDRepository[entity.Site]()
// 创建 Service
svc := service.NewGenericService(repo, service.Config[entity.Site]{
EntityName: "site",
EnableOpLog: true,
EnableUniqueValidation: true,
UniqueFields: [][]string{{"site_code"}},
})
// 创建 Handler
h := handler.NewGenericHandlerWithSvc(svc, handler.HandlerConfig[entity.Site]{
PathPrefix: "/api/v1/sites",
})
// 注册路由
r := gin.Default()
h.RegisterRoutes(r.Group("/api/v1/sites"))
r.Run(":8080")
}
注册后将自动创建以下路由:
| 方法 | 路径 | 说明 |
|---|---|---|
POST |
/{prefix}/create |
批量创建 |
GET |
/{prefix}/list |
列表查询(分页+过滤) |
GET |
/{prefix}/get |
详情查询(按 ID/Code) |
POST |
/{prefix}/update |
更新记录 |
POST |
/{prefix}/delete |
批量删除 |
当 Service 启用 VersionMode 时,额外注册:
| 方法 | 路径 | 说明 |
|---|---|---|
POST |
/{prefix}/activate |
激活版本(发布/回滚) |
GET |
/{prefix}/versions |
版本历史列表 |
POST |
/{prefix}/edit-version |
修改版本元数据 |
实体定义
Record 接口
所有实体必须实现 service.Record 接口:
type Record interface {
SetDefaults() // 设置默认值
SetID() // 自动生成主键(ULID/自增)
SetCreatedAt(t time.Time) // 设置创建时间
SetCreatedBy(userID string) // 设置创建人
SetUpdatedAt(t time.Time) // 设置更新时间
SetUpdatedBy(userID string) // 设置更新人
SupportsDraft() bool // 是否支持草稿箱(版本管理)
SetDelete() bool // 软删除标记(返回 true=软删, false=物理删)
PKField() string // 主键数据库列名
}
软删除
SetDelete() bool返回true:实体有is_deleted字段,删除时执行UPDATE SET is_deleted=1- 返回
false:物理删除,先写备份日志再DELETE
SupportsDraft
- 返回
true时实体需提供VersionStatus字段(通过VersionFieldMapping映射) - 版本化模式下,Update 创建新草稿;Activate 发布草稿为正式版本
Service 层配置
service.Config[M]
type Config[M Record] struct {
EnableUniqueValidation bool // 启用唯一性校验
EnableOpLog bool // 自动记录操作日志
EntityName string // 实体中文名(用于日志)
VersionMode bool // 启用版本管理模式
VersionFields *VersionFieldMapping // 版本字段映射
UniqueFields [][]string // 唯一字段组
}
配置项详解
EnableUniqueValidation — 启用后,Create 和 Update 时自动校验 UniqueFields 中声明的字段组是否已有重复记录。支持联合唯一索引。
UniqueFields: [][]string{
{"Mobile"}, // mobile 单独唯一
{"DeptID", "RoleID"}, // dept_id + role_id 联合唯一
}
EnableOpLog — 启用后,Create/Update/Delete/Activate 完成时自动向 sys_operation_log 表写入操作日志。需注入 opLogRepo:
svc.SetOpLogRepo(repository.NewCRUDRepository[entity.SysOperationLog]())
EntityName — 日志中 EntityType 字段的值,建议使用英文表名(如 "site", "role")。
版本管理模式
VersionMode — 启用后 Update 不原地修改,而是:旧行 is_current=0 → 插入新行(is_current=1)
VersionFieldMapping — 启用 VersionMode 时必须配置:
type VersionFieldMapping struct {
ULIDField string // ULID 字段,如 "SiteULID"
CodeField string // 业务编码字段,如 "SiteCode"
VersionField string // 版本号字段,如 "VersionCode"
CurrentField string // 当前标记字段,如 "IsCurrent"
StatusField string // 版本状态字段,如 "VersionStatus"
ParentField string // 父版本字段,如 "ParentULID"
RemarkField string // 版本说明字段,如 "VersionRemark"
PublishedAtField string // 发布时间字段,如 "PublishedAt"
PublishedByField string // 发布人字段,如 "PublishedBy"
}
版本状态流转:
draft ──Activate──→ published ──(新版本发布)──→ deprecated
│ │
└──EditVersion──→ abolished ←──EditVersion─────────┘
(直接废弃) │ (废弃版本恢复为草稿)
└──EditVersion──→ draft
Handler 层配置
handler.HandlerConfig[M]
type HandlerConfig[M service.Record] struct {
PathPrefix string // 路由前缀
Cascades []CascadeRelation // 向下级联(父→子)
References []ReferenceRelation // 向上引用(子→父)
ChildRefs []ChildRefRelation // 向下 FK 列表引用
ReqFactory *RequestFactory[M] // 请求构造器
Auth Authenticator // 认证钩子
Perm Authorizer // 权限钩子
MaxExpandDepth int // 全局最大递归展开深度(>0 启用,默认 0=不递归)
FieldDepthLimits map[string]int // 单字段深度上限(如 {"dept_ulid": 1})
FieldStopRules map[string][]StopRule // 字段级截止规则(如 dept_ulid→-department:manager)
ResponseMapper func(M) any // Entity→DTO 响应映射(可选,仅 HTTP 出口生效)
}
PathPrefix
路由前缀,如 /api/v1/sites。框架自动注册标准 CRUD 路由。
RequestFactory
为 Create/Update/List 操作分别指定请求类型构造器。配置后 Handler 会将 HTTP body 反序列化为具体类型并调用其 Validate() 方法进行字段级校验。
ReqFactory: &handler.RequestFactory[entity.Site]{
Create: func() service.CrudRequest[entity.Site] { return &CreateSiteRequest{} },
Update: func() service.CrudRequest[entity.Site] { return &UpdateSiteRequest{} },
List: func() any { return &ListSiteRequest{} },
},
未配置时 fallback 到内置 MapRequest,无 schema 校验但兼容任意 JSON。
MapRequest 默认行为
若未配置 ReqFactory,HTTP body 会被绑定为 map[string]any,自动适配:
GetID()按优先级查找id→ulid→ID→ULIDGetIdempotencyKey()从idempotency_key字段提取幂等键MergeTo()通过 JSON 序列化/反序列化完成 map→struct 映射Validate()始终通过(无 schema 校验)
References(向上引用)
配置当前实体中指向父实体的逻辑外键字段,Get/List 时自动解析。
References: []handler.ReferenceRelation{
{
Field: "site_ulid", // 当前实体的 FK 字段
HandlerName: "site", // 父 Handler 的注册名
ResultField: "site", // 结果键名(空则自动推导:去掉 _ulid)
},
}
Get 场景:单次查父实体;List 场景:收集所有 FK 值 → 批量 DoList 展开。
ChildRefs(向下 FK 列表引用)
配置当前实体通过 FK 列表(如 tag_ulids: [1,2,3])引用的子实体,Get/List 时批量展开。
ChildRefs: []handler.ChildRefRelation{
{
FKListField: "tag_ulids", // FK 列表字段名
HandlerName: "tag", // 目标 Handler 注册名
ResultField: "tags", // 结果键名(空则自动推导:去掉 _ulids 加 s)
},
}
注意:ChildRefs 仅关联已有实体,不参与级联创建/删除/更新。
展开深度控制
当配置了 References、ChildRefs 或 Cascades 后,Get/List 会自动展开关联数据。框架提供三层深度控制:
1. 全局最大深度 (MaxExpandDepth)
MaxExpandDepth: 3, // References/ChildRefs/Cascades 递归展开最多 3 层
设置为 0 时只展开一层(不递归)。HTTP 可临时降级:
GET /api/v1/sites/get?id=xxx&depth=2 # 上限不可超过 MaxExpandDepth
2. 单字段深度上限 (FieldDepthLimits)
对特定字段单独限制展开深度:
FieldDepthLimits: map[string]int{
"dept_ulid": 1, // dept 字段只展开 1 层(即平铺后不递归)
"site_ulid": 2, // site 字段最多展开 2 层
},
HTTP 参数(逗号分隔 字段:深度 对):
GET /api/v1/users/get?id=xxx&fdepth=dept_ulid:1,site_ulid:2
3. 字段级截止规则 (FieldStopRules)
控制某个字段展开到目标子 Handler 后,子 Handler 的哪些字段被截止(跳过不展开):
FieldStopRules: map[string][]handler.StopRule{
"dept_ulid": {
{OnHandler: "department", Field: "manager", Stop: true}, // -department:manager → 跳过
{OnHandler: "department", Field: "parent_id", Stop: false}, // department:parent_id → 展开一层后截止
},
},
HTTP compact 格式:
GET /api/v1/users/get?fstop=dept_ulid=-department:manager,department:parent_id
格式规则:前缀 - 表示 Stop:true(完全跳过),不带前缀表示 Stop:false(展开一层后截止)。多规则用逗号分隔,多字段使用多个 fstop 参数。
设计说明:
MaxExpandDepth、FieldDepthLimits、FieldStopRules是服务端默认配置,HTTP 参数depth/fdepth/fstop作为限缩性覆盖(只能降级不能放大),避免 URL 过长问题。
忽略控制
通过 HTTP query 按需跳过特定展开环节(不覆盖配置,仅做减法):
| 参数 | 作用 |
|---|---|
?ignore=ref |
跳过 References 展开 |
?ignore=cascade |
跳过 Cascades 展开 |
?ignore=all |
跳过所有展开 |
?ignoreRef=site_ulid |
跳过特定 References 字段 |
?ignoreCascade=domains |
跳过特定 Cascades 字段 |
自关联展开
当实体需要引用自身时(如部门表 parent_dept_ulid 指向同表的父部门),可通过配置 References 实现自关联展开。
示例:
// 1. 实体实现 SelfFKField(),声明自关联外键字段
func (d *SysDept) SelfFKField() string { return "parent_dept_ulid" }
// 2. 配置 References 指向自身
HandlerConfig[*entity.SysDept]{
PathPrefix: "/api/v1/dept",
MaxExpandDepth: 5, // 最多展开 5 层
References: []handler.ReferenceRelation{
{
Field: "parent_dept_ulid",
HandlerName: "dept", // 指向自身
ResultField: "parent",
},
},
}
Get 请求 /api/v1/dept/get?id=xxx 会递归展开父部门链,形成层级树:
{
"dept_name": "研发三组",
"parent": {
"dept_name": "研发部",
"parent": {
"dept_name": "技术中心",
"parent": null
}
}
}
循环防护(无需手动阻止自关联):
- 深度控制:
MaxExpandDepth限制最大递归层数,全局硬上限hardMaxExpandDepth=10,到 0 时自动停止 - visited 追踪(Get 场景):记录每条展开线上的
(HandlerName, RecordID),遇到已访问的记录立即终止该条展开线,防止 A→B→A 跨实体环或 A→A→A 自环
注意:级联写操作(OnCreate/OnDelete/OnUpdate)的
SelfFKField()仅用于读展开的循环防护,不影响写行为。
List 字段裁剪
注意:以下配置仅影响 List 接口(_doList 返回前执行),Get 接口始终返回全字段。
ListSkipFields — 黑名单模式(优先级高于 Keep)
从 List 响应中移除指定字段,常用于跳过较大的 JSON/Text 字段(如 form_config、entity_config),减少网络传输量。
HandlerConfig[entity.SysForm]{
PathPrefix: "/api/v1/sys-form",
ListSkipFields: []string{"form_config", "entity_config", "flow_config"},
}
ListKeepFields — 白名单模式(仅 Skip 为空时生效)
仅保留指定字段,所有未声明的字段从 List 响应中移除。
HandlerConfig[entity.SysForm]{
PathPrefix: "/api/v1/sys-form",
ListKeepFields: []string{"form_ulid", "form_code", "form_name", "form_type"},
}
优先级:ListSkipFields > ListKeepFields。两者同时配置时 hanya 生效 Skip,Keep 被忽略。均未配置时全字段返回(向后兼容)。
执行时机:所有级联展开(References/ChildRefs/Cascades)完成之后执行,级联数据不受裁剪影响。
Entity → DTO 响应映射
DB 实体通常携带存储层专属字段(is_deleted、password、is_current、parent_ulid 等),不应直接暴露给 API 消费者。ResponseMapper 在 HTTP 出口处将 Entity 转换为 DTO,裁剪敏感/冗余字段。
设计约束:
- 仅在 HTTP handler 出口执行(Get/List),管道(pipeline)和级联调用(DoGetByID/DoList)不执行映射
ResponseMapper == nil时零开销,完全向后兼容- 展开后的级联数据(References/ChildRefs/Cascades)自动从原始 map 合并回 DTO 输出
使用方式:
// 场景 1:不映射 — 完全兼容旧行为(默认)
gh := handler.NewGenericHandler[*entity.SysDept](svcReg, "sys_dept",
handler.HandlerConfig[*entity.SysDept]{
PathPrefix: "/api/v1/sys-dept",
// ResponseMapper: nil (默认)
})
// 场景 2:映射为 DTO(字段裁剪,如去掉 is_deleted、password 等)
gh := handler.NewGenericHandler[*entity.SysSite](svcReg, "sys_site",
handler.HandlerConfig[*entity.SysSite]{
PathPrefix: "/api/v1/sys-site",
ResponseMapper: func(s *entity.SysSite) any {
return s.ToDTO() // gentity 自动生成的结构体方法
},
})
// 场景 3:自定义映射(如 List 只返回概要字段)
gh := handler.NewGenericHandler[*entity.SysSite](svcReg, "sys_site",
handler.HandlerConfig[*entity.SysSite]{
PathPrefix: "/api/v1/sys-site",
ResponseMapper: func(s *entity.SysSite) any {
return &BriefSite{
Code: s.SiteCode,
Name: s.SiteName,
}
},
})
Get 流程:
Get() → injectDepth/injectIgnore/injectStop → create entityHolder(ctx)
→ getPipeline → _doGet(entity→holder) → expandGet
→ applyResponseMapper(entityHolder, expandedMap) ← 这里是映射点
→ Success()
List 流程:
List() → inject… → create entitiesHolder(ctx) → listPipeline
→ _doList(entities→holder) → 批量展开
→ for each entity: applyResponseMapper(entity, item) ← 这里是映射点
→ Success()
DTO 结构体生成(gentity):
gentity --dto --dto-exclude is_deleted,is_current,parent_ulid --all --out generated
参数说明:
| 参数 | 说明 | 默认值 |
|---|---|---|
--dto |
启用 DTO 生成 | false |
--dto-exclude |
全局排除字段列表,逗号分隔 | is_deleted,is_current,parent_ulid |
--dto-pkg |
DTO 输出包名/子目录 | dto |
注意:ResponseMapper 仅裁剪 Entity 自有字段,级联展开数据(References/ChildRefs/Cascades 的 ResultField/ChildrenField)不受 DTO 裁剪影响——它们由框架在映射后自动合并。
Cascades(向下级联)
详见 级联机制 章节。
Auth(认证)
详见 身份认证与授权 章节。
Perm(权限)
详见 身份认证与授权 章节。
路由注册
直接注册
h := handler.NewGenericHandlerWithSvc(svc, cfg)
h.RegisterRoutes(router.Group("/api/v1/sites"))
通过注册表注册(推荐,支持级联)
// 1. 创建注册表
svcReg := service.NewServiceRegistry()
handlerReg := handler.NewHandlerRegistry()
txCoord := handler.NewTxCoordinator(db) // *gorm.DB
// 2. 创建 Service 并注册
siteSvc := service.NewGenericService(siteRepo, siteCfg)
svcReg.Register("site", siteSvc)
// 3. 创建 Handler 并注册
siteHandler := handler.NewGenericHandler[entity.Site](svcReg, "site", handlerCfg)
siteHandler.SetHandlerReg(handlerReg)
siteHandler.SetTxCoord(txCoord)
handlerReg.Register("site", siteHandler)
// 4. 注册路由
siteHandler.RegisterRoutes(api.Group("/api/v1/sites"))
钩子系统
Handler 层钩子 (HandlerHooks[M])
每个 CRUD 操作对应 before / do / after 三个钩子,覆盖后完全接管对应环节。
h.SetHooks(handler.HandlerHooks[entity.Site]{
BeforeCreate: func(ctx context.Context, input []service.CrudRequest[entity.Site]) ([]service.CrudRequest[entity.Site], error) {
// 前置处理:校验、转换、补充字段等
return input, nil
},
DoCreate: func(ctx context.Context, input []service.CrudRequest[entity.Site]) ([]*entity.Site, error) {
// 自定义创建逻辑(替代默认的 svc.Create + 级联编排)
return nil, nil
},
AfterCreate: func(ctx context.Context, result []*entity.Site) ([]*entity.Site, error) {
// 后置处理:发送通知、更新缓存等
return result, nil
},
// 同样支持:BeforeUpdate/DoUpdate/AfterUpdate、
// BeforeDelete/DoDelete/AfterDelete、
// BeforeGet/DoGet/AfterGet、
// BeforeList/DoList/AfterList、
// BeforeActivate/DoActivate/AfterActivate、
// BeforeListVersions/DoListVersions/AfterListVersions、
// BeforeEditVersion/DoEditVersion/AfterEditVersion
})
重要:Handler 层的 before/after 钩子不依赖 *gin.Context,无论走 HTTP 入口还是级联入口,钩子都能正常工作。
Service 层钩子 (Hooks[M])
与 Handler 层对称,同样支持 before / do / after 三段钩子。
svc.SetHooks(service.Hooks[entity.Site]{
BeforeCreate: func(ctx context.Context, input []service.CrudRequest[entity.Site]) ([]*entity.Site, error) {
// 数据组装、默认值、唯一性校验等
return entities, nil
},
// ... 其他钩子
})
钩子覆盖策略
handler.hooks.XxxBefore != nil ? → 使用自定义钩子
: → fallback handler._beforeXxx()
│
handler._beforeXxx() 调用 svc.beforeXxx()
│
svc.hooks.BeforeXxx != nil ? → 使用自定义钩子
: → fallback svc._beforeXxx()
输入校验
框架在 Handler 层对 Create / Update / List 接口提供内置输入校验,无需显式配置即可获得基础的类型和长度保护。
核心特性
| 特性 | 说明 |
|---|---|
| 零配置 | 从 entity struct 的 gorm 标签自动推导规则 |
| 宽松类型 | 类型不匹配时优先尝试转换,而非直接报错 |
| 内置格式 | datetime / date / email / phone / url / ulid 等开箱即用 |
| 双层模型 | 框架校验(字段级)→ 业务校验(跨字段 / DB 唯一性) |
自动推导(零配置)
Handler 构造时自动从 entity struct 反射出字段类型规则:
| entity 字段类型 | 自动规则 |
|---|---|
string(gorm size:N) |
type=string, max_length=N |
string(*_ulid 后缀) |
type=string, max_length=26, format=ulid |
int/int8/.../int64 |
type=int |
float32/float64 |
type=float |
bool |
type=bool |
time.Time |
type=time |
gorm not null |
required=true(仅 Create) |
宽松类型转换
框架在类型不匹配时优先尝试转换,能转就不报错:
| 输入值 | 期望类型 | 结果 |
|---|---|---|
123(JSON 数字) |
string |
✅ → "123" |
"123"(字符串) |
int |
✅ → 123 |
"1" / 1 |
bool |
✅ → true |
0 |
bool |
✅ → false |
"abc" |
int |
❌ 无法转换才报错 |
"" |
datetime |
❌ 空字符串时间 → 报错 |
校验范围
| 接口 | 校验内容 |
|---|---|
| Create | body 中每个字段的类型转换 + 格式 + 必填 + 长度 |
| Update | body 中每个字段的类型转换 + 格式 + 长度(不强制必填) |
| List | 分页参数(page≥1, page_size∈[1,100], order_dir∈{asc|desc})+ 过滤字段 |
示例:无配置时的行为
type Site struct {
SiteULID string `gorm:"column:site_ulid;primaryKey;size:26" json:"site_ulid"`
SiteCode string `gorm:"column:site_code;size:64;not null" json:"site_code"`
SortOrder int `gorm:"column:sort_order" json:"sort_order"`
}
Create 时(JSON body):
{"site_code": 123, "sort_order": "5"}
→ 框架自动转换:site_code → "123",sort_order → 5,正常入库
List 时:
GET /api/v1/sites/list?page=a
→ 框架拦截:字段[page] 应为整数("a" 无法转为数字)
内置格式校验(format 字段)
无需手写正则的常见格式:
| 格式 | 说明 | 示例 |
|---|---|---|
datetime |
日期时间(支持多种格式) | 2024-01-01 10:00:00 |
date |
日期 | 2024-01-01 |
time |
时间 | 10:00:00 |
email |
邮箱 | user@example.com |
url |
URL | https://example.com |
phone |
手机号(中国大陆) | 13800138000 |
ulid |
26位 Crockford base32 | 01JXXXXX...(自动开启) |
使用场景:
# 防止前端传空字符串 "" 导致 MySQL datetime 插入失败
created_at:
format: datetime
// 代码方式
"email": {Format: "email"}
"phone": {Format: "phone"}
自定义规则(YAML 覆写)
通过 Validate 配置字段覆盖或增强自动推导规则。
代码方式:
handler.HandlerConfig[entity.SysSite]{
PathPrefix: "/api/v1/sys-site",
Validate: &handler.ValidateConfig{
Create: &handler.EndpointRules{
"site_code": {Required: true, MaxLength: handler.IntPtr(64)},
"site_type": {Enum: []string{"web", "app", "miniapp"}},
"domain": {Format: "url"},
},
List: &handler.EndpointRules{
"page_size": {Max: handler.Float64Ptr(200)},
},
},
}
YAML 文件方式(configs/validations.example.yaml):
validations:
sys_site:
create:
site_code:
required: true
max_length: 64
site_type:
enum: ["web", "app", "miniapp"]
domain:
format: url
list:
page_size:
max: 200
sys_user:
create:
email:
format: email
phone:
format: phone
加载:
vcMap, _ := handler.LoadValidationConfig("configs/validations.yaml")
siteHandler := handler.NewGenericHandler[*entity.SysSite](svcReg, "sys_site",
handler.HandlerConfig[*entity.SysSite]{
PathPrefix: "/api/v1/sys-site",
Validate: vcMap["sys_site"],
})
支持的校验规则
| 属性 | 类型 | 说明 | 适用操作 |
|---|---|---|---|
type |
string |
期望类型:string/int/float/bool/time |
全部 |
required |
bool |
是否必填(Create 由 gorm not null 自动推导) |
Create |
min / max |
float64 |
数值范围 | 全部 |
min_length / max_length |
int |
字符串长度 | 全部 |
enum |
[]string |
枚举值白名单 | 全部 |
pattern |
string |
正则表达式 | 全部 |
format |
string |
内置格式:datetime/date/time/email/url/phone/ulid |
全部 |
双层校验模型
框架校验与业务校验协同工作:
HTTP Body
│
├─ validateInput(raw, rules) ← ★ 框架校验(类型转换 + 格式 + 长度,自动+配置)
│ └─ 失败 → ErrReqValidation(Create)/ ErrUpdateReqValidation(Update)
│
├─ req.Validate() ← ★ 业务校验(ReqFactory 注入,可选)
│ └─ 失败 → 同上
│
└─ Service before hooks ← Service 层校验(唯一性等)
两层的分工:
- 框架校验:字段级别的类型、长度、范围、枚举 — 零配置即可用
- 业务校验:跨字段逻辑、数据库唯一性、状态迁移 — 按需覆盖
级联机制
级联是 gocrux 核心特性之一,通过 CascadeRelation 声明父子关系,框架自动在事务内编排父实体与子实体的联动操作。
CascadeRelation 配置
type CascadeRelation struct {
HandlerName string // 子 Handler 在 HandlerRegistry 中的注册名称
ChildrenField string // 请求体中子数据的字段名(如 "domains")
FKField string // 子表中的外键字段名(如 "site_ulid")
OnCreate bool // 创建父时级联创建子
OnDelete bool // 删除父前级联删除子
OnUpdate bool // 更新父时级联更新子
OnActivate bool // 激活父版本时级联激活子
OnEditVersion bool // 编辑父版本时级联编辑子
FollowPublished bool // 级联检索时是否返回正式发布版本
ChildrenWrapKey string // 子数据为标量数组时的包裹键名
}
级联创建示例
Cascades: []handler.CascadeRelation{
{
HandlerName: "domain",
ChildrenField: "domains", // HTTP body 中: {"domains": [{...}, {...}]}
FKField: "site_ulid", // 自动注入到每条子数据
OnCreate: true,
OnDelete: true,
},
}
HTTP 请求体:
[{
"site_code": "S001",
"site_name": "主站",
"domains": [
{"domain_name": "example.com"},
{"domain_name": "test.com"}
]
}]
框架自动:创建 Site → 将 site_ulid 注入 domains 中的每条数据 → 调用 domainHandler.DoCreate → 同一事务内完成。
级联更新策略
Update 时级联行为与子数据存在性相关:
| 场景 | 行为 |
|---|---|
| 有子数据 + 非版本化 + 已有旧子记录 | 先删旧子记录 → 全量替换为新子数据 |
| 有子数据 + 版本化 | 创建新版本子记录(保留旧版本关联) |
| 无子数据 + 已有旧子记录 | 回填旧子数据 → 更新 FK 指向新父实体 |
| 无子数据 + 无旧子记录 | 跳过 |
ChildrenWrapKey
当子数据不是完整对象而是标量数组时使用:
{
HandlerName: "tag",
ChildrenField: "tags",
FKField: "user_ulid",
ChildrenWrapKey: "tag_id", // [1,2,3] → [{"tag_id":1},{"tag_id":2},{"tag_id":3}]
OnCreate: true,
}
前端可直接传 "tags": [1, 2, 3],框架自动包裹。
TxCoordinator
事务编排器,Handler 层不直接接触 gorm.DB。
tc := handler.NewTxCoordinator(db) // *gorm.DB
// 注入到每个需要级联的 Handler
handler.SetTxCoord(tc)
// 内部通过 tc.Run(ctx, func(txCtx) {...}) 在事务中执行
// Service 通过 service.GetTx(ctx) / common.GetTx(ctx) 自动感知事务
HandlerRegistry
管理所有 Handler 实例的注册表,级联时通过名称查找子 Handler。
handlerReg := handler.NewHandlerRegistry()
handlerReg.Register("site", siteHandler)
handlerReg.Register("domain", domainHandler)
handlerReg.Register("tag", tagHandler)
// 注入到需要级联的 Handler
siteHandler.SetHandlerReg(handlerReg)
版本管理
启用条件
Config[entity.Site]{
VersionMode: true,
VersionFields: &service.VersionFieldMapping{
ULIDField: "SiteULID",
CodeField: "SiteCode",
VersionField: "VersionCode",
CurrentField: "IsCurrent",
StatusField: "VersionStatus",
ParentField: "ParentULID",
RemarkField: "VersionRemark",
PublishedAtField: "PublishedAt",
PublishedByField: "PublishedBy",
},
}
版本化 Update 流程
非原地修改,而是:
- 取出现有记录
- 深拷贝 → 合并请求字段 → 生成新 ULID + 新 VersionCode
- 事务中:旧行
is_current=0→ 插入新行is_current=1,新版本默认为draft
Activate(发布/回滚)
POST /api/v1/sites/activate
{"id": "01JXXX..."}
- draft/deprecated → published:发布/回滚
- 同 code 所有行退位 → 目标行
is_current=1 - 级联激活子记录(按
OnActivate配置)
ListVersions
GET /api/v1/sites/versions?code=S001
按 code 查所有版本,按版本号降序排列。
EditVersion
POST /api/v1/sites/edit-version
{"id": "01JXXX...", "patches": {"VersionStatus": "abolished"}}
状态迁移限制:
draft→abolished(直接废弃)deprecated→abolished(归档)abolished→draft(恢复为草稿)published禁止直接 abolished
FollowPublished 机制
Get/List/Cascade 查询时可能 FK 指向旧版本,followPublished 控制是否解析为正式发布版:
followPublished=false:返回 FK 精确指向的版本(如订单快照)followPublished=true:找到同 code 的version_status='published'版本
HTTP 入口:
GET /api/v1/sites/get?id=xxx&follow_published=true
GET /api/v1/sites/get?code=S001 # 直接按 code 查 published 版本
身份认证与授权
Authenticator(认证)
type Authenticator interface {
Middleware() gin.HandlerFunc
FromContext(c *gin.Context) (UserInfo, bool)
}
实现示例(JWT):
type JWTAuth struct{}
func (a *JWTAuth) Middleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
claims := parseJWT(token) // 使用者自定义
c.Set("user", handler.UserInfo{
ULID: claims.Sub,
Name: claims.Name,
})
c.Next()
}
}
func (a *JWTAuth) FromContext(c *gin.Context) (handler.UserInfo, bool) {
v, ok := c.Get("user")
if !ok { return handler.UserInfo{}, false }
return v.(handler.UserInfo), true
}
注入方式一:全局中间件
middleware.DefaultAuthenticator = &JWTAuth{}
router.Use(middleware.AuthMiddleware())
注入方式二:在 HandlerConfig 中指定
HandlerConfig[entity.Site]{
Auth: &JWTAuth{},
}
Authorizer(授权)
type Authorizer interface {
Check(info UserInfo, resource string, action string) bool
}
实现示例(RBAC):
type RBACAuthorizer struct{}
func (a *RBACAuthorizer) Check(info handler.UserInfo, resource, action string) bool {
roles := info.Extra["roles"].([]string)
return hasPermission(roles, resource, action)
}
注入后,每个操作执行前自动调用 Check:
| resource | action |
|---|---|
site |
create, update, delete, get, list |
role |
create, update, delete, get, list |
幂等支持
通过 IdempotencyStore 缓存创建结果,相同幂等键的重复请求直接返回缓存。
store := service.NewIdempotencyStore[entity.Site](time.Hour) // 1小时 TTL
svc.SetIdemStore(store)
HTTP 请求中传入 idempotency_key 字段即可:
{
"idempotency_key": "order-2024-001",
"site_code": "S001",
"site_name": "主站"
}
注意:内存缓存,服务重启后丢失。生产环境可替换为 Redis 实现。
操作日志与备份
操作日志表
启用 EnableOpLog 后自动写入 sys_operation_log 表:
svc.SetOpLogRepo(repository.NewCRUDRepository[entity.SysOperationLog]())
日志字段:log_ulid、entity_type、entity_id、operation、operator_ulid、request_id、operated_at。
备份写入器
非版本化 Update 时旧数据会被覆盖丢失,可通过 BakWriter 在更新前写备份日志文件:
svc.SetBakWriter(func(ctx context.Context, tableName string, recordID any, operation string, oldData any, requestID string) error {
// 写入文件或 MongoDB
return nil
})
运行时日志(Request/Response/Business)
框架内置一套独立的运行时追踪日志(internal/logger),与 config.yaml 中的 log 配置段是两套不同的日志系统:
| 系统 | 初始化方式 | 输出 | 用途 |
|---|---|---|---|
| 全局 logrus | config.yaml → log 段 |
stdout / file(由配置决定) | 应用级日志(启动、配置加载等) |
| 运行时追踪日志 | logger.Init(logDir) |
按天滚动文件 | 每个 HTTP 请求的完整链路追踪 |
三个独立实例,每次请求生成唯一 request_id 串联:
| 实例 | 文件 | 内容 |
|---|---|---|
RequestLog |
logs/request_YYYY-MM-DD.log |
URL、GET 参数、POST body |
ResponseLog |
logs/response_YYYY-MM-DD.log |
HTTP 状态码、返回体 |
BusinessLog |
logs/business_YYYY-MM-DD.log |
业务节点(如 internal_error) |
初始化:
import "github.com/Huey1979/gocrux/internal/logger"
logger.Init("./logs") // 可选,默认为 ./logs
安全特性:即使不调用 logger.Init(),日志实例也已内置默认值(输出到 stderr),不会因未初始化而 nil panic。调用 Init() 后切换为按天滚动文件模式。
InternalError 双重日志说明
当发生内部错误时,handler.InternalError 会同时写入两套日志:
// ① 全局 logrus(受 config.yaml log 段控制,即时可见)
logrus.WithFields(...).Error("内部错误")
// ② 运行时 BusinessLog(按天滚动,带 request_id 串联请求链路)
logger.LogBusiness(c, "internal_error", ...)
这不是重复记录,而是双通道保障:
- logrus 通道:遵循配置的格式和输出(stdout/JSON/text),便于运维实时监控和日志采集
- BusinessLog 通道:按天独立文件 +
request_id串联完整请求链路,便于事后排查
列表查询条件
HTTP 查询方式
GET /api/v1/sites/list?page=1&page_size=20&site_code=S001&keyword=xxx&order_by=created_at&order_dir=desc
URL query 参数自动转为 map[string]any 过滤条件。当值为切片时自动使用 OpIn。
结构化过滤(Repository 层)
type ListFilters struct {
Page int // 页码(>=1)
PageSize int // 每页条数(<=0 不分页)
Filters []Filter // 过滤条件
Logic string // "and"(默认)或 "or"
OrderBy string // 排序字段(DB 列名)
OrderDir string // "asc"(默认)或 "desc"
}
type Filter struct {
Field string // DB 列名
Op FilterOp // 操作符
Value any // 值
}
支持的操作符
| 操作符 | 常量 | SQL | Value 要求 |
|---|---|---|---|
| 等于 | OpEQ |
field = ? |
单个值 |
| 不等于 | OpNEQ |
field != ? |
单个值 |
| 模糊匹配 | OpLike |
field LIKE ? |
字符串(需自行拼 %) |
| 大于 | OpGT |
field > ? |
数字/时间 |
| 大于等于 | OpGTE |
field >= ? |
数字/时间 |
| 小于 | OpLT |
field < ? |
数字/时间 |
| 小于等于 | OpLTE |
field <= ? |
数字/时间 |
| IN | OpIn |
field IN (?,?) |
切片 |
| BETWEEN | OpRange |
field BETWEEN ? AND ? |
长度为 2 的切片 |
使用示例
records, total, err := repo.ListByFilters(ctx, repository.ListFilters{
Filters: []repository.Filter{
{Field: "status", Op: repository.OpEQ, Value: "active"},
{Field: "name", Op: repository.OpLike, Value: "%测试%"},
{Field: "level", Op: repository.OpGTE, Value: 3},
},
OrderBy: "created_at",
OrderDir: "desc",
Page: 1,
PageSize: 20,
})
配置文件
config.yaml 完整配置项说明:
app — 应用配置
| 字段 | 类型 | 说明 |
|---|---|---|
name |
string | 应用名称 |
mode |
string | 运行模式:debug / release |
host |
string | 监听地址,如 0.0.0.0 |
port |
int | 监听端口 |
mysql — MySQL 配置
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
host |
string | 数据库地址 | |
port |
int | 3306 | 端口 |
user |
string | 用户名 | |
password |
string | 密码 | |
database |
string | 数据库名 | |
charset |
string | utf8mb4 | 字符集 |
max_open_conns |
int | 100 | 最大打开连接数 |
max_idle_conns |
int | 10 | 最大空闲连接数 |
max_life_time |
int | 3600 | 连接最大生命周期(秒) |
mongodb — MongoDB 配置(业务数据)
| 字段 | 类型 | 说明 |
|---|---|---|
hosts |
[]string | 地址列表 |
database |
string | 数据库名 |
username |
string | 用户名 |
password |
string | 密码 |
min_pool_size |
int | 最小连接池 |
max_pool_size |
int | 最大连接池 |
redis — Redis 配置
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
host |
string | 地址 | |
port |
int | 6379 | 端口 |
password |
string | 密码 | |
db |
int | 0 | 数据库编号 |
pool_size |
int | 10 | 连接池大小 |
log — 日志配置
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
level |
string | debug | 日志级别:debug/info/warn/error |
format |
string | json | 格式:json/text |
output |
string | stdout | 输出:stdout/file |
file.path |
string | 文件路径 | |
file.max_size |
int | 100 | 文件最大 MB |
file.max_backups |
int | 7 | 保留备份数 |
file.max_age |
int | 30 | 保留天数 |
file.compress |
bool | true | 是否压缩 |
security — 安全配置
| 字段 | 类型 | 说明 |
|---|---|---|
jwt_secret |
string | JWT 签名密钥 |
jwt_expire |
int | JWT 过期时间(秒) |
salt |
string | 密码盐值 |
storage — 存储配置
| 字段 | 类型 | 说明 |
|---|---|---|
type |
string | 存储类型:local/oss/s3 |
local.base_path |
string | 本地存储路径 |
local.base_url |
string | 本地访问 URL |
加载配置
import "github.com/Huey1979/gocrux/internal/config"
cfg, err := config.Load("config.yaml")
// 全局可通过 config.Cfg 访问
代码生成器 gentity
tools/gentity 是一个独立的 MySQL→Go 代码生成器,根据表结构自动生成实体定义和注册蓝图。同时支持字段存在性检查,可自动发现表中缺少的框架约定字段(is_deleted、created_at/by、updated_at/by)并生成 ALTER TABLE SQL。
安装
go build -o gentity.exe ./tools/gentity/
正常生成模式
# 单表生成
gentity --dsn "user:pass@tcp(localhost:3306)/db?charset=utf8mb4&parseTime=true" \
--table users --out generated
# 全库生成
gentity --dsn "user:pass@tcp(localhost:3306)/db?charset=utf8mb4&parseTime=true" \
--all --out generated
# 全库生成 + 字段映射 + 排除日志表
gentity --dsn "user:pass@tcp(localhost:3306)/db?charset=utf8mb4&parseTime=true" \
--all \
--field-config configs/gentity_fields.yaml \
--out generated
检查模式(--check)
检查所有表(排除日志表)是否缺少框架约定字段,若缺失则生成 ALTER TABLE SQL 文件。
gentity --dsn "user:pass@tcp(localhost:3306)/db?charset=utf8mb4&parseTime=true" \
--check \
--field-config configs/gentity_fields.yaml \
--out migration
# 输出: migration/migration_add_fields.sql
默认检查的 5 个必填字段:
| 框架字段 | MySQL 类型 | 默认值 |
|---|---|---|
is_deleted |
tinyint(1) |
0 |
created_at |
datetime |
CURRENT_TIMESTAMP |
created_by |
varchar(26) |
'' |
updated_at |
datetime |
CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP |
updated_by |
varchar(26) |
'' |
字段映射配置(--field-config)
通过 YAML 文件自定义框架字段在实际表中的列名。
配置文件格式(configs/gentity_fields.example.yaml):
field_mapping:
is_deleted: del_flag # 用 del_flag 替代 is_deleted
created_by: creator # 用 creator 替代 created_by
updated_by: updater # 用 updater 替代 updated_by
deleted_at: deleted_time # 用 deleted_time 替代 deleted_at
created_at: gmt_create # 用 gmt_create 替代 created_at
updated_at: gmt_modified # 用 gmt_modified 替代 updated_at
exclude_tables: # 排除表(不检查也不生成)
- sys_operation_log
- sys_publish_history
field_mapping:按需覆盖,未覆盖的字段使用默认列名exclude_tables:日志表等无需框架字段的特殊表,检查/生成时均跳过- 检查模式下:若配置
is_deleted: del_flag,则检查表中是否有del_flag列
生成物
generated/
├── entity/
│ └── users.go # struct + TableName + Record 接口实现
└── blueprint/
├── blueprints.go # Blueprints 管理器
└── users.go # 注册蓝图(Repository→Service→Handler→Routes)
Record 接口实现
生成的 entity 文件自动实现 service.Record 接口的全部方法:
| 方法 | 行为 |
|---|---|
SetDefaults() |
遍历 DEFAULT 值,零值时回填 |
SetID() |
主键为 *_ulid 生成 ULID;自增主键空操作 |
SetCreatedAt(t) |
若表存在 created_at(或映射列)则赋值 |
SetCreatedBy(uid) |
若表存在 created_by(或映射列)则赋值 |
SetUpdatedAt(t) |
若表存在 updated_at(或映射列)则赋值 |
SetUpdatedBy(uid) |
若表存在 updated_by(或映射列)则赋值 |
SupportsDraft() |
检测 version_status 或 is_draft 列 |
SetDelete() |
is_deleted 列 → 赋值为 1(int8);deleted_at 列 → 赋值为 time.Now();否则返回 false |
PKField() |
返回主键数据库列名 |
SelfFKField() |
检测 parent_ulid 或 parent_id 列 |
自动类型映射
| MySQL 类型 | Go 类型 | 说明 |
|---|---|---|
varchar/char/text/json |
string |
|
int/int unsigned |
int/uint |
|
bigint |
int64 |
|
decimal |
float64 |
|
datetime/timestamp |
time.Time |
|
tinyint(1) |
int8 |
is_deleted 专用,与 heims 约定对齐 |
集成方式
- 复制
generated/entity/*.go→ 项目实体目录 - 复制
generated/blueprint/*.go→ 项目蓝图目录 - 在主程序中注册
blues := bp.NewBlueprints(svcReg, handlerReg)
blues.RegisterUser(apiGroup)
项目结构
gocrux/
├── cmd/ # 入口示例
├── handler/ # HTTP 处理层
│ ├── generic.go # GenericHandler 定义 + before/do/after
│ ├── generic_impl.go # 内置 _before/_do/_after 默认实现
│ ├── hooks.go # HandlerHooks 钩子类型定义
│ ├── cascade.go # CascadeHandler 接口 + CascadeRelation/ReferenceRelation/ChildRefRelation
│ ├── registry.go # HandlerRegistry 注册表
│ ├── txcoordinator.go # TxCoordinator 事务编排器
│ ├── request.go # RequestFactory + MapRequest + map→struct 合并
│ ├── request_util.go # BindJSON/BindQuery/GetPageParams 工具
│ ├── response.go # Response 结构 + Success/Error/InternalError
│ ├── auth_hooks.go # UserInfo + Authenticator + Authorizer 接口
│ ├── errors.go # Service error → HTTP BusinessCode 映射
│ └── utils.go # extractPK/extractMapID/removeMapID
├── service/ # 业务逻辑层
│ ├── generic.go # GenericService 定义 + CrudRequest/Record 接口
│ ├── generic_impl.go # 内置 _before/_do/_after 默认实现
│ ├── hooks.go # Hooks 钩子类型定义
│ ├── registry.go # ServiceRegistry 注册表
│ ├── request.go # CrudRequest/Mergeable/Identifiable/Validatable 接口
│ ├── idempotency.go # IdempotencyStore 幂等缓存
│ └── tx.go # WithTx/GetTx 事务透传
├── repository/ # 数据访问层
│ ├── crud.go # CRUDRepository 泛型仓储 + ListFilters + FilterOp
│ ├── base.go # BaseRepository + VersionRepository
│ └── dao.go # BaseDAO(缓存/审计扩展点)
├── internal/ # 框架内部
│ ├── bootstrap/ # 启动引导(Init/InitMySQL/InitOther/Migrate/Close)
│ ├── config/ # 配置加载(Config/Load + 各配置段结构体)
│ ├── database/
│ │ ├── mysql/ # MySQL 连接 + AutoMigrate + 类型校验
│ │ ├── mongodb/ # MongoDB 连接
│ │ └── redis/ # Redis 连接
│ ├── logger/ # 日志系统(RequestLog/ResponseLog/BusinessLog + 按天滚动)
│ ├── middleware/ # HTTP 中间件(RequestLogger/Cors/Recovery/AuthMiddleware)
│ ├── model/entity/ # 框架内置实体(SysOperationLog)
│ └── router/ # 基础路由注册
├── common/ # 通用工具
│ ├── ulid.go # ULID 生成器
│ ├── reflect.go # SetFieldValue 反射辅助
│ └── tx.go # WithTx/GetTx context 事务传递
├── constants/ # 业务状态码(BusinessCode + 消息映射)
├── errors/ # 哨兵错误 + 格式化错误函数
├── tools/gentity/ # 代码生成器(独立工具)
├── configs/ # YAML 配置样例
│ ├── gentity_fields.example.yaml # gentity 字段映射
│ ├── validations.example.yaml # 输入校验规则
│ └── menu.example.yaml # 默认菜单配置
├── config.yaml # 应用主配置(脱敏,不提交)
└── go.mod
License
MIT