// +====================================================================== // | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] // +---------------------------------------------------------------------- // | Copyright (c) 2019-2026 半城风雨团队 版权所有 // +---------------------------------------------------------------------- // | 开源协议: Apache-2.0 // | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 // +---------------------------------------------------------------------- // | 官方网站: https://www.easygoadmin.vip // +---------------------------------------------------------------------- // | 研发团队: 小蚂蚁云团队 荣誉出品 // +====================================================================== // | 版权和免责声明: // | 一、知识产权声明 // | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 // | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, // | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 // | 二、使用限制与禁止条款 // | 您理解并同意,禁止将本产品用于以下任何行为: // | 1. 任何违反中华人民共和国法律法规的用途; // | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; // | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 // | 三、责任豁免 // | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 // | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 // | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 // | 四、违约与法律责任 // | 如发现任何违反本声明的行为,本团队有权: // | 1. 无条件配合公安机关及其他有权机关的调查取证工作; // | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; // | 3. 保留进一步采取刑事控告等法律措施的权利。 // | 五、许可范围说明 // | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 // | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 // | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 // | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 // | 成的一切经济损失及商誉损害。 // | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 // | 许可协议》附件为准。 // +====================================================================== package logic import ( "context" "fmt" "go/format" "os" "path/filepath" "regexp" "strings" "text/template" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "easygoadmin/api/v1" "easygoadmin/internal/consts" "easygoadmin/internal/service" ) // ============================== 代码生成业务实现 ============================== // sGenerator 代码生成业务实现 // 实现 service.IGenerator 接口,承载代码生成模块的业务逻辑 type sGenerator struct{} func init() { service.RegisterGenerator(&sGenerator{}) } // ============================================================================ // 模板数据模型:后端三模板(api/controller/logic)与前端 ui/ui2 模板共用。 // 模板语法:Go text/template({{.Var}} / {{range}} / {{if}})。 // ============================================================================ // genChoice 枚举选项 // 从字段注释解析,例如 "状态:1-正常 2-禁用" type genChoice struct { Label string `json:"label"` // 选项显示名 Value string `json:"value"` // 选项值 Type string `json:"type"` // 标签类型:number / string ValueType string `json:"valueType"` // TS 类型注释 } // genField 单字段 // 驱动前后端模板的字段循环 type genField struct { Name string // 数据库列名(snake) CamelName string // JS 驼峰(parentId) CamelExport string // Go 导出名(ParentId) CamelLower string // 首字母小写驼峰(供 filtered 参数) Label string // 中文名(注释清洗) Comment string // 原始注释 Type string // 伪 Eloquent 类型(CharField/IntegerField/...) GoType string // Go 类型(string/int/float64) EntityGoType string // 实体与 dto 的 Go 类型(int64/*gtime.Time 等) IsGtime bool // 时间列(entity 使用 *gtime.Time,do 同步) RowMethod string // 行取值方法(.String()/.Int()/.Float64()) IsNumber bool // 数值类(驱动 if 判断) Required bool // 表单必填 Default string // 表单默认值 IsImage bool // 图片字段 IsRichText bool // 富文本字段 IsTextarea bool // 多行文本字段 MinValue string // 数值控件最小值 MaxValue string // 数值控件最大值 Choices []genChoice // 枚举选项集合 HasChoices bool // 是否包含枚举选项 IsPk bool // 主键列 IsAudit bool // 审计列(不进表单与列表) } // genVars 模板顶层数据 type genVars struct { AppName string // 前端 DOM 与表单名(等于模块驼峰) ApiPath string // 前端 API 导入路径(domain/moduleCamel) Domain string // 路由域(system/data/...) DomainPkg string // Go 包名 DomainPkgExport string // 包名导出形(System/Data) Module string // 模块名(snake:config_item) ModuleCamel string // 模块驼峰(configItem) ModelClass string // 结构体类名(ConfigItem) TableNameFull string // 原表名(goframe_config_item) ModuleComment string // 中文名 RoutePrefix string // 路由前缀(由模块名直接承担) PermissionPrefix string // 权限前缀(sys:{module}) PrimaryPk string // 主键列名 DaoName string // dao 变量名(ConfigItem) DisplayField string // 模糊搜索列 DisplayLabel string // 模糊搜索列的中文名 DisplayFieldCamel string // 模糊搜索列的驼峰名 ParentIdField string // 上级字段列名 ParentIdCamel string // 上级字段驼峰名 ParentIdCamelExport string // 上级字段导出名 OrderField string // 排序字段 OrderType string // 排序方向 TemplateType string // 模板类型:ui / ui2 IsTree bool // 是否树形结构 HasStatus bool // 是否含状态字段 HasSort bool // 是否含排序字段 HasImport bool // 是否支持导入 HasExport bool // 是否支持导出 HasCreateUser bool // 是否含创建人字段 HasCreateTime bool // 是否含创建时间字段 NeedGtime bool // 是否需要引入 gtime HasGtime bool // 是否存在时间列 Fields []genField // 全部字段 ListFields []genField // 列表字段 FormFields []genField // 表单字段 FilterFields []genField // 筛选字段 HasImageField bool // 是否含图片字段 HasRichTextField bool // 是否含富文本字段 } // genDomainMap 表到域的映射 // 架构文档约定的域划分,未匹配时默认 tool var genDomainMap = map[string]string{ "user": "system", "role": "system", "menu": "system", "dept": "system", "level": "system", "position": "system", "tenant": "system", "config": "data", "config_item": "data", "param": "data", "dict": "data", "dict_item": "data", "notice": "data", "city": "data", "article": "content", "category": "content", "link": "content", "operation_log": "monitor", "login_log": "monitor", "job": "monitor", "job_log": "monitor", "example": "tool", "file_template": "tool", "generator": "tool", "upload": "tool", } // Page 数据表分页 // MySQL 查询 information_schema,其他方言使用 gdb 元数据降级 func (s *sGenerator) Page(ctx context.Context, req *v1.GeneratorPageReq) (res *v1.GeneratorPageRes, err error) { tables, err := s.tableList(ctx) if err != nil { return nil, err } // 过滤 filtered := make([]v1.GeneratorTableItem, 0, len(tables)) for _, t := range tables { if req.TableName != "" && !strings.Contains(strings.ToLower(t.TableName), strings.ToLower(req.TableName)) { continue } if req.TableComment != "" && !strings.Contains(t.TableComment, req.TableComment) { continue } filtered = append(filtered, t) } total := len(filtered) start := req.Offset() if start > total { start = total } end := start + req.GetPageSize() if end > total { end = total } return v1.NewPageData(req.GetPageNo(), req.GetPageSize(), total, filtered[start:end]), nil } // Preview 预览代码 // 渲染模板但不落盘,返回文件清单 func (s *sGenerator) Preview(ctx context.Context, req *v1.GeneratorPreviewReq) (res *v1.GeneratorPreviewRes, err error) { res = &v1.GeneratorPreviewRes{List: []v1.GeneratorFileItem{}} for _, raw := range req.TableNames { tableName := parseTableArg(raw) files, _, e := s.renderTable(ctx, tableName) if e != nil { return nil, gerror.NewCodef(consts.CodeParamError, "渲染 %s 失败: %v", tableName, e) } for _, f := range files { res.List = append(res.List, v1.GeneratorFileItem{Path: f.path, Content: f.content}) } } return res, nil } // Generate 一键生成 // // 缺失文件补生成;已存在文件跳过; // 开启 generator.overwrite 时覆盖,并同步创建菜单、字典与路由 func (s *sGenerator) Generate(ctx context.Context, req *v1.GeneratorGenerateReq) (res *v1.GeneratorGenerateRes, err error) { res = &v1.GeneratorGenerateRes{Generated: []string{}, Skipped: []string{}, Failed: []string{}, Overwritten: []string{}, Dict: []v1.GeneratorDictResult{}} overwrite := s.overwriteEnabled(ctx) for _, raw := range req.TableNames { tableName := parseTableArg(raw) skipFiles := skipTables(tableName) var vars *genVars var files []genFile var e error if skipFiles { // 关联表:跳过文件写入,但幂等副作用(菜单/字典/路由)照常执行 vars, e = s.buildVars(ctx, tableName) } else { files, vars, e = s.renderTable(ctx, tableName) } if e != nil { res.Failed = append(res.Failed, fmt.Sprintf("%s: %v", tableName, e)) continue } if skipFiles { res.Skipped = append(res.Skipped, tableName+"(关联表,跳过文件)") } for _, f := range files { switch s.writeFile(f.path, f.content, overwrite) { case fileWritten: res.Generated = append(res.Generated, f.path) case fileExists: res.Skipped = append(res.Skipped, f.path) case fileOverwritten: res.Overwritten = append(res.Overwritten, f.path) default: res.Failed = append(res.Failed, f.path) } } // 同步创建:菜单 + 字典 + 路由注册(多表时以最后一张为准合并提示) res.Menu = s.createMenu(ctx, vars) res.Dict = s.createDict(ctx, vars) res.Route = s.registerRouter(vars) } return res, nil } // BatchGenerate 批量生成 // 逐表渲染并统计生成、跳过、覆盖、失败数量 func (s *sGenerator) BatchGenerate(ctx context.Context, req *v1.GeneratorBatchGenerateReq) (res *v1.GeneratorBatchGenerateRes, err error) { res = &v1.GeneratorBatchGenerateRes{List: []v1.GeneratorBatchItem{}} overwrite := s.overwriteEnabled(ctx) for _, raw := range req.TableNames { tableName := parseTableArg(raw) item := v1.GeneratorBatchItem{Table: tableName, Success: true, Dict: []v1.GeneratorDictResult{}} skipFiles := skipTables(tableName) var vars *genVars var files []genFile var e error if skipFiles { vars, e = s.buildVars(ctx, tableName) } else { files, vars, e = s.renderTable(ctx, tableName) } if e != nil { item.Success = false item.Message = e.Error() res.List = append(res.List, item) continue } if skipFiles { item.Message = "关联表(跳过文件)" } for _, f := range files { switch s.writeFile(f.path, f.content, overwrite) { case fileWritten: item.Generated++ case fileExists: item.Skipped++ case fileOverwritten: item.Overwritten++ default: item.Failed++ } } item.Menu = s.createMenu(ctx, vars) item.Dict = s.createDict(ctx, vars) item.Route = s.registerRouter(vars) item.Success = item.Failed == 0 if !skipFiles { item.Message = fmt.Sprintf("生成 %d,跳过 %d,覆盖 %d,失败 %d", item.Generated, item.Skipped, item.Overwritten, item.Failed) } res.List = append(res.List, item) } return res, nil } // ============================================================================ // 表结构与模板数据 // ============================================================================ // fileOutcome 文件写入结果 type fileOutcome int const ( fileWritten fileOutcome = iota // 新写入 fileExists // 已存在并跳过 fileFailed // 写入失败 fileOverwritten // 已存在并覆盖 ) // overwriteEnabled 读取覆盖开关 // // 唯一入口为配置 generator.overwrite; // true 时生成一律覆盖已存在文件,false 或未配置时缺失补生成、存在跳过。 func (s *sGenerator) overwriteEnabled(ctx context.Context) bool { return g.Cfg().MustGet(ctx, "generator.overwrite").Bool() } // skipTables 生成保护 // // 仅多对多关联表整表跳过,这类表本就不应有模块文件; // 其余表一律渲染,落盘由 writeFile 按文件粒度处理,不存在则补生成、存在则跳过、开启覆盖开关时覆盖。 func skipTables(tableName string) bool { switch stripPrefix(tableName) { case "role_menu", "user_role": return true } return false } // parseTableArg 解析表参数 // 将 "表名|注释" 解析为表名 func parseTableArg(arg string) string { if i := strings.Index(arg, "|"); i >= 0 { return strings.TrimSpace(arg[:i]) } return strings.TrimSpace(arg) } // tableList 查询数据表清单 // MySQL 走 information_schema,其他方言走 gdb 元数据降级 func (s *sGenerator) tableList(ctx context.Context) ([]v1.GeneratorTableItem, error) { dbTypeVar, _ := g.Cfg().GetWithEnv(ctx, "database.default.type") dbType := dbTypeVar.String() if dbType != "mysql" { // 非 MySQL:gdb 元数据仅提供表名(注释留空,模块名回退表名) names, err := g.DB().Tables(ctx) if err != nil { return nil, err } items := make([]v1.GeneratorTableItem, 0, len(names)) for _, n := range names { items = append(items, v1.GeneratorTableItem{TableName: n}) } return items, nil } linkVar, _ := g.Cfg().GetWithEnv(ctx, "database.default.link") dbName := linkVar.String() // link 形如 mysql:user:pass@tcp(host:port)/dbname?charset=... if i := strings.Index(dbName, "/"); i >= 0 { dbName = dbName[i+1:] } if i := strings.IndexAny(dbName, "?"); i >= 0 { dbName = dbName[:i] } rows, err := g.DB().GetAll(ctx, "SELECT TABLE_NAME AS table_name, TABLE_COMMENT AS table_comment, ENGINE AS engine,"+ " TABLE_ROWS AS table_rows, DATA_LENGTH AS data_length, AUTO_INCREMENT AS auto_increment,"+ " TABLE_COLLATION AS table_collation, CREATE_TIME AS create_time"+ " FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? ORDER BY TABLE_NAME", dbName, ) if err != nil { return nil, err } items := make([]v1.GeneratorTableItem, 0, len(rows)) for _, r := range rows { items = append(items, v1.GeneratorTableItem{ TableName: r["table_name"].String(), TableComment: r["table_comment"].String(), Engine: r["engine"].String(), TableRows: r["table_rows"].Int64(), DataLength: r["data_length"].Int64(), AutoIncrement: r["auto_increment"].Int64(), TableCollation: r["table_collation"].String(), CreateTime: r["create_time"].GTime().String(), }) } return items, nil } // renderTable 渲染单张表的全部目标文件 // 返回渲染结果与模板变量,供副作用同步使用 type genFile struct{ path, content string } func (s *sGenerator) renderTable(ctx context.Context, tableName string) ([]genFile, *genVars, error) { vars, err := s.buildVars(ctx, tableName) if err != nil { return nil, nil, err } // 目标文件规划:后端三件套 + 前端(ui 普通列表 / ui2 树形) type tplTarget struct { tpl string path string } // 后端平铺:api/v1、controller、logic、service 均不分域目录 // dao 四件套(dao/index + dao/internal + entity + do)由生成器直接落盘 targets := []tplTarget{ {"templates/api.tpl", filepath.Join("api", "v1", vars.Module+".go")}, {"templates/controller.tpl", filepath.Join("internal", "controller", vars.Module+".go")}, {"templates/logic.tpl", filepath.Join("internal", "logic", vars.Module+".go")}, {"templates/service.tpl", filepath.Join("internal", "service", vars.Module+".go")}, {"templates/dao_index.tpl", filepath.Join("internal", "dao", vars.Module+".go")}, {"templates/dao_internal.tpl", filepath.Join("internal", "dao", "internal", vars.Module+".go")}, {"templates/entity.tpl", filepath.Join("internal", "model", "entity", vars.Module+".go")}, {"templates/do.tpl", filepath.Join("internal", "model", "do", vars.Module+".go")}, } if vars.TemplateType == "ui2" { targets = append(targets, tplTarget{"templates/ui2/api.ts.tpl", filepath.Join("ui", "src", "api", vars.Domain, vars.ModuleCamel+".ts")}, tplTarget{"templates/ui2/index.vue.tpl", filepath.Join("ui", "src", "views", vars.Domain, vars.ModuleCamel, "index.vue")}, tplTarget{"templates/ui2/edit.vue.tpl", filepath.Join("ui", "src", "views", vars.Domain, vars.ModuleCamel, "edit.vue")}, tplTarget{"templates/ui2/detail.vue.tpl", filepath.Join("ui", "src", "views", vars.Domain, vars.ModuleCamel, "detail.vue")}, ) } else { targets = append(targets, tplTarget{"templates/ui/api.ts.tpl", filepath.Join("ui", "src", "api", vars.Domain, vars.ModuleCamel+".ts")}, tplTarget{"templates/ui/index.vue.tpl", filepath.Join("ui", "src", "views", vars.Domain, vars.ModuleCamel, "index.vue")}, tplTarget{"templates/ui/edit.vue.tpl", filepath.Join("ui", "src", "views", vars.Domain, vars.ModuleCamel, "edit.vue")}, tplTarget{"templates/ui/detail.vue.tpl", filepath.Join("ui", "src", "views", vars.Domain, vars.ModuleCamel, "detail.vue")}, tplTarget{"templates/ui/columns.ts.tpl", filepath.Join("ui", "src", "views", vars.Domain, vars.ModuleCamel, "columns.ts")}, tplTarget{"templates/ui/querySchemas.ts.tpl", filepath.Join("ui", "src", "views", vars.Domain, vars.ModuleCamel, "querySchemas.ts")}, ) } files := make([]genFile, 0, len(targets)) for _, t := range targets { content, e := renderTemplate(t.tpl, vars) if e != nil { return nil, nil, fmt.Errorf("%s: %v", t.tpl, e) } // Go 产物统一 gofmt(结构体标签与注释对齐) if strings.HasSuffix(t.path, ".go") { if formatted, fe := format.Source([]byte(content)); fe == nil { content = string(formatted) } else { return nil, nil, fmt.Errorf("%s: gofmt: %v", t.tpl, fe) } } files = append(files, genFile{path: t.path, content: content}) } return files, vars, nil } // renderTemplate 解析并渲染单个模板 // // 后端模板使用 Go 默认 {{ }} 定界; // 前端模板因与 Vue 插值 {{ }} 冲突,改用 [[ ]] 定界 func renderTemplate(tplPath string, vars *genVars) (string, error) { raw, err := os.ReadFile(tplPath) if err != nil { return "", err } name := filepath.Base(tplPath) tpl := template.New(name).Funcs(template.FuncMap{ // UI 模板条件辅助 "containsAny": func(s string, subs ...string) bool { for _, sub := range subs { if strings.Contains(s, sub) { return true } } return false }, "isUrlField": func(name string) bool { switch strings.ToLower(name) { case "website", "site", "url", "link": return true } return false }, }) if strings.HasPrefix(filepath.ToSlash(tplPath), "templates/ui") { tpl = tpl.Delims("[[", "]]") } tpl, err = tpl.Parse(string(raw)) if err != nil { return "", err } var sb strings.Builder if err = tpl.ExecuteTemplate(&sb, name, vars); err != nil { return "", err } return sb.String(), nil } // writeFile 写入文件 // 缺失时补写,已存在默认跳过以保留手工代码,overwrite 为 true 时覆盖 func (s *sGenerator) writeFile(path, content string, overwrite bool) fileOutcome { _, statErr := os.Stat(path) exists := statErr == nil if exists && !overwrite { return fileExists } if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { return fileFailed } if err := os.WriteFile(path, []byte(content), 0o644); err != nil { return fileFailed } if exists { return fileOverwritten } return fileWritten } // buildVars 组装模板变量 // 读取表结构元数据,构建字段集合与命名变量 func (s *sGenerator) buildVars(ctx context.Context, tableName string) (*genVars, error) { fieldsMap, err := g.DB().TableFields(ctx, tableName) if err != nil { return nil, fmt.Errorf("读取表结构失败: %v", err) } // 按元数据 Index 还原列顺序(map 无序) ordered := make([]*genField, 0, len(fieldsMap)) used := make(map[string]bool, len(fieldsMap)) for i := 0; i < len(fieldsMap); i++ { for name, f := range fieldsMap { if !used[name] && f.Index == i { used[name] = true ordered = append(ordered, buildField(f)) break } } } // 兜底:Index 不连续时按名称补齐 if len(ordered) != len(fieldsMap) { ordered = ordered[:0] for name, f := range fieldsMap { _ = name ordered = append(ordered, buildField(f)) } } fields := ordered moduleName := stripPrefix(tableName) comment := tableCommentOf(ctx, tableName) comment = cleanModuleComment(comment, moduleName) // 字段集合 all := make([]genField, 0, len(fields)) listF := make([]genField, 0, len(fields)) formF := make([]genField, 0, len(fields)) filterF := make([]genField, 0, len(fields)) var displayField = "name" displayCandidates := []string{"name", "title", "label", "username", "realname", "nickname", "category_name", "job_name", "dept_name", "role_name", "position_name", "level_name"} foundDisplay := false hasStatus, hasSort, hasParent := false, false, false hasImage, hasRich := false, false hasCreateUser, hasCreateTime := false, false hasGtime := false for _, f := range fields { all = append(all, *f) if f.IsGtime { hasGtime = true } switch f.Name { case "id": // 主键进列表(树形/分页需要 item.Id),不进表单与筛选 listF = append(listF, *f) continue case "create_user": hasCreateUser = true continue case "create_time": hasCreateTime = true continue case "update_user", "update_time", "del_flag", "tenant_id": continue // 审计/租户列不进前后端字段 case "status": hasStatus = true case "sort": hasSort = true case "parent_id", "pid": hasParent = true } // 显示字段(优先级) if !foundDisplay { for _, c := range displayCandidates { if f.Name == c { displayField = f.Name foundDisplay = true break } } } if f.IsRichText { hasRich = true } if f.IsImage { hasImage = true } // 列表:排除长文本/密码类 if !f.IsRichText && f.Name != "password" && f.Name != "salt" && f.Type != "BinaryField" { listF = append(listF, *f) } // 表单:全部业务列 if f.Type != "BinaryField" { formF = append(formF, *f) } // 筛选:字符串非图片/非URL → Input 逐列模糊;带枚举 → Select;外键 *_id → InputNumber; // 排序/统计等普通数值列无查询意义,不进筛选 switch { case f.Type == "CharField" && !f.IsImage && !isUrlFieldName(f.Name): filterF = append(filterF, *f) case f.HasChoices && (f.Type == "IntegerField" || f.Type == "BooleanField"): filterF = append(filterF, *f) case f.Type == "IntegerField" && strings.HasSuffix(f.Name, "_id"): filterF = append(filterF, *f) } } // display 必须在 list 中展示 if !foundDisplay && len(listF) > 0 { displayField = listF[0].Name } // 域与命名 domain := genDomainMap[moduleName] if domain == "" { domain = "tool" } modelClass := pascal(moduleName) moduleCamel := camelLower(moduleName) parentIdField := "parent_id" if hasParentField(fields, "pid") { parentIdField = "pid" } vars := &genVars{ AppName: moduleCamel, ApiPath: domain + "/" + moduleCamel, Domain: domain, DomainPkg: strings.ReplaceAll(domain, "_", ""), DomainPkgExport: pascal(strings.ReplaceAll(domain, "_", "")), Module: moduleName, ModuleCamel: moduleCamel, ModelClass: modelClass, TableNameFull: tableName, ModuleComment: comment, RoutePrefix: moduleName, PermissionPrefix: "sys:" + camelLower(moduleName), PrimaryPk: "id", DaoName: pascal(moduleName), DisplayField: displayField, DisplayLabel: labelOf(displayField, fields), DisplayFieldCamel: camelLower(displayField), ParentIdField: parentIdField, ParentIdCamel: camelLower(parentIdField), ParentIdCamelExport: pascal(camelLower(parentIdField)), OrderField: orderField(hasSort), OrderType: orderType(hasSort), TemplateType: templateType(hasParent), IsTree: hasParent, HasStatus: hasStatus, HasSort: hasSort, HasImport: true, HasExport: true, HasCreateUser: hasCreateUser, HasCreateTime: hasCreateTime, NeedGtime: hasCreateTime, HasGtime: hasGtime, Fields: ptrDeref(fields), ListFields: listF, FormFields: formF, FilterFields: filterF, HasImageField: hasImage, HasRichTextField: hasRich, } return vars, nil } // ---------- 字段构建 ---------- var ( reTinyInt1 = regexp.MustCompile(`^tinyint\(1\)`) reInt = regexp.MustCompile(`^(tinyint|smallint|mediumint|int|bigint)\b`) reDec = regexp.MustCompile(`^(decimal|numeric)\b`) reFloat = regexp.MustCompile(`^(float|double|real)\b`) reVarchar = regexp.MustCompile(`^(varchar|char|enum|set)\b`) reText = regexp.MustCompile(`^(tinytext|text|mediumtext|longtext)\b`) reDate = regexp.MustCompile(`^date\b`) reDateTime = regexp.MustCompile(`^(datetime|timestamp|time)\b`) reBlob = regexp.MustCompile(`^(tinyblob|blob|mediumblob|longblob|binary|varbinary)\b`) reBigint = regexp.MustCompile(`^bigint\b`) reChoice = regexp.MustCompile(`(\d+)\s*[-—-=]\s*([^\s,,;;]+)`) ) // buildField 数据库元数据转换为模板字段 // 依据列类型推断伪 Eloquent 类型、Go 类型与行取值方法 func buildField(f *gdb.TableField) *genField { field := &genField{ Name: f.Name, Comment: f.Comment, CamelName: camelLower(f.Name), CamelExport: pascal(camelLower(f.Name)), CamelLower: camelLower(f.Name), Label: cleanLabel(f.Comment, f.Name), } switch { case reTinyInt1.MatchString(f.Type): field.Type = "BooleanField" field.GoType = "int" field.RowMethod = ".Int()" field.IsNumber = true case reInt.MatchString(f.Type): field.Type = "IntegerField" field.GoType = "int" field.RowMethod = ".Int()" field.IsNumber = true case reDec.MatchString(f.Type): field.Type = "DecimalField" field.GoType = "float64" field.RowMethod = ".Float64()" field.IsNumber = true case reFloat.MatchString(f.Type): field.Type = "FloatField" field.GoType = "float64" field.RowMethod = ".Float64()" field.IsNumber = true case reDate.MatchString(f.Type): field.Type = "DateField" field.GoType = "string" field.RowMethod = ".String()" case reDateTime.MatchString(f.Type): field.Type = "DateTimeField" field.GoType = "string" field.RowMethod = ".String()" case reBlob.MatchString(f.Type): field.Type = "BinaryField" field.GoType = "string" field.RowMethod = ".String()" case reText.MatchString(f.Type): field.Type = "TextField" field.GoType = "string" field.RowMethod = ".String()" field.IsTextarea = true case reVarchar.MatchString(f.Type): field.Type = "CharField" field.GoType = "string" field.RowMethod = ".String()" default: field.Type = "CharField" field.GoType = "string" field.RowMethod = ".String()" } field.IsPk = f.Key == "PRI" || f.Name == "id" field.IsAudit = isAuditColumn(f.Name) field.Required = !f.Null && f.Default == nil && f.Extra != "auto_increment" && !field.IsPk // 实体与 dto 类型映射:bigint 转 int64,时间列转 *gtime.Time,其余值类型为非指针 switch { case reBigint.MatchString(f.Type): field.EntityGoType = "int64" case reDec.MatchString(f.Type), reFloat.MatchString(f.Type): field.EntityGoType = "float64" case reDate.MatchString(f.Type), reDateTime.MatchString(f.Type): field.EntityGoType = "*gtime.Time" field.IsGtime = true case reBlob.MatchString(f.Type): field.EntityGoType = "[]byte" case reInt.MatchString(f.Type): field.EntityGoType = "int" default: field.EntityGoType = "string" } // 注释解析:枚举、图片、富文本 field.Choices = parseChoices(f.Comment) field.HasChoices = len(field.Choices) > 0 lower := strings.ToLower(f.Name) field.IsImage = strings.Contains(f.Comment, "图片") || strings.Contains(f.Comment, "头像") || strings.Contains(f.Comment, "封面") || containsAny(lower, "avatar", "cover", "image", "icon", "photo") field.IsRichText = strings.Contains(f.Comment, "富文本") || (field.Type == "TextField" && containsAny(lower, "content", "body", "editor")) if field.Type == "TextField" { field.IsTextarea = true } // 表单默认值与数字控件边界:数值字段默认 0,输入框 min=0 max=99999 field.Default = "" field.MinValue = "0" field.MaxValue = "99999" switch field.Type { case "IntegerField", "BooleanField", "FloatField", "DecimalField": field.Default = "0" } return field } // isAuditColumn 判断是否为审计列 func isAuditColumn(name string) bool { switch name { case "id", "create_user", "create_time", "update_user", "update_time", "del_flag", "tenant_id", "salt": return true } return false } // parseChoices 从注释解析枚举 // 例如从 "状态:1-正常 2-禁用" 解析出选项集合 func parseChoices(comment string) []genChoice { matches := reChoice.FindAllStringSubmatch(comment, -1) if len(matches) == 0 { return nil } choices := make([]genChoice, 0, len(matches)) for _, m := range matches { choices = append(choices, genChoice{ Label: m[2], Value: m[1], Type: choiceTagType(m[1]), ValueType: "number", }) } return choices } // choiceTagType 枚举值转换为标签类型 // 1 对应 success,2 对应 danger,3 对应 warning,其余为 info func choiceTagType(value string) string { switch value { case "1": return "success" case "2": return "danger" case "3": return "warning" default: return "info" } } // cleanLabel 字段注释转换为表单标签 // 去除枚举尾巴,保留可读的中文名 func cleanLabel(comment, name string) string { if comment == "" { return camelLower(name) } // 去掉 "状态:1-正常 2-禁用" 的枚举尾巴 if i := strings.IndexAny(comment, "::"); i > 0 { comment = comment[:i] } return strings.TrimSpace(comment) } // cleanModuleComment 表注释转换为模块中文名 // 去除常见的表、管理、信息、数据等后缀 func cleanModuleComment(comment, fallback string) string { comment = strings.TrimSpace(comment) if comment == "" { return fallback } comment = strings.TrimSuffix(comment, "表") for _, suffix := range []string{"管理", "信息", "数据"} { comment = strings.TrimSuffix(comment, suffix) } return comment } // stripPrefix 去除表名前缀 // 将 goframe_x 转换为 x func stripPrefix(tableName string) string { return strings.TrimPrefix(tableName, "goframe_") } // tableCommentOf 查询表注释 // MySQL 查询 information_schema,其他方言回退表名 func tableCommentOf(ctx context.Context, tableName string) string { dbTypeVar, _ := g.Cfg().GetWithEnv(ctx, "database.default.type") if dbTypeVar.String() != "mysql" { return "" } row, err := g.DB().GetOne(ctx, "SELECT TABLE_COMMENT AS c FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?", tableName) if err != nil || len(row) == 0 { return "" } return row["c"].String() } // templateType 根据是否树形选择模板类型 func templateType(isTree bool) string { if isTree { return "ui2" } return "ui" } // orderField 返回排序字段 func orderField(hasSort bool) string { if hasSort { return "sort" } return "id" } // orderType 返回排序方向 func orderType(hasSort bool) string { if hasSort { return "asc" } return "desc" } // hasParentField 判断字段集合中是否包含指定字段 func hasParentField(fields []*genField, name string) bool { for _, f := range fields { if f.Name == name { return true } } return false } // labelOf 查询字段的中文名 func labelOf(name string, fields []*genField) string { for _, f := range fields { if f.Name == name { return f.Label } } return name } // ptrDeref 指针切片转为值切片 func ptrDeref(fields []*genField) []genField { out := make([]genField, 0, len(fields)) for _, f := range fields { out = append(out, *f) } return out } // pascal 命名转换 // 将 snake 或 kebab 命名转换为 Pascal,例如 config_item 转换为 ConfigItem func pascal(s string) string { parts := strings.FieldsFunc(s, func(r rune) bool { return r == '_' || r == '-' || r == ' ' }) for i, p := range parts { if p == "" { continue } parts[i] = strings.ToUpper(p[:1]) + p[1:] } return strings.Join(parts, "") } // camelLower 命名转换 // 将 snake 命名转换为小驼峰,例如 config_item 转换为 configItem func camelLower(s string) string { p := pascal(s) if p == "" { return p } return strings.ToLower(p[:1]) + p[1:] } // containsAny 判断字符串是否包含任一子串 func containsAny(s string, subs ...string) bool { for _, sub := range subs { if strings.Contains(s, sub) { return true } } return false } // isUrlFieldName 判断是否为 URL 或链接类列名 // 这类列存储的是地址而非可读文本,不作为模糊筛选条件 func isUrlFieldName(name string) bool { switch strings.ToLower(name) { case "website", "site", "url", "link": return true } return false }