5 分钟发布一次
This commit is contained in:
@@ -2,8 +2,10 @@ package listing
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"hfb_sys/backend/pkg/response"
|
"hfb_sys/backend/pkg/response"
|
||||||
|
|
||||||
@@ -56,6 +58,8 @@ func writeListingError(c *gin.Context, err error) {
|
|||||||
response.BadRequest(c, "发布信息不符合规则")
|
response.BadRequest(c, "发布信息不符合规则")
|
||||||
case errors.Is(err, ErrListingLocked):
|
case errors.Is(err, ErrListingLocked):
|
||||||
response.Error(c, http.StatusConflict, "listing_locked", "当前发布不可修改")
|
response.Error(c, http.StatusConflict, "listing_locked", "当前发布不可修改")
|
||||||
|
case errors.Is(err, ErrPublishCooldown):
|
||||||
|
response.Error(c, http.StatusTooManyRequests, "publish_cooldown", publishCooldownMessage(err))
|
||||||
case IsNotFound(err):
|
case IsNotFound(err):
|
||||||
response.Error(c, http.StatusNotFound, "not_found", "发布不存在")
|
response.Error(c, http.StatusNotFound, "not_found", "发布不存在")
|
||||||
default:
|
default:
|
||||||
@@ -72,3 +76,25 @@ func isUploadValidationError(err error) bool {
|
|||||||
var validationErr UploadValidationError
|
var validationErr UploadValidationError
|
||||||
return errors.As(err, &validationErr)
|
return errors.As(err, &validationErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func publishCooldownMessage(err error) string {
|
||||||
|
var cooldownErr PublishCooldownError
|
||||||
|
if !errors.As(err, &cooldownErr) {
|
||||||
|
return "发布太频繁,请稍后再试"
|
||||||
|
}
|
||||||
|
cooldownMinutes := ceilDuration(cooldownErr.Cooldown, time.Minute)
|
||||||
|
if cooldownMinutes <= 0 {
|
||||||
|
return "发布太频繁,请稍后再试"
|
||||||
|
}
|
||||||
|
if cooldownErr.Remaining < time.Minute {
|
||||||
|
return fmt.Sprintf("发布太频繁,每%d分钟只能发布一个账号,请%d秒后再试", cooldownMinutes, ceilDuration(cooldownErr.Remaining, time.Second))
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("发布太频繁,每%d分钟只能发布一个账号,请%d分钟后再试", cooldownMinutes, ceilDuration(cooldownErr.Remaining, time.Minute))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ceilDuration(value time.Duration, unit time.Duration) int {
|
||||||
|
if value <= 0 || unit <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return int((value + unit - time.Nanosecond) / unit)
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,12 +11,17 @@ import (
|
|||||||
|
|
||||||
"gorm.io/datatypes"
|
"gorm.io/datatypes"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r *Repository) Create(ctx context.Context, ownerID uint64, req CreateRequest, reviewRequired bool) (*ListingDTO, error) {
|
func (r *Repository) Create(ctx context.Context, ownerID uint64, req CreateRequest, reviewRequired bool, publishCooldown time.Duration) (*ListingDTO, error) {
|
||||||
var dto *ListingDTO
|
var dto *ListingDTO
|
||||||
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
listingNo, err := r.nextListingNo(tx, time.Now())
|
now := time.Now()
|
||||||
|
if err := r.ensureCreateCooldown(tx, ownerID, publishCooldown, now); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
listingNo, err := r.nextListingNo(tx, now)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -77,6 +82,33 @@ func (r *Repository) Create(ctx context.Context, ownerID uint64, req CreateReque
|
|||||||
return dto, err
|
return dto, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Repository) ensureCreateCooldown(tx *gorm.DB, ownerID uint64, cooldown time.Duration, now time.Time) error {
|
||||||
|
if cooldown <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var user model.User
|
||||||
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Select("id").First(&user, ownerID).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var latest model.RentalListing
|
||||||
|
err := tx.Where("owner_id = ?", ownerID).
|
||||||
|
Order("created_at DESC, id DESC").
|
||||||
|
First(&latest).Error
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
remaining := latest.CreatedAt.Add(cooldown).Sub(now)
|
||||||
|
if remaining > 0 {
|
||||||
|
return PublishCooldownError{Cooldown: cooldown, Remaining: remaining}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type externalUploadCreate struct {
|
type externalUploadCreate struct {
|
||||||
UploaderName string
|
UploaderName string
|
||||||
ClientUploadTime *time.Time
|
ClientUploadTime *time.Time
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrDependencyUnavailable = errors.New("dependency unavailable")
|
ErrDependencyUnavailable = errors.New("dependency unavailable")
|
||||||
ErrInvalidInput = errors.New("invalid listing input")
|
ErrInvalidInput = errors.New("invalid listing input")
|
||||||
ErrListingLocked = errors.New("listing locked")
|
ErrListingLocked = errors.New("listing locked")
|
||||||
|
ErrPublishCooldown = errors.New("listing publish cooldown")
|
||||||
ErrMissingTitle = errors.New("missing listing title")
|
ErrMissingTitle = errors.New("missing listing title")
|
||||||
ErrMissingServerRegion = errors.New("missing server region")
|
ErrMissingServerRegion = errors.New("missing server region")
|
||||||
ErrInvalidPrice = errors.New("invalid listing price")
|
ErrInvalidPrice = errors.New("invalid listing price")
|
||||||
@@ -40,11 +42,26 @@ type ConfigReader interface {
|
|||||||
const (
|
const (
|
||||||
reviewRequiredConfigKey = "listing.review_required"
|
reviewRequiredConfigKey = "listing.review_required"
|
||||||
publishOptionsConfigKey = "listing.publish_options"
|
publishOptionsConfigKey = "listing.publish_options"
|
||||||
|
publishCooldownMinutesKey = "listing.publish_cooldown_minutes"
|
||||||
defaultFireLevelMin = 38
|
defaultFireLevelMin = 38
|
||||||
|
defaultPublishCooldownMinutes = 5
|
||||||
maxExternalUploadItems = 10
|
maxExternalUploadItems = 10
|
||||||
defaultUploadScreenshot = "/api/listings/default-upload-screenshot"
|
defaultUploadScreenshot = "/api/listings/default-upload-screenshot"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type PublishCooldownError struct {
|
||||||
|
Cooldown time.Duration
|
||||||
|
Remaining time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e PublishCooldownError) Error() string {
|
||||||
|
return ErrPublishCooldown.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e PublishCooldownError) Is(target error) bool {
|
||||||
|
return target == ErrPublishCooldown
|
||||||
|
}
|
||||||
|
|
||||||
type FireLevelTooLowError struct {
|
type FireLevelTooLowError struct {
|
||||||
Min int
|
Min int
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Service) reviewRequired(ctx context.Context) (bool, error) {
|
func (s *Service) reviewRequired(ctx context.Context) (bool, error) {
|
||||||
@@ -42,3 +43,21 @@ func (s *Service) publishRules(ctx context.Context) (publishRules, error) {
|
|||||||
}
|
}
|
||||||
return rules, nil
|
return rules, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) publishCooldown(ctx context.Context) (time.Duration, error) {
|
||||||
|
if s.config == nil {
|
||||||
|
return time.Duration(defaultPublishCooldownMinutes) * time.Minute, nil
|
||||||
|
}
|
||||||
|
value, err := s.config.FindValue(ctx, publishCooldownMinutesKey)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
minutes, err := strconv.Atoi(strings.TrimSpace(value))
|
||||||
|
if err != nil {
|
||||||
|
return time.Duration(defaultPublishCooldownMinutes) * time.Minute, nil
|
||||||
|
}
|
||||||
|
if minutes <= 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
return time.Duration(minutes) * time.Minute, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,11 +21,15 @@ func (s *Service) Create(ctx context.Context, ownerID uint64, req CreateRequest)
|
|||||||
if err := validateRequiredOnlineTime(req); err != nil {
|
if err := validateRequiredOnlineTime(req); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
cooldown, err := s.publishCooldown(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
reviewRequired, err := s.reviewRequired(ctx)
|
reviewRequired, err := s.reviewRequired(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return s.repo.Create(ctx, ownerID, req, reviewRequired)
|
return s.repo.Create(ctx, ownerID, req, reviewRequired, cooldown)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Update(ctx context.Context, ownerID uint64, id uint64, req UpdateRequest) (*ListingDTO, error) {
|
func (s *Service) Update(ctx context.Context, ownerID uint64, id uint64, req UpdateRequest) (*ListingDTO, error) {
|
||||||
|
|||||||
@@ -1,13 +1,22 @@
|
|||||||
package listing
|
package listing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"hfb_sys/backend/internal/database"
|
"hfb_sys/backend/internal/database"
|
||||||
"hfb_sys/backend/internal/model"
|
"hfb_sys/backend/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type stubConfigReader map[string]string
|
||||||
|
|
||||||
|
func (s stubConfigReader) FindValue(_ context.Context, key string) (string, error) {
|
||||||
|
return s[key], nil
|
||||||
|
}
|
||||||
|
|
||||||
func TestConsumableValueOnlyCountsChargedResources(t *testing.T) {
|
func TestConsumableValueOnlyCountsChargedResources(t *testing.T) {
|
||||||
value := consumableValue(map[string]any{
|
value := consumableValue(map[string]any{
|
||||||
"resources": []any{
|
"resources": []any{
|
||||||
@@ -108,6 +117,62 @@ func TestCreateRequiresPublishAgreements(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPublishCooldownUsesConfiguredMinutes(t *testing.T) {
|
||||||
|
service := NewService(nil, stubConfigReader{publishCooldownMinutesKey: "2"})
|
||||||
|
|
||||||
|
cooldown, err := service.publishCooldown(t.Context())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
if cooldown != 2*time.Minute {
|
||||||
|
t.Fatalf("expected 2m cooldown, got %s", cooldown)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPublishCooldownCanBeDisabled(t *testing.T) {
|
||||||
|
service := NewService(nil, stubConfigReader{publishCooldownMinutesKey: "0"})
|
||||||
|
|
||||||
|
cooldown, err := service.publishCooldown(t.Context())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
if cooldown != 0 {
|
||||||
|
t.Fatalf("expected disabled cooldown, got %s", cooldown)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRepositoryCreateRejectsRecentOwnerPublish(t *testing.T) {
|
||||||
|
db := database.NewTestDB()
|
||||||
|
if err := db.AutoMigrate(&model.User{}, &model.GameAccount{}, &model.RentalListing{}); err != nil {
|
||||||
|
t.Fatalf("failed to migrate test db: %v", err)
|
||||||
|
}
|
||||||
|
user := model.User{Phone: "18800000000", RealnameStatus: "verified", Status: "active"}
|
||||||
|
if err := db.Create(&user).Error; err != nil {
|
||||||
|
t.Fatalf("failed to create user: %v", err)
|
||||||
|
}
|
||||||
|
latestCreatedAt := time.Now().Add(-2 * time.Minute)
|
||||||
|
if err := db.Create(&model.RentalListing{
|
||||||
|
ListingNo: "202606270001",
|
||||||
|
AccountID: 1,
|
||||||
|
OwnerID: user.ID,
|
||||||
|
Status: "published",
|
||||||
|
CreatedAt: latestCreatedAt,
|
||||||
|
ReviewStatus: "approved",
|
||||||
|
}).Error; err != nil {
|
||||||
|
t.Fatalf("failed to create latest listing: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
repo := NewRepository(db, nil)
|
||||||
|
_, err := repo.Create(t.Context(), user.ID, validCreateRequest(), false, 5*time.Minute)
|
||||||
|
var cooldownErr PublishCooldownError
|
||||||
|
if !errors.As(err, &cooldownErr) {
|
||||||
|
t.Fatalf("expected PublishCooldownError, got %v", err)
|
||||||
|
}
|
||||||
|
if cooldownErr.Remaining <= 0 {
|
||||||
|
t.Fatalf("expected positive remaining cooldown, got %s", cooldownErr.Remaining)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRepositoryUpdateAllowsOfflineListingResubmit(t *testing.T) {
|
func TestRepositoryUpdateAllowsOfflineListingResubmit(t *testing.T) {
|
||||||
db := database.NewTestDB()
|
db := database.NewTestDB()
|
||||||
if err := db.AutoMigrate(&model.GameAccount{}, &model.RentalListing{}); err != nil {
|
if err := db.AutoMigrate(&model.GameAccount{}, &model.RentalListing{}); err != nil {
|
||||||
@@ -127,7 +192,7 @@ func TestRepositoryUpdateAllowsOfflineListingResubmit(t *testing.T) {
|
|||||||
"online_time": map[string]any{"start": "09:00", "end": "23:00"},
|
"online_time": map[string]any{"start": "09:00", "end": "23:00"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
created, err := repo.Create(t.Context(), 1, req, false)
|
created, err := repo.Create(t.Context(), 1, req, false, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create listing: %v", err)
|
t.Fatalf("failed to create listing: %v", err)
|
||||||
}
|
}
|
||||||
@@ -148,6 +213,22 @@ func TestRepositoryUpdateAllowsOfflineListingResubmit(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validCreateRequest() CreateRequest {
|
||||||
|
return CreateRequest{
|
||||||
|
Title: "测试账号",
|
||||||
|
ServerRegion: "QQ",
|
||||||
|
LoginPlatform: "QQ账号密码",
|
||||||
|
RankLevel: "黑鹰",
|
||||||
|
HafCoinAmount: 100000000,
|
||||||
|
PriceCent: 10000,
|
||||||
|
DepositAmountCent: 50000,
|
||||||
|
ScreenshotURLS: []string{"https://example.com/a.png"},
|
||||||
|
AssetSummary: map[string]any{
|
||||||
|
"online_time": map[string]any{"start": "09:00", "end": "23:00"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestApplySellerListingPriceUsesSellerTotalPrice(t *testing.T) {
|
func TestApplySellerListingPriceUsesSellerTotalPrice(t *testing.T) {
|
||||||
item := &ListingDTO{
|
item := &ListingDTO{
|
||||||
PriceCent: 23800,
|
PriceCent: 23800,
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ var defaultConfigs = []defaultConfig{
|
|||||||
{Key: "order.pending_payment_timeout_minutes", Value: "15", Description: "订单待支付超时取消分钟数"},
|
{Key: "order.pending_payment_timeout_minutes", Value: "15", Description: "订单待支付超时取消分钟数"},
|
||||||
{Key: "order.return_overdue_grace_minutes", Value: "10", Description: "预计截止后结账宽限分钟数"},
|
{Key: "order.return_overdue_grace_minutes", Value: "10", Description: "预计截止后结账宽限分钟数"},
|
||||||
{Key: "listing.review_required", Value: "false", Description: "发布账号是否需要后台人工审核"},
|
{Key: "listing.review_required", Value: "false", Description: "发布账号是否需要后台人工审核"},
|
||||||
|
{Key: "listing.publish_cooldown_minutes", Value: "5", Description: "同一用户两次新增发布账号的最小间隔分钟数(0 表示关闭)"},
|
||||||
{Key: listingPublishAgreementsConfigKey, Value: defaultListingPublishAgreementsConfigValue(), Description: "发布账号前协议配置 JSON"},
|
{Key: listingPublishAgreementsConfigKey, Value: defaultListingPublishAgreementsConfigValue(), Description: "发布账号前协议配置 JSON"},
|
||||||
{Key: orderAgreementsConfigKey, Value: defaultOrderAgreementsConfigValue(), Description: "下单前协议配置 JSON"},
|
{Key: orderAgreementsConfigKey, Value: defaultOrderAgreementsConfigValue(), Description: "下单前协议配置 JSON"},
|
||||||
{Key: postRentalNoticeConfigKey, Value: defaultPostRentalNoticeConfigValue(), Description: "租后须知配置 JSON"},
|
{Key: postRentalNoticeConfigKey, Value: defaultPostRentalNoticeConfigValue(), Description: "租后须知配置 JSON"},
|
||||||
@@ -60,6 +61,7 @@ var adminVisibleConfigKeys = []string{
|
|||||||
"integration.paddle_ocr_model",
|
"integration.paddle_ocr_model",
|
||||||
"integration.paddle_ocr_token",
|
"integration.paddle_ocr_token",
|
||||||
"listing.publish_agreements",
|
"listing.publish_agreements",
|
||||||
|
"listing.publish_cooldown_minutes",
|
||||||
"listing.publish_options",
|
"listing.publish_options",
|
||||||
"listing.review_required",
|
"listing.review_required",
|
||||||
"listing.sale_price_config",
|
"listing.sale_price_config",
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package systemconfig
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Service) Update(ctx context.Context, actorID uint64, key string, req UpdateRequest, meta AuditMeta) (*ConfigDTO, error) {
|
func (s *Service) Update(ctx context.Context, actorID uint64, key string, req UpdateRequest, meta AuditMeta) (*ConfigDTO, error) {
|
||||||
@@ -11,5 +13,11 @@ func (s *Service) Update(ctx context.Context, actorID uint64, key string, req Up
|
|||||||
if key == "" || (req.Value == "" && key != "integration.paddle_ocr_token") {
|
if key == "" || (req.Value == "" && key != "integration.paddle_ocr_token") {
|
||||||
return nil, ErrInvalidConfig
|
return nil, ErrInvalidConfig
|
||||||
}
|
}
|
||||||
|
if key == "listing.publish_cooldown_minutes" {
|
||||||
|
minutes, err := strconv.Atoi(strings.TrimSpace(req.Value))
|
||||||
|
if err != nil || minutes < 0 {
|
||||||
|
return nil, ErrInvalidConfig
|
||||||
|
}
|
||||||
|
}
|
||||||
return s.repo.Update(ctx, actorID, key, req, meta)
|
return s.repo.Update(ctx, actorID, key, req, meta)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
|
||||||
|
INSERT INTO system_configs (`key`, `value`, description) VALUES
|
||||||
|
('listing.publish_cooldown_minutes', '5', '同一用户两次新增发布账号的最小间隔分钟数(0 表示关闭)')
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
description = VALUES(description);
|
||||||
|
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
|
||||||
|
DELETE FROM system_configs WHERE `key` = 'listing.publish_cooldown_minutes';
|
||||||
|
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -81,6 +81,9 @@ const publishConfig = computed(
|
|||||||
const salePriceConfig = computed(
|
const salePriceConfig = computed(
|
||||||
() => configs.value.find(item => item.key === 'listing.sale_price_config') || null
|
() => configs.value.find(item => item.key === 'listing.sale_price_config') || null
|
||||||
)
|
)
|
||||||
|
const listingPublishCooldownConfig = computed(
|
||||||
|
() => configs.value.find(item => item.key === 'listing.publish_cooldown_minutes') || null
|
||||||
|
)
|
||||||
const homeAnnouncementsConfig = computed(
|
const homeAnnouncementsConfig = computed(
|
||||||
() => configs.value.find(item => item.key === 'mobile.home_announcements') || null
|
() => configs.value.find(item => item.key === 'mobile.home_announcements') || null
|
||||||
)
|
)
|
||||||
@@ -128,6 +131,7 @@ const regularConfigs = computed(() =>
|
|||||||
item =>
|
item =>
|
||||||
item.key !== 'listing.publish_options' &&
|
item.key !== 'listing.publish_options' &&
|
||||||
item.key !== 'listing.sale_price_config' &&
|
item.key !== 'listing.sale_price_config' &&
|
||||||
|
item.key !== 'listing.publish_cooldown_minutes' &&
|
||||||
item.key !== 'mobile.home_announcements' &&
|
item.key !== 'mobile.home_announcements' &&
|
||||||
item.key !== 'mobile.home_banners' &&
|
item.key !== 'mobile.home_banners' &&
|
||||||
item.key !== 'listing.publish_agreements' &&
|
item.key !== 'listing.publish_agreements' &&
|
||||||
@@ -341,6 +345,14 @@ function formatHomeConfigStatus(row: SystemConfig | null, fallback: string) {
|
|||||||
return formatDateTime(row.updated_at, fallback)
|
return formatDateTime(row.updated_at, fallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatPublishCooldown(value: string) {
|
||||||
|
const trimmed = value.trim()
|
||||||
|
if (!/^\d+$/.test(trimmed)) return '配置异常'
|
||||||
|
const minutes = Number(trimmed)
|
||||||
|
if (minutes === 0) return '已关闭'
|
||||||
|
return `${minutes} 分钟`
|
||||||
|
}
|
||||||
|
|
||||||
function formatConfigValue(row: SystemConfig) {
|
function formatConfigValue(row: SystemConfig) {
|
||||||
if (row.key.includes('token')) {
|
if (row.key.includes('token')) {
|
||||||
return row.value.trim() ? '已配置' : '未配置'
|
return row.value.trim() ? '已配置' : '未配置'
|
||||||
@@ -398,6 +410,41 @@ function formatConfigValue(row: SystemConfig) {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section v-if="listingPublishCooldownConfig" class="publish-config-panel">
|
||||||
|
<div class="publish-config-main">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">Publish Guard</p>
|
||||||
|
<h2>发布频率限制</h2>
|
||||||
|
<span>限制同一用户新增发布账号的间隔,默认 5 分钟;填 0 可关闭限制。</span>
|
||||||
|
</div>
|
||||||
|
<el-button type="primary" @click="openEdit(listingPublishCooldownConfig)">
|
||||||
|
编辑限制
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="publish-stat-grid home-stat-grid">
|
||||||
|
<div class="publish-stat">
|
||||||
|
<strong>{{ formatPublishCooldown(listingPublishCooldownConfig.value) }}</strong>
|
||||||
|
<span>当前间隔</span>
|
||||||
|
</div>
|
||||||
|
<div class="publish-stat">
|
||||||
|
<strong>用户维度</strong>
|
||||||
|
<span>每个用户单独计算</span>
|
||||||
|
</div>
|
||||||
|
<div class="publish-stat">
|
||||||
|
<strong>新增发布</strong>
|
||||||
|
<span>不限制编辑和下架</span>
|
||||||
|
</div>
|
||||||
|
<div class="publish-stat">
|
||||||
|
<strong>键</strong>
|
||||||
|
<span>{{ listingPublishCooldownConfig.key }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="publish-stat">
|
||||||
|
<strong>更新</strong>
|
||||||
|
<span>{{ formatHomeConfigStatus(listingPublishCooldownConfig, '未初始化') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section v-if="salePriceConfig" class="publish-config-panel">
|
<section v-if="salePriceConfig" class="publish-config-panel">
|
||||||
<div class="publish-config-main">
|
<div class="publish-config-main">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
Reference in New Issue
Block a user