Files
go-admin/app/other/apis/tools/gen.go
T
zhangwenjian d89e0cd8eb chore⬆️: upgrade go-admin-core to v2.11.0
v2.11.0 makes pkg.FileCreate return its error instead of ending the
process when the file cannot be created. Two comments in the code
generator described the old behaviour in the present tense.
2026-09-27 14:01:48 +08:00

568 lines
18 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package tools
import (
"bytes"
"fmt"
"go-admin/app/admin/service"
"go-admin/app/admin/service/dto"
"os"
"path/filepath"
"strconv"
"strings"
"text/template"
"time"
"github.com/gin-gonic/gin"
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
"github.com/go-admin-team/go-admin-core/v2/sdk/config"
"github.com/go-admin-team/go-admin-core/v2/sdk/pkg"
"go-admin/app/other/models/tools"
)
type Gen struct {
api.Api
}
// genLangFuncs backs the lang-zh/lang-en templates (PRD 010 F3/F9). The
// generated files are TypeScript, and go-admin-ui's eslint config requires
// single-quoted strings with no trailing comma (@stylistic/quotes,
// @stylistic/comma-dangle: never) - text/template's builtin `printf "%q"`
// only produces Go/JSON-style double-quoted output, so this supplies a
// single-quote equivalent instead of leaning on the builtin.
var genLangFuncs = template.FuncMap{
"singleQuote": func(s string) string {
r := strings.NewReplacer(`\`, `\\`, `'`, `\'`, "\n", `\n`, "\r", `\r`)
return "'" + r.Replace(s) + "'"
},
}
// parseGenTemplate is template.ParseFiles plus genLangFuncs, for the two
// language-pack templates. template.New's name must match the file's base
// name - ParseFiles reuses the template already registered under that name
// instead of creating an unnamed second one, which is what makes Execute
// find the parsed content afterwards.
func parseGenTemplate(path string) (*template.Template, error) {
base := path[strings.LastIndex(path, "/")+1:]
return template.New(base).Funcs(genLangFuncs).ParseFiles(path)
}
func (e Gen) Preview(c *gin.Context) {
e.Context = c
log := e.GetLogger()
table := tools.SysTables{}
id, err := pkg.StringToInt(c.Param("tableId"))
if err != nil {
log.Error(err)
e.Error(500, err, fmt.Sprintf("tableId接收失败!错误详情:%s", err.Error()))
return
}
table.TableId = id
t1, err := template.ParseFiles("template/v4/model.go.template")
if err != nil {
log.Error(err)
e.Error(500, err, fmt.Sprintf("model模版读取失败!错误详情:%s", err.Error()))
return
}
t2, err := template.ParseFiles("template/v4/no_actions/apis.go.template")
if err != nil {
log.Error(err)
e.Error(500, err, fmt.Sprintf("api模版读取失败!错误详情:%s", err.Error()))
return
}
t3, err := template.ParseFiles("template/v4/ts.go.template")
if err != nil {
log.Error(err)
e.Error(500, err, fmt.Sprintf("ts模版读取失败!错误详情:%s", err.Error()))
return
}
t4, err := template.ParseFiles("template/v4/vue.go.template")
if err != nil {
log.Error(err)
e.Error(500, err, fmt.Sprintf("vue模版读取失败!错误详情:%s", err.Error()))
return
}
t5, err := template.ParseFiles("template/v4/no_actions/router_check_role.go.template")
if err != nil {
log.Error(err)
e.Error(500, err, fmt.Sprintf("路由模版读取失败!错误详情:%s", err.Error()))
return
}
t6, err := template.ParseFiles("template/v4/dto.go.template")
if err != nil {
log.Error(err)
e.Error(500, err, fmt.Sprintf("dto模版读取失败!错误详情:%s", err.Error()))
return
}
t7, err := template.ParseFiles("template/v4/no_actions/service.go.template")
if err != nil {
log.Error(err)
e.Error(500, err, fmt.Sprintf("service模版读取失败!错误详情:%s", err.Error()))
return
}
// t8/t9 back F3/F9 (PRD 010): one language pack per locale, nested under
// gen/{PackageName}/{BusinessName}.ts by NOActionsGen below so go-admin-ui's
// gen-namespace.ts glob (`./*/*.ts` under each locale's gen/) picks them up.
// See docs-prd/010-代码生成器前端模板迁移Vue3/API契约.md §2.3.
t8, err := parseGenTemplate("template/v4/lang-zh.go.template")
if err != nil {
log.Error(err)
e.Error(500, err, fmt.Sprintf("zh语言包模版读取失败!错误详情:%s", err.Error()))
return
}
t9, err := parseGenTemplate("template/v4/lang-en.go.template")
if err != nil {
log.Error(err)
e.Error(500, err, fmt.Sprintf("en语言包模版读取失败!错误详情:%s", err.Error()))
return
}
db, err := pkg.GetOrm(c)
if err != nil {
log.Errorf("get db connection error, %s", err.Error())
e.Error(500, err, fmt.Sprintf("数据库链接获取失败!错误详情:%s", err.Error()))
return
}
tab, err := table.Get(db, false)
if err != nil {
log.Errorf("get table error, %s", err.Error())
e.Error(500, err, fmt.Sprintf("读取表配置失败!错误详情:%s", err.Error()))
return
}
// MLTBName (table_name with underscores turned to dashes) is a gorm:"-"
// field - table.Get never fills it in, so every template that reads it
// (the .vue/.ts import paths, e.g. "@/api/{PackageName}/{MLTBName}")
// silently rendered it empty here. NOActionsGen has set this since it
// existed (see below); Preview never did, which is why the two paths
// are not interchangeable stand-ins for each other and should not be
// assumed to be.
tab.MLTBName = strings.Replace(tab.TBName, "_", "-", -1)
if err := requireSinglePrimaryKey(tab); err != nil {
e.Error(500, err, err.Error())
return
}
// R2: infer a width for any column the config page left at colWidth's 0
// sentinel, before vue.go.template reads .ColWidth - see column_width.go.
applyInferredColumnWidths(tab.Columns)
out, err := renderAll(tab, t1, t2, t3, t4, t5, t6, t7, t8, t9)
if err != nil {
log.Error(err)
e.Error(500, err, fmt.Sprintf("模版渲染失败!错误详情:%s", err.Error()))
return
}
mp := make(map[string]interface{})
mp["template/model.go.template"] = string(out[0])
mp["template/api.go.template"] = string(out[1])
mp["template/api.ts.template"] = string(out[2])
mp["template/vue.go.template"] = string(out[3])
mp["template/router.go.template"] = string(out[4])
mp["template/dto.go.template"] = string(out[5])
mp["template/service.go.template"] = string(out[6])
mp["template/lang-zh.go.template"] = string(out[7])
mp["template/lang-en.go.template"] = string(out[8])
e.OK(mp, "")
}
func (e Gen) GenCode(c *gin.Context) {
e.Context = c
log := e.GetLogger()
table := tools.SysTables{}
id, err := pkg.StringToInt(c.Param("tableId"))
if err != nil {
log.Error(err)
e.Error(500, err, fmt.Sprintf("tableId参数接收失败!错误详情:%s", err.Error()))
return
}
db, err := pkg.GetOrm(c)
if err != nil {
log.Errorf("get db connection error, %s", err.Error())
e.Error(500, err, fmt.Sprintf("数据库链接获取失败!错误详情:%s", err.Error()))
return
}
table.TableId = id
tab, err := table.Get(db, false)
if err != nil {
log.Errorf("get table error, %s", err.Error())
e.Error(500, err, fmt.Sprintf("读取表配置失败!错误详情:%s", err.Error()))
return
}
if !e.NOActionsGen(c, tab) {
return
}
e.OK("", "Code generated successfully!")
}
func (e Gen) GenApiToFile(c *gin.Context) {
e.Context = c
log := e.GetLogger()
table := tools.SysTables{}
id, err := pkg.StringToInt(c.Param("tableId"))
if err != nil {
log.Error(err)
e.Error(500, err, fmt.Sprintf("tableId参数获取失败!错误详情:%s", err.Error()))
return
}
db, err := pkg.GetOrm(c)
if err != nil {
log.Errorf("get db connection error, %s", err.Error())
e.Error(500, err, fmt.Sprintf("数据库链接获取失败!错误详情:%s", err.Error()))
return
}
table.TableId = id
tab, err := table.Get(db, false)
if err != nil {
log.Errorf("get table error, %s", err.Error())
e.Error(500, err, fmt.Sprintf("读取表配置失败!错误详情:%s", err.Error()))
return
}
if !e.genApiToFile(c, tab) {
return
}
e.OK("", "Code generated successfully!")
}
// NOActionsGen renders every template for tab and writes the files, and
// reports whether it did. On false it has already written the error response,
// and nothing has been written to disk if a template failed to render.
func (e Gen) NOActionsGen(c *gin.Context, tab tools.SysTables) bool {
e.Context = c
log := e.GetLogger()
tab.MLTBName = strings.Replace(tab.TBName, "_", "-", -1)
if err := requireSinglePrimaryKey(tab); err != nil {
e.Error(500, err, err.Error())
return false
}
// R2: see the matching call and comment in Preview above.
applyInferredColumnWidths(tab.Columns)
basePath := "template/v4/"
routerFile := basePath + "no_actions/router_check_role.go.template"
if tab.IsAuth == 2 {
routerFile = basePath + "no_actions/router_no_check_role.go.template"
}
t1, err := template.ParseFiles(basePath + "model.go.template")
if err != nil {
log.Error(err)
e.Error(500, err, fmt.Sprintf("model模版读取失败!错误详情:%s", err.Error()))
return false
}
t2, err := template.ParseFiles(basePath + "no_actions/apis.go.template")
if err != nil {
log.Error(err)
e.Error(500, err, fmt.Sprintf("api模版读取失败!错误详情:%s", err.Error()))
return false
}
t3, err := template.ParseFiles(routerFile)
if err != nil {
log.Error(err)
e.Error(500, err, fmt.Sprintf("路由模版失败!错误详情:%s", err.Error()))
return false
}
t4, err := template.ParseFiles(basePath + "ts.go.template")
if err != nil {
log.Error(err)
e.Error(500, err, fmt.Sprintf("ts模版解析失败!错误详情:%s", err.Error()))
return false
}
t5, err := template.ParseFiles(basePath + "vue.go.template")
if err != nil {
log.Error(err)
e.Error(500, err, fmt.Sprintf("vue模版解析失败!错误详情:%s", err.Error()))
return false
}
t6, err := template.ParseFiles(basePath + "dto.go.template")
if err != nil {
log.Error(err)
e.Error(500, err, fmt.Sprintf("dto模版解析失败失败!错误详情:%s", err.Error()))
return false
}
t7, err := template.ParseFiles(basePath + "no_actions/service.go.template")
if err != nil {
log.Error(err)
e.Error(500, err, fmt.Sprintf("service模版失败!错误详情:%s", err.Error()))
return false
}
// t8/t9 back F3/F9 (PRD 010): see the matching comment in Preview above.
t8, err := parseGenTemplate(basePath + "lang-zh.go.template")
if err != nil {
log.Error(err)
e.Error(500, err, fmt.Sprintf("zh语言包模版解析失败!错误详情:%s", err.Error()))
return false
}
t9, err := parseGenTemplate(basePath + "lang-en.go.template")
if err != nil {
log.Error(err)
e.Error(500, err, fmt.Sprintf("en语言包模版解析失败!错误详情:%s", err.Error()))
return false
}
out, err := renderAll(tab, t1, t2, t3, t4, t5, t6, t7, t8, t9)
if err != nil {
log.Error(err)
e.Error(500, err, fmt.Sprintf("模版渲染失败!错误详情:%s", err.Error()))
return false
}
// gen/{PackageName}/ nests under each locale so go-admin-ui's
// gen-namespace.ts (`./*/*.ts` glob, one level under gen/) picks the file
// up - a flat gen/{BusinessName}.ts would let two tables in different
// packages silently overwrite each other's translations, since
// BusinessName only has a pattern check, no uniqueness check.
files := []struct {
path string
content []byte
}{
{"./app/" + tab.PackageName + "/models/" + tab.TBName + ".go", out[0]},
{"./app/" + tab.PackageName + "/apis/" + tab.TBName + ".go", out[1]},
{"./app/" + tab.PackageName + "/router/" + tab.TBName + ".go", out[2]},
{config.GenConfig.FrontPath + "/api/" + tab.PackageName + "/" + tab.MLTBName + ".ts", out[3]},
{config.GenConfig.FrontPath + "/views/" + tab.PackageName + "/" + tab.MLTBName + "/index.vue", out[4]},
{"./app/" + tab.PackageName + "/service/dto/" + tab.TBName + ".go", out[5]},
{"./app/" + tab.PackageName + "/service/" + tab.TBName + ".go", out[6]},
{config.GenConfig.FrontPath + "/lang/zh-CN/gen/" + tab.PackageName + "/" + tab.BusinessName + ".ts", out[7]},
{config.GenConfig.FrontPath + "/lang/en-US/gen/" + tab.PackageName + "/" + tab.BusinessName + ".ts", out[8]},
}
for _, f := range files {
if err := writeGenerated(f.path, f.content); err != nil {
log.Error(err)
e.Error(500, err, fmt.Sprintf("生成文件写入失败!错误详情:%s", err.Error()))
return false
}
}
return true
}
// genApiToFile writes the migration that seeds tab's menu and APIs, and
// reports whether it did; on false the error response is already written.
func (e Gen) genApiToFile(c *gin.Context, tab tools.SysTables) bool {
err := e.MakeContext(c).
MakeOrm().
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return false
}
basePath := "template/"
t1, err := template.ParseFiles(basePath + "api_migrate.template")
if err != nil {
e.Logger.Error(err)
e.Error(500, err, fmt.Sprintf("数据迁移模版解析失败!错误详情:%s", err.Error()))
return false
}
i := strconv.FormatInt(time.Now().UnixNano()/1e6, 10)
out, err := renderAll(struct {
tools.SysTables
GenerateTime string
}{tab, i}, t1)
if err != nil {
e.Logger.Error(err)
e.Error(500, err, fmt.Sprintf("数据迁移模版渲染失败!错误详情:%s", err.Error()))
return false
}
if err := writeGenerated("./cmd/migrate/migration/version-local/"+i+"_migrate.go", out[0]); err != nil {
e.Logger.Error(err)
e.Error(500, err, fmt.Sprintf("数据迁移文件写入失败!错误详情:%s", err.Error()))
return false
}
return true
}
func (e Gen) GenMenuAndApi(c *gin.Context) {
s := service.SysMenu{}
err := e.MakeContext(c).
MakeOrm().
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
table := tools.SysTables{}
id, err := pkg.StringToInt(c.Param("tableId"))
if err != nil {
e.Logger.Error(err)
e.Error(500, err, fmt.Sprintf("tableId参数解析失败!错误详情:%s", err.Error()))
return
}
table.TableId = id
tab, err := table.Get(e.Orm, true)
if err != nil {
e.Logger.Errorf("get table error, %s", err.Error())
e.Error(500, err, fmt.Sprintf("读取表配置失败!错误详情:%s", err.Error()))
return
}
tab.MLTBName = strings.Replace(tab.TBName, "_", "-", -1)
Mmenu := dto.SysMenuInsertReq{}
Mmenu.Title = tab.TableComment
Mmenu.Icon = "pass"
Mmenu.Path = "/" + tab.MLTBName
Mmenu.MenuType = "M"
Mmenu.Action = "无"
Mmenu.ParentId = 0
Mmenu.NoCache = false
Mmenu.Component = "Layout"
Mmenu.Sort = 0
Mmenu.Visible = "0"
Mmenu.IsFrame = "0"
Mmenu.CreateBy = 1
s.Insert(&Mmenu)
Cmenu := dto.SysMenuInsertReq{}
Cmenu.MenuName = tab.ClassName + "Manage"
Cmenu.Title = tab.TableComment
Cmenu.Icon = "pass"
Cmenu.Path = "/" + tab.PackageName + "/" + tab.MLTBName
Cmenu.MenuType = "C"
Cmenu.Action = "无"
Cmenu.Permission = tab.PackageName + ":" + tab.BusinessName + ":list"
Cmenu.ParentId = Mmenu.MenuId
Cmenu.NoCache = false
Cmenu.Component = "/" + tab.PackageName + "/" + tab.MLTBName + "/index"
Cmenu.Sort = 0
Cmenu.Visible = "0"
Cmenu.IsFrame = "0"
Cmenu.CreateBy = 1
Cmenu.UpdateBy = 1
s.Insert(&Cmenu)
MList := dto.SysMenuInsertReq{}
MList.MenuName = ""
MList.Title = "分页获取" + tab.TableComment
MList.Icon = ""
MList.Path = tab.TBName
MList.MenuType = "F"
MList.Action = "无"
MList.Permission = tab.PackageName + ":" + tab.BusinessName + ":query"
MList.ParentId = Cmenu.MenuId
MList.NoCache = false
MList.Sort = 0
MList.Visible = "0"
MList.IsFrame = "0"
MList.CreateBy = 1
MList.UpdateBy = 1
s.Insert(&MList)
MCreate := dto.SysMenuInsertReq{}
MCreate.MenuName = ""
MCreate.Title = "创建" + tab.TableComment
MCreate.Icon = ""
MCreate.Path = tab.TBName
MCreate.MenuType = "F"
MCreate.Action = "无"
MCreate.Permission = tab.PackageName + ":" + tab.BusinessName + ":add"
MCreate.ParentId = Cmenu.MenuId
MCreate.NoCache = false
MCreate.Sort = 0
MCreate.Visible = "0"
MCreate.IsFrame = "0"
MCreate.CreateBy = 1
MCreate.UpdateBy = 1
s.Insert(&MCreate)
MUpdate := dto.SysMenuInsertReq{}
MUpdate.MenuName = ""
MUpdate.Title = "修改" + tab.TableComment
MUpdate.Icon = ""
MUpdate.Path = tab.TBName
MUpdate.MenuType = "F"
MUpdate.Action = "无"
MUpdate.Permission = tab.PackageName + ":" + tab.BusinessName + ":edit"
MUpdate.ParentId = Cmenu.MenuId
MUpdate.NoCache = false
MUpdate.Sort = 0
MUpdate.Visible = "0"
MUpdate.IsFrame = "0"
MUpdate.CreateBy = 1
MUpdate.UpdateBy = 1
s.Insert(&MUpdate)
MDelete := dto.SysMenuInsertReq{}
MDelete.MenuName = ""
MDelete.Title = "删除" + tab.TableComment
MDelete.Icon = ""
MDelete.Path = tab.TBName
MDelete.MenuType = "F"
MDelete.Action = "无"
MDelete.Permission = tab.PackageName + ":" + tab.BusinessName + ":remove"
MDelete.ParentId = Cmenu.MenuId
MDelete.NoCache = false
MDelete.Sort = 0
MDelete.Visible = "0"
MDelete.IsFrame = "0"
MDelete.CreateBy = 1
MDelete.UpdateBy = 1
s.Insert(&MDelete)
e.OK("", "数据生成成功!")
}
// requireSinglePrimaryKey refuses a table whose primary key is not exactly one
// column. The templates address a row by a single key - one field on the
// model, one path parameter, one value per row in a delete - so a table with
// no primary key, or a composite one, has no shape they can render: generating
// it anyway writes a model whose GetId names nothing, or silently keys the
// whole table on whichever key column happened to be imported last.
func requireSinglePrimaryKey(tab tools.SysTables) error {
var keys []string
for _, c := range tab.Columns {
if c.Pk {
keys = append(keys, c.ColumnName)
}
}
switch len(keys) {
case 1:
return nil
case 0:
return fmt.Errorf("表 %s 没有主键,代码生成需要恰好一个主键列", tab.TBName)
default:
return fmt.Errorf("表 %s 是联合主键(%s),代码生成需要恰好一个主键列", tab.TBName, strings.Join(keys, ", "))
}
}
// renderAll executes each template against data and returns the outputs in
// the same order, stopping at the first template that fails. The caller
// writes nothing until every template has rendered: a template that fails
// part-way leaves a truncated file behind it, which compiles as nothing and
// at a glance looks like the real thing.
func renderAll(data any, tpls ...*template.Template) ([][]byte, error) {
out := make([][]byte, len(tpls))
for i, t := range tpls {
var b bytes.Buffer
if err := t.Execute(&b, data); err != nil {
return nil, err
}
out[i] = b.Bytes()
}
return out, nil
}
// writeGenerated writes one generated file, creating its directory first,
// which pkg.FileCreate does not do. It replaced pkg.FileCreate here while
// that returned no error and, when the file could not be created, ended the
// process with log.Fatalln - one unwritable path took the whole server down
// with it. go-admin-core v2.11.0 made pkg.FileCreate return the error.
func writeGenerated(path string, content []byte) error {
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
return err
}
return os.WriteFile(path, content, 0o644)
}