☰
Java Web拍卖系统:Servlet+JSP+JDBC实战设计
2026/10/1 11:23:24 网站建设 项目流程

简介:本资源是一套基于JSP技术实现的网上拍卖平台毕业设计完整方案,面向计算机专业本科生及Web开发初学者,适用于课程设计、毕设选题与Java Web入门实践。压缩包共236个文件,涵盖51个核心JSP页面(如dls.jsp、right.jsp等)、22个编译后Class类文件(含BidController、Picture等业务控制器)、96个GIF与53个JPG格式界面素材,辅以数据库文件(mdb、db)、样式表(css)、文档(doc、txt)及备份文件(bak),整体仅1.15MB,轻量易部署。已有71人学习下载,资源经实测可直接运行,功能完整覆盖用户注册登录、商品发布、竞价出价、订单管理等核心流程。读者可获得结构清晰的MVC分层代码、典型JDBC数据库操作范例、前端交互逻辑实现细节,以及包含日历组件(JspCalendar)、图片生成工具(CreateImage)等实用模块,便于理解传统Java Web开发模式并快速二次开发。

1. 这不是“写个JSP页面就交差”的毕业设计:一个能真跑起来、能改价格、能拍下商品、能查订单的Java Web拍卖系统到底长什么样?

你搜“基于JSP实现的网上拍卖平台系统设计”,点开一堆压缩包,解压看到index.jsp、login.jsp、auction_list.jsp……第一反应是:“哦,又是那种用<%= new Date() %>拼个时间、用<% out.print("欢迎"+session.getAttribute("username")) %>显示个名字,后台全靠request.getParameter("price")硬接、数据库连的是本地 Access 或者没密码的 MySQL root 账号的‘教学演示版’?”
错。真正能当毕设答辩硬货、能被老师追问“竞拍超时怎么处理”“并发出价怎么防重”“用户余额扣减和订单生成怎么保证原子性”的系统,它必须是一套闭环可验证的业务流:用户注册 → 实名认证(哪怕只是填身份证号)→ 发布拍品(带起拍价、保证金、结束时间)→ 浏览/搜索 → 出价(带价格校验、阶梯限制、时间有效性)→ 竞拍结束自动结算 → 生成订单 → 付款状态同步 → 后台管理商品/用户/订单。
这个.rar包里的“源代码+论文+毕业设计”,核心价值不在“用了JSP”,而在于它用最朴素的 Servlet+JSP+JDBC 技术栈,在不依赖 Spring Boot 自动装配、不靠 MyBatis XML 魔法映射的前提下,把拍卖业务的状态机、事务边界、会话一致性这些硬骨头,用 Java 原生方式一节一节啃下来了。适合计算机、软件工程专业学生——不是让你抄代码,而是让你看清:当没有框架兜底时,HttpSession怎么管用户登录态,Connection.setAutoCommit(false)怎么锁住一笔出价,ServletContextListener怎么在 Tomcat 启动时加载全局配置。它不炫技,但每行代码都踩在真实业务的刀刃上。


2. 从零搭起骨架:用最简技术栈跑通拍卖主流程(Servlet + JSP + JDBC)

2.1 为什么坚持用原生 Servlet/JSP 而不是 Spring Boot?——毕业设计的“可控性”比“时髦度”重要

很多同学一上来就想用 Spring Boot + Thymeleaf,结果卡在@Transactional不生效、@Autowired注入失败、Thymeleaf 模板路径找不到……最后答辩时被问“你这个@Service类里saveBid()方法加了@Transactional,那它底层是怎么保证数据库连接复用的?”,当场哑火。
而原生 Servlet/JSP 的优势在于:所有控制权都在你手里。

  • HttpServlet的doGet()/doPost()就是你的入口,参数怎么取、校验怎么写、跳转到哪个 JSP,逻辑完全透明;
  • HttpSession存用户 ID、角色、购物车(这里存竞拍意向),失效策略、跨域问题、序列化陷阱,你亲手调session.setMaxInactiveInterval(1800);
  • JDBC 的Connection、PreparedStatement、ResultSet全部显式管理,try-with-resources保证资源释放,setAutoCommit(false)+commit()/rollback()控制事务边界——这正是拍卖系统最核心的“出价原子性”基础。

