You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Spring Boot与Hibernate的发票号动态生成器开发问题

解决Hibernate生成依赖发票日期的【年月/递增编号】发票号问题

看起来你已经走对了方向——自定义IdentifierGenerator是实现这种业务专属编号的正确思路,但当前代码的核心问题是用了系统当前时间而非实体的发票日期来生成年月前缀,而且序列统计逻辑没有按年月分组。下面我会一步步帮你调整代码,实现需求:

1. 先确保实体类包含发票日期字段

首先你的Invoice实体必须有明确的发票日期字段,并且在保存前要设置好这个值,这样生成器才能拿到业务上的发票日期:

@Entity
@Table(name = "invoice")
public class Invoice {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO, generator = "year_gen")
    @GenericGenerator(name = "year_gen", strategy = "com.example.generator.CustomGenerator")
    @Column(name = "invoice_no")
    private String invoiceNo;

    // 必须存在的发票日期字段,保存前需赋值
    @Column(name = "invoice_date", nullable = false)
    private LocalDate invoiceDate;

    // 其他业务字段、getter/setter省略
}

2. 重写自定义生成器逻辑

修改你的CustomGenerator,核心改动点:

  • 从传入的实体对象中获取发票日期,而非系统当前时间
  • 按发票日期的年月分组统计最大编号,确保每个年月的序列独立递增
  • 处理并发场景的锁机制,避免重复编号
public class CustomGenerator implements IdentifierGenerator {
    @Override
    public Serializable generate(SharedSessionContractImplementor sessionImpl, Object data) throws HibernateException {
        // 校验传入的实体类型
        if (!(data instanceof Invoice)) {
            throw new HibernateException("CustomGenerator仅支持Invoice实体的ID生成");
        }

        Invoice invoice = (Invoice) data;
        LocalDate invoiceDate = invoice.getInvoiceDate();
        // 强制要求保存前设置发票日期,避免生成错误编号
        if (invoiceDate == null) {
            throw new HibernateException("保存发票前必须设置invoiceDate字段");
        }

        // 从发票日期提取年月前缀
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMM");
        String yyyymm = formatter.format(invoiceDate);
        String prefix = "INV/" + yyyymm + "/";
        String invoiceNo = null;

        Connection connection = null;
        PreparedStatement statement = null;
        ResultSet resultSet = null;

        try {
            connection = sessionImpl.connection();
            // 按年月前缀查询当前最大编号,并用FOR UPDATE锁避免并发重复
            String sql = """
                SELECT MAX(CAST(SUBSTRING(invoice_no, ?) AS INTEGER)) 
                FROM invoice 
                WHERE invoice_no LIKE ? 
                FOR UPDATE
                """;
            statement = connection.prepareStatement(sql);
            // SUBSTRING从索引1开始,所以起始位置是前缀长度+1
            statement.setInt(1, prefix.length() + 1);
            statement.setString(2, prefix + "%");

            resultSet = statement.executeQuery();
            int nextSeq = 1;
            // 如果当前年月已有发票,取最大编号+1
            if (resultSet.next() && resultSet.getInt(1) != 0) {
                nextSeq = resultSet.getInt(1) + 1;
            }

            // 格式化为5位补零的后缀,拼接成最终发票号
            String suffix = String.format("%05d", nextSeq);
            invoiceNo = prefix.concat(suffix);

        } catch (SQLException e) {
            throw new HibernateException("生成发票号失败", e);
        } finally {
            // 关闭数据库资源
            try {
                if (resultSet != null) resultSet.close();
                if (statement != null) statement.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }

        return invoiceNo;
    }
}

3. 关键逻辑说明

  • 依赖发票日期:生成器直接从实体的invoiceDate字段获取年月,完全贴合业务需求,不再依赖系统时间
  • 年月独立序列:通过WHERE invoice_no LIKE 'INV/yyyyMM/%'过滤,每个年月的编号单独统计,自然实现财年结束后序列重置
  • 并发安全:添加FOR UPDATE锁,确保同一时间只有一个请求能查询并计算当前年月的最大编号,避免重复生成
  • 强制校验:如果未设置发票日期就保存,直接抛出异常,避免生成无效编号

4. 使用注意事项

  • 保存发票前必须先设置invoiceDate字段,比如:
    Invoice invoice = new Invoice();
    invoice.setInvoiceDate(LocalDate.of(2024, 5, 15)); // 设置业务上的发票日期
    invoiceRepository.save(invoice);
    
  • 如果你的财年不是自然年月(比如每年4月到次年3月),可以调整yyyyMM的格式化逻辑,比如自定义财年的年月转换规则

内容的提问来源于stack exchange,提问作者Shiv1987

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.07 19:52:32