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:
@@ -16,11 +16,7 @@ import (
|
||||
// Start 发起订单支付,并按请求支付方式选择对应默认渠道配置。
|
||||
func (r *Repository) Start(ctx context.Context, userID uint64, orderID uint64, req StartPaymentRequest, clientIP string) (*PaymentDTO, error) {
|
||||
req.PayWay = normalizePayWay(req.PayWay)
|
||||
defaultConfig, err := r.defaultRuntimeConfig(ctx, req.PayWay)
|
||||
if err != nil {
|
||||
return nil, ErrPaymentUnavailable
|
||||
}
|
||||
payment, orderRow, err := r.preparePayment(ctx, userID, orderID, req, *defaultConfig)
|
||||
payment, orderRow, err := r.preparePayment(ctx, userID, orderID, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -122,7 +118,7 @@ func (r *Repository) Start(ctx context.Context, userID uint64, orderID uint64, r
|
||||
}
|
||||
|
||||
// preparePayment 创建或复用订单支付单,并写入本次命中的支付配置 ID。
|
||||
func (r *Repository) preparePayment(ctx context.Context, userID uint64, orderID uint64, req StartPaymentRequest, runtimeConfig runtimePaymentConfig) (*model.PaymentOrder, *model.RentalOrder, error) {
|
||||
func (r *Repository) preparePayment(ctx context.Context, userID uint64, orderID uint64, req StartPaymentRequest) (*model.PaymentOrder, *model.RentalOrder, error) {
|
||||
var paymentID uint64
|
||||
var orderRow model.RentalOrder
|
||||
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
@@ -139,13 +135,17 @@ func (r *Repository) preparePayment(ctx context.Context, userID uint64, orderID
|
||||
if amountCent <= 0 {
|
||||
return ErrPaymentCannotStart
|
||||
}
|
||||
runtimeConfig, err := r.resolveOrderRuntimeConfig(ctx, tx, row.DistributionSiteID, req.PayWay)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var existing model.PaymentOrder
|
||||
err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
err = tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("order_id = ? AND biz_type = ?", row.ID, "order_pay").
|
||||
Order("id DESC").
|
||||
First(&existing).Error
|
||||
if err == nil {
|
||||
if canReuseOrderPayment(existing, runtimeConfig) {
|
||||
if canReuseOrderPayment(existing, *runtimeConfig) {
|
||||
existing.PayWay = firstNonEmpty(req.PayWay, existing.PayWay, runtimeConfig.PayWay, "ZFBZF")
|
||||
existing.JSPayFlag = firstNonEmpty(req.JSPayFlag, existing.JSPayFlag, runtimeConfig.JSPayFlag, "2")
|
||||
existing.AmountCent = amountCent
|
||||
@@ -165,7 +165,7 @@ func (r *Repository) preparePayment(ctx context.Context, userID uint64, orderID
|
||||
} else if err != gorm.ErrRecordNotFound {
|
||||
return err
|
||||
}
|
||||
payment, err := newOrderPayment(row, amountCent, req, runtimeConfig)
|
||||
payment, err := newOrderPayment(row, amountCent, req, *runtimeConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -161,6 +161,30 @@ func (c runtimePaymentConfig) isMockMode() bool {
|
||||
return c.Provider == "mock"
|
||||
}
|
||||
|
||||
// resolveOrderRuntimeConfig 根据订单所属分销站点配置或全局默认配置解析运行时支付配置。
|
||||
func (r *Repository) resolveOrderRuntimeConfig(ctx context.Context, tx *gorm.DB, siteID *uint64, payWay string) (*runtimePaymentConfig, error) {
|
||||
if siteID != nil && *siteID > 0 {
|
||||
var configID uint64
|
||||
queryDB := tx
|
||||
if queryDB == nil {
|
||||
queryDB = r.db
|
||||
}
|
||||
err := queryDB.WithContext(ctx).Table("distribution_site_payment_channels dspc").
|
||||
Joins("JOIN payment_merchant_configs pmc ON pmc.id = dspc.config_id").
|
||||
Where("dspc.site_id = ? AND dspc.status = 'active' AND pmc.status = 'active' AND (pmc.pay_way = ? OR pmc.pay_way = '')", *siteID, payWay).
|
||||
Order("dspc.priority ASC, dspc.id ASC").
|
||||
Limit(1).
|
||||
Pluck("pmc.id", &configID).Error
|
||||
if err == nil && configID > 0 && r.configRepo != nil {
|
||||
dto, err := r.configRepo.FindRuntimeByID(ctx, configID, true)
|
||||
if err == nil {
|
||||
return runtimeConfigFromDTO(dto), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return r.defaultRuntimeConfig(ctx, payWay)
|
||||
}
|
||||
|
||||
// defaultRuntimeConfig 按支付方式获取默认启用配置,用于新支付单创建。
|
||||
func (r *Repository) defaultRuntimeConfig(ctx context.Context, payWay string) (*runtimePaymentConfig, error) {
|
||||
if r.configRepo == nil {
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"hfb_sys/backend/internal/database"
|
||||
"hfb_sys/backend/internal/model"
|
||||
ordermodule "hfb_sys/backend/internal/modules/order"
|
||||
"hfb_sys/backend/internal/modules/paymentconfig"
|
||||
"hfb_sys/backend/internal/modules/rentergrowth"
|
||||
|
||||
"gorm.io/gorm"
|
||||
@@ -824,3 +825,106 @@ func TestApplyChannelStatusKeepsTerminalPayment(t *testing.T) {
|
||||
t.Fatalf("status = %q, want closed", latest.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartPaymentRoutesToDistributionSitePaymentChannel(t *testing.T) {
|
||||
db := setupPaymentTestDB(t)
|
||||
if err := db.AutoMigrate(
|
||||
&model.DistributionSite{},
|
||||
&model.DistributionSitePaymentChannel{},
|
||||
&model.PaymentMerchantConfig{},
|
||||
); err != nil {
|
||||
t.Fatalf("migrate distribution payment schema failed: %v", err)
|
||||
}
|
||||
|
||||
configRepo := paymentconfig.NewRepository(db, &paymentconfig.MockEncryptor{})
|
||||
orderRepo := ordermodule.NewRepository(db)
|
||||
repo := NewRepository(db, configRepo, orderRepo)
|
||||
|
||||
defaultConfig := model.PaymentMerchantConfig{
|
||||
Name: "平台默认微信",
|
||||
Provider: "mock",
|
||||
MerchantID: "PLATFORM_MOCK",
|
||||
PayWay: "WX",
|
||||
IsDefault: true,
|
||||
Status: "active",
|
||||
Environment: "sandbox",
|
||||
}
|
||||
if err := db.Create(&defaultConfig).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
siteConfig := model.PaymentMerchantConfig{
|
||||
Name: "分销站专属微信",
|
||||
Provider: "mock",
|
||||
MerchantID: "SITE_MOCK_01",
|
||||
PayWay: "WX",
|
||||
IsDefault: false,
|
||||
Status: "active",
|
||||
Environment: "sandbox",
|
||||
}
|
||||
if err := db.Create(&siteConfig).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
site := model.DistributionSite{SiteCode: "pay_site", SiteName: "支付分站", Domain: "pay.example.com", Status: "active"}
|
||||
if err := db.Create(&site).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
channel := model.DistributionSitePaymentChannel{
|
||||
SiteID: site.ID,
|
||||
ConfigID: siteConfig.ID,
|
||||
Priority: 1,
|
||||
Status: "active",
|
||||
}
|
||||
if err := db.Create(&channel).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Order A: Belongs to distribution site
|
||||
fixA := createPayableOrderFixture(t, db, "siteA", false)
|
||||
fixA.Order.DistributionSiteID = &site.ID
|
||||
if err := db.Save(&fixA.Order).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Order B: Normal platform order
|
||||
fixB := createPayableOrderFixture(t, db, "siteB", false)
|
||||
fixB.Order.DistributionSiteID = nil
|
||||
if err := db.Save(&fixB.Order).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Start payment for Order A -> should route to siteConfig
|
||||
dtoA, err := repo.Start(t.Context(), fixA.Renter.ID, fixA.Order.ID, StartPaymentRequest{PayWay: "WX"}, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("Start Order A failed: %v", err)
|
||||
}
|
||||
if dtoA.PaymentConfigID != siteConfig.ID {
|
||||
t.Fatalf("Order A expected PaymentConfigID=%d, got %d", siteConfig.ID, dtoA.PaymentConfigID)
|
||||
}
|
||||
var savedA model.PaymentOrder
|
||||
if err := db.First(&savedA, dtoA.ID).Error; err != nil {
|
||||
t.Fatalf("find saved payment A failed: %v", err)
|
||||
}
|
||||
if savedA.MerchantID != siteConfig.MerchantID {
|
||||
t.Fatalf("Order A expected MerchantID=%s, got %s", siteConfig.MerchantID, savedA.MerchantID)
|
||||
}
|
||||
|
||||
// Start payment for Order B -> should route to defaultConfig
|
||||
dtoB, err := repo.Start(t.Context(), fixB.Renter.ID, fixB.Order.ID, StartPaymentRequest{PayWay: "WX"}, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("Start Order B failed: %v", err)
|
||||
}
|
||||
if dtoB.PaymentConfigID != defaultConfig.ID {
|
||||
t.Fatalf("Order B expected PaymentConfigID=%d, got %d", defaultConfig.ID, dtoB.PaymentConfigID)
|
||||
}
|
||||
var savedB model.PaymentOrder
|
||||
if err := db.First(&savedB, dtoB.ID).Error; err != nil {
|
||||
t.Fatalf("find saved payment B failed: %v", err)
|
||||
}
|
||||
if savedB.MerchantID != defaultConfig.MerchantID {
|
||||
t.Fatalf("Order B expected MerchantID=%s, got %s", defaultConfig.MerchantID, savedB.MerchantID)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user