
简介这是一份基于SSM框架的餐饮管理系统设计与实现文档主要面向正在学习Java Web开发的初学者、准备毕业设计的高校学生以及需要参考餐饮信息化管理方案的项目人员。文档以餐饮行业的实际管理需求为出发点针对传统人工管理效率低、数据易出错等问题设计了包含系统用户管理、菜品类别管理、餐桌信息管理、菜品信息管理、登录模块、退出模块在内的完整系统。实现过程采用B/S架构和MVC三层设计模式以Java作为主要开发语言配合Eclipse集成开发环境和MySQL关系型数据库覆盖了从需求分析、模块设计、编码实现到系统测试的各个环节体现了高内聚、低耦合的开发思想。资源包内为1个docx文件大小945KB文档包含中英文摘要、目录与完整正文结构规范既可用于毕业设计论文写作参考也可作为餐饮管理系统课程设计或SSM框架项目实践的蓝本。目前已有35人学习下载适合需要快速梳理系统设计思路、理解SSM项目结构并完成配套设计文档的读者。1. 从毕业设计到可运行的系统拆一份基于 SSM 的餐饮管理源码拿到一份标题写着“基于 ssm 的餐饮管理系统设计与实现.docx”的毕业设计材料第一反应通常是找代码压缩包但真正值得看的是它背后的工程决策为什么在 Spring Boot 已经普及的今天这套系统依然选择 SSM 三件套答案在于 MVC 分层思想在单体应用里的完整落地以及 MyBatis 对复杂 SQL 的高度可控性。这份文档描述的餐饮管理系统覆盖了用户、菜品类别、餐桌、菜品信息、购买记录等核心业务采用 B/S 结构与 Eclipse MySQL 开发环境是典型的 Java Web 入门到进阶的完整闭环。对刚接触框架整合的开发者来说它的价值在于能看清 JSP、Controller、Service、Mapper 之间一条完整的请求链路对工作三五年的工程师来说也能从状态流转、事务边界和数据一致性这些角度重新审视一份看似简单的管理系统里藏着的设计取舍。下面按从选型到实现的顺序把这套系统的结构和关键代码拆开讲透。2. 技术栈拆解SSM 的分工边界与 MVC 三层在 B/S 架构中的落地方式2.1 SSM 三件套各自管什么Spring、SpringMVC、MyBatis 的职责边界SSM 是 Spring、SpringMVC、MyBatis 的组合框架但很多初学者把它当成一个整体来用出了问题不知道去哪一层排查。实际上三者的边界非常清晰Spring 是容器和管理者负责对象的创建、依赖注入和事务管理SpringMVC 是表现层框架负责接收 HTTP 请求、调用业务逻辑并返回视图MyBatis 是持久层框架负责把 Java 方法与 SQL 语句映射起来处理数据库的读写。以餐饮管理系统里的“菜品信息管理”为例一次查询请求会这样流转浏览器发送请求到 SpringMVC 的 DispatcherServlet它根据 URL 找到对应的 ControllerController 调用 Service 接口Service 实现类里通过 Spring 注入的 Mapper 接口去操作数据库MyBatis 再把结果集映射成 Java 对象返回最终由 JSP 页面渲染成表格。这个过程中Spring 的 IoC 容器负责把 Service、Mapper 等对象装配在一起AOP 则在 Service 层方法上拦截事务。!-- spring-context.xml 中的关键配置 -- context:component-scan base-packagecom.catering / !-- 开启注解事务管理 -- tx:annotation-driven transaction-managertransactionManager / bean idtransactionManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource / /bean这段配置的核心是component-scan让 Spring 自动扫描com.catering包下的注解组件transactionManager则把事务管理绑定到数据源上。注意tx:annotation-driven的作用是启用Transactional注解这样在 Service 方法上标注该注解后方法内所有的数据库操作会被包在同一个事务里任何一个步骤失败都会整体回滚。2.1.1 Service 层接口与实现分离的意义在餐饮管理系统里菜品类别管理和菜品信息管理是两个不同的模块但它们都依赖同一套 Service 层设计规范。接口定义业务方法签名实现类写具体逻辑。这样做的直接好处是事务代理可以挂在接口方法上Spring AOP 生成的代理对象会拦截调用并在方法执行前开启事务。public interface DishService { ListDish queryDishList(String keyword, Integer categoryId, int pageNum, int pageSize); boolean addDish(Dish dish); boolean updateDish(Dish dish); boolean deleteDish(Integer dishId); }接口里定义了四个方法分页多条件查询、新增菜品、修改菜品、删除菜品。queryDishList的参数里keyword是菜品名称的关键字categoryId是菜品类别的外键pageNum和pageSize用于分页。调用方只依赖这个接口不关心底层是 MyBatis 还是其他 ORM 实现后续如果要替换持久层框架只需要改实现类。2.1.2 MyBatis 作为半自动 ORM 的选择理由文档中提到 SSM 组合中 MyBatis 相对于 Hibernate 是半自动框架这个说法在餐饮管理系统这种场景下是成立的。Hibernate 的全自动映射在处理固定实体关系时开发效率更高但像“餐桌状态统计”、“菜品销量排行”这类需要手写聚合 SQL 的场景Hibernate 的 HQL 和 Criteria API 反而成为束缚。MyBatis 允许直接编写原生 SQL通过if、where等动态标签在 XML 里拼接条件既保留了 SQL 的灵活性又避免了 JDBC 手动设置参数和解析结果集的重复劳动。2.2 MVC 三层在 B/S 架构中的具体表现餐饮管理系统采用 B/S 结构用户通过浏览器访问服务器端用 JSP 做视图层Servlet 或 SpringMVC 的 Controller 做控制层业务逻辑封装在 Service 层。JSP 页面里只有 HTML 标签和 JSTL/EL 表达式不写 Java 业务代码Controller 只做参数接收、调用 Service、把结果放回 Model 这三个动作SQL 全部集中在 MyBatis 的 Mapper XML 文件里。这样做的好处可以从一次“添加菜品”的操作看出来表单提交到 ControllerController 构造 Dish 对象调用 Service 的addDish方法Service 里先校验菜品名称是否重复再调用 Mapper 执行 INSERT最后返回结果给页面提示。整个流程每一层只做自己的事出了问题可以直接定位到对应的类或 XML 文件。按照这套系统的开发文档项目路径一般是src/main/java下按controller、service、dao分包src/main/resources下放 MyBatis 的 mapper XMLWebContent下放 JSP 页面。2.3 MySQL 数据库连接与密码安全处理系统后端采用 MySQL 数据库连接配置集中在jdbc.properties文件里。连接池使用 dbcp 或 c3p0 都是常见做法这里以最基本的配置说明参数含义jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/catering_db?useUnicodetruecharacterEncodingutf-8 jdbc.usernameroot jdbc.password123456useUnicodetruecharacterEncodingutf-8是必须加上的否则 JSP 页面提交的中文菜品名写入数据库后会变成乱码。characterEncodingutf-8指定了客户端与服务器通信时的字符集与数据库表的 utf8 编码保持一致。如果要在生产环境部署通常还会追加autoReconnecttrue来应对数据库连接空闲超时被断开的问题。关于用户密码文档明确提到使用 MD5 加密后存储。这里有个容易忽略的细节仅对密码做 MD5 哈希是不够的因为彩虹表可以直接反查简单密码。常见的改进做法是加盐把用户名或随机字符串与密码拼接后再做摘要。// 注册时对密码做加盐 MD5 String salt UUID.randomUUID().toString().substring(0, 8); String hashed DigestUtils.md5Hex(salt password); // 数据库保存 salt 和 hashed 两个字段这样即便两个用户设置了相同的密码数据库中存储的盐不同最终的哈希值也不同。登录校验时先用用户名查到这个用户的盐再用同样的拼接方式计算哈希值与数据库里的hashed字段比对。3. 数据库设计从业务流程图到六张核心表的字段定义3.1 概念设计从数据流图梳理实体关系餐饮管理系统文档中给出了 0 层和 1 层数据流图实体包括系统用户、菜品类别、菜品信息、餐桌信息、购买记录等。在把这些实体转成表之前先要确认它们之间的关系一个菜品类别下包含多个菜品这是一对多关系一个餐桌可以产生多个订单这也是一对多关系一个订单里包含多个菜品一个菜品也可以出现在多个订单里所以订单与菜品是多对多关系需要一张中间表来拆解。实际项目中设计表结构的第一步是画出 ER 图明确主外键关系。用户表和角色表之间通过role字段区分超级管理员和普通用户系统登录时根据角色跳转到不同的首页。这个设计在文档的“不同用户登录问题”一节有明确说明根据用户类别实现操作权限的区分并显示不同的操作界面。3.2 表结构设计核心表字段与建表 SQL3.2.1 用户表、菜品表、餐桌表用户表保存系统登录账号和用户资料菜品表保存菜品的基础信息餐桌表管理餐厅内各餐桌的状态。这三张表构成系统的基础数据骨架。CREATE TABLE sys_user ( id INT(11) NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL COMMENT 登录用户名, password VARCHAR(64) NOT NULL COMMENT MD5加密后的密码, salt VARCHAR(16) DEFAULT NULL COMMENT 密码盐值, real_name VARCHAR(50) DEFAULT NULL COMMENT 真实姓名, phone VARCHAR(20) DEFAULT NULL, role TINYINT(4) DEFAULT 1 COMMENT 角色0-超级管理员1-普通管理员2-用户, status TINYINT(4) DEFAULT 1 COMMENT 状态1-启用0-禁用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8 COMMENT系统用户表;role字段用数字表示角色而不是单独建角色表适用于权限层级固定的管理系统。salt字段与上一章的加盐 MD5 对应uk_username唯一索引保证用户名不重复注册时如果插入重名用户名会抛出 DuplicateKeyExceptionController 捕获后返回“用户名已存在”提示即可。CREATE TABLE dish ( id INT(11) NOT NULL AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT 菜品名称, category_id INT(11) NOT NULL COMMENT 所属菜品类别ID, price DECIMAL(10,2) NOT NULL COMMENT 单价, image VARCHAR(255) DEFAULT NULL COMMENT 菜品图片路径, description TEXT COMMENT 菜品描述, status TINYINT(4) DEFAULT 1 COMMENT 状态1-在售0-下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_category (category_id), CONSTRAINT fk_dish_category FOREIGN KEY (category_id) REFERENCES dish_category (id) ) ENGINEInnoDB DEFAULT CHARSETutf8 COMMENT菜品信息表;菜品价格用DECIMAL(10,2)而不是 FLOAT 或 DOUBLE是因为浮点数在二进制中无法精确表示 0.1 这类小数累计求和时会产生误差。idx_category索引加速按类别查询外键约束保证插入菜品时的类别 ID 在类别表中一定存在。餐桌表相对简单关键字段是状态字段。可用值可以设计为 0-空闲、1-占用、2-已预定比用字符串更省存储空间查询时比较数字也比比较字符串快。CREATE TABLE dining_table ( id INT(11) NOT NULL AUTO_INCREMENT, table_no VARCHAR(20) NOT NULL COMMENT 餐桌编号如A001, seat_count INT(11) DEFAULT 4 COMMENT 座位数, status TINYINT(4) DEFAULT 0 COMMENT 状态0-空闲1-占用2-已预定, remark VARCHAR(255) DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8 COMMENT餐桌信息表;3.2.2 订单表与订单明细表为什么要拆成两张表购买信息管理是本系统的核心业务模块。如果只建一张订单表菜品的名称和价格直接存在订单字段里那么一个订单点了 5 道菜就需要 5 条记录同一订单的收货人、下单时间等信息在每条记录里重复存储数据冗余明显而且订单表会非常宽。拆成订单主表和订单明细表后一次下单在主表插入一条记录明细表插入多条记录通过订单 ID 关联。CREATE TABLE orders ( id INT(11) NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 订单编号, user_id INT(11) NOT NULL COMMENT 下单用户ID, table_id INT(11) DEFAULT NULL COMMENT 关联餐桌ID, total_amount DECIMAL(10,2) NOT NULL COMMENT 订单总金额, status TINYINT(4) DEFAULT 0 COMMENT 状态0-进行中1-已完成2-已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8 COMMENT订单主表; CREATE TABLE order_item ( id INT(11) NOT NULL AUTO_INCREMENT, order_id INT(11) NOT NULL, dish_id INT(11) NOT NULL, dish_name VARCHAR(100) NOT NULL COMMENT 冗余菜品名称, price DECIMAL(10,2) NOT NULL COMMENT 下单时单价, quantity INT(11) NOT NULL DEFAULT 1 COMMENT 数量, PRIMARY KEY (id), KEY idx_order (order_id), CONSTRAINT fk_item_order FOREIGN KEY (order_id) REFERENCES orders (id) ) ENGINEInnoDB DEFAULT CHARSETutf8 COMMENT订单明细表;order_item里冗余了dish_name和price是因为菜品名称和价格可能调整但历史订单里的快照不能跟着变。这一点在餐饮行业的账单核销场景里特别重要三个月前的订单菜品价格必须保持下单时的原样。3.3 数据一致性保证事务与外键约束在用餐高峰期用户在较短的时间内频繁下单数据库同时处理大量的 INSERT 和 UPDATE 操作。订单主表插入和明细表插入如果分开执行没有事务保护中途出错时主表有记录而明细表没记录订单金额和实际消费就对不上。在 Service 方法上加上Transactional并把propagation设为默认的REQUIRED就能保证两次插入要么都成功要么都回滚。另一个容易忽视的问题是数据库的备份策略。文档的研究内容里提到“定期对数据库进行备份”在 MySQL 环境下最常见的备份方式是使用mysqldump命令。对于开发和学习环境手动执行即可mysqldump -uroot -p catering_db /data/backup/catering_$(date %Y%m%d).sql这个命令把catering_db数据库导出成 SQL 文件$(date %Y%m%d)会把当天日期拼到文件名里例如catering_20250120.sql。恢复时用mysql -uroot -p catering_db backup.sql。如果数据量变大可以考虑用 Binlog 做增量备份但那是另一个话题了。4. 核心功能实现SSM 整合、登录认证与菜品 CRUD 的完整链路4.1 SSM 整合的三个关键配置文件SSM 整合是这套系统从文档走向可运行的第一步也是最容易卡住的一步。需要三个配置文件web.xml配置 SpringMVC 的前端控制器和字符集过滤器spring-context.xml配置数据源、事务管理和组件扫描spring-mvc.xml配置 SpringMVC 的注解驱动、视图解析器和静态资源映射。web.xml里最先要配置的是字符集过滤器不配的话 JSP 表单提交的中文数据到 Controller 里就是乱码filter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mappingCharacterEncodingFilter会把请求和响应的编码统一设为 UTF-8url-pattern设为/*表示过滤所有路径。这个过滤器必须放在其他过滤器之前因为后续处理流程都依赖参数的正确编码。spring-mvc.xml里的视图解析器是关键配置它决定了 Controller 返回的字符串如何映射到具体的 JSP 页面bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/views/ / property namesuffix value.jsp / /beanprefix和suffix组合起来的作用是Controller 返回dish/list时SpringMVC 会找到/WEB-INF/views/dish/list.jsp这个物理文件。把 JSP 放在WEB-INF目录下的好处是浏览器无法直接访问必须经过 Controller 转发才能渲染可以避免用户绕过登录直接访问管理页面。4.2 登录模块从表单提交到 Session 校验登录模块是系统的第一道安全屏障文档中明确要求不同用户类别区分权限。实现上分为三层JSP 页面收集用户名和密码Controller 接收并调用 ServiceService 里做校验并把用户信息写入 Session。JSP 表单部分不做过多说明重点看 Controller 的写法Controller RequestMapping(/user) public class UserController { Autowired private UserService userService; RequestMapping(value /login, method RequestMethod.POST) public String login(String username, String password, HttpSession session, Model model) { User user userService.login(username, password); if (user null) { model.addAttribute(msg, 用户名或密码错误); return login; } session.setAttribute(loginUser, user); // 根据角色跳转不同首页 if (user.getRole() 0) { return redirect:/admin/index; } return redirect:/index; } }Autowired注入 UserService 接口RequestMapping把 POST 请求映射到login方法。方法参数里的username和password是 SpringMVC 根据参数名自动绑定的HttpSession 由框架注入。校验失败时把错误消息放进 Model返回login逻辑视图名由视图解析器渲染回登录页成功时把用户对象放进 Session然后按角色重定向到不同页面。使用重定向而不是直接返回视图可以避免刷新页面时重复提交登录表单。Service 层的校验逻辑要把密码加盐哈希后再比对public User login(String username, String rawPassword) { User user userMapper.findByUsername(username); if (user null) { return null; } String hashed DigestUtils.md5Hex(user.getSalt() rawPassword); if (!hashed.equals(user.getPassword())) { return null; } return user; }这里先从数据库查出用户记录拿到这个用户的盐值计算出输入密码的哈希值与库里的密码比对。两个null判断把“用户不存在”和“密码错误”统一成同一种返回结果避免攻击者通过不同的提示信息猜测系统中是否存在某个用户名。4.2.1 登录拦截器保护管理页面不被直接访问仅仅在登录时判断用户身份远远不够用户如果直接输入管理页面的 URL比如/admin/dish/list仍然可以绕过登录。需要一个拦截器在请求到达 Controller 之前校验 Sessionpublic class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); User user (User) session.getAttribute(loginUser); if (user null) { response.sendRedirect(request.getContextPath() /login); return false; } return true; } }preHandle在请求进入 Controller 之前执行返回false表示请求被拦截不再继续往后传递。把它注册到 SpringMVC 配置里并对/admin/**路径设置拦截规则。如果后续要细分权限比如普通用户不能访问超级管理员页面可以在同一处加角色校验逻辑。4.3 菜品信息管理的 CRUD 与 MyBatis 动态 SQL菜品信息管理是系统中操作最频繁的模块。列表页需要支持多条件组合查询按菜品名称模糊搜索、按类别下拉筛选、支持分页。MyBatis 的动态 SQL 是处理这类场景最顺手的工具。select idqueryDishList resultTypecom.catering.entity.Dish SELECT d.*, c.name AS category_name FROM dish d LEFT JOIN dish_category c ON d.category_id c.id where if testdishName ! null and dishName ! AND d.name LIKE CONCAT(%, #{dishName}, %) /if if testcategoryId ! null AND d.category_id #{categoryId} /if if teststatus ! null AND d.status #{status} /if /where ORDER BY d.create_time DESC LIMIT #{offset}, #{pageSize} /selectwhere标签会自动处理条件拼接如果所有的if都不成立生成的 SQL 里不会出现 WHERE 关键字如果只有部分条件成立它会去掉第一个条件前面的 AND避免 SQL 语法错误。LIKE CONCAT(%, #{dishName}, %)在 Java 层不拼%而是交给 SQL 拼接可以防止dishName里带有特殊字符时被误当成通配符。分页使用LIMIT #{offset}, #{pageSize}offset的计算公式是(pageNum - 1) * pageSize在 Service 层算好后传入 Mapper。这里的#{}是预编译占位符防止 SQL 注入。4.3.1 Service 层分页参数计算public PageResultDish queryDishList(String dishName, Integer categoryId, int pageNum, int pageSize) { int offset (pageNum - 1) * pageSize; ListDish list dishMapper.queryDishList(dishName, categoryId, offset, pageSize); int total dishMapper.countDishList(dishName, categoryId); return new PageResult(list, total, pageNum, pageSize); }分页查询需要两次 SQL一次查当前页的数据一次查符合条件的总条数。总条数用于前端计算总页数。PageResult里封装了列表数据和分页信息JSP 页面通过 JSTL 标签遍历结果集渲染表格。4.3.2 菜品类别联动的下拉框处理菜品列表页通常会有一个类别筛选下拉框选项来自dish_category表。管理员新增菜品时要选择菜品所属类别这里需要保证类别表里有数据时才能添加菜品。Controller 在跳转到新增页面前先查询全部类别放入 ModelRequestMapping(/addPage) public String addPage(Model model) { ListDishCategory categories dishCategoryService.queryAll(); model.addAttribute(categories, categories); return dish/add; }JSP 页面的下拉框用 JSTL 的c:forEach遍历categories每个选项的 value 是类别 ID显示文本是类别名称。提交表单时Controller 接收categoryId参数转换为 Dish 实体的categoryId字段后保存到数据库。5. 餐桌状态管理与订单流程状态流转、事务与并发控制5.1 餐桌状态机的定义与流转逻辑文档中的餐桌信息管理模块要求维护餐桌的状态信息包括预定、使用、空闲等。表面上看只是一个字段的增删改查但实际业务中餐桌状态变化是有顺序的空闲的餐桌才能被预定预定的餐桌在客人到店后变成使用中客人结账后恢复为空闲。如果在代码里随意修改状态字段就会出现“占用中的餐桌被再次预订”这类数据不一致问题。处理方式是定义状态机的流转条件public boolean changeTableStatus(Integer tableId, Integer fromStatus, Integer toStatus) { DiningTable table tableMapper.findById(tableId); if (table null || !table.getStatus().equals(fromStatus)) { return false; } return tableMapper.updateStatus(tableId, fromStatus, toStatus) 1; }changeTableStatus的调用必须传入期望的初始状态fromStatus更新时通过 SQL 里的 WHERE 条件再次校验状态这一步是防止并发改动的关键UPDATE dining_table SET status #{toStatus} WHERE id #{tableId} AND status #{fromStatus}这条 UPDATE 语句把状态校验从 Java 层下沉到数据库层。两个管理员同时把同一张餐桌从空闲改成占用只有一个 UPDATE 能匹配到status 0的条件另一个影响行数为 0从返回值就能判断操作失败。更新影响行数作为判断依据写回 Service 的返回值。5.2 下单流程的事务边界与防超卖设计下单是订单模块的核心操作一个完整的下单动作包含以下几个步骤校验用户登录状态、查询菜品信息、计算总金额、写入订单主表、写入订单明细表、更新餐桌状态。这个流程涉及多张表的写入必须在一个事务里执行。Transactional(rollbackFor Exception.class) public boolean createOrder(OrderVO orderVO) { // 1. 校验菜品是否在售 for (OrderItemVO item : orderVO.getItems()) { Dish dish dishMapper.findById(item.getDishId()); if (dish null || dish.getStatus() ! 1) { throw new BusinessException(菜品已下架 item.getDishId()); } } // 2. 生成订单号并计算总价 String orderNo ORD System.currentTimeMillis(); BigDecimal total calculateTotal(orderVO.getItems()); // 3. 插入订单主表 Orders order new Orders(); order.setOrderNo(orderNo); order.setUserId(orderVO.getUserId()); order.setTableId(orderVO.getTableId()); order.setTotalAmount(total); ordersMapper.insert(order); // 4. 插入明细表 for (OrderItemVO item : orderVO.getItems()) { orderItemMapper.insert(new OrderItem(order.getId(), item.getDishId(), item.getDishName(), item.getPrice(), item.getQuantity())); } // 5. 更新餐桌状态 tableMapper.updateStatus(orderVO.getTableId(), 0, 1); return true; }Transactional(rollbackFor Exception.class)的rollbackFor比较关键。Spring 默认情况下只对 RuntimeException 回滚如果 Service 方法抛出的是受检异常如 IOException事务不会回滚。rollbackFor声明所有异常都触发回滚可以避免这种意外。关于并发控制前面提到的UPDATE ... WHERE status #{fromStatus}在这里同样用于防止两张订单同时占用同一张餐桌。先验证后再插入的方式虽然存在时间窗口但由于更新语句的条件是原状态问题不大。5.3 购买信息订单的查询与删除操作订单查询页面支持按订单号、用户、时间范围筛选管理员也可以直接删除错误或过期的订单。删除时要把主表记录和对应的明细一起删除这个操作同样需要在事务里完成Transactional(rollbackFor Exception.class) public boolean deleteOrder(Integer orderId) { orderItemMapper.deleteByOrderId(orderId); ordersMapper.deleteById(orderId); return true; }先删除明细再删除主表避免外键约束导致删除失败。如果先删主表订单表记录不存在后明细表的外键校验会抛出异常。查询功能则复用上一章 MyBatis 动态 SQL 的思路把orderNo作为模糊查询条件、把status作为精确筛选条件拼接进 SQL分页显示。6. 测试与排错从登录用例到 SSM 分层调用的验证技巧6.1 登录与注册的单元测试设计文档的系统测试部分列出了注册测试和登录测试这两个用例写起来并不复杂但覆盖的边界条件值得整理。注册测试的核心是校验重复用户名、密码长度、两次密码一致性登录测试的核心是正确密码通过、错误密码失败、不存在的用户返回统一提示。常见的做法是为 Service 层写 JUnit 测试用 Spring 的测试框架加载配置文件不需要启动 Tomcat 就能验证业务逻辑RunWith(SpringJUnit4ClassRunner.class) ContextConfiguration(locations {classpath:spring-context.xml}) public class UserServiceTest { Autowired private UserService userService; Test public void testLoginWithWrongPassword() { User user userService.login(admin, wrongpass); Assert.assertNull(user); } Test public void testRegisterDuplicateUsername() { boolean result userService.register(new User(admin, 123456)); Assert.assertFalse(result); } }ContextConfiguration指定加载 Spring 的上下文配置测试方法直接调用 Service 接口数据库操作会真实落到测试库上。为测试准备独立的数据库不要在生产库上跑测试。6.2 集成测试中的常见问题与排查顺序系统联调阶段最常见的报错集中在三个位置Spring 容器初始化失败、MyBatis 映射错误、页面渲染异常。排查顺序一般是从控制台日志往回追溯先看是否是 Bean 找不到再看 SQL 是否执行出错。BeanCreationException或NoSuchBeanDefinitionException说明component-scan扫描的包路径不对或者类上缺少Service、Repository注解。BindingException说明 Mapper 接口与 XML 文件的 namespace 或方法 id 对不上检查mapper XML的 namespace 是否等于接口全限定名。SQLSyntaxErrorException则要按前面说的动态 SQL 拼接规则在日志里打印出最终执行的 SQL 语句把参数代入后单独在 MySQL 客户端执行确认语法是否正确。6.2.1 实现 MyBatis SQL 日志打印在 MyBatis 配置里开启日志输出是排查 SQL 问题的第一步configuration settings setting namelogImpl valueSTDOUT_LOGGING / /settings /configurationSTDOUT_LOGGING会让 MyBatis 把执行的基础 SQL 和参数值直接打印到控制台前面这个配置写的是logImpl注意不要在 value 里配置成StdOutImpl那是 MyBatis 内部实现类名配置成STDOUT_LOGGING是为了走标准输出。如果项目里已经集成了 Log4j2 或 SLF4J也可以改为对应的实现类名称。6.3 一个实用的分层验证技巧从 JSP 到数据库的逐段定位当页面上报 500 错误但控制台日志只显示一段堆栈时可以用一个笨但有效的方法快速二分定位先确认请求有没有到达 Controller再确认 Controller 有没有拿到参数再确认 Service 执行到哪一步。在关键方法入口加一行System.out.println或日志输出比一步步断点调试更快地缩小范围。更省事的做法是在浏览器开发者工具的 Network 面板里直接看请求状态。404 说明 URL 映射有问题看 Controller 的RequestMapping是否与前端提交的地址一致500 则要重点看服务器端日志。如果请求根本没发出去问题大概率在前端 JavaScript 的表单校验或按钮事件绑定上。对于这套餐饮管理系统验证登录模块是否正常工作的操作顺序为先在数据库手动插入一条测试用户记录并计算对应盐值的 MD5 密文然后通过页面登录看是否能跳转到后台首页如果登录失败检查 Service 的哈希计算结果与数据库存储值是否一致。这个流程可以排除大量配置层面的干扰因素直接定位是代码逻辑问题还是数据问题。本文还有配套的精品资源点击获取