package codegen import ( "bytes" "fmt" "os" "path/filepath" "strings" "text/template" codegenModel "sundynix-go/model/codegen" "github.com/glebarez/sqlite" "gorm.io/driver/mysql" "gorm.io/driver/postgres" "gorm.io/gorm" ) type CodegenService struct{} var CodegenServiceApp = new(CodegenService) // ---------------------------------------- // 模板:model 实体(继承 global.BaseModel) // ---------------------------------------- const modelTmpl = `package {{.PackageName}} import "sundynix-go/global" {{if .HasTimeField}}import "time" {{end}} // {{.Feature.Name}} {{.Feature.Comment}} type {{.Feature.Name}} struct { global.BaseModel {{- range .Feature.Fields}} {{.Name}} {{.Type}} ` + "`" + `gorm:"column:{{.ColumnName}}{{if .GormTag}};{{.GormTag}}{{end}}" json:"{{.JsonTag}}"` + "`" + ` // {{.Comment}} {{- end}} {{- range .Feature.Relations}} {{- if eq .Type "OneToOne"}} {{.FieldName}} *{{.TargetFeature}} ` + "`" + `gorm:"foreignKey:{{.ForeignKey}}" json:"{{lowerFirst .FieldName}}"` + "`" + ` {{- else if eq .Type "OneToMany"}} {{.FieldName}} []{{.TargetFeature}} ` + "`" + `gorm:"foreignKey:{{.ForeignKey}}" json:"{{lowerFirst .FieldName}}"` + "`" + ` {{- else if eq .Type "ManyToMany"}} {{.FieldName}} []{{.TargetFeature}} ` + "`" + `gorm:"many2many:{{.JoinTable}};" json:"{{lowerFirst .FieldName}}"` + "`" + ` {{- end}} {{- end}} } func ({{.Feature.Name}}) TableName() string { return "{{.Feature.TableName}}" } ` // ---------------------------------------- // 模板:request 结构体(每个模块一个文件,包含所有实体的请求结构体) // ---------------------------------------- const requestTmpl = `package request {{range .Features}} // ---- {{.Comment}} ---- // Save{{.Name}}Req 新增{{.Comment}}请求 type Save{{.Name}}Req struct { {{- range .Fields}} {{.Name}} {{.Type}} ` + "`" + `json:"{{.JsonTag}}" form:"{{.JsonTag}}"{{if .Required}} binding:"required"{{end}}` + "`" + ` // {{.Comment}} {{- end}} } // Update{{.Name}}Req 更新{{.Comment}}请求 type Update{{.Name}}Req struct { Id string ` + "`" + `json:"id" binding:"required"` + "`" + ` // ID {{- range .Fields}} {{.Name}} {{.Type}} ` + "`" + `json:"{{.JsonTag}}" form:"{{.JsonTag}}"` + "`" + ` // {{.Comment}} {{- end}} } // List{{.Name}}Req 分页查询{{.Comment}}请求 type List{{.Name}}Req struct { Current int ` + "`" + `json:"current" form:"current"` + "`" + ` // 页码 PageSize int ` + "`" + `json:"pageSize" form:"pageSize"` + "`" + ` // 每页大小 Keyword string ` + "`" + `json:"keyword" form:"keyword"` + "`" + ` // 关键字搜索 } {{end}}` // ---------------------------------------- // 模板:service 层(使用 request 包的结构体) // ---------------------------------------- const serviceTmpl = `package {{.PackageName}} import ( "sundynix-go/global" model "sundynix-go/model/{{.PackageName}}" req "sundynix-go/model/{{.PackageName}}/request" ) type {{.Feature.Name}}Service struct{} var {{.Feature.Name}}ServiceApp = new({{.Feature.Name}}Service) func (s *{{.Feature.Name}}Service) Save(r req.Save{{.Feature.Name}}Req) error { data := model.{{.Feature.Name}}{ {{- range .Feature.Fields}} {{.Name}}: r.{{.Name}}, {{- end}} } return global.DB.Create(&data).Error } func (s *{{.Feature.Name}}Service) Update(r req.Update{{.Feature.Name}}Req) error { updates := map[string]any{ {{- range .Feature.Fields}} "{{.ColumnName}}": r.{{.Name}}, {{- end}} } return global.DB.Model(&model.{{.Feature.Name}}{}).Where("id = ?", r.Id).Updates(updates).Error } func (s *{{.Feature.Name}}Service) Delete(ids []string) error { return global.DB.Where("id IN ?", ids).Delete(&model.{{.Feature.Name}}{}).Error } func (s *{{.Feature.Name}}Service) Detail(id string) (data *model.{{.Feature.Name}}, err error) { var record model.{{.Feature.Name}} err = global.DB.Where("id = ?", id).First(&record).Error return &record, err } func (s *{{.Feature.Name}}Service) List(r req.List{{.Feature.Name}}Req) (list []model.{{.Feature.Name}}, total int64, err error) { if r.PageSize <= 0 { r.PageSize = 10 } if r.Current <= 0 { r.Current = 1 } db := global.DB.Model(&model.{{.Feature.Name}}{}) if r.Keyword != "" { db = db.Where("id LIKE ?", "%"+r.Keyword+"%") } if err = db.Count(&total).Error; err != nil { return } err = db.Limit(r.PageSize).Offset(r.PageSize * (r.Current - 1)).Find(&list).Error return } ` // ---------------------------------------- // 模板:service enter.go // ---------------------------------------- const serviceEnterTmpl = `package {{.PackageName}} type ServiceGroup struct { {{- range .Features}} {{.Name}}Service {{- end}} } ` // ---------------------------------------- // 模板:api handler 层(使用 request 包的结构体) // ---------------------------------------- const apiTmpl = `package {{.PackageName}} import ( "sundynix-go/global" "sundynix-go/model/commom/response" req "sundynix-go/model/{{.PackageName}}/request" "sundynix-go/service" "github.com/gin-gonic/gin" "go.uber.org/zap" ) type {{.Feature.Name}}Api struct{} var {{.LowerName}}Service = service.ServiceGroupApp.{{.PascalModuleName}}ServiceGroup.{{.Feature.Name}}Service // Save // @Tags {{.Feature.Comment}}管理 // @Summary 新增{{.Feature.Comment}} // @Security BasicAuth // @accept json // @Produce json // @Param data body req.Save{{.Feature.Name}}Req true "新增{{.Feature.Comment}}参数" // @Success 200 {object} response.Response "新增成功" // @Router /{{.RouterPrefix}}/save [post] func (a *{{.Feature.Name}}Api) Save(c *gin.Context) { var r req.Save{{.Feature.Name}}Req if err := c.ShouldBindJSON(&r); err != nil { response.FailWithMsg("参数错误:"+err.Error(), c) return } if err := {{.LowerName}}Service.Save(r); err != nil { global.Logger.Error("新增{{.Feature.Comment}}失败!", zap.Error(err)) response.FailWithMsg(err.Error(), c) return } response.OkWithMsg("新增成功", c) } // Update // @Tags {{.Feature.Comment}}管理 // @Summary 更新{{.Feature.Comment}} // @Security BasicAuth // @accept json // @Produce json // @Param data body req.Update{{.Feature.Name}}Req true "更新{{.Feature.Comment}}参数" // @Success 200 {object} response.Response "更新成功" // @Router /{{.RouterPrefix}}/update [post] func (a *{{.Feature.Name}}Api) Update(c *gin.Context) { var r req.Update{{.Feature.Name}}Req if err := c.ShouldBindJSON(&r); err != nil { response.FailWithMsg("参数错误:"+err.Error(), c) return } if err := {{.LowerName}}Service.Update(r); err != nil { global.Logger.Error("更新{{.Feature.Comment}}失败!", zap.Error(err)) response.FailWithMsg(err.Error(), c) return } response.OkWithMsg("更新成功", c) } // Delete // @Tags {{.Feature.Comment}}管理 // @Summary 删除{{.Feature.Comment}}(支持批量) // @Security BasicAuth // @accept json // @Produce json // @Param data body object{ids=[]string} true "id列表" // @Success 200 {object} response.Response "删除成功" // @Router /{{.RouterPrefix}}/delete [post] func (a *{{.Feature.Name}}Api) Delete(c *gin.Context) { var req struct { Ids []string ` + "`" + `json:"ids" binding:"required,min=1"` + "`" + ` } if err := c.ShouldBindJSON(&req); err != nil { response.FailWithMsg("参数错误:"+err.Error(), c) return } if err := {{.LowerName}}Service.Delete(req.Ids); err != nil { global.Logger.Error("删除{{.Feature.Comment}}失败!", zap.Error(err)) response.FailWithMsg(err.Error(), c) return } response.OkWithMsg("删除成功", c) } // Detail // @Tags {{.Feature.Comment}}管理 // @Summary 获取{{.Feature.Comment}}详情 // @Security BasicAuth // @Produce json // @Param id query string true "ID" // @Success 200 {object} response.Response "获取详情成功" // @Router /{{.RouterPrefix}}/detail [get] func (a *{{.Feature.Name}}Api) Detail(c *gin.Context) { id := c.Query("id") data, err := {{.LowerName}}Service.Detail(id) if err != nil { global.Logger.Error("获取{{.Feature.Comment}}详情失败!", zap.Error(err)) response.FailWithMsg(err.Error(), c) return } response.OkWithData(data, c) } // List // @Tags {{.Feature.Comment}}管理 // @Summary 分页获取{{.Feature.Comment}}列表 // @Security BasicAuth // @accept json // @Produce json // @Param data body req.List{{.Feature.Name}}Req true "分页参数" // @Success 200 {object} response.Response{data=response.PageResult} "获取列表成功" // @Router /{{.RouterPrefix}}/list [post] func (a *{{.Feature.Name}}Api) List(c *gin.Context) { var r req.List{{.Feature.Name}}Req if err := c.ShouldBindJSON(&r); err != nil { response.FailWithMsg("参数错误:"+err.Error(), c) return } list, total, err := {{.LowerName}}Service.List(r) if err != nil { global.Logger.Error("获取{{.Feature.Comment}}列表失败!", zap.Error(err)) response.FailWithMsg(err.Error(), c) return } response.OkWithData(response.PageResult{ List: list, Total: total, Page: r.Current, PageSize: r.PageSize, }, c) } ` // ---------------------------------------- // 模板:api enter.go // ---------------------------------------- const apiEnterTmpl = `package {{.PackageName}} type ApiGroup struct { {{- range .Features}} {{.Name}}Api {{- end}} } ` // ---------------------------------------- // 模板:router // ---------------------------------------- const routerTmpl = `package {{.PackageName}} import "github.com/gin-gonic/gin" type {{.Feature.Name}}Router struct{} func (r *{{.Feature.Name}}Router) Init{{.Feature.Name}}Router(Router *gin.RouterGroup) { {{.LowerName}}Router := Router.Group("{{.RouterGroup}}") { {{.LowerName}}Router.POST("save", {{.LowerName}}Api.Save) {{.LowerName}}Router.POST("update", {{.LowerName}}Api.Update) {{.LowerName}}Router.POST("delete", {{.LowerName}}Api.Delete) {{.LowerName}}Router.GET("detail", {{.LowerName}}Api.Detail) {{.LowerName}}Router.POST("list", {{.LowerName}}Api.List) } } ` // ---------------------------------------- // 模板:router enter.go // ---------------------------------------- const routerEnterTmpl = `package {{.PackageName}} import v1 "sundynix-go/api/v1" type RouterGroup struct { {{- range .Features}} {{.Name}}Router {{- end}} } var ( {{- range .Features}} {{lowerFirst .Name}}Api = v1.ApiGroupApp.{{.PascalModuleName}}ApiGroup.{{.Name}}Api {{- end}} ) ` // ---------------------------------------- // 前端模板:TypeScript 类型定义 // ---------------------------------------- const feTsTypeTmpl = `// {{.Module.Name}} 模块类型定义(自动生成,请勿手动修改) import type { BaseModel } from '@/types' {{range .Features}} // ---- {{.Comment}} ---- /** {{.Comment}}实体 */ export interface {{.Name}} extends BaseModel { {{- range .Fields}} {{.JsonTag}}: {{goTypeToTs .Type}} // {{.Comment}} {{- end}} } /** 新增{{.Comment}}请求 */ export interface Save{{.Name}}Req { {{- range .Fields}} {{.JsonTag}}{{if not .Required}}?{{end}}: {{goTypeToTs .Type}} // {{.Comment}} {{- end}} } /** 更新{{.Comment}}请求 */ export interface Update{{.Name}}Req { id: string {{- range .Fields}} {{.JsonTag}}?: {{goTypeToTs .Type}} // {{.Comment}} {{- end}} } /** 分页查询{{.Comment}}请求 */ export interface List{{.Name}}Req { current?: number pageSize?: number keyword?: string } {{end}}` // ---------------------------------------- // 前端模板:API service (Axios) // ---------------------------------------- const feApiTmpl = `import { get, post } from '@/lib/request' import type { {{.Feature.Name}}, Save{{.Feature.Name}}Req, Update{{.Feature.Name}}Req, List{{.Feature.Name}}Req, } from '@/types/{{.Module.PackageName}}' import type { PageResult } from '@/types' const BASE = '/{{.Module.PackageName}}/{{.SnakeName}}' export const {{lowerFirst .Feature.Name}}Api = { save: (data: Save{{.Feature.Name}}Req) => post(` + "`" + `${BASE}/save` + "`" + `, data), update: (data: Update{{.Feature.Name}}Req) => post(` + "`" + `${BASE}/update` + "`" + `, data), delete: (ids: string[]) => post(` + "`" + `${BASE}/delete` + "`" + `, { ids }), detail: (id: string) => get<{{.Feature.Name}}>(` + "`" + `${BASE}/detail` + "`" + `, { id }), list: (params: List{{.Feature.Name}}Req) => post>(` + "`" + `${BASE}/list` + "`" + `, params), } ` // ---------------------------------------- // 前端模板:React 管理页面 // ---------------------------------------- const fePageTmpl = `import { useState, useEffect } from 'react' import { {{lowerFirst .Feature.Name}}Api } from '@/api/{{.Module.PackageName}}/{{.SnakeName}}' import type { {{.Feature.Name}}, List{{.Feature.Name}}Req } from '@/types/{{.Module.PackageName}}' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Card, CardContent } from '@/components/ui/card' import { toast } from 'sonner' import { Plus, Pencil, Trash2, Search, RefreshCw } from 'lucide-react' export default function {{.Feature.Name}}Page() { const [list, setList] = useState<{{.Feature.Name}}[]>([]) const [total, setTotal] = useState(0) const [loading, setLoading] = useState(false) const [params, setParams] = useState({ current: 1, pageSize: 10, keyword: '' }) const load = async () => { setLoading(true) try { const res = await {{lowerFirst .Feature.Name}}Api.list(params) setList(res.list) setTotal(res.total) } catch {} finally { setLoading(false) } } useEffect(() => { load() }, [params.current]) const handleDelete = async (id: string) => { if (!confirm('确认删除?')) return try { await {{lowerFirst .Feature.Name}}Api.delete([id]) toast.success('删除成功') load() } catch {} } const totalPages = Math.ceil(total / (params.pageSize ?? 10)) return (

