资讯动态

前后端分离环保网站实战:SpringBoot+Vue3+MyBatis全栈开发

发布时间:2026/10/21 5:44:27 来源:尧图企业网站定制
1. 项目是什么为什么用前后端分离做环保站先直接说结论这个项目是一套完整的环保资讯类网站系统后端用 SpringBoot MyBatis 操作 MySQL前端用 Vue3 独立开发前后端通过 RESTful 接口通信。它的核心功能包括环境新闻发布、环保项目展示、空气质量数据展示、垃圾分类指南、用户注册登录、后台内容管理等。从源码结构看它不是一个“玩具项目”而是把实际业务网站里最常见的模块都做出来了非常适合拿来学习完整的 Web 全栈开发流程或者作为毕业设计、个人作品集、中小型内容管理系统的底座。我先聊一个很多人问过的问题为什么偏偏选了前后端分离如果你只是做一个简单的展示型网站用 Thymeleaf 服务端渲染确实更省事SpringBoot 一套搞定页面和数据。但一旦业务开始变复杂比如前端需要实时刷新空气质量数据、后台需要独立管理内容、移动端可能还要复用接口服务端渲染就会显得很笨重。前后端分离的核心优势是两个工程解耦前端专注交互和展示后端专注数据和业务规则。这样团队分工更清晰后端接口可以同时被网站、小程序、App 复用而且前端构建产物是纯静态文件部署时可以直接扔到 Nginx 里性能和安全性都更好控制。再说技术选型。SpringBoot 不用多解释Java 生态里做后端服务的标配约定大于配置内置 Tomcat一个 jar 包就能跑。MyBatis 可能有人觉得不如 JPA 方便但实际做这种业务型网站MyBatis 的 SQL 可控性非常关键。环保网站会有很多组合查询场景——比如按发布时间、按污染等级、按垃圾类别筛选这种需求用动态 SQL 写起来干净利落也好优化。Vue3 现在是前端主流组合式 API 比 Vue2 的选项式 API 更简洁配合 Vite 构建开发体验非常顺。MySQL 作为数据存储则是最稳妥的选择生态成熟资料多部署简单。所以这套技术栈并不是碰巧凑在一起而是目前中小型 Web 项目里很经典、很务实的一套组合。下面我会从数据库设计、后端核心实现、前端页面开发、部署联调、问题排查这几个维度把这个项目真正跑通所需要的细节都讲清楚。2. 数据库设计与核心表结构2.1 用户与角色体系任何内容型网站都绕不开用户体系这套环保网站也不例外。数据库里最基本的表就是用户表和角色表。用户表我建议至少包含这些字段CREATE TABLE user ( id BIGINT AUTO_INCREMENT COMMENT 主键ID, username VARCHAR(50) NOT NULL COMMENT 用户名, password VARCHAR(255) NOT NULL COMMENT 加密后的密码, nickname VARCHAR(50) DEFAULT NULL COMMENT 昵称, email VARCHAR(100) DEFAULT NULL COMMENT 邮箱, phone VARCHAR(20) DEFAULT NULL COMMENT 手机号, avatar VARCHAR(255) DEFAULT NULL COMMENT 头像地址, status TINYINT DEFAULT 1 COMMENT 状态 1启用 0禁用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;这里有两个细节值得注意。第一密码字段长度不要设成 32 或 64 那种固定值。因为现在主流做法是用 BCrypt 做哈希默认输出长度是 60 个字符加上未来算法升级的可能直接给 255 最省心。明文存储这种事儿就不多说了正规项目绝对不能这么干。第二用户名必须加唯一索引。实际开发中很多人建表时不加唯一约束靠着代码里先查询再插入来防重复结果并发请求一到照样插进去两条相同用户名的记录。数据库层面的唯一索引是最后一道保障必须有。角色表不需要搞太复杂这套系统用 RBAC 模型就够了。角色表存角色编码和名称比如 admin、editor、user用户角色关联表把用户和角色关联起来。有些简单的网站会直接给用户表加一个 role 字段那样虽然查起来方便但扩展性很差。万一以后要加“环保专家”这种角色或者一个用户既是编辑又是审核员单字段就应付不过来了。2.2 环保内容模块设计环保网站最重要的还是内容。围绕这个主题我拆出了几类核心数据资讯文章、环保项目、空气质量数据、垃圾分类知识。资讯文章表是内容型网站的标准结构字段包括标题、摘要、封面图、正文、分类ID、是否置顶、是否发布、浏览量、发布时间等。这里有一个关键设计点状态字段。很多人喜欢用 is_delete 表示删除用 status 表示启用但文章发布这种场景状态会比想象中复杂。我通常用一组状态组合来表达status TINYINT DEFAULT 0 COMMENT 文章状态 0草稿 1待审核 2已发布 3已下架, is_top TINYINT DEFAULT 0 COMMENT 是否置顶 0否 1是, is_delete TINYINT DEFAULT 0 COMMENT 逻辑删除 0否 1是为什么不直接用“已发布/未发布”两个状态因为真实业务里编辑写稿后需要审核审核通过后才能上线下架也是常见操作。多几个状态代码里用 switch 判断就行但业务扩展时不用改表结构。千万别把删除做成物理删除用户误删了文章就真找不回来了。逻辑删除字段加上查询条件里统一带上 is_delete0成本极低收益极大。环保项目表跟资讯表不一样的地方在于项目通常有地理位置、进展状态、参与人数这类属性。可以设计成项目名称、项目类型、覆盖区域、项目简介、详情内容、开始时间、结束时间、当前状态。空气质量数据这种动态数据就值得单独建表了。因为它的特点是高频写入、频繁查询最近几条。以城市为单位记录监测时间、AQI 指数、PM2.5、PM10、SO2、NO2、O3、CO 等指标。这类表需要注意索引设计监测时间是大头。我给这个表加的是联合索引KEY idx_city_time (city_code, monitor_time)这样按城市查最近数据时走索引非常快。数据量大了以后还可以按月份做分区或者做定时清理保留最近一年的数据就够用。垃圾分类知识表又是一个特殊形态。它不太像文章更像一种词典数据。字段可以精简为垃圾名称、垃圾类别、所属分类描述、投放提示。垃圾类别可以用类型编码比如可回收物、有害垃圾、厨余垃圾、其他垃圾前端展示时把编码映射成文字和图标。这种数据量不会太大几百条撑死了不需要复杂的分页但对查询的便捷性要求高——用户很可能直接在搜索框输入“电池”“塑料瓶”后端就要给出模糊匹配结果。2.3 数据关联与查询设计数据库设计里最忌讳的事情之一就是“面向查询建表”和“面向对象建表”打架。在这个项目里内容分类建议单独建一张分类表然后文章表里存分类ID。这样做有好处分类名称修改时只需要改一张表不需要批量更新文章前端做导航栏时直接查分类表就能拿到所有分类和文章数量。评论模块如果需要做那就要设计评论表和回复关系。一般评论表包含评论人、评论内容、评论对象类型、评论对象ID、父评论ID这几项。一个比较简单实用的做法是评论表不区分一级评论和二级评论统一用 parent_id 为空表示顶级评论不为空表示回复。查询时先查顶级评论再根据顶级评论的 ID 批量查回复避免递归查询数据库导致性能问题。这一步在环保资讯这种内容型网站上体验差异很大用户刷文章看到评论楼里每一层都要等接口返回就会觉得卡。另外很多做这种项目的人容易忽略的一点是表格中标记为“冗余”的字段其实对性能至关重要。比如文章表里加一个 comment_count 字段每次新增评论时对文章表做一次自增操作而不是在查询时 count 评论表这样文章列表页加载的时候就非常快。类似地news 表里也可以冗余加一个 category_name省得每次列表查询都要 JOIN 分类表。这种取舍在数据量大时效果显著。3. 后端实现SpringBoot MyBatis 的关键细节3.1 项目分层与代码组织拿到这套系统源码第一步先看目录结构。一个清晰的后端工程一般会有这几个包com.example.environment ├── controller # 接口层接收请求、返回结果 ├── service # 业务逻辑层处理核心业务 ├── mapper # MyBatis 数据访问层 ├── entity # 数据库实体类 ├── dto # 数据传输对象避免把实体直接暴露给前端 ├── vo # 视图对象按需组装接口返回值 ├── config # 配置类比如跨域、拦截器 ├── common # 通用类统一返回结果、异常处理、工具类 └── utils # 工具类很多初学者喜欢把 entity 直接返回到前端省事是省事但隐患很大。比如用户表里有密码字段如果你把 User 实体直接序列化返回密码就泄露了。更合理的做法是单独定义 UserVO只包含用户名、昵称、头像这些安全字段。同样接收前端参数时用 DTO不要直接用实体否则前端传过来一个 status1 就可能把数据状态改掉了。这种安全习惯越早养成越好。3.2 接口设计与统一返回结构RESTful 接口看起来简单但实际做起来容易五花八门。这套系统里我建议统一返回一个 JSON 结构{ code: 200, message: success, data: { } }对应的 Java 类大概长这样public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(success); result.setData(data); return result; } public static T ResultT error(Integer code, String message) { ResultT result new Result(); result.setCode(code); result.setMessage(message); return result; } }别小看这个统一结构。它解决了几个实际问题前端 axios 拦截器可以统一判断 code如果不为 200 就全局弹错误提示后端异常可以通过全局异常处理器转换成统一的错误结构而不是让 SpringBoot 默认返回一堆难以理解的堆栈信息接口文档里的返回示例也变得清爽。Controller 层的写法也有一些规范。比如路径定义我习惯用复数名词GET /api/article/list 文章列表 GET /api/article/{id} 文章详情 POST /api/article 新增文章 PUT /api/article/{id} 更新文章 DELETE /api/article/{id} 删除文章这种写法虽然简单但遵循了 HTTP 动词的语义前端对接时一目了然。3.3 登录鉴权与 JWT用户登录是前端分离项目里最容易踩坑的一环。传统 Session 方案在前后端分离下不太好用因为前端域名和后端域名不一致Cookie 跨域问题很麻烦。所以我推荐用 JWT 做登录态管理这也是这套系统源码里通常采用的方式。JWT 的思路是用户登录成功后后端生成一个 token 返回给前端前端存到 localStorage 里每次请求时在请求头带上Authorization: Bearer token。后端通过拦截器解析 token判断用户身份和权限。生成 token 的代码大致是这样String token Jwts.builder() .setSubject(userId.toString()) .claim(role, user.getRole()) .setExpiration(new Date(System.currentTimeMillis() 1000 * 60 * 60 * 24)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact();拦截器里解析 token 时我建议把解析出的用户信息放到 ThreadLocal 或 RequestContext 里这样 Controller 层不用在参数里显式传递 userId直接用工具类就能拿到当前登录用户。实际使用中有一个很烦的问题用户 token 过期后前端请求返回 401前端需要统一处理跳转登录页。这个逻辑写在 axios 响应拦截器里instance.interceptors.response.use( (response) { if (response.data.code 401) { localStorage.removeItem(token) router.push(/login) } return response.data }, (error) { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } return Promise.reject(error) } )3.4 MyBatis 动态 SQL 与多条件查询MyBatis 最大的价值就是动态 SQL。环保网站的资讯列表页通常有多个筛选条件按分类、按关键词、按发布时间范围。如果用 JPA 硬拼查询逻辑会很痛苦但 MyBatis 写起来非常顺手select idselectArticleList resultTypecom.example.environment.vo.ArticleVO SELECT id, title, summary, cover_image, category_name, view_count, create_time FROM article where if testcategoryId ! null AND category_id #{categoryId} /if if testkeyword ! null and keyword ! AND (title LIKE CONCAT(%, #{keyword}, %) OR summary LIKE CONCAT(%, #{keyword}, %)) /if if teststartTime ! null AND create_time gt; #{startTime} /if if testendTime ! null AND create_time lt; #{endTime} /if AND is_delete 0 /where ORDER BY is_top DESC, create_time DESC /select注意这里的where标签它会在至少一个条件成立时自动加上 WHERE 关键字并且自动去掉多余的 AND。很多人不知道这一点手动写 WHERE 11虽然能跑但 SQL 很不优雅而且可能错失数据库索引优化机会。分页方面我推荐使用 PageHelper 插件因为它用起来太省心了PageHelper.startPage(pageNum, pageSize); ListArticleVO list articleMapper.selectArticleList(query); PageInfoArticleVO pageInfo new PageInfo(list);只需要在查询前调一行 PageHelper.startPage它就会自动拦截下一条 SQL 并生成 count 查询和分页 SQL。PageInfo 里已经包含了总条数、总页数、当前页、每页大小等字段前端分页组件直接使用就行。使用 PageHelper 有一个坑需要注意startPage 之后必须紧跟一条 Mapper 查询中间不能穿插其他数据库操作或逻辑否则分页效果会出错。有些经验不足的人喜欢在 service 层先调 PageHelper.startPage 再去查别的表结果分页参数被套到错误的 SQL 上排查半天都找不到原因。4. 前端实现Vue3 里的组件与状态管理4.1 页面规划与路由结构前端工程如果用的是 Vite 创建那目录结构很清爽。以这套环保网站为例页面规划大致是src ├── api # 接口调用封装 ├── assets # 静态资源 ├── components # 公共组件 ├── layout # 布局组件 ├── router # 路由配置 ├── store # Pinia 状态管理 ├── views # 页面组件 │ ├── home # 首页 │ ├── news # 环保资讯 │ ├── project # 环保项目 │ ├── air # 空气质量 │ ├── garbage # 垃圾分类 │ ├── user # 用户中心 │ └── admin # 后台管理 └── utils # 工具函数路由配置我是这么设计的用了 children 做嵌套路由const routes [ { path: /, component: Layout, redirect: /home, children: [ { path: home, component: Home, meta: { title: 首页 } }, { path: news, component: NewsList, meta: { title: 环保资讯 } }, { path: news/:id, component: NewsDetail, meta: { title: 资讯详情 } }, { path: air, component: AirQuality, meta: { title: 空气质量 } }, ]}, { path: /login, component: Login }, { path: /admin, component: AdminLayout, children: [...] } ]前端的路由懒加载是提升首屏速度的重要手段。用() import(/views/NewsList.vue)这种方式Webpack 或 Vite 会把每个路由对应的组件打成单独的 JS chunk用户访问首页时不会加载后台管理的代码体验差别很明显。4.2 Axios 封装与接口对接前后端分离项目里前端和后端唯一沟通通道就是 HTTP 接口。如果把 axios 调用直接写在业务组件里几十个页面就要写几十套重复代码。标准化做法是封装一个统一的 request 实例import axios from axios const instance axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || /api, timeout: 10000 }) instance.interceptors.request.use((config) { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) export default instance然后每个模块的接口单独建文件管理比如 article.jsimport request from /utils/request export function getArticleList(params) { return request.get(/article/list, { params }) } export function getArticleDetail(id) { return request.get(/article/${id}) } export function createArticle(data) { return request.post(/article, data) }这样做的意义在于后端接口路径可能要调整页面里不需要挨个改只需要改 api 文件接口返回值如果要做统一处理也只需要改 request 封装。跨域问题的处理也会简单很多开发环境用 Vite 代理生产环境用 Nginx 反向代理前端 axios 的 baseURL 统一配成 /api 就行。开发环境 Vite 代理配置在vite.config.js里export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) } } } })这里的 rewrite 是把 /api 前缀去掉再转发给后端。后端接口路径不要带 /api保持资源路径本身足够清晰。4.3 组件化设计与状态管理环保网站的页面复用性很高首页有资讯推荐列表资讯列表页有分页列表后台管理页也有内容列表。这些列表在视觉和交互上高度相似完全可以抽成一个公共组件。我自己通常会抽一个 ArticleCard 组件接收 article 对象作为 prop内部渲染标题、摘要、封面图、浏览量、发布时间。列表页只需要循环渲染这个组件就行template div classnews-grid ArticleCard v-forarticle in articleList :keyarticle.id :articlearticle clickgoDetail(article.id) / /div /template script setup import ArticleCard from /components/ArticleCard.vue import { useRouter } from vue-router const router useRouter() const goDetail (id) { router.push(/news/${id}) } /script组合式 API 里的script setup语法比 Options API 的 data/methods 结构清爽很多。定义响应式数据直接用 ref 和 reactive逻辑内聚代码量也少。状态管理方面只要不是特别复杂的应用Pinia 就够用了。常见的做法是建一个全局 user store存用户信息和登录状态import { defineStore } from pinia export const useUserStore defineStore(user, { state: () ({ token: localStorage.getItem(token) || , userInfo: null }), actions: { setToken(token) { this.token token localStorage.setItem(token, token) }, setUserInfo(info) { this.userInfo info }, logout() { this.token this.userInfo null localStorage.removeItem(token) } } })不要把所有数据都塞进 store。文章列表、空气质量数据这种从接口拉取的数据放在组件内部用 ref 管理就好否则 store 会越来越大维护起来很痛苦。4.4 前端视图组件刻不容缓的几个功能点环保网站的空气质量页面是体现前端能力的一个关键窗口。页面上要展示城市列表、AQI 数值、污染等级、各污染物浓度。数值变化时颜色也会跟着变化。这些是可以用 CSS 类和响应式数据轻松实现的。垃圾分类查询页面则更注重交互效率。用户输入关键词后前端需要防抖请求接口避免每敲一个字符就发一次请求。防抖逻辑直接写在函数里let timer null const searchGarbage (keyword) { clearTimeout(timer) timer setTimeout(() { fetchGarbageList({ keyword }).then(...) }, 300) }提一下防抖和节流看起来很简单但实际开发中经常被忽略导致接口瞬间被请求轰炸。如果后端还有限流策略用户就会碰到“操作过于频繁”的提示体验很差。5. 联调、测试与部署的完整闭环5.1 本地开发环境怎么搭建拿到这套源码后第一步是把环境跑起来。后端需要 JDK 8 或 11、Maven、MySQL前端需要 Node.js 14 以上、npm 或 yarn。我建议用 Docker 跑 MySQL省去本机安装数据库的麻烦也方便统一版本docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD123456 \ -e MYSQL_DATABASEenvironment_db \ mysql:8.0数据库创建好之后执行项目里的sql目录下的初始化脚本。这一步要注意如果脚本文件里有中文字符文件编码必须是 UTF-8否则执行建表和数据插入时会报乱码错误。Windows 下用记事本编辑过 SQL 文件之后就很容易出现这种问题建议用 VS Code 打开确认右下角编码。后端启动前修改application.yml里的数据库账号密码以及 JWT 的密钥。前端启动前检查vite.config.js里的代理配置和目标端口。两个服务都启动后浏览器访问http://localhost:5173默认就会请求后端的/api接口了。首次联调时最容易出现的问题就是接口 404 或 500。404 通常是路径写错了尤其要注意后端 Controller 里的类级路径和方法级路径拼接后是否与前端请求路径一致。500 则大概率是 SQL 语句或实体字段映射有问题直接看后端控制台的堆栈信息比反复刷新前端页面更高效。5.2 生产环境部署方案生产环境部署我强烈推荐用 Nginx 托管前端静态文件同时反向代理后端接口。后端打 jar 包通过 systemd 或守护进程管理。前端构建npm run build这会在dist目录生成静态文件把这些文件上传到服务器/var/www/environment-site然后配置 Nginxserver { listen 80; server_name your-domain.com; root /var/www/environment-site; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/; 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 /里的try_files必须带上$uri/和/index.html。这行配置是 SPA 应用的核心逻辑因为 Vue Router 默认是 history 模式前端路由跳转后如果直接刷新页面Nginx 找不到对应的物理路径就需要 fallback 到 index.html然后由前端路由接管。第二location /api/里的proxy_pass http://127.0.0.1:8080/;后面有一个斜杠。这个斜杠决定 Nginx 要不要保留/api前缀。有斜杠时请求/api/article/list会被转发到http://127.0.0.1:8080/article/list后端接收到的路径就没有/api了。没斜杠则会原样转发后端也需要有/api前缀才能匹配。5.3 环境配置与日志管理项目里的配置管理是个容易忽视但值得认真对待的问题。开发环境和生产环境的数据库地址、日志级别、文件上传路径都不一样不能每次都改 application.yml 再重新打包。SpringBoot 支持多环境配置我常用的做法是建三个文件application.yml # 公共配置 application-dev.yml # 开发环境 application-prod.yml # 生产环境启动时用--spring.profiles.activeprod指定环境。这样同一个 jar 包部署到哪个环境就用哪个配置非常灵活。日志方面建议至少配置 Info 级别并输出到文件。SpringBoot 默认只在控制台打印日志服务器重启后历史日志就没了。简单加一段配置logging: file: name: logs/application.log level: com.example.environment: DEBUG org.springframework: WARN生产级的日志滚动、按天切割这些可以用logback-spring.xml来实现但这个项目初期用日志文件落地就足够排查问题了。6. 常见问题与排查实录6.1 跨域问题一个绕不开的坑前后端分离后跨域问题几乎必然会碰到。前端在http://localhost:5173后端在http://localhost:8080两个端口不一样浏览器就会发起预检请求如果后端没做处理控制台就会出现“Access-Control-Allow-Origin”相关报错。解决办法有两个方向。开发环境利用 Vite 的 proxy 把请求代理到后端前端代码里没有跨域这个概念所有请求都指向同源的/api路径。生产环境用 Nginx 的 location 代理效果一样。这两个方向都建议优先采用而不是在后端开启全局 CORS。为什么因为 CORS 要处理预检请求、允许携带凭证、配置允许的域名多点配置就容易漏而且一旦对所有人放开还会引入安全风险。用代理方案的话前端代码不需要关心后端的真实地址部署时只需改代理配置是最稳妥的。如果真的需要在后端配置 CORS注意别用allowedOrigins(*)加上allowCredentials(true)这种组合浏览器会直接拒绝这种配置。还有SpringBoot 里如果自己写了 WebMvcConfigurer 的 addCorsMappings同时又用了拦截器要小心拦截器拦截了预检请求导致 CORS 配置根本没机会生效。预检请求的方法是 OPTIONS拦截器里要对 OPTIONS 请求直接放行。6.2 数据库连接与时区问题比较典型的报错是连接 MySQL 8 时出现java.sql.SQLException: The server time zone value ... is unrecognized原因是 MySQL 8 的驱动对时区要求更严格了URL 里必须指定时区。在application.yml里改成url: jdbc:mysql://localhost:3306/environment_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue注意allowPublicKeyRetrievaltrue这个参数MySQL 8 默认使用 caching_sha2_password 认证方式不少版本在不加这个参数时会报错“Public Key Retrieval is not allowed”。另一个容易遇到的问题数据库连接池配置了最大连接数但部署后过一段时间就报“Connection is not available, request timed out”。大概率是连接池没有正确释放连接或者某个慢查询占用了连接太久。解决思路是检查 MyBatis 的 SQL 有没有索引失效、事务有没有正确闭合同时把 HikariCP 的 maximum-pool-size 调到一个合理的值不要盲目开很大。6.3 登录状态丢失与权限拦截前端刷新页面后登录状态丢失这个问题在 JWT 方案下一般不会出现因为 token 存在 localStorage刷新后依然在。真正的问题是用户手动清除浏览器缓存、或者 token 过期后前端没有正确引导用户重新登录。后端的权限拦截器里不要只判断 token 是否存在还要解析 token 是否过期、用户是否被封禁。我这里提供一个拦截器设计思路public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { token token.substring(7); try { Claims claims Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(token) .getBody(); request.setAttribute(userId, claims.getSubject()); return true; } catch (Exception e) { // token 无效或过期 } } response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\message\:\未登录或登录已过期\,\data\:null}); return false; } }这里有一个细节如果请求的是公开接口比如首页新闻列表、空气质量数据那就不要把这个拦截器注册上去或者加一个白名单机制。很多人把所有接口都拦截了结果用户没登录连首页都打不开这不是正常网站该有的行为。后端返回 401 后前端 axios 拦截器再做响应处理跳转登录页。要注意跳转前把当前页面路径记录下来登录成功后还可以回跳。这是一个很小的体验优化但做不做对用户影响很大。6.4 头像上传与文件路径问题环保网站的用户中心和资讯编辑通常都需要上传图片。这个功能坑也不少最常见的坑就是前后端对文件访问路径的理解不一致。后端保存文件的目录可能是/data/upload/返回给前端的路径却是相对路径/upload/xxx.jpg。如果前端直接拿这个路径去请求会被 Nginx 的 SPA 配置拦下来返回 index.html 而不是图片于是图片加载失败。解决办法是在 Nginx 里加一个静态资源映射location /upload/ { alias /data/upload/; }同时文件上传接口要注意文件大小限制。SpringBoot 默认的单文件大小限制是 1MB如果不上调用户传一张稍微大点的图片就会报错。在配置里加上spring: servlet: multipart: max-file-size: 10MB max-request-size: 20MB前端也要做相应的校验比如文件类型限制在 jpg/png/webp文件大小限制在 5MB。双重校验能避免很多无意义的请求走到后端才发现失败。7. 我踩过的坑和后续扩展建议说实话这类前后端分离的内容型网站做起来并不难但想把细节做好确实要花不少心思。我个人的经验是不要急着写代码先把数据库表结构设计清楚再写后端接口最后才是前端页面。数据模型定了后面改动的成本会小很多。有几个小坑我每次带新人做类似项目时都会特别强调。第一个是时间格式的问题。SpringBoot 返回时间默认是时间戳前端拿到后如果不做格式化页面上的发布时间就会显示成一串数字。比较省心的解决办法是在 application.yml 里配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8但要注意这种配置只作用于 LocalDateTime 以外的 java.util.Date 类型。如果实体类用的是 LocalDateTime还是需要加JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8)注解或者统一在 VO 里处理。第二个是数据库索引的大意。很多初学者在列表页的查询 SQL 里直接对 create_time 做范围查询但没有给 create_time 建索引数据量只要超过几万条查询就会很慢。所以建表时就把查询频率高的字段想好加上索引以后能省很多事。第三个是接口返回字段的敏感性。接口列表能少返回字段就少返回比如文章详情接口要认真考虑返回值。给后台管理用的接口和给前台展示用的接口尽量分开不要图省事共用一套。这个项目后续如果要扩展有几个方向值得考虑。一个是把空气质量数据的展示能力升级一下用图表库比如 ECharts做历史趋势曲线用户可以看到过去几天的 AQI 变化。另一个是增加内容审核流程前台展示的文章必须经过审核这对面向公众的网站来说很重要。还有一个方向是增加数据导出功能比如管理员可以按月份导出环保项目进展报告这个用 EasyExcel 就能实现代码量不大但很实用。做环保类网站有个独特的优势就是这个领域的业务模式相对成熟数据模型不复杂又包含文本内容展示、动态数据查询、用户交互、后台管理等常见模块非常适合练手和学习全栈开发。如果你能把这套系统从头到尾自己动手实现一遍再碰其他内容型项目时会发现很多逻辑都是相通的。最后分享一个我的个人习惯每次改完代码或配置先用浏览器开发者工具看一下 Network 面板里的请求从请求头、请求体、响应体三步排查。很多前后端分离项目的疑难杂症其实都能在这一步定位到问题。别急着去翻代码先看清接口到底返回了什么、前端到底发送了什么这才是最快的调试路径。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