资讯动态

PHP二维码防重防错系统:唯一索引与校验位实战

发布时间:2026/10/23 10:42:40 来源:尧图企业网站定制
简介这是一套基于PHP开发的二维码防重防错系统适用于需要每日生成唯一二维码并进行打印、扫码统计的Web应用场景。系统功能覆盖首页扫码入口、每日二维码生成、打印输出、数量统计以及后台设置等核心模块支持自定义二维码前缀1至8位、默认日打印量、序号位数并可选是否在码中包含年月日序号安装后正确设置基础数据即可启用防重防错能力有效避免重复码与错码。资源以ZIP压缩包形式提供整体大小约5.24MB方便快速下载、部署或二次开发。已有233人浏览学习适合具有一定PHP基础的中级开发者、企业内部系统维护人员以及需要快速落地二维码管理功能的技术团队。通过该源码读者可以系统掌握二维码生成规则设计、重复码校验、打印联动与扫码数据统计的完整实现方法并能直接借鉴其模块划分和配置流程构建出稳定可靠的防重防错业务闭环。1. 二维码防重防错系统先想清楚防什么再谈怎么生成上个月在本地用 PHP 写了一个防重防错的二维码生成系统给一批需要逐张核销的电子券用。市面上的二维码生成器很多但真正在企业场景里跑问题从来不在生成图片而在你怎么保证两张码不会重、别人复制你的码去扫你能不能认出来。这套二维码防重防错系统把生成、落库、扫描验码串成一条闭环核心就三件事生成时用唯一性约束防重扫码时用校验位和状态机防错再把每一次扫描行为落成日志。适合做优惠券、设备绑定、工单流转这类需要给每件东西发数字身份证的 PHP 项目PHP 7.4 以上加 MySQL 就能跑代码量不大但把防重和防错两条线走通了。2. 架构与数据模型批次、码表、日志三层存储设计2.1 系统分层与请求链路整个系统拆成三个部分生成端、存储端、扫码端。生成端是 CLI 脚本在后台执行批量生成任务每生成一张码就把码值落进 MySQL然后再调 qrcode 库输出 PNG 图片。存储端就是数据库负责保存批次信息、码表、扫描日志。扫码端是一个简单的 PHP 接口手机扫出来跳到这个接口接口查库、校验状态、写日志返回 JSON 给前端展示。链路是这样的发起生成任务创建批次记录循环生成唯一码并插入码表绘制 PNG 输出到目录然后用户扫码scan.php 接收码值查码表判断状态写扫描日志返回结果。这条链路里最容易翻车的就是第二步到第四步循环里如果出现重复码数据库层面没有约束系统就会静默地把两张一模一样的图发出去。这里我没有选择用 Web 页面触发生成而是直接用 CLI 脚本原因很简单生成一批二维码可能要几万次循环跑在 Web 进程里容易触发请求超时中途断了很难续跑CLI 脚本能断点续跑也方便写进计划任务。架构上我建议把唯一性兜底放在最底层也就是数据库的唯一索引而不是只依赖 PHP 端的生成算法。生成算法写得再严谨也防不住脚本中途中断后重跑、多线程并发生成这类黑天鹅。数据库约束是最后一道保险两道都上才能叫防重系统。顺序上一定是先落库再生成图片如果先画图后写库万一写库撞了唯一索引这张 PNG 就得作废重画白白浪费 IO。2.2 数据表设计三张表怎么划分职责核心表三张我用 InnoDB字符集统一 utf8mb4。先看建表脚本CREATE TABLE qr_batch ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, batch_no VARCHAR(32) NOT NULL UNIQUE, prefix VARCHAR(16) NOT NULL, total INT UNSIGNED NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE qr_code ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, batch_id INT UNSIGNED NOT NULL, code VARCHAR(64) NOT NULL UNIQUE, status TINYINT NOT NULL DEFAULT 0 COMMENT 0正常 1已绑定 2作废, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX idx_batch (batch_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE qr_scan_log ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, code_id BIGINT UNSIGNED NOT NULL, scan_token VARCHAR(32) NOT NULL, scan_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, ip VARCHAR(45) DEFAULT NULL, user_agent VARCHAR(255) DEFAULT NULL, result TINYINT NOT NULL DEFAULT 0, INDEX idx_code (code_id), INDEX idx_scan_time (scan_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;batch 表管批次。batch_no 是给业务方看的批次号唯一约束保证同一批生成任务不会重复建批次。prefix 是码值前缀方便一批码从外观上就看得出来自哪个批次、什么活动排查问题的时候一眼定位。total 记录这一批应该生成多少张生成完成后再核对它和 qr_code 表里 count 出来的数量是不是一致这是后期防漏检的依据。qr_code 表的重点是 code 字段的 UNIQUE 约束这是整个防重设计的命根子。status 字段承载防错逻辑的其中一环0 表示正常待使用1 表示已经被扫码绑定2 表示作废。业务上如果你核销一张码把它从 0 改成 1那同一张码再被扫接口就会直接拒绝。注意我在 status 上保留的是逻辑状态而不是物理删除删除一条码会让之前发出去的记录全部失联审计的时候说不清这张码是删了还是从未存在过这个坑我在避坑章节会展开说。qr_scan_log 表记录每一次扫码。scan_token 我用 code 加时间戳取 md5目的是给每条日志一个独立的业务标识避免同一张码在极短时间内被扫两次时日志主键冲突。ip 和 user_agent 留作事后审计比如同一个 IP 在一分钟内扫了 50 个不同的码大概率是脚本在遍历你的码库。result 字段记录本次扫码判定结果0 是失败1 是成功后面排查问题直接按这个字段分组统计就行。2.3 唯一标识生成三段式码值设计码值本身怎么生成我的策略是三段拼装可读前缀加业务序号加随机因子。前缀从批次表的 prefix 字段取比如设备绑定场景用 DVC 加年月日业务序号是这一批内从 1 开始的自增序号填充到固定长度随机因子是 4 位随机字母数字用来打破按序号递增的可预测性。举个例子DVC260313-0000123-A7K9。这样设计有三个理由。第一前缀让运营人员光看码就能猜出是哪个批次第二序号部分方便人工核对和补码第三随机因子防止有人顺着序号规律遍历你的码。注意随机因子不承担防重职责防重只靠数据库唯一索引随机因子只是降低被猜中的概率。我也对比过直接用 UUID 或雪花 ID它们唯一性更好但码值太长二维码图案会密集到识别率下降而且运营在电话里念码时UUID 中间带横线容易念错。对业务码来说可读性有时比绝对唯一更重要。生成逻辑放在 cli/gen.php 里循环里拼好码值先尝试插入插入冲突就重新拼。这里我用的是先插入后判断冲突的方式比先 SELECT 再 INSERT 安全得多具体原因在第 5 章防重失效那条里详细讲。数据库层面我在 code 字段上的唯一索引是防重底线生成端循环里每插入一行都要 catch 一下 23000 错误码撞上就重拼随机因子。3. 核心模块拆解生成、防重、校验的完整链路3.1 二维码图片生成phpqrcode 参数与输出图片生成我用 PHP 的 phpqrcode 扩展包Composer 引入后直接调 QRcode 类的静态方法composer require phpqrcode/phpqrcode生成核心代码在 cli/gen.php 里?php // cli/gen.php 批量生成二维码 require __DIR__ . /../vendor/autoload.php; $pdo initPdo(); $batchNo B . date(YmdHis); $prefix DVC . date(ymd); $total 500; // 1. 创建批次记录 $stmt $pdo-prepare(INSERT INTO qr_batch (batch_no, prefix, total) VALUES (?, ?, ?)); $stmt-execute([$batchNo, $prefix, $total]); $batchId (int)$pdo-lastInsertId(); $insertStmt $pdo-prepare(INSERT INTO qr_code (batch_id, code) VALUES (?, ?)); $dir __DIR__ . /../output/ . $batchNo; mkdir($dir, 0777, true); for ($i 0; $i $total; $i) { $uniqueCode $prefix . - . str_pad($i 1, 7, 0, STR_PAD_LEFT) . - . randomKey(4); try { $insertStmt-execute([$batchId, $uniqueCode]); } catch (PDOException $e) { if ($e-getCode() 23000) { // 唯一索引冲突重新拼随机因子 $uniqueCode $prefix . - . str_pad($i 1, 7, 0, STR_PAD_LEFT) . - . randomKey(4); $insertStmt-execute([$batchId, $uniqueCode]); } else { throw $e; } } QRcode::png($uniqueCode, $dir . / . $uniqueCode . .png, QR_ECLEVEL_M, 6); } echo batch . $batchNo . generated . $total . PHP_EOL; function randomKey(int $len): string { $chars ABCDEFGHJKLMNPQRSTUVWXYZ23456789; $key ; $max strlen($chars) - 1; for ($i 0; $i $len; $i) { $key . $chars[random_int(0, $max)]; } return $key; }代码里两个细节值得说明。第一randomKey 用的字符集合去掉了 0/O、1/I 这类容易混淆的字符生成出来是给用户扫码的如果码值里混了 0 和 O客服根本没法电话读码。第二QRcode::png 的第三个参数是纠错级别QR_ECLEVEL_M 表示中等纠错能容忍大约 15% 的码字损坏。对设备标签这种可能被磨损、沾脏的场景我用 QR_ECLEVEL_M 起步如果你的码要印在金属表面、经常被摩擦直接上 QR_ECLEVEL_H容错到 30%代价是图案更密、识别慢一点点但耐用性高一个档。第四个参数是图片尺寸6 表示按模块比例放大 6 倍实际输出大约 250 像素左右适合手机扫码。注意这个参数只管像素大小不管信息密度码里内容越长同样尺寸下单个模块就越小。所以生成前一定要控制二维码内容的长度我一般在码值里存完整的跳转 URL那字符串长度可能到 90 个字符以上如果图片尺寸不够扫不出来是常有的事。3.2 防重实现数据库唯一索引与行锁上面那段代码里防重靠的是 INSERT 撞唯一索引后捕获异常这在单进程生成场景下够用。但要是你开了多进程并行生成或者生成中途脚本被杀掉重启两个进程可能同时拼出一样的码值都去走 INSERT。这时候数据库唯一索引会把第二次插入拒掉这是第一层防重。还有一类场景是扫码绑定时的防重。用户扫一个码系统要先确认这个码存在、状态是 0然后把状态改成 1。这个先查后改的过程如果不在事务里两个用户几乎同时扫同一张码就可能两个都查到了状态 0两个人都绑定成功。解决办法是用 SELECT ... FOR UPDATE 锁行?php // api/bind.php 扫码绑定接口的关键片段 $code $_GET[code] ?? ; if (!$code) { jsonReturn([code 1, msg 缺少码值]); } $pdo-beginTransaction(); try { $stmt $pdo-prepare(SELECT id, status FROM qr_code WHERE code ? FOR UPDATE); $stmt-execute([$code]); $row $stmt-fetch(); if (!$row) { $pdo-rollBack(); jsonReturn([code 2, msg 码不存在]); } if ((int)$row[status] ! 0) { $pdo-rollBack(); jsonReturn([code 3, msg 码已被使用或作废]); } $update $pdo-prepare(UPDATE qr_code SET status 1 WHERE id ?); $update-execute([$row[id]]); // 写扫描日志 $logStmt $pdo-prepare( INSERT INTO qr_scan_log (code_id, scan_token, ip, user_agent, result) VALUES (?, ?, ?, ?, 1) ); $logStmt-execute([ $row[id], md5($code . microtime(true)), $_SERVER[REMOTE_ADDR] ?? , substr($_SERVER[HTTP_USER_AGENT] ?? , 0, 250) ]); $pdo-commit(); jsonReturn([code 0, msg 绑定成功]); } catch (Throwable $e) { if ($pdo-inTransaction()) { $pdo-rollBack(); } jsonReturn([code 500, msg 系统繁忙]); }FOR UPDATE 的作用是把这一行锁住直到事务结束。第一个用户拿到锁改完状态提交事务第二个用户拿到锁是等第一个用户提交之后再查这一行看到的 status 已经是 1直接返回已被使用。这就是防错的第二层状态机约束。注意这里的坑事务里查出来的 $row 在做 UPDATE 之前不会再校验一次因为 FOR UPDATE 已经保证行级别的串行化不需要重复查。真正要小心的是UPDATE 的条件要带 id不能只带 code万一你前面查的是旧数据按 code 更新会把别行的状态连带改掉。还要注意 FOR UPDATE 只对 InnoDB 生效MyISAM 表不支持行锁用 MyISAM 的话这个方案直接失效。3.3 防错实现校验位与扫码状态流转除了状态机防错还要解决码值本身被人改了一个字符的情况。比如有人拿一张真实码 DVC260313-0000123-A7K9把最后一位改成 A7K8生成一张假码去扫。如果系统只按码值查库查不到就直接返回不存在这是能接受的但更友好的做法是加一个校验位让明显非法的码在查库之前就被拦截省一次数据库查询。校验位实现方式很多我常用的是给码值末尾拼一个由码值内容计算出来的字符。比如对码值里每个字符的 ASCII 码加权求和取模 36 得到一个字符?php // lib/checksum.php 校验位工具 function appendChecksum(string $code): string { $charset ABCDEFGHJKLMNPQRSTUVWXYZ23456789; $sum 0; $len strlen($code); for ($i 0; $i $len; $i) { $sum ord($code[$i]) * ($i 1); } $pos $sum % 32; return $code . $charset[$pos]; } function verifyChecksum(string $fullCode): bool { if (strlen($fullCode) 2) { return false; } $body substr($fullCode, 0, -1); return appendChecksum($body) $fullCode; }scan.php 或 bind.php 在接收码值后第一件事先跑 verifyChecksum不通过直接返回码格式非法不进数据库。这个校验位不承担安全防伪职责人家真想伪造可以照着算法算它主要的价值是过滤手滑输入、OCR 识别错一个字符这种低级错误让日志里少一些无效查询。配合校验位扫码判定逻辑的完整顺序是格式校验校验位校验查码表状态校验返回。每一步失败的返回码在日志里都要区分开后面排查哪个环节出问题直接看日志里的 result 字段是几就行。状态码我固定成0 成功、2 码不存在、3 已使用或作废、4 校验位错误、1 参数缺失对外返回统一带 code 和 msg 字段前端就好处理了。4. 运行与部署从本地联调到线上配置4.1 环境准备与项目初始化这套系统的运行环境要求是 PHP 7.4 以上、MySQL 5.7 以上、Composer、GD 扩展。phpqrcode 其实不依赖 GD 也能输出图片但你要在控制台直接看到 PNG建议装 GD 扩展如果不装库默认输出 base64 编码的图片内容也可以配合前端 img 标签展示。环境准备的操作顺序我一般这样走php -v # 确认 PHP 版本在 7.4 以上 php -m | grep gd # 确认 GD 扩展存在 composer --version # 确认 Composer 可用 mysql -u root -p # 确认 MySQL 能连上实际项目里我见过不少环境翻车是 GD 没装导致 png 输出一直报错。GD 在大部分 Linux 发行版的 PHP 包里是默认带的但有些精简镜像会把 gd 从默认扩展里去掉运行时才想起来。装扩展的方式因操作系统而异这里不展开注意一个点装完 GD 后一定要 php -m 确认不要装了就当它存在否则后面跑到 QRcode 输出图片时报Call to undefined function imagecreate你会一头雾水。4.2 配置文件、数据库迁移与 Nginx 接入初始化步骤按顺序来。先把目录结构建好这套系统的代码组织我放在下面qr-anti-duplication/ ├── api/ │ ├── scan.php # 扫码验证接口 │ └── bind.php # 扫码绑定接口事务改状态 ├── cli/ │ └── gen.php # 命令行批量生成 ├── lib/ │ ├── checksum.php # 校验位工具 │ └── db.php # PDO 连接公共类 ├── config/ │ └── config.php # 数据库与路径配置 ├── schema.sql # 建表脚本 ├── output/ # 生成图片输出目录 └── vendor/ # Composer 依赖config.php 里暴露数据库连接参数和输出目录路径我习惯把数据库配置集中放在一个文件里方便根据不同环境切换。示例配置长这样?php // config/config.php 公共配置 return [ db [ host 127.0.0.1, port 3306, dbname qr_system, user root, pass your_password, charset utf8mb4 ], output_dir __DIR__ . /../output, scan_base_url https://your-domain.cn/api/scan.php?code ];配置里的 scan_base_url 是给前端拼跳转地址用的。生成图片时我通常直接把整个 URL 作为二维码内容扫出来就跳接口返回 JSON前端再展示。注意这个 URL 不能太长二维码内容每加 20 个字符图案密度就明显上升扫码成功率会下降。所以接口路径要短域名能短则短参数名也尽量简写别放一长串 query string 进去。数据库迁移直接把 schema.sql 导入mysql -u root -p qr_system schema.sql导入后顺手跑一条校验语句确认三张表都在SHOW TABLES;线上部署我建议 Nginx 加 PHP-FPM。Nginx 站点配置里PHP 文件的 fastcgi_pass 指向 PHP-FPM 的 socket 即可server { listen 80; server_name your-domain.cn; root /var/www/qr-anti-duplication; index index.php; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { fastcgi_pass unix:/run/php/php7.4-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } }PHP-FPM 里有两个配置点要动。一个是 request_terminate_timeout 设成 30 秒防止生成大批量二维码时进程被长时间占用另一个是 pm.max_children 按机器内核算给到 50 以上高并发下会看到大量 SELECT 进来连接数不够会直接 502。4.3 扫码接口联调与回归验证本地起服务我用 PHP 内置服务器不用 Nginx。在项目根目录敲php -S 127.0.0.1:8080然后打开另一个终端生成一批测试码php cli/gen.php脚本跑完output 目录下会出现一个以批次号命名的文件夹里面是几百张 PNG。复制其中一张的码值直接拼一个扫码 URL 测试接口curl http://127.0.0.1:8080/api/scan.php?codeDVC260313-0000123-A7K9正常返回的 JSON 大概是{code:0,msg:扫码成功,data:{batch_id:1}}如果这张码已经 bind 过返回的就是 {code:3,msg:码已被使用或作废}。联调阶段要故意做几件坏事来验证防重防错生效。第一件把同一个码值请求两次第二次必须失败。第二件随便改一个字符再请求校验位要拦住。第三件把码值清空请求接口要返回缺少参数而不是白屏。这几条我都写成一个 shell 脚本放在项目里改到核心逻辑后会自动回归省得每次改完手动点。5. 常见问题与避坑记录重复码、乱码、并发穿透5.1 生成的码内容重复活动刚发出半天就收到用户反馈说两个设备扫出来的码一样。排查下来原因是生成端只靠 uniqid() 拼码值没做任何数据库唯一约束脚本跑重了或者并发时uniqid 在同一进程同一微秒内可能返回同一个值。当时生成脚本是手动跑了两遍第一次跑到一半被 CtrlC 杀掉第二次重新跑两张码就重了。解决方法是给 qr_code.code 加 UNIQUE 索引生成时用 INSERT 撞索引而不是先 SELECT 查重。从那以后我再也不信任何唯一性算法的洋气名字只信数据库约束。要补充的是uniqid 在单机低并发下确实很少重复但一旦铺开到多台机器或者跑批任务叠加这种靠运气的事迟早会碰上。5.2 扫码内容出现乱码用户扫码跳到一个页面标题栏显示一串 %E7%A2%A0 之类的字符页面内容也是看不懂的编码。原因通常是码值里的字符串包含非 ASCII 字符二维码编码格式和手机解码时默认的字符集不一致或者 URL 参数没有 urlencode。做设备编码或者发放码的业务码值里带着中文或特殊符号扫码结果就很容易翻车。解决方法是码值里只放 ASCII 字符URL 里的参数一律 urlencode同时在前端页面 meta 标签里声明 utf-8。我后来把码值格式直接规定为大写字母加数字加横线从源头杜绝非 ASCII 字符进入这个约定也写进了生成函数里任何不走这套规则的码在生成时直接报错。5.3 高并发下先查后插依然出现重复码并发量上去之后数据库唯一索引确实存在但两条并发请求居然都插入成功了。原因是对唯一索引的冲突处理不够严用的方式是先 SELECT 判断不存在再 INSERT两个请求同时 SELECT 都不存在就顺次插入成功唯一索引被第二条请求拦截后程序没捕获异常继续执行等于防重逻辑被绕过。解决方法是把查询和插入放到同一个事务里查的时候用 SELECT ... FOR UPDATE或者干脆不查直接 INSERT撞到 23000 异常再重拼码值。我推荐后者少一次查询逻辑还简单。这个坑的教训是防重逻辑一定要写在写入这一步而不是读取这一步任何先查后写都有竞态窗口。5.4 二维码局部破损后扫不出来这个场景一般出现在设备标签上。金属表面反光、标签边角被蹭掉、机器喷墨不均匀这些导致二维码缺一块手机扫半天没反应。原因是生成时用了默认容错级别 QR_ECLEVEL_L只能容忍大约 7% 的码字损坏。解决方法是把容错级别提高到 QR_ECLEVEL_M 或者 H分别能容忍 15%、30% 的损坏。代价是图案更密所以测试时要用手工遮住码的一角来模拟破损别等上线后业务方拿新标签纸贴上去才发现扫不了。我当时踩这个坑是拿了默认设置跑了三百张码打印出来贴在设备上一测十张里有三张扫不出来立马把容错调到 H重新生成。5.5 扫码日志偶尔少一条查不到完整记录用户扫码成功绑定也成功了但回查日志表里没有那条扫码记录。原因是写日志和改状态不在同一个事务里。绑定接口里先 UPDATE 状态再单独 INSERT 日志如果 INSERT 日志时抛了异常状态已经改了但日志丢了。解决方法是把日志 INSERT 和状态 UPDATE 放在同一个事务里如果两条 SQL 里任何一条失败全部回滚。这也是为什么我前面 bind.php 的代码里要把日志写在事务内。这个坑比较隐蔽因为日志缺失不影响用户操作等到要审计的时候才发现记录对不上那时候数据已经改过了后悔药都没得吃。6. 二次开发批量导入和统计报表的改造思路6.1 批量导入历史码现有系统里已经有一批旧的二维码可能存在于 Excel 表格或者别的系统里想统一纳管那就需要批量导入功能。在 cli 下加一个 import.php读取 CSV 文件逐行校验码值长度和校验位校验通过的插入 qr_code 表批次号统一用一个新批次。插入时同样靠唯一索引兜底重复的码单独写一个 import_error.log。这个功能不用改表结构只加一个入口脚本是我认为这套系统最值得先做的扩展。导入脚本里有一个细节文件读取用 fgetcsv 而不是自己 explode 逗号因为 CSV 字段里可能带引号和换行直接手动拆分很容易把一行数据拆成两行插入的时候报 Data truncated 错。6.2 扫码统计与异常预警扫码日志表的数据日积月累后有价值。可以按天统计扫码次数、独立码数、第一次扫码时间分布用来观察业务活动热度。更进一步加一个定时任务扫描日志里同一个 code 被扫多次、同一个 IP 短时间内扫大量不同码的情况抽出来生成预警清单。二次开发时我会在 qr_scan_log 表上再加一个 idx_token 索引支撑按 scan_token 查重。这里有个改造要点查询统计时要避免全表扫描先按日聚合到一张统计中间表比如 scan_daily 表存日期、code_id、扫码次数每日凌晨跑一次聚合。否则活动结束三个月后回看报表一次 group by 能把数据库卡到报警。聚合脚本挂在计划任务里每天凌晨两点跑不影响主业务。这套系统我改到第三版时把生成、绑定、日志三条链路全部列成一个叫核心回归清单的检查项每次发布前照着清单跑一遍。最开始上线设备绑定功能时我就是没跑并发测试活动日当天两个用户同时绑定同一台设备状态都被置成 1数据修了两天才平账。从那以后我每次改防重防错逻辑都会强制走一遍双请求同一码、伪造码值、破损识别三条验证路径确认接口返回码全部符合预期才算过。防重系统靠的不是某个天才算法而是每一层都老老实实兜住。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