fix(distribution): 修复收号站订单详情权限、专属支付通道路由、大盘统计与仲裁积分隔离
- 修复 FindOrder 在收号站模式(two_party_distributor_receive / three_party)下的 403 权限判定,放行参与方站点成员并精准计算收益 - 串联 distribution_site_payment_channels 至 payment.Start 核心支付发起流程,分销订单优先使用绑定的专属商户通道 - 修复数据大盘 GetDashboardAnalytics 趋势中 ReceivedCount 恒为 0 的问题,并对齐今日订单与 GMV 统计 - 修复 dispute/arbitration.go 仲裁完成时向分销租客发放成长积分的泄漏风险 - 提现管理新增 distribution_site_name/distribution_site_code 字段并在后台列表与详情弹窗中区分展示分销站 - GetPublicSiteInfo 增加 X-Distribution-Site-Code 请求头兜底支持
This commit is contained in:
@@ -2,12 +2,15 @@ package distribution
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
"hfb_sys/backend/internal/modules/auth"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func TestOnboardSiteAndLoginFlow(t *testing.T) {
|
||||
@@ -176,3 +179,32 @@ func TestOnboardSiteAndLoginFlow(t *testing.T) {
|
||||
t.Fatalf("unexpected profile: %#v", profile)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPublicSiteInfoHeaderFallback(t *testing.T) {
|
||||
db := distributionTestDB(t)
|
||||
repo := NewRepository(db)
|
||||
svc := NewService(repo, nil)
|
||||
handler := NewHandler(svc)
|
||||
|
||||
site := model.DistributionSite{
|
||||
SiteCode: "header_site",
|
||||
SiteName: "头部测试站",
|
||||
Domain: "header.example.com",
|
||||
Status: "active",
|
||||
}
|
||||
if err := db.Create(&site).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
req, _ := http.NewRequest("GET", "/api/v1/distribution/public/site-info", nil)
|
||||
req.Header.Set("X-Distribution-Site-Code", "header_site")
|
||||
c.Request = req
|
||||
|
||||
handler.GetPublicSiteInfo(c)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d, body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,6 +308,9 @@ func (h *Handler) GetPublicSiteInfo(c *gin.Context) {
|
||||
if code == "" {
|
||||
code = strings.TrimSpace(c.Query("site_code"))
|
||||
}
|
||||
if code == "" {
|
||||
code = strings.TrimSpace(c.GetHeader("X-Distribution-Site-Code"))
|
||||
}
|
||||
domain := strings.TrimSpace(c.Query("domain"))
|
||||
if code == "" && domain == "" {
|
||||
host := c.Request.Host
|
||||
|
||||
@@ -662,40 +662,101 @@ func (r *Repository) FindOrder(ctx context.Context, adminID, id uint64) (*Distri
|
||||
if item.OrderID == 0 {
|
||||
return nil, ErrOrderNotFound
|
||||
}
|
||||
if err := r.ensureSiteAccess(ctx, adminID, item.SiteID); err != nil {
|
||||
var parties []model.DistributionOrderParty
|
||||
if err := r.db.WithContext(ctx).Where("order_id = ?", item.OrderID).Find(&parties).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var parties []model.DistributionOrderParty
|
||||
if err := r.db.WithContext(ctx).Where("order_id = ? AND (site_id = ? OR party_id = ?)", item.OrderID, item.SiteID, item.SiteID).Find(&parties).Error; err == nil {
|
||||
var totalEarning int64
|
||||
var markupCent int64
|
||||
var splitCent int64
|
||||
var roles []string
|
||||
for _, p := range parties {
|
||||
if p.PartyType != PartyTypeSite {
|
||||
continue
|
||||
var siteSet map[uint64]struct{}
|
||||
if adminID != 0 {
|
||||
global, err := r.hasGlobalDistributionAccessWithDB(ctx, r.db, adminID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !global {
|
||||
var adminSiteIDs []uint64
|
||||
if err := r.db.WithContext(ctx).Model(&model.DistributionSiteMember{}).
|
||||
Where("admin_user_id = ? AND status = 'active'", adminID).
|
||||
Pluck("site_id", &adminSiteIDs).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
totalEarning += p.IncomeAmountCent
|
||||
if p.PartyRole == "distributor_sell" || p.PartyRole == "distributor_b" {
|
||||
roles = append(roles, "sell")
|
||||
// 退款压缩加价后收入可能小于货架加价:加价部分取两者较小值,
|
||||
// 避免展示负的提价分成。
|
||||
markup := item.ShelfMarkupCent
|
||||
if p.IncomeAmountCent < markup {
|
||||
markup = p.IncomeAmountCent
|
||||
siteSet = make(map[uint64]struct{}, len(adminSiteIDs))
|
||||
for _, sid := range adminSiteIDs {
|
||||
siteSet[sid] = struct{}{}
|
||||
}
|
||||
hasAccess := false
|
||||
if item.SiteID > 0 {
|
||||
if _, ok := siteSet[item.SiteID]; ok {
|
||||
hasAccess = true
|
||||
}
|
||||
markupCent += markup
|
||||
splitCent += p.IncomeAmountCent - markup
|
||||
} else if p.PartyRole == "distributor_receive" || p.PartyRole == "distributor_a" {
|
||||
roles = append(roles, "receive")
|
||||
splitCent += p.IncomeAmountCent
|
||||
}
|
||||
if !hasAccess {
|
||||
for _, p := range parties {
|
||||
if p.PartyType == PartyTypeSite {
|
||||
if p.SiteID > 0 {
|
||||
if _, ok := siteSet[p.SiteID]; ok {
|
||||
hasAccess = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if p.PartyID > 0 {
|
||||
if _, ok := siteSet[p.PartyID]; ok {
|
||||
hasAccess = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !hasAccess {
|
||||
return nil, ErrSiteAccessDenied
|
||||
}
|
||||
}
|
||||
item.DistributorEarningCent = totalEarning
|
||||
item.DistributorMarkupCent = markupCent
|
||||
item.DistributorSplitCent = splitCent
|
||||
item.DistributorRole = strings.Join(roles, ",")
|
||||
}
|
||||
|
||||
var totalEarning int64
|
||||
var markupCent int64
|
||||
var splitCent int64
|
||||
var roles []string
|
||||
for _, p := range parties {
|
||||
if p.PartyType != PartyTypeSite {
|
||||
continue
|
||||
}
|
||||
if len(siteSet) > 0 {
|
||||
match := false
|
||||
if p.SiteID > 0 {
|
||||
if _, ok := siteSet[p.SiteID]; ok {
|
||||
match = true
|
||||
}
|
||||
}
|
||||
if p.PartyID > 0 {
|
||||
if _, ok := siteSet[p.PartyID]; ok {
|
||||
match = true
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
continue
|
||||
}
|
||||
}
|
||||
totalEarning += p.IncomeAmountCent
|
||||
if p.PartyRole == "distributor_sell" || p.PartyRole == "distributor_b" {
|
||||
roles = append(roles, "sell")
|
||||
// 退款压缩加价后收入可能小于货架加价:加价部分取两者较小值,
|
||||
// 避免展示负的提价分成。
|
||||
markup := item.ShelfMarkupCent
|
||||
if p.IncomeAmountCent < markup {
|
||||
markup = p.IncomeAmountCent
|
||||
}
|
||||
markupCent += markup
|
||||
splitCent += p.IncomeAmountCent - markup
|
||||
} else if p.PartyRole == "distributor_receive" || p.PartyRole == "distributor_a" {
|
||||
roles = append(roles, "receive")
|
||||
splitCent += p.IncomeAmountCent
|
||||
}
|
||||
}
|
||||
item.DistributorEarningCent = totalEarning
|
||||
item.DistributorMarkupCent = markupCent
|
||||
item.DistributorSplitCent = splitCent
|
||||
item.DistributorRole = strings.Join(roles, ",")
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
@@ -2350,13 +2411,14 @@ func (r *Repository) GetDashboardAnalytics(ctx context.Context, adminID, siteID
|
||||
Where("distribution_site_id = ? AND status = 'published' AND review_status = 'approved'", siteID).Count(&activeListings).Error
|
||||
|
||||
var todayOrders int64
|
||||
_ = r.db.WithContext(ctx).Model(&model.RentalOrder{}).
|
||||
Where("distribution_site_id = ? AND created_at >= ?", siteID, todayStr+" 00:00:00").Count(&todayOrders).Error
|
||||
_ = r.db.WithContext(ctx).Table("rental_orders ro").
|
||||
Where("(ro.distribution_site_id = ? OR EXISTS (SELECT 1 FROM distribution_order_parties p WHERE p.order_id = ro.id AND (p.site_id = ? OR p.party_id = ?))) AND ro.created_at >= ?", siteID, siteID, siteID, todayStr+" 00:00:00").
|
||||
Count(&todayOrders).Error
|
||||
|
||||
var todayGMV int64
|
||||
_ = r.db.WithContext(ctx).Table("rental_orders ro").
|
||||
Joins("JOIN distribution_order_financials f ON f.order_id = ro.id").
|
||||
Where("ro.distribution_site_id = ? AND ro.created_at >= ?", siteID, todayStr+" 00:00:00").
|
||||
Where("(ro.distribution_site_id = ? OR EXISTS (SELECT 1 FROM distribution_order_parties p WHERE p.order_id = ro.id AND (p.site_id = ? OR p.party_id = ?))) AND ro.created_at >= ?", siteID, siteID, siteID, todayStr+" 00:00:00").
|
||||
Select("COALESCE(SUM(f.gross_paid_amount_cent), 0)").Scan(&todayGMV).Error
|
||||
|
||||
var todayProfit int64
|
||||
@@ -2374,11 +2436,9 @@ func (r *Repository) GetDashboardAnalytics(ctx context.Context, adminID, siteID
|
||||
|
||||
trendDays := make([]DistributionTrendPoint, 0, 7)
|
||||
type trendRow struct {
|
||||
DateStr string `gorm:"column:date_str"`
|
||||
Received int64 `gorm:"column:received"`
|
||||
Orders int64 `gorm:"column:orders"`
|
||||
GMV int64 `gorm:"column:gmv"`
|
||||
Profit int64 `gorm:"column:profit"`
|
||||
DateStr string `gorm:"column:date_str"`
|
||||
Orders int64 `gorm:"column:orders"`
|
||||
GMV int64 `gorm:"column:gmv"`
|
||||
}
|
||||
var tRows []trendRow
|
||||
startDate := time.Now().AddDate(0, 0, -6).Format("2006-01-02")
|
||||
@@ -2393,6 +2453,22 @@ func (r *Repository) GetDashboardAnalytics(ctx context.Context, adminID, siteID
|
||||
Order("date_str ASC").
|
||||
Scan(&tRows).Error
|
||||
|
||||
type receivedRow struct {
|
||||
DateStr string `gorm:"column:date_str"`
|
||||
Received int64 `gorm:"column:received"`
|
||||
}
|
||||
var rRows []receivedRow
|
||||
dateFormatListingExpr := "DATE_FORMAT(created_at, '%Y-%m-%d')"
|
||||
_ = r.db.WithContext(ctx).Model(&model.RentalListing{}).
|
||||
Where("distribution_site_id = ? AND created_at >= ?", siteID, startDate+" 00:00:00").
|
||||
Select(fmt.Sprintf("%s AS date_str, COUNT(id) AS received", dateFormatListingExpr)).
|
||||
Group(dateFormatListingExpr).
|
||||
Scan(&rRows).Error
|
||||
rRowMap := make(map[string]int64, len(rRows))
|
||||
for _, rr := range rRows {
|
||||
rRowMap[rr.DateStr] = rr.Received
|
||||
}
|
||||
|
||||
type profitRow struct {
|
||||
DateStr string `gorm:"column:date_str"`
|
||||
Profit int64 `gorm:"column:profit"`
|
||||
@@ -2417,7 +2493,7 @@ func (r *Repository) GetDashboardAnalytics(ctx context.Context, adminID, siteID
|
||||
tr := tRowMap[dStr]
|
||||
trendDays = append(trendDays, DistributionTrendPoint{
|
||||
Date: dStr,
|
||||
ReceivedCount: tr.Received,
|
||||
ReceivedCount: rRowMap[dStr],
|
||||
OrderCount: tr.Orders,
|
||||
GMVCent: tr.GMV,
|
||||
ProfitCent: pRowMap[dStr],
|
||||
|
||||
@@ -74,6 +74,31 @@ func TestDistributionOrderSnapshotAndQueries(t *testing.T) {
|
||||
if _, err := repo.FindOrder(t.Context(), 0, order.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// 验证分销站成员管理员可查看本站参与的订单
|
||||
adminUser := model.AdminUser{Username: "dist_admin", Status: "active"}
|
||||
if err := db.Create(&adminUser).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Create(&model.DistributionSiteMember{SiteID: site.ID, AdminUserID: adminUser.ID, MemberType: "site_operator", Status: "active"}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
detail, err := repo.FindOrder(t.Context(), adminUser.ID, order.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("FindOrder by site member failed: %v", err)
|
||||
}
|
||||
if detail.DistributorEarningCent != 1000 || detail.DistributorRole != "receive" {
|
||||
t.Fatalf("unexpected detail for site member: %+v", detail)
|
||||
}
|
||||
|
||||
// 验证未关联站点的管理员无法查看
|
||||
otherAdmin := model.AdminUser{Username: "other_admin", Status: "active"}
|
||||
if err := db.Create(&otherAdmin).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := repo.FindOrder(t.Context(), otherAdmin.ID, order.ID); err != ErrSiteAccessDenied {
|
||||
t.Fatalf("expected ErrSiteAccessDenied, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDistributionPaymentChannelAndSettlementBatch(t *testing.T) {
|
||||
@@ -862,6 +887,9 @@ func TestDistributionReceivedListingsAndWallet(t *testing.T) {
|
||||
if analytics.TotalReceivedCount != 1 || analytics.AvailableBalanceCent != 2500 {
|
||||
t.Fatalf("unexpected analytics: %+v", analytics)
|
||||
}
|
||||
if len(analytics.TrendDays) == 0 || analytics.TrendDays[len(analytics.TrendDays)-1].ReceivedCount != 1 {
|
||||
t.Fatalf("expected trend today ReceivedCount=1, got: %+v", analytics.TrendDays)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlatformSellSettlementCreditsOnlySiteShare(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user