优化结构
This commit is contained in:
@@ -135,14 +135,26 @@ type mediaUploadRepo struct {
|
||||
chunks []*UploadChunk
|
||||
claimed bool
|
||||
failed bool
|
||||
err error
|
||||
}
|
||||
|
||||
func (r *mediaUploadRepo) FindUploadSession(context.Context, uint) (*UploadSession, error) {
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
}
|
||||
if r.session == nil {
|
||||
return nil, ErrUploadSessionNotFound
|
||||
}
|
||||
return r.session, nil
|
||||
}
|
||||
|
||||
func TestSaveChunkPreservesUploadRepositoryErrors(t *testing.T) {
|
||||
want := errors.New("database unavailable")
|
||||
uc := NewMediaUsecase(&mediaUploadRepo{err: want}, &mediaLimitStorage{}, mediaLimitSettings{max: 10})
|
||||
if err := uc.SaveChunk(context.Background(), 1, 7, 0, md5Text("abc"), strings.NewReader("abc")); !errors.Is(err, want) {
|
||||
t.Fatalf("SaveChunk() error = %v, want %v", err, want)
|
||||
}
|
||||
}
|
||||
func (r *mediaUploadRepo) ClaimUploadSession(context.Context, uint) (bool, error) {
|
||||
return r.claimed, nil
|
||||
}
|
||||
|
||||
@@ -58,6 +58,17 @@ func expectedChunkSize(session *UploadSession, index int) (int64, error) {
|
||||
return last, nil
|
||||
}
|
||||
|
||||
func (uc *MediaUsecase) uploadSession(ctx context.Context, id uint) (*UploadSession, error) {
|
||||
session, err := uc.FindUploadSession(ctx, id)
|
||||
if errors.Is(err, ErrUploadSessionNotFound) {
|
||||
return nil, fmt.Errorf("上传会话不存在: %w", ErrUploadSessionNotFound)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (uc *MediaUsecase) chunkPrefix(uploadID uint) string {
|
||||
directory := "uploads/chunks"
|
||||
if uc.settings != nil {
|
||||
@@ -117,9 +128,9 @@ func (uc *MediaUsecase) InitUpload(ctx context.Context, userID uint, name, hash
|
||||
return session, nil, indices, nil
|
||||
}
|
||||
func (uc *MediaUsecase) SaveChunk(ctx context.Context, userID, uploadID uint, index int, expected string, reader io.Reader) error {
|
||||
session, err := uc.FindUploadSession(ctx, uploadID)
|
||||
session, err := uc.uploadSession(ctx, uploadID)
|
||||
if err != nil {
|
||||
return errors.New("上传会话不存在")
|
||||
return err
|
||||
}
|
||||
if session.UserID != userID {
|
||||
return errors.New("无权操作该上传")
|
||||
@@ -173,9 +184,9 @@ func (uc *MediaUsecase) SaveChunk(ctx context.Context, userID, uploadID uint, in
|
||||
return nil
|
||||
}
|
||||
func (uc *MediaUsecase) CompleteUpload(ctx context.Context, userID, uploadID uint, mime string) (*MediaFile, error) {
|
||||
session, err := uc.FindUploadSession(ctx, uploadID)
|
||||
session, err := uc.uploadSession(ctx, uploadID)
|
||||
if err != nil {
|
||||
return nil, errors.New("上传会话不存在")
|
||||
return nil, err
|
||||
}
|
||||
if session.UserID != userID {
|
||||
return nil, errors.New("无权操作该上传")
|
||||
@@ -241,9 +252,9 @@ func (uc *MediaUsecase) CompleteUpload(ctx context.Context, userID, uploadID uin
|
||||
return media, nil
|
||||
}
|
||||
func (uc *MediaUsecase) CancelUpload(ctx context.Context, userID, uploadID uint) error {
|
||||
session, err := uc.FindUploadSession(ctx, uploadID)
|
||||
session, err := uc.uploadSession(ctx, uploadID)
|
||||
if err != nil {
|
||||
return errors.New("上传会话不存在")
|
||||
return err
|
||||
}
|
||||
if session.UserID != userID {
|
||||
return errors.New("无权操作该上传")
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package system
|
||||
|
||||
import "context"
|
||||
|
||||
type ServerDiskInfo struct {
|
||||
MountPoint string
|
||||
UsedMB uint64
|
||||
UsedGB uint64
|
||||
TotalMB uint64
|
||||
TotalGB uint64
|
||||
UsedPercent int
|
||||
}
|
||||
|
||||
type ServerInfo struct {
|
||||
GOOS string
|
||||
NumCPU int
|
||||
Compiler string
|
||||
GoVersion string
|
||||
NumGoroutine int
|
||||
CPUs []float64
|
||||
Cores int
|
||||
UsedMB uint64
|
||||
TotalMB uint64
|
||||
UsedPercent int
|
||||
Disk []ServerDiskInfo
|
||||
}
|
||||
|
||||
type SystemInfoProvider interface {
|
||||
Collect(context.Context, []string) (*ServerInfo, error)
|
||||
}
|
||||
@@ -50,10 +50,11 @@ type TaskReloader interface {
|
||||
type SystemConfigUsecase struct {
|
||||
repo InitializationRepo
|
||||
tasks TaskReloader
|
||||
info SystemInfoProvider
|
||||
}
|
||||
|
||||
func NewSystemConfigUsecase(repo InitializationRepo, tasks TaskReloader) *SystemConfigUsecase {
|
||||
return &SystemConfigUsecase{repo: repo, tasks: tasks}
|
||||
func NewSystemConfigUsecase(repo InitializationRepo, tasks TaskReloader, info SystemInfoProvider) *SystemConfigUsecase {
|
||||
return &SystemConfigUsecase{repo: repo, tasks: tasks, info: info}
|
||||
}
|
||||
|
||||
func (uc *SystemConfigUsecase) IsInitialized(ctx context.Context) (bool, error) {
|
||||
@@ -128,3 +129,10 @@ func (uc *SystemConfigUsecase) DiskMountPoints() []string {
|
||||
}
|
||||
return uc.repo.DiskMountPoints()
|
||||
}
|
||||
|
||||
func (uc *SystemConfigUsecase) ServerInfo(ctx context.Context) (*ServerInfo, error) {
|
||||
if uc == nil || uc.repo == nil || uc.info == nil {
|
||||
return nil, errors.New("系统信息服务未就绪")
|
||||
}
|
||||
return uc.info.Collect(ctx, uc.repo.DiskMountPoints())
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ func (r taskReloaderStub) Reload(context.Context) error { return r.err }
|
||||
|
||||
func TestInitializeClassifiesPostCommitTaskReloadFailure(t *testing.T) {
|
||||
reloadErr := errors.New("scheduler unavailable")
|
||||
uc := NewSystemConfigUsecase(&initializationRepoStub{}, taskReloaderStub{err: reloadErr})
|
||||
uc := NewSystemConfigUsecase(&initializationRepoStub{}, taskReloaderStub{err: reloadErr}, nil)
|
||||
err := uc.Initialize(context.Background(), &DatabaseConfig{})
|
||||
if !errors.Is(err, ErrTaskRuntimeReload) || !errors.Is(err, reloadErr) {
|
||||
t.Fatalf("Initialize() error = %v", err)
|
||||
@@ -45,7 +45,7 @@ func TestInitializeClassifiesPostCommitTaskReloadFailure(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestReloadConfigClassifiesMissingTaskRuntime(t *testing.T) {
|
||||
uc := NewSystemConfigUsecase(&initializationRepoStub{}, nil)
|
||||
uc := NewSystemConfigUsecase(&initializationRepoStub{}, nil, nil)
|
||||
err := uc.ReloadConfig(context.Background())
|
||||
if !errors.Is(err, ErrTaskRuntimeReload) {
|
||||
t.Fatalf("ReloadConfig() error = %v", err)
|
||||
@@ -53,7 +53,7 @@ func TestReloadConfigClassifiesMissingTaskRuntime(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSystemConfigUsecaseRejectsMissingRepository(t *testing.T) {
|
||||
uc := NewSystemConfigUsecase(nil, nil)
|
||||
uc := NewSystemConfigUsecase(nil, nil, nil)
|
||||
if _, err := uc.IsInitialized(context.Background()); err == nil {
|
||||
t.Fatal("IsInitialized accepted a missing repository")
|
||||
}
|
||||
|
||||
@@ -10,3 +10,44 @@ func CloneAdmin(value *Admin) *Admin { return cloneAdmin(value) }
|
||||
func CloneDatabase(value *Database) *Database { return clonePtr(value) }
|
||||
func CloneStorage(value *Storage) *Storage { return cloneStorage(value) }
|
||||
func CloneEmail(value *Email) *Email { return clonePtr(value) }
|
||||
|
||||
// MergeRuntimeConfig applies the runtime replacement contract shared by file
|
||||
// watching and management updates. Omitted top-level sections keep their
|
||||
// current values, while database-backed storage/email settings are never
|
||||
// erased merely because they are absent from config.yaml.
|
||||
func MergeRuntimeConfig(current, next *Config) *Config {
|
||||
merged := cloneConfig(next)
|
||||
if merged == nil {
|
||||
merged = &Config{}
|
||||
}
|
||||
if current == nil {
|
||||
if merged.Admin == nil {
|
||||
merged.Admin = &Admin{}
|
||||
}
|
||||
return merged
|
||||
}
|
||||
if merged.Server == nil {
|
||||
merged.Server = clonePtr(current.Server)
|
||||
}
|
||||
if merged.Data == nil {
|
||||
merged.Data = cloneData(current.Data)
|
||||
}
|
||||
if merged.Admin == nil {
|
||||
merged.Admin = cloneAdmin(current.Admin)
|
||||
}
|
||||
if merged.Admin == nil {
|
||||
merged.Admin = &Admin{}
|
||||
}
|
||||
if current.Admin != nil {
|
||||
if merged.Admin.Storage == nil {
|
||||
merged.Admin.Storage = cloneStorage(current.Admin.Storage)
|
||||
}
|
||||
if merged.Admin.Email == nil {
|
||||
merged.Admin.Email = clonePtr(current.Admin.Email)
|
||||
}
|
||||
if merged.Admin.ConfigPath == "" {
|
||||
merged.Admin.ConfigPath = current.Admin.ConfigPath
|
||||
}
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
@@ -45,6 +45,13 @@ func (d *Document) Set(key string, value any) error {
|
||||
// Delete removes the nested key path if present.
|
||||
func (d *Document) Delete(keys ...string) { deleteYAMLMapping(&d.node, keys...) }
|
||||
|
||||
// DeleteIntegrationConfig removes settings owned by the database-backed
|
||||
// integration store rather than by config.yaml.
|
||||
func (d *Document) DeleteIntegrationConfig() {
|
||||
d.Delete("admin", "storage")
|
||||
d.Delete("admin", "email")
|
||||
}
|
||||
|
||||
// Has reports whether the nested key path exists.
|
||||
func (d *Document) Has(keys ...string) bool { return yamlMappingValue(&d.node, keys...) != nil }
|
||||
|
||||
|
||||
@@ -268,23 +268,7 @@ func (r *Store) watchLoop(watcher *fsnotify.Watcher, stop <-chan struct{}, done
|
||||
// take down a running process or publish invalid state.
|
||||
continue
|
||||
}
|
||||
current := r.Snapshot()
|
||||
if next.Admin == nil {
|
||||
next.Admin = &Admin{}
|
||||
}
|
||||
// Storage and Email move to database-backed integration settings after
|
||||
// first initialization. External config file edits must not erase the
|
||||
// active values when those sections are absent from config.yaml.
|
||||
if current != nil && current.Admin != nil {
|
||||
if next.Admin.Storage == nil {
|
||||
next.Admin.Storage = cloneStorage(current.Admin.Storage)
|
||||
}
|
||||
if next.Admin.Email == nil && current.Admin.Email != nil {
|
||||
email := *current.Admin.Email
|
||||
next.Admin.Email = &email
|
||||
}
|
||||
}
|
||||
r.Replace(next)
|
||||
r.Replace(MergeRuntimeConfig(r.Snapshot(), next))
|
||||
case _, ok := <-errors:
|
||||
// fsnotify errors are intentionally non-fatal; the watcher remains
|
||||
// useful for subsequent events and Close always terminates it.
|
||||
|
||||
@@ -145,3 +145,26 @@ func TestStoreWatchPreservesDatabaseBackedIntegrationSettings(t *testing.T) {
|
||||
}
|
||||
t.Fatal("watcher did not publish the changed file")
|
||||
}
|
||||
|
||||
func TestMergeRuntimeConfigPreservesOmittedSections(t *testing.T) {
|
||||
current := &Config{
|
||||
Server: &Server{HTTP: &ServerHTTP{Addr: ":8080"}},
|
||||
Data: &Data{Database: &Database{Source: "current-dsn"}},
|
||||
Admin: &Admin{ConfigPath: "config.yaml", Storage: &Storage{Type: "local"}, Email: &Email{Host: "smtp.example.com"}},
|
||||
}
|
||||
next := &Config{Admin: &Admin{App: &App{AppID: "next"}}}
|
||||
merged := MergeRuntimeConfig(current, next)
|
||||
if merged.Server == nil || merged.Server.HTTP == nil || merged.Server.HTTP.Addr != ":8080" {
|
||||
t.Fatalf("server was not preserved: %#v", merged.Server)
|
||||
}
|
||||
if merged.Data == nil || merged.Data.Database == nil || merged.Data.Database.Source != "current-dsn" {
|
||||
t.Fatalf("data was not preserved: %#v", merged.Data)
|
||||
}
|
||||
if merged.Admin.Storage == nil || merged.Admin.Storage.Type != "local" || merged.Admin.Email == nil || merged.Admin.Email.Host != "smtp.example.com" {
|
||||
t.Fatalf("integration settings were not preserved: %#v", merged.Admin)
|
||||
}
|
||||
merged.Admin.Storage.Type = "changed"
|
||||
if current.Admin.Storage.Type != "local" {
|
||||
t.Fatal("MergeRuntimeConfig leaked mutable storage state into current config")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,8 +48,7 @@ func (d *Data) persistConfigValues(dataConfig *config.Data, adminConfig *config.
|
||||
if err = document.Set("admin", fileAdmin); err != nil {
|
||||
return err
|
||||
}
|
||||
document.Delete("admin", "storage")
|
||||
document.Delete("admin", "email")
|
||||
document.DeleteIntegrationConfig()
|
||||
if adminConfig.System != nil {
|
||||
if err = document.SetServerHTTPPort(adminConfig.System.Addr); err != nil {
|
||||
return err
|
||||
@@ -100,8 +99,7 @@ func (d *Data) persistDatabaseConfig(database *config.Database, signingKey strin
|
||||
return err
|
||||
}
|
||||
}
|
||||
document.Delete("admin", "storage")
|
||||
document.Delete("admin", "email")
|
||||
document.DeleteIntegrationConfig()
|
||||
return document.Save()
|
||||
}
|
||||
|
||||
@@ -119,8 +117,7 @@ func (d *Data) removeIntegrationConfigFromFile() error {
|
||||
if !document.Has("admin", "storage") && !document.Has("admin", "email") {
|
||||
return nil
|
||||
}
|
||||
document.Delete("admin", "storage")
|
||||
document.Delete("admin", "email")
|
||||
document.DeleteIntegrationConfig()
|
||||
return document.Save()
|
||||
}
|
||||
|
||||
@@ -135,6 +132,7 @@ func (d *Data) reloadConfig(ctx context.Context) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
next = config.MergeRuntimeConfig(d.Config(), next)
|
||||
if next.Data == nil || next.Data.Database == nil || next.Admin == nil {
|
||||
return fmt.Errorf("data.database and admin configuration are required")
|
||||
}
|
||||
@@ -170,23 +168,12 @@ func (d *Data) reloadConfig(ctx context.Context) error {
|
||||
return fmt.Errorf("reload database migrations: %w", err)
|
||||
}
|
||||
}
|
||||
legacyStorage := next.Admin.Storage
|
||||
legacyEmail := next.Admin.Email
|
||||
currentAdmin := d.runtime.Admin()
|
||||
if legacyStorage == nil {
|
||||
if currentAdmin != nil {
|
||||
legacyStorage = currentAdmin.Storage
|
||||
}
|
||||
}
|
||||
if legacyEmail == nil && currentAdmin != nil {
|
||||
legacyEmail = currentAdmin.Email
|
||||
}
|
||||
storageConfig, err := dataintegration.ResolveStorageConfig(candidateDB.WithContext(ctx), legacyStorage)
|
||||
storageConfig, err := dataintegration.ResolveStorageConfig(candidateDB.WithContext(ctx), next.Admin.Storage)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reload storage configuration: %w", err)
|
||||
}
|
||||
next.Admin.Storage = storageConfig
|
||||
emailConfig, err := dataintegration.ResolveEmailConfig(candidateDB.WithContext(ctx), legacyEmail)
|
||||
emailConfig, err := dataintegration.ResolveEmailConfig(candidateDB.WithContext(ctx), next.Admin.Email)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reload email configuration: %w", err)
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ func normalizedDriver(driver string) string {
|
||||
}
|
||||
|
||||
func databaseDSN(c *config.Database, name string) (string, error) {
|
||||
if c.Source != "" && c.Host == "" && c.Path == "" {
|
||||
if c.Source != "" && !databaseHasStructuredFields(c) {
|
||||
return c.Source, nil
|
||||
}
|
||||
driver := normalizedDriver(c.Driver)
|
||||
@@ -118,6 +118,10 @@ func databaseDSN(c *config.Database, name string) (string, error) {
|
||||
return "", fmt.Errorf("unsupported database driver %q", c.Driver)
|
||||
}
|
||||
|
||||
func databaseHasStructuredFields(c *config.Database) bool {
|
||||
return c != nil && (c.Host != "" || c.Port != "" || c.User != "" || c.Password != "" || c.Name != "" || c.Config != "" || c.Path != "")
|
||||
}
|
||||
|
||||
func gormConfig(config *config.Database, appLogger *slog.Logger) *gorm.Config {
|
||||
level := logger.Info
|
||||
switch strings.ToLower(config.LogMode) {
|
||||
|
||||
@@ -43,15 +43,11 @@ func refreshDatabaseSource(database *configpkg.Database) error {
|
||||
if database == nil {
|
||||
return nil
|
||||
}
|
||||
hasStructuredConfig := database.Host != "" || database.Port != "" || database.User != "" || database.Password != "" || database.Name != "" || database.Config != "" || database.Path != ""
|
||||
if !hasStructuredConfig {
|
||||
if !databaseHasStructuredFields(database) {
|
||||
return nil
|
||||
}
|
||||
previousSource := database.Source
|
||||
database.Source = ""
|
||||
source, err := databaseDSN(database, "")
|
||||
if err != nil {
|
||||
database.Source = previousSource
|
||||
return err
|
||||
}
|
||||
database.Source = source
|
||||
@@ -64,25 +60,7 @@ func (d *Data) PersistRuntimeConfig(ctx context.Context, value *configpkg.Config
|
||||
if current == nil {
|
||||
current = &configpkg.Config{}
|
||||
}
|
||||
next := configpkg.Clone(value)
|
||||
if next == nil {
|
||||
next = configpkg.Clone(current)
|
||||
}
|
||||
if next.Data == nil {
|
||||
next.Data = configpkg.CloneData(current.Data)
|
||||
}
|
||||
if next.Admin == nil {
|
||||
next.Admin = configpkg.CloneAdmin(current.Admin)
|
||||
}
|
||||
if next.Admin != nil && current.Admin != nil {
|
||||
if next.Admin.Storage == nil {
|
||||
next.Admin.Storage = configpkg.CloneStorage(current.Admin.Storage)
|
||||
}
|
||||
if next.Admin.Email == nil {
|
||||
next.Admin.Email = configpkg.CloneEmail(current.Admin.Email)
|
||||
}
|
||||
next.Admin.ConfigPath = current.Admin.ConfigPath
|
||||
}
|
||||
next := configpkg.MergeRuntimeConfig(current, value)
|
||||
if next.Data == nil {
|
||||
next.Data = &configpkg.Data{}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"fmt"
|
||||
integrationbiz "kra/internal/biz/integration"
|
||||
bizpayment "kra/internal/biz/payment"
|
||||
"net/url"
|
||||
"kra/internal/paymentkit"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -21,10 +21,11 @@ type paymentRepo struct {
|
||||
data Provider
|
||||
config integrationbiz.PaymentConfigReader
|
||||
factory bizpayment.PaymentAdapterFactory
|
||||
orders bizpayment.PaymentOrderRepo
|
||||
}
|
||||
|
||||
func NewPaymentRepo(data Provider, config integrationbiz.PaymentConfigReader, factory bizpayment.PaymentAdapterFactory) bizpayment.PaymentRepo {
|
||||
return &paymentRepo{data: data, config: config, factory: factory}
|
||||
func NewPaymentRepo(data Provider, config integrationbiz.PaymentConfigReader, factory bizpayment.PaymentAdapterFactory, orders bizpayment.PaymentOrderRepo) bizpayment.PaymentRepo {
|
||||
return &paymentRepo{data: data, config: config, factory: factory, orders: orders}
|
||||
}
|
||||
|
||||
func (r *paymentRepo) values(ctx context.Context, provider string) (map[string]any, error) {
|
||||
@@ -71,7 +72,7 @@ func (r *paymentRepo) TestProvider(ctx context.Context, provider string) (*bizpa
|
||||
add("config", "failed", err.Error(), "", started)
|
||||
return test, err
|
||||
}
|
||||
test.Mode = strings.ToLower(strings.TrimSpace(text(values, "environment")))
|
||||
test.Mode = strings.ToLower(strings.TrimSpace(paymentkit.Text(values, "environment")))
|
||||
configStart := time.Now()
|
||||
if err = integrationbiz.ValidateIntegrationConfig(integrationbiz.IntegrationKindPayment, provider, values); err != nil {
|
||||
add("config", "failed", err.Error(), "", configStart)
|
||||
@@ -94,12 +95,16 @@ func (r *paymentRepo) TestProvider(ctx context.Context, provider string) (*bizpa
|
||||
add("adapter", "failed", err.Error(), "", time.Now())
|
||||
return test, err
|
||||
}
|
||||
if r.orders == nil {
|
||||
err = errors.New("支付订单仓储未接入")
|
||||
add("local_order", "failed", err.Error(), "", time.Now())
|
||||
return test, err
|
||||
}
|
||||
req := paymentTestRequest(provider, values)
|
||||
test.TradeNo = req.TradeNo
|
||||
orders := &paymentOrderRepo{data: r.data}
|
||||
extra, _ := json.Marshal(req.Extra)
|
||||
localStart := time.Now()
|
||||
order, _, err := orders.CreatePaymentOrder(ctx, &bizpayment.PaymentOrder{
|
||||
order, _, err := r.orders.CreatePaymentOrder(ctx, &bizpayment.PaymentOrder{
|
||||
TradeNo: req.TradeNo, Provider: provider, BusinessType: req.BusinessType, BusinessID: req.BusinessID,
|
||||
Subject: req.Subject, PaymentMode: bizpayment.PaymentModeExternal, OriginalAmount: req.Amount, Amount: req.Amount,
|
||||
Currency: req.Currency, PaymentStatus: bizpayment.PaymentStatusInitialized, FulfillmentStatus: bizpayment.FulfillmentStatusPending,
|
||||
@@ -127,7 +132,7 @@ func (r *paymentRepo) TestProvider(ctx context.Context, provider string) (*bizpa
|
||||
add("create", "failed", err.Error(), req.TradeNo, createStart)
|
||||
return test, err
|
||||
}
|
||||
if order, err = orders.RecordPaymentCreate(ctx, provider, req.TradeNo, paymentTestProviderUpdate(created)); err != nil {
|
||||
if order, err = r.orders.RecordPaymentCreate(ctx, provider, req.TradeNo, paymentTestProviderUpdate(created)); err != nil {
|
||||
add("local_order", "failed", "记录第三方下单结果失败: "+err.Error(), req.TradeNo, createStart)
|
||||
return test, err
|
||||
}
|
||||
@@ -142,7 +147,7 @@ func (r *paymentRepo) TestProvider(ctx context.Context, provider string) (*bizpa
|
||||
queryID = req.TradeNo
|
||||
}
|
||||
if provider == bizpayment.PaymentApple {
|
||||
queryID = strings.TrimSpace(text(values, "test_transaction_id"))
|
||||
queryID = strings.TrimSpace(paymentkit.Text(values, "test_transaction_id"))
|
||||
if queryID == "" {
|
||||
err = errors.New("Apple 连通性测试需要配置 test_transaction_id(沙箱交易 ID)")
|
||||
add("query", "failed", err.Error(), req.TradeNo, time.Now())
|
||||
@@ -162,7 +167,7 @@ func (r *paymentRepo) TestProvider(ctx context.Context, provider string) (*bizpa
|
||||
return test, err
|
||||
}
|
||||
if provider != bizpayment.PaymentApple {
|
||||
if order, err = orders.ApplyPaymentResult(ctx, provider, req.TradeNo, paymentTestProviderUpdate(queried)); err != nil {
|
||||
if order, err = r.orders.ApplyPaymentResult(ctx, provider, req.TradeNo, paymentTestProviderUpdate(queried)); err != nil {
|
||||
add("local_order", "failed", "回写测试查单结果失败: "+err.Error(), req.TradeNo, queryStart)
|
||||
return test, err
|
||||
}
|
||||
@@ -185,7 +190,7 @@ func (r *paymentRepo) TestProvider(ctx context.Context, provider string) (*bizpa
|
||||
return test, nil
|
||||
}
|
||||
refundStart := time.Now()
|
||||
order, refundToken, beginErr := orders.BeginPaymentRefund(ctx, provider, req.TradeNo, req.Amount, time.Minute)
|
||||
order, refundToken, beginErr := r.orders.BeginPaymentRefund(ctx, provider, req.TradeNo, req.Amount, time.Minute)
|
||||
if beginErr != nil {
|
||||
add("refund", "failed", beginErr.Error(), req.TradeNo, refundStart)
|
||||
return test, beginErr
|
||||
@@ -201,7 +206,7 @@ func (r *paymentRepo) TestProvider(ctx context.Context, provider string) (*bizpa
|
||||
add("refund", "failed", err.Error(), req.TradeNo, refundStart)
|
||||
return test, err
|
||||
}
|
||||
if _, err = orders.CompletePaymentRefundRequest(ctx, provider, req.TradeNo, refundToken, true, ""); err != nil {
|
||||
if _, err = r.orders.CompletePaymentRefundRequest(ctx, provider, req.TradeNo, refundToken, true, ""); err != nil {
|
||||
add("local_order", "failed", "回写测试退款结果失败: "+err.Error(), req.TradeNo, refundStart)
|
||||
return test, err
|
||||
}
|
||||
@@ -301,19 +306,19 @@ func (r *paymentRepo) testRow(ctx context.Context, provider string) (map[string]
|
||||
|
||||
func paymentTestRequest(provider string, values map[string]any) *bizpayment.PaymentRequest {
|
||||
tradeNo := "kra-test-" + time.Now().UTC().Format("20060102150405.000000000")
|
||||
amount := configuredInt64(values, "test_amount", 1)
|
||||
amount := paymentkit.ConfiguredInt64(values, "test_amount", 1)
|
||||
if amount <= 0 {
|
||||
amount = 1
|
||||
}
|
||||
req := &bizpayment.PaymentRequest{Provider: provider, TradeNo: strings.ReplaceAll(tradeNo, ".", ""), Subject: "Kra 支付渠道连通性测试", Amount: amount, Currency: strings.ToUpper(firstAny(values, "test_currency", "currency", "fee_type")), NotifyURL: text(values, "notify_url"), ReturnURL: text(values, "return_url"), BusinessType: "system_payment_test", BusinessID: uuid.NewString(), Extra: map[string]any{}}
|
||||
req := &bizpayment.PaymentRequest{Provider: provider, TradeNo: strings.ReplaceAll(tradeNo, ".", ""), Subject: "Kra 支付渠道连通性测试", Amount: amount, Currency: strings.ToUpper(paymentkit.FirstText(values, "test_currency", "currency", "fee_type")), NotifyURL: paymentkit.Text(values, "notify_url"), ReturnURL: paymentkit.Text(values, "return_url"), BusinessType: "system_payment_test", BusinessID: uuid.NewString(), Extra: map[string]any{}}
|
||||
if req.Currency == "" {
|
||||
req.Currency = "CNY"
|
||||
}
|
||||
if provider == bizpayment.PaymentApple {
|
||||
req.TradeNo = uuid.NewString()
|
||||
req.Extra["product_id"] = firstAny(values, "product_id", "test_product_id")
|
||||
req.Extra["product_id"] = paymentkit.FirstText(values, "product_id", "test_product_id")
|
||||
}
|
||||
if raw := strings.TrimSpace(text(values, "test_extra")); raw != "" {
|
||||
if raw := strings.TrimSpace(paymentkit.Text(values, "test_extra")); raw != "" {
|
||||
var extra map[string]any
|
||||
if json.Unmarshal([]byte(raw), &extra) == nil {
|
||||
for key, value := range extra {
|
||||
@@ -322,7 +327,7 @@ func paymentTestRequest(provider string, values map[string]any) *bizpayment.Paym
|
||||
}
|
||||
}
|
||||
for _, key := range []string{"trade_type", "method", "pay_type", "channel", "openid", "open_id", "auth_code", "authcode", "barcode"} {
|
||||
if value := text(values, key); value != "" {
|
||||
if value := paymentkit.Text(values, key); value != "" {
|
||||
req.Extra[key] = value
|
||||
}
|
||||
}
|
||||
@@ -333,16 +338,16 @@ func validatePaymentTestSettings(provider string, values map[string]any) error {
|
||||
if !testModeEnabled(values) {
|
||||
return errors.New("请先打开 test_mode(允许执行渠道测试)")
|
||||
}
|
||||
if raw := strings.TrimSpace(text(values, "test_extra")); raw != "" {
|
||||
if raw := strings.TrimSpace(paymentkit.Text(values, "test_extra")); raw != "" {
|
||||
var extra map[string]any
|
||||
if err := json.Unmarshal([]byte(raw), &extra); err != nil {
|
||||
return fmt.Errorf("test_extra 必须是 JSON 对象: %w", err)
|
||||
}
|
||||
}
|
||||
if provider == bizpayment.PaymentApple && strings.TrimSpace(text(values, "test_transaction_id")) == "" {
|
||||
if provider == bizpayment.PaymentApple && strings.TrimSpace(paymentkit.Text(values, "test_transaction_id")) == "" {
|
||||
return errors.New("Apple 测试需要 test_transaction_id(沙箱交易 ID)")
|
||||
}
|
||||
if provider == bizpayment.PaymentApple && strings.TrimSpace(firstAny(values, "test_product_id", "product_id")) == "" {
|
||||
if provider == bizpayment.PaymentApple && strings.TrimSpace(paymentkit.FirstText(values, "test_product_id", "product_id")) == "" {
|
||||
return errors.New("Apple 测试需要 test_product_id(沙箱商品 ID)")
|
||||
}
|
||||
return nil
|
||||
@@ -374,8 +379,8 @@ func (r *paymentRepo) Create(ctx context.Context, req *bizpayment.PaymentRequest
|
||||
return nil, err
|
||||
}
|
||||
effective := *req
|
||||
effective.NotifyURL = strings.TrimSpace(text(c, "notify_url"))
|
||||
effective.ReturnURL = strings.TrimSpace(text(c, "return_url"))
|
||||
effective.NotifyURL = strings.TrimSpace(paymentkit.Text(c, "notify_url"))
|
||||
effective.ReturnURL = strings.TrimSpace(paymentkit.Text(c, "return_url"))
|
||||
if paymentCreateRequiresNotifyURL(req.Provider, req.Extra, c) && effective.NotifyURL == "" {
|
||||
return nil, fmt.Errorf("支付渠道 %s 未配置服务端 notify_url", req.Provider)
|
||||
}
|
||||
@@ -412,9 +417,9 @@ func paymentCreateRequiresNotifyURL(provider string, extra, config map[string]an
|
||||
case bizpayment.PaymentLakala:
|
||||
keys = []string{"method", "pay_method", "trade_type"}
|
||||
}
|
||||
value := firstAny(extra, keys...)
|
||||
value := paymentkit.FirstText(extra, keys...)
|
||||
if value == "" {
|
||||
value = firstAny(config, keys...)
|
||||
value = paymentkit.FirstText(config, keys...)
|
||||
}
|
||||
normalized := strings.NewReplacer(".", "_", "-", "_", " ", "_").Replace(strings.ToLower(strings.TrimSpace(value)))
|
||||
switch provider {
|
||||
@@ -484,8 +489,8 @@ func paymentCallbackEventID(callback *bizpayment.PaymentCallback, result *bizpay
|
||||
if callback == nil {
|
||||
return ""
|
||||
}
|
||||
fields := callbackFields(callback)
|
||||
if eventID := strings.TrimSpace(first(fields, "event_id", "notify_id", "notificationUUID", "id")); eventID != "" {
|
||||
fields := paymentkit.CallbackFields(callback.Query, callback.Headers, callback.Body)
|
||||
if eventID := strings.TrimSpace(paymentkit.FirstString(fields, "event_id", "notify_id", "notificationUUID", "id")); eventID != "" {
|
||||
return eventID
|
||||
}
|
||||
hash := sha256.Sum256(append([]byte(callback.Provider+"\x00"), callback.Body...))
|
||||
@@ -498,12 +503,12 @@ func paymentCallbackAck(provider string, values map[string]any, success bool) bi
|
||||
if !success {
|
||||
prefix = "callback_failure_"
|
||||
}
|
||||
if configured := strings.TrimSpace(text(values, prefix+"status")); configured != "" {
|
||||
if configured := strings.TrimSpace(paymentkit.Text(values, prefix+"status")); configured != "" {
|
||||
if status, err := strconv.Atoi(configured); err == nil && status >= 200 && status <= 599 {
|
||||
ack.StatusCode = status
|
||||
}
|
||||
}
|
||||
if contentType := strings.TrimSpace(text(values, prefix+"content_type")); contentType != "" {
|
||||
if contentType := strings.TrimSpace(paymentkit.Text(values, prefix+"content_type")); contentType != "" {
|
||||
ack.ContentType = contentType
|
||||
}
|
||||
if body, exists := values[prefix+"body"]; exists {
|
||||
@@ -512,41 +517,6 @@ func paymentCallbackAck(provider string, values map[string]any, success bool) bi
|
||||
return ack
|
||||
}
|
||||
|
||||
func callbackFields(callback *bizpayment.PaymentCallback) map[string]string {
|
||||
fields := map[string]string{}
|
||||
for key, value := range callback.Query {
|
||||
fields[key] = value
|
||||
}
|
||||
contentType := strings.ToLower(callback.Headers["Content-Type"])
|
||||
if strings.Contains(contentType, "application/x-www-form-urlencoded") {
|
||||
if values, err := url.ParseQuery(string(callback.Body)); err == nil {
|
||||
for key, value := range values {
|
||||
if len(value) > 0 {
|
||||
fields[key] = value[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var object map[string]any
|
||||
if json.Unmarshal(callback.Body, &object) == nil {
|
||||
for key, value := range object {
|
||||
if text, ok := value.(string); ok {
|
||||
fields[key] = text
|
||||
}
|
||||
}
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
func first(values map[string]string, keys ...string) string {
|
||||
for _, key := range keys {
|
||||
if values[key] != "" {
|
||||
return values[key]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func contains(values []string, value string) bool {
|
||||
for _, item := range values {
|
||||
if item == value {
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
package payment
|
||||
|
||||
import "kra/internal/paymentkit"
|
||||
|
||||
func text(values map[string]any, key string) string {
|
||||
return paymentkit.Text(values, key)
|
||||
}
|
||||
|
||||
func firstAny(values map[string]any, keys ...string) string {
|
||||
return paymentkit.FirstText(values, keys...)
|
||||
}
|
||||
|
||||
// configuredInt64 keeps repository-side validation independent from the
|
||||
// provider adapter package while sharing the canonical payment parser.
|
||||
func configuredInt64(values map[string]any, key string, fallback int64) int64 {
|
||||
return paymentkit.ConfiguredInt64(values, key, fallback)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
bizpayment "kra/internal/biz/payment"
|
||||
"kra/internal/paymentkit"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -476,12 +477,12 @@ func applyProviderIdentity(tx *gorm.DB, po *paymentOrderPO, update *bizpayment.P
|
||||
}
|
||||
|
||||
func normalizeOrderPaymentStatus(value string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "success", "paid", "fulfilled":
|
||||
switch paymentkit.NormalizeStatus(value, "") {
|
||||
case "success":
|
||||
return bizpayment.PaymentStatusPaid
|
||||
case "pending", "created", "client_pending", "processing":
|
||||
case "pending":
|
||||
return bizpayment.PaymentStatusPending
|
||||
case "failed", "closed", "cancelled", "canceled":
|
||||
case "failed":
|
||||
return bizpayment.PaymentStatusFailed
|
||||
default:
|
||||
return ""
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"time"
|
||||
|
||||
"kra/pkg/database/gormkit"
|
||||
"kra/pkg/database/pagination"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -72,22 +71,9 @@ func (r *announcementRepo) List(ctx context.Context, filter system.AnnouncementF
|
||||
if filter.StartCreatedAt != nil && filter.EndCreatedAt != nil {
|
||||
db = db.Where("created_at BETWEEN ? AND ?", filter.StartCreatedAt, filter.EndCreatedAt)
|
||||
}
|
||||
var total int64
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if filter.PageSize > 0 {
|
||||
db = pagination.Apply(db, filter.Page, filter.PageSize, 100)
|
||||
}
|
||||
var pos []announcementPO
|
||||
if err := db.Find(&pos).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
items := make([]*system.Announcement, 0, len(pos))
|
||||
for _, po := range pos {
|
||||
items = append(items, announcementToBiz(po))
|
||||
}
|
||||
return items, total, nil
|
||||
return listRows(db, filter.Page, filter.PageSize, filter.PageSize > 0, func(po announcementPO) *system.Announcement {
|
||||
return announcementToBiz(po)
|
||||
})
|
||||
}
|
||||
|
||||
func (r *announcementRepo) UserOptions(ctx context.Context) ([]system.UserOption, error) {
|
||||
|
||||
@@ -131,15 +131,7 @@ func (r *authorityAccessRepo) ensureAuthorityParentAcyclic(ctx context.Context,
|
||||
}
|
||||
|
||||
func (r *authorityAccessRepo) CreateAuthority(ctx context.Context, value *system.Authority) error {
|
||||
if value.DataScope == 0 {
|
||||
value.DataScope = 1
|
||||
} else if value.DataScope < 1 || value.DataScope > 5 {
|
||||
return errInvalidDataScope
|
||||
}
|
||||
if err := r.checkDataScopeGrant(ctx, value.DataScope, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
actor, allowed, strict, err := r.strictAuthorityAccess(ctx)
|
||||
actor, allowed, strict, err := r.prepareAuthorityCreate(ctx, value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -208,16 +200,24 @@ func (r *authorityAccessRepo) CreateAuthority(ctx context.Context, value *system
|
||||
return nil
|
||||
})
|
||||
}
|
||||
func (r *authorityAccessRepo) CopyAuthority(ctx context.Context, sourceID uint, value *system.Authority) error {
|
||||
|
||||
func (r *authorityAccessRepo) prepareAuthorityCreate(ctx context.Context, value *system.Authority) (system.Actor, map[uint]bool, bool, error) {
|
||||
if value.DataScope == 0 {
|
||||
value.DataScope = 1
|
||||
} else if value.DataScope < 1 || value.DataScope > 5 {
|
||||
return errInvalidDataScope
|
||||
return system.Actor{}, nil, false, errInvalidDataScope
|
||||
}
|
||||
if err := r.checkDataScopeGrant(ctx, value.DataScope, nil); err != nil {
|
||||
return err
|
||||
return system.Actor{}, nil, false, err
|
||||
}
|
||||
actor, allowed, strict, err := r.strictAuthorityAccess(ctx)
|
||||
if err != nil {
|
||||
return system.Actor{}, nil, false, err
|
||||
}
|
||||
return actor, allowed, strict, nil
|
||||
}
|
||||
func (r *authorityAccessRepo) CopyAuthority(ctx context.Context, sourceID uint, value *system.Authority) error {
|
||||
actor, allowed, strict, err := r.prepareAuthorityCreate(ctx, value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@ import (
|
||||
"kra/internal/biz/system"
|
||||
"time"
|
||||
|
||||
"kra/pkg/database/pagination"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -39,12 +37,8 @@ func (r *auditQueryRepo) ListDataAccess(ctx context.Context, page, size int, q *
|
||||
db = db.Where("target_table LIKE ?", "%"+q.TargetTable+"%")
|
||||
}
|
||||
}
|
||||
var total int64
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
var pos []DataAccessLogPO
|
||||
if err := pagination.ApplyRequired(db.Order("id desc"), page, size, 100).Find(&pos).Error; err != nil {
|
||||
pos, total, err := queryRows[DataAccessLogPO](db.Order("id desc"), page, size, true, true)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
out := make([]*system.DataAccessLog, 0, len(pos))
|
||||
|
||||
@@ -5,8 +5,6 @@ import (
|
||||
"kra/internal/biz/system"
|
||||
"time"
|
||||
|
||||
"kra/pkg/database/pagination"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -92,17 +90,7 @@ func (r *auditQueryRepo) ListErrors(ctx context.Context, page, size int, q *syst
|
||||
db = db.Where("info LIKE ?", "%"+*q.Info+"%")
|
||||
}
|
||||
}
|
||||
var total int64
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
var pos []errorRecordPO
|
||||
if err := pagination.Apply(db.Order("created_at desc"), page, size, 100).Find(&pos).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
out := make([]*system.ErrorRecord, 0, len(pos))
|
||||
for _, po := range pos {
|
||||
out = append(out, errorFromPO(po))
|
||||
}
|
||||
return out, total, nil
|
||||
return listRows(db.Order("created_at desc"), page, size, true, func(po errorRecordPO) *system.ErrorRecord {
|
||||
return errorFromPO(po)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"kra/pkg/database/pagination"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -171,11 +173,7 @@ func (r *exportRepo) ListExportTemplates(ctx context.Context, page, size int, q
|
||||
return nil, 0, err
|
||||
}
|
||||
var pos []exportTemplatePO
|
||||
// This module applies pageSize and its raw offset formula verbatim instead
|
||||
// of using the shared PageInfo normalization.
|
||||
if size != 0 {
|
||||
db = db.Limit(size).Offset(size * (page - 1))
|
||||
}
|
||||
db = pagination.Apply(db, page, size, 100)
|
||||
if err := db.Find(&pos).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"kra/pkg/database/pagination"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// listRows centralizes the storage-only part shared by simple repositories:
|
||||
// count the filtered query, apply the bounded page, load POs, and convert them
|
||||
// at the data/biz boundary. Domain-specific filters and ordering stay in each
|
||||
// repository.
|
||||
func queryRows[PO any](db *gorm.DB, page, size int, paginate, required bool) ([]PO, int64, error) {
|
||||
var total int64
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if paginate {
|
||||
if required {
|
||||
db = pagination.ApplyRequired(db, page, size, 100)
|
||||
} else {
|
||||
db = pagination.Apply(db, page, size, 100)
|
||||
}
|
||||
}
|
||||
var pos []PO
|
||||
if err := db.Find(&pos).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return pos, total, nil
|
||||
}
|
||||
|
||||
func listRows[PO any, DO any](db *gorm.DB, page, size int, paginate bool, convert func(PO) *DO) ([]*DO, int64, error) {
|
||||
pos, total, err := queryRows[PO](db, page, size, paginate, false)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
out := make([]*DO, 0, len(pos))
|
||||
for _, po := range pos {
|
||||
out = append(out, convert(po))
|
||||
}
|
||||
return out, total, nil
|
||||
}
|
||||
@@ -5,8 +5,6 @@ import (
|
||||
"kra/internal/biz/system"
|
||||
"time"
|
||||
|
||||
"kra/pkg/database/pagination"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -53,12 +51,8 @@ func (r *auditQueryRepo) ListLogins(ctx context.Context, page, size int, q *syst
|
||||
db = db.Where("created_at <= ?", *q.EndCreatedAt)
|
||||
}
|
||||
}
|
||||
var total int64
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
var pos []loginLogPO
|
||||
if err := pagination.ApplyRequired(db.Order("id desc"), page, size, 100).Find(&pos).Error; err != nil {
|
||||
pos, total, err := queryRows[loginLogPO](db.Order("id desc"), page, size, true, true)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
ids := make([]uint, 0, len(pos))
|
||||
|
||||
@@ -75,6 +75,9 @@ func (r *mediaRepo) CreateUploadSession(ctx context.Context, v *system.UploadSes
|
||||
func (r *mediaRepo) FindUploadSession(ctx context.Context, id uint) (*system.UploadSession, error) {
|
||||
var po uploadSessionPO
|
||||
if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, system.ErrUploadSessionNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return uploadFromPO(po), nil
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"kra/internal/biz/system"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -14,12 +13,7 @@ type menuRepo struct{ data Provider }
|
||||
func NewMenuRepo(data Provider) system.MenuRepo { return &menuRepo{data: data} }
|
||||
|
||||
func menuFromPO(po menuPO) *system.Menu {
|
||||
var deletedAt *time.Time
|
||||
if po.DeletedAt.Valid {
|
||||
value := po.DeletedAt.Time
|
||||
deletedAt = &value
|
||||
}
|
||||
return &system.Menu{CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, DeletedAt: deletedAt, ID: po.ID, ParentID: po.ParentID, Path: po.Path, Name: po.Name, Hidden: po.Hidden, Component: po.Component, Sort: po.Sort, ActiveName: po.ActiveName, KeepAlive: po.KeepAlive, DefaultMenu: po.DefaultMenu, Title: po.Title, Icon: po.Icon, CloseTab: po.CloseTab, TransitionType: po.TransitionType}
|
||||
return &system.Menu{CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, DeletedAt: deletedAtPointer(po.DeletedAt), ID: po.ID, ParentID: po.ParentID, Path: po.Path, Name: po.Name, Hidden: po.Hidden, Component: po.Component, Sort: po.Sort, ActiveName: po.ActiveName, KeepAlive: po.KeepAlive, DefaultMenu: po.DefaultMenu, Title: po.Title, Icon: po.Icon, CloseTab: po.CloseTab, TransitionType: po.TransitionType}
|
||||
}
|
||||
|
||||
func menuToPO(v *system.Menu) menuPO {
|
||||
@@ -239,21 +233,11 @@ func (r *menuRepo) List(ctx context.Context) ([]*system.Menu, error) {
|
||||
}
|
||||
|
||||
func menuParameterFromPO(po menuParameterPO) *system.MenuParameter {
|
||||
var deletedAt *time.Time
|
||||
if po.DeletedAt.Valid {
|
||||
value := po.DeletedAt.Time
|
||||
deletedAt = &value
|
||||
}
|
||||
return &system.MenuParameter{CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, DeletedAt: deletedAt, ID: po.ID, MenuID: po.MenuID, Type: po.Type, Key: po.Key, Value: po.Value}
|
||||
return &system.MenuParameter{CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, DeletedAt: deletedAtPointer(po.DeletedAt), ID: po.ID, MenuID: po.MenuID, Type: po.Type, Key: po.Key, Value: po.Value}
|
||||
}
|
||||
|
||||
func menuButtonFromPO(po menuButtonPO) *system.MenuButton {
|
||||
var deletedAt *time.Time
|
||||
if po.DeletedAt.Valid {
|
||||
value := po.DeletedAt.Time
|
||||
deletedAt = &value
|
||||
}
|
||||
return &system.MenuButton{CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, DeletedAt: deletedAt, ID: po.ID, Name: po.Name, Description: po.Description, MenuID: po.MenuID}
|
||||
return &system.MenuButton{CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, DeletedAt: deletedAtPointer(po.DeletedAt), ID: po.ID, Name: po.Name, Description: po.Description, MenuID: po.MenuID}
|
||||
}
|
||||
|
||||
func (r *menuRepo) SetAuthorityMenus(ctx context.Context, id uint, ids []uint) error {
|
||||
|
||||
@@ -5,8 +5,6 @@ import (
|
||||
"kra/internal/biz/system"
|
||||
"time"
|
||||
|
||||
"kra/pkg/database/pagination"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -49,12 +47,8 @@ func (r *auditQueryRepo) ListOperations(ctx context.Context, page, size int, q *
|
||||
db = db.Where("status = ?", q.Status)
|
||||
}
|
||||
}
|
||||
var total int64
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
var pos []operationPO
|
||||
if err := pagination.ApplyRequired(db.Order("id desc"), page, size, 100).Find(&pos).Error; err != nil {
|
||||
pos, total, err := queryRows[operationPO](db.Order("id desc"), page, size, true, true)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
users := auditUsers(ctx, r.data.DB().WithContext(ctx), operationUserIDs(pos))
|
||||
|
||||
@@ -5,8 +5,6 @@ import (
|
||||
"kra/internal/biz/system"
|
||||
"time"
|
||||
|
||||
"kra/pkg/database/pagination"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
@@ -72,17 +70,7 @@ func (r *parameterRepo) ListParameters(ctx context.Context, page, size int, q *s
|
||||
db = db.Where(clause.Like{Column: clause.Column{Name: "key"}, Value: "%" + q.Key + "%"})
|
||||
}
|
||||
}
|
||||
var total int64
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
var pos []parameterPO
|
||||
if err := pagination.Apply(db, page, size, 100).Find(&pos).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
out := make([]*system.SystemParameter, 0, len(pos))
|
||||
for _, po := range pos {
|
||||
out = append(out, parameterFromPO(po))
|
||||
}
|
||||
return out, total, nil
|
||||
return listRows(db, page, size, true, func(po parameterPO) *system.SystemParameter {
|
||||
return parameterFromPO(po)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"kra/internal/biz/system"
|
||||
"kra/pkg/database/pagination"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
@@ -83,10 +84,7 @@ func (r *positionRepo) ListPositions(ctx context.Context, page, size int, q *sys
|
||||
return nil, 0, err
|
||||
}
|
||||
var pos []positionPO
|
||||
// The compatible position endpoint applies the supplied LIMIT/OFFSET
|
||||
// directly (it does not normalize missing page values through the shared
|
||||
// pagination helper).
|
||||
if err := db.Order("sort").Limit(size).Offset(size * (page - 1)).Find(&pos).Error; err != nil {
|
||||
if err := pagination.ApplyRequired(db.Order("sort"), page, size, 100).Find(&pos).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
out := make([]*system.Position, 0, len(pos))
|
||||
@@ -95,6 +93,7 @@ func (r *positionRepo) ListPositions(ctx context.Context, page, size int, q *sys
|
||||
}
|
||||
return out, total, nil
|
||||
}
|
||||
|
||||
func (r *positionRepo) PositionUserIDs(ctx context.Context, id uint) ([]uint, error) {
|
||||
var ids []uint
|
||||
err := r.data.DB().WithContext(ctx).Model(&userPositionPO{}).Where("sys_position_id = ?", id).Pluck("sys_user_id", &ids).Error
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func deletedAtPointer(value gorm.DeletedAt) *time.Time {
|
||||
if !value.Valid {
|
||||
return nil
|
||||
}
|
||||
result := value.Time
|
||||
return &result
|
||||
}
|
||||
@@ -37,7 +37,7 @@ func generateToken(secret, issuer string, expires, buffer time.Duration, userID,
|
||||
}
|
||||
now := time.Now()
|
||||
claims := &tokenClaims{UUID: uuid, ID: userID, Username: username, NickName: nickname, AuthorityID: authorityID, BufferTime: int64(buffer / time.Second), UserType: "admin", MustChangePwd: mustChange, PasswordVersion: passwordVersion, RegisteredClaims: jwt.RegisteredClaims{Audience: jwt.ClaimStrings{tokenAudience}, Issuer: issuer, IssuedAt: jwt.NewNumericDate(now), NotBefore: jwt.NewNumericDate(now.Add(-time.Second)), ExpiresAt: jwt.NewNumericDate(now.Add(expires))}}
|
||||
token, err := signToken(secret, claims)
|
||||
token, err := signTokenUnchecked(secret, claims)
|
||||
return token, claims, err
|
||||
}
|
||||
|
||||
@@ -48,6 +48,10 @@ func signToken(secret string, claims *tokenClaims) (string, error) {
|
||||
if claims == nil {
|
||||
return "", errInvalidClaims
|
||||
}
|
||||
return signTokenUnchecked(secret, claims)
|
||||
}
|
||||
|
||||
func signTokenUnchecked(secret string, claims *tokenClaims) (string, error) {
|
||||
return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(secret))
|
||||
}
|
||||
|
||||
|
||||
@@ -97,12 +97,7 @@ func (r *userRepo) loadUser(ctx context.Context, po *userPO) (*system.User, erro
|
||||
}
|
||||
|
||||
func toBizAuthority(po authorityPO) system.Authority {
|
||||
var deletedAt *time.Time
|
||||
if po.DeletedAt.Valid {
|
||||
value := po.DeletedAt.Time
|
||||
deletedAt = &value
|
||||
}
|
||||
return system.Authority{CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, DeletedAt: deletedAt, AuthorityID: po.AuthorityID, AuthorityName: po.AuthorityName, ParentID: po.ParentID, DataScope: po.DataScope, DefaultRouter: po.DefaultRouter}
|
||||
return system.Authority{CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, DeletedAt: deletedAtPointer(po.DeletedAt), AuthorityID: po.AuthorityID, AuthorityName: po.AuthorityName, ParentID: po.ParentID, DataScope: po.DataScope, DefaultRouter: po.DefaultRouter}
|
||||
}
|
||||
|
||||
func parseOriginSetting(raw string) (map[string]any, error) {
|
||||
|
||||
@@ -82,7 +82,7 @@ func (a *alipayAdapter) Create(ctx context.Context, req *bizpayment.PaymentReque
|
||||
case "10003":
|
||||
status = "pending"
|
||||
default:
|
||||
return nil, fmt.Errorf("支付宝付款码支付失败: %s", firstNonEmptyAlipay(response.SubMsg, response.Msg, response.SubCode, response.Code))
|
||||
return nil, fmt.Errorf("支付宝付款码支付失败: %s", firstNonEmpty(response.SubMsg, response.Msg, response.SubCode, response.Code))
|
||||
}
|
||||
if strings.TrimSpace(response.OutTradeNo) == "" {
|
||||
return nil, errors.New("支付宝付款码支付响应缺少 out_trade_no")
|
||||
@@ -125,7 +125,7 @@ func (a *alipayAdapter) Create(ctx context.Context, req *bizpayment.PaymentReque
|
||||
}
|
||||
return &bizpayment.PaymentResult{
|
||||
Provider: bizpayment.PaymentAlipay, Status: "created", TradeNo: req.TradeNo,
|
||||
Payload: mustMarshalAlipayPayload(map[string]string{"order_string": orderString}),
|
||||
Payload: mustJSON(map[string]string{"order_string": orderString}),
|
||||
}, nil
|
||||
case "alipay.trade.page.pay":
|
||||
payURL, callErr := sdk.client.TradePagePay(ctx, bm)
|
||||
@@ -141,7 +141,7 @@ func (a *alipayAdapter) Create(ctx context.Context, req *bizpayment.PaymentReque
|
||||
}
|
||||
return &bizpayment.PaymentResult{
|
||||
Provider: bizpayment.PaymentAlipay, Status: "created", TradeNo: req.TradeNo,
|
||||
Payload: mustMarshalAlipayPayload(map[string]string{"pay_url": payURL}),
|
||||
Payload: mustJSON(map[string]string{"pay_url": payURL}),
|
||||
}, nil
|
||||
case "alipay.trade.wap.pay":
|
||||
payURL, callErr := sdk.client.TradeWapPay(ctx, bm)
|
||||
@@ -157,7 +157,7 @@ func (a *alipayAdapter) Create(ctx context.Context, req *bizpayment.PaymentReque
|
||||
}
|
||||
return &bizpayment.PaymentResult{
|
||||
Provider: bizpayment.PaymentAlipay, Status: "created", TradeNo: req.TradeNo,
|
||||
Payload: mustMarshalAlipayPayload(map[string]string{"pay_url": payURL}),
|
||||
Payload: mustJSON(map[string]string{"pay_url": payURL}),
|
||||
}, nil
|
||||
default:
|
||||
rsp, callErr := sdk.client.TradeCreate(ctx, bm)
|
||||
@@ -214,7 +214,7 @@ func (a *alipayAdapter) Query(ctx context.Context, tradeNo string, c map[string]
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析支付宝订单金额: %w", err)
|
||||
}
|
||||
result.Currency = strings.ToUpper(firstNonEmptyAlipay(response.TransCurrency, response.PayCurrency))
|
||||
result.Currency = strings.ToUpper(firstNonEmpty(response.TransCurrency, response.PayCurrency))
|
||||
if result.Currency == "" {
|
||||
result.Currency = "CNY"
|
||||
}
|
||||
@@ -383,7 +383,7 @@ func (c *alipayClient) payload(fallback any) []byte {
|
||||
if c.response.Len() > 0 {
|
||||
return append([]byte(nil), c.response.Bytes()...)
|
||||
}
|
||||
return mustMarshalAlipayPayload(fallback)
|
||||
return mustJSON(fallback)
|
||||
}
|
||||
|
||||
func (c *alipayClient) rewriteGeneratedURL(value string) (string, error) {
|
||||
@@ -466,20 +466,6 @@ func alipayResponseValues(value any) (map[string]any, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func mustMarshalAlipayPayload(value any) []byte {
|
||||
raw, _ := json.Marshal(value)
|
||||
return raw
|
||||
}
|
||||
|
||||
func firstNonEmptyAlipay(values ...string) string {
|
||||
for _, value := range values {
|
||||
if value = strings.TrimSpace(value); value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func populateAlipayBreakdown(result *bizpayment.PaymentResult, response map[string]any) error {
|
||||
if result == nil || result.Status != "success" {
|
||||
return nil
|
||||
|
||||
@@ -688,15 +688,6 @@ func paypalNestedMapString(values map[string]any, keys ...string) string {
|
||||
return strings.TrimSpace(fmt.Sprint(current))
|
||||
}
|
||||
|
||||
func firstNonEmpty(values ...string) string {
|
||||
for _, value := range values {
|
||||
if value = strings.TrimSpace(value); value != "" && value != "<nil>" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func paypalResponseError(operation string, code int, detail string) error {
|
||||
detail = strings.TrimSpace(detail)
|
||||
if detail == "" {
|
||||
|
||||
@@ -103,7 +103,7 @@ func (a *qqAdapter) Create(ctx context.Context, req *bizpayment.PaymentRequest,
|
||||
if status == "pending" && strings.EqualFold(rsp.ResultCode, "SUCCESS") && strings.TrimSpace(rsp.TransactionId) != "" {
|
||||
status = "success"
|
||||
}
|
||||
return &bizpayment.PaymentResult{Provider: bizpayment.PaymentQQ, Status: status, TradeNo: firstNonEmptyQQ(rsp.OutTradeNo, req.TradeNo), ProviderTradeNo: strings.TrimSpace(rsp.TransactionId), Payload: mustJSON(rsp)}, nil
|
||||
return &bizpayment.PaymentResult{Provider: bizpayment.PaymentQQ, Status: status, TradeNo: firstNonEmpty(rsp.OutTradeNo, req.TradeNo), ProviderTradeNo: strings.TrimSpace(rsp.TransactionId), Payload: mustJSON(rsp)}, nil
|
||||
}
|
||||
bm.Set("trade_type", tradeType)
|
||||
mergeGoPayExtras(bm, req.Extra, "nonce_str", "body", "out_trade_no", "total_fee", "spbill_create_ip", "notify_url", "trade_type", "mch_id", "sign_type")
|
||||
@@ -143,15 +143,6 @@ func qqCreateMethod(extra, config map[string]any) (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func firstNonEmptyQQ(values ...string) string {
|
||||
for _, value := range values {
|
||||
if strings.TrimSpace(value) != "" {
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (a *qqAdapter) Query(ctx context.Context, tradeNo string, c map[string]any) (*bizpayment.PaymentResult, error) {
|
||||
tradeNo = strings.TrimSpace(tradeNo)
|
||||
if tradeNo == "" {
|
||||
|
||||
@@ -1,55 +1,29 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
bizpayment "kra/internal/biz/payment"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"kra/internal/paymentkit"
|
||||
)
|
||||
|
||||
func callbackFields(callback *bizpayment.PaymentCallback) map[string]string {
|
||||
fields := map[string]string{}
|
||||
if callback == nil {
|
||||
return fields
|
||||
return map[string]string{}
|
||||
}
|
||||
for key, value := range callback.Query {
|
||||
fields[key] = value
|
||||
}
|
||||
contentType := ""
|
||||
for key, value := range callback.Headers {
|
||||
if strings.EqualFold(key, "Content-Type") {
|
||||
contentType = strings.ToLower(value)
|
||||
break
|
||||
}
|
||||
}
|
||||
if strings.Contains(contentType, "application/x-www-form-urlencoded") {
|
||||
if values, err := url.ParseQuery(string(callback.Body)); err == nil {
|
||||
for key, value := range values {
|
||||
if len(value) > 0 {
|
||||
fields[key] = value[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var object map[string]any
|
||||
if json.Unmarshal(callback.Body, &object) == nil {
|
||||
for key, value := range object {
|
||||
if stringValue, ok := value.(string); ok {
|
||||
fields[key] = stringValue
|
||||
}
|
||||
}
|
||||
}
|
||||
return fields
|
||||
return paymentkit.CallbackFields(callback.Query, callback.Headers, callback.Body)
|
||||
}
|
||||
|
||||
func first(values map[string]string, keys ...string) string {
|
||||
for _, key := range keys {
|
||||
if values[key] != "" {
|
||||
return values[key]
|
||||
return paymentkit.FirstString(values, keys...)
|
||||
}
|
||||
|
||||
func firstNonEmpty(values ...string) string {
|
||||
for _, value := range values {
|
||||
if value = strings.TrimSpace(value); value != "" && value != "<nil>" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
@@ -69,6 +43,10 @@ func normalizePaymentStatus(value, fallback string) string {
|
||||
return paymentkit.NormalizeStatus(value, fallback)
|
||||
}
|
||||
|
||||
func normalizeRefundStatus(value, fallback string) string {
|
||||
return paymentkit.NormalizeRefundStatus(value, fallback)
|
||||
}
|
||||
|
||||
func parseIntegerAmount(value string) (int64, error) {
|
||||
return paymentkit.ParseIntegerAmount(value)
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ func (a *vendorPaymentAdapter) call(ctx context.Context, endpointKey string, pay
|
||||
result.Status = "created"
|
||||
default:
|
||||
if operation == "refund" {
|
||||
normalized := normalizePaymentStatus(state, "")
|
||||
normalized := normalizeRefundStatus(state, "")
|
||||
switch normalized {
|
||||
case "success", "pending", "failed":
|
||||
result.Status = normalized
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
mqintegration "kra/internal/integration/mq"
|
||||
paymentintegration "kra/internal/integration/payment"
|
||||
"kra/internal/integration/storage"
|
||||
"kra/internal/integration/systeminfo"
|
||||
websocketintegration "kra/internal/integration/websocket"
|
||||
"kra/pkg/mq"
|
||||
platformws "kra/pkg/websocket"
|
||||
@@ -25,6 +26,8 @@ var ProviderSet = wire.NewSet(
|
||||
paymentintegration.NewFactory,
|
||||
wire.Bind(new(bizpayment.PaymentAdapterFactory), new(*paymentintegration.Factory)),
|
||||
storage.NewFileStorage,
|
||||
systeminfo.NewProvider,
|
||||
wire.Bind(new(system.SystemInfoProvider), new(*systeminfo.Provider)),
|
||||
NewConnectivityTester,
|
||||
wire.Bind(new(integrationbiz.IntegrationConnectionTester), new(*ConnectivityTester)),
|
||||
mqintegration.New,
|
||||
|
||||
@@ -69,29 +69,7 @@ func (s *aliyunStorage) Compose(ctx context.Context, names []string, destination
|
||||
return composeFiles(ctx, s, names, destination)
|
||||
}
|
||||
func (s *aliyunStorage) DeletePrefix(ctx context.Context, prefix string) error {
|
||||
prefix, err := normalizeDeletePrefix(prefix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cursor := ""
|
||||
for {
|
||||
items, next, more, err := s.List(ctx, prefix+"/", cursor, 1000)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, item := range items {
|
||||
if err = s.Delete(ctx, item.Path); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !more {
|
||||
return nil
|
||||
}
|
||||
cursor, err = advanceDeletePrefixCursor(cursor, next, more)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return deletePrefixViaList(ctx, prefix, s.List, s.Delete)
|
||||
}
|
||||
func (s *aliyunStorage) List(_ context.Context, prefix, cursor string, limit int) ([]*system.StoredFile, string, bool, error) {
|
||||
if limit <= 0 {
|
||||
|
||||
@@ -106,29 +106,7 @@ func (s *awsStorage) Compose(ctx context.Context, names []string, destination st
|
||||
return composeFiles(ctx, s, names, destination)
|
||||
}
|
||||
func (s *awsStorage) DeletePrefix(ctx context.Context, prefix string) error {
|
||||
prefix, err := normalizeDeletePrefix(prefix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cursor := ""
|
||||
for {
|
||||
items, next, more, err := s.List(ctx, prefix+"/", cursor, 1000)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, item := range items {
|
||||
if err = s.Delete(ctx, item.Path); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !more {
|
||||
return nil
|
||||
}
|
||||
cursor, err = advanceDeletePrefixCursor(cursor, next, more)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return deletePrefixViaList(ctx, prefix, s.List, s.Delete)
|
||||
}
|
||||
func (s *awsStorage) List(ctx context.Context, prefix, cursor string, limit int) ([]*system.StoredFile, string, bool, error) {
|
||||
if limit <= 0 {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"io"
|
||||
"kra/internal/biz/system"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -117,3 +118,38 @@ func TestAdvanceDeletePrefixCursor(t *testing.T) {
|
||||
t.Fatalf("advance cursor = %q, %v", next, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeletePrefixViaListDeletesAllPages(t *testing.T) {
|
||||
pages := map[string][]*system.StoredFile{
|
||||
"": {{Path: "uploads/a"}, {Path: "uploads/b"}},
|
||||
"next": {{Path: "uploads/c"}},
|
||||
}
|
||||
removed := []string{}
|
||||
if err := deletePrefixViaList(context.Background(), "uploads", func(_ context.Context, prefix, cursor string, limit int) ([]*system.StoredFile, string, bool, error) {
|
||||
if prefix != "uploads/" || limit != 1000 {
|
||||
t.Fatalf("list arguments = %q, %q, %d", prefix, cursor, limit)
|
||||
}
|
||||
items := pages[cursor]
|
||||
if cursor == "" {
|
||||
return items, "next", true, nil
|
||||
}
|
||||
return items, "", false, nil
|
||||
}, func(_ context.Context, path string) error {
|
||||
removed = append(removed, path)
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(removed) != 3 || removed[2] != "uploads/c" {
|
||||
t.Fatalf("removed paths = %#v", removed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeletePrefixViaListRejectsStalledCursor(t *testing.T) {
|
||||
err := deletePrefixViaList(context.Background(), "uploads", func(_ context.Context, _, _ string, _ int) ([]*system.StoredFile, string, bool, error) {
|
||||
return nil, "", true, nil
|
||||
}, func(context.Context, string) error { return nil })
|
||||
if err == nil || !strings.Contains(err.Error(), "no progress") {
|
||||
t.Fatalf("stalled pagination error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,29 +68,7 @@ func (s *huaweiStorage) Compose(ctx context.Context, names []string, destination
|
||||
return composeFiles(ctx, s, names, destination)
|
||||
}
|
||||
func (s *huaweiStorage) DeletePrefix(ctx context.Context, prefix string) error {
|
||||
prefix, err := normalizeDeletePrefix(prefix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cursor := ""
|
||||
for {
|
||||
items, next, more, err := s.List(ctx, prefix+"/", cursor, 1000)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, item := range items {
|
||||
if err = s.Delete(ctx, item.Path); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !more {
|
||||
return nil
|
||||
}
|
||||
cursor, err = advanceDeletePrefixCursor(cursor, next, more)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return deletePrefixViaList(ctx, prefix, s.List, s.Delete)
|
||||
}
|
||||
func (s *huaweiStorage) List(_ context.Context, prefix, cursor string, limit int) ([]*system.StoredFile, string, bool, error) {
|
||||
if limit <= 0 {
|
||||
|
||||
@@ -1,11 +1,39 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"kra/internal/biz/system"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func deletePrefixViaList(ctx context.Context, prefix string, list func(context.Context, string, string, int) ([]*system.StoredFile, string, bool, error), remove func(context.Context, string) error) error {
|
||||
prefix, err := normalizeDeletePrefix(prefix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cursor := ""
|
||||
for {
|
||||
items, next, more, err := list(ctx, prefix+"/", cursor, 1000)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, item := range items {
|
||||
if err := remove(ctx, item.Path); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !more {
|
||||
return nil
|
||||
}
|
||||
cursor, err = advanceDeletePrefixCursor(cursor, next, more)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// normalizeDeletePrefix rejects requests that could accidentally target the
|
||||
// storage root and returns the canonical key form used by all backends.
|
||||
func normalizeDeletePrefix(prefix string) (string, error) {
|
||||
|
||||
@@ -99,29 +99,7 @@ func (s *qiniuStorage) Compose(ctx context.Context, names []string, destination
|
||||
return composeFiles(ctx, s, names, destination)
|
||||
}
|
||||
func (s *qiniuStorage) DeletePrefix(ctx context.Context, prefix string) error {
|
||||
prefix, err := normalizeDeletePrefix(prefix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cursor := ""
|
||||
for {
|
||||
items, next, more, err := s.List(ctx, prefix+"/", cursor, 1000)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, item := range items {
|
||||
if err = s.Delete(ctx, item.Path); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !more {
|
||||
return nil
|
||||
}
|
||||
cursor, err = advanceDeletePrefixCursor(cursor, next, more)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return deletePrefixViaList(ctx, prefix, s.List, s.Delete)
|
||||
}
|
||||
func (s *qiniuStorage) List(ctx context.Context, prefix, cursor string, limit int) ([]*system.StoredFile, string, bool, error) {
|
||||
if limit <= 0 {
|
||||
|
||||
@@ -82,29 +82,7 @@ func (s *tencentStorage) Compose(ctx context.Context, names []string, destinatio
|
||||
return composeFiles(ctx, s, names, destination)
|
||||
}
|
||||
func (s *tencentStorage) DeletePrefix(ctx context.Context, prefix string) error {
|
||||
prefix, err := normalizeDeletePrefix(prefix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cursor := ""
|
||||
for {
|
||||
items, next, more, err := s.List(ctx, prefix+"/", cursor, 1000)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, item := range items {
|
||||
if err = s.Delete(ctx, item.Path); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !more {
|
||||
return nil
|
||||
}
|
||||
cursor, err = advanceDeletePrefixCursor(cursor, next, more)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return deletePrefixViaList(ctx, prefix, s.List, s.Delete)
|
||||
}
|
||||
func (s *tencentStorage) List(ctx context.Context, prefix, cursor string, limit int) ([]*system.StoredFile, string, bool, error) {
|
||||
if limit <= 0 {
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package systeminfo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"kra/internal/biz/system"
|
||||
|
||||
"github.com/shirou/gopsutil/v4/cpu"
|
||||
"github.com/shirou/gopsutil/v4/disk"
|
||||
"github.com/shirou/gopsutil/v4/mem"
|
||||
)
|
||||
|
||||
type Provider struct{}
|
||||
|
||||
func NewProvider() *Provider { return &Provider{} }
|
||||
|
||||
func (*Provider) Collect(ctx context.Context, mountPoints []string) (*system.ServerInfo, error) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
physicalCores, err := cpu.Counts(false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cpuPercent, err := cpu.PercentWithContext(ctx, 200*time.Millisecond, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
memory, err := mem.VirtualMemoryWithContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
diskInfo := make([]system.ServerDiskInfo, 0, len(mountPoints))
|
||||
for _, mountPoint := range mountPoints {
|
||||
if err = ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
usage, usageErr := disk.UsageWithContext(ctx, mountPoint)
|
||||
if usageErr != nil {
|
||||
return nil, usageErr
|
||||
}
|
||||
diskInfo = append(diskInfo, system.ServerDiskInfo{MountPoint: mountPoint, UsedMB: usage.Used / 1024 / 1024, UsedGB: usage.Used / 1024 / 1024 / 1024, TotalMB: usage.Total / 1024 / 1024, TotalGB: usage.Total / 1024 / 1024 / 1024, UsedPercent: int(usage.UsedPercent)})
|
||||
}
|
||||
return &system.ServerInfo{GOOS: runtime.GOOS, NumCPU: runtime.NumCPU(), Compiler: runtime.Compiler, GoVersion: runtime.Version(), NumGoroutine: runtime.NumGoroutine(), CPUs: cpuPercent, Cores: physicalCores, UsedMB: memory.Used / 1024 / 1024, TotalMB: memory.Total / 1024 / 1024, UsedPercent: int(memory.UsedPercent), Disk: diskInfo}, nil
|
||||
}
|
||||
+53
-12
@@ -6,9 +6,12 @@ import (
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type stackFrame struct {
|
||||
@@ -19,6 +22,23 @@ type stackFrame struct {
|
||||
|
||||
var stackFileLinePattern = regexp.MustCompile(`\s*(.+\.go):(\d+)\s*$`)
|
||||
|
||||
type sourceFunction struct {
|
||||
name string
|
||||
source string
|
||||
startLine, endLine int
|
||||
}
|
||||
|
||||
type sourceFileCache struct {
|
||||
size int64
|
||||
modTime time.Time
|
||||
items []sourceFunction
|
||||
}
|
||||
|
||||
var sourceCache = struct {
|
||||
sync.RWMutex
|
||||
files map[string]sourceFileCache
|
||||
}{files: make(map[string]sourceFileCache)}
|
||||
|
||||
func finalApplicationCaller(stack string) (stackFrame, bool) {
|
||||
if stack == "" {
|
||||
return stackFrame{}, false
|
||||
@@ -69,6 +89,17 @@ func skipStackFile(filename string) bool {
|
||||
}
|
||||
|
||||
func functionSourceAt(filename string, line int) (name, source string, startLine, endLine int, err error) {
|
||||
filename = filepath.Clean(filename)
|
||||
info, err := os.Stat(filename)
|
||||
if err != nil {
|
||||
return "", "", 0, 0, fmt.Errorf("stat file failed: %w", err)
|
||||
}
|
||||
sourceCache.RLock()
|
||||
cached, found := sourceCache.files[filename]
|
||||
sourceCache.RUnlock()
|
||||
if found && cached.size == info.Size() && cached.modTime.Equal(info.ModTime()) {
|
||||
return sourceFunctionAtLine(cached.items, line, filename)
|
||||
}
|
||||
content, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return "", "", 0, 0, fmt.Errorf("read file failed: %w", err)
|
||||
@@ -78,7 +109,15 @@ func functionSourceAt(filename string, line int) (name, source string, startLine
|
||||
if err != nil {
|
||||
return "", "", 0, 0, fmt.Errorf("parse file failed: %w", err)
|
||||
}
|
||||
var target *ast.FuncDecl
|
||||
sourceCache.Lock()
|
||||
sourceCache.files[filename] = sourceFileCache{size: info.Size(), modTime: info.ModTime(), items: allFunctions(parsed, files, content)}
|
||||
cached = sourceCache.files[filename]
|
||||
sourceCache.Unlock()
|
||||
return sourceFunctionAtLine(cached.items, line, filename)
|
||||
}
|
||||
|
||||
func allFunctions(parsed *ast.File, files *token.FileSet, content []byte) []sourceFunction {
|
||||
items := make([]sourceFunction, 0)
|
||||
ast.Inspect(parsed, func(node ast.Node) bool {
|
||||
declaration, ok := node.(*ast.FuncDecl)
|
||||
if !ok {
|
||||
@@ -86,19 +125,21 @@ func functionSourceAt(filename string, line int) (name, source string, startLine
|
||||
}
|
||||
start := files.Position(declaration.Pos()).Line
|
||||
end := files.Position(declaration.End()).Line
|
||||
if line >= start && line <= end {
|
||||
target, startLine, endLine = declaration, start, end
|
||||
return false
|
||||
startOffset := files.Position(declaration.Pos()).Offset
|
||||
endOffset := files.Position(declaration.End()).Offset
|
||||
if startOffset >= 0 && endOffset <= len(content) && startOffset < endOffset {
|
||||
items = append(items, sourceFunction{name: declaration.Name.Name, source: string(content[startOffset:endOffset]), startLine: start, endLine: end})
|
||||
}
|
||||
return true
|
||||
})
|
||||
if target == nil {
|
||||
return "", "", 0, 0, fmt.Errorf("no function encloses line %d in %s", line, filename)
|
||||
return items
|
||||
}
|
||||
|
||||
func sourceFunctionAtLine(items []sourceFunction, line int, filename string) (string, string, int, int, error) {
|
||||
for _, item := range items {
|
||||
if line >= item.startLine && line <= item.endLine {
|
||||
return item.name, item.source, item.startLine, item.endLine, nil
|
||||
}
|
||||
}
|
||||
start := files.Position(target.Pos()).Offset
|
||||
end := files.Position(target.End()).Offset
|
||||
if start < 0 || end > len(content) || start >= end {
|
||||
return "", "", 0, 0, fmt.Errorf("invalid offsets for function: start=%d end=%d len=%d", start, end, len(content))
|
||||
}
|
||||
return target.Name.Name, string(content[start:end]), startLine, endLine, nil
|
||||
return "", "", 0, 0, fmt.Errorf("no function encloses line %d in %s", line, filename)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,28 @@
|
||||
package paymentkit
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func NormalizeStatus(value, fallback string) string {
|
||||
switch strings.ToUpper(strings.TrimSpace(value)) {
|
||||
case "SUCCESS", "OK", "PAID", "TRADE_SUCCESS", "TRADE_FINISHED", "COMPLETED", "PAY_SUCCESS", "REFUND_SUCCESS", "REFUNDED":
|
||||
case "SUCCESS", "OK", "PAID", "FULFILLED", "TRADE_SUCCESS", "TRADE_FINISHED", "COMPLETED", "PAY_SUCCESS", "REFUND_SUCCESS", "REFUNDED":
|
||||
return "success"
|
||||
case "WAIT_BUYER_PAY", "USERPAYING", "NOTPAY", "PROCESSING", "PENDING", "CREATED", "CLIENT_PENDING", "ACCEPT", "ACCEPTED", "PAYING":
|
||||
return "pending"
|
||||
case "CLOSED", "TRADE_CLOSED", "CANCELLED", "CANCELED", "REVOKED", "REFUND", "FAILED", "FAIL", "REJECTED", "DENIED", "DECLINED", "ERROR", "PAYERROR":
|
||||
return "failed"
|
||||
default:
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
|
||||
func NormalizeRefundStatus(value, fallback string) string {
|
||||
switch strings.ToUpper(strings.TrimSpace(value)) {
|
||||
case "SUCCESS", "OK", "PAID", "REFUND_SUCCESS", "REFUNDED", "COMPLETED", "FINISHED":
|
||||
return "success"
|
||||
case "WAIT_BUYER_PAY", "USERPAYING", "NOTPAY", "PROCESSING", "PENDING", "CREATED", "ACCEPT", "ACCEPTED", "PAYING":
|
||||
return "pending"
|
||||
@@ -65,6 +80,49 @@ func FirstText(values map[string]any, keys ...string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func FirstString(values map[string]string, keys ...string) string {
|
||||
for _, key := range keys {
|
||||
if values[key] != "" {
|
||||
return values[key]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// CallbackFields merges query parameters, form data, and top-level JSON
|
||||
// strings using the same precedence for every payment boundary.
|
||||
func CallbackFields(query, headers map[string]string, body []byte) map[string]string {
|
||||
fields := make(map[string]string, len(query))
|
||||
for key, value := range query {
|
||||
fields[key] = value
|
||||
}
|
||||
contentType := ""
|
||||
for key, value := range headers {
|
||||
if strings.EqualFold(key, "Content-Type") {
|
||||
contentType = strings.ToLower(value)
|
||||
break
|
||||
}
|
||||
}
|
||||
if strings.Contains(contentType, "application/x-www-form-urlencoded") {
|
||||
if values, err := url.ParseQuery(string(body)); err == nil {
|
||||
for key, value := range values {
|
||||
if len(value) > 0 {
|
||||
fields[key] = value[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var object map[string]any
|
||||
if json.Unmarshal(body, &object) == nil {
|
||||
for key, value := range object {
|
||||
if text, ok := value.(string); ok {
|
||||
fields[key] = text
|
||||
}
|
||||
}
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
func toString(value any) string {
|
||||
if value == nil {
|
||||
return ""
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package paymentkit
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNormalizeStatusCoversCanonicalPaymentStates(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
input, want string
|
||||
}{
|
||||
{input: "fulfilled", want: "success"},
|
||||
{input: "client_pending", want: "pending"},
|
||||
{input: "trade_closed", want: "failed"},
|
||||
} {
|
||||
if got := NormalizeStatus(test.input, "unknown"); got != test.want {
|
||||
t.Fatalf("NormalizeStatus(%q) = %q, want %q", test.input, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeStatusSeparatesRefundStates(t *testing.T) {
|
||||
if got := NormalizeStatus("REFUND_SUCCESS", "unknown"); got != "success" {
|
||||
t.Fatalf("payment status for completed payment = %q, want success", got)
|
||||
}
|
||||
if got := NormalizeRefundStatus("REFUND_SUCCESS", "unknown"); got != "success" {
|
||||
t.Fatalf("refund status = %q, want success", got)
|
||||
}
|
||||
}
|
||||
@@ -210,7 +210,7 @@ func (h *Dictionary) Details(c *gin.Context) {
|
||||
func (h *Dictionary) Tree(byType bool) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
rawID := c.Query("sysDictionaryID")
|
||||
id, parseErr := strconv.ParseUint(rawID, 10, 32)
|
||||
var id uint64
|
||||
typ := ""
|
||||
if byType {
|
||||
typ = c.Query("type")
|
||||
@@ -221,9 +221,13 @@ func (h *Dictionary) Tree(byType bool) gin.HandlerFunc {
|
||||
} else if rawID == "" {
|
||||
Fail(c, "字典ID不能为空")
|
||||
return
|
||||
} else if parseErr != nil {
|
||||
Fail(c, "字典ID格式错误")
|
||||
return
|
||||
} else {
|
||||
var parseErr error
|
||||
id, parseErr = strconv.ParseUint(rawID, 10, 32)
|
||||
if parseErr != nil {
|
||||
Fail(c, "字典ID格式错误")
|
||||
return
|
||||
}
|
||||
}
|
||||
items, err := h.service.DictionaryTree(c.Request.Context(), uint(id), typ)
|
||||
if err != nil {
|
||||
|
||||
@@ -27,29 +27,20 @@ func NewPublic(auth *systemservice.AuthService, system *systemservice.SystemConf
|
||||
return &Public{auth: auth, system: system, settings: settings, store: &captchaStore{service: settings}}
|
||||
}
|
||||
|
||||
func (h *Public) captchaConfig() (int, int, int) {
|
||||
config := h.settings.CaptchaSettings()
|
||||
return config.KeyLong, config.ImageWidth, config.ImageHeight
|
||||
}
|
||||
|
||||
func (h *Public) Captcha(c *gin.Context) {
|
||||
keyLong, width, height := h.captchaConfig()
|
||||
security, err := h.settings.CurrentSecurity(c.Request.Context())
|
||||
if err != nil || security == nil {
|
||||
Fail(c, "安全服务暂不可用")
|
||||
return
|
||||
}
|
||||
openCaptcha := true
|
||||
if security != nil {
|
||||
keyLong, width, height = security.KeyLong, security.ImgWidth, security.ImgHeight
|
||||
ttl := time.Duration(security.CaptchaTimeout) * time.Second
|
||||
failures, counterErr := h.settings.EnsureLoginIPCounter(c.Request.Context(), c.ClientIP(), ttl)
|
||||
if counterErr != nil {
|
||||
Fail(c, "安全服务暂不可用")
|
||||
return
|
||||
}
|
||||
openCaptcha = security.CaptchaOpen == 0 || failures > security.CaptchaOpen
|
||||
keyLong, width, height := security.KeyLong, security.ImgWidth, security.ImgHeight
|
||||
ttl := time.Duration(security.CaptchaTimeout) * time.Second
|
||||
failures, counterErr := h.settings.EnsureLoginIPCounter(c.Request.Context(), c.ClientIP(), ttl)
|
||||
if counterErr != nil {
|
||||
Fail(c, "安全服务暂不可用")
|
||||
return
|
||||
}
|
||||
openCaptcha := security.CaptchaOpen == 0 || failures > security.CaptchaOpen
|
||||
driver := base64Captcha.NewDriverDigit(height, width, keyLong, 0.7, 80)
|
||||
id, picture, _, err := base64Captcha.NewCaptcha(driver, h.store).Generate()
|
||||
if err != nil {
|
||||
|
||||
@@ -81,7 +81,7 @@ func (h *SystemConfig) Reload(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *SystemConfig) ServerInfo(c *gin.Context) {
|
||||
server, err := h.system.ServerInfo()
|
||||
server, err := h.system.ServerInfo(c.Request.Context())
|
||||
if err != nil {
|
||||
Fail(c, "获取失败")
|
||||
return
|
||||
|
||||
@@ -87,8 +87,22 @@ func OperationAudit(runtime *config.Store, recorder *systemservice.AuditRecorder
|
||||
responseBody = "[超出记录长度]"
|
||||
}
|
||||
errorMessage := c.Errors.ByType(gin.ErrorTypePrivate).String()
|
||||
operationBody := operationRequestBody(requestBody, c.GetHeader("Content-Type"), maxBytes)
|
||||
if routecatalog.BodyPolicyFor(c.Request.Method, path) == routecatalog.BodyPolicyPaymentConfig {
|
||||
operationBody := ""
|
||||
capturedBody := false
|
||||
if c.Request.Method != http.MethodGet {
|
||||
if value, ok := c.Get(ctxReqBodyKey); ok {
|
||||
// AccessLog has already captured, redacted, summarized, and bounded the
|
||||
// request body. Treat that value as final so audit does not process it a
|
||||
// second time.
|
||||
operationBody = stringValue(value)
|
||||
capturedBody = true
|
||||
} else {
|
||||
operationBody = operationRequestBody(requestBody, c.GetHeader("Content-Type"), maxBytes)
|
||||
}
|
||||
} else {
|
||||
operationBody = operationRequestBody(requestBody, c.GetHeader("Content-Type"), maxBytes)
|
||||
}
|
||||
if !capturedBody && routecatalog.BodyPolicyFor(c.Request.Method, path) == routecatalog.BodyPolicyPaymentConfig {
|
||||
operationBody = paymentConfigSummary(requestBody)
|
||||
}
|
||||
if err := recorder.RecordOperationRequest(c.Request.Context(), &dto.OperationRecordRequest{IP: c.ClientIP(), Method: c.Request.Method, Path: path, Status: status, LatencyMS: time.Since(started).Milliseconds(), Agent: c.Request.UserAgent(), ErrorMessage: errorMessage, Body: operationBody, Response: responseBody, UserID: userID, RequestID: stringValue(requestID), TraceID: stringValueFromContext(c, "trace_id"), DeviceID: c.GetHeader("X-Device-Id")}); err != nil {
|
||||
@@ -105,7 +119,11 @@ func operationQueryBody(raw string) []byte {
|
||||
values := make(map[string]string, len(parsed))
|
||||
for key, items := range parsed {
|
||||
if len(items) > 0 {
|
||||
values[key] = items[len(items)-1]
|
||||
value := items[len(items)-1]
|
||||
if isSensitivePayloadKey(key) {
|
||||
value = redactedValue
|
||||
}
|
||||
values[key] = value
|
||||
}
|
||||
}
|
||||
body, _ := json.Marshal(&values)
|
||||
|
||||
@@ -23,6 +23,16 @@ func TestOperationQueryBodyPreservesEncodedSeparators(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestOperationQueryBodyRedactsSensitiveValues(t *testing.T) {
|
||||
var values map[string]string
|
||||
if err := json.Unmarshal(operationQueryBody("token=secret-token&name=alice"), &values); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if values["token"] != redactedValue || values["name"] != "alice" {
|
||||
t.Fatalf("unexpected redacted query body: %#v", values)
|
||||
}
|
||||
}
|
||||
|
||||
func runErrorAudit(t *testing.T, path, response string, logger *slog.Logger) {
|
||||
t.Helper()
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
@@ -27,14 +27,6 @@ func authorityDomain(req *dto.AuthorityRequest) *system.Authority {
|
||||
return &system.Authority{AuthorityID: req.AuthorityID, AuthorityName: req.AuthorityName, ParentID: req.ParentID, DataScope: req.DataScope, DefaultRouter: req.DefaultRouter}
|
||||
}
|
||||
|
||||
func authorityResponse(value *system.Authority) *dto.AuthorityResponse {
|
||||
var menus []*dto.MenuResponse
|
||||
if value.Menus != nil {
|
||||
menus = menuResponses(value.Menus)
|
||||
}
|
||||
return &dto.AuthorityResponse{CreatedAt: value.CreatedAt, UpdatedAt: value.UpdatedAt, DeletedAt: value.DeletedAt, AuthorityID: value.AuthorityID, AuthorityName: value.AuthorityName, ParentID: value.ParentID, Children: nil, Menus: menus, DataScope: value.DataScope, DefaultRouter: value.DefaultRouter}
|
||||
}
|
||||
|
||||
func (s *AuthorityService) Authorities(ctx context.Context) ([]*dto.AuthorityResponse, error) {
|
||||
items, err := s.uc.AuthorityTree(ctx)
|
||||
if err != nil {
|
||||
@@ -52,7 +44,7 @@ func (s *AuthorityService) CreateAuthorityRequest(ctx context.Context, req *dto.
|
||||
if err := s.uc.CreateAuthority(ctx, value); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return authorityResponse(value), nil
|
||||
return convertAuthority(*value), nil
|
||||
}
|
||||
|
||||
func (s *AuthorityService) CopyAuthorityRequest(ctx context.Context, req *dto.CopyAuthorityRequest) (*dto.AuthorityResponse, error) {
|
||||
@@ -60,7 +52,7 @@ func (s *AuthorityService) CopyAuthorityRequest(ctx context.Context, req *dto.Co
|
||||
if err := s.uc.CopyAuthority(ctx, req.OldAuthorityID, value); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := authorityResponse(value)
|
||||
result := convertAuthority(*value)
|
||||
result.Children = []*dto.AuthorityResponse{}
|
||||
return result, nil
|
||||
}
|
||||
@@ -70,7 +62,7 @@ func (s *AuthorityService) UpdateAuthorityRequest(ctx context.Context, req *dto.
|
||||
if err := s.uc.UpdateAuthority(ctx, value); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return authorityResponse(value), nil
|
||||
return convertAuthority(*value), nil
|
||||
}
|
||||
|
||||
func (s *AuthorityService) DeleteAuthority(ctx context.Context, id uint) error {
|
||||
|
||||
@@ -1,42 +1,23 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"context"
|
||||
"kra/internal/service/dto"
|
||||
|
||||
"github.com/shirou/gopsutil/v4/cpu"
|
||||
"github.com/shirou/gopsutil/v4/disk"
|
||||
"github.com/shirou/gopsutil/v4/mem"
|
||||
)
|
||||
|
||||
func (s *SystemConfigService) ServerInfo() (*dto.ServerInfoResponse, error) {
|
||||
physicalCores, err := cpu.Counts(false)
|
||||
func (s *SystemConfigService) ServerInfo(ctx context.Context) (*dto.ServerInfoResponse, error) {
|
||||
value, err := s.uc.ServerInfo(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cpuPercent, err := cpu.Percent(200*time.Millisecond, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
diskInfo := make([]dto.ServerDiskResponse, 0, len(value.Disk))
|
||||
for _, disk := range value.Disk {
|
||||
diskInfo = append(diskInfo, dto.ServerDiskResponse{MountPoint: disk.MountPoint, UsedMB: disk.UsedMB, UsedGB: disk.UsedGB, TotalMB: disk.TotalMB, TotalGB: disk.TotalGB, UsedPercent: disk.UsedPercent})
|
||||
}
|
||||
memory, err := mem.VirtualMemory()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
diskInfo := make([]dto.ServerDiskResponse, 0)
|
||||
for _, mountPoint := range s.DiskMountPoints() {
|
||||
usage, usageErr := disk.Usage(mountPoint)
|
||||
if usageErr != nil {
|
||||
return nil, usageErr
|
||||
}
|
||||
diskInfo = append(diskInfo, dto.ServerDiskResponse{MountPoint: mountPoint, UsedMB: usage.Used / 1024 / 1024, UsedGB: usage.Used / 1024 / 1024 / 1024, TotalMB: usage.Total / 1024 / 1024, TotalGB: usage.Total / 1024 / 1024 / 1024, UsedPercent: int(usage.UsedPercent)})
|
||||
}
|
||||
usedMB, totalMB := memory.Used/1024/1024, memory.Total/1024/1024
|
||||
return &dto.ServerInfoResponse{
|
||||
OS: dto.ServerOSResponse{GOOS: runtime.GOOS, NumCPU: runtime.NumCPU(), Compiler: runtime.Compiler, GoVersion: runtime.Version(), NumGoroutine: runtime.NumGoroutine()},
|
||||
CPU: dto.ServerCPUResponse{CPUs: cpuPercent, Cores: physicalCores},
|
||||
RAM: dto.ServerRAMResponse{UsedMB: usedMB, TotalMB: totalMB, UsedPercent: int(memory.UsedPercent)},
|
||||
OS: dto.ServerOSResponse{GOOS: value.GOOS, NumCPU: value.NumCPU, Compiler: value.Compiler, GoVersion: value.GoVersion, NumGoroutine: value.NumGoroutine},
|
||||
CPU: dto.ServerCPUResponse{CPUs: value.CPUs, Cores: value.Cores},
|
||||
RAM: dto.ServerRAMResponse{UsedMB: value.UsedMB, TotalMB: value.TotalMB, UsedPercent: value.UsedPercent},
|
||||
Disk: diskInfo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -21,34 +21,16 @@ func taskDomain(v *dto.TaskRequest) *taskbiz.TimedTask {
|
||||
}
|
||||
func (s *TaskService) CreateRequest(ctx context.Context, req *dto.TaskRequest) (uint, error) {
|
||||
value := taskDomain(req)
|
||||
if err := s.Create(ctx, value); err != nil {
|
||||
if err := s.uc.Create(ctx, value); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return value.ID, nil
|
||||
}
|
||||
func (s *TaskService) UpdateRequest(ctx context.Context, req *dto.TaskRequest) error {
|
||||
return s.Update(ctx, taskDomain(req))
|
||||
return s.uc.Update(ctx, taskDomain(req))
|
||||
}
|
||||
func (s *TaskService) ListRequest(ctx context.Context, page, size int, name, executorType string, enabled *bool) ([]*dto.TaskResponse, int64, error) {
|
||||
return s.Tasks(ctx, page, size, &taskbiz.TimedTask{Name: name, ExecutorType: executorType, EnabledFilter: enabled})
|
||||
}
|
||||
func (s *TaskService) Create(ctx context.Context, v *taskbiz.TimedTask) error {
|
||||
return s.uc.Create(ctx, v)
|
||||
}
|
||||
func (s *TaskService) Update(ctx context.Context, v *taskbiz.TimedTask) error {
|
||||
return s.uc.Update(ctx, v)
|
||||
}
|
||||
func (s *TaskService) Delete(ctx context.Context, id uint) error {
|
||||
return s.uc.Delete(ctx, id)
|
||||
}
|
||||
func (s *TaskService) Toggle(ctx context.Context, id uint, enabled bool) error {
|
||||
return s.uc.Toggle(ctx, id, enabled)
|
||||
}
|
||||
func taskDTO(v *taskbiz.TimedTask, next *time.Time) *dto.TaskResponse {
|
||||
return &dto.TaskResponse{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, DeletedAt: nil, Name: v.Name, Description: v.Description, Spec: v.Spec, WithSeconds: v.WithSeconds, ExecutorType: v.ExecutorType, MethodName: v.MethodName, Params: json.RawMessage(v.Params), HTTPURL: v.HTTPURL, HTTPMethod: v.HTTPMethod, HTTPHeader: json.RawMessage(v.HTTPHeader), HTTPBody: v.HTTPBody, HTTPAllowPrivate: v.HTTPAllowPrivate, Enabled: v.Enabled, NextRunAt: next}
|
||||
}
|
||||
func (s *TaskService) Tasks(ctx context.Context, page, size int, q *taskbiz.TimedTask) ([]*dto.TaskResponse, int64, error) {
|
||||
items, total, next, err := s.uc.List(ctx, page, size, q)
|
||||
items, total, next, err := s.uc.List(ctx, page, size, &taskbiz.TimedTask{Name: name, ExecutorType: executorType, EnabledFilter: enabled})
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
@@ -63,7 +45,15 @@ func (s *TaskService) Tasks(ctx context.Context, page, size int, q *taskbiz.Time
|
||||
}
|
||||
return out, total, nil
|
||||
}
|
||||
|
||||
func (s *TaskService) Delete(ctx context.Context, id uint) error {
|
||||
return s.uc.Delete(ctx, id)
|
||||
}
|
||||
func (s *TaskService) Toggle(ctx context.Context, id uint, enabled bool) error {
|
||||
return s.uc.Toggle(ctx, id, enabled)
|
||||
}
|
||||
func taskDTO(v *taskbiz.TimedTask, next *time.Time) *dto.TaskResponse {
|
||||
return &dto.TaskResponse{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, DeletedAt: nil, Name: v.Name, Description: v.Description, Spec: v.Spec, WithSeconds: v.WithSeconds, ExecutorType: v.ExecutorType, MethodName: v.MethodName, Params: json.RawMessage(v.Params), HTTPURL: v.HTTPURL, HTTPMethod: v.HTTPMethod, HTTPHeader: json.RawMessage(v.HTTPHeader), HTTPBody: v.HTTPBody, HTTPAllowPrivate: v.HTTPAllowPrivate, Enabled: v.Enabled, NextRunAt: next}
|
||||
}
|
||||
func (s *TaskService) Logs(ctx context.Context, page, size int, taskID uint, status string) ([]*dto.TaskLogResponse, int64, error) {
|
||||
items, total, err := s.uc.Logs(ctx, page, size, taskID, status)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user