package system import ( "context" "encoding/json" "fmt" "go/ast" "go/format" "go/parser" "go/token" "os" "path/filepath" "strings" "text/template" "github.com/go-kratos/kratos/v2/log" "github.com/pkg/errors" "gorm.io/gorm" "kra/internal/conf" "kra/pkg/utils" "kra/pkg/utils/autocode" ) // AutoCodeTemplateRepo 自动代码模板仓储接口 type AutoCodeTemplateRepo interface { GetPackageByName(ctx context.Context, packageName string) (*SysAutoCodePackage, error) CreateApis(ctx context.Context, apis []SysApi) ([]uint, error) GetMenuByName(ctx context.Context, name string) (*SysBaseMenu, error) CreateMenu(ctx context.Context, menu *SysBaseMenu) error CreateExportTemplate(ctx context.Context, tpl *SysExportTemplate) error Repeat(ctx context.Context, businessDB, structName, abbreviation, pkg string) bool } // AutoCodeTemplateUsecase 自动代码模板用例 type AutoCodeTemplateUsecase struct { repo AutoCodeTemplateRepo historyRepo AutoCodeHistoryRepo packageRepo AutoCodePackageRepo config *conf.AutoCodeConfig log *log.Helper db *gorm.DB } // NewAutoCodeTemplateUsecase 创建自动代码模板用例 func NewAutoCodeTemplateUsecase( repo AutoCodeTemplateRepo, historyRepo AutoCodeHistoryRepo, packageRepo AutoCodePackageRepo, config *conf.AutoCodeConfig, logger log.Logger, db *gorm.DB, ) *AutoCodeTemplateUsecase { return &AutoCodeTemplateUsecase{ repo: repo, historyRepo: historyRepo, packageRepo: packageRepo, config: config, log: log.NewHelper(logger), db: db, } } // checkPackage 检查包结构是否完整 // 支持 Kratos DDD 层次结构检查: // - biz/{package}/enter.go - 业务逻辑层入口 // - data/{package}/enter.go - 数据访问层入口 // - service/{package}/enter.go - 服务层入口 // - server/handler/{package}/enter.go - HTTP Handler 层入口 // - server/router/{package}/enter.go - 路由层入口 func (uc *AutoCodeTemplateUsecase) checkPackage(pkg string, template string) error { switch template { case "package": // Kratos DDD 架构目录检查 // 检查 Biz 层 bizEnter := filepath.Join(uc.config.Root, uc.config.Server, "internal", "biz", pkg, "enter.go") if _, err := os.Stat(bizEnter); err != nil { return fmt.Errorf("package结构异常,缺少internal/biz/%s/enter.go", pkg) } // 检查 Data 层 dataEnter := filepath.Join(uc.config.Root, uc.config.Server, "internal", "data", pkg, "enter.go") if _, err := os.Stat(dataEnter); err != nil { return fmt.Errorf("package结构异常,缺少internal/data/%s/enter.go", pkg) } // 检查 Service 层 serviceEnter := filepath.Join(uc.config.Root, uc.config.Server, "internal", "service", pkg, "enter.go") if _, err := os.Stat(serviceEnter); err != nil { return fmt.Errorf("package结构异常,缺少internal/service/%s/enter.go", pkg) } // 检查 Handler 层 handlerEnter := filepath.Join(uc.config.Root, uc.config.Server, "internal", "server", "handler", pkg, "enter.go") if _, err := os.Stat(handlerEnter); err != nil { return fmt.Errorf("package结构异常,缺少internal/server/handler/%s/enter.go", pkg) } // 检查 Router 层 routerEnter := filepath.Join(uc.config.Root, uc.config.Server, "internal", "server", "router", pkg, "enter.go") if _, err := os.Stat(routerEnter); err != nil { return fmt.Errorf("package结构异常,缺少internal/server/router/%s/enter.go", pkg) } case "plugin": pluginEnter := filepath.Join(uc.config.Root, uc.config.Server, "internal", "plugin", pkg, "plugin.go") if _, err := os.Stat(pluginEnter); err != nil { return fmt.Errorf("plugin结构异常,缺少internal/plugin/%s/plugin.go", pkg) } } return nil } // Create 创建生成自动化代码 func (uc *AutoCodeTemplateUsecase) Create(ctx context.Context, info *AutoCodeInfo) error { // 查询包信息 autoPkg, err := uc.repo.GetPackageByName(ctx, info.Package) if err != nil { return errors.Wrap(err, "查询包失败!") } // 检查包结构 if err = uc.checkPackage(info.Package, autoPkg.Template); err != nil { return err } // 检查重复创建 if uc.repo.Repeat(ctx, info.BusinessDB, info.StructName, info.Abbreviation, info.Package) { return errors.New("已经创建过此数据结构,请勿重复创建!") } // 生成代码 generate, templates, injections, err := uc.generate(ctx, info, autoPkg) if err != nil { return err } // 写入文件 for key, builder := range generate { if err = os.MkdirAll(filepath.Dir(key), os.ModePerm); err != nil { return errors.Wrapf(err, "[filepath:%s]创建文件夹失败!", key) } if err = os.WriteFile(key, []byte(builder.String()), 0666); err != nil { return errors.Wrapf(err, "[filepath:%s]写入文件失败!", key) } } // 创建历史记录 history := &SysAutoCodeHistory{ Table: info.TableName, Package: info.Package, StructName: info.StructName, Abbreviation: info.Abbreviation, BusinessDB: info.BusinessDB, Description: info.Description, Templates: templates, } // 自动创建API if info.AutoCreateApiToSql && !info.OnlyTemplate { apis := uc.buildApis(info) apiIDs, err := uc.repo.CreateApis(ctx, apis) if err != nil { return err } history.ApiIDs = apiIDs } // 自动创建菜单 if info.AutoCreateMenuToSql { menu, err := uc.repo.GetMenuByName(ctx, info.Abbreviation) if err == nil && menu != nil { history.MenuID = menu.ID } else { newMenu := uc.buildMenu(info, autoPkg.Template) if info.AutoCreateBtnAuth && !info.OnlyTemplate { newMenu.MenuBtn = uc.buildMenuBtns(info) } if err = uc.repo.CreateMenu(ctx, newMenu); err != nil { return errors.Wrap(err, "创建菜单失败!") } history.MenuID = newMenu.ID } } // 处理Excel导出模板 if info.HasExcel { exportTpl := uc.buildExportTemplate(info) if err = uc.repo.CreateExportTemplate(ctx, exportTpl); err != nil { return err } history.ExportTemplateID = exportTpl.ID } // 保存注入信息 history.Injections = make(map[string]string, len(injections)) for key, value := range injections { bytes, _ := json.Marshal(value) history.Injections[key] = string(bytes) } // 创建历史记录 return uc.historyRepo.Create(ctx, history) } // Preview 预览自动化代码 func (uc *AutoCodeTemplateUsecase) Preview(ctx context.Context, info *AutoCodeInfo) (map[string]string, error) { // 查询包信息 entity, err := uc.repo.GetPackageByName(ctx, info.Package) if err != nil { return nil, errors.Wrap(err, "查询包失败!") } // 检查重复创建 if uc.repo.Repeat(ctx, info.BusinessDB, info.StructName, info.Abbreviation, info.Package) && !info.IsAdd { return nil, errors.New("已经创建过此数据结构或重复简称,请勿重复创建!") } preview := make(map[string]string) codes, _, _, err := uc.generate(ctx, info, entity) if err != nil { return nil, err } for key, writer := range codes { if len(key) > len(uc.config.Root) { key, _ = filepath.Rel(uc.config.Root, key) } suffix := filepath.Ext(key)[1:] var builder strings.Builder builder.WriteString("```" + suffix + "\n\n") builder.WriteString(writer.String()) builder.WriteString("\n\n```") preview[key] = builder.String() } return preview, nil } // generate 生成代码 func (uc *AutoCodeTemplateUsecase) generate(ctx context.Context, info *AutoCodeInfo, entity *SysAutoCodePackage) (map[string]strings.Builder, map[string]string, map[string]interface{}, error) { dataList, asts, fileList, err := uc.packageRepo.Templates(ctx, entity, info, false) if err != nil { return nil, nil, nil, err } code := make(map[string]strings.Builder) for templatePath := range dataList { // 获取目标文件路径 targetPath, ok := fileList[templatePath] if !ok { continue } // 解析并执行模板 files, err := template.New(filepath.Base(templatePath)).Funcs(autocode.GetTemplateFuncMap()).ParseFiles(templatePath) if err != nil { return nil, nil, nil, errors.Wrapf(err, "[filepath:%s]读取模版文件失败!", templatePath) } var builder strings.Builder if err = files.Execute(&builder, info); err != nil { return nil, nil, nil, errors.Wrapf(err, "[filepath:%s]生成文件失败!", targetPath) } code[targetPath] = builder } injections := make(map[string]interface{}, len(asts)) for key, value := range asts { keys := strings.Split(key, "=>") if len(keys) == 2 { if keys[1] == TypePluginInitializeV2 { continue } if info.OnlyTemplate { if keys[1] == TypePackageInitializeGorm || keys[1] == TypePluginInitializeGorm { continue } } if !info.AutoMigrate { if keys[1] == TypePackageInitializeGorm || keys[1] == TypePluginInitializeGorm { continue } } var builder strings.Builder parse, _ := value.Parse("", &builder) if parse != nil { _ = value.Injection(parse) if err = value.Format("", &builder, parse); err != nil { return nil, nil, nil, err } code[keys[0]] = builder injections[keys[1]] = value fmt.Println(keys[0], "注入成功!") } } } return code, fileList, injections, nil } // AddFunc 添加函数 func (uc *AutoCodeTemplateUsecase) AddFunc(info *AutoFuncInfo) error { autoPkg, err := uc.packageRepo.FindByPackageName(context.Background(), info.Package) if err != nil { return err } if autoPkg.Template != "package" { info.IsPlugin = true } if err = uc.addTemplateToFile("api.go", info); err != nil { return err } if err = uc.addTemplateToFile("server.go", info); err != nil { return err } if err = uc.addTemplateToFile("api.js", info); err != nil { return err } return uc.addTemplateToAst("router", info) } // GetApiAndServer 获取API和Server代码 func (uc *AutoCodeTemplateUsecase) GetApiAndServer(info *AutoFuncInfo) (map[string]string, error) { autoPkg, err := uc.packageRepo.FindByPackageName(context.Background(), info.Package) if err != nil { return nil, err } if autoPkg.Template != "package" { info.IsPlugin = true } apiStr, err := uc.getTemplateStr("api.go", info) if err != nil { return nil, err } serverStr, err := uc.getTemplateStr("server.go", info) if err != nil { return nil, err } jsStr, err := uc.getTemplateStr("api.js", info) if err != nil { return nil, err } return map[string]string{"api": apiStr, "server": serverStr, "js": jsStr}, nil } // getTemplateStr 获取模板字符串 func (uc *AutoCodeTemplateUsecase) getTemplateStr(t string, info *AutoFuncInfo) (string, error) { tempPath := filepath.Join(uc.config.Root, uc.config.Server, "resource", "function", t+".tpl") files, err := template.New(filepath.Base(tempPath)).Funcs(autocode.GetTemplateFuncMap()).ParseFiles(tempPath) if err != nil { return "", errors.Wrapf(err, "[filepath:%s]读取模版文件失败!", tempPath) } var builder strings.Builder if err = files.Execute(&builder, info); err != nil { return "", errors.Wrapf(err, "[filepath:%s]生成文件失败!", tempPath) } return builder.String(), nil } // addTemplateToAst 添加模板到AST // 支持 Kratos DDD 架构的路由注入 func (uc *AutoCodeTemplateUsecase) addTemplateToAst(t string, info *AutoFuncInfo) error { // Kratos DDD 架构:路由文件在 internal/server/router/{package}/ tPath := filepath.Join(uc.config.Root, uc.config.Server, "internal", "server", "router", info.Package, info.HumpPackageName+".go") funcName := fmt.Sprintf("Init%sRouter", info.StructName) routerStr := "RouterWithoutAuth" if info.IsAuth { routerStr = "Router" } stmtStr := fmt.Sprintf("%s%s.%s(\"%s\", %sApi.%s)", info.Abbreviation, routerStr, info.Method, info.Router, info.Abbreviation, info.FuncName) if info.IsPlugin { tPath = filepath.Join(uc.config.Root, uc.config.Server, "internal", "plugin", info.Package, "router", info.HumpPackageName+".go") stmtStr = fmt.Sprintf("group.%s(\"%s\", api%s.%s)", info.Method, info.Router, info.StructName, info.FuncName) funcName = "Init" } src, err := os.ReadFile(tPath) if err != nil { return err } fileSet := token.NewFileSet() astFile, err := parser.ParseFile(fileSet, "", src, 0) if err != nil { return err } funcDecl := utils.FindFunction(astFile, funcName) stmtNode := utils.CreateStmt(stmtStr) if info.IsAuth { for i := 0; i < len(funcDecl.Body.List); i++ { st := funcDecl.Body.List[i] if blockStmt, ok := st.(*ast.BlockStmt); ok { blockStmt.List = append(blockStmt.List, stmtNode) break } } } else { for i := len(funcDecl.Body.List) - 1; i >= 0; i-- { st := funcDecl.Body.List[i] if blockStmt, ok := st.(*ast.BlockStmt); ok { blockStmt.List = append(blockStmt.List, stmtNode) break } } } f, err := os.Create(tPath) if err != nil { return err } defer f.Close() return format.Node(f, fileSet, astFile) } // addTemplateToFile 添加模板到文件 // 支持 Kratos DDD 架构的文件路径: // - handler: internal/server/handler/{package}/ // - service: internal/service/{package}/ // - api.ts: web/src/services/kratos/ func (uc *AutoCodeTemplateUsecase) addTemplateToFile(t string, info *AutoFuncInfo) error { getTemplateStr, err := uc.getTemplateStr(t, info) if err != nil { return err } var target string switch t { case "api.go": // Kratos DDD 架构:Handler 在 internal/server/handler/{package}/ if info.IsAi && info.ApiFunc != "" { getTemplateStr = info.ApiFunc } target = filepath.Join(uc.config.Root, uc.config.Server, "internal", "server", "handler", info.Package, info.HumpPackageName+".go") case "server.go": // Kratos DDD 架构:Service 在 internal/service/{package}/ if info.IsAi && info.ServerFunc != "" { getTemplateStr = info.ServerFunc } target = filepath.Join(uc.config.Root, uc.config.Server, "internal", "service", info.Package, info.HumpPackageName+".go") case "api.js": // Kratos DDD 架构:前端 API 在 web/src/services/kratos/ if info.IsAi && info.JsFunc != "" { getTemplateStr = info.JsFunc } target = filepath.Join(uc.config.Root, uc.config.Web, "src", "services", "kratos", info.PackageName+".ts") } if info.IsPlugin { switch t { case "api.go": target = filepath.Join(uc.config.Root, uc.config.Server, "internal", "plugin", info.Package, "api", info.HumpPackageName+".go") case "server.go": target = filepath.Join(uc.config.Root, uc.config.Server, "internal", "plugin", info.Package, "service", info.HumpPackageName+".go") case "api.js": target = filepath.Join(uc.config.Root, uc.config.Web, "src", "plugin", info.Package, "api", info.PackageName+".ts") } } file, err := os.OpenFile(target, os.O_WRONLY|os.O_APPEND, 0644) if err != nil { return err } defer file.Close() _, err = fmt.Fprintln(file, getTemplateStr) return err } // buildApis 构建API列表 func (uc *AutoCodeTemplateUsecase) buildApis(info *AutoCodeInfo) []SysApi { apis := []SysApi{ {Path: "/" + info.Abbreviation + "/" + "create" + info.StructName, Description: "新增" + info.Description, ApiGroup: info.Description, Method: "POST"}, {Path: "/" + info.Abbreviation + "/" + "delete" + info.StructName, Description: "删除" + info.Description, ApiGroup: info.Description, Method: "DELETE"}, {Path: "/" + info.Abbreviation + "/" + "delete" + info.StructName + "ByIds", Description: "批量删除" + info.Description, ApiGroup: info.Description, Method: "DELETE"}, {Path: "/" + info.Abbreviation + "/" + "update" + info.StructName, Description: "更新" + info.Description, ApiGroup: info.Description, Method: "PUT"}, {Path: "/" + info.Abbreviation + "/" + "find" + info.StructName, Description: "根据ID获取" + info.Description, ApiGroup: info.Description, Method: "GET"}, {Path: "/" + info.Abbreviation + "/" + "get" + info.StructName + "List", Description: "获取" + info.Description + "列表", ApiGroup: info.Description, Method: "GET"}, } if info.HasExcel { apis = append(apis, []SysApi{ {Path: "/" + info.Abbreviation + "/" + "export" + info.StructName + "Excel", Description: "导出" + info.Description + "Excel", ApiGroup: info.Description, Method: "GET"}, {Path: "/" + info.Abbreviation + "/" + "import" + info.StructName + "Excel", Description: "导入" + info.Description + "Excel", ApiGroup: info.Description, Method: "POST"}, {Path: "/" + info.Abbreviation + "/" + "download" + info.StructName + "Template", Description: "下载" + info.Description + "模板", ApiGroup: info.Description, Method: "GET"}, }...) } return apis } // buildMenu 构建菜单 // 支持 Kratos DDD 架构的前端组件路径: // - package: pages/{package}/{entity}/index // - plugin: plugin/{package}/view/{entity} func (uc *AutoCodeTemplateUsecase) buildMenu(info *AutoCodeInfo, template string) *SysBaseMenu { // Kratos DDD 架构:React 页面在 pages/{package}/{entity}/ component := "pages/" + info.Package + "/" + info.PackageName + "/index" if template == "plugin" { component = "plugin/" + info.Package + "/view/" + info.PackageName } return &SysBaseMenu{ ParentId: 0, Path: info.Abbreviation, Name: info.Abbreviation, Hidden: false, Component: component, Sort: 0, Meta: Meta{ Title: info.Description, Icon: "menu", }, } } // buildMenuBtns 构建菜单按钮 func (uc *AutoCodeTemplateUsecase) buildMenuBtns(info *AutoCodeInfo) []SysBaseMenuBtn { btns := []SysBaseMenuBtn{ {Name: "add", Desc: "新增"}, {Name: "batchDelete", Desc: "批量删除"}, {Name: "delete", Desc: "删除"}, {Name: "edit", Desc: "编辑"}, {Name: "info", Desc: "详情"}, } if info.HasExcel { btns = append(btns, []SysBaseMenuBtn{ {Name: "exportTemplate", Desc: "导出模板"}, {Name: "exportExcel", Desc: "导出Excel"}, {Name: "importExcel", Desc: "导入Excel"}, }...) } return btns } // buildExportTemplate 构建导出模板 func (uc *AutoCodeTemplateUsecase) buildExportTemplate(info *AutoCodeInfo) *SysExportTemplate { fieldsMap := make(map[string]string, len(info.Fields)) for _, field := range info.Fields { if field.Excel { fieldsMap[field.ColumnName] = field.FieldDesc } } templateInfo, _ := json.Marshal(fieldsMap) return &SysExportTemplate{ DBName: info.BusinessDB, Name: info.Package + "_" + info.StructName, Table: info.TableName, TemplateID: info.Package + "_" + info.StructName, TemplateInfo: string(templateInfo), } } // AST类型常量 const ( TypePackageApiEnter = "PackageApiEnter" TypePackageRouterEnter = "PackageRouterEnter" TypePackageServiceEnter = "PackageServiceEnter" TypePackageInitializeGorm = "PackageInitializeGorm" TypePackageInitializeRouter = "PackageInitializeRouter" TypePackageGormGen = "PackageGormGen" TypePluginGen = "PluginGen" TypePluginApiEnter = "PluginApiEnter" TypePluginInitializeV2 = "PluginInitializeV2" TypePluginRouterEnter = "PluginRouterEnter" TypePluginServiceEnter = "PluginServiceEnter" TypePluginInitializeGorm = "PluginInitializeGorm" TypePluginInitializeRouter = "PluginInitializeRouter" ) // AutoCodeInfo 相关实体类型 type AutoCodeInfo struct { Package string PackageT string TableName string BusinessDB string StructName string PackageName string Description string Abbreviation string HumpPackageName string GvaModel bool AutoMigrate bool AutoCreateResource bool AutoCreateApiToSql bool AutoCreateMenuToSql bool AutoCreateBtnAuth bool OnlyTemplate bool IsTree bool TreeJson string IsAdd bool Fields []*AutoCodeField GenerateWeb bool GenerateServer bool Module string DictTypes []string PrimaryField *AutoCodeField DataSourceMap map[string]*DataSource HasPic bool HasFile bool HasTimer bool NeedSort bool NeedJSON bool HasRichText bool HasDataSource bool HasSearchTimer bool HasArray bool HasExcel bool CustomMethods []*CustomMethod // 自定义查询方法 HasCustomMethods bool // 是否有自定义查询方法 } // CustomMethod 自定义查询方法 // 用于在 GORM Gen 生成的代码中添加自定义查询方法 type CustomMethod struct { Name string `json:"name"` // 方法名称 (如: FindByName, GetActiveUsers) Description string `json:"description"` // 方法描述 ReturnType string `json:"returnType"` // 返回类型: single(单个), list(列表), count(计数), exists(存在检查) Params []*CustomMethodParam `json:"params"` // 方法参数 Conditions []*CustomCondition `json:"conditions"` // 查询条件 OrderBy string `json:"orderBy"` // 排序字段 (如: created_at DESC) Limit int `json:"limit"` // 限制数量 (0表示不限制) } // CustomMethodParam 自定义方法参数 type CustomMethodParam struct { Name string `json:"name"` // 参数名称 Type string `json:"type"` // 参数类型 (string, int, uint, bool, time.Time等) FieldName string `json:"fieldName"` // 对应的字段名 (用于生成查询条件) } // CustomCondition 自定义查询条件 type CustomCondition struct { FieldName string `json:"fieldName"` // 字段名 Operator string `json:"operator"` // 操作符: eq, neq, gt, gte, lt, lte, like, in, between, isNull, isNotNull ParamName string `json:"paramName"` // 参数名 (对应 CustomMethodParam.Name) } // DataSource 数据源 type DataSource struct { DBName string `json:"dbName"` Table string `json:"table"` Label string `json:"label"` Value string `json:"value"` Association int `json:"association"` HasDeletedAt bool `json:"hasDeletedAt"` } // AutoCodeField 自动代码字段 type AutoCodeField struct { FieldName string `json:"fieldName"` FieldDesc string `json:"fieldDesc"` FieldType string `json:"fieldType"` FieldJson string `json:"fieldJson"` DataTypeLong string `json:"dataTypeLong"` Comment string `json:"comment"` ColumnName string `json:"columnName"` FieldSearchType string `json:"fieldSearchType"` FieldSearchHide bool `json:"fieldSearchHide"` DictType string `json:"dictType"` Form bool `json:"form"` Table bool `json:"table"` Desc bool `json:"desc"` Excel bool `json:"excel"` Require bool `json:"require"` DefaultValue string `json:"defaultValue"` ErrorText string `json:"errorText"` Clearable bool `json:"clearable"` Sort bool `json:"sort"` PrimaryKey bool `json:"primaryKey"` DataSource *DataSource `json:"dataSource"` CheckDataSource bool `json:"checkDataSource"` FieldIndexType string `json:"fieldIndexType"` } // AutoFuncInfo 自动函数信息 type AutoFuncInfo struct { Package string FuncName string Router string FuncDesc string BusinessDB string StructName string PackageName string Description string Abbreviation string HumpPackageName string Method string IsPlugin bool IsAuth bool IsPreview bool IsAi bool ApiFunc string ServerFunc string JsFunc string } // SysExportTemplate 导出模板 type SysExportTemplate struct { ID uint `json:"ID" gorm:"primarykey"` DBName string `json:"dbName" gorm:"column:db_name;comment:数据库名"` Name string `json:"name" gorm:"column:name;comment:模板名称"` Table string `json:"tableName" gorm:"column:table_name;comment:表名"` TemplateID string `json:"templateId" gorm:"column:template_id;comment:模板ID"` TemplateInfo string `json:"templateInfo" gorm:"column:template_info;type:text;comment:模板信息"` } func (SysExportTemplate) TableName() string { return "sys_export_templates" }