简介:面向高校Java Web实训课程,这份JSP+MySQL学生宿舍管理系统源码包适合正在完成课程设计或需要参考完整增删改查项目的学生。系统覆盖宿舍分配、住宿费管理、报修处理、统计报表等典型模块,展示JSP页面、Servlet、JavaBean与MySQL数据库的协作方式,可直接部署学习或在此基础上扩展。压缩包共67个文件,约5.55MB,包含17个Java源文件与17个class文件、10个JSP页面、8个XML配置、7个Jar依赖包、1个SQL数据库脚本及1份docx作业文档,pom.xml与iml文件便于识别项目结构;src、web、sql、doc等目录划分清晰。已有69人学习下载。除了完整源代码,附带文档说明了系统架构、数据库设计、功能模块、安装配置与运行步骤,适合对照理解后端逻辑、理清数据表关系,也可作为实训报告与答辩准备的参考。SQL脚本提供建库建表与初始数据,可快速完成环境初始化;结合web.xml可了解应用的基础配置。
1. 实训选型:为什么说JSP+MySQL的学生宿舍管理系统是最稳的课设题
实训周最常见的进度的这样的:老师说要交一个管理系统,你打开Eclipse建了个Java项目,写了三天登录界面,最后发现数据库还没连上,连DBUtil是什么都不知道。这篇笔记要讲的基于JSP+MySQL的学生宿舍管理系统,就是这类课设的典型代表——它不算新,胜在结构完整、覆盖面广:JSP负责页面展示,Servlet处理请求,MySQL存业务数据,再配上一份数据库脚本和设计文档,新手能一步步跑通,熟手能快速改造成自己的需求。它适合拿来交实训作业、期末课设,也适合在学Spring Boot之前练一把手。接下来的内容不是让你抄源码,而是把这条链路完整拆开,告诉每一步为什么要这么写、参数怎么调、哪里最容易翻车。
2. 看清骨架:JSP、Servlet、MySQL这三层到底怎么分工
2.1 为什么是JSP而不是Spring Boot:课设场景的务实选择
先回答很多同学纠结的问题:2025年了,为什么老师还在布置JSP的作业?答案很现实——实训进度决定的。Java Web课程从Servlet讲到JSP再到JDBC,结课时间刚好够布置一个管理系统;Spring Boot虽然好用,但实训周里硬上它等于给自己加戏:配Maven、理解自动配置、处理打包部署,这些都不是两周能消化的。
JSP+MySQL的核心价值在于,它把Web开发三个基本功暴露得很清楚:页面怎么展示数据(JSP)、请求怎么分发处理(Servlet)、数据怎么持久化(MySQL加JDBC)。没有框架替你遮住这些,你反而能把“浏览器发请求→Servlet接住→DAO查数据库→结果返回页面”这条链路彻底看明白。这套底子打好了,后面学Spring MVC、MyBatis的时候,会发现那些框架不过是你现在手写代码的封装和托管。
2.2 三层架构拆解:JSP、Servlet、DAO的职责边界
这个标题下的代码包结构,最常见的分层是这样的:
- src/com/xxx/entity:实体类,和数据库表字段一一映射,字段名建议保持和表结构一致
- src/com/xxx/dao:数据访问层,手写SQL,对外提供增删改查方法
- src/com/xxx/servlet:控制层,接收参数、调用DAO、把结果塞进request或session
- WebContent/jsp:视图层,只做展示,不写业务逻辑
- WebContent/WEB-INF/web.xml:配置Servlet映射、欢迎页面、编码过滤器
这里有一个实训验收时很加分的约定:JSP页面只从request和session里取数据,用JSTL或表达式输出,不直接new DAO。这么做的直接好处是,以后想换成别的框架,只需要动Servlet这一层,视图和数据库都不受影响。很多同学的源码被老师说“像一锅粥”,问题就出在JSP里嵌了大量Java代码——页面里com.mysql开头的import一出现,分基本就要扣。
2.3 源码里没明说但验收一定看的三个点
第一是数据库脚本完不完整。一份合格的sql文件,至少要包含建库、建表、默认管理员账号和一批带中文的测试数据。老师拿到项目后第一个动作往往是直接跑你的sql文件,跑不通的话后面代码都懒得看。
第二是连接方式够不够讲究。课设用DriverManager就能跑,但简历上写“使用Druid连接池”明显更拿得出手。连接池本质上就是帮你管理Connection的创建和复用,避免每次请求都new一个连接,第3章我会给接入写法。
第三是文档有没有认真写。那份设计文档不是凑字数的,老师往往先翻文档后看代码。文档里的ER图、用例表、数据字典,比代码本身更能说明你“做了设计”。哪怕代码是参考的,文档写成自己的理解,答辩时也敢讲。
3. 环境搭到能跑:JDK、Tomcat、MySQL 8.0的安装与连通
3.1 MySQL 8.0安装与初始化:两个必勾的选项
关于mysql安装教程网上很多,但针对这个项目,安装MySQL 8.0时有两点和课设强相关,选错后面会多踩一小时坑。
第一是字符集。安装到最后一步选Character Set时,别用默认的utf8mb4吗?恰恰相反,就要选utf8mb4。MySQL里的utf8是旧的utf8mb3,存emoji和生僻字会报错,而学生填姓名时总有人名字里带很生僻的字。项目里所有数据库、表、连接URL统一utf8mb4,中文乱码问题能少一半。
第二是认证插件。MySQL 8.0默认的认证插件是caching_sha2_password,如果你的JDBC驱动是5.x,或者用了老版本Navicat,连接时会直接报密码错误或连不上。解决办法是安装时选择Legacy Authentication(兼容老版本),或者安装后用命令行改掉root密码的插件。课设电脑上怎么省事怎么来,选Legacy最稳。
安装完成后,建议顺手确认MySQL服务是手动还是自动启动。实训教室的电脑经常重启,如果你设成手动,第二天开机连接数据库报错,第一反应往往是“代码坏了”,其实是服务没起来。
3.2 JDK、Tomcat、Eclipse的版本搭配与动态Web项目
JDK建议用8或11,Tomcat用9.x,Eclipse用Enterprise Java and Web版本,这三个版本组合最省心。JDK版本太高(比如17)配老Tomcat,会有模块访问报错;Tomcat版本太旧配新版JDK,启动直接失败。
新建项目时选Dynamic Web Project,注意三个配置:
- Target runtime选你本机装好的Tomcat
- Dynamic web module version选3.1或4.0
- 勾选Generate web.xml deployment descriptor,因为后面要手动配置Servlet映射
项目建好后,把mysql-connector-java的jar包放到WebContent/WEB-INF/lib目录下,这一步是初学者最容易漏的。放到别的地方哪怕写了Class.forName,运行时会报ClassNotFoundException,因为Tomcat只加载WEB-INF/lib下的jar。
3.3 数据库连通测试:JDBC连接工具类与URL参数
写DAO之前,先把数据库连通性搞定。下面这段就是项目里最核心的DBUtil工具类,复用它就能让所有DAO免去重复创建连接的代码:
package com.sushe.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/dormitory" + "?useUnicode=true&characterEncoding=utf8" + "&useSSL=false" + "&serverTimezone=Asia/Shanghai" + "&allowPublicKeyRetrieval=true"; private static final String USER = "root"; private static final String PASSWORD = "123456"; static { try { // MySQL 8.0 的驱动类名是 com.mysql.cj.jdbc.Driver Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError("MySQL驱动加载失败,检查jar包是否放在WEB-INF/lib下"); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { // 关闭顺序必须与打开顺序相反:ResultSet -> PreparedStatement -> Connection if (rs != null) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } } if (ps != null) { try { ps.close(); } catch (SQLException e) { e.printStackTrace(); } } if (conn != null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }连接串里四个参数逐个说清楚。useUnicode和characterEncoding是保证中文进数据库不乱码,前后端和数据库三层字符集必须一致。useSSL=false是关闭SSL握手,本地开发开着它除了拖慢连接速度没别的作用。serverTimezone=Asia/Shanghai是告诉驱动用东八区时间,不然MySQL 8.0会拿服务器默认时区做时间换算,日期字段可能差8小时。allowPublicKeyRetrieval=true是配合caching_sha2_password插件的,不加它连接时可能报Public Key Retrieval is not allowed,这个报错在MySQL 8.0特别常见。
USER和PASSWORD要改成你自己MySQL的账号密码。很多同学在这里直接把视频里的root/123456套进自己电脑,结果连不上就怀疑代码,其实是密码没改。写代码时建议先在main方法里打印一下connection对象,能拿到连接对象再往下写DAO,否则后面的所有操作都会在同一个坑里反复绊倒。
4. 把核心功能写出来:建表、登录、宿舍分配与报修的一条龙实现
4.1 数据库表设计:学生表、宿舍表、报修表的字段取舍
学生宿舍管理系统逃不开的几张表:学生表、宿舍表、报修表,外加一个公告表用来装饰首页。建表SQL如下,字段注释已经写清楚,直接跑即可:
CREATE DATABASE IF NOT EXISTS dormitory DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE dormitory; CREATE TABLE t_student ( stu_id VARCHAR(20) PRIMARY KEY COMMENT '学号', stu_name VARCHAR(50) NOT NULL COMMENT '姓名', password VARCHAR(64) NOT NULL COMMENT '密码,建议存MD5哈希,不存明文', sex CHAR(2) COMMENT '性别', dorm_id VARCHAR(10) COMMENT '宿舍编号,为空表示未分配', phone VARCHAR(11) COMMENT '手机号', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间' ) ENGINE=InnoDB COMMENT='学生表'; CREATE TABLE t_dorm ( dorm_id VARCHAR(10) PRIMARY KEY COMMENT '宿舍编号,如A-101', building VARCHAR(20) COMMENT '楼栋名', room_no VARCHAR(10) COMMENT '房间号', capacity INT DEFAULT 4 COMMENT '可住人数', current_count INT DEFAULT 0 COMMENT '当前已住人数' ) ENGINE=InnoDB COMMENT='宿舍表'; CREATE TABLE t_repair ( rep_id INT AUTO_INCREMENT PRIMARY KEY, stu_id VARCHAR(20) COMMENT '报修人学号', dorm_id VARCHAR(10) COMMENT '宿舍号', content VARCHAR(255) COMMENT '报修内容', status TINYINT DEFAULT 0 COMMENT '0待处理 1处理中 2已完成', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '报修时间' ) ENGINE=InnoDB COMMENT='报修表';关于MySQL排序和查询的细节,在表设计阶段就要想好。t_repair表按create_time排序展示时,直接ORDER BY create_time DESC即可,但如果有“按待处理优先级高”的需求,就写成ORDER BY status ASC, create_time DESC,让状态为0的记录排在前面。这个语法本身不难,难的是把排序规则和业务逻辑对应上。
字段设计有三个经验。一是密码字段长度别用20,MD5是32位、SHA-256是64位,直接给64最省事。二是宿舍表和学生的关联别用外键约束。课设里写外键看着规范,但后期导入测试数据、修改宿舍编号时会被外键卡住,实训场景里建议只保留逻辑关联,不建物理外键。三是所有时间字段给默认值CURRENT_TIMESTAMP,这样插入数据时少写一个字段,也避免Java端时间格式和MySQL对不上。
4.2 登录功能:Servlet校验、Session会话与编码过滤器
登录是所有管理系统的入口,也是老师必看的部分。从数据库增删改查的角度看,登录就是一个最简单的SELECT加条件查询。但要把登录写得规范,至少涉及三个层面的处理。
先写LoginServlet的代码骨架:
package com.sushe.servlet; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.*; import com.sushe.dao.StudentDao; import com.sushe.entity.Student; import com.sushe.util.MD5Util; public class LoginServlet extends HttpServlet { private static final long serialVersionUID = 1L; protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 解决中文参数乱码,必须在读取参数之前设置 request.setCharacterEncoding("UTF-8"); // 2. 接收前端表单参数 String stuId = request.getParameter("stuId"); String password = request.getParameter("password"); // 3. 简单非空校验,避免无效查询 if (stuId == null || stuId.isEmpty() || password == null || password.isEmpty()) { request.setAttribute("errorMsg", "学号和密码不能为空"); request.getRequestDispatcher("/jsp/login.jsp").forward(request, response); return; } // 4. 调用DAO查询用户,密码存的是MD5 StudentDao dao = new StudentDao(); Student student = dao.findByStuIdAndPassword(stuId, MD5Util.md5(password)); if (student != null) { // 5. 登录成功:写入Session,跳转主页面 HttpSession session = request.getSession(); session.setAttribute("loginUser", student); // 6. 设置会话超时时间为30分钟,单位是秒 session.setMaxInactiveInterval(30 * 60); response.sendRedirect(request.getContextPath() + "/jsp/index.jsp"); } else { // 7. 登录失败:回传错误信息,转发回登录页 request.setAttribute("errorMsg", "学号或密码错误"); request.getRequestDispatcher("/jsp/login.jsp").forward(request, response); } } }这段代码里有三个细节容易被忽略。第一是request.setCharacterEncoding必须在读取任何参数之前调用,放在doPost第一行是规矩,放后面就失效了。第二是登录失败用forward转发而不是sendRedirect,因为需要把errorMsg带过去;登录成功用sendRedirect重定向,是为了防止用户刷新页面时重复提交表单。第三是Session只是简单的键值对,只要有对象,不需要校验客户端IP,课设做到这个程度已经够了。
DAO层的findByStuIdAndPassword方法,用PreparedStatement而不是Statement:
public Student findByStuIdAndPassword(String stuId, String md5Password) { String sql = "SELECT * FROM t_student WHERE stu_id=? AND password=?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, stuId); ps.setString(2, md5Password); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { Student s = new Student(); s.setStuId(rs.getString("stu_id")); s.setStuName(rs.getString("stu_name")); s.setDormId(rs.getString("dorm_id")); s.setPhone(rs.getString("phone")); return s; } } } catch (Exception e) { e.printStackTrace(); } return null; }这里用PreparedStatement的原因不是方便,而是安全。以前用Statement时,把参数直接拼进SQL字符串,stuId传一个'1' OR '1'='1',登录就被绕过了。PreparedStatement先预编译再传参,参数永远只是数据,不会变成SQL结构。这也是“数据库sql注入”这个老话题里最基础也最关键的一道防线。
登录页还有一个常被抽查的细节:跨页面跳转后,Session里的用户信息怎么在JSP上展示。比如首页顶部要显示“当前登录:张三”,JSP里直接取session即可:
<span>当前登录:${sessionScope.loginUser.stuName}</span>这种jsp个人信息展示页面的写法,比用Java脚本片段干净,也不会因为Servlet往request里塞的对象没同步而报错。
4.3 宿舍分配与调换:事务边界必须卡在“人数校验+双表更新”
宿舍管理里最有技术含量的一块,是把一个学生分到某个宿舍。很多同学的写法是两步:先UPDATE t_dorm的current_count加1,再UPDATE t_student的dorm_id。这两步之间只要程序中途出错,就会出现宿舍人数加了但学生表没更新,或者反过来学生有宿舍了但计数没变。
正确的做法是把两步包进同一个事务里,要么都成功,要么都回滚。下面这段代码是宿舍分配的核心逻辑:
public boolean assignDorm(String stuId, String dormId) { Connection conn = null; try { conn = DBUtil.getConnection(); // 1. 关闭自动提交,开启手动事务 conn.setAutoCommit(false); // 2. 查询宿舍当前人数,FOR UPDATE加行级锁,防止并发时两人同时住进最后一个床位 String querySql = "SELECT current_count, capacity FROM t_dorm WHERE dorm_id=? FOR UPDATE"; try (PreparedStatement ps = conn.prepareStatement(querySql)) { ps.setString(1, dormId); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { int current = rs.getInt("current_count"); int capacity = rs.getInt("capacity"); if (current >= capacity) { conn.rollback(); return false; // 宿舍已满 } } else { conn.rollback(); return false; // 宿舍不存在 } } } // 3. 宿舍人数加1 String updateDorm = "UPDATE t_dorm SET current_count = current_count + 1 WHERE dorm_id=?"; try (PreparedStatement ps = conn.prepareStatement(updateDorm)) { ps.setString(1, dormId); ps.executeUpdate(); } // 4. 学生表绑定宿舍 String updateStudent = "UPDATE t_student SET dorm_id=? WHERE stu_id=?"; try (PreparedStatement ps = conn.prepareStatement(updateStudent)) { ps.setString(1, dormId); ps.setString(2, stuId); ps.executeUpdate(); } // 5. 全部成功,提交事务 conn.commit(); return true; } catch (Exception e) { // 6. 任何一步出错,回滚到事务开始前的状态 if (conn != null) { try { conn.rollback(); } catch (Exception ex) { ex.printStackTrace(); } } e.printStackTrace(); return false; } finally { // 7. 恢复自动提交并关闭连接 if (conn != null) { try { conn.setAutoCommit(true); conn.close(); } catch (Exception e) { e.printStackTrace(); } } } }这段代码要理解的不是语法本身,而是“事务边界”的思维方式。setAutoCommit(false)一旦开启,后面所有SQL都在一个事务里,commit之前任何一步异常,rollback就能撤销全部修改。FOR UPDATE是给宿舍记录加锁,如果两个管理员同时分配同一个宿舍的最后两个床位,行锁会让第二个请求等待第一个提交后再执行,重新读到最新的人数,这个细节在并发演示时很加分。
这样做也能避免数据库事务处理课上讲过的脏读和更新丢失。实训演示时老师如果问“为什么提交失败数据没有乱”,可以直接回答:事务回滚了,这两张表始终一致。
报修功能相对简单,单表INSERT即可。但有一个实操细节:报修内容content字段是用户在文本域里输入的,可能存在英文单引号。用PreparedStatement的setString传参时,单引号会被自动转义,不需要手动替换,这也是为什么前面的挖坑经验里强调一定要用PreparedStatement。
5. 常见问题排查:学生宿舍管理系统跑不起来的5个经典原因
5.1 JSP页面中文乱码:三个位置必须都是UTF-8
现象:页面显示一堆问号,或者数据库里查出来的中文变成了“???”,但英文数字正常。
原因:字符集三层不一致。JSP文件本身的编码、Servlet接收请求时的编码、数据库连接串里的编码,这三层只要有一层掉队,中文就在中间某一段被错误转码。
解决:JSP文件头写pageEncoding="UTF-8",Servlet在doGet和doPost开头都加request.setCharacterEncoding("UTF-8"),连接串里保留characterEncoding=utf8,建库时用utf8mb4。建议再用一个Filter兜底,把所有请求的编码统一成UTF-8,这样后续加Servlet不需要重复写。检查乱码时,按“页面→Servlet→数据库”的顺序逐层打印中文看哪一层开始变样,比瞎改要快。
5.2 MySQL 8.0连接失败:Public Key Retrieval is not allowed
现象:运行程序时控制台报Public Key Retrieval is not allowed,或者Client does not support authentication protocol requested by server。前者多为连接串缺参数,后者多为驱动版本或者认证插件不兼容。
原因:MySQL 8.0默认使用caching_sha2_password插件,老版本JDBC驱动和这个插件握手时,需要额外的公钥获取步骤。
解决:连接串里加allowPublicKeyRetrieval=true,同时确认用的是mysql-connector-java 8.x版本。如果驱动类名写的是com.mysql.jdbc.Driver,改成com.mysql.cj.jdbc.Driver,老驱动类在8.x里还会给警告,但已经不推荐用。还没有解决就回到第3章,把MySQL的认证方式改成Legacy,一劳永逸。
5.3 Tomcat端口被占用:8080起不来
现象:Tomcat启动日志报Port 8080 required by Tomcat v9.0 Server at localhost is already in use,或者启动后浏览器访问localhost:8080提示404。
原因:上一次开发时Tomcat没有正常停止,或者系统里另一个Java进程占了8080端口。
解决:Windows下先执行netstat -ano | findstr 8080查到占用端口的PID,再打开任务管理器结束对应进程。如果这个端口总被占,干脆在server.xml里把HTTP端口换成9090或8081,一劳永逸。注意改端口后,访问地址全部跟着变,如果代码里有写死8080的跳转,也要一起改。
5.4 JSP页面图片和CSS路径全挂:坐标定位的经典翻车
现象:登录页能打开,但img标签的小图标裂开,CSS样式完全没有,点击跳转后URL路径多了一层导致页面找不到静态资源。
原因:这是JSP页面最常见的路径基准问题。用/jsp/login.jsp直接访问时,相对路径images/logo.png解析正确;但从Servlet转发到该页面时,浏览器地址栏停留在/login这个虚拟路径上,相对路径变成了/images/logo.png,自然就404了。这也牵扯到jsp图片如何对坐标定位的典型场景——不管图片放在页面什么位置,src路径基准都是页面URL,不是文件位置。
解决:所有静态资源路径都写绝对路径:<img src="${pageContext.request.contextPath}/images/logo.png">。这个写法在JSP里适用所有href、src、action属性,contextPath会自动带上项目名,任何来源的跳转路径都不会错。检查途径也简单,浏览器F12找到Failed to load resource,看URL路径是否少了项目名。
5.5 数据操作后页面不刷新:要么重定向要么手动刷新
现象:新增一条报修记录后,跳回列表页看不到新数据,要手动刷新一次才出现。这是jsp页面加载后没有自动刷新的典型表现。
原因:新增操作用的是forward转发,请求里带的是旧的列表数据;浏览器地址没有变化,页面停在旧的内存视图上。
解决:表单提交完成之后,凡是涉及数据变化的操作,都用sendRedirect重定向到列表页,让浏览器重新发起一次全新请求。比如新增报修成功后,response.sendRedirect(request.getContextPath() + "/repair/list")。记住一个原则:新增、修改、删除之后必须重定向,否则按刷新按钮就会重复提交一条数据。
6. 验收前的最后一小时:部署演示与数据备份的实用技巧
实训演示最怕的不是代码写错,而是当场环境出问题。这里说一个我养成的习惯:演示之前用命令行把数据库完整导出一次,并确认备份文件能恢复。导出命令如下:
mysqldump -uroot -p123456 --default-character-set=utf8mb4 dormitory > dormitory_backup.sql恢复时执行:
mysql -uroot -p123456 --default-character-set=utf8mb4 dormitory < dormitory_backup.sql恢复报错就删库重来:先mysql进入后用DROP DATABASE dormitory,再回到第4章的建表SQL重建。备份文件里因为有INSERT语句,执行时间可能超过课设其它脚本,这是正常的。
部署到服务器时,把项目整个导出成war包,放到Tomcat的webapps目录下,启动Tomcat即可自动解压。war包在Eclipse里右键项目Export选WAR file生成。服务器上的MySQL密码和本机不同时,记得同步修改DBUtil里的配置。连接串里的localhost要不要改,取决于MySQL是不是装在同一台机器上。
演示前按这个清单过一遍:管理员默认账号能否登录;宿舍分配后学生列表是否联动更新;重复学号是否被拦截;报修流程能否走完三个状态;数据库脚本从零重建是否一次通过。每一项都是老师爱问的点,也是系统容易翻车的暗礁。特别是重复学号,如果表结构里没设主键约束,就会写进两条同样的学号,答辩时被问到“数据冗余怎么办”只能干瞪眼。
回到最开始说的,JSP+MySQL的课设项目真正的分水岭,不在于代码写得有多炫,而在于你对自己写的东西有没有完整掌控。我当年实训时帮室友连数据库,改了他MySQL的认证方式导致他后面所有项目都连不上,从此就养成把所有版本和密码写进README的习惯。哪个版本对不上、哪个配置改了会有连带影响,全都写清楚,反而比代码本身更有价值。希望这篇笔记能帮你少走几步弯路,把实训周过得从容一点。
本文还有配套的精品资源,点击获取