package com.kaidi.oa.common; import java.math.BigDecimal; import java.math.RoundingMode; /** * 金额工具(统一标度与空值处理)。 * * 所有"钱"字段一律用 {@link BigDecimal} 而非 double,避免二进制浮点累加误差导致 * 对账(已付=合同额、分包累计、发票回写)判等不可靠。本工具把请求层传来的装箱 * {@code Double}(前端 JSON number 反序列化结果)安全收口成标度 2、HALF_UP 的 * BigDecimal,并提供 null 安全的加减比较,供各控制器/服务复用。 */ public final class Money { /** 金额标准标度:分(2 位小数)。 */ public static final int SCALE = 2; public static final BigDecimal ZERO = scaled(BigDecimal.ZERO); private Money() { } /** 装箱 Double(可空)→ 标度 2 的 BigDecimal;null 视为 0。非有限数(NaN/Infinity)→干净 400。 */ public static BigDecimal of(Double v) { if (v == null) { return ZERO; } requireFinite(v); return scaled(BigDecimal.valueOf(v)); } /** double → 标度 2 的 BigDecimal。非有限数(NaN/Infinity)→干净 400。 */ public static BigDecimal of(double v) { requireFinite(v); return scaled(BigDecimal.valueOf(v)); } /** * 金额必须是有限数值:拒绝 NaN/Infinity/-Infinity(JSON 里的 "NaN"/"Infinity"/1e400 等)。 * 统一在金额收口处给出干净的 400 中文提示,杜绝把 {@code BigDecimal.valueOf(非有限)} 的底层 * NumberFormatException 原文("Character I is neither a decimal digit...")外泄给前端。 */ private static void requireFinite(double v) { if (!Double.isFinite(v)) { throw new ApiException(400, "金额必须是有限数值"); } } /** BigDecimal(可空)→ 规整标度;null 视为 0。供已是 BigDecimal 的表达式统一收口。 */ public static BigDecimal of(BigDecimal v) { return nz(v); } /** null 安全:null → 0,否则规整标度。 */ public static BigDecimal nz(BigDecimal v) { return v == null ? ZERO : scaled(v); } public static BigDecimal add(BigDecimal a, BigDecimal b) { return scaled(nz(a).add(nz(b))); } public static BigDecimal sub(BigDecimal a, BigDecimal b) { return scaled(nz(a).subtract(nz(b))); } /** a 是否 > b(null 当 0)。 */ public static boolean gt(BigDecimal a, BigDecimal b) { return nz(a).compareTo(nz(b)) > 0; } /** a 是否 <= 0(null 当 0)。 */ public static boolean lte0(BigDecimal a) { return nz(a).compareTo(ZERO) <= 0; } private static BigDecimal scaled(BigDecimal v) { return v.setScale(SCALE, RoundingMode.HALF_UP); } }