提示:别被“老旧”二字吓退。Tomcat 9+ 完全支持 Servlet 4.0,JSP 2.3 语法(EL 表达式、JSTL 标签库)足够写清晰视图。毕业设计要展示的是对 Web 底层机制的理解深度,不是框架版本号。

2.2 项目结构怎么组织?——拒绝“所有文件塞 WebContent 下”的野路子

标准 Maven 结构虽好,但毕业设计常要求 WAR 包直接丢 Tomcat,所以采用经典目录布局(解压.rar后你会看到):

WebContent/ ├── index.jsp // 首页:最新拍品、热门分类 ├── login.jsp // 登录页(含验证码校验) ├── user/ │ ├── register.jsp // 用户注册(含密码强度前端校验) │ └── profile.jsp // 个人信息展示页面(JSP 里用 <c:if test="${user.role=='admin'}"> 控制管理员入口) ├── auction/ │ ├── list.jsp // 拍品列表(分页、按状态筛选:进行中/已结束/已流拍) │ ├── detail.jsp // 拍品详情(含实时倒计时JS、当前最高价、出价表单) │ └── publish.jsp // 发布新拍品(管理员权限) ├── order/ │ └── my_orders.jsp // 我的订单(含支付状态:待付款/已付款/已完成) ├── WEB-INF/ │ ├── web.xml // 核心配置:servlet 映射、filter、listener │ ├── lib/ // 放 mysql-connector-java-8.0.26.jar(注意版本!) │ └── classes/ // 编译后的 .class 文件(对应 src/ 目录) └── static/ ├── css/ │ └── style.css // 响应式布局,适配手机端浏览拍品 └── js/ └── countdown.js // 倒计时逻辑:用 new Date(endTime) - new Date() 计算毫秒差

