ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SpringBoot+Vue前后端分离二手车交易系统开发与部署实践

SpringBoot+Vue前后端分离二手车交易系统开发与部署实践 聊到二手车交易系统很多人第一反应是这不就是个增删改查吗把车辆信息塞进数据库前端表格一展示完事。真上手做一套前后端分离的二手车交易系统后会发现它比普通后台管理系统多出来的东西不少车辆审核、多条件检索、图片上传、订单流转、三角色权限每一项都牵扯到具体的技术决策。这套基于SpringBootVueMyBatisMySQL的完整项目正好把这些点全部串了起来。这篇文章不打算只贴一份部署命令而是把从业务建模、数据库设计、核心代码实现到部署上线的真实过程拆开讲。适合刚学完SpringBoot和Vue、想拿一个完整项目练手的人也适合需要做课程设计或者打算快速搭建二手车交易网站做二次开发的开发者。看完之后你至少能回答清楚三个问题这套系统为什么这么设计核心代码到底解决了什么问题部署上线时常见的坑在哪里1. 先把业务理清楚二手车交易系统到底在管什么很多项目做不下去不是代码写不出来而是业务边界没划清楚。二手车交易不是一个简单的“卖车”动作它至少包含三件事车辆信息的发布与审核、买家检索与询价、交易订单的状态流转。这三个环节对应后台、前台、交易三个模块把模块之间的数据流理顺代码写起来就顺了。1.1 三个角色与一条完整交易链路这套系统里一共有三个角色管理员、卖家、买家。别小看角色划分它直接决定了路由权限、接口鉴权和页面菜单怎么写。管理员负责车辆审核、用户管理、基础数据维护。二手车平台必须有人工审核环节否则假车源、重复车源会迅速拉低平台可信度。所以车辆发布后不能直接上架要先进“待审核”状态。卖家可以发布车源、编辑自己的车辆信息、下架车辆、查看买家订单。卖家只能操作自己名下的车辆接口层面要做数据归属校验。买家可以浏览车辆列表、按条件筛选、收藏、询价、下单购买。买家看不到“待审核”和“已售出”的车辆。一条完整的交易链路是这样的卖家发布车辆 → 车辆进入待审核 → 管理员审核通过 → 车辆上架展示 → 买家浏览检索 → 买家下单 → 车辆标记已售出 → 卖家确认订单 → 交易完成。这个链路里有两个状态机。一个是车辆状态待审核、已上架、已下架、已售出。另一个是订单状态待支付、待确认、已完成、已取消。很多初学者会把这两个状态混在一起比如给车辆表直接加一个“订单状态”字段这是错误的设计。车辆状态描述的是车辆本身能否被售卖订单状态描述的是某一次交易走到哪一步两者必须分开。一辆车被下单后车辆状态变成“已售出”但订单可能还在“待确认”。如果合并成一个字段后面统计车辆库存和订单数据都会乱。1.2 数据库表设计的核心取舍数据库设计我建议尽量精简但该拆的表一定要拆。这套项目核心表可以控制在六张左右user用户表包含用户名、密码、手机号、角色类型、状态。角色用类型字段区分比单独建角色表简单直接。car车辆表包含标题、品牌、车系、上牌日期、行驶里程、变速箱、排量、排放标准、颜色、城市、售价、车况描述、卖家ID、审核状态、浏览量。car_image车辆图片表包含车辆ID、图片URL、排序号。图片单独拆表不要用逗号拼接存在car表里。orders订单表包含订单编号、车辆ID、买家ID、卖家ID、成交金额、订单状态、创建时间。favorite收藏表买家ID、车辆ID、创建时间。message询价/留言表车辆ID、发送方ID、接收方ID、内容、发送时间。图片单独建表是很多新手不愿意做的觉得多一张表麻烦。实际上二手车一辆车至少五六张图片如果直接存到car表里要么加一堆image1、image2字段要么用逗号拼接。加字段的方式完全没法扩展逗号拼接的方式每次展示都要拆分字符串。拆成子表之后加图片、删图片、设置主图都非常灵活后续如果想做图片懒加载或者分图集展示直接查这张子表就行。车辆表有几个字段要注意。价格字段用decimal(10,2)不要用varchar否则价格区间排序会变成字符串排序10万会排在9万前面。里程字段用int单位是公里。浏览量用于列表排序和热门车源推荐每次详情访问加1即可不需要精确到防刷个人项目没那么高要求。查询频繁的字段要建索引尤其品牌、状态、价格、上牌时间。下面这段SQL可以直接用在初始化脚本里ALTER TABLE car ADD INDEX idx_car_brand (brand); ALTER TABLE car ADD INDEX idx_car_status (status); ALTER TABLE car ADD INDEX idx_car_price (price); ALTER TABLE car ADD INDEX idx_car_create_time (create_time);索引不是越多越好但它对业务查询的提升立竿见影。二手车列表页最常见的操作就是按品牌筛选、按价格排序、按发布时间倒序这三个索引基本覆盖了核心查询路径。2. 技术栈选型为什么是SpringBoot、Vue、MyBatis、MySQL这一套这套组合不是最炫的但一定是最不缺教程、最容易找人问、踩坑成本最低的。选技术栈不是选秀要考虑维护成本和熟悉度。2.1 后端选型的现实考量SpringBoot 2.7 JDK8 是我在这类项目上比较推荐的组合。SpringBoot 3.x 虽然已经普及但它要求JDK17并且把javax包迁移到了jakarta很多老版本的依赖和网上教程会直接报ClassNotFoundException。如果你拿到的是一套成熟源码跑不起来的第一原因往往是JDK版本和SpringBoot版本不匹配。先用SpringBoot 2.7.18 JDK8把项目跑通后续再考虑升级这是最务实的路线。MyBatis 对比 JPA最核心的优势是SQL完全可控。二手车交易系统的查询条件非常不固定品牌、价格区间、里程区间、变速箱类型、排放标准用户可能任意组合条件。这种场景如果用JPQL或者Spring Data JPA的Specification写起来远不如MyBatis的动态SQL直观。你直接在XML里写where加if每个条件一眼就能看懂出了问题也好排查。MySQL 在这个量级下没什么可挑剔的。单表几十万条数据配合索引和分页查询完全扛得住。存储过程、视图这些尽量别用逻辑放在Java代码里后续维护和迁移都更方便。数据库字符集统一用 utf8mb4别用 utf8因为utf8在MySQL里存不了emoji和一些生僻字虽然车辆描述里不一定有但统一用utf8mb4能少一个隐患。2.2 前端Vue与接口约定前端我建议Vue2 Element UI Vue Router Axios。这套组合虽然老但生态成熟稳定坑基本都被前人踩平了。Vue3的Composition API确实更好但如果你是配合源码二次开发尽量别在选型上折腾先跑通再重构。前后端分离不只是“前端一个项目、后端一个项目”这么简单真正关键的是接口约定。如果前后端各写各的联调阶段会非常痛苦。我习惯先约定一套统一响应结构{ code: 200, message: 操作成功, data: {} }后端所有接口都返回这个结构前端Axios响应拦截器统一处理。code为200时取datacode为401时跳登录页code为500时弹出错误提示。这样前端业务代码里不会到处散落try-catch和状态判断。接口路径要遵守RESTful风格比如GET /api/car/list 车辆分页列表GET /api/car/detail/{id} 车辆详情POST /api/car 发布车辆PUT /api/car/{id} 编辑车辆PUT /api/car/{id}/audit 管理员审核车辆POST /api/order 买家下单统一加 /api 前缀是为了部署时的反向代理。前端Nginx可以把这个前缀直接转发到后端服务后面部署章节会专门讲。2.3 用户认证Session、JWT还是Redis前后端分离项目我不推荐用传统Session。Session依赖CookieCookie存在浏览器里跨域、跨端口、移动端调用都会遇到麻烦。用JWTJSON Web Token更符合前后端分离的架构。JWT的核心思路是用户登录成功后后端把用户ID、角色、过期时间等信息签名生成一个token字符串返回前端。前端把token存在localStorage每次请求在请求头里带上Authorization: Bearer token。后端写一个拦截器解析token、校验过期时间、把用户信息放到ThreadLocal或请求上下文里。如果不想引入额外组件JWT不需要Redis项目部署时少一个依赖更稳。但JWT也有缺点服务器端无法主动让token失效。如果用户被封禁或者改了密码JWT在过期前仍然有效。解决这个问题的简单做法是token有效期设短一些比如2小时前端在拦截器里检测到401时自动重新登录。对个人项目来说完全够用。如果后续要做“记住登录状态7天”“强制下线”等功能再引入Redis也不迟。Redis存token可以随时删除管理起来更灵活代价是多一个中间件要维护。教学项目和中小型系统JWT足够。3. 源码实现中最容易卡住的五个点无论你是自己写还是拿源码二次开发下面这几个地方是最容易出问题的。我把它们单独挑出来讲因为这些都是我在实际项目里真正排查过的坑。3.1 车辆多条件检索的SQL怎么写车辆列表页的筛选条件很多品牌、价格区间、里程区间、变速箱、排放标准、城市、排序方式。这些条件组合起来可能是一二三四五六七八个但用户不会一次全选。MyBatis动态SQL就是为这种场景设计的。核心思路是用一个where标签包住所有ifMyBatis会自动去掉第一个AND/OR前缀同时没有条件时不会拼出语法错误的WHERE子句。select idselectCarList resultMapCarResultMap SELECT * FROM car where if testbrand ! null and brand ! AND brand #{brand} /if if testminPrice ! null AND price gt; #{minPrice} /if if testmaxPrice ! null AND price lt; #{maxPrice} /if if testmileage ! null AND mileage lt; #{mileage} /if if testgearbox ! null and gearbox ! AND gearbox #{gearbox} /if if testcity ! null and city ! AND city #{city} /if AND status 已上架 /where ORDER BY ${orderByColumn} ${orderByType} /select注意两点。第一price gt;和price lt;中间的空格不能少否则MyBatis解析XML时会误判成标签的开始或结束。这个错误非常隐蔽报错时你会看到一堆莫名其妙的SQL语法异常。第二ORDER BY ${orderByColumn}使用了字符串拼接而不是预编译的#{}这里必须做白名单校验。排序字段只允许传入create_time、price、mileage、view_count这几个固定值排序方向只允许asc和desc。如果直接把前端参数拼接进去会产生SQL注入风险。我在代码里通常先校验一遍String[] allowOrderColumns {create_time, price, mileage, view_count}; if (!Arrays.asList(allowOrderColumns).contains(orderByColumn)) { orderByColumn create_time; }3.2 图片上传与静态资源映射二手车图片是刚需一个车源至少五六张图。上传流程看起来简单实际部署时出问题最多的就是图片。后端接收文件的接口大致是这样的逻辑校验文件大小和类型然后生成UUID文件名保存到本地磁盘目录把文件相对路径存到car_image表返回给前端拼接后的访问URL。PostMapping(/api/upload) public Result upload(RequestParam(file) MultipartFile file) { if (file.isEmpty()) { return Result.error(文件不能为空); } String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); String fileName UUID.randomUUID().toString().replace(-, ) ext; String dir uploadPath /car/ LocalDate.now().format(DateTimeFormatter.ofPattern(yyyyMMdd)); File dirFile new File(dir); if (!dirFile.exists()) { dirFile.mkdirs(); } file.transferTo(new File(dir / fileName)); String url /api/upload/car/ LocalDate.now().format(DateTimeFormatter.ofPattern(yyyyMMdd)) / fileName; return Result.success(url); }为什么文件名要用UUID因为用户上传的照片可能叫“微信图片_20240101120000.jpg”也可能两个人上传同名文件。不用唯一文件名后上传的会覆盖先上传的这是一个很经典的生产事故。数据库里不要存图片的绝对路径比如D:/project/upload/car/xxx.jpg。一是用户换机器部署时数据库里的路径全部失效二是绝对路径会泄露服务器目录结构。正确做法是存URL路径比如/api/upload/car/20240101/xxx.jpg部署时通过Nginx将/api/upload/映射到磁盘目录这样数据库的路径和部署环境解耦。SpringBoot本地开发时需要配置静态资源映射让/api/upload/**请求能对应到本地磁盘目录。可以在WebMvcConfigurer里加Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/api/upload/**) .addResourceLocations(file: uploadPath /); }注意file:后面要跟带分隔符的路径并且addResourceHandler的路径要以/结尾。如果图片404大概率是这里的斜杠问题。3.3 交易状态机与并发下单在线教育项目里最常见的错误是先查询车辆状态再执行创建订单和修改车辆状态。但在高并发场景下两个买家同时看到一辆“已上架”的车同时点击下单两个请求都查询到“已上架”状态然后各自创建订单最终一辆车被卖了两次。解决方式不要用同步锁也不要用Redis分布式锁单机场景下最简单可靠的是数据库乐观更新。创建订单前先执行一条带条件的状态更新Transactional public Result createOrder(OrderCreateParam param) { Car car carMapper.selectByIdForUpdate(param.getCarId()); if (!已上架.equals(car.getStatus())) { return Result.error(车辆已下架或已售出); } // 创建订单 Order order new Order(); order.setCarId(car.getId()); order.setBuyerId(currentUserId()); order.setSellerId(car.getSellerId()); order.setAmount(car.getPrice()); order.setStatus(待确认); orderMapper.insert(order); // 原子更新车辆状态 int rows carMapper.updateStatus(car.getId(), 已上架, 已售出); if (rows 0) { throw new RuntimeException(车辆已售出订单回滚); } return Result.success(true); }这句update car set status 已售出 where id #{id} and status 已上架是核心。MySQL在同一时刻只允许一个事务把这条记录的status从“已上架”改成“已售出”第二个请求的rows必然为0然后抛异常让整个事务回滚。既不会出现超卖也不用手写复杂的锁代码。事务里有个容易被忽视的点Transactional默认只回滚RuntimeException和Error。如果你在事务里用try-catch把异常接住又没抛出事务不会回滚订单创建了车辆状态回滚了或没改成功数据就错了。所以在Service层做事务控制时尽量别自己catch异常让异常往上层抛交给Spring事务管理器统一回滚。3.4 分页查询和MyBatis字段映射的坑分页我建议自己写limit而不是依赖PageHelper。PageHelper用起来确实方便但它在复杂动态SQL、多表联查、嵌套结果时偶尔会分页失效或者count语句生成错误。自己写分页其实不复杂查询条件里带pageNum和pageSize计算offset后拼上limit再单独写一个count查询返回总条数。int offset (pageNum - 1) * pageSize; ListCar list carMapper.selectCarList(query, offset, pageSize); int total carMapper.countCarList(query);MyBatis里最坑的问题是下划线字段映射。如果数据库字段叫create_time实体类属性叫createTime而MyBatis配置里没有开启驼峰映射查出来的createTime永远是null。这个问题的排查很折磨人页面数据和SQL都能查到就是Java对象里字段为空。在application.yml里加上mybatis: configuration: map-underscore-to-camel-case: true开启之后create_time自动映射到createTime。如果你用的是多表联查比如订单表关联车辆表两个表都有id字段那就不能靠自动映射必须在XML里写resultMap把每个字段的映射关系彻底明确。还有一个经验resultType不要用Map。Map虽然写着方便但下划线字段名会原样出现在Key里前端拿到的JSON字段名全变成create_time这种风格和实体类映射出来的createTime对不上。建议每个查询都定义一个VO类。3.5 跨域问题开发环境与生产环境的两种处理前后端分离必然会遇到跨域。开发环境最简单的方式不是在后端开CORS而是在Vue脚手架里配置devServer代理。前端页面跑在http://localhost:8080后端跑在http://localhost:9090让Vue的devServer把/api开头的请求代理到后端。// vue.config.js module.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:9090, changeOrigin: true } } } }这样前端代码里请求地址直接写/api/car/list浏览器看到的所有请求都指向当前前端域名没有跨域问题。生产环境我一般直接用Nginx解决让前端页面和后端接口在同一个域名下。比如访问https://yourdomain.com是前端页面访问https://yourdomain.com/api时Nginx把请求反向代理到后端的http://localhost:9090。这样对外完全看不出前后端分离也彻底规避了跨域。如果非要后端单独开跨域可以写一个全局CorsFilter允许来源、方法和请求头。但要注意跨域请求会先发一个OPTIONS预检请求这个请求必须被后端正常响应否则POST请求会失败。如果你在后端配置了登录拦截器一定要把OPTIONS请求放行否则前端会报CORS错误但后端日志里看不到任何异常。4. 部署教程从裸机到可访问部署是另一个大坑。很多项目本地跑得好好的搬到服务器上就各种404、白屏、图片加载不出来。这一章我按顺序把从零到能访问的完整步骤写一遍。4.1 部署前的版本与工具准备先确认服务器上的基础环境。Linux服务器建议CentOS 7或者Ubuntu 20.04Windows服务器也能跑但生产环境还是Linux为主。需要安装的东西有JDK 1.8Maven 3.6以上用于后端打包Node.js 14或16用于前端构建MySQL 5.7或8.0Nginx注意Node版本。Vue2项目对Node版本敏感Node 18以上的版本有时会在npm install阶段报OpenSSL错误。如果遇到这种问题优先使用Node 16。前端构建前可以先看package.json里有没有node-sass这类老旧依赖如果有它的版本对Node版本有严格限制。版本确认命令java -version mvn -version node -v npm -v mysql --version nginx -v4.2 数据库初始化如果源码保含SQL脚本通常是一个.sql文件。建议先手动创建数据库再导入脚本这样编码和字符集可控。mysql -uroot -p CREATE DATABASE car_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE car_system; SOURCE /usr/local/car_system.sql;如果你用的是Navicat这类图形工具导入时注意选择utf8mb4编码。SQL脚本里有中文注释或初始化数据时编码错误会导致乱码或者导入失败。导入成功后要改后端配置文件里的数据库连接信息。重点看application.ymlspring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/car_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: yourpassword这里serverTimezoneAsia/Shanghai必加。MySQL 8.0和JDBC驱动版本不匹配时不加时区参数会直接报The server time zone value Öйú±ê׼ʱ¼ä is unrecognized这个是乱码显示的中文时区名不是MySQL真的坏了在连接串里明确指定时区即可解决。4.3 后端打包与启动后端代码拉到服务器或者本地打包后传到服务器。在项目根目录执行mvn clean package -DskipTests-DskipTests跳过测试避免打包过程中因为测试环境不完整而失败。打包完成后target目录下会生成一个jar包名字类似car-system-0.0.1-SNAPSHOT.jar。启动时不要直接java -jar xxx.jar因为一旦关掉终端进程就没了。用nohup放到后台nohup java -jar car-system-0.0.1-SNAPSHOT.jar --spring.profiles.activeprod car.log 21 查看启动日志tail -f car.log看到Started CarApplication in x seconds说明启动成功。如果启动失败99%是数据库连接问题看日志里的Caused by部分不要被前面的长堆栈吓到。后期维护要用到停止和重启ps -ef | grep car-system kill -9 进程号如果你想更规范一点可以配置systemd服务让SpringBoot作为系统服务托管开机自启、崩溃自动重启。不过个人项目先用nohup就够。4.4 前端构建与Nginx配置前端构建前先确认后端接口地址在哪里配置。如果开发环境的Axios基地址写的是http://localhost:9090/api那打包前必须改成/api让前端所有请求都发到当前域名下由Nginx反代到后端否则用户浏览器会直接向后端服务发起跨域请求。安装依赖并构建npm install npm run build构建完成后前端项目下会出现dist目录。把dist目录上传到服务器比如/usr/local/dist。然后配置Nginx。server { listen 80; server_name yourdomain.com; root /usr/local/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:9090; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /api/upload/ { alias /usr/local/upload/; } }这里有几个关键点。try_files $uri $uri/ /index.html;是为了适配Vue Router的history模式。如果前端路由是history模式访问/detail/1时服务器找不到这个物理文件就必须回退到index.html由前端路由接管。如果不写这一行刷新页面就会404。如果你的项目用的是hash模式URL里带#那不需要这个配置但我建议用history模式URL更干净。location /api/upload/单独放在/api/外面是为了让图片请求直接由Nginx读取磁盘文件不经过Java后端。这样图片加载速度快也不占用后端线程。注意Nginx的alias路径末尾一定要有/否则/api/upload/car/1.jpg会匹配到/usr/local/uploadcar/1.jpg这个错误非常隐蔽。配置改完后重载Nginxnginx -s reload然后访问http://yourdomain.com如果能看到首页就说明前端部署成功。4.5 部署中常见的坑我把部署阶段最常见的四类问题整理一下遇到时可以照着排查。第一端口被占用。后端启动失败日志提示端口被占用时先查端口lsof -i:9090 netstat -tlnp | grep 9090要么杀掉占用进程要么在application.yml里改端口。第二前端页面能打开但接口全部报404。先看浏览器Network请求的实际URL是什么。如果请求的是http://localhost:9090/api/...说明前端打包时没有把接口地址改成/api需要重新配置Axios基地址并重新构建。如果请求的是https://yourdomain.com/api/...再看Nginx是否配置了/api/的反向代理。第三图片上传成功但访问不了。上传接口返回了URL但浏览器访问图片返回404。先确认后端配置的静态资源映射路径和实际磁盘路径是否一致再看Nginx的alias路径是否正确。我遇到过很多次图片存在了/usr/local/upload/前端请求/api/upload/xxx但Nginx里忘写alias直接把请求代理到了Java后端后端又没有对应的Controller于是404。第四MySQL 8.0连接报错。如果数据库用的是MySQL 8.0驱动类要写com.mysql.cj.jdbc.Driver不是com.mysql.jdbc.Driver后者是MySQL 5.x的驱动。同时pom.xml里的数据库驱动版本要跟着MySQL版本走dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency5. 拿到项目后怎么二次开发和排错源码跑通只是第一步后续你大概率要在这个基础上加功能、改逻辑。这一章聊一聊二次开发的切入方式以及几个高频报错的具体定位思路。5.1 新功能模块的切入方式如果你想加一个“车辆年检到期提醒”之类的功能不要一上来就写Controller。我的习惯是严格按照数据库 → Mapper → Service → Controller → Vue页面的顺序来加。第一步先设计表和字段。比如车辆表加一个inspection_date字段。第二步在实体类加对应属性。第三步在Mapper接口和XML里加对应的查询方法。第四步写Service业务逻辑。第五步Controller暴露接口。第六步前端写API文件和页面。如果直接先写Controller你很快会发现没有实体类没有Mapper接口根本没法落。逆向开发看起来效率高但中间会反复补课。顺着数据层往上写每一层都知道上一层需要什么出错率最低。前端新增一个页面时有三个地方容易漏路由配置、菜单配置、API文件。如果一个新页面点击后白屏控制台报Failed to resolve component十有八九是路由组件没注册。Element UI的侧边栏菜单如果是根据路由数组自动生成的别忘了同步加菜单项。5.2 性能优化与存储方案扩展二手车列表页如果数据量上来了第一件事是先看慢查询日志。MyBatis可以在配置里开启SQL日志mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl打开后每个查询SQL都会打印耗时和实际语句一目了然。如果发现列表查询很慢先explain看执行计划重点检查有没有走索引、扫描行数有没有超过预期。图片存储从本地上传到对象存储是这类项目做大后的必经之路。本地磁盘存储有单机容量瓶颈也没有CDN加速能力。改造思路不复杂上传文件时传给对象存储拿到一个URL直接存到car_image表Nginx里那个upload映射就可以拆掉了。这个改动对前端完全透明因为前端始终只认图片URL。车辆检索量非常大的时候MySQL组合索引也扛不住全文检索类的需求。这时可以考虑引入Elasticsearch把车辆数据同步到ES查询走ES后端MySQL只作为权威数据源。但我要提醒一句个人项目和中小企业项目数据量没到几十万条之前别引入这个复杂度。MySQL 合理索引 分页完全能满足几千到几万条的数据量。另外可以加个定时清理任务用SpringBoot自带的 Scheduled 注解每天凌晨自动把超过30天未支付且状态为待确认的订单置为已取消把车辆重新置为已上架。这类小功能对系统运营很重要但很多课程设计项目会漏掉。Component public class OrderClearTask { Scheduled(cron 0 0 2 * * ?) public void clearExpiredOrders() { // 查询超过30天未支付的订单 // 更新订单状态为已取消 // 将对应车辆状态恢复为已上架 } }别忘了在启动类或配置类加上EnableScheduling否则定时任务不会生效。这又是一个经常被忽略的配置项。5.3 几个高频报错的处理思路最后说几个我在跑这套项目时处理过的高频报错。第一个启动报Invalid bound statement (not found): xxxMapper.xxxMethod。这是MyBatis最常见的问题意思是Mapper接口的方法在XML里找不到对应的SQL。排查顺序XML文件里的namespace是否和接口全限定名一致方法名是否和XML的id一致XML文件是否被Maven打包到了classes目录application.yml里mapper-locations是否配置了classpath:mapper/*.xml。个人项目里还有一个很隐蔽的原因XML文件放在了src/main/java目录下而不是resources目录结果没被编译进去。把XML移到resources/mapper目录问题一般就解决了。第二个前端点击登录后一直转圈接口报401。先看请求头里有没有带Authorization。如果登录接口本身是401说明认证拦截器把登录接口也拦截了。登录接口需要在拦截器配置里放行还有图片访问、车辆列表等公开接口也要放行。我一般把接口分成两部分不需要登录的公开接口车辆列表、车辆详情、图片访问和需要登录的业务接口发布车辆、下单、收藏、后台管理用路径匹配来区分。第三个Element UI表格数据加载正常但时间显示成一串数字或者2024-01-01T00:00:00.00008:00这种格式。这是JSON序列化时间格式的问题。后端在application.yml里统一配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8这样传给前端的时间字符串就是人类可读的格式前端不用再做二次转换。第四个页面能打开但是菜单和权限很乱。如果角色权限是前端控制的通常逻辑是在登录接口返回用户角色前端根据角色字段决定菜单显示项。排查这种问题先打开浏览器Network看登录接口返回的JSON确认role字段的值。然后再看路由守卫里对比的是不是这个字段。很多人会在后端返回role: admin前端路由守卫却判断 管理员两边不一致导致菜单怎么都不显示。字段值是字符串前后端必须约定一致这是最简单的坑也最容易因为改了一个地方忘了另一个地方而踩中。四、这套项目跑通之后我的一个核心建议是不要急着往里面堆功能。先把你自己的业务疑问写在纸上逐条对着代码找实现弄明白每条数据是怎么从数据库流转到前端页面的。这个过程比单纯复制粘贴源码有价值得多。毕竟二手车交易系统的难点从来不在技术而在于能不能把状态、权限、数据流之间的关系理清楚。理清楚了后面加什么功能都是顺手的事。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进