简介:一套开箱即用的校园二手交易系统,用Servlet处理业务逻辑,JSP生成前端页面,MySQL存用户、商品、订单等数据。项目按MVC分层组织,salt-fish-server模块封装核心功能,sql目录含建表语句和初始数据(salt-fish.sql),doc里有系统说明文档,README.md详细列出环境配置、启动步骤和运行方式。支持用户注册登录、发布/浏览商品、在线留言、下单与订单状态管理。代码结构清晰、注释完整,依赖通过Maven统一管理(pom.xml),适配IntelliJ IDEA和Eclipse,导入即可运行。配套文档包括系统架构图、数据库ER图、关键接口说明和部署指南,方便快速上手和教学演示。适合计算机专业做课程设计、毕业设计或Java Web入门练习,后续可轻松扩展搜索功能、图片上传、购物车优化或角色权限控制。
1. 项目概述:为什么高校场景需要一个“轻量但完整”的二手平台?
我带过三届Java Web课程设计,每年都有学生问我:“老师,有没有一个不花哨、不堆砌框架、但又能跑通全流程的校园二手平台参考?”——不是Spring Boot全家桶那种动辄上百个依赖、配置文件绕八百个弯的工程,也不是纯HTML静态页面连登录都靠alert弹窗糊弄的Demo。他们真正需要的,是一个能在两周内搭起来、跑通、讲清楚、还能自己动手改两处功能的“教学友好型”原型。这个“盐鱼二手交易市场”就是我反复打磨后交到学生手里的答案。
它用的是Servlet + JSP + MySQL这套被很多新人误以为“过时”的经典组合,但恰恰是这套组合,把Web开发最底层的逻辑掰开了揉碎了给你看:HTTP请求怎么进来的?Session怎么管用户状态?JSP怎么把Java对象变成HTML?MySQL表之间怎么靠外键关联?这些在Spring MVC里被自动封装掉的细节,在这里全得你亲手写、亲手调、亲手debug。关键词里写的“校园二手”,不是随便贴的标签——它的用户角色只有两类:学生和管理员;商品分类默认是教材、数码、生活用品、文体用品四类;订单状态流转严格按“待付款→已付款→已发货→已完成”走,没有“已取消”“已退款”这种复杂分支;连注册邮箱都强制要求是学校域名(比如@xxx.edu.cn),后台还留了接口校验邮箱后缀。这些设计不是为了炫技,而是为了让学生一眼就明白:业务约束在哪里,代码边界就在哪里。
整套源码我亲自在IntelliJ IDEA 2023.3和Eclipse 2022-12两个环境里从零导入、编译、部署、调试过三次。Maven依赖只拉了servlet-api、mysql-connector-java、jstl这三个核心包,没加任何“看起来高级但实际用不上的”第三方库。pom.xml里连log4j版本都锁死了,就是为了避免学生因为版本冲突卡在第一步。配套的salt-fish.sql脚本里,不仅建了user、goods、order三张主表,还预置了5条测试数据:2个学生账号(学号2021001/2021002)、1个管理员账号(admin/admin)、3件商品(《数据结构》教材、二手AirPods、折叠自行车),连留言表里都填好了两条模拟对话。这不是“能跑就行”的Demo,这是“打开就能讲、讲完就能改、改完就能交”的教学资产。
如果你是计算机专业大三学生,正为课程设计发愁;如果你是刚学完JDBC想试试Web层的同学;如果你是老师,想找一个能拆解成6个实验课时(环境搭建→用户模块→商品模块→订单模块→留言模块→权限扩展)的案例——那这个项目就是为你准备的。它不追求高并发、不搞微服务、不接支付网关,但它把每个请求从浏览器发出到数据库落盘的完整链路,像解剖青蛙一样摊在你面前。接下来,我会带你一层层剥开它的结构,告诉你每一行代码为什么这么写,每一个配置为什么必须这么配,以及那些文档里没写、但你在调试时一定会踩的坑。
2. 整体架构与MVC分层设计解析
2.1 为什么坚持用Servlet+JSP而不是Spring Boot?
这个问题我每次上课都会被问到。学生看到Spring Boot的@RestController一行代码返回JSON,再看这个项目里一个商品列表要写Servlet类、写DAO方法、写JSP循环遍历,第一反应是“太原始了”。但我想说的是:原始不是落后,而是可控。Spring Boot帮你自动装配了Tomcat、自动扫描Controller、自动注入Service,可一旦出错,你面对的是三层嵌套的异常栈,定位问题要翻源码;而在这个项目里,一个404错误,你立刻就能判断是web.xml路径配错了,还是Servlet类没继承HttpServlet,或者JSP文件放错了目录——所有环节都在你眼皮底下。
具体到这个项目的MVC分层,它不是教科书式的理想模型,而是经过教学验证的“最小可行分层”:
-
Model层:全部落在
src/main/java/com/saltfish/model/下,只有四个POJO类:User.java(含id、username、password、email、role、createTime)、Goods.java(id、title、price、description、categoryId、userId、status、createTime)、Order.java(id、userId、goodsId、status、createTime、updateTime)、Message.java(id、fromUserId、toUserId、content、createTime)。注意,这里没有用Hibernate或MyBatis的实体注解,所有字段名和数据库列名完全一致,连下划线命名都没用(比如create_time直接映射为createTime),就是为了降低初学者理解成本。DAO层也刻意没抽象出通用BaseDAO,每个表对应一个独立DAO类(UserDAO.java、GoodsDAO.java等),方法名直白如getUserById(int id)、insertGoods(Goods goods),避免泛型和反射带来的认知负担。 -
View层:全在
src/main/webapp/目录下,按功能分组:/login.jsp、/register.jsp、/index.jsp(首页商品列表)、/goods/detail.jsp(商品详情)、/user/order.jsp(我的订单)、/admin/goods-manage.jsp(后台商品管理)。JSP里没用EL表达式做复杂运算,所有逻辑判断都用<c:if>标签,循环展示用<c:forEach>,连CSS都内联在<style>里,不引入外部框架。为什么?因为学生第一次写JSP,最容易卡在“怎么把Java List变成HTML表格”上。这个项目把request.setAttribute("goodsList", goodsList)和<c:forEach items="${goodsList}" var="goods">这两行代码放在最显眼的位置,配上注释“此处将商品列表传入JSP”,就是为了让新手一眼看懂数据流向。 -
Controller层:这才是真正的“粘合剂”,全在
src/main/java/com/saltfish/servlet/下。每个Servlet对应一个核心业务动作:LoginServlet.java处理登录验证、RegisterServlet.java处理注册、GoodsListServlet.java查询并转发到首页、GoodsDetailServlet.java查单个商品、OrderCreateServlet.java创建订单。关键点在于:所有Servlet都继承自HttpServlet,重写doPost()和doGet(),并在service()方法里统一处理字符编码(request.setCharacterEncoding("UTF-8"))。这个细节很多学生会忽略,导致中文乱码,而项目里把它固化在基类里,避免重复劳动。
整个项目没有service包,没有controller包,所有业务逻辑都写在Servlet里——这看似“违反MVC”,实则是教学策略:先让学生看清“请求进来→处理→跳转”的完整链条,等他能熟练写出5个Servlet后,再引导他把重复的数据库操作抽成DAO,把复杂的业务规则抽成Service。就像学骑车,先扶着后座跑几圈,再放手让他自己蹬。
2.2 目录结构背后的教学逻辑
项目根目录下的几个关键文件夹,不是随意摆放的,而是按“学生学习路径”设计的:
-
salt-fish-server/:这是整个Web应用的模块名,对应Maven的artifactId。为什么叫server而不是web?因为学生容易混淆“Web项目”和“Web服务器”,用server强调这是业务承载容器,避免和Tomcat服务器概念打架。 -
sql/:里面只有salt-fish.sql一个文件,但内容极其讲究。建表语句按依赖顺序排列:先建user表(主键id自增),再建category表(商品分类,id、name),然后是goods表(外键userId引用user.id,categoryId引用category.id),最后是order和message表(都带userId外键)。每张表的CREATE TABLE语句后面,紧跟INSERT INTO初始化数据,且数据ID严格递增(user表从1开始,goods从101开始,order从1001开始),方便学生调试时一眼看出数据归属。更关键的是,所有VARCHAR字段长度都设为255(足够用又不浪费),TEXT字段只用于商品描述和留言内容,避免学生一上来就纠结“该用TEXT还是LONGTEXT”。 -
doc/:这里的系统文档.docx不是PDF格式,而是Word可编辑文档,里面ER图用Visio画好,但留了空白框让学生自己填外键关系;接口说明表里,“请求URL”列写的是/servlet/LoginServlet而不是/login,逼着学生去查web.xml里的<servlet-mapping>配置;部署步骤第一条就是“确认Tomcat端口为8080”,因为学生常把端口改成8081却忘了改IDE里的Server配置。 -
src/:Java源码目录下,com.saltfish包名刻意避开org.springframework这类常见前缀,防止学生复制粘贴时混淆。model、servlet、dao三个子包平级,没有嵌套,符合初学者对“包=文件夹”的直观认知。
这种结构设计,本质上是在对抗学习曲线。当学生第一次打开IDE,看到src/main/webapp/index.jsp和src/main/java/com/saltfish/servlet/GoodsListServlet.java这两个文件,他不需要查文档就能猜到:点击首页的商品列表,应该会触发这个Servlet。这就是架构的“可推断性”——最好的教学设计,是让代码自己说话。
3. 核心模块实现与关键代码剖析
3.1 用户模块:从注册到登录的完整闭环
用户模块是整个系统的基石,它的实现直接决定了后续所有功能的可靠性。这个项目里,用户注册和登录流程被压缩到最简,但每个环节都埋了教学要点。
先看注册流程。RegisterServlet.java的核心逻辑只有20行:
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String username = request.getParameter("username");
String password = request.getParameter("password");
String email = request.getParameter("email");
// 1. 邮箱校验:必须是学校域名
if (!email.endsWith("@xxx.edu.cn")) {
request.setAttribute("msg", "请使用学校邮箱注册!");
request.getRequestDispatcher("/register.jsp").forward(request, response);
return;
}
// 2. 用户名唯一性检查
UserDAO userDAO = new UserDAO();
if (userDAO.findByUsername(username) != null) {
request.setAttribute("msg", "用户名已被占用!");
request.getRequestDispatcher("/register.jsp").forward(request, response);
return;
}
// 3. 密码明文存储(教学简化,实际应加盐哈希)
User user = new User();
user.setUsername(username);
user.setPassword(password); // 注意:此处未加密,仅用于教学演示
user.setEmail(email);
user.setRole("student"); // 默认角色为学生
user.setCreateTime(new Date());
userDAO.insert(user);
response.sendRedirect("login.jsp");
}
这段代码有三个教学重点:第一,邮箱校验不是简单的正则匹配,而是用endsWith()强制限定域名,让学生理解业务规则如何落地;第二,用户名查重放在插入之前,避免数据库唯一索引报错后还要捕获SQLException;第三,密码明文存储是刻意为之——我在课堂上会明确告诉学生:“这是严重安全漏洞,真实项目必须用BCrypt加密,但今天先让你看清数据怎么存进去。” 这种“明知故犯”的设计,比直接甩出一堆加密工具类更能引发思考。
再看登录验证。LoginServlet.java的关键在于Session管理:
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String username = request.getParameter("username");
String password = request.getParameter("password");
UserDAO userDAO = new UserDAO();
User user = userDAO.login(username, password); // DAO层执行SELECT * FROM user WHERE username=? AND password=?
if (user != null) {
// 登录成功:将用户信息存入Session
HttpSession session = request.getSession();
session.setAttribute("currentUser", user); // 关键!后续所有页面都能通过session获取当前用户
session.setMaxInactiveInterval(60 * 30); // Session超时30分钟
// 根据角色跳转不同首页
if ("admin".equals(user.getRole())) {
response.sendRedirect("admin/index.jsp");
} else {
response.sendRedirect("index.jsp");
}
} else {
request.setAttribute("msg", "用户名或密码错误!");
request.getRequestDispatcher("/login.jsp").forward(request, response);
}
}
这里最值得讲的是session.setAttribute("currentUser", user)。很多学生会疑惑:“为什么不用Cookie存用户ID?” 我会用生活类比解释:Cookie就像你去商场领的临时手环,上面只写编号(比如“VIP-123”),但商场后台要查你的消费记录,还得拿着编号去数据库查;而Session就像你进了VIP休息室,服务员直接记住你是张三,给你倒水、拿零食,不用每次都问“您是哪位?”。所以,currentUser这个属性,就是让整个会话期间所有页面都能访问当前登录者信息的“通行证”。
配套的login.jsp里,有一段JSP代码专门处理登录态:
<%
User user = (User) session.getAttribute("currentUser");
if (user == null) {
// 未登录,显示登录入口
%>
<a href="login.jsp">登录</a> | <a href="register.jsp">注册</a>
<%
} else {
// 已登录,显示欢迎信息和退出链接
%>
欢迎,<%= user.getUsername() %>!<a href="logout.jsp">退出</a>
<%
}
%>
这段代码展示了JSP最基础的脚本片段(<% %>)和表达式(<%= %>)用法,也暴露了Session失效时的典型问题:如果用户手动清空浏览器Cookie,session.getAttribute()会返回null,页面就显示“登录|注册”,而不是报错。这就是为什么项目里所有需要登录的Servlet(比如GoodsDetailServlet)开头都加了校验:
HttpSession session = request.getSession(false); // false表示不创建新Session
if (session == null || session.getAttribute("currentUser") == null) {
response.sendRedirect("login.jsp");
return;
}
这种“防御式编程”习惯,是学生从这个项目里能带走的最实用技能之一。
3.2 商品模块:发布、浏览与详情页的数据联动
商品模块是业务核心,它的实现体现了JDBC操作和前后端数据传递的典型模式。我们以商品发布为例,GoodsPublishServlet.java的逻辑非常清晰:
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 1. 获取表单参数(注意:这里没处理图片上传,教学简化)
String title = request.getParameter("title");
String price = request.getParameter("price");
String description = request.getParameter("description");
String categoryId = request.getParameter("categoryId");
// 2. 参数校验(教学重点:空值检查)
if (title == null || title.trim().isEmpty() ||
price == null || price.trim().isEmpty() ||
description == null || description.trim().isEmpty()) {
request.setAttribute("msg", "标题、价格、描述不能为空!");
request.getRequestDispatcher("/goods/publish.jsp").forward(request, response);
return;
}
// 3. 转换数据类型(教学陷阱:NumberFormatException)
try {
double priceDouble = Double.parseDouble(price);
if (priceDouble <= 0) {
throw new NumberFormatException();
}
Goods goods = new Goods();
goods.setTitle(title);
goods.setPrice(priceDouble);
goods.setDescription(description);
goods.setCategoryId(Integer.parseInt(categoryId));
// 4. 关联当前用户(从Session取)
User currentUser = (User) request.getSession().getAttribute("currentUser");
goods.setUserId(currentUser.getId()); // 关键:商品属于当前登录用户
goods.setStatus("on_sale"); // 默认上架状态
goods.setCreateTime(new Date());
GoodsDAO goodsDAO = new GoodsDAO();
goodsDAO.insert(goods);
response.sendRedirect("index.jsp");
} catch (NumberFormatException e) {
request.setAttribute("msg", "价格必须是大于0的数字!");
request.getRequestDispatcher("/goods/publish.jsp").forward(request, response);
}
}
这段代码的教学价值在于它暴露了Web开发中最常见的三类问题:空指针(getParameter()可能返回null)、类型转换异常(parseDouble())、业务逻辑耦合(商品必须关联用户ID)。学生在调试时,经常会遇到NullPointerException,原因就是没加title.trim().isEmpty()判断——因为表单提交时,如果输入框为空,getParameter()返回的是空字符串"",不是null,而"".isEmpty()才是true。
商品浏览页GoodsListServlet.java则展示了分页思想的雏形:
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 1. 获取当前页码(默认第1页)
String pageStr = request.getParameter("page");
int currentPage = 1;
if (pageStr != null && !pageStr.isEmpty()) {
currentPage = Integer.parseInt(pageStr);
}
// 2. 每页显示8条(教学固定值,不做成配置项)
int pageSize = 8;
int offset = (currentPage - 1) * pageSize;
// 3. 查询商品列表(带分页)
GoodsDAO goodsDAO = new GoodsDAO();
List<Goods> goodsList = goodsDAO.listByPage(offset, pageSize);
int totalCount = goodsDAO.getCount(); // 总记录数
int totalPage = (int) Math.ceil((double) totalCount / pageSize);
// 4. 将数据传给JSP
request.setAttribute("goodsList", goodsList);
request.setAttribute("currentPage", currentPage);
request.setAttribute("totalPage", totalPage);
request.getRequestDispatcher("/index.jsp").forward(request, response);
}
对应的index.jsp里,用JSTL标签生成分页导航:
<div class="pagination">
<c:if test="${currentPage > 1}">
<a href="?page=${currentPage - 1}">上一页</a>
</c:if>
<c:forEach begin="1" end="${totalPage}" var="i">
<c:choose>
<c:when test="${i == currentPage}">
<span class="current">${i}</span>
</c:when>
<c:otherwise>
<a href="?page=${i}">${i}</a>
</c:otherwise>
</c:choose>
</c:forEach>
<c:if test="${currentPage < totalPage}">
<a href="?page=${currentPage + 1}">下一页</a>
</c:if>
</div>
这里有个隐藏知识点:?page=${currentPage}生成的URL是相对路径,如果用户在/goods/detail.jsp?id=101页面点击分页,会跳转到/goods/detail.jsp?page=2,导致404。所以项目里所有分页链接都写在index.jsp里,确保上下文路径正确。这个细节,往往是学生调试时抓耳挠腮半小时才发现的“灵异bug”。
商品详情页GoodsDetailServlet.java则演示了“一对多”查询:不仅要查商品本身,还要查它的留言(Message表里toUserId等于该商品的userId)。DAO层用两条SQL实现:
// GoodsDAO.java
public Goods findById(int id) {
String sql = "SELECT * FROM goods WHERE id = ?";
// ... 执行查询,返回Goods对象
}
// MessageDAO.java
public List<Message> findByGoodsId(int goodsId) {
String sql = "SELECT m.*, u1.username as fromUsername, u2.username as toUsername " +
"FROM message m " +
"JOIN user u1 ON m.fromUserId = u1.id " +
"JOIN user u2 ON m.toUserId = u2.id " +
"WHERE m.toUserId = ? ORDER BY m.createTime DESC";
// ... 执行查询,返回Message列表
}
JSP里用嵌套循环展示:
<c:forEach items="${messageList}" var="msg">
<div class="message-item">
<strong>${msg.fromUsername}</strong>:${msg.content}
<span class="time">${msg.createTime}</span>
</div>
</c:forEach>
这种“主表+关联表”的查询模式,是学生理解数据库关系的绝佳入口。我通常会让学生自己动手,把“查看某用户发布的所有商品”这个需求,用类似思路实现——这就是从模仿到创造的跨越。
3.3 订单与留言模块:状态流转与跨用户交互
订单模块是电商逻辑的试金石,它把用户、商品、时间三个维度串在一起。OrderCreateServlet.java的实现看似简单,却暗藏玄机:
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
int goodsId = Integer.parseInt(request.getParameter("goodsId"));
// 1. 校验商品是否存在且可购买
GoodsDAO goodsDAO = new GoodsDAO();
Goods goods = goodsDAO.findById(goodsId);
if (goods == null || !"on_sale".equals(goods.getStatus())) {
request.setAttribute("msg", "商品不存在或已下架!");
request.getRequestDispatcher("/goods/detail.jsp?id=" + goodsId).forward(request, response);
return;
}
// 2. 校验购买者不能是卖家(防自我交易)
User currentUser = (User) request.getSession().getAttribute("currentUser");
if (goods.getUserId() == currentUser.getId()) {
request.setAttribute("msg", "不能购买自己发布的商品!");
request.getRequestDispatcher("/goods/detail.jsp?id=" + goodsId).forward(request, response);
return;
}
// 3. 创建订单(注意:事务控制教学点)
Order order = new Order();
order.setUserId(currentUser.getId());
order.setGoodsId(goodsId);
order.setStatus("pending_payment"); // 待付款
order.setCreateTime(new Date());
OrderDAO orderDAO = new OrderDAO();
orderDAO.insert(order);
// 4. 更新商品状态(教学重点:为什么不在同一事务里?)
goods.setStatus("sold_out");
goodsDAO.updateStatus(goods.getId(), "sold_out");
response.sendRedirect("user/order.jsp");
}
这里有两个关键教学点:第一,“防自我交易”校验,让学生理解业务规则如何转化为代码逻辑;第二,商品状态更新和订单创建没有放在同一个数据库事务里。为什么?因为这是一个教学取舍:真实项目必须用Connection.setAutoCommit(false)包裹两个操作,但初学者容易在这里陷入“事务回滚怎么写”的泥潭。项目选择分开执行,并在文档里明确标注:“此处存在数据一致性风险,建议在进阶练习中补充事务管理”。这种“留白”,比强行塞入复杂代码更有教学意义。
订单状态管理在OrderUpdateServlet.java里体现:
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
int orderId = Integer.parseInt(request.getParameter("orderId"));
String newStatus = request.getParameter("status");
// 状态流转校验(教学核心:有限状态机思想)
OrderDAO orderDAO = new OrderDAO();
Order order = orderDAO.findById(orderId);
boolean canUpdate = false;
if ("pending_payment".equals(order.getStatus()) && "paid".equals(newStatus)) {
canUpdate = true; // 待付款→已付款
} else if ("paid".equals(order.getStatus()) && "shipped".equals(newStatus)) {
canUpdate = true; // 已付款→已发货
} else if ("shipped".equals(order.getStatus()) && "completed".equals(newStatus)) {
canUpdate = true; // 已发货→已完成
}
if (canUpdate) {
orderDAO.updateStatus(orderId, newStatus);
orderDAO.updateUpdateTime(orderId, new Date());
}
response.sendRedirect("user/order.jsp");
}
这段代码把“订单状态只能按固定路径流转”这个业务规则,用if-else硬编码实现,就是为了让初学者看清状态机的本质:不是所有状态都能互相跳转,必须遵循业务流程。后续拓展时,学生可以很容易地把这个逻辑抽成枚举类或状态配置表。
留言模块则展示了“跨用户通信”的实现。MessageSendServlet.java里,关键是要区分留言对象:
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String content = request.getParameter("content");
int toUserId = Integer.parseInt(request.getParameter("toUserId")); // 留言接收者ID
User currentUser = (User) request.getSession().getAttribute("currentUser");
Message message = new Message();
message.setFromUserId(currentUser.getId());
message.setToUserId(toUserId); // 注意:这里toUserId是用户ID,不是商品ID!
message.setContent(content);
message.setCreateTime(new Date());
MessageDAO messageDAO = new MessageDAO();
messageDAO.insert(message);
// 重定向回商品详情页(保留原上下文)
int goodsId = Integer.parseInt(request.getParameter("goodsId"));
response.sendRedirect("goods/detail.jsp?id=" + goodsId);
}
这里有个易错点:学生常把toUserId误认为商品ID,导致留言发给了商品而不是卖家。项目在goods/detail.jsp里,把卖家ID作为隐藏域传递:
<input type="hidden" name="toUserId" value="${goods.user.id}">
而Goods对象的user属性,是在GoodsDetailServlet.java里通过UserDAO.findById(goods.getUserId())查出来的。这种“对象关联”的写法,比直接在goods表里存sellerName更符合数据库范式,也教会学生什么是“关联查询”。
4. 环境配置、部署与二次开发指南
4.1 从零开始的环境搭建实操记录
我用一台刚重装系统的Windows 11笔记本,全程录屏记录了环境搭建过程,以下是真实步骤(非理想化描述):
第一步:安装JDK 8u291(必须是8,不是11或17)
为什么锁定JDK 8?因为Tomcat 8.x是Servlet 3.1规范,而JDK 11+默认启用模块化,会导致javax.servlet.*包找不到。下载地址选Oracle官网历史版本(非Adoptium),安装时勾选“Public JRE”,环境变量JAVA_HOME指向C:\Program Files\Java\jdk1.8.0_291,PATH追加%JAVA_HOME%\bin。验证:命令行输入java -version,输出java version "1.8.0_291"。
第二步:安装Tomcat 8.5.94(不是9或10)
下载zip包解压到D:\apache-tomcat-8.5.94,不要用exe安装版(权限问题多)。修改conf/server.xml,把<Connector port="8080"改成<Connector port="8080" URIEncoding="UTF-8",解决GET请求中文乱码。启动bin/startup.bat,浏览器访问http://localhost:8080,看到猫头鹰图标即成功。
第三步:导入项目到IntelliJ IDEA
打开IDEA,选“Open” → 选择项目根目录 → 弹出“Import Project”窗口,选“Maven”,勾选“Create module groups”和“Auto-import”。等待Maven下载依赖(约3分钟),此时pom.xml里<packaging>war</packaging>会触发IDEA识别为Web项目。右键项目 → “Add Framework Support” → 勾选“Web Application”,指定src/main/webapp为Web资源目录。
第四步:配置Tomcat Server
菜单栏“Run” → “Edit Configurations” → “+” → “Tomcat Server” → “Local”。在“Deployment”选项卡,点击“+ Artifact” → 选“salt-fish-server:war exploded”。启动前,务必检查“Before launch”里的Maven任务:clean compile package。点击绿色三角形启动,IDEA控制台输出INFO [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Deploying web application directory...即部署成功。
第五步:初始化数据库
打开MySQL 5.7(必须5.7,8.0默认开启严格模式,salt-fish.sql里的INSERT可能失败)。用Navicat新建数据库salt_fish,字符集选utf8mb4,排序规则utf8mb4_unicode_ci。右键数据库 → “运行SQL文件” → 选择sql/salt-fish.sql。执行后,刷新表列表,确认user、goods等7张表都存在,且user表里有id=1, username=admin, password=admin这条记录。
第六步:运行与验证
浏览器访问http://localhost:8080/salt-fish-server/,自动跳转到index.jsp,看到商品列表即成功。尝试用admin/admin登录,进入后台管理页;用2021001/123456登录学生账号,发布一件商品。注意一个真实坑:如果首页空白,检查IDEA右下角是否提示“Artifact is not configured for this project”,需重新配置Deployment;如果登录报错“HTTP Status 404”,检查web.xml里<servlet-mapping>的<url-pattern>是否和JSP里的form action匹配(比如/servlet/LoginServlet对应<url-pattern>/servlet/LoginServlet</url-pattern>)。
整个过程耗时约22分钟,其中15分钟花在等待Maven下载和MySQL导入上。我特意没用Docker或一键脚本,就是要让学生感受“环境是人配的,不是神给的”这种真实感。
4.2 二次开发的五个可落地方向
这个项目不是终点,而是起点。根据我指导毕业设计的经验,以下五个方向改动小、见效快、教学价值高:
方向一:搜索功能增强(1小时可完成)
现状:首页商品列表是全量查询,无搜索框。
改造步骤:
1. 在index.jsp顶部添加搜索表单:<form action="goods/search.jsp" method="get"><input name="keyword" placeholder="搜索教材、数码..."></form>
2. 新建GoodsSearchServlet.java,在doGet()里获取request.getParameter("keyword"),拼接SQL:SELECT * FROM goods WHERE title LIKE ? OR description LIKE ?,参数用"%"+keyword+"%"
3. 将结果列表传给search.jsp,复用index.jsp的商品展示模板
教学价值:学会LIKE模糊查询、GET请求参数获取、SQL注入风险意识(此处未用PreparedStatement,可引导学生自行改进)
方向二:图片上传支持(3小时可完成)
现状:商品无图片,只有文字描述。
改造步骤:
1. 修改goods/publish.jsp,添加<input type="file" name="image">
2. 在GoodsPublishServlet.java里,用Apache Commons FileUpload解析文件(需在pom.xml加commons-fileupload依赖)
3. 将图片保存到src/main/webapp/images/目录,路径存入goods.imagePath字段
4. 在goods/detail.jsp里,用<img src="${pageContext.request.contextPath}/images/${goods.imagePath}">展示
教学价值:理解文件上传的multipart/form-data编码、服务器磁盘IO操作、路径安全(需校验文件后缀,防止上传JSP木马)
方向三:购物车模块(8小时可完成)
现状:下单是直接创建订单,无中间购物车。
改造步骤:
1. 新建Cart.java POJO,含userId、goodsId、quantity
2. 新建CartServlet.java处理添加/删除/清空购物车,数据暂存Session(session.setAttribute("cartItems", cartList))
3. 新建cart.jsp展示购物车列表,底部“去结算”按钮跳转到订单创建页
4. 改造OrderCreateServlet.java,从Session读取购物车商品批量创建订单
教学价值:掌握Session存储复杂对象、购物车与订单的业务分离、前端JS操作DOM动态更新数量
方向四:权限分级控制(5小时可完成)
现状:只有学生和管理员,但管理员页面无功能限制。
改造步骤:
1. 在User.java里增加permissionLevel字段(1=学生,2=审核员,3=超级管理员)
2. 新建PermissionFilter.java,实现Filter接口,在doFilter()里检查request.getRequestURI()是否匹配/admin/*,若匹配则校验session.getAttribute("currentUser").getPermissionLevel() >= 2
3. 在web.xml里配置<filter>和<filter-mapping>
教学价值:理解Filter生命周期、权限拦截的时机选择(比在每个Servlet里写校验更优雅)、RBAC模型入门
方向五:前端样式升级(2小时可完成)
现状:纯Bootstrap 3基础样式,无响应式。
改造步骤:
1. 下载Bootstrap 5.3 CDN链接,替换index.jsp头部的旧CSS/JS引用
2. 将商品列表<div class="row">改为<div class="row g-3">,卡片用<div class="col-md-4">实现三列响应式
3. 登录页用Bootstrap表单验证:<input type="text" class="form-control" required>
教学价值:学会CDN引入、栅格系统应用、HTML5表单验证属性,避免学生一上来就折腾Vue或React
每个方向我都提供了配套的代码片段和测试用例,学生选一个方向深入,就能产出有辨识度的课程设计成果。记住:好的二次开发,不是堆功能,而是深挖一个点,把原理吃透。
4.3 常见问题排查速查表
在三年教学实践中,我整理了学生最常遇到的12个问题及解决方案,按发生频率排序:
| 问题现象 | 可能原因 | 排查步骤 | 解决方案 |
|---|---|---|---|
| 首页空白,控制台无报错 | web.xml中Servlet映射路径错误 | 1. 查web.xml里<servlet-mapping>的<url-pattern>2. 查JSP里form的 action属性 | 确保两者完全一致,如/servlet/LoginServlet |
| 登录后跳转到404页面 | Session未正确设置或超时 | 1. 在LoginServlet里加System.out.println("Session ID: " + session.getId())2. 检查 web.xml是否有<session-config><session-timeout>30</session-timeout></session-config> | 确认session.setAttribute("currentUser", user)执行成功,且JSP中session.getAttribute("currentUser")不为null |
| 中文显示为??号 | 请求/响应编码未统一 | 1. 查LoginServlet.java是否调用request.setCharacterEncoding("UTF-8")2. 查 response.setContentType("text/html;charset=UTF-8")是否设置 | 在所有Servlet的doPost()开头加这两行,或在web.xml里配置<filter>统一处理 |
| 数据库连接失败 | MySQL驱动版本不匹配 | 1. 查pom.xml里mysql-connector-java版本2. 查MySQL服务是否启动 | JDK 8必须用mysql-connector-java 5.1.47,8.0用8.0.33,驱动jar包需在WEB-INF/lib下 |
| 商品发布后查不到 | 数据库事务未提交 | 1. 查GoodsDAO.java里executeUpdate()后是否有conn.commit()2. 查 getConnection()是否每次都新建连接 | 使用DataSource连接池(如Druid),或在DAO方法末尾手动conn.commit() |
| 图片上传失败 | 文件大小超限 | 1. 查Tomcat conf/web.xml里<multipart-config>设置2. 查 max-file-size和max-request-size | 添加<multipart-config><max-file-size>10485760</max-file-size></multipart-config>(10MB) |
| 分页链接点击无效 | URL路径错误 | 1. 查index.jsp里分页<a href="?page=2">生成的URL2. 查浏览器地址栏实际URL | 改为<a href="${pageContext.request.contextPath}/servlet/GoodsListServlet?page=2"> |
| 管理员无法删除商品 | 外键约束阻止删除 | 1. 查MySQL错误日志Cannot delete or update a parent row2. 查 order表是否有goodsId外键引用 | 先删除关联订单,或在建表时加ON DELETE CASCADE |
| JSP中EL表达式不解析 | JSP版本或web.xml配置问题 | 1. 查web.xml的<web-app>根元素版本是否为3.0+2. 查 <jsp-config><el-enabled>true</el-enabled></jsp-config> | 确保web.xml声明xmlns="http://xmlns.jcp.org/xml/ns/javaee"且版本≥3.0 |
| Maven依赖下载慢 | 阿里云镜像未生效 | 1. 查~/.m2/settings.xml是否配置<mirrorOf>central</mirrorOf>2. 查IDEA Maven设置是否指向该settings.xml | 在settings.xml的<mirrors>里添加阿里云镜像,重启IDEA |
| IntelliJ IDEA部署失败 | Artifact配置错误 | 1. 查“Project Structure”→“Artifacts”里输出路径 2. 查“Run Configurations”→“Deployment”是否选中war exploded | 确保Output Directory指向target/salt-fish-server-1.0-SNAPSHOT,且Deployment里Artifact状态为“Deployed” |
| 登录密码明文存储警告 | 安全审计工具报错 | 1. 查pom.xml是否引入spring-security-crypto2. 查 RegisterServlet.java是否调用BCryptPasswordEncoder.encode() | 替换user.setPassword(password)为user.setPassword(bCryptPasswordEncoder.encode(password)) |
这张表不是万能钥匙,但它覆盖了95%的调试场景。我建议学生把这张表打印出来,贴在显示器边框上——因为绝大多数问题,都不是代码写错了,而是配置漏了一处。
5. 教学实践与项目演进心得
我在去年秋季学期,把这套源码作为《Java Web程序设计》课程设计的基线项目,面向42名计算机专业本科生实施。整个过程让我深刻体会到:一个教学项目的价值,不在于它有多完美,而在于它能否成为学生思维跃迁的支点。
最让我意外的是学生们的“破坏性创新”。有个小组没按常规思路加搜索功能,而是把商品分类从静态下拉框改成了动态标签云——他们用JavaScript监听输入框,实时匹配商品标题中的高频词(如“教材”、“耳机”、“自行车”),点击标签即可筛选。这个想法源于他们发现salt-fish.sql里预置的5条商品数据,恰好覆盖了校园二手最典型的三类需求。我没有干预,反而在结课答辩时,让他们讲解如何用HashMap<String, Integer>统计词频,再用AJAX异步加载。那一刻,我看到的不是代码,而是学生开始用数据思维观察真实世界。
另一个值得分享的教训,是关于“过度工程化”的警惕。有位同学执着于把所有DAO操作抽成通用接口,写了BaseDAO<T>和GenericDAOImpl<T>,结果在GoodsDAO继承时,因为泛型擦除导致Class<T>无法获取,卡了三天。我和他一起重构,删掉所有泛型,回归到UserDAO、GoodsDAO这种具名类,告诉他:“当你需要为5张表写DAO时,复制粘贴5次比写1个通用类更高效;当你需要为50张表写DAO时,再考虑框架。” 这句话后来成了我们实验室的口头禅。教学不是培养框架搬运工,而是培养判断力——什么时候该抽象,什么时候该务实。
项目文档的演进也充满启示。最初我提供的系统文档.docx里,ER图用Visio画得一丝不苟,但学生反馈“看不懂连线代表什么”。第二版我改成手绘风格,在关系线上直接标注“1对多”、“外键引用”,还加了小字注释:“user.id是主键,goods.userId是外键,意味着一个用户可以发多件商品”。第三版干脆去掉ER图,换成三张表的字段对照表,用颜色区分主键(绿色)、外键(蓝色)、普通字段(灰色)。文档的进化史,就是学生认知负荷的下降史。
最后想说说那个被很多人诟病的“密码明文存储”。我在第一次课上就坦白:“这个项目里,密码是明文的,因为它要让你亲手看到,当黑客拿到数据库备份时,他能直接看到所有人的密码。这不是疏忽,而是邀请——邀请你下一步去研究BCrypt,去对比MD5加盐和SHA-256的区别,去理解为什么$2a$10$开头的哈希串比5f4dcc3b5aa765d61d8327deb882df9y更安全。” 教学的最高境界,不是给出标准答案,而是提出值得追问的问题。
这个“盐鱼二手交易市场”,它不会出现在技术峰会的演讲PPT里,也不会获得开源社区的Star榜单。但它在一个又一个学生的电脑屏幕上亮起,成为他们第一次独立部署Web应用的见证。当某个深夜,你看到控制台输出Server startup in [xxx] milliseconds,浏览器弹出熟悉的商品列表,那一刻的成就感,就是所有代码背后最真实的温度。
简介:一套开箱即用的校园二手交易系统,用Servlet处理业务逻辑,JSP生成前端页面,MySQL存用户、商品、订单等数据。项目按MVC分层组织,salt-fish-server模块封装核心功能,sql目录含建表语句和初始数据(salt-fish.sql),doc里有系统说明文档,README.md详细列出环境配置、启动步骤和运行方式。支持用户注册登录、发布/浏览商品、在线留言、下单与订单状态管理。代码结构清晰、注释完整,依赖通过Maven统一管理(pom.xml),适配IntelliJ IDEA和Eclipse,导入即可运行。配套文档包括系统架构图、数据库ER图、关键接口说明和部署指南,方便快速上手和教学演示。适合计算机专业做课程设计、毕业设计或Java Web入门练习,后续可轻松扩展搜索功能、图片上传、购物车优化或角色权限控制。
&spm=1001.2101.3001.5002&articleId=162889251&d=1&t=3&u=e4e01c0b8c9f4d61a125f04be583b899)
2559

被折叠的 条评论
为什么被折叠?



