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) }