news 2026/10/4 4:22:42

JSP+Servlet外卖系统:四角色协同与订单状态机实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JSP+Servlet外卖系统:四角色协同与订单状态机实战

简介:这是一套基于Java Web技术栈开发的完整外卖订餐系统实战项目,面向Java初学者与Web开发入门者,帮助掌握JSP、Servlet、MySQL及MVC分层架构在真实业务场景中的落地应用。资源包为ZIP格式,大小93.63MB,包含源代码、数据库脚本(online.sql)、角色权限说明、系统截图及配套视频教程(.wmv),覆盖会员下单、商家接单、骑手配送、管理员监管等全链路功能模块。已有660人学习下载,项目结构清晰,Eclipse可直接导入运行,数据库预置初始数据,降低环境搭建门槛;同时提供多角色登录界面与业务逻辑实现细节,便于理解前后端协作机制与典型电商类系统设计范式,是巩固Servlet生命周期、JSP页面交互、MySQL表关系建模等核心技能的优质实践案例。

1. 这不是“又一个JSP练手项目”:它真能跑通四角色协同的外卖闭环,从会员下单、商家接单、骑手抢单到管理员调度,全链路可调试、可断点、可改数据库字段

你可能已经下载过十几个标着“JSP外卖系统”的压缩包——解压后发现只有3个JSP页面+1个Servlet+空荡荡的MySQL建表语句,登录页能跳转,但点“下单”就404,查日志全是NullPointerException,连骑手怎么抢单都找不到入口。而这个基于JSP+Servlet开发外卖订餐(点餐)系统(会员 骑手 商家 管理员).zip,是少有的、真实跑通四角色状态机流转的完整工程:会员下单后,订单自动进入“待接单”状态;商家后台刷新即见新订单,点击“确认接单”后状态变“制作中”;骑手登录后首页显示“可抢单列表”,点击即锁定并更新为“配送中”;管理员能实时看到各角色在线数、订单完成率、超时订单告警。它不靠前端JS模拟状态,所有流转由Servlet调用DAO层执行SQL事务控制,MySQL里order_status字段真实参与业务逻辑判断。适合Java Web新手建立“请求→Servlet→Service→DAO→DB→响应”的肌肉记忆,也适合老手快速复用其角色权限控制模块(RBAC实现未用框架,纯JDBC+session+URL拦截)。注意:它不是Spring Boot项目,没有自动装配,所有依赖、连接池、路径映射都得你亲手配——但正因如此,每个web.xml里的<servlet-mapping>、每个context.xml里的Resource定义、每个DAO里手动写的PreparedStatement参数绑定,你都能一眼看懂、一改就生效。

2. 拆包即用:从Eclipse导入到Tomcat启动,绕过90%新手卡点的实操路径

2.1 解压后目录结构与关键文件定位:别急着Run As → Run on Server

解压基于JSP+Servlet开发外卖订餐(点餐)系统(会员 骑手 商家 管理员).zip后,你会看到如下核心目录(非全部,仅列必须关注项):

/online/ ← 项目根目录(Eclipse导入时选这个) ├── WebContent/ ← JSP、CSS、JS、图片存放处(相当于标准WebApp的WebRoot) │ ├── index.jsp ← 会员首页,含搜索框和菜品瀑布流 │ ├── login.jsp ← 四角色统一登录入口(通过role参数区分) │ ├── merchant/ ← 商家专属目录(含addDish.jsp, orderList.jsp等) │ ├── rider/ ← 骑手专属目录(riderHome.jsp, takeOrder.jsp) │ └── admin/ ← 管理员目录(userManage.jsp, orderMonitor.jsp) ├── src/ ← Java源码(包结构:com.online.dao, com.online.service, com.online.servlet) │ ├── com.online.dao/ │ │ ├── BaseDao.java ← 封装JDBC连接获取、关闭(关键!用的是DBCP连接池) │ │ ├── OrderDao.java ← 订单状态变更核心DAO(updateStatus()方法含事务) │ │ └── UserDao.java ← 角色查询(根据username查role_id) │ ├── com.online.servlet/ │ │ ├── LoginServlet.java ← 处理login.jsp提交,校验后存role到session │ │ ├── OrderServlet.java ← 处理会员下单、商家确认、骑手抢单(同一Servlet多action) │ │ └── AdminServlet.java ← 管理员操作(禁用用户、强制完成订单等) │ └── com.online.service/ ← Service层(含OrderService.java,调用多个DAO保证事务) ├── WEB-INF/ │ ├── web.xml ← Servlet注册、过滤器、欢迎文件列表(重点看<servlet-mapping>) │ └── context.xml ← Tomcat数据源配置(JNDI名:java:comp/env/jdbc/online) ├── online.sql ← MySQL建库建表+初始数据(含4类用户账号) └── 帐号.txt ← 明文记录默认账号密码(会员:user/123,商家:shop/123...)

