fix: align admin behavior and Kratos integrations
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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(¶meterPO{})
|
||||
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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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))
|
||||
|
||||
@@ -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
@@ -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(¶meters).Error; err != nil {
|
||||
if err := r.data.gormDB.WithContext(ctx).Where("sys_base_menu_id IN ?", menuIDs).Order("id").Find(¶meters).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
@@ -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(¶meters).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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user