資損防控:搞定跨境交易系統(tǒng)中金額處理規(guī)范

2 評(píng)論 923 瀏覽 3 收藏 30 分鐘

在跨境交易日益頻繁的當(dāng)下,資損防控成為支付系統(tǒng)設(shè)計(jì)的關(guān)鍵環(huán)節(jié)。金額處理的規(guī)范性直接關(guān)系到資金安全與企業(yè)信譽(yù)。本文將深入探討交易系統(tǒng)中金額計(jì)算、存儲(chǔ)、傳輸?shù)淖罴褜?shí)踐,詳細(xì)剖析常見(jiàn)資損場(chǎng)景及解決方案。

這篇文章主要講清楚:交易系統(tǒng)(電商、支付等)金額處理常見(jiàn)資損場(chǎng)景,如何構(gòu)建一個(gè)適合跨境支付業(yè)務(wù)(中國(guó)業(yè)務(wù)更不在話下)的Money類,應(yīng)用Money的最佳實(shí)踐,包括計(jì)算、存儲(chǔ)、傳輸,目的是在金額處理上減少資損風(fēng)險(xiǎn)。

一、背景

前幾天有讀者私聊我問(wèn)了幾個(gè)問(wèn)題:

“做國(guó)際支付,不同的幣種的最小單位不同,比如人民幣是分,日元是元,那數(shù)據(jù)庫(kù)里面應(yīng)該應(yīng)該保存整數(shù)還是小數(shù)?”

“從哪里獲取到這個(gè)幣種的最小單位是多少?”

“我贊成你說(shuō)的不應(yīng)該直接對(duì)金額進(jìn)行加減乘除操作,但我還是不知道怎么做,怎樣才能落地呢?”

以前在公司時(shí),也有兄弟部門因?yàn)榻痤~處理不當(dāng),導(dǎo)致了好幾萬(wàn)的資損故障,然后過(guò)來(lái)問(wèn)我金額處理的最佳實(shí)踐,當(dāng)時(shí)也給他們做過(guò)一次相關(guān)分享。

二、金額處理場(chǎng)景

從上圖可以看到,對(duì)于交易系統(tǒng)而言,一共有下面幾種場(chǎng)景需要做金額處理:

  1. 接收外部請(qǐng)求。比如商戶下單100元,或用戶轉(zhuǎn)賬1000元。
  2. 內(nèi)部應(yīng)用處理。比如計(jì)算手續(xù)費(fèi)等。
  3. 內(nèi)部應(yīng)用保存到數(shù)據(jù)庫(kù),從數(shù)據(jù)庫(kù)讀取。
  4. 內(nèi)部應(yīng)用之間傳輸。
  5. 發(fā)送給外部系統(tǒng)或銀行渠道。比如向銀行請(qǐng)求扣款100元。

三、金額計(jì)算常見(jiàn)誤區(qū)及嚴(yán)重后果

對(duì)于研發(fā)經(jīng)驗(yàn)不足的團(tuán)隊(duì)而言,經(jīng)常會(huì)犯以下幾種錯(cuò)誤:

  • 沒(méi)有定義統(tǒng)一的Money類,各系統(tǒng)間使用BigDecimal、double、long等數(shù)據(jù)類型進(jìn)行金額處理及存儲(chǔ)。
  • 定義了統(tǒng)一的Money類,但是寫(xiě)代碼時(shí)不嚴(yán)格遵守,仍然有些代碼使用BigDecimal、double、long等數(shù)據(jù)類型進(jìn)行金額處理。
  • 手動(dòng)對(duì)金額進(jìn)行加、減、乘、除運(yùn)算,單位(元與分)換算。

帶來(lái)的后果,通常就是資金損失,再細(xì)化一下,最常見(jiàn)的情況有下面3種:

