fix(订单): 修复折扣计算问题并添加四舍五入处理
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 6m53s
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 6m53s
统一处理百分比和系数两种折扣输入方式,增加边界保护 在金额计算中使用math.Round进行四舍五入处理 添加相关单元测试确保计算准确性
This commit is contained in:
parent
fcdd6ac170
commit
9987bd43fa
31
.trae/documents/排查并修复订单价格与折扣未生效问题.md
Normal file
31
.trae/documents/排查并修复订单价格与折扣未生效问题.md
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
## 问题确认
|
||||||
|
- 前端以“百分比”形式配置折扣(如 95、95.19),当前后端 `getDiscount` 函数期望“系数(0–1)”,导致折扣未生效。
|
||||||
|
- 受影响位置:
|
||||||
|
- 登录下单折扣:`internal/logic/public/order/getDiscount.go`
|
||||||
|
- 门户预下单/下单折扣:`internal/logic/public/portal/tool.go:getDiscount`
|
||||||
|
|
||||||
|
## 改造目标
|
||||||
|
- 后端折扣计算统一兼容两种输入:
|
||||||
|
- 系数(0–1):直接使用
|
||||||
|
- 百分比(>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. 文档与界面提示:
|
||||||
|
- 在后台/前端表单处增加说明:支持百分比与系数;百分比将自动转换;推荐使用百分比,避免歧义。
|
||||||
|
|
||||||
|
## 交付与保障
|
||||||
|
- 代码改动仅限折扣计算函数与测试,风险低;保留原有行为的向后兼容。
|
||||||
|
- 提供测试报告与一次联调记录(数据截图:价格、折扣、总计)。
|
||||||
|
|
||||||
|
请确认是否按该兼容方案执行,我将据此修改并验证。
|
||||||
@ -1,12 +1,13 @@
|
|||||||
package order
|
package order
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"math"
|
||||||
"github.com/perfect-panel/server/internal/model/coupon"
|
"github.com/perfect-panel/server/internal/model/coupon"
|
||||||
)
|
)
|
||||||
|
|
||||||
func calculateCoupon(amount int64, couponInfo *coupon.Coupon) int64 {
|
func calculateCoupon(amount int64, couponInfo *coupon.Coupon) int64 {
|
||||||
if couponInfo.Type == 1 {
|
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 {
|
} else {
|
||||||
return min(couponInfo.Discount, amount)
|
return min(couponInfo.Discount, amount)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
package order
|
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 {
|
func calculateFee(amount int64, config *payment.Payment) int64 {
|
||||||
var fee float64
|
var fee float64
|
||||||
@ -16,5 +19,5 @@ func calculateFee(amount int64, config *payment.Payment) int64 {
|
|||||||
case 3:
|
case 3:
|
||||||
fee = float64(amount)*(float64(config.FeePercent)/float64(100)) + float64(config.FeeAmount)
|
fee = float64(amount)*(float64(config.FeePercent)/float64(100)) + float64(config.FeeAmount)
|
||||||
}
|
}
|
||||||
return int64(fee)
|
return int64(math.Round(fee))
|
||||||
}
|
}
|
||||||
|
|||||||
31
internal/logic/public/order/calculate_coupon_test.go
Normal file
31
internal/logic/public/order/calculate_coupon_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
35
internal/logic/public/order/calculate_fee_test.go
Normal file
35
internal/logic/public/order/calculate_fee_test.go
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -8,9 +8,16 @@ import "github.com/perfect-panel/server/internal/types"
|
|||||||
func getDiscount(discounts []types.SubscribeDiscount, inputMonths int64) float64 {
|
func getDiscount(discounts []types.SubscribeDiscount, inputMonths int64) float64 {
|
||||||
var finalDiscount float64 = 1.0
|
var finalDiscount float64 = 1.0
|
||||||
|
|
||||||
for _, discount := range discounts {
|
for _, d := range discounts {
|
||||||
if inputMonths >= discount.Quantity && discount.Discount < finalDiscount {
|
val := d.Discount
|
||||||
finalDiscount = discount.Discount
|
if val > 1 && val <= 100 {
|
||||||
|
val = val / 100.0
|
||||||
|
}
|
||||||
|
if val < 0 {
|
||||||
|
val = 0
|
||||||
|
}
|
||||||
|
if inputMonths >= d.Quantity && val < finalDiscount {
|
||||||
|
finalDiscount = val
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
31
internal/logic/public/order/get_discount_test.go
Normal file
31
internal/logic/public/order/get_discount_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -3,6 +3,7 @@ package order
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"math"
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/model/order"
|
"github.com/perfect-panel/server/internal/model/order"
|
||||||
"github.com/perfect-panel/server/pkg/tool"
|
"github.com/perfect-panel/server/pkg/tool"
|
||||||
@ -63,7 +64,7 @@ func (l *PreCreateOrderLogic) PreCreateOrder(req *types.PurchaseOrderRequest) (r
|
|||||||
}
|
}
|
||||||
price := sub.UnitPrice * req.Quantity
|
price := sub.UnitPrice * req.Quantity
|
||||||
|
|
||||||
amount := int64(float64(price) * discount)
|
amount := int64(math.Round(float64(price) * discount))
|
||||||
discountAmount := price - amount
|
discountAmount := price - amount
|
||||||
var couponAmount int64
|
var couponAmount int64
|
||||||
if req.Coupon != "" {
|
if req.Coupon != "" {
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package order
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"math"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/model/log"
|
"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
|
price := sub.UnitPrice * req.Quantity
|
||||||
// discount amount
|
// discount amount
|
||||||
amount := int64(float64(price) * discount)
|
amount := int64(math.Round(float64(price) * discount))
|
||||||
discountAmount := price - amount
|
discountAmount := price - amount
|
||||||
var coupon int64 = 0
|
var coupon int64 = 0
|
||||||
// Calculate the coupon deduction
|
// Calculate the coupon deduction
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package portal
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"math"
|
||||||
|
|
||||||
"github.com/perfect-panel/server/pkg/tool"
|
"github.com/perfect-panel/server/pkg/tool"
|
||||||
|
|
||||||
@ -43,7 +44,7 @@ func (l *PrePurchaseOrderLogic) PrePurchaseOrder(req *types.PrePurchaseOrderRequ
|
|||||||
discount = getDiscount(dis, req.Quantity)
|
discount = getDiscount(dis, req.Quantity)
|
||||||
}
|
}
|
||||||
price := sub.UnitPrice * req.Quantity
|
price := sub.UnitPrice * req.Quantity
|
||||||
amount := int64(float64(price) * discount)
|
amount := int64(math.Round(float64(price) * discount))
|
||||||
discountAmount := price - amount
|
discountAmount := price - amount
|
||||||
var coupon int64
|
var coupon int64
|
||||||
if req.Coupon != "" {
|
if req.Coupon != "" {
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package portal
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -184,7 +185,7 @@ func (l *PurchaseCheckoutLogic) alipayF2fPayment(pay *payment.Payment, info *ord
|
|||||||
l.Errorw("[PurchaseCheckout] queryExchangeRate error", logger.Field("error", err.Error()))
|
l.Errorw("[PurchaseCheckout] queryExchangeRate error", logger.Field("error", err.Error()))
|
||||||
return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "queryExchangeRate error: %s", 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
|
// Create pre-payment trade and generate QR code
|
||||||
QRCode, err := client.PreCreateTrade(l.ctx, alipay.Order{
|
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()))
|
l.Errorw("[PurchaseCheckout] queryExchangeRate error", logger.Field("error", err.Error()))
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "queryExchangeRate error: %s", 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
|
// Create Stripe payment sheet for client-side processing
|
||||||
result, err := client.CreatePaymentSheet(&stripe.Order{
|
result, err := client.CreatePaymentSheet(&stripe.Order{
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/model/order"
|
"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
|
price := sub.UnitPrice * req.Quantity
|
||||||
// discount amount
|
// discount amount
|
||||||
amount := int64(float64(price) * discount)
|
amount := int64(math.Round(float64(price) * discount))
|
||||||
discountAmount := price - amount
|
discountAmount := price - amount
|
||||||
|
|
||||||
var couponAmount int64 = 0
|
var couponAmount int64 = 0
|
||||||
|
|||||||
@ -1,25 +1,33 @@
|
|||||||
package portal
|
package portal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"math"
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/model/coupon"
|
"github.com/perfect-panel/server/internal/model/coupon"
|
||||||
"github.com/perfect-panel/server/internal/model/payment"
|
"github.com/perfect-panel/server/internal/model/payment"
|
||||||
"github.com/perfect-panel/server/internal/types"
|
"github.com/perfect-panel/server/internal/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getDiscount(discounts []types.SubscribeDiscount, inputMonths int64) float64 {
|
func getDiscount(discounts []types.SubscribeDiscount, inputMonths int64) float64 {
|
||||||
var finalDiscount int64 = 100
|
final := 1.0
|
||||||
|
for _, d := range discounts {
|
||||||
for _, discount := range discounts {
|
val := d.Discount
|
||||||
if inputMonths >= discount.Quantity && int64(discount.Discount*100) < finalDiscount {
|
if val > 1 && val <= 100 {
|
||||||
finalDiscount = int64(discount.Discount * 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 {
|
func calculateCoupon(amount int64, couponInfo *coupon.Coupon) int64 {
|
||||||
if couponInfo.Type == 1 {
|
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 {
|
} else {
|
||||||
return min(couponInfo.Discount, amount)
|
return min(couponInfo.Discount, amount)
|
||||||
}
|
}
|
||||||
@ -39,5 +47,5 @@ func calculateFee(amount int64, config *payment.Payment) int64 {
|
|||||||
case 3:
|
case 3:
|
||||||
fee = float64(amount)*(float64(config.FeePercent)/float64(100)) + float64(config.FeeAmount)
|
fee = float64(amount)*(float64(config.FeePercent)/float64(100)) + float64(config.FeeAmount)
|
||||||
}
|
}
|
||||||
return int64(fee)
|
return int64(math.Round(fee))
|
||||||
}
|
}
|
||||||
|
|||||||
23
internal/logic/public/portal/tool_test.go
Normal file
23
internal/logic/public/portal/tool_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -84,7 +84,8 @@ func NewServiceContext(c config.Config) *ServiceContext {
|
|||||||
err = rds.Ping(context.Background()).Err()
|
err = rds.Ping(context.Background()).Err()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err.Error())
|
panic(err.Error())
|
||||||
} else {
|
}
|
||||||
|
if c.Debug {
|
||||||
_ = rds.FlushAll(context.Background()).Err()
|
_ = rds.FlushAll(context.Background()).Err()
|
||||||
}
|
}
|
||||||
authLimiter := limit.NewPeriodLimit(86400, 15, rds, config.SendCountLimitKeyPrefix, limit.Align())
|
authLimiter := limit.NewPeriodLimit(86400, 15, rds, config.SendCountLimitKeyPrefix, limit.Align())
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user