- PC端交互:HomeView 排序按钮支持点击切换升降序翻转(比例、哈夫币、AWM数量、合并价格为单个价格项),并带有高低方向箭头(▲/▼)高亮指示 - 移动端交互:MobileHomeView 菜单提供清晰的双向文案(如「比例从高到低 / 比例从低到高」),且点击当前选中项时支持自动翻转方向,顶部工具栏标题紧凑对齐 - 后端排序:public_filter 与 public_query 完整支持 ratioDesc/ratioAsc、priceAsc/priceDesc、coinDesc/coinAsc、awmDesc/awmAsc - 验证方式:新增并运行 TestSortPublicListingsByRatio 包含升序/降序多断言,全量运行 ./scripts/check.sh 门禁全绿通过
323 lines
9.3 KiB
Go
323 lines
9.3 KiB
Go
package listing
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"gorm.io/datatypes"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
func (r *Repository) ListPublic(ctx context.Context, query PublicListQuery) (*PublicListResult, error) {
|
|
page, pageSize := normalizedPublicPage(query)
|
|
if canListPublicWithSQL(query) {
|
|
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 := db.Order("l.published_at DESC, l.id DESC").
|
|
Scan(&rows).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
items := publicListings(rowsToDTO(rows))
|
|
if err := r.applyShelfMarkup(ctx, query.DistributionSiteCode, items); err != nil {
|
|
return nil, err
|
|
}
|
|
baseQuery := query
|
|
baseQuery.Zone = ""
|
|
items = filterPublicListings(items, baseQuery)
|
|
zoneCounts := publicZoneCounts(items)
|
|
if query.Zone != "" && query.Zone != "all" {
|
|
items = filterPublicListings(items, query)
|
|
}
|
|
sortPublicListings(items, query.Sort)
|
|
total := int64(len(items))
|
|
start := (page - 1) * pageSize
|
|
if start < 0 {
|
|
start = 0
|
|
}
|
|
if start >= len(items) {
|
|
items = []ListingDTO{}
|
|
} else {
|
|
end := start + pageSize
|
|
if end > len(items) {
|
|
end = len(items)
|
|
}
|
|
items = items[start:end]
|
|
}
|
|
return &PublicListResult{
|
|
Items: items,
|
|
Total: total,
|
|
Page: page,
|
|
PageSize: pageSize,
|
|
ZoneCounts: zoneCounts,
|
|
}, nil
|
|
}
|
|
|
|
func (r *Repository) listPublicPage(ctx context.Context, query PublicListQuery, page int, pageSize int) (*PublicListResult, error) {
|
|
var total int64
|
|
if err := r.db.WithContext(ctx).Table("rental_listings AS l").
|
|
Where("l.status = ? AND l.review_status = ? AND l.in_transaction = ?", "published", "approved", false).
|
|
Count(&total).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var rows []listingRow
|
|
offset := (page - 1) * pageSize
|
|
err := applyPublicSQLSort(r.baseQuery(ctx), query.Sort).
|
|
Where("l.status = ? AND l.review_status = ? AND l.in_transaction = ?", "published", "approved", false).
|
|
Limit(pageSize).
|
|
Offset(offset).
|
|
Scan(&rows).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
zoneCounts, err := r.publicZoneCountsCached(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &PublicListResult{
|
|
Items: publicListings(rowsToDTO(rows)),
|
|
Total: total,
|
|
Page: page,
|
|
PageSize: pageSize,
|
|
ZoneCounts: zoneCounts,
|
|
}, nil
|
|
}
|
|
|
|
func normalizedPublicPage(query PublicListQuery) (int, int) {
|
|
page := query.Page
|
|
if page <= 0 {
|
|
page = 1
|
|
}
|
|
pageSize := query.PageSize
|
|
if pageSize <= 0 {
|
|
pageSize = 20
|
|
}
|
|
if pageSize > 50 {
|
|
pageSize = 50
|
|
}
|
|
return page, pageSize
|
|
}
|
|
|
|
func canListPublicWithSQL(query PublicListQuery) bool {
|
|
if query.DistributionSiteCode != "" {
|
|
return false
|
|
}
|
|
if query.Keyword != "" {
|
|
return false
|
|
}
|
|
if query.Zone != "" && query.Zone != "all" {
|
|
return false
|
|
}
|
|
if len(query.Server) > 0 || len(query.Region) > 0 || len(query.LoginMethod) > 0 || len(query.Rank) > 0 {
|
|
return false
|
|
}
|
|
if len(query.Insurance) > 0 || len(query.Stamina) > 0 || len(query.Load) > 0 {
|
|
return false
|
|
}
|
|
if len(query.SkinGroup) > 0 || len(query.SkinName) > 0 || len(query.ResourceRanges) > 0 {
|
|
return false
|
|
}
|
|
if query.MinCoin != nil || query.MaxCoin != nil || query.MinRatio != nil || query.MaxRatio != nil {
|
|
return false
|
|
}
|
|
if query.MinPrice != nil || query.MaxPrice != nil {
|
|
return false
|
|
}
|
|
if query.MinDeposit != nil || query.MaxDeposit != nil || query.MinTotal != nil || query.MaxTotal != nil {
|
|
return false
|
|
}
|
|
if query.MinFireLevel != nil || query.MaxFireLevel != nil || query.MinSecretKD != nil || query.MaxSecretKD != nil {
|
|
return false
|
|
}
|
|
if query.MinDailyLoss != nil || query.MaxDailyLoss != nil {
|
|
return false
|
|
}
|
|
switch query.Sort {
|
|
case "", "published", "recommended", "comprehensive", "priceAsc", "priceDesc", "coinDesc", "coinAsc":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func applyPublicSQLSort(db *gorm.DB, sortKey string) *gorm.DB {
|
|
switch sortKey {
|
|
case "priceAsc":
|
|
return db.Order("l.price_cent ASC, l.published_at DESC, l.id DESC")
|
|
case "priceDesc":
|
|
return db.Order("l.price_cent DESC, l.published_at DESC, l.id DESC")
|
|
case "coinDesc":
|
|
return db.Order("a.haf_coin_amount DESC, l.published_at DESC, l.id DESC")
|
|
case "coinAsc":
|
|
return db.Order("a.haf_coin_amount ASC, l.published_at DESC, l.id DESC")
|
|
case "published":
|
|
return db.Order("l.published_at DESC, l.id DESC")
|
|
default:
|
|
return db.Order(fmt.Sprintf("((l.id * 1103515245 + %d) %% 2147483647) ASC, l.id ASC", publicShuffleSeed()))
|
|
}
|
|
}
|
|
|
|
func publicShuffleSeed() int64 {
|
|
now := time.Now().UTC()
|
|
return int64(now.Year()*366 + now.YearDay())
|
|
}
|
|
|
|
func (r *Repository) publicZoneCountsCached(ctx context.Context) (map[string]int64, error) {
|
|
now := time.Now()
|
|
r.publicZoneCountsMu.Lock()
|
|
defer r.publicZoneCountsMu.Unlock()
|
|
if r.publicZoneCounts.Counts != nil && now.Before(r.publicZoneCounts.ExpiresAt) {
|
|
return copyPublicZoneCounts(r.publicZoneCounts.Counts), nil
|
|
}
|
|
|
|
var rows []publicZoneRow
|
|
err := r.db.WithContext(ctx).Table("rental_listings AS l").
|
|
Select("a.login_platform, a.haf_coin_amount, a.asset_summary").
|
|
Joins("JOIN game_accounts AS a ON a.id = l.account_id").
|
|
Where("l.status = ? AND l.review_status = ? AND l.in_transaction = ?", "published", "approved", false).
|
|
Scan(&rows).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
counts := map[string]int64{
|
|
"all": int64(len(rows)),
|
|
"sale": 0,
|
|
"gift": 0,
|
|
"night": 0,
|
|
"password": 0,
|
|
"highCoin": 0,
|
|
}
|
|
for _, row := range rows {
|
|
summary := decodeAssetSummary(row.AssetSummary)
|
|
if isAcceleratedSale(summary) {
|
|
counts["sale"]++
|
|
}
|
|
if hasGiftResourcesSummary(summary) {
|
|
counts["gift"]++
|
|
}
|
|
if isNightAvailableSummary(summary) {
|
|
counts["night"]++
|
|
}
|
|
if strings.Contains(row.LoginPlatform, "账密") || strings.Contains(row.LoginPlatform, "账号密码") {
|
|
counts["password"]++
|
|
}
|
|
if float64(row.HafCoinAmount)/1000000 >= 100 {
|
|
counts["highCoin"]++
|
|
}
|
|
}
|
|
r.publicZoneCounts = publicZoneCountCache{
|
|
Counts: counts,
|
|
ExpiresAt: now.Add(publicZoneCountCacheTTL),
|
|
}
|
|
return copyPublicZoneCounts(counts), nil
|
|
}
|
|
|
|
func copyPublicZoneCounts(counts map[string]int64) map[string]int64 {
|
|
copied := make(map[string]int64, len(counts))
|
|
for key, value := range counts {
|
|
copied[key] = value
|
|
}
|
|
return copied
|
|
}
|
|
|
|
func (r *Repository) FindPublic(ctx context.Context, id uint64, siteCode string) (*ListingDTO, error) {
|
|
dto, err := r.findDTO(ctx, "l.id = ? AND l.status = ? AND l.review_status = ? AND l.in_transaction = ?", id, "published", "approved", false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
applyPublicListingURLs(dto)
|
|
items := []ListingDTO{*dto}
|
|
if err := r.applyShelfMarkup(ctx, siteCode, items); err != nil {
|
|
return nil, err
|
|
}
|
|
*dto = items[0]
|
|
return dto, nil
|
|
}
|
|
|
|
func (r *Repository) applyShelfMarkup(ctx context.Context, siteCode string, items []ListingDTO) error {
|
|
if siteCode == "" || len(items) == 0 {
|
|
return nil
|
|
}
|
|
ids := make([]uint64, 0, len(items))
|
|
for _, item := range items {
|
|
ids = append(ids, item.ID)
|
|
}
|
|
deltas, err := r.shelfRatioDeltaByListingID(ctx, siteCode, ids)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for i := range items {
|
|
delta := deltas[items[i].ID]
|
|
if delta == 0 {
|
|
continue
|
|
}
|
|
items[i].PriceCent, _, _ = ApplyShelfRatio(items[i].HafCoinAmount, items[i].PriceCent, delta)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *Repository) shelfRatioDeltaByListingID(ctx context.Context, siteCode string, listingIDs []uint64) (map[uint64]float64, error) {
|
|
result := make(map[uint64]float64, len(listingIDs))
|
|
if siteCode == "" || len(listingIDs) == 0 {
|
|
return result, nil
|
|
}
|
|
type ratioRow struct {
|
|
ListingID uint64 `gorm:"column:listing_id"`
|
|
ShelfRatioDelta float64 `gorm:"column:shelf_ratio_delta"`
|
|
}
|
|
var rows []ratioRow
|
|
if err := r.db.WithContext(ctx).Table("distribution_site_accounts AS dsa").
|
|
Select("dsa.listing_id, COALESCE(dsa.shelf_ratio_delta, 0) AS shelf_ratio_delta").
|
|
Joins("INNER JOIN distribution_sites AS ds ON ds.id = dsa.site_id AND ds.site_code = ? AND ds.status = 'active'", siteCode).
|
|
Where("dsa.display_status = 'enabled' AND dsa.listing_id IN ?", listingIDs).
|
|
Scan(&rows).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
for _, row := range rows {
|
|
if row.ShelfRatioDelta > 0 {
|
|
result[row.ListingID] = row.ShelfRatioDelta
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (r *Repository) FindPublicCoverKey(ctx context.Context, id uint64) (string, error) {
|
|
return r.FindPublicScreenshotKey(ctx, id, 0)
|
|
}
|
|
|
|
func (r *Repository) FindPublicScreenshotKey(ctx context.Context, id uint64, index int) (string, error) {
|
|
if index < 0 {
|
|
return "", gorm.ErrRecordNotFound
|
|
}
|
|
dto, err := r.findDTO(ctx, "l.id = ? AND l.status = ? AND l.review_status = ? AND l.in_transaction = ?", id, "published", "approved", false)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if index >= len(dto.ScreenshotURLS) {
|
|
return "", gorm.ErrRecordNotFound
|
|
}
|
|
if key := extractListingObjectKey(dto.ScreenshotURLS[index]); key != "" {
|
|
return key, nil
|
|
}
|
|
return "", gorm.ErrRecordNotFound
|
|
}
|
|
|
|
type publicZoneRow struct {
|
|
LoginPlatform string
|
|
HafCoinAmount int64
|
|
AssetSummary datatypes.JSON `gorm:"column:asset_summary"`
|
|
}
|