ARTICLE DETAIL

资讯详情

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

JSP+Servlet四角色外卖系统:权限控制与订单状态机实战

JSP+Servlet四角色外卖系统:权限控制与订单状态机实战 简介本资源是一套基于JSPServlet开发的完整外卖订餐系统实战项目面向Java Web初学者与课程设计学生解决多角色协同业务建模与MVC架构落地实践问题。压缩包为ZIP格式大小93.63MB包含源代码、MySQL数据库脚本online.sql、系统部署说明、角色功能截图及配套视频教程.wmv覆盖会员下单、商家接单、骑手配送、管理员监控等全流程模块。已有660人学习下载项目采用Eclipse开发环境以JSP实现动态页面展示Servlet处理请求逻辑MySQL支撑用户、订单、商品等核心数据管理结构清晰、职责分明便于理解MVC分层思想与Web应用开发规范。读者可直接导入运行快速掌握Java Web基础技术栈集成、多角色权限划分及真实业务场景下的数据库设计与交互逻辑。1. 这不是又一个“学生作业式”外卖系统它用 JSPServlet 跑通了四角色协同闭环真能接单、派单、结算、查轨迹你搜“JSP 外卖系统”十有八九点开是单页静态菜单模拟下单的 demo连登录都只校验用户名密码更别说骑手接单后状态怎么同步、商家确认出餐后怎么触发配送、会员积分怎么实时扣减——这些不是锦上添花而是业务流卡点。这个.zip包里的系统从数据库表设计到 Servlet 分发逻辑完整覆盖「会员下单 → 商家接单/出餐 → 骑手接单/送达 → 管理员监控」全链路所有角色共用同一套 Session 管理和权限路由不是靠 if-else 切页面而是用role_idfilter动态拦截请求路径。它不依赖 Spring Boot 或 Vue 前端框架纯 JSP 渲染 Servlet 控制 JDBC 直连 MySQL部署在 Tomcat 8.5 上就能跑适合教学演示、课程设计答辩、或作为中小本地餐饮 SaaS 的最小可行原型MVP。如果你正被“如何让 Servlet 真正管住四类用户权限”“JSP 怎么安全传参又不暴露订单 ID”“骑手端地图坐标怎么和后台订单绑定”这类问题卡住这个包不是玩具是能照着改、能上线试跑、能让你讲清楚“为什么这里必须用HttpSessionListener而不是Filter”的实操基线。2. 四角色权限体系从数据库建模到 Servlet Filter 的硬核落地2.1 数据库设计一张 user 表撑起四角色靠 role_id 和 status 字段做分流系统没搞四张用户表user_member、user_rider、user_merchant、user_admin而是在t_user表里用role_id TINYINT区分角色1会员2骑手3商家4管理员再用status TINYINT控制生命周期0禁用1启用2冻结。关键字段如下字段名类型说明idBIGINT PK主键自增usernameVARCHAR(32) NOT NULL UNIQUE登录账号全局唯一passwordVARCHAR(64) NOT NULLSHA-256 加密存储非明文role_idTINYINT NOT NULL DEFAULT 1角色标识决定首页跳转和菜单可见性statusTINYINT NOT NULL DEFAULT 1账号状态登录时强制校验real_nameVARCHAR(20)会员/骑手/商家真实姓名phoneVARCHAR(15)绑定手机号用于短信通知预留字段balanceDECIMAL(10,2) DEFAULT 0.00会员余额单位元rider_licenseVARCHAR(64)骑手身份证号仅骑手角色填充merchant_addressTEXT商家详细地址仅商家角色填充提示role_id不是外键关联角色表而是硬编码值。好处是查询快避免 JOIN坏处是扩展新角色需改代码。教学场景下这是合理取舍——你真要在生产环境加“供应商”角色先重构权限模块而不是改个数字。2.2 登录认证Servlet 层统一拦截Session 存对象而非字符串登录入口是LoginServlet.java核心逻辑不是简单request.getParameter(username)而是// LoginServlet.java protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username request.getParameter(username).trim(); String password request.getParameter(password); // 1. 密码加盐校验使用 SHA-256 随机 saltsalt 存在数据库 salt 字段 User user userService.findByUsername(username); if (user null || !PasswordUtil.verify(password, user.getPassword(), user.getSalt())) { request.setAttribute(error, 用户名或密码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); return; } // 2. 检查账号状态 if (user.getStatus() ! 1) { request.setAttribute(error, 账号已被禁用请联系管理员); request.getRequestDispatcher(/login.jsp).forward(request, response); return; } // 3. 将完整 User 对象存入 Session不是只存 username HttpSession session request.getSession(true); session.setAttribute(currentUser, user); // 关键存对象后续所有 Servlet 可直接 getAttribute session.setMaxInactiveInterval(1800); // 30 分钟无操作自动失效 // 4. 根据 role_id 重定向首页 String redirectPath getHomePathByRole(user.getRoleId()); response.sendRedirect(redirectPath); }getHomePathByRole()方法返回role_id1→/member/index.jsprole_id2→/rider/order_list.jsp骑手首页是待接单列表role_id3→/merchant/order_manage.jsp商家首页是待处理订单role_id4→/admin/dashboard.jsp管理员首页是数据看板注意这里没用web.xml的security-constraint做声明式安全因为四角色页面路径混杂如/order/detail.jsp会员、骑手、商家都能访问但展示字段不同必须靠代码级控制。Session 存User对象而非username是为了在后续OrderServlet中能直接调用user.getRoleId()判断权限避免反复查库。2.3 权限过滤器用 Filter 拦截非法路径比 JSP 里写c:if更可靠系统在src/filter/RoleFilter.java实现了基于路径前缀的角色白名单// RoleFilter.java public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String path request.getServletPath(); // 允许静态资源和登录页直接访问 if (path.startsWith(/css/) || path.startsWith(/js/) || path.startsWith(/images/) || path.equals(/login.jsp) || path.equals(/LoginServlet)) { chain.doFilter(req, resp); return; } // 必须已登录 HttpSession session request.getSession(false); if (session null || session.getAttribute(currentUser) null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } User user (User) session.getAttribute(currentUser); int roleId user.getRoleId(); // 按路径前缀定义角色权限硬编码教学项目可接受 if (path.startsWith(/member/)) { if (roleId ! 1) { unauthorized(response); return; } } else if (path.startsWith(/rider/)) { if (roleId ! 2) { unauthorized(response); return; } } else if (path.startsWith(/merchant/)) { if (roleId ! 3) { unauthorized(response); return; } } else if (path.startsWith(/admin/)) { if (roleId ! 4) { unauthorized(response); return; } } chain.doFilter(req, resp); } private void unauthorized(HttpServletResponse response) throws IOException { response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.getWriter().write(无权限访问此页面); }血泪经验很多初学者在 JSP 页面里用c:if test${sessionScope.currentUser.roleId 1}控制按钮显示这完全无效——URL 可以直接输入/admin/user_list.jsp绕过前端判断。Filter 是 Servlet 容器层拦截只要请求进来了就必须过这一关。这个 Filter 在web.xml中配置为url-pattern/*/url-pattern确保所有请求必经之路。3. 订单状态机从会员下单到骑手送达的七步流转与 Servlet 协同3.1 状态定义与数据库字段用 status_code status_desc 双保险订单主表t_order的状态字段设计为status_codestatus_desc说明可触发角色10待支付会员下单后未付款会员支付、管理员人工代付20已支付支付成功进入配单池系统自动、管理员手动30配单中商家未接单系统正在匹配骑手系统定时任务40商家已接单商家点击“确认接单”商家50骑手已接单骑手点击“我要接单”骑手60骑手中途骑手已取餐正在配送骑手更新位置、系统超时预警70已完成骑手点击“确认送达”骑手80已取消会员/商家/管理员任一角色发起取消多角色提示status_code用两位数而非字符串方便 SQL 排序和范围查询如WHERE status_code BETWEEN 40 AND 60查配送中订单status_desc用于 JSP 页面直接显示避免硬编码中文。3.2 关键 Servlet 协同OrderServlet 承担状态变更中枢所有订单状态变更都走OrderServlet通过action参数区分操作类型// OrderServlet.java protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String action request.getParameter(action); HttpSession session request.getSession(); User currentUser (User) session.getAttribute(currentUser); switch (action) { case create: handleCreateOrder(request, response, currentUser); break; case pay: handlePayOrder(request, response, currentUser); break; case accept: handleAcceptOrder(request, response, currentUser); // 商家接单 break; case take: handleTakeOrder(request, response, currentUser); // 骑手接单 break; case deliver: handleDeliverOrder(request, response, currentUser); // 骑手送达 break; case cancel: handleCancelOrder(request, response, currentUser); // 取消订单 break; default: response.sendError(HttpServletResponse.SC_BAD_REQUEST); } }以handleAcceptOrder为例它不只是更新数据库private void handleAcceptOrder(HttpServletRequest request, HttpServletResponse response, User user) throws IOException { long orderId Long.parseLong(request.getParameter(orderId)); // 1. 校验只能是当前商家自己的订单且状态为 20已支付或 30配单中 Order order orderService.findById(orderId); if (order null || order.getMerchantId() ! user.getId() || !(order.getStatusCode() 20 || order.getStatusCode() 30)) { response.sendError(HttpServletResponse.SC_FORBIDDEN, 无权操作此订单); return; } // 2. 更新订单状态 设置商家ID 记录操作时间 order.setStatusCode(40); order.setMerchantId(user.getId()); order.setAcceptTime(new Date()); orderService.update(order); // 3. 发送站内信给骑手模拟实际可集成短信/推送 messageService.sendToRiders(新订单 order.getOrderNo() 请尽快接单, order.getId()); // 4. 重定向回商家订单管理页 response.sendRedirect(request.getContextPath() /merchant/order_manage.jsp?tabaccepted); }注意这里没有用事务注解因为没 Spring而是手动用Connection.setAutoCommit(false)try-catch-finally包裹 DB 操作。教学项目中明确写出事务边界比依赖框架更利于理解一致性保障。3.3 骑手端实时定位JSP 页面用 JS 获取坐标Servlet 接收并存入 t_rider_location骑手在/rider/order_detail.jsp页面点击“开始配送”后触发 JS 获取 GPS!-- rider/order_detail.jsp -- script function startDelivery() { if (navigator.geolocation) { navigator.geolocation.getCurrentPosition( function(position) { const lat position.coords.latitude; const lng position.coords.longitude; // 发送坐标到 Servlet fetch(%request.getContextPath()%/RiderLocationServlet, { method: POST, headers: {Content-Type: application/x-www-form-urlencoded}, body: orderId%order.getId()%lat lat lng lng }).then(r r.text()).then(text alert(text)); }, function(error) { console.error(获取位置失败:, error); } ); } } /script button onclickstartDelivery()开始配送/button对应RiderLocationServlet.java// RiderLocationServlet.java protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { long orderId Long.parseLong(request.getParameter(orderId)); double lat Double.parseDouble(request.getParameter(lat)); double lng Double.parseDouble(request.getParameter(lng)); HttpSession session request.getSession(); User rider (User) session.getAttribute(currentUser); // 插入或更新骑手位置按 orderId 关联 RiderLocation location new RiderLocation(); location.setOrderId(orderId); location.setRiderId(rider.getId()); location.setLatitude(lat); location.setLongitude(lng); location.setUpdateTime(new Date()); riderLocationService.saveOrUpdate(location); // 使用 REPLACE INTO 或 ON DUPLICATE KEY UPDATE response.getWriter().write(位置已更新); }t_rider_location表结构id BIGINT PKorder_id BIGINT INDEXrider_id BIGINTlatitude DECIMAL(10,8)longitude DECIMAL(10,8)update_time DATETIME玄学提醒Android WebView 或 iOS Safari 在某些版本下getCurrentPosition可能静默失败。生产环境必须加超时和错误 fallback如手动输入地址教学项目至少在 JS 里补error回调打印 console否则调试时会以为“功能没反应”。4. 避坑JSPServlet 外卖系统里最常翻车的 4 个硬伤4.1 现象会员登录后刷新页面就登出骑手接单后订单状态不更新原因HttpSession默认使用 Cookie 存储 Session ID但开发时用localhost:8080访问而图片/JS 引用用了http://127.0.0.1:8080浏览器视为不同域Cookie 不共享。解决统一所有 URL 前缀为${pageContext.request.contextPath}JSP 中写img src${pageContext.request.contextPath}/images/logo.pngServlet 重定向用response.sendRedirect(request.getContextPath() /xxx)检查浏览器开发者工具 Application → Cookies确认JSESSIONID是否出现在所有请求中。4.2 现象商家修改菜品价格会员端购物车里的价格还是旧的原因购物车数据存在HttpSession中但菜品价格变更后Session 里的CartItem对象没同步更新导致“内存价格”和“数据库价格”不一致。解决购物车渲染时JSP 不直接输出item.getPrice()而是调用productService.findById(item.getProductId()).getPrice()实时查库或在商家修改价格后用ServletContext广播事件遍历所有 Session 清空购物车缓存教学项目推荐前者简单可控。4.3 现象骑手端地图坐标定位偏差 500 米订单配送超时误判原因navigator.geolocation返回的是 WGS-84 坐标GPS 标准但国内地图 SDK如高德、百度使用 GCJ-02 坐标系直接画点会偏移。解决服务端不做转换JSPServlet 项目无地图 SDK而是在 JSP 页面引入高德 JS API用AMap.convertFrom([lng, lat], gps)转换后再marker.setPosition()或约定前端获取坐标后调用高德 Web Service API 转换需申请 Key教学项目可 mock 偏移量。4.4 现象管理员删除会员该会员的订单记录级联删除失败数据库报外键约束异常原因t_order表的member_id字段设置了FOREIGN KEY但删除语句没加ON DELETE CASCADE或 MySQL 存储引擎不是 InnoDBMyISAM 不支持外键。解决建表时显式声明CREATE TABLE t_order ( id BIGINT PRIMARY KEY, member_id BIGINT NOT NULL, FOREIGN KEY (member_id) REFERENCES t_user(id) ON DELETE CASCADE ) ENGINEInnoDB;并确认 MySQL 配置show engines;输出中InnoDB为YES若已建表用ALTER TABLE t_order DROP FOREIGN KEY xxx;先删旧约束再重建。5. JSP 页面性能与安全加固三招让老技术栈扛住真实流量5.1 JSP 缓存策略用% page isELIgnoredfalse % 自定义 Taglib 减少重复逻辑系统里大量订单列表页如/member/order_list.jsp需要格式化金额、状态文案、时间戳。别在每个c:forEach里写fmt:formatNumber和c:choose而是封装成自定义 Tag// src/tag/OrderStatusTag.java Tag(nameorderStatus, tagClassNamecom.example.tag.OrderStatusTag) public class OrderStatusTag extends SimpleTagSupport { private int statusCode; Override public void doTag() throws JspException, IOException { JspWriter out getJspContext().getOut(); String text getStatusText(statusCode); out.write(text); } private String getStatusText(int code) { switch (code) { case 10: return 待支付; case 20: return 已支付; case 40: return 商家已接单; case 50: return 骑手已接单; case 70: return 已完成; default: return 未知状态; } } // getter/setter... }在 JSP 中使用% taglib prefixmy uri/WEB-INF/tags/my.tld % c:forEach items${orderList} varorder tr td${order.orderNo}/td tdmy:orderStatus statusCode${order.statusCode}//td tdfmt:formatNumber value${order.totalAmount} pattern¥#,##0.00//td /tr /c:forEach这样做的好处状态文案集中维护改一处全站生效避免 EL 表达式嵌套过深导致 JSP 编译失败Tag 执行比 JSTLc:choose更快无 XML 解析开销。5.2 防 XSS 与 SQL 注入JSP 输出一律用c:out value${xxx} /Servlet 参数校验前置所有用户输入用户名、地址、评论存库前必须过滤使用 Apache Commons Text 的StringEscapeUtils.escapeHtml4(input)处理富文本对数字型参数如orderId用Long.parseLong()包裹捕获NumberFormatException对字符串参数如searchKey用正则校验^[a-zA-Z0-9\u4e00-\u9fa5\\s]{1,50}$允许中文、字母、数字、空格长度 1~50。JSP 页面输出时永远不用${param.xxx}或${user.name}直接插值必须!-- 正确 -- tdc:out value${order.merchantName} //td !-- 错误XSS 风险 -- td${order.merchantName}/td后悔药如果已上线发现 XSS紧急补救是在web.xml中配置context-param开启 EL 忽略但这会破坏所有功能。最好的后悔药是——从第一行 JSP 就养成c:out习惯。5.3 Tomcat 调优把maxThreads和connectionTimeout写进server.xml默认 Tomcat 配置conf/server.xml对并发支撑不足。针对外卖系统高频订单查询修改ConnectorConnector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 maxThreads200 !-- 默认 200够教学用 -- minSpareThreads25 maxSpareThreads75 acceptCount100 !-- 请求队列长度 -- compressionon !-- 开启 GZIP 压缩 -- compressionMinSize2048 noCompressionUserAgentsgozilla, traviata /同时在conf/web.xml中开启 Session 持久化防重启丢失manager classNameorg.apache.catalina.session.PersistentManager saveOnRestarttrue store classNameorg.apache.catalina.session.FileStore directorysessionStore / /manager我一般会在src/main/webapp/WEB-INF/web.xml里加session-config显式声明超时session-config session-timeout30/session-timeout !-- 单位分钟 -- cookie-http-onlytrue/cookie-http-only !-- 防 XSS 窃取 Session -- tracking-modeCOOKIE/tracking-mode /session-config这样比依赖 Tomcat 默认值更可控也符合 OWASP 安全基线。6. 从 ZIP 包到可交付系统五个必须亲手验证的验收动作6.1 验证点一数据库初始化脚本能否一键建库建表含测试数据解压 ZIP 后找到db/init.sql或类似命名用 MySQL 客户端执行mysql -u root -p init.sql执行后检查库是否存在SHOW DATABASES LIKE food_delivery;表是否齐全USE food_delivery; SHOW TABLES;应包含t_user,t_merchant,t_food,t_order,t_order_item,t_rider_location等 10 张表测试账号是否插入SELECT username, role_id, status FROM t_user WHERE username IN (member1, rider1, merchant1, admin1);应返回四条记录role_id分别为 1/2/3/4status1。如果init.sql缺失或执行报错立刻停下手——这不是可运行系统是半成品。我遇到过三次都是ENGINEInnoDB被写成ENGINEMyISAM或AUTO_INCREMENT字段漏写PRIMARY KEY。宁可花 20 分钟修 SQL别指望“运行时自动建表”。6.2 验证点二Tomcat 启动后四角色首页能否正确跳转且菜单可见部署 WAR 包或直接放webapps/ROOT后启动 Tomcat依次访问http://localhost:8080/login.jsp→ 输入member1/123456→ 应跳转到/member/index.jsp页面顶部显示“欢迎会员1”左侧菜单有“我的订单”“购物车”“个人中心”http://localhost:8080/login.jsp→ 输入rider1/123456→ 应跳转到/rider/order_list.jsp页面有“待接单”“进行中”“已完成”Tabhttp://localhost:8080/login.jsp→ 输入merchant1/123456→ 应跳转到/merchant/order_manage.jsp表格列出测试订单http://localhost:8080/login.jsp→ 输入admin1/123456→ 应跳转到/admin/dashboard.jsp有订单统计图表。关键检查打开浏览器开发者工具 Network 标签看跳转后的 HTML 是否加载成功Status 200而非 302 循环或 404。如果卡在登录页一定是RoleFilter拦截了检查web.xml中 Filter 的url-pattern是否配置为/*。6.3 验证点三订单创建→支付→商家接单→骑手接单→送达全流程状态是否连贯手动走一遍闭环会员登录加购商品提交订单 →t_order.status_code应为10待支付点击“去支付” →status_code变为20已支付商家登录进入“待处理订单”点击“确认接单” →status_code变为40accept_time有值骑手登录进入“待接单”点击“我要接单” →status_code变为50rider_id被填充骑手进入订单详情页点击“确认送达” →status_code变为70deliver_time有值。每一步后用 MySQL 客户端查t_order表验证字段变更。不要只信页面文字——页面可能缓存旧状态数据库才是真相。6.4 验证点四骑手定位坐标能否存入数据库并被查询骑手登录后打开/rider/order_detail.jsp选一个status_code50的订单点击“发送位置”按钮或页面自带自动上报然后执行SELECT * FROM t_rider_location WHERE order_id ? ORDER BY update_time DESC LIMIT 1;应返回一条记录latitude和longitude是合理数值如39.9042,116.4074北京坐标update_time是当前时间。如果查不到检查RiderLocationServlet是否被正确映射web.xml中servlet-mapping的url-pattern是否为/RiderLocationServlet以及t_rider_location表是否有UNIQUE KEY (order_id)约束避免重复插入。6.5 验证点五管理员能否禁用会员账号且该会员立即无法登录管理员登录 → 进入/admin/user_list.jsp→ 找到member1→ 点击“禁用” → 数据库中t_user.status变为0。此时新开浏览器窗口访问http://localhost:8080/login.jsp用member1/123456登录应提示“账号已被禁用请联系管理员”且 Session 不创建。这是权限体系的终极考验。如果禁用后还能登录说明LoginServlet没校验status字段或RoleFilter没拦截/member/**路径。必须追到代码里一行行确认校验逻辑。我带学生做这个项目时总强调ZIP 包不是终点是起点。它给你骨架但血肉要你亲手填——改一个status_code常量调一次Connection.commit()修一行web.xml的 Filter 配置这些动作比背概念重要十倍。当你亲手让骑手的坐标点在地图上跳动起来那刻你就真正懂了什么叫“四角色协同”。希望帮到你。本文还有配套的精品资源点击获取
返回列表