ARTICLE DETAIL

资讯详情

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

PHP防伪码查询系统:从码生成算法到SHA-256哈希存储与查询链路

PHP防伪码查询系统:从码生成算法到SHA-256哈希存储与查询链路 简介针对产品防伪和商品验证场景这套PHP实现的防伪码查询系统源码面向中小电商企业、品牌厂商及个人开发者可用于搭建自有防伪查询平台。核心功能包括防伪码自动生成、单条添加与指定长度/前缀/组合方式支持xls、txt、csv批量导入及防伪码导出并提供查询次数统计、历史记录关键词、时间、IP与自定义结果页等能力适合快速部署或二次开发。包内共68个文件压缩包仅3.16MB以php程序文件为主配以css/js前端样式、jpg/png/gif辅助图片及csv/xls/txt示例数据目录结构清晰便于上手。另附在线安装脚本和说明文档可依据环境要求直接安装使用。目前已有1898人学习下载对需要低成本启用正品验证与渠道管理的团队尤为实用。1. 防伪码查询系统源码先别急着写查询页面把“验证链路”想清楚消费者扫一个码、填一串数字就能知道手里东西是不是正品——这是“防伪码查询系统”在大多数从业者心里的最终画面。但真正做过的人都会说同一句话查询页一小时能写出来难的是前端那串码怎么生成的后端怎么查才算真“防伪”。如果你的码能被逆向推算出规律能被相同批次查询出重复状态这系统上线就是翻车现场。本文不评框架不扯微服务只讲一套用 PHP 实现的最小可用防伪码查询系统该怎么做码生成算法、表结构、查询链路、导出和售后排查全程给出能直接跑通的关键代码与参数。适合刚接手企业官网或电商后台需要快速产出防伪查询模块的 PHP 工程师。2. 数据库设计与防伪码生成算法决定这套系统值不值得做的基础2.1 防伪码表结构为什么只存哈希、不存明文先讲设计上最重要、也是很多人最容易忽略的一个决定防伪码要不要明文入库。大部分入门教程会让你建一张表把防伪码字符串直接作为唯一键存入查询时WHERE code $input。单机小数据量确实能跑但两个问题很快会冒出来第一防伪码明文一旦落到数据库备份、日志或者var_dump里等于把“黑匣子”里的钥匙也一起公布了后续想保密很难第二后台老需要支持客服“帮我查一下这个码”明文码就必须频繁出现在各种接口和文件的入参里泄露面自然增大。我在实际项目里更倾向于防伪码表里只存 SHA-256 加盐后的哈希查询时先把用户输入规范化再算哈希去匹配。匹配仍然走唯一索引性能不会因为多了一次哈希而垮掉。为了不影响运营端的批次检索表里再冗余batch_id和serial_no这样既能把码存成不可逆摘要又能在后台按批次筛选、作废、看导出进度。CREATE TABLE anti_fake_codes ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, batch_id INT UNSIGNED NOT NULL COMMENT 批次ID, product_id INT UNSIGNED NOT NULL COMMENT 产品ID, code_hash CHAR(64) NOT NULL COMMENT SHA256加盐哈希, serial_no INT UNSIGNED NOT NULL COMMENT 批次内递增序号, status TINYINT NOT NULL DEFAULT 0 COMMENT 0正常 1待激活 2作废, first_scan_at DATETIME DEFAULT NULL COMMENT 首次查询时间, last_scan_at DATETIME DEFAULT NULL COMMENT 最近查询时间, scan_count INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 累计查询次数, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_code_hash (code_hash), KEY idx_batch_serial (batch_id, serial_no), KEY idx_product_status (product_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT防伪码主表;这里有个取舍要说明只存哈希之后查询系统本身是好的但客服后台要给人看码就看不到了。如果运营上硬性要求“客服能根据订单号查码”常见做法是再加一列plain_cipher用 AES 把明文加密存进去应用层解密后才展示密钥单独放到配置中心或环境变量不跟随代码仓库发布。文中后面所有示例会先按只存哈希的最简方案走原因是安全链路更明确也足够支撑印刷厂和消费者两条主流程。查询日志表单独建每次查询都写一条CREATE TABLE anti_fake_query_logs ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, code_hash CHAR(64) NOT NULL COMMENT 查询的码哈希, ip VARCHAR(45) DEFAULT NULL, user_agent VARCHAR(255) DEFAULT NULL, result TINYINT NOT NULL COMMENT 1首次正品 2重复查询 3码无效 4查询超限, request_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_code_hash_time (code_hash, request_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;日志表不存明文只存哈希这样即使日志被拉走也不至于直接把一批码泄出去。业务上要按码审计行为时用code_hash关联主表即可。2.2 20位码的生成算法随机数、自增序号与校验位的权衡防伪码生成是整套系统里最像“玄学”的部分——看起来只要随机实际要考虑可读性、容量、抗逆向和断点续跑。行业里常见的码长是 16 位数字或 20 位字母数字。我做的是 20 位字母数字按每 4 位一组分割中间用连字符显示比如KJ3H-9T2M-4F7Q-8A2N-7XR5。这个长度在印刷二维码时不会太密在电话客服场景也能流利念出来。字符集一定要排除容易看错的字符这是血泪经验I和1、L和O、O和0在打印体里很容易混用户抄错一次就会形成“查不到”客诉。所以我用下面这个去除易混淆字符的字母表ABCDEFGHJKMNPQRSTUVWXYZ23456789然后在生成算法上我没有用常见的“反复随机直到不重复”而是用“批次ID 序号 盐”做种子喂给 SHA-256再映射成码function generate_verify_code(int $batchId, int $serialNo, string $salt): string { $seed sprintf(%d-%d-%s, $batchId, $serialNo, $salt); $hash hash(sha256, $seed); $alphabet ABCDEFGHJKMNPQRSTUVWXYZ23456789; // 去掉 I L O 0 1 $code ; for ($i 0; $i 20; $i) { $pos hexdec(substr($hash, $i * 2, 2)) % strlen($alphabet); $code . $alphabet[$pos]; } return implode(-, str_split($code, 4)); }逻辑说明SHA-256 的输出是 64 位十六进制每取其中 2 位映射一个字符正好产生 20 个字符。因为字符集长度是 32理论上存在极小概率的模偏差但对于防伪码场景这个偏差不影响安全性。种子里的serial_no让每个码都不同salt防止攻击者拿到生成规则后暴力反推整批码。盐一旦泄露就要重新生成所以一般把它放在服务端环境变量里不进代码仓库。这种生成方式最大的优势是确定性。后台生成到一半断电了只要知道批次和已生成的最大序号就能断点续跑不会因为“重跑产生不同码”导致重复票。有读者会问为什么不在码末尾加 2 位校验位对于纯数字码场景校验位可以在本地就剔除“输错一位”的输入能省一次数据库查询。但这里查询接口一定会落库哈希检索本身有唯一索引先做规范化已经挡掉了格式问题再加校验位反而缩短有效码长。如果你面对的是老人机、电话语音输入这种极端场景再考虑加模 10 校验位。2.3 批量生成的两种主流方式对比维度随机数 查重确定性掩码本方案生成速度随冲突率波动码越多重试越多O(n)一次扫描完成冲突处理依赖唯一索引和重试逻辑设计上无冲突抗预测依赖随机源质量理论更随机依赖盐的保密强度批次追溯码与批次之间无固定映射由序号可直接回批次断点续跑不支持重跑码会变支持按最大序号继续适合场景小批量兑换码、优惠券大量商品防伪码这个表是我在实际接需求时给对话双方看的第 1 张图如果对方只做几千张礼品卡用随机 查重就够了如果对方是按箱发货、每箱几十个码一造就是几百万码就选确定性掩码。容量上20 位字母数字的码空间是 32 的 20 次方配合每秒 10 次查询的攻击速度实际能被碰巧扫中的概率非常低不用过度设计。3. 核心查询链路从扫码到结果页的 PHP 实现3.1 查询入参规范大小写、连字符与多余空格的处理用户可不会老老实实按你印的码来输入。扫码枪会带空格手机会自动把首字母大写复印纸上的字母在传输过程中还可能混进 OCR 错误。查询接口的第一道防线是把输入“烫平”。我一般在控制器入口先做一个normalize_code()function normalize_code(string $raw): string { $code strtoupper(trim($raw)); $code str_replace([-, , \t, \n, \r], , $code); if (preg_match(/[^A-Z0-9]/, $code)) { throw new InvalidArgumentException(防伪码包含非法字符); } if (strlen($code) 32) { throw new InvalidArgumentException(防伪码长度非法); } return $code; }参数说明先转大写一致化再去掉连字符和肉眼不可见字符。正则用白名单校验不在A-ZA-Z0-9范围内的直接拦截避免后续把特殊字符送进数据库查询。长度上限设 32 而不是 20是因为以后可能要兼容升级版长码同时防止有人拿超大字符串来打哈希给一个长度上限保护内存和 CPU。完成后调用方得到的是不带分隔符的纯 20 位码后续所有逻辑都基于这个字符串处理。强调一下白名单过滤的意思是只保留A-Z和0-9中文、标点一律视为非法。3.2 查询主流程库查找、状态判断、差异化响应查询链路的核心逻辑我一般用一个返回数组的函数来表达便于被 Web 控制器、命令行工具和接口层复用function lookup_code(string $normalizedCode, string $salt): array { $hash hash(sha256, $normalizedCode . $salt); $row db_query_one( SELECT * FROM anti_fake_codes WHERE code_hash ?, [$hash] ); if (!$row) { log_query($hash, 3, $_SERVER[REMOTE_ADDR]); return [result invalid, msg 该防伪码不存在谨防假冒]; } if ($row[status] 2) { log_query($hash, 3, $_SERVER[REMOTE_ADDR]); return [result invalid, msg 该防伪码已作废]; } $isFirst ($row[scan_count] 0); if ($isFirst) { $affected db_execute( UPDATE anti_fake_codes SET scan_count scan_count 1, first_scan_at NOW(), last_scan_at NOW() WHERE id ? AND scan_count 0, [$row[id]] ); if ($affected 0) { // 并发下被别的请求抢先重查一次生产环境建议改成循环最多3次 return lookup_code($normalizedCode, $salt); } log_query($hash, 1, $_SERVER[REMOTE_ADDR]); return [result genuine, msg 验证通过正品首次查询]; } $intervalSec time() - strtotime($row[last_scan_at]); if ($intervalSec 3) { log_query($hash, 2, $_SERVER[REMOTE_ADDR]); return [result warning, msg 刚刚查询过请勿重复操作]; } db_execute( UPDATE anti_fake_codes SET scan_count scan_count 1, last_scan_at NOW() WHERE id ?, [$row[id]] ); log_query($hash, 2, $_SERVER[REMOTE_ADDR]); $firstTime date(Y-m-d H:i:s, strtotime($row[first_scan_at])); return [ result repeated, first_time $firstTime, msg 该码曾于 . $firstTime . 被查询过请确认购买渠道 ]; }逻辑说明第一步用哈希查主表查不到直接判无效。这里必须把“无效”和“作废”分开商品售后时才能区分“码本来就不存在”和“这个码被厂家收回了”。首次查询更新时采用WHERE id ? AND scan_count 0做条件更新这是一条关键的并发保护。两个请求同时读到scan_count 0时只有一个 UPDATE 成功另一个 affected rows 为 0于是重查一次保证“第一次查询”在并发下只算一次。生产环境里我会把这里的递归改成for循环最多重试 3 次防止极端情况下无限自旋。非首次查询时如果距离上次查询不到 3 秒直接提示“请勿重复操作”防止用户对着二维码疯狂连扫导致日志爆炸。超过 3 秒就正常累计次数并把首次查询时间回给前端。第一次查询和再次查询的响应文案要设计成不同风格。“首次正品”是给消费者看的定心丸“曾于xx被查询过”是给二手买家看的预警两种状态都要忠实记录到日志。代码基于 PHP 7.4 的写法PHP 8 环境下没有兼容问题db_query_one和db_execute是对 PDO 的两个薄封装。3.3 查询次数限制别用 IP 做硬限制防伪码查询系统走到线上后最先被攻击的点一定不是页面而是查询接口被脚本批量遍历。常见做法是给每个码加每日查询上限。但我吃过亏的是一开始把限制写成“同一 IP 每日最多 N 次”结果一个公司办公网几十个员工同时扫码出口 IP 全是同一个正常用户直接被误杀。我现在的策略是“按码限次”IP 只做观察和风控不设死上限$cacheKey fake:limit: . $hash . : . date(Ymd); $todayCount $cache-incr($cacheKey); if ($todayCount 1) { $cache-expire($cacheKey, 86400); } if ($todayCount 10) { log_query($hash, 4, $_SERVER[REMOTE_ADDR]); return [result warning, msg 查询次数超限如有疑问请通过官方客服电话核验]; }参数说明这里用了 Redis 的内存自增目的是不把次数判定打在 MySQL 上。每次查询都要读一次这个状态用缓存把热点码的查询压力隔离开。上限取 10 是经验值一个码通常被经销商验一次、终端消费者验一两次10 次足够覆盖正常链路超过 10 次的码哪怕没有脚本攻击也说明产品流通链路存在异常值得记录。如果同一码在 1 分钟内被连续查了 4 次以上风控侧会给运营人员推送一条预警而不是直接拒绝用户。4. 后台批量生成与导出把码从数据库打印到包装上后台批量生成是运营部门每天要用的功能也是代码最容易“翻车”的地方。常见坑是不管量多大都在 Web 请求里跑完几千码还好几十万码直接超时。4.1 用 CLI 脚本批量生成防伪码我一般会做一个命令行脚本让运维或者运营在服务器上、在低峰时段执行。一次生成的范围控制在 10 万以内避免单事务太长影响在线查询。php bin/generate_codes.php --batch20250101 --count50000 --product7 --salt...脚本内部的核心逻辑是用事务片提交 即时输出 CSV让生成过程既高效又能拿到印刷文件// bin/generate_codes.php节选 $opts getopt(, [batch:, count:, product:, salt:]); $batchId (int)$opts[batch]; $count (int)$opts[count]; $productId (int)$opts[product]; $salt $opts[salt]; if ($count 0 || $count 100000) { throw new RuntimeException(单批数量需在 1~100000 之间); } $csvPath storage_path(/export/verify_codes_{$batchId}.csv); $csv fopen($csvPath, w); fputcsv($csv, [防伪码, 批次, 序号]); $pdo-beginTransaction(); try { $stmt $pdo-prepare( INSERT INTO anti_fake_codes (batch_id, product_id, code_hash, serial_no) VALUES (?, ?, ?, ?) ); for ($i 1; $i $count; $i) { $code generate_verify_code($batchId, $i, $salt); $hash hash(sha256, str_replace(-, , $code) . $salt); $stmt-execute([$batchId, $productId, $hash, $i]); fputcsv($csv, [$code, $batchId, $i]); if ($i % 1000 0) { $pdo-commit(); $pdo-beginTransaction(); } } $pdo-commit(); } catch (Throwable $e) { $pdo-rollBack(); fclose($csv); unlink($csvPath); error_log($e-getMessage()); exit(1); } fclose($csv); echo batch {$batchId} generated {$count} codes, csv: {$csvPath}\n;逻辑说明每生成一个码先按已有的确定性算法得到明文码再把整个码拼接盐算哈希入库。明文码在这时写进 CSV印刷厂拿走的正是这份文件数据库里只有哈希两边内容对上就行。每 1000 条提交一次事务避免几十万数据在一个事务里持有大量行锁导致插入速度越来越慢。if 里 commit 后再beginTransaction()这点别漏漏了后面就变成自动提交模式。生成失败时直接删除不完整的 CSV防止印厂拿到半个文件。批次、数量和盐都作为命令行参数不是写死在代码里不同品牌可以跑不同批次。4.2 导出时防 Excel 翻车的细节只要导出的是数字型防伪码Excel 打开会按数字处理超过 15 位就变成科学计数法常见教训是 13 位数字码被显示成8.23523E17。我的码是 20 位字母数字本身不受影响但很多运营还会顺带导出“产品编号”“流水号”这种纯数字列所以导出逻辑必须强制处理。最稳妥的方案是导出 CSV 时给每个文本字段加一个不可见 tab 前缀或者把字段写成...的形式让 Excel 强制按文本解析。我用的是 fputcsv 加 tab 前缀function export_as_text_csv(array $rows, string $filename): void { header(Content-Type: text/csv; charsetutf-8); header(Content-Disposition: attachment; filename . $filename . ); $out fopen(php://output, w); foreach ($rows as $row) { // \t 前缀让 Excel 强制按文本处理 $line array_map(fn($v) \t . $v, $row); fputcsv($out, $line); } fclose($out); }参数说明加 tab 是 Excel 历史遗留兼容方式CSV 标准不支持但国内多数印刷厂和运营都用 Excel 打开这个做法最省事。如果你用 phpoffice/phpspreadsheet 生成 .xlsx直接调用setCellValueExplicit()并把数据类型设为DataType::TYPE_STRING即可不需要加 tab。导出的文件名要对齐批次方便印厂回执时引用比如verify_codes_20250101.csv。4.3 二维码怎么带防伪码落地防伪码最终会印在包装盒或塑封袋上用户扫码直接进入查询结果页比手输码的体验好太多。二维码内容是一个带?code参数的查询 URL用 phpqrcode 这类单文件库就能搞定require_once phpqrcode.php; $queryUrl https://your.domain/verify?code . urlencode($plainCode); QRcode::png($queryUrl, false, QR_ECLEVEL_M, 6, 2);参数说明第三个参数QR_ECLEVEL_M是容错级别M 级容错约 15%适合印刷品表面有一点污损或油墨扩散还能扫出来。追求最大化容错可以选 H但码点会更密扫码速度会略降。第四个参数6是放大倍数第五个参数2是边框宽度至少留 2 个模块宽否则印刷裁切容易切掉边缘。urlencode($plainCode)里存的应该是带连字符的展示码这样二维码内容短、人眼可读扫码后进入查询页再交给normalize_code()统一清洗。5. 防伪码查询系统最常见的五个坑从上线到售后的血泪排查这些坑是系统能不能撑住运营的关键。运行环境里的坑往往不是“功能没实现”而是“实现方式在生产环境不成立”。坑 1生成脚本越跑越慢最后卡死现象同样的生成脚本前 1 万个码很快到 5 万个时每秒只生成几个CPU 没满但进程像卡住。看了SHOW PROCESSLIST大部分连接都卡在 INSERT 前的 SELECT 上。原因典型原因是每条插入前都做一次唯一性预查询。代码先SELECT判断码是否存在再INSERT随着码表增大每次 SELECT 走唯一索引的耗时也在增加事务又不及时提交行锁越积越多最终把整个脚本拖死。解决改成确定性码方案去掉 SELECT 预查唯一索引做兜底万一真碰撞INSERT 会直接报错而不是靠反复探测。事务按 1000 条一批提交从根上消掉了重复查询和长事务。排查时打开 MySQL 的slow_query_log能看到大量 0.1 秒以内的 SELECT 总量占比超过 70%这种“量多但单条不慢”的问题最容易被忽略。坑 2IP 限制打死正常用户线上客诉爆发现象上线当天某个经销商反馈“扫码显示查询次数超限再扫也说超限”但码本身是正品用户到店里吵了一整天。原因初始版本里给“同一 IP 每日查询次数”设了 20 次硬上限。经销商店里一天来几十个顾客扫码全部走同一个宽带出口 IP很快触顶后续用户全部被挡。解决把限制改成“同一码每日最多 10 次”IP 限制从“拦截”降级为“观察名单”。日志照常写但 24 小时内同一 IP 查了 50 个以上不同码才人工核验。如果线上已经误杀补救方式是先把 IP 限制直接调成 0 即不限次日再观察日志重建名单不要在大白天改配置重启会再引发一波“刚才还能扫现在不能扫”的混乱。坑 3码里的 I 和 1 让用户输了一次又一次现象质量客诉里出现“输入码提示不存在”客服核对后发现用户把I输成了1而且这个码印刷在深色包装上衬线字体下I和1几乎一样。原因码字符集没有做易混字符过滤印刷体又放大了视觉误差。用户抄错一次就开始骂街这种客诉处理成本比技术成本高得多。解决生成算法全部换成ABCDEFGHJKMNPQRSTUVWXYZ23456789这个白名单同时在查询接口做一次兜底转换把用户输入的0转成O、1转成I避免老码里历史遗留的易混字符继续引发问题。这条是老规矩但真踩到才长记性。坑 4Excel 打开导出文件码尾数全变成 0现象印厂收到verify_codes_xxx.csv反馈“码全乱了第 6 位之后全是 0”。原因当时有一批老码是纯 16 位数字Excel 把超过 15 位的数字列当成浮点数低位强制补 0表格形态上是“数字”一保存就永久丢精度。即使后来换了字母数字码只要导出列里混入纯数字的产品编号一样会触发。解决导出时给每个单元格加\t前缀强制文本或者直接用 phpspreadsheet 生成 .xlsx 并把列类型设为字符串。验收时必须拿“数字型码”实测一遍 Excel 打开、另存、再打开确认精度无损才算过。印厂拿到的文件里不允许出现任何“数值型”的码列。坑 5并发抢首次查询正品结果被覆盖现象大促时一个码被两个请求几乎同时查询一个拿了“正品首次”另一个也拿了“正品首次”而后台first_scan_at时间不一致。原因查询逻辑是“先 SELECT 读 scan_count再 UPDATE 写”两个请求同时读到 0都走了首次分支后写的覆盖了前面的时间戳。解决UPDATE 语句必须加AND scan_count 0这个条件再检查受影响行数。等于更新失败就重查用数据库自身的行锁来决定谁是真正的第一次。生产环境建议用循环重试最多 3 次替代直接递归。这个模式在其他需要“只允许一人完成”的业务里也通用别依赖应用层互斥。6. 用日志回放验证整套链路把查询结果变成可信凭证查询系统上线后最该做的一件“没被点名但必须做”的事是定期回放查询日志验证没有人在暴力试码同时把“首次查询时间”变成整个防伪体系的可信凭证。我每天会用一条 SQL 找出异常 IPSELECT ip, COUNT(*) AS total_queries, COUNT(DISTINCT code_hash) AS uniq_codes, SUM(result 3) AS invalid_queries FROM anti_fake_query_logs WHERE request_at NOW() - INTERVAL 1 HOUR GROUP BY ip HAVING uniq_codes 50 ORDER BY uniq_codes DESC LIMIT 100;逻辑说明uniq_codes高代表该 IP 在一小时内试了超过 50 个不同码这不是正常消费者行为基本可判定为扫库脚本。invalid_queries高则进一步说明它在碰运气查十个码九个不存在。把这条 SQL 放进一个定时任务每天把结果同步到内部群运营和研发都能看到风控情报。更细一层我会把日志回放成“每小时查询量曲线”看有没有反常尖峰。比如一个没有活动、没有大促的日子某小时查询量突然是平日的 30 倍多半是脚本在扫而不是运营投了广告。这时从日志里抽一条“同一码被连续查询超过 5 次”的样本就能定位到是被刷了还是另有缘由。除了风控把“首次查询时间戳”用好能显著提升消费者信任感。第二次查询的结果页不要只写“请确认购买渠道”而是把第一次查询的具体时间展示出来比如“该防伪码已于 2026-01-08 14:23 首次验证请您核对包装是否一致”。这比空泛的警告更有说服力也留住了售后环节的佐证。我给自己定过验收底线无效查码占比低于 0.1%首次查询响应时间 P99 低于 200ms码查询日志至少保留一年。达不到这三条系统就只是个花架子。开发这类系统重要的不是把每个模块做得“好玩”而是保证码不可预测、查询不可抵赖、导出不出错。有这些基础再往上加商城、溯源、渠道管理才有意义。希望帮到你。本文还有配套的精品资源点击获取
返回列表