
简介一套基于PHP的二维码防重防错系统主要面向需要生成每日唯一二维码、并对扫码过程进行防重防错管理的中小企业、仓储或巡检场景。系统将首页设计为扫码功能页面支持生成当日及任意日期的二维码打印生成后的二维码并统计二维码总数、当天生成数、打印数量和扫码次数等关键指标。后台设置模块涵盖二维码前缀1~8位、默认日打印量、是否携带年月日序号以及序号位数等参数安装后需先完成基础数据配置再生成二维码以避免编码冲突。资源已打包为zip压缩包大小约5.24MB方便直接部署学习和改造目前已有233人学习使用适合具备基础PHP开发能力、想了解二维码生成与防重防错逻辑的读者可通过阅读源码快速掌握日期序号编码、扫码校验与统计查询的实现方式并应用到自有业务中。1. 二维码防重防错系统为什么你的码总是重号、错扫、被薅羊毛某次在一个做食品包装印刷的工厂里我亲眼看到整条产线因为一批二维码标签重复而停工返工。那个场景很直接同一盒巧克力上贴着两个一模一样的溯源码消费者扫码后看到的是另一包产品的信息售后电话直接被投诉打爆。这个问题不是个例凡是做过二维码生成系统的PHP开发者基本都踩过类似的坑并发下重复码、内容编码被扫码枪误读、状态流转后还能二次核销。市面上单讲“生成二维码”的库和教程很多但真正把“防重”和“防错”两条链路都做完整的项目很少。这套PHP版的二维码防重防错系统解决的就是这两个核心问题码的唯一性怎么做到底码的校验怎么做闭环。它覆盖从发号、生成、入库、校验到状态流转的完整流程适合正在做溯源、营销活动、电子凭证、仓储标签这类需要“一码一物”场景的PHP开发者也适合准备接触二维码业务开发的人当作工程样板来读。2. 二维码生成不止是库的选择编码策略决定后面少踩多少坑很多人拿到二维码需求第一反应是找个PHP库把字符串转成图片能扫出来就完事了。结果上线后问题全在编码层内容太密扫不出、中文转码乱掉、码被扫码枪误读成另一个码。这一章先用实际代码过一遍生成环节的技术选型和编码设计这部分是后面防重防错的地基地基歪了后面加再多的校验都拉不回来。2.1 两个主流PHP二维码库怎么选轻量派与工程派的取舍PHP生态里生成二维码用得最多的两个库一个是老牌的phpqrcode一个是基于endroid/qr-code的现代方案。这两个我都在生产项目里用过差别很具体。phpqrcode的优点是零依赖一个PHP文件就能跑适合老项目或者不允许装Composer的服务器环境。它输出PNG的方式很直接但也因为太直接格式支持只停留在图片文件层面想输出SVG、想在图片上加Logo、想控制容错级别和颜色都得手动再处理一层。而且它内部对中文内容默认走UTF-8但某些扫码设备处理非ASCII字符时仍会翻车。endroid/qr-code是Composer安装的现代库支持PNG、SVG、EPS等输出格式API设计也更贴近“生成一张含参数的码”这种工程语义。它把容错级别、尺寸、Margin、颜色、Logo嵌入都做成了构造参数测试和后续维护都轻松不少。主流新项目用这个更合适。如果服务器环境完全离线、也没有Composer那老老实实用phpqrcode把内容压缩成短码再生成如果能正常走Composer直接上endroid/qr-code省掉很多自己封装的时间。下面以endroid/qr-code为例写一套生成实现。2.2 基础生成代码参数逐步拆解先通过Composer装依赖composer require endroid/qr-code一个最基础的生成并保存到本地的例子?php // 引入自动加载文件 require __DIR__ . /vendor/autoload.php; use Endroid\QrCode\QrCode; use Endroid\QrCode\Writer\PngWriter; use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelMedium; // 二维码承载的内容注意这里不要直接塞整个业务数据 $payload QR2024A00100012345; // 创建二维码对象 $qrCode QrCode::create($payload) -setSize(300) // 输出图片边长单位像素 -setMargin(10) // 白边大小太窄部分设备可能扫不出 -setErrorCorrectionLevel(new ErrorCorrectionLevelMedium()); // PNG写入器 $writer new PngWriter(); $result $writer-write($qrCode); // 保存到本地 file_put_contents(/data/qrcodes/QR2024A00100012345.png, $result-getString());这段代码的核心逻辑是构造“内容字符串”到“二维码图片”的转换。QrCode::create()接收的字符串就是最终扫码设备读出来的内容这个内容的设计比图片样式重要得多。setSize(300)控制像素尺寸常见场景下300乘300足够setMargin(10)是白边扫码设备识别时需要这圈白边做定位设成0会导致识别率明显下降。setErrorCorrectionLevel()是很多人容易忽略的参数。容错级别从低到高是 L、M、Q、H级别越高码面能承受的污损和遮挡越多但码的复杂度也随之上升。手机扫码场景用M就够了印刷标签建议用到Q因为印刷过程的油墨扩散和贴标褶皱会让识别变得困难。2.3 编码内容的设计短码优先别把JSON塞进二维码一个频繁出现的翻车现场开发图省事把数组json_encode之后整个塞进二维码。结果就是码眼越密集印刷出来黑压压一片扫码设备要么识别慢要么直接识别失败。二维码内容设计的工程准则是二维码里只放稳定且短的标识符业务数据放服务端。比如一张生产订单的二维码内容不是存订单的所有字段而是存一个唯一编码扫码后拿这个编码调接口查详情。这样做有三个直接好处第一码面简洁识别快第二编码长度固定后生成重复码的概率窗口变小第三业务字段变动时不用重新印码。?php // 推荐的内容格式业务前缀 批次号 序号 校验位 $prefix QR; // 业务前缀固定长度用于区分业务线 $batchNo A001; // 批次号区分不同生产批次 $sequence sprintf(%08d, 12345); // 序号定长8位不足前补零 $payload $prefix . $batchNo . $sequence; // 校验位单独生成后面防错章节会展开讲 $payload $payload . generateCheckDigit($payload);注意这里的sprintf(%08d, 12345)序号固定位数的意义在于保证同一批次的二维码长度一致。如果序号不补零QR0001和QR00001就会被设备识别成两个内容人眼看不出问题数据库查重时会搞出重复怀疑所以建议序号一律定长。3. 防重机制从数据库唯一索引到Redis锁的层层兜底二维码生成系统的第一道生死线是“不能重码”。开发阶段单线程测不出来一到生产并发批次生成、多个工人同时点按钮的场景就会原形毕露。防重的正确姿势是分层设计业务层生成唯一码靠发号算法存储层靠数据库唯一索引兜底并发入口靠Redis锁挡住重复提交。这一章按从简单到复杂的顺序把三条防重战线全部铺开。3.1 唯一码生成方案时间戳、随机数、雪花ID各自的边界最常见也最省事的做法是用时间戳加随机数拼一个字符串当唯一码?php $code date(YmdHis) . rand(100000, 999999);这个方案在单机、低频场景下能用但它有两个隐患并发高的时候同一秒内随机数可能碰撞加前缀后撞概率虽然不高但溯源类业务一旦撞码就是事故服务器时间如果被NTP回调后生成的码可能比先生成的码时间早破坏按时间排序的逻辑。所以时间戳随机数方案只适合内部测试不适合正式防重。推荐的做法是生成带业务语义的自增序号由数据库或发号器统一分配?php // 利用MySQL的自增主键拿批次范围内的号段 $startId 100000; $endId 200000; // 对每个码取当前批次下一个自增值作为序号 $sequence $redis-incr(qr:sequence:batch:A001); if ($sequence $endId) { throw new \RuntimeException(当前批次号段已耗尽); } $code QR . A001 . sprintf(%08d, $sequence);这里用Redis的incr做分布式递增计数原因在于多台PHP应用服务器同时生成码时本地文件锁或数据库自增都容易变成瓶颈而Redis单线程的原子自增天然不会重复。incr的返回值是当前批次已经发到第几个超出号段上限就说明批次用完需要开新批次。如果项目里没有Redis退而求其次用数据库事务配合SELECT ... FOR UPDATE也能实现但吞吐量会明显下降。另外一个补充方案是雪花算法优点是纯本地计算、不依赖外部服务缺点是需要处理机器ID分配和时间回拨问题代码复杂度比Redis自增高一个量级。3.2 数据库层的最后一道兜底唯一索引与自愈逻辑业务层的唯一码设计得再好也不能保证并发场景下没有极端碰撞漏网。数据库是最后一道防线必须在表结构上就把“重复”堵死。CREATE TABLE qr_code ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键, code VARCHAR(64) NOT NULL COMMENT 二维码编码内容, batch_no VARCHAR(32) NOT NULL COMMENT 批次号, status TINYINT NOT NULL DEFAULT 0 COMMENT 状态: 0待生成 1已生成 2已激活 3已核销 4已作废, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id), UNIQUE KEY uk_code (code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT二维码台账表;建表的关键点是UNIQUE KEY uk_code (code)这一行索引能挡住任何程序逻辑缺陷导致的重复写入。InnoDB引擎在插入重复键值时不会静默跳过而是直接报Duplicate entry错误所以插入前要主动处理这种冲突。?php try { $stmt $pdo-prepare(INSERT INTO qr_code (code, batch_no, status) VALUES (?, ?, 0)); $stmt-execute([$code, $batchNo]); } catch (\PDOException $e) { if ($e-getCode() 23000) { // 主键/唯一索引冲突说明码重复了 // 此时不要再尝试插入而是记录冲突并重新生成码 logConflict($code, $batchNo); // 重新走发号流程生成下一个码 continue; } throw $e; }这段代码的作用是让PDO捕获唯一索引冲突的异常进入自愈分支不把重复码写入表而是记录日志后重新生成下一个码。getCode() 23000是SQLSTATE的完整性约束冲突代码不仅包含唯一索引还包括外键约束失败等情况可以用errorInfo[1]里的MySQL错误码1062精确定位重复插入。3.3 并发提交与Redis分布式锁锁住批量生成入口数据库唯一索引只能保证写入不重复治不了另一类问题同一批次的生成任务被两个管理员同时点了两遍导致重复劳动、重复打印、甚至一半码写入一半又停止。这种情况靠锁入口解决。?php // 批次生成二维码的入口加锁防止重复提交 $lockKey qr:lock:batch: . $batchNo; $lockToken uniqid(, true); // 只有拿到锁的进程才能继续 $locked $redis-set($lockKey, $lockToken, [NX, EX 300]); if (!$locked) { throw new \RuntimeException(该批次正在生成中请勿重复操作); } try { // 在锁保护下执行批量生成 generateBatchCodes($batchNo, $total); } finally { // 释放锁时校验token防止误删其他进程的锁 $script LUA if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end LUA; $redis-eval($script, [$lockKey, $lockToken], 1); }set($lockKey, $lockToken, [NX, EX 300])是Redis的原子操作NX表示只有键不存在时才写入确保同一时间只有一个请求能拿到锁EX设置锁的过期时间为300秒防止进程崩溃导致锁永远不释放。锁Token用uniqid(, true)生成释放锁时通过Lua脚本比对Token只删除自己持有的锁避免误删别人刚拿到的锁。生成完码之后记得主动释放锁不要全指望过期时间。虽然EX已经兜底但如果业务执行时间超过过期时间锁会自动释放另一个请求会趁虚而入产生双写。所以锁的过期时间要设置得比最慢一次批量生成更宽裕一般取正常耗时的5到10倍。4. 防错校验校验位、状态机与幂等消费的三重闭环防住了重复码只是解决了“每一个码都唯一”的问题。接下来要解决的是“每一个码都能被正确校验”——扫码后能安全地走完业务流程不被他人的错误码干扰。这一章的防错设计覆盖三个层面编码层面的校验位防误读业务层面的状态机防乱序接口层面的幂等防重复消费。4.1 校验位算法与实现让误读的码当场现形二维码和条形码不同虽然有一定容错能力但在印刷质量差、扫码设备偏旧、光照复杂的环境里仍然存在一位字符被误识别的可能性。这时如果系统直接把误读的码当作合法码处理轻则数据错乱重则资损。解决方案是在编码里内嵌一位校验位。?php // 生成校验位对编码主体做加权求和后取模 function generateCheckDigit(string $data): string { $weights [3, 7, 1]; // 循环加权因子 $sum 0; $len strlen($data); for ($i 0; $i $len; $i) { $charValue ord($data[$i]); $sum $charValue * $weights[$i % 3]; } // 取模10的结果映射成单个字符 return (string)($sum % 10); } // 校验时重新计算并比对 function verifyCheckDigit(string $payload): bool { if (strlen($payload) 2) { return false; } $data substr($payload, 0, -1); // 去掉最后一位校验位 $checkDigit substr($payload, -1); // 末位是校验位 return generateCheckDigit($data) $checkDigit; }这个算法的思路与EAN-13条形码的校验位类似但不是照搬而是用ASCII码值而不是数字位做加权累加这样对字母数字混合编码也同样适用。加权因子[3, 7, 1]循环使用比固定因子更灵敏相邻位置写反也会导致校验失败。取模10得到0到9的单字符直接拼在编码末尾。扫码校验的第一步永远是先跑verifyCheckDigit()通过后才继续查数据库。这个前置判断能拦截掉大量印刷脏污、磨损、残缺导致的误读码让“错码”在业务逻辑之前就被扔掉。4.2 状态机设计从待激活到核销每一步都留下脚印二维码的内容本身是静态的但它在业务系统里的状态是动态的。如果不设计状态流转规则就会出现“核销过的码还能再核销一次”的严重漏洞。一套完整的二维码台账需要最少四条状态0 - 待生成批次号已分配码尚未生成 1 - 待激活码已生成尚未进入流通 2 - 已激活已投入使用可以正常扫码 3 - 已核销已完成一次业务核销不可再次使用 4 - 已作废异常情况下的终止态状态机更新用SQL条件更新实现原子性?php $sql UPDATE qr_code SET status 3, verify_time NOW(), updated_at NOW() WHERE code :code AND status 2; $stmt $pdo-prepare($sql); $stmt-execute([:code $code]); if ($stmt-rowCount() 0) { // 更新影响行数为0说明状态不是“已激活”拒绝核销 throw new \RuntimeException(当前二维码状态不可核销); }这里的关键点是WHERE code :code AND status 2在一条SQL里同时校验码存在和状态合法。UPDATE的rowCount()只有在状态确实从2变成3时才返回1如果码已被核销过、已作废或根本不存在结果为0业务自然被拒。这比先SELECT再UPDATE的写法安全得多因为后者在并发场景下两个请求同时读到status2然后都去更新就能双花。4.3 接口幂等校验Redis按码去重阻止同一码秒级重复请求状态机解决的是长期重复核销问题但防不住另一类短期并发用户手速过快在0.1秒内连续发起两次扫码请求两次都读到status2然后分别执行核销。为了避免这种瞬间并发破坏状态机需要在接口层做幂等去重。?php // 核销接口的幂等判断 $idemKey qr:verify:idem: . $code; // 第一次请求能设置成功后续重复请求设置失败 $first $redis-set($idemKey, 1, [NX, EX 10]); if (!$first) { // 10秒内已有请求在处理该码直接拒绝 throw new \RuntimeException(请勿重复提交); } // 幂等锁拿到后执行上面的状态更新逻辑RedisSET NX EX在这里充当了短时间内的“请求去重器”。第一次请求设置成功后续请求在10秒内设置时发现键已存在直接抛异常。EX 10的时间窗口要大于状态更新的SQL执行时间一般2到5秒足够10秒是为了应对慢查询的裕量。等锁过期后码的状态已经变成已核销即便请求再次进来也会在上一步的状态机条件更新里被拦住。两层配合长周期和短周期都能覆盖。5. 避坑与常见问题排查清单生产环境里真实踩过的五个坑前面几章写的都是设计思想这一章把实践环节最容易翻车的具体场景列出来。每一条都是从真实故障里复盘出来的格式统一按“现象 → 原因 → 解决”来写排查时可以直接对照。5.1 刚印好的二维码死活扫不出现象用手机和扫码枪扫同一批标签部分码完全无反应。原因二维码内容过长导致版本号升高、模块密度变大印刷稍有扩散就超出设备的纠错能力。这种情况往往出现在直接把JSON或超长URL塞进码里的设计里。解决规范编码内容为短码业务参数一律放服务端。同时把容错级别从默认的Low提升为Medium或Quartile印刷场景定在Medium以上。印刷前用在线解码工具随机抽检5%的码验证可读性。5.2 MySQL唯一索引报错应用日志出现大量Duplicate entry现象批量生成二维码时日志刷出Duplicate entry QR... for key uk_code部分码未生成成功。原因并发场景下两个进程同时从发号器拿到同一个序号或者发号逻辑在时间回拨时产生了相同内容。业务层的唯一码设计存在漏洞。解决唯一索引保留不能去掉。将发号迁移到Redisincr或数据库自增序列保证序号源唯一。冲突发生时不要重试同一个码而是生成下一个码进入下一轮写入循环同时记录冲突日志。5.3 已核销的码在另一个渠道还能再次核销现象同一个券码在A渠道核销成功后短时间内又被B渠道核销成功造成资损。原因状态更新SQL没有把status作为条件写入或者业务先用SELECT判断再执行UPDATE产生了时间窗口竞争。解决改成单条条件更新语句UPDATE ... WHERE code ? AND status 2以rowCount()判断是否成功。秒级并发下再用Redis幂等键拦截真正实现“一次核销处处拦截”的效果。5.4 批次号用时间生成服务器时间回拨后出现了重复批次现象某天NTP同步导致服务器时间倒退新生成的批次号与前一天相同历史二维码内容与新批次完全一样。原因批次号直接取自date(Ymd)时间回拨后生成的批次号自然与过去重合。解决批次号改为数据库自增主键或Redis自增值时间只作展示字段不作业务标识。生成码时用批次自增ID拼接序号彻底与机器时间解耦。5.5 扫一个模糊的码跳到了另一个接近的码现象用户反馈扫描结果与标签上肉眼看到的文字不一致两个码内容很相似。原因二维码内容本身没有冗余校验识别错误后系统照单全收误读码被当作合法码处理了。解决把校验位纳入编码内容扫码后先校验末位通不过直接拒绝通过后再查业务数据。这一步能拦截绝大多数因印刷磨损导致的误读。6. 上线前的压测与验收技巧用一组命令验证防重防错是否真的可靠系统写完不能直接上线二维码这种业务一旦出问题影响的是线下生产环节轻则撕标返工重则影响结算。这章分享一个我每次都会强制走一遍的验收流程并发生成压测、重复核销攻击、误读码模拟三件事做完心里才有底。6.1 并发生成压测验证防重机制是否扛得住真实生产速率用Apache Bench打批量生成的接口确认在高并发下不会产生重码# -n 总请求数-c 并发数POST提交批次生成请求 ab -n 2000 -c 100 -T application/json -p batch_generate.json \ http://api.example.local/qr/batch/generate压测完成后查一下码表里是否有重复SELECT code, COUNT(*) AS cnt FROM qr_code WHERE batch_no A001 GROUP BY code HAVING cnt 1;只要HAVING cnt 1返回任何一条记录说明防重链路仍然有漏洞需要回头检查Redis发号器有没有正确加锁、唯一索引是否生效。我一般在测试环境用这个组合跑三遍第二遍和第三遍提高并发到200和300确认没有重码才允许打印标签。6.2 重复核销攻击测试模拟同码并发提交这一段验证接口幂等与状态机的可靠性。用脚本模拟两个并发请求核销同一个码# 发起100次核销请求目标都是同一个code ab -n 100 -c 50 -T application/json -p verify_payload.json \ http://api.example.local/qr/verify校验规则是100次请求中最多只有1次返回成功其余全是“不可核销”的异常。如果成功次数大于1说明状态机或幂等锁存在并发缝隙需要按5.3的修复方式检查UPDATE语句与Redis锁的配合。6.3 误读码模拟测试人为破坏校验位验证拦截效果二维码的误读场景虽然随机但可以人为构造一个内容把合法的码末位校验位改掉再走完整核销流程要求必须返回校验失败。这一条验证的是系统在没有数据库参与的情况下能否在接口入口直接把非法码拒绝掉。?php // 模拟一个校验位被破坏的码传入校验接口 $badPayload QRA00100012345; // 最后一位应该是4这里故意写成5 $result verifyCheckDigit($badPayload); if ($result) { echo 校验通过说明算法有问题\n; } else { echo 校验拦截成功\n; }这看起来是微不足道的一条测试但很多系统之所以在复杂环境翻车就是栽在这种基础校验上。从那以后我每次上线带二维码的业务体统之前都强制把这三条命令和这条PHP校验脚本完整走一遍顺手检查一遍状态机里是否漏了某个状态流转这套流程已经帮我拦住过至少三次潜在的重复发码事故。希望这些踩出来的经验帮到你让你在做自己的二维码系统时少熬几个夜。本文还有配套的精品资源点击获取