ARTICLE DETAIL

资讯详情

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

SSM框架餐饮订餐系统实战:从配置到部署全流程解析

SSM框架餐饮订餐系统实战:从配置到部署全流程解析 简介基于SSM框架的餐饮订餐管理系统设计源码面向Java Web方向的初学者、课程设计及毕业设计者能够帮助理解SSM三大框架的整合方式、分层开发思想以及餐饮业务中常见的订餐管理流程。压缩包内共365个文件以157个GIF演示图和59个JPG效果图直观展示系统页面与操作过程另含39个Java源码类、31个JSP页面、36个CSS样式、21个JavaScript脚本以及XML配置、db数据库文件和properties配置文件等包体仅6.74MB目录结构规范便于按模块查阅与二次开发。系统覆盖菜品管理、订单管理、公告管理、类别管理和销售统计等核心功能界面简洁直观、操作流畅完整呈现了用户端点餐与后台管理端的数据交互链。已有327人学习下载适合需要借用典型业务场景梳理SSM项目脉络或快速搭建订餐系统原型的开发者参考。1. SSM框架下餐饮订餐系统的正确打开方式接手或学习一个“基于SSM框架的餐饮订餐管理系统”最怕的是把它当成又一个增删改查 Demo。这个标题真正值钱的地方在于它把三件事揉在了一起Spring 的容器管理、SpringMVC 的请求流转、MyBatis 的持久层映射外加餐饮业务里最典型的“库存-订单-支付-权限”状态机。你能从这个项目里拿走的不只是几个 CRUD 页面而是一套 Java Web 服务端开发的标准骨架。适合正在准备毕设、刚入职要接手 SSM 老项目、或者想把自己代码从 Servlet 提升到框架层面的人。文中涉及源码结构、核心配置、业务实现和部署排错跟着走一遍你就能搞清楚 SSM 三兄弟在项目里究竟各管哪一段。2. SSM框架三大组件的职责边界与整合原理2.1 为什么是 SSM 而不是 Spring Boot先把一个观念摆正SSM 并不是被淘汰的技术它在存量企业项目和教学场景里依然占着相当比例。Spring Boot 的自动配置确实能减少 XML 配置但 SSM 让你必须手动装配每一个 Bean而这个“手动”的过程恰恰是理解 IoC 和 AOP 的最佳路径。餐饮订餐系统的业务复杂度不高不低正好适合用 SSM 把各层边界撕开看清楚。Spring 负责的是对象生命周期。你会在applicationContext.xml里定义数据源、事务管理器、MapperScannerConfigurer让 Service 层和 Mapper 接口能被自动代理注入。SpringMVC 是 Web 层的入口它接管所有/路径的请求通过 DispatcherServlet 把 URL 映射到 Controller 方法上。MyBatis 则处于最底层的数据访问位置它把 SQL 语句和 Java 方法绑定让selectByPrimaryKey这样的方法背后对应一条真实可调的 SQL。在整合时有个容易忽视的点Spring 容器和 SpringMVC 容器是父子关系。SpringMVC 的子容器只扫描Controller注解父容器扫描 Service、Mapper、工具类。如果配置错误比如在子容器里也扫描了 Service会导致事务失效因为 AOP 代理没有生效。整合时你会在spring-mvc.xml中设置context:component-scan base-packagecom.restaurant.controller/在applicationContext.xml中设置扫描com.restaurant.service和com.restaurant.dao。2.2 三层架构与包结构设计订餐系统的包结构直接反映职责边界我见过太多把所有类塞进一个包的项目结果后期改一个需求要动四个文件。常见做法是按controller、service、dao、entity、common分层。com.restaurant ├── controller # SpringMVC 控制层 │ ├── UserController.java │ ├── OrderController.java │ └── AdminController.java ├── service # 业务逻辑层 │ ├── OrderService.java │ └── impl/OrderServiceImpl.java ├── dao # MyBatis Mapper 接口层 │ ├── OrderMapper.java │ └── DishMapper.java ├── entity # 数据库实体类 │ ├── Order.java │ └── Dish.java └── common # 公共工具类、常量、统一返回体 ├── Result.java └── PageBean.javaentity 包里的类与数据库表字段一一对应属性类型要注意数据库decimal对应 JavaBigDecimaldatetime对应java.util.Date或LocalDateTime。如果使用 MySQL 8 以上版本建议使用LocalDateTime配合 MyBatis 的jdbcTypeTIMESTAMP可以避免时区问题。Controller 只负责参数接收和视图转发不写业务逻辑Service 层做事务控制和业务判断Dao 层一个方法对应一条 SQL不做跨表拼接。这套规矩是所有 SSM 项目的地基先认同再谈优化。2.3 核心配置文件的完整清单SSM 整合最少需要四个配置文件缺失任何一个都会在启动时报出让人摸不着头脑的异常。web.xml是 Web 容器的入口它配置 ContextLoaderListener 来加载 Spring 根容器同时配置 DispatcherServlet 来启动 SpringMVC。applicationContext.xml是 Spring 的根配置包含数据源、SqlSessionFactory、事务管理器。spring-mvc.xml负责开启注解驱动、配置视图解析器、静态资源放行。jdbc.properties存放数据库连接参数。下面是一个能跑通的applicationContext.xml骨架注意注释里的坑点。context:property-placeholder locationclasspath:jdbc.properties/ bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName value${jdbc.driver}/ property nameurl value${jdbc.url}/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ !-- 实体类别名Mapper XML 中可直接写 Dish 而不必写全限定名 -- property nametypeAliasesPackage valuecom.restaurant.entity/ !-- Mapper XML 文件位置 -- property namemapperLocations valueclasspath:mapper/*.xml/ !-- 下划线转驼峰数据库 dish_name 自动映射 dishName -- property nameconfiguration bean classorg.apache.ibatis.session.Configuration property namemapUnderscoreToCamelCase valuetrue/ /bean /property /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer !-- Dao 接口所在包扫描后可直接注入 Mapper -- property namebasePackage valuecom.restaurant.dao/ /bean bean idtransactionManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean tx:annotation-driven transaction-managertransactionManager/逻辑说明typeAliasesPackage简化了 Mapper XML 中parameterType和resultType的书写mapUnderscoreToCamelCase是必须开启的选项否则你就要在 resultMap 里手写每一列映射MapperScannerConfigurer让所有 Dao 接口自动生成代理实现不需要你写实现类。事务配置里Transactional注解必须放在 Service 实现类上不要放在接口上否则使用 JDK 动态代理时会失效。3. 餐饮订餐系统从建表到菜品管理的完整实现3.1 数据库设计五张核心表订餐系统的数据库设计要能支撑“用户点餐-生成订单-商家处理-库存扣减”这条完整链路。最少需要五张表用户表、菜品分类表、菜品表、订单表、订单明细表。订单表与订单明细表是一对多关系这个拆分是必须的因为一个订单可能包含多份菜品每份菜品的数量、单价都要独立记录否则后期统计营业额会非常痛苦。建表 SQL 中需要注意字段类型的选择。价格字段使用DECIMAL(10,2)不要使用FLOAT浮点类型在金额计算时会产生精度丢失这在餐饮结算场景下是致命的。状态字段使用TINYINT并加注释0 表示待支付1 表示已支付2 表示制作中3 表示已完成4 表示已取消。时间字段统一DATETIME并设置DEFAULT CURRENT_TIMESTAMP让数据库自己记录创建时间。CREATE TABLE tb_dish ( id INT NOT NULL AUTO_INCREMENT COMMENT 菜品ID, category_id INT NOT NULL COMMENT 所属分类ID, name VARCHAR(50) NOT NULL COMMENT 菜品名称, price DECIMAL(10,2) NOT NULL COMMENT 单价, image VARCHAR(255) DEFAULT NULL COMMENT 图片路径, stock INT NOT NULL DEFAULT 0 COMMENT 当日库存, status TINYINT NOT NULL DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_category (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT菜品表; CREATE TABLE tb_order ( id INT NOT NULL AUTO_INCREMENT COMMENT 订单ID, order_no VARCHAR(32) NOT NULL COMMENT 订单编号, user_id INT NOT NULL COMMENT 下单用户ID, total_amount DECIMAL(10,2) NOT NULL COMMENT 订单总金额, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2制作中 3已完成 4已取消, remark VARCHAR(255) DEFAULT NULL COMMENT 备注, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表;逻辑说明order_no设置唯一索引在并发下单时能防止重复订单号外键在这里有意不建而是在 Service 层做逻辑关联原因是在高并发插入时外键会带来额外的锁开销而且餐饮系统业务相对简单逻辑外键足够。菜品表加category_id索引是因为点餐首页要按分类展示全表扫描会随菜品数量增长而变慢。3.2 MyBatis Mapper 与动态 SQL 实战MyBatis 的优势在动态 SQL这是订餐系统查询条件多变时的解法。菜品列表可能需要按名称模糊搜索、按分类过滤、按价格区间查询三个条件组合起来如果写死 SQL你得拼出几十种可能。动态 SQL 用where标签配合if就轻松解决。下面是一个菜品分页查询的 Mapper XML注意parameterType使用 Map 传参因为查询参数超过一个时 Map 是最灵活的方式。select idselectDishPage parameterTypemap resultTypecom.restaurant.entity.Dish SELECT id, category_id, name, price, image, stock, status FROM tb_dish where if testname ! null and name ! AND name LIKE CONCAT(%, #{name}, %) /if if testcategoryId ! null AND category_id #{categoryId} /if if testminPrice ! null AND price gt; #{minPrice} /if if teststatus ! null AND status #{status} /if /where ORDER BY id DESC LIMIT #{offset}, #{pageSize} /select逻辑说明where标签会自动去掉第一个if前面的AND这是 MyBatis 的智能处理你不需要在AND后面加空格担心拼接问题。CONCAT拼接模糊查询比直接在 Java 层拼好%关键字%更安全因为#{}预编译会处理特殊字符。gt;是 XML 中大于等于号的转义写法直接写会导致 XML 解析报错。offset和pageSize的乘法计算最好在 Java 层完成不要在 SQL 里做算术。对应 Mapper 接口方法名必须和 XML 中id一致参数用Param注解绑定。public interface DishMapper { ListDish selectDishPage(Param(name) String name, Param(categoryId) Integer categoryId, Param(minPrice) BigDecimal minPrice, Param(status) Integer status, Param(offset) int offset, Param(pageSize) int pageSize); }3.3 菜品分类与图片上传的 Controller 实现Controller 层要处理表单提交和文件上传两种请求。菜品新增时你需要接收菜品基本信息加一张图片文件。SpringMVC 的MultipartFile类型可以直接接收input typefile上传的内容但要在配置中注册CommonsMultipartResolver。Controller RequestMapping(/admin/dish) public class DishController { Autowired private DishService dishService; PostMapping(/add) public String addDish(Dish dish, RequestParam(file) MultipartFile file, HttpServletRequest request) { if (!file.isEmpty()) { // 原始文件名可能包含路径信息必须截取最后一个斜杠后的部分 String originalFilename file.getOriginalFilename(); String suffix originalFilename.substring(originalFilename.lastIndexOf(.)); // 使用时间戳加随机数生成文件名避免中文名乱码和重名覆盖 String newFileName System.currentTimeMillis() _ new Random().nextInt(1000) suffix; String realPath request.getServletContext() .getRealPath(/upload/dish/); File dir new File(realPath); if (!dir.exists()) { dir.mkdirs(); } try { file.transferTo(new File(realPath newFileName)); } catch (IOException e) { e.printStackTrace(); } dish.setImage(/upload/dish/ newFileName); } dishService.addDish(dish); // 使用重定向避免表单重复提交 return redirect:/admin/dish/list; } }逻辑说明transferTo是 MultipartFile 提供的直接落盘方法它内部处理了临时文件的清理。文件名用时间戳_随机数拼后缀是因为直接用用户上传的原始文件名容易遇到中文乱码和路径注入问题。getRealPath获取的是当前 Web 应用部署目录下的物理路径在 IDE 中运行和在 Tomcat 中部署这个路径的根目录会不同。重定向返回而不是 forward 转发是为了遵守 PRG 模式Post-Redirect-Get防止刷新浏览器时重复提交订单或菜品。4. 订单核心流程与购物车会话管理4.1 购物车用 Session 还是 Redis餐饮点餐系统里购物车的数据量小、生命周期短常见做法是保存在 Session 中。Session 的键是用户登录后的用户 ID值是MapInteger, Integer菜品 ID 对应数量。使用 Session 不需要额外引入缓存组件重启系统数据自然清空这也符合“购物车是临时选择清单”的语义用户下次登录需要重新点餐。订单表生成时需要把 Session 中的购物车数据读取出来计算出总金额并写入订单明细表。这里有一个必须做的校验菜品当前价格和加入购物车时的价格可能不一致。餐饮行业中菜品调价是常态用户看到的价格是加入时的价格但结算时如果直接读取数据库的最新价会产生“用户以为 20 元结账变 25 元”的纠纷。正确的做法是在加入购物车时就把单价存入结算时用购物车内的单价乘以数量。public Order createOrder(Integer userId, MapInteger, Integer cart) { // 生成订单号时间戳 用户ID 三位随机数保证高并发下不重复 String orderNo System.currentTimeMillis() userId (int)((Math.random() * 9 1) * 100); Order order new Order(); order.setOrderNo(orderNo); order.setUserId(userId); order.setStatus(0); // 待支付 BigDecimal totalAmount BigDecimal.ZERO; ListOrderItem itemList new ArrayList(); for (Map.EntryInteger, Integer entry : cart.entrySet()) { Integer dishId entry.getKey(); Integer quantity entry.getValue(); Dish dish dishMapper.selectByPrimaryKey(dishId); // 校验菜品是否下架 if (dish null || dish.getStatus() 0) { throw new RuntimeException(菜品已下架 dishId); } // 校验库存是否充足 if (dish.getStock() quantity) { throw new RuntimeException(库存不足 dish.getName()); } OrderItem item new OrderItem(); item.setDishId(dishId); item.setDishName(dish.getName()); item.setPrice(dish.getPrice()); item.setQuantity(quantity); item.setSubtotal(dish.getPrice().multiply(new BigDecimal(quantity))); totalAmount totalAmount.add(item.getSubtotal()); itemList.add(item); } order.setTotalAmount(totalAmount); orderMapper.insert(order); orderItemMapper.batchInsert(itemList); return order; }逻辑说明BigDecimal的multiply和add生成新对象不能直接赋值给原引用做累加以外的操作因为BigDecimal是不可变类。订单号拼接方案中Math.random()生成的是[0,1)区间的小数乘以 9 加 1 后取整能得到 100 到 999 的随机数字符串拼接时使用 是为了强制转换为字符串否则整数之间会发生加法运算而不是拼接。batchInsert是 MyBatis 的批量插入需要接收一个 List 参数在 XML 中使用foreach标签遍历生成多组 VALUES。4.2 Service 层事务与库存扣减的原子性订单生成时涉及三个写操作插入订单表、批量插入订单明细表、扣减菜品库存。这三个操作必须在一个事务中完成任何一步失败都要全部回滚。在OrderServiceImpl的createOrder方法上加Transactional(rollbackFor Exception.class)同时注意事务的传播行为是 REQUIRED默认加入当前事务。库存扣减必须使用乐观锁或悲观锁思路否则并发场景下超卖是必然的。最简单可靠的是乐观锁方案UPDATE tb_dish SET stock stock - #{quantity} WHERE id #{dishId} AND stock #{quantity}这个 SQL 利用数据库行锁保证原子性返回影响行数为 0 时说明库存不足。Transactional(rollbackFor Exception.class) public boolean payOrder(Integer orderId) { Order order orderMapper.selectByPrimaryKey(orderId); if (order.getStatus() ! 0) { throw new RuntimeException(订单状态异常无法支付); } // 扣减库存update 返回影响行数 int rows dishMapper.deductStock(orderId); if (rows 0) { throw new RuntimeException(库存不足支付失败); } orderMapper.updateStatus(orderId, 1); return true; }逻辑说明deductStock的 SQL 是UPDATE tb_dish SET stock stock - #{quantity} WHERE id #{dishId} AND stock #{quantity}。这里不用SELECT ... FOR UPDATE因为悲观锁会在事务结束前一直持有行锁如果支付流程里有其他耗时操作比如调用第三方支付接口锁等待时间会飙升。乐观锁的判断在 UPDATE 语句中完成失败时直接抛异常触发事务回滚订单状态和库存都不能更新成功。注意Transactional默认只回滚RuntimeException如果需要处理受检异常则必须指定rollbackFor。4.3 订单状态机与后端校验餐饮订餐的订单状态流转有严格方向约束不能从“已取消”直接跳到“已完成”也不能从“待支付”直接跳到“制作中”。后端必须做状态校验不能信任前端传入的目标状态。常见做法是定义一个状态流转 Map 或者使用 Switch 判断。当前状态允许流转到触发动作0 待支付1 已支付 / 4 已取消用户支付 / 用户取消1 已支付2 制作中 / 4 已取消商家接单 / 商家取消2 制作中3 已完成商家出餐3 已完成无终态4 已取消无终态状态变更的在OrderService.updateStatus(Integer orderId, Integer targetStatus)中实现先查出当前状态再判断目标状态是否在许可列表中不符合则抛出业务异常。菜品库存是当日制每晚定时任务将库存恢复为初始配置这样才能配合上架下架功能实现“每日限量”的营销玩法。5. 用户权限控制与拦截器实现细节5.1 登录态的管理方式订餐系统有普通用户和管理员两类角色Controller 中大量接口需要区分权限。Servlet 规范中的HttpSession是存储登录态的传统方案用户登录成功后session.setAttribute(loginUser, user)后续请求从 Session 中取出用户对象判断是否为空。这个方案实现简单单机部署完全没有问题但要注意 Session 默认过期时间是 30 分钟用户在点餐页面停留过久后提交订单会报未登录。更规范的替代方案是使用拦截器统一校验下面是一个用于管理员接口的拦截器它拦截/admin/**路径下的所有请求。public class AdminInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录请求本身避免死循环 if (request.getRequestURI().contains(/admin/login)) { return true; } Object admin request.getSession().getAttribute(adminUser); if (admin null) { // AJAX 请求返回 JSON 状态码页面跳转返回重定向 String requestedWith request.getHeader(X-Requested-With); if (XMLHttpRequest.equals(requestedWith)) { response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\msg\:\未登录\}); } else { response.sendRedirect(request.getContextPath() /admin/login); } return false; } // 将用户信息放入 request后续 Controller 直接获取 request.setAttribute(adminUser, admin); return true; } }逻辑说明preHandle返回值决定请求是否继续执行true放行false拦截并直接响应。区分 AJAX 请求和普通页面跳转是实际开发中的关键细节如果拦截器对所有请求都做sendRedirect前端 jQuery 的$.post拿到的是登录页 HTML 而不是 JSON解析会报错。X-Requested-With头是 jQuery 等库发起 AJAX 请求时自动加入的原生fetch不会携带前端要手动设置。handler参数可以用来判断是否HandlerMethod类型从而排除静态资源请求。5.2 拦截器在 spring-mvc.xml 中的注册顺序拦截器注册顺序影响执行顺序多个拦截器形成调用链时第一个注册的最先执行preHandle但postHandle和afterCompletion的执行顺序是反过来的。管理系统常见的拦截器链是登录拦截器前置日志记录拦截器前置两者顺序不能颠倒否则未登录的请求会先打印日志再被拦截器拦下日志中记录的访问信息就不完整。mvc:interceptors mvc:interceptor mvc:mapping path/admin/**/ bean classcom.restaurant.interceptor.AdminInterceptor/ /mvc:interceptor mvc:interceptor mvc:mapping path/**/ exclude-mapping path/static/**/ exclude-mapping path/user/login/ bean classcom.restaurant.interceptor.AccessLogInterceptor/ /mvc:interceptor /mvc:interceptors逻辑说明mvc:mapping指定拦截的 URL 模式/**表示所有请求/admin/**表示管理员模块。exclude-mapping用于放行静态资源和公开接口必须把/static/**排除否则 CSS、JS 文件全部被拦截后页面会变成纯文本排版。拦截器 Bean 不需要注释扫描直接在这里声明实例即可如果需要注入 Service可以在这里添加property注入或者交给 Spring 容器管理后使用ref引用。注意拦截器只对经过 DispatcherServlet 的请求生效web.xml中配置的 Servlet 路径如果不是/而是*.do拦截路径也要相应调整。5.3 用户端与管理员端的功能矩阵权限控制不只在拦截器层菜单和按钮的显示也需要根据当前用户角色做隐藏。普通用户端显示菜品浏览、购物车、我的订单、个人中心管理员端显示菜品管理、分类管理、订单处理、统计报表。这个功能的常见实现是使用 JSP 标签库中的c:if判断或在后端把菜单权限列表写入 Session前端遍历输出。用户端 Controller 中的方法都要显式获取当前用户 ID不能从表单参数里取防止用户通过改请求参数操作他人订单。从 Session 中拿用户对象然后getId()订单查询全部带user_id条件这样即使有人猜测订单号也不能看到别人的订单详情。这是权限校验的底线逻辑只做登录拦截不做数据归属校验系统等于裸奔。6. 部署到 Tomcat 的路径问题与性能调优参数SSM 项目打包成 WAR 才能部署到 Tomcat这里最容易出错的是上传图片的路径。在 IntelliJ IDEA 中运行项目时getRealPath返回的是 target 目录下的临时路径重启后文件会被清理。生产环境要单独配置一个静态资源映射路径常见做法是在 spring-mvc.xml 中配置虚拟目录映射到服务器本地磁盘但通过 Tomcat 的docBase配置虚拟主机可以达到同样的效果。Context 配置中docBase/data/upload与 Web 应用路径/upload/**建立映射不要把文件直接写入应用目录内原因有两个WAR 包重部署时会清空整个应用目录Tomcat 运行账号对 webapps 目录通常没有写权限。JVM 启动参数在catalina.sh中调整如果订餐高峰期用户量超过一百人-Xms512m -Xmx1024m是底线配置。使用JAVA_OPTS-Xms512m -Xmx1024m -XX:UseG1GCG1 回收器适合餐饮系统这种中等对象分配频率的场景。数据库连接池使用 Druid 时initialSize5、minIdle5、maxActive20是单机 Tomcat 的常用配置。超过maxWait60000毫秒拿不到连接就报错这个值在点餐高峰期要调大否则库存扣减和订单生成会频繁抛异常。SQL 层面show full processlist查看慢查询优先为tb_order的user_id和status加联合索引因为用户查看订单列表是高频操作。最后说一个排错技巧SSM 项目启动报 Bean 找不到或注入失败时先检查applicationContext.xml的context:component-scan有没有把 Service 实现类所在的包完整覆盖再检查 Mapper XML 的namespace是否和接口全限定名一致这两处是 SSM 整合中最高频的配置失误点。调试时在log4j.properties中开启org.mybatis的日志级别为 DEBUG能看到每条 SQL 的完整执行参数定位问题是「SQL 写错」还是「参数传错」避免对着代码空想。本文还有配套的精品资源点击获取
返回列表