二手车交易系统源码实战:SpringBoot+Vue+MySQL从启动到二次开发
二手车交易系统的开发需求这两年一直不少,无论是课程设计、毕业设计,还是小团队接私活,都能碰到类似的单子。市面上的开源项目鱼龙混杂,很多号称“完整源码”的项目,下载下来不是缺依赖就是数据库脚本对不上,真正能下载下来就启动成功的少之又少。这套基于SpringBoot后端、Vue前端、MySQL数据库的二手车交易系统源码,最大的优点就是“可直接运行”,不需要你花一星期去补环境、改配置、对字段。这篇博文我会从项目架构、功能设计、启动步骤、核心业务实现到踩坑排除,完整地拆一遍,无论你是刚接触SpringBoot和Vue的学生,还是想快速接手二次开发的开发者,都能直接对照着操作。
先说下我的测试环境,避免后面步骤对不上:JDK 1.8、SpringBoot 2.7.x、Vue 2.x(vue-cli构建)、MySQL 5.7/8.0都可以跑,Node建议14以上。这套组合在Windows和Linux服务器上都实测过,没有隐藏雷点。下面进入正题。
1. 项目架构与设计思路拆解
1.1 为什么选了SpringBoot+Vue+MySQL这套组合
二手车交易系统的业务本质是什么?其实就是“车源信息的发布、展示、检索、交易意向撮合”,外加后台的审核与管理。它不涉及复杂的实时计算、高并发抢购、消息队列,核心诉求是业务逻辑清晰、开发效率高、部署成本低。
SpringBoot在Java后端里属于“开箱即用”的典型——内置Tomcat,不需要单独配置服务器;依赖管理统一由Maven/Gradle搞定,application.yml里配一下数据源就能跑起来。对比传统的SSH架构(Spring+Struts+Hibernate),SpringBoot省掉了大量XML配置,尤其适合快速交付项目。
Vue前端选型则看中它的组件化开发和前后端分离的协作模式。二手车系统的页面逻辑不算特别复杂,但车辆列表、筛选条件、详情展示、后台表单这些UI模块复用率很高。如果用传统的JSP模板渲染,每加一个页面就要写一堆HTML片段;而Vue把这些模块抽成组件,一个CarCard组件能被列表页、推荐位、收藏夹共用,开发效率明显更高。
MySQL不用多说,关系型存储对交易系统这种强事务、强一致性的场景依然是最优解。车辆信息、用户订单、管理员操作记录之间都有明确的外键关系,MySQL的事务ACID特性可以保证一笔订单从创建到支付的状态流转不会出现数据错乱。
这套架构还有一个隐形的优势——人才储备丰富。随便在招聘网站上搜一下,SpringBoot和Vue的岗位需求多到看不完,这意味着项目后续维护和二次开发不怕找不到人接手。对需要验收项目的学生开发者来说,答辩时也可以很清晰地讲出自己的技术选型理由。
1.2 前后端分离与单体部署的平衡
前后端分离是这个项目的一大特点。前端跑在localhost:8080(默认端口),后端跑在localhost:8081,两者通过RESTful API通信。前端调用后端接口时,通过vue.config.js里的devServer.proxy配置把/api前缀的请求代理到后端地址,这样在开发阶段就不会被浏览器跨域策略拦住。
但要注意,分离只是“代码结构分离”,部署上这套项目依然可以非常简单。后端打包成jar包直接java -jar运行,前端npm run build生成dist静态目录,然后用Nginx把静态页面和API请求统一指到同一个域名下,就完成了上线。很多学生朋友容易把前后端分离理解成“必须两台服务器”,其实完全不需要——Nginx一个端口就能满足。这一点在实际交付时非常关键,因为客户通常只提供一个服务器环境。
项目的核心模块划分也很清楚:
- 用户端:注册登录、车辆浏览、条件搜索、收藏、预约看车、提交订单
- 商家/管理员端:车辆发布与编辑、订单处理、车辆上下架、用户管理、数据统计
这种“用户前台+管理后台”的双端模式是大多数交易系统的标准形态,也是面试官比较认可的业务结构。前后端通过JWT(JSON Web Token)做身份认证,登录取到token后,前端每次请求都把它放进Header的Authorization字段,后端拦截器校验通过后才放行接口权限相关的请求。
1.3 数据库设计思路
数据库是这个项目里最值得细看的部分。二手车交易系统的表设计核心是车辆表、用户表、订单表这三张,另外围绕它们还要有车型品牌表、图片表、收藏表、留言表等辅助表。
车辆表(car)的设计我重点说几个关键字段:car_status用来标记车辆状态,取值范围是“待审核、在售、已售、下架”,这个字段在后端业务里处处被引用——用户只能查看到“在售”状态的车,管理员审核通过后状态才会从“待审核”变成“在售”;seller_id关联商家用户ID,标识车辆归属;view_count记录浏览次数,可以用来做“热门推荐”排行;价格字段建议用decimal(10,2),别用double——浮点数在计算金额时会出现精度问题,虽然车辆标价不像订单金额那样涉及精确分账,但后面如果扩展定金支付功能,用double准会踩坑。
用户表(user)要区分角色类型,我用role字段(字符串或整数枚举)区分管理员、商家和普通买家,前端根据角色动态渲染不同的导航菜单和后端权限控制规则。用户的三级分类在真实的二手车场景里是必要的:商家发布车源,买家浏览购车,管理员审核监管。
订单表(order)则记录买卖双方、对应车辆、成交价格、订单状态(待支付/已支付/已取消/已完成)、创建时间等。注意订单表里最好冗余一份car_title和car_price快照,避免车辆信息后续被修改或下架导致订单详情页出现“已售车型信息丢失”的问题——这种冗余在电商系统里是常规操作,但很多初学者不会留意到。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心功能模块与业务逻辑实现
2.1 用户注册登录与权限控制
用户模块没有用Spring Security那套重框架,而是直接采用JWT + 拦截器的方式。为什么这么选?Spring Security功能强大但配置繁琐,过滤链、认证管理器、密码加密器都要自己调,对于以业务功能展示为主的项目来说太重了。而JWT方案只需要一个拦截器,逻辑直白很多。
JWT的核心流程是:
- 用户提交账号密码,后端验证数据库中的用户记录;
- 验证通过后,用
io.jsonwebtoken.JJWT库生成一段加密token,里面封装用户ID和角色信息; - 前端把token存到
localStorage,每次请求在拦截器里把它注入到Authorization请求头; - 后端拦截器解析token,从claims中拿到用户身份,放入ThreadLocal或直接返回给请求处理链。
简单来说,JWT就是一张“服务器签过名、无法伪造的通行证”。后端只认签名合法不合法、过期没过期,完全不需要在服务器端保存用户会话状态——这也让项目天然支持横向扩展,以后多部署几个后端实例负载均衡,也不需要共享Session。对于防止随便修改用户身份越权操作,这种做法已经足够。
需要提醒一个细节:修改密码或强制下线场景下,JWT是无状态的,签发的token在过期前无法作废。如果你后续要增加“修改密码后所有终端重新登录”的功能,需要在数据库加一个token_version字段,每次修改密码时自增,JWT生成时把这个版本号也写进claims里,校验时发现版本对不上就拒绝。
2.2 车辆信息发布与图片上传
车辆信息的发布是商家的核心操作,也是后台管理审核的重要对象。表单字段比较多:品牌、车系、车型、上牌时间、排放标准、变速箱类型、表显里程、售价、车辆描述、车辆图片等。前端用Vue的双向绑定一次收集,提交后以JSON格式发送到后端。
品牌和车系通常是两级联动选择,后端用户选择一级品牌后,前端根据ID动态加载二级车型数据。这类数据如果在MySQL里一张表保存品牌和车型的树形关系,需要频繁查询;如果数据量不大,更推荐直接在前端维护一份静态JSON配置。系统启动时加载一次,之后内存复用,大幅减少数据库压力。
图片处理是新手容易忽略的重点。这套系统的车辆图片上传走了我建议的标准方案:前端将文件以multipart/form-data上传到后端的/api/file/upload接口,后端把文件保存到服务器磁盘指定目录(比如/data/upload/),返回一个可访问的图片URL,然后在车辆信息提交时把URL存到数据库里。
这里有个容易踩坑的细节:前端页面上图片能不能直接显示,取决于后端静态资源映射是否配置正确。SpringBoot默认只处理/static目录下的资源,如果你把图片保存到了项目外的路径(比如D:/upload/),就必须在配置类里加一个资源映射:
java复制@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 把 /upload/** 请求映射到本地磁盘目录
registry.addResourceHandler("/upload/**")
.addResourceMapping("/upload/**")
.addResourceLocations("file:D:/upload/");
}
}
建议管理系统将图片统一存储在某个固定目录,数据库里存相对路径。这样打包部署后,迁移服务器时只需要把整个目录复制走,数据库里的图片路径不用改动任何东西。
2.3 车辆检索与筛选列表
车辆列表页是用户端访问最多的页面,也是最容易产生数据库慢查询的地方。我看了这套源码里相关的实现,列表页支持多条件组合筛选:品牌、价格区间、车龄、里程、排放标准、变速箱类型等。
后端接口的典型查询逻辑是这样:接收Map<String, Object>类型的查询条件,通过MyBatis-Plus(或手动拼接SQL)动态构建QueryWrapper,非空条件才追加进SQL。
这里给一个实用建议:多条件查询代码写不好就很容易堆If-Else。正确姿势是用MyBatis-Plus提供的条件构造器,或者用MyBatis的<where>标签 + <if>条件判断,保持SQL清晰可读。分页用PageHelper或MyBatis-Plus内置的分页插件,前端传递current和size参数,一页12条左右比较符合浏览习惯。
另外一个业务细节是排序。默认按发布时间倒序,热度较高的车辆可以按浏览量降序展示。我建议在车辆表加一个sort_weight字段(初始值为0,后台可以手动调整数值),综合排序规则的优先级就是:sort_weight > 发布时间 > 浏览量——这样运营人员可以把重点车源置顶,不被新发布的车自动顶掉。这块源码已经是支持了的,可以直接用。
2.4 订单与交易流程
二手车交易和普通电商买卖订单流程有相似之处,但也有自己的业务特点——因为线下看车、验车环节才是重头。该系统在线上订单模块主要完成的是交易意向的表达和定金/全款锁定。
用户点击“我要买车”后,生成一笔订单,关联买家和车辆信息。订单状态流转为:待付款(提交后)→ 已付款(买家支付定金后)→ 已完成(线下完成过户)→ 已取消。系统同时提供一个“取消订单”的入口,方便买家改变主意。
建单的后端逻辑里,有个高并发场景下的关键操作:防止同一辆车被同时下单。如果不做约束,两个买家同时点击购买,可能给同一辆车生成两笔订单。解决办法是对车辆状态加乐观锁或唯一约束,在订单生成时先发送SQL更新车辆状态为“已锁定”,影响行数为0则说明车辆已被别人抢先锁定,直接返回“车辆已售出或已锁定”的提示。
源码里这种逻辑用@Transactional事务注解控制原子性,但要注意事务和内嵌的SQL更新顺序写错会导致锁粒度不够,正确做法是在事务方法的第一条语句执行UPDATE car SET status='锁定' WHERE id=? AND status='在售',利用数据库行锁天然挡住并发,后面的订单插入才安全。
3. 环境准备与一键启动全流程
3.1 本地环境配置清单
开始跑项目之前,先把环境核对一遍,不然启动报错时会无头苍蝇一样到处找问题。下面是我建议的版本组合:
- JDK:1.8 或 11(SpringBoot 2.x对这两个版本支持最稳)
- Maven:3.6+(IDEA自带的话也行,但不建议用太老的3.5)
- Node.js:14.x 或 16.x(Vue2项目不要上Node 18+,部分老依赖会报OpenSSL错误)
- MySQL:5.7 或 8.0(两个版本都测试过了,没问题)
- IDE:IDEA 2020以上即可,需要装Lombok插件
如果MySQL没有安装,Windows下建议直接下载zip压缩包解压,然后执行mysqld --initialize-insecure初始化(生成无密码的root账户),再执行mysqld --install安装成Windows服务,最后用net start mysql启动服务。Linux下用apt install mysql-server或yum install mysql-server就好,注意初始化时的临时密码默认会写到日志文件里,首次登录时要先去找出来。
3.2 MySQL数据库初始化
打开MySQL控制台,先创建数据库,再导入项目里自带的SQL脚本。官方建议命名用了secondhand_car之类,你可以根据自己的偏好改,但要同步修改后端的application.yml配置。
bash复制mysql -uroot -p
create database car_db default character set utf8mb4 collate utf8mb4_general_ci;
use car_db;
source /path/to/项目目录/sql/car_db.sql;
导入SQL脚本完成之后,你会看到系统里已经创建好了所有数据表。需要特别检查的是默认管理员账号——我去看了源码里的SQL文件,初始化数据里通常会写入一个admin/admin123的账号,密码是用MD5或BCrypt加密过的。登录成功之后第一件事建议去后台修改默认密码,别等上线了才想起这事。
MySQL 8.0以上会有一个兼容小坑——默认的认证插件是caching_sha2_password,如果后端使用的JDBC驱动版本较老(比如5.1.x),会出现连接超时或认证失败的报错。解决办法是拿新驱动(mysql-connector-java 8.0.x)替换驱动包版本,或者创建用户时指定mysql_native_password认证方式:
sql复制ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码';
3.3 后端启动步骤
后端项目用IDEA打开,等待Maven把依赖下载完。核心的配置在src/main/resources/application.yml中:
yaml复制server:
port: 8081
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/car_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: 123456
hikari:
maximum-pool-size: 10
minimum-idle: 2
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
注意serverTimezone=Asia/Shanghai这个参数一定不能少,MySQL 8.0默认时区与本地不一致,不带这个参数启动后所有时间字段会差8小时。
然后直接运行启动类的main方法。正常情况下,控制台会打印SpringBoot的启动横幅,最后显示Started Application in x.xx seconds。再顺手验证一下接口是否通了——浏览器访问http://localhost:8081/api/car/list,如果返回JSON,说明后端已经起来了。
3.4 前端启动步骤
前端项目用VSCode或WebStorm打开,在项目目录下执行命令:
bash复制npm install
npm run serve
npm install有时候会非常慢,国内建议先设置淘宝镜像:
bash复制npm config set registry https://registry.npmmirror.com
启动成功之后,控制台会打印:
text复制App running at:
- Local: http://localhost:8080/
打开浏览器访问这个地址,进入系统首页;如果你在登录界面登录管理员账号,再进后台管理界面。前端启动步骤到这里就结束了,前后端已经打通。
如果你发现前端页面能打开但接口请求报404,又是路径404、不是500错误,大概率是前端代理配置没生效。打开vue.config.js检查proxy里的目标地址是不是指向了后端端口8081:
javascript复制module.exports = {
devServer: {
port: 8080,
proxy: {
'/api': {
target: 'http://localhost:8081',
changeOrigin: true
}
}
}
};
3.5 数据验证与页面体验
整套系统跑起来后,先逛一遍用户端、管理后台各个功能页面,确认数据正常显示和操作能成功提交。重点关注几个关键场景:注册一个新用户账号、发布一辆测试车辆信息(用一张本地图片测试上传)、用管理后台审核这条车辆通过、在用户列表页查找到该车辆、模拟下一条订单。把这套流程走完,基本上核心功能就没有大问题了。
4. 关键代码实现与二次开发扩展
4.1 数据库表设计精华
我把核心的表结构梳理一下,二次开发前先理解底层数据模型,后面的改造会顺利得多。重点的业务表大概有以下这些:
| 表名 | 核心字段 | 说明 |
|---|---|---|
| user | id, username, password, role, phone, avatar | 用户表,role区分管理员/商家/买家 |
| car | id, brand_id, series_name, title, price, mileage, register_date, car_status, seller_id, view_count, sort_weight, description | 车辆表,car_status标明状态 |
| car_image | id, car_id, image_url, sort | 车辆图片表,一辆车对应多张图 |
| brand | id, brand_name | 品牌表 |
| series | id, brand_id, series_name | 车系列表,与品牌表关联 |
| order | id, order_no, car_id, buyer_id, seller_id, deal_price, status, create_time, finish_time | 订单表 |
| favorite | id, user_id, car_id, create_time | 收藏表 |
| comment | id, car_id, user_id, content, create_time | 留言/咨询表 |
这套表结构非常规范,没有多余的冗余设计,适合直接作为项目设计的“参考答案”。唯一建议扩充的是加一张car_audit_log(车辆审核日志表),记录管理员对车辆状态的每次变更操作,方便审计追溯。在二手车业务场景里,车辆审核异常是监管关注点,有这个表会让你在答辩时多一个亮点。
4.2 JWT登录拦截实现示例
看源码里,后端实现了一个JwtInterceptor类,实现了SpringMVC的HandlerInterceptor接口,在preHandle里做token校验:
java复制public class JwtInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 放行登录接口
if (request.getRequestURI().contains("/api/user/login")) {
return true;
}
String token = request.getHeader("Authorization");
if (token == null || token.isEmpty()) {
// 返回401
response.setStatus(401);
return false;
}
try {
Claims claims = JwtUtil.parseToken(token.replace("Bearer ", ""));
request.setAttribute("userId", claims.get("userId"));
request.setAttribute("userRole", claims.get("role"));
return true;
} catch (Exception e) {
response.setStatus(401);
return false;
}
}
}
拦截器注册方式也比较常规,在WebConfig里addInterceptors,并用addPathPatterns("/api/**")指定拦截范围,再用excludePathPatterns()排除登录注册、车辆查询等公开接口。
这里有一个很多初学者会忽略的问题——针对管理员的接口权限控制。比如“审核车辆”接口不能随便让普通用户调。如果在拦截器里只判断“token是否合法”,那一个普通用户只要自己注册个账号、拿自己的token去调审核接口,就绕过权限了。正确做法是在拦截器解析token拿到角色后,再去比对接口要求的最低角色等级:
java复制// 伪代码示例
if (request.getRequestURI().contains("/api/admin/")) {
String role = (String) request.getAttribute("userRole");
if (!"ADMIN".equals(role)) {
response.setStatus(403);
return false;
}
}
源码里这种角色校验是通过自定义注解@RequireAdmin加上Spring AOP实现的,思路清晰,值得学习。
4.3 车辆列表查询优化
列表查询的核心其实就在动态查询条件的构建上。我看了一遍源码,用的是MyBatis-Plus的QueryWrapper,大概逻辑是:
java复制QueryWrapper<Car> wrapper = new QueryWrapper<>();
if (StringUtils.isNotBlank(query.getBrandId())) {
wrapper.eq("brand_id", query.getBrandId());
}
if (query.getMinPrice() != null) {
wrapper.ge("price", query.getMinPrice());
}
if (query.getMaxPrice() != null) {
wrapper.le("price", query.getMaxPrice());
}
if (StringUtils.isNotBlank(query.getStatus())) {
wrapper.eq("car_status", query.getStatus());
} else {
wrapper.eq("car_status", "在售");
}
wrapper.orderByDesc("sort_weight").orderByDesc("create_time");
需要注意的是,如果列表查询还要关联品牌名、卖家昵称等信息,直接用QueryWrapper是搞不定的,需要写自定义SQL关联查询。建议查询走Mapper XML里的自定义SQL,自己控制JOIN的效率和返回字段,避免一次性查出大字段导致列表页卡顿。
一个小的优化点:“关键字模糊搜”不要用LIKE '%keyword%'这种全模糊匹配,在数据量大时索引会失效。完全匹配项用普通索引,模糊匹配可以在索引上做前缀匹配;如果非要全模糊搜,用全文索引(Fulltext)或者上Elasticsearch——这套系统的数据量级还不需要ES,但如果在答辩时能聊出这个优化方向,考官印象分会好很多。
4.4 车辆图片上传接口实现
图片上传接口看起来简单,实现的细节里潜藏着不少可以优化的位置——文件大小限制、类型校验、文件名防重、存储路径规划等:
java复制@PostMapping("/api/file/upload")
public Result upload(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return Result.error("文件不能为空");
}
// 限制文件大小5MB
if (file.getSize() > 5 * 1024 * 1024) {
return Result.error("图片大小不能超过5MB");
}
String originalFilename = file.getOriginalFilename();
String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
if (!Arrays.asList(".jpg", ".jpeg", ".png", ".gif").contains(suffix.toLowerCase())) {
return Result.error("图片格式不支持");
}
// 生成新文件名,避免冲突
String newFileName = UUID.randomUUID().toString().replace("-", "") + suffix;
// 保存文件
File dest = new File(UPLOAD_DIR + newFileName);
file.transferTo(dest);
return Result.success("/upload/" + newFileName);
}
这里用了UUID做文件名就是为了防止重名覆盖,还有一种方案是用时间戳加随机数,但UUID完全没有撞名风险。如果你后续要接入云存储(比如阿里云OSS),只需要把file.transferTo(dest)替换为调用OSS的SDK上传方法,再返回OSS的访问URL——接口层代码几乎不用动,这也是分层的好处。
4.5 Vue前端核心页面设计
前端部分我建议重点看路由配置和状态管理。项目采用了典型的前后端分离SPA结构,Vue Router负责页面跳转,Vuex(或Pinia)负责登录状态和用户信息的全局管理。
路由设计上,用户端和管理后台分别是独立的布局:用户端有首页、车辆列表、车辆详情、个人中心等路由;管理后台在/admin路径下有车辆审核、用户管理、订单管理等路由。路由守卫里加入登录判断:
javascript复制router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token');
if (to.path.startsWith('/admin') && !token) {
next('/login');
} else {
next();
}
});
Vue的组件化优势在前端也体现得很充分。车辆卡片CarCard组件可以做得很灵活,通过props传入车辆数据对象,列表页循环渲染。接收的车辆数据格式是固定的,如果后端字段命名和前端不一致,可以在封装层做一次数据映射,保持组件内部纯展示逻辑的简洁。
4.6 二次开发的方向与建议
如果把这套系统作为基础,后续可以扩展的方向很多,按性价比排序:
- 增加在线预约看车功能:用户选定车辆后可以选择预约时间段,商家收到预约提醒后确认,形成完整的线下看车链路。这样只需要新增预约表和相关的CRUD接口,与现有订单体系平滑衔接。
- 接入地图定位:给车辆增加所在城市和经销商门店位置,列表页可以按城市筛选,详情页直接展示商家地图位置。
- 引入消息通知:用户收藏的车降价时、商家审核结果出来时,通过短信或站内信通知用户。这块可以先用简单的站内信表实现,后期再接入微信模板消息。
- 管理后台数据看板:用ECharts展示每日新增车辆数、成交量、成交金额趋势图,这些数据在现成的order表和car表里都有,只需写几个聚合统计SQL,不值得大动干戈。
- 车辆检测报告模块:二手车交易中检测报告是建立信任的重要环节,可以在车辆详情里展示一份检测报告(表格形式记录外观、发动机、变速箱等项目的检测结果)。
5. 常见问题与排查技巧实录
跑通这个项目的过程中,我整理了一份出问题频率最高的清单,希望能让你少走一些弯路。
5.1 后端启动失败高频原因
问题1:端口被占用。 如果提示Port 8081 was already in use,说明8081端口已经被别的进程占了。在Windows命令行执行:
bash复制netstat -ano | findstr 8081
taskkill /PID 进程号 /F
或者直接改application.yml里的server.port换一个端口。
问题2:数据库连接失败。 如果启动时报Communications link failure,大概率是MySQL服务没启动,或者IP、账号密码配置错误。先用命令行mysql -uroot -p试一下本地数据库是否正常连接。还有一个隐蔽点——如果数据库和代码不在同一台服务器,要确认服务器防火墙没有屏蔽3306端口。
问题3:Mapper扫描不到。 如果你启动后提示Invalid bound statement (not found),说明MyBatis的Mapper接口和XML文件没有对应上。检查启动类上的@MapperScan注解是否扫描到了正确的包路径,以及application.yml里mapper-locations是否覆盖了XML文件的路径。
5.2 前端启动失败高频原因
问题1:npm install 报错。 大多数情况下是依赖下载超时或版本冲突。先设置淘宝镜像,再删掉node_modules和package-lock.json重新安装:
bash复制npm config set registry https://registry.npmmirror.com
rm -rf node_modules package-lock.json
npm install
问题2:Node版本过高导致OpenSSL错误。 如果你用的Node 17以上版本,运行npm run serve时可能会报:
text复制Error: error:0308010C:digital envelope routines::unsupported
这是因为webpack 4不支持新版本Node的OpenSSL API。解决方案有两条:要么把Node降到16.x,要么在启动命令前设置环境变量:
bash复制set NODE_OPTIONS=--openssl-legacy-provider
npm run serve
问题3:跨域请求失败。 浏览器Network面板看到的请求如果被CORS拦截,优先检查vue.config.js的proxy配置是否生效,以及后端CORS配置是否允许了对应来源。开发环境只配置Vue的代理就足够了,千万不要在前端代码里写死后端IP地址。
5.3 业务功能异常问题速查
| 现象 | 原因分析 | 解决办法 |
|---|---|---|
| 图片上传后页面不显示 | 后端静态资源映射未配置 | 检查WebConfig里的addResourceHandlers映射路径 |
| 登录成功后刷新页面就退出 | token保存到sessionStorage而不是localStorage | 改用localStorage持久化保存token |
| 分页数据总是返回全部 | 前端未传current/size参数 | 检查请求参数绑定,加上默认分页值 |
| 审核车辆后前端状态不刷新 | 审核成功后没有重新拉取列表 | 在Vue回调里重新调用列表接口 |
| 用户管理页面不显示用户列表 | 后端接口权限未放开 | 检查管理员拦截器是否放行了该接口 |
5.4 数据库相关的注意点
数据库大小写的问题在Windows本地开发不容易暴露,但部署到Linux服务器上就是致命伤。建议在建表时统一使用小写表名,并确保后端SQL里的表名与库里的完全一致。MySQL在Linux下默认区分大小写,Windows不区分,如果源码里写的是Car而建表是car,在Linux环境就会报Table not exist错误。
MySQL 8.0的时区问题也需要统一处理。不只是JDBC连接串要加serverTimezone,数据库本身的全局时区最好也做了设置:
sql复制set global time_zone = '+08:00';
set time_zone = '+08:00';
不做这步,你在后台统计交易记录按天分组时,数据会整体偏移到前一天夜里。
6. 部署上线与运维要点
6.1 打包构建
本地开发跑通之后,部署上线只是两步。后端在项目根目录执行:
bash复制mvn clean package -DskipTests
生成target目录下的xxx.jar,然后在服务器上运行:
bash复制nohup java -jar xxx.jar --spring.profiles.active=prod > app.log 2>&1 &
前端打包:
bash复制npm run build
生成dist目录,里面全是静态文件。把这个目录整个复制到服务器上的Nginx配置的web根目录,然后配置反向代理:
nginx复制server {
listen 80;
server_name yourdomain.com;
location / {
root /var/www/dist;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://127.0.0.1:8081;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
后端打包时,建议把生产环境的数据库地址、账号密码等配置放进独立的application-prod.yml,启动时用--spring.profiles.active=prod指定,避免把本地开发配置带到生产上。
6.2 上线前的安全性检查清单
- 修改默认管理员密码和数据库root密码
- 关闭后端端口的外网直接访问(只允许Nginx通过内网访问8081端口)
- 配置HTTPS证书,
location /里加SSL配置 - 定期备份MySQL数据库,至少每天一次;备份命令可以写进crontab定时任务
- 检查上传目录是否设置了禁止执行脚本的权限
这几点里,最容易忽视的是“关闭后端端口外网访问”。很多读者朋友在服务器上直接java -jar跑起来后,发现公网都能通过http://服务器IP:8081/api/xx访问到后端接口。如果某个管理接口没有严格鉴权,数据在这个口子上就是裸奔状态。正确做法是Nginx监听80/443,后端只绑定127.0.0.1:8081,所有外部流量只走Nginx转发。
6.3 性能层面的基础优化
二手车交易系统的访问量不会大到需要微服务架构的程度,但基础的数据库优化还是要做的:
- 给
car表的brand_id、car_status、create_time建立联合索引 - 给
order表的buyer_id建立索引 - 分页查询超过100页后使用子查询延迟关联,避免深度分页的性能问题
- 车辆列表页设置Redis缓存,车辆浏览数据变更时再删除缓存,减少数据库直接压力
这套项目做完基础优化后,应对日活几千的中小规模使用场景是绰绰有余的。
二手车交易系统的源码跑起来不难,难的是理解它每一层设计背后的业务逻辑。我建议你在启动成功之后不要急着交作业或上线,花一两天时间把JWT认证的完整链路、车辆状态机的流转、订单的事务处理逻辑全部走一遍,把这些代码吃透,之后无论面试聊项目还是二次开发扩展新功能,都会从容很多。这套系统最可贵的地方是数据结构足够规范,业务流程完整,踩坑的地方都已经替你填平了,剩下的就看你怎么把它玩出自己的花样了。
