告别订单号重复!Spring Boot + MySQL 优雅生成唯一批次号实战!!!

告别订单号重复!🎉 Spring Boot + MySQL 优雅生成唯一批次号实战

哈喽,各位开发者伙伴们!👋 在日常开发中,生成唯一的订单号、流水号或批次号是一个非常常见的需求。最近我在处理一个寄售(Consignment)业务时,就遇到了一个关于批次订单号 (orderNo) 生成的小挑战,并最终采用了一种简单高效的方式解决了它。今天就来和大家分享一下这个过程,希望能给大家带来一些启发!💡

遇到的问题:恼人的“Duplicate entry” 😭

最初的需求是:前端一次请求会发送一个包含多个寄售商品信息的数组到后端 /saveConsignmentSummary 接口,后端需要为这次请求的所有商品生成一个相同的批次订单号 (orderNo)。下次请求再来时,生成的 orderNo 要和上一次不同。

听起来很简单?🤔 我一开始也是这么觉得的!于是我快速地在 Controller 层生成了一个基于“高精度时间戳+随机数”的 orderNo,并赋给列表中的每一个对象,然后调用 saveAll 保存。

然而,现实很快给了我一记“重拳” 🧱:

ERROR ... Duplicate entry 'CON20250430145255427-4083' for key 'consignment_summary.UK_catfmq4ovjdvqndwvh6r6jas1'
Caused by: java.sql.SQLIntegrityConstraintViolationException: Duplicate entry ...

啊哦!数据库报了唯一约束冲突的错误!😱

检查了一下 ConsignmentSummary 实体类和数据库表结构:

// 实体类注解 (部分)
@Column(name = "order_no", unique = true, nullable = true, length = 50)
private String orderNo;
-- 数据库表结构 (部分)
create table consignment_summary (
    -- ... 其他列 ...
    order_no varchar(50) null,
    constraint UK_catfmq4ovjdvqndwvh6r6jas1 unique (order_no) -- 问题根源!
);

原因很明显了:我的代码逻辑要求同一批次内的多条记录拥有相同orderNo,而数据库层面却给 order_no 加了唯一约束!这两者是互相矛盾的。当 saveAll 尝试插入第二条具有相同 orderNo 的记录时,数据库就无情地拒绝了。❌

解决方案:调整约束 + 时间戳大法 ✨

既然 orderNo 的业务含义是批次号,允许多条记录共享同一个批次号,那么解决方案就很清晰了:

  1. 移除数据库唯一约束 🔧:让数据库允许 order_no 列存在重复值。
  2. 更新实体类注解 📝:保持代码与数据库结构一致。
  3. 坚持时间戳+随机数生成策略 ✅:继续使用方案三生成批次号,因为它简单且能满足批次间的唯一性。

Step 1: 移除数据库唯一约束

⚠️ 警告: 修改数据库结构前,请务必备份!

根据之前的表结构,执行以下 SQL 语句删除 order_no 上的唯一索引(即唯一约束):

ALTER TABLE consignment_summary
DROP INDEX UK_catfmq4ovjdvqndwvh6r6jas1;

Step 2: 更新实体类注解

修改 ConsignmentSummary.java,移除 orderNo 字段上 @Column 注解的 unique = true 属性:

import javax.persistence.Column;
// ... 其他 import ...

public class ConsignmentSummary extends BaseEntity {
    // ... 其他字段 ...

    @ApiModelProperty(value = "单号", example = "CON20231027001")
    // 移除了 unique = true
    @Column(name = "order_no", nullable = true, length = 50)
    private String orderNo;

    // ... 其他字段 ...
}

Step 3: Controller 生成批次号逻辑 (最终版)

Controller 中的代码基本保持不变,核心逻辑就是为整个请求生成一个 batchOrderNo,并应用到所有条目上:

import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import springfox.documentation.annotations.ApiIgnore;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;

@RestController
@RequestMapping("/api/consignmentSettlement")
public class ConsignmentSettlementController {

    @Autowired
    private ConsignmentSummaryService consignmentSummaryService;

