十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

JSP+Servlet外卖订餐系统拆解:从订单状态机到多角色权限设计

JSP+Servlet外卖订餐系统拆解:从订单状态机到多角色权限设计 简介一套基于JSPServlet开发的外卖订餐系统项目采用经典MVC架构覆盖会员、骑手、商家、管理员四类角色完整实现从点餐、接单、配送到后台管理的业务流程。项目以Eclipse为开发环境配合MySQL数据库适合Java Web课程设计、毕业设计或初级开发者系统学习Servlet与JSP协作方式。压缩包整体约93.63MB内含项目源代码、online.sql数据库脚本、账号说明文档、系统界面截图以及配套的演示视频便于部署对照与运行验证。代码分层清晰控制器、业务逻辑和页面展示分离能直观体会MVC模式在真实项目中的落地方式。目前已有659人学习使用。通过该资源可获得一套可运行的外卖订餐项目源码与设计思路学习多角色权限控制、订单状态流转、数据库表结构设计等关键技能对巩固Java Web基础并提升项目开发能力有实际帮助。1. 为什么一个课程设计值得拆开看这套基于JSPServlet的外卖订餐系统表面是典型的Java Web课程设计实际是一份能跑通「会员下单、商家接单、骑手配送、管理员监管」全链路的完整业务闭环。它没有用Spring Boot那种约定大于配置的封装所有请求路由、参数封装、页面跳转全靠手写Servlet恰恰因为笨反而把Web应用最底层的运行机制摊开在代码里。适合刚学完Java基础、想用Eclipse把Servlet和JSP串成完整项目的人也适合需要快速交付课设、又不想只交一个空壳页面的同学。拆这个项目不只是为了跑通而是借一套真实业务理解JSP和Servlet在MVC里到底各自扛什么活订单状态又是如何在四个角色之间流转的。2. JSP与Servlet的分工从web.xml配置看请求如何流转很多初学者会把JSP和Servlet当成两个孤立的技术去学这个外卖项目正好把它们拧在一起。核心区别在职责Servlet工作在服务端接收HTTP请求、调用业务逻辑、控制页面跳转但绝不直接拼HTMLJSP负责渲染视图把Java对象的数据输出成用户看到的页面。两者通过request和session对象交换数据配合web.xml里的映射关系构成一条完整的请求链路。2.1 手写Servlet映射与注解式开发的差异在Eclipse里新建Dynamic Web Project后最关键的配置文件是WEB-INF/web.xml。早期版本需要手动注册Servlet现在的Servlet 3.0支持注解但看懂web.xml的写法能帮你在老项目和面试题里不踩坑servlet servlet-nameLoginServlet/servlet-name servlet-classcom.online.servlet.LoginServlet/servlet-class /servlet servlet-mapping servlet-nameLoginServlet/servlet-name url-pattern/login/url-pattern /servlet-mapping这段配置做了两件事声明LoginServlet这个类由容器管理并把/login这个URL路径映射到它。当浏览器访问http://localhost:8080/online/login时Tomcat根据url-pattern找到对应的servlet-name再通过servlet-name找到servlet-class完成实例化并调用doGet或doPost。如果你在类上写了WebServlet(/login)效果一样但写web.xml的好处是修改映射不用重新编译Java类。提示servlet-mapping的url-pattern必须带斜杠开头但不需要写项目名。项目名是容器部署时决定的访问路径中的/online来自Eclipse里配置的Context Path。2.2 doGet与doPost的分流逻辑LoginServlet是系统的入口之一它接收用户从登录表单提交的账号密码调用UserDao查询数据库再根据结果决定跳转到会员首页还是商家工作台。核心代码如下protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String username request.getParameter(username); String password request.getParameter(password); UserDao userDao new UserDao(); User user userDao.findUserByUsernameAndPassword(username, password); if (user ! null) { HttpSession session request.getSession(); session.setAttribute(user, user); session.setAttribute(role, user.getRole()); // 按角色分发到不同首页 if (admin.equals(user.getRole())) { response.sendRedirect(request.getContextPath() /admin/index.jsp); } else if (merchant.equals(user.getRole())) { response.sendRedirect(request.getContextPath() /merchant/index.jsp); } else { response.sendRedirect(request.getContextPath() /member/index.jsp); } } else { request.setAttribute(msg, 账号或密码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); } }这段代码里有几个值得注意的细节。setCharacterEncoding(UTF-8)必须放在getParameter之前否则POST提交的中文会乱码这是JSP项目最常见的坑。getParameter拿到的是表单字段值getSession()在没有session时自动创建sendRedirect是重定向浏览器地址栏会变forward是服务端跳转地址栏不变还能携带request属性。项目里正是用这两种跳转区分登录成功进入新页面和登录失败返回错误提示。2.3 JSP页面如何取数据Servlet处理完业务逻辑数据存放在request或session域中JSP页面用EL表达式和JSTL标签取值。以会员点餐页面的购物车数量为例c:if test${sessionScope.cart ! null} span购物车商品数${fn:length(sessionScope.cart.items)}/span /c:ifsessionScope.cart拿到会话中的购物车对象fn:length是JSTL函数库提供的方法直接计算集合尺寸。这里强调sessionScope前缀是因为用${cart}也能取到值但会先在pageScope、requestScope、sessionScope、applicationScope依次查找显式指定sessionScope可以避免和其他域中的同名属性冲突。3. 多角色权限模型会话跟踪与页面访问控制四种角色复用同一个登录接口但进入系统后看到的界面和能执行的操作完全不同。这背后是Java Web经典的会话跟踪机制以及权限判断的代码实现。3.1 基于Session的角色标识登录成功后系统把User对象和role字符串写入session后续每个请求都靠session.getAttribute(role)判断当前用户身份。这种做法的优点是实现简单缺点是无法防止越权访问——用户手动在浏览器地址栏输入/admin/index.jsp如果该JSP没有做权限校验就绕过了登录限制。3.2 Filter拦截未授权请求正规的项目会加一层Filter做粗粒度拦截本项目虽然以纯ServletJSP为主但一样可以用Filter解决垂直越权问题。在WEB-INF/web.xml中配置filter filter-nameAuthFilter/filter-name filter-classcom.online.filter.AuthFilter/filter-class /filter filter-mapping filter-nameAuthFilter/filter-name url-pattern/admin/*/url-pattern url-pattern/merchant/*/url-pattern url-pattern/rider/*/url-pattern /filter-mappingFilter代码如下public class AuthFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) res; HttpSession session request.getSession(false); String uri request.getRequestURI(); if (session null || session.getAttribute(user) null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } String role (String) session.getAttribute(role); if (uri.startsWith(request.getContextPath() /admin) !admin.equals(role)) { response.sendError(HttpServletResponse.SC_FORBIDDEN); return; } chain.doFilter(req, res); } }逻辑分三层判断session不存在或未登录统一踢回登录页已登录但角色不匹配返回403状态码只有通过校验才调用chain.doFilter放行。注意request.getSession(false)传了false参数表示如果没有session就返回null而不是新建一个避免给静态资源也创建会话。3.3 四种角色的数据隔离数据库层面通过user表里的role字段区分身份订单表通过外键关联不同角色的用户ID。角色与核心操作权限对应关系如下角色核心操作关联数据表会员浏览菜品、加入购物车、提交订单、订单评价user, orders, order_detail, comment商家菜品上架下架、修改库存、接单、标记出餐dish, orders, order_detail骑手接配送单、更新配送状态、查看配送记录orders, delivery管理员用户管理、订单监管、数据统计、系统公告全部表比如商家表里存了商家ID菜品表dish通过merchant_id关联商家会员下单后订单落到orders表骑手接单时只能看到配送范围内的订单。每个角色在自己的维度上操作同一批数据但权限边界清晰这是项目数据库设计最值得学习的地方。4. 订单状态机设计从购物车到配送完成的流转外卖系统最核心的业务逻辑是订单状态流转它不是简单的字段更新而是一套状态机待支付 → 待接单 → 待配送 → 配送中 → 已完成中间还可能插入已取消和退款。这个项目的订单模块完整覆盖了这些状态看懂了状态机就拿到了整个系统的钥匙。4.1 购物车与结算流程会员添加菜品到购物车购物车存储在session域中这是JSP项目的常见做法数据不落库、用户刷新不丢。结算时遍历购物车条目计算总价生成订单主表和明细表然后清空session中的购物车。下单核心方法public int createOrder(HttpServletRequest request) { HttpSession session request.getSession(); Cart cart (Cart) session.getAttribute(cart); User user (User) session.getAttribute(user); // 1. 校验购物车非空 if (cart null || cart.getItems().isEmpty()) { return -1; } // 2. 插入订单主表 Order order new Order(); order.setUserId(user.getId()); order.setMerchantId(cart.getMerchantId()); order.setTotalAmount(cart.getTotalAmount()); order.setStatus(0); // 0待接单 1已接单 2配送中 3已完成 4已取消 int orderId orderDao.insertOrder(order); // 3. 批量插入订单明细 for (CartItem item : cart.getItems()) { OrderDetail detail new OrderDetail(); detail.setOrderId(orderId); detail.setDishId(item.getDishId()); detail.setDishName(item.getDishName()); detail.setPrice(item.getPrice()); detail.setQuantity(item.getQuantity()); orderDetailDao.insert(detail); } // 4. 清空购物车 session.removeAttribute(cart); return orderId; }状态值用int表示而非字符串好处是存储占用小、比较判断快坏处是代码可读性差。如果接手这种项目我会建议用常量类定义状态值比如OrderStatus.PENDING 0避免在Service层到处写魔法数字。这个逻辑里需要思考的是订单主表存merchantId省去多商家拆单的复杂度假设一个购物车只对应一个商家的菜品适合单品店场景但到了多商家聚合下单的平台阶段就不适用了。4.2 商家接单与骑手配送的并发控制当会员提交订单后商家端轮询“待接单”列表点击接单把状态从0改为1同时把订单分配给当前登录的骑手。多商家同时操作同一张订单时高并发下可能超卖或重复接单。本项目是课设级别用同步代码块加简单状态校验即可public synchronized boolean acceptOrder(int orderId, int merchantId) { Order order orderDao.findById(orderId); if (order null || order.getStatus() ! 0) { return false; } if (order.getMerchantId() ! merchantId) { return false; } order.setStatus(1); order.setAcceptTime(new Date()); return orderDao.updateStatus(order) 0; }synchronized在单Tomcat实例下可以阻止两个线程同时读到status0但分布式部署时无效正确做法是用数据库乐观锁SQL层做条件更新UPDATE orders SET status1 WHERE id? AND status0影响行数为0说明订单已被别人抢走。这是课设代码和生产系统的分水岭面试被问“高并发下订单重复接单怎么解决”时回答WHERE status0这个条件更新能让面试官对你刮目相看。4.3 订单状态变更的SQL实现订单状态一旦流转就需要记录时间线这是一个被很多课设忽略的细节订单状态变更不仅要更新主表还要在状态轨迹表中留痕。数据库里可以新建一张order_trace表CREATE TABLE order_trace ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, status TINYINT NOT NULL COMMENT 状态值0待接单 1已接单 2配送中 3已完成 4已取消, operate_role VARCHAR(20) NOT NULL COMMENT 操作角色member/merchant/rider/admin, operate_user_id INT NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_order_id (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单状态流转轨迹表;每次调用updateStatus之前先插入一条trace记录之后查看订单详情时按create_time升序排列就能还原整个订单生命周期。opreate_role记录谁干的活配合operate_user_id能精确到人这个设计在售后纠纷排查时极其有用。MySQL的utf8mb4字符集必须用因为utf8在MySQL里最多存3字节表情符号和生僻字会报错。5. 数据导入与多表联查online.sql的还原技巧拿到压缩包后第一件事是导入数据库。online.sql文件里既包含建表语句又包含初始数据不同版本的MySQL导入方式有差异处理不当会出现编码问题或外键约束失败。5.1 命令行导入与Navicat导入的差异命令行导入适合MySQL 5.7和8.0通用mysql -u root -p --default-character-setutf8mb4 online online.sql--default-character-setutf8mb4强制客户端按utf8mb4编码发送SQL语句。如果你用Navicat导入注意在导入向导中选择「遇到错误继续」SQL文件里可能存在DROP TABLE IF EXISTS语句某些版本会在第一次执行时中断事务。导入完成后执行SHOW TABLES;确认表数量再用SELECT COUNT(*) FROM user;检查数据是否完整。如果表里有中文乱码多半是SQL文件的编码格式和数据库连接的character_set不一致用SET NAMES utf8mb4;手动切换后再查询。5.2 订单列表的多表关联查询系统后台的管理员订单管理页面需要同时展示会员昵称、商家名称、骑手姓名和订单金额这要求四表联查。以查询所有订单为例SELECT o.id AS order_id, u.nickname AS member_name, m.shop_name AS merchant_name, r.nickname AS rider_name, o.total_amount, o.status, o.create_time FROM orders o LEFT JOIN user u ON o.user_id u.id LEFT JOIN user m ON o.merchant_id m.id LEFT JOIN user r ON o.rider_id r.id ORDER BY o.create_time DESC;这里用LEFT JOIN而不是INNER JOIN因为骑手未分配时rider_id为空内连接会把未接单的订单过滤掉。user表同时关联了会员、商家、骑手三种角色通过不同的外键字段关联同一张表这是数据库设计里的自关联技巧。项目源码中尽量复用这个SQL避免在Java层循环查库造成的N1问题。6. Eclipse部署踩坑Tomcat版本、JSP编译目录与乱码定位部署JSP项目最容易出问题的环节集中在三处Tomcat版本不兼容、JSP编译后的class文件路径、以及请求响应乱码。这里给出针对这套系统的验证与排错方法。6.1 检查JSP编译产物在Eclipse里启动Tomcat后如果页面报500错误先别怀疑代码逻辑打开{Tomcat安装目录}/work/Catalina/localhost/online/org/apache/jsp/目录找到对应的login_jsp.java和login_jsp.class。JSP本质是一个继承了HttpJspBase的Servlet容器第一次访问页面时把JSP文件翻译成Java源文件再编译。如果这里没有生成class文件说明JSP语法有误查看Tomcat的localhost.log日志能定位到具体行。提示修改JSP后刷新浏览器不生效是因为Tomcat检测文件变化后重新编译了但某些文件锁和Eclipse的自动发布机制冲突。右键项目 → Clean再重新Publish到Tomcat基本能解决90%的页面不生效问题。6.2 中文乱码的三层排查JSP页面中文乱码要从三个层面逐一排查。第一层是JSP文件头声明% page contentTypetext/html; charsetUTF-8 languagejava %这决定了Tomcat用什么编码读JSP文件、输出什么编码到浏览器。第二层是Servlet侧的request.setCharacterEncoding(UTF-8)它必须在读取参数前调用否则POST参数按ISO-8859-1解码中文必乱。第三层是数据库连接串JDBC的URL必须带characterEncodingutf8private static final String DB_URL jdbc:mysql://localhost:3306/online ?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai;useSSLfalse是因为本地开发不需要加密连接serverTimezoneAsia/Shanghai解决MySQL 8.0的时区报错。注意MySQL 8.0的驱动类名是com.mysql.cj.jdbc.Driver老项目的com.mysql.jdbc.Driver需要换成新的否则抛ClassNotFoundException。6.3 账号验证与业务链路自测压缩包中的帐号.txt内置了四种角色的测试账号登录后建议按照这个顺序走一遍完整链路用会员账号下单 → 商家账号登录查看新订单并接单 → 骑手账号接配送单并标记送达 → 会员确认收货并评价 → 管理员后台查看订单状态是否逐级更新。每一级状态变更后回到order_trace表执行SELECT * FROM order_trace WHERE order_id 刚下单的ID;对照时间线检查状态值是否按0→1→2→3递增任何一步跳变都说明控制层逻辑有漏洞或SQL条件更新没有命中。这条自测路径能覆盖登录会话、角色拦截、数据关联、状态流转四大核心模块比单点调试某个功能页更能暴露系统集成问题。本文还有配套的精品资源点击获取
返回列表