提示:帐号.txt不是摆设!它对应online.sql里user表的username和password(明文存储,无加密,调试阶段可直接用)。online.sql必须先导入,否则LoginServlet查不到用户直接报500。

2.2 Eclipse导入三步法:拒绝“Project Facets不匹配”报错

  1. 关闭自动构建:Eclipse菜单 → Project → Uncheck “Build Automatically”(避免导入时编译失败中断)
  2. 导入为Dynamic Web Project:
    • File → Import → Web → Existing Dynamic Web Project
    • Root directory 选/online(不是zip包,是解压后的文件夹)
    • 关键设置:
      • Target runtime:选已配置好的Apache Tomcat v8.5(或v9.0,勿选v10+,JSP EL表达式兼容性问题)
      • Configuration:选“Generate web.xml deployment descriptor”(确保生成web.xml,本项目依赖它)
  3. 修复Java Build Path:
    • 右键项目 → Properties → Java Build Path → Libraries → Add Library → Server Runtime → 选同上的Tomcat
    • 再Add External JARs:添加/online/WebContent/WEB-INF/lib/下的commons-dbcp-1.4.jar、mysql-connector-java-5.1.47.jar(版本必须匹配,高版本MySQL驱动在JSP EL中会报javax.el.PropertyNotFoundException)

2.3 Tomcat配置与数据库连接:context.xml和web.xml的生死联动

本项目用JNDI方式获取数据库连接,不是在DAO里硬编码DriverManager.getConnection()。这意味着两处配置必须严丝合缝:

第一步:确认Tomcatconf/context.xml已注入数据源
打开Tomcat安装目录 →conf/context.xml,在<Context>标签内添加:

<Resource name="jdbc/online" auth="Container" type="javax.sql.DataSource" maxTotal="20" maxIdle="10" minIdle="5" initialSize="5" maxWaitMillis="10000" username="root" password="your_mysql_password" <!-- 替换为你本地MySQL密码 --> driverClassName="com.mysql.jdbc.Driver" url="jdbc:mysql://localhost:3306/online?useUnicode=true&amp;characterEncoding=UTF-8&amp;serverTimezone=GMT%2B8"/>

注意:url中的&amp;是XML转义,不可写成&;serverTimezone=GMT%2B8解决时区报错;useUnicode=true&amp;characterEncoding=UTF-8防止中文乱码。

第二步:验证web.xml中资源引用正确
检查/online/WEB-INF/web.xml,确认存在:

<resource-ref> <description>DB Connection</description> <res-ref-name>jdbc/online</res-ref-name> <res-type>javax.sql.DataSource</res-type> <res-auth>Container</res-auth> </resource-ref>

此段声明告诉Tomcat:“我在代码里要用名为jdbc/online的数据源,请从context.xml里找”。

第三步:DAO层获取连接的代码必须匹配
打开BaseDao.java,核心方法:

