feat: complete Kra admin backend migration

This commit is contained in:
2026-08-14 00:28:37 +08:00
parent c4bf7ee03b
commit acba9624d7
88 changed files with 1765 additions and 8623 deletions
+124 -12
View File
@@ -4,6 +4,8 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"kra/internal/biz"
@@ -11,6 +13,8 @@ import (
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"gorm.io/gorm"
)
@@ -86,28 +90,72 @@ type authorityMenuPO struct {
func (authorityMenuPO) TableName() string { return "sys_authority_menus" }
type menuParameterPO struct {
ID uint `gorm:"primaryKey"`
MenuID uint `gorm:"column:sys_base_menu_id;index"`
Type string
Key string
Value string
}
func (menuParameterPO) TableName() string { return "sys_base_menu_parameters" }
type systemRepo struct{ data *Data }
func NewSystemRepo(data *Data) biz.SystemRepo { return &systemRepo{data: data} }
func (r *systemRepo) PersistConfig(context.Context) error { return r.data.persistConfig() }
func (r *systemRepo) PersistAdminConfig(_ context.Context, raw []byte) error {
next := proto.Clone(r.data.admin).(*conf.AdminBackend)
if err := (protojson.UnmarshalOptions{DiscardUnknown: true}).Unmarshal(raw, next); err != nil {
return err
}
next.ConfigPath = r.data.admin.ConfigPath
return r.data.persistConfigValues(r.data.config, next)
}
func (r *systemRepo) ReloadConfig(ctx context.Context) error {
return r.data.reloadConfig(ctx)
}
func (r *systemRepo) IsInitialized(ctx context.Context) (bool, error) {
return r.data.gormDB.WithContext(ctx).Migrator().HasTable(&userPO{}), nil
db := r.data.gormDB.WithContext(ctx)
if !db.Migrator().HasTable(&userPO{}) {
return false, nil
}
var count int64
if err := db.Model(&userPO{}).Where("username = ?", "admin").Count(&count).Error; err != nil {
return false, err
}
return count > 0, nil
}
func (r *systemRepo) Initialize(ctx context.Context, input *biz.DatabaseConfig) error {
config := &conf.Data_Database{Driver: input.Driver, Host: input.Host, Port: input.Port, User: input.User, Password: input.Password, Name: input.Name, Path: input.Path, Config: input.Config}
if err := r.data.switchDatabase(config); err != nil {
r.data.initMu.Lock()
defer r.data.initMu.Unlock()
initialized, err := r.IsInitialized(ctx)
if err != nil {
return err
}
db := r.data.gormDB.WithContext(ctx)
if err := db.AutoMigrate(&adminPO{}); err != nil {
if initialized {
return errors.New("数据库已初始化,无需重复初始化")
}
candidate, err := openDatabase(config, true, input.Template)
if err != nil {
return err
}
if err := db.AutoMigrate(&userPO{}, &authorityPO{}, &menuPO{}, &userAuthorityPO{}, &authorityMenuPO{}, &apiPO{}, &ignoredAPIPO{}, &authorityAPIPO{}, &menuButtonPO{}, &authorityButtonPO{}, &departmentPO{}, &positionPO{}, &userDepartmentPO{}, &userPositionPO{}, &authorityDepartmentPO{}, &dictionaryPO{}, &dictionaryDetailPO{}, &parameterPO{}, &apiTokenPO{}, &securityConfigPO{}, &versionPO{}, &exportTemplatePO{}, &exportConditionPO{}, &exportJoinPO{}, &operationPO{}, &loginLogPO{}, &dataAccessLogPO{}, &errorRecordPO{}, &taskPO{}, &taskLogPO{}, &mediaPO{}, &categoryPO{}, &uploadSessionPO{}, &uploadChunkPO{}, &announcementPO{}); err != nil {
activated := false
defer func() {
if !activated {
if sqlDB, closeErr := candidate.DB(); closeErr == nil {
_ = sqlDB.Close()
}
}
}()
db := candidate.WithContext(ctx)
if err := migrateAll(db); err != nil {
return err
}
return db.Transaction(func(tx *gorm.DB) error {
if err := db.Transaction(func(tx *gorm.DB) error {
authority := authorityPO{AuthorityID: 888, AuthorityName: "超级管理员", DataScope: 1, DefaultRouter: "dashboard"}
if err := tx.FirstOrCreate(&authority, authorityPO{AuthorityID: 888}).Error; err != nil {
return err
@@ -127,10 +175,12 @@ func (r *systemRepo) Initialize(ctx context.Context, input *biz.DatabaseConfig)
nameID[menu.Name] = menu.ID
}
for i := range menus {
if menus[i].ParentID == 0 || menus[i].ActiveName == "" {
if menus[i].ActiveName == "" {
continue
}
_ = tx.Model(&menuPO{}).Where("name = ?", menus[i].Name).Updates(map[string]any{"parent_id": nameID[menus[i].ActiveName], "active_name": ""}).Error
if err := tx.Model(&menuPO{}).Where("name = ?", menus[i].Name).Updates(map[string]any{"parent_id": nameID[menus[i].ActiveName], "active_name": ""}).Error; err != nil {
return err
}
}
if err := tx.Where("sys_authority_authority_id = ?", 888).Delete(&authorityMenuPO{}).Error; err != nil {
return err
@@ -162,15 +212,56 @@ func (r *systemRepo) Initialize(ctx context.Context, input *biz.DatabaseConfig)
return err
}
}
department := departmentPO{Name: "总公司", ParentID: 0, Ancestors: "0", Sort: 0, Status: true}
if err := tx.Where("name = ?", department.Name).FirstOrCreate(&department).Error; err != nil {
return err
}
for _, position := range []positionPO{{Name: "总经理", Code: "CEO", Sort: 1, Status: true}, {Name: "普通员工", Code: "STAFF", Sort: 2, Status: true}} {
if err := tx.Where("code = ?", position.Code).FirstOrCreate(&position).Error; err != nil {
return err
}
}
security := defaultSecurityConfig()
if err := tx.FirstOrCreate(&security, securityConfigPO{ID: 1}).Error; err != nil {
return err
}
exportTemplate := exportTemplatePO{Name: "api", DBTableName: "sys_apis", TemplateID: "api", TemplateInfo: "{\n\"path\":\"路径\",\n\"method\":\"方法(大写)\",\n\"description\":\"方法介绍\",\n\"api_group\":\"方法分组\"\n}"}
if err := tx.Where("template_id = ?", exportTemplate.TemplateID).FirstOrCreate(&exportTemplate).Error; err != nil {
return err
}
for _, task := range []taskPO{{Name: "ClearDB", Description: "定时清理数据库过期日志(操作记录/JWT黑名单/定时任务执行日志)", Spec: "@daily", ExecutorType: "method", MethodName: "ClearDB", Enabled: true}, {Name: "CleanStaleUploads", Description: "定时清理过期大文件上传会话", Spec: "@hourly", ExecutorType: "method", MethodName: "CleanStaleUploads", Enabled: true}} {
if err := tx.Where("name = ?", task.Name).FirstOrCreate(&task).Error; err != nil {
return err
}
}
for _, item := range input.APIs {
if item == nil {
continue
}
po := apiPO{Path: item.Path, Method: strings.ToUpper(item.Method), Description: item.Description, APIGroup: item.APIGroup}
if err := tx.Where("path = ? AND method = ?", po.Path, po.Method).FirstOrCreate(&po).Error; err != nil {
return err
}
}
return nil
})
}); err != nil {
return err
}
nextData := proto.Clone(r.data.config).(*conf.Data)
nextData.Database = config
if err := r.data.persistConfigValues(nextData, r.data.admin); err != nil {
return fmt.Errorf("persist database configuration: %w", err)
}
r.data.activateDatabase(candidate, config)
activated = true
return nil
}
func (r *systemRepo) FindUserByUsername(ctx context.Context, username string) (*biz.User, error) {
var po userPO
if err := r.data.gormDB.WithContext(ctx).Where("username = ?", username).First(&po).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, biz.ErrAdminNotFound
return nil, biz.ErrUserNotFound
}
return nil, err
}
@@ -181,7 +272,7 @@ func (r *systemRepo) FindUserByID(ctx context.Context, id uint) (*biz.User, erro
var po userPO
if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, biz.ErrAdminNotFound
return nil, biz.ErrUserNotFound
}
return nil, err
}
@@ -245,7 +336,22 @@ func (r *systemRepo) MenusByAuthority(ctx context.Context, authorityID uint) ([]
}
byID := make(map[uint]*biz.Menu, len(pos))
for _, po := range pos {
byID[po.ID] = &biz.Menu{ID: po.ID, ParentID: po.ParentID, Path: po.Path, Name: po.Name, Hidden: po.Hidden, Component: po.Component, Sort: po.Sort, ActiveName: po.ActiveName, KeepAlive: po.KeepAlive, DefaultMenu: po.DefaultMenu, Title: po.Title, Icon: po.Icon, CloseTab: po.CloseTab, TransitionType: po.TransitionType, Children: []*biz.Menu{}}
menu := &biz.Menu{ID: po.ID, ParentID: po.ParentID, Path: po.Path, Name: po.Name, Hidden: po.Hidden, Component: po.Component, Sort: po.Sort, ActiveName: po.ActiveName, KeepAlive: po.KeepAlive, DefaultMenu: po.DefaultMenu, Title: po.Title, Icon: po.Icon, CloseTab: po.CloseTab, TransitionType: po.TransitionType, Children: []*biz.Menu{}}
var parameters []menuParameterPO
if err := r.data.gormDB.WithContext(ctx).Where("sys_base_menu_id = ?", po.ID).Order("id").Find(&parameters).Error; err != nil {
return nil, err
}
for _, parameter := range parameters {
menu.Parameters = append(menu.Parameters, &biz.MenuParameter{ID: parameter.ID, MenuID: parameter.MenuID, Type: parameter.Type, Key: parameter.Key, Value: parameter.Value})
}
var buttons []menuButtonPO
if err := r.data.gormDB.WithContext(ctx).Table("sys_base_menu_btns").Joins("JOIN sys_authority_btns ON sys_authority_btns.sys_base_menu_btn_id = sys_base_menu_btns.id").Where("sys_authority_btns.authority_id = ? AND sys_authority_btns.sys_menu_id = ?", authorityID, po.ID).Find(&buttons).Error; err != nil {
return nil, err
}
for _, button := range buttons {
menu.Buttons = append(menu.Buttons, &biz.MenuButton{ID: button.ID, Name: button.Name, Description: button.Description, MenuID: button.MenuID, AuthorityID: authorityID})
}
byID[po.ID] = menu
}
roots := make([]*biz.Menu, 0)
for _, po := range pos {
@@ -355,6 +461,12 @@ func (r *systemRepo) DeleteUser(ctx context.Context, id uint) error {
if err := tx.Where("sys_user_id = ?", id).Delete(&userAuthorityPO{}).Error; err != nil {
return err
}
if err := tx.Where("sys_user_id = ?", id).Delete(&userDepartmentPO{}).Error; err != nil {
return err
}
if err := tx.Where("sys_user_id = ?", id).Delete(&userPositionPO{}).Error; err != nil {
return err
}
return tx.Delete(&userPO{}, id).Error
})
}