refactor(money): 统一金额分为纯整数工具
This commit is contained in:
@@ -8,7 +8,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
"hfb_sys/backend/pkg/money"
|
||||
|
||||
"gorm.io/datatypes"
|
||||
)
|
||||
@@ -462,7 +461,11 @@ func extractListingObjectKey(fileURL string) string {
|
||||
return key
|
||||
}
|
||||
|
||||
// roundMoney 使用统一的角精度(0.1元)
|
||||
// roundMoney 是历史外部上传/审核链路的兼容实现,仍按 0.1 元精度工作。
|
||||
// 卖家发布链路已改走整数分/基点计算,不再调用该函数。
|
||||
func roundMoney(value float64) float64 {
|
||||
return money.Round(value)
|
||||
if value <= 0 || math.IsNaN(value) || math.IsInf(value, 0) {
|
||||
return 0
|
||||
}
|
||||
return math.Round(value*10) / 10
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
"hfb_sys/backend/internal/modules/rentergrowth"
|
||||
"hfb_sys/backend/pkg/money"
|
||||
|
||||
"gorm.io/datatypes"
|
||||
)
|
||||
@@ -486,23 +485,32 @@ func readOrderSnapshotAssetNumber(raw datatypes.JSON, key string) float64 {
|
||||
return readJSONNumber(snapshot[key])
|
||||
}
|
||||
|
||||
// roundMoney 使用统一的角精度(0.1元)
|
||||
// roundMoney 是订单历史链路的角精度兼容实现;新金额链路应直接使用整数分。
|
||||
func roundMoney(value float64) float64 {
|
||||
return money.Round(value)
|
||||
if value <= 0 || math.IsNaN(value) || math.IsInf(value, 0) {
|
||||
return 0
|
||||
}
|
||||
return math.Round(value*10) / 10
|
||||
}
|
||||
|
||||
func roundQuantity(value float64) float64 {
|
||||
return math.Round(value*100) / 100
|
||||
}
|
||||
|
||||
// minMoney 返回较小金额(角精度)
|
||||
// minMoney 返回较小金额(角精度兼容)。
|
||||
func minMoney(a float64, b float64) float64 {
|
||||
return money.Min(a, b)
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// maxMoney 返回较大金额(角精度)
|
||||
// maxMoney 返回较大金额(角精度兼容)。
|
||||
func maxMoney(a float64, b float64) float64 {
|
||||
return money.Max(a, b)
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func minCent(a int64, b int64) int64 {
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
"hfb_sys/backend/internal/timeutil"
|
||||
"hfb_sys/backend/pkg/money"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
@@ -264,10 +263,6 @@ func applyEntry(account *model.WalletAccount, entry Entry) (int64, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func roundWalletMoney(yuan float64) float64 {
|
||||
return money.Round(yuan)
|
||||
}
|
||||
|
||||
func toAccountDTO(account model.WalletAccount) *AccountDTO {
|
||||
return &AccountDTO{
|
||||
UserID: account.UserID,
|
||||
|
||||
@@ -53,10 +53,6 @@ func TestApplyEntryKeepsWalletMoneyAtJiaoPrecision(t *testing.T) {
|
||||
if account.AvailableBalanceCent != 230 {
|
||||
t.Fatalf("AvailableBalanceCent = %d, want 230", account.AvailableBalanceCent)
|
||||
}
|
||||
|
||||
if rounded := roundWalletMoney(2.36); rounded != 2.4 {
|
||||
t.Fatalf("roundWalletMoney(2.36) = %v, want 2.4", rounded)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewLedgerNoUsesReadableFormat(t *testing.T) {
|
||||
|
||||
+65
-41
@@ -1,58 +1,83 @@
|
||||
package money
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Round 将金额四舍五入到角(0.1元),统一全项目金额精度
|
||||
// 例如:12.34 -> 12.3, 12.36 -> 12.4, 12.35 -> 12.4
|
||||
func Round(value float64) float64 {
|
||||
return math.Round(value*10) / 10
|
||||
// CentsPerYuan 是人民币元与分的固定换算比例。
|
||||
const CentsPerYuan int64 = 100
|
||||
|
||||
var ErrInvalidYuan = errors.New("金额格式不正确")
|
||||
|
||||
// ParseYuan 将用户输入的元字符串精确解析为分,不经过 float64。
|
||||
// 支持 "12"、"12.3"、"12.34"、"-1.20";超过两位小数直接拒绝。
|
||||
func ParseYuan(value string) (int64, error) {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
if trimmed == "" {
|
||||
return 0, ErrInvalidYuan
|
||||
}
|
||||
sign := int64(1)
|
||||
if strings.HasPrefix(trimmed, "-") {
|
||||
sign = -1
|
||||
trimmed = strings.TrimPrefix(trimmed, "-")
|
||||
} else if strings.HasPrefix(trimmed, "+") {
|
||||
trimmed = strings.TrimPrefix(trimmed, "+")
|
||||
}
|
||||
if trimmed == "" {
|
||||
return 0, ErrInvalidYuan
|
||||
}
|
||||
parts := strings.Split(trimmed, ".")
|
||||
if len(parts) > 2 {
|
||||
return 0, ErrInvalidYuan
|
||||
}
|
||||
wholeText := parts[0]
|
||||
if wholeText == "" {
|
||||
wholeText = "0"
|
||||
}
|
||||
whole, err := strconv.ParseInt(wholeText, 10, 64)
|
||||
if err != nil || whole < 0 {
|
||||
return 0, ErrInvalidYuan
|
||||
}
|
||||
fraction := int64(0)
|
||||
if len(parts) == 2 {
|
||||
fracText := parts[1]
|
||||
if len(fracText) > 2 {
|
||||
return 0, ErrInvalidYuan
|
||||
}
|
||||
fracText = fracText + strings.Repeat("0", 2-len(fracText))
|
||||
if fracText == "" {
|
||||
fracText = "00"
|
||||
}
|
||||
fraction, err = strconv.ParseInt(fracText, 10, 64)
|
||||
if err != nil || fraction < 0 || fraction >= CentsPerYuan {
|
||||
return 0, ErrInvalidYuan
|
||||
}
|
||||
}
|
||||
if whole > (1<<62)/CentsPerYuan {
|
||||
return 0, ErrInvalidYuan
|
||||
}
|
||||
return sign * (whole*CentsPerYuan + fraction), nil
|
||||
}
|
||||
|
||||
// ToCent 将元转换为分(整数),用于存储。
|
||||
// 12.34 元 -> 1234 分
|
||||
func ToCent(yuan float64) int64 {
|
||||
return int64(math.Round(yuan * 100))
|
||||
}
|
||||
|
||||
// ToDisplayYuan 将分转换为按角精度展示的元值。
|
||||
// 1234 分 -> 12.3 元
|
||||
func ToDisplayYuan(cent int64) float64 {
|
||||
return Round(float64(cent) / 100)
|
||||
}
|
||||
|
||||
// Format 格式化分为字符串(角精度,保留1位小数)
|
||||
// 1234 分 -> "12.3"
|
||||
// 1230 分 -> "12.3"
|
||||
// Format 将分精确格式化为两位小数,例如 1234 -> "12.34"。
|
||||
// 全程使用整数运算,不经过 float64,也不做四舍五入。
|
||||
func Format(cent int64) string {
|
||||
return fmt.Sprintf("%.1f", ToDisplayYuan(cent))
|
||||
sign := ""
|
||||
value := cent
|
||||
if value < 0 {
|
||||
sign = "-"
|
||||
value = -value
|
||||
}
|
||||
return fmt.Sprintf("%s%d.%02d", sign, value/CentsPerYuan, value%CentsPerYuan)
|
||||
}
|
||||
|
||||
// FormatWithSymbol 格式化分为带符号的字符串
|
||||
// 123分 -> "¥12.3"
|
||||
func FormatWithSymbol(cent int64) string {
|
||||
return "¥" + Format(cent)
|
||||
}
|
||||
|
||||
// Min 返回两个金额中较小的值(角精度,兼容旧代码)
|
||||
func Min(a, b float64) float64 {
|
||||
if a < b {
|
||||
return Round(a)
|
||||
}
|
||||
return Round(b)
|
||||
}
|
||||
|
||||
// Max 返回两个金额中较大的值(角精度,兼容旧代码)
|
||||
func Max(a, b float64) float64 {
|
||||
if a > b {
|
||||
return Round(a)
|
||||
}
|
||||
return Round(b)
|
||||
}
|
||||
|
||||
// MinCent 返回两个分值中较小的值
|
||||
func MinCent(a, b int64) int64 {
|
||||
if a < b {
|
||||
return a
|
||||
@@ -60,7 +85,6 @@ func MinCent(a, b int64) int64 {
|
||||
return b
|
||||
}
|
||||
|
||||
// MaxCent 返回两个分值中较大的值
|
||||
func MaxCent(a, b int64) int64 {
|
||||
if a > b {
|
||||
return a
|
||||
|
||||
@@ -2,49 +2,37 @@ package money
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestToCent(t *testing.T) {
|
||||
func TestParseYuan(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
yuan float64
|
||||
want int64
|
||||
name string
|
||||
value string
|
||||
want int64
|
||||
}{
|
||||
{"12.3元转1230分", 12.3, 1230},
|
||||
{"12.34元转1234分", 12.34, 1234},
|
||||
{"12.36元转1236分", 12.36, 1236},
|
||||
{"0.1元转10分", 0.1, 10},
|
||||
{"100元转10000分", 100.0, 10000},
|
||||
{"零值", 0.0, 0},
|
||||
{"整数", "100", 10000},
|
||||
{"一位小数", "12.3", 1230},
|
||||
{"两位小数", "12.34", 1234},
|
||||
{"前导零", "0.05", 5},
|
||||
{"负金额", "-1.20", -120},
|
||||
{"正号", "+0.1", 10},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := ToCent(tt.yuan)
|
||||
got, err := ParseYuan(tt.value)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseYuan(%q) error: %v", tt.value, err)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("ToCent(%v) = %v, want %v", tt.yuan, got, tt.want)
|
||||
t.Fatalf("ParseYuan(%q) = %d, want %d", tt.value, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestToDisplayYuan(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cent int64
|
||||
want float64
|
||||
}{
|
||||
{"123分展示1.2元", 123, 1.2},
|
||||
{"1234分展示12.3元", 1234, 12.3},
|
||||
{"1236分展示12.4元", 1236, 12.4},
|
||||
{"10分展示0.1元", 10, 0.1},
|
||||
{"1000分展示10.0元", 1000, 10.0},
|
||||
{"零值", 0, 0.0},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := ToDisplayYuan(tt.cent)
|
||||
if got != tt.want {
|
||||
t.Errorf("ToDisplayYuan(%v) = %v, want %v", tt.cent, got, tt.want)
|
||||
}
|
||||
})
|
||||
func TestParseYuanRejectsInvalid(t *testing.T) {
|
||||
for _, value := range []string{"", "12.345", "abc", "1.2.3", "1.-2"} {
|
||||
if _, err := ParseYuan(value); err == nil {
|
||||
t.Fatalf("ParseYuan(%q) 应返回错误", value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,105 +42,33 @@ func TestFormat(t *testing.T) {
|
||||
cent int64
|
||||
want string
|
||||
}{
|
||||
{"123分格式化", 123, "1.2"},
|
||||
{"1234分格式化", 1234, "12.3"},
|
||||
{"1236分格式化", 1236, "12.4"},
|
||||
{"10分格式化", 10, "0.1"},
|
||||
{"1000分格式化", 1000, "10.0"},
|
||||
{"零值格式化", 0, "0.0"},
|
||||
{"两位小数", 1234, "12.34"},
|
||||
{"末尾零", 1230, "12.30"},
|
||||
{"五分", 5, "0.05"},
|
||||
{"整数", 10000, "100.00"},
|
||||
{"负数", -120, "-1.20"},
|
||||
{"零值", 0, "0.00"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := Format(tt.cent)
|
||||
if got != tt.want {
|
||||
t.Errorf("Format(%v) = %v, want %v", tt.cent, got, tt.want)
|
||||
if got := Format(tt.cent); got != tt.want {
|
||||
t.Fatalf("Format(%d) = %q, want %q", tt.cent, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatWithSymbol(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cent int64
|
||||
want string
|
||||
}{
|
||||
{"123分带符号", 123, "¥1.2"},
|
||||
{"1000分带符号", 1000, "¥10.0"},
|
||||
{"零值带符号", 0, "¥0.0"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := FormatWithSymbol(tt.cent)
|
||||
if got != tt.want {
|
||||
t.Errorf("FormatWithSymbol(%v) = %v, want %v", tt.cent, got, tt.want)
|
||||
}
|
||||
})
|
||||
if got := FormatWithSymbol(1234); got != "¥12.34" {
|
||||
t.Fatalf("FormatWithSymbol(1234) = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRound(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value float64
|
||||
want float64
|
||||
}{
|
||||
{"12.34舍入到12.3", 12.34, 12.3},
|
||||
{"12.36舍入到12.4", 12.36, 12.4},
|
||||
{"12.35舍入到12.4", 12.35, 12.4},
|
||||
{"0.05舍入到0.1", 0.05, 0.1},
|
||||
{"0.04舍入到0.0", 0.04, 0.0},
|
||||
func TestMinMaxCent(t *testing.T) {
|
||||
if got := MinCent(100, 200); got != 100 {
|
||||
t.Fatalf("MinCent = %d", got)
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := Round(tt.value)
|
||||
if got != tt.want {
|
||||
t.Errorf("Round(%v) = %v, want %v", tt.value, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMinMax(t *testing.T) {
|
||||
t.Run("Min", func(t *testing.T) {
|
||||
if got := Min(100, 200); got != 100 {
|
||||
t.Errorf("Min(100, 200) = %v, want 100", got)
|
||||
}
|
||||
if got := Min(200, 100); got != 100 {
|
||||
t.Errorf("Min(200, 100) = %v, want 100", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Max", func(t *testing.T) {
|
||||
if got := Max(100, 200); got != 200 {
|
||||
t.Errorf("Max(100, 200) = %v, want 200", got)
|
||||
}
|
||||
if got := Max(200, 100); got != 200 {
|
||||
t.Errorf("Max(200, 100) = %v, want 200", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 测试双向转换的精度
|
||||
func TestRoundTrip(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
originalYuan float64
|
||||
expectDisplay float64
|
||||
}{
|
||||
{"12.3元往返", 12.3, 12.3},
|
||||
{"12.34元往返(展示到角)", 12.34, 12.3},
|
||||
{"100.0元往返", 100.0, 100.0},
|
||||
{"0.1元往返", 0.1, 0.1},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cent := ToCent(tt.originalYuan)
|
||||
got := ToDisplayYuan(cent)
|
||||
if got != tt.expectDisplay {
|
||||
t.Errorf("往返转换:%v元 -> %v分 -> %v元, 期望 %v元",
|
||||
tt.originalYuan, cent, got, tt.expectDisplay)
|
||||
}
|
||||
})
|
||||
if got := MaxCent(100, 200); got != 200 {
|
||||
t.Fatalf("MaxCent = %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user