ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PHP防伪码查询系统核心实现:防伪码生成、哈希存储与查询状态机设计

PHP防伪码查询系统核心实现:防伪码生成、哈希存储与查询状态机设计 简介一套基于PHP与MySQL的产品商品防伪码查询系统源码适合需要为品牌商品搭建防伪验证功能的开发者、电商运营者或中小企业技术团队使用。系统支持防伪码自动生成、批量导入导出、查询记录追踪等功能能够有效支撑正品验证与渠道管理场景。压缩包共68个文件以PHP主程序为核心辅以CSS、JS前端资源及图片素材整体体积仅3.16MB轻量易部署。程序附带在线安装脚本便于快速配置运行环境。目前已有1898人学习或下载说明该源码具备一定的实用性和参考价值。通过研究这套源码读者可以掌握防伪码生成算法、文件读写交互、管理员后台管理等关键实现思路也可直接在其基础上二次开发适配自身业务需求。1. 防伪码查询系统先搞懂它解决了什么一套防伪码查询系统能不能真正拦住仿冒九成不在服务器配置而在防伪码本身的设计和查询状态机的严谨程度。很多做品牌防伪的项目翻车不是查询接口崩了而是防伪码用顺序号生成被批量穷举出来或者同一个码可以无限次查询成功让假货商钻了空子。这套 PHP 产品商品防伪码查询系统源码核心就是围绕“码的不可预测性”和“查询结果的确定性”两件事来做的后台批量生成随机防伪码消费者输入或扫码查询系统根据码的状态返回正品、重复查询或无效三种结果。它适合中小品牌方直接部署给自己的商品做验证也适合刚接触 PHP 业务开发的从业者拿来拆一套完整的“数据表设计 码生成算法 查询接口”闭环。2. 系统模块与数据表设计防伪码不是“存个字符串”那么简单2.1 系统模块与请求链路这套系统拆开看是三个模块码管理、查询接口、后台统计。码管理负责防伪码的批量生成、批次管理、作废与导出查询接口是消费者实际访问的那一端接收码值、校验格式、查库、更新状态、返回结果后台统计则把每个码的查询次数、首次查询时间、查询 IP 汇总成报表用来判断异常查询行为。请求链路是标准的 PHP MySQL 流程用户提交防伪码 → 服务端做格式校验和长度校验 → 按哈希值查防伪码表 → 判断码状态未查询 / 已查询 / 作废→ 更新查询记录和状态字段 → 返回 JSON。这里最容易做错的一点是直接把用户输入的码原文拿去查库。正确做法是先对码做 SHA256 哈希再拿哈希值查库这样即使数据库泄露攻击者拿到的也只是一堆不可逆的哈希串。2.2 数据表设计防伪码表与查询记录表防伪码表是整个系统的地基字段不能只留一个“码值”加“状态”。我一般会至少规划这些字段字段名类型说明idINT UNSIGNED AUTO_INCREMENT主键code_hashCHAR(64)防伪码去重取哈希唯一索引code_shortVARCHAR(24)脱敏展示用的后四位码值batch_noVARCHAR(32)批次号按生产批次入库product_idINT UNSIGNED关联商品 IDstatusTINYINT0 未查询、1 已查询、2 作废query_timesINT UNSIGNED累计查询次数first_query_timeDATETIME首次查询时间默认 NULLcreated_atDATETIME入库时间CREATE TABLE tc_security_code ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, code_hash CHAR(64) NOT NULL COMMENT 防伪码SHA256, code_short VARCHAR(24) NOT NULL COMMENT 脱敏展示后四位, batch_no VARCHAR(32) NOT NULL DEFAULT , product_id INT UNSIGNED NOT NULL DEFAULT 0, status TINYINT NOT NULL DEFAULT 0 COMMENT 0未查询 1已查询 2作废, query_times INT UNSIGNED NOT NULL DEFAULT 0, first_query_time DATETIME DEFAULT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_code_hash (code_hash), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT防伪码表;存储code_hash而不存原文是为了防库泄露但code_short这个字段不能省很多查询结果页需要显示“您查询的防伪码尾号 8842”不可能把完整哈希值甩给用户。首次查询时间单独留空、查到了再补写目的后面查询接口会用到——这是区分“正品首次验证”和“多次复查”的关键依据。查询记录表单独建不跟防伪码表混在一起。它负责把每次查询行为都存下来方便后续做异常分析比如一个码短时间内被查了几十次就要警觉是否被批量扫描。CREATE TABLE tc_query_log ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, code_hash CHAR(64) NOT NULL, ip VARCHAR(45) NOT NULL, user_agent VARCHAR(255) NOT NULL DEFAULT , result_status TINYINT NOT NULL DEFAULT 0, query_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_code_hash (code_hash), KEY idx_query_time (query_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT查询记录表;数据表的核心取舍可以参考几个底层逻辑第一唯一索引建在哈希上而不是原文上既能去重又能防止原始码泄露。第二状态字段用 TINYINT 而不用 VARCHAR后续做状态统计时SUM、GROUP BY的效率差异在百万级数据下能明显感知。第三查询记录表不加外键约束原因就是查询接口是高频写路径外键校验在这种场景下只会拖慢写入速度业务上的完整性由查询接口自己保证。3. 防伪码生成随机性、批量导入与不可逆哈希3.1 生成算法选型防伪码生成是整个系统的命门可以说码生成得不好后面所有查询逻辑都白搭。最忌讳的就是用自增 ID、时间戳或固定前缀加流水号因为防伪码一旦存在可预测的规律假货商就可以枚举出全量有效码系统直接失效。行业里常见做法是用 CSPRNG 生成随机字符串再用 SHA256 做哈希入库随机字符串本身作为“明文码”印刷到商品包装或标签上。为什么选 SHA256 而不是 MD5MD5 的碰撞攻击风险在防伪码场景下不是主要问题主要问题是 MD5 的运算速度太快如果数据库泄露攻击者可以用字典表对哈希串做高速反查。SHA256 计算开销更高配合加盐还能进一步增加破解成本。另一个细节是入库后要把盐和哈希一起存但盐的生成也要走random_bytes()不能是写死的固定字符串。明文码本身的格式我建议控制在 16 到 20 位之间去掉容易混淆的字符。注意产品包装上的码是给消费者肉眼输入的不是给程序读的所以0O1lI这类字符必须剔除否则用户输了半天提示“防伪码不存在”体验直接归零。3.2 批量生成与导出的 PHP 实现?php /** * 批量生成防伪码 * 参数: batchNo 批次号, count 生成数量, codeLen 码长度 */ function generateCodes(string $batchNo, int $count, int $codeLen 18): array { $chars ABCDEFGHJKLMNPQRSTUVWXYZ23456789; // 去掉0O1lI共32字符 $codes []; while (count($codes) $count) { // random_bytes 是 CSPRNG比 mt_rand 更适合生成防伪码 $randomBytes random_bytes($codeLen); $code ; for ($i 0; $i $codeLen; $i) { $idx ord($randomBytes[$i]) % strlen($chars); $code . $chars[$idx]; } $hash hash(sha256, $batchNo . $code); // 用哈希做去重键避免重复码 $codes[$hash] [ code_hash $hash, code_short substr($code, -4), batch_no $batchNo ]; } return $codes; } // 调用示例生成1000个18位防伪码 $batchNo date(Ymd) . -A01; $codeList generateCodes($batchNo, 1000, 18);这里做两件事一是用random_bytes替代mt_rand前者是系统级随机源后者是伪随机数生成器性能差不多但安全性不在一个级别对防伪码来说这是原则问题。二是用哈希值作为数组键去重PHP 数组中键唯一这一步天然过滤掉重复码。批量生成 1000 个码时合理但如果一次性要生成十万级码这个方案会消耗大量内存常见做法是每攒 5000 条就批量写入数据库再用UNIQUE KEY兜底防重。真正上线生成时需要注意几点批次号batchNo建议用业务日期加流水号它不只是业务标识还参与哈希计算相当于给每个码加了一层“批次盐”。code_short取后四位即可不要取前四位用户手输错误时后几位更容易记住不易混淆。生成的码要导出成 CSV 或 Excel 时码值需要做格式保留不能当长数字处理否则 Excel 会把超过 15 位的码截断成科学计数法这种翻车现象在防伪码批量导出场景里极其常见。4. 查询接口实现参数校验、状态流转与返回值设计4.1 查询接口的 PHP 实现查询接口是系统的门面用户能感知到的只有“我输入了码页面告诉我正品还是假货”。但后端要做的事远不止查一下表。完整的处理链路是接收参数 → 清洗输入 → 格式预校验 → 哈希查库 → 按状态分支处理 → 记录查询日志 → 返回统一 JSON。每一步都有专门的坑尤其是并发场景下的状态更新。?php /** * 防伪码查询接口 * 入参: code 防伪码明文 * 出参: JSON { status: 0|1|2, message: ..., data: {} } */ header(Content-Type: application/json; charsetutf-8); $code trim($_POST[code] ?? ); if ($code || strlen($code) 16 || strlen($code) 24) { echo json_encode([status -1, message 防伪码格式不正确请核对后重新输入]); exit; } // 格式预校验只允许字母和数字 if (!preg_match(/^[A-Z0-9]$/, $code)) { echo json_encode([status -1, message 防伪码包含非法字符]); exit; } $pdo new PDO( mysql:host127.0.0.1;dbnamesecurity_code;charsetutf8mb4, root, your_password, [PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION] ); $codeHash hash(sha256, $code); $stmt $pdo-prepare(SELECT id, code_hash, status, query_times, first_query_time FROM tc_security_code WHERE code_hash ?); $stmt-execute([$codeHash]); $row $stmt-fetch(PDO::FETCH_ASSOC); if (!$row) { echo json_encode([status -1, message 未查询到该防伪码请确认是否输入错误]); exit; } // 关键先更新再查用受影响行数判断并发冲突 $updateSql UPDATE tc_security_code SET query_times query_times 1, first_query_time COALESCE(first_query_time, NOW()) WHERE id ? AND status 0; $upStmt $pdo-prepare($updateSql); $upStmt-execute([$row[id]]); if ($upStmt-rowCount() 0) { $message 您查询的是正品本防伪码为首次查询; $resultStatus 1; // 正品且首次查询 } else { // 更新影响行数为0说明状态已经不是未查询了 $message ($row[status] 1) ? 该防伪码已被查询过请确认商品来源 : 该防伪码已作废商品可能为假冒; $resultStatus $row[status] 1 ? 2 : 3; } // 记录查询日志 $logStmt $pdo-prepare(INSERT INTO tc_query_log (code_hash, ip, user_agent, result_status) VALUES (?, ?, ?, ?)); $logStmt-execute([ $codeHash, $_SERVER[REMOTE_ADDR] ?? , substr($_SERVER[HTTP_USER_AGENT] ?? , 0, 255), $resultStatus ]); echo json_encode([ status $resultStatus, message $message, data [ code_short substr($code, -4), query_times $row[query_times] 1 ] ], JSON_UNESCAPED_UNICODE);接口里的几个关键动作要理解不然改了参数都不知道会踩什么雷。preg_match这段格式预校验必须在查库之前做否则用户提交一个带 SQL 语义的特殊字符虽然 PDO 预处理能挡住 SQL 注入但格式校验可以提前拦截掉无效请求减少数据库无效查询的压力。UPDATE ... WHERE id ? AND status 0是先到先得的并发控制比“先查状态再 UPDATE”的经典写法安全得多因为两个请求同时进来时后一个的rowCount()会返回 0直接走“已被查询过”分支不会出现一个码被并发查出两次正品。4.2 状态流转与返回值设计防伪码的状态只有三档但查询结果需要细分不然会给用户错误暗示。我一般让接口返回四类结果status1表示正品且首次查询这是品牌方最想让消费者看到的结果status2表示码存在但已被查过提示“请确认商品来源”语气不能直接说假货因为存在正品二手转卖或消费者自己重复查询的情况status3表示码已作废可能是品牌方主动作废的批次status-1则是格式错误或查无此码。状态机的流转规则必须写清楚未查询码在首次被查后立即变成已查询同时记录首次查询时间。之后所有对该码的查询都进入已查询分支只追加查询次数和日志不再修改首次查询时间。这里的边界场景是某个码被消费者查询了第一次后用户间隔一年又查了一次系统仍然要提示“该防伪码已被查询过首次查询时间XXXX”这个首次查询时间就是消费者判断商品是否被提前拆封过的依据。返回值设计上建议所有查询结果统一 JSON 结构前端拿到status字段做分支渲染不要返回纯文本或跳转页面混用。接口额外返回query_times的作用是给前端展示累计查询次数这个数字本身就能起到吓阻假货商的效果——一套正常销售的品牌商品一个码的查询次数长期超过 5 次基本可以判定码被复制了。5. 常见问题排查从“查不到”到“被判假”的六个坑5.1 防伪码查不到或提示不存在现象是后台生成的码明明在库里查询接口却返回“未查询到该防伪码”。这种问题九成出在哈希拼接不一致上。我之前排查一个案例生成代码里hash(sha256, $batchNo . $code)查询接口却只对$code单独哈希结果自然是查不到。解决方式是把生成和查询的哈希逻辑抽成一个公共函数比如buildCodeHash($batchNo, $code)生成和查询都必须调用它从源头杜绝两处逻辑分叉。另一种更隐蔽的情况是把批次号里的空格或全角字符带进去了生成的 SHA256 和查询时不一致这种只能靠打印日志比对哈希值来排查。5.2 批量生成时出现大量重复码现象是生成 1000 个码库里出现几十个重复。原因通常是用了mt_rand生成随机码短码长下碰撞概率突增。mt_rand是伪随机生成器同一个进程内快速连续调用时种子变化范围有限生成的码容易出现周期性重复。解决方式是一律改random_bytes并且在写入数据库前先用SELECT COUNT(*)对哈希结果做一次去重检测。还有一个细节是码长低于 12 位时碰撞概率会显著增加即便用 CSPRNG 也建议至少 16 位起步。5.3 查询接口写法没问题但脚本执行报 vcruntime 错误现象是在 Windows 本地跑 PHP 开发环境时页面直接抛vcruntime140.dll 14.0 is not compatible的 warning接口完全无法访问。原因是 PHP 版本和 VC 运行库版本不匹配比如 PHP 8.x 需要对应版本的 Visual C Redistributable。解决方式是去微软官网下载和 PHP 版本匹配的 VC 运行库装好重启或者直接换用集成环境自带运行库的版本。这个坑在本地开发时不明显部署到 Linux 服务器就消失但要是在 Windows 上做联调这个问题会卡住大半天。5.4 查询接口被刷日志表暴涨现象是某几个码的查询记录一天多出几千条数据库空间占用暴涨。原因在于接口没有做频率限制任何人都可以写脚本循环请求。解决方式是加两层防护第一层在查询接口入口加验证码或滑动验证这个改动前端也要配合第二层对同一 IP 的查询频率做限流比如每分钟超过 30 次直接返回“操作过于频繁”。常见做法是把限流计数放到 Redis 里但如果没有 Redis用 MySQL 查询日志表里的IP 最近一分钟计数也能凑合实现。5.5 查询结果页面中文乱码现象是接口返回的 JSON 里“正品”两个字变成乱码。原因有两个方向一是 PHP 文件本身不是 UTF-8 编码保存二是 MySQL 连接字符集不是 utf8mb4。PHP 侧需要在header(Content-Type: application/json; charsetutf-8)里显式声明数据库侧要用 PDO DSN 里的charsetutf8mb4保证连接层一致。排查时先看接口用curl直接访问是否正常如果 curl 正常、页面异常问题在浏览器端缓存或前端页面的meta charset声明上。5.6 并发查询时同一个码被判定两次“首次正品”现象是用户快速点击查询按钮两次接口返回两条都是“正品且首次查询”。原因是查询接口用“先 SELECT 状态再 UPDATE”的方式并发请求同时读到status0随后各自执行 UPDATE都认为自己抢到了首查权。解决方式是走代码 4.1 里的原子更新写法UPDATE ... WHERE id ? AND status 0这行语句在 InnoDB 下会自动加行锁后到的请求rowCount()为 0从而进入已查询分支。这是排查优先级最高的一个坑因为它直接影响系统公信力。6. 查询链路自测与跨域封装上线前的最后一公里防伪码系统做完并不算完接口能不能扛住并发、日志能不能正确落库、跨域场景下前端能不能拿到数据这三个问题每次改动都要重新验证。我先说一个自己的习惯每次改完查询链路不走浏览器先写一个命令行脚本自测。#!/bin/bash # 自测防伪码查询接口正常码、已查询码、无效码三类各跑一次 CODEXK7F2M9PL4QH8RTS echo --- 正品首查 --- curl -s -X POST http://127.0.0.1/query.php \ -d code$CODE \ -H Content-Type: application/x-www-form-urlencoded echo echo --- 重复查询 --- curl -s -X POST http://127.0.0.1/query.php \ -d code$CODE \ -H Content-Type: application/x-www-form-urlencoded echo echo --- 无效码 --- curl -s -X POST http://127.0.0.1/query.php \ -d codeINVALID123 \ -H Content-Type: application/x-www-form-urlencoded这三条命令覆盖了查询接口最核心的三个分支状态跑完看返回的 JSON 里status字段是否符合预期。自测脚本里每次都要重新生成一个全新的测试码否则第二次跑时“正品首查”那个分支永远不会被覆盖到。跨域问题是项目上线后另一个常踩的坑。防伪码查询接口如果部署在security.example.com而官网是www.example.com浏览器发 Ajax 请求会被同源策略拦下来。常见做法是服务端加 CORS 头header(Access-Control-Allow-Origin: https://www.example.com)注意不能直接用*因为业务接口要带状态结果不限定来源会被别的站点恶意调用白白消耗查询资源。如果需要兼容老版本浏览器的跨域请求可以封装一层 JSONP但 JSONP 只支持 GET意味着防伪码会出现在 URL 查询字符串里容易被代理或日志工具记录所以能上 CORS 就优先 CORS。查询链路全程走完还剩一个每批码上线前的例行检查随机抽样 50 个码做真实的查询测试验证码状态流转是否正确再核对后台统计里的查询次数和日志表原始记录是否对上数。从那以后我每次部署防伪码系统或调整查询逻辑都强制先跑一遍 CLI 自测脚本再在浏览器里过一遍完整流程确认status输出、乱码、跨域三个点全部正常才放行。这套“生成 → 入库 → 查询 → 自测”的链路走顺了防伪码系统基本不会出大问题。希望帮到你。本文还有配套的精品资源点击获取
返回列表