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

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()

三、整体执行流程

这段代码整体流程如下:

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️⃣ 汇总统计


四、第一步:参数防御

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)

十二、余额统计架构图

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

graph TD A[用户余额查询接口] --> B[余额汇总服务] B --> C[批次余额表 account_batch] C --> D[现金余额批次] C --> E[赠送余额批次] B --> F[统计结果] F --> G[userId -> BatchBalanceSummary]

十三、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 安全计算
  • 批次余额设计

一句话总结:

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

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

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

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

相关推荐
SeeD NICK2 小时前
Spring Boot 3.4 正式发布,结构化日志!
java·spring boot·后端
de_wizard2 小时前
Spring Boot 整合 Apollo 配置中心实战
java·spring boot·后端
用户6757049885022 小时前
AI开发实战1、手摸手教你一行代码不写,全程AI写个小程序——前端布局
后端·aigc·ai编程
ooseabiscuit2 小时前
Spring报错解决一览
java·后端·spring
未秃头的程序猿2 小时前
从“拆东墙补西墙”到“最终一致”:分布式事务在Spring Boot/Cloud中的破局之道
分布式·后端·spring cloud
Java编程爱好者2 小时前
Java高级面试必问:AQS 到底是什么?
后端
dgvri2 小时前
Spring Boot接收参数的19种方式
java·spring boot·后端
做个文艺程序员2 小时前
Function Calling 与工具调用:让 AI 真正干活【OpenClAW + Spring Boot 系列 第5篇】
人工智能·spring boot·后端
rOuN STAT2 小时前
Spring Boot 2.7.x 至 2.7.18 及更旧的版本,漏洞说明
java·spring boot·后端