优化结构

This commit is contained in:
Yvan
2026-08-20 20:46:18 +08:00
parent 01b5c2f78a
commit 23bc29ce0f
67 changed files with 1594 additions and 465 deletions
-13
View File
@@ -1,13 +0,0 @@
package data
import "kra/internal/biz"
type auditQueryRepo struct{ data *Data }
type auditRecorderRepo struct{ data *Data }
type logFileRepo struct{ data *Data }
func NewAuditRepo(data *Data) biz.AuditQueryRepo { return &auditQueryRepo{data: data} }
func NewAuditRecorderRepo(data *Data) biz.AuditRecordRepo { return &auditRecorderRepo{data: data} }
func NewLogFileRepo(data *Data) biz.LogFileRepo { return &logFileRepo{data: data} }
+3 -2
View File
@@ -10,6 +10,7 @@ import (
"strconv" "strconv"
"kra/internal/conf" "kra/internal/conf"
"kra/internal/integration/storage"
"google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
@@ -375,7 +376,7 @@ func (d *Data) reloadConfig(ctx context.Context) error {
return fmt.Errorf("reload email configuration: %w", err) return fmt.Errorf("reload email configuration: %w", err)
} }
next.Admin.Email = emailConfig next.Admin.Email = emailConfig
candidateStorage, err := buildFileStorage(next.Admin) candidateStorage, err := storage.New(next.Admin)
if err != nil { if err != nil {
return fmt.Errorf("reload storage: %w", err) return fmt.Errorf("reload storage: %w", err)
} }
@@ -410,7 +411,7 @@ func (d *Data) reloadConfig(ctx context.Context) error {
} }
d.runtime.Replace(next.Data, next.Admin) d.runtime.Replace(next.Data, next.Admin)
if d.storage != nil { if d.storage != nil {
d.storage.replace(candidateStorage) d.storage.Replace(candidateStorage)
} }
closeCandidate = false closeCandidate = false
return nil return nil
+22
View File
@@ -0,0 +1,22 @@
package data
import (
"time"
"gorm.io/gorm"
)
// dataAccessLogPO is the infrastructure-side write model used by GORM
// callbacks. The system module owns the query repository for the same table.
type dataAccessLogPO struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
EventType, TargetTable, Operation string
UserID, AuthorityID uint
Scope int
RequestID, Method, Path, Detail string
}
func (dataAccessLogPO) TableName() string { return "sys_data_access_logs" }
-65
View File
@@ -1,65 +0,0 @@
package data
import (
"context"
"io"
"sync"
"kra/internal/biz"
"kra/internal/conf"
datastorage "kra/internal/data/storage"
)
type reloadableStorage struct {
mu sync.RWMutex
current biz.FileStorage
}
func NewFileStorage(data *Data) (biz.FileStorage, error) {
storage, err := buildFileStorage(data.runtime.Admin())
if err != nil {
return nil, err
}
wrapper := &reloadableStorage{current: storage}
data.storage = wrapper
return wrapper, nil
}
func buildFileStorage(config *conf.AdminBackend) (biz.FileStorage, error) {
return datastorage.New(config)
}
func (s *reloadableStorage) replace(storage biz.FileStorage) {
s.mu.Lock()
s.current = storage
s.mu.Unlock()
}
func (s *reloadableStorage) Put(ctx context.Context, name string, reader io.Reader) (*biz.StoredFile, error) {
s.mu.RLock()
defer s.mu.RUnlock()
return s.current.Put(ctx, name, reader)
}
func (s *reloadableStorage) Open(ctx context.Context, name string) (io.ReadCloser, error) {
s.mu.RLock()
defer s.mu.RUnlock()
return s.current.Open(ctx, name)
}
func (s *reloadableStorage) Delete(ctx context.Context, name string) error {
s.mu.RLock()
defer s.mu.RUnlock()
return s.current.Delete(ctx, name)
}
func (s *reloadableStorage) Compose(ctx context.Context, names []string, destination string) (*biz.StoredFile, string, error) {
s.mu.RLock()
defer s.mu.RUnlock()
return s.current.Compose(ctx, names, destination)
}
func (s *reloadableStorage) DeletePrefix(ctx context.Context, prefix string) error {
s.mu.RLock()
defer s.mu.RUnlock()
return s.current.DeletePrefix(ctx, prefix)
}
func (s *reloadableStorage) List(ctx context.Context, prefix, cursor string, limit int) ([]*biz.StoredFile, string, bool, error) {
s.mu.RLock()
defer s.mu.RUnlock()
return s.current.List(ctx, prefix, cursor, limit)
}
+9
View File
@@ -0,0 +1,9 @@
package data
import "kra/internal/biz"
type initializationRepo struct{ data *Data }
func NewInitializationRepo(data *Data) biz.InitializationRepo {
return &initializationRepo{data: data}
}
+3 -2
View File
@@ -8,6 +8,7 @@ import (
"testing" "testing"
"kra/internal/conf" "kra/internal/conf"
"kra/internal/integration/storage"
"google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/encoding/protojson"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
@@ -239,7 +240,7 @@ func TestPersistRuntimeConfigReplacesActiveStorage(t *testing.T) {
Local: &conf.AdminBackend_Local{StorePath: oldRoot, PathPrefix: "old-files"}, Local: &conf.AdminBackend_Local{StorePath: oldRoot, PathPrefix: "old-files"},
Storage: &conf.AdminBackend_Storage{Type: "local"}, Storage: &conf.AdminBackend_Storage{Type: "local"},
} }
currentStorage, err := buildFileStorage(currentAdmin) currentStorage, err := storage.NewReloadable(currentAdmin)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -248,7 +249,7 @@ func TestPersistRuntimeConfigReplacesActiveStorage(t *testing.T) {
d := &Data{ d := &Data{
runtime: conf.NewRuntime(&conf.Data{}, currentAdmin), runtime: conf.NewRuntime(&conf.Data{}, currentAdmin),
gormDB: reloadableDB, gormDB: reloadableDB,
storage: &reloadableStorage{current: currentStorage}, storage: currentStorage,
} }
d.databaseReady.Store(true) d.databaseReady.Store(true)
+3
View File
@@ -24,5 +24,8 @@ func migrateAll(db *gorm.DB) error {
} }
return datasystem.CurrentDataMigration(db) return datasystem.CurrentDataMigration(db)
}}, }},
{ID: "202608200003_payment_admin_surface", Migrate: func(db *gorm.DB) error {
return datasystem.EnsureAdminSurface(db, datapayment.AdminSurface())
}},
}) })
} }
+35
View File
@@ -0,0 +1,35 @@
package payment
import (
"kra/internal/data/system"
"gorm.io/gorm"
)
// Migrate creates the persistence owned by the payment module.
func Migrate(db *gorm.DB) error {
return db.AutoMigrate(&integrationConfigPO{}, &paymentOrderPO{})
}
// Reconcile seeds disabled configuration rows for all built-in providers.
func Reconcile(db *gorm.DB) error {
return ensurePaymentIntegrationConfigs(db)
}
// AdminSurface describes the payment-owned entries shown in the system
// administration UI. The system module persists these records because it owns
// the menu/API/policy tables.
func AdminSurface() system.AdminSurface {
return system.AdminSurface{
Menus: []system.AdminMenu{
{Name: "paymentOrders", Path: "paymentOrders", ParentName: "extensions", Component: "view/systemTools/payment/orders.vue", Title: "支付订单", Icon: "wallet", Sort: 6},
{Name: "paymentConfig", Path: "paymentConfig", ParentName: "extensions", Component: "view/systemTools/payment/config.vue", Title: "支付配置", Icon: "credit-card", Sort: 7},
},
APIs: []system.AdminAPI{
{Path: "/payment/configs", Method: "GET", APIGroup: "支付", Description: "获取支付渠道配置"},
{Path: "/payment/config", Method: "POST", APIGroup: "支付", Description: "保存支付渠道配置"},
{Path: "/payment/orders", Method: "GET", APIGroup: "支付", Description: "分页查询支付订单"},
{Path: "/payment/order", Method: "POST", APIGroup: "支付", Description: "查询支付订单"},
},
}
}
+17
View File
@@ -0,0 +1,17 @@
package payment
import "time"
const integrationKindPayment = "payment"
type integrationConfigPO struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
Kind string `gorm:"size:32;not null;uniqueIndex:idx_integration_kind_provider"`
Provider string `gorm:"size:64;not null;uniqueIndex:idx_integration_kind_provider"`
Enabled bool `gorm:"not null;default:false;index"`
Config string `gorm:"type:text;not null"`
}
func (integrationConfigPO) TableName() string { return "sys_integration_configs" }
@@ -1,4 +1,4 @@
package data package payment
import ( import (
"context" "context"
@@ -12,14 +12,14 @@ import (
"strings" "strings"
"kra/internal/biz" "kra/internal/biz"
datapayment "kra/internal/data/payment" datapayment "kra/internal/integration/payment"
"gorm.io/gorm" "gorm.io/gorm"
) )
type paymentRepo struct{ data *Data } type paymentRepo struct{ data Provider }
func NewPaymentRepo(data *Data) biz.PaymentRepo { return &paymentRepo{data: data} } func NewPaymentRepo(data Provider) biz.PaymentRepo { return &paymentRepo{data: data} }
func ensurePaymentIntegrationConfigs(db *gorm.DB) error { func ensurePaymentIntegrationConfigs(db *gorm.DB) error {
for _, provider := range biz.SupportedPaymentProviders { for _, provider := range biz.SupportedPaymentProviders {
@@ -38,7 +38,7 @@ func ensurePaymentIntegrationConfigs(db *gorm.DB) error {
func (r *paymentRepo) row(ctx context.Context, provider string) (*integrationConfigPO, map[string]any, error) { func (r *paymentRepo) row(ctx context.Context, provider string) (*integrationConfigPO, map[string]any, error) {
var row integrationConfigPO var row integrationConfigPO
if err := r.data.gormDB.WithContext(ctx).Where("kind = ? AND provider = ?", integrationKindPayment, provider).First(&row).Error; err != nil { if err := r.data.DB().WithContext(ctx).Where("kind = ? AND provider = ?", integrationKindPayment, provider).First(&row).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil, biz.ErrPaymentProviderNotFound return nil, nil, biz.ErrPaymentProviderNotFound
} }
@@ -56,7 +56,7 @@ func (r *paymentRepo) row(ctx context.Context, provider string) (*integrationCon
func (r *paymentRepo) ListConfigs(ctx context.Context) ([]*biz.PaymentConfig, error) { func (r *paymentRepo) ListConfigs(ctx context.Context) ([]*biz.PaymentConfig, error) {
var rows []integrationConfigPO var rows []integrationConfigPO
if err := r.data.gormDB.WithContext(ctx).Where("kind = ?", integrationKindPayment).Order("provider ASC").Find(&rows).Error; err != nil { if err := r.data.DB().WithContext(ctx).Where("kind = ?", integrationKindPayment).Order("provider ASC").Find(&rows).Error; err != nil {
return nil, err return nil, err
} }
out := make([]*biz.PaymentConfig, 0, len(rows)) out := make([]*biz.PaymentConfig, 0, len(rows))
@@ -84,7 +84,7 @@ func (r *paymentRepo) SaveConfig(ctx context.Context, config *biz.PaymentConfig)
if err := json.Unmarshal(config.Values, &values); err != nil { if err := json.Unmarshal(config.Values, &values); err != nil {
return errors.New("支付配置必须是 JSON 对象") return errors.New("支付配置必须是 JSON 对象")
} }
db := r.data.gormDB.WithContext(ctx) db := r.data.DB().WithContext(ctx)
var row integrationConfigPO var row integrationConfigPO
err := db.Where("kind = ? AND provider = ?", integrationKindPayment, config.Provider).First(&row).Error err := db.Where("kind = ? AND provider = ?", integrationKindPayment, config.Provider).First(&row).Error
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -1,4 +1,4 @@
package data package payment
import ( import (
"context" "context"
@@ -1,4 +1,4 @@
package data package payment
import "kra/pkg/paymentkit" import "kra/pkg/paymentkit"
@@ -1,4 +1,4 @@
package data package payment
import ( import (
"context" "context"
@@ -1,4 +1,4 @@
package data package payment
import ( import (
"context" "context"
@@ -11,6 +11,7 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/clause" "gorm.io/gorm/clause"
"kra/internal/biz" "kra/internal/biz"
"kra/pkg/pagination"
) )
type paymentOrderPO struct { type paymentOrderPO struct {
@@ -64,9 +65,9 @@ type paymentOrderPO struct {
func (paymentOrderPO) TableName() string { return "pay_orders" } func (paymentOrderPO) TableName() string { return "pay_orders" }
type paymentOrderRepo struct{ data *Data } type paymentOrderRepo struct{ data Provider }
func NewPaymentOrderRepo(data *Data) biz.PaymentOrderRepo { func NewPaymentOrderRepo(data Provider) biz.PaymentOrderRepo {
return &paymentOrderRepo{data: data} return &paymentOrderRepo{data: data}
} }
@@ -134,7 +135,7 @@ func (r *paymentOrderRepo) CreatePaymentOrder(ctx context.Context, order *biz.Pa
if err != nil { if err != nil {
return nil, false, err return nil, false, err
} }
db := r.data.gormDB.WithContext(ctx) db := r.data.DB().WithContext(ctx)
var current paymentOrderPO var current paymentOrderPO
err = db.Where("provider = ? AND trade_no = ?", po.Provider, po.TradeNo).First(&current).Error err = db.Where("provider = ? AND trade_no = ?", po.Provider, po.TradeNo).First(&current).Error
if err == nil { if err == nil {
@@ -154,7 +155,7 @@ func (r *paymentOrderRepo) CreatePaymentOrder(ctx context.Context, order *biz.Pa
func (r *paymentOrderRepo) FindPaymentOrder(ctx context.Context, provider, tradeNo string) (*biz.PaymentOrder, error) { func (r *paymentOrderRepo) FindPaymentOrder(ctx context.Context, provider, tradeNo string) (*biz.PaymentOrder, error) {
var po paymentOrderPO var po paymentOrderPO
if err := r.data.gormDB.WithContext(ctx).Where("provider = ? AND trade_no = ?", provider, tradeNo).First(&po).Error; err != nil { if err := r.data.DB().WithContext(ctx).Where("provider = ? AND trade_no = ?", provider, tradeNo).First(&po).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, biz.ErrPaymentOrderNotFound return nil, biz.ErrPaymentOrderNotFound
} }
@@ -163,6 +164,41 @@ func (r *paymentOrderRepo) FindPaymentOrder(ctx context.Context, provider, trade
return toBizPaymentOrder(&po), nil return toBizPaymentOrder(&po), nil
} }
func (r *paymentOrderRepo) ListPaymentOrders(ctx context.Context, page, pageSize int, filter biz.PaymentOrderFilter) ([]*biz.PaymentOrder, int64, error) {
db := r.data.DB().WithContext(ctx).Model(&paymentOrderPO{})
if value := strings.TrimSpace(filter.Provider); value != "" {
db = db.Where("provider = ?", value)
}
if value := strings.TrimSpace(filter.TradeNo); value != "" {
db = db.Where("trade_no LIKE ?", "%"+value+"%")
}
if value := strings.TrimSpace(filter.BusinessType); value != "" {
db = db.Where("business_type = ?", value)
}
if value := strings.TrimSpace(filter.BusinessID); value != "" {
db = db.Where("business_id LIKE ?", "%"+value+"%")
}
if value := strings.TrimSpace(filter.PaymentStatus); value != "" {
db = db.Where("payment_status = ?", value)
}
if value := strings.TrimSpace(filter.RefundStatus); value != "" {
db = db.Where("refund_status = ?", value)
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
var rows []paymentOrderPO
if err := pagination.ApplyRequired(db.Order("id desc"), page, pageSize, 100).Find(&rows).Error; err != nil {
return nil, 0, err
}
items := make([]*biz.PaymentOrder, 0, len(rows))
for i := range rows {
items = append(items, toBizPaymentOrder(&rows[i]))
}
return items, total, nil
}
func (r *paymentOrderRepo) RecordPaymentCreate(ctx context.Context, provider, tradeNo string, update *biz.PaymentProviderUpdate) (*biz.PaymentOrder, error) { func (r *paymentOrderRepo) RecordPaymentCreate(ctx context.Context, provider, tradeNo string, update *biz.PaymentProviderUpdate) (*biz.PaymentOrder, error) {
return r.withLockedOrder(ctx, provider, tradeNo, func(tx *gorm.DB, po *paymentOrderPO) error { return r.withLockedOrder(ctx, provider, tradeNo, func(tx *gorm.DB, po *paymentOrderPO) error {
if update == nil { if update == nil {
@@ -396,7 +432,7 @@ func (r *paymentOrderRepo) ConfirmPaymentRefund(ctx context.Context, provider, t
func (r *paymentOrderRepo) withLockedOrder(ctx context.Context, provider, tradeNo string, fn func(*gorm.DB, *paymentOrderPO) error) (*biz.PaymentOrder, error) { func (r *paymentOrderRepo) withLockedOrder(ctx context.Context, provider, tradeNo string, fn func(*gorm.DB, *paymentOrderPO) error) (*biz.PaymentOrder, error) {
var result *biz.PaymentOrder var result *biz.PaymentOrder
err := r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { err := r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var po paymentOrderPO var po paymentOrderPO
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("provider = ? AND trade_no = ?", provider, tradeNo).First(&po).Error; err != nil { if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("provider = ? AND trade_no = ?", provider, tradeNo).First(&po).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -1,4 +1,4 @@
package data package payment
import ( import (
"context" "context"
@@ -91,3 +91,27 @@ func TestPaymentOrderRepositoryRejectsProviderTradeReuse(t *testing.T) {
t.Fatalf("provider trade reuse err = %v", err) t.Fatalf("provider trade reuse err = %v", err)
} }
} }
func TestPaymentOrderRepositoryListsWithFilters(t *testing.T) {
repo := newPaymentOrderRepoForTest(t)
ctx := context.Background()
first := testPaymentOrder()
if _, _, err := repo.CreatePaymentOrder(ctx, first); err != nil {
t.Fatal(err)
}
second := testPaymentOrder()
second.TradeNo = "wechat-order-2"
second.Provider = biz.PaymentWechatV3
second.BusinessID = "item-2"
second.ConfirmationID = "33333333-3333-3333-3333-333333333333"
if _, _, err := repo.CreatePaymentOrder(ctx, second); err != nil {
t.Fatal(err)
}
items, total, err := repo.ListPaymentOrders(ctx, 1, 10, biz.PaymentOrderFilter{Provider: biz.PaymentWechatV3, TradeNo: "wechat", BusinessID: "item-2"})
if err != nil {
t.Fatal(err)
}
if total != 1 || len(items) != 1 || items[0].TradeNo != second.TradeNo {
t.Fatalf("filtered orders = %#v total=%d", items, total)
}
}
+11
View File
@@ -0,0 +1,11 @@
package payment
import (
"gorm.io/gorm"
)
// Provider is the narrow persistence seam required by payment repositories.
// Keeping it here lets payment remain an independent data module.
type Provider interface {
DB() *gorm.DB
}
@@ -0,0 +1,67 @@
package payment
import (
"fmt"
"testing"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
"kra/internal/biz"
)
type Data struct{ gormDB *reloadableDB }
type reloadableDB struct{ db *gorm.DB }
func newReloadableDB(db *gorm.DB, _ any) *reloadableDB { return &reloadableDB{db: db} }
func (r *reloadableDB) close() {
if r == nil || r.db == nil {
return
}
if sqlDB, err := r.db.DB(); err == nil {
_ = sqlDB.Close()
}
}
func (d *Data) DB() *gorm.DB { return d.gormDB.db }
func openWithDriver(driver, dsn string) (*gorm.DB, error) {
if driver != "sqlite" {
return nil, fmt.Errorf("unsupported test database driver %q", driver)
}
return gorm.Open(sqlite.Open(dsn), &gorm.Config{})
}
func openIntegrationConfigTestDB(t *testing.T) *gorm.DB {
t.Helper()
db, err := openWithDriver("sqlite", "file:"+t.Name()+"?mode=memory&cache=shared")
if err != nil {
t.Fatal(err)
}
if err := db.AutoMigrate(&integrationConfigPO{}); err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if sqlDB, err := db.DB(); err == nil {
_ = sqlDB.Close()
}
})
return db
}
func migrateAll(db *gorm.DB) error {
if err := db.AutoMigrate(&integrationConfigPO{}, &paymentOrderPO{}); err != nil {
return err
}
for _, provider := range biz.SupportedPaymentProviders {
var count int64
if err := db.Model(&integrationConfigPO{}).Where("kind = ? AND provider = ?", integrationKindPayment, provider).Count(&count).Error; err != nil {
return err
}
if count == 0 {
if err := db.Create(&integrationConfigPO{Kind: integrationKindPayment, Provider: provider, Config: "{}"}).Error; err != nil {
return err
}
}
}
return nil
}
@@ -1,4 +1,4 @@
package data package system
import ( import (
"context" "context"
@@ -6,6 +6,8 @@ import (
"time" "time"
"kra/internal/biz" "kra/internal/biz"
"kra/pkg/gormkit"
"kra/pkg/pagination"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -18,17 +20,17 @@ type announcementPO struct {
Title string Title string
Content string `gorm:"type:text"` Content string `gorm:"type:text"`
UserID *int `gorm:"column:user_id"` UserID *int `gorm:"column:user_id"`
Attachments jsonPO Attachments gormkit.JSON
} }
func (announcementPO) TableName() string { return "kra_announcements_info" } func (announcementPO) TableName() string { return "kra_announcements_info" }
type announcementRepo struct{ data *Data } type announcementRepo struct{ data Provider }
func NewAnnouncementRepo(data *Data) biz.AnnouncementRepo { return &announcementRepo{data: data} } func NewAnnouncementRepo(data Provider) biz.AnnouncementRepo { return &announcementRepo{data: data} }
func newAnnouncement(item *biz.Announcement) announcementPO { func newAnnouncement(item *biz.Announcement) announcementPO {
return announcementPO{ID: item.ID, Title: item.Title, Content: item.Content, UserID: item.UserID, Attachments: jsonPO(item.Attachments)} return announcementPO{ID: item.ID, Title: item.Title, Content: item.Content, UserID: item.UserID, Attachments: gormkit.JSON(item.Attachments)}
} }
func announcementToBiz(item announcementPO) *biz.Announcement { func announcementToBiz(item announcementPO) *biz.Announcement {
@@ -37,7 +39,7 @@ func announcementToBiz(item announcementPO) *biz.Announcement {
func (r *announcementRepo) Create(ctx context.Context, item *biz.Announcement) error { func (r *announcementRepo) Create(ctx context.Context, item *biz.Announcement) error {
po := newAnnouncement(item) po := newAnnouncement(item)
if err := r.data.gormDB.WithContext(ctx).Create(&po).Error; err != nil { if err := r.data.DB().WithContext(ctx).Create(&po).Error; err != nil {
return err return err
} }
item.ID, item.CreatedAt, item.UpdatedAt = po.ID, po.CreatedAt, po.UpdatedAt item.ID, item.CreatedAt, item.UpdatedAt = po.ID, po.CreatedAt, po.UpdatedAt
@@ -45,28 +47,28 @@ func (r *announcementRepo) Create(ctx context.Context, item *biz.Announcement) e
} }
func (r *announcementRepo) Delete(ctx context.Context, id string) error { func (r *announcementRepo) Delete(ctx context.Context, id string) error {
return r.data.gormDB.WithContext(ctx).Delete(&announcementPO{}, "id = ?", id).Error return r.data.DB().WithContext(ctx).Delete(&announcementPO{}, "id = ?", id).Error
} }
func (r *announcementRepo) DeleteByIDs(ctx context.Context, ids []string) error { func (r *announcementRepo) DeleteByIDs(ctx context.Context, ids []string) error {
return r.data.gormDB.WithContext(ctx).Delete(&[]announcementPO{}, "id IN ?", ids).Error return r.data.DB().WithContext(ctx).Delete(&[]announcementPO{}, "id IN ?", ids).Error
} }
func (r *announcementRepo) Update(ctx context.Context, item *biz.Announcement) error { func (r *announcementRepo) Update(ctx context.Context, item *biz.Announcement) error {
po := newAnnouncement(item) po := newAnnouncement(item)
return r.data.gormDB.WithContext(ctx).Model(&announcementPO{}).Where("id = ?", item.ID).Updates(&po).Error return r.data.DB().WithContext(ctx).Model(&announcementPO{}).Where("id = ?", item.ID).Updates(&po).Error
} }
func (r *announcementRepo) Find(ctx context.Context, id string) (*biz.Announcement, error) { func (r *announcementRepo) Find(ctx context.Context, id string) (*biz.Announcement, error) {
var po announcementPO var po announcementPO
if err := r.data.gormDB.WithContext(ctx).Where("id = ?", id).First(&po).Error; err != nil { if err := r.data.DB().WithContext(ctx).Where("id = ?", id).First(&po).Error; err != nil {
return nil, err return nil, err
} }
return announcementToBiz(po), nil return announcementToBiz(po), nil
} }
func (r *announcementRepo) List(ctx context.Context, filter biz.AnnouncementFilter) ([]*biz.Announcement, int64, error) { func (r *announcementRepo) List(ctx context.Context, filter biz.AnnouncementFilter) ([]*biz.Announcement, int64, error) {
db := r.data.gormDB.WithContext(ctx).Model(&announcementPO{}) db := r.data.DB().WithContext(ctx).Model(&announcementPO{})
if filter.StartCreatedAt != nil && filter.EndCreatedAt != nil { if filter.StartCreatedAt != nil && filter.EndCreatedAt != nil {
db = db.Where("created_at BETWEEN ? AND ?", filter.StartCreatedAt, filter.EndCreatedAt) db = db.Where("created_at BETWEEN ? AND ?", filter.StartCreatedAt, filter.EndCreatedAt)
} }
@@ -75,7 +77,7 @@ func (r *announcementRepo) List(ctx context.Context, filter biz.AnnouncementFilt
return nil, 0, err return nil, 0, err
} }
if filter.PageSize > 0 { if filter.PageSize > 0 {
db = applyPagination(db, filter.Page, filter.PageSize, 100) db = pagination.Apply(db, filter.Page, filter.PageSize, 100)
} }
var pos []announcementPO var pos []announcementPO
if err := db.Find(&pos).Error; err != nil { if err := db.Find(&pos).Error; err != nil {
@@ -95,7 +97,7 @@ func (r *announcementRepo) UserOptions(ctx context.Context) ([]biz.UserOption, e
} }
// The generated data-source endpoint is best effort: return collected // The generated data-source endpoint is best effort: return collected
// options even when the underlying scan reports an error. // 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 _ = r.data.DB().WithContext(ctx).Table("sys_users").Select("nick_name AS label, id AS value").Scan(&rows).Error
items := make([]biz.UserOption, 0, len(rows)) items := make([]biz.UserOption, 0, len(rows))
for _, row := range rows { for _, row := range rows {
items = append(items, biz.UserOption{Label: row.Label, Value: row.Value}) items = append(items, biz.UserOption{Label: row.Label, Value: row.Value})
@@ -1,4 +1,4 @@
package data package system
import ( import (
"context" "context"
@@ -1,4 +1,4 @@
package data package system
import ( import (
"context" "context"
@@ -7,13 +7,14 @@ import (
"time" "time"
"kra/internal/biz" "kra/internal/biz"
"kra/pkg/pagination"
"gorm.io/gorm" "gorm.io/gorm"
) )
type apiRepo struct{ data *Data } type apiRepo struct{ data Provider }
func NewAPIRepo(data *Data) biz.APIRepo { return &apiRepo{data: data} } func NewAPIRepo(data Provider) biz.APIRepo { return &apiRepo{data: data} }
type apiPO struct { type apiPO struct {
ID uint `gorm:"primaryKey"` ID uint `gorm:"primaryKey"`
@@ -52,20 +53,20 @@ func apiFromPO(po apiPO) *biz.API {
func (r *apiRepo) CreateAPI(ctx context.Context, v *biz.API) error { func (r *apiRepo) CreateAPI(ctx context.Context, v *biz.API) error {
po := apiPO{Path: v.Path, Description: v.Description, APIGroup: v.APIGroup, Method: v.Method} po := apiPO{Path: v.Path, Description: v.Description, APIGroup: v.APIGroup, Method: v.Method}
var count int64 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 { if err := r.data.DB().WithContext(ctx).Model(&apiPO{}).Where("path = ? AND method = ?", po.Path, po.Method).Count(&count).Error; err != nil {
return err return err
} }
if count > 0 { if count > 0 {
return errors.New("存在相同api") return errors.New("存在相同api")
} }
if err := r.data.gormDB.WithContext(ctx).Create(&po).Error; err != nil { if err := r.data.DB().WithContext(ctx).Create(&po).Error; err != nil {
return err return err
} }
v.ID, v.CreatedAt, v.UpdatedAt, v.Method = po.ID, po.CreatedAt, po.UpdatedAt, po.Method v.ID, v.CreatedAt, v.UpdatedAt, v.Method = po.ID, po.CreatedAt, po.UpdatedAt, po.Method
return nil return nil
} }
func (r *apiRepo) UpdateAPI(ctx context.Context, v *biz.API) error { func (r *apiRepo) UpdateAPI(ctx context.Context, v *biz.API) error {
db := r.data.gormDB.WithContext(ctx) db := r.data.DB().WithContext(ctx)
var old apiPO var old apiPO
if err := db.First(&old, v.ID).Error; err != nil { if err := db.First(&old, v.ID).Error; err != nil {
return err return err
@@ -90,7 +91,7 @@ func (r *apiRepo) UpdateAPI(ctx context.Context, v *biz.API) error {
return db.Model(&old).Updates(map[string]any{"path": v.Path, "description": v.Description, "api_group": v.APIGroup, "method": method}).Error 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 { func (r *apiRepo) DeleteAPIs(ctx context.Context, ids []uint) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var apis []apiPO var apis []apiPO
if err := tx.Where("id IN ?", ids).Find(&apis).Error; err != nil { if err := tx.Where("id IN ?", ids).Find(&apis).Error; err != nil {
return err return err
@@ -110,18 +111,18 @@ func (r *apiRepo) DeleteAPIs(ctx context.Context, ids []uint) error {
} }
func (r *apiRepo) FindAPI(ctx context.Context, id uint) (*biz.API, error) { func (r *apiRepo) FindAPI(ctx context.Context, id uint) (*biz.API, error) {
var po apiPO var po apiPO
if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil { if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err return nil, err
} }
return apiFromPO(po), nil return apiFromPO(po), nil
} }
func (r *apiRepo) ListAPIs(ctx context.Context, page, size int, q *biz.API) ([]*biz.API, int64, error) { func (r *apiRepo) ListAPIs(ctx context.Context, page, size int, q *biz.API) ([]*biz.API, int64, error) {
db := r.data.gormDB.WithContext(ctx).Model(&apiPO{}) db := r.data.DB().WithContext(ctx).Model(&apiPO{})
if q != nil && q.StrictAll { if q != nil && q.StrictAll {
config := r.data.runtime.Admin() config := r.data.Runtime().Admin()
if actor, ok := biz.ActorFromContext(ctx); ok && config != nil && config.System != nil && config.System.UseStrictAuth { if actor, ok := biz.ActorFromContext(ctx); ok && config != nil && config.System != nil && config.System.UseStrictAuth {
var authority authorityPO var authority authorityPO
if err := r.data.gormDB.WithContext(ctx).Where("authority_id = ?", actor.AuthorityID).First(&authority).Error; err != nil { if err := r.data.DB().WithContext(ctx).Where("authority_id = ?", actor.AuthorityID).First(&authority).Error; err != nil {
return nil, 0, err return nil, 0, err
} }
if authority.ParentID != nil && *authority.ParentID != 0 { if authority.ParentID != nil && *authority.ParentID != 0 {
@@ -167,7 +168,7 @@ func (r *apiRepo) ListAPIs(ctx context.Context, page, size int, q *biz.API) ([]*
var pos []apiPO var pos []apiPO
query := db.Order(order) query := db.Order(order)
if q == nil || !q.StrictAll { if q == nil || !q.StrictAll {
query = applyRequiredPagination(query, page, size, 100) query = pagination.ApplyRequired(query, page, size, 100)
} }
if err := query.Find(&pos).Error; err != nil { if err := query.Find(&pos).Error; err != nil {
return nil, 0, err return nil, 0, err
@@ -1,4 +1,4 @@
package data package system
import ( import (
"context" "context"
@@ -15,7 +15,7 @@ import (
func (r *apiRepo) APIRoleIDs(ctx context.Context, path, method string) ([]uint, error) { func (r *apiRepo) APIRoleIDs(ctx context.Context, path, method string) ([]uint, error) {
rows := make([]casbinRulePO, 0) rows := make([]casbinRulePO, 0)
err := policyScope(r.data.gormDB.WithContext(ctx)). err := policyScope(r.data.DB().WithContext(ctx)).
Where("v1 = ? AND v2 = ?", path, method). Where("v1 = ? AND v2 = ?", path, method).
Find(&rows).Error Find(&rows).Error
if err != nil { if err != nil {
@@ -47,7 +47,7 @@ func (r *apiRepo) SetAPIRoles(ctx context.Context, path, method string, ids []ui
return err return err
} }
} }
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if strict { if strict {
authorityIDs := make([]string, 0, len(allowedAuthorities)) authorityIDs := make([]string, 0, len(allowedAuthorities))
for authorityID := range allowedAuthorities { for authorityID := range allowedAuthorities {
@@ -74,11 +74,11 @@ func (r *apiRepo) SetAPIRoles(ctx context.Context, path, method string, ids []ui
func (r *apiRepo) CheckPolicyStore(ctx context.Context) error { func (r *apiRepo) CheckPolicyStore(ctx context.Context) error {
var count int64 var count int64
return r.data.gormDB.WithContext(ctx).Model(&casbinRulePO{}).Count(&count).Error return r.data.DB().WithContext(ctx).Model(&casbinRulePO{}).Count(&count).Error
} }
func (r *apiRepo) Authorize(ctx context.Context, aid uint, path, method string) (bool, error) { func (r *apiRepo) Authorize(ctx context.Context, aid uint, path, method string) (bool, error) {
rows, err := policyRowsForAuthority(r.data.gormDB.WithContext(ctx), aid) rows, err := policyRowsForAuthority(r.data.DB().WithContext(ctx), aid)
if err != nil { if err != nil {
return false, err return false, err
} }
@@ -106,7 +106,7 @@ m = r.sub == p.sub && keyMatch2(r.obj, p.obj) && r.act == p.act`)
return enforcer.Enforce(subject, path, method) return enforcer.Enforce(subject, path, method)
} }
func (r *apiRepo) PolicyPaths(ctx context.Context, aid uint) ([]*biz.API, error) { func (r *apiRepo) PolicyPaths(ctx context.Context, aid uint) ([]*biz.API, error) {
rows, err := policyRowsForAuthority(r.data.gormDB.WithContext(ctx), aid) rows, err := policyRowsForAuthority(r.data.DB().WithContext(ctx), aid)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -126,7 +126,7 @@ func (r *apiRepo) SetPolicyPaths(ctx context.Context, aid uint, paths []*biz.API
if err := r.checkPolicyPathsAuth(ctx, paths); err != nil { if err := r.checkPolicyPathsAuth(ctx, paths); err != nil {
return err return err
} }
db := r.data.gormDB.WithContext(ctx) db := r.data.DB().WithContext(ctx)
// The reference enforcer removes the old authority policies before it // The reference enforcer removes the old authority policies before it
// attempts to add the replacement set. Keep that ordering visible even // attempts to add the replacement set. Keep that ordering visible even
// though Kra reads policies directly from the database rather than through // though Kra reads policies directly from the database rather than through
@@ -161,11 +161,11 @@ func (r *apiRepo) checkPolicyPathsAuth(ctx context.Context, paths []*biz.API) er
return err return err
} }
var authority authorityPO var authority authorityPO
if err := r.data.gormDB.WithContext(ctx).Where("authority_id = ?", actor.AuthorityID).First(&authority).Error; err != nil { if err := r.data.DB().WithContext(ctx).Where("authority_id = ?", actor.AuthorityID).First(&authority).Error; err != nil {
return err return err
} }
var registered []apiPO var registered []apiPO
if err := r.data.gormDB.WithContext(ctx).Find(&registered).Error; err != nil { if err := r.data.DB().WithContext(ctx).Find(&registered).Error; err != nil {
return err return err
} }
allowedSet := make(map[string]bool, len(registered)) allowedSet := make(map[string]bool, len(registered))
@@ -174,7 +174,7 @@ func (r *apiRepo) checkPolicyPathsAuth(ctx context.Context, paths []*biz.API) er
allowedSet[item.Path+"\x00"+item.Method] = true allowedSet[item.Path+"\x00"+item.Method] = true
} }
} else { } else {
policies, err := policyRowsForAuthority(r.data.gormDB.WithContext(ctx), actor.AuthorityID) policies, err := policyRowsForAuthority(r.data.DB().WithContext(ctx), actor.AuthorityID)
if err != nil { if err != nil {
return err return err
} }
@@ -1,4 +1,4 @@
package data package system
import ( import (
"context" "context"
@@ -1,4 +1,4 @@
package data package system
import ( import (
"context" "context"
@@ -10,7 +10,7 @@ import (
func (r *apiRepo) IgnoredAPIs(ctx context.Context) ([]*biz.API, error) { func (r *apiRepo) IgnoredAPIs(ctx context.Context) ([]*biz.API, error) {
var pos []ignoredAPIPO var pos []ignoredAPIPO
if err := r.data.gormDB.WithContext(ctx).Find(&pos).Error; err != nil { if err := r.data.DB().WithContext(ctx).Find(&pos).Error; err != nil {
return nil, err return nil, err
} }
out := make([]*biz.API, 0, len(pos)) out := make([]*biz.API, 0, len(pos))
@@ -24,12 +24,12 @@ func (r *apiRepo) SetAPIIgnored(ctx context.Context, path, method string, ignore
if ignored { if ignored {
// The compatible endpoint creates an ignore row on every request (the table has no // The compatible endpoint creates an ignore row on every request (the table has no
// path/method uniqueness constraint); retain that observable behavior. // path/method uniqueness constraint); retain that observable behavior.
return r.data.gormDB.WithContext(ctx).Create(&po).Error return r.data.DB().WithContext(ctx).Create(&po).Error
} }
return r.data.gormDB.WithContext(ctx).Unscoped().Where("path = ? AND method = ?", po.Path, po.Method).Delete(&ignoredAPIPO{}).Error return r.data.DB().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 { func (r *apiRepo) ApplyAPISync(ctx context.Context, added, deleted []*biz.API) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if len(added) > 0 { if len(added) > 0 {
pos := make([]apiPO, 0, len(added)) pos := make([]apiPO, 0, len(added))
for _, item := range added { for _, item := range added {
@@ -1,4 +1,4 @@
package data package system
import ( import (
"context" "context"
@@ -6,13 +6,14 @@ import (
"time" "time"
"kra/internal/biz" "kra/internal/biz"
"kra/pkg/pagination"
"gorm.io/gorm" "gorm.io/gorm"
) )
type apiTokenRepo struct{ data *Data } type apiTokenRepo struct{ data Provider }
func NewAPITokenRepo(data *Data) biz.APITokenRepo { return &apiTokenRepo{data: data} } func NewAPITokenRepo(data Provider) biz.APITokenRepo { return &apiTokenRepo{data: data} }
type apiTokenPO struct { type apiTokenPO struct {
ID uint `gorm:"primaryKey"` ID uint `gorm:"primaryKey"`
@@ -41,14 +42,14 @@ func (jwtBlacklistPO) TableName() string { return "jwt_blacklists" }
func (r *apiTokenRepo) UserHasAuthority(ctx context.Context, userID, authorityID uint) (*biz.User, bool, error) { func (r *apiTokenRepo) UserHasAuthority(ctx context.Context, userID, authorityID uint) (*biz.User, bool, error) {
var po userPO var po userPO
if err := r.data.gormDB.WithContext(ctx).First(&po, userID).Error; err != nil { if err := r.data.DB().WithContext(ctx).First(&po, userID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, false, errors.New("用户不存在") return nil, false, errors.New("用户不存在")
} }
return nil, false, err return nil, false, err
} }
var count int64 var count int64
err := r.data.gormDB.WithContext(ctx).Model(&userAuthorityPO{}). err := r.data.DB().WithContext(ctx).Model(&userAuthorityPO{}).
Joins("JOIN sys_authorities ON sys_authorities.authority_id = sys_user_authority.sys_authority_authority_id"). 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). 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 Count(&count).Error
@@ -60,7 +61,7 @@ func (r *apiTokenRepo) UserHasAuthority(ctx context.Context, userID, authorityID
} }
func (r *apiTokenRepo) CreateAPIToken(ctx context.Context, v *biz.APIToken) error { func (r *apiTokenRepo) CreateAPIToken(ctx context.Context, v *biz.APIToken) error {
po := apiTokenPO{UserID: v.UserID, AuthorityID: v.AuthorityID, Token: v.Token, Status: v.Status, ExpiresAt: v.ExpiresAt, Remark: v.Remark} po := apiTokenPO{UserID: v.UserID, AuthorityID: v.AuthorityID, Token: v.Token, Status: v.Status, ExpiresAt: v.ExpiresAt, Remark: v.Remark}
if err := r.data.gormDB.WithContext(ctx).Create(&po).Error; err != nil { if err := r.data.DB().WithContext(ctx).Create(&po).Error; err != nil {
return err return err
} }
v.ID = po.ID v.ID = po.ID
@@ -68,7 +69,7 @@ func (r *apiTokenRepo) CreateAPIToken(ctx context.Context, v *biz.APIToken) erro
return nil return nil
} }
func (r *apiTokenRepo) ListAPITokens(ctx context.Context, page, size int, userID uint, status *bool) ([]*biz.APIToken, int64, error) { func (r *apiTokenRepo) ListAPITokens(ctx context.Context, page, size int, userID uint, status *bool) ([]*biz.APIToken, int64, error) {
db := r.data.gormDB.WithContext(ctx).Model(&apiTokenPO{}) db := r.data.DB().WithContext(ctx).Model(&apiTokenPO{})
if userID != 0 { if userID != 0 {
db = db.Where("user_id = ?", userID) db = db.Where("user_id = ?", userID)
} }
@@ -80,7 +81,7 @@ func (r *apiTokenRepo) ListAPITokens(ctx context.Context, page, size int, userID
return nil, 0, err return nil, 0, err
} }
var pos []apiTokenPO var pos []apiTokenPO
if err := applyRequiredPagination(db.Order("created_at desc"), page, size, 100).Find(&pos).Error; err != nil { if err := pagination.ApplyRequired(db.Order("created_at desc"), page, size, 100).Find(&pos).Error; err != nil {
return nil, 0, err return nil, 0, err
} }
userIDs := make([]uint, 0, len(pos)) userIDs := make([]uint, 0, len(pos))
@@ -89,7 +90,7 @@ func (r *apiTokenRepo) ListAPITokens(ctx context.Context, page, size int, userID
} }
var userPOs []userPO var userPOs []userPO
if len(userIDs) > 0 { if len(userIDs) > 0 {
if err := r.data.gormDB.WithContext(ctx).Where("id IN ?", userIDs).Find(&userPOs).Error; err != nil { if err := r.data.DB().WithContext(ctx).Where("id IN ?", userIDs).Find(&userPOs).Error; err != nil {
return nil, 0, err return nil, 0, err
} }
} }
@@ -106,10 +107,10 @@ func (r *apiTokenRepo) ListAPITokens(ctx context.Context, page, size int, userID
} }
func (r *apiTokenRepo) DisableAPIToken(ctx context.Context, id uint) (string, error) { func (r *apiTokenRepo) DisableAPIToken(ctx context.Context, id uint) (string, error) {
var po apiTokenPO var po apiTokenPO
if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil { if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
return "", err return "", err
} }
return po.Token, r.data.gormDB.WithContext(ctx).Model(&po).Update("status", false).Error return po.Token, r.data.DB().WithContext(ctx).Model(&po).Update("status", false).Error
} }
func (r *apiTokenRepo) DisableAndBlacklistAPIToken(ctx context.Context, id uint) error { func (r *apiTokenRepo) DisableAndBlacklistAPIToken(ctx context.Context, id uint) error {
// Persist the blacklist row first, then mark the // Persist the blacklist row first, then mark the
@@ -117,20 +118,20 @@ func (r *apiTokenRepo) DisableAndBlacklistAPIToken(ctx context.Context, id uint)
// the reference implementation, so a storage error has the same observable // the reference implementation, so a storage error has the same observable
// result here. // result here.
var po apiTokenPO var po apiTokenPO
if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil { if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
return err return err
} }
if err := r.BlacklistToken(ctx, po.Token); err != nil { if err := r.BlacklistToken(ctx, po.Token); err != nil {
return err return err
} }
return r.data.gormDB.WithContext(ctx).Model(&po).Update("status", false).Error return r.data.DB().WithContext(ctx).Model(&po).Update("status", false).Error
} }
func (r *apiTokenRepo) BlacklistToken(ctx context.Context, token string) error { func (r *apiTokenRepo) BlacklistToken(ctx context.Context, token string) error {
return r.data.gormDB.WithContext(ctx).Create(&jwtBlacklistPO{JWT: token}).Error return r.data.DB().WithContext(ctx).Create(&jwtBlacklistPO{JWT: token}).Error
} }
func (r *apiTokenRepo) IsTokenDisabled(ctx context.Context, token string) (bool, error) { func (r *apiTokenRepo) IsTokenDisabled(ctx context.Context, token string) (bool, error) {
var blacklistCount int64 var blacklistCount int64
if err := r.data.gormDB.WithContext(ctx).Model(&jwtBlacklistPO{}).Where("jwt = ?", token).Count(&blacklistCount).Error; err != nil { if err := r.data.DB().WithContext(ctx).Model(&jwtBlacklistPO{}).Where("jwt = ?", token).Count(&blacklistCount).Error; err != nil {
return false, err return false, err
} }
// The JWT middleware consults only jwt_blacklists and lets JWT parsing // The JWT middleware consults only jwt_blacklists and lets JWT parsing
+13
View File
@@ -0,0 +1,13 @@
package system
import "kra/internal/biz"
type auditQueryRepo struct{ data Provider }
type auditRecorderRepo struct{ data Provider }
type logFileRepo struct{ data Provider }
func NewAuditRepo(data Provider) biz.AuditQueryRepo { return &auditQueryRepo{data: data} }
func NewAuditRecorderRepo(data Provider) biz.AuditRecordRepo { return &auditRecorderRepo{data: data} }
func NewLogFileRepo(data Provider) biz.LogFileRepo { return &logFileRepo{data: data} }
@@ -1,4 +1,4 @@
package data package system
import ( import (
"context" "context"
@@ -1,4 +1,4 @@
package data package system
import ( import (
"context" "context"
@@ -9,9 +9,11 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
type authorityAccessRepo struct{ data *Data } var errInvalidDataScope = errors.New("数据权限范围不合法")
func NewAuthorityAccessRepo(data *Data) biz.AuthorityAccessRepo { type authorityAccessRepo struct{ data Provider }
func NewAuthorityAccessRepo(data Provider) biz.AuthorityAccessRepo {
return &authorityAccessRepo{data: data} return &authorityAccessRepo{data: data}
} }
@@ -21,11 +23,11 @@ func (r *authorityAccessRepo) strictAuthorityIDs(ctx context.Context, actorID ui
return allowed, nil return allowed, nil
} }
var actor authorityPO var actor authorityPO
if err := r.data.gormDB.WithContext(ctx).Where("authority_id = ?", actorID).First(&actor).Error; err != nil { if err := r.data.DB().WithContext(ctx).Where("authority_id = ?", actorID).First(&actor).Error; err != nil {
return nil, err return nil, err
} }
var authorities []authorityPO var authorities []authorityPO
if err := r.data.gormDB.WithContext(ctx).Find(&authorities).Error; err != nil { if err := r.data.DB().WithContext(ctx).Find(&authorities).Error; err != nil {
return nil, err return nil, err
} }
children := make(map[uint][]uint) children := make(map[uint][]uint)
@@ -57,7 +59,7 @@ func (r *authorityAccessRepo) strictAuthorityIDs(ctx context.Context, actorID ui
} }
func (r *authorityAccessRepo) strictAuthorityAccess(ctx context.Context) (biz.Actor, map[uint]bool, bool, error) { func (r *authorityAccessRepo) strictAuthorityAccess(ctx context.Context) (biz.Actor, map[uint]bool, bool, error) {
config := r.data.runtime.Admin() config := r.data.Runtime().Admin()
if config == nil || config.System == nil || !config.System.UseStrictAuth { if config == nil || config.System == nil || !config.System.UseStrictAuth {
return biz.Actor{}, nil, false, nil return biz.Actor{}, nil, false, nil
} }
@@ -118,7 +120,7 @@ func (r *authorityAccessRepo) ensureAuthorityParentAcyclic(ctx context.Context,
} }
visited[current] = struct{}{} visited[current] = struct{}{}
var authority authorityPO var authority authorityPO
if err := r.data.gormDB.WithContext(ctx).Select("authority_id", "parent_id").Where("authority_id = ?", current).First(&authority).Error; err != nil { if err := r.data.DB().WithContext(ctx).Select("authority_id", "parent_id").Where("authority_id = ?", current).First(&authority).Error; err != nil {
return err return err
} }
if authority.ParentID == nil { if authority.ParentID == nil {
@@ -148,7 +150,7 @@ func (r *authorityAccessRepo) CreateAuthority(ctx context.Context, value *biz.Au
return err return err
} }
} }
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
// The administration contract checks existence with First and treats every result other than // The administration contract checks existence with First and treats every result other than
// ErrRecordNotFound as a duplicate-role error. Keep that precedence so // ErrRecordNotFound as a duplicate-role error. Keep that precedence so
// duplicate IDs win over strict-tree validation and the API message stays // duplicate IDs win over strict-tree validation and the API message stays
@@ -220,7 +222,7 @@ func (r *authorityAccessRepo) CopyAuthority(ctx context.Context, sourceID uint,
if err != nil { if err != nil {
return err return err
} }
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
// Reject a duplicate target ID before performing the // Reject a duplicate target ID before performing the
// hierarchy/Casbin checks that happen later in UpdateCasbin. // hierarchy/Casbin checks that happen later in UpdateCasbin.
var existing authorityPO var existing authorityPO
@@ -393,7 +395,7 @@ func (r *authorityAccessRepo) UpdateAuthority(ctx context.Context, value *biz.Au
if err = r.ensureAuthorityParentAcyclic(ctx, value.AuthorityID, value.ParentID); err != nil { if err = r.ensureAuthorityParentAcyclic(ctx, value.AuthorityID, value.ParentID); err != nil {
return err return err
} }
db := r.data.gormDB.WithContext(ctx) db := r.data.DB().WithContext(ctx)
var current authorityPO var current authorityPO
if err := db.Where("authority_id = ?", value.AuthorityID).First(&current).Error; err != nil { if err := db.Where("authority_id = ?", value.AuthorityID).First(&current).Error; err != nil {
return errors.New("查询角色数据失败") return errors.New("查询角色数据失败")
@@ -405,7 +407,7 @@ func (r *authorityAccessRepo) DeleteAuthority(ctx context.Context, id uint) erro
if err := r.checkAuthorityIDAuth(ctx, id); err != nil { if err := r.checkAuthorityIDAuth(ctx, id); err != nil {
return err return err
} }
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var authority authorityPO var authority authorityPO
if err := tx.Where("authority_id = ?", id).First(&authority).Error; err != nil { if err := tx.Where("authority_id = ?", id).First(&authority).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -463,9 +465,9 @@ func (r *authorityAccessRepo) DeleteAuthority(ctx context.Context, id uint) erro
}) })
} }
func (r *authorityAccessRepo) ListAuthorities(ctx context.Context) ([]*biz.Authority, error) { func (r *authorityAccessRepo) ListAuthorities(ctx context.Context) ([]*biz.Authority, error) {
db := r.data.gormDB.WithContext(ctx) db := r.data.DB().WithContext(ctx)
var allowed map[uint]bool var allowed map[uint]bool
config := r.data.runtime.Admin() config := r.data.Runtime().Admin()
if actor, ok := biz.ActorFromContext(ctx); ok { if actor, ok := biz.ActorFromContext(ctx); ok {
// The current authority is loaded even when strict mode is disabled; // The current authority is loaded even when strict mode is disabled;
// an invalid token authority therefore fails the list request instead of // an invalid token authority therefore fails the list request instead of
@@ -504,7 +506,7 @@ func (r *authorityAccessRepo) SetAuthorityUsers(ctx context.Context, id uint, id
if strict && !allowed[id] { if strict && !allowed[id] {
return errors.New("您提交的角色ID不合法") return errors.New("您提交的角色ID不合法")
} }
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var oldIDs []uint var oldIDs []uint
if err := tx.Model(&userAuthorityPO{}).Where("sys_authority_authority_id = ?", id).Pluck("sys_user_id", &oldIDs).Error; err != nil { if err := tx.Model(&userAuthorityPO{}).Where("sys_authority_authority_id = ?", id).Pluck("sys_user_id", &oldIDs).Error; err != nil {
return err return err
@@ -609,7 +611,7 @@ func (r *authorityAccessRepo) checkUserIDAuth(ctx context.Context, id uint, allo
if allowSelf && actor.UserID != 0 && actor.UserID == id { if allowSelf && actor.UserID != 0 && actor.UserID == id {
return nil return nil
} }
return checkManagedUserIDs(r.data.gormDB.WithContext(ctx), []uint{id}, allowed, true) return checkManagedUserIDs(r.data.DB().WithContext(ctx), []uint{id}, allowed, true)
} }
func (r *authorityAccessRepo) checkDepartmentIDsAuth(ctx context.Context, ids []uint) error { func (r *authorityAccessRepo) checkDepartmentIDsAuth(ctx context.Context, ids []uint) error {
@@ -621,7 +623,7 @@ func (r *authorityAccessRepo) checkDepartmentIDsAuth(ctx context.Context, ids []
return nil return nil
} }
var authority authorityPO var authority authorityPO
if err := r.data.gormDB.WithContext(ctx).Select("authority_id", "data_scope").Where("authority_id = ?", actor.AuthorityID).First(&authority).Error; err != nil { if err := r.data.DB().WithContext(ctx).Select("authority_id", "data_scope").Where("authority_id = ?", actor.AuthorityID).First(&authority).Error; err != nil {
return err return err
} }
if authority.DataScope == 1 { if authority.DataScope == 1 {
@@ -645,7 +647,7 @@ func (r *authorityAccessRepo) checkDepartmentIDsAuth(ctx context.Context, ids []
func (r *authorityAccessRepo) AuthorityUserIDs(ctx context.Context, id uint) ([]uint, error) { func (r *authorityAccessRepo) AuthorityUserIDs(ctx context.Context, id uint) ([]uint, error) {
var ids []uint var ids []uint
err := r.data.gormDB.WithContext(ctx).Model(&userAuthorityPO{}).Where("sys_authority_authority_id = ?", id).Pluck("sys_user_id", &ids).Error err := r.data.DB().WithContext(ctx).Model(&userAuthorityPO{}).Where("sys_authority_authority_id = ?", id).Pluck("sys_user_id", &ids).Error
return ids, err return ids, err
} }
func (r *authorityAccessRepo) SetDataScope(ctx context.Context, id uint, scope int, deptIDs []uint) error { func (r *authorityAccessRepo) SetDataScope(ctx context.Context, id uint, scope int, deptIDs []uint) error {
@@ -658,7 +660,7 @@ func (r *authorityAccessRepo) SetDataScope(ctx context.Context, id uint, scope i
if err := r.checkDataScopeGrant(ctx, scope, deptIDs); err != nil { if err := r.checkDataScopeGrant(ctx, scope, deptIDs); err != nil {
return err return err
} }
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Model(&authorityPO{}).Where("authority_id = ?", id).Update("data_scope", scope).Error; err != nil { if err := tx.Model(&authorityPO{}).Where("authority_id = ?", id).Update("data_scope", scope).Error; err != nil {
return err return err
} }
@@ -693,7 +695,7 @@ func (r *authorityAccessRepo) checkDataScopeGrant(ctx context.Context, requested
} }
if len(requestedDepartments) > 0 { if len(requestedDepartments) > 0 {
var count int64 var count int64
if err := r.data.gormDB.WithContext(ctx).Model(&departmentPO{}).Where("id IN ?", departmentIDs).Count(&count).Error; err != nil { if err := r.data.DB().WithContext(ctx).Model(&departmentPO{}).Where("id IN ?", departmentIDs).Count(&count).Error; err != nil {
return err return err
} }
if count != int64(len(requestedDepartments)) { if count != int64(len(requestedDepartments)) {
@@ -705,7 +707,7 @@ func (r *authorityAccessRepo) checkDataScopeGrant(ctx context.Context, requested
return nil return nil
} }
var actorAuthority authorityPO var actorAuthority authorityPO
if err := r.data.gormDB.WithContext(ctx).Select("authority_id", "data_scope").Where("authority_id = ?", actor.AuthorityID).First(&actorAuthority).Error; err != nil { if err := r.data.DB().WithContext(ctx).Select("authority_id", "data_scope").Where("authority_id = ?", actor.AuthorityID).First(&actorAuthority).Error; err != nil {
return err return err
} }
grantable := false grantable := false
@@ -747,18 +749,18 @@ func (r *authorityAccessRepo) checkDataScopeGrant(ctx context.Context, requested
func (r *authorityAccessRepo) DataScopeDepartmentIDs(ctx context.Context, id uint) ([]uint, error) { func (r *authorityAccessRepo) DataScopeDepartmentIDs(ctx context.Context, id uint) ([]uint, error) {
var ids []uint var ids []uint
err := r.data.gormDB.WithContext(ctx).Model(&authorityDepartmentPO{}).Where("sys_authority_authority_id = ?", id).Pluck("sys_department_id", &ids).Error err := r.data.DB().WithContext(ctx).Model(&authorityDepartmentPO{}).Where("sys_authority_authority_id = ?", id).Pluck("sys_department_id", &ids).Error
return ids, err return ids, err
} }
func (r *authorityAccessRepo) ResolveDataScope(ctx context.Context, authorityID, userID uint) (biz.DataScope, error) { func (r *authorityAccessRepo) ResolveDataScope(ctx context.Context, authorityID, userID uint) (biz.DataScope, error) {
identity := biz.DataScope{UserID: userID, AuthorityID: authorityID} identity := biz.DataScope{UserID: userID, AuthorityID: authorityID}
var user userPO var user userPO
if err := r.data.gormDB.WithContext(ctx).Select("id", "dept_id").First(&user, userID).Error; err != nil { if err := r.data.DB().WithContext(ctx).Select("id", "dept_id").First(&user, userID).Error; err != nil {
return identity, err return identity, err
} }
identity.PrimaryDeptID = user.DeptID identity.PrimaryDeptID = user.DeptID
var authority authorityPO var authority authorityPO
if err := r.data.gormDB.WithContext(ctx).Select("authority_id", "data_scope").First(&authority, "authority_id = ?", authorityID).Error; err != nil { if err := r.data.DB().WithContext(ctx).Select("authority_id", "data_scope").First(&authority, "authority_id = ?", authorityID).Error; err != nil {
return identity, err return identity, err
} }
identity.Scope = authority.DataScope identity.Scope = authority.DataScope
@@ -772,7 +774,7 @@ func (r *authorityAccessRepo) ResolveDataScope(ctx context.Context, authorityID,
var ids []uint var ids []uint
selected := make(map[uint]bool) selected := make(map[uint]bool)
if identity.Scope == 2 || identity.Scope == 3 { if identity.Scope == 2 || identity.Scope == 3 {
if err := r.data.gormDB.WithContext(ctx).Model(&userDepartmentPO{}).Where("sys_user_id = ?", userID).Pluck("sys_department_id", &ids).Error; err != nil { if err := r.data.DB().WithContext(ctx).Model(&userDepartmentPO{}).Where("sys_user_id = ?", userID).Pluck("sys_department_id", &ids).Error; err != nil {
return identity, err return identity, err
} }
selected = make(map[uint]bool, len(ids)+1) selected = make(map[uint]bool, len(ids)+1)
@@ -792,7 +794,7 @@ func (r *authorityAccessRepo) ResolveDataScope(ctx context.Context, authorityID,
// parent_id edges at read time, rather than trusting the denormalized // parent_id edges at read time, rather than trusting the denormalized
// ancestors string (which may be stale after a department move). // ancestors string (which may be stale after a department move).
var departments []departmentPO var departments []departmentPO
if err := r.data.gormDB.WithContext(ctx).Select("id", "parent_id").Find(&departments).Error; err != nil { if err := r.data.DB().WithContext(ctx).Select("id", "parent_id").Find(&departments).Error; err != nil {
return identity, err return identity, err
} }
children := make(map[uint][]uint, len(departments)) children := make(map[uint][]uint, len(departments))
@@ -1,4 +1,4 @@
package data package system
import ( import (
"context" "context"
+25
View File
@@ -10,6 +10,31 @@ type IgnoredAPI struct {
Path string Path string
} }
// AdminMenu describes a menu entry contributed by a module. ParentName is
// resolved by the system persistence layer so feature modules do not need to
// know the menu PO shape.
type AdminMenu struct {
Name string
Path string
ParentName string
Component string
Title string
Icon string
Sort int
}
type AdminAPI struct {
Path string
Method string
APIGroup string
Description string
}
type AdminSurface struct {
Menus []AdminMenu
APIs []AdminAPI
}
func DefaultIgnoredAPIs(staticPath string) []IgnoredAPI { func DefaultIgnoredAPIs(staticPath string) []IgnoredAPI {
staticRoute := "/" + strings.Trim(staticPath, "/") + "/*filepath" staticRoute := "/" + strings.Trim(staticPath, "/") + "/*filepath"
return []IgnoredAPI{ return []IgnoredAPI{
@@ -1,4 +1,4 @@
package data package system
import ( import (
"strconv" "strconv"
@@ -1,10 +1,11 @@
package data package system
import ( import (
"context" "context"
"time" "time"
"kra/internal/biz" "kra/internal/biz"
"kra/pkg/pagination"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -23,13 +24,13 @@ type dataAccessLogPO struct {
func (dataAccessLogPO) TableName() string { return "sys_data_access_logs" } func (dataAccessLogPO) TableName() string { return "sys_data_access_logs" }
func (r *auditRecorderRepo) RecordDataAccess(ctx context.Context, v *biz.DataAccessLog) error { func (r *auditRecorderRepo) RecordDataAccess(ctx context.Context, v *biz.DataAccessLog) error {
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 return r.data.DB().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 { func dataAccessFromPO(v dataAccessLogPO) *biz.DataAccessLog {
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} 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) { 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{}) db := r.data.DB().WithContext(ctx).Model(&dataAccessLogPO{})
if q != nil { if q != nil {
if q.EventType != "" { if q.EventType != "" {
db = db.Where("event_type = ?", q.EventType) db = db.Where("event_type = ?", q.EventType)
@@ -43,7 +44,7 @@ func (r *auditQueryRepo) ListDataAccess(ctx context.Context, page, size int, q *
return nil, 0, err return nil, 0, err
} }
var pos []dataAccessLogPO var pos []dataAccessLogPO
if err := applyRequiredPagination(db.Order("id desc"), page, size, 100).Find(&pos).Error; err != nil { if err := pagination.ApplyRequired(db.Order("id desc"), page, size, 100).Find(&pos).Error; err != nil {
return nil, 0, err return nil, 0, err
} }
out := make([]*biz.DataAccessLog, 0, len(pos)) out := make([]*biz.DataAccessLog, 0, len(pos))
@@ -53,5 +54,5 @@ func (r *auditQueryRepo) ListDataAccess(ctx context.Context, page, size int, q *
return out, total, nil return out, total, nil
} }
func (r *auditQueryRepo) DeleteDataAccess(ctx context.Context, ids []uint) error { func (r *auditQueryRepo) DeleteDataAccess(ctx context.Context, ids []uint) error {
return r.data.gormDB.WithContext(ctx).Delete(&dataAccessLogPO{}, "id IN ?", ids).Error return r.data.DB().WithContext(ctx).Delete(&dataAccessLogPO{}, "id IN ?", ids).Error
} }
@@ -1,4 +1,4 @@
package data package system
import ( import (
"context" "context"
@@ -12,9 +12,9 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
type departmentRepo struct{ data *Data } type departmentRepo struct{ data Provider }
func NewDepartmentRepo(data *Data) biz.DepartmentRepo { return &departmentRepo{data: data} } func NewDepartmentRepo(data Provider) biz.DepartmentRepo { return &departmentRepo{data: data} }
type departmentPO struct { type departmentPO struct {
ID uint `gorm:"primaryKey"` ID uint `gorm:"primaryKey"`
@@ -62,7 +62,7 @@ func (r *departmentRepo) attachDepartmentLeaders(ctx context.Context, department
return nil return nil
} }
var leaders []userPO var leaders []userPO
if err := r.data.gormDB.WithContext(ctx).Where("id IN ?", leaderIDs).Find(&leaders).Error; err != nil { if err := r.data.DB().WithContext(ctx).Where("id IN ?", leaderIDs).Find(&leaders).Error; err != nil {
return err return err
} }
leaderByID := make(map[uint]*biz.User, len(leaders)) leaderByID := make(map[uint]*biz.User, len(leaders))
@@ -78,7 +78,7 @@ func (r *departmentRepo) CreateDepartment(ctx context.Context, v *biz.Department
v.Ancestors = "0" v.Ancestors = "0"
if v.ParentID != 0 { if v.ParentID != 0 {
var parent departmentPO var parent departmentPO
if err := r.data.gormDB.WithContext(ctx).First(&parent, v.ParentID).Error; err != nil { if err := r.data.DB().WithContext(ctx).First(&parent, v.ParentID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("父部门不存在") return errors.New("父部门不存在")
} }
@@ -86,7 +86,7 @@ func (r *departmentRepo) CreateDepartment(ctx context.Context, v *biz.Department
} }
v.Ancestors = strings.Trim(parent.Ancestors+","+strconv.FormatUint(uint64(parent.ID), 10), ",") v.Ancestors = strings.Trim(parent.Ancestors+","+strconv.FormatUint(uint64(parent.ID), 10), ",")
} }
return r.data.gormDB.WithContext(ctx).Create(&departmentPO{Name: v.Name, ParentID: v.ParentID, Ancestors: v.Ancestors, Sort: v.Sort, LeaderID: v.LeaderID, Status: v.Status}).Error return r.data.DB().WithContext(ctx).Create(&departmentPO{Name: v.Name, ParentID: v.ParentID, Ancestors: v.Ancestors, Sort: v.Sort, LeaderID: v.LeaderID, Status: v.Status}).Error
} }
func (r *departmentRepo) UpdateDepartment(ctx context.Context, v *biz.Department) error { func (r *departmentRepo) UpdateDepartment(ctx context.Context, v *biz.Department) error {
if v.ParentID == v.ID { if v.ParentID == v.ID {
@@ -95,7 +95,7 @@ func (r *departmentRepo) UpdateDepartment(ctx context.Context, v *biz.Department
v.Ancestors = "0" v.Ancestors = "0"
if v.ParentID != 0 { if v.ParentID != 0 {
var parent departmentPO var parent departmentPO
if err := r.data.gormDB.WithContext(ctx).First(&parent, v.ParentID).Error; err != nil { if err := r.data.DB().WithContext(ctx).First(&parent, v.ParentID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("父部门不存在") return errors.New("父部门不存在")
} }
@@ -103,36 +103,36 @@ func (r *departmentRepo) UpdateDepartment(ctx context.Context, v *biz.Department
} }
v.Ancestors = strings.Trim(parent.Ancestors+","+strconv.FormatUint(uint64(parent.ID), 10), ",") v.Ancestors = strings.Trim(parent.Ancestors+","+strconv.FormatUint(uint64(parent.ID), 10), ",")
} }
return r.data.gormDB.WithContext(ctx).Model(&departmentPO{}).Where("id = ?", v.ID).Updates(map[string]any{"name": v.Name, "parent_id": v.ParentID, "ancestors": v.Ancestors, "sort": v.Sort, "leader_id": v.LeaderID, "status": v.Status}).Error return r.data.DB().WithContext(ctx).Model(&departmentPO{}).Where("id = ?", v.ID).Updates(map[string]any{"name": v.Name, "parent_id": v.ParentID, "ancestors": v.Ancestors, "sort": v.Sort, "leader_id": v.LeaderID, "status": v.Status}).Error
} }
func (r *departmentRepo) DeleteDepartment(ctx context.Context, id uint) error { func (r *departmentRepo) DeleteDepartment(ctx context.Context, id uint) error {
if id == 0 { if id == 0 {
return errors.New("部门ID不能为空") return errors.New("部门ID不能为空")
} }
var count int64 var count int64
if err := r.data.gormDB.WithContext(ctx).Model(&departmentPO{}).Where("parent_id = ?", id).Count(&count).Error; err != nil { if err := r.data.DB().WithContext(ctx).Model(&departmentPO{}).Where("parent_id = ?", id).Count(&count).Error; err != nil {
return err return err
} }
if count > 0 { if count > 0 {
return errors.New("存在子部门,不允许删除") return errors.New("存在子部门,不允许删除")
} }
if err := r.data.gormDB.WithContext(ctx).Model(&userPO{}).Where("dept_id = ?", id).Count(&count).Error; err != nil { if err := r.data.DB().WithContext(ctx).Model(&userPO{}).Where("dept_id = ?", id).Count(&count).Error; err != nil {
return err return err
} }
if count > 0 { if count > 0 {
return errors.New("该部门下存在用户,不允许删除") return errors.New("该部门下存在用户,不允许删除")
} }
if err := r.data.gormDB.WithContext(ctx).Model(&userDepartmentPO{}).Where("sys_department_id = ?", id).Count(&count).Error; err != nil { if err := r.data.DB().WithContext(ctx).Model(&userDepartmentPO{}).Where("sys_department_id = ?", id).Count(&count).Error; err != nil {
return err return err
} }
if count > 0 { if count > 0 {
return errors.New("该部门下存在用户,不允许删除") return errors.New("该部门下存在用户,不允许删除")
} }
return r.data.gormDB.WithContext(ctx).Delete(&departmentPO{}, id).Error return r.data.DB().WithContext(ctx).Delete(&departmentPO{}, id).Error
} }
func (r *departmentRepo) FindDepartment(ctx context.Context, id uint) (*biz.Department, error) { func (r *departmentRepo) FindDepartment(ctx context.Context, id uint) (*biz.Department, error) {
var po departmentPO var po departmentPO
if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil { if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err return nil, err
} }
department := deptFromPO(po) department := deptFromPO(po)
@@ -143,7 +143,7 @@ func (r *departmentRepo) FindDepartment(ctx context.Context, id uint) (*biz.Depa
} }
func (r *departmentRepo) ListDepartments(ctx context.Context, name string) ([]*biz.Department, error) { func (r *departmentRepo) ListDepartments(ctx context.Context, name string) ([]*biz.Department, error) {
var pos []departmentPO var pos []departmentPO
db := r.data.gormDB.WithContext(ctx).Order("sort") db := r.data.DB().WithContext(ctx).Order("sort")
if name != "" { if name != "" {
db = db.Where("name LIKE ?", "%"+name+"%") db = db.Where("name LIKE ?", "%"+name+"%")
} }
@@ -182,7 +182,7 @@ func (r *departmentRepo) ListDepartments(ctx context.Context, name string) ([]*b
} }
func (r *departmentRepo) DepartmentUserIDs(ctx context.Context, id uint) ([]uint, error) { func (r *departmentRepo) DepartmentUserIDs(ctx context.Context, id uint) ([]uint, error) {
var ids []uint var ids []uint
err := r.data.gormDB.WithContext(ctx).Model(&userDepartmentPO{}).Where("sys_department_id = ?", id).Pluck("sys_user_id", &ids).Error err := r.data.DB().WithContext(ctx).Model(&userDepartmentPO{}).Where("sys_department_id = ?", id).Pluck("sys_user_id", &ids).Error
return ids, err return ids, err
} }
func (r *departmentRepo) SetDepartmentUsers(ctx context.Context, id uint, ids []uint) error { func (r *departmentRepo) SetDepartmentUsers(ctx context.Context, id uint, ids []uint) error {
@@ -192,7 +192,7 @@ func (r *departmentRepo) replaceUserDepartments(ctx context.Context, id uint, id
if id == 0 { if id == 0 {
return errors.New("部门ID不能为空") return errors.New("部门ID不能为空")
} }
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var current []uint var current []uint
if err := tx.Model(&userDepartmentPO{}).Where("sys_department_id = ?", id).Pluck("sys_user_id", &current).Error; err != nil { if err := tx.Model(&userDepartmentPO{}).Where("sys_department_id = ?", id).Pluck("sys_user_id", &current).Error; err != nil {
return err return err
@@ -233,7 +233,7 @@ func (r *departmentRepo) replaceUserDepartments(ctx context.Context, id uint, id
}) })
} }
func (r *departmentRepo) SetUserDepartments(ctx context.Context, uid uint, ids []uint, primary uint) error { func (r *departmentRepo) SetUserDepartments(ctx context.Context, uid uint, ids []uint, primary uint) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var user userPO var user userPO
if err := tx.First(&user, uid).Error; err != nil { if err := tx.First(&user, uid).Error; err != nil {
return errors.New("查询用户数据失败") return errors.New("查询用户数据失败")
@@ -1,4 +1,4 @@
package data package system
import ( import (
"context" "context"
@@ -8,13 +8,14 @@ import (
"time" "time"
"kra/internal/biz" "kra/internal/biz"
"kra/pkg/pagination"
"gorm.io/gorm" "gorm.io/gorm"
) )
type dictionaryRepo struct{ data *Data } type dictionaryRepo struct{ data Provider }
func NewDictionaryRepo(data *Data) biz.DictionaryRepo { return &dictionaryRepo{data: data} } func NewDictionaryRepo(data Provider) biz.DictionaryRepo { return &dictionaryRepo{data: data} }
type dictionaryPO struct { type dictionaryPO struct {
ID uint `gorm:"primaryKey"` ID uint `gorm:"primaryKey"`
@@ -60,13 +61,13 @@ func parameterFromPO(po parameterPO) *biz.SystemParameter {
func (r *dictionaryRepo) CreateDictionary(ctx context.Context, v *biz.Dictionary) error { func (r *dictionaryRepo) CreateDictionary(ctx context.Context, v *biz.Dictionary) error {
var existing dictionaryPO var existing dictionaryPO
if err := r.data.gormDB.WithContext(ctx).Where("type = ?", v.Type).First(&existing).Error; err == nil { if err := r.data.DB().WithContext(ctx).Where("type = ?", v.Type).First(&existing).Error; err == nil {
return errors.New("存在相同的type,不允许创建") return errors.New("存在相同的type,不允许创建")
} else if !errors.Is(err, gorm.ErrRecordNotFound) { } else if !errors.Is(err, gorm.ErrRecordNotFound) {
return err return err
} }
po := dictionaryPO{Name: v.Name, Type: v.Type, Status: v.Status, Desc: v.Desc, ParentID: v.ParentID} 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 { if err := r.data.DB().WithContext(ctx).Create(&po).Error; err != nil {
return err return err
} }
v.ID = po.ID v.ID = po.ID
@@ -76,10 +77,10 @@ func (r *dictionaryRepo) CreateDictionary(ctx context.Context, v *biz.Dictionary
func (r *dictionaryRepo) ImportDictionary(ctx context.Context, dictionary *biz.Dictionary, details []*biz.DictionaryDetail) error { func (r *dictionaryRepo) ImportDictionary(ctx context.Context, dictionary *biz.Dictionary, details []*biz.DictionaryDetail) error {
var existing dictionaryPO var existing dictionaryPO
if err := r.data.gormDB.WithContext(ctx).Where("type = ?", dictionary.Type).First(&existing).Error; !errors.Is(err, gorm.ErrRecordNotFound) { if err := r.data.DB().WithContext(ctx).Where("type = ?", dictionary.Type).First(&existing).Error; !errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("存在相同的type,不允许导入") return errors.New("存在相同的type,不允许导入")
} }
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
po := dictionaryPO{Name: dictionary.Name, Type: dictionary.Type, Status: dictionary.Status, Desc: dictionary.Desc, ParentID: dictionary.ParentID} po := dictionaryPO{Name: dictionary.Name, Type: dictionary.Type, Status: dictionary.Status, Desc: dictionary.Desc, ParentID: dictionary.ParentID}
if err := tx.Create(&po).Error; err != nil { if err := tx.Create(&po).Error; err != nil {
return err return err
@@ -119,7 +120,7 @@ func (r *dictionaryRepo) ImportDictionary(ctx context.Context, dictionary *biz.D
}) })
} }
func (r *dictionaryRepo) UpdateDictionary(ctx context.Context, v *biz.Dictionary) error { func (r *dictionaryRepo) UpdateDictionary(ctx context.Context, v *biz.Dictionary) error {
db := r.data.gormDB.WithContext(ctx) db := r.data.DB().WithContext(ctx)
var current dictionaryPO var current dictionaryPO
if err := db.Where("id = ?", v.ID).First(&current).Error; err != nil { if err := db.Where("id = ?", v.ID).First(&current).Error; err != nil {
return errors.New("查询字典数据失败") return errors.New("查询字典数据失败")
@@ -148,7 +149,7 @@ func (r *dictionaryRepo) checkDictionaryCircularReference(ctx context.Context, c
return errors.New("不能将字典设置为自己的父级") return errors.New("不能将字典设置为自己的父级")
} }
var parent dictionaryPO var parent dictionaryPO
err := r.data.gormDB.WithContext(ctx).Where("id = ?", parentID).First(&parent).Error err := r.data.DB().WithContext(ctx).Where("id = ?", parentID).First(&parent).Error
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil return nil
} }
@@ -161,7 +162,7 @@ func (r *dictionaryRepo) checkDictionaryCircularReference(ctx context.Context, c
return nil return nil
} }
func (r *dictionaryRepo) DeleteDictionary(ctx context.Context, id uint) error { func (r *dictionaryRepo) DeleteDictionary(ctx context.Context, id uint) error {
db := r.data.gormDB.WithContext(ctx) db := r.data.DB().WithContext(ctx)
var dictionary dictionaryPO var dictionary dictionaryPO
if err := db.First(&dictionary, id).Error; err != nil { if err := db.First(&dictionary, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -176,7 +177,7 @@ func (r *dictionaryRepo) DeleteDictionary(ctx context.Context, id uint) error {
} }
func (r *dictionaryRepo) FindDictionary(ctx context.Context, id uint, typ string, status *bool, details bool) (*biz.Dictionary, error) { func (r *dictionaryRepo) FindDictionary(ctx context.Context, id uint, typ string, status *bool, details bool) (*biz.Dictionary, error) {
var po dictionaryPO var po dictionaryPO
db := r.data.gormDB.WithContext(ctx) db := r.data.DB().WithContext(ctx)
active := true active := true
if status != nil { if status != nil {
active = *status active = *status
@@ -201,12 +202,12 @@ func (r *dictionaryRepo) FindDictionary(ctx context.Context, id uint, typ string
func (r *dictionaryRepo) ExportDictionary(ctx context.Context, id uint) (*biz.Dictionary, error) { func (r *dictionaryRepo) ExportDictionary(ctx context.Context, id uint) (*biz.Dictionary, error) {
var po dictionaryPO var po dictionaryPO
if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil { if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err return nil, err
} }
value := dictionaryFromPO(po) value := dictionaryFromPO(po)
var detailPOs []dictionaryDetailPO var detailPOs []dictionaryDetailPO
if err := r.data.gormDB.WithContext(ctx).Where("sys_dictionary_id = ?", id).Order("sort").Find(&detailPOs).Error; err != nil { if err := r.data.DB().WithContext(ctx).Where("sys_dictionary_id = ?", id).Order("sort").Find(&detailPOs).Error; err != nil {
return nil, err return nil, err
} }
value.Details = make([]*biz.DictionaryDetail, 0, len(detailPOs)) value.Details = make([]*biz.DictionaryDetail, 0, len(detailPOs))
@@ -216,7 +217,7 @@ func (r *dictionaryRepo) ExportDictionary(ctx context.Context, id uint) (*biz.Di
return value, nil return value, nil
} }
func (r *dictionaryRepo) ListDictionaries(ctx context.Context, page, size int, name, typ string, details bool) ([]*biz.Dictionary, int64, error) { func (r *dictionaryRepo) ListDictionaries(ctx context.Context, page, size int, name, typ string, details bool) ([]*biz.Dictionary, int64, error) {
db := r.data.gormDB.WithContext(ctx).Model(&dictionaryPO{}) db := r.data.DB().WithContext(ctx).Model(&dictionaryPO{})
if name != "" { if name != "" {
like := "%" + name + "%" like := "%" + name + "%"
db = db.Where("name LIKE ? OR type LIKE ?", like, like) db = db.Where("name LIKE ? OR type LIKE ?", like, like)
@@ -245,7 +246,7 @@ func (r *dictionaryRepo) ListDictionaries(ctx context.Context, page, size int, n
childrenByParent := make(map[uint][]dictionaryPO) childrenByParent := make(map[uint][]dictionaryPO)
if !details && len(parentIDs) > 0 { if !details && len(parentIDs) > 0 {
var children []dictionaryPO var children []dictionaryPO
if err := r.data.gormDB.WithContext(ctx).Where("parent_id IN ?", parentIDs).Find(&children).Error; err != nil { if err := r.data.DB().WithContext(ctx).Where("parent_id IN ?", parentIDs).Find(&children).Error; err != nil {
return nil, 0, err return nil, 0, err
} }
for _, child := range children { for _, child := range children {
@@ -257,7 +258,7 @@ func (r *dictionaryRepo) ListDictionaries(ctx context.Context, page, size int, n
detailsByDictionary := make(map[uint][]dictionaryDetailPO) detailsByDictionary := make(map[uint][]dictionaryDetailPO)
if details && len(dictionaryIDs) > 0 { if details && len(dictionaryIDs) > 0 {
var values []dictionaryDetailPO var values []dictionaryDetailPO
if err := r.data.gormDB.WithContext(ctx).Where("sys_dictionary_id IN ?", dictionaryIDs).Order("sort").Find(&values).Error; err != nil { if err := r.data.DB().WithContext(ctx).Where("sys_dictionary_id IN ?", dictionaryIDs).Order("sort").Find(&values).Error; err != nil {
return nil, 0, err return nil, 0, err
} }
for _, value := range values { for _, value := range values {
@@ -289,13 +290,13 @@ func (r *dictionaryRepo) CreateDictionaryDetail(ctx context.Context, v *biz.Dict
po.Path = "" po.Path = ""
if v.ParentID != nil { if v.ParentID != nil {
var parent dictionaryDetailPO var parent dictionaryDetailPO
if err := r.data.gormDB.WithContext(ctx).First(&parent, *v.ParentID).Error; err != nil { if err := r.data.DB().WithContext(ctx).First(&parent, *v.ParentID).Error; err != nil {
return err return err
} }
po.Level = parent.Level + 1 po.Level = parent.Level + 1
po.Path = strings.Trim(strings.Join([]string{parent.Path, fmt.Sprint(parent.ID)}, ","), ",") po.Path = strings.Trim(strings.Join([]string{parent.Path, fmt.Sprint(parent.ID)}, ","), ",")
} }
if err := r.data.gormDB.WithContext(ctx).Create(&po).Error; err != nil { if err := r.data.DB().WithContext(ctx).Create(&po).Error; err != nil {
return err return err
} }
v.ID = po.ID v.ID = po.ID
@@ -305,7 +306,7 @@ func (r *dictionaryRepo) CreateDictionaryDetail(ctx context.Context, v *biz.Dict
} }
func (r *dictionaryRepo) UpdateDictionaryDetail(ctx context.Context, v *biz.DictionaryDetail) error { func (r *dictionaryRepo) UpdateDictionaryDetail(ctx context.Context, v *biz.DictionaryDetail) error {
var po dictionaryDetailPO var po dictionaryDetailPO
db := r.data.gormDB.WithContext(ctx) db := r.data.DB().WithContext(ctx)
if err := db.First(&po, v.ID).Error; err != nil { if err := db.First(&po, v.ID).Error; err != nil {
return err return err
} }
@@ -350,23 +351,23 @@ func (r *dictionaryRepo) updateDictionaryDetailChildren(db *gorm.DB, parentID ui
} }
func (r *dictionaryRepo) DeleteDictionaryDetail(ctx context.Context, id uint) error { func (r *dictionaryRepo) DeleteDictionaryDetail(ctx context.Context, id uint) error {
var count int64 var count int64
if err := r.data.gormDB.WithContext(ctx).Model(&dictionaryDetailPO{}).Where("parent_id = ?", id).Count(&count).Error; err != nil { if err := r.data.DB().WithContext(ctx).Model(&dictionaryDetailPO{}).Where("parent_id = ?", id).Count(&count).Error; err != nil {
return err return err
} }
if count > 0 { if count > 0 {
return errors.New("该字典详情下还有子项,无法删除") return errors.New("该字典详情下还有子项,无法删除")
} }
return r.data.gormDB.WithContext(ctx).Delete(&dictionaryDetailPO{}, id).Error return r.data.DB().WithContext(ctx).Delete(&dictionaryDetailPO{}, id).Error
} }
func (r *dictionaryRepo) FindDictionaryDetail(ctx context.Context, id uint) (*biz.DictionaryDetail, error) { func (r *dictionaryRepo) FindDictionaryDetail(ctx context.Context, id uint) (*biz.DictionaryDetail, error) {
var po dictionaryDetailPO var po dictionaryDetailPO
if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil { if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err return nil, err
} }
return detailFromPO(po), nil return detailFromPO(po), nil
} }
func (r *dictionaryRepo) ListDictionaryDetails(ctx context.Context, page, size int, filter biz.DictionaryDetailFilter) ([]*biz.DictionaryDetail, int64, error) { func (r *dictionaryRepo) ListDictionaryDetails(ctx context.Context, page, size int, filter biz.DictionaryDetailFilter) ([]*biz.DictionaryDetail, int64, error) {
db := r.data.gormDB.WithContext(ctx).Model(&dictionaryDetailPO{}) db := r.data.DB().WithContext(ctx).Model(&dictionaryDetailPO{})
if filter.DictionaryID != 0 { if filter.DictionaryID != 0 {
db = db.Where("sys_dictionary_id = ?", filter.DictionaryID) db = db.Where("sys_dictionary_id = ?", filter.DictionaryID)
} }
@@ -390,7 +391,7 @@ func (r *dictionaryRepo) ListDictionaryDetails(ctx context.Context, page, size i
return nil, 0, err return nil, 0, err
} }
var pos []dictionaryDetailPO var pos []dictionaryDetailPO
if err := applyRequiredPagination(db.Order("sort,id"), page, size, 100).Find(&pos).Error; err != nil { if err := pagination.ApplyRequired(db.Order("sort,id"), page, size, 100).Find(&pos).Error; err != nil {
return nil, 0, err return nil, 0, err
} }
out := make([]*biz.DictionaryDetail, 0, len(pos)) out := make([]*biz.DictionaryDetail, 0, len(pos))
@@ -405,13 +406,13 @@ func (r *dictionaryRepo) DictionaryDetailTree(ctx context.Context, dictionaryID
// public dictionary lookup, it does not require the dictionary itself to // public dictionary lookup, it does not require the dictionary itself to
// be enabled. // be enabled.
var dictionary dictionaryPO var dictionary dictionaryPO
if err := r.data.gormDB.WithContext(ctx).Where("type = ?", typ).First(&dictionary).Error; err != nil { if err := r.data.DB().WithContext(ctx).Where("type = ?", typ).First(&dictionary).Error; err != nil {
return nil, err return nil, err
} }
dictionaryID = dictionary.ID dictionaryID = dictionary.ID
} }
var pos []dictionaryDetailPO var pos []dictionaryDetailPO
if err := r.data.gormDB.WithContext(ctx).Where("sys_dictionary_id = ? AND parent_id IS NULL", dictionaryID).Order("sort").Find(&pos).Error; err != nil { if err := r.data.DB().WithContext(ctx).Where("sys_dictionary_id = ? AND parent_id IS NULL", dictionaryID).Order("sort").Find(&pos).Error; err != nil {
return nil, err return nil, err
} }
roots := make([]*biz.DictionaryDetail, 0, len(pos)) roots := make([]*biz.DictionaryDetail, 0, len(pos))
@@ -426,7 +427,7 @@ func (r *dictionaryRepo) DictionaryDetailTree(ctx context.Context, dictionaryID
} }
func (r *dictionaryRepo) DictionaryDetailsByParent(ctx context.Context, dictionaryID uint, parentID *uint, includeChildren bool) ([]*biz.DictionaryDetail, error) { func (r *dictionaryRepo) DictionaryDetailsByParent(ctx context.Context, dictionaryID uint, parentID *uint, includeChildren bool) ([]*biz.DictionaryDetail, error) {
db := r.data.gormDB.WithContext(ctx).Where("sys_dictionary_id = ?", dictionaryID) db := r.data.DB().WithContext(ctx).Where("sys_dictionary_id = ?", dictionaryID)
if parentID == nil { if parentID == nil {
db = db.Where("parent_id IS NULL") db = db.Where("parent_id IS NULL")
} else { } else {
@@ -451,7 +452,7 @@ func (r *dictionaryRepo) DictionaryDetailsByParent(ctx context.Context, dictiona
func (r *dictionaryRepo) loadDictionaryDetailChildren(ctx context.Context, parent *biz.DictionaryDetail) error { func (r *dictionaryRepo) loadDictionaryDetailChildren(ctx context.Context, parent *biz.DictionaryDetail) error {
var pos []dictionaryDetailPO var pos []dictionaryDetailPO
if err := r.data.gormDB.WithContext(ctx).Where("parent_id = ?", parent.ID).Order("sort").Find(&pos).Error; err != nil { if err := r.data.DB().WithContext(ctx).Where("parent_id = ?", parent.ID).Order("sort").Find(&pos).Error; err != nil {
return err return err
} }
parent.Children = make([]*biz.DictionaryDetail, 0, len(pos)) parent.Children = make([]*biz.DictionaryDetail, 0, len(pos))
@@ -1,4 +1,4 @@
package data package system
import ( import (
"context" "context"
@@ -1,10 +1,11 @@
package data package system
import ( import (
"context" "context"
"time" "time"
"kra/internal/biz" "kra/internal/biz"
"kra/pkg/pagination"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -29,14 +30,14 @@ func errorFromPO(v errorRecordPO) *biz.ErrorRecord {
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} 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 { func (r *auditRecorderRepo) CreateError(ctx context.Context, v *biz.ErrorRecord) error {
if !r.data.databaseReady.Load() { if !r.data.DatabaseReady() {
// Silently ignore error records before the database is initialized. // Silently ignore error records before the database is initialized.
return nil return nil
} }
if v.Status == "" { if v.Status == "" {
v.Status = "未处理" v.Status = "未处理"
} }
return r.data.gormDB.WithContext(ctx).Create(&errorRecordPO{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}).Error return r.data.DB().WithContext(ctx).Create(&errorRecordPO{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}).Error
} }
func (r *auditQueryRepo) UpdateError(ctx context.Context, v *biz.ErrorRecord) error { func (r *auditQueryRepo) UpdateError(ctx context.Context, v *biz.ErrorRecord) error {
updates := make(map[string]any, 9) updates := make(map[string]any, 9)
@@ -67,20 +68,20 @@ func (r *auditQueryRepo) UpdateError(ctx context.Context, v *biz.ErrorRecord) er
if v.Status != "" { if v.Status != "" {
updates["status"] = v.Status updates["status"] = v.Status
} }
return r.data.gormDB.WithContext(ctx).Model(&errorRecordPO{}).Where("id = ?", v.ID).Updates(updates).Error return r.data.DB().WithContext(ctx).Model(&errorRecordPO{}).Where("id = ?", v.ID).Updates(updates).Error
} }
func (r *auditQueryRepo) DeleteErrors(ctx context.Context, ids []uint) error { func (r *auditQueryRepo) DeleteErrors(ctx context.Context, ids []uint) error {
return r.data.gormDB.WithContext(ctx).Delete(&errorRecordPO{}, ids).Error return r.data.DB().WithContext(ctx).Delete(&errorRecordPO{}, ids).Error
} }
func (r *auditQueryRepo) FindError(ctx context.Context, id uint) (*biz.ErrorRecord, error) { func (r *auditQueryRepo) FindError(ctx context.Context, id uint) (*biz.ErrorRecord, error) {
var po errorRecordPO var po errorRecordPO
if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil { if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err return nil, err
} }
return errorFromPO(po), nil return errorFromPO(po), nil
} }
func (r *auditQueryRepo) ListErrors(ctx context.Context, page, size int, q *biz.ErrorRecord) ([]*biz.ErrorRecord, int64, error) { func (r *auditQueryRepo) ListErrors(ctx context.Context, page, size int, q *biz.ErrorRecord) ([]*biz.ErrorRecord, int64, error) {
db := r.data.gormDB.WithContext(ctx).Model(&errorRecordPO{}) db := r.data.DB().WithContext(ctx).Model(&errorRecordPO{})
if q != nil { if q != nil {
if len(q.CreatedAtRange) == 2 { if len(q.CreatedAtRange) == 2 {
db = db.Where("created_at BETWEEN ? AND ?", q.CreatedAtRange[0], q.CreatedAtRange[1]) db = db.Where("created_at BETWEEN ? AND ?", q.CreatedAtRange[0], q.CreatedAtRange[1])
@@ -97,7 +98,7 @@ func (r *auditQueryRepo) ListErrors(ctx context.Context, page, size int, q *biz.
return nil, 0, err return nil, 0, err
} }
var pos []errorRecordPO var pos []errorRecordPO
if err := applyPagination(db.Order("created_at desc"), page, size, 100).Find(&pos).Error; err != nil { if err := pagination.Apply(db.Order("created_at desc"), page, size, 100).Find(&pos).Error; err != nil {
return nil, 0, err return nil, 0, err
} }
out := make([]*biz.ErrorRecord, 0, len(pos)) out := make([]*biz.ErrorRecord, 0, len(pos))
@@ -1,4 +1,4 @@
package data package system
import ( import (
"context" "context"
@@ -1,4 +1,4 @@
package data package system
import ( import (
"context" "context"
@@ -57,9 +57,9 @@ type exportJoinPO struct {
func (exportJoinPO) TableName() string { return "sys_export_template_join" } func (exportJoinPO) TableName() string { return "sys_export_template_join" }
type exportRepo struct{ data *Data } type exportRepo struct{ data Provider }
func NewExportRepo(data *Data) biz.ExportRepo { return &exportRepo{data: data} } func NewExportRepo(data Provider) biz.ExportRepo { return &exportRepo{data: data} }
func exportFromPO(po exportTemplatePO, conditions []exportConditionPO, joins []exportJoinPO) *biz.ExportTemplate { 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} 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}
if conditions != nil { if conditions != nil {
@@ -138,7 +138,7 @@ func (r *exportRepo) saveRelations(tx *gorm.DB, v *biz.ExportTemplate, resetIDs,
return nil return nil
} }
func (r *exportRepo) CreateExportTemplate(ctx context.Context, v *biz.ExportTemplate) error { func (r *exportRepo) CreateExportTemplate(ctx context.Context, v *biz.ExportTemplate) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
po := exportToPO(v) po := exportToPO(v)
if err := tx.Create(&po).Error; err != nil { if err := tx.Create(&po).Error; err != nil {
return err return err
@@ -148,7 +148,7 @@ func (r *exportRepo) CreateExportTemplate(ctx context.Context, v *biz.ExportTemp
}) })
} }
func (r *exportRepo) UpdateExportTemplate(ctx context.Context, v *biz.ExportTemplate) error { func (r *exportRepo) UpdateExportTemplate(ctx context.Context, v *biz.ExportTemplate) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
po := exportToPO(v) po := exportToPO(v)
if err := tx.Model(&exportTemplatePO{}).Where("id = ?", v.ID).Updates(&po).Error; err != nil { if err := tx.Model(&exportTemplatePO{}).Where("id = ?", v.ID).Updates(&po).Error; err != nil {
return err return err
@@ -157,11 +157,11 @@ func (r *exportRepo) UpdateExportTemplate(ctx context.Context, v *biz.ExportTemp
}) })
} }
func (r *exportRepo) DeleteExportTemplates(ctx context.Context, ids []uint) error { func (r *exportRepo) DeleteExportTemplates(ctx context.Context, ids []uint) error {
return r.data.gormDB.WithContext(ctx).Delete(&[]exportTemplatePO{}, "id IN ?", ids).Error return r.data.DB().WithContext(ctx).Delete(&[]exportTemplatePO{}, "id IN ?", ids).Error
} }
func (r *exportRepo) FindExportTemplate(ctx context.Context, id uint, tid string) (*biz.ExportTemplate, error) { func (r *exportRepo) FindExportTemplate(ctx context.Context, id uint, tid string) (*biz.ExportTemplate, error) {
var po exportTemplatePO var po exportTemplatePO
db := r.data.gormDB.WithContext(ctx) db := r.data.DB().WithContext(ctx)
var err error var err error
if tid != "" { if tid != "" {
err = db.Where("template_id = ?", tid).First(&po).Error err = db.Where("template_id = ?", tid).First(&po).Error
@@ -182,7 +182,7 @@ func (r *exportRepo) FindExportTemplate(ctx context.Context, id uint, tid string
return exportFromPO(po, conditions, joins), nil return exportFromPO(po, conditions, joins), nil
} }
func (r *exportRepo) ListExportTemplates(ctx context.Context, page, size int, q *biz.ExportTemplate) ([]*biz.ExportTemplate, int64, error) { func (r *exportRepo) ListExportTemplates(ctx context.Context, page, size int, q *biz.ExportTemplate) ([]*biz.ExportTemplate, int64, error) {
db := r.data.gormDB.WithContext(ctx).Model(&exportTemplatePO{}) db := r.data.DB().WithContext(ctx).Model(&exportTemplatePO{})
if q != nil { if q != nil {
if q.StartCreatedAt != nil && q.EndCreatedAt != nil { if q.StartCreatedAt != nil && q.EndCreatedAt != nil {
db = db.Where("created_at BETWEEN ? AND ?", q.StartCreatedAt, q.EndCreatedAt) db = db.Where("created_at BETWEEN ? AND ?", q.StartCreatedAt, q.EndCreatedAt)
@@ -218,7 +218,7 @@ func (r *exportRepo) ListExportTemplates(ctx context.Context, page, size int, q
} }
func (r *exportRepo) QueryExport(ctx context.Context, t *biz.ExportTemplate, params map[string]string) ([]map[string]any, string, error) { func (r *exportRepo) QueryExport(ctx context.Context, t *biz.ExportTemplate, params map[string]string) ([]map[string]any, string, error) {
selected, err := r.data.database(t.DBName) selected, err := r.data.Database(t.DBName)
if err != nil { if err != nil {
return nil, "", err return nil, "", err
} }
@@ -355,7 +355,7 @@ func (r *exportRepo) PreviewExport(ctx context.Context, t *biz.ExportTemplate, p
wheres := make([]string, 0) wheres := make([]string, 0)
if params["filterDeleted"] == "true" { if params["filterDeleted"] == "true" {
wheres = append(wheres, fmt.Sprintf("%s.deleted_at IS NULL", t.TableName)) wheres = append(wheres, fmt.Sprintf("%s.deleted_at IS NULL", t.TableName))
selected, err := r.data.database(t.DBName) selected, err := r.data.Database(t.DBName)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -442,7 +442,7 @@ func (r *exportRepo) ImportExportRows(ctx context.Context, t *biz.ExportTemplate
// is still treated as custom SQL and is allowed to return the driver's // is still treated as custom SQL and is allowed to return the driver's
// native error instead of silently falling back to GORM insertion. // native error instead of silently falling back to GORM insertion.
sql := t.ImportSQL sql := t.ImportSQL
selected, err := r.data.database(t.DBName) selected, err := r.data.Database(t.DBName)
if err != nil { if err != nil {
return err return err
} }
@@ -1,4 +1,4 @@
package data package system
import ( import (
"bytes" "bytes"
@@ -24,7 +24,7 @@ const (
) )
func (r *logFileRepo) configuredLogRoot() (root string, exists bool, err error) { func (r *logFileRepo) configuredLogRoot() (root string, exists bool, err error) {
admin := r.data.runtime.Admin() admin := r.data.Runtime().Admin()
if admin == nil || admin.Zap == nil || strings.TrimSpace(admin.Zap.Director) == "" { if admin == nil || admin.Zap == nil || strings.TrimSpace(admin.Zap.Director) == "" {
return "", false, biz.ErrLogRootUnavailable return "", false, biz.ErrLogRootUnavailable
} }
@@ -1,4 +1,4 @@
package data package system
import ( import (
"context" "context"
@@ -1,10 +1,11 @@
package data package system
import ( import (
"context" "context"
"time" "time"
"kra/internal/biz" "kra/internal/biz"
"kra/pkg/pagination"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -23,18 +24,18 @@ type loginLogPO struct {
func (loginLogPO) TableName() string { return "sys_login_logs" } func (loginLogPO) TableName() string { return "sys_login_logs" }
func (r *auditRecorderRepo) RecordLogin(ctx context.Context, v *biz.LoginLog) error { func (r *auditRecorderRepo) RecordLogin(ctx context.Context, v *biz.LoginLog) error {
if !r.data.databaseReady.Load() { if !r.data.DatabaseReady() {
// The login endpoint remains reachable before database initialization; // The login endpoint remains reachable before database initialization;
// skip the audit write until storage is ready. // skip the audit write until storage is ready.
return nil return nil
} }
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 return r.data.DB().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 { func loginFromPO(v loginLogPO) *biz.LoginLog {
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} 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) { 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{}) db := r.data.DB().WithContext(ctx).Model(&loginLogPO{})
if q != nil { if q != nil {
if q.Username != "" { if q.Username != "" {
db = db.Where("username LIKE ?", "%"+q.Username+"%") db = db.Where("username LIKE ?", "%"+q.Username+"%")
@@ -48,7 +49,7 @@ func (r *auditQueryRepo) ListLogins(ctx context.Context, page, size int, q *biz.
return nil, 0, err return nil, 0, err
} }
var pos []loginLogPO var pos []loginLogPO
if err := applyRequiredPagination(db.Order("id desc"), page, size, 100).Find(&pos).Error; err != nil { if err := pagination.ApplyRequired(db.Order("id desc"), page, size, 100).Find(&pos).Error; err != nil {
return nil, 0, err return nil, 0, err
} }
ids := make([]uint, 0, len(pos)) ids := make([]uint, 0, len(pos))
@@ -57,7 +58,7 @@ func (r *auditQueryRepo) ListLogins(ctx context.Context, page, size int, q *biz.
ids = append(ids, value.UserID) ids = append(ids, value.UserID)
} }
} }
users := auditUsers(ctx, r.data.gormDB.WithContext(ctx), ids) users := auditUsers(ctx, r.data.DB().WithContext(ctx), ids)
out := make([]*biz.LoginLog, 0, len(pos)) out := make([]*biz.LoginLog, 0, len(pos))
for _, po := range pos { for _, po := range pos {
value := loginFromPO(po) value := loginFromPO(po)
@@ -68,14 +69,14 @@ func (r *auditQueryRepo) ListLogins(ctx context.Context, page, size int, q *biz.
} }
func (r *auditQueryRepo) FindLogin(ctx context.Context, id uint) (*biz.LoginLog, error) { func (r *auditQueryRepo) FindLogin(ctx context.Context, id uint) (*biz.LoginLog, error) {
var po loginLogPO var po loginLogPO
if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil { if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err return nil, err
} }
return loginFromPO(po), nil return loginFromPO(po), nil
} }
func (r *auditQueryRepo) DeleteLogins(ctx context.Context, ids []int) error { func (r *auditQueryRepo) DeleteLogins(ctx context.Context, ids []int) error {
if len(ids) == 1 && ids[0] == 0 { if len(ids) == 1 && ids[0] == 0 {
return r.data.gormDB.WithContext(ctx).Delete(&loginLogPO{}).Error return r.data.DB().WithContext(ctx).Delete(&loginLogPO{}).Error
} }
return r.data.gormDB.WithContext(ctx).Delete(&loginLogPO{}, "id IN ?", ids).Error return r.data.DB().WithContext(ctx).Delete(&loginLogPO{}, "id IN ?", ids).Error
} }
@@ -1,4 +1,4 @@
package data package system
import ( import (
"context" "context"
@@ -39,9 +39,9 @@ type categoryPO struct {
func (categoryPO) TableName() string { return "media_attachment_category" } func (categoryPO) TableName() string { return "media_attachment_category" }
type mediaRepo struct{ data *Data } type mediaRepo struct{ data Provider }
func NewMediaRepo(data *Data) biz.MediaRepo { return &mediaRepo{data: data} } func NewMediaRepo(data Provider) biz.MediaRepo { return &mediaRepo{data: data} }
func mediaFromPO(v mediaPO) *biz.MediaFile { func mediaFromPO(v mediaPO) *biz.MediaFile {
return &biz.MediaFile{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, Name: v.Name, CategoryID: v.CategoryID, URL: v.URL, Tag: v.Tag, Key: v.Key, Size: v.Size, Mime: v.Mime, MD5: v.MD5, UserID: v.UserID} return &biz.MediaFile{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, Name: v.Name, CategoryID: v.CategoryID, URL: v.URL, Tag: v.Tag, Key: v.Key, Size: v.Size, Mime: v.Mime, MD5: v.MD5, UserID: v.UserID}
} }
@@ -50,7 +50,7 @@ func mediaToPO(v *biz.MediaFile) mediaPO {
} }
func (r *mediaRepo) CreateMedia(ctx context.Context, v *biz.MediaFile) error { func (r *mediaRepo) CreateMedia(ctx context.Context, v *biz.MediaFile) error {
po := mediaToPO(v) po := mediaToPO(v)
if err := r.data.gormDB.WithContext(ctx).Create(&po).Error; err != nil { if err := r.data.DB().WithContext(ctx).Create(&po).Error; err != nil {
return err return err
} }
v.ID = po.ID v.ID = po.ID
@@ -60,20 +60,20 @@ func (r *mediaRepo) CreateMedia(ctx context.Context, v *biz.MediaFile) error {
} }
func (r *mediaRepo) FindMedia(ctx context.Context, id uint) (*biz.MediaFile, error) { func (r *mediaRepo) FindMedia(ctx context.Context, id uint) (*biz.MediaFile, error) {
var po mediaPO var po mediaPO
if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil { if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err return nil, err
} }
return mediaFromPO(po), nil return mediaFromPO(po), nil
} }
func (r *mediaRepo) FindMediaByHash(ctx context.Context, userID uint, hash string) (*biz.MediaFile, error) { func (r *mediaRepo) FindMediaByHash(ctx context.Context, userID uint, hash string) (*biz.MediaFile, error) {
var po mediaPO var po mediaPO
if err := r.data.gormDB.WithContext(ctx).Where("user_id = ? AND md5 = ?", userID, hash).First(&po).Error; err != nil { if err := r.data.DB().WithContext(ctx).Where("user_id = ? AND md5 = ?", userID, hash).First(&po).Error; err != nil {
return nil, err return nil, err
} }
return mediaFromPO(po), nil return mediaFromPO(po), nil
} }
func (r *mediaRepo) ListMedia(ctx context.Context, filter biz.MediaFilter) ([]*biz.MediaFile, int64, error) { func (r *mediaRepo) ListMedia(ctx context.Context, filter biz.MediaFilter) ([]*biz.MediaFile, int64, error) {
db := r.data.gormDB.WithContext(ctx).Model(&mediaPO{}) db := r.data.DB().WithContext(ctx).Model(&mediaPO{})
if filter.Keyword != "" { if filter.Keyword != "" {
db = db.Where("name LIKE ?", "%"+filter.Keyword+"%") db = db.Where("name LIKE ?", "%"+filter.Keyword+"%")
} }
@@ -129,17 +129,17 @@ func (r *mediaRepo) UpdateMediaName(ctx context.Context, id uint, name string) e
// The compatible flow performs a First before Update, so a missing record returns a // The compatible flow performs a First before Update, so a missing record returns a
// not-found error instead of silently succeeding with RowsAffected=0. // not-found error instead of silently succeeding with RowsAffected=0.
var po mediaPO var po mediaPO
if err := r.data.gormDB.WithContext(ctx).Where("id = ?", id).First(&po).Error; err != nil { if err := r.data.DB().WithContext(ctx).Where("id = ?", id).First(&po).Error; err != nil {
return err return err
} }
return r.data.gormDB.WithContext(ctx).Model(&po).Update("name", name).Error return r.data.DB().WithContext(ctx).Model(&po).Update("name", name).Error
} }
func (r *mediaRepo) DeleteMedia(ctx context.Context, id uint) error { func (r *mediaRepo) DeleteMedia(ctx context.Context, id uint) error {
return r.data.gormDB.WithContext(ctx).Unscoped().Delete(&mediaPO{}, id).Error return r.data.DB().WithContext(ctx).Unscoped().Delete(&mediaPO{}, id).Error
} }
func (r *mediaRepo) MediaKeyReferences(ctx context.Context, key string) (int64, error) { func (r *mediaRepo) MediaKeyReferences(ctx context.Context, key string) (int64, error) {
var count int64 var count int64
err := r.data.gormDB.WithContext(ctx).Model(&mediaPO{}).Where(map[string]any{"key": key}).Count(&count).Error err := r.data.DB().WithContext(ctx).Model(&mediaPO{}).Where(map[string]any{"key": key}).Count(&count).Error
return count, err return count, err
} }
func (r *mediaRepo) CreateMediaBatch(ctx context.Context, items []*biz.MediaFile) error { func (r *mediaRepo) CreateMediaBatch(ctx context.Context, items []*biz.MediaFile) error {
@@ -147,10 +147,10 @@ func (r *mediaRepo) CreateMediaBatch(ctx context.Context, items []*biz.MediaFile
for _, v := range items { for _, v := range items {
pos = append(pos, mediaToPO(v)) pos = append(pos, mediaToPO(v))
} }
return r.data.gormDB.WithContext(ctx).Create(&pos).Error return r.data.DB().WithContext(ctx).Create(&pos).Error
} }
func (r *mediaRepo) SaveCategory(ctx context.Context, v *biz.AttachmentCategory) error { func (r *mediaRepo) SaveCategory(ctx context.Context, v *biz.AttachmentCategory) error {
db := r.data.gormDB.WithContext(ctx) db := r.data.DB().WithContext(ctx)
var count int64 var count int64
query := db.Model(&categoryPO{}).Where("name = ? AND pid = ?", v.Name, v.ParentID) query := db.Model(&categoryPO{}).Where("name = ? AND pid = ?", v.Name, v.ParentID)
if err := query.Count(&count).Error; err != nil { if err := query.Count(&count).Error; err != nil {
@@ -171,17 +171,17 @@ func (r *mediaRepo) SaveCategory(ctx context.Context, v *biz.AttachmentCategory)
} }
func (r *mediaRepo) DeleteCategory(ctx context.Context, id uint) error { func (r *mediaRepo) DeleteCategory(ctx context.Context, id uint) error {
var count int64 var count int64
if err := r.data.gormDB.WithContext(ctx).Model(&categoryPO{}).Where("pid = ?", id).Count(&count).Error; err != nil { if err := r.data.DB().WithContext(ctx).Model(&categoryPO{}).Where("pid = ?", id).Count(&count).Error; err != nil {
return err return err
} }
if count > 0 { if count > 0 {
return errors.New("请先删除子级") return errors.New("请先删除子级")
} }
return r.data.gormDB.WithContext(ctx).Unscoped().Delete(&categoryPO{}, id).Error return r.data.DB().WithContext(ctx).Unscoped().Delete(&categoryPO{}, id).Error
} }
func (r *mediaRepo) ListCategories(ctx context.Context) ([]*biz.AttachmentCategory, error) { func (r *mediaRepo) ListCategories(ctx context.Context) ([]*biz.AttachmentCategory, error) {
var pos []categoryPO var pos []categoryPO
if err := r.data.gormDB.WithContext(ctx).Find(&pos).Error; err != nil { if err := r.data.DB().WithContext(ctx).Find(&pos).Error; err != nil {
return nil, err return nil, err
} }
byID := map[uint]*biz.AttachmentCategory{} byID := map[uint]*biz.AttachmentCategory{}
@@ -1,4 +1,4 @@
package data package system
import ( import (
"context" "context"
@@ -1,4 +1,4 @@
package data package system
import ( import (
"context" "context"
@@ -47,7 +47,7 @@ func uploadFromPO(v uploadSessionPO) *biz.UploadSession {
} }
func (r *mediaRepo) FindCompletedSession(ctx context.Context, userID uint, hash string) (*biz.UploadSession, error) { func (r *mediaRepo) FindCompletedSession(ctx context.Context, userID uint, hash string) (*biz.UploadSession, error) {
var po uploadSessionPO var po uploadSessionPO
if err := r.data.gormDB.WithContext(ctx).Where("user_id = ? AND file_hash = ? AND status = ?", userID, hash, "completed").First(&po).Error; err != nil { if err := r.data.DB().WithContext(ctx).Where("user_id = ? AND file_hash = ? AND status = ?", userID, hash, "completed").First(&po).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, biz.ErrUploadSessionNotFound return nil, biz.ErrUploadSessionNotFound
} }
@@ -57,7 +57,7 @@ func (r *mediaRepo) FindCompletedSession(ctx context.Context, userID uint, hash
} }
func (r *mediaRepo) FindUploadingSession(ctx context.Context, userID uint, hash string) (*biz.UploadSession, error) { func (r *mediaRepo) FindUploadingSession(ctx context.Context, userID uint, hash string) (*biz.UploadSession, error) {
var po uploadSessionPO var po uploadSessionPO
if err := r.data.gormDB.WithContext(ctx).Where("user_id = ? AND file_hash = ? AND status = ?", userID, hash, "uploading").First(&po).Error; err != nil { if err := r.data.DB().WithContext(ctx).Where("user_id = ? AND file_hash = ? AND status = ?", userID, hash, "uploading").First(&po).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, biz.ErrUploadSessionNotFound return nil, biz.ErrUploadSessionNotFound
} }
@@ -67,7 +67,7 @@ func (r *mediaRepo) FindUploadingSession(ctx context.Context, userID uint, hash
} }
func (r *mediaRepo) CreateUploadSession(ctx context.Context, v *biz.UploadSession) error { func (r *mediaRepo) CreateUploadSession(ctx context.Context, v *biz.UploadSession) error {
po := uploadSessionPO{UserID: v.UserID, FileName: v.FileName, FileHash: v.FileHash, FileSize: v.FileSize, ChunkSize: v.ChunkSize, ChunkTotal: v.ChunkTotal, Status: v.Status} po := uploadSessionPO{UserID: v.UserID, FileName: v.FileName, FileHash: v.FileHash, FileSize: v.FileSize, ChunkSize: v.ChunkSize, ChunkTotal: v.ChunkTotal, Status: v.Status}
if err := r.data.gormDB.WithContext(ctx).Create(&po).Error; err != nil { if err := r.data.DB().WithContext(ctx).Create(&po).Error; err != nil {
return err return err
} }
v.ID = po.ID v.ID = po.ID
@@ -75,36 +75,36 @@ func (r *mediaRepo) CreateUploadSession(ctx context.Context, v *biz.UploadSessio
} }
func (r *mediaRepo) FindUploadSession(ctx context.Context, id uint) (*biz.UploadSession, error) { func (r *mediaRepo) FindUploadSession(ctx context.Context, id uint) (*biz.UploadSession, error) {
var po uploadSessionPO var po uploadSessionPO
if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil { if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err return nil, err
} }
return uploadFromPO(po), nil return uploadFromPO(po), nil
} }
func (r *mediaRepo) ClaimUploadSession(ctx context.Context, id uint) (bool, error) { func (r *mediaRepo) ClaimUploadSession(ctx context.Context, id uint) (bool, error) {
result := r.data.gormDB.WithContext(ctx).Model(&uploadSessionPO{}).Where("id = ? AND status = ?", id, "uploading").Update("status", "merging") result := r.data.DB().WithContext(ctx).Model(&uploadSessionPO{}).Where("id = ? AND status = ?", id, "uploading").Update("status", "merging")
return result.RowsAffected == 1, result.Error return result.RowsAffected == 1, result.Error
} }
func (r *mediaRepo) FailUploadSession(ctx context.Context, id uint) error { func (r *mediaRepo) FailUploadSession(ctx context.Context, id uint) error {
return r.data.gormDB.WithContext(ctx).Model(&uploadSessionPO{}).Where("id = ?", id).Update("status", "failed").Error return r.data.DB().WithContext(ctx).Model(&uploadSessionPO{}).Where("id = ?", id).Update("status", "failed").Error
} }
func (r *mediaRepo) CompleteUploadSession(ctx context.Context, id uint, key string, mediaID uint) error { func (r *mediaRepo) CompleteUploadSession(ctx context.Context, id uint, key string, mediaID uint) error {
return r.data.gormDB.WithContext(ctx).Model(&uploadSessionPO{}).Where("id = ?", id).Updates(map[string]any{"status": "completed", "storage_key": key, "media_id": mediaID}).Error return r.data.DB().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 { func (r *mediaRepo) DeleteUploadSession(ctx context.Context, id uint) error {
// The compatible flow uses GORM's normal Delete here, retaining the soft-deleted session // The compatible flow uses GORM's normal Delete here, retaining the soft-deleted session
// for audit/recovery rather than physically removing it. // for audit/recovery rather than physically removing it.
return r.data.gormDB.WithContext(ctx).Delete(&uploadSessionPO{}, id).Error return r.data.DB().WithContext(ctx).Delete(&uploadSessionPO{}, id).Error
} }
func (r *mediaRepo) UpsertChunk(ctx context.Context, uploadID uint, v *biz.UploadChunk) 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} po := uploadChunkPO{UploadID: uploadID, ChunkIndex: v.Index, ChunkHash: v.Hash, Size: v.Size}
return r.data.gormDB.WithContext(ctx).Clauses(clause.OnConflict{ return r.data.DB().WithContext(ctx).Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "upload_id"}, {Name: "chunk_index"}}, Columns: []clause.Column{{Name: "upload_id"}, {Name: "chunk_index"}},
DoUpdates: clause.AssignmentColumns([]string{"chunk_hash", "size", "updated_at", "deleted_at"}), DoUpdates: clause.AssignmentColumns([]string{"chunk_hash", "size", "updated_at", "deleted_at"}),
}).Create(&po).Error }).Create(&po).Error
} }
func (r *mediaRepo) ListChunks(ctx context.Context, uploadID uint) ([]*biz.UploadChunk, error) { func (r *mediaRepo) ListChunks(ctx context.Context, uploadID uint) ([]*biz.UploadChunk, error) {
var pos []uploadChunkPO var pos []uploadChunkPO
if err := r.data.gormDB.WithContext(ctx).Where("upload_id = ?", uploadID).Order("chunk_index").Find(&pos).Error; err != nil { if err := r.data.DB().WithContext(ctx).Where("upload_id = ?", uploadID).Order("chunk_index").Find(&pos).Error; err != nil {
return nil, err return nil, err
} }
out := make([]*biz.UploadChunk, 0, len(pos)) out := make([]*biz.UploadChunk, 0, len(pos))
@@ -114,15 +114,15 @@ func (r *mediaRepo) ListChunks(ctx context.Context, uploadID uint) ([]*biz.Uploa
return out, nil return out, nil
} }
func (r *mediaRepo) DeleteChunks(ctx context.Context, uploadID uint) error { func (r *mediaRepo) DeleteChunks(ctx context.Context, uploadID uint) error {
return r.data.gormDB.WithContext(ctx).Where("upload_id = ?", uploadID).Delete(&uploadChunkPO{}).Error return r.data.DB().WithContext(ctx).Where("upload_id = ?", uploadID).Delete(&uploadChunkPO{}).Error
} }
func (r *mediaRepo) StaleUploadSessionIDs(ctx context.Context, before time.Time) ([]uint, error) { func (r *mediaRepo) StaleUploadSessionIDs(ctx context.Context, before time.Time) ([]uint, error) {
var ids []uint var ids []uint
err := r.data.gormDB.WithContext(ctx).Model(&uploadSessionPO{}).Where("status = ? AND updated_at < ?", "uploading", before).Pluck("id", &ids).Error err := r.data.DB().WithContext(ctx).Model(&uploadSessionPO{}).Where("status = ? AND updated_at < ?", "uploading", before).Pluck("id", &ids).Error
return ids, err return ids, err
} }
func (r *mediaRepo) DeleteUploadData(ctx context.Context, uploadID uint) error { func (r *mediaRepo) DeleteUploadData(ctx context.Context, uploadID uint) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("upload_id = ?", uploadID).Delete(&uploadChunkPO{}).Error; err != nil { if err := tx.Where("upload_id = ?", uploadID).Delete(&uploadChunkPO{}).Error; err != nil {
return err return err
} }
@@ -1,4 +1,4 @@
package data package system
import ( import (
"context" "context"
@@ -10,9 +10,9 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
type menuRepo struct{ data *Data } type menuRepo struct{ data Provider }
func NewMenuRepo(data *Data) biz.MenuRepo { return &menuRepo{data: data} } func NewMenuRepo(data Provider) biz.MenuRepo { return &menuRepo{data: data} }
func menuFromPO(po menuPO) *biz.Menu { func menuFromPO(po menuPO) *biz.Menu {
var deletedAt *time.Time var deletedAt *time.Time
@@ -58,7 +58,7 @@ func createMenuRelations(tx *gorm.DB, menu *biz.Menu) error {
} }
func (r *menuRepo) Create(ctx context.Context, v *biz.Menu) error { func (r *menuRepo) Create(ctx context.Context, v *biz.Menu) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var count int64 var count int64
if err := tx.Model(&menuPO{}).Where("name = ?", v.Name).Count(&count).Error; err != nil { if err := tx.Model(&menuPO{}).Where("name = ?", v.Name).Count(&count).Error; err != nil {
return err return err
@@ -99,7 +99,7 @@ func (r *menuRepo) Create(ctx context.Context, v *biz.Menu) error {
} }
func (r *menuRepo) Update(ctx context.Context, v *biz.Menu) error { func (r *menuRepo) Update(ctx context.Context, v *biz.Menu) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var old menuPO var old menuPO
if err := tx.First(&old, v.ID).Error; err != nil { if err := tx.First(&old, v.ID).Error; err != nil {
return err return err
@@ -122,7 +122,7 @@ func (r *menuRepo) Update(ctx context.Context, v *biz.Menu) error {
} }
func (r *menuRepo) Delete(ctx context.Context, id uint) error { func (r *menuRepo) Delete(ctx context.Context, id uint) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var count int64 var count int64
if err := tx.Model(&menuPO{}).Where("parent_id = ?", id).Count(&count).Error; err != nil { if err := tx.Model(&menuPO{}).Where("parent_id = ?", id).Count(&count).Error; err != nil {
return err return err
@@ -158,7 +158,7 @@ func (r *menuRepo) Delete(ctx context.Context, id uint) error {
func (r *menuRepo) loadRelations(ctx context.Context, menu *biz.Menu) error { func (r *menuRepo) loadRelations(ctx context.Context, menu *biz.Menu) error {
var parameters []menuParameterPO var parameters []menuParameterPO
if err := r.data.gormDB.WithContext(ctx).Where("sys_base_menu_id = ?", menu.ID).Find(&parameters).Error; err != nil { if err := r.data.DB().WithContext(ctx).Where("sys_base_menu_id = ?", menu.ID).Find(&parameters).Error; err != nil {
return err return err
} }
menu.Parameters = make([]*biz.MenuParameter, 0, len(parameters)) menu.Parameters = make([]*biz.MenuParameter, 0, len(parameters))
@@ -166,7 +166,7 @@ func (r *menuRepo) loadRelations(ctx context.Context, menu *biz.Menu) error {
menu.Parameters = append(menu.Parameters, menuParameterFromPO(parameter)) menu.Parameters = append(menu.Parameters, menuParameterFromPO(parameter))
} }
var buttons []menuButtonPO var buttons []menuButtonPO
if err := r.data.gormDB.WithContext(ctx).Where("sys_base_menu_id = ?", menu.ID).Find(&buttons).Error; err != nil { if err := r.data.DB().WithContext(ctx).Where("sys_base_menu_id = ?", menu.ID).Find(&buttons).Error; err != nil {
return err return err
} }
menu.Buttons = make([]*biz.MenuButton, 0, len(buttons)) menu.Buttons = make([]*biz.MenuButton, 0, len(buttons))
@@ -178,7 +178,7 @@ func (r *menuRepo) loadRelations(ctx context.Context, menu *biz.Menu) error {
func (r *menuRepo) Find(ctx context.Context, id uint) (*biz.Menu, error) { func (r *menuRepo) Find(ctx context.Context, id uint) (*biz.Menu, error) {
var po menuPO var po menuPO
if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil { if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err return nil, err
} }
menu := menuFromPO(po) menu := menuFromPO(po)
@@ -190,15 +190,15 @@ func (r *menuRepo) Find(ctx context.Context, id uint) (*biz.Menu, error) {
func (r *menuRepo) List(ctx context.Context) ([]*biz.Menu, error) { func (r *menuRepo) List(ctx context.Context) ([]*biz.Menu, error) {
var pos []menuPO var pos []menuPO
db := r.data.gormDB.WithContext(ctx).Model(&menuPO{}).Order("sort") db := r.data.DB().WithContext(ctx).Model(&menuPO{}).Order("sort")
config := r.data.runtime.Admin() config := r.data.Runtime().Admin()
if actor, ok := biz.ActorFromContext(ctx); ok && config != nil && config.System != nil && config.System.UseStrictAuth { if actor, ok := biz.ActorFromContext(ctx); ok && config != nil && config.System != nil && config.System.UseStrictAuth {
var authority authorityPO var authority authorityPO
if err := r.data.gormDB.WithContext(ctx).Where("authority_id = ?", actor.AuthorityID).First(&authority).Error; err != nil { if err := r.data.DB().WithContext(ctx).Where("authority_id = ?", actor.AuthorityID).First(&authority).Error; err != nil {
return nil, err return nil, err
} }
if authority.ParentID != nil && *authority.ParentID != 0 { if authority.ParentID != nil && *authority.ParentID != 0 {
db = db.Where("id IN (?)", r.data.gormDB.WithContext(ctx).Model(&authorityMenuPO{}).Select("sys_base_menu_id").Where("sys_authority_authority_id = ?", actor.AuthorityID)) db = db.Where("id IN (?)", r.data.DB().WithContext(ctx).Model(&authorityMenuPO{}).Select("sys_base_menu_id").Where("sys_authority_authority_id = ?", actor.AuthorityID))
} }
} }
if err := db.Find(&pos).Error; err != nil { if err := db.Find(&pos).Error; err != nil {
@@ -220,7 +220,7 @@ func (r *menuRepo) List(ctx context.Context) ([]*biz.Menu, error) {
menu.Buttons = []*biz.MenuButton{} menu.Buttons = []*biz.MenuButton{}
} }
var parameters []menuParameterPO var parameters []menuParameterPO
if err := r.data.gormDB.WithContext(ctx).Where("sys_base_menu_id IN ?", ids).Find(&parameters).Error; err != nil { if err := r.data.DB().WithContext(ctx).Where("sys_base_menu_id IN ?", ids).Find(&parameters).Error; err != nil {
return nil, err return nil, err
} }
for _, parameter := range parameters { for _, parameter := range parameters {
@@ -228,7 +228,7 @@ func (r *menuRepo) List(ctx context.Context) ([]*biz.Menu, error) {
menu.Parameters = append(menu.Parameters, menuParameterFromPO(parameter)) menu.Parameters = append(menu.Parameters, menuParameterFromPO(parameter))
} }
var buttons []menuButtonPO var buttons []menuButtonPO
if err := r.data.gormDB.WithContext(ctx).Where("sys_base_menu_id IN ?", ids).Find(&buttons).Error; err != nil { if err := r.data.DB().WithContext(ctx).Where("sys_base_menu_id IN ?", ids).Find(&buttons).Error; err != nil {
return nil, err return nil, err
} }
for _, button := range buttons { for _, button := range buttons {
@@ -260,13 +260,13 @@ func (r *menuRepo) SetAuthorityMenus(ctx context.Context, id uint, ids []uint) e
if err := (&authorityAccessRepo{data: r.data}).checkAuthorityIDAuth(ctx, id); err != nil { if err := (&authorityAccessRepo{data: r.data}).checkAuthorityIDAuth(ctx, id); err != nil {
return err return err
} }
if err := r.data.gormDB.WithContext(ctx).Where("authority_id = ?", id).First(&authorityPO{}).Error; err != nil { if err := r.data.DB().WithContext(ctx).Where("authority_id = ?", id).First(&authorityPO{}).Error; err != nil {
return err return err
} }
if err := r.checkMenuAssignmentAuth(ctx, ids); err != nil { if err := r.checkMenuAssignmentAuth(ctx, ids); err != nil {
return err return err
} }
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("sys_authority_authority_id = ?", id).Delete(&authorityMenuPO{}).Error; err != nil { if err := tx.Where("sys_authority_authority_id = ?", id).Delete(&authorityMenuPO{}).Error; err != nil {
return err return err
} }
@@ -283,7 +283,7 @@ func (r *menuRepo) SetAuthorityMenus(ctx context.Context, id uint, ids []uint) e
func (r *menuRepo) AuthorityMenuIDs(ctx context.Context, id uint) ([]uint, error) { func (r *menuRepo) AuthorityMenuIDs(ctx context.Context, id uint) ([]uint, error) {
var ids []uint var ids []uint
err := r.data.gormDB.WithContext(ctx).Model(&authorityMenuPO{}).Where("sys_authority_authority_id = ?", id).Pluck("sys_base_menu_id", &ids).Error err := r.data.DB().WithContext(ctx).Model(&authorityMenuPO{}).Where("sys_authority_authority_id = ?", id).Pluck("sys_base_menu_id", &ids).Error
return ids, err return ids, err
} }
@@ -294,8 +294,8 @@ func (r *menuRepo) AuthorityMenuIDs(ctx context.Context, id uint) ([]uint, error
// menus when an administrator inspects a different (child) authority. // menus when an administrator inspects a different (child) authority.
func (r *menuRepo) ListAuthorityMenus(ctx context.Context, authorityID uint) ([]*biz.Menu, error) { func (r *menuRepo) ListAuthorityMenus(ctx context.Context, authorityID uint) ([]*biz.Menu, error) {
var pos []menuPO var pos []menuPO
db := r.data.gormDB.WithContext(ctx).Model(&menuPO{}). db := r.data.DB().WithContext(ctx).Model(&menuPO{}).
Where("id IN (?)", r.data.gormDB.WithContext(ctx).Model(&authorityMenuPO{}). Where("id IN (?)", r.data.DB().WithContext(ctx).Model(&authorityMenuPO{}).
Select("sys_base_menu_id").Where("sys_authority_authority_id = ?", authorityID)). Select("sys_base_menu_id").Where("sys_authority_authority_id = ?", authorityID)).
Order("sort") Order("sort")
if err := db.Find(&pos).Error; err != nil { if err := db.Find(&pos).Error; err != nil {
@@ -312,17 +312,17 @@ func (r *menuRepo) ListAuthorityMenus(ctx context.Context, authorityID uint) ([]
func (r *menuRepo) MenuRoleIDs(ctx context.Context, id uint) ([]uint, error) { func (r *menuRepo) MenuRoleIDs(ctx context.Context, id uint) ([]uint, error) {
var ids []uint var ids []uint
err := r.data.gormDB.WithContext(ctx).Model(&authorityMenuPO{}).Where("sys_base_menu_id = ?", id).Pluck("sys_authority_authority_id", &ids).Error err := r.data.DB().WithContext(ctx).Model(&authorityMenuPO{}).Where("sys_base_menu_id = ?", id).Pluck("sys_authority_authority_id", &ids).Error
return ids, err return ids, err
} }
func (r *menuRepo) DefaultRouterRoleIDs(ctx context.Context, id uint) ([]uint, error) { func (r *menuRepo) DefaultRouterRoleIDs(ctx context.Context, id uint) ([]uint, error) {
var menu menuPO var menu menuPO
if err := r.data.gormDB.WithContext(ctx).First(&menu, id).Error; err != nil { if err := r.data.DB().WithContext(ctx).First(&menu, id).Error; err != nil {
return nil, err return nil, err
} }
var ids []uint var ids []uint
err := r.data.gormDB.WithContext(ctx).Model(&authorityPO{}).Where("default_router = ?", menu.Name).Pluck("authority_id", &ids).Error err := r.data.DB().WithContext(ctx).Model(&authorityPO{}).Where("default_router = ?", menu.Name).Pluck("authority_id", &ids).Error
return ids, err return ids, err
} }
@@ -342,7 +342,7 @@ func (r *menuRepo) SetMenuRoles(ctx context.Context, id uint, ids []uint) error
} }
} }
} }
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if strict { if strict {
if len(allowedAuthorities) > 0 { if len(allowedAuthorities) > 0 {
if err := tx.Where("sys_base_menu_id = ? AND sys_authority_authority_id IN ?", id, authorityIDs(allowedAuthorities)).Delete(&authorityMenuPO{}).Error; err != nil { if err := tx.Where("sys_base_menu_id = ? AND sys_authority_authority_id IN ?", id, authorityIDs(allowedAuthorities)).Delete(&authorityMenuPO{}).Error; err != nil {
@@ -376,7 +376,7 @@ func (r *menuRepo) checkMenuAssignmentAuth(ctx context.Context, menuIDs []uint)
if err != nil || !strict { if err != nil || !strict {
return err return err
} }
return checkMenuAssignment(r.data.gormDB.WithContext(ctx), actor.AuthorityID, allowedAuthorities[actor.AuthorityID], menuIDs) return checkMenuAssignment(r.data.DB().WithContext(ctx), actor.AuthorityID, allowedAuthorities[actor.AuthorityID], menuIDs)
} }
func checkMenuAssignment(db *gorm.DB, actorID uint, root bool, menuIDs []uint) error { func checkMenuAssignment(db *gorm.DB, actorID uint, root bool, menuIDs []uint) error {
@@ -1,4 +1,4 @@
package data package system
import ( import (
"context" "context"
+507
View File
@@ -0,0 +1,507 @@
package system
import (
"errors"
"fmt"
"strings"
"time"
"gorm.io/gorm"
)
// legacySchemaMigration creates the complete schema and repairs legacy table
// shapes. It is intentionally called only by the one-time baseline migration.
func LegacySchemaMigration(db *gorm.DB) error {
if err := migrateLegacyIgnoreAPITable(db); err != nil {
return err
}
if err := migrateLegacyAuthorityDepartmentColumns(db); err != nil {
return err
}
return db.AutoMigrate(
&userPO{}, &authorityPO{}, &menuPO{}, &userAuthorityPO{}, &authorityMenuPO{}, &menuParameterPO{},
&apiPO{}, &ignoredAPIPO{}, &authorityAPIPO{}, &casbinRulePO{}, &menuButtonPO{}, &authorityButtonPO{},
&departmentPO{}, &positionPO{}, &userDepartmentPO{}, &userPositionPO{}, &authorityDepartmentPO{},
&dictionaryPO{}, &dictionaryDetailPO{}, &parameterPO{}, &apiTokenPO{}, &jwtBlacklistPO{}, &SecurityConfigPO{},
&versionPO{}, &exportTemplatePO{}, &exportConditionPO{}, &exportJoinPO{},
&operationPO{}, &loginLogPO{}, &dataAccessLogPO{}, &errorRecordPO{},
&taskPO{}, &taskLogPO{}, &mediaPO{}, &categoryPO{}, &uploadSessionPO{}, &uploadChunkPO{},
&announcementPO{},
)
}
// CurrentDataMigration contains idempotent system data and authorization
// reconciliation that belongs to an explicit gormigrate version.
func CurrentDataMigration(db *gorm.DB) error {
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)
}
// EnsureAdminSurface upgrades an existing database with menu/API metadata
// contributed by a feature module. The operation is idempotent and safe to run
// before the root authority exists; bootstrap seeding links the full menu tree
// when the first administrator is created.
func EnsureAdminSurface(db *gorm.DB, surface AdminSurface) error {
clean := db.Session(&gorm.Session{NewDB: true})
return clean.Transaction(func(tx *gorm.DB) error {
menus := make([]menuPO, 0, len(surface.Menus))
for _, item := range surface.Menus {
parentID := uint(0)
if item.ParentName != "" {
var parent menuPO
if err := tx.Where("name = ?", item.ParentName).First(&parent).Error; err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
parent = menuPO{Path: item.ParentName, Name: item.ParentName, Component: "view/routerHolder.vue", Title: item.ParentName, Sort: item.Sort}
if err := tx.Create(&parent).Error; err != nil {
return err
}
}
parentID = parent.ID
}
menu := menuPO{MenuLevel: 1, ParentID: parentID, Path: item.Path, Name: item.Name, Component: item.Component, Title: item.Title, Icon: item.Icon, Sort: item.Sort}
if item.ParentName == "" {
menu.MenuLevel = 0
}
menus = append(menus, menu)
}
for _, item := range menus {
var current menuPO
err := tx.Where("name = ?", item.Name).First(&current).Error
switch {
case errors.Is(err, gorm.ErrRecordNotFound):
if err := tx.Create(&item).Error; err != nil {
return err
}
case err != nil:
return err
default:
if err := tx.Model(&current).Updates(map[string]any{
"menu_level": item.MenuLevel, "parent_id": parent.ID, "path": item.Path,
"component": item.Component, "title": item.Title, "icon": item.Icon, "sort": item.Sort,
}).Error; err != nil {
return err
}
}
}
apis := make([]apiPO, 0, len(surface.APIs))
for _, item := range surface.APIs {
apis = append(apis, apiPO{Path: item.Path, Method: item.Method, APIGroup: item.APIGroup, Description: item.Description})
}
for _, item := range apis {
var current apiPO
err := tx.Where("path = ? AND method = ?", item.Path, item.Method).First(&current).Error
switch {
case errors.Is(err, gorm.ErrRecordNotFound):
if err := tx.Create(&item).Error; err != nil {
return err
}
case err != nil:
return err
default:
if err := tx.Model(&current).Updates(map[string]any{"api_group": item.APIGroup, "description": item.Description}).Error; err != nil {
return err
}
}
}
var authorityCount int64
if err := tx.Model(&authorityPO{}).Where("authority_id = ?", 888).Count(&authorityCount).Error; err != nil {
return err
}
if authorityCount == 0 {
return nil
}
for _, item := range menus {
var current menuPO
if err := tx.Where("name = ?", item.Name).First(&current).Error; err != nil {
return err
}
var count int64
if err := tx.Model(&authorityMenuPO{}).Where("sys_authority_authority_id = ? AND sys_base_menu_id = ?", 888, current.ID).Count(&count).Error; err != nil {
return err
}
if count == 0 {
if err := tx.Create(&authorityMenuPO{SysAuthorityAuthorityID: 888, SysBaseMenuID: current.ID}).Error; err != nil {
return err
}
}
}
for _, item := range apis {
var current apiPO
if err := tx.Where("path = ? AND method = ?", item.Path, item.Method).First(&current).Error; err != nil {
return err
}
exists, err := policyExists(tx, 888, current.Path, current.Method)
if err != nil {
return err
}
if !exists {
if err := tx.Create(&casbinRulePO{Ptype: "p", V0: "888", V1: current.Path, V2: current.Method}).Error; err != nil {
return err
}
}
}
return nil
})
}
// migrateLegacyAuthorityDepartmentColumns preserves data created by early
// Kra builds, which used shortened join-column names and a composite primary
// key. The administration connection model has neither a primary key nor a
// uniqueness constraint, so rebuild the small table before AutoMigrate.
func migrateLegacyAuthorityDepartmentColumns(db *gorm.DB) error {
clean := db.Session(&gorm.Session{NewDB: true})
const (
table = "sys_authority_departments"
backup = "sys_authority_departments_kra_legacy"
)
// MySQL and Oracle auto-commit DDL. If a prior process stopped between the
// rename and cleanup steps, restore the untouched backup first and retry the
// migration from a known state.
if clean.Migrator().HasTable(backup) {
if clean.Migrator().HasTable(table) {
if err := clean.Migrator().DropTable(table); err != nil {
return fmt.Errorf("remove incomplete authority-department table: %w", err)
}
}
if err := clean.Migrator().RenameTable(backup, table); err != nil {
return fmt.Errorf("restore authority-department backup: %w", err)
}
}
if !clean.Migrator().HasTable(table) {
return nil
}
authorityColumn := "sys_authority_authority_id"
if !tableHasColumn(clean, table, authorityColumn) {
if !tableHasColumn(clean, table, "authority_id") {
return fmt.Errorf("authority-department table has no authority column")
}
authorityColumn = "authority_id"
}
departmentColumn := "sys_department_id"
if !tableHasColumn(clean, table, departmentColumn) {
if !tableHasColumn(clean, table, "department_id") {
return fmt.Errorf("authority-department table has no department column")
}
departmentColumn = "department_id"
}
hasPrimaryKey, err := tableHasPrimaryKey(clean, table)
if err != nil {
return err
}
if authorityColumn == "sys_authority_authority_id" && departmentColumn == "sys_department_id" && !hasPrimaryKey {
return nil
}
type relation struct {
AuthorityID uint `gorm:"column:authority_id"`
DepartmentID uint `gorm:"column:department_id"`
}
var rows []relation
selectColumns := authorityColumn + " AS authority_id, " + departmentColumn + " AS department_id"
if err := clean.Table(table).Select(selectColumns).Scan(&rows).Error; err != nil {
return fmt.Errorf("read legacy authority-department rows: %w", err)
}
rebuild := func(tx *gorm.DB) error {
if err := tx.Migrator().RenameTable(table, backup); err != nil {
return fmt.Errorf("rename legacy authority-department table: %w", err)
}
if err := tx.AutoMigrate(&authorityDepartmentPO{}); err != nil {
return fmt.Errorf("create authority-department table: %w", err)
}
if len(rows) > 0 {
items := make([]authorityDepartmentPO, 0, len(rows))
for _, row := range rows {
items = append(items, authorityDepartmentPO{AuthorityID: row.AuthorityID, DepartmentID: row.DepartmentID})
}
if err := tx.Create(&items).Error; err != nil {
return fmt.Errorf("copy authority-department rows: %w", err)
}
}
var count int64
if err := tx.Model(&authorityDepartmentPO{}).Count(&count).Error; err != nil {
return fmt.Errorf("verify authority-department rows: %w", err)
}
if count != int64(len(rows)) {
return fmt.Errorf("verify authority-department rows: got %d want %d", count, len(rows))
}
if err := tx.Migrator().DropTable(backup); err != nil {
return fmt.Errorf("drop legacy authority-department table: %w", err)
}
return nil
}
switch clean.Dialector.Name() {
case "mysql", "oracle":
if err := rebuild(clean); err != nil {
restoreErr := restoreAuthorityDepartmentBackup(clean, table, backup)
if restoreErr != nil {
return fmt.Errorf("%v; restore authority-department backup: %w", err, restoreErr)
}
return err
}
return nil
default:
return clean.Transaction(rebuild)
}
}
func restoreAuthorityDepartmentBackup(db *gorm.DB, table, backup string) error {
if db.Migrator().HasTable(table) {
if err := db.Migrator().DropTable(table); err != nil {
return err
}
}
if db.Migrator().HasTable(backup) {
return db.Migrator().RenameTable(backup, table)
}
return nil
}
func tableHasPrimaryKey(db *gorm.DB, table string) (bool, error) {
columns, err := db.Migrator().ColumnTypes(table)
if err != nil {
return false, err
}
for _, column := range columns {
if primary, ok := column.PrimaryKey(); ok && primary {
return true, nil
}
}
return false, nil
}
// Older builds used a status label outside the administration page's supported
// state set, so normalize existing rows during migration.
func normalizeErrorRecordStatuses(db *gorm.DB) error {
return db.Session(&gorm.Session{NewDB: true}).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 {
clean := db.Session(&gorm.Session{NewDB: true})
if !clean.Migrator().HasTable(&authorityAPIPO{}) || !clean.Migrator().HasTable(&casbinRulePO{}) {
return nil
}
type legacyPolicy struct {
AuthorityID uint
Path string
Method string
}
baseQuery := func() *gorm.DB {
return clean.Table("sys_authority_apis sa").
Select("sa.authority_id, a.path, a.method").
Joins("JOIN sys_apis a ON a.id = sa.api_id")
}
query := baseQuery()
// Early Kra schemas stored sys_apis without soft-delete timestamps. The
// legacy-policy migration must run before assuming that column exists;
// otherwise an upgrade from those schemas cannot start on MySQL.
if tableHasColumn(clean, "sys_apis", "deleted_at") {
query = query.Where("a.deleted_at IS NULL")
}
var rows []legacyPolicy
if err := query.Find(&rows).Error; err != nil {
// A few MySQL-compatible drivers report stale/incomplete metadata from
// INFORMATION_SCHEMA during startup. If the optional soft-delete column
// was reported present but the join still rejects it, retry using only
// columns shared by every legacy schema. This migration must never block
// startup of an older database solely because deleted_at is absent.
if strings.Contains(strings.ToLower(err.Error()), "unknown column") && strings.Contains(strings.ToLower(err.Error()), "deleted_at") {
if retryErr := baseQuery().Find(&rows).Error; retryErr != nil {
return retryErr
}
} else {
return err
}
}
for _, row := range rows {
exists, err := policyExists(clean, row.AuthorityID, row.Path, row.Method)
if err != nil {
return err
}
if exists {
continue
}
if err := clean.Create(&casbinRulePO{Ptype: "p", V0: fmt.Sprint(row.AuthorityID), V1: row.Path, V2: row.Method}).Error; err != nil {
return err
}
}
return nil
}
// tableHasColumn deliberately inspects the physical table rather than the
// model schema. Legacy databases may predate soft-delete columns even though
// the current PO includes gorm.DeletedAt. Metadata inspection failures are
// treated as "unknown" so callers use the portable query shape.
func tableHasColumn(db *gorm.DB, table, column string) bool {
columns, err := db.Migrator().ColumnTypes(table)
if err != nil {
return false
}
for _, item := range columns {
if strings.EqualFold(item.Name(), column) {
return true
}
}
return false
}
// 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 {
clean := db.Session(&gorm.Session{NewDB: true})
if !clean.Migrator().HasTable(&ignoredAPIPO{}) || clean.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 clean.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 {
clean := db.Session(&gorm.Session{NewDB: true})
var authorityCount int64
if err := clean.Session(&gorm.Session{NewDB: true}).Model(&authorityPO{}).Where("authority_id = ?", 888).Count(&authorityCount).Error; err != nil || authorityCount == 0 {
return err
}
var policyCount int64
if err := policyScope(clean).Where("v0 = ?", "888").Count(&policyCount).Error; err != nil || policyCount != 0 {
return err
}
var ignored []ignoredAPIPO
if err := clean.Session(&gorm.Session{NewDB: true}).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 := clean.Session(&gorm.Session{NewDB: true}).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 clean.Session(&gorm.Session{NewDB: true}).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.
func reconcileReferenceIndexes(db *gorm.DB) error {
clean := db.Session(&gorm.Session{NewDB: true})
obsolete := []struct {
model any
name string
}{
{&apiPO{}, "idx_api_path_method"},
{&dictionaryPO{}, "idx_sys_dictionaries_type"},
{&parameterPO{}, "idx_sys_params_key"},
{&apiTokenPO{}, "idx_sys_api_tokens_token"},
{&exportTemplatePO{}, "idx_sys_export_templates_template_id"},
}
for _, item := range obsolete {
migrator := clean.Session(&gorm.Session{NewDB: true}).Migrator()
if migrator.HasIndex(item.model, item.name) {
if err := 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(clean.Session(&gorm.Session{NewDB: true}), &userPO{}, item.name)
if err != nil {
return err
}
if !unique {
continue
}
migrator := clean.Session(&gorm.Session{NewDB: true}).Migrator()
if err = migrator.DropIndex(&userPO{}, item.name); err != nil {
return fmt.Errorf("drop legacy unique index %s: %w", item.name, err)
}
if err = 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
}
@@ -1,9 +1,117 @@
package data package system
import ( import (
"testing" "testing"
"kra/internal/data/migration"
) )
func TestMigrateAllUsesVersionTableAndIsIdempotent(t *testing.T) {
db, err := openWithDriver("sqlite", "file:"+t.Name()+"?mode=memory&cache=shared")
if err != nil {
t.Fatal(err)
}
sqlDB, err := db.DB()
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = sqlDB.Close() })
if err = migrateAll(db); err != nil {
t.Fatalf("first migration: %v", err)
}
if err = migrateAll(db); err != nil {
t.Fatalf("second migration: %v", err)
}
if !db.Migrator().HasTable(migration.TableName) {
t.Fatalf("missing migration table %q", migration.TableName)
}
var count int64
if err = db.Table(migration.TableName).Count(&count).Error; err != nil {
t.Fatal(err)
}
if count != 2 {
t.Fatalf("migration rows = %d, want 2", count)
}
}
func TestEnsurePaymentAdminSurfaceIsIdempotent(t *testing.T) {
db, err := openWithDriver("sqlite", "file:"+t.Name()+"?mode=memory&cache=shared")
if err != nil {
t.Fatal(err)
}
sqlDB, err := db.DB()
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = sqlDB.Close() })
if err = db.AutoMigrate(&menuPO{}, &authorityMenuPO{}, &authorityPO{}, &apiPO{}, &casbinRulePO{}); err != nil {
t.Fatal(err)
}
rootParentID := uint(0)
if err = db.Create(&authorityPO{AuthorityID: 888, AuthorityName: "root", ParentID: &rootParentID}).Error; err != nil {
t.Fatal(err)
}
if err = db.Create(&menuPO{Name: "extensions", Path: "legacy-extensions", Title: "旧扩展"}).Error; err != nil {
t.Fatal(err)
}
if err = db.Create(&menuPO{Name: "paymentOrders", Component: "legacy.vue", Title: "旧支付订单"}).Error; err != nil {
t.Fatal(err)
}
if err = db.Create(&apiPO{Path: "/payment/configs", Method: "GET", APIGroup: "legacy"}).Error; err != nil {
t.Fatal(err)
}
for i := 0; i < 2; i++ {
if err = ensurePaymentAdminSurface(db); err != nil {
t.Fatalf("ensure payment admin surface pass %d: %v", i+1, err)
}
}
var parent menuPO
if err = db.Where("name = ?", "extensions").First(&parent).Error; err != nil {
t.Fatal(err)
}
for name, component := range map[string]string{
"paymentOrders": "view/systemTools/payment/orders.vue",
"paymentConfig": "view/systemTools/payment/config.vue",
} {
var menu menuPO
if err = db.Where("name = ?", name).First(&menu).Error; err != nil {
t.Fatal(err)
}
if menu.ParentID != parent.ID || menu.Component != component {
t.Fatalf("menu %s = %#v", name, menu)
}
var linkCount int64
if err = db.Model(&authorityMenuPO{}).Where("sys_authority_authority_id = ? AND sys_base_menu_id = ?", 888, menu.ID).Count(&linkCount).Error; err != nil {
t.Fatal(err)
}
if linkCount != 1 {
t.Fatalf("menu %s root links = %d, want 1", name, linkCount)
}
}
for _, item := range []struct{ method, path string }{
{"GET", "/payment/configs"}, {"POST", "/payment/config"},
{"GET", "/payment/orders"}, {"POST", "/payment/order"},
} {
var apiCount int64
if err = db.Model(&apiPO{}).Where("path = ? AND method = ?", item.path, item.method).Count(&apiCount).Error; err != nil {
t.Fatal(err)
}
if apiCount != 1 {
t.Fatalf("API %s %s rows = %d, want 1", item.method, item.path, apiCount)
}
var policyCount int64
if err = policyScope(db).Where("v0 = ? AND v1 = ? AND v2 = ?", "888", item.path, item.method).Count(&policyCount).Error; err != nil {
t.Fatal(err)
}
if policyCount != 1 {
t.Fatalf("policy %s %s rows = %d, want 1", item.method, item.path, policyCount)
}
}
}
func TestMigrateLegacyAuthorityAPIsWithoutDeletedAt(t *testing.T) { func TestMigrateLegacyAuthorityAPIsWithoutDeletedAt(t *testing.T) {
db, err := openWithDriver("sqlite", "file:"+t.Name()+"?mode=memory&cache=shared") db, err := openWithDriver("sqlite", "file:"+t.Name()+"?mode=memory&cache=shared")
if err != nil { if err != nil {
@@ -1,4 +1,4 @@
package data package system
import ( import (
"time" "time"
@@ -96,9 +96,6 @@ type menuParameterPO struct {
func (menuParameterPO) TableName() string { return "sys_base_menu_parameters" } func (menuParameterPO) TableName() string { return "sys_base_menu_parameters" }
type userRepo struct{ data *Data } type userRepo struct{ data Provider }
type initializationRepo struct{ data *Data }
func NewUserRepo(data *Data) biz.UserRepo { return &userRepo{data: data} } func NewUserRepo(data Provider) biz.UserRepo { return &userRepo{data: data} }
func NewInitializationRepo(data *Data) biz.InitializationRepo { return &initializationRepo{data: data} }
@@ -1,10 +1,11 @@
package data package system
import ( import (
"context" "context"
"time" "time"
"kra/internal/biz" "kra/internal/biz"
"kra/pkg/pagination"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -30,13 +31,13 @@ type operationPO struct {
func (operationPO) TableName() string { return "sys_operation_records" } func (operationPO) TableName() string { return "sys_operation_records" }
func (r *auditRecorderRepo) RecordOperation(ctx context.Context, v *biz.OperationRecord) error { func (r *auditRecorderRepo) RecordOperation(ctx context.Context, v *biz.OperationRecord) error {
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 return r.data.DB().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 { func opFromPO(v operationPO) *biz.OperationRecord {
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} 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) { 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{}) db := r.data.DB().WithContext(ctx).Model(&operationPO{})
if q != nil { if q != nil {
if q.Path != "" { if q.Path != "" {
db = db.Where("path LIKE ?", "%"+q.Path+"%") db = db.Where("path LIKE ?", "%"+q.Path+"%")
@@ -53,10 +54,10 @@ func (r *auditQueryRepo) ListOperations(ctx context.Context, page, size int, q *
return nil, 0, err return nil, 0, err
} }
var pos []operationPO var pos []operationPO
if err := applyRequiredPagination(db.Order("id desc"), page, size, 100).Find(&pos).Error; err != nil { if err := pagination.ApplyRequired(db.Order("id desc"), page, size, 100).Find(&pos).Error; err != nil {
return nil, 0, err return nil, 0, err
} }
users := auditUsers(ctx, r.data.gormDB.WithContext(ctx), operationUserIDs(pos)) users := auditUsers(ctx, r.data.DB().WithContext(ctx), operationUserIDs(pos))
out := make([]*biz.OperationRecord, 0, len(pos)) out := make([]*biz.OperationRecord, 0, len(pos))
for _, po := range pos { for _, po := range pos {
value := opFromPO(po) value := opFromPO(po)
@@ -92,7 +93,7 @@ func auditUsers(ctx context.Context, db *gorm.DB, ids []uint) map[uint]*biz.User
} }
func (r *auditQueryRepo) FindOperation(ctx context.Context, id uint) (*biz.OperationRecord, error) { func (r *auditQueryRepo) FindOperation(ctx context.Context, id uint) (*biz.OperationRecord, error) {
var po operationPO var po operationPO
if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil { if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err return nil, err
} }
return opFromPO(po), nil return opFromPO(po), nil
@@ -100,7 +101,7 @@ func (r *auditQueryRepo) FindOperation(ctx context.Context, id uint) (*biz.Opera
func (r *auditQueryRepo) DeleteOperations(ctx context.Context, ids []int) error { func (r *auditQueryRepo) DeleteOperations(ctx context.Context, ids []int) error {
if len(ids) == 1 && ids[0] == 0 { if len(ids) == 1 && ids[0] == 0 {
return r.data.gormDB.WithContext(ctx).Delete(&operationPO{}).Error return r.data.DB().WithContext(ctx).Delete(&operationPO{}).Error
} }
return r.data.gormDB.WithContext(ctx).Delete(&operationPO{}, "id IN ?", ids).Error return r.data.DB().WithContext(ctx).Delete(&operationPO{}, "id IN ?", ids).Error
} }
@@ -1,4 +1,4 @@
package data package system
import ( import (
"context" "context"
@@ -1,18 +1,19 @@
package data package system
import ( import (
"context" "context"
"time" "time"
"kra/internal/biz" "kra/internal/biz"
"kra/pkg/pagination"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/clause" "gorm.io/gorm/clause"
) )
type parameterRepo struct{ data *Data } type parameterRepo struct{ data Provider }
func NewParameterRepo(data *Data) biz.ParameterRepo { return &parameterRepo{data: data} } func NewParameterRepo(data Provider) biz.ParameterRepo { return &parameterRepo{data: data} }
type parameterPO struct { type parameterPO struct {
ID uint `gorm:"primaryKey"` ID uint `gorm:"primaryKey"`
@@ -29,7 +30,7 @@ func (parameterPO) TableName() string { return "sys_params" }
func (r *parameterRepo) CreateParameter(ctx context.Context, v *biz.SystemParameter) error { func (r *parameterRepo) CreateParameter(ctx context.Context, v *biz.SystemParameter) error {
po := parameterPO{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, Name: v.Name, Key: v.Key, Value: v.Value, Desc: v.Desc} po := parameterPO{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, Name: v.Name, Key: v.Key, Value: v.Value, Desc: v.Desc}
if err := r.data.gormDB.WithContext(ctx).Create(&po).Error; err != nil { if err := r.data.DB().WithContext(ctx).Create(&po).Error; err != nil {
return err return err
} }
v.ID, v.CreatedAt, v.UpdatedAt = po.ID, po.CreatedAt, po.UpdatedAt v.ID, v.CreatedAt, v.UpdatedAt = po.ID, po.CreatedAt, po.UpdatedAt
@@ -38,30 +39,30 @@ func (r *parameterRepo) CreateParameter(ctx context.Context, v *biz.SystemParame
func (r *parameterRepo) UpdateParameter(ctx context.Context, v *biz.SystemParameter) error { func (r *parameterRepo) UpdateParameter(ctx context.Context, v *biz.SystemParameter) error {
// The compatible update uses a struct, so zero-value optional fields (notably desc) // The compatible update uses a struct, so zero-value optional fields (notably desc)
// are intentionally ignored rather than clearing an existing value. // are intentionally ignored rather than clearing an existing value.
return r.data.gormDB.WithContext(ctx).Model(&parameterPO{}).Where("id = ?", v.ID).Updates(&parameterPO{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, Name: v.Name, Key: v.Key, Value: v.Value, Desc: v.Desc}).Error return r.data.DB().WithContext(ctx).Model(&parameterPO{}).Where("id = ?", v.ID).Updates(&parameterPO{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, Name: v.Name, Key: v.Key, Value: v.Value, Desc: v.Desc}).Error
} }
func (r *parameterRepo) DeleteParameters(ctx context.Context, ids []string) error { func (r *parameterRepo) DeleteParameters(ctx context.Context, ids []string) error {
if len(ids) == 1 { if len(ids) == 1 {
return r.data.gormDB.WithContext(ctx).Delete(&parameterPO{}, "id = ?", ids[0]).Error return r.data.DB().WithContext(ctx).Delete(&parameterPO{}, "id = ?", ids[0]).Error
} }
return r.data.gormDB.WithContext(ctx).Delete(&[]parameterPO{}, "id in ?", ids).Error return r.data.DB().WithContext(ctx).Delete(&[]parameterPO{}, "id in ?", ids).Error
} }
func (r *parameterRepo) FindParameterByID(ctx context.Context, id string) (*biz.SystemParameter, error) { func (r *parameterRepo) FindParameterByID(ctx context.Context, id string) (*biz.SystemParameter, error) {
var po parameterPO var po parameterPO
if err := r.data.gormDB.WithContext(ctx).Where("id = ?", id).First(&po).Error; err != nil { if err := r.data.DB().WithContext(ctx).Where("id = ?", id).First(&po).Error; err != nil {
return nil, err return nil, err
} }
return parameterFromPO(po), nil return parameterFromPO(po), nil
} }
func (r *parameterRepo) FindParameterByKey(ctx context.Context, key string) (*biz.SystemParameter, error) { func (r *parameterRepo) FindParameterByKey(ctx context.Context, key string) (*biz.SystemParameter, error) {
var po parameterPO var po parameterPO
if err := r.data.gormDB.WithContext(ctx).Where(parameterPO{Key: key}).First(&po).Error; err != nil { if err := r.data.DB().WithContext(ctx).Where(parameterPO{Key: key}).First(&po).Error; err != nil {
return nil, err return nil, err
} }
return parameterFromPO(po), nil return parameterFromPO(po), nil
} }
func (r *parameterRepo) ListParameters(ctx context.Context, page, size int, q *biz.SystemParameter) ([]*biz.SystemParameter, int64, error) { func (r *parameterRepo) ListParameters(ctx context.Context, page, size int, q *biz.SystemParameter) ([]*biz.SystemParameter, int64, error) {
db := r.data.gormDB.WithContext(ctx).Model(&parameterPO{}) db := r.data.DB().WithContext(ctx).Model(&parameterPO{})
if q != nil { if q != nil {
if q.StartCreatedAt != nil && q.EndCreatedAt != nil { if q.StartCreatedAt != nil && q.EndCreatedAt != nil {
db = db.Where("created_at BETWEEN ? AND ?", q.StartCreatedAt, q.EndCreatedAt) db = db.Where("created_at BETWEEN ? AND ?", q.StartCreatedAt, q.EndCreatedAt)
@@ -78,7 +79,7 @@ func (r *parameterRepo) ListParameters(ctx context.Context, page, size int, q *b
return nil, 0, err return nil, 0, err
} }
var pos []parameterPO var pos []parameterPO
if err := applyPagination(db, page, size, 100).Find(&pos).Error; err != nil { if err := pagination.Apply(db, page, size, 100).Find(&pos).Error; err != nil {
return nil, 0, err return nil, 0, err
} }
out := make([]*biz.SystemParameter, 0, len(pos)) out := make([]*biz.SystemParameter, 0, len(pos))
@@ -1,4 +1,4 @@
package data package system
import ( import (
"context" "context"
@@ -1,4 +1,4 @@
package data package system
import ( import (
"context" "context"
@@ -10,9 +10,9 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
type permissionRepo struct{ data *Data } type permissionRepo struct{ data Provider }
func NewPermissionRepo(data *Data) biz.PermissionRepo { return &permissionRepo{data: data} } func NewPermissionRepo(data Provider) biz.PermissionRepo { return &permissionRepo{data: data} }
type menuButtonPO struct { type menuButtonPO struct {
ID uint `gorm:"primaryKey"` ID uint `gorm:"primaryKey"`
@@ -36,7 +36,7 @@ func (authorityButtonPO) TableName() string { return "sys_authority_btns" }
func (r *permissionRepo) Buttons(ctx context.Context, menuID uint) ([]*biz.MenuButton, error) { func (r *permissionRepo) Buttons(ctx context.Context, menuID uint) ([]*biz.MenuButton, error) {
var pos []menuButtonPO var pos []menuButtonPO
if err := r.data.gormDB.WithContext(ctx).Where("sys_base_menu_id = ?", menuID).Find(&pos).Error; err != nil { if err := r.data.DB().WithContext(ctx).Where("sys_base_menu_id = ?", menuID).Find(&pos).Error; err != nil {
return nil, err return nil, err
} }
out := make([]*biz.MenuButton, 0, len(pos)) out := make([]*biz.MenuButton, 0, len(pos))
@@ -52,7 +52,7 @@ func (r *permissionRepo) SetAuthorityButtons(ctx context.Context, aid uint, butt
if err := r.checkButtonAssignmentAuth(ctx, buttons); err != nil { if err := r.checkButtonAssignmentAuth(ctx, buttons); err != nil {
return err return err
} }
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("authority_id = ?", aid).Delete(&authorityButtonPO{}).Error; err != nil { if err := tx.Where("authority_id = ?", aid).Delete(&authorityButtonPO{}).Error; err != nil {
return err return err
} }
@@ -70,7 +70,7 @@ func (r *permissionRepo) SetAuthorityButtons(ctx context.Context, aid uint, butt
} }
func (r *permissionRepo) SelectedButtons(ctx context.Context, aid, menuID uint) ([]uint, error) { func (r *permissionRepo) SelectedButtons(ctx context.Context, aid, menuID uint) ([]uint, error) {
var rows []authorityButtonPO var rows []authorityButtonPO
if err := r.data.gormDB.WithContext(ctx).Where("authority_id = ? AND sys_menu_id = ?", aid, menuID).Find(&rows).Error; err != nil { if err := r.data.DB().WithContext(ctx).Where("authority_id = ? AND sys_menu_id = ?", aid, menuID).Find(&rows).Error; err != nil {
return nil, err return nil, err
} }
var ids []uint var ids []uint
@@ -86,7 +86,7 @@ func (r *permissionRepo) SetSelectedButtons(ctx context.Context, aid, menuID uin
if err := r.checkButtonAssignmentAuth(ctx, map[uint][]uint{menuID: ids}); err != nil { if err := r.checkButtonAssignmentAuth(ctx, map[uint][]uint{menuID: ids}); err != nil {
return err return err
} }
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("authority_id = ? AND sys_menu_id = ?", aid, menuID).Delete(&authorityButtonPO{}).Error; err != nil { if err := tx.Where("authority_id = ? AND sys_menu_id = ?", aid, menuID).Delete(&authorityButtonPO{}).Error; err != nil {
return err return err
} }
@@ -102,7 +102,7 @@ func (r *permissionRepo) SetSelectedButtons(ctx context.Context, aid, menuID uin
} }
func (r *permissionRepo) CanRemoveButton(ctx context.Context, id string) (bool, error) { func (r *permissionRepo) CanRemoveButton(ctx context.Context, id string) (bool, error) {
var row authorityButtonPO var row authorityButtonPO
err := r.data.gormDB.WithContext(ctx).Where("sys_base_menu_btn_id = ?", id).First(&row).Error err := r.data.DB().WithContext(ctx).Where("sys_base_menu_btn_id = ?", id).First(&row).Error
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return true, nil return true, nil
} }
@@ -113,7 +113,7 @@ func (r *permissionRepo) CanRemoveButton(ctx context.Context, id string) (bool,
} }
func (r *permissionRepo) AuthorityButtonIDs(ctx context.Context, aid uint) ([]uint, error) { func (r *permissionRepo) AuthorityButtonIDs(ctx context.Context, aid uint) ([]uint, error) {
var ids []uint var ids []uint
err := r.data.gormDB.WithContext(ctx).Model(&authorityButtonPO{}).Where("authority_id = ?", aid).Pluck("sys_base_menu_btn_id", &ids).Error err := r.data.DB().WithContext(ctx).Model(&authorityButtonPO{}).Where("authority_id = ?", aid).Pluck("sys_base_menu_btn_id", &ids).Error
return ids, err return ids, err
} }
@@ -122,7 +122,7 @@ func (r *permissionRepo) checkButtonAssignmentAuth(ctx context.Context, buttons
if err != nil || !strict { if err != nil || !strict {
return err return err
} }
return checkButtonAssignment(r.data.gormDB.WithContext(ctx), actor.AuthorityID, allowedAuthorities[actor.AuthorityID], buttons) return checkButtonAssignment(r.data.DB().WithContext(ctx), actor.AuthorityID, allowedAuthorities[actor.AuthorityID], buttons)
} }
func checkButtonAssignment(db *gorm.DB, actorID uint, root bool, buttons map[uint][]uint) error { func checkButtonAssignment(db *gorm.DB, actorID uint, root bool, buttons map[uint][]uint) error {
@@ -1,4 +1,4 @@
package data package system
import ( import (
"context" "context"
@@ -1,4 +1,4 @@
package data package system
import ( import (
"context" "context"
@@ -10,9 +10,9 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
type positionRepo struct{ data *Data } type positionRepo struct{ data Provider }
func NewPositionRepo(data *Data) biz.PositionRepo { return &positionRepo{data: data} } func NewPositionRepo(data Provider) biz.PositionRepo { return &positionRepo{data: data} }
type positionPO struct { type positionPO struct {
ID uint `gorm:"primaryKey"` ID uint `gorm:"primaryKey"`
@@ -41,33 +41,33 @@ func posFromPO(po positionPO) *biz.Position {
return &biz.Position{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Name: po.Name, Code: po.Code, Sort: po.Sort, Status: po.Status, Remark: po.Remark} return &biz.Position{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Name: po.Name, Code: po.Code, Sort: po.Sort, Status: po.Status, Remark: po.Remark}
} }
func (r *positionRepo) CreatePosition(ctx context.Context, v *biz.Position) error { func (r *positionRepo) CreatePosition(ctx context.Context, v *biz.Position) error {
return r.data.gormDB.WithContext(ctx).Create(&positionPO{Name: v.Name, Code: v.Code, Sort: v.Sort, Status: v.Status, Remark: v.Remark}).Error return r.data.DB().WithContext(ctx).Create(&positionPO{Name: v.Name, Code: v.Code, Sort: v.Sort, Status: v.Status, Remark: v.Remark}).Error
} }
func (r *positionRepo) UpdatePosition(ctx context.Context, v *biz.Position) error { func (r *positionRepo) UpdatePosition(ctx context.Context, v *biz.Position) error {
return r.data.gormDB.WithContext(ctx).Model(&positionPO{}).Where("id = ?", v.ID).Updates(map[string]any{"name": v.Name, "code": v.Code, "sort": v.Sort, "status": v.Status, "remark": v.Remark}).Error return r.data.DB().WithContext(ctx).Model(&positionPO{}).Where("id = ?", v.ID).Updates(map[string]any{"name": v.Name, "code": v.Code, "sort": v.Sort, "status": v.Status, "remark": v.Remark}).Error
} }
func (r *positionRepo) DeletePosition(ctx context.Context, id uint) error { func (r *positionRepo) DeletePosition(ctx context.Context, id uint) error {
if id == 0 { if id == 0 {
return errors.New("岗位ID不能为空") return errors.New("岗位ID不能为空")
} }
var count int64 var count int64
if err := r.data.gormDB.WithContext(ctx).Model(&userPositionPO{}).Where("sys_position_id = ?", id).Count(&count).Error; err != nil { if err := r.data.DB().WithContext(ctx).Model(&userPositionPO{}).Where("sys_position_id = ?", id).Count(&count).Error; err != nil {
return err return err
} }
if count > 0 { if count > 0 {
return errors.New("该岗位下存在用户,不允许删除") return errors.New("该岗位下存在用户,不允许删除")
} }
return r.data.gormDB.WithContext(ctx).Delete(&positionPO{}, id).Error return r.data.DB().WithContext(ctx).Delete(&positionPO{}, id).Error
} }
func (r *positionRepo) FindPosition(ctx context.Context, id uint) (*biz.Position, error) { func (r *positionRepo) FindPosition(ctx context.Context, id uint) (*biz.Position, error) {
var po positionPO var po positionPO
if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil { if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err return nil, err
} }
return posFromPO(po), nil return posFromPO(po), nil
} }
func (r *positionRepo) ListPositions(ctx context.Context, page, size int, q *biz.PositionListFilter) ([]*biz.Position, int64, error) { func (r *positionRepo) ListPositions(ctx context.Context, page, size int, q *biz.PositionListFilter) ([]*biz.Position, int64, error) {
db := r.data.gormDB.WithContext(ctx).Model(&positionPO{}) db := r.data.DB().WithContext(ctx).Model(&positionPO{})
if q != nil { if q != nil {
if q.Name != "" { if q.Name != "" {
db = db.Where("name LIKE ?", "%"+q.Name+"%") db = db.Where("name LIKE ?", "%"+q.Name+"%")
@@ -98,14 +98,14 @@ func (r *positionRepo) ListPositions(ctx context.Context, page, size int, q *biz
} }
func (r *positionRepo) PositionUserIDs(ctx context.Context, id uint) ([]uint, error) { func (r *positionRepo) PositionUserIDs(ctx context.Context, id uint) ([]uint, error) {
var ids []uint var ids []uint
err := r.data.gormDB.WithContext(ctx).Model(&userPositionPO{}).Where("sys_position_id = ?", id).Pluck("sys_user_id", &ids).Error err := r.data.DB().WithContext(ctx).Model(&userPositionPO{}).Where("sys_position_id = ?", id).Pluck("sys_user_id", &ids).Error
return ids, err return ids, err
} }
func (r *positionRepo) SetPositionUsers(ctx context.Context, id uint, ids []uint) error { func (r *positionRepo) SetPositionUsers(ctx context.Context, id uint, ids []uint) error {
if id == 0 { if id == 0 {
return errors.New("岗位ID不能为空") return errors.New("岗位ID不能为空")
} }
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("sys_position_id = ?", id).Delete(&userPositionPO{}).Error; err != nil { if err := tx.Where("sys_position_id = ?", id).Delete(&userPositionPO{}).Error; err != nil {
return err return err
} }
@@ -120,7 +120,7 @@ func (r *positionRepo) SetPositionUsers(ctx context.Context, id uint, ids []uint
}) })
} }
func (r *positionRepo) SetUserPositions(ctx context.Context, uid uint, ids []uint) error { func (r *positionRepo) SetUserPositions(ctx context.Context, uid uint, ids []uint) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("sys_user_id = ?", uid).Delete(&userPositionPO{}).Error; err != nil { if err := tx.Where("sys_user_id = ?", uid).Delete(&userPositionPO{}).Error; err != nil {
return err return err
} }
+16
View File
@@ -0,0 +1,16 @@
package system
import (
"kra/internal/conf"
"gorm.io/gorm"
)
// Provider exposes the shared data infrastructure needed by system
// repositories without coupling this module to the parent data package.
type Provider interface {
DB() *gorm.DB
Database(name string) (*gorm.DB, error)
DatabaseReady() bool
Runtime() *conf.Runtime
}
+156
View File
@@ -0,0 +1,156 @@
package system
import (
"context"
"strings"
"time"
"kra/internal/biz"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
func SeedSystem(ctx context.Context, db *gorm.DB, input *biz.DatabaseConfig) error {
return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
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
}
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 {
return err
}
}
var persisted []menuPO
if err := tx.Order("sort asc, id asc").Find(&persisted).Error; err != nil {
return err
}
nameID := make(map[string]uint, len(persisted))
for _, menu := range persisted {
nameID[menu.Name] = menu.ID
}
for i := range menus {
if menus[i].ActiveName != "" {
if err := tx.Model(&menuPO{}).Where("name = ?", menus[i].Name).Updates(map[string]any{"parent_id": nameID[menus[i].ActiveName], "active_name": ""}).Error; err != nil {
return err
}
}
}
if err := tx.Where("sys_authority_authority_id = ?", 888).Delete(&authorityMenuPO{}).Error; err != nil {
return err
}
links := make([]authorityMenuPO, 0, len(persisted))
for _, menu := range persisted {
links = append(links, authorityMenuPO{SysAuthorityAuthorityID: 888, SysBaseMenuID: menu.ID})
}
if len(links) > 0 {
if err := tx.Create(&links).Error; err != nil {
return err
}
}
var count int64
if err := tx.Model(&userPO{}).Where("username = ?", "admin").Count(&count).Error; err != nil {
return err
}
if count == 0 {
hash, err := bcrypt.GenerateFromPassword([]byte(input.AdminPassword), bcrypt.DefaultCost)
if err != nil {
return err
}
now := time.Now()
user := userPO{UUID: uuid.NewString(), Username: "admin", Password: string(hash), NickName: "超级管理员", AuthorityID: 888, Enable: 1, PasswordUpdatedAt: &now}
if err := tx.Create(&user).Error; err != nil {
return err
}
if err := tx.Create(&userAuthorityPO{SysUserID: user.ID, SysAuthorityAuthorityID: 888}).Error; err != nil {
return err
}
}
enabled := true
department := departmentPO{Name: "总公司", ParentID: 0, Ancestors: "0", Sort: 0, Status: &enabled}
if err := tx.Where("name = ?", department.Name).FirstOrCreate(&department).Error; err != nil {
return err
}
for _, position := range []positionPO{{Name: "总经理", Code: "CEO", Sort: 1, Status: &enabled}, {Name: "普通员工", Code: "STAFF", Sort: 2, Status: &enabled}} {
if err := tx.Where("code = ?", position.Code).FirstOrCreate(&position).Error; err != nil {
return err
}
}
security := DefaultSecurityConfig()
if err := tx.FirstOrCreate(&security, SecurityConfigPO{ID: 1}).Error; err != nil {
return err
}
exportTemplate := exportTemplatePO{Name: "api", DBTableName: "sys_apis", TemplateID: "api", TemplateInfo: "{\n\"path\":\"路径\",\n\"method\":\"方法(大写)\",\n\"description\":\"方法介绍\",\n\"api_group\":\"方法分组\"\n}"}
if err := tx.Where("template_id = ?", exportTemplate.TemplateID).FirstOrCreate(&exportTemplate).Error; err != nil {
return err
}
for _, task := range []taskPO{{Name: "ClearDB", Description: "定时清理数据库过期日志(操作记录/JWT黑名单/定时任务执行日志)", Spec: "@daily", ExecutorType: "method", MethodName: "ClearDB", Enabled: true}, {Name: "CleanStaleUploads", Description: "定时清理过期大文件上传会话", Spec: "@hourly", ExecutorType: "method", MethodName: "CleanStaleUploads", Enabled: true}} {
if err := tx.Where("name = ?", task.Name).FirstOrCreate(&task).Error; err != nil {
return err
}
}
for _, item := range input.APIs {
if item != nil {
po := apiPO{Path: item.Path, Method: strings.ToUpper(item.Method), Description: item.Description, APIGroup: item.APIGroup}
if err := tx.Where("path = ? AND method = ?", po.Path, po.Method).FirstOrCreate(&po).Error; err != nil {
return err
}
}
}
ignoredAPIs := make([]ignoredAPIPO, 0)
for _, item := range DefaultIgnoredAPIs("uploads/file") {
ignoredAPIs = append(ignoredAPIs, ignoredAPIPO{Method: item.Method, Path: item.Path})
}
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 {
rule := newPolicyRule(888, api.Path, api.Method)
if err := tx.Create(&rule).Error; err != nil {
return err
}
}
}
return nil
})
}
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}
}
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), 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), child("media", "upload", "upload", "view/media/upload.vue", "媒体库(上传下载)", "upload", 1), child("media", "chunkUpload", "chunkUpload", "view/media/chunkUpload.vue", "大文件上传", "folder-add", 2), child("extensions", "email", "email", "modules/email/view/index.vue", "邮件发送", "message", 4), child("extensions", "anInfo", "anInfo", "modules/announcement/view/info.vue", "公告管理", "bell", 5)}
}
@@ -1,4 +1,4 @@
package data package system
import "testing" import "testing"
@@ -8,7 +8,7 @@ func TestDefaultIgnoredAPIsIncludeSwagger(t *testing.T) {
"GET /uploads/file/*filepath": false, "GET /uploads/file/*filepath": false,
"HEAD /uploads/file/*filepath": false, "HEAD /uploads/file/*filepath": false,
} }
for _, api := range defaultIgnoredAPIs("uploads/file") { for _, api := range DefaultIgnoredAPIs("uploads/file") {
key := api.Method + " " + api.Path key := api.Method + " " + api.Path
if _, ok := wants[key]; ok { if _, ok := wants[key]; ok {
wants[key] = true wants[key] = true
@@ -1,8 +1,10 @@
package data package system
import ( import (
"context" "context"
"kra/internal/biz" "kra/internal/biz"
"kra/pkg/gormkit"
"kra/pkg/pagination"
"time" "time"
"gorm.io/gorm" "gorm.io/gorm"
@@ -17,10 +19,10 @@ type taskPO struct {
Description, Spec string Description, Spec string
WithSeconds bool WithSeconds bool
ExecutorType, MethodName string ExecutorType, MethodName string
Params jsonPO Params gormkit.JSON
HTTPURL string HTTPURL string
HTTPMethod string HTTPMethod string
HTTPHeader jsonPO HTTPHeader gormkit.JSON
HTTPBody string `gorm:"type:text"` HTTPBody string `gorm:"type:text"`
HTTPAllowPrivate, Enabled bool HTTPAllowPrivate, Enabled bool
} }
@@ -42,18 +44,18 @@ type taskLogPO struct {
func (taskLogPO) TableName() string { return "sys_timed_task_logs" } func (taskLogPO) TableName() string { return "sys_timed_task_logs" }
type taskRepo struct{ data *Data } type taskRepo struct{ data Provider }
func NewTaskRepo(data *Data) biz.TaskRepo { return &taskRepo{data: data} } func NewTaskRepo(data Provider) biz.TaskRepo { return &taskRepo{data: data} }
func taskToPO(v *biz.TimedTask) taskPO { func taskToPO(v *biz.TimedTask) taskPO {
return taskPO{ID: v.ID, Name: v.Name, Description: v.Description, Spec: v.Spec, WithSeconds: v.WithSeconds, ExecutorType: v.ExecutorType, MethodName: v.MethodName, Params: jsonPO(v.Params), HTTPURL: v.HTTPURL, HTTPMethod: v.HTTPMethod, HTTPHeader: jsonPO(v.HTTPHeader), HTTPBody: v.HTTPBody, HTTPAllowPrivate: v.HTTPAllowPrivate, Enabled: v.Enabled} return taskPO{ID: v.ID, Name: v.Name, Description: v.Description, Spec: v.Spec, WithSeconds: v.WithSeconds, ExecutorType: v.ExecutorType, MethodName: v.MethodName, Params: gormkit.JSON(v.Params), HTTPURL: v.HTTPURL, HTTPMethod: v.HTTPMethod, HTTPHeader: gormkit.JSON(v.HTTPHeader), HTTPBody: v.HTTPBody, HTTPAllowPrivate: v.HTTPAllowPrivate, Enabled: v.Enabled}
} }
func taskFromPO(v taskPO) *biz.TimedTask { func taskFromPO(v taskPO) *biz.TimedTask {
return &biz.TimedTask{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, Name: v.Name, Description: v.Description, Spec: v.Spec, WithSeconds: v.WithSeconds, ExecutorType: v.ExecutorType, MethodName: v.MethodName, Params: []byte(v.Params), HTTPURL: v.HTTPURL, HTTPMethod: v.HTTPMethod, HTTPHeader: []byte(v.HTTPHeader), HTTPBody: v.HTTPBody, HTTPAllowPrivate: v.HTTPAllowPrivate, Enabled: v.Enabled} return &biz.TimedTask{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, Name: v.Name, Description: v.Description, Spec: v.Spec, WithSeconds: v.WithSeconds, ExecutorType: v.ExecutorType, MethodName: v.MethodName, Params: []byte(v.Params), HTTPURL: v.HTTPURL, HTTPMethod: v.HTTPMethod, HTTPHeader: []byte(v.HTTPHeader), HTTPBody: v.HTTPBody, HTTPAllowPrivate: v.HTTPAllowPrivate, Enabled: v.Enabled}
} }
func (r *taskRepo) CreateTask(ctx context.Context, v *biz.TimedTask) error { func (r *taskRepo) CreateTask(ctx context.Context, v *biz.TimedTask) error {
po := taskToPO(v) po := taskToPO(v)
if err := r.data.gormDB.WithContext(ctx).Create(&po).Error; err != nil { if err := r.data.DB().WithContext(ctx).Create(&po).Error; err != nil {
return err return err
} }
v.ID = po.ID v.ID = po.ID
@@ -61,7 +63,7 @@ func (r *taskRepo) CreateTask(ctx context.Context, v *biz.TimedTask) error {
} }
func (r *taskRepo) TaskNameExists(ctx context.Context, name string, excludeID uint) (bool, error) { func (r *taskRepo) TaskNameExists(ctx context.Context, name string, excludeID uint) (bool, error) {
var count int64 var count int64
db := r.data.gormDB.WithContext(ctx).Model(&taskPO{}).Where("name = ?", name) db := r.data.DB().WithContext(ctx).Model(&taskPO{}).Where("name = ?", name)
if excludeID > 0 { if excludeID > 0 {
db = db.Where("id <> ?", excludeID) db = db.Where("id <> ?", excludeID)
} }
@@ -70,20 +72,20 @@ func (r *taskRepo) TaskNameExists(ctx context.Context, name string, excludeID ui
} }
func (r *taskRepo) UpdateTask(ctx context.Context, v *biz.TimedTask) error { func (r *taskRepo) UpdateTask(ctx context.Context, v *biz.TimedTask) error {
po := taskToPO(v) po := taskToPO(v)
return r.data.gormDB.WithContext(ctx).Model(&taskPO{}).Where("id = ?", v.ID).Select("name", "description", "spec", "with_seconds", "executor_type", "method_name", "params", "http_url", "http_method", "http_header", "http_body", "http_allow_private", "enabled").Updates(&po).Error return r.data.DB().WithContext(ctx).Model(&taskPO{}).Where("id = ?", v.ID).Select("name", "description", "spec", "with_seconds", "executor_type", "method_name", "params", "http_url", "http_method", "http_header", "http_body", "http_allow_private", "enabled").Updates(&po).Error
} }
func (r *taskRepo) DeleteTask(ctx context.Context, id uint) error { func (r *taskRepo) DeleteTask(ctx context.Context, id uint) error {
return r.data.gormDB.WithContext(ctx).Delete(&taskPO{}, id).Error return r.data.DB().WithContext(ctx).Delete(&taskPO{}, id).Error
} }
func (r *taskRepo) FindTask(ctx context.Context, id uint) (*biz.TimedTask, error) { func (r *taskRepo) FindTask(ctx context.Context, id uint) (*biz.TimedTask, error) {
var po taskPO var po taskPO
if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil { if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err return nil, err
} }
return taskFromPO(po), nil return taskFromPO(po), nil
} }
func (r *taskRepo) ListTasks(ctx context.Context, page, size int, q *biz.TimedTask) ([]*biz.TimedTask, int64, error) { func (r *taskRepo) ListTasks(ctx context.Context, page, size int, q *biz.TimedTask) ([]*biz.TimedTask, int64, error) {
db := r.data.gormDB.WithContext(ctx).Model(&taskPO{}) db := r.data.DB().WithContext(ctx).Model(&taskPO{})
if q != nil { if q != nil {
if q.Name != "" { if q.Name != "" {
db = db.Where("name LIKE ?", "%"+q.Name+"%") db = db.Where("name LIKE ?", "%"+q.Name+"%")
@@ -100,7 +102,7 @@ func (r *taskRepo) ListTasks(ctx context.Context, page, size int, q *biz.TimedTa
return nil, 0, err return nil, 0, err
} }
var pos []taskPO var pos []taskPO
if err := applyPagination(db.Order("id desc"), page, size, 100).Find(&pos).Error; err != nil { if err := pagination.Apply(db.Order("id desc"), page, size, 100).Find(&pos).Error; err != nil {
return nil, 0, err return nil, 0, err
} }
out := make([]*biz.TimedTask, 0, len(pos)) out := make([]*biz.TimedTask, 0, len(pos))
@@ -110,16 +112,16 @@ func (r *taskRepo) ListTasks(ctx context.Context, page, size int, q *biz.TimedTa
return out, total, nil return out, total, nil
} }
func (r *taskRepo) ToggleTask(ctx context.Context, id uint, enabled bool) error { func (r *taskRepo) ToggleTask(ctx context.Context, id uint, enabled bool) error {
return r.data.gormDB.WithContext(ctx).Model(&taskPO{}).Where("id = ?", id).Update("enabled", enabled).Error return r.data.DB().WithContext(ctx).Model(&taskPO{}).Where("id = ?", id).Update("enabled", enabled).Error
} }
func (r *taskRepo) RecordTaskLog(ctx context.Context, v *biz.TimedTaskLog) error { 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 return r.data.DB().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 { func taskLogFromPO(v taskLogPO) *biz.TimedTaskLog {
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} 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) { 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{}) db := r.data.DB().WithContext(ctx).Model(&taskLogPO{})
if taskID != 0 { if taskID != 0 {
db = db.Where("task_id = ?", taskID) db = db.Where("task_id = ?", taskID)
} }
@@ -131,7 +133,7 @@ func (r *taskRepo) ListTaskLogs(ctx context.Context, page, size int, taskID uint
return nil, 0, err return nil, 0, err
} }
var pos []taskLogPO var pos []taskLogPO
if err := applyPagination(db.Order("id desc"), page, size, 100).Find(&pos).Error; err != nil { if err := pagination.Apply(db.Order("id desc"), page, size, 100).Find(&pos).Error; err != nil {
return nil, 0, err return nil, 0, err
} }
out := make([]*biz.TimedTaskLog, 0, len(pos)) out := make([]*biz.TimedTaskLog, 0, len(pos))
@@ -142,7 +144,7 @@ func (r *taskRepo) ListTaskLogs(ctx context.Context, page, size int, taskID uint
} }
func (r *taskRepo) CleanupLogs(ctx context.Context) error { func (r *taskRepo) CleanupLogs(ctx context.Context) error {
now := time.Now() now := time.Now()
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Unscoped().Where("created_at < ?", now.Add(-2160*time.Hour)).Delete(&operationPO{}).Error; err != nil { if err := tx.Unscoped().Where("created_at < ?", now.Add(-2160*time.Hour)).Delete(&operationPO{}).Error; err != nil {
return err return err
} }
@@ -0,0 +1,76 @@
package system
import (
"context"
"fmt"
"sync/atomic"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
"kra/internal/conf"
"kra/internal/data/migration"
)
// Data is a small in-package test harness. Production repositories depend on
// Provider; tests keep the old, convenient field-based setup without coupling
// the system module back to internal/data.
type Data struct {
gormDB *reloadableDB
redis *reloadableRedis
runtime *conf.Runtime
databaseReady atomic.Bool
}
func (d *Data) DB() *gorm.DB {
if d == nil || d.gormDB == nil {
return nil
}
return d.gormDB.DB()
}
func (d *Data) Database(string) (*gorm.DB, error) { return d.DB(), nil }
func (d *Data) DatabaseReady() bool { return d != nil && d.databaseReady.Load() }
func (d *Data) Runtime() *conf.Runtime {
if d == nil {
return nil
}
return d.runtime
}
type reloadableDB struct{ db *gorm.DB }
func newReloadableDB(db *gorm.DB, _ any) *reloadableDB { return &reloadableDB{db: db} }
func (r *reloadableDB) DB() *gorm.DB {
if r == nil {
return nil
}
return r.db
}
func (r *reloadableDB) WithContext(ctx context.Context) *gorm.DB { return r.db.WithContext(ctx) }
func (r *reloadableDB) close() {
if r == nil || r.db == nil {
return
}
sqlDB, err := r.db.DB()
if err == nil {
_ = sqlDB.Close()
}
}
type reloadableRedis struct{}
func newReloadableRedis(_ any) *reloadableRedis { return &reloadableRedis{} }
func openWithDriver(driver, dsn string) (*gorm.DB, error) {
if driver != "sqlite" {
return nil, fmt.Errorf("unsupported test database driver %q", driver)
}
return gorm.Open(sqlite.Open(dsn), &gorm.Config{})
}
func migrateAll(db *gorm.DB) error {
return migration.Run(db, []migration.Step{
{ID: "test_baseline", Migrate: LegacySchemaMigration},
{ID: "test_reconcile", Migrate: CurrentDataMigration},
})
}
@@ -1,4 +1,4 @@
package data package system
import ( import (
"context" "context"
@@ -1,4 +1,4 @@
package data package system
import ( import (
"context" "context"
@@ -9,6 +9,7 @@ import (
"time" "time"
"kra/internal/biz" "kra/internal/biz"
"kra/pkg/pagination"
"github.com/google/uuid" "github.com/google/uuid"
"gorm.io/gorm" "gorm.io/gorm"
@@ -17,7 +18,7 @@ import (
func (r *userRepo) FindUserByUsername(ctx context.Context, username string) (*biz.User, error) { func (r *userRepo) FindUserByUsername(ctx context.Context, username string) (*biz.User, error) {
var po userPO var po userPO
if err := r.data.gormDB.WithContext(ctx).Where("username = ?", username).First(&po).Error; err != nil { if err := r.data.DB().WithContext(ctx).Where("username = ?", username).First(&po).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, biz.ErrUserNotFound return nil, biz.ErrUserNotFound
} }
@@ -28,7 +29,7 @@ func (r *userRepo) FindUserByUsername(ctx context.Context, username string) (*bi
func (r *userRepo) FindUserByID(ctx context.Context, id uint) (*biz.User, error) { func (r *userRepo) FindUserByID(ctx context.Context, id uint) (*biz.User, error) {
var po userPO var po userPO
if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil { if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, biz.ErrUserNotFound return nil, biz.ErrUserNotFound
} }
@@ -39,7 +40,7 @@ func (r *userRepo) FindUserByID(ctx context.Context, id uint) (*biz.User, error)
func (r *userRepo) FindUserByUUID(ctx context.Context, value string) (*biz.User, error) { func (r *userRepo) FindUserByUUID(ctx context.Context, value string) (*biz.User, error) {
var po userPO var po userPO
if err := r.data.gormDB.WithContext(ctx).Where("uuid = ?", value).First(&po).Error; err != nil { if err := r.data.DB().WithContext(ctx).Where("uuid = ?", value).First(&po).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, biz.ErrUserNotFound return nil, biz.ErrUserNotFound
} }
@@ -50,11 +51,11 @@ func (r *userRepo) FindUserByUUID(ctx context.Context, value string) (*biz.User,
func (r *userRepo) loadUser(ctx context.Context, po *userPO) (*biz.User, error) { func (r *userRepo) loadUser(ctx context.Context, po *userPO) (*biz.User, error) {
var authority authorityPO var authority authorityPO
if err := r.data.gormDB.WithContext(ctx).First(&authority, "authority_id = ?", po.AuthorityID).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { if err := r.data.DB().WithContext(ctx).First(&authority, "authority_id = ?", po.AuthorityID).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err return nil, err
} }
var authorityPOs []authorityPO var authorityPOs []authorityPO
if err := r.data.gormDB.WithContext(ctx).Table("sys_authorities").Joins("JOIN sys_user_authority ON sys_user_authority.sys_authority_authority_id = sys_authorities.authority_id").Where("sys_user_authority.sys_user_id = ? AND sys_authorities.deleted_at IS NULL", po.ID).Find(&authorityPOs).Error; err != nil { if err := r.data.DB().WithContext(ctx).Table("sys_authorities").Joins("JOIN sys_user_authority ON sys_user_authority.sys_authority_authority_id = sys_authorities.authority_id").Where("sys_user_authority.sys_user_id = ? AND sys_authorities.deleted_at IS NULL", po.ID).Find(&authorityPOs).Error; err != nil {
return nil, err return nil, err
} }
authorities := make([]biz.Authority, 0, len(authorityPOs)) authorities := make([]biz.Authority, 0, len(authorityPOs))
@@ -66,7 +67,7 @@ func (r *userRepo) loadUser(ctx context.Context, po *userPO) (*biz.User, error)
_ = json.Unmarshal([]byte(po.OriginSetting), &setting) _ = json.Unmarshal([]byte(po.OriginSetting), &setting)
} }
var departmentPOs []departmentPO var departmentPOs []departmentPO
if err := r.data.gormDB.WithContext(ctx).Table("sys_departments").Joins("JOIN sys_user_departments ON sys_user_departments.sys_department_id = sys_departments.id").Where("sys_user_departments.sys_user_id = ? AND sys_departments.deleted_at IS NULL", po.ID).Find(&departmentPOs).Error; err != nil { if err := r.data.DB().WithContext(ctx).Table("sys_departments").Joins("JOIN sys_user_departments ON sys_user_departments.sys_department_id = sys_departments.id").Where("sys_user_departments.sys_user_id = ? AND sys_departments.deleted_at IS NULL", po.ID).Find(&departmentPOs).Error; err != nil {
return nil, err return nil, err
} }
departments := make([]biz.Department, 0, len(departmentPOs)) departments := make([]biz.Department, 0, len(departmentPOs))
@@ -77,14 +78,14 @@ func (r *userRepo) loadUser(ctx context.Context, po *userPO) (*biz.User, error)
var primary *biz.Department var primary *biz.Department
if po.DeptID != 0 { if po.DeptID != 0 {
var primaryPO departmentPO var primaryPO departmentPO
if err := r.data.gormDB.WithContext(ctx).First(&primaryPO, po.DeptID).Error; err == nil { if err := r.data.DB().WithContext(ctx).First(&primaryPO, po.DeptID).Error; err == nil {
primary = deptFromPO(primaryPO) primary = deptFromPO(primaryPO)
} else if !errors.Is(err, gorm.ErrRecordNotFound) { } else if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err return nil, err
} }
} }
var positionPOs []positionPO var positionPOs []positionPO
if err := r.data.gormDB.WithContext(ctx).Table("sys_positions").Joins("JOIN sys_user_positions ON sys_user_positions.sys_position_id = sys_positions.id").Where("sys_user_positions.sys_user_id = ? AND sys_positions.deleted_at IS NULL", po.ID).Find(&positionPOs).Error; err != nil { if err := r.data.DB().WithContext(ctx).Table("sys_positions").Joins("JOIN sys_user_positions ON sys_user_positions.sys_position_id = sys_positions.id").Where("sys_user_positions.sys_user_id = ? AND sys_positions.deleted_at IS NULL", po.ID).Find(&positionPOs).Error; err != nil {
return nil, err return nil, err
} }
positions := make([]biz.Position, 0, len(positionPOs)) positions := make([]biz.Position, 0, len(positionPOs))
@@ -115,7 +116,7 @@ func (r *userRepo) loadUsers(ctx context.Context, pos []userPO) ([]*biz.User, er
if len(pos) == 0 { if len(pos) == 0 {
return []*biz.User{}, nil return []*biz.User{}, nil
} }
db := r.data.gormDB.WithContext(ctx) db := r.data.DB().WithContext(ctx)
userIDs := make([]uint, 0, len(pos)) userIDs := make([]uint, 0, len(pos))
authorityIDs := make([]uint, 0, len(pos)) authorityIDs := make([]uint, 0, len(pos))
departmentIDs := make([]uint, 0, len(pos)) departmentIDs := make([]uint, 0, len(pos))
@@ -218,7 +219,7 @@ func (r *userRepo) loadUsers(ctx context.Context, pos []userPO) ([]*biz.User, er
func (r *userRepo) HasAuthorityMenu(ctx context.Context, authorityID uint, name string) (bool, error) { func (r *userRepo) HasAuthorityMenu(ctx context.Context, authorityID uint, name string) (bool, error) {
var count int64 var count int64
err := r.data.gormDB.WithContext(ctx).Model(&menuPO{}). err := r.data.DB().WithContext(ctx).Model(&menuPO{}).
Joins("JOIN sys_authority_menus ON sys_authority_menus.sys_base_menu_id = sys_base_menus.id"). Joins("JOIN sys_authority_menus ON sys_authority_menus.sys_base_menu_id = sys_base_menus.id").
Where("sys_authority_menus.sys_authority_authority_id = ? AND sys_base_menus.name = ?", authorityID, name). Where("sys_authority_menus.sys_authority_authority_id = ? AND sys_base_menus.name = ?", authorityID, name).
Count(&count).Error Count(&count).Error
@@ -227,7 +228,7 @@ func (r *userRepo) HasAuthorityMenu(ctx context.Context, authorityID uint, name
func (r *userRepo) MenusByAuthority(ctx context.Context, authorityID uint) ([]*biz.Menu, error) { func (r *userRepo) MenusByAuthority(ctx context.Context, authorityID uint) ([]*biz.Menu, error) {
var pos []menuPO var pos []menuPO
err := r.data.gormDB.WithContext(ctx).Table("sys_base_menus"). err := r.data.DB().WithContext(ctx).Table("sys_base_menus").
Joins("JOIN sys_authority_menus ON sys_authority_menus.sys_base_menu_id = sys_base_menus.id"). Joins("JOIN sys_authority_menus ON sys_authority_menus.sys_base_menu_id = sys_base_menus.id").
Where("sys_authority_menus.sys_authority_authority_id = ? AND sys_base_menus.deleted_at IS NULL", authorityID). Where("sys_authority_menus.sys_authority_authority_id = ? AND sys_base_menus.deleted_at IS NULL", authorityID).
Order("sys_base_menus.sort").Scan(&pos).Error Order("sys_base_menus.sort").Scan(&pos).Error
@@ -247,7 +248,7 @@ func (r *userRepo) MenusByAuthority(ctx context.Context, authorityID uint) ([]*b
} }
if len(menuIDs) > 0 { if len(menuIDs) > 0 {
var parameters []menuParameterPO var parameters []menuParameterPO
if err := r.data.gormDB.WithContext(ctx).Where("sys_base_menu_id IN ?", menuIDs).Find(&parameters).Error; err != nil { if err := r.data.DB().WithContext(ctx).Where("sys_base_menu_id IN ?", menuIDs).Find(&parameters).Error; err != nil {
return nil, err return nil, err
} }
for _, parameter := range parameters { for _, parameter := range parameters {
@@ -255,7 +256,7 @@ func (r *userRepo) MenusByAuthority(ctx context.Context, authorityID uint) ([]*b
menu.Parameters = append(menu.Parameters, menuParameterFromPO(parameter)) menu.Parameters = append(menu.Parameters, menuParameterFromPO(parameter))
} }
var buttons []menuButtonPO 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 { if err := r.data.DB().WithContext(ctx).Table("sys_base_menu_btns").Joins("JOIN sys_authority_btns ON sys_authority_btns.sys_base_menu_btn_id = sys_base_menu_btns.id").Where("sys_authority_btns.authority_id = ? AND sys_authority_btns.sys_menu_id IN ?", authorityID, menuIDs).Find(&buttons).Error; err != nil {
return nil, err return nil, err
} }
for _, button := range buttons { for _, button := range buttons {
@@ -278,7 +279,7 @@ func (r *userRepo) MenusByAuthority(ctx context.Context, authorityID uint) ([]*b
} }
func (r *userRepo) ListUsers(ctx context.Context, page, pageSize int, filter *biz.UserListFilter) ([]*biz.User, int64, error) { func (r *userRepo) ListUsers(ctx context.Context, page, pageSize int, filter *biz.UserListFilter) ([]*biz.User, int64, error) {
db := r.data.gormDB.WithContext(ctx).Model(&userPO{}) db := r.data.DB().WithContext(ctx).Model(&userPO{})
order := "id desc" order := "id desc"
if filter != nil { if filter != nil {
if filter.Username != "" { if filter.Username != "" {
@@ -306,7 +307,7 @@ func (r *userRepo) ListUsers(ctx context.Context, page, pageSize int, filter *bi
return nil, 0, err return nil, 0, err
} }
var pos []userPO var pos []userPO
if err := applyRequiredPagination(db.Order(order), page, pageSize, 100).Find(&pos).Error; err != nil { if err := pagination.ApplyRequired(db.Order(order), page, pageSize, 100).Find(&pos).Error; err != nil {
return nil, 0, err return nil, 0, err
} }
users, err := r.loadUsers(ctx, pos) users, err := r.loadUsers(ctx, pos)
@@ -337,7 +338,7 @@ func (r *userRepo) CreateUserWithAuthorities(ctx context.Context, user *biz.User
now := time.Now() now := time.Now()
po := userPO{UUID: user.UUID, Username: user.Username, Password: user.Password, NickName: user.NickName, HeaderImg: user.HeaderImg, AuthorityID: user.AuthorityID, Phone: user.Phone, Email: user.Email, Enable: user.Enable, PasswordUpdatedAt: &now, MustChangePassword: user.MustChangePassword} po := userPO{UUID: user.UUID, Username: user.Username, Password: user.Password, NickName: user.NickName, HeaderImg: user.HeaderImg, AuthorityID: user.AuthorityID, Phone: user.Phone, Email: user.Email, Enable: user.Enable, PasswordUpdatedAt: &now, MustChangePassword: user.MustChangePassword}
var createdAuthorities []biz.Authority var createdAuthorities []biz.Authority
if err := r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
// The compatible registration rejects an existing non-deleted username before creating the // The compatible registration rejects an existing non-deleted username before creating the
// account. The username column is intentionally not unique for legacy // account. The username column is intentionally not unique for legacy
// schema compatibility, so keep the behavioral check here rather than // schema compatibility, so keep the behavioral check here rather than
@@ -384,7 +385,7 @@ func (r *userRepo) UpdateUser(ctx context.Context, user *biz.User) error {
if err := r.checkUserUpdateAuth(ctx, user); err != nil { if err := r.checkUserUpdateAuth(ctx, user); err != nil {
return err return err
} }
return r.updateUser(r.data.gormDB.WithContext(ctx), user) return r.updateUser(r.data.DB().WithContext(ctx), user)
} }
func (r *userRepo) checkUserUpdateAuth(ctx context.Context, user *biz.User) error { func (r *userRepo) checkUserUpdateAuth(ctx context.Context, user *biz.User) error {
@@ -418,7 +419,7 @@ func (r *userRepo) UpdateSelfUser(ctx context.Context, user *biz.User) error {
if len(updates) == 0 { if len(updates) == 0 {
return nil return nil
} }
return r.data.gormDB.WithContext(ctx).Model(&userPO{}).Where("id = ?", user.ID).Updates(updates).Error return r.data.DB().WithContext(ctx).Model(&userPO{}).Where("id = ?", user.ID).Updates(updates).Error
} }
func (r *userRepo) FillDepartmentNamePaths(ctx context.Context, user *biz.User) error { func (r *userRepo) FillDepartmentNamePaths(ctx context.Context, user *biz.User) error {
@@ -441,7 +442,7 @@ func (r *userRepo) FillDepartmentNamePaths(ctx context.Context, user *biz.User)
nameByID := make(map[uint]string, len(ids)) nameByID := make(map[uint]string, len(ids))
if len(ids) > 0 { if len(ids) > 0 {
var rows []departmentPO var rows []departmentPO
if err := r.data.gormDB.WithContext(ctx).Select("id", "name").Where("id IN ?", ids).Find(&rows).Error; err != nil { if err := r.data.DB().WithContext(ctx).Select("id", "name").Where("id IN ?", ids).Find(&rows).Error; err != nil {
return err return err
} }
for _, row := range rows { for _, row := range rows {
@@ -478,7 +479,7 @@ func (r *userRepo) UpdateUserWithAuthorities(ctx context.Context, user *biz.User
if err := access.checkAuthorityIDsAuth(ctx, authorityIDs); err != nil { if err := access.checkAuthorityIDsAuth(ctx, authorityIDs); err != nil {
return err return err
} }
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := r.updateUser(tx, user); err != nil { if err := r.updateUser(tx, user); err != nil {
return err return err
} }
@@ -492,7 +493,7 @@ func (r *userRepo) DeleteUser(ctx context.Context, id uint) error {
if err := (&authorityAccessRepo{data: r.data}).checkUserIDAuth(ctx, id, false); err != nil { if err := (&authorityAccessRepo{data: r.data}).checkUserIDAuth(ctx, id, false); err != nil {
return err return err
} }
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("id = ?", id).Delete(&userPO{}).Error; err != nil { if err := tx.Where("id = ?", id).Delete(&userPO{}).Error; err != nil {
return err return err
} }
@@ -517,11 +518,11 @@ func (r *userRepo) UpdatePassword(ctx context.Context, id uint, password string,
if clearMustChange { if clearMustChange {
updates["must_change_password"] = false updates["must_change_password"] = false
} }
return r.data.gormDB.WithContext(ctx).Model(&userPO{}).Where("id = ?", id).Updates(updates).Error return r.data.DB().WithContext(ctx).Model(&userPO{}).Where("id = ?", id).Updates(updates).Error
} }
func (r *userRepo) ListAuthorities(ctx context.Context) ([]*biz.Authority, error) { func (r *userRepo) ListAuthorities(ctx context.Context) ([]*biz.Authority, error) {
var pos []authorityPO var pos []authorityPO
if err := r.data.gormDB.WithContext(ctx).Order("authority_id").Find(&pos).Error; err != nil { if err := r.data.DB().WithContext(ctx).Order("authority_id").Find(&pos).Error; err != nil {
return nil, err return nil, err
} }
result := make([]*biz.Authority, 0, len(pos)) result := make([]*biz.Authority, 0, len(pos))
@@ -540,7 +541,7 @@ func (r *userRepo) SetUserAuthorities(ctx context.Context, id uint, authorityIDs
if err := access.checkAuthorityIDsAuth(ctx, authorityIDs); err != nil { if err := access.checkAuthorityIDsAuth(ctx, authorityIDs); err != nil {
return err return err
} }
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var user userPO var user userPO
if err := tx.Where("id = ?", id).First(&user).Error; err != nil { if err := tx.Where("id = ?", id).First(&user).Error; err != nil {
return errors.New("查询用户数据失败") return errors.New("查询用户数据失败")
@@ -567,7 +568,7 @@ func setUserAuthorities(tx *gorm.DB, id uint, authorityIDs []uint) error {
} }
func (r *userRepo) SetUserAuthority(ctx context.Context, id, authorityID uint) error { func (r *userRepo) SetUserAuthority(ctx context.Context, id, authorityID uint) error {
db := r.data.gormDB.WithContext(ctx) db := r.data.DB().WithContext(ctx)
var count int64 var count int64
if err := db.Model(&userAuthorityPO{}).Where("sys_user_id = ? AND sys_authority_authority_id = ?", id, authorityID).Count(&count).Error; err != nil { if err := db.Model(&userAuthorityPO{}).Where("sys_user_id = ? AND sys_authority_authority_id = ?", id, authorityID).Count(&count).Error; err != nil {
return err return err
@@ -597,7 +598,7 @@ func (r *userRepo) SetUserSetting(ctx context.Context, id uint, setting map[stri
if err != nil { if err != nil {
return err return err
} }
return r.data.gormDB.WithContext(ctx).Model(&userPO{}).Where("id = ?", id).Update("origin_setting", string(value)).Error return r.data.DB().WithContext(ctx).Model(&userPO{}).Where("id = ?", id).Update("origin_setting", string(value)).Error
} }
// ActiveName temporarily carries the parent menu name during seeding. It is // ActiveName temporarily carries the parent menu name during seeding. It is
@@ -1,4 +1,4 @@
package data package system
import ( import (
"context" "context"
@@ -1,4 +1,4 @@
package data package system
import ( import (
"context" "context"
@@ -7,6 +7,7 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
"kra/internal/biz" "kra/internal/biz"
"kra/pkg/pagination"
) )
type versionPO struct { type versionPO struct {
@@ -22,32 +23,32 @@ type versionPO struct {
func (versionPO) TableName() string { return "sys_versions" } func (versionPO) TableName() string { return "sys_versions" }
type versionRepo struct{ data *Data } type versionRepo struct{ data Provider }
func NewVersionRepo(data *Data) biz.VersionRepo { return &versionRepo{data: data} } func NewVersionRepo(data Provider) biz.VersionRepo { return &versionRepo{data: data} }
func versionFromPO(v versionPO) *biz.Version { func versionFromPO(v versionPO) *biz.Version {
return &biz.Version{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, Name: v.Name, Code: v.Code, Description: v.Description, Data: v.Data} return &biz.Version{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, Name: v.Name, Code: v.Code, Description: v.Description, Data: v.Data}
} }
func (r *versionRepo) CreateVersion(ctx context.Context, v *biz.Version) error { func (r *versionRepo) CreateVersion(ctx context.Context, v *biz.Version) error {
po := versionPO{Name: v.Name, Code: v.Code, Description: v.Description, Data: v.Data} po := versionPO{Name: v.Name, Code: v.Code, Description: v.Description, Data: v.Data}
if err := r.data.gormDB.WithContext(ctx).Create(&po).Error; err != nil { if err := r.data.DB().WithContext(ctx).Create(&po).Error; err != nil {
return err return err
} }
v.ID = po.ID v.ID = po.ID
return nil return nil
} }
func (r *versionRepo) DeleteVersions(ctx context.Context, ids []uint) error { func (r *versionRepo) DeleteVersions(ctx context.Context, ids []uint) error {
return r.data.gormDB.WithContext(ctx).Where("id IN ?", ids).Delete(&versionPO{}).Error return r.data.DB().WithContext(ctx).Where("id IN ?", ids).Delete(&versionPO{}).Error
} }
func (r *versionRepo) FindVersion(ctx context.Context, id uint) (*biz.Version, error) { func (r *versionRepo) FindVersion(ctx context.Context, id uint) (*biz.Version, error) {
var po versionPO var po versionPO
if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil { if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err return nil, err
} }
return versionFromPO(po), nil return versionFromPO(po), nil
} }
func (r *versionRepo) ListVersions(ctx context.Context, page, size int, name, code string, createdAtRange []*time.Time) ([]*biz.Version, int64, error) { func (r *versionRepo) ListVersions(ctx context.Context, page, size int, name, code string, createdAtRange []*time.Time) ([]*biz.Version, int64, error) {
db := r.data.gormDB.WithContext(ctx).Model(&versionPO{}) db := r.data.DB().WithContext(ctx).Model(&versionPO{})
if len(createdAtRange) == 2 && createdAtRange[0] != nil && createdAtRange[1] != nil { if len(createdAtRange) == 2 && createdAtRange[0] != nil && createdAtRange[1] != nil {
db = db.Where("created_at BETWEEN ? AND ?", createdAtRange[0], createdAtRange[1]) db = db.Where("created_at BETWEEN ? AND ?", createdAtRange[0], createdAtRange[1])
} }
@@ -62,7 +63,7 @@ func (r *versionRepo) ListVersions(ctx context.Context, page, size int, name, co
return nil, 0, err return nil, 0, err
} }
var pos []versionPO var pos []versionPO
if err := applyPagination(db, page, size, 100).Find(&pos).Error; err != nil { if err := pagination.Apply(db, page, size, 100).Find(&pos).Error; err != nil {
return nil, 0, err return nil, 0, err
} }
out := make([]*biz.Version, 0, len(pos)) out := make([]*biz.Version, 0, len(pos))
@@ -75,7 +76,7 @@ func (r *versionRepo) BuildVersionBundle(ctx context.Context, menuIDs, apiIDs, d
bundle := &biz.VersionBundle{} bundle := &biz.VersionBundle{}
var menus []menuPO var menus []menuPO
if len(menuIDs) > 0 { if len(menuIDs) > 0 {
if err := r.data.gormDB.WithContext(ctx).Where("id IN ?", menuIDs).Order("sort,id").Find(&menus).Error; err != nil { if err := r.data.DB().WithContext(ctx).Where("id IN ?", menuIDs).Order("sort,id").Find(&menus).Error; err != nil {
return nil, &biz.VersionStageError{Stage: biz.VersionStageMenus, Err: err} return nil, &biz.VersionStageError{Stage: biz.VersionStageMenus, Err: err}
} }
} }
@@ -86,7 +87,7 @@ func (r *versionRepo) BuildVersionBundle(ctx context.Context, menuIDs, apiIDs, d
} }
if len(menuIDs) > 0 { if len(menuIDs) > 0 {
var buttons []menuButtonPO var buttons []menuButtonPO
if err := r.data.gormDB.WithContext(ctx).Where("sys_base_menu_id IN ?", menuIDs).Find(&buttons).Error; err != nil { if err := r.data.DB().WithContext(ctx).Where("sys_base_menu_id IN ?", menuIDs).Find(&buttons).Error; err != nil {
return nil, &biz.VersionStageError{Stage: biz.VersionStageMenus, Err: err} return nil, &biz.VersionStageError{Stage: biz.VersionStageMenus, Err: err}
} }
for _, button := range buttons { for _, button := range buttons {
@@ -95,7 +96,7 @@ func (r *versionRepo) BuildVersionBundle(ctx context.Context, menuIDs, apiIDs, d
} }
} }
var parameters []menuParameterPO var parameters []menuParameterPO
if err := r.data.gormDB.WithContext(ctx).Where("sys_base_menu_id IN ?", menuIDs).Find(&parameters).Error; err != nil { if err := r.data.DB().WithContext(ctx).Where("sys_base_menu_id IN ?", menuIDs).Find(&parameters).Error; err != nil {
return nil, &biz.VersionStageError{Stage: biz.VersionStageMenus, Err: err} return nil, &biz.VersionStageError{Stage: biz.VersionStageMenus, Err: err}
} }
for _, parameter := range parameters { for _, parameter := range parameters {
@@ -142,7 +143,7 @@ func (r *versionRepo) BuildVersionBundle(ctx context.Context, menuIDs, apiIDs, d
} }
var apis []apiPO var apis []apiPO
if len(apiIDs) > 0 { if len(apiIDs) > 0 {
if err := r.data.gormDB.WithContext(ctx).Where("id IN ?", apiIDs).Find(&apis).Error; err != nil { if err := r.data.DB().WithContext(ctx).Where("id IN ?", apiIDs).Find(&apis).Error; err != nil {
return nil, &biz.VersionStageError{Stage: biz.VersionStageAPIs, Err: err} return nil, &biz.VersionStageError{Stage: biz.VersionStageAPIs, Err: err}
} }
} }
@@ -153,7 +154,7 @@ func (r *versionRepo) BuildVersionBundle(ctx context.Context, menuIDs, apiIDs, d
} }
var dictionaries []dictionaryPO var dictionaries []dictionaryPO
if len(dictIDs) > 0 { if len(dictIDs) > 0 {
if err := r.data.gormDB.WithContext(ctx).Where("id IN ?", dictIDs).Find(&dictionaries).Error; err != nil { if err := r.data.DB().WithContext(ctx).Where("id IN ?", dictIDs).Find(&dictionaries).Error; err != nil {
return nil, &biz.VersionStageError{Stage: biz.VersionStageDictionaries, Err: err} return nil, &biz.VersionStageError{Stage: biz.VersionStageDictionaries, Err: err}
} }
} }
@@ -178,7 +179,7 @@ func (r *versionRepo) BuildVersionBundle(ctx context.Context, menuIDs, apiIDs, d
return bundle, nil return bundle, nil
} }
func (r *versionRepo) ImportVersionBundle(ctx context.Context, b *biz.VersionBundle) error { func (r *versionRepo) ImportVersionBundle(ctx context.Context, b *biz.VersionBundle) error {
db := r.data.gormDB.WithContext(ctx) db := r.data.DB().WithContext(ctx)
if len(b.Menus) > 0 { if len(b.Menus) > 0 {
if err := db.Transaction(func(tx *gorm.DB) error { return importVersionMenus(tx, b.Menus, 0) }); err != nil { 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} return &biz.VersionStageError{Stage: biz.VersionStageMenus, Err: err}
@@ -1,4 +1,4 @@
package data package system
import ( import (
"context" "context"
@@ -1,4 +1,4 @@
package data package cache
import ( import (
"context" "context"
@@ -6,33 +6,34 @@ import (
"sync" "sync"
"time" "time"
"kra/internal/biz"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
"kra/internal/biz"
) )
type memoryCacheEntry struct { type RedisProvider interface {
RedisClient() redis.UniversalClient
}
type memoryEntry struct {
value string value string
expiresAt time.Time expiresAt time.Time
} }
type cacheStore struct { type Store struct {
data *Data provider RedisProvider
mu sync.Mutex mu sync.Mutex
memory map[string]memoryCacheEntry memory map[string]memoryEntry
} }
const maxMemoryCacheEntries = 10000 const maxMemoryEntries = 10000
func NewCache(data *Data) biz.Cache { func New(provider RedisProvider) biz.Cache {
return &cacheStore{data: data, memory: make(map[string]memoryCacheEntry)} return &Store{provider: provider, memory: make(map[string]memoryEntry)}
} }
func (s *cacheStore) client() redis.UniversalClient { func (s *Store) client() redis.UniversalClient { return s.provider.RedisClient() }
return s.data.redis.load()
}
func (s *cacheStore) Get(ctx context.Context, key string) (string, bool, error) { func (s *Store) Get(ctx context.Context, key string) (string, bool, error) {
if client := s.client(); client != nil { if client := s.client(); client != nil {
value, err := client.Get(ctx, key).Result() value, err := client.Get(ctx, key).Result()
if err == nil { if err == nil {
@@ -56,14 +57,14 @@ func (s *cacheStore) Get(ctx context.Context, key string) (string, bool, error)
return entry.value, true, nil return entry.value, true, nil
} }
func (s *cacheStore) Set(ctx context.Context, key, value string, expiration time.Duration) error { func (s *Store) Set(ctx context.Context, key, value string, expiration time.Duration) error {
if client := s.client(); client != nil { if client := s.client(); client != nil {
return client.Set(ctx, key, value, expiration).Err() return client.Set(ctx, key, value, expiration).Err()
} }
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
s.makeMemoryRoom(key) s.makeRoom(key)
entry := memoryCacheEntry{value: value} entry := memoryEntry{value: value}
if expiration > 0 { if expiration > 0 {
entry.expiresAt = time.Now().Add(expiration) entry.expiresAt = time.Now().Add(expiration)
} }
@@ -71,7 +72,7 @@ func (s *cacheStore) Set(ctx context.Context, key, value string, expiration time
return nil return nil
} }
func (s *cacheStore) Delete(ctx context.Context, key string) error { func (s *Store) Delete(ctx context.Context, key string) error {
if client := s.client(); client != nil { if client := s.client(); client != nil {
return client.Del(ctx, key).Err() return client.Del(ctx, key).Err()
} }
@@ -81,7 +82,7 @@ func (s *cacheStore) Delete(ctx context.Context, key string) error {
return nil return nil
} }
func (s *cacheStore) Increment(ctx context.Context, key string, expiration time.Duration) (int64, error) { func (s *Store) Increment(ctx context.Context, key string, expiration time.Duration) (int64, error) {
if client := s.client(); client != nil { if client := s.client(); client != nil {
value, err := client.Incr(ctx, key).Result() value, err := client.Incr(ctx, key).Result()
if err != nil { if err != nil {
@@ -96,7 +97,7 @@ func (s *cacheStore) Increment(ctx context.Context, key string, expiration time.
} }
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
s.makeMemoryRoom(key) s.makeRoom(key)
entry, ok := s.memory[key] entry, ok := s.memory[key]
if ok && !entry.expiresAt.IsZero() && time.Now().After(entry.expiresAt) { if ok && !entry.expiresAt.IsZero() && time.Now().After(entry.expiresAt) {
ok = false ok = false
@@ -114,8 +115,8 @@ func (s *cacheStore) Increment(ctx context.Context, key string, expiration time.
return value, nil return value, nil
} }
func (s *cacheStore) makeMemoryRoom(incoming string) { func (s *Store) makeRoom(incoming string) {
if len(s.memory) < maxMemoryCacheEntries { if len(s.memory) < maxMemoryEntries {
return return
} }
now := time.Now() now := time.Now()
@@ -124,14 +125,12 @@ func (s *cacheStore) makeMemoryRoom(incoming string) {
delete(s.memory, key) delete(s.memory, key)
} }
} }
if len(s.memory) < maxMemoryCacheEntries { if len(s.memory) < maxMemoryEntries {
return return
} }
if _, exists := s.memory[incoming]; exists { if _, exists := s.memory[incoming]; exists {
return return
} }
// The in-memory store is only a Redis fallback. A bounded arbitrary eviction
// is preferable to unbounded growth under IP churn.
for key := range s.memory { for key := range s.memory {
delete(s.memory, key) delete(s.memory, key)
break break
@@ -0,0 +1,68 @@
package storage
import (
"context"
"io"
"sync"
"kra/internal/biz"
"kra/internal/conf"
)
type Reloadable struct {
mu sync.RWMutex
current biz.FileStorage
}
func NewFileStorage(runtime *conf.Runtime) (*Reloadable, error) {
var config *conf.AdminBackend
if runtime != nil {
config = runtime.Admin()
}
return NewReloadable(config)
}
func NewReloadable(config *conf.AdminBackend) (*Reloadable, error) {
current, err := New(config)
if err != nil {
return nil, err
}
return &Reloadable{current: current}, nil
}
func (s *Reloadable) Replace(current biz.FileStorage) {
s.mu.Lock()
s.current = current
s.mu.Unlock()
}
func (s *Reloadable) Put(ctx context.Context, name string, reader io.Reader) (*biz.StoredFile, error) {
s.mu.RLock()
defer s.mu.RUnlock()
return s.current.Put(ctx, name, reader)
}
func (s *Reloadable) Open(ctx context.Context, name string) (io.ReadCloser, error) {
s.mu.RLock()
defer s.mu.RUnlock()
return s.current.Open(ctx, name)
}
func (s *Reloadable) Delete(ctx context.Context, name string) error {
s.mu.RLock()
defer s.mu.RUnlock()
return s.current.Delete(ctx, name)
}
func (s *Reloadable) Compose(ctx context.Context, names []string, destination string) (*biz.StoredFile, string, error) {
s.mu.RLock()
defer s.mu.RUnlock()
return s.current.Compose(ctx, names, destination)
}
func (s *Reloadable) DeletePrefix(ctx context.Context, prefix string) error {
s.mu.RLock()
defer s.mu.RUnlock()
return s.current.DeletePrefix(ctx, prefix)
}
func (s *Reloadable) List(ctx context.Context, prefix, cursor string, limit int) ([]*biz.StoredFile, string, bool, error) {
s.mu.RLock()
defer s.mu.RUnlock()
return s.current.List(ctx, prefix, cursor, limit)
}