
简介这份《数据库系统课程设计报告——学生选课管理信息系统》面向高校数据库课程设计的学生与指导教师提供可直接参考的完整范文与模板。报告围绕选课系统展开依次覆盖需求分析、组织结构与业务流/数据流分析、数据字典、概念模型CDM、逻辑模型PDM、SQL Server 表设计与完整性约束以及视图、触发器、存储过程、索引的创建和功能调试并延伸到登录与成绩管理模块的应用程序设计便于读者理解从建模到实现的完整链路。压缩包内共 1 个 docx 文件约 3.96MB结构完整、目录清晰适合按章节检索和二次修改。目前已有 852 人学习可作为课程设计选题、报告撰写和数据库实验排版的实用素材。1. 学生选课管理信息系统的课程设计真正拉开差距的是数据一致性答辩现场最容易被追问的一句话往往不是“你画了几张 ER 图”而是“两个学生同一秒抢最后一个名额你的系统会不会超卖”。学生选课管理信息系统这个题目看起来温和——无非学生、课程、教师、选课几张表——但课程设计报告的分数常常就压在这种边界上。能建表、能增删改查只是及格线约束有没有写进 DDL、选课事务有没有锁住课容量、高频查询有没有走索引才是报告里真正能拿出来讲的东西。这篇内容面向正在做数据库系统课程设计的同学也面向想补一遍数据库工程落地经验的开发者从需求拆实体、写 DDL、做事务与并发一路做到用执行计划证明优化有效每一步都给能直接抄的结构和语句也会把踩过的坑摊开讲清楚。2. 从需求到 ER 图选课系统最容易画错的三处关系2.1 用业务动作反推实体清单而不是先画方框很多人一上来就在纸上画“学生”“课程”两个方框然后中间连一条线写个“选”。这样画出来的图能交但一落到表结构就会出问题成绩往哪放同一门课这学期开、下学期还开课程名要重复录几遍教师和课程是什么关系这些都得回头改图。我一般的做法是先把业务动作列全再从动作里抠名词。选课系统的动作其实是固定的几条学生查可选课程、学生选课、学生退课、教师查看选课名单、教师录入成绩、教务管理员排课开教学班、教务管理员统计选课人数。把每条动作涉及的对象写下来实体清单基本就出来了。业务动作涉及实体关键属性先粗后细学生查可选课程学生、教学班、课程学号、班级号、课程名、学分、余量学生选课 / 退课学生、教学班、选课记录学号、班级号、选课时间、状态教师查名单教师、教学班、选课记录工号、班级号、学生名单教师录成绩选课记录平时分、期末分、总评管理员排课课程、教师、教学班、学期课程号、工号、容量、上课时间管理员统计教学班、选课记录选课人数、余量、课程分布这张表的用处不只是列实体它同时告诉你哪些字段会被频繁查询。比如“查可选课程”这条动作前端列表要显示课程名、学分、余量、教师名这意味着如果严格按范式拆表这个查询至少要 join 四张表——那就得考虑视图或者适度冗余这个判断在后面的索引一节还会用到。主键的选择在这里就该定下来。学号用业务主键定长字符串比自增整数好因为它本身有含义、跨系统对得上教学班号用自增整数没有语义可以放心用代理主键。这一点看着是小事但报告里写清楚“为什么这张表用业务主键、那张表用代理主键”就是一个能体现设计思考的点。2.2 选课记录为什么必须独立成表“学生”和“教学班”是多对多关系多对多必须落成中间表这个大家都知道。但真正容易翻车的是这张中间表的粒度。第一个问题是联系上的属性。选课这个动作带了一堆属性选课时间、选课状态已选、已退、已锁定、成绩。这些属性既不属于学生也不属于教学班只能挂在联系上。如果图省事把成绩塞进学生表比如加一列“已修课程成绩”那一个学生修十门课就没地方存了。所以选课记录必须是一张独立的表一行代表“某个学生在某个教学班的一次选课”。第二个问题是重修。同一个学生、同一门课程可能在不同学期各选一次甚至同一学期因为挂科补考产生多条记录。这时候如果把唯一约束设成(学号, 课程号)重修直接选不上——这是课程设计里出现频率极高的一个坑我会在第 5 章展开讲。正确的唯一键应该是(学号, 教学班号)因为教学班本身已经绑定了学期。第三个问题是状态。选课不是一个瞬时动作它有生命周期选了可能退退了可能再选教师录完成绩之后这条记录就冻结。把状态做成一个字段而不是靠“删行”来表达退课好处是能留痕、能统计退课率坏处是唯一索引会跟你较劲。这个取舍在报告里值得单独写一段它体现的是你对“删除 vs 状态位”的理解。2.3 ER 图转关系模式教学班到底该不该从课程里拆出来这是整个设计里最能体现水平的一个决策也是我见过最多人做错的地方。新手常见写法是只建一张“课程表”字段是课程号、课程名、学分、教师、上课时间、容量。问题是同一门“数据结构”这学期张老师教、下学期李老师教你要么插两行课程课程号重复主键冲突要么改原来那行历史数据没了。这两种都不行。正确做法是拆成两层课程课程号、课程名、学分、学时、开课院系和教学班班级号、课程号、教师工号、学期、容量、已选人数、上课时间、教室。课程是“这门课是什么”教学班是“这门课这学期由谁在哪上”。一门课程可以对应多个教学班一个教师可以带多个教学班学生选的是教学班而不是课程。转换规则其实就三条写报告时可以直接对应1:N 联系课程—教学班、教师—教学班把“1”端的主键放到“N”端做外键不要为它单独建表。M:N 联系学生—教学班必须独立建表并把两端主键组合成联合唯一键。带属性的联系选课记录上的成绩、时间、状态属性跟着中间表走不要上提到实体表。再往上一层还有院系。学生表里如果直接存“院系名称”改一次学院名就要更新几百行还会出现同一学院两种写法。按第三范式把院系拆出去学生表只留院系编号这个动作在报告里一句话就能说清但答辩时被问到“你的表满足第几范式”能指着这里答比背定义强得多。最后提醒一点ER 图上的菱形联系和矩形实体数量不必追求多。我见过画了十几个实体、结果一半没有对应查询的表这种图好看但没用。判断标准很简单——每个实体都要能回答“谁会查它、查它干什么”答不上来的就删掉。3. 建表落地把选课规则写进 DDL 约束3.1 字段类型与编码课程号为什么不建议用自增整数课程号、学号这类字段有一个共同点它们有语义、跨系统要对得上。学号里通常编码了入学年份和院系课程号里通常编码了开课单位。如果图省事全部用自增整数导数据的时候对不上答辩演示时也没法口头解释“这个 10023 是哪门课”。所以我的习惯是业务主键用定长字符学号CHAR(12)、课程号CHAR(8)代理主键只在没有自然键的表上用比如教学班号、选课流水号。几个容易选错的类型列一下字段推荐类型不推荐原因学号CHAR(12)VARCHAR、INT定长、有前导零、有语义成绩DECIMAL(5,2)FLOAT、DOUBLE浮点会出现 89.99999 这种值学分DECIMAL(3,1)INT存在 0.5、1.5 学分选课状态TINYINT 注释VARCHAR省空间、比较快、便于加索引选课时间DATETIMEVARCHAR要按时间排序和范围查询学期CHAR(11)如 2024-2025-1两个INT单字段比较排序天然正确字符集这一项单独说。中文课程名、教师名必须用utf8mb4老版本里的utf8实际是三个字节遇到生僻字或者某些符号会直接截断报错。更麻烦的是如果建库时用了一个字符集、建表时用了另一个join 的时候会走隐式转换索引直接失效——这个坑在第 5 章会详细说。注意成绩字段允许为NULL表示“还没录入”不要用 0 代替。0 分和未录入在统计及格率时是完全不同的两件事。3.2 主外键、唯一约束与 CHECK把业务规则从代码搬到数据库约束写在数据库里比写在应用层代码里可靠得多。应用层可能有好几个入口网页、脚本、手工 SQL只要有一个漏了校验脏数据就进去了。下面是我常用的建表顺序注意外键依赖被引用的表必须先建。-- 1. 院系被学生、教师、课程引用最先建 CREATE TABLE department ( dept_id INT PRIMARY KEY AUTO_INCREMENT, dept_name VARCHAR(50) NOT NULL, UNIQUE KEY uk_dept_name (dept_name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 2. 学生学号用业务主键状态 1 在读 / 0 离校 CREATE TABLE student ( sno CHAR(12) PRIMARY KEY, sname VARCHAR(30) NOT NULL, gender CHAR(1) NOT NULL DEFAULT U, dept_id INT, enroll_year SMALLINT NOT NULL, status TINYINT NOT NULL DEFAULT 1, CONSTRAINT ck_student_gender CHECK (gender IN (M,F,U)), CONSTRAINT fk_student_dept FOREIGN KEY (dept_id) REFERENCES department(dept_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 3. 课程课程本身的属性不含教师和学期 CREATE TABLE course ( cno CHAR(8) PRIMARY KEY, cname VARCHAR(60) NOT NULL, credit DECIMAL(3,1) NOT NULL, hours SMALLINT NOT NULL DEFAULT 0, dept_id INT, CONSTRAINT ck_course_credit CHECK (credit 0 AND credit 10), CONSTRAINT fk_course_dept FOREIGN KEY (dept_id) REFERENCES department(dept_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 4. 教学班一门课在某学期由某教师开的一个班 CREATE TABLE teaching_class ( class_id INT PRIMARY KEY AUTO_INCREMENT, cno CHAR(8) NOT NULL, teacher_id INT NOT NULL, semester CHAR(11) NOT NULL, capacity SMALLINT NOT NULL, selected SMALLINT NOT NULL DEFAULT 0, classroom VARCHAR(30), schedule VARCHAR(60), CONSTRAINT ck_tc_capacity CHECK (capacity 0 AND capacity 500), CONSTRAINT ck_tc_selected CHECK (selected 0 AND selected capacity), UNIQUE KEY uk_tc (cno, teacher_id, semester, schedule), KEY idx_tc_semester (semester, cno), CONSTRAINT fk_tc_course FOREIGN KEY (cno) REFERENCES course(cno), CONSTRAINT fk_tc_teacher FOREIGN KEY (teacher_id) REFERENCES teacher(teacher_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有几个细节值得展开。uk_tc这个联合唯一键防的是“同一位老师在同一学期、同一时间段开了两门同样的课”属于排课阶段的重复录入不是选课阶段的重复选课两者别混。ck_tc_selected这个 CHECK 是最后一道防线——它保证已选人数永远不会超过容量就算应用层逻辑写错了数据库也会把这条 UPDATE 挡回去。选课表单独拿出来因为它的约束最多CREATE TABLE enrollment ( enroll_id BIGINT PRIMARY KEY AUTO_INCREMENT, sno CHAR(12) NOT NULL, class_id INT NOT NULL, status TINYINT NOT NULL DEFAULT 1, -- 1 已选 2 已退 3 已锁 select_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, score DECIMAL(5,2) DEFAULT NULL, CONSTRAINT ck_enroll_score CHECK (score IS NULL OR (score 0 AND score 100)), UNIQUE KEY uk_enroll (sno, class_id, status), KEY idx_enroll_class (class_id, status), KEY idx_enroll_sno (sno), CONSTRAINT fk_enroll_student FOREIGN KEY (sno) REFERENCES student(sno), CONSTRAINT fk_enroll_class FOREIGN KEY (class_id) REFERENCES teaching_class(class_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;uk_enroll里带上status是为了解决退课重选退课不删行把status改成 2学生再选时插入一条status1的新行不会撞唯一键。这个设计有代价——同一个学生同一门课会积累多条历史行查询当前有效选课必须带status 1条件漏了就会重复统计。另一种更干脆的做法是退课直接DELETE唯一键只留(sno, class_id)简单但丢历史。两种都能用报告里选一种并说明理由即可。提示MySQL 在 8.0.16 之前的版本会解析但不执行CHECK 约束。如果你的环境版本偏低上面那些 CHECK 等于没写必须改用触发器或者应用层校验别以为写上去就生效了。3.3 索引规划选课系统的四类高频查询索引不是越多越好每加一个索引插入和更新就多一份维护成本。选课系统的查询其实很集中按使用频率排下来大概是这四类第一类是“查某学期可选课程列表”条件通常是semester ?可能再按课程号过滤走idx_tc_semester。这个索引是(semester, cno)的联合索引注意最左前缀——如果查询只带cno不带semester用不上它。第二类是“查某个教学班已选人数”或者“查某班名单”条件是class_id ?走idx_enroll_class。之所以把status也放进联合索引是因为几乎所有这类查询都要加status 1两列一起走能少回表。第三类是“查某个学生的课表”条件是sno ?走idx_enroll_sno。第四类是成绩统计比如“某课程平均分、及格率”这类查询往往要扫全表或大范围数据索引帮不上太多忙。我的处理方式是接受它慢把统计结果物化成一张汇总表录完成绩后批量刷新而不是让报表页面每次实时算。反过来哪些列不该建索引性别、状态这种只有两三个取值的列选择性太低走索引还不如全表扫频繁更新的selected列也不要单独建索引因为每次选课都要更新它索引维护成本会拖慢整个事务。这一条写进报告比列一堆索引更有说服力。4. 选课事务与并发最后一张余票为什么会被超卖4.1 事务边界一次选课到底包含哪几步先把“选一门课”这个动作拆成数据库层面看它至少包含检查教学班是否还有余量、写入一条选课记录、把教学班的已选人数加一。这三步必须在同一个事务里缺一不可。如果分开做就会出现这样的时序甲查到余量是 1乙也查到余量是 1甲插入记录并把人数字段加到 1乙插入记录也把人数字段加到 1——最后容量 30 的班收了 31 个人这就是超卖。注意这里有两个根源一是“先查后改”的读和写之间有窗口期二是冗余的selected字段本身就是一个需要维护的派生数据。所以事务边界的划法有两种思路对应下面两种写法。4.2 条件更新与行锁两种防超卖写法怎么选第一种是悲观锁先锁行再判断。关键是FOR UPDATE必须放在事务里且查询条件要能命中索引走主键或唯一键否则 InnoDB 会退化成锁全表并发一上来就集体卡住。START TRANSACTION; -- 锁住这个教学班这一行其他人此时只能等 SELECT capacity, selected INTO cap, sel FROM teaching_class WHERE class_id 1001 FOR UPDATE; -- 应用层判断 sel cap不满足就 ROLLBACK UPDATE teaching_class SET selected selected 1 WHERE class_id 1001; INSERT INTO enrollment (sno, class_id, status) VALUES (202301010001, 1001, 1); COMMIT;第二种是条件更新把判断塞进 WHERE 里靠影响行数来判断成败。这种写法不需要显式加锁InnoDB 在执行 UPDATE 时会自动给命中的行加排他锁语义更紧凑-- 只有还有余量时才会更新成功rowcount 返回 0 就代表满了 UPDATE teaching_class SET selected selected 1 WHERE class_id 1001 AND selected capacity; -- 上一步影响行数为 1 才继续否则直接回滚返回“已满” INSERT INTO enrollment (sno, class_id, status) VALUES (202301010001, 1001, 1);放到应用层用 Python 写出来大概是这样重点看异常分支的顺序import mysql.connector def enroll(conn, sno, class_id): cur conn.cursor() try: conn.start_transaction() # 显式开事务别依赖 autocommit cur.execute( UPDATE teaching_class SET selected selected 1 WHERE class_id %s AND selected capacity, (class_id,) ) if cur.rowcount 0: # 余量为 0抢失败了 conn.rollback() return CAPACITY_FULL cur.execute( INSERT INTO enrollment (sno, class_id, status) VALUES (%s, %s, 1), (sno, class_id) ) conn.commit() return OK except mysql.connector.IntegrityError: conn.rollback() # 撞唯一键重复选课 return DUPLICATE except Exception: conn.rollback() # 任何异常都要回滚否则计数会虚高 raise这段代码有两个顺序上的讲究。第一必须先更新计数再插选课记录。如果反过来先插记录再更新计数一旦计数更新失败比如容量满你得手动删掉刚插的那行而回滚整件事更干净。第二插入时撞唯一键抛异常回滚会把刚才那次selected 1一起撤销计数不会多出来——这正是把两步放进同一个事务的价值所在。两种写法的对比维度SELECT ... FOR UPDATE条件 UPDATE锁的范围显式锁住命中的行隐式锁住更新的行代码复杂度需要读出来再判断判断写在 WHERE 里失败语义判断后主动回滚看 rowcount适用场景选课前还要做复杂校验单纯抢名额逻辑简单风险点事务里夹杂慢查询会长时间持锁WHERE 条件写错会导致全表更新注意条件 UPDATE 里的WHERE class_id ?必须命中主键或索引。少了它一条 UPDATE 会锁住整张教学班表别人的选课全部排队看起来就像“系统卡死”。4.3 隔离级别怎么选死锁出现了怎么看选课场景我一般用READ COMMITTED。原因是这个业务对“不可重复读”不敏感——学生刷新一下看到余量变了是正常的、也是应该的但REPEATABLE READ下快照读可能让他一直看到旧余量反而容易误判。四个级别的取舍列个表报告里可以直接用隔离级别脏读不可重复读幻读选课场景适用性READ UNCOMMITTED可能可能可能不推荐会读到别人未提交的计数READ COMMITTED不会可能可能推荐余量变化实时可见REPEATABLE READ不会不会快照读下基本不会可用但要注意快照读不是最新值SERIALIZABLE不会不会不会并发下大量锁等待不适合选课高峰死锁在选课系统里是真实会发生的甲同学先选 A 课再选 B 课乙同学先选 B 课再选 A 课两个事务各持一把锁等对方释放数据库检测到环路后干掉其中一个报错码一般是 1213。解决办法不是加重试就完事而是统一加锁顺序——比如批量选课时按class_id升序逐个加锁让所有人的加锁路径一致环路就形不成。排查的时候先看数据库的死锁日志能看到具体是哪两条 SQL 在互相等待。这一步别猜日志里写得比你回忆的准。4.4 唯一索引、触发器、存储过程各管哪一段这三个东西经常被混用其实职责不同。唯一索引管“不许重复”。uk_enroll是防重复选课的最后一道闸应用层判重可能因为并发漏掉唯一索引不会。它的代价是插入失败会抛异常代码里必须捕获并转成友好提示否则用户看到的是 500。触发器管“自动联动”。比如选课记录插入后自动把selected加一听起来很优雅但我不推荐在选课主链路上用它。原因是触发器里的逻辑对应用层是黑匣子出问题时你只看到“插了一条记录计数莫名其妙多了 2”排查成本极高而且触发器执行失败会连带整个事务回滚错误信息往往被吞掉。触发器更适合做审计日志、状态变更留痕这种“失败了也不影响主流程”的事。存储过程管“封装多步”。把选课逻辑写成存储过程好处是网络交互少、逻辑集中坏处是可移植性差、调试困难、版本管理麻烦。课程设计的场景下我更倾向于把逻辑放在应用层用事务包住数据库只负责约束和存储——这样报告里讲起来清楚代码也方便贴进附录。5. 排查实录五个把我卡到后半夜的坑5.1 建表与数据导入阶段的三个坑坑一外键依赖顺序错了建表直接报 1215 / 1452。现象是执行建表脚本时前面几张表成功到中间某张表报“Cannot add foreign key constraint”或者导入测试数据时报 1452 外键约束失败。原因很简单被引用的表还没建或者被引用的列上没有索引、类型和引用列不一致比如一边INT一边BIGINT一边utf8mb4一边utf8。解决方式是按依赖顺序建表院系 → 学生/教师 → 课程 → 教学班 → 选课记录导入数据同理。另外脚本开头先执行关闭外键检查、结尾再打开可以规避历史数据顺序问题但生产环境慎用它只是让错误延后暴露。坑二唯一键设成(sno, cno)重修的学生选不上课。现象很直观某个学生上学期这门课挂了这学期重新选界面提示“已选过该课程”但查选课记录明明是上学期那条。原因是唯一索引建在了学号和课程号上而课程号在不同学期是同一个值。解决方式是把唯一键改成(sno, class_id)因为教学班号已经隐含了学期信息如果确实要限制“同一学期不能重复选同一门课”那也应该是(sno, cno, semester)这种带学期的组合而不是裸的(sno, cno)。改完记得清理历史脏数据否则加索引那一步会失败。坑三字符集不一致中文课程名变问号join 还特别慢。现象分两种轻的是查出来一堆???重的是本来该走索引的关联查询突然变成全表扫。原因通常是建库用了utf8mb4、某张历史表却是latin1或三字节的utf8两表 join 时数据库要做隐式字符集转换索引直接失效。解决办法是统一在库级别指定DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_general_ci逐表检查SHOW CREATE TABLE的输出把不一致的用ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4改过来。这一步在导入外部提供的测试数据时尤其要做。5.2 事务与并发阶段的两个坑坑四事务里夹了一个慢查询锁等待超时 1205 满天飞。现象是压测时零星报“Lock wait timeout exceeded”但代码逻辑看着没问题。抓日志才发现选课事务里除了更新计数还顺手查了一次“该生已修学分总计”而这个查询没走索引、扫了几万行把行锁持有了好几秒。原因是事务的边界画大了——所有查询都被包在一个事务里锁的持有时间等于最慢那条 SQL 的耗时。解决办法是把只读查询挪到事务外面先做事务里只保留“更新计数 插入记录”这两步锁持有时间从秒级降到毫秒级。坑五写了FOR UPDATE还是超卖最后发现是 autocommit 惹的祸。现象很邪门代码里明明有SELECT ... FOR UPDATE压测还是超卖。原因有两个可能一是连接开了 autocommit每条 SQL 自成一个事务FOR UPDATE加的锁在语句结束就释放了等于没锁二是 ORM 或者连接池把事务悄悄提交了你以为在同一个事务里其实不是。排查方式是打开数据库的通用日志看实际发出去的语句序列里有没有BEGIN/START TRANSACTION以及COMMIT出现的时机。解决方式是显式start_transaction()并在连接池配置里关掉自动提交压测时用两个并发各跑一百次验证计数是否精确等于成功次数。6. 让报告站得住执行计划、造数脚本和答辩前的自检索引建了不等于生效这句话在报告里最好用数据证明。最直接的办法是拿EXPLAIN对比加索引前后的执行计划EXPLAIN SELECT c.cname, t.selected, t.capacity FROM teaching_class t JOIN course c ON c.cno t.cno WHERE t.semester 2024-2025-1 AND t.selected t.capacity;重点看四列type是不是ref或range出现ALL就是全表扫、key是不是你建的那个索引、rows的估算量级、Extra里有没有Using filesort或Using temporary。把加索引前后的两份结果并排贴进报告比写十句“性能得到优化”都有说服力。造测试数据也别手工插。写个脚本批量生成注意两点选课记录要随机分布到各个教学班不然所有数据挤在一个班里索引效果根本测不出来批量插入要分批提交一次插十万行容易把日志撑爆。import random import mysql.connector conn mysql.connector.connect(hostlocalhost, databasecourse_selection, userapp, password***, autocommitFalse) cur conn.cursor() classes list(range(1, 201)) # 假设有 200 个教学班 students [f2023{i:08d} for i in range(1, 2001)] batch [] for sno in students: for class_id in random.sample(classes, 8): # 每人随机选 8 门 batch.append((sno, class_id, 1)) if len(batch) 1000: # 每 1000 条提交一次 cur.executemany( INSERT IGNORE INTO enrollment (sno, class_id, status) VALUES (%s, %s, %s), batch) conn.commit() batch.clear() conn.commit() cur.close() conn.close()INSERT IGNORE是为了跳过撞唯一键的重复数据避免脚本跑到一半中断autocommitFalse配合分批提交是为了让一万六千条记录在可接受的时间内写完同时不给回滚日志太大压力。答辩前我会自己跑三项检查一是拿两个并发线程各选同一个只剩一个名额的班看成功数是不是严格等于 1二是把所有外键约束逐个试一遍确认插非法数据会被挡回来三是把报告里出现的每条 SQL 都在干净库上重跑一次确保没有依赖本地残留数据。这三项都过基本不会被问倒。我自己的习惯是写完 DDL 先别急着写应用代码拿几十条手工构造的脏数据往库里灌一遍看约束能不能挡住。挡不住的地方就是我们血泪经验里最值钱的部分——因为答辩老师问的往往就是这些挡不住的地方。希望帮到你。本文还有配套的精品资源点击获取