简介:防伪码作为产品溯源与品牌保护的核心手段,广泛应用于正品验证、渠道管控和窜货追踪场景。一套可靠的防伪查询系统,不仅依赖随机码生成,更需结合加密算法、校验位设计、数据库拆分及接口安全防护。本文从防伪码的技术本质出发,阐述如何通过业务字段加密生成不可枚举的码值,并利用首查判真逻辑识别重复查询、防范重贴行为。同时,系统通过码表与查询日志分离、并发控制、IP限频等手段,保障高并发下的稳定性和安全性。无论是品牌方的防伪需求,还是渠道方的窜货预警,这套基于PHP的工程实践均能提供可落地的解决方案,帮助开发者快速构建从发码、查询、统计到部署的完整闭环。 做防伪码查询系统这件事,我前前后后做过好几个版本,从最早简单的“随机码+查询页”,到后来支持批次管理、首查判真、操作日志、扫码趋势统计的完整方案,算是把这里面的坑基本踩了一遍。这篇文章就把一套能直接上线的 PHP 产品防伪码查询系统的设计思路和核心实现拆开讲清楚。文章不会只贴一段完整源码了事,而是把每个关键模块的取舍、实现逻辑、常见坑点都说明白,你拿到之后照着拼,就能搭出一套自己的系统。
1. 防伪码的需求本质:不是防“伪造”,是防“串货与重贴”
做技术的人容易把防伪系统理解成“生成一堆别人猜不出来的码”,然后用户输入查询,返回真或假。这确实是最基础的样子,但真实业务里,品牌方要的往往不止这些。
我接触过的几个真实需求里,厂家最关心的事情其实是三件:
第一,产品被仿冒。仿冒者直接抄外观、抄包装,但没法在每件货里都带一个有效的防伪码。只要码的生成算法足够安全,仿冒包装就无法通过验证。
第二,渠道串货。这种情况比仿冒更常见。同一个品牌,给经销商的货带有区域编号,结果某些经销商的货跑到别的区域去卖,价格体系被打乱。防伪码里如果藏着批次、经销区域之类的信息,厂家就能通过用户扫码定位这批货是从哪个渠道出去。这个需求是大多数业务方真正关心的点,但文档里基本不写。
第三,码被重贴。回收正品包装,贴上假货或劣质品。防伪码本身是“一次性”的,第一次查询时记录时间和查询者信息,同一个码第二次再被查询时系统要能提示“该码已被查询过”。
所以,一套完整的防伪码查询系统,核心模块是三个:发码引擎、查询与判真服务、管理后台。如果再加一个贴近业务的点,就是首查信息留存。
这里要先说清楚一个容易被忽略的事实:防伪码并不是“绝对不可伪造”的。无论用多强的加密算法,用户看到的码本身是一串字符,别人拿到真实码之后完全可以复制生产。所以防伪系统真正能做的,是让伪造、复制、窜货这些行为变得极度不划算。码的随机空间要足够大,大到批量枚举不可行;查询策略要足够快,能在首次查询时就把这个码“绑定”到具体的查询时间、IP、设备指纹上;后台要能看到码的查询轨迹,一旦某个码或某个区域出现异常次数,能及时预警。
2. 防伪码生成算法:从随机字符串到可解密的业务码
2.1 最朴素的方案:直接随机字符串
最简单的一种生成方式是用随机算法拼出一串字符,比如 16 位大写字母+数字混合,然后存进数据库。这种方案写起来非常快:
function generateRandomCode($length = 16) { $chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; $code = ''; $max = strlen($chars) - 1; for ($i = 0; $i < $length; $i++) { $code .= $chars[random_int(0, $max)]; } return $code; }注意这里剔除了容易混淆的字母:I、O、1、0,避免用户在手工输入时看错。随机数用的是random_int,不是rand或mt_rand,在生成防伪码这种安全敏感场景,rand系列是绝对不能用的。
但纯随机的方案有个致命问题:这张码表本身成了核心资产。码的生成没有业务规律,意味着你没法从码反推信息。一旦数据库泄露,攻击者拿到全部码值,系统就彻底失效。此外,如果需要做“渠道溯源”,纯随机字符串完全无法承载这些信息。
2.2 进阶方案:业务字段 + 加密变换
我更推荐的做法是“明文信息 + 加密规则生成码值”。具体来说,把产品ID、批次号、序号、校验位等业务字段拼成一个字符串,然后对这个字符串做加密或Hash变换,最后转成便于输入的字符集。
下面是一个我自己在项目里用过的思路:
- 定义明文:
产品ID + 批次号 + 序号拼接。 - 在明文中加入固定盐值。
- 用 AES 加密,再转成十六进制字符串。
- 将十六进制字符串转成 32 进制或自定义字符集,分成几组,用短横线连接。
这样做的好处有三个:
- 可反解:厂里一个客服打电话过来,说“我手里有个产品,帮我查一下是哪批货”,你在后台解码就能直接看到批次、产品ID,不需要查数据库。
- 每个码都不重复:因为产品ID和序号全局唯一,加密结果理论上不重复。
- 不可枚举:AES 密钥只有你的服务端知道,攻击者拿到任意多个码样本,也无法推导生成规则,因为没有密钥。
需要注意,AES 加密输出的长度与明文长度有关。如果你对码长有硬性要求(比如很多厂要求 20 位以内),需要控制好明文字段长度。我一般做法是:产品ID用 2 位(如 01~99),批次号用 6 位(如 202501),序号用 5 位(00001~99999),拼起来是 13 位明文,AES 加密后转字符集大概是 20 位左右,加短横线后 25 位,稍微长一点,但对扫码枪和手输都还算友好。
2.3 最终实现:带校验位防止输入错误
无论用上面哪种方式,都建议在码尾加一个校验位。校验位的作用不是防伪,而是让用户在输错一位时立即提示“码格式不正确”,而不是查数据库后返回“无效码”,避免无谓的数据库查询。
校验位算法用简单的模运算就行:
function checkDigit($codeWithoutCheck) { $sum = 0; $len = strlen($codeWithoutCheck); for ($i = 0; $i < $len; $i++) { $sum += ord($codeWithoutCheck[$i]) * ($i + 1); } return strtoupper(dechex($sum % 16)); }把校验位追加在码末尾。查询时先对码做同样的计算,不匹配就直接返回“输入码有误”,连数据库都不用碰。这一步虽然简单,但对系统整体的抗无效请求能力提升很明显。
3. 数据库设计:码表与查询日志的边界划分
防伪系统数据库设计上有一个容易踩的坑:把“码表”和“查询记录”混在一张表里。
如果只做最简单的查询,一张表确实够:码值、产品ID、状态、查询次数。但业务跑起来后,你会发现品牌方最关心的是“每一个码的完整生命周期”——什么时候发行、什么时候被首次查询、查询了几次、分别来自哪个IP、用了什么设备。这些信息如果都堆在码表里,表行数会极其庞大,而且每次写入都会锁行,影响并发。
我的建议是拆两张表:防伪码表和查询日志表。
防伪码表:
CREATE TABLE `security_code` ( `id` int(11) NOT NULL AUTO_INCREMENT, `code` varchar(32) NOT NULL COMMENT '防伪码', `product_id` int(11) NOT NULL COMMENT '产品ID', `batch_no` varchar(16) NOT NULL COMMENT '批次号', `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0未查询 1已首次查询 2已核销', `first_query_time` datetime DEFAULT NULL COMMENT '首次查询时间', `first_query_ip` varchar(46) DEFAULT NULL COMMENT '首次查询IP', `created_at` datetime NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_code` (`code`), KEY `idx_product_batch` (`product_id`, `batch_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;查询日志表:
CREATE TABLE `security_query_log` ( `id` int(11) NOT NULL AUTO_INCREMENT, `code` varchar(32) NOT NULL, `ip` varchar(46) DEFAULT NULL, `user_agent` varchar(255) DEFAULT NULL, `device_fingerprint` varchar(128) DEFAULT NULL COMMENT '设备指纹', `query_time` datetime NOT NULL, `query_result` tinyint(1) NOT NULL COMMENT '1有效 0无效', `remark` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_code_time` (`code`, `query_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;拆表的逻辑很简单:码表的数据相对固定(生成后基本不变),查询日志是增量追加。查询日志表会越来越大,时间久了要做归档或分区。如果你把查询记录存进码表本身,每次查询都做一次 UPDATE,锁竞争会很严重;而拆出来后,码表首次查询那一次 UPDATE 之后基本不再变动,日志表只做 INSERT,写压力分散很多。
这里还涉及一个关键业务设计:“首次查询时间”字段不要挪到日志表里去算,而是单独放在码表中。我第一次做的时候没想清楚,直接在日志表里查MIN(query_time),结果每次展示码状态都要跑子查询,码量大了之后慢得没法看。把首次查询时间冗余在码表里,是一个典型的空间换时间方案,对这种项目完全值得。
4. 查询接口与前端交互:首次查询判真的完整链路
4.1 查询接口的响应码设计
查询接口不建议只返回“success/error”。因为前端要根据不同结果展示不同文案和样式,比如“首次查询为真”“该码已被查询过,请核对”“无效码”“系统繁忙”,而且每种结果的视觉引导也不同。我常用的方式是统一返回code + message + data结构:
{ "code": 1, "message": "查询成功", "data": { "result": "genuine", "first_query_time": "2024-06-01 10:23:45", "query_count": 1, "product_name": "XX品牌经典款保温杯" } }各结果码含义:
| code | result | 含义 |
|---|---|---|
| 1 | genuine | 该码首次查询,验证为正品 |
| 2 | duplicate | 该码已被查询过,显示首次查询时间 |
| 3 | invalid | 该码不存在 |
| 4 | format_error | 码格式错误 |
| 5 | banned | 该码被标记为异常/禁用 |
| 500 | system_error | 系统异常 |
data里带的query_count是一次附加数据,用来告诉用户“这个码一共被查过多少次”,对判断风险有参考价值。
4.2 判真逻辑代码骨架
核心查询逻辑大致如下:
public function queryCode($code, $meta = []) { // 1. 格式校验 + 校验位校验 if (!$this->checkFormat($code)) { return $this->response(4, '码格式错误'); } // 2. 查码表 $row = $this->pdo->prepare('SELECT * FROM security_code WHERE code = ?'); $row->execute([$code]); $info = $row->fetch(PDO::FETCH_ASSOC); if (!$info) { $this->logQuery($code, 0, '码不存在', $meta); return $this->response(3, '无效防伪码'); } if ($info['status'] == 2) { $this->logQuery($code, 0, '码已禁用', $meta); return $this->response(5, '防伪码已被禁用'); } // 3. 首次查询处理 if ($info['status'] == 0) { $this->pdo->prepare('UPDATE security_code SET status = 1, first_query_time = NOW(), first_query_ip = ? WHERE id = ? AND status = 0') ->execute([$meta['ip'], $info['id']]); $this->logQuery($code, 1, '首次查询', $meta); return $this->response(1, '查询成功,正品验证通过'); } // 4. 重复查询处理 $this->logQuery($code, 1, '重复查询', $meta); return $this->response(2, '该码已被查询过', [ 'first_query_time' => $info['first_query_time'], 'query_count' => $this->getQueryCount($code), ]); }这里有几个容易被忽略的细节。
第一,第 3 步的 UPDATE 语句带了AND status = 0条件,这是避免并发下同一码被两个请求同时命中首次查询。虽然 PHP-FPM 单进程下不太容易出现同一个码的真并发,但在 Nginx 多 worker、多机器部署时,这个条件至关重要。UPDATE影响行数为 1 的那个请求才是真正的首次查询。
第二,查询日志写入要尽可能轻量。日志表本来就会越来越大,如果每查询一次还要做复杂的解析,接口耗时会上来。IP、User-Agent、设备指纹这些字段,能取到就存,取不到就空,不要因为这些小字段报错导致整个查询失败。
第三,设备指纹的获取方式不用太复杂。真实项目中我用的是UA + IP + 时间窗口内的行为特征拼接后取 MD5,足够用了。如果要用更严谨的设备识别,可以引入前端 JS 采集 canvas 指纹,但这对移动端微信内打开的兼容性是个挑战,我没有在普通查询场景里启用它。
4.3 前端提示文案的细节
前端页面不需要太花哨,但“首次查询”与“重复查询”的展示文案一定要区分好。
首次查询时,页面显示“您查询的是正品”并展示产品信息,同时提示“这是该防伪码首次被查询,请放心使用”。重复查询时,则显示“该防伪码已于 2024-06-01 10:23:45 被查询过,如果您是首次查询,请警惕假冒产品”。这种提示文案的设计,才是防伪系统给用户真正信任感的来源。
还有一个容易被忽略的场景:同一个码被同一个用户短时间内查了两遍。比如用户第一遍手输错了一位,输了个不存在的码,然后又查了一遍正确的码。这时候正确的码显示“首次查询”没问题。但如果用户第一次查的是正确码,结果页面人眼没看清,又查了一遍,这时候第二次显示“该码已被查询过”,会让用户恐慌。我在系统里对这种情况做了窗口期优化:同一个码在 10 分钟内被同一个 IP 重复查询,数据库 status 依然置为首次查询,但日志照记。这是一个很小但实际体验提升很大的优化点。
5. 管理端的码管理、统计与导出要点
5.1 发码流程:Excel导入是最好的方案
很多厂家的实际情况是:码要发给印刷厂,印在外包装上,然后再把同一个码对应到具体的产品ID和批次上。所以管理端发码不应该是一个一个手动建码,而应当支持批量生成后导出 CSV,由印刷厂直接使用,或者由业务人员录入关联关系。
我这里的实现是两步:
- 后台选择产品、批次、生成数量,系统调用发码引擎生成码并写入码表;
- 支持导出筛选范围内的码值 CSV 文件,这个文件就是给印刷厂或贴标机用的。
批量生成注意控制内存,不要一次在循环里拼太大数据再写。稳妥做法是:
public function batchGenerate($productId, $batchNo, $count) { $batchSize = 1000; $total = 0; $pdo = $this->pdo; $pdo->beginTransaction(); try { $stmt = $pdo->prepare('INSERT INTO security_code (code, product_id, batch_no, created_at) VALUES (?, ?, ?, NOW())'); while ($total < $count) { $code = $this->generateEncodeCode($productId, $batchNo, $total); $stmt->execute([$code, $productId, $batchNo]); $total++; if ($total % $batchSize == 0) { $pdo->commit(); $pdo->beginTransaction(); } } $pdo->commit(); } catch (Exception $e) { $pdo->rollBack(); throw $e; } }注意,我在生成码之前会先检查唯一索引冲突。虽然算法上同一个产品ID+序号理论上不会重复,但在并发或回溯场景下仍可能有重复风险。捕获唯一索引的异常,并跳过该码换一个新的,是正确做法。不要单纯依赖INSERT IGNORE,因为 IGNORE 会吞掉其他错误,排错时很难受。
5.2 统计报表:扫码量、区域分布与异常预警
管理端除了码管理之外,最有价值的就是统计模块。核心几个报表:
- 每日扫码量:折线图,观察活动/促销周期对扫码量的影响。
- 各产品扫码分布:饼图或柱状图,看哪些产品关注度高。
- 重复查询码 Top 列表:经常被查询且查询次数特别多的码,很可能被扫描复制了,需要留意。
- IP 归属地分布:如果厂里所有码第一次查询的 IP 都集中在某些城市,而产品实际销售区域不符,那就是窜货疑点。
我接 IP 归属地用的方案是先按 IP 前三位分组统计,再在配置表里维护一个 IP 段与区域的映射表。对中小系统来说不需要引第三方庞大的 IP 库,每年更新一次映射表就够用了。
统计查询时尽量避免大表的无索引扫描。查询日志表如果超过几百万行,按日统计的 SQL 会很吃力。我一般会在查询日志表上加一个query_date的冗余字段,统计时直接WHERE query_date = ?走索引,而不是用DATE(query_time)做函数计算,后者会导致索引失效。
6. 核心接口安全:防遍历、防刷量与接口参数签名
防伪码查询接口是公开接口,最容易被打的就是“批量遍历”。攻击者会尝试循环请求大量码值,试图碰撞出有效码,或者批量标记某个码为“已被查询”,干扰正常用户的首次查询。
6.1 IP 频率限制
限频是最基本的一层。用 Nginx + Redis 做全局限频最方便,但如果你不想引入 Redis,用 MySQL 建一张ip_limit表也能做。这里我直接用 PHP 内置文件锁配合 Redis 的伪代码说一下思路:
public function rateLimit($ip) { $key = 'rate:' . date('YmdHi') . ':' . $ip; $current = $this->redis->incr($key); if ($current == 1) { $this->redis->expire($key, 60); } return $current > 30; // 同一IP每分钟最多30次查询 }这里的阈值 30 次/分钟对正常用户足够宽松(正常情况下一个人手动查一个码不会超过几十次),又能挡住大部分脚本遍历。真要对付高速遍历,还得在 Nginx 层做连接数限制和限速。
6.2 防码遍历:查询前加图形验证码
如果只限频但不禁脚本,攻击者可以用大量代理 IP 绕过。要更稳的话,在用户查询前要求输入图形验证码是常见方案。不过验证码会降低用户体验,尤其是在微信扫码场景下,用户本来就是用摄像头对准二维码扫,再让他输验证码就很反人性。
折中方案是“按风险动态出验证码”:普通用户直接查询;同一 IP 短时间内查询超过 3 次,则要求输验证码。这样既能保护接口,又不影响大多数用户。
6.3 接口签名
如果你打算把查询接口开放给第三方(比如印刷厂系统、渠道方的小程序),就需要对请求做签名校验。签名算法不需要太复杂,常见做法是:
sign = md5(code + timestamp + secret)服务端校验 timestamp 不能比当前时间差超过 5 分钟,再校验 sign 是否正确。这个方案能防简单的伪造请求,但不能防重放——同一个请求被原样再次发送仍然合法。要防重放,需要在服务端记住最近使用过的nonce(一次随机数),每个 nonce 只允许使用一次。
我在实际项目中推荐的做法是:对外 API 用“timestamp + nonce + sign”;对内页面查询则靠 IP 限频和动态验证码就够了,不需要过度设计。
6.4 防批量判定与响应混淆
另一个比较“阴”的思路是给攻击者的批量查询增加成本。我见过有团队在非首次查询时故意延迟 1 到 2 秒返回,让批量遍历的脚本跑得极其缓慢。这种方案实现成本很低,在重复查询分支里加usleep(rand(500000, 1500000))即可。但我没有在生产环境里长期启用它,因为它会影响真实用户连续查询的体验,所以只把它做成一个开关,在受到批量攻击时人工打开。
7. 上线部署与 PHP 常见坑:Nginx 配置、错误日志与并发
7.1 部署环境选型
这套系统我用过的技术栈组合很多,最后稳定下来的是一套很朴素的组合:Nginx + PHP-FPM + MySQL。如果访问量不大,一台 1 核 2G 的小服务器完全能扛住。
不引 Laravel、ThinkPHP 这类重框架的原因不是它们不好,而是这套系统本身业务不复杂,核心就是码表的增删改查和查询日志。用原生 PHP + PDO 写一套,部署时不需要处理框架的路由缓存、配置缓存等额外问题,维护成本反而更低。如果你团队已经熟悉某个框架,用它也没问题,核心逻辑是通用的。
7.2 Nginx 伪静态配置
如果接口路径设计为https://domain.com/api/query,Nginx 配置里要加一条 rewrite 规则,把请求转发到 PHP 入口文件:
location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; }这里有一个项目上线时很容易忽视的点:防伪码里的短横线要不要在 URL 里传递。如果用户在小程序或 H5 里输入防伪码,页面会先用 JavaScript 把短横线去掉再传给后端,这样 URL 里不会出现-,也就不需要额外处理编码问题。但如果你把防伪码放在 URL 参数里(比如?code=XXXX-XXXX-XXXX),后端收到后建议统一做一次过滤:
$code = strtoupper(preg_replace('/[^A-Z0-9]/', '', $code));这个过滤既去了短横线,也去掉了空格和异常字符,防止用户手输时带了空格或全角字符导致查不到。
7.3 PHP 错误处理与日志
线上环境必须关闭display_errors,否则防伪码查询接口一旦报错,PHP 会把带有路径或表结构的错误信息直接吐给用户。这既是安全问题,也是体验问题。
正确做法是在入口文件统一设置:
ini_set('display_errors', '0'); ini_set('log_errors', '1'); ini_set('error_log', '/var/log/php-fpm/php_errors.log');但光有全局错误日志还不够,业务异常日志要单独记录。举例:如果查询到一个不存在的码,这不算 PHP 错误,但算业务事件;如果一次批量导入时某个码插入失败了,要能定位到具体是哪个码。我在项目里会封装一个writeBizLog($type, $content)方法,把业务日志写到独立文件,按天切割:
public function writeBizLog($type, $content) { $path = '/var/log/security_biz/' . date('Y-m-d') . '.log'; $line = sprintf("[%s] [%s] %s%s", date('Y-m-d H:i:s'), $type, $content, PHP_EOL); file_put_contents($path, $line, FILE_APPEND | LOCK_EX); }这样排查问题时会轻松很多。
7.4 PDO 使用与 SQL 注入防护
查询接口和后台的 SQL 务必全部用 PDO 预处理语句,不要拼接字符串。防伪码系统直接对外暴露查询接口,是最容易被 SQL 注入攻击的对象。
$stmt = $pdo->prepare('SELECT * FROM security_code WHERE code = ?'); $stmt->execute([$code]);PDO 的连接配置里还要设置异常模式:
$pdo = new PDO($dsn, $user, $pass, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, ]);ATTR_EMULATE_PREPARES => false这个配置很关键。它让 PDO 使用 MySQL 真正的预处理能力,而不是在 PHP 端模拟。虽然对防注入来说两种方式都够,但后者在一些特殊字符场景下行为更符合预期。
7.5 接口返回 JSON 与编码问题
后台与前端交互的接口统一返回 JSON,但 PHP 的json_encode在多字节字符上有个经典坑:如果返回的数据里有中文,在不加JSON_UNESCAPED_UNICODE的情况下,中文会被转成\uXXXX形式。浏览器端解析没问题,但直接看接口响应时很难排查问题。
echo json_encode($data, JSON_UNESCAPED_UNICODE);还有更隐蔽的问题:数据表连接字符集与 PDO 连接字符集不一致。如果表是 utf8mb4,但 PDO 新建连接时没有指定 utf8mb4,查询出来的中文可能直接变成乱码。所以 PDO 的 DSN 里要带上:
$dsn = 'mysql:host=127.0.0.1;dbname=security;charset=utf8mb4';这个charset=utf8mb4必须写,不能省。
8. 一次完整的上线前排查流程
最后把我做这类项目上线前必过一遍的排查清单分享出来,每一条都是实际踩过坑之后沉淀下来的。
第一,防伪码生成唯一性自测。生成 10 万条码,导入数据库,执行SELECT code, COUNT(*) FROM security_code GROUP BY code HAVING COUNT(*) > 1,确保没有重复。高级一点的做法是直接检查唯一索引,但导入前的独立检查更能定位生成算法的问题。
第二,校验位正确性自测。随机抽 1000 条码,每条把最后一位改掉再查询,接口应立即返回format_error,而不是打数据库。
第三,并发首查测试。用 Apache ab 或 curl 并发测试同一个码的首查请求,看是否只有一个请求能拿到首次查询成功。具体命令参考:
ab -n 50 -c 10 "http://yourdomain.com/api/query?code=ABCD-1234-EFGH-5678"如果并发 50 个请求里有超过一个返回“首查正品”,说明 UPDATE 的并发控制没做好。
第四,IP 限频测试。循环发 31 个请求,第 31 个应该被限频拦截。返回的 JSON 里 code 应当是一个明确的“请求过于频繁”的提示。
第五,日志完整性测试。查询一次后,检查security_query_log表是否新增了一条记录,字段是否完整,尤其是首次查询时间和 IP。
第六,后台权限自测。如果管理端不做用户登录,那整个系统就等于裸奔。至少要做个简单的账号密码登录 + Session 管理,后台的所有接口都要校验登录态。有条件的建议上两因素认证,不发短信验证码也至少用邮箱验证码。
第七,数据库备份策略。码表是核心资产,必须定时自动备份。日志表可以按天归档后清理,码表不行,一张都不能丢。
9. 扩展与后续优化方向
一套能跑的防伪查询系统做到这个程度已经可以交付了,但如果业务量涨起来,有几个方向值得继续投入。
第一个是让防伪码承载更多信息。目前码里只藏了产品ID、批次和序号。如果产品线更复杂,可以在明文里再加上“规格型号”“生产日期”“经销商编号”等字段。字段变长之后码也会变长,但这个代价在大多数场景下可以接受。
第二个是对接企业微信/小程序推送。用户扫码验真之后,自动推送产品相关信息、保修登记入口、促销活动链接。这本质上是把防伪码从“验真工具”升级成了“品牌触达用户的入口”,商业价值会大不少。
第三个是引入图片识别和扫码枪批量验货。仓库到货时,工作人员用扫码枪快速扫描一批防伪码,系统批量判断状态并输出结果,这比一个个手工输入快得多。接口层面只需要提供一个批量查询接口,单次最多接收比如 50 个码,循环验证后汇总返回。
第四个是查询异常自动预警。可以用定时任务每日统计每个码的查询次数,如果某码当天被查询超过 5 次,自动标记为风险状态并给管理员发邮件或短信通知。实现上就是写个 crontab 跑 PHP 脚本,非常简单。
这几个方向不需要一次性做完,但至少在系统设计时把数据表结构、接口风格留好扩展余地,后面加功能时不会推翻重来。我自己做这类项目的经验是:不要一开始就上高大上的架构,把发码、查询、日志、统计、基础安全这五个动作做扎实了,系统已经有九成可用度了。剩下的锦上添花,在有真实业务需求的时候再加也不迟。
本文还有配套的精品资源,点击获取