尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

SpringBoot+Vue二手车交易系统开发实战:从需求分析到部署上线

SpringBoot+Vue二手车交易系统开发实战:从需求分析到部署上线 1. 二手车交易系统到底要解决什么从需求倒推项目边界做这个项目之前我一直觉得二手车交易系统是个挺成熟的品类随便找一个开源项目改改就能用。直到自己真去跑了一遍业务才发现没那么简单。先说个背景去年家里换车我把旧车挂在几个平台上卖结果一天能接七八个电话约看车的时间、地点全乱套中间还差点被车贩子压价套路。后来我索性自己写一套系统专门管车辆信息发布—买家浏览预约—线下成交后归档这一条完整的交易链路。这套系统最终做出来长这样车主注册登录后可以发布车辆信息包括品牌、型号、上牌时间、行驶里程、排放标准、售价、实拍图买家用关键词、价格区间、车龄、里程这些条件筛选车辆看到合适的车提交预约看车申请管理员在后台审核车辆是否真实、状态是否合规、价格是否合理还可以管理用户、处理交易记录。整个流程走下来解决了三个核心痛点一是所有车辆信息有统一录入规范不会像在论坛发帖那样格式混乱二是预约看车和交易状态有系统记录谁预约了、看到什么程度、最后谈没谈拢都有迹可循三是管理员能从后台看全局数据哪些车卖得快、哪些价格虚高一目了然。这个项目适合两类人参考一类是正在做JavaWeb毕设或课程设计的学生另一类是刚工作不久、想完整掌握前后端分离联表查询权限控制这套技能的开发者。它的代码量不大但麻雀虽小五脏俱全能把SpringBoot、Vue、MyBatis、MySQL这几样东西怎么配合干活讲清楚。我在给系统划边界的时候特意砍掉了几个看起来热门但实际会拖垮进度的功能比如在线支付、实时聊天、AI估价。原因很简单交易系统的核心价值是信息撮合状态管理支付属于金融级业务需要资质和复杂的对账逻辑实时聊天要引入WebSocket复杂度翻倍AI估价依赖大量市场数据个人项目根本喂不饱模型。先把核心主链路做扎实比堆一堆半成品功能有意义得多。1.1 交易场景里的角色划分与权限诉求做系统第一步不是写代码而是把角色想清楚。我在这个项目里划分了三种角色普通用户包含买家和卖家两种身份、系统管理员。普通用户注册后可以发布车辆、编辑自己发布的车辆、上下架自己的车辆同时也能浏览所有车辆、提交预约看车。管理员则负责审核车辆、管理用户状态、查看交易记录、统计核心数据。这里有个容易踩坑的细节如果发布车辆和浏览车辆属于同一个角色在权限上其实是有冲突的——比如用户A发布的车辆用户B能不能编辑显然不能。所以后端做权限控制时不能只靠一个角色标识一刀切还必须加入数据归属校验也就是判断当前登录用户是否是这条车辆记录的所有者。我后面在实现章节会详细说这个校验怎么写。1.2 业务闭环怎么串起来我一开始画业务流程图时特别容易乱后来抽象成一条主链路才清晰起来用户发布车辆 → 管理员审核 → 车辆上架可被检索 → 买家提交预约 → 卖家确认 → 线下看车成交 → 管理员归档交易记录。这条链路里最容易被忽略的是审核这一环。不少同学做类似系统时会把车辆发布和上架当成同一个操作用户提交完直接就显示在列表里了。但现实中二手车平台一定有人工审核的否则虚假车源、盗图凑数的信息会泛滥。我在表设计里给车辆表加了一个status字段用数字区分草稿、待审核、已上架、已下架、已售出五种状态每次状态流转都由后端接口严格校验。这个设计虽然多写了很多判断逻辑但后续做管理端统计时特别顺手。2. 技术选型不是跟风SpringBootVueMySQLMyBatis这套组合到底赢在哪很多人拿到一个项目标题第一反应是用SpringBootVue就完事了但问一句为什么不用SSM为什么不用JPA就答不上来了。我做选型时考虑过好几套方案最后定下来的组合里每个组件都是经得起推敲的。2.1 后端框架SpringBoot解决的是配置地狱早期的SSM项目光配置文件就有数据源、MyBatis工厂、事务管理器、SpringMVC视图解析器、web.xml等六七份新手配置一小时还没开始写业务逻辑。SpringBoot最大的价值是约定优于配置它通过自动配置把常见组件的初始化过程接管了。我只需要在pom.xml里引入依赖写一个application.yml再用SpringBootApplication注解启动一个可运行的Web服务就成了。这对快速验证业务逻辑非常有帮助。当然SpringBoot不是把配置消灭了而是把通用配置变成默认值把个性配置留在配置文件里。比如我在application.yml里配置了数据源、MyBatis的Mapper扫描路径、文件上传大小限制这些都是项目需要自定义的部分。如果你想把自动配置的细节摸透可以启动时加--debug参数看自动配置报告这个技巧后面部署部分会提到。2.2 数据层选MyBatis而不是JPA控制SQL的主动权二手车交易系统里最难写的是多条件筛选查询。买家可能同时按品牌、价格区间、车龄、里程、排量、变速箱类型筛选而且每个条件都可选可不选。这种场景我用MyBatis的动态SQL处理起来非常舒服——写一个where标签配合if标签按需拼接条件代码清晰且性能可控。JPA抽象层次更高但遇到复杂查询时要么写JPQL要么用Specification要么退回去用原生SQL反而多了一层转换成本。MyBatis直接面向SQL联表查询、子查询、聚合统计都按数据库原生语法写出了问题也容易排查。唯一要注意的是Mapper接口和XML文件一定要对应好我后文会专门讲这个坑。2.3 前端Vue组件化让管理后台开发效率翻倍管理后台这种页面千篇一律、逻辑重复率高的项目用Vue的组件化开发非常合适。我把侧边栏、头部导航、表格、表单弹窗都封装成独立组件页面之间通过Vue Router切换路由数据状态用Vuex4.x版本对应Pinia但我当时用的是Vue2生态所以还是Vuex管理。列表页和详情页分别对应路由/cars和/cars/:id组件复用率相当高。选择Vue还因为它的生态对国内开发者太友好了Element UI组件库拿来即用表格分页、日期选择、上传组件全都现成能节省大量样式和交互的编码时间。如果你用的是Vue3建议搭配Element PlusAPI基本兼容文档也齐全。2.4 MySQL版本选择的一个提醒开发环境我用的是MySQL 5.7生产环境用的MySQL 8.0。如果你是刚接触建议直接从8.0开始。一方面8.0是长期支持版本另一方面默认的字符集是utf8mb4对中文和表情符号支持更好。MySQL 5.7默认字符集经常是latin1如果建库时没注意指定utf8mb4插入中文会出现乱码而且数据进去后再改字符集非常麻烦。我建库时的统一操作是CREATE DATABASE car_trade DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;这样建出来的库从根上杜绝了中文乱码问题。3. 数据库设计交易系统的表结构是骨架字段设计决定开发顺不顺利数据库设计是整个项目里最不能急的环节。我见过很多半途返工的项目问题基本都出在表结构没想清楚。二手车交易系统的核心表可以归纳为四张用户表、车辆表、预约表、交易记录表。下面挨个说设计思路和关键字段。3.1 用户表角色和状态必须分开存用户表字段其实很简单id、username、password存的是BCrypt加密后的哈希值不是明文、phone、role0表示普通用户1表示管理员、status0正常1禁用、create_time。这里要说两个细节。第一密码加密用BCrypt而不是MD5。MD5是摘要算法被彩虹表攻击的风险很大BCrypt是专门为密码哈希设计的算法自带随机盐同样的密码每次生成的哈希都不同。Spring Security里直接有BCryptPasswordEncoder可以用。第二role字段没有单独建角色表因为业务里只有两种角色建表反而复杂化了。如果以后要扩展多角色、多权限再拆也不迟。3.2 车辆表状态机字段是业务逻辑的核心车辆表是最重要的表字段如下字段名类型说明idbigint主键自增user_idbigint发布人ID关联用户表brandvarchar(50)品牌如宝马、奥迪modelvarchar(50)车型如3系、A4Llicense_datedate上牌日期mileagedecimal(10,2)行驶里程万公里pricedecimal(10,2)售价万元gearboxvarchar(20)变速箱手动/自动displacementvarchar(20)排量如1.5T、2.0Lemission_standardvarchar(20)排放标准国五/国六colorvarchar(20)颜色descriptiontext车况描述cover_imagevarchar(255)封面图地址imagestext图片地址逗号分隔statustinyint0草稿1待审核2已上架3已下架4已售出view_countint浏览次数create_timedatetime发布时间update_timedatetime更新时间status字段是整个系统最核心的业务字段。我在实现时定义了一个状态机用户发布后是1待审核管理员通过后变2已上架卖家手动下架变3已下架买家预约后交易成功变4已售出。每个状态之间的跳转都是有权限和条件的比如已售出只能从已上架状态流转且必须由管理员确认。3.3 预约表和交易记录表关联关系要设计成快照而不只是外键预约表的字段是id、car_id、buyer_id、seller_id、appoint_time期望看车时间、status0待确认、1已确认、2已取消、3已完成、remark备注、create_time。交易记录表字段是id、car_id、buyer_id、seller_id、deal_price实际成交价、deal_time、remark。这里有一个很多初学者想不到的设计细节交易记录里的deal_price不能直接关联车辆表的price而要单独存一个值。原因是车辆的标价和最终成交价往往不一样如果只存一个关联ID回头查历史记录时价格早就变了。这种把交易发生时的信息复制一份存下来的思路叫快照设计在订单、账单、合同这类强历史追溯需求的表里是基本操作。同理交易记录里也冗余存储了buyer_id和seller_id而不是通过预约表间接查减少一次联表。3.4 索引设计查询慢很多时候是没建对索引车辆列表页是流量最大的页面搜索条件集中在brand、price、mileage、license_date这几个字段上。我在建表时给这些字段建了联合索引ALTER TABLE car ADD INDEX idx_brand_price (brand, price); ALTER TABLE car ADD INDEX idx_mileage (mileage); ALTER TABLE car ADD INDEX idx_status_create (status, create_time);idx_status_create这个索引是给管理员后台用的——后台列表通常要按时间倒序查某个状态下的车辆两条查询条件正好命中联合索引。如果只建了status的单列索引虽然也能用但回表次数更多。这里的原则是把查询频率最高的两列组合成联合索引比给每个列单独建索引更高效。4. 后端核心模块实现登录鉴权、车辆审核、动态筛选一个都不能少后端我用标准的Controller、Service、Mapper三层结构。下面挑几个技术含量比较高的模块详细展开会贴关键代码片断。4.1 登录认证与接口拦截登录这块我用JWTJSON Web Token做无状态认证。用户登录成功后后端生成一个带过期时间的Token返回给前端前端存到本地每次请求在请求头里带Authorization: Bearer token。后端写一个拦截器对除登录、注册、车辆列表查询之外的接口做Token校验。拦截器核心逻辑如下public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String authHeader request.getHeader(Authorization); if (authHeader null || !authHeader.startsWith(Bearer )) { response.setStatus(401); return false; } String token authHeader.substring(7); try { Claims claims Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(token) .getBody(); request.setAttribute(userId, claims.get(userId, Integer.class)); request.setAttribute(role, claims.get(role, Integer.class)); return true; } catch (Exception e) { response.setStatus(401); return false; } }解析出来的userId和role放到request属性里后面的Controller就能直接用了。管理端接口还要再做一层角色校验判断role 1才放行。这里要提醒一个安全细节JWT的密钥不能硬编码在代码里至少也要放到配置文件中最好用环境变量注入。我见过直接把密钥写在常量类里的项目一旦源码泄露任何人都能伪造Token登录管理员账号。4.2 车辆发布与审核状态流转发布车辆接口看起来只是把表单数据插入车辆表但有几个细节必须处理第一必须从登录拦截器里拿当前用户的ID作为user_id不能用前端传的参数第二车辆初始状态固定为1待审核不能让用户自己选第三封面图和详情图要分开存储。审核接口则是管理员的专属操作。管理员把车辆从待审核改为已上架时本质上是一个状态更新操作。我的实现是这样的PutMapping(/admin/car/audit) public Result auditCar(RequestBody AuditRequest req) { // 校验当前用户是管理员这个在拦截器或AOP里做 Car car carMapper.selectById(req.getCarId()); if (car null) return Result.error(车辆不存在); if (car.getStatus() ! 1) return Result.error(只有待审核状态的车辆才能审核); car.setStatus(req.getPass() ? 2 : 3); if (!req.getPass()) car.setAuditRemark(req.getRemark()); carMapper.updateById(car); return Result.success(); }这个接口的关键在先查状态再改状态。如果不加状态判断管理员对同一辆车重复点击审核状态会被覆盖成不同值造成逻辑混乱。这就是状态机设计对业务规则的约束。4.3 多条件筛选查询MyBatis动态SQL的主场车辆列表页的筛选条件我刚才说过了前端把选中的条件拼成GET参数传给后端后端用MyBatis的动态SQL组装查询。核心Mapper如下select idsearchCars resultTypecom.example.entity.Car 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 testminMileage ! null AND mileage gt; #{minMileage} /if if testmaxMileage ! null AND mileage lt; #{maxMileage} /if if testgearbox ! null and gearbox ! AND gearbox #{gearbox} /if AND status 2 /where ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} /select注意几个细节。第一gt是在XML中的转义写法直接用会解析报错第二AND status 2写在where内部但不在if里这样无论前端选了什么条件都只能查出已上架车辆避免待审核车辆泄露到公网列表第三分页是用LIMIT #{offset}, #{pageSize}实现的offset (pageNum - 1) * pageSize在Service层算好再传入。4.4 图片上传别把文件存进数据库图片上传是个经常被做错的功能。我见过有人把图片转成Base64字符串直接存MySQL的TEXT字段结果一张图几十万字符数据库很快就被撑爆查询也慢得离谱。正确做法是图片文件存到服务器磁盘或OSS对象存储数据库里只存图片的访问URL。我本地开发的实现是配置了一个静态资源映射目录spring: servlet: multipart: max-file-size: 5MB max-request-size: 20MB然后写一个上传接口把文件保存到D:/upload/目录返回可访问的URL。为了让Vue能访问到这个目录我在SpringBoot里加了映射配置Configuration public class WebConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceHandler(file:D:/upload/); } }这样前端访问http://localhost:8080/upload/xxx.jpg就能看到图片了。部署到Linux服务器时把路径改成绝对路径比如/home/app/upload/其余逻辑不变。5. 前端实现思路Vue负责交互体验Element UI负责管理端效率后端接口都通了前端就是把这些接口翻译成用户能看懂的界面。我前端的整体目录结构大概是src/views放页面组件src/api放接口请求方法src/router放路由配置src/store放用户状态管理。下面挑三个前端开发中比较关键的模块说说。5.1 Vue Router动态路由设计前端访问控制的核心在路由。我的路由分两部分静态路由和动态路由。静态路由包括登录页/login、注册页/register、首页车辆列表/cars、车辆详情/cars/:id这些不需要登录就能访问。动态路由则是后台管理页面包括用户管理、车辆审核、交易记录、数据统计。用户在登录成功后前端拿到角色信息如果是管理员就动态添加管理路由如果是普通用户就不添加。Vue Router有一个addRoutes方法Vue Router 4 中改成了addRoute可以动态注册路由。有一点要提醒前端路由隐藏不等于权限安全。即使前端不显示管理入口用户手动访问管理页面URL请求还是会发到后端。所以真正的权限控制一定在后端接口上前端的动态路由只是提升用户体验的手段。我在项目里把这两层都做了后端拦截是安全底线前端动态路由是交互优化。5.2 车辆列表页的状态管理车辆列表页要维护很多筛选条件如果每个条件都用组件内部data存储组件销毁再重建时状态就丢了。我的做法是把筛选条件放到Vuex中即使用户从列表页跳到详情页再返回筛选条件依然保留。具体流程是列表页mounted时检查Vuex里有没有筛选条件有就恢复没有就请求全量数据。筛选条件变化时通过watch触发重新查询。这个交互细节很影响体验——很多初学者做出来的是返回列表页就回到第一页条件全没了用户翻了几页后不小心点进去一个车回退还得从头筛选非常烦躁。5.3 后台管理表格分页、筛选、状态操作一条龙管理端我用Element UI的el-table展示车辆数据配合el-pagination做分页el-tag展示状态标签。审核操作直接用弹出框管理员可以写上/下架理由。表格加载时我会传给后端两个必要参数pageNum和pageSize把筛选条件也一并传过去。后端返回的是{ total: 100, list: [...] }这样的结构前端拿到后渲染表格并更新分页器。这里有个习惯对调试很有帮助在api目录里把每个后端接口封装成独立函数比如searchCars(params)、auditCar(data)、getUserList(params)。这样后端的接口变动只影响一个文件业务组件里不会直接出现axios.get(/car/search?page1)这种裸请求可维护性好很多。6. 编译、打包与部署前端如何塞进SpringBoot里一起跑开发调试时前后端分离跑两个服务前端占8080端口Vite/WebpackDevServer后端占8081端口。但最终交付给用户时不可能要求人家同时启动两个服务。最常见也是最稳妥的方案前端构建完生成静态资源文件SpringBoot把它当作静态资源一并加载。这样用户只需要java -jar一条命令就能启动整个系统。6.1 前端构建并复制到后端前端项目构建npm run build构建完成后会在dist目录生成index.html和一堆css、js资源。我的做法是把dist目录下的所有文件复制到SpringBoot项目的src/main/resources/static目录下重新打包即可。但这里有个大坑。前端用Vue Router的History模式时访问/cars这样的路由SpringBoot默认返回404因为它没有这个接口。解决办法有两个第一种把Vue Router改成Hash模式URL带#如/#/cars请求路径始终是index.html不会出现404。这种模式部署简单兼容性也好缺点是不美观。第二种在SpringBoot里配置一个兜底路由所有非/api开头的路径都转发到index.htmlConfiguration public class WebConfig implements WebMvcConfigurer { Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController(/{path:[^\\.]*}).setViewName(forward:/index.html); } }我实际项目里用的是第二种因为URL更清爽。这个配置要注意正则表达式要排除带点号的路径否则图片、JS、CSS等静态资源也会转发到index.html。6.2 后端打包的运行细节SpringBoot打包用的是Maven插件mvn clean package -DskipTests生成的jar包在target目录下运行指令java -jar car-trade-system.jar --server.port8080注意整个项目如果只有SpringBoot自己跑跨域问题基本不存在因为前端页面和后端接口是同一台服务器同端口。只有开发调试时才需要后端开启CORSConfiguration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOrigins(http://localhost:8080) .allowedMethods(GET, POST, PUT, DELETE); } }这个配置只需要在开发环境开启部署集成后建议把它注释掉减少不必要的请求放行。7. 真实验证环节完整跑通一个交易闭环要多久我拿到完整源码后花了一整个晚上把系统在本地跑通。这期间遇到不少坑也排查了不少问题挑几个有代表性的记录一下大家复现时能少走弯路。7.1 第一个坑MySQL时区问题导致日期错乱后端启动时报错The server time zone value ... is unrecognizedSpringBoot默认使用的连接字符串里没有时区参数。我的解决方法是修改application.yml中的JDBC连接串配置url: jdbc:mysql://localhost:3306/car_trade?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiserverTimezoneAsia/Shanghai这个参数必须显式指定否则不仅是启动报错就算能连上查出来的时间也可能和北京时间差8小时。7.2 第二个坑MyBatis的Mapper接口和XML路径不对项目跑起来后只要一调用车辆查询接口就报Invalid bound statement (not found)。排查路径是先确认application.yml里mapper-locations配置的是否指到了所有的XML文件再确认XML文件的namespace是否和Mapper接口的全限定名完全一致最后确认Mapper接口的方法名和XML里id是否一致。这三层有一层不匹配都会触发这个报错。7.3 第三个坑图片上传成功但前端访问404图片上传后浏览器直接访问URL报404。这个问题的关键定位是SpringBoot静态资源映射路径没生效。新版本SpringBoot对静态资源的拦截规则有调整如果和RequestMapping冲突需要像前面那样单独实现WebMvcConfigurer配置映射。另外路径里的文件分隔符也要注意Windows和Linux的写法不同配置跨平台部署时要留意。7.4 交易闭环的实测流程与数据结果系统跑通后我用测试数据走了一遍完整流程注册一个普通账号登录后发布一辆2020款宝马3系2.0T行驶3.2万公里售价22.5万提交后状态变成待审核。切换管理员账号进入后台看到该车相关信息点击审核通过。回到普通用户视角在首页按品牌宝马和价格区间20-25万筛选能搜到刚才那辆车。点击进入详情页提交预约看车申请填写期望时间。之后管理员在后台能看到这条预约记录标记为已确认。管理员将该车辆状态更新为已售出系统生成一条交易记录。全程走下来分页正常、筛选正常、权限拦截正常状态流转没有跳级或重复提交的问题。这就是一个核心闭环也是判断这套系统是否完整的最直接标准。8. 个人总结与后续可扩展的方向把整个项目做完再回头看我个人最大的体会是这种管理系统类项目的难度不在某一个技术点而在把一条业务链路用代码完整表达出来。发布、审核、筛选、预约、交易、归档每一步都有对应的表结构、接口逻辑和界面交互任何一个环节偷懒整个系统就会显得缺一块。这个项目后续还可以往三个方向扩展一是引入图片延迟加载和压缩图降低列表页流量消耗二是增加车辆浏览记录的推荐逻辑让买家更容易发现自己关注的车源三是把交易数据做成可视化报表让管理员能看到近半年的成交趋势。这三个方向都不需要动现有架构属于增量迭代。如果大家要复现这套项目我的建议是自己先把car表的状态机画清楚再动手写代码。状态机理清了后端接口怎么写、前端按钮怎么置灰、管理员操作权限怎么控制全都顺理成章。最后留个小技巧开发时把所有SQL打印开关打开MyBatis配置里加一句log-impl: org.apache.ibatis.logging.stdout.StdOutImpl调试动态SQL拼接是否正确会直观很多。
返回列表