feat: 添加独立其他设置菜单及有效端参数保存
This commit is contained in:
@@ -4,6 +4,7 @@ import "gorm.io/gorm"
|
||||
|
||||
// 使用 Kra 全局注册的 Element Plus 组件名;不可直接沿用 RuoYi 的 SVG 文件名。
|
||||
var businessMenuIcons = map[string]string{
|
||||
"sw_OtherSettings": "Setting",
|
||||
"sw_BusMgmt": "Briefcase",
|
||||
"sw_BusinessList": "UserFilled",
|
||||
"sw_ChannelList": "Connection",
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"kra/internal/biz/business"
|
||||
)
|
||||
|
||||
const otherSettingsKey = "sw.channel_effectiveness"
|
||||
|
||||
// Projection of the existing parameter-management table; no new settings table.
|
||||
type otherSettingsPO struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
CreatedAt, UpdatedAt time.Time
|
||||
DeletedAt gorm.DeletedAt
|
||||
Name, Key, Value, Desc string
|
||||
}
|
||||
|
||||
func (otherSettingsPO) TableName() string { return "sys_params" }
|
||||
|
||||
type effectivenessJSON struct {
|
||||
Amount *int64 `json:"amount"`
|
||||
Days *int64 `json:"days"`
|
||||
}
|
||||
|
||||
func newOtherSettings(value business.OtherSettings) otherSettingsPO {
|
||||
encoded, _ := json.Marshal(effectivenessJSON{Amount: &value.Amount, Days: &value.Days})
|
||||
return otherSettingsPO{Name: "渠道有效端判定", Key: otherSettingsKey, Value: string(encoded), Desc: "其他设置:达标金额(元)和判定窗口(天)"}
|
||||
}
|
||||
|
||||
func toBizOtherSettings(po otherSettingsPO) (*business.OtherSettings, error) {
|
||||
var value effectivenessJSON
|
||||
if err := json.Unmarshal([]byte(po.Value), &value); err != nil || value.Amount == nil || value.Days == nil {
|
||||
return nil, business.ErrOtherSettingsStored
|
||||
}
|
||||
result := &business.OtherSettings{Amount: *value.Amount, Days: *value.Days, UpdatedAt: po.UpdatedAt}
|
||||
if err := business.ValidateOtherSettings(*result); err != nil {
|
||||
return nil, business.ErrOtherSettingsStored
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type otherSettingsRepo struct {
|
||||
data Provider
|
||||
// Serialize first creation as sys_params has no unique index on key.
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func NewOtherSettingsRepo(data Provider) business.OtherSettingsRepo {
|
||||
return &otherSettingsRepo{data: data}
|
||||
}
|
||||
|
||||
func (r *otherSettingsRepo) Find(ctx context.Context) (*business.OtherSettings, error) {
|
||||
var po otherSettingsPO
|
||||
err := r.data.DB().WithContext(ctx).Where(map[string]any{"key": otherSettingsKey}).First(&po).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, business.ErrStorage
|
||||
}
|
||||
return toBizOtherSettings(po)
|
||||
}
|
||||
|
||||
func (r *otherSettingsRepo) Save(ctx context.Context, value business.OtherSettings) (*business.OtherSettings, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
po := newOtherSettings(value)
|
||||
err := r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var existing otherSettingsPO
|
||||
err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where(map[string]any{"key": otherSettingsKey}).First(&existing).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return tx.Create(&po).Error
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
po.ID, po.CreatedAt, po.UpdatedAt = existing.ID, existing.CreatedAt, time.Now()
|
||||
return tx.Model(&existing).Updates(map[string]any{"value": po.Value, "updated_at": po.UpdatedAt}).Error
|
||||
})
|
||||
if err != nil {
|
||||
return nil, business.ErrStorage
|
||||
}
|
||||
return toBizOtherSettings(po)
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"kra/internal/biz/business"
|
||||
)
|
||||
|
||||
func TestOtherSettingsReuseParametersAndSaveAtomically(t *testing.T) {
|
||||
db := openTestDB(t)
|
||||
if err := db.AutoMigrate(&otherSettingsPO{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Create(&otherSettingsPO{Name: "other", Key: "unrelated", Value: "keep"}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r := NewOtherSettingsRepo(testData{db})
|
||||
ctx := context.Background()
|
||||
if value, err := r.Find(ctx); err != nil || value != nil {
|
||||
t.Fatal("缺省记录应由用例提供默认值")
|
||||
}
|
||||
var wg sync.WaitGroup
|
||||
for i := int64(1); i <= 8; i++ {
|
||||
wg.Add(1)
|
||||
go func(value int64) {
|
||||
defer wg.Done()
|
||||
if _, err := r.Save(ctx, business.OtherSettings{Amount: value * 100, Days: value}); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
value, err := NewOtherSettingsRepo(testData{db}).Find(ctx)
|
||||
if err != nil || value.Amount != value.Days*100 || value.UpdatedAt.IsZero() {
|
||||
t.Fatalf("重建仓储后应回显完整持久化参数: %+v %v", value, err)
|
||||
}
|
||||
var count int64
|
||||
if err := db.Model(&otherSettingsPO{}).Where(map[string]any{"key": otherSettingsKey}).Count(&count).Error; err != nil || count != 1 {
|
||||
t.Fatal("反复保存不能新增多份参数")
|
||||
}
|
||||
var other otherSettingsPO
|
||||
if err := db.Where(map[string]any{"key": "unrelated"}).First(&other).Error; err != nil || other.Value != "keep" {
|
||||
t.Fatal("保存不得覆盖其他参数")
|
||||
}
|
||||
if _, err := r.Save(ctx, business.OtherSettings{Amount: 0, Days: 1}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
value, _ = r.Find(ctx)
|
||||
if value.Amount != 0 || value.Days != 1 {
|
||||
t.Fatal("零金额必须持久化")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOtherSettingsMalformedStoredValueCanBeRepaired(t *testing.T) {
|
||||
db := openTestDB(t)
|
||||
if err := db.AutoMigrate(&otherSettingsPO{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r := NewOtherSettingsRepo(testData{db})
|
||||
ctx := context.Background()
|
||||
if _, err := r.Save(ctx, business.DefaultOtherSettings()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, raw := range []string{`{"amount":3000}`, `{"amount":3000,"days":0}`, `{"amount":1.5,"days":30}`, `invalid`} {
|
||||
if err := db.Model(&otherSettingsPO{}).Where(map[string]any{"key": otherSettingsKey}).Update("value", raw).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := r.Find(ctx); err != business.ErrOtherSettingsStored {
|
||||
t.Fatalf("错误参数不能静默当成默认值: %v", err)
|
||||
}
|
||||
}
|
||||
if _, err := r.Save(ctx, business.DefaultOtherSettings()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
value, err := r.Find(ctx)
|
||||
if err != nil || value.Amount != 3000 || value.Days != 30 {
|
||||
t.Fatal("恢复默认保存应修复格式错误的参数")
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ var ProviderSet = wire.NewSet(
|
||||
wire.Bind(new(databusiness.Provider), new(*Data)),
|
||||
wire.Bind(new(databusiness.PlatformProvider), new(*Data)),
|
||||
databusiness.NewUserPreferenceRepo,
|
||||
databusiness.NewOtherSettingsRepo,
|
||||
databusiness.NewAccountRepo,
|
||||
databusiness.NewBindingRepo,
|
||||
databusiness.NewPlatformAccountRepo,
|
||||
|
||||
@@ -34,8 +34,8 @@ func TestMigrateAllRunsModuleSchemasWithoutBootstrapSeed(t *testing.T) {
|
||||
if err = db.Table(migration.TableName).Count(&versions).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if versions != 28 {
|
||||
t.Fatalf("migration versions = %d, want 28", versions)
|
||||
if expected := int64(len(testCatalog().MigrationSteps())); versions != expected {
|
||||
t.Fatalf("migration versions = %d, want %d", versions, expected)
|
||||
}
|
||||
var communicationRows []dataintegration.ConfigPO
|
||||
if err = db.Where("kind IN ?", []string{"mq", "websocket"}).Order("kind, provider").Find(&communicationRows).Error; err != nil {
|
||||
|
||||
Reference in New Issue
Block a user