高安全券码、注册码生成

下面给你提供一个生产级 Java 工具类,可以生成 C6Y2-6CK8-VF7J 这种格式的券码:

  • 去掉易混淆字符(I、1、L、O、0)
  • 可自定义:长度、分组大小、分隔符
  • 高安全性:基于 SecureRandom
  • 支持批量生成 + 去重校验

你给的示例 C6Y2-6CK8-VF7J 实际上只有 13 位有效字符(不含横杠),下面代码按 12 位 + 3 组 实现,你也可以灵活调整。

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Logger;

/**
 * 生产级券码生成器 - JDK 8 兼容版本
 */
public class CouponCodeGenerator {
    private static final Logger logger = Logger.getLogger(CouponCodeGenerator.class.getName());
    private static final String ALPHABET = "ABCDEFGHJKMNOPQRSTUVWXYZ23456789";
    private static final int BASE = ALPHABET.length();  // 32, 2^5
    private static final SecureRandom RANDOM = new SecureRandom();

    // JDK 8 使用 ThreadLocal 避免 StringBuilder 重复创建
    private static final ThreadLocal<StringBuilder> THREAD_BUFFER =
            ThreadLocal.withInitial(() -> new StringBuilder(64));

    /**
     * 生成单个券码
     */
    public static String generate(int totalLen, int groupSize, String separator) {
        validateParams(totalLen, groupSize);

        char[] chars = new char[totalLen];
        for (int i = 0; i < totalLen; i++) {
            chars[i] = ALPHABET.charAt(RANDOM.nextInt(BASE));
        }

        String actualSeparator = separator == null ? "" : separator;
        return formatGroups(new String(chars), groupSize, actualSeparator);
    }

    /**
     * 格式化分组(JDK 8 优化版)
     */
    private static String formatGroups(String str, int groupSize, String separator) {
        if (separator.isEmpty()) {
            return str;
        }

        int len = str.length();
        StringBuilder sb = THREAD_BUFFER.get();
        sb.setLength(0);  // 清空重用

        for (int i = 0; i < len; i++) {
            if (i > 0 && i % groupSize == 0) {
                sb.append(separator);
            }
            sb.append(str.charAt(i));
        }

        return sb.toString();
    }

    /**
     * 批量生成(自动选择串行/并行)
     */
    public static Set<String> generateBatch(int count, int totalLen, int groupSize,
                                            String separator, boolean parallel) {
        // 容量检查
        checkCapacity(count, totalLen);

        if (parallel && count > 5000) {  // JDK 8 并行阈值调低
            return generateBatchParallel(count, totalLen, groupSize, separator);
        } else {
            return generateBatchSequential(count, totalLen, groupSize, separator);
        }
    }

    /**
     * 串行生成(修正计数逻辑)
     */
    private static Set<String> generateBatchSequential(int count, int totalLen,
                                                       int groupSize, String separator) {
        Set<String> codes = new LinkedHashSet<>(count * 2);
        int collisions = 0;
        int maxCollisions = count * 10;
        String actualSeparator = separator == null ? "" : separator;

        while (codes.size() < count && collisions < maxCollisions) {
            String code = generate(totalLen, groupSize, actualSeparator);
            if (!codes.add(code)) {
                collisions++;
            }
        }

        if (codes.size() < count) {
            double rate = collisions * 100.0 / (collisions + codes.size());
            throw new CouponExhaustedException(
                    String.format(Locale.US,
                            "仅生成 %d/%d 个券码,碰撞次数: %d,碰撞率: %.2f%%",
                            codes.size(), count, collisions, rate));
        }
        logger.info(String.format("串行生成 %d 个券码完成,碰撞次数: %d", count, collisions));
        return codes;
    }

