简介:这是一套面向高校计算机及相关专业数据库课程设计场景的教务管理系统实践项目,基于MySQL与Java实现。内容涵盖需求分析、数据库设计、系统实现与测试环节,可作为课程设计报告蓝本或二次开发起点,适合正在学习数据库原理、Java编程的学生梳理开发全流程。资源包共28个文件,主体为9个Java源文件、对应class字节码、SQL建库脚本、配置与依赖jar包,还包含项目配置和说明文档,压缩后大小约4.45MB,轻量易用。已有2649人浏览学习。借助这份资料,可以借鉴教务管理中的E-R模型与表结构设计,快速还原SQL脚本并运行调试;通过阅读源码与设计报告,能掌握JDBC操作、界面实现和测试方法,在此基础上扩展功能模块或调整表关系,并加以完善,即可形成有个人特色的课程设计成果。
1. 教务管理系统这个课设,到底在逼你练什么
每个计算机相关专业的学生,大概率都会在大学某一年碰上数据库课程设计,而“教务管理系统”绝对是出场率最高的题目之一。很多初学者拿到这个题目第一反应是“又是一套增删改查”,但真动手写起来才发现,一个能跑通的教务系统,远不是几个页面拼在一起那么简单:学生表、教师表、课程表、选课表、成绩表之间的外键关系怎么设计才不混乱?一个学生选同一门课怎么防止重复?老师录入成绩时怎么保证分数范围合法?这些恰恰是数据库设计里最值钱的基本功。用 Java + MySQL 来做这套系统,几乎是你离“真实企业级开发”最近的一次模拟演练。
选这个组合的理由也很直白:MySQL 开源免费、部署难度低,是课设和中小型项目最常见的数据库选型;Java 生态成熟,JDBC、MyBatis、Spring Boot 各有各的玩法,无论你未来走后端开发还是数据方向,这套配合都必须玩明白。如果你正准备做这个课设,或者做了一半卡在“表结构总改、代码总报错”的状态,这篇文章就是给你准备的。我会从技术选型、数据库建模、Java 连接数据库到业务代码落地,把每一步的细节和坑都拆开讲清楚。
2. 技术栈选型:为什么固定是 Java + MySQL,以及能换的替代品
2.1 Java 与 MySQL 的黄金搭档逻辑
数据库课程设计选择 Java + MySQL,核心原因是这两个技术在高校教学和企业落地之间取得了最佳平衡。MySQL 是关系型数据库里最容易上手的一个,下载安装包、改个密码、执行几个 SQL 语句,半小时就能跑起来;而 Java 的语言特性和三层架构思维,恰好能让你把“界面层、业务层、数据层”的概念落到实际项目里。用 JDBC 写原生的数据库操作,你会理解一条 SQL 到底是怎么被发送到 MySQL 执行的;用 MyBatis 或 Spring Boot 做进阶,你又能体会连接池、事务管理这些生产环境必备的机制解决的是什么问题。
常见的替代方案也确实存在,比如 SQL Server + C#、Python + SQLite,或者前端用 Vue + 后端用 Go。但如果你是做课设,我强烈建议不要为了“显得前沿”去选冷门组合。原因很简单:课设的评审老师最熟悉的就是 Java + MySQL 这套链路,你遇到任何问题,随手搜到的教程、实验室同学的踩坑记录、甚至老师给的参考代码,全是围绕这套组合的。选冷门技术意味着你不仅要搞定业务,还要独自扛下环境配置的玄学问题,这会极大消耗你做正事的精力。
2.2 核心依赖与开发环境的具体版本组合
如果你用的是传统 Servlet + JSP 方式,环境组合相对轻量:JDK 8 或 11,MySQL 5.7 或 8.0,Tomcat 9,再加一个 IDEA 或 Eclipse。如果你选择 Spring Boot 方式,建议 JDK 8 以上,Spring Boot 2.x 配 MySQL 8.0 是目前课设里兼容性最好的组合,不容易出现驱动类找不到的翻车问题。
驱动选择上,MySQL 8.x 必须使用com.mysql.cj.jdbc.Driver,URL 里一定要带时区参数,这是无数新人翻车的第一现场。连接池方面,哪怕你只做课设,也建议用 HikariCP 或 Druid 替代原生的DriverManager.getConnection(),这能让你体会一下生产环境里“连接复用”的思路,防止群交并发模拟时把数据库连接数打爆。
# MySQL 8.x 的 JDBC 连接关键参数 jdbc:mysql://localhost:3306/edu_admin?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8serverTimezone=Asia/Shanghai:MySQL 8.x 的驱动强制要求设置时区,不设直接报时区错误characterEncoding=utf8:防止中文字符在写入和读取时变成乱码useSSL=false:本地开发没必要启用 SSL 握手,能省掉不少连接延迟
如果你还是在校生,手里电脑配置一般,不建议强行上 Docker 跑 MySQL。直接在宿主机装一个 MySQL 5.7 或 8.0,把服务启动起来就够用了。Docker 适合进阶练习,但课设阶段它会引入端口映射、数据持久化、容器生命周期这些额外概念,排查起来会分散你写业务的注意力。
2.3 选型时的三个避坑思考
第一,不要一开始就用 MyBatis-Plus 自动生成全部 CRUD。课设答辩时,老师大概率会问你某一条 SQL 的逻辑,如果你连select * from course where id = ?都说不清楚,会很尴尬。建议手写至少一半的 SQL。第二,如果你决定用 Spring Boot,请把spring.jpa.hibernate.ddl-auto设置为none或validate,不要让它自动改表结构。否则 MySQL 和 Hibernate 的方言不一致时,会出现字段类型对不上、索引丢失的隐蔽问题。第三,实体类命名别偷懒直接叫User,Java 里User是太通用的类名,后续你写拦截器、权限管理时容易和框架内部类冲突。叫Student、Teacher、AdminUser各自分开,能省掉很多 import 上的心智负担。
3. 数据库设计:从 ER 图到建表语句的完整落地
3.1 教务系统的核心实体与关系拆解
教务管理系统的数据模型,往复杂了说可以有几十张表,但课设阶段真正需要落地的核心实体只有四类:学生、教师、课程、成绩。围绕这四类实体延伸出来的关联关系,才是设计的精华所在。一个学生可以选多门课程,一门课程可以被多个学生选修,这是典型的多对多关系,必须通过中间表选课表来解耦;一位教师可以教授多门课程,一门课程通常由一个教师负责,这是一对多关系,直接在课程表里放教师 ID 作为外键即可。
成绩表不该独立存在,它本质上是选课关系的一个属性。也就是说,当你把“谁选了哪门课”记录在选课表里时,成绩这个字段就应该待在选课表那一行记录上。很多初学者会把成绩单独建一张表,然后用选课记录 ID 去关联,这在逻辑上没错,但会产生冗余和额外关联查询。课设阶段,把成绩字段放在选课表里是最工程化的选择。
学生表(Student) - id BIGINT PRIMARY KEY AUTO_INCREMENT - student_no VARCHAR(20) UNIQUE NOT NULL -- 学号 - name VARCHAR(50) NOT NULL - gender TINYINT DEFAULT 0 -- 0未知 1男 2女 - major VARCHAR(100) -- 专业 - class_name VARCHAR(50) -- 班级 - enroll_year INT -- 入学年份 - password VARCHAR(100) DEFAULT '123456' -- 初始密码教师表(Teacher) - id BIGINT PRIMARY KEY AUTO_INCREMENT - teacher_no VARCHAR(20) UNIQUE NOT NULL - name VARCHAR(50) NOT NULL - title VARCHAR(50) -- 职称 - department VARCHAR(100) -- 院系 - password VARCHAR(100) DEFAULT '123456'课程表(Course) - id BIGINT PRIMARY KEY AUTO_INCREMENT - course_no VARCHAR(20) UNIQUE NOT NULL -- 课程编号 - course_name VARCHAR(100) NOT NULL - credit DECIMAL(3,1) -- 学分 - teacher_id BIGINT NOT NULL -- 授课教师ID - max_students INT DEFAULT 50 -- 选课人数上限 - FOREIGN KEY (teacher_id) REFERENCES Teacher(id)3.2 选课表与成绩字段的落表设计
选课表是连接学生和课程的桥梁,也是整个系统里数据增长最快、最容易出现逻辑漏洞的地方。设计这张表时,首先要保证联合唯一性:同一个学生不能重复选同一门课。这个约束必须在数据库层面用UNIQUE KEY实现,而不是靠 Java 代码里先查再插,否则并发情况下会出现两条重复记录。
选课表(Enrollment) - id BIGINT PRIMARY KEY AUTO_INCREMENT - student_id BIGINT NOT NULL - course_id BIGINT NOT NULL - score DECIMAL(5,2) -- 成绩,可空 - status TINYINT DEFAULT 0 -- 0已选 1已退 2已结课 - create_time DATETIME DEFAULT CURRENT_TIMESTAMP - UNIQUE KEY uk_student_course (student_id, course_id) - FOREIGN KEY (student_id) REFERENCES Student(id) - FOREIGN KEY (course_id) REFERENCES Course(id)score允许为空,意味着选了课但还没出成绩;status字段的存在,让退课操作变成一种更新而非删除,保证历史选课数据可追溯。这里有一个很多教程不会提的细节:如果你把退课做成 DELETE 物理删除,那么以后想统计“某门课的退课率”或者“某学生的选课轨迹”时,数据就永远找不回来了。所以哪怕只是课设,也要养成逻辑删除的习惯,这是做事和做产品的思维方式差异。
3.3 三范式理论在课设中的实际应用边界
学校教材里反复强调范式理论,但真正做课设时,你不需要死背定义,只需要记住两个规律:第一,一张表只描述一种实体类型,不要混入其他实体的独立属性;第二,一个字段只能有一个含义,不要出现“备注里既写手机号又写宿舍号”这种情况。只要你遵守这两条,你的表设计基本能达到第三范式。
但范式不是越高越好。比如,如果你把学生的院系单独建一张表,学生表里只存院系 ID,这确实符合第三范式,但你的课设查询页面里,每次展示学生列表都需要 JOIN 一次院系表。课设数据量不大,这种 JOIN 没什么感觉;但如果你频繁在综合信息查询页面里一次性展示所有学生及班级信息,就不得不伴随三次 JOIN。这个时候,适度冗余反而更合理——在学生表里同时保存major(专业)和department(院系代码),业务层直接取,不用 JOIN。课设答辩时,老师看到你能够在范式和性能之间做取舍,这反而是加分项。
查询示例:查看选了某门课的所有学生及成绩 SELECT s.student_no, s.name, e.score FROM Student s INNER JOIN Enrollment e ON s.id = e.student_id WHERE e.course_id = 1 AND e.status = 0;注意这里用INNER JOIN,而不是写子查询。MySQL 优化器对 JOIN 的优化比较成熟,而且这个语句的语义是“只要在选课表里有记录的就返回”,不会误伤没有选课的学生。很多同学会在这种简单场景下写出WHERE student_id IN (SELECT ...),功能等价,但可读性和执行效率都不如 JOIN。
3.4 索引设计:别只会建主键
课程设计里,很多人的表只有主键索引,其他查询全靠全表扫描。数据量小的时候无所谓,但如果你导入几千条模拟数据,再执行一些关联查询,速度差异立刻显现。常见的加索引规则很简单:WHERE 子句里频繁出现的字段、JOIN 的关联字段、表上带UNIQUE KEY的字段,都应该建索引。选课表里的student_id + course_id联合唯一索引,本身就兼顾了约束和查询性能。成绩查询场景里,如果你经常按student_id查这个学生所有选课,单字段索引也值得加。
不要给每个字段都加索引,因为索引会占用存储空间,并且写操作时 MySQL 要同步更新所有索引,反而拖慢速度。课设里你只需要保证“主键、学号/工号/课程编号、外键字段”这三类有索引就足够了。如果你用EXPLAIN查看执行计划,看到type=ALL且possible_keys=NULL,说明这条 SQL 正在全表扫描,就能判断该建索引了。
4. Java 实现数据访问层:从 JDBC 到连接池的演进
4.1 原生 JDBC 的最小可用代码
如果你用的是 Servlet + JSP 模式,数据访问层通常自己封装一个 JDBCUtil。这个工具类负责加载驱动、获取连接、释放资源。很多新人在这一步容易踩进“每个方法都写一遍加载驱动”的泥潭,正确做法是只在静态块里加载一次驱动。
public class JDBCUtil { private static String url = "jdbc:mysql://localhost:3306/edu_admin?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; private static String user = "root"; private static String password = "yourpassword"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, user, password); } public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { if (rs != null) try { rs.close(); } catch (SQLException e) {} if (ps != null) try { ps.close(); } catch (SQLException e) {} if (conn != null) try { conn.close(); } catch (SQLException e) {} } }这段代码里的关键点在Class.forName只执行一次,以及关闭资源时用try-catch包裹但不抛出。很多教程写的close()方法会直接声明throws SQLException,但资源关闭失败时,你更希望吞掉异常而不是覆盖主逻辑里的报错信息。参数方面,连接 MySQL 8.0 必须用com.mysql.cj.jdbc.Driver,如果你用的是 MySQL 5.7,则要保留com.mysql.jdbc.Driver,两个驱动类名差异不大,但用错了会直接报ClassNotFoundException。
4.2 PreparedStatement 为什么是唯一选择
数据访问层里写 SQL 只有两种姿势:Statement直接拼字符串,或者PreparedStatement占位符预编译。课设里如果出现用户的输入直接拼进 SQL 字符串的情况,那就是典型的注入漏洞,你只要在学生登录框输入' or '1'='1,整张表可能都被查出来。预编译走的是?占位符,MySQL 驱动会把用户输入当作纯数据,根本不会参与 SQL 语法解析,这是高层级的安全习惯。
public boolean addEnrollment(long studentId, long courseId) { String sql = "INSERT INTO Enrollment(student_id, course_id, status) VALUES(?, ?, 0)"; try (Connection conn = JDBCUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setLong(1, studentId); ps.setLong(2, courseId); return ps.executeUpdate() > 0; } catch (SQLException e) { e.printStackTrace(); return false; } }注意这里用了 try-with-resources 语法,连接和语句用完自动关闭,省去了手动调用close()的麻烦。setLong和setString这种类型化方法,会自动处理 Java 类型到 SQL 类型的映射;如果你传入字符串形式的学号,也建议在业务层先转成long再传入,避免索引失效。
4.3 更换连接池的升级路线
JDBC 原生方式最大的痛点是每次getConnection()都要经历 TCP 握手、MySQL 认证等完整流程,在高频访问下响应会很慢。连接池的思路是提前创建一批物理连接放在池子里,业务代码用时借、用后还,避免反复创建销毁。课程设计阶段,你不需要自己实现连接池(之前甚至有刁钻老师让你手写一个,纯属自虐),直接用 HikariCP 即可。
<dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> <version>4.0.3</version> </dependency>HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:mysql://localhost:3306/edu_admin?useSSL=false&serverTimezone=Asia/Shanghai"); config.setUsername("root"); config.setPassword("yourpassword"); config.setMaximumPoolSize(10); config.setMinimumIdle(2); config.setConnectionTimeout(30000); HikariDataSource dataSource = new HikariDataSource(config);maximumPoolSize决定池中最大连接数,课设项目开 10 就好,没必要开几十个;minimumIdle代表最少保留的空闲连接,设为 2 可以保证启动后第一次请求不用等待建连;connectionTimeout是等待连接的超时时间,30 秒已经是底线,超过这个时间还在阻塞,说明你的连接很可能泄漏了。用了连接池之后,你每次从池里拿到了一个Connection,用完关闭时并不是真正断开物理连接,而是归还给池子复用,这是连接池高效的原因。
4.4 事务控制:选课与退课时必设逻辑
选课操作涉及两步:检查课程是否还有余位,然后插入选课记录。这两步如果分开执行,第一步查出还有 1 个名额,第二步插入选课记录前另一个请求抢占了最后一个名额,你就产生了超卖。防止超卖的正确姿势里,数据库层的唯一索引是第一道防线,事务是第二道防线。事务能力在 JDBC 里默认是自动提交的,你需要手动setAutoCommit(false)来开启。
Connection conn = JDBCUtil.getConnection(); try { conn.setAutoCommit(false); // 查询当前课程已选人数(加行锁) String checkSql = "SELECT COUNT(*) FROM Enrollment WHERE course_id = ? AND status = 0 FOR UPDATE"; // 如果已选人数 < max_students,插入选课记录 String insertSql = "INSERT INTO Enrollment(student_id, course_id, status) VALUES(?, ?, 0)"; // 两条操作都成功后提交 conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); }SELECT ... FOR UPDATE会锁住命中的记录,防止并发下重复选课。这是 MySQL InnoDB 引擎提供的悲观锁方案,课设里使用是完全合适的。如果你对性能有更高追求,可以改成先 UPDATE 再判断影响行数的方式,但那就超出课设的复杂度了。注意finally里把autoCommit恢复成true,否则连接归还连接池后,下一个使用者会继承你的事务状态。
5. 业务逻辑避坑:选课并发、成绩录入与级联删除的翻车现场
5.1 现象一:学生重复选同一门课
当我第一次做这个系统时,连续点了两次“选课”按钮,系统里出现了两条一模一样的记录。当时的第一反应是前端按钮没做防抖,但后来发现即使前端做了限制,用模拟并发工具同时发送多个请求,还是会出问题。原因是 Java 代码里先执行SELECT检查是否已选,再执行INSERT插入记录,两步之间存在时间差。解决办法有两个层面:数据库层面在选课表上建联合唯一索引(student_id, course_id),这是最后一道防线;应用层面,选课前先用SELECT ... FOR UPDATE锁住该学生的选课记录,或者直接捕获插入时的DuplicateKeyException,拿到冲突后给用户返回“已选过该课程”。
5.2 现象二:成绩录入范围不合法
成绩表允许输入 0 到 100 的数值,但有人通过接口传了98.5,按逻辑没问题;传了-10或120,数据库也能存进去。问题就出在课设里没有做字段级的合法性校验。解决方式分两层:前端绑定和校验规则,JS 里限制成绩输入框只能填数字且范围 0-100;后端 Java 里解析score字段后,直接用if (score < 0 || score > 100)拦截。后端校验是必须存在的,因为它不受前端框架限制,哪怕别人绕过前端用 Postman 直连,后端也能挡住非法数据。
5.3 现象三:删除教师时连带删掉了课程
某天你为了测试数据方便,删了一位教师,结果发现他名下所有课程全没了,连带着学生选课记录也被清空。这是因为你建外键时用了ON DELETE CASCADE。课设里这种级联删除是非常危险的操作,真实业务里几乎不会把核心数据做成级联删除。正确的做法有两种:第一种,把教师表的删除改成逻辑删除,加一个is_deleted字段,默认 0,删除时 UPDATE 成 1;第二种,外键约束用ON DELETE RESTRICT,强行删除教师时,MySQL 会报外键约束错误,强制你先把关联课程转移或删除。课设阶段推荐第二种,因为它的报错逻辑更直白,能训练你思考数据依赖关系。
5.4 现象四:连接忘记关闭,网站越跑越慢
我用原生 JDBC 写的时候,有一次测验系统开了一下午,第 37 次操作时整个页面彻底卡死,重启 Tomcat 才好。看日志才发现是连接池里的连接全被占满,原因是我有一段代码漏写了conn.close()。连接没关闭时,MySQL 侧能看到对应连接一直处于Sleep状态。解决这种问题,首选try-with-resources语句块,确保Connection、PreparedStatement、ResultSet全部实现AutoCloseable接口并自动释放;其次,排查时用SHOW PROCESSLIST查看 MySQL 当前连接列表,重点观察Time字段特别大且State为Sleep的连接,往往就是泄漏点。
-- 查看 MySQL 当前连接状态 SHOW PROCESSLIST; -- 输出里重点看 Command = Sleep 并且 Time 很大的记录5.5 现象五:中文乱码与表情符号报错
JSP 页面提交的中文数据写到 MySQL 后全是问号,这是字符集配置不一致导致的老问题。需要从上到下排查四层:MySQL 数据库和表的charset必须为utf8mb4,连接 URL 里加characterEncoding=utf8,JSP 页面头部设置contentType="text/html; charset=UTF-8",Tomcat 的server.xml里连接器加上URIEncoding="UTF-8"。四个位置只要有一个漏掉,中文就可能变成乱码。特别提醒,数据库的字符集千万别用utf8,MySQL 的utf8是假的,它只支持 3 字节的 UTF-8 字符,遇到表情符号会直接报错或存储失败;正确姿势是utf8mb4,这才是完整版 UTF-8。
6. 课设验收技巧:把演示数据做出“真实感”与答辩加分项
6.1 造一套有业务含义的模拟数据
课设答辩时,老师最常做的一件事是点开你的系统,看页面里是否空空如也。如果你数据库里只有三条学生记录、两门课程,界面上看起来会非常单薄,老师也很难判断你的系统逻辑是否完备。我会强烈建议你写一个数据生成脚本,用 Java 或直接写 SQL 存储过程批量造数据。比如生成 200 个学生、30 门课程、每个学生随机选 3-6 门课,关键是保证数据是符合真实逻辑的:学生的学号连续有规律、课程学分分布合理、选课人数不超过课程容量、成绩整体分布在 60 到 95 之间。
INSERT INTO Student (student_no, name, gender, major, class_name, enroll_year, password) SELECT CONCAT('2024', LPAD(n, 4, '0')), CONCAT('学生', n), IF(n % 2 = 0, 1, 2), ELT(1 + FLOOR(RAND() * 4), '计算机科学与技术', '软件工程', '数据科学', '网络工程'), CONCAT('计科', n % 5 + 1, '班'), 2024, MD5('123456') FROM ( SELECT 1 AS n UNION SELECT 2 UNION SELECT 3 -- 实际使用存储过程循环生成 ) t;这里学号故意用2024 + 四位流水号的格式,专业和班级做了随机映射,目的是让页面上的筛选和搜索功能有实际意义。当你用SELECT * FROM Enrollment能一次性翻到几百条选课记录时,不管是你自己调试分页还是答辩演示,都会从容得多。数据量建议在 500 到 1000 行之间,太少看不出查询性能差异,太多会让你操作卡顿、反而增加演示风险。
6.2 演示前必做的操作路径演练
答辩演示和平时自己开发完全是两种状态。平时你熟悉每个页面的跳转逻辑,但答辩现场如果你从头开始点,很容易因为某个录入环节缺少一条初始化数据而卡住。我自己的习惯是准备三条演示路径,每条路径都从登录开始,并且确保每一步点完都有可预期的反馈。路径一:管理员登录,新增一门课程,然后查看课程列表确认新课程出现;路径二:学生登录,查询可选课程,成功选课后在“我的课表”里能看到记录;路径三:教师登录,给某门课录入成绩,保存后切换到学生账号查看分数是否更新。
这三条路径覆盖了系统里最核心的权限和业务流转。如果答辩时时间紧张,优先演示路径二,因为它涉及学生选课、课程余量变化、选课记录新增三个联动效果,最能体现数据库设计的完整性。提前用专用账号把这三条路径的所有前置数据准备好,比如学生 A 已经选了一部分课、教师 B 已经分配了课程,这样演示时不会出现“空页面”的尴尬。
6.3 一道高频答辩问题:如何应对并发选课
很多老师会在答辩时问:“如果两个学生同时选同一门只剩一个名额的课,你的系统会怎么处理?”如果你回答“先判断再加”,老师基本会追问“判断完到加入之间的时间差怎么办”。这时候你要主动说出三个层面的防御:数据库唯一索引兜底、事务包裹检查与插入、业务层捕获重复键异常后返回友好提示。这比背概念有效得多,因为它证明你真的思考过数据一致性问题。类似的高频问题还有“你为什么要在这个字段上加索引”“你的班级字段和院系为什么不分表”等,回答时只需要用“避免全表扫描”“保持查询语义简洁”这类工程化理由,而不是“大家都是这么设计的”。
课设做完之后,建议留出时间做一次彻底的“数据体检”:用EXPLAIN看几条核心 SQL 的执行计划,用SHOW INDEX FROM Enrollment确认索引都生效,最后把 MySQL 数据目录做个备份存档。这些都是很小但对未来极有用的习惯。希望这篇文章能帮你把教务管理系统做得比平均水平更扎实一点,少走弯路,一次答辩顺利通过。
本文还有配套的精品资源,点击获取