简介:基于SSM的二手车交易网站设计项目源码包,面向Java开发者、高校毕业生及课程设计人群,帮助理解Spring+SpringMVC+MyBatis整合开发与电商交易流程。压缩包共1325个文件、约22.8MB,涵盖127个Java源码、155个JSP页面、137个CSS样式、354个JavaScript脚本、图片素材以及SQL数据库脚本等,目录结构清晰,便于按模块定位前端展示、后端逻辑与数据持久化代码。已有57人学习下载,适合作为毕业设计与课程设计的完整参考。资源内含数据库建表语句、项目配置文件及运行说明文档,读者可据此搭建运行环境,快速跑通二手车发布、浏览与交易等核心功能,并在此基础上进行功能扩展与界面调整,从而掌握SSM框架从配置到联调的全流程实践。
1. 基于ssm的二手车交易网站设计:一个项目吃透Java Web全链路
拿到“基于ssm的二手车交易网站设计.zip”这个项目的人,通常是三类:正在准备毕业设计的学生、刚转Java开发没多久的初级工程师、还有帮小团队评估技术方案的开发者。ssm指的是Spring、SpringMVC和MyBatis的组合,三个框架各管一摊——Spring管对象生命周期,SpringMVC管HTTP请求分发,MyBatis管数据库SQL和Java对象的映射。这个项目要解决的业务很明确:注册登录、发布车辆、浏览检索、预约看车、交易状态追踪。它之所以值得拆开来看,是因为这套技术栈不复杂但链路完整,能把Java Web从配置到部署的所有基础环节串在一起。接下来我就按实际做这个项目的顺序,从技术选型、数据库设计、业务代码,再到部署排错和优化方向,逐一拆开。
2. SSM技术栈选型与项目骨架搭建:让二手车交易后端先跑起来
2.1 为什么是SSM:三个框架在二手车业务里的边界划分
在Spring Boot大行其道的今天,选SSM看起来有点反潮流,但你要意识到它解决的问题没变:把业务代码和底层基础设施的耦合拆开。Spring的IoC容器负责创建UserService、CarService这些业务对象,Controller里不需要手动new一个Service出来;SpringMVC把前端发来的请求参数绑定到Controller方法上,同时决定返回哪个视图;MyBatis在Mapper接口和SQL映射文件之间建立桥梁,把t_car表里的一行数据变成Java的Car对象。
在二手车交易网站里,这个分工对应的就是一个完整请求的处理链路:用户在前端点击登录按钮,浏览器发POST请求到服务端,DispatcherServlet根据URL找到LoginController对应的方法,Controller调用UserService接口,UserService实现类通过UserMapper执行一条SQL查询,结果再原路返回。每一步的调用关系都非常直白,这对学习阶段的人来说,比什么都重要——出了问题你能迅速判断是哪一层出错。如果换作Spring Boot,自动配置会把很多细节隐藏起来,反而增加了理解的难度。
但这个选择有边界。如果项目要上微服务,或者要前后端彻底分离、接口数量很多,SSM的XML配置和JSP视图渲染方式就会变成负担。二手车交易网站的核心定位是单体应用,业务逻辑不复杂,部署环境也相对单一,SSM这套组合恰好匹配这个定位。
2.2 Maven多模块结构:先搭骨架再写业务
做这个项目时,我见过两种组织方式。一种是把所有代码堆在一个web项目里,包名用controller、service、mapper、pojo硬分;另一种是用Maven多模块拆分成五个子模块。说实话,单模块在几百条数据的测试环境下完全能跑,但一旦功能开始扩展——比如加了收藏、留言、车源管理——你就会发现Controller直接调Mapper的情况越来越多,改一个Service接口要牵连好几个文件。
我倾向于一开始就搭多模块,用依赖关系约束代码走向。父工程pom声明如下:
<groupId>com.cartrading</groupId> <artifactId>ssm-car-trading</artifactId> <version>1.0.0</version> <packaging>pom</packaging> <modules> <module>ssm-car-common</module> <module>ssm-car-pojo</module> <module>ssm-car-mapper</module> <module>ssm-car-service</module> <module>ssm-car-web</module> </modules>各模块的依赖顺序是:web依赖service,service依赖mapper,mapper依赖pojo,common被所有人依赖。这个依赖方向写死后,Controller想直接调Mapper就会被Maven依赖规则拦住,你只能绕道Service层。这种结构在大型项目里被反复实践,在二手车交易网站这么小的项目里提前用上,能让代码生长路径清晰很多。
pom里有些细节值得注意。比如Spring版本和JDK版本强绑定,Spring 5.x要求JDK 8,Spring 6.x则要求JDK 17。如果你用IDE自带的JDK 17还跑着Spring 4.x,启动时大概率会报ClassNotFoundException。版本组合是SSM项目的第一道坎,我建议直接照下面这套稳定组合来配:
<properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <spring.version>5.1.20.RELEASE</spring.version> <mybatis.version>3.5.5</mybatis.version> <mybatis-spring.version>2.0.6</mybatis-spring.version> <mysql.version>5.1.49</mysql.version> </properties> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>${mybatis.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>${mybatis-spring.version}</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> </dependencies>这组版本是我在多个项目验证过能直接跑的组合。Spring 5.1.x同时兼容JDK 8和Servlet 3.1,MyBatis 3.5.x支持动态SQL的全部特性,mybatis-spring 2.0.x解决了Mapper接口扫描和SqlSession管理的问题。MySQL驱动版本这里给的是5.1.49,对MySQL 5.7没问题;如果你本机装的是MySQL 8.0,后面第5章会单独说这个坑。
2.3 Spring和MyBatis的整合配置:三个必调参数
SSM项目里三个配置文件——applicationContext.xml、spring-mvc.xml、mybatis-config.xml——构成了所有代码的运行环境。前两个文件里各有一个参数最容易踩坑。
第一个是数据源的URL。车辆发布时写入车辆描述,查询列表时读出车辆标题,一旦中文字符在某个环节丢了一次编码,数据就再也救不回来。URL参数里必须这样写:
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/car_trading?useUnicode=true&characterEncoding=utf8&useSSL=false"/> <property name="username" value="root"/> <property name="password" value="123456"/> </bean>这里最坑的是URL里的&符号。XML规范要求用&替代&,如果你直接写useUnicode=true&characterEncoding=utf8,XML解析器会报错。很多新手第一次配置数据源失败,报的却是“元素类型必须由匹配的结束标签终止”——原因就在这。
第二个是MyBatis的Mapper扫描路径,这是运行时才会暴露的问题:
<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:mapper/*.xml"/> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.cartrading.mapper"/> </bean>mapperLocations指向Mapper XML文件所在目录,MapperScannerConfigurer的basePackage指向接口所在包。这两者必须同时配对,否则启动时不报错,第一次执行查询才抛出“Invalid bound statement (not found)”。这个错误非常误导人——你大概率会去检查SQL是不是写错了,但实际上就是XML没被扫描进来。
第三个是SpringMVC的注解驱动和静态资源放行。少了annotation-driven,Controller上的@PostMapping、@ResponseBody全部失效;少了resources放行,页面引用的CSS和JS会被DispatcherServlet拦截后404。这组配置是SSM项目里“删掉也不会报错、但页面全是问题”的典型代表。
3. 数据库设计:车辆、用户、订单三张核心表如何支撑交易闭环
3.1 从业务流程反推实体:先想清楚交易链条
数据库设计不能从建表开始,先从业务流程梳理。二手车交易网站的核心链路是:用户注册登录后,作为买家浏览车辆列表、查看详情、联系卖家预约看车;作为卖家发布车源、管理在售车辆、处理买家约看请求。围绕这个链路,核心实体有四类:用户、车辆、订单、以及辅助的收藏/留言记录。
这里有个设计决策需要提前做:买家和卖家要不要分成两张表?答案是不要。大多数二手车交易平台里,一个用户既可能卖车也可能买车,把身份分开会产生大量冗余的关联表。设计上我倾向在用户表上用role字段区分买家卖家身份,同时保持t_user这张表的独立性。车辆和用户之间是典型的多对一关系,一辆车只能属于一个发布者,一个用户能发布多辆车。
订单表是整个设计里最需要想清楚的地方。二手车交易和电商买卖不一样,它不是一个纯粹的线上支付闭环:买家通常先线上看车,再到线下看实车,满意后支付定金或全款。所以订单表里不能只存“已支付/未支付”两个状态,而要设计一整套状态流转。这个状态机设计的好坏,直接影响后续代码的复杂度和可维护性。
3.2 用户表与车辆表DDL:字段类型和索引怎么定
用户表的设计相对简单,但要关注两个细节。username字段上加唯一索引,不仅是为了查询效率,更是为了防止并发注册同名用户产生脏数据。密码字段长度用varchar(64),因为MD5加密后是32位十六进制字符串,BCrypt加密后是60位,留一些余量。建表语句如下:
CREATE TABLE `t_user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(32) NOT NULL, `password` varchar(64) NOT NULL, `phone` varchar(20) DEFAULT NULL, `role` tinyint(1) DEFAULT '0', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;车辆表是整个项目的核心表,字段设计直接影响功能实现的难度。
CREATE TABLE `t_car` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL, `title` varchar(50) NOT NULL, `brand` varchar(20) DEFAULT NULL, `model` varchar(30) DEFAULT NULL, `price` decimal(10,2) NOT NULL, `mileage` decimal(10,1) DEFAULT NULL, `year` int(4) DEFAULT NULL, `description` text, `image_url` varchar(200) DEFAULT NULL, `status` tinyint(1) DEFAULT '0', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_brand` (`brand`), KEY `idx_price` (`price`), CONSTRAINT `fk_car_user` FOREIGN KEY (`user_id`) REFERENCES `t_user` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;字段类型选择里,price用decimal(10,2)而不用float/double,是因为浮点数在MySQL中展示价格会有精度偏差,例如实际展示价格出现9.99而不是10.00。mileage字段用decimal(10,1),因为通常显示“里程:12.5万公里”,一位小数就够。year字段只保存上牌年份,用int(4)表示年份,不存具体日期。description字段用text,因为车况描述可能很长。
索引设计要服务于真实查询。二手车列表页最常见的筛选条件是品牌和价格,所以brand和price各自建立普通索引。如果你经常执行“按品牌+价格区间+状态”的组合查询,那两个单列索引未必是最优解——MySQL一次查询只能用一个索引,组合条件时会选择其中一个,另一个条件就退化为全表过滤。这时就该考虑后文的联合索引方案。外键约束在这个项目里值得保留——它保证车辆发布者必须真实存在于用户表,避免业务层在逻辑上判断之后仍然产生孤儿数据。
3.3 订单表与状态流转:用整数状态还是字符串状态
订单表的设计是区分一个数据库设计是“能用”还是“靠谱”的标准之一。核心问题在于:怎么记录一笔二手车交易从意向到达成的全过程。
CREATE TABLE `t_order` ( `id` int(11) NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL, `car_id` int(11) NOT NULL, `buyer_id` int(11) NOT NULL, `seller_id` int(11) NOT NULL, `price` decimal(10,2) NOT NULL, `status` tinyint(1) DEFAULT '0', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `pay_time` datetime DEFAULT NULL, `finish_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_car_id` (`car_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;buyer_id和seller_id都指向t_user表,不需要额外建两张用户表。订单价格price单独存一份,因为线下交易时实际成交价经常比挂牌价低几千块,不能回头去车辆表里取。order_no用唯一字符串生成规则,例如“ORD”加时间戳加随机数。
status字段的定义必须提前约定清楚。我习惯用tinyint:0是待确认、1是进行中、2是已完成、3是已取消。为什么不用varchar?因为状态值在Java代码里要做逻辑判断,整型比较比字符串比较更快,同时节省空间。但整型状态有个明显缺点——可读性差。项目里应该在Constants类或枚举里定义状态常量,严禁在Controller里直接写if(order.getStatus() == 1)这样的魔法数字。
订单状态流转还牵涉一个事务边界:买家创建订单时,需要同时执行两步操作——在t_order表插入订单,把t_car表中对应车辆的status从0改为1(已售出)。如果这两步之间发生异常,就出现车被标记已售但订单不存在的脏数据。第6章会专门讲这个事务控制怎么做。
4. 核心业务功能落地:登录、发布车辆、检索分页的代码路径
4.1 登录注册与权限拦截:Controller-Service-Mapper三层调用
用户模块是网站的入口,代码结构最能体现SSM的分层思想。Controller负责接收参数和跳转页面,Service负责业务逻辑,Mapper负责数据库读写。注册和登录的两个Controller方法如下:
@Controller @RequestMapping("/user") public class UserController { @Autowired private UserService userService; @PostMapping("/register") public String register(User user, Model model) { // 1. 校验用户名不能重复 if (userService.findByUsername(user.getUsername()) != null) { model.addAttribute("msg", "用户名已存在"); return "register"; } // 2. 密码用MD5加密后入库 user.setPassword(DigestUtils.md5Hex(user.getPassword())); userService.addUser(user); return "redirect:/login"; } @PostMapping("/login") public String login(String username, String password, HttpSession session, Model model) { // 1. 按用户名和加密后的密码查询 User user = userService.findByUsernameAndPassword(username, DigestUtils.md5Hex(password)); if (user == null) { model.addAttribute("msg", "用户名或密码错误"); return "login"; } // 2. 登录成功放入Session session.setAttribute("loginUser", user); return "redirect:/car/list"; } }这段代码里有一个细节值得注意:注册和登录时都对密码做了MD5加密再入库或查询。密码绝不能用明文保存,MD5在SSM项目里足够作为基础防线,因为二手车交易网站的数据敏感度不需要银行级别的加密。但如果你想在简历里写得更漂亮,可以换成BCrypt。
登录成功后的Session对象是整个网站“当前用户是谁”的唯一凭证。后续发布车辆时,需要从Session里取出loginUser,拿它的ID作为车辆表的user_id。如果Session为空,说明用户未登录或登录已过期,这时要重定向到登录页。
这个拦截逻辑单独写在LoginInterceptor里比较合适:
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); if (session.getAttribute("loginUser") == null) { response.sendRedirect(request.getContextPath() + "/login"); return false; } return true; } }然后在spring-mvc.xml里配置拦截路径:
<mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/car/**"/> <mvc:exclude-mapping path="/car/list"/> <mvc:exclude-mapping path="/car/detail/**"/> <bean class="com.cartrading.interceptor.LoginInterceptor"/> </mvc:interceptor> </mvc:interceptors>这个配置的含义是:拦截所有以/car/开头的路径,但列表页和详情页不需要登录就能看,所以排除掉。有人容易把路径写成/*,那会把静态资源也拦截掉,最终页面CSS、JS全部失效。
4.2 车辆发布:表单与图片上传的组合处理
发布车辆是这个项目里最典型的“复合数据提交”场景——页面上既有文本字段(标题、品牌、价格、里程),又有二进制文件(车辆照片)。SpringMVC处理这种组合的惯例是用MultipartFile接收文件,再配合一个MultipartResolver配置。
@PostMapping("/publish") public String publish(@RequestParam("file") MultipartFile file, Car car, HttpSession session) throws IOException { // 1. 判断用户是否已登录 User loginUser = (User) session.getAttribute("loginUser"); if (loginUser == null) { return "redirect:/login"; } // 2. 保存上传的图片到本地目录 String realPath = "D:/uploads/car_images/"; String originalFilename = file.getOriginalFilename(); String filename = System.currentTimeMillis() + "_" + originalFilename; file.transferTo(new File(realPath + filename)); // 3. 把图片URL和当前用户ID组装进Car对象 car.setUserId(loginUser.getId()); car.setImageUrl(filename); carService.addCar(car); return "redirect:/car/list"; }保存图片时文件名用“时间戳+原始文件名”拼接,是为了避免不同用户上传相同文件名图片时互相覆盖。图片保存路径不要写死在代码里,最好做一个配置项,部署时可以切换。如果你保存图片后,结果方法一直运行不出错但是上传图片为空,多半是spring-mvc.xml里没配multipartResolver,表单的enctype类型无法被解析。
multipartResolver在上传时限制文件大小:
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> <property name="maxUploadSize" value="5242880"/> <property name="defaultEncoding" value="UTF-8"/> </bean>maxUploadSize默认5MB,超过后SpringMVC会抛出MaxUploadSizeExceededException,这个错误需要在全局异常处理器里接管,返回“图片大小不能超过5MB”之类的提示。如果不做这层处理,用户看到的将是白屏和堆栈信息。
4.3 车辆检索与分页:MyBatis动态SQL的参数绑定
二手车列表页的检索条件是复合的:用户可能只选品牌,也可能同时选品牌和价格区间。如果为每一种组合单独写SQL,代码会变成灾难。MyBatis动态SQL就是为了解决这个场景存在的。
首先在Mapper接口中定义:
List<Car> searchCars(@Param("brand") String brand, @Param("minPrice") BigDecimal minPrice, @Param("maxPrice") BigDecimal maxPrice, @Param("status") Integer status, @Param("offset") int offset, @Param("pageSize") int pageSize);对应的Mapper XML里写动态SQL:
<select id="searchCars" resultType="com.cartrading.pojo.Car"> SELECT * FROM t_car <where> <if test="brand != null and brand != ''"> AND brand = #{brand} </if> <if test="minPrice != null"> AND price >= #{minPrice} </if> <if test="maxPrice != null"> AND price <= #{maxPrice} </if> <if test="status != null"> AND status = #{status} </if> </where> ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} </select>这段XML里有三个高频踩坑点。第一, 标签会自动去掉第一个条件前多余的AND,如果你写成WHERE 1=1然后到处加AND,也不是不行,但 更规范。第二,XML里写>=必须转义成>=,否则XML解析器会把>当作标签结尾,报错方式非常隐秘。第三,LIMIT #{offset}, #{pageSize}这两个值需要在Controller里算好:offset=(pageNum-1)*pageSize。如果你发现翻页时第一页的数据在第二页重复出现,先检查这个公式有没有写对。
5. 部署避坑与常见问题排查:SSM网站稳定上线的四个关键细节
5.1 数据库连接失败:驱动版本与URL参数错位
现象:Tomcat启动后,访问任何页面都报Cannot create JDBC driver of class 'com.mysql.jdbc.Driver',驱动说找不到,但pom里明明引入了MySQL依赖。
原因:MySQL 8.0及以上版本的官方驱动把驱动类名从com.mysql.jdbc.Driver改成了com.mysql.cj.jdbc.Driver,并且URL里必须追加serverTimezone参数。如果你用的是MySQL 8.0数据库,却按旧项目的配置写了5.1版本的驱动类名,连接就会失败。
解决:先明确本机MySQL的版本。MySQL 5.7继续用5.1.49驱动;MySQL 8.0则要调整三处:pom里换mysql-connector-java的8.0.x版本,driverClassName改为com.mysql.cj.jdbc.Driver,URL末尾加上serverTimezone=Asia/Shanghai。这三处有一个不对,连接都会失败。
5.2 图片上传成功但页面无法访问:上传目录与访问路径不一致
现象:车辆发布时图片上传没有报错,数据库里的image_url字段也存了文件名,但页面用img标签访问时图片显示为404,直接打开浏览器输入图片完整路径也一样打不开。
原因:图片上传到了本地磁盘路径,比如D:/uploads/car_images,但Web应用是部署在Tomcat的webapps目录下,两个位置根本不在一个文件系统空间。img标签引用的/images/xxx/jpg路径是相对于Web应用根目录的,Tomcat在Web应用里找不到这个文件,自然返回404。
解决:常见做法是配置Tomcat的虚拟目录,在server.xml的Host节点下加一行Context映射:
<Context path="/images" docBase="D:/uploads/car_images" reloadable="true"/>这样访问/images/xxx.jpg时,Tomcat会去D盘对应目录找文件。另一种更推荐的方式是改用Controller做图片代理——写一个返回ResponseEntity的接口,根据文件名从指定磁盘目录读字节流返回给前端。好处是不用改Tomcat配置,坏处是多一次网络请求。项目体量小的话,虚拟目录方案就够用了。
5.3 中文乱码:三层编码不一致导致数据进得去出不来
现象:发布车辆后,车辆标题和车况描述在页面上显示为问号,或者读取时正常、写入后变乱码。更严重的还有一种——数据库里存的已经是乱码,怎么改页面编码都恢复不了。
原因:乱码不是某一个环节的问题,而是请求→服务端→数据库整条链路上编码不一致。常见组合是:JSP页面用UTF-8,但数据源URL没有指定characterEncoding=utf8,MySQL连接默认用latin1字符集,写入时就把中文字符变成了无法恢复的乱码。
解决:三层全部统一用UTF-8。数据源URL带上useUnicode=true&characterEncoding=utf8;JSP页面头部写死contentType="text/html; charset=UTF-8";web.xml里加CharacterEncodingFilter,强制所有请求和响应使用UTF-8。这三层都配好后,新写入的数据就不会再乱,但已经乱掉的数据只有重建。
5.4 启动报错找不到Bean:包扫描路径设得太细
现象:Tomcat启动成功,SSM项目部署没有报错,但访问页面时提示No qualifying bean of type 'com.cartrading.service.UserService'。
原因:applicationContext.xml里 context:component-scan 配置的base-package只扫了com.cartrading.service,但UserServiceImpl实现在com.cartrading.service.impl子包里;Spring容器只找到了UserService接口,没有找到实现类,注入时就抛异常。这个错误的隐蔽之处在于编译和启动阶段都不报错,只有在请求真正进来,Spring要注入UserService时才会暴露。
解决:把扫描路径放大到包含接口和实现类的共同上级包:
<context:component-scan base-package="com.cartrading"/>同时检查@Service注解是否写在了实现类上而不是接口上。如果注解在接口上,Spring扫描时同样无法注册Bean。这个坑我踩过一次,排障花了两个小时,最后只是把一行包名改宽。
6. 从“能跑”到“好用”:三个值得投入的SSM改造方向
6.1 车辆检索的联合索引:让分页翻到后几页不卡
做了数据量测试就会发现,单列索引在简单查询下没问题,但组合筛选时MySQL只能命中一个索引,另一个条件退化为全表过滤。这里做个简单的性能对比:
| 查询条件 | 单列索引 | 联合索引(brand, price, status) |
|---|---|---|
| 品牌 = 大众 | 命中brand | 命中联合索引最左前缀 |
| 品牌=大众 + 价格3-8万 | 只命中brand,价格过滤走全表 | 一次索引定位两次范围条件 |
| 品牌=大众+价格3-8万+状态在售 | 最差,全表扫描叠加多个条件 | 完全命中索引 |
代价是每次插入、更新车辆记录时,索引维护的开销会增加一点点。对这个业务量级来说,收益远大于代价。
6.2 给成交流程加上事务:一辆车不能被卖两次
用户下单购买时有两个操作必须同时成功或同时失败:订单表插入新订单,以及车辆表把status从在售改为已售。如果不加事务,前一步成功、后一步异常时,就会出现“车已经卖掉了,但订单不存在”的状态。
解决方法是在Service层方法加@Transactional注解,由Spring管理事务边界。
@Transactional(rollbackFor = Exception.class) public void createOrder(Order order) { // 1. 更新车辆状态为已售 carMapper.updateStatus(order.getCarId(), 1); // 2. 插入订单记录,如果这里抛异常,步骤1回滚 orderMapper.insert(order); }要确保DataSourceTransactionManager已在applicationContext.xml中声明。回滚不生效的常见原因是配置了事务管理器,但用的是原生SqlSession,绕过了Spring的事务边界。出了这类问题,优先检查mybatis-spring的整合方式是否正确。
6.3 统一接口返回结构:少一半联调传话费
SSM项目的Controller直接返回String跳JSP页面没问题,但车列表页如果用了Ajax异步刷新,就需要返回JSON。如果每个方法返回的JSON结构都不一样,前端处理起来就会很痛苦。改进方式是定义一个统一Result对象:
public class Result<T> { private int code; private String msg; private T data; // 构造器、getter/setter省略 }所有Controller接口统一返回Result对象,配合@ResponseBody输出JSON。前端只需判断code是否为200,不成功则展示msg。这个习惯是从项目改造中总结出来的——最开始接口直接返回List ,前端解析逻辑散落在各个JS文件里,改一个接口要同步改三个地方的解析代码。
做这个项目时我最大的体会是:SSM项目本身不复杂,但每一步都有它的“规定动作”。从版本组合、数据源配置到事务边界,踩过一个坑就长一次记性。像分页偏移量计算、图片上传路径映射这些细节,看文档十遍不如自己跑一遍代码记得牢。希望这些拆解能帮你在做同类项目时,少走几趟弯路。
本文还有配套的精品资源,点击获取