Files
hfb_sys/backend/internal/e2e/distribution_flow_test.go
T

331 lines
13 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"
"fmt"
"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()
db := database.NewTestDBWithName(fmt.Sprintf("dist_e2e_%d", time.Now().UnixNano()))
sqlDB, err := db.DB()
if err != nil {
t.Fatal(err)
}
sqlDB.SetMaxOpenConns(1)
if err := database.MigrateRentalTransactionTestSchema(db); err != nil {
t.Fatalf("migrate rental transaction schema failed: %v", err)
}
if err := db.AutoMigrate(
&model.Role{}, &model.Permission{}, &model.AdminUserRole{}, &model.RolePermission{},
&model.PaymentMerchantConfig{},
&model.DistributionSiteMember{}, &model.DistributionRuleVersion{}, &model.DistributionOrderFinancial{},
&model.DistributionOrderParty{}, &model.DistributionSitePaymentChannel{},
&model.DistributionSettlementBatch{}, &model.DistributionSettlementItem{},
&model.WithdrawalRequest{},
); err != nil {
t.Fatalf("auto migrate distribution schema failed: %v", err)
}
return db
}
// TestE2EScenario1_MainSiteReceive_SubSiteMarkupSell_Settlement
// 场景 1 完整端到端测试:
// 1. 主站收号(号主底价 80 元,主站加价定价 100 元,100M 币,比例 100);
// 2. 分站 A 同步商品,设置自主提价(调减 20 比例,售价自动升为 125 元,分站加价 25 元);
// 3. 租客在分站前台浏览并下单,实付售价 125 元;
// 4. 支付成功自动触发快照:
// - 号主成本 80 元保底;
// - 分站自主加价 25 元 100% 归卖出分站;
// - 主站提价净利润(20 元 - 通道费 0.63 元 = 19.37 元)按 35% / 40% / 25% 严丝合缝切分;
// - 卖出分站总获得 32.75 元;
// 5. 完单后创建结算批次并执行结算,分站金库钱包精确入账 32.75 元,财务状态置为 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 分,扣除通道费 63 分后利润基数 = 1937 分
if financial.SplitBaseAmountCent != 1937 {
t.Fatalf("split base amount = %d, want 1937", financial.SplitBaseAmountCent)
}
// 平台服务费 25%:1937 * 25% = 484 分
if financial.ServiceFeeCent != 484 {
t.Fatalf("service fee = %d, want 484", 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% (1937 * 40% = 775) = 3275 分
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 != 3275 || distributorParty.PartyID != siteA.ID {
t.Fatalf("distributor_sell party error: income=%d (want 3275), partyID=%d", distributorParty.IncomeAmountCent, distributorParty.PartyID)
}
// 参与方 1:platform_receive(平台收号方,35% 分成:1937 * 35% = 678 分)
if platformParty.IncomeAmountCent != 678 {
t.Fatalf("platform_receive party error: income=%d (want 678)", platformParty.IncomeAmountCent)
}
// 尾差校验:1937 - (775 + 678) - 484 = 0
if financial.RoundingAdjustmentCent != 0 {
t.Fatalf("rounding adjustment = %d, want 0", financial.RoundingAdjustmentCent)
}
// ----------------------------------------------------
// 6. 批次结算与分站金库到账 (Settlement Batch & Wallet Credit)
// ----------------------------------------------------
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 != 3275 {
t.Fatalf("batch total amount = %d, want 3275", 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 != 3275 {
t.Fatalf("site wallet balance = %d, want 3275", 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 != 3275 || 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 分 (19.37元)", financial.SplitBaseAmountCent)
t.Logf(" ├─ 收号方(主站 35%%): %d 分 (6.78元)", platformParty.IncomeAmountCent)
t.Logf(" ├─ 卖出方提价分成(分站 40%%): %d 分 (7.75元)", distributorParty.IncomeAmountCent-shelfMarkupCent)
t.Logf(" └─ 平台服务费(25%%): %d 分 (4.84元)", financial.ServiceFeeCent)
t.Logf("分站 A 最终结算到账: %d 分 (32.75元) [加价25元 + 分成7.75元]", siteWallet.AvailableBalanceCent)
}