This commit is contained in:
2026-08-16 09:02:39 +08:00
parent c554152f4f
commit f7f6db8695
105 changed files with 2535 additions and 948 deletions
@@ -11,7 +11,7 @@ import (
"google.golang.org/protobuf/types/known/durationpb"
)
func gvaDatabase(value *conf.Data_Database) map[string]any {
func compatibleDatabase(value *conf.Data_Database) map[string]any {
if value == nil {
return map[string]any{}
}
@@ -28,14 +28,14 @@ func gvaDatabase(value *conf.Data_Database) map[string]any {
}
}
func gvaRedis(value *conf.Data_Redis) map[string]any {
func compatibleRedis(value *conf.Data_Redis) map[string]any {
if value == nil {
return map[string]any{}
}
return map[string]any{"name": value.Name, "addr": value.Addr, "password": value.Password, "db": value.Db, "useCluster": value.UseCluster, "clusterAddrs": value.ClusterAddrs}
}
func gvaObjectStore(value *conf.AdminBackend_ObjectStore) map[string]any {
func compatibleObjectStore(value *conf.AdminBackend_ObjectStore) map[string]any {
if value == nil {
return map[string]any{}
}
@@ -47,7 +47,7 @@ func gvaObjectStore(value *conf.AdminBackend_ObjectStore) map[string]any {
}
}
func gvaObjectStores(storage *conf.AdminBackend_Storage) map[string]map[string]any {
func compatibleObjectStores(storage *conf.AdminBackend_Storage) map[string]map[string]any {
if storage == nil {
storage = &conf.AdminBackend_Storage{}
}
@@ -82,7 +82,7 @@ func gvaObjectStores(storage *conf.AdminBackend_Storage) map[string]map[string]a
}
}
func gvaConfiguration(data *conf.Data, admin *conf.AdminBackend) map[string]any {
func compatibleConfiguration(data *conf.Data, admin *conf.AdminBackend) map[string]any {
result := map[string]any{}
if data == nil {
data = &conf.Data{}
@@ -107,10 +107,10 @@ func gvaConfiguration(data *conf.Data, admin *conf.AdminBackend) map[string]any
"access-req-headers": zap.AccessReqHeaders, "access-log-max-bytes": zap.AccessLogMaxBytes,
"file-only-modules": zap.FileOnlyModules,
}
result["redis"] = gvaRedis(data.Redis)
result["redis"] = compatibleRedis(data.Redis)
redisList := make([]map[string]any, 0, len(data.RedisList))
for _, item := range data.RedisList {
redisList = append(redisList, gvaRedis(item))
redisList = append(redisList, compatibleRedis(item))
}
result["redis-list"] = redisList
mongo := data.Mongo
@@ -146,11 +146,11 @@ func gvaConfiguration(data *conf.Data, admin *conf.AdminBackend) map[string]any
result[name] = map[string]any{}
}
if driver != "" {
result[driver] = gvaDatabase(data.Database)
result[driver] = compatibleDatabase(data.Database)
}
dbList := make([]map[string]any, 0, len(data.DatabaseList))
for _, item := range data.DatabaseList {
row := gvaDatabase(item)
row := compatibleDatabase(item)
if item != nil {
row["type"], row["alias-name"], row["disable"] = item.Driver, item.AliasName, item.Disable
}
@@ -166,7 +166,11 @@ func gvaConfiguration(data *conf.Data, admin *conf.AdminBackend) map[string]any
if media == nil {
media = &conf.AdminBackend_Media{}
}
result["media"] = map[string]any{"session-ttl": media.SessionTtl, "max-file-size": media.MaxFileSize, "chunk-dir": media.ChunkDir}
chunkDir := media.ChunkDir
if chunkDir == "" {
chunkDir = "uploads/chunks"
}
result["media"] = map[string]any{"session-ttl": media.SessionTtl, "max-file-size": media.MaxFileSize, "chunk-dir": chunkDir}
if admin.Storage == nil {
admin.Storage = &conf.AdminBackend_Storage{}
}
@@ -175,7 +179,7 @@ func gvaConfiguration(data *conf.Data, admin *conf.AdminBackend) map[string]any
qiniu = &conf.AdminBackend_Qiniu{}
}
result["qiniu"] = map[string]any{"zone": qiniu.Zone, "bucket": qiniu.Bucket, "img-path": qiniu.BaseUrl, "access-key": qiniu.AccessKey, "secret-key": qiniu.SecretKey, "use-https": qiniu.UseHttps, "use-cdn-domains": qiniu.UseCdnDomains}
for key, value := range gvaObjectStores(admin.Storage) {
for key, value := range compatibleObjectStores(admin.Storage) {
result[key] = value
}
disks := make([]map[string]any, 0, len(admin.DiskList))
@@ -240,7 +244,7 @@ func stringSliceValue(values map[string]any, key string, fallback []string) []st
return result
}
func applyGVARedis(target *conf.Data_Redis, values map[string]any) {
func applyCompatibleRedis(target *conf.Data_Redis, values map[string]any) {
if target == nil || values == nil {
return
}
@@ -257,14 +261,14 @@ func durationValue(raw string, fallback *durationpb.Duration) *durationpb.Durati
if raw == "" {
return fallback
}
value, err := parseGVADuration(raw)
value, err := parseCompatibleDuration(raw)
if err != nil {
return fallback
}
return durationpb.New(value)
}
func parseGVADuration(raw string) (time.Duration, error) {
func parseCompatibleDuration(raw string) (time.Duration, error) {
raw = strings.TrimSpace(raw)
if value, err := time.ParseDuration(raw); err == nil {
return value, nil
@@ -282,7 +286,7 @@ func parseGVADuration(raw string) (time.Duration, error) {
return time.Duration(nanoseconds), err
}
func applyGVADatabase(target *conf.Data_Database, values map[string]any, driver string) {
func applyCompatibleDatabase(target *conf.Data_Database, values map[string]any, driver string) {
if target == nil || values == nil {
return
}
@@ -304,7 +308,7 @@ func applyGVADatabase(target *conf.Data_Database, values map[string]any, driver
target.Singular = boolValue(values, "singular", target.Singular)
}
func applyGVAObjectStore(target *conf.AdminBackend_ObjectStore, values map[string]any) {
func applyCompatibleObjectStore(target *conf.AdminBackend_ObjectStore, values map[string]any) {
if target == nil || values == nil {
return
}
@@ -345,12 +349,12 @@ func applyGVAObjectStore(target *conf.AdminBackend_ObjectStore, values map[strin
target.AccountId = stringValue(values, "account-id")
}
func applyGVAConfiguration(raw json.RawMessage, data *conf.Data, admin *conf.AdminBackend) error {
func applyCompatibleConfiguration(raw json.RawMessage, data *conf.Data, admin *conf.AdminBackend) error {
var values map[string]any
if err := json.Unmarshal(raw, &values); err != nil {
return err
}
if _, isGVA := values["system"]; !isGVA {
if _, compatibleShape := values["system"]; !compatibleShape {
return nil
}
if data.Database == nil {
@@ -367,7 +371,7 @@ func applyGVAConfiguration(raw json.RawMessage, data *conf.Data, admin *conf.Adm
if driver == "" {
driver = data.Database.Driver
}
applyGVADatabase(data.Database, objectMap(values[driver]), driver)
applyCompatibleDatabase(data.Database, objectMap(values[driver]), driver)
admin.Storage.Type = stringValue(system, "oss-type")
admin.RouterPrefix = stringValue(system, "router-prefix")
admin.System.UseMultipoint = boolValue(system, "use-multipoint", admin.System.UseMultipoint)
@@ -411,7 +415,7 @@ func applyGVAConfiguration(raw json.RawMessage, data *conf.Data, admin *conf.Adm
if data.Redis == nil {
data.Redis = &conf.Data_Redis{}
}
applyGVARedis(data.Redis, objectMap(values["redis"]))
applyCompatibleRedis(data.Redis, objectMap(values["redis"]))
if items, ok := values["redis-list"].([]any); ok {
current := make(map[string]*conf.Data_Redis, len(data.RedisList))
for _, item := range data.RedisList {
@@ -426,7 +430,7 @@ func applyGVAConfiguration(raw json.RawMessage, data *conf.Data, admin *conf.Adm
if target == nil {
target = &conf.Data_Redis{}
}
applyGVARedis(target, values)
applyCompatibleRedis(target, values)
data.RedisList = append(data.RedisList, target)
}
}
@@ -467,7 +471,7 @@ func applyGVAConfiguration(raw json.RawMessage, data *conf.Data, admin *conf.Adm
if target == nil {
target = &conf.Data_Database{}
}
applyGVADatabase(target, row, stringValue(row, "type"))
applyCompatibleDatabase(target, row, stringValue(row, "type"))
target.AliasName, target.Disable = alias, boolValue(row, "disable", target.Disable)
data.DatabaseList = append(data.DatabaseList, target)
}
@@ -513,7 +517,7 @@ func applyGVAConfiguration(raw json.RawMessage, data *conf.Data, admin *conf.Adm
if *store.target == nil {
*store.target = &conf.AdminBackend_ObjectStore{}
}
applyGVAObjectStore(*store.target, objectMap(values[store.key]))
applyCompatibleObjectStore(*store.target, objectMap(values[store.key]))
}
if disks, ok := values["disk-list"].([]any); ok {
admin.DiskList = make([]*conf.AdminBackend_Disk, 0, len(disks))
+3 -5
View File
@@ -3,7 +3,6 @@ package data
import (
"context"
"encoding/json"
"errors"
"time"
"kra/internal/biz"
@@ -94,10 +93,9 @@ func (r *announcementRepo) UserOptions(ctx context.Context) ([]biz.UserOption, e
Label string
Value uint
}
err := r.data.gormDB.WithContext(ctx).Table("sys_users").Select("nick_name AS label, id AS value").Scan(&rows).Error
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
// The generated data-source endpoint is best effort: return collected
// options even when the underlying scan reports an error.
_ = r.data.gormDB.WithContext(ctx).Table("sys_users").Select("nick_name AS label, id AS value").Scan(&rows).Error
items := make([]biz.UserOption, 0, len(rows))
for _, row := range rows {
items = append(items, biz.UserOption{Label: row.Label, Value: row.Value})
+102 -56
View File
@@ -4,7 +4,6 @@ import (
"context"
"errors"
"strconv"
"strings"
"time"
"kra/internal/biz"
@@ -32,8 +31,12 @@ type apiPO struct {
func (apiPO) TableName() string { return "sys_apis" }
type ignoredAPIPO struct {
Path string `gorm:"primaryKey"`
Method string `gorm:"primaryKey"`
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
Path string
Method string `gorm:"default:POST"`
}
func (ignoredAPIPO) TableName() string { return "sys_ignore_apis" }
@@ -49,7 +52,7 @@ 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 *apiRepo) CreateAPI(ctx context.Context, v *biz.API) error {
po := apiPO{Path: v.Path, Description: v.Description, APIGroup: v.APIGroup, Method: strings.ToUpper(v.Method)}
po := apiPO{Path: v.Path, Description: v.Description, APIGroup: v.APIGroup, Method: v.Method}
var count int64
if err := r.data.gormDB.WithContext(ctx).Model(&apiPO{}).Where("path = ? AND method = ?", po.Path, po.Method).Count(&count).Error; err != nil {
return err
@@ -69,7 +72,7 @@ func (r *apiRepo) UpdateAPI(ctx context.Context, v *biz.API) error {
if err := db.First(&old, v.ID).Error; err != nil {
return err
}
method := strings.ToUpper(v.Method)
method := v.Method
if old.Path != v.Path || old.Method != method {
var count int64
if err := db.Model(&apiPO{}).Where("id <> ? AND path = ? AND method = ?", v.ID, v.Path, method).Count(&count).Error; err != nil {
@@ -79,10 +82,28 @@ func (r *apiRepo) UpdateAPI(ctx context.Context, v *biz.API) error {
return errors.New("存在相同api路径")
}
}
// Authorization is stored independently in casbin_rule and updates every
// matching policy when an API path or method changes.
if err := db.Model(&casbinRulePO{}).
Where("ptype = ? AND v1 = ? AND v2 = ?", "p", old.Path, old.Method).
Updates(map[string]any{"v1": v.Path, "v2": method}).Error; err != nil {
return err
}
return db.Model(&old).Updates(map[string]any{"path": v.Path, "description": v.Description, "api_group": v.APIGroup, "method": method}).Error
}
func (r *apiRepo) DeleteAPIs(ctx context.Context, ids []uint) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var apis []apiPO
if err := tx.Where("id IN ?", ids).Find(&apis).Error; err != nil {
return err
}
for _, api := range apis {
if err := deletePoliciesForPath(tx, api.Path, api.Method); err != nil {
return err
}
}
// Keep the legacy relation table clean for upgraded installations. It is
// no longer the authorization source.
if err := tx.Where("api_id IN ?", ids).Delete(&authorityAPIPO{}).Error; err != nil {
return err
}
@@ -106,7 +127,13 @@ func (r *apiRepo) ListAPIs(ctx context.Context, page, size int, q *biz.API) ([]*
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))
// Strict mode filters API metadata by the actor's persisted
// Casbin policies, matching on path/method rather than an API FK.
db = db.Where(`EXISTS (
SELECT 1 FROM casbin_rule cr
WHERE cr.ptype = 'p' AND cr.v0 = ?
AND cr.v1 = sys_apis.path AND cr.v2 = sys_apis.method
)`, strconv.FormatUint(uint64(actor.AuthorityID), 10))
}
}
}
@@ -121,7 +148,7 @@ func (r *apiRepo) ListAPIs(ctx context.Context, page, size int, q *biz.API) ([]*
db = db.Where("api_group = ?", q.APIGroup)
}
if q.Method != "" {
db = db.Where("method = ?", strings.ToUpper(q.Method))
db = db.Where("method = ?", q.Method)
}
}
var total int64
@@ -154,34 +181,42 @@ func (r *apiRepo) ListAPIs(ctx context.Context, page, size int, q *biz.API) ([]*
return out, total, nil
}
func (r *apiRepo) APIRoleIDs(ctx context.Context, path, method string) ([]uint, error) {
var ids []uint
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
rows := make([]casbinRulePO, 0)
err := policyScope(r.data.gormDB.WithContext(ctx)).
Where("v1 = ? AND v2 = ?", path, method).
Find(&rows).Error
if err != nil {
return nil, err
}
ids := make([]uint, 0, len(rows))
for _, row := range rows {
id, err := strconv.ParseUint(row.V0, 10, 64)
if err != nil {
continue
}
ids = append(ids, uint(id))
}
return ids, nil
}
func (r *apiRepo) SetAPIRoles(ctx context.Context, path, method string, ids []uint) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var api apiPO
if err := tx.Where("path = ? AND method = ?", path, strings.ToUpper(method)).First(&api).Error; err != nil {
// A path/method need not be present in sys_apis when replacing
// all matching policy rows. Do not validate or deduplicate authority IDs.
if err := deletePoliciesForPath(tx, path, method); err != nil {
return err
}
if err := tx.Where("api_id = ?", api.ID).Delete(&authorityAPIPO{}).Error; err != nil {
return err
if len(ids) == 0 {
return nil
}
links := make([]authorityAPIPO, 0, len(ids))
rules := make([]casbinRulePO, 0, len(ids))
for _, aid := range ids {
links = append(links, authorityAPIPO{AuthorityID: aid, APIID: api.ID})
rules = append(rules, newPolicyRule(aid, path, method))
}
if len(links) > 0 {
return tx.Create(&links).Error
}
return nil
return tx.Create(&rules).Error
})
}
func (r *apiRepo) Authorize(ctx context.Context, aid uint, path, method string) (bool, error) {
policies, err := r.PolicyPaths(ctx, aid)
rows, err := policyRowsForAuthority(r.data.gormDB.WithContext(ctx), aid)
if err != nil {
return false, err
}
@@ -192,7 +227,7 @@ p = sub, obj, act
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = r.sub == p.sub && keyMatch2(r.obj, p.obj) && regexMatch(r.act, p.act)`)
m = r.sub == p.sub && keyMatch2(r.obj, p.obj) && r.act == p.act`)
if err != nil {
return false, err
}
@@ -201,22 +236,21 @@ m = r.sub == p.sub && keyMatch2(r.obj, p.obj) && regexMatch(r.act, p.act)`)
return false, err
}
subject := strconv.FormatUint(uint64(aid), 10)
for _, policy := range policies {
if _, err := enforcer.AddPolicy(subject, policy.Path, strings.ToUpper(policy.Method)); err != nil {
for _, policy := range rows {
if _, err := enforcer.AddPolicy(subject, policy.V1, policy.V2); err != nil {
return false, err
}
}
return enforcer.Enforce(subject, path, strings.ToUpper(method))
return enforcer.Enforce(subject, path, method)
}
func (r *apiRepo) PolicyPaths(ctx context.Context, aid uint) ([]*biz.API, error) {
var pos []apiPO
err := r.data.gormDB.WithContext(ctx).Table("sys_apis").Joins("JOIN sys_authority_apis ON sys_authority_apis.api_id = sys_apis.id").Where("sys_authority_apis.authority_id = ? AND sys_apis.deleted_at IS NULL", aid).Order("sys_apis.path,sys_apis.method").Find(&pos).Error
rows, err := policyRowsForAuthority(r.data.gormDB.WithContext(ctx), aid)
if err != nil {
return nil, err
}
out := make([]*biz.API, 0, len(pos))
for _, po := range pos {
out = append(out, apiFromPO(po))
out := make([]*biz.API, 0, len(rows))
for _, row := range rows {
out = append(out, &biz.API{Path: row.V1, Method: row.V2})
}
return out, nil
}
@@ -237,35 +271,36 @@ func (r *apiRepo) SetPolicyPaths(ctx context.Context, aid uint, paths []*biz.API
}
allowedSet := make(map[string]bool, len(allowed))
for _, item := range allowed {
allowedSet[item.Path+"\x00"+strings.ToUpper(item.Method)] = true
allowedSet[item.Path+"\x00"+item.Method] = true
}
for _, item := range paths {
if !allowedSet[item.Path+"\x00"+strings.ToUpper(item.Method)] {
if !allowedSet[item.Path+"\x00"+item.Method] {
return errors.New("存在api不在权限列表中")
}
}
}
}
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := deletePoliciesForAuthority(tx, aid); err != nil {
return err
}
// Keep the legacy relation table clean for upgraded installations. It
// is not consulted for authorization anymore.
if err := tx.Where("authority_id = ?", aid).Delete(&authorityAPIPO{}).Error; err != nil {
return err
}
links := make([]authorityAPIPO, 0, len(paths))
rules := make([]casbinRulePO, 0, len(paths))
seen := make(map[string]struct{}, len(paths))
for _, path := range paths {
var po apiPO
err := tx.Where("path = ? AND method = ?", path.Path, strings.ToUpper(path.Method)).First(&po).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
po = apiPO{Path: path.Path, Method: strings.ToUpper(path.Method), Description: path.Description, APIGroup: path.APIGroup}
if err = tx.Create(&po).Error; err != nil {
return err
}
} else if err != nil {
return err
key := path.Path + "\x00" + path.Method
if _, ok := seen[key]; ok {
continue
}
links = append(links, authorityAPIPO{AuthorityID: aid, APIID: po.ID})
seen[key] = struct{}{}
rules = append(rules, newPolicyRule(aid, path.Path, path.Method))
}
if len(links) > 0 {
return tx.Create(&links).Error
if len(rules) > 0 {
return tx.Create(&rules).Error
}
return nil
})
@@ -273,7 +308,7 @@ func (r *apiRepo) SetPolicyPaths(ctx context.Context, aid uint, paths []*biz.API
func (r *apiRepo) IgnoredAPIs(ctx context.Context) ([]*biz.API, error) {
var pos []ignoredAPIPO
if err := r.data.gormDB.WithContext(ctx).Order("path,method").Find(&pos).Error; err != nil {
if err := r.data.gormDB.WithContext(ctx).Find(&pos).Error; err != nil {
return nil, err
}
out := make([]*biz.API, 0, len(pos))
@@ -283,26 +318,37 @@ func (r *apiRepo) IgnoredAPIs(ctx context.Context) ([]*biz.API, error) {
return out, nil
}
func (r *apiRepo) SetAPIIgnored(ctx context.Context, path, method string, ignored bool) error {
po := ignoredAPIPO{Path: path, Method: strings.ToUpper(method)}
po := ignoredAPIPO{Path: path, Method: method}
if ignored {
return r.data.gormDB.WithContext(ctx).FirstOrCreate(&po, po).Error
// The compatible endpoint creates an ignore row on every request (the table has no
// path/method uniqueness constraint); retain that observable behavior.
return r.data.gormDB.WithContext(ctx).Create(&po).Error
}
return r.data.gormDB.WithContext(ctx).Where("path = ? AND method = ?", po.Path, po.Method).Delete(&ignoredAPIPO{}).Error
return r.data.gormDB.WithContext(ctx).Unscoped().Where("path = ? AND method = ?", po.Path, po.Method).Delete(&ignoredAPIPO{}).Error
}
func (r *apiRepo) ApplyAPISync(ctx context.Context, added, deleted []*biz.API) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
for _, item := range added {
po := apiPO{Path: item.Path, Method: strings.ToUpper(item.Method), Description: item.Description, APIGroup: item.APIGroup}
if err := tx.Where("path = ? AND method = ?", po.Path, po.Method).FirstOrCreate(&po).Error; err != nil {
if len(added) > 0 {
pos := make([]apiPO, 0, len(added))
for _, item := range added {
pos = append(pos, apiPO{Path: item.Path, Method: item.Method, Description: item.Description, APIGroup: item.APIGroup})
}
// Insert the submitted newApis slice as-is. Do not silently
// collapse repeated/existing path+method pairs here; sys_apis itself
// intentionally has no uniqueness constraint.
if err := tx.Create(&pos).Error; err != nil {
return err
}
}
for _, item := range deleted {
var ids []uint
if err := tx.Model(&apiPO{}).Where("path = ? AND method = ?", item.Path, strings.ToUpper(item.Method)).Pluck("id", &ids).Error; err != nil {
if err := tx.Model(&apiPO{}).Where("path = ? AND method = ?", item.Path, item.Method).Pluck("id", &ids).Error; err != nil {
return err
}
if len(ids) > 0 {
if err := deletePoliciesForPath(tx, item.Path, item.Method); err != nil {
return err
}
if err := tx.Where("api_id IN ?", ids).Delete(&authorityAPIPO{}).Error; err != nil {
return err
}
+25 -27
View File
@@ -42,10 +42,16 @@ func (jwtBlacklistPO) TableName() string { return "jwt_blacklists" }
func (r *apiTokenRepo) UserHasAuthority(ctx context.Context, userID, authorityID uint) (*biz.User, bool, error) {
var po userPO
if err := r.data.gormDB.WithContext(ctx).First(&po, userID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, false, errors.New("用户不存在")
}
return nil, false, err
}
var count int64
err := r.data.gormDB.WithContext(ctx).Model(&userAuthorityPO{}).Where("sys_user_id = ? AND sys_authority_authority_id = ?", userID, authorityID).Count(&count).Error
err := r.data.gormDB.WithContext(ctx).Model(&userAuthorityPO{}).
Joins("JOIN sys_authorities ON sys_authorities.authority_id = sys_user_authority.sys_authority_authority_id").
Where("sys_user_authority.sys_user_id = ? AND sys_user_authority.sys_authority_authority_id = ? AND sys_authorities.deleted_at IS NULL", userID, authorityID).
Count(&count).Error
if err != nil {
return nil, false, err
}
@@ -106,22 +112,20 @@ func (r *apiTokenRepo) DisableAPIToken(ctx context.Context, id uint) (string, er
return po.Token, r.data.gormDB.WithContext(ctx).Model(&po).Update("status", false).Error
}
func (r *apiTokenRepo) 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
})
// Persist the blacklist row first, then mark the
// API-token record disabled. The two writes are intentionally separate in
// the reference implementation, so a storage error has the same observable
// result here.
var po apiTokenPO
if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil {
return err
}
if err := r.BlacklistToken(ctx, po.Token); err != nil {
return err
}
return r.data.gormDB.WithContext(ctx).Model(&po).Update("status", false).Error
}
func (r *apiTokenRepo) BlacklistToken(ctx context.Context, token string) error {
if token == "" {
return errors.New("token is empty")
}
return r.data.gormDB.WithContext(ctx).Create(&jwtBlacklistPO{JWT: token}).Error
}
func (r *apiTokenRepo) IsTokenDisabled(ctx context.Context, token string) (bool, error) {
@@ -129,16 +133,10 @@ func (r *apiTokenRepo) IsTokenDisabled(ctx context.Context, token string) (bool,
if err := r.data.gormDB.WithContext(ctx).Model(&jwtBlacklistPO{}).Where("jwt = ?", token).Count(&blacklistCount).Error; err != nil {
return false, err
}
if blacklistCount > 0 {
return true, nil
}
var po apiTokenPO
result := r.data.gormDB.WithContext(ctx).Where("token = ?", token).Limit(1).Find(&po)
if result.Error != nil {
return false, result.Error
}
if result.RowsAffected == 0 {
return false, nil
}
return !po.Status || time.Now().After(po.ExpiresAt), nil
// The JWT middleware consults only jwt_blacklists and lets JWT parsing
// report expiry. API-token deletion writes this row before changing the
// token status, so consulting status/ExpiresAt here would produce a
// different error message and would make manually-disabled tokens behave
// unlike tokens issued through the regular login flow.
return blacklistCount > 0, nil
}
+63 -29
View File
@@ -3,8 +3,6 @@ package data
import (
"context"
"errors"
"strconv"
"strings"
"kra/internal/biz"
@@ -89,9 +87,11 @@ func (r *authorityAccessRepo) CreateAuthority(ctx context.Context, value *biz.Au
if value.DefaultRouter == "" {
value.DefaultRouter = "dashboard"
}
if err := tx.Create(&authorityPO{AuthorityID: value.AuthorityID, AuthorityName: value.AuthorityName, ParentID: value.ParentID, DataScope: value.DataScope, DefaultRouter: value.DefaultRouter}).Error; err != nil {
po := authorityPO{AuthorityID: value.AuthorityID, AuthorityName: value.AuthorityName, ParentID: value.ParentID, DataScope: value.DataScope, DefaultRouter: value.DefaultRouter}
if err := tx.Create(&po).Error; err != nil {
return err
}
value.CreatedAt, value.UpdatedAt = po.CreatedAt, po.UpdatedAt
var dashboard menuPO
if err := tx.Where("name = ?", "dashboard").First(&dashboard).Error; err == nil {
if err = tx.Create(&authorityMenuPO{SysAuthorityAuthorityID: value.AuthorityID, SysBaseMenuID: dashboard.ID}).Error; err != nil {
@@ -99,13 +99,12 @@ func (r *authorityAccessRepo) CreateAuthority(ctx context.Context, value *biz.Au
}
}
defaults := []struct{ path, method string }{{"/menu/getMenu", "POST"}, {"/jwt/jsonInBlacklist", "POST"}, {"/base/login", "POST"}, {"/user/changePassword", "POST"}, {"/user/setUserAuthority", "POST"}, {"/user/getUserInfo", "GET"}, {"/user/setSelfInfo", "PUT"}, {"/fileUploadAndDownload/upload", "POST"}, {"/sysDictionary/findSysDictionary", "GET"}}
rules := make([]casbinRulePO, 0, len(defaults))
for _, item := range defaults {
var api apiPO
if err := tx.Where("path = ? AND method = ?", item.path, item.method).First(&api).Error; err == nil {
if err = tx.Create(&authorityAPIPO{AuthorityID: value.AuthorityID, APIID: api.ID}).Error; err != nil {
return err
}
}
rules = append(rules, newPolicyRule(value.AuthorityID, item.path, item.method))
}
if err := tx.Create(&rules).Error; err != nil {
return err
}
return nil
})
@@ -114,13 +113,18 @@ func (r *authorityAccessRepo) CopyAuthority(ctx context.Context, sourceID uint,
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
// Strict-authority checks apply to every actor. A root actor is
// allowed to create/copy a role directly below itself; non-root actors
// may target themselves' descendants. Do not special-case 888 here:
// the configured strict-auth switch is the source of truth.
strict := hasActor && 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] {
parentAllowed := value.ParentID != nil && (allowed[*value.ParentID] || *value.ParentID == actor.AuthorityID)
if !parentAllowed {
return errors.New("严格角色模式下只能在自己的角色树中复制角色")
}
}
@@ -131,9 +135,11 @@ func (r *authorityAccessRepo) CopyAuthority(ctx context.Context, sourceID uint,
if count != 0 {
return errors.New("存在相同角色id")
}
if err := tx.Create(&authorityPO{AuthorityID: value.AuthorityID, AuthorityName: value.AuthorityName, ParentID: value.ParentID, DataScope: value.DataScope, DefaultRouter: value.DefaultRouter}).Error; err != nil {
po := authorityPO{AuthorityID: value.AuthorityID, AuthorityName: value.AuthorityName, ParentID: value.ParentID, DataScope: value.DataScope, DefaultRouter: value.DefaultRouter}
if err := tx.Create(&po).Error; err != nil {
return err
}
value.CreatedAt, value.UpdatedAt = po.CreatedAt, po.UpdatedAt
copyLinks := func(table string, destination any, columns map[string]any) error {
return tx.Table(table).Where(columns).Find(destination).Error
}
@@ -149,24 +155,31 @@ func (r *authorityAccessRepo) CopyAuthority(ctx context.Context, sourceID uint,
return err
}
}
var apis []authorityAPIPO
if err := copyLinks("sys_authority_apis", &apis, map[string]any{"authority_id": sourceID}); err != nil {
apis, err := policyRowsForAuthority(tx, sourceID)
if err != nil {
return err
}
for i := range apis {
copiedRules := make([]casbinRulePO, 0, len(apis))
seenPolicies := make(map[string]struct{}, len(apis))
for _, api := 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 {
allowed, err := policyExists(tx, actor.AuthorityID, api.V1, api.V2)
if err != nil {
return err
}
if allowedCount == 0 {
if !allowed {
return errors.New("严格角色模式下不能复制当前角色未拥有的 API")
}
}
apis[i].AuthorityID = value.AuthorityID
key := api.V1 + "\x00" + api.V2
if _, ok := seenPolicies[key]; ok {
continue
}
seenPolicies[key] = struct{}{}
copiedRules = append(copiedRules, newPolicyRule(value.AuthorityID, api.V1, api.V2))
}
if len(apis) > 0 {
if err := tx.Create(&apis).Error; err != nil {
if len(copiedRules) > 0 {
if err := tx.Create(&copiedRules).Error; err != nil {
return err
}
}
@@ -225,13 +238,18 @@ func (r *authorityAccessRepo) DeleteAuthority(ctx context.Context, id uint) erro
if err := tx.Where("sys_authority_authority_id = ?", id).Delete(&authorityMenuPO{}).Error; err != nil {
return err
}
if err := deletePoliciesForAuthority(tx, id); err != nil {
return err
}
if err := tx.Where("authority_id = ?", id).Delete(&authorityAPIPO{}).Error; err != nil {
return err
}
if err := tx.Where("authority_id = ?", id).Delete(&authorityButtonPO{}).Error; err != nil {
return err
}
return tx.Delete(&authorityPO{}, "authority_id = ?", id).Error
// The compatible delete removes the role row permanently with Unscoped().Delete,
// so reusing an authority ID after deletion behaves the same way.
return tx.Unscoped().Delete(&authorityPO{}, "authority_id = ?", id).Error
})
}
func (r *authorityAccessRepo) ListAuthorities(ctx context.Context) ([]*biz.Authority, error) {
@@ -359,19 +377,35 @@ func (r *authorityAccessRepo) ResolveDataScope(ctx context.Context, authorityID,
ids = append(ids, id)
}
if identity.Scope == 2 && len(ids) > 0 {
// Match the administration subtree-union semantics: derive the visible set from
// parent_id edges at read time, rather than trusting the denormalized
// ancestors string (which may be stale after a department move).
var departments []departmentPO
_ = r.data.gormDB.WithContext(ctx).Find(&departments).Error
_ = r.data.gormDB.WithContext(ctx).Select("id", "parent_id").Find(&departments).Error
children := make(map[uint][]uint, len(departments))
for _, department := range departments {
for _, part := range strings.Split(department.Ancestors, ",") {
value, _ := strconv.ParseUint(part, 10, 64)
if selected[uint(value)] {
selected[department.ID] = true
break
children[department.ParentID] = append(children[department.ParentID], department.ID)
}
queue := make([]uint, 0, len(selected))
for id := range selected {
queue = append(queue, id)
}
visited := make(map[uint]struct{}, len(selected))
for len(queue) > 0 {
current := queue[0]
queue = queue[1:]
if _, ok := visited[current]; ok {
continue
}
visited[current] = struct{}{}
for _, child := range children[current] {
if _, ok := visited[child]; !ok {
queue = append(queue, child)
}
}
}
ids = ids[:0]
for id := range selected {
for id := range visited {
ids = append(ids, id)
}
} else if identity.Scope == 5 {
+60
View File
@@ -0,0 +1,60 @@
package data
import (
"strconv"
"gorm.io/gorm"
)
// casbinRulePO mirrors the gorm-adapter CasbinRule storage shape. API
// metadata (sys_apis) and authorization policy are intentionally independent:
// a policy may refer to a route that has not been registered in sys_apis yet.
// Keep this type private so the storage representation does not leak above
// the data layer.
type casbinRulePO struct {
ID uint `gorm:"primaryKey;autoIncrement"`
Ptype string `gorm:"size:100"`
V0 string `gorm:"size:100"`
V1 string `gorm:"size:100"`
V2 string `gorm:"size:100"`
V3 string `gorm:"size:100"`
V4 string `gorm:"size:100"`
V5 string `gorm:"size:100"`
}
func (casbinRulePO) TableName() string { return "casbin_rule" }
func newPolicyRule(authorityID uint, path, method string) casbinRulePO {
return casbinRulePO{
Ptype: "p",
V0: strconv.FormatUint(uint64(authorityID), 10),
V1: path,
V2: method,
}
}
func policyScope(db *gorm.DB) *gorm.DB {
return db.Where("ptype = ?", "p")
}
func deletePoliciesForAuthority(db *gorm.DB, authorityID uint) error {
return policyScope(db).Where("v0 = ?", strconv.FormatUint(uint64(authorityID), 10)).Delete(&casbinRulePO{}).Error
}
func deletePoliciesForPath(db *gorm.DB, path, method string) error {
return policyScope(db).Where("v1 = ? AND v2 = ?", path, method).Delete(&casbinRulePO{}).Error
}
func policyRowsForAuthority(db *gorm.DB, authorityID uint) ([]casbinRulePO, error) {
var rows []casbinRulePO
err := policyScope(db).Where("v0 = ?", strconv.FormatUint(uint64(authorityID), 10)).Find(&rows).Error
return rows, err
}
func policyExists(db *gorm.DB, authorityID uint, path, method string) (bool, error) {
var count int64
err := policyScope(db).
Where("v0 = ? AND v1 = ? AND v2 = ?", strconv.FormatUint(uint64(authorityID), 10), path, method).
Count(&count).Error
return count > 0, err
}
+156 -74
View File
@@ -3,6 +3,7 @@ package data
import (
"context"
"encoding/json"
"strings"
"kra/internal/conf"
@@ -33,7 +34,16 @@ func (r *initializationRepo) ConfigurationJSON() (json.RawMessage, error) {
if adminConfig != nil {
admin["routerPrefix"] = adminConfig.RouterPrefix
if adminConfig.System != nil {
admin["system"] = map[string]any{"useRedis": adminConfig.System.UseRedis, "useMultipoint": adminConfig.System.UseMultipoint, "useStrictAuth": adminConfig.System.UseStrictAuth, "disableAutoMigrate": adminConfig.System.DisableAutoMigrate, "useMongo": adminConfig.System.UseMongo}
admin["system"] = map[string]any{
"useRedis": adminConfig.System.UseRedis,
"useMultipoint": adminConfig.System.UseMultipoint,
"useStrictAuth": adminConfig.System.UseStrictAuth,
"disableAutoMigrate": adminConfig.System.DisableAutoMigrate,
"useMongo": adminConfig.System.UseMongo,
"addr": adminConfig.System.Addr,
"iplimitCount": adminConfig.System.IplimitCount,
"iplimitTime": adminConfig.System.IplimitTime,
}
}
if adminConfig.Jwt != nil {
admin["jwt"] = map[string]any{"signingKey": "******", "expiresTime": durationString(adminConfig.Jwt.ExpiresTime), "bufferTime": durationString(adminConfig.Jwt.BufferTime), "issuer": adminConfig.Jwt.Issuer}
@@ -45,7 +55,11 @@ func (r *initializationRepo) ConfigurationJSON() (json.RawMessage, error) {
admin["local"] = map[string]any{"storePath": adminConfig.Local.StorePath, "pathPrefix": adminConfig.Local.PathPrefix}
}
if adminConfig.Media != nil {
admin["media"] = map[string]any{"sessionTtl": adminConfig.Media.SessionTtl, "maxFileSize": adminConfig.Media.MaxFileSize}
chunkDir := adminConfig.Media.ChunkDir
if chunkDir == "" {
chunkDir = "uploads/chunks"
}
admin["media"] = map[string]any{"sessionTtl": adminConfig.Media.SessionTtl, "maxFileSize": adminConfig.Media.MaxFileSize, "chunkDir": chunkDir}
}
if adminConfig.Email != nil {
email = map[string]any{"to": adminConfig.Email.To, "from": adminConfig.Email.From, "host": adminConfig.Email.Host, "secret": "******", "nickname": adminConfig.Email.Nickname, "port": adminConfig.Email.Port, "is-ssl": adminConfig.Email.IsSsl, "is-loginauth": adminConfig.Email.IsLoginAuth}
@@ -55,6 +69,13 @@ func (r *initializationRepo) ConfigurationJSON() (json.RawMessage, error) {
maskStorageSecrets(storage)
admin["storage"] = storage
}
disks := make([]map[string]any, 0, len(adminConfig.DiskList))
for _, disk := range adminConfig.DiskList {
if disk != nil {
disks = append(disks, map[string]any{"mount_point": disk.MountPoint})
}
}
admin["disk_list"] = disks
if adminConfig.Zap != nil {
admin["zap"] = adminConfig.Zap
}
@@ -65,7 +86,12 @@ func (r *initializationRepo) ConfigurationJSON() (json.RawMessage, error) {
admin["app"] = adminConfig.App
}
}
maskDataSecrets(dataConfig)
// Never mask secrets on the live runtime object. ConfigurationJSON is a
// read-only operation; mutating dataConfig here would replace the actual
// database/Redis/Mongo credentials with "******" and make the next reload
// fail. Work on a clone, just as we already do for adminConfig.
safeData := cloneDataConfig(dataConfig)
maskDataSecrets(safeData)
safeAdmin := cloneAdminConfig(adminConfig)
if safeAdmin != nil {
if safeAdmin.Jwt != nil {
@@ -77,8 +103,8 @@ func (r *initializationRepo) ConfigurationJSON() (json.RawMessage, error) {
maskStorageSecrets(safeAdmin.Storage)
}
dataMap := map[string]any{}
if dataConfig != nil {
raw, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(dataConfig)
if safeData != nil {
raw, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(safeData)
if err != nil {
return nil, err
}
@@ -86,7 +112,7 @@ func (r *initializationRepo) ConfigurationJSON() (json.RawMessage, error) {
return nil, err
}
}
config := gvaConfiguration(dataConfig, safeAdmin)
config := compatibleConfiguration(safeData, safeAdmin)
config["admin"], config["email"], config["data"] = admin, email, dataMap
return json.Marshal(map[string]any{"config": config})
}
@@ -98,7 +124,7 @@ func (r *initializationRepo) SaveConfigurationJSON(ctx context.Context, raw json
}
nextData := cloneDataConfig(currentData)
nextAdmin := proto.Clone(currentAdmin).(*conf.AdminBackend)
if err := applyGVAConfiguration(raw, nextData, nextAdmin); err != nil {
if err := applyCompatibleConfiguration(raw, nextData, nextAdmin); err != nil {
return err
}
var value configurationEnvelope
@@ -107,18 +133,14 @@ func (r *initializationRepo) SaveConfigurationJSON(ctx context.Context, raw json
}
options := protojson.UnmarshalOptions{DiscardUnknown: true}
if len(value.Data) > 0 && string(value.Data) != "null" {
patch := &conf.Data{}
if err := options.Unmarshal(value.Data, patch); err != nil {
if err := mergeConfigurationMessage(nextData, value.Data, options); err != nil {
return err
}
applyDataPatch(nextData, patch)
}
if len(value.Admin) > 0 && string(value.Admin) != "null" {
patch := &conf.AdminBackend{}
if err := options.Unmarshal(value.Admin, patch); err != nil {
if err := mergeConfigurationMessage(nextAdmin, value.Admin, options); err != nil {
return err
}
applyAdminPatch(nextAdmin, patch)
}
preserveDataSecrets(nextData, currentData)
preserveAdminSecrets(nextAdmin, currentAdmin)
@@ -216,6 +238,80 @@ func refreshDatabaseSource(database *conf.Data_Database) error {
return nil
}
// mergeConfigurationMessage applies a JSON object as a partial update while
// retaining fields that were not sent by the caller. protojson.Unmarshal is
// intentionally not used directly here because its reset semantics would
// clear unrelated configuration sections when the management page submits a
// partial object.
func mergeConfigurationMessage(target proto.Message, patch json.RawMessage, options protojson.UnmarshalOptions) error {
currentRaw, err := protojson.MarshalOptions{UseProtoNames: false}.Marshal(target)
if err != nil {
return err
}
var current map[string]any
if err = json.Unmarshal(currentRaw, &current); err != nil {
return err
}
var incoming map[string]any
if err = json.Unmarshal(patch, &incoming); err != nil {
return err
}
incoming = normalizeJSONKeys(incoming).(map[string]any)
mergeJSONObjects(current, incoming)
merged, err := json.Marshal(current)
if err != nil {
return err
}
return options.Unmarshal(merged, target)
}
func normalizeJSONKeys(value any) any {
switch item := value.(type) {
case map[string]any:
result := make(map[string]any, len(item))
for key, nested := range item {
result[snakeToLowerCamel(key)] = normalizeJSONKeys(nested)
}
return result
case []any:
result := make([]any, len(item))
for index, nested := range item {
result[index] = normalizeJSONKeys(nested)
}
return result
default:
return value
}
}
func snakeToLowerCamel(value string) string {
if !strings.Contains(value, "_") {
return value
}
parts := strings.Split(value, "_")
result := parts[0]
for _, part := range parts[1:] {
if part == "" {
continue
}
result += strings.ToUpper(part[:1]) + part[1:]
}
return result
}
func mergeJSONObjects(target, patch map[string]any) {
for key, value := range patch {
incoming, isObject := value.(map[string]any)
if isObject {
if existing, ok := target[key].(map[string]any); ok {
mergeJSONObjects(existing, incoming)
continue
}
}
target[key] = value
}
}
func durationString(value *durationpb.Duration) string {
if value == nil {
return "0s"
@@ -223,69 +319,17 @@ func durationString(value *durationpb.Duration) string {
return value.AsDuration().String()
}
func applyDataPatch(target, patch *conf.Data) {
if patch.Database != nil {
target.Database = patch.Database
}
if patch.Redis != nil {
target.Redis = patch.Redis
}
if patch.Mongo != nil {
target.Mongo = patch.Mongo
}
if patch.DatabaseList != nil {
target.DatabaseList = patch.DatabaseList
}
if patch.RedisList != nil {
target.RedisList = patch.RedisList
}
}
func applyAdminPatch(target, patch *conf.AdminBackend) {
target.RouterPrefix = patch.RouterPrefix
if patch.System != nil {
if target.System != nil {
patch.System.Addr = target.System.Addr
patch.System.IplimitCount = target.System.IplimitCount
patch.System.IplimitTime = target.System.IplimitTime
}
target.System = patch.System
}
if patch.Jwt != nil {
target.Jwt = patch.Jwt
}
if patch.Captcha != nil {
target.Captcha = patch.Captcha
}
if patch.Local != nil {
target.Local = patch.Local
}
if patch.Media != nil {
if patch.Media.ChunkDir == "" && target.Media != nil {
patch.Media.ChunkDir = target.Media.ChunkDir
}
target.Media = patch.Media
}
if patch.Storage != nil {
target.Storage = patch.Storage
}
if patch.Zap != nil {
target.Zap = patch.Zap
}
if patch.Cors != nil {
target.Cors = patch.Cors
}
if patch.App != nil {
target.App = patch.App
}
}
func maskDataSecrets(value *conf.Data) {
if value == nil {
return
}
if value.Database != nil {
value.Database.Password = "******"
// Source may contain an embedded password (for example a MySQL DSN).
// The management response must never expose it. Structured database
// fields are retained and refreshDatabaseSources rebuilds Source when
// the configuration is saved.
value.Database.Source = ""
}
if value.Redis != nil {
value.Redis.Password = "******"
@@ -296,6 +340,7 @@ func maskDataSecrets(value *conf.Data) {
for _, item := range value.DatabaseList {
if item != nil {
item.Password = "******"
item.Source = ""
}
}
for _, item := range value.RedisList {
@@ -312,20 +357,54 @@ func preserveDataSecrets(next, current *conf.Data) {
if next.Database != nil && current.Database != nil && maskedSecret(next.Database.Password) {
next.Database.Password = current.Database.Password
}
if next.Database != nil && current.Database != nil && next.Database.Source == "" {
next.Database.Source = current.Database.Source
}
if next.Redis != nil && current.Redis != nil && maskedSecret(next.Redis.Password) {
next.Redis.Password = current.Redis.Password
}
if next.Mongo != nil && current.Mongo != nil && maskedSecret(next.Mongo.Password) {
next.Mongo.Password = current.Mongo.Password
}
currentDatabases := make(map[string]*conf.Data_Database, len(current.DatabaseList))
for _, item := range current.DatabaseList {
if item != nil && item.AliasName != "" {
currentDatabases[item.AliasName] = item
}
}
for i, item := range next.DatabaseList {
if item != nil && i < len(current.DatabaseList) && current.DatabaseList[i] != nil && maskedSecret(item.Password) {
item.Password = current.DatabaseList[i].Password
var previous *conf.Data_Database
if item != nil && item.AliasName != "" {
previous = currentDatabases[item.AliasName]
}
if previous == nil && i < len(current.DatabaseList) {
previous = current.DatabaseList[i]
}
if item != nil && previous != nil {
if maskedSecret(item.Password) {
item.Password = previous.Password
}
if item.Source == "" {
item.Source = previous.Source
}
}
}
currentRedis := make(map[string]*conf.Data_Redis, len(current.RedisList))
for _, item := range current.RedisList {
if item != nil && item.Name != "" {
currentRedis[item.Name] = item
}
}
for i, item := range next.RedisList {
if item != nil && i < len(current.RedisList) && current.RedisList[i] != nil && maskedSecret(item.Password) {
item.Password = current.RedisList[i].Password
var previous *conf.Data_Redis
if item != nil && item.Name != "" {
previous = currentRedis[item.Name]
}
if previous == nil && i < len(current.RedisList) {
previous = current.RedisList[i]
}
if item != nil && previous != nil && maskedSecret(item.Password) {
item.Password = previous.Password
}
}
}
@@ -350,6 +429,9 @@ func objectStores(storage *conf.AdminBackend_Storage) []*conf.AdminBackend_Objec
}
func maskStorageSecrets(storage *conf.AdminBackend_Storage) {
if storage == nil {
return
}
if storage.Qiniu != nil {
storage.Qiniu.SecretKey = "******"
}
+2 -5
View File
@@ -26,7 +26,7 @@ func (r *auditRecorderRepo) RecordDataAccess(ctx context.Context, v *biz.DataAcc
return r.data.gormDB.WithContext(ctx).Create(&dataAccessLogPO{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}).Error
}
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}
return &biz.DataAccessLog{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, 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 *auditQueryRepo) ListDataAccess(ctx context.Context, page, size int, q *biz.DataAccessLog) ([]*biz.DataAccessLog, int64, error) {
db := r.data.gormDB.WithContext(ctx).Model(&dataAccessLogPO{})
@@ -37,9 +37,6 @@ func (r *auditQueryRepo) ListDataAccess(ctx context.Context, page, size int, q *
if q.TargetTable != "" {
db = db.Where("target_table LIKE ?", "%"+q.TargetTable+"%")
}
if q.UserID != 0 {
db = db.Where("user_id = ?", q.UserID)
}
}
var total int64
if err := db.Count(&total).Error; err != nil {
@@ -56,5 +53,5 @@ func (r *auditQueryRepo) ListDataAccess(ctx context.Context, page, size int, q *
return out, total, nil
}
func (r *auditQueryRepo) DeleteDataAccess(ctx context.Context, ids []uint) error {
return r.data.gormDB.WithContext(ctx).Delete(&dataAccessLogPO{}, ids).Error
return r.data.gormDB.WithContext(ctx).Delete(&dataAccessLogPO{}, "id IN ?", ids).Error
}
+12
View File
@@ -77,6 +77,9 @@ func (r *departmentRepo) CreateDepartment(ctx context.Context, v *biz.Department
if v.ParentID != 0 {
var parent departmentPO
if err := r.data.gormDB.WithContext(ctx).First(&parent, v.ParentID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("父部门不存在")
}
return err
}
v.Ancestors = strings.Trim(parent.Ancestors+","+strconv.FormatUint(uint64(parent.ID), 10), ",")
@@ -91,6 +94,9 @@ func (r *departmentRepo) UpdateDepartment(ctx context.Context, v *biz.Department
if v.ParentID != 0 {
var parent departmentPO
if err := r.data.gormDB.WithContext(ctx).First(&parent, v.ParentID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("父部门不存在")
}
return err
}
v.Ancestors = strings.Trim(parent.Ancestors+","+strconv.FormatUint(uint64(parent.ID), 10), ",")
@@ -108,6 +114,12 @@ func (r *departmentRepo) DeleteDepartment(ctx context.Context, id uint) error {
if count > 0 {
return errors.New("存在子部门,不允许删除")
}
if err := r.data.gormDB.WithContext(ctx).Model(&userPO{}).Where("dept_id = ?", id).Count(&count).Error; err != nil {
return err
}
if count > 0 {
return errors.New("该部门下存在用户,不允许删除")
}
if err := r.data.gormDB.WithContext(ctx).Model(&userDepartmentPO{}).Where("sys_department_id = ?", id).Count(&count).Error; err != nil {
return err
}
+80 -15
View File
@@ -23,7 +23,7 @@ type dictionaryPO struct {
DeletedAt gorm.DeletedAt `gorm:"index"`
Name string
Type string
Status bool
Status *bool
Desc string
ParentID *uint
}
@@ -38,7 +38,7 @@ type dictionaryDetailPO struct {
Label string
Value string
Extend string
Status bool
Status *bool
Sort int
DictionaryID uint `gorm:"column:sys_dictionary_id;index"`
ParentID *uint
@@ -59,11 +59,18 @@ func parameterFromPO(po parameterPO) *biz.SystemParameter {
}
func (r *dictionaryRepo) CreateDictionary(ctx context.Context, v *biz.Dictionary) error {
var existing dictionaryPO
if err := r.data.gormDB.WithContext(ctx).Where("type = ?", v.Type).First(&existing).Error; err == nil {
return errors.New("存在相同的type,不允许创建")
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
po := dictionaryPO{Name: v.Name, Type: v.Type, Status: v.Status, Desc: v.Desc, ParentID: v.ParentID}
if err := r.data.gormDB.WithContext(ctx).Create(&po).Error; err != nil {
return err
}
v.ID = po.ID
v.CreatedAt, v.UpdatedAt = po.CreatedAt, po.UpdatedAt
return nil
}
@@ -108,7 +115,46 @@ func (r *dictionaryRepo) ImportDictionary(ctx context.Context, dictionary *biz.D
})
}
func (r *dictionaryRepo) 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
db := r.data.gormDB.WithContext(ctx)
var current dictionaryPO
if err := db.Where("id = ?", v.ID).First(&current).Error; err != nil {
return errors.New("查询字典数据失败")
}
if current.Type != v.Type {
var duplicate dictionaryPO
if err := db.Where("type = ?", v.Type).First(&duplicate).Error; err == nil {
return errors.New("存在相同的type,不允许创建")
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
}
if v.ParentID != nil && *v.ParentID != 0 {
if err := r.checkDictionaryCircularReference(ctx, v.ID, *v.ParentID); err != nil {
return err
}
}
return db.Model(&current).Updates(map[string]any{"name": v.Name, "type": v.Type, "status": v.Status, "desc": v.Desc, "parent_id": v.ParentID}).Error
}
// checkDictionaryCircularReference mirrors the compatible recursive parent-chain
// check. A missing parent is allowed (the reference implementation permits a
// dangling parent ID); only a self-reference or an ancestor cycle is rejected.
func (r *dictionaryRepo) checkDictionaryCircularReference(ctx context.Context, currentID, parentID uint) error {
if currentID == parentID {
return errors.New("不能将字典设置为自己的父级")
}
var parent dictionaryPO
err := r.data.gormDB.WithContext(ctx).Where("id = ?", parentID).First(&parent).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil
}
if err != nil {
return err
}
if parent.ParentID != nil && *parent.ParentID != 0 {
return r.checkDictionaryCircularReference(ctx, currentID, *parent.ParentID)
}
return nil
}
func (r *dictionaryRepo) DeleteDictionary(ctx context.Context, id uint) error {
db := r.data.gormDB.WithContext(ctx)
@@ -141,7 +187,7 @@ func (r *dictionaryRepo) FindDictionary(ctx context.Context, id uint, typ string
if err == nil {
filtered := out.Details[:0]
for _, item := range out.Details {
if item.Status {
if item.Status != nil && *item.Status {
filtered = append(filtered, item)
}
}
@@ -226,7 +272,7 @@ func (r *dictionaryRepo) CreateDictionaryDetail(ctx context.Context, v *biz.Dict
po := dictionaryDetailPO{Label: v.Label, Value: v.Value, Extend: v.Extend, Status: v.Status, Sort: v.Sort, DictionaryID: v.DictionaryID, ParentID: v.ParentID}
po.Level = 0
po.Path = ""
if v.ParentID != nil && *v.ParentID != 0 {
if v.ParentID != nil {
var parent dictionaryDetailPO
if err := r.data.gormDB.WithContext(ctx).First(&parent, *v.ParentID).Error; err != nil {
return err
@@ -244,40 +290,59 @@ func (r *dictionaryRepo) CreateDictionaryDetail(ctx context.Context, v *biz.Dict
}
func (r *dictionaryRepo) UpdateDictionaryDetail(ctx context.Context, v *biz.DictionaryDetail) error {
var po dictionaryDetailPO
if err := r.data.gormDB.WithContext(ctx).First(&po, v.ID).Error; err != nil {
db := r.data.gormDB.WithContext(ctx)
if err := db.First(&po, v.ID).Error; err != nil {
return err
}
level, path := 0, ""
if v.ParentID != nil && *v.ParentID != 0 {
if v.ParentID != nil {
if *v.ParentID == v.ID {
return errors.New("不能将自身设为父级")
return errors.New("不能将字典详情设置为自己或其子项的父级")
}
var parent dictionaryDetailPO
if err := r.data.gormDB.WithContext(ctx).First(&parent, *v.ParentID).Error; err != nil {
if err := db.First(&parent, *v.ParentID).Error; err != nil {
return err
}
if strings.Contains(","+parent.Path+",", fmt.Sprintf(",%d,", v.ID)) {
return errors.New("不能移动到自己的子级")
return errors.New("不能将字典详情设置为自己或其子项的父级")
}
level = parent.Level + 1
path = strings.Trim(strings.Join([]string{parent.Path, fmt.Sprint(parent.ID)}, ","), ",")
}
oldPrefix := strings.Trim(strings.Join([]string{po.Path, fmt.Sprint(po.ID)}, ","), ",")
newPrefix := strings.Trim(strings.Join([]string{path, fmt.Sprint(po.ID)}, ","), ",")
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error {
if err := tx.Model(&dictionaryDetailPO{}).Where("id = ?", v.ID).Updates(map[string]any{"label": v.Label, "value": v.Value, "extend": v.Extend, "status": v.Status, "sort": v.Sort, "sys_dictionary_id": v.DictionaryID, "parent_id": v.ParentID, "level": level, "path": path}).Error; err != nil {
return err
}
return tx.Model(&dictionaryDetailPO{}).Where("path = ? OR path LIKE ?", oldPrefix, oldPrefix+",%").Update("path", gorm.Expr("REPLACE(path, ?, ?)", oldPrefix, newPrefix)).Error
return r.updateDictionaryDetailChildren(tx, v.ID, level, path)
})
}
// updateDictionaryDetailChildren recomputes descendants after a move, as the
// The service does this recursively. Updating level as well as path matters when a
// subtree is moved beneath a different depth.
func (r *dictionaryRepo) updateDictionaryDetailChildren(tx *gorm.DB, parentID uint, parentLevel int, parentPath string) error {
var children []dictionaryDetailPO
if err := tx.Where("parent_id = ?", parentID).Order("sort,id").Find(&children).Error; err != nil {
return err
}
for _, child := range children {
childPath := strings.Trim(strings.Join([]string{parentPath, fmt.Sprint(parentID)}, ","), ",")
if err := tx.Model(&dictionaryDetailPO{}).Where("id = ?", child.ID).Updates(map[string]any{"level": parentLevel + 1, "path": childPath}).Error; err != nil {
return err
}
if err := r.updateDictionaryDetailChildren(tx, child.ID, parentLevel+1, childPath); err != nil {
return err
}
}
return nil
}
func (r *dictionaryRepo) DeleteDictionaryDetail(ctx context.Context, id uint) error {
var count int64
if err := r.data.gormDB.WithContext(ctx).Model(&dictionaryDetailPO{}).Where("parent_id = ?", id).Count(&count).Error; err != nil {
return err
}
if count > 0 {
return errors.New("存在子级不可删除")
return errors.New("该字典详情下还有子项,无法删除")
}
return r.data.gormDB.WithContext(ctx).Delete(&dictionaryDetailPO{}, id).Error
}
+1 -1
View File
@@ -26,7 +26,7 @@ type errorRecordPO struct {
func (errorRecordPO) TableName() string { return "sys_error" }
func errorFromPO(v errorRecordPO) *biz.ErrorRecord {
return &biz.ErrorRecord{ID: v.ID, CreatedAt: v.CreatedAt, Form: v.Form, Info: v.Info, Level: v.Level, RequestID: v.RequestID, TraceID: v.TraceID, Solution: v.Solution, Status: v.Status}
return &biz.ErrorRecord{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, Form: v.Form, Info: v.Info, Level: v.Level, RequestID: v.RequestID, TraceID: v.TraceID, Solution: v.Solution, Status: v.Status}
}
func (r *auditRecorderRepo) CreateError(ctx context.Context, v *biz.ErrorRecord) error {
if v.Status == "" {
+51 -15
View File
@@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"kra/internal/biz"
"strconv"
"strings"
@@ -31,8 +32,11 @@ type exportTemplatePO struct {
func (exportTemplatePO) TableName() string { return "sys_export_templates" }
type exportConditionPO struct {
ID uint `gorm:"primaryKey"`
TemplateID string `gorm:"index"`
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
TemplateID string `gorm:"index"`
From string
Column string
Operator string
@@ -41,9 +45,12 @@ type exportConditionPO struct {
func (exportConditionPO) TableName() string { return "sys_export_template_condition" }
type exportJoinPO struct {
ID uint `gorm:"primaryKey"`
TemplateID string `gorm:"index"`
Join string `gorm:"column:joins"`
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
TemplateID string `gorm:"index"`
Join string `gorm:"column:joins"`
Table string
On string `gorm:"column:on"`
}
@@ -56,17 +63,17 @@ func NewExportRepo(data *Data) biz.ExportRepo { return &exportRepo{data: data} }
func exportFromPO(po exportTemplatePO, conditions []exportConditionPO, joins []exportJoinPO) *biz.ExportTemplate {
v := &biz.ExportTemplate{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, DBName: po.DBName, Name: po.Name, TableName: po.DBTableName, TemplateID: po.TemplateID, TemplateInfo: po.TemplateInfo, SQL: po.SQL, ImportSQL: po.ImportSQL, Limit: po.Limit, Order: po.Order}
for _, x := range conditions {
v.Conditions = append(v.Conditions, biz.ExportCondition{From: x.From, Column: x.Column, Operator: x.Operator})
v.Conditions = append(v.Conditions, biz.ExportCondition{ID: x.ID, CreatedAt: x.CreatedAt, UpdatedAt: x.UpdatedAt, TemplateID: x.TemplateID, From: x.From, Column: x.Column, Operator: x.Operator})
}
for _, x := range joins {
v.Joins = append(v.Joins, biz.ExportJoin{Join: x.Join, Table: x.Table, On: x.On})
v.Joins = append(v.Joins, biz.ExportJoin{ID: x.ID, CreatedAt: x.CreatedAt, UpdatedAt: x.UpdatedAt, TemplateID: x.TemplateID, Join: x.Join, Table: x.Table, On: x.On})
}
return v
}
func exportToPO(v *biz.ExportTemplate) exportTemplatePO {
return exportTemplatePO{ID: v.ID, DBName: v.DBName, Name: v.Name, DBTableName: v.TableName, TemplateID: v.TemplateID, TemplateInfo: v.TemplateInfo, SQL: v.SQL, ImportSQL: v.ImportSQL, Limit: v.Limit, Order: v.Order}
}
func (r *exportRepo) saveRelations(tx *gorm.DB, v *biz.ExportTemplate) error {
func (r *exportRepo) saveRelations(tx *gorm.DB, v *biz.ExportTemplate, resetIDs bool) error {
if err := tx.Where("template_id = ?", v.TemplateID).Delete(&exportConditionPO{}).Error; err != nil {
return err
}
@@ -75,19 +82,45 @@ func (r *exportRepo) saveRelations(tx *gorm.DB, v *biz.ExportTemplate) error {
}
conditions := make([]exportConditionPO, 0, len(v.Conditions))
for _, x := range v.Conditions {
conditions = append(conditions, exportConditionPO{TemplateID: v.TemplateID, From: x.From, Column: x.Column, Operator: x.Operator})
id := x.ID
if resetIDs {
id = 0
}
templateID := x.TemplateID
if templateID == "" {
templateID = v.TemplateID
}
conditions = append(conditions, exportConditionPO{ID: id, CreatedAt: x.CreatedAt, UpdatedAt: x.UpdatedAt, TemplateID: templateID, From: x.From, Column: x.Column, Operator: x.Operator})
}
if len(conditions) > 0 {
if err := tx.Create(&conditions).Error; err != nil {
create := tx
if !resetIDs {
// GORM saves Create associations with ON CONFLICT DO NOTHING.
// Preserve that behavior for copied templates carrying relation IDs.
create = create.Clauses(clause.OnConflict{DoNothing: true})
}
if err := create.Create(&conditions).Error; err != nil {
return err
}
}
joins := make([]exportJoinPO, 0, len(v.Joins))
for _, x := range v.Joins {
joins = append(joins, exportJoinPO{TemplateID: v.TemplateID, Join: x.Join, Table: x.Table, On: x.On})
id := x.ID
if resetIDs {
id = 0
}
templateID := x.TemplateID
if templateID == "" {
templateID = v.TemplateID
}
joins = append(joins, exportJoinPO{ID: id, CreatedAt: x.CreatedAt, UpdatedAt: x.UpdatedAt, TemplateID: templateID, Join: x.Join, Table: x.Table, On: x.On})
}
if len(joins) > 0 {
return tx.Create(&joins).Error
create := tx
if !resetIDs {
create = create.Clauses(clause.OnConflict{DoNothing: true})
}
return create.Create(&joins).Error
}
return nil
}
@@ -98,7 +131,7 @@ func (r *exportRepo) CreateExportTemplate(ctx context.Context, v *biz.ExportTemp
return err
}
v.ID = po.ID
return r.saveRelations(tx, v)
return r.saveRelations(tx, v, false)
})
}
func (r *exportRepo) UpdateExportTemplate(ctx context.Context, v *biz.ExportTemplate) error {
@@ -107,7 +140,7 @@ func (r *exportRepo) UpdateExportTemplate(ctx context.Context, v *biz.ExportTemp
if err := tx.Model(&exportTemplatePO{}).Where("id = ?", v.ID).Updates(&po).Error; err != nil {
return err
}
return r.saveRelations(tx, v)
return r.saveRelations(tx, v, true)
})
}
func (r *exportRepo) DeleteExportTemplates(ctx context.Context, ids []uint) error {
@@ -387,7 +420,10 @@ func parseTemplateColumns(raw string) map[string]string {
return out
}
func (r *exportRepo) ImportExportRows(ctx context.Context, t *biz.ExportTemplate, rows []map[string]any) error {
sql := strings.TrimSpace(t.ImportSQL)
// ImportSQL is checked verbatim. In particular, a whitespace-only value
// is still treated as custom SQL and is allowed to return the driver's
// native error instead of silently falling back to GORM insertion.
sql := t.ImportSQL
selected, err := r.data.database(t.DBName)
if err != nil {
return err
+2 -2
View File
@@ -26,7 +26,7 @@ func (r *auditRecorderRepo) RecordLogin(ctx context.Context, v *biz.LoginLog) er
return r.data.gormDB.WithContext(ctx).Create(&loginLogPO{Username: v.Username, IP: v.IP, Status: v.Status, ErrorMessage: v.ErrorMessage, Agent: v.Agent, UserID: v.UserID}).Error
}
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}
return &biz.LoginLog{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, Username: v.Username, IP: v.IP, Status: v.Status, ErrorMessage: v.ErrorMessage, Agent: v.Agent, UserID: v.UserID}
}
func (r *auditQueryRepo) ListLogins(ctx context.Context, page, size int, q *biz.LoginLog) ([]*biz.LoginLog, int64, error) {
db := r.data.gormDB.WithContext(ctx).Model(&loginLogPO{})
@@ -69,5 +69,5 @@ func (r *auditQueryRepo) FindLogin(ctx context.Context, id uint) (*biz.LoginLog,
return loginFromPO(po), nil
}
func (r *auditQueryRepo) DeleteLogins(ctx context.Context, ids []uint) error {
return r.data.gormDB.WithContext(ctx).Delete(&loginLogPO{}, ids).Error
return r.data.gormDB.WithContext(ctx).Delete(&loginLogPO{}, "id IN ?", ids).Error
}
+13 -3
View File
@@ -55,6 +55,7 @@ func (r *mediaRepo) CreateMedia(ctx context.Context, v *biz.MediaFile) error {
}
v.ID = po.ID
v.CreatedAt = po.CreatedAt
v.UpdatedAt = po.UpdatedAt
return nil
}
func (r *mediaRepo) FindMedia(ctx context.Context, id uint) (*biz.MediaFile, error) {
@@ -119,7 +120,13 @@ func (r *mediaRepo) ListMedia(ctx context.Context, filter biz.MediaFilter) ([]*b
return out, total, nil
}
func (r *mediaRepo) UpdateMediaName(ctx context.Context, id uint, name string) error {
return r.data.gormDB.WithContext(ctx).Model(&mediaPO{}).Where("id = ?", id).Update("name", name).Error
// The compatible flow performs a First before Update, so a missing record returns a
// not-found error instead of silently succeeding with RowsAffected=0.
var po mediaPO
if err := r.data.gormDB.WithContext(ctx).Where("id = ?", id).First(&po).Error; err != nil {
return err
}
return r.data.gormDB.WithContext(ctx).Model(&po).Update("name", name).Error
}
func (r *mediaRepo) DeleteMedia(ctx context.Context, id uint) error {
return r.data.gormDB.WithContext(ctx).Unscoped().Delete(&mediaPO{}, id).Error
@@ -176,14 +183,17 @@ func (r *mediaRepo) ListCategories(ctx context.Context) ([]*biz.AttachmentCatego
}
byID := map[uint]*biz.AttachmentCategory{}
for _, po := range pos {
byID[po.ID] = &biz.AttachmentCategory{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Name: po.Name, ParentID: po.ParentID, Children: []*biz.AttachmentCategory{}}
// Children remains nil for leaf nodes, which serializes as null.
byID[po.ID] = &biz.AttachmentCategory{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Name: po.Name, ParentID: po.ParentID}
}
roots := []*biz.AttachmentCategory{}
for _, po := range pos {
v := byID[po.ID]
if parent := byID[po.ParentID]; parent != nil {
parent.Children = append(parent.Children, v)
} else {
} else if po.ParentID == 0 {
// The tree starts at parent id 0; non-zero orphan nodes are
// intentionally omitted instead of being promoted to roots.
roots = append(roots, v)
}
}
+6 -4
View File
@@ -84,7 +84,9 @@ func (r *mediaRepo) CompleteUploadSession(ctx context.Context, id uint, key stri
return r.data.gormDB.WithContext(ctx).Model(&uploadSessionPO{}).Where("id = ?", id).Updates(map[string]any{"status": "completed", "storage_key": key, "media_id": mediaID}).Error
}
func (r *mediaRepo) DeleteUploadSession(ctx context.Context, id uint) error {
return r.data.gormDB.WithContext(ctx).Unscoped().Delete(&uploadSessionPO{}, id).Error
// The compatible flow uses GORM's normal Delete here, retaining the soft-deleted session
// for audit/recovery rather than physically removing it.
return r.data.gormDB.WithContext(ctx).Delete(&uploadSessionPO{}, id).Error
}
func (r *mediaRepo) UpsertChunk(ctx context.Context, uploadID uint, v *biz.UploadChunk) error {
po := uploadChunkPO{UploadID: uploadID, ChunkIndex: v.Index, ChunkHash: v.Hash, Size: v.Size}
@@ -102,7 +104,7 @@ func (r *mediaRepo) ListChunks(ctx context.Context, uploadID uint) ([]*biz.Uploa
return out, nil
}
func (r *mediaRepo) DeleteChunks(ctx context.Context, uploadID uint) error {
return r.data.gormDB.WithContext(ctx).Unscoped().Where("upload_id = ?", uploadID).Delete(&uploadChunkPO{}).Error
return r.data.gormDB.WithContext(ctx).Where("upload_id = ?", uploadID).Delete(&uploadChunkPO{}).Error
}
func (r *mediaRepo) StaleUploadSessionIDs(ctx context.Context, before time.Time) ([]uint, error) {
var ids []uint
@@ -111,9 +113,9 @@ func (r *mediaRepo) StaleUploadSessionIDs(ctx context.Context, before time.Time)
}
func (r *mediaRepo) DeleteUploadData(ctx context.Context, uploadID uint) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Unscoped().Where("upload_id = ?", uploadID).Delete(&uploadChunkPO{}).Error; err != nil {
if err := tx.Where("upload_id = ?", uploadID).Delete(&uploadChunkPO{}).Error; err != nil {
return err
}
return tx.Unscoped().Delete(&uploadSessionPO{}, uploadID).Error
return tx.Delete(&uploadSessionPO{}, uploadID).Error
})
}
+35 -16
View File
@@ -3,6 +3,7 @@ package data
import (
"context"
"errors"
"time"
"kra/internal/biz"
@@ -14,7 +15,12 @@ type menuRepo struct{ data *Data }
func NewMenuRepo(data *Data) biz.MenuRepo { return &menuRepo{data: data} }
func menuFromPO(po menuPO) *biz.Menu {
return &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}
var deletedAt *time.Time
if po.DeletedAt.Valid {
value := po.DeletedAt.Time
deletedAt = &value
}
return &biz.Menu{CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, DeletedAt: deletedAt, 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}
}
func menuToPO(v *biz.Menu) menuPO {
@@ -22,19 +28,14 @@ func menuToPO(v *biz.Menu) menuPO {
}
func replaceMenuRelations(tx *gorm.DB, menu *biz.Menu) error {
var oldButtonIDs []uint
if err := tx.Model(&menuButtonPO{}).Where("sys_base_menu_id = ?", menu.ID).Pluck("id", &oldButtonIDs).Error; err != nil {
// The compatible update rebuilds menu-button rows but does not remove the
// existing authority-button rows that point at the old button IDs. Keep
// that observable behavior here; menu deletion itself still cleans those
// rows in Delete below, matching the administration endpoint behavior.
if err := tx.Unscoped().Where("sys_base_menu_id = ?", menu.ID).Delete(&menuButtonPO{}).Error; err != nil {
return err
}
if len(oldButtonIDs) > 0 {
if err := tx.Where("sys_base_menu_btn_id IN ?", oldButtonIDs).Delete(&authorityButtonPO{}).Error; err != nil {
return err
}
}
if err := tx.Where("sys_base_menu_id = ?", menu.ID).Delete(&menuButtonPO{}).Error; err != nil {
return err
}
if err := tx.Where("sys_base_menu_id = ?", menu.ID).Delete(&menuParameterPO{}).Error; err != nil {
if err := tx.Unscoped().Where("sys_base_menu_id = ?", menu.ID).Delete(&menuParameterPO{}).Error; err != nil {
return err
}
buttons := make([]menuButtonPO, 0, len(menu.Buttons))
@@ -166,7 +167,7 @@ func (r *menuRepo) loadRelations(ctx context.Context, menu *biz.Menu) error {
}
menu.Parameters = make([]*biz.MenuParameter, 0, len(parameters))
for _, parameter := range parameters {
menu.Parameters = append(menu.Parameters, &biz.MenuParameter{ID: parameter.ID, MenuID: parameter.MenuID, Type: parameter.Type, Key: parameter.Key, Value: parameter.Value})
menu.Parameters = append(menu.Parameters, menuParameterFromPO(parameter))
}
var buttons []menuButtonPO
if err := r.data.gormDB.WithContext(ctx).Where("sys_base_menu_id = ?", menu.ID).Order("id").Find(&buttons).Error; err != nil {
@@ -174,7 +175,7 @@ func (r *menuRepo) loadRelations(ctx context.Context, menu *biz.Menu) error {
}
menu.Buttons = make([]*biz.MenuButton, 0, len(buttons))
for _, button := range buttons {
menu.Buttons = append(menu.Buttons, &biz.MenuButton{ID: button.ID, Name: button.Name, Description: button.Description, MenuID: button.MenuID})
menu.Buttons = append(menu.Buttons, menuButtonFromPO(button))
}
return nil
}
@@ -228,7 +229,7 @@ func (r *menuRepo) List(ctx context.Context) ([]*biz.Menu, error) {
}
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})
menu.Parameters = append(menu.Parameters, menuParameterFromPO(parameter))
}
var buttons []menuButtonPO
if err := r.data.gormDB.WithContext(ctx).Where("sys_base_menu_id IN ?", ids).Order("id").Find(&buttons).Error; err != nil {
@@ -236,11 +237,29 @@ func (r *menuRepo) List(ctx context.Context) ([]*biz.Menu, error) {
}
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})
menu.Buttons = append(menu.Buttons, menuButtonFromPO(button))
}
return out, nil
}
func menuParameterFromPO(po menuParameterPO) *biz.MenuParameter {
var deletedAt *time.Time
if po.DeletedAt.Valid {
value := po.DeletedAt.Time
deletedAt = &value
}
return &biz.MenuParameter{CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, DeletedAt: deletedAt, ID: po.ID, MenuID: po.MenuID, Type: po.Type, Key: po.Key, Value: po.Value}
}
func menuButtonFromPO(po menuButtonPO) *biz.MenuButton {
var deletedAt *time.Time
if po.DeletedAt.Valid {
value := po.DeletedAt.Time
deletedAt = &value
}
return &biz.MenuButton{CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, DeletedAt: deletedAt, ID: po.ID, Name: po.Name, Description: po.Description, MenuID: po.MenuID}
}
func (r *menuRepo) SetAuthorityMenus(ctx context.Context, id uint, ids []uint) error {
if err := (&authorityAccessRepo{data: r.data}).checkAuthorityIDAuth(ctx, id); err != nil {
return err
+136 -1
View File
@@ -2,14 +2,18 @@ package data
import (
"fmt"
"time"
"gorm.io/gorm"
)
func migrateAll(db *gorm.DB) error {
if err := migrateLegacyIgnoreAPITable(db); err != nil {
return err
}
if err := db.AutoMigrate(
&userPO{}, &authorityPO{}, &menuPO{}, &userAuthorityPO{}, &authorityMenuPO{}, &menuParameterPO{},
&apiPO{}, &ignoredAPIPO{}, &authorityAPIPO{}, &menuButtonPO{}, &authorityButtonPO{},
&apiPO{}, &ignoredAPIPO{}, &authorityAPIPO{}, &casbinRulePO{}, &menuButtonPO{}, &authorityButtonPO{},
&departmentPO{}, &positionPO{}, &userDepartmentPO{}, &userPositionPO{}, &authorityDepartmentPO{},
&dictionaryPO{}, &dictionaryDetailPO{}, &parameterPO{}, &apiTokenPO{}, &jwtBlacklistPO{}, &securityConfigPO{},
&versionPO{}, &exportTemplatePO{}, &exportConditionPO{}, &exportJoinPO{},
@@ -19,9 +23,140 @@ func migrateAll(db *gorm.DB) error {
); err != nil {
return err
}
if err := migrateLegacyAuthorityAPIsToCasbinRules(db); err != nil {
return err
}
if err := normalizeErrorRecordStatuses(db); err != nil {
return err
}
if err := reconcileRootAuthorityAPIs(db); err != nil {
return err
}
return reconcileReferenceIndexes(db)
}
// Older Kra builds used a status label that is not part of the GVA error-log
// contract and is therefore rendered as an unknown state by the compatible
// administration page.
func normalizeErrorRecordStatuses(db *gorm.DB) error {
return db.Model(&errorRecordPO{}).Where("status = ?", "未解决").Update("status", "未处理").Error
}
// migrateLegacyAuthorityAPIsToCasbinRules upgrades the early Kra join-table
// representation to the independent Casbin policy table. Keep the legacy
// table in place for backwards compatibility, but make casbin_rule the sole
// live policy source. Existing policy rows are not duplicated.
func migrateLegacyAuthorityAPIsToCasbinRules(db *gorm.DB) error {
if !db.Migrator().HasTable(&authorityAPIPO{}) || !db.Migrator().HasTable(&casbinRulePO{}) {
return nil
}
type legacyPolicy struct {
AuthorityID uint
Path string
Method string
}
var rows []legacyPolicy
if err := db.Table("sys_authority_apis sa").
Select("sa.authority_id, a.path, a.method").
Joins("JOIN sys_apis a ON a.id = sa.api_id").
Where("a.deleted_at IS NULL").Find(&rows).Error; err != nil {
return err
}
for _, row := range rows {
exists, err := policyExists(db, row.AuthorityID, row.Path, row.Method)
if err != nil {
return err
}
if exists {
continue
}
if err := db.Create(&casbinRulePO{Ptype: "p", V0: fmt.Sprint(row.AuthorityID), V1: row.Path, V2: row.Method}).Error; err != nil {
return err
}
}
return nil
}
// migrateLegacyIgnoreAPITable upgrades the early Kra-only composite-key
// shape (path, method) to the compatible model shape (ID/timestamps/soft
// delete). AutoMigrate can add columns but cannot replace an existing
// composite primary key portably, so rebuild the small table once while
// preserving every existing ignore rule.
func migrateLegacyIgnoreAPITable(db *gorm.DB) error {
if !db.Migrator().HasTable(&ignoredAPIPO{}) || db.Migrator().HasColumn(&ignoredAPIPO{}, "id") {
return nil
}
legacyTable := fmt.Sprintf("sys_ignore_apis_legacy_%d", time.Now().UnixNano())
type legacyIgnoredAPI struct {
Path string
Method string
}
return db.Transaction(func(tx *gorm.DB) error {
if err := tx.Migrator().RenameTable(ignoredAPIPO{}.TableName(), legacyTable); err != nil {
return fmt.Errorf("rename legacy ignore API table: %w", err)
}
if err := tx.AutoMigrate(&ignoredAPIPO{}); err != nil {
return fmt.Errorf("create compatible ignore API table: %w", err)
}
var rows []legacyIgnoredAPI
if err := tx.Table(legacyTable).Find(&rows).Error; err != nil {
return fmt.Errorf("read legacy ignore API rows: %w", err)
}
if len(rows) > 0 {
items := make([]ignoredAPIPO, 0, len(rows))
for _, row := range rows {
items = append(items, ignoredAPIPO{Path: row.Path, Method: row.Method})
}
if err := tx.Create(&items).Error; err != nil {
return fmt.Errorf("copy legacy ignore API rows: %w", err)
}
}
if err := tx.Migrator().DropTable(legacyTable); err != nil {
return fmt.Errorf("drop legacy ignore API table: %w", err)
}
return nil
})
}
// reconcileRootAuthorityAPIs is a one-time upgrade path from the former Kra
// implementation where authority 888 bypassed policy storage entirely. The compatible behavior
// grants its root role through persisted Casbin policies, so when a legacy
// database has the root role but no stored API links, materialize the same
// policy set and let normal authorization read it thereafter.
func reconcileRootAuthorityAPIs(db *gorm.DB) error {
var authorityCount int64
if err := db.Model(&authorityPO{}).Where("authority_id = ?", 888).Count(&authorityCount).Error; err != nil || authorityCount == 0 {
return err
}
var policyCount int64
if err := policyScope(db).Where("v0 = ?", "888").Count(&policyCount).Error; err != nil || policyCount != 0 {
return err
}
var ignored []ignoredAPIPO
if err := db.Find(&ignored).Error; err != nil {
return err
}
ignoreSet := make(map[string]struct{}, len(ignored))
for _, item := range ignored {
ignoreSet[item.Method+"\x00"+item.Path] = struct{}{}
}
var apis []apiPO
if err := db.Find(&apis).Error; err != nil {
return err
}
rules := make([]casbinRulePO, 0, len(apis))
for _, api := range apis {
if _, ok := ignoreSet[api.Method+"\x00"+api.Path]; ok {
continue
}
rules = append(rules, newPolicyRule(888, api.Path, api.Method))
}
if len(rules) == 0 {
return nil
}
return db.Create(&rules).Error
}
// reconcileReferenceIndexes removes constraints created by older Kra builds
// that are not part of the administration data model. Business services own
// duplicate checks and their user-facing error messages.
+2 -2
View File
@@ -33,7 +33,7 @@ func (r *auditRecorderRepo) RecordOperation(ctx context.Context, v *biz.Operatio
return r.data.gormDB.WithContext(ctx).Create(&operationPO{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}).Error
}
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}
return &biz.OperationRecord{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, 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 *auditQueryRepo) ListOperations(ctx context.Context, page, size int, q *biz.OperationRecord) ([]*biz.OperationRecord, int64, error) {
db := r.data.gormDB.WithContext(ctx).Model(&operationPO{})
@@ -98,5 +98,5 @@ func (r *auditQueryRepo) FindOperation(ctx context.Context, id uint) (*biz.Opera
return opFromPO(po), nil
}
func (r *auditQueryRepo) DeleteOperations(ctx context.Context, ids []uint) error {
return r.data.gormDB.WithContext(ctx).Delete(&operationPO{}, ids).Error
return r.data.gormDB.WithContext(ctx).Delete(&operationPO{}, "id IN ?", ids).Error
}
+3 -1
View File
@@ -36,7 +36,9 @@ func (r *parameterRepo) CreateParameter(ctx context.Context, v *biz.SystemParame
return nil
}
func (r *parameterRepo) UpdateParameter(ctx context.Context, v *biz.SystemParameter) error {
return r.data.gormDB.WithContext(ctx).Model(&parameterPO{}).Where("id = ?", v.ID).Updates(map[string]any{"name": v.Name, "key": v.Key, "value": v.Value, "desc": v.Desc}).Error
// The compatible update uses a struct, so zero-value optional fields (notably desc)
// are intentionally ignored rather than clearing an existing value.
return r.data.gormDB.WithContext(ctx).Model(&parameterPO{}).Where("id = ?", v.ID).Updates(&parameterPO{Name: v.Name, Key: v.Key, Value: v.Value, Desc: v.Desc}).Error
}
func (r *parameterRepo) DeleteParameters(ctx context.Context, ids []uint) error {
return r.data.gormDB.WithContext(ctx).Delete(&parameterPO{}, ids).Error
+5 -1
View File
@@ -2,6 +2,7 @@ package data
import (
"context"
"time"
"kra/internal/biz"
@@ -14,6 +15,9 @@ func NewPermissionRepo(data *Data) biz.PermissionRepo { return &permissionRepo{d
type menuButtonPO struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
Name string
Description string `gorm:"column:desc"`
MenuID uint `gorm:"column:sys_base_menu_id"`
@@ -36,7 +40,7 @@ func (r *permissionRepo) Buttons(ctx context.Context, menuID uint) ([]*biz.MenuB
}
out := make([]*biz.MenuButton, 0, len(pos))
for _, po := range pos {
out = append(out, &biz.MenuButton{ID: po.ID, Name: po.Name, Description: po.Description, MenuID: po.MenuID})
out = append(out, menuButtonFromPO(po))
}
return out, nil
}
+7 -2
View File
@@ -16,6 +16,9 @@ func NewSecurityRepo(data *Data) biz.SecurityRepo { return &securityRepo{data: d
type securityConfigPO struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
CaptchaOpen int
CaptchaTimeout int
KeyLong int
@@ -43,10 +46,10 @@ func defaultSecurityConfig() securityConfigPO {
return securityConfigPO{ID: 1, CaptchaTimeout: 3600, KeyLong: 6, ImgWidth: 240, ImgHeight: 80, PwdMinLength: 8, LimitWindow: 60, LimitCount: 30, LockThreshold: 5, LockDuration: 30, PwdExpireDays: 90}
}
func securityFromPO(v securityConfigPO) *biz.SecurityConfig {
return &biz.SecurityConfig{ID: v.ID, CaptchaOpen: v.CaptchaOpen, CaptchaTimeout: v.CaptchaTimeout, KeyLong: v.KeyLong, ImgWidth: v.ImgWidth, ImgHeight: v.ImgHeight, PwdMinLength: v.PwdMinLength, PwdRequireUpper: v.PwdRequireUpper, PwdRequireLower: v.PwdRequireLower, PwdRequireDigit: v.PwdRequireDigit, PwdRequireSpecial: v.PwdRequireSpecial, LimitEnable: v.LimitEnable, LimitWindow: v.LimitWindow, LimitCount: v.LimitCount, LockEnable: v.LockEnable, LockThreshold: v.LockThreshold, LockDuration: v.LockDuration, PwdExpireEnable: v.PwdExpireEnable, PwdExpireDays: v.PwdExpireDays, ForceNewUserChangePassword: v.ForceNewUserChangePassword}
return &biz.SecurityConfig{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, CaptchaOpen: v.CaptchaOpen, CaptchaTimeout: v.CaptchaTimeout, KeyLong: v.KeyLong, ImgWidth: v.ImgWidth, ImgHeight: v.ImgHeight, PwdMinLength: v.PwdMinLength, PwdRequireUpper: v.PwdRequireUpper, PwdRequireLower: v.PwdRequireLower, PwdRequireDigit: v.PwdRequireDigit, PwdRequireSpecial: v.PwdRequireSpecial, LimitEnable: v.LimitEnable, LimitWindow: v.LimitWindow, LimitCount: v.LimitCount, LockEnable: v.LockEnable, LockThreshold: v.LockThreshold, LockDuration: v.LockDuration, PwdExpireEnable: v.PwdExpireEnable, PwdExpireDays: v.PwdExpireDays, ForceNewUserChangePassword: v.ForceNewUserChangePassword}
}
func securityToPO(v *biz.SecurityConfig) securityConfigPO {
return securityConfigPO{ID: 1, CaptchaOpen: v.CaptchaOpen, CaptchaTimeout: v.CaptchaTimeout, KeyLong: v.KeyLong, ImgWidth: v.ImgWidth, ImgHeight: v.ImgHeight, PwdMinLength: v.PwdMinLength, PwdRequireUpper: v.PwdRequireUpper, PwdRequireLower: v.PwdRequireLower, PwdRequireDigit: v.PwdRequireDigit, PwdRequireSpecial: v.PwdRequireSpecial, LimitEnable: v.LimitEnable, LimitWindow: v.LimitWindow, LimitCount: v.LimitCount, LockEnable: v.LockEnable, LockThreshold: v.LockThreshold, LockDuration: v.LockDuration, PwdExpireEnable: v.PwdExpireEnable, PwdExpireDays: v.PwdExpireDays, ForceNewUserChangePassword: v.ForceNewUserChangePassword}
return securityConfigPO{ID: 1, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, CaptchaOpen: v.CaptchaOpen, CaptchaTimeout: v.CaptchaTimeout, KeyLong: v.KeyLong, ImgWidth: v.ImgWidth, ImgHeight: v.ImgHeight, PwdMinLength: v.PwdMinLength, PwdRequireUpper: v.PwdRequireUpper, PwdRequireLower: v.PwdRequireLower, PwdRequireDigit: v.PwdRequireDigit, PwdRequireSpecial: v.PwdRequireSpecial, LimitEnable: v.LimitEnable, LimitWindow: v.LimitWindow, LimitCount: v.LimitCount, LockEnable: v.LockEnable, LockThreshold: v.LockThreshold, LockDuration: v.LockDuration, PwdExpireEnable: v.PwdExpireEnable, PwdExpireDays: v.PwdExpireDays, ForceNewUserChangePassword: v.ForceNewUserChangePassword}
}
func (r *securityRepo) SecurityConfig(ctx context.Context) (*biz.SecurityConfig, error) {
var po securityConfigPO
@@ -66,6 +69,7 @@ func (r *securityRepo) SaveSecurityConfig(ctx context.Context, v *biz.SecurityCo
return err
}
po := securityToPO(v)
po.CreatedAt, po.UpdatedAt = previous.CreatedAt, previous.UpdatedAt
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Save(&po).Error; err != nil {
return err
@@ -73,6 +77,7 @@ func (r *securityRepo) SaveSecurityConfig(ctx context.Context, v *biz.SecurityCo
if v.PwdExpireEnable && !previous.PwdExpireEnable {
return tx.Model(&userPO{}).Where("password_updated_at IS NULL").Update("password_updated_at", time.Now()).Error
}
v.ID, v.CreatedAt, v.UpdatedAt = po.ID, po.CreatedAt, po.UpdatedAt
return nil
})
}
+1 -1
View File
@@ -86,7 +86,7 @@ func (s *aliyunStorage) DeletePrefix(ctx context.Context, prefix string) error {
}
}
func (s *aliyunStorage) List(_ context.Context, prefix, cursor string, limit int) ([]*biz.StoredFile, string, bool, error) {
if limit < 1 || limit > 1000 {
if limit <= 0 {
limit = 100
}
result, err := s.bucket.ListObjects(oss.Prefix(s.key(prefix)), oss.Marker(cursor), oss.MaxKeys(limit))
+1 -1
View File
@@ -121,7 +121,7 @@ func (s *awsStorage) DeletePrefix(ctx context.Context, prefix string) error {
}
}
func (s *awsStorage) List(ctx context.Context, prefix, cursor string, limit int) ([]*biz.StoredFile, string, bool, error) {
if limit < 1 || limit > 1000 {
if limit <= 0 {
limit = 100
}
input := &s3.ListObjectsV2Input{Bucket: aws.String(s.bucket), Prefix: aws.String(s.key(prefix)), MaxKeys: aws.Int32(int32(limit))}
+1 -1
View File
@@ -85,7 +85,7 @@ func (s *huaweiStorage) DeletePrefix(ctx context.Context, prefix string) error {
}
}
func (s *huaweiStorage) List(_ context.Context, prefix, cursor string, limit int) ([]*biz.StoredFile, string, bool, error) {
if limit < 1 || limit > 1000 {
if limit <= 0 {
limit = 100
}
result, err := s.client.ListObjects(&obs.ListObjectsInput{ListObjsInput: obs.ListObjsInput{Prefix: s.key(prefix), MaxKeys: limit}, Bucket: s.bucket, Marker: cursor})
+10 -2
View File
@@ -103,6 +103,9 @@ func composeFiles(ctx context.Context, storage biz.FileStorage, names []string,
}
func (s *fileStorage) resolve(name string) (string, error) {
if name == "" {
return "", fmt.Errorf("key不能为空")
}
clean := filepath.Clean(strings.TrimPrefix(name, "/"))
if clean == "." || filepath.IsAbs(clean) || clean == ".." || strings.HasPrefix(clean, ".."+string(filepath.Separator)) {
return "", fmt.Errorf("invalid storage path")
@@ -146,7 +149,12 @@ func (s *fileStorage) Delete(ctx context.Context, name string) error {
if err != nil {
return err
}
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
// Match the compatible Local.DeleteFile behavior: a missing object is an error and the
// media DB record is retained when the physical file cannot be removed.
if _, err := os.Stat(path); os.IsNotExist(err) {
return fmt.Errorf("文件不存在")
}
if err := os.Remove(path); err != nil {
return err
}
return nil
@@ -170,7 +178,7 @@ func (s *fileStorage) DeletePrefix(ctx context.Context, prefix string) error {
return nil
}
func (s *fileStorage) List(ctx context.Context, prefix, cursor string, limit int) ([]*biz.StoredFile, string, bool, error) {
if limit < 1 || limit > 1000 {
if limit <= 0 {
limit = 100
}
root, err := s.resolve(strings.TrimSuffix(prefix, "/") + "/placeholder")
+1 -1
View File
@@ -111,7 +111,7 @@ func (s *qiniuStorage) DeletePrefix(ctx context.Context, prefix string) error {
}
}
func (s *qiniuStorage) List(ctx context.Context, prefix, cursor string, limit int) ([]*biz.StoredFile, string, bool, error) {
if limit < 1 || limit > 1000 {
if limit <= 0 {
limit = 100
}
entries, _, marker, more, err := s.manager.ListFiles(s.config.Bucket, prefix, "", cursor, limit)
+1 -1
View File
@@ -109,7 +109,7 @@ func (s *s3Storage) DeletePrefix(ctx context.Context, prefix string) error {
return nil
}
func (s *s3Storage) List(ctx context.Context, prefix, cursor string, limit int) ([]*biz.StoredFile, string, bool, error) {
if limit < 1 || limit > 1000 {
if limit <= 0 {
limit = 100
}
items := s.client.ListObjects(ctx, s.bucket, minio.ListObjectsOptions{Prefix: s.key(prefix), Recursive: true, StartAfter: s.key(cursor)})
+1 -1
View File
@@ -99,7 +99,7 @@ func (s *tencentStorage) DeletePrefix(ctx context.Context, prefix string) error
}
}
func (s *tencentStorage) List(ctx context.Context, prefix, cursor string, limit int) ([]*biz.StoredFile, string, bool, error) {
if limit < 1 || limit > 1000 {
if limit <= 0 {
limit = 100
}
result, _, err := s.client.Bucket.Get(ctx, &cos.BucketGetOptions{Prefix: s.key(prefix), Marker: cursor, MaxKeys: limit})
+8 -5
View File
@@ -81,11 +81,14 @@ type authorityMenuPO struct {
func (authorityMenuPO) TableName() string { return "sys_authority_menus" }
type menuParameterPO struct {
ID uint `gorm:"primaryKey"`
MenuID uint `gorm:"column:sys_base_menu_id;index"`
Type string
Key string
Value string
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
MenuID uint `gorm:"column:sys_base_menu_id;index"`
Type string
Key string
Value string
}
func (menuParameterPO) TableName() string { return "sys_base_menu_parameters" }
+43 -8
View File
@@ -112,10 +112,16 @@ func (r *initializationRepo) Initialize(ctx context.Context, input *biz.Database
return err
}
if err := db.Transaction(func(tx *gorm.DB) error {
authority := authorityPO{AuthorityID: 888, AuthorityName: "超级管理员", DataScope: 1, DefaultRouter: "dashboard"}
rootParentID := uint(0)
authority := authorityPO{AuthorityID: 888, AuthorityName: "超级管理员", ParentID: &rootParentID, DataScope: 1, DefaultRouter: "dashboard"}
if err := tx.FirstOrCreate(&authority, authorityPO{AuthorityID: 888}).Error; err != nil {
return err
}
// The root role is seeded with parent_id=0. Older Kra databases used
// NULL, which makes the role disappear from the same root-only queries.
if err := tx.Model(&authorityPO{}).Where("authority_id = ? AND parent_id IS NULL", 888).Update("parent_id", 0).Error; err != nil {
return err
}
menus := defaultMenus()
for i := range menus {
if err := tx.Where("name = ?", menus[i].Name).FirstOrCreate(&menus[i]).Error; err != nil {
@@ -199,17 +205,36 @@ func (r *initializationRepo) Initialize(ctx context.Context, input *biz.Database
return err
}
}
for _, ignored := range []ignoredAPIPO{
{Method: "GET", Path: "/api/freshCasbin"}, {Method: "GET", Path: "/health"},
{Method: "POST", Path: "/system/reloadSystem"}, {Method: "POST", Path: "/base/login"},
{Method: "POST", Path: "/base/captcha"}, {Method: "POST", Path: "/init/initdb"},
{Method: "POST", Path: "/init/checkdb"}, {Method: "GET", Path: "/info/getInfoDataSource"},
{Method: "GET", Path: "/info/getInfoPublic"},
} {
ignoredAPIs := defaultIgnoredAPIs()
for _, ignored := range ignoredAPIs {
if err := tx.FirstOrCreate(&ignored, ignored).Error; err != nil {
return err
}
}
ignoreSet := make(map[string]struct{}, len(ignoredAPIs))
for _, ignored := range ignoredAPIs {
ignoreSet[ignored.Method+"\x00"+ignored.Path] = struct{}{}
}
var apiRows []apiPO
if err := tx.Find(&apiRows).Error; err != nil {
return err
}
for _, api := range apiRows {
if _, ignored := ignoreSet[api.Method+"\x00"+api.Path]; ignored {
continue
}
exists, err := policyExists(tx, 888, api.Path, api.Method)
if err != nil {
return err
}
if exists {
continue
}
rule := newPolicyRule(888, api.Path, api.Method)
if err := tx.Create(&rule).Error; err != nil {
return err
}
}
return nil
}); err != nil {
return err
@@ -222,6 +247,16 @@ func (r *initializationRepo) Initialize(ctx context.Context, input *biz.Database
return nil
}
func defaultIgnoredAPIs() []ignoredAPIPO {
return []ignoredAPIPO{
{Method: "GET", Path: "/api/freshCasbin"}, {Method: "GET", Path: "/health"},
{Method: "POST", Path: "/system/reloadSystem"}, {Method: "POST", Path: "/base/login"},
{Method: "POST", Path: "/base/captcha"}, {Method: "POST", Path: "/init/initdb"},
{Method: "POST", Path: "/init/checkdb"}, {Method: "GET", Path: "/info/getInfoDataSource"},
{Method: "GET", Path: "/info/getInfoPublic"},
}
}
func defaultMenus() []menuPO {
root := func(path, name, title, icon string, sort int) menuPO {
return menuPO{Path: path, Name: name, Component: "view/routerHolder.vue", Title: title, Icon: icon, Sort: sort}
+1 -1
View File
@@ -116,7 +116,7 @@ func (r *taskRepo) RecordTaskLog(ctx context.Context, v *biz.TimedTaskLog) error
return r.data.gormDB.WithContext(ctx).Create(&taskLogPO{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}).Error
}
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}
return &biz.TimedTaskLog{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, 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) {
db := r.data.gormDB.WithContext(ctx).Model(&taskLogPO{})
+3 -2
View File
@@ -68,8 +68,9 @@ func TestDictionaryImportKeepsHierarchyInOneTransaction(t *testing.T) {
ctx := context.Background()
repo := &dictionaryRepo{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}}
active := true
dictionary := &biz.Dictionary{Name: "status", Type: "status", Status: &active}
details := []*biz.DictionaryDetail{{ID: 10, Label: "parent", Value: "1", Status: &active}, {ID: 11, Label: "child", Value: "2", ParentID: &parentID, Status: &active}}
if err := repo.ImportDictionary(ctx, dictionary, details); err != nil {
t.Fatal(err)
}
+24 -5
View File
@@ -79,7 +79,12 @@ func (r *userRepo) loadUser(ctx context.Context, po *userPO) (*biz.User, error)
}
func toBizAuthority(po authorityPO) biz.Authority {
return biz.Authority{AuthorityID: po.AuthorityID, AuthorityName: po.AuthorityName, ParentID: po.ParentID, DataScope: po.DataScope, DefaultRouter: po.DefaultRouter}
var deletedAt *time.Time
if po.DeletedAt.Valid {
value := po.DeletedAt.Time
deletedAt = &value
}
return biz.Authority{CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, DeletedAt: deletedAt, AuthorityID: po.AuthorityID, AuthorityName: po.AuthorityName, ParentID: po.ParentID, DataScope: po.DataScope, DefaultRouter: po.DefaultRouter}
}
func baseBizUser(po *userPO) *biz.User {
@@ -207,7 +212,9 @@ func (r *userRepo) MenusByAuthority(ctx context.Context, authorityID uint) ([]*b
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 := menuFromPO(po)
menu.MenuID = po.ID
menu.Children = []*biz.Menu{}
menu.Parameters = []*biz.MenuParameter{}
menu.Buttons = []*biz.MenuButton{}
byID[po.ID] = menu
@@ -220,7 +227,7 @@ func (r *userRepo) MenusByAuthority(ctx context.Context, authorityID uint) ([]*b
}
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})
menu.Parameters = append(menu.Parameters, menuParameterFromPO(parameter))
}
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 IN ?", authorityID, menuIDs).Find(&buttons).Error; err != nil {
@@ -228,7 +235,9 @@ func (r *userRepo) MenusByAuthority(ctx context.Context, authorityID uint) ([]*b
}
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})
value := menuButtonFromPO(button)
value.AuthorityID = authorityID
menu.Buttons = append(menu.Buttons, value)
}
}
roots := make([]*biz.Menu, 0)
@@ -236,7 +245,7 @@ func (r *userRepo) MenusByAuthority(ctx context.Context, authorityID uint) ([]*b
menu := byID[po.ID]
if parent := byID[po.ParentID]; parent != nil {
parent.Children = append(parent.Children, menu)
} else {
} else if po.ParentID == 0 {
roots = append(roots, menu)
}
}
@@ -302,6 +311,16 @@ func (r *userRepo) CreateUserWithAuthorities(ctx context.Context, user *biz.User
authorityIDs = []uint{user.AuthorityID}
}
if err := r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
// The compatible registration rejects an existing non-deleted username before creating the
// account. The username column is intentionally not unique for legacy
// schema compatibility, so keep the behavioral check here rather than
// relying on a database constraint.
var existing userPO
if err := tx.Where("username = ?", user.Username).First(&existing).Error; err == nil {
return errors.New("用户名已注册")
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
if err := validateAuthorities(tx, authorityIDs); err != nil {
return err
}
+97 -68
View File
@@ -2,8 +2,7 @@ package data
import (
"context"
"errors"
"strings"
"sort"
"time"
"gorm.io/gorm"
@@ -38,7 +37,7 @@ func (r *versionRepo) CreateVersion(ctx context.Context, v *biz.Version) error {
return nil
}
func (r *versionRepo) DeleteVersions(ctx context.Context, ids []uint) error {
return r.data.gormDB.WithContext(ctx).Delete(&versionPO{}, ids).Error
return r.data.gormDB.WithContext(ctx).Where("id IN ?", ids).Delete(&versionPO{}).Error
}
func (r *versionRepo) FindVersion(ctx context.Context, id uint) (*biz.Version, error) {
var po versionPO
@@ -77,24 +76,27 @@ func (r *versionRepo) BuildVersionBundle(ctx context.Context, menuIDs, apiIDs, d
var menus []menuPO
if len(menuIDs) > 0 {
if err := r.data.gormDB.WithContext(ctx).Where("id IN ?", menuIDs).Order("sort,id").Find(&menus).Error; err != nil {
return nil, err
return nil, &biz.VersionStageError{Stage: biz.VersionStageMenus, Err: err}
}
}
byID := map[uint]*biz.Menu{}
for _, po := range menus {
v := menuFromPO(po)
v.Children = []*biz.Menu{}
var btns []menuButtonPO
_ = r.data.gormDB.WithContext(ctx).Where("sys_base_menu_id = ?", po.ID).Find(&btns).Error
for _, btn := range btns {
v.Buttons = append(v.Buttons, &biz.MenuButton{Name: btn.Name, Description: btn.Description})
}
byID[po.ID] = v
}
if len(menuIDs) > 0 {
var buttons []menuButtonPO
if err := r.data.gormDB.WithContext(ctx).Where("sys_base_menu_id IN ?", menuIDs).Find(&buttons).Error; err != nil {
return nil, &biz.VersionStageError{Stage: biz.VersionStageMenus, Err: err}
}
for _, button := range buttons {
if menu := byID[button.MenuID]; menu != nil {
menu.Buttons = append(menu.Buttons, &biz.MenuButton{Name: button.Name, Description: button.Description})
}
}
var parameters []menuParameterPO
if err := r.data.gormDB.WithContext(ctx).Where("sys_base_menu_id IN ?", menuIDs).Find(&parameters).Error; err != nil {
return nil, err
return nil, &biz.VersionStageError{Stage: biz.VersionStageMenus, Err: err}
}
for _, parameter := range parameters {
if menu := byID[parameter.MenuID]; menu != nil {
@@ -110,6 +112,14 @@ func (r *versionRepo) BuildVersionBundle(ctx context.Context, menuIDs, apiIDs, d
bundle.Menus = append(bundle.Menus, v)
}
}
var sortMenus func([]*biz.Menu)
sortMenus = func(items []*biz.Menu) {
sort.Slice(items, func(i, j int) bool { return items[i].Sort < items[j].Sort })
for _, item := range items {
sortMenus(item.Children)
}
}
sortMenus(bundle.Menus)
var cleanMenu func(*biz.Menu)
cleanMenu = func(menu *biz.Menu) {
menu.ID = 0
@@ -133,7 +143,7 @@ func (r *versionRepo) BuildVersionBundle(ctx context.Context, menuIDs, apiIDs, d
var apis []apiPO
if len(apiIDs) > 0 {
if err := r.data.gormDB.WithContext(ctx).Where("id IN ?", apiIDs).Find(&apis).Error; err != nil {
return nil, err
return nil, &biz.VersionStageError{Stage: biz.VersionStageAPIs, Err: err}
}
}
for _, po := range apis {
@@ -144,7 +154,7 @@ func (r *versionRepo) BuildVersionBundle(ctx context.Context, menuIDs, apiIDs, d
var dictionaries []dictionaryPO
if len(dictIDs) > 0 {
if err := r.data.gormDB.WithContext(ctx).Where("id IN ?", dictIDs).Find(&dictionaries).Error; err != nil {
return nil, err
return nil, &biz.VersionStageError{Stage: biz.VersionStageDictionaries, Err: err}
}
}
dictionariesRepo := &dictionaryRepo{data: r.data}
@@ -153,7 +163,7 @@ func (r *versionRepo) BuildVersionBundle(ctx context.Context, menuIDs, apiIDs, d
v.ID = 0
details, _, err := dictionariesRepo.ListDictionaryDetails(ctx, 0, 0, biz.DictionaryDetailFilter{DictionaryID: po.ID})
if err != nil {
return nil, err
return nil, &biz.VersionStageError{Stage: biz.VersionStageDictionaries, Err: err}
}
for _, d := range details {
d.ID = 0
@@ -168,67 +178,86 @@ func (r *versionRepo) BuildVersionBundle(ctx context.Context, menuIDs, apiIDs, d
return bundle, nil
}
func (r *versionRepo) ImportVersionBundle(ctx context.Context, b *biz.VersionBundle) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var addMenus func([]*biz.Menu, uint) error
addMenus = func(items []*biz.Menu, parent uint) error {
for _, item := range items {
var existing menuPO
err := tx.Where("name = ? AND path = ?", item.Name, item.Path).First(&existing).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
po := menuToPO(item)
po.ID = 0
po.ParentID = parent
if err = tx.Create(&po).Error; err != nil {
return err
}
existing = po
for _, button := range item.Buttons {
if err = tx.Create(&menuButtonPO{Name: button.Name, Description: button.Description, MenuID: po.ID}).Error; err != nil {
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
}
if err = addMenus(item.Children, existing.ID); err != nil {
return err
}
}
return nil
db := r.data.gormDB.WithContext(ctx)
if len(b.Menus) > 0 {
if err := db.Transaction(func(tx *gorm.DB) error { return importVersionMenus(tx, b.Menus, 0) }); err != nil {
return &biz.VersionStageError{Stage: biz.VersionStageMenus, Err: err}
}
if err := addMenus(b.Menus, 0); err != nil {
}
if len(b.APIs) > 0 {
if err := db.Transaction(func(tx *gorm.DB) error { return importVersionAPIs(tx, b.APIs) }); err != nil {
return &biz.VersionStageError{Stage: biz.VersionStageAPIs, Err: err}
}
}
if len(b.Dictionaries) > 0 {
if err := db.Transaction(func(tx *gorm.DB) error { return importVersionDictionaries(tx, b.Dictionaries) }); err != nil {
return &biz.VersionStageError{Stage: biz.VersionStageDictionaries, Err: err}
}
}
return nil
}
func importVersionMenus(tx *gorm.DB, items []*biz.Menu, parentID uint) error {
for _, item := range items {
var existing menuPO
if err := tx.Where("name = ? AND path = ?", item.Name, item.Path).First(&existing).Error; err == nil {
if err = importVersionMenus(tx, item.Children, existing.ID); err != nil {
return err
}
continue
}
po := menuToPO(item)
po.ID = 0
po.ParentID = parentID
if err := tx.Create(&po).Error; err != nil {
return err
}
for _, item := range b.APIs {
po := apiPO{Path: item.Path, Description: item.Description, APIGroup: item.APIGroup, Method: strings.ToUpper(item.Method)}
if err := tx.Where("path = ? AND method = ?", po.Path, po.Method).FirstOrCreate(&po).Error; err != nil {
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
}
}
for _, item := range b.Dictionaries {
var count int64
if err := tx.Model(&dictionaryPO{}).Where("type = ?", item.Type).Count(&count).Error; err != nil {
for _, button := range item.Buttons {
if err := tx.Create(&menuButtonPO{Name: button.Name, Description: button.Description, MenuID: po.ID}).Error; err != nil {
return err
}
if count > 0 {
continue
}
po := dictionaryPO{Name: item.Name, Type: item.Type, Status: item.Status, Desc: item.Desc}
if err := tx.Create(&po).Error; err != nil {
return err
}
for _, detail := range item.Details {
d := dictionaryDetailPO{Label: detail.Label, Value: detail.Value, Extend: detail.Extend, Status: detail.Status, Sort: detail.Sort, DictionaryID: po.ID}
if err := tx.Create(&d).Error; err != nil {
return err
}
}
}
return nil
})
if err := importVersionMenus(tx, item.Children, po.ID); err != nil {
return err
}
}
return nil
}
func importVersionAPIs(tx *gorm.DB, items []*biz.API) error {
for _, item := range items {
var existing apiPO
if err := tx.Where("path = ? AND method = ?", item.Path, item.Method).First(&existing).Error; err == nil {
continue
}
if err := tx.Create(&apiPO{Path: item.Path, Description: item.Description, APIGroup: item.APIGroup, Method: item.Method}).Error; err != nil {
return err
}
}
return nil
}
func importVersionDictionaries(tx *gorm.DB, items []*biz.Dictionary) error {
for _, item := range items {
var existing dictionaryPO
if err := tx.Where("type = ?", item.Type).First(&existing).Error; err == nil {
continue
}
po := dictionaryPO{Name: item.Name, Type: item.Type, Status: item.Status, Desc: item.Desc}
if err := tx.Create(&po).Error; err != nil {
return err
}
for _, detail := range item.Details {
value := dictionaryDetailPO{Label: detail.Label, Value: detail.Value, Extend: detail.Extend, Status: detail.Status, Sort: detail.Sort, DictionaryID: po.ID}
if err := tx.Create(&value).Error; err != nil {
return err
}
}
}
return nil
}