简介这套基于jspservlet的拍卖管理系统是一份面向计算机专业毕业设计或多课程实训的完整工程源码与数据库资源角色覆盖管理员和用户两端整合商品竞拍、分类管理、商品管理、订单管理等核心业务模块适合需要快速搭建Web项目或进行二次开发的学习者。压缩包内共438个文件约23.19MB包含jsp页面、Servlet及Java类、js、css、图片、字体图标以及sql数据库脚本与项目配置文件既可导入IDE运行预览也能对照源码理解前后端交互和JDBC操作流程。目前已有26人学习下载适合作为JavaWeb课程设计、毕业设计答辩预演或企业级电商竞拍场景的参考原型。资源附带完整的数据库初始化脚本和目录化素材能帮助使用者省去从零建表与配置环境的时间更高效地掌握拍卖流程设计中出价、限时竞拍、订单生成等关键逻辑。1. 基于 jspservlet 的拍卖管理系统为什么这个老组合还能当作源码库来用标题里这个基于 jspservlet 的拍卖管理系统是 Java Web 课程设计里最常见的一类项目Servlet 处理请求JSP 渲染页面MySQL 保存用户、拍品和出价记录。很多人觉得 JSP 已经过时但在这类小规模业务里它部署成本低、结构直观一条 Tomcat、一个数据库脚本就能跑起来。源码加数据库的形态也方便你看清从前端到后端的完整数据流。这篇文章会按表设计、Servlet 路由、并发出价、部署排错这条线展开新手能照着复现熟手则可以直接把里面的事务和并发处理挪到自己的项目里。2. 把拍卖业务拆成数据库表用户、拍品、出价记录要这样设计数据库是这套系统的地基。我看到很多人拿到源码后先急着部署结果运行时报表不存在、字段对不上就是因为没看数据库脚本。拍卖业务的核心动作有三个用户注册登录、发布拍卖品、对拍品出价。围绕这三个动作最少需要五张表用户表、拍品表、出价记录表、分类表再加一张订单或中拍记录表。下面按我自己的设计习惯逐张说明。2.1 用户表、拍品表、出价记录表的主键与外键设计用户表保存账号、密码、昵称、联系方式。密码不建议用明文常见做法是存 MD5 或 SHA-256 摘要。注意 JSPServlet 项目没有 Spring Security 之类的东西加密要自己写工具类或者用 JDK 自带的 MessageDigest。拍品表要保存拍卖的起拍价、当前价、加价幅度、开始时间和结束时间以及拍品图片路径。出价记录表是拍卖系统的核心每一次出价都要插入一条记录记录出价人、出价金额、出价时间并关联到对应的拍品。CREATE TABLE user ( id BIGINT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL COMMENT 登录账号, password VARCHAR(64) NOT NULL COMMENT MD5加密后的密码, nickname VARCHAR(50) DEFAULT NULL COMMENT 昵称, phone VARCHAR(20) DEFAULT NULL COMMENT 联系方式, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE auction_item ( id BIGINT NOT NULL AUTO_INCREMENT, item_name VARCHAR(100) NOT NULL COMMENT 拍品名称, description TEXT COMMENT 拍品描述, category_id INT DEFAULT NULL COMMENT 分类ID, start_price DECIMAL(10,2) NOT NULL COMMENT 起拍价, current_price DECIMAL(10,2) DEFAULT NULL COMMENT 当前最高价, step_price DECIMAL(10,2) DEFAULT NULL COMMENT 加价幅度, image_path VARCHAR(255) DEFAULT NULL COMMENT 图片路径, seller_id BIGINT NOT NULL COMMENT 发布人ID关联user.id, start_time DATETIME NOT NULL COMMENT 拍卖开始时间, end_time DATETIME NOT NULL COMMENT 拍卖结束时间, status TINYINT DEFAULT 0 COMMENT 0未开始 1进行中 2已结束 3流拍, PRIMARY KEY (id), KEY idx_end_time (end_time), KEY idx_seller_id (seller_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT拍卖品表; CREATE TABLE bid_record ( id BIGINT NOT NULL AUTO_INCREMENT, item_id BIGINT NOT NULL COMMENT 拍品ID, user_id BIGINT NOT NULL COMMENT 出价人ID, bid_price DECIMAL(10,2) NOT NULL COMMENT 出价金额, bid_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_item_time (item_id, bid_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT出价记录表;这段建表 SQL 里的字段我挑几个说明。金额一律用 DECIMAL(10,2)不要用 DOUBLE不然累加几次之后会出现 0.01 的误差。时间用 DATETIME 而不是 TIMESTAMP因为 TIMESTAMP 的范围有限而且在某些 MySQL 版本里会受时区影响。出价记录表的索引很有讲究按拍品和时间建联合索引查“某个拍品的出价历史”时就快得多。外键我故意没有加物理外键只留了逻辑关联字段理由是项目教科书喜欢加FOREIGN KEY但实际部署时加外键容易在删数据时被绊住而且很多件套代码里用的是 MyISAM 或者兼容模式外键约束不一定生效。所以这里用seller_id、item_id作为逻辑外键业务层去校验。2.2 初始化 SQLutf8mb4、时间字段、金额精度的具体配置初始化脚本除了建表还要给一些必要字段插入初始数据。比如分类表我会预先插入“字画、玉器、数码、家具”这几类让页面上的下拉框不至于空着。管理员账号也需要一条基础数据密码字段用 MD5 的admin生成串。注意一点如果你的 MySQL 是 5.5 以下的版本utf8mb4 是 mysql 5.5.3 之后才有的更老的版本要用 utf8。不过现在正常使用 5.7 和 8.0 都是支持 utf8mb4 的。mysql -uroot -p auction_db.sql这条命令会把auction_db.sql里的建表语句和 insert 语句全部执行。在 Windows 上执行时如果提示Using a password on the command line interface can be insecure不用担心这只是 MySQL 的警告不影响执行。有人喜欢用 Navicat 或者 idea 自带的数据库工具来导入也可以但要注意文件的编码。我一般把 SQL 文件存成 UTF-8 无 BOM 格式因为带 BOM 的脚本在 MySQL 8.0 里第一条语句可能报错。数据库初始化完成后第一件事不是写代码而是验证表结构。常用的命令是show create table bid_record;和desc auction_item;。如果你发现某个字段和源码里的实体类对不上比如源码里有个sellerName字段而表里没有就要先改表结构。这个阶段改表最便宜等代码跑起来再改就要处理脏数据了。很多同学遇到“Cant find column xxx”就是没做这一步。如果确实要改用ALTER TABLE加列、改类型或加索引也要同步改源码里的实体类和 DAO 的 SQL改完再用desc验证。2.3 JDBC 连接与 Druid 连接池黑匣子变成可见参数JSPServlet 项目不集成 MyBatis 的话数据库操作就是用 JDBC。单纯使用DriverManager.getConnection()在小型 demo 里够用但拍卖系统里每次出价都要写库频繁创建连接会让数据库响应变慢。常见做法是引入 Druid 连接池。Druid 是阿里开源的连接池组件也可以监控 SQL配置文件很简单。在src目录下放一个druid.properties然后在工具类里读进来。driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/auction_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse usernameroot password123456 initialSize5 minIdle1 maxActive20 maxWait60000import com.alibaba.druid.pool.DruidDataSourceFactory; import javax.sql.DataSource; import java.io.InputStream; import java.util.Properties; public class DbUtil { private static DataSource dataSource; static { try { InputStream in DbUtil.class.getClassLoader().getResourceAsStream(druid.properties); Properties props new Properties(); props.load(in); dataSource DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static DataSource getDataSource() { return dataSource; } }这段代码把连接池配置存在静态块里项目启动时加载一次。dataSource在后续所有 DAO 类里通过DbUtil.getDataSource().getConnection()取连接。注意 url 里的几个参数是血泪经验serverTimezoneAsia/Shanghai必须有否则 JDBC 驱动会报时区异常useSSLfalse避免 MySQL 8.0 默认 SSL 握手带来的警告characterEncodingutf8要和页面、数据库的编码一致。Druid 的maxActive20表示最多同时有 20 个连接超出后请求会等待maxWait60000毫秒60 秒内拿不到连接就报错。对于课程设计级别的并发量20 已经够了你要是想压测可以调大到 50但不要无限大因为每个连接都会占用 MySQL 的线程和内存。从这一节你能看到数据库设计不是几个建表语句的事。表结构决定业务能不能跑通连接参数决定部署完会不会半夜报错。后面所有 Servlet 里的 SQL 都是围绕这几张表做的增删改查。接下来我把请求链路从浏览器到 JSP 再到 Servlet 走一遍。3. Servlet 做路由、JSP 做展示从登录到出价的完整请求链路有了表结构下一步就是把页面提交的请求交给 Servlet 处理。在 JSPServlet 这个组合里JSP 本身也能写 Java 代码但那样维护起来非常痛苦。我一般把 JSP 只当作视图业务逻辑都放在 Servlet 和 Service 层页面里的% %尽量少出现。3.1 多个 Servlet 还是统一入口我用 BaseServlet 的 action 分发最直白的写法是一个功能一个 ServletLoginServlet、RegisterServlet、AddAuctionServlet、BidServlet……每个 Servlet 继承HttpServlet并重写doGet和doPost。对只有十来个页面的系统来说这完全没问题但每次加功能都要新建类web.xml 里也要多配置一行。我个人的习惯是用一个BaseServlet做统一入口通过method参数指定要调用的方法。这个思路在很多开源项目里都能看到Struts 的 DispatchAction 也是类似做法。import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.lang.reflect.Method; public abstract class BaseServlet extends HttpServlet { Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); resp.setContentType(text/html;charsetUTF-8); String methodName req.getParameter(method); if (methodName null || methodName.trim().isEmpty()) { resp.getWriter().write(缺少method参数); return; } try { Method method this.getClass().getMethod(methodName, HttpServletRequest.class, HttpServletResponse.class); String forward (String) method.invoke(this, req, resp); if (forward ! null !forward.startsWith(redirect:)) { req.getRequestDispatcher(forward).forward(req, resp); } else if (forward ! null forward.startsWith(redirect:)) { resp.sendRedirect(forward.substring(redirect:.length())); } } catch (NoSuchMethodException e) { resp.getWriter().write(方法不存在); } catch (Exception e) { e.printStackTrace(); resp.getWriter().write(服务器错误); } } }这段代码比一个个写 Servlet 方便在哪新加一个功能只要在子类里写一个同名public String方法返回要跳转的 JSP 路径不需要改 web.xml。比如拍卖品管理写一个AuctionServlet extends BaseServlet里面提供list、publish、bid、delete方法前端表单都提交到这个 Servlet靠methodpublish区分。方法的返回字符串redirect:开头时执行重定向否则执行请求转发。转发和重定向的区别要注意重定向会改变浏览器地址栏适合“提交后避免刷新再次提交”的场景转发只是服务端内部跳转URL 不变适合页面数据渲染。注意 setCharacterEncoding 一定要放在最前面并且要放在第一次读取请求参数之前。复制代码时经常看到有人把它放在写响应之后那实际请求参数还是乱码。使用反射调用会有轻微的性能损耗但拍卖系统这种低并发场景完全感知不到。如果你不喜欢反射也可以写一个 switch 分发两种都行我选择反射是觉得维护方便加个方法比加一行 case 更不容易出错。3.2 JSP 个人信息展示页与图片坐标定位那点事做用户中心的时候JSP 页面上要展示当前登录用户的信息包括头像、昵称、手机号还有这个用户发布过的拍品列表。个人信息展示页的 JSP 代码通常是这样的c:if test${not empty sessionScope.loginUser} div classuser-info img src${pageContext.request.contextPath}${loginUser.avatar} width80 height80/ span${loginUser.nickname}/span span${loginUser.phone}/span /div /c:if这里用 JSTL 的c:if判断登录状态${sessionScope.loginUser}是从 Session 取出的用户对象。图片的 src 一定要拼上pageContext.request.contextPath也就是项目部署路径否则在非根路径部署时图片全部 404。有人问“jsp 图片如何对坐标定位”这其实是个前端问题。JSP 本身只是生成 HTML图片上的可点击区域可以用map和area标签定义坐标比如在拍品图片上画几个热区或者用 CSS 的position: relative加left/top偏移。JSP 要做的只是把图片路径和坐标数据从后端输出到前端定位计算发生在浏览器里。我见过有人试图用 JSP 脚本去控制图片内部位置那方向就错了。3.3 Session 与登录过滤器防止串号和未登录访问拍卖系统的 Session 管理有一个典型的坑用户 A 登录后直接访问另一个用户中心页面的 URL结果看到的是自己的信息还是别人的如果你在 Servlet 里用request.getAttribute(user)且 attribute 名写错可能取到上一次请求的数据。更常见的问题是没有登录过滤器未登录用户也能直接打开出价链接导致空指针。我一般会写一个LoginFilter在 web.xml 里拦截所有受保护路径。filter filter-nameLoginFilter/filter-name filter-classcom.auction.filter.LoginFilter/filter-class /filter filter-mapping filter-nameLoginFilter/filter-name url-pattern/*/url-pattern /filter-mappingimport javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; public class LoginFilter 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); String uri request.getRequestURI(); boolean isLoginPage uri.endsWith(login.jsp) || uri.endsWith(register.jsp) || uri.endsWith(LoginServlet); boolean isStaticResource uri.contains(/css/) || uri.contains(/js/) || uri.contains(/images/); if (isLoginPage || isStaticResource) { chain.doFilter(req, resp); return; } if (session null || session.getAttribute(loginUser) null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); } }这段过滤器放行的规则要仔细看登录页、注册页、静态资源放行其余路径都要检查 Session。使用request.getSession(false)而不是getSession()避免过滤器给每个匿名请求都创建 Session否则内存会有大量无用 Session。放行列表里把/LoginServlet直接放行了这样登录请求本身可以到达 Servlet。有人会想那未登录用户直接访问userCenter.jsp呢只要 JSP 放在受拦截的目录就会被过滤器拉回登录页。实际项目中JSP 页面最好不要直接放在 webapp 根目录下而是放在/WEB-INF/pages/下面这样浏览器无法直接访问 JSP 文件只能通过 Servlet 转发进去安全性会好很多。但很多件套源码图省事直接放根目录这时候过滤器就是最后一道防线。4. 竞拍出价并发控制与拍卖状态判断拍卖系统最核心、最容易出 bug 的就是出价功能。表面上看就是一个 SQL insert但实际要考虑怎么确定当前最高价两个人几乎同时出价怎么办拍卖已经结束还允许出价吗这些问题不解决源码就是花架子。4.1 出价逻辑的最小实现先查最高价再插入还是单条 SQL我见过有人写出的 Servlet 逻辑是先查出当前最高价然后在 Java 里比较用户出的价格再 insert。这个思路在单线程测试时没问题但两个用户同时点“出价”时两头都查到同一个最高价都通过比较然后各插入一条自己的记录最后数据库里最高价就混乱了。正确做法是把“比较并写入”做成一个原子操作。public boolean placeBid(BidRecord record) throws SQLException { String sql UPDATE auction_item SET current_price ? WHERE id ? AND status 1 AND current_price ? AND end_time NOW(); try (Connection conn DbUtil.getDataSource().getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setBigDecimal(1, record.getBidPrice()); ps.setLong(2, record.getItemId()); ps.setBigDecimal(3, record.getBidPrice()); int rows ps.executeUpdate(); if (rows 1) { insertBidRecord(conn, record); return true; } return false; } }用一条 UPDATE 语句先更新拍品表的当前价更新条件里带上current_price ?和end_time NOW()。SQL 执行时数据库会对该行加锁如果当前价已经比这个出价高或拍卖已结束更新行数就是 0出价失败。只有更新成功才插入出价记录。这样避免了查询-判断-插入之间的空档。这里的关键是NOW()要用数据库时间而不是 Java 的new Date()。虽然两者通常一致但在服务器和数据库分离时会有时间差。一条 SQL 同时完成了状态判断和价格更新是这套系统里最值得参考的一笔。4.2 用事务和行锁处理同一拍品的同时出价上面的写法能解决“先取出旧值再更新”的丢失更新问题但插入出价记录和更新拍品价格之间依然有失败风险。比如更新成功了插入出价记录时数据库突然报错那拍品价格变了但出价记录丢了最后中拍者无从追溯。所以我在正式环境里会把这两步包在同一个事务中使用SELECT ... FOR UPDATE锁住拍品行。public boolean placeBidWithTx(BidRecord record) throws SQLException { String lockSql SELECT current_price, end_time, status FROM auction_item WHERE id ? FOR UPDATE; String updateSql UPDATE auction_item SET current_price ? WHERE id ?; String insertSql INSERT INTO bid_record(item_id, user_id, bid_price, bid_time) VALUES (?,?,?,NOW()); try (Connection conn DbUtil.getDataSource().getConnection()) { conn.setAutoCommit(false); conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); try (PreparedStatement psLock conn.prepareStatement(lockSql); PreparedStatement psUpdate conn.prepareStatement(updateSql); PreparedStatement psInsert conn.prepareStatement(insertSql)) { psLock.setLong(1, record.getItemId()); ResultSet rs psLock.executeQuery(); if (!rs.next()) { conn.rollback(); return false; } BigDecimal currentMax rs.getBigDecimal(current_price); int status rs.getInt(status); Timestamp endTime rs.getTimestamp(end_time); if (status ! 1 || record.getBidPrice().compareTo(currentMax) 0 || endTime.before(new Timestamp(System.currentTimeMillis()))) { conn.rollback(); return false; } psUpdate.setBigDecimal(1, record.getBidPrice()); psUpdate.setLong(2, record.getItemId()); psUpdate.executeUpdate(); psInsert.setLong(1, record.getItemId()); psInsert.setLong(2, record.getUser_id()); psInsert.setBigDecimal(3, record.getBidPrice()); psInsert.executeUpdate(); conn.commit(); return true; } catch (Exception e) { conn.rollback(); throw e; } } }这段代码有几个细节要讲清楚。第一FOR UPDATE是排他锁两个并发事务同时执行这条查询时第二个会阻塞直到第一个提交或回滚。这相当于把竞拍处理串行化对单件拍品来说完全正确。第二TRANSACTION_READ_COMMITTED是 MySQL 常用的隔离级别没必要用REPEATABLE_READ因为FOR UPDATE加锁已经解决并发了。第三所有操作都在同一个 Connection 上执行不能用DbUtil.getDataSource().getConnection()每次取新连接那样就不是同一事务了。注意endTime要取Timestamp而不是Date否则比较秒级时间会丢掉时分秒。事务里的顺序也很重要先查后改。如果先改再查可能出现自己更新后看到的当前价已经包含自己的出价然后插入重复记录。另外抛异常时一定要rollback()并且把数据库连接归还给连接池不然连接池很快被耗尽。有人用try-with-resources管理连接和语句但要注意rollback必须放在提交之后或异常分支里别无限递归。4.3 截止时间判定以服务器时间为准的两种写法拍卖结束时间的判定如果写在 Java 代码里用new Date().after(endTime)一旦服务器时钟被手动改过或者客户端和服务器在不同时区判断就不可靠。所以我更倾向把判定交给 SQL。前面 UPDATE 里已经带了end_time NOW()的条件这是第一种写法。另一种是单独写一个定时任务把已经到期的拍品状态改成“已结束”避免用户还能打开出价表单。UPDATE auction_item SET status 2 WHERE status 1 AND end_time NOW();这条 SQL 可以让定时任务每分钟执行一次。在 JSPServlet 环境里最简单的定时任务是使用ServletContextListener启动一个ScheduledExecutorService或者用Timer。不过要注意如果你直接把拍品状态改成“已结束”需要同时决定最高价最终落入哪笔出价记录。常见做法是查出出价最高且时间最早的那条记录在订单表里生成一笔中拍订单。public void finishAuction(long itemId) { String selectBid SELECT * FROM bid_record WHERE item_id ? ORDER BY bid_price DESC, bid_time ASC LIMIT 1; // 查到中拍者后插入 order 表并更新 item 状态为已结束 }排序规则是bid_price DESC, bid_time ASC。出价金额越高越靠前金额相同时最早出价者获胜。这个规则最好和拍卖规则保持一致并在页面上提前写清楚。不少件套代码里的 ORDER BY 写反了导致最后中拍人是最晚的低价那就成了笑话。5. 排错与避坑这套系统在部署和运行时的常见问题到这一章假设你已经跑起了项目开始点页面、测流程。下面几个问题是我在类似系统上翻车最多的按“现象-原因-解决”列清楚你可以直接对照排查。5.1 中文乱码问题页面、请求、数据库三层编码不一致现象JSP 页面显示中文正常但提交到 Servlet 后数据库里存的是???或者乱码。原因页面编码是UTF-8但 Servlet 里没有设置request.setCharacterEncoding(UTF-8)而且在读参数前没有设置或者数据库连接 URL 缺少characterEncodingutf8。解决在BaseServlet.service()最上面加request.setCharacterEncoding(UTF-8)response 也设置response.setContentType(text/html;charsetUTF-8)然后在 JSP 顶部放% page contentTypetext/html;charsetUTF-8 languagejava %。最后检查 MySQL 连接参数和表字符集要统一成 utf8mb4。5.2 数据库连接失败驱动版本、连接参数与防火墙现象本地运行时报ClassNotFoundException: com.mysql.jdbc.Driver或Communications link failure。原因第一种是 jar 包没放到WEB-INF/lib或驱动类名写错。MySQL 5.x 用com.mysql.jdbc.DriverMySQL 8.0 用com.mysql.cj.jdbc.Driver。第二种是 MySQL 服务没启动、端口不是 3306、或者机器上有防火墙。解决先telnet 127.0.0.1 3306测端口再用 Navicat 等工具测账号密码。特别提醒如果你是 MySQL 8.0连接 URL 必须带上serverTimezoneAsia/Shanghai否则驱动会抛时区异常。5.3 出价被覆盖的玄学并发时丢失更新现象两个人同时出价最终 price 更新结果不对或者有人明明出了高价页面上却还是低价。原因代码里先查询再判断没有锁两个事务同时读到旧值。解决使用我在第 4 章写的单条 UPDATE 加条件或者SELECT ... FOR UPDATE。实在不想改事务也可以用乐观锁给 auction_item 表加一个version字段UPDATE 时带上version ?更新成功后version1。但竞拍场景本来就是行级冲突强悲观锁更直接。注意出价失败要给用户明确提示不能只返回 false 仍显示成功。5.4 时间显示不对时区、Tomcat 与 JDBC 各差 8 小时现象页面写了一条出价记录显示的bidTime比本地时间早或晚 8 小时。原因MySQL 连接 URL 没有设置serverTimezoneAsia/Shanghai或者 JVM 默认时区不是中国时区导致 JDBC 读取时间时按 UTC 转换。解决在连接 URL 加serverTimezoneAsia/Shanghai同时在 Tomcat 启动参数CATALINA_OPTS里加-Duser.timezoneAsia/Shanghai。如果你的数据库本身存的已经是错误时间需要检查 MySQL 的time_zoneSELECT global.time_zone, session.time_zone;然后考虑修正配置后重新插入测试数据而不是急着改历史数据。5.5 图片上传后 404路径、虚拟目录与重启丢失现象图片能上传成功数据库里也存了路径但 img 标签显示 404或者重启 Tomcat 后上传的图片没了。原因代码里把图片写到了项目部署目录比如webapp/upload而 Tomcat 重启或 redeploy 时会清空临时目录或者实际工作目录不是项目源码目录。解决在 Tomcat 里配置虚拟目录把上传文件存在项目外部比如D:/auction_upload/。在server.xml的Host里加Context path/upload docBaseD:/auction_upload reloadablefalse/。代码里保存图片时写入这个物理目录页面访问时用相对路径。注意保存路径和访问路径不要混在一起很多翻车都是用了getServletContext().getRealPath()。6. 跑通并验证整个拍卖系统本地部署的最终检查清单这一章讲怎么从零把这个系统跑起来并验证核心链路没有问题。假设你已经拿到了源码和数据库脚本环境是 JDK8 Tomcat8.5 MySQL5.7 或 8.0。第一步把源码导入 Eclipse 或 IDEA用 Maven 的话直接刷新依赖如果是 lib 目录已有 jar 包的件套要检查WEB-INF/lib下有没有 servlet-api.jar 和 MySQL 驱动。第二步创建数据库并导入 SQL。第三步修改 druid.properties 里的用户名密码。第四步配置 Tomcat 并启动。# Linux / Mac 下启动 Tomcat cd /path/to/apache-tomcat-8.5/bin ./startup.sh # Windows 下启动 startup.bat启动成功后看日志有没有INFO: Server startup in xxx ms没有就去看logs/catalina.out。然后访问http://localhost:8080/auction。如果用了 contextPath注意是/auction不是/。我先在页面注册一个测试用户随便填昵称和手机号然后去发布一件拍品起拍价 100加价幅度 10结束时间设为 10 分钟后。再用另一个浏览器或隐身窗口注册第二个用户登录后对这个拍品出价 110刷新详情页确认最高价变成了 110。接着尝试出一口 105 的价格系统应该提示出价失败因为 105 低于当前最高价。再让第一件拍品时间到点调低数据库的结束时间或者用 SQL 手动改时间触发生成中拍订单确认中拍记录是最早最高价。-- 手动把某个拍品结束时间改成过去触发结束逻辑 UPDATE auction_item SET end_time NOW() - INTERVAL 1 SECOND WHERE id 1;一个细节很多源码会在 Servlet 里写死本地 IP 或 localhost。如果你的数据库在另一台机器上url 里的localhost要改成那台机器的 IP同时保证 MySQL 授权允许远程访问。我见过有人改了 url 但 MySQL 用户权限只允许 localhost依然连不上最终要执行GRANT ALL ON auction_db.* TO root% IDENTIFIED BY ...。这里就要注意安全生产环境肯定不能用%。作为本地验证无所谓。最后分享一个我的验证习惯在跑完常规流程后我习惯用curl直接打几个 GET 接口测试未登录访问是否会跳转到登录页。比如curl -I http://localhost:8080/auction/userCenter.jsp如果返回 302 到 login.jsp说明过滤器生效。然后我再开两个浏览器窗口同时出价看最终数据库里的价格是不是只被更新到最高值以及出价记录是不是只有一条生效。这两个测试过了我就不再担心部署现场出幺蛾子了。希望帮到你。本文还有配套的精品资源点击获取