简介:面向课程设计与毕业设计场景的JavaEE网上书店项目,提供完整源代码与MySQL建库脚本,适合需要完成Web课程作业、理解企业级应用分层与全栈流程的学生参考。压缩包共88个文件,约519KB,以40个JSP页面、11个Java源文件及11个class文件为主体,并配有CSS样式、图片素材、XML配置和SQL脚本,分别承担页面展示、业务逻辑、资源支撑与数据库初始化等工作。项目围绕用户注册登录、图书展示与搜索、购物车、订单处理及后台管理等模块展开,读者可结合Servlet、JSP、JPA等技术点,梳理请求处理、数据持久化与权限控制思路,并据此搭建可运行环境。项目还涉及依赖管理、单元测试与第三方支付集成等拓展方向,对理解JavaEE架构、数据库表结构设计与后台权限区分具有参考价值。已有3142人学习,可作为课程设计原型或毕业设计基础工程,便于二次开发与功能扩展。
1. 从“能登录”到“能下单”这段最难啃的链路
做网上书店的课程设计,大部分人会卡在第三步:需求写了三页,表建了十张,图书列表也跑出来了,结果购物车一刷新就清空,下单那段事务一跑就抛异常。这套基于 JavaEE 的网上书店项目(完整代码 + sql 文件)要解决的就是从“能登录”到“能下单”这段最短也最难的链路——图书分类浏览、关键词检索、购物车增减、订单生成、后台图书与订单管理,全部落在同一套 MySQL 表结构里打通。它面向的是要交一份完整毕业设计或课程设计的同学,也适合想拿一套标准三层架构当脚手架、往上叠功能的开发者。下面按需求、选型、库表、模块、避坑、验收的顺序,把这套东西拆开讲。
2. 需求拆解与技术选型:三层架构下六个模块的职责边界
2.1 业务需求:把书店压成一条最短闭环
网上书店的业务看着杂,真正要跑通的只有一条闭环:游客能按分类或关键词找到书,登录用户能把书加进购物车,结算时填一个收货地址,系统生成订单并扣减库存,管理员在后台能看到这笔订单并把状态往前推。围绕这条闭环往外扩,才轮到收藏、评论、销量排序、订单取消这些加分项。
前台模块的划分通常是这样:用户模块负责注册、登录、退出和地址簿;图书模块负责分类导航、列表分页、详情展示;购物车模块负责加入、改数量、删除、勾选结算;订单模块负责提交、查询、取消。后台模块则是图书上下架、分类维护、订单发货、用户禁用。两边共用同一套实体和 DAO,只是控制器和页面不同。
需要注意的是,课设和毕设的评分点集中在“业务逻辑是否闭环”,而不是功能数量。宁可把下单扣库存这一条路走扎实,也不要把收藏、积分、优惠券全堆上去,最后每一块都只到列表页。
下面这张表是我一般会写进开题报告里的模块职责划分,直接拿去改改就能用:
| 模块 | 主要职责 | 关键接口 | 对应页面 |
|---|---|---|---|
| 用户 | 注册登录、密码校验、地址增删改 | /user/login、/user/register | login.jsp、register.jsp |
| 图书 | 分类导航、多条件检索、分页、详情 | /book/list、/book/detail | book_list.jsp、book_detail.jsp |
| 购物车 | 加入、改数量、删除、合计金额 | /cart/add、/cart/update | cart.jsp |
| 订单 | 结算、生成订单、查询、取消 | /order/submit、/order/list | order_confirm.jsp、order_list.jsp |
| 后台图书 | 增删改查、上下架、库存调整 | /admin/book/save | admin/book_edit.jsp |
| 后台订单 | 订单列表、发货、关闭 | /admin/order/ship | admin/order_list.jsp |
2.2 技术选型:Servlet + JSP + JDBC 这套“老组合”为什么反而最稳
现在一提 Java Web,很多人第一反应是 Spring Boot 一把梭。放到课程设计这个场景里,我反而不推荐。原因很实际:答辩老师要看你写的代码里有没有分层、有没有事务、有没有过滤器,Spring Boot 的自动配置把很多东西藏进了注解和 starter 里,你讲不出“连接是谁关的”“事务是谁提交的”,反而容易被追问。
这套项目用的是 Servlet + JSP + JDBC + JSTL 的经典组合,通常的做法是:JDK 8 及以上、Tomcat 8.5 或 9、MySQL 5.7 或 8.0、Druid 或 C3P0 连接池、JSTL 1.2 做页面循环和格式化。依赖少,war 包丢进 webapps 就能跑,不需要额外装什么运行时。血泪经验是,课设环境经常是机房老机器,依赖越少,部署当天翻车的概率越低。
分层的职责要划清楚,不然写到后面就是 Servlet 里塞 SQL:
- entity(实体层):和数据库表一一对应的 POJO,只放字段、getter/setter,不放业务逻辑。
- dao(数据访问层):只做 CRUD,一个方法对应一条或一组 SQL,不判断业务规则。
- service(业务层):事务边界在这里,下单、扣库存、清购物车必须写在同一个方法里。
- controller(控制层):Servlet 只负责收参数、调 service、跳页面,不写 SQL 也不写业务判断。
- util(工具层):连接池、MD5、分页封装、日期处理。
判断自己有没有分错层,有个土办法:把 dao 层整个删掉,service 层应该编译不过;把 controller 层删掉,service 层应该还能单独跑单元测试。做不到这两点,说明业务逻辑漏到别处去了。
2.3 包结构与请求流转:一次“加入购物车”到底经过了谁
包结构建议按层划分而不是按功能划分,因为课设代码量不大,按层更好讲。目录大致长这样,每一层的作用我写在注释里:
src/main/java/com/bookstore ├── entity # 实体类:Book、User、Order、OrderItem、CartItem ├── dao # 接口 + impl,只写 SQL,不写业务判断 │ ├── BookDao.java │ ├── OrderDao.java │ └── impl/BookDaoImpl.java ├── service # 业务与事务边界,下单在这里提交/回滚 ├── controller # Servlet,收参 → 调 service → 转发或重定向 ├── filter # 编码过滤器、登录拦截过滤器 └── util # DBUtil 连接池、MD5Util、PageBean 分页封装 src/main/webapp ├── WEB-INF/views # JSP 放这里,防止被直接访问 ├── static # css、js、图书封面图 └── WEB-INF/web.xml一次“加入购物车”的完整链路是这样的:浏览器 POST 到/cart/add,编码过滤器先把request.setCharacterEncoding("UTF-8")设好,登录拦截过滤器检查 session 里有没有loginUser,没有就重定向到登录页;CartServlet 拿到bookId和quantity,做非空和正整数校验,调 CartService;Service 先查库存够不够,再决定是 insert 还是 update 已有记录;最后 Servlet 重定向回/cart/list,而不是直接 forward,避免刷新页面重复提交。
提示:列表页跳转用重定向、表单回显用转发,这条规矩能省掉一大半“刷新重复提交”的问题。
3. 数据库设计:七张表撑起图书、购物车与订单主链路
3.1 核心表结构:字段、索引与注释一次到位
库表是这个项目的地基,sql 文件里通常包含建库、建表、初始化数据三段,导入顺序不能乱。下面给出最关键的几张表,字段名和注释都保留了,改改就能用。
CREATE DATABASE IF NOT EXISTS bookstore DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci; USE bookstore; -- 用户表:role 区分前后台,不单独建管理员表 CREATE TABLE t_user ( id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', username VARCHAR(32) NOT NULL COMMENT '登录名', password CHAR(32) NOT NULL COMMENT 'MD5 摘要,不存明文', nickname VARCHAR(32) DEFAULT NULL COMMENT '昵称', phone VARCHAR(20) DEFAULT NULL, role TINYINT NOT NULL DEFAULT 0 COMMENT '0-普通用户 1-管理员', status TINYINT NOT NULL DEFAULT 1 COMMENT '1-正常 0-禁用', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; -- 图书表:discount_price 为空表示不打折,检索时用 IFNULL 兜底 CREATE TABLE t_book ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, title VARCHAR(128) NOT NULL COMMENT '书名', author VARCHAR(64) NOT NULL COMMENT '作者', publisher VARCHAR(64) DEFAULT NULL, isbn VARCHAR(20) DEFAULT NULL, category_id INT UNSIGNED NOT NULL COMMENT '所属分类', price DECIMAL(10,2) NOT NULL COMMENT '定价', discount_price DECIMAL(10,2) DEFAULT NULL COMMENT '折后价,NULL 表示无折扣', stock INT NOT NULL DEFAULT 0 COMMENT '库存', sales INT NOT NULL DEFAULT 0 COMMENT '销量,用于排序', cover_img VARCHAR(255) DEFAULT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT '1-上架 0-下架', PRIMARY KEY (id), KEY idx_category (category_id), KEY idx_title (title) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='图书表'; -- 订单表:金额与状态放主表,明细放子表 CREATE TABLE t_order ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT '业务单号,非自增主键', user_id INT UNSIGNED NOT NULL, total_amount DECIMAL(10,2) NOT NULL COMMENT '订单总额', receiver VARCHAR(32) NOT NULL COMMENT '收货人', phone VARCHAR(20) NOT NULL, address VARCHAR(255) NOT NULL, order_status TINYINT NOT NULL DEFAULT 0 COMMENT '0-待付款 1-已付款 2-已发货 3-已完成 4-已取消', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user (user_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单主表'; -- 订单明细:书名和单价冗余存一份,做历史快照 CREATE TABLE t_order_item ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, order_id INT UNSIGNED NOT NULL, book_id INT UNSIGNED NOT NULL, book_title VARCHAR(128) NOT NULL COMMENT '下单时的书名快照', price DECIMAL(10,2) NOT NULL COMMENT '下单时的单价快照', quantity INT NOT NULL, PRIMARY KEY (id), KEY idx_order (order_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单明细表';购物车表t_cart、分类表t_category、地址表t_address的结构同理,字段不多,重点是购物车表要有uk_user_book (user_id, book_id)这个唯一索引,靠它把“重复加同一本书”变成一次 update 而不是插两条。
索引这块,课设里最常见的错误是只建主键。t_book.title上有 LIKE 查询、t_order.user_id上有“我的订单”查询,这两个字段不加索引,数据量一过两千条页面就开始转圈。
3.2 订单与订单明细为什么必须拆两张表
经常有人问,订单里就买两三本书,为什么不能直接在订单表里塞一个book_ids字段,用逗号隔开?能跑,但答辩时会被问到答不上来。
拆表的核心理由是“一个订单对应多个商品”是一对多关系,塞逗号串等于把关系模型压平成字符串,之后就做不了三件事:按图书统计销量、按明细退款、按商品查历史订单。更关键的是快照问题——图书的价格和书名是会变的,如果明细表只存book_id,用户三个月后翻订单,看到的会是当时的订单金额配上现在的书名和现价,对不上账。所以明细表里冗余book_title和price两个字段,是刻意为之,不是设计缺陷。
order_no也值得单独说。主键id用自增,但对外展示和查询用order_no,通常是“年月日时分秒 + 四位随机数”拼出来的字符串。这样做的好处是:不暴露订单总量、方便按单号检索、将来分库也不用改主键策略。
3.3 sql 文件的导入顺序与初始数据
sql 文件导入失败的九成原因是顺序和字符集。正确的顺序是:先建库并USE,再建表(如果有外键依赖,被依赖的表先建),最后插数据。字符集统一用utf8mb4,别用utf8,后者在 MySQL 里其实是三字节的 utf8mb3,存 emoji 或者某些生僻字会报错。
初始数据一般准备三类:一条管理员账号(密码存 MD5 摘要,登录时前端传明文、后端摘要比对)、五到八个图书分类、二十到三十本图书。图书数据不用手写,用INSERT INTO ... VALUES (...),(...),(...)批量插,一条语句搞定,效率比一条条 insert 高一个数量级。
注意:导入前先确认 MySQL 的
sql_mode里有没有STRICT_TRANS_TABLES。开了严格模式,缺字段的非空列会直接报错而不是给默认值,很多人在自己机器上能导入、换台机器就失败,就是差在这里。
4. 核心模块实现:图书分页检索、购物车与下单事务
4.1 图书分页与多条件检索:一条 SQL 兼顾关键词和分类
分页最容易写崩的地方是“动态条件拼装 + 总条数统计”这两段。前者容易漏掉空格导致 SQL 语法错误,后者容易和列表查询的条件不一致,导致总页数算错。我的做法是把条件抽成一个私有方法,列表查询和计数查询共用。
public class BookDaoImpl implements BookDao { /** 拼接 where 条件,列表查询和 count 查询共用同一段,避免条件不一致 */ private String buildWhere(String keyword, Integer categoryId, List<Object> params) { StringBuilder where = new StringBuilder(" WHERE status = 1 "); if (StringUtils.isNotBlank(keyword)) { where.append(" AND (title LIKE ? OR author LIKE ?) "); params.add("%" + keyword.trim() + "%"); params.add("%" + keyword.trim() + "%"); } if (categoryId != null && categoryId > 0) { where.append(" AND category_id = ? "); params.add(categoryId); } return where.toString(); } @Override public List<Book> findByPage(String keyword, Integer categoryId, int offset, int pageSize) { List<Object> params = new ArrayList<>(); // IFNULL 兜底:没有折后价时按定价展示,前端不用再判空 String sql = "SELECT id, title, author, price, IFNULL(discount_price, price) AS salePrice," + " stock, cover_img FROM t_book " + buildWhere(keyword, categoryId, params) + " ORDER BY sales DESC, id DESC LIMIT ?, ?"; params.add(offset); params.add(pageSize); return DBUtil.queryList(sql, params, Book.class); } @Override public int countByCondition(String keyword, Integer categoryId) { List<Object> params = new ArrayList<>(); String sql = "SELECT COUNT(*) FROM t_book " + buildWhere(keyword, categoryId, params); return DBUtil.queryCount(sql, params); } }几个参数得说清楚。offset是(pageNo - 1) * pageSize,PageBean 里算好再传进来,DAO 不关心当前是第几页。pageSize一般固定 10 或 12,跟着列表页的栅格走,别做成用户可调,课设里没必要。ORDER BY sales DESC, id DESC的后半段是必须的,否则销量相同的书在翻页时顺序会抖动,出现“第二页又看到第一页的书”这种玄学现象。
DBUtil.queryList内部用的是PreparedStatement,参数按顺序setObject,这套写法防注入,也避免了拼接字符串时引号处理出错。
4.2 购物车:Session 存还是数据库存,别两头都做
购物车有两种存法,各有取舍。Session 存法把Map<Integer, CartItem>挂在会话上,好处是不用建表、响应快,坏处是换浏览器就没了、服务器重启就丢了。数据库存法落地到t_cart表,好处是能跨设备、能统计弃购,坏处是每次加减都要读写库。
我的建议是:登录用户走数据库,未登录游客走 Session,登录成功后做一次合并。这个逻辑写在登录的 service 里,别散落在各处。合并的规则很简单——同book_id的数量相加,但不超过库存。
@Override public void mergeSessionCart(int userId, Map<Integer, CartItem> sessionCart) { if (sessionCart == null || sessionCart.isEmpty()) return; for (CartItem item : sessionCart.values()) { // 先查库里有没有这条记录,有就累加,没有就新增 CartItem exist = cartDao.findByUserAndBook(userId, item.getBookId()); if (exist == null) { item.setUserId(userId); cartDao.insert(item); } else { int total = exist.getQuantity() + item.getQuantity(); // 上限锁死在库存,防止购物车里出现买不到的数量 exist.setQuantity(Math.min(total, item.getStock())); cartDao.updateQuantity(exist.getId(), exist.getQuantity()); } } sessionCart.clear(); // 合并完清空,避免下次登录重复累加 }最后那行sessionCart.clear()是个容易被忘掉的细节,忘了就会出现“退出再登录,购物车数量翻倍”的怪事。
4.3 下单:一个事务里必须做完的四件事
下单是整个项目里唯一必须用事务的地方。一个订单提交成功,意味着四件事同时成立:写入订单主表、写入订单明细、扣减库存、清空已下单的购物车记录。任何一步失败,前面做的都要撤回,否则就会出现“扣了库存没生成订单”或者“生成了订单没扣库存”的脏数据。
public String submitOrder(int userId, List<CartItem> items, Address addr) throws SQLException { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务,关掉自动提交 // 1. 校验库存,任意一本不足就整体失败(宁可全失败,不要部分成功) for (CartItem item : items) { int stock = bookDao.getStockForUpdate(conn, item.getBookId()); // SELECT ... FOR UPDATE if (stock < item.getQuantity()) { throw new BizException("《" + item.getTitle() + "》库存不足"); } } // 2. 写订单主表,order_no 用时间戳 + 随机数生成 String orderNo = OrderNoUtil.generate(); int orderId = orderDao.insertOrder(conn, orderNo, userId, calcTotal(items), addr); // 3. 逐条写明细并扣库存,扣减用 stock = stock - ? 而不是先查后改 for (CartItem item : items) { orderItemDao.insert(conn, orderId, item); bookDao.decreaseStock(conn, item.getBookId(), item.getQuantity()); } // 4. 清掉已下单的购物车记录 cartDao.deleteByIds(conn, items.stream().map(CartItem::getId).collect(Collectors.toList())); conn.commit(); // 四步全过才提交 return orderNo; } catch (Exception e) { if (conn != null) conn.rollback(); // 任何一步异常,整体回滚 throw new BizException("下单失败:" + e.getMessage()); } finally { if (conn != null) { conn.setAutoCommit(true); // 归还连接池前恢复现场 conn.close(); } } }几个设计点值得单独讲。getStockForUpdate用的是SELECT ... FOR UPDATE,在事务里给这一行加排他锁,防止两个用户同时下单把库存扣成负数,这是并发场景下最省事的做法。扣减库存的 SQL 写成UPDATE t_book SET stock = stock - ? WHERE id = ? AND stock >= ?,把判断和扣减合成一条原子语句,比“先查再改”稳得多。finally里恢复autoCommit那一行千万别省,连接池是复用连接的,不恢复现场,下一个拿到这条连接的请求会莫名其妙地不提交事务。
4.4 编码过滤器与登录拦截:两个 Filter 管住全局
中文乱码和越权访问,是课设里最高频的两个问题,都能用一个 Filter 解决掉,不用在每个 Servlet 里重复写。
@WebFilter(filterName = "encodingFilter", urlPatterns = "/*") public class EncodingFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; // 请求体编码,解决 POST 中文乱码 request.setCharacterEncoding("UTF-8"); // 响应编码 + 内容类型,解决输出中文乱码 response.setCharacterEncoding("UTF-8"); response.setContentType("text/html;charset=UTF-8"); chain.doFilter(req, resp); } }登录拦截 Filter 的放行名单要写全,一般包括登录页、注册页、静态资源目录、图书列表和详情页——游客也应该能浏览书。判断依据是session.getAttribute("loginUser")是否为空,为空就response.sendRedirect(request.getContextPath() + "/login.jsp")。这里用重定向不用转发,是因为转发后浏览器地址栏还停在原地址,用户刷新会重复提交。
提示:
request.getContextPath()不能省。项目部署名不叫 ROOT 时,写死/login.jsp会 404,这是本地能跑、换个环境就挂的经典原因。
5. 常见问题排查:五个最容易翻车的点
5.1 中文乱码:POST 和 GET 走的是两条路
现象:表单提交的中文书名存进数据库变成问号,图书列表页显示成“???”,但英文和数字一切正常。
原因:POST 请求体的编码由request.setCharacterEncoding("UTF-8")决定,必须在第一次读取参数之前调用;GET 请求的参数走的是 URL,受 Tomcat 的server.xml里Connector的URIEncoding控制,跟上面那行代码没关系。两种请求混着改,就很容易只修好一半。
解决:Filter 里设请求和响应编码解决 POST;Tomcat 8 及以上默认URIEncoding已经是 UTF-8,如果还乱,检查数据库连接串有没有带useUnicode=true&characterEncoding=utf8,以及建库时是不是用了utf8mb4。三层都要对:连接串、库表字符集、请求响应编码。
5.2 库存被扣成负数
现象:两个人同时买最后一本书,订单都生成成功,库存变成 -1。
原因:“先 SELECT 查库存,判断够不够,再 UPDATE 扣减”这个写法在并发下不安全,两个事务可能都读到 1,都判断通过,然后各自扣 1。
解决:把判断塞进 UPDATE 语句里,用UPDATE t_book SET stock = stock - ? WHERE id = ? AND stock >= ?,看受影响行数是不是 1,是 0 就抛业务异常回滚。或者在事务开头用SELECT ... FOR UPDATE锁住这一行。前者更轻量,后者更好讲原理,答辩时两种都说一遍最稳。
5.3 分页的总条数和列表对不上
现象:列表页显示 10 条数据,但分页条只有 1 页,或者翻到第二页出现第一页的书。
原因:列表查询用了完整条件,计数查询漏了某个条件;或者ORDER BY没有唯一字段兜底,销量相同的书在两次查询里顺序不一致。
解决:像我前面写的那样,把 where 条件抽成一个方法共用。ORDER BY后面永远补一个主键id DESC,保证排序结果稳定。另外LIMIT的偏移量用(pageNo - 1) * pageSize算,pageNo从 1 开始,别从 0 开始。
5.4 连接池耗尽,页面卡死不动
现象:点几下页面之后整个应用没反应,日志里出现waiting for a connection或者超时异常。
原因:某个 DAO 方法里Connection、PreparedStatement、ResultSet有一处没关。连接池最大连接数就那么几个,泄漏几次就耗光了。
解决:用 try-with-resources 包住所有 JDBC 资源,或者统一在DBUtil的close方法里按ResultSet → Statement → Connection的逆序关闭,每个都判空并 try-catch 包住。排查阶段可以临时把连接池最大连接数调小到 2,问题会立刻暴露出来,改完再调回去。
5.5 部署到服务器上全是 404
现象:本地 IDEA 里跑得好好的,打成 war 包丢进 Tomcat 就 404,静态资源也加载不出来。
原因:路径写死了。/css/style.css这种以斜杠开头的绝对路径,在部署名不是 ROOT 的时候会指向服务器根,而不是应用根。JSP 放在WEB-INF外面还会导致能绕过控制器直接访问。
解决:所有页面里的链接和静态资源引用统一加${pageContext.request.contextPath}前缀;JSP 全部放进WEB-INF/views,通过 Servlet 转发访问。这样即使项目部署名改成bookstore,路径也不会错。
6. 验收与答辩:一套能照着念的演示顺序
验收前先跑一遍这份清单,任何一项不过,答辩现场就会变成现场调试:
| 检查项 | 通过标准 | 常见失败点 |
|---|---|---|
| sql 文件导入 | 空库执行无报错,7 张表齐全 | 字符集不是 utf8mb4、外键顺序错 |
| 注册登录 | 新用户能注册,密码错误有提示 | 密码没做 MD5、重复用户名没拦截 |
| 图书检索 | 关键词 + 分类组合查询有结果 | where 条件拼接漏空格 |
| 分页 | 翻到最后一页不报错,总数正确 | count 查询条件不一致 |
| 购物车 | 加、改、删、合并正常 | 登录后 Session 购物车没合并 |
| 下单 | 库存正确扣减,购物车清空 | 事务没回滚、autoCommit 没恢复 |
| 后台 | 上下架后前台立即生效 | 缓存或 status 字段没判断 |
演示顺序建议按“用户视角走一遍完整链路”,而不是按模块逐个点:先注册一个新账号,检索一本书,加购物车,改数量,填地址,提交订单,然后切到管理员账号查这笔订单并发货,最后切回用户看订单状态变成已发货。这条线走完,业务闭环就讲清楚了,剩下的功能介绍都是补充。
答辩时最容易被追问的三个点,提前准备好答案:为什么用 Servlet 不用 Spring Boot(答分层可见、事务和连接管理能讲清楚);下单的事务边界在哪(答从setAutoCommit(false)到commit(),中间四步任何一步失败都rollback());并发下单怎么防超卖(答stock = stock - ? WHERE stock >= ?或者FOR UPDATE)。
我自己的习惯是,答辩前一晚一定要在干净的机器上从零走一遍:装 MySQL、导 sql、部署 war、注册新号、下单。有一回就是漏了这步,现场发现连接串里还写着我本地的密码,白丢十分钟。从那以后我每次交付前都强制走一遍“空库导入 + 全新账号下单”,不跳步。希望帮到你。
本文还有配套的精品资源,点击获取