1)手動(dòng)做單位換算導(dǎo)致金額被放大或縮小100倍。

  • 比如大家規(guī)定傳的是元,但是其中有位同學(xué)忘記了,以為傳的是分,外部渠道要求傳元,就手動(dòng)乘以100。或者反過(guò)來(lái)。
  • 還有一種情況,部分幣種比如日元最小單元就是元,假如系統(tǒng)約定傳的是分,外部渠道要求傳元,就可能在網(wǎng)關(guān)處理時(shí)手動(dòng)乘以100。

2)1分錢歸屬問(wèn)題。比如結(jié)算給商家,或計(jì)算手續(xù)費(fèi)時(shí),碰到除不盡時(shí),使用四舍五入,還是向零舍入,還是銀行家舍入?這取決于財(cái)務(wù)策略。

3)精度丟失。在大金額時(shí),double有可能會(huì)有精度丟失問(wèn)題。

四、金額處理原則

直接上答案:

  • 制定適用于公司業(yè)務(wù)的Money類來(lái)統(tǒng)一處理金額。
  • 在入口網(wǎng)關(guān)接收到請(qǐng)求后,就轉(zhuǎn)換為Money類。
  • 所有內(nèi)部應(yīng)用的金額處理,強(qiáng)制全部使用Money類運(yùn)算、傳輸,禁止自己手動(dòng)加減乘除、單位換算(比如元到分)。
  • 數(shù)據(jù)庫(kù)使用DECIMAL類型保存,保存單位為元。
  • 在出口網(wǎng)關(guān)外發(fā)時(shí),再根據(jù)外部接口文檔要求,轉(zhuǎn)換成使用指定的單位。有些是元,有些是分(最小貨幣單位)

五、制定Money類

JAVA有制定金額處理規(guī)范JSR 354(Java Specification Request 354),對(duì)應(yīng)的實(shí)現(xiàn)包是Java Money API(javax.money),它提供了一套用于處理貨幣和貨幣計(jì)算的API。不過(guò)我們通常選擇實(shí)現(xiàn)自己的Money類,主要是方便,可以自由定制,比如小數(shù)舍入問(wèn)題。

一個(gè)Money類通常包括以下幾個(gè)主要方面:

  • 通過(guò)參數(shù)生成一個(gè)Money類。
  • 加減乘除處理。
  • 比較處理。
  • 獲取金額(元)和獲取最小單位金額(元或分)。

下面的代碼由ChatGPT o1模型生成。

提示詞為:

編寫(xiě)一個(gè)Money類,支持跨境支付場(chǎng)景下的多幣種訴求。要求:

1)實(shí)現(xiàn)Comparable和Serializable。

2)成員變量幣種使用BigDecimal amount,Currency currency。

3)靜態(tài)方法傳入幣種和數(shù)字返回一個(gè)Money類實(shí)例。

4)支持:加、減、乘、除、比較大小操作,加減和比較需要判斷幣種相同。

5)默認(rèn)使用四余五入,但是支持RoundingMode能力。

6)提供getAmount和getAmountMinorUnit方法,前者返回單位元,后者返回幣種的最小單位,通過(guò)Currency.getDefaultFractionDigits()和amount計(jì)算出來(lái)。

生成的 Money 類實(shí)現(xiàn)了跨境支付場(chǎng)景下多幣種的高精度金額處理,滿足了以下關(guān)鍵需求:

  • 實(shí)現(xiàn)了 Comparable 和 Serializable 接口,支持排序和序列化。
  • 使用 BigDecimal 和 Currency 來(lái)表示金額和幣種,確保高精度和標(biāo)準(zhǔn)化。
  • 提供了豐富的操作方法,包括加、減、乘、除,并且支持自定義和四舍五入模式。
  • 提供了 getAmount 和 getAmountMinorUnit 方法,分別返回單位元和最小單位的金額。
  • 確保類的不可變性和線程安全性。
  • 根據(jù)幣種自動(dòng)計(jì)算出最小單位,比如人民幣就是分,而日元就是元。
  • 只有相同幣種才能做加、減。
  • 乘除支持舍入,默認(rèn)使用四舍五入,但是支持其它舍入方式。

