feat: 添加独立其他设置菜单及有效端参数保存
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
businessbiz "kra/internal/biz/business"
|
||||
"kra/internal/biz/system"
|
||||
businessdata "kra/internal/data/business"
|
||||
systemdata "kra/internal/data/system"
|
||||
"kra/internal/server/handler"
|
||||
"kra/internal/server/router"
|
||||
businessservice "kra/internal/service/business"
|
||||
)
|
||||
|
||||
func TestOtherSettingsHTTPPersistenceAndAccess(t *testing.T) {
|
||||
db, err := gorm.Open(sqlite.Open(filepath.Join(t.TempDir(), "settings.db")), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pool, _ := db.DB()
|
||||
t.Cleanup(func() { _ = pool.Close() })
|
||||
for _, step := range systemdata.Migrations() {
|
||||
if err := step.Migrate(db); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
uc := businessbiz.NewOtherSettingsUsecase(businessdata.NewOtherSettingsRepo(businessHTTPData{db}))
|
||||
engine := gin.New()
|
||||
engine.Use(func(c *gin.Context) {
|
||||
role, _ := strconv.ParseUint(c.GetHeader("X-Test-Role"), 10, 64)
|
||||
if role != 0 {
|
||||
c.Request = c.Request.WithContext(system.NewActorContext(c.Request.Context(), system.Actor{UserID: 1, AuthorityID: uint(role)}))
|
||||
}
|
||||
c.Next()
|
||||
})
|
||||
router.RegisterOtherSettings(engine.Group(""), handler.NewOtherSettings(businessservice.NewOtherSettingsService(uc)))
|
||||
call := func(role int, method, body string) map[string]any {
|
||||
t.Helper()
|
||||
req := httptest.NewRequest(method, "/sw/settings/effectiveness", strings.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Test-Role", strconv.Itoa(role))
|
||||
recorder := httptest.NewRecorder()
|
||||
engine.ServeHTTP(recorder, req)
|
||||
var result map[string]any
|
||||
if err := json.Unmarshal(recorder.Body.Bytes(), &result); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return result
|
||||
}
|
||||
for _, role := range []int{0, 901, 902, 903} {
|
||||
if call(role, http.MethodGet, "")["code"] == float64(0) {
|
||||
t.Fatal("非超管不得读取设置")
|
||||
}
|
||||
if call(role, http.MethodPut, `{"amount":10,"days":10}`)["code"] == float64(0) {
|
||||
t.Fatal("非超管不得写入设置")
|
||||
}
|
||||
}
|
||||
value := call(888, http.MethodGet, "")["data"].(map[string]any)
|
||||
if value["amount"] != float64(3000) || value["days"] != float64(30) {
|
||||
t.Fatal("首次访问默认值错误")
|
||||
}
|
||||
for _, body := range []string{`{}`, `{"amount":0}`, `{"amount":-1,"days":30}`, `{"amount":3.5,"days":30}`, `{"amount":0,"days":0}`, `{"amount":"3000","days":30}`} {
|
||||
if call(888, http.MethodPut, body)["code"] == float64(0) {
|
||||
t.Fatalf("非法输入被接受: %s", body)
|
||||
}
|
||||
}
|
||||
if call(888, http.MethodPut, `{"amount":0,"days":14}`)["code"] != float64(0) {
|
||||
t.Fatal("合法设置保存失败")
|
||||
}
|
||||
value = call(888, http.MethodGet, "")["data"].(map[string]any)
|
||||
if value["amount"] != float64(0) || value["days"] != float64(14) || value["updatedAt"] == "" {
|
||||
t.Fatal("读取未回显已保存值")
|
||||
}
|
||||
var raw string
|
||||
if err := db.Table("sys_params").Select("value").Where(map[string]any{"key": "sw.channel_effectiveness"}).Scan(&raw).Error; err != nil || raw != `{"amount":0,"days":14}` {
|
||||
t.Fatalf("应使用参数表的单条 JSON: %s %v", raw, err)
|
||||
}
|
||||
if call(888, http.MethodPut, `{"amount":3000,"days":30}`)["code"] != float64(0) {
|
||||
t.Fatal("默认值无法重新保存")
|
||||
}
|
||||
}
|
||||
Generated
+5
@@ -82,6 +82,10 @@ func wireApp(configServer *config.Server, store *config.Store, logger *slog.Logg
|
||||
auditRecorderUsecase := system2.NewAuditRecorderUsecase(auditRecordRepo)
|
||||
auditRecorder := system3.NewAuditRecorder(auditRecorderUsecase)
|
||||
registry := taskRegistry()
|
||||
otherSettingsRepo := business.NewOtherSettingsRepo(dataData)
|
||||
otherSettingsUsecase := business2.NewOtherSettingsUsecase(otherSettingsRepo)
|
||||
otherSettingsService := business3.NewOtherSettingsService(otherSettingsUsecase)
|
||||
otherSettings := handler.NewOtherSettings(otherSettingsService)
|
||||
rankingRepo := business.NewRankingRepo(dataData)
|
||||
accountRepo := business.NewAccountRepo(dataData)
|
||||
accountUsecase := business2.NewAccountUsecase(accountRepo)
|
||||
@@ -247,6 +251,7 @@ func wireApp(configServer *config.Server, store *config.Store, logger *slog.Logg
|
||||
integrationConfigService := integration2.NewIntegrationConfigService(integrationConfigUsecase)
|
||||
integrationConfig := handler.NewIntegrationConfig(integrationConfigService)
|
||||
set := &handler.Set{
|
||||
OtherSettings: otherSettings,
|
||||
Ranking: ranking,
|
||||
Workbench: workbench,
|
||||
ServerAnalysis: serverAnalysis,
|
||||
|
||||
+1
-1
@@ -11,4 +11,4 @@ import (
|
||||
)
|
||||
|
||||
// ProviderSet is biz providers.
|
||||
var ProviderSet = wire.NewSet(system.ProviderSet, payment.ProviderSet, integration.ProviderSet, task.ProviderSet, business.NewUserPreferenceUsecase, business.NewAccountUsecase, business.NewBindingUsecase, business.NewPasswordResetUsecase, business.NewBusinessPromoteUsecase, business.NewAlertRuleUsecase, business.NewAlertRecordUsecase, business.NewActivationUsecase, business.NewChannelUsecase, business.NewBusinessPortraitUsecase, business.NewChannelPortraitUsecase, business.NewLedgerUsecase, business.NewAnalyticsUsecase, business.NewServerAnalysisUsecase, business.NewWorkbenchUsecase, business.NewRankingUsecase, business.NewReportBuildUsecase, business.NewAlertTriggerUsecase)
|
||||
var ProviderSet = wire.NewSet(system.ProviderSet, payment.ProviderSet, integration.ProviderSet, task.ProviderSet, business.NewUserPreferenceUsecase, business.NewOtherSettingsUsecase, business.NewAccountUsecase, business.NewBindingUsecase, business.NewPasswordResetUsecase, business.NewBusinessPromoteUsecase, business.NewAlertRuleUsecase, business.NewAlertRecordUsecase, business.NewActivationUsecase, business.NewChannelUsecase, business.NewBusinessPortraitUsecase, business.NewChannelPortraitUsecase, business.NewLedgerUsecase, business.NewAnalyticsUsecase, business.NewServerAnalysisUsecase, business.NewWorkbenchUsecase, business.NewRankingUsecase, business.NewReportBuildUsecase, business.NewAlertTriggerUsecase)
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/go-kratos/kratos/v3/errors"
|
||||
"kra/internal/biz/system"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrOtherSettingsScope = errors.Forbidden("SW_OTHER_SETTINGS_SCOPE", "仅超级管理员可以查看和修改其他设置")
|
||||
ErrOtherSettingsInput = errors.BadRequest("SW_OTHER_SETTINGS_INPUT", "达标金额须为不小于0的整数,判定窗口须为不小于1的整数,且不能超过安全整数范围")
|
||||
ErrOtherSettingsStored = errors.BadRequest("SW_OTHER_SETTINGS_STORED", "已保存的设置格式不正确,请恢复默认后重新保存")
|
||||
)
|
||||
|
||||
const MaxSettingsInteger int64 = 9007199254740991
|
||||
|
||||
type OtherSettings struct {
|
||||
Amount, Days int64
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
func DefaultOtherSettings() OtherSettings { return OtherSettings{Amount: 3000, Days: 30} }
|
||||
|
||||
func ValidateOtherSettings(value OtherSettings) error {
|
||||
if value.Amount < 0 || value.Amount > MaxSettingsInteger || value.Days < 1 || value.Days > MaxSettingsInteger {
|
||||
return ErrOtherSettingsInput
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type OtherSettingsRepo interface {
|
||||
Find(context.Context) (*OtherSettings, error)
|
||||
Save(context.Context, OtherSettings) (*OtherSettings, error)
|
||||
}
|
||||
|
||||
type OtherSettingsUsecase struct{ repo OtherSettingsRepo }
|
||||
|
||||
func NewOtherSettingsUsecase(repo OtherSettingsRepo) *OtherSettingsUsecase {
|
||||
return &OtherSettingsUsecase{repo: repo}
|
||||
}
|
||||
|
||||
func requireSettingsAdmin(ctx context.Context) error {
|
||||
actor, ok := system.ActorFromContext(ctx)
|
||||
if !ok || actor.UserID == 0 {
|
||||
return ErrUnauthenticated
|
||||
}
|
||||
if actor.AuthorityID != system.SuperAdminAuthorityID {
|
||||
return ErrOtherSettingsScope
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (uc *OtherSettingsUsecase) Get(ctx context.Context) (*OtherSettings, error) {
|
||||
if err := requireSettingsAdmin(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
value, err := uc.repo.Find(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if value == nil {
|
||||
defaults := DefaultOtherSettings()
|
||||
return &defaults, nil
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func (uc *OtherSettingsUsecase) Save(ctx context.Context, value OtherSettings) (*OtherSettings, error) {
|
||||
if err := requireSettingsAdmin(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := ValidateOtherSettings(value); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return uc.repo.Save(ctx, value)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type otherSettingsFake struct {
|
||||
value *OtherSettings
|
||||
calls int
|
||||
}
|
||||
|
||||
func (r *otherSettingsFake) Find(context.Context) (*OtherSettings, error) {
|
||||
r.calls++
|
||||
return r.value, nil
|
||||
}
|
||||
func (r *otherSettingsFake) Save(_ context.Context, value OtherSettings) (*OtherSettings, error) {
|
||||
r.calls++
|
||||
r.value = &value
|
||||
return r.value, nil
|
||||
}
|
||||
|
||||
func TestOtherSettingsOnlySuperAdminAndDefaults(t *testing.T) {
|
||||
r := &otherSettingsFake{}
|
||||
uc := NewOtherSettingsUsecase(r)
|
||||
for _, ctx := range []context.Context{context.Background(), accountContext(7, 901), accountContext(8, 902), accountContext(9, 903)} {
|
||||
if _, err := uc.Get(ctx); err == nil {
|
||||
t.Fatal("非超管不能读取设置")
|
||||
}
|
||||
if _, err := uc.Save(ctx, DefaultOtherSettings()); err == nil {
|
||||
t.Fatal("非超管不能保存设置")
|
||||
}
|
||||
}
|
||||
if r.calls != 0 {
|
||||
t.Fatal("无权限请求不得访问存储")
|
||||
}
|
||||
value, err := uc.Get(accountContext(1, 888))
|
||||
if err != nil || *value != DefaultOtherSettings() {
|
||||
t.Fatalf("首次读取应返回默认值: %+v %v", value, err)
|
||||
}
|
||||
if _, err := uc.Save(accountContext(1, 888), OtherSettings{Amount: 0, Days: 1}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
value, err = uc.Get(accountContext(2, 888))
|
||||
if err != nil || value.Amount != 0 || value.Days != 1 {
|
||||
t.Fatal("参数应为全站共享,不能按用户各存一份")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOtherSettingsRejectInvalidRange(t *testing.T) {
|
||||
uc := NewOtherSettingsUsecase(&otherSettingsFake{})
|
||||
for _, value := range []OtherSettings{{Amount: -1, Days: 30}, {Amount: 3000, Days: 0}, {Amount: MaxSettingsInteger + 1, Days: 30}, {Amount: 3000, Days: MaxSettingsInteger + 1}} {
|
||||
if _, err := uc.Save(accountContext(1, 888), value); err != ErrOtherSettingsInput {
|
||||
t.Fatalf("不应保存非法值: %+v %v", value, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,14 +1,59 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
databusiness "kra/internal/data/business"
|
||||
datasystem "kra/internal/data/system"
|
||||
"kra/internal/modules/surface"
|
||||
"kra/pkg/database/migration"
|
||||
"kra/pkg/module"
|
||||
)
|
||||
|
||||
func Definition() module.Definition {
|
||||
return module.Definition{Name: "business", Migrations: databusiness.Migrations(), Surface: module.Surface{
|
||||
settingsMenu := module.Menu{Name: "sw_OtherSettings", Path: "OtherSettings", Component: "view/sw/OtherSettings.vue", Title: "其他设置", Icon: databusiness.MenuIcon("sw_OtherSettings"), Sort: 5}
|
||||
migrations := append(databusiness.Migrations(), migration.Step{ID: "202611040016_business_other_settings_surface", Migrate: func(db *gorm.DB) error {
|
||||
// First-install menus are created by the normal seed flow.
|
||||
if !db.Migrator().HasTable("sys_authorities") {
|
||||
return nil
|
||||
}
|
||||
var admins int64
|
||||
if err := db.Table("sys_authorities").Where("authority_id=888 AND deleted_at IS NULL").Count(&admins).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if admins == 0 {
|
||||
return nil
|
||||
}
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
menu := settingsMenu
|
||||
var alert struct{ Sort int }
|
||||
result := tx.Table("sys_base_menus").Select("sort").Where("name=? AND parent_id=0 AND deleted_at IS NULL", "sw_AlertCenter").Limit(1).Find(&alert)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected > 0 {
|
||||
menu.Sort = alert.Sort + 1
|
||||
}
|
||||
var collisions int64
|
||||
if err := tx.Table("sys_base_menus").Where("parent_id=0 AND deleted_at IS NULL AND name<>? AND sort=?", menu.Name, menu.Sort).Count(&collisions).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if collisions > 0 {
|
||||
if err := tx.Table("sys_base_menus").Where("parent_id=0 AND deleted_at IS NULL AND name<>? AND sort>=?", menu.Name, menu.Sort).Update("sort", gorm.Expr("sort + 1")).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return datasystem.EnsureAdminSurface(tx, module.Surface{
|
||||
Menus: []module.Menu{menu},
|
||||
APIs: []module.API{
|
||||
{Path: "/sw/settings/effectiveness", Method: "GET", Group: "其他设置", Description: "获取有效端判定参数"},
|
||||
{Path: "/sw/settings/effectiveness", Method: "PUT", Group: "其他设置", Description: "保存有效端判定参数"},
|
||||
},
|
||||
}, 888)
|
||||
})
|
||||
}})
|
||||
return module.Definition{Name: "business", Migrations: migrations, Surface: module.Surface{
|
||||
Menus: []module.Menu{
|
||||
settingsMenu,
|
||||
{Name: "sw_RankingAnalysis", Path: "RankingAnalysis", ParentName: "sw_DataAnalysis", Component: "view/sw/RankingAnalysis.vue", Title: "排行榜", Icon: databusiness.MenuIcon("sw_RankingAnalysis"), Sort: 6},
|
||||
{Name: "sw_ServerAnalysis", Path: "ServerAnalysis", ParentName: "sw_DataAnalysis", Component: "view/sw/ServerAnalysis.vue", Title: "区服分析", Icon: databusiness.MenuIcon("sw_ServerAnalysis"), Sort: 5},
|
||||
{Name: "sw_ChannelDataStatistics", Path: "ChannelDataStatistics", ParentName: "sw_DataAnalysis", Component: "view/sw/ChannelDataStatistics.vue", Title: "渠道数据统计", Icon: databusiness.MenuIcon("sw_ChannelDataStatistics"), Sort: 1},
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
datasystem "kra/internal/data/system"
|
||||
)
|
||||
|
||||
func TestOtherSettingsUpgradeAddsStandaloneMenuForSuperAdmin(t *testing.T) {
|
||||
db, err := gorm.Open(sqlite.Open(filepath.Join(t.TempDir(), "settings.db")), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pool, _ := db.DB()
|
||||
t.Cleanup(func() { _ = pool.Close() })
|
||||
for _, step := range datasystem.Migrations() {
|
||||
if err := step.Migrate(db); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := db.Exec("INSERT INTO sys_authorities(authority_id,authority_name,parent_id) VALUES(888,'admin',0),(901,'business',888)").Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Exec("INSERT INTO sys_base_menus(name,title,parent_id,sort) VALUES('sw_AlertCenter','预警中心',0,4),('permission','权限管理',0,5)").Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for attempt := 0; attempt < 2; attempt++ {
|
||||
for _, step := range Definition().Migrations {
|
||||
if err := step.Migrate(db); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
var menu struct {
|
||||
ID, ParentID uint
|
||||
Sort int
|
||||
Title, Component string
|
||||
}
|
||||
if err := db.Table("sys_base_menus").Where("name=?", "sw_OtherSettings").Take(&menu).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if menu.ParentID != 0 || menu.Sort != 5 || menu.Title != "其他设置" || menu.Component != "view/sw/OtherSettings.vue" {
|
||||
t.Fatalf("必须是独立菜单: %+v", menu)
|
||||
}
|
||||
var permissionSort int
|
||||
if err := db.Table("sys_base_menus").Select("sort").Where("name='permission'").Scan(&permissionSort).Error; err != nil || permissionSort != 6 {
|
||||
t.Fatal("其他设置必须紧接预警中心,后续菜单只应顺延一次")
|
||||
}
|
||||
var roles []uint
|
||||
if err := db.Table("sys_authority_menus").Where("sys_base_menu_id=?", menu.ID).Pluck("sys_authority_authority_id", &roles).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(roles) != 1 || roles[0] != 888 {
|
||||
t.Fatalf("只有超管应收到菜单,重复升级不能重复授权: %v", roles)
|
||||
}
|
||||
var count int64
|
||||
if err := db.Table("casbin_rule").Where("ptype='p' AND v1=? AND v0='888'", "/sw/settings/effectiveness").Count(&count).Error; err != nil || count != 2 {
|
||||
t.Fatal("超管读写 API 授权缺失或重复")
|
||||
}
|
||||
if err := db.Table("casbin_rule").Where("ptype='p' AND v1=? AND v0<>'888'", "/sw/settings/effectiveness").Count(&count).Error; err != nil || count != 0 {
|
||||
t.Fatal("不应向商务账号开放设置 API")
|
||||
}
|
||||
}
|
||||
@@ -25,16 +25,16 @@ func TestCatalogContainsBuiltInModulesInDependencyOrder(t *testing.T) {
|
||||
t.Fatalf("definition[%d] = %q, want %q", index, got, name)
|
||||
}
|
||||
}
|
||||
if got := catalog.MigrationSteps(); len(got) != 28 {
|
||||
t.Fatalf("module migrations = %d, want 28", len(got))
|
||||
if got := catalog.MigrationSteps(); len(got) != 30 {
|
||||
t.Fatalf("module migrations = %d, want 30", len(got))
|
||||
}
|
||||
var menus, apis int
|
||||
for _, definition := range catalog.Definitions {
|
||||
menus += len(definition.Surface.Menus)
|
||||
apis += len(definition.Surface.APIs)
|
||||
}
|
||||
if menus != 18 || apis != 69 {
|
||||
t.Fatalf("admin surface = %d menus/%d APIs, want 18/69", menus, apis)
|
||||
if menus != 19 || apis != 71 {
|
||||
t.Fatalf("admin surface = %d menus/%d APIs, want 19/71", menus, apis)
|
||||
}
|
||||
if got := catalog.DefaultTimedTasks(); len(got) != 2 {
|
||||
t.Fatalf("default timed tasks = %d, want 2", len(got))
|
||||
|
||||
@@ -95,6 +95,8 @@ var routes = map[string]routeValue{
|
||||
"POST /sw/business": {group: "商务管理", description: "添加子商务", audit: true},
|
||||
"PUT /sw/business": {group: "商务管理", description: "编辑商务资料和审核状态", audit: true},
|
||||
"GET /sw/user/preference": {group: "商务偏好", description: "获取当前用户商务偏好"},
|
||||
"GET /sw/settings/effectiveness": {group: "其他设置", description: "获取有效端判定参数"},
|
||||
"PUT /sw/settings/effectiveness": {group: "其他设置", description: "保存有效端判定参数", audit: true},
|
||||
"PUT /sw/user/preference": {group: "商务偏好", description: "更新当前用户商务偏好", audit: true},
|
||||
"DELETE /api/deleteApisByIds": {group: "api", description: "批量删除api", audit: true},
|
||||
"DELETE /dataAccessLog/deleteDataAccessLogByIds": {group: "数据权限审计", description: "批量删除数据权限审计日志", audit: true},
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -136,7 +137,7 @@ func TestGinStartupLogsEveryRegisteredRoute(t *testing.T) {
|
||||
if got, want := strings.Count(text, `"msg":"router registered"`), len(engine.Routes()); got != want {
|
||||
t.Fatalf("registered route log count = %d, want %d", got, want)
|
||||
}
|
||||
if !strings.Contains(text, `"msg":"router register success"`) || !strings.Contains(text, `"route_count":252`) {
|
||||
if !strings.Contains(text, `"msg":"router register success"`) || !strings.Contains(text, `"route_count":`+strconv.Itoa(len(engine.Routes()))) {
|
||||
t.Fatalf("startup route summary is missing: %s", text)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-kratos/kratos/v3/errors"
|
||||
"kra/internal/service/business"
|
||||
"kra/internal/service/dto"
|
||||
)
|
||||
|
||||
type OtherSettings struct {
|
||||
service *business.OtherSettingsService
|
||||
}
|
||||
|
||||
func NewOtherSettings(service *business.OtherSettingsService) *OtherSettings {
|
||||
return &OtherSettings{service: service}
|
||||
}
|
||||
|
||||
func (h *OtherSettings) Get(c *gin.Context) {
|
||||
value, err := h.service.Get(c.Request.Context())
|
||||
if err != nil {
|
||||
Fail(c, errors.FromError(err).Message)
|
||||
return
|
||||
}
|
||||
OKWithData(c, value)
|
||||
}
|
||||
|
||||
func (h *OtherSettings) Save(c *gin.Context) {
|
||||
var req dto.OtherSettingsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
Fail(c, "请输入完整的整数金额和天数")
|
||||
return
|
||||
}
|
||||
value, err := h.service.Save(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
Fail(c, errors.FromError(err).Message)
|
||||
return
|
||||
}
|
||||
OKWithData(c, value)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import "github.com/google/wire"
|
||||
// ProviderSet wires the system HTTP handlers. Set itself is filled field by
|
||||
// field so adding a handler only means adding its constructor here.
|
||||
var ProviderSet = wire.NewSet(
|
||||
NewOtherSettings,
|
||||
NewBusiness,
|
||||
NewBusinessPassword,
|
||||
NewBusinessPromote,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package handler
|
||||
|
||||
type Set struct {
|
||||
OtherSettings *OtherSettings
|
||||
Ranking *Ranking
|
||||
Workbench *Workbench
|
||||
ServerAnalysis *ServerAnalysis
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"kra/internal/server/handler"
|
||||
)
|
||||
|
||||
func RegisterOtherSettings(private *gin.RouterGroup, h *handler.OtherSettings) {
|
||||
private.GET("/sw/settings/effectiveness", h.Get)
|
||||
private.PUT("/sw/settings/effectiveness", h.Save)
|
||||
}
|
||||
@@ -20,6 +20,7 @@ func (r *Routes) RegisterRoutes(public, private *gin.RouterGroup, engine *gin.En
|
||||
return
|
||||
}
|
||||
registrations := []func(){
|
||||
func() { RegisterOtherSettings(private, r.handlers.OtherSettings) },
|
||||
func() {
|
||||
private.GET("/sw/rank/channel", r.handlers.Ranking.Channel)
|
||||
private.GET("/sw/rank/business", r.handlers.Ranking.Business)
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"kra/internal/biz/business"
|
||||
"kra/internal/service/dto"
|
||||
)
|
||||
|
||||
type OtherSettingsUsecase interface {
|
||||
Get(context.Context) (*business.OtherSettings, error)
|
||||
Save(context.Context, business.OtherSettings) (*business.OtherSettings, error)
|
||||
}
|
||||
|
||||
type OtherSettingsService struct{ uc OtherSettingsUsecase }
|
||||
|
||||
func NewOtherSettingsService(uc *business.OtherSettingsUsecase) *OtherSettingsService {
|
||||
return &OtherSettingsService{uc: uc}
|
||||
}
|
||||
|
||||
func otherSettingsResponse(value *business.OtherSettings) *dto.OtherSettingsResponse {
|
||||
defaults := business.DefaultOtherSettings()
|
||||
result := &dto.OtherSettingsResponse{OtherSettingsValues: dto.OtherSettingsValues{Amount: value.Amount, Days: value.Days}, Defaults: dto.OtherSettingsValues{Amount: defaults.Amount, Days: defaults.Days}}
|
||||
if !value.UpdatedAt.IsZero() {
|
||||
result.UpdatedAt = value.UpdatedAt.Format(time.RFC3339)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *OtherSettingsService) Get(ctx context.Context) (*dto.OtherSettingsResponse, error) {
|
||||
value, err := s.uc.Get(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return otherSettingsResponse(value), nil
|
||||
}
|
||||
|
||||
func (s *OtherSettingsService) Save(ctx context.Context, req *dto.OtherSettingsRequest) (*dto.OtherSettingsResponse, error) {
|
||||
if req == nil || req.Amount == nil || req.Days == nil {
|
||||
return nil, business.ErrOtherSettingsInput
|
||||
}
|
||||
value := business.OtherSettings{Amount: *req.Amount, Days: *req.Days}
|
||||
if err := business.ValidateOtherSettings(value); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
saved, err := s.uc.Save(ctx, value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return otherSettingsResponse(saved), nil
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"kra/internal/biz/business"
|
||||
"kra/internal/service/dto"
|
||||
)
|
||||
|
||||
type settingsUsecaseFake struct{ calls int }
|
||||
|
||||
func (f *settingsUsecaseFake) Get(context.Context) (*business.OtherSettings, error) {
|
||||
value := business.DefaultOtherSettings()
|
||||
return &value, nil
|
||||
}
|
||||
func (f *settingsUsecaseFake) Save(_ context.Context, value business.OtherSettings) (*business.OtherSettings, error) {
|
||||
f.calls++
|
||||
return &value, nil
|
||||
}
|
||||
|
||||
func TestOtherSettingsServiceRequiresBothValidFields(t *testing.T) {
|
||||
f := &settingsUsecaseFake{}
|
||||
s := &OtherSettingsService{uc: f}
|
||||
zero, days, negative := int64(0), int64(30), int64(-1)
|
||||
for _, req := range []*dto.OtherSettingsRequest{nil, {}, {Amount: &zero}, {Days: &days}, {Amount: &negative, Days: &days}, {Amount: &zero, Days: &zero}} {
|
||||
if _, err := s.Save(context.Background(), req); err != business.ErrOtherSettingsInput {
|
||||
t.Fatalf("边界校验错误: %v", err)
|
||||
}
|
||||
}
|
||||
if f.calls != 0 {
|
||||
t.Fatal("无效输入不得进入用例")
|
||||
}
|
||||
res, err := s.Save(context.Background(), &dto.OtherSettingsRequest{Amount: &zero, Days: &days})
|
||||
if err != nil || res.Amount != 0 || res.Days != 30 || res.Defaults.Amount != 3000 || f.calls != 1 {
|
||||
t.Fatalf("DTO 转换或零值错误: %+v %v", res, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package dto
|
||||
|
||||
type OtherSettingsRequest struct {
|
||||
Amount *int64 `json:"amount"`
|
||||
Days *int64 `json:"days"`
|
||||
}
|
||||
|
||||
type OtherSettingsValues struct {
|
||||
Amount int64 `json:"amount"`
|
||||
Days int64 `json:"days"`
|
||||
}
|
||||
|
||||
type OtherSettingsResponse struct {
|
||||
OtherSettingsValues
|
||||
Defaults OtherSettingsValues `json:"defaults"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
// ProviderSet aggregates all service modules for Wire.
|
||||
var ProviderSet = wire.NewSet(
|
||||
businessservice.NewUserPreferenceService,
|
||||
businessservice.NewOtherSettingsService,
|
||||
businessservice.NewAccountService,
|
||||
businessservice.NewBindingService,
|
||||
businessservice.NewPasswordResetService,
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import service from '@/utils/request'
|
||||
|
||||
export const getOtherSettings = () => service({ url: '/sw/settings/effectiveness', method: 'get', donNotShowLoading: true })
|
||||
export const saveOtherSettings = data => service({ url: '/sw/settings/effectiveness', method: 'put', data, donNotShowLoading: true })
|
||||
@@ -59,6 +59,7 @@
|
||||
"/src/view/sw/ChannelDataStatistics.vue": "SwChannelDataStatistics",
|
||||
"/src/view/sw/ChannelList.vue": "SwChannelList",
|
||||
"/src/view/sw/GameAnalytics.vue": "SwGameAnalytics",
|
||||
"/src/view/sw/OtherSettings.vue": "SwOtherSettings",
|
||||
"/src/view/sw/RankingAnalysis.vue": "RankingAnalysis",
|
||||
"/src/view/sw/RechargeDetails.vue": "SwRechargeDetails",
|
||||
"/src/view/sw/RegistrationDetails.vue": "SwRegistrationDetails",
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<el-result v-if="!admin" icon="warning" title="无权访问" sub-title="其他设置仅超级管理员可见、可修改。" />
|
||||
<div v-else class="other-settings">
|
||||
<el-text class="settings-intro" type="info">集中配置渠道有效端的判定参数,仅超级管理员可修改。</el-text>
|
||||
<el-card shadow="never" v-loading="loading">
|
||||
<el-tabs v-model="tab">
|
||||
<el-tab-pane label="有效端" name="effectiveness">
|
||||
<el-alert v-if="error" :title="error" type="error" :closable="false" show-icon class="settings-error" />
|
||||
<div class="settings-heading">
|
||||
<h3>有效端判定参数</h3>
|
||||
<el-text type="info">渠道注册后,在判定窗口内累计充值达到金额门槛,即视为有效端。</el-text>
|
||||
</div>
|
||||
<el-form v-if="loaded" ref="formRef" :model="form" :rules="rules" label-position="top" :disabled="loading || saving" @submit.prevent="save">
|
||||
<el-form-item label="达标金额(元)" prop="amount">
|
||||
<el-input-number v-model="form.amount" :min="0" :max="Number.MAX_SAFE_INTEGER" :step="100" :precision="0" controls-position="right" />
|
||||
<el-text class="field-help" type="info">默认 ¥3,000,支持 0 及以上整数。</el-text>
|
||||
</el-form-item>
|
||||
<el-form-item label="判定窗口(天)" prop="days">
|
||||
<el-input-number v-model="form.days" :min="1" :max="Number.MAX_SAFE_INTEGER" :precision="0" controls-position="right" />
|
||||
<el-text class="field-help" type="info">从渠道注册时间起算的连续天数,默认 30 天。</el-text>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="rule-preview">
|
||||
<strong>判定口径说明</strong>
|
||||
<ol>
|
||||
<li>渠道注册后 <b>{{ form.days ?? '—' }} 天</b>内累计充值流水满 <b>{{ amountText }}</b>,即为有效端。</li>
|
||||
<li>归属月份为<b>达标当天所在的月份</b>,该渠道计入当月有效端。</li>
|
||||
<li>注册满 <b>{{ form.days ?? '—' }} 天</b>仍未达标,不算有效端,之后流水上涨也不补记。</li>
|
||||
<li>一旦达标,有效端标记保持,不因后续流水回落而取消。</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="settings-actions">
|
||||
<el-button type="primary" :loading="saving" :disabled="loading || !loaded" @click="save">保存</el-button>
|
||||
<el-button :disabled="loading || saving" @click="restoreDefaults">恢复默认</el-button>
|
||||
<el-button v-if="error" link :disabled="loading || saving" @click="load">重新加载</el-button>
|
||||
<el-text type="info" role="status">{{ status }}</el-text>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useUserStore } from '@/pinia/modules/user'
|
||||
import { getOtherSettings, saveOtherSettings } from '@/api/otherSettings'
|
||||
|
||||
defineOptions({ name: 'SwOtherSettings' })
|
||||
const user = useUserStore()
|
||||
const admin = computed(() => Number(user.userInfo.authorityId) === 888)
|
||||
const tab = ref('effectiveness'), formRef = ref()
|
||||
const defaults = ref({ amount: 3000, days: 30 })
|
||||
const form = reactive({ ...defaults.value })
|
||||
const loading = ref(false), saving = ref(false), loaded = ref(false), error = ref(''), status = ref('')
|
||||
const amountText = computed(() => Number.isSafeInteger(form.amount) ? `¥${form.amount.toLocaleString('zh-CN')}` : '—')
|
||||
const validateInteger = minimum => (_rule, value, callback) => callback(Number.isSafeInteger(value) && value >= minimum ? undefined : new Error(`请输入不小于 ${minimum} 的有效整数`))
|
||||
const rules = {
|
||||
amount: [{ validator: validateInteger(0), trigger: ['blur', 'change'] }],
|
||||
days: [{ validator: validateInteger(1), trigger: ['blur', 'change'] }]
|
||||
}
|
||||
const apply = data => {
|
||||
Object.assign(form, { amount: data.amount, days: data.days })
|
||||
defaults.value = data.defaults
|
||||
loaded.value = true
|
||||
}
|
||||
const load = async () => {
|
||||
if (!admin.value || loading.value || saving.value) return
|
||||
loading.value = true; loaded.value = false; error.value = ''; status.value = ''
|
||||
try {
|
||||
const res = await getOtherSettings()
|
||||
if (res.code !== 0) { error.value = res.msg || '设置读取失败,请重试'; return }
|
||||
apply(res.data)
|
||||
} catch { error.value = '设置读取失败,请重试' } finally { loading.value = false }
|
||||
}
|
||||
const restoreDefaults = () => {
|
||||
Object.assign(form, defaults.value)
|
||||
loaded.value = true; error.value = ''; status.value = '已恢复默认值,点击「保存」后生效'
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
const save = async () => {
|
||||
if (!admin.value || saving.value || loading.value || !loaded.value) return
|
||||
if (!await formRef.value.validate().catch(() => false)) return
|
||||
if (saving.value) return
|
||||
saving.value = true
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认保存有效端参数?达标金额 ${amountText.value},判定窗口 ${form.days} 天。`, '保存设置', { type: 'warning', confirmButtonText: '确认保存', cancelButtonText: '取消' })
|
||||
} catch { saving.value = false; return }
|
||||
try {
|
||||
const res = await saveOtherSettings({ amount: form.amount, days: form.days })
|
||||
if (res.code !== 0) { error.value = res.msg || '保存失败,请重试'; return }
|
||||
apply(res.data); error.value = ''; status.value = '参数已保存'
|
||||
ElMessage.success('参数已保存')
|
||||
} catch { error.value = '保存失败,请重试' } finally { saving.value = false }
|
||||
}
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.other-settings { max-width: 860px; }
|
||||
.settings-intro { display: block; margin-bottom: 16px; }
|
||||
.settings-heading { margin: 4px 0 24px; }
|
||||
.settings-heading h3 { margin: 0 0 8px; font-size: 17px; }
|
||||
.settings-error { margin-bottom: 16px; }
|
||||
.field-help { margin-left: 16px; }
|
||||
.rule-preview { padding: 16px; border-radius: 6px; background: var(--el-fill-color-light); line-height: 1.9; color: var(--el-text-color-regular); }
|
||||
.rule-preview ol { margin: 8px 0 0; padding-left: 24px; }
|
||||
.settings-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 12px; margin: 24px 0 4px; }
|
||||
.settings-actions .el-button + .el-button { margin-left: 0; }
|
||||
@media (max-width: 600px) { .field-help { display: block; margin: 8px 0 0; width: 100%; } }
|
||||
</style>
|
||||
Reference in New Issue
Block a user