修正外部上传加价逻辑
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user