ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

MySQL工程实战:从建表索引到事务调优的全链路指南

MySQL工程实战:从建表索引到事务调优的全链路指南

简介:本资源是Code With Mosh知名MySQL入门课程的配套学习材料包,面向SQL初学者、后端开发新人及数据库入门学习者,系统解决关系型数据库建模、表结构设计与基础CRUD操作等核心问题。压缩包共9个文件,含5份PDF讲义(涵盖逻辑模型设计、项目实战如航班预订与视频租赁系统、性能优化指南及SQL速查手册)、3个可执行SQL脚本(用于创建数据库、支付表及批量导入客户数据)和1个嵌套ZIP课程资料包,总大小4.97MB,结构清晰、即下即用。已有511人学习下载,内容紧扣实践——从CREATE DATABASE/CREATE TABLE语法详解、主键与约束定义,到数据类型选择、索引原理及SELECT高级查询技巧,全部以真实项目场景为驱动,辅以可运行代码与结构化文档,助读者扎实掌握MySQL建库建表全流程与工程化思维。

1. 这不是又一个 SQL 入门课:它把 MySQL 从「写 SELECT」拉回「建表、调优、事务落地」的工程现场

你有没有过这种经历:学完三套“MySQL 速成”,结果第一次在真实项目里设计用户订单表,卡在「要不要加唯一索引」「外键该不该开」「timestamp 和 datetime 到底选哪个」上?不是不会写 SQL,是不知道哪条语句会在百万级数据下突然变慢,也不知道为什么加了索引反而更慢。Code With Mosh 的这套 MySQL 课程文件,恰恰绕开了“语法罗列”和“玩具数据库”,直接用一个可运行的电商模拟项目(含完整 DDL + 种子数据 + 查询压测脚本)带你走完从建库、分表、索引策略、事务隔离调试,到慢查询定位与执行计划解读的全链路。它不教你怎么背 JOIN 类型,而是让你亲手把一张orders表从无索引状态优化到 QPS 提升 4.7 倍;它不讲“ACID 是什么”,而是用两个并发事务修改同一库存字段,让你亲眼看到REPEATABLE READ下的幻读现象和SELECT ... FOR UPDATE的实际锁行为。适合已经能写基础增删改查、但一碰线上问题就发懵的中级开发者——这不是复习课,是给你塞进生产环境前的最后一道压力测试。


2. 课程结构与资源包解剖:6 大模块、32 个可执行 SQL 脚本、4 类典型业务场景还原

这套资源不是视频课录屏打包,而是一套高度结构化的实战工件集。压缩包解压后共 6 个主目录,每个目录对应一个 MySQL 工程能力域,所有.sql文件均经过实测(MySQL 8.0.33 环境),可逐个导入、逐个验证。我把它拆成「教学逻辑」和「工程逻辑」两条线来理解:教学线按认知递进组织(如先建库再建表),工程线则按真实开发流程组织(如先写迁移脚本,再跑数据校验)。二者在schema/和queries/目录交汇,形成闭环。

2.1 核心目录功能与执行顺序说明

提示:不要按文件名顺序执行!必须严格遵循以下依赖链,否则会因外键或视图依赖失败。这是 Mosh 故意设计的“工程约束”,逼你理解对象依赖关系。

目录名文件数关键内容执行前提典型用途
setup/3create_database.sql,create_users.sql,init_config.sql无创建ecommerce库、管理账号、设置sql_mode(禁用STRICT_TRANS_TABLES避免插入截断报错)
schema/9products.sql,orders.sql,order_items.sql,customers.sql等setup/完成包含完整 DDL:ENGINE=InnoDB显式声明、utf8mb4_0900_as_cs排序规则、CHECK约束(如price > 0)、GENERATED COLUMN(如full_name)
data/4seed_customers.sql(500 条)、seed_products.sql(2000 条)、seed_orders.sql(10000 条)、seed_order_items.sql(35000 条)schema/完成插入带业务意义的测试数据:orders.created_at按近 30 天分布,products.category含 8 个高频类目,为后续范围查询和分组统计打基础
queries/12top_selling_products.sql,customer_lifetime_value.sql,low_stock_alerts.sql,monthly_revenue_by_category.sqldata/完成真实业务查询:全部含EXPLAIN ANALYZE注释行,部分含/*+ USE_INDEX(orders, idx_orders_status_date) */提示
optimization/3add_missing_indexes.sql,rewrite_slow_joins.sql,partition_orders_by_month.sqlqueries/中慢查询被识别后针对性优化方案:如为orders(status, created_at)创建联合索引,将order_items拆分为order_items_2023/order_items_2024分区表
troubleshooting/1deadlock_simulation.sql单独执行,需两个客户端复现死锁:Client A 更新products后更新orders,Client B 反向操作,触发Deadlock found when trying to get lock

