优化结构

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

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} }

View File

@ -10,6 +10,7 @@ import (
"strconv"
"kra/internal/conf"
"kra/internal/integration/storage"
"google.golang.org/protobuf/encoding/protojson"
"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)
}
next.Admin.Email = emailConfig
candidateStorage, err := buildFileStorage(next.Admin)
candidateStorage, err := storage.New(next.Admin)
if err != nil {
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)
if d.storage != nil {
d.storage.replace(candidateStorage)
d.storage.Replace(candidateStorage)
}
closeCandidate = false
return nil

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" }

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)
}

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}
}

View File

@ -8,6 +8,7 @@ import (
"testing"
"kra/internal/conf"
"kra/internal/integration/storage"
"google.golang.org/protobuf/encoding/protojson"
"gopkg.in/yaml.v3"
@ -239,7 +240,7 @@ func TestPersistRuntimeConfigReplacesActiveStorage(t *testing.T) {
Local: &conf.AdminBackend_Local{StorePath: oldRoot, PathPrefix: "old-files"},
Storage: &conf.AdminBackend_Storage{Type: "local"},
}
currentStorage, err := buildFileStorage(currentAdmin)
currentStorage, err := storage.NewReloadable(currentAdmin)
if err != nil {
t.Fatal(err)
}
@ -248,7 +249,7 @@ func TestPersistRuntimeConfigReplacesActiveStorage(t *testing.T) {
d := &Data{
runtime: conf.NewRuntime(&conf.Data{}, currentAdmin),
gormDB: reloadableDB,
storage: &reloadableStorage{current: currentStorage},
storage: currentStorage,
}
d.databaseReady.Store(true)

View File

@ -24,5 +24,8 @@ func migrateAll(db *gorm.DB) error {
}
return datasystem.CurrentDataMigration(db)
}},
{ID: "202608200003_payment_admin_surface", Migrate: func(db *gorm.DB) error {
return datasystem.EnsureAdminSurface(db, datapayment.AdminSurface())
}},
})
}

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: "查询支付订单"},
},
}
}

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" }

View File

