简介一份面向高校计算机专业毕业设计的JSP智能排课系统资源包适配需要完成课程设计、毕业论文或答辩展示的学生与开发者。系统围绕《基于JSP的高校智能排课系统设计》展开覆盖用户管理、课程管理、教师管理、教室管理、排课算法、报表查询等核心模块涉及JSP技术、MVC架构、数据库设计以及贪心或回溯等排课优化思路可帮助读者贯通JSP开发与系统设计的完整流程。压缩包约11.07MB主体为源代码和论文文档并配套开题报告与答辩材料能支撑从代码阅读到论文撰写的全阶段参考。已有350人学习下载对准备毕业设计或希望提升Java Web实战能力的学习者具有较高参考价值。1. 高校智能排课系统到底难在哪JSP技术栈下的现实问题每年毕业季都会看到一批“JSP高校智能排课系统设计”这个题目在流传源代码和论文一起打包。它不是简单的增删改查真正的难点在于“智能”两个字几百门课程、几十个教室、上百位教师要在满足时间、地点、容量、班级这些约束的条件下自动排出一份没冲突的课表。我在帮人调试这套代码时最深的感受是数据库表结构如果设计歪了后面算法怎么改都救不回来算法参数如果拍脑袋设跑一晚上也出不了一份能用的课表。这篇笔记就用JSPServletMySQL这套经典组合从建模、实现到排错完整过一遍让新手能照着做让做过的人知道坑在哪里。2. 排课系统的核心模型时间、教室、班级与教师四维约束2.1 先想清楚约束关系再动手建表排课本质是一个约束满足问题。手工排课时教务员的脑子里同时装着四条规则同一个班级不能在同一时间上两门课同一个教师不能在同一时间出现在两个教室同一个教室不能在同一时间被两个班级占用教室容量还得装下选课人数。这就是四条硬约束任何一条被打破课表就是废纸。另外还有软约束比如教师希望上午不排课、某门课需要连上两节这些在学术上叫偏好在代码里就是惩罚项。在JSP项目里很多人一上来就写算法结果越写越乱。我一般会先花半天把数据库里四张表的关联关系理清楚教师、班级、课程、教室再加上排课结果表。排课结果表里的三个字段——课程、时间片、教室——就是算法要输出的答案而班级和教师通过课程表间接关联。表结构想通了算法只是在填一张合法的表而已。2.2 数据库表设计五张表搞定核心数据为了演示我用最简化的业务模型来建表teacher存教师classroom存教室course存课程里面直接写teacher_id和class_idtime_slot存每周的时间片schedule_result存最终排课结果。下面是MySQL建表语句-- 教师表 CREATE TABLE teacher ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, department VARCHAR(50), max_hours_per_week INT DEFAULT 20 -- 一周最多排课时数 ); -- 教室表 CREATE TABLE classroom ( id INT PRIMARY KEY AUTO_INCREMENT, code VARCHAR(20) NOT NULL, -- 如 A101 capacity INT NOT NULL, -- 座位数 type VARCHAR(20) DEFAULT 普通, -- 普通/多媒体/机房/实验室 is_available TINYINT DEFAULT 1 -- 是否可用排课时只取可用教室 ); -- 班级表 CREATE TABLE class ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, student_count INT NOT NULL -- 班级人数用于教室容量校验 ); -- 课程表 CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, teacher_id INT, class_id INT, hours_per_week INT DEFAULT 2, -- 每周需要排几个时间片 weeks INT DEFAULT 16, -- 总周数 student_count INT DEFAULT 30, -- 选课人数也可以从班级表冗余 need_room_type VARCHAR(20) DEFAULT 普通, is_required TINYINT DEFAULT 1, -- 是否必修将来可以算优先级 FOREIGN KEY (teacher_id) REFERENCES teacher(id), FOREIGN KEY (class_id) REFERENCES class(id) ); -- 时间片字典weekday slot每周循环 CREATE TABLE time_slot ( id INT PRIMARY KEY AUTO_INCREMENT, weekday VARCHAR(10) NOT NULL, -- Mon 到 Fri可扩展 Sat slot INT NOT NULL, -- 第几大节1~5 分别代表早一、早二、下午一、下午二、晚上 UNIQUE KEY (weekday, slot) ); -- 排课结果表 CREATE TABLE schedule_result ( id INT PRIMARY KEY AUTO_INCREMENT, course_id INT, teacher_id INT, class_id INT, classroom_id INT, time_slot_id INT, semester VARCHAR(20) DEFAULT 2024-2025-1, FOREIGN KEY (course_id) REFERENCES course(id), FOREIGN KEY (teacher_id) REFERENCES teacher(id), FOREIGN KEY (class_id) REFERENCES class(id), FOREIGN KEY (classroom_id) REFERENCES classroom(id), FOREIGN KEY (time_slot_id) REFERENCES time_slot(id), UNIQUE KEY uniq_room_time (classroom_id, time_slot_id), UNIQUE KEY uniq_teacher_time (teacher_id, time_slot_id), UNIQUE KEY uniq_class_time (class_id, time_slot_id) );这段SQL里最关键的是schedule_result上的三个唯一键同一教室同一时间只能排一门课同一教师同一时间只能排一门课同一班级同一时间只能排一门课。这三条唯一键从数据库层面挡住了硬约束冲突算法里万一某轮交叉或变异产生了非法个体写入时会被SQL直接拒绝至少不会污染最终结果。唯一要注意的是这三个唯一键要求schedule_result表里同时存teacher_id和class_id它们其实可以从course表冗余过来这是为了查询方便代价是写入时要多维护两个字段。参数说明一下hours_per_week这个值很关键它决定一个课程每周要占用几个不同的时间片而不仅仅是排一次就行。很多毕设翻车就翻在这里课程数据结构里根本没有这个字段算法只给每门课排了一个时间片最后课表看起来一周只上了一次课。max_hours_per_week是教师每周上限属于软约束可以不放进表里但在适应度函数里建议加一个超时惩罚。2.3 时间片建模为什么用“星期节次”而不是直接存日期高校课表是以周为周期循环的同一门课每周在固定的时间出现在课表上。所以时间片不应该用具体日期而应该用“星期几 第几大节”。国内大多数高校把一天分为五个大节上午1-2节、3-4节下午5-6节、7-8节晚上9-10节。一周五天就是25个时间片。如果学校周六也有课就加到26个。用这个模型时间片表可以用下面的SQL快速生成INSERT INTO time_slot (weekday, slot) SELECT w.wd, s.sl FROM ( SELECT Mon AS wd UNION SELECT Tue UNION SELECT Wed UNION SELECT Thu UNION SELECT Fri ) w CROSS JOIN ( SELECT 1 AS sl UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 ) s;这段SQL用了两个子查询做笛卡尔积5天乘5节正好25行。如果你想支持“单双周”之类的复杂规则可以在time_slot表里加一个week_type字段0代表每周都排1代表单周2代表双周。不过毕设阶段建议先不加把基础周期排好扩展留给后面。时间片id的生成顺序对算法很友好从1到25依次对应周一第一节到周五第五节。算法里可以用(time_slot_id - 1) / 5算出星期几用(time_slot_id - 1) % 5算出第几节避免来回查表。值得注意的是教室容量校验要放在算法生成初始种群时做而不是等到适应度函数里去兜底。初始种群里的每个基因如果随机分配教室时不考虑容量后续适应度函数虽然能惩罚它但惩罚只体现在数值上基因本身依然可能是非法的——比如一个30人的班被塞进20人的教室。所以在编码时最好从可用教室列表里按容量过滤后再随机选。这个习惯能让后面的迭代收敛快很多。3. 用JSPServlet实现智能排课从建库到核心算法落地3.1 三层架构与项目目录让人一眼看懂你的毕设代码JSP项目虽然老但分层做清楚了一样能打。我见过太多把Java代码直接写进JSP的毕设数据库连接、SQL、页面渲染全挤在一个文件里运行没问题答辩一被问就露馅。经典做法是entity放实体类dao放数据库操作servlet做控制层JSP只管展示算法单独放一个包。下面是一个常见的目录结构src/ com/edu/schedule/ entity/ -- Teacher, Course, Classroom, ScheduleResult dao/ -- CourseDao, ClassroomDao, ScheduleDao util/ -- DBUtil algorithm/ -- GAScheduler, Chromosome, FitnessCalculator servlet/ -- ScheduleServlet, InitDataServlet web/ WEB-INF/ web.xml index.jsp -- 参数配置页 schedule_list.jsp -- 结果展示页 lib/ mysql-connector-java-5.1.49.jar这个结构里JSP页面放在web根目录Java源码交给IDE编译后输出到WEB-INF/classes。传统JSP项目打包最终是一个war文件放进Tomcat的webapps目录下自动解压。现在用IDEA的Artifacts功能可以一键打包war但如果你的毕业设计是用Eclipse老办法是右键项目Export成war。两种方式都能用核心是把WEB-INF/lib下的MySQL驱动带上不然部署到服务器上就会报ClassNotFoundException。3.2 数据库连接与资源释放一个最小可用的DBUtilJSP里最基础的操作就是JDBC。为了不让每个DAO重复写获取连接和关闭资源的逻辑我会抽出一个DBUtil。下面是一个最小实现package com.edu.schedule.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/schedule_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD root; static { try { Class.forName(com.mysql.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(ResultSet rs, PreparedStatement ps, Connection conn) { try { if (rs ! null) rs.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (ps ! null) ps.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (conn ! null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }逻辑说明static块在类加载时注册驱动getConnection负责建立连接close方法按ResultSet、PreparedStatement、Connection的逆序关闭资源。参数说明URL里的characterEncodingutf8是为了让中文不乱码useSSLfalse在MySQL 5.7和8.0下都能避免警告serverTimezoneAsia/Shanghai是MySQL 8.0的必填项。USER和PASSWORD写成常量只是演示真正的项目应该读配置文件。另外注意如果你用的是MySQL 8.0以上驱动类名要改成com.mysql.cj.jdbc.Driver否则会报找不到驱动。3.3 从数据库加载课程与教室排课算法的输入准备算法不管你怎么存数据它只认Java对象。所以第一步是把课程、教师、班级、教室查出来塞进List。以课程为例CourseDao里的查询方法长这样public ListCourse findAllCourses() { ListCourse list new ArrayList(); String sql SELECT c.id, c.name, c.teacher_id, c.class_id, c.hours_per_week, c.student_count, c.need_room_type, t.name AS teacher_name, cl.name AS class_name FROM course c JOIN teacher t ON c.teacher_id t.id JOIN class cl ON c.class_id cl.id; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { Course course new Course(); course.setId(rs.getInt(id)); course.setName(rs.getString(name)); course.setTeacherId(rs.getInt(teacher_id)); course.setClassId(rs.getInt(class_id)); course.setHoursPerWeek(rs.getInt(hours_per_week)); course.setStudentCount(rs.getInt(student_count)); course.setNeedRoomType(rs.getString(need_room_type)); course.setTeacherName(rs.getString(teacher_name)); course.setClassName(rs.getString(class_name)); list.add(course); } } catch (SQLException e) { e.printStackTrace(); } return list; }逻辑说明这条SQL把course表和teacher表、class表做一个连接查询一次性把排课算法需要的教师名、班级名都带出来省得算法运行时反复查库。try-with-resources写法在Java 7以上可用它会在退出代码块时自动关闭PreparedStatement和ResultSet但Connection还是建议放进DBUtil.close里统一处理。参数说明student_count在表里放在了course表如果你建模时放在了class表把SQL里的c.student_count改成cl.student_count即可。教室DAO的查询相对简单查所有is_available1的教室按容量排序。算法里需要按课程需要的人数和教室类型先过滤出一个子列表再随机选。3.4 核心排课Servlet接收参数并触发算法用户从前端页面提交种群大小、交叉率、变异率、迭代次数这些参数Servlet收到后组装成一个调度器运行算法再把结果落库。代码骨架WebServlet(/schedule/run) public class ScheduleServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { req.setCharacterEncoding(utf-8); resp.setContentType(text/html;charsetutf-8); int populationSize Integer.parseInt(req.getParameter(populationSize)); int maxIterations Integer.parseInt(req.getParameter(maxIterations)); double crossoverRate Double.parseDouble(req.getParameter(crossoverRate)); double mutationRate Double.parseDouble(req.getParameter(mutationRate)); GAScheduler scheduler new GAScheduler(populationSize, crossoverRate, mutationRate); ListCourse allCourses new CourseDao().findAllCourses(); ListClassroom allRooms new ClassroomDao().findAvailableRooms(); ListScheduleResult results scheduler.run(allCourses, allRooms, maxIterations); boolean saved ScheduleDao.saveAll(results); resp.getWriter().write(saved ? success : failed); } }逻辑说明这里把四个关键参数直接从request里取出来交给GAScheduler。DAO层在每次请求时都查库对毕设系统来说没啥性能问题。参数说明populationSize建议范围50到200太大虽然更稳但每一代计算量也大maxIterations一般500到2000crossoverRate在0.7到0.9之间mutationRate在0.05到0.2之间。这四个参数跟课程规模有直接关系如果你的课程超过200门种群和迭代次数都要往上走。这一层还有一个容易被忽略的点当算法跑完ScheduleDao.saveAll时如果结果里有重复冲突数据库的唯一键会直接抛异常导致整批插入失败。所以保存之前最好在内存里再做一次冲突检查把非法结果剔掉。否则算法跑完一提交就报错用户看到的不是课表而是500页面。4. 遗传算法在排课中的应用编码、适应度函数与关键参数4.1 为什么选遗传算法贪心解不干净回溯太慢排课问题是个典型的NP难问题。最简单的贪心思路是按课程优先级依次安排每个课程找第一个没有冲突的时间片和教室。优点是快缺点是很容易把后面的课程逼进死胡同因为没有全局视角。回溯算法理论上能找到最优解但课程一多搜索空间是天文数字几十门课就跑不动了。遗传算法在毕设里是最合适的选择实现不算太复杂效果能直观看到适应度曲线收敛而且答辩老师几乎都会追问“为什么用遗传算法”你一句话就能回答它不追求全局最优而是在可接受时间内找到近似最优解符合排课的实际需求。配合精英保留策略每一代的最优个体不会丢失最后输出的一定是这轮迭代里最好的课表。4.2 编码方式用数组表示每个课程的时间片和教室遗传算法的第一步是把解表示成染色体。我通常用一个一维int数组每两个基因位代表一门课第i门课的时间片id放在genes[2i]教室id放在genes[2i1]。这样设计的好处是交叉和变异操作都很好写只需要按课程为单位交换或修改基因块。public class Chromosome { private int[] genes; // 长度 课程数 * 2 private int fitness; public Chromosome(int courseCount) { this.genes new int[courseCount * 2]; this.fitness -1; } public int getTimeSlot(int courseIndex) { return genes[courseIndex * 2]; } public int getClassroom(int courseIndex) { return genes[courseIndex * 2 1]; } public void setGene(int courseIndex, int timeSlotId, int classroomId) { genes[courseIndex * 2] timeSlotId; genes[courseIndex * 2 1] classroomId; } public int[] getGenes() { return genes; } public int getFitness() { return fitness; } public void setFitness(int fitness) { this.fitness fitness; } }参数说明courseCount是课程总数基因数组长度是它的两倍。初始化染色体时对每个课程从合法的教室列表容量够、类型匹配里随机选一个教室时间片也从所有时间片里随机选但要避开同一门课已经占用的时间片——如果一门课每周要上两节hours_per_week2它在染色体里应该占两个课程位或者你用二维数组存储反正要保证同一个courseId的两个基因块时间片不同。4.3 适应度函数把冲突变成数字适应度函数是遗传算法的方向盘。排课里的硬约束冲突需要被罚得很重否则算法会把时间花在优化一些无关紧要的地方。我习惯用负的冲突数作为适应度冲突越多分越低0分表示完全没冲突但实际很难达到。下面是一个简化版public int calculateFitness(Chromosome chromosome, ListCourse courses, ListClassroom classrooms) { int conflicts 0; MapInteger, ListInteger teacherAtTime new HashMap(); MapInteger, ListInteger classAtTime new HashMap(); MapInteger, ListInteger roomAtTime new HashMap(); for (int i 0; i courses.size(); i) { Course course courses.get(i); int timeSlot chromosome.getTimeSlot(i); int classroomId chromosome.getClassroom(i); int teacherId course.getTeacherId(); int classId course.getClassId(); if (teacherAtTime.computeIfAbsent(timeSlot, k - new ArrayList()).contains(teacherId)) { conflicts 10; } if (classAtTime.computeIfAbsent(timeSlot, k - new ArrayList()).contains(classId)) { conflicts 10; } if (roomAtTime.computeIfAbsent(timeSlot, k - new ArrayList()).contains(classroomId)) { conflicts 10; } Classroom room findClassroomById(classrooms, classroomId); if (room.getCapacity() course.getStudentCount()) { conflicts 5; } if (!room.getType().equals(course.getNeedRoomType())) { conflicts 2; } teacherAtTime.get(timeSlot).add(teacherId); classAtTime.get(timeSlot).add(classId); roomAtTime.get(timeSlot).add(classroomId); } return -conflicts; }逻辑说明三个Map分别记录每个时间片上已经排了哪些教师、班级、教室。遍历时先查再存冲突数按权重累加教师/班级/教室冲突是最严重的各加10容量不足加5教室类型不匹配加2。返回负的冲突数适应度函数数值越大越好。参数说明权重值10、5、2是经验值如果你想优先保证教师不冲突可以把教师冲突调到20如果不关心教室类型可以直接把类型惩罚去掉。4.4 关键参数怎么调种群、交叉率、变异率遗传算法的参数决定了它最终是收敛到一个好解还是困在局部最优里。我可以给出一个参考值的表格但别忘了参数要跟随你的课程规模调整。课程少50门以内时种群设60就够课程多200门以上建议至少150。参数建议范围说明populationSize50~200种群越大搜索越广但每一代耗时线性增加maxIterations500~2000看适应度曲线的拐点不平了就可以提前停crossoverRate0.7~0.9交叉概率太高会破坏好个体太低又探索不足mutationRate0.05~0.2变异率低于0.05容易早熟高于0.2会导致结果飘忽不定eliteCount1~5每一代保留前几名精英直接进入下一代防止最优解丢失有经验的开发者会用“适应度曲线”来判断参数每次迭代记录当前最优适应度画成折线图。如果10代都没变化可能是变异率太低或者种群多样性不足这个时候把mutationRate调高一档再试。如果曲线一直在震荡下不来说明交叉率太高好结构被频繁拆散。毕设答辩时能用这条曲线来佐证你的系统是“智能”的比贴一张课表更有说服力。5. 排课系统避坑指南5个常见问题与排查办法5.1 教室被重复安排算法合法数据库报错现象排课结果保存时抛出Duplicate entry看日志是UNIQUE KEY uniq_room_time冲突同一教室同一时间出现了两个课程。原因遗传算法的交叉和变异操作只保证了时间片基因合法但没有每次都检查教室是否已被占用。适应度函数虽然会惩罚但在一代内的个体之间仍然可能出现教室冲突的染色体。解决在生成初始种群时为每个课程建立一个“已被占用的教室ID集合”随机选教室时从可用集合里除掉已被占用的。更简单的方法是在保存之前写一个冲突检测方法把有冲突的结果剔除掉。我一般会在ScheduleDao.saveAll里捕获到SQLIntegrityConstraintViolationException时先整批回滚再逐条插入跳过冲突行同时打印出冲突的课程编号方便排查。5.2 JSP页面中文乱码显示成问号或乱码现象index.jsp上用GBK写的中文标题浏览器显示成乱码或者问号。原因JSP页面本身没指定字符编码Tomcat默认按ISO-8859-1读而数据库里存的是UTF-8两边不一致就乱。解决在JSP页面第一行加上% page contentTypetext/html;charsetUTF-8 languagejava %。同时Servlet里接收请求参数时加req.setCharacterEncoding(UTF-8)JDBC URL里带上characterEncodingutf8。三处都统一了基本不会再乱。注意如果MySQL建库时用了latin1需要重建库为utf8mb4否则乱码是治不好的。5.3 部署到Tomcat后找不到MySQL驱动现象本机IDE里运行正常打成war放到Linux服务器的Tomcat里启动后访问页面报ClassNotFoundException: com.mysql.jdbc.Driver。原因war包里WEB-INF/lib下没有mysql驱动jar。IDE运行时用classpath自动带上了jar但打包war时漏了。解决确认你的mysql-connector-java.jar已经放到web/WEB-INF/lib目录下重新打war再部署。如果是Maven项目检查dependency里有没有 provided 那个会让jar不进war包去掉即可。另外MySQL 8以上要把驱动类名改成com.mysql.cj.jdbc.Driver。5.4 适应度曲线长时间不动算法陷入早熟现象跑了几百代适应度一直停在-15左右上下浮动不超过1。原因变异率太低导致种群里的基因逐渐同质化交叉怎么换都换不出新花样。或者是初始种群生成时用了固定顺序多样性天生不足。解决把变异率从0.05提高到0.15同时引入精英保留策略——每次迭代保留前2个最优个体直接复制到下一代这样即使变异和交叉破坏了部分个体最优解也不会丢。如果还是不动重新初始化种群确保初始时间片和教室的分配是纯随机的而不是按课程顺序循环分配。5.5 一门课一周只排了一次hours_per_week被忽略现象课表生成后每周只显示一门课一次但业务要求是每周上两次比如周二1-2节和周四3-4节。原因许多排课系统的编码阶段根本没把hours_per_week放进约束。课程表里有这字段但算法循环里只处理了一遍课程列表一门课就占一个基因块一个时间片自然只排一次。解决把每门课按hours_per_week展开成多个“排课单元”例如课程id3每周上2次就在待排列表里生成两条排课记录共享课程id、教师、班级、教室类型但时间片要强制不同。适应度函数计算时也要按课程去重避免同一门课的两个时间片被当成不同课程。我记得第一次写排课代码时就踩了这个坑后来在DAO查询里做了一次展开才算真正解决。6. 验证与进阶用真实课表数据检验你的排课结果系统跑完别急着截图写论文。先拿它跟学校教务处的真实课表做一次对比看看你的结果能不能用。最简单的方法是写个SQL统计冲突-- 统计同一个教室同一个时间片出现了几次冲突检查 SELECT classroom_id, time_slot_id, COUNT(*) AS cnt FROM schedule_result WHERE semester 2024-2025-1 GROUP BY classroom_id, time_slot_id HAVING cnt 1; -- 统计同一教师同一时间片的重复 SELECT teacher_id, time_slot_id, COUNT(*) AS cnt FROM schedule_result GROUP BY teacher_id, time_slot_id HAVING cnt 1;把这两条SQL跑一遍如果返回空结果说明硬约束基本满足了。接着再人工抽查十门课教室容量是否足够时间是否在合理范围内。进阶方向可以加一个软约束把“教师不希望晚上排课”这类偏好变成惩罚项。实现上很简单在适应度函数里如果时间片属于晚上就给特定教师加上额外惩罚。这样算法会在满足硬约束的前提下尽量让课表更接近人手排出来的效果。我在自己调试这套系统时最大的教训是一定要先跑一个10门课的小数据集把算法调通再上全量真实数据。小数据集能让你用肉眼验证每一个排课单元是否合理全量数据一上来就是几百门课一处小bug全被淹没在冲突里很难定位。参数调优也是同样逻辑先用小集找到最合适的交叉和变异率再搬到大数据集上能省好几个晚上的迭代时间。希望这篇笔记能让你少踩几个坑顺利把系统做出来、论文写下去祝你答辩顺利。希望帮到你。本文还有配套的精品资源点击获取