feat(distribution): 完善分销收号资产双视图、商户提现、自主加价、品牌装修与数据大盘

This commit is contained in:
yml2213
2026-09-08 16:05:30 +08:00
parent fd4e490ed3
commit 2c4e87475a
19 changed files with 3145 additions and 108 deletions
@@ -23,7 +23,8 @@ func distributionTestDB(t *testing.T) *gorm.DB {
&model.DistributionSiteMember{}, &model.DistributionRuleVersion{}, &model.DistributionOrderFinancial{},
&model.DistributionOrderParty{}, &model.DistributionSiteAccount{}, &model.DistributionSitePaymentChannel{},
&model.DistributionSettlementBatch{}, &model.DistributionSettlementItem{},
&model.RentalListing{}, &model.GameAccount{},
&model.RentalListing{}, &model.GameAccount{}, &model.User{},
&model.WalletAccount{}, &model.WalletLedger{}, &model.WithdrawalRequest{},
); err != nil {
t.Fatal(err)
}
@@ -390,3 +391,228 @@ func TestGetPublicSiteInfo(t *testing.T) {
}
}
func TestDistributionBrandingAndMarkup(t *testing.T) {
db := distributionTestDB(t)
repo := NewRepository(db)
site := model.DistributionSite{
SiteCode: "brand_site",
SiteName: "品牌测试站",
Domain: "brand.example.com",
Status: "active",
DisplayAccountEnabled: true,
}
if err := db.Create(&site).Error; err != nil {
t.Fatal(err)
}
// 1. Update branding
updated, err := repo.UpdateSiteBranding(t.Context(), 0, site.ID, UpdateSiteBrandingRequest{
LogoURL: "https://cdn.example.com/logo.png",
FaviconURL: "https://cdn.example.com/favicon.ico",
BannersJSON: `[{"image_url":"https://cdn.example.com/b1.jpg"}]`,
AnnouncementsJSON: `[{"title":"开业大吉"}]`,
FooterInfoJSON: `{"copyright":"2026 Brand"}`,
})
if err != nil {
t.Fatalf("UpdateSiteBranding failed: %v", err)
}
if updated.LogoURL != "https://cdn.example.com/logo.png" || updated.FaviconURL != "https://cdn.example.com/favicon.ico" {
t.Fatalf("unexpected branding: %+v", updated)
}
// Verify PublicSiteInfo contains branding
pubInfo, err := repo.GetPublicSiteInfo(t.Context(), "brand_site", "")
if err != nil {
t.Fatalf("GetPublicSiteInfo failed: %v", err)
}
if pubInfo.LogoURL != "https://cdn.example.com/logo.png" {
t.Fatalf("expected LogoURL, got: %+v", pubInfo)
}
// 2. Account Markup
dsa := model.DistributionSiteAccount{
SiteID: site.ID,
AccountID: 101,
ListingID: 201,
DisplayStatus: "enabled",
}
if err := db.Create(&dsa).Error; err != nil {
t.Fatal(err)
}
display, err := repo.UpdateAccountMarkup(t.Context(), 0, dsa.ID, 500)
if err != nil {
t.Fatalf("UpdateAccountMarkup failed: %v", err)
}
if display.MarkupAmountCent != 500 {
t.Fatalf("expected markup 500, got %d", display.MarkupAmountCent)
}
// Batch update markup
if err := repo.BatchUpdateAccountMarkup(t.Context(), 0, site.ID, BatchUpdateAccountMarkupRequest{
ListingIDs: []uint64{201},
MarkupAmountCent: 800,
}); err != nil {
t.Fatalf("BatchUpdateAccountMarkup failed: %v", err)
}
displays, err := repo.ListAccountDisplays(t.Context(), site.ID, "")
if err != nil || len(displays) == 0 || displays[0].MarkupAmountCent != 800 {
t.Fatalf("expected markup 800 after batch, got: %+v", displays)
}
}
func TestDistributionReceivedListingsAndWallet(t *testing.T) {
db := distributionTestDB(t)
repo := NewRepository(db)
site := model.DistributionSite{
SiteCode: "recv_site",
SiteName: "收号测试站",
Domain: "recv.example.com",
Status: "active",
DisplayAccountEnabled: true,
}
if err := db.Create(&site).Error; err != nil {
t.Fatal(err)
}
user := model.User{
Phone: "13800001234",
Nickname: "seller01",
}
if err := db.Create(&user).Error; err != nil {
t.Fatal(err)
}
ga := model.GameAccount{
OwnerID: user.ID,
GameName: "delta_force",
ServerRegion: "cn",
LoginPlatform: "pc",
Title: "满配三角洲账号",
}
if err := db.Create(&ga).Error; err != nil {
t.Fatal(err)
}
listing := model.RentalListing{
ListingNo: "RL-99001",
AccountID: ga.ID,
OwnerID: user.ID,
PriceCent: 2000,
DepositAmountCent: 5000,
Status: "published",
ReviewStatus: "approved",
DistributionSiteID: &site.ID,
}
if err := db.Create(&listing).Error; err != nil {
t.Fatal(err)
}
// 1. List received listings
recPage, err := repo.ListReceivedListings(t.Context(), 0, site.ID, ReceivedListingQuery{
Page: 1,
PageSize: 10,
})
if err != nil {
t.Fatalf("ListReceivedListings failed: %v", err)
}
if recPage.Total != 1 || len(recPage.Items) != 1 {
t.Fatalf("expected 1 received listing, got %d", recPage.Total)
}
item := recPage.Items[0]
if item.ListingNo != "RL-99001" || item.GameName != "delta_force" || item.OwnerNickname != "seller01" {
t.Fatalf("unexpected received listing: %+v", item)
}
// Update received listing status
if err := repo.UpdateReceivedListingStatus(t.Context(), 0, listing.ID, "inactive"); err != nil {
t.Fatalf("UpdateReceivedListingStatus failed: %v", err)
}
// 2. Settlement batch settles and credits wallet
rule := model.DistributionRuleVersion{SiteID: &site.ID, Mode: ModeTwoPartyDistributorReceive, Version: 1, Status: "active", EffectiveFrom: time.Now()}
if err := db.Create(&rule).Error; err != nil {
t.Fatal(err)
}
batch := model.DistributionSettlementBatch{
SiteID: site.ID,
BatchNo: "B-WALLET-01",
PeriodStart: time.Now().Add(-24 * time.Hour),
PeriodEnd: time.Now(),
TotalAmountCent: 10000,
Status: "pending",
}
if err := db.Create(&batch).Error; err != nil {
t.Fatal(err)
}
batchItem := model.DistributionSettlementItem{
BatchID: batch.ID,
OrderID: 888,
PartyID: site.ID,
AmountCent: 10000,
Status: "pending",
}
if err := db.Create(&batchItem).Error; err != nil {
t.Fatal(err)
}
// Mark batch settled -> auto-credits merchant wallet
if _, err := repo.MarkSettlementBatch(t.Context(), 0, batch.ID); err != nil {
t.Fatalf("MarkSettlementBatch failed: %v", err)
}
walletDTO, err := repo.GetSiteWallet(t.Context(), 0, site.ID)
if err != nil {
t.Fatalf("GetSiteWallet failed: %v", err)
}
if walletDTO.AvailableBalanceCent != 10000 || walletDTO.TotalSettledCent != 10000 {
t.Fatalf("expected available balance 10000, got: %+v", walletDTO)
}
// 3. Apply withdrawal
wd, err := repo.ApplySiteWithdrawal(t.Context(), 0, site.ID, ApplySiteWithdrawalRequest{
SiteID: site.ID,
AmountCent: 4000,
AccountType: "alipay",
AccountName: "分销商李四",
AccountNo: "13800005678",
})
if err != nil {
t.Fatalf("ApplySiteWithdrawal failed: %v", err)
}
if wd.AmountCent != 4000 || wd.Status != "pending" {
t.Fatalf("unexpected withdrawal: %+v", wd)
}
// Check balance after freeze
walletAfterWD, err := repo.GetSiteWallet(t.Context(), 0, site.ID)
if err != nil {
t.Fatalf("GetSiteWallet after WD failed: %v", err)
}
if walletAfterWD.AvailableBalanceCent != 6000 || walletAfterWD.FrozenBalanceCent != 4000 {
t.Fatalf("expected available 6000 frozen 4000, got: %+v", walletAfterWD)
}
// 4. List withdrawals
wdPage, err := repo.ListSiteWithdrawals(t.Context(), 0, site.ID, 1, 10)
if err != nil {
t.Fatalf("ListSiteWithdrawals failed: %v", err)
}
if wdPage.Total != 1 || len(wdPage.Items) != 1 {
t.Fatalf("expected 1 withdrawal record, got %d", wdPage.Total)
}
// 5. Dashboard analytics
analytics, err := repo.GetDashboardAnalytics(t.Context(), 0, site.ID)
if err != nil {
t.Fatalf("GetDashboardAnalytics failed: %v", err)
}
if analytics.TotalReceivedCount != 1 || analytics.AvailableBalanceCent != 6000 {
t.Fatalf("unexpected analytics: %+v", analytics)
}
}