- 权限落库:新增 000066 迁移,注册 11 个 distribution:* 权限并授权 super_admin/ops/finance - 越权修复:siteAccessAllowed 校验贯穿站点/规则/订单/展示/支付通道/结算全部按 ID 操作 - 分账闭环:SplitSettlement 按比例拆分分成基数,销售加价归卖出方,尾差记入 rounding_adjustment_cent 归平台;支付快照与退款重算均按拆分写入参与方金额 - 结算批次:创建时聚合周期内待结算订单生成明细并汇总金额,结算时校验明细与总额一致后联动置位财务快照 - 手工补数接口禁止覆盖已有快照并写入审计日志 - 杂项:二维码池校验站点启用状态且不吞真实 DB 错误;绑定支付通道校验商户配置存在;表探测改为 sync.Once 缓存
230 lines
7.3 KiB
Go
230 lines
7.3 KiB
Go
package distribution
|
||
|
||
import (
|
||
"errors"
|
||
"math"
|
||
)
|
||
|
||
const (
|
||
ModeTwoPartyDistributorReceive = "two_party_distributor_receive"
|
||
ModeTwoPartyPlatformReceive = "two_party_platform_receive"
|
||
ModeThreeParty = "three_party"
|
||
|
||
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
|
||
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
|
||
}
|