{{.Feature.Comment}}管理

共 {total} 条记录

setParams(p => ({ ...p, keyword: e.target.value, current: 1 }))} className="max-w-60" />
{{- range .Feature.Fields}}{{if .Comment}} {{- end}}{{end}} {loading ? ( ) : list.length === 0 ? ( ) : list.map(item => ( {{- range .Feature.Fields}}{{if .JsonTag}}{{end}}{{end}} ))}
{{.Comment}}创建时间 操作
加载中…
暂无数据
{String(item.{{.JsonTag}} ?? '—')}{new Date(item.createdAt).toLocaleString()}
{totalPages > 1 && (
第 {params.current} / {totalPages} 页
)}
) } ` // ---------------------------------------- // 辅助函数 // ---------------------------------------- func lowerFirst(s string) string { if s == "" { return s } return strings.ToLower(s[:1]) + s[1:] } func hasTimeField(feature codegenModel.Feature) bool { for _, f := range feature.Fields { if f.Type == "time.Time" { return true } } return false } // goTypeToTs 将 Go 类型映射为 TypeScript 类型 func goTypeToTs(goType string) string { switch goType { case "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64", "float32", "float64": return "number" case "bool": return "boolean" case "time.Time": return "string" default: return "string" } } // ---------------------------------------- // TestConnection 测试数据库连接 // ---------------------------------------- func (s *CodegenService) TestConnection(cfg codegenModel.DbConfig) error { _, err := openDB(cfg) return err } func openDB(cfg codegenModel.DbConfig) (*gorm.DB, error) { switch strings.ToLower(cfg.DbType) { case "mysql": dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.DbName) return gorm.Open(mysql.Open(dsn), &gorm.Config{}) case "postgres", "postgresql": dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable", cfg.Host, cfg.User, cfg.Password, cfg.DbName, cfg.Port) return gorm.Open(postgres.Open(dsn), &gorm.Config{}) case "sqlite": return gorm.Open(sqlite.Open(cfg.DbName), &gorm.Config{}) default: return nil, fmt.Errorf("不支持的数据库类型: %s", cfg.DbType) } } // ---------------------------------------- // Preview 预览生成代码 // ---------------------------------------- func (s *CodegenService) Preview(config codegenModel.GenConfig) ([]codegenModel.PreviewFile, error) { backendFiles, err := s.renderAll(config, config.OutputDir) if err != nil { return nil, err } if config.FrontendOutputDir != "" { feFiles, err := s.renderFrontend(config) if err != nil { return nil, err } backendFiles = append(backendFiles, feFiles...) } return backendFiles, nil } // ---------------------------------------- // Generate 生成并写入文件 // ---------------------------------------- func (s *CodegenService) Generate(config codegenModel.GenConfig) (*codegenModel.GenResult, error) { outputDir := config.OutputDir if outputDir == "" { cwd, _ := os.Getwd() outputDir = cwd } // 1. 渲染后端文件 backendFiles, err := s.renderAll(config, outputDir) if err != nil { return nil, err } // 2. 自动注册(必须在写入文件之前,否则 enter.go 已存在导致误判为已有模块) regMsg := "" if err := s.autoRegister(outputDir, config); err != nil { regMsg = fmt.Sprintf("⚠️ 自动注册失败: %s", err.Error()) } else { regMsg = "✅ 自动注册成功(gorm/router/enter)" } // 3. 写入后端文件(增量模式下跳过已存在文件) written, skipped := writeFilesIncremental(outputDir, backendFiles, config.Overwrite) allFiles := backendFiles // 4. 生成前端文件(如果配置了前端输出目录) if config.FrontendOutputDir != "" { feFiles, err := s.renderFrontend(config) if err != nil { return nil, err } w, sk := writeFilesIncremental(config.FrontendOutputDir, feFiles, config.Overwrite) written += w skipped += sk allFiles = append(allFiles, feFiles...) } return &codegenModel.GenResult{ OutputDir: outputDir, Files: allFiles, Message: fmt.Sprintf("写入 %d 个文件,跳过 %d 个已存在文件。%s", written, skipped, regMsg), }, nil } // writeFilesIncremental 写入文件,支持增量模式 // overwrite=false 时,已存在的文件会被跳过 // 返回 (写入数, 跳过数) func writeFilesIncremental(baseDir string, files []codegenModel.PreviewFile, overwrite bool) (written int, skipped int) { for _, f := range files { fullPath := filepath.Join(baseDir, f.FilePath) // 增量模式下跳过已存在文件(但 AlwaysOverwrite 的聚合文件始终覆盖) if !overwrite && !f.AlwaysOverwrite { if _, err := os.Stat(fullPath); err == nil { skipped++ continue } } if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil { continue } if err := os.WriteFile(fullPath, []byte(f.Content), 0644); err != nil { continue } written++ } return } // renderFrontend 渲染前端 TypeScript / React 文件 func (s *CodegenService) renderFrontend(config codegenModel.GenConfig) ([]codegenModel.PreviewFile, error) { var result []codegenModel.PreviewFile funcMap := template.FuncMap{ "lowerFirst": lowerFirst, "goTypeToTs": goTypeToTs, "not": func(b bool) bool { return !b }, } for _, module := range config.Modules { // 跳过未填写包名的模块 if strings.TrimSpace(module.PackageName) == "" || strings.TrimSpace(module.Name) == "" { continue } // ---- TypeScript 类型(模块级,所有实体合并到 index.ts)---- tsData := struct { Module codegenModel.Module Features []codegenModel.Feature }{module, module.Features} tsContent, err := renderTmpl("fe_ts_type", feTsTypeTmpl, funcMap, tsData) if err != nil { return nil, err } result = append(result, codegenModel.PreviewFile{ FilePath: filepath.Join("src", "types", module.PackageName, "index.ts"), Content: tsContent, AlwaysOverwrite: true, }) for _, feature := range module.Features { // 跳过未命名的实体 if strings.TrimSpace(feature.Name) == "" { continue } snakeName := toSnake(feature.Name) feData := struct { Module codegenModel.Module Feature codegenModel.Feature SnakeName string ColCount int }{module, feature, snakeName, len(feature.Fields) + 2} // API service apiContent, err := renderTmpl("fe_api", feApiTmpl, funcMap, feData) if err != nil { return nil, err } result = append(result, codegenModel.PreviewFile{ FilePath: filepath.Join("src", "api", module.PackageName, snakeName+".ts"), Content: apiContent, }) // React page pageContent, err := renderTmpl("fe_page", fePageTmpl, funcMap, feData) if err != nil { return nil, err } result = append(result, codegenModel.PreviewFile{ FilePath: filepath.Join("src", "pages", module.PackageName, feature.Name+"Page.tsx"), Content: pageContent, }) } } return result, nil } // ---------------------------------------- // renderAll 渲染所有模块的所有文件 // ---------------------------------------- func (s *CodegenService) renderAll(config codegenModel.GenConfig, outputDir string) ([]codegenModel.PreviewFile, error) { var result []codegenModel.PreviewFile funcMap := template.FuncMap{ "lowerFirst": lowerFirst, } for _, module := range config.Modules { // 跳过未填写包名的模块 if strings.TrimSpace(module.PackageName) == "" || strings.TrimSpace(module.Name) == "" { continue } pkg := module.PackageName pascalModule := module.Name type featureEntry struct { Name string PascalModuleName string } // 发现已存在的 feature(扫描 service/{pkg}/ 下的 *_service.go 文件) existingFeatures := discoverExistingFeatures(outputDir, pkg) // 合并:已有 feature + 当前配置中的 feature(去重) featureSet := make(map[string]bool) var allFeatureEntries []featureEntry for _, name := range existingFeatures { if !featureSet[name] { featureSet[name] = true allFeatureEntries = append(allFeatureEntries, featureEntry{name, pascalModule}) } } for _, f := range module.Features { if strings.TrimSpace(f.Name) == "" { continue } if !featureSet[f.Name] { featureSet[f.Name] = true allFeatureEntries = append(allFeatureEntries, featureEntry{f.Name, pascalModule}) } } enterData := struct { PackageName string Features []featureEntry }{pkg, allFeatureEntries} // service/enter.go(始终覆盖,包含所有已有+新增 feature) svcEnter, err := renderTmpl("service_enter", serviceEnterTmpl, funcMap, enterData) if err != nil { return nil, err } result = append(result, codegenModel.PreviewFile{ FilePath: filepath.Join("service", pkg, "enter.go"), Content: svcEnter, AlwaysOverwrite: true, }) // api/v1/enter.go apiEnter, err := renderTmpl("api_enter", apiEnterTmpl, funcMap, enterData) if err != nil { return nil, err } result = append(result, codegenModel.PreviewFile{ FilePath: filepath.Join("api", "v1", pkg, "enter.go"), Content: apiEnter, AlwaysOverwrite: true, }) // router/enter.go routerEnter, err := renderTmpl("router_enter", routerEnterTmpl, funcMap, enterData) if err != nil { return nil, err } result = append(result, codegenModel.PreviewFile{ FilePath: filepath.Join("router", pkg, "enter.go"), Content: routerEnter, AlwaysOverwrite: true, }) // ---- request 结构体(按 feature 分文件,增量安全)---- for _, feature := range module.Features { if strings.TrimSpace(feature.Name) == "" { continue } reqData := struct { PackageName string Features []codegenModel.Feature }{pkg, []codegenModel.Feature{feature}} reqContent, err := renderTmpl("request", requestTmpl, funcMap, reqData) if err != nil { return nil, err } result = append(result, codegenModel.PreviewFile{ FilePath: filepath.Join("model", pkg, "request", toSnake(feature.Name)+"_request.go"), Content: reqContent, }) } // 每个 Feature 生成:model / service / api / router for _, feature := range module.Features { // 跳过未命名的实体 if strings.TrimSpace(feature.Name) == "" { continue } lower := lowerFirst(feature.Name) snakeName := toSnake(feature.Name) routerGroup := snakeName routerPrefix := pkg + "/" + snakeName // ---- model ---- modelData := struct { PackageName string Feature codegenModel.Feature HasTimeField bool }{pkg, feature, hasTimeField(feature)} modelContent, err := renderTmpl("model", modelTmpl, funcMap, modelData) if err != nil { return nil, err } result = append(result, codegenModel.PreviewFile{ FilePath: filepath.Join("model", pkg, snakeName+".go"), Content: modelContent, }) // ---- service ---- svcData := struct { PackageName string Feature codegenModel.Feature }{pkg, feature} svcContent, err := renderTmpl("service", serviceTmpl, funcMap, svcData) if err != nil { return nil, err } result = append(result, codegenModel.PreviewFile{ FilePath: filepath.Join("service", pkg, snakeName+"_service.go"), Content: svcContent, }) // ---- api handler ---- apiData := struct { PackageName string Feature codegenModel.Feature LowerName string RouterPrefix string PascalModuleName string }{pkg, feature, lower, routerPrefix, pascalModule} apiContent, err := renderTmpl("api", apiTmpl, funcMap, apiData) if err != nil { return nil, err } result = append(result, codegenModel.PreviewFile{ FilePath: filepath.Join("api", "v1", pkg, snakeName+".go"), Content: apiContent, }) // ---- router ---- routerData := struct { PackageName string Feature codegenModel.Feature LowerName string RouterGroup string }{pkg, feature, lower, routerGroup} routerContent, err := renderTmpl("router", routerTmpl, funcMap, routerData) if err != nil { return nil, err } result = append(result, codegenModel.PreviewFile{ FilePath: filepath.Join("router", pkg, snakeName+"_router.go"), Content: routerContent, }) } } return result, nil } func renderTmpl(name, tmplStr string, funcMap template.FuncMap, data any) (string, error) { t, err := template.New(name).Funcs(funcMap).Parse(tmplStr) if err != nil { return "", fmt.Errorf("解析模板 [%s] 失败: %w", name, err) } var buf bytes.Buffer if err = t.Execute(&buf, data); err != nil { return "", fmt.Errorf("渲染模板 [%s] 失败: %w", name, err) } return buf.String(), nil } // toSnake 将 PascalCase 转为 snake_case func toSnake(s string) string { var result []rune for i, r := range s { if r >= 'A' && r <= 'Z' { if i > 0 { result = append(result, '_') } result = append(result, r+32) } else { result = append(result, r) } } return string(result) }