通過(guò)這種設(shè)計(jì),可以在跨境支付、金融應(yīng)用等需要高精度、多幣種支持的場(chǎng)景中安全、有效地使用 Money 類進(jìn)行金額處理。

后面會(huì)做拆解說(shuō)明。

完整代碼如下:

import java.io.Serializable;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Currency;
import java.util.Objects;


/**
 * Money類用于表示不同幣種的金額,支持高精度計(jì)算和多幣種操作。
 * 該類是不可變的(immutable),并且實(shí)現(xiàn)了Comparable和Serializable接口。
 */
public final class Money implements Comparable, Serializable {
    private static final long serialVersionUID = 1L;


    private final BigDecimal amount;
    private final Currency currency;


    /**
     * 私有構(gòu)造函數(shù),確保通過(guò)工廠方法創(chuàng)建實(shí)例。
     *
     * @param amount   金額,單位為元
     * @param currency 幣種,非空
     */
    private Money(BigDecimal amount, Currency currency) {
        if (amount == null) {
            throw new IllegalArgumentException("Amount cannot be null.");
        }
        if (currency == null) {
            throw new IllegalArgumentException("Currency cannot be null.");
        }
        this.amount = amount;
        this.currency = currency;
    }


    /**
     * 靜態(tài)工廠方法,通過(guò)傳入Currency和BigDecimal金額創(chuàng)建Money實(shí)例。
     * 默認(rèn)使用RoundingMode.HALF_UP進(jìn)行四舍五入。
     *
     * @param currency 幣種
     * @param amount   金額,單位為元
     * @return 新的Money實(shí)例
     */
    public static Money of(Currency currency, BigDecimal amount) {
        return of(currency, amount, RoundingMode.HALF_UP);
    }


    /**
     * 靜態(tài)工廠方法,通過(guò)傳入Currency和BigDecimal金額創(chuàng)建Money實(shí)例。
     * 允許指定RoundingMode進(jìn)行四舍五入。
     *
     * @param currency     幣種
     * @param amount       金額,單位為元
     * @param roundingMode 四舍五入模式
     * @return 新的Money實(shí)例
     */
    public static Money of(Currency currency, BigDecimal amount, RoundingMode roundingMode) {
        Objects.requireNonNull(currency, "Currency cannot be null.");
        Objects.requireNonNull(amount, "Amount cannot be null.");
        Objects.requireNonNull(roundingMode, "RoundingMode cannot be null.");


        BigDecimal scaledAmount = amount.setScale(
                currency.getDefaultFractionDigits(),
                roundingMode
        );


        return new Money(scaledAmount, currency);
    }


    /**
     * 加法操作,返回新的Money實(shí)例。
     * 僅允許相同幣種的加法操作。
     *
     * @param other 加數(shù)
     * @return 相加后的Money實(shí)例
     * @throws IllegalArgumentException 如果幣種不一致
     */
    public Money add(Money other) {
        validateSameCurrency(other);
        BigDecimal resultAmount = this.amount.add(other.amount);
        return new Money(resultAmount, this.currency);
    }


    /**
     * 減法操作,返回新的Money實(shí)例。
     * 僅允許相同幣種的減法操作。
     *
     * @param other 減數(shù)
     * @return 相減后的Money實(shí)例
     * @throws IllegalArgumentException 如果幣種不一致
     */
    public Money subtract(Money other) {
        validateSameCurrency(other);
        BigDecimal resultAmount = this.amount.subtract(other.amount);
        return new Money(resultAmount, this.currency);
    }


    /**
     * 乘法操作,使用默認(rèn)舍入模式(RoundingMode.HALF_UP),返回新的Money實(shí)例。
     *
     * @param multiplier 乘數(shù)
     * @return 乘法后的Money實(shí)例
     * @throws ArithmeticException 如果需要進(jìn)行舍入但無(wú)法進(jìn)行
     * @throws IllegalArgumentException 如果multiplier為null
     */
    public Money multiply(BigDecimal multiplier) {
        return multiply(multiplier, RoundingMode.HALF_UP);
    }


