fix
This commit is contained in:
@@ -19,10 +19,10 @@ type apiPO struct {
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
Path string `gorm:"uniqueIndex:idx_api_path_method"`
|
||||
Path string
|
||||
Description string
|
||||
APIGroup string `gorm:"column:api_group"`
|
||||
Method string `gorm:"uniqueIndex:idx_api_path_method"`
|
||||
Method string
|
||||
}
|
||||
|
||||
func (apiPO) TableName() string { return "sys_apis" }
|
||||
|
||||
@@ -17,7 +17,7 @@ type apiTokenPO struct {
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
UserID uint
|
||||
AuthorityID uint
|
||||
Token string `gorm:"type:text;uniqueIndex"`
|
||||
Token string `gorm:"type:text"`
|
||||
Status bool
|
||||
ExpiresAt time.Time
|
||||
Remark string
|
||||
|
||||
+40
-41
@@ -318,53 +318,52 @@ func (r *accessRepo) DataScopeDepartmentIDs(ctx context.Context, id uint) ([]uin
|
||||
return ids, err
|
||||
}
|
||||
func (r *accessRepo) ResolveDataScope(ctx context.Context, authorityID, userID uint) (biz.DataScope, error) {
|
||||
if authorityID == 888 {
|
||||
return biz.DataScope{All: true}, nil
|
||||
}
|
||||
identity := biz.DataScope{UserID: userID, AuthorityID: authorityID}
|
||||
var user userPO
|
||||
_ = r.data.gormDB.WithContext(ctx).Select("id", "dept_id").First(&user, userID).Error
|
||||
identity.PrimaryDeptID = user.DeptID
|
||||
var authority authorityPO
|
||||
if err := r.data.gormDB.WithContext(ctx).First(&authority, "authority_id = ?", authorityID).Error; err != nil {
|
||||
return biz.DataScope{}, err
|
||||
_ = r.data.gormDB.WithContext(ctx).Select("authority_id", "data_scope").First(&authority, "authority_id = ?", authorityID).Error
|
||||
identity.Scope = authority.DataScope
|
||||
if identity.Scope == 0 {
|
||||
identity.Scope = 1
|
||||
}
|
||||
if authority.DataScope == 1 {
|
||||
return biz.DataScope{All: true}, nil
|
||||
}
|
||||
if authority.DataScope == 4 {
|
||||
return biz.DataScope{OwnerUserID: userID}, nil
|
||||
identity.All = identity.Scope == 1
|
||||
if identity.Scope == 4 {
|
||||
identity.OwnerUserID = userID
|
||||
}
|
||||
var ids []uint
|
||||
if authority.DataScope == 5 {
|
||||
var err error
|
||||
ids, err = r.DataScopeDepartmentIDs(ctx, authorityID)
|
||||
if err != nil {
|
||||
return biz.DataScope{}, err
|
||||
}
|
||||
} else {
|
||||
if err := r.data.gormDB.WithContext(ctx).Model(&userDepartmentPO{}).Where("sys_user_id = ?", userID).Pluck("sys_department_id", &ids).Error; err != nil {
|
||||
return biz.DataScope{}, err
|
||||
}
|
||||
if authority.DataScope == 2 && len(ids) > 0 {
|
||||
var departments []departmentPO
|
||||
if err := r.data.gormDB.WithContext(ctx).Find(&departments).Error; err != nil {
|
||||
return biz.DataScope{}, err
|
||||
}
|
||||
selected := make(map[uint]bool, len(ids))
|
||||
for _, id := range ids {
|
||||
selected[id] = true
|
||||
}
|
||||
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
|
||||
}
|
||||
_ = r.data.gormDB.WithContext(ctx).Model(&userDepartmentPO{}).Where("sys_user_id = ?", userID).Pluck("sys_department_id", &ids).Error
|
||||
selected := make(map[uint]bool, len(ids)+1)
|
||||
for _, id := range ids {
|
||||
selected[id] = true
|
||||
}
|
||||
if user.DeptID != 0 {
|
||||
selected[user.DeptID] = true
|
||||
}
|
||||
ids = ids[:0]
|
||||
for id := range selected {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
if identity.Scope == 2 && len(ids) > 0 {
|
||||
var departments []departmentPO
|
||||
_ = r.data.gormDB.WithContext(ctx).Find(&departments).Error
|
||||
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
|
||||
}
|
||||
}
|
||||
ids = ids[:0]
|
||||
for id := range selected {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
}
|
||||
ids = ids[:0]
|
||||
for id := range selected {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
} else if identity.Scope == 5 {
|
||||
ids, _ = r.DataScopeDepartmentIDs(ctx, authorityID)
|
||||
}
|
||||
return biz.DataScope{DepartmentIDs: ids}, nil
|
||||
identity.DepartmentIDs = ids
|
||||
return identity, nil
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ func NewCache(data *Data) biz.Cache {
|
||||
return &cacheStore{data: data, memory: make(map[string]memoryCacheEntry)}
|
||||
}
|
||||
|
||||
func (s *cacheStore) client() *redis.Client {
|
||||
func (s *cacheStore) client() redis.UniversalClient {
|
||||
return s.data.redis.load()
|
||||
}
|
||||
|
||||
|
||||
@@ -160,6 +160,14 @@ func (d *Data) reloadConfig(ctx context.Context) error {
|
||||
}
|
||||
useRedis := next.Admin.System != nil && next.Admin.System.UseRedis
|
||||
candidateRedis := openRedis(next.Data.Redis, useRedis)
|
||||
useMongo := next.Admin.System != nil && next.Admin.System.UseMongo
|
||||
candidateMongo, mongoErr := openMongo(next.Data.Mongo, useMongo)
|
||||
mongoAccepted := false
|
||||
defer func() {
|
||||
if !mongoAccepted && candidateMongo != nil {
|
||||
_ = candidateMongo.Disconnect(context.Background())
|
||||
}
|
||||
}()
|
||||
candidateDBList, err := openDatabaseList(next.Data.DatabaseList)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -168,6 +176,10 @@ func (d *Data) reloadConfig(ctx context.Context) error {
|
||||
d.gormDB.replace(candidateDB)
|
||||
d.replaceDatabaseList(candidateDBList)
|
||||
d.redis.replace(candidateRedis)
|
||||
if mongoErr == nil {
|
||||
d.mongo.replace(candidateMongo)
|
||||
mongoAccepted = true
|
||||
}
|
||||
d.runtime.Replace(next.Data, next.Admin)
|
||||
if d.storage != nil {
|
||||
d.storage.replace(candidateStorage)
|
||||
|
||||
+28
-9
@@ -20,6 +20,7 @@ type Data struct {
|
||||
configMu sync.Mutex
|
||||
gormDB *reloadableDB
|
||||
redis *reloadableRedis
|
||||
mongo *reloadableMongo
|
||||
runtime *conf.Runtime
|
||||
storage *reloadableStorage
|
||||
dbListMu sync.RWMutex
|
||||
@@ -41,6 +42,7 @@ func openDatabaseList(configs []*conf.Data_Database) (map[string]*gorm.DB, error
|
||||
}
|
||||
return nil, fmt.Errorf("open database %q: %w", config.AliasName, err)
|
||||
}
|
||||
registerDataScopeCallbacks(db)
|
||||
items[config.AliasName] = db
|
||||
}
|
||||
return items, nil
|
||||
@@ -108,28 +110,45 @@ func NewData(runtime *conf.Runtime) (*Data, func(), error) {
|
||||
}
|
||||
useRedis := admin != nil && admin.System != nil && admin.System.UseRedis
|
||||
d.redis = newReloadableRedis(openRedis(c.Redis, useRedis))
|
||||
useMongo := admin != nil && admin.System != nil && admin.System.UseMongo
|
||||
mongoClient, err := openMongo(c.Mongo, useMongo)
|
||||
if err != nil {
|
||||
log.Printf("mongo unavailable: %v", err)
|
||||
mongoClient = nil
|
||||
}
|
||||
d.mongo = newReloadableMongo(mongoClient)
|
||||
stopConfigWatcher := d.watchConfig()
|
||||
cleanup := func() {
|
||||
stopConfigWatcher()
|
||||
d.gormDB.close()
|
||||
closeDatabaseList(d.dbList)
|
||||
d.redis.close()
|
||||
d.mongo.close()
|
||||
}
|
||||
return d, cleanup, nil
|
||||
}
|
||||
|
||||
func openRedis(config *conf.Data_Redis, enabled bool) *redis.Client {
|
||||
if !enabled || config == nil || config.Addr == "" {
|
||||
func openRedis(config *conf.Data_Redis, enabled bool) redis.UniversalClient {
|
||||
if !enabled || config == nil || (config.Addr == "" && len(config.ClusterAddrs) == 0) {
|
||||
return nil
|
||||
}
|
||||
options := &redis.Options{Addr: config.Addr, Network: config.Network}
|
||||
if config.ReadTimeout != nil {
|
||||
options.ReadTimeout = config.ReadTimeout.AsDuration()
|
||||
var candidate redis.UniversalClient
|
||||
if config.UseCluster {
|
||||
addresses := config.ClusterAddrs
|
||||
if len(addresses) == 0 && config.Addr != "" {
|
||||
addresses = []string{config.Addr}
|
||||
}
|
||||
candidate = redis.NewClusterClient(&redis.ClusterOptions{Addrs: addresses, Password: config.Password})
|
||||
} else {
|
||||
options := &redis.Options{Addr: config.Addr, Network: config.Network, Password: config.Password, DB: int(config.Db)}
|
||||
if config.ReadTimeout != nil {
|
||||
options.ReadTimeout = config.ReadTimeout.AsDuration()
|
||||
}
|
||||
if config.WriteTimeout != nil {
|
||||
options.WriteTimeout = config.WriteTimeout.AsDuration()
|
||||
}
|
||||
candidate = redis.NewClient(options)
|
||||
}
|
||||
if config.WriteTimeout != nil {
|
||||
options.WriteTimeout = config.WriteTimeout.AsDuration()
|
||||
}
|
||||
candidate := redis.NewClient(options)
|
||||
pingCtx, cancel := context.WithTimeout(context.Background(), 800*time.Millisecond)
|
||||
defer cancel()
|
||||
if err := candidate.Ping(pingCtx).Err(); err != nil {
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"kra/internal/biz"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"gorm.io/gorm/schema"
|
||||
)
|
||||
|
||||
// registerDataScopeCallbacks installs the global GORM data-scope engine.
|
||||
// System tables are deliberately excluded: their access is controlled by
|
||||
// Casbin, while ownership columns on business tables are row-level scope.
|
||||
func registerDataScopeCallbacks(db *gorm.DB) {
|
||||
if db == nil {
|
||||
return
|
||||
}
|
||||
q := db.Callback().Query()
|
||||
if q.Get("data_scope:query") == nil {
|
||||
_ = q.Before("gorm:query").Register("data_scope:query", applyDataScope("query"))
|
||||
}
|
||||
u := db.Callback().Update()
|
||||
if u.Get("data_scope:update") == nil {
|
||||
_ = u.Before("gorm:update").Register("data_scope:update", applyDataScope("update"))
|
||||
_ = u.Before("gorm:update").Register("data_scope:stamp_update", stampUpdatedBy)
|
||||
_ = u.After("gorm:update").Register("data_scope:audit_update", auditBlockedWrite("update"))
|
||||
}
|
||||
d := db.Callback().Delete()
|
||||
if d.Get("data_scope:delete") == nil {
|
||||
_ = d.Before("gorm:delete").Register("data_scope:delete", applyDataScope("delete"))
|
||||
_ = d.Before("gorm:delete").After("data_scope:delete").Register("data_scope:stamp_delete", stampDeletedBy)
|
||||
_ = d.After("gorm:delete").Register("data_scope:audit_delete", auditBlockedWrite("delete"))
|
||||
}
|
||||
c := db.Callback().Create()
|
||||
if c.Get("data_scope:stamp") == nil {
|
||||
_ = c.Before("gorm:create").Register("data_scope:stamp", stampOwnership)
|
||||
}
|
||||
}
|
||||
|
||||
func hasScopeField(db *gorm.DB, name string) bool {
|
||||
return db.Statement.Schema != nil && db.Statement.Schema.LookUpField(name) != nil
|
||||
}
|
||||
|
||||
func isControlledTable(db *gorm.DB) bool {
|
||||
return db.Statement.Schema != nil && !strings.HasPrefix(db.Statement.Table, "sys_") && (hasScopeField(db, "dept_id") || hasScopeField(db, "created_by"))
|
||||
}
|
||||
|
||||
func applyDataScope(operation string) func(*gorm.DB) {
|
||||
return func(db *gorm.DB) {
|
||||
if !isControlledTable(db) {
|
||||
return
|
||||
}
|
||||
if _, done := db.Statement.Clauses["data_scope:applied"]; done {
|
||||
return
|
||||
}
|
||||
if skip, ok := db.Get("data_scope:skip"); ok {
|
||||
if value, _ := skip.(bool); value {
|
||||
return
|
||||
}
|
||||
}
|
||||
scope, ok := biz.DataScopeFromContext(db.Statement.Context)
|
||||
if !ok {
|
||||
log.Printf("数据权限: 业务表[%s]访问无身份上下文, 已放行(待补 ctx / 或使用系统上下文)", db.Statement.Table)
|
||||
recordDataScopeEvent(db, "no_identity", operation, "无身份上下文访问受控表, 已放行", biz.DataScope{})
|
||||
return
|
||||
}
|
||||
if (operation == "update" || operation == "delete") && !db.AllowGlobalUpdate && !hasWriteConditions(db) {
|
||||
return
|
||||
}
|
||||
db.Statement.Clauses["data_scope:applied"] = clause.Clause{}
|
||||
if scope.All {
|
||||
return
|
||||
}
|
||||
table := db.Statement.Table
|
||||
if scope.OwnerUserID != 0 && hasScopeField(db, "created_by") {
|
||||
db.Where(table+".created_by = ?", scope.OwnerUserID)
|
||||
return
|
||||
}
|
||||
if hasScopeField(db, "dept_id") {
|
||||
ids := scope.DepartmentIDs
|
||||
if len(ids) == 0 {
|
||||
ids = []uint{0}
|
||||
}
|
||||
db.Where(table+".dept_id IN ?", ids)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func recordDataScopeEvent(db *gorm.DB, eventType, operation, detail string, scope biz.DataScope) {
|
||||
record := &dataAccessLogPO{EventType: eventType, TargetTable: db.Statement.Table, Operation: operation, UserID: scope.UserID, AuthorityID: scope.AuthorityID, Scope: scope.Scope, RequestID: scope.RequestID, Method: scope.Method, Path: scope.Path, Detail: detail}
|
||||
_ = db.Session(&gorm.Session{NewDB: true, SkipHooks: true}).Create(record).Error
|
||||
}
|
||||
|
||||
func auditBlockedWrite(operation string) func(*gorm.DB) {
|
||||
return func(db *gorm.DB) {
|
||||
if _, applied := db.Statement.Clauses["data_scope:applied"]; !applied || db.Error != nil || db.RowsAffected != 0 {
|
||||
return
|
||||
}
|
||||
if scope, ok := biz.DataScopeFromContext(db.Statement.Context); ok && !scope.All {
|
||||
recordDataScopeEvent(db, "blocked_write", operation, "数据范围过滤后写操作影响 0 行(疑似越权尝试)", scope)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stampOwnership(db *gorm.DB) {
|
||||
if !isControlledTable(db) {
|
||||
return
|
||||
}
|
||||
scope, ok := biz.DataScopeFromContext(db.Statement.Context)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if hasScopeField(db, "created_by") && scope.UserID != 0 {
|
||||
db.Statement.SetColumn("created_by", scope.UserID, true)
|
||||
}
|
||||
if hasScopeField(db, "dept_id") && scope.PrimaryDeptID != 0 {
|
||||
db.Statement.SetColumn("dept_id", scope.PrimaryDeptID, true)
|
||||
}
|
||||
}
|
||||
|
||||
func stampUpdatedBy(db *gorm.DB) {
|
||||
stmt := db.Statement
|
||||
if !isControlledTable(db) || stmt.SkipHooks || !hasScopeField(db, "updated_by") {
|
||||
return
|
||||
}
|
||||
scope, ok := biz.DataScopeFromContext(db.Statement.Context)
|
||||
if !ok || scope.UserID == 0 {
|
||||
return
|
||||
}
|
||||
if _, isMap := stmt.Dest.(map[string]interface{}); !isMap {
|
||||
if _, isMaps := stmt.Dest.([]map[string]interface{}); !isMaps && stmt.ReflectValue.Kind() == reflect.Struct && !stmt.ReflectValue.CanAddr() {
|
||||
return
|
||||
}
|
||||
}
|
||||
for _, omitted := range stmt.Omits {
|
||||
if omitted == "updated_by" {
|
||||
return
|
||||
}
|
||||
}
|
||||
if len(stmt.Selects) > 0 {
|
||||
found := false
|
||||
for _, selected := range stmt.Selects {
|
||||
if selected == "updated_by" || selected == "*" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
stmt.Selects = append(stmt.Selects, "updated_by")
|
||||
}
|
||||
}
|
||||
stmt.SetColumn("updated_by", scope.UserID, true)
|
||||
}
|
||||
|
||||
func stampDeletedBy(db *gorm.DB) {
|
||||
stmt := db.Statement
|
||||
if db.Error != nil || !isControlledTable(db) || stmt.SQL.Len() != 0 || stmt.Unscoped || !hasScopeField(db, "deleted_by") {
|
||||
return
|
||||
}
|
||||
deletedAtType := reflect.TypeOf(gorm.DeletedAt{})
|
||||
var deletedAt *schema.Field
|
||||
for _, field := range stmt.Schema.Fields {
|
||||
if field.FieldType == deletedAtType || field.IndirectFieldType == deletedAtType {
|
||||
deletedAt = field
|
||||
break
|
||||
}
|
||||
}
|
||||
if deletedAt == nil {
|
||||
return
|
||||
}
|
||||
if _, customZero := deletedAt.TagSettings["ZEROVALUE"]; customZero {
|
||||
return
|
||||
}
|
||||
scope, ok := biz.DataScopeFromContext(stmt.Context)
|
||||
if !ok || scope.UserID == 0 {
|
||||
return
|
||||
}
|
||||
var primaryExpressions []clause.Expression
|
||||
_, queryValues := schema.GetIdentityFieldValuesMap(stmt.Context, stmt.ReflectValue, stmt.Schema.PrimaryFields)
|
||||
column, values := schema.ToQueryValues(stmt.Table, stmt.Schema.PrimaryFieldDBNames, queryValues)
|
||||
if len(values) > 0 {
|
||||
primaryExpressions = append(primaryExpressions, clause.IN{Column: column, Values: values})
|
||||
}
|
||||
if stmt.ReflectValue.CanAddr() && stmt.Dest != stmt.Model && stmt.Model != nil {
|
||||
_, queryValues = schema.GetIdentityFieldValuesMap(stmt.Context, reflect.ValueOf(stmt.Model), stmt.Schema.PrimaryFields)
|
||||
column, values = schema.ToQueryValues(stmt.Table, stmt.Schema.PrimaryFieldDBNames, queryValues)
|
||||
if len(values) > 0 {
|
||||
primaryExpressions = append(primaryExpressions, clause.IN{Column: column, Values: values})
|
||||
}
|
||||
}
|
||||
if _, hasWhere := stmt.Clauses["WHERE"]; !hasWhere && len(primaryExpressions) == 0 && !db.AllowGlobalUpdate {
|
||||
return
|
||||
}
|
||||
now := db.NowFunc()
|
||||
stmt.AddClause(clause.Set{{Column: clause.Column{Name: deletedAt.DBName}, Value: now}, {Column: clause.Column{Name: "deleted_by"}, Value: scope.UserID}})
|
||||
stmt.SetColumn(deletedAt.DBName, now, true)
|
||||
stmt.SetColumn("deleted_by", scope.UserID, true)
|
||||
if len(primaryExpressions) > 0 {
|
||||
stmt.AddClause(clause.Where{Exprs: primaryExpressions})
|
||||
}
|
||||
gorm.SoftDeleteQueryClause{ZeroValue: sql.NullString{Valid: false}, Field: deletedAt}.ModifyStatement(stmt)
|
||||
stmt.AddClauseIfNotExists(clause.Update{})
|
||||
stmt.Build(stmt.DB.Callback().Update().Clauses...)
|
||||
}
|
||||
|
||||
func hasWriteConditions(db *gorm.DB) bool {
|
||||
if value, ok := db.Statement.Clauses["WHERE"]; ok {
|
||||
if where, ok := value.Expression.(clause.Where); ok && len(where.Exprs) > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if db.Statement.Schema == nil || !db.Statement.ReflectValue.IsValid() {
|
||||
return false
|
||||
}
|
||||
_, values := schema.GetIdentityFieldValuesMap(db.Statement.Context, db.Statement.ReflectValue, db.Statement.Schema.PrimaryFields)
|
||||
if _, query := schema.ToQueryValues(db.Statement.Table, db.Statement.Schema.PrimaryFieldDBNames, values); len(query) > 0 {
|
||||
return true
|
||||
}
|
||||
if db.Statement.ReflectValue.CanAddr() && db.Statement.Dest != db.Statement.Model && db.Statement.Model != nil {
|
||||
_, values = schema.GetIdentityFieldValuesMap(db.Statement.Context, reflect.ValueOf(db.Statement.Model), db.Statement.Schema.PrimaryFields)
|
||||
_, query := schema.ToQueryValues(db.Statement.Table, db.Statement.Schema.PrimaryFieldDBNames, values)
|
||||
return len(query) > 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
oracle "github.com/dzwvip/gorm-oracle"
|
||||
"github.com/glebarez/sqlite"
|
||||
@@ -15,6 +16,8 @@ import (
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/driver/sqlserver"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
"gorm.io/gorm/schema"
|
||||
"kra/internal/conf"
|
||||
)
|
||||
|
||||
@@ -99,21 +102,61 @@ func databaseDSN(c *conf.Data_Database, name string) (string, error) {
|
||||
return "", fmt.Errorf("unsupported database driver %q", c.Driver)
|
||||
}
|
||||
|
||||
func gormConfig(config *conf.Data_Database) *gorm.Config {
|
||||
level := logger.Info
|
||||
switch strings.ToLower(config.LogMode) {
|
||||
case "silent":
|
||||
level = logger.Silent
|
||||
case "error":
|
||||
level = logger.Error
|
||||
case "warn":
|
||||
level = logger.Warn
|
||||
}
|
||||
return &gorm.Config{Logger: logger.Default.LogMode(level), NamingStrategy: schema.NamingStrategy{TablePrefix: config.Prefix, SingularTable: config.Singular}}
|
||||
}
|
||||
|
||||
func openWithDriver(driver, dsn string) (*gorm.DB, error) {
|
||||
return openWithDriverConfig(driver, dsn, &conf.Data_Database{Driver: driver})
|
||||
}
|
||||
|
||||
func openWithDriverConfig(driver, dsn string, config *conf.Data_Database) (*gorm.DB, error) {
|
||||
gormConfig := gormConfig(config)
|
||||
var db *gorm.DB
|
||||
var err error
|
||||
switch normalizedDriver(driver) {
|
||||
case "mysql":
|
||||
return gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||
db, err = gorm.Open(mysql.Open(dsn), gormConfig)
|
||||
case "pgsql":
|
||||
return gorm.Open(postgres.Open(dsn), &gorm.Config{})
|
||||
db, err = gorm.Open(postgres.Open(dsn), gormConfig)
|
||||
case "mssql":
|
||||
return gorm.Open(sqlserver.Open(dsn), &gorm.Config{})
|
||||
db, err = gorm.Open(sqlserver.Open(dsn), gormConfig)
|
||||
case "oracle":
|
||||
return gorm.Open(oracle.Open(dsn), &gorm.Config{})
|
||||
db, err = gorm.Open(oracle.Open(dsn), gormConfig)
|
||||
case "sqlite":
|
||||
return gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
db, err = gorm.Open(sqlite.Open(dsn), gormConfig)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported database driver %q", driver)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if config.MaxIdleConns > 0 {
|
||||
sqlDB.SetMaxIdleConns(int(config.MaxIdleConns))
|
||||
}
|
||||
if config.MaxOpenConns > 0 {
|
||||
sqlDB.SetMaxOpenConns(int(config.MaxOpenConns))
|
||||
}
|
||||
if config.ConnMaxLifetime > 0 {
|
||||
sqlDB.SetConnMaxLifetime(time.Duration(config.ConnMaxLifetime) * time.Second)
|
||||
}
|
||||
if config.Engine != "" && (normalizedDriver(driver) == "mysql" || normalizedDriver(driver) == "mssql") {
|
||||
db = db.Set("gorm:table_options", "ENGINE="+config.Engine)
|
||||
}
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func openDatabase(c *conf.Data_Database, create bool, template string) (*gorm.DB, error) {
|
||||
@@ -129,7 +172,7 @@ func openDatabase(c *conf.Data_Database, create bool, template string) (*gorm.DB
|
||||
if err = os.MkdirAll(filepath.Dir(dsn), 0o755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return openWithDriver(driver, dsn)
|
||||
return openWithDriverConfig(driver, dsn, c)
|
||||
}
|
||||
if create && driver != "oracle" {
|
||||
if !databaseNamePattern.MatchString(c.Name) {
|
||||
@@ -146,7 +189,7 @@ func openDatabase(c *conf.Data_Database, create bool, template string) (*gorm.DB
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
adminDB, err := openWithDriver(driver, dsn)
|
||||
adminDB, err := openWithDriverConfig(driver, dsn, c)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("connect database server: %w", err)
|
||||
}
|
||||
@@ -182,7 +225,7 @@ func openDatabase(c *conf.Data_Database, create bool, template string) (*gorm.DB
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return openWithDriver(driver, dsn)
|
||||
return openWithDriverConfig(driver, dsn, c)
|
||||
}
|
||||
|
||||
func openFallbackDatabase() (*gorm.DB, error) {
|
||||
|
||||
@@ -18,7 +18,7 @@ type dictionaryPO struct {
|
||||
UpdatedAt time.Time
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
Name string
|
||||
Type string `gorm:"uniqueIndex"`
|
||||
Type string
|
||||
Status bool
|
||||
Desc string
|
||||
ParentID *uint
|
||||
|
||||
@@ -45,14 +45,7 @@ func (r *emailRepo) DefaultRecipients() []string {
|
||||
if config == nil {
|
||||
return nil
|
||||
}
|
||||
parts := strings.Split(config.To, ",")
|
||||
result := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
if recipient := strings.TrimSpace(part); recipient != "" {
|
||||
result = append(result, recipient)
|
||||
}
|
||||
}
|
||||
return result
|
||||
return []string{config.To}
|
||||
}
|
||||
|
||||
func cleanHeader(value string) string {
|
||||
|
||||
@@ -20,7 +20,7 @@ type exportTemplatePO struct {
|
||||
DBName string
|
||||
Name string
|
||||
DBTableName string `gorm:"column:table_name"`
|
||||
TemplateID string `gorm:"uniqueIndex"`
|
||||
TemplateID string
|
||||
TemplateInfo string `gorm:"type:text"`
|
||||
SQL string `gorm:"type:text"`
|
||||
ImportSQL string `gorm:"type:text"`
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
package data
|
||||
|
||||
import "gorm.io/gorm"
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func migrateAll(db *gorm.DB) error {
|
||||
return db.AutoMigrate(
|
||||
if err := db.AutoMigrate(
|
||||
&userPO{}, &authorityPO{}, &menuPO{}, &userAuthorityPO{}, &authorityMenuPO{}, &menuParameterPO{},
|
||||
&apiPO{}, &ignoredAPIPO{}, &authorityAPIPO{}, &menuButtonPO{}, &authorityButtonPO{},
|
||||
&departmentPO{}, &positionPO{}, &userDepartmentPO{}, &userPositionPO{}, &authorityDepartmentPO{},
|
||||
@@ -12,5 +16,66 @@ func migrateAll(db *gorm.DB) error {
|
||||
&operationPO{}, &loginLogPO{}, &dataAccessLogPO{}, &errorRecordPO{},
|
||||
&taskPO{}, &taskLogPO{}, &mediaPO{}, &categoryPO{}, &uploadSessionPO{}, &uploadChunkPO{},
|
||||
&announcementPO{},
|
||||
)
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
return reconcileReferenceIndexes(db)
|
||||
}
|
||||
|
||||
// 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.
|
||||
func reconcileReferenceIndexes(db *gorm.DB) error {
|
||||
obsolete := []struct {
|
||||
model any
|
||||
name string
|
||||
}{
|
||||
{&apiPO{}, "idx_api_path_method"},
|
||||
{&dictionaryPO{}, "idx_sys_dictionaries_type"},
|
||||
{¶meterPO{}, "idx_sys_params_key"},
|
||||
{&apiTokenPO{}, "idx_sys_api_tokens_token"},
|
||||
{&exportTemplatePO{}, "idx_sys_export_templates_template_id"},
|
||||
}
|
||||
for _, item := range obsolete {
|
||||
if db.Migrator().HasIndex(item.model, item.name) {
|
||||
if err := db.Migrator().DropIndex(item.model, item.name); err != nil {
|
||||
return fmt.Errorf("drop obsolete index %s: %w", item.name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, item := range []struct {
|
||||
name string
|
||||
field string
|
||||
}{{"idx_sys_users_uuid", "UUID"}, {"idx_sys_users_username", "Username"}} {
|
||||
unique, err := indexIsUnique(db, &userPO{}, item.name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !unique {
|
||||
continue
|
||||
}
|
||||
if err = db.Migrator().DropIndex(&userPO{}, item.name); err != nil {
|
||||
return fmt.Errorf("drop legacy unique index %s: %w", item.name, err)
|
||||
}
|
||||
if err = db.Migrator().CreateIndex(&userPO{}, item.field); err != nil {
|
||||
return fmt.Errorf("create reference index %s: %w", item.name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func indexIsUnique(db *gorm.DB, model any, name string) (bool, error) {
|
||||
indexes, err := db.Migrator().GetIndexes(model)
|
||||
if err != nil {
|
||||
// Some third-party GORM drivers do not implement index inspection.
|
||||
// Fresh schemas are already correct; skip only the legacy repair there.
|
||||
return false, nil
|
||||
}
|
||||
for _, index := range indexes {
|
||||
if index.Name() == name {
|
||||
unique, known := index.Unique()
|
||||
return known && unique, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"kra/internal/conf"
|
||||
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
func mongoURI(config *conf.Data_Mongo) string {
|
||||
hosts := make([]string, 0, len(config.Hosts))
|
||||
for _, host := range config.Hosts {
|
||||
if host != nil && host.Host != "" && host.Port != "" {
|
||||
hosts = append(hosts, host.Host+":"+host.Port)
|
||||
}
|
||||
}
|
||||
uri := "mongodb://" + strings.Join(hosts, ",") + "/" + config.Database
|
||||
if config.Options != "" {
|
||||
uri += "?" + config.Options
|
||||
}
|
||||
return uri
|
||||
}
|
||||
|
||||
func openMongo(config *conf.Data_Mongo, enabled bool) (*mongo.Client, error) {
|
||||
if !enabled {
|
||||
return nil, nil
|
||||
}
|
||||
if config == nil || len(config.Hosts) == 0 {
|
||||
return nil, fmt.Errorf("mongo hosts are required")
|
||||
}
|
||||
clientOptions := options.Client().ApplyURI(mongoURI(config))
|
||||
if config.Username != "" && config.Password != "" {
|
||||
clientOptions.SetAuth(options.Credential{Username: config.Username, Password: config.Password, AuthSource: config.AuthSource})
|
||||
}
|
||||
if config.MinPoolSize > 0 {
|
||||
clientOptions.SetMinPoolSize(config.MinPoolSize)
|
||||
}
|
||||
if config.MaxPoolSize > 0 {
|
||||
clientOptions.SetMaxPoolSize(config.MaxPoolSize)
|
||||
}
|
||||
if config.ConnectTimeoutMs > 0 {
|
||||
clientOptions.SetConnectTimeout(time.Duration(config.ConnectTimeoutMs) * time.Millisecond)
|
||||
}
|
||||
if config.SocketTimeoutMs > 0 {
|
||||
clientOptions.SetSocketTimeout(time.Duration(config.SocketTimeoutMs) * time.Millisecond)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
client, err := mongo.Connect(ctx, clientOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = client.Ping(ctx, nil); err != nil {
|
||||
_ = client.Disconnect(context.Background())
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
@@ -16,7 +16,7 @@ type parameterPO struct {
|
||||
UpdatedAt time.Time
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
Name string
|
||||
Key string `gorm:"uniqueIndex"`
|
||||
Key string
|
||||
Value string
|
||||
Desc string
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -17,8 +18,40 @@ type reloadableDB struct {
|
||||
retired []*gorm.DB
|
||||
}
|
||||
|
||||
type reloadableMongo struct {
|
||||
mu sync.RWMutex
|
||||
current *mongo.Client
|
||||
retired []*mongo.Client
|
||||
}
|
||||
|
||||
func newReloadableMongo(client *mongo.Client) *reloadableMongo {
|
||||
return &reloadableMongo{current: client}
|
||||
}
|
||||
func (r *reloadableMongo) replace(client *mongo.Client) {
|
||||
r.mu.Lock()
|
||||
old := r.current
|
||||
r.current = client
|
||||
if old != nil && old != client {
|
||||
r.retired = append(r.retired, old)
|
||||
}
|
||||
r.mu.Unlock()
|
||||
}
|
||||
func (r *reloadableMongo) close() {
|
||||
r.mu.Lock()
|
||||
all := append([]*mongo.Client{r.current}, r.retired...)
|
||||
r.current = nil
|
||||
r.retired = nil
|
||||
r.mu.Unlock()
|
||||
for _, client := range all {
|
||||
if client != nil {
|
||||
_ = client.Disconnect(context.Background())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newReloadableDB(db *gorm.DB) *reloadableDB {
|
||||
r := &reloadableDB{}
|
||||
registerDataScopeCallbacks(db)
|
||||
r.current.Store(db)
|
||||
return r
|
||||
}
|
||||
@@ -30,6 +63,7 @@ func (r *reloadableDB) WithContext(ctx context.Context) *gorm.DB {
|
||||
func (r *reloadableDB) DB() *gorm.DB { return r.current.Load() }
|
||||
|
||||
func (r *reloadableDB) replace(db *gorm.DB) {
|
||||
registerDataScopeCallbacks(db)
|
||||
old := r.current.Swap(db)
|
||||
if old != nil && old != db {
|
||||
r.mu.Lock()
|
||||
@@ -60,45 +94,41 @@ func (r *reloadableDB) close() {
|
||||
}
|
||||
|
||||
type reloadableRedis struct {
|
||||
current atomic.Pointer[redis.Client]
|
||||
mu sync.Mutex
|
||||
retired []*redis.Client
|
||||
mu sync.RWMutex
|
||||
current redis.UniversalClient
|
||||
retired []redis.UniversalClient
|
||||
}
|
||||
|
||||
func newReloadableRedis(client *redis.Client) *reloadableRedis {
|
||||
r := &reloadableRedis{}
|
||||
if client != nil {
|
||||
r.current.Store(client)
|
||||
}
|
||||
return r
|
||||
func newReloadableRedis(client redis.UniversalClient) *reloadableRedis {
|
||||
return &reloadableRedis{current: client}
|
||||
}
|
||||
|
||||
func (r *reloadableRedis) load() *redis.Client { return r.current.Load() }
|
||||
func (r *reloadableRedis) load() redis.UniversalClient {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
return r.current
|
||||
}
|
||||
|
||||
func (r *reloadableRedis) replace(client *redis.Client) {
|
||||
old := r.current.Swap(client)
|
||||
func (r *reloadableRedis) replace(client redis.UniversalClient) {
|
||||
r.mu.Lock()
|
||||
old := r.current
|
||||
r.current = client
|
||||
if old != nil && old != client {
|
||||
r.mu.Lock()
|
||||
r.retired = append(r.retired, old)
|
||||
r.mu.Unlock()
|
||||
}
|
||||
r.mu.Unlock()
|
||||
}
|
||||
|
||||
func (r *reloadableRedis) close() {
|
||||
current := r.current.Load()
|
||||
r.mu.Lock()
|
||||
all := append([]*redis.Client{current}, r.retired...)
|
||||
all := append([]redis.UniversalClient{r.current}, r.retired...)
|
||||
r.current = nil
|
||||
r.retired = nil
|
||||
r.mu.Unlock()
|
||||
seen := map[*redis.Client]struct{}{}
|
||||
for _, client := range all {
|
||||
if client == nil {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[client]; ok {
|
||||
continue
|
||||
}
|
||||
seen[client] = struct{}{}
|
||||
_ = client.Close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ type userPO struct {
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
UUID string `gorm:"type:char(36);uniqueIndex"`
|
||||
Username string `gorm:"index;uniqueIndex"`
|
||||
UUID string `gorm:"type:char(36);index"`
|
||||
Username string `gorm:"index"`
|
||||
Password string
|
||||
NickName string `gorm:"column:nick_name"`
|
||||
HeaderImg string `gorm:"column:header_img"`
|
||||
|
||||
@@ -182,6 +182,17 @@ func (r *systemRepo) Initialize(ctx context.Context, input *biz.DatabaseConfig)
|
||||
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"},
|
||||
} {
|
||||
if err := tx.FirstOrCreate(&ignored, ignored).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
@@ -204,11 +215,16 @@ func defaultMenus() []menuPO {
|
||||
child := func(parent, path, name, component, title, icon string, sort int) menuPO {
|
||||
return menuPO{MenuLevel: 1, Path: path, Name: name, Component: component, Title: title, Icon: icon, Sort: sort, ActiveName: parent}
|
||||
}
|
||||
cachedChild := func(parent, path, name, component, title, icon string, sort int) menuPO {
|
||||
value := child(parent, path, name, component, title, icon, sort)
|
||||
value.KeepAlive = true
|
||||
return value
|
||||
}
|
||||
return []menuPO{
|
||||
{Path: "dashboard", Name: "dashboard", Component: "view/dashboard/index.vue", Title: "仪表盘", Icon: "odometer", Sort: 1},
|
||||
root("permission", "permission", "权限管理", "perm-kra", 2), root("org", "org", "组织管理", "share", 3), root("systemConfig", "systemConfig", "系统设置", "config-kra", 4), root("monitor", "monitor", "运维监控", "monitor-kra", 5), root("media", "media", "媒体管理", "folder-opened", 6), root("extensions", "extensions", "扩展功能", "cherry", 10),
|
||||
{Path: "person", Name: "person", Component: "view/person/person.vue", Title: "个人信息", Icon: "postcard", Hidden: true, Sort: 13},
|
||||
child("permission", "authority", "authority", "view/superAdmin/authority/authority.vue", "角色管理", "role-kra", 1), child("permission", "menu", "menu", "view/superAdmin/menu/menu.vue", "菜单管理", "tickets", 2), child("permission", "api", "api", "view/superAdmin/api/api.vue", "api管理", "api-kra", 3), child("permission", "apiToken", "apiToken", "view/systemTools/apiToken/index.vue", "API Token", "key", 4),
|
||||
child("permission", "authority", "authority", "view/superAdmin/authority/authority.vue", "角色管理", "role-kra", 1), cachedChild("permission", "menu", "menu", "view/superAdmin/menu/menu.vue", "菜单管理", "tickets", 2), cachedChild("permission", "api", "api", "view/superAdmin/api/api.vue", "api管理", "api-kra", 3), child("permission", "apiToken", "apiToken", "view/systemTools/apiToken/index.vue", "API Token", "key", 4),
|
||||
child("org", "user", "user", "view/superAdmin/user/user.vue", "用户管理", "user", 1), child("org", "department", "department", "view/superAdmin/department/department.vue", "部门管理", "office-building", 2), child("org", "position", "position", "view/superAdmin/position/position.vue", "岗位管理", "postcard", 3),
|
||||
child("systemConfig", "system", "system", "view/systemTools/system/system.vue", "配置文件", "config-file-kra", 1), child("systemConfig", "dictionary", "dictionary", "view/superAdmin/dictionary/sysDictionary.vue", "字典管理", "notebook", 2), child("systemConfig", "sysParams", "sysParams", "view/superAdmin/params/sysParams.vue", "参数管理", "set-up", 3), child("systemConfig", "security", "security", "view/system/security/index.vue", "安全配置", "security-kra", 4),
|
||||
child("monitor", "operation", "operation", "view/superAdmin/operation/sysOperationRecord.vue", "操作历史", "document", 1), child("monitor", "loginLog", "loginLog", "view/systemTools/loginLog/index.vue", "登录日志", "clock", 2), child("monitor", "sysError", "sysError", "view/systemTools/sysError/sysError.vue", "错误日志", "error-kra", 3), child("monitor", "sysVersion", "sysVersion", "view/systemTools/version/version.vue", "版本管理", "version-kra", 4), child("monitor", "state", "state", "view/system/state.vue", "服务器状态", "server", 5), child("monitor", "dataAccessLog", "dataAccessLog", "view/superAdmin/dataAccessLog/dataAccessLog.vue", "数据权限审计", "warning", 6), child("monitor", "timedTask", "timedTask", "view/systemTools/timedTask/index.vue", "定时任务", "timer", 7), child("monitor", "logViewer", "logViewer", "view/systemTools/logViewer/index.vue", "文件日志", "document", 8),
|
||||
|
||||
Reference in New Issue
Block a user