ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JSP+Servlet拍卖管理系统实战:MVC分层、数据库设计与并发出价避坑指南

JSP+Servlet拍卖管理系统实战:MVC分层、数据库设计与并发出价避坑指南 简介这是一套面向高校计算机专业毕业设计场景的拍卖管理系统完整源码包采用JSPServlet技术栈前端使用jQuery后端基于Servlet与JDBC实现角色划分为管理员与普通用户。系统集成商品竞拍、分类管理、商品管理、订单管理、留言互动等核心模块用户端支持注册登录、商品分类检索与名称搜索、竞拍出价、竞拍记录与订单查询管理端覆盖账号管理、分类与商品增删改查、竞拍信息维护、订单处理及轮播图、友情链接、单页简介等系统配置功能链路较为完整。资源包共438个文件包含79个jsp页面、42个css样式、42个js脚本、17个jar依赖及1个sql数据库脚本另有大量png、gif等图片素材压缩包约23.19MB目录结构清晰便于按模块查阅与二次开发。运行环境为IDEA 2021.3、MySQL 5.7.26、Tomcat 7.0.73与JDK 1.8适合作为毕业设计参考或Java Web入门练手项目已有26人学习关注。1. 拍卖管理系统从 JSPServlet 到数据库落地的完整路径很多 Java Web 初学者在课程设计或毕业设计阶段都会碰到一个经典命题用 JSP Servlet 搭一套拍卖管理系统还要附带源码和数据库。这个标题背后其实藏着三层需求——第一层是理解 JSP 和 Servlet 在 MVC 架构里各自扮演什么角色第二层是把拍卖业务的核心流程用户注册登录、商品发布、出价竞拍、倒计时截拍、订单生成翻译成数据库表和 Servlet 请求处理逻辑第三层是让这套系统能在本地 Tomcat 里真正跑起来而不是只停留在 IDE 里编译通过。拍卖管理系统和普通 CRUD 系统最大的区别在于「时间敏感」和「并发出价」。一件商品在截止时间前可能有多人同时出价谁的价格最高、出价时间最接近截止点这些都需要在数据库层面做约束而不是只靠前端页面刷新。JSP 负责把后端数据渲染成用户能看懂的页面Servlet 负责接收请求、校验参数、调用业务逻辑、控制跳转数据库负责持久化用户、商品、出价记录和订单。三者配合不好就会出现「页面显示已截拍但数据库还能插入出价」这类玄学问题。这套方案适合谁适合正在做 Java Web 课程设计的学生、需要快速理解 MVC 落地方式的初级开发者以及想拿一个完整案例练手数据库增删改查和事务控制的工程师。下面我会按「先立住架构、再动手建表、然后写核心 Servlet、最后排坑」的顺序把这条路径拆开讲清楚。2. 拍卖管理系统的 MVC 分层与数据库表设计2.1 为什么 JSPServlet 仍然值得拿来练手现在主流做法是 Spring Boot Thymeleaf 或者前后端分离但 JSP Servlet 的价值在于它把 HTTP 请求响应周期暴露得非常彻底。你写一个HttpServlet的子类重写doGet和doPost在web.xml或者注解里配好 URL 映射请求进来之后容器怎么调你的代码、request和response对象怎么传递、session什么时候创建这些在 Spring 里被封装掉的东西在 Servlet 里全是显式的。拍卖管理系统的业务链路比较长用户登录后要能看到商品列表点进详情要能出价出价后要能刷新当前最高价截止时间到了要能自动或手动截拍截拍后要生成订单。这条链路如果直接用 JSP 写脚本片段Scriptlet代码会迅速变成一锅粥。所以常见做法是JSP 只负责展示Servlet 负责控制JavaBean 负责数据承载DAO 负责数据库操作。这就是最朴素的 MVC。提示不要一上来就追求「前后端分离」。在 JSPServlet 阶段先把请求转发forward和重定向redirect的区别搞清楚比学 Vue 更有助于理解 Web 本质。2.2 拍卖业务需要哪几张核心表数据库设计是这套系统的地基。拍卖场景下至少需要五张表用户表、商品表、出价记录表、订单表、分类表。下面这张表列出了每张表的职责和关键字段字段类型以 MySQL 为例。表名职责关键字段说明user存用户账号与角色id,username,password,role,balancerole区分普通用户和管理员category商品分类id,name管理员维护商品发布时选择item拍卖商品id,name,start_price,current_price,end_time,status,seller_id,category_idstatus标记进行中/已截拍bid出价记录id,item_id,user_id,bid_price,bid_time每次出价插一条用于追溯orders截拍后生成订单id,item_id,buyer_id,final_price,create_time一个商品最多一条订单建表时有两个容易翻车的地方。第一item.current_price和bid.bid_price存在冗余但这是有意为之列表页需要快速显示当前价如果每次都去bid表查MAX(bid_price)商品多了之后查询会变慢。第二bid表要加联合索引(item_id, bid_price)因为校验「新出价必须高于当前最高价」这个逻辑会频繁用到。CREATE TABLE item ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, start_price DECIMAL(10,2) NOT NULL, current_price DECIMAL(10,2) NOT NULL, end_time DATETIME NOT NULL, status TINYINT DEFAULT 0 COMMENT 0进行中 1已截拍, seller_id INT NOT NULL, category_id INT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_end_status (end_time, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段 SQL 里idx_end_status索引是为了支持「查询所有进行中且未到截止时间的商品」这个高频操作。DECIMAL(10,2)而不是FLOAT是因为金额计算不能用浮点数否则会出现0.10.2 ! 0.3的经典问题。status用TINYINT而不是VARCHAR是为了在 SQL 里直接做条件判断减少 Java 层的字符串比较。2.3 出价并发问题在数据库层怎么兜底拍卖系统最核心的并发点就是出价。假设两个用户同时看到当前价是 100 元同时出价 110 元如果代码写成「先查当前价再判断再插入」中间没有任何锁两条出价都会成功数据库里出现两条 110 元的记录但item.current_price只更新了一次。这就是典型的竞态条件。常见做法有两种。第一种是在 SQL 里用条件更新UPDATE item SET current_price #{newPrice}, version version 1 WHERE id #{itemId} AND current_price #{newPrice} AND status 0 AND end_time NOW();这条 SQL 执行后返回受影响行数。如果返回 0说明当前价已经被别人抬高了或者商品已截拍或者已过截止时间。Java 层根据返回值决定是提示用户「出价失败当前价已变化」还是继续插入bid记录。第二种是使用SELECT ... FOR UPDATE在事务里锁住商品行但这种方式在并发量稍大时容易造成锁等待所以更推荐第一种。注意end_time NOW()这个条件必须放在 SQL 里不能只靠 Java 层判断。因为 Java 层判断完到 SQL 执行之间还有时间窗口极端情况下会插入截止后的出价。3. 用 Servlet 实现拍卖核心流程3.1 商品列表与详情页的请求处理商品列表页对应一个ItemListServlet映射到/item/list。它做的事情很直接调用 DAO 查询所有status0且end_time NOW()的商品把结果集放到request属性里然后转发到item_list.jsp。WebServlet(/item/list) public class ItemListServlet extends HttpServlet { private ItemDao itemDao new ItemDao(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 查询进行中的商品按截止时间升序快截拍的排前面 ListItem items itemDao.findActiveItems(); req.setAttribute(items, items); // 转发到 JSP由 JSP 负责渲染 req.getRequestDispatcher(/WEB-INF/jsp/item_list.jsp).forward(req, resp); } }这里用forward而不是redirect是因为request属性只能在同一次请求里传递。如果重定向items就丢了。/WEB-INF/jsp/这个路径是为了防止用户直接通过 URL 访问 JSP 文件强制所有页面请求都走 Servlet。商品详情页对应ItemDetailServlet映射到/item/detail接收id参数。除了查商品本身还要查出该商品的所有出价记录按价格降序排列这样页面能显示「出价历史」。如果当前登录用户是卖家页面要隐藏出价按钮如果是买家且商品进行中显示出价表单。3.2 出价 Servlet 的参数校验与事务控制出价请求通常用 POST对应BidServlet映射到/bid/add。它需要接收三个参数itemId、bidPrice、以及从session里取当前用户userId。参数校验的顺序很重要先做廉价校验再做昂贵校验。WebServlet(/bid/add) public class BidServlet extends HttpServlet { private ItemDao itemDao new ItemDao(); private BidDao bidDao new BidDao(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { HttpSession session req.getSession(); User user (User) session.getAttribute(loginUser); if (user null) { resp.sendRedirect(req.getContextPath() /login.jsp); return; } int itemId Integer.parseInt(req.getParameter(itemId)); BigDecimal bidPrice new BigDecimal(req.getParameter(bidPrice)); // 基础校验出价必须大于0 if (bidPrice.compareTo(BigDecimal.ZERO) 0) { req.setAttribute(error, 出价必须大于0); req.getRequestDispatcher(/item/detail?id itemId).forward(req, resp); return; } // 核心条件更新返回受影响行数 int updated itemDao.updateCurrentPrice(itemId, bidPrice); if (updated 0) { req.setAttribute(error, 出价失败当前价已变化或商品已截拍); req.getRequestDispatcher(/item/detail?id itemId).forward(req, resp); return; } // 更新成功后再插入出价记录 bidDao.insert(itemId, user.getId(), bidPrice); resp.sendRedirect(req.getContextPath() /item/detail?id itemId); } }这段代码的关键在于updateCurrentPrice返回 0 时的处理。它不抛异常而是把错误信息放回request再转发回详情页。这样用户能看到失败原因而不是一个空白页。另外bidDao.insert放在更新成功之后如果插入失败item.current_price已经被改了这就需要用事务包起来。常见做法是在 DAO 层用Connection.setAutoCommit(false)手动控制或者用 ThreadLocal 管理连接。3.3 截拍逻辑定时任务还是懒加载拍卖系统必须处理「截止时间到了」这个事件。有两种实现方式。第一种是定时任务比如用ScheduledExecutorService每隔 10 秒扫描一次item表把end_time NOW()且status0的商品改成status1并生成订单。第二种是懒加载不主动改状态而是在查询和出价时都带上end_time NOW()条件用户看到的就是「已结束」但数据库里status可能还是 0。定时任务的好处是状态明确订单能及时生成坏处是需要额外线程Tomcat 重启后任务会丢。懒加载的好处是简单不需要额外组件坏处是订单生成时机不确定如果用户不访问订单永远不生成。我一般会两者结合定时任务负责生成订单查询和出价时用end_time做实时判断这样即使定时任务挂了用户也不会对已截止商品出价。// 定时任务示例每30秒扫描一次 ScheduledExecutorService scheduler Executors.newSingleThreadScheduledExecutor(); scheduler.scheduleAtFixedRate(() - { ListItem expired itemDao.findExpiredItems(); for (Item item : expired) { // 标记截拍并生成订单 itemDao.markAsEnded(item.getId()); orderDao.createFromItem(item); } }, 0, 30, TimeUnit.SECONDS);这段代码放在ServletContextListener的contextInitialized里启动contextDestroyed里关闭避免 Tomcat 停止时线程泄漏。findExpiredItems查询的是end_time NOW() AND status 0markAsEnded要加status 0条件防止重复处理。4. 拍卖系统开发中容易翻车的五个坑4.1 坑一JSP 页面显示乱码数据库存进去也是问号现象用户注册时输入中文用户名页面提交后数据库里显示???或者 JSP 页面上的中文变成乱码。原因三个环节的编码不一致。第一JSP 文件本身的pageEncoding没设成 UTF-8第二request.setCharacterEncoding(UTF-8)没在读取参数之前调用第三数据库连接 URL 没加useUnicodetruecharacterEncodingutf8。解决JSP 头部加% page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8 %Servlet 里在doPost第一行写req.setCharacterEncoding(UTF-8)JDBC URL 写成jdbc:mysql://localhost:3306/auction?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai。三个地方缺一不可。4.2 坑二出价成功但当前价没变或者变了但没记录现象用户出价后页面提示成功但商品列表里的当前价还是旧价格或者当前价更新了但出价历史里找不到这条记录。原因item.current_price更新和bid插入不在同一个事务里。如果先更新item成功再插入bid时抛异常比如字段超长item的更新不会回滚。解决在 DAO 层把两个操作放在同一个Connection里手动setAutoCommit(false)两个操作都成功后commit()任一失败rollback()。不要依赖Transactional因为纯 Servlet 项目没有 Spring 的事务管理。4.3 坑三截止时间到了还能出价现象商品详情页显示「已结束」但通过直接构造 POST 请求仍然能出价成功。原因出价 SQL 里只判断了current_price newPrice没有判断end_time NOW()和status 0。前端隐藏按钮只是视觉上的不能替代后端校验。解决在UPDATE item的WHERE子句里加上AND status 0 AND end_time NOW()。这样即使请求被伪造数据库层也会拒绝。4.4 坑四Tomcat 启动报 ClassNotFoundException 但类明明存在现象项目在 IDE 里编译通过放到 Tomcat 的webapps目录下启动报java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver。原因MySQL 驱动 jar 包没有放到WEB-INF/lib目录下。IDE 里运行时IDE 会把依赖加到 classpath但手动部署到 Tomcat 时Tomcat 只认WEB-INF/lib和Tomcat/lib。解决把mysql-connector-java.jar复制到WEB-INF/lib下。如果用的是 Maven确认pom.xml里依赖的scope不是provided否则打包时不会带进去。4.5 坑五session 失效后出价报空指针现象用户登录后打开商品详情页放置很长时间再点出价后台报NullPointerException因为session.getAttribute(loginUser)返回了null。原因session 默认超时时间是 30 分钟超时后 session 被容器回收但用户浏览器里的页面还是旧的。解决在BidServlet里取loginUser后先判空如果为空就重定向到登录页并带上redirect参数登录后跳回原页面。不要直接user.getId()否则必崩。5. 用 JSTL 优化 JSP 渲染与本地验证清单5.1 把 Scriptlet 从 JSP 里赶出去很多 JSPServlet 项目的 JSP 页面里充斥着% for(...) { %和% item.getName() %这种写法在商品列表页会变成嵌套循环加条件判断可读性极差。更好的做法是引入 JSTL 和 EL 表达式。比如渲染商品列表% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % c:forEach items${items} varitem div classitem-card h3${item.name}/h3 p当前价fmt:formatNumber value${item.currentPrice} pattern#,##0.00//p p截止fmt:formatDate value${item.endTime} patternyyyy-MM-dd HH:mm//p c:choose c:when test${item.status 1} span classended已截拍/span /c:when c:otherwise a href${pageContext.request.contextPath}/item/detail?id${item.id}去出价/a /c:otherwise /c:choose /div /c:forEachc:forEach替代了 Java 的 for 循环fmt:formatNumber处理金额千分位c:choose处理状态分支。这样 JSP 里不再出现任何 Java 代码前端人员也能看懂。${pageContext.request.contextPath}用来拼接项目上下文路径避免部署到不同路径时链接失效。5.2 本地跑通之后按这个清单逐项验证系统能在 Tomcat 里打开首页不代表业务逻辑没问题。我一般会按下面这个清单逐项过一遍每项都手动操作并观察数据库变化。验证项操作预期结果检查点注册登录注册新用户并登录跳转到商品列表user表新增记录密码非明文发布商品管理员发布一件商品列表页出现该商品item表status0current_pricestart_price正常出价用户A出价高于当前价页面提示成功当前价更新bid表新增记录item.current_price同步低价出价用户B出价低于当前价页面提示失败bid表无新增item.current_price不变并发出价两个浏览器同时出相同价格只有一个成功bid表只有一条该价格记录截止后出价手动改end_time为过去时间再出价提示已截拍item.status仍为0但出价被拒绝截拍生成订单等待定时任务或手动触发orders表新增记录item.status1订单金额等于最终价这张表里的「并发出价」和「截止后出价」是最容易漏测的。并发出价可以用两个不同浏览器登录不同账号同时点出价按钮来模拟截止后出价可以直接在数据库里把end_time改成2020-01-01 00:00:00然后刷新页面看出价按钮是否还在再尝试提交。5.3 一个我踩过的坑订单重复生成定时任务扫描过期商品时如果两次扫描间隔内第一次已经生成了订单但status还没来得及更新第二次扫描会再次查到同一件商品导致重复生成订单。我当时的做法是在markAsEnded的 SQL 里加AND status 0并且检查受影响行数只有返回 1 才继续生成订单。这样即使扫描到同一件商品第二次更新会返回 0订单就不会重复插入。int updated itemDao.markAsEnded(item.getId()); if (updated 1) { orderDao.createFromItem(item); }这个判断看起来简单但少了它订单表里就会出现同一商品两条记录后面做订单列表时用户会看到重复条目。希望帮到你。本文还有配套的精品资源点击获取
返回列表