SSM鲜花商城复现:从骨架搭建到库存扣减与部署排错

发布时间:2026/10/7 8:01:41
SSM鲜花商城复现:从骨架搭建到库存扣减与部署排错 简介基于Java Web和SSM框架的鲜花商城系统完整项目源码面向需要学习SSM整合开发、完成课程设计或毕业设计的Java学习者同时也可作为Java Web入门到进阶的参考案例解决从零搭建前后台电商系统的需求。压缩包共1454个文件涵盖398个JavaScript脚本、165个CSS样式、140个JSP页面及131个Java类另有SQL数据库脚本、XML配置与图片素材整体约21.82MB目录层次分明便于按模块查阅与二次开发。前台功能包含用户注册登录、商品分类展示、购物车加购、订单生成与个人中心管理后台集成管理员登录、商品信息增删改、订单状态处理、用户信息维护及系统配置覆盖商城系统最常见的业务闭环。目前已有39人学习使用适合通过研读源码理解Spring、SpringMVC、MyBatis的整合方式、MVC分层思想与权限控制逻辑也能直接作为毕业设计或期末项目的基础框架。1. 一个“鲜花商城”的 SSM 源码为什么值得照着复现一遍在 Java 后端面试里十个课程设计八个是商城可很多人把项目交上去之后连“购物车存哪张表”都说不清楚。这个基于 Java Web 和 SSM 框架的鲜花商城系统规模不大但把 Spring、Spring MVC、MyBatis 三者的协作关系完整闭环了一遍注册登录、花品列表、购物车、下单、订单状态每一环都是你以后做复杂业务时要复用的底子。把它跑通你会有两种收获一是手里多一个能演示的成型项目二是遇到 404、乱码、事务不回滚这类经典问题时不至于全靠“改几次重启”的玄学。这篇笔记就按工程落地的顺序从建表到部署排查一层层拆给你看新手能跟着复现熟手也能在这里对一下自己的边界。2. 在 IDEA 里把 SSM 骨架搭起来Maven 坐标、三层目录与数据库设计拿到一份 SSM 源码第一件事不是急着启动而是先把目录结构看清。目录就是架构架构顺了后面每一步都有落脚点。2.1 项目目录结构先别急着写代码把层级固定下来一个干净的 SSM 工程目录结构就代表架构。我通常会按 controller → service/impl → mapper → pojo → utils 这五层组织前端页面放到 webapp 下。这个鲜花商城项目的结构就是这样的标准形态flower-shop/ ├── pom.xml └── src ├── main │ ├── java/com/flowershop │ │ ├── controller/ # Spring MVC 控制层只做参数接收和视图转发 │ │ ├── service/ # 业务层接口 │ │ │ └── impl/ # 业务实现事务加在这一层 │ │ ├── mapper/ # MyBatis 的 mapper 接口 │ │ ├── pojo/ # 实体类对应数据表 │ │ └── utils/ # 订单号生成、MD5 加密等工具 │ ├── resources/ │ │ ├── jdbc.properties # 数据库连接参数 │ │ ├── spring-context.xml # Spring 根容器 │ │ ├── spring-mvc.xml # Spring MVC 子容器 │ │ └── mybatis-config.xml # MyBatis 全局配置 │ └── webapp/ │ ├── WEB-INF/web.xml # 3.0 以后可省但很多教程仍保留 │ └── static/ # CSS/JS/图片看到这棵树其实你已经把 SSM 的启动顺序猜出了大半web.xml 或注解配置负责引导Spring 根容器加载 service、mapper 和数据源Spring MVC 的子容器只管 controller。注意这里有个容易翻车的细节mapper 接口和 mapper XML 文件必须放在同一个同名包里否则扫描不到——这是后面专门的踩坑章节要展开的问题。用这个结构为范本基本能应对绝大部分课程设计和中小型项目。接下来决定 Maven 坐标。pom.xml 是唯一能同时坑新手和老手的文件!-- pom.xml 关键依赖版本是踩过坑后的最低配置 -- properties maven.compiler.source8/maven.compiler.source maven.compiler.target8/maven.compiler.target /properties dependencies dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version5.3.23/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.7/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.16/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version scoperuntime/scope /dependency /dependencies依赖里有三个参数值得单独说明。第一Spring 用 5.3.x 而不是 6.x因为这个工程要跑在主流课程设计和服务器环境里JDK 8 还占大头Spring 6 强制 JDK 17你本地就算能编过放到旧服务器上也会直接启动失败。第二mybatis-spring 不要和 mybatis 本体、mybatis-spring-boot-starter 混用版本错位会出现 SqlSessionFactory 找不到的诡异异常。第三mysql-connector-java 的 scope 写 runtime意思是编译时不需要运行期才由 Tomcat 加载这样打包 war 时不会把驱动重复塞进去也避免 IDEA 在编译阶段报一堆没必要的错。很多所谓“源码运行不起来”一半问题都出在依赖范围和版本不匹配上。2.2 三个 XML 配置文件数据源、Spring 根容器、Spring MVC 子容器的职责边界我刚拿到这种 SSM 工程习惯先看配置文件而不是先看代码因为框架的装配关系都在这里。核心就三样数据源、Spring 根容器、SpringMVC 子容器。以数据源配置为例!-- jdbc.properties -- jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/flower_shop?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456!-- spring-context.xml 片段 -- context:property-placeholder locationclasspath:jdbc.properties/ 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}/ property nameinitialSize value5/ property namemaxActive value20/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property nameconfigLocation valueclasspath:mybatis-config.xml/ property namemapperLocations valueclasspath:com/flowershop/mapper/*.xml/ /bean mybatis:scan base-packagecom.flowershop.mapper/这一段里有几个常被忽略的点。jdbc.url 里的useUnicodetruecharacterEncodingutf8是后面页面中文不乱码的第一道保险别只依赖数据库端设置serverTimezoneAsia/Shanghai是 MySQL 8.x 的必填项缺了它大概率报时区异常。Druid 的 initialSize 和 maxActive我一般习惯一个 5 一个 20 起步如果你本地数据库开了太多连接再把 maxActive 调小不然并发跑一个下单脚本连接池先耗尽。mybatis:scan来自 mybatis-spring 的命名空间它把 mapper 接口直接注册成 Spring Bean这样 service 里才敢Autowired。mapperLocations 指向 XML 文件路径注意它用的是 classpath 相对路径前面那个“接口和 XML 同包”的要求就是这么来的——你如果图省事把 XML 放到 resources 根目录必须在这里把路径改对。这两个配置一错启动阶段不一定报错运行期访问第一个 mapper 方法才炸属于典型的“慢炸弹”。2.3 数据库设计核心五张表不要贪多商城系统的复杂度不在表多而在表之间的联动。这个项目的核心表就五张用户表、花品表、购物车表、订单主表、订单子表。新手容易犯的错是给每个商品字段单独建表或者把购物车和订单详情合并——我建议守住一个原则能在一张表里用字段表达的不要拆成两张表。CREATE TABLE t_flower ( flower_id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL, category VARCHAR(50) NOT NULL, price DECIMAL(10,2) NOT NULL, cover VARCHAR(255) COMMENT 商品图片 URL, stock INT NOT NULL DEFAULT 0, sales INT NOT NULL DEFAULT 0, detail TEXT ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_user ( user_id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(200) NOT NULL COMMENT 存 MD5 后的值, phone VARCHAR(20), address VARCHAR(255) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_cart ( cart_id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, flower_id INT NOT NULL, quantity INT NOT NULL DEFAULT 1 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;三个细节。第一price 用 DECIMAL(10,2)不要用 FLOAT否则订单金额对账会出现 0.1 加 0.2 不等于 0.3 的尴尬。第二所有表和字段一律 utf8mb4它不是 utf8 的替代品而是 MySQL 里真正的“超集”玫瑰这种 emoji 字符只有 utf8mb4 存得进。第三没有建任何外键约束。这是课程设计和商业系统一个明显的分界点外键会让下单事务里出现很多隐式锁代码排查时外键有时候表现得很像死锁——对这样的小项目来说逻辑外键足够物理外键是给自己找麻烦。订单主表和订单子表在写到下单链路时再展开。3. 让页面真正“动”起来登录鉴权、花品列表与购物车实现配置骨架搭完这个工程就到了最能学到东西的业务实现部分。这里不看花哨的设计模式就看 SSM 在日常业务里怎么配合。3.1 登录状态一个拦截器顶过 Spring Security 的“全家桶”这个规模的项目登录鉴权用 Spring Security 属于用牛刀杀鸡配置两行就出一个登录页新手不好变通。实现一个 HandlerInterceptor 更可控public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 跳过登录页、注册页和静态资源这些在 mvc 配置里排除 Object userId request.getSession().getAttribute(userId); if (userId null) { if (XMLHttpRequest.equals(request.getHeader(X-Requested-With))) { // AJAX 请求没法重定向直接给 401前端根据状态码弹登录提示 response.setStatus(401); } else { response.sendRedirect(request.getContextPath() /login); } return false; } return true; } }在 spring-mvc.xml 里挂载这个拦截器mvc:interceptors interceptor mvc:mapping path/**/ mvc:exclude-mapping path/login/ mvc:exclude-mapping path/register/ mvc:exclude-mapping path/static/**/ /interceptor /mvc:interceptors这段代码逻辑不算复杂但有两个边界值得较真。第一为什么要单独判断X-Requested-With因为页面被重定向到 /login 没感觉而 AJAX 收到 302 会把 JSON 解析失败前端永远进不了登录页。第二exclude-mapping 的 path 规则是按 Ant 风格匹配/static/**只匹配 static 目录下的资源如果页面引用了别的图片路径记得一并把前缀加进去。很多项目看着像是“登录成功后跳转还是 404”其实不是登录逻辑错了而是静态资源被拦了。3.2 花品列表手写分页 SQL比 PageHelper 更看得见摸得着花品列表页要支持分类筛选、关键词搜索和分页。用 PageHelper 插件可以少写几行代码但对于学习框架的人我强烈建议先用明白手写的方式。MyBatis 的动态 SQL 在这里是一个经典场景select idselectPage resultTypecom.flowershop.pojo.Flower SELECT flower_id, name, category, price, cover, stock FROM t_flower where if testcategory ! null and category ! AND category #{category} /if if testkeyword ! null and keyword ! AND name LIKE CONCAT(%, #{keyword}, %) /if /where ORDER BY choose when testsort priceprice ASC/when when testsort salessales DESC/when otherwiseflower_id DESC/otherwise /choose LIMIT #{offset}, #{pageSize} /select对应的 Java 接口方法ListFlower selectPage(Param(category) String category, Param(keyword) String keyword, Param(sort) String sort, Param(offset) Integer offset, Param(pageSize) Integer pageSize);这个 SQL 有两个容易写错的地方。第一个是where标签会自动去掉第一个AND所以每个条件前加AND不会出错但如果你手动拼接 SQL就一定要检查前面是否有 WHERE 和多余 AND字符串拼出来的翻车现场基本集中在这里。第二个是LIKE CONCAT(%, #{keyword}, %)而不是%${keyword}%$是直接字符串替换用户输入一个%就能把全表数据查出来这个坑在 SQL 注入里属于入门级但每年都能见到。分页参数offset (currentPage - 1) * pageSize页面上传页码时记得做整数校验。有人用 PageHelper 插件它底层会改写 SQL 并在执行完后自动查 count便捷是便捷但如果你的查询里有多表关联或者子查询统计 SQL 有时候会抽风报错到那时候排查起来就像在跟黑匣子搏斗。项目想要稳手写分页能省一半心。3.3 购物车Cookie、数据库还是 Redis三种方案怎么选购物车是商城需求里最容易摇摆的点有人把购物车塞进 Cookie图的是免登录和轻量有人直接建表重登录后也不丢。这个鲜花商城的主流做法是加购后直接往 t_cart 表里写理由很直白——中间页面不做省这个存储而订单流程反正要读用户信息数据库购物车和订单子表结构相近少一道转换。Mapper public interface CartMapper { Insert(INSERT INTO t_cart (user_id, flower_id, quantity) VALUES (#{userId}, #{flowerId}, #{quantity})) int insert(Cart cart); }Service public class CartServiceImpl implements CartService { Autowired private CartMapper cartMapper; Override public boolean addToCart(Cart cart) { // 先查是否已在购物车在则改数量不在则新增 Cart existing cartMapper.selectByUserIdAndFlowerId(cart.getUserId(), cart.getFlowerId()); if (existing ! null) { return cartMapper.updateQuantity(cart.getUserId(), cart.getFlowerId(), existing.getQuantity() cart.getQuantity()) 0; } return cartMapper.insert(cart) 0; } }这里的逻辑判断“有则加数量、无则新增”是购物车最少要保留的一条业务规则。如果直接把数据写成同一条记录用户的购物车只会越点越多。如果要用 Redis 购物车常见的取舍是这样把购物车 key 设计成cart:userId用一个 Hash 存flowerId - quantity加购只做HSET和HINCRBY性能比 MySQL 高一个量级但会有几个新问题——登录用户未登录时购物车要不要合并、Redis 里数据没了怎么办。课程设计和演示项目里MySQL 购物车最稳代码直观想给简历加亮点把第 6 章的缓存方案做好就够用了不必一开始就上 Redis 购物车。4. 下单链路与库存扣减订单号生成、事务边界和并发下的超卖问题商城系统里真正决定项目质量的是下单这一段。花品列表写得再花哨下单链路有隐蔽缺陷演示时也容易当场翻车。4.1 订单号生成时间戳加随机数并不保证唯一下单首先要解决“订单号从哪来”。如果只是演示一个由时间戳加随机数拼出来的 ID 就够了但它没有唯一性保证public final class OrderNoGenerator { private static final DateTimeFormatter FMT DateTimeFormatter.ofPattern(yyyyMMddHHmmss); public static String generate() { // 前缀 F 代表鲜花商城后接时间戳 三位随机数 String timePart LocalDateTime.now().format(FMT); int random ThreadLocalRandom.current().nextInt(100, 999); return F timePart random; } }这段代码在单机低并发下没毛病但在压测脚本一跑就会出现重复订单号。原因很简单同一秒内两个请求可能随机到同一个数或者 JVM 多线程下 LocalDateTime.now() 拿到的纳秒被格式化成秒随机数的样本空间只有 900 个很容易摔在同一秒。真要保证唯一常见做法有三个一是接一个 Redis用 INCR 命令生成自增段二是数据库表里加一个雪花算法的 ID 字段三是用数据库自增主键做订单流水号业务订单号只是展示字段。课程设计阶段用时间戳加随机数能应付但你要知道它的上限在哪。4.2 库存扣减一条带条件的 UPDATE 解决超卖下单链路里最核心的问题不是订单写不进去而是库存别被多卖。常见的错误写法是先select stock在 Java 里判断if (stock quantity)再update stock stock - quantity。这就是教科书级的超卖翻车现场两个请求同时查到 stock1各判断一次“有货”各扣一次最后库存变成 -1。解决超卖一条带有库存条件的 UPDATE 就够了UPDATE t_flower SET stock stock - #{quantity} WHERE flower_id #{flowerId} AND stock - #{quantity} 0;执行这条语句后只会有两种返回结果影响行数是 1扣减成功影响行数是 0说明库存不足。在 MyBatis 的 mapper 里返回值就是int。于是下单方法里可以这样组织int affected flowerMapper.deductStock(flowerId, quantity); if (affected 0) { // 库存不足直接把业务异常抛出去交给事务回滚 throw new BusinessException(库存不足); }库存扣减和订单写入的先后顺序其实没有绝对标准但必须放在同一个事务里保证要么订单和库存一起成功要么一起回滚。你先插入订单子表再减库存风险是减库存失败时订单表已经写了脏数据先减库存再插入订单那么插入失败时库存已经扣了需要回滚。无论哪个顺序事务都不许拆开。另外这一步并不需要SELECT FOR UPDATE一条带条件的UPDATE已经天然上了行锁并发足够满足这个规模的业务。4.3 Transactional 事务边界放在 impl 层别放在接口或 controller事务注解放在哪一层是 SSM 框架一个常见考点。我见过有人把Transactional加到 Controller 的方法上Tomcat 启动不报错但事务根本没生效还有人加到接口方法上Spring 默认的代理策略用的是 CGLIB放在接口上是无效的。正规做法是放在ServiceImpl的实现类方法上Service public class OrderServiceImpl implements OrderService { Autowired private OrderMapper orderMapper; Autowired private FlowerMapper flowerMapper; Transactional(rollbackFor Exception.class) Override public void createOrder(Order order, ListOrderDetail details) { // 1. 生成订单号并写入主表 order.setOrderNo(OrderNoGenerator.generate()); orderMapper.insertOrder(order); // 2. 减库存注意检查返回值 for (OrderDetail detail : details) { if (flowerMapper.deductStock(detail.getFlowerId(), detail.getQuantity()) 0) { throw new BusinessException(库存不足); } } // 3. 写订单明细 for (OrderDetail detail : details) { orderMapper.insertDetail(detail); } } }这个rollbackFor Exception.class是第二道保险。Spring 的默认事务只在遇到RuntimeException时回滚如果你在减库存时抛了一个自定义的BusinessException继承的是 Exception受检异常Spring 默认事务不会回滚——订单主表落库库存也没减成功这条脏数据能恶心你半天。显式声明 rollbackFor 后无论运行时还是受检异常都会触发回滚。还有一个隐蔽的坑叫“自调用”在 OrderService 内部如果另一个方法直接调用this.createOrder()事务不会生效。因为事务拦截器是靠代理对象实现的this是原始对象代理逻辑没有进来注解直接被绕过。排查这个问题时先看调用方是不是拿到了 Spring 注入的代理对象再看是不是用了this.开头。5. 部署与排查Tomcat 404、中文乱码和 MyBatis 绑定异常对照手册源码项目最容易让复现者放弃的地方不是业务逻辑而是环境问题。本章把我在部署这种 SSM 商城时遇到最多的四类问题列出来每一条都按现象、原因、解决的顺序写清楚。5.1 Tomcat 404先分清是路径问题还是拦截器问题现象启动成功访问http://localhost:8080/却 404访问http://localhost:8080/flower-shop/能出首页但页面里所有图片、Ajax 请求都 404。原因Tomcat 部署的上下文路径context path不是根路径默认带了工程名flower-shop。而页面里写的静态资源路径常常是src./static/css/app.css或者跳转地址写死了/login不带 contextPath。解决跳转和请求前缀全部改为动态获取Java 侧用request.getContextPath()JSP 里用${pageContext.request.contextPath}前端资源可以放到c:url标签里。前面拦截器代码里我已经用了request.getContextPath()这块最容易忽略的就是 AJAX 的 URL比如$.get(/flower/list)部署到根路径时这个开头没问题项目名变了就找不到接口。如果访问根路径 404 但其他地址能打开那就要查另一类问题web.xml 里没配欢迎页。Tomcat 默认找 index.jsp如果你的首页是 flowerList.jsp就在 web.xml 里加welcome-file-list否则只会看到空白或 404 黑匣子。先把这两类确认掉再往下查日志——Tomcat 的 localhost.log 里如果只有“资源未找到”而没有异常堆栈基本就是路径问题。5.2 数据库中文乱码四层设置少一层就乱现象网页上“玫瑰”变成了???或者写入数据库后 Java 控制台能看到中文、MySQL 客户端里却是乱码。原因中文乱码九成是四层编码不统一。Java 代码和页面用的是 UTF-8MySQL 连接字符串没给characterEncodingutf8数据库连接时使用系统默认字符集或者数据库表本身就是 latin1。这几类原因不在一处排查起来像拆盲盒。解决第一MySQL 配置文件 my.ini 里character-set-serverutf8mb4第二表创建语句指定DEFAULT CHARSETutf8mb4已经建错的表要执行ALTER TABLE t_flower CONVERT TO CHARACTER SET utf8mb4;第三jdbc.url 里的useUnicodetruecharacterEncodingutf8必须带上第四如果用到 JSP页面第一行写% page contentTypetext/html;charsetUTF-8 %并设置响应头。我最常犯的错是只改了连接串、忘了改表字符集导致注册的新用户只要含中文就乱码。排错时先执行SHOW CREATE TABLE t_user;看实际字符集再拿一条连接字符串在测试类里直接执行插入能快速锁定是哪一层丢的编码。5.3 Invalid bound statementnot found八成是扫描路径问题现象启动正常一访问带 Mapper 的方法就报org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.flowershop.mapper.FlowerMapper.selectPage。原因MyBatis 在容器里没找到 mapper 接口对应的 XML 文件。常见有四种XML 文件名和接口名不一致XML 没放到 mapperLocations 指定的目录resources 里目录层级不对或接口方法在 XML 中没有对应的 statement id。很多人只检查了前两种漏了第四种。解决先确认 mapper 接口和 XML 文件放在同一个包路径下比如com.flowershop.mapper然后到 spring-context.xml 里核对mapperLocations配置最后在 XML 里看select idselectPage的 id 是否和接口方法名完全一样参数类型是否匹配。还有一个隐蔽的原因是 XML 里的namespace写成了类名缩写必须全限定名com.flowershop.mapper.FlowerMapper。排查顺序我一般是从 class 目录开始IDEA 编译后打开 target/classes看 XML 有没有被复制出来如果 target 里就是空的那一定是 resources 路径配错。5.4 IDEA 热部署导致的 ClassNotFoundException现象IDEA 里用内置 Tomcat 改了几次 Java 类再次点击 update 之后偶尔抛ClassNotFoundException重启 Tomcat 又恢复正常。原因IDEA 的热部署机制HotSwap只能处理方法体内部变化不能添加或删除方法一旦遇到类结构变动老 class 加载器和新 class 加载器共存就可能找不到类。这不是工程逻辑问题是 IDE 的常见坑。解决不要依赖 IDEA 的热部署改完 Java 类就重启 Tomcat改 JSP 和静态资源才能即时刷新。保留热部署并且机器够新的话可以打开Build project automatically但遇到 ClassNotFoundException 别往工程上怀疑先重启就完事了。如果你用的是外部 Tomcat把 Tomcat 的 deploy 目录 clean 一下再重新部署比在 IDEA 里死磕缓存要快得多。6. 上线前把业务闭环验证一遍再谈加缓存和增强6.1 用 Postman 把下单链路完整跑一遍我把这类源码工程验收时最少要做一次的四步验证注册一个新用户 → 登录拿到 Session → 往购物车加一件库存只剩 1 的花 → 下单成功后重新查商品列表确认库存减 1。Postman 里打开请求登录接口返回的 set-cookie 会带上 JSESSIONID后续请求带上这个 Cookie就能模拟真实浏览器的会话。重点验证下面这个顺序POST /register body: {username:test,password:123456,phone:13800000000} POST /login body: {username:test,password:123456} GET /flower/list?current1pageSize5 POST /cart/add body: {flowerId:3,quantity:1} POST /order/create body: {flowerId:3,quantity:1,address:测试路 1 号} GET /order/list如果中途出现 401先检查 JSESSIONID 是否已在 Header 里带上出现 500马上翻 Tomcat 的 localhost.log而不是先刷新页面重试——刷新只是把同一个异常再触发一遍。这一步能跑通说明登录、权限、购物车、库存、订单五张表之间的协作关系是正常的项目具备演示条件。6.2 给花品列表加一个缓存只动 20 行的增强点项目跑通之后这个方向最顺手的增强是给花品列表加缓存。把热点查询结果缓存起来TTL 设 60 秒比直接靠数据库扛请求要稳得多。如果不想引入 Redis先用 ConcurrentHashMap 做一个简单的本地缓存Component public class FlowerCache { private final ConcurrentHashMapInteger, CacheEntry map new ConcurrentHashMap(); private static final long TTL 60_000L; public Flower get(Integer id) { CacheEntry entry map.get(id); if (entry null || System.currentTimeMillis() - entry.time TTL) { return null; } return entry.flower; } public void put(Integer id, Flower flower) { map.put(id, new CacheEntry(flower, System.currentTimeMillis())); } public void evict(Integer id) { map.remove(id); } }查询花品时先走缓存未命中再查数据库加购和下单时主动调用evict(id)否则更新后的数据要等缓存过期才可见。只加一个 service 再加两个调用点改动量就在 20 行左右却能把列表接口的耗时从几十毫秒降到个位数毫秒。别一上来就上消息队列、分库分表那不是这个工程该关注的复杂度。我自己的习惯是每次拿到 SSM 练手源码第一件事不是看效果而是把 pom.xml、jdbc.url、mapper XML 路径这三个位置的配置先看一遍再对照项目目录结构确认层级。养成这个习惯以后遇到跑不起来的 Java Web 项目我能直接避开九成路由和编码问题省掉的全是暗时间。希望这篇笔记能帮你在复现这个鲜花商城项目的时候少走几趟弯路。本文还有配套的精品资源点击获取