简介:面向毕业设计与课程设计场景,该压缩包为基于 SSM(Spring+SpringMVC+MyBatis)框架的二手车交易网站完整项目,覆盖二手车辆信息发布、浏览检索、预约交易等典型业务模块,适合 Java 初学者、本科及专科毕业生作为项目参考或二次开发基础。包内共 1325 个文件、约 22.8MB,含 127 个 Java 源码、155 个 JSP 页面、137 个 CSS 样式、354 个 JS 脚本,以及图片、字体、XML 配置和 SQL 脚本等资源,前端页面与后端逻辑配套齐全,结构便于直接导入 IDE 运行调试。项目还包含 docx 设计说明、PPTX 答辩演示等材料,可辅助完成论文撰写与答辩准备。目前已有 57 人学习下载,适合需要完整项目案例和系统实现思路的读者。
1. SSM 二手车交易网站设计:从压缩包到跑通前先弄清的三个问题
打开这份「基于 SSM 的二手车交易网站设计」压缩包,先别急着解压导入 IDEA。它是一套把 Spring、SpringMVC、MyBatis 三件套的配置、建表 SQL、用户前台和管理员后台都打包齐的完整课设工程,适合两类人:正在做 Java 毕业设计或课程设计、需要一份能改能答辩项目的人,以及想搞清楚完整业务里三层架构、分页、权限、图片上传怎么落地的新手。它解决的问题很具体:二手车信息发布、条件搜索、在线下单咨询、管理员审核这一整条链路,怎么在一个 SSM 工程里闭环。难点不在某个算法,而在表结构、状态流转、权限拦截和页面联动怎么拼起来,这恰恰是课堂上很少讲的部分。
2. 技术栈与工程结构:SSM 三件套的分工和一个请求的完整链路
2.1 为什么课设场景下 SSM 依然是大量项目的选型
接触这套工程之前,先回答一个绕不开的问题:现在新项目都在用 Spring Boot,为什么还有这么多毕业设计和课程设计在指定 SSM?原因很现实,不少高校的课题库和评分标准至今依然按 SSM 这套经典组合在走,Spring Boot 在简历上好看,但课程大纲更新没那么快。另一个原因是学习价值。SSM 的配置全是显式的:DispatcherServlet 在 web.xml 里手工注册,数据源在 spring-mybatis.xml 里手工配置,Mapper 接口的扫描路径一行行写清楚。你完整跑通一个 SSM 工程,等于把 Spring 容器初始化、SpringMVC 请求分发、MyBatis 动态代理这三件事的底层机制过了一遍,而 Spring Boot 把这些都自动配置了,答辩时反而很难讲出细节。
这套工程里,Controller 层是典型的 @Controller 加 @RequestMapping 风格,Service 是接口加实现类的双件套,持久层是 Mapper 接口配合 XML 映射文件,页面是 JSP 配 JSTL。看代码的顺序我建议是这样:先翻 controller 包看有哪些功能入口,再顺着每个方法调用的 service 找业务逻辑,最后去 mapper 目录看对应的 XML 里 SQL 怎么写。三十分钟基本能把整个项目的功能边界摸清,比上来就看实体类高效得多。
2.2 包结构与一次请求从浏览器到数据库的路径
工程导入后第一件事是看包结构。这套工程里典型的划分是 controller、service、service.impl、mapper、entity、vo 六个层次,各自的职责和代表类可以对照下面这张表:
| 包 | 职责 | 典型内容 |
|---|---|---|
| controller | 接收请求、参数绑定、返回页面或 JSON | CarController、UserController |
| service / service.impl | 业务规则与事务边界 | CarService 接口与实现类 |
| mapper | MyBatis 接口,同名 XML 负责 SQL 映射 | CarMapper.java、CarMapper.xml |
| entity | 与表字段一一对应的实体 | Car、User、Order、Brand |
| vo | 页面展示用的组合查询对象 | CarQueryVO |
一次列表页请求的完整路径是这样:浏览器访问 /car/list?pageNum=1&pageSize=8,web.xml 里注册的 DispatcherServlet 把请求接住,SpringMVC 通过 HandlerMapping 找到 CarController 的 list 方法,方法里调用 CarService 的 queryPage,实现类先算出 offset 再调 CarMapper 的 queryPage,拿到 List 结果返回给 ModelAndView,最后由 JSP 渲染成页面。理解这条链路之后,你改功能就知道该动哪一层:改查询条件动 mapper XML,改业务规则动 service 实现类,改页面展示动 JSP。很多人改需求改到崩溃,就是因为不知道问题出在链路哪一环。
2.3 运行环境与依赖版本:先对齐再谈跑通
SSM 对版本非常敏感,环境不对代码一跑全是玄学报错。我一般建议先按下面这组组合把底座搭好:
| 组件 | 推荐版本 | 注意点 |
|---|---|---|
| JDK | 1.8 | 不建议直接用 17,老框架兼容性差 |
| Tomcat | 8.5 或 9.0 | 用 9.0 时确认 Spring 版本支持 |
| MySQL | 5.7 或 8.0 | 驱动版本必须和 MySQL 对应 |
| Maven | 3.6.x | IDEA 自带或单独装均可 |
pom.xml 里的依赖版本是第一个坑。Spring 用 5.0.x、MyBatis 用 3.4.x、mybatis-spring 用 1.3.x,这套组合经过大量课设验证是稳定的。MySQL 驱动要单独说:本机是 MySQL 5.7 就用 5.1.47,本机是 MySQL 8.0 就得换 8.0.x,同时 jdbc.properties 里的 driverClass 改成 com.mysql.cj.jdbc.Driver,连接 URL 后面补上 serverTimezone=Asia/Shanghai。很多 Communications link failure 报错,根因就是驱动版本和 MySQL 服务端对不上,或者时区参数没加。
resources 目录下三份配置文件要分清职责:jdbc.properties 管连接信息,spring-mybatis.xml 管数据源、SqlSessionFactory 和 Mapper 扫描,springmvc.xml 管控制器扫描、视图解析器和静态资源放行。改配置前先确认改的是哪个文件,改错文件的后果往往是启动成功但功能全挂。
3. 数据库设计:六张核心表、状态流转和图片存储的三个关键选择
3.1 核心表结构与字段选型
这套工程里数据库一般是六张表左右,下面这张表是核心结构。具体表名前缀可能略有出入,但职责基本一致:
| 表名 | 关键字段 | 职责 |
|---|---|---|
| tb_user | id、username、password、phone、role | 用户/卖家/管理员共用一张表,role 区分身份 |
| tb_brand | id、brand_name、logo_url | 品牌字典表,下拉框数据源 |
| tb_car | id、title、brand_id、price、mileage、gearbox、status、cover_img、publish_time | 车辆主表 |
| tb_order | id、car_id、buyer_id、seller_id、status、create_time | 线上意向单 |
| tb_comment | id、car_id、user_id、content、score | 车辆评价 |
字段选型有三个细节值得抄。价格用 decimal(10,2) 而不是 float,否则累计比较时会出现 19.99 变成 19.989999 这样的精度问题;里程用 int 存公里数,不要用 varchar 存"2.3万公里",后面做排序筛选会非常痛苦;状态一律用 tinyint 存数字码,好处在下一节展开。
解压包里一般会带建表脚本,导入后用 source 命令或者 IDE 的导入功能都行。导入后我建议花十分钟对照脚本检查三件事:字符集是不是 utf8mb4、有没有加物理外键、测试数据是否完整。字符集不对会导致中文乱码在数据库层面就发生了,这和后面讲的编码过滤器是两个不同层级的坑。物理外键则建议在课设里直接不要加,逻辑上关联就够了,真加了约束,删数据、批量导入时会冒出一堆报错,得不偿失。
3.2 状态流转:把业务规则收敛成数字码
二手车业务里最容易讲不清的就是状态。tb_car 的 status 沿用这套约定:0 待审核、1 在售、2 已下架、3 已售出。tb_order 的 status:0 待确认、1 已联系、2 已成交、3 已取消。用数字存状态有三个好处:省空间、方便 where 条件过滤、不会出现"在售"和"在售中"这种同一含义两种写法的情况。
真正的难点不在存储,而在状态变更的触发点。拿到这套工程后重点看两条链路:第一,车辆发布后 status 从 0 变成 1,是用户直接触发,还是管理员审核通过后由审核方法触发;第二,买家下单并且确认成交后,车辆 status 从 1 变成 3,这个动作发生在哪个 service 方法里。这两条链路能讲清楚,整个项目的业务逻辑你就吃透了。状态更新代码要收敛在 service 层,用统一的 updateStatus 方法,不散落在 Controller 里到处写 UPDATE,以后要加前置判断就会很容易。
还要留意一个容易被忽略的字段 publish_time,它最好用数据库默认值 CURRENT_TIMESTAMP,而不是由代码传入。原因很简单:应用服务器时间和数据库时间一旦不一致,order by publish_time 的排序结果就漂移了。用数据库默认时间,跨环境的稳定性要好得多。
3.3 图片存储:路径、目录和静态资源映射的三角关系
车辆图片是二手车网站的命门。这套工程通用的做法是:前端表单 enctype 设为 multipart/form-data,后端用 MultipartFile 接收,文件用 UUID 重命名后存到磁盘目录,数据库只存相对路径,页面上用 img 标签直接引用。数据库存相对路径的好处是以后迁移服务器不用改数据,整个上传目录拷走就行。
这里有个必踩的配置:如果 DispatcherServlet 的 url-pattern 配的是 /,它会拦截 /upload 下的所有图片请求,CSS、JS、图片全部 404。必须在 springmvc.xml 里显式放行:
<mvc:resources mapping="/upload/**" location="file:D:/car_upload/" />注意 location 用的是 file: 协议指向磁盘绝对路径,这比指向项目内部目录靠谱。因为 IDEA 的 Tomcat 插件会把项目部署到 target 下的临时目录,图片写进项目里,clean 一次就全没了。数据库里的相对路径保持 /upload/xxx.jpg 不变,真实文件落在 D:/car_upload/ 下,这样刷新图片消失的问题从根上就没影了。图片大小和格式校验也建议在 service 层做一层,比如限制 5MB、只允许 jpg/png,MultipartFile 的 getSize 和 getContentType 直接可用,不用引额外依赖。
4. 核心业务实现:车辆发布、分页搜索与管理员审核的代码路径
4.1 车辆发布:一次完整的 Controller 到 Service 调用
先看发布车辆的入口。Controller 层负责接参和基础校验,业务逻辑下沉到 Service。典型写法是这样:
@Controller @RequestMapping("/car") public class CarController { @Autowired private CarService carService; @RequestMapping(value = "/publish", method = RequestMethod.POST) public String publish(@ModelAttribute Car car, @RequestParam(value = "file", required = false) MultipartFile file, HttpSession session) { User user = (User) session.getAttribute("loginUser"); if (user == null) { return "redirect:/login"; } car.setUserId(user.getId()); car.setStatus(0); // 新发布的车默认待审核 carService.addCar(car, file); return "redirect:/car/myList"; } }这里有个容易忽略的点:@ModelAttribute 会把表单里 name 属性和 Car 字段同名的参数自动绑定。JSP 里的 input name 是 title 就对应 car.title,是 brandId 就对应 car.brandId。绑定方便,但也要防一手:表单里如果混进 id、status 这种不该由用户传的字段,会被一并绑定覆盖。安全写法是只接收需要的字段,或者手动从 request 里取值,这也是答辩时可以说出来的加分点。
Service 里做的是组合事务:先写车辆主记录,再处理图片保存,图片保存失败要回滚数据库操作,保证不会出现"车发布了但图片是坏的"这种脏数据:
@Transactional public void addCar(Car car, MultipartFile file) { if (file != null && !file.isEmpty()) { String fileName = UUID.randomUUID().toString().replace("-", "") + "." + StringUtils.getFilenameExtension(file.getOriginalFilename()); File dest = new File(uploadDir + fileName); file.transferTo(dest); car.setCoverImg("/upload/" + fileName); } carMapper.insert(car); }注意 @Transactional 放在 Service 实现类上,而不是接口上。放在接口上的注解在 JDK 动态代理场景下有时不生效,这是很多"事务写了却不回滚"的玄学来源之一。
提示:uploadDir 建议从配置文件读,不要硬编码成常量,后面换路径时只需要改一处。
4.2 分页搜索:MyBatis 动态 SQL 的典型写法
二手车列表页必然是带条件搜索的分页。常见做法是把查询条件封装成 Query 对象,Mapper XML 里用 where 加 if 拼动态 SQL:
<select id="searchCars" resultType="entity.Car"> select * from tb_car <where> <if test="brandId != null"> and brand_id = #{brandId} </if> <if test="minPrice != null"> and price >= #{minPrice} </if> <if test="maxPrice != null"> and price <= #{maxPrice} </if> <if test="keyword != null and keyword != ''"> and title like concat('%', #{keyword}, '%') </if> and status = 1 </where> order by publish_time desc limit #{offset}, #{pageSize} </select>两个细节值得说透。第一,XML 里的小于号必须用 < 转义,直接写 < 会被解析器当成标签报错,这是 MyBatis 新手必踩的坑。第二,模糊查询用 concat('%', #{keyword}, '%') 而不是 '%${keyword}%',后者是字符串拼接,存在 SQL 注入风险,前者走的是预编译,两者体感差距在关键词带特殊字符时尤其明显。
分页参数怎么算:前端传来 pageNum 和 pageSize,service 层算 offset = (pageNum - 1) * pageSize。也可以用 PageHelper 插件,但课设里手写 limit 反而更好解释,答辩被问分页原理时可以直接答"就是 SQL 的 limit 加偏移量",一句话说清楚。
4.3 管理员审核:拦截器与角色权限的隔离
管理员后台要和前台用户操作隔离开。最常规的做法是 SpringMVC 拦截器,在 preHandle 里检查 session 中的登录对象和他的角色:
public class AdminInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { User user = (User) request.getSession().getAttribute("loginUser"); if (user == null || !"admin".equals(user.getRole())) { response.sendRedirect(request.getContextPath() + "/admin/login"); return false; } return true; } }然后在 springmvc.xml 里注册,只拦 /admin 开头的路径:
<mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/admin/**"/> <bean class="config.AdminInterceptor"/> </mvc:interceptor> </mvc:interceptors>注意 path 写的是 /admin/** 而不是 /admin/,两个斜杠的差别很大,/只匹配一层路径,访问 /admin/car/list 这种多级路径会直接漏掉。审核业务的本质就是状态更新:管理员把 status 从 0 改成 1,或者从 1 改成 2。对应的 SQL 很简单,但要做到只允许管理员触发,这就是拦截器存在的意义。单独加一层判断往往比在 Controller 里到处 if 要干净得多。
5. 部署与避坑:SSM 项目从导入到跑通的 5 个高频翻车点
5.1 启动就报 ClassNotFoundException: DispatcherServlet
现象:Tomcat 一启动就抛异常,找不到 org.springframework.web.servlet.DispatcherServlet,页面直接 500。
原因:用 IDEA 导入 Maven 工程后,依赖虽然下载到了本地仓库,但 Web 模块的 WEB-INF/lib 里并没有打包进这些 jar,或者 Maven 工程没被正确识别成 Web 工程,Artifacts 里少了 lib。
解决:右键项目打开 Project Structure,确认 Facets 里已经添加 Web,Artifacts 的 output layout 里把 lib 目录加进去;然后去 Maven 面板执行 clean 和 package,重新部署 war exploded。验证方法很直接:打开部署目录看 WEB-INF/lib 下有没有 spring-webmvc 的 jar 文件。
5.2 页面和数据库里的中文全是问号
现象:数据库里手工写的中文正常,但页面显示全是问号,或者表单提交后存进数据库的是乱码。
原因:SpringMVC 的 CharacterEncodingFilter 没配置,或者配置了但 forceEncoding 没设成 true。这个过滤器只对请求和响应做编码转换,不设 forceEncoding 时,如果其他过滤器提前写了响应,编码就不会被覆盖。
解决:在 web.xml 里加编码过滤器,并且放在过滤器链最前面:
<filter> <filter-name>encoding</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>encoding</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>还要检查三处:JSP 头部的 pageEncoding 和 contentType 是不是 UTF-8,数据库连接 URL 里有没有 characterEncoding=utf8,建表脚本里表的字符集是不是 utf8mb4。三处常常互相背锅,按 filter、JSP、JDBC URL、表字符集的顺序查。
5.3 数据库连接失败:Communications link failure
现象:启动时创建数据源失败,报 Communications link failure,或者 Access denied for user。
原因:八成是 mysql-connector-java 版本和 MySQL 服务端版本不匹配。MySQL 8 要求驱动至少 8.0.x,而且 driverClass 要从 com.mysql.jdbc.Driver 改成 com.mysql.cj.jdbc.Driver;同时 URL 里不加 serverTimezone 参数,MySQL 8 的驱动会直接拒绝连接。
解决:先确认本机 MySQL 的版本号,再统一改三处:pom 里驱动的版本、jdbc.properties 里的 driverClass、连接 URL 里的时区参数。改完先用 Navicat 或命令行用同一套账号密码连一遍,排除账号权限问题。这一步做完,绝大部分连接报错都能消掉。
5.4 登录后页面样式和 JS 全部 404
现象:页面能打开,但 CSS、JS、图片全部加载不出来,F12 里资源请求全是 404。
原因:DispatcherServlet 映射为 / 后,把所有静态资源请求也拦进了 SpringMVC,而工程里没有对 static 目录做放行配置。
解决:springmvc.xml 里显式加静态资源映射:
<mvc:resources mapping="/static/**" location="/static/"/>也可以用 default-servlet-handler 兜底,但前者更可控,适合静态资源都在同一目录下的工程。改完记得清理 Tomcat 缓存重启。还有一个容易混进来的坑:IDEA 里 Tomcat 部署路径变化导致静态资源根本没被拷进部署目录,这时要回去检查 Artifacts 的资源配置。
5.5 图片上传成功,刷新或重启后图片就没了
现象:发布车辆时图片显示上传成功,数据库里也有路径,但刷新后 img 标签挂掉,或者重启 Tomcat 后图片全部消失。
原因:文件被写进了 Tomcat 的临时部署目录,比如 target 下面的某个目录,而不是一个持久的磁盘路径。每次 redeploy 或 clean 时整个临时目录被删掉,图片自然就没影了。
解决:把上传目录配置成外部绝对路径,比如 D:/car_upload,springmvc 里用 file: 协议映射:
<mvc:resources mapping="/upload/**" location="file:D:/car_upload/" />这样 Tomcat 怎么重启图片都在,也是给自己省掉反复传图的力气。数据库里存的相对路径不用变,只是文件实际落点换到了外部目录。
6. 改造进阶:往 Spring Boot 迁移时保留 MyBatis 映射的一个省力技巧
6.1 核心思路:SQL 不动,配置砍半
这套 SSM 工程里最有复用价值的资产,是 mapper 目录下的 XML 映射文件。手工迁移 SQL 每一条都可能出错,而 Spring Boot 可以直接复用这些 XML。我做过几次这类迁移,最快路径永远是"先跑通再删配置",而不是新写一套代码。
做法是这样的:新建一个 Spring Boot 工程,加入 web 和 mybatis 两个 starter,把原来的 mapper XML 原封不动拷到新工程的 src/main/resources/mapper/ 下,然后配置 application.yml:
spring: datasource: url: jdbc:mysql://localhost:3306/car_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.car.entity接着在启动类上加 @MapperScan("com.example.car.mapper"),原来的 Mapper 接口方法签名一个都不用改。service 和 controller 的代码可以直接拷过来,因为它们依赖的是接口而不是 XML 配置。真正要重写的地方很少:web.xml 里的 DispatcherServlet 配置删掉,springmvc.xml 的视图解析器换成 Thymeleaf 或保留 JSP 依赖,拦截器改成一个 WebMvcConfigurer 的 Bean 注册。
迁移收益是立竿见影的:三份 XML 配置文件消失,启动从配置 Tomcat 变成一键 main 方法,但 mapper XML 里的 SQL 原样保留,省掉了最费时且最容易翻车的 SQL 迁移环节。从那以后,我每次接手这类课设工程,都强制自己先导数据库、对齐版本、跑通一次,再动手改任何逻辑,这个习惯至少让排错时间缩掉一半。希望帮到你。
本文还有配套的精品资源,点击获取