public Connection getConnection() throws SQLException { Context ctx = null; DataSource ds = null; try { ctx = new InitialContext(); // JNDI上下文 ds = (DataSource) ctx.lookup("java:comp/env/jdbc/online"); // 注意前缀java:comp/env/ return ds.getConnection(); } catch (NamingException e) { throw new SQLException("JNDI lookup failed", e); } }

关键点:ctx.lookup("java:comp/env/jdbc/online")中的java:comp/env/是JNDI标准前缀,缺一字符(如漏掉/env/)就会抛NameNotFoundException,且错误日志只显示“Cannot create JDBC driver”,极易误判为驱动问题。

2.4 启动前必做:MySQL建库、导入SQL、验证账号

  1. 创建数据库(UTF8mb4编码,兼容emoji):
    CREATE DATABASE online CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
  2. 导入online.sql:
    • 在MySQL命令行或Navicat中,选中online库 → 执行online.sql全文
    • 验证数据:SELECT username, role_id FROM user;应返回至少4条记录(会员、商家、骑手、管理员各1)
  3. 检查帐号.txt与数据库一致性:
    • 文件中写的admin/admin123,必须在user表里存在且role_id=1(管理员)
    • 若修改过密码,需同步更新user表的password字段(本项目无密码加密,存明文)

启动Tomcat后,在浏览器访问http://localhost:8080/online/login.jsp,输入帐号.txt任一账号,应跳转至对应角色首页。若卡在登录页无反应,90%是JNDI配置或MySQL连接问题;若跳转后页面空白,检查JSP中EL表达式是否被禁用(见3.2节)。

3. 四角色权限落地:URL拦截、Session角色校验、状态机驱动的订单流转

3.1LoginServlet如何识别角色并注入Session:不止是request.getParameter("role")

login.jsp表单提交到LoginServlet,其doPost()方法核心逻辑:

String username = request.getParameter("username"); String password = request.getParameter("password"); User user = userService.login(username, password); // 调用Service查库 if (user != null) { // 关键:不仅存user对象,还存role_id和role_name到session request.getSession().setAttribute("user", user); request.getSession().setAttribute("role_id", user.getRoleId()); request.getSession().setAttribute("role_name", getRoleName(user.getRoleId())); // 1→管理员, 2→商家... // 根据role_id重定向到不同首页(非前端JS跳转!) String redirectPath = ""; switch (user.getRoleId()) { case 1: redirectPath = "/admin/adminHome.jsp"; break; case 2: redirectPath = "/merchant/merchantHome.jsp"; break; case 3: redirectPath = "/rider/riderHome.jsp"; break; case 4: redirectPath = "/index.jsp"; break; // 会员首页 default: redirectPath = "/login.jsp?error=role"; } response.sendRedirect(request.getContextPath() + redirectPath); } else { request.setAttribute("error", "用户名或密码错误"); request.getRequestDispatcher("/login.jsp").forward(request, response); }

逻辑说明:角色判断在服务端完成,role_id存入Session后,后续所有Servlet都可通过session.getAttribute("role_id")获取,不依赖URL参数或隐藏域,杜绝前端篡改。getRoleName()是硬编码映射(非数据库查),确保性能。

3.2 URL过滤器实现角色隔离:AdminFilter和MerchantFilter的双重校验

项目在web.xml中配置了两个过滤器:

<filter> <filter-name>AdminFilter</filter-name> <filter-class>com.online.filter.AdminFilter</filter-class> </filter> <filter-mapping> <filter-name>AdminFilter</filter-name> <url-pattern>/admin/*</url-pattern> <!-- 拦截所有/admin/路径 --> </filter-mapping> <filter> <filter-name>MerchantFilter</filter-name> <filter-class>com.online.filter.MerchantFilter</filter-class> </filter> <filter-mapping> <filter-name>MerchantFilter</filter-name> <url-pattern>/merchant/*</url-pattern> </filter-mapping>

以AdminFilter.java为例:

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); Integer roleId = (Integer) session.getAttribute("role_id"); // 必须同时满足:有session、role_id存在、且为管理员(1) if (session == null || roleId == null || roleId != 1) { request.setAttribute("error", "请先以管理员身份登录"); request.getRequestDispatcher("/login.jsp").forward(request, response); return; } chain.doFilter(req, resp); // 放行 }

参数说明:session.getAttribute("role_id")返回Integer而非String,因数据库role_id是INT类型;roleId != 1严格等于,不接受字符串"1";session == null判断防止NPE。此设计比单纯检查session.getAttribute("user") != null更安全——用户可能登录后手动修改session中user对象,但无法伪造role_id整型值。

3.3 订单状态机:OrderServlet如何用单一URL处理四角色操作

所有订单操作(会员下单、商家接单、骑手抢单、管理员强制完成)均提交到同一URL:/OrderServlet?action=xxx。OrderServlet.doPost()通过action参数分发:

String action = request.getParameter("action"); switch (action) { case "create": createOrder(request, response); break; // 会员调用 case "confirm": confirmOrder(request, response); break; // 商家调用 case "take": takeOrder(request, response); break; // 骑手调用 case "finish": finishOrder(request, response); break; // 管理员调用 default: response.sendError(HttpServletResponse.SC_BAD_REQUEST); }

以takeOrder()为例,体现状态校验:

private void takeOrder(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Integer riderId = (Integer) request.getSession().getAttribute("user_id"); Integer orderId = Integer.parseInt(request.getParameter("orderId")); // 关键状态校验:只允许抢单“待接单”状态的订单 Order order = orderService.findById(orderId); if (!"待接单".equals(order.getStatus())) { request.setAttribute("error", "该订单状态不可抢单(当前:" + order.getStatus() + ")"); request.getRequestDispatcher("/rider/riderHome.jsp").forward(request, response); return; } // 事务:更新订单状态 + 绑定骑手ID boolean success = orderService.takeOrder(orderId, riderId); if (success) { response.sendRedirect(request.getContextPath() + "/rider/riderHome.jsp?msg=抢单成功"); } else { request.setAttribute("error", "抢单失败,请重试"); request.getRequestDispatcher("/rider/riderHome.jsp").forward(request, response); } }

状态机价值:order.getStatus()从数据库读取,orderService.takeOrder()内部执行SQLUPDATE order SET status='配送中', rider_id=? WHERE id=? AND status='待接单',WHERE条件包含status,确保并发抢单时不会覆盖他人操作(乐观锁思想)。这是比单纯UPDATE ... WHERE id=?更健壮的设计。

4. 避坑指南:那些让新手调试3小时却只差一行配置的致命细节

4.1 现象:登录成功后跳转到空白页,查看源码发现JSP中${user.username}不解析,显示原样文字

原因:Tomcat 8.5+默认禁用EL表达式(Expression Language),因安全考虑。本项目JSP大量使用${}取Session值(如<h2>欢迎 ${user.username}!</h2>),若EL被禁,页面将显示${user.username}而非真实用户名。
解决:在/online/WebContent/WEB-INF/web.xml的<web-app>根标签内,顶部添加:

<jsp-config> <jsp-property-group> <url-pattern>*.jsp</url-pattern> <el-ignored>false</el-ignored> <!-- 关键!设为false --> </jsp-property-group> </jsp-config>

血泪经验:此配置必须放在<web-app>最开头,若插在<servlet>之后,Tomcat会忽略。且el-ignored默认值为true,不显式声明即失效。

4.2 现象:点击“下单”按钮报HTTP 404,浏览器地址栏显示http://localhost:8080/online/OrderServlet

原因:web.xml中OrderServlet的<url-pattern>配置错误。常见错误是写成/OrderServlet(绝对路径),但实际请求URL是/online/OrderServlet(项目上下文路径/online+ Servlet路径)。
解决:检查web.xml中OrderServlet的映射:

<servlet-mapping> <servlet-name>OrderServlet</servlet-name> <url-pattern>/OrderServlet</url-pattern> <!-- 正确!相对路径,Tomcat自动补上下文 --> </servlet-mapping>

玄学提醒:若仍404,右键项目 → Properties → Web Project Settings → Context root 必须为online(与压缩包名一致),否则Tomcat部署路径错位。

4.3 现象:MySQL插入中文显示为??,订单状态“制作中”存库后变“??`

原因:三处编码未统一:MySQL服务器编码、数据库编码、JDBC连接URL编码、JSP页面编码。
解决:四步强制统一

  1. MySQL服务器:my.cnf中[mysqld]下加character-set-server=utf8mb4
  2. 数据库:CREATE DATABASE online CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
  3. JDBC URL:url="jdbc:mysql://localhost:3306/online?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8"(注意&在XML中要写&amp;)
  4. JSP页面:每页顶部加<%@ page contentType="text/html;charset=UTF-8" %>

4.4 现象:骑手抢单后,订单状态变为“配送中”,但商家后台订单列表仍显示“待接单”

原因:浏览器缓存了商家订单列表页面(/merchant/orderList.jsp),AJAX未触发,或页面未重载。本项目未用AJAX,完全依赖页面刷新。
解决:在/merchant/orderList.jsp的<head>中强制禁止缓存:

<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> <meta http-equiv="Pragma" content="no-cache" /> <meta http-equiv="Expires" content="0" />

后悔药:开发时按Ctrl+F5强制刷新,而非普通F5。

4.5 现象:online.sql导入后,user表有数据,但LoginServlet查不到,日志报SQLException: Column 'role_id' not found in ResultSet

原因:MySQL驱动版本与JDBC代码不匹配。online.sql中user表字段为role_id(INT),但旧版驱动(如mysql-connector-java-5.1.13)在ResultSet.getInt("role_id")时可能因元数据问题找不到列。
解决:

  • 确认/online/WebContent/WEB-INF/lib/下驱动jar为mysql-connector-java-5.1.47.jar(项目自带)
  • 删除其他版本驱动(如mysql-connector-java-8.0.x.jar),8.0+驱动要求driverClassName="com.mysql.cj.jdbc.Driver"且URL加allowPublicKeyRetrieval=true,本项目未适配。

5. 进阶技巧:把“会员下单”流程拆解为可验证的原子操作,建立你的调试肌肉记忆

5.1 用Postman模拟会员下单:绕过前端,直击Servlet逻辑

会员下单本质是向/OrderServlet?action=create发送POST请求,携带菜品ID和数量。用Postman构造请求验证:

KeyValue说明
actioncreate必填,指定操作类型
dishId1菜品ID,从dish表查(如SELECT id FROM dish LIMIT 1)
quantity2数量,整数
address北京市朝阳区XX大厦1栋会员收货地址

发送后验证:

  • 响应应为302重定向到/index.jsp?msg=下单成功
  • 查MySQL:SELECT * FROM order_info WHERE user_id=4 ORDER BY create_time DESC LIMIT 1;(会员user_id=4)
  • 关键字段:status='待接单'、dish_id=1、quantity=2、address正确

为什么这么做:前端JSP可能因JavaScript错误阻断提交,而Postman直连Servlet,能100%确认后端逻辑是否畅通。这是排查“前端说点了没反应,后端说没收到请求”的黄金手段。

5.2 在OrderService.createOrder()中埋设日志,追踪事务边界

createOrder()方法是订单创建的核心,含事务控制。在src/com/online/service/OrderService.java中,在关键步骤添加System.out.println()(生产环境应换为log4j):

public boolean createOrder(Order order) { System.out.println("[DEBUG] 开始创建订单,用户ID:" + order.getUserId()); Connection conn = null; PreparedStatement ps = null; try { conn = BaseDao.getConnection(); conn.setAutoCommit(false); // 开启事务 // 插入订单主表 String sql1 = "INSERT INTO order_info (user_id, dish_id, quantity, address, status) VALUES (?, ?, ?, ?, ?)"; ps = conn.prepareStatement(sql1, Statement.RETURN_GENERATED_KEYS); ps.setInt(1, order.getUserId()); ps.setInt(2, order.getDishId()); ps.setInt(3, order.getQuantity()); ps.setString(4, order.getAddress()); ps.setString(5, "待接单"); ps.executeUpdate(); // 获取生成的order_id ResultSet rs = ps.getGeneratedKeys(); if (rs.next()) { order.setId(rs.getInt(1)); System.out.println("[DEBUG] 订单主表插入成功,生成order_id:" + order.getId()); } // 插入订单详情(本项目简化,无详情表,此步省略) conn.commit(); System.out.println("[DEBUG] 事务提交成功"); return true; } catch (SQLException e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) {} } System.out.println("[ERROR] 订单创建失败,回滚事务:" + e.getMessage()); return false; } finally { BaseDao.close(ps, conn); } }

验证价值:启动Tomcat后,控制台输出清晰显示事务流程。若看到开始创建订单但没看到事务提交成功,说明卡在某步SQL执行;若看到回滚事务,则根据e.getMessage()定位具体SQL错误(如字段长度超限、外键约束失败)。

5.3 修改订单状态的SQL必须带AND status条件:一份可复用的状态机SQL模板

OrderDao.java中更新状态的方法,如updateStatus(),其SQL应始终包含状态校验:

// 安全写法:只更新指定状态的记录,防并发覆盖 String sql = "UPDATE order_info SET status=?, rider_id=? WHERE id=? AND status=?"; ps.setString(1, newStatus); // 如"配送中" ps.setInt(2, riderId); ps.setInt(3, orderId); ps.setString(4, oldStatus); // 如"待接单"

对比危险写法(项目中不存在,但新手易犯):

// ❌ 危险!无状态校验,任何角色都能强行改状态 String sql = "UPDATE order_info SET status=? WHERE id=?";

我的习惯:从那以后我每次写状态更新SQL,都强制走一遍“WHERE status=?”条件,并在DAO方法注释中写明允许的状态流转(如@param oldStatus 允许从'待接单'状态更新)。这比在Service层if判断更底层、更可靠——数据库才是最终仲裁者。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/4 4:22:05

MATLAB高频问题与算法实战:从安装License到图像处理

1. 安装、激活与License报错&#xff1a;从根源上解决“装不上、打不开”用MATLAB这些年&#xff0c;我见过最多的求助帖基本都集中在同一个阶段——软件刚下载完&#xff0c;还没来得及体验矩阵运算的爽快&#xff0c;就被安装和激活流程按在地上摩擦。尤其是这几年新版本迭代…

作者头像 李华
网站建设 2026/10/4 4:18:57

ACPI设备子树恢复:解析_CTXT还原与gReadyQueue调度机制

1. 问题现象&#xff1a;一条让你摸不着头脑的内核日志先说我是在什么场景下碰到这个问题的。一台跑着较新内核的服务器&#xff0c;固件里用了比较完整的ACPI表&#xff0c;系统在空闲状态下会自动触发PCIe设备的电源状态迁移&#xff0c;部分设备会进入D3cold。某次我在抓电源…

作者头像 李华
网站建设 2026/10/4 4:17:36

微信小程序点餐系统毕业设计:Java后端与数据库实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/4 4:16:44

基因组重复序列注释全流程:RepeatModeler建库与RepeatMasker实战指南

拿到一个刚组装的基因组&#xff0c;第一步不是急着做基因注释&#xff0c;而是先把里面的重复序列全部“揪出来”。这个问题我吃过亏&#xff1a;早期做某个非模式物种基因组时&#xff0c;直接拿Repbase数据库跑了RepeatMasker&#xff0c;结果后来做基因结构预测时&#xff…

作者头像 李华
网站建设 2026/10/4 4:14:48

MSRP客户端开发指南:从SIP SDP协商到消息传输与调试避坑

简介&#xff1a;MSRP&#xff08;Message Session Relay Protocol&#xff09;是SIP会话中承载富媒体内容的重要扩展&#xff0c;这套C源码以轻量实现展示了SIP客户端如何集成MSRP协议&#xff0c;适合VoIP、即时通信及SIP协议栈开发者作为参考样例。压缩包内共46个文件&#…

作者头像 李华