    /**
     * 并行生成(JDK 8 兼容版本)
     */
    private static Set<String> generateBatchParallel(int count, int totalLen,
                                                     int groupSize, String separator) {
        // JDK 8 使用 ConcurrentHashMap.newKeySet() 需要 Java 8+
        Set<String> result = ConcurrentHashMap.newKeySet();
        AtomicInteger collisions = new AtomicInteger(0);
        AtomicInteger completed = new AtomicInteger(0);
        int maxCollisions = count * 10;
        String actualSeparator = separator == null ? "" : separator;

        int cores = Runtime.getRuntime().availableProcessors();
        ExecutorService executor = Executors.newFixedThreadPool(cores);

        // 每个线程的目标生成数量
        int perThreadCount = (count + cores - 1) / cores;
        List<Future<Void>> futures = new ArrayList<>();

        for (int t = 0; t < cores; t++) {
            final int targetPerThread = perThreadCount;
            futures.add(executor.submit(() -> {
                Set<String> localSet = new HashSet<>(targetPerThread);
                int localCollisions = 0;
                int maxLocalCollisions = targetPerThread * 10;

                while (localSet.size() < targetPerThread &&
                        result.size() < count &&
                        collisions.get() < maxCollisions) {

                    String code = generate(totalLen, groupSize, actualSeparator);

                    // 先尝试加本地集,再加全局集(减少全局锁竞争)
                    if (localSet.add(code)) {
                        if (result.add(code)) {
                            completed.incrementAndGet();
                        } else {
                            // 全局已存在,从本地移除并计数碰撞
                            localSet.remove(code);
                            localCollisions++;
                            collisions.incrementAndGet();
                        }
                    } else {
                        // 本地重复
                        localCollisions++;
                        collisions.incrementAndGet();
                    }
                }

                // 将本地剩余数据合并到全局
                for (String code : localSet) {
                    result.add(code);
                }

                return null;
            }));
        }

        // 等待所有线程完成
        for (Future<Void> future : futures) {
            try {
                future.get(30, TimeUnit.SECONDS);
            } catch (Exception e) {
                executor.shutdownNow();
                throw new CouponExhaustedException("并行生成失败: " + e.getMessage());
            }
        }

        executor.shutdown();

        if (result.size() < count) {
            throw new CouponExhaustedException(
                    String.format(Locale.US,
                            "并行生成仅得到 %d/%d 个券码,碰撞次数: %d",
                            result.size(), count, collisions.get()));
        }

        logger.info(() -> String.format("并行生成 %d 个券码完成,碰撞次数: %d", count, collisions.get()));
        return new LinkedHashSet<>(result);
    }

    /**
     * 流式生成到文件(避免 OOM,JDK 8 兼容)
     */
    public static void generateBatchToFile(int count, int totalLen, int groupSize,
                                           String separator, String filePath) throws IOException {
        try (PrintWriter writer = new PrintWriter(new BufferedWriter(new FileWriter(filePath)))) {
            Set<String> batchCache = new HashSet<>();
            int batchSize = 10000;  // 每批1万条
            int generated = 0;
            int collisions = 0;
            int maxCollisions = count * 10;
            String actualSeparator = separator == null ? "" : separator;

            while (generated < count && collisions < maxCollisions) {
                String code = generate(totalLen, groupSize, actualSeparator);

                if (batchCache.add(code)) {
                    writer.println(code);
                    generated++;

                    if (generated % batchSize == 0) {
                        writer.flush();
                        batchCache.clear();  // 释放内存
                        logger.info(String.format("已生成 %d/%d 个券码", generated, count));
                    }
                } else {
                    collisions++;
                }
            }

            if (generated < count) {
                throw new CouponExhaustedException(
                        String.format("仅生成 %d/%d 个券码,碰撞次数: %d", generated, count, collisions));
            }

            writer.flush();
            logger.info(() -> String.format("券码已保存到文件: %s", filePath));
        }
    }

    /**
     * 容量检查(使用 BigInteger 避免溢出)
     */
    private static void checkCapacity(int count, int totalLen) {
        BigInteger capacity = BigInteger.valueOf(BASE).pow(totalLen);
        BigInteger needed = BigInteger.valueOf(count);
        BigInteger threshold = capacity.multiply(BigInteger.valueOf(70))
                .divide(BigInteger.valueOf(100));

        if (needed.compareTo(threshold) > 0) {
            double ratio = needed.doubleValue() / capacity.doubleValue() * 100;
            logger.warning(String.format(Locale.US,
                    "警告:生成数量 %d 接近理论容量 %.0e,碰撞概率较高 (%.1f%%)",
                    count, Math.pow(BASE, totalLen), ratio));
        }
    }

    /**
     * 验证券码格式
     */
    public static boolean validate(String code, int totalLen, int groupSize, String separator) {
        if (code == null || code.isEmpty()) return false;

        String actualSeparator = separator == null ? "" : separator;
        String clean = code.replace(actualSeparator, "");

        if (clean.length() != totalLen) return false;

        for (char c : clean.toCharArray()) {
            if (ALPHABET.indexOf(c) == -1) return false;
        }

        // 验证分隔符位置
        if (!actualSeparator.isEmpty()) {
            String reformatted = formatGroups(clean, groupSize, actualSeparator);
            if (!reformatted.equals(code)) return false;
        }

        return true;
    }

    private static void validateParams(int totalLen, int groupSize) {
        if (totalLen < 4 || totalLen > 32) {
            throw new IllegalArgumentException("totalLen 必须介于 4-32 之间");
        }
        if (groupSize < 1 || groupSize > totalLen) {
            throw new IllegalArgumentException("groupSize 必须介于 1-" + totalLen);
        }
    }