    /**
     * 乘法操作,返回新的Money實(shí)例。
     *
     * @param multiplier    乘數(shù)
     * @param roundingMode  四舍五入模式
     * @return 乘法后的Money實(shí)例
     * @throws ArithmeticException 如果需要進(jìn)行舍入但沒(méi)有指定舍入模式
     * @throws IllegalArgumentException 如果multiplier或roundingMode為null
     */
    public Money multiply(BigDecimal multiplier, RoundingMode roundingMode) {
        Objects.requireNonNull(multiplier, "Multiplier cannot be null.");
        Objects.requireNonNull(roundingMode, "RoundingMode cannot be null.");


        BigDecimal resultAmount = this.amount.multiply(multiplier)
                .setScale(currency.getDefaultFractionDigits(), roundingMode);
        return new Money(resultAmount, this.currency);
    }


    /**
     * 除法操作,返回新的Money實(shí)例。
     *
     * @param divisor       除數(shù)
     * @param scale         保留的小數(shù)位數(shù)
     * @param roundingMode  四舍五入模式
     * @return 除法后的Money實(shí)例
     * @throws ArithmeticException 如果除數(shù)為零或無(wú)法精確表示
     * @throws IllegalArgumentException 如果divisor或roundingMode為null
     */
    public Money divide(BigDecimal divisor, int scale, RoundingMode roundingMode) {
        Objects.requireNonNull(divisor, "Divisor cannot be null.");
        Objects.requireNonNull(roundingMode, "RoundingMode cannot be null.");
        if (divisor.compareTo(BigDecimal.ZERO) == 0) {
            throw new ArithmeticException("Division by zero.");
        }


        BigDecimal resultAmount = this.amount.divide(divisor, scale, roundingMode)
                .setScale(currency.getDefaultFractionDigits(), roundingMode);
        return new Money(resultAmount, this.currency);
    }


    /**
     * 比較大小,僅允許相同幣種的比較。
     *
     * @param other 要比較的Money對(duì)象
     * @return 負(fù)數(shù)、零或正數(shù),分別表示小于、等于或大于
     * @throws IllegalArgumentException 如果幣種不一致
     */
    @Override
    public int compareTo(Money other) {
        validateSameCurrency(other);
        return this.amount.compareTo(other.amount);
    }


    /**
     * 獲取金額,單位為元。
     *
     * @return 金額
     */
    public BigDecimal getAmount() {
        return amount;
    }


    /**
     * 獲取最小單位金額,通過(guò)Currency.getDefaultFractionDigits()和amount計(jì)算。
     * 例如,人民幣1元 = 100分,日元1元 = 1元。
     *
     * @return 最小單位金額
     */
    public BigDecimal getAmountMinorUnit() {
        int fractionDigits = currency.getDefaultFractionDigits();
        return amount.movePointRight(fractionDigits);
    }


    /**
     * 獲取幣種。
     *
     * @return 幣種
     */
    public Currency getCurrency() {
        return currency;
    }


    /**
     * 校驗(yàn)兩個(gè)Money對(duì)象的幣種是否相同。
     *
     * @param other 另一個(gè)Money對(duì)象
     * @throws IllegalArgumentException 如果幣種不一致
     */
    private void validateSameCurrency(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new IllegalArgumentException("Currencies do not match.");
        }
    }


    /**
     * 重寫(xiě)equals方法,基于金額和幣種判斷相等。
     *
     * @param o 其他對(duì)象
     * @return 是否相等
     */
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;


        Money money = (Money) o;
        return amount.equals(money.amount) &&
                currency.equals(money.currency);
    }


    /**
     * 重寫(xiě)hashCode方法,基于金額和幣種生成哈希碼。
     *
     * @return 哈希碼
     */
    @Override
    public int hashCode() {
        return Objects.hash(amount, currency);
    }


    /**
     * 重寫(xiě)toString方法,格式化輸出幣種和金額。
     *
     * @return 格式化后的字符串
     */
    @Override
    public String toString() {
        return String.format("%s %s", currency.getCurrencyCode(), amount);
    }
}

