feat(distribution): 默认结算冷却天数改为系统配置驱动
新建站点的默认冷却期原先硬编码 7 天(OnboardSite 字面量、DB 列默认、两套前端表单), 改一处要同时动三处。现统一到系统配置键 distribution.default_settlement_hold_days: - systemconfig 注册该键(默认 7,0=立即释放,0-365 校验),纳入 admin 可见配置 - distribution 建站两条路径(CreateSite 未传值、OnboardSite 缺省)读取该配置,缺失或非法回落 7 - 新增 GET /admin/distribution/site-defaults 暴露当前有效默认值 - Vue / React 站点表单改为运行时拉取,拉取失败保持 7 - DB 列默认值保留作为兜底,站点级 settlement_hold_days 仍可单独覆盖
This commit is contained in:
@@ -54,6 +54,15 @@ func (h *Handler) CreateSite(c *gin.Context) {
|
|||||||
response.Created(c, item)
|
response.Created(c, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) GetSiteDefaults(c *gin.Context) {
|
||||||
|
holdDays, err := h.service.DefaultSettlementHoldDays(c.Request.Context())
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.OK(c, gin.H{"default_settlement_hold_days": holdDays})
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) UpdateSite(c *gin.Context) {
|
func (h *Handler) UpdateSite(c *gin.Context) {
|
||||||
id, ok := parseID(c)
|
id, ok := parseID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"hfb_sys/backend/internal/auditlog"
|
"hfb_sys/backend/internal/auditlog"
|
||||||
"hfb_sys/backend/internal/model"
|
"hfb_sys/backend/internal/model"
|
||||||
listingpkg "hfb_sys/backend/internal/modules/listing"
|
listingpkg "hfb_sys/backend/internal/modules/listing"
|
||||||
|
"hfb_sys/backend/internal/modules/systemconfig"
|
||||||
"hfb_sys/backend/internal/modules/wallet"
|
"hfb_sys/backend/internal/modules/wallet"
|
||||||
|
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
@@ -220,6 +221,23 @@ func (r *Repository) FindSite(ctx context.Context, adminID, id uint64) (*SiteDTO
|
|||||||
return &item, nil
|
return &item, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resolveDefaultSettlementHoldDays 读取系统配置里的新建站点默认结算冷却天数,缺失或异常时回落到内置默认值。
|
||||||
|
func (r *Repository) resolveDefaultSettlementHoldDays(ctx context.Context) int {
|
||||||
|
if r == nil || r.db == nil {
|
||||||
|
return systemconfig.DefaultDistributionSettlementHoldDays
|
||||||
|
}
|
||||||
|
var row model.SystemConfig
|
||||||
|
if err := r.db.WithContext(ctx).Where("`key` = ?", systemconfig.DistributionSettlementHoldDaysKey).First(&row).Error; err != nil {
|
||||||
|
return systemconfig.DefaultDistributionSettlementHoldDays
|
||||||
|
}
|
||||||
|
return systemconfig.ParseDistributionSettlementHoldDays(row.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultSettlementHoldDays 暴露当前有效的默认结算冷却天数,供前端表单使用。
|
||||||
|
func (r *Repository) DefaultSettlementHoldDays(ctx context.Context) int {
|
||||||
|
return r.resolveDefaultSettlementHoldDays(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Repository) CreateSite(ctx context.Context, req CreateSiteRequest) (*SiteDTO, error) {
|
func (r *Repository) CreateSite(ctx context.Context, req CreateSiteRequest) (*SiteDTO, error) {
|
||||||
row := model.DistributionSite{
|
row := model.DistributionSite{
|
||||||
SiteCode: strings.TrimSpace(req.SiteCode), SiteName: strings.TrimSpace(req.SiteName), Domain: strings.TrimSpace(req.Domain),
|
SiteCode: strings.TrimSpace(req.SiteCode), SiteName: strings.TrimSpace(req.SiteName), Domain: strings.TrimSpace(req.Domain),
|
||||||
@@ -243,6 +261,8 @@ func (r *Repository) CreateSite(ctx context.Context, req CreateSiteRequest) (*Si
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
row.SettlementHoldDays = holdDays
|
row.SettlementHoldDays = holdDays
|
||||||
|
} else {
|
||||||
|
row.SettlementHoldDays = r.resolveDefaultSettlementHoldDays(ctx)
|
||||||
}
|
}
|
||||||
if err := r.db.WithContext(ctx).Create(&row).Error; err != nil {
|
if err := r.db.WithContext(ctx).Create(&row).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1404,7 +1424,7 @@ func (r *Repository) OnboardSite(ctx context.Context, req OnboardSiteRequest) (*
|
|||||||
if req.TaxRateBPS != nil {
|
if req.TaxRateBPS != nil {
|
||||||
taxRate = *req.TaxRateBPS
|
taxRate = *req.TaxRateBPS
|
||||||
}
|
}
|
||||||
holdDays := 7
|
holdDays := r.resolveDefaultSettlementHoldDays(ctx)
|
||||||
if req.SettlementHoldDays != nil {
|
if req.SettlementHoldDays != nil {
|
||||||
normalized, err := normalizeSettlementHoldDays(*req.SettlementHoldDays)
|
normalized, err := normalizeSettlementHoldDays(*req.SettlementHoldDays)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -86,6 +86,12 @@ func (s *Service) CreateSite(ctx context.Context, req CreateSiteRequest) (*SiteD
|
|||||||
}
|
}
|
||||||
return s.repo.CreateSite(ctx, req)
|
return s.repo.CreateSite(ctx, req)
|
||||||
}
|
}
|
||||||
|
func (s *Service) DefaultSettlementHoldDays(ctx context.Context) (int, error) {
|
||||||
|
if err := s.available(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return s.repo.DefaultSettlementHoldDays(ctx), nil
|
||||||
|
}
|
||||||
func (s *Service) UpdateSite(ctx context.Context, adminID, id uint64, req UpdateSiteRequest) (*SiteDTO, error) {
|
func (s *Service) UpdateSite(ctx context.Context, adminID, id uint64, req UpdateSiteRequest) (*SiteDTO, error) {
|
||||||
if err := s.available(); err != nil {
|
if err := s.available(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package distribution
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"hfb_sys/backend/internal/model"
|
||||||
|
"hfb_sys/backend/internal/modules/systemconfig"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestCreateSiteUsesConfiguredDefaultSettlementHoldDays 验证新建站点未显式传值时读取系统配置的默认冷却天数。
|
||||||
|
func TestCreateSiteUsesConfiguredDefaultSettlementHoldDays(t *testing.T) {
|
||||||
|
db := distributionTestDB(t)
|
||||||
|
repo := NewRepository(db)
|
||||||
|
if err := db.Create(&model.SystemConfig{
|
||||||
|
Key: systemconfig.DistributionSettlementHoldDaysKey,
|
||||||
|
Value: "3",
|
||||||
|
}).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
dto, err := repo.CreateSite(t.Context(), CreateSiteRequest{SiteCode: "cfg_hold", SiteName: "配置冷却站", Domain: "cfghold.dist.com"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateSite err = %v", err)
|
||||||
|
}
|
||||||
|
if dto.SettlementHoldDays != 3 {
|
||||||
|
t.Fatalf("DTO settlement_hold_days = %d, want 3", dto.SettlementHoldDays)
|
||||||
|
}
|
||||||
|
var site model.DistributionSite
|
||||||
|
if err := db.Where("site_code = ?", "cfg_hold").First(&site).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if site.SettlementHoldDays != 3 {
|
||||||
|
t.Fatalf("DB settlement_hold_days = %d, want 3", site.SettlementHoldDays)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCreateSiteFallsBackWhenConfigMissing 验证系统配置缺失时回落到内置默认 7 天。
|
||||||
|
func TestCreateSiteFallsBackWhenConfigMissing(t *testing.T) {
|
||||||
|
db := distributionTestDB(t)
|
||||||
|
repo := NewRepository(db)
|
||||||
|
dto, err := repo.CreateSite(t.Context(), CreateSiteRequest{SiteCode: "no_cfg_hold", SiteName: "兜底冷却站", Domain: "nocfghold.dist.com"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateSite err = %v", err)
|
||||||
|
}
|
||||||
|
if dto.SettlementHoldDays != systemconfig.DefaultDistributionSettlementHoldDays {
|
||||||
|
t.Fatalf("DTO settlement_hold_days = %d, want %d", dto.SettlementHoldDays, systemconfig.DefaultDistributionSettlementHoldDays)
|
||||||
|
}
|
||||||
|
var site model.DistributionSite
|
||||||
|
if err := db.Where("site_code = ?", "no_cfg_hold").First(&site).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if site.SettlementHoldDays != systemconfig.DefaultDistributionSettlementHoldDays {
|
||||||
|
t.Fatalf("DB settlement_hold_days = %d, want %d", site.SettlementHoldDays, systemconfig.DefaultDistributionSettlementHoldDays)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package systemconfig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DistributionSettlementHoldDaysKey 是「新建分销站点默认结算冷却天数」的系统配置键。
|
||||||
|
const DistributionSettlementHoldDaysKey = "distribution.default_settlement_hold_days"
|
||||||
|
|
||||||
|
// DefaultDistributionSettlementHoldDays 是新建分销站点默认结算冷却天数的兜底值。
|
||||||
|
const DefaultDistributionSettlementHoldDays = 7
|
||||||
|
|
||||||
|
func defaultDistributionSettlementHoldDaysValue() string {
|
||||||
|
return strconv.Itoa(DefaultDistributionSettlementHoldDays)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseDistributionSettlementHoldDays 解析结算冷却天数配置,为空、非法或越界时回落到默认 7 天。
|
||||||
|
func ParseDistributionSettlementHoldDays(raw string) int {
|
||||||
|
days, err := strconv.Atoi(strings.TrimSpace(raw))
|
||||||
|
if err != nil || days < 0 || days > 365 {
|
||||||
|
return DefaultDistributionSettlementHoldDays
|
||||||
|
}
|
||||||
|
return days
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateConfigValue 按配置键分发校验逻辑,未注册校验的键直接放行。
|
||||||
|
func validateConfigValue(key, value string) error {
|
||||||
|
switch key {
|
||||||
|
case DistributionSettlementHoldDaysKey:
|
||||||
|
return validateDistributionSettlementHoldDays(value)
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateDistributionSettlementHoldDays(value string) error {
|
||||||
|
days, err := strconv.Atoi(strings.TrimSpace(value))
|
||||||
|
if err != nil || days < 0 || days > 365 {
|
||||||
|
return ErrInvalidConfig
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package systemconfig
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestParseDistributionSettlementHoldDays(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
raw string
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{name: "零天立即释放", raw: "0", want: 0},
|
||||||
|
{name: "默认七天", raw: "7", want: 7},
|
||||||
|
{name: "上限三百六十五天", raw: "365", want: 365},
|
||||||
|
{name: "负数回落", raw: "-1", want: DefaultDistributionSettlementHoldDays},
|
||||||
|
{name: "超过上限回落", raw: "366", want: DefaultDistributionSettlementHoldDays},
|
||||||
|
{name: "空值回落", raw: " ", want: DefaultDistributionSettlementHoldDays},
|
||||||
|
{name: "非数字回落", raw: "abc", want: DefaultDistributionSettlementHoldDays},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if got := ParseDistributionSettlementHoldDays(tc.raw); got != tc.want {
|
||||||
|
t.Fatalf("ParseDistributionSettlementHoldDays(%q) = %d, want %d", tc.raw, got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDistributionSettlementHoldDaysConfigRegistered(t *testing.T) {
|
||||||
|
value := ""
|
||||||
|
for _, item := range defaultConfigs {
|
||||||
|
if item.Key == DistributionSettlementHoldDaysKey {
|
||||||
|
value = item.Value
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if value != defaultDistributionSettlementHoldDaysValue() {
|
||||||
|
t.Fatalf("defaultConfigs 中 %s 的默认值 = %q, want %q", DistributionSettlementHoldDaysKey, value, defaultDistributionSettlementHoldDaysValue())
|
||||||
|
}
|
||||||
|
visible := false
|
||||||
|
for _, key := range adminVisibleConfigKeys {
|
||||||
|
if key == DistributionSettlementHoldDaysKey {
|
||||||
|
visible = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !visible {
|
||||||
|
t.Fatalf("adminVisibleConfigKeys 缺少 %s", DistributionSettlementHoldDaysKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateConfigValueDistributionSettlementHoldDays(t *testing.T) {
|
||||||
|
for _, value := range []string{"0", "7", "365"} {
|
||||||
|
if err := validateConfigValue(DistributionSettlementHoldDaysKey, value); err != nil {
|
||||||
|
t.Fatalf("validateConfigValue(%q) = %v, want nil", value, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, value := range []string{"-1", "366", "", "abc"} {
|
||||||
|
if err := validateConfigValue(DistributionSettlementHoldDaysKey, value); err != ErrInvalidConfig {
|
||||||
|
t.Fatalf("validateConfigValue(%q) = %v, want ErrInvalidConfig", value, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 未注册校验的键直接放行。
|
||||||
|
if err := validateConfigValue("handoff.owner_submit_timeout_minutes", "abc"); err != nil {
|
||||||
|
t.Fatalf("无关配置键应放行, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,6 +54,7 @@ var defaultConfigs = []defaultConfig{
|
|||||||
{Key: cooperationFeedbackConfigKey, Value: defaultCooperationFeedbackConfigValue(), Description: "首页 合作与反馈入口配置 JSON(标题/副标题/展示图片)"},
|
{Key: cooperationFeedbackConfigKey, Value: defaultCooperationFeedbackConfigValue(), Description: "首页 合作与反馈入口配置 JSON(标题/副标题/展示图片)"},
|
||||||
{Key: publishOptionsConfigKey, Value: defaultPublishOptionsConfigValue(), Description: "发布页选项配置 JSON"},
|
{Key: publishOptionsConfigKey, Value: defaultPublishOptionsConfigValue(), Description: "发布页选项配置 JSON"},
|
||||||
{Key: salePriceConfigKey, Value: defaultSalePriceConfigValue(), Description: "内部出售定价规则 JSON"},
|
{Key: salePriceConfigKey, Value: defaultSalePriceConfigValue(), Description: "内部出售定价规则 JSON"},
|
||||||
|
{Key: DistributionSettlementHoldDaysKey, Value: defaultDistributionSettlementHoldDaysValue(), Description: "新建分销站点默认结算冷却天数(0=立即释放,最大 365)"},
|
||||||
{Key: "backup.schedule_enabled", Value: "true", Description: "在线备份定时任务是否启用"},
|
{Key: "backup.schedule_enabled", Value: "true", Description: "在线备份定时任务是否启用"},
|
||||||
{Key: "backup.full_hour", Value: "4", Description: "每日全量备份小时(Asia/Shanghai)"},
|
{Key: "backup.full_hour", Value: "4", Description: "每日全量备份小时(Asia/Shanghai)"},
|
||||||
{Key: "backup.full_minute", Value: "30", Description: "每日全量备份分钟(Asia/Shanghai)"},
|
{Key: "backup.full_minute", Value: "30", Description: "每日全量备份分钟(Asia/Shanghai)"},
|
||||||
@@ -65,6 +66,7 @@ var adminVisibleConfigKeys = []string{
|
|||||||
"chat.default_support_admin_id",
|
"chat.default_support_admin_id",
|
||||||
"chat.listing_group_welcome",
|
"chat.listing_group_welcome",
|
||||||
"chat.renter_retention_days_after_order_end",
|
"chat.renter_retention_days_after_order_end",
|
||||||
|
"distribution.default_settlement_hold_days",
|
||||||
"handoff.owner_return_confirm_timeout_minutes",
|
"handoff.owner_return_confirm_timeout_minutes",
|
||||||
"handoff.owner_submit_timeout_minutes",
|
"handoff.owner_submit_timeout_minutes",
|
||||||
"handoff.renter_confirm_timeout_minutes",
|
"handoff.renter_confirm_timeout_minutes",
|
||||||
@@ -140,6 +142,9 @@ func (r *Repository) FindValue(ctx context.Context, key string) (string, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repository) Update(ctx context.Context, actorID uint64, key string, req UpdateRequest, meta AuditMeta) (*ConfigDTO, error) {
|
func (r *Repository) Update(ctx context.Context, actorID uint64, key string, req UpdateRequest, meta AuditMeta) (*ConfigDTO, error) {
|
||||||
|
if err := validateConfigValue(key, req.Value); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
var row model.SystemConfig
|
var row model.SystemConfig
|
||||||
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("`key` = ?", key).First(&row).Error; err != nil {
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("`key` = ?", key).First(&row).Error; err != nil {
|
||||||
|
|||||||
@@ -638,6 +638,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
|
|||||||
adminRoutes.POST("/files/upload", fileHandler.Upload)
|
adminRoutes.POST("/files/upload", fileHandler.Upload)
|
||||||
adminRoutes.GET("/dashboard", requirePerm("dashboard:view"), adminDashboardHandler.Summary)
|
adminRoutes.GET("/dashboard", requirePerm("dashboard:view"), adminDashboardHandler.Summary)
|
||||||
adminRoutes.GET("/distribution/sites", requirePerm("distribution:site_view"), distributionHandler.ListSites)
|
adminRoutes.GET("/distribution/sites", requirePerm("distribution:site_view"), distributionHandler.ListSites)
|
||||||
|
adminRoutes.GET("/distribution/site-defaults", requirePerm("distribution:site_view"), distributionHandler.GetSiteDefaults)
|
||||||
adminRoutes.GET("/distribution/sites/:id", requirePerm("distribution:site_view"), distributionHandler.GetSite)
|
adminRoutes.GET("/distribution/sites/:id", requirePerm("distribution:site_view"), distributionHandler.GetSite)
|
||||||
adminRoutes.POST("/distribution/sites", requirePerm("distribution:site_manage"), distributionHandler.CreateSite)
|
adminRoutes.POST("/distribution/sites", requirePerm("distribution:site_manage"), distributionHandler.CreateSite)
|
||||||
adminRoutes.POST("/distribution/sites/onboard", requirePerm("distribution:site_manage"), distributionHandler.OnboardSite)
|
adminRoutes.POST("/distribution/sites/onboard", requirePerm("distribution:site_manage"), distributionHandler.OnboardSite)
|
||||||
|
|||||||
@@ -322,6 +322,18 @@ export async function listSites(): Promise<Site[]> {
|
|||||||
return extract(resp).items ?? []
|
return extract(resp).items ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DistributionSiteDefaults {
|
||||||
|
default_settlement_hold_days: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchDistributionSiteDefaults 读取新建站点的默认结算冷却天数,供开通表单预填。
|
||||||
|
export async function fetchDistributionSiteDefaults(): Promise<DistributionSiteDefaults> {
|
||||||
|
const resp = await api.get<ApiResponse<DistributionSiteDefaults>>(
|
||||||
|
'/admin/distribution/site-defaults'
|
||||||
|
)
|
||||||
|
return extract(resp)
|
||||||
|
}
|
||||||
|
|
||||||
export async function listRules(siteID?: number, mode?: string): Promise<Rule[]> {
|
export async function listRules(siteID?: number, mode?: string): Promise<Rule[]> {
|
||||||
const resp = await api.get<ApiResponse<{ items: Rule[] }>>('/admin/distribution/rules', {
|
const resp = await api.get<ApiResponse<{ items: Rule[] }>>('/admin/distribution/rules', {
|
||||||
params: { site_id: siteID || undefined, mode: mode || undefined },
|
params: { site_id: siteID || undefined, mode: mode || undefined },
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
Descriptions,
|
Descriptions,
|
||||||
} from 'antd'
|
} from 'antd'
|
||||||
import { PlusOutlined, ShopOutlined, CopyOutlined } from '@ant-design/icons'
|
import { PlusOutlined, ShopOutlined, CopyOutlined } from '@ant-design/icons'
|
||||||
import { listSites, onboardSite, type Site, type OnboardSitePayload, type OnboardSiteResult } from '../api'
|
import { listSites, onboardSite, fetchDistributionSiteDefaults, type Site, type OnboardSitePayload, type OnboardSiteResult } from '../api'
|
||||||
|
|
||||||
export default function Sites({ isSuperadmin = true }: { isSuperadmin?: boolean }) {
|
export default function Sites({ isSuperadmin = true }: { isSuperadmin?: boolean }) {
|
||||||
const [items, setItems] = useState<Site[]>([])
|
const [items, setItems] = useState<Site[]>([])
|
||||||
@@ -25,6 +25,7 @@ export default function Sites({ isSuperadmin = true }: { isSuperadmin?: boolean
|
|||||||
const [modalOpen, setModalOpen] = useState(false)
|
const [modalOpen, setModalOpen] = useState(false)
|
||||||
const [submitting, setSubmitting] = useState(false)
|
const [submitting, setSubmitting] = useState(false)
|
||||||
const [successResult, setSuccessResult] = useState<OnboardSiteResult | null>(null)
|
const [successResult, setSuccessResult] = useState<OnboardSiteResult | null>(null)
|
||||||
|
const [defaultSettlementHoldDays, setDefaultSettlementHoldDays] = useState(7)
|
||||||
const [form] = Form.useForm()
|
const [form] = Form.useForm()
|
||||||
|
|
||||||
const loadData = () => {
|
const loadData = () => {
|
||||||
@@ -37,6 +38,16 @@ export default function Sites({ isSuperadmin = true }: { isSuperadmin?: boolean
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadData()
|
loadData()
|
||||||
|
fetchDistributionSiteDefaults()
|
||||||
|
.then((res) => {
|
||||||
|
const days = res?.default_settlement_hold_days
|
||||||
|
if (typeof days === 'number' && days >= 0 && days <= 365) {
|
||||||
|
setDefaultSettlementHoldDays(days)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// 拉取失败时保持内置默认值 7
|
||||||
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleOpenModal = () => {
|
const handleOpenModal = () => {
|
||||||
@@ -46,7 +57,7 @@ export default function Sites({ isSuperadmin = true }: { isSuperadmin?: boolean
|
|||||||
display_account_enabled: true,
|
display_account_enabled: true,
|
||||||
tax_enabled: false,
|
tax_enabled: false,
|
||||||
tax_rate_bps: 100,
|
tax_rate_bps: 100,
|
||||||
settlement_hold_days: 7,
|
settlement_hold_days: defaultSettlementHoldDays,
|
||||||
})
|
})
|
||||||
setModalOpen(true)
|
setModalOpen(true)
|
||||||
}
|
}
|
||||||
@@ -65,7 +76,7 @@ export default function Sites({ isSuperadmin = true }: { isSuperadmin?: boolean
|
|||||||
display_account_enabled: values.display_account_enabled,
|
display_account_enabled: values.display_account_enabled,
|
||||||
tax_enabled: values.tax_enabled,
|
tax_enabled: values.tax_enabled,
|
||||||
tax_rate_bps: values.tax_enabled ? values.tax_rate_bps : 0,
|
tax_rate_bps: values.tax_enabled ? values.tax_rate_bps : 0,
|
||||||
settlement_hold_days: Number(values.settlement_hold_days ?? 7),
|
settlement_hold_days: Number(values.settlement_hold_days ?? defaultSettlementHoldDays),
|
||||||
}
|
}
|
||||||
const res = await onboardSite(payload)
|
const res = await onboardSite(payload)
|
||||||
message.success('分销站点及分销商管理员开通成功!')
|
message.success('分销站点及分销商管理员开通成功!')
|
||||||
|
|||||||
@@ -828,4 +828,5 @@ distribution.hao858.com {
|
|||||||
5. 兑换比例提价语义明确(比例越小售价越高,提价使分站售价增加)。
|
5. 兑换比例提价语义明确(比例越小售价越高,提价使分站售价增加)。
|
||||||
- 管理 API 覆盖站点、规则、售出/收号订单查询、财务快照、账号展示开关、支付通道绑定和结算批次。
|
- 管理 API 覆盖站点、规则、售出/收号订单查询、财务快照、账号展示开关、支付通道绑定和结算批次。
|
||||||
- 生产与测试口径一致:结算批次入账、站点提现与冷却期自动释放不再按 `APP_ENV` 冻结(原 `liveMoneyMovementEnabled` 生产保护已移除)。
|
- 生产与测试口径一致:结算批次入账、站点提现与冷却期自动释放不再按 `APP_ENV` 冻结(原 `liveMoneyMovementEnabled` 生产保护已移除)。
|
||||||
|
- 新建站点的默认结算冷却天数由系统配置 `distribution.default_settlement_hold_days` 统一控制(默认 7 天,0=立即释放,最大 365),管理端可在系统配置页调整;站点级 `settlement_hold_days` 仍可单独覆盖。
|
||||||
- `distribution-admin` 基于 React + TypeScript + Vite + Ant Design,覆盖经营大盘、双视图资产管理、选品货架、订单中心、品牌装修与钱包明细。
|
- `distribution-admin` 基于 React + TypeScript + Vite + Ant Design,覆盖经营大盘、双视图资产管理、选品货架、订单中心、品牌装修与钱包明细。
|
||||||
|
|||||||
@@ -119,6 +119,17 @@ export async function fetchDistributionSites() {
|
|||||||
return data.data.items ?? []
|
return data.data.items ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DistributionSiteDefaults {
|
||||||
|
default_settlement_hold_days: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchDistributionSiteDefaults() {
|
||||||
|
const { data } = await apiClient.get<ApiResponse<DistributionSiteDefaults>>(
|
||||||
|
'/admin/distribution/site-defaults'
|
||||||
|
)
|
||||||
|
return data.data
|
||||||
|
}
|
||||||
|
|
||||||
export async function onboardDistributionSite(payload: OnboardDistributionSitePayload) {
|
export async function onboardDistributionSite(payload: OnboardDistributionSitePayload) {
|
||||||
const { data } = await apiClient.post<ApiResponse<OnboardDistributionSiteResult>>(
|
const { data } = await apiClient.post<ApiResponse<OnboardDistributionSiteResult>>(
|
||||||
'/admin/distribution/sites/onboard',
|
'/admin/distribution/sites/onboard',
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
|||||||
import { Plus, Shop, Link, DocumentCopy, Refresh, Clock } from '@element-plus/icons-vue'
|
import { Plus, Shop, Link, DocumentCopy, Refresh, Clock } from '@element-plus/icons-vue'
|
||||||
import {
|
import {
|
||||||
fetchDistributionSites,
|
fetchDistributionSites,
|
||||||
|
fetchDistributionSiteDefaults,
|
||||||
onboardDistributionSite,
|
onboardDistributionSite,
|
||||||
syncDistributionSiteAccounts,
|
syncDistributionSiteAccounts,
|
||||||
updateDistributionSite,
|
updateDistributionSite,
|
||||||
@@ -16,6 +17,7 @@ import { readError } from '@/shared/utils/error'
|
|||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const sites = ref<DistributionSite[]>([])
|
const sites = ref<DistributionSite[]>([])
|
||||||
|
const defaultSettlementHoldDays = ref(7)
|
||||||
|
|
||||||
const onboardDialogVisible = ref(false)
|
const onboardDialogVisible = ref(false)
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
@@ -29,13 +31,13 @@ const settlementSubmitting = ref(false)
|
|||||||
const settlementForm = reactive({
|
const settlementForm = reactive({
|
||||||
site_id: 0,
|
site_id: 0,
|
||||||
site_name: '',
|
site_name: '',
|
||||||
settlement_hold_days: 7,
|
settlement_hold_days: defaultSettlementHoldDays.value,
|
||||||
})
|
})
|
||||||
|
|
||||||
function handleOpenSettlement(row: DistributionSite) {
|
function handleOpenSettlement(row: DistributionSite) {
|
||||||
settlementForm.site_id = row.id
|
settlementForm.site_id = row.id
|
||||||
settlementForm.site_name = row.site_name
|
settlementForm.site_name = row.site_name
|
||||||
settlementForm.settlement_hold_days = row.settlement_hold_days ?? 7
|
settlementForm.settlement_hold_days = row.settlement_hold_days ?? defaultSettlementHoldDays.value
|
||||||
settlementDialogVisible.value = true
|
settlementDialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +65,7 @@ const onboardForm = reactive({
|
|||||||
admin_password: '',
|
admin_password: '',
|
||||||
admin_nickname: '',
|
admin_nickname: '',
|
||||||
member_type: 'operator',
|
member_type: 'operator',
|
||||||
settlement_hold_days: 7,
|
settlement_hold_days: defaultSettlementHoldDays.value,
|
||||||
display_account_enabled: true,
|
display_account_enabled: true,
|
||||||
tax_enabled: false,
|
tax_enabled: false,
|
||||||
tax_rate_bps: 100,
|
tax_rate_bps: 100,
|
||||||
@@ -102,7 +104,7 @@ function handleOpenOnboard() {
|
|||||||
onboardForm.admin_password = ''
|
onboardForm.admin_password = ''
|
||||||
onboardForm.admin_nickname = ''
|
onboardForm.admin_nickname = ''
|
||||||
onboardForm.member_type = 'operator'
|
onboardForm.member_type = 'operator'
|
||||||
onboardForm.settlement_hold_days = 7
|
onboardForm.settlement_hold_days = defaultSettlementHoldDays.value
|
||||||
onboardForm.display_account_enabled = true
|
onboardForm.display_account_enabled = true
|
||||||
onboardForm.tax_enabled = false
|
onboardForm.tax_enabled = false
|
||||||
onboardForm.tax_rate_bps = 100
|
onboardForm.tax_rate_bps = 100
|
||||||
@@ -171,6 +173,16 @@ async function handleSyncAccounts(row: DistributionSite) {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadSites()
|
loadSites()
|
||||||
|
fetchDistributionSiteDefaults()
|
||||||
|
.then(res => {
|
||||||
|
const days = res?.default_settlement_hold_days
|
||||||
|
if (typeof days === 'number' && days >= 0 && days <= 365) {
|
||||||
|
defaultSettlementHoldDays.value = days
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// 拉取失败时保持内置默认值 7,不弹错误提示
|
||||||
|
})
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user