From b884d018559ba5e48e69d0bee37905aafc541d42 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Thu, 5 Nov 2026 19:19:12 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=8C=89=E5=AE=9E=E9=99=85=E7=BA=AF?= =?UTF-8?q?=E5=B8=81=E7=BB=93=E7=AE=97=E9=87=91=E9=A2=9D=E8=AE=A1=E7=AE=97?= =?UTF-8?q?=E4=BF=9D=E9=99=A9=E8=B4=B9=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/modules/order/pricing.go | 2 +- .../modules/order/pricing_insurance_test.go | 45 ++++++++++-- .../modules/systemconfig/normalize.go | 21 ++++++ .../modules/systemconfig/publish_options.go | 4 +- .../systemconfig/publish_options_test.go | 27 +++++++ .../components/QualityOwnerConfigDialog.vue | 2 +- .../features/listings/api/listingOptions.ts | 4 +- .../__tests__/useCheckoutSimulator.spec.ts | 72 +++++++++++++++++++ .../composables/useCheckoutSimulator.ts | 2 +- .../seller/composables/usePublishForm.ts | 2 +- .../views/MobileSellerListingCreateView.vue | 6 +- .../seller/views/SellerListingCreateView.vue | 8 +-- .../shared/utils/__tests__/pricing.spec.ts | 6 +- frontend/src/shared/utils/pricing.ts | 8 +-- 14 files changed, 184 insertions(+), 25 deletions(-) create mode 100644 frontend/src/features/orders/composables/__tests__/useCheckoutSimulator.spec.ts diff --git a/backend/internal/modules/order/pricing.go b/backend/internal/modules/order/pricing.go index d9ab64b9..762ba99e 100644 --- a/backend/internal/modules/order/pricing.go +++ b/backend/internal/modules/order/pricing.go @@ -406,7 +406,7 @@ func calculateCheckoutSettlement(order model.RentalOrder, consumableAmountCent i // 可完结时租客实际承担的租金向金额;有 shortfall 时仍展示完整应付便于协商 actualRentAmountCent := actualBuyerRentCent ownerRentIncomeCent := actualOwnerRentCent - insuranceFeeCent := calculateInsuranceFeeCent(ownerRentIncomeCent, order.InsuranceFeeBps) + insuranceFeeCent := calculateInsuranceFeeCent(usedOwnerCoinPriceCent, order.InsuranceFeeBps) ownerRentIncomeCent = maxCent(ownerRentIncomeCent-insuranceFeeCent, 0) platformFeeCent := maxCent(actualRentAmountCent-ownerRentIncomeCent, 0) depositCompensationCent := damageAppliedCent diff --git a/backend/internal/modules/order/pricing_insurance_test.go b/backend/internal/modules/order/pricing_insurance_test.go index 80aad44a..82977569 100644 --- a/backend/internal/modules/order/pricing_insurance_test.go +++ b/backend/internal/modules/order/pricing_insurance_test.go @@ -49,13 +49,48 @@ func TestCalculateCheckoutSettlementDeductsPremiumInsuranceFee(t *testing.T) { } settlement := calculateCheckoutSettlement(order, 12000, 100, 0) - if settlement.InsuranceFeeBps != 400 || settlement.InsuranceFeeCent != 1412 { + if settlement.InsuranceFeeBps != 400 || settlement.InsuranceFeeCent != 932 { t.Fatalf("保险费用快照不正确: bps=%d fee=%d", settlement.InsuranceFeeBps, settlement.InsuranceFeeCent) } - if settlement.OwnerRentIncomeCent != 33888 { - t.Fatalf("号主净租金 = %d, want 33888", settlement.OwnerRentIncomeCent) + if settlement.OwnerRentIncomeCent != 34368 { + t.Fatalf("号主净租金 = %d, want 34368", settlement.OwnerRentIncomeCent) } - if settlement.PlatformFeeCent != 4412 { - t.Fatalf("平台收入 = %d, want 4412", settlement.PlatformFeeCent) + if settlement.PlatformFeeCent != 3932 { + t.Fatalf("平台收入 = %d, want 3932", settlement.PlatformFeeCent) + } +} + +func TestCalculateCheckoutSettlementUsesActualPureCoinAsInsuranceBase(t *testing.T) { + order := model.RentalOrder{ + RentAmountCent: 25000, + OwnerRentAmountCent: 25000, + DepositAmountCent: 15000, + InsuranceFeeBps: 800, + AccountSnapshot: datatypes.JSON([]byte(`{ + "haf_coin_amount": 100000000, + "asset_summary": { + "price_breakdown": { + "buyer_ratio": 40, + "seller_ratio": 40, + "buyer_coin_base_price": 250, + "seller_coin_base_price": 250, + "consumable_price": 0 + } + } + }`)), + } + + settlement := calculateCheckoutSettlement(order, 5000, 50, 0) + if settlement.PureCoinAmountCent != 12500 { + t.Fatalf("实际纯币原价 = %d, want 12500", settlement.PureCoinAmountCent) + } + if settlement.InsuranceFeeCent != 1000 { + t.Fatalf("保险费用 = %d, want 1000", settlement.InsuranceFeeCent) + } + if settlement.ExtraItemAmountCent != 5000 { + t.Fatalf("额外物品 = %d, want 5000", settlement.ExtraItemAmountCent) + } + if settlement.OwnerRentIncomeCent != 16500 { + t.Fatalf("号主净租金 = %d, want 16500", settlement.OwnerRentIncomeCent) } } diff --git a/backend/internal/modules/systemconfig/normalize.go b/backend/internal/modules/systemconfig/normalize.go index 7bd91d15..4dad84bd 100644 --- a/backend/internal/modules/systemconfig/normalize.go +++ b/backend/internal/modules/systemconfig/normalize.go @@ -9,6 +9,21 @@ import ( const defaultBannerHotspotHoverScale = 1.06 const maxBannerHotspotHoverScale = 1.5 +func migrateInsuranceBillingCopy(value string) string { + value = strings.Replace( + value, + "注:选择此方案,需额外抽取订单佣金的8.0%作为服务费:押金须达到推荐押金方可购买。", + "无忧保险服务费按实际纯币结算金额计算,额外物品不计入计费基数;具体费率以当前配置为准。押金须达到推荐押金方可购买。", + 1, + ) + return strings.Replace( + value, + "并从卖家实际到手租金中扣除。", + "并按实际纯币结算金额计算,额外物品不计入保险基数。", + 1, + ) +} + func normalizeAnnouncements(items []string) []string { normalized := make([]string, 0, len(items)) for _, item := range items { @@ -236,9 +251,15 @@ func normalizePublishOptions(options *PublishOptionsDTO) { } if options.PriceConfig.PremiumZoneTooltip == "" { options.PriceConfig.PremiumZoneTooltip = defaults.PriceConfig.PremiumZoneTooltip + } else { + options.PriceConfig.PremiumZoneTooltip = + migrateInsuranceBillingCopy(options.PriceConfig.PremiumZoneTooltip) } if options.PriceConfig.CompensationNote == "" { options.PriceConfig.CompensationNote = defaults.PriceConfig.CompensationNote + } else { + options.PriceConfig.CompensationNote = + migrateInsuranceBillingCopy(options.PriceConfig.CompensationNote) } if options.PriceConfig.PricePlaceholder == "" { options.PriceConfig.PricePlaceholder = defaults.PriceConfig.PricePlaceholder diff --git a/backend/internal/modules/systemconfig/publish_options.go b/backend/internal/modules/systemconfig/publish_options.go index efc969ec..316f7ca1 100644 --- a/backend/internal/modules/systemconfig/publish_options.go +++ b/backend/internal/modules/systemconfig/publish_options.go @@ -120,9 +120,9 @@ func DefaultPublishOptions() PublishOptionsDTO { 1. 发布条件:仅已认证且有效的优质号主可以开启;未开通时需先缴纳平台要求的信用保证金并完成优质号主认证。 2. 专区展示:商品审核通过后将在首页“极品专区”展示,获得更聚焦的高端账号曝光。 3. 价格规则:系统会按后台配置在基础比例上计算极品专区专享比例,审核页展示的最终价格通过后不再改价。 -4. 保险服务:可选择无忧租号保险,服务费率按优质号主及极品专区的当前配置执行,并从卖家实际到手租金中扣除。 +4. 保险服务:可选择无忧租号保险,服务费率按优质号主及极品专区的当前配置执行,并按实际纯币结算金额计算,额外物品不计入保险基数。 5. 责任说明:开启专区不会免除商品审核、资料真实性及发布协议中的责任,请如实填写账号资产与出租条件。`, - CompensationNote: "普通赔付方案如下:账号在租期内或祖期外出现封禁,追回、洗号等情况,最高可获赔全款相金+押金\n\n无忧保险赔付方案:按账号市场价值进行赔付(可参照螃蟹,盼之等主流交易平台)。注:选择此方案,需额外抽取订单佣金的8.0%作为服务费:押金须达到推荐押金方可购买。", + CompensationNote: "普通赔付方案如下:账号在租期内或祖期外出现封禁,追回、洗号等情况,最高可获赔全款相金+押金\n\n无忧保险赔付方案:按账号市场价值进行赔付(可参照螃蟹,盼之等主流交易平台)。无忧保险服务费按实际纯币结算金额计算,额外物品不计入计费基数;具体费率以当前配置为准。押金须达到推荐押金方可购买。", PricePlaceholder: "填写币数、保险、体力和负重后自动计算;发布总价=纯币基础价+额外消耗品", RatioDescription: "1:xx 表示 1 元人民币(RMB)等价兑换 xx 万哈夫币", }, diff --git a/backend/internal/modules/systemconfig/publish_options_test.go b/backend/internal/modules/systemconfig/publish_options_test.go index 94ec3692..aa9608c5 100644 --- a/backend/internal/modules/systemconfig/publish_options_test.go +++ b/backend/internal/modules/systemconfig/publish_options_test.go @@ -178,6 +178,33 @@ func TestNormalizePublishOptionsValueDefaultsPremiumZoneTooltip(t *testing.T) { } } +func TestNormalizePublishOptionsValueMigratesInsuranceBillingBaseCopy(t *testing.T) { + options := DefaultPublishOptions() + options.PriceConfig.CompensationNote = "注:选择此方案,需额外抽取订单佣金的8.0%作为服务费:押金须达到推荐押金方可购买。" + options.PriceConfig.PremiumZoneTooltip = "4. 保险服务:服务费率按当前配置执行,并从卖家实际到手租金中扣除。" + raw, err := json.Marshal(options) + if err != nil { + t.Fatalf("构造配置失败: %v", err) + } + normalized, err := normalizePublishOptionsValue(string(raw)) + if err != nil { + t.Fatalf("规范化失败: %v", err) + } + var got PublishOptionsDTO + if err := json.Unmarshal([]byte(normalized), &got); err != nil { + t.Fatalf("解析规范化结果失败: %v", err) + } + if !strings.Contains(got.PriceConfig.CompensationNote, "按实际纯币结算金额计算") { + t.Fatalf("赔付说明未迁移纯币计费口径: %q", got.PriceConfig.CompensationNote) + } + if strings.Contains(got.PriceConfig.CompensationNote, "订单佣金") { + t.Fatalf("赔付说明仍含旧计费口径: %q", got.PriceConfig.CompensationNote) + } + if !strings.Contains(got.PriceConfig.PremiumZoneTooltip, "额外物品不计入保险基数") { + t.Fatalf("专区说明未迁移纯币计费口径: %q", got.PriceConfig.PremiumZoneTooltip) + } +} + func TestNormalizePublishOptionsValuePreservesExplicitZeroPremiumReviewRatioMarkup(t *testing.T) { options := DefaultPublishOptions() options.RatioConfig.PremiumReviewRatioMarkup = 0 diff --git a/frontend/src/features/admin/components/QualityOwnerConfigDialog.vue b/frontend/src/features/admin/components/QualityOwnerConfigDialog.vue index 5365b6d0..34e67397 100644 --- a/frontend/src/features/admin/components/QualityOwnerConfigDialog.vue +++ b/frontend/src/features/admin/components/QualityOwnerConfigDialog.vue @@ -156,7 +156,7 @@ async function save() {