386 lines
10 KiB
Go
386 lines
10 KiB
Go
package aeusadmin
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"html/template"
|
|
httpkg "net/http"
|
|
"os"
|
|
"path"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.nobla.cn/golang/aeus-admin/migrate"
|
|
"git.nobla.cn/golang/aeus-admin/models"
|
|
"git.nobla.cn/golang/aeus-admin/pkg/dbcache"
|
|
adminTypes "git.nobla.cn/golang/aeus-admin/types"
|
|
"git.nobla.cn/golang/aeus/middleware/auth"
|
|
"git.nobla.cn/golang/aeus/pkg/errors"
|
|
"git.nobla.cn/golang/aeus/pkg/pool"
|
|
"git.nobla.cn/golang/aeus/transport/http"
|
|
"git.nobla.cn/golang/rest"
|
|
"git.nobla.cn/golang/rest/inflector"
|
|
"git.nobla.cn/golang/rest/types"
|
|
restTypes "git.nobla.cn/golang/rest/types"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// getModels 获取预定义的模型列表
|
|
func getModels() []any {
|
|
return []any{
|
|
&models.Department{},
|
|
&models.Role{},
|
|
&models.User{},
|
|
&models.Menu{},
|
|
&models.Login{},
|
|
&models.Permission{},
|
|
&models.RolePermission{},
|
|
&models.Setting{},
|
|
&models.Activity{},
|
|
}
|
|
}
|
|
|
|
// checkModelMenu 检查模型菜单
|
|
func checkModelMenu(db *gorm.DB, viewPath string, apiPrefix string, model *rest.Model, translate Translate) (value *models.Menu, err error) {
|
|
refVal := reflect.New(model.Value().Type()).Interface()
|
|
if v, ok := refVal.(adminTypes.MenuModel); ok {
|
|
row := v.GetMenu()
|
|
value = &models.Menu{}
|
|
if row.Name == "" {
|
|
row.Name = inflector.Camelize(model.Naming().ModuleName) + inflector.Camelize(model.Naming().Singular)
|
|
}
|
|
if err = db.Where("name = ?", row.Name).First(value).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
if row.Parent != "" {
|
|
value.Parent = row.Parent
|
|
}
|
|
value.Name = row.Name
|
|
value.Hidden = row.Hidden
|
|
value.Icon = row.Icon
|
|
value.Label = row.Label
|
|
value.Uri = row.Uri
|
|
value.ViewPath = row.ViewPath
|
|
if value.Label == "" {
|
|
value.Label = inflector.Camel2words(model.Naming().Pluralize)
|
|
if translate != nil {
|
|
value.Label = translate.Menu(model, value.Label)
|
|
}
|
|
}
|
|
if value.Uri == "" {
|
|
value.Uri = strings.TrimPrefix(model.Uri(types.ScenarioList), apiPrefix)
|
|
}
|
|
if value.ViewPath == "" {
|
|
value.ViewPath = path.Join(viewPath, model.ModuleName(), model.Naming().Singular, "Index.vue")
|
|
}
|
|
err = db.Create(value).Error
|
|
}
|
|
}
|
|
} else {
|
|
menuName := inflector.Camelize(model.Naming().ModuleName) + inflector.Camelize(model.Naming().Singular)
|
|
value = &models.Menu{}
|
|
if err = db.Where("name = ?", menuName).First(value).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
value.Name = menuName
|
|
value.Parent = ""
|
|
value.Label = inflector.Camel2words(model.Naming().Pluralize)
|
|
if translate != nil {
|
|
value.Label = translate.Menu(model, value.Label)
|
|
}
|
|
value.Uri = strings.TrimPrefix(model.Uri(types.ScenarioList), apiPrefix)
|
|
value.ViewPath = path.Join(viewPath, model.ModuleName(), model.Naming().Singular, "Index.vue")
|
|
err = db.Create(value).Error
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// checkModelPermission 检查模型权限是否写入到数据库
|
|
func checkModelPermission(db *gorm.DB, menuName string, scene string, model *rest.Model, translate Translate) (permissionModel *models.Permission, err error) {
|
|
permissionModel = &models.Permission{}
|
|
permission := model.Permission(scene)
|
|
label := scene
|
|
if translate != nil {
|
|
label = translate.Permission(model, scene, label)
|
|
}
|
|
permissionModel, err = migrate.Permission(db, menuName, permission, label)
|
|
return
|
|
}
|
|
|
|
// checkModel 检查模型
|
|
func checkModel(opts *options, model *rest.Model) (err error) {
|
|
var (
|
|
menuModel *models.Menu
|
|
)
|
|
tx := opts.db.Begin()
|
|
if menuModel, err = checkModelMenu(tx, opts.viewPrefix, opts.apiPrefix, model, opts.translate); err != nil {
|
|
tx.Rollback()
|
|
return
|
|
}
|
|
for _, s := range defaultScenarios {
|
|
if model.HasScenario(s) {
|
|
if _, err = checkModelPermission(tx, menuModel.Name, s, model, opts.translate); err != nil {
|
|
tx.Rollback()
|
|
return
|
|
}
|
|
}
|
|
}
|
|
modelValue := reflect.New(model.Value().Type()).Interface()
|
|
if v, ok := modelValue.(adminTypes.PerrmissionModule); ok {
|
|
for k, v := range v.ModelPermissions() {
|
|
if _, err = migrate.Permission(tx, menuModel.Name, k, v); err != nil {
|
|
tx.Rollback()
|
|
return
|
|
}
|
|
}
|
|
}
|
|
tx.Commit()
|
|
return
|
|
}
|
|
|
|
// generateVueFile 生成Vue文件
|
|
func generateVueFile(prefix string, apiPrefix string, mv *rest.Model) (err error) {
|
|
refVal := reflect.New(mv.Value().Type()).Interface()
|
|
if v, ok := refVal.(adminTypes.MenuModel); ok {
|
|
instance := v.GetMenu()
|
|
if instance != nil {
|
|
if instance.Hidden {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
filename := path.Join(prefix, mv.Naming().ModuleName, mv.Naming().Singular, "Index.vue")
|
|
if _, err = os.Stat(filename); err == nil {
|
|
return
|
|
}
|
|
dirname := path.Dir(filename)
|
|
if _, err = os.Stat(dirname); err != nil {
|
|
if err = os.MkdirAll(dirname, os.ModePerm); err != nil {
|
|
return
|
|
}
|
|
}
|
|
var (
|
|
editable bool
|
|
temp *template.Template
|
|
)
|
|
if temp, err = template.New("vue").Parse(vueTemplate); err != nil {
|
|
return
|
|
}
|
|
permissions := make(map[string]string)
|
|
for _, s := range defaultScenarios {
|
|
if mv.HasScenario(s) {
|
|
if s == types.ScenarioCreate || s == types.ScenarioUpdate {
|
|
editable = true
|
|
}
|
|
permissions[s] = mv.Permission(s)
|
|
}
|
|
}
|
|
data := &vueTemplateData{
|
|
ModuleName: mv.ModuleName(),
|
|
TableName: mv.TableName(),
|
|
Permissions: permissions,
|
|
Readonly: !editable,
|
|
ApiPrefix: strings.TrimPrefix(apiPrefix, "/"),
|
|
}
|
|
writer := pool.GetBuffer()
|
|
defer pool.PutBuffer(writer)
|
|
if err = temp.Execute(writer, data); err != nil {
|
|
return
|
|
}
|
|
return os.WriteFile(filename, writer.Bytes(), 0644)
|
|
}
|
|
|
|
// restValueLookup 特殊字段获取方式
|
|
func restValueLookup(column string, w httpkg.ResponseWriter, r *httpkg.Request) string {
|
|
switch column {
|
|
case "user":
|
|
// 从授权信息里面获取用户的ID
|
|
if t, ok := auth.FromContext(r.Context()); ok {
|
|
uid, _ := t.GetSubject()
|
|
return uid
|
|
}
|
|
}
|
|
return r.Header.Get(column)
|
|
}
|
|
|
|
// initREST 初始化REST模块
|
|
func initREST(ctx context.Context, o *options) (err error) {
|
|
tx := o.db
|
|
if tx == nil {
|
|
return errors.ErrUnavailable
|
|
}
|
|
opts := make([]rest.Option, 0)
|
|
opts = append(opts, o.restOpts...)
|
|
opts = append(opts, rest.WithDB(tx))
|
|
opts = append(opts, rest.WithValueLookup(restValueLookup))
|
|
if o.apiPrefix != "" {
|
|
opts = append(opts, rest.WithUriPrefix(o.apiPrefix))
|
|
}
|
|
if err = rest.Init(opts...); err != nil {
|
|
return
|
|
}
|
|
if err = tx.AutoMigrate(getModels()...); err != nil {
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
// initRBAC 初始化权限控制, 用于生成角色权限相关的信息
|
|
func initModels(ctx context.Context, o *options) (err error) {
|
|
var mv *rest.Model
|
|
for _, v := range getModels() {
|
|
moduleName := o.moduleName
|
|
if mm, ok := v.(adminTypes.ModuleModel); ok {
|
|
moduleName = mm.ModuleName()
|
|
}
|
|
if mv, err = rest.AutoMigrate(ctx, v, rest.WithModuleName(moduleName)); err != nil {
|
|
return
|
|
} else {
|
|
if err = checkModel(o, mv); err != nil {
|
|
return
|
|
}
|
|
if o.vuePath != "" {
|
|
if err = generateVueFile(o.vuePath, o.apiPrefix, mv); err != nil {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// AutoMigrate 自动生成一个模型的schema和权限的定义
|
|
func AutoMigrate(ctx context.Context, db *gorm.DB, model any, cbs ...Option) (err error) {
|
|
var (
|
|
mv *rest.Model
|
|
)
|
|
opts := newOptions(cbs...)
|
|
if mm, ok := model.(adminTypes.ModuleModel); ok {
|
|
moduleName := mm.ModuleName()
|
|
opts.restOpts = append(opts.restOpts, rest.WithModuleName(moduleName))
|
|
}
|
|
if mv, err = rest.AutoMigrate(ctx, model, opts.restOpts...); err != nil {
|
|
return
|
|
}
|
|
err = checkModel(opts, mv)
|
|
return
|
|
}
|
|
|
|
func registerRESTRoute(domain string, db *gorm.DB, hs *http.Server) {
|
|
handleListSchemas := func(ctx *http.Context) (err error) {
|
|
var (
|
|
schemas []*restTypes.Schema
|
|
)
|
|
scenario := ctx.Request().URL.Query().Get("scenario")
|
|
if scenario == "" {
|
|
schemas, err = dbcache.TryCache(
|
|
ctx.Request().Context(),
|
|
fmt.Sprintf("rest:schems:%s:%s", ctx.Param("module"), ctx.Param("table")),
|
|
func(tx *gorm.DB) ([]*restTypes.Schema, error) {
|
|
return rest.GetSchemas(
|
|
ctx.Request().Context(),
|
|
tx,
|
|
"",
|
|
ctx.Param("module"),
|
|
ctx.Param("table"),
|
|
)
|
|
},
|
|
dbcache.WithDB(db),
|
|
dbcache.WithDependency(dbcache.NewSqlDependency("SELECT MAX(`updated_at`) FROM `schemas`")),
|
|
)
|
|
} else {
|
|
schemas, err = dbcache.TryCache(
|
|
ctx.Request().Context(),
|
|
fmt.Sprintf("rest:schems:%s:%s", ctx.Param("module"), ctx.Param("table")),
|
|
func(tx *gorm.DB) ([]*restTypes.Schema, error) {
|
|
return rest.VisibleSchemas(
|
|
ctx.Request().Context(),
|
|
db.WithContext(ctx.Request().Context()),
|
|
"",
|
|
ctx.Param("module"),
|
|
ctx.Param("table"),
|
|
scenario,
|
|
)
|
|
},
|
|
dbcache.WithDB(db),
|
|
dbcache.WithDependency(dbcache.NewSqlDependency("SELECT MAX(`updated_at`) FROM `schemas`")),
|
|
)
|
|
}
|
|
if err != nil {
|
|
return ctx.Error(errors.NotFound, err.Error())
|
|
} else {
|
|
return ctx.Success(schemas)
|
|
}
|
|
}
|
|
|
|
handleUpdateSchemas := func(ctx *http.Context) (err error) {
|
|
schemas := make([]*restTypes.Schema, 0)
|
|
if err = ctx.Bind(&schemas); err != nil {
|
|
return ctx.Error(errors.Invalid, err.Error())
|
|
}
|
|
for i := range schemas {
|
|
schemas[i].Domain = domain
|
|
}
|
|
if err = db.WithContext(ctx.Request().Context()).Transaction(func(tx *gorm.DB) (errTx error) {
|
|
for _, row := range schemas {
|
|
if errTx = tx.Save(row).Error; errTx != nil {
|
|
return
|
|
}
|
|
}
|
|
return
|
|
}); err == nil {
|
|
return ctx.Success(map[string]interface{}{
|
|
"count": len(schemas),
|
|
"state": "success",
|
|
})
|
|
} else {
|
|
return ctx.Error(errors.Unavailable, err.Error())
|
|
}
|
|
}
|
|
|
|
handleDeleteSchema := func(ctx *http.Context) (err error) {
|
|
id, _ := strconv.Atoi(ctx.Param("id"))
|
|
model := &restTypes.Schema{Id: uint64(id)}
|
|
if err = db.WithContext(ctx.Request().Context()).Delete(model).Error; err == nil {
|
|
return ctx.Success(map[string]any{
|
|
"id": id,
|
|
})
|
|
} else {
|
|
return ctx.Error(errors.Unavailable, err.Error())
|
|
}
|
|
}
|
|
|
|
hs.GET("/rest/schema/:module/:table", handleListSchemas)
|
|
hs.PUT("/rest/schema/:module/:table", handleUpdateSchemas)
|
|
hs.DELETE("/rest/schema/:id", handleDeleteSchema)
|
|
|
|
}
|
|
|
|
// Init 初始化模块
|
|
func Init(ctx context.Context, cbs ...Option) (err error) {
|
|
opts := newOptions(cbs...)
|
|
if err = initREST(ctx, opts); err != nil {
|
|
return
|
|
}
|
|
if err = initModels(ctx, opts); err != nil {
|
|
return
|
|
}
|
|
if opts.httpServer != nil {
|
|
registerRESTRoute(opts.domain, opts.db, opts.httpServer)
|
|
}
|
|
if !opts.disableDefault {
|
|
if err = migrate.Default(opts.db); err != nil {
|
|
return
|
|
}
|
|
}
|
|
if !opts.disableRecorder {
|
|
//启用操作记录
|
|
NewActivityRecorder(ctx, opts.db).Recoder()
|
|
}
|
|
|
|
//注册渲染器
|
|
NewFormatter(opts.db, opts.cache).Register()
|
|
return
|
|
}
|