package e2e import ( "context" "encoding/json" "net/http" "net/http/httptest" "testing" "time" "github.com/gin-gonic/gin" "gorm.io/datatypes" "gorm.io/gorm" "hfb_sys/backend/internal/database" "hfb_sys/backend/internal/model" "hfb_sys/backend/internal/modules/auth" "hfb_sys/backend/internal/modules/dispute" "hfb_sys/backend/internal/modules/distribution" listingpkg "hfb_sys/backend/internal/modules/listing" orderpkg "hfb_sys/backend/internal/modules/order" "hfb_sys/backend/internal/modules/payment" "hfb_sys/backend/internal/modules/paymentconfig" "hfb_sys/backend/internal/modules/wallet" "hfb_sys/backend/internal/modules/withdrawal" "hfb_sys/backend/pkg/crypto" ) 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) } // TestE2EScenario2_SubSiteReceive_MainSiteSell_FullFlow // 场景 2 完整端到端测试: // 1. 分站 A 收号录入(货源归属分站 A,号主底价 80 元,挂牌基准价 100 元,主站提价 20 元); // 2. 买家在主站前台浏览并直接下单(无分站加价,实付 100 元); // 3. 支付成功自动触发快照 (two_party_distributor_receive): // - 分站 A 作为收号方获得 35% 分成(7.00 元); // - 平台作为卖出方获得 40% 分成(8.00 元)+ 25% 服务费(5.00 元); // // 4. 鉴权验证:分站 A 管理员调用 FindOrder 成功查看本站收益 7.00 元与 receive 角色,且无 403 权限拒绝; // 5. 数据大盘验证:分站 A 查看大盘,今日收号数、今日订单、今日 GMV 及 7 天趋势中 ReceivedCount 均正确计算; // 6. 订单完结后分站 A 创建结算批次并执行入账,分站金库钱包精准入账 7.00 元; // 7. 分站 A 申请金库提现 5.00 元,总台管理端审核(显示分站名称与代号)并确认打款放款,金库可用余额扣除 5.00 元。 func TestE2EScenario2_SubSiteReceive_MainSiteSell_FullFlow(t *testing.T) { db := openDistributionE2EDB(t) ctx := context.Background() now := time.Now() // 1. 基础主体初始化 adminUser := model.AdminUser{Username: "super_admin_2", Status: "active"} if err := db.Create(&adminUser).Error; err != nil { t.Fatal(err) } siteAAdmin := model.AdminUser{Username: "site_receive_admin", Status: "active"} if err := db.Create(&siteAAdmin).Error; err != nil { t.Fatal(err) } siteA := model.DistributionSite{ SiteCode: "branch_beta", SiteName: "分销乙站", Domain: "beta.example.com", Status: "active", DisplayAccountEnabled: true, } if err := db.Create(&siteA).Error; err != nil { t.Fatal(err) } if err := db.Create(&model.DistributionSiteMember{ SiteID: siteA.ID, AdminUserID: siteAAdmin.ID, MemberType: "site_operator", Status: "active", DataScope: "site", }).Error; err != nil { t.Fatal(err) } // 初始化分站 A 金库钱包 if err := db.Create(&model.WalletAccount{ UserID: 0, DistributionSiteID: siteA.ID, AvailableBalanceCent: 0, FrozenBalanceCent: 0, Status: "active", }).Error; err != nil { t.Fatal(err) } // 规则版本:分销收号、主站卖出 (two_party_distributor_receive) // 分站收号 35%,平台卖出 40%,平台服务费 25%,通道费 50 BPS (0.5%) ruleVersion := model.DistributionRuleVersion{ SiteID: &siteA.ID, Mode: distribution.ModeTwoPartyDistributorReceive, Version: 1, Status: "active", DistributorReceiveRatio: 35, PlatformSellRatio: 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: "13800000003", Status: "active"} renter := model.User{Phone: "13900000004", 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. 分站 A 收号: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, "buyer_ratio": 100.0, "buyer_total_price": 100.0, "platform_markup_amount": 20.0, }, }) account := model.GameAccount{ Title: "分销收号王者账号-100M", HafCoinAmount: 100_000_000, AssetSummary: datatypes.JSON(assetSummaryJSON), Status: "active", } if err := db.Create(&account).Error; err != nil { t.Fatal(err) } listing := model.RentalListing{ ListingNo: "L-E2E-RECEIVE-201", AccountID: account.ID, OwnerID: owner.ID, PriceCent: 10000, DepositAmountCent: 0, Status: "published", ReviewStatus: "approved", DistributionSiteID: &siteA.ID, // 货源归属分站 A! } if err := db.Create(&listing).Error; err != nil { t.Fatal(err) } // 3. 买家在主站直接下单(主站无自主加价,DistributionSiteCode 为空) orderRepo := orderpkg.NewRepository(db, orderpkg.Dependencies{}) orderDTO, err := orderRepo.Create(ctx, renter.ID, orderpkg.CreateRequest{ ListingID: listing.ID, }) if err != nil { t.Fatalf("order create failed: %v", err) } if orderDTO.DistributionSiteID == nil || *orderDTO.DistributionSiteID != siteA.ID { t.Fatalf("subsite receive order DistributionSiteID should be site A %d, got %v", siteA.ID, orderDTO.DistributionSiteID) } // 4. 支付成功并触发财务快照 grossPaidCent := int64(10000) 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.Fatal(err) } if financial.SplitBaseAmountCent != 2000 { t.Fatalf("split base amount = %d, want 2000", financial.SplitBaseAmountCent) } var parties []model.DistributionOrderParty if err := db.Where("order_id = ?", orderDTO.ID).Find(&parties).Error; err != nil || len(parties) != 2 { t.Fatalf("parties error: len=%d, err=%v", len(parties), err) } var receiveParty, sellParty model.DistributionOrderParty for _, p := range parties { if p.PartyRole == "distributor_receive" { receiveParty = p } else if p.PartyRole == "platform_sell" { sellParty = p } } // 分站 A 获得收号分成: 2000 * 35% = 700 分 if receiveParty.IncomeAmountCent != 700 || receiveParty.PartyID != siteA.ID { t.Fatalf("receiveParty income=%d (want 700), partyID=%d", receiveParty.IncomeAmountCent, receiveParty.PartyID) } // 平台获得卖出分成: 2000 * 40% = 800 分 if sellParty.IncomeAmountCent != 800 { t.Fatalf("sellParty income=%d (want 800)", sellParty.IncomeAmountCent) } // 5. 权限验证:分站 A 管理员调用 FindOrder (校验无 403 权限拒绝) distRepo := distribution.NewRepository(db) orderDetail, err := distRepo.FindOrder(ctx, siteAAdmin.ID, orderDTO.ID) if err != nil { t.Fatalf("site A admin FindOrder failed: %v (must not return 403)", err) } if orderDetail.DistributorEarningCent != 700 || orderDetail.DistributorRole != "receive" { t.Fatalf("unexpected order detail: earning=%d, role=%s", orderDetail.DistributorEarningCent, orderDetail.DistributorRole) } // 6. 数据大盘统计校验 analytics, err := distRepo.GetDashboardAnalytics(ctx, siteAAdmin.ID, siteA.ID) if err != nil { t.Fatalf("GetDashboardAnalytics failed: %v", err) } if analytics.TotalReceivedCount != 1 || analytics.TodayOrderCount != 1 || analytics.TodayGMVCent != 10000 || analytics.TodayProfitCent != 700 { t.Fatalf("unexpected analytics: %+v", analytics) } if len(analytics.TrendDays) == 0 || analytics.TrendDays[len(analytics.TrendDays)-1].ReceivedCount != 1 { t.Fatalf("unexpected trend received count: %+v", analytics.TrendDays) } // 7. 批次结算与钱包到账 if err := db.Model(&model.RentalOrder{}).Where("id = ?", orderDTO.ID).Update("status", "completed").Error; err != nil { t.Fatal(err) } batch, err := distRepo.CreateSettlementBatch(ctx, siteAAdmin.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 != 700 { t.Fatalf("batch total = %d, want 700", batch.TotalAmountCent) } if _, err := distRepo.MarkSettlementBatch(ctx, siteAAdmin.ID, batch.ID); err != nil { t.Fatalf("MarkSettlementBatch failed: %v", err) } var siteWallet model.WalletAccount if err := db.Where("user_id = 0 AND distribution_site_id = ?", siteA.ID).First(&siteWallet).Error; err != nil { t.Fatal(err) } if siteWallet.AvailableBalanceCent != 700 { t.Fatalf("wallet balance = %d, want 700", siteWallet.AvailableBalanceCent) } // 8. 申请金库提现与总台审核 wdRecord, err := distRepo.ApplySiteWithdrawal(ctx, siteAAdmin.ID, siteA.ID, distribution.ApplySiteWithdrawalRequest{ AmountCent: 500, AccountType: "alipay", AccountName: "分销乙站负责人", AccountNo: "beta@example.com", }) if err != nil { t.Fatalf("ApplySiteWithdrawal failed: %v", err) } if wdRecord.AmountCent != 500 || wdRecord.Status != "pending" { t.Fatalf("withdrawal record = %+v", wdRecord) } // 总台管理员审核并打款 withdrawalRepo := withdrawal.NewRepository(db, wallet.NewRepository(db), &crypto.MockEncryptor{}) wdDetail, err := withdrawalRepo.AdminFindByID(ctx, wdRecord.ID) if err != nil { t.Fatalf("AdminFindByID failed: %v", err) } if wdDetail.DistributionSiteName != "分销乙站" || wdDetail.DistributionSiteCode != "branch_beta" { t.Fatalf("withdrawal detail site name/code mismatch: %+v", wdDetail) } approved := true if _, err := withdrawalRepo.Review(ctx, adminUser.ID, wdRecord.ID, withdrawal.ReviewWithdrawalRequest{ Approved: &approved, Remark: "审核通过", }); err != nil { t.Fatalf("Review withdrawal failed: %v", err) } if _, err := withdrawalRepo.ConfirmPayment(ctx, adminUser.ID, wdRecord.ID, withdrawal.ConfirmPaymentRequest{ Remark: "已打款", }); err != nil { t.Fatalf("ConfirmPayment failed: %v", err) } // 验证提现完成后的金库余额 if err := db.Where("user_id = 0 AND distribution_site_id = ?", siteA.ID).First(&siteWallet).Error; err != nil { t.Fatal(err) } if siteWallet.AvailableBalanceCent != 200 || siteWallet.FrozenBalanceCent != 0 { t.Fatalf("wallet after withdrawal: available=%d, frozen=%d", siteWallet.AvailableBalanceCent, siteWallet.FrozenBalanceCent) } t.Logf("=== 场景 2 全链路端到端验证通过!===") } // TestE2EScenario3_ThreeParty_CrossSiteSell_DedicatedPayment_Settlement // 场景 3 完整端到端测试: // 1. 分站 A 收号(货源归属分站 A); // 2. 分站 B 作为销售分站,配置专属支付商户通道(微信优先级 1); // 3. 分站 B 选品上架并自主加价(提价 20 比例点,标价 100 元变为售价 125 元,分站加价 25 元); // 4. 买家在分站 B 下单实付 125 元; // 5. 核心收银台支付发起 (payment.Start):断言精准命中分站 B 绑定的专属商户号,而非平台默认商户号; // 6. 支付成功自动触发三方分销快照 (three_party): // - 分站 B 获得专属加价 25 元 + 卖出分成 40% (8 元) = 33 元; // - 分站 A 获得收号分成 35% (7 元); // - 平台获得服务费 25% (5 元); // // 7. 隔离鉴权验证: // - 分站 A 管理员仅查看到收号角色与 7 元收益; // - 分站 B 管理员查看到卖出角色、25 元提价及 33 元总收益; // - 无关分站 C 管理员调用 FindOrder 返回 ErrSiteAccessDenied; // // 8. 双方分别创建结算批次并结算,分站 A 金库入账 7 元,分站 B 金库入账 33 元。 func TestE2EScenario3_ThreeParty_CrossSiteSell_DedicatedPayment_Settlement(t *testing.T) { db := openDistributionE2EDB(t) ctx := context.Background() now := time.Now() // 1. 站点初始化 siteA := model.DistributionSite{SiteCode: "site_source", SiteName: "货源分站", Domain: "source.example.com", Status: "active"} siteB := model.DistributionSite{SiteCode: "site_seller", SiteName: "零售分站", Domain: "seller.example.com", Status: "active"} siteC := model.DistributionSite{SiteCode: "site_other", SiteName: "无关分站", Domain: "other.example.com", Status: "active"} if err := db.Create(&siteA).Error; err != nil { t.Fatal(err) } if err := db.Create(&siteB).Error; err != nil { t.Fatal(err) } if err := db.Create(&siteC).Error; err != nil { t.Fatal(err) } // 各站管理员 adminA := model.AdminUser{Username: "admin_a", Status: "active"} adminB := model.AdminUser{Username: "admin_b", Status: "active"} adminC := model.AdminUser{Username: "admin_c", Status: "active"} db.Create(&adminA) db.Create(&adminB) db.Create(&adminC) db.Create(&model.DistributionSiteMember{SiteID: siteA.ID, AdminUserID: adminA.ID, MemberType: "site_operator", Status: "active"}) db.Create(&model.DistributionSiteMember{SiteID: siteB.ID, AdminUserID: adminB.ID, MemberType: "site_operator", Status: "active"}) db.Create(&model.DistributionSiteMember{SiteID: siteC.ID, AdminUserID: adminC.ID, MemberType: "site_operator", Status: "active"}) // 初始化金库 db.Create(&model.WalletAccount{UserID: 0, DistributionSiteID: siteA.ID, Status: "active"}) db.Create(&model.WalletAccount{UserID: 0, DistributionSiteID: siteB.ID, Status: "active"}) // 2. 支付渠道配置 configRepo := paymentconfig.NewRepository(db, &paymentconfig.MockEncryptor{}) orderRepo := orderpkg.NewRepository(db, orderpkg.Dependencies{}) paymentRepo := payment.NewRepository(db, configRepo, orderRepo) defaultConfig := model.PaymentMerchantConfig{ Name: "平台默认微信", Provider: "mock", MerchantID: "PLATFORM_MOCK", PayWay: "WX", IsDefault: true, Status: "active", Environment: "sandbox", } db.Create(&defaultConfig) siteBMerchant := model.PaymentMerchantConfig{ Name: "零售分站微信", Provider: "mock", MerchantID: "SITE_B_MOCK", PayWay: "WX", IsDefault: false, Status: "active", Environment: "sandbox", } db.Create(&siteBMerchant) // 分站 B 绑定专属通道 db.Create(&model.DistributionSitePaymentChannel{ SiteID: siteB.ID, ConfigID: siteBMerchant.ID, Priority: 1, Status: "active", }) // 3. 三方规则 (three_party) ruleVersion := model.DistributionRuleVersion{ SiteID: &siteB.ID, Mode: distribution.ModeThreeParty, Version: 1, Status: "active", DistributorARatio: 35, DistributorBRatio: 40, ServiceFeeValue: 25, ServiceFeeUnit: distribution.ServiceFeeUnitPercent, PaymentFeeBPS: 50, EffectiveFrom: now.Add(-2 * time.Hour), } db.Create(&ruleVersion) // 4. 货源录入到分站 A owner := model.User{Phone: "13800000005", Status: "active"} renter := model.User{Phone: "13900000006", Status: "active"} db.Create(&owner) db.Create(&renter) assetSummaryJSON, _ := json.Marshal(map[string]any{ "publish_ratio": 100.0, "price_breakdown": map[string]any{ "seller_ratio": 125.0, "seller_total_price": 80.0, "buyer_ratio": 100.0, "buyer_total_price": 100.0, "platform_markup_amount": 20.0, }, }) account := model.GameAccount{ Title: "三方分销账号-100M", HafCoinAmount: 100_000_000, AssetSummary: datatypes.JSON(assetSummaryJSON), Status: "active", } db.Create(&account) listing := model.RentalListing{ ListingNo: "L-E2E-THREE-301", AccountID: account.ID, OwnerID: owner.ID, PriceCent: 10000, DepositAmountCent: 0, Status: "published", ReviewStatus: "approved", DistributionSiteID: &siteA.ID, // 货源归属分站 A! } db.Create(&listing) // 5. 分站 B 货架上架并加价 25 元 (售价 125 元) db.Create(&model.DistributionSiteAccount{ SiteID: siteB.ID, AccountID: account.ID, ListingID: listing.ID, DisplayStatus: "enabled", ShelfRatioDelta: 20, }) // 6. 买家在分站 B 下单 orderDTO, err := orderRepo.Create(ctx, renter.ID, orderpkg.CreateRequest{ ListingID: listing.ID, DistributionSiteCode: siteB.SiteCode, }) if err != nil { t.Fatalf("order create failed: %v", err) } if orderDTO.DistributionSiteID == nil || *orderDTO.DistributionSiteID != siteB.ID || orderDTO.RentAmountCent == nil || *orderDTO.RentAmountCent != 12500 { t.Fatalf("order info mismatch: site=%v, amount=%v", orderDTO.DistributionSiteID, orderDTO.RentAmountCent) } // 7. 发起支付:验证命中分站 B 专属商户号 payDTO, err := paymentRepo.Start(ctx, renter.ID, orderDTO.ID, payment.StartPaymentRequest{PayWay: "WX"}, "127.0.0.1") if err != nil { t.Fatalf("payment start failed: %v", err) } if payDTO.PaymentConfigID != siteBMerchant.ID { t.Fatalf("payment expected site B config %d, got %d", siteBMerchant.ID, payDTO.PaymentConfigID) } // 8. 触发财务快照 if err := distribution.SnapshotPaidOrderTx(db, orderDTO.ID, 12500); err != nil { t.Fatalf("SnapshotPaidOrderTx failed: %v", err) } // 验证三方快照拆分 var parties []model.DistributionOrderParty db.Where("order_id = ?", orderDTO.ID).Find(&parties) if len(parties) != 2 { t.Fatalf("expected 2 parties, got %d", len(parties)) } var partyA, partyB model.DistributionOrderParty for _, p := range parties { if p.PartyRole == "distributor_receive" || p.PartyRole == "distributor_a" { partyA = p } else if p.PartyRole == "distributor_sell" || p.PartyRole == "distributor_b" { partyB = p } } // 分站 A 获得收号分成: 2000 * 35% = 700 分 if partyA.IncomeAmountCent != 700 || partyA.PartyID != siteA.ID { t.Fatalf("party A error: income=%d (want 700), partyID=%d", partyA.IncomeAmountCent, partyA.PartyID) } // 分站 B 获得加价 2500 + 卖出分成 800 = 3300 分 if partyB.IncomeAmountCent != 3300 || partyB.PartyID != siteB.ID { t.Fatalf("party B error: income=%d (want 3300), partyID=%d", partyB.IncomeAmountCent, partyB.PartyID) } // 9. 隔离鉴权验证 distRepo := distribution.NewRepository(db) detailA, err := distRepo.FindOrder(ctx, adminA.ID, orderDTO.ID) if err != nil { t.Fatalf("FindOrder admin A failed: %v", err) } if detailA.DistributorEarningCent != 700 || detailA.DistributorRole != "receive" { t.Fatalf("detail A mismatch: earning=%d, role=%s", detailA.DistributorEarningCent, detailA.DistributorRole) } detailB, err := distRepo.FindOrder(ctx, adminB.ID, orderDTO.ID) if err != nil { t.Fatalf("FindOrder admin B failed: %v", err) } if detailB.DistributorEarningCent != 3300 || detailB.DistributorRole != "sell" || detailB.DistributorMarkupCent != 2500 { t.Fatalf("detail B mismatch: earning=%d, role=%s, markup=%d", detailB.DistributorEarningCent, detailB.DistributorRole, detailB.DistributorMarkupCent) } // 无关分站 C 查询必须返回 ErrSiteAccessDenied if _, err := distRepo.FindOrder(ctx, adminC.ID, orderDTO.ID); err != distribution.ErrSiteAccessDenied { t.Fatalf("admin C expected ErrSiteAccessDenied, got %v", err) } // 10. 完单与分别结算 db.Model(&model.RentalOrder{}).Where("id = ?", orderDTO.ID).Update("status", "completed") // 结算分站 A batchA, err := distRepo.CreateSettlementBatch(ctx, adminA.ID, distribution.CreateSettlementBatchRequest{ SiteID: siteA.ID, PeriodStart: now.Add(-24 * time.Hour), PeriodEnd: now.Add(24 * time.Hour), }) if err != nil { t.Fatalf("CreateSettlementBatch A failed: %v", err) } if batchA.TotalAmountCent != 700 { t.Fatalf("batch A total=%d, want 700", batchA.TotalAmountCent) } distRepo.MarkSettlementBatch(ctx, adminA.ID, batchA.ID) // 结算分站 B batchB, err := distRepo.CreateSettlementBatch(ctx, adminB.ID, distribution.CreateSettlementBatchRequest{ SiteID: siteB.ID, PeriodStart: now.Add(-24 * time.Hour), PeriodEnd: now.Add(24 * time.Hour), }) if err != nil { t.Fatalf("CreateSettlementBatch B failed: %v", err) } if batchB.TotalAmountCent != 3300 { t.Fatalf("batch B total=%d, want 3300", batchB.TotalAmountCent) } distRepo.MarkSettlementBatch(ctx, adminB.ID, batchB.ID) // 校验双方钱包余额 var walletA, walletB model.WalletAccount db.Where("user_id = 0 AND distribution_site_id = ?", siteA.ID).First(&walletA) db.Where("user_id = 0 AND distribution_site_id = ?", siteB.ID).First(&walletB) if walletA.AvailableBalanceCent != 700 { t.Fatalf("wallet A balance = %d, want 700", walletA.AvailableBalanceCent) } if walletB.AvailableBalanceCent != 3300 { t.Fatalf("wallet B balance = %d, want 3300", walletB.AvailableBalanceCent) } t.Logf("=== 场景 3 全链路端到端验证通过!===") } // TestE2EScenario4_SiteSelfSell_CoolingOff_AutoRelease // 场景 4 完整端到端测试: // 1. 分站 S 自收自卖(site_self_sell 模式,设置结算冷却期 settlement_hold_days = 3); // 2. 分站 S 货架上架并自主加价 25 元(标价 100 元,加价为 125 元,成本 80 元); // 3. 买家在分站 S 下单实付 125 元; // 4. 支付成功自动触发快照: // - 号主成本 80 元; // - 利润 20 元中,平台收取 25% 服务费(5 元),分站 S 获得 75% 利润(15 元); // - 分站 S 自主加价 25 元 100% 归分站 S; // - 分站 S 总收益 = 25 + 15 = 40 元; // // 5. 订单完单并自动写入冷却期释放时间 release_at(当前时间 + 3 天); // 6. 到期前执行 AutoReleaseDueSettlements:未到期,释放 0 单; // 7. 时间流逝(修改 release_at 为过去时间),执行 AutoReleaseDueSettlements: // - 成功自动释放 1 单; // - 订单财务快照置为 settled,released_at 非空; // - 分站 S 金库钱包入账 40.00 元; // // 8. 再次执行 AutoReleaseDueSettlements:释放 0 单,分站金库钱包余额仍为 40.00 元(幂等无重复入账)。 func TestE2EScenario4_SiteSelfSell_CoolingOff_AutoRelease(t *testing.T) { db := openDistributionE2EDB(t) ctx := context.Background() now := time.Now() // 1. 创建站点 S(配置冷却期 3 天)及管理员 adminS := model.AdminUser{Username: "self_site_operator", Status: "active"} if err := db.Create(&adminS).Error; err != nil { t.Fatal(err) } siteS := model.DistributionSite{ SiteCode: "branch_self", SiteName: "自营分销站", Domain: "self.example.com", Status: "active", DisplayAccountEnabled: true, SettlementHoldDays: 3, } if err := db.Create(&siteS).Error; err != nil { t.Fatal(err) } if err := db.Create(&model.DistributionSiteMember{ SiteID: siteS.ID, AdminUserID: adminS.ID, MemberType: "operator", Status: "active", DataScope: "site", }).Error; err != nil { t.Fatal(err) } // 自收自卖规则:75% 归分站,25% 归平台服务费 rule := model.DistributionRuleVersion{ SiteID: &siteS.ID, Mode: distribution.ModeSiteSelfSell, Version: 1, Status: "active", DistributorSellRatio: 75, ServiceFeeValue: 25, ServiceFeeUnit: distribution.ServiceFeeUnitPercent, EffectiveFrom: now.Add(-time.Hour), } if err := db.Create(&rule).Error; err != nil { t.Fatal(err) } // 2. 号主与租客 owner := model.User{Phone: "13900000007", Status: "active"} renter := model.User{Phone: "13900000008", Status: "active"} db.Create(&owner) db.Create(&renter) // 分站 S 收号 assetSummaryJSON, _ := json.Marshal(map[string]any{ "publish_ratio": 100.0, "price_breakdown": map[string]any{ "seller_ratio": 125.0, "seller_total_price": 80.0, "buyer_ratio": 100.0, "buyer_total_price": 100.0, "platform_markup_amount": 20.0, }, }) account := model.GameAccount{ Title: "自收自卖测试账号-100M", HafCoinAmount: 100_000_000, AssetSummary: datatypes.JSON(assetSummaryJSON), Status: "active", } db.Create(&account) listing := model.RentalListing{ ListingNo: "L-E2E-SELF-401", AccountID: account.ID, OwnerID: owner.ID, PriceCent: 10000, DepositAmountCent: 0, Status: "published", ReviewStatus: "approved", DistributionSiteID: &siteS.ID, } db.Create(&listing) // 分站 S 上架加价 25 元 db.Create(&model.DistributionSiteAccount{ SiteID: siteS.ID, AccountID: account.ID, ListingID: listing.ID, DisplayStatus: "enabled", ShelfRatioDelta: 20, }) // 3. 买家在分站 S 下单 orderRepo := orderpkg.NewRepository(db, orderpkg.Dependencies{}) orderDTO, err := orderRepo.Create(ctx, renter.ID, orderpkg.CreateRequest{ ListingID: listing.ID, DistributionSiteCode: siteS.SiteCode, }) if err != nil { t.Fatalf("order create failed: %v", err) } // 4. 支付成功并触发快照 if err := distribution.SnapshotPaidOrderTx(db, orderDTO.ID, 12500); err != nil { t.Fatalf("SnapshotPaidOrderTx failed: %v", err) } var parties []model.DistributionOrderParty db.Where("order_id = ?", orderDTO.ID).Find(&parties) if len(parties) != 1 { t.Fatalf("expected 1 party for site self sell, got %d", len(parties)) } // 分站 S 获得加价 2500 + 卖出分成 1500 = 4000 分 if parties[0].IncomeAmountCent != 4000 || parties[0].PartyID != siteS.ID { t.Fatalf("party S income = %d, want 4000, partyID = %d", parties[0].IncomeAmountCent, parties[0].PartyID) } // 5. 订单完单并触发 release_at 计算 if err := db.Model(&model.RentalOrder{}).Where("id = ?", orderDTO.ID).Update("status", "completed").Error; err != nil { t.Fatal(err) } if err := distribution.EnsureCompletedReleaseAtTx(db, orderDTO.ID); err != nil { t.Fatalf("EnsureCompletedReleaseAtTx failed: %v", err) } var fin model.DistributionOrderFinancial if err := db.Where("order_id = ?", orderDTO.ID).First(&fin).Error; err != nil { t.Fatal(err) } if fin.ReleaseAt == nil || fin.ReleaseAt.Before(now) { t.Fatalf("expected release_at in future (+3 days), got %v", fin.ReleaseAt) } // 6. 冷却期内释放:应释放 0 单 distRepo := distribution.NewRepository(db) count, err := distRepo.AutoReleaseDueSettlements(ctx) if err != nil { t.Fatalf("AutoReleaseDueSettlements failed: %v", err) } if count != 0 { t.Fatalf("expected 0 released during cooling-off, got %d", count) } // 7. 时间流逝(将 release_at 调整至过去) pastTime := now.Add(-10 * time.Minute) if err := db.Model(&model.DistributionOrderFinancial{}).Where("order_id = ?", orderDTO.ID). Update("release_at", &pastTime).Error; err != nil { t.Fatal(err) } // 执行自动释放 count, err = distRepo.AutoReleaseDueSettlements(ctx) if err != nil { t.Fatalf("AutoReleaseDueSettlements failed: %v", err) } if count != 1 { t.Fatalf("expected 1 released after cooling-off, got %d", count) } // 验证快照已置为已结算 if err := db.Where("order_id = ?", orderDTO.ID).First(&fin).Error; err != nil { t.Fatal(err) } if fin.SettlementStatus != "settled" || fin.ReleasedAt == nil { t.Fatalf("expected financial settled with released_at, got %+v", fin) } // 验证分站 S 金库钱包到账 40.00 元 (4000 分) var siteWallet model.WalletAccount if err := db.Where("user_id = 0 AND distribution_site_id = ?", siteS.ID).First(&siteWallet).Error; err != nil { t.Fatal(err) } if siteWallet.AvailableBalanceCent != 4000 { t.Fatalf("site S wallet balance = %d, want 4000", siteWallet.AvailableBalanceCent) } // 8. 幂等性校验:重复调用不重复到账 count, err = distRepo.AutoReleaseDueSettlements(ctx) if err != nil { t.Fatalf("second AutoReleaseDueSettlements failed: %v", err) } if count != 0 { t.Fatalf("second run expected 0, got %d", count) } if err := db.Where("user_id = 0 AND distribution_site_id = ?", siteS.ID).First(&siteWallet).Error; err != nil { t.Fatal(err) } if siteWallet.AvailableBalanceCent != 4000 { t.Fatalf("second run wallet balance changed = %d, want 4000", siteWallet.AvailableBalanceCent) } t.Logf("=== 场景 4 自收自卖与冷却期自动释放测试通过!===") } // TestE2EScenario5_PostSettlementRefund_And_DisputeArbitration // 场景 5 完整端到端测试: // Part A: 仲裁与风控隔离 // 1. 租客就分销订单发起争议申诉(押金暂扣中); // 2. 客服介入仲裁全额退款; // 3. 断言退款扣除暂扣押金,分销财务快照同步更新净付款与退款; // 4. 重点断言:分销订单在仲裁完单后严禁发放租客成长积分(RenterGrowthLedger 条数为 0),严防刷单套利; // Part B: 结算后退款资金回冲(Post-Settlement Refund Reversal) // 5. 分站订单已完成结算,收益已入账分站金库钱包; // 6. 产生售后退款,触发 ApplyRefundTx: // - 动态重算分站应得收益; // - 自动从分站金库钱包回冲扣减差额; // // 7. 提现掏空钱包后再次发生退款回冲: // - 系统允许扣减至负余额,确保平台兜底不受损且不阻断退款流程。 func TestE2EScenario5_PostSettlementRefund_And_DisputeArbitration(t *testing.T) { db := openDistributionE2EDB(t) ctx := context.Background() now := time.Now() // Part A: 仲裁与成长积分隔离 startedRefundCent := int64(-1) disputeRepo := dispute.NewRepository(db, dispute.Dependencies{ RefundStarter: dispute.RefundStarterFunc(func(_ context.Context, _ uint64, refundAmountCent int64, _ string, _ string) (string, error) { startedRefundCent = refundAmountCent return "refunded", nil }), }) siteD := model.DistributionSite{ SiteCode: "arb_site", SiteName: "仲裁隔离站", Domain: "arb.dist.com", Status: "active", DisplayAccountEnabled: true, SettlementHoldDays: 0, } db.Create(&siteD) ruleD := model.DistributionRuleVersion{ SiteID: &siteD.ID, Mode: distribution.ModeTwoPartyDistributorReceive, Version: 1, Status: "active", DistributorReceiveRatio: 35, PlatformSellRatio: 40, ServiceFeeValue: 25, ServiceFeeUnit: "percent", EffectiveFrom: now.Add(-time.Hour), } db.Create(&ruleD) renter := model.User{Phone: "13900000009", Status: "active"} owner := model.User{Phone: "13900000010", Status: "active"} db.Create(&renter) db.Create(&owner) arbAccount := model.GameAccount{ OwnerID: owner.ID, Title: "仲裁测试账号", ServerRegion: "国服", LoginPlatform: "安卓", Status: "active", } db.Create(&arbAccount) arbListing := model.RentalListing{ AccountID: arbAccount.ID, OwnerID: owner.ID, PriceCent: 10000, DepositAmountCent: 6000, Status: "rented", ReviewStatus: "approved", DistributionSiteID: &siteD.ID, } db.Create(&arbListing) arbOrder := model.RentalOrder{ OrderNo: "ORD-ARB-E2E-1", ListingID: arbListing.ID, AccountID: arbAccount.ID, OwnerID: owner.ID, RenterID: renter.ID, Status: "renting", HandoffStatus: "received", SettlementStatus: "unsettled", RentAmountCent: 10000, OwnerRentAmountCent: 8000, DepositAmountCent: 6000, DepositHoldStatus: model.DepositHoldStatusHeld, DepositHoldAmountCent: 6000, DistributionSiteID: &siteD.ID, DistributionRuleVersionID: &ruleD.ID, } if err := db.Create(&arbOrder).Error; err != nil { t.Fatalf("create arbOrder failed: %v", err) } arbFinancial := model.DistributionOrderFinancial{ OrderID: arbOrder.ID, SiteID: siteD.ID, RuleVersionID: ruleD.ID, GrossPaidAmountCent: arbOrder.RentAmountCent, NetPaidAmountCent: arbOrder.RentAmountCent, SplitBaseAmountCent: 2000, SettlementStatus: "pending", } db.Create(&arbFinancial) // 发起争议并仲裁全额退款 disputeReq, err := disputeRepo.Create(ctx, renter.ID, arbOrder.ID, dispute.CreateRequest{ Type: "haf_coin_dispute", Description: "仲裁测试-租客申请全额退款", }) if err != nil { t.Fatalf("create dispute failed: %v", err) } if _, err := disputeRepo.Arbitrate(ctx, 9001, disputeReq.ID, dispute.ArbitrateRequest{ Result: "full_refund", Remark: "裁决支持全额退款", }, dispute.AuditMeta{}); err != nil { t.Fatalf("arbitrate failed: %v", err) } // 断言实际发起退款金额(裁决总额 16000 - 暂扣押金 6000 = 实际退款 10000) if startedRefundCent != 10000 { t.Fatalf("started refund = %d, want 10000", startedRefundCent) } // 核心风控断言:分销订单仲裁完单严禁产生用户成长积分! var growthLedgerCount int64 db.Model(&model.RenterGrowthLedger{}).Where("order_id = ?", arbOrder.ID).Count(&growthLedgerCount) if growthLedgerCount != 0 { t.Fatalf("分销订单仲裁不应发放用户成长积分,但产生了 %d 条积分记录", growthLedgerCount) } // Part B: 结算后售后退款回冲(Post-Settlement Refund Reversal) siteR := model.DistributionSite{ SiteCode: "rev_site", SiteName: "退款回冲站", Domain: "rev.dist.com", Status: "active", DisplayAccountEnabled: true, SettlementHoldDays: 0, } db.Create(&siteR) ruleR := model.DistributionRuleVersion{ SiteID: &siteR.ID, Mode: distribution.ModeTwoPartyDistributorReceive, Version: 1, Status: "active", DistributorReceiveRatio: 35, PlatformSellRatio: 40, ServiceFeeValue: 25, ServiceFeeUnit: "percent", EffectiveFrom: now.Add(-time.Hour), } db.Create(&ruleR) revAccount := model.GameAccount{ OwnerID: owner.ID, Title: "回冲测试账号", ServerRegion: "国服", LoginPlatform: "安卓", Status: "active", } db.Create(&revAccount) revListing := model.RentalListing{ AccountID: revAccount.ID, OwnerID: owner.ID, PriceCent: 10000, DepositAmountCent: 0, Status: "completed", ReviewStatus: "approved", DistributionSiteID: &siteR.ID, } db.Create(&revListing) revOrder := model.RentalOrder{ OrderNo: "ORD-REV-E2E-1", ListingID: revListing.ID, AccountID: revAccount.ID, OwnerID: owner.ID, RenterID: renter.ID, Status: "completed", RentAmountCent: 10000, OwnerRentAmountCent: 6000, DistributionSiteID: &siteR.ID, DistributionRuleVersionID: &ruleR.ID, } if err := db.Create(&revOrder).Error; err != nil { t.Fatalf("create revOrder failed: %v", err) } past := now.Add(-time.Minute) db.Create(&model.DistributionOrderFinancial{ OrderID: revOrder.ID, SiteID: siteR.ID, RuleVersionID: ruleR.ID, GrossPaidAmountCent: 10000, NetPaidAmountCent: 10000, SplitBaseAmountCent: 4000, SettlementStatus: "pending", ReleaseAt: &past, }) db.Create(&model.DistributionOrderParty{ OrderID: revOrder.ID, SiteID: siteR.ID, PartyRole: "distributor_receive", PartyType: distribution.PartyTypeSite, PartyID: siteR.ID, IncomeAmountCent: 1400, }) distRepo := distribution.NewRepository(db) released, err := distRepo.AutoReleaseDueSettlements(ctx) if err != nil || released != 1 { t.Fatalf("AutoReleaseDueSettlements failed: released=%d, err=%v", released, err) } // 确认释放后钱包有 1400 分 var rWallet model.WalletAccount db.Where("user_id = 0 AND distribution_site_id = ?", siteR.ID).First(&rWallet) if rWallet.AvailableBalanceCent != 1400 { t.Fatalf("wallet balance = %d, want 1400", rWallet.AvailableBalanceCent) } // 售后退款 3000 分:利润从 4000 降为 1000,分站应得份额由 1400 降为 350,自动回冲 1050 if err := distribution.ApplyRefundTx(db, revOrder.ID, 3000); err != nil { t.Fatalf("ApplyRefundTx failed: %v", err) } db.Where("user_id = 0 AND distribution_site_id = ?", siteR.ID).First(&rWallet) if rWallet.AvailableBalanceCent != 350 { t.Fatalf("wallet balance after refund reversal = %d, want 350", rWallet.AvailableBalanceCent) } // 模拟分站已提现掏空钱包(可用余额置 0) db.Model(&model.WalletAccount{}).Where("id = ?", rWallet.ID).Update("available_balance_cent", 0) // 再次发生全额退款回冲(退款 10000 分):分站应得份额归 0,系统必须允许扣减为负余额 (-350) if err := distribution.ApplyRefundTx(db, revOrder.ID, 10000); err != nil { t.Fatalf("reverse with empty wallet failed: %v", err) } db.Where("user_id = 0 AND distribution_site_id = ?", siteR.ID).First(&rWallet) if rWallet.AvailableBalanceCent != -350 { t.Fatalf("wallet balance = %d, want -350 (allow negative balance on reversal)", rWallet.AvailableBalanceCent) } t.Logf("=== 场景 5 仲裁隔离与退款回冲全链路测试通过!===") } // TestE2EScenario6_SiteOnboarding_Branding_PublicStorefront_Sync // 场景 6 完整端到端测试: // 1. 总台管理员一键开站(OnboardSite): // - 校验生成 DistributionSite、独立管理员、成员关系、独立金库钱包与初始规则; // // 2. 分站管理员登录与身份探针: // - Login 签发分站管理员 JWT; // - Me 验证当前登录主体信息与所属分站; // // 3. 分站品牌个性化装修(UpdateSiteBranding): // - 更新站点标题、Logo、Favicon、页脚版权文字; // // 4. 前台三模多端解析验证: // - 模式 A:通过 site_code 参数解析; // - 模式 B:通过独立域名 domain 解析; // - 模式 C:通过 X-Distribution-Site-Code 请求头解析; // // 5. 货架商品同步与上架加价透传: // - 主站发布商品,分站调用 SyncSiteAccounts 一键同步; // - 分站管理员上调货架比例(UpdateAccountMarkup); // - 租客访问分站前台列表(ListPublicSiteListings): // 断言返回商品售价与加价金额精准上浮,展示分站个性化定价。 func TestE2EScenario6_SiteOnboarding_Branding_PublicStorefront_Sync(t *testing.T) { db := openDistributionE2EDB(t) ctx := context.Background() jwtMgr := auth.NewJWTManager("test_secret_key_for_distribution_e2e_32_bytes") distRepo := distribution.NewRepository(db) distSvc := distribution.NewService(distRepo, jwtMgr) distHandler := distribution.NewHandler(distSvc) // 1. 总台管理员开通分站 onboardReq := distribution.OnboardSiteRequest{ SiteCode: "sh_brand_site", SiteName: "魔都电竞分销站", Domain: "sh.esports-dist.com", AdminUsername: "sh_store_admin", AdminPassword: "Password123!", AdminNickname: "魔都站长", MemberType: "operator", } onboardResp, err := distSvc.OnboardSite(ctx, onboardReq) if err != nil { t.Fatalf("OnboardSite failed: %v", err) } if onboardResp.Site.SiteCode != "sh_brand_site" { t.Fatalf("expected SiteCode sh_brand_site, got %s", onboardResp.Site.SiteCode) } siteID := onboardResp.Site.ID // 校验独立金库钱包已同步创建 var siteWallet model.WalletAccount if err := db.Where("user_id = 0 AND distribution_site_id = ?", siteID).First(&siteWallet).Error; err != nil { t.Fatalf("site treasury wallet not created: %v", err) } // 2. 分站管理员登录与 Me 验证 loginResp, err := distSvc.Login(ctx, distribution.DistributionLoginRequest{ Username: "sh_store_admin", Password: "Password123!", }, "127.0.0.1") if err != nil { t.Fatalf("Login failed: %v", err) } if loginResp.Token == "" || loginResp.Member.SiteID == nil || *loginResp.Member.SiteID != siteID { t.Fatalf("unexpected login response: %+v", loginResp) } meResp, err := distSvc.Me(ctx, loginResp.Member.AdminID) if err != nil { t.Fatalf("Me failed: %v", err) } if meResp.SiteID == nil || *meResp.SiteID != siteID || meResp.SiteName != "魔都电竞分销站" { t.Fatalf("unexpected Me response: %+v", meResp) } // 3. 分站品牌个性化装修 logo := "https://cdn.example.com/sh-logo.png" favicon := "https://cdn.example.com/sh-favicon.ico" footerJSON := `{"copyright":"Copyright © 2026 魔都电竞分销站"}` announcementsJSON := `[{"title":"优惠公告","content":"全场极品账号特惠上新"}]` _, err = distRepo.UpdateSiteBranding(ctx, loginResp.Member.AdminID, siteID, distribution.UpdateSiteBrandingRequest{ LogoURL: &logo, FaviconURL: &favicon, FooterInfoJSON: &footerJSON, AnnouncementsJSON: &announcementsJSON, }) if err != nil { t.Fatalf("UpdateSiteBranding failed: %v", err) } // 4. 前台三模解析验证 // 模式 A: 通过 site_code 查询 infoA, err := distRepo.GetPublicSiteInfo(ctx, "sh_brand_site", "") if err != nil { t.Fatalf("GetPublicSiteInfo by code failed: %v", err) } if infoA.LogoURL != logo || infoA.FooterInfoJSON != footerJSON || infoA.AnnouncementsJSON != announcementsJSON { t.Fatalf("info A branding mismatch: %+v", infoA) } // 模式 B: 通过 domain 查询 infoB, err := distRepo.GetPublicSiteInfo(ctx, "", "sh.esports-dist.com") if err != nil { t.Fatalf("GetPublicSiteInfo by domain failed: %v", err) } if infoB.SiteCode != "sh_brand_site" || infoB.FaviconURL != favicon { t.Fatalf("info B domain mismatch: %+v", infoB) } // 模式 C: 通过 X-Distribution-Site-Code 请求头解析 (HTTP Handler 级别) gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) req, _ := http.NewRequest("GET", "/api/v1/distribution/public/site-info", nil) req.Header.Set("X-Distribution-Site-Code", "sh_brand_site") c.Request = req distHandler.GetPublicSiteInfo(c) if w.Code != http.StatusOK { t.Fatalf("GetPublicSiteInfo header expected 200, got %d: %s", w.Code, w.Body.String()) } var httpResp struct { Code string `json:"code"` Data distribution.PublicSiteInfoDTO `json:"data"` } if err := json.Unmarshal(w.Body.Bytes(), &httpResp); err != nil { t.Fatalf("unmarshal header resp failed: %v", err) } if httpResp.Code != "ok" || httpResp.Data.SiteCode != "sh_brand_site" || httpResp.Data.LogoURL != logo { t.Fatalf("header resolution data mismatch: %+v", httpResp) } // 5. 货架商品同步与上架加价透传 // 主站发布极品账号 masterAccount := model.GameAccount{ Title: "CFHD极品生化账号-100M", GameName: "cfhd", ServerRegion: "东部大区", LoginPlatform: "PC", RankLevel: "枪王", HafCoinAmount: 100_000_000, Status: "active", } db.Create(&masterAccount) masterListing := model.RentalListing{ ListingNo: "L-CFHD-SYNC-1", AccountID: masterAccount.ID, OwnerID: 1, PriceCent: 5000, DepositAmountCent: 2000, Status: "published", ReviewStatus: "approved", } db.Create(&masterListing) // 分站管理员调用同步接口 syncRes, err := distRepo.SyncSiteAccounts(ctx, loginResp.Member.AdminID, siteID, distribution.SyncSiteAccountsRequest{CleanStaleListings: true}) if err != nil { t.Fatalf("SyncSiteAccounts failed: %v", err) } if syncRes.SyncedCount == 0 { t.Fatalf("expected at least 1 account synced, got %+v", syncRes) } // 查询同步生成的货架记录 var dsa model.DistributionSiteAccount if err := db.Where("site_id = ? AND listing_id = ?", siteID, masterListing.ID).First(&dsa).Error; err != nil { t.Fatalf("DistributionSiteAccount not found: %v", err) } // 分站管理员调整自主加价比例 (调减比例 20,使前台标价上浮) updatedDisplay, err := distRepo.UpdateAccountMarkup(ctx, loginResp.Member.AdminID, dsa.ID, 20) if err != nil { t.Fatalf("UpdateAccountMarkup failed: %v", err) } if updatedDisplay.ShelfRatioDelta != 20 { t.Fatalf("shelf ratio delta = %v, want 20", updatedDisplay.ShelfRatioDelta) } // 租客访问前台商品列表 (ListPublicSiteListings) publicListings, err := distRepo.ListPublicSiteListings(ctx, "sh_brand_site", 1, 10) if err != nil { t.Fatalf("ListPublicSiteListings failed: %v", err) } if len(publicListings.Items) == 0 { t.Fatalf("expected at least 1 public listing") } var foundListing *distribution.PublicDistributionListingDTO for _, item := range publicListings.Items { if item.ListingID == masterListing.ID { itemCopy := item foundListing = &itemCopy break } } if foundListing == nil { t.Fatalf("synced listing %d not found in public listings", masterListing.ID) } // 验证加价与前台售价生效 (标价从 5000 经 ApplyShelfRatio 调整为 5560,加价 560 分) if foundListing.PriceCent != 5560 || foundListing.MarkupAmountCent != 560 { t.Fatalf("listing pricing mismatch: price=%d (want 5560), markup=%d (want 560)", foundListing.PriceCent, foundListing.MarkupAmountCent) } t.Logf("=== 场景 6 一键开站、品牌装修与前台货架同步加价全链路验证通过!===") }