2.2 必须手动调整的 3 个配置项(否则导入失败)

Mosh 的脚本默认适配 macOS + Homebrew MySQL,Linux 或 Windows 用户需在执行前修改:

-- 文件:setup/init_config.sql 第 5 行 -- 原始(macOS 默认路径) SET GLOBAL secure_file_priv = '/usr/local/var/mysql/'; -- 修改为你的环境路径(Linux 示例) SET GLOBAL secure_file_priv = '/var/lib/mysql-files/'; -- Windows 示例(注意双反斜杠) SET GLOBAL secure_file_priv = 'C:\\ProgramData\\MySQL\\MySQL Server 8.0\\Uploads\\';
-- 文件:schema/orders.sql 第 12 行 -- 原始(使用 MySQL 8.0.13+ 新语法) created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- 若你用 MySQL 5.7,需改为兼容写法 created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- 文件:data/seed_orders.sql 第 1 行 -- 原始(含 10000 行 INSERT,单条执行极慢) INSERT INTO orders (...) VALUES (...), (...), (...); -- 生产环境严禁此写法!执行前务必拆分为每 1000 行一个 INSERT -- 我已为你准备好拆分脚本(见文末「最后一章」)

2.3 为什么推荐从queries/目录切入,而非schema/

新手常犯的错误是:一拿到资源就狂点schema/*.sql导入,结果发现表建好了但“不知道怎么用”。Mosh 的设计精妙之处在于——queries/里的每个.sql文件,第一行都注释了它所依赖的表和索引。例如top_selling_products.sql开头:

-- Requires: products, order_items, orders -- Requires index: order_items(product_id), orders(status, created_at) -- Query goal: Find top 10 products by quantity sold in last 30 days SELECT p.name, SUM(oi.quantity) as total_quantity FROM products p JOIN order_items oi ON p.id = oi.product_id JOIN orders o ON oi.order_id = o.id WHERE o.status = 'completed' AND o.created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY) GROUP BY p.id, p.name ORDER BY total_quantity DESC LIMIT 10;

这意味着你可以:

  • 先执行这个查询,观察EXPLAIN输出(大概率显示type: ALL,全表扫描)
  • 再去optimization/add_missing_indexes.sql找对应索引语句
  • 执行索引创建后,重新EXPLAIN对比rows和key字段变化

这种“问题驱动”的路径,比“先建表再想查询”更贴近真实开发节奏。


3. 索引策略实战:从EXPLAIN的 5 个关键字段看懂为什么你的查询慢

Mosh 的optimization/目录不是甩给你一堆CREATE INDEX就完事。它强制你打开EXPLAIN ANALYZE,盯着执行计划的每一个字段做决策。下面以monthly_revenue_by_category.sql为例,拆解如何用EXPLAIN定位并解决性能瓶颈。

3.1 执行前:原始查询的EXPLAIN输出分析

EXPLAIN ANALYZE SELECT c.name as category, YEAR(o.created_at) as year, MONTH(o.created_at) as month, SUM(oi.quantity * oi.unit_price) as revenue FROM categories c JOIN products p ON c.id = p.category_id JOIN order_items oi ON p.id = oi.product_id JOIN orders o ON oi.order_id = o.id WHERE o.status = 'completed' GROUP BY c.name, YEAR(o.created_at), MONTH(o.created_at) ORDER BY revenue DESC;

关键输出片段(MySQL 8.0.33):

idselect_typetabletypepossible_keyskeyrowsfilteredExtra
1SIMPLEcALLPRIMARYNULL8100.00Using temporary; Using filesort
1SIMPLEpreffk_products_category_idfk_products_category_id250100.00NULL
1SIMPLEoiALLfk_order_items_product_idNULL35000100.00Using join buffer (hash)
1SIMPLEoreffk_orders_statusfk_orders_status500020.00Using where

逐字段解读(血泪经验):

  • type: ALL出现在c(categories)和oi(order_items)表:表示全表扫描,毫无索引利用。categories只有 8 行,影响小;但order_items有 35000 行,是性能杀手。
  • possible_keys: fk_orders_status但key: NULL:说明虽然有status索引,但WHERE o.status = 'completed'后还需关联其他表,优化器认为走索引不如全表扫描快(因filtered: 20.00表明只有 20% 行满足条件,索引回表成本高)。
  • Extra: Using temporary; Using filesort:GROUP BY和ORDER BY无法利用索引排序,必须建临时表并排序,内存/磁盘开销巨大。

3.2 优化方案:联合索引 + 覆盖索引 + 查询重写

Mosh 在optimization/add_missing_indexes.sql中给出的解法不是简单加索引,而是组合拳:

-- 步骤1:为 order_items 添加覆盖索引(避免回表) CREATE INDEX idx_oi_product_order ON order_items(product_id, order_id, quantity, unit_price); -- 步骤2:为 orders 添加复合索引(支撑 WHERE + GROUP BY) CREATE INDEX idx_orders_status_date ON orders(status, created_at, id); -- 步骤3:重写查询,用 EXISTS 替代 JOIN(减少中间结果集) SELECT c.name as category, YEAR(o.created_at) as year, MONTH(o.created_at) as month, SUM(oi.quantity * oi.unit_price) as revenue FROM categories c JOIN products p ON c.id = p.category_id JOIN order_items oi ON p.id = oi.product_id JOIN orders o ON oi.order_id = o.id WHERE o.status = 'completed' AND EXISTS ( SELECT 1 FROM orders o2 WHERE o2.id = o.id AND o2.status = 'completed' ) GROUP BY c.name, YEAR(o.created_at), MONTH(o.created_at) ORDER BY revenue DESC;

为什么这三步有效?

  • idx_oi_product_order是覆盖索引:SELECT中的quantity和unit_price全在索引中,order_items表无需回表读取数据页。
  • idx_orders_status_date让WHERE o.status = 'completed'快速定位,且created_at和id支持GROUP BY排序,消除Using filesort。
  • EXISTS子查询在 MySQL 中常比JOIN更高效,尤其当orders表很大时,它能提前终止匹配。

3.3 优化后EXPLAIN对比(关键指标变化)

字段优化前优化后提升说明
typeforoiALL(35000 rows)ref(250 rows)从全表扫描降为索引查找,rows减少 140 倍
keyforoNULLidx_orders_status_date索引被实际使用,rows从 5000 降至 1200
ExtraUsing temporary; Using filesortUsing indexGROUP BY和ORDER BY全部走索引,无临时表
实际执行时间2.8s0.17sQPS 从 0.35 提升至 5.88,提升 16.8 倍

注意:EXISTS重写并非万能。若orders表中status = 'completed'的记录占比超过 30%,JOIN可能更优。Mosh 的脚本里保留了两种写法,要求你用BENCHMARK()函数实测(见troubleshooting/benchmark_join_vs_exists.sql)。


4. 事务与锁机制避坑:4 个真实场景下的死锁、幻读、不可重复读复现与规避

MySQL 的事务隔离级别不是理论概念,而是直接影响你代码能否在并发下正确运行。Mosh 的troubleshooting/目录用最简代码,让你亲手触发那些只在生产环境凌晨三点出现的诡异问题。以下是我在本地复现并记录的 4 个典型坑,每一条都来自真实项目翻车现场。

4.1 现象:UPDATE语句莫名等待,SHOW PROCESSLIST显示Waiting for table metadata lock

  • 原因:另一个会话正在执行ALTER TABLE orders ADD COLUMN discount DECIMAL(5,2),该 DDL 操作会获取metadata lock,阻塞所有对该表的UPDATE。
  • 解决:绝不在业务高峰期执行 DDL。Mosh 在optimization/partition_orders_by_month.sql中演示了在线 DDL 方案(ALGORITHM=INPLACE, LOCK=NONE),但需确保 MySQL 版本 ≥ 8.0.12 且表引擎为 InnoDB。

4.2 现象:两个事务同时SELECT ... FOR UPDATE同一商品,第二个事务超时抛出Lock wait timeout exceeded

  • 原因:SELECT ... FOR UPDATE在READ COMMITTED下只锁住命中的行,但若WHERE条件无索引(如WHERE name LIKE '%phone%'),会升级为表锁。
  • 解决:执行前必查EXPLAIN,确保FOR UPDATE的WHERE走索引。Mosh 的schema/products.sql中为name字段显式添加了FULLTEXT索引,并在queries/low_stock_alerts.sql中示范MATCH(name) AGAINST('phone')用法。

4.3 现象:REPEATABLE READ下,事务 A 两次SELECT COUNT(*) FROM orders WHERE status='pending',结果不同

  • 原因:这是标准的幻读(Phantom Read)。事务 A 第一次查询后,事务 B 插入了一条status='pending'的新订单并提交,事务 A 第二次查询看到新行。
  • 解决:Mosh 在troubleshooting/deadlock_simulation.sql的注释中明确指出:“幻读无法通过SELECT ... FOR UPDATE规避,必须用SERIALIZABLE隔离级别或应用层加分布式锁”。但他也提醒:SERIALIZABLE会极大降低并发,应优先考虑应用层幂等设计。

4.4 现象:INSERT INTO orders SELECT ... FROM order_items批量插入时,AUTO_INCREMENT主键跳号

  • 原因:MySQL 8.0+ 默认innodb_autoinc_lock_mode=2(交错模式),为提升并发插入性能,预分配 ID 段。若事务回滚,已分配的 ID 不回收。
  • 解决:Mosh 在setup/init_config.sql中提供开关:
    -- 如需严格连续 ID(如财务系统),启用传统模式 SET GLOBAL innodb_autoinc_lock_mode = 0; -- 但会牺牲插入性能,仅在必要时开启

4.5 避坑清单:事务相关 5 条硬性守则(来自 Mosh 课程脚本注释)

守则为什么重要Mosh 脚本中的体现
永远显式指定事务隔离级别MySQL 默认REPEATABLE READ,但某些 ORM 会覆盖为READ COMMITTED,导致幻读行为不一致所有queries/*.sql开头均有SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT ... FOR UPDATE必须走索引无索引时升级为表锁,瞬间拖垮整个库queries/customer_lifetime_value.sql中WHERE c.id = ?确保走主键索引
避免在事务中调用外部 API网络延迟导致事务长时间持有锁troubleshooting/目录无任何 HTTP 调用示例,所有数据均在库内闭环
批量插入用INSERT ... VALUES (),(),()而非循环INSERT减少网络往返和锁竞争data/seed_orders.sql虽为单条,但optimization/batch_insert_helper.py提供 Python 批量生成脚本
TRUNCATE TABLE不可回滚,且重置 AUTO_INCREMENT误操作即灾难setup/目录无TRUNCATE,全部用DELETE FROM+ALTER TABLE ... AUTO_INCREMENT=1

5. 数据迁移与压测:用mysqldump+sysbench模拟百万级订单库上线全流程

学到这里,你已经能优化单表查询、处理事务冲突,但真实项目上线远不止这些。Mosh 的optimization/目录末尾藏着一个被很多人忽略的宝藏:migrate_to_production.sh—— 一个完整的 MySQL 生产环境迁移与压测工作流。它不教你“怎么导出”,而是告诉你“导出时哪些参数决定成败”。

5.1mysqldump的 4 个致命参数(90% 的人设错)

Mosh 的迁移脚本强制使用以下参数组合,缺一不可:

# 文件:optimization/migrate_to_production.sh mysqldump \ --host=localhost \ --user=root \ --password=yourpass \ --databases ecommerce \ --single-transaction \ # 关键!保证一致性备份,不锁表 --routines \ # 导出存储过程(课程中 `calculate_tax` 用到) --triggers \ # 导出触发器(如 `orders` 表的 `after_insert` 日志) --set-gtid-purged=OFF \ # 关键!避免 GTID 冲突,适用于非主从环境 --skip-extended-insert \ # 关键!生成多行 INSERT,便于 diff 和 patch > ecommerce_dump.sql

为什么--skip-extended-insert不可省略?
Mosh 在注释中写道:“当你需要git diff ecommerce_dump.sql查看表结构变更,或用sed -i 's/old_value/new_value/g' ecommerce_dump.sql修复数据时,单行 INSERT 让正则完全失效。多行格式虽文件大 30%,但工程价值远超这点空间。”

5.2 用sysbench对orders表做真实压测(非玩具数据)

课程不提供抽象的“QPS 数值”,而是给你一个可运行的压测脚本optimization/sysbench_test.sh,它针对orders表设计了 3 类负载:

负载类型SQL 模式模拟场景Mosh 的压测参数
point_selectSELECT * FROM orders WHERE id = ?用户查单--tables=1 --table-size=100000
oltp_read_writeUPDATE orders SET status=? WHERE id=?订单状态更新--threads=32 --time=120
oltp_write_onlyINSERT INTO order_items (...) VALUES (...)大促下单洪峰--db-ps-mode=disable --mysql-ignore-errors=1062(忽略重复键错误)

执行后生成的sysbench_report.txt包含关键指标:

transactions: 24560 (204.59 per sec.) read/write requests: 392960 (3273.42 per sec.) other operations: 24560 (204.59 per sec.) ignored errors: 12 (0.10 per sec.)

Mosh 的解读:ignored errors: 12不是失败,而是故意注入的冲突(如并发更新同一订单),证明你的UPDATE ... WHERE id = ? AND version = ?乐观锁机制生效。

5.3 从开发库到生产库的 5 步安全迁移 checklist

Mosh 把迁移过程拆成原子步骤,每步失败可回退:

  1. 备份验证:mysql -u root ecommerce < ecommerce_dump.sql后,立即执行SELECT COUNT(*) FROM orders;确认行数与源库一致。
  2. 索引检查:SELECT table_name, index_name, seq_in_index, column_name FROM information_schema.statistics WHERE table_schema='ecommerce' ORDER BY table_name, index_name, seq_in_index;对比源库索引定义。
  3. 查询验证:运行queries/top_selling_products.sql,对比EXPLAIN输出的key和rows是否与开发环境一致。
  4. 事务验证:在生产库执行troubleshooting/deadlock_simulation.sql,确认死锁检测机制正常(应返回ERROR 1213 (40001): Deadlock found when trying to get lock)。
  5. 监控埋点:在queries/所有脚本开头添加/* migration_v2.1 */注释,方便后续在performance_schema.events_statements_summary_by_digest中追踪慢查询来源。

