从零搭建一套汽车资讯系统前后端分离到底该怎么落地我用 SpringBoot Vue3 MyBatis MySQL 把这件事完整做了一遍踩了不少坑也总结出一套可以直接照着复用的开发路线。这篇文章就把整个项目的拆解全过程写出来从技术选型、数据库设计、后端接口到前端页面、联调部署全部按实际开发顺序展开适合正在学习前后端分离项目、或者想快速上手一套完整源码的开发者参考。1. 项目整体规划与技术选型1.1 为什么选 SpringBoot Vue3 MyBatis先回答一个很多人会问的问题汽车资讯网站这种项目技术栈那么多为什么偏偏选这套组合我的判断标准只有一个——能否用最小代价把“完整链路”跑通。SpringBoot 解决了传统 SSM 项目里大量 XML 配置的繁琐问题内嵌 Tomcat打一个 jar 包就能跑起来Vue3 配合 Composition API 写前端逻辑比 Vue2 的 Options API 更贴近工程化开发习惯生态也成熟MyBatis 上手门槛低灵活控制 SQL尤其适合资讯站这种查询条件动态变化的场景。如果换 JPA实体关系映射虽然省事但复杂动态查询写起来反而不直观。选 MyBatis 的理由很直接汽车资讯站有大量列表筛选、条件搜索、多表关联MyBatis 的动态 SQL 能让我清楚知道每一句 SQL 在干什么。至于 MySQL完全够用且免费部署方便不用额外为数据库付成本。这套组合还有一个隐性好处招聘市场上熟悉这套栈的开发者多后续即使团队换人接手成本也低。做项目不只是自己能用还得考虑长期维护。1.2 前后端分离的架构思路所谓前后端分离核心就是两个独立应用通过 HTTP 接口通信。前端跑在浏览器里负责渲染页面、交互反馈后端跑在服务器上负责提供数据、校验逻辑、控制权限。两者只通过 JSON 格式的接口交换信息。实际开发中我把整个系统分成三个工程目录car-news-backendSpringBoot 后端工程负责接口、业务逻辑、数据持久化car-news-frontendVue3 前端工程负责页面展示与用户交互sql目录数据库初始化脚本包含建库建表语句以及少量测试数据这种分离带来的直接好处是并行开发。前端可以先用 Mock 数据模拟接口返回后端专注接口实现最后再联调。调试效率高维护边界也清楚。部署时前端打包成静态文件交给 Nginx 托管后端打成 jar 包独立运行互不干扰。1.3 功能模块划分汽车资讯网站不是一个简单的“文章列表页”按用户视角和管理员视角我把功能拆成两大块。用户端核心功能包括首页轮播图推荐、最新资讯、热门车型速览资讯模块按分类浏览文章支持关键词搜索、分页加载、文章详情车型模块品牌筛选、车型列表、参数对比、图片展示用户系统注册、登录、收藏资讯、收藏车型评论模块对文章发表评论登录用户才能操作管理端核心功能包括仪表盘统计今日资讯发布量、用户总量、评论总量资讯管理文章的增删改查、封面图上传、上下架状态切换车型管理品牌维护、车系维护、车型参数维护评论管理查看评论、删除违规评论用户管理查看注册用户列表、禁用账号功能范围确定之后才能开始设计数据库表结构。这一步想清楚后面开发基本就是按部就班。2. 数据库设计汽车资讯网站的表结构数据库设计是整个项目的地基。很多新手一上来就写代码结果后期频繁改表、接口返工就是因为这个阶段偷了懒。2.1 核心用户与权限设计我设计了用户表sys_user字段包括主键 id、用户名 username、密码 password、昵称 nickname、头像 avatar、手机号 phone、角色 role、状态 status、创建时间 create_time。密码必须是加密存储的我在后端使用 BCrypt 对原始密码做哈希处理数据库中绝不存明文。角色字段用简单字符串区分ROLE_ADMIN和ROLE_USER权限控制时直接校验该字段。这里有一个设计取舍没有单独建一张复杂的权限表用户表、角色表、菜单表、权限表因为资讯网站的管理端功能不算极复杂两张表、一个角色字段就能覆盖需求。过度设计反而增加开发成本。如果以后要接入多角色、细粒度权限再扩展成 RBAC 模型也不迟。2.2 资讯内容与分类表资讯模块有两张表资讯分类表news_category和 资讯文章表news_article。分类表字段简单id、分类名称 category_name、分类标识 category_key、排序 sort_order。文章表字段要多一些id主键category_id关联分类表外键title文章标题summary摘要content详情内容LongText 类型cover_image封面图地址author作者view_count浏览量status状态0 草稿 / 1 已发布 / 2 下架create_time / update_time时间戳内容字段用 LongText 而不是 Text因为汽车评测文章通常篇幅长还会嵌入多张图片地址长度预留充足可以避免后期迁移。一个容易忽略的细节文章表单独存一个 category_id而不是在分类表里存文章列表。查询单篇文章时通过 JOIN 或者两条 SQL 就能拿到分类名称但如果反过来设计查询分类列表就得扫描所有文章性能无法接受。2.3 车型参数表设计车型模块是汽车网站的特殊点和普通新闻网站不同。车型数据具备“品牌-车系-具体车型”的三级结构我建了三张表car_brand品牌、car_series车系/型号、car_model具体配置车型。品牌表id、品牌名称 brand_name、品牌首字母 brand_letter、品牌 LOGO logo_url、排序 sort_order。车系列表id、品牌外键 brand_id、车系名称 series_name、级别 series_level比如紧凑型轿车、中型SUV、厂商指导价 guide_price、图片 image_url。车型参数表id、车系外键 series_id、车型名称 model_name、年款 year、发动机排量 displacement、最大马力 max_power、变速箱类型 transmission、车身结构 body_type、油耗 fuel_consumption、价格 price。这里需要特别注意车型参数尽量用与业务直接相关的字段而不是把这些参数塞进一个大 JSON 字段里。虽然 JSON 灵活但后续做参数对比、范围筛选时SQL 写法会非常痛苦。字段细分之后写“按价格区间筛选”“按排量筛选”就非常简单。2.4 索引设计与 SQL 优化只建表不建索引是常见的坑。我按查询频率给关键字段加了索引news_article.category_id支持按分类筛选普通索引即可news_article.status与分类组成联合索引idx_category_status因为列表页查询通常是“某分类下已发布内容的列表”car_series.brand_id按品牌筛选车型必须建索引car_model.series_id按车系列出具体配置车型必须建索引标题字段 title 没建索引因为搜索用的是 LIKE %关键词%这种写法即使有索引也走不了索引全表扫描在数据量不大时没有明显性能问题等数据量大了再引入全文搜索更合理create_time等排序字段也建了索引避免排序导致临时文件特别是浏览量高的资讯列表。3. 后端 SpringBoot 接口开发实录3.1 项目分层结构与初始化后端工程按标准四层结构组织Controller → Service → Mapper → Entity。entity实体类与数据库表字段一一对应mapperMyBatis 映射接口写 SQL 的地方service业务逻辑层负责事务控制、组合逻辑controller接收 HTTP 请求调用 service返回统一响应项目依赖方面核心依赖只有几个Spring Web、MyBatis Starter、MySQL Driver、Lombok、JWT 库用于登录令牌、Hutool工具包处理字符串和集合很方便。启动类不用特殊配置SpringBoot 自动扫描在同级及以下的所有组件。一个容易忽略的点MapperScan 注解要加在启动类上否则 Mapper 接口无法被 Spring 容器扫描到。3.2 MyBatis Mapper 层写法动态 SQL 是核心资讯列表页的查询条件不固定可能是“按分类查”“按关键词查”“按状态查”的任意组合。这种场景 MyBatis 的动态 SQL 是最好用的方案。以一个典型场景为例管理端资讯列表分页查询支持按标题模糊搜索、按分类筛选、按下架状态筛选。Mapper 接口方法这样写ListNewsArticle selectArticleList(Param(title) String title, Param(categoryId) Integer categoryId, Param(status) Integer status, Param(offset) int offset, Param(limit) int limit);对应 XML 里的动态 SQLselect idselectArticleList resultTypecom.example.entity.NewsArticle SELECT * FROM news_article where if testtitle ! null and title ! AND title LIKE CONCAT(%, #{title}, %) /if if testcategoryId ! null AND category_id #{categoryId} /if if teststatus ! null AND status #{status} /if /where ORDER BY create_time DESC LIMIT #{offset}, #{limit} /select关键点在这里where标签会自动去掉第一个多余 AND 或 OR省去处理 SQL 拼接边界的麻烦。COALESCE函数在空值处理上也很有用比如浏览量为空时按 0 展示。另一个高频操作是批量插入比如常用类比、车型图片的一次性导入。foreach标签配合VALUES可以实现批量插入insert idbatchInsertModel INSERT INTO car_model (series_id, model_name, year, price) VALUES foreach collectionlist itemitem separator, (#{item.seriesId}, #{item.modelName}, #{item.year}, #{item.price}) /foreach /insert批量插入一次 SQL 提交比循环调单条插入快很多尤其导入几十条车型数据时差距非常明显。3.3 Service 层与事务控制Service 层是业务逻辑的核心。比如发布一篇资讯文章除了插入文章表还需要更新分类表里的文章数量统计。这两步必须放在同一个事务里。我们用Transactional注解并指定回滚条件Transactional(rollbackFor Exception.class) public void publishArticle(NewsArticle article) { newsArticleMapper.insert(article); newsCategoryMapper.increaseCount(article.getCategoryId()); }这里的rollbackFor Exception.class是必须强调的如果不指定Spring 的事务默认只在运行时异常RuntimeException时回滚遇到受检异常比如文件写入失败抛出的 IOException不会回滚容易产生“数据一半写入一半没写入”的脏状态。我实际开发中遇到过这类问题上传封面图片成功但文章插入失败结果封面文件留下了一个孤儿文件用户看不到还占磁盘空间。我的处理方案是文章插入失败时在异常捕获中主动删除刚上传的图片文件。3.4 Controller 层与统一返回结构所有接口返回统一的数据结构前端处理起来非常省心。我的标准返回类包含三个字段code业务状态码200 成功400 参数错误401 未登录500 系统异常message提示信息data业务数据返回示例{ code: 200, message: 操作成功, data: { list: [], total: 100 } }Controller 只负责接收参数、调用 Service、包装返回不写业务逻辑。比如登录接口PostMapping(/login) public Result login(RequestBody LoginDTO dto) { UserVO user userService.login(dto.getUsername(), dto.getPassword()); return Result.success(user); }一个小技巧参数对象用 DTO 接收不要直接把实体类拿来接收前端传参。因为前端传的很多字段比如确认密码、验证码在实体类里不存在直接用实体类接收会报参数注入失败。DTO 还能做参数校验比如用NotBlank注解标注必填字段。3.5 登录鉴权与接口安全用户登录后后端签发一个 JWT 令牌前端存在本地存储里后续每次请求在请求头带上Authorization: Bearer token。后端写了一个拦截器统一校验public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { throw new BusinessException(401, 请先登录); } // 解析 token如果失败则抛出异常 LoginUser loginUser JwtUtil.parseToken(token.replace(Bearer , )); request.setAttribute(loginUser, loginUser); return true; } }只对需要登录的路径放行比如收藏、评论、管理端接口公告资讯、车型列表这些公开接口不拦截。这里有一个安全细节管理端接口不能只靠前端隐藏按钮来防止未授权访问后端必须校验当前登录用户的角色。我在一个自定义注解RequireRole(ROLE_ADMIN)上做了拦截或者直接在 Controller 方法里取当前用户角色判断写起来也不复杂但能挡住很多不怀好意的直接调用。4. 前端 Vue3 页面开发与接口对接4.1 初始化项目与路由配置前端用 Vite 创建工程选 Vue3 JavaScript不用 TypeScript 是因为项目不算大型JS 写起来效率更高。安装的模块包括vue-router路由管理pinia状态管理axiosHTTP 请求element-plus后台管理端 UI 组件库sass样式预处理器管理端布局用得多路由配置分两块用户端和管理端。用户端路由挂在主布局下管理端单独一套布局。两个布局不同登录逻辑分开所以路由表做了顶层组件拆分const routes [ { path: /, component: MainLayout, children: [ { path: , component: HomeView }, { path: news/:id, component: NewsDetail }, { path: cars, component: CarList }, ]}, { path: /admin, component: AdminLayout, children: [ { path: dashboard, component: Dashboard }, { path: articles, component: ArticleList }, { path: models, component: ModelList }, ]} ]文件上传的接口路径、图片回显路径我用了一个常量工具统一管理避免多处硬编码导致改路径时全部都要改。4.2 Axios 请求封装与拦截器请求封装是前端的重中之重。所有接口请求都通过封装好的 axios 实例发出统一配置const request axios.create({ baseURL: /api, timeout: 10000 }); request.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers.Authorization Bearer ${token}; } return config; }); request.interceptors.response.use( response { const res response.data; if (res.code 200) { return res.data; } else if (res.code 401) { localStorage.removeItem(token); router.push(/login); } else { ElMessage.error(res.message); return Promise.reject(new Error(res.message)); } }, error { ElMessage.error(网络请求失败); return Promise.reject(error); } );响应拦截器里做了三件关键事统一取出data字段、token 失效时自动跳转登录页、错误信息统一弹提示。这样每个页面调用接口的代码会非常干净const list await getArticleList({ page: 1, pageSize: 10 });不需要每个页面写一遍response.data.data的取值逻辑。拦截器还有一个细节401 跳登录时不能死循环跳转我用一个标志位区分当前是否已在登录页。4.3 Pinia 状态管理用户信息与收藏状态Pinia 主要存全局共享的状态。我建了一个用户 Store保存登录用户的昵称、头像、角色还建了一个收藏 Store保存用户收藏的资讯 ID 集合和车型 ID 集合。收藏 Store 的 getter 可以快速判断某篇文章是否已收藏export const useCollectStore defineStore(collect, { state: () ({ newsIds: [], modelIds: [] }), getters: { isNewsCollected: state id state.newsIds.includes(id) }, actions: { async toggleNewsCollect(newsId) { await collectApi.toggleNews(newsId); if (this.newsIds.includes(newsId)) { this.newsIds this.newsIds.filter(item item ! newsId); } else { this.newsIds.push(newsId); } } } });把收藏状态放到 Pinia 里的好处是多个组件共享收藏状态时无需逐层传递 props。比如详情页收藏了文章列表页的收藏图标下一步能立即更新不需要刷新页面。4.4 资讯列表页与详情页的实现细节资讯列表页的关键在于“筛选条件 分页”。我用 reactive 对象保存查询条件监听条件变化时重新请求数据const queryParams reactive({ categoryId: null, keyword: , page: 1, pageSize: 10 }); const loadData async () { const data await getArticleList(queryParams); articleList.value data.list; total.value data.total; };页面结构上左侧放分类导航主区域是文章卡片列表底部是分页组件。文章卡片用图片 标题 摘要 浏览量来展示封面图使用 el-image 组件带加载失败时的占位图。详情页用路由参数newsId请求文章详情接口。有一个性能优化细节详情页里更新浏览量的操作用无感请求方式前端不用等返回结果点击页面时直接调用一个递增接口不阻塞渲染。内容展示方面详情接口返回的是富文本 HTML 字符串前端直接用v-html渲染。这里有 XSS 风险如果文章内容由用户自行编辑必须做纯净化处理。我的方案是后端在存储前对 HTML 内容做标签白名单过滤去掉script、iframe、onerror等危险内容。如果是纯管理员编辑的内容风险相对较小但也建议加过滤。4.5 车型对比页的交互设计车型对比页是汽车网站比较有特色的功能。用户勾选两到三个车型页面并排展示参数。前端实现上我用 checkbox 选择模型选中的车型数据存到一个数组里。由于车型参数较多我封装了一个参数项映射表展示时把每个车型的数据按映射表顺序渲染成对比表格。这里踩过一个性能坑初始版本直接渲染三个车型的所有参数表格十几列每个单元格是完整的组件导致切换筛选条件时卡顿。后来优化成不在表格里嵌套额外组件纯文本展示图片懒加载切换车型时用数组复用。修复后明显流畅。车型对比的实现另一个关键点是参数对齐。不同车型有不同参数燃油车有排量电动车没有对比表格需要预留“一”占位符避免表格错位。5. 前后端联调与部署上线实战5.1 联调阶段踩过的坑前后端分开开发联调时才暴露各种对接问题。我把遇到过的典型问题列成一张排查表问题现象根因解决方案前端请求 404后端接口路径与前端请求路径不一致统一用接口文档管理使用 Swagger 或 Apifox 导出接口列表跨域请求被拦截前后端端口不同违反浏览器同源策略后端配置 CORS 跨域配置生产环境由 Nginx 代理同源时间字段显示为数组JSON 默认序列化 LocalDateTime 为对象结构配置 Jackson 统一日期格式或前端按格式解析中文乱码前后端编码不一致统一 UTF-8数据库连接串加characterEncodingutf8传参类型不匹配前端数字写成字符串后端用 DTO 类型校验提前拦截无效请求跨域问题联调中最常见。开发环境我的方案是前端 Vite 配置 proxy 代理请求/api时转发到后端http://localhost:8080这样浏览器看到的是同源请求不需要后端处理跨域。生产环境再用 Nginx 做反向代理一举两得。时间序列化问题很多新手会遇到。SpringBoot 默认把LocalDateTime以数组形式输出[2025, 3, 15, 10, 30, 0]前端展示很麻烦。我在配置里统一指定格式字符串spring.jackson.date-formatyyyy-MM-dd HH:mm:ss spring.jackson.time-zoneGMT8这样接口返回的时间字符串就直接是“2025-03-15 10:30:00”格式前端展示无需二次处理。5.2 打包部署流程后端打包用 Mavenmvn clean package -DskipTests打出的 jar 包一般在target/目录下。把 jar 上传到服务器用如下命令启动java -jar car-news-backend-1.0.0.jar --spring.profiles.activeprod我把不同环境的配置拆成了application-dev.yml和application-prod.yml生产环境里配置严格的数据库连接串、日志级别和资源限制。前端构建npm run build构建产物是dist/目录下的静态文件上传到服务器的 Nginx 站点目录。Nginx 配置里最关键的一点是把前端路由的 history 模式 fallback 到 index.htmllocation / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }如果不配置try_files刷新前端页面的子路由 URL 会直接 404比如/news/5会被 Nginx 当成本地文件路径查找。这个坑非常经典我几乎每次部署都能碰到有人问。5.3 数据库迁移与数据初始化数据库初始化脚本我用 Flyway 做版本管理而不是直接在服务器上手动执行建表 SQL。好处是每个环境执行的脚本版本一致不会出现测试库有表、生产库少表的情况。初始脚本分两批写建表脚本V1__create_tables.sql基础数据脚本V2__init_data.sql基础数据包括管理员的初始账号密码加密后的、默认的资讯分类、几个热门汽车品牌等。后续如果修改表结构直接新增 V3、V4 脚本Flyway 会自动记录执行状态。还有一个数据问题值得提测试数据必须与真实业务隔离。我单独写了测试数据脚本只在开发环境执行生产环境不执行。如果不隔离生产库里出现“测试文章”“测试用户”会污染真实数据也影响后期报表统计。6. 常见问题排查速查表与项目复盘6.1 高频报错与解决记录把开发过程中出现频率最高的几个问题记录下来遇到类似情况可以直接参考。问题一启动时报Failed to configure a DataSource这个报错通常是因为 SpringBoot 启动时找不到数据源配置。原因往往不是真的没配置而是配置文件名写错或者application.yml里的缩进不对。排查方法先确认配置文件存在再检查spring.datasource.url、username、password三个配置项是否都在最后看数据库连接串里的数据库名是否存在。问题二MyBatis 接口调用时报Invalid bound statement (not found)Mapper 接口与 XML 文件的 namespace 不匹配或者 XML 文件没放在 Mapper 接口同名同包路径下。配置mybatis.mapper-locationsclasspath:mapper/*.xml后XML 文件放在resources/mapper目录下文件名与接口名一致就能被扫描到。问题三前端请求Network Error跨域配置没生效或者后端服务没启动。先把后端跑起来直接用浏览器访问后端接口确认能返回数据再用前端请求排查是否是跨域或代理问题。问题四JWT 过期后前端一直反复跳登录页没有在拦截器里设置登录页跳转的防循环。改为在跳转前判断当前路由不是/login如果不是才跳转。6.2 开发周期与工作量估算整个项目从零开始到联调上线我实际用了大概 15 个工作日后端 6 天、前端 6 天、联调部署 3 天。这个工期安排也是一个参考不用焦虑“赶进度”合理规划功能优先级更关键。第一优先级是“核心浏览链路”首页 → 资讯列表 → 文章详情 → 车型对比。这些都是用户打开网站最常点的功能先保证它们稳定可用。第二优先级是“用户互动链路”注册登录 → 收藏 → 评论。第三优先级才是管理端的增删改查。按优先级迭代能保证项目每天都在“可用”状态而不是最后一周冲刺。6.3 技术扩展这个项目还能往哪个方向走这个汽车资讯网站的骨架搭好之后扩展空间非常大。如果继续做下去我会按以下几个方向迭代。一是接入搜索引擎。目前搜索只能靠LIKE模糊匹配数据量上了十万级性能就会下降。换 Elasticsearch 或者使用 MySQL 全文索引支持分词搜索、搜索推荐才符合真实资讯站的水准。二是增加缓存层。首页资讯、热门车型这些接口都是高频访问直接用 Redis 缓存 5 分钟能显著减轻数据库压力。再配合 Spring Cache 注解开发成本很低。三是补充数据统计功能。给后台增加用户行为埋点、文章阅读量趋势图、品牌热度排行这部分对内容运营决策非常有价值也是资讯类网站商业化的基础。四是在部署上引入 Docker。把后端 jar、前端静态文件、MySQL、Nginx 都容器化配合编排工具一键启停环境迁移成本能降到零。这套项目用 Docker Compose 只要一份配置文件就能管理全部服务。这些都不是空谈每一条都是实际从业经验里验证过的方向。资讯网站最核心的资产是内容和用户技术选型要服务于这两件事的稳定与迭代速度。项目做到“能上线、能维护、能扩展”这三条标准就已经比很多烂尾项目强很多。我个人体会最深的一点是前后端分离项目的难点不在某个单点技术而在联调思路和工程习惯。接口文档从第一天就开始维护每个字段清晰说明数据库变更第一时间同步到团队环境配置按 dev / prod 分开隔离。这些事不做代码写得再多也会在集成阶段出各种稀奇古怪的问题。把这些基础工作做好项目的交付速度和稳定性都会有质的提升。