做了好几个月的汽车销售管理类项目,这次这套基于SpringBoot+Vue+MyBatis+MySQL的靓车汽车销售网站系统算是我觉得最能直接拿来复用的产物。整个系统覆盖了常见的商用场景:用户端看车、搜车、看视频、预约试驾、在线询价,管理端负责车辆上下架、分类管理、预约处理、资讯发布和轮播图维护,一套前后端分离的完整闭环。不管你是准备做毕业设计、接私活,还是想拿一个全栈项目练手、搞懂前后端分离的协作模式,这套东西都比较值得参考。文章后面我会把技术选型逻辑、数据库设计、核心代码结构、联调细节和完整部署流程全部捋一遍,也会把我在实际搭建过程中踩过的坑一并交代清楚。
1. 这个汽车网站项目到底解决了什么问题
1.1 一类非常标准的"商业展示+业务闭环"系统
汽车销售网站不是简单的企业官网,它必须同时具备对外展示和对内管理的双向能力。用户端要能在首页看到品牌车型轮播、热门车型推荐,能够按照品牌、价格区间、车型关键词多条件筛选车辆,进入详情页之后查看大图、视频介绍、车辆参数,甚至可以直接在线提交试驾预约或询价申请。管理员端则需要一套清晰的后台,维护分类、维护车辆信息、处理用户提交的试驾预约和询价单、发布促销资讯、管理首页轮播图。
这个系统我把数据模型划分成七张核心业务表,内部跑通的是"用户注册登录 → 浏览车辆 → 提交试驾/询价 → 管理员后台审核处理"的完整路径。和市面上很多只有增删改查的练习项目不同,这套系统的业务是有方向的,用户操作产生的数据会流转到管理后台,后台的状态变更又会反馈给用户。这种有来有回的闭环设计,才是真实商业项目的基本形态。
1.2 为什么拿它做前后端分离实战项目最合适
我见过太多人学框架的时候只看单页Demo,结果真到做项目的时候完全不知道怎么把一个系统拆开。选汽车销售网站作为前后端分离实战项目,是因为它的业务复杂度刚刚好。
- 业务足够典型:用户端+管理端双视图,涉及权限区分,能覆盖Vue Router路由守卫的前后端联动。
- 数据结构适中:七张表的关联关系刚好能练到MyBatis的动态SQL、多表联查、分页查询。
- 交互有层次:图片上传、富文本资讯、视频播放、状态流转,每一样都是真实业务里躲不开的需求。
- 部署路径清晰:前端打包成静态资源、后端打成Jar包,用Nginx做反向代理,是当前中小企业最常用的交付形态。
换句话说,做完这一个项目,你对"一个正经的商业Web应用是怎么从零搭起来"这件事会有完整的体感,而不是停留在会调接口的层面。
2. 技术栈选型:这套组合背后的取舍逻辑
2.1 SpringBoot为什么是后端的稳妥之选
后端框架我直接在SpringBoot里选型,原因很直接:它把Spring生态里最繁琐的Bean装配、事务管理、Web配置全部做成了自动配置,开发者只需要关注业务代码和少量自定义配置。
我用的稳定组合是Spring Boot 2.7.x + JDK 1.8,这两个版本搭配经历了大量生产环境验证,兼容性最保险。这里特别想提醒一句:现在很多新手直接去官网下载最新的Spring Boot 3.x,然后发现JDK版本要求17、MyBatis相关依赖也换了命名空间(javax.servlet变成了jakarta.servlet),一上来就卡住。如果不是有特殊需求,做这类传统管理系统老老实实选2.7.x,能省掉一大半环境问题。
后端的项目结构我分包很清晰:
- controller:接收请求、参数校验、返回统一结果
- service:业务逻辑层,处理事务边界
- mapper:MyBatis接口层,SQL绑定
- entity:数据库表对应的实体类
- dto:前端请求参数对象和响应对象
- config:跨域、拦截器、WebMVC配置
- common:统一返回结构、异常处理、工具类
2.2 Vue 3 + Vite 的前端体验
前端我选择的是Vue 3 + Vite + Vue Router + Pinia + Element Plus的组合。Vite相比Webpack在开发体验上的提升是质的飞跃,冷启动几乎秒开,HMR响应速度非常快,做前端调试的心情会好很多。
在Vue 3里我全面使用<script setup>语法,代码量比Options API减少一大截,配合Element Plus的el-table、el-form、el-dialog、el-tabs这些现成组件,后台管理界面的搭建效率非常高。再加上Pinia做全局状态管理,用户登录信息、菜单权限这些全局数据都放在store里,组件之间传参的压力小很多。
2.3 MyBatis与MyBatis-Plus该选谁
这里有个不少初学者会纠结的问题:MyBatis和MyBatis-Plus到底用哪个?我的建议是这套项目直接用MyBatis,但把单表通用操作的部分用MBG(MyBatis Generator)生成一遍。原因有两个:
第一,自己手写动态SQL是对SQL能力的基本功训练。汽车列表页的品牌+价格区间+关键词多条件筛选,如果直接上MyBatis-Plus的LambdaQueryWrapper,确实快,但你对SQL的掌控感会缺失。第二,MyBatis-Plus的启动依赖和Spring Boot版本的适配偶尔会出现小坑,纯MyBatis的兼容性曲线更平滑。实践中我把XML映射文件放在resources/mapper目录下,通过mybatis.mapper-locations配置扫描,SQL和Java代码完全解耦,维护起来很清晰。
2.4 版本匹配:最容易在起步阶段卡死的环节
版本匹配这个问题我踩过很多次,值得单独列一张对照表给大家参考:
| 组件 | 推荐版本 | 备注 |
|---|---|---|
| JDK | 1.8 | 稳定可靠,兼容绝大多数开源库 |
| Spring Boot | 2.7.x | 使用javax命名空间,避免3.x迁移问题 |
| MyBatis Spring Boot Starter | 2.3.x | 对Spring Boot 2.7兼容良好 |
| MySQL | 5.7或8.0 | 5.7稳妥,8.0需注意驱动和时区 |
| Node.js | 16.x或18.x | 对应Vite 4/5的要求 |
| Vue / Vite | 3.4.x / 5.x | 组合稳定 |
| Element Plus | 2.6.x | 与Vue 3匹配 |
填这些版本号不是我凭记忆随便给的,都是在实际安装部署中出现过兼容性问题之后验证出来的组合。比如Spring Boot 2.7和MyBatis Starter 2.3.x配合,事务注解和Mapper扫描一切正常;换成Spring Boot 3.2之后,同样的代码启动直接报ClassNotFoundException: javax.servlet.Filter。这类问题排查起来非常消耗时间,一开始就把版本锁死,能少掉很多头发。
3. 数据库建模与MyBatis落地细节
3.1 七张核心业务表的职责与关系
数据库设计部分,我按业务域拆成了七个核心表,下面这张表把每张表的职责列清楚了:
| 表名 | 核心字段 | 主要职责 |
|---|---|---|
| user | id, username, password, real_name, phone, role, avatar | 存储系统用户,区分普通用户和管理员 |
| car_brand | id, brand_name, logo_url, sort_order | 车辆品牌分类 |
| car | id, brand_id, car_name, price, cover_image, video_url, sale_status, car_desc, sale_count | 车辆主表,关联品牌,记录售价、封面、视频 |
| test_drive | id, user_id, car_id, phone, drive_date, status, remark | 试驾预约申请 |
| inquiry | id, user_id, car_id, phone, content, status | 在线询价申请 |
| news | id, title, cover_image, content, view_count, create_time | 促销资讯与新闻 |
| banner | id, image_url, link_url, sort_order, status | 首页轮播图 |
表结构设计上有几个关键点:
- 不用外键约束,直接用逻辑关联。汽车表里的brand_id对应品牌表的id,在查询的时候通过JOIN语句关联,而不是靠数据库外键强约束。这样删除和更新数据的灵活性更高,也是主流互联网项目的做法。
- 价格字段用decimal(10,2),不要用float/double。浮点数在MySQL里做比较运算时会出精度问题,车价这种金额数据必须用定点数。
- 所有表都保留create_time和update_time两个字段,业务排查时极有用。后端用MyBatis插入数据时直接往这两个字段写当前时间即可。
3.2 分页与多条件查询的SQL设计
汽车列表页是整个系统最核心的查询场景,用户可能按品牌、价格区间、关键字、排序方式任意组合筛选。MyBatis动态SQL在这里发挥了不可替代的作用。
我的做法是参数统一封装成一个CarQueryDTO对象,包含brandId、minPrice、maxPrice、keyword、saleStatus、pageNum、pageSize等字段,然后XML映射文件里用<where>标签动态拼接查询条件。核心SQL类似这样:
SELECT c.*, b.brand_name FROM car c LEFT JOIN car_brand b ON c.brand_id = b.id <where> <if test="brandId != null and brandId != 0"> AND c.brand_id = #{brandId} </if> <if test="minPrice != null"> AND c.price >= #{minPrice} </if> <if test="maxPrice != null"> AND c.price <= #{maxPrice} </if> <if test="keyword != null and keyword != ''"> AND (c.car_name LIKE CONCAT('%', #{keyword}, '%') OR b.brand_name LIKE CONCAT('%', #{keyword}, '%')) </if> </where> ORDER BY c.sale_count DESC LIMIT #{offset}, #{pageSize}这里有一个新手特别容易踩的坑:在MyBatis的XML里,"<"是不能直接写的,必须写成<转义字符,否则XML解析阶段就会报错。我见过不少人在这个细节上卡了半小时排查。另外分页我直接用的MySQL的LIMIT语法,手动计算offset:(pageNum - 1) * pageSize,不需要额外引入PageHelper插件,减少一层中间件的兼容风险。
3.3 MyBatis缓存配置、日志打印与常见坑
MyBatis的缓存机制是个老生常谈的话题。一级缓存是SqlSession级别的,默认开启,同一个SqlSession内重复查询同一个Mapper方法且参数一致,第二次会直接命中缓存,不再查库。Spring集成MyBatis后,每次Mapper调用默认会新开SqlSession(并且用完关闭),所以一级缓存的实际作用范围比想象中小很多,不用过度依赖。
二级缓存是Mapper级别的,需要在XML里显式配置<cache/>标签。这个功能我建议在项目里谨慎使用。汽车销售网站的数据实时性要求没有高并发场景,开启二级缓存反而可能带来脏读问题——比如修改车辆信息后,如果缓存刷新策略配置不对,用户端还是能看到旧数据。实际上我在这个项目里只配置了一级缓存和日志打印,不开启二级缓存,保证数据实时性,这个决定在后来的测试中也避免了无数起"数据改了半天页面不变"的诡异情况。
日志打印是排查SQL问题的必要工具,我在application.yml里加了两行配置:
mybatis: mapper-locations: classpath:mapper/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这样每次执行SQL,控制台会完整打印预编译SQL和参数列表。上线的生产环境建议把log-impl改成Slf4jImpl并通过日志级别控制,否则所有SQL都会明文刷到日志文件里,有条件泄露风险。
4. 后端核心实现:鉴权、接口规范与视频支持
4.1 统一返回结构与全局异常处理
后端接口的返回格式如果不统一,前端每个人的判断逻辑都不一样,联调的时候会非常痛苦。我在项目里统一封装了一个Result<T>结构:
public class Result<T> implements Serializable { private Integer code; // 200成功,401未登录,500系统异常 private String message; // 提示信息 private T data; // 返回数据 public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("操作成功"); result.setData(data); return result; } public static <T> Result<T> error(Integer code, String message) { Result<T> result = new Result<>(); result.setCode(code); result.setMessage(message); result.setData(null); return result; } }配合统一返回结构,我写了一个@RestControllerAdvice全局异常处理器,专门拦截业务异常和未捕获的Exception。这样代码里想抛错就抛错,不用每个方法都包一层try-catch,前端拿到的永远是一个格式稳定的响应体。后端接口的入参校验我放在Controller层用Spring的@Validated注解做,避免脏数据流入Service层。
4.2 JWT登录鉴权与拦截器
前后端分离项目没有Session概念,登录状态用JWT来维护是最通用方案。用户登录成功之后,后端用密钥签发一个token,里面带上用户id、用户名、角色信息,设置过期时间(我这边设置为24小时)。前端把token存到localStorage,之后每次请求都在请求头里带上Authorization: Bearer <token>。
后端用一个HandlerInterceptor实现token校验:
@Component public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行OPTIONS预检请求 if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { return true; } String token = request.getHeader("Authorization"); if (token == null || !token.startsWith("Bearer ")) { throw new BusinessException(401, "未登录或登录已过期"); } // 解析token,校验签名和过期时间 Claims claims = JwtUtil.parseToken(token.replace("Bearer ", "")); // 将用户信息放入request attribute,方便后续获取 request.setAttribute("userId", claims.get("userId")); request.setAttribute("role", claims.get("role")); return true; } }在WebMvcConfig里注册拦截器,并配置放行的白名单路径,比如/api/auth/login、/api/car/list、/api/banner/list这些不需要登录就能访问的公开接口。这种做法比把token校验分散在Service层要干净得多,而且可以针对/api/admin/**路径额外校验管理员角色。
4.3 车辆接口与预约试驾的并发考虑
车辆列表接口我设计成了GET请求,参数全部通过Query String传递,这样做的好处是接口天然幂等,前端传参简单,也方便搜索引擎收录。详情接口是/api/car/{id},每次访问浏览量+1的实现我放在SQL层:UPDATE car SET view_count = view_count + 1 WHERE id = #{id},原子自增,不需要先查后改。
预约试驾这类写接口要注意防重复提交。我的处理方案是在test_drive表里加了user_id + car_id + drive_date三个字段的唯一约束,后端插入前先做一次检查,如果同一个人在同一天对同一辆车已经提交过预约,直接返回"您已预约过该车辆的试驾,请勿重复提交"的提示。这个设计在线上实测中非常有用,能挡住大量手抖和恶意刷接口的问题。
4.4 视频/图片资源管理与m3u8播放支持
汽车详情页通常会有车辆展示视频,这对销售转化率很重要。这个系统的资源文件我采用本地磁盘存储 + Nginx静态资源映射的方式,不引入FastDFS或OSS,保持了项目的轻量化。
视频播放这里要特别注意,实测中直接放一个mp4文件到服务器上,浏览器播放体验并不好,尤其是预览加载速度慢、拖动进度条需要等缓冲。更稳妥的方案是把视频切成HLS流(m3u8 + ts分片),前端用支持HLS的播放器处理。
至于m3u8切片的生成,可以用FFmpeg命令把上传的mp4统一转成HLS流文件,脚本类似:
ffmpeg -i input.mp4 -codec copy -hls_time 10 -hls_list_size 0 -hls_segment_filename "video_%03d.ts" output.m3u8生成后的m3u8文件和ts分片放在服务器的静态资源目录下。前端拿到视频地址后,用hls.js或者video.js来播放,这个我在下一节详细讲。这里要提醒的是,m3u8涉及的ts分片文件有跨域请求的场景,Nginx的静态资源响应头要记得补上Access-Control-Allow-Origin: *,否则浏览器里会报跨域错误,视频黑屏。
5. Vue前端页面体系与视频播放方案
5.1 前端目录结构与核心依赖
前端项目我基于Vite构建,目录结构按"视图-组件-状态-网络"四个维度划分:
src/ ├── api/ # 接口请求封装,按模块拆分 │ ├── auth.js # 登录/注册相关接口 │ ├── car.js # 车辆模块接口 │ ├── order.js # 试驾/询价接口 │ └── admin.js # 后台管理接口 ├── router/ # 路由配置 ├── stores/ # Pinia状态管理 ├── views/ # 页面组件 │ ├── home/ # 首页 │ ├── car/ # 车辆列表与详情 │ ├── user/ # 个人中心 │ └── admin/ # 管理后台 ├── components/ # 公共组件 ├── utils/ # 工具类 │ └── request.js # axios实例 └── App.vue核心依赖我只保留了必要的几项:axios(网络请求)、vue-router(路由)、pinia(状态管理)、element-plus(UI组件库)、nprogress(顶部进度条)、hls.js(视频播放)。不需要额外的UI主题包,Element Plus默认主题已经足够清爽。
5.2 axios封装、Token注入与401自动处理
前端所有请求我统一走一个封装的axios实例,这个文件是整个前后端联调的咽喉。核心逻辑包括三部分:请求拦截器注入token、响应拦截器统一处理业务码、401时自动跳转登录页。
// utils/request.js import axios from 'axios' import { ElMessage } from 'element-plus' import router from '@/router' const service = axios.create({ baseURL: '/api', timeout: 15000 }) // 请求拦截器:注入token service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config }, error => { return Promise.reject(error) }) // 响应拦截器:统一处理业务码 service.interceptors.response.use(response => { const res = response.data // 二进制数据直接返回(文件流) if (response.request.responseType === 'blob') { return response } if (res.code !== 200) { if (res.code === 401) { localStorage.removeItem('token') localStorage.removeItem('userInfo') router.push('/login') ElMessage.error('登录已过期,请重新登录') } else { ElMessage.error(res.message || '请求失败') } return Promise.reject(new Error(res.message)) } return res }, error => { ElMessage.error(error.message || '网络错误') return Promise.reject(error) })这里有个联调期很容易漏掉的细节:后端接口返回的code字段如果是401,前端不仅要弹错误提示,还必须立刻清除本地缓存的token和用户信息,并且强制跳转登录页。如果漏了这一步,用户会看到一个"未登录"的报错,但点哪个页面都进不去,体验非常差。
5.3 路由守卫与页面权限控制
路由配置我分为三类:公开页面(登录页、车辆列表、车辆详情)、需登录页面(预约试驾、个人中心)、仅管理员页面(后台管理)。通过路由元信息里的requiresAuth和role字段来控制。
前置守卫的核心逻辑:
router.beforeEach((to, from, next) => { if (to.path === '/login') { next() return } const token = localStorage.getItem('token') const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}') if (to.meta.requiresAuth && !token) { ElMessage.warning('请先登录') next('/login') return } if (to.meta.role === 'admin' && userInfo.role !== 'admin') { ElMessage.warning('无权访问该页面') next('/') return } next() })这种"后端拦截器 + 前端路由守卫"的双重防护,才是前后端分离项目正确的鉴权姿势。前端守卫只是改善交互体验,真正的安全防线在后端接口层,这个理念我希望每个做全栈项目的人都记住。
5.4 车载视频m3u8播放落地实践
视频播放模块是这个项目前端的一个亮点,也是热词里被反复提到的点。我用hls.js来实现m3u8视频流的播放,核心逻辑是:先通过video.canPlayType('application/vnd.apple.mpegurl')判断当前浏览器是否原生支持HLS(Safari支持),如果支持直接用video标签的src播放;不支持的话,就用hls.js加载并接管视频元素。
<template> <div class="video-wrapper"> <video ref="videoRef" controls playsinline></video> </div> </template> <script setup> import { ref, onMounted, watch } from 'vue' import Hls from 'hls.js' const props = defineProps({ src: { type: String, required: true } }) const videoRef = ref(null) function initVideo() { const video = videoRef.value if (!video) return // Safari原生支持HLS if (video.canPlayType('application/vnd.apple.mpegurl')) { video.src = props.src return } // 其他浏览器用hls.js if (Hls.isSupported()) { const hls = new Hls({ maxBufferLength: 30, maxMaxBufferLength: 60 }) hls.loadSource(props.src) hls.attachMedia(video) hls.on(Hls.Events.MANIFEST_PARSED, () => { video.play() }) } } onMounted(() => { initVideo() }) watch(() => props.src, () => { initVideo() }) </script>实测下来,10秒一分片的m3u8流在4G网络下首屏加载速度比直接放mp4有明显提升,拖动进度条也不需要像之前那样等整段缓冲完成。这个方案在PC端Chrome、Edge以及移动端微信内置浏览器里都做过兼容测试,表现稳定。唯一要注意的是跨域问题,ts分片的请求必须是可跨域的,所以Nginx那边一定要配好CORS响应头。
6. 前后端联调:跨域、Token与常见拦路虎
6.1 CORS配置的正确姿势
前后端分离开发阶段最烦人的问题就是跨域。我推荐的解决方式是后端开启CORS全局配置,前端开发环境用Vite的proxy代理,生产环境用Nginx反向代理。三层配置各有各的场景,不是只配一种就万事大吉。
后端CORS配置我用一个WebMvcConfigurer实现:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }这里有个细节,allowCredentials(true)开启后,allowedOrigins不能直接用*,必须用allowedOriginPatterns("*"),否则浏览器会拒绝携带Cookie跨域。虽然我们用的是token认证而不是Cookie,但这个配置的严谨性还是要保证。
6.2 联调阶段最容易出问题的三个细节
第一处是时间字段的格式统一。Java后端的LocalDateTime序列化后默认格式是2025-01-15T10:30:00,前端Element Plus的日期时间选择器期望的格式是2025-01-15 10:30:00,两者不匹配,列表页和表单回显就会出现"时间显示后跟一个T"这种丑陋的结果。我在application.yml里做了全局配置,把jackson的日期格式统一成yyyy-MM-dd HH:mm:ss。
第二处是空值处理。MyBatis查询结果里如果车价字段是null,JSON序列化后前端拿到的是null,但Element Plus表格不会自动把null显示成空字符串,界面上会出现一大片"null"字样。解决方法是前端封装一个格式化函数处理所有可能为null的字段,或者后端在DTO里给默认值。我更推荐前端处理,因为后端返回null是合理的,前端展示层应该自己兜底。
第三处是文件上传的Content-Type。用Element Plus的el-upload组件上传图片,默认用的是multipart/form-data格式,后端接收时参数名必须和前端FormData的key一致。如果不一致,后端会一直报参数缺失,但前端看到的只是500错误。调试方法很简单,F12打开Network面板看请求Payload里的字段名,和后端@RequestParam("xxx")对上即可。
6.3 从Postman到浏览器:联调工具链建议
我的联调流程分三步走。第一步后端接口写完先自己用Postman跑通,包括正常的业务路径和异常路径(比如传非法参数、不传token),确保接口本身没问题。第二步前端对接的时候用浏览器开发者工具看Network,重点检查请求URL、请求头、响应体是否符合预期。第三步遇到跨域或代理问题时看Vite的proxy配置和后端CORS配置是否正确。
这里分享一个经验:当前后端联调出问题的时候,先别急着怀疑后端逻辑。先看请求到底有没有发出去、发出去的请求头里有没有token、请求URL是不是正确。很多时候问题出在前端拦截器配置错误,请求根本没发到后端,后端再对也白搭。
7. 完整部署流程与高频坑点排查
7.1 本地环境准备清单
部署之前先检查环境,我把这套系统要求的清单列出来:
| 软件 | 版本要求 | 部署用途 |
|---|---|---|
| JDK | 1.8+ | 运行后端Jar包 |
| Maven | 3.6+ | 后端构建打包 |
| Node.js | 16.x或18.x | 前端构建 |
| MySQL | 5.7或8.0 | 数据库 |
| Nginx | 1.20+ | 前端静态资源与反向代理 |
MySQL的安装我多说两句。Windows环境下推荐用免安装版,解压后做几步:把bin目录加入环境变量、在根目录建my.ini配置文件、以管理员身份执行mysqld --initialize-insecure初始化(这个命令会生成root空密码)、执行mysqld --install安装服务、net start mysql启动服务。MySQL 8.0的用户要特别注意时区问题,JDBC连接串里必须加上serverTimezone=Asia/Shanghai,否则会直接报The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized的错误。
7.2 后端打包与启动
后端打包前先检查application.yml里的数据库连接串和端口配置,确认没有问题后执行打包命令:
mvn clean package -DskipTests打包成功后在target目录下会生成一个xxx.jar文件,体积一般在50MB左右。启动命令是:
java -jar car-sales-system.jar --server.port=8080如果要后台运行,Linux下用nohup:
nohup java -jar car-sales-system.jar --spring.profiles.active=prod > app.log 2>&1 &启动之后看日志有没有Started Application in x.xx seconds字样,同时确认8080端口在监听:netstat -tlnp | grep 8080。如果启动失败,90%的情况出在MySQL连接权限问题或者端口被占用,把日志往上翻几行就能看到具体原因。
7.3 前端构建与Nginx部署
前端构建命令很简单:
npm install npm run build构建产物在dist目录。把dist目录整体上传到服务器Nginx的html目录下(比如/usr/share/nginx/html/car),然后配置Nginx。配置文件核心片段如下:
server { listen 80; server_name your-domain.com; # 前端静态资源 root /usr/share/nginx/html/car; index index.html; # 解决history路由模式刷新404问题 location / { try_files $uri $uri/ /index.html; } # 后端API反向代理,解决跨域 location /api/ { proxy_pass http://127.0.0.1:8080/api/; 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 /static/ { expires 30d; } }这里最关键的一项是try_files $uri $uri/ /index.html;。如果漏配这一行,Vue Router使用history模式时刷新非首页路由(比如/car/detail/4)会直接报404,这个坑几乎每个用history模式的人都踩过。
7.4 部署阶段高频问题:版本过高、打包布局异常、数据库连接
我在多次部署实测中总结出三个高频问题,这里把排查链路写清楚。
第一个是Spring Boot版本过高导致的各种兼容性问题。表象是项目启动时报Failed to configure a DataSource或者Mapper接口注入失败,根因往往是Spring Boot 3.x下某些自动配置类行为变了。排查思路:第一步看pom.xml里的spring-boot-starter-parent版本,如果是3.x开头,先降级到2.7.x;第二步检查MyBatis Starter的group是否从org.mybatis.spring.boot换成了别的;第三步看有没有使用javax包下的类。大多数版本问题走完这三步都能解决。
第二个是前端打包后布局异常。典型场景是开发环境一切正常,npm run build部署到生产环境后样式乱了或者图片404。排查链路:第一步看浏览器Console的静态资源请求路径,如果是绝对路径/static/js/xxx.js报404,说明静态资源没放在正确位置;第二步检查vite.config.js里的base配置,如果部署在子目录下,base要设置为/car/;第三步看图片资源是相对路径还是绝对路径引用的。Element Plus的字体会被打包成字体文件,如果Nginx没有给woff/woff2类型的文件配MIME类型,字体会请求失败,界面上的图标全部显示成方块,这种问题去Nginx的mime.types配置里补上font/woff2 woff2;即可。
第三个是数据库连接问题。应用启动成功但一旦访问接口就报Communications link failure,或者过一段时间就报连接超时。排查思路:第一步telnet 127.0.0.1 3306确认MySQL端口是否可连通;第二步检查MySQL的max_connections是否被耗尽,通过SHOW PROCESSLIST查看活动连接;第三步在JDBC连接串里加上autoReconnect=true和connectionTimeout参数,避免MySQL主动断开长时间空闲的连接。实测下来这三步可以解决绝大部分"后端启动正常、接口不稳定"的诡异问题。
最后再分享三个小技巧
系统做完之后我自己回看,觉得有三件事如果一开始就做,能省掉大量返工时间。
第一个是编写接口文档。不需要用Swagger那套重家伙,直接在项目里维护一个Markdown版的接口清单,包含路径、请求方法、参数说明、返回示例,前后端照着同一份文档开发,联调效率能翻一倍。如果你是单人在写这个项目,这个习惯也能让你在隔了几天回头改代码时快速回忆起来接口约定。
第二个是做一个简单的数据初始化脚本。系统第一次部署时,管理员账号、车辆品牌基础数据、几条示例车辆信息必须手动录入很麻烦。我写了一个data.sql,在Spring Boot的启动配置里设置spring.sql.init.mode=always,首次启动自动执行初始化脚本,以后每次Demo演示或交付客户都只要拷一个dist目录和一个Jar包就能跑起来。
第三个是前端做了一个"开发环境自动登录"的开关。在store里写个判断,如果当前环境是development且本地存储里有devToken,request拦截器自动注入一个虚拟的管理员token,后端也做一个test环境的filter放行。这样每次改了后端代码重启之后,不需要重新走一遍登录流程就能直接调试后台页面,这个体验上的小优化对于日活几百次的开发者自己来说,幸福感提升非常明显。
这套系统从数据库设计、后端接口、前端页面到生产部署,是我完整迭代过一遍之后觉得各方面都比较均衡的方案。技术栈不偏不怪,业务逻辑有真实参考价值,部署链路也完全覆盖了现在中小企业最常见的交付模式。你看完如果准备照着自己的需求改造,优先从车辆模块和预约模块下手,把那两个部分的数据字段改成你实际业务里的字段,整个系统的骨架就能直接复用了。