下面做拆解說(shuō)明。

5.1. 核心屬性

public final class Money implements Comparable, Serializable {
    private static final long serialVersionUID = 1L;

    private final BigDecimal amount;
    private final Currency currency;
}
    

5.2. 通過(guò)金額數(shù)值和幣種構(gòu)建一個(gè)Money類

/**
     * 私有構(gòu)造函數(shù),確保通過(guò)工廠方法創(chuàng)建實(shí)例。
     *
     * @param amount   金額,單位為元
     * @param currency 幣種,非空
     */
    private Money(BigDecimal amount, Currency currency) {
        if (amount == null) {
            throw new IllegalArgumentException("Amount cannot be null.");
        }
        if (currency == null) {
            throw new IllegalArgumentException("Currency cannot be null.");
        }
        this.amount = amount;
        this.currency = currency;
    }


    /**
     * 靜態(tài)工廠方法,通過(guò)傳入Currency和BigDecimal金額創(chuàng)建Money實(shí)例。
     * 默認(rèn)使用RoundingMode.HALF_UP進(jìn)行四舍五入。
     *
     * @param currency 幣種
     * @param amount   金額,單位為元
     * @return 新的Money實(shí)例
     */
    public static Money of(Currency currency, BigDecimal amount) {
        return of(currency, amount, RoundingMode.HALF_UP);
    }


    /**
     * 靜態(tài)工廠方法,通過(guò)傳入Currency和BigDecimal金額創(chuàng)建Money實(shí)例。
     * 允許指定RoundingMode進(jìn)行四舍五入。
     *
     * @param currency     幣種
     * @param amount       金額,單位為元
     * @param roundingMode 四舍五入模式
     * @return 新的Money實(shí)例
     */
    public static Money of(Currency currency, BigDecimal amount, RoundingMode roundingMode) {
        Objects.requireNonNull(currency, "Currency cannot be null.");
        Objects.requireNonNull(amount, "Amount cannot be null.");
        Objects.requireNonNull(roundingMode, "RoundingMode cannot be null.");


        BigDecimal scaledAmount = amount.setScale(
                currency.getDefaultFractionDigits(),
                roundingMode
        );


        return new Money(scaledAmount, currency);
    }

5.3. 加減乘除

1)注意除法有除不盡舍入的問(wèn)題,需要根據(jù)業(yè)務(wù)來(lái)指定舍入的模式,建議默認(rèn)提供四舍五入,但是保留指定模式的能力。具體可以參考:java.math.RoundingMode。

  • UP:遠(yuǎn)零方向舍入。示例:1.6返回2,-1.6返回-2。
  • DOWN:向零方向舍入。示例:1.6返回1,-1.6返回-1。
  • CEILING:向上舍入。示例:1.6返回2,-1.6返回-1。
  • FLOOR:向下舍入。示例:1.6返回1,-1.6返回-2。
  • HALF_UP:四舍五入。示例:1.5返回2,-1.5返回-2。
  • HALF_DOWN:五舍六入。示例:1.5返回1,-1.5返回-1,1.6返回2,-1.6返回-2。
  • HALF_EVEN:銀行家算法,尾數(shù)小于0.5舍,尾數(shù)大于0.5入,尾數(shù)等于0.5往最終結(jié)果是偶數(shù)的方向進(jìn)。示例:1.51返回2,-1.49返回-1,2.5返回2,3.5返回4(1.5,2.5,3.5,4.5,5.5等這些最終只出現(xiàn)2,4,4,4,6等偶數(shù))。

