JSP+MySQL校园二手交易系统实战:从数据库设计到避坑部署全解析

发布时间:2026/10/7 3:01:29
JSP+MySQL校园二手交易系统实战:从数据库设计到避坑部署全解析 简介面向毕业设计/课程设计的校园二手物品交易系统完整源码基于 B/S 架构与 SSMSpringSpringMVCMyBatis框架采用 JSP、MySQL 和 Maven 构建帮助开发者快速掌握前后台分离的二手交易平台开发流程。压缩包内共 202 个文件约 12.63MB包含 Java 源码、JSP 页面、CSS/JS 前端资源、SQL 脚本及项目配置文件其中 Java 与 JSP 承载核心业务逻辑CSS/JS 负责界面交互SQL 脚本可直接初始化数据库。目前已有 21 人学习适合正在完成相关课题或希望入门 SSM 整合开发的学习者参考使用。资源包含完整的管理员后台与用户前台功能如注册登录、物品发布、搜索购买、订单管理、分类管理等前后台分层清晰按 Controller、Service、Mapper 三层组织源码可直接部署运行也便于二次开发并附有数据库脚本与说明文档适合毕业设计答辩和课程实践的重要参考。1. 为什么选 JSP MySQL 做校园二手交易这个组合在今天仍能打的三个理由如果你正为毕业设计或课程设计找方向基于JSP和MySQL实现的校园二手物品交易系统会频繁出现在搜索结果里。这套技术栈常被调侃“老”但它恰恰是学生项目里交付门槛最低、最容易讲清楚业务闭环的搭配JSP负责把页面动态渲染出来Servlet处理请求流转MySQL存用户、商品和订单数据Tomcat一跑就是一个完整可演示的Web应用。很多同学纠结要不要赶Spring Boot的时髦我的看法是毕设评审看的是你对业务和数据库的理解而不是框架版本号JSP的MVC结构反而让每一层都暴露在眼前答辩时不心虚。这套系统的典型使用场景是校园内的低门槛交易——大四学长出考研资料毕业生甩卖床垫台灯同校学生买卖二手自行车。相比闲鱼这类平台它的优势是“同校信任线下自提”所以业务模型里天然带着学号验证、校园分类、站内留言这些功能点。我接下来会用一套可复现的工程结构把数据库设计、核心页面、Servlet逻辑和部署配置全部拆开讲并标注清楚最容易让人翻车的坑比如JDK版本不匹配导致JSP编译失败、MySQL 8的认证插件连不上、表单提交后中文乱码等。这套东西做完你拿到的不仅是一份能跑的源码更是一整套能跟评委讲清楚的设计思路。文章面向两类人一类是时间紧、想要直接改改用的学生另一类是打算理解JSP全链路、想自己重写一遍的初学者。下面所有代码块都按可复制到IDE直接运行的标准来写。2. JSP Servlet MySQL 的经典三层结构先理清请求是怎么绕一圈的2.1 从浏览器输入网址到页面渲染JSP在这条链路上扮演什么角色校园二手交易系统看起来功能很多但拆开就是“浏览商品、登录、发布、留言、下单”这几个动作。每个动作在JSP架构里都遵循同一条路径浏览器发起HTTP请求Tomcat根据web.xml里的映射规则找到对应的ServletServlet调用DAO层去MySQL里查数据拿到结果后把数据塞进request或session再转发forward或重定向redirect给JSP页面。JSP负责把Java代码的结果渲染成HTML最终由浏览器解析展示。这个链路里最容易让新手误解的是JSP和Servlet的关系——JSP本质上是一个被Tomcat编译成Servlet的模板文件它并不是独立于Servlet之外的另一种技术。很多初学同学以为JSP就是写HTML的地方把JDBC连接代码也堆在JSP里结果页面里全是% ... %脚本片段后期根本维护不了。我一般会建议按“Servlet负责干活JSP只负责显示”的原则分工这对应的是Model 2架构也就是MVC的Web实现。在这样的结构里模型是JavaBean和DAO视图是JSP控制器是Servlet。二手交易系统需要的Servlet不会太多按业务划分大概是用户登录注册一个、商品发布和管理一个、留言和订单各一个再加上一个首页加载商品列表的。用这种方式写出来的项目答辩时能清楚画出请求流程图这是加分项。下面这张表是我做这套系统时习惯分配的三层职责照着这个边界写代码基本不会乱。层次典型文件职责与数据库关系视图层index.jsp、login.jsp、publish.jsp渲染HTML、接收表单参数、显示EL表达式结果不直接接触JDBC控制层UserServlet、GoodsServlet、OrderServlet解析请求参数、调用业务方法、决定跳转路径通过DAO间接访问数据模型层UserDao、GoodsDao、Goods、User封装数据库访问逻辑、承载表结构对应的字段直接写JDBC或DBUtils2.2 建立工程骨架这里每个文件夹都被Tomcat规定了用途先建出一个符合Tomcat规范的Web工程。用IDEA新建一个Java Enterprise工程勾选Web Application模板它会自动生成src目录和web目录。需要手动补上的文件夹是WEB-INF下的lib放MySQL驱动包和classes放编译后的class文件。如果你的IDEA里用的不是这种方式直接手工创建同样的目录结构也可以关键是把下面的结构保持住src/ ├─ com/example/pojo/ # 实体类User、Goods、Comment ├─ com/example/dao/ # 数据库操作UserDao、GoodsDao ├─ com/example/servlet/ # 控制器LoginServlet、GoodsServlet ├─ com/example/util/ # 工具类DBUtil、StringUtil └─ com/example/filter/ # 编码过滤器解决中文乱码 web/ ├─ index.jsp # 商品列表首页 ├─ login.jsp # 登录页 ├─ register.jsp # 注册页 ├─ publish.jsp # 发布商品页 ├─ goods_detail.jsp # 商品详情/留言页 └─ WEB-INF/ ├─ web.xml # Servlet映射、欢迎页配置 └─ lib/mysql-connector-java-xxx.jar这里解释一下为什么要单独分出util包。DBUtil是全局数据库连接入口如果把它放在DAO里每个DAO都会重复一段“加载驱动、获取连接”的代码。单独提出来之后连接地址、用户名、密码只在一个类里配置后期切换本机数据库或部署到服务器时只改一处。项目发布到Tomcat后WEB-INF目录下的内容浏览器无法直接访问这正好用来放web.xmlJSP页面不会通过地址栏暴露这个目录的物理路径。2.3 让项目在Tomcat下跑起来的最小组件如果你的搜索记录里有“mysql安装教程”“mysql在windows10上怎么安装”“jsp页面加载”这些词那大概率你遇到过环境层面的卡壳。跑这套系统最朴素的配置是这样JDK 8或JDK 11、Tomcat 8.5或9.0、MySQL 5.7。选择这三个版本的组合是有原因的JSP在Tomcat 8.5以后对EL表达式的支持更稳定MySQL 5.7的认证插件是mysql_native_password和JDBC驱动的兼容性比MySQL 8默认的caching_sha2_password省心得多。如果你装了MySQL 8连接字符串里必须加上allowPublicKeyRetrievaltrueuseSSLfalse否则会报“Public Key Retrieval is not allowed”的错误这个问题我在第五部分会细说。一个最容易忽略的细节是MySQL驱动jar包版本要和数据库版本匹配。很多同学下载了最新的mysql-connector-java-8.0.x去连MySQL 5.7这没问题但反过来用5.1.x驱动去连MySQL 8就会报CommunicationsException。我的习惯是用5.1.49配MySQL 5.7或者用8.0.33配MySQL 8配分清楚动机。部署顺序也有讲究先启动MySQL用命令行或Navicat建好数据库和表再启动Tomcat。如果先启动Tomcat项目里的连接池初始化阶段会一直报错日志刷得很快但大家经常忽略。3. 数据库设计全套SQL三张核心表与三个极易出错的外键3.1 用户表、商品表、订单留言表怎么定义才经得起答辩追问校园二手交易系统的数据库设计是答辩重点评委通常会让解释“为什么这么设计”。我的思路是四张表user用户、goods商品、comment留言、orders交易记录。很多同学觉得订单和留言可以合并成一张实际拆开后业务边界更清晰——留言是“我想要”订单是“我买了”状态流转不同。下面是一套可以直接执行的建表SQL字段类型和长度都按实际业务需求估过CREATE DATABASE IF NOT EXISTS campus_trade DEFAULT CHARACTER SET utf8mb4; USE campus_trade; CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL COMMENT 学号登录账号, password VARCHAR(32) NOT NULL COMMENT 存储MD5值不存明文, nickname VARCHAR(30) DEFAULT NULL, phone VARCHAR(11) DEFAULT NULL, avatar VARCHAR(255) DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_student_no (student_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE goods ( id INT NOT NULL AUTO_INCREMENT, user_id INT NOT NULL COMMENT 发布者关联user.id, title VARCHAR(50) NOT NULL, description TEXT, price DECIMAL(10,2) NOT NULL DEFAULT 0, category VARCHAR(20) DEFAULT 其他, image VARCHAR(255) DEFAULT NULL, status TINYINT DEFAULT 1 COMMENT 1上架 0下架 2已售出, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品表; CREATE TABLE orders ( id INT NOT NULL AUTO_INCREMENT, goods_id INT NOT NULL, buyer_id INT NOT NULL, seller_id INT NOT NULL COMMENT 冗余卖家ID方便查询, price DECIMAL(10,2) NOT NULL COMMENT 成交价下单时快照, status TINYINT DEFAULT 0 COMMENT 0待付款 1已付款 2已完成, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_goods_id (goods_id), KEY idx_buyer_id (buyer_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表;在设计表的时候有一处刻意为之的细节orders表里同时存了buyer_id和seller_id。理论上卖家可以从goods.user_id查出来但做“我卖出的”和“我买到的”两个订单列表时双字段写法能少一次连表查询这是数据冗余换查询效率的典型案例答辩时主动提出来反而加分。商品表的status字段我用TINYINT而不是VARCHAR因为状态只有上架、下架、已售出三种数字维护比重写中文更不容易出错。注意所有字段都显式指定DEFAULT否则用框架反向生成实体类时非空字段会让插入语句直接报错。3.2 查询语句里的三个高频命中的写法分页、模糊搜索、连表查昵称商品列表页不可能一次性把所有商品全查出用LIMIT做分页是标准动作。我见过不少同学在这里踩坑他们用LIMIT 10, 20写死了偏移量结果翻到第三页后数据不对。正确做法是在Servlet里接收page参数计算出偏移量再传给DAO-- 分页查询page为页码pageSize为每页条数 SELECT * FROM goods WHERE status 1 ORDER BY create_time DESC LIMIT #{offset}, #{pageSize}; -- 搜索场景注意标题和描述同时匹配用 OR 而不是 AND SELECT * FROM goods WHERE status 1 AND (title LIKE CONCAT(%, #{keyword}, %) OR description LIKE CONCAT(%, #{keyword}, %) OR category LIKE CONCAT(%, #{keyword}, %)) ORDER BY create_time DESC;模糊搜索里最容易被忽略的是CONCAT(%, ?, %)这种写法很多同学会直接写成%${keyword}%这在JSPJDBC里意味着用字符串拼接SQL存在注入风险。虽然毕设系统不要求绝对安全但答辩评委很可能会问“你怎么防SQL注入”用PreparedStatement的占位符配合CONCAT就是标准答案。连表查询在商品详情页会出现一次——查商品信息时把发布者的昵称和手机号带出来这样页面不用再发一次请求。SQL写法用LEFT JOIN user ON goods.user_id user.id注意别用INNER JOIN万一用户被删了记录商品详情页会直接空白。3.3 两处外键约束到底要不要在数据库层面加关于外键不少教程里推荐“物理外键”。我在这个项目里建议建立逻辑关联而不添加FOREIGN KEY约束原因是毕设系统的数据量级极小物理外键的维护成本远大于收益。比如发布商品的用户被删除时如果商品表有FOREIGN KEY (user_id) REFERENCES user(id)删除用户的SQL会直接被数据库拒绝而逻辑外键模式下我们可以在删除用户前手动把其商品全部下架或删除。这样业务逻辑更可控也避免了在代码里处理一堆数据库异常。但要注意goods.user_id和orders.buyer_id上必须建普通索引KEY否则按用户查询商品列表时全表扫描会让页面变慢。虽然是数据的“关系”由代码维持但索引不能省。4. 页面与Servlet的约定登录、发布、列表刷新的完整实现4.1 用户登录与登录态保持用session还是cookie登录模块是整个系统的门面写得好不好直接影响后续所有页面的权限控制。最常见的设计是login.jsp提交表单到LoginServletServlet里校验学号和密码是否匹配成功后把User对象放进session然后重定向到首页失败则返回登录页并带一个错误提示。下面这个Servlet代码是完整可用的WebServlet(/login) public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(utf-8); String studentNo request.getParameter(student_no); String password request.getParameter(password); // 对密码做MD5加密后再比对避免数据库存明文 String md5Pwd MD5Util.md5(password); UserDao dao new UserDao(); User user dao.findByStudentNoAndPassword(studentNo, md5Pwd); if (user null) { // 失败时带着错误码回登录页页面用EL表达式显示 request.setAttribute(error, 学号或密码错误); request.getRequestDispatcher(login.jsp).forward(request, response); } else { HttpSession session request.getSession(); session.setAttribute(loginUser, user); session.setMaxInactiveInterval(30 * 60); // 30分钟过期 response.sendRedirect(index.jsp); } } }这里有两个值得展开的设计决策。第一是密码存MD5值而不是明文虽然MD5早已不算安全哈希但毕设系统里它足以防止“数据库被导出后密码裸奔”这类低级问题。答辩时如果能补一句“实际生产会用BCrypt加盐哈希”评委一般不会再深究。第二是登录成功后用sendRedirect而不是forward这能避免刷新页面时表单重复提交是“PRG模式”里的重定向环节。如果你发现刷新登录页会弹“确认重新提交表单”就是少了这一步。页面侧判断用户有没有登录用JSP的c:if配合session对象就能实现% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % c:choose c:when test${not empty sessionScope.loginUser} 欢迎你${sessionScope.loginUser.nickname} a hrefgoods?actionmy我发布的/a a hreflogout退出/a /c:when c:otherwise a hreflogin.jsp登录/a | a hrefregister.jsp注册/a /c:otherwise /c:choose4.2 发布商品页表单提交到Servlet再插入数据库的完整链路发布商品功能是区分“完整项目”和“演示玩具”的分水岭。一个合格的发布页面至少要接收标题、分类、描述、价格、图片五个字段然后插入数据库并跳转到详情页。这里最容易出问题的地方是图片上传——很多JSP教程停留在“填一个图片URL”的阶段但实际使用中用户没法手动输入URL。如果要实现真正的文件上传Servlet要用MultipartConfig注解配合request.getPart(image)同时需要引入commons-fileupload和commons-io依赖。这个动作比较长先看常规版本的GoodsServlet里处理发布的代码MultipartConfig(maxFileSize 1024 * 1024 * 5) // 限制图片大小5MB WebServlet(/goods) public class GoodsServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(utf-8); String action request.getParameter(action); if (publish.equals(action)) { // 从session取当前登录用户如果为空则踢回登录页 User loginUser (User) request.getSession().getAttribute(loginUser); if (loginUser null) { response.sendRedirect(login.jsp); return; } String title request.getParameter(title); String description request.getParameter(description); String category request.getParameter(category); String priceStr request.getParameter(price); Goods goods new Goods(); goods.setUserId(loginUser.getId()); goods.setTitle(title); goods.setDescription(description); goods.setCategory(category); goods.setPrice(new BigDecimal(priceStr)); goods.setStatus(1); // 新发布默认上架 // 如果有图片上传则保存到指定目录 Part part request.getPart(image); if (part ! null part.getSize() 0) { String fileName UUID.randomUUID().toString().replace(-, ) getExtension(part.getSubmittedFileName()); String savePath getServletContext().getRealPath(/uploads) File.separator fileName; part.write(savePath); goods.setImage(uploads/ fileName); } GoodsDao dao new GoodsDao(); boolean success dao.insert(goods); if (success) { response.sendRedirect(goods?actiondetailid goods.getId()); } else { request.setAttribute(error, 发布失败); request.getRequestDispatcher(publish.jsp).forward(request, response); } } } }这段代码里隐含了三个容易被忽略的细节。第一文件名的生成用了UUID而不是用户原名否则两个人都传photo.jpg会发生覆盖。第二保存路径用getServletContext().getRealPath(/uploads)这是Tomcat下Web应用的物理路径部署到服务器后会自动指向发布目录不需要手工改配置。第三页面里form的enctype必须设置为multipart/form-data否则「取不到请求参数所有字段都是null」你如果搜索过“jsp页面表单提交null”大概率就是漏了这句。对应的JSP表单片段是这样form actiongoods methodpost enctypemultipart/form-data input typehidden nameaction valuepublish 标题input typetext nametitle requiredbr 分类 select namecategory option value学习资料学习资料/option option value电子产品电子产品/option option value生活用品生活用品/option /selectbr 价格input typenumber step0.01 nameprice requiredbr 描述textarea namedescription rows5 cols30/textareabr 图片input typefile nameimage acceptimage/*br button typesubmit发布商品/button /form4.3 首页商品列表与分页展示的JSP写法首页列表是数据流向视图的最直观示例。我们会在index.jsp里通过c:forEach循环渲染request里的商品集合并用${goods.price}这类EL表达式直接取值。分页组件需要从Servlet传来的总记录数、当前页码、每页条数来计算总页数。这里给一个可用的列表渲染核心片段% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % div classgoods-list c:forEach items${goodsList} vargoods div classgoods-item a hrefgoods?actiondetailid${goods.id} c:choose c:when test${not empty goods.image} img src${goods.image} alt${goods.title} width200 height150 /c:when c:otherwise img srcimages/default.png alt暂无图片 width200 height150 /c:otherwise /c:choose h3${goods.title}/h3 /a p classpricefmt:formatNumber value${goods.price} pattern#.##//p p classmeta${goods.category} · ${goods.createTime}/p /div /c:forEach /div c:if test${totalPages 1} div classpagination c:forEach begin1 end${totalPages} varpage a hrefgoods?actionlistpage${page}${page}/a /c:forEach /div /c:if先取出商品集合再进行循环渲染。这里要注意的是createTime字段类型是DATETIMEJSTL的fmt:formatNumber只能格式化数字格式化日期必须用fmt:formatDate两者不要混淆。商品图片路径在4.2里存的是uploads/xxx.jpg这种相对路径页面渲染时浏览器会相对于当前域名解析也就是http://localhost:8080/项目名/uploads/xxx.jpg能正常访问的前提是Tomcat没有拦截/uploads目录的静态资源请求。如果你的项目里静态资源全被404检查一下web.xml里是否配置了servlet-mapping//servlet-mapping把默认Servlet覆盖了这个问题出现了很多次。5. JSPMySQL 常见问题与避坑清单从“跑不起来”到“乱码与连不上库”5.1 Tomcat启动即报错Commons DbUtils或驱动类找不到的排查顺序如果你的context.xml里配置了DriverManager方式获取连接项目启动后控制台报ClassNotFoundException: com.mysql.jdbc.Driver排查顺序应该这样走第一步确认mysql-connector-java-x.x.x.jar确实在WEB-INF/lib目录下而不是只在IDEA的External Libraries里第二步看MySQL驱动jar包的版本如果用的是8.x驱动注册驱动的类名变成了com.mysql.cj.jdbc.Driver旧写法com.mysql.jdbc.Driver虽然还能兼容但会打印警告第三步检查WEB-INF/lib是否被IDEA标记成资源目录。这里有一个血泪经验IDEA的Artifacts输出设置里如果lib没有被添加到WEB-INF/lib的对应位置运行配置会静默跳过这个目录页面能打开但所有数据库操作都报驱动找不到。解决后顺手做一件事在DBUtil的静态代码块里加一段判断把SQL驱动加载和连接URL从硬编码改成可配置用Properties读取db.properties文件这样发布到别的电脑上时只需要改配置文件不用重新打包。下面这个DBUtil是我在这个项目里一直沿用的写法可以直接复制改配置public class DBUtil { private static String url; private static String username; private static String password; static { try { // 读取src根目录下的db.properties InputStream in DBUtil.class.getClassLoader() .getResourceAsStream(db.properties); Properties prop new Properties(); prop.load(in); Class.forName(prop.getProperty(driver)); url prop.getProperty(url); username prop.getProperty(username); password prop.getProperty(password); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, username, password); } public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { if (rs ! null) try { rs.close(); } catch (SQLException ignored) {} if (ps ! null) try { ps.close(); } catch (SQLException ignored) {} if (conn ! null) try { conn.close(); } catch (SQLException ignored) {} } }这么做的好处一是把JDBC4.0自动加载驱动的行为显式化二是避免在DAO里反复处理try-catch连接关闭的逻辑集中在一个方法里不容易漏掉。很多项目最后出现“连接数不够Tomcat卡死”的现象多半就是某个查询路径里rs或ps没关长期运行把连接池耗尽。5.2 MySQL 8 连接报 Public Key Retrieval is not allowed“mysql安装教程8.0”相关搜索排得很靠前说明确实有大量同学在用MySQL 8跑老项目。MySQL 8默认的认证插件是caching_sha2_password而JDBC驱动尤其是5.x版本默认不信任服务器的公钥会报Public Key Retrieval is not allowed。这个错我看到过无数遍。解决办法有两个一是建用户时指定认证插件为mysql_native_password二是在JDBC连接URL上追加两个参数。两种都列一下-- 方案A在MySQL里修改已有用户的认证插件 ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码; FLUSH PRIVILEGES;// 方案B连接URL上追加参数我推荐这个因为不用改数据库 String url jdbc:mysql://localhost:3306/campus_trade ?useSSLfalseallowPublicKeyRetrievaltrue characterEncodingutf8serverTimezoneAsia/Shanghai;方案B里的useSSLfalse也值得解释一下MySQL 8的JDBC驱动默认要求SSL连接本地开发环境没有配置证书直接连就会因为SSL握手失败而报错所以必须关闭。serverTimezone参数是因为MySQL 8的时区默认是UTC和本地时间对不上时操作DATETIME类型会报The server time zone value ???ú±ê׼ʱ?? is unrecognized。这两个参数写全基本能解决八成“驱动连不上数据库”的问题。5.3 中文乱码解决请求参数和页面显示的双向乱码JSP页面显示中文出现“”或者æ ¸¡å这类乱码原因基本是两个方向要么是请求参数编码不对要么是响应输出编码不对。请求方向的解决方法是给每个doPost开头加request.setCharacterEncoding(utf-8)但如果你有很多Servlet每个都写一遍太容易漏。更优雅的做法是写一个CharacterEncodingFilter在web.xml里配置filter-mapping让它拦截所有URLfilter filter-nameencodingFilter/filter-name filter-classcom.example.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueutf-8/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping响应方向的问题往往出在JSP页面头部声明上。如果页面顶部漏了% page contentTypetext/html; charsetutf-8 %Tomcat默认按ISO-8859-1输出HTML浏览器解析时中文全变乱码。所以每个JSP页面的第一行必须带着这句page指令。另外还要注意MySQL数据库和连接串编码的一致性建库时用utf8mb4连接串里写characterEncodingutf8页面和过滤器都是UTF-8三层对齐后基本不会出现乱码。我在实际排查时见过一种隐蔽情况页面、过滤器和连接串都正常唯独数据库表在创建时用了latin1这时JSP一切正常但查询出来的中文全是问号。检查手段是执行SHOW CREATE TABLE goods\G看DEFAULT CHARSET是不是utf8mb4。5.4 运行IDEA时端口被占用与404的定位方法Tomcat默认端口是8080如果本地开了其他服务占用了8080启动时会报Port 8080 was already in use。解决方式有两个方向一是找出占用进程杀掉Windows下在cmd执行netstat -ano | findstr 8080拿PID去任务管理器结束二是直接改Tomcat的端口在conf/server.xml里把Connector port8080改成8081。我建议新手直接用第二个方案因为杀进程容易误杀系统服务。改动后访问地址变成http://localhost:8081/项目名/。另一个高频问题是项目能启动但首页404。确认三件事web.xml里有没有配置welcome-file-list指向index.jspindex.jsp是否真的放在了web根目录下浏览器访问的URL里有没有带项目上下文路径。IDEA里Tomcat的Application context默认是/项目名_war_exploded如果你设置的部署上下文是/就直接访问http://localhost:8080/否则要拼上路径。很多搜索“基于jsp的毕业论文管理过程系统设计与实现”的同学后来发现404都是因为URL路径写错而不是代码有bug。5.5 数据删除后页面空白关联查询中LEFT JOIN没留空的预案这是最后一个经典问题。商品详情页通常会连表查出发布者昵称但如果管理员在后台把某个用户删掉了goods.user_id对应的用户不存在时LEFT JOIN会返回NULL。JSP页面如果直接用${seller.nickname}EL表达式会把null渲染成空字符串页面不至于报错但如果用了goods.sellerId再去做一次数据库查询就可能因为查不到记录而抛出异常。解决的办法是在JSP里给默认值c:choose c:when test${not empty seller.nickname} ${seller.nickname} /c:when c:otherwise 匿名用户 /c:otherwise /c:choose不要嫌这行代码多余。答辩现场演示时如果恰好在后台删了一个用户再回来看商品详情页面炸掉会非常尴尬。所有连表查询出来的字段都要假设它可能为null页面展示层做兜底。6. 从能跑到能答辩验证功能完整性的五个自测步骤与三个扩展方向当系统已经能运行起来下一步不是急着写论文而是按照“用户视角”把整个业务流程走一遍。我建议准备一份测试清单按顺序执行第一步注册一个新账号检查学号重复时是否有错误提示密码在数据库里是否以MD5密文存储第二步登录后发布一件商品上传一张图片确认图片能在列表页和详情页正常显示第三步换一个账号登录搜索刚刚发布的商品发起留言再下订单第四步用卖家账号登录查看“我卖出的”订单列表点击“确认交易完成”第五步退出登录后尝试直接访问发布页或订单管理页确认会被重定向到登录页。这五步做完覆盖了登录态、CRUD、关联查询、权限拦截四类核心逻辑就算面试官让你现场演示心里也有底。验证之外如果想把这个项目从“课程设计”拔高到“能写进简历的项目”可以在三个方向里选一个做扩展。方向一是引入Filter做统一的登录拦截替代在每个Servlet里手动判断session是否为空这样既能精简代码又能体现你对“横切关注点”的理解但要注意把登录页和静态资源排除在拦截范围外。方向二是给商品表加views浏览数字段每次访问详情页时执行一次UPDATE goods SET views views 1 WHERE id ?加一个排行榜列表从“排序”角度展示SQL能力。方向三是把图片上传从本机目录迁移到OSS这种云存储代码里只存URL答辩时能引出“分布式部署”的话题但实现成本会增加不少时间紧的话跳过。最后一个建议把项目里所有资源文件的路径都核对一遍包括上传图片保存的getRealPath路径和页面引用的相对路径。Tomcat的work目录如果残留了旧的编译文件修改JSP后不生效清理Tomcat/work/Catalina目录或重启Tomcat都能解决。当初我在做类似项目时因为没清work目录改了index.jsp却总显示旧页面一度以为是浏览器缓存问题折腾了整整一个下午后来养成每次启动前删一次work下项目目录的习惯这个“玄学”问题就彻底消失了。希望这些经验能帮你把毕设或课设的路走得更顺一点少踩几个我已经替你踩过的坑。本文还有配套的精品资源点击获取