资讯动态

Java Web学生信息管理系统实战:Servlet+JSP+JDBC三层架构设计与避坑指南

发布时间:2026/10/8 10:11:27 来源:尧图企业网站定制
简介面向学校教务管理场景的Java Web学生信息管理系统设计与实现项目适合Java Web初学者、毕业设计或课程实训使用。系统覆盖登录界面构建、Servlet后台请求处理、Session会话管理、JSP页面数据展示与采集以及基于JDBC的增删改查操作完整呈现学生信息从录入到检索的典型流程。技术细节包括使用PreparedStatement预编译SQL防止注入、合理设计主键与索引保证数据一致性、通过MD5/SHA加密密码与HTTPS传输保障安全这些内容对于理解Web应用分层开发具有直接参考价值。资源包体量约1.21MB已有3006人学习。项目从实际管理需求出发在实现功能的同时强调可扩展性与安全性便于读者对照源码梳理前端交互、后端逻辑与数据库结构之间的协作关系从而快速掌握Java Web开发的核心方法并应用到实际项目中。1. 为什么「学生信息管理系统」是 Java Web 课设的常青树先认清楚要交付什么每到毕业设计开题季Java Web 方向的学生很难避开一个题目基于 Java Web 学生信息管理系统的设计与实现。它听起来普通甚至有点老气但真正动手做的人才会发现一个能跑到答辩现场的完整系统要在 Servlet 生命周期、会话保持、分页 SQL、中文编码这些基本功上全部过关缺一环都会翻车。这个系统本质上是一套带登录认证的学生档案管理应用覆盖了前后端交互、数据库操作、请求转发与重定向、Filter 拦截这几条 Java Web 的主线索。它适合正在做课设或毕设的在校生也适合想用一个完整练手项目夯实基础的初级 Java 工程师。很多跟风去抄 Spring Cloud 微服务项目的同学答辩时连「请求从浏览器到数据库经历了什么」都说不清楚反而是把一个学生信息管理系统按三层架构做扎实的人能在十分钟演示里把每个环节讲透。本文不绕弯子直接按从业者常用的经典技术栈把设计思路、核心代码、参数配置和踩坑记录一次讲完。2. 从技术选型到项目骨架Servlet JSP JDBC 的三层架构怎么立住2.1 先说实话为什么经典 Servlet JSP 反而更好答辩现在的学生一上来就想用 Spring Boot但题目写的是「Java Web」答辩老师的默认预期其实就是 JSP Servlet JDBC 这套经典链路。Spring Boot 把 Tomcat 内嵌、自动配置、依赖管理全部藏起来了很多学生离了 IDE 向导连启动流程都讲不清评委问两句就露馅。我一般会建议课设场景优先选 Servlet JSP JDBC理由很实际每个环节都暴露在代码里Service、DAO、Controller 分层边界清晰答辨时能指着代码讲「请求怎么进来、数据怎么查、页面怎么渲染」。对比维度Servlet JSP JDBCSSMSpring SpringMVC MyBatisSpring Boot MyBatis学习成本低全是 Java 基础中要理解 Spring 容器和 AOP低但黑匣子多配置复杂度低一个 web.xml 搞定中多份 XML 或配置类极低自动配置代码可见性高每行 SQL 和跳转都可见中MyBatis 把 SQL 挪到 XML低启动过程难讲清答辩友好度高评委能顺着代码提问中低容易被追问底层适合场景课设、毕设、练手校招项目企业开发选型时还要看导师要求。如果导师明确说「用 Spring Boot」那就用框架如果只给了题目标题经典三层是性价比最高的选择。新版 IntelliJ IDEA 2024 建 Web 工程的入口是 New Project 里选 Jakarta EE 或 Web Application创建时注意 Artifact 名称后面部署到 Tomcat 时这个名称决定了访问路径前后不一致会白屏。2.2 项目骨架与包结构controller、service、dao 各自管什么把整个系统按三层架构拆开是「设计」二字最直接的体现。我一般会建这样的包结构src/main/java ├── com/example/stums │ ├── entity │ │ ├── Student.java │ │ └── User.java │ ├── dao │ │ ├── StudentDao.java │ │ └── UserDao.java │ ├── service │ │ ├── StudentService.java │ │ └── UserService.java │ ├── controller │ │ ├── LoginServlet.java │ │ ├── StudentServlet.java │ │ └── LogoutServlet.java │ ├── filter │ │ └── LoginFilter.java │ └── util │ ├── DBUtil.java │ └── PageBean.java ├── resources │ └── db.properties └── webapp ├── login.jsp ├── student_list.jsp ├── student_add.jsp ├── student_edit.jsp └── WEB-INF/web.xmlentity 是数据库表对应的 POJO字段和列一一对应dao 层只写 SQL 和 JDBC 操作不写业务判断service 层做参数校验、业务逻辑和事务边界controller 层Servlet只负责收参数、调 service、跳页面。很多新手把 SQL 直接写在 Servlet 里当时觉得省事后面加一个分页条件就要同时改三个文件这就是「设计」没做好的典型症状。2.3 数据库设计与连接工具类建表脚本 DBUtil学生管理系统的核心表就两张学生表和用户表。学生表字段不要贪多学号、姓名、性别、班级、手机号、邮箱、创建时间足够覆盖演示场景。性别建议用 TINYINT 存0 和 1 在页面上再映射成「男/女」比直接存字符串更抗脏数据。CREATE DATABASE stu_db DEFAULT CHARACTER SET utf8mb4; USE stu_db; CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, stu_no VARCHAR(20) NOT NULL UNIQUE COMMENT 学号, name VARCHAR(50) NOT NULL COMMENT 姓名, gender TINYINT DEFAULT 1 COMMENT 1-男 0-女, class_name VARCHAR(50) COMMENT 班级, phone VARCHAR(20), email VARCHAR(64), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL ) ENGINEInnoDB;建表脚本里有三个参数值得注意字符集用 utf8mb4是为了兼容特殊字符和后面 JDBC URL 里的 characterEncoding 配套使用学号字段加了 UNIQUE 约束新增和编辑时要用它查重复create_time 用数据库默认时间代码里不必手动维护。如果你连的是 SQL Server 2008 而不是 MySQL驱动类要换成 com.microsoft.sqlserver.jdbc.SQLServerDriverURL 改成 jdbc:sqlserver://localhost:1433;DatabaseNamestu_db新手最容易在这类连接串上照抄 MySQL 模板导致连不上。数据库连接统一封装成 DBUtil不在每个 DAO 方法里重复写加载驱动的逻辑public class DBUtil { private static String url; private static String username; private static String password; static { try { Properties props new Properties(); props.load(DBUtil.class.getClassLoader() .getResourceAsStream(db.properties)); Class.forName(props.getProperty(driver)); url props.getProperty(url); username props.getProperty(username); password props.getProperty(password); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, username, password); } public static void close(Connection conn, Statement st, ResultSet rs) { if (rs ! null) { try { rs.close(); } catch (SQLException ignored) {} } if (st ! null) { try { st.close(); } catch (SQLException ignored) {} } if (conn ! null) { try { conn.close(); } catch (SQLException ignored) {} } } }对应的 db.properties 放在 classpath 下drivercom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/stu_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai usernameroot password你的密码url 上的三个参数是血泪经验useUnicode 和 characterEncoding 不配写入 MySQL 的中文大概率变问号serverTimezone 是 MySQL 8.x 驱动的硬性要求不配直接报时区错useSSLfalse 是为了关掉本地开发时多余的加密握手省去一堆告警日志。静态块里加载驱动后getConnection 每次现取连接课设规模够用不必上 Druid 连接池想上连接池的把 DriverManager.getConnection 换成 DruidDataSource 的 getConnection 即可。3. 用 JDBC 封装 DAO 层CRUD 的四个方法与分页 count 的边界3.1 为什么要把 SQL 从 Servlet 里赶出去DAO 层的边界如果打开一个 Servlet看到的全是 Connection、PreparedStatement、ResultSet那这个项目基本告别维护了。DAO 存在的意义不是「多写一层显得规范」而是把数据访问的逻辑收敛到一个类里查询条件的增减、表结构的调整、连接释放的管理全部只改 DAO。Servlet 只关心「调用 dao.search() 返回了一个 List」不关心 SQL 怎么拼。我一般会在 StudentDao 里定义这六个方法基本覆盖答辩所有提问点findAll()查全部学生用于不分页的简单列表findById(int id)查详情和编辑回显search(String keyword, int offset, int size)模糊查询加分页count(String keyword)按相同条件统计总数算总页数add(Student s)新增学号重复时抛异常update(Student s) / deleteById(int id)修改和删除3.2 PreparedStatement 防注入CRUD 的四个核心方法写法新增、修改、删除在写法上是同一个套路拼 SQL 模板用 ? 占位符再按顺序 set 参数。以新增为例public int add(Student s) { String sql INSERT INTO student (stu_no, name, gender, class_name, phone, email) VALUES (?, ?, ?, ?, ?, ?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, s.getStuNo()); ps.setString(2, s.getName()); ps.setInt(3, s.getGender()); ps.setString(4, s.getClassName()); ps.setString(5, s.getPhone()); ps.setString(6, s.getEmail()); return ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException(新增学生失败, e); } }这里必须用 PreparedStatement不要用 Statement 去拼接字符串否则姓名里传一个「; DROP TABLE student;--」这类输入就可能变成一条删表语句这不是玄学而是公开的安全事故。set 参数的顺序必须和 SQL 里 ? 的顺序一一对齐错一个就报数据类型转换错。executeUpdate 返回的是受影响行数新增和删除后判断返回值是否为 0可以快速发现 SQL 写对了没。查询方法要注意 ResultSet 的指针语义public Student findById(int id) { String sql SELECT id, stu_no, name, gender, class_name, phone, email, create_time FROM student WHERE id ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, id); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { Student s new Student(); s.setId(rs.getInt(id)); s.setStuNo(rs.getString(stu_no)); s.setName(rs.getString(name)); s.setGender(rs.getInt(gender)); s.setClassName(rs.getString(class_name)); s.setPhone(rs.getString(phone)); s.setEmail(rs.getString(email)); s.setCreateTime(rs.getTimestamp(create_time)); return s; } return null; } } catch (SQLException e) { throw new RuntimeException(查询学生失败, e); } }rs.next() 第一次调用会把指针移到第一行返回 false 表示没有数据。新手常常忘了先 next() 就直接 getString结果抛 IllegalStateException。get 方法的参数是列名必须和 SELECT 里的字段名一致如果 SQL 里写了别名这里要跟别名走。所有资源都放在 try-with-resources 里方法结束自动释放不需要再手动调用 DBUtil.close这套写法比 finally 里手工关闭干净得多。3.3 分页 SQL 与 count 查询别在页面上写死「第 1 页」列表页必须做分页这是答辩的高频考点。MySQL 的分页语法是 LIMIT offset, sizeoffset 是跳过的行数size 是每页条数。页面传过来的页码从 1 开始所以 offset 的换算公式是(pageNum - 1) * pageSize。public ListStudent search(String keyword, int offset, int size) { String sql SELECT id, stu_no, name, gender, class_name, phone, email, create_time FROM student WHERE name LIKE ? OR stu_no LIKE ? ORDER BY id DESC LIMIT ?, ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { String like % keyword %; ps.setString(1, like); ps.setString(2, like); ps.setInt(3, offset); ps.setInt(4, size); try (ResultSet rs ps.executeQuery()) { ListStudent list new ArrayList(); while (rs.next()) { Student s new Student(); s.setId(rs.getInt(id)); s.setStuNo(rs.getString(stu_no)); s.setName(rs.getString(name)); s.setGender(rs.getInt(gender)); s.setClassName(rs.getString(class_name)); s.setPhone(rs.getString(phone)); s.setEmail(rs.getString(email)); list.add(s); } return list; } } catch (SQLException e) { throw new RuntimeException(搜索学生失败, e); } }配套的 count 方法不是简单的SELECT COUNT(*) FROM student而是必须带上同一个 keyword 条件。很多翻车现场就是列表查出来 5 条总数却统计了全表 500 条总页数算得离谱。原因是 count 和 search 的 WHERE 条件不一致。LIMIT 的 ? 占位符是整型setInt 直接传不需要拼字符串这也是 PreparedStatement 在分页上的一个隐藏优势——你不需要担心 offset 被注入成负数或非法值。pageSize 一般固定为 10可以在 constants 类里定义分页组件里总页数用(total pageSize - 1) / pageSize计算避免总数为 0 时出现页数负数的尴尬。4. 登录、分页与模糊查询的完整链路从 Session 到 JSP 渲染4.1 登录不是简单的 if elseSession 与 Filter 的角色划分登录功能如果只在 LoginServlet 里查一下用户名密码就跳转那等于没做认证。真正的问题是用户不通过登录页直接敲student_list.jsp的地址怎么办答案是用 Filter 统一拦截这是 Java Web 里标准的会话控制手段。public class LoginFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; HttpSession session request.getSession(false); String uri request.getRequestURI(); if (uri.endsWith(login.jsp) || uri.endsWith(LoginServlet) || uri.endsWith(.css) || uri.endsWith(.js) || uri.endsWith(logout)) { chain.doFilter(req, resp); return; } if (session null || session.getAttribute(user) null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); } }这段代码的关键在放行名单。静态资源必须放行否则浏览器加载 CSS、JS 时会被拦回登录页页面样式全丢LoginServlet 和 login.jsp 是登录入口和登录页本体不放行会造成死循环跳转。这里用了 request.getSession(false)和 getSession(true) 的区别是没有会话时不创建新会话避免给每个静态资源请求都硬造一个 HttpSession。Filter 在 web.xml 里配置 url-pattern 为/*注意配置顺序会决定多个过滤器之间的链式调用顺序。登录成功后的 Session 里存什么也有讲究。我一般会存 User 对象整体而不是只存 userId这样 JSP 页面可以用${sessionScope.user.username}直接显示当前登录人答辩演示时自然流畅。修改密码功能是基于这个 Session 的扩展校验旧密码、更新 sys_user 表、销毁 Session 重新登录三个步骤缺一不可。4.2 分页与模糊查询怎么组合参数透传和页码回退列表页同时承载分页和模糊查询最常见的问题是点翻页后搜索关键词丢了页面跳到第 2 页但数据变成全表第 21 到 30 条。我在 StudentServlet 里一般这样处理protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String keyword request.getParameter(keyword); if (keyword null) { keyword ; } int pageNum 1; String pageNumStr request.getParameter(pageNum); if (pageNumStr ! null !pageNumStr.isEmpty()) { pageNum Integer.parseInt(pageNumStr); } if (pageNum 1) { pageNum 1; } int pageSize 10; StudentService service new StudentService(); int total service.count(keyword); int totalPage (total pageSize - 1) / pageSize; if (pageNum totalPage totalPage 0) { pageNum totalPage; } int offset (pageNum - 1) * pageSize; ListStudent list service.search(keyword, offset, pageSize); PageBean pageBean new PageBean(); pageBean.setPageNum(pageNum); pageBean.setPageSize(pageSize); pageBean.setTotal(total); pageBean.setTotalPage(totalPage); pageBean.setKeyword(keyword); request.setAttribute(pageBean, pageBean); request.setAttribute(list, list); request.getRequestDispatcher(/student_list.jsp).forward(request, response); }这串代码里有三个边界处理是答辩加分点。第一是页码越界回退用户手动把 URL 里的 pageNum 改成 999程序要能兜住否则 LIMIT 990,10 查出来空列表页面上直接白表格第二是删除当前页最后一条数据后页码停留在空页上所以pageNum totalPage时要自动回退到最后一页第三是 keyword 必须放进 PageBean 并原样传回页面翻页链接才能把它拼回去。4.3 JSP 渲染EL 表达式、翻页链接和路径三座山JSP 页面最忌直接写 Java 脚本片段% %答辩被问「为什么页面里写 Java」会很掉分。用 JSTL 标签和 EL 表达式页面干净而且和 Servlet 传参天然配套。页面顶部的 taglib 引入和编码声明是隐形成本% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core %列表表格用 c:forEach 遍历翻页区域用 c:if 控制按钮可用状态c:forEach items${list} varstu tr td${stu.stuNo}/td td${stu.name}/td td${stu.gender 1 ? 男 : 女}/td td${stu.className}/td td${stu.phone}/td td a hrefedit?id${stu.id}编辑/a a hrefdelete?id${stu.id} onclickreturn confirm(确定删除?)删除/a /td /tr /c:forEach c:if test${pageBean.pageNum 1} a hreflist?pageNum${pageBean.pageNum - 1}keyword${pageBean.keyword}上一页/a /c:if span第 ${pageBean.pageNum} / ${pageBean.totalPage} 页/span c:if test${pageBean.pageNum pageBean.totalPage} a hreflist?pageNum${pageBean.pageNum 1}keyword${pageBean.keyword}下一页/a /c:if三目运算符在 EL 里直接写${cond ? 男 : 女}性别显示不需要 Java 代码。翻页链接的关键是 keyword 透传两个翻页按钮都必须带keyword并且用 c:if 把第一页的「上一页」和最后一页的「下一页」隐藏掉。JSP 里的资源引用要用${pageContext.request.contextPath}拼绝对路径比如${pageContext.request.contextPath}/css/style.css千万不要写死/css/style.css——项目部署时带上下文路径写死路径 404 是家常便饭。5. 避坑指南学生信息管理系统最常见的 5 个翻车现场5.1 表单提交后中文全部变成问号或乱码现象新增学生后数据库里姓名、班级字段读出来全是???或æå这类乱码英文和数字正常。原因分两层POST 请求时Servlet 默认按 ISO-8859-1 解码请求体Tomcat 8 之前尤其明显JSP 页面编码、数据库表字符集、JDBC URL 的 characterEncoding 三处不一致任何一处掉链子都乱。解决入口统一加一个 CharacterEncodingFilter在 doFilter 里先调用request.setCharacterEncoding(UTF-8)再放行同时保证 JSP 顶部contentTypetext/html;charsetUTF-8、数据库表用 utf8mb4、JDBC URL 带上characterEncodingutf8。GET 请求的 URL 参数乱码是另一条线Tomcat 8 之后的版本默认 URIEncoding 已是 UTF-8老版本要在 server.xml 的 Connector 上配URIEncodingUTF-8。5.2 MySQL 8 驱动连不上报「Unable to load authentication plugin」或时区错现象启动 Tomcat 后第一次访问列表页控制台报Public Key Retrieval is not allowed或The server time zone value异常。原因MySQL 8.x 的驱动从com.mysql.jdbc.Driver换成了com.mysql.cj.jdbc.Driver而且默认需要显式声明时区本地连接不走 SSL但驱动默认尝试用 SSL 协商。解决URL 上补全三个参数useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltruedriver 属性写com.mysql.cj.jdbc.Driver不要沿用旧驱动类名。这个坑不是代码逻辑错是环境参数错查错时先看连接串再看代码。5.3 页面能打开但 CSS 和 JS 全部失效控制台一堆 404现象登录页能显示但没有任何样式浏览器开发者工具里 css/style.css 请求状态是 404。原因JSP 里引用了绝对路径/css/style.css而项目部署到 Tomcat 后访问路径带了上下文名比如http://localhost:8080/stums/浏览器找的是http://localhost:8080/css/style.css自然 404。解决所有静态资源引用统一改为${pageContext.request.contextPath}/css/style.css或者在 JSP 的 head 里放一个 base 标签base href${pageContext.request.contextPath}/后面相对路径全部生效。后排错思路先看地址栏当前 URL 的上下文名再看请求资源的 URL 路径两者拼接就是浏览器实际请求地址。5.4 更新数据后没变化但也不报错事务提交和连接对象的问题现象页面上点击编辑、改成新值、提交跳回列表后数据还是旧的控制台没有任何异常。原因通常是两个如果手动调用了conn.setAutoCommit(false)之后忘了commit()数据在事务里没落库另一个更常见的是 try-with-resources 的关闭顺序Connection 先于 ResultSet 关闭或者复用了一个已经关闭的 ConnectionPreparedStatement 执行时静默失败。解决课设项目不要手动控制事务保持默认的 setAutoCommit(true) 即可每次操作从 DBUtil.getConnection() 现取连接用完即关在 DAO 的 catch 里把 e.printStackTrace() 打出来不要吞异常否则这种「不报错的故障」最难定位。排错时优先看数据库连接数有没有被泄漏连接不关会导致后续请求全部卡在获取连接上。5.5 翻页后数据对不上或者关键词丢失现象第 1 页搜「张」得到 3 条点下一页后出来 10 条不同的人或者直接跳到第 1 页。原因翻页链接没有拼 keyword 参数servlet 拿到的 keyword 为空条件查询退化成全表查询另一个隐蔽点是 offset 计算错误比如把(pageNum - 1) * pageSize写成了pageNum * pageSize导致第一页就跳过了前 10 条。解决翻页链接必须同时携带 pageNum 和 keyword并且 servlet 端对空 keyword 做默认值处理keyword null ? : keyword把 offset 换算统一收口在 servlet 或 PageBean 里不要散落多处。排错技巧在 servlet 里临时输出LIMIT ${offset}, ${size}和 keyword 值对照数据库实际数据条数验证逻辑比盯着页面猜快得多。6. 从能跑到能答辩验证码、导出 CSV 与框架改造清单答辩演示时有两个低成本功能能明显拉高完成度登录页加图形验证码列表页加导出数据。验证码不要自己造轮子直接用 hutool 的 CaptchaUtil三行代码出图LineCaptcha captcha CaptchaUtil.createLineCaptcha(120, 40); session.setAttribute(captchaCode, captcha.getCode()); captcha.write(response.getOutputStream());登录校验时把用户输入的验证码和 session 里的 captchaCode 比较忽略大小写用完后立即移除 session 中的验证码防止同一个验证码被重复提交。注意生成验证码的 Servlet 必须在登录过滤器里放行否则加载不到图片。导出数据用 CSV 比 POI 轻量得多不需要引入庞大的 Apache POI 依赖response.setContentType(text/csv;charsetUTF-8); response.setHeader(Content-Disposition, attachment;filenamestudents.csv); response.getWriter().write(\uFEFF); // 写入 BOM避免 Excel 打开中文乱码 // 然后按行输出标题和学生数据字段间用英文逗号分隔CSV 导出是很多企业报表的简化形态答辩时讲「为什么用 CSV 而不是 Excel」可以给出明确答案CSV 是纯文本格式无需额外库浏览器直接下载数据量大时性能开销远低于 POI 内存模型。如果后续要往企业开发方向走把经典三层换成 Spring Boot MyBatis 的改动清单其实很固定Servlet 换成Controller或RestControllerweb.xml 消失DBUtil 由 Druid 连接池和 MyBatis 配置取代JSP 换成 Thymeleaf 或者前后端分离返回 JSON。用 MyBatis-Plus 的话可以根据实体类的字段注解直接生成建表 SQL把第 2 章的手写建表脚本自动化。但换框架不换思路——分页、登录拦截、参数透传这些核心逻辑在经典写法里理解透了换框架只是把同一套思维搬运到新语法上。我当年交这个题目时最亏的一件事就是花了两周调一个好看的 Layui 前端模板却没把「删除最后一页最后一条数据后页码空翻」这个边界想清楚答辩现场被评委一个追问卡住。如果你只从这篇里带走一个习惯我希望是把每个功能的边界条件先写在注释里再动手写代码。工具会换代技术栈会变但这套从现实需求拆解到落地的思路不会过时希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