修正外部上传加价逻辑

This commit is contained in:
yml2213
2026-07-25 15:30:57 +08:00
parent 9bb57b7094
commit 4cc35a1587
3 changed files with 332 additions and 16 deletions
@@ -0,0 +1,241 @@
package listing
import (
"context"
"encoding/json"
"math"
"sort"
"strings"
)
const salePriceConfigKey = "listing.sale_price_config"
type salePriceConfig struct {
FixedMarkupRules []saleFixedMarkupRule `json:"fixed_markup_rules"`
RatioAdjustmentRules []saleRatioAdjustmentRule `json:"ratio_adjustment_rules"`
}
type saleFixedMarkupRule struct {
MinM float64 `json:"min_m"`
MaxM float64 `json:"max_m"`
MarkupAmount float64 `json:"markup_amount"`
}
type saleRatioAdjustmentRule struct {
MinM float64 `json:"min_m"`
MaxM float64 `json:"max_m"`
RatioSubtract float64 `json:"ratio_subtract"`
}
type externalPlatformPricing struct {
BuyerCoinBasePrice float64
BuyerTotalPrice float64
BuyerRatio float64
PlatformMarkupPrice float64
RuleType string
}
func (s *Service) externalSalePriceConfig(ctx context.Context) (salePriceConfig, error) {
config := defaultSalePriceConfig()
if s.config == nil {
return config, nil
}
value, err := s.config.FindValue(ctx, salePriceConfigKey)
if err != nil {
return config, err
}
if strings.TrimSpace(value) == "" {
return config, nil
}
if err := json.Unmarshal([]byte(value), &config); err != nil {
return defaultSalePriceConfig(), nil
}
return normalizeSalePriceConfig(config), nil
}
func defaultSalePriceConfig() salePriceConfig {
return salePriceConfig{
FixedMarkupRules: []saleFixedMarkupRule{
{MinM: 10, MaxM: 30, MarkupAmount: 28},
{MinM: 30, MaxM: 50, MarkupAmount: 31},
{MinM: 50, MaxM: 70, MarkupAmount: 34},
{MinM: 70, MaxM: 90, MarkupAmount: 38},
},
RatioAdjustmentRules: []saleRatioAdjustmentRule{
{MinM: 90, MaxM: 150, RatioSubtract: 5},
{MinM: 150, MaxM: 230, RatioSubtract: 4},
{MinM: 230, MaxM: 310, RatioSubtract: 3.5},
{MinM: 310, MaxM: 390, RatioSubtract: 3},
{MinM: 390, MaxM: 470, RatioSubtract: 0},
{MinM: 470, MaxM: 550, RatioSubtract: 0},
{MinM: 550, RatioSubtract: 0},
},
}
}
func normalizeSalePriceConfig(config salePriceConfig) salePriceConfig {
config.FixedMarkupRules = filterFixedMarkupRules(config.FixedMarkupRules)
config.RatioAdjustmentRules = filterRatioAdjustmentRules(config.RatioAdjustmentRules)
if len(config.FixedMarkupRules) == 0 && len(config.RatioAdjustmentRules) == 0 {
return defaultSalePriceConfig()
}
return config
}
func filterFixedMarkupRules(rules []saleFixedMarkupRule) []saleFixedMarkupRule {
result := make([]saleFixedMarkupRule, 0, len(rules))
for _, rule := range rules {
if !isFiniteNonNegative(rule.MinM) ||
!isFiniteNonNegative(rule.MaxM) ||
!isFiniteNonNegative(rule.MarkupAmount) ||
(rule.MaxM > 0 && rule.MaxM < rule.MinM) {
continue
}
result = append(result, rule)
}
return result
}
func filterRatioAdjustmentRules(rules []saleRatioAdjustmentRule) []saleRatioAdjustmentRule {
result := make([]saleRatioAdjustmentRule, 0, len(rules))
for _, rule := range rules {
if !isFiniteNonNegative(rule.MinM) ||
!isFiniteNonNegative(rule.MaxM) ||
!isFiniteNonNegative(rule.RatioSubtract) ||
(rule.MaxM > 0 && rule.MaxM < rule.MinM) {
continue
}
result = append(result, rule)
}
return result
}
func isFiniteNonNegative(value float64) bool {
return value >= 0 && !math.IsNaN(value) && !math.IsInf(value, 0)
}
// calculateExternalPlatformPricing 将外部回收租金视为纯币价格;额外物品与平台加价均单独计算。
func calculateExternalPlatformPricing(
coinM float64,
sellerRatio float64,
sellerCoinBasePrice float64,
consumablePrice float64,
config salePriceConfig,
) externalPlatformPricing {
sellerCoinBasePrice = nonNegativeMoney(sellerCoinBasePrice)
consumablePrice = nonNegativeMoney(consumablePrice)
sellerTotalPrice := roundMoney(sellerCoinBasePrice + consumablePrice)
coinWan := coinM * 100
sellerRatio = roundRatio(sellerRatio)
if sellerRatio <= 0 && coinWan > 0 && sellerCoinBasePrice > 0 {
sellerRatio = roundRatio(coinWan / sellerCoinBasePrice)
}
if rule, ok := findRatioAdjustmentRule(config.RatioAdjustmentRules, coinM); ok && sellerRatio > 0 {
buyerRatio := sellerRatio - rule.RatioSubtract
if buyerRatio > 0 && coinWan > 0 {
buyerCoinBasePrice := roundMoney(coinWan / buyerRatio)
if buyerCoinBasePrice < sellerCoinBasePrice {
buyerCoinBasePrice = sellerCoinBasePrice
}
return buildExternalPlatformPricing(
coinWan,
sellerTotalPrice,
consumablePrice,
buyerCoinBasePrice,
"ratio_subtract",
)
}
}
if rule, ok := findFixedMarkupRule(config.FixedMarkupRules, coinM); ok {
return buildExternalPlatformPricing(
coinWan,
sellerTotalPrice,
consumablePrice,
roundMoney(sellerCoinBasePrice+rule.MarkupAmount),
"fixed_markup",
)
}
return buildExternalPlatformPricing(
coinWan,
sellerTotalPrice,
consumablePrice,
sellerCoinBasePrice,
"none",
)
}
func nonNegativeMoney(value float64) float64 {
if value <= 0 || math.IsNaN(value) || math.IsInf(value, 0) {
return 0
}
return roundMoney(value)
}
func buildExternalPlatformPricing(
coinWan float64,
sellerTotalPrice float64,
consumablePrice float64,
buyerCoinBasePrice float64,
ruleType string,
) externalPlatformPricing {
buyerCoinBasePrice = nonNegativeMoney(buyerCoinBasePrice)
buyerTotalPrice := roundMoney(buyerCoinBasePrice + consumablePrice)
return externalPlatformPricing{
BuyerCoinBasePrice: buyerCoinBasePrice,
BuyerTotalPrice: buyerTotalPrice,
BuyerRatio: roundRatio(coinWan / buyerCoinBasePrice),
PlatformMarkupPrice: roundMoney(buyerTotalPrice - sellerTotalPrice),
RuleType: ruleType,
}
}
func findFixedMarkupRule(rules []saleFixedMarkupRule, coinM float64) (saleFixedMarkupRule, bool) {
sorted := append([]saleFixedMarkupRule(nil), rules...)
sort.Slice(sorted, func(i, j int) bool {
return sorted[i].MinM < sorted[j].MinM
})
for index, rule := range sorted {
if isCoinInPriceRuleRange(rule.MinM, rule.MaxM, index, len(sorted), coinM, true, false) {
return rule, true
}
}
return saleFixedMarkupRule{}, false
}
func findRatioAdjustmentRule(rules []saleRatioAdjustmentRule, coinM float64) (saleRatioAdjustmentRule, bool) {
sorted := append([]saleRatioAdjustmentRule(nil), rules...)
sort.Slice(sorted, func(i, j int) bool {
return sorted[i].MinM < sorted[j].MinM
})
for index, rule := range sorted {
if isCoinInPriceRuleRange(rule.MinM, rule.MaxM, index, len(sorted), coinM, false, true) {
return rule, true
}
}
return saleRatioAdjustmentRule{}, false
}
func isCoinInPriceRuleRange(
minM float64,
maxM float64,
index int,
total int,
coinM float64,
includeLastMax bool,
excludeFirstMin bool,
) bool {
minMatched := coinM >= minM
if excludeFirstMin && index == 0 {
minMatched = coinM > minM
}
if !minMatched {
return false
}
if maxM <= 0 || coinM < maxM {
return true
}
return includeLastMax && index == total-1 && coinM <= maxM
}
@@ -33,11 +33,15 @@ func (s *Service) ImportExternalUpload(ctx context.Context, req ExternalUploadRe
if err != nil {
return nil, err
}
salePriceConfig, err := s.externalSalePriceConfig(ctx)
if err != nil {
return nil, err
}
clientUploadTime := parseClientUploadTime(req.UploadTime)
results := make([]ExternalUploadResult, 0, len(items))
resp := &ExternalUploadResponse{Total: len(items)}
for index, item := range items {
createReq := externalAccountToCreateRequest(uploaderName, req.UploadTime, item)
createReq := externalAccountToCreateRequest(uploaderName, req.UploadTime, item, salePriceConfig)
if err := validateRequest(createReq, rules); err != nil {
if len(items) == 1 {
return nil, err
@@ -235,9 +239,14 @@ func parseClientUploadTime(value int64) *time.Time {
return &parsed
}
func externalAccountToCreateRequest(uploaderName string, uploadTime int64, item ExternalAccountData) CreateRequest {
func externalAccountToCreateRequest(
uploaderName string,
uploadTime int64,
item ExternalAccountData,
salePriceConfig salePriceConfig,
) CreateRequest {
hafCoinM := item.Currency.HafuCoin
price := item.Currency.RecycleRent
pureCoinPrice := item.Currency.RecycleRent
ratio := item.Currency.RecycleRatio
insurance := insuranceFromSafeSlots(item.SafeSlots)
staminaLevel := levelText(item.DailyConsumption.Stamina)
@@ -245,16 +254,25 @@ func externalAccountToCreateRequest(uploaderName string, uploadTime int64, item
skins := cleanStrings(item.Inventory.Skins)
remark := strings.TrimSpace(item.Remark)
onlineTimeText := normalizeExternalOnlineTimeText(item.OwnerOnlineTime)
resources := externalResources(item.Inventory)
consumablePrice := consumableValue(map[string]any{"resources": resources})
pricing := calculateExternalPlatformPricing(
hafCoinM,
ratio,
pureCoinPrice,
consumablePrice,
salePriceConfig,
)
assetSummary := map[string]any{
"face_owner": "",
"secret_kd": item.SecretKD,
"fire_level": item.Level,
"daily_loss_m": item.DailyLossM,
"publish_ratio": ratio,
"publish_ratio": pricing.BuyerRatio,
"season_insurance": insurance,
"stamina_level": staminaLevel,
"load_level": loadLevel,
"resources": externalResources(item.Inventory),
"resources": resources,
"skin_groups": externalSkinGroups(skins),
"online_time_text": onlineTimeText,
"ban_record": normalizeBanRecord(item.BanRecord),
@@ -268,14 +286,14 @@ func externalAccountToCreateRequest(uploaderName string, uploadTime int64, item
"price_breakdown": map[string]any{
"seller_reference_ratio": ratio,
"seller_ratio": ratio,
"seller_coin_base_price": price,
"seller_total_price": price,
"consumable_price": consumableValue(map[string]any{"resources": externalResources(item.Inventory)}),
"buyer_coin_base_price": price,
"buyer_total_price": price,
"buyer_ratio": ratio,
"platform_markup_amount": 0,
"platform_rule_type": "external_upload",
"seller_coin_base_price": roundMoney(pureCoinPrice),
"seller_total_price": roundMoney(pureCoinPrice + consumablePrice),
"consumable_price": consumablePrice,
"buyer_coin_base_price": pricing.BuyerCoinBasePrice,
"buyer_total_price": pricing.BuyerTotalPrice,
"buyer_ratio": pricing.BuyerRatio,
"platform_markup_amount": pricing.PlatformMarkupPrice,
"platform_rule_type": pricing.RuleType,
},
}
if start, end, ok := parseExternalOnlineTimeRange(onlineTimeText); ok {
@@ -293,7 +311,7 @@ func externalAccountToCreateRequest(uploaderName string, uploadTime int64, item
HafCoinAmount: int64(math.Round(hafCoinM * 1000000)),
AssetSummary: assetSummary,
ScreenshotURLS: []string{defaultUploadScreenshot},
PriceCent: yuanToCent(price),
PriceCent: yuanToCent(pricing.BuyerTotalPrice),
DepositAmountCent: yuanToCent(item.Deposit),
}
}
@@ -498,7 +498,7 @@ func TestExternalAccountToCreateRequestMapsUploadFields(t *testing.T) {
Level6Armor: 17,
Skins: []string{"信条", "电锯惊魂"},
},
})
}, defaultSalePriceConfig())
if req.Description != "有语音封禁\n封禁历史:2026/01/23封禁3天" {
t.Fatalf("Description = %q", req.Description)
@@ -515,7 +515,7 @@ func TestExternalAccountToCreateRequestMapsUploadFields(t *testing.T) {
if req.HafCoinAmount != 197100000 {
t.Fatalf("expected haf coin amount 197100000, got %d", req.HafCoinAmount)
}
if req.PriceCent != 45800 || req.DepositAmountCent != 40000 {
if req.PriceCent != 59590 || req.DepositAmountCent != 40000 {
t.Fatalf("unexpected price/deposit cent %d/%d", req.PriceCent, req.DepositAmountCent)
}
if req.AssetSummary["season_insurance"] != "3*3" {
@@ -547,6 +547,63 @@ func TestExternalAccountToCreateRequestMapsUploadFields(t *testing.T) {
if len(req.ScreenshotURLS) != 1 || req.ScreenshotURLS[0] != defaultUploadScreenshot {
t.Fatalf("expected default screenshot, got %#v", req.ScreenshotURLS)
}
breakdown, ok := req.AssetSummary["price_breakdown"].(map[string]any)
if !ok {
t.Fatalf("price_breakdown = %#v", req.AssetSummary["price_breakdown"])
}
if breakdown["seller_coin_base_price"] != 458.0 ||
breakdown["consumable_price"] != 90.5 ||
breakdown["seller_total_price"] != 548.5 ||
breakdown["buyer_coin_base_price"] != 505.4 ||
breakdown["buyer_total_price"] != 595.9 ||
breakdown["buyer_ratio"] != 39.0 ||
breakdown["platform_markup_amount"] != 47.4 ||
breakdown["platform_rule_type"] != "ratio_subtract" {
t.Fatalf("unexpected price breakdown: %#v", breakdown)
}
if req.AssetSummary["publish_ratio"] != 39.0 {
t.Fatalf("publish_ratio = %#v", req.AssetSummary["publish_ratio"])
}
}
func TestExternalAccountToCreateRequestSeparatesPureCoinAndExtraItems(t *testing.T) {
req := externalAccountToCreateRequest("客服1", 1772526103000, ExternalAccountData{
LoginMethod: "QQ账号密码",
Rank: "黑鹰",
Level: 60,
SafeSlots: 9,
Deposit: 400,
Currency: ExternalUploadCurrency{
HafuCoin: 282,
RecycleRatio: 42.8,
RecycleRent: 659,
},
DailyConsumption: ExternalDailyConsumption{Stamina: 7, Weight: 7},
Inventory: ExternalUploadInventory{
AWMBullets: 10,
Level6Armor: 2,
},
}, defaultSalePriceConfig())
breakdown, ok := req.AssetSummary["price_breakdown"].(map[string]any)
if !ok {
t.Fatalf("price_breakdown = %#v", req.AssetSummary["price_breakdown"])
}
if breakdown["seller_coin_base_price"] != 659.0 ||
breakdown["consumable_price"] != 11.0 ||
breakdown["seller_total_price"] != 670.0 {
t.Fatalf("回收租金未按纯币价与额外物品拆分:%#v", breakdown)
}
if breakdown["buyer_coin_base_price"] != 717.6 ||
breakdown["buyer_total_price"] != 728.6 ||
breakdown["buyer_ratio"] != 39.3 ||
breakdown["platform_markup_amount"] != 58.6 ||
breakdown["platform_rule_type"] != "ratio_subtract" {
t.Fatalf("外部上传未按比例加价:%#v", breakdown)
}
if req.PriceCent != 72860 {
t.Fatalf("PriceCent = %d, want 72860", req.PriceCent)
}
}
func TestExternalSkinGroupsClassifiesKnownSkins(t *testing.T) {