fix: align admin behavior and Kratos integrations

This commit is contained in:
2026-08-14 14:14:51 +08:00
parent 8c4c6ba661
commit e0b0e1d8c8
123 changed files with 4334 additions and 1877 deletions
+1 -5
View File
@@ -93,11 +93,7 @@ func (r *announcementRepo) List(ctx context.Context, filter biz.AnnouncementFilt
return nil, 0, err
}
if filter.PageSize > 0 {
page := filter.Page
if page < 1 {
page = 1
}
db = db.Offset((page - 1) * filter.PageSize).Limit(filter.PageSize)
db = applyPagination(db, filter.Page, filter.PageSize, 100)
}
var pos []announcementPO
if err := db.Find(&pos).Error; err != nil {
+74 -13
View File
@@ -45,7 +45,12 @@ func apiFromPO(po apiPO) *biz.API {
return &biz.API{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Path: po.Path, Description: po.Description, APIGroup: po.APIGroup, Method: po.Method}
}
func (r *accessRepo) CreateAPI(ctx context.Context, v *biz.API) error {
return r.data.gormDB.WithContext(ctx).Create(&apiPO{Path: v.Path, Description: v.Description, APIGroup: v.APIGroup, Method: strings.ToUpper(v.Method)}).Error
po := apiPO{Path: v.Path, Description: v.Description, APIGroup: v.APIGroup, Method: strings.ToUpper(v.Method)}
if err := r.data.gormDB.WithContext(ctx).Create(&po).Error; err != nil {
return err
}
v.ID, v.CreatedAt, v.UpdatedAt, v.Method = po.ID, po.CreatedAt, po.UpdatedAt, po.Method
return nil
}
func (r *accessRepo) UpdateAPI(ctx context.Context, v *biz.API) error {
return r.data.gormDB.WithContext(ctx).Model(&apiPO{}).Where("id = ?", v.ID).Updates(map[string]any{"path": v.Path, "description": v.Description, "api_group": v.APIGroup, "method": strings.ToUpper(v.Method)}).Error
@@ -66,17 +71,26 @@ func (r *accessRepo) FindAPI(ctx context.Context, id uint) (*biz.API, error) {
return apiFromPO(po), nil
}
func (r *accessRepo) ListAPIs(ctx context.Context, page, size int, q *biz.API) ([]*biz.API, int64, error) {
if page < 1 {
page = 1
}
if size < 1 {
size = 10
}
db := r.data.gormDB.WithContext(ctx).Model(&apiPO{})
if q != nil && q.StrictAll {
config := r.data.runtime.Admin()
if actor, ok := biz.ActorFromContext(ctx); ok && config != nil && config.System != nil && config.System.UseStrictAuth {
var authority authorityPO
if err := r.data.gormDB.WithContext(ctx).Where("authority_id = ?", actor.AuthorityID).First(&authority).Error; err != nil {
return nil, 0, err
}
if authority.ParentID != nil && *authority.ParentID != 0 {
db = db.Where("id IN (?)", r.data.gormDB.WithContext(ctx).Model(&authorityAPIPO{}).Select("api_id").Where("authority_id = ?", actor.AuthorityID))
}
}
}
if q != nil {
if q.Path != "" {
db = db.Where("path LIKE ?", "%"+q.Path+"%")
}
if q.Description != "" {
db = db.Where("description LIKE ?", "%"+q.Description+"%")
}
if q.APIGroup != "" {
db = db.Where("api_group = ?", q.APIGroup)
}
@@ -88,8 +102,23 @@ func (r *accessRepo) ListAPIs(ctx context.Context, page, size int, q *biz.API) (
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
order := "id desc"
if q != nil && q.OrderKey != "" {
allowed := map[string]bool{"id": true, "path": true, "api_group": true, "description": true, "method": true}
if !allowed[q.OrderKey] {
return nil, 0, errors.New("非法的排序字段: " + q.OrderKey)
}
order = q.OrderKey
if q.Desc {
order += " desc"
}
}
var pos []apiPO
if err := db.Order("api_group,path,method").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
maxSize := 100
if q != nil && q.StrictAll {
maxSize = 0
}
if err := applyPagination(db.Order(order), page, size, maxSize).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.API, 0, len(pos))
@@ -98,19 +127,26 @@ func (r *accessRepo) ListAPIs(ctx context.Context, page, size int, q *biz.API) (
}
return out, total, nil
}
func (r *accessRepo) APIRoleIDs(ctx context.Context, id uint) ([]uint, error) {
func (r *accessRepo) APIRoleIDs(ctx context.Context, path, method string) ([]uint, error) {
var ids []uint
err := r.data.gormDB.WithContext(ctx).Model(&authorityAPIPO{}).Where("api_id = ?", id).Pluck("authority_id", &ids).Error
err := r.data.gormDB.WithContext(ctx).Table("sys_authority_apis").
Joins("JOIN sys_apis ON sys_apis.id = sys_authority_apis.api_id").
Where("sys_apis.path = ? AND sys_apis.method = ? AND sys_apis.deleted_at IS NULL", path, strings.ToUpper(method)).
Pluck("sys_authority_apis.authority_id", &ids).Error
return ids, err
}
func (r *accessRepo) SetAPIRoles(ctx context.Context, id uint, ids []uint) error {
func (r *accessRepo) SetAPIRoles(ctx context.Context, path, method string, ids []uint) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("api_id = ?", id).Delete(&authorityAPIPO{}).Error; err != nil {
var api apiPO
if err := tx.Where("path = ? AND method = ?", path, strings.ToUpper(method)).First(&api).Error; err != nil {
return err
}
if err := tx.Where("api_id = ?", api.ID).Delete(&authorityAPIPO{}).Error; err != nil {
return err
}
links := make([]authorityAPIPO, 0, len(ids))
for _, aid := range ids {
links = append(links, authorityAPIPO{AuthorityID: aid, APIID: id})
links = append(links, authorityAPIPO{AuthorityID: aid, APIID: api.ID})
}
if len(links) > 0 {
return tx.Create(&links).Error
@@ -159,6 +195,31 @@ func (r *accessRepo) PolicyPaths(ctx context.Context, aid uint) ([]*biz.API, err
return out, nil
}
func (r *accessRepo) SetPolicyPaths(ctx context.Context, aid uint, paths []*biz.API) error {
if err := r.checkAuthorityIDAuth(ctx, aid); err != nil {
return err
}
config := r.data.runtime.Admin()
if actor, ok := biz.ActorFromContext(ctx); ok && config != nil && config.System != nil && config.System.UseStrictAuth {
var authority authorityPO
if err := r.data.gormDB.WithContext(ctx).Where("authority_id = ?", actor.AuthorityID).First(&authority).Error; err != nil {
return err
}
if authority.ParentID != nil && *authority.ParentID != 0 {
allowed, err := r.PolicyPaths(ctx, actor.AuthorityID)
if err != nil {
return err
}
allowedSet := make(map[string]bool, len(allowed))
for _, item := range allowed {
allowedSet[item.Path+"\x00"+strings.ToUpper(item.Method)] = true
}
for _, item := range paths {
if !allowedSet[item.Path+"\x00"+strings.ToUpper(item.Method)] {
return errors.New("存在api不在权限列表中")
}
}
}
}
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("authority_id = ?", aid).Delete(&authorityAPIPO{}).Error; err != nil {
return err
+29 -12
View File
@@ -58,12 +58,6 @@ func (r *settingsRepo) CreateAPIToken(ctx context.Context, v *biz.APIToken) erro
return nil
}
func (r *settingsRepo) ListAPITokens(ctx context.Context, page, size int, userID uint, status *bool) ([]*biz.APIToken, int64, error) {
if page < 1 {
page = 1
}
if size < 1 {
size = 10
}
db := r.data.gormDB.WithContext(ctx).Model(&apiTokenPO{})
if userID != 0 {
db = db.Where("user_id = ?", userID)
@@ -76,16 +70,26 @@ func (r *settingsRepo) ListAPITokens(ctx context.Context, page, size int, userID
return nil, 0, err
}
var pos []apiTokenPO
if err := db.Order("created_at desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
if err := applyPagination(db.Order("created_at desc"), page, size, 100).Find(&pos).Error; err != nil {
return nil, 0, err
}
userIDs := make([]uint, 0, len(pos))
for _, po := range pos {
userIDs = append(userIDs, po.UserID)
}
var userPOs []userPO
if len(userIDs) > 0 {
if err := r.data.gormDB.WithContext(ctx).Where("id IN ?", userIDs).Find(&userPOs).Error; err != nil {
return nil, 0, err
}
}
users := make(map[uint]*biz.User, len(userPOs))
for i := range userPOs {
users[userPOs[i].ID] = baseBizUser(&userPOs[i])
}
out := make([]*biz.APIToken, 0, len(pos))
for _, po := range pos {
v := &biz.APIToken{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, UserID: po.UserID, AuthorityID: po.AuthorityID, Token: po.Token, Status: po.Status, ExpiresAt: po.ExpiresAt, Remark: po.Remark}
var userPO userPO
if r.data.gormDB.WithContext(ctx).First(&userPO, po.UserID).Error == nil {
v.User, _ = (&systemRepo{data: r.data}).toBizUser(ctx, &userPO)
}
v := &biz.APIToken{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, UserID: po.UserID, AuthorityID: po.AuthorityID, Token: po.Token, Status: po.Status, ExpiresAt: po.ExpiresAt, Remark: po.Remark, User: users[po.UserID]}
out = append(out, v)
}
return out, total, nil
@@ -97,6 +101,19 @@ func (r *settingsRepo) DisableAPIToken(ctx context.Context, id uint) (string, er
}
return po.Token, r.data.gormDB.WithContext(ctx).Model(&po).Update("status", false).Error
}
func (r *settingsRepo) DisableAndBlacklistAPIToken(ctx context.Context, id uint) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var po apiTokenPO
if err := tx.First(&po, id).Error; err != nil {
return err
}
// Blacklist the token before disabling it while making both writes atomic.
if err := tx.Create(&jwtBlacklistPO{JWT: po.Token}).Error; err != nil {
return err
}
return tx.Model(&po).Update("status", false).Error
})
}
func (r *settingsRepo) BlacklistToken(ctx context.Context, token string) error {
if token == "" {
return errors.New("token is empty")
+106 -19
View File
@@ -11,7 +11,67 @@ import (
"gorm.io/gorm"
)
func (r *accessRepo) strictAuthorityIDs(ctx context.Context, actorID uint) (map[uint]bool, error) {
allowed := make(map[uint]bool)
if actorID == 0 {
return allowed, nil
}
var actor authorityPO
if err := r.data.gormDB.WithContext(ctx).Where("authority_id = ?", actorID).First(&actor).Error; err != nil {
return nil, err
}
var authorities []authorityPO
if err := r.data.gormDB.WithContext(ctx).Find(&authorities).Error; err != nil {
return nil, err
}
children := make(map[uint][]uint)
for _, authority := range authorities {
if authority.ParentID != nil {
children[*authority.ParentID] = append(children[*authority.ParentID], authority.AuthorityID)
}
}
var walk func(uint)
walk = func(id uint) {
for _, child := range children[id] {
if !allowed[child] {
allowed[child] = true
walk(child)
}
}
}
if actor.ParentID == nil || *actor.ParentID == 0 {
allowed[actorID] = true
}
walk(actorID)
return allowed, nil
}
func (r *accessRepo) checkAuthorityIDAuth(ctx context.Context, targetID uint) error {
config := r.data.runtime.Admin()
if config == nil || config.System == nil || !config.System.UseStrictAuth {
return nil
}
actor, ok := biz.ActorFromContext(ctx)
if !ok {
return errors.New("您提交的角色ID不合法")
}
allowed, err := r.strictAuthorityIDs(ctx, actor.AuthorityID)
if err != nil {
return err
}
if !allowed[targetID] {
return errors.New("您提交的角色ID不合法")
}
return nil
}
func (r *accessRepo) CreateAuthority(ctx context.Context, value *biz.Authority) error {
config := r.data.runtime.Admin()
if config != nil && config.System != nil && config.System.UseStrictAuth && (value.ParentID == nil || *value.ParentID == 0) {
if actor, ok := biz.ActorFromContext(ctx); ok {
value.ParentID = &actor.AuthorityID
}
}
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var count int64
if err := tx.Model(&authorityPO{}).Where("authority_id = ?", value.AuthorityID).Count(&count).Error; err != nil {
@@ -46,6 +106,18 @@ func (r *accessRepo) CreateAuthority(ctx context.Context, value *biz.Authority)
}
func (r *accessRepo) CopyAuthority(ctx context.Context, sourceID uint, value *biz.Authority) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
config := r.data.runtime.Admin()
actor, hasActor := biz.ActorFromContext(ctx)
strict := hasActor && actor.AuthorityID != 888 && config != nil && config.System != nil && config.System.UseStrictAuth
if strict {
allowed, err := r.strictAuthorityIDs(ctx, actor.AuthorityID)
if err != nil {
return err
}
if value.ParentID == nil || !allowed[*value.ParentID] {
return errors.New("严格角色模式下只能在自己的角色树中复制角色")
}
}
var count int64
if err := tx.Model(&authorityPO{}).Where("authority_id = ?", value.AuthorityID).Count(&count).Error; err != nil {
return err
@@ -76,6 +148,15 @@ func (r *accessRepo) CopyAuthority(ctx context.Context, sourceID uint, value *bi
return err
}
for i := range apis {
if strict {
var allowedCount int64
if err := tx.Model(&authorityAPIPO{}).Where("authority_id = ? AND api_id = ?", actor.AuthorityID, apis[i].APIID).Count(&allowedCount).Error; err != nil {
return err
}
if allowedCount == 0 {
return errors.New("严格角色模式下不能复制当前角色未拥有的 API")
}
}
apis[i].AuthorityID = value.AuthorityID
}
if len(apis) > 0 {
@@ -95,16 +176,6 @@ func (r *accessRepo) CopyAuthority(ctx context.Context, sourceID uint, value *bi
return err
}
}
var departments []authorityDepartmentPO
if err := copyLinks("sys_authority_departments", &departments, map[string]any{"authority_id": sourceID}); err != nil {
return err
}
for i := range departments {
departments[i].AuthorityID = value.AuthorityID
}
if len(departments) > 0 {
return tx.Create(&departments).Error
}
return nil
})
}
@@ -155,8 +226,20 @@ func (r *accessRepo) ListAuthorities(ctx context.Context) ([]*biz.Authority, err
if err := r.data.gormDB.WithContext(ctx).Order("authority_id").Find(&pos).Error; err != nil {
return nil, err
}
var allowed map[uint]bool
config := r.data.runtime.Admin()
if actor, ok := biz.ActorFromContext(ctx); ok && config != nil && config.System != nil && config.System.UseStrictAuth {
var err error
allowed, err = r.strictAuthorityIDs(ctx, actor.AuthorityID)
if err != nil {
return nil, err
}
}
out := make([]*biz.Authority, 0, len(pos))
for _, po := range pos {
if allowed != nil && !allowed[po.AuthorityID] {
continue
}
v := toBizAuthority(po)
out = append(out, &v)
}
@@ -190,11 +273,13 @@ func (r *accessRepo) SetAuthorityUsers(ctx context.Context, id uint, ids []uint)
}
var user userPO
if err := tx.First(&user, uid).Error; err == nil && user.AuthorityID == id {
if err = tx.Model(&user).Update("authority_id", 888).Error; err != nil {
var another userAuthorityPO
if err = tx.Where("sys_user_id = ?", uid).First(&another).Error; errors.Is(err, gorm.ErrRecordNotFound) {
continue
} else if err != nil {
return err
}
fallback := userAuthorityPO{SysUserID: uid, SysAuthorityAuthorityID: 888}
if err = tx.FirstOrCreate(&fallback, fallback).Error; err != nil {
if err = tx.Model(&user).Update("authority_id", another.SysAuthorityAuthorityID).Error; err != nil {
return err
}
}
@@ -215,12 +300,14 @@ func (r *accessRepo) SetDataScope(ctx context.Context, id uint, scope int, deptI
if err := tx.Where("authority_id = ?", id).Delete(&authorityDepartmentPO{}).Error; err != nil {
return err
}
links := make([]authorityDepartmentPO, 0, len(deptIDs))
for _, did := range deptIDs {
links = append(links, authorityDepartmentPO{AuthorityID: id, DepartmentID: did})
}
if len(links) > 0 {
return tx.Create(&links).Error
if scope == 5 {
links := make([]authorityDepartmentPO, 0, len(deptIDs))
for _, did := range deptIDs {
links = append(links, authorityDepartmentPO{AuthorityID: id, DepartmentID: did})
}
if len(links) > 0 {
return tx.Create(&links).Error
}
}
return nil
})
+29 -3
View File
@@ -22,14 +22,14 @@ type cacheStore struct {
memory map[string]memoryCacheEntry
}
const maxMemoryCacheEntries = 10000
func NewCache(data *Data) biz.Cache {
return &cacheStore{data: data, memory: make(map[string]memoryCacheEntry)}
}
func (s *cacheStore) client() *redis.Client {
s.data.mu.RLock()
defer s.data.mu.RUnlock()
return s.data.redis
return s.data.redis.load()
}
func (s *cacheStore) Get(ctx context.Context, key string) (string, bool, error) {
@@ -62,6 +62,7 @@ func (s *cacheStore) Set(ctx context.Context, key, value string, expiration time
}
s.mu.Lock()
defer s.mu.Unlock()
s.makeMemoryRoom(key)
entry := memoryCacheEntry{value: value}
if expiration > 0 {
entry.expiresAt = time.Now().Add(expiration)
@@ -95,6 +96,7 @@ func (s *cacheStore) Increment(ctx context.Context, key string, expiration time.
}
s.mu.Lock()
defer s.mu.Unlock()
s.makeMemoryRoom(key)
entry, ok := s.memory[key]
if ok && !entry.expiresAt.IsZero() && time.Now().After(entry.expiresAt) {
ok = false
@@ -111,3 +113,27 @@ func (s *cacheStore) Increment(ctx context.Context, key string, expiration time.
s.memory[key] = entry
return value, nil
}
func (s *cacheStore) makeMemoryRoom(incoming string) {
if len(s.memory) < maxMemoryCacheEntries {
return
}
now := time.Now()
for key, entry := range s.memory {
if !entry.expiresAt.IsZero() && now.After(entry.expiresAt) {
delete(s.memory, key)
}
}
if len(s.memory) < maxMemoryCacheEntries {
return
}
if _, exists := s.memory[incoming]; exists {
return
}
// The in-memory store is only a Redis fallback. A bounded arbitrary eviction
// is preferable to unbounded growth under IP churn.
for key := range s.memory {
delete(s.memory, key)
break
}
}
+54 -41
View File
@@ -53,14 +53,22 @@ func setYAMLMapping(node *yaml.Node, key string, value any) error {
}
func (d *Data) persistConfig() error {
return d.persistConfigValues(d.config, d.admin)
dataConfig, adminConfig := d.runtime.Values()
return d.persistConfigValues(dataConfig, adminConfig)
}
func (d *Data) persistConfigValues(dataConfig *conf.Data, adminConfig *conf.AdminBackend) error {
if d.admin == nil || d.admin.ConfigPath == "" {
d.configMu.Lock()
defer d.configMu.Unlock()
return d.persistConfigValuesLocked(dataConfig, adminConfig)
}
func (d *Data) persistConfigValuesLocked(dataConfig *conf.Data, adminConfig *conf.AdminBackend) error {
if adminConfig == nil || adminConfig.ConfigPath == "" {
return nil
}
raw, err := os.ReadFile(d.admin.ConfigPath)
configPath := adminConfig.ConfigPath
raw, err := os.ReadFile(configPath)
if err != nil {
return err
}
@@ -86,10 +94,10 @@ func (d *Data) persistConfigValues(dataConfig *conf.Data, adminConfig *conf.Admi
if err != nil {
return err
}
if err = os.MkdirAll(filepath.Dir(d.admin.ConfigPath), 0o755); err != nil {
if err = os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil {
return err
}
temporary, err := os.CreateTemp(filepath.Dir(d.admin.ConfigPath), ".kra-config-*.yaml")
temporary, err := os.CreateTemp(filepath.Dir(configPath), ".kra-config-*.yaml")
if err != nil {
return err
}
@@ -106,33 +114,24 @@ func (d *Data) persistConfigValues(dataConfig *conf.Data, adminConfig *conf.Admi
if err = temporary.Close(); err != nil {
return err
}
return os.Rename(tempName, d.admin.ConfigPath)
return os.Rename(tempName, configPath)
}
func (d *Data) reloadConfig(ctx context.Context) error {
if d.admin == nil || d.admin.ConfigPath == "" {
d.configMu.Lock()
defer d.configMu.Unlock()
configPath := d.runtime.ConfigPath()
if configPath == "" {
return fmt.Errorf("configuration path is not set")
}
raw, err := os.ReadFile(d.admin.ConfigPath)
next, err := readBootstrap(configPath)
if err != nil {
return err
}
var plain map[string]any
if err = yaml.Unmarshal(raw, &plain); err != nil {
return err
}
encoded, err := json.Marshal(plain)
if err != nil {
return err
}
var next conf.Bootstrap
if err = (protojson.UnmarshalOptions{DiscardUnknown: true}).Unmarshal(encoded, &next); err != nil {
return err
}
if next.Data == nil || next.Data.Database == nil || next.Admin == nil {
return fmt.Errorf("data.database and admin configuration are required")
}
next.Admin.ConfigPath = d.admin.ConfigPath
next.Admin.ConfigPath = configPath
candidateDB, err := openDatabase(next.Data.Database, false, "")
if err != nil {
return fmt.Errorf("reload database: %w", err)
@@ -150,35 +149,49 @@ func (d *Data) reloadConfig(ctx context.Context) error {
} else if err = sqlDB.PingContext(ctx); err != nil {
return fmt.Errorf("reload database: %w", err)
}
if err = migrateAll(candidateDB.WithContext(ctx)); err != nil {
return fmt.Errorf("reload database migrations: %w", err)
if next.Admin.System == nil || !next.Admin.System.DisableAutoMigrate {
if err = migrateAll(candidateDB.WithContext(ctx)); err != nil {
return fmt.Errorf("reload database migrations: %w", err)
}
}
candidateStorage, err := buildFileStorage(next.Admin)
if err != nil {
return fmt.Errorf("reload storage: %w", err)
}
candidateRedis := openRedis(next.Data.Redis)
useRedis := next.Admin.System != nil && next.Admin.System.UseRedis
candidateRedis := openRedis(next.Data.Redis, useRedis)
candidateDBList, err := openDatabaseList(next.Data.DatabaseList)
if err != nil {
return err
}
d.mu.Lock()
oldDB, oldRedis := d.gormDB, d.redis
d.gormDB, d.redis = candidateDB, candidateRedis
proto.Reset(d.config)
proto.Merge(d.config, next.Data)
proto.Reset(d.admin)
proto.Merge(d.admin, next.Admin)
d.database = d.config.Database
d.mu.Unlock()
d.gormDB.replace(candidateDB)
d.replaceDatabaseList(candidateDBList)
d.redis.replace(candidateRedis)
d.runtime.Replace(next.Data, next.Admin)
if d.storage != nil {
d.storage.replace(candidateStorage)
}
closeCandidate = false
if oldDB != nil {
if sqlDB, closeErr := oldDB.DB(); closeErr == nil {
_ = sqlDB.Close()
}
}
if oldRedis != nil {
_ = oldRedis.Close()
}
return nil
}
func readBootstrap(configPath string) (*conf.Bootstrap, error) {
raw, err := os.ReadFile(configPath)
if err != nil {
return nil, err
}
var plain map[string]any
if err = yaml.Unmarshal(raw, &plain); err != nil {
return nil, err
}
encoded, err := json.Marshal(plain)
if err != nil {
return nil, err
}
var next conf.Bootstrap
if err = (protojson.UnmarshalOptions{DiscardUnknown: true}).Unmarshal(encoded, &next); err != nil {
return nil, err
}
return &next, nil
}
+80
View File
@@ -0,0 +1,80 @@
package data
import (
"log"
"path/filepath"
"sync"
"time"
"github.com/fsnotify/fsnotify"
)
// watchConfig refreshes the in-memory config snapshot on file changes, while
// /system/reloadSystem remains responsible
// for rebuilding database, Redis, storage, and scheduled tasks.
func (d *Data) watchConfig() func() {
configPath := d.runtime.ConfigPath()
if configPath == "" {
return func() {}
}
absolute, err := filepath.Abs(configPath)
if err != nil {
log.Printf("resolve config watch path: %v", err)
return func() {}
}
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Printf("create config watcher: %v", err)
return func() {}
}
if err = watcher.Add(filepath.Dir(absolute)); err != nil {
log.Printf("watch config directory: %v", err)
_ = watcher.Close()
return func() {}
}
done := make(chan struct{})
var once sync.Once
go func() {
var timer *time.Timer
for {
select {
case event, ok := <-watcher.Events:
if !ok || filepath.Clean(event.Name) != absolute || event.Op&(fsnotify.Write|fsnotify.Create|fsnotify.Rename) == 0 {
continue
}
if timer != nil {
timer.Stop()
}
timer = time.AfterFunc(100*time.Millisecond, func() {
next, loadErr := readBootstrap(absolute)
if loadErr != nil {
log.Printf("reload changed config: %v", loadErr)
return
}
if next.Data == nil || next.Admin == nil {
log.Printf("reload changed config: data and admin configuration are required")
return
}
next.Admin.ConfigPath = absolute
d.runtime.Replace(next.Data, next.Admin)
log.Printf("config file changed: %s", absolute)
})
case watchErr, ok := <-watcher.Errors:
if ok {
log.Printf("config watcher error: %v", watchErr)
}
case <-done:
if timer != nil {
timer.Stop()
}
return
}
}
}()
return func() {
once.Do(func() {
close(done)
_ = watcher.Close()
})
}
}
+78 -35
View File
@@ -16,21 +16,71 @@ import (
var ProviderSet = wire.NewSet(NewData, NewSystemRepo, NewAccessRepo, NewMenuRepo, NewOrganizationRepo, NewSettingsRepo, NewVersionRepo, NewExportRepo, NewAuditRepo, NewTaskRepo, NewMediaRepo, NewAnnouncementRepo, NewEmailRepo, NewCache, NewFileStorage)
type Data struct {
mu sync.RWMutex
initMu sync.Mutex
gormDB *gorm.DB
redis *redis.Client
database *conf.Data_Database
config *conf.Data
admin *conf.AdminBackend
configMu sync.Mutex
gormDB *reloadableDB
redis *reloadableRedis
runtime *conf.Runtime
storage *reloadableStorage
dbListMu sync.RWMutex
dbList map[string]*gorm.DB
}
func NewData(c *conf.Data, admin *conf.AdminBackend) (*Data, func(), error) {
func openDatabaseList(configs []*conf.Data_Database) (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, "")
if err != nil {
for _, opened := range items {
if sqlDB, dbErr := opened.DB(); dbErr == nil {
_ = sqlDB.Close()
}
}
return nil, fmt.Errorf("open database %q: %w", config.AliasName, err)
}
items[config.AliasName] = db
}
return items, nil
}
func closeDatabaseList(items map[string]*gorm.DB) {
for _, db := range items {
if sqlDB, err := db.DB(); err == nil {
_ = sqlDB.Close()
}
}
}
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
}
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 *conf.Runtime) (*Data, func(), error) {
c := runtime.Data()
if c == nil || c.Database == nil {
return nil, nil, fmt.Errorf("database configuration is required")
}
d := &Data{database: c.Database, config: c, admin: admin}
d := &Data{runtime: runtime}
db, err := openDatabase(c.Database, false, "")
usingFallback := false
if err != nil {
@@ -43,31 +93,33 @@ func NewData(c *conf.Data, admin *conf.AdminBackend) (*Data, func(), error) {
}
usingFallback = true
}
d.gormDB = db
if !usingFallback {
d.gormDB = newReloadableDB(db)
d.dbList, err = openDatabaseList(c.DatabaseList)
if err != nil {
d.gormDB.close()
return nil, nil, err
}
admin := runtime.Admin()
disableAutoMigrate := admin != nil && admin.System != nil && admin.System.DisableAutoMigrate
if !usingFallback && !disableAutoMigrate {
if err = migrateAll(db); err != nil {
return nil, nil, fmt.Errorf("migrate tables: %w", err)
}
}
if candidate := openRedis(c.Redis); candidate != nil {
d.redis = candidate
}
useRedis := admin != nil && admin.System != nil && admin.System.UseRedis
d.redis = newReloadableRedis(openRedis(c.Redis, useRedis))
stopConfigWatcher := d.watchConfig()
cleanup := func() {
d.mu.RLock()
db := d.gormDB
d.mu.RUnlock()
if sqlDB, closeErr := db.DB(); closeErr == nil {
_ = sqlDB.Close()
}
if d.redis != nil {
_ = d.redis.Close()
}
stopConfigWatcher()
d.gormDB.close()
closeDatabaseList(d.dbList)
d.redis.close()
}
return d, cleanup, nil
}
func openRedis(config *conf.Data_Redis) *redis.Client {
if config == nil || config.Addr == "" {
func openRedis(config *conf.Data_Redis, enabled bool) *redis.Client {
if !enabled || config == nil || config.Addr == "" {
return nil
}
options := &redis.Options{Addr: config.Addr, Network: config.Network}
@@ -89,15 +141,6 @@ func openRedis(config *conf.Data_Redis) *redis.Client {
}
func (d *Data) activateDatabase(db *gorm.DB, config *conf.Data_Database) {
d.mu.Lock()
old := d.gormDB
d.gormDB = db
d.database = config
d.config.Database = config
d.mu.Unlock()
if old != nil {
if sqlDB, e := old.DB(); e == nil {
_ = sqlDB.Close()
}
}
d.gormDB.replace(db)
d.runtime.UpdateDatabase(config)
}
+1 -7
View File
@@ -29,12 +29,6 @@ func dataAccessFromPO(v dataAccessLogPO) *biz.DataAccessLog {
return &biz.DataAccessLog{ID: v.ID, CreatedAt: v.CreatedAt, EventType: v.EventType, TargetTable: v.TargetTable, Operation: v.Operation, UserID: v.UserID, AuthorityID: v.AuthorityID, Scope: v.Scope, RequestID: v.RequestID, Method: v.Method, Path: v.Path, Detail: v.Detail}
}
func (r *auditRepo) ListDataAccess(ctx context.Context, page, size int, q *biz.DataAccessLog) ([]*biz.DataAccessLog, int64, error) {
if page < 1 {
page = 1
}
if size < 1 {
size = 10
}
db := r.data.gormDB.WithContext(ctx).Model(&dataAccessLogPO{})
if q != nil {
if q.EventType != "" {
@@ -52,7 +46,7 @@ func (r *auditRepo) ListDataAccess(ctx context.Context, page, size int, q *biz.D
return nil, 0, err
}
var pos []dataAccessLogPO
if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
if err := applyPagination(db.Order("id desc"), page, size, 100).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.DataAccessLog, 0, len(pos))
+39 -2
View File
@@ -44,6 +44,30 @@ func (authorityDepartmentPO) TableName() string { return "sys_authority_departme
func deptFromPO(po departmentPO) *biz.Department {
return &biz.Department{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Name: po.Name, ParentID: po.ParentID, Ancestors: po.Ancestors, Sort: po.Sort, LeaderID: po.LeaderID, Status: po.Status}
}
func (r *organizationRepo) attachDepartmentLeaders(ctx context.Context, departments []*biz.Department) error {
leaderIDs := make([]uint, 0, len(departments))
for _, department := range departments {
if department.LeaderID != 0 {
leaderIDs = append(leaderIDs, department.LeaderID)
}
}
if len(leaderIDs) == 0 {
return nil
}
var leaders []userPO
if err := r.data.gormDB.WithContext(ctx).Where("id IN ?", leaderIDs).Find(&leaders).Error; err != nil {
return err
}
leaderByID := make(map[uint]*biz.User, len(leaders))
for i := range leaders {
leaderByID[leaders[i].ID] = baseBizUser(&leaders[i])
}
for _, department := range departments {
department.Leader = leaderByID[department.LeaderID]
}
return nil
}
func (r *organizationRepo) CreateDepartment(ctx context.Context, v *biz.Department) error {
v.Ancestors = "0"
if v.ParentID != 0 {
@@ -99,7 +123,11 @@ func (r *organizationRepo) FindDepartment(ctx context.Context, id uint) (*biz.De
if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err
}
return deptFromPO(po), nil
department := deptFromPO(po)
if err := r.attachDepartmentLeaders(ctx, []*biz.Department{department}); err != nil {
return nil, err
}
return department, nil
}
func (r *organizationRepo) ListDepartments(ctx context.Context, name string) ([]*biz.Department, error) {
var pos []departmentPO
@@ -111,8 +139,17 @@ func (r *organizationRepo) ListDepartments(ctx context.Context, name string) ([]
return nil, err
}
nodes := map[uint]*biz.Department{}
items := make([]*biz.Department, 0, len(pos))
for _, po := range pos {
nodes[po.ID] = deptFromPO(po)
item := deptFromPO(po)
nodes[po.ID] = item
items = append(items, item)
}
if err := r.attachDepartmentLeaders(ctx, items); err != nil {
return nil, err
}
if name != "" {
return items, nil
}
var roots []*biz.Department
for _, po := range pos {
+127 -35
View File
@@ -62,6 +62,47 @@ func (r *settingsRepo) CreateDictionary(ctx context.Context, v *biz.Dictionary)
v.ID = po.ID
return nil
}
func (r *settingsRepo) ImportDictionary(ctx context.Context, dictionary *biz.Dictionary, details []*biz.DictionaryDetail) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
po := dictionaryPO{Name: dictionary.Name, Type: dictionary.Type, Status: dictionary.Status, Desc: dictionary.Desc, ParentID: dictionary.ParentID}
if err := tx.Create(&po).Error; err != nil {
return err
}
dictionary.ID = po.ID
idMap := make(map[uint]uint, len(details))
created := make(map[uint]uint, len(details))
for index, item := range details {
// Incomplete detail rows are ignored to preserve import behavior.
if item.Label == "" || item.Value == "" {
continue
}
oldID := item.ID
detail := dictionaryDetailPO{Label: item.Label, Value: item.Value, Extend: item.Extend, Status: item.Status, Sort: item.Sort, DictionaryID: po.ID, Level: item.Level, Path: item.Path}
if err := tx.Create(&detail).Error; err != nil {
return err
}
created[uint(index)] = detail.ID
if oldID > 0 {
idMap[oldID] = detail.ID
}
}
for index, item := range details {
if item.ID == 0 || item.ParentID == nil || *item.ParentID == 0 {
continue
}
newID, exists := created[uint(index)]
newParentID, parentExists := idMap[*item.ParentID]
if !exists || !parentExists {
continue
}
if err := tx.Model(&dictionaryDetailPO{}).Where("id = ?", newID).Update("parent_id", newParentID).Error; err != nil {
return err
}
}
return nil
})
}
func (r *settingsRepo) UpdateDictionary(ctx context.Context, v *biz.Dictionary) error {
return r.data.gormDB.WithContext(ctx).Model(&dictionaryPO{}).Where("id = ?", v.ID).Updates(map[string]any{"name": v.Name, "type": v.Type, "status": v.Status, "desc": v.Desc, "parent_id": v.ParentID}).Error
}
@@ -80,51 +121,98 @@ func (r *settingsRepo) DeleteDictionary(ctx context.Context, id uint) error {
return tx.Delete(&dictionaryPO{}, id).Error
})
}
func (r *settingsRepo) FindDictionary(ctx context.Context, id uint, typ string, details bool) (*biz.Dictionary, error) {
func (r *settingsRepo) FindDictionary(ctx context.Context, id uint, typ string, status *bool, details bool) (*biz.Dictionary, error) {
var po dictionaryPO
db := r.data.gormDB.WithContext(ctx)
var err error
if id != 0 {
err = db.First(&po, id).Error
} else {
err = db.Where("type = ?", typ).First(&po).Error
active := true
if status != nil {
active = *status
}
err := db.Where("(type = ? OR id = ?) AND status = ?", typ, id, active).First(&po).Error
if err != nil {
return nil, err
}
out := dictionaryFromPO(po)
if details {
out.Details, _, err = r.ListDictionaryDetails(ctx, 1, 10000, out.ID, "", "")
out.Details, _, err = r.ListDictionaryDetails(ctx, 0, 0, biz.DictionaryDetailFilter{DictionaryID: out.ID})
if err == nil {
filtered := out.Details[:0]
for _, item := range out.Details {
if item.Status {
filtered = append(filtered, item)
}
}
out.Details = filtered
}
}
return out, err
}
func (r *settingsRepo) ExportDictionary(ctx context.Context, id uint) (*biz.Dictionary, error) {
var po dictionaryPO
if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err
}
value := dictionaryFromPO(po)
details, _, err := r.ListDictionaryDetails(ctx, 0, 0, biz.DictionaryDetailFilter{DictionaryID: id})
if err != nil {
return nil, err
}
value.Details = details
return value, nil
}
func (r *settingsRepo) ListDictionaries(ctx context.Context, page, size int, name, typ string, details bool) ([]*biz.Dictionary, int64, error) {
if page < 1 {
page = 1
}
if size < 1 {
size = 10
}
db := r.data.gormDB.WithContext(ctx).Model(&dictionaryPO{})
if name != "" {
db = db.Where("name LIKE ?", "%"+name+"%")
}
if typ != "" {
db = db.Where("type LIKE ?", "%"+typ+"%")
like := "%" + name + "%"
db = db.Where("name LIKE ? OR type LIKE ?", like, like)
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
var pos []dictionaryPO
if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
if err := applyPagination(db.Order("id"), page, size, 0).Find(&pos).Error; err != nil {
return nil, 0, err
}
parentIDs := make([]uint, 0, len(pos))
dictionaryIDs := make([]uint, 0, len(pos))
for _, po := range pos {
parentIDs = append(parentIDs, po.ID)
dictionaryIDs = append(dictionaryIDs, po.ID)
}
childrenByParent := make(map[uint][]dictionaryPO)
if len(parentIDs) > 0 {
var children []dictionaryPO
if err := r.data.gormDB.WithContext(ctx).Where("parent_id IN ?", parentIDs).Order("id").Find(&children).Error; err != nil {
return nil, 0, err
}
for _, child := range children {
if child.ParentID != nil {
childrenByParent[*child.ParentID] = append(childrenByParent[*child.ParentID], child)
}
}
}
detailsByDictionary := make(map[uint][]dictionaryDetailPO)
if details && len(dictionaryIDs) > 0 {
var values []dictionaryDetailPO
if err := r.data.gormDB.WithContext(ctx).Where("sys_dictionary_id IN ?", dictionaryIDs).Order("sort").Find(&values).Error; err != nil {
return nil, 0, err
}
for _, value := range values {
detailsByDictionary[value.DictionaryID] = append(detailsByDictionary[value.DictionaryID], value)
}
}
out := make([]*biz.Dictionary, 0, len(pos))
for _, po := range pos {
v := dictionaryFromPO(po)
for _, child := range childrenByParent[po.ID] {
v.Children = append(v.Children, dictionaryFromPO(child))
}
if details {
v.Details, _, _ = r.ListDictionaryDetails(ctx, 1, 10000, v.ID, "", "")
for _, detail := range detailsByDictionary[v.ID] {
v.Details = append(v.Details, detailFromPO(detail))
}
}
out = append(out, v)
}
@@ -197,29 +285,32 @@ func (r *settingsRepo) FindDictionaryDetail(ctx context.Context, id uint) (*biz.
}
return detailFromPO(po), nil
}
func (r *settingsRepo) ListDictionaryDetails(ctx context.Context, page, size int, dictionaryID uint, label, value string) ([]*biz.DictionaryDetail, int64, error) {
if page < 1 {
page = 1
}
if size < 1 {
size = 10
}
func (r *settingsRepo) ListDictionaryDetails(ctx context.Context, page, size int, filter biz.DictionaryDetailFilter) ([]*biz.DictionaryDetail, int64, error) {
db := r.data.gormDB.WithContext(ctx).Model(&dictionaryDetailPO{})
if dictionaryID != 0 {
db = db.Where("sys_dictionary_id = ?", dictionaryID)
if filter.DictionaryID != 0 {
db = db.Where("sys_dictionary_id = ?", filter.DictionaryID)
}
if label != "" {
db = db.Where("label LIKE ?", "%"+label+"%")
if filter.Label != "" {
db = db.Where("label LIKE ?", "%"+filter.Label+"%")
}
if value != "" {
db = db.Where("value LIKE ?", "%"+value+"%")
if filter.Value != "" {
db = db.Where("value = ?", filter.Value)
}
if filter.Status != nil {
db = db.Where("status = ?", *filter.Status)
}
if filter.ParentID != nil {
db = db.Where("parent_id = ?", *filter.ParentID)
}
if filter.Level != nil {
db = db.Where("level = ?", *filter.Level)
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
var pos []dictionaryDetailPO
if err := db.Order("sort,id").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
if err := applyPagination(db.Order("sort,id"), page, size, 100).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.DictionaryDetail, 0, len(pos))
@@ -230,13 +321,14 @@ func (r *settingsRepo) ListDictionaryDetails(ctx context.Context, page, size int
}
func (r *settingsRepo) DictionaryDetailTree(ctx context.Context, dictionaryID uint, typ string) ([]*biz.DictionaryDetail, error) {
if dictionaryID == 0 {
dictionary, err := r.FindDictionary(ctx, 0, typ, false)
active := true
dictionary, err := r.FindDictionary(ctx, 0, typ, &active, false)
if err != nil {
return nil, err
}
dictionaryID = dictionary.ID
}
items, _, err := r.ListDictionaryDetails(ctx, 1, 100000, dictionaryID, "", "")
items, _, err := r.ListDictionaryDetails(ctx, 0, 0, biz.DictionaryDetailFilter{DictionaryID: dictionaryID})
if err != nil {
return nil, err
}
+9 -8
View File
@@ -17,21 +17,22 @@ import (
)
type emailRepo struct {
config *conf.AdminBackend
runtime *conf.Runtime
}
func NewEmailRepo(config *conf.AdminBackend) biz.EmailRepo {
if config == nil {
return &emailRepo{}
}
return &emailRepo{config: config}
func NewEmailRepo(runtime *conf.Runtime) biz.EmailRepo {
return &emailRepo{runtime: runtime}
}
func (r *emailRepo) email() *conf.AdminBackend_Email {
if r.config == nil {
if r.runtime == nil {
return nil
}
return r.config.Email
config := r.runtime.Admin()
if config == nil {
return nil
}
return config.Email
}
func (r *emailRepo) Enabled() bool {
+7 -13
View File
@@ -48,22 +48,16 @@ func (r *auditRepo) FindError(ctx context.Context, id uint) (*biz.ErrorRecord, e
return errorFromPO(po), nil
}
func (r *auditRepo) ListErrors(ctx context.Context, page, size int, q *biz.ErrorRecord) ([]*biz.ErrorRecord, int64, error) {
if page < 1 {
page = 1
}
if size < 1 {
size = 10
}
db := r.data.gormDB.WithContext(ctx).Model(&errorRecordPO{})
if q != nil {
if len(q.CreatedAtRange) == 2 {
db = db.Where("created_at BETWEEN ? AND ?", q.CreatedAtRange[0], q.CreatedAtRange[1])
}
if q.Form != "" {
db = db.Where("form LIKE ?", "%"+q.Form+"%")
db = db.Where("form = ?", q.Form)
}
if q.Level != "" {
db = db.Where("level = ?", q.Level)
}
if q.Status != "" {
db = db.Where("status = ?", q.Status)
if q.Info != "" {
db = db.Where("info LIKE ?", "%"+q.Info+"%")
}
}
var total int64
@@ -71,7 +65,7 @@ func (r *auditRepo) ListErrors(ctx context.Context, page, size int, q *biz.Error
return nil, 0, err
}
var pos []errorRecordPO
if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
if err := applyPagination(db.Order("created_at desc"), page, size, 100).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.ErrorRecord, 0, len(pos))
+212 -68
View File
@@ -2,12 +2,11 @@ package data
import (
"context"
"encoding/json"
"errors"
"fmt"
"gorm.io/gorm"
"kra/internal/biz"
"regexp"
"sort"
"strconv"
"strings"
"time"
@@ -151,14 +150,11 @@ func (r *exportRepo) FindExportTemplate(ctx context.Context, id uint, tid string
return exportFromPO(po, conditions, joins), nil
}
func (r *exportRepo) ListExportTemplates(ctx context.Context, page, size int, q *biz.ExportTemplate) ([]*biz.ExportTemplate, int64, error) {
if page < 1 {
page = 1
}
if size < 1 {
size = 10
}
db := r.data.gormDB.WithContext(ctx).Model(&exportTemplatePO{})
if q != nil {
if q.StartCreatedAt != nil && q.EndCreatedAt != nil {
db = db.Where("created_at BETWEEN ? AND ?", q.StartCreatedAt, q.EndCreatedAt)
}
if q.Name != "" {
db = db.Where("name LIKE ?", "%"+q.Name+"%")
}
@@ -174,37 +170,46 @@ func (r *exportRepo) ListExportTemplates(ctx context.Context, page, size int, q
return nil, 0, err
}
var pos []exportTemplatePO
if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
if err := applyPagination(db, page, size, 0).Find(&pos).Error; err != nil {
return nil, 0, err
}
templateIDs := make([]string, 0, len(pos))
for _, po := range pos {
templateIDs = append(templateIDs, po.TemplateID)
}
var conditions []exportConditionPO
var joins []exportJoinPO
if len(templateIDs) > 0 {
if err := r.data.gormDB.WithContext(ctx).Where("template_id IN ?", templateIDs).Find(&conditions).Error; err != nil {
return nil, 0, err
}
if err := r.data.gormDB.WithContext(ctx).Where("template_id IN ?", templateIDs).Find(&joins).Error; err != nil {
return nil, 0, err
}
}
conditionsByTemplate := make(map[string][]exportConditionPO, len(pos))
for _, condition := range conditions {
conditionsByTemplate[condition.TemplateID] = append(conditionsByTemplate[condition.TemplateID], condition)
}
joinsByTemplate := make(map[string][]exportJoinPO, len(pos))
for _, join := range joins {
joinsByTemplate[join.TemplateID] = append(joinsByTemplate[join.TemplateID], join)
}
out := make([]*biz.ExportTemplate, 0, len(pos))
for _, po := range pos {
v, err := r.FindExportTemplate(ctx, po.ID, "")
if err != nil {
return nil, 0, err
}
out = append(out, v)
out = append(out, exportFromPO(po, conditionsByTemplate[po.TemplateID], joinsByTemplate[po.TemplateID]))
}
return out, total, nil
}
var safeIdentifier = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_.]*$`)
var allowedOperator = map[string]bool{"=": true, "!=": true, ">": true, ">=": true, "<": true, "<=": true, "LIKE": true, "IN": true, "NOT IN": true, "BETWEEN": true}
func validateSelectSQL(sql string) error {
normalized := strings.ToLower(strings.TrimSpace(sql))
if !strings.HasPrefix(normalized, "select ") || strings.Contains(normalized, ";") {
return errors.New("仅允许单条 SELECT 查询")
}
return nil
}
func (r *exportRepo) QueryExport(ctx context.Context, t *biz.ExportTemplate, params map[string]string) ([]map[string]any, string, error) {
db := r.data.gormDB.WithContext(ctx)
selected, err := r.data.database(t.DBName)
if err != nil {
return nil, "", err
}
db := selected.WithContext(ctx)
var rows []map[string]any
if t.SQL != "" {
if err := validateSelectSQL(t.SQL); err != nil {
return nil, "", err
}
args := map[string]any{}
for k, v := range params {
args[k] = v
@@ -212,52 +217,54 @@ func (r *exportRepo) QueryExport(ctx context.Context, t *biz.ExportTemplate, par
err := db.Raw(t.SQL, args).Scan(&rows).Error
return rows, t.SQL, err
}
if !safeIdentifier.MatchString(t.TableName) {
return nil, "", errors.New("表名不合法")
}
columns := make([]string, 0)
for key := range parseTemplateColumns(t.TemplateInfo) {
if !safeIdentifier.MatchString(key) {
return nil, "", errors.New("列名不合法")
}
columns = append(columns, key)
if err := json.Unmarshal([]byte(t.TemplateInfo), &map[string]string{}); err != nil {
return nil, "", err
}
decoder := json.NewDecoder(strings.NewReader(t.TemplateInfo))
_, _ = decoder.Token()
for decoder.More() {
key, _ := decoder.Token()
columns = append(columns, key.(string))
var ignored any
_ = decoder.Decode(&ignored)
}
sort.Strings(columns)
if len(columns) == 0 {
return nil, "", errors.New("模板列为空")
}
query := db.Table(t.TableName).Select(strings.Join(columns, ","))
for _, j := range t.Joins {
joinType := strings.ToUpper(strings.TrimSpace(j.Join))
if joinType != "LEFT JOIN" && joinType != "RIGHT JOIN" && joinType != "INNER JOIN" && joinType != "JOIN" {
return nil, "", errors.New("关联类型不合法")
query = query.Joins(j.Join + " " + j.Table + " ON " + j.On)
}
if params["filterDeleted"] == "true" {
query = query.Where(fmt.Sprintf("%s.deleted_at IS NULL", t.TableName))
for _, join := range t.Joins {
if db.Migrator().HasColumn(join.Table, "deleted_at") {
query = query.Where(fmt.Sprintf("%s.deleted_at IS NULL", join.Table))
}
}
if !safeIdentifier.MatchString(j.Table) {
return nil, "", errors.New("关联表不合法")
}
query = query.Joins(joinType + " " + j.Table + " ON " + j.On)
}
for _, condition := range t.Conditions {
operator := strings.ToUpper(strings.TrimSpace(condition.Operator))
if !allowedOperator[operator] || !safeIdentifier.MatchString(condition.Column) {
return nil, "", errors.New("查询条件不合法")
}
operator := condition.Operator
value := params[condition.From]
if value == "" {
continue
}
switch operator {
case "LIKE":
query = query.Where(condition.Column+" LIKE ?", "%"+value+"%")
if value != "" {
query = query.Where(condition.Column+" LIKE ?", "%"+value+"%")
}
case "IN", "NOT IN":
query = query.Where(condition.Column+" "+operator+" ?", strings.Split(value, ","))
if value != "" {
query = query.Where(condition.Column+" "+operator+" (?)", value)
}
case "BETWEEN":
start, end := params["start"+condition.From], params["end"+condition.From]
if start != "" && end != "" {
query = query.Where(condition.Column+" BETWEEN ? AND ?", start, end)
}
default:
query = query.Where(condition.Column+" "+operator+" ?", value)
if value != "" {
query = query.Where(condition.Column+" "+operator+" ?", value)
}
}
}
limit := 0
@@ -269,17 +276,141 @@ func (r *exportRepo) QueryExport(ctx context.Context, t *biz.ExportTemplate, par
if limit > 0 {
query = query.Limit(limit)
}
if t.Order != "" {
parts := strings.Fields(t.Order)
if len(parts) < 1 || len(parts) > 2 || !safeIdentifier.MatchString(parts[0]) || (len(parts) == 2 && strings.ToUpper(parts[1]) != "ASC" && strings.ToUpper(parts[1]) != "DESC") {
return nil, "", errors.New("排序不合法")
if raw := params["offset"]; raw != "" {
if offset, parseErr := strconv.Atoi(raw); parseErr == nil {
query = query.Offset(offset)
}
query = query.Order(t.Order)
}
order := params["order"]
if order == "" {
order = t.Order
}
if order != "" {
parts := strings.Split(order, " ")
fields := map[string]bool{}
columnTypes, columnErr := db.Migrator().ColumnTypes(t.TableName)
if columnErr != nil {
return nil, "", columnErr
}
for _, column := range columnTypes {
fields[column.Name()] = true
}
if !fields[parts[0]] {
return nil, "", fmt.Errorf("order by %s is not in the fields", order)
}
orderSQL := parts[0]
if len(parts) > 1 {
if parts[1] != "asc" && parts[1] != "desc" {
return nil, "", fmt.Errorf("order by %s is not secure", order)
}
orderSQL += " " + parts[1]
}
query = query.Order(orderSQL)
}
preview := query.ToSQL(func(tx *gorm.DB) *gorm.DB { return tx.Find(&[]map[string]any{}) })
err := query.Find(&rows).Error
err = query.Find(&rows).Error
return rows, preview, err
}
func (r *exportRepo) PreviewExport(ctx context.Context, t *biz.ExportTemplate, params map[string]string) (string, error) {
columns := make([]string, 0)
decoder := json.NewDecoder(strings.NewReader(t.TemplateInfo))
if token, err := decoder.Token(); err != nil || token != json.Delim('{') {
return "", err
}
for decoder.More() {
key, err := decoder.Token()
if err != nil {
return "", err
}
columns = append(columns, key.(string))
var ignored any
if err = decoder.Decode(&ignored); err != nil {
return "", err
}
}
var builder strings.Builder
builder.WriteString("SELECT ")
builder.WriteString(strings.Join(columns, ", "))
builder.WriteString(" FROM ")
builder.WriteString(t.TableName)
for _, join := range t.Joins {
builder.WriteString(" " + join.Join + " " + join.Table + " ON " + join.On)
}
wheres := make([]string, 0)
if params["filterDeleted"] == "true" {
wheres = append(wheres, fmt.Sprintf("%s.deleted_at IS NULL", t.TableName))
selected, err := r.data.database(t.DBName)
if err != nil {
return "", err
}
db := selected.WithContext(ctx)
for _, join := range t.Joins {
if db.Migrator().HasColumn(join.Table, "deleted_at") {
wheres = append(wheres, fmt.Sprintf("%s.deleted_at IS NULL", join.Table))
}
}
}
for _, condition := range t.Conditions {
op, column, value := strings.ToUpper(strings.TrimSpace(condition.Operator)), strings.TrimSpace(condition.Column), params[condition.From]
switch op {
case "BETWEEN":
start, end := params["start"+condition.From], params["end"+condition.From]
if start != "" && end != "" {
wheres = append(wheres, fmt.Sprintf("%s BETWEEN '%s' AND '%s'", column, start, end))
} else {
wheres = append(wheres, fmt.Sprintf("%s BETWEEN {start%s} AND {end%s}", column, condition.From, condition.From))
}
case "IN", "NOT IN":
if value != "" {
parts := strings.Split(value, ",")
for index := range parts {
parts[index] = strings.TrimSpace(parts[index])
}
wheres = append(wheres, fmt.Sprintf("%s %s ('%s')", column, op, strings.Join(parts, "','")))
} else {
wheres = append(wheres, fmt.Sprintf("%s %s ({%s})", column, op, condition.From))
}
case "LIKE":
if value != "" {
wheres = append(wheres, fmt.Sprintf("%s LIKE '%%%s%%'", column, value))
} else {
wheres = append(wheres, fmt.Sprintf("%s LIKE {%%%s%%}", column, condition.From))
}
default:
if value != "" {
wheres = append(wheres, fmt.Sprintf("%s %s '%s'", column, op, value))
} else {
wheres = append(wheres, fmt.Sprintf("%s %s {%s}", column, op, condition.From))
}
}
}
if len(wheres) > 0 {
builder.WriteString(" WHERE " + strings.Join(wheres, " AND "))
}
order := params["order"]
if order == "" {
order = t.Order
}
if order != "" {
builder.WriteString(" ORDER BY " + order)
}
limitRaw, offsetRaw := params["limit"], params["offset"]
if limitRaw == "" && t.Limit != nil && *t.Limit != 0 {
limitRaw = strconv.Itoa(*t.Limit)
}
limit, _ := strconv.Atoi(limitRaw)
offset, _ := strconv.Atoi(offsetRaw)
if limit > 0 {
builder.WriteString(" LIMIT " + strconv.Itoa(limit))
if offset > 0 {
builder.WriteString(" OFFSET " + strconv.Itoa(offset))
}
} else if offset > 0 {
builder.WriteString(" OFFSET " + strconv.Itoa(offset))
}
return builder.String(), nil
}
func parseTemplateColumns(raw string) map[string]string {
out := map[string]string{}
parts := strings.Split(strings.Trim(strings.TrimSpace(raw), "{}"), ",")
@@ -293,16 +424,29 @@ func parseTemplateColumns(raw string) map[string]string {
}
func (r *exportRepo) ImportExportRows(ctx context.Context, t *biz.ExportTemplate, rows []map[string]any) error {
sql := strings.TrimSpace(t.ImportSQL)
lower := strings.ToLower(sql)
if sql == "" || strings.Contains(sql, ";") || (!strings.HasPrefix(lower, "insert ") && !strings.HasPrefix(lower, "update ")) {
return errors.New("导入 SQL 仅允许单条 INSERT 或 UPDATE")
selected, err := r.data.database(t.DBName)
if err != nil {
return err
}
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
for index, row := range rows {
if err := tx.Exec(sql, row).Error; err != nil {
return fmt.Errorf("第%d行导入失败: %w", index+2, err)
return selected.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if sql != "" {
for _, row := range rows {
if err := tx.Exec(sql, row).Error; err != nil {
return err
}
}
return nil
}
needCreated := tx.Migrator().HasColumn(t.TableName, "created_at")
needUpdated := tx.Migrator().HasColumn(t.TableName, "updated_at")
for _, row := range rows {
if row["created_at"] == nil && needCreated {
row["created_at"] = time.Now()
}
if row["updated_at"] == nil && needUpdated {
row["updated_at"] = time.Now()
}
}
return nil
return tx.Table(t.TableName).CreateInBatches(&rows, 1000).Error
})
}
+1 -1
View File
@@ -16,7 +16,7 @@ type reloadableStorage struct {
}
func NewFileStorage(data *Data) (biz.FileStorage, error) {
storage, err := buildFileStorage(data.admin)
storage, err := buildFileStorage(data.runtime.Admin())
if err != nil {
return nil, err
}
+13 -10
View File
@@ -29,19 +29,13 @@ func loginFromPO(v loginLogPO) *biz.LoginLog {
return &biz.LoginLog{ID: v.ID, CreatedAt: v.CreatedAt, Username: v.Username, IP: v.IP, Status: v.Status, ErrorMessage: v.ErrorMessage, Agent: v.Agent, UserID: v.UserID}
}
func (r *auditRepo) ListLogins(ctx context.Context, page, size int, q *biz.LoginLog) ([]*biz.LoginLog, int64, error) {
if page < 1 {
page = 1
}
if size < 1 {
size = 10
}
db := r.data.gormDB.WithContext(ctx).Model(&loginLogPO{})
if q != nil {
if q.Username != "" {
db = db.Where("username LIKE ?", "%"+q.Username+"%")
}
if q.IP != "" {
db = db.Where("ip LIKE ?", "%"+q.IP+"%")
if q.FilterByStatus {
db = db.Where("status = ?", q.Status)
}
}
var total int64
@@ -49,12 +43,21 @@ func (r *auditRepo) ListLogins(ctx context.Context, page, size int, q *biz.Login
return nil, 0, err
}
var pos []loginLogPO
if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
if err := applyPagination(db.Order("id desc"), page, size, 100).Find(&pos).Error; err != nil {
return nil, 0, err
}
ids := make([]uint, 0, len(pos))
for _, value := range pos {
if value.UserID != 0 {
ids = append(ids, value.UserID)
}
}
users := auditUsers(ctx, r.data.gormDB.WithContext(ctx), ids)
out := make([]*biz.LoginLog, 0, len(pos))
for _, po := range pos {
out = append(out, loginFromPO(po))
value := loginFromPO(po)
value.User = users[po.UserID]
out = append(out, value)
}
return out, total, nil
}
+27 -17
View File
@@ -71,32 +71,45 @@ func (r *mediaRepo) FindMediaByHash(ctx context.Context, userID uint, hash strin
}
return mediaFromPO(po), nil
}
func (r *mediaRepo) ListMedia(ctx context.Context, page, size int, keyword string, categoryID int, tag string, userID uint) ([]*biz.MediaFile, int64, error) {
if page < 1 {
page = 1
func (r *mediaRepo) ListMedia(ctx context.Context, filter biz.MediaFilter) ([]*biz.MediaFile, int64, error) {
if filter.Page < 1 {
filter.Page = 1
}
if size < 1 {
size = 10
if filter.PageSize < 1 {
filter.PageSize = 10
}
db := r.data.gormDB.WithContext(ctx).Model(&mediaPO{})
if keyword != "" {
db = db.Where("name LIKE ?", "%"+keyword+"%")
if filter.Keyword != "" {
db = db.Where("name LIKE ?", "%"+filter.Keyword+"%")
}
if categoryID > 0 {
db = db.Where("class_id = ?", categoryID)
if filter.CategoryID > 0 {
db = db.Where("class_id = ?", filter.CategoryID)
}
if tag != "" {
db = db.Where("tag = ?", tag)
if filter.Tag != "" {
db = db.Where("tag = ?", filter.Tag)
}
if userID > 0 {
db = db.Where("user_id = ?", userID)
if filter.UserID > 0 {
db = db.Where("user_id = ?", filter.UserID)
}
if !filter.StartCreatedAt.IsZero() {
db = db.Where("created_at >= ?", filter.StartCreatedAt)
}
if !filter.EndCreatedAt.IsZero() {
db = db.Where("created_at < ?", filter.EndCreatedAt)
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
var pos []mediaPO
if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
orderKey := filter.OrderKey
if orderKey != "id" && orderKey != "size" && orderKey != "created_at" && orderKey != "name" {
orderKey = "id"
}
if filter.Desc {
orderKey += " desc"
}
if err := applyPagination(db.Order(orderKey), filter.Page, filter.PageSize, 100).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.MediaFile, 0, len(pos))
@@ -130,9 +143,6 @@ func (r *mediaRepo) SaveCategory(ctx context.Context, v *biz.AttachmentCategory)
db := r.data.gormDB.WithContext(ctx)
var count int64
query := db.Model(&categoryPO{}).Where("name = ? AND pid = ?", v.Name, v.ParentID)
if v.ID != 0 {
query = query.Where("id <> ?", v.ID)
}
if err := query.Count(&count).Error; err != nil {
return err
}
+47 -1
View File
@@ -193,7 +193,18 @@ func (r *menuRepo) Find(ctx context.Context, id uint) (*biz.Menu, error) {
func (r *menuRepo) List(ctx context.Context) ([]*biz.Menu, error) {
var pos []menuPO
if err := r.data.gormDB.WithContext(ctx).Order("sort,id").Find(&pos).Error; err != nil {
db := r.data.gormDB.WithContext(ctx).Model(&menuPO{}).Order("sort,id")
config := r.data.runtime.Admin()
if actor, ok := biz.ActorFromContext(ctx); ok && config != nil && config.System != nil && config.System.UseStrictAuth {
var authority authorityPO
if err := r.data.gormDB.WithContext(ctx).Where("authority_id = ?", actor.AuthorityID).First(&authority).Error; err != nil {
return nil, err
}
if authority.ParentID != nil && *authority.ParentID != 0 {
db = db.Where("id IN (?)", r.data.gormDB.WithContext(ctx).Model(&authorityMenuPO{}).Select("sys_base_menu_id").Where("sys_authority_authority_id = ?", actor.AuthorityID))
}
}
if err := db.Find(&pos).Error; err != nil {
return nil, err
}
out := make([]*biz.Menu, 0, len(pos))
@@ -231,6 +242,31 @@ func (r *menuRepo) List(ctx context.Context) ([]*biz.Menu, error) {
}
func (r *menuRepo) SetAuthorityMenus(ctx context.Context, id uint, ids []uint) error {
if err := (&accessRepo{data: r.data}).checkAuthorityIDAuth(ctx, id); err != nil {
return err
}
config := r.data.runtime.Admin()
if actor, ok := biz.ActorFromContext(ctx); ok && config != nil && config.System != nil && config.System.UseStrictAuth {
var authority authorityPO
if err := r.data.gormDB.WithContext(ctx).Where("authority_id = ?", actor.AuthorityID).First(&authority).Error; err != nil {
return err
}
if authority.ParentID != nil && *authority.ParentID != 0 {
var allowedIDs []uint
if err := r.data.gormDB.WithContext(ctx).Model(&authorityMenuPO{}).Where("sys_authority_authority_id = ?", actor.AuthorityID).Pluck("sys_base_menu_id", &allowedIDs).Error; err != nil {
return err
}
allowed := make(map[uint]bool, len(allowedIDs))
for _, menuID := range allowedIDs {
allowed[menuID] = true
}
for _, menuID := range ids {
if !allowed[menuID] {
return errors.New("添加失败,请勿跨级操作")
}
}
}
}
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("sys_authority_authority_id = ?", id).Delete(&authorityMenuPO{}).Error; err != nil {
return err
@@ -258,6 +294,16 @@ func (r *menuRepo) MenuRoleIDs(ctx context.Context, id uint) ([]uint, error) {
return ids, err
}
func (r *menuRepo) DefaultRouterRoleIDs(ctx context.Context, id uint) ([]uint, error) {
var menu menuPO
if err := r.data.gormDB.WithContext(ctx).First(&menu, id).Error; err != nil {
return nil, err
}
var ids []uint
err := r.data.gormDB.WithContext(ctx).Model(&authorityPO{}).Where("default_router = ?", menu.Name).Pluck("authority_id", &ids).Error
return ids, err
}
func (r *menuRepo) SetMenuRoles(ctx context.Context, id uint, ids []uint) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("sys_base_menu_id = ?", id).Delete(&authorityMenuPO{}).Error; err != nil {
+30 -14
View File
@@ -36,12 +36,6 @@ func opFromPO(v operationPO) *biz.OperationRecord {
return &biz.OperationRecord{ID: v.ID, CreatedAt: v.CreatedAt, IP: v.IP, Method: v.Method, Path: v.Path, Status: v.Status, LatencyMS: v.LatencyMS, Agent: v.Agent, ErrorMessage: v.ErrorMessage, Body: v.Body, Response: v.Response, UserID: v.UserID, RequestID: v.RequestID, TraceID: v.TraceID, DeviceID: v.DeviceID}
}
func (r *auditRepo) ListOperations(ctx context.Context, page, size int, q *biz.OperationRecord) ([]*biz.OperationRecord, int64, error) {
if page < 1 {
page = 1
}
if size < 1 {
size = 10
}
db := r.data.gormDB.WithContext(ctx).Model(&operationPO{})
if q != nil {
if q.Path != "" {
@@ -53,27 +47,49 @@ func (r *auditRepo) ListOperations(ctx context.Context, page, size int, q *biz.O
if q.Status != 0 {
db = db.Where("status = ?", q.Status)
}
if q.IP != "" {
db = db.Where("ip LIKE ?", "%"+q.IP+"%")
}
if q.UserID != 0 {
db = db.Where("user_id = ?", q.UserID)
}
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
var pos []operationPO
if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
if err := applyPagination(db.Order("id desc"), page, size, 100).Find(&pos).Error; err != nil {
return nil, 0, err
}
users := auditUsers(ctx, r.data.gormDB.WithContext(ctx), operationUserIDs(pos))
out := make([]*biz.OperationRecord, 0, len(pos))
for _, po := range pos {
out = append(out, opFromPO(po))
value := opFromPO(po)
value.User = users[po.UserID]
out = append(out, value)
}
return out, total, nil
}
func operationUserIDs(values []operationPO) []uint {
ids := make([]uint, 0, len(values))
for _, value := range values {
if value.UserID != 0 {
ids = append(ids, value.UserID)
}
}
return ids
}
func auditUsers(ctx context.Context, db *gorm.DB, ids []uint) map[uint]*biz.User {
result := make(map[uint]*biz.User)
if len(ids) == 0 {
return result
}
var users []userPO
if db.WithContext(ctx).Where("id IN ?", ids).Find(&users).Error != nil {
return result
}
for _, value := range users {
result[value.ID] = &biz.User{ID: value.ID, CreatedAt: value.CreatedAt, UpdatedAt: value.UpdatedAt, UUID: value.UUID, Username: value.Username, NickName: value.NickName, HeaderImg: value.HeaderImg, AuthorityID: value.AuthorityID, DeptID: value.DeptID, Phone: value.Phone, Email: value.Email, Enable: value.Enable}
}
return result
}
func (r *auditRepo) FindOperation(ctx context.Context, id uint) (*biz.OperationRecord, error) {
var po operationPO
if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil {
+16
View File
@@ -0,0 +1,16 @@
package data
import "gorm.io/gorm"
func applyPagination(db *gorm.DB, page, size, maxSize int) *gorm.DB {
if size <= 0 {
return db
}
if maxSize > 0 && size > maxSize {
size = maxSize
}
if page <= 0 {
page = 1
}
return db.Offset((page - 1) * size).Limit(size)
}
+4 -7
View File
@@ -52,14 +52,11 @@ func (r *settingsRepo) FindParameter(ctx context.Context, id uint, key string) (
return parameterFromPO(po), nil
}
func (r *settingsRepo) ListParameters(ctx context.Context, page, size int, q *biz.SystemParameter) ([]*biz.SystemParameter, int64, error) {
if page < 1 {
page = 1
}
if size < 1 {
size = 10
}
db := r.data.gormDB.WithContext(ctx).Model(&parameterPO{})
if q != nil {
if q.StartCreatedAt != nil && q.EndCreatedAt != nil {
db = db.Where("created_at BETWEEN ? AND ?", q.StartCreatedAt, q.EndCreatedAt)
}
if q.Name != "" {
db = db.Where("name LIKE ?", "%"+q.Name+"%")
}
@@ -72,7 +69,7 @@ func (r *settingsRepo) ListParameters(ctx context.Context, page, size int, q *bi
return nil, 0, err
}
var pos []parameterPO
if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
if err := applyPagination(db, page, size, 100).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.SystemParameter, 0, len(pos))
+8 -8
View File
@@ -60,13 +60,7 @@ func (r *organizationRepo) FindPosition(ctx context.Context, id uint) (*biz.Posi
}
return posFromPO(po), nil
}
func (r *organizationRepo) ListPositions(ctx context.Context, page, size int, q *biz.Position) ([]*biz.Position, int64, error) {
if page < 1 {
page = 1
}
if size < 1 {
size = 10
}
func (r *organizationRepo) ListPositions(ctx context.Context, page, size int, q *biz.PositionListFilter) ([]*biz.Position, int64, error) {
db := r.data.gormDB.WithContext(ctx).Model(&positionPO{})
if q != nil {
if q.Name != "" {
@@ -75,13 +69,16 @@ func (r *organizationRepo) ListPositions(ctx context.Context, page, size int, q
if q.Code != "" {
db = db.Where("code LIKE ?", "%"+q.Code+"%")
}
if q.Status != nil {
db = db.Where("status = ?", *q.Status)
}
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
var pos []positionPO
if err := db.Order("sort,id").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
if err := applyPagination(db.Order("sort"), page, size, 0).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.Position, 0, len(pos))
@@ -96,6 +93,9 @@ func (r *organizationRepo) PositionUserIDs(ctx context.Context, id uint) ([]uint
return ids, err
}
func (r *organizationRepo) SetPositionUsers(ctx context.Context, id uint, ids []uint) error {
if id == 0 {
return errors.New("岗位ID不能为空")
}
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("sys_position_id = ?", id).Delete(&userPositionPO{}).Error; err != nil {
return err
+104
View File
@@ -0,0 +1,104 @@
package data
import (
"context"
"sync"
"sync/atomic"
"github.com/redis/go-redis/v9"
"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
}
func newReloadableDB(db *gorm.DB) *reloadableDB {
r := &reloadableDB{}
r.current.Store(db)
return r
}
func (r *reloadableDB) WithContext(ctx context.Context) *gorm.DB {
return r.current.Load().WithContext(ctx)
}
func (r *reloadableDB) DB() *gorm.DB { return r.current.Load() }
func (r *reloadableDB) replace(db *gorm.DB) {
old := r.current.Swap(db)
if old != nil && old != db {
r.mu.Lock()
r.retired = append(r.retired, old)
r.mu.Unlock()
}
}
func (r *reloadableDB) close() {
current := r.current.Load()
r.mu.Lock()
all := append([]*gorm.DB{current}, r.retired...)
r.retired = nil
r.mu.Unlock()
seen := map[*gorm.DB]struct{}{}
for _, db := range all {
if db == nil {
continue
}
if _, ok := seen[db]; ok {
continue
}
seen[db] = struct{}{}
if sqlDB, err := db.DB(); err == nil {
_ = sqlDB.Close()
}
}
}
type reloadableRedis struct {
current atomic.Pointer[redis.Client]
mu sync.Mutex
retired []*redis.Client
}
func newReloadableRedis(client *redis.Client) *reloadableRedis {
r := &reloadableRedis{}
if client != nil {
r.current.Store(client)
}
return r
}
func (r *reloadableRedis) load() *redis.Client { return r.current.Load() }
func (r *reloadableRedis) replace(client *redis.Client) {
old := r.current.Swap(client)
if old != nil && old != client {
r.mu.Lock()
r.retired = append(r.retired, old)
r.mu.Unlock()
}
}
func (r *reloadableRedis) close() {
current := r.current.Load()
r.mu.Lock()
all := append([]*redis.Client{current}, r.retired...)
r.retired = nil
r.mu.Unlock()
seen := map[*redis.Client]struct{}{}
for _, client := range all {
if client == nil {
continue
}
if _, ok := seen[client]; ok {
continue
}
seen[client] = struct{}{}
_ = client.Close()
}
}
+31 -5
View File
@@ -19,12 +19,37 @@ import (
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)
currentData, currentAdmin := r.data.runtime.Values()
next := proto.Clone(currentAdmin).(*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)
next.ConfigPath = currentAdmin.ConfigPath
if err := r.data.persistConfigValues(currentData, next); err != nil {
return err
}
// Writing through the management API updates the same in-memory values
// immediately; the file watcher remains the fallback for external edits.
r.data.runtime.Replace(currentData, next)
return nil
}
func (r *systemRepo) PersistRuntimeConfig(_ context.Context, dataRaw, adminRaw []byte) error {
currentData, currentAdmin := r.data.runtime.Values()
nextData := proto.Clone(currentData).(*conf.Data)
nextAdmin := proto.Clone(currentAdmin).(*conf.AdminBackend)
options := protojson.UnmarshalOptions{DiscardUnknown: true}
if err := options.Unmarshal(dataRaw, nextData); err != nil {
return err
}
if err := options.Unmarshal(adminRaw, nextAdmin); err != nil {
return err
}
nextAdmin.ConfigPath = currentAdmin.ConfigPath
if err := r.data.persistConfigValues(nextData, nextAdmin); err != nil {
return err
}
r.data.runtime.Replace(nextData, nextAdmin)
return nil
}
func (r *systemRepo) ReloadConfig(ctx context.Context) error {
return r.data.reloadConfig(ctx)
@@ -161,9 +186,10 @@ func (r *systemRepo) Initialize(ctx context.Context, input *biz.DatabaseConfig)
}); err != nil {
return err
}
nextData := proto.Clone(r.data.config).(*conf.Data)
currentData, currentAdmin := r.data.runtime.Values()
nextData := proto.Clone(currentData).(*conf.Data)
nextData.Database = config
if err := r.data.persistConfigValues(nextData, r.data.admin); err != nil {
if err := r.data.persistConfigValues(nextData, currentAdmin); err != nil {
return fmt.Errorf("persist database configuration: %w", err)
}
r.data.activateDatabase(candidate, config)
+2 -14
View File
@@ -83,12 +83,6 @@ func (r *taskRepo) FindTask(ctx context.Context, id uint) (*biz.TimedTask, error
return taskFromPO(po), nil
}
func (r *taskRepo) ListTasks(ctx context.Context, page, size int, q *biz.TimedTask) ([]*biz.TimedTask, int64, error) {
if page < 1 {
page = 1
}
if size < 1 {
size = 10
}
db := r.data.gormDB.WithContext(ctx).Model(&taskPO{})
if q != nil {
if q.Name != "" {
@@ -106,7 +100,7 @@ func (r *taskRepo) ListTasks(ctx context.Context, page, size int, q *biz.TimedTa
return nil, 0, err
}
var pos []taskPO
if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
if err := applyPagination(db.Order("id desc"), page, size, 100).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.TimedTask, 0, len(pos))
@@ -125,12 +119,6 @@ func taskLogFromPO(v taskLogPO) *biz.TimedTaskLog {
return &biz.TimedTaskLog{ID: v.ID, CreatedAt: v.CreatedAt, TaskID: v.TaskID, TaskName: v.TaskName, TriggerType: v.TriggerType, StartedAt: v.StartedAt, FinishedAt: v.FinishedAt, DurationMS: v.DurationMS, Status: v.Status, ErrorMsg: v.ErrorMsg, Output: v.Output}
}
func (r *taskRepo) ListTaskLogs(ctx context.Context, page, size int, taskID uint, status string) ([]*biz.TimedTaskLog, int64, error) {
if page < 1 {
page = 1
}
if size < 1 {
size = 10
}
db := r.data.gormDB.WithContext(ctx).Model(&taskLogPO{})
if taskID != 0 {
db = db.Where("task_id = ?", taskID)
@@ -143,7 +131,7 @@ func (r *taskRepo) ListTaskLogs(ctx context.Context, page, size int, taskID uint
return nil, 0, err
}
var pos []taskLogPO
if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
if err := applyPagination(db.Order("id desc"), page, size, 100).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.TimedTaskLog, 0, len(pos))
+83
View File
@@ -0,0 +1,83 @@
package data
import (
"context"
"strings"
"testing"
"kra/internal/biz"
"kra/internal/conf"
)
func newTransactionTestData(t *testing.T) *Data {
t.Helper()
name := strings.NewReplacer("/", "_", " ", "_").Replace(t.Name())
db, err := openWithDriver("sqlite", "file:"+name+"?mode=memory&cache=shared")
if err != nil {
t.Fatal(err)
}
if err = migrateAll(db); err != nil {
t.Fatal(err)
}
data := &Data{gormDB: newReloadableDB(db), redis: newReloadableRedis(nil), runtime: conf.NewRuntime(&conf.Data{Database: &conf.Data_Database{Driver: "sqlite"}}, &conf.AdminBackend{})}
t.Cleanup(func() { data.gormDB.close() })
return data
}
func TestUserAuthorityWritesAreAtomic(t *testing.T) {
data := newTransactionTestData(t)
ctx := context.Background()
if err := data.gormDB.WithContext(ctx).Create(&[]authorityPO{{AuthorityID: 888, AuthorityName: "admin"}, {AuthorityID: 999, AuthorityName: "operator"}}).Error; err != nil {
t.Fatal(err)
}
repo := &systemRepo{data: data}
user := &biz.User{Username: "atomic", Password: "hash", NickName: "before", AuthorityID: 888, Enable: 1}
created, err := repo.CreateUserWithAuthorities(ctx, user, []uint{888, 999})
if err != nil {
t.Fatal(err)
}
var links int64
if err = data.gormDB.WithContext(ctx).Model(&userAuthorityPO{}).Where("sys_user_id = ?", created.ID).Count(&links).Error; err != nil || links != 2 {
t.Fatalf("authority links = %d, err = %v", links, err)
}
created.NickName = "must rollback"
if err = repo.UpdateUserWithAuthorities(ctx, created, []uint{888, 123456}); err == nil {
t.Fatal("expected invalid authority update to fail")
}
var persisted userPO
if err = data.gormDB.WithContext(ctx).First(&persisted, created.ID).Error; err != nil {
t.Fatal(err)
}
if persisted.NickName != "before" {
t.Fatalf("user update was not rolled back: %q", persisted.NickName)
}
invalid := &biz.User{Username: "invalid", Password: "hash", AuthorityID: 888, Enable: 1}
if _, err = repo.CreateUserWithAuthorities(ctx, invalid, []uint{888, 123456}); err == nil {
t.Fatal("expected invalid authority create to fail")
}
var users int64
if err = data.gormDB.WithContext(ctx).Model(&userPO{}).Where("username = ?", "invalid").Count(&users).Error; err != nil || users != 0 {
t.Fatalf("partial user persisted: count=%d err=%v", users, err)
}
}
func TestDictionaryImportKeepsHierarchyInOneTransaction(t *testing.T) {
data := newTransactionTestData(t)
ctx := context.Background()
repo := &settingsRepo{data: data}
parentID := uint(10)
dictionary := &biz.Dictionary{Name: "status", Type: "status", Status: true}
details := []*biz.DictionaryDetail{{ID: 10, Label: "parent", Value: "1", Status: true}, {ID: 11, Label: "child", Value: "2", ParentID: &parentID, Status: true}}
if err := repo.ImportDictionary(ctx, dictionary, details); err != nil {
t.Fatal(err)
}
var persisted []dictionaryDetailPO
if err := data.gormDB.WithContext(ctx).Where("sys_dictionary_id = ?", dictionary.ID).Order("id").Find(&persisted).Error; err != nil {
t.Fatal(err)
}
if len(persisted) != 2 || persisted[1].ParentID == nil || *persisted[1].ParentID != persisted[0].ID {
t.Fatalf("unexpected imported hierarchy: %+v", persisted)
}
}
+288 -48
View File
@@ -4,6 +4,8 @@ import (
"context"
"encoding/json"
"errors"
"strconv"
"strings"
"time"
"kra/internal/biz"
@@ -80,6 +82,119 @@ func toBizAuthority(po authorityPO) biz.Authority {
return biz.Authority{AuthorityID: po.AuthorityID, AuthorityName: po.AuthorityName, ParentID: po.ParentID, DataScope: po.DataScope, DefaultRouter: po.DefaultRouter}
}
func baseBizUser(po *userPO) *biz.User {
setting := map[string]any(nil)
if po.OriginSetting != "" {
_ = json.Unmarshal([]byte(po.OriginSetting), &setting)
}
return &biz.User{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, UUID: po.UUID, Username: po.Username, Password: po.Password, NickName: po.NickName, HeaderImg: po.HeaderImg, AuthorityID: po.AuthorityID, DeptID: po.DeptID, Phone: po.Phone, Email: po.Email, Enable: po.Enable, OriginSetting: setting, MustChangePassword: po.MustChangePassword, PasswordUpdatedAt: po.PasswordUpdatedAt}
}
func (r *systemRepo) toBizUsers(ctx context.Context, pos []userPO) ([]*biz.User, error) {
if len(pos) == 0 {
return []*biz.User{}, nil
}
db := r.data.gormDB.WithContext(ctx)
userIDs := make([]uint, 0, len(pos))
authorityIDs := make([]uint, 0, len(pos))
departmentIDs := make([]uint, 0, len(pos))
for i := range pos {
userIDs = append(userIDs, pos[i].ID)
authorityIDs = append(authorityIDs, pos[i].AuthorityID)
if pos[i].DeptID != 0 {
departmentIDs = append(departmentIDs, pos[i].DeptID)
}
}
var authorityLinks []userAuthorityPO
if err := db.Where("sys_user_id IN ?", userIDs).Find(&authorityLinks).Error; err != nil {
return nil, err
}
authorityByUser := make(map[uint][]uint, len(pos))
for _, link := range authorityLinks {
authorityByUser[link.SysUserID] = append(authorityByUser[link.SysUserID], link.SysAuthorityAuthorityID)
authorityIDs = append(authorityIDs, link.SysAuthorityAuthorityID)
}
var authorityPOs []authorityPO
if err := db.Where("authority_id IN ?", authorityIDs).Find(&authorityPOs).Error; err != nil {
return nil, err
}
authorityByID := make(map[uint]authorityPO, len(authorityPOs))
for _, item := range authorityPOs {
authorityByID[item.AuthorityID] = item
}
var departmentLinks []userDepartmentPO
if err := db.Where("sys_user_id IN ?", userIDs).Find(&departmentLinks).Error; err != nil {
return nil, err
}
departmentByUser := make(map[uint][]uint, len(pos))
for _, link := range departmentLinks {
departmentByUser[link.UserID] = append(departmentByUser[link.UserID], link.DepartmentID)
departmentIDs = append(departmentIDs, link.DepartmentID)
}
var departmentPOs []departmentPO
if len(departmentIDs) > 0 {
if err := db.Where("id IN ?", departmentIDs).Find(&departmentPOs).Error; err != nil {
return nil, err
}
}
departmentByID := make(map[uint]departmentPO, len(departmentPOs))
for _, item := range departmentPOs {
departmentByID[item.ID] = item
}
var positionLinks []userPositionPO
if err := db.Where("sys_user_id IN ?", userIDs).Find(&positionLinks).Error; err != nil {
return nil, err
}
positionByUser := make(map[uint][]uint, len(pos))
positionIDs := make([]uint, 0, len(positionLinks))
for _, link := range positionLinks {
positionByUser[link.UserID] = append(positionByUser[link.UserID], link.PositionID)
positionIDs = append(positionIDs, link.PositionID)
}
var positionPOs []positionPO
if len(positionIDs) > 0 {
if err := db.Where("id IN ?", positionIDs).Find(&positionPOs).Error; err != nil {
return nil, err
}
}
positionByID := make(map[uint]positionPO, len(positionPOs))
for _, item := range positionPOs {
positionByID[item.ID] = item
}
users := make([]*biz.User, 0, len(pos))
for i := range pos {
po := &pos[i]
user := baseBizUser(po)
user.Authority = toBizAuthority(authorityByID[po.AuthorityID])
user.Authorities = make([]biz.Authority, 0, len(authorityByUser[po.ID]))
for _, id := range authorityByUser[po.ID] {
if item, ok := authorityByID[id]; ok {
user.Authorities = append(user.Authorities, toBizAuthority(item))
}
}
user.Departments = make([]biz.Department, 0, len(departmentByUser[po.ID]))
for _, id := range departmentByUser[po.ID] {
if item, ok := departmentByID[id]; ok {
user.Departments = append(user.Departments, *deptFromPO(item))
}
}
if primary, ok := departmentByID[po.DeptID]; ok {
user.Department = deptFromPO(primary)
}
user.Positions = make([]biz.Position, 0, len(positionByUser[po.ID]))
for _, id := range positionByUser[po.ID] {
if item, ok := positionByID[id]; ok {
user.Positions = append(user.Positions, *posFromPO(item))
}
}
users = append(users, user)
}
return users, nil
}
func (r *systemRepo) MenusByAuthority(ctx context.Context, authorityID uint) ([]*biz.Menu, error) {
var pos []menuPO
err := r.data.gormDB.WithContext(ctx).Table("sys_base_menus").
@@ -90,23 +205,31 @@ func (r *systemRepo) MenusByAuthority(ctx context.Context, authorityID uint) ([]
return nil, err
}
byID := make(map[uint]*biz.Menu, len(pos))
menuIDs := make([]uint, 0, len(pos))
for _, po := range pos {
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{}}
menu.Parameters = []*biz.MenuParameter{}
menu.Buttons = []*biz.MenuButton{}
byID[po.ID] = menu
menuIDs = append(menuIDs, po.ID)
}
if len(menuIDs) > 0 {
var parameters []menuParameterPO
if err := r.data.gormDB.WithContext(ctx).Where("sys_base_menu_id = ?", po.ID).Order("id").Find(&parameters).Error; err != nil {
if err := r.data.gormDB.WithContext(ctx).Where("sys_base_menu_id IN ?", menuIDs).Order("id").Find(&parameters).Error; err != nil {
return nil, err
}
for _, parameter := range parameters {
menu := byID[parameter.MenuID]
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 {
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 IN ?", authorityID, menuIDs).Find(&buttons).Error; err != nil {
return nil, err
}
for _, button := range buttons {
menu := byID[button.MenuID]
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 {
@@ -121,22 +244,7 @@ func (r *systemRepo) MenusByAuthority(ctx context.Context, authorityID uint) ([]
}
func (r *systemRepo) ListUsers(ctx context.Context, page, pageSize int, filter *biz.UserListFilter) ([]*biz.User, int64, error) {
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 10
}
db := r.data.gormDB.WithContext(ctx).Model(&userPO{})
if scope, ok := biz.DataScopeFromContext(ctx); ok && !scope.All {
if scope.OwnerUserID != 0 {
db = db.Where("sys_users.id = ?", scope.OwnerUserID)
} else if len(scope.DepartmentIDs) > 0 {
db = db.Joins("JOIN sys_user_departments ON sys_user_departments.sys_user_id = sys_users.id").Where("sys_user_departments.sys_department_id IN ?", scope.DepartmentIDs).Distinct("sys_users.id")
} else {
db = db.Where("1 = 0")
}
}
order := "id desc"
if filter != nil {
if filter.Username != "" {
@@ -151,7 +259,7 @@ func (r *systemRepo) ListUsers(ctx context.Context, page, pageSize int, filter *
if filter.Email != "" {
db = db.Where("email LIKE ?", "%"+filter.Email+"%")
}
allowed := map[string]bool{"id": true, "username": true, "nick_name": true, "phone": true, "email": true, "created_at": true}
allowed := map[string]bool{"id": true, "username": true, "nick_name": true, "phone": true, "email": true}
if allowed[filter.OrderKey] {
order = filter.OrderKey
if filter.Desc {
@@ -164,21 +272,18 @@ func (r *systemRepo) ListUsers(ctx context.Context, page, pageSize int, filter *
return nil, 0, err
}
var pos []userPO
if err := db.Order(order).Offset((page - 1) * pageSize).Limit(pageSize).Find(&pos).Error; err != nil {
if err := applyPagination(db.Order(order), page, pageSize, 100).Find(&pos).Error; err != nil {
return nil, 0, err
}
users := make([]*biz.User, 0, len(pos))
for i := range pos {
user, err := r.toBizUser(ctx, &pos[i])
if err != nil {
return nil, 0, err
}
users = append(users, user)
}
return users, total, nil
users, err := r.toBizUsers(ctx, pos)
return users, total, err
}
func (r *systemRepo) CreateUser(ctx context.Context, user *biz.User) (*biz.User, error) {
return r.CreateUserWithAuthorities(ctx, user, nil)
}
func (r *systemRepo) CreateUserWithAuthorities(ctx context.Context, user *biz.User, authorityIDs []uint) (*biz.User, error) {
if user.UUID == "" {
user.UUID = uuid.NewString()
}
@@ -193,23 +298,134 @@ func (r *systemRepo) CreateUser(ctx context.Context, user *biz.User) (*biz.User,
}
now := time.Now()
po := userPO{UUID: user.UUID, Username: user.Username, Password: user.Password, NickName: user.NickName, HeaderImg: user.HeaderImg, AuthorityID: user.AuthorityID, Phone: user.Phone, Email: user.Email, Enable: user.Enable, PasswordUpdatedAt: &now, MustChangePassword: user.MustChangePassword}
if len(authorityIDs) == 0 {
authorityIDs = []uint{user.AuthorityID}
}
if err := r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := validateAuthorities(tx, authorityIDs); err != nil {
return err
}
if err := tx.Create(&po).Error; err != nil {
return err
}
return tx.Create(&userAuthorityPO{SysUserID: po.ID, SysAuthorityAuthorityID: po.AuthorityID}).Error
links := make([]userAuthorityPO, 0, len(authorityIDs))
for _, authorityID := range authorityIDs {
links = append(links, userAuthorityPO{SysUserID: po.ID, SysAuthorityAuthorityID: authorityID})
}
return tx.Create(&links).Error
}); err != nil {
return nil, err
}
return r.FindUserByID(ctx, po.ID)
}
func validateAuthorities(tx *gorm.DB, authorityIDs []uint) error {
for _, authorityID := range authorityIDs {
var count int64
if err := tx.Model(&authorityPO{}).Where("authority_id = ?", authorityID).Count(&count).Error; err != nil {
return err
}
if count == 0 {
return errors.New("角色不存在")
}
}
return nil
}
func (r *systemRepo) UpdateUser(ctx context.Context, user *biz.User) error {
return r.updateUser(r.data.gormDB.WithContext(ctx), user)
}
func (r *systemRepo) UpdateSelfUser(ctx context.Context, user *biz.User) error {
updates := make(map[string]any)
if user.NickName != "" {
updates["nick_name"] = user.NickName
}
if user.HeaderImg != "" {
updates["header_img"] = user.HeaderImg
}
if user.Phone != "" {
updates["phone"] = user.Phone
}
if user.Email != "" {
updates["email"] = user.Email
}
if user.Enable != 0 {
updates["enable"] = user.Enable
}
if len(updates) == 0 {
return nil
}
return r.data.gormDB.WithContext(ctx).Model(&userPO{}).Where("id = ?", user.ID).Updates(updates).Error
}
func (r *systemRepo) FillDepartmentNamePaths(ctx context.Context, user *biz.User) error {
if user == nil || len(user.Departments) == 0 {
return nil
}
idSet := make(map[uint]bool)
for i := range user.Departments {
for _, part := range strings.Split(user.Departments[i].Ancestors, ",") {
id, err := strconv.ParseUint(strings.TrimSpace(part), 10, 64)
if err == nil && id != 0 {
idSet[uint(id)] = true
}
}
}
ids := make([]uint, 0, len(idSet))
for id := range idSet {
ids = append(ids, id)
}
nameByID := make(map[uint]string, len(ids))
if len(ids) > 0 {
var rows []departmentPO
if err := r.data.gormDB.WithContext(ctx).Select("id", "name").Where("id IN ?", ids).Find(&rows).Error; err != nil {
return err
}
for _, row := range rows {
nameByID[row.ID] = row.Name
}
}
for i := range user.Departments {
parts := make([]string, 0)
for _, part := range strings.Split(user.Departments[i].Ancestors, ",") {
id, err := strconv.ParseUint(strings.TrimSpace(part), 10, 64)
if err == nil && id != 0 && nameByID[uint(id)] != "" {
parts = append(parts, nameByID[uint(id)])
}
}
parts = append(parts, user.Departments[i].Name)
user.Departments[i].NamePath = strings.Join(parts, "/")
}
if user.Department != nil {
for i := range user.Departments {
if user.Departments[i].ID == user.Department.ID {
user.Department.NamePath = user.Departments[i].NamePath
break
}
}
}
return nil
}
func (r *systemRepo) updateUser(tx *gorm.DB, user *biz.User) error {
updates := map[string]any{"nick_name": user.NickName, "header_img": user.HeaderImg, "phone": user.Phone, "email": user.Email, "enable": user.Enable}
if user.AuthorityID != 0 {
updates["authority_id"] = user.AuthorityID
}
return r.data.gormDB.WithContext(ctx).Model(&userPO{}).Where("id = ?", user.ID).Updates(updates).Error
return tx.Model(&userPO{}).Where("id = ?", user.ID).Updates(updates).Error
}
func (r *systemRepo) UpdateUserWithAuthorities(ctx context.Context, user *biz.User, authorityIDs []uint) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := r.updateUser(tx, user); err != nil {
return err
}
if len(authorityIDs) == 0 {
return nil
}
return setUserAuthorities(tx, user.ID, authorityIDs)
})
}
func (r *systemRepo) DeleteUser(ctx context.Context, id uint) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
@@ -248,36 +464,60 @@ func (r *systemRepo) ListAuthorities(ctx context.Context) ([]*biz.Authority, err
func (r *systemRepo) SetUserAuthorities(ctx context.Context, id uint, authorityIDs []uint) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("sys_user_id = ?", id).Delete(&userAuthorityPO{}).Error; err != nil {
return err
}
links := make([]userAuthorityPO, 0, len(authorityIDs))
access := &accessRepo{data: r.data}
for _, authorityID := range authorityIDs {
var count int64
if err := tx.Model(&authorityPO{}).Where("authority_id = ?", authorityID).Count(&count).Error; err != nil || count == 0 {
if err != nil {
return err
}
return errors.New("角色不存在")
if err := access.checkAuthorityIDAuth(ctx, authorityID); err != nil {
return err
}
links = append(links, userAuthorityPO{SysUserID: id, SysAuthorityAuthorityID: authorityID})
}
if err := tx.Create(&links).Error; err != nil {
return err
}
return tx.Model(&userPO{}).Where("id = ?", id).Update("authority_id", authorityIDs[0]).Error
return setUserAuthorities(tx, id, authorityIDs)
})
}
func setUserAuthorities(tx *gorm.DB, id uint, authorityIDs []uint) error {
if len(authorityIDs) == 0 {
return errors.New("角色不能为空")
}
if err := validateAuthorities(tx, authorityIDs); err != nil {
return err
}
if err := tx.Where("sys_user_id = ?", id).Delete(&userAuthorityPO{}).Error; err != nil {
return err
}
links := make([]userAuthorityPO, 0, len(authorityIDs))
for _, authorityID := range authorityIDs {
links = append(links, userAuthorityPO{SysUserID: id, SysAuthorityAuthorityID: authorityID})
}
if err := tx.Create(&links).Error; err != nil {
return err
}
return tx.Model(&userPO{}).Where("id = ?", id).Update("authority_id", authorityIDs[0]).Error
}
func (r *systemRepo) SetUserAuthority(ctx context.Context, id, authorityID uint) error {
db := r.data.gormDB.WithContext(ctx)
var count int64
if err := r.data.gormDB.WithContext(ctx).Model(&userAuthorityPO{}).Where("sys_user_id = ? AND sys_authority_authority_id = ?", id, authorityID).Count(&count).Error; err != nil {
if err := db.Model(&userAuthorityPO{}).Where("sys_user_id = ? AND sys_authority_authority_id = ?", id, authorityID).Count(&count).Error; err != nil {
return err
}
if count == 0 {
return errors.New("该用户无此角色")
}
return r.data.gormDB.WithContext(ctx).Model(&userPO{}).Where("id = ?", id).Update("authority_id", authorityID).Error
var authority authorityPO
if err := db.Where("authority_id = ?", authorityID).First(&authority).Error; err != nil {
return err
}
var menuCount int64
if err := db.Model(&menuPO{}).
Joins("JOIN sys_authority_menus ON sys_authority_menus.sys_base_menu_id = sys_base_menus.id").
Where("sys_authority_menus.sys_authority_authority_id = ? AND sys_base_menus.name = ?", authorityID, authority.DefaultRouter).
Count(&menuCount).Error; err != nil {
return err
}
if menuCount == 0 {
return errors.New("找不到默认路由,无法切换本角色")
}
return db.Model(&userPO{}).Where("id = ?", id).Update("authority_id", authorityID).Error
}
func (r *systemRepo) SetUserSetting(ctx context.Context, id uint, setting map[string]any) error {
+43 -10
View File
@@ -47,14 +47,11 @@ func (r *versionRepo) FindVersion(ctx context.Context, id uint) (*biz.Version, e
}
return versionFromPO(po), nil
}
func (r *versionRepo) ListVersions(ctx context.Context, page, size int, name, code string) ([]*biz.Version, int64, error) {
if page < 1 {
page = 1
}
if size < 1 {
size = 10
}
func (r *versionRepo) ListVersions(ctx context.Context, page, size int, name, code string, createdAtRange []*time.Time) ([]*biz.Version, int64, error) {
db := r.data.gormDB.WithContext(ctx).Model(&versionPO{})
if len(createdAtRange) == 2 && createdAtRange[0] != nil && createdAtRange[1] != nil {
db = db.Where("created_at BETWEEN ? AND ?", createdAtRange[0], createdAtRange[1])
}
if name != "" {
db = db.Where("version_name LIKE ?", "%"+name+"%")
}
@@ -66,7 +63,7 @@ func (r *versionRepo) ListVersions(ctx context.Context, page, size int, name, co
return nil, 0, err
}
var pos []versionPO
if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
if err := applyPagination(db, page, size, 100).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.Version, 0, len(pos))
@@ -94,14 +91,45 @@ func (r *versionRepo) BuildVersionBundle(ctx context.Context, menuIDs, apiIDs, d
}
byID[po.ID] = v
}
if len(menuIDs) > 0 {
var parameters []menuParameterPO
if err := r.data.gormDB.WithContext(ctx).Where("sys_base_menu_id IN ?", menuIDs).Find(&parameters).Error; err != nil {
return nil, err
}
for _, parameter := range parameters {
if menu := byID[parameter.MenuID]; menu != nil {
menu.Parameters = append(menu.Parameters, &biz.MenuParameter{Type: parameter.Type, Key: parameter.Key, Value: parameter.Value})
}
}
}
for _, po := range menus {
v := byID[po.ID]
if parent := byID[po.ParentID]; parent != nil {
parent.Children = append(parent.Children, v)
} else {
} else if po.ParentID == 0 {
bundle.Menus = append(bundle.Menus, v)
}
}
var cleanMenu func(*biz.Menu)
cleanMenu = func(menu *biz.Menu) {
menu.ID = 0
menu.ParentID = 0
for _, button := range menu.Buttons {
button.ID = 0
button.MenuID = 0
button.AuthorityID = 0
}
for _, parameter := range menu.Parameters {
parameter.ID = 0
parameter.MenuID = 0
}
for _, child := range menu.Children {
cleanMenu(child)
}
}
for _, menu := range bundle.Menus {
cleanMenu(menu)
}
var apis []apiPO
if len(apiIDs) > 0 {
if err := r.data.gormDB.WithContext(ctx).Where("id IN ?", apiIDs).Find(&apis).Error; err != nil {
@@ -123,7 +151,7 @@ func (r *versionRepo) BuildVersionBundle(ctx context.Context, menuIDs, apiIDs, d
for _, po := range dictionaries {
v := dictionaryFromPO(po)
v.ID = 0
details, _, err := settings.ListDictionaryDetails(ctx, 1, 100000, po.ID, "", "")
details, _, err := settings.ListDictionaryDetails(ctx, 0, 0, biz.DictionaryDetailFilter{DictionaryID: po.ID})
if err != nil {
return nil, err
}
@@ -159,6 +187,11 @@ func (r *versionRepo) ImportVersionBundle(ctx context.Context, b *biz.VersionBun
return err
}
}
for _, parameter := range item.Parameters {
if err = tx.Create(&menuParameterPO{MenuID: po.ID, Type: parameter.Type, Key: parameter.Key, Value: parameter.Value}).Error; err != nil {
return err
}
}
} else if err != nil {
return err
}