refactor(distribution): 重写分销迁移并落地利润百分比分成与货架比例
将 000064-000078 合并为 064/065/066,默认规则改为收号 35%、卖出 40%、服务费 25%。 货架调价改为增加分站 1 元兑换哈夫币比例。sql-guard 仅保护 main 已发布迁移。
This commit is contained in:
@@ -18,6 +18,10 @@ const (
|
||||
ServiceFeeUnitPercent = "percent"
|
||||
ServiceFeeUnitFixedAmount = "fixed_amount"
|
||||
ServiceFeeUnitRatio = "ratio"
|
||||
|
||||
DefaultReceivePercent = 35.0
|
||||
DefaultSellPercent = 40.0
|
||||
DefaultServicePercent = 25.0
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -27,7 +31,7 @@ var (
|
||||
ErrUnsupportedMode = errors.New("unsupported distribution mode")
|
||||
)
|
||||
|
||||
// Rule 是后台可编辑的分账规则。比例字段按参与方角色命名,避免方向歧义。
|
||||
// Rule 是后台可编辑的分账规则。比例字段存百分数(35/40/25),按参与方角色命名。
|
||||
type Rule struct {
|
||||
Mode string
|
||||
PlatformReceiveRatio float64
|
||||
@@ -47,6 +51,7 @@ type Rule struct {
|
||||
type SettlementInput struct {
|
||||
GrossPaidAmountCent int64
|
||||
RefundAmountCent int64
|
||||
OwnerRentAmountCent int64
|
||||
MarkupAmountCent int64
|
||||
GrossProfitByParty []PartyProfitInput
|
||||
}
|
||||
@@ -75,23 +80,22 @@ type SettlementResult struct {
|
||||
TaxByParty []PartyTaxResult
|
||||
}
|
||||
|
||||
// DefaultRule 返回文档中的第一版占位值,仅供对照,禁止当作已手算确认的生效分成。
|
||||
// 本站自卖没有默认比例,必须单独配置。
|
||||
// DefaultRule 返回已确认的默认百分比:收号 35%、卖出 40%、服务费 25%。
|
||||
func DefaultRule(mode string) (Rule, error) {
|
||||
base := Rule{
|
||||
Mode: mode, ServiceFeeValue: 2.5, ServiceFeeUnit: ServiceFeeUnitRatio,
|
||||
Mode: mode, ServiceFeeValue: DefaultServicePercent, ServiceFeeUnit: ServiceFeeUnitPercent,
|
||||
PaymentFeeBPS: 50, TaxEnabled: false, TaxRateBPS: 100,
|
||||
}
|
||||
switch mode {
|
||||
case ModeTwoPartyDistributorReceive:
|
||||
base.DistributorReceiveRatio = 3.5
|
||||
base.PlatformSellRatio = 4
|
||||
base.DistributorReceiveRatio = DefaultReceivePercent
|
||||
base.PlatformSellRatio = DefaultSellPercent
|
||||
case ModeTwoPartyPlatformReceive:
|
||||
base.PlatformReceiveRatio = 3.5
|
||||
base.DistributorSellRatio = 4
|
||||
base.PlatformReceiveRatio = DefaultReceivePercent
|
||||
base.DistributorSellRatio = DefaultSellPercent
|
||||
case ModeThreeParty:
|
||||
base.DistributorARatio = 3.5
|
||||
base.DistributorBRatio = 4
|
||||
base.DistributorARatio = DefaultReceivePercent
|
||||
base.DistributorBRatio = DefaultSellPercent
|
||||
case ModeSiteSelfSell:
|
||||
return Rule{}, ErrUnsupportedMode
|
||||
default:
|
||||
@@ -100,9 +104,38 @@ func DefaultRule(mode string) (Rule, error) {
|
||||
return base, nil
|
||||
}
|
||||
|
||||
// CalculateSettlement 先扣支付手续费,再计算服务费和分成基数;退款金额不能超过支付金额。
|
||||
// NormalizeSplitPercent 把口头 3.5/4/2.5 换成 35/40/25;已经是百分数的原样保留。
|
||||
func NormalizeSplitPercent(value float64) float64 {
|
||||
if value <= 0 || math.IsNaN(value) || math.IsInf(value, 0) {
|
||||
return 0
|
||||
}
|
||||
if value <= 10 {
|
||||
return math.Round(value*10*10) / 10
|
||||
}
|
||||
return math.Round(value*10) / 10
|
||||
}
|
||||
|
||||
func normalizeRulePercents(row *Rule) {
|
||||
if row == nil {
|
||||
return
|
||||
}
|
||||
row.PlatformReceiveRatio = NormalizeSplitPercent(row.PlatformReceiveRatio)
|
||||
row.PlatformSellRatio = NormalizeSplitPercent(row.PlatformSellRatio)
|
||||
row.DistributorReceiveRatio = NormalizeSplitPercent(row.DistributorReceiveRatio)
|
||||
row.DistributorSellRatio = NormalizeSplitPercent(row.DistributorSellRatio)
|
||||
row.DistributorARatio = NormalizeSplitPercent(row.DistributorARatio)
|
||||
row.DistributorBRatio = NormalizeSplitPercent(row.DistributorBRatio)
|
||||
if row.ServiceFeeUnit == ServiceFeeUnitRatio || row.ServiceFeeUnit == "" {
|
||||
row.ServiceFeeUnit = ServiceFeeUnitPercent
|
||||
}
|
||||
if row.ServiceFeeUnit == ServiceFeeUnitPercent {
|
||||
row.ServiceFeeValue = NormalizeSplitPercent(row.ServiceFeeValue)
|
||||
}
|
||||
}
|
||||
|
||||
// CalculateSettlement 先扣通道手续费和号主应得,得到利润后再按 35/40/25 切。
|
||||
func CalculateSettlement(rule Rule, input SettlementInput) (SettlementResult, error) {
|
||||
if input.GrossPaidAmountCent < 0 || input.RefundAmountCent < 0 || input.MarkupAmountCent < 0 || input.RefundAmountCent > input.GrossPaidAmountCent {
|
||||
if input.GrossPaidAmountCent < 0 || input.RefundAmountCent < 0 || input.OwnerRentAmountCent < 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 {
|
||||
@@ -113,13 +146,16 @@ func CalculateSettlement(rule Rule, input SettlementInput) (SettlementResult, er
|
||||
}
|
||||
netPaid := input.GrossPaidAmountCent - input.RefundAmountCent
|
||||
paymentFee := roundHalfUp(float64(netPaid) * float64(rule.PaymentFeeBPS) / 10000)
|
||||
serviceFee, err := serviceFeeCent(rule, netPaid)
|
||||
profit := netPaid - input.OwnerRentAmountCent - paymentFee
|
||||
if profit < 0 {
|
||||
profit = 0
|
||||
}
|
||||
serviceFee, err := serviceFeeCent(rule, profit, netPaid)
|
||||
if err != nil {
|
||||
return SettlementResult{}, err
|
||||
}
|
||||
splitBase := netPaid - paymentFee - serviceFee
|
||||
if splitBase < 0 {
|
||||
splitBase = 0
|
||||
if serviceFee > profit {
|
||||
serviceFee = profit
|
||||
}
|
||||
result := SettlementResult{
|
||||
GrossPaidAmountCent: input.GrossPaidAmountCent,
|
||||
@@ -128,24 +164,24 @@ func CalculateSettlement(rule Rule, input SettlementInput) (SettlementResult, er
|
||||
PaymentFeeCent: paymentFee,
|
||||
ServiceFeeCent: serviceFee,
|
||||
MarkupAmountCent: input.MarkupAmountCent,
|
||||
SplitBaseAmountCent: splitBase,
|
||||
SplitBaseAmountCent: profit,
|
||||
}
|
||||
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)
|
||||
profitParty := party.IncomeAmountCent - party.CostAmountCent
|
||||
tax := roundHalfUp(float64(maxInt64(profitParty, 0)) * float64(rule.TaxRateBPS) / 10000)
|
||||
result.TaxAmountCent += tax
|
||||
result.TaxByParty = append(result.TaxByParty, PartyTaxResult{PartyRole: party.PartyRole, GrossProfitCent: profit, TaxAmountCent: tax})
|
||||
result.TaxByParty = append(result.TaxByParty, PartyTaxResult{PartyRole: party.PartyRole, GrossProfitCent: profitParty, TaxAmountCent: tax})
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func serviceFeeCent(rule Rule, netPaid int64) (int64, error) {
|
||||
func serviceFeeCent(rule Rule, profit, netPaid int64) (int64, error) {
|
||||
switch rule.ServiceFeeUnit {
|
||||
case ServiceFeeUnitPercent:
|
||||
return roundHalfUp(float64(netPaid) * rule.ServiceFeeValue / 100), nil
|
||||
return roundHalfUp(float64(profit) * rule.ServiceFeeValue / 100), nil
|
||||
case ServiceFeeUnitFixedAmount:
|
||||
return roundHalfUp(rule.ServiceFeeValue * 100), nil
|
||||
case ServiceFeeUnitRatio:
|
||||
@@ -176,11 +212,8 @@ type PartySplit struct {
|
||||
AmountCent int64
|
||||
}
|
||||
|
||||
// SplitSettlement 按规则模式把分成基数以百分比拆给各分销参与方。
|
||||
// 销售加价全部计入卖出方收入(设计文档 7.6);
|
||||
// 剩余金额(含四舍五入尾差)由调用方归入平台留存并记录 RoundingAdjustmentCent,
|
||||
// 最终业务口径以设计文档 7.2 待确认项的结论为准。
|
||||
func SplitSettlement(rule Rule, splitBase int64, markupCent int64) []PartySplit {
|
||||
// SplitSettlement 按百分数切利润:收号 35%、卖出 40%。服务费 25% 记在财务快照,不并进参与方。
|
||||
func SplitSettlement(rule Rule, profitCent int64) []PartySplit {
|
||||
roles := []struct {
|
||||
role string
|
||||
ratio float64
|
||||
@@ -206,28 +239,13 @@ func SplitSettlement(rule Rule, splitBase int64, markupCent int64) []PartySplit
|
||||
}
|
||||
splits := make([]PartySplit, 0, len(roles))
|
||||
for _, item := range roles {
|
||||
amount := roundHalfUp(float64(splitBase) * item.ratio / 100)
|
||||
amount := roundHalfUp(float64(profitCent) * 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 返回分成基数扣除各方分成后的平台留存金额(含尾差)。
|
||||
// SplitRemainderCent 返回利润扣除各方分成后的剩余(未扣服务费)。
|
||||
func SplitRemainderCent(splitBase int64, splits []PartySplit) int64 {
|
||||
sum := int64(0)
|
||||
for _, item := range splits {
|
||||
@@ -235,3 +253,8 @@ func SplitRemainderCent(splitBase int64, splits []PartySplit) int64 {
|
||||
}
|
||||
return splitBase - sum
|
||||
}
|
||||
|
||||
// SplitRoundingCent 返回利润扣完收号、卖出和服务费后的四舍五入尾差。
|
||||
func SplitRoundingCent(profitCent, serviceFeeCent int64, splits []PartySplit) int64 {
|
||||
return SplitRemainderCent(profitCent, splits) - serviceFeeCent
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user