
简介这是一份基于SSM框架SpringSpringMVCMyBatis并结合JSP技术的网上花店系统毕业设计项目源码与配套说明文档面向Java方向毕业生、课程设计学生及需要快速搭建企业级Web系统的开发者。项目完整覆盖在线购花业务闭环包含商品展示管理、购物车与订单、支付配送、促销优惠券、用户评价反馈、库存报警及销售数据统计等核心模块可用于课程设计、毕业设计答辩或作为JavaWeb综合练习参考。压缩包共29.38MB内含1458个文件主要类型包括131个Java源码、140个JSP页面、398个JS脚本、165个CSS样式以及数据库SQL文件、说明文档和LW论文/文档等前后端资源齐全目录结构清晰便于直接导入IDE部署运行。目前已有92人学习/下载。除可运行的完整代码外还附带数据库初始化脚本与设计说明帮助理解SSM框架整合流程和电商系统常见业务实现思路值得Java学习者深入研读。1. SSMJSP花店系统毕业设计为什么绕不开这套组合每年毕设季选题列表里总能看到Java基于ssm框架的网上花店系统的设计与实现jsp这个标题。它不用高并发、不碰微服务却能长期排在热门榜上原因是这套技术栈恰好覆盖了Java Web开发的完整主线Spring管对象、Spring MVC管请求、MyBatis管SQLJSP把数据渲染成页面。相比图书管理这类经典题目花店业务的展示性更强。商品分类、购物车、订单状态流转整条链路都能截图写进论文工作量刚好撑起一份本科毕设不会因为太简单被评委质疑深度。下面按实际开发顺序往下拆先定数据表再讲SSM整合和核心业务最后集中处理部署排错新手能照着跑起来熟手可直接对照参数和边界。2. 花店系统的数据表设计用户、商品、订单三张核心表的边界做SSMJSP的系统顺序上我建议先定数据表再写代码。很多同学喜欢先搭框架再建表结果写到Service层才发现字段不够、关系混乱回头改表还要连带改页面。花店系统的核心业务就三块用户、商品、订单。把这三块的表结构和边界定清楚后面所有Controller和Mapper都只是对表的读写代码写起来会快很多。2.1 用户表与权限前台顾客和后台管理员为什么要分表最常见的偷懒做法是一张用户表加一个role字段1是顾客、2是管理员。但花店系统里这两类角色的字段差异很大。顾客需要收货地址、电话、昵称后台管理员需要的是登录IP、最后操作时间这类字段。硬塞进同一张表要么大量字段为空要么后台功能写到一半发现字段对不上。我一般拆成tb_user和tb_admin两张表登录也从两个入口分开走前台登录不影响后台后台不用处理顾客数据。CREATE TABLE tb_user ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 用户ID, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(64) NOT NULL COMMENT 密码密文, nickname VARCHAR(50) NULL COMMENT 昵称, phone VARCHAR(20) NULL COMMENT 手机号, address VARCHAR(255) NULL COMMENT 默认收货地址, email VARCHAR(100) NULL COMMENT 邮箱, status TINYINT DEFAULT 1 COMMENT 1启用 0禁用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 注册时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT前台用户表;username加UNIQUE是为了登录查询走唯一索引避免用户名重复注册的边界问题password预留64位是因为论文里常用MD532位但如果你想换成BCrypt60位也放得下不用回头改字段长度status字段是给后台禁用用户功能用的很多系统没设计这个字段答辩时被问用户恶意下单怎么办就只能干瞪眼。tb_admin结构更简单id、username、password、role区分超级管理员和普通管理员、last_login_time。两张表都不加物理外键用逻辑外键代码里关联查询就够了。物理外键在删除商品、删除用户时会不断报约束错误毕业设计阶段撑不起这个维护成本。提示论文里写MD5加密没问题但如果你想把BCrypt加盐写进创新点字段长度已经留好了代码改动只在注册和登录两个方法。2.2 商品-分类-库存建模花店的SKU粒度该拆到哪一层花店商品有特殊性一束花可能有花材、包装、色系、适用场景多种属性。成熟电商会做多规格SKU同一款花红玫瑰和香槟玫瑰算两个SKU价格不同库存不同。但本科毕设做多规格意味着商品表、规格表、SKU表、购物车都要跟着改工作量直接翻倍。常见折中方案是单规格商品一个商品一条记录一个价格一个库存属性统一写在描述里。CREATE TABLE tb_category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, parent_id INT DEFAULT 0 COMMENT 父分类0为顶级, sort INT DEFAULT 0 COMMENT 排序值 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE tb_flower ( id INT PRIMARY KEY AUTO_INCREMENT, category_id INT NOT NULL, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL COMMENT 售价, original_price DECIMAL(10,2) DEFAULT NULL COMMENT 划线价, stock INT UNSIGNED DEFAULT 0 COMMENT 库存, sales INT UNSIGNED DEFAULT 0 COMMENT 销量统计, cover_img VARCHAR(255) DEFAULT NULL COMMENT 列表页图片, detail_img VARCHAR(255) DEFAULT NULL COMMENT 详情页大图, description TEXT COMMENT 商品描述, status TINYINT DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_category (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT花店商品表;价格用DECIMAL(10,2)而不是FLOAT是因为浮点型在计算0.10.2这类场景会有误差订单金额一旦出现0.30000000000000004这种数答辩演示时非常难看。stock用INT UNSIGNED避免库存被扣成负数时出现-3条这种逻辑错误。cover_img和detail_img存的是相对路径例如/upload/rose.jpg图片文件放在webapp/upload目录下而不是把图片以base64塞进数据库后者会让页面渲染慢一个量级。parent_id字段给分类留了二级扩展顶级分类放鲜花、绿植、礼盒二级分排放红玫瑰、康乃馨、向日葵这类具体花材。不一定要做但留一个字段的成本几乎为零论文里还能写支持二级分类扩展。2.3 购物车与订单表状态流转从建表就要想清楚购物车表建议和用户表、商品表分别建立关联。tb_cart(id, user_id, flower_id, quantity, create_time)给user_id和flower_id加一个UNIQUE组合索引保证同一个用户对同一件商品只产生一条购物车记录重复加购时走ON DUPLICATE KEY UPDATE把数量累加而不是产生两行脏数据。订单表要拆主表和明细表两张。主表记录这笔订单是谁的、多少钱、发到哪明细表记录买了哪些商品、当时什么价格。拆开的原因很简单一张订单可能包含多束花如果只做一张订单表要么字段冗余要么只能允许一单一件。CREATE TABLE tb_order ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT 订单号, user_id INT NOT NULL, total_price DECIMAL(10,2) NOT NULL COMMENT 订单总金额, status TINYINT DEFAULT 0 COMMENT 0待付款 1待发货 2待收货 3已完成 4已取消, receive_name VARCHAR(50) NOT NULL, receive_phone VARCHAR(20) NOT NULL, receive_address VARCHAR(255) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单主表; CREATE TABLE tb_order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, flower_id INT NOT NULL, flower_name VARCHAR(100) NOT NULL COMMENT 商品名称快照, price DECIMAL(10,2) NOT NULL COMMENT 成交价快照, quantity INT NOT NULL COMMENT 购买数量 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单明细表;订单状态用TINYINT数字表示不要直接存中文。后续代码里用常量类映射0待付款、1待发货、2待收货、3已完成、4已取消。状态流转顺序是下单生成0用户点付款变成1后台发货变成2用户确认收货变成3在0状态下用户取消则变成4。这条链路写清楚论文里的UML状态图直接就有素材。明细表里特意冗余了flower_name和price这是订单系统的通用做法。商品改价、商品下架甚至删除历史订单依然能还原出当时买了什么、付了多少钱。如果不做快照订单查询时遇到商品被删的情况明细直接成空后台发货界面就会报空指针。2.4 建表顺序与初始化数据一个容易被忽略的细节建表顺序上有个细节先建tb_category、tb_flower、tb_user这类基础表再建tb_cart、tb_order、tb_order_item这类依赖前者的表。MySQL不强制先建被引用表但后续插入测试数据时如果订单引用了不存在的用户还得回头清理平白浪费时间。初始化数据一定要准备两批一批是后台管理员账号admin/123456一批是6到10个商品分类和每个分类下3到5条商品记录。网上很多源码包只给表结构不给种子数据你跑起来首页空空荡荡还要自己一张张插图。建议写一个data.sql把分类、商品、演示账号全部放进去首次启动时导入数据库换环境也能一键初始化。SQL这块的索引也值得说一句所有外键关联字段都加INDEX比如tb_order.user_id、tb_order_item.order_id、tb_flower.category_id。小数据量下索引看不出差别但答辩评委如果问订单列表为什么按用户查询快你就能回答user_id建了普通索引。别在这类简单问题上翻车。3. SSM框架整合链路从web.xml到Controller再到JSP页面数据表定好后下一步是把SSM框架跑通。这一步卡住的人最多原因不是代码难而是三个框架各自要配一堆文件配置顺序错一点启动就报错。我这里按一条完整请求的路径讲浏览器发请求到web.xmlDispatcherServlet接手按URL找到ControllerService调Mapper查数据库最后返回ModelAndView交给JSP渲染。3.1 三个框架的分工为什么父子容器是jsp项目面试的常客Spring、Spring MVC、MyBatis各管一段。Spring是容器负责管理Service、DAO这些业务对象的创建和依赖注入Spring MVC负责Web层接收HTTP请求、调用Service、把结果交给视图MyBatis负责数据访问把SQL写在XML里避免在Java代码里拼字符串。Spring MVC和Spring之间是父子容器关系。DispatcherServlet启动时会创建一个子容器这个子容器只放Controller。Service、DAO这些放在父容器由ContextLoaderListener创建。Controller能引用父容器的Service但父容器不能反向引用子容器的Bean。这个知识点java面试题里反复出现在毕设答辩里也容易成为追问点。如果你的Service上写了Service却注入不进来十有八九是组件扫描的base-package写错位置Controller和Service扫进了不同的容器。三个框架的依赖也互相关联MyBatis的SqlSessionFactory需要数据源事务管理器依赖数据源Spring要管理MyBatis的Mapper接口。所以配置有严格的先后顺序数据源最先SqlSessionFactory其次事务管理器再次最后扫描Mapper。顺序乱了会报BeanCreationException或者莫名其妙的空指针。3.2 核心配置web.xml、applicationContext.xml、spring-mvc.xml先看web.xml它是整个应用的入口。?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd version4.0 display-nameflower-shop/display-name !-- Spring父容器 -- context-param param-namecontextConfigLocation/param-name param-valueclasspath:spring/applicationContext.xml/param-value /context-param listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener !-- 中文乱码过滤器 -- filter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping !-- Spring MVC前端控制器 -- servlet servlet-namedispatcherServlet/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:spring/spring-mvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namedispatcherServlet/servlet-name url-pattern//url-pattern /servlet-mapping /web-appcontextConfigLocation指向两份XML分别是applicationContext.xml和spring-mvc.xml两份文件各管各的扫描范围。CharacterEncodingFilter必须声明在DispatcherServlet之前否则请求参数先进Controller乱码已经发生过滤器再设置编码也补救不回来。servlet-mapping的url-pattern配成/意味着所有请求都先进DispatcherServletJSP本身不算Servlet路径不会冲突。spring-mvc.xml是Spring MVC的专用配置。?xml version1.0 encodingUTF-8? beans xmlnshttp://www.springframework.org/schema/beans xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xmlns:contexthttp://www.springframework.org/schema/context xmlns:mvchttp://www.springframework.org/schema/mvc xsi:schemaLocationhttp://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd context:component-scan base-packagecom.flowershop.controller/ mvc:annotation-driven/ mvc:resources mapping/static/** location/static// mvc:resources mapping/upload/** location/upload// bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/jsp// property namesuffix value.jsp/ /bean mvc:interceptors mvc:interceptor mvc:mapping path/**/ mvc:exclude-mapping path/login/ mvc:exclude-mapping path/register/ mvc:exclude-mapping path/flower/**/ mvc:exclude-mapping path/static/**/ bean classcom.flowershop.interceptor.LoginInterceptor/ /mvc:interceptor /mvc:interceptors /beans这里有两个关键配置。mvc:resources是给静态资源放行的不配的话浏览器请求css、js、图片会全部404页面光秃秃只有HTML骨架。InternalResourceViewResolver把Controller返回的逻辑视图名转成物理JSP路径比如Controller返回flower/list实际转发到/WEB-INF/jsp/flower/list.jsp。把JSP放在WEB-INF下的好处是用户不能通过URL直接访问JSP必须经过Controller这对后台管理页面的安全很重要。applicationContext.xml里要装的是数据源、MyBatis、事务管理器。context:component-scan base-packagecom.flowershop.service/ 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}/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ property nametypeAliasesPackage valuecom.flowershop.entity/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.flowershop.dao/ /bean bean idtransactionManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean tx:annotation-driven transaction-managertransactionManager/DruidDataSource是阿里开源的连接池毕设用它比用默认的DriverManagerDataSource更贴近真实项目。jdbc.properties里放数据库连接参数注意MySQL 8的driverClassName是com.mysql.cj.jdbc.DriverURL里要带serverTimezoneAsia/Shanghai否则连库直接报时区错误。mapperLocations扫描mapper目录下的XMLtypeAliasesPackage让MyBatis能用实体类名做别名XML里写resultTypeUser就行。MapperScannerConfigurer把basePackage下的接口全扫成Spring BeanController里Autowired注入Mapper接口直接用。3.3 JSPJSTL渲染商品列表EL表达式和forEach的核心写法SSM框架配好之后页面渲染是另一道坎。很多同学在IDEA新建JSP项目后照着网上代码写c:forEach结果页面把标签原样打出来原因是没引入JSTL依赖和taglib指令。pom.xml里要加JSTL依赖javax.servlet:jstl:1.2或按你的Servlet版本选对应的jakarta坐标JSP页面头部写清楚taglib。% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % div classflower-grid c:forEach items${page.list} varflower div classflower-card img src${flower.coverImg} alt${flower.name}/ h3${flower.name}/h3 p classprice fmt:formatNumber value${flower.price} typecurrency/ /p a href${pageContext.request.contextPath}/flower/detail?id${flower.id} 查看详情 /a /div /c:forEach /divEL表达式${page.list}是从Controller的Model里取值varflower定义循环变量页面里用${flower.name}访问商品名称。JSP访问JavaBean属性时实际上调用的是getName()方法实体类属性名要保持驼峰命名。fmt:formatNumber是JSTL的格式化标签用来处理金额和时间格式比在Java代码里拼字符串省事。所有URL前面加${pageContext.request.contextPath}拼上下文路径这样部署到Tomcat不管应用名是什么链接都不会断。3.4 一次表单提交的完整链路从JSP表单到Controller再到数据库商品列表是查询链路表单提交是写入链路。以用户登录为例页面把username和password提交到Controller。Controller RequestMapping(/user) public class UserController { Autowired private UserService userService; PostMapping(/login) public String login(String username, String password, HttpSession session, Model model) { User user userService.login(username, password); if (user null) { model.addAttribute(error, 用户名或密码错误); return login; } session.setAttribute(loginUser, user); return redirect:/flower/list; } }DispatcherServlet收到POST /user/login请求后根据RequestMapping匹配到login方法。参数名username、password与表单的name属性一一对应Spring MVC自动完成类型转换。登录失败时返回逻辑视图名login视图解析器拼成/WEB-INF/jsp/login.jsp渲染成功时返回redirect:/flower/list这是重定向浏览器地址栏变成商品列表地址刷新页面不会再次重复提交表单。这里有个新手常踩的坑Controller方法直接返回字符串如果字符串以redirect:开头Spring MVC发的是重定向否则走视图解析器转发。两者行为完全不同调试时先分清当前是转发还是重定向。forward:前缀也可以强制指定转发但一般只在跨Controller跳转时用。4. 核心业务模块实现登录拦截、购物车与下单扣库存的三个关键点配置链路通了接下来做真正的业务代码。花店系统最核心的四个模块是登录注册、购物车、下单、后台管理。这四个模块覆盖了SSM框架最高频的技能点拦截器、Session与数据库会话、事务、状态流转。做完这四个模块系统主体就成型了。4.1 登录拦截器用HandlerInterceptor拦住未登录用户花店系统有两类需要保护的路由购物车和订单需要用户登录后台管理需要管理员登录。如果每个Controller方法都手写判断session代码会非常啰嗦。常见做法是写一个HandlerInterceptor在preHandle里统一判断。public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); Object loginUser session.getAttribute(loginUser); if (loginUser null) { // 没登录跳回登录页 response.sendRedirect(request.getContextPath() /login); return false; } return true; } }preHandle返回false时请求被拦住不会进入Controller。这里用sendRedirect而不是返回JSON是因为系统是JSP同步跳转模型重定向到登录页对用户最友好。spring-mvc.xml里已经配好拦截器路径把购物车、订单、个人中心相关路径加进mvc:mapping即可登录页、注册页、商品浏览页要排除掉。拦截器只拦截Spring MVC管理的请求JSP页面的直接访问不走拦截器。这也印证了把JSP放到WEB-INF下的价值用户无法绕过拦截器直接打开购物车页面少了一个安全隐患。4.2 购物车存Session还是数据库毕设为什么推荐存数据库网上教程对购物车有两种做法存Session和存数据库。存Session的优点是开发简单不用建表往Session里塞一个List就行缺点也很明显用户换个浏览器、清一下缓存购物车就没了后台完全看不到用户加购数据论文里购物车模块只能写出半页内容。存数据库正好相反需要多写几张表的增删改查但数据持久化用户换设备登录购物车还在管理员还能看到加购活跃度论文内容更充实。花店系统建议存数据库。登录用户点击加入购物车时先查tb_cart是否存在该用户的这条商品记录存在则数量加1不存在则插入新记录。Autowired private CartService cartService; PostMapping(/add) public String addToCart(Integer flowerId, HttpSession session, RedirectAttributes ra) { User user (User) session.getAttribute(loginUser); if (user null) { return redirect:/login; } cartService.add(user.getId(), flowerId, 1); ra.addFlashAttribute(msg, 已加入购物车); return redirect:/cart/list; }RedirectAttributes的addFlashAttribute是重定向时传递提示信息的标准写法。重定向是两次请求第一次请求的request对象在第二次请求里已经不存在了直接用Model传值会丢失flash attribute会把数据放进Session重定向后读取一次立即清除避免刷新页面重复提示。购物车列表页面还要处理两个边界商品下架后购物车显示已下架库存不足时给用户明确提示这两条都写在关联查询里。4.3 下单扣库存事务与并发超卖的两种解法下单模块是整份代码里最脆弱的地方。新手写法通常是先查库存库存足够再扣库存、生成订单。这套流程单用户测试一切正常但并发场景下会超卖——两个用户同时看到库存剩1都执行了扣减最后卖了2件。原因是查库存和扣库存之间别的请求插了进来。正确做法是两条腿走路方法加事务保证三个写操作原子性扣库存SQL写成带条件的原子更新。Transactional(rollbackFor Exception.class) public Order createOrder(Integer userId, Integer[] flowerIds, Integer[] quantities) { // 1. 生成订单主表 Order order new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setStatus(0); // 计算总价并填充收货信息 orderMapper.insert(order); // 2. 扣库存UPDATE 语句带条件 for (int i 0; i flowerIds.length; i) { int rows flowerMapper.deductStock(flowerIds[i], quantities[i]); if (rows 0) { throw new RuntimeException(库存不足); } } // 3. 插入订单明细略 return order; }update iddeductStock UPDATE tb_flower SET stock stock - #{quantity}, sales sales #{quantity} WHERE id #{flowerId} AND stock #{quantity} /updatedeductStock的update影响行数等于0说明库存不足或商品不存在直接抛异常事务回滚订单不落库。关键点在WHERE子句的stock #{quantity}库存扣减和条件判断在同一条SQL里完成这条UPDATE会在行上加锁两个并发请求同时执行时只有一个能更新成功。MyBatis XML里要写转义符直接写会报XML解析错误。只加事务不做条件扣减或只在Java里先查库存再判断并发照样超卖。事务解决的是失败回滚原子更新解决的是并发扣超两者缺一不可。4.4 订单状态流转与后台发货从待付款到已完成订单状态流转在前台和后台各有一半逻辑。前台用户提交订单时状态是0待付款点模拟付款变成1待发货收到货后点确认收货变成3已完成待付款状态下取消变成4已取消。后台管理员看到1待发货的订单点击发货变成2待收货。完整链路是0到1到2到3以及0到4的分支。UPDATE tb_order SET status 1 WHERE id #{id} AND status 0;更新状态时一定要带当前状态条件。上面这条SQL只有订单处于0时才能变成1可以防止用户重复点击付款按钮把状态从1改回1甚至跳变成2也不会出现已取消订单被付款的脏逻辑。Controller里用一个订单状态常量类把所有状态值集中管理前台页面和后台页面都引用同一个常量类不要在每个方法里写魔法数字。后台订单列表需要关联用户名联表查询tb_order和tb_user。分页常用PageHelper查询前调用PageHelper.startPage(pageNum, pageSize)紧跟的那条查询会被自动改写成分页SQL。这个插件好用但参数有个坑下一章细说。5. 避坑与常见问题排查SSMJSP项目最容易翻车的6个现场写SSMJSP的毕设代码出问题不丢人丢人的是报错看不懂、不知道去哪查。我在帮人调这类项目时反复见到下面六个场景每个都按现象、原因、解决记录一下你照着排查能少走不少弯路。5.1 JSP页面中文乱码过滤器配了还是乱现象页面中文显示成问号后台日志里明明能打印出正确的中文浏览器里看到的却是乱码。这个问题在POST表单提交时最典型注册用户名带中文时入库的数据直接变成???登录时怎么都对不上。原因有三层。第一层是JSP文件本身编码不对IDEA新建JSP项目时项目编码如果不是UTF-8中文在保存时已经损坏程序再怎么运行都是乱。第二层是请求参数乱码POST请求没走CharacterEncodingFilter。第三层是响应乱码JSP页面头部的page指令没写contentTypetext/html;charsetUTF-8。解决先在IDEA的File Encodings里把项目编码、属性文件编码全部设为UTF-8再检查web.xml里CharacterEncodingFilter存在且url-pattern为/*最后确认每个JSP页面头部都有page contentType指令。还有一个隐蔽位置是Maven的tomcat7插件如果用了tomcat7-maven-plugin跑调试需要显式配置uriEncodingUTF-8否则GET请求的中文参数会乱。5.2 MyBatis查询商品详情突然报no session现象列表页一切正常点进商品详情页页面报org.apache.ibatis相关的Session is closed或者no session异常控制台指向实体类里取关联分类的那一行。原因MyBatis开启了懒加载实体里关联了Category对象第一次访问flower.getCategory()时SqlSession已经关闭了懒加载没法再去数据库补查于是抛异常。这个坑在关联查询多的时候特别隐蔽因为触发时机取决于你什么时候访问关联属性。解决最稳的方案是不开懒加载花店这种数据量用不上它。如果确实开了把mybatis-config.xml里aggressiveLazyLoading设为false并确保关联查询在Service层事务内完成——事务不结束SqlSession不会提前关闭。也可以改成一次联表查询把商品和分类一次性查出来从根上躲开懒加载。5.3 静态资源全部404页面有HTML没样式现象页面能打开但css、js、图片全挂了F12控制台看到一堆静态资源请求是404页面只剩文字和凌乱的排版。原因DispatcherServlet的url-pattern配了/它接管了所有请求包括/static/css/style.css这种静态文件。Spring MVC默认不处理静态资源所以需要专门放行。这个问题几乎每个SSM项目都会遇到一次越早配好越省事。解决在spring-mvc.xml里加mvc:resources mapping/static/ location/static//和mvc:resources mapping/upload/ location/upload//并把对应目录放在webapp根目录下。注意mapping和location的路径要配对location末尾的斜杠不能省。配完记得重启Tomcat静态资源配置在项目启动时就加载了。5.4 PageHelper分页错位页码从0开始还是从1开始现象用PageHelper之后第一页显示的是第二页的数据或者翻到最后一页直接空白后台SQL日志里LIMIT的参数总是比预期大1。原因PageHelper的pageNum从1开始很多前端分页组件却默认从0开始。如果你在页面里把当前页码减了1再传给startPage第一页会被当成第0页处理行为全部错位。另外一个高频问题是startPage之后又执行了两次查询PageHelper只对紧跟的第一条查询生效第二条查询会被ThreadLocal里残留的分页参数影响查出莫名其妙的结果。解决统一约定pageNum从1开始Controller接收参数时默认值设1前端分页条传什么就传什么不要再做加减法。startPage之后立即执行目标SQL中间不要插入其他查询必要时用PageHelper.clearPage()手动清理ThreadLocal。5.5 打包war部署后JSP访问404本地能跑、服务器上不行现象IDEA里Tomcat跑得好好的mvn package打出war包扔到外部Tomcat的webapps目录启动后访问首页404控制台没有任何明显报错。原因多半是war包结构不对。传统JSP项目打包war时JSP页面必须位于war包的根目录或WEB-INF目录下如果Maven配置把JSP放进了src/main/resources打包后JSP不会出现在正确位置运行时就找不到。另一个常见原因是IDEA里跑的是内置Tomcat用的是项目的target路径外部Tomcat解压war包后目录名是war包名访问地址要带上下文路径。解决用jar tf xxx.war查看压缩包内容确认WEB-INF/jsp目录下JSP文件都在部署后统一用http://ip:8080/应用名/访问。如果要用nginx做入口转发记住nginx本身不解析JSP它只把请求转发给Tomcatlocation里配置转发到8080端口即可否则会出现404或者页面源码被直接显示出来的诡异现象。5.6 MySQL连接时区错误本地没问题、换台电脑就报错现象数据库连接报The server time zone value is unrecognized或者连接成功但时间字段差8小时注册时间比本地时间晚了一大截。原因MySQL 8.0的驱动要求明确指定时区URL里没配serverTimezone时驱动用服务器本地时区不同机器表现不一致。这类问题最容易出现在答辩前换电脑演示的环节代码没改环境变了就崩。解决在jdbc.properties的jdbc.url里拼上serverTimezoneAsia/Shanghai和useUnicodetruecharacterEncodingutf8再顺手加上useSSLfalse避免连接时反复警告。数据库连接串是SSM项目换机器后出错的重灾区启动日志里一看到时区或者SSL的报错先检查这个文件。6. 打包war前的最后一轮自检验证清单和两个不过时的优化点代码全部写完、页面也调通了别急着打包交文档先按下面这份清单过一遍。我吃亏的地方是答辩前一周才发现后台管理页面居然能用普通用户身份直接访问原因就是拦截器只拦截了前台路径admin路径完全漏在外面。检查项操作预期结果未登录访问购物车退出登录后直接输入/cart/list路径跳转登录页普通用户访问后台登录普通用户后访问/admin/**被拦截并提示无权限重复提交订单在订单确认页连续点击提交两次只生成一笔订单库存边界把商品库存设为1下单2件提示库存不足订单不生成中文数据新增商品名含玫瑰、收货地址含中文列表、详情显示正常部署可移植性换一台机器部署war包首页、登录、下单全部正常两个值得做的优化点。一是把传统JSP项目向Spring Boot迁移Spring Boot自带内嵌Tomcat把web.xml和XML配置换成JavaConfig业务代码完全不用动部署从装Tomcat丢war包变成java -jar直接跑。关于springboot2集成jsp的目录结构争议很多我实践下来的结论是JSP必须放在src/main/webapp下访问路径和传统项目保持一致。二是密码加密从MD5升级到BCrypt用Spring Security的BCryptPasswordEncoder替换MD5代码改动集中在注册和登录两个方法论文里能写清楚为什么要加盐、为什么BCrypt比MD5抗彩虹表。最后说一个我坚持到现在的习惯每次改完代码都在本地完整跑一遍注册新用户、加购、下单、后台发货、确认收货这条链路全部走通才算这个版本闭环。这个方法帮我挡掉过不少只改了一个模块、把另一个模块搞挂的尴尬情况。花店系统这种毕设项目能做到代码能跑、边界能讲、论文有图答辩就已经成功一大半了。希望帮到你。本文还有配套的精品资源点击获取