@ -1,4 +1,4 @@
package data
package payment
import (
"context"
@ -12,14 +12,14 @@ import (
"strings"
"kra/internal/biz"
datapayment "kra/internal/data/payment"
datapayment "kra/internal/integration/payment"
"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 {
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) {
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) {
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) {
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
}
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 {
return errors.New("支付配置必须是 JSON 对象")
}
db := r.data.gormDB.WithContext(ctx)
db := r.data.DB().WithContext(ctx)
var row integrationConfigPO
err := db.Where("kind = ? AND provider = ?", integrationKindPayment, config.Provider).First(&row).Error
if errors.Is(err, gorm.ErrRecordNotFound) {

View File

@ -1,4 +1,4 @@
package data
package payment
import (
"context"

View File

@ -1,4 +1,4 @@
package data
package payment
import "kra/pkg/paymentkit"

View File

@ -1,4 +1,4 @@
package data
package payment
import (
"context"

View File

@ -1,4 +1,4 @@
package data
package payment
import (
"context"
@ -11,6 +11,7 @@ import (
"gorm.io/gorm"
"gorm.io/gorm/clause"
"kra/internal/biz"
"kra/pkg/pagination"
)
type paymentOrderPO struct {
@ -64,9 +65,9 @@ type paymentOrderPO struct {
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}
}
@ -134,7 +135,7 @@ func (r *paymentOrderRepo) CreatePaymentOrder(ctx context.Context, order *biz.Pa
if err != nil {
return nil, false, err
}
db := r.data.gormDB.WithContext(ctx)
db := r.data.DB().WithContext(ctx)
var current paymentOrderPO
err = db.Where("provider = ? AND trade_no = ?", po.Provider, po.TradeNo).First(&current).Error
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) {
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) {
return nil, biz.ErrPaymentOrderNotFound
}
@ -163,6 +164,41 @@ func (r *paymentOrderRepo) FindPaymentOrder(ctx context.Context, provider, trade
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) {
return r.withLockedOrder(ctx, provider, tradeNo, func(tx *gorm.DB, po *paymentOrderPO) error {
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) {
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
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) {

View File

@ -1,4 +1,4 @@
package data
package payment
import (
"context"
@ -91,3 +91,27 @@ func TestPaymentOrderRepositoryRejectsProviderTradeReuse(t *testing.T) {
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)
}
}

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
}

View File

@ -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
}

View File

@ -1,4 +1,4 @@
package data
package system
import (
"context"
@ -6,6 +6,8 @@ import (
"time"
"kra/internal/biz"
"kra/pkg/gormkit"
"kra/pkg/pagination"
"gorm.io/gorm"
)
@ -18,17 +20,17 @@ type announcementPO struct {
Title string
Content string `gorm:"type:text"`
UserID *int `gorm:"column:user_id"`
Attachments jsonPO
Attachments gormkit.JSON
}
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 {
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 {
@ -37,7 +39,7 @@ func announcementToBiz(item announcementPO) *biz.Announcement {
func (r *announcementRepo) Create(ctx context.Context, item *biz.Announcement) error {
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
}
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 {
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 {
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 {
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) {
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 announcementToBiz(po), nil
}
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 {
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
}
if filter.PageSize > 0 {
db = applyPagination(db, filter.Page, filter.PageSize, 100)
db = pagination.Apply(db, filter.Page, filter.PageSize, 100)
}
var pos []announcementPO
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
// 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))
for _, row := range rows {
items = append(items, biz.UserOption{Label: row.Label, Value: row.Value})

View File

@ -1,4 +1,4 @@
package data
package system
import (
"context"

View File

@ -1,4 +1,4 @@
package data
package system
import (
"context"
@ -7,13 +7,14 @@ import (
"time"
"kra/internal/biz"
"kra/pkg/pagination"
"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 {
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 {
po := apiPO{Path: v.Path, Description: v.Description, APIGroup: v.APIGroup, Method: v.Method}
var count int64
if err := r.data.gormDB.WithContext(ctx).Model(&apiPO{}).Where("path = ? AND method = ?", po.Path, po.Method).Count(&count).Error; err != nil {
if err := r.data.DB().WithContext(ctx).Model(&apiPO{}).Where("path = ? AND method = ?", po.Path, po.Method).Count(&count).Error; err != nil {
return err
}
if count > 0 {
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
}
v.ID, v.CreatedAt, v.UpdatedAt, v.Method = po.ID, po.CreatedAt, po.UpdatedAt, po.Method
return nil
}
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
if err := db.First(&old, v.ID).Error; err != nil {
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
}
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
if err := tx.Where("id IN ?", ids).Find(&apis).Error; err != nil {
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) {
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 apiFromPO(po), nil
}
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 {
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 {
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
}
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
query := db.Order(order)
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 {
return nil, 0, err

View File

@ -1,4 +1,4 @@
package data
package system
import (
"context"
@ -15,7 +15,7 @@ import (
func (r *apiRepo) APIRoleIDs(ctx context.Context, path, method string) ([]uint, error) {
rows := make([]casbinRulePO, 0)
err := policyScope(r.data.gormDB.WithContext(ctx)).
err := policyScope(r.data.DB().WithContext(ctx)).
Where("v1 = ? AND v2 = ?", path, method).
Find(&rows).Error
if err != nil {
@ -47,7 +47,7 @@ func (r *apiRepo) SetAPIRoles(ctx context.Context, path, method string, ids []ui
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 {
authorityIDs := make([]string, 0, len(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 {
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) {
rows, err := policyRowsForAuthority(r.data.gormDB.WithContext(ctx), aid)
rows, err := policyRowsForAuthority(r.data.DB().WithContext(ctx), aid)
if err != nil {
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)
}
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 {
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 {
return err
}
db := r.data.gormDB.WithContext(ctx)
db := r.data.DB().WithContext(ctx)
// The reference enforcer removes the old authority policies before it
// attempts to add the replacement set. Keep that ordering visible even
// 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
}
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
}
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
}
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
}
} else {
policies, err := policyRowsForAuthority(r.data.gormDB.WithContext(ctx), actor.AuthorityID)
policies, err := policyRowsForAuthority(r.data.DB().WithContext(ctx), actor.AuthorityID)
if err != nil {
return err
}

View File

@ -1,4 +1,4 @@
package data
package system
import (
"context"

View File

@ -1,4 +1,4 @@
package data
package system
import (
"context"
@ -10,7 +10,7 @@ import (
func (r *apiRepo) IgnoredAPIs(ctx context.Context) ([]*biz.API, error) {
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
}
out := make([]*biz.API, 0, len(pos))
@ -24,12 +24,12 @@ func (r *apiRepo) SetAPIIgnored(ctx context.Context, path, method string, ignore
if ignored {
// The compatible endpoint creates an ignore row on every request (the table has no
// path/method uniqueness constraint); retain that observable behavior.
return r.data.gormDB.WithContext(ctx).Create(&po).Error
return r.data.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 {
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 {
pos := make([]apiPO, 0, len(added))
for _, item := range added {

View File

@ -1,4 +1,4 @@
package data
package system
import (
"context"
@ -6,13 +6,14 @@ import (
"time"
"kra/internal/biz"
"kra/pkg/pagination"
"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 {
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) {
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) {
return nil, false, errors.New("用户不存在")
}
return nil, false, err
}
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").
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
@ -60,7 +61,7 @@ func (r *apiTokenRepo) UserHasAuthority(ctx context.Context, userID, authorityID
}
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}
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
}
v.ID = po.ID
@ -68,7 +69,7 @@ func (r *apiTokenRepo) CreateAPIToken(ctx context.Context, v *biz.APIToken) erro
return nil
}
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 {
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
}
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
}
userIDs := make([]uint, 0, len(pos))
@ -89,7 +90,7 @@ func (r *apiTokenRepo) ListAPITokens(ctx context.Context, page, size int, userID
}
var userPOs []userPO
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
}
}
@ -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) {
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 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 {
// 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
// result here.
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
}
if err := r.BlacklistToken(ctx, po.Token); err != nil {
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 {
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) {
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
}
// The JWT middleware consults only jwt_blacklists and lets JWT parsing

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} }

View File

@ -1,4 +1,4 @@
package data
package system
import (
"context"

View File

@ -1,4 +1,4 @@
package data
package system
import (
"context"
@ -9,9 +9,11 @@ import (
"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}
}
@ -21,11 +23,11 @@ func (r *authorityAccessRepo) strictAuthorityIDs(ctx context.Context, actorID ui
return allowed, nil
}
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
}
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
}
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) {
config := r.data.runtime.Admin()
config := r.data.Runtime().Admin()
if config == nil || config.System == nil || !config.System.UseStrictAuth {
return biz.Actor{}, nil, false, nil
}
@ -118,7 +120,7 @@ func (r *authorityAccessRepo) ensureAuthorityParentAcyclic(ctx context.Context,
}
visited[current] = struct{}{}
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
}
if authority.ParentID == nil {
@ -148,7 +150,7 @@ func (r *authorityAccessRepo) CreateAuthority(ctx context.Context, value *biz.Au
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
// ErrRecordNotFound as a duplicate-role error. Keep that precedence so
// 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 {
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
// hierarchy/Casbin checks that happen later in UpdateCasbin.
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 {
return err
}
db := r.data.gormDB.WithContext(ctx)
db := r.data.DB().WithContext(ctx)
var current authorityPO
if err := db.Where("authority_id = ?", value.AuthorityID).First(&current).Error; err != nil {
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 {
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
if err := tx.Where("authority_id = ?", id).First(&authority).Error; err != nil {
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) {
db := r.data.gormDB.WithContext(ctx)
db := r.data.DB().WithContext(ctx)
var allowed map[uint]bool
config := r.data.runtime.Admin()
config := r.data.Runtime().Admin()
if actor, ok := biz.ActorFromContext(ctx); ok {
// The current authority is loaded even when strict mode is disabled;
// 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] {
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
if err := tx.Model(&userAuthorityPO{}).Where("sys_authority_authority_id = ?", id).Pluck("sys_user_id", &oldIDs).Error; err != nil {
return err
@ -609,7 +611,7 @@ func (r *authorityAccessRepo) checkUserIDAuth(ctx context.Context, id uint, allo
if allowSelf && actor.UserID != 0 && actor.UserID == id {
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 {
@ -621,7 +623,7 @@ func (r *authorityAccessRepo) checkDepartmentIDsAuth(ctx context.Context, ids []
return nil
}
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
}
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) {
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
}
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 {
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 {
return err
}
@ -693,7 +695,7 @@ func (r *authorityAccessRepo) checkDataScopeGrant(ctx context.Context, requested
}
if len(requestedDepartments) > 0 {
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
}
if count != int64(len(requestedDepartments)) {
@ -705,7 +707,7 @@ func (r *authorityAccessRepo) checkDataScopeGrant(ctx context.Context, requested
return nil
}
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
}
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) {
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
}
func (r *authorityAccessRepo) ResolveDataScope(ctx context.Context, authorityID, userID uint) (biz.DataScope, error) {
identity := biz.DataScope{UserID: userID, AuthorityID: authorityID}
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
}
identity.PrimaryDeptID = user.DeptID
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
}
identity.Scope = authority.DataScope
@ -772,7 +774,7 @@ func (r *authorityAccessRepo) ResolveDataScope(ctx context.Context, authorityID,
var ids []uint
selected := make(map[uint]bool)
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
}
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
// ancestors string (which may be stale after a department move).
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
}
children := make(map[uint][]uint, len(departments))

View File

@ -1,4 +1,4 @@
package data
package system
import (
"context"

View File

@ -10,6 +10,31 @@ type IgnoredAPI struct {
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 {
staticRoute := "/" + strings.Trim(staticPath, "/") + "/*filepath"
return []IgnoredAPI{

View File

@ -1,4 +1,4 @@
package data
package system
import (
"strconv"

View File

@ -1,10 +1,11 @@
package data
package system
import (
"context"
"time"
"kra/internal/biz"
"kra/pkg/pagination"
"gorm.io/gorm"
)
@ -23,13 +24,13 @@ type dataAccessLogPO struct {
func (dataAccessLogPO) TableName() string { return "sys_data_access_logs" }
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 {
return &biz.DataAccessLog{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, EventType: v.EventType, TargetTable: v.TargetTable, Operation: v.Operation, UserID: v.UserID, AuthorityID: v.AuthorityID, Scope: v.Scope, RequestID: v.RequestID, Method: v.Method, Path: v.Path, Detail: v.Detail}
}
func (r *auditQueryRepo) ListDataAccess(ctx context.Context, page, size int, q *biz.DataAccessLog) ([]*biz.DataAccessLog, int64, error) {
db := r.data.gormDB.WithContext(ctx).Model(&dataAccessLogPO{})
db := r.data.DB().WithContext(ctx).Model(&dataAccessLogPO{})
if q != nil {
if 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
}
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
}
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
}
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
}

View File

@ -1,4 +1,4 @@
package data
package system
import (
"context"
@ -12,9 +12,9 @@ import (
"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 {
ID uint `gorm:"primaryKey"`
@ -62,7 +62,7 @@ func (r *departmentRepo) attachDepartmentLeaders(ctx context.Context, department
return nil
}
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
}
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"
if v.ParentID != 0 {
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) {
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), ",")
}
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 {
if v.ParentID == v.ID {
@ -95,7 +95,7 @@ func (r *departmentRepo) UpdateDepartment(ctx context.Context, v *biz.Department
v.Ancestors = "0"
if v.ParentID != 0 {
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) {
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), ",")
}
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 {
if id == 0 {
return errors.New("部门ID不能为空")
}
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
}
if count > 0 {
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
}
if count > 0 {
return errors.New("该部门下存在用户,不允许删除")
}
if err := r.data.gormDB.WithContext(ctx).Model(&userDepartmentPO{}).Where("sys_department_id = ?", id).Count(&count).Error; err != nil {
if err := r.data.DB().WithContext(ctx).Model(&userDepartmentPO{}).Where("sys_department_id = ?", id).Count(&count).Error; err != nil {
return err
}
if count > 0 {
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) {
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
}
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) {
var pos []departmentPO
db := r.data.gormDB.WithContext(ctx).Order("sort")
db := r.data.DB().WithContext(ctx).Order("sort")
if 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) {
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
}
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 {
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
if err := tx.Model(&userDepartmentPO{}).Where("sys_department_id = ?", id).Pluck("sys_user_id", &current).Error; err != nil {
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 {
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
if err := tx.First(&user, uid).Error; err != nil {
return errors.New("查询用户数据失败")

View File

@ -1,4 +1,4 @@
package data
package system
import (
"context"
@ -8,13 +8,14 @@ import (
"time"
"kra/internal/biz"
"kra/pkg/pagination"
"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 {
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 {
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不允许创建")
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
po := dictionaryPO{Name: v.Name, Type: v.Type, Status: v.Status, Desc: v.Desc, ParentID: v.ParentID}
if err := r.data.gormDB.WithContext(ctx).Create(&po).Error; err != nil {
if err := r.data.DB().WithContext(ctx).Create(&po).Error; err != nil {
return err
}
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 {
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 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}
if err := tx.Create(&po).Error; err != nil {
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 {
db := r.data.gormDB.WithContext(ctx)
db := r.data.DB().WithContext(ctx)
var current dictionaryPO
if err := db.Where("id = ?", v.ID).First(&current).Error; err != nil {
return errors.New("查询字典数据失败")
@ -148,7 +149,7 @@ func (r *dictionaryRepo) checkDictionaryCircularReference(ctx context.Context, c
return errors.New("不能将字典设置为自己的父级")
}
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) {
return nil
}
@ -161,7 +162,7 @@ func (r *dictionaryRepo) checkDictionaryCircularReference(ctx context.Context, c
return nil
}
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
if err := db.First(&dictionary, id).Error; err != nil {
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) {
var po dictionaryPO
db := r.data.gormDB.WithContext(ctx)
db := r.data.DB().WithContext(ctx)
active := true
if status != nil {
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) {
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
}
value := dictionaryFromPO(po)
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
}
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
}
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 != "" {
like := "%" + name + "%"
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)
if !details && len(parentIDs) > 0 {
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
}
for _, child := range children {
@ -257,7 +258,7 @@ func (r *dictionaryRepo) ListDictionaries(ctx context.Context, page, size int, n
detailsByDictionary := make(map[uint][]dictionaryDetailPO)
if details && len(dictionaryIDs) > 0 {
var values []dictionaryDetailPO
if err := r.data.gormDB.WithContext(ctx).Where("sys_dictionary_id IN ?", dictionaryIDs).Order("sort").Find(&values).Error; err != nil {
if err := r.data.DB().WithContext(ctx).Where("sys_dictionary_id IN ?", dictionaryIDs).Order("sort").Find(&values).Error; err != nil {
return nil, 0, err
}
for _, value := range values {
@ -289,13 +290,13 @@ func (r *dictionaryRepo) CreateDictionaryDetail(ctx context.Context, v *biz.Dict
po.Path = ""
if v.ParentID != nil {
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
}
po.Level = parent.Level + 1
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
}
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 {
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 {
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 {
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
}
if count > 0 {
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) {
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 detailFromPO(po), nil
}
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 {
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
}
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
}
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
// be enabled.
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
}
dictionaryID = dictionary.ID
}
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
}
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) {
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 {
db = db.Where("parent_id IS NULL")
} else {
@ -451,7 +452,7 @@ func (r *dictionaryRepo) DictionaryDetailsByParent(ctx context.Context, dictiona
func (r *dictionaryRepo) loadDictionaryDetailChildren(ctx context.Context, parent *biz.DictionaryDetail) error {
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
}
parent.Children = make([]*biz.DictionaryDetail, 0, len(pos))

View File

@ -1,4 +1,4 @@
package data
package system
import (
"context"

View File

@ -1,10 +1,11 @@
package data
package system
import (
"context"
"time"
"kra/internal/biz"
"kra/pkg/pagination"
"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}
}
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.
return nil
}
if 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 {
updates := make(map[string]any, 9)
@ -67,20 +68,20 @@ func (r *auditQueryRepo) UpdateError(ctx context.Context, v *biz.ErrorRecord) er
if 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 {
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) {
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 errorFromPO(po), nil
}
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 len(q.CreatedAtRange) == 2 {
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
}
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
}
out := make([]*biz.ErrorRecord, 0, len(pos))

View File

@ -1,4 +1,4 @@
package data
package system
import (
"context"

View File

@ -1,4 +1,4 @@
package data
package system
import (
"context"
@ -57,9 +57,9 @@ type exportJoinPO struct {
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 {
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 {
@ -138,7 +138,7 @@ func (r *exportRepo) saveRelations(tx *gorm.DB, v *biz.ExportTemplate, resetIDs,
return nil
}
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)
if err := tx.Create(&po).Error; err != nil {
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 {
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)
if err := tx.Model(&exportTemplatePO{}).Where("id = ?", v.ID).Updates(&po).Error; err != nil {
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 {
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) {
var po exportTemplatePO
db := r.data.gormDB.WithContext(ctx)
db := r.data.DB().WithContext(ctx)
var err error
if tid != "" {
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
}
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.StartCreatedAt != nil && q.EndCreatedAt != nil {
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) {
selected, err := r.data.database(t.DBName)
selected, err := r.data.Database(t.DBName)
if err != nil {
return nil, "", err
}
@ -355,7 +355,7 @@ func (r *exportRepo) PreviewExport(ctx context.Context, t *biz.ExportTemplate, p
wheres := make([]string, 0)
if params["filterDeleted"] == "true" {
wheres = append(wheres, fmt.Sprintf("%s.deleted_at IS NULL", t.TableName))
selected, err := r.data.database(t.DBName)
selected, err := r.data.Database(t.DBName)
if err != nil {
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
// native error instead of silently falling back to GORM insertion.
sql := t.ImportSQL
selected, err := r.data.database(t.DBName)
selected, err := r.data.Database(t.DBName)
if err != nil {
return err
}

View File

@ -1,4 +1,4 @@
package data
package system
import (
"bytes"
@ -24,7 +24,7 @@ const (
)
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) == "" {
return "", false, biz.ErrLogRootUnavailable
}

View File

@ -1,4 +1,4 @@
package data
package system
import (
"context"

View File

@ -1,10 +1,11 @@
package data
package system
import (
"context"
"time"
"kra/internal/biz"
"kra/pkg/pagination"
"gorm.io/gorm"
)
@ -23,18 +24,18 @@ type loginLogPO struct {
func (loginLogPO) TableName() string { return "sys_login_logs" }
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;
// skip the audit write until storage is ready.
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 {
return &biz.LoginLog{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, Username: v.Username, IP: v.IP, Status: v.Status, ErrorMessage: v.ErrorMessage, Agent: v.Agent, UserID: v.UserID}
}
func (r *auditQueryRepo) ListLogins(ctx context.Context, page, size int, q *biz.LoginLog) ([]*biz.LoginLog, int64, error) {
db := r.data.gormDB.WithContext(ctx).Model(&loginLogPO{})
db := r.data.DB().WithContext(ctx).Model(&loginLogPO{})
if q != nil {
if 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
}
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
}
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)
}
}
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))
for _, po := range pos {
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) {
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 loginFromPO(po), nil
}
func (r *auditQueryRepo) DeleteLogins(ctx context.Context, ids []int) error {
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
}

View File

@ -1,4 +1,4 @@
package data
package system
import (
"context"
@ -39,9 +39,9 @@ type categoryPO struct {
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 {
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 {
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
}
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) {
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 mediaFromPO(po), nil
}
func (r *mediaRepo) FindMediaByHash(ctx context.Context, userID uint, hash string) (*biz.MediaFile, error) {
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 mediaFromPO(po), nil
}
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 != "" {
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
// not-found error instead of silently succeeding with RowsAffected=0.
var po mediaPO
if err := r.data.gormDB.WithContext(ctx).Where("id = ?", id).First(&po).Error; err != nil {
if err := r.data.DB().WithContext(ctx).Where("id = ?", id).First(&po).Error; err != nil {
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 {
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) {
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
}
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 {
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 {
db := r.data.gormDB.WithContext(ctx)
db := r.data.DB().WithContext(ctx)
var count int64
query := db.Model(&categoryPO{}).Where("name = ? AND pid = ?", v.Name, v.ParentID)
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 {
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
}
if count > 0 {
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) {
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
}
byID := map[uint]*biz.AttachmentCategory{}

View File

@ -1,4 +1,4 @@
package data
package system
import (
"context"

View File

@ -1,4 +1,4 @@
package data
package system
import (
"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) {
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) {
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) {
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) {
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 {
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
}
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) {
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 uploadFromPO(po), nil
}
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
}
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 {
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 {
// The compatible flow uses GORM's normal Delete here, retaining the soft-deleted session
// for audit/recovery rather than physically removing it.
return r.data.gormDB.WithContext(ctx).Delete(&uploadSessionPO{}, id).Error
return r.data.DB().WithContext(ctx).Delete(&uploadSessionPO{}, id).Error
}
func (r *mediaRepo) UpsertChunk(ctx context.Context, uploadID uint, v *biz.UploadChunk) error {
po := uploadChunkPO{UploadID: uploadID, ChunkIndex: v.Index, ChunkHash: v.Hash, Size: v.Size}
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"}},
DoUpdates: clause.AssignmentColumns([]string{"chunk_hash", "size", "updated_at", "deleted_at"}),
}).Create(&po).Error
}
func (r *mediaRepo) ListChunks(ctx context.Context, uploadID uint) ([]*biz.UploadChunk, error) {
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
}
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
}
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) {
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
}
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 {
return err
}

View File

@ -1,4 +1,4 @@
package data
package system
import (
"context"
@ -10,9 +10,9 @@ import (
"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 {
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 {
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
if err := tx.Model(&menuPO{}).Where("name = ?", v.Name).Count(&count).Error; err != nil {
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 {
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
if err := tx.First(&old, v.ID).Error; err != nil {
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 {
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
if err := tx.Model(&menuPO{}).Where("parent_id = ?", id).Count(&count).Error; err != nil {
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 {
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
}
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))
}
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
}
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) {
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
}
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) {
var pos []menuPO
db := r.data.gormDB.WithContext(ctx).Model(&menuPO{}).Order("sort")
config := r.data.runtime.Admin()
db := r.data.DB().WithContext(ctx).Model(&menuPO{}).Order("sort")
config := r.data.Runtime().Admin()
if actor, ok := biz.ActorFromContext(ctx); ok && config != nil && config.System != nil && config.System.UseStrictAuth {
var authority authorityPO
if err := r.data.gormDB.WithContext(ctx).Where("authority_id = ?", actor.AuthorityID).First(&authority).Error; err != nil {
if err := r.data.DB().WithContext(ctx).Where("authority_id = ?", actor.AuthorityID).First(&authority).Error; err != nil {
return nil, err
}
if authority.ParentID != nil && *authority.ParentID != 0 {
db = db.Where("id IN (?)", r.data.gormDB.WithContext(ctx).Model(&authorityMenuPO{}).Select("sys_base_menu_id").Where("sys_authority_authority_id = ?", actor.AuthorityID))
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 {
@ -220,7 +220,7 @@ func (r *menuRepo) List(ctx context.Context) ([]*biz.Menu, error) {
menu.Buttons = []*biz.MenuButton{}
}
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
}
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))
}
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
}
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 {
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
}
if err := r.checkMenuAssignmentAuth(ctx, ids); err != nil {
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 {
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) {
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
}
@ -294,8 +294,8 @@ func (r *menuRepo) AuthorityMenuIDs(ctx context.Context, id uint) ([]uint, error
// menus when an administrator inspects a different (child) authority.
func (r *menuRepo) ListAuthorityMenus(ctx context.Context, authorityID uint) ([]*biz.Menu, error) {
var pos []menuPO
db := r.data.gormDB.WithContext(ctx).Model(&menuPO{}).
Where("id IN (?)", r.data.gormDB.WithContext(ctx).Model(&authorityMenuPO{}).
db := r.data.DB().WithContext(ctx).Model(&menuPO{}).
Where("id IN (?)", r.data.DB().WithContext(ctx).Model(&authorityMenuPO{}).
Select("sys_base_menu_id").Where("sys_authority_authority_id = ?", authorityID)).
Order("sort")
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) {
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
}
func (r *menuRepo) DefaultRouterRoleIDs(ctx context.Context, id uint) ([]uint, error) {
var menu menuPO
if err := r.data.gormDB.WithContext(ctx).First(&menu, id).Error; err != nil {
if err := r.data.DB().WithContext(ctx).First(&menu, id).Error; err != nil {
return nil, err
}
var ids []uint
err := r.data.gormDB.WithContext(ctx).Model(&authorityPO{}).Where("default_router = ?", menu.Name).Pluck("authority_id", &ids).Error
err := r.data.DB().WithContext(ctx).Model(&authorityPO{}).Where("default_router = ?", menu.Name).Pluck("authority_id", &ids).Error
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 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 {
@ -376,7 +376,7 @@ func (r *menuRepo) checkMenuAssignmentAuth(ctx context.Context, menuIDs []uint)
if err != nil || !strict {
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 {

View File

@ -1,4 +1,4 @@
package data
package system
import (
"context"

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
}

View File

@ -1,9 +1,117 @@
package data
package system
import (
"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) {
db, err := openWithDriver("sqlite", "file:"+t.Name()+"?mode=memory&cache=shared")
if err != nil {

View File

@ -1,4 +1,4 @@
package data
package system
import (
"time"
@ -96,9 +96,6 @@ type menuParameterPO struct {
func (menuParameterPO) TableName() string { return "sys_base_menu_parameters" }
type userRepo struct{ data *Data }
type initializationRepo struct{ data *Data }
type userRepo struct{ data Provider }
func NewUserRepo(data *Data) biz.UserRepo { return &userRepo{data: data} }
func NewInitializationRepo(data *Data) biz.InitializationRepo { return &initializationRepo{data: data} }
func NewUserRepo(data Provider) biz.UserRepo { return &userRepo{data: data} }

View File

@ -1,10 +1,11 @@
package data
package system
import (
"context"
"time"
"kra/internal/biz"
"kra/pkg/pagination"
"gorm.io/gorm"
)
@ -30,13 +31,13 @@ type operationPO struct {
func (operationPO) TableName() string { return "sys_operation_records" }
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 {
return &biz.OperationRecord{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, IP: v.IP, Method: v.Method, Path: v.Path, Status: v.Status, LatencyMS: v.LatencyMS, Agent: v.Agent, ErrorMessage: v.ErrorMessage, Body: v.Body, Response: v.Response, UserID: v.UserID, RequestID: v.RequestID, TraceID: v.TraceID, DeviceID: v.DeviceID}
}
func (r *auditQueryRepo) ListOperations(ctx context.Context, page, size int, q *biz.OperationRecord) ([]*biz.OperationRecord, int64, error) {
db := r.data.gormDB.WithContext(ctx).Model(&operationPO{})
db := r.data.DB().WithContext(ctx).Model(&operationPO{})
if q != nil {
if 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
}
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
}
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))
for _, po := range pos {
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) {
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 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 {
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
}

View File

@ -1,4 +1,4 @@
package data
package system
import (
"context"

View File

@ -1,18 +1,19 @@
package data
package system
import (
"context"
"time"
"kra/internal/biz"
"kra/pkg/pagination"
"gorm.io/gorm"
"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 {
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 {
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
}
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 {
// The compatible update uses a struct, so zero-value optional fields (notably desc)
// are intentionally ignored rather than clearing an existing value.
return r.data.gormDB.WithContext(ctx).Model(&parameterPO{}).Where("id = ?", v.ID).Updates(&parameterPO{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 {
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) {
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 parameterFromPO(po), nil
}
func (r *parameterRepo) FindParameterByKey(ctx context.Context, key string) (*biz.SystemParameter, error) {
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 parameterFromPO(po), nil
}
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.StartCreatedAt != nil && q.EndCreatedAt != nil {
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
}
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
}
out := make([]*biz.SystemParameter, 0, len(pos))

View File

@ -1,4 +1,4 @@
package data
package system
import (
"context"

View File

@ -1,4 +1,4 @@
package data
package system
import (
"context"
@ -10,9 +10,9 @@ import (
"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 {
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) {
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
}
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 {
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 {
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) {
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
}
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 {
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 {
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) {
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) {
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) {
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
}
@ -122,7 +122,7 @@ func (r *permissionRepo) checkButtonAssignmentAuth(ctx context.Context, buttons
if err != nil || !strict {
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 {

View File

@ -1,4 +1,4 @@
package data
package system
import (
"context"

View File

@ -1,4 +1,4 @@
package data
package system
import (
"context"
@ -10,9 +10,9 @@ import (
"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 {
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}
}
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 {
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 {
if id == 0 {
return errors.New("岗位ID不能为空")
}
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
}
if count > 0 {
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) {
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 posFromPO(po), nil
}
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.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) {
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
}
func (r *positionRepo) SetPositionUsers(ctx context.Context, id uint, ids []uint) error {
if id == 0 {
return errors.New("岗位ID不能为空")
}
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
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 {
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 {
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 {
return err
}

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
}

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)}
}

View File

@ -1,4 +1,4 @@
package data
package system
import "testing"
@ -8,7 +8,7 @@ func TestDefaultIgnoredAPIsIncludeSwagger(t *testing.T) {
"GET /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
if _, ok := wants[key]; ok {
wants[key] = true

View File

@ -1,8 +1,10 @@
package data
package system
import (
"context"
"kra/internal/biz"
"kra/pkg/gormkit"
"kra/pkg/pagination"
"time"
"gorm.io/gorm"
@ -17,10 +19,10 @@ type taskPO struct {
Description, Spec string
WithSeconds bool
ExecutorType, MethodName string
Params jsonPO
Params gormkit.JSON
HTTPURL string
HTTPMethod string
HTTPHeader jsonPO
HTTPHeader gormkit.JSON
HTTPBody string `gorm:"type:text"`
HTTPAllowPrivate, Enabled bool
}
@ -42,18 +44,18 @@ type taskLogPO struct {
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 {
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 {
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 {
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
}
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) {
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 {
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 {
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 {
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) {
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 taskFromPO(po), nil
}
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.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
}
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
}
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
}
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 {
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 {
return &biz.TimedTaskLog{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, TaskID: v.TaskID, TaskName: v.TaskName, TriggerType: v.TriggerType, StartedAt: v.StartedAt, FinishedAt: v.FinishedAt, DurationMS: v.DurationMS, Status: v.Status, ErrorMsg: v.ErrorMsg, Output: v.Output}
}
func (r *taskRepo) ListTaskLogs(ctx context.Context, page, size int, taskID uint, status string) ([]*biz.TimedTaskLog, int64, error) {
db := r.data.gormDB.WithContext(ctx).Model(&taskLogPO{})
db := r.data.DB().WithContext(ctx).Model(&taskLogPO{})
if taskID != 0 {
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
}
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
}
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 {
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 {
return err
}

View File

@ -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},
})
}

View File

@ -1,4 +1,4 @@
package data
package system
import (
"context"

View File

@ -1,4 +1,4 @@
package data
package system
import (
"context"
@ -9,6 +9,7 @@ import (
"time"
"kra/internal/biz"
"kra/pkg/pagination"
"github.com/google/uuid"
"gorm.io/gorm"
@ -17,7 +18,7 @@ import (
func (r *userRepo) FindUserByUsername(ctx context.Context, username string) (*biz.User, error) {
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) {
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) {
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) {
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) {
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) {
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) {
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
}
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
}
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)
}
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
}
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
if po.DeptID != 0 {
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)
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
}
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
}
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 {
return []*biz.User{}, nil
}
db := r.data.gormDB.WithContext(ctx)
db := r.data.DB().WithContext(ctx)
userIDs := make([]uint, 0, len(pos))
authorityIDs := 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) {
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").
Where("sys_authority_menus.sys_authority_authority_id = ? AND sys_base_menus.name = ?", authorityID, name).
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) {
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").
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
@ -247,7 +248,7 @@ func (r *userRepo) MenusByAuthority(ctx context.Context, authorityID uint) ([]*b
}
if len(menuIDs) > 0 {
var parameters []menuParameterPO
if err := r.data.gormDB.WithContext(ctx).Where("sys_base_menu_id IN ?", menuIDs).Find(&parameters).Error; err != nil {
if err := r.data.DB().WithContext(ctx).Where("sys_base_menu_id IN ?", menuIDs).Find(&parameters).Error; err != nil {
return nil, err
}
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))
}
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
}
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) {
db := r.data.gormDB.WithContext(ctx).Model(&userPO{})
db := r.data.DB().WithContext(ctx).Model(&userPO{})
order := "id desc"
if filter != nil {
if filter.Username != "" {
@ -306,7 +307,7 @@ func (r *userRepo) ListUsers(ctx context.Context, page, pageSize int, filter *bi
return nil, 0, err
}
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
}
users, err := r.loadUsers(ctx, pos)
@ -337,7 +338,7 @@ func (r *userRepo) CreateUserWithAuthorities(ctx context.Context, user *biz.User
now := time.Now()
po := userPO{UUID: user.UUID, Username: user.Username, Password: user.Password, NickName: user.NickName, HeaderImg: user.HeaderImg, AuthorityID: user.AuthorityID, Phone: user.Phone, Email: user.Email, Enable: user.Enable, PasswordUpdatedAt: &now, MustChangePassword: user.MustChangePassword}
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
// account. The username column is intentionally not unique for legacy
// 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 {
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 {
@ -418,7 +419,7 @@ func (r *userRepo) UpdateSelfUser(ctx context.Context, user *biz.User) error {
if len(updates) == 0 {
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 {
@ -441,7 +442,7 @@ func (r *userRepo) FillDepartmentNamePaths(ctx context.Context, user *biz.User)
nameByID := make(map[uint]string, len(ids))
if len(ids) > 0 {
var rows []departmentPO
if err := r.data.gormDB.WithContext(ctx).Select("id", "name").Where("id IN ?", ids).Find(&rows).Error; err != nil {
if err := r.data.DB().WithContext(ctx).Select("id", "name").Where("id IN ?", ids).Find(&rows).Error; err != nil {
return err
}
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 {
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 {
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 {
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 {
return err
}
@ -517,11 +518,11 @@ func (r *userRepo) UpdatePassword(ctx context.Context, id uint, password string,
if clearMustChange {
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) {
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
}
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 {
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
if err := tx.Where("id = ?", id).First(&user).Error; err != nil {
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 {
db := r.data.gormDB.WithContext(ctx)
db := r.data.DB().WithContext(ctx)
var count int64
if err := db.Model(&userAuthorityPO{}).Where("sys_user_id = ? AND sys_authority_authority_id = ?", id, authorityID).Count(&count).Error; err != nil {
return err
@ -597,7 +598,7 @@ func (r *userRepo) SetUserSetting(ctx context.Context, id uint, setting map[stri
if err != nil {
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

View File

@ -1,4 +1,4 @@
package data
package system
import (
"context"

View File

@ -1,4 +1,4 @@
package data
package system
import (
"context"
@ -7,6 +7,7 @@ import (
"gorm.io/gorm"
"kra/internal/biz"
"kra/pkg/pagination"
)
type versionPO struct {
@ -22,32 +23,32 @@ type versionPO struct {
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 {
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 {
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
}
v.ID = po.ID
return nil
}
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) {
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 versionFromPO(po), nil
}
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 {
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
}
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
}
out := make([]*biz.Version, 0, len(pos))
@ -75,7 +76,7 @@ func (r *versionRepo) BuildVersionBundle(ctx context.Context, menuIDs, apiIDs, d
bundle := &biz.VersionBundle{}
var menus []menuPO
if len(menuIDs) > 0 {
if err := r.data.gormDB.WithContext(ctx).Where("id IN ?", menuIDs).Order("sort,id").Find(&menus).Error; err != nil {
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}
}
}
@ -86,7 +87,7 @@ func (r *versionRepo) BuildVersionBundle(ctx context.Context, menuIDs, apiIDs, d
}
if len(menuIDs) > 0 {
var buttons []menuButtonPO
if err := r.data.gormDB.WithContext(ctx).Where("sys_base_menu_id IN ?", menuIDs).Find(&buttons).Error; err != nil {
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}
}
for _, button := range buttons {
@ -95,7 +96,7 @@ func (r *versionRepo) BuildVersionBundle(ctx context.Context, menuIDs, apiIDs, d
}
}
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}
}
for _, parameter := range parameters {
@ -142,7 +143,7 @@ func (r *versionRepo) BuildVersionBundle(ctx context.Context, menuIDs, apiIDs, d
}
var apis []apiPO
if len(apiIDs) > 0 {
if err := r.data.gormDB.WithContext(ctx).Where("id IN ?", apiIDs).Find(&apis).Error; err != nil {
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}
}
}
@ -153,7 +154,7 @@ func (r *versionRepo) BuildVersionBundle(ctx context.Context, menuIDs, apiIDs, d
}
var dictionaries []dictionaryPO
if len(dictIDs) > 0 {
if err := r.data.gormDB.WithContext(ctx).Where("id IN ?", dictIDs).Find(&dictionaries).Error; err != nil {
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}
}
}
@ -178,7 +179,7 @@ func (r *versionRepo) BuildVersionBundle(ctx context.Context, menuIDs, apiIDs, d
return bundle, nil
}
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 err := db.Transaction(func(tx *gorm.DB) error { return importVersionMenus(tx, b.Menus, 0) }); err != nil {
return &biz.VersionStageError{Stage: biz.VersionStageMenus, Err: err}

View File

@ -1,4 +1,4 @@
package data
package system
import (
"context"

View File

@ -1,4 +1,4 @@
package data
package cache
import (
"context"
@ -6,33 +6,34 @@ import (
"sync"
"time"
"kra/internal/biz"
"github.com/redis/go-redis/v9"
"kra/internal/biz"
)
type memoryCacheEntry struct {
type RedisProvider interface {
RedisClient() redis.UniversalClient
}
type memoryEntry struct {
value string
expiresAt time.Time
}
type cacheStore struct {
data *Data
mu sync.Mutex
memory map[string]memoryCacheEntry
type Store struct {
provider RedisProvider
mu sync.Mutex
memory map[string]memoryEntry
}
const maxMemoryCacheEntries = 10000
const maxMemoryEntries = 10000
func NewCache(data *Data) biz.Cache {
return &cacheStore{data: data, memory: make(map[string]memoryCacheEntry)}
func New(provider RedisProvider) biz.Cache {
return &Store{provider: provider, memory: make(map[string]memoryEntry)}
}
func (s *cacheStore) client() redis.UniversalClient {
return s.data.redis.load()
}
func (s *Store) client() redis.UniversalClient { return s.provider.RedisClient() }
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 {
value, err := client.Get(ctx, key).Result()
if err == nil {
@ -56,14 +57,14 @@ func (s *cacheStore) Get(ctx context.Context, key string) (string, bool, error)
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 {
return client.Set(ctx, key, value, expiration).Err()
}
s.mu.Lock()
defer s.mu.Unlock()
s.makeMemoryRoom(key)
entry := memoryCacheEntry{value: value}
s.makeRoom(key)
entry := memoryEntry{value: value}
if expiration > 0 {
entry.expiresAt = time.Now().Add(expiration)
}
@ -71,7 +72,7 @@ func (s *cacheStore) Set(ctx context.Context, key, value string, expiration time
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 {
return client.Del(ctx, key).Err()
}
@ -81,7 +82,7 @@ func (s *cacheStore) Delete(ctx context.Context, key string) error {
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 {
value, err := client.Incr(ctx, key).Result()
if err != nil {
@ -96,7 +97,7 @@ func (s *cacheStore) Increment(ctx context.Context, key string, expiration time.
}
s.mu.Lock()
defer s.mu.Unlock()
s.makeMemoryRoom(key)
s.makeRoom(key)
entry, ok := s.memory[key]
if ok && !entry.expiresAt.IsZero() && time.Now().After(entry.expiresAt) {
ok = false
@ -114,8 +115,8 @@ func (s *cacheStore) Increment(ctx context.Context, key string, expiration time.
return value, nil
}
func (s *cacheStore) makeMemoryRoom(incoming string) {
if len(s.memory) < maxMemoryCacheEntries {
func (s *Store) makeRoom(incoming string) {
if len(s.memory) < maxMemoryEntries {
return
}
now := time.Now()
@ -124,14 +125,12 @@ func (s *cacheStore) makeMemoryRoom(incoming string) {
delete(s.memory, key)
}
}
if len(s.memory) < maxMemoryCacheEntries {
if len(s.memory) < maxMemoryEntries {
return
}
if _, exists := s.memory[incoming]; exists {
return
}
// The in-memory store is only a Redis fallback. A bounded arbitrary eviction
// is preferable to unbounded growth under IP churn.
for key := range s.memory {
delete(s.memory, key)
break

View File

@ -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)
}