优化结构

This commit is contained in:
Yvan
2026-08-24 21:50:33 +08:00
parent d3c20c578f
commit 969afee284
20 changed files with 497 additions and 244 deletions
+16 -8
View File
@@ -303,7 +303,7 @@ func (d *Data) reloadConfig(ctx context.Context) error {
if configPath == "" {
return fmt.Errorf("configuration path is not set")
}
next, err := readBootstrap(configPath)
next, err := config.Load(configPath)
if err != nil {
return err
}
@@ -406,7 +406,7 @@ func (d *Data) reloadConfig(ctx context.Context) error {
closeDatabaseList(candidateDBList)
}
}()
integrationConfigs, err := readIntegrationRuntime(candidateDB)
integrationConfigs, err := readIntegrationRuntime(candidateDB.WithContext(ctx))
if err != nil {
return fmt.Errorf("reload integration runtime: %w", err)
}
@@ -417,8 +417,20 @@ func (d *Data) reloadConfig(ctx context.Context) error {
registerDataScopeCallbacks(item, d.enqueueDataScopeAudit)
}
d.replaceDatabaseList(candidateDBList)
d.redis.replace(candidateRedis)
d.replaceRedisList(candidateRedisList)
// openRedis and openRedisList return nil when a ping fails, so replacing
// unconditionally would let a transient Redis outage during an unrelated
// configuration reload retire the still-healthy clients and silently downgrade
// the process to the in-memory cache until the next reload.
if candidateRedis != nil || !useRedis || !redisConnectionConfigured(next.Data.Redis) {
d.redis.replace(candidateRedis)
} else {
d.logger().Warn("keeping the previous redis client because the reloaded configuration failed to connect", "mod", "redis")
}
if candidateRedisList != nil || !useRedisList || len(next.Data.RedisList) == 0 {
d.replaceRedisList(candidateRedisList)
} else {
d.logger().Warn("keeping the previous redis list because the reloaded configuration failed to connect", "mod", "redis")
}
if mongoErr == nil {
d.mongo.replace(candidateMongo)
mongoAccepted = true
@@ -437,7 +449,3 @@ func (d *Data) reloadConfig(ctx context.Context) error {
d.notifyResources()
return nil
}
func readBootstrap(configPath string) (*config.Config, error) {
return config.Load(configPath)
}
+60 -33
View File
@@ -58,10 +58,14 @@ type Data struct {
storage *storage.Reloadable
dbListMu sync.RWMutex
dbList map[string]*gorm.DB
appLogger *slog.Logger
auditLog *dataScopeAuditWriter
catalog module.Catalog
resourceHook func()
// Handles replaced by a hot reload of the named lists. They follow the same
// grace period as the primary pool so in-flight queries are not cut off.
retiredDBList retiredSet[*gorm.DB]
retiredRedisList retiredSet[redis.UniversalClient]
appLogger *slog.Logger
auditLog *dataScopeAuditWriter
catalog module.Catalog
resourceHook func()
}
// DB exposes the active primary database to narrowly scoped data submodules.
@@ -100,7 +104,16 @@ func (d *Data) IntegrationRuntime() *runtimeconfig.Store {
// Database resolves the primary or a named database for repositories such as
// the system export module.
func (d *Data) Database(name string) (*gorm.DB, error) {
return d.database(name)
if name == "" {
return d.gormDB.DB(), nil
}
d.dbListMu.RLock()
db := d.dbList[name]
d.dbListMu.RUnlock()
if db == nil {
return nil, fmt.Errorf("database %q not found", name)
}
return db, nil
}
func (d *Data) RedisClient() redis.UniversalClient {
@@ -190,13 +203,13 @@ func (d *Data) logger() *slog.Logger {
return slog.Default()
}
func openDatabaseList(configs []*config.Database, appLogger ...*slog.Logger) (map[string]*gorm.DB, error) {
func openDatabaseList(configs []*config.Database, appLogger *slog.Logger) (map[string]*gorm.DB, error) {
items := make(map[string]*gorm.DB)
for _, config := range configs {
if config == nil || config.Disable || config.AliasName == "" {
continue
}
db, err := openDatabase(config, false, "", appLogger...)
db, err := openDatabase(config, false, "", appLogger)
if err != nil {
for _, opened := range items {
if sqlDB, dbErr := opened.DB(); dbErr == nil {
@@ -212,31 +225,26 @@ func openDatabaseList(configs []*config.Database, appLogger ...*slog.Logger) (ma
func closeDatabaseList(items map[string]*gorm.DB) {
for _, db := range items {
if sqlDB, err := db.DB(); err == nil {
_ = sqlDB.Close()
if db != nil {
closeGormDB(db)
}
}
}
// replaceDatabaseList swaps the named database handles. Superseded handles are
// retired instead of closed on the spot: a request that already resolved a
// handle would otherwise fail with "sql: database is closed" mid-flight.
func (d *Data) replaceDatabaseList(items map[string]*gorm.DB) {
d.dbListMu.Lock()
old := d.dbList
d.dbList = items
d.dbListMu.Unlock()
closeDatabaseList(old)
}
func (d *Data) database(name string) (*gorm.DB, error) {
if name == "" {
return d.gormDB.DB(), nil
for name, db := range old {
if db == nil || db == items[name] {
continue
}
d.retiredDBList.retire(db, closeGormDB)
}
d.dbListMu.RLock()
db := d.dbList[name]
d.dbListMu.RUnlock()
if db == nil {
return nil, fmt.Errorf("database %q not found", name)
}
return db, nil
}
func NewData(runtime *config.Store, appLogger *slog.Logger, storageManager *storage.Reloadable, catalog module.Catalog) (*Data, func(), error) {
@@ -264,10 +272,16 @@ func NewData(runtime *config.Store, appLogger *slog.Logger, storageManager *stor
d.gormDB.close()
}
closeDatabaseList(d.dbList)
for _, db := range d.retiredDBList.drain() {
closeGormDB(db)
}
if d.redis != nil {
d.redis.close()
}
closeRedisList(d.redisList)
for _, client := range d.retiredRedisList.drain() {
closeRedisClient(client)
}
if d.mongo != nil {
d.mongo.close()
}
@@ -363,10 +377,20 @@ func NewData(runtime *config.Store, appLogger *slog.Logger, storageManager *stor
return d, cleanup, nil
}
func openRedis(config *config.Redis, enabled bool, appLogger ...*slog.Logger) redis.UniversalClient {
if !enabled || config == nil || (config.Addr == "" && len(config.ClusterAddrs) == 0) {
// redisConnectionConfigured reports whether a Redis block names an endpoint.
// The reload path shares it with openRedis so "no endpoint configured" is never
// confused with "configured endpoint failed to answer".
func redisConnectionConfigured(config *config.Redis) bool {
return config != nil && (config.Addr != "" || len(config.ClusterAddrs) > 0)
}
func openRedis(config *config.Redis, enabled bool, appLogger *slog.Logger) redis.UniversalClient {
if !enabled || !redisConnectionConfigured(config) {
return nil
}
if appLogger == nil {
appLogger = slog.Default()
}
var candidate redis.UniversalClient
if config.UseCluster {
addresses := config.ClusterAddrs
@@ -387,18 +411,14 @@ func openRedis(config *config.Redis, enabled bool, appLogger ...*slog.Logger) re
pingCtx, cancel := context.WithTimeout(context.Background(), 800*time.Millisecond)
defer cancel()
if err := candidate.Ping(pingCtx).Err(); err != nil {
log := slog.Default()
if len(appLogger) > 0 && appLogger[0] != nil {
log = appLogger[0]
}
log.Warn("redis unavailable, using in-memory cache", "mod", "redis", "error", err)
appLogger.Warn("redis unavailable, using in-memory cache", "mod", "redis", "error", err)
_ = candidate.Close()
return nil
}
return candidate
}
func openRedisList(configs []*config.Redis, enabled bool, appLogger ...*slog.Logger) map[string]redis.UniversalClient {
func openRedisList(configs []*config.Redis, enabled bool, appLogger *slog.Logger) map[string]redis.UniversalClient {
if !enabled || len(configs) == 0 {
return nil
}
@@ -407,7 +427,7 @@ func openRedisList(configs []*config.Redis, enabled bool, appLogger ...*slog.Log
if item == nil || item.Name == "" {
continue
}
if client := openRedis(item, true, appLogger...); client != nil {
if client := openRedis(item, true, appLogger); client != nil {
clients[item.Name] = client
}
}
@@ -420,11 +440,13 @@ func openRedisList(configs []*config.Redis, enabled bool, appLogger ...*slog.Log
func closeRedisList(clients map[string]redis.UniversalClient) {
for _, client := range clients {
if client != nil {
_ = client.Close()
closeRedisClient(client)
}
}
}
// replaceRedisList mirrors replaceDatabaseList: superseded clients stay open for
// the retire grace period so in-flight commands are not aborted.
func (d *Data) replaceRedisList(clients map[string]redis.UniversalClient) {
if d == nil {
return
@@ -433,7 +455,12 @@ func (d *Data) replaceRedisList(clients map[string]redis.UniversalClient) {
old := d.redisList
d.redisList = clients
d.redisListMu.Unlock()
closeRedisList(old)
for name, client := range old {
if client == nil || client == clients[name] {
continue
}
d.retiredRedisList.retire(client, closeRedisClient)
}
}
func (d *Data) activateDatabase(db *gorm.DB, config *config.Database) {
+1 -1
View File
@@ -20,7 +20,7 @@ func (dataScopeRecord) TableName() string { return "business_scope_records" }
func newDataScopeTestDB(t *testing.T) *gorm.DB {
t.Helper()
db, err := openWithDriver("sqlite", "file:"+t.Name()+"?mode=memory&cache=shared")
db, err := openWithDriver("sqlite", "file:"+t.Name()+"?mode=memory&cache=shared", nil)
if err != nil {
t.Fatal(err)
}
+12 -16
View File
@@ -118,7 +118,7 @@ func databaseDSN(c *config.Database, name string) (string, error) {
return "", fmt.Errorf("unsupported database driver %q", c.Driver)
}
func gormConfig(config *config.Database, appLogger ...*slog.Logger) *gorm.Config {
func gormConfig(config *config.Database, appLogger *slog.Logger) *gorm.Config {
level := logger.Info
switch strings.ToLower(config.LogMode) {
case "silent":
@@ -128,19 +128,15 @@ func gormConfig(config *config.Database, appLogger ...*slog.Logger) *gorm.Config
case "warn":
level = logger.Warn
}
var log *slog.Logger
if len(appLogger) > 0 {
log = appLogger[0]
}
return &gorm.Config{Logger: gormkit.NewLogger(log, level), NamingStrategy: schema.NamingStrategy{TablePrefix: config.Prefix, SingularTable: config.Singular}}
return &gorm.Config{Logger: gormkit.NewLogger(appLogger, level), NamingStrategy: schema.NamingStrategy{TablePrefix: config.Prefix, SingularTable: config.Singular}}
}
func openWithDriver(driver, dsn string, appLogger ...*slog.Logger) (*gorm.DB, error) {
return openWithDriverConfig(driver, dsn, &config.Database{Driver: driver}, appLogger...)
func openWithDriver(driver, dsn string, appLogger *slog.Logger) (*gorm.DB, error) {
return openWithDriverConfig(driver, dsn, &config.Database{Driver: driver}, appLogger)
}
func openWithDriverConfig(driver, dsn string, config *config.Database, appLogger ...*slog.Logger) (*gorm.DB, error) {
gormConfig := gormConfig(config, appLogger...)
func openWithDriverConfig(driver, dsn string, config *config.Database, appLogger *slog.Logger) (*gorm.DB, error) {
gormConfig := gormConfig(config, appLogger)
var db *gorm.DB
var err error
switch normalizedDriver(driver) {
@@ -179,7 +175,7 @@ func openWithDriverConfig(driver, dsn string, config *config.Database, appLogger
return db, nil
}
func openDatabase(c *config.Database, create bool, template string, appLogger ...*slog.Logger) (*gorm.DB, error) {
func openDatabase(c *config.Database, create bool, template string, appLogger *slog.Logger) (*gorm.DB, error) {
driver := normalizedDriver(c.Driver)
if driver == "" {
return nil, fmt.Errorf("unsupported database driver %q", c.Driver)
@@ -192,7 +188,7 @@ func openDatabase(c *config.Database, create bool, template string, appLogger ..
if err = os.MkdirAll(filepath.Dir(dsn), 0o755); err != nil {
return nil, err
}
return openWithDriverConfig(driver, dsn, c, appLogger...)
return openWithDriverConfig(driver, dsn, c, appLogger)
}
if create && driver != "oracle" {
if !databaseNamePattern.MatchString(c.Name) {
@@ -209,7 +205,7 @@ func openDatabase(c *config.Database, create bool, template string, appLogger ..
if err != nil {
return nil, err
}
adminDB, err := openWithDriverConfig(driver, dsn, c, appLogger...)
adminDB, err := openWithDriverConfig(driver, dsn, c, appLogger)
if err != nil {
return nil, fmt.Errorf("connect database server: %w", err)
}
@@ -245,9 +241,9 @@ func openDatabase(c *config.Database, create bool, template string, appLogger ..
if err != nil {
return nil, err
}
return openWithDriverConfig(driver, dsn, c, appLogger...)
return openWithDriverConfig(driver, dsn, c, appLogger)
}
func openFallbackDatabase(appLogger ...*slog.Logger) (*gorm.DB, error) {
return openWithDriver("sqlite", "file:kra-bootstrap?mode=memory&cache=shared", appLogger...)
func openFallbackDatabase(appLogger *slog.Logger) (*gorm.DB, error) {
return openWithDriver("sqlite", "file:kra-bootstrap?mode=memory&cache=shared", appLogger)
}
+2 -2
View File
@@ -18,7 +18,7 @@ import (
func openIntegrationConfigTestDB(t *testing.T) *gorm.DB {
t.Helper()
db, err := openWithDriver("sqlite", "file:"+t.Name()+"?mode=memory&cache=shared")
db, err := openWithDriver("sqlite", "file:"+t.Name()+"?mode=memory&cache=shared", nil)
if err != nil {
t.Fatal(err)
}
@@ -34,7 +34,7 @@ func openIntegrationConfigTestDB(t *testing.T) *gorm.DB {
}
func TestMigrateAllCreatesIntegrationConfigTable(t *testing.T) {
db, err := openWithDriver("sqlite", "file:"+t.Name()+"?mode=memory&cache=shared")
db, err := openWithDriver("sqlite", "file:"+t.Name()+"?mode=memory&cache=shared", nil)
if err != nil {
t.Fatal(err)
}
+1 -1
View File
@@ -15,7 +15,7 @@ func testCatalog() platformmodule.Catalog {
}
func TestMigrateAllRunsModuleSchemasWithoutBootstrapSeed(t *testing.T) {
db, err := openWithDriver("sqlite", "file:"+t.Name()+"?mode=memory&cache=shared")
db, err := openWithDriver("sqlite", "file:"+t.Name()+"?mode=memory&cache=shared", nil)
if err != nil {
t.Fatal(err)
}
+110 -62
View File
@@ -4,60 +4,63 @@ import (
"context"
"sync"
"sync/atomic"
"time"
"github.com/redis/go-redis/v9"
"go.mongodb.org/mongo-driver/mongo"
"gorm.io/gorm"
)
// reloadableDB makes the pointer swap atomic. Replaced pools are retained
// until application shutdown so in-flight GORM operations remain valid.
type reloadableDB struct {
current atomic.Pointer[gorm.DB]
mu sync.Mutex
retired []*gorm.DB
// retireGrace bounds how long a client replaced by a hot reload stays open.
// In-flight operations still hold the old handle, so it cannot be closed at
// swap time; keeping it until shutdown would leak one full pool per reload.
const retireGrace = 5 * time.Minute
// retiredSet holds clients replaced by a hot reload and closes each of them
// once the grace period expires. Whatever is still pending at shutdown is
// drained by the owner's close.
type retiredSet[T comparable] struct {
mu sync.Mutex
items []T
}
type reloadableMongo struct {
mu sync.RWMutex
current *mongo.Client
retired []*mongo.Client
func (s *retiredSet[T]) retire(item T, closeItem func(T)) {
s.mu.Lock()
s.items = append(s.items, item)
s.mu.Unlock()
time.AfterFunc(retireGrace, func() {
if s.take(item) {
closeItem(item)
}
})
}
func newReloadableMongo(client *mongo.Client) *reloadableMongo {
return &reloadableMongo{current: client}
}
func (r *reloadableMongo) replace(client *mongo.Client) {
r.mu.Lock()
old := r.current
r.current = client
if old != nil && old != client {
r.retired = append(r.retired, old)
}
r.mu.Unlock()
}
func (r *reloadableMongo) load() *mongo.Client {
if r == nil {
return nil
}
r.mu.RLock()
client := r.current
r.mu.RUnlock()
return client
}
func (r *reloadableMongo) close() {
r.mu.Lock()
all := append([]*mongo.Client{r.current}, r.retired...)
r.current = nil
r.retired = nil
r.mu.Unlock()
for _, client := range all {
if client != nil {
_ = client.Disconnect(context.Background())
// take removes item and reports whether this caller now owns closing it.
func (s *retiredSet[T]) take(item T) bool {
s.mu.Lock()
defer s.mu.Unlock()
for index, existing := range s.items {
if existing == item {
s.items = append(s.items[:index], s.items[index+1:]...)
return true
}
}
return false
}
func (s *retiredSet[T]) drain() []T {
s.mu.Lock()
defer s.mu.Unlock()
items := s.items
s.items = nil
return items
}
// reloadableDB makes the pointer swap atomic. Replaced pools stay open for
// retireGrace so in-flight GORM operations remain valid.
type reloadableDB struct {
current atomic.Pointer[gorm.DB]
retired retiredSet[*gorm.DB]
}
func newReloadableDB(db *gorm.DB, enqueue dataScopeAuditEnqueue) *reloadableDB {
@@ -77,18 +80,12 @@ func (r *reloadableDB) replace(db *gorm.DB, enqueue dataScopeAuditEnqueue) {
registerDataScopeCallbacks(db, enqueue)
old := r.current.Swap(db)
if old != nil && old != db {
r.mu.Lock()
r.retired = append(r.retired, old)
r.mu.Unlock()
r.retired.retire(old, closeGormDB)
}
}
func (r *reloadableDB) close() {
current := r.current.Load()
r.mu.Lock()
all := append([]*gorm.DB{current}, r.retired...)
r.retired = nil
r.mu.Unlock()
all := append([]*gorm.DB{r.current.Load()}, r.retired.drain()...)
seen := map[*gorm.DB]struct{}{}
for _, db := range all {
if db == nil {
@@ -98,16 +95,65 @@ func (r *reloadableDB) close() {
continue
}
seen[db] = struct{}{}
if sqlDB, err := db.DB(); err == nil {
_ = sqlDB.Close()
closeGormDB(db)
}
}
func closeGormDB(db *gorm.DB) {
if sqlDB, err := db.DB(); err == nil {
_ = sqlDB.Close()
}
}
type reloadableMongo struct {
mu sync.RWMutex
current *mongo.Client
retired retiredSet[*mongo.Client]
}
func newReloadableMongo(client *mongo.Client) *reloadableMongo {
return &reloadableMongo{current: client}
}
func (r *reloadableMongo) replace(client *mongo.Client) {
r.mu.Lock()
old := r.current
r.current = client
r.mu.Unlock()
if old != nil && old != client {
r.retired.retire(old, closeMongoClient)
}
}
func (r *reloadableMongo) load() *mongo.Client {
if r == nil {
return nil
}
r.mu.RLock()
defer r.mu.RUnlock()
return r.current
}
func (r *reloadableMongo) close() {
r.mu.Lock()
current := r.current
r.current = nil
r.mu.Unlock()
for _, client := range append([]*mongo.Client{current}, r.retired.drain()...) {
if client != nil {
closeMongoClient(client)
}
}
}
func closeMongoClient(client *mongo.Client) {
_ = client.Disconnect(context.Background())
}
type reloadableRedis struct {
mu sync.RWMutex
current redis.UniversalClient
retired []redis.UniversalClient
retired retiredSet[redis.UniversalClient]
}
func newReloadableRedis(client redis.UniversalClient) *reloadableRedis {
@@ -124,22 +170,24 @@ func (r *reloadableRedis) replace(client redis.UniversalClient) {
r.mu.Lock()
old := r.current
r.current = client
if old != nil && old != client {
r.retired = append(r.retired, old)
}
r.mu.Unlock()
if old != nil && old != client {
r.retired.retire(old, closeRedisClient)
}
}
func (r *reloadableRedis) close() {
r.mu.Lock()
all := append([]redis.UniversalClient{r.current}, r.retired...)
current := r.current
r.current = nil
r.retired = nil
r.mu.Unlock()
for _, client := range all {
if client == nil {
continue
for _, client := range append([]redis.UniversalClient{current}, r.retired.drain()...) {
if client != nil {
closeRedisClient(client)
}
_ = client.Close()
}
}
func closeRedisClient(client redis.UniversalClient) {
_ = client.Close()
}
+33 -1
View File
@@ -208,5 +208,37 @@ func defaultMenus() []menuPO {
value.KeepAlive = true
return value
}
return []menuPO{{Path: "dashboard", Name: "dashboard", Component: "view/dashboard/index.vue", Title: "仪表盘", Icon: "odometer", Sort: 1}, root("permission", "permission", "权限管理", "perm-kra", 2), root("org", "org", "组织管理", "share", 3), root("systemConfig", "systemConfig", "系统设置", "config-kra", 4), root("monitor", "monitor", "运维监控", "monitor-kra", 5), root("media", "media", "媒体管理", "folder-opened", 6), root("extensions", "extensions", "扩展功能", "cherry", 10), {Path: "person", Name: "person", Component: "view/person/person.vue", Title: "个人信息", Icon: "postcard", Hidden: true, Sort: 13}, child("permission", "authority", "authority", "view/superAdmin/authority/authority.vue", "角色管理", "role-kra", 1), cachedChild("permission", "menu", "menu", "view/superAdmin/menu/menu.vue", "菜单管理", "tickets", 2), cachedChild("permission", "api", "api", "view/superAdmin/api/api.vue", "api管理", "api-kra", 3), child("permission", "apiToken", "apiToken", "view/systemTools/apiToken/index.vue", "API Token", "key", 4), child("org", "user", "user", "view/superAdmin/user/user.vue", "用户管理", "user", 1), child("org", "department", "department", "view/superAdmin/department/department.vue", "部门管理", "office-building", 2), child("org", "position", "position", "view/superAdmin/position/position.vue", "岗位管理", "postcard", 3), child("systemConfig", "system", "system", "view/systemTools/system/system.vue", "配置文件", "config-file-kra", 1), child("systemConfig", "dictionary", "dictionary", "view/superAdmin/dictionary/sysDictionary.vue", "字典管理", "notebook", 2), child("systemConfig", "sysParams", "sysParams", "view/superAdmin/params/sysParams.vue", "参数管理", "set-up", 3), child("systemConfig", "security", "security", "view/system/security/index.vue", "安全配置", "security-kra", 4), child("monitor", "operation", "operation", "view/superAdmin/operation/sysOperationRecord.vue", "操作历史", "document", 1), child("monitor", "loginLog", "loginLog", "view/systemTools/loginLog/index.vue", "登录日志", "clock", 2), child("monitor", "sysError", "sysError", "view/systemTools/sysError/sysError.vue", "错误日志", "error-kra", 3), child("monitor", "sysVersion", "sysVersion", "view/systemTools/version/version.vue", "版本管理", "version-kra", 4), child("monitor", "state", "state", "view/system/state.vue", "服务器状态", "server", 5), child("monitor", "dataAccessLog", "dataAccessLog", "view/superAdmin/dataAccessLog/dataAccessLog.vue", "数据权限审计", "warning", 6), child("monitor", "timedTask", "timedTask", "view/systemTools/timedTask/index.vue", "定时任务", "timer", 7), child("monitor", "logViewer", "logViewer", "view/systemTools/logViewer/index.vue", "文件日志", "document", 8), child("media", "upload", "upload", "view/media/upload.vue", "媒体库(上传下载)", "upload", 1), child("media", "chunkUpload", "chunkUpload", "view/media/chunkUpload.vue", "大文件上传", "folder-add", 2), child("extensions", "email", "email", "modules/email/view/index.vue", "邮件发送", "message", 4), child("extensions", "anInfo", "anInfo", "modules/announcement/view/info.vue", "公告管理", "bell", 5)}
return []menuPO{
{Path: "dashboard", Name: "dashboard", Component: "view/dashboard/index.vue", Title: "仪表盘", Icon: "odometer", Sort: 1},
root("permission", "permission", "权限管理", "perm-kra", 2),
root("org", "org", "组织管理", "share", 3),
root("systemConfig", "systemConfig", "系统设置", "config-kra", 4),
root("monitor", "monitor", "运维监控", "monitor-kra", 5),
root("media", "media", "媒体管理", "folder-opened", 6),
root("extensions", "extensions", "扩展功能", "cherry", 10),
{Path: "person", Name: "person", Component: "view/person/person.vue", Title: "个人信息", Icon: "postcard", Hidden: true, Sort: 13},
child("permission", "authority", "authority", "view/superAdmin/authority/authority.vue", "角色管理", "role-kra", 1),
cachedChild("permission", "menu", "menu", "view/superAdmin/menu/menu.vue", "菜单管理", "tickets", 2),
cachedChild("permission", "api", "api", "view/superAdmin/api/api.vue", "api管理", "api-kra", 3),
child("permission", "apiToken", "apiToken", "view/systemTools/apiToken/index.vue", "API Token", "key", 4),
child("org", "user", "user", "view/superAdmin/user/user.vue", "用户管理", "user", 1),
child("org", "department", "department", "view/superAdmin/department/department.vue", "部门管理", "office-building", 2),
child("org", "position", "position", "view/superAdmin/position/position.vue", "岗位管理", "postcard", 3),
child("systemConfig", "system", "system", "view/systemTools/system/system.vue", "配置文件", "config-file-kra", 1),
child("systemConfig", "dictionary", "dictionary", "view/superAdmin/dictionary/sysDictionary.vue", "字典管理", "notebook", 2),
child("systemConfig", "sysParams", "sysParams", "view/superAdmin/params/sysParams.vue", "参数管理", "set-up", 3),
child("systemConfig", "security", "security", "view/system/security/index.vue", "安全配置", "security-kra", 4),
child("monitor", "operation", "operation", "view/superAdmin/operation/sysOperationRecord.vue", "操作历史", "document", 1),
child("monitor", "loginLog", "loginLog", "view/systemTools/loginLog/index.vue", "登录日志", "clock", 2),
child("monitor", "sysError", "sysError", "view/systemTools/sysError/sysError.vue", "错误日志", "error-kra", 3),
child("monitor", "sysVersion", "sysVersion", "view/systemTools/version/version.vue", "版本管理", "version-kra", 4),
child("monitor", "state", "state", "view/system/state.vue", "服务器状态", "server", 5),
child("monitor", "dataAccessLog", "dataAccessLog", "view/superAdmin/dataAccessLog/dataAccessLog.vue", "数据权限审计", "warning", 6),
child("monitor", "timedTask", "timedTask", "view/systemTools/timedTask/index.vue", "定时任务", "timer", 7),
child("monitor", "logViewer", "logViewer", "view/systemTools/logViewer/index.vue", "文件日志", "document", 8),
child("media", "upload", "upload", "view/media/upload.vue", "媒体库(上传下载)", "upload", 1),
child("media", "chunkUpload", "chunkUpload", "view/media/chunkUpload.vue", "大文件上传", "folder-add", 2),
child("extensions", "email", "email", "modules/email/view/index.vue", "邮件发送", "message", 4),
child("extensions", "anInfo", "anInfo", "modules/announcement/view/info.vue", "公告管理", "bell", 5),
}
}