feat: 添加独立其他设置菜单及有效端参数保存

This commit is contained in:
Yvan
2026-11-04 21:18:31 +08:00
parent c2e8b729be
commit ca9ce99b66
27 changed files with 805 additions and 9 deletions
+1
View File
@@ -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",
+91
View File
@@ -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("恢复默认保存应修复格式错误的参数")
}
}
+1
View File
@@ -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,
+2 -2
View File
@@ -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 {