简介:基于SSM+MySQL+JSP实现的水果蔬菜商城系统,是面向计算机相关专业毕业设计、课程设计或期末大作业的完整可运行项目,兼具实际参考与直接交付价值。系统按角色划分为客户端与后台管理端,客户可完成注册登录、浏览商品、管理购物车和查看订单,管理员则能处理订单、客户、商品、类目、公告及留言等核心业务,功能覆盖完整,界面简洁易操作。随包提供约227.54MB的RAR压缩资源,包含2000个文件,涉及451个jpg与411个png等界面图片素材、302个css与227个js等前端样式脚本、73个java与38个jsp等后端代码文件,同时有sql数据库脚本、xml配置文件和运行说明,便于在Tomcat8.0+JDK1.8+IDEA环境下配合MySQL5.7以上版本直接启动。资源中还包含jar依赖包、class编译产物及文档笔记,有助于理解SSM整合流程和项目部署细节。已有129人学习下载,项目经过导师指导并通过高分验收,下载后无需修改即可运行,适合需要快速获取高质量JavaWeb毕设方案的读者。
1. 基于SSM+MySQL+JSP的水果蔬菜商城,这个技术组合到底在解决什么问题
SSM+MySQL+JSP这个组合第一次出现,基本都在JavaWeb的毕业设计清单上。水果蔬菜商城系统就是其中很典型的题目:用户能注册登录、浏览商品分类、把水果蔬菜加进购物车、生成订单,后台还要能维护商品、分类和订单状态。这条链路覆盖了一个业务系统最常见的分层、事务、关联查询和会话管理,所以被当成高分毕设选题一点也不奇怪。
Spring负责对象创建和事务控制,SpringMVC处理浏览器请求的分发,MyBatis把SQL和对象映射绑定在一起,MySQL把用户、商品、购物车、订单数据落库,最后交给JSP渲染成能点击和结算的页面。这个组合的缺点是没有SpringBoot那种开箱即用的自动配置,但反过来,它每一步都看得见,也更方便讲清楚JavaWeb底层的路由、过滤器和会话机制。
这篇笔记按做这个项目的顺序来:先摆架构,再设计数据库,接着把SSM后端接口和JSP页面联调起来,最后把本地运行最常见的几个坑提前标出来。如果你正在做JavaWeb毕设、想找一个能二次改造的商城脚手架,或者想彻底看清SSM框架在业务里怎么协作,可以参考下面这套做法。
2. 先把架构立住:SSM分工、请求链路和本地Maven骨架一次配好
2.1 SSM三个框架在商城系统里各管什么
SSM框架在很多人脑子里是个整体,但进了开发还是要拆开看。Spring管的是对象和事务。水果商城里的ProductService、OrderService、CartService这些类,全部由Spring容器统一创建和装配,Controller里写一个@Autowired,Spring就把实现类注入进来。另一个关键职责是事务,后面下单扣库存那一步,如果没有Spring事务,就会出现“订单写进表了、库存没扣”这种数据不一致问题。
SpringMVC管的是请求分发。浏览器的每一个URL最终都要落到Controller的某个方法上,这个映射就是SpringMVC在维护。DispatcherServlet是它的大门口,通过@RequestMapping把URL映射到方法,把请求参数转换为Java类型。商城项目里,访问/product/list和/cart/add是两类不同请求:一个返回页面视图,一个处理Ajax返回JSON。
MyBatis管的是SQL和Java对象之间的映射。直接用JDBC写库存更新,每次都要处理Connection、PreparedStatement、ResultSet,商品字段一多,重复代码就让人头疼。MyBatis允许把SQL写在XML里,查询结果自动组装成Product、User这样的实体对象。参数用#{}写法会转成预编译占位符,也避开了一部分拼接SQL可能带来的注入问题。
| 框架 | 核心职责 | 本项目里的落点 |
|---|---|---|
| Spring | 容器管理、事务控制 | Service实现类、@Transactional |
| SpringMVC | URL映射、参数绑定、视图解析 | Controller、JSP返回、JSON响应 |
| MyBatis | SQL与对象映射 | Mapper接口、mapper/*.xml |
2.2 从商品列表到结算:一条完整请求链路
以商品列表为例。用户打开首页,浏览器发起/product/list?categoryId=1,请求先到DispatcherServlet,SpringMVC根据HandlerMapping找到ProductController里的list方法,categoryId自动绑定到方法参数。Controller不直接查库,而是调用ProductService;Service里再调ProductMapper接口;Mapper对应的XML文件里有一条查询SQL,最终由MyBatis执行返回商品分类列表。
返回路径再走一层:Controller把列表放进ModelAndView,视图名指向productList.jsp。JSP用EL表达式和JSTL标签把列表渲染成商品卡片,浏览器最终看到带图片、价格和“加入购物车”按钮的果蔬列表页。中间如果出现异常,会逐层抛到全局异常处理器,不会把一堆堆栈信息直接丢给用户。
下单这类操作则走Ajax,前端提交购物车ID数组,Controller返回JSON,JSP端拿到结果再决定跳到订单页还是弹错误提示。理解了这条链路,后面所有功能其实都是在往不同节点里填代码。
2.3 本地环境准备与Maven工程骨架
环境版本建议:JDK 8、Tomcat 8.5或9.0、Maven 3.6+、MySQL 5.7或8.0,IDE用IDEA。MySQL安装教程里最容易被忽略的一步是字符集,装完第一时间把编码改成utf8mb4,再用Navicat连接MySQL验证一次,确认端口、账号、密码都没问题再写代码。很多同学项目跑不起来,不是代码逻辑错,是MySQL服务压根没启动,或者root密码和jdbc.properties里对不上。
工程结构先按分层建好,不要把所有类堆在一个包下。
fruit-store ├── pom.xml └── src ├── main │ ├── java/com/fruit/mall │ │ ├── controller # SpringMVC控制器 │ │ ├── service # 业务接口与实现 │ │ ├── dao # MyBatis Mapper接口 │ │ ├── entity # 与数据库表对应的实体 │ │ └── vo # 页面展示专用对象 │ ├── resources │ │ ├── mapper # MyBatis XML映射文件 │ │ ├── mybatis-config.xml │ │ ├── spring-mybatis.xml │ │ ├── spring-mvc.xml │ │ └── jdbc.properties │ └── webapp │ ├── WEB-INF/web.xml │ ├── WEB-INF/jsp │ └── static └── testpom.xml里最核心的几个依赖如下。
<properties> <spring.version>4.3.7.RELEASE</spring.version> <mybatis.version>3.4.5</mybatis.version> </properties> <dependencies> <!-- SpringMVC会连带引入spring-core/context/beans --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <!-- MyBatis与Spring整合包 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>${mybatis.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.3.1</version> </dependency> <!-- MySQL驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.11</version> </dependency> <!-- Druid连接池 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.10</version> </dependency> <!-- JSP与JSTL --> <dependency> <groupId>javax.servlet</groupId> <artifactId>jsp-api</artifactId> <version>2.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> </dependencies>这里解释几个容易出问题的参数。spring-webmvc一个依赖就把Spring容器相关包带齐了,不用再单独引spring-core。mysql-connector-java选择8.x还是5.1.x,取决于你本地MySQL版本,用8.0就配com.mysql.cj.jdbc.Driver,用5.7就配com.mysql.jdbc.Driver。jsp-api的scope必须是provided,因为Tomcat已经自带Servlet和JSP的API,重复打包会冲突。
web.xml里需要配置SpringMVC的前端控制器和编码过滤器。CharsetEncodingFilter这个过滤器能解决大部分POST请求乱码问题,但数据库、页面、Tomcat三处编码不一致时它也无能为力。
<context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mybatis.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <servlet> <servlet-name>springmvc</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>springmvc</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <filter> <filter-name>encodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>这里其实是两份Spring配置:spring-mybatis.xml管数据源、事务、Mapper扫描,由ContextLoaderListener加载;spring-mvc.xml只管Controller、视图解析器和静态资源,由DispatcherServlet加载。模块拆开,后面维护或排错会清清爽爽。
3. MySQL数据建模:用户、商品、购物车、订单四类表一次设计到位
3.1 建库先定字符集,用户表的设计不能只图省事
创建数据库时字符集要选对,不然之后中文全部变问号。MySQL 5.7以上推荐直接用utf8mb4,它比utf8多支持emoji和生僻字,兼容性也够好。存储引擎用InnoDB,支持事务,下单场景依赖它做回滚。
CREATE DATABASE fruit_mall DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE fruit_mall; CREATE TABLE t_user ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL, password VARCHAR(64) NOT NULL COMMENT 'MD5加密后的密码', nickname VARCHAR(30) DEFAULT NULL, phone VARCHAR(20) DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;username加唯一索引是必要的,注册接口要先查重,这个唯一键就是最后一道防线。password字段长度64,MD5加密后是32位,留一点余量给后续加盐处理。
3.2 商品表和分类表:字段类型、排序和图片路径
果蔬商城的商品属性比服装类简单:名字、价格、库存、图片、描述就够了。分类表单独拆一张,避免把分类名直接写死在商品表里,后续在后台加分类就不用改表结构。
CREATE TABLE t_category ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(30) NOT NULL, sort INT DEFAULT 0 COMMENT '排序值,越小越靠前', PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE t_product ( id INT NOT NULL AUTO_INCREMENT, category_id INT NOT NULL, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0, image VARCHAR(200) DEFAULT NULL, description VARCHAR(500) DEFAULT NULL, PRIMARY KEY (id), KEY idx_category (category_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;价格字段必须用DECIMAL而不是FLOAT或DOUBLE,浮点数在计算总价时会出现4.9999这种误差,下单金额对不上就麻烦了。分类表加个sort字段做自定义排序,商品列表按sort升序再用id降序补充,这是MySQL排序时很常见的组合写法。
3.3 购物车和订单:唯一约束、状态流转与订单明细
购物车表的设计要点是用户和商品不能重复,否则用户点两次加入购物车就会出现两条相同记录。这里用唯一索引约束,配合ON DUPLICATE KEY UPDATE实现数量累加。
CREATE TABLE t_cart ( id INT NOT NULL AUTO_INCREMENT, user_id INT NOT NULL, product_id INT NOT NULL, quantity INT NOT NULL DEFAULT 1, PRIMARY KEY (id), UNIQUE KEY uk_user_product (user_id, product_id), KEY idx_user (user_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;订单表是商城的核心。主订单表存总金额和状态,订单明细表存每个商品快照,包括商品名称、单价、数量。注意这里一定要把商品名称和价格冗余一份,因为商品表以后可能改价、改名,但历史订单必须保留当时的购买信息。
CREATE TABLE t_order ( id INT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, user_id INT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT '0待支付 1已支付 2已发货 3已完成 4已取消', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user (user_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE t_order_item ( id INT NOT NULL AUTO_INCREMENT, order_id INT NOT NULL, product_id INT NOT NULL, product_name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, quantity INT NOT NULL, PRIMARY KEY (id), KEY idx_order (order_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;status字段用TINYINT存数值而不是字符串,后端通过常量类或枚举来做状态流转判断。比如代码里定义OrderStatus.WAIT_PAY = 0,页面用JSTL选择一个数字输出对应中文,这样数据库不冗余,展示也灵活。
3.4 连接池配置:数据库连接池参数到底在调什么
数据库连接池不能只看“能连上就行”。下单场景并发一上来,频繁创建、销毁Connection会让MySQL服务器压力陡增。Druid连接池在这里的作用是维持一批连接复用,参数设置直接影响性能表现。
jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/fruit_mall?useUnicode=true&characterEncoding=utf8&useSSL=false jdbc.username=root jdbc.password=123456<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"/> <property name="maxWait" value="60000"/> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> <property name="configLocation" value="classpath:mybatis-config.xml"/> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.fruit.mall.dao"/> </bean>initialSize是启动时创建的连接数,maxActive是连接池最大连接数,maxWait是取连接的超时时间,单位毫秒。毕设规模下initialSize设5、maxActive设20完全够用,不需要盲目调大,连接数太多反而拖慢MySQL。MySQL连接url里有几个参数也很关键:useUnicode=true和characterEncoding=utf8保证中文不乱码,useSSL=false省去本地环境的SSL握手耗时。如果用MySQL 8.x,url里的serverTimezone也要加上,例如serverTimezone=Asia/Shanghai,否则时间字段会报时区错误。
MyBatis配置文件里有一个开关能省掉很多字段映射的麻烦。
<configuration> <settings> <setting name="mapUnderscoreToCamelCase" value="true"/> </settings> </configuration>打开下划线转驼峰后,数据库的category_id会自动映射到实体字段categoryId,不用每个字段都写resultMap。这个开关建议从一开始就打开,后面写SQL会轻松很多。
4. SSM后端和JSP前端联调:从商品列表到下单结算的完整链路
4.1 Mapper层:用MyBatis XML管理SQL,参数和排序一次配清
Mapper接口先定义方法,XML里再写具体SQL。商品列表需要支持分类筛选和MySQL排序,这里用ORDER BY p.id DESC控制新品优先。
public interface ProductMapper { List<Product> findByCategoryId(@Param("categoryId") Integer categoryId); Product findById(@Param("id") Long id); int decreaseStock(@Param("productId") Long productId, @Param("quantity") int quantity); }<select id="findByCategoryId" resultType="com.fruit.mall.entity.Product"> SELECT id, category_id, name, price, stock, image, description FROM t_product WHERE category_id = #{categoryId} ORDER BY id DESC </select> <update id="decreaseStock"> UPDATE t_product SET stock = stock - #{quantity} WHERE id = #{productId} AND stock >= #{quantity} </update>这里有两个容易被忽略的细节。第一,#{categoryId}会自动预编译,不会拼SQL;第二,库存扣减语句带了AND stock >= #{quantity}条件,SQL执行后返回受影响行数,等于0说明库存不足,后端根据这个返回值判断是否能继续下单。这个写法比先查库存再更新更原子,能天然挡住并发超卖。MySQL update语法本身就支持在SET里引用字段原值再计算,不需要先select出来加加减减再update回去。
4.2 Service层:下单必须开事务,否则库存对不上
Service层是业务规则的集中地。商品列表查询不需要事务,只读方法加@Transactional(readOnly = true)可以减少事务开销。下单方法则必须加事务,因为一次下单很可能涉及插入订单、插入明细、扣库存、删购物车四步,任何一步失败,前面写入的数据都要回滚。
@Service public class OrderServiceImpl implements OrderService { @Autowired private CartMapper cartMapper; @Autowired private OrderMapper orderMapper; @Autowired private OrderItemMapper orderItemMapper; @Autowired private ProductMapper productMapper; @Override @Transactional(rollbackFor = Exception.class) public Order checkout(Long userId, Long[] cartIds) { // 一次性查出购物车与商品表的关联数据 List<CartView> items = cartMapper.selectCheckedItems(userId, cartIds); if (items == null || items.isEmpty()) { throw new BusinessException("购物车为空,无法结算"); } // 先算总金额,再生成订单头 BigDecimal total = BigDecimal.ZERO; for (CartView item : items) { total = total.add(item.getPrice() .multiply(BigDecimal.valueOf(item.getQuantity()))); } Order order = new Order(); order.setOrderNo("OD" + System.currentTimeMillis()); order.setUserId(userId); order.setTotalAmount(total); order.setStatus(0); orderMapper.insert(order); // 写订单明细并扣减库存 for (CartView item : items) { OrderItem oi = new OrderItem(); oi.setOrderId(order.getId()); oi.setProductId(item.getProductId()); oi.setProductName(item.getProductName()); oi.setPrice(item.getPrice()); oi.setQuantity(item.getQuantity()); orderItemMapper.insert(oi); int rows = productMapper.decreaseStock( item.getProductId(), item.getQuantity()); if (rows == 0) { throw new BusinessException( item.getProductName() + "库存不足"); } } // 清掉已结算的购物车记录 cartMapper.deleteByIds(userId, cartIds); return order; } }这段代码里,@Transactional是关键。Spring默认只回滚运行时异常,UncheckedException才会触发回滚,可检查异常则不会。所以用rollbackFor = Exception.class显式声明,让所有异常都回滚。另一个隐蔽问题是事务只在Spring代理对象调用时生效,如果A方法没加事务、在类内部调用了加了事务的B方法,B的事务会失效,因为走的是this调用,没经过代理。排查事务不生效时,先查是不是这个原因。
4.3 Controller层:返回JSP视图还是返回JSON,怎么选
商品列表页需要返回页面,下单按钮需要返回JSON。Controller里的两个方法分别处理这两种情况。
@Controller @RequestMapping("/product") public class ProductController { @Autowired private ProductService productService; @RequestMapping("/list") public String list(@RequestParam(required = false) Integer categoryId, Model model) { List<Product> products = productService.findByCategoryId(categoryId); model.addAttribute("productList", products); return "productList"; } }@Controller @RequestMapping("/order") public class OrderController { @Autowired private OrderService orderService; @RequestMapping(value = "/checkout", method = RequestMethod.POST) @ResponseBody public Result checkout(Long[] cartIds, HttpSession session) { User loginUser = (User) session.getAttribute("loginUser"); if (loginUser == null) { return Result.error("请先登录"); } try { Order order = orderService.checkout(loginUser.getId(), cartIds); return Result.ok(order.getOrderNo()); } catch (BusinessException e) { return Result.error(e.getMessage()); } } }第一个方法的返回值是productList字符串,SpringMVC视图解析器会在前缀/WEB-INF/jsp/和后缀.jsp之间拼出完整路径。第二个方法加了@ResponseBody,返回值Result对象由Jackson序列化成JSON。前端Ajax里拿到res.code === 200再决定跳转还是弹错误。
4.4 JSP页面渲染:EL表达式、JSTL标签与购物车交互
JSP页面用EL表达式取值,配合JSTL标签处理循环和金额格式化。商品列表页核心代码如下。
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <html> <head> <meta charset="UTF-8"> <title>水果蔬菜商城</title> </head> <body> <div class="product-grid"> <c:forEach items="${productList}" var="p"> <div class="product-card"> <img src="${p.image}" alt="${p.name}"> <h3>${p.name}</h3> <p class="price">¥ <fmt:formatNumber value="${p.price}" pattern="0.00"/> </p> <p>库存:${p.stock}</p> <button onclick="addToCart(${p.id}, 1)">加入购物车</button> </div> </c:forEach> </div> <script src="${pageContext.request.contextPath}/static/js/jquery.min.js"></script> <script> function addToCart(productId, quantity) { $.post('${pageContext.request.contextPath}/cart/add', { productId: productId, quantity: quantity }, function (res) { if (res.code === 200) { alert('已加入购物车,请到购物车结算'); } else { alert(res.msg); } }, 'json'); } </script> </body> </html>EL表达式${p.name}会自动调用Product对象的getName方法,不用手动写getter调用。fmt:formatNumber用来格式化小数金额,避免出现19.90显示成19.9这种不整齐的数字。Ajax请求走了contextPath拼接,避免项目部署在根路径以外时URL找不到资源。
购物车页面的核心是选中商品然后结算,提交的是一组购物车ID。后端接收Long[] cartIds,SpringMVC会自动把请求参数绑定成数组,不用逐个解析。
5. 运行与排查:数据库连接、乱码、事务失效这几个高频坑要防
5.1 现象:启动Tomcat后连接池爆红,日志里是“Cannot create PoolableConnectionFactory”
最常见的原因是数据库连接信息不对,或者MySQL服务没有启动。排错顺序很固定:先确认MySQL服务在运行,Linux下用systemctl status mysqld查看,Windows下看服务列表;再用Navicat连接MySQL,用配置里的同一账号密码验证能否连上;最后看jdbc.properties里的驱动类、URL、端口是否匹配。
如果是MySQL 8.x却用了5.x的驱动,会报Public Key Retrieval is not allowed或failed to connect这类错误。把驱动升级到8.x,并将驱动类名改为com.mysql.cj.jdbc.Driver。URL里加上useSSL=false和serverTimezone=Asia/Shanghai两个参数,能消掉一大半连接时区报错。
5.2 现象:JSP页面打开后,JSTL标签显示成乱码或者直接报404
页面顶部用了<c:forEach>标签,但浏览器把它当普通文本输出了,说明JSTL的jar包没有正确加载。Tomcat 8自带JSP引擎,但JSTL实现需要单独引入。
先检查pom.xml里jstl依赖是否在,maven依赖里是否爆红;再用解压方式看生成的war包WEB-INF/lib下有没有jstl-1.2.jar。另外确认jsp-api的scope是provided,如果打成compile,和Tomcat自带的API冲突,也会出现诡异报错。这种拌一拌就好了,解决起来不难,但它属于那种一眼看不出原因的开发坑,很容易让人怀疑代码写错。
5.3 现象:商品名称、用户昵称都能存进去,但页面全是问号
乱码问题要排查四个环节,任何一环不一致都会前功尽弃。JSP页面要声明<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>;MySQL连接url上要有useUnicode=true&characterEncoding=utf8;数据库和表的字符集要确认是utf8mb4,而不是latin1;web.xml里的CharacterEncodingFilter要配置并映射到/*。
排查时先看一个点:直接用Navicat查询这条数据,如果Navicat里显示正常但页面问号,说明是JSP或请求链路编码问题;如果Navicat里就是问号,说明存进去之前就坏了,重点改连接url和建表语句。
5.4 现象:页面能打开,但商品列表是空的,或者实体对象里某些字段为null
能打开页面说明Controller和JSP没问题,空数据和null字段多半出在MyBatis映射。字段找不到的错误我不止一次遇到:数据库表是category_id,实体属性是categoryId,如果没有开启mapUnderscoreToCamelCase,MyBatis默认不会自动做这个映射转换,结果就是categoryId一直为null。
解决方式有两种,一是在mybatis-config.xml里开启下划线转驼峰,二是在XML里给每个字段写<result>映射。建议用前者,一行配置解决所有类似问题。SQL里用别名也是一种补救,但每张表都要写别名,维护起来明显不划算。
5.5 现象:下单成功后订单生成了,但库存没扣
这是事务没生效的典型信号。先看Service方法有没有加@Transactional,再看有没有用rollbackFor = Exception.class。Spring事务默认只回滚运行时异常,如果Service里把异常catch住并吞掉,事务自然感知不到问题。
还有一个隐蔽原因:同一个类里方法之间互相调用。比如checkout方法没加事务,内部调用了加了事务的doStock方法,外层方法没有走Spring的代理,内层事务根本不会启动。解决办法是把事务边界放到最外层方法上,或者拆到另一个Service Bean里。验证事务是否生效的土办法是故意抛异常,看订单和明细是不是都回滚,这个测试比看日志更直观。
6. 验收与加分技巧:让这版果蔬商城摆得上答辩桌
项目能跑通只是第一步,答辩时老师更关心你对自己系统的理解。我的习惯是准备一个小检查清单:商品列表必须能按分类筛选并排序,购物车能改数量,下单后库存减少、购物车清空、订单状态进入待支付,后台能新增和上下架商品,管理员能修改订单状态。任何一条走不通,先回去修基础功能,不要急着加新东西。
线上都跑通之后,留几个加分点。第一,给核心SQL做一次EXPLAIN,比如订单查询按user_id建了索引,用EXPLAIN确认type是ref而不是ALL全表扫描,把这条优化记录写进课设为“MySQL性能调优”部分,比单纯写功能更能体现思考。第二,用PageHelper做商品分页,而不是一次把几百条数据全查出来。第三,给商品列表加一个简单的搜索条件,前端输入关键词,后端用LIKE CONCAT('%', #{keyword}, '%')模糊查询,配合索引说明搜索场景的取舍。
-- 在Navicat或命令行执行,查看SQL是否走索引 EXPLAIN SELECT * FROM t_product WHERE category_id = 1 ORDER BY id DESC;输出结果里如果key列为空,表示没走任何索引。商品表里已经给category_id建了普通索引,这条查询应该在range或ref级别。把执行计划截图放进课设里,就是一个可展示的量点。
这套方案做完以后你会发现,SSM项目最大的收获不是会配几个框架,而是理解了事务边界、对象映射和分层思想。这两件事在之后转SpringBoot、微服务时依然成立。我自己每写一个接口都会有意识先想一遍“这条SQL会不会全表扫描、这个事务边界划在哪”,这个习惯保留下来,能少改很多线上血泪问题的返工。希望能帮到你,把手里这个果蔬商城做成一个真正能讲清楚、能扛住追问的毕业设计。
本文还有配套的精品资源,点击获取