基于JSP+Servlet的拍卖管理系统:数据库设计与并发出价实战
简介这是一套面向高校计算机专业毕业设计场景的拍卖管理系统完整源码包采用JSPServlet技术栈前端使用jQuery后端基于Servlet与JDBC实现角色划分为管理员与普通用户。系统集成商品竞拍、分类管理、商品管理、订单管理、在线留言等核心模块用户端支持注册登录、商品分类检索与名称搜索、竞拍操作、竞拍记录与订单查询管理端则覆盖账号管理、分类与商品增删改查、竞拍与订单管理、友情链接及轮播图等系统配置功能链路较为完整。资源包共438个文件包含79个jsp页面、126个png与66个gif图像资源、42个css与42个js脚本以及17个jar依赖、8个java源文件、1个sql数据库脚本等压缩包约23.19MB目录结构清晰便于按模块查阅与二次开发。目前已有26人浏览学习适合作为毕业设计选题参考或课程设计实践素材读者可据此快速理解拍卖类系统的业务逻辑与前后端协作方式并在此基础上完成功能扩展与论文撰写。1. 拍卖管理系统为什么还在用 JSPServlet一次把选型逻辑讲透如果你在 GitHub 或各类源码站搜「拍卖管理系统」跳出来的结果十有八九是 SpringBoot Vue 那一套。但如果你翻的是高校课程设计、JavaWeb 实训、或者一些中小企业的内部老系统JSP Servlet 依然是主力。这不是技术落后而是场景决定的部署简单、依赖少、一个 Tomcat 加一个 MySQL 就能跑起来不需要 Node 环境、不需要前后端分离的联调成本。这套「基于 JSPServlet 的拍卖管理系统」要解决的核心问题很明确用户注册登录、发布拍卖商品、浏览竞价、出价、倒计时结束自动成交、后台管理商品和用户。听起来像电商但拍卖的业务逻辑比普通商城多了一层——价格是动态的状态是随时间变化的。这就决定了数据库设计和 Servlet 的职责划分不能照搬普通 CRUD 项目。适合谁看如果你是 JavaWeb 入门阶段想找一个比「图书管理系统」更有业务厚度的项目练手或者你手头有一个传统 JSP 项目要维护、要改再或者你在准备课程设计、毕设需要一个能讲清楚业务逻辑而不是纯增删改查的选题——那这套东西值得你花时间吃透。下面我从数据库设计一路讲到 Servlet 的并发出价处理把能踩的坑都标出来。2. 数据库表设计与拍卖状态机先把地基打对2.1 五张核心表怎么拆拍卖管理系统的数据库设计最容易翻车的地方是把商品信息和拍卖信息混在一张表里。我见过不少课程设计项目一张goods表里塞了start_price、current_price、end_time、winner_id结果商品下架后历史拍卖记录全丢了。正确的做法是拆开表名作用关键字段user用户信息id, username, password, role, phoneauction_item拍卖商品id, name, description, image, start_price, seller_id, create_timeauction_session拍卖场次id, item_id, start_time, end_time, current_price, status, winner_idbid_record出价记录id, session_id, user_id, bid_price, bid_timecategory商品分类id, name, sort_orderauction_item存的是商品的静态属性auction_session存的是这次拍卖的动态状态。同一个商品可以多次上拍比如流拍后重新上架每次生成一条新的 session。status字段用整数枚举0未开始1进行中2已结束3已流拍。建表 SQL 我一般这样写CREATE TABLE auction_session ( id INT PRIMARY KEY AUTO_INCREMENT, item_id INT NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, current_price DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0 COMMENT 0未开始 1进行中 2已结束 3流拍, winner_id INT DEFAULT NULL, FOREIGN KEY (item_id) REFERENCES auction_item(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;注意current_price用DECIMAL而不是FLOAT金额计算用浮点数迟早出精度问题。utf8mb4是为了支持商品名里的 emoji 和生僻字用utf8在某些 MySQL 版本下会截断。2.2 拍卖状态流转谁在什么时候改 status状态机是这套系统的灵魂。很多同学写完发现「拍卖结束了但状态还是进行中」就是因为没有统一的入口去改状态。我的做法是状态变更只在一个地方发生——一个AuctionStatusService类提供startSession()、endSession()、cancelSession()三个方法所有涉及状态修改的操作都走它。流转规则创建 session 时 status0到达 start_time 后由定时任务或首次访问时惰性触发改为 1到达 end_time 后改为 2同时把最高出价者写入 winner_id如果 end_time 到达时没有任何出价记录改为 3流拍这里有个细节不要依赖数据库的定时事件。MySQL 的 EVENT 虽然能定时执行但调试困难、权限要求高而且项目迁移时容易忘。我一般用一个ServletContextListener在应用启动时开一个ScheduledExecutorService每分钟扫一次需要变更状态的 session。WebListener public class AuctionScheduler implements ServletContextListener { private ScheduledExecutorService executor; Override public void contextInitialized(ServletContextEvent sce) { executor Executors.newSingleThreadScheduledExecutor(); executor.scheduleAtFixedRate(() - { // 每分钟检查一次把到期的 session 状态推进 auctionService.refreshExpiredSessions(); }, 0, 1, TimeUnit.MINUTES); } Override public void contextDestroyed(ServletContextEvent sce) { if (executor ! null) executor.shutdownNow(); } }refreshExpiredSessions()里做两件事把end_time NOW()且 status1 的记录改为 2 并计算 winner把start_time NOW()且 status0 的改为 1。用单线程是因为状态变更不需要并发反而并发容易导致重复处理。注意contextDestroyed里必须调用shutdownNow()否则热部署时旧线程不会退出Tomcat 会报内存泄漏警告。3. Servlet 层怎么写从登录到出价的完整链路3.1 项目结构与 web.xml 还是注解传统 JSP 项目有两种 Servlet 注册方式web.xml配置和WebServlet注解。2024 年了除非你维护的是 Servlet 2.5 的老项目否则一律用注解。项目结构我习惯这样分src/main/java/ com.auction.servlet/ -- 所有 Servlet com.auction.service/ -- 业务逻辑 com.auction.dao/ -- 数据库操作 com.auction.entity/ -- 实体类 com.auction.util/ -- 工具类DBUtil, MD5Util 等 src/main/webapp/ WEB-INF/views/ -- JSP 页面外部不能直接访问 static/ -- css/js/图片 index.jspJSP 放在WEB-INF/views/下面是为了强制走 Servlet 转发避免用户直接访问 JSP 绕过权限检查。这是很多入门项目忽略的点——JSP 直接放 webapp 根目录用户输个 URL 就能看到管理页面。3.2 登录 ServletSession 管理和密码存储登录逻辑本身不复杂但有两个坑密码明文存储和 Session 固定攻击。密码必须哈希我一般用 SHA-256 加盐public class PasswordUtil { public static String hash(String password, String salt) { try { MessageDigest md MessageDigest.getInstance(SHA-256); md.update(salt.getBytes(StandardCharsets.UTF_8)); byte[] digest md.digest(password.getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(digest); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(SHA-256 not available, e); } } }salt 存在 user 表里每个用户注册时随机生成。登录时用同样的 salt 哈希后比对。不要用 MD5不要不加盐。Session 管理方面登录成功后先request.getSession().invalidate()再创建新 Session防止 Session 固定攻击protected void doPost(HttpServletRequest request, HttpServletResponse response) { String username request.getParameter(username); String password request.getParameter(password); User user userService.login(username, password); if (user ! null) { request.getSession().invalidate(); HttpSession session request.getSession(true); session.setAttribute(currentUser, user); session.setMaxInactiveInterval(30 * 60); // 30分钟 response.sendRedirect(request.getContextPath() /auction/list); } else { request.setAttribute(error, 用户名或密码错误); request.getRequestDispatcher(/WEB-INF/views/login.jsp).forward(request, response); } }setMaxInactiveInterval设 30 分钟是拍卖场景的合理值——用户可能在看商品详情时停留较久太短会频繁掉登录。3.3 出价 Servlet并发下的价格覆盖问题出价是整套系统里唯一需要认真考虑并发的地方。两个用户同时出价如果不加控制可能出现「后出价的人价格更低却覆盖了高价」的玄学问题。核心在于检查当前价和更新当前价必须是原子操作。方案一数据库行锁。在事务里用SELECT ... FOR UPDATEpublic boolean placeBid(int sessionId, int userId, BigDecimal bidPrice) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 锁定这一行其他事务必须等待 String lockSql SELECT current_price, status, end_time FROM auction_session WHERE id ? FOR UPDATE; PreparedStatement ps conn.prepareStatement(lockSql); ps.setInt(1, sessionId); ResultSet rs ps.executeQuery(); if (!rs.next()) { conn.rollback(); return false; } BigDecimal currentPrice rs.getBigDecimal(current_price); int status rs.getInt(status); Timestamp endTime rs.getTimestamp(end_time); // 校验拍卖进行中、未结束、出价必须高于当前价 if (status ! 1 || endTime.before(new Timestamp(System.currentTimeMillis()))) { conn.rollback(); return false; } if (bidPrice.compareTo(currentPrice) 0) { conn.rollback(); return false; } // 更新当前价 String updateSql UPDATE auction_session SET current_price ? WHERE id ?; PreparedStatement ups conn.prepareStatement(updateSql); ups.setBigDecimal(1, bidPrice); ups.setInt(2, sessionId); ups.executeUpdate(); // 插入出价记录 String insertSql INSERT INTO bid_record(session_id, user_id, bid_price, bid_time) VALUES(?,?,?,NOW()); PreparedStatement ips conn.prepareStatement(insertSql); ips.setInt(1, sessionId); ips.setInt(2, userId); ips.setBigDecimal(3, bidPrice); ips.executeUpdate(); conn.commit(); return true; } catch (SQLException e) { if (conn ! null) try { conn.rollback(); } catch (SQLException ignored) {} return false; } finally { if (conn ! null) try { conn.setAutoCommit(true); conn.close(); } catch (SQLException ignored) {} } }FOR UPDATE会在 sessionId 这一行上加排他锁第二个请求必须等第一个事务提交后才能读到最新的 current_price。这样就不会出现价格覆盖。方案二用UPDATE ... WHERE current_price ?的乐观锁写法不需要显式事务UPDATE auction_session SET current_price ? WHERE id ? AND status 1 AND current_price ? AND end_time NOW()然后检查executeUpdate()返回的影响行数如果是 0 说明出价失败被别人抢先或价格不够。这种写法更轻量但插入 bid_record 需要单独处理且失败时无法区分是价格不够还是拍卖已结束。我一般用方案一因为拍卖出价频率不高行锁的等待时间可以接受而且逻辑清晰、错误可区分。提示DBUtil.getConnection()不要每次新建连接用连接池。最简单的方案是在contextInitialized里初始化一个HikariCP或DBCP数据源存到ServletContext里。手写DriverManager.getConnection在并发下会迅速耗尽数据库连接数。4. 避坑与排查那些让我加班到凌晨的问题4.1 中文乱码POST 和 GET 要分开处理现象表单提交的商品名在数据库里变成???或者页面显示乱码。原因Tomcat 8 以后 GET 请求默认 URI 编码是 UTF-8但 POST 请求体默认是 ISO-8859-1。很多人只加了request.setCharacterEncoding(UTF-8)这只对 POST 有效。解决POST 用request.setCharacterEncoding(UTF-8)GET 参数如果乱码需要在 Tomcat 的server.xml里给 Connector 加URIEncodingUTF-8。另外 JSP 页面头部必须写% page contentTypetext/html;charsetUTF-8 languagejava %HTML 的meta charsetUTF-8也要加。数据库连接 URL 加?useUnicodetruecharacterEncodingutf8mb4。4.2 出价成功但页面价格没变现象用户出价后提示成功但刷新页面看到的还是旧价格。原因JSP 页面被浏览器缓存了或者 Servlet 转发到的 JSP 读取的是 request 里的旧数据。解决在出价成功后用response.sendRedirect()重定向到列表页而不是forward。重定向会发起新的 GET 请求拿到最新数据。同时在 JSP 里加meta http-equivCache-Control contentno-cache。如果用了 AJAX 出价确保返回的是最新价格而不是固定字符串。4.3 定时任务在 Tomcat 热部署时重复启动现象改了代码重新部署发现每分钟执行的任务跑了两次出价记录被重复处理。原因contextDestroyed里没有正确关闭线程池旧的应用实例还在运行。解决确保executor.shutdownNow()被调用并且在contextInitialized里先检查是否已有线程池在运行。更稳妥的做法是用ServletContext的setAttribute存一个标志位启动前先清理。另外开发阶段建议关闭 Tomcat 的自动热部署手动重启更可控。4.4 数据库连接泄漏导致 Tomcat 卡死现象系统运行一段时间后所有请求都超时重启 Tomcat 恢复正常。原因某个 Servlet 里Connection没有在finally里关闭或者ResultSet、PreparedStatement没关。连接池耗尽后新请求全部阻塞。解决用 try-with-resources 改写所有 DAO 方法try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { // ... } catch (SQLException e) { // ... }如果已经上线无法快速改代码临时方案是在连接池配置里加leakDetectionThreshold60000HikariCP超过 60 秒未归还的连接会打印堆栈帮你定位泄漏点。4.5 JSP 里写 Java 代码导致页面报错难排查现象JSP 页面 500 错误但堆栈指向_jspService行号根本看不出是哪段逻辑出错。原因在 JSP 里写了大量% %脚本片段业务逻辑和页面渲染混在一起。解决JSP 只负责展示数据由 Servlet 通过request.setAttribute传入页面用 EL 表达式${}和 JSTL 标签输出。如果必须在 JSP 里做判断用c:if而不是% if %。这样出错时堆栈会指向 Servlet 或 Service 类定位快很多。5. 进阶技巧用过滤器统一权限和编码少写重复代码写到后面你会发现每个 Servlet 开头都在做两件事设置编码、检查登录。这两件事完全可以用 Filter 统一处理代码量直接砍掉三分之一。编码过滤器WebFilter(/*) 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; request.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8); chain.doFilter(req, resp); } }权限过滤器拦截需要登录的路径WebFilter(urlPatterns {/auction/bid, /auction/publish, /admin/*}) public class AuthFilter 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); if (session null || session.getAttribute(currentUser) null) { response.sendRedirect(request.getContextPath() /login); return; } chain.doFilter(req, resp); } }urlPatterns里列出所有需要登录的路径。注意/admin/*只拦截管理员路径但还需要在 Filter 里进一步判断role是否为 admin。我一般把管理员判断单独写一个AdminFilter避免逻辑混在一起。还有一个实用技巧用监听器统计在线人数。实现HttpSessionListener在sessionCreated里给一个AtomicInteger加一sessionDestroyed里减一把值存到ServletContext。JSP 页面直接${applicationScope.onlineCount}就能显示。拍卖系统里这个功能很加分用户能看到「当前 XX 人在线竞拍」有氛围感。最后说一个我自己的习惯每次改完 DAO 层的 SQL先在 MySQL 客户端里手动跑一遍确认查询结果正确再写进 Java 代码。JSPServlet 项目没有 MyBatis 那样的 SQL 日志出了错只能靠堆栈猜提前在客户端验证能省很多时间。另外拍卖结束的定时任务我会在本地把系统时间调到结束前 1 分钟盯着它跑完整个流程确认 winner_id 写入正确、状态变为 2、页面显示「已成交」。这个手动验证步骤看起来笨但比出了线上问题再回头查强得多。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →