☰
PHP防伪码查询系统源码实战:从生成规则到防刷库的完整设计
2026/9/29 17:49:22 网站建设 项目流程

简介:这是一套基于PHP与MySQL开发的产品商品防伪码查询系统源码,面向中小型企业、电商卖家及需要为商品建立防伪验证机制的开发者,可用于搭建品牌防伪查询平台,解决防伪码生成、管理与真伪核验等需求。压缩包共68个文件,约3.16MB,包含11个php核心程序文件、7个css与7个js前端资源、13张jpg与11张png图片素材,以及csv、xls、txt等格式的导入模板和字体文件,结构完整可直接部署。系统支持按指定长度、前缀与组合形式自动生成防伪码,也可批量导入xls、txt、csv文件,并支持导出txt文档;防伪码管理保留两个备用字段,可查看单个码的被查询次数,自定义搜索结果内容,同时记录查询关键字、时间与IP地址,后台支持管理员登录及增删改操作,通过install.php即可在线安装。目前已有1898人学习下载,适合需要快速落地防伪查询功能的PHP开发者参考使用。

1. 防伪码查询系统到底在防谁:从一串 16 位码说起

一瓶酒、一盒药、一袋奶粉,包装上那串刮开涂层的数字,背后跑的就是一套 PHP 产品商品防伪码查询系统。它的核心逻辑不复杂:生成唯一码、存进数据库、消费者输入码、系统比对并返回「首次查询 / 已被查询过 N 次 / 不存在」。但真正上线后你会发现,难点从来不是「查得到」,而是「防得住」——防批量猜码、防重复入库、防并发写坏状态、防有人拿真码去仿冒。这套 PHP 防伪码查询系统源码,适合中小品牌自建溯源入口,也适合接进已有的商城或公众号。它解决的是「让每一件商品有一个可验证的身份」,而不是做一个花哨的查询页面。谁适合做:有实体商品、有经销商体系、想低成本拿到消费者扫码数据的团队。谁不适合:纯虚拟商品、没有一物一码印刷能力的场景。

2. 防伪码的生成规则与数据库设计:别让随机数坑了你

2.1 为什么不能用 rand() 直接生成防伪码

很多人第一版源码里写的是rand(100000, 999999),跑一段时间就发现码会撞、会被猜。防伪码的本质是「不可预测 + 不可枚举 + 可校验」。常见做法是:用密码学安全的随机源生成原始字节,再做一次可逆或不可逆的编码。PHP 里推荐random_bytes(),不要用rand()或mt_rand(),后两者是可预测的伪随机。

生成后一般做两件事:一是加校验位,让系统在查库前就能挡掉明显乱输的码;二是做 Base32 或自定义字符集编码,去掉容易混淆的 0/O、1/I。下面是一段可直接用的生成函数:

<?php // 生成一批防伪码:原始随机 + 校验位 + 去混淆字符集 function genCode(int $len = 12): string { // 字符集刻意去掉 0 O 1 I L,降低人工输入错误 $chars = '23456789ABCDEFGHJKMNPQRSTUVWXYZ'; $max = strlen($chars) - 1; $body = ''; for ($i = 0; $i < $len; $i++) { // random_int 是密码学安全随机,别用 rand $body .= $chars[random_int(0, $max)]; } return $body . checkDigit($body); } // 简单校验位:加权和取模,用于快速挡掉乱输 function checkDigit(string $body): string { $chars = '23456789ABCDEFGHJKMNPQRSTUVWXYZ'; $sum = 0; for ($i = 0; $i < strlen($body); $i++) { $sum += (strpos($chars, $body[$i]) + 1) * ($i + 1); } return $chars[$sum % strlen($chars)]; }

逻辑说明:genCode先生成 12 位主体,再拼 1 位校验位,总长 13。checkDigit用位置加权和取模,保证同一个主体永远得到同一个校验位。参数上,$len建议 10~14,太短容易被枚举,太长消费者输不完。字符集去掉易混字符是血泪经验,客服每天接的「码输不进去」有一半是 0 和 O 分不清。

2.2 数据表怎么建才扛得住并发查询

防伪码表的设计直接决定后面查询和统计能不能做。核心字段:码本身、商品批次、状态、首次查询时间、查询次数、首次查询 IP 归属地。状态字段不要用字符串,用 tinyint,0 未查、1 已查、2 已冻结。码字段必须建唯一索引,这是防重复入库的最后一道闸。

