尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

SSM校园点餐系统源码详解:从数据库设计到部署压测实战

SSM校园点餐系统源码详解:从数据库设计到部署压测实战 简介面向高校 Java 课程设计与毕业设计的 SSM 校园在线点餐系统源码基于 Spring Spring MVC MyBatis 整合框架前端采用 Layui、JSP 与 jQuery提供用户注册登录、购物车、订单评论、校园资讯以及后台用户/商品/订单/评论等完整功能。资源共 2000 个文件、46.05MB涵盖 Java 源码、JSP 页面、HTML/JS/CSS 前端资源、图片素材与数据库 SQL 脚本并附有启动配置说明便于本地部署与二次开发。运行环境为 JDK8、Tomcat8、MySQL5.6访问路径与登录账号已在描述中给出适合需要快速搭建在线点餐系统并理解 SSM 分层开发的读者参考学习。已有 949 人学习浏览可帮助学习者掌握前后台业务实现、MyBatis 数据操作及前端页面整合等关键技能并从中学习 SSM 分层设计、JSP 标签使用与 Layui 组件集成等实践经验。1. SSM校园点餐系统源码这套老框架仍是 Java 后端绕不过去的坎中午 11:35食堂窗口还没坐满后台却在一分钟内挤进三百多个下单请求——校园点餐系统的流量尖峰通常集中在午休前十五分钟。这种并发不高但业务链路完整的场景正是 Spring SpringMVC MyBatis 最舒服的射程用户端做菜单浏览、购物车和下单管理端做菜品维护与订单处理增删改查、事务、分页、状态流转全都有。标题里的含数据库意味着压缩包里带可导入的 SQL 脚本不用从零建表就能跑通业务。对准备 java 面试题中独立负责过什么项目的提问、要交数据库课程设计或想把 ssm 框架协作关系过一遍的 Java 从业者来说这是投入产出比很高的练习对象。下面按表结构、代码分层、部署排错、高峰验证四条线展开。2. SSM点餐系统的数据库表设计与 ER 关系映射2.1 Spring、SpringMVC、MyBatis 三层的职责边界在点餐系统里三层各管一摊Spring 负责管理 Service 层的 Bean、事务和 AOP是后两者之间的黏合剂SpringMVC 接收 HTTP 请求做参数绑定、校验和视图/JSON 返回MyBatis 把 SQL 与 Java 方法做映射负责数据库增删改查。常见的返工原因是把业务逻辑写在 Controller 里、把 SQL 拼在 Service 里——短期能跑但后续加一个按日核算营业额的功能就要改三层。包结构一般按 controller、service、service.impl、daomapper、entity、common 划分。entity 里的每个类对应一张表字段名与表列名保持驼峰和下滑线一一对应最好打开 MyBatis 的 mapUnderscoreToCamelCase 配置省掉大量 resultMap。表设计是这套分层的地基表结构错了后面所有 Mapper 和 Service 都要跟着返工所以先把表定住。2.2 用户、菜品、订单三组核心表怎么定一份典型的校园点餐系统数据库至少包含七张表按业务归属分组如下分组表名职责关键字段用户tb_user学生/普通用户id, username, password, phone, status用户tb_admin后台管理员id, username, password, real_name菜品tb_dish_category菜品分类id, name, sort菜品tb_dish菜品id, category_id, name, price, stock, image, status交易tb_cart购物车id, user_id, dish_id, count, create_time交易tb_order订单主表id, order_no, user_id, total_amount, status, create_time交易tb_order_detail订单明细id, order_id, dish_id, dish_name, price, countER 关系很直观一个分类下有多个菜品1:N一个用户产生多个订单1:N一个订单包含多个明细1:N。明细表里的 dish_name、price 是冗余快照字段——下单那一刻菜品改价或改名历史订单仍要显示当时的快照所以只存 dish_id 是错误做法。购物车是典型的弱实体可以不建外键用户清空或退出时直接按 user_id 删除。2.3 建表脚本与字段参数说明核心的表是 tb_dish 与 tb_order其余表结构都可以沿着这两张推出来。以下是一份可直接执行的 DDLCREATE TABLE tb_dish ( id int NOT NULL AUTO_INCREMENT COMMENT 主键, category_id int NOT NULL COMMENT 所属分类 id, name varchar(50) NOT NULL COMMENT 菜品名称, price decimal(10,2) NOT NULL COMMENT 销售单价, stock int NOT NULL DEFAULT 0 COMMENT 当日库存, image varchar(255) DEFAULT NULL COMMENT 图片相对路径, status tinyint NOT NULL DEFAULT 1 COMMENT 1 上架 0 下架, PRIMARY KEY (id), KEY idx_category (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT菜品表; CREATE TABLE tb_order ( id int NOT NULL AUTO_INCREMENT COMMENT 主键, order_no varchar(32) NOT NULL COMMENT 业务订单号, user_id int NOT NULL COMMENT 下单用户, total_amount decimal(10,2) NOT NULL COMMENT 订单总额, status tinyint NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2制作中 3待取餐 4已完成 5已取消, remark varchar(255) DEFAULT NULL COMMENT 备注, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, pay_time datetime DEFAULT NULL COMMENT 支付时间, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单主表;字段参数有几个细节值得注意。价格必须用 decimal(10,2) 而不是 float否则累加订单总额会出现 0.1 0.2 的精度尾巴utf8mb4 是为了兼容学生的 emoji 签名和生僻字不要图省事用 utf8order_no 建唯一索引业务上用它做支付回调的幂等判断status 用 tinyint 而非 varchar状态含义变化只改代码枚举不改表。外键可以不建索引就是性能上的关系这也是 MyBatis 项目里的主流做法。2.4 从表结构还原 ER 图数据库课程设计要画的对应关系数据库课程设计报告里通常要求配三张图ER 图、关系模式、表结构说明。手头只有 SQL 文件时还原方法如下先从建表语句里找出所有主键和 KEY 索引主外键对应关系画菱形连线1:N 关系标在一方的主键引用侧再把每张表的字段按实体属性、联系属性归类写进关系模式。点餐系统里最容易画错的是订单与菜品的关系。它是多对多经过订单明细拆解后的两个一对多订单 1:N 明细 N:1 菜品。ER 图里应把 tb_order_detail 画成联系实体而不是直接给 order 和 dish 画 M:N 连线。报告里再补一段说明快照字段的理由导师一般不会追问下去。3. 用 MyBatis 动态 SQL 和 Spring 事务实现点餐核心链路3.1 pom.xml 依赖版本与兼容性选型SSM 项目的依赖版本选择比代码本身更容易踩坑。我一般会锁定 Spring 5.2.x 而不是 6.x因为 6.x 强制依赖 Jakarta EE和市面上大量基于 javax.servlet 的 Tomcat 8.5/9 环境不兼容MyBatis 用 3.5.xmybatis-spring 用 2.0.x这两个版本与 Spring 5 的集成包是官方验证过的组合。最小依赖集如下properties spring.version5.2.22.RELEASE/spring.version /properties dependencies dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version${spring.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.6/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.6/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.8/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.28/version scoperuntime/scope /dependency /dependenciesmysql-connector 的 scope 设成 runtime 即可编译期用不上但打包期会进 WAR 的 lib 目录如果误打成 provided部署后就会报 ClassNotFoundException。druid 配一个就够连接池参数集中在 jdbc.properties 里维护。3.2 菜品列表Mapper 接口 动态 SQL 手动分页点餐首页的菜品列表要支持按分类筛选、只显示上架菜品、分页返回。动态 SQL 的if标签用来处理分类 id 是否必传分页优先用手写 LIMIT而不是引入 PageHelper 插件——一次页面查询少一重拦截器链调试成本更低。Mapper 接口定义如下Mapper public interface DishMapper { ListDish selectOnSale(Param(categoryId) Integer categoryId, Param(offset) int offset, Param(limit) int limit); Dish selectById(Integer id); int reduceStock(Param(id) Integer id, Param(count) int count); }对应的 XML 映射文件select idselectOnSale resultTypecom.campus.entity.Dish SELECT id, category_id, name, price, stock, image, status FROM tb_dish WHERE status 1 if testcategoryId ! null AND category_id #{categoryId} /if ORDER BY id DESC LIMIT #{offset}, #{limit} /select update idreduceStock UPDATE tb_dish SET stock stock - #{count} WHERE id #{id} AND stock #{count} /update参数说明categoryId 传 null 时返回全部分类传具体值则追加条件这是动态 SQL 解决可选查询条件的标准写法offset 是跳过行数(page-1) × pageSize 在 Service 层算好再传入不要在前端算LIMIT 两个参数分别对应 offset 和 limit。这里有个容易被面试官追问的点为什么stock #{count}而不是先 SELECT 再判断因为条件 UPDATE 是原子操作数据库行锁保证两个并发请求同时扣减时只有一个成功这正是点餐高峰期不超卖的第一道防线。下面下单事务会继续用这个方法。3.3 下单事务条件扣库存与主键回填下单链路要写四张表校验菜品、扣库存、插订单主表、插订单明细。任何一个环节失败都必须整体回滚所以 Service 方法上要加 Transactional。关键实现在下面的代码里Service public class OrderServiceImpl implements OrderService { Autowired private DishMapper dishMapper; Autowired private OrderMapper orderMapper; Autowired private OrderDetailMapper orderDetailMapper; Override Transactional(rollbackFor Exception.class) public String createOrder(OrderCreateParam param) { if (param.getItems() null || param.getItems().isEmpty()) { throw new BizException(购物车不能为空); } BigDecimal total BigDecimal.ZERO; for (OrderItem item : param.getItems()) { Dish dish dishMapper.selectById(item.getDishId()); if (dish null || dish.getStatus() ! 1) { throw new BizException(菜品不存在或已下架 item.getDishId()); } // 条件更新库存足够才扣减返回 0 表示扣减失败 int rows dishMapper.reduceStock(item.getDishId(), item.getCount()); if (rows 0) { throw new BizException(库存不足 dish.getName()); } total total.add(dish.getPrice() .multiply(BigDecimal.valueOf(item.getCount()))); } Order order new Order(); order.setOrderNo(genOrderNo()); order.setUserId(param.getUserId()); order.setTotalAmount(total); order.setStatus(0); orderMapper.insert(order); // order.getId() 由 MyBatis 回填供明细表关联使用 for (OrderItem item : param.getItems()) { orderDetailMapper.insert(new OrderDetail( order.getId(), item.getDishId(), item.getDishName(), item.getPrice(), item.getCount())); } return order.getOrderNo(); } }逻辑说明分四步先校验菜品存在且上架再逐项条件扣库存返回行数为 0 说明库存不足直接抛异常让事务回滚然后插入订单主表MyBatis 的 useGeneratedKeys 会把自增主键写回 order.getId()最后插入明细时用这个回填主键关联父子表。订单号用时间戳加随机数拼出避免依赖数据库自增 id 对外暴露单量。事务有一个容易忽略的边界Transactional 只对公共方法的外部调用生效。如果同一类内部 A 方法调用带事务的 B 方法事务不会开启因为代理对象没有参与调用。分层越清楚越不容易踩这个坑。3.4 Controller 参数绑定与统一返回体后端接口用 JSON 交互比 JSP 转发适合点餐场景因为前端页面原生 JS 或小程序需要拿到结构化响应再渲染。Controller 层约定统一的 Result 结构code 为 0 表示成功非 0 携带错误信息Controller RequestMapping(/order) public class OrderController { Autowired private OrderService orderService; PostMapping(value /create, produces application/json;charsetUTF-8) ResponseBody public ResultString create(RequestBody OrderCreateParam param) { if (param null || param.getUserId() null) { return Result.error(下单参数缺失); } try { return Result.ok(orderService.createOrder(param)); } catch (BizException e) { return Result.error(e.getMessage()); } } }RequestBody 把 HTTP body 里的 JSON 反序列化成 OrderCreateParam要求前端必须传 Content-Type: application/json另一个常用注解 RequestParam 则从 URL 查询串取值适合菜品列表这种 GET 请求。这个案例里把 BizException 在 Controller 捕获并转成 Result.error避免 500 页面直接暴露给用户。4. SSM点餐系统从 zip 到可访问数据库导入、Tomcat 部署与 6 个高频坑4.1 解压后先按这个顺序看文件拿到 .zip 先别急着导入 IDEA按下面顺序确认内容能省掉一半排错时间。最先看 sql 目录下的 .sql 脚本确认建库语句用没用 CREATE DATABASE用了就要先删掉或者手动建库再导入再看 src/main/resources 下的 jdbc.properties 和 mybatis-config.xml数据库账号、密码、URL 参数都在这里然后看 pom.xml 的打包方式是 war 还是 jarSSM 配 JSP 页面的项目基本是 war最后看 webapp/WEB-INF/web.xmlDispatcherServlet 的映射路径决定访问 URL 前缀。常见误区是先把代码跑起来再回头配库结果报错全堆在数据库连接上。正确的顺序一定是先建库导数据、改配置、再启动 Tomcat。4.2 MySQL 建库导入与 jdbc.properties 关键参数数据库初始化用命令行完成比在 Navicat 里点向导更可复现# 建库字符集必须和建表脚本一致 mysql -uroot -p -e CREATE DATABASE IF NOT EXISTS campus_order DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; # 导入表结构与初始数据 mysql -uroot -p campus_order sql/campus_order.sql # 验证表数量 mysql -uroot -p -e USE campus_order; SHOW TABLES;导入后打开 jdbc.properties这是整个部署过程中最需要对齐环境的文件jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/campus_order?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456 jdbc.initialSize5 jdbc.maxActive20 jdbc.minIdle2 jdbc.validationQuerySELECT 1参数说明MySQL 5.7 用 com.mysql.jdbc.DriverMySQL 8 以上必须换成 com.mysql.cj.jdbc.Driver两者不对应会直接报驱动类找不到serverTimezone 不写MySQL 8 连接会抛时区异常useSSLfalse 是为了避免本地环境证书警告拖慢首次连接initialSize 和 maxActive 是 Druid 连接池的初始连接数与峰值连接数本地单机 5/20 足够压测时再上调。4.3 Maven 打包与 Tomcat 发布IDEA 里直接跑 Tomcat 适合调试交付验证还是走命令行打包更干净# 跳过测试打包 mvn clean package -DskipTests # 复制到 Tomcat webapps 目录 cp target/campus_order.war /opt/tomcat85/webapps/ # 启动并跟踪日志 /opt/tomcat85/bin/startup.sh tail -f /opt/tomcat85/logs/catalina.outTomcat 检测到新 WAR 会自动解压启动完成后访问路径是http://localhost:8080/campus_order/多一层上下文路径是正常的。如果 404先确认 webapps 下解压出来的目录名和 WAR 名一致再确认 web.xml 里 DispatcherServlet 的 url-pattern 是/还是*.do后者访问时必须在路径里带上.do后缀。4.4 六个高频坑的现场表现与处理这六个问题在 SSM 点餐系统里出现频率最高症状和对应处理直接列出现象原因处理编译错误HttpServletRequest 找不到Tomcat 10 改用 jakarta.servlet旧代码基于 javax换 Tomcat 8.5/9或全局替换 import连接报 The server time zone value 乱码MySQL 8 时区识别失败jdbc.url 加 serverTimezoneAsia/Shanghai8080 端口启动报错端口被其他进程占用换 8081或杀掉占用进程ClassNotFoundException: com.mysql.cj.jdbc.Driver驱动依赖 scope 配成 provided改成 runtime 或直接去掉 scopeJSP 页面中文全部乱码请求/响应编码不一致配置 CharacterEncodingFilterJSP 头加 UTF-8打开页面只显示源码web.xml 把 JSP 请求拦截url-pattern 改为/静态资源用 mvc:resources 放行最后一个坑比较隐蔽url-pattern 配成/*时JSP 会被 DispatcherServlet 当普通请求处理浏览器就拿到 JSP 源码。正解是 Controller 返回视图名用 InternalResourceViewResolver 解析或者干脆前后端分离、所有页面走静态资源。5. SSM点餐系统订单状态机与午间高峰的最小压测方案5.1 order.status 的五状态流转点餐系统能演示状态机这个面试亮点关键是 status 字段的流转有明确的触发方和前置条件。标准约定如下status含义触发动作前置状态0待支付用户提交订单无1已支付用户支付成功回调02制作中后厨接单13待取餐出餐完成24已完成用户确认取餐35已取消用户取消或超时0 或 1Service 里更新状态时建议用UPDATE tb_order SET status #{target} WHERE id #{id} AND status #{expect}比如待支付转已支付必须带AND status 0。这比先查再更安全能避免重复回调把订单状态覆盖。5.2 curl 验证下单与库存一致性部署完成后用三个请求完成冒烟验证不用打开浏览器# 1. 拉取分类 1 的第一页菜品 curl -s http://localhost:8080/campus_order/dish/list?categoryId1page1pageSize6 # 2. 提交一笔两件商品的订单 curl -s -X POST http://localhost:8080/campus_order/order/create \ -H Content-Type: application/json \ -d {userId:1,items:[{dishId:1,count:2}]} # 3. 核对库存是否扣减 2 mysql -uroot -p -e SELECT name, stock FROM campus_order.tb_dish WHERE id1;验证要点不是接口返回 200而是第三步的库存数字恰好等于原库存减下单数量。连续重复执行第二步第二次起应当收到库存不足的 JSON 响应这能证明条件 UPDATE 的防超卖逻辑生效。5.3 午间高峰前的一组并发验证压测工具用 JMeter 就够线程组设 100 并发、Ramp-Up 5 秒、循环 20 次请求指向 /order/create。跑完后核对两个数tb_order 表新增行数等于成功响应数每个菜品库存扣减总数等于所有成功订单里该菜品的购买量之和。两个数对不上优先看 catalina.out 里有没有 Deadlock 异常——并发扣库存偶发死锁时重试一次即可恢复业务上可接受。启动前可以加一个预热动作容器初始化时执行一次dishMapper.selectOnSale(null, 0, 10)把菜品数据加载进 MyBatis 二级缓存和 MySQL 的 buffer pool避免开闸第一批请求全部命中冷缓存。这个细节在面试里描述为冷启动预热比泛泛说缓存更有说服力。最后留一个自查动作打开 Tomcat 的 AccessLog观察下单接口响应时间分布超过 500ms 的请求用SHOW PROCESSLIST看是否在Copying to tmp table——出现这个状态时优先检查 tb_order_detail 的 order_id 是否单独建了索引。本文还有配套的精品资源点击获取
返回列表