feat(distribution): 独立分销前台商城多租户体系、货源与订单数据严格隔离

This commit is contained in:
yml2213
2026-09-07 17:18:14 +08:00
parent 550b42eb22
commit ecaf15417b
34 changed files with 2397 additions and 119 deletions
+56 -5
View File
@@ -121,13 +121,64 @@ type DistributionOrderPage struct {
}
type AccountDisplayDTO struct {
ID uint64 `json:"id"`
SiteID uint64 `json:"site_id"`
AccountID uint64 `json:"account_id"`
ListingID uint64 `json:"listing_id"`
DisplayStatus string `json:"display_status"`
ID uint64 `json:"id"`
SiteID uint64 `json:"site_id"`
AccountID uint64 `json:"account_id"`
ListingID uint64 `json:"listing_id"`
DisplayStatus string `json:"display_status"`
ListingNo string `json:"listing_no,omitempty"`
Title string `json:"title,omitempty"`
GameName string `json:"game_name,omitempty"`
ServerRegion string `json:"server_region,omitempty"`
LoginPlatform string `json:"login_platform,omitempty"`
RankLevel string `json:"rank_level,omitempty"`
HafCoinAmount int64 `json:"haf_coin_amount,omitempty"`
PriceCent int64 `json:"price_cent,omitempty"`
DepositAmountCent int64 `json:"deposit_amount_cent,omitempty"`
ListingStatus string `json:"listing_status,omitempty"`
InTransaction bool `json:"in_transaction,omitempty"`
}
type SyncSiteAccountsResult struct {
SyncedCount int64 `json:"synced_count"`
SkippedCount int64 `json:"skipped_count"`
TotalActive int64 `json:"total_active"`
}
type PublicDistributionListingDTO struct {
ListingID uint64 `json:"listing_id"`
ListingNo string `json:"listing_no"`
AccountID uint64 `json:"account_id"`
Title string `json:"title"`
GameName string `json:"game_name"`
ServerRegion string `json:"server_region"`
LoginPlatform string `json:"login_platform"`
RankLevel string `json:"rank_level"`
HafCoinAmount int64 `json:"haf_coin_amount"`
PriceCent int64 `json:"price_cent"`
DepositAmountCent int64 `json:"deposit_amount_cent"`
InTransaction bool `json:"in_transaction"`
SiteCode string `json:"site_code"`
SiteName string `json:"site_name"`
}
type PublicDistributionListingsResult struct {
Items []PublicDistributionListingDTO `json:"items"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
type PublicSiteInfoDTO struct {
SiteCode string `json:"site_code"`
SiteName string `json:"site_name"`
Domain string `json:"domain"`
Status string `json:"status"`
DisplayAccountEnabled bool `json:"display_account_enabled"`
DefaultSupportGroupID *uint64 `json:"default_support_group_id,omitempty"`
}
type UpdateAccountDisplayRequest struct {
DisplayStatus string `json:"display_status" binding:"required,oneof=enabled disabled sold offline"`
}
@@ -186,10 +186,7 @@ func (h *Handler) CreateFinancialSnapshot(c *gin.Context) {
func (h *Handler) ListAccountDisplays(c *gin.Context) {
siteID := parseQueryUint(c, "site_id")
if siteID <= 0 {
if siteID == 0 {
response.BadRequest(c, "site_id 不正确")
}
if siteID < 0 {
return
}
adminID, _ := currentAdminID(c)
@@ -198,6 +195,10 @@ func (h *Handler) ListAccountDisplays(c *gin.Context) {
writeError(c, err)
return
}
if scoped == 0 && siteID == 0 {
response.BadRequest(c, "site_id 不正确")
return
}
items, err := h.service.ListAccountDisplays(c.Request.Context(), scoped, c.Query("status"))
if err != nil {
writeError(c, err)
@@ -225,6 +226,75 @@ func (h *Handler) UpdateAccountDisplay(c *gin.Context) {
response.OK(c, item)
}
func (h *Handler) SyncSiteAccounts(c *gin.Context) {
var siteID uint64
id, ok := parseID(c)
if ok && id > 0 {
siteID = id
} else {
qID := parseQueryUint(c, "site_id")
if qID > 0 {
siteID = uint64(qID)
}
}
adminID, _ := currentAdminID(c)
scoped, err := h.service.ScopeSiteID(c.Request.Context(), adminID, siteID)
if err != nil {
writeError(c, err)
return
}
if scoped == 0 {
response.BadRequest(c, "site_id 不正确")
return
}
result, err := h.service.SyncSiteAccounts(c.Request.Context(), adminID, scoped)
if err != nil {
writeError(c, err)
return
}
response.OK(c, result)
}
func (h *Handler) ListPublicSiteListings(c *gin.Context) {
siteCode := strings.TrimSpace(c.Param("site_code"))
if siteCode == "" {
siteCode = strings.TrimSpace(c.Query("site_code"))
}
if siteCode == "" {
response.BadRequest(c, "site_code 不能为空")
return
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
result, err := h.service.ListPublicSiteListings(c.Request.Context(), siteCode, page, pageSize)
if err != nil {
writeError(c, err)
return
}
response.OK(c, result)
}
func (h *Handler) GetPublicSiteInfo(c *gin.Context) {
code := strings.TrimSpace(c.Query("code"))
if code == "" {
code = strings.TrimSpace(c.Query("site_code"))
}
domain := strings.TrimSpace(c.Query("domain"))
if code == "" && domain == "" {
host := c.Request.Host
if colon := strings.Index(host, ":"); colon != -1 {
host = host[:colon]
}
domain = host
}
info, err := h.service.GetPublicSiteInfo(c.Request.Context(), code, domain)
if err != nil {
writeError(c, err)
return
}
response.OK(c, info)
}
func (h *Handler) ListPaymentChannels(c *gin.Context) {
siteID := parseQueryUint(c, "site_id")
if siteID <= 0 {
@@ -346,17 +346,76 @@ func (r *Repository) FindOrder(ctx context.Context, adminID, id uint64) (*Distri
}
func (r *Repository) ListAccountDisplays(ctx context.Context, siteID uint64, status string) ([]AccountDisplayDTO, error) {
var rows []model.DistributionSiteAccount
db := r.db.WithContext(ctx).Where("site_id = ?", siteID).Order("id DESC")
if status != "" {
db = db.Where("display_status = ?", status)
type accountDisplayRow struct {
ID uint64 `gorm:"column:id"`
SiteID uint64 `gorm:"column:site_id"`
AccountID uint64 `gorm:"column:account_id"`
ListingID uint64 `gorm:"column:listing_id"`
DisplayStatus string `gorm:"column:display_status"`
ListingNo string `gorm:"column:listing_no"`
Title string `gorm:"column:title"`
GameName string `gorm:"column:game_name"`
ServerRegion string `gorm:"column:server_region"`
LoginPlatform string `gorm:"column:login_platform"`
RankLevel string `gorm:"column:rank_level"`
HafCoinAmount int64 `gorm:"column:haf_coin_amount"`
PriceCent int64 `gorm:"column:price_cent"`
DepositAmountCent int64 `gorm:"column:deposit_amount_cent"`
ListingStatus string `gorm:"column:listing_status"`
InTransaction bool `gorm:"column:in_transaction"`
}
if err := db.Find(&rows).Error; err != nil {
var rows []accountDisplayRow
query := r.db.WithContext(ctx).
Table("distribution_site_accounts AS dsa").
Select(`
dsa.id,
dsa.site_id,
dsa.account_id,
dsa.listing_id,
dsa.display_status,
COALESCE(l.listing_no, '') AS listing_no,
COALESCE(ga.title, '') AS title,
COALESCE(ga.game_name, '') AS game_name,
COALESCE(ga.server_region, '') AS server_region,
COALESCE(ga.login_platform, '') AS login_platform,
COALESCE(ga.rank_level, '') AS rank_level,
COALESCE(ga.haf_coin_amount, 0) AS haf_coin_amount,
COALESCE(l.price_cent, 0) AS price_cent,
COALESCE(l.deposit_amount_cent, 0) AS deposit_amount_cent,
COALESCE(l.status, '') AS listing_status,
COALESCE(l.in_transaction, 0) AS in_transaction
`).
Joins("LEFT JOIN rental_listings AS l ON l.id = dsa.listing_id").
Joins("LEFT JOIN game_accounts AS ga ON ga.id = dsa.account_id").
Where("dsa.site_id = ?", siteID)
if status != "" {
query = query.Where("dsa.display_status = ?", status)
}
if err := query.Order("dsa.id DESC").Scan(&rows).Error; err != nil {
return nil, err
}
items := make([]AccountDisplayDTO, 0, len(rows))
for _, row := range rows {
items = append(items, AccountDisplayDTO{ID: row.ID, SiteID: row.SiteID, AccountID: row.AccountID, ListingID: row.ListingID, DisplayStatus: row.DisplayStatus})
items = append(items, AccountDisplayDTO{
ID: row.ID,
SiteID: row.SiteID,
AccountID: row.AccountID,
ListingID: row.ListingID,
DisplayStatus: row.DisplayStatus,
ListingNo: row.ListingNo,
Title: row.Title,
GameName: row.GameName,
ServerRegion: row.ServerRegion,
LoginPlatform: row.LoginPlatform,
RankLevel: row.RankLevel,
HafCoinAmount: row.HafCoinAmount,
PriceCent: row.PriceCent,
DepositAmountCent: row.DepositAmountCent,
ListingStatus: row.ListingStatus,
InTransaction: row.InTransaction,
})
}
return items, nil
}
@@ -376,10 +435,107 @@ func (r *Repository) UpdateAccountDisplay(ctx context.Context, adminID, id uint6
if err := r.db.WithContext(ctx).Save(&row).Error; err != nil {
return nil, err
}
item := &AccountDisplayDTO{ID: row.ID, SiteID: row.SiteID, AccountID: row.AccountID, ListingID: row.ListingID, DisplayStatus: row.DisplayStatus}
item := &AccountDisplayDTO{
ID: row.ID,
SiteID: row.SiteID,
AccountID: row.AccountID,
ListingID: row.ListingID,
DisplayStatus: row.DisplayStatus,
}
var listing model.RentalListing
if err := r.db.WithContext(ctx).First(&listing, row.ListingID).Error; err == nil {
item.ListingNo = listing.ListingNo
item.PriceCent = listing.PriceCent
item.DepositAmountCent = listing.DepositAmountCent
item.ListingStatus = listing.Status
item.InTransaction = listing.InTransaction
}
var account model.GameAccount
if err := r.db.WithContext(ctx).First(&account, row.AccountID).Error; err == nil {
item.Title = account.Title
item.GameName = account.GameName
item.ServerRegion = account.ServerRegion
item.LoginPlatform = account.LoginPlatform
item.RankLevel = account.RankLevel
item.HafCoinAmount = account.HafCoinAmount
}
return item, nil
}
func (r *Repository) SyncSiteAccounts(ctx context.Context, adminID, siteID uint64) (*SyncSiteAccountsResult, error) {
if r.db == nil {
return nil, ErrDependencyUnavailable
}
if err := r.ensureSiteAccess(ctx, adminID, siteID); err != nil {
return nil, err
}
var site model.DistributionSite
if err := r.db.WithContext(ctx).First(&site, siteID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrSiteNotFound
}
return nil, err
}
var activeListings []model.RentalListing
if err := r.db.WithContext(ctx).
Where("status = ? AND review_status = ?", "published", "approved").
Order("id ASC").
Find(&activeListings).Error; err != nil {
return nil, err
}
var existingAccounts []model.DistributionSiteAccount
if err := r.db.WithContext(ctx).
Where("site_id = ?", siteID).
Find(&existingAccounts).Error; err != nil {
return nil, err
}
existingMap := make(map[uint64]model.DistributionSiteAccount, len(existingAccounts))
for _, ea := range existingAccounts {
existingMap[ea.AccountID] = ea
}
var toInsert []model.DistributionSiteAccount
seenAccount := make(map[uint64]bool)
var skippedCount int64
for _, l := range activeListings {
if seenAccount[l.AccountID] {
continue
}
seenAccount[l.AccountID] = true
if ea, exists := existingMap[l.AccountID]; exists {
if ea.ListingID != l.ID {
_ = r.db.WithContext(ctx).Model(&model.DistributionSiteAccount{}).
Where("id = ?", ea.ID).Update("listing_id", l.ID).Error
}
skippedCount++
} else {
toInsert = append(toInsert, model.DistributionSiteAccount{
SiteID: siteID,
AccountID: l.AccountID,
ListingID: l.ID,
DisplayStatus: "enabled",
})
}
}
if len(toInsert) > 0 {
if err := r.db.WithContext(ctx).Create(&toInsert).Error; err != nil {
return nil, err
}
}
return &SyncSiteAccountsResult{
SyncedCount: int64(len(toInsert)),
SkippedCount: skippedCount,
TotalActive: int64(len(activeListings)),
}, nil
}
func (r *Repository) ListPaymentChannels(ctx context.Context, siteID uint64) ([]PaymentChannelDTO, error) {
var rows []model.DistributionSitePaymentChannel
if err := r.db.WithContext(ctx).Where("site_id = ?", siteID).Order("priority ASC, id ASC").Find(&rows).Error; err != nil {
@@ -860,6 +1016,34 @@ func (r *Repository) OnboardSite(ctx context.Context, req OnboardSiteRequest) (*
return err
}
if displayEnabled {
var activeListings []model.RentalListing
if err := tx.Where("status = ? AND review_status = ?", "published", "approved").Order("id ASC").Find(&activeListings).Error; err != nil {
return err
}
if len(activeListings) > 0 {
siteAccounts := make([]model.DistributionSiteAccount, 0, len(activeListings))
seenAccount := make(map[uint64]bool)
for _, l := range activeListings {
if seenAccount[l.AccountID] {
continue
}
seenAccount[l.AccountID] = true
siteAccounts = append(siteAccounts, model.DistributionSiteAccount{
SiteID: site.ID,
AccountID: l.AccountID,
ListingID: l.ID,
DisplayStatus: "enabled",
})
}
if len(siteAccounts) > 0 {
if err := tx.Create(&siteAccounts).Error; err != nil {
return err
}
}
}
}
resp = OnboardSiteResponse{
Site: siteDTO(site),
Rule: ruleDTO(rule),
@@ -882,3 +1066,150 @@ func (r *Repository) OnboardSite(ctx context.Context, req OnboardSiteRequest) (*
}
return &resp, nil
}
func (r *Repository) ListPublicSiteListings(ctx context.Context, siteCode string, page, pageSize int) (*PublicDistributionListingsResult, error) {
if r.db == nil {
return nil, ErrDependencyUnavailable
}
if strings.TrimSpace(siteCode) == "" {
return nil, ErrSiteNotFound
}
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 20
}
if pageSize > 100 {
pageSize = 100
}
var site model.DistributionSite
if err := r.db.WithContext(ctx).Where("site_code = ? AND status = 'active'", siteCode).First(&site).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrSiteNotFound
}
return nil, err
}
if !site.DisplayAccountEnabled {
return &PublicDistributionListingsResult{
Items: []PublicDistributionListingDTO{},
Total: 0,
Page: page,
PageSize: pageSize,
}, nil
}
type publicListingRow struct {
ListingID uint64 `gorm:"column:listing_id"`
ListingNo string `gorm:"column:listing_no"`
AccountID uint64 `gorm:"column:account_id"`
Title string `gorm:"column:title"`
GameName string `gorm:"column:game_name"`
ServerRegion string `gorm:"column:server_region"`
LoginPlatform string `gorm:"column:login_platform"`
RankLevel string `gorm:"column:rank_level"`
HafCoinAmount int64 `gorm:"column:haf_coin_amount"`
PriceCent int64 `gorm:"column:price_cent"`
DepositAmountCent int64 `gorm:"column:deposit_amount_cent"`
InTransaction bool `gorm:"column:in_transaction"`
}
baseQuery := r.db.WithContext(ctx).
Table("distribution_site_accounts AS dsa").
Joins("INNER JOIN rental_listings AS l ON l.id = dsa.listing_id AND l.status = 'published' AND l.review_status = 'approved'").
Joins("INNER JOIN game_accounts AS ga ON ga.id = dsa.account_id").
Where("dsa.site_id = ? AND dsa.display_status = 'enabled'", site.ID)
var total int64
if err := baseQuery.Count(&total).Error; err != nil {
return nil, err
}
var rows []publicListingRow
err := baseQuery.
Select(`
dsa.listing_id,
l.listing_no,
dsa.account_id,
COALESCE(ga.title, '') AS title,
COALESCE(ga.game_name, '') AS game_name,
COALESCE(ga.server_region, '') AS server_region,
COALESCE(ga.login_platform, '') AS login_platform,
COALESCE(ga.rank_level, '') AS rank_level,
COALESCE(ga.haf_coin_amount, 0) AS haf_coin_amount,
COALESCE(l.price_cent, 0) AS price_cent,
COALESCE(l.deposit_amount_cent, 0) AS deposit_amount_cent,
COALESCE(l.in_transaction, 0) AS in_transaction
`).
Order("dsa.id DESC").
Offset((page - 1) * pageSize).
Limit(pageSize).
Scan(&rows).Error
if err != nil {
return nil, err
}
items := make([]PublicDistributionListingDTO, 0, len(rows))
for _, row := range rows {
items = append(items, PublicDistributionListingDTO{
ListingID: row.ListingID,
ListingNo: row.ListingNo,
AccountID: row.AccountID,
Title: row.Title,
GameName: row.GameName,
ServerRegion: row.ServerRegion,
LoginPlatform: row.LoginPlatform,
RankLevel: row.RankLevel,
HafCoinAmount: row.HafCoinAmount,
PriceCent: row.PriceCent,
DepositAmountCent: row.DepositAmountCent,
InTransaction: row.InTransaction,
SiteCode: site.SiteCode,
SiteName: site.SiteName,
})
}
return &PublicDistributionListingsResult{
Items: items,
Total: total,
Page: page,
PageSize: pageSize,
}, nil
}
func (r *Repository) GetPublicSiteInfo(ctx context.Context, code, domain string) (*PublicSiteInfoDTO, error) {
if r.db == nil {
return nil, ErrDependencyUnavailable
}
code = strings.TrimSpace(code)
domain = strings.TrimSpace(domain)
if code == "" && domain == "" {
return nil, ErrSiteNotFound
}
var site model.DistributionSite
query := r.db.WithContext(ctx).Where("status = 'active'")
if code != "" {
query = query.Where("site_code = ?", code)
} else {
query = query.Where("domain = ?", domain)
}
if err := query.First(&site).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrSiteNotFound
}
return nil, err
}
return &PublicSiteInfoDTO{
SiteCode: site.SiteCode,
SiteName: site.SiteName,
Domain: site.Domain,
Status: site.Status,
DisplayAccountEnabled: site.DisplayAccountEnabled,
DefaultSupportGroupID: site.DefaultSupportGroupID,
}, nil
}
@@ -1,6 +1,7 @@
package distribution
import (
"errors"
"testing"
"time"
@@ -16,7 +17,14 @@ func distributionTestDB(t *testing.T) *gorm.DB {
if err != nil {
t.Fatal(err)
}
if err := db.AutoMigrate(&model.AdminUser{}, &model.Role{}, &model.ChatConversation{}, &model.RentalOrder{}, &model.AuditLog{}, &model.PaymentMerchantConfig{}, &model.DistributionSite{}, &model.DistributionSiteMember{}, &model.DistributionRuleVersion{}, &model.DistributionOrderFinancial{}, &model.DistributionOrderParty{}, &model.DistributionSiteAccount{}, &model.DistributionSitePaymentChannel{}, &model.DistributionSettlementBatch{}, &model.DistributionSettlementItem{}); err != nil {
if err := db.AutoMigrate(
&model.AdminUser{}, &model.Role{}, &model.ChatConversation{}, &model.RentalOrder{},
&model.AuditLog{}, &model.PaymentMerchantConfig{}, &model.DistributionSite{},
&model.DistributionSiteMember{}, &model.DistributionRuleVersion{}, &model.DistributionOrderFinancial{},
&model.DistributionOrderParty{}, &model.DistributionSiteAccount{}, &model.DistributionSitePaymentChannel{},
&model.DistributionSettlementBatch{}, &model.DistributionSettlementItem{},
&model.RentalListing{}, &model.GameAccount{},
); err != nil {
t.Fatal(err)
}
return db
@@ -177,3 +185,208 @@ func TestScopeSiteIDRestrictsBoundMember(t *testing.T) {
t.Fatalf("UpdateAccountDisplay cross-site err = %v", err)
}
}
func TestSyncSiteAccountsAndPublicListings(t *testing.T) {
db := distributionTestDB(t)
repo := NewRepository(db)
site := model.DistributionSite{
SiteCode: "sync_demo",
SiteName: "同步演示站",
Domain: "sync.example.com",
DisplayAccountEnabled: true,
Status: "active",
}
if err := db.Create(&site).Error; err != nil {
t.Fatal(err)
}
account1 := model.GameAccount{
Title: "三角洲极品金枪号",
GameName: "delta_force",
ServerRegion: "微信区",
LoginPlatform: "PC",
RankLevel: "王者",
HafCoinAmount: 500000,
Status: "active",
}
if err := db.Create(&account1).Error; err != nil {
t.Fatal(err)
}
listing1 := model.RentalListing{
ListingNo: "L10001",
AccountID: account1.ID,
OwnerID: 1,
PriceCent: 2500,
DepositAmountCent: 10000,
Status: "published",
ReviewStatus: "approved",
InTransaction: false,
}
if err := db.Create(&listing1).Error; err != nil {
t.Fatal(err)
}
// 草稿状态的商品不应该被同步
listingDraft := model.RentalListing{
ListingNo: "L10002",
AccountID: 999,
OwnerID: 1,
PriceCent: 3000,
DepositAmountCent: 5000,
Status: "draft",
ReviewStatus: "pending",
}
if err := db.Create(&listingDraft).Error; err != nil {
t.Fatal(err)
}
// 首次同步:应当成功新增 1 条,跳过 0 条
res1, err := repo.SyncSiteAccounts(t.Context(), 0, site.ID)
if err != nil {
t.Fatalf("SyncSiteAccounts err = %v", err)
}
if res1.SyncedCount != 1 || res1.SkippedCount != 0 || res1.TotalActive != 1 {
t.Fatalf("unexpected res1: %+v", res1)
}
// 再次同步:幂等保证,跳过 1 条,新增 0 条
res2, err := repo.SyncSiteAccounts(t.Context(), 0, site.ID)
if err != nil {
t.Fatalf("SyncSiteAccounts retry err = %v", err)
}
if res2.SyncedCount != 0 || res2.SkippedCount != 1 || res2.TotalActive != 1 {
t.Fatalf("unexpected res2: %+v", res2)
}
// 验证管理后台列表查询携带丰富字段
displays, err := repo.ListAccountDisplays(t.Context(), site.ID, "")
if err != nil {
t.Fatalf("ListAccountDisplays err = %v", err)
}
if len(displays) != 1 {
t.Fatalf("expected 1 display, got %d", len(displays))
}
d := displays[0]
if d.Title != "三角洲极品金枪号" || d.GameName != "delta_force" || d.PriceCent != 2500 || d.DepositAmountCent != 10000 || d.ListingNo != "L10001" {
t.Fatalf("unexpected enriched display: %+v", d)
}
// 验证公共端点浏览查询
pubList, err := repo.ListPublicSiteListings(t.Context(), site.SiteCode, 1, 20)
if err != nil {
t.Fatalf("ListPublicSiteListings err = %v", err)
}
if pubList.Total != 1 || len(pubList.Items) != 1 {
t.Fatalf("expected 1 public item, got %+v", pubList)
}
if pubList.Items[0].Title != "三角洲极品金枪号" || pubList.Items[0].SiteCode != site.SiteCode {
t.Fatalf("unexpected public item: %+v", pubList.Items[0])
}
// 下架该展示账号后,公共端点不再返回
if _, err := repo.UpdateAccountDisplay(t.Context(), 0, d.ID, "disabled"); err != nil {
t.Fatalf("UpdateAccountDisplay disabled err = %v", err)
}
pubList2, err := repo.ListPublicSiteListings(t.Context(), site.SiteCode, 1, 20)
if err != nil {
t.Fatalf("ListPublicSiteListings after disabled err = %v", err)
}
if pubList2.Total != 0 || len(pubList2.Items) != 0 {
t.Fatalf("expected 0 public items after disabled, got %+v", pubList2)
}
}
func TestOnboardSiteAutoSyncsActiveListings(t *testing.T) {
db := distributionTestDB(t)
repo := NewRepository(db)
account := model.GameAccount{
Title: "王者荣耀国服账号",
GameName: "honor_of_kings",
ServerRegion: "手Q一区",
LoginPlatform: "iOS",
Status: "active",
}
if err := db.Create(&account).Error; err != nil {
t.Fatal(err)
}
listing := model.RentalListing{
ListingNo: "L20001",
AccountID: account.ID,
OwnerID: 2,
PriceCent: 5000,
Status: "published",
ReviewStatus: "approved",
}
if err := db.Create(&listing).Error; err != nil {
t.Fatal(err)
}
dispEnabled := true
onboardResp, err := repo.OnboardSite(t.Context(), OnboardSiteRequest{
SiteCode: "auto_sync_site",
SiteName: "自动同步站点",
Domain: "auto.example.com",
AdminUsername: "auto_admin",
AdminPassword: "securepass123",
DisplayAccountEnabled: &dispEnabled,
})
if err != nil {
t.Fatalf("OnboardSite err = %v", err)
}
displays, err := repo.ListAccountDisplays(t.Context(), onboardResp.Site.ID, "")
if err != nil {
t.Fatalf("ListAccountDisplays err = %v", err)
}
if len(displays) != 1 {
t.Fatalf("expected 1 auto-synced account, got %d", len(displays))
}
if displays[0].Title != "王者荣耀国服账号" || displays[0].ListingNo != "L20001" {
t.Fatalf("unexpected auto-synced display: %+v", displays[0])
}
}
func TestGetPublicSiteInfo(t *testing.T) {
db := distributionTestDB(t)
repo := NewRepository(db)
site := model.DistributionSite{
SiteCode: "info_site",
SiteName: "信息测试站",
Domain: "info.example.com",
Status: "active",
DisplayAccountEnabled: true,
}
if err := db.Create(&site).Error; err != nil {
t.Fatal(err)
}
// Query by code
info, err := repo.GetPublicSiteInfo(t.Context(), "info_site", "")
if err != nil {
t.Fatalf("GetPublicSiteInfo by code failed: %v", err)
}
if info.SiteName != "信息测试站" || info.SiteCode != "info_site" {
t.Fatalf("unexpected info: %+v", info)
}
// Query by domain
infoByDomain, err := repo.GetPublicSiteInfo(t.Context(), "", "info.example.com")
if err != nil {
t.Fatalf("GetPublicSiteInfo by domain failed: %v", err)
}
if infoByDomain.SiteName != "信息测试站" {
t.Fatalf("unexpected info by domain: %+v", infoByDomain)
}
// Query not found
_, err = repo.GetPublicSiteInfo(t.Context(), "not_found", "")
if !errors.Is(err, ErrSiteNotFound) {
t.Fatalf("expected ErrSiteNotFound, got %v", err)
}
}
@@ -218,3 +218,25 @@ func (s *Service) OnboardSite(ctx context.Context, req OnboardSiteRequest) (*Onb
return s.repo.OnboardSite(ctx, req)
}
func (s *Service) SyncSiteAccounts(ctx context.Context, adminID, siteID uint64) (*SyncSiteAccountsResult, error) {
if err := s.available(); err != nil {
return nil, err
}
return s.repo.SyncSiteAccounts(ctx, adminID, siteID)
}
func (s *Service) ListPublicSiteListings(ctx context.Context, siteCode string, page, pageSize int) (*PublicDistributionListingsResult, error) {
if err := s.available(); err != nil {
return nil, err
}
return s.repo.ListPublicSiteListings(ctx, siteCode, page, pageSize)
}
func (s *Service) GetPublicSiteInfo(ctx context.Context, code, domain string) (*PublicSiteInfoDTO, error) {
if err := s.available(); err != nil {
return nil, err
}
return s.repo.GetPublicSiteInfo(ctx, code, domain)
}