CREATE TABLE `anti_fake_code` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, `code` char(13) NOT NULL COMMENT '防伪码含校验位', `batch_id` int unsigned NOT NULL DEFAULT 0 COMMENT '批次', `product_id` int unsigned NOT NULL DEFAULT 0, `status` tinyint NOT NULL DEFAULT 0 COMMENT '0未查1已查2冻结', `first_query_at` datetime DEFAULT NULL, `query_count` int unsigned NOT NULL DEFAULT 0, `first_ip` varchar(45) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_code` (`code`), KEY `idx_batch` (`batch_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

参数说明:code用 char 而不是 varchar,定长查询更快;uk_code唯一索引让重复插入直接报错,比先 select 再 insert 安全得多。query_count单独存,不要每次去 count 日志表,查询接口要快。批次索引是为了后面按批次统计「有多少码被查过」,这是品牌方最关心的数据。

2.3 批量生成与入库:一次十万条别用循环 insert

生成十万条码,如果一条一条 insert,数据库连接和事务开销会让你等到怀疑人生。常见做法是拼批量 insert,每 500~1000 条一批,配合事务。同时要注意:批量生成时先查重,虽然唯一索引能兜底,但报错回滚会拖慢整体。

<?php // 批量入库:分批拼 SQL,单批 500 条 function batchInsert(PDO $pdo, array $codes, int $batchId, int $productId): int { $ok = 0; foreach (array_chunk($codes, 500) as $chunk) { $values = []; $params = []; foreach ($chunk as $i => $code) { $values[] = "(?, ?, ?)"; $params[] = $code; $params[] = $batchId; $params[] = $productId; } $sql = "INSERT IGNORE INTO anti_fake_code (code, batch_id, product_id) VALUES " . implode(',', $values); $stmt = $pdo->prepare($sql); $stmt->execute($params); $ok += $stmt->rowCount(); } return $ok; }

逻辑说明:array_chunk把大数组切块,避免单条 SQL 过长触发max_allowed_packet。用INSERT IGNORE而不是普通 insert,遇到重复码直接跳过不报错,适合批量场景。参数上,单批 500 是经验值,太小浪费往返,太大 SQL 文本过长。返回rowCount能让你知道实际入库多少条,和生成数对不上就说明有重复。

3. 查询接口怎么写:一次请求要挡住三类攻击

3.1 查询主流程与状态更新

查询接口是整个系统被访问最频繁的地方,也是被攻击最多的地方。正常流程:接收码 → 校验格式和校验位 → 查库 → 判断状态 → 更新状态和次数 → 返回结果。这里有个经典坑:并发下两个请求同时查到「未查询」,都去更新,导致查询次数少算。解决办法是用带条件的原子更新。

<?php // 查询防伪码:原子更新,避免并发下状态错乱 function queryCode(PDO $pdo, string $code, string $ip): array { // 先做格式和校验位校验,挡掉明显乱输 if (!preg_match('/^[23456789A-HJ-NP-Z]{13}$/', $code)) { return ['status' => -1, 'msg' => '格式错误']; } $stmt = $pdo->prepare("SELECT id, status, query_count FROM anti_fake_code WHERE code = ?"); $stmt->execute([$code]); $row = $stmt->fetch(PDO::FETCH_ASSOC); if (!$row) { return ['status' => 0, 'msg' => '未找到该防伪码']; } if ((int)$row['status'] === 2) { return ['status' => 2, 'msg' => '该码已被冻结,请联系客服']; } // 原子更新:只有当前状态为 0 时才写 first_query_at $upd = $pdo->prepare( "UPDATE anti_fake_code SET query_count = query_count + 1, status = 1, first_query_at = IFNULL(first_query_at, NOW()), first_ip = IFNULL(first_ip, ?) WHERE id = ?" ); $upd->execute([$ip, $row['id']]); $count = (int)$row['query_count'] + 1; return [ 'status' => 1, 'msg' => $count === 1 ? '首次查询,正品' : "已被查询 {$count} 次", 'count' => $count, ]; }

逻辑说明:先用正则挡掉格式不对的请求,减少数据库压力。IFNULL(first_query_at, NOW())保证首次查询时间只写一次,后续查询不覆盖。query_count + 1在 SQL 层自增,避免读出来加一再写回去的竞态。参数上,$ip字段留 45 位是为了兼容 IPv6。返回的count直接告诉消费者这是第几次查询,比只返回「正品」更有说服力。

3.2 限流与防枚举:别让人拿脚本刷你的库

防伪码系统最怕的就是有人写脚本,用字典或顺序去撞码。撞中了就能拿到真码去仿冒。常见做法有三层:IP 限流、码段异常检测、验证码。IP 限流用 Redis 最简单,每个 IP 每分钟最多查 10 次,超过直接拒绝。

