☰
SSM鲜花商城复现:从骨架搭建到库存扣减与部署排错
2026/10/7 8:01:32 网站建设 项目流程

简介:基于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.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.3.23</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.7</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.16</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> <scope>runtime</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.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/flower_shop?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=123456
<!-- spring-context.xml 片段 --> <context:property-placeholder location="classpath:jdbc.properties"/> <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}"/> <property name="initialSize" value="5"/> <property name="maxActive" value="20"/> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="configLocation" value="classpath:mybatis-config.xml"/> <property name="mapperLocations" value="classpath:com/flowershop/mapper/*.xml"/> </bean> <mybatis:scan base-package="com.flowershop.mapper"/>

这一段里有几个常被忽略的点。jdbc.url 里的useUnicode=true&characterEncoding=utf8是后面页面中文不乱码的第一道保险,别只依赖数据库端设置;serverTimezone=Asia/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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

三个细节。第一,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 id="selectPage" resultType="com.flowershop.pojo.Flower"> SELECT flower_id, name, category, price, cover, stock FROM t_flower <where> <if test="category != null and category != ''"> AND category = #{category} </if> <if test="keyword != null and keyword != ''"> AND name LIKE CONCAT('%', #{keyword}, '%') </if> </where> ORDER BY <choose> <when test="sort == 'price'">price ASC</when> <when test="sort == 'sales'">sales DESC</when> <otherwise>flower_id DESC</otherwise> </choose> LIMIT #{offset}, #{pageSize} </select>

对应的 Java 接口方法:

List<Flower> 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。这就是教科书级的超卖翻车现场:两个请求同时查到 stock=1,各判断一次“有货”,各扣一次,最后库存变成 -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, List<OrderDetail> 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-8,MySQL 连接字符串没给characterEncoding=utf8,数据库连接时使用系统默认字符集;或者数据库表本身就是 latin1。这几类原因不在一处,排查起来像拆盲盒。

解决:第一,MySQL 配置文件 my.ini 里character-set-server=utf8mb4;第二,表创建语句指定DEFAULT CHARSET=utf8mb4,已经建错的表要执行ALTER TABLE t_flower CONVERT TO CHARACTER SET utf8mb4;第三,jdbc.url 里的useUnicode=true&characterEncoding=utf8必须带上;第四,如果用到 JSP,页面第一行写<%@ page contentType="text/html;charset=UTF-8" %>,并设置响应头。我最常犯的错是只改了连接串、忘了改表字符集,导致注册的新用户只要含中文就乱码。排错时先执行SHOW CREATE TABLE t_user;看实际字符集,再拿一条连接字符串在测试类里直接执行插入,能快速锁定是哪一层丢的编码。

5.3 Invalid bound statement(not 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 id="selectPage">的 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?current=1&pageSize=5 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 ConcurrentHashMap<Integer, 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 项目,我能直接避开九成路由和编码问题,省掉的全是暗时间。希望这篇笔记能帮你在复现这个鲜花商城项目的时候少走几趟弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询