buguge - Keep it simple,stupid

知识就是力量,但更重要的,是运用知识的能力why buguge?

导航

< 2025年2月 >
26 27 28 29 30 31 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 1
2 3 4 5 6 7 8

统计

来了,资金类交易业务(如电商交易、支付结算)中,经常提到的Money类!

 

资金类交易业务中 经常提到的Money类,大家了解一下。 了解了Money类,就会对资金类业务如电商交易、支付更了解。

资金类业务中,金额如果处理得不好,带来的直接后果就是资金损失(资损风险)。

对于研发经验不足的团队而言,经常会犯以下几种错误:

不统一,存在各系统使用BigDecimal、double、long等数据类型来定义金额。

手动对金额进行加、减、乘、除运算,单位(元与分)换算。

带来的后果,通常就是资金损失,再细化一下,最常见的情况有下面3种:

1)手动做单位换算导致金额被放大或缩小100倍。比如大家规定传的是元,但是其中有位同学忘记了,以为传的是分,外部渠道要求传元,就手动乘以100。或者反过来。

2)1分钱归属问题。比如结算给商家,或计算手续费时,碰到除不尽时,使用四舍五入,还是向零舍入,还是银行家舍入?这取决于财务策略。

3)精度丢失。在大金额时,double有可能会有精度丢失问题。

这时候,解决方案就是定义统一的 Money 类。并且,要明确统一使用 Money 类来处理金额数据。

回到顶部

1. 定义统一的Money 类

制定适用于公司业务的Money类来统一处理金额。

我司是灵活用工类企业服务平台,money不涉及外币,因此我们的 Money 类不用涉及 java.util.Currency,只关注对人民币币种金额的处理。

talk is cheap , show you the code directly.

点击查看代码
package com.emaxpay.common.trans;

import cn.hutool.core.lang.Assert;
import lombok.Getter;

import java.io.Serializable;
import java.math.BigDecimal;
import java.math.RoundingMode;

/**
 * 资金类交易业务(如电商交易、支付)中,经常提到的Money类。
 * <p>该类是不可变的(immutable),并且实现了Comparable和Serializable接口。
 * <p><a href="https://mp.weixin.qq.com/s/34JEFRrV_b6J_8Z_O3MbLg">原始文章</a>
 *
 * @author zhangguozhan
 * @date 2025-01-07 14:40
 */
@Getter
public class Money implements Serializable, Comparable<Money> {
    private static final long serialVersionUID = 1L;

    private BigDecimal yuan;
    private long fen;


    /**
     * 私有构造函数,确保通过工厂方法{@link #of}来创建实例。
     *
     * @param yuan
     * @param fen
     */
    private Money(BigDecimal yuan, long fen) {
        this.yuan = yuan;
        this.fen = fen;
    }

    public static Money of(BigDecimal yuan) {
        Assert.notNull(yuan, "yuan is null");
        return new Money(yuan, yuan.movePointRight(2).longValue());
    }

    public static Money of(Long fen) {
        Assert.notNull(fen, "fen is null");
        return new Money(BigDecimal.valueOf(fen * 100), fen);
    }

    /**
     * 加法操作,返回新的Money实例。(注意Money类是不可变类,此操作不会修改当前Money对象的属性值)
     *
     * @param other
     * @return
     */
    public Money add(Money other) {
        Assert.notNull(other, "other is null");
        Assert.notNull(other.yuan, "other.yuan is null");
        BigDecimal sum = this.yuan.add(other.yuan);
        return of(sum);
    }

    /**
     * (按百分比费率)计算手续费
     *
     * @param feeRatePercentage 百分比 的手续费费率比例,例如 5.5% 传 5.5
     * @return
     */
    public BigDecimal calculateFeeByPercentageRate(BigDecimal feeRatePercentage) {
        return calculateFeeByPercentageRate(feeRatePercentage, RoundingMode.HALF_UP);
    }

    /**
     * (按百分比费率)计算手续费
     *
     * @param feeRatePercentage 百分比 的手续费费率比例,例如 5.5% 传 5.5
     * @param roundingMode
     * @return
     */
    public BigDecimal calculateFeeByPercentageRate(BigDecimal feeRatePercentage, RoundingMode roundingMode) {
        Assert.notNull(feeRatePercentage, "feeRatePercentage is null");
        return yuan.multiply(feeRatePercentage).divide(BigDecimal.valueOf(100), 2, roundingMode);
    }

    /**
     * (按<0的实际费率)计算手续费
     *
     * @param feeRateAbs <0 的手续费费率,例如 0.55
     * @return
     */
    public BigDecimal calculateFeeByAbsRate(BigDecimal feeRateAbs) {
        return calculateFeeByAbsRate(feeRateAbs, RoundingMode.HALF_UP);
    }

    /**
     * (按<0的实际费率)计算手续费
     *
     * @param feeRateAbs   <0 的手续费费率,例如 0.55
     * @param roundingMode
     * @return
     */
    public BigDecimal calculateFeeByAbsRate(BigDecimal feeRateAbs, RoundingMode roundingMode) {
        Assert.notNull(feeRateAbs, "feeRateAbs is null");
        return yuan.multiply(feeRateAbs).setScale(2, roundingMode);
    }


    @Override
    public int compareTo(Money o) {
        return Long.compare(fen, o.fen);
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this) return true;
        if (obj instanceof Money) return compareTo((Money) obj) == 0;
        return false;
    }

    @Override
    public String toString() {
        return String.format("%s元(%s分)", yuan, fen);
    }
}



测试:

    public static void main(String[] args) {
        Money money = Money.of(BigDecimal.valueOf(100));
        Money money2 = Money.of(BigDecimal.valueOf(200));
        System.out.println(money.compareTo(money2));
        System.out.println(money.equals(money2));
        System.out.println(money.add(money2));
        System.out.println(money.calculateFeeByPercentageRate(BigDecimal.valueOf(5.5)));
        System.out.println(money.calculateFeeByAbsRate(BigDecimal.valueOf(0.065)));
    }

输出:

-1
false
300元(30000分)
5.50
6.50

回到顶部

2. 如何统一使用 Money 类

【要点】

  • 在入口网关接收到请求后,就转换为Money类。

  • 所有内部应用的金额处理,强制全部使用Money类运算、传输,禁止自己手动加减乘除、单位换算(比如元到分)。

  • 数据库最好使用 bigint 类型来保存,单位为分(最小货币单位)。便于数字计算。

  • 在出口网关外发时,再根据外部接口文档要求,转换成使用指定的单位(有些是元,有些是分)

文档完善中(代码示例待补充)~



金额如果处理得不好,带来的直接后果就是资金损失,哪怕不是今天,早晚也得出事。
如果你是研发同学,发现内部还没有使用Money类处理金额,建议早点对内部系统做改造。如果你是产品经理,建议转给内部研发工程师,避免踩资损的坑。

posted on   buguge  阅读(98)  评论(0编辑  收藏  举报

相关博文:
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· spring官宣接入deepseek,真的太香了~
历史上的今天:
2022-01-07 借助SimpleDateFormat来谈谈java里的多线程不安全
点击右上角即可分享
微信分享提示