fix(订单): 修复折扣计算问题并添加四舍五入处理
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 6m53s

统一处理百分比和系数两种折扣输入方式,增加边界保护
在金额计算中使用math.Round进行四舍五入处理
添加相关单元测试确保计算准确性
This commit is contained in:
shanshanzhong 2025-12-02 02:22:09 -08:00
parent fcdd6ac170
commit 9987bd43fa
15 changed files with 206 additions and 30 deletions

View File

@ -0,0 +1,31 @@
## 问题确认
- 前端以“百分比”形式配置折扣(如 95、95.19),当前后端 `getDiscount` 函数期望“系数01导致折扣未生效。
- 受影响位置:
- 登录下单折扣:`internal/logic/public/order/getDiscount.go`
- 门户预下单/下单折扣:`internal/logic/public/portal/tool.go:getDiscount`
## 改造目标
- 后端折扣计算统一兼容两种输入:
- 系数01直接使用
- 百分比(>1 且 <=100自动转换为 `值/100` 再使用
- 对非法值进行边界保护(<0 0>100 → 忽略或按 1 处理),避免异常。
## 实施步骤
1. 修改折扣计算函数:
- `internal/logic/public/order/getDiscount.go`
- 若 `discount.Discount > 1 && discount.Discount <= 100`,转换为 `discount.Discount/100`
- 保持“取满足阈值的最小折扣”策略,默认 `finalDiscount=1.0`
- `internal/logic/public/portal/tool.go:getDiscount`
- 同上逻辑,移除对 `*100` 的中间整数化处理,统一用浮点小数系数比较。
2. 单元测试补充:
- 既测系数(如 0.95),也测百分比输入(如 95、95.19以及边界0、100、>100
3. 验证流程:
- 用你当前 7 天的配置(前端百分比)进行“预下单→下单→订单查询”,确认 `Discount``Amount` 按预期生效。
4. 文档与界面提示:
- 在后台/前端表单处增加说明:支持百分比与系数;百分比将自动转换;推荐使用百分比,避免歧义。
## 交付与保障
- 代码改动仅限折扣计算函数与测试,风险低;保留原有行为的向后兼容。
- 提供测试报告与一次联调记录(数据截图:价格、折扣、总计)。
请确认是否按该兼容方案执行,我将据此修改并验证。

View File

@ -1,13 +1,14 @@
package order
import (
"github.com/perfect-panel/server/internal/model/coupon"
"math"
"github.com/perfect-panel/server/internal/model/coupon"
)
func calculateCoupon(amount int64, couponInfo *coupon.Coupon) int64 {
if couponInfo.Type == 1 {
return int64(float64(amount) * (float64(couponInfo.Discount) / float64(100)))
} else {
return min(couponInfo.Discount, amount)
}
if couponInfo.Type == 1 {
return int64(math.Round(float64(amount) * (float64(couponInfo.Discount) / float64(100))))
} else {
return min(couponInfo.Discount, amount)
}
}

View File

@ -1,6 +1,9 @@
package order
import "github.com/perfect-panel/server/internal/model/payment"
import (
"math"
"github.com/perfect-panel/server/internal/model/payment"
)
func calculateFee(amount int64, config *payment.Payment) int64 {
var fee float64
@ -16,5 +19,5 @@ func calculateFee(amount int64, config *payment.Payment) int64 {
case 3:
fee = float64(amount)*(float64(config.FeePercent)/float64(100)) + float64(config.FeeAmount)
}
return int64(fee)
return int64(math.Round(fee))
}

View File

@ -0,0 +1,31 @@
package order
import (
"testing"
"github.com/perfect-panel/server/internal/model/coupon"
)
func TestCalculateCoupon_Percent(t *testing.T) {
c := &coupon.Coupon{Type: 1, Discount: 10}
got := calculateCoupon(1000, c)
if got != 100 {
t.Fatalf("percent coupon expected 100, got %d", got)
}
}
func TestCalculateCoupon_PercentRounding(t *testing.T) {
c := &coupon.Coupon{Type: 1, Discount: 10}
got := calculateCoupon(999, c) // 999*10% = 99.9 → round to 100
if got != 100 {
t.Fatalf("percent coupon rounding expected 100, got %d", got)
}
}
func TestCalculateCoupon_FixedCap(t *testing.T) {
c := &coupon.Coupon{Type: 2, Discount: 300}
got := calculateCoupon(200, c)
if got != 200 {
t.Fatalf("fixed coupon capped by amount expected 200, got %d", got)
}
}

View File

@ -0,0 +1,35 @@
package order
import (
"testing"
"github.com/perfect-panel/server/internal/model/payment"
)
func TestCalculateFee_Percent(t *testing.T) {
p := &payment.Payment{FeeMode: 1, FeePercent: 5}
if calculateFee(1000, p) != 50 {
t.Fatal("fee percent 5% of 1000 should be 50")
}
}
func TestCalculateFee_PercentRounding(t *testing.T) {
p := &payment.Payment{FeeMode: 1, FeePercent: 5}
if calculateFee(999, p) != 50 { // 999*5% = 49.95 → round to 50
t.Fatal("fee percent 5% of 999 should round to 50")
}
}
func TestCalculateFee_Fixed(t *testing.T) {
p := &payment.Payment{FeeMode: 2, FeeAmount: 300}
if calculateFee(1000, p) != 300 {
t.Fatal("fixed fee 300 should be 300")
}
}
func TestCalculateFee_Mixed(t *testing.T) {
p := &payment.Payment{FeeMode: 3, FeePercent: 10, FeeAmount: 100}
if calculateFee(1000, p) != 200 {
t.Fatal("mixed fee 10% + 100 of 1000 should be 200")
}
}

View File

@ -6,13 +6,20 @@ import "github.com/perfect-panel/server/internal/types"
// 参数discounts 折扣规则列表(数量阈值 + 折扣倍数inputMonths 购买时长
// 返回:最终价格倍数(如 0.95 表示按 95% 计价)
func getDiscount(discounts []types.SubscribeDiscount, inputMonths int64) float64 {
var finalDiscount float64 = 1.0
var finalDiscount float64 = 1.0
for _, discount := range discounts {
if inputMonths >= discount.Quantity && discount.Discount < finalDiscount {
finalDiscount = discount.Discount
}
}
for _, d := range discounts {
val := d.Discount
if val > 1 && val <= 100 {
val = val / 100.0
}
if val < 0 {
val = 0
}
if inputMonths >= d.Quantity && val < finalDiscount {
finalDiscount = val
}
}
return finalDiscount
return finalDiscount
}

View File

@ -0,0 +1,31 @@
package order
import (
"testing"
"github.com/perfect-panel/server/internal/types"
)
func TestGetDiscount_Coefficient(t *testing.T) {
discounts := []types.SubscribeDiscount{{Quantity: 7, Discount: 0.95}}
got := getDiscount(discounts, 7)
if got != 0.95 {
t.Fatalf("expected 0.95, got %v", got)
}
}
func TestGetDiscount_Percentage(t *testing.T) {
discounts := []types.SubscribeDiscount{{Quantity: 7, Discount: 95}}
got := getDiscount(discounts, 7)
if got != 0.95 {
t.Fatalf("expected 0.95 from 95%%, got %v", got)
}
}
func TestGetDiscount_InvalidOver100(t *testing.T) {
discounts := []types.SubscribeDiscount{{Quantity: 7, Discount: 120}}
got := getDiscount(discounts, 7)
if got != 1.0 {
t.Fatalf("expected 1.0 when invalid >100, got %v", got)
}
}

View File

@ -3,6 +3,7 @@ package order
import (
"context"
"encoding/json"
"math"
"github.com/perfect-panel/server/internal/model/order"
"github.com/perfect-panel/server/pkg/tool"
@ -63,7 +64,7 @@ func (l *PreCreateOrderLogic) PreCreateOrder(req *types.PurchaseOrderRequest) (r
}
price := sub.UnitPrice * req.Quantity
amount := int64(float64(price) * discount)
amount := int64(math.Round(float64(price) * discount))
discountAmount := price - amount
var couponAmount int64
if req.Coupon != "" {

View File

@ -3,6 +3,7 @@ package order
import (
"context"
"encoding/json"
"math"
"time"
"github.com/perfect-panel/server/internal/model/log"
@ -102,7 +103,7 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P
}
price := sub.UnitPrice * req.Quantity
// discount amount
amount := int64(float64(price) * discount)
amount := int64(math.Round(float64(price) * discount))
discountAmount := price - amount
var coupon int64 = 0
// Calculate the coupon deduction

View File

@ -3,6 +3,7 @@ package portal
import (
"context"
"encoding/json"
"math"
"github.com/perfect-panel/server/pkg/tool"
@ -43,7 +44,7 @@ func (l *PrePurchaseOrderLogic) PrePurchaseOrder(req *types.PrePurchaseOrderRequ
discount = getDiscount(dis, req.Quantity)
}
price := sub.UnitPrice * req.Quantity
amount := int64(float64(price) * discount)
amount := int64(math.Round(float64(price) * discount))
discountAmount := price - amount
var coupon int64
if req.Coupon != "" {

View File

@ -3,6 +3,7 @@ package portal
import (
"context"
"encoding/json"
"math"
"strconv"
"time"
@ -184,7 +185,7 @@ func (l *PurchaseCheckoutLogic) alipayF2fPayment(pay *payment.Payment, info *ord
l.Errorw("[PurchaseCheckout] queryExchangeRate error", logger.Field("error", err.Error()))
return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "queryExchangeRate error: %s", err.Error())
}
convertAmount := int64(amount * 100) // Convert to cents for API
convertAmount := int64(math.Round(amount * 100)) // Convert to cents for API
// Create pre-payment trade and generate QR code
QRCode, err := client.PreCreateTrade(l.ctx, alipay.Order{
@ -222,7 +223,7 @@ func (l *PurchaseCheckoutLogic) stripePayment(config string, info *order.Order,
l.Errorw("[PurchaseCheckout] queryExchangeRate error", logger.Field("error", err.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "queryExchangeRate error: %s", err.Error())
}
convertAmount := int64(amount * 100) // Convert to cents for Stripe API
convertAmount := int64(math.Round(amount * 100)) // Convert to cents for Stripe API
// Create Stripe payment sheet for client-side processing
result, err := client.CreatePaymentSheet(&stripe.Order{

View File

@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"math"
"time"
"github.com/perfect-panel/server/internal/model/order"
@ -67,7 +68,7 @@ func (l *PurchaseLogic) Purchase(req *types.PortalPurchaseRequest) (resp *types.
}
price := sub.UnitPrice * req.Quantity
// discount amount
amount := int64(float64(price) * discount)
amount := int64(math.Round(float64(price) * discount))
discountAmount := price - amount
var couponAmount int64 = 0

View File

@ -1,25 +1,33 @@
package portal
import (
"math"
"github.com/perfect-panel/server/internal/model/coupon"
"github.com/perfect-panel/server/internal/model/payment"
"github.com/perfect-panel/server/internal/types"
)
func getDiscount(discounts []types.SubscribeDiscount, inputMonths int64) float64 {
var finalDiscount int64 = 100
for _, discount := range discounts {
if inputMonths >= discount.Quantity && int64(discount.Discount*100) < finalDiscount {
finalDiscount = int64(discount.Discount * 100)
final := 1.0
for _, d := range discounts {
val := d.Discount
if val > 1 && val <= 100 {
val = val / 100.0
}
if val < 0 {
val = 0
}
if inputMonths >= d.Quantity && val < final {
final = val
}
}
return float64(finalDiscount) / float64(100)
return final
}
func calculateCoupon(amount int64, couponInfo *coupon.Coupon) int64 {
if couponInfo.Type == 1 {
return int64(float64(amount) * (float64(couponInfo.Discount) / float64(100)))
return int64(math.Round(float64(amount) * (float64(couponInfo.Discount) / float64(100))))
} else {
return min(couponInfo.Discount, amount)
}
@ -39,5 +47,5 @@ func calculateFee(amount int64, config *payment.Payment) int64 {
case 3:
fee = float64(amount)*(float64(config.FeePercent)/float64(100)) + float64(config.FeeAmount)
}
return int64(fee)
return int64(math.Round(fee))
}

View File

@ -0,0 +1,23 @@
package portal
import (
"testing"
"github.com/perfect-panel/server/internal/types"
)
func TestGetDiscount_Coefficient(t *testing.T) {
discounts := []types.SubscribeDiscount{{Quantity: 7, Discount: 0.9}}
got := getDiscount(discounts, 7)
if got != 0.9 {
t.Fatalf("expected 0.9, got %v", got)
}
}
func TestGetDiscount_Percentage(t *testing.T) {
discounts := []types.SubscribeDiscount{{Quantity: 7, Discount: 90}}
got := getDiscount(discounts, 7)
if got != 0.9 {
t.Fatalf("expected 0.9 from 90%%, got %v", got)
}
}

View File

@ -84,7 +84,8 @@ func NewServiceContext(c config.Config) *ServiceContext {
err = rds.Ping(context.Background()).Err()
if err != nil {
panic(err.Error())
} else {
}
if c.Debug {
_ = rds.FlushAll(context.Background()).Err()
}
authLimiter := limit.NewPeriodLimit(86400, 15, rds, config.SendCountLimitKeyPrefix, limit.Align())