PaperIdGenerator.php 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. <?php
  2. namespace App\Services;
  3. /**
  4. * 试卷ID生成器
  5. * 参考行业标准 Snowflake ID 思想
  6. *
  7. * 【修复】增强多进程并发安全:
  8. * - 添加进程ID区分不同 PHP-FPM Worker
  9. * - 增加随机数位数降低碰撞概率
  10. */
  11. class PaperIdGenerator
  12. {
  13. // 基准时间:2020-01-01 00:00:00 (秒)
  14. private const EPOCH = 1577836800;
  15. // 15位数字的位分配
  16. // 格式:时间戳(8位) + 进程ID(2位) + 序列号(2位) + 随机数(3位) = 15位数字
  17. private const TIMESTAMP_BITS = 35; // 时间戳(秒)- 可覆盖约34年
  18. private const SEQUENCE_BITS = 11; // 序列号 - 2048个值
  19. /**
  20. * 生成15位数字ID(增强版,支持多进程并发)
  21. * 格式:TTTTTTTT + PP + SS + RRR(时间戳 + 进程ID + 序列号 + 随机数)
  22. *
  23. * @return string 15位数字字符串
  24. */
  25. public static function generate(): string
  26. {
  27. // 使用时间戳(分钟)而不是秒,以减少位数
  28. $timestamp = intdiv(time() - self::EPOCH, 60); // 从基准时间开始的分钟数
  29. $timestamp &= (1 << self::TIMESTAMP_BITS) - 1; // 截取指定位数
  30. // 获取进程ID(区分不同的 PHP-FPM Worker)
  31. $processId = getmypid() % 100; // 2位进程ID:00-99
  32. // 同一分钟内使用序列号递增,避免并发重复
  33. static $lastTimestamp = 0;
  34. static $sequence = 0;
  35. if ($timestamp == $lastTimestamp) {
  36. $sequence = ($sequence + 1) & ((1 << self::SEQUENCE_BITS) - 1);
  37. // 序列号用完,等待下一分钟
  38. if ($sequence == 0) {
  39. do {
  40. $timestamp = intdiv(time() - self::EPOCH, 60);
  41. $timestamp &= (1 << self::TIMESTAMP_BITS) - 1;
  42. } while ($timestamp <= $lastTimestamp);
  43. }
  44. } else {
  45. $sequence = 0;
  46. }
  47. $lastTimestamp = $timestamp;
  48. // 添加随机数后缀避免猜测(3位:000-999,碰撞概率 0.1%)
  49. $random = random_int(0, 999);
  50. // 组合:时间戳(8位) + 进程ID(2位) + 序列号(2位) + 随机数(3位) = 15位数字
  51. $timestampStr = str_pad((string)$timestamp, 8, '0', STR_PAD_LEFT);
  52. $processIdStr = str_pad((string)$processId, 2, '0', STR_PAD_LEFT);
  53. $sequenceStr = str_pad((string)($sequence % 100), 2, '0', STR_PAD_LEFT); // 确保只取2位
  54. $randomStr = str_pad((string)$random, 3, '0', STR_PAD_LEFT);
  55. $id = $timestampStr . $processIdStr . $sequenceStr . $randomStr;
  56. // 确保第一位不为0
  57. if ($id[0] === '0') {
  58. $id[0] = '1';
  59. }
  60. return $id;
  61. }
  62. /**
  63. * 验证数字ID格式(15位)
  64. *
  65. * @param string $id
  66. * @return bool
  67. */
  68. public static function validate(string $id): bool
  69. {
  70. return preg_match('/^[1-9]\d{14}$/', $id) === 1;
  71. }
  72. /**
  73. * 从ID提取时间戳
  74. *
  75. * @param string $id
  76. * @return int|null
  77. */
  78. public static function extractTimestamp(string $id): ?int
  79. {
  80. if (!self::validate($id)) {
  81. return null;
  82. }
  83. // 提取前8位时间戳(分钟),转换为秒
  84. $timestampMinutes = (int)substr($id, 0, 8);
  85. return $timestampMinutes * 60 + self::EPOCH;
  86. }
  87. /**
  88. * 批量生成不重复的ID
  89. *
  90. * @param int $count
  91. * @return array
  92. */
  93. public static function generateBatch(int $count): array
  94. {
  95. $ids = [];
  96. $attempts = 0;
  97. $maxAttempts = $count * 10;
  98. while (count($ids) < $count && $attempts < $maxAttempts) {
  99. $id = self::generate();
  100. if (!in_array($id, $ids)) {
  101. $ids[] = $id;
  102. }
  103. $attempts++;
  104. }
  105. return $ids;
  106. }
  107. }