2)加和減,需要先判斷幣種,只有幣種相同才能做加減。

    /**
     * 加法操作,返回新的Money實(shí)例。
     * 僅允許相同幣種的加法操作。
     *
     * @param other 加數(shù)
     * @return 相加后的Money實(shí)例
     * @throws IllegalArgumentException 如果幣種不一致
     */
    public Money add(Money other) {
        validateSameCurrency(other);
        BigDecimal resultAmount = this.amount.add(other.amount);
        return new Money(resultAmount, this.currency);
    }


    /**
     * 減法操作,返回新的Money實(shí)例。
     * 僅允許相同幣種的減法操作。
     *
     * @param other 減數(shù)
     * @return 相減后的Money實(shí)例
     * @throws IllegalArgumentException 如果幣種不一致
     */
    public Money subtract(Money other) {
        validateSameCurrency(other);
        BigDecimal resultAmount = this.amount.subtract(other.amount);
        return new Money(resultAmount, this.currency);
    }


    /**
     * 乘法操作,使用默認(rèn)舍入模式(RoundingMode.HALF_UP),返回新的Money實(shí)例。
     *
     * @param multiplier 乘數(shù)
     * @return 乘法后的Money實(shí)例
     * @throws ArithmeticException 如果需要進(jìn)行舍入但無(wú)法進(jìn)行
     * @throws IllegalArgumentException 如果multiplier為null
     */
    public Money multiply(BigDecimal multiplier) {
        return multiply(multiplier, RoundingMode.HALF_UP);
    }


    /**
     * 乘法操作,返回新的Money實(shí)例。
     *
     * @param multiplier    乘數(shù)
     * @param roundingMode  四舍五入模式
     * @return 乘法后的Money實(shí)例
     * @throws ArithmeticException 如果需要進(jìn)行舍入但沒(méi)有指定舍入模式
     * @throws IllegalArgumentException 如果multiplier或roundingMode為null
     */
    public Money multiply(BigDecimal multiplier, RoundingMode roundingMode) {
        Objects.requireNonNull(multiplier, "Multiplier cannot be null.");
        Objects.requireNonNull(roundingMode, "RoundingMode cannot be null.");


        BigDecimal resultAmount = this.amount.multiply(multiplier)
                .setScale(currency.getDefaultFractionDigits(), roundingMode);
        return new Money(resultAmount, this.currency);
    }


    /**
     * 除法操作,返回新的Money實(shí)例。
     *
     * @param divisor       除數(shù)
     * @param scale         保留的小數(shù)位數(shù)
     * @param roundingMode  四舍五入模式
     * @return 除法后的Money實(shí)例
     * @throws ArithmeticException 如果除數(shù)為零或無(wú)法精確表示
     * @throws IllegalArgumentException 如果divisor或roundingMode為null
     */
    public Money divide(BigDecimal divisor, int scale, RoundingMode roundingMode) {
        Objects.requireNonNull(divisor, "Divisor cannot be null.");
        Objects.requireNonNull(roundingMode, "RoundingMode cannot be null.");
        if (divisor.compareTo(BigDecimal.ZERO) == 0) {
            throw new ArithmeticException("Division by zero.");
        }


        BigDecimal resultAmount = this.amount.divide(divisor, scale, roundingMode)
                .setScale(currency.getDefaultFractionDigits(), roundingMode);
        return new Money(resultAmount, this.currency);
    }

5.4. 比較大小

    /**
     * 比較大小,僅允許相同幣種的比較。
     *
     * @param other 要比較的Money對(duì)象
     * @return 負(fù)數(shù)、零或正數(shù),分別表示小于、等于或大于
     * @throws IllegalArgumentException 如果幣種不一致
     */
    @Override
    public int compareTo(Money other) {
        validateSameCurrency(other);
        return this.amount.compareTo(other.amount);
    }

5.5. 返回元和分單位的數(shù)字

所有內(nèi)部應(yīng)用全部使用getAmount(),不允許使用getAmountMinorUnit()。保證內(nèi)部應(yīng)用大家的語(yǔ)義保持一致。

