feat: 添加独立其他设置菜单及有效端参数保存
This commit is contained in:
+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,
|
||||
|
||||
Reference in New Issue
Block a user