fix(distribution): 货架加价打通前台、下单与卖方分成

分站公开列表和详情展示当前货架加价;下单写入 base_price / shelf_markup / markup_site_id 快照并计入实付。
支付分账改用快照加价,不再误用 PlatformFeeCent;结账不退加价,事后改价不影响历史订单。
This commit is contained in:
yml2213
2026-09-08 17:07:17 +08:00
parent 8521bf4753
commit 96bc6a4db6
16 changed files with 417 additions and 42 deletions
@@ -88,38 +88,38 @@ func parseAdminListQuery(c *gin.Context) (AdminListQuery, bool) {
func parsePublicListQuery(c *gin.Context) PublicListQuery {
query := PublicListQuery{
Page: parsePositiveInt(c.DefaultQuery("page", "1"), 1),
PageSize: parsePositiveInt(c.DefaultQuery("page_size", "20"), 20),
Keyword: strings.TrimSpace(c.Query("keyword")),
Sort: strings.TrimSpace(c.Query("sort")),
Zone: strings.TrimSpace(c.Query("zone")),
Server: parseCSVQuery(c.Query("server")),
Region: parseCSVQuery(c.Query("region")),
LoginMethod: parseCSVQuery(firstNonEmpty(c.Query("login_method"), c.Query("login"))),
Rank: parseCSVQuery(c.Query("rank")),
Insurance: parseCSVQuery(c.Query("insurance")),
Stamina: parseCSVQuery(c.Query("stamina")),
Load: parseCSVQuery(c.Query("load")),
SkinGroup: parseCSVQuery(c.Query("skin_group")),
SkinName: parseCSVQuery(firstNonEmpty(c.Query("skin_name"), c.Query("skin"))),
MinCoin: parseOptionalFloat(c.Query("min_coin")),
MaxCoin: parseOptionalFloat(c.Query("max_coin")),
MinRatio: parseOptionalFloat(c.Query("min_ratio")),
MaxRatio: parseOptionalFloat(c.Query("max_ratio")),
MinPrice: parseOptionalFloat(c.Query("min_price")),
MaxPrice: parseOptionalFloat(c.Query("max_price")),
MinDeposit: parseOptionalFloat(c.Query("min_deposit")),
MaxDeposit: parseOptionalFloat(c.Query("max_deposit")),
MinTotal: parseOptionalFloat(c.Query("min_total")),
MaxTotal: parseOptionalFloat(c.Query("max_total")),
MinFireLevel: parseOptionalFloat(c.Query("min_fire_level")),
MaxFireLevel: parseOptionalFloat(c.Query("max_fire_level")),
MinDailyLoss: parseOptionalFloat(c.Query("min_daily_loss")),
MaxDailyLoss: parseOptionalFloat(c.Query("max_daily_loss")),
Page: parsePositiveInt(c.DefaultQuery("page", "1"), 1),
PageSize: parsePositiveInt(c.DefaultQuery("page_size", "20"), 20),
Keyword: strings.TrimSpace(c.Query("keyword")),
Sort: strings.TrimSpace(c.Query("sort")),
Zone: strings.TrimSpace(c.Query("zone")),
Server: parseCSVQuery(c.Query("server")),
Region: parseCSVQuery(c.Query("region")),
LoginMethod: parseCSVQuery(firstNonEmpty(c.Query("login_method"), c.Query("login"))),
Rank: parseCSVQuery(c.Query("rank")),
Insurance: parseCSVQuery(c.Query("insurance")),
Stamina: parseCSVQuery(c.Query("stamina")),
Load: parseCSVQuery(c.Query("load")),
SkinGroup: parseCSVQuery(c.Query("skin_group")),
SkinName: parseCSVQuery(firstNonEmpty(c.Query("skin_name"), c.Query("skin"))),
MinCoin: parseOptionalFloat(c.Query("min_coin")),
MaxCoin: parseOptionalFloat(c.Query("max_coin")),
MinRatio: parseOptionalFloat(c.Query("min_ratio")),
MaxRatio: parseOptionalFloat(c.Query("max_ratio")),
MinPrice: parseOptionalFloat(c.Query("min_price")),
MaxPrice: parseOptionalFloat(c.Query("max_price")),
MinDeposit: parseOptionalFloat(c.Query("min_deposit")),
MaxDeposit: parseOptionalFloat(c.Query("max_deposit")),
MinTotal: parseOptionalFloat(c.Query("min_total")),
MaxTotal: parseOptionalFloat(c.Query("max_total")),
MinFireLevel: parseOptionalFloat(c.Query("min_fire_level")),
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),
DistributionSiteCode: strings.TrimSpace(firstNonEmpty(c.Query("distribution_site_code"), c.Query("site_code"))),
DistributionSiteCode: strings.TrimSpace(firstNonEmpty(c.GetHeader("X-Distribution-Site-Code"), c.Query("distribution_site_code"), c.Query("site_code"))),
}
if query.PageSize > 50 {
query.PageSize = 50
@@ -3,6 +3,7 @@ package listing
import (
"net/http"
"strconv"
"strings"
filemodule "hfb_sys/backend/internal/modules/file"
"hfb_sys/backend/pkg/response"
@@ -24,7 +25,8 @@ func (h *Handler) FindPublic(c *gin.Context) {
if !ok {
return
}
item, err := h.service.FindPublic(c.Request.Context(), id)
siteCode := strings.TrimSpace(firstNonEmpty(c.GetHeader("X-Distribution-Site-Code"), c.Query("distribution_site_code"), c.Query("site_code")))
item, err := h.service.FindPublic(c.Request.Context(), id, siteCode)
if err != nil {
writeListingError(c, err)
return
@@ -31,6 +31,9 @@ func (r *Repository) ListPublic(ctx context.Context, query PublicListQuery) (*Pu
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)
@@ -228,15 +231,63 @@ func copyPublicZoneCounts(counts map[string]int64) map[string]int64 {
return copied
}
func (r *Repository) FindPublic(ctx context.Context, id uint64) (*ListingDTO, error) {
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)
markups, err := r.shelfMarkupByListingID(ctx, siteCode, []uint64{dto.ID})
if err != nil {
return nil, err
}
dto.PriceCent += markups[dto.ID]
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)
}
markups, err := r.shelfMarkupByListingID(ctx, siteCode, ids)
if err != nil {
return err
}
for i := range items {
items[i].PriceCent += markups[items[i].ID]
}
return nil
}
func (r *Repository) shelfMarkupByListingID(ctx context.Context, siteCode string, listingIDs []uint64) (map[uint64]int64, error) {
result := make(map[uint64]int64, len(listingIDs))
if siteCode == "" || len(listingIDs) == 0 {
return result, nil
}
type markupRow struct {
ListingID uint64 `gorm:"column:listing_id"`
MarkupAmountCent int64 `gorm:"column:markup_amount_cent"`
}
var rows []markupRow
if err := r.db.WithContext(ctx).Table("distribution_site_accounts AS dsa").
Select("dsa.listing_id, COALESCE(dsa.markup_amount_cent, 0) AS markup_amount_cent").
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.MarkupAmountCent > 0 {
result[row.ListingID] = row.MarkupAmountCent
}
}
return result, nil
}
func (r *Repository) FindPublicCoverKey(ctx context.Context, id uint64) (string, error) {
return r.FindPublicScreenshotKey(ctx, id, 0)
}
@@ -18,11 +18,11 @@ func (s *Service) ListMine(ctx context.Context, ownerID uint64, siteCode ...stri
return s.repo.ListMine(ctx, ownerID, siteCode...)
}
func (s *Service) FindPublic(ctx context.Context, id uint64) (*ListingDTO, error) {
func (s *Service) FindPublic(ctx context.Context, id uint64, siteCode string) (*ListingDTO, error) {
if s.repo == nil {
return nil, ErrDependencyUnavailable
}
return s.repo.FindPublic(ctx, id)
return s.repo.FindPublic(ctx, id, siteCode)
}
func (s *Service) FindPublicCoverKey(ctx context.Context, id uint64) (string, error) {
@@ -348,8 +348,6 @@ func TestApplyPublicListingURLsHidesSourceChannel(t *testing.T) {
}
}
func uint64Pointer(value uint64) *uint64 {
return &value
}
@@ -703,7 +701,7 @@ func TestListPublicWithDistributionSiteCode(t *testing.T) {
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"}
dsa := model.DistributionSiteAccount{SiteID: site.ID, AccountID: account1.ID, ListingID: l1.ID, DisplayStatus: "enabled", MarkupAmountCent: 10000}
db.Create(&dsa)
res, err := repo.ListPublic(context.Background(), PublicListQuery{
@@ -715,6 +713,24 @@ func TestListPublicWithDistributionSiteCode(t *testing.T) {
if len(res.Items) != 1 || res.Items[0].ListingNo != "L101" {
t.Fatalf("expected only L101, got %+v", res.Items)
}
if res.Items[0].PriceCent != 11000 {
t.Fatalf("expected public price 11000 with markup, got %d", res.Items[0].PriceCent)
}
detail, err := repo.FindPublic(context.Background(), l1.ID, "site_alpha")
if err != nil {
t.Fatalf("FindPublic err: %v", err)
}
if detail.PriceCent != 11000 {
t.Fatalf("expected detail price 11000 with markup, got %d", detail.PriceCent)
}
mainDetail, err := repo.FindPublic(context.Background(), l1.ID, "")
if err != nil {
t.Fatalf("FindPublic main err: %v", err)
}
if mainDetail.PriceCent != 1000 {
t.Fatalf("main site must keep original price, got %d", mainDetail.PriceCent)
}
allRes, err := repo.ListPublic(context.Background(), PublicListQuery{})
if err != nil {
@@ -946,5 +962,3 @@ func TestListMineSiteIsolation(t *testing.T) {
t.Fatalf("expected 2 listings, got %d", len(allItems))
}
}