Java record 关键词+ Map 汇总统计实战:一段余额统计代码背后的设计思想
很多 Java 程序员在日常开发中都会遇到类似需求:
统计一批用户当前可用余额
但当代码写出来后,往往会变成一坨:
- if/else 到处都是
- BigDecimal 累加混乱
- Map 使用不规范
- DTO 写一堆模板代码
最近在项目中看到一段非常典型的实现代码,涉及:
- Java
record Map.getOrDefault- 批次余额统计
- BigDecimal 累加
- MyBatis LambdaQueryWrapper
虽然代码不长,但里面隐藏着很多 值得学习的设计思想。
本文就带大家 逐行拆解这段代码,并分析背后的实现逻辑。
一、完整代码
先看完整方法:
java
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;
}
这段代码的核心作用其实就是:
统计一批用户当前可用余额(现金 + 赠送金)
返回结构:
rust
userId -> 余额汇总
二、余额汇总对象:Java record
代码中使用了 record 定义余额对象:
java
public record BatchBalanceSummary(
BigDecimal cashBalance,
BigDecimal promoBalance) {
}
record 是 Java 16 引入的 数据类语法糖。
它会自动生成:
- 构造函数
- getter
- equals
- hashCode
- toString
传统写法:
java
public class BatchBalanceSummary {
private BigDecimal cashBalance;
private BigDecimal promoBalance;
}
至少要写几十行代码。
而 record:
1 行搞定
record 编译后结构
scss
BatchBalanceSummary
│
├── cashBalance
├── promoBalance
├── equals()
├── hashCode()
└── toString()
三、整体执行流程
这段代码整体流程如下:
可以看到整个过程非常清晰:
1️⃣ 输入用户列表 2️⃣ 数据清洗 3️⃣ 账户状态校验 4️⃣ 查询批次余额 5️⃣ 汇总统计
四、第一步:参数防御
java
if (userIds == null || userIds.isEmpty()) {
return Map.of();
}
这是典型的 防御式编程。
如果用户列表为空:
直接返回:
{}
避免后面逻辑执行。
Map.of() 是 Java 9 新特性:
返回 不可变 Map。
五、第二步:数据清洗
java
List<Long> distinctUserIds = userIds.stream()
.filter(Objects::nonNull)
.distinct()
.toList();
作用:
- 去掉
null - 去重
例如:
输入:
yaml
[1001,1002,null,1001]
输出:
yaml
[1001,1002]
这样可以避免:
- 重复查询数据库
- 重复统计余额
六、第三步:确保钱包存在
java
ensureUserBatchWallet(userId);
这一步通常是:
初始化用户钱包
如果用户第一次使用余额系统:
就创建钱包记录。
例如:
user_wallet
表中插入一条数据。
七、第四步:处理余额过期
java
expireIfNeeded(userId);
余额系统通常都有:
- 赠送金
- 优惠金
- 过期时间
所以需要在统计前:
把过期余额标记为失效
否则会统计到错误金额。
八、第五步:初始化结果 Map
java
Map<Long, BatchBalanceSummary> result = new LinkedHashMap<>();
为什么用 LinkedHashMap?
因为:
可以保持插入顺序
初始化结果:
rust
1001 -> (0,0)
1002 -> (0,0)
代码:
java
result.put(userId,
new BatchBalanceSummary(BigDecimal.ZERO, BigDecimal.ZERO));
九、第六步:查询数据库批次
java
List<AccountBatchEntity> batches =
accountBatchMapper.selectList(...)
SQL 等价:
sql
SELECT *
FROM account_batch
WHERE user_id IN (...)
AND status = ACTIVE
AND remaining_amount > 0
查询条件:
| 条件 | 含义 |
|---|---|
| user_id | 指定用户 |
| status | 批次有效 |
| remaining_amount > 0 | 还有余额 |
十、第七步:余额累加
核心代码:
java
BatchBalanceSummary current =
result.getOrDefault(...)
作用:
获取当前用户已经累计的余额。
如果不存在:
返回默认:
scss
(0,0)
判断余额类型
java
ACCOUNT_TYPE_PROMO
如果是:
赠送余额
就加到:
promoBalance
否则:
cashBalance
现金余额累加
java
current.cashBalance().add(amount)
赠送余额累加
java
current.promoBalance().add(amount)
十一、余额统计示例
假设数据库数据:
| userId | type | amount |
|---|---|---|
| 1001 | cash | 100 |
| 1001 | promo | 20 |
| 1001 | cash | 30 |
| 1002 | promo | 50 |
统计过程:
rust
1001 -> (0,0)
+100 cash
1001 -> (100,0)
+20 promo
1001 -> (100,20)
+30 cash
1001 -> (130,20)
最终:
rust
1001 -> (130,20)
1002 -> (0,50)
十二、余额统计架构图
整个余额系统一般是这样设计:
十三、record 的优势
使用 record 后代码变得非常干净:
BatchBalanceSummary
只负责:
数据承载
优点:
- 不可变对象
- 线程安全
- 减少模板代码
- 更清晰的领域模型
十四、几个可以优化的地方
1 使用 computeIfAbsent
可以替换:
getOrDefault
写法更优雅:
java
result.computeIfAbsent(
userId,
k -> new BatchBalanceSummary(BigDecimal.ZERO, BigDecimal.ZERO)
);
2 SQL 聚合优化
如果数据量很大:
可以用 SQL 聚合:
sql
SELECT user_id,
account_type,
SUM(remaining_amount)
FROM account_batch
GROUP BY user_id,account_type
减少 Java 层循环。
3 批量过期处理
目前代码:
scss
expireIfNeeded(userId)
如果用户很多:
可能产生大量 SQL。
可以改成:
批量过期处理
十五、总结
这段代码虽然只有几十行,但实际上包含了很多优秀的设计思想:
- record 数据对象
- Map 汇总统计
- 防御式编程
- BigDecimal 安全计算
- 批次余额设计
一句话总结:
通过批次账户模型,实现用户余额的安全、可扩展统计。
这种设计在很多系统中都会出现:
- 钱包系统
- 积分系统
- 余额系统
- 账户系统
如果你在做 支付 / 钱包 / 优惠金系统,这种设计模式基本是必备技能。