酒店客房送餐和餐厅点餐,听起来像是两个独立业务,但在实际运营里往往共用一套菜品库存、一套订单流水和同一个收银入口。最近我整理了一套基于Java SSM框架的酒店客房与餐饮点餐管理系统(项目编号90340),它把客房状态、菜品管理、在线点餐、订单跟踪全部串在了一个Web应用里。如果你正在找Java课程设计源码、准备用SSM写毕业设计,或者想看看经典三层架构在真实业务里怎么落地,这套系统的设计思路和实现细节可以直接参考。
1. 系统定位与整体设计思路
1.1 这个系统到底解决什么问题
传统酒店的点餐流程通常是这样:客人拿起房间里的纸质菜单,勾选菜品后打电话到前台或餐饮部,服务员手工记录,再送到后厨。这个过程有几个明显问题:一是有电话沟通就有听错、记漏的可能;二是高峰期多个房间同时点餐,前台根本忙不过来;三是订单数据没有沉淀,月底对账只能翻纸质单据。餐饮部那边,菜品库存、价格调整、上菜状态也全靠人来盯,效率非常低。
这套SSM点餐管理系统就是把这些手工流程搬到线上。客人可以在客房内浏览菜品、加购物车、提交订单;后厨能看到实时订单并按状态推进;前台和管理员则通过后台管理客房房态、菜品价格、订单流水和用户数据。系统跑起来之后,整个点餐链路从“电话+纸笔”变成“页面+数据库”,数据实时同步,每一笔订单都有记录可查,对账和统计自然就轻松了。
对做课程设计或毕业设计的同学来说,这个系统还有一个额外价值:它覆盖了SSM框架最常见的功能点——基础的CRUD、关联查询、登录拦截、文件上传、分页搜索、事务控制。把这些模块真正跑通,比背多少面试题都管用,面试官问起来也有实际项目可以讲。
1.2 为什么选SSM而不是Spring Boot
现在很多人一上来就用Spring Boot,因为启动快、配置少、社区资料多。但作为课程设计和毕业设计,SSM反而是一个更合适的选择。原因有几个:首先,很多学校课程大纲还是以SSM为主线,Spring、SpringMVC、MyBatis三件套是教学标配,课程作业验收也默认这套架构;其次,SSM要求你手写每个配置——数据源、事务管理器、Mapper扫描、视图解析器,这些配置写一遍,你对Spring容器和MyBatis执行原理的理解会深很多。Spring Boot把这些全自动了,用了半年你都不知道底层是怎么装配的。
当然,SSM也有它的缺点:配置繁琐、版本兼容问题多、环境搭建耗时。从时间成本看,搭建一个可运行的SSM项目骨架大概需要半天到一天,Spring Boot可能半小时就够了。如果你是纯粹为了快速出成果,Spring Boot确实省事;但如果你需要的是一个能讲清楚原理、能应对答辩追问的项目,SSM这种“什么都靠手写”的方式反而是加分项。
提示:做技术选型时先问清楚你的目标。课程设计/毕业设计选SSM,是奔着原理学习和满足教学要求去的;自己练手或做落地小项目,Spring Boot的工程效率更高。这两者没有绝对的好坏,关键是匹配场景。
1.3 功能模块拆分
整个系统按照角色和业务可以拆成三大块:前台用户端、后台管理端、公共支撑模块。前台用户端面向普通住客,功能包括账号登录注册、客房信息浏览、菜品分类展示、购物车管理、订单提交与查询;后台管理端面向酒店工作人员和管理员,包括客房管理、菜品管理、订单管理、用户管理和数据统计。
公共支撑模块是指登录拦截、异常处理、分页组件、统一返回格式这些横切逻辑。它们不直接对用户呈现,但决定了系统的稳定性和可维护性。比如登录拦截器,如果没有它,任何游客都能直接访问后台接口,那系统基本等于裸奔。下面我用表格把模块和核心页面罗列一下,后面编码时就是照着这个清单去落地的。
| 模块 | 核心功能 | 对应页面/接口 |
|---|---|---|
| 用户模块 | 注册、登录、退出、个人信息 | login.jsp / register.jsp / UserController |
| 客房模块 | 房型查询、房价展示、房态变更 | roomList.jsp / RoomController |
| 菜品模块 | 菜品分类展示、菜品详情、菜品管理 | dishList.jsp / DishController |
| 购物车模块 | 加入购物车、修改数量、清空 | CartController |
| 订单模块 | 提交订单、订单列表、状态流转 | orderConfirm.jsp / OrderController |
| 后台管理 | 客房CRUD、菜品CRUD、订单管理 | admin/ 下各JSP页面 |
每个模块的边界都在Controller层体现,一个Controller对应一个核心资源,Service层做业务编排,Mapper层做数据访问。这样分层清晰,出问题也容易定位,不会出现改订单功能时误伤客房模块的情况。
2. 数据库设计与核心表结构
2.1 客房模块的表设计
客房模块最核心的就是两张表:room_type和room。room_type存房型信息,比如大床房、双床房、套房;room存具体的房间,比如“307”就是一间属于“标准双床房”这个类型的实体房间。之所以把房型和房间拆成两张表,是为了避免重复存储。如果30个房间都是标准双床房,每个房间都存一遍“标准双床房、面积28平米、床型1.5米双床”,数据冗余会很严重,改一个属性还要改30条记录。
room_type表通常包含type_name、bed_type、area、price、max_people、description这些字段;room表则包含room_number、type_id、floor、status。room表的type_id是外键,关联room_type表的主键id。查询房态时只需要JOIN一下就能拿到完整信息。
room表的status字段用来表示房间当前状态,一般取值有空闲、已入住、打扫中。这个状态会和订单、入住登记联动:客人入住时房间状态从“空闲”变成“已入住”,退房后变成“打扫中”,保洁完成再改回“空闲”。这套状态机很简单,但很实用,直接决定了前台能不能接受新订单。如果某个房间处于“打扫中”状态,点餐功能就不应该给这个房间分配新订单,不然餐送到了房间还没收拾好,非常尴尬。
2.2 菜品与订单模块的表设计
菜品模块也是两张表:category和dish。category是菜品分类,比如凉菜、热菜、主食、饮品;dish是具体菜品,包含dish_name、price、image、description、category_id、status。status字段在这里表示上架状态,0代表下架、1代表上架。点餐页面只查status=1的菜品,下架的菜不会出现在菜单里,这样在做菜品调整时不用动前端页面。
菜品图片字段建议存相对路径,比如/upload/dish_001.jpg,而不是直接把图片的二进制数据存进数据库。图片文件上传到服务器的指定目录,数据库只存路径,查询时通过前端拼接完整地址访问。这样既减小了数据库体积,也让图片的更新变得简单——覆盖同名文件即可,不用重新上传一张就产生一条新记录。
订单模块是这套系统里表结构最复杂的部分,至少要拆成两张表:orders和order_item。orders是订单主表,记录订单整体信息,包括订单编号、下单人、房间号、总金额、下单时间、订单状态;order_item是订单明细表,记录订单中包含的每一项菜品,包括菜品ID、菜品名称、购买数量、单价、小计金额。之所以要拆表,是因为一个订单可能包含多个菜品,如果只在orders表里存菜品ID列表,查询和统计都会非常痛苦。想查“哪个菜卖得最好”,就需要遍历每个订单里的菜品列表去做字符串拆分,这在SQL里几乎没法高效实现。
设计订单编号时有个常用技巧:直接用时间戳加随机数生成,比如202405212030456789,这样既保证唯一性,又能在日志和页面里直观看到下单时间。不要用数据库自增id直接当订单编号展示给用户,因为这样会暴露订单量,而且不便于后续做订单号规则扩展。
2.3 购物车与订单状态流转
购物车模块是点餐业务里比较有意思的部分。购物车本质上是“未提交的订单草稿”,它的数据可以放在Session里,不需要单独建表。用户把菜品加入购物车、调整数量、删除菜品,操作的都是Session中的Map结构,key是菜品id,value是数量和单价等信息。这样做的好处是简单、无状态,用户关闭浏览器购物车自然清空,不用清理数据库垃圾数据。如果酒店对购物车持久化有要求,比如客人断网重新登录后还能看到之前的购物车内容,那才需要另外建一张cart表,用userId关联。对于课程设计来说,Session方案已经完全够用。
订单状态流转是系统的核心业务逻辑。我建议用整数字段status表示订单状态,定义一套明确的取值规则:0待支付、1已支付待制作、2制作中、3已上菜、4已完成、5已取消。后端和前端都要维护同一套状态值,前端页面按状态值显示不同的按钮和文案,后端在修改状态时校验状态转换的合法性,比如已取消的订单不能直接变成已完成。
注意:订单状态的修改必须走Service层的统一方法,不能在各处的Controller里直接update。这样状态校验逻辑就集中在同一个地方,避免出现“订单还在待支付状态却显示已上菜”这种低级事故。
3. SSM框架整合与核心配置
3.1 环境准备与项目骨架
开发这套系统我用的环境是:JDK 1.8、Tomcat 8.5、Maven 3.6、MySQL 5.7,IDE是IDEA。这几个版本组合是SSM项目里最稳的一组,JDK 8和MySQL 5.7的兼容性非常好,遇到问题网上也最容易搜到解决方案。如果你用JDK 17配老版本Tomcat,光环境问题就能折腾你两天。
创建项目时用Maven的webapp骨架,或者直接在IDEA里新建Maven工程然后手动添加web目录。groupId一般写成自己域名反写,artifactId用项目名,我这里用的是com.lesson的包名,你按自己的习惯来。注意打包方式必须是war,因为要部署到Tomcat运行。pom.xml里需要引入spring-webmvc、mybatis、mybatis-spring、mysql-connector-java、druid、jstl、servlet-api等依赖,servlet-api和jsp-api的依赖范围要配置成provided,否则打war包时会和Tomcat自带的类起冲突。
项目目录结构按照Maven的规范来:src/main/java放Java源码,src/main/resources放配置文件,src/main/webapp放JSP页面和静态资源。Java包结构上,我习惯分成controller、service、mapper、pojo、interceptor这几个包,如果项目再大一点可以再加dto、vo、common、utils这些子包。包结构的清晰程度直接决定了后期维护效率,千万不要把所有类都堆在默认包下。
3.2 Spring、SpringMVC、MyBatis配置文件要点
SSM整合的关键在于三个配置文件的配合:applicationContext.xml是Spring的核心配置,spring-mvc.xml是SpringMVC的配置,mybatis-config.xml是MyBatis的全局配置。web.xml负责把它们串起来,并注册DispatcherServlet和编码过滤器。
applicationContext.xml中最重要的配置是数据源和事务管理器。数据源我用的是Druid,它自带连接池监控,配置c3p0或者dbcp也可以,但Druid在排查慢SQL时方便很多。数据源里driverClassName、url、username、password四项必须配对,url要加上useUnicode=true&characterEncoding=utf-8参数,否则中文写入数据库会变成问号。事务管理器使用DataSourceTransactionManager,然后通过tx:annotation-driven开启注解事务,后续在Service层加@Transactional就能声明事务边界。
spring-mvc.xml的配置重点是注解驱动、静态资源放行和视图解析器。注解驱动开启后,RequestMapping才能生效。SSM搭建早期最常踩的坑是静态资源被DispatcherServlet拦截,导致CSS、JS加载不出来,所以在spring-mvc.xml里必须加mvc:resources配置,把css、js、upload这些目录放行。视图解析器用InternalResourceViewResolver,prefix指向/WEB-INF/views/,suffix是.jsp,这样Controller返回的字符串会拼接成JSP路径。
mybatis-config.xml配置量不大,但下划线转驼峰配置很关键。数据库字段一般用下划线命名,比如room_number,而Java属性是驼峰命名roomNumber,开启mapUnderscoreToCamelCase后MyBatis会自动完成映射,不用写一堆resultMap。另外还可以配置懒加载和日志功能,但懒加载在简单项目里不建议开,容易引发延迟加载异常,日志功能用SLF4J的标准配置就行。
提示:三个配置文件的加载顺序有讲究。web.xml里先加载applicationContext.xml,再加载spring-mvc.xml。Spring容器和SpringMVC容器是父子容器关系,SpringMVC容器可以访问Spring容器的Bean,反过来不行。如果在Controller里注入Service失败,先检查Service是不是在Spring容器里注册了。
3.3 三层架构的代码组织方式
拿到一个需求,先判断它属于哪个Controller,再把想清楚的业务逻辑放到Service层,最后在Mapper里写对应的SQL。Controller层只做三件事:接收参数、调用Service、返回视图或JSON。业务逻辑、事务控制、状态校验全部在Service层。Mapper层则是纯粹的SQL映射,不写任何业务判断。
写Mapper有两种方式:写XML文件和用注解。简单项目用注解@Select、@Insert确实省事,但SQL稍微复杂一点,比如多表JOIN、动态条件查询,注解就会变得很难读。我更推荐XML方式的Mapper,把SQL集中放在resources下的mapper目录里,SQL和Java代码完全分离,排查问题的时候直接打开XML就能看到完整SQL,比翻注解直观得多。
Service接口和实现类的选择上,如果你是教学作业或毕业设计,建议按标准套路写接口+实现类。这样做的好处是符合课程评分规范,也方便后续用JDK动态代理扩展功能。如果项目是你自己练手,直接类上加@Service也行,少写一套接口文件会清爽很多,但答辩时评委可能会问“为什么没有接口层”,这就是你自己需要权衡的问题。
注意:Controller返回JSON时,如果返回的是Java对象,必须加上@ResponseBody注解。SSM项目里忘记加这个注解是高频错误,不加的话Spring会把对象当视图名去找对应JSP,一运行就报404。
4. 核心功能实现细节
4.1 用户登录与权限拦截
用户模块是系统的基础功能,登录逻辑本身不复杂:从前端拿到用户名和密码,密码做一次MD5加密后和数据库里的密文比对,匹配成功就把用户信息存入Session,同时把用户ID和角色ID也放进去。为什么要加密存储?因为数据库一旦泄露,明文密码就是灾难性的。MD5虽然不够强,但对于课程设计来说够用了,你可以在日志里提醒自己真实项目要用BCrypt加盐。
角色区分采用简单的字段标记:用户表里加一个role字段,0表示管理员,1表示普通住客。这样登录后就能区分用户身份,不同角色看到的菜单和能访问的页面不一样。权限控制通过拦截器实现,我写了一个LoginInterceptor,在preHandle方法里检查Session中是否有用户对象,没有就重定向到登录页,同时放行登录和注册相关接口。
拦截器配置在spring-mvc.xml里,通过mvc:interceptors注册。拦截路径一般配置为/,表示拦截所有请求,然后在校验逻辑里排除登录页、登录接口、注册页和静态资源路径。这里有个坑:静态资源虽然已经在mvc:resources中放行了,但如果拦截器也拦截了/,静态资源请求还是会先进拦截器,所以要在拦截器的excludePath里再次排除静态资源目录,或者把拦截路径写得更细化一些。
4.2 客房点餐完整流程
整个系统最核心的流程就是点餐。一般酒店的点餐场景有两种:住客在客房通过线上页面点餐,以及住客在餐厅堂食时点餐。这套系统把两条路径统一成一条:选择房号或桌号,浏览菜品,加入购物车,确认订单,支付,后厨接单,制作,上菜。流程打通之后,无论哪个入口来的订单,后台看到的都是统一格式的订单列表。
具体到代码实现,点餐链路的主要环节是这样的:
用户浏览菜品列表。Controller里写一个list方法,接收分类ID作为参数,调用Service查询菜品列表并返回给页面。菜品列表页用JSTL的forEach标签循环渲染,每条菜品信息放在一个卡片里,显示图片、名称、价格和一个“加入购物车”按钮。
加入购物车。这是一个Ajax请求,前端把菜品ID通过POST传给CartController的add方法。后端拿到菜品ID后,从Session中取出购物车Map,如果菜品已经存在就数量加1,否则放入新条目。操作完成后返回JSON,前端提示“已加入购物车”。
查看购物车并确认订单。用户点击“去结算”时,页面展示购物车里的全部条目、总金额、选择配送的房间号。确认无误后提交订单,后端在OrderService里开启一个事务:先插入订单主表记录,再遍历购物车逐项插入订单明细表,最后扣减菜品库存、清空购物车。
后厨处理订单。管理员或后厨登录后台后,在订单管理页面能看到新订单。点击“开始制作”把订单状态从1改成2,制作完成点击“上菜”改成3,客人确认后改成4完成。每一步操作都更新status字段,并在页面上显示当前状态。
@Transactional public void createOrder(OrderDTO dto, HttpSession session) { // 1. 生成订单号并组装订单主表数据 Orders order = new Orders(); order.setOrderNo(generateOrderNo()); order.setUserId(dto.getUserId()); order.setRoomId(dto.getRoomId()); order.setTotalAmount(dto.getTotalAmount()); order.setStatus(0); orderMapper.insert(order); // 2. 遍历购物车明细,写入订单明细表 for (CartItem item : dto.getCartItems()) { OrderItem detail = new OrderItem(); detail.setOrderId(order.getId()); detail.setDishId(item.getDishId()); detail.setDishName(item.getDishName()); detail.setPrice(item.getPrice()); detail.setQuantity(item.getQuantity()); detail.setSubTotal(item.getPrice() * item.getQuantity()); orderItemMapper.insert(detail); // 3. 扣减菜品库存 dishMapper.deductStock(item.getDishId(), item.getQuantity()); } // 4. 清空Session中的购物车 session.removeAttribute("cart"); }这段代码看上去简单,但有两个细节要注意。第一,@Transactional注解必须生效,它保证订单主表、明细表、库存扣减要么全部成功要么全部回滚,否则就会出现“订单主表有记录但明细表没数据”这种脏数据。第二,库存扣减应该在应用层判断库存是否充足,如果扣减时库存不足直接抛出异常,触发事务回滚,前端就能收到下单失败的提示。
4.3 后台菜品与订单管理
后台管理模块面向管理员,核心是菜品的增删改查和订单管理。菜品新增需要处理图片上传。一般做法是:前端表单提交菜品信息和图片文件,后端用MultipartFile接收文件,把文件写入服务器磁盘上的upload目录,文件名用时间戳加随机数生成,避免重复,然后把拼接好的相对路径存到数据库。文件保存时要注意目录的物理路径,IDEA里运行Tomcat时上传到相对路径,部署到实际服务器后要改用绝对路径或配置化路径,否则会出现图片写到了别的目录、页面加载不出来的问题。
菜品编辑的逻辑和新增类似,但要注意更新时是否换了新图片。如果没换图片,就直接更新文本字段;如果换了图片,需要覆盖旧文件或先删除旧文件再保存新文件。删除菜品时如果不是物理删除,可以用逻辑删除:把status改成0表示下架,这样历史订单里的菜品记录不受影响,也避免了外键关联删除的麻烦。
订单管理页面是后台使用频率最高的页面。列表默认按下单时间倒序排列,每条订单显示订单号、房号、用户名、菜品数量、总金额、状态和操作按钮。状态按钮根据当前状态动态生成:待支付的可以“取消”,已支付的可以“开始制作”,制作中的可以“上菜”。这个动态按钮逻辑在前端JSP里用c:if标签判断status值渲染,后端则提供一个updateStatus接口,接口里做状态合法性校验,防止倒流。
4.4 分页查询与模糊搜索
后台列表页越到后期数据量越大,不分页的话一次查出几百条记录,页面渲染会明显卡顿,数据库压力也不小。SSM项目最常用的分页方案是PageHelper插件,使用非常简洁:在查询前调用PageHelper.startPage(pageNum, pageSize),紧接着的第一次MyBatis查询就自动带上LIMIT,并返回Page对象,同时通过PageInfo包装后,可以在页面上拿到总记录数、总页数、当前页等分页信息。
PageHelper的使用有一个需要注意的细节:startPage之后的下一步必须紧跟要分页的查询语句,中间不能插入任何其他MyBatis操作。如果中间多执行了一条查询,分页拦截器会作用在那条SQL上,分页就乱了。我在项目里就在这个坑上花过时间——在startPage和查询之间加了个日志记录操作,结果分页数量对不上。
模糊搜索常和分页配合使用。比如菜品管理页,用户输入关键词,后台根据keyword模糊查询dish_name。SQL用LIKE '%keyword%',但要注意SQL注入风险:使用MyBatis时用#{}占位符而不是${}拼接。下面是一个分页+关键词搜索的Mapper示例:
<select id="searchDishes" resultType="com.lesson.pojo.Dish"> SELECT * FROM dish <where> <if test="keyword != null and keyword != ''"> dish_name LIKE CONCAT('%', #{keyword}, '%') </if> </where> ORDER BY id DESC </select>这里为什么要用CONCAT('%', #{keyword}, '%')而不是直接写'%#{keyword}%'?因为#{}在字符串内部会被MyBatis当作普通字符串处理,不会进行参数替换。写成'%#{keyword}%'时,keyword永远是不变的字面量,搜索功能就废了。用CONCAT拼参数是最规范的做法。
5. 踩坑记录与排查思路
5.1 中文乱码问题
SSM项目里的中文乱码,我基本每次搭建都要遇到一次,原因一般出在三个环节。第一个是请求乱码:表单提交中文到后端,Controller收到的值变成问号。解决办法是在web.xml里配置CharacterEncodingFilter,强制请求编码为UTF-8,注意这个过滤器必须注册在DispatcherServlet之前,否则不生效。第二个是响应乱码:Controller返回JSON或字符串时前端显示乱码,需要在spring-mvc.xml的注解驱动里配置消息转换器,或者直接在每个返回JSON的方法上指定produces = "application/json;charset=UTF-8"。第三个是数据库乱码:数据写入MySQL后中文变成问号或乱码,检查数据库连接URL是否加了characterEncoding=utf-8,同时确认表结构和连接的字符集是utf8mb4。
排查乱码问题时可以分三步走:先在JSP页面顶部检查pageEncoding,再检查web.xml的过滤器,最后看数据库连接参数。大部分乱码问题都逃不出这三步。还要注意一点,IDEA的Tomcat启动配置里,VM options加上-Dfile.encoding=UTF-8,能避免控制台日志乱码,这个细节很多人会忽略。
5.2 数据库连接池配置
我最初用C3P0,后来换成Druid,原因是Druid自带监控页面和慢SQL日志,排查起来方便很多。Druid的配置相对简单,核心参数是initialSize、minIdle、maxActive这几个。在这里踩过的坑是maxActive配太大。有一次我配了100,本意是提高并发能力,结果数据库连接数一下子被占满,其他服务连不上数据库,整个数据库被拖垮。合理值一般设在20到50之间,配合minIdle和maxWait使用,够用就好。
连接池另一个常见问题是连接失效。MySQL默认的wait_timeout是8小时,超过这个时间空闲连接会被服务端关闭,但连接池不知道,下次取出这个连接执行SQL时会报Communications link failure。解决办法是设置druid的testWhileIdle参数为true,并配置validationQuery为SELECT 1,这样每次取出连接前做一次探活测试,失效连接会被丢弃并重新建立。
碰到数据库连接异常时,别急着改代码。先确认MySQL服务是否启动,端口3306是否被占用,账号密码是否正确,网络是否能通。用Navicat或命令行直接连接一下数据库,如果能连上,问题就在应用配置;连不上,就是数据库本身或网络出了状况。这种排查思路比盲改配置高效得多。
5.3 事务不生效
@Transactional注解配了但事务不生效,是我被问得最多的问题之一。最常见的原因有三个。第一,Spring容器没有开启注解事务驱动,applicationContext.xml里缺少tx:annotation-driven配置;第二,事务管理器没有注入数据源,DataSourceTransactionManager的dataSource属性没配置或配错;第三,事务方法不是public的,或者Service类没有被Spring扫描到。
还有一个容易被忽略的点:在SSM的父子容器结构里,事务如果配置在applicationContext.xml(Spring容器)中,那它只能对Service层的Bean生效,因为Service在Spring容器里注册。如果你把@Transactional加在Controller层的方法上,而Controller是在SpringMVC容器里注册的,事务配置就不在它的管辖范围内,注解自然不会生效。
排查事务问题时,最简单的验证方法是故意让事务方法中途抛异常,看数据库里前面的操作是否被回滚。如果没回滚,就按上面三个原因逐一排查:先看配置、再看扫描包、最后看方法修饰符。事务不生效的坑通常藏得很深,但有了这个排查顺序,花十分钟就能定位。
5.4 前端传参格式问题
前后端联调时,最常出现“后端接收不到参数”的问题。做Ajax请求时,如果前端用JSON.stringify把对象转成了JSON字符串,而后端用的是普通的@RequestParam接收,那肯定是收不到值的,因为@RequestParam期望的是表单数据格式。解决办法有两个:一是后端参数加@RequestBody,改用JSON格式接收;二是前端不要JSON.stringify,直接把数据序列化成普通表单字段。
还有一个坑就是日期和数字格式。SpringMVC默认的日期转换格式是yyyy/MM/dd,如果前端传的是yyyy-MM-dd,后端接收日期类型时会报400错误。解决办法是在POJO的日期字段上加上@DateTimeFormat(pattern = "yyyy-MM-dd")注解,或者在处理器中注册全局的CustomDateEditor。数字类型同理,前端传了带小数的字符串给Integer字段,后端会转换失败,联调时要先确认字段类型匹配。
排查传参问题时,我习惯在Controller方法第一行先用@RequestParam逐个打印接收到的参数,再和后端期望的格式比对。这样能快速定位是参数名不匹配、类型不匹配,还是格式不匹配。等逻辑稳定后再把打印去掉,减少日志噪音。
5.5 部署打包注意事项
SSM项目最终要部署到Tomcat。IDEA里直接用Tomcat运行是调试阶段,但最终交付课程设计或毕设,通常需要打好war包让评委自己部署。用Maven的package命令打包,输出target目录下的war文件,把war放到Tomcat的webapps目录,启动Tomcat后会自动解压部署。这里有几个注意点。
第一,如果项目里面用了外部依赖,确保pom.xml里依赖都完整,否则打出来的war缺包,部署后启动直接报ClassNotFoundException。第二,数据库连接配置写在applicationContext.xml里,打包后如果换了机器,数据库地址、账号密码都要改。为了避免每次打包都要改配置,可以在启动时通过-D参数或者jdbc.properties外置的方式动态加载。更简单一点的做法是直接编辑war包里解压出来的配置文件再重启。第三,MySQL版本兼容。如果开发环境是MySQL 5.7,部署机器是MySQL 8.0,连接驱动的版本要升级,数据库URL也需要调整,比如driverClassName从com.mysql.jdbc.Driver改成com.mysql.cj.jdbc.Driver,并保证MySQL Connector/J的版本在8.x以上。第四,端口和上下文路径。默认Tomcat是8080端口,war名的上下文路径就是war包名,比如ROOT.war部署后访问http://localhost:8080/,如果叫ssm_hotel.war就访问http://localhost:8080/ssm_hotel/。访问不了时第一反应检查war包是否成功解压、Tomcat日志有没有报错。
这套系统我完整跑过一遍之后,最大的体会是:SSM框架本身不难,真正花时间的是把业务闭环想清楚。比如点餐流程里状态流转的顺序、库存扣减的时机、购物车与订单的关系,这些都是在动手写代码之前就应该在脑子里画清楚的。如果只照着教程敲CRUD,系统看似能跑,但只要业务稍微复杂一点就会露馅。最后再分享一个小技巧:给订单表加一个创建时间索引,按时间倒序查订单的SQL会快很多;另外把订单状态值的定义写成一个Java常量类,页面和Service都引用这个类,就不会出现状态值对不上的情况。