简介:一份面向软件工程专业数据库课程的宾馆房间管理系统课程设计报告,结合SQL Server 2000与C#.NET,完整走完课程设计全过程。内容按教材式结构展开:第1章明确课程设计目的、环境与参考资料;第2章为核心数据库设计,涵盖需求分析、业务流程图与数据流程图绘制、数据字典定义、概念与逻辑设计、物理设计、数据库实现,再到应用程序的概要设计与C#.NET程序实现,涉及登录验证、客房种类管理、客房信息管理、客户入住登记、客户查询与结账等功能。第3章为课程设计总结,文末附参考文献。报告完整展示了宾馆客房管理信息系统的数据库建模思路和编码衔接方式,适合软件工程、计算机等相关专业的学生参考课程设计流程、撰写报告或准备答辩。包内含1个doc文档,大小约293KB;目前已有139人学习,可作为学习数据库课程设计时的典型实例。
1. 为什么这个课设题库里的老题,反而不容易做好
做 SQL 数据库课程设计,十个题目里有八个绕不开宾馆房间管理系统。很多人觉得它简单:无非是房间表、顾客表、订单表,再配几个增删改查页面,一星期就能交差,拿到成绩却只有及格线。反直觉的地方在于,这个系统真正的难点根本不在增删改查,而在“房间状态”的一致性上——一间房从空闲到预订、从入住到退房,中间任何一个环节漏更新一张表,报表就会对不上账,演示现场就会被老师一句“你这数据是怎么变的”问住。
这篇笔记就围绕这套题目展开,按课设文档的推进顺序讲:先说业务规则和 ER 模型怎么定,再给可直接照抄的 T-SQL 建库建表脚本,然后实现入住、退房、日志和报表的存储过程与视图,最后聊课设验收时最常见的一组翻车场景。适合正在做课设的学生,也适合刚接触数据库开发、想找一套规范写法作参照的初级工程师。下面进入正题。
2. 从状态机到 ER 模型:先画对状态流转,再动手建表
2.1 房间状态是整张数据库的重心:先画状态机
宾馆管理系统业务上最核心的实体不是顾客,而是“房间的状态变化”。一间房从营业开始,通常会经历这么几个状态:空闲、已预订、占用、清理中、维修中。
这五个状态之间是有严格迁移规则的:空闲房可以被预订,变成已预订;已预订房在顾客到达后办理入住,变成占用;占用房办理退房后变成清理中;清理完成回到空闲;空闲房也可以直接进入维修中,维修完成再回到空闲。反过来,占用房不能被直接预订,维修中的房间也不能办理入住——这些规则不是靠前端按钮控制的,而是要在数据库的表结构、约束和存储过程里兜住的。
我的习惯是先把状态迁移画出草图,确认每个箭头都有对应的业务动作,再开始设计表。因为状态迁决定了两件事:room 表上状态字段的取值范围,以及订单表、入住记录表在什么时机被写入。很多课设翻车,都是因为状态字段类型和取值没定好,最后业务代码里到处写魔法数字,报表统计时还要靠猜。
2.2 最小实体集:六张表怎么连成一张网
围绕状态机,最少需要六张表:房型表、房间表、顾客表、预订订单表、入住记录表、账单表。如果还想展示触发器和日志,再加一张操作日志表,凑成七张。它们的关联关系是:
| 表名 | 记录什么 | 与谁关联 | 关系基数 |
|---|---|---|---|
| room_type | 房型与基准价格 | room | 1 : N |
| room | 房间号、房型、状态 | room_type, checkin_detail | 1 : N |
| customer | 顾客身份与联系方式 | reservation | 1 : N |
| reservation | 预订单 | customer, room | N : 1 |
| checkin_detail | 每次实际入住记录 | room, customer | N : 1 |
| bill | 账单明细 | checkin_detail | 1 : N |
预订订单和入住记录为什么要分开?因为现实里存在“预订后未入住”和“没预订直接入住”两种情况。把两张表合并成一张订单表,退房结账逻辑就会被各种为空的状态干扰。拆开之后,reservation 管预约承诺,checkin_detail 管实际占用,两张表通过 room_id 和 customer_id 关联,边界非常清楚。
2.3 范式的边界:价格快照属于有意的冗余
做课设时,老师通常会在文档里要求按第三范式设计。但宾馆系统里有一处必须“违反”第三范式:reservation 表里要冗余一份当时的房间价格,甚至把顾客姓名也冗余进来。原因很简单,房型价格会调整,顾客可能改名,但订单成交时的价格和顾客身份是历史事实。如果不做快照,三个月后再查这张订单,只能连表到房型表取最新价格,对出来的财务数据全是错的。
这个冗余不是设计失误,而是业务上的“历史快照”。在课程设计文档里,主动写一句“此处有意冗余以保证订单价格的历史可追溯性”,比被老师提问时支支吾吾要体面得多。ER 图里把这条关系标注清楚,这一步做好了,后面建表才有底气。
3. 建库与建表:把约束直接写进 DDL,别留给业务代码
3.1 建库脚本:文件路径、文件增长和排序规则一次到位
拿到 SQL Server 后,先不要急着 CREATE TABLE,第一件事是把数据库文件放到非系统盘,并显式设置增长参数。我一般用这套建库脚本:
CREATE DATABASE HotelMS ON PRIMARY ( NAME = N'HotelMS', FILENAME = N'D:\Data\HotelMS.mdf', SIZE = 8MB, MAXSIZE = UNLIMITED, FILEGROWTH = 8MB ) LOG ON ( NAME = N'HotelMS_log', FILENAME = N'D:\Data\HotelMS_log.ldf', SIZE = 4MB, MAXSIZE = 2048MB, FILEGROWTH = 10% ) COLLATE Chinese_PRC_CI_AS;把文件放在数据盘而不是默认的 C 盘,是防止系统盘空间紧张导致数据库写入失败。FILEGROWTH 的设定有个细节:数据文件按固定 MB 增长,日志文件按百分比增长。日志按百分比是 SQL Server 的常见做法,因为日志增长与事务量相关,写死固定大小容易频繁触发自动增长,反而拖慢性能。COLLATE 使用 Chinese_PRC_CI_AS,这是简体中文、不区分大小写的排序规则,如果选错成英文排序规则,后续查询中文条件时会出现诡异的排序和匹配问题。
如果学校指定使用达梦或 GBase 这类国产数据库,建库语法和这里的参数含义基本一致,管理工具换一下就行,思维模型完全通用。
3.2 核心表 DDL:状态字段的取值范围由 CHECK 约束兜底
下面是五张核心业务表的建表脚本。注意我使用的字段命名风格和约束方式,这是课设文档里最值得模仿的部分:
CREATE TABLE dbo.room_type ( type_id INT IDENTITY(1,1) PRIMARY KEY, type_name NVARCHAR(40) NOT NULL UNIQUE, base_price DECIMAL(10,2) NOT NULL CHECK (base_price > 0) ); CREATE TABLE dbo.room ( room_id INT IDENTITY(1,1) PRIMARY KEY, room_no CHAR(4) NOT NULL UNIQUE, type_id INT NOT NULL REFERENCES dbo.room_type(type_id), room_status TINYINT NOT NULL DEFAULT 0, remark NVARCHAR(200) NULL, CONSTRAINT ck_room_status CHECK (room_status IN (0, 1, 2, 3, 4)) ); CREATE TABLE dbo.customer ( customer_id INT IDENTITY(1,1) PRIMARY KEY, customer_name NVARCHAR(40) NOT NULL, id_card CHAR(18) NOT NULL UNIQUE, phone VARCHAR(20) NULL, CONSTRAINT ck_id_card CHECK (id_card LIKE '[0-9][0-9]%') ); CREATE TABLE dbo.reservation ( reservation_id INT IDENTITY(1,1) PRIMARY KEY, room_id INT NOT NULL REFERENCES dbo.room(room_id), customer_id INT NOT NULL REFERENCES dbo.customer(customer_id), price_snapshot DECIMAL(10,2) NOT NULL, arrival_date DATE NOT NULL, leave_date DATE NOT NULL, order_status TINYINT NOT NULL DEFAULT 0, create_time DATETIME2(0) NOT NULL DEFAULT SYSDATETIME(), CONSTRAINT ck_order_date CHECK (leave_date > arrival_date), CONSTRAINT ck_order_status CHECK (order_status IN (0, 1, 2)) ); CREATE TABLE dbo.checkin_detail ( checkin_id INT IDENTITY(1,1) PRIMARY KEY, room_id INT NOT NULL REFERENCES dbo.room(room_id), customer_id INT NOT NULL REFERENCES dbo.customer(customer_id), reservation_id INT NULL REFERENCES dbo.reservation(reservation_id), checkin_time DATETIME2(0) NOT NULL DEFAULT SYSDATETIME(), checkout_time DATETIME2(0) NULL );参数说明:room_status 用 TINYINT 而不是 CHAR(10),因为状态在程序里只会做等值判断,数值型占用空间小且索引效率高。0 到 4 分别对应空闲、已预订、占用、清理中、维修中,这个映射关系要在设计文档里写清楚。customer 表的 id_card 用 CHAR(18),因为身份证长度固定,用 CHAR 比 VARCHAR 更省空间且符合语义。CHECK 约束里写 LIKE 模式只是为了演示格式校验,实际项目里身份证还有校验位算法,课设做到位可以补充说明,但不必过度设计。
checkin_detail 表的 reservation_id 允许 NULL,这是为了支持“无预订直接入住”的场景。如果把它设为 NOT NULL,店外客就无法入住,这个设计会卡住真实业务。
3.3 索引策略:报表查询还没写,索引先建出来
很多课设脚本只有主键和外键,等到写报表查询时才发现全表扫描慢得离谱。我一般在建表完成后直接加三条索引:
CREATE INDEX ix_reservation_arrival ON dbo.reservation(arrival_date, room_id); CREATE INDEX ix_checkin_room_time ON dbo.checkin_detail(room_id, checkin_time); CREATE UNIQUE INDEX ux_customer_idcard ON dbo.customer(id_card);第一条索引支撑“按日期查当日预订”这类高频报表;第二条支撑“查某间房的历史入住记录”;第三条是唯一索引,身份证号本身有唯一性要求。这三条已经覆盖课设里 90% 的查询场景。不要为了显得专业给每一列都建索引,索引过多会拖慢 INSERT 和 UPDATE,房间入住退房这种高频操作反而变慢,老师问起来也解释不清楚。
4. 让业务自动流转:视图、存储过程与触发器
4.1 一个入住动作要更新四张表:为什么要用存储过程封装
入住是一个典型的多表联动动作:把 room 的状态从已预订改成占用,在 checkin_detail 插入一条入住记录,把 reservation 的订单状态改成已入住,再写一条操作日志。如果这些逻辑散落在前端代码里,等于把数据库的一致性交给客户端自觉,业务代码里任何一步遗漏,房间状态就会卡死。
用存储过程封装有两个直接好处:一是事务边界明确,要么全部成功,要么全部回滚;二是演示和答辩时,老师让你现场跑一遍入住流程,你在查询分析器里调一个存储过程就能讲清楚“这一条命令背后做了什么”,说服力比贴十行前端代码强得多。
4.2 入住和退房两个存储过程:事务、锁与错误处理
这是整套系统里我建议优先完成的核心过程。入住过程的完整写法如下:
CREATE OR ALTER PROCEDURE dbo.usp_CheckIn @RoomID INT, @CustomerID INT, @ReservationID INT = NULL, @ActualPrice DECIMAL(10,2) OUTPUT AS BEGIN SET NOCOUNT ON; SET XACT_ABORT ON; BEGIN TRAN; BEGIN TRY -- 锁住房间行,防止并发下同一间房被两次入住 SELECT @ActualPrice = rt.base_price FROM dbo.room r WITH (UPDLOCK, ROWLOCK) JOIN dbo.room_type rt ON r.type_id = rt.type_id WHERE r.room_id = @RoomID; IF @ActualPrice IS NULL BEGIN RAISERROR(N'房间不存在', 16, 1); RETURN; END; IF NOT EXISTS (SELECT 1 FROM dbo.room WHERE room_id = @RoomID AND room_status = 1) AND NOT EXISTS (SELECT 1 FROM dbo.room WHERE room_id = @RoomID AND room_status = 0) BEGIN RAISERROR(N'房间当前不可入住', 16, 1); RETURN; END; UPDATE dbo.room SET room_status = 2 WHERE room_id = @RoomID; INSERT INTO dbo.checkin_detail(room_id, customer_id, reservation_id) VALUES (@RoomID, @CustomerID, @ReservationID); IF @ReservationID IS NOT NULL UPDATE dbo.reservation SET order_status = 1 WHERE reservation_id = @ReservationID; COMMIT; END TRY BEGIN CATCH ROLLBACK; THROW; END CATCH; END;逻辑说明:第一步用 WITH (UPDLOCK, ROWLOCK) 锁住目标房间的行,这是防止并发下“一房两卖”的关键,没有这行锁,两个前台同时办理入住时房间状态可能被覆盖。后续检查房间状态时,我同时允许状态 0(空闲)和状态 1(已预订)办理入住,分别对应散客直接入住和预订客人到店入住。事务结束后通过输出参数 @ActualPrice 把实际价格返回给调用方,便于前端展示。
退房过程与之对称,注意的坑在于计算房价和更新房间状态必须放在同一个事务里:
CREATE OR ALTER PROCEDURE dbo.usp_CheckOut @CheckinID INT, @TotalPrice DECIMAL(10,2) OUTPUT AS BEGIN SET NOCOUNT ON; SET XACT_ABORT ON; DECLARE @RoomID INT; DECLARE @CheckinTime DATETIME2(0); BEGIN TRAN; BEGIN TRY SELECT @RoomID = room_id, @CheckinTime = checkin_time FROM dbo.checkin_detail WITH (UPDLOCK) WHERE checkin_id = @CheckinID AND checkout_time IS NULL; IF @RoomID IS NULL BEGIN RAISERROR(N'无效的入住记录', 16, 1); RETURN; END; SET @TotalPrice = DATEDIFF(HOUR, @CheckinTime, SYSDATETIME()) * 1.0 / 24 * (SELECT base_price FROM dbo.room_type rt JOIN dbo.room r ON r.type_id = rt.type_id WHERE r.room_id = @RoomID); UPDATE dbo.checkin_detail SET checkout_time = SYSDATETIME() WHERE checkin_id = @CheckinID; UPDATE dbo.room SET room_status = 3 WHERE room_id = @RoomID; COMMIT; END TRY BEGIN CATCH ROLLBACK; THROW; END CATCH; END;这里使用 DATEDIFF 按小时计费,再除以 24 换算成天数乘以每日房价,这是宾馆计费的常见口径。实际项目中房费计算往往还涉及钟点房、延时退房等规则,但课设做到按天计费已经能把事务、存储过程、约束这些考点全部覆盖。注意退房成功后房间状态直接改成 3(清理中),而不是回到 0(空闲),这一步是状态机设计里最容易漏的。
4.3 视图与窗口函数:当日占用报表和最新入住记录一次查出来
报表查询是课设文档里最能体现 SQL 功力的部分。一个“当前所有占用房间”的视图就可以写成这样:
CREATE OR ALTER VIEW dbo.v_CurrentOccupancy AS SELECT r.room_no, rt.type_name, c.customer_name, ci.checkin_time, DATEDIFF(DAY, ci.checkin_time, SYSDATETIME()) AS stay_days FROM dbo.checkin_detail ci JOIN dbo.room r ON r.room_id = ci.room_id JOIN dbo.room_type rt ON rt.type_id = r.type_id JOIN dbo.customer c ON c.customer_id = ci.customer_id WHERE ci.checkout_time IS NULL;这个视图直接对应前台“当前在住客人”的实时展示,是演示时的亮点功能。稍微进阶一点,如果需要“每个房间最近一次入住记录”,用 GROUP BY 很难表达“最近一条”,而 SQL Server 的窗口函数 ROW_NUMBER 就是为此设计的:
SELECT room_no, customer_name, checkin_time FROM ( SELECT r.room_no, c.customer_name, ci.checkin_time, ROW_NUMBER() OVER (PARTITION BY ci.room_id ORDER BY ci.checkin_time DESC) AS rn FROM dbo.checkin_detail ci JOIN dbo.room r ON r.room_id = ci.room_id JOIN dbo.customer c ON c.customer_id = ci.customer_id ) AS t WHERE t.rn = 1;窗口函数是近几年数据库课设的高频考点,在文档里写一段这样的查询作为“进阶实现”,比满篇 SELECT * 更让老师信服。这里先用内层查询给每个房间的入住记录按时间倒序编号,再取 rn = 1,语义非常清晰。需要注意 PARTITION BY 后面跟的是 ci.room_id,如果只写 r.room_no,因为 room_no 也是唯一的,结果相同,但语义上不严谨。
4.4 触发器:用一张日志表记录状态变更,但不要让它无限递归
存储过程处理了业务,触发器负责审计。我建议用一个 AFTER UPDATE 触发器记录 room 表状态变化,这是课设里最安全的触发器的用法:
CREATE TABLE dbo.room_status_log ( log_id INT IDENTITY(1,1) PRIMARY KEY, room_id INT NOT NULL, old_status TINYINT NOT NULL, new_status TINYINT NOT NULL, change_time DATETIME2(0) NOT NULL DEFAULT SYSDATETIME() ); CREATE OR ALTER TRIGGER dbo.trg_room_status_change ON dbo.room AFTER UPDATE AS BEGIN SET NOCOUNT ON; IF NOT EXISTS (SELECT 1 FROM inserted i JOIN deleted d ON i.room_id = d.room_id WHERE i.room_status <> d.room_status) RETURN; INSERT INTO dbo.room_status_log(room_id, old_status, new_status) SELECT d.room_id, d.room_status, i.room_status FROM inserted i JOIN deleted d ON i.room_id = d.room_id WHERE i.room_status <> d.room_status; END;触发器里最忌讳的操作是在触发器内再次 UPDATE 同一张表,这会造成触发器递归触发,直到达到嵌套层数上限报错。这里的实现只做 INSERT 审计日志,不去反向修改任何业务表,很安全。嵌套层级相关的服务器配置 RECURSIVE_TRIGGERS 保持默认关闭状态即可,不必动它。日志表的数据量会不断增长,课设演示期间体量很小,索引暂时不需要额外建。
5. 从设计到交付:课设验收最常见的六条踩坑记录
5.1 外键被删除时直接报错:没有理清引用关系就删主表
现象:想删掉一间房或一个房型,DELETE 语句报外键冲突,老师点了一下“为什么删不掉”,当场答不上来。
原因:room_type 表被 room 表的外键引用,checkin_detail 又引用 room。只要历史入住记录存在,房间行就和这些记录绑在一起,直接 DELETE 当然被拒,这是数据库引用完整性的正常保护。
解决:业务上房间不能物理删除,应该把 room 表加一个 is_active 标志位,做逻辑删除。房型表如果确实需要清理,必须先删除关联的房间,再删除房型,并且这个操作要在事务里执行。课设文档里写明“采用逻辑删除而非物理删除”,是标准答案。
5.2 触发器循环触发导致事务爆掉
现象:给 room 表写了一个 UPDATE 触发器,执行退房存储过程时,报表立刻报错,错误信息提示触发器嵌套超出最大层数。
原因:退房过程先 UPDATE room 设置状态,这触发日志触发器;如果日志触发器里顺手又 UPDATE 了 room 表,就形成 A 触发 B、B 又触发 A 的无限循环。
解决:触发器只做跨表写入,不要回写自身表;确有必要回写时,使用 IF UPDATE(column) 配合条件判断,让触发器只在特定列变化时才继续执行。最稳妥的做法是像 4.4 节那样,触发器只用 inserted 和 deleted 表做 INSERT 操作。
5.3 参数拼串导致 SQL 注入:演示当天被现场报错打脸
现象:登录或查询功能用字符串拼接 SQL 实现,输入一个带单引号的顾客姓名,页面报语法错误;如果输入的是恶意构造的语句,还可能直接绕过查询条件。
原因:前端传过来的值被原样拼进 SQL 字符串,等于把语法控制权交给了输入者。这是数据库开发里最典型的安全反面教材。
解决:存储过程内部使用参数化查询,外部调用也用 EXEC 或 ADO.NET 的 SqlParameter 传参,永远不使用拼接。课设文档的“安全性分析”一节把这个写进去,比写十行冠冕堂皇的“系统安全性高”要实在得多。
5.4 并发下同一间房被订两次:忘记加锁
现象:两张相同房间的入住记录同时存在,checkin_detail 里出现同一 room_id 的两条未退房记录。
原因:两个会话同时执行入住逻辑,都先在内存里读到房间状态为空闲,随后各自执行 INSERT 和 UPDATE,互相覆盖,数据库层面没有任何保护。
解决:入住存储过程里对房间行加 UPDLOCK 锁,如 4.2 节代码所示。这也是事务隔离级别解决不了的,因为问题出在“先读后写”之间的间隙。演示时可以用 SSMS 开两个查询窗口同时跑,证明第二次调用会被阻塞或报错,这本身就是课设答辩的一个亮点环节。
5.5 日期字段类型选错,当天退房怎么都算一天房费
现象:入住时间是当天的 23:50,退房是第二天的 00:20,房费被算成两天;或者入住时传入了带时间的值,查询“当日入住”时因时间干扰导致统计结果错误。
原因:checkin_detail 的时间用 DATE 类型存储,丢失了具体时刻;或者相反,预订订单的日期用了 DATETIME,导致按天分组统计时把跨天的数据错误归组。
解决:区分业务语义——预订日期用 DATE,入住退房时刻用 DATETIME2(0)。计算房费时按 DATEDIFF(HOUR) 换算,结果更合理;统计“今日入住”时使用 CONVERT(DATE, checkin_time) 之后再比较,或者直接按 checkin_time >= 当天零点 来过滤。
5.6 报告文档只有建表语句,没有运行说明:老师没法复现
现象:交上来的课设报告里贴了大量 CREATE TABLE,却没有数据库初始化顺序、测试数据脚本、账号说明,老师在自己电脑上根本跑不起来。
原因:文档只记录了设计结果,没记录运行环境和使用方法。对于以 .doc 形式提交的课程设计报告,运行说明和测试用例几乎和代码同等重要。
解决:在文档中固定包含三部分内容——创建数据库的顺序(先建库、再建表、再建索引、再写存储过程);一组可重复执行的测试数据脚本;至少五个测试场景(预订、入住、退房、报表、非法状态修改)的操作步骤和预期结果。做到这一步,报告的完成度远超平均水平。
6. 验收前的最后一次排演:演示数据、查询计划与答辩思路
课设答辩前一天的排演,比写代码更值得花时间。第一件事是检查演示数据:不要用 1、2、3 这种编号当顾客名和房间号,建 20 间房、12 个顾客,房间号用 101、102、201、202,顾客用真实感强的中文姓名,并让入住记录的时间跨度覆盖过去十天。这样报表视图一眼看去像真实系统,老师对数据可信度产生好感之后,提问也会更友好。
第二件事是验证存储过程是否走了索引。在 SSMS 里执行 EXEC dbo.usp_CheckIn 后,点击“显示估计的执行计划”看是否有表扫描。如果出现 Table Scan,说明 4.2 节建索引时漏了字段,补上即可。这一项也直接回应了“慢 SQL 优化”这个话题——课设数据的量很小,索引优化体现不出性能差异,但执行计划里能看到 Seek 和 Scan 的区别,这就足够在答辩时讲清楚索引的价值。
第三件事是准备一套固定的演示脚本,顺序建议这样:先打开 v_CurrentOccupancy 视图展示当前数据概览;然后调用 usp_CheckIn 演示从预订到入住的完整流程,特意展示房间状态从 1 变成 2;再调用 usp_CheckOut 演示退房和房费计算;最后查询 room_status_log 表,展示触发器自动生成的日志。这套顺序是顺着状态机走下来的,老师全程不需要打断你,就能看到系统的完整闭环。
最后讲一下 .doc 报告本身的排版细节。网上流传的课设模板大多要求粘贴代码截图,但在 Word 里直接插入文本代码并切换为等宽字体(如 Consolas)才是最稳妥的做法,一是文字可复制,二是打印出来清晰不糊。ER 图和数据流图务必用清晰的矢量格式导出,不要截图后压缩到模糊。所有代码按“库表结构—数据初始化—存储过程—视图触发器—测试用例”的顺序排列,这与本笔记的章节顺序一致,老师评审时按图索骥,印象分会稳很多。
我做过几年课设评审,见过太多实现得不错、却因为演示前半小时手改数据库导致现场翻车的例子。我自己也犯过这个错——把 room_status 手工改错之后忘记改回来,视频演示录了三遍才通过。从那以后我养成一个习惯:演示前不直接手改数据,所有状态变更一律走存储过程,最后再跑一遍验收脚本确认所有状态机分支都正常。希望这个习惯和这篇笔记能帮到你,把课设做成一门真正属于自己的拿得出手的数据库作品。
本文还有配套的精品资源,点击获取