ARTICLE DETAIL

资讯详情

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

JSP+Servlet开发外卖订餐系统:从会话管理到订单状态机实战

JSP+Servlet开发外卖订餐系统:从会话管理到订单状态机实战 简介基于JSP与Servlet开发的外卖订餐系统完整项目采用MVC分层架构覆盖会员、骑手、商家、管理员四类角色前台点餐与后台管理逻辑完整适合Java Web初学者、课程设计及毕业设计开发者参考。项目压缩包约93.63MB内含源代码、MySQL数据库脚本、账号说明文本及操作演示视频等文件导入Eclipse并执行数据库脚本即可快速搭建运行省去手工建表环节。系统以Servlet作为控制层负责请求分发JSP承担页面渲染结合数据库完成菜品浏览、购物车、订单流转、配送管理等核心业务不同角色拥有独立权限。已有659人学习下载通过该项目可系统掌握JSP、Servlet、MVC模式、数据库表设计、多角色权限控制等关键技能也能为后续SSM或Spring Boot学习打下坚实基础。配套演示视频可直观了解部署与使用流程便于参照源码逐项理解实现细节。1. 为什么用JSPServlet做外卖订餐系统反而更容易入门很多新手第一眼看到“JSPServlet开发外卖订餐系统”会觉得这技术栈老掉牙现在Spring Boot满天飞谁还碰这些但真去带几个做毕设或者练手的同学你就知道JSPServlet这套组合把HTTP请求从Tomcat进来、Servlet处理逻辑、JSP渲染页面这个过程拆得极其透明没有任何自动配置的黑匣子。这个外卖系统覆盖会员点餐、骑手接单、商家出餐、管理员统计四个角色正好把会话跟踪、过滤器、状态机、多表联查这些最核心的Java Web知识全部串起来了。如果你正在选毕设方向或者想把Servlet从底层吃透这套项目是值得认真投入的。别被“老”吓退老技术学透了看新框架三天就能上手。2. IDEA新建JSP项目与数据库设计先搭好Servlet容器的骨架2.1 在IDEA里新建传统Web工程而不是创建Spring Boot以前用Eclipse建Dynamic Web Project现在大家都用IDEA但IDEA默认的新项目向导会把人往Spring Boot上带。做传统JSPServlet项目我一般是创建Maven工程然后手工补war插件这样最可控。在IDEA里选择Maven Archetype时选maven-archetype-webapp或者直接建普通Maven工程后在pom.xml里把packagingwar/packaging写死再补src/main/webapp目录。这里千万别点Spring Boot因为Spring Boot内嵌Tomcat对JSP支持有限IDE里能跑打包后JSP找不到是常事。pom.xml里的Servlet和JSP依赖是这样的dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdjavax.servlet.jsp/groupId artifactIdjsp-api/artifactId version2.3.3/version scopeprovided/scope /dependency这里scopeprovided非常关键。Tomcat自带了Servlet和JSP的实现类如果你把依赖打进war包里部署时可能和Tomcat自带版本冲突典型现象是启动报java.lang.LinkageError或者NoSuchMethodError。改成provided后编译期有依赖打包时排除交给Tomcat运行时提供。JDBC驱动和数据库连接池则要正常打进去比如mysql-connector-java和HikariCP这两个不写provided。2.2 Servlet生命周期与请求映射先搞清楚两个入门概念Servlet的生命周期不是玄学就三个阶段init()只在第一次请求或服务器启动时执行一次适合初始化数据库连接池service()处理每次请求根据GET/POST转发到doGet和doPostdestroy()在容器关闭时执行释放资源。很多人翻车是因为过度在doGet里做数据库查询其实读请求数据放在doPost更安全避免URL参数泄露到访问日志里。请求映射有两种方式。第一种是web.xml里写servlet-mapping第二种是Servlet类上用WebServlet(/order/submit)注解。我推荐注解少改配置。但要注意url-pattern的匹配规则/会匹配所有路径但排除JSP/*会匹配包括JSP在内的一切*.do是扩展名匹配。很多新手用/*拦截所有请求结果把JSP页面也拦截了页面直接返回空白或者404。一个完整的Servlet模板长这样WebServlet(/order/submit) public class OrderSubmitServlet extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String orderId req.getParameter(orderId); // 业务逻辑 resp.sendRedirect(req.getContextPath() /order/list.jsp); } }这里注意req.setCharacterEncoding必须在读取参数之前调用否则POST请求的中文参数全是乱码。getParameter返回的是字符串转数字时要处理NumberFormatException否则用户传一个非数字参数服务器直接500。2.3 外卖数据库表设计四类角色加订单状态怎么落表数据库设计是这套项目的地基。四类角色各一张表再加菜品、订单、订单明细。最核心的是订单表它把会员、商家、骑手全部关联起来。我给出最小可用的建表语句CREATE TABLE member ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password CHAR(64) NOT NULL, phone VARCHAR(20), address VARCHAR(255) ); CREATE TABLE merchant ( id INT PRIMARY KEY AUTO_INCREMENT, shop_name VARCHAR(100) NOT NULL, phone VARCHAR(20), status TINYINT DEFAULT 1 ); CREATE TABLE rider ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, phone VARCHAR(20), status TINYINT DEFAULT 0 COMMENT 0空闲,1配送中 ); CREATE TABLE dish ( id INT PRIMARY KEY AUTO_INCREMENT, merchant_id INT NOT NULL, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, stock INT DEFAULT 0 ); CREATE TABLE order_info ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_sn VARCHAR(32) UNIQUE, member_id INT NOT NULL, merchant_id INT NOT NULL, rider_id INT, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付,1待接单,2配送中,3已完成,4已取消, total_amount DECIMAL(10,2), create_time DATETIME, pay_time DATETIME );注意订单表名千万别用order那是SQL保留字MySQL里可以用反引号包着但所有查询都得写反引号太痛苦直接叫order_info省心。状态字段用TINYINT而不是字符串好处是状态机流转时写SET status2 WHERE status1这种条件更新特别清晰而且索引体积小。order_sn要设唯一索引因为订单号需要保证并发下不重复。3. 会员点餐到商家接单用Servlet实现完整业务流3.1 会员端会话购物车用HttpSession而不是Cookie购物车是会员端最典型的需求。有人图省事把购物车数据放Cookie里结果很快就后悔。Cookie上限只有4KB存几个菜品ID和数量还好一旦加备注、规格、优惠信息就爆了而且每次HTTP请求都要来回传递JSON序列化转义折腾得头皮发麻。正确做法是放HttpSession里默认30分钟过期用户关掉浏览器购物车还在体验更好。我给一个加购的Servlet实现WebServlet(/cart/add) public class CartAddServlet extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); Integer dishId Integer.valueOf(req.getParameter(dishId)); int count Integer.parseInt(req.getParameter(count)); HttpSession session req.getSession(); MapInteger, Integer cart (MapInteger, Integer) session.getAttribute(cart); if (cart null) { cart new HashMap(); } cart.merge(dishId, count, Integer::sum); session.setAttribute(cart, cart); resp.sendRedirect(req.getContextPath() /cart.jsp); } }cart.merge是Java 8给Map加的语法糖键不存在就放初始值存在就执行Integer::sum把同一种菜品的数量累加。这里有个细节dishId和count直接Integer.valueOf和parseInt一旦前端传了异常参数就会抛异常。真实项目要包一层异常处理返回JSON提示参数错误但很多演示项目就这么裸写能跑就行你自己做的时候最好加上。为什么用Session而不是数据库购物车是临时数据没下单前不需要持久化。如果每次加购都写数据库压力全在DB上而且用户随手关页面会产生一堆僵尸数据。Session天然跟着会话走超时自动清理数据库零压力。3.2 商家接单与订单状态机防止重复接单的乐观锁写法订单状态机是这个系统的核心脉络。设计上最容易被忽略的点是并发。外卖场景里一个订单同时被两个骑手看到两个人都点“抢单”如果代码写成“先查状态再更新”必翻车。两个请求都查到状态是待接单然后都执行UPDATE order_info SET rider_id? WHERE id?第二个请求会直接覆盖第一个两个骑手都以为自己抢到了但数据库里只有一个人。解决办法是乐观锁用条件更新代替“先查后改”String sql UPDATE order_info SET status2, rider_id? WHERE id? AND status1; int rows db.update(sql, riderId, orderId); if (rows 0) { // 抢单失败订单已被别人处理 }这个SQL的意思是只有状态还是待接单status1时才能把状态改成配送中status2。UPDATE返回的受影响行数rows是关键。如果rows是0说明订单状态已经变了这次抢单失败。不用锁表不用事务一行SQL就把并发问题解决了。实践中骑手抢单会频繁调用这个接口核心就是这条批量更新语句。商家端的接单逻辑同理。商家点击“接单”时执行UPDATE order_info SET status1 WHERE id? AND status0防止同一个订单被重复确认。整个状态机就靠WHERE里带一个当前状态条件来推进简单有效。注意状态流里待支付→待接单需要事务因为要同时更新库存和状态但待接单→配送中这种单状态变更不需要事务。3.3 JSP页面展示数据request转发与EL表达式的配合有同学喜欢在Servlet里直接response.getWriter().println(html)拼页面一顿操作猛如虎改样式时想死的心都有。正确做法是Servlet只准备数据JSP负责渲染。比如订单列表页面Servlet里把订单集合塞进requestreq.setAttribute(orderList, orderService.listOrders(memberId)); req.getRequestDispatcher(/member/orderList.jsp).forward(req, resp);JSP里用EL表达式和JSTL遍历% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html body table c:forEach varorder items${orderList} tr td${order.orderSn}/td td c:choose c:when test${order.status 0}待支付/c:when c:when test${order.status 1}待接单/c:when c:otherwise配送中/c:otherwise /c:choose /td /tr /c:forEach /table /body /html这里要注意转发和重定向的区别。forward是服务器内部跳转浏览器地址栏不变request里的属性还能读到sendRedirect是浏览器发起新请求地址栏变request里的数据全丢。所以setAttribute后必须用forward。如果用了sendRedirect去JSP页面${orderList}永远是空的这个坑特别隐蔽。另外JSP页面里的状态码展示推荐用c:choose而不是在Java代码里拼字符串。因为订单状态字段在数据库是数字显示层要转成中文把转换逻辑放JSP里可以让Servlet保持纯净。如果想省事也可以在订单模型里加一个getStatusText()方法直接在EL里调用。4. 避坑与排查权限拦截、状态并发与打包部署的翻车记录4.1 过滤器拦掉了JSP样式和页面一起崩了现象给系统加登录验证过滤器配置了WebFilter(/*)结果打开登录页发现所有CSS、图片全挂点击登录按钮后跳转的JSP页面是空白的控制台还报404。原因/*真的匹配所有请求包括/css/style.css、/login.jsp过滤器把这些请求全部拦住往login.jsp重定向变成了无限重定向循环。Tomcat收到循环请求后直接拒绝表现为页面白屏或404。实际上JSP和静态资源本应由Tomcat默认Servlet处理不需要经过权限过滤器。解决把你的权限校验过滤器拦截范围改小只拦截Servlet路径。比如所有后端请求都以/api/开头或者以.action结尾静态资源和JSP页面放行。如果是注解式过滤可以写成WebFilter(urlPatterns {/api/*, /servlet/*})。如果是web.xml配置注意filter-mapping里的url-pattern别偷懒全用/*。另外JSP页面内部的include如果被过滤器拦截还会出现半个页面加载出来的诡异现象排查时先看过滤器配置。4.2 两个骑手同时抢单订单状态被后提交的覆盖现象测试环境下A、B两个骑手同时点“抢单”两个人都看到“抢单成功”刷新订单详情发现骑手信息是BA不服查日志发现A的更新也执行了只是后更新的把前面覆盖了。原因就是上一章讲的“先查询后更新”问题。代码写成先SELECT status判断是待接单再UPDATE SET rider_id?这两个操作之间有间隙并发时两个请求都通过判断都执行更新没有条件约束后面的覆盖前面的。解决把两条SQL合并成一条条件更新。UPDATE order_info SET status2, rider_id? WHERE id? AND status1然后检查返回行数。这就是乐观锁的经典应用。如果对应项目里有冗余字段比如“抢单次数”要累加可以写成SET confirm_countconfirm_count1但核心还是WHERE status1。记住一条状态机流转必须用条件更新任何“先查再改”在并发面前都是裸奔。另外如果用的是MySQL注意事务隔离级别默认REPEATABLE_READ但这里因为是一个原子UPDATE不会碰到幻读问题放心。4.3 Nginx无法直接跑JSP反向代理才能救你现象把项目打包成war部署到Tomcat8080端口访问一切正常。但为了对外提供80端口服务装了Nginx配置了location / { root /opt/tomcat/webapps/xxx; }然后访问页面发现JSP内容全被原样打印出来或者直接502。原因很多人以为Nginx能解析JSP。其实Nginx天生不是Servlet容器它只能处理静态文件或者把请求转发给后端。JSP文件必须交给Tomcat这类Servlet容器翻译成Servlet再编译执行。把JSP放在Nginx的root目录里Nginx不认识直接把JSP源码当文本返回了。解决Nginx只做反向代理把动态请求转发给Tomcat。配置大概是这样的server { listen 80; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这样所有请求都交给Tomcat处理JSP能被正确解析。如果想用Nginx加速静态资源可以单独配location ~* \.(css|js|png)$ { expires 7d; root /opt/tomcat/webapps/xxx; }但图片路径要对上Tomcat的部署目录否则还是404。这个坑在毕设答辩演示时特别容易踩因为很多人的电脑性能一般开了Nginx就觉得改了配置就能跑JSP结果白屏一脸懵。5. 打包war与上线验证最后再补几个保命骚操作先用Maven把项目打成war包这一步能暴露一半以上的环境问题。在项目根目录执行mvn clean package -DskipTests生成的target/外卖订餐系统.war直接复制到Tomcat的webapps目录下启动Tomcat就会自动解压部署。这里注意如果你用IDEA自带的Artifacts打包可能漏掉一些依赖而Maven打包会按照pom.xml完整引入依赖更稳妥。部署后别急着浏览页面先看日志tail -f /opt/tomcat/logs/catalina.out重点看有没有ClassNotFoundException和JasperException。前者多半是依赖没打进去后者多半是JDK版本和JSP编译器不匹配。Tomcat 9用JDK 8或11都行但你要是用JDK 17部分老版本Tomcat会直接崩溃建议Tomcat 9.0.50以上配JDK 11。验证接口时我习惯用curl而不是点浏览器因为能看状态码和响应头定位问题更快curl -X POST http://localhost:8080/order/submit -d orderId1001memberId8如果返回乱码检查两处JSP页面pageEncodingUTF-8以及Tomcat的server.xml里URIEncodingUTF-8。这基本是中文乱码的万能解药。最后提一句线上部署时一定要关闭JSP的自动编译开关也就是在web.xml里把development设为false否则每次访问页面都会检查有没有更新性能损耗肉眼可见。我把这些踩坑经验放在手边每次重做项目都少折腾半天希望帮到你。本文还有配套的精品资源点击获取
返回列表