资讯动态

PHP轻量脚本实现MySQL批量删表与DDL备份

发布时间:2026/10/27 3:47:13 来源:尧图企业网站定制
简介这是一款用PHP编写的MySQL数据库维护小工具面向需要频繁清理开发和测试库的开发者与数据库管理员核心解决按统一前缀批量删除数据表时逐表操作费时且易误删的问题。压缩包共3个文件以php脚本为主辅以htm说明文档和txt参考信息整体仅3KB轻量易部署可在支持PHP的服务器环境中运行。现有202人学习关注适合作为数据库清理与维护的入门参考。主脚本围绕数据库连接、按前缀检索表名并执行删除展开说明文档则对参数配置、使用流程和安全注意事项做了交代读者可借此理解前缀匹配批量删表的实现思路并将其改造成符合自身项目规范的维护脚本同时可参考其中的错误处理与备份提醒降低误操作风险提升测试环境重置和临时表清理的效率。1. 一个没人敢点确认的删除按钮被这份 PHP 源码拆成了四步日志表按天建、导入任务按批次建几百张tmp_开头的表堆在一个库里DELETE 语句删不掉它们只能一张张 DROP。用客户端右键删除删到第四十张手就开始抖——前缀手滑多打一个字母主表就跟着陪葬。这份 php 源码针对的就是这个具体场景给它库名和一个前缀它先扫描、再列清单、再要你确认、最后分批执行 DROP中间顺手把SHOW CREATE TABLE的建表语句存成一份可回滚的 SQL。它既不是数据库中间件也不是运维平台就是一个入口文件加一个配置数组的轻量脚本任何能跑 PHP 的环境都能塞进去。适合手上有几十上百张同前缀历史表、又不敢直接对information_schema动刀的人。2. 技术选型与文件分工五个 PHP 文件撑起一次可控的批量删表这个项目的技术栈很简单原生 PHP不依赖任何框架和 Composer 包数据库访问走 PDO元数据查询走information_schema。看起来朴素但三个选型都是有理由的。第一用 PDO 而不是 mysqli是因为 PDO 的异常模式能把「表不存在」「锁等待超时」「权限不足」统一成PDOException删表这种高危操作逐表捕获异常比中途崩掉强太多。第二元数据不从SHOW TABLES来而是查information_schema.TABLES因为后者能一次带回表的行数、数据容量和创建时间才能让操作者在确认前判断「这批表里有没有一张其实还在用的大表」。第三不上框架是因为这东西大概率要丢进一台已经跑着老业务的机器里临时执行多一个依赖就多一次装不上。2.1 目录结构五个文件如何分工一份按职责切开的写法大致是这样源码包解压后基本就是这几层文件职责通常要改的地方config.php连接参数、前缀白名单、批大小、备份目录必改数据库账号密码index.phpWeb 入口渲染扫描结果和确认表单一般不改进程逻辑cli.php命令行入口支持--dry-run与--confirm接 CI 时改参数lib/Scanner.php按前缀查information_schema返回待删清单要加过滤条件时改这里lib/Dropper.php分批执行 DROP、逐表异常隔离、写日志要换成软删除时改这里lib/Backup.php导出建表语句生成回滚用的.sql要改成连数据一起备份时改这里backup/落地 DDL 备份和运行日志注意别放在 Web 可访问目录把入口和逻辑分开是这份源码最值得抄的一点。真正跑起来以后你会发现出问题的从来不是「怎么执行 DROP」而是「执行到一半发现前缀选错了想回退」。入口薄、逻辑厚改起来才不会牵连一片。config.php最好放在 Web 根目录之外用require __DIR__ . /../config.php引进来否则哪天配置里的账号密码被扫目录的爬虫捞走麻烦比删错表还大。2.2 连接层PDO 里三个必须打开的属性配置数组通常长这样每一项都有明确用途不是随手写的?php // config.php —— 建议放在 Web 根目录之外 return [ host 127.0.0.1, port 3306, user db_cleaner, // 只给目标库的 DROP 权限别用 root pass change-me, dbname app_log, charset utf8mb4, // 前缀白名单即使前端传了别的值也要在这里出现过才允许执行 allow_prefix [tmp_, log_bak_], batch_size 50, // 每批 DROP 多少张表 sleep_ms 200, // 每批之间的间隔给主从复制留缓冲 backup_dir __DIR__ . /backup, ];连接函数的写法决定了后面的容错能力?php function db(array $cfg): PDO { $dsn sprintf( mysql:host%s;port%d;dbname%s;charset%s, $cfg[host], $cfg[port], $cfg[dbname], $cfg[charset] ); return new PDO($dsn, $cfg[user], $cfg[pass], [ // 1. 出错抛异常而不是返回 false 让下面 continue 静默跳过 PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION, // 2. 关联数组取值列名直接用 TABLE_NAME PDO::ATTR_DEFAULT_FETCH_MODE PDO::FETCH_ASSOC, // 3. 关掉模拟预处理让占位符真正走到服务端 PDO::ATTR_EMULATE_PREPARES false, PDO::ATTR_TIMEOUT 10, // 连接超时避免页面一直转圈 ]); }三个属性的含义值得说清楚ERRMODE_EXCEPTION是逐表try/catch的前提没有它DROP 失败只会返回false你以为删了 50 张实际只删了 12 张FETCH_ASSOC决定后面拼清单时不用记数字下标EMULATE_PREPARES false让参数走真正的服务端预处理顺便避免字符集不一致时参数被本地转义搞出乱码。ATTR_TIMEOUT容易被忽略但删表脚本一旦卡在锁等待上浏览器那边早就超时重发请求了配上这个至少连接阶段不会无限挂着。提示脚本用的账号务必单独建只授予目标库的SELECT, DROP, SHOW VIEW别图省事复用业务账号。后面第 4 章会讲为什么这个细节能救命。2.3 前缀扫描为什么走 information_schema 而不是 SHOW TABLES LIKE很多人第一反应是SHOW TABLES LIKE tmp_%一行命令就出结果为什么还要绕去information_schema因为SHOW TABLES只给你表名不给任何上下文。批量删表最需要的恰恰是上下文这批表一共占多少空间、最近有没有新建的表混进来、哪几张表行数特别大需要单独处理。information_schema.TABLES一次就能带回来?php // lib/Scanner.php function findTablesByPrefix(PDO $pdo, string $db, string $prefix): array { // 前缀里的 _ 和 % 是 LIKE 通配符必须转义否则 tmp_ 会匹配到 tmpX $escaped str_replace([\\, %, _], [\\\\, \%, \_], $prefix); $pattern $escaped . %; $sql SELECT TABLE_NAME, ENGINE, TABLE_ROWS, DATA_LENGTH, CREATE_TIME FROM information_schema.TABLES WHERE TABLE_SCHEMA :db AND TABLE_TYPE :type AND TABLE_NAME LIKE :p ORDER BY CREATE_TIME ASC; $stmt $pdo-prepare($sql); $stmt-execute([ :db $db, :type BASE TABLE, // 排除视图视图要用 DROP VIEW :p $pattern, ]); return $stmt-fetchAll(); }几个参数值得单独拎出来TABLE_SCHEMA :db必须绑死当前库否则在同一个连接上换库执行时会扫到别的库TABLE_TYPE BASE TABLE是把视图排除掉DROP TABLE撞上视图会直接报错混在批量任务里就是一个卡点ORDER BY CREATE_TIME ASC让最老的表排前面先删老的、后删新的中途叫停时留下的都是较新的数据损失最小。DATA_LENGTH拿来求和页面上显示「本次将释放约 4.7 GB」比单纯列 200 个表名更有说服力。3. 核心链路拆解从 information_schema 扫描到分批 DROP 的完整流程整个流程被拆成四步扫描、预览、备份、执行。这四步的顺序不能换尤其备份必须夹在预览和执行之间——因为 MySQL 的 DROP TABLE 是 DDL会隐式提交事务回滚对它无效。这条特性决定了整个项目的设计既然数据库层面没有后悔药那就只能在应用层自己造一份。3.1 扫描前缀转义加白名单双保险扫描函数本身不长但入口处要先过一道白名单这一步是防前端传参被人为篡改?php // index.php 里接收参数的那几行 $cfg require __DIR__ . /../config.php; $prefix $_POST[prefix] ?? ; $db $_POST[db] ?? $cfg[dbname]; // 白名单校验不是配置里登记过的前缀一律拒绝 if (!in_array($prefix, $cfg[allow_prefix], true)) { exit(前缀不在白名单内 . htmlspecialchars($prefix, ENT_QUOTES)); } $pdo db($cfg); $scanner new Scanner($pdo); $tables $scanner-findByPrefix($db, $prefix); if (count($tables) 0) { exit(没有匹配到任何表检查前缀是否写错); }这里的in_array(..., true)第三个参数一定要开严格比较。SQL 里不会出现类型混淆但 PHP 里会——如果白名单里配了0之类的值松散比较会把它和空字符串、false都判成相等。前缀白名单的意义在于就算攻击者或者手滑的同事在表单里提交了空字符串扫描函数也不会退化成「匹配全部表」。空前缀加上LIKE %那是能把整个库清空的操作一定要在进入查询前就拦掉。3.2 预览dry-run 生成待删清单和容量报表预览层做的事是把数据库元数据翻译成人能看懂的一张表?php // lib/Report.php function renderPreview(array $tables): void { $totalRows 0; $totalBytes 0; echo table border1 cellpadding6; echo trth#/thth表名/thth引擎/thth约行数/thth大小/thth创建时间/th/tr; foreach ($tables as $i $t) { $totalRows (int)$t[TABLE_ROWS]; $totalBytes (int)$t[DATA_LENGTH]; printf( trtd%d/tdtd%s/tdtd%s/tdtd%s/tdtd%s/tdtd%s/td/tr, $i 1, htmlspecialchars($t[TABLE_NAME], ENT_QUOTES), htmlspecialchars($t[ENGINE] ?? -, ENT_QUOTES), number_format((int)$t[TABLE_ROWS]), round($t[DATA_LENGTH] / 1048576, 2) . MB, htmlspecialchars((string)$t[CREATE_TIME], ENT_QUOTES) ); } printf( trtd colspan3合计 %d 张表/tdtd%s 行/tdtd%s MB/tdtd/td/tr, count($tables), number_format($totalRows), round($totalBytes / 1048576, 2) ); echo /table; }TABLE_ROWS对 InnoDB 来说是个估算值误差可能到 30% 以上所以页面上写「约行数」而不是「行数」别让人拿这个数去做业务对账。DATA_LENGTH只统计数据和索引占用的页空间不含碎片实际释放的磁盘可能小于这个数——不过它作为量级参考已经够用。全部输出都套了htmlspecialchars因为表名由数据库管理员创建但也不能保证里面不含尖括号。预览页底下通常跟一个确认表单把当前待删清单的哈希写进隐藏域执行时再比对一次。这个细节是为了防止「预览看了 12 张提交时前缀被改成了 200 张」这种前后不一致的情况本质上是给自己加一层廉价的乐观锁。3.3 执行分批 DROP、单表失败隔离、幂等推进执行层是整个项目最容易翻车的地方因为它必须同时处理三种情况单表失败要继续、大表删除要限速、脚本被中断要能接上。?php // lib/Dropper.php function dropInBatches(PDO $pdo, array $tables, array $cfg, string $logFile): array { $ok $fail 0; $chunks array_chunk($tables, (int)$cfg[batch_size]); foreach ($chunks as $batchNo $chunk) { foreach ($chunk as $t) { $name $t[TABLE_NAME]; // 标识符不能用占位符只能做反引号转义 前置白名单校验 $safe . str_replace(, , $name) . ; try { $pdo-exec(DROP TABLE IF EXISTS . $safe); $ok; logLine($logFile, OK\t{$name}); } catch (PDOException $e) { $fail; // 单表失败不影响后续把 SQLSTATE 和报错原样落盘 logLine($logFile, FAIL\t{$name}\t . $e-getCode() . \t . $e-getMessage()); } } $done min(($batchNo 1) * $cfg[batch_size], count($tables)); file_put_contents($cfg[backup_dir] . /progress.json, json_encode([ done $done, ok $ok, fail $fail, ts time(), ])); usleep((int)$cfg[sleep_ms] * 1000); } return [ok $ok, fail $fail]; }代码里三个点值得展开。第一DROP TABLE IF EXISTS避免了「表已被其他任务删掉」导致的报错让脚本天然带点幂等性第二标识符无法用 PDO 占位符绑定这是预处理机制的硬限制所以表名的安全只能靠「先白名单、后反引号转义」两道关千万别把用户输入直接拼进 SQL第三progress.json每批覆盖一次脚本被 CtrlC 或者被超时打断后重跑时读这个文件跳过已完成的部分不用从头再来。sleep_ms那个 200 毫秒看着微不足道但在上百张表的场景里它能明显削平主从延迟的尖峰血泪经验都是这么换来的。3.4 备份把 SHOW CREATE TABLE 落成一份能回滚的 SQLDROP 不能回滚唯一能做的就是提前把表结构存下来。生产环境里删掉的表通常不需要恢复数据但需要恢复一张「空架子」来接上后续的写入所以只备份 DDL 就够用?php // lib/Backup.php function dumpDdl(PDO $pdo, array $tables, string $dir): string { if (!is_dir($dir)) { mkdir($dir, 0700, true); } $file $dir . /ddl_ . date(Ymd_His) . .sql; $fh fopen($file, wb); // 恢复时先关外键检查否则带外键的子表建不出来 fwrite($fh, SET FOREIGN_KEY_CHECKS0;\n); fwrite($fh, SET SQL_MODENO_AUTO_VALUE_ON_ZERO;\n); foreach ($tables as $t) { $name $t[TABLE_NAME]; $safe . str_replace(, , $name) . ; $row $pdo-query(SHOW CREATE TABLE . $safe)-fetch(); if ($row false || !isset($row[Create Table])) { continue; // 视图或其他对象跳过 } fwrite($fh, DROP TABLE IF EXISTS {$safe};\n); fwrite($fh, $row[Create Table] . ;\n\n); } fwrite($fh, SET FOREIGN_KEY_CHECKS1;\n); fclose($fh); return $file; }SHOW CREATE TABLE返回的列名会随着对象类型变化表是Create Table视图是Create View所以这里用isset判断而不是直接取。文件里在每条 CREATE 语句前加一句DROP TABLE IF EXISTS是让这份备份同时具备「恢复」和「重放」两种能力表还在的时候执行它等于重建表没了的时候执行它等于还原。文件权限设成0700、目录设在 Web 根之外因为建表语句里经常带着字段名和索引名泄露出去对做数据的人不算小事。4. 避坑与排查五条删表翻车记录每条都能耗尽一个下午的时间批量删表的坑有个共同特征出错那一刻不报错等你发现时已经没有回退余地。下面五条按「现象 → 原因 → 解决」整理都是这类脚本在真实环境里反复撞到的问题看一遍能省掉很多返工。4.1 现象前缀tmp_把tmpX的表也删了现象很直接扫描结果里出现了tmpArchive、tmpX2023这种根本不该匹配的表删之前没细看就执行了。原因是 SQL 的LIKE里_是单字符通配符tmp_%的实际语义是「tmp 加任意一个字符再加任意后缀」tmpX正好命中。解决办法是转义前缀里的元字符str_replace([\\,%,_], [\\\\,\%,\_], $prefix)MySQL 默认用反斜杠做转义符不用额外写ESCAPE子句。更稳妥的做法是在预览页把「匹配到的表名」全部列出来并要求勾选确认人眼过一遍永远比相信一段正则便宜。4.2 现象同一个前缀跑了两遍第二次报「表不存在」现象是页面刷新或者点两次提交第二次执行时满屏Table doesnt exist的报错日志里 OK 和 FAIL 混在一起分不清。原因是浏览器表单重复提交加脚本自身没有状态记录而 DDL 又无法回滚两次请求就是两批真实操作。解决办法有两层执行前用progress.json记录已处理清单重跑时先读它做差集SQL 上统一用DROP TABLE IF EXISTS让重复执行变成无害操作。表单侧加一个一次性 token同一 token 只能提交一次挡住手快的人连点。4.3 现象删到一半主从延迟飙到几百秒业务查询开始超时现象是脚本跑到 60 多张表时监控里从库延迟曲线直线上冲读业务报超时。原因是一次性连发几十条 DROP每条都要写 binlog 并在从库重放从库单线程回放跟不上越积越多。解决办法是把batch_size压到 2050每批之间usleep200500 毫秒让从库有时间追平表特别大数据容量超过 1 GB时先在从库上确认延迟必要时临时把从库摘出读流量。事后一条简单规则清理任务永远不要在业务高峰跑。4.4 现象用备份文件恢复时报外键约束错误现象是把生成的.sql喂给mysql客户端前面几张表建得好好的突然来一句errno: 150 - Foreign key constraint is incorrectly formed。原因是备份里没关外键检查子表先于父表被重建引用找不到目标。解决办法是在备份文件开头写SET FOREIGN_KEY_CHECKS0;、结尾写回 1这在本项目里由dumpDdl负责。另外要确认恢复时连接的库跟原库同名外键定义里带库名的话跨库恢复还得改 SQL。4.5 现象扫描能出结果执行全部Access denied现象很迷惑预览页正常列出 200 张表一点执行全是权限拒绝。原因是脚本用的账号只有SELECTinformation_schema的查询权限和表上的DROP权限是两回事。解决办法是给独立账号补权限GRANT SELECT, DROP ON app_log.* TO db_cleaner127.0.0.1;然后FLUSH PRIVILEGES。这里刻意不授全库权限就是要让「删错库」这件事在权限层面直接不可能发生这比任何代码校验都硬。4.6 出问题时的三步定位顺序真出事的时候容易慌我一般按固定顺序查先看backup/下最近那份ddl_*.sql确认要恢复的表结构在不在里面再翻同目录的运行日志用grep FAIL找出具体失败的表和 SQLSTATE最后进库跑一遍SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMAapp_log AND TABLE_NAME LIKE tmp\_%确认现在还剩什么。三步走完损失范围就完全清楚了再决定是恢复、重跑还是止损。日志里把 SQLSTATE 和原始错误信息一起记下来这一步别嫌啰嗦排查时它就是唯一的黑匣子。5. 跑通与验证本地造 100 张表把 dry-run 和真删各走一遍拿到源码的第一件事不是往生产库上连而是本地造一批假表把全流程走通。这一章按「环境准备 → 配置 → 浏览器跑 → 命令行跑 → 造表验证」的顺序来全部在测试库上做。5.1 环境准备与配置项的含义要求很基础PHP 7.0 以上打开pdo_mysql扩展MySQL 5.6 以上即可information_schema从 5.x 就有。检查扩展用php -m | grep pdo_mysql没有输出就去php.ini里把extensionpdo_mysql前面的注释去掉。本地用集成环境或者自己起的 MySQL 都行关键是别拿线上库练手。配置上要动的只有config.php那几项逐项说清楚配置项含义常见取值与注意点host/port数据库地址与端口本机127.0.0.1:3306写 IP 别写localhost避免走 socketuser/pass专用清理账号单独建号只授目标库SELECT, DROPdbname目标库改成你的库名别留默认值allow_prefix允许执行的前缀白名单建议带下划线结尾如tmp_减少误匹配batch_size每批删表数量2050表大就往下压sleep_ms批间隔毫秒200 起步从库延迟高就加到 500backup_dirDDL 备份与日志目录放在 Web 根之外权限 07005.2 浏览器端的三步操作Web 入口的流程是刻意的三步少一步都不行第一步填库名和前缀点「扫描」。这一步只查information_schema不执行任何写操作所以最坏的结果就是查了个空。第二步看预览表格。重点看三样匹配到的表总数是不是符合预期、最老和最新的创建时间跨度、合计占用空间。如果总数比预期多出一大截八成是前缀没加下划线或者写得太短。第三步勾选确认框、点「执行删除」。执行过程中页面会持续输出每张表的结果别关浏览器标签脚本虽然写了progress.json但中途断开还是得手动重跑。注意dry-run的意义在于让你在「零风险」状态下看一遍清单。养成习惯——不管多着急先跑预览再删。5.3 命令行跑法接 CI 做定时清理Web 界面适合一次性操作长期定时清理更适合命令行。入口一般这样写?php // cli.php 用法php cli.php --prefixtmp_ --dry-run // php cli.php --prefixtmp_ --confirm $opts getopt(, [prefix:, db::, dry-run, confirm]); $cfg require __DIR__ . /config.php; $prefix $opts[prefix] ?? ; $dryRun isset($opts[dry-run]); $confirm isset($opts[confirm]); if (!in_array($prefix, $cfg[allow_prefix], true)) { fwrite(STDERR, 前缀不在白名单内\n); exit(1); } if (!$dryRun !$confirm) { fwrite(STDERR, 真删必须显式加 --confirm先跑 --dry-run 看一眼\n); exit(2); } $pdo db($cfg); $tables (new Scanner($pdo))-findByPrefix($opts[db] ?? $cfg[dbname], $prefix); if ($dryRun) { foreach ($tables as $t) { printf(%-40s %10d rows %8.2f MB\n, $t[TABLE_NAME], $t[TABLE_ROWS], $t[DATA_LENGTH] / 1048576); } printf(共 %d 张表\n, count($tables)); exit(0); } $file (new Backup($pdo))-dumpDdl($tables, $cfg[backup_dir]); fwrite(STDOUT, DDL 已备份到 {$file}\n); $r dropInBatches($pdo, $tables, $cfg, $cfg[backup_dir] . /run.log); fwrite(STDOUT, 完成成功 {$r[ok]}失败 {$r[fail]}\n);getopt里prefix:后面那个冒号表示必须带值db::两个冒号表示可选值。真删必须显式加--confirm这个设计很关键CI 里写错一行参数的概率远比想象中高多一道显式开关就少一次事故。备份在删之前同步执行而不是异步排队是因为一旦脚本在执行中途挂掉异步备份可能还没落地那批表就真的没了。5.4 造 100 张表把流程跑通验证用不着真数据造一批空表就够# 造 100 张 tmp_ 前缀的测试表外加 3 张不该被匹配的干扰表 for i in $(seq -w 1 100); do mysql -h127.0.0.1 -uroot -p$MYSQL_PWD app_log \ -e CREATE TABLE tmp_import_${i} (id BIGINT PRIMARY KEY AUTO_INCREMENT, payload TEXT); done for n in tmpArchive tmpX2023 keep_tmp_1; do mysql -h127.0.0.1 -uroot -p$MYSQL_PWD app_log \ -e CREATE TABLE ${n} (id INT PRIMARY KEY); done造完先跑php cli.php --prefixtmp_ --dry-run预期结果是 100 张tmpArchive和tmpX2023不能被列进去——如果列进去了说明前缀转义那一步没生效回去检查 3.1 的str_replace。确认清单没问题再跑php cli.php --prefixtmp_ --confirm然后ls backup/看 DDL 文件是不是生成了、里面是不是 100 条CREATE TABLE。最后把备份文件导回一个空库验证恢复链路能走通整个闭环才算跑完。6. 二次开发把「直接 DROP」改造成能后悔的两阶段删除真正上线一段时间后多数人都会想改一件事不希望点下确认的那一刻表就消失了。这时最值得做的改造是两阶段删除——第一次只做重命名把表挪进「回收站」隔几天再统一清理。改法很轻把Dropper里那一句换成两条分支即可?php // 阶段一重命名到回收站前缀随时可以改回来 $trash zz_trash_ . date(Ymd) . _ . $name; $pdo-exec(sprintf(RENAME TABLE %s TO %s, str_replace(, , $name), str_replace(, , $trash))); // 阶段二N 天后由定时任务扫 zz_trash_ 前缀真正 DROP // 只处理创建时间早于 now() - INTERVAL 7 DAY 的回收站表配套要加一张审计表字段至少包含原表名、回收站表名、操作人、执行时间、DDL 备份文件路径。这张表本身就是二次开发的入口想做审批流加一个status字段从pending走到approved才允许执行阶段二想做白名单动态化把allow_prefix从配置数组挪到这张表里按库区分。再进一步把扫描函数里TABLE_NAME LIKE换成REGEXP就能支持「删掉log_开头且创建时间早于 90 天」这类组合条件适用面立刻宽一圈。验证改造是否成功别只看表有没有改名要跑三个检查RENAME TABLE之后原表名在information_schema里确实消失、zz_trash_前缀的表能正常查询、把回收站表改回原名后业务读写恢复。第三个检查最容易被跳过但恰恰是它决定了这套机制在真出事的那天到底能不能救命。我自己的习惯是每次改完删表逻辑都先在测试库造 200 张表跑一遍完整的两阶段流程再对着审计表逐行核对数量对不上就不许发。从那以后我每次动这类高危脚本都强制走一遍「造表、dry-run、真删、恢复」四步一次没落过。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