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 -1
View File
@@ -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)
+78
View File
@@ -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)
}
}
}
+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 {
+46 -1
View File
@@ -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")
}
}
+4 -4
View File
@@ -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))
+2
View File
@@ -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},
+2 -1
View File
@@ -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)
}
}
+39
View File
@@ -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)
}
+1
View File
@@ -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
View File
@@ -1,6 +1,7 @@
package handler
type Set struct {
OtherSettings *OtherSettings
Ranking *Ranking
Workbench *Workbench
ServerAnalysis *ServerAnalysis
+11
View File
@@ -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)
}
+1
View File
@@ -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)
}
}
+17
View File
@@ -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"`
}
+1
View File
@@ -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,