- 后端优化: - 引入 Redis + 内存 sync.Map 双重互斥锁,避免并发/连击导致唯一索引冲突 (ErrSyncInProgress) - 使用 CreateInBatches(500) 进行大数据量批量插入,杜绝超长 SQL 与锁表超时 - 增加 CleanStaleListings 支持,自动排查并联动下架主站非在售的货架商品,清理历史幽灵数据 - 增加 DefaultShelfRatioDelta 支持,新同步商品直接继承初始加价比例,免除二次手动改价 - 前端优化: - distribution-admin 货架管理新增一键同步配置弹窗(可勾选自动下架失效商品、配置初始加价) - 同步完成后展示结构化统计弹窗(新增、保持、联动下架、主站在售总量) - 前后端类型定义与 API 客户端对齐升级 - 测试覆盖: - 补充 TestSyncSiteAccountsOptimizations 单元测试与 handler 集成测试 - 全工程测试与门禁 check.sh 100% 通过
1496 lines
56 KiB
Go
1496 lines
56 KiB
Go
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 一键开站、品牌装修与前台货架同步加价全链路验证通过!===")
|
||
}
|