Files
hfb_sys/backend/internal/e2e/distribution_flow_test.go
T
yml2213andCursor 768902f2f7 feat(distribution): 补齐退款加价回退、本站自卖与开通默认规则
退款时压缩卖方加价,人工/自动批次仅完结单可入账,号主收入进主站钱包;仲裁后重算释放并支持作废未入账 DS。开通时四种模式按默认生效,对外口径合并为 35/40/25,不再单独展示通道费。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-10 18:13:46 +08:00

314 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package e2e
import (
"context"
"encoding/json"
"testing"
"time"
"gorm.io/datatypes"
"gorm.io/gorm"
"hfb_sys/backend/internal/database"
"hfb_sys/backend/internal/model"
"hfb_sys/backend/internal/modules/distribution"
listingpkg "hfb_sys/backend/internal/modules/listing"
orderpkg "hfb_sys/backend/internal/modules/order"
)
func openDistributionE2EDB(t *testing.T) *gorm.DB {
t.Helper()
return database.NewTestDB()
}
// TestE2EScenario1_MainSiteReceive_SubSiteMarkupSell_Settlement
// 场景 1 完整端到端测试:
// 1. 主站收号(号主底价 80 元,主站加价定价 100 元,100M 币,比例 100);
// 2. 分站 A 同步商品,设置自主提价(调减 20 比例,售价自动升为 125 元,分站加价 25 元);
// 3. 租客在分站前台浏览并下单,实付售价 125 元;
// 4. 支付成功自动触发快照:
// - 号主成本 80 元保底;
// - 分站自主加价 25 元 100% 归卖出分站;
// - 主站提价利润 20 元(通道费 0.63 元由主站承担)按 35% / 40% / 25% 严丝合缝切分;
// - 卖出分站总获得 33.00 元;
//
// 5. 完单后创建结算批次并执行结算,分站金库钱包精确入账 33.00 元,财务状态置为 settled。
func TestE2EScenario1_MainSiteReceive_SubSiteMarkupSell_Settlement(t *testing.T) {
db := openDistributionE2EDB(t)
ctx := context.Background()
now := time.Now()
// ----------------------------------------------------
// 1. 基础主体初始化
// ----------------------------------------------------
adminUser := model.AdminUser{Username: "super_admin", Status: "active"}
if err := db.Create(&adminUser).Error; err != nil {
t.Fatal(err)
}
subSiteAdmin := model.AdminUser{Username: "site_a_manager", Status: "active"}
if err := db.Create(&subSiteAdmin).Error; err != nil {
t.Fatal(err)
}
siteA := model.DistributionSite{
SiteCode: "branch_alpha",
SiteName: "分销甲站",
Domain: "alpha.example.com",
Status: "active",
DisplayAccountEnabled: true,
}
if err := db.Create(&siteA).Error; err != nil {
t.Fatal(err)
}
siteMember := model.DistributionSiteMember{
SiteID: siteA.ID,
AdminUserID: subSiteAdmin.ID,
MemberType: "operator",
Status: "active",
DataScope: "site",
}
if err := db.Create(&siteMember).Error; err != nil {
t.Fatal(err)
}
// 规则版本:主站收号、分站卖出(两方-平台收号模式)
// 收号 35%(归平台),卖出 40%(归分站),服务费 25%(归平台),通道费 50 BPS (0.5%)
ruleVersion := model.DistributionRuleVersion{
SiteID: &siteA.ID,
Mode: distribution.ModeTwoPartyPlatformReceive,
Version: 1,
Status: "active",
PlatformReceiveRatio: 35,
DistributorSellRatio: 40,
ServiceFeeValue: 25,
ServiceFeeUnit: distribution.ServiceFeeUnitPercent,
PaymentFeeBPS: 50,
EffectiveFrom: now.Add(-2 * time.Hour),
}
if err := db.Create(&ruleVersion).Error; err != nil {
t.Fatal(err)
}
owner := model.User{Phone: "13800000001", Status: "active"}
renter := model.User{Phone: "13900000002", Status: "active"}
if err := db.Create(&owner).Error; err != nil {
t.Fatal(err)
}
if err := db.Create(&renter).Error; err != nil {
t.Fatal(err)
}
// ----------------------------------------------------
// 2. 主站收号:100M 币,号主底价 80 元,主站定价 100 元
// ----------------------------------------------------
assetSummaryJSON, _ := json.Marshal(map[string]any{
"publish_ratio": 100.0,
"price_breakdown": map[string]any{
"seller_ratio": 125.0,
"seller_total_price": 80.0, // 号主底价 80 元
"buyer_ratio": 100.0,
"buyer_total_price": 100.0, // 主站标价 100 元
"platform_markup_amount": 20.0, // 主站提价 20 元
},
})
account := model.GameAccount{
Title: "王者账号-100M",
HafCoinAmount: 100_000_000, // 10000 万币
AssetSummary: datatypes.JSON(assetSummaryJSON),
Status: "active",
}
if err := db.Create(&account).Error; err != nil {
t.Fatal(err)
}
listing := model.RentalListing{
ListingNo: "L-E2E-101",
AccountID: account.ID,
OwnerID: owner.ID,
PriceCent: 10000, // 主站挂牌基准价 100 元 (10000 分)
DepositAmountCent: 0,
Status: "published",
ReviewStatus: "approved",
DistributionSiteID: nil, // 主站自营收号
}
if err := db.Create(&listing).Error; err != nil {
t.Fatal(err)
}
// ----------------------------------------------------
// 3. 分站货架选品并自主提价(调减 20 比例,售价变为 125 元)
// ----------------------------------------------------
shelfRelation := model.DistributionSiteAccount{
SiteID: siteA.ID,
AccountID: account.ID,
ListingID: listing.ID,
DisplayStatus: "enabled",
ShelfRatioDelta: 20, // 提价减 20 比例点 (100 -> 80)
}
if err := db.Create(&shelfRelation).Error; err != nil {
t.Fatal(err)
}
// 验证货架公共查询计算价格
sitePriceCent, baseRatio, siteRatio := listingpkg.ApplyShelfRatio(account.HafCoinAmount, listing.PriceCent, shelfRelation.ShelfRatioDelta)
if baseRatio != 100 || siteRatio != 80 {
t.Fatalf("shelf ratio calculation error: base=%v, site=%v", baseRatio, siteRatio)
}
if sitePriceCent != 12500 {
t.Fatalf("shelf price calculation error: got %d, want 12500", sitePriceCent)
}
shelfMarkupCent := sitePriceCent - listing.PriceCent
if shelfMarkupCent != 2500 {
t.Fatalf("shelf markup error: got %d, want 2500", shelfMarkupCent)
}
// ----------------------------------------------------
// 4. 租客在分站下单并生成快照
// ----------------------------------------------------
orderRepo := orderpkg.NewRepository(db, orderpkg.Dependencies{})
orderDTO, err := orderRepo.Create(ctx, renter.ID, orderpkg.CreateRequest{
ListingID: listing.ID,
DistributionSiteCode: siteA.SiteCode,
})
if err != nil {
t.Fatalf("order create failed: %v", err)
}
// 校验订单快照
if orderDTO.RentAmountCent == nil || *orderDTO.RentAmountCent != 12500 {
t.Fatalf("order rent amount = %v, want 12500", orderDTO.RentAmountCent)
}
if orderDTO.BasePriceCent != 10000 || orderDTO.ShelfMarkupCent != 2500 || orderDTO.MarkupSiteID != siteA.ID {
t.Fatalf("order snapshot error: base=%d, markup=%d, site=%d", orderDTO.BasePriceCent, orderDTO.ShelfMarkupCent, orderDTO.MarkupSiteID)
}
// ----------------------------------------------------
// 5. 支付成功并触发分销财务快照 SnapshotPaidOrderTx
// ----------------------------------------------------
grossPaidCent := int64(12500)
if err := distribution.SnapshotPaidOrderTx(db, orderDTO.ID, grossPaidCent); err != nil {
t.Fatalf("SnapshotPaidOrderTx failed: %v", err)
}
// 验证财务快照
var financial model.DistributionOrderFinancial
if err := db.Where("order_id = ?", orderDTO.ID).First(&financial).Error; err != nil {
t.Fatalf("find financial snapshot failed: %v", err)
}
if financial.GrossPaidAmountCent != 12500 || financial.MarkupAmountCent != 2500 {
t.Fatalf("financial snapshot error: gross=%d, markup=%d", financial.GrossPaidAmountCent, financial.MarkupAmountCent)
}
// 支付通道费 0.5%:12500 * 50 / 10000 = 63 分,由主站承担,不参与分成基数
if financial.PaymentFeeCent != 63 {
t.Fatalf("payment fee = %d, want 63", financial.PaymentFeeCent)
}
// 主站提价总额 10000 - 8000 = 2000 分,通道费不扣减,利润基数 = 2000 分
if financial.SplitBaseAmountCent != 2000 {
t.Fatalf("split base amount = %d, want 2000", financial.SplitBaseAmountCent)
}
// 平台服务费 25%:2000 * 25% = 500 分
if financial.ServiceFeeCent != 500 {
t.Fatalf("service fee = %d, want 500", financial.ServiceFeeCent)
}
// 验证各参与方收益快照
var parties []model.DistributionOrderParty
if err := db.Where("order_id = ?", orderDTO.ID).Order("id ASC").Find(&parties).Error; err != nil || len(parties) != 2 {
t.Fatalf("parties error: len=%d, err=%v", len(parties), err)
}
// 参与方 0:distributor_sell(分站 A,卖方)
// 收益 = 分站自主加价 2500 分 + 提价分成 40% (2000 * 40% = 800) = 3300 分
var distributorParty, platformParty model.DistributionOrderParty
for _, p := range parties {
if p.PartyRole == "distributor_sell" {
distributorParty = p
} else if p.PartyRole == "platform_receive" {
platformParty = p
}
}
if distributorParty.IncomeAmountCent != 3300 || distributorParty.PartyID != siteA.ID {
t.Fatalf("distributor_sell party error: income=%d (want 3300), partyID=%d", distributorParty.IncomeAmountCent, distributorParty.PartyID)
}
// 参与方 1:platform_receive(平台收号方,35% 分成:2000 * 35% = 700 分)
if platformParty.IncomeAmountCent != 700 {
t.Fatalf("platform_receive party error: income=%d (want 700)", platformParty.IncomeAmountCent)
}
// 尾差校验:2000 - (800 + 700) - 500 = 0
if financial.RoundingAdjustmentCent != 0 {
t.Fatalf("rounding adjustment = %d, want 0", financial.RoundingAdjustmentCent)
}
// ----------------------------------------------------
// 6. 批次结算与分站金库到账 (Settlement Batch & Wallet Credit)
// ----------------------------------------------------
if err := db.Model(&model.RentalOrder{}).Where("id = ?", orderDTO.ID).Update("status", "completed").Error; err != nil {
t.Fatal(err)
}
distRepo := distribution.NewRepository(db)
// 创建结算批次
batch, err := distRepo.CreateSettlementBatch(ctx, subSiteAdmin.ID, distribution.CreateSettlementBatchRequest{
SiteID: siteA.ID,
PeriodStart: now.Add(-24 * time.Hour),
PeriodEnd: now.Add(24 * time.Hour),
})
if err != nil {
t.Fatalf("CreateSettlementBatch failed: %v", err)
}
if batch.TotalAmountCent != 3300 {
t.Fatalf("batch total amount = %d, want 3300", batch.TotalAmountCent)
}
// 确认结算批次入账
settledBatch, err := distRepo.MarkSettlementBatch(ctx, subSiteAdmin.ID, batch.ID)
if err != nil {
t.Fatalf("MarkSettlementBatch failed: %v", err)
}
if settledBatch.Status != "settled" {
t.Fatalf("settled batch status = %s, want settled", settledBatch.Status)
}
// 验证财务快照状态更新为 settled
var updatedFinancial model.DistributionOrderFinancial
if err := db.Where("order_id = ?", orderDTO.ID).First(&updatedFinancial).Error; err != nil {
t.Fatal(err)
}
if updatedFinancial.SettlementStatus != "settled" {
t.Fatalf("financial settlement status = %s, want settled", updatedFinancial.SettlementStatus)
}
// 验证分站金库钱包账户余额
var siteWallet model.WalletAccount
if err := db.Where("user_id = 0 AND distribution_site_id = ?", siteA.ID).First(&siteWallet).Error; err != nil {
t.Fatalf("find site wallet failed: %v", err)
}
if siteWallet.AvailableBalanceCent != 3300 {
t.Fatalf("site wallet balance = %d, want 3300", siteWallet.AvailableBalanceCent)
}
// 验证钱包流水记录
var ledger model.WalletLedger
if err := db.Where("user_id = 0 AND distribution_site_id = ? AND biz_type = 'distribution_settlement'", siteA.ID).First(&ledger).Error; err != nil {
t.Fatalf("find wallet ledger failed: %v", err)
}
if ledger.AmountCent != 3300 || ledger.Direction != "in" {
t.Fatalf("wallet ledger error: amount=%d, direction=%s", ledger.AmountCent, ledger.Direction)
}
t.Logf("=== 场景 1 全链路端到端验证通过!===")
t.Logf("实付总额: %d 分 (125.00元)", grossPaidCent)
t.Logf("号主保底成本: %d 分 (80.00元)", 8000)
t.Logf("分站自主加价(独享): %d 分 (25.00元)", shelfMarkupCent)
t.Logf("支付通道手续费(0.5%%): %d 分 (0.63元)", financial.PaymentFeeCent)
t.Logf("主站提价净利润(主站承担通道费): %d 分 (20.00元)", financial.SplitBaseAmountCent)
t.Logf(" ├─ 收号方(主站 35%%): %d 分 (7.00元)", platformParty.IncomeAmountCent)
t.Logf(" ├─ 卖出方提价分成(分站 40%%): %d 分 (8.00元)", distributorParty.IncomeAmountCent-shelfMarkupCent)
t.Logf(" └─ 平台服务费(25%%): %d 分 (5.00元)", financial.ServiceFeeCent)
t.Logf("分站 A 最终结算到账: %d 分 (33.00元) [加价25元 + 分成8元]", siteWallet.AvailableBalanceCent)
}