fix(distribution): 修正分销模式判定与结算入账,冻结未确认分成
按买卖站点纯函数判定五种模式,分站卖主站货命中 two_party_platform_receive。 结算批次只收本站 site party,平台分成不再进入分销钱包;生产环境冻结结算入账与站点提现。
This commit is contained in:
@@ -24,74 +24,149 @@ func distributionTablesPresent(tx *gorm.DB) bool {
|
||||
return distributionTablePresent
|
||||
}
|
||||
|
||||
// ResolveOrderSourceTx 校验收号方站点与售出方站点、账号展示关系和生效规则,供正常订单创建事务写入快照引用。
|
||||
func ResolveOrderSourceTx(tx *gorm.DB, siteCode string, listingID, accountID uint64, at time.Time) (*model.DistributionSite, *model.DistributionRuleVersion, error) {
|
||||
// resolveDistributionMode 只根据买卖双方站点判定模式,不查库、不猜规则。
|
||||
// buySiteID / listingSiteID 为 0 表示主站。
|
||||
func resolveDistributionMode(buySiteID, listingSiteID uint64) string {
|
||||
switch {
|
||||
case buySiteID == 0 && listingSiteID == 0:
|
||||
return ""
|
||||
case buySiteID == 0:
|
||||
return ModeTwoPartyDistributorReceive
|
||||
case listingSiteID == 0:
|
||||
return ModeTwoPartyPlatformReceive
|
||||
case buySiteID == listingSiteID:
|
||||
return ModeSiteSelfSell
|
||||
default:
|
||||
return ModeThreeParty
|
||||
}
|
||||
}
|
||||
|
||||
func derefSiteID(id *uint64) uint64 {
|
||||
if id == nil || *id == 0 {
|
||||
return 0
|
||||
}
|
||||
return *id
|
||||
}
|
||||
|
||||
func loadListingSiteID(tx *gorm.DB, listingID uint64) (uint64, error) {
|
||||
if listingID == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
var listing model.RentalListing
|
||||
var listingSiteID *uint64
|
||||
if listingID > 0 {
|
||||
if err := tx.Select("id, distribution_site_id").First(&listing, listingID).Error; err == nil {
|
||||
listingSiteID = listing.DistributionSiteID
|
||||
if err := tx.Select("id, distribution_site_id").First(&listing, listingID).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
return derefSiteID(listing.DistributionSiteID), nil
|
||||
}
|
||||
|
||||
func loadActiveSiteByID(tx *gorm.DB, siteID uint64) (*model.DistributionSite, error) {
|
||||
var site model.DistributionSite
|
||||
if err := tx.Where("id = ? AND status = 'active'", siteID).First(&site).Error; err != nil {
|
||||
return nil, ErrSiteNotFound
|
||||
}
|
||||
return &site, nil
|
||||
}
|
||||
|
||||
func loadActiveSellSiteByCode(tx *gorm.DB, siteCode string) (*model.DistributionSite, error) {
|
||||
var site model.DistributionSite
|
||||
if err := tx.Where("site_code = ? AND status = 'active' AND display_account_enabled = ?", siteCode, true).First(&site).Error; err != nil {
|
||||
return nil, ErrSiteNotFound
|
||||
}
|
||||
return &site, nil
|
||||
}
|
||||
|
||||
func ensureAccountDisplayTx(tx *gorm.DB, siteID, listingID, accountID uint64) error {
|
||||
var relation model.DistributionSiteAccount
|
||||
if err := tx.Where("site_id = ? AND listing_id = ? AND account_id = ? AND display_status = 'enabled'", siteID, listingID, accountID).First(&relation).Error; err != nil {
|
||||
return ErrAccountDisplayNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func findActiveRuleTx(tx *gorm.DB, siteID uint64, mode string, at time.Time) (*model.DistributionRuleVersion, error) {
|
||||
var rule model.DistributionRuleVersion
|
||||
err := tx.Where("mode = ? AND status = 'active' AND effective_from <= ? AND (effective_to IS NULL OR effective_to > ?)", mode, at, at).
|
||||
Where("site_id = ? OR site_id IS NULL", siteID).
|
||||
Order("CASE WHEN site_id IS NULL THEN 1 ELSE 0 END, effective_from DESC, id DESC").
|
||||
First(&rule).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrRuleNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &rule, nil
|
||||
}
|
||||
|
||||
// ResolveOrderSourceTx 先按买卖站点判定模式,再查该 mode 的生效规则,不再在两方规则之间猜优先级。
|
||||
func ResolveOrderSourceTx(tx *gorm.DB, siteCode string, listingID, accountID uint64, at time.Time) (*model.DistributionSite, *model.DistributionRuleVersion, error) {
|
||||
listingSiteID, err := loadListingSiteID(tx, listingID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// 1. 两方均无分销站:纯主站交易
|
||||
if siteCode == "" && listingSiteID == nil {
|
||||
var buySite *model.DistributionSite
|
||||
buySiteID := uint64(0)
|
||||
if siteCode != "" {
|
||||
buySite, err = loadActiveSellSiteByCode(tx, siteCode)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if err := ensureAccountDisplayTx(tx, buySite.ID, listingID, accountID); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
buySiteID = buySite.ID
|
||||
}
|
||||
|
||||
mode := resolveDistributionMode(buySiteID, listingSiteID)
|
||||
if mode == "" {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
// 2. 模式一:分销网收号、主站卖出 (ModeTwoPartyDistributorReceive)
|
||||
if siteCode == "" && listingSiteID != nil {
|
||||
var site model.DistributionSite
|
||||
if err := tx.Where("id = ? AND status = 'active'", *listingSiteID).First(&site).Error; err != nil {
|
||||
var anchor *model.DistributionSite
|
||||
switch mode {
|
||||
case ModeTwoPartyDistributorReceive:
|
||||
anchor, err = loadActiveSiteByID(tx, listingSiteID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
default:
|
||||
if buySite == nil {
|
||||
return nil, nil, ErrSiteNotFound
|
||||
}
|
||||
var rule model.DistributionRuleVersion
|
||||
query := tx.Where("mode = ? AND status = 'active' AND effective_from <= ? AND (effective_to IS NULL OR effective_to > ?)", ModeTwoPartyDistributorReceive, at, at).
|
||||
Where("site_id = ? OR site_id IS NULL", site.ID).
|
||||
Order("CASE WHEN site_id IS NULL THEN 1 ELSE 0 END, effective_from DESC, id DESC")
|
||||
if err := query.First(&rule).Error; err != nil {
|
||||
return nil, nil, ErrRuleNotFound
|
||||
}
|
||||
return &site, &rule, nil
|
||||
anchor = buySite
|
||||
}
|
||||
|
||||
// 3. 买家在分销站下单:校验售出站点状态与账号展示关系
|
||||
var sellSite model.DistributionSite
|
||||
if err := tx.Where("site_code = ? AND status = 'active' AND display_account_enabled = ?", siteCode, true).First(&sellSite).Error; err != nil {
|
||||
return nil, nil, ErrSiteNotFound
|
||||
}
|
||||
var relation model.DistributionSiteAccount
|
||||
if err := tx.Where("site_id = ? AND listing_id = ? AND account_id = ? AND display_status = 'enabled'", sellSite.ID, listingID, accountID).First(&relation).Error; err != nil {
|
||||
return nil, nil, ErrAccountDisplayNotFound
|
||||
rule, err := findActiveRuleTx(tx, anchor.ID, mode, at)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return anchor, rule, nil
|
||||
}
|
||||
|
||||
// 4. 解析生效规则:优先匹配有效规则版本
|
||||
targetMode := ModeTwoPartyDistributorReceive
|
||||
if listingSiteID != nil && *listingSiteID != sellSite.ID {
|
||||
targetMode = ModeThreeParty
|
||||
func ruleFromVersion(rule model.DistributionRuleVersion) Rule {
|
||||
return Rule{
|
||||
Mode: rule.Mode,
|
||||
PlatformReceiveRatio: rule.PlatformReceiveRatio,
|
||||
PlatformSellRatio: rule.PlatformSellRatio,
|
||||
DistributorReceiveRatio: rule.DistributorReceiveRatio,
|
||||
DistributorSellRatio: rule.DistributorSellRatio,
|
||||
DistributorARatio: rule.DistributorARatio,
|
||||
DistributorBRatio: rule.DistributorBRatio,
|
||||
ServiceFeeValue: rule.ServiceFeeValue,
|
||||
ServiceFeeUnit: rule.ServiceFeeUnit,
|
||||
PaymentFeeBPS: rule.PaymentFeeBPS,
|
||||
TaxEnabled: rule.TaxEnabled,
|
||||
TaxRateBPS: rule.TaxRateBPS,
|
||||
}
|
||||
var rule model.DistributionRuleVersion
|
||||
query := tx.Where("status = 'active' AND effective_from <= ? AND (effective_to IS NULL OR effective_to > ?)", at, at).
|
||||
Where("site_id = ? OR site_id IS NULL", sellSite.ID)
|
||||
if targetMode == ModeThreeParty {
|
||||
query = query.Where("mode = ?", ModeThreeParty)
|
||||
} else {
|
||||
query = query.Where("mode IN (?, ?)", ModeTwoPartyDistributorReceive, ModeTwoPartyPlatformReceive).
|
||||
Order("CASE WHEN mode = '" + ModeTwoPartyDistributorReceive + "' THEN 0 ELSE 1 END")
|
||||
}
|
||||
query = query.Order("CASE WHEN site_id IS NULL THEN 1 ELSE 0 END, effective_from DESC, id DESC")
|
||||
if err := query.First(&rule).Error; err != nil {
|
||||
return nil, nil, ErrRuleNotFound
|
||||
}
|
||||
return &sellSite, &rule, nil
|
||||
}
|
||||
|
||||
// calculateForRule 按规则快照计算手续费、服务费和分成基数。
|
||||
func calculateForRule(rule model.DistributionRuleVersion, input SettlementInput) (SettlementResult, error) {
|
||||
return CalculateSettlement(Rule{
|
||||
Mode: rule.Mode, ServiceFeeValue: rule.ServiceFeeValue, ServiceFeeUnit: rule.ServiceFeeUnit,
|
||||
PaymentFeeBPS: rule.PaymentFeeBPS, TaxEnabled: rule.TaxEnabled, TaxRateBPS: rule.TaxRateBPS,
|
||||
}, input)
|
||||
return CalculateSettlement(ruleFromVersion(rule), input)
|
||||
}
|
||||
|
||||
// SnapshotPaidOrderTx 在支付确认事务内幂等写入财务和参与方快照。
|
||||
@@ -116,7 +191,7 @@ func SnapshotPaidOrderTx(tx *gorm.DB, orderID uint64, grossPaidCent int64) error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
splits := SplitSettlement(Rule{Mode: rule.Mode}, result.SplitBaseAmountCent, markup)
|
||||
splits := SplitSettlement(ruleFromVersion(rule), result.SplitBaseAmountCent, markup)
|
||||
financial := model.DistributionOrderFinancial{OrderID: order.ID, SiteID: *order.DistributionSiteID, RuleVersionID: rule.ID, GrossPaidAmountCent: grossPaidCent, NetPaidAmountCent: result.NetPaidAmountCent, PaymentFeeBPS: rule.PaymentFeeBPS, PaymentFeeCent: result.PaymentFeeCent, ServiceFeeValue: rule.ServiceFeeValue, ServiceFeeUnit: rule.ServiceFeeUnit, ServiceFeeCent: result.ServiceFeeCent, MarkupAmountCent: markup, TaxEnabled: rule.TaxEnabled, TaxRateBPS: rule.TaxRateBPS, TaxAmountCent: result.TaxAmountCent, SplitBaseAmountCent: result.SplitBaseAmountCent, RoundingAdjustmentCent: SplitRemainderCent(result.SplitBaseAmountCent, splits), SettlementStatus: "pending"}
|
||||
if err := tx.Where("order_id = ?", order.ID).Assign(financial).FirstOrCreate(&financial).Error; err != nil {
|
||||
return err
|
||||
@@ -130,12 +205,12 @@ func SnapshotPaidOrderTx(tx *gorm.DB, orderID uint64, grossPaidCent int64) error
|
||||
}
|
||||
parties := make([]model.DistributionOrderParty, 0, len(splits))
|
||||
for _, split := range splits {
|
||||
partyType := "site"
|
||||
partyType := PartyTypeSite
|
||||
if split.PartyRole == "platform_sell" || split.PartyRole == "platform_receive" {
|
||||
partyType = "platform"
|
||||
partyType = PartyTypePlatform
|
||||
}
|
||||
partyID := *order.DistributionSiteID
|
||||
if partyType == "platform" {
|
||||
if partyType == PartyTypePlatform {
|
||||
partyID = 0
|
||||
} else if split.PartyRole == "distributor_a" {
|
||||
// 三方模式下 distributor_a 为商品收号站
|
||||
@@ -175,7 +250,7 @@ func ApplyRefundTx(tx *gorm.DB, orderID uint64, refundAmountCent int64) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
splits := SplitSettlement(Rule{Mode: rule.Mode}, result.SplitBaseAmountCent, financial.MarkupAmountCent)
|
||||
splits := SplitSettlement(ruleFromVersion(rule), result.SplitBaseAmountCent, financial.MarkupAmountCent)
|
||||
if err := tx.Model(&financial).Updates(map[string]any{"refund_amount_cent": refundAmountCent, "net_paid_amount_cent": result.NetPaidAmountCent, "payment_fee_cent": result.PaymentFeeCent, "service_fee_cent": result.ServiceFeeCent, "split_base_amount_cent": result.SplitBaseAmountCent, "rounding_adjustment_cent": SplitRemainderCent(result.SplitBaseAmountCent, splits), "settlement_status": "pending"}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user