Java record 关键词+ Map 汇总统计实战:一段余额统计代码背后的设计思想

0 阅读4分钟

Java record 关键词+ Map 汇总统计实战:一段余额统计代码背后的设计思想

很多 Java 程序员在日常开发中都会遇到类似需求:

统计一批用户当前可用余额

但当代码写出来后,往往会变成一坨:

  • if/else 到处都是
  • BigDecimal 累加混乱
  • Map 使用不规范
  • DTO 写一堆模板代码

最近在项目中看到一段非常典型的实现代码,涉及:

  • Java record
  • Map.getOrDefault
  • 批次余额统计
  • BigDecimal 累加
  • MyBatis LambdaQueryWrapper

虽然代码不长,但里面隐藏着很多 值得学习的设计思想

本文就带大家 逐行拆解这段代码,并分析背后的实现逻辑


一、完整代码

先看完整方法:

public Map<Long, BatchBalanceSummary> summarizeAvailableBalances(List<Long> userIds) {
    if (userIds == null || userIds.isEmpty()) {
        return Map.of();
    }

    List<Long> distinctUserIds = userIds.stream()
        .filter(java.util.Objects::nonNull)
        .distinct()
        .toList();

    if (distinctUserIds.isEmpty()) {
        return Map.of();
    }

    for (Long userId : distinctUserIds) {
        ensureUserBatchWallet(userId);
        expireIfNeeded(userId);
    }

    Map<Long, BatchBalanceSummary> result = new LinkedHashMap<>();

    for (Long userId : distinctUserIds) {
        result.put(userId, new BatchBalanceSummary(BigDecimal.ZERO, BigDecimal.ZERO));
    }

    List<AccountBatchEntity> batches = accountBatchMapper.selectList(
        new LambdaQueryWrapper<AccountBatchEntity>()
            .in(AccountBatchEntity::getUserId, distinctUserIds)
            .eq(AccountBatchEntity::getStatus, STATUS_ACTIVE)
            .gt(AccountBatchEntity::getRemainingAmount, BigDecimal.ZERO)
    );

    for (AccountBatchEntity batch : batches) {

        BatchBalanceSummary current =
            result.getOrDefault(batch.getUserId(),
                new BatchBalanceSummary(BigDecimal.ZERO, BigDecimal.ZERO));

        if (ACCOUNT_TYPE_PROMO.equalsIgnoreCase(batch.getAccountType())) {

            result.put(batch.getUserId(),
                new BatchBalanceSummary(
                    current.cashBalance(),
                    current.promoBalance()
                        .add(normalize(batch.getRemainingAmount()))
                ));

        } else {

            result.put(batch.getUserId(),
                new BatchBalanceSummary(
                    current.cashBalance()
                        .add(normalize(batch.getRemainingAmount())),
                    current.promoBalance()
                ));
        }
    }

    return result;
}

这段代码的核心作用其实就是:

统计一批用户当前可用余额(现金 + 赠送金)

返回结构:

userId -> 余额汇总

二、余额汇总对象:Java record

代码中使用了 record 定义余额对象:

public record BatchBalanceSummary(
        BigDecimal cashBalance,
        BigDecimal promoBalance) {
}

record 是 Java 16 引入的 数据类语法糖

它会自动生成:

  • 构造函数
  • getter
  • equals
  • hashCode
  • toString

传统写法:

public class BatchBalanceSummary {

    private BigDecimal cashBalance;
    private BigDecimal promoBalance;

}

至少要写几十行代码。

而 record:

1 行搞定

record 编译后结构

BatchBalanceSummary
│
├── cashBalance
├── promoBalance
├── equals()
├── hashCode()
└── toString()

三、整体执行流程

这段代码整体流程如下:

flowchart TD

A[输入 userIds] --> B[过滤 null]
B --> C[去重]
C --> D[确保钱包存在]
D --> E[处理过期余额]
E --> F[初始化结果 Map]
F --> G[查询所有余额批次]
G --> H[按用户累加余额]
H --> I[返回汇总结果]

可以看到整个过程非常清晰:

1️⃣ 输入用户列表 2️⃣ 数据清洗 3️⃣ 账户状态校验 4️⃣ 查询批次余额 5️⃣ 汇总统计


四、第一步:参数防御

if (userIds == null || userIds.isEmpty()) {
    return Map.of();
}

