feat(distribution): 独立分销前台商城多租户体系、货源与订单数据严格隔离
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -175,9 +175,10 @@ type PublicListQuery struct {
|
||||
MaxFireLevel *float64
|
||||
MinDailyLoss *float64
|
||||
MaxDailyLoss *float64
|
||||
MinSecretKD *float64
|
||||
MaxSecretKD *float64
|
||||
ResourceRanges map[string]NumberRange
|
||||
MinSecretKD *float64
|
||||
MaxSecretKD *float64
|
||||
ResourceRanges map[string]NumberRange
|
||||
DistributionSiteCode string
|
||||
}
|
||||
|
||||
type NumberRange struct {
|
||||
|
||||
@@ -116,9 +116,10 @@ func parsePublicListQuery(c *gin.Context) PublicListQuery {
|
||||
MaxFireLevel: parseOptionalFloat(c.Query("max_fire_level")),
|
||||
MinDailyLoss: parseOptionalFloat(c.Query("min_daily_loss")),
|
||||
MaxDailyLoss: parseOptionalFloat(c.Query("max_daily_loss")),
|
||||
MinSecretKD: parseOptionalFloat(c.Query("min_secret_kd")),
|
||||
MaxSecretKD: parseOptionalFloat(c.Query("max_secret_kd")),
|
||||
ResourceRanges: parseResourceRanges(c),
|
||||
MinSecretKD: parseOptionalFloat(c.Query("min_secret_kd")),
|
||||
MaxSecretKD: parseOptionalFloat(c.Query("max_secret_kd")),
|
||||
ResourceRanges: parseResourceRanges(c),
|
||||
DistributionSiteCode: strings.TrimSpace(firstNonEmpty(c.Query("distribution_site_code"), c.Query("site_code"))),
|
||||
}
|
||||
if query.PageSize > 50 {
|
||||
query.PageSize = 50
|
||||
|
||||
@@ -16,10 +16,16 @@ func (r *Repository) ListPublic(ctx context.Context, query PublicListQuery) (*Pu
|
||||
return r.listPublicPage(ctx, query, page, pageSize)
|
||||
}
|
||||
|
||||
db := r.baseQuery(ctx).
|
||||
Where("l.status = ? AND l.review_status = ? AND l.in_transaction = ?", "published", "approved", false)
|
||||
|
||||
if query.DistributionSiteCode != "" {
|
||||
db = db.Joins("INNER JOIN distribution_site_accounts AS dsa ON dsa.listing_id = l.id AND dsa.display_status = 'enabled'").
|
||||
Joins("INNER JOIN distribution_sites AS ds ON ds.id = dsa.site_id AND ds.site_code = ? AND ds.status = 'active' AND ds.display_account_enabled = ?", query.DistributionSiteCode, true)
|
||||
}
|
||||
|
||||
var rows []listingRow
|
||||
err := r.baseQuery(ctx).
|
||||
Where("l.status = ? AND l.review_status = ? AND l.in_transaction = ?", "published", "approved", false).
|
||||
Order("l.published_at DESC, l.id DESC").
|
||||
err := db.Order("l.published_at DESC, l.id DESC").
|
||||
Scan(&rows).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -103,6 +109,9 @@ func normalizedPublicPage(query PublicListQuery) (int, int) {
|
||||
}
|
||||
|
||||
func canListPublicWithSQL(query PublicListQuery) bool {
|
||||
if query.DistributionSiteCode != "" {
|
||||
return false
|
||||
}
|
||||
if query.Keyword != "" {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -746,3 +746,49 @@ func containsString(values []string, target string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestListPublicWithDistributionSiteCode(t *testing.T) {
|
||||
db := database.NewTestDB()
|
||||
if err := database.MigrateListingLifecycleTestSchema(db); err != nil {
|
||||
t.Fatalf("migrate err: %v", err)
|
||||
}
|
||||
if err := db.AutoMigrate(&model.DistributionSite{}, &model.DistributionSiteAccount{}); err != nil {
|
||||
t.Fatalf("migrate distribution err: %v", err)
|
||||
}
|
||||
|
||||
repo := NewRepository(db, nil)
|
||||
|
||||
account1 := model.GameAccount{Title: "号1", GameName: "delta", Status: "active"}
|
||||
account2 := model.GameAccount{Title: "号2", GameName: "delta", Status: "active"}
|
||||
db.Create(&account1)
|
||||
db.Create(&account2)
|
||||
|
||||
l1 := model.RentalListing{ListingNo: "L101", AccountID: account1.ID, Status: "published", ReviewStatus: "approved", PriceCent: 1000}
|
||||
l2 := model.RentalListing{ListingNo: "L102", AccountID: account2.ID, Status: "published", ReviewStatus: "approved", PriceCent: 2000}
|
||||
db.Create(&l1)
|
||||
db.Create(&l2)
|
||||
|
||||
site := model.DistributionSite{SiteCode: "site_alpha", SiteName: "分销A", Domain: "a.com", Status: "active", DisplayAccountEnabled: true}
|
||||
db.Create(&site)
|
||||
|
||||
dsa := model.DistributionSiteAccount{SiteID: site.ID, AccountID: account1.ID, ListingID: l1.ID, DisplayStatus: "enabled"}
|
||||
db.Create(&dsa)
|
||||
|
||||
res, err := repo.ListPublic(context.Background(), PublicListQuery{
|
||||
DistributionSiteCode: "site_alpha",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ListPublic err: %v", err)
|
||||
}
|
||||
if len(res.Items) != 1 || res.Items[0].ListingNo != "L101" {
|
||||
t.Fatalf("expected only L101, got %+v", res.Items)
|
||||
}
|
||||
|
||||
allRes, err := repo.ListPublic(context.Background(), PublicListQuery{})
|
||||
if err != nil {
|
||||
t.Fatalf("ListPublic err: %v", err)
|
||||
}
|
||||
if len(allRes.Items) != 2 {
|
||||
t.Fatalf("expected 2 listings for main site, got %d", len(allRes.Items))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ type OrderDTO struct {
|
||||
Checkout *CheckoutDTO `json:"checkout,omitempty"`
|
||||
AdminActions *AdminActionsDTO `json:"admin_actions,omitempty"`
|
||||
PaymentDeadlineAt *time.Time `json:"payment_deadline_at,omitempty"`
|
||||
DistributionSiteID *uint64 `json:"distribution_site_id,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
@@ -95,6 +96,7 @@ type UserOrderListItemDTO struct {
|
||||
Status string `json:"status"`
|
||||
HandoffStatus string `json:"handoff_status"`
|
||||
PaymentDeadlineAt *time.Time `json:"payment_deadline_at,omitempty"`
|
||||
DistributionSiteID *uint64 `json:"distribution_site_id,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"hfb_sys/backend/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -32,7 +34,8 @@ func (h *Handler) List(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
page, pageSize := parsePagination(c)
|
||||
items, err := h.service.ListForUser(c.Request.Context(), userID, page, pageSize)
|
||||
siteCode := strings.TrimSpace(firstNonEmpty(c.Query("distribution_site_code"), c.Query("site_code")))
|
||||
items, err := h.service.ListForUser(c.Request.Context(), userID, page, pageSize, siteCode)
|
||||
if err != nil {
|
||||
writeOrderError(c, err)
|
||||
return
|
||||
@@ -69,7 +72,8 @@ func (h *Handler) Detail(c *gin.Context) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
item, err := h.service.FindForUser(c.Request.Context(), userID, id)
|
||||
siteCode := strings.TrimSpace(firstNonEmpty(c.Query("distribution_site_code"), c.Query("site_code")))
|
||||
item, err := h.service.FindForUser(c.Request.Context(), userID, id, siteCode)
|
||||
if err != nil {
|
||||
writeOrderError(c, err)
|
||||
return
|
||||
|
||||
@@ -129,6 +129,7 @@ func (row orderRow) toAdminDTO() OrderDTO {
|
||||
DepositHeldAt: row.DepositHeldAt,
|
||||
DepositHoldReleasedAt: row.DepositHoldReleasedAt,
|
||||
AdminActions: adminActionsForOrder(row.RentalOrder),
|
||||
DistributionSiteID: row.DistributionSiteID,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
}
|
||||
@@ -187,6 +188,7 @@ func (row orderRow) toUserListItem(userID uint64, paymentDeadlineAt *time.Time)
|
||||
Status: row.Status,
|
||||
HandoffStatus: row.HandoffStatus,
|
||||
PaymentDeadlineAt: paymentDeadlineAt,
|
||||
DistributionSiteID: row.DistributionSiteID,
|
||||
CreatedAt: row.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,13 +11,25 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func (r *Repository) ListForUser(ctx context.Context, userID uint64, page, pageSize int) (*UserPaginatedResult, error) {
|
||||
func (r *Repository) ListForUser(ctx context.Context, userID uint64, page, pageSize int, siteCode ...string) (*UserPaginatedResult, error) {
|
||||
page, pageSize = normalizePagination(page, pageSize)
|
||||
base := r.baseQuery(ctx).Where("o.renter_id = ? OR o.owner_id = ?", userID, userID)
|
||||
countBase := r.db.WithContext(ctx).Table("rental_orders AS o").Where("o.renter_id = ? OR o.owner_id = ?", userID, userID)
|
||||
|
||||
var code string
|
||||
if len(siteCode) > 0 {
|
||||
code = strings.TrimSpace(siteCode[0])
|
||||
}
|
||||
if code != "" {
|
||||
base = base.Joins("INNER JOIN distribution_sites AS ds ON ds.id = o.distribution_site_id AND ds.site_code = ?", code)
|
||||
countBase = countBase.Joins("INNER JOIN distribution_sites AS ds ON ds.id = o.distribution_site_id AND ds.site_code = ?", code)
|
||||
} else {
|
||||
base = base.Where("o.distribution_site_id IS NULL OR o.distribution_site_id = 0")
|
||||
countBase = countBase.Where("o.distribution_site_id IS NULL OR o.distribution_site_id = 0")
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := r.db.WithContext(ctx).Table("rental_orders AS o").
|
||||
Where("o.renter_id = ? OR o.owner_id = ?", userID, userID).
|
||||
Count(&total).Error; err != nil {
|
||||
if err := countBase.Count(&total).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var rows []orderRow
|
||||
@@ -308,12 +320,22 @@ func (r *Repository) FindLatestAdminByListing(ctx context.Context, listingID uin
|
||||
return &dto, nil
|
||||
}
|
||||
|
||||
func (r *Repository) FindForUser(ctx context.Context, userID uint64, orderID uint64) (*OrderDTO, error) {
|
||||
func (r *Repository) FindForUser(ctx context.Context, userID uint64, orderID uint64, siteCode ...string) (*OrderDTO, error) {
|
||||
var row orderRow
|
||||
db := r.db.WithContext(ctx)
|
||||
if err := r.baseQuery(ctx).
|
||||
Where("o.id = ? AND (o.renter_id = ? OR o.owner_id = ?)", orderID, userID, userID).
|
||||
First(&row).Error; err != nil {
|
||||
query := r.baseQuery(ctx).Where("o.id = ? AND (o.renter_id = ? OR o.owner_id = ?)", orderID, userID, userID)
|
||||
|
||||
var code string
|
||||
if len(siteCode) > 0 {
|
||||
code = strings.TrimSpace(siteCode[0])
|
||||
}
|
||||
if code != "" {
|
||||
query = query.Joins("INNER JOIN distribution_sites AS ds ON ds.id = o.distribution_site_id AND ds.site_code = ?", code)
|
||||
} else if len(siteCode) > 0 {
|
||||
query = query.Where("o.distribution_site_id IS NULL OR o.distribution_site_id = 0")
|
||||
}
|
||||
|
||||
if err := query.First(&row).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dto := row.toDTOForUser(userID)
|
||||
|
||||
@@ -1884,3 +1884,91 @@ func TestAdminPlatformCheckoutConfirmRequiresExplicitConfirmationForZeroOwnerInc
|
||||
t.Fatalf("确认零收入后订单状态 = %s, want %s", completed.Status, orderStatusCompleted)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListForUserAndFindForUserSiteIsolation(t *testing.T) {
|
||||
db := setupOrderTestDB(t)
|
||||
if err := db.AutoMigrate(&model.DistributionSite{}); err != nil {
|
||||
t.Fatalf("migrate distribution_sites err: %v", err)
|
||||
}
|
||||
repo := NewRepository(db)
|
||||
|
||||
owner := model.User{Phone: "13800000010"}
|
||||
db.Create(&owner)
|
||||
renter := model.User{Phone: "13800000020"}
|
||||
db.Create(&renter)
|
||||
|
||||
account := model.GameAccount{OwnerID: owner.ID, Title: "隔离测试账号", Status: "active"}
|
||||
db.Create(&account)
|
||||
listing := model.RentalListing{ListingNo: "L30001", AccountID: account.ID, OwnerID: owner.ID, Status: "published", ReviewStatus: "approved", PriceCent: 1000}
|
||||
db.Create(&listing)
|
||||
|
||||
site := model.DistributionSite{
|
||||
SiteCode: "site_isolate_1",
|
||||
SiteName: "隔离分销站",
|
||||
Domain: "isolate.com",
|
||||
Status: "active",
|
||||
DisplayAccountEnabled: true,
|
||||
}
|
||||
db.Create(&site)
|
||||
|
||||
// Order 1: on main site (distribution_site_id is nil)
|
||||
mainOrder := model.RentalOrder{
|
||||
OrderNo: "RO_MAIN_001",
|
||||
ListingID: listing.ID,
|
||||
AccountID: account.ID,
|
||||
OwnerID: owner.ID,
|
||||
RenterID: renter.ID,
|
||||
Status: orderStatusCompleted,
|
||||
}
|
||||
db.Create(&mainOrder)
|
||||
|
||||
// Order 2: on distribution site (distribution_site_id = site.ID)
|
||||
distOrder := model.RentalOrder{
|
||||
OrderNo: "RO_DIST_001",
|
||||
ListingID: listing.ID,
|
||||
AccountID: account.ID,
|
||||
OwnerID: owner.ID,
|
||||
RenterID: renter.ID,
|
||||
Status: orderStatusCompleted,
|
||||
DistributionSiteID: &site.ID,
|
||||
}
|
||||
db.Create(&distOrder)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// 1. Query with site_code = "site_isolate_1": should return ONLY distOrder
|
||||
distList, err := repo.ListForUser(ctx, renter.ID, 1, 20, "site_isolate_1")
|
||||
if err != nil {
|
||||
t.Fatalf("ListForUser distribution err: %v", err)
|
||||
}
|
||||
if len(distList.Items) != 1 || distList.Items[0].OrderNo != "RO_DIST_001" {
|
||||
t.Fatalf("expected only RO_DIST_001, got %+v", distList.Items)
|
||||
}
|
||||
|
||||
// 2. Query without siteCode (main site mode): should return ONLY mainOrder
|
||||
mainList, err := repo.ListForUser(ctx, renter.ID, 1, 20, "")
|
||||
if err != nil {
|
||||
t.Fatalf("ListForUser main err: %v", err)
|
||||
}
|
||||
if len(mainList.Items) != 1 || mainList.Items[0].OrderNo != "RO_MAIN_001" {
|
||||
t.Fatalf("expected only RO_MAIN_001, got %+v", mainList.Items)
|
||||
}
|
||||
|
||||
// 3. FindForUser: distOrder found under "site_isolate_1"
|
||||
dItem, err := repo.FindForUser(ctx, renter.ID, distOrder.ID, "site_isolate_1")
|
||||
if err != nil || dItem.OrderNo != "RO_DIST_001" {
|
||||
t.Fatalf("FindForUser distOrder failed: %v", err)
|
||||
}
|
||||
|
||||
// 4. FindForUser: mainOrder CANNOT be accessed under "site_isolate_1"
|
||||
_, err = repo.FindForUser(ctx, renter.ID, mainOrder.ID, "site_isolate_1")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error accessing mainOrder under distribution site, got nil")
|
||||
}
|
||||
|
||||
// 5. FindForUser: distOrder CANNOT be accessed under main site mode ("")
|
||||
_, err = repo.FindForUser(ctx, renter.ID, distOrder.ID, "")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error accessing distOrder under main site mode, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,11 +148,11 @@ func (s *Service) AcceptCheckout(ctx context.Context, userID uint64, orderID uin
|
||||
return s.repo.AcceptCheckout(ctx, userID, orderID)
|
||||
}
|
||||
|
||||
func (s *Service) ListForUser(ctx context.Context, userID uint64, page, pageSize int) (*UserPaginatedResult, error) {
|
||||
func (s *Service) ListForUser(ctx context.Context, userID uint64, page, pageSize int, siteCode ...string) (*UserPaginatedResult, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
return s.repo.ListForUser(ctx, userID, page, pageSize)
|
||||
return s.repo.ListForUser(ctx, userID, page, pageSize, siteCode...)
|
||||
}
|
||||
|
||||
func (s *Service) ListSellerHandoffs(ctx context.Context, userID uint64, query SellerHandoffQuery) (*SellerHandoffPaginatedResult, error) {
|
||||
@@ -377,9 +377,9 @@ func (s *Service) PendingRefundCount(ctx context.Context) (int64, error) {
|
||||
return s.repo.PendingRefundCount(ctx)
|
||||
}
|
||||
|
||||
func (s *Service) FindForUser(ctx context.Context, userID uint64, orderID uint64) (*OrderDTO, error) {
|
||||
func (s *Service) FindForUser(ctx context.Context, userID uint64, orderID uint64, siteCode ...string) (*OrderDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
return s.repo.FindForUser(ctx, userID, orderID)
|
||||
return s.repo.FindForUser(ctx, userID, orderID, siteCode...)
|
||||
}
|
||||
|
||||
@@ -414,6 +414,8 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
|
||||
api.GET("/home-announcements", systemConfigHandler.HomeAnnouncements)
|
||||
api.GET("/mobile-home-config", systemConfigHandler.HomeConfig)
|
||||
api.GET("/public/files/object", fileHandler.PublicObject)
|
||||
api.GET("/distribution/sites/:site_code/listings", distributionHandler.ListPublicSiteListings)
|
||||
api.GET("/distribution/site-info", distributionHandler.GetPublicSiteInfo)
|
||||
api.POST("/payments/leshua/notify", paymentHandler.LeshuaNotify)
|
||||
api.POST("/payments/lakala/notify", paymentHandler.LakalaNotify)
|
||||
api.POST("/payments/shuncheng/notify", paymentHandler.ShunchengNotify)
|
||||
@@ -602,6 +604,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
|
||||
{
|
||||
distributionProtectedRoutes.GET("/auth/me", distributionHandler.Me)
|
||||
distributionProtectedRoutes.POST("/auth/logout", distributionHandler.Logout)
|
||||
distributionProtectedRoutes.POST("/sites/:id/sync-accounts", distributionHandler.SyncSiteAccounts)
|
||||
}
|
||||
|
||||
adminRoutes := api.Group("/admin", requireAdmin, middleware.RequireAdminPasswordChanged())
|
||||
@@ -617,6 +620,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
|
||||
adminRoutes.GET("/distribution/sites/:id", requirePerm("distribution:site_view"), distributionHandler.GetSite)
|
||||
adminRoutes.POST("/distribution/sites", requirePerm("distribution:site_manage"), distributionHandler.CreateSite)
|
||||
adminRoutes.POST("/distribution/sites/onboard", requirePerm("distribution:site_manage"), distributionHandler.OnboardSite)
|
||||
adminRoutes.POST("/distribution/sites/:id/sync-accounts", requirePerm("distribution:account_display"), distributionHandler.SyncSiteAccounts)
|
||||
adminRoutes.PUT("/distribution/sites/:id", requirePerm("distribution:site_manage"), distributionHandler.UpdateSite)
|
||||
adminRoutes.GET("/distribution/rules", requirePerm("distribution:rule_view"), distributionHandler.ListRules)
|
||||
adminRoutes.POST("/distribution/rules", requirePerm("distribution:rule_manage"), distributionHandler.CreateRule)
|
||||
|
||||
Reference in New Issue
Block a user