package aeusadmin import ( "context" "fmt" "html/template" httpkg "net/http" "os" "path" "reflect" "strconv" "strings" "git.nobla.cn/golang/aeus-admin/defaults" "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) if err = db.Where("permission = ? AND menu = ?", permission, menuName).First(permissionModel).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { permissionModel.Menu = menuName permissionModel.Label = scene if translate != nil { permissionModel.Label = translate.Permission(model, scene, permissionModel.Label) } permissionModel.Permission = permission err = db.Create(permissionModel).Error } } return } func replaceModelPermission(db *gorm.DB, menuName string, permission string, label string) (permissionModel *models.Permission, err error) { permissionModel = &models.Permission{} if err = db.Where("permission = ? AND menu = ?", permission, menuName).First(permissionModel).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { permissionModel.Menu = menuName permissionModel.Label = label permissionModel.Permission = permission err = db.Create(permissionModel).Error } } 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 = replaceModelPermission(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 = defaults.Generate(opts.db); err != nil { return } } return }