    /**
     * 自定义异常
     */
    public static class CouponExhaustedException extends RuntimeException {
        public CouponExhaustedException(String message) {
            super(message);
        }
    }

    /**
     * 测试示例
     */
    public static void main(String[] args) {
        // 1. 基本生成测试
        String code = generate(16, 4, "-");
        System.out.println("生成单个券码: " + code);
        System.out.println("验证结果: " + validate(code, 16, 4, "-") + "\n");

        // 2. 串行批量生成
        long start = System.nanoTime();
        Set<String> batch1 = generateBatch(1000, 12, 4, "-", false);
        long time = (System.nanoTime() - start) / 1_000_000;
        System.out.printf("串行生成 1000 个券码: %d ms%n", time);
        System.out.println("示例: " + batch1.iterator().next() + "\n");

        // 3. 并行批量生成(JDK 8 ForkJoinPool)
        start = System.nanoTime();
        Set<String> batch2 = generateBatch(5000, 12, 4, "-", true);
        time = (System.nanoTime() - start) / 1_000_000;
        System.out.printf("并行生成 5000 个券码: %d ms%n", time);
        System.out.println("示例: " + batch2.iterator().next() + "\n");

        // 4. 理论容量测试
        BigInteger cap = BigInteger.valueOf(BASE).pow(12);
        System.out.printf("12位券码理论容量: %s (%.2e)%n", cap, Math.pow(BASE, 12));

        // 5. 异常测试
        try {
            generateBatch(1000000, 8, 4, "-", false);
        } catch (CouponExhaustedException e) {
            System.out.println("\n预期异常: " + e.getMessage());
        }
    }
}

五、生产环境还需要考虑什么(可选)

  1. 不要用毫秒时间戳参与生成:会降低熵,且分布式下可能重复。
  2. 如果需要更短(8~10位):可以将 ALPHABET 改为 "ABCDEFGHJKLMNPQRSTUVWXYZ123456789"(进一步增加位数密度),但不建议低于 10 位。
  3. 如果券码需要反解出业务信息:可以在前面加固定业务前缀(如 JD),但会降低安全性,需要配合其他风控。

一句话总结:上面的代码就是你要的 C6Y2-6CK8-VF7J 这种风格的高安全、可读性好的 Java 实现,拿来即用。

高熵随机是一个信息论/密码学的概念,简单理解就是:结果非常难以预测,且每个可能的值出现的概率几乎相等

把它拆成两个词来解释,你就明白了。

1. 先看“熵”

在这里代表不确定性混乱程度

  • 低熵:非常确定,可预测。比如流水号 000001, 000002...。你看到 000001,就能猜出下一个大概率是 000002
  • 高熵:非常不确定,不可预测。比如一个真正的随机数。你看到 A3F9,完全无法猜出下一个是 K2M7 还是 9D4F

熵越高,意味着你掌握的信息越少,猜测的难度越大。

2. 再来看“高熵随机”

它不是一个具体算法,而是对随机性质量的形容,通常具备三个特点:

  • 真正的随机源:不是用 new Random() 这种基于时间戳的“伪随机”(可预测),而是用 SecureRandom 这类基于硬件噪音、鼠标移动等物理现象的“密码学安全随机”。
  • 充足的位数:比如12位,每位有32种可能(去掉混淆字符后),总组合空间巨大(32^12),无法暴力枚举。
  • 均匀分布:每个字符(如A、B、C...9)出现的概率是相等的,不会出现某个字母明显偏多。

举个直观的例子

类型 例子 熵的高低 原因
流水号 MP240615000001 极低熵 看到前面,就能推算后面;攻击者可遍历。
伪随机 new Random().nextInt() 生成的 845321 中等熵 看似随机,但如果你知道种子(时间戳),就能推算出整个序列。
高熵随机 SecureRandom 生成的 C6Y2-6CK8-VF7J 极高熵 无法预测下一张券码;哪怕你拿到1000万个历史码,也无法推算第1001个。

对你之前的券码问题来说

  • 你问 C6YV2-6CKK8-VF7JB 怎么样
    → 这就是高熵随机的典型例子。它无法被暴力遍历,因为攻击者面对的是天文数字级别的可能性,只能一个个瞎猜。

  • 为什么不用 MP240615000123
    → 因为它是低熵的,攻击者可以系统性地枚举(从1试到100万),几分钟就能扫光。

一句话总结

高熵随机 = 用真正的随机源生成的、组合空间巨大、无法预测的随机数。它是防暴力遍历、防猜测的核心武器。

在实际开发中,用 java.security.SecureRandom 生成的券码,就可以称为“高熵随机券码”。

posted @ 2026-05-29 16:14  VipSoft  阅读(14)  评论(0)    收藏  举报