实现外部商品调价

This commit is contained in:
yml2213
2026-07-26 19:17:09 +08:00
parent 6b44ab599d
commit 99e8eb28af
14 changed files with 1163 additions and 17 deletions
+18
View File
@@ -16,6 +16,7 @@ type ListingDTO struct {
OwnerPhone string `json:"owner_phone,omitempty"`
OwnerNickname string `json:"owner_nickname,omitempty"`
SourceChannel string `json:"source_channel,omitempty"`
IsExternalUpload bool `json:"is_external_upload"`
Title string `json:"title"`
Description string `json:"description"`
GameName string `json:"game_name"`
@@ -70,6 +71,23 @@ type AdminPriceAdjustRequest struct {
Reason string `json:"reason"`
}
const (
ExternalAdjustModeDiscountAmount = "discount_amount"
ExternalAdjustModeSaleRatio = "sale_ratio"
)
type AdminExternalListingAdjustRequest struct {
HafCoinAmount int64 `json:"haf_coin_amount" binding:"required,min=1"`
AWMBullets int `json:"awm_bullets" binding:"min=0,max=1000000"`
Level6Helmets int `json:"level6_helmets" binding:"min=0,max=1000000"`
Level6Armor int `json:"level6_armor" binding:"min=0,max=1000000"`
DepositAmountCent int64 `json:"deposit_amount_cent" binding:"min=0"`
AdjustMode string `json:"adjust_mode" binding:"required,oneof=discount_amount sale_ratio"`
DiscountAmountCent int64 `json:"discount_amount_cent" binding:"min=0"`
SaleRatio float64 `json:"sale_ratio" binding:"min=0"`
Reason string `json:"reason" binding:"required,max=255"`
}
type TransferOwnerRequest struct {
TargetUserID uint64 `json:"target_user_id" binding:"required"`
Reason string `json:"reason"`
@@ -0,0 +1,275 @@
package listing
import (
"context"
"math"
"strings"
"time"
"hfb_sys/backend/internal/model"
"gorm.io/gorm"
)
const maxExternalSaleRatioIncrement = 10.0
type externalAdjustmentResult struct {
Resources []any
ReferenceRatio float64
ReferenceCoinPrice float64
SaleRatio float64
SellerCoinPrice float64
SellerTotalPrice float64
ConsumablePrice float64
ActualDiscountAmount float64
PlatformPricing externalPlatformPricing
}
func (s *Service) AdjustExternalListing(
ctx context.Context,
adminID uint64,
listingID uint64,
req AdminExternalListingAdjustRequest,
meta AuditMeta,
) (*ListingDTO, error) {
if s.repo == nil {
return nil, ErrDependencyUnavailable
}
if listingID == 0 || req.HafCoinAmount <= 0 || req.DepositAmountCent < 0 ||
req.AWMBullets < 0 || req.Level6Helmets < 0 || req.Level6Armor < 0 ||
strings.TrimSpace(req.Reason) == "" {
return nil, ErrExternalAdjustment
}
switch req.AdjustMode {
case ExternalAdjustModeDiscountAmount:
if req.DiscountAmountCent < 0 {
return nil, ErrExternalAdjustment
}
case ExternalAdjustModeSaleRatio:
if req.SaleRatio <= 0 || math.IsNaN(req.SaleRatio) || math.IsInf(req.SaleRatio, 0) {
return nil, ErrExternalAdjustment
}
default:
return nil, ErrExternalAdjustment
}
config, err := s.externalSalePriceConfig(ctx)
if err != nil {
return nil, err
}
return s.repo.AdjustExternalListing(ctx, adminID, listingID, req, config, meta)
}
func (r *Repository) AdjustExternalListing(
ctx context.Context,
adminID uint64,
listingID uint64,
req AdminExternalListingAdjustRequest,
config salePriceConfig,
meta AuditMeta,
) (*ListingDTO, error) {
if r == nil || r.db == nil {
return nil, ErrDependencyUnavailable
}
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
listing, account, err := r.findForReviewUpdate(tx, listingID)
if err != nil {
return err
}
if listing.InTransaction || listing.Status == "rented" || listing.Status == "completed" {
return ErrListingLocked
}
switch listing.Status {
case "draft", "published", "offline", "abnormal":
default:
return ErrListingLocked
}
var uploadCount int64
if err := tx.Model(&model.ListingUpload{}).Where("listing_id = ?", listing.ID).Count(&uploadCount).Error; err != nil {
return err
}
if uploadCount == 0 {
return ErrExternalListingRequired
}
summary := decodeAssetSummary(account.AssetSummary)
if summary == nil {
summary = map[string]any{}
}
breakdown := ensurePriceBreakdown(summary)
referenceRatio := readSummaryNumber(breakdown["seller_reference_ratio"])
if referenceRatio <= 0 {
referenceRatio = readSummaryNumber(breakdown["seller_ratio"])
}
if referenceRatio <= 0 {
return ErrExternalAdjustment
}
before := externalAdjustmentAuditSnapshot(account, listing, summary, breakdown)
adjustment, err := calculateExternalListingAdjustment(req, summary, referenceRatio, config)
if err != nil {
return err
}
if adjustment.ConsumablePrice > 0 && req.DepositAmountCent <= yuanToCent(adjustment.ConsumablePrice) {
return ErrDepositTooLow
}
now := time.Now()
summary["resources"] = adjustment.Resources
summary["publish_ratio"] = adjustment.PlatformPricing.BuyerRatio
breakdown["seller_reference_ratio"] = adjustment.ReferenceRatio
breakdown["seller_ratio"] = adjustment.SaleRatio
breakdown["accelerated_sale_ratio"] = adjustment.SaleRatio
breakdown["seller_coin_base_price"] = adjustment.SellerCoinPrice
breakdown["seller_total_price"] = adjustment.SellerTotalPrice
breakdown["consumable_price"] = adjustment.ConsumablePrice
breakdown["buyer_coin_base_price"] = adjustment.PlatformPricing.BuyerCoinBasePrice
breakdown["buyer_total_price"] = adjustment.PlatformPricing.BuyerTotalPrice
breakdown["buyer_ratio"] = adjustment.PlatformPricing.BuyerRatio
breakdown["platform_markup_amount"] = adjustment.PlatformPricing.PlatformMarkupPrice
breakdown["platform_rule_type"] = adjustment.PlatformPricing.RuleType
breakdown["external_admin_adjust_mode"] = req.AdjustMode
breakdown["external_admin_discount_amount"] = adjustment.ActualDiscountAmount
breakdown["external_admin_adjust_reason"] = strings.TrimSpace(req.Reason)
breakdown["external_admin_adjusted_at"] = now.Format(time.RFC3339)
breakdown["external_admin_adjusted_by"] = adminID
summary["price_breakdown"] = breakdown
assetSummary, err := marshalAssetSummary(summary)
if err != nil {
return err
}
account.HafCoinAmount = req.HafCoinAmount
account.AssetSummary = assetSummary
account.Title = buildExternalListingTitle(
account.RankLevel,
readAssetString(summary, "season_insurance"),
float64(req.HafCoinAmount)/1000000,
account.LoginPlatform,
)
listing.PriceCent = yuanToCent(adjustment.PlatformPricing.BuyerTotalPrice)
listing.DepositAmountCent = req.DepositAmountCent
if err := tx.Save(account).Error; err != nil {
return err
}
if err := tx.Save(listing).Error; err != nil {
return err
}
after := externalAdjustmentAuditSnapshot(account, listing, summary, breakdown)
return appendAuditLog(tx, adminID, "listing.adjust_external", "listing", listing.ID, meta, map[string]any{
"listing_id": listing.ID,
"account_id": account.ID,
"owner_id": listing.OwnerID,
"adjust_mode": req.AdjustMode,
"requested_discount_amount_cent": req.DiscountAmountCent,
"requested_sale_ratio": req.SaleRatio,
"reason": strings.TrimSpace(req.Reason),
"before": before,
"after": after,
})
})
if err != nil {
return nil, err
}
return r.FindAdmin(ctx, listingID)
}
func calculateExternalListingAdjustment(
req AdminExternalListingAdjustRequest,
summary map[string]any,
referenceRatio float64,
config salePriceConfig,
) (externalAdjustmentResult, error) {
coinM := float64(req.HafCoinAmount) / 1000000
coinWan := float64(req.HafCoinAmount) / 10000
referenceCoinPrice := roundMoney(coinWan / referenceRatio)
if coinM <= 0 || coinWan <= 0 || referenceCoinPrice <= 0 {
return externalAdjustmentResult{}, ErrExternalAdjustment
}
saleRatio := roundRatio(req.SaleRatio)
if req.AdjustMode == ExternalAdjustModeDiscountAmount {
desiredCoinPrice := referenceCoinPrice - centToYuan(req.DiscountAmountCent)
if desiredCoinPrice <= 0 {
return externalAdjustmentResult{}, ErrSaleRatioOutOfRange
}
saleRatio = roundRatio(coinWan / desiredCoinPrice)
}
if saleRatio < referenceRatio || saleRatio > referenceRatio+maxExternalSaleRatioIncrement {
return externalAdjustmentResult{}, ErrSaleRatioOutOfRange
}
sellerCoinPrice := roundMoney(coinWan / saleRatio)
if sellerCoinPrice <= 0 {
return externalAdjustmentResult{}, ErrExternalAdjustment
}
coreResources := externalResources(ExternalUploadInventory{
AWMBullets: req.AWMBullets,
Level6Helmets: req.Level6Helmets,
Level6Armor: req.Level6Armor,
})
resources := mergeExternalCoreResources(summary, coreResources)
consumablePrice := consumableValue(map[string]any{"resources": resources})
sellerTotalPrice := roundMoney(sellerCoinPrice + consumablePrice)
platformPricing := calculateExternalPlatformPricing(
coinM,
saleRatio,
sellerCoinPrice,
consumablePrice,
config,
)
if platformPricing.BuyerCoinBasePrice <= 0 || platformPricing.BuyerTotalPrice <= 0 || platformPricing.BuyerRatio <= 0 {
return externalAdjustmentResult{}, ErrInvalidPrice
}
return externalAdjustmentResult{
Resources: resources,
ReferenceRatio: referenceRatio,
ReferenceCoinPrice: referenceCoinPrice,
SaleRatio: saleRatio,
SellerCoinPrice: sellerCoinPrice,
SellerTotalPrice: sellerTotalPrice,
ConsumablePrice: consumablePrice,
ActualDiscountAmount: roundMoney(referenceCoinPrice - sellerCoinPrice),
PlatformPricing: platformPricing,
}, nil
}
func mergeExternalCoreResources(summary map[string]any, core []any) []any {
coreKeys := map[string]struct{}{"awmAmmo": {}, "helmet6": {}, "armor6": {}}
result := append([]any{}, core...)
rawResources, _ := summary["resources"].([]any)
for _, raw := range rawResources {
row, ok := raw.(map[string]any)
if !ok {
continue
}
key, _ := row["key"].(string)
if _, isCore := coreKeys[key]; isCore {
continue
}
result = append(result, row)
}
return result
}
func externalAdjustmentAuditSnapshot(
account *model.GameAccount,
listing *model.RentalListing,
summary map[string]any,
breakdown map[string]any,
) map[string]any {
return map[string]any{
"haf_coin_amount": account.HafCoinAmount,
"awm_bullets": resourceQuantity(summary, "awmAmmo"),
"level6_helmets": resourceQuantity(summary, "helmet6"),
"level6_armor": resourceQuantity(summary, "armor6"),
"deposit_amount_cent": listing.DepositAmountCent,
"reference_ratio": readSummaryNumber(breakdown["seller_reference_ratio"]),
"sale_ratio": readSummaryNumber(breakdown["seller_ratio"]),
"seller_coin_base_price": readSummaryNumber(breakdown["seller_coin_base_price"]),
"seller_total_price": readSummaryNumber(breakdown["seller_total_price"]),
"buyer_ratio": readSummaryNumber(breakdown["buyer_ratio"]),
"buyer_total_price": readSummaryNumber(breakdown["buyer_total_price"]),
"listing_price_cent": listing.PriceCent,
}
}
@@ -0,0 +1,302 @@
package listing
import (
"encoding/json"
"errors"
"math"
"strings"
"testing"
"hfb_sys/backend/internal/database"
"hfb_sys/backend/internal/model"
"gorm.io/gorm"
)
func TestCalculateExternalListingAdjustmentByDiscountAmount(t *testing.T) {
result, err := calculateExternalListingAdjustment(
AdminExternalListingAdjustRequest{
HafCoinAmount: 100000000,
AWMBullets: 10,
Level6Helmets: 2,
Level6Armor: 1,
AdjustMode: ExternalAdjustModeDiscountAmount,
DiscountAmountCent: 2500,
},
map[string]any{},
40,
defaultSalePriceConfig(),
)
if err != nil {
t.Fatalf("按降价金额计算失败: %v", err)
}
assertExternalAdjustmentNumber(t, "参考纯币价", result.ReferenceCoinPrice, 250)
assertExternalAdjustmentNumber(t, "发布比例", result.SaleRatio, 44.4)
assertExternalAdjustmentNumber(t, "号主纯币价", result.SellerCoinPrice, 225.2)
assertExternalAdjustmentNumber(t, "实际让利", result.ActualDiscountAmount, 24.8)
assertExternalAdjustmentNumber(t, "额外物品", result.ConsumablePrice, 11.5)
assertExternalAdjustmentNumber(t, "号主合计", result.SellerTotalPrice, 236.7)
if result.SaleRatio <= result.ReferenceRatio {
t.Fatalf("降价后应进入特惠状态,参考比例 %.1f,调整比例 %.1f", result.ReferenceRatio, result.SaleRatio)
}
}
func TestCalculateExternalListingAdjustmentBySaleRatio(t *testing.T) {
result, err := calculateExternalListingAdjustment(
AdminExternalListingAdjustRequest{
HafCoinAmount: 100000000,
AdjustMode: ExternalAdjustModeSaleRatio,
SaleRatio: 45,
},
map[string]any{},
40,
defaultSalePriceConfig(),
)
if err != nil {
t.Fatalf("按发布比例计算失败: %v", err)
}
assertExternalAdjustmentNumber(t, "发布比例", result.SaleRatio, 45)
assertExternalAdjustmentNumber(t, "号主纯币价", result.SellerCoinPrice, 222.2)
assertExternalAdjustmentNumber(t, "租客比例", result.PlatformPricing.BuyerRatio, 40)
assertExternalAdjustmentNumber(t, "租客售价", result.PlatformPricing.BuyerTotalPrice, 250)
}
func TestAdjustExternalListingSynchronizesAssetsPricesAndAudit(t *testing.T) {
db, service, account, listing := newExternalAdjustmentFixture(t, "published", false, true)
updated, err := service.AdjustExternalListing(t.Context(), 88, listing.ID, AdminExternalListingAdjustRequest{
HafCoinAmount: 120000000,
AWMBullets: 10,
Level6Helmets: 2,
Level6Armor: 3,
DepositAmountCent: 50000,
AdjustMode: ExternalAdjustModeDiscountAmount,
DiscountAmountCent: 2000,
Reason: "用户申请加入特惠",
}, AuditMeta{IP: "127.0.0.1", UserAgent: "adjustment-test", RequestID: "request-1"})
if err != nil {
t.Fatalf("调整外部商品失败: %v", err)
}
if !updated.IsExternalUpload {
t.Fatal("后台详情应标记为外部上传商品")
}
if !updated.IsAccelerated {
t.Fatal("调整后应展示特惠标签")
}
if updated.SourceChannel != "淘宝" {
t.Fatalf("来源渠道 = %q,期望 淘宝", updated.SourceChannel)
}
if updated.PriceCent != 33310 || updated.DepositAmountCent != 50000 {
t.Fatalf("商品价格/押金 = %d/%d,期望 33310/50000", updated.PriceCent, updated.DepositAmountCent)
}
var storedAccount model.GameAccount
if err := db.First(&storedAccount, account.ID).Error; err != nil {
t.Fatalf("查询账号失败: %v", err)
}
if storedAccount.HafCoinAmount != 120000000 {
t.Fatalf("纯币数量 = %d,期望 120000000", storedAccount.HafCoinAmount)
}
if !strings.Contains(storedAccount.Title, "120.0M") {
t.Fatalf("商品标题未同步纯币数量: %q", storedAccount.Title)
}
summary := decodeAssetSummary(storedAccount.AssetSummary)
if resourceQuantity(summary, "awmAmmo") != 10 ||
resourceQuantity(summary, "helmet6") != 2 ||
resourceQuantity(summary, "armor6") != 3 {
t.Fatalf("额外物品数量未同步: %#v", summary["resources"])
}
if resourceQuantity(summary, "giftMed") != 4 {
t.Fatalf("非核心物品应保留: %#v", summary["resources"])
}
breakdown := ensurePriceBreakdown(summary)
assertExternalAdjustmentNumber(t, "账号发布比例", readSummaryNumber(breakdown["seller_ratio"]), 42.9)
assertExternalAdjustmentNumber(t, "号主纯币价", readSummaryNumber(breakdown["seller_coin_base_price"]), 279.7)
assertExternalAdjustmentNumber(t, "额外物品价格", readSummaryNumber(breakdown["consumable_price"]), 16.5)
assertExternalAdjustmentNumber(t, "号主合计", readSummaryNumber(breakdown["seller_total_price"]), 296.2)
assertExternalAdjustmentNumber(t, "租客比例", readSummaryNumber(breakdown["buyer_ratio"]), 37.9)
assertExternalAdjustmentNumber(t, "租客售价", readSummaryNumber(breakdown["buyer_total_price"]), 333.1)
if breakdown["external_admin_adjust_reason"] != "用户申请加入特惠" {
t.Fatalf("调整原因未写入价格快照: %#v", breakdown["external_admin_adjust_reason"])
}
var audit model.AuditLog
if err := db.Where("action = ?", "listing.adjust_external").First(&audit).Error; err != nil {
t.Fatalf("查询审计日志失败: %v", err)
}
if audit.ActorID != 88 || audit.IP != "127.0.0.1" || audit.UserAgent != "adjustment-test" {
t.Fatalf("审计操作人或请求信息不正确: %#v", audit)
}
var detail map[string]any
if err := json.Unmarshal(audit.Detail, &detail); err != nil {
t.Fatalf("解析审计详情失败: %v", err)
}
before, _ := detail["before"].(map[string]any)
after, _ := detail["after"].(map[string]any)
assertExternalAdjustmentNumber(t, "审计调整前纯币", readSummaryNumber(before["haf_coin_amount"]), 100000000)
assertExternalAdjustmentNumber(t, "审计调整后纯币", readSummaryNumber(after["haf_coin_amount"]), 120000000)
assertExternalAdjustmentNumber(t, "审计调整后比例", readSummaryNumber(after["sale_ratio"]), 42.9)
assertExternalAdjustmentNumber(t, "审计调整后售价", readSummaryNumber(after["listing_price_cent"]), 33310)
if detail["request_id"] != "request-1" {
t.Fatalf("审计 request_id = %#v,期望 request-1", detail["request_id"])
}
}
func TestAdjustExternalListingRejectsWebsiteListing(t *testing.T) {
_, service, _, listing := newExternalAdjustmentFixture(t, "published", false, false)
_, err := service.AdjustExternalListing(t.Context(), 1, listing.ID, validExternalAdjustmentRequest(), AuditMeta{})
if !errors.Is(err, ErrExternalListingRequired) {
t.Fatalf("普通站内商品应返回 ErrExternalListingRequired,实际 %v", err)
}
}
func TestAdjustExternalListingRejectsLockedStates(t *testing.T) {
tests := []struct {
name string
status string
inTransaction bool
}{
{name: "交易锁定", status: "published", inTransaction: true},
{name: "租用中", status: "rented"},
{name: "已完成", status: "completed"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, service, _, listing := newExternalAdjustmentFixture(t, tt.status, tt.inTransaction, true)
_, err := service.AdjustExternalListing(t.Context(), 1, listing.ID, validExternalAdjustmentRequest(), AuditMeta{})
if !errors.Is(err, ErrListingLocked) {
t.Fatalf("应返回 ErrListingLocked,实际 %v", err)
}
})
}
}
func TestAdjustExternalListingRejectsRatioAboveLimit(t *testing.T) {
_, service, _, listing := newExternalAdjustmentFixture(t, "published", false, true)
req := validExternalAdjustmentRequest()
req.AdjustMode = ExternalAdjustModeSaleRatio
req.SaleRatio = 50.1
_, err := service.AdjustExternalListing(t.Context(), 1, listing.ID, req, AuditMeta{})
if !errors.Is(err, ErrSaleRatioOutOfRange) {
t.Fatalf("超过参考比例加 10 应返回 ErrSaleRatioOutOfRange,实际 %v", err)
}
}
func TestAdjustExternalListingRejectsDepositNotAboveResources(t *testing.T) {
_, service, _, listing := newExternalAdjustmentFixture(t, "published", false, true)
req := validExternalAdjustmentRequest()
req.AWMBullets = 10
req.DepositAmountCent = 600
_, err := service.AdjustExternalListing(t.Context(), 1, listing.ID, req, AuditMeta{})
if !errors.Is(err, ErrDepositTooLow) {
t.Fatalf("押金不高于物品价值应返回 ErrDepositTooLow,实际 %v", err)
}
}
func newExternalAdjustmentFixture(
t *testing.T,
status string,
inTransaction bool,
external bool,
) (*gorm.DB, *Service, model.GameAccount, model.RentalListing) {
t.Helper()
db := database.NewTestDB()
if err := database.MigrateListingLifecycleTestSchema(db); err != nil {
t.Fatalf("初始化商品测试表失败: %v", err)
}
if err := db.AutoMigrate(&model.ListingUpload{}, &model.AuditLog{}); err != nil {
t.Fatalf("初始化外部商品测试表失败: %v", err)
}
user := model.User{Phone: "18800000009", Nickname: "测试号主", RealnameStatus: "verified", Status: "active"}
if err := db.Create(&user).Error; err != nil {
t.Fatalf("创建测试号主失败: %v", err)
}
createReq := externalAccountToCreateRequest("客服1", 0, ExternalAccountData{
LoginMethod: "QQ账号密码",
Rank: "黑鹰",
SafeSlots: 9,
Deposit: 500,
Currency: ExternalUploadCurrency{
HafuCoin: 100,
RecycleRatio: 40,
RecycleRent: 250,
},
DailyConsumption: ExternalDailyConsumption{Stamina: 7, Weight: 7},
Inventory: ExternalUploadInventory{
AWMBullets: 5,
Level6Helmets: 1,
Level6Armor: 1,
},
}, defaultSalePriceConfig())
createReq.AssetSummary["resources"] = append(
createReq.AssetSummary["resources"].([]any),
map[string]any{"key": "giftMed", "label": "赠送医疗物资", "price": "1元/个", "quantity": 4, "mode": "赠送"},
)
assetSummary, err := marshalAssetSummary(createReq.AssetSummary)
if err != nil {
t.Fatalf("编码资产摘要失败: %v", err)
}
account := model.GameAccount{
OwnerID: user.ID,
GameName: "delta_force",
ServerRegion: createReq.ServerRegion,
LoginPlatform: createReq.LoginPlatform,
Title: createReq.Title,
RankLevel: createReq.RankLevel,
HafCoinAmount: createReq.HafCoinAmount,
AssetSummary: assetSummary,
Status: status,
}
if err := db.Create(&account).Error; err != nil {
t.Fatalf("创建测试账号失败: %v", err)
}
listing := model.RentalListing{
ListingNo: "202607260001",
AccountID: account.ID,
OwnerID: user.ID,
PriceCent: createReq.PriceCent,
DepositAmountCent: createReq.DepositAmountCent,
InTransaction: inTransaction,
Status: status,
ReviewStatus: "approved",
}
if err := db.Create(&listing).Error; err != nil {
t.Fatalf("创建测试商品失败: %v", err)
}
if external {
listingID := listing.ID
ownerID := user.ID
if err := db.Create(&model.ListingUpload{
UploaderName: "客服1",
SourceChannel: "淘宝",
OwnerID: &ownerID,
ListingID: &listingID,
Status: "draft_created",
}).Error; err != nil {
t.Fatalf("创建外部上传记录失败: %v", err)
}
}
return db, NewService(NewRepository(db, nil), nil), account, listing
}
func validExternalAdjustmentRequest() AdminExternalListingAdjustRequest {
return AdminExternalListingAdjustRequest{
HafCoinAmount: 100000000,
DepositAmountCent: 50000,
AdjustMode: ExternalAdjustModeSaleRatio,
SaleRatio: 40,
Reason: "修正商品信息",
}
}
func assertExternalAdjustmentNumber(t *testing.T, field string, actual float64, expected float64) {
t.Helper()
if math.Abs(actual-expected) > 0.001 {
t.Fatalf("%s = %.4f,期望 %.4f", field, actual, expected)
}
}
@@ -360,11 +360,15 @@ func normalizeExternalTimePart(hourText, minuteText string) (string, bool) {
}
func externalUploadTitle(item ExternalAccountData, insurance string, hafCoinM float64) string {
return buildExternalListingTitle(item.Rank, insurance, hafCoinM, item.LoginMethod)
}
func buildExternalListingTitle(rank string, insurance string, hafCoinM float64, loginMethod string) string {
parts := []string{
strings.TrimSpace(item.Rank),
strings.TrimSpace(rank),
insurance,
fmt.Sprintf("%.1fM", hafCoinM),
strings.TrimSpace(item.LoginMethod),
strings.TrimSpace(loginMethod),
}
title := strings.TrimSpace(strings.Join(cleanStrings(parts), " "))
if title == "" {
@@ -142,6 +142,29 @@ func (h *Handler) AdjustReviewPrice(c *gin.Context) {
response.OK(c, item)
}
func (h *Handler) AdjustExternalListing(c *gin.Context) {
adminID, ok := currentAdminID(c)
if !ok {
response.Unauthorized(c, "缺少管理员上下文")
return
}
id, ok := parseID(c)
if !ok {
return
}
var req AdminExternalListingAdjustRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "外部商品调整参数不正确")
return
}
item, err := h.service.AdjustExternalListing(c.Request.Context(), adminID, id, req, auditMeta(c))
if err != nil {
writeListingError(c, err)
return
}
response.OK(c, item)
}
func (h *Handler) Reject(c *gin.Context) {
id, ok := parseID(c)
if !ok {
@@ -48,6 +48,12 @@ func writeListingError(c *gin.Context, err error) {
response.BadRequest(c, "上传人名称匹配到多个后台用户,请使用唯一用户名")
case errors.Is(err, ErrTooManyUploadItems):
response.BadRequest(c, "单次上传账号数量过多")
case errors.Is(err, ErrExternalListingRequired):
response.BadRequest(c, "仅支持修改外部 API 上传的商品")
case errors.Is(err, ErrExternalAdjustment):
response.BadRequest(c, "外部商品调整参数不正确")
case errors.Is(err, ErrSaleRatioOutOfRange):
response.BadRequest(c, "发布比例必须在参考比例至参考比例加 10 之间")
case isUploadValidationError(err):
response.BadRequest(c, err.Error())
case isFireLevelTooLow(err):
+16 -12
View File
@@ -15,18 +15,19 @@ import (
type listingRow struct {
model.RentalListing
Title string
OwnerPhone string
OwnerNickname string
SourceChannel string `gorm:"column:source_channel"`
Description string
GameName string
ServerRegion string
LoginPlatform string
RankLevel string
HafCoinAmount int64
AssetSummary datatypes.JSON `gorm:"column:asset_summary"`
ScreenshotURLS datatypes.JSON `gorm:"column:screenshot_urls"`
Title string
OwnerPhone string
OwnerNickname string
SourceChannel string `gorm:"column:source_channel"`
IsExternalUpload bool `gorm:"column:is_external_upload"`
Description string
GameName string
ServerRegion string
LoginPlatform string
RankLevel string
HafCoinAmount int64
AssetSummary datatypes.JSON `gorm:"column:asset_summary"`
ScreenshotURLS datatypes.JSON `gorm:"column:screenshot_urls"`
}
func rowsToDTO(rows []listingRow) []ListingDTO {
@@ -65,6 +66,7 @@ func sellerListings(items []ListingDTO) []ListingDTO {
func applyPublicListingURLs(item *ListingDTO) {
item.SourceChannel = ""
item.IsExternalUpload = false
item.ScreenshotURLS = publicScreenshotURLs(item.ID, item.ScreenshotURLS, item.Status, item.ReviewStatus)
if item.AssetSummary != nil {
delete(item.AssetSummary, "import_meta")
@@ -77,6 +79,7 @@ func applySellerListingPrice(item *ListingDTO) {
return
}
item.SourceChannel = ""
item.IsExternalUpload = false
if item.AssetSummary == nil {
return
}
@@ -111,6 +114,7 @@ func (row listingRow) toDTO() ListingDTO {
OwnerPhone: row.OwnerPhone,
OwnerNickname: row.OwnerNickname,
SourceChannel: row.SourceChannel,
IsExternalUpload: row.IsExternalUpload,
Title: row.Title,
Description: row.Description,
GameName: row.GameName,
@@ -211,6 +211,7 @@ func (r *Repository) baseQuery(ctx context.Context) *gorm.DB {
return r.db.WithContext(ctx).Table("rental_listings AS l").
Select(`l.*, a.title, a.description, a.game_name, a.server_region, a.login_platform, a.rank_level,
a.haf_coin_amount, a.asset_summary, a.screenshot_urls, COALESCE(u.phone, '') AS owner_phone, COALESCE(u.nickname, '') AS owner_nickname,
CASE WHEN lu.upload_id IS NULL THEN 0 ELSE 1 END AS is_external_upload,
CASE
WHEN lu.upload_id IS NULL THEN ?
WHEN COALESCE(lu.source_channel, '') = '' THEN ?
@@ -29,6 +29,9 @@ var (
ErrUploaderNotFound = errors.New("uploader not found")
ErrUploaderAmbiguous = errors.New("uploader ambiguous")
ErrTooManyUploadItems = errors.New("too many upload items")
ErrExternalListingRequired = errors.New("external listing required")
ErrExternalAdjustment = errors.New("invalid external listing adjustment")
ErrSaleRatioOutOfRange = errors.New("sale ratio out of range")
)
type Service struct {