做社区订餐系统这类 SSM 项目,最怕的不是写代码,而是整个项目从零到一跑通的全流程。很多同学拿到一套“ssm基于EE的社区订餐系统t34g4”的源码,第一反应是赶紧跑起来、截个图交差,结果卡在环境配置上好几个小时,最后才发现是数据库编码没对上。这篇文章我就以这套社区订餐系统为载体,把 SSM 项目从设计、编码到调试部署的完整链路拆开讲透,包括为什么用 SSM、数据库怎么建表、后端核心逻辑怎么落、前端怎么和接口对接,以及最容易被忽略的部署细节。不管你是正在做课程设计、毕业设计,还是刚进入 Java 后端想找个完整项目练手,这套流程都值得过一遍——因为这背后是 Java EE 企业级应用最常见的开发范式,搞懂了它,你以后看任何 SSM 项目都会觉得眼熟。
1. 项目整体设计与技术选型思路
1.1 社区订餐系统的业务定位
社区订餐系统说白了就是一个小范围的“外卖平台”——它不像美团饿了么那样覆盖全城,而是把服务范围锁定在一个社区、一个园区或者一栋写字楼里,用户下单、商家接单、骑手配送都围绕这个局部区域运转。这个定位决定了它的业务模型不会特别复杂,但又足够覆盖电商系统最核心的几个环节:用户管理、商品浏览、购物车、订单状态流转。
这套系统的角色通常分为三类:顾客、商家(或食堂档口)、管理员。顾客负责注册、浏览菜品、加购、下单、支付(在课程设计里一般用模拟支付或者货到付款)、查看订单状态;商家负责上架菜品、修改菜品信息、接单、出餐;管理员负责审核商家、管理用户、查看全平台订单数据。三套角色对应三套权限,这也是权限控制模块的天然练习场景。
1.2 为什么还是选 SSM 这套老组合
你可能会问:现在 Spring Boot 都普及成这样了,为什么课程设计、毕业设计里还是大量出现 SSM?答案很实在:SSM(Spring + Spring MVC + MyBatis)是 Java EE 企业级应用的经典组合,它的分层思想直接继承自早期 Java EE 规范,而且几乎所有高校的 Java Web 课程还在用这套技术栈教学。把 SSM 吃透,Spring Boot 对你来说就是一个“自动配置 + 约定优于配置”的加成,而不是一个全新的东西。
这套系统的技术选型如下:
| 技术组件 | 具体选型 | 作用 |
|---|---|---|
| 后端框架 | Spring + Spring MVC + MyBatis | 依赖注入、Web MVC、持久层映射 |
| 运行时环境 | JDK 8 + Tomcat 8.5 | Java EE Web 容器 |
| 前端技术 | JSP + JSTL + Bootstrap | 服务端渲染页面,简单直接 |
| 数据库 | MySQL 5.7 | 订单、用户、菜品等核心业务数据 |
| 构建工具 | Maven 3.6 | 依赖管理和项目打包 |
| 开发工具 | IDEA 或 Eclipse | Java 开发 IDE |
这套组合最大的好处是“每一层都看得见摸得着”:Controller 里的每个方法都明明白白写着请求映射,Service 里的事务注解一眼就能看到,Mapper 里的 SQL 是你自己写的而不是框架生成的。对学习来说,这种“透明感”非常宝贵。
1.3 分层架构:从 Controller 到 Mapper 的数据流
SSM 项目的分层是固定的,理解了这个数据流,你就理解了大半个项目。前端 JSP 页面发起 HTTP 请求,请求到达DispatcherServlet,Spring MVC 通过HandlerMapping找到对应的Controller;Controller 负责接收参数、调用Service接口;Service 层写业务逻辑,事务也是在这一层控制;Service 调用Mapper(也就是 DAO),MyBatis 通过动态代理生成实现类,执行 XML 或注解里的 SQL,把结果集映射成实体对象;然后一层层返回,最后 Controller 把数据塞进ModelAndView,渲染 JSP 页面返回给浏览器。
这套链路里最核心的规约就是:Controller 不写业务逻辑,只做参数接收和视图跳转;Service 不拼接 SQL,只做业务编排;Mapper 不处理业务,只做数据访问。我在检查代码的时候经常看到有人把 SQL 写在 Controller 里——这确实能跑,但改起来就是灾难。遵守分层规矩,哪怕只是课程设计,也值得养成这个习惯。
2. 数据库设计与核心表结构拆解
2.1 订单数据的生命周期分析
社区订餐系统数据库设计的重点不是表多,而是表的字段能不能覆盖订单的完整生命周期。先想清楚一个订单从产生到结束经过哪些状态:顾客提交订单(待支付),确认支付(待接单),商家接单(制作中/配送中),顾客确认收货(已完成),或者中途取消(已取消)。这套状态流转需要订单表和订单详情表配合,还需要一个字段记录当前状态值。
围绕这个生命周期,系统需要如下核心表:
user用户表:顾客、商家、管理员共用一张表,用role字段区分角色。category菜品分类表:如热菜、凉菜、主食、饮品。dish菜品表:包含菜品名称、图片、价格、描述、所属商家、上下架状态。cart购物车表:记录用户加购但未下单的临时数据。orders订单表:记录订单编号、用户 ID、商家 ID、总金额、订单状态、下单时间。order_detail订单详情表:记录订单里每个菜品的快照信息(名称、单价、数量、小计)。address收货地址表:记录用户填写的配送地址。comment评论表:记录用户对订单/菜品的评价。
有的项目还会加banner轮播图表、notice公告表这些锦上添花的内容,看个人需要。
2.2 关键表字段设计与建表 SQL
我挑两张最核心的表展开说说。一张是orders,一张是order_detail。
orders表的关键字段设计如下:
CREATE TABLE `orders` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '订单ID', `order_no` varchar(32) NOT NULL COMMENT '订单编号', `user_id` int(11) NOT NULL COMMENT '下单用户ID', `merchant_id` int(11) NOT NULL COMMENT '商家ID', `total_amount` decimal(10,2) NOT NULL COMMENT '订单总金额', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '订单状态:0待支付 1待接单 2配送中 3已完成 4已取消', `remark` varchar(255) DEFAULT NULL COMMENT '备注', `address` varchar(255) NOT NULL COMMENT '配送地址', `create_time` datetime NOT NULL COMMENT '下单时间', `pay_time` datetime DEFAULT NULL COMMENT '支付时间', `finish_time` datetime DEFAULT NULL COMMENT '完成时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';这个表里有几个设计细节值得注意。order_no设置唯一索引,是为了方便展示订单编号并防止重复;status用tinyint存数字而不是直接存字符串,是为了方便状态判断和后续扩展状态值;total_amount用decimal(10,2)而不是float,因为浮点数在金额计算上会有精度问题。这些都是实际开发中的惯用做法。
order_detail表则是另一个思路——它是订单数据的“快照表”:
CREATE TABLE `order_detail` ( `id` int(11) NOT NULL AUTO_INCREMENT, `order_id` int(11) NOT NULL COMMENT '关联订单ID', `dish_id` int(11) NOT NULL COMMENT '菜品ID', `dish_name` varchar(100) NOT NULL COMMENT '菜品名称(快照)', `dish_image` varchar(255) DEFAULT NULL COMMENT '菜品图片(快照)', `price` decimal(10,2) NOT NULL COMMENT '下单时单价(快照)', `quantity` int(11) NOT NULL DEFAULT '1' COMMENT '购买数量', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单详情表';为什么订单详情要存“快照”而不是直接关联菜品表?因为商家的菜品价格改了、名字改了已经不重要了——用户下单那一刻的信息必须永远保留。这也是电商系统里非常经典的设计思想,很多新手一开始不懂,容易在订单详情里只存一个dish_id,后面查历史订单发现菜品名和价格全变了,追都追不回来。
2.3 常见数据库设计踩坑提醒
数据库这块有四个高频踩坑点,我每次带项目都会强调一遍:
第一,字段类型不用utf8而用utf8mb4。MySQL 的utf8其实不是真正的 UTF-8,它最多支持 3 字节,emoji 表情和一些生僻字需要 4 字节,存进去就是???或者报错。社区订餐系统的用户地址、备注里完全可能出现表情符号,所以建表时统一utf8mb4是标配。已经有表了想改也不要慌,一行命令的事。
第二,逻辑删除优于物理删除。用户取消订单、商家删除菜品,尽量不要执行DELETE,而是加一个is_deleted或status字段标记。好处是数据可追溯,订单统计报表也好做。物理删除在课程设计里可能看不出问题,但养成逻辑删除的习惯绝对没坏处。
第三,金额计算用BigDecimal。这一点在 Java 侧也要注意。double和float计算金额会产生误差,比如0.1 + 0.2并不等于0.3。订单总金额、单价这些字段,Java 实体类都用BigDecimal对应。
第四,外键约束能不用尽量不用。很多教材强调外键,但在实际项目里,表之间的关联关系更多靠应用层维护,数据库层面只建索引不建外键。原因是外键会影响插入和删除的性能,还会让业务解耦变得困难。SSM 项目里,订单表关联用户表、订单详情表关联订单表,靠的是 Java 代码和 SQL 的JOIN,而不是数据库外键。
3. SSH 到 SSM:核心注解与后端功能落地
3.1 先搞懂 SSM 三个框架各自干了什么
我之前遇到一个同学,SSM 项目跑起来了,但是让他说说 Spring IOC 起什么作用、MyBatis 是怎么把接口和 SQL 绑起来的,他答不上来。这说明他做项目是在“抄代码”而不是在“理解代码”。我建议拿到源码之后,先花 30 分钟搞清楚三件事:
Spring 解决的是“对象管理”问题。传统 Java Web 里,对象由开发者手动new,Service 依赖 DAO,就要手动new DAO,层层手动创建,耦合极高。Spring 用 IOC 容器统一创建和管理对象,需要的时候用@Autowired注入,对象之间的依赖关系交给容器处理。
Spring MVC 解决的是“请求分发”问题。它有一个核心的DispatcherServlet,所有请求都先进它,再由它找对应的 Controller 方法。这取代了早期 Servlet 里一堆if/else判断 URL 的做法。
MyBatis 解决的是“SQL 映射”问题。它在 DAO 接口方法和 SQL 语句之间建立映射,我们只需要写接口、写 XML 里的 SQL 或注解 SQL,MyBatis 会在运行时动态生成接口的实现类。这也是为什么你会发现 Mapper 接口没有impl类却能直接注入使用。
3.2 SSM 常用注解清单与正确用法
这套系统里用到的注解,我可以给你列个清单:
| 注解 | 所在层 | 作用 |
|---|---|---|
@Controller | Controller | 标记该类是 Spring MVC 的控制器 |
@RequestMapping | Controller | 映射 HTTP 请求路径和 Controller 方法 |
@ResponseBody | Controller | 把方法返回值直接写入 HTTP 响应体(返回 JSON) |
@RequestParam | Controller | 绑定请求参数,如@RequestParam("id") Integer id |
@PathVariable | Controller | 绑定 URL 路径变量,如/order/{id} |
@Autowired | 全局 | 按类型自动注入依赖 |
@Service | Service | 标记业务层组件,纳入 Spring 容器管理 |
@Transactional | Service | 开启事务,方法或类中所有数据库操作同生共死 |
@Repository | Mapper | 标记持久层组件 |
@Select/@Insert/@Update/@Delete | Mapper | 注解式 SQL 语句 |
这里有三个使用细节要单独说。
@Transactional是 Service 层的“保命符”。比如用户下单这个操作,至少要执行三步:插入订单表、插入订单详情表、清空购物车。如果第二步失败而第一步没回滚,就会出现垃圾订单数据。在 Service 方法上加上@Transactional,三个操作要么全部成功要么全部回滚。默认情况下,只有RuntimeException会触发回滚,受检异常(checked exception)不会——这一点很多人不知道,如果你在方法里手动throws Exception,就等着数据不一致吧。
@Autowired默认是按类型注入。如果同一个接口有多个实现类,需要配合@Qualifier("beanName")指定具体注入哪个。SSM 项目里 Service 接口通常只有一个实现,所以实际很少用到@Qualifier,但面试时会被问到。
@ResponseBody加在方法上,返回的对象会被 Jackson 转成 JSON。现在前后端分离很普遍,但我建议社区订餐这种课程设计用 JSP 服务端渲染就够了,AJAX 请求的接口才用@ResponseBody返回 JSON 给前端 JS 处理。
3.3 订单模块:一段核心业务代码的逻辑拆解
订单模块是整个系统的核心,我拿“提交订单”这个功能来拆解一下 Service 层的业务代码长什么样。这会比 Spring 的基础知识实用得多。
@Service public class OrderServiceImpl implements OrderService { @Autowired private OrderMapper orderMapper; @Autowired private OrderDetailMapper orderDetailMapper; @Autowired private CartMapper cartMapper; @Override @Transactional(rollbackFor = Exception.class) public boolean submitOrder(Order order, List<Cart> cartList) { // 1. 生成订单编号:时间戳 + 用户ID + 随机数 String orderNo = "ORD" + System.currentTimeMillis() + order.getUserId(); order.setOrderNo(orderNo); order.setStatus(0); // 待支付 // 2. 计算订单总金额 BigDecimal totalAmount = new BigDecimal(0); for (Cart cart : cartList) { totalAmount = totalAmount.add(cart.getPrice().multiply(new BigDecimal(cart.getQuantity()))); } order.setTotalAmount(totalAmount); // 3. 插入订单主表 orderMapper.insertOrder(order); // 4. 遍历购物车生成订单快照 for (Cart cart : cartList) { OrderDetail detail = new OrderDetail(); detail.setOrderId(order.getId()); detail.setDishId(cart.getDishId()); detail.setDishName(cart.getDishName()); detail.setDishImage(cart.getDishImage()); detail.setPrice(cart.getPrice()); detail.setQuantity(cart.getQuantity()); orderDetailMapper.insertOrderDetail(detail); } // 5. 清空该用户的购物车 cartMapper.deleteByUserId(order.getUserId()); return true; } }这段代码就是完整的订单提交事务链,注意@Transactional(rollbackFor = Exception.class)这一行,它覆盖了所有异常类型。这里重点不是代码本身,而是这个思考顺序:先确定业务步骤,再确定哪些步骤属于同一个事务,最后事务的开启位置应该在 Service 层而不是 Controller 层。
3.4 权限控制:拦截器与 Session 的配合
社区订餐系统的用户、商家、管理员共用一套登录体系,权限控制通常用 Spring MVC 的拦截器实现。先创建一个LoginInterceptor,实现HandlerInterceptor接口,在preHandle方法里检查 Session 里有没有loginUser:
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object loginUser = request.getSession().getAttribute("loginUser"); if (loginUser == null) { // 未登录,跳转到登录页 response.sendRedirect(request.getContextPath() + "/login.jsp"); return false; } return true; } }然后在 Spring MVC 配置文件里注册拦截器,配置拦截路径:
<mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/**"/> <mvc:exclude-mapping path="/login"/> <mvc:exclude-mapping path="/register"/> <mvc:exclude-mapping path="/dish/list"/> <mvc:exclude-mapping path="/css/**"/> <mvc:exclude-mapping path="/js/**"/> <mvc:exclude-mapping path="/images/**"/> <bean class="com.xxx.interceptor.LoginInterceptor"/> </mvc:interceptor> </mvc:interceptors>这里有一个容易踩的坑:/css/**、/js/**这些静态资源路径如果被拦截器接管,页面样式会全部失效。很多新手部署完发现页面白板一块,控制台全是 404,就是这个原因。Spring MVC 默认会拦截所有请求,静态资源必须通过<mvc:resources>标签或者上面的 exclude-mapping 放行。
管理员和商家的权限一般不用再做一层拦截器,而是通过用户角色字段判断。比如商家后台的入口,在 JSP 页面里判断loginUser.role == 2才显示入口;商家管理类操作在 Controller 里再校验一遍角色,双重保险不会错。
4. 前端交互与页面实现细节
4.1 JSP 服务端渲染为什么还是首选
现在前端框架满天飞,但社区订餐系统这类课程设计仍是 JSP 为主。原因不是技术落后,而是服务的对象决定了技术选型:JSP 渲染天然适合“多页面 + 服务端数据的传统 Web 应用”,而且和 SSM 的集成是零成本——Controller 返回ModelAndView,直接把数据塞给页面,页面用${}取值,整个过程没有任何跨域、异步等额外复杂度。
这套系统的典型页面对照如下:
login.jsp / register.jsp:用户登录注册。index.jsp:首页,展示分类和推荐菜品。dishList.jsp:菜品列表,按分类筛选。cart.jsp:购物车页面。orderConfirm.jsp:订单确认页,填写地址和备注。myOrder.jsp:我的订单列表。merchant/orderManage.jsp:商家接单页面。admin/userManage.jsp:管理员用户管理页面。
4.2 购物车的三种实现方案对比
购物车是社区订餐系统里最容易写出“表面功能”的模块。很多同学的购物车是用 JavaScript 的数组做的,刷新页面购物车就没了,这顶多算个“演示壳”。要做完整,有常见三种方案:
方案一:Session 购物车。购物车数据结构存在 Session 里,刷新不丢,关闭浏览器才丢。优点是实现简单,不需要写数据库;缺点是不能跨设备同步,也无法在数据库里追踪用户加购行为。
方案二:Cookie 购物车。加购信息序列化后存在 Cookie 里。优点是也能保持状态;缺点是容量限制(4KB),菜品一多就得往上堆。
方案三:数据库购物车(本系统推荐)。加购时直接插cart表,用户 ID 关联。购物车数据和用户账号绑定,换设备登录购物车还在,而且后端可以随时查询用户购物车里的具体内容。这套系统我用的是第三种。
数据库购物车实现时有一个小细节值得注意:如果用户把一个菜品反复加入购物车,是直接增加数量还是新增一条记录?正确做法是先查该用户购物车中是否已有该菜品,有则更新数量,无则插入。这个需要你在CartServiceImpl里多写一个判断。
4.3 AJAX 异步请求与 JSON 数据交互
虽然页面用 JSP 渲染,但还是有几个场景需要 AJAX 异步请求,不然用户体验会很差。比如用户点击“加入购物车”,跳个页面体验不好,用 AJAX 悄悄把数据送到后端,前端弹个“加入成功”的提示;商家在后台点击“接单”,用 AJAX 更新订单状态,页面局部刷新。
Spring MVC 处理 AJAX 的套路是固定的:前端用 jQuery 的$.ajax或$.post发送请求,后端 Controller 方法加@ResponseBody返回一个封装好的结果对象,比如Result类:
public class Result<T> { private Integer code; // 200成功,500失败 private String msg; private T data; // getter/setter 略 }前端收到 JSON 后解析处理。这里有一个新手必踩的坑:JSON 日期格式问题。MyBatis 查询出的订单创建时间create_time是java.util.Date,Jackson 默认序列化出来的格式是"yyyy-MM-dd'T'HH:mm:ss.SSSZ"这种,前端拿到的日期字符串和预期完全不一样。解决办法是在 Spring MVC 配置文件里配置 Jackson 的日期格式化:
<mvc:annotation-driven> <mvc:message-converters register-defaults="true"> <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"> <property name="objectMapper"> <bean class="com.fasterxml.jackson.databind.ObjectMapper"> <property name="dateFormat"> <bean class="java.text.SimpleDateFormat"> <constructor-arg value="yyyy-MM-dd HH:mm:ss"/> </bean> </property> </bean> </property> </bean> </mvc:message-converters> </mvc:annotation-driven>4.4 订单状态流转的页面呈现技巧
订单状态从 0 到 4 是数字,不能直接展示给用户看。JSP 里通过<c:choose>判断展示对应的状态文字和按钮,这是最朴素也最可靠的方式:
<c:choose> <c:when test="${order.status == 0}"> <span class="label label-warning">待支付</span> <a href="${pageContext.request.contextPath}/order/pay?id=${order.id}" class="btn btn-primary btn-xs">去支付</a> <a href="${pageContext.request.contextPath}/order/cancel?id=${order.id}" class="btn btn-default btn-xs">取消订单</a> </c:when> <c:when test="${order.status == 1}"> <span class="label label-info">待接单</span> </c:when> <c:when test="${order.status == 2}"> <span class="label label-primary">配送中</span> </c:when> <c:when test="${order.status == 3}"> <span class="label label-success">已完成</span> </c:when> <c:when test="${order.status == 4}"> <span class="label label-default">已取消</span> </c:when> </c:choose>这样做的好处是不管后端状态怎么扩展,前端逻辑都清清楚楚。页面布局用 Bootstrap 的栅格系统,简单整洁,不需要前端基础多好就能写出能看的页面。
5. 环境配置与调试部署全流程
5.1 开发环境搭建清单
标题里写了“开发环境”,所以我把整套环境配置过程列出来,踩的坑也一并说了。这个是 SSM 项目能不能跑起来的第一步,也是最容易被低估的一步。
| 软件 | 版本建议 | 安装注意事项 |
|---|---|---|
| JDK | JDK 8(1.8) | 不要装 JDK 17,老项目大概率跑不起来,Tomcat 版本不匹配 |
| Maven | 3.6.x | 配置settings.xml里的阿里云镜像,不然下载依赖能急死人 |
| Tomcat | 8.5.x | 解压版即可,不要用 Tomcat 10,包名从javax变成了jakarta,SSM 项目直接用不了 |
| MySQL | 5.7 或 8.0 | 设置 root 密码,记住字符集选utf8mb4 |
| IDEA | 任何较新版本 | 安装 Lombok 插件(如果用了) |
Tomcat 版本这块我再强调一遍:很多同学图新装了 Tomcat 10,结果项目启动直接报ClassNotFoundException: javax.servlet.http.HttpServlet。因为 Tomcat 10 开始,Servlet 包名从javax.servlet迁移到了jakarta.servlet,而 SSM 项目是基于javax的,必须用 Tomcat 8.5 或 9.0。这不是代码问题,是环境版本不匹配。
5.2 Maven 项目导入与依赖配置
拿到源码后,在 IDEA 里选择File -> Open -> 选择 pom.xml,第一次加载会自动下载依赖。如果下载速度慢,去Maven 安装目录/conf/settings.xml加镜像:
<mirror> <id>aliyunmaven</id> <mirrorOf>central</mirrorOf> <name>阿里云公共仓库</name> <url>https://maven.aliyun.com/repository/central</url> </mirror>pom.xml 里的核心依赖就这几个,SSM 项目都是这一套,不用多也不用少:
<dependencies> <!-- Spring --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.23</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.3.23</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.3.23</version> </dependency> <!-- MyBatis 与 适配器 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.13</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.7</version> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.30</version> </dependency> <!-- 连接池 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.16</version> </dependency> </dependencies>版本之间不用追求最新,稳定最重要。Spring 5.3.x、MyBatis 3.5.x、MySQL 8.0.x 驱动,这一套组合非常稳定,比那些最新版本出现不兼容问题再排查半天的情况舒服多了。
5.3 配置文件逐个拆解:jdbc、Spring、SpringMVC、MyBatis
SSM 项目的配置集中在src/main/resources目录下面,大部分启动失败都出在这几个文件里。先说jdbc.properties:
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/community_order?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=123456community_order是数据库名,根据实际建的库修改。URL 里的characterEncoding=utf8一定要有,否则数据库里中文全变问号。serverTimezone=Asia/Shanghai是 MySQL 8.0 的要求,不加会报时区错误。如果用的是 MySQL 5.7,驱动类是com.mysql.jdbc.Driver,8.0 则是com.mysql.cj.jdbc.Driver,多了个.cj。
然后是spring-mybatis.xml,重点是配置数据源、SqlSessionFactory 和 Mapper 扫描:
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> <property name="typeAliasesPackage" value="com.xxx.entity"/> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.xxx.mapper"/> </bean>mapperLocations指定的是 MyBatis 的 XML 映射文件路径,很多项目报 “Invalid bound statement (not found)” 错误,就是因为这个地方没有扫描到 XML 文件。typeAliasesPackage是实体类包路径,配了它之后,XML 里写resultType="User"就不用写全路径com.xxx.entity.User了。
最后是spring-mvc.xml,配置组件扫描、注解驱动和视图解析器:
<context:component-scan base-package="com.xxx.controller"/> <mvc:annotation-driven/> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/jsp/"/> <property name="suffix" value=".jsp"/> </bean>InternalResourceViewResolver的prefix和suffix决定了 Controller 返回的字符串如何映射到 JSP 文件。Controller 里写return "orderList",就会去找/WEB-INF/jsp/orderList.jsp。
5.4 war 包部署与 IDEA 本地部署两种方式
部署这块有两种方式,我建议先用 IDEA 本地部署调试,最后再打 war 包放独立 Tomcat。本地部署是:IDEA 里配置 Tomcat Server,选择Local,在Deployment标签点+添加Artifact -> 项目名:war exploded,Application context 填/或者/community_order。这种方式是“热部署”,改了代码直接重新部署很快。
正式环境或需要交付的时候打 war 包:Maven 面板里双击package,或者在命令行执行mvn clean package,然后去target目录找项目名.war,复制到 Tomcat 的webapps目录下,启动 Tomcat 即可。访问路径就是http://localhost:8080/项目名/。记得修改jdbc.properties里的数据库地址和账号密码,改成部署服务器的实际信息。
注意:war 包部署后,如果访问页面报 404,先确认 Tomcat 启动日志里有没有Deployment of web application archive [xxx.war] has finished这行字。没有这行就是部署失败,去logs/catalina.out看报错。另外,如果改过jdbc.properties,一定要重新打包,Tomcat 不会主动读你改过的配置文件。
5.5 数据库脚本导入的两种方式
数据库脚本一般是.sql文件,导入方式有两种。命令行方式:
mysql -u root -p create database community_order default character set utf8mb4; use community_order; source /path/to/community_order.sql;Navicat 方式更简单:新建数据库,字符集选utf8mb4,然后右键数据库选择“运行 SQL 文件”,选择你的.sql文件即可。导入后重点检查三件事:表结构是否完整(数一下表数量)、每个表有没有数据(尤其 admin 用户)、中文是否显示正常。
如果导入后发现中文乱码,大概率是 SQL 文件本身是utf8编码,但数据库连接默认用了latin1,导入的时候指定一下就行:
mysql -u root -p --default-character-set=utf8mb4 community_order < community_order.sql6. 高频报错与排查技巧实录
6.1 启动期三大报错速查表
SSM 项目从启动到运行,报错基本集中在启动期和运行期两个阶段,我把最常见的整理成一张表:
| 报错现象 | 根本原因 | 解决方案 |
|---|---|---|
ClassNotFoundException: javax.servlet.* | Tomcat 版本太高(10+),包名变了 | 换 Tomcat 8.5 或 9.0 |
Invalid bound statement (not found) | Mapper XML 没有被扫描 | 检查spring-mybatis.xml的mapperLocations路径 |
Failed to configure a DataSource | jdbc.properties路径或参数错误 | 检查数据库地址、账号、密码、驱动类 |
Table 'xxx' doesn't exist | SQL 脚本没导入或导错库 | 确认数据库名,重新导入脚本 |
Access denied for user 'root'@'localhost' | 数据库密码错误 | 检查jdbc.properties的jdbc.password |
There is no getter for property named 'xxx' | 实体类属性和数据库字段对不上 | 检查 MyBatis XML 里的resultMap或#{}参数 |
The valid characters are defined in RFC 7230 | URL 里带了中文或特殊字符 | 参数使用POST提交或前端encodeURIComponent编码 |
6.2 运行期数据异常排查思路
启动成功后,运行阶段也有一堆奇怪问题。我挑几个典型的说说排查思路。
问题一:列表页能打开但是数据为空。先不要怀疑 SQL,先用浏览器直接访问 Controller 的 URL,或者看 IDEA 控制台的 SQL 日志(SSM 项目可以在mybatis-config.xml里配置log-impl: STDOUT_LOGGING),确认 SQL 语句真正执行了什么。很多时候是因为 Mapper XML 里的<if test="categoryId != null">条件没有命中,参数传进来是字符串"null"而不是null,整个筛选条件没生效,查出来了全表数据又被 JSP 的<c:forEach>遍历空对象搞得报错。
问题二:下单报错“事务回滚”,但不知道哪一步失败。在submitOrder方法的每个数据库操作后面加日志,或者直接注释掉事务注解逐排查。最常见的失败点是订单详情表的外键约束(如果你建了外键)或者order_id没有拿到——订单主表插入后,order.getId()是null,因为 MyBatis 插入后没有回填自增主键。解决方法是 XML 里的insert语句加两个属性:
<insert id="insertOrder" parameterType="Order" useGeneratedKeys="true" keyProperty="id">问题三:登录后跳回首页,刷新一下又变游客了。这是 Session 作用域的管理问题。登录成功后要session.setAttribute("loginUser", user),而logout时要session.invalidate()或者session.removeAttribute("loginUser")。还有一种情况是web.xml里的<session-config>设置超时过短,默认 30 分钟,如果调试时反复重启 Tomcat,Session 会消失,看起来像“登录失效”。
问题四:AJAX 请求 404。检查 Spring MVC 配置里有没有注解驱动<mvc:annotation-driven/>,没有这个标签@ResponseBody不生效,Controller 会直接把返回结果当 JSP 路径找,然后报 404。另外检查请求 URL 是否少了pageContext.request.contextPath,JSP 页面里 AJAX 请求的 URL 一定要拼上项目上下文路径,否则会请求到别的路径上去。
6.3 我用过最顺手的排查工具组合
排查 SSM 项目的日常工具,我一般是“日志 + 浏览器开发者工具 + 数据库客户端”三件套。
SSM 项目可以使用 log4j 或 logback 输出日志。在log4j.properties里把 Mapper 包的日志级别调到DEBUG:
log4j.logger.com.xxx.mapper=DEBUG这样 MyBatis 执行每条 SQL 的时候,控制台能看到完整的 SQL 语句和参数值。这是排查数据问题的第一步,比任何高级工具都直观。
浏览器开发者工具主要看Network面板:请求是否发出、请求参数对不对、响应状态码多少、响应体长什么样。但凡 AJAX 有问题,先看这里,基本一眼定位。
数据库客户端我用 Navicat 或 DBeaver,主要干三件事:查看表数据(确认数据状态)、手动执行 SQL(验证 SQL 是否正确)、模拟业务操作(比如手动改订单状态,看前端页面怎么展示)。这三件套配合下来,95% 的问题能在 10 分钟内定位。
7. 这套系统还能怎么扩展
到这里,一套完整的 SSM 社区订餐系统就算落地了。从数据库表设计、SSM 三层架构、订单状态流转,到环境配置、war 包部署、问题排查,整个链路都跑通了。我个人在实际操作中的体会是:SSM 项目最大的价值不在于技术栈多新,而在于它把 Java Web 开发最核心的“请求-处理-存储-响应”链路完整地暴露在你面前,每一个环节都可以 debug、可以加日志、可以查文档,这对建立后端开发的整体认知特别有帮助。
最后再分享一个小技巧:如果你把整套源码跑通了,别急着提交,试着做三件小事——第一,给订单模块加一个“订单超时自动取消”的定时任务,用 Spring 的@Scheduled注解就能实现;第二,把图片上传功能从“本地上传”改成“上传到项目根目录的 upload 文件夹”,理解一下相对路径和绝对路径的区别;第三,把分页查询从“手写 LIMIT”改成用 PageHelper 插件,对比一下开发效率。这三件事做完,你对 SSM 的掌握就比单纯跑通代码深入了一大截。这套系统的后续扩展空间也正在于此——它能成长的边界,取决于你想在这个经典框架上继续走多远。