    @PostMapping("/saveConsignmentSummary")
    @ApiOperation("保存寄售总表 (基于时间戳+随机数生成批次订单号)")
    public BaseResult saveConsignmentSummary(
            @ApiIgnore @SessionAttribute(Constants.ADMIN_ID) Integer adminId,
            @RequestBody List<ConsignmentSummary> consignmentSummaries) {
        try {
            if (consignmentSummaries == null || consignmentSummaries.isEmpty()) {
                return BaseResult.failure(400, "保存列表不能为空");
            }

            // ✨ 核心:为本次请求生成唯一的批次号 ✨
            String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"));
            String randomPart = String.format("%04d", ThreadLocalRandom.current().nextInt(0, 10000));
            String batchOrderNo = "CON" + timestamp + "-" + randomPart; // "CON" 是业务前缀

            // 遍历列表,设置 adminId 和 统一的批次号
            for (ConsignmentSummary summary : consignmentSummaries) {
                summary.setAdminId(adminId);
                summary.setOrderNo(batchOrderNo); // 应用同一个批次号
            }

            // 批量保存
            consignmentSummaryService.saveAll(consignmentSummaries);

            return BaseResult.success("保存成功,批次订单号:" + batchOrderNo);

        } catch (Exception e) {
            System.err.println("保存 ConsignmentSummary 列表时发生错误: " + e.getMessage());
            e.printStackTrace();
            return BaseResult.failure(500, "保存处理失败: " + e.getMessage());
        }
    }
    // ... 其他方法 ...
}

现在,再次发送请求,包含多条记录的数组,saveAll 操作就能顺利执行了!🚀 同一批次的记录共享同一个 orderNo,不同批次的 orderNo 也因时间戳和随机数而不同。

总结与流程回顾 📊

下面用表格总结一下整个过程:

方面描述
🎯 目标为同一请求内的多条记录生成相同且唯一的批次 orderNo
😭 初始问题order_no 列存在数据库唯一约束,导致 saveAll 时报 Duplicate entry 错误。
🤔 原因分析代码逻辑(共享批次号)与数据库约束(orderNo 唯一)冲突。
解决方案1. 移除数据库唯一约束。 2. 更新实体类注解。 3. 采用时间戳+随机数生成批次号。
🔑 关键代码ALTER TABLE ... DROP INDEX ... (SQL) & Controller 中的批次号生成逻辑 (Java)
🎉 最终结果成功实现需求,同一批次记录共享 orderNo,不同批次 orderNo 不同。

业务处理流程图 (Mermaid Flowchart) 🌊

核心逻辑
数据库交互
对每个 summary
遍历完成
Controller: 生成唯一批次订单号 (时间戳+随机数) batchOrderNo
Controller: 遍历 List
设置 summary.setAdminId(adminId)
设置 summary.setOrderNo(batchOrderNo)
Service: 执行批量插入数据库 (多条记录共享相同 orderNo)
接收 /saveConsignmentSummary 请求 (List)
调用 ConsignmentSummaryService.saveAll(list)
返回成功响应 (包含 batchOrderNo)

请求处理时序图 (Mermaid Sequence Diagram) ⏳➡️

客户端 ConsignmentSettlementController ConsignmentSummaryService MySQL数据库 POST /api/saveConsignmentSummary (含List<Summary>) 生成唯一批次订单号 batchOrderNo (时间戳+随机数) summary.setAdminId(adminId) summary.setOrderNo(batchOrderNo) loop [遍历 List<Summary>] saveAll(处理后的List<Summary>) 执行批量 INSERT INTO consignment_summary (...) VALUES (...), (...), ... (共享相同 orderNo) 批量插入成功 保存成功 返回 BaseResult.success (含 batchOrderNo) 客户端 ConsignmentSettlementController ConsignmentSummaryService MySQL数据库

结语 👍

通过移除不必要的数据库唯一约束,并坚持使用“高精度时间戳+随机数”的策略,我们成功解决了批次订单号生成的问题。这个方法虽然简单,但在并发量不是特别极端,且不强求严格递增的场景下,是一个非常实用且易于实现的方案。

当然,对于需要严格递增超高并发下绝对无冲突的场景,可以考虑使用数据库序列(如 PostgreSQL/Oracle 的 SEQUENCE 或 MySQL 的模拟序列表方案,见之前讨论)。选择哪种方案,最终还是要根据具体的业务需求和系统环境来决定。

希望这次分享对你有所帮助!如果你有更好的方法或者遇到了其他有趣的问题,欢迎在评论区交流!✨ Happy Coding! 🎉


Markdown 思维导图 🧠🔗

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值