
做社区订餐系统这类 SSM 项目最怕的不是写代码而是整个项目从零到一跑通的全流程。很多同学拿到一套“ssm基于EE的社区订餐系统t34g4”的源码第一反应是赶紧跑起来、截个图交差结果卡在环境配置上好几个小时最后才发现是数据库编码没对上。这篇文章我就以这套社区订餐系统为载体把 SSM 项目从设计、编码到调试部署的完整链路拆开讲透包括为什么用 SSM、数据库怎么建表、后端核心逻辑怎么落、前端怎么和接口对接以及最容易被忽略的部署细节。不管你是正在做课程设计、毕业设计还是刚进入 Java 后端想找个完整项目练手这套流程都值得过一遍——因为这背后是 Java EE 企业级应用最常见的开发范式搞懂了它你以后看任何 SSM 项目都会觉得眼熟。1. 项目整体设计与技术选型思路1.1 社区订餐系统的业务定位社区订餐系统说白了就是一个小范围的“外卖平台”——它不像美团饿了么那样覆盖全城而是把服务范围锁定在一个社区、一个园区或者一栋写字楼里用户下单、商家接单、骑手配送都围绕这个局部区域运转。这个定位决定了它的业务模型不会特别复杂但又足够覆盖电商系统最核心的几个环节用户管理、商品浏览、购物车、订单状态流转。这套系统的角色通常分为三类顾客、商家或食堂档口、管理员。顾客负责注册、浏览菜品、加购、下单、支付在课程设计里一般用模拟支付或者货到付款、查看订单状态商家负责上架菜品、修改菜品信息、接单、出餐管理员负责审核商家、管理用户、查看全平台订单数据。三套角色对应三套权限这也是权限控制模块的天然练习场景。1.2 为什么还是选 SSM 这套老组合你可能会问现在 Spring Boot 都普及成这样了为什么课程设计、毕业设计里还是大量出现 SSM答案很实在SSMSpring Spring MVC MyBatis是 Java EE 企业级应用的经典组合它的分层思想直接继承自早期 Java EE 规范而且几乎所有高校的 Java Web 课程还在用这套技术栈教学。把 SSM 吃透Spring Boot 对你来说就是一个“自动配置 约定优于配置”的加成而不是一个全新的东西。这套系统的技术选型如下技术组件具体选型作用后端框架Spring Spring MVC MyBatis依赖注入、Web MVC、持久层映射运行时环境JDK 8 Tomcat 8.5Java EE Web 容器前端技术JSP JSTL Bootstrap服务端渲染页面简单直接数据库MySQL 5.7订单、用户、菜品等核心业务数据构建工具Maven 3.6依赖管理和项目打包开发工具IDEA 或 EclipseJava 开发 IDE这套组合最大的好处是“每一层都看得见摸得着”Controller 里的每个方法都明明白白写着请求映射Service 里的事务注解一眼就能看到Mapper 里的 SQL 是你自己写的而不是框架生成的。对学习来说这种“透明感”非常宝贵。1.3 分层架构从 Controller 到 Mapper 的数据流SSM 项目的分层是固定的理解了这个数据流你就理解了大半个项目。前端 JSP 页面发起 HTTP 请求请求到达DispatcherServletSpring MVC 通过HandlerMapping找到对应的ControllerController 负责接收参数、调用Service接口Service 层写业务逻辑事务也是在这一层控制Service 调用Mapper也就是 DAOMyBatis 通过动态代理生成实现类执行 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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 里对象由开发者手动newService 依赖 DAO就要手动new DAO层层手动创建耦合极高。Spring 用 IOC 容器统一创建和管理对象需要的时候用Autowired注入对象之间的依赖关系交给容器处理。Spring MVC 解决的是“请求分发”问题。它有一个核心的DispatcherServlet所有请求都先进它再由它找对应的 Controller 方法。这取代了早期 Servlet 里一堆if/else判断 URL 的做法。MyBatis 解决的是“SQL 映射”问题。它在 DAO 接口方法和 SQL 语句之间建立映射我们只需要写接口、写 XML 里的 SQL 或注解 SQLMyBatis 会在运行时动态生成接口的实现类。这也是为什么你会发现 Mapper 接口没有impl类却能直接注入使用。3.2 SSM 常用注解清单与正确用法这套系统里用到的注解我可以给你列个清单注解所在层作用ControllerController标记该类是 Spring MVC 的控制器RequestMappingController映射 HTTP 请求路径和 Controller 方法ResponseBodyController把方法返回值直接写入 HTTP 响应体返回 JSONRequestParamController绑定请求参数如RequestParam(id) Integer idPathVariableController绑定 URL 路径变量如/order/{id}Autowired全局按类型自动注入依赖ServiceService标记业务层组件纳入 Spring 容器管理TransactionalService开启事务方法或类中所有数据库操作同生共死RepositoryMapper标记持久层组件Select/Insert/Update/DeleteMapper注解式 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, ListCart 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 里有没有loginUserpublic 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 classcom.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 ResultT { private Integer code; // 200成功500失败 private String msg; private T data; // getter/setter 略 }前端收到 JSON 后解析处理。这里有一个新手必踩的坑JSON 日期格式问题。MyBatis 查询出的订单创建时间create_time是java.util.DateJackson 默认序列化出来的格式是yyyy-MM-ddTHH:mm:ss.SSSZ这种前端拿到的日期字符串和预期完全不一样。解决办法是在 Spring MVC 配置文件里配置 Jackson 的日期格式化mvc:annotation-driven mvc:message-converters register-defaultstrue bean classorg.springframework.http.converter.json.MappingJackson2HttpMessageConverter property nameobjectMapper bean classcom.fasterxml.jackson.databind.ObjectMapper property namedateFormat bean classjava.text.SimpleDateFormat constructor-arg valueyyyy-MM-dd HH:mm:ss/ /bean /property /bean /property /bean /mvc:message-converters /mvc:annotation-driven4.4 订单状态流转的页面呈现技巧订单状态从 0 到 4 是数字不能直接展示给用户看。JSP 里通过c:choose判断展示对应的状态文字和按钮这是最朴素也最可靠的方式c:choose c:when test${order.status 0} span classlabel label-warning待支付/span a href${pageContext.request.contextPath}/order/pay?id${order.id} classbtn btn-primary btn-xs去支付/a a href${pageContext.request.contextPath}/order/cancel?id${order.id} classbtn btn-default btn-xs取消订单/a /c:when c:when test${order.status 1} span classlabel label-info待接单/span /c:when c:when test${order.status 2} span classlabel label-primary配送中/span /c:when c:when test${order.status 3} span classlabel label-success已完成/span /c:when c:when test${order.status 4} span classlabel label-default已取消/span /c:when /c:choose这样做的好处是不管后端状态怎么扩展前端逻辑都清清楚楚。页面布局用 Bootstrap 的栅格系统简单整洁不需要前端基础多好就能写出能看的页面。5. 环境配置与调试部署全流程5.1 开发环境搭建清单标题里写了“开发环境”所以我把整套环境配置过程列出来踩的坑也一并说了。这个是 SSM 项目能不能跑起来的第一步也是最容易被低估的一步。软件版本建议安装注意事项JDKJDK 81.8不要装 JDK 17老项目大概率跑不起来Tomcat 版本不匹配Maven3.6.x配置settings.xml里的阿里云镜像不然下载依赖能急死人Tomcat8.5.x解压版即可不要用 Tomcat 10包名从javax变成了jakartaSSM 项目直接用不了MySQL5.7 或 8.0设置 root 密码记住字符集选utf8mb4IDEA任何较新版本安装 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 idaliyunmaven/id mirrorOfcentral/mirrorOf name阿里云公共仓库/name urlhttps://maven.aliyun.com/repository/central/url /mirrorpom.xml 里的核心依赖就这几个SSM 项目都是这一套不用多也不用少dependencies !-- Spring -- dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version5.3.23/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version5.3.23/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version5.3.23/version /dependency !-- MyBatis 与 适配器 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.13/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.7/version /dependency !-- MySQL 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.30/version /dependency !-- 连接池 -- dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.16/version /dependency /dependencies版本之间不用追求最新稳定最重要。Spring 5.3.x、MyBatis 3.5.x、MySQL 8.0.x 驱动这一套组合非常稳定比那些最新版本出现不兼容问题再排查半天的情况舒服多了。5.3 配置文件逐个拆解jdbc、Spring、SpringMVC、MyBatisSSM 项目的配置集中在src/main/resources目录下面大部分启动失败都出在这几个文件里。先说jdbc.propertiesjdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/community_order?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456community_order是数据库名根据实际建的库修改。URL 里的characterEncodingutf8一定要有否则数据库里中文全变问号。serverTimezoneAsia/Shanghai是 MySQL 8.0 的要求不加会报时区错误。如果用的是 MySQL 5.7驱动类是com.mysql.jdbc.Driver8.0 则是com.mysql.cj.jdbc.Driver多了个.cj。然后是spring-mybatis.xml重点是配置数据源、SqlSessionFactory 和 Mapper 扫描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/ property namemapperLocations valueclasspath:mapper/*.xml/ property nametypeAliasesPackage valuecom.xxx.entity/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.xxx.mapper/ /beanmapperLocations指定的是 MyBatis 的 XML 映射文件路径很多项目报 “Invalid bound statement (not found)” 错误就是因为这个地方没有扫描到 XML 文件。typeAliasesPackage是实体类包路径配了它之后XML 里写resultTypeUser就不用写全路径com.xxx.entity.User了。最后是spring-mvc.xml配置组件扫描、注解驱动和视图解析器context:component-scan base-packagecom.xxx.controller/ mvc:annotation-driven/ bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/jsp// property namesuffix value.jsp/ /beanInternalResourceViewResolver的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 explodedApplication 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-setutf8mb4 community_order community_order.sql6. 高频报错与排查技巧实录6.1 启动期三大报错速查表SSM 项目从启动到运行报错基本集中在启动期和运行期两个阶段我把最常见的整理成一张表报错现象根本原因解决方案ClassNotFoundException: javax.servlet.*Tomcat 版本太高10包名变了换 Tomcat 8.5 或 9.0Invalid bound statement (not found)Mapper XML 没有被扫描检查spring-mybatis.xml的mapperLocations路径Failed to configure a DataSourcejdbc.properties路径或参数错误检查数据库地址、账号、密码、驱动类Table xxx doesnt existSQL 脚本没导入或导错库确认数据库名重新导入脚本Access denied for user rootlocalhost数据库密码错误检查jdbc.properties的jdbc.passwordThere is no getter for property named xxx实体类属性和数据库字段对不上检查 MyBatis XML 里的resultMap或#{}参数The valid characters are defined in RFC 7230URL 里带了中文或特殊字符参数使用POST提交或前端encodeURIComponent编码6.2 运行期数据异常排查思路启动成功后运行阶段也有一堆奇怪问题。我挑几个典型的说说排查思路。问题一列表页能打开但是数据为空。先不要怀疑 SQL先用浏览器直接访问 Controller 的 URL或者看 IDEA 控制台的 SQL 日志SSM 项目可以在mybatis-config.xml里配置log-impl: STDOUT_LOGGING确认 SQL 语句真正执行了什么。很多时候是因为 Mapper XML 里的if testcategoryId ! null条件没有命中参数传进来是字符串null而不是null整个筛选条件没生效查出来了全表数据又被 JSP 的c:forEach遍历空对象搞得报错。问题二下单报错“事务回滚”但不知道哪一步失败。在submitOrder方法的每个数据库操作后面加日志或者直接注释掉事务注解逐排查。最常见的失败点是订单详情表的外键约束如果你建了外键或者order_id没有拿到——订单主表插入后order.getId()是null因为 MyBatis 插入后没有回填自增主键。解决方法是 XML 里的insert语句加两个属性insert idinsertOrder parameterTypeOrder useGeneratedKeystrue keyPropertyid问题三登录后跳回首页刷新一下又变游客了。这是 Session 作用域的管理问题。登录成功后要session.setAttribute(loginUser, user)而logout时要session.invalidate()或者session.removeAttribute(loginUser)。还有一种情况是web.xml里的session-config设置超时过短默认 30 分钟如果调试时反复重启 TomcatSession 会消失看起来像“登录失效”。问题四AJAX 请求 404。检查 Spring MVC 配置里有没有注解驱动mvc:annotation-driven/没有这个标签ResponseBody不生效Controller 会直接把返回结果当 JSP 路径找然后报 404。另外检查请求 URL 是否少了pageContext.request.contextPathJSP 页面里 AJAX 请求的 URL 一定要拼上项目上下文路径否则会请求到别的路径上去。6.3 我用过最顺手的排查工具组合排查 SSM 项目的日常工具我一般是“日志 浏览器开发者工具 数据库客户端”三件套。SSM 项目可以使用 log4j 或 logback 输出日志。在log4j.properties里把 Mapper 包的日志级别调到DEBUGlog4j.logger.com.xxx.mapperDEBUG这样 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 的掌握就比单纯跑通代码深入了一大截。这套系统的后续扩展空间也正在于此——它能成长的边界取决于你想在这个经典框架上继续走多远。