这是典型的 防御式编程

如果用户列表为空:

直接返回:

{}

避免后面逻辑执行。

Map.of() 是 Java 9 新特性:

返回 不可变 Map


五、第二步:数据清洗

List<Long> distinctUserIds = userIds.stream()
        .filter(Objects::nonNull)
        .distinct()
        .toList();

作用:

  • 去掉 null
  • 去重

例如:

输入:

[1001,1002,null,1001]

输出:

[1001,1002]

这样可以避免:

  • 重复查询数据库
  • 重复统计余额

六、第三步:确保钱包存在

ensureUserBatchWallet(userId);

这一步通常是:

初始化用户钱包

如果用户第一次使用余额系统:

就创建钱包记录。

例如:

user_wallet

表中插入一条数据。


七、第四步:处理余额过期

expireIfNeeded(userId);

余额系统通常都有:

  • 赠送金
  • 优惠金
  • 过期时间

所以需要在统计前:

把过期余额标记为失效

否则会统计到错误金额。


八、第五步:初始化结果 Map

Map<Long, BatchBalanceSummary> result = new LinkedHashMap<>();

为什么用 LinkedHashMap

因为:

可以保持插入顺序

初始化结果:

1001 -> (0,0)
1002 -> (0,0)

代码:

result.put(userId,
    new BatchBalanceSummary(BigDecimal.ZERO, BigDecimal.ZERO));

九、第六步:查询数据库批次

List<AccountBatchEntity> batches =
    accountBatchMapper.selectList(...)

SQL 等价:

SELECT *
FROM account_batch
WHERE user_id IN (...)
AND status = ACTIVE
AND remaining_amount > 0

查询条件:

条件含义
user_id指定用户
status批次有效
remaining_amount > 0还有余额

十、第七步:余额累加

核心代码:

BatchBalanceSummary current =
    result.getOrDefault(...)

作用:

获取当前用户已经累计的余额。

如果不存在:

返回默认:

(0,0)

判断余额类型

ACCOUNT_TYPE_PROMO

如果是:

赠送余额

就加到:

promoBalance

否则:

cashBalance

现金余额累加

current.cashBalance().add(amount)

赠送余额累加

current.promoBalance().add(amount)

十一、余额统计示例

假设数据库数据:

userIdtypeamount
1001cash100
1001promo20
1001cash30
1002promo50

统计过程:

1001 -> (0,0)

+100 cash
1001 -> (100,0)

+20 promo
1001 -> (100,20)

+30 cash
1001 -> (130,20)

最终:

1001 -> (130,20)
1002 -> (0,50)

十二、余额统计架构图

整个余额系统一般是这样设计:

graph TD

A[用户余额查询接口] --> B[余额汇总服务]

B --> C[批次余额表 account_batch]

C --> D[现金余额批次]

C --> E[赠送余额批次]

B --> F[统计结果]

F --> G[userId -> BatchBalanceSummary]

十三、record 的优势

使用 record 后代码变得非常干净:

BatchBalanceSummary

只负责:

数据承载

优点:

  • 不可变对象
  • 线程安全
  • 减少模板代码
  • 更清晰的领域模型

十四、几个可以优化的地方

1 使用 computeIfAbsent

可以替换:

getOrDefault

写法更优雅:

result.computeIfAbsent(
    userId,
    k -> new BatchBalanceSummary(BigDecimal.ZERO, BigDecimal.ZERO)
);

2 SQL 聚合优化

如果数据量很大:

可以用 SQL 聚合:

SELECT user_id,
       account_type,
       SUM(remaining_amount)
FROM account_batch
GROUP BY user_id,account_type

减少 Java 层循环。


3 批量过期处理

目前代码:

expireIfNeeded(userId)

如果用户很多:

可能产生大量 SQL。

可以改成:

批量过期处理

十五、总结

这段代码虽然只有几十行,但实际上包含了很多优秀的设计思想:

  • record 数据对象
  • Map 汇总统计
  • 防御式编程
  • BigDecimal 安全计算
  • 批次余额设计

一句话总结:

通过批次账户模型,实现用户余额的安全、可扩展统计。

这种设计在很多系统中都会出现:

  • 钱包系统
  • 积分系统
  • 余额系统
  • 账户系统

如果你在做 支付 / 钱包 / 优惠金系统,这种设计模式基本是必备技能。