331 lines
13 KiB
Go
331 lines
13 KiB
Go
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)
|
||
}
|