Files
hfb_sys/backend/internal/modules/distribution/pricing.go
T
yml2213 8521bf4753 fix(distribution): 修正分销模式判定与结算入账,冻结未确认分成
按买卖站点纯函数判定五种模式,分站卖主站货命中 two_party_platform_receive。
结算批次只收本站 site party,平台分成不再进入分销钱包;生产环境冻结结算入账与站点提现。
2026-09-08 16:52:09 +08:00

238 lines
7.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"
"math"
)
const (
ModeTwoPartyDistributorReceive = "two_party_distributor_receive"
ModeTwoPartyPlatformReceive = "two_party_platform_receive"
ModeThreeParty = "three_party"
// ModeSiteSelfSell 是分站卖本站货,不复用两方默认规则。
ModeSiteSelfSell = "site_self_sell"
PartyTypeSite = "site"
PartyTypePlatform = "platform"
ServiceFeeUnitPercent = "percent"
ServiceFeeUnitFixedAmount = "fixed_amount"
ServiceFeeUnitRatio = "ratio"
)
var (
ErrInvalidAmount = errors.New("invalid distribution amount")
ErrInvalidRate = errors.New("invalid distribution rate")
ErrInvalidServiceUnit = errors.New("invalid service fee unit")
ErrUnsupportedMode = errors.New("unsupported distribution mode")
)
// Rule 是后台可编辑的分账规则。比例字段按参与方角色命名,避免方向歧义。
type Rule struct {
Mode string
PlatformReceiveRatio float64
PlatformSellRatio float64
DistributorReceiveRatio float64
DistributorSellRatio float64
DistributorARatio float64
DistributorBRatio float64
ServiceFeeValue float64
ServiceFeeUnit string
PaymentFeeBPS int64
TaxEnabled bool
TaxRateBPS int64
}
// SettlementInput 使用支付单和退款单的整数分快照计算分销费用。
type SettlementInput struct {
GrossPaidAmountCent int64
RefundAmountCent int64
MarkupAmountCent int64
GrossProfitByParty []PartyProfitInput
}
type PartyProfitInput struct {
PartyRole string
IncomeAmountCent int64
CostAmountCent int64
}
type PartyTaxResult struct {
PartyRole string `json:"party_role"`
GrossProfitCent int64 `json:"gross_profit_cent"`
TaxAmountCent int64 `json:"tax_amount_cent"`
}
type SettlementResult struct {
GrossPaidAmountCent int64
RefundAmountCent int64
NetPaidAmountCent int64
PaymentFeeCent int64
ServiceFeeCent int64
MarkupAmountCent int64
SplitBaseAmountCent int64
TaxAmountCent int64
TaxByParty []PartyTaxResult
}
// DefaultRule 返回文档中的第一版占位值,仅供对照,禁止当作已手算确认的生效分成。
// 本站自卖没有默认比例,必须单独配置。
func DefaultRule(mode string) (Rule, error) {
base := Rule{
Mode: mode, ServiceFeeValue: 2.5, ServiceFeeUnit: ServiceFeeUnitRatio,
PaymentFeeBPS: 50, TaxEnabled: false, TaxRateBPS: 100,
}
switch mode {
case ModeTwoPartyDistributorReceive:
base.DistributorReceiveRatio = 3.5
base.PlatformSellRatio = 4
case ModeTwoPartyPlatformReceive:
base.PlatformReceiveRatio = 3.5
base.DistributorSellRatio = 4
case ModeThreeParty:
base.DistributorARatio = 3.5
base.DistributorBRatio = 4
case ModeSiteSelfSell:
return Rule{}, ErrUnsupportedMode
default:
return Rule{}, ErrUnsupportedMode
}
return base, nil
}
// CalculateSettlement 先扣支付手续费,再计算服务费和分成基数;退款金额不能超过支付金额。
func CalculateSettlement(rule Rule, input SettlementInput) (SettlementResult, error) {
if input.GrossPaidAmountCent < 0 || input.RefundAmountCent < 0 || input.MarkupAmountCent < 0 || input.RefundAmountCent > input.GrossPaidAmountCent {
return SettlementResult{}, ErrInvalidAmount
}
if rule.PaymentFeeBPS < 0 || rule.PaymentFeeBPS > 10000 || rule.TaxRateBPS < 0 || rule.TaxRateBPS > 10000 || rule.ServiceFeeValue < 0 {
return SettlementResult{}, ErrInvalidRate
}
if rule.ServiceFeeUnit != ServiceFeeUnitPercent && rule.ServiceFeeUnit != ServiceFeeUnitFixedAmount && rule.ServiceFeeUnit != ServiceFeeUnitRatio {
return SettlementResult{}, ErrInvalidServiceUnit
}
netPaid := input.GrossPaidAmountCent - input.RefundAmountCent
paymentFee := roundHalfUp(float64(netPaid) * float64(rule.PaymentFeeBPS) / 10000)
serviceFee, err := serviceFeeCent(rule, netPaid)
if err != nil {
return SettlementResult{}, err
}
splitBase := netPaid - paymentFee - serviceFee
if splitBase < 0 {
splitBase = 0
}
result := SettlementResult{
GrossPaidAmountCent: input.GrossPaidAmountCent,
RefundAmountCent: input.RefundAmountCent,
NetPaidAmountCent: netPaid,
PaymentFeeCent: paymentFee,
ServiceFeeCent: serviceFee,
MarkupAmountCent: input.MarkupAmountCent,
SplitBaseAmountCent: splitBase,
}
if rule.TaxEnabled {
result.TaxByParty = make([]PartyTaxResult, 0, len(input.GrossProfitByParty))
for _, party := range input.GrossProfitByParty {
profit := party.IncomeAmountCent - party.CostAmountCent
tax := roundHalfUp(float64(maxInt64(profit, 0)) * float64(rule.TaxRateBPS) / 10000)
result.TaxAmountCent += tax
result.TaxByParty = append(result.TaxByParty, PartyTaxResult{PartyRole: party.PartyRole, GrossProfitCent: profit, TaxAmountCent: tax})
}
}
return result, nil
}
func serviceFeeCent(rule Rule, netPaid int64) (int64, error) {
switch rule.ServiceFeeUnit {
case ServiceFeeUnitPercent:
return roundHalfUp(float64(netPaid) * rule.ServiceFeeValue / 100), nil
case ServiceFeeUnitFixedAmount:
return roundHalfUp(rule.ServiceFeeValue * 100), nil
case ServiceFeeUnitRatio:
if rule.ServiceFeeValue <= 0 {
return 0, nil
}
return roundHalfUp(float64(netPaid) / rule.ServiceFeeValue), nil
default:
return 0, ErrInvalidServiceUnit
}
}
func roundHalfUp(value float64) int64 {
return int64(math.Floor(value + 0.5))
}
func maxInt64(a, b int64) int64 {
if a > b {
return a
}
return b
}
// PartySplit 是一次分成拆分中单个参与方的金额。
type PartySplit struct {
PartyRole string
Ratio float64
AmountCent int64
}
// SplitSettlement 按规则模式把分成基数以百分比拆给各分销参与方。
// 销售加价全部计入卖出方收入(设计文档 7.6);
// 剩余金额(含四舍五入尾差)由调用方归入平台留存并记录 RoundingAdjustmentCent,
// 最终业务口径以设计文档 7.2 待确认项的结论为准。
func SplitSettlement(rule Rule, splitBase int64, markupCent int64) []PartySplit {
roles := []struct {
role string
ratio float64
}{}
switch rule.Mode {
case ModeTwoPartyDistributorReceive:
roles = []struct {
role string
ratio float64
}{{"distributor_receive", rule.DistributorReceiveRatio}, {"platform_sell", rule.PlatformSellRatio}}
case ModeTwoPartyPlatformReceive:
roles = []struct {
role string
ratio float64
}{{"distributor_sell", rule.DistributorSellRatio}, {"platform_receive", rule.PlatformReceiveRatio}}
case ModeThreeParty:
roles = []struct {
role string
ratio float64
}{{"distributor_a", rule.DistributorARatio}, {"distributor_b", rule.DistributorBRatio}}
default:
return nil
}
splits := make([]PartySplit, 0, len(roles))
for _, item := range roles {
amount := roundHalfUp(float64(splitBase) * item.ratio / 100)
splits = append(splits, PartySplit{PartyRole: item.role, Ratio: item.ratio, AmountCent: maxInt64(amount, 0)})
}
if markupCent > 0 {
// 加价归卖出方:两方-分销收号与三方模式的卖出方分别是 platform_sell / distributor_b。
sellerRole := "distributor_sell"
switch rule.Mode {
case ModeTwoPartyDistributorReceive:
sellerRole = "platform_sell"
case ModeThreeParty:
sellerRole = "distributor_b"
}
for i := range splits {
if splits[i].PartyRole == sellerRole {
splits[i].AmountCent += markupCent
}
}
}
return splits
}
// SplitRemainderCent 返回分成基数扣除各方分成后的平台留存金额(含尾差)。
func SplitRemainderCent(splitBase int64, splits []PartySplit) int64 {
sum := int64(0)
for _, item := range splits {
sum += item.AmountCent
}
return splitBase - sum
}