资讯动态

Java Web购物车实战:Servlet+JSP+DAO实现分页与后台管理

发布时间:2026/10/16 4:00:25 来源:尧图企业网站定制
之前接了个课程设计辅导对方直接甩给我一个标题“史上功能最全Web购物车实现基于jspjavaservletdao包括后台图书管理、实现数据库分页等功能”。这标题看起来像是一个学习项目但听完需求才发现这其实是一整套Java Web单体应用的综合练习前台要有商品浏览、购物车操作、模拟结算后台要有图书管理、图片上传、模糊搜索还要在列表页玩出数据库分页的花样。这种项目在初学者眼里是“期末作业天花板”在面试场景里又是常被拿来聊的经典模型。很多同学能把“往购物车里塞一本书”的流程跑通但一旦涉及后台管理、分页、会话隔离、连接池管理就会暴露出大量问题。这篇文章我就以这套模拟项目X为蓝本从数据库设计、DAO封装、Servlet控制、购物车会话操作、后台分页管理到实际排坑完整拆一遍实现思路。适合正在做Java Web课程设计的人也适合想用购物车项目系统整理Servlet/JSP/DAO知识点的同学参考。1. 购物车项目到底“全”在哪里先从标题拆出功能矩阵1.1 “功能最全”不是功能堆砌而是三层结构都要完整很多人一看到“史上功能最全”就觉得是夸张但用来做课程设计或面试作品这句话其实有它的道理。它不是在说购物车本身能玩出多少花样而是指整个项目覆盖了Java Web开发里最核心的三层结构表现层、控制层、数据访问层。表现层用JSP承担页面渲染控制层用Servlet接收请求并控制页面跳转数据访问层通过DAO完成对数据库的增删改查。购物车只是把这三层串起来的那条业务线。这套模拟项目X的业务线主要有两条。一条是前台面向普通用户的浏览与购物链路分页查看图书列表、查看图书详情、登录、加入购物车、修改购物车数量、删除单项、清空购物车、查看总金额。另一条是后台面向管理员的图书管理链路管理员登录校验、图书列表、新增图书、编辑图书、删除图书、封面上传、按书名或分类模糊搜索。这两条链路通过同一套图书表关联起来后台改了数据前台列表和购物车会跟着变。标题里特别点出的“数据库分页”其实是后台图书列表用的能力但前台图书分页也应该实现否则后台能用、前台不能用会显得项目很割裂。我一并做了前后台共用同一个分页Bean和同一个DAO查询方法。所以项目的“全”指的是前台交易链路、后台管理链路、通用分页机制、会话与权限控制这几块都有自己的落地而不是简单堆了一堆按钮。1.2 适合谁拿去练手以及练完能沉淀什么如果你是刚学完JavaWeb基础、准备做课程设计的同学这套项目至少能帮你把如下知识点串成一条线JDBC操作MySQL、PreparedStatement防注入、DAO层的结果集映射、连接池配置、Servlet请求分发、Session购物车状态、Filter权限拦截、EL表达式与JSTL标签、文件上传、分页SQL写法。这些都是面试Java后端岗位时的高频问题也是工作中很容易碰到的日常开发场景。如果你是准备求职、想要一个项目写进简历的同学我不建议你停在这个“前后台分页”的版本。更合理的做法是先把这个版本完整跑通然后在此基础上做减法或加法。减法指的是把JSP页面里的Java脚本彻底清干净全部换成EL和JSTL提升代码可读性加法指的是引入Maven、把数据库连接改成Druid连接池、加入订单表和库存扣减逻辑、把分页改成Ajax异步加载。这样这个“购物车”项目就能从课程设计水平往工程化水平靠拢。1.3 项目目录结构与模块划分参考我这里采用的包结构是这样的大家可以作为参考com.demo.entity放实体类Book、User、CartItem、PageBeancom.demo.dao放BookDAO、UserDAO以及BaseDAO这个通用父类com.demo.service放业务逻辑比如BookService、CartServicecom.demo.servlet放BookServlet、CartServlet、UserServlet、AdminServletcom.demo.filter放登录过滤器com.demo.util放DBUtil、UploadUtil等工具类。JSP页面则在WebContent下按前台和后台分成front和admin两个目录。这种分法不花哨但很实用。它把“谁负责取数据”“谁负责算业务”“谁负责展示”分得清清楚楚后续扩展订单功能时只需仿照BookDAO再加一个OrderDAO业务逻辑放进OrderService页面再加几个JSP整体不会乱。2. 从数据库第一行建表开始图书、用户与购物车数据模型怎么设计才不返工2.1 三张核心表的字段取舍做Web项目我习惯先定数据库表结构因为表结构直接决定了DAO层怎么写、前端页面要展示哪些字段。这套模拟项目X里最基础的是图书表book、用户表user、购物车表cart_item。很多同学做购物车时不建购物车表把购物车整个放到Session里这在前台免登录场景下是可以的后面我会详细说。但如果要做“后台管理”和“用户体系”建议还是把购物车表建出来一方面体现数据建模能力另一方面为以后做“未登录收藏、登录后合并购物车”的功能留扩展点。图书表我推荐的字段结构是id、title书名、author作者、price定价、stock库存、cover封面图路径、category分类、publisher出版社、description简介、create_time创建时间。这里有三点容易踩坑。第一价格字段必须用DECIMAL(10,2)千万不能用float或double否则金额会出现0.10.2不等于0.3这类浮点误差第二库存字段用INT并建议在查询列表时加一个stock 0条件这样“已售罄”的图书不会出现在前台第三封面路径不要存完整网址或绝对路径只存相对路径比如upload/xxx.jpg页面渲染时动态拼接项目上下文路径方便项目整体迁移。用户表字段是id、username、password、nickname、role、create_time。这里的角色字段用来区分普通用户和管理员普通用户浏览前台、购买图书管理员进入后台管理图书。密码字段需要强调一下明文存储是绝对不允许的哪怕只是练手项目也至少要用MD5加固定盐或者SHA-256做一次散列至少要在简历和文档里体现出你有这个安全意识。实际项目中还会加动态盐、加次数迭代但课程设计阶段做到固定盐哈希已经算过关了。2.2 购物车表建不建怎么建购物车表按“用户图书”维度去重存储建议字段为id、user_id、book_id、quantity、add_time并给user_id book_id加唯一索引。这样同一个用户重复添加同一本图书时可以走ON DUPLICATE KEY UPDATE quantity quantity 1或者先在DAO里查一次再决定update还是insert。这个设计在面试里非常容易聊出亮点因为大多数人只会往Session里塞Map很少会想到把购物车落库。我的建议是两者结合用户未登录时购物车数据放Session保证游客也能加购用户登录后把Session里的购物车合并到数据库以数据库购物车为准。这套模拟项目X因为篇幅和篇幅限制前台购物车主流程还是以Session为主但数据库表已经提前建好关键代码里也留了CartDAO的空实现方便后续扩展。你要是想做得更完整可以把登录成功后的购物车合并逻辑补上。2.3 建表SQL与初始化数据下面是基本可用的建表SQLMySQL环境下直接执行即可。如果你是SQL Server或Oracle只需把自增主键的写法对应替换。CREATE DATABASE IF NOT EXISTS shop_db DEFAULT CHARSET utf8mb4; USE shop_db; CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(255) NOT NULL, author VARCHAR(100), price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 100, cover VARCHAR(255), category VARCHAR(50), publisher VARCHAR(100), description TEXT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, nickname VARCHAR(50), role VARCHAR(20) DEFAULT user, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE cart_item ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, book_id INT NOT NULL, quantity INT NOT NULL DEFAULT 1, add_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_book (user_id, book_id) ); INSERT INTO book (title, author, price, stock, cover, category, publisher, description) VALUES (Java编程思想, 某经典作者, 89.90, 200, upload/java.jpg, 编程, 某出版社, Java进阶必读), (深入理解计算机系统, 某作者团队, 128.00, 150, upload/csapp.jpg, 计算机, 某出版社, 系统底层经典), (数据库系统概论, 某数据库作者, 45.00, 300, upload/db.jpg, 数据库, 某出版社, 数据库入门教材);这段SQL里加了个小细节DEFAULT CHARSET utf8mb4而不是utf8。原因很简单utf8mb4是完整的UTF-8实现能存下一些生僻字和特殊符号避免中文乱码的隐性坑。3. 分层不是应付面试JSP、Servlet、DAO的调用链与职责边界3.1 一次图书列表请求在代码里到底怎么走我见过太多JSP项目页面里直接写JDBC连接、直接查库、直接拼HTML逻辑确实能跑但后续修改一次就要动一次页面维护成本极高。好的做法是严格分层让请求走一条清晰链路。以“前台第一页图书列表”为例完整的调用顺序是浏览器访问BookServlet?actionlistpage1Servlet接收请求并解析action和page参数调用BookService或直接调用BookDAO查询出PageBean对象把PageBean放到request作用域再转发到front/book_list.jsp。JSP页面用EL和JSTL取出PageBean里的list和分页信息渲染成HTML浏览器展示。这套链路里最容易写歪的地方是参数解析。比如page参数用户可能传abc也可能传-1如果不做容错处理Integer.parseInt会直接抛异常。我的做法是写一个内部小工具方法把字符串转int转换失败就返回默认值1同时如果页码小于1就重置为1大于总页数就重置为总页数。这样用户在地址栏乱改参数页面也不会白屏。3.2 DAO层只做数据读写别把业务塞进来DAO层的核心是“一行数据映射成一个对象”。JDBC查询返回的是ResultSet使用起来不直观所以DAO里的职责就是执行SQL、遍历ResultSet、把每一行封装成Book对象然后返回ListBook或单个Book。这个过程是重复劳动但也是最有必要写好的一环。我常用的封装方法是抽取一个Book getBookFromResultSet(ResultSet rs)私有方法或者用一个小心的工具类把rs.getInt(id)、rs.getString(title)、rs.getBigDecimal(price)都集中到一个地方。这样不管是按ID查询、分页查询还是模糊搜索都能复用同一套映射逻辑不会出现“这个页面少了个字段、那个页面类型转换异常”的问题。连接管理方面我建议用连接池而不是每次手动DriverManager.getConnection。手动获取连接在并发场景下会反复创建和销毁数据库连接性能很差。模拟项目X里用的是DBCP连接池配置文件写在db.properties里连接池参数按学生机配置调整初始连接数5、最大连接数20、最大等待时间3000毫秒。实际练习时Druid也可以配置方式大同小异。3.3 Servlet层是“交通警察”转发还是重定向要想清楚Servlet比较容易翻车的地方是不清楚“请求转发”和“重定向”该用哪个。简单的判断标准是如果这次请求结束后页面需要带数据比如图书列表、购物车详情、后台图书管理列表就用request.getRequestDispatcher(xxx.jsp).forward(request, response)如果是表单提交后想防止刷新重复提交比如添加图书成功后跳回列表页就用response.sendRedirect(BookServlet?actionlist)。转发是同一个请求内带着request作用域的数据到目标页面重定向是浏览器重新发起一次全新请求request里的数据自然就丢了。这套逻辑在购物车里尤其重要。用户点击“加入购物车”如果Servlet处理完后转发到购物车页面那用户按F5刷新时浏览器会重新提交一次“加入购物车”请求导致同一本图书被加了两次。正确做法是“加购成功后重定向到购物车展示URL购物车展示这个动作才用转发”。这也是我刚才提到的RedirectAfterPost模式在表单提交场景里属于基础设施级别的技巧。4. 购物车会话玩法的四种操作加购、改数量、删除、清空的完整实现4.1 Session里的购物车到底是个什么结构购物车最核心的载体是Session对象。Session是跟着用户会话走的每个浏览器对应独立的一份天然隔离不需要考虑两个人互相污染数据。购物车在Session里的结构我推荐用HashMapInteger, CartItemkey是bookIdvalue是CartItem对象。CartItem里除了持有Book对象再单独存一个quantity数量字段。为什么用Map而不是List因为Map通过bookId判断“这本书在不在购物车里”的代价是O(1)而用List就得一遍遍遍历才能知道里面有没有这本书。Map在执行“加购时数量累加”“修改数量时直接set”这两类高频操作时非常顺手。CartItem这个实体类属于典型的POJO需要实现Serializable接口因为Tomcat在做Session钝化或集群Session同步时如果类没实现序列化接口会抛出NotSerializableException。这个细节很多人没注意直到部署到集群环境才被坑。4.2 加购与数量修改的代码骨架加购流程的Servlet核心逻辑大致如下protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String action request.getParameter(action); if (add.equals(action)) { int bookId Integer.parseInt(request.getParameter(bookId)); int quantity Integer.parseInt(request.getParameter(quantity)); if (quantity 1) { quantity 1; } HttpSession session request.getSession(); MapInteger, CartItem cart (MapInteger, CartItem) session.getAttribute(cart); if (cart null) { cart new HashMap(); session.setAttribute(cart, cart); } Book book bookDAO.findById(bookId); if (book null) { response.sendRedirect(front/error.jsp?msgbook_not_found); return; } CartItem item cart.get(bookId); if (item null) { cart.put(bookId, new CartItem(book, quantity)); } else { item.setQuantity(item.getQuantity() quantity); } response.sendRedirect(CartServlet?actionview); } }这段代码要额外处理一个边界库存上限。如果用户加了5本实际库存只有3本服务端要么提示“库存不足”要么把数量封顶到3。我建议在加购和修改数量两处都校验一下quantity book.stock否则管理员把库存调小之后购物车里还可能挂着一个超卖数量结算时会非常尴尬。修改数量的操作和加购类似前端可以把操作类型拆成increase和decrease或者直接传一个最终数量值。暴力但稳妥的方案是传最终数量值服务端做边界判断后item.setQuantity(quantity)。如果传的是“加减一”的增量就要注意快速点击时请求并发可能出现旧值覆盖新值的脏写。课程设计阶段用最终数量值方案就好代码简单逻辑清晰。4.3 删除、清空与金额计算的边界处理删除单项就是cart.remove(bookId)然后判断一下如果cart为空就把Session里的“cart”属性也移除避免留下一个空Map造成前端显示异常。清空购物车更简单直接session.removeAttribute(cart)。这两个操作几乎没有技术难度真正需要费心的是金额计算和空购物车展示。金额计算我强烈建议用BigDecimal而不是把单价和数量转成int去乘再除以100。虽然以分为单位用整数可以规避浮点误差但JSP页面渲染、用户输入、价格存储全都是以“元”为单位来回换单位容易出错。直接new BigDecimal(price).multiply(new BigDecimal(quantity))虽然多写几个单词但可读性高也不会出现精度问题。最后在JSP里用fmt:formatNumber标签保留两位小数输出界面干净整洁。另一个边界是“图书下架”。如果管理员在后台把某本图书删除或把库存改为0用户Session里还残留着这本图书点击结算或修改数量时会查不到Book对象。我的策略是每次渲染购物车页面时遍历cart中的每一项根据bookId重新查库查不到的就直接从Map里移除并在页面上提示“部分商品已失效”。这样能保证购物车数据和库存状态尽量一致。5. 数据库分页是标题里最值钱的部分物理分页SQL与PageBean封装5.1 分页三要素与LIMIT参数计算数据库分页是这个项目里最值得展开讲的知识点也是面试官最爱追问的“为什么不用内存分页”。先说什么叫物理分页数据库每页只返回需要的记录条数比如每页10条第二页就通过LIMIT 10 OFFSET 10只拿第11到20条。执行两次SQL一次查总数一次查当前页数据。与之相对的内存分页是一次性把所有数据SELECT *查出然后JVM里截取需要的部分这在小数据量时看不出问题但图书表一旦有几十万条内存和响应时间都会崩。分页需要三个基本参数当前页currentPage、页大小pageSize、总记录数totalCount。totalCount来自SELECT COUNT(*) FROM book总页数totalPage的计算公式是(totalCount pageSize - 1) / pageSize。当前页的数据查询SQL是SELECT * FROM book ORDER BY id DESC LIMIT ?, ?第一个问号是offset第二个是limit。offset的计算公式是(currentPage - 1) * pageSize注意不是currentPage * pageSize第二页的offset是10而不是20这是初学最容易写错的点。5.2 能不能只查一次就拿到分页数据MySQL的LIMIT本身就带起始位置和条数所以查询当前页数据时天然物理分页。有些同学会问能不能用LIMIT ? OFFSET ?代替两个问号语法上可以但用PreparedStatement的动态参数绑定其实都一样的我习惯写成LIMIT ?, ?第一个参数是offset第二个是size顺序千万别反。还有一个小优化点当数据量很大的时候深分页LIMIT 100000, 10会因为数据库要扫描并丢掉前10万行而变慢。更优的写法是利用主键做条件先查出上一页最后一条记录的ID再WHERE id 上一页最大ID ORDER BY id DESC LIMIT 10这样数据库能走主键索引。但这种键集分页在不改代码结构的前提下不容易在通用PageBean里实现所以我一般建议课程设计阶段用标准LIMIT offset, size即可但要在文档里写出这个优化思路面试时会加分。5.3 PageBean对象与前置条件校验PageBean是分页功能的核心载体我建议做成泛型类比如PageBeanT不管图书列表还是用户列表都能复用。字段包括ListT list存放当前页数据int currentPage当前页int pageSize页大小int totalCount总记录数int totalPage总页数再提供一个boolean isHasNext判断是否有下一页。JSP页面渲染分页导航时有了这些字段就足够拼出上一页、下一页、页码列表。DAO层的方法签名建议写成public ListBook findPage(int offset, int pageSize) throws SQLException;然后在Service里计算offsetint offset (currentPage - 1) * pageSize;这样DAO不关心业务层怎么计算页码参数干净后续想加缓存或改查询条件也不受影响。Service层还需要在查询总数之后校验当前页越界比如删除最后一页的最后一条数据后当前页可能超出总页数这时候要把currentPage重置为totalPage再重新查询一次。5.4 分页和模糊搜索同时存在的SQL组织方式标题里的“后台图书管理”通常离不开搜索搜索和分页叠在一起才是完整需求。搜索条件下列表查询SQL和总数查询SQL都必须带同样的WHERE条件否则页数对不上。查询SQL形如SELECT * FROM book WHERE title LIKE ? [AND category ?] ORDER BY id DESC LIMIT ?, ?;总数SQL形如SELECT COUNT(*) FROM book WHERE title LIKE ? [AND category ?];这里需要注意PreparedStatement的参数索引如果先拼接了搜索条件再追加分页参数参数顺序必须和SQL里的问号顺序完全一致。常见Bug是WHERE子句里有如果分类为空就不拼结果参数个数和问号数对不上执行时直接报Parameter index out of range。我建议统一用一个ListObject params累积参数最后再转成数组传给PreparedStatement的参数设值循环这样最稳妥。模糊搜索还有一个细节LIKE ?的参数要手动拼上%比如%Java%而不是在SQL里写LIKE %?%。后者会把?当成字符串的一部分查出来的结果是空。6. 后台图书管理实战登录拦截、图片上传、模糊搜索一起落地6.1 Filter拦截后台页面不能裸奔后台管理页面必须具备权限控制否则任何人都可以直接输入admin/book_list.jsp看到管理界面。靠每个Servlet里写一遍“检查session里有没有loginUser”是能实现但重复代码太多而且JSP如果被直接访问Servlet根本不会经过。所以正确姿势是写一个Filter把后台访问请求统一拦下来做登录判断。我建议Filter按“路径前缀”做拦截比如/admin/*和/AdminServlet。Filter核心逻辑并不复杂从Session里取管理员信息存在就放行不存在就重定向到登录页。由于后台管理员的登录页本身不能拦截还要在Filter里加一个白名单判断把login.jsp、loginServlet放行。Web.xml里配置Filter映射时注意顺序多个Filter时会按配置顺序依次执行权限Filter尽量排在编码Filter后面否则乱码问题还没处理就先被拦截跳转排查起来很痛苦。6.2 图片上传Tomcat下用getPart还是Commons FileUpload图书封面是后台图书管理的标配字段。传统做法是用Apache Commons FileUpload组件解析multipart表单但对Servlet 3.0以上版本来说直接用request.getPart(cover)更简单前提是Servlet类上要标注MultipartConfig注解同时在JSP表单里设置enctypemultipart/form-data这两个条件缺一个都拿不到文件内容。保存文件的思路是这样先拿到上传文件名但一定不要直接用原始文件名保存因为不同用户可能会传同名文件会互相覆盖。我用UUID.randomUUID().toString().replace(-, )拼上原文件扩展名生成唯一文件名然后把文件写到WebContent/upload目录下。数据库里存的是upload/abc123.jpg这种相对路径。页面上渲染时再拼接request.getContextPath()得到完整的/shop/upload/abc123.jpg路径。这个拼接如果漏了直接写相对路径在部署到带ContextPath的服务器时会面临图片404的困扰。还有文件大小限制问题MultipartConfig(maxFileSize 1024 * 1024 * 2)限制单文件2MB后台捕获IllegalStateException后返回友好提示避免用户传一个10MB的图片直接让服务器响应异常。6.3 图书新增、编辑、删除的后台操作细节新增图书的表单和编辑图书的表单可以共用一套JSP页面只是提交目标不同。新增走AdminBookServlet?actioninsert编辑走AdminBookServlet?actionupdate页面通过EL表达式判断传入的book对象是否为空为空则是新增不为空则回填字段。回填的时候注意checked状态和下拉框选中项要用JSTL的c:if去动态输出常见错误是只在编辑页写死默认选项导致新增和编辑行为不一致。删除图书时除了执行DELETE FROM book WHERE id?还要考虑要不要删封面图片文件。如果封面文件确实上传到了本地磁盘可以先查一次book记录拿到cover路径再执行删除SQL最后用java.io.File删除文件。这里要小心路径穿越删除文件名不能直接拼接用户传入的相对路径而应该从数据库里取出的路径来删防止有人构造../../xxx路径。不过课程设计阶段只要做到“路径来自数据库”就已经算有安全意识到了。7. 中文乱码、搜索分页丢失、购物车快照过期真正恶心的坑都在这里7.1 中文乱码的三个传播节点JSP Servlet MySQL这套组合里中文乱码通常不是某个单一节点造成的而是一串节点层层累积的结果。第一个节点是请求参数编码POST请求参数要调用request.setCharacterEncoding(UTF-8)且必须在拿到任何参数之前设置GET请求的URL参数编码取决于服务器连接器配置Tomcat 8以上默认是UTF-8老版Tomcat则需要在server.xml里加URIEncodingUTF-8否则地址栏传中文搜索词必然乱码。第二个节点是响应编码response.setContentType(text/html;charsetUTF-8)要写在最前面同时JSP页面头部的pageEncoding和contentType里的charset也要一致。第三个节点是数据库连接URL必须在JDBC连接串里加useUnicodetruecharacterEncodingUTF-8否则数据库客户端和服务器端编码不一致写入的中文会变成问号。排查这段流程有一个实用技巧先把页面上显示的中文和数据库里存的中文对比如果在页面输入时就已经乱码问题在请求参数编码如果页面显示正常、数据库里是乱码问题在连接URL如果数据库正常、页面乱码问题在响应编码。按这个思路排查比随机改配置高效得多。7.2 分页导航一点就把搜索条件弄丢了这个坑在图书管理后台特别常见用户在第一页搜索“Java”得到结果点击第二页结果却变成了全部图书因为分页导航里的链接只写了BookServlet?actionlistpage2完全没有带上搜索关键词。要修复这个bug必须在拼接分页链接时把当前搜索条件原样带上。我建议在Servlet接收参数时把搜索条件同时放入request作用域比如keyword、category。然后在JSP的分页导航里拼链接时用JSTL做一次URL参数拼接链接变成BookServlet?actionlistpage2keyword${param.keyword}category${param.category}。这里有个额外提示如果keyword中含中文或特殊字符最好做一次URL编码否则部分服务器解析会异常。这种情况下可以用fn:replace或者维护一个专门输出当前搜索条件的MVC辅助方法保证URL合法。7.3 购物车里的价格快照和图书下架问题设计购物车时为了减少查库压力很多人会直接把完整Book对象塞进Session页面直接取cartItem.book.price展示。这样有个隐患管理员在后台改了图书价格或库存后用户Session里存的还是旧数据页面照旧显示旧价格结账时可能会按旧价成交也可以按旧库存判断这都属于业务逻辑漏洞。我的处理方案是Session里只存bookId和quantity购物车页面每次渲染前由CartService根据bookId集合批量重新查库组装出最新的CartItem列表。这样价格、书名、封面永远以数据库为准购物车里绝不放完整的可变业务数据。代价是多一次查询但这个项目规模完全可以接受。配合前面第4.3节提到的失效商品清理逻辑购物车模块就只依赖当前数据库状态不容易出现脏数据。这个方法也引出一个面试常见题Session和数据库购物车如何选择。Session的优点是免登录、无数据库压力、实现简单缺点是用户换浏览器或清Cookie就丢失数据库的优点是跨设备同步、可持续存储缺点是每次访问都要查库同时未登录用户无法关联用户ID。所以现网电商大多用Redis做临时购物车登录后合并到数据库但那是后话了。7.4 表单重复提交与连接释放表单重复提交导致数据重复是Web初学者耳熟能详的坑。比如后台新增图书时用户点击“保存”后长时间没有响应于是再点一次结果数据库里插入了两条一模一样的图书。处理方式就是我前面提到的“POST后重定向”请求处理完成之后不再转发到JSP页面而是重定向到图书列表Servlet浏览器地址栏变成列表URL用户再刷新也只是刷新列表不会重新提交POST数据。连接释放的问题则藏得更深。DAO层在finally块里只关闭了ResultSet和Statement但连接是从连接池借出来的如果没有关闭连接并不会回到池中而是被占用。高并发下一旦连接池被耗尽整个应用会卡在等待连接上。正确做法是从连接池拿连接时不管是否发生异常finally里都要调用conn.close()这里close的真实逻辑是归还连接而不是物理断开。使用DBCP连接池时还可以开启removeAbandonedtrue和logAbandonedtrue帮助排查连接泄漏。8. 从课程设计到工程化下一步还能怎么升级这套购物车骨架跑通之后如果想把它从“期末作业”升级成“能写进简历的作品”我建议优先做三件事。第一件事是把原生的JDBC DAO迁移到MyBatis或Spring JDBC Template配置上Druid连接池让数据访问层更直观也更易维护。第二件事是给图书表加订单表和订单明细表同时把库存扣减做成事务防止并发下单出现超卖。第三件事是引入Maven统一管理依赖把JSP里的脚本彻底清理干净并把分页改成Ajax局部刷新减少整页跳转的呆板感。我个人在带这个模拟项目X时的体会是购物车虽小但它把Web开发的各个关键节点都串了一遍。很多同学卡住的不是技术难点而是“不知道下一步该做什么”。如果你现在正写这个项目先把后台图书管理和分页这两个硬骨头啃下来再回头去优化购物车的交互整个项目的完成度就会有明显提升。最后分享一个小技巧开发时尽量把日志打印完整尤其是SQL参数和异常堆栈购物车这类状态型项目大部分诡异bug都是“看不出数据在哪里丢的”导致的。希望这篇拆解能帮到正在做Java Web课程设计或者准备面试项目的你。有问题欢迎在评论区交流我会挑有代表性的问题继续展开。

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

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

免费获取报价 →
↑