价格计算需分层处理基础价、平台折扣和券后抵扣,用BigDecimal不可变对象封装并显式指定精度与舍入模式,优惠券逻辑采用策略模式隔离,严格测试边界值并审计价格变更。

价格计算必须区分基础价、折扣价和最终价
Java里做商品价格计算,最容易出错的是把 discountRate、finalPrice、originalPrice 混着用,尤其在叠加满减+会员折扣+优惠券时。实际业务中,这三者必须严格分层:先算基础价(含规格加价),再应用平台级折扣(如 95 折),最后扣减券后金额(注意券是否限品类、是否可叠加)。别在同一个 double 字段上反复赋值,建议用不可变对象封装:
public record PriceCalculation(
BigDecimal originalPrice,
BigDecimal discountAmount,
BigDecimal couponDeduction,
BigDecimal finalPrice
) {}
别用 double 做价格运算,BigDecimal 的 scale 和 roundingMode 得显式指定
写 double price = 199.9 * 0.95; 看似简单,但会得到 189.90499999999998 这种结果,四舍五入后可能多收或少收 1 分钱。所有价格字段必须用 BigDecimal,且每次运算都要明确 scale 和 RoundingMode:
- 电商场景统一用
setScale(2, RoundingMode.HALF_UP) - 不要依赖构造函数传
double,改用字符串构造:new BigDecimal("199.90") -
divide()必须带scale和roundingMode参数,否则抛ArithmeticException
优惠券叠加逻辑要靠策略模式隔离,别用 if-else 堆砌
当出现“满 300 减 30”、“品类券打 8 折”、“店铺红包限本店”时,硬编码判断会迅速失控。推荐用策略接口 + Spring 的 @Qualifier 自动注入:
public interface CouponStrategy {
BigDecimal calculateDeduction(BigDecimal orderAmount, List- items);
}
// 实现类如 FullReductionCoupon、CategoryDiscountCoupon...
// 调用时根据 coupon.type 查找对应 bean
这样新增券类型只需加实现类,不改原有计算主流程。注意:券的生效顺序(先减后折 or 先折后减)必须由业务方明确定义,代码里不能自行猜测。
测试必须覆盖边界值:0 元、1 分、超大金额、负向优惠
真实线上问题常出在极端 case:
-
originalPrice为BigDecimal.ZERO时,multiply()是否仍返回 0? - 优惠券面额大于订单金额,是否允许负数抵扣?(多数系统要求最小为 0)
- 金额达百万级时,
toString()是否触发科学计数法(影响日志和展示)? - 多线程并发调用价格计算器,是否因共享
MathContext或静态变量出错?
@ParameterizedTest 驱动这些 case,比只测 “199.9 × 0.95 = 189.9” 有用得多。实际项目里,最常被跳过的不是算法,而是价格变更的审计留痕——比如谁在什么时候把某 SKU 的 basePrice 从 299 改成了 279,这个操作必须落库并关联工单号。没留痕的价格系统,等于没做。










