Files

242 lines
6.7 KiB
Go

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
}