SSM框架实战:甜品交易系统设计与核心配置解析
做了这么多年 Java 开发也带过不少实习生每年毕业季总有人来问我“SSM 项目到底该做什么”。说实话与其纠结到底做什么不如直接选一个业务闭环完整、能覆盖增删改查、又不会复杂到做不完的题目。我给大多数人推荐过“甜品交易系统”自己也完整实现过一版今天就把整个思路、表设计、配置和踩坑经历拆开讲清楚。这篇内容适合正在做毕业设计、或者想通过手写一个 SSM 项目来巩固 Spring、Spring MVC、MyBatis 知识的人我会尽量把每一步的“为什么”也说透而不是只给你一堆可以抄的代码。1. 为什么“甜品交易系统”最适合用来吃透 SSM 框架1.1 交易类项目是 SSM 三板斧的最佳练兵场很多同学一上来就想做“仿天猫商城”“仿京东”结果被复杂的权限、分布式库存、秒杀一类的东西直接劝退。SSM 这种轻量级组合本质上是解决三层架构的落地问题Spring 管对象、Spring MVC 管请求转发、MyBatis 管数据库访问。甜品交易系统恰恰把这三层都占全了。用户登录注册、收货地址管理考验的是 Spring MVC 的表单绑定和会话管理。甜品分类、商品列表、搜索分页考验的是 MyBatis 的动态 SQL 和分页逻辑。购物车、下单、订单列表考验的是 Spring 的声明式事务。后台管理端考验的是增删改查的基本功和前端表格的对接。这套业务规模不大不小既不会像学生信息管理系统那样只有单一实体、显得单薄又不会像大型电商那样让人无从下手。做完它你对 SSM 框架的理解不会停留在“会配置”而是真正知道一个请求是如何从浏览器一路走进数据库再返回页面的。1.2 这个项目的真实定位它不是玩具而是一套完整闭环我见过很多毕设项目表面上是“XX系统”实际上只有列表和新增两个页面。甜品交易系统如果认真做可以形成完整闭环访客浏览甜品 → 注册登录 → 加入购物车 → 提交订单 → 管理员接单发货 → 用户确认收货。每一步都有独立页面、独立 Controller、独立 Service 方法环环相扣这才是“系统”而不是“页面集合”。我在项目里还会额外加一个后台数据统计的简单的销售额排行页面其实也就是几条 SQL 查询加一个表格但答辩的时候一演示效果完全不一样。它能告诉评委你不是只会照着课件做 CRUD而是有产品思维。1.3 适合上手的用户角色划分这个系统至少要有两类角色前台用户注册、登录、浏览甜品、搜索、加入购物车、提交订单、查看个人订单。后台管理员登录后台、管理甜品分类、管理甜品信息、处理订单状态、管理用户状态。如果精力充足还可以加一个“店长”角色负责查看统计报表但通常情况下两类角色足够。角色控制不需要引入复杂的 Shiro 或 Spring Security用 Spring MVC 拦截器判断 session 里的 role 字段就能搞定等后面有余力再替换成安全框架。2. 把“卖甜品”翻译成数据库表核心表设计思路2.1 表结构全景从用户、甜品到订单数据库是整个系统的地基。我的建议是先画清楚表关系再动手写代码否则后期改表要连带着改 Mapper、Service、Controller简直想哭。甜品交易系统最少需要以下六张表表名作用关键字段t_user用户表id、username、password、real_name、phone、address、role、statust_category甜品分类表id、name、sort_ordert_dessert甜品信息表id、category_id、name、description、price、stock、image、sales、statust_cart购物车表id、user_id、dessert_id、quantityt_order订单表id、order_no、user_id、total_price、status、create_time、pay_time、deliver_timet_order_item订单明细表id、order_id、dessert_id、dessert_name、price、quantity其中t_order和t_order_item是一对多关系这是交易系统里非常关键的设计。一个订单可能包含多个甜品如果只存一个字段肯定不够如果全部塞在订单表里又会产生大量冗余。把订单头信息和订单明细分开后续统计销量、生成对账单、退款拆分都会方便很多。2.2 订单状态设计的细节关于“购物车如何落库”很多第一次做交易系统的同学会很困惑购物车表为什么还要单独建直接每次下单临时组装不就行了购物车表的价值在于用户可能七天前加了一块提拉米苏今天才来结账。如果购物车数据不持久化一旦 session 过期或者用户换了浏览器购物车就空了体验很差。所以我们用t_cart表把“用户与甜品”的关系存储下来user_id和dessert_id联合起来就代表一条购买意向。订单状态我建议用数字字典维护而不是直接在代码里到处写死字符串0待付款1待发货已付款2待收货已发货3已完成4已取消5退款中这个状态机很简单但它是整个交易流程的中枢。实测下来最容易出 Bug 的地方不是状态判断而是“状态流转过程中用户重复提交”。比如用户点击下单后网络卡顿他会忍不住再点一次如果不做幂等处理就会生成两笔一模一样的订单。解决方式有两种前端按钮置灰 后端校验下单间隔。后端校验可以用“同一用户在 5 秒内重复提交同样金额的订单直接拦截”也可以在下单接口里先查一下是否存在相同user_id且状态为“待付款”的订单存在则返回原订单号。2.3 设计时容易忽视的字段约束建表时如果只盯着主键和外键后期会遇到一堆坑。price字段不要用double要用decimal(10,2)否则金额会出现 0.1 0.2 0.30000000000000004 的尴尬。库存stock字段要加unsigned约束并且更新库存的 SQL 要写成update t_dessert set stock stock - #{quantity} where id #{dessertId} and stock #{quantity}这样在并发下单时不容易出现超卖。用户密码不能明文存储至少要基于 BCrypt 做哈希。如果只是毕设也可以用 MD5 加盐但要在论文里说明这不适合生产环境。order_no订单号建议用“时间戳 用户ID 随机数”拼成一个唯一字符串不要用自增 ID 当订单号否则用户很容易猜到销量。3. 从零搭建 SSM 骨架配置文件是最大的坑3.1 项目结构和依赖版本选择如果你还在用 Eclipse 创建 Web Project然后手动去 lib 目录里找 jar 包我建议停一停。SSM 虽然老但依然可以用 Maven 管理依赖清爽得多。项目结构我采用的是标准 Maven war 包结构src/main/java ├── com.sweet.controller // 前台与后台的控制器 ├── com.sweet.service // 业务接口 ├── com.sweet.service.impl // 业务实现 ├── com.sweet.dao // MyBatis Mapper 接口 ├── com.sweet.entity // 实体类 ├── com.sweet.interceptor // 登录/权限拦截器 └── com.sweet.common // 工具类和常量类 src/main/resources ├── jdbc.properties ├── spring-context.xml // Spring 核心配置 ├── spring-mvc.xml // Spring MVC 配置 └── mybatis-config.xml // MyBatis 配置 src/main/webapp ├── WEB-INF/web.xml ├── static // css、js、图片 └── jsp // 前台和后台页面关于版本我实测下来比较稳的组合是Spring 5.2.xSpring MVC 5.2.xMyBatis 3.5.xMyBatis-Spring 2.0.x数据库连接池用 Alibaba Druid 1.1.x。Java 8 或 Java 11 都可以不要追求最新版本毕设项目稳定第一。3.2 Spring、Spring MVC、MyBatis 三类配置文件的职责边界很多初次接触 SSM 的同学会把配置全写在一个文件里结果项目一启动就报ClassNotFoundException或者BeanCreationException排查半天也找不出原因。我始终建议把 Spring 容器和 Spring MVC 容器分开配置这是 Spring 官方推荐的方式也是减少混乱的关键。spring-context.xml负责扫描com.sweet.service、com.sweet.dao配置数据源、事务管理器、SqlSessionFactoryBean、MapperScannerConfigurer也就是“后端三层”相关的东西。spring-mvc.xml负责扫描com.sweet.controller配置注解驱动、视图解析器、静态资源映射和拦截器。它只管 Web 层。这样区分的好处是职责清晰Service 和 DAO 是业务底座Controller 是请求入口。如果你让两个容器重复扫描同一个包可能会出现事务失效、AOP 代理混乱等问题最典型的现象就是 Controller 里注入的 Service 虽然不为 null但方法上的Transactional不生效。3.3 web.xml 和注解驱动让请求正确敲门web.xml是 SSM 项目的总入口。我建议使用 Servlet 3.0 标准不需要写繁琐的 XML schema 版本信息只需要注册两个关键组件ContextLoaderListener启动时加载 Spring 根容器。DispatcherServlet加载 Spring MVC 容器并设置url-pattern为/这样可以接管所有非静态资源的请求。说到静态资源这里有个经典坑如果DispatcherServlet拦截了/那么.js、.css、图片也会被 Controller 处理器扫描到结果找不到映射直接 404。解决方式是在spring-mvc.xml里加一句mvc:resources location/static/ mapping/static/** / mvc:annotation-driven /还要给 Controller 类加上Controller、方法上加RequestMapping并且开启组件扫描否则 Spring MVC 根本看不到这些类。4. 核心交易链路从“浏览甜品”到“提交订单”4.1 用户登录与拦截器设计登录逻辑看起来很简单就是在UserController里接收username和password查询数据库核对密码然后往session里放入user对象。但实际落地时要注意三个细节密码比对不能在前端做不能只是if(password.equals(user.getPassword()))因为数据库里存的应该是加密后的密文。登录成功后要把原始密码字段置空避免把敏感信息带到页面上。前台用户和管理员的 session 标志要区分。我是在User实体里加了一个role字段前台用户为1管理员为2。拦截器是整个访问控制的枢纽。我在spring-mvc.xml里配置了两个拦截路径前台配置/user/**、/cart/**、/order/**要求必须登录才能访问。后台配置/admin/**要求登录用户的role必须是 2。拦截器的处理逻辑就是检查 session 中是否存在loginUser不存在就跳转到登录页。注意不要拦截/login、/register、/dessert/**这些公开资源否则用户连商品都看不到了。4.2 商品列表的分页与条件检索甜品展示页面是用户接触系统的第一界面如果直接select * from t_dessert然后把所有数据塞到页面上数据少还行数据一多页面会加载很慢也不利于答辩演示。我使用PageHelper这个分页插件它的原理是拦截 MyBatis 的执行过程在 SQL 后面自动拼接LIMIT语句。用法非常简单PageHelper.startPage(pageNum, pageSize); ListDessert dessertList dessertMapper.selectByCondition(name, categoryId); PageInfoDessert pageInfo new PageInfo(dessertList);这里有个极其容易踩的坑PageHelper.startPage()之后必须紧跟一条 Mapper 查询中间不能有任何其他 SQL 操作否则分页会失效。我见过有人在这两行之间打印日志结果日志里意外触发了一次数据库查询分页就跑到别的查询上去了。4.3 购物车模块cookie和数据库的选择购物车有两种实现路线存 cookie 或者存数据库。纯 cookie 方案适合未登录状态但用户换浏览器就丢数据而且 cookie 有 4KB 大小限制装不了太多商品。我的系统里直接采用了“登录后存数据库”的方案让购物车和用户强绑定。购物车添加的核心逻辑是先根据user_id和dessert_id查询t_cart如果记录存在就把quantity加一如果不存在就新建一条记录。这样不会出现同一个甜品在购物车里有三行重复记录。购物车列表展示时需要和内连接t_dessert查询出当前价格、甜品的名称和图片而不是只显示一个dessert_id让用户猜是什么。所以我在 Mapper 里写了一条联表查询不建议在 Service 层循环去查甜品表这会产生 N1 问题。4.4 下单事务一不注意就会超卖或数据错乱下单是整个系统最核心的代码也是最能体现“会不会做事务”的地方。一个完整的下单流程包括根据购物车记录查出所有要购买的甜品。计算总金额。生成订单主表记录状态为待付款。批量生成订单明细记录。清空购物车。扣减库存。这六步只要有任何一步失败前面已经插入的数据都应该回滚。Spring 的Transactional可以帮我们搞定注意事项是这个方法不能被同类调用比如OrderServiceImpl里有一个createOrder方法是 public 并且标了Transactional如果同类的另一个方法直接this.createOrder()事务注解是不会生效的。因为 Spring 的声明式事务基于 AOP 代理同类内部调用绕过了代理。扣库存的时候SQL 一定要带上库存充足条件update t_dessert set stock stock - #{quantity} where id #{dessertId} and stock #{quantity}如果返回影响行数为 0说明库存不足要抛出业务异常来触发事务回滚。在演示环节我会故意把库存设成 1再开两个浏览器窗口同时下单用这个细节来展示系统的严谨性。5. 踩坑实录一些常见报错的实际排查过程5.1 报错 404先检查 web.xml 和页面路径“页面 404”可能是 SSM 项目里出现频率最高的错误了。我第一次做的时候前端页面放在webapp/jsp/dessert/list.jspController 返回的是dessert/list设置了 InternalResourceViewResolver 的前缀为/jsp/、后缀为.jsp按理说没问题结果浏览器访问时依然 404。我当时的排查思路是这样的先看控制台有没有异常没有异常说明 Controller 找到了问题出在视图解析上。打开浏览器开发者工具发现请求已经返回 200但响应体里是空的或者提示找不到 JSP。检查 Tomcat 的部署目录发现项目名后面加了一层路径我写的返回路径少了/前缀。最后定位到问题是我没有在 spring-mvc.xml 中配置viewResolver的prefix导致 Spring MVC 直接把dessert/list当作完整路径去找webapp/dessert/list.jsp自然能找到但我的 JSP 文件实际在webapp/jsp/dessert/list.jsp所以就需要前缀。一个小技巧遇到 404 先别急着猜直接在浏览器地址栏访问静态 JSP 文件如果能打开说明项目部署没问题如果打不开就要检查web.xml里的欢迎页和部署描述符。再结合控制台输出和浏览器 Network 面板定位效率会高很多。5.2 MyBatis 实体类属性与数据库字段不一致导致的坑我在设计表时数据库字段用了下划线风格比如category_id、create_time而 Java 实体类里习惯写小驼峰categoryId、createTime。第一次执行查询时所有字段都是 null也不报错。这是因为 MyBatis 默认使用列名作为属性名映射category_id无法自动匹配到categoryId。解决方式有两种第一种是给每一条 SQL 的列起别名比如select category_id as categoryId ...太繁琐。第二种是开启 MyBatis 的驼峰映射在mybatis-config.xml里加上settings setting namemapUnderscoreToCamelCase valuetrue/ /settings我强烈建议第二种方式。虽然我在长期工作中见过团队坚持手动别名但开启驼峰映射后代码会干净很多。注意这个配置对结果映射和参数映射都生效前提是数据库字段确实是下划线风格Java 属性确实是驼峰风格。另外还有一个容易忽略的点resultType时 MyBatis 会通过反射创建实体对象所以实体类必须有默认构造函数不能只写带参的构造函数。否则很多同学会看到一个诡异异常There is no getter for property named id in java.lang.String一看就是 MyBatis 在尝试把字符串类型的参数映射到了实体类多半是 Mapper 接口方法的参数没加Param注解。5.3 数据库连接池超时长时间挂机后第一次点击就报错系统做完了本地运行一切正常但部署到服务器之后经常出现“用户早上打开页面输入账号密码点登录白屏或者报 500”的现象。刷新一下又好了。这个问题是典型的数据库连接池超时。Druid 默认会保持空闲连接如果 MySQL 的wait_timeout默认 8 小时连接空闲超过 8 小时就会被数据库服务端主动断开连接池里的连接变成了“死连接”。客户端再拿这个连接执行 SQL就会报Communications link failure。解决方式有三种我建议组合使用在 JDBC URL 上添加autoReconnecttrue参数但这只对 MySQL 5.0 以下有效新版本意义不大。在 Druid 配置里设置testWhileIdletrue和timeBetweenEvictionRunsMillis60000让连接池定时检测空闲连接。配置 Druid 的validationQuerySELECT 1在启动时会主动验证连接。我本地是加了一个定时任务每天凌晨四点主动执行一次简单的数据库查询来保持连接活跃本质上和 Druid 的心跳是同一个逻辑。毕设阶段不用做太复杂但这点经验写进“项目难点与解决方案”那一章会显得很有价值。6. 打磨细节让项目从“能跑”变成“值得展示”6.1 前端页面的观感提升方法答辩或者演示的时候评委第一眼看的是页面不是代码。很多 SSM 项目默认的样式都不好看白白让成果打折扣。我用了 Bootstrap 4 美化页面好处是不需要自己写复杂的 CSS只需要引入 CDN 或者本地静态文件然后按照它的栅格系统布局。甜品交易系统天然适合做“卡片式”陈列一张卡片放甜品图片、名字、价格、添加购物车按钮。我还在首页加了一个搜索框通过 GET 请求把keyword传给DessertController查询条件用 MyBatis 的动态 SQL 实现select idselectByCondition resultTypecom.sweet.entity.Dessert select * from t_dessert where if testkeyword ! null and keyword ! and name like concat(%, #{keyword}, %) /if if testcategoryId ! null and category_id #{categoryId} /if /where order by sales desc /selectwhere标签会自动处理条件拼接时的多余 AND这是 MyBatis 动态 SQL 里最常用的小技巧一定要会。6.2 测试时要重点覆盖的几条流程给项目写测试不能只点一遍正常流程就收工。我会按照下面的清单走一遍避免答辩现场翻车用户注册时如果用户名已存在系统是否给出友好提示而不是直接跳到 500 页面。访问需要登录的购物车页面时是否自动跳转到登录页登录成功之后能否回到刚才想去的页面。下单时如果某个甜品库存只有 1但购物车里有 2 份是否会提示库存不足并回滚整个订单。后台修改甜品价格后已在购物车里的价格是否还是旧价格这里我特意做了快照也就是订单明细里存的是下单时价格而不是实时价格这也是交易系统的常规做法。浏览器直接输入/admin/xxx路径非管理员用户是否被拦截。这些测试点看起来零散其实对应的是权限、事务、快照、异常处理四个核心能力评委一问就能答得有底气。6.3 从毕设项目中提炼“可讲的故事”如果你只是把代码写完答辩的时候说“我用了 SSM 框架实现了增删改查”那这个项目最多打及格分。但如果能从项目里提炼出几个核心难点和解决方案效果就完全不一样。我给自己归纳了三个可以展开讲的点第一是“交易一致性”也就是下单事务。可以从浏览器同时下单导致库存变为负数说起引出数据库乐观锁或库存扣减条件最后说明为什么不能直接在代码里先查库存再更新。第二是“权限控制”可以从普通用户直接输入管理员 URL 越权的那一刻说起引出拦截器方案和角色字段设计。第三是“性能优化”可以从首页商品列表遍历查询 N1 问题说起引出联表查询和 PageHelper 分页。这样整场答辩下来你讲的不是一个“项目功能列表”而是一串“发现问题、分析原因、验证方案”的技术故事。这比任何华丽的 PPT 都更能打动评委。7. 写在最后SSM 甜品交易系统还能怎么扩展项目做到这里其实已经是一份很完整的 SSM 综合练习了。如果你想再提升一点有两个方向可以参考。第一个方向是引入 Redis 缓存热门甜品列表。当首页访问量大时每次刷新都去 MySQL 里查一遍列表数据压力不小。可以在第一次查询后把数据缓存到 Redis设置过期时间 5 分钟甜品表更新时主动删除缓存。这会让系统的技术栈多一个亮点而且 Redis 和 SSM 的集成并不算难Spring Data Redis 已经封装得足够好用。第二个方向是把控制层参数校验做得更规范。目前项目里的参数判断大多堆积在 Service 或 Controller 里如果要追求规范可以引入spring-boot-starter-validation的注解校验方式在实体类字段上标注NotBlank、NotNull、Min等注解在 Controller 方法参数前加Validated即可。这个小改动能让代码可读性明显提升。最后分享一个我自己的体会项目做得好不好不取决于框架多新、功能多炫而取决于你能不能把一条业务链路从头到尾讲透。SSM 甜品交易系统的价值不在于它是一个“甜品网站”而在于它完整地模拟了一次真实商品交易过程中的所有关键环节。把这个过程吃透了以后不管是用 Spring Boot 重构还是转去学微服务底子都已经打好了。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →