Files
hfb_sys/backend/internal/modules/distribution/lifecycle.go
T

191 lines
8.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package distribution
import (
"errors"
"sync"
"time"
"hfb_sys/backend/internal/model"
"gorm.io/gorm"
)
// distributionTablesPresent 探测分销财务表是否存在,仅首次访问查询 information_schema,
// 兼容尚未执行分销迁移的旧环境,避免每次支付/退款事务都做系统表查询。
var (
distributionTablePresent bool
distributionTableCheckOnce sync.Once
)
func distributionTablesPresent(tx *gorm.DB) bool {
distributionTableCheckOnce.Do(func() {
distributionTablePresent = tx.Migrator().HasTable(&model.DistributionOrderFinancial{})
})
return distributionTablePresent
}
// ResolveOrderSourceTx 校验收号方站点与售出方站点、账号展示关系和生效规则,供正常订单创建事务写入快照引用。
func ResolveOrderSourceTx(tx *gorm.DB, siteCode string, listingID, accountID uint64, at time.Time) (*model.DistributionSite, *model.DistributionRuleVersion, error) {
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
}
}
// 1. 两方均无分销站:纯主站交易
if siteCode == "" && listingSiteID == nil {
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 {
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
}
// 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
}
// 4. 解析生效规则:优先匹配有效规则版本
targetMode := ModeTwoPartyDistributorReceive
if listingSiteID != nil && *listingSiteID != sellSite.ID {
targetMode = ModeThreeParty
}
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)
}
// SnapshotPaidOrderTx 在支付确认事务内幂等写入财务和参与方快照。
// 参与方金额按规则比例对分成基数拆分,剩余(含尾差)记入 RoundingAdjustmentCent 归平台。
func SnapshotPaidOrderTx(tx *gorm.DB, orderID uint64, grossPaidCent int64) error {
if !distributionTablesPresent(tx) {
return nil
}
var order model.RentalOrder
if err := tx.First(&order, orderID).Error; err != nil {
return err
}
if order.DistributionSiteID == nil || order.DistributionRuleVersionID == nil {
return nil
}
var rule model.DistributionRuleVersion
if err := tx.First(&rule, *order.DistributionRuleVersionID).Error; err != nil {
return err
}
markup := order.PlatformFeeCent
result, err := calculateForRule(rule, SettlementInput{GrossPaidAmountCent: grossPaidCent, MarkupAmountCent: markup})
if err != nil {
return err
}
splits := SplitSettlement(Rule{Mode: rule.Mode}, 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
}
var count int64
if err := tx.Model(&model.DistributionOrderParty{}).Where("order_id = ?", order.ID).Count(&count).Error; err != nil {
return err
}
if count > 0 {
return nil
}
parties := make([]model.DistributionOrderParty, 0, len(splits))
for _, split := range splits {
partyType := "site"
if split.PartyRole == "platform_sell" || split.PartyRole == "platform_receive" {
partyType = "platform"
}
partyID := *order.DistributionSiteID
if partyType == "platform" {
partyID = 0
} else if split.PartyRole == "distributor_a" {
// 三方模式下 distributor_a 为商品收号站
var listing model.RentalListing
if err := tx.Select("distribution_site_id").First(&listing, order.ListingID).Error; err == nil && listing.DistributionSiteID != nil {
partyID = *listing.DistributionSiteID
}
}
parties = append(parties, model.DistributionOrderParty{OrderID: order.ID, SiteID: *order.DistributionSiteID, PartyRole: split.PartyRole, PartyType: partyType, PartyID: partyID, Ratio: split.Ratio, IncomeAmountCent: split.AmountCent, ProfitAmountCent: split.AmountCent})
}
if len(parties) == 0 {
return nil
}
return tx.Create(&parties).Error
}
// ApplyRefundTx 按累计成功退款金额重算手续费、分成基数和各方分成,不覆盖订单创建时的规则版本。
func ApplyRefundTx(tx *gorm.DB, orderID uint64, refundAmountCent int64) error {
if !distributionTablesPresent(tx) {
return nil
}
var financial model.DistributionOrderFinancial
if err := tx.Where("order_id = ?", orderID).First(&financial).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil
}
return err
}
if refundAmountCent < 0 || refundAmountCent > financial.GrossPaidAmountCent {
return ErrInvalidAmount
}
var rule model.DistributionRuleVersion
if err := tx.First(&rule, financial.RuleVersionID).Error; err != nil {
return err
}
result, err := calculateForRule(rule, SettlementInput{GrossPaidAmountCent: financial.GrossPaidAmountCent, RefundAmountCent: refundAmountCent, MarkupAmountCent: financial.MarkupAmountCent})
if err != nil {
return err
}
splits := SplitSettlement(Rule{Mode: rule.Mode}, 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
}
for _, split := range splits {
if err := tx.Model(&model.DistributionOrderParty{}).
Where("order_id = ? AND party_role = ?", orderID, split.PartyRole).
Updates(map[string]any{"income_amount_cent": split.AmountCent, "profit_amount_cent": split.AmountCent}).Error; err != nil {
return err
}
}
return nil
}