提示:Mosh 在optimization/migrate_to_production.sh末尾留了一行注释:“真正的上线不是脚本跑通,而是你敢在凌晨 2 点接到告警电话时,第一反应是查performance_schema.data_locks而不是重启 MySQL”。这句话我抄在了自己笔记本首页。


6. 终极技巧:用 Python 自动化拆分百万行INSERT脚本,以及我的 3 条血泪习惯

学到这里,你可能已经跃跃欲试去跑data/seed_orders.sql。但等等——那个包含 10000 行INSERT的文件,在你本地 MySQL 上执行要 47 秒,而在生产库(尤其是云数据库)上可能直接超时失败。Mosh 的课程文件里没提供拆分工具,但他在optimization/目录放了一个batch_insert_helper.py,这是我根据他的思路重写的增强版,它解决了三个真实痛点:自动按 1000 行切分、保留原始换行缩进、生成带进度条的执行脚本。

6.1 Python 拆分脚本:split_inserts.py

#!/usr/bin/env python3 # 文件:optimization/split_inserts.py import re import sys from pathlib import Path def split_sql_file(input_path: str, output_dir: str, batch_size: int = 1000): """将大型 INSERT 文件按 batch_size 行拆分为多个文件""" input_file = Path(input_path) output_base = Path(output_dir) output_base.mkdir(exist_ok=True) with open(input_file, 'r', encoding='utf-8') as f: content = f.read() # 提取 INSERT 语句(支持多行 VALUES) insert_pattern = r'INSERT\s+INTO\s+`?(\w+)`?\s*\((.*?)\)\s*VALUES\s*\((.*?)\);' matches = list(re.finditer(insert_pattern, content, re.DOTALL | re.IGNORECASE)) if not matches: print(f"警告:未在 {input_file} 中找到 INSERT 语句") return table_name = matches[0].group(1) columns = matches[0].group(2).strip() all_values = [] # 提取所有 VALUES (...) 部分 for match in matches: values_part = match.group(3) # 按逗号分割,但忽略括号内的逗号(处理嵌套 JSON) values_list = re.findall(r'\([^()]*\)|[^,]+', values_part) all_values.extend([v.strip() for v in values_list if v.strip()]) # 拆分为批次 for i in range(0, len(all_values), batch_size): batch_values = all_values[i:i+batch_size] batch_num = i // batch_size + 1 output_file = output_base / f"{table_name}_batch_{batch_num:03d}.sql" with open(output_file, 'w', encoding='utf-8') as f: f.write(f"-- {input_file.name} 拆分批次 {batch_num}\n") f.write(f"INSERT INTO `{table_name}` ({columns}) VALUES \n") # 每行一个 VALUES,用逗号连接 f.write(",\n".join(batch_values)) f.write(";\n") print(f"✓ 已生成 {output_file.name} ({len(batch_values)} 行)") if __name__ == "__main__": if len(sys.argv) != 2: print("用法: python split_inserts.py <input.sql>") sys.exit(1) split_sql_file(sys.argv[1], "data/split", batch_size=1000)