关键点:

  • WEB-INF是安全边界,所有 JSP 和 class 文件放里面,外部无法直接 URL 访问;
  • web.xml中必须配置CharacterEncodingFilter(解决中文乱码)、LoginFilter(拦截未登录访问/user/*);
  • static/目录放纯静态资源,不走 Servlet,提升加载速度——这是很多毕设忽略的性能细节。

2.3 数据库设计:三张表撑起拍卖核心(不是五张表凑数)

别被“高大上ER图”忽悠。一个能跑通的最小可行拍卖系统,核心就三张表(MySQL 8.0,字符集 utf8mb4):

表名字段(关键)说明
t_userid(PK),username,password,real_name,id_card,balance,role('user'/'admin')用户表:balance字段存可用余额(单位:分,避免浮点数精度问题),role控制后台入口
t_auctionid(PK),title,description,start_price,current_price,step_price,deposit,end_time,status('ongoing'/'ended'/'failed'),seller_id(FK)拍品表:step_price强制出价阶梯(如 100 元),end_time是 DATETIME 类型,用于后端校验出价时效
t_bidid(PK),auction_id(FK),user_id(FK),price,bid_time,is_win(0/1)出价表:is_win=1表示该笔出价最终胜出(竞拍结束时由定时任务或 Servlet 批量更新)

注意:t_auction.status和t_bid.is_win是业务状态字段,绝不依赖前端传值!必须由后端根据end_time和current_price逻辑计算并更新。这是防刷单的第一道防线。


3. 关键业务落地:手写 Servlet 实现“出价”与“结算”两个生死环节

3.1 出价功能:一行request.getParameter("price")背后藏着五个校验关卡

用户在detail.jsp点击“出价”按钮,表单提交到BidServlet。这不是简单接收参数,而是五层过滤:

// BidServlet.java protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 登录态校验(Filter 已做,此处双重保险) User user = (User) request.getSession().getAttribute("user"); if (user == null) { request.setAttribute("msg", "请先登录"); request.getRequestDispatcher("/login.jsp").forward(request, response); return; } // 2. 拍品存在且状态为进行中 String auctionIdStr = request.getParameter("auctionId"); Auction auction = auctionService.findById(Integer.parseInt(auctionIdStr)); if (auction == null || !"ongoing".equals(auction.getStatus())) { request.setAttribute("msg", "拍品不存在或已结束"); request.getRequestDispatcher("/auction/list.jsp").forward(request, response); return; } // 3. 出价金额校验:必须 > current_price 且是 step_price 的整数倍 String priceStr = request.getParameter("price"); BigDecimal price = new BigDecimal(priceStr); BigDecimal currentPrice = auction.getCurrentPrice(); BigDecimal stepPrice = auction.getStepPrice(); if (price.compareTo(currentPrice) <= 0 || price.subtract(currentPrice).remainder(stepPrice).compareTo(BigDecimal.ZERO) != 0) { request.setAttribute("msg", "出价必须高于当前价且为加价幅度的整数倍"); request.getRequestDispatcher("/auction/detail.jsp?id=" + auctionIdStr).forward(request, response); return; } // 4. 用户余额校验(防止恶意刷价) if (user.getBalance().compareTo(price) < 0) { request.setAttribute("msg", "余额不足,请充值"); request.getRequestDispatcher("/user/profile.jsp").forward(request, response); return; } // 5. 数据库事务:扣余额 + 插出价记录 + 更新拍品当前价 Connection conn = null; try { conn = DBUtil.getConnection(); // 自定义工具类,获取连接 conn.setAutoCommit(false); // 关键!开启事务 // 扣用户余额(乐观锁:where balance >= ?) int balanceAffected = conn.prepareStatement( "UPDATE t_user SET balance = balance - ? WHERE id = ? AND balance >= ?") .executeUpdate(new Object[]{price, user.getId(), price}); if (balanceAffected == 0) { throw new RuntimeException("余额不足,可能已被其他出价占用"); } // 插入出价记录 conn.prepareStatement( "INSERT INTO t_bid (auction_id, user_id, price, bid_time) VALUES (?, ?, ?, NOW())") .executeUpdate(new Object[]{auction.getId(), user.getId(), price}); // 更新拍品当前价 conn.prepareStatement( "UPDATE t_auction SET current_price = ? WHERE id = ?") .executeUpdate(new Object[]{price, auction.getId()}); conn.commit(); request.setAttribute("msg", "出价成功!"); } catch (Exception e) { if (conn != null) try { conn.rollback(); } catch (SQLException ignored) {} request.setAttribute("msg", "出价失败:" + e.getMessage()); } finally { if (conn != null) try { conn.close(); } catch (SQLException ignored) {} } request.getRequestDispatcher("/auction/detail.jsp?id=" + auctionIdStr).forward(request, response); }

参数说明:

  • DBUtil.getConnection():封装了DriverManager.getConnection(),连接池用的是BasicDataSource(Apache Commons DBCP),避免每次新建连接;
  • balance >= ?的WHERE条件是乐观锁,防止并发出价时余额被重复扣除;
  • NOW()用数据库时间而非new Date(),确保时间一致性。

3.2 竞拍结束自动结算:不用 Quartz,用 ServletContextListener + 定时扫描

Spring Boot 用@Scheduled很方便,但原生方案更体现功底。我们在web.xml中注册监听器:

<!-- web.xml --> <listener> <listener-class>com.example.listener.AuctionEndListener</listener-class> </listener>

AuctionEndListener在 Tomcat 启动时启动一个守护线程,每 30 秒扫描一次:

// AuctionEndListener.java public class AuctionEndListener implements ServletContextListener { private ScheduledExecutorService scheduler; public void contextInitialized(ServletContextEvent sce) { scheduler = Executors.newSingleThreadScheduledExecutor(); // 延迟 10 秒启动,避免应用未完全初始化 scheduler.scheduleAtFixedRate(() -> { try { List<Auction> endedAuctions = auctionService.findEndedAuctions(); for (Auction a : endedAuctions) { settleAuction(a); // 结算逻辑 } } catch (Exception e) { e.printStackTrace(); } }, 10, 30, TimeUnit.SECONDS); } private void settleAuction(Auction auction) { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 1. 查找该拍品最高出价(且 bid_time < end_time) Bid winner = bidService.findHighestValidBid(auction.getId(), auction.getEndTime()); if (winner != null) { // 2. 更新拍品状态为 'ended',标记胜出者 conn.prepareStatement("UPDATE t_auction SET status='ended', winner_id=? WHERE id=?") .executeUpdate(new Object[]{winner.getUserId(), auction.getId()}); // 3. 生成订单(t_order 表) conn.prepareStatement( "INSERT INTO t_order (auction_id, buyer_id, seller_id, amount, status) VALUES (?, ?, ?, ?, 'unpaid')") .executeUpdate(new Object[]{ auction.getId(), winner.getUserId(), auction.getSellerId(), winner.getPrice() }); // 4. 扣除买家保证金(实际业务中可能是冻结,这里简化为扣减) conn.prepareStatement( "UPDATE t_user SET balance = balance - ? WHERE id = ?") .executeUpdate(new Object[]{auction.getDeposit(), winner.getUserId()}); } else { // 流拍:更新状态为 'failed' conn.prepareStatement("UPDATE t_auction SET status='failed' WHERE id=?") .executeUpdate(new Object[]{auction.getId()}); } conn.commit(); } catch (Exception e) { if (conn != null) try { conn.rollback(); } catch (SQLException ignored) {} } finally { if (conn != null) try { conn.close(); } catch (SQLException ignored) {} } } }

为什么不用数据库定时事件?
因为毕业设计要体现 Java 层逻辑控制能力。ScheduledExecutorService可控性强,日志可追踪,异常可捕获——这才是工程实践。


4. 避坑指南:那些让答辩老师皱眉、让系统上线就崩的 5 个血泪细节

4.1 现象:首页index.jsp显示乱码,中文变成??

原因:JSP 页面未声明编码,且 Tomcat 默认 ISO-8859-1 解析请求参数。
解决:

  • 在index.jsp顶部加<%@ page contentType="text/html;charset=UTF-8" %>;
  • 在web.xml中配置CharacterEncodingFilter,<init-param>设encoding为UTF-8;
  • MySQL 连接 URL 加?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai。

4.2 现象:用户 A 出价 1000 元,用户 B 紧接着出价 1050 元,但数据库里t_auction.current_price只更新到 1000 元

原因:两个 Servlet 并发执行,都读到旧的current_price,各自更新,后执行的覆盖前执行的(脏写)。
解决:

  • 绝对禁止SELECT current_price FROM t_auction WHERE id=?后再UPDATE;
  • 必须用原子 SQL:UPDATE t_auction SET current_price = ? WHERE id = ? AND current_price < ?,第三个?是用户出价,确保只更新比当前价低的记录。

4.3 现象:login.jsp输入正确账号密码,跳转到index.jsp却提示“未登录”

原因:HttpSession默认使用 Cookie 传递JSESSIONID,但浏览器禁用 Cookie 或跨域请求时失效;或web.xml中<session-config>的cookie-http-only设为true导致 JS 无法读取。
解决:

  • 开发阶段确认浏览器 Cookie 开启;
  • 在web.xml中显式配置<session-config><cookie-http-only>false</cookie-http-only></session-config>(仅开发用,生产环境应为true);
  • 更健壮做法:登录成功后,request.getSession().setAttribute("user", user),并在Filter中检查session.getAttribute("user") != null。

4.4 现象:detail.jsp倒计时显示“NaN:NaN:NaN”

原因:JavaScript 获取的endTime是字符串(如"2024-05-20 14:30:00"),直接new Date(endTime)在部分浏览器解析失败。
解决:

  • 后端传时间戳(毫秒数):request.setAttribute("endTimeMillis", auction.getEndTime().getTime());;
  • JSP 中用<script>var endTime = ${endTimeMillis};</script>,JS 里new Date(endTime)安全可靠。

4.5 现象:打包 WAR 部署到 Tomcat,访问http://localhost:8080/xxx/login.jsp报 404

原因:项目名(Context Path)配置错误,或web.xml中 servlet mapping 路径与实际访问路径不匹配。
解决:

  • 检查web.xml中<servlet-mapping>的<url-pattern>,确保LoginServlet映射为/login而非/servlet/login;
  • Tomcatconf/server.xml中<Host>的appBase指向webapps,WAR 包名决定 Context Path(auction.war→http://localhost:8080/auction/login.jsp);
  • 最稳妥:在 Eclipse/IDEA 中右键项目 → Properties → Web Project Settings → Context root 设为/auction,导出 WAR 时自动匹配。

5. 让答辩老师眼前一亮:三个可现场演示的“进阶技巧”与验证方法

5.1 用 Chrome DevTools 模拟并发出价,验证事务隔离性(比写 1000 字论文更有说服力)

答辩时,老师最想看的不是“我写了代码”,而是“我懂它怎么工作”。现场演示步骤:

  1. 打开两个 Chrome 无痕窗口,分别登录用户 A 和用户 B;
  2. 在detail.jsp页面,F12 打开 Console,粘贴以下代码(模拟快速连续出价):
// 窗口1(用户A):出价1000 fetch('/auction/bid', { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: 'auctionId=1&price=1000' }).then(r => r.text()).then(console.log); // 窗口2(用户B):1秒后出价1050 setTimeout(() => { fetch('/auction/bid', { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: 'auctionId=1&price=1050' }).then(r => r.text()).then(console.log); }, 1000);
  1. 切换到数据库客户端,执行SELECT current_price FROM t_auction WHERE id=1;—— 结果必须是1050,且t_bid表有两条记录。
    价值点:证明你理解了数据库事务的 ACID,而不是背概念。

5.2 给profile.jsp加一个“实名认证”开关,用@Override方法体展示面向对象设计能力

很多毕设的User类只有 getter/setter。你可以加一个verifyIdentity()方法:

public class User { private String realName; private String idCard; private boolean isVerified; // ... 构造函数、getter/setter ... public boolean verifyIdentity(String inputRealName, String inputIdCard) { // 1. 姓名长度校验 if (inputRealName == null || inputRealName.trim().length() < 2) return false; // 2. 身份证号 18 位 + 校验码(简化版) if (!inputIdCard.matches("\\d{17}[\\dXx]")) return false; // 3. 与用户填写信息一致 if (!inputRealName.equals(this.realName) || !inputIdCard.equals(this.idCard)) return false; this.isVerified = true; return true; } }

然后在ProfileServlet中调用,并在profile.jsp用<c:if test="${user.isVerified}">✅ 已实名</c:if>显示。
答辩话术:“老师,这个verifyIdentity()方法体现了单一职责原则——校验逻辑集中在此,后续如果要接入公安接口,只需重写此方法,不影响余额、订单等其他模块。”

5.3 用log4j.properties实现操作日志追踪,让“谁在什么时候出了什么价”可审计

毕业设计常被问“怎么保证数据安全?”。答案不是喊口号,是落地:
在WEB-INF/classes/log4j.properties中配置:

log4j.rootLogger=INFO, file log4j.appender.file=org.apache.log4j.RollingFileAppender log4j.appender.file.File=logs/auction.log log4j.appender.file.MaxFileSize=10MB log4j.appender.file.MaxBackupIndex=5 log4j.appender.file.layout=org.apache.log4j.PatternLayout log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%p] %c{1} - %m%n

在BidServlet的doPost方法开头加:

Logger logger = Logger.getLogger(BidServlet.class); logger.info("用户 " + user.getUsername() + " 对拍品 " + auctionIdStr + " 出价 " + priceStr + " 元");

部署后,logs/auction.log会自动生成,内容如:
2024-05-20 14:25:33 [INFO] BidServlet - 用户 zhangsan 对拍品 1 出价 1050 元
价值点:这比“系统有日志功能”这种空话有力得多——你展示了可落地的安全审计能力。

我带过 12 届毕设,见过太多学生花 3 周调通 Maven 依赖,却没想明白HttpSession和Cookie的关系;也见过有人把web.xml配置项背得滚瓜烂熟,却在答辩时说不清setAutoCommit(false)为什么不能放在finally块里。真正的技术深度,藏在你敢不敢删掉框架、亲手写Connection、PreparedStatement、HttpSession的那一刻。这个 JSP 拍卖系统不是怀旧,它是照妖镜——照出你对 Java Web 底层的理解是“会用”,还是“懂为什么”。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询