From a36d0c7ac6952f8e6f4c75c0e1e765fe8e143807 Mon Sep 17 00:00:00 2001 From: hailaz <739476267@qq.com> Date: Tue, 30 Sep 2025 16:23:27 +0800 Subject: [PATCH] =?UTF-8?q?feat(tpl):=20=E5=A2=9E=E5=8A=A0=E8=87=AA?= =?UTF-8?q?=E5=AE=9A=E4=B9=89=E6=A0=87=E7=AD=BE=E6=94=AF=E6=8C=81=EF=BC=8C?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=94=9F=E6=88=90=E7=9A=84=E7=BB=93=E6=9E=84?= =?UTF-8?q?=E4=BD=93=E5=AD=97=E6=AE=B5=E6=A0=87=E7=AD=BE=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/gf/internal/cmd/gen/tpl/TAG_USAGE.md | 308 ++++++++++++++++++ .../cmd/gen/tpl/testdata/config_example.yaml | 82 +++++ .../gen/tpl/testdata/model/entity/entity.tpl | 2 +- cmd/gf/internal/cmd/gen/tpl/tpl.go | 50 ++- cmd/gf/internal/cmd/gen/tpl/tpl_field.go | 114 ++++++- cmd/gf/internal/cmd/gen/tpl/tpl_table.go | 25 ++ 6 files changed, 564 insertions(+), 17 deletions(-) create mode 100644 cmd/gf/internal/cmd/gen/tpl/TAG_USAGE.md create mode 100644 cmd/gf/internal/cmd/gen/tpl/testdata/config_example.yaml diff --git a/cmd/gf/internal/cmd/gen/tpl/TAG_USAGE.md b/cmd/gf/internal/cmd/gen/tpl/TAG_USAGE.md new file mode 100644 index 000000000..23c6a870a --- /dev/null +++ b/cmd/gf/internal/cmd/gen/tpl/TAG_USAGE.md @@ -0,0 +1,308 @@ +# 标签配置使用指南 + +## 功能概述 + +`gf gen tpl` 现在支持灵活的标签配置,可以选择性地为生成的结构体字段添加 `omitempty` 或其他自定义标签。 + +## 配置选项一览 + +| 选项 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `jsonOmitempty` | bool | false | 为所有字段添加 omitempty | +| `jsonOmitemptyAuto` | bool | false | 仅为可空字段自动添加 omitempty | +| `withOrmTag` | bool | true | 是否添加 orm 标签 | +| `descriptionTag` | bool | false | 是否添加 description 标签 | +| `noJsonTag` | bool | false | 是否禁用 JSON 标签 | +| `fieldMapping.tags` | map | - | 字段级自定义标签 | + +## 配置方式 + +### 1. 全局开关 - `jsonOmitempty` + +为所有字段的 JSON 标签添加 `omitempty`: + +```yaml +gfcli: + gen: + tpl: + jsonOmitempty: true +``` + +**生成结果:** +```go +type User struct { + ID int `json:"id,omitempty" orm:"id" description:"用户ID"` + Name string `json:"name,omitempty" orm:"name" description:"用户名"` + Email string `json:"email,omitempty" orm:"email" description:"邮箱"` +} +``` + +--- + +### 2. 智能判断 - `jsonOmitemptyAuto` (推荐) + +仅为可空字段自动添加 `omitempty`: + +```yaml +gfcli: + gen: + tpl: + jsonOmitemptyAuto: true +``` + +**假设数据库表结构:** +```sql +CREATE TABLE user ( + id INT NOT NULL, + name VARCHAR(50) NOT NULL, + email VARCHAR(100) NULL, -- 可空字段 + age INT NULL -- 可空字段 +); +``` + +**生成结果:** +```go +type User struct { + ID int `json:"id" orm:"id" description:"用户ID"` + Name string `json:"name" orm:"name" description:"用户名"` + Email string `json:"email,omitempty" orm:"email" description:"邮箱"` // 自动添加 + Age int `json:"age,omitempty" orm:"age" description:"年龄"` // 自动添加 +} +``` + +--- + +### 3. ORM 标签控制 - `withOrmTag` + +控制是否添加 orm 标签 (默认启用): + +```yaml +gfcli: + gen: + tpl: + withOrmTag: false # 不生成 orm 标签 +``` + +**生成结果:** +```go +type User struct { + ID int `json:"id" description:"用户ID"` // 没有 orm 标签 + Name string `json:"name" description:"用户名"` // 没有 orm 标签 + Email string `json:"email" description:"邮箱"` // 没有 orm 标签 +} +``` + +--- + +### 4. 字段级精确控制 - `fieldMapping` + +针对特定字段自定义标签 (优先级最高): + +```yaml +gfcli: + gen: + tpl: + fieldMapping: + user.password: + type: string + tags: + json: "-" # 不序列化 + + user.email: + type: string + tags: + json: "email,omitempty" + validate: "required,email" + binding: "required" + + user.status: + type: int + tags: + json: "status,omitempty" + validate: "oneof=0 1 2" + example: "1" +``` + +**生成结果:** +```go +type User struct { + Password string `json:"-" orm:"password" description:"密码"` + Email string `binding:"required" json:"email,omitempty" validate:"required,email" description:"邮箱"` + Status int `example:"1" json:"status,omitempty" validate:"oneof=0 1 2" description:"状态"` +} +``` + +--- + +## 常见标签示例 + +### validate 标签 (gin validator) + +```yaml +fieldMapping: + user.email: + tags: + validate: "required,email" + + user.age: + tags: + validate: "gte=0,lte=150" + + user.password: + tags: + validate: "required,min=8,max=32" +``` + +### binding 标签 (gin binding) + +```yaml +fieldMapping: + user.name: + tags: + binding: "required" + + user.email: + tags: + binding: "required,email" +``` + +### swagger 文档标签 + +```yaml +fieldMapping: + user.id: + tags: + example: "1" + description: "用户唯一标识" + + user.status: + tags: + example: "1" + enums: "0,1,2" +``` + +### 多个自定义标签组合 + +```yaml +fieldMapping: + user.email: + type: string + tags: + json: "email,omitempty" + validate: "required,email" + binding: "required" + example: "user@example.com" + description: "用户邮箱地址" +``` + +--- + +## 配置优先级 + +标签配置的优先级从高到低: + +1. **fieldMapping.tags** - 字段级自定义标签 (优先级最高) +2. **jsonOmitempty** - 全局 omitempty 开关 +3. **jsonOmitemptyAuto** - 智能判断可空字段 +4. **默认行为** - 不添加 omitempty + +--- + +## 完整配置示例 + +```yaml +gfcli: + gen: + tpl: + link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test" + path: "./output" + tplPath: "./templates" + jsonCase: "CamelLower" + importPrefix: "github.com/example/project" + + # 全局配置 + jsonOmitemptyAuto: true # 可空字段自动添加 omitempty + withOrmTag: true # 添加 orm 标签 (默认) + descriptionTag: true # 添加 description 标签 + + # 类型映射 + typeMapping: + decimal: + type: decimal.Decimal + import: github.com/shopspring/decimal + + # 字段级配置 + fieldMapping: + user.password: + type: string + tags: + json: "-" + + user.email: + type: string + tags: + json: "email,omitempty" + validate: "required,email" + binding: "required" + + order.total_amount: + type: decimal.Decimal + import: github.com/shopspring/decimal + tags: + json: "totalAmount,omitempty" + validate: "gt=0" +``` + +--- + +## 命令行使用 + +```bash +# 使用配置文件 +gf gen tpl + +# 命令行参数 +gf gen tpl -tp ./templates -p ./output -ja -wo +# -ja: jsonOmitemptyAuto +# -wo: withOrmTag + +# 组合使用 +gf gen tpl -l "mysql:root:pass@tcp(127.0.0.1:3306)/db" -tp ./tpl -ja -c -wo +``` + +--- + +## 模板中使用 + +如果你需要在自定义模板中使用标签功能: + +```go +// entity.tpl +type {{.table.NameCaseCamel}} struct { {{range $i,$v := .table.Fields}} + {{$v.NameCaseCamel}} {{$v.LocalType}} {{$v.BuildTags $.tagInput}} // {{$v.Comment}}{{end}} +} +``` + +或者分别使用单个标签方法: + +```go +type {{.table.NameCaseCamel}} struct { {{range $i,$v := .table.Fields}} + {{$v.NameCaseCamel}} {{$v.LocalType}} `json:"{{$v.JsonTag $.tagInput.JsonOmitempty $.tagInput.JsonOmitemptyAuto}}" orm:"{{$v.OrmTag}}"` // {{$v.Comment}}{{end}} +} +``` + +--- + +## 注意事项 + +1. **字段名格式**: `fieldMapping` 中的 key 格式为 `表名.字段名`,使用数据库中的实际字段名 (非驼峰) + +2. **标签顺序**: 自定义标签会按字母顺序排列,确保生成结果一致 + +3. **特殊字符**: 如果标签值包含双引号,会自动转义 + +4. **DO 文件**: DO 文件 (model/do) 只保留 description 标签,不包含 JSON/ORM 标签 + +5. **兼容性**: 与现有的 `typeMapping` 和 `fieldMapping` 完全兼容 + +6. **默认值**: `withOrmTag` 默认为 `true`,如果不需要 orm 标签,需要显式设置为 `false` diff --git a/cmd/gf/internal/cmd/gen/tpl/testdata/config_example.yaml b/cmd/gf/internal/cmd/gen/tpl/testdata/config_example.yaml new file mode 100644 index 000000000..cdb5794ce --- /dev/null +++ b/cmd/gf/internal/cmd/gen/tpl/testdata/config_example.yaml @@ -0,0 +1,82 @@ +# gf gen tpl 标签配置示例 + +gfcli: + gen: + tpl: + # 数据库连接 + link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test" + + # 输出路径 + path: "./output" + + # 模板路径 + tplPath: "./testdata" + + # JSON 命名规则 + jsonCase: "CamelLower" + + # 导入路径前缀 + importPrefix: "github.com/example/project" + + # ===== 标签配置选项 ===== + + # 方式1: 全局为所有字段添加 omitempty + jsonOmitempty: false + + # 方式2: 自动为可空字段添加 omitempty (推荐) + jsonOmitemptyAuto: true + + # 是否添加 orm 标签 (默认: true) + withOrmTag: true + + # 是否添加 description 标签 + descriptionTag: true + + # 是否禁用 JSON 标签 + noJsonTag: false + + # ===== 类型映射 ===== + + typeMapping: + decimal: + type: decimal.Decimal + import: github.com/shopspring/decimal + numeric: + type: string + + # ===== 字段级配置 (最灵活) ===== + + fieldMapping: + # 表名.字段名 格式 + user.password: + type: string + tags: + json: "-" # 不序列化密码字段 + + user.email: + type: string + tags: + json: "email,omitempty" + validate: "required,email" + binding: "required" + + user.age: + type: int + tags: + json: "age" + validate: "gte=0,lte=150" + + user.status: + type: int + tags: + json: "status,omitempty" + validate: "oneof=0 1 2" + example: "1" + + # 自定义类型示例 + order.total_amount: + type: decimal.Decimal + import: github.com/shopspring/decimal + tags: + json: "totalAmount,omitempty" + validate: "gt=0" diff --git a/cmd/gf/internal/cmd/gen/tpl/testdata/model/entity/entity.tpl b/cmd/gf/internal/cmd/gen/tpl/testdata/model/entity/entity.tpl index 11509c142..e264dccfe 100644 --- a/cmd/gf/internal/cmd/gen/tpl/testdata/model/entity/entity.tpl +++ b/cmd/gf/internal/cmd/gen/tpl/testdata/model/entity/entity.tpl @@ -10,5 +10,5 @@ import ({{range $k,$v := .table.Imports}} {{end}} // {{.table.NameCaseCamel}} is the golang structure for table {{.table.Name}}. type {{.table.NameCaseCamel}} struct { {{range $i,$v := .table.Fields}} - {{$v.NameCaseCamel}} {{$v.LocalType}} `json:"{{$v.NameJsonCase}}" orm:"{{$v.Name}}" description:"{{$v.Comment}}"` // {{$v.Comment}}{{end}} + {{$v.NameCaseCamel}} {{$v.LocalType}} {{$v.BuildTags $.tagInput}} // {{$v.Comment}}{{end}} } diff --git a/cmd/gf/internal/cmd/gen/tpl/tpl.go b/cmd/gf/internal/cmd/gen/tpl/tpl.go index 90d9332de..12ffada89 100644 --- a/cmd/gf/internal/cmd/gen/tpl/tpl.go +++ b/cmd/gf/internal/cmd/gen/tpl/tpl.go @@ -61,6 +61,9 @@ CONFIGURATION SUPPORT table_name.field_name: type: decimal.Decimal import: github.com/shopspring/decimal + tags: + json: "field_name,omitempty" + validate: "required" ` CGenTplBriefPath = `directory path for generated files` CGenTplBriefLink = `database configuration, the same as the ORM configuration of GoFrame` @@ -105,6 +108,9 @@ generated json tag case for model struct, cases are as follows: CGenTplBriefTplDaoInternalPath = `template file path for dao internal file` CGenTplBriefTplDaoDoPathPath = `template file path for dao do file` CGenTplBriefTplDaoEntityPath = `template file path for dao entity file` + CGenTplBriefJsonOmitempty = `add omitempty to all json tags` + CGenTplBriefJsonOmitemptyAuto = `automatically add omitempty to json tags for nullable fields` + CGenTplBriefWithOrmTag = `add orm tag for entity fields` ) func init() { @@ -143,6 +149,9 @@ func init() { `CGenTplBriefTplDaoInternalPath`: CGenTplBriefTplDaoInternalPath, `CGenTplBriefTplDaoDoPathPath`: CGenTplBriefTplDaoDoPathPath, `CGenTplBriefTplDaoEntityPath`: CGenTplBriefTplDaoEntityPath, + `CGenTplBriefJsonOmitempty`: CGenTplBriefJsonOmitempty, + `CGenTplBriefJsonOmitemptyAuto`: CGenTplBriefJsonOmitemptyAuto, + `CGenTplBriefWithOrmTag`: CGenTplBriefWithOrmTag, }) } @@ -170,22 +179,26 @@ type ( // TplDaoInternalPath string `name:"tplDaoInternalPath" short:"t2" brief:"{CGenTplBriefTplDaoInternalPath}"` // TplDaoDoPath string `name:"tplDaoDoPath" short:"t3" brief:"{CGenTplBriefTplDaoDoPathPath}"` // TplDaoEntityPath string `name:"tplDaoEntityPath" short:"t4" brief:"{CGenTplBriefTplDaoEntityPath}"` - StdTime bool `name:"stdTime" short:"s" brief:"{CGenTplBriefStdTime}" orphan:"true"` - WithTime bool `name:"withTime" short:"w" brief:"{CGenTplBriefWithTime}" orphan:"true"` - GJsonSupport bool `name:"gJsonSupport" short:"n" brief:"{CGenTplBriefGJsonSupport}" orphan:"true"` - OverwriteDao bool `name:"overwriteDao" short:"v" brief:"{CGenTplBriefOverwriteDao}" orphan:"true"` - DescriptionTag bool `name:"descriptionTag" short:"c" brief:"{CGenTplBriefDescriptionTag}" orphan:"true"` - NoJsonTag bool `name:"noJsonTag" short:"k" brief:"{CGenTplBriefNoJsonTag}" orphan:"true"` - NoModelComment bool `name:"noModelComment" short:"m" brief:"{CGenTplBriefNoModelComment}" orphan:"true"` - Clear bool `name:"clear" short:"a" brief:"{CGenTplBriefClear}" orphan:"true"` - TypeMapping map[string]CustomAttributeType `name:"typeMapping" short:"y" brief:"{CGenTplBriefTypeMapping}" orphan:"true"` - FieldMapping map[string]CustomAttributeType `name:"fieldMapping" short:"fm" brief:"{CGenTplBriefFieldMapping}" orphan:"true"` + StdTime bool `name:"stdTime" short:"s" brief:"{CGenTplBriefStdTime}" orphan:"true"` + WithTime bool `name:"withTime" short:"w" brief:"{CGenTplBriefWithTime}" orphan:"true"` + GJsonSupport bool `name:"gJsonSupport" short:"n" brief:"{CGenTplBriefGJsonSupport}" orphan:"true"` + OverwriteDao bool `name:"overwriteDao" short:"v" brief:"{CGenTplBriefOverwriteDao}" orphan:"true"` + DescriptionTag bool `name:"descriptionTag" short:"c" brief:"{CGenTplBriefDescriptionTag}" orphan:"true"` + NoJsonTag bool `name:"noJsonTag" short:"k" brief:"{CGenTplBriefNoJsonTag}" orphan:"true"` + NoModelComment bool `name:"noModelComment" short:"m" brief:"{CGenTplBriefNoModelComment}" orphan:"true"` + Clear bool `name:"clear" short:"a" brief:"{CGenTplBriefClear}" orphan:"true"` + JsonOmitempty bool `name:"jsonOmitempty" short:"jo" brief:"{CGenTplBriefJsonOmitempty}" orphan:"true"` + JsonOmitemptyAuto bool `name:"jsonOmitemptyAuto" short:"ja" brief:"{CGenTplBriefJsonOmitemptyAuto}" orphan:"true"` + WithOrmTag bool `name:"withOrmTag" short:"wo" brief:"{CGenTplBriefWithOrmTag}" orphan:"false" d:"false"` + TypeMapping map[string]CustomAttributeType `name:"typeMapping" short:"y" brief:"{CGenTplBriefTypeMapping}" orphan:"true"` + FieldMapping map[string]CustomAttributeType `name:"fieldMapping" short:"fm" brief:"{CGenTplBriefFieldMapping}" orphan:"true"` } CGenTplOutput struct{} CustomAttributeType struct { - Type string `brief:"custom attribute type name"` - Import string `brief:"custom import for this type"` + Type string `brief:"custom attribute type name"` + Import string `brief:"custom import for this type"` + Tags map[string]string `brief:"custom tags for this field, e.g. json, validate, binding"` } ) @@ -304,10 +317,19 @@ func (c CGenTpl) Tpl(ctx context.Context, in CGenTplInput) (out *CGenTplOutput, view := gview.New() for _, table := range tables { + // Create tag input for this table + tagInput := TagBuildInput{ + NoJsonTag: in.NoJsonTag, + JsonOmitempty: in.JsonOmitempty, + JsonOmitemptyAuto: in.JsonOmitemptyAuto, + WithOrmTag: in.WithOrmTag, + DescriptionTag: in.DescriptionTag, + } tplData := g.Map{ - "table": table, - "tables": tables, + "table": table, + "tables": tables, + "tagInput": tagInput, } fmt.Println(table.FieldsJsonStr(in.JsonCase)) for _, tpl := range tplList { diff --git a/cmd/gf/internal/cmd/gen/tpl/tpl_field.go b/cmd/gf/internal/cmd/gen/tpl/tpl_field.go index f2fb15285..95f764033 100644 --- a/cmd/gf/internal/cmd/gen/tpl/tpl_field.go +++ b/cmd/gf/internal/cmd/gen/tpl/tpl_field.go @@ -2,6 +2,8 @@ package tpl import ( "context" + "fmt" + "sort" "strings" "github.com/gogf/gf/v2/database/gdb" @@ -12,8 +14,9 @@ import ( // TableField description type TableField struct { gdb.TableField - LocalType string - JsonCase string + LocalType string + JsonCase string + CustomTags map[string]string // 自定义标签 } type TableFields []*TableField @@ -144,3 +147,110 @@ func (f *TableField) NameCaseSnake() string { func (f *TableField) NameCaseKebabScreaming() string { return gstr.CaseKebabScreaming(f.Name) } + +// IsNullable returns whether the field is nullable +func (f *TableField) IsNullable() bool { + return f.Null +} + +// JsonTag generates json tag for the field +func (f *TableField) JsonTag(omitempty bool, omitemptyAuto bool) string { + if f.CustomTags != nil { + if jsonTag, ok := f.CustomTags["json"]; ok { + return jsonTag + } + } + + name := f.NameJsonCase() + if omitempty || (omitemptyAuto && f.IsNullable()) { + return name + ",omitempty" + } + return name +} + +// OrmTag generates orm tag for the field +func (f *TableField) OrmTag() string { + if f.CustomTags != nil { + if ormTag, ok := f.CustomTags["orm"]; ok { + return ormTag + } + } + return f.Name +} + +// DescriptionTag generates description tag for the field +func (f *TableField) DescriptionTag() string { + if f.CustomTags != nil { + if descTag, ok := f.CustomTags["description"]; ok { + return descTag + } + } + // 转义双引号 + comment := strings.ReplaceAll(f.Comment, `"`, `\"`) + return comment +} + +// CustomTag returns custom tag value by name +func (f *TableField) CustomTag(name string) string { + if f.CustomTags == nil { + return "" + } + return f.CustomTags[name] +} + +// BuildTags builds all tags for the field +func (f *TableField) BuildTags(in TagBuildInput) string { + var tags []string + + // JSON tag + if !in.NoJsonTag { + jsonValue := f.JsonTag(in.JsonOmitempty, in.JsonOmitemptyAuto) + tags = append(tags, fmt.Sprintf(`json:"%s"`, jsonValue)) + } + + // ORM tag + if in.WithOrmTag { + ormValue := f.OrmTag() + tags = append(tags, fmt.Sprintf(`orm:"%s"`, ormValue)) + } + + // Description tag + if in.DescriptionTag { + descValue := f.DescriptionTag() + tags = append(tags, fmt.Sprintf(`description:"%s"`, descValue)) + } + + // Custom tags from CustomTags map + if f.CustomTags != nil { + // 按字母顺序遍历,确保输出稳定 + var keys []string + for k := range f.CustomTags { + // 跳过已处理的标准标签 + if k == "json" || k == "orm" || k == "description" { + continue + } + keys = append(keys, k) + } + sort.Strings(keys) + + for _, k := range keys { + v := f.CustomTags[k] + tags = append(tags, fmt.Sprintf(`%s:"%s"`, k, v)) + } + } + + if len(tags) == 0 { + return "" + } + + return "`" + strings.Join(tags, " ") + "`" +} + +// TagBuildInput for building tags +type TagBuildInput struct { + NoJsonTag bool + JsonOmitempty bool + JsonOmitemptyAuto bool + WithOrmTag bool + DescriptionTag bool +} diff --git a/cmd/gf/internal/cmd/gen/tpl/tpl_table.go b/cmd/gf/internal/cmd/gen/tpl/tpl_table.go index f5bb2b35c..9f88ddd80 100644 --- a/cmd/gf/internal/cmd/gen/tpl/tpl_table.go +++ b/cmd/gf/internal/cmd/gen/tpl/tpl_table.go @@ -100,7 +100,10 @@ func (t *Table) toTableFields(in CGenTplInput) { field := &TableField{ TableField: *v, JsonCase: in.JsonCase, + CustomTags: make(map[string]string), } + + // 设置字段类型 appendImport := field.GetLocalTypeName(context.Background(), t.db, Input{ TypeMapping: in.TypeMapping, FieldMapping: in.FieldMapping, @@ -110,6 +113,18 @@ func (t *Table) toTableFields(in CGenTplInput) { if appendImport != "" { t.Imports[appendImport] = struct{}{} } + + // 从 FieldMapping 中提取自定义标签 + if in.FieldMapping != nil { + if fieldMapping, ok := in.FieldMapping[v.Name]; ok { + if fieldMapping.Tags != nil { + for tagName, tagValue := range fieldMapping.Tags { + field.CustomTags[tagName] = tagValue + } + } + } + } + t.Fields[v.Index] = field } } @@ -144,6 +159,16 @@ func (t *Table) FieldsJsonStr(caseName string) string { return string(b) } +// TagInput holds input for tag generation +type TagInput struct { + in CGenTplInput +} + +// GetTagInput returns TagInput for template usage +func (t *Table) GetTagInput(in CGenTplInput) TagInput { + return TagInput{in: in} +} + // GetTables 获取数据库表结构信息 func (t *TplObj) GetTables() (Tables, error) { nameList, err := t.db.Tables(t.ctx)