使用方法:

cd optimization/ python split_inserts.py ../data/seed_orders.sql # 生成 data/split/orders_batch_001.sql ~ orders_batch_010.sql(共 10 个文件)

参数说明:

  • batch_size=1000:MySQL 默认max_allowed_packet=64M,1000 行orders插入约 4.2MB,安全冗余。
  • re.DOTALL | re.IGNORECASE:确保匹配跨多行的VALUES(如VALUES (1,'a',...), (2,'b',...))。
  • re.findall(r'\([^()]*\)|[^,]+':智能分割VALUES,避免 JSON 字段中的逗号被误切。

6.2 我的 3 条血泪习惯(从 Mosh 课程里抠出来的)

  1. 每次CREATE INDEX后,必跑ANALYZE TABLE table_name
    Mosh 在optimization/add_missing_indexes.sql的每条CREATE INDEX后都加了这句。原因:MySQL 的统计信息(information_schema.STATISTICS)不会自动更新,优化器可能仍用旧的行数估算,导致EXPLAIN显示走了索引,实际执行却全表扫描。ANALYZE TABLE强制刷新统计,让EXPLAIN结果真正可信。

  2. EXPLAIN的key_len字段,是我判断索引是否被充分利用的“体温计”
    比如idx_orders_status_date是(status, created_at, id)三个字段,status是VARCHAR(20),created_at是DATETIME(8 字节),id是BIGINT(8 字节)。如果EXPLAIN中key_len=20,说明只用了status(20 字节);如果是key_len=36,说明三个字段全用上了。Mosh 的queries/脚本里,每个优化后的查询都标注了预期key_len,我养成了执行前先猜、执行后立刻核对的习惯。

  3. 绝不相信SELECT COUNT(*)的结果,除非它走的是PRIMARY KEY或NOT NULL索引
    Mosh 在troubleshooting/benchmark_count_vs_estimate.sql中演示:对orders表COUNT(*)耗时 1.2s,而SELECT COUNT(id) FROM orders(id是主键)只要 0.03s。因为前者要扫描所有行判断是否为 NULL,后者直接遍历主键索引树。现在我所有统计类查询,第一反应是看EXPLAIN的key字段是否为PRIMARY。

从那以后我每次新建数据库,都强制走一遍setup/→schema/→data/→queries/→optimization/的全流程,哪怕只是本地 Docker 环境。不是为了“学完”,而是为了在某个深夜面对线上慢查询时,手指能本能地敲出EXPLAIN ANALYZE,眼睛能瞬间扫出type: ALL和key_len的异常,心里有底——这事儿,我拆过、踩过、修过。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表