refactor(listing): 卖家发布定价改用整数分与基点

This commit is contained in:
yml2213
2026-10-30 14:49:33 +08:00
parent 37e1980d2d
commit ce93f1636d
18 changed files with 1492 additions and 343 deletions
@@ -136,6 +136,7 @@ func (r *Repository) AdjustExternalListing(
breakdown["external_admin_adjust_reason"] = strings.TrimSpace(req.Reason)
breakdown["external_admin_adjusted_at"] = now.Format(time.RFC3339)
breakdown["external_admin_adjusted_by"] = adminID
syncCanonicalPriceBreakdownFromLegacy(breakdown)
summary["price_breakdown"] = breakdown
assetSummary, err := marshalAssetSummary(summary)
@@ -19,12 +19,16 @@ type saleFixedMarkupRule struct {
MinM float64 `json:"min_m"`
MaxM float64 `json:"max_m"`
MarkupAmount float64 `json:"markup_amount"`
// MarkupAmountCent 是规范化固定加价(分);大于 0 时优先使用。
MarkupAmountCent int64 `json:"markup_amount_cent,omitempty"`
}
type saleRatioAdjustmentRule struct {
MinM float64 `json:"min_m"`
MaxM float64 `json:"max_m"`
RatioSubtract float64 `json:"ratio_subtract"`
// RatioSubtractBPS 是规范化比例修正(基点);大于 0 时优先使用。
RatioSubtractBPS int64 `json:"ratio_subtract_bps,omitempty"`
}
type externalPlatformPricing struct {
@@ -87,7 +91,7 @@ func filterFixedMarkupRules(rules []saleFixedMarkupRule) []saleFixedMarkupRule {
for _, rule := range rules {
if !isFiniteNonNegative(rule.MinM) ||
!isFiniteNonNegative(rule.MaxM) ||
!isFiniteNonNegative(rule.MarkupAmount) ||
(rule.MarkupAmountCent <= 0 && !isFiniteNonNegative(rule.MarkupAmount)) ||
(rule.MaxM > 0 && rule.MaxM < rule.MinM) {
continue
}
@@ -101,7 +105,7 @@ func filterRatioAdjustmentRules(rules []saleRatioAdjustmentRule) []saleRatioAdju
for _, rule := range rules {
if !isFiniteNonNegative(rule.MinM) ||
!isFiniteNonNegative(rule.MaxM) ||
!isFiniteNonNegative(rule.RatioSubtract) ||
(rule.RatioSubtractBPS <= 0 && !isFiniteNonNegative(rule.RatioSubtract)) ||
(rule.MaxM > 0 && rule.MaxM < rule.MinM) {
continue
}
@@ -122,6 +126,25 @@ func calculateExternalPlatformPricing(
consumablePrice float64,
config salePriceConfig,
) externalPlatformPricing {
// 新配置提供了 cent/bps 字段时,计算优先走整数路径。
if salePriceConfigHasCentRules(config) {
hafCoinAmount := int64(math.Round(coinM * float64(hafCoinUnitsPerM)))
result := calculateExternalPlatformPricingCent(
hafCoinAmount,
ratioToBPS(sellerRatio),
centFromYuanFloat(sellerCoinBasePrice),
centFromYuanFloat(consumablePrice),
config,
)
return externalPlatformPricing{
BuyerCoinBasePrice: centToYuanFloat(result.BuyerCoinBasePriceCent),
BuyerTotalPrice: centToYuanFloat(result.BuyerTotalPriceCent),
BuyerRatio: bpsToRatio(result.BuyerRatioBPS),
PlatformMarkupPrice: centToYuanFloat(result.PlatformMarkupCent),
RuleType: result.RuleType,
}
}
sellerCoinBasePrice = nonNegativeMoney(sellerCoinBasePrice)
consumablePrice = nonNegativeMoney(consumablePrice)
sellerTotalPrice := roundMoney(sellerCoinBasePrice + consumablePrice)
@@ -42,6 +42,9 @@ func rowsToDTO(rows []listingRow) []ListingDTO {
func normalizedListingPriceCent(req CreateRequest) int64 {
if req.AssetSummary != nil {
if breakdown, ok := req.AssetSummary["price_breakdown"].(map[string]any); ok {
if buyerPriceCent := readSummaryInt64(breakdown["buyer_total_price_cent"]); buyerPriceCent > 0 {
return buyerPriceCent
}
buyerPrice := readSummaryNumber(breakdown["buyer_total_price"])
if buyerPrice > 0 {
return yuanToCent(buyerPrice)
@@ -144,11 +147,11 @@ func applySellerListingPrice(item *ListingDTO) {
if !ok {
return
}
sellerPrice := readSummaryNumber(breakdown["seller_total_price"])
if sellerPrice > 0 {
item.PriceCent = int64(math.Round(sellerPrice * 100))
sellerPriceCent := summaryMoneyCent(breakdown, "seller_total_price_cent", "seller_total_price")
if sellerPriceCent > 0 {
item.PriceCent = sellerPriceCent
}
sellerRatio := readSummaryNumber(breakdown["seller_ratio"])
sellerRatio := summaryRatio(breakdown, "seller_ratio_bps", "seller_ratio")
if sellerRatio > 0 {
item.AssetSummary["publish_ratio"] = sellerRatio
}
@@ -0,0 +1,530 @@
package listing
import (
"encoding/json"
"math"
"sort"
"strconv"
"strings"
"hfb_sys/backend/internal/modules/systemconfig"
"hfb_sys/backend/pkg/money"
)
const (
// ratioBPSScale 把比例统一到基点:1.0 = 10000 bps,1% = 100 bps。
ratioBPSScale int64 = 10000
// hafCoinUnitsPerM 是 1M 哈夫币对应的整数微单位。
hafCoinUnitsPerM int64 = 1_000_000
// maxAcceleratedSaleRatioBPSIncrement 是加速出售比例相对参考比例的上限增量(10 个比例点)。
maxAcceleratedSaleRatioBPSIncrement int64 = 10 * ratioBPSScale
)
// ratioToBPS 仅在读取后台旧 float 配置或表单输入时做一次边界换算。
func ratioToBPS(value float64) int64 {
if value <= 0 || math.IsNaN(value) || math.IsInf(value, 0) {
return 0
}
return int64(math.Round(value * float64(ratioBPSScale)))
}
func bpsToRatio(value int64) float64 {
return float64(value) / float64(ratioBPSScale)
}
// centFromYuanFloat 仅在兼容旧 float 配置/金额时做一次边界换算。
func centFromYuanFloat(value float64) int64 {
if value <= 0 || math.IsNaN(value) || math.IsInf(value, 0) {
return 0
}
return int64(math.Round(value * float64(money.CentsPerYuan)))
}
func centToYuanFloat(value int64) float64 {
return float64(value) / float64(money.CentsPerYuan)
}
func readSummaryInt64(value any) int64 {
switch typed := value.(type) {
case int:
return int64(typed)
case int32:
return int64(typed)
case int64:
return typed
case uint:
return int64(typed)
case uint64:
if typed > math.MaxInt64 {
return 0
}
return int64(typed)
case float64:
if math.IsNaN(typed) || math.IsInf(typed, 0) {
return 0
}
return int64(typed)
case float32:
return int64(typed)
case json.Number:
if parsed, err := typed.Int64(); err == nil {
return parsed
}
if parsed, err := typed.Float64(); err == nil {
return int64(parsed)
}
return 0
case string:
trimmed := strings.TrimSpace(typed)
if parsed, err := strconv.ParseInt(trimmed, 10, 64); err == nil {
return parsed
}
if parsed, err := strconv.ParseFloat(trimmed, 64); err == nil {
return int64(parsed)
}
return 0
default:
return 0
}
}
// parseUnitPriceCent 把「8元/个」「1元/2个」等旧文案精确解析为单价(分)。
func parseUnitPriceCent(priceText string) int64 {
numbers := priceNumberPattern.FindAllString(priceText, -1)
if len(numbers) == 0 {
return 0
}
amountCent, err := money.ParseYuan(numbers[0])
if err != nil || amountCent <= 0 {
return 0
}
if len(numbers) >= 2 {
countCent, err := money.ParseYuan(numbers[1])
if err == nil && countCent >= money.CentsPerYuan && countCent%money.CentsPerYuan == 0 {
return amountCent / (countCent / money.CentsPerYuan)
}
}
return amountCent
}
func quantityItemPriceCent(item systemconfig.PublishQuantityItem) int64 {
if item.PriceCent > 0 {
return item.PriceCent
}
return parseUnitPriceCent(item.Price)
}
func isGridCardResource(key, label string) bool {
return strings.TrimSpace(key) == "gridCard9" || strings.Contains(label, "9格体验卡")
}
// consumableValueCent 按 resources 的整数数量与单价(分)求和。
// 配置项 price_cent 优先;其次资源行 price_cent;最后回退旧 price 文案解析。
func consumableValueCent(summary map[string]any, quantityItems []systemconfig.PublishQuantityItem) int64 {
if summary == nil {
return 0
}
rawResources, ok := summary["resources"].([]any)
if !ok {
return 0
}
configByKey := make(map[string]systemconfig.PublishQuantityItem, len(quantityItems))
for _, item := range quantityItems {
configByKey[item.Key] = item
}
insurance, _ := summary["season_insurance"].(string)
total := int64(0)
for _, raw := range rawResources {
resource, ok := raw.(map[string]any)
if !ok {
continue
}
mode, _ := resource["mode"].(string)
if strings.TrimSpace(mode) != "收费" {
continue
}
key, _ := resource["key"].(string)
label, _ := resource["label"].(string)
if strings.TrimSpace(insurance) == "3*3" && isGridCardResource(key, label) {
continue
}
quantity := readSummaryInt64(resource["quantity"])
if quantity <= 0 {
continue
}
unitCent := int64(0)
if item, exists := configByKey[strings.TrimSpace(key)]; exists && item.PriceCent > 0 {
unitCent = item.PriceCent
}
if unitCent <= 0 {
unitCent = readSummaryInt64(resource["price_cent"])
}
if unitCent <= 0 {
priceText, _ := resource["price"].(string)
unitCent = parseUnitPriceCent(priceText)
}
if unitCent <= 0 {
continue
}
total += quantity * unitCent
}
return total
}
// recommendedDepositCent 优先使用后台 cent 配置,否则对旧 float 配置做一次边界换算。
func recommendedDepositCent(
config systemconfig.PublishDepositRecommend,
skinGroups []systemconfig.PublishOptionGroup,
selectedSkins []string,
) int64 {
baseCent := config.BaseAmountCent
if baseCent <= 0 {
baseCent = centFromYuanFloat(config.BaseAmount)
}
total := baseCent
for _, rule := range config.SkinGroupRules {
amountCent := rule.AmountPerItemCent
if amountCent <= 0 {
amountCent = centFromYuanFloat(rule.AmountPerItem)
}
if amountCent <= 0 {
continue
}
count := int64(0)
for _, group := range skinGroups {
if group.Key != rule.GroupKey {
continue
}
for _, skin := range group.Options {
if selectedSkinContains(selectedSkins, skin) {
count++
}
}
break
}
if count > 0 {
total += count * amountCent
}
}
return total
}
func selectedSkinContains(selectedSkins []string, skin string) bool {
for _, item := range selectedSkins {
if item == skin {
return true
}
}
return false
}
func calculateSellerReferenceRatioBPS(hafCoinAmount int64, summary map[string]any, options systemconfig.PublishOptionsDTO) int64 {
if hafCoinAmount <= 0 || summary == nil {
return 0
}
insurance := readSummaryString(summary["season_insurance"])
stamina := readSummaryString(summary["stamina_level"])
load := readSummaryString(summary["load_level"])
if insurance == "" || stamina == "" || load == "" {
return 0
}
baseBPS := insuranceBaseRatioBPS(options.RatioConfig.InsuranceBaseRatios, insurance)
if baseBPS <= 0 {
return 0
}
return baseBPS +
calculateRatioConfigPenaltyBPS(options, summary, stamina, load) +
coinCorrectionBPS(options.RatioConfig.CoinCorrections, hafCoinAmount) +
calculateDailyLossRatioAdjustmentBPS(summary)
}
func insuranceBaseRatioBPS(items []systemconfig.PublishInsuranceBaseRatio, insurance string) int64 {
for _, item := range items {
if item.Insurance != insurance {
continue
}
if item.RatioBPS > 0 {
return item.RatioBPS
}
return ratioToBPS(item.Ratio)
}
return 0
}
func calculateRatioConfigPenaltyBPS(options systemconfig.PublishOptionsDTO, summary map[string]any, stamina, load string) int64 {
selectedSkins := flattenSkinNames(summary)
penalty := int64(0)
for _, item := range options.RatioConfig.ConfigItems {
if ratioConfigItemMatched(item, options, selectedSkins, stamina, load) {
continue
}
if item.MissingPenaltyBPS > 0 {
penalty += item.MissingPenaltyBPS
continue
}
penalty += ratioToBPS(item.MissingPenalty)
}
return penalty
}
func coinCorrectionBPS(items []systemconfig.PublishCoinCorrection, hafCoinAmount int64) int64 {
bestThresholdMicro := int64(-1)
correction := int64(0)
for _, item := range items {
thresholdMicro := int64(math.Round(item.ThresholdM * float64(hafCoinUnitsPerM)))
if hafCoinAmount <= thresholdMicro || thresholdMicro < bestThresholdMicro {
continue
}
bestThresholdMicro = thresholdMicro
if item.CorrectionBPS > 0 {
correction = item.CorrectionBPS
} else {
correction = ratioToBPS(item.Correction)
}
}
return correction
}
func calculateDailyLossRatioAdjustmentBPS(summary map[string]any) int64 {
if summary == nil {
return 0
}
dailyLossM := readSummaryInt64(summary["daily_loss_m"])
if dailyLossM <= 10 {
return 0
}
adjustment := (dailyLossM - 10) / 10
if adjustment > 4 {
adjustment = 4
}
return adjustment * ratioBPSScale
}
func clampSaleRatioBPS(referenceBPS, requestedBPS int64) int64 {
if referenceBPS <= 0 {
return 0
}
if requestedBPS <= 0 || requestedBPS < referenceBPS {
return referenceBPS
}
maxBPS := referenceBPS + maxAcceleratedSaleRatioBPSIncrement
if requestedBPS > maxBPS {
return maxBPS
}
return requestedBPS
}
func applySiteReceiveCostDeltaBPS(referenceBPS int64, delta float64) int64 {
if referenceBPS <= 0 {
return 0
}
return referenceBPS + ratioToBPS(NormalizeSiteReceiveCostDelta(delta))
}
// sellerCoinBasePriceCentFromHafCoin 使用整数分公式:哈夫币微单位 * 100 / 比例 bps,向下取整。
func sellerCoinBasePriceCentFromHafCoin(hafCoinAmount int64, ratioBPS int64) int64 {
if hafCoinAmount <= 0 || ratioBPS <= 0 || hafCoinAmount > math.MaxInt64/100 {
return 0
}
return hafCoinAmount * 100 / ratioBPS
}
type externalPlatformPricingCent struct {
BuyerCoinBasePriceCent int64
BuyerTotalPriceCent int64
BuyerRatioBPS int64
PlatformMarkupCent int64
RuleType string
}
// calculateExternalPlatformPricingCent 是卖家发布定价的唯一算术入口,全程使用 int64。
func calculateExternalPlatformPricingCent(
hafCoinAmount int64,
sellerRatioBPS int64,
sellerCoinBasePriceCent int64,
consumablePriceCent int64,
config salePriceConfig,
) externalPlatformPricingCent {
if sellerCoinBasePriceCent < 0 {
sellerCoinBasePriceCent = 0
}
if consumablePriceCent < 0 {
consumablePriceCent = 0
}
if rule, ok := findRatioAdjustmentRuleCent(config.RatioAdjustmentRules, hafCoinAmount); ok && sellerRatioBPS > 0 {
buyerRatioBPS := sellerRatioBPS - saleRatioSubtractBPS(rule)
if buyerRatioBPS > 0 {
buyerCoinBasePriceCent := sellerCoinBasePriceCentFromHafCoin(hafCoinAmount, buyerRatioBPS)
if buyerCoinBasePriceCent < sellerCoinBasePriceCent {
buyerCoinBasePriceCent = sellerCoinBasePriceCent
}
return buildExternalPlatformPricingCent(
hafCoinAmount,
sellerCoinBasePriceCent,
consumablePriceCent,
buyerCoinBasePriceCent,
"ratio_subtract",
)
}
}
if rule, ok := findFixedMarkupRuleCent(config.FixedMarkupRules, hafCoinAmount); ok {
return buildExternalPlatformPricingCent(
hafCoinAmount,
sellerCoinBasePriceCent,
consumablePriceCent,
sellerCoinBasePriceCent+saleFixedMarkupCent(rule),
"fixed_markup",
)
}
return buildExternalPlatformPricingCent(
hafCoinAmount,
sellerCoinBasePriceCent,
consumablePriceCent,
sellerCoinBasePriceCent,
"none",
)
}
func buildExternalPlatformPricingCent(
hafCoinAmount int64,
anchorCoinBasePriceCent int64,
consumablePriceCent int64,
buyerCoinBasePriceCent int64,
ruleType string,
) externalPlatformPricingCent {
if buyerCoinBasePriceCent < 0 {
buyerCoinBasePriceCent = 0
}
buyerTotalPriceCent := buyerCoinBasePriceCent + consumablePriceCent
buyerRatioBPS := int64(0)
if buyerCoinBasePriceCent > 0 && hafCoinAmount > 0 && hafCoinAmount <= math.MaxInt64/100 {
// 展示值:由整数分反推 bps,存在向下取整误差。
buyerRatioBPS = hafCoinAmount * 100 / buyerCoinBasePriceCent
}
anchorTotalCent := anchorCoinBasePriceCent + consumablePriceCent
return externalPlatformPricingCent{
BuyerCoinBasePriceCent: buyerCoinBasePriceCent,
BuyerTotalPriceCent: buyerTotalPriceCent,
BuyerRatioBPS: buyerRatioBPS,
PlatformMarkupCent: buyerTotalPriceCent - anchorTotalCent,
RuleType: ruleType,
}
}
func saleFixedMarkupCent(rule saleFixedMarkupRule) int64 {
if rule.MarkupAmountCent > 0 {
return rule.MarkupAmountCent
}
return centFromYuanFloat(rule.MarkupAmount)
}
func saleRatioSubtractBPS(rule saleRatioAdjustmentRule) int64 {
if rule.RatioSubtractBPS > 0 {
return rule.RatioSubtractBPS
}
return ratioToBPS(rule.RatioSubtract)
}
func findFixedMarkupRuleCent(rules []saleFixedMarkupRule, hafCoinAmount int64) (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 isCoinInPriceRuleRangeCent(rule.MinM, rule.MaxM, index, len(sorted), hafCoinAmount, true, false) {
return rule, true
}
}
return saleFixedMarkupRule{}, false
}
func findRatioAdjustmentRuleCent(rules []saleRatioAdjustmentRule, hafCoinAmount int64) (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 isCoinInPriceRuleRangeCent(rule.MinM, rule.MaxM, index, len(sorted), hafCoinAmount, false, true) {
return rule, true
}
}
return saleRatioAdjustmentRule{}, false
}
func isCoinInPriceRuleRangeCent(
minM float64,
maxM float64,
index int,
total int,
hafCoinAmount int64,
includeLastMax bool,
excludeFirstMin bool,
) bool {
minMicro := int64(math.Round(minM * float64(hafCoinUnitsPerM)))
maxMicro := int64(math.Round(maxM * float64(hafCoinUnitsPerM)))
minMatched := hafCoinAmount >= minMicro
if excludeFirstMin && index == 0 {
minMatched = hafCoinAmount > minMicro
}
if !minMatched {
return false
}
if maxMicro <= 0 || hafCoinAmount < maxMicro {
return true
}
return includeLastMax && index == total-1 && hafCoinAmount <= maxMicro
}
func salePriceConfigHasCentRules(config salePriceConfig) bool {
for _, rule := range config.FixedMarkupRules {
if rule.MarkupAmountCent > 0 {
return true
}
}
for _, rule := range config.RatioAdjustmentRules {
if rule.RatioSubtractBPS > 0 {
return true
}
}
return false
}
// syncCanonicalPriceBreakdownFromLegacy 供历史审核/外部调价路径使用:
// 这些路径仍以旧 float 字段为准,写回后同步派生规范字段,避免卖家读侧优先读到过期 cent/bps。
func syncCanonicalPriceBreakdownFromLegacy(breakdown map[string]any) {
if breakdown == nil {
return
}
for bpsKey, ratioKey := range map[string]string{
"seller_reference_ratio_bps": "seller_reference_ratio",
"seller_ratio_bps": "seller_ratio",
"accelerated_sale_ratio_bps": "accelerated_sale_ratio",
"buyer_ratio_bps": "buyer_ratio",
} {
ratio := readSummaryNumber(breakdown[ratioKey])
if ratio > 0 {
breakdown[bpsKey] = ratioToBPS(ratio)
continue
}
if bps := readSummaryInt64(breakdown[bpsKey]); bps > 0 {
breakdown[bpsKey] = bps
}
}
for centKey, yuanKey := range map[string]string{
"seller_coin_base_price_cent": "seller_coin_base_price",
"seller_total_price_cent": "seller_total_price",
"consumable_price_cent": "consumable_price",
"buyer_coin_base_price_cent": "buyer_coin_base_price",
"buyer_total_price_cent": "buyer_total_price",
"platform_markup_amount_cent": "platform_markup_amount",
} {
yuan := readSummaryNumber(breakdown[yuanKey])
if yuan > 0 {
breakdown[centKey] = centFromYuanFloat(yuan)
continue
}
if cent := readSummaryInt64(breakdown[centKey]); cent > 0 {
breakdown[centKey] = cent
}
}
}
@@ -0,0 +1,227 @@
package listing
import (
"testing"
"hfb_sys/backend/internal/modules/systemconfig"
)
func TestSellerCoinBasePriceCentUsesIntegerFormula(t *testing.T) {
if got := sellerCoinBasePriceCentFromHafCoin(100_000_000, 430_000); got != 23255 {
t.Fatalf("sellerCoinBasePriceCentFromHafCoin() = %d, want 23255 (floor)", got)
}
if got := sellerCoinBasePriceCentFromHafCoin(100_000_000, 380_000); got != 26315 {
t.Fatalf("sellerCoinBasePriceCentFromHafCoin() = %d, want 26315 (floor)", got)
}
}
func TestCalculateExternalPlatformPricingCentUsesRatioRule(t *testing.T) {
config := salePriceConfig{
RatioAdjustmentRules: []saleRatioAdjustmentRule{
{MinM: 90, MaxM: 150, RatioSubtract: 5},
},
}
pricing := calculateExternalPlatformPricingCent(100_000_000, 430_000, 23255, 0, config)
if pricing.RuleType != "ratio_subtract" {
t.Fatalf("RuleType = %q, want ratio_subtract", pricing.RuleType)
}
if pricing.BuyerCoinBasePriceCent != 26315 {
t.Fatalf("BuyerCoinBasePriceCent = %d, want 26315", pricing.BuyerCoinBasePriceCent)
}
if pricing.BuyerTotalPriceCent != 26315 {
t.Fatalf("BuyerTotalPriceCent = %d, want 26315", pricing.BuyerTotalPriceCent)
}
if pricing.PlatformMarkupCent != 3060 {
t.Fatalf("PlatformMarkupCent = %d, want 3060", pricing.PlatformMarkupCent)
}
if pricing.BuyerRatioBPS != 380011 {
t.Fatalf("BuyerRatioBPS = %d, want 380011 display bps", pricing.BuyerRatioBPS)
}
}
func TestCalculateExternalPlatformPricingCentPrefersBPSAndCentConfig(t *testing.T) {
config := salePriceConfig{
RatioAdjustmentRules: []saleRatioAdjustmentRule{
{MinM: 90, MaxM: 150, RatioSubtract: 3.5, RatioSubtractBPS: 35_000},
},
}
pricing := calculateExternalPlatformPricingCent(100_000_000, 430_000, 23255, 0, config)
if pricing.BuyerCoinBasePriceCent != 25316 {
t.Fatalf("BuyerCoinBasePriceCent = %d, want 25316 from 395000 bps", pricing.BuyerCoinBasePriceCent)
}
if pricing.BuyerRatioBPS != 395007 {
t.Fatalf("BuyerRatioBPS = %d, want 395007 display bps", pricing.BuyerRatioBPS)
}
fixedConfig := salePriceConfig{
FixedMarkupRules: []saleFixedMarkupRule{
{MinM: 90, MaxM: 150, MarkupAmount: 99, MarkupAmountCent: 1234},
},
}
fixed := calculateExternalPlatformPricingCent(100_000_000, 430_000, 20000, 500, fixedConfig)
if fixed.RuleType != "fixed_markup" {
t.Fatalf("RuleType = %q, want fixed_markup", fixed.RuleType)
}
if fixed.BuyerCoinBasePriceCent != 21234 || fixed.BuyerTotalPriceCent != 21734 {
t.Fatalf("fixed pricing = %d/%d, want 21234/21734", fixed.BuyerCoinBasePriceCent, fixed.BuyerTotalPriceCent)
}
if fixed.PlatformMarkupCent != 1234 {
t.Fatalf("PlatformMarkupCent = %d, want 1234", fixed.PlatformMarkupCent)
}
}
func TestCalculateExternalPlatformPricingCentFallsBackToLegacyFloatConfig(t *testing.T) {
config := salePriceConfig{
FixedMarkupRules: []saleFixedMarkupRule{
{MinM: 90, MaxM: 150, MarkupAmount: 28},
},
}
pricing := calculateExternalPlatformPricingCent(100_000_000, 430_000, 20000, 0, config)
if pricing.RuleType != "fixed_markup" {
t.Fatalf("RuleType = %q, want fixed_markup", pricing.RuleType)
}
if pricing.BuyerCoinBasePriceCent != 22800 {
t.Fatalf("BuyerCoinBasePriceCent = %d, want 22800 from legacy 28 元", pricing.BuyerCoinBasePriceCent)
}
if centFromYuanFloat(3.5) != 350 {
t.Fatalf("centFromYuanFloat(3.5) = %d, want 350", centFromYuanFloat(3.5))
}
if ratioToBPS(3.5) != 35_000 {
t.Fatalf("ratioToBPS(3.5) = %d, want 35000", ratioToBPS(3.5))
}
}
func TestConsumableValueCentPrefersConfigAndParsesLegacyPrice(t *testing.T) {
summary := map[string]any{
"season_insurance": "3*3",
"resources": []any{
map[string]any{"key": "awmAmmo", "label": "AWM子弹", "mode": "收费", "quantity": 10, "price": "0.6元/发"},
map[string]any{"key": "gridCard9", "label": "9格体验卡", "mode": "收费", "quantity": 1, "price": "3元/张"},
map[string]any{"key": "armor6", "label": "6甲", "mode": "收费", "quantity": 2, "price": "2.5元/个"},
map[string]any{"key": "barrettAmmo", "label": "巴雷特子弹", "mode": "收费", "quantity": 3, "price": "1元/2发"},
map[string]any{"key": "gift", "label": "赠送项", "mode": "赠送", "quantity": 9, "price": "10元/个"},
},
}
items := []systemconfig.PublishQuantityItem{
{Key: "awmAmmo", Label: "AWM子弹", Price: "0.6元/发", PriceCent: 70},
}
if got := consumableValueCent(summary, items); got != 700+500+150 {
t.Fatalf("consumableValueCent() = %d, want 1350 (price_cent priority, 3*3 skips grid card)", got)
}
if got := consumableValueCent(summary, nil); got != 600+500+150 {
t.Fatalf("consumableValueCent() legacy fallback = %d, want 1250", got)
}
summary["resources"].([]any)[0].(map[string]any)["price_cent"] = 1
if got := consumableValueCent(summary, nil); got != 10+500+150 {
t.Fatalf("consumableValueCent() resource price_cent = %d, want 660", got)
}
}
func TestRecommendedDepositCentUsesCentConfigOrLegacyFloat(t *testing.T) {
skinGroups := []systemconfig.PublishOptionGroup{
{Key: "melee", Title: "刀皮", Options: []string{"信条", "暗星"}},
{Key: "operatorRed", Title: "红皮", Options: []string{"凌霄成卫"}},
}
selected := []string{"信条", "暗星", "凌霄成卫"}
legacy := systemconfig.PublishDepositRecommend{
BaseAmount: 50,
SkinGroupRules: []systemconfig.PublishDepositSkinGroupRule{
{GroupKey: "melee", Label: "刀皮", AmountPerItem: 5},
{GroupKey: "operatorRed", Label: "红皮", AmountPerItem: 30},
},
}
if got := recommendedDepositCent(legacy, skinGroups, selected); got != 9000 {
t.Fatalf("recommendedDepositCent() legacy = %d, want 9000", got)
}
canonical := systemconfig.PublishDepositRecommend{
BaseAmount: 50,
BaseAmountCent: 6000,
SkinGroupRules: []systemconfig.PublishDepositSkinGroupRule{
{GroupKey: "melee", Label: "刀皮", AmountPerItem: 5, AmountPerItemCent: 500},
{GroupKey: "operatorRed", Label: "红皮", AmountPerItem: 30, AmountPerItemCent: 3000},
},
}
if got := recommendedDepositCent(canonical, skinGroups, selected); got != 10000 {
t.Fatalf("recommendedDepositCent() cent = %d, want 10000", got)
}
}
func TestCalculateSellerReferenceRatioBPSPrefersCanonicalFields(t *testing.T) {
options := systemconfig.DefaultPublishOptions()
options.RatioConfig.InsuranceBaseRatios = []systemconfig.PublishInsuranceBaseRatio{
{Insurance: "3*3", Ratio: 1, RatioBPS: 400_000},
}
options.RatioConfig.ConfigItems = []systemconfig.PublishRatioConfigItem{
{Key: "missing", Label: "缺失项", Kind: "skin_group", GroupKey: "missing", MissingPenalty: 99, MissingPenaltyBPS: 10_000},
}
options.RatioConfig.CoinCorrections = []systemconfig.PublishCoinCorrection{
{ThresholdM: 0, Correction: 99, CorrectionBPS: 20_000},
}
summary := map[string]any{
"season_insurance": "3*3",
"stamina_level": "7级",
"load_level": "7级",
"daily_loss_m": 10,
}
if got := calculateSellerReferenceRatioBPS(100_000_000, summary, options); got != 430_000 {
t.Fatalf("calculateSellerReferenceRatioBPS() = %d, want 430000", got)
}
}
func TestApplyMainSiteReceivePricingWritesConsistentCanonicalAndLegacyFields(t *testing.T) {
req := fullReceivePricingRequest(10)
applyMainSiteReceivePricing(&req, systemconfig.DefaultPublishOptions(), defaultSalePriceConfig(), 0, 0, false)
breakdown := ensurePriceBreakdown(req.AssetSummary)
for centKey, yuanKey := range map[string]string{
"seller_coin_base_price_cent": "seller_coin_base_price",
"seller_total_price_cent": "seller_total_price",
"consumable_price_cent": "consumable_price",
"buyer_coin_base_price_cent": "buyer_coin_base_price",
"buyer_total_price_cent": "buyer_total_price",
"platform_markup_amount_cent": "platform_markup_amount",
} {
cent := readSummaryInt64(breakdown[centKey])
if got := readSummaryNumber(breakdown[yuanKey]); got != centToYuanFloat(cent) {
t.Fatalf("%s = %v, want %v derived from %s=%d", yuanKey, got, centToYuanFloat(cent), centKey, cent)
}
}
for bpsKey, ratioKey := range map[string]string{
"seller_reference_ratio_bps": "seller_reference_ratio",
"seller_ratio_bps": "seller_ratio",
"buyer_ratio_bps": "buyer_ratio",
} {
bps := readSummaryInt64(breakdown[bpsKey])
if got := readSummaryNumber(breakdown[ratioKey]); got != bpsToRatio(bps) {
t.Fatalf("%s = %v, want %v derived from %s=%d", ratioKey, got, bpsToRatio(bps), bpsKey, bps)
}
}
}
func TestSyncCanonicalPriceBreakdownFromLegacyRefreshesStaleFields(t *testing.T) {
breakdown := map[string]any{
"seller_ratio": 45.5,
"seller_ratio_bps": 430000,
"seller_total_price": 218.25,
"seller_total_price_cent": 23255,
"buyer_ratio": 40.5,
"buyer_ratio_bps": 380000,
"platform_markup_amount": 12.3,
"platform_markup_amount_cent": 9999,
}
syncCanonicalPriceBreakdownFromLegacy(breakdown)
if got := readSummaryInt64(breakdown["seller_ratio_bps"]); got != 455000 {
t.Fatalf("seller_ratio_bps = %d, want 455000", got)
}
if got := readSummaryInt64(breakdown["seller_total_price_cent"]); got != 21825 {
t.Fatalf("seller_total_price_cent = %d, want 21825", got)
}
if got := readSummaryInt64(breakdown["buyer_ratio_bps"]); got != 405000 {
t.Fatalf("buyer_ratio_bps = %d, want 405000", got)
}
if got := readSummaryInt64(breakdown["platform_markup_amount_cent"]); got != 1230 {
t.Fatalf("platform_markup_amount_cent = %d, want 1230", got)
}
}
@@ -28,14 +28,37 @@ type ReceivePricingView struct {
func ReceivePricingFromAssetSummary(raw datatypes.JSON) ReceivePricingView {
summary := decodeAssetSummary(raw)
breakdown := ensurePriceBreakdown(summary)
sellerTotal := readSummaryNumber(breakdown["seller_total_price"])
return ReceivePricingView{
SellerRatio: readSummaryNumber(breakdown["seller_ratio"]),
SellerTotalPriceCent: yuanToCent(sellerTotal),
BuyerRatio: readSummaryNumber(breakdown["buyer_ratio"]),
SellerRatio: summaryRatio(breakdown, "seller_ratio_bps", "seller_ratio"),
SellerTotalPriceCent: summaryMoneyCent(breakdown, "seller_total_price_cent", "seller_total_price"),
BuyerRatio: summaryRatio(breakdown, "buyer_ratio_bps", "buyer_ratio"),
}
}
func summaryMoneyCent(breakdown map[string]any, centKey, yuanKey string) int64 {
if breakdown == nil {
return 0
}
if cent := readSummaryInt64(breakdown[centKey]); cent > 0 {
return cent
}
return centFromYuanFloat(readSummaryNumber(breakdown[yuanKey]))
}
func summaryBPS(breakdown map[string]any, bpsKey, ratioKey string) int64 {
if breakdown == nil {
return 0
}
if bps := readSummaryInt64(breakdown[bpsKey]); bps > 0 {
return bps
}
return ratioToBPS(readSummaryNumber(breakdown[ratioKey]))
}
func summaryRatio(breakdown map[string]any, bpsKey, ratioKey string) float64 {
return bpsToRatio(summaryBPS(breakdown, bpsKey, ratioKey))
}
func (s *Service) applyMainSiteReceivePricing(ctx context.Context, req *CreateRequest, receiveRatioDelta, receiveCostDelta float64, onDistributionSite bool) error {
if req == nil {
return nil
@@ -60,58 +83,76 @@ func applyMainSiteReceivePricing(req *CreateRequest, options systemconfig.Publis
if req.AssetSummary == nil {
req.AssetSummary = map[string]any{}
}
coinM := coinMFromHafCoinAmount(req.HafCoinAmount)
referenceRatio := calculateSellerReferenceRatio(coinM, req.AssetSummary, options)
if referenceRatio <= 0 {
referenceRatioBPS := calculateSellerReferenceRatioBPS(req.HafCoinAmount, req.AssetSummary, options)
if referenceRatioBPS <= 0 {
return
}
breakdown := ensurePriceBreakdown(req.AssetSummary)
requestedRatio := readSummaryNumber(breakdown["seller_ratio"])
if requestedRatio <= 0 {
requestedRatio = readSummaryNumber(breakdown["accelerated_sale_ratio"])
requestedRatioBPS := summaryBPS(breakdown, "seller_ratio_bps", "seller_ratio")
if requestedRatioBPS <= 0 {
requestedRatioBPS = summaryBPS(breakdown, "accelerated_sale_ratio_bps", "accelerated_sale_ratio")
}
costDelta := NormalizeSiteReceiveCostDelta(receiveCostDelta)
ratioDelta := NormalizeSiteReceiveRatioDelta(receiveRatioDelta)
sellerRatio := clampSaleRatio(referenceRatio, requestedRatio)
sellerRatioBPS := clampSaleRatioBPS(referenceRatioBPS, requestedRatioBPS)
// 分站新口径:收号比例 = R + 压价点数(收号价更低),买家定价锚定主站参考比例 R,不随收号价联动。
if onDistributionSite {
sellerRatio = ApplySiteReceiveCostDelta(referenceRatio, costDelta)
sellerRatioBPS = applySiteReceiveCostDeltaBPS(referenceRatioBPS, costDelta)
}
coinWan := coinM * 100
sellerCoinBasePrice := roundMoney(coinWan / sellerRatio)
consumablePrice := consumableValue(req.AssetSummary)
sellerTotalPrice := roundMoney(sellerCoinBasePrice + consumablePrice)
dailyLoss := readSummaryNumber(req.AssetSummary["daily_loss_m"])
sellerCoinBasePriceCent := sellerCoinBasePriceCentFromHafCoin(req.HafCoinAmount, sellerRatioBPS)
consumablePriceCent := consumableValueCent(req.AssetSummary, options.QuantityItems)
sellerTotalPriceCent := sellerCoinBasePriceCent + consumablePriceCent
// 主站买家价仍跟 sellerRatio(含加速出售);分站买家价锚定 R,压价差进入三方分成池。
buyerAnchorRatio := sellerRatio
buyerAnchorCoinBase := sellerCoinBasePrice
buyerAnchorRatioBPS := sellerRatioBPS
buyerAnchorCoinBasePriceCent := sellerCoinBasePriceCent
if onDistributionSite {
buyerAnchorRatio = referenceRatio
buyerAnchorCoinBase = referenceSellerCoinBasePrice(coinWan, referenceRatio)
buyerAnchorRatioBPS = referenceRatioBPS
buyerAnchorCoinBasePriceCent = sellerCoinBasePriceCentFromHafCoin(req.HafCoinAmount, referenceRatioBPS)
}
pricing := calculateExternalPlatformPricing(coinM, buyerAnchorRatio, buyerAnchorCoinBase, consumablePrice, config)
breakdown["seller_reference_ratio"] = referenceRatio
breakdown["seller_ratio"] = sellerRatio
breakdown["seller_coin_base_price"] = sellerCoinBasePrice
breakdown["seller_total_price"] = sellerTotalPrice
breakdown["consumable_price"] = consumablePrice
breakdown["daily_loss_ratio_adjustment"] = calculateDailyLossRatioAdjustment(dailyLoss)
breakdown["accelerated_sale_ratio"] = sellerRatio
pricing := calculateExternalPlatformPricingCent(
req.HafCoinAmount,
buyerAnchorRatioBPS,
buyerAnchorCoinBasePriceCent,
consumablePriceCent,
config,
)
dailyLossRatioAdjustmentBPS := calculateDailyLossRatioAdjustmentBPS(req.AssetSummary)
// 规范字段:整数分 + 基点。
breakdown["seller_reference_ratio_bps"] = referenceRatioBPS
breakdown["seller_ratio_bps"] = sellerRatioBPS
breakdown["buyer_ratio_bps"] = pricing.BuyerRatioBPS
breakdown["seller_coin_base_price_cent"] = sellerCoinBasePriceCent
breakdown["seller_total_price_cent"] = sellerTotalPriceCent
breakdown["consumable_price_cent"] = consumablePriceCent
breakdown["buyer_coin_base_price_cent"] = pricing.BuyerCoinBasePriceCent
breakdown["buyer_total_price_cent"] = pricing.BuyerTotalPriceCent
breakdown["platform_markup_amount_cent"] = pricing.PlatformMarkupCent
// 旧 float 字段:由规范字段一次性派生,供历史前端与历史商品读取。
breakdown["seller_reference_ratio"] = bpsToRatio(referenceRatioBPS)
breakdown["seller_ratio"] = bpsToRatio(sellerRatioBPS)
breakdown["seller_coin_base_price"] = centToYuanFloat(sellerCoinBasePriceCent)
breakdown["seller_total_price"] = centToYuanFloat(sellerTotalPriceCent)
breakdown["consumable_price"] = centToYuanFloat(consumablePriceCent)
breakdown["daily_loss_ratio_adjustment"] = bpsToRatio(dailyLossRatioAdjustmentBPS)
breakdown["accelerated_sale_ratio"] = bpsToRatio(sellerRatioBPS)
breakdown["accelerated_sale_ratio_bps"] = sellerRatioBPS
if onDistributionSite {
breakdown["site_receive_cost_delta"] = costDelta
breakdown["pricing_model"] = "site_cost_decoupled_v1"
} else {
breakdown["site_receive_ratio_delta"] = ratioDelta
}
breakdown["buyer_coin_base_price"] = pricing.BuyerCoinBasePrice
breakdown["buyer_total_price"] = pricing.BuyerTotalPrice
breakdown["buyer_ratio"] = pricing.BuyerRatio
breakdown["platform_markup_amount"] = pricing.PlatformMarkupPrice
breakdown["buyer_coin_base_price"] = centToYuanFloat(pricing.BuyerCoinBasePriceCent)
breakdown["buyer_total_price"] = centToYuanFloat(pricing.BuyerTotalPriceCent)
breakdown["buyer_ratio"] = bpsToRatio(pricing.BuyerRatioBPS)
breakdown["platform_markup_amount"] = centToYuanFloat(pricing.PlatformMarkupCent)
breakdown["platform_rule_type"] = pricing.RuleType
req.AssetSummary["price_breakdown"] = breakdown
req.AssetSummary["publish_ratio"] = pricing.BuyerRatio
req.PriceCent = yuanToCent(pricing.BuyerTotalPrice)
req.AssetSummary["publish_ratio"] = bpsToRatio(pricing.BuyerRatioBPS)
req.PriceCent = pricing.BuyerTotalPriceCent
}
// NormalizeSiteReceiveCostDelta 将分站收号压价点数限制在 [0, 20]。
@@ -32,9 +32,9 @@ func TestCalculateSellerReferenceRatioUsesMainSiteConfig(t *testing.T) {
"load_level": "7级",
"daily_loss_m": 10,
}
got := calculateSellerReferenceRatio(100, summary, options)
if got != 43 {
t.Fatalf("reference ratio = %v, want 43 (base 40 + missing skin penalties 3)", got)
got := calculateSellerReferenceRatioBPS(100_000_000, summary, options)
if got != 430_000 {
t.Fatalf("reference ratio = %d bps, want 430000 (base 40 + missing skin penalties 3)", got)
}
}
@@ -48,14 +48,33 @@ func TestApplyMainSiteReceivePricingOverwritesClientRatio(t *testing.T) {
if readSummaryNumber(breakdown["seller_reference_ratio"]) != 43 {
t.Fatalf("seller_reference_ratio = %v, want 43", breakdown["seller_reference_ratio"])
}
if readSummaryNumber(breakdown["seller_coin_base_price"]) != 232.6 {
t.Fatalf("seller_coin_base_price = %v, want 232.6", breakdown["seller_coin_base_price"])
if readSummaryNumber(breakdown["seller_coin_base_price"]) != 232.55 {
t.Fatalf("seller_coin_base_price = %v, want 232.55", breakdown["seller_coin_base_price"])
}
if readSummaryNumber(breakdown["buyer_ratio"]) != 38 {
t.Fatalf("buyer_ratio = %v, want 38", breakdown["buyer_ratio"])
if got := readSummaryInt64(breakdown["seller_reference_ratio_bps"]); got != 430000 {
t.Fatalf("seller_reference_ratio_bps = %d, want 430000", got)
}
if req.PriceCent != 26320 {
t.Fatalf("PriceCent = %d, want 26320", req.PriceCent)
if got := readSummaryInt64(breakdown["seller_coin_base_price_cent"]); got != 23255 {
t.Fatalf("seller_coin_base_price_cent = %d, want 23255", got)
}
if got := readSummaryInt64(breakdown["buyer_coin_base_price_cent"]); got != 26315 {
t.Fatalf("buyer_coin_base_price_cent = %d, want 26315", got)
}
if got := readSummaryInt64(breakdown["seller_total_price_cent"]); got != 23255 {
t.Fatalf("seller_total_price_cent = %d, want 23255", got)
}
if got := readSummaryInt64(breakdown["platform_markup_amount_cent"]); got != 3060 {
t.Fatalf("platform_markup_amount_cent = %d, want 3060", got)
}
buyerRatioBPS := readSummaryInt64(breakdown["buyer_ratio_bps"])
if buyerRatioBPS != 380011 {
t.Fatalf("buyer_ratio_bps = %d, want 380011 derived from integer price", buyerRatioBPS)
}
if readSummaryNumber(breakdown["buyer_ratio"]) != float64(buyerRatioBPS)/10000 {
t.Fatalf("buyer_ratio = %v, want derived from %d bps", breakdown["buyer_ratio"], buyerRatioBPS)
}
if req.PriceCent != 26315 {
t.Fatalf("PriceCent = %d, want 26315", req.PriceCent)
}
}
@@ -69,15 +88,19 @@ func TestApplyMainSiteReceivePricingKeepsAcceleratedRatioWithinMainSiteRange(t *
if readSummaryNumber(breakdown["seller_reference_ratio"]) != 43 {
t.Fatalf("seller_reference_ratio = %v, want 43", breakdown["seller_reference_ratio"])
}
if got := readSummaryNumber(breakdown["seller_coin_base_price"]); got != 200 {
t.Fatalf("accelerated seller_coin_base_price = %v, want 200", got)
if got := readSummaryInt64(breakdown["seller_coin_base_price_cent"]); got != 20000 {
t.Fatalf("accelerated seller_coin_base_price_cent = %d, want 20000", got)
}
// 主站加速出售买卖仍联动:买家比例 = 50 − 5 = 45,不能被误锚到参考比例 38。
if readSummaryNumber(breakdown["buyer_ratio"]) != 45 {
t.Fatalf("accelerated buyer_ratio = %v, want 45 (follows seller ratio, not reference 38)", breakdown["buyer_ratio"])
if got := readSummaryInt64(breakdown["buyer_coin_base_price_cent"]); got != 22222 {
t.Fatalf("accelerated buyer_coin_base_price_cent = %d, want 22222", got)
}
if req.PriceCent != 22220 {
t.Fatalf("accelerated PriceCent = %d, want 22220", req.PriceCent)
// 主站加速出售买卖仍联动:买家比例按 50 − 5 定价,不能被误锚到参考比例 38。
buyerRatioBPS := readSummaryInt64(breakdown["buyer_ratio_bps"])
if buyerRatioBPS != 450004 {
t.Fatalf("accelerated buyer_ratio_bps = %d, want 450004 derived from integer price", buyerRatioBPS)
}
if req.PriceCent != 22222 {
t.Fatalf("accelerated PriceCent = %d, want 22222", req.PriceCent)
}
}
@@ -88,8 +111,8 @@ func TestDistributionReceiveCostDeltaZeroKeepsMainSitePrice(t *testing.T) {
if readSummaryNumber(breakdown["seller_ratio"]) != 43 {
t.Fatalf("distribution seller_ratio = %v, want main-site reference 43 when cost delta is 0", breakdown["seller_ratio"])
}
if readSummaryNumber(breakdown["buyer_total_price"]) != 263.2 {
t.Fatalf("distribution buyer_total_price = %v, want 263.2 anchored to main site", breakdown["buyer_total_price"])
if readSummaryNumber(breakdown["buyer_total_price"]) != 263.15 {
t.Fatalf("distribution buyer_total_price = %v, want 263.15 anchored to main site", breakdown["buyer_total_price"])
}
}
@@ -97,19 +120,19 @@ func TestDistributionReceiveCostDeltaLowersReceivePriceKeepsSellPrice(t *testing
req := fullReceivePricingRequest(50)
applyMainSiteReceivePricing(&req, systemconfig.DefaultPublishOptions(), defaultSalePriceConfig(), 0, 3, true)
breakdown := ensurePriceBreakdown(req.AssetSummary)
// 压价 3 点:收号比例 = R+3 = 46,号主收 217.4(低于主站 232.6)。
// 压价 3 点:收号比例 = R+3 = 46,号主收 217.39(低于主站 232.55)。
if readSummaryNumber(breakdown["seller_ratio"]) != 46 {
t.Fatalf("distribution seller_ratio = %v, want 46 (R+3) under cost-decoupled model", breakdown["seller_ratio"])
}
if got := readSummaryNumber(breakdown["seller_coin_base_price"]); got != 217.4 {
t.Fatalf("seller_coin_base_price = %v, want 217.4 (lower than main site 232.6)", got)
if got := readSummaryNumber(breakdown["seller_coin_base_price"]); got != 217.39 {
t.Fatalf("seller_coin_base_price = %v, want 217.39 (lower than main site 232.55)", got)
}
// 卖价锚定主站:买家比例仍为 38、买家价仍为 263.2,不随收号价下降。
if readSummaryNumber(breakdown["buyer_ratio"]) != 38 {
t.Fatalf("buyer_ratio = %v, want 38 (decoupled from receive ratio)", breakdown["buyer_ratio"])
// 卖价锚定主站:买家比例仍按 38 定价、买家价仍为 263.15,不随收号价下降。
if readSummaryNumber(breakdown["buyer_ratio"]) < 38 || readSummaryNumber(breakdown["buyer_ratio"]) > 38.01 {
t.Fatalf("buyer_ratio = %v, want about 38 (decoupled from receive ratio)", breakdown["buyer_ratio"])
}
if readSummaryNumber(breakdown["buyer_total_price"]) != 263.2 {
t.Fatalf("buyer_total_price = %v, want 263.2 unchanged by cost delta", breakdown["buyer_total_price"])
if readSummaryNumber(breakdown["buyer_total_price"]) != 263.15 {
t.Fatalf("buyer_total_price = %v, want 263.15 unchanged by cost delta", breakdown["buyer_total_price"])
}
// 注意:发布快照的 platform_markup_amount 仍为主站口径提价(30.6,锚定价→买价);
// 压价差不在此体现,而是结算层 profit = 净付款 − 号主快照价(217.4)自动进入 35/40/25 分成池。
@@ -323,6 +323,7 @@ func (r *Repository) AdjustReviewPrice(ctx context.Context, adminID uint64, list
breakdown["admin_adjust_reason"] = strings.TrimSpace(req.Reason)
breakdown["admin_adjusted_at"] = time.Now().Format(time.RFC3339)
breakdown["admin_adjusted_by"] = adminID
syncCanonicalPriceBreakdownFromLegacy(breakdown)
summary["price_breakdown"] = breakdown
assetSummary, err := marshalAssetSummary(summary)
@@ -1106,11 +1106,11 @@ func TestCreateDistributionListingUsesMainSiteReceiveRatio(t *testing.T) {
if pricing.SellerRatio != 43 {
t.Fatalf("distribution receive seller_ratio = %v, want 43 from main-site ratio_config", pricing.SellerRatio)
}
if pricing.SellerTotalPriceCent != 23260 {
t.Fatalf("seller_total_price_cent = %d, want 23260", pricing.SellerTotalPriceCent)
if pricing.SellerTotalPriceCent != 23255 {
t.Fatalf("seller_total_price_cent = %d, want 23255", pricing.SellerTotalPriceCent)
}
if dto.PriceCent != 26320 {
t.Fatalf("listing PriceCent = %d, want 26320 from main-site sale price", dto.PriceCent)
if dto.PriceCent != 26315 {
t.Fatalf("listing PriceCent = %d, want 26315 from main-site sale price", dto.PriceCent)
}
if dto.DistributionSiteCode != "dist_ratio_1" {
t.Fatalf("DistributionSiteCode = %s, want dist_ratio_1", dto.DistributionSiteCode)
@@ -1173,8 +1173,8 @@ func TestCreateDistributionListingAppliesSiteReceiveCostDelta(t *testing.T) {
if pricing.SellerRatio != 46 {
t.Fatalf("seller_ratio = %v, want 46 after site cost delta 3 (R=43+3) under decoupled model", pricing.SellerRatio)
}
if dto.PriceCent != 26320 {
t.Fatalf("listing PriceCent = %d, want 26320 (buyer price stays anchored to main-site R)", dto.PriceCent)
if dto.PriceCent != 26315 {
t.Fatalf("listing PriceCent = %d, want 26315 (buyer price stays anchored to main-site R)", dto.PriceCent)
}
}
@@ -26,7 +26,7 @@ func validateRequest(req CreateRequest, rules publishRules) error {
if req.DepositAmountCent < 0 {
return ErrInvalidDeposit
}
if consumables := consumableValue(req.AssetSummary); consumables > 0 && req.DepositAmountCent <= yuanToCent(consumables) {
if consumables := consumableValueCent(req.AssetSummary, nil); consumables > 0 && req.DepositAmountCent <= consumables {
return ErrDepositTooLow
}
if req.HafCoinAmount < 0 {