ARTICLE DETAIL

资讯详情

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

基于JSP+MYSQL的网上订餐管理系统设计与实现

基于JSP+MYSQL的网上订餐管理系统设计与实现 简介面向Java Web课程设计或毕业设计场景这套基于JSP与MySQL开发的网上订餐管理系统是一份可直接运行的完整项目适合具有Java基础、需要完成Web综合实训的在校学生使用。系统覆盖管理员与用户两端核心流程后台包括管理员登录认证、菜谱与推荐菜品增删、菜品信息与商家介绍修改、购物车订单删除、订单查看与支付管理前台包括用户注册、登录、退出、个人资料及配送地址维护、提交订单等功能模块角色分工明确业务逻辑完整。资源压缩包共含138个文件大小约2.52MB代码主体为41个Java类、41个class文件及19个JSP页面另有8个JAR依赖库、SQL建表脚本、项目配置文件和少量CSS/JS目录按controller、dao、model等层次划分导入Eclipse或IDEA并配置Tomcat即可部署。目前已有503人学习下载。整套源码附带数据库初始化脚本和连接池工具类数据库中的用户表、菜品表、订单表等结构完整读者可据此快速还原可演示系统理解JSPServletMySQL的CRUD实现、会话控制与分层设计也能直接在现有模块上扩展功能适合课程设计答辩或二次开发。1. 这个网上订餐管理系统到底要解决什么问题如果你正在为课程设计或毕业设计选题目基于JSPMYSQL实现的网上订餐管理系统很可能是你手里那份任务书上的名字。它要解决的是一个小型餐饮商户的线上点餐需求顾客能浏览菜单、注册登录、把菜加进购物车、下单商家能维护菜品、处理订单状态。听起来不复杂但真正把它做成一个能演示、能答辩、能二次开发的项目背后涉及数据表设计、JDBC连接、Session会话、事务提交这些JavaWeb最核心的东西。这套组合常被说成过时但恰恰是它让没做过完整Web项目的人能在最短时间内跑出一个看得见摸得着的系统也让接手老代码的人能顺着页面一路理到SQL。本文就按我实际做这类项目的顺序把设计、实现、踩坑一次讲透。2. 拆解网上订餐管理系统从下单流程到MySQL表设计的映射关系2.1 一个订单在系统里怎么流转用户、菜品、购物车、订单的关系拿到需求别急着写代码先在纸上画出业务流。这个系统的主线很清晰用户打开首页看到菜品列表点加入购物车购物车存放在Session里然后去结算、填地址、提交订单。此时后端要做三件事生成一条订单主记录、把购物车里的每一项拆成订单明细、更新菜品的销量。这条链路里最容易被新手做坏的就是订单主表和订单明细表的关系。为什么必须拆两张表第一一份订单对应多个菜品如果按一个订单一行数据设计把菜品名、价格、数量拼在一个字段里后续想统计哪个菜卖得好就完全没法用SQL做了。第二菜品价格会变动而订单明细里必须存下单那一刻的单价否则三个月后查历史订单金额对不上商家自己都说不清。所以订单明细表里冗余一列价格是故意为之这叫交易快照不是设计失误。下单这一步也是对mysql事务处理和mysql锁理解最深的地方。用UPDATE dishes SET sales sales 1 WHERE id ?更新销量时如果两个用户同时给同一道菜下单InnoDB的行锁会保证两次更新串行执行销量不会丢。这也是为什么建表务必用InnoDB——它支持行级锁和事务而MyISAM只有表级锁并发一高两个请求互相堵死用户体验就是页面转圈转半天。2.2 五张核心表就够了订餐管理系统的MySQL表结构设计与字段说明课程设计级别的订餐系统五张表封顶users用户表、category菜品分类表、dishes菜品表、orders订单主表、order_items订单明细表。有些项目会额外加一张cart购物车表但把购物车放进Session可以减少一张表单机演示完全够用。我一般建议先把表做少、做清楚别上来就设计七八张互相外键的表最后自己都理不清关系。字段设计上有几个经验值可以直接抄。价格用DECIMAL(10,2)绝不用FLOAT浮点数精度问题会导致金额对不上。状态字段用TINYINT数字而不是字符串比如订单状态0待支付、1已支付、2配送中、3已完成、4已取消数字排序和统计都比字符串高效。users表里password存MD5或SHA-256的十六进制字符串不存明文这个后面登录章节会展开。create_time用DATETIME DEFAULT CURRENT_TIMESTAMP插入时不传时间也自动带上。两个容易被忽略的索引点orders表的user_id和order_no要建索引查某个用户的订单和按订单号查单是最高频的SQLorder_items表的order_id也必须建索引否则订单详情页会逐行扫描明细表。order_no建议用日期加自增ID拼一段唯一字符串比如20250514001比裸自增ID更好排查问题也值得建唯一索引防重复。2.3 为什么JSPMYSQL还值得写技术选型与适用边界先说结论这个组合不是为大厂高并发准备的它的适用场景就是课程设计、毕业设计、中小企业内部管理系统、需要快速交付的Demo。选它有三个理由。第一JSPServletJDBC是JavaWeb的地基把这套跑通了以后学Spring MVC、MyBatis时脑子里会有完整的请求映射关系框架用得明白出了错也知道往哪查。第二部署链路极短一个Tomcat、一个MySQL拷个WAR包丢进webapps就能跑不像前后端分离还要Node环境、Nginx、配跨域。第三答辩时好讲——你能对着代码说清楚一次请求走了哪些类、SQL为什么这么写这是实实在在的加分项。但边界也得说透。JSP页面里混Java代码的写法在多人协作、频繁改版的项目里是灾难前端改个样式都怕碰着逻辑。高并发场景下Session存购物车、DriverManager直连数据库这套组合撑不住流量。这个系统的定位是跑通、讲透、堪用不是拿去跟互联网外卖平台比。MySQL版本上5.7是课程设计最常见的选择驱动、教程、报错解法最齐8.0以上的默认认证插件caching_sha2_password会带来SSL连接错误和驱动类名变化新手容易在第一关就翻车。我的建议是图省心用5.7.x想学新特性用8.0但连接URL里的驱动类和参数要一次性改对。3. 环境搭建与数据库初始化MySQL安装、建库脚本与JDBC连接三件套3.1 MySQL安装的两个选择解压版手动配置与镜像一键启动Windows上装MySQL常见的是msi安装向导和解压版。msi一路Next看似简单服务名、root密码、端口这些选择反而容易埋坑解压版所有配置自己写第一次觉得麻烦真正出了问题反而好排查。我一般用5.7.44解压版下zip包、写my.ini、初始化data目录、注册服务四步走。my.ini参考[mysqld] basedirD:/mysql-5.7.44-winx64 datadirD:/mysql-5.7.44-winx64/data port3306 character-set-serverutf8mb4 default-storage-engineINNODB max_connections200 [client] default-character-setutf8mb4初始化并启动服务mysqld --initialize-insecure mysqld -install net start mysql mysql -uroot -p逻辑说明--initialize-insecure会生成数据目录且root初始密码为空方便第一次登录如果用了--initialize系统会生成一串随机密码新手往往翻遍日志找不到白费时间。mysqld -install把MySQL注册成Windows服务之后就能用net start mysql启停。注意my.ini里的basedir和datadir路径必须与解压位置完全一致路径里的反斜杠要写成双反斜杠或正斜杠否则服务启动直接报找不到目录。不想在宿主机装的话用Docker跑MySQL镜像同样是成熟做法docker run -d --name mysql568 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDroot \ mysql:5.7这条命令里-p 3306:3306把容器内3306映射到宿主机的3306MYSQL_ROOT_PASSWORD指定root密码。这里最容易踩的坑是宿主机3306已经被占用docker run直接失败——对应搜索热度很高的docker安装mysql失败问题。解决方法是换宿主机端口比如-p 3307:3306但后续JDBC连接URL里的端口也要同步改成3307。3.2 建库建表SQL把第2章的设计落到MySQL命令行表结构设计好之后用一段完整SQL落地。这段脚本后面调试时会反复用到CREATE DATABASE IF NOT EXISTS ordering DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci; USE ordering; CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password CHAR(32) NOT NULL, phone VARCHAR(20), address VARCHAR(200), role TINYINT NOT NULL DEFAULT 0, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, sort TINYINT DEFAULT 0 ) ENGINEInnoDB; CREATE TABLE dishes ( id INT PRIMARY KEY AUTO_INCREMENT, category_id INT NOT NULL, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, image VARCHAR(200), description VARCHAR(500), sales INT NOT NULL DEFAULT 0, status TINYINT NOT NULL DEFAULT 1, KEY idx_category (category_id), KEY idx_sales (sales) ) ENGINEInnoDB; CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, user_id INT NOT NULL, total_price DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_user (user_id) ) ENGINEInnoDB; CREATE TABLE order_items ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, dish_id INT NOT NULL, dish_name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, quantity INT NOT NULL, KEY idx_order (order_id) ) ENGINEInnoDB;逻辑说明users表password用CHAR(32)专门存MD5。dishes的sales字段默认值为0对应mysql设置默认值为0这个常见诉求——插入菜品时不传销量就自动是0。idx_sales索引配合ORDER BY sales DESC做销量排序这是菜单页最常用的排序方式。orders表order_no建了UNIQUE索引防止并发下生成重复订单号。注意我这里刻意不建物理外键原因是课程设计阶段物理外键会带来删除菜品时的连锁报错用普通索引加应用层逻辑维护关系更灵活具体坑在第5章展开。3.3 JDBC连接三件套驱动JAR、连接URL、工具类数据库建好后Java这边第一件事是搞定JDBC。常见做法是写一个DBUtil静态工具类把连接参数放在src根目录的db.properties里。MySQL 8.0以上驱动类名是com.mysql.cj.jdbc.Driver5.x是com.mysql.jdbc.Driver用错直接ClassNotFoundException。同时驱动JAR必须复制到WEB-INF/lib目录下这是最容易被忽略的一步。db.propertiesjdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/ordering?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.passwordrootDBUtil.javaimport java.sql.*; public class DBUtil { private static String url; private static String user; private static String password; static { try { Class.forName(com.mysql.jdbc.Driver); url jdbc:mysql://localhost:3306/ordering ?useUnicodetruecharacterEncodingutf8 useSSLfalseserverTimezoneAsia/Shanghai; user root; password root; } catch (ClassNotFoundException e) { throw new RuntimeException(MySQL驱动加载失败, e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, user, password); } public static void close(Connection conn, Statement st, ResultSet rs) { try { if (rs ! null) rs.close(); } catch (SQLException ignored) {} try { if (st ! null) st.close(); } catch (SQLException ignored) {} try { if (conn ! null) conn.close(); } catch (SQLException ignored) {} } }逻辑说明静态块里Class.forName加载驱动getConnection直接返回DriverManager创建的新连接。URL上的useSSLfalse用来避开MySQL 8.0的SSL握手问题serverTimezoneAsia/Shanghai避免报服务器时区未指定的错characterEncodingutf8配合建库的utf8mb4处理中文。这个工具类虽然简单但它是后面所有DAO代码的地基。进阶做法是用C3P0或Druid连接池替换DriverManager避免每次查询都新建物理连接——课程设计阶段性能不是瓶颈但答辩时能说出连接池减少TCP握手开销、控制连接上限这句话会让老师觉得你确实考虑过生产问题。4. 核心功能实现登录、菜单列表、购物车与下单事务的代码落地4.1 用户登录与Session管理JSPServlet的MVC切分登录是系统第一道门。login.jsp提交username和password到LoginServletServlet调UserDao查库成功就把User对象放进Session失败回登录页并带错误提示。典型代码WebServlet(/login) public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String username req.getParameter(username); String password MD5Util.md5(req.getParameter(password)); User user UserDao.findByUsernameAndPassword(username, password); if (user ! null) { req.getSession().setAttribute(loginUser, user); if (user.getRole() 1) { resp.sendRedirect(admin/index.jsp); } else { resp.sendRedirect(index.jsp); } } else { req.setAttribute(error, 用户名或密码错误); req.getRequestDispatcher(login.jsp).forward(req, resp); } } }逻辑说明密码先走一次MD5再传参而不是把明文交给DAO这样即使数据库被拖库攻击者拿到的也只是哈希。findByUsernameAndPassword内部用PreparedStatement绑定参数防止SQL注入——这是JavaWeb开发的安全底线。登录成功后的跳转按role分流1是管理员进后台0是普通用户回前台。页面里${sessionScope.loginUser.username}就能显示当前登录人的昵称这就是jsp个人信息展示页面的标准做法。Session的边界要清楚它存在服务器内存里默认30分钟过期。浏览器关闭Session不一定会立刻失效Tomcat没重启的话下次打开浏览器带着同一个JSESSIONID Cookie依然能取到Session。课程设计里很多人以为关浏览器等于退出登录这是误解要实现真正的退出必须调用session.invalidate()。4.2 菜单展示与销量排序JSTLEL让JSP页面保持干净菜单页面的数据渲染我强烈建议用JSTL标签和EL表达式而不要在JSP里写% for (...) %。JSP里混Java代码的问题在于页面和后端逻辑揉成一锅粥改个样式都可能出错。Servlet查出List 后转发给menu.jsp% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % div classdish-grid c:forEach items${dishList} vardish div classdish-card img classdish-img src${pageContext.request.contextPath}/upload/${dish.image} alt${dish.name} / h3${dish.name}/h3 p classpricefmt:formatNumber value${dish.price} pattern0.00//p p classsales已售 c:out value${dish.sales}//p a href${pageContext.request.contextPath}/cart?actionadddishId${dish.id}加入购物车/a /div /c:forEach /div逻辑说明${dishList}来自Servlet的req.setAttribute(dishList, list)。c:forEach遍历菜品卡片fmt:formatNumber把价格格式化成两位小数避免出现12.5这种不规整金额。图片路径用${pageContext.request.contextPath}拼出项目根路径这是解决jsp图片路径问题的最稳妥方式——很多项目部署到Tomcat后因为项目名不是ROOT写死的/upload/xxx.jpg全部失效。图片定位和尺寸控制放在CSS里完成.dish-img { width: 100%; height: 180px; object-fit: cover; }object-fit: cover让图片按容器比例居中裁剪而不是被拉伸变形这是jsp图片如何对坐标定位这个问题在现代CSS里的答案——不是用绝对坐标去抠图而是让容器帮你裁。菜单排序是常见需求。DishDao的list方法支持两个参数categoryId和orderBySQL写成SELECT * FROM dishes WHERE status 1 AND (category_id ? OR ? 0) ORDER BY CASE WHEN ? sales THEN sales END DESC, CASE WHEN ? price THEN price END ASC;逻辑说明categoryId传0表示全部分类用? 0这个技巧绕开参数为空的判断比拼接动态WHERE干净。ORDER BY用CASE WHEN实现动态排序避免字符串拼接SQL带来的注入风险。前台默认按销量倒序这就是mysql排序在实际业务里的用法——数据库帮你排好Java这边只管遍历展示。4.3 购物车与下单事务多条SQL必须同时成功或同时回滚购物车在课程设计里最常用的是Session。购物车对象可以是MapInteger, Integerkey是dishIdvalue是数量。加购、改数量、删商品都在Session里操作不发数据库请求。用户结算时把Map里的数据一次性写入订单表和明细表这是整个系统里对mysql事务处理要求最高的代码public int createOrder(User user, MapInteger, Integer cart) { String insertOrder INSERT INTO orders(order_no, user_id, total_price, status) VALUES(?,?,?,0); String insertItem INSERT INTO order_items(order_id, dish_id, dish_name, price, quantity) VALUES(?,?,?,?,?); String updateSales UPDATE dishes SET sales sales ? WHERE id ?; Connection conn null; PreparedStatement ps null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 BigDecimal total BigDecimal.ZERO; for (Map.EntryInteger, Integer entry : cart.entrySet()) { Dish dish DishDao.findById(entry.getKey()); total total.add(dish.getPrice() .multiply(BigDecimal.valueOf(entry.getValue()))); } ps conn.prepareStatement(insertOrder, Statement.RETURN_GENERATED_KEYS); ps.setString(1, genOrderNo()); ps.setInt(2, user.getId()); ps.setBigDecimal(3, total); ps.executeUpdate(); ResultSet keys ps.getGeneratedKeys(); int orderId 0; if (keys.next()) orderId keys.getInt(1); ps conn.prepareStatement(insertItem); for (Map.EntryInteger, Integer entry : cart.entrySet()) { Dish dish DishDao.findById(entry.getKey()); ps.setInt(1, orderId); ps.setInt(2, dish.getId()); ps.setString(3, dish.getName()); ps.setBigDecimal(4, dish.getPrice()); ps.setInt(5, entry.getValue()); ps.addBatch(); } ps.executeBatch(); ps conn.prepareStatement(updateSales); for (Map.EntryInteger, Integer entry : cart.entrySet()) { ps.setInt(1, entry.getValue()); ps.setInt(2, entry.getKey()); ps.addBatch(); } ps.executeBatch(); conn.commit(); // 全部成功才提交 return orderId; } catch (Exception e) { try { conn.rollback(); } catch (SQLException ignored) {} throw new RuntimeException(下单失败, e); } finally { try { conn.setAutoCommit(true); } catch (SQLException ignored) {} DBUtil.close(conn, ps, null); } }逻辑说明setAutoCommit(false)之后所有SQL都在同一个事务里commit才真正落库。任何一步抛异常rollback会把已插入的订单和明细全部撤销避免出现订单主表多了一条记录但明细是空的这种脏数据。订单明细里冗余了dish_name和price是因为菜品可能改名、改价而历史订单必须保持原样。updateSales用sales sales ?而非直接覆盖配合InnoDB行锁并发下单时销量不会互相覆盖。这是mysql锁和mysql事务处理在实战中最典型的一个落点。4.4 后台管理权限一个Filter挡住未登录的管理员后台页面如果只靠菜单里不放链接来隐藏就是个纸糊的门——用户直接输入/admin/dishes.jsp照样能访问。正确做法是加一个Filter拦截所有/admin/*请求WebFilter(/admin/*) public class AdminFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; Object user request.getSession().getAttribute(loginUser); if (user null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } User u (User) user; if (u.getRole() ! 1) { response.sendError(HttpServletResponse.SC_FORBIDDEN); return; } chain.doFilter(req, resp); } }逻辑说明doFilter里先检查Session里有没有loginUser没有就踢回登录页有但role不是1就返回403。这样即使知道后台地址也进不去。菜品管理的增删改查就是普通表单加DAO层。注意删除菜品时要做逻辑删除把status置0而不是物理DELETE因为order_items表里还留存着这个菜品的引用物理删会触发外键或产生孤儿数据。这正是第5章要展开的重点坑。5. 避坑手册网上订餐系统从开发到部署最常见的6个现场事故5.1 连接数据库报错驱动类SSL与时区三连击现象Tomcat一启动访问登录页报ClassNotFoundException: com.mysql.jdbc.Driver或者更隐蔽的Unable to load authentication plugin caching_sha2_password、SSL connection error。这三类报错占连库失败问题的八成以上。原因驱动JAR没放进WEB-INF/lib驱动类名与MySQL版本不匹配——MySQL 8.0把驱动类改成了com.mysql.cj.jdbc.Driver老代码里写com.mysql.jdbc.Driver虽然在8.0驱动里也能触发加载但会告警8.0默认认证插件是caching_sha2_password驱动版本太旧握手协议对不上就报SSL连接错误。解决把Connector/J的JAR复制到WEB-INF/lib目录驱动类名改成com.mysql.cj.jdbc.DriverURL加上useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue。这三个参数就是MySQL 8.0连不上时的后悔药按这个顺序检查基本都能救回来。5.2 中文乱码不是一处问题是四层链路全部要检查现象JSP页面上中文显示正常但提交表单后数据库里存进去的是或者页面直接显示乱码刷新一次又好了。原因字符编码要过四道关——JSP文件自身的编码、HTTP请求体编码、JDBC URL里的characterEncoding、MySQL表字段的字符集。任何一层断了中文就变乱码。最常见的坑是忘了在Servlet里写req.setCharacterEncoding(UTF-8)导致POST请求的参数按ISO-8859-1被解析。解决JSP页面顶部的pageEncodingUTF-8不要省写一个统一的EncodingFilter在doFilter里对request和response都setCharacterEncoding(UTF-8)JDBC URL加characterEncodingutf8建库建表就用utf8mb4。四层对齐乱码问题绝迹。5.3 mysql服务无法启动先看日志别靠玄学现象net start mysql提示服务无法启动或者服务显示正在运行但马上又自动停止事件查看器里只有一句服务特定错误没有细节。原因my.ini里basedir或datadir路径写错、data目录损坏、3306端口被占用。常见的是data目录不是由mysqld初始化出来的而是从别处拷来的权限和文件都对不上。解决用命令行前台启动看真实报错——先net stop mysql停掉服务再执行mysqld --console错误会直接打到屏幕上而不是吞进日志。路径问题修正my.ini里的斜杠端口占用改my.ini的port为3307连接URL同步改data目录损坏就备份后删掉重新执行mysqld --initialize-insecure。mysql的报错不会无缘无故出现错误日志写在data目录下以.err结尾的文件里打开看最后几行比到处搜热词快得多。5.4 购物车串台Session的作用域没搞清现象同一台电脑上A用户的购物车里出现了B用户加的菜或者用户下完单再开一个新标签页购物车居然还是满的。原因同一浏览器的不同标签页共享同一份CookieSession自然也是同一个。购物车放在Session里意味着它属于浏览器会话而不属于用户——用户没登录也能加购物车换浏览器再登录购物车就丢了。下单后不清空cart属性刷新页面旧商品还在这是另一个被忽略的细节。解决课程设计里把购物车放Session是常规操作但要明确它的边界是单浏览器演示。如果确实要做到一个账号的购物车处处可见就需要设计cart表把购物车持久化到MySQL用user_id关联。另外用户下单成功后记得session.removeAttribute(cart)清空购物车。5.5 删除菜品被外键拦住物理删除不如逻辑删除现象后台想删掉一个已下架的菜品SQL执行报错Cannot delete or update a parent row: a foreign key constraint fails或者虽然没建外键翻看历史订单时明细里的菜名变成了null。原因order_items表里还留存着引用这个菜品的历史记录。物理外键会直接拦截删除没有物理外键但应用层做了级联删除或置空同样会污染历史数据。这是当初没想清楚订单要留痕导致的连锁反应。解决最稳的办法是设计表时就不建物理外键用普通索引加应用层维护关系删除菜品做逻辑删除把dishes表的status从1改成0前台查询天然过滤掉下架菜品历史订单不受影响。这也是第2章dishes表留status字段的原因——凡是会被业务引用的主数据一律不物理删除只做置位失效。5.6 菜品图片不显示或变形路径和CSS定位问题现象图片在本地运行正常部署到Tomcat后全部裂图或者图片显示了但被压成椭圆。原因写死路径是元凶。如果项目部署名不是ROOT/upload/xxx.jpg实际会指向Tomcat根下的upload目录而不是项目自己的upload目录。另外图片尺寸不一只用width和height硬撑比例就被破坏。解决图片src统一用${pageContext.request.contextPath}/upload/${dish.image}动态拼路径。图片定位交给CSS容器固定宽高比加object-fit: cover浏览器自动居中裁剪成目标比例。这是jsp图片如何对坐标定位的标准解法——不是你去找图片坐标而是让容器帮你裁。6. 让系统从能跑到扛打SQL注入防护与答辩前的三个验证动作最后说两件让系统脱胎换骨的事安全习惯和验证流程。先说SQL注入。登录模块如果图省事写成字符串拼接String sql SELECT * FROM users WHERE username username AND password password ;那用户在用户名框里输入 OR 11SQL就变成了恒等式直接以管理员身份登进去。改成PreparedStatement的?占位符绑定参数驱动会帮你转义这是成本最低、效果最可靠的防护。翻开源码搜索Statement.createStatement只要在拼SQL的地方就是隐患答辩前务必全部换掉。我习惯在答辩前做三个验证动作。第一删库重跑一遍初始化脚本把第3.2节的SQL在Navicat里重跑再登录、下单、查订单确认全流程在零数据状态下依然正常——这能揪出那些依赖已有数据才能跑通的隐蔽空指针。第二跨浏览器测登录和购物车Chrome登录加购下单再开Edge登录同一账号确认两者数据互不干扰Session行为符合预期。第三用Navicat直接翻orders和order_items表核对下单后的total_price是否等于每个明细单价乘数量之和——这是事务回滚和金额计算的最后一道审计。部署到Tomcat时把项目打成WAR包丢进webapps目录一两秒后看logs/catalina.out出现Deployment of web application archive ... has finished in ... ms就算成功。当年我第一次部署折腾到深夜结局是驱动JAR没打进去WAR包一启动就ClassNotFound。从那以后我养成了一个习惯打完WAR先执行unzip -l xxx.war | grep mysql-connector确认驱动在里面再启动部署。这套流程跑顺了你手里的订餐系统就不只是能跑而是经得起别人上手点一轮、翻一轮代码的能交付。希望帮到你。本文还有配套的精品资源点击获取
返回列表