package model import ( "database/sql/driver" "encoding/json" "fmt" "math/big" "strings" ) // Decimal24_2 用规范十进制文本承载 MySQL DECIMAL(24,2),避免金额经过 float64 丢精度。 // JSON 输出字符串以保护 22 位整数精度;数据库读写通过 Scanner/Valuer 完成。 type Decimal24_2 string func ParseDecimal24_2(value string) (Decimal24_2, error) { value = strings.TrimSpace(value) if value == "" { return "", fmt.Errorf("decimal is blank") } negative := false if value[0] == '-' { negative, value = true, value[1:] } else if value[0] == '+' { return "", fmt.Errorf("decimal plus sign is not supported") } if value == "" || strings.ContainsAny(value, "eE") { return "", fmt.Errorf("invalid decimal") } parts := strings.Split(value, ".") if len(parts) > 2 || parts[0] == "" { return "", fmt.Errorf("invalid decimal") } for _, char := range parts[0] { if char < '0' || char > '9' { return "", fmt.Errorf("invalid decimal") } } integer := strings.TrimLeft(parts[0], "0") if integer == "" { integer = "0" } fraction := "" if len(parts) == 2 { fraction = parts[1] for _, char := range fraction { if char < '0' || char > '9' { return "", fmt.Errorf("invalid decimal") } } // BigDecimal.stripTrailingZeros 后 scale<=2 的值均可接受,例如 1.2300。 if len(fraction) > 2 && strings.Trim(fraction[2:], "0") != "" { return "", fmt.Errorf("decimal scale exceeds 2") } } if len(integer) > 22 { return "", fmt.Errorf("decimal integer digits exceed 22") } if len(fraction) > 2 { fraction = fraction[:2] } fraction += strings.Repeat("0", 2-len(fraction)) if integer == "0" && fraction == "00" { negative = false } prefix := "" if negative { prefix = "-" } return Decimal24_2(prefix + integer + "." + fraction), nil } func (d *Decimal24_2) UnmarshalJSON(data []byte) error { raw := strings.TrimSpace(string(data)) if raw == "" || raw == "null" || raw == `""` { *d = DecimalZero() return nil } if strings.HasPrefix(raw, `"`) && strings.HasSuffix(raw, `"`) { var value string if err := json.Unmarshal(data, &value); err != nil { return err } raw = value } parsed, err := ParseDecimal24_2(raw) if err != nil { return err } *d = parsed return nil } func (d Decimal24_2) MarshalJSON() ([]byte, error) { return json.Marshal(d.String()) } func (d *Decimal24_2) Scan(value any) error { var raw string switch typed := value.(type) { case nil: raw = "0" case []byte: raw = string(typed) case string: raw = typed case int64: raw = fmt.Sprintf("%d", typed) default: raw = fmt.Sprint(typed) } parsed, err := ParseDecimal24_2(raw) if err != nil { return err } *d = parsed return nil } func (d Decimal24_2) Value() (driver.Value, error) { return d.String(), nil } func (d Decimal24_2) String() string { if strings.TrimSpace(string(d)) == "" { return "0.00" } return string(d) } func DecimalZero() Decimal24_2 { return Decimal24_2("0.00") } func (d Decimal24_2) Positive() bool { return d.cents().Sign() > 0 } func (d Decimal24_2) Compare(other Decimal24_2) int { return d.cents().Cmp(other.cents()) } func (d Decimal24_2) MultiplyInt(multiplier int64) (Decimal24_2, error) { if multiplier < 0 { return "", fmt.Errorf("decimal multiplier must not be negative") } value := new(big.Int).Mul(d.cents(), big.NewInt(multiplier)) if len(new(big.Int).Abs(new(big.Int).Set(value)).String()) > 24 { return "", fmt.Errorf("decimal exceeds DECIMAL(24,2)") } return decimalFromCents(value), nil } func (d Decimal24_2) cents() *big.Int { raw := d.String() negative := strings.HasPrefix(raw, "-") raw = strings.TrimPrefix(raw, "-") digits := strings.ReplaceAll(raw, ".", "") value, ok := new(big.Int).SetString(digits, 10) if !ok { return new(big.Int) } if negative { value.Neg(value) } return value } func decimalFromCents(value *big.Int) Decimal24_2 { negative := value.Sign() < 0 abs := new(big.Int).Abs(new(big.Int).Set(value)).String() if len(abs) < 3 { abs = strings.Repeat("0", 3-len(abs)) + abs } result := abs[:len(abs)-2] + "." + abs[len(abs)-2:] if negative && result != "0.00" { result = "-" + result } return Decimal24_2(result) }