PHP防伪证书系统源码:一物一码与查询防刷实战
简介这是一套基于PHP与MySQL开发的产品防伪证书在线生成与查询系统源码面向需要为商品搭建防伪验证平台的中小企业、电商卖家及PHP开发者可解决证书批量制作、真伪查询与代理商分级管理等需求。压缩包共1017个文件约133.2MB以gif、jpg、png等图片素材和js、css前端资源为主另有79个php程序文件、9个html页面及少量psd、ttf、mp3等素材结构完整、开箱即用。源码自带90套授权证书模板并附PSD公章模板与证书PSD源文件方便二次设计与替换。系统支持后台管理与等级版代理商独立登录安装向导化按提示上传文件并访问install即可完成部署。目前已有541人学习下载适合希望快速搭建防伪查询站点、研究证书生成逻辑或进行二次开发的读者参考使用。1. 防伪证书系统到底在防什么从一张假标签说起去年帮一个做地方特产的朋友处理过一桩糟心事他的蜂蜜产品被人仿了包装连防伪标签上的查询电话都照抄消费者打过去对面居然有人工客服一本正经地告诉他「您买的是正品」。问题出在哪他的防伪体系只有一个静态二维码扫出来永远显示同一句「正品验证通过」造假者只要把这张图复印一万份就能贴一万个假货。这就是典型的「防伪不防伪」——验证逻辑是死的没有一物一码没有查询记录没有后台可追溯。PHP在线生成查询产品防伪证书系统源码要解决的就是这件事给每一件产品生成唯一编码消费者通过网页输入编码或扫码系统实时回查数据库返回该码的首次查询时间、查询次数和产品信息。如果同一个码在短时间内被不同地区反复查询系统就能标记异常。这套东西适合中小厂商、农产品品牌、区域代理商不需要复杂的区块链一台普通服务器加PHP环境就能跑起来。下面我从建库、生成、查询、防刷到部署把这条链路拆开讲清楚。2. 建库与编码规则防伪码怎么生成才不容易被批量猜中2.1 防伪码的熵值底线与字符集选择很多人第一反应是用自增ID加前缀比如FP000001、FP000002。这种码有两个致命问题一是可枚举造假者写个循环就能把整批码跑出来二是没有校验位输错一位系统无法判断是无效码还是输入错误。我一般会用「随机段 校验位」的结构随机段保证不可预测校验位挡住手误。字符集上纯数字容易被暴力遍历纯字母容易混淆O和0、I和1。常见做法是采用 Crockford Base32 变体去掉 I、L、O、U 四个字母剩下 32 个字符。每 5 个字符一组用连字符分隔方便人工输入。一个 15 位随机段的理论空间是 32^15约等于 3.7×10^22足够任何中小厂商用几十年不碰撞。校验位用模 32 的加权和计算权重取质数序列这样单个字符输错或相邻两位调换都能被检测出来。下面这段 PHP 代码是我常用的生成函数直接可以拿去改?php // 防伪码生成15位随机段 1位校验位Crockford Base32 变体 function generateAntiFakeCode(): string { // 去掉易混淆字符 I L O U $chars 0123456789ABCDEFGHJKMNPQRSTVWXYZ; $len strlen($chars); $raw ; for ($i 0; $i 15; $i) { $raw . $chars[random_int(0, $len - 1)]; } // 计算校验位质数权重加权和模32 $weights [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]; $sum 0; for ($i 0; $i 15; $i) { $sum strpos($chars, $raw[$i]) * $weights[$i]; } $check $chars[$sum % 32]; // 每5位加连字符方便人工输入 $formatted implode(-, str_split($raw, 5)) . - . $check; return $formatted; }逻辑说明random_int是 PHP 7 引入的密码学安全随机数函数比rand和mt_rand更适合防伪场景因为后两者的种子可被推测。权重数组用了前 15 个质数保证每一位的贡献不同相邻位调换时加权和会变化。校验位放在最后查询时先验证校验位再查库能挡掉大量无效请求减轻数据库压力。参数说明随机段长度 15 是我根据「人工输入可接受」和「碰撞概率可忽略」两个约束折中的结果。如果只做扫码不做手输可以缩到 12 位如果面向高价值商品建议加到 18 位。字符集不要随意增删一旦上线已生成的码就固定了中途改字符集会导致老码校验失败。2.2 数据库表结构三张表撑起整条链路防伪系统的数据层不需要复杂设计三张表足够产品表存品类信息码表存每一个防伪码及其状态查询日志表记录每一次查询行为。下面是我在 MySQL 里常用的建表语句-- 产品表一个产品对应一批防伪码 CREATE TABLE product ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(128) NOT NULL COMMENT 产品名称, batch_no VARCHAR(64) NOT NULL COMMENT 生产批次号, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 防伪码表核心表每个码一行 CREATE TABLE anti_fake_code ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, code CHAR(19) NOT NULL COMMENT 带连字符的完整码, product_id INT UNSIGNED NOT NULL, status TINYINT DEFAULT 0 COMMENT 0未激活 1已激活 2已冻结, first_query_at DATETIME DEFAULT NULL COMMENT 首次查询时间, query_count INT UNSIGNED DEFAULT 0 COMMENT 累计查询次数, UNIQUE KEY uk_code (code), KEY idx_product (product_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 查询日志表用于异常分析和防刷 CREATE TABLE query_log ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, code CHAR(19) NOT NULL, ip VARCHAR(45) NOT NULL, user_agent VARCHAR(256) DEFAULT , queried_at DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_code_time (code, queried_at), KEY idx_ip_time (ip, queried_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明anti_fake_code表的code字段建了唯一索引这是防重复生成的最后一道保险——即使生成函数出现极端碰撞插入时也会报错不会产生两个相同的码。status字段留了冻结能力发现某批码泄露时可以批量冻结。query_log表的复合索引idx_code_time支撑「查某个码最近的查询记录」idx_ip_time支撑「查某个IP最近的查询频率」这两个索引是后面防刷逻辑的基础。参数说明code用 CHAR(19) 是因为格式固定为 5-5-5-1 加三个连字符共 19 个字符。如果调整了随机段长度这个字段长度要同步改。ip用 VARCHAR(45) 是为了兼容 IPv6。日志表会随时间膨胀建议按月分区或定期归档保留最近 6 个月即可更早的日志导出到冷存储。3. 在线生成与批量导出从后台到车间的落地路径3.1 生成接口的并发安全与批量插入后台生成防伪码时最怕的是并发请求导致重复。虽然唯一索引能兜底但频繁触发唯一键冲突会影响体验。我一般会在生成前先算好一批码用INSERT IGNORE批量插入然后检查实际插入行数不够就补。下面是一个批量生成的示例?php // 批量生成防伪码$count 为本次生成数量 function batchGenerate(PDO $pdo, int $productId, int $count): array { $batchSize 500; // 每批插入500条避免单条SQL过大 $generated []; $remaining $count; while ($remaining 0) { $currentBatch min($batchSize, $remaining); $values []; $params []; for ($i 0; $i $currentBatch; $i) { $code generateAntiFakeCode(); $values[] (?, ?, 0); $params[] $code; $params[] $productId; $generated[] $code; } $sql INSERT IGNORE INTO anti_fake_code (code, product_id, status) VALUES . implode(,, $values); $stmt $pdo-prepare($sql); $stmt-execute($params); $inserted $stmt-rowCount(); // 实际插入数少于预期说明有碰撞需要补生成 $remaining - $inserted; if ($inserted $currentBatch) { // 把碰撞的码从结果里剔除下次循环补上 $generated array_slice($generated, 0, count($generated) - ($currentBatch - $inserted)); } } return $generated; }逻辑说明INSERT IGNORE在遇到唯一键冲突时会跳过该行而不是报错rowCount()返回实际插入的行数。如果实际插入数小于本批数量说明有碰撞$remaining只减去实际插入数循环会继续补生成。$generated数组在碰撞时做了截断保证返回的码都是真正入库的。参数说明$batchSize设为 500 是经验值太小会导致 SQL 次数多太大可能超过max_allowed_packet限制。如果服务器配置较高可以调到 1000。生成 10 万个码大约需要 200 次插入耗时在几秒到十几秒之间建议放到后台队列执行不要在前端请求里同步等待。3.2 导出为印刷文件CSV 与二维码的配合生成完的码要交给印刷厂通常导出成 CSV每行一个码附带产品名称和批次号。如果要做成二维码标签可以用 PHP 的二维码库如 endroid/qr-code批量生成图片。下面是一个导出 CSV 并同时生成二维码的脚本片段?php // 导出CSV并生成二维码图片 require vendor/autoload.php; use Endroid\QrCode\QrCode; use Endroid\QrCode\Writer\PngWriter; function exportBatch(PDO $pdo, int $productId, string $outputDir): void { $stmt $pdo-prepare(SELECT code FROM anti_fake_code WHERE product_id ? AND status 0); $stmt-execute([$productId]); $codes $stmt-fetchAll(PDO::FETCH_COLUMN); $csvPath $outputDir . /codes.csv; $fp fopen($csvPath, w); fputcsv($fp, [防伪码, 查询网址]); $baseUrl https://your-domain.com/verify.php?code; $writer new PngWriter(); foreach ($codes as $code) { fputcsv($fp, [$code, $baseUrl . urlencode($code)]); // 二维码内容直接放完整查询URL扫码即查 $qr QrCode::create($baseUrl . urlencode($code))-setSize(300)-setMargin(10); $result $writer-write($qr); $result-saveToFile($outputDir . /qr/ . str_replace(-, , $code) . .png); } fclose($fp); }逻辑说明CSV 里同时放了防伪码和完整查询 URL印刷厂可以直接把 URL 生成二维码也可以人工输入码。二维码内容用完整 URL 而不是纯码消费者扫码后直接跳转查询页少一步输入。二维码文件名去掉了连字符避免文件系统兼容问题。参数说明setSize(300)是二维码图片边长300 像素在 3cm×3cm 标签上打印足够清晰。setMargin(10)是静默区印刷时不要裁掉否则扫码识别率会下降。如果标签很小可以把 size 降到 200但不要低于 150。4. 查询接口与防刷策略让假货查询露出马脚4.1 查询主流程校验位、状态、首次查询时间查询接口是整个系统被访问最频繁的入口逻辑要轻但该做的判断一个不能少。下面是我常用的查询处理函数?php function verifyCode(PDO $pdo, string $inputCode, string $ip): array { // 统一转大写并去掉空格容忍用户输入时的格式差异 $code strtoupper(trim($inputCode)); // 先验证校验位不合法直接返回不查库 if (!validateCheckDigit($code)) { return [status invalid, msg 防伪码格式不正确]; } $stmt $pdo-prepare(SELECT id, product_id, status, first_query_at, query_count FROM anti_fake_code WHERE code ?); $stmt-execute([$code]); $row $stmt-fetch(PDO::FETCH_ASSOC); if (!$row) { return [status not_found, msg 该防伪码不存在请核对后重试]; } if ($row[status] 2) { return [status frozen, msg 该防伪码已被冻结请联系厂家]; } // 记录查询日志 $log $pdo-prepare(INSERT INTO query_log (code, ip, queried_at) VALUES (?, ?, NOW())); $log-execute([$code, $ip]); // 更新查询计数和首次查询时间 if ($row[first_query_at] null) { $upd $pdo-prepare(UPDATE anti_fake_code SET first_query_at NOW(), query_count 1 WHERE id ?); $upd-execute([$row[id]]); return [status first, msg 首次查询该产品为正品, product_id $row[product_id]]; } else { $upd $pdo-prepare(UPDATE anti_fake_code SET query_count query_count 1 WHERE id ?); $upd-execute([$row[id]]); $count $row[query_count] 1; return [status repeat, msg 该码已被查询 {$count} 次首次查询时间{$row[first_query_at]}, product_id $row[product_id]]; } }逻辑说明校验位验证放在查库之前能挡掉大量格式错误的请求。查询日志先写入再更新计数保证即使更新失败也有日志可查。首次查询和重复查询返回不同提示首次查询给消费者正品确认重复查询给出首次查询时间让消费者自己判断是否异常。参数说明strtoupper和trim是必要的容错用户手输时可能带空格或小写。如果系统只支持扫码可以去掉这两个操作。query_count在重复查询时递增但要注意如果同一个用户反复刷新页面计数会虚高后面防刷策略里会处理这个问题。4.2 防刷与异常标记三个维度的频率控制防刷的核心思路是限制单位时间内的查询次数同时记录异常行为供后台分析。我一般从三个维度做单IP频率、单码频率、全局频率。下面是一个基于 Redis 的简单实现?php function checkRateLimit(string $ip, string $code): bool { $redis new Redis(); $redis-connect(127.0.0.1, 6379); $now time(); // 维度1单IP每分钟最多30次查询 $ipKey rl:ip:{$ip}; $ipCount $redis-incr($ipKey); if ($ipCount 1) $redis-expire($ipKey, 60); if ($ipCount 30) return false; // 维度2单码每分钟最多5次查询 $codeKey rl:code:{$code}; $codeCount $redis-incr($codeKey); if ($codeCount 1) $redis-expire($codeKey, 60); if ($codeCount 5) return false; // 维度3全局每秒最多200次查询防止突发流量打垮数据库 $globalKey rl:global: . $now; $globalCount $redis-incr($globalKey); if ($globalCount 1) $redis-expire($globalKey, 2); if ($globalCount 200) return false; return true; }逻辑说明三个维度的键都设了过期时间Redis 自动清理不需要手动维护。单IP限制挡的是批量扫描单码限制挡的是针对某个码的暴力查询全局限制挡的是突发流量。三个都通过才放行任何一个超限就拒绝。参数说明单IP 30次/分钟适合普通消费者场景如果是在展会现场多人共用WiFi可以放宽到 60。单码 5次/分钟是因为正常消费者不会一分钟内查同一个码五次以上超过基本可以判定为异常。全局 200次/秒是保守估计具体数值要根据服务器配置压测后调整。提示Redis 不是必须的如果服务器上没有 Redis可以用 MySQL 的查询日志做频率统计但性能会差很多。中小规模用文件缓存也能凑合但并发一高就不行了。5. 避坑与排查上线后最容易翻车的五个地方5.1 现象消费者扫码显示「防伪码不存在」但后台能查到原因二维码内容里的 URL 参数没有做 URL 编码防伪码里的连字符在某些扫码软件里被截断或转义。另外如果二维码内容用了短链接跳转短链接服务不稳定也会导致查询失败。解决二维码内容直接放完整 URL并且对 code 参数做urlencode。查询接口接收参数后先urldecode再处理。不要用第三方短链接自己域名下的重定向最可靠。5.2 现象首次查询时间显示为服务器当前时间但消费者说刚买到就查了原因防伪码在生成时没有激活first_query_at为空消费者第一次查询时才写入时间。如果这批码在仓库放了一年才卖出首次查询时间就是卖出时间这是正常的。但如果消费者查询时显示的时间和他实际查询时间差了几个小时那是服务器时区没设对。解决在 PHP 入口文件里统一设置date_default_timezone_set(Asia/Shanghai)MySQL 也要设time_zone 8:00。两个地方都要改只改一个会出现时间不一致。5.3 现象查询日志表几天就几百万行数据库越来越慢原因每次查询都写日志没有清理机制。日志表的索引虽然能加速查询但写入量大了之后索引维护本身也是负担。解决日志表按月分区或者用定时任务每天凌晨删除 6 个月前的记录。如果用的是云数据库可以开启自动归档。另外查询日志的写入可以改成异步用消息队列缓冲但中小系统没必要直接定时清理就行。5.4 现象同一个码被查询了几百次但都是不同IP防刷没拦住原因单IP限制只能挡同一来源的批量查询如果攻击者用代理池轮换IP单IP限制就失效了。这时候需要看单码频率和查询时间分布。解决单码频率限制要保留同时增加「同一码在短时间内被不同IP查询」的告警。后台可以设一个阈值比如 10 分钟内超过 20 个不同IP查询同一个码就自动冻结该码并通知管理员。这个逻辑不需要实时拦截定时任务扫描即可。5.5 现象批量生成 10 万个码时 PHP 内存溢出原因把生成的码全部放在数组里返回10 万个码的数组占用内存超过默认的 128M 限制。解决生成时分批处理每批 500 个直接写数据库不要全部攒在内存里。导出 CSV 时也用流式写入用fputcsv逐行写不要先拼成一个大字符串。如果确实需要返回大量数据用生成器yield代替数组。6. 从查询到追溯把防伪系统用成渠道管理工具防伪系统上线后最容易被忽略的价值是查询数据本身。每一次查询都带着时间、IP、码的信息这些数据积累起来能看出很多问题。我一般会在后台加一个简单的分析页面按产品、按批次、按时间段统计查询量和首次查询率。如果某个批次的码在短时间内集中被查询而且查询IP集中在某个区域那很可能这批货被窜货了或者码被泄露了。更进一步的做法是给每个经销商分配独立的码段查询时记录码段归属这样窜货查询会直接暴露经销商身份。下面是一个按码段统计的 SQL 示例-- 按产品批次统计查询情况找出异常批次 SELECT p.batch_no, COUNT(DISTINCT c.id) AS total_codes, COUNT(DISTINCT CASE WHEN c.first_query_at IS NOT NULL THEN c.id END) AS queried_codes, ROUND(COUNT(DISTINCT CASE WHEN c.first_query_at IS NOT NULL THEN c.id END) / COUNT(DISTINCT c.id) * 100, 2) AS query_rate, COUNT(l.id) AS total_queries FROM product p JOIN anti_fake_code c ON c.product_id p.id LEFT JOIN query_log l ON l.code c.code GROUP BY p.batch_no ORDER BY query_rate DESC;逻辑说明这个查询按批次统计了总码数、已查询码数、查询率和总查询次数。查询率异常高的批次要么是畅销品要么是码被泄露后被人批量查询。查询率异常低的批次可能是货还没卖出去也可能是渠道压货。结合时间维度看趋势比单看一个数字更有意义。参数说明query_rate是已查询码数除以总码数正常消费品这个值在 10% 到 40% 之间取决于消费者的查询习惯。如果超过 80%基本可以判定异常。total_queries和queried_codes的比值能看出重复查询的程度比值越高说明同一个码被查的次数越多。我自己的习惯是每周看一次这个统计发现异常批次先冻结码段再联系渠道核实。这套系统不复杂但把生成、查询、日志、分析四块串起来就能从一张静态标签变成一个能说话的追溯工具。希望帮到你。本文还有配套的精品资源点击获取