<?php // 基于 Redis 的简单限流:每 IP 每分钟 10 次 function rateLimit(Redis $redis, string $ip, int $limit = 10): bool { $key = 'af:limit:' . md5($ip); $cur = $redis->incr($key); if ($cur === 1) { // 第一次访问设置过期,窗口 60 秒 $redis->expire($key, 60); } return $cur <= $limit; }

逻辑说明:incr是原子操作,天然适合计数。第一次自增后设置 60 秒过期,形成滑动窗口的简化版。参数上,$limit默认 10,太严会误伤正常用户,太松挡不住脚本。除了限流,还要监控「同一 IP 短时间内查询大量不存在的码」,这基本就是枚举行为,可以直接封 IP 段。注意别把正常消费者的多次查询也封了,所以阈值要结合业务调。

3.3 返回结果的设计:给消费者看什么

查询结果页不是给工程师看的,是给拿着手机、刮开涂层、心里犯嘀咕的消费者看的。返回内容要短、要明确、要有下一步。首次查询返回绿色「正品,首次查询」;已查询返回黄色「该码已被查询过 N 次,首次查询时间 X」;不存在返回红色「未找到,请核对」。不要把数据库 id、批次号这些内部信息暴露给前端,那是给攻击者送情报。

提示:首次查询时间精确到分钟即可,精确到秒反而容易被用来做时间侧信道分析。

4. 避坑与排查:上线后最容易翻车的五个地方

4.1 现象:批量生成十万条,入库只有九万八

原因:随机生成存在极小概率碰撞,加上INSERT IGNORE把重复的静默跳过了。解决:生成时用「生成一批 → 查库去重 → 补生成」的循环,直到数量达标。别指望一次生成就刚好,碰撞概率随数量上升,十万条量级必须做去重补偿。

4.2 现象:消费者反馈「第一次查就显示已被查过」

原因:多半是印刷厂或仓库在出货前做了测试查询,把状态改成了已查。解决:给系统加一个「测试模式」,测试查询不写状态;或者出货前统一把该批次状态重置。这个坑非常常见,血泪经验是:上线前一定要和仓库确认有没有人拿真码试过。

4.3 现象:查询接口偶尔超时,数据库 CPU 飙高

原因:code字段没建唯一索引,或者建了但查询用了LIKE导致全表扫描。解决:确认查询走的是等值匹配,EXPLAIN看 type 是不是 const 或 eq_ref。另外,查询日志表如果和码表放同一个库,高频写入会拖慢查询,建议日志异步写或分表。

4.4 现象:同一批码,有的能查有的查不到

原因:批量入库时事务没提交完整,或者分批 insert 中途报错但没回滚。解决:每批入库后校验rowCount,和预期对不上就记录日志并重试。别用自动提交模式跑批量,显式beginTransaction和commit,出错rollBack。

4.5 现象:被刷库导致大量码状态变成已查

原因:没有限流,攻击者用脚本遍历。解决:立刻上 IP 限流 + 验证码,同时把异常 IP 段加入黑名单。事后要把被刷的码状态回滚,但注意:如果这些码已经流入市场,回滚会让真正的消费者查到「首次查询」,反而掩盖了问题。所以回滚要结合批次和实际出货记录判断。

5. 进阶:把查询日志变成渠道窜货预警

防伪码系统跑起来之后,最有价值的不是「查真伪」,而是查询日志里的地理和时间分布。同一个批次的码,如果大量在非授权区域被查询,基本就是窜货。实现思路:查询时记录 IP,离线把 IP 转成城市,按批次 + 城市做聚合,超过阈值就告警。

-- 按批次和城市统计查询分布,找出异常区域 SELECT batch_id, first_ip_city, COUNT(*) AS query_cnt FROM anti_fake_code WHERE first_query_at >= DATE_SUB(NOW(), INTERVAL 7 DAY) GROUP BY batch_id, first_ip_city HAVING query_cnt > 50 ORDER BY query_cnt DESC;

参数说明:first_ip_city需要你提前把 IP 转成城市存进去,可以在查询接口里异步做,也可以离线跑。阈值 50 是示例,实际要按批次总量调,小批次可能 10 就算异常。这个查询跑在从库上,别在主库跑聚合。

验证方法:拿一个已知窜货的批次,看这个查询能不能把它标出来。如果能,就把告警接进企业微信或邮件。我一般会先跑一周观察基线,再定阈值,不然误报会把运营逼疯。

最后一个技巧:防伪码的校验位别用太复杂的算法,消费者输错一位时,系统要能立刻判断「这不是有效码」而不是去查库。校验位的作用就是挡掉 90% 的乱输,让数据库只处理「格式正确」的请求。这个习惯我保持了多年,每次做查询类系统都会先想「怎么在进库前挡掉一半流量」。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询