只有請(qǐng)求外部渠道時(shí),如果渠道要求使用幣種最小單位,才使用getAmountMinorUnit()。

    /**
     * 獲取金額,單位為元。
     *
     * @return 金額
     */
    public BigDecimal getAmount() {
        return amount;
    }


    /**
     * 獲取最小單位金額,通過(guò)Currency.getDefaultFractionDigits()和amount計(jì)算。
     * 例如,人民幣1元 = 100分,日元1元 = 1元。
     *
     * @return 最小單位金額
     */
    public BigDecimal getAmountMinorUnit() {
        int fractionDigits = currency.getDefaultFractionDigits();
        return amount.movePointRight(fractionDigits);
    }

六、Money類實(shí)際應(yīng)用最佳實(shí)踐

從接收外部請(qǐng)求開(kāi)始,到內(nèi)部計(jì)算、存儲(chǔ),最后外發(fā)到渠道,完整實(shí)踐說(shuō)明。

6.1. 接收入口請(qǐng)求

在入口網(wǎng)關(guān)處,先轉(zhuǎn)換成Money類,再往后請(qǐng)求。

// 使用外部請(qǐng)求的參數(shù)構(gòu)建Money類
Money payAmount = Money.of(BigDecimal.valueOf(outRequest.getPayAmount()), outRequest.getCurrency()); 

// 構(gòu)建內(nèi)部請(qǐng)求
PayRequest request = new PayRequest();
request.setPayAmount(payAmount);
... ...

// 發(fā)給內(nèi)部應(yīng)用
payService.pay(request);

6.2. 內(nèi)部應(yīng)用運(yùn)算

內(nèi)部所有應(yīng)用,全部使用Money類流轉(zhuǎn)和計(jì)算。

Money payAmount = request.getPayAmount();
Money fee = payAmount.multiply(BigDecimal.valueOf(0.03));

// 其它處理

6.3. 內(nèi)部數(shù)據(jù)庫(kù)存儲(chǔ)

Money payAmount = request.getPayAmount();
BigDecimal amount = payAmount.getAmount();
String currency = payAmount.getCurrency().getCurrencyCode();

// 構(gòu)建DO
Order order = new Order();
order.setAmount(amount);
order.setCurrency(currency);
...

// 保存入庫(kù)
saveToDB(order);

6.4. 外發(fā)處理

渠道要求是元,使用:

payAmount.getAmount();

如果要求是分或最小幣種單位,使用:

payAmount.getAmountMinorUnit();

7. 結(jié)束語(yǔ)

金額如果處理得不好,帶來(lái)的直接后果就是資金損失,哪怕不是今天,早晚也得出事。

如果你是研發(fā)同學(xué),發(fā)現(xiàn)內(nèi)部還沒(méi)有使用Money類處理金額,建議早點(diǎn)對(duì)內(nèi)部系統(tǒng)做改造。如果你是產(chǎn)品經(jīng)理,建議轉(zhuǎn)給內(nèi)部研發(fā)工程師,避免踩資損的坑。

本文由人人都是產(chǎn)品經(jīng)理作者【隱墨星辰】,微信公眾號(hào):【隱墨星辰】,原創(chuàng)/授權(quán) 發(fā)布于人人都是產(chǎn)品經(jīng)理,未經(jīng)許可,禁止轉(zhuǎn)載。

題圖來(lái)自Unsplash,基于 CC0 協(xié)議。

更多精彩內(nèi)容,請(qǐng)關(guān)注人人都是產(chǎn)品經(jīng)理微信公眾號(hào)或下載App
評(píng)論
評(píng)論請(qǐng)登錄
  1. mark

    來(lái)自浙江 回復(fù)
  2. 資損防控很重要,搞定跨境交易的金額處理規(guī)范,就能更安心地做生意了!

    來(lái)自遼寧 回復(fù)