aeus-admin/server.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
}