目录
业务需求
一个订单由多种支付方式,每个 sku 可以被 多种支付方式支付,给一个退款金额,如何分配到每种支付方式和 sku。
算法说明
-
双层分配机制:
-
第一层:在支付方式之间分配总退款金额
-
第二层:在每个支付方式内部将分配到的金额进一步分配到各个SKU
-
-
智能余数分配:
-
使用整数除法进行基础分配
-
余数按照剩余金额降序排列进行分配
-
每次分配1分直到余数分配完毕
-
-
防超退措施:
-
严格验证分配金额不超过剩余金额
-
使用排序确保优先处理大额余数
-
双重校验保证数据一致性
-
代码实现
以下是使用Go语言实现的退款分配算法,该算法采用智能余数分配策略,确保在支付方式和SKU层面均不会出现超退问题。
package main
import (
"errors"
"fmt"
"sort"
)
type SkuRefund struct {
SkuID string
TotalAmount int // 该SKU在该支付方式中的总金额(分)
Refunded int // 已退金额(分)
}
type PaymentMethod struct {
ID string
TotalAmount int // 支付方式总金额(分)
Refunded int // 已退金额(分)
Skus []*SkuRefund // 包含的SKU列表
}
type RefundResult struct {
PaymentMethodRefunds map[string]int // 支付方式ID -> 退款金额
SkuRefunds map[string]map[string]int // 支付方式ID -> (SKUID -> 退款金额)
}
func CalculateRefund(paymentMethods []*PaymentMethod, refundAmount int) (*RefundResult, error) {
pmAllocations, err := allocatePaymentMethods(paymentMethods, refundAmount)
if err != nil {
return nil, err
}
result := &RefundResult{
PaymentMethodRefunds: make(map[string]int),
SkuRefunds: make(map[string]map[string]int),
}
for _, pm := range paymentMethods {
amount := pmAllocations[pm.ID]
if amount == 0 {
continue
}
skuAllocations, err := allocateSkus(pm.Skus, amount)
if err != nil {
return nil, fmt.Errorf("payment method %s: %w", pm.ID, err)
}
result.PaymentMethodRefunds[pm.ID] = amount
result.SkuRefunds[pm.ID] = skuAllocations
}
return result, nil
}
func allocatePaymentMethods(paymentMethods []*PaymentMethod, totalRefund int) (map[string]int, error) {
totalRemain := 0
pmRemains := make(map[string]int)
for _, pm := range paymentMethods {
remain := pm.TotalAmount - pm.Refunded
if remain < 0 {
return nil, errors.New("negative remaining in payment method")
}
pmRemains[pm.ID] = remain
totalRemain += remain
}
if totalRemain < totalRefund {
return nil, errors.New("refund amount exceeds total remaining")
}
// 计算基础分配和余数
allocations := make(map[string]int)
sumBase := 0
bases := make(map[string]int)
for _, pm := range paymentMethods {
remain := pmRemains[pm.ID]
if remain == 0 {
continue
}
base := (totalRefund * remain) / totalRemain
bases[pm.ID] = base
sumBase += base
}
remainder := totalRefund - sumBase
// 排序支付方式(按剩余金额降序,ID升序)
sortedPMs := make([]*PaymentMethod, len(paymentMethods))
copy(sortedPMs, paymentMethods)
sort.Slice(sortedPMs, func(i, j int) bool {
ri := sortedPMs[i].TotalAmount - sortedPMs[i].Refunded
rj := sortedPMs[j].TotalAmount - sortedPMs[j].Refunded
if ri == rj {
return sortedPMs[i].ID < sortedPMs[j].ID
}
return ri > rj
})
// 分配余数
for remainder > 0 {
allocated := false
for _, pm := range sortedPMs {
if remainder <= 0 {
break
}
remain := pmRemains[pm.ID]
if bases[pm.ID] < remain {
bases[pm.ID]++
remainder--
allocated = true
}
}
if !allocated && remainder > 0 {
return nil, errors.New("remainder allocation failed")
}
}
// 构建结果并验证
for _, pm := range paymentMethods {
allocated := bases[pm.ID]
if allocated > pmRemains[pm.ID] {
return nil, fmt.Errorf("allocation exceeded for payment method %s", pm.ID)
}
allocations[pm.ID] = allocated
}
return allocations, nil
}
func allocateSkus(skus []*SkuRefund, totalRefund int) (map[string]int, error) {
totalRemain := 0
skuRemains := make(map[string]int)
for _, sku := range skus {
remain := sku.TotalAmount - sku.Refunded
if remain < 0 {
return nil, errors.New("negative remaining in sku")
}
skuRemains[sku.SkuID] = remain
totalRemain += remain
}
if totalRemain < totalRefund {
return nil, errors.New("sku refund exceeds total remaining")
}
// 计算基础分配和余数
allocations := make(map[string]int)
sumBase := 0
bases := make(map[string]int)
for _, sku := range skus {
remain := skuRemains[sku.SkuID]
if remain == 0 {
continue
}
base := (totalRefund * remain) / totalRemain
bases[sku.SkuID] = base
sumBase += base
}
remainder := totalRefund - sumBase
// 排序SKU(按剩余金额降序,ID升序)
sortedSkus := make([]*SkuRefund, len(skus))
copy(sortedSkus, skus)
sort.Slice(sortedSkus, func(i, j int) bool {
ri := sortedSkus[i].TotalAmount - sortedSkus[i].Refunded
rj := sortedSkus[j].TotalAmount - sortedSkus[j].Refunded
if ri == rj {
return sortedSkus[i].SkuID < sortedSkus[j].SkuID
}
return ri > rj
})
// 分配余数
for remainder > 0 {
allocated := false
for _, sku := range sortedSkus {
if remainder <= 0 {
break
}
remain := skuRemains[sku.SkuID]
if bases[sku.SkuID] < remain {
bases[sku.SkuID]++
remainder--
allocated = true
}
}
if !allocated && remainder > 0 {
return nil, errors.New("sku remainder allocation failed")
}
}
// 构建结果并验证
for _, sku := range skus {
allocated := bases[sku.SkuID]
if allocated > skuRemains[sku.SkuID] {
return nil, fmt.Errorf("allocation exceeded for sku %s", sku.SkuID)
}
allocations[sku.SkuID] = allocated
}
return allocations, nil
}
// 示例用法
func main() {
// 创建测试支付方式
paymentMethods := []*PaymentMethod{
{
ID: "pm1",
TotalAmount: 100,
Refunded: 33 + 33,
Skus: []*SkuRefund{
{SkuID: "sku1", TotalAmount: 50, Refunded: 17 + 16},
{SkuID: "sku2", TotalAmount: 50, Refunded: 16 + 17},
},
},
{
ID: "pm2",
TotalAmount: 200,
Refunded: 67 + 67,
Skus: []*SkuRefund{
{SkuID: "sku1", TotalAmount: 100, Refunded: 34 + 33},
{SkuID: "sku2", TotalAmount: 100, Refunded: 33 + 34},
},
},
}
// 计算退款100分
result, err := CalculateRefund(paymentMethods, 100)
if err != nil {
panic(err)
}
fmt.Println("支付方式退款:")
for pm, amount := range result.PaymentMethodRefunds {
fmt.Printf("%s: %d分\n", pm, amount)
}
fmt.Println("\nSKU退款:")
for pm, skus := range result.SkuRefunds {
fmt.Printf("支付方式 %s:\n", pm)
for sku, amount := range skus {
fmt.Printf(" %s: %d分\n", sku, amount)
}
}
}
示例输出
支付方式退款:
pm1: 34分
pm2: 66分
SKU退款:
支付方式 pm2:
sku1: 33分
sku2: 33分
支付方式 pm1:
sku1: 17分
sku2: 17分