Files
go-admin/app/other/apis/tools/gen.go
T
zhangwenjian c0ca07c95b fix🐛: refuse generator names that cannot be path components
The package, table and business names a table's configuration carries
become directories and file names, and nothing on the server checked
them; the config page's patterns were the only check.

Each is now checked against what it becomes: a package name is
lowercase letters and digits, a table name letters, digits and
underscores, a business name an identifier starting with a lowercase
letter. None allows a dot or a separator, and none is stricter than
the config page. The check runs on import, where a list holding one
bad name is refused whole, on save, and again before generating, so a
configuration saved before this check existed is refused rather than
trusted.
2026-09-27 19:43:04 +08:00

591 lines
19 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
}
// Checked again here, not only on save: a configuration saved before the
// save-time check existed is still in sys_tables.
if err := validateGenPathFields(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.
back, front := ".", config.GenConfig.FrontPath
files := []struct {
root, name string
content []byte
}{
{back, "app/" + tab.PackageName + "/models/" + tab.TBName + ".go", out[0]},
{back, "app/" + tab.PackageName + "/apis/" + tab.TBName + ".go", out[1]},
{back, "app/" + tab.PackageName + "/router/" + tab.TBName + ".go", out[2]},
{front, "api/" + tab.PackageName + "/" + tab.MLTBName + ".ts", out[3]},
{front, "views/" + tab.PackageName + "/" + tab.MLTBName + "/index.vue", out[4]},
{back, "app/" + tab.PackageName + "/service/dto/" + tab.TBName + ".go", out[5]},
{back, "app/" + tab.PackageName + "/service/" + tab.TBName + ".go", out[6]},
{front, "lang/zh-CN/gen/" + tab.PackageName + "/" + tab.BusinessName + ".ts", out[7]},
{front, "lang/en-US/gen/" + tab.PackageName + "/" + tab.BusinessName + ".ts", out[8]},
}
for _, f := range files {
if err := writeGenerated(f.root, f.name, 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 content to name, a slash-separated path under root,
// creating root and the directories in between.
//
// It goes through os.Root, so name cannot leave root: a ".." component, an
// absolute path, or a symlink under root that points outside it is refused
// rather than followed. root itself is configuration (the working directory,
// or gen.frontpath), and may be or contain a symlink.
//
// 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(root, name string, content []byte) error {
if err := os.MkdirAll(root, os.ModePerm); err != nil {
return err
}
r, err := os.OpenRoot(root)
if err != nil {
return err
}
defer r.Close()
name = filepath.FromSlash(name)
if err := r.MkdirAll(filepath.Dir(name), os.ModePerm); err != nil {
return err
}
return r.WriteFile(name, content, 0o644)
}