简介:ACCP5.0第一学期结业项目是一份面向初学者的综合实践资源,以MyQQ在线聊天工具为主题,通过实现一个桌面社交应用,帮助学员掌握从界面设计到后台交互的完整开发流程。项目包含添加好友、在线聊天、更换头像、陌生人关系管理、个人资料修改等模块,涵盖了好友请求处理、即时消息发送、头像图片压缩存储以及权限访问设置等细节,覆盖数据库查询、网络通信与界面交互,有助于理解社交软件的工作原理。资源包共283个文件,大小7.78MB,以bmp图片素材和cs源代码为主体,同时有jpg、gif、ico图标,resx与resources配置,ssk皮肤,dll和exe程序以及mdf数据库文件,结构清晰,便于对照学习。目前已有178人学习下载。研读这套项目,不仅可获得完整结业作品和配套素材,还能从源码中梳理好友添加时的数据库操作、聊天窗口的消息收发与状态更新、头像上传时的图片压缩与裁剪等实现细节,对课程设计、结业答辩或二次开发均有参考价值。
1. ACCP5.0第一学期结业项目:别把它当成考试,当成一次交付演练
ACCP5.0第一学期结业项目,对很多刚从语法和SQL里爬出来的同学来说,是一道分水岭。有人拿着题目憋了半个月,最后界面能跑、数据能进,答辩时被问“为什么这张表要有两个主键”就卡住;也有人选了个最普通的“超市收银”,却把表结构、事务、权限设计讲得明明白白,最后成了优秀项目。差距不在代码量,而在你想没想清楚一件事:结业项目不是证明你会写代码,是证明你能把一个需求从数据库一路交付到用户能用的界面上。这篇文章就是按这条链路来拆:该做什么、代码怎么写、踩坑在哪、答辩怎么过。适合正在准备结业项目、又不想只靠复制别人源码混过去的同学。
2. 从题目到建库:先把分层思路立住,再决定写哪几张表
2.1 为什么结业项目十有八九选“信息管理系统”,又该怎么选
第一学期的课程范围决定了你能用什么技术:Java 基础语法、面向对象、集合框架、JDBC 访问数据库、Swing 写桌面界面,外加 SQL 的建库建表和增删改查。这个组合最匹配的项目形态就是单机版的管理信息系统。数据库常见用 MySQL,培训机构也可能要求 SQL Server,如果你所在环境是后者,把后面脚本里的方言替换一下即可。
选题目有一个很实际的原则:尽量选带“主表 + 明细表”结构的业务,而不是单表增删改查。比如图书管理系统容易退化成“一本书一行记录”,太单薄;学生成绩管理又容易写成一人一表,毫无关联。超市收银是很好的练手题,因为它天然存在两张核心表:销售单主表(一次结账产生一条)和销售明细表(这次结账买了哪些商品)。这两张表的关联、合计金额的计算、库存扣减,正好能体现你会不会做事务。类似的还有进销存、宾馆入住退房、药店销售。
我见过不少同学在选题上纠结半天,最后挑了一个“运动器材租借系统”,业务上只有租和还,事务关系不明显,答辩时老师都没什么可问的。你选超市收银或图书借阅这类有“订单头 + 订单明细”业务的题目,至少能从建表阶段讲出设计逻辑,后期扩展空间也大。
2.2 表结构设计:字段不是越少越好,是要能支撑“结业答辩被追问”
以超市收银系统为例,最小可用是四张表:用户表、商品表、销售单主表、销售明细表。很多同学会漏掉“商品分类表”,认为直接存个字符串“饮料”就完了。短期看能用,但答辩时一问“如果要把所有饮料类商品价格上调 10% 你怎么做”,你就只能写一条带 LIKE 的 SQL。加一张分类表,这件事就变成一次简单的 UPDATE。
下面是我建议的四表结构(MySQL 方言),字段名用下划线风格,这是主流实训代码的习惯,也方便与 Java 实体类字段映射:
CREATE DATABASE IF NOT EXISTS supermarket DEFAULT CHARACTER SET utf8mb4; USE supermarket; CREATE TABLE tb_user ( user_id INT PRIMARY KEY AUTO_INCREMENT, account VARCHAR(20) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, role VARCHAR(10) NOT NULL DEFAULT '收银员', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB; CREATE TABLE tb_category ( category_id INT PRIMARY KEY AUTO_INCREMENT, category_name VARCHAR(30) NOT NULL UNIQUE ) ENGINE=InnoDB; CREATE TABLE tb_product ( product_code VARCHAR(32) PRIMARY KEY, product_name VARCHAR(50) NOT NULL, category_id INT NOT NULL, purchase_price DECIMAL(10,2) NOT NULL, sale_price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0, status TINYINT NOT NULL DEFAULT 1, CONSTRAINT fk_product_category FOREIGN KEY (category_id) REFERENCES tb_category(category_id) ) ENGINE=InnoDB; CREATE TABLE tb_sale ( sale_no VARCHAR(20) PRIMARY KEY, operator_id INT NOT NULL, sale_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, total_amount DECIMAL(10,2) NOT NULL, pay_method VARCHAR(10) NOT NULL DEFAULT '现金', CONSTRAINT fk_sale_operator FOREIGN KEY (operator_id) REFERENCES tb_user(user_id) ) ENGINE=InnoDB; CREATE TABLE tb_sale_detail ( id BIGINT PRIMARY KEY AUTO_INCREMENT, sale_no VARCHAR(20) NOT NULL, product_code VARCHAR(32) NOT NULL, sale_price DECIMAL(10,2) NOT NULL, quantity INT NOT NULL, amount DECIMAL(10,2) NOT NULL, CONSTRAINT fk_detail_sale FOREIGN KEY (sale_no) REFERENCES tb_sale(sale_no), CONSTRAINT fk_detail_product FOREIGN KEY (product_code) REFERENCES tb_product(product_code) ) ENGINE=InnoDB;这里有几个字段设计上的用意,答辩时能说出来会很加分。商品表主键用的是业务字符串 product_code,而不是自增 ID,因为超市刷卡系统扫的是条形码,条形码本身就是唯一业务主键,直接在代码里以条形码判断商品是否存在,不用再查一次“where product_name = ?”。销售明细表保留了一个自增主键 id,虽然明细行本身可以通过 sale_no + product_code 唯一,但保留自增主键,是为了以后做退换货、按行号定位的时候省事。
2.3 初始数据与权限:一个默认账号就能启动,但角色别只写一个
建完库要插入启动数据,至少有一个管理员账号和几个商品分类。插入管理员密码时常见做法是先取 MD5 值再存入,不在代码里传明文。虽然第一学期不要求你掌握加密算法,但答辩时提一句“密码不落明文,是项目里该有的安全意识”会显得你考虑过安全问题。
INSERT INTO tb_user (account, password, role) VALUES ('admin', MD5('123456'), '管理员'), ('cashier01', MD5('123456'), '收银员'); INSERT INTO tb_category (category_name) VALUES ('饮料'), ('零食'), ('日用品'), ('生鲜'); INSERT INTO tb_product (product_code, product_name, category_id, purchase_price, sale_price, stock) VALUES ('690000000001', '矿泉水 550ml', 1, 1.20, 2.00, 200), ('690000000002', '薯片 原味', 2, 4.50, 6.50, 80), ('690000000003', '抽纸 3层', 3, 9.00, 12.90, 50);角色字段只写“管理员 / 收银员”两档,已经符合第一学期难度。你可以顺手做一个最小权限判断:管理员能看到“商品入库和修改价格”的按钮,收银员登录只看到销售界面。Swing 里就是button.setVisible(user.getRole().equals("管理员"))这么简单,但功能权限的概念就落地了。
3. 用 JDBC + DAO 把系统跑通:三层代码分清楚,比把代码写对更重要
3.1 两包三层:实体类、DAO、Service 和界面的分工
我见过的结业项目代码,最容易出现的问题是“万能类”:一个类里既有数据库连接,又有界面监听器,还有计算逻辑。代码确实能跑,可答辩时如果你说“这是三层架构”,老师一打开类文件看到全部揉在一起,印象分会掉很多。
常见做法是分成两个包、三个层次:
entity包放实体类,与表结构一一对应,字段格式保持驼峰命名;dao包只负责 SQL 执行和结果集转对象;service包负责业务规则,比如计算总金额、判断库存、事务控制;- 界面层单独放
view包,Swing 组件里不出现 SQL。
第一学期项目体量不大,这套分法不会增加多少文件,却能让你在答辩时说清楚每个类的作用。类之间的关系一句话就能讲明白:界面调用 service,service 调 dao,dao 负责访问数据库。
3.2 一个 BaseDao:把连接参数集中管理,连接开关别到处写
数据库连接配置是重复代码最多的地方。一个 BaseDao 类足够支撑整个第一学期项目,不要复制连接代码到处贴。我看过有同学的登录类里一份连接配置,商品查询类里又一份,改一次数据库密码要改八个文件,这就是连接管理不集中的后果。
package util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class BaseDao { private static final String URL = "jdbc:mysql://localhost:3306/supermarket?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; private static final String USER = "root"; private static final String PASSWORD = "123456"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new RuntimeException("MySQL 驱动加载失败,请检查 jar 包是否引入", e); } } protected Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }连接 URL 里三个参数必须配置,否则后面一定会踩坑。serverTimezone=Asia/Shanghai解决日期时间回显偏差;characterEncoding=utf8解决中文乱码;useSSL=false是因为实训环境一般没有配置 SSL 证书,强制校验会连不上。这里的用户名密码是写死在类里的,属于实训项目的常见做法,生产代码应该放到配置文件或环境变量里,答辩时可以主动说明你知道这个区别。
3.3 登录 DAO:用 PreparedStatement 而不是拼接 SQL
登录功能的 DAO 方法,是演示 PreparedStatement 最好的地方。拼 SQL 的方式一个典型反例是SELECT * FROM tb_user WHERE account = '" + account + "' AND password = '" + password + "',账号里带个单引号就能让语句直接报错,甚至改变查询逻辑。PreparedStatement 的意义不只是防 SQL 注入,它还帮你处理了字符串转义和数据类型绑定。
package dao; import entity.User; import util.BaseDao; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; public class UserDao extends BaseDao { public User login(String account, String passwordMd5) { String sql = "SELECT user_id, account, role FROM tb_user WHERE account = ? AND password = ?"; try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, account); ps.setString(2, passwordMd5); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { User u = new User(); u.setUserId(rs.getInt("user_id")); u.setAccount(rs.getString("account")); u.setRole(rs.getString("role")); return u; } } return null; } catch (Exception e) { e.printStackTrace(); return null; } } }这段代码体现三个习惯。try-with-resources确保 Connection、PreparedStatement、ResultSet 在方法结束后自动关闭,不会因为忘记关闭连接导致“连接数耗尽”;参数通过setString绑定,而不是拼进 SQL 字符串;返回 null 表示账号或密码错误,调用方只需判断空值即可。
3.4 销售下单的 Service:事务把“主表、明细、库存”绑成一次操作
收银系统最核心的业务是一张销售单的保存:往tb_sale插主表记录、往tb_sale_detail插多条明细、再把tb_product.stock减掉。这三步任何一步失败,都不该留下半截数据。如果销售单保存成功但库存没减,盘点时就对不上;如果明细没插进去但主表存在,合计金额就变成空数据。
package service; import util.BaseDao; import java.math.BigDecimal; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; public class SaleService extends BaseDao { public void createSale(String saleNo, int operatorId, BigDecimal totalAmount, String payMethod, String[] productCodes, int[] quantities, BigDecimal[] prices) throws SQLException { Connection conn = null; try { conn = getConnection(); conn.setAutoCommit(false); String insertSale = "INSERT INTO tb_sale (sale_no, operator_id, total_amount, pay_method) VALUES (?, ?, ?, ?)"; try (PreparedStatement psSale = conn.prepareStatement(insertSale)) { psSale.setString(1, saleNo); psSale.setInt(2, operatorId); psSale.setBigDecimal(3, totalAmount); psSale.setString(4, payMethod); psSale.executeUpdate(); } String insertDetail = "INSERT INTO tb_sale_detail (sale_no, product_code, sale_price, quantity, amount) VALUES (?, ?, ?, ?, ?)"; try (PreparedStatement psDetail = conn.prepareStatement(insertDetail)) { for (int i = 0; i < productCodes.length; i++) { psDetail.setString(1, saleNo); psDetail.setString(2, productCodes[i]); psDetail.setBigDecimal(3, prices[i]); psDetail.setInt(4, quantities[i]); psDetail.setBigDecimal(5, prices[i].multiply(BigDecimal.valueOf(quantities[i]))); psDetail.addBatch(); } psDetail.executeBatch(); } String updateStock = "UPDATE tb_product SET stock = stock - ? WHERE product_code = ? AND stock >= ?"; try (PreparedStatement psStock = conn.prepareStatement(updateStock)) { for (int i = 0; i < productCodes.length; i++) { psStock.setInt(1, quantities[i]); psStock.setString(2, productCodes[i]); psStock.setInt(3, quantities[i]); int rows = psStock.executeUpdate(); if (rows == 0) { throw new SQLException("商品 " + productCodes[i] + " 库存不足"); } } } conn.commit(); } catch (SQLException e) { if (conn != null) { conn.rollback(); } throw e; } finally { if (conn != null) { conn.setAutoCommit(true); conn.close(); } } } }事务的关键在setAutoCommit(false)、commit()、rollback()这三行。executeBatch()用来批量插入明细,避免一条条执行几百遍,数据库交互次数从 N 次降成 1 次。库存更新时的AND stock >= ?是一个数据库层面的保护条件,即使界面已经校验过库存,并发情况下也能防止库存变负数。第一学期项目里没有并发,但你能写出这个防御条件,说明你想到了数据竞争。
4. 结业项目避坑:五个让答辩现场当场翻车的真实案例
4.1 现象:运行时报“ClassNotFoundException: com.mysql.cj.jdbc.Driver”
某同学在自己电脑上把项目跑通了,到答辩机房一运行直接报驱动类找不到。原因往往是 IDEA 里编译时能看到 jar 包,但打包输出时没有把 MySQL 驱动带进运行环境,或者项目里添加的还是旧的com.mysql.jdbc.Driver。
解决分两步。先把驱动统一成 MySQL Connector/J 8.x,代码里写com.mysql.cj.jdbc.Driver;然后在 IDEA 里检查 File -> Project Structure -> Artifacts,确认驱动 jar 被放进了 Output Layout 的 lib 目录。最省事的办法是用 Maven/Gradle 管理依赖,但第一学期很多同学还不熟构建工具,那就手动把 mysql-connector-java 的 jar 复制到项目根目录下的lib文件夹,并在 Project Structure 里 Add as Library。答辩机如果没网,不要现场下载依赖,提前把带 lib 的完整项目拷到 U 盘。
4.2 现象:登录界面上输入中文用户名能查到数据,但新增商品的中文名保存后全是“??”
数据库能建库,说明 SQL 基本语法通了,但中文变问号是字符集没对齐。原因有三层:MySQL 库表默认字符集不是 utf8mb4、连接 URL 没指定characterEncoding=utf8、页面 Swing 的 JTextField 本身没有转码问题,问题基本集中在前面两层。
解决:建库脚本里明确写DEFAULT CHARACTER SET utf8mb4,连接 URL 带characterEncoding=utf8。如果你已经建好库,用ALTER DATABASE supermarket CHARACTER SET utf8mb4;重建,表也需要ALTER TABLE tb_product CONVERT TO CHARACTER SET utf8mb4;。还有一层容易漏:MySQL 服务端配置文件my.ini里的character_set_server如果不是 utf8mb4,新库可能仍然继承旧默认值,可以在连接后执行SHOW VARIABLES LIKE 'character%'检查。
4.3 现象:销售单的下单时间显示比北京时间快了 8 个小时
数据库里存的是正确时间,界面上却多了 8 小时。这是serverTimezone参数写成了 UTC。MySQL 驱动把数据库的 TIMESTAMP 值按服务器时区读取,再按 Java 虚拟机的TimeZone.getDefault()转成本地时间,UTC 跟北京时间差整整一个时区。
解决:连接 URL 里固定写serverTimezone=Asia/Shanghai,不要写UTC,也不要不写。新的 MySQL 驱动对不写该参数的情况会直接抛异常不让连接。另外有个更好的习惯:这张表的时间字段用DATETIME而不是TIMESTAMP,DATETIME 不参与时区换算,存的是什么样取回来就是什么样。Java 侧用LocalDateTime接收,不要用Timestamp,能少掉一组老 API 的时区坑。
4.4 现象:按商品名模糊查询,输入“饮料”能查出来,输入“饮’料”直接把系统弄崩了
这属于典型 SQL 拼接事故。用户的输入直接拼进 SQL,单引号把语句截断,要么报语法错误,要么变成一条你预期之外的查询。第一学期的课程可能还没系统讲安全攻击,但结业答辩老师一定喜欢问这句:如果用户在查询框里输入'; DROP TABLE tb_product; --会发生什么?
解决只有一个标准答案:所有 SQL 参数一律走PreparedStatement。模糊查询的写法是ps.setString(1, "%" + keyword + "%"),通配符拼接发生在 Java 侧,SQL 语句本身保持LIKE ?的结构。商品编号精确查询、登录账号查询同理。养成“只要 SQL 里有动态值就使用?占位”这个习惯,比背十遍注入原理都管用。
4.5 现象:点击“查询销售单”后界面白屏,窗口标题出现“未响应”
Swing 是单线程模型,所有界面更新和事件处理都在事件分发线程 EDT 上执行。你在按钮的 ActionListener 里直接访问数据库,数据库查询如果耗时 2 秒,整个窗口就冻结 2 秒;如果数据库连接超时,界面直接卡到人以为死机。这个体验在答辩演示时非常致命,因为评审老师可能就站在旁边盯着转圈。
解决:把耗时操作放进SwingWorker后台线程,查询结束后再回到 EDT 更新界面组件。一个最小模式是SwingWorker<List<Product>, Void>,在doInBackground()里执行 DAO 查询,在done()里把结果填充到 JTable。我一般会在连接超市的演示项目里写一个通用的 Worker,查询按钮只负责worker.execute(),界面立刻反馈“正在查询”,查完再刷新表格。代码量增加不多,但“未响应”消失,答辩观感完全不同。
5. 答辩前自测:把边界场景和 SQL 查询过一遍再交
5.1 结业答辩老师最常追问的三类问题,以及你的标准答案
你一定猜得到,老师不会只看功能跑不跑,他要在十分钟内判断这是不是你自己写的。最常问的三类问题其实有固定套路。
第一类:表设计。问“为什么商品表用条形码当主键,销售明细表却要自增 id?”,你要回答条形码是业务唯一标识,退出收银场景也能用,明细自增 id 是为了行级操作和以后对接退换货。第二类:事务。问“库存扣减失败了会怎样”,你要能说出 Service 层事务回滚的完整链路,并指出库存更新里加了stock >= ?的条件。第三类:性能。问“商品几百条时怎么保证查询不慢”,你要引出索引和分页。可以把建索引的 SQL 放在项目里:
CREATE INDEX idx_sale_time ON tb_sale(sale_time); CREATE INDEX idx_detail_sale_no ON tb_sale_detail(sale_no);查询优化先看EXPLAIN,这里放一条常用查询演示怎么定位慢查询:
EXPLAIN SELECT s.sale_no, s.sale_time, u.account, s.total_amount FROM tb_sale s JOIN tb_user u ON s.operator_id = u.user_id WHERE s.sale_time >= '2024-06-01' ORDER BY s.sale_time DESC;如果type是 ALL 或者 rows 很大,说明索引没生效,检查一下 JOIN 字段是否有索引。这个解释比说“我加了索引所以快”有说服力得多。
5.2 交之前至少过一遍的验收清单
我见过太多项目交上去才发现登录框能按回车、商品编号输了个负数也能入库。结业项目不需要多复杂,但边界条件一定要干净。把下面这张表当成你提交前的自测单,每一项都用真实操作验证一遍:
| 功能模块 | 测试操作 | 预期结果 |
|---|---|---|
| 登录 | 错误密码输入 3 次 | 提示密码错误,不抛出异常 |
| 商品管理 | 新增重复条形码商品 | 提示“该商品已存在” |
| 商品管理 | 库存输入负数 | 拒绝保存或自动归零 |
| 商品管理 | 删除被销售明细引用的商品 | 不报外键错误,给出提示 |
| 收银下单 | 不选商品直接结账 | 销售单无法保存 |
| 收银下单 | 商品库存 0 时下单 | 提示库存不足,且主表无残留数据 |
| 报表查询 | 查询日期范围反向(开始晚于结束) | 自动交换日期或给出提示 |
| 权限 | 收银员登录后看到“商品管理”按钮 | 按钮隐藏或禁用 |
这套用例也是你答辩演示的脚本。演示的时候不要只走“正常流程”,主动做一次库存不足的尝试,反而能让老师看到你处理异常的经验。
5.3 一个能加分的统计功能:当日销售额报表
如果你有余力,不要去做花哨的图表,先加一个最实际的管理统计:按日汇总销售额。SQL 核心语句只占四五行,但功能价值直接翻倍。
SELECT DATE_FORMAT(sale_time, '%Y-%m-%d') AS sale_day, COUNT(*) AS order_count, SUM(total_amount) AS day_total FROM tb_sale WHERE operator_id = ? GROUP BY sale_day ORDER BY sale_day DESC LIMIT 10;JTable 展示结果就行,不需要引入任何报表框架。答辩时这段代码可以完整讲出分组聚合和日期处理,比堆十个没用的按钮强。
6. 用连接池和分页把老项目变成进阶作品
如果你还有两天时间,最值得做的一件事是给数据库访问层换上一个小连接池。第一学期教材里通常教DriverManager.getConnection(),但静态页面和 JDBC 直连存在一个明显的问题:每次点击查询都重连数据库,桌面程序不明显,一旦以后接 Web 就暴露。
常见做法是引入 HikariCP,配置一个全局数据源,DAO 从数据源拿连接而不是直接 DriverManager 创建:
package util; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; public class DbPool { private static HikariDataSource dataSource; static { HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:mysql://localhost:3306/supermarket?serverTimezone=Asia/Shanghai&characterEncoding=utf8"); config.setUsername("root"); config.setPassword("123456"); config.setMaximumPoolSize(10); config.setMinimumIdle(2); dataSource = new HikariDataSource(config); } public static java.sql.Connection getConnection() throws java.sql.SQLException { return dataSource.getConnection(); } }连接池的几个参数值得在答辩时解释:maximumPoolSize控制最大连接数,桌面程序设 10 已经足够;minimumIdle是保持的空闲连接。注意连接池拿到的连接用完一定要close(),否则会耗尽池子里的连接,这是 JDBC 直连时代不太容易犯、接入了连接池反而更容易出的毛病。
另一个低成本优化是商品列表分页。给商品查询 DAO 加上limit参数,只查当前页的数据,再返回总数,界面上一页 20 条。代码如下:
public List<Product> listProducts(int page, int pageSize) { String sql = "SELECT p.*, c.category_name FROM tb_product p " + "JOIN tb_category c ON p.category_id = c.category_id " + "ORDER BY p.product_code LIMIT ? OFFSET ?"; try (Connection conn = DbPool.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, pageSize); ps.setInt(2, (page - 1) * pageSize); // 执行查询并转换结果集 } // 省略异常处理与结果封装 }OFFSET的语义是跳过前 N 条,第 1 页 OFFSET 0,第 2 页 OFFSET pageSize。这个方法不需要复杂封装,但能把项目从“一次性加载全部数据”升级成“可扩展的列表查询”。答辩老师问“如果商品有 10 万条,你的系统还扛得住吗”,你就可以指着这段代码回答。
最后说说我的习惯:做结业项目时,我每周固定留出半天只用来“搞坏系统”——故意输错参数、断掉数据库、重复快速点击按钮,把翻车现场截图下来,再一个个修。结业项目本身不是为了拿满分,是为了让你在无人庇护的现场也能把一件事从头讲到尾。如果你现在卡在某个报错上,先按上面第 4 章的五个场景排查一遍,80% 的坑都在那里等着你。希望帮到你。
本文还有配套的精品资源,点击获取