mirror of
https://github.com/go-admin-team/go-admin.git
synced 2026-09-29 04:27:17 +00:00
Merge pull request #950 from go-admin-team/fix/gen-non-id-primary-key
Code generator: support any single-column primary key
This commit is contained in:
@@ -131,6 +131,10 @@ func (e Gen) Preview(c *gin.Context) {
|
||||
// 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)
|
||||
@@ -221,6 +225,10 @@ func (e Gen) NOActionsGen(c *gin.Context, tab tools.SysTables) {
|
||||
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
|
||||
}
|
||||
// R2: see the matching call and comment in Preview above.
|
||||
applyInferredColumnWidths(tab.Columns)
|
||||
|
||||
@@ -502,3 +510,26 @@ func (e Gen) GenMenuAndApi(c *gin.Context) {
|
||||
|
||||
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, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,385 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"text/template"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/config"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
|
||||
"go-admin/app/other/models/tools"
|
||||
)
|
||||
|
||||
// The importer reads tables from information_schema, which only MySQL is
|
||||
// supported for, so this runs only when a MySQL DSN is set - and must run in
|
||||
// CI, where one is. A suite that skipped there would report the generator
|
||||
// fine for every primary key it has never been shown.
|
||||
const genMySQLDSNEnv = "GO_ADMIN_TEST_MYSQL_DSN"
|
||||
|
||||
// auditColumns are the columns every generated model maps through
|
||||
// models.ModelTime and models.ControlBy, typed the way those structs store
|
||||
// them. A fixture without them would describe a table the generated model
|
||||
// cannot read or write.
|
||||
const auditColumns = `name varchar(64) NOT NULL COMMENT 'name',
|
||||
create_by bigint NULL,
|
||||
update_by bigint NULL,
|
||||
created_at datetime(3) NULL,
|
||||
updated_at datetime(3) NULL,
|
||||
deleted_at bigint NOT NULL DEFAULT 0`
|
||||
|
||||
// pkShape is one table the importer can be pointed at. The shapes are the
|
||||
// primary keys a real table has, not the one the templates were written
|
||||
// for.
|
||||
type pkShape struct {
|
||||
Table string
|
||||
Class string
|
||||
Key string // key column definition, before the audit columns
|
||||
Tail string // table constraints, after them
|
||||
Refused bool // generation must refuse the table outright
|
||||
PkColumn string
|
||||
// Assigned is true when the database assigns the key; otherwise Create
|
||||
// carries it and ID is the value it carries.
|
||||
Assigned bool
|
||||
Create string
|
||||
ID string
|
||||
IDJSON string
|
||||
Missing string
|
||||
}
|
||||
|
||||
var pkShapes = []pkShape{{
|
||||
Table: "gpk_serial", Class: "GpkSerial", PkColumn: "id",
|
||||
Key: "id int NOT NULL AUTO_INCREMENT PRIMARY KEY,",
|
||||
Assigned: true, Create: `{"name":"alpha"}`, Missing: "987654",
|
||||
}, {
|
||||
Table: "gpk_order", Class: "GpkOrder", PkColumn: "order_no",
|
||||
Key: "order_no int NOT NULL AUTO_INCREMENT PRIMARY KEY,",
|
||||
Assigned: true, Create: `{"name":"alpha"}`, Missing: "987654",
|
||||
}, {
|
||||
Table: "gpk_code", Class: "GpkCode", PkColumn: "code",
|
||||
Key: "code varchar(32) NOT NULL PRIMARY KEY,",
|
||||
Create: `{"code":"c-1","name":"alpha"}`, ID: "c-1", IDJSON: `"c-1"`, Missing: "no-such-code",
|
||||
}, {
|
||||
Table: "gpk_nokey", Key: "ref int NOT NULL,", Refused: true,
|
||||
}, {
|
||||
Table: "gpk_pair", Key: "a int NOT NULL, b int NOT NULL,", Tail: ", PRIMARY KEY (a, b)", Refused: true,
|
||||
}}
|
||||
|
||||
func TestGeneratorHandlesEveryPrimaryKeyShape(t *testing.T) {
|
||||
dsn := os.Getenv(genMySQLDSNEnv)
|
||||
if dsn == "" {
|
||||
if os.Getenv("CI") != "" {
|
||||
t.Fatalf("%s is not set while CI is: the generator must not go untested", genMySQLDSNEnv)
|
||||
}
|
||||
t.Skipf("%s is not set; the importer only reads MySQL", genMySQLDSNEnv)
|
||||
}
|
||||
gin.SetMode(gin.TestMode)
|
||||
root, err := filepath.Abs("../../../..")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
|
||||
if err != nil {
|
||||
t.Fatalf("connecting to %s: %v", genMySQLDSNEnv, err)
|
||||
}
|
||||
var dbName string
|
||||
if err := db.Raw("SELECT DATABASE()").Scan(&dbName).Error; err != nil || dbName == "" {
|
||||
t.Fatalf("reading the database name: %v", err)
|
||||
}
|
||||
|
||||
out := t.TempDir()
|
||||
restore := [3]string{config.DatabaseConfig.Driver, config.GenConfig.DBName, config.GenConfig.FrontPath}
|
||||
t.Cleanup(func() {
|
||||
config.DatabaseConfig.Driver, config.GenConfig.DBName, config.GenConfig.FrontPath = restore[0], restore[1], restore[2]
|
||||
})
|
||||
config.DatabaseConfig.Driver = "mysql"
|
||||
config.GenConfig.DBName = dbName
|
||||
config.GenConfig.FrontPath = filepath.Join(out, "ui")
|
||||
|
||||
if err := db.AutoMigrate(&tools.SysTables{}, &tools.SysColumns{}); err != nil {
|
||||
t.Fatalf("migrating the generator's own tables: %v", err)
|
||||
}
|
||||
for _, s := range pkShapes {
|
||||
dropFixture(t, db, s.Table)
|
||||
ddl := "CREATE TABLE " + s.Table + " (" + s.Key + auditColumns + s.Tail + ")"
|
||||
if err := db.Exec(ddl).Error; err != nil {
|
||||
t.Fatalf("creating %s: %v", s.Table, err)
|
||||
}
|
||||
t.Cleanup(func() { dropFixture(t, db, s.Table) })
|
||||
}
|
||||
|
||||
// The generator resolves its templates, and writes its output, relative to
|
||||
// the working directory, exactly as it does in a running server.
|
||||
if err := os.Symlink(filepath.Join(root, "template"), filepath.Join(out, "template")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Chdir(out)
|
||||
|
||||
generated := map[string]string{}
|
||||
var runnable []pkShape
|
||||
genFailed := false
|
||||
for _, s := range pkShapes {
|
||||
if code := callHandler(t, db, SysTable{}.Insert, "/?tables="+s.Table, nil); code != http.StatusOK {
|
||||
t.Fatalf("importing %s: response code %d", s.Table, code)
|
||||
}
|
||||
var row tools.SysTables
|
||||
if err := db.Where("table_name = ?", s.Table).First(&row).Error; err != nil {
|
||||
t.Fatalf("finding %s after import: %v", s.Table, err)
|
||||
}
|
||||
code := callHandler(t, db, Gen{}.GenCode, "/", gin.Params{{Key: "tableId", Value: itoa(row.TableId)}})
|
||||
files := backendFiles(out, root, s.Table)
|
||||
|
||||
if s.Refused {
|
||||
if code == http.StatusOK {
|
||||
t.Errorf("%s: generation succeeded; a table without exactly one primary key column must be refused", s.Table)
|
||||
}
|
||||
for _, generatedFile := range files {
|
||||
if _, err := os.Stat(generatedFile); err == nil {
|
||||
t.Errorf("%s: refused, but %s was written anyway", s.Table, generatedFile)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if code != http.StatusOK {
|
||||
t.Errorf("%s: generation failed with response code %d", s.Table, code)
|
||||
genFailed = true
|
||||
continue
|
||||
}
|
||||
for dst, src := range files {
|
||||
generated[dst] = src
|
||||
}
|
||||
runnable = append(runnable, s)
|
||||
}
|
||||
if genFailed {
|
||||
return
|
||||
}
|
||||
|
||||
// Compile the generated files in place, inside this module, through an
|
||||
// overlay: nothing is written into the working tree, and the generated
|
||||
// code is checked against the real packages it will live in.
|
||||
crud := filepath.Join(out, "generated_crud_test.go")
|
||||
f, err := os.Create(crud)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := crudTest.Execute(f, runnable); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.Close()
|
||||
generated[filepath.Join(root, "app/admin/apis/zz_generated_crud_test.go")] = crud
|
||||
|
||||
overlay := filepath.Join(out, "overlay.json")
|
||||
b, _ := json.Marshal(map[string]any{"Replace": generated})
|
||||
if err := os.WriteFile(overlay, b, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
goCmd(t, root, "vet", "-overlay="+overlay, "./app/admin/...")
|
||||
ran := goCmd(t, root, "test", "-overlay="+overlay, "-count=1", "-v", "-run", "^TestGeneratedCRUD_", "./app/admin/apis/")
|
||||
// A -run pattern that matches nothing also exits 0. Each table has to
|
||||
// have been driven, not merely compiled.
|
||||
for _, s := range runnable {
|
||||
if !strings.Contains(string(ran), "--- PASS: TestGeneratedCRUD_"+s.Class+" ") {
|
||||
t.Errorf("%s: TestGeneratedCRUD_%s did not run", s.Table, s.Class)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func dropFixture(t *testing.T, db *gorm.DB, table string) {
|
||||
t.Helper()
|
||||
var ids []int
|
||||
db.Model(&tools.SysTables{}).Where("table_name = ?", table).Pluck("table_id", &ids)
|
||||
if len(ids) > 0 {
|
||||
db.Where("table_id IN ?", ids).Delete(&tools.SysColumns{})
|
||||
db.Where("table_id IN ?", ids).Delete(&tools.SysTables{})
|
||||
}
|
||||
if err := db.Exec("DROP TABLE IF EXISTS " + table).Error; err != nil {
|
||||
t.Fatalf("dropping %s: %v", table, err)
|
||||
}
|
||||
}
|
||||
|
||||
// callHandler runs one handler the way a request would reach it and returns
|
||||
// the code its first response body carries. GenCode can write a second body
|
||||
// after an error body, so only the first is the verdict.
|
||||
func callHandler(t *testing.T, db *gorm.DB, h gin.HandlerFunc, target string, params gin.Params) int {
|
||||
t.Helper()
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest(http.MethodPost, target, nil)
|
||||
c.Params = params
|
||||
c.Set("db", db)
|
||||
h(c)
|
||||
var body struct {
|
||||
Code int `json:"code"`
|
||||
}
|
||||
if err := json.NewDecoder(strings.NewReader(w.Body.String())).Decode(&body); err != nil {
|
||||
t.Fatalf("decoding the response to %s: %v (body %q)", target, err, w.Body.String())
|
||||
}
|
||||
return body.Code
|
||||
}
|
||||
|
||||
// backendFiles maps where each generated backend file belongs in this module
|
||||
// to where the generator wrote it.
|
||||
func backendFiles(out, root, table string) map[string]string {
|
||||
m := map[string]string{}
|
||||
for _, dir := range []string{"models", "apis", "router", "service", "service/dto"} {
|
||||
rel := filepath.Join("app/admin", dir, table+".go")
|
||||
m[filepath.Join(root, rel)] = filepath.Join(out, rel)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func goCmd(t *testing.T, dir string, args ...string) []byte {
|
||||
t.Helper()
|
||||
cmd := exec.Command("go", args...)
|
||||
cmd.Dir = dir
|
||||
out, err := cmd.CombinedOutput()
|
||||
var exit *exec.ExitError
|
||||
if errors.As(err, &exit) {
|
||||
t.Fatalf("go %s failed:\n%s", strings.Join(args, " "), out)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("running go %s: %v", args[0], err)
|
||||
}
|
||||
t.Logf("go %s:\n%s", args[0], out)
|
||||
return out
|
||||
}
|
||||
|
||||
func itoa(n int) string {
|
||||
b, _ := json.Marshal(n)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// crudTest drives each generated table through its generated handlers, over
|
||||
// HTTP, against the fixture table. The routes use the same "/:id" pattern the
|
||||
// generated routers register.
|
||||
var crudTest = template.Must(template.New("crud").Parse(`package apis
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
type generatedResp struct {
|
||||
Code int ` + "`json:\"code\"`" + `
|
||||
Data json.RawMessage ` + "`json:\"data\"`" + `
|
||||
}
|
||||
|
||||
func generatedEngine(t *testing.T) (*gin.Engine, *gorm.DB) {
|
||||
t.Helper()
|
||||
db, err := gorm.Open(mysql.Open(os.Getenv("GO_ADMIN_TEST_MYSQL_DSN")), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
r.Use(func(c *gin.Context) { c.Set("db", db.WithContext(c)); c.Next() })
|
||||
return r, db
|
||||
}
|
||||
|
||||
func generatedDo(r *gin.Engine, method, path, body string) generatedResp {
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(method, path, strings.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(w, req)
|
||||
var res generatedResp
|
||||
_ = json.NewDecoder(strings.NewReader(w.Body.String())).Decode(&res)
|
||||
return res
|
||||
}
|
||||
|
||||
func generatedLive(db *gorm.DB, table string) int64 {
|
||||
var n int64
|
||||
db.Table(table).Where("deleted_at = 0").Count(&n)
|
||||
return n
|
||||
}
|
||||
{{range .}}
|
||||
func TestGeneratedCRUD_{{.Class}}(t *testing.T) {
|
||||
r, db := generatedEngine(t)
|
||||
a := {{.Class}}{}
|
||||
r.POST("/x", a.Insert)
|
||||
r.GET("/x/:id", a.Get)
|
||||
r.PUT("/x/:id", a.Update)
|
||||
r.DELETE("/x", a.Delete)
|
||||
|
||||
if res := generatedDo(r, http.MethodPost, "/x", ` + "`{{.Create}}`" + `); res.Code != 200 {
|
||||
t.Fatalf("insert: code %d", res.Code)
|
||||
}
|
||||
{{- if .Assigned}}
|
||||
var n int
|
||||
db.Table("{{.Table}}").Select("MAX({{.PkColumn}})").Scan(&n)
|
||||
id, idJSON := itoaGenerated(n), itoaGenerated(n)
|
||||
{{- else}}
|
||||
id, idJSON := "{{.ID}}", ` + "`{{.IDJSON}}`" + `
|
||||
{{- end}}
|
||||
|
||||
t.Run("get by the path parameter", func(t *testing.T) {
|
||||
res := generatedDo(r, http.MethodGet, "/x/"+url.PathEscape(id), "")
|
||||
var row map[string]any
|
||||
_ = json.Unmarshal(res.Data, &row)
|
||||
if res.Code != 200 || row["name"] != "alpha" {
|
||||
t.Errorf("code %d, data %s", res.Code, res.Data)
|
||||
}
|
||||
})
|
||||
t.Run("update by the path parameter updates, not inserts", func(t *testing.T) {
|
||||
live := generatedLive(db, "{{.Table}}")
|
||||
res := generatedDo(r, http.MethodPut, "/x/"+url.PathEscape(id), ` + "`{\"name\":\"beta\"}`" + `)
|
||||
var name string
|
||||
db.Table("{{.Table}}").Where("{{.PkColumn}} = ?", id).Select("name").Scan(&name)
|
||||
if res.Code != 200 || name != "beta" || generatedLive(db, "{{.Table}}") != live {
|
||||
t.Errorf("code %d, name %q, live rows %d want %d", res.Code, name, generatedLive(db, "{{.Table}}"), live)
|
||||
}
|
||||
})
|
||||
t.Run("a missing key is not found", func(t *testing.T) {
|
||||
if res := generatedDo(r, http.MethodGet, "/x/{{.Missing}}", ""); res.Code == 200 {
|
||||
t.Errorf("found: %s", res.Data)
|
||||
}
|
||||
})
|
||||
t.Run("updating a missing key does not create a row", func(t *testing.T) {
|
||||
live := generatedLive(db, "{{.Table}}")
|
||||
res := generatedDo(r, http.MethodPut, "/x/{{.Missing}}", ` + "`{\"name\":\"ghost\"}`" + `)
|
||||
if got := generatedLive(db, "{{.Table}}"); got != live || res.Code == 200 {
|
||||
t.Errorf("code %d, live rows %d want %d", res.Code, got, live)
|
||||
}
|
||||
})
|
||||
// "1=1" rather than a quote-breaking payload: a key handed to GORM as a
|
||||
// bare string becomes the WHERE clause itself, and this one is valid SQL
|
||||
// that matches every row, so the failure it exposes is a successful read.
|
||||
t.Run("a key carrying SQL is a value, not a condition", func(t *testing.T) {
|
||||
if res := generatedDo(r, http.MethodGet, "/x/"+url.PathEscape("1=1"), ""); res.Code == 200 {
|
||||
t.Errorf("found: %s", res.Data)
|
||||
}
|
||||
})
|
||||
t.Run("delete by key", func(t *testing.T) {
|
||||
live := generatedLive(db, "{{.Table}}")
|
||||
res := generatedDo(r, http.MethodDelete, "/x", ` + "`{\"ids\":[`+idJSON+`]}`" + `)
|
||||
if res.Code != 200 || generatedLive(db, "{{.Table}}") != live-1 {
|
||||
t.Errorf("code %d, live rows %d want %d", res.Code, generatedLive(db, "{{.Table}}"), live-1)
|
||||
}
|
||||
})
|
||||
}
|
||||
{{end}}
|
||||
func itoaGenerated(n int) string {
|
||||
b, _ := json.Marshal(n)
|
||||
return string(b)
|
||||
}
|
||||
`))
|
||||
@@ -1,6 +1,13 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
{{- /*
|
||||
$embed: the key is models.Model's own Id, so the model embeds it.
|
||||
$pkType: the key's Go type, for the delete request's id list.
|
||||
*/ -}}
|
||||
{{- $embed := false -}}
|
||||
{{- $pkType := "int" -}}
|
||||
{{- range .Columns -}}{{- if .Pk -}}{{- $pkType = .GoType -}}{{- if and (eq .GoField "Id") (eq .GoType "int") -}}{{- $embed = true -}}{{- end -}}{{- end -}}{{- end -}}
|
||||
{{- $bb := false -}}
|
||||
{{- range .Columns -}}
|
||||
{{- $z := .IsQuery -}}
|
||||
@@ -52,7 +59,7 @@ type {{.ClassName}}InsertReq struct {
|
||||
{{- range .Columns -}}
|
||||
{{$x := .Pk}}
|
||||
{{- if ($x) }}
|
||||
{{.GoField}} {{.GoType}} `json:"-" comment:"{{.ColumnComment}}"` // {{.ColumnComment}}
|
||||
{{.GoField}} {{.GoType}} `json:"{{if eq .GoType "int"}}-{{else}}{{.JsonField}}{{end}}" comment:"{{.ColumnComment}}"` // {{.ColumnComment}}
|
||||
{{- else if eq .GoField "CreatedAt" -}}
|
||||
{{- else if eq .GoField "UpdatedAt" -}}
|
||||
{{- else if eq .GoField "DeletedAt" -}}
|
||||
@@ -69,9 +76,13 @@ func (s *{{.ClassName}}InsertReq) Generate(model *models.{{.ClassName}}) {
|
||||
{{- range .Columns -}}
|
||||
{{$x := .Pk}}
|
||||
{{- if ($x) }}
|
||||
{{- if $embed }}
|
||||
if s.{{.GoField}} == 0 {
|
||||
model.Model = common.Model{ {{.GoField}}: s.{{.GoField}} }
|
||||
}
|
||||
{{- else }}
|
||||
model.{{.GoField}} = s.{{.GoField}}
|
||||
{{- end }}
|
||||
{{- else if eq .GoField "CreatedAt" -}}
|
||||
{{- else if eq .GoField "UpdatedAt" -}}
|
||||
{{- else if eq .GoField "DeletedAt" -}}
|
||||
@@ -92,7 +103,7 @@ type {{.ClassName}}UpdateReq struct {
|
||||
{{- range .Columns -}}
|
||||
{{$x := .Pk}}
|
||||
{{- if ($x) }}
|
||||
{{.GoField}} {{.GoType}} `uri:"{{.JsonField}}" comment:"{{.ColumnComment}}"` // {{.ColumnComment}}
|
||||
{{.GoField}} {{.GoType}} `uri:"id" comment:"{{.ColumnComment}}"` // {{.ColumnComment}}
|
||||
{{- else if eq .GoField "CreatedAt" -}}
|
||||
{{- else if eq .GoField "UpdatedAt" -}}
|
||||
{{- else if eq .GoField "DeletedAt" -}}
|
||||
@@ -109,9 +120,11 @@ func (s *{{.ClassName}}UpdateReq) Generate(model *models.{{.ClassName}}) {
|
||||
{{- range .Columns -}}
|
||||
{{$x := .Pk}}
|
||||
{{- if ($x) }}
|
||||
{{- if $embed }}
|
||||
if s.{{.GoField}} == 0 {
|
||||
model.Model = common.Model{ {{.GoField}}: s.{{.GoField}} }
|
||||
}
|
||||
{{- end }}
|
||||
{{- else if eq .GoField "CreatedAt" -}}
|
||||
{{- else if eq .GoField "UpdatedAt" -}}
|
||||
{{- else if eq .GoField "DeletedAt" -}}
|
||||
@@ -133,7 +146,7 @@ type {{.ClassName}}GetReq struct {
|
||||
{{- range .Columns -}}
|
||||
{{$x := .Pk}}
|
||||
{{- if ($x) }}
|
||||
{{.GoField}} {{.GoType}} `uri:"{{.JsonField}}"`
|
||||
{{.GoField}} {{.GoType}} `uri:"id"`
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
}
|
||||
@@ -143,7 +156,7 @@ func (s *{{.ClassName}}GetReq) GetId() interface{} {
|
||||
|
||||
// {{.ClassName}}DeleteReq 功能删除请求参数
|
||||
type {{.ClassName}}DeleteReq struct {
|
||||
Ids []int `json:"ids"`
|
||||
Ids []{{$pkType}} `json:"ids"`
|
||||
}
|
||||
|
||||
func (s *{{.ClassName}}DeleteReq) GetId() interface{} {
|
||||
|
||||
@@ -24,11 +24,24 @@ import (
|
||||
|
||||
)
|
||||
|
||||
{{/*
|
||||
models.Model supplies an int primary key named Id, so it stands in for the
|
||||
key column only when that is exactly the key the table has. Any other key -
|
||||
a different name, or a string - is declared as its own field, or GetId below
|
||||
would name a field that does not exist.
|
||||
*/ -}}
|
||||
{{- $embed := false -}}
|
||||
{{- range .Columns -}}{{- if and .Pk (eq .GoField "Id") (eq .GoType "int") -}}{{- $embed = true -}}{{- end -}}{{- end -}}
|
||||
type {{.ClassName}} struct {
|
||||
{{- if $embed }}
|
||||
models.Model
|
||||
{{- end }}
|
||||
{{ range .Columns -}}
|
||||
{{$x := .Pk}}
|
||||
{{- if ($x) }}
|
||||
{{- if not $embed }}
|
||||
{{.GoField}} {{.GoType}} `json:"{{.JsonField}}" gorm:"primaryKey;type:{{.ColumnType}};comment:{{- if eq .ColumnComment "" -}}{{.GoField}}{{- else -}}{{.ColumnComment}}{{end -}}"`
|
||||
{{- end }}
|
||||
{{- else if eq .GoField "CreatedAt" -}}
|
||||
{{- else if eq .GoField "UpdatedAt" -}}
|
||||
{{- else if eq .GoField "DeletedAt" -}}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/service"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
|
||||
"go-admin/app/{{.PackageName}}/models"
|
||||
"go-admin/app/{{.PackageName}}/service/dto"
|
||||
@@ -37,6 +38,9 @@ func (e *{{.ClassName}}) GetPage(c *dto.{{.ClassName}}GetPageReq, p *actions.Dat
|
||||
}
|
||||
|
||||
// Get 获取{{.ClassName}}对象
|
||||
//
|
||||
// The key is matched as a value of the model's primary-key column. Handed to
|
||||
// First on its own, a string key would be read as a SQL condition.
|
||||
func (e *{{.ClassName}}) Get(d *dto.{{.ClassName}}GetReq, p *actions.DataPermission, model *models.{{.ClassName}}) error {
|
||||
var data models.{{.ClassName}}
|
||||
|
||||
@@ -44,7 +48,8 @@ func (e *{{.ClassName}}) Get(d *dto.{{.ClassName}}GetReq, p *actions.DataPermiss
|
||||
Scopes(
|
||||
actions.Permission(data.TableName(), p),
|
||||
).
|
||||
First(model, d.GetId()).Error
|
||||
Where(clause.Eq{Column: clause.PrimaryColumn, Value: d.GetId()}).
|
||||
First(model).Error
|
||||
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
err = errors.New("查看对象不存在或无权查看")
|
||||
e.Log.Errorf("Service Get{{.ClassName}} error:%s \r\n", err)
|
||||
@@ -71,12 +76,25 @@ func (e *{{.ClassName}}) Insert(c *dto.{{.ClassName}}InsertReq) error {
|
||||
}
|
||||
|
||||
// Update 修改{{.ClassName}}对象
|
||||
//
|
||||
// The row has to be found first. Save on a model whose key is still zero
|
||||
// inserts, so a row that is missing - or that p filters out - must stop here
|
||||
// rather than turn the update into a new row.
|
||||
func (e *{{.ClassName}}) Update(c *dto.{{.ClassName}}UpdateReq, p *actions.DataPermission) error {
|
||||
var err error
|
||||
var data = models.{{.ClassName}}{}
|
||||
e.Orm.Scopes(
|
||||
err = e.Orm.Scopes(
|
||||
actions.Permission(data.TableName(), p),
|
||||
).First(&data, c.GetId())
|
||||
).
|
||||
Where(clause.Eq{Column: clause.PrimaryColumn, Value: c.GetId()}).
|
||||
First(&data).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New("无权更新该数据")
|
||||
}
|
||||
if err != nil {
|
||||
e.Log.Errorf("{{.ClassName}}Service Update error:%s \r\n", err)
|
||||
return err
|
||||
}
|
||||
c.Generate(&data)
|
||||
|
||||
db := e.Orm.Save(&data)
|
||||
|
||||
Reference in New Issue
Block a user