刚拿到这套“基于SpringBoot+Vue的HTML语言环保网站管理系统”的时候,第一反应是:这不就是典型的Java全栈毕设/实训项目模板嘛。但深入拆完代码和设计思路后,我发现这个项目比想象中要有意思得多。它不只是一个增删改查的堆砌,而是把当前Java后端主流技术栈(Spring Boot 3.x + MyBatis + MySQL)和前端工程化方案(Vue 3 + Vite + Element Plus)完整串起来的一个环保行业信息管理系统。对于正在找毕设题目、或者想系统练一遍前后端分离开发流程的同学来说,这个项目的参考价值很高。
整个系统定位很明确:服务于环保资讯发布、环保项目管理、用户互动和后台数据维护。前台面向普通访客,提供环保新闻浏览、政策文件查询、项目公示等功能;后台面向管理员,提供内容管理、用户管理、数据统计等能力。技术上最大的亮点在于,它把“环保业务”和“Web管理系统”典型操作结合在一起,既有前端页面展示,又有后端权限管控,是一个少见的、业务领域比较具体的全栈练手项目。这篇文章我就从设计思路、核心功能、数据库设计、关键代码实现到部署上线,完完整整拆一遍,最后附上我实操中踩过的坑和排查记录,希望能让你少走弯路。
1. 内容整体设计与技术选型思路
1.1 为什么是SpringBoot + Vue,而不是其他组合
我先说结论:这套组合是目前Java全栈项目里“性价比最高”的方案,没有之一。Spring Boot负责后端接口开发和业务逻辑处理,Vue负责前端页面渲染和用户交互,两者通过RESTful API通信。对于环保网站管理系统这类典型CRUD密集型项目,这种前后端分离架构能让开发效率最大化。
后端选Spring Boot的核心原因是“约定优于配置”。传统SSH(Spring + SpringMVC + Hibernate)时代,一个Hello World都要写一堆XML配置,而Spring Boot通过自动配置和内嵌Tomcat,几乎能做到开箱即用。注意,早年的很多项目还在用Spring Boot 2.x,而这个项目是基于Spring Boot 3.x,这带来了两个重要变化:一是javax包名换成了jakarta,二是底层Java版本要求17以上。如果直接把网上老教程的代码拿过来,编译大概率会报错,这一点后面细说。
持久层框架选择MyBatis而不是Spring Data JPA,我的看法是:MyBatis对SQL的控制力更强,尤其适合多表关联查询和复杂统计场景。环保网站管理系统中,首页要展示最新资讯、项目进度要关联多张表、后台要按时间段汇总数据,这些场景用MyBatis写SQL非常直观。而且对于学习者来说,MyBatis的SQL日志能让你清楚地看到每一次数据库交互发生了什么,比JPA那种自动生成的SQL更容易排查问题。
前端选Vue 3 + Element Plus也有讲究。Vue 3的Composition API让逻辑复用更干净,Element Plus提供了现成的表格、表单、弹窗、分页组件,做后台管理系统几乎不需要自己写UI。项目里再配合Vite构建工具,开发时的热更新速度和构建效率明显优于Webpack时代的Vue 2项目。
1.2 环保业务场景怎么融入管理系统
这个项目有趣的点在于,它不是空泛的“通用后台”,而是围绕环保领域做了业务建模。核心业务链路可以拆成三条:
第一条是“内容发布-展示”链路。管理员在后台发布环保资讯(空气质量日报、水质监测报告、政策解读等),审核通过后展示在前台新闻列表页,用户可以按分类、时间、关键词筛选。这里涉及到内容状态管理(草稿/发布/下架),是内容管理系统的通用套路。
第二条是“用户互动-反馈”链路。前台用户注册登录后,可以针对环保项目发表评论、提交投诉建议,后台管理员能看到反馈列表并处理。这里需要设计用户表和反馈表的一对多关系,同时要注意接口的权限控制——未登录用户不能提交反馈,管理员才能处理反馈。
第三条是“数据统计-展示”链路。首页大屏或者数据看板需要展示环保数据指标,比如当月新增企业环保备案数、各分类资讯占比、用户反馈处理率等。这些数据一般通过聚合查询实现,MyBatis的SQL映射在这里发挥了主要作用。
这三条链路串起来,系统就不再是孤立的CRUD,而是一个有业务逻辑闭环的信息化管理工具。
2. 核心功能模块拆解与实现要点
2.1 前台门户模块:从首页到详情页的完整数据流
前台门户是访客最先接触的部分,直接决定感官体验。项目里前台页面包括首页、新闻列表页、新闻详情页、项目公示列表、用户登录注册页等。
以首页为例,它需要的不是单一接口,而是多个接口的聚合数据:轮播图(环保活动banner)、最新资讯列表(前6条)、重点环保项目展示(前4条)、统计数据卡片(三个数字)。如果后端分别提供5个接口,前端就要发5次请求,性能体验打折扣。比较优雅的做法是设计一个门户聚合接口,一次返回首页所需全部数据,用Map或者专门的VO类来封装。
项目里的做法是写了一个PortalController,里面通过调用多个Service方法组装数据。这里有个经验:聚合接口的Service层不要写一堆private方法堆在类里,建议每个业务块(bannerService、newsService、projectService)单独注入,保持代码整洁。前端Vue组件里用Promise.all并发请求也没问题,但如果接口数量多且接口有依赖关系,聚合接口仍然是首选项。
新闻详情页是一个很典型的细节考验点。它的核心流程是:前端通过路由参数传递新闻id,后端根据id查询新闻详情,同时需要增加浏览量。一个容易踩坑的地方是浏览量更新和详情查询的原子性——如果先查详情再更新浏览量,那么每次刷新页面都会导致浏览量不准确,因为查询是在更新之前的旧值。正确写法应该是在同一个事务里,先执行update浏览量+1,再执行select详情,或者直接用一条SQL update后再查询。我在代码里看到的是先update后select,这个顺序是对的。
2.2 后台管理模块:权限控制与通用CRUD
后台管理模块是管理系统的重头戏,包含:仪表盘(统计卡片)、资讯管理(发布/编辑/删除/审核)、项目管理(填报/进度跟踪)、反馈管理(查看/回复)、用户管理(禁用/重置密码)。
这里最关键的是权限控制。项目里用的是基于拦截器的简单权限方案:用户登录成功后把userId和role存到Session,然后写一个LoginInterceptor拦截器,在进入/admin/**路径前验证Session是否存在,并检查用户角色是否为管理员。这种方案比Spring Security轻量很多,作为学习项目完全够用,也能避免引入Spring Security后复杂配置劝退新手。
做按钮级权限控制的时候,常见的做法是数据库存权限标识,前端通过自定义指令v-permission判断是否渲染按钮。但这个项目里没有做这么细,只是在后端接口层面做了角色校验。对于学习项目来说,这个粒度是合理的。如果你想把权限做得更细,可以后续在Vue Router的路由守卫里根据角色动态添加可访问路由,同时后端接口再加一层@RequireRole注解,双保险。
通用的CRUD实现也有讲究。就拿资讯管理来说,后端封装了一个BaseService,抽出了分页查询、新增、修改、删除、逻辑删除这几个通用方法。建议把逻辑删除做进通用层——也就是表里加一个deleted字段,删除操作只是update deleted=1,这样所有业务表的数据都不会物理消失,方便后续数据回溯和恢复。项目里资讯表、反馈表都已经有is_deleted字段了,如果你是自己从零开发,强烈建议从一开始就带上这个字段。
2.3 前端工程化:路由、状态管理与接口封装
前端的工程化程度决定了后续维护效率。这个项目前端我梳理下来的结构是:
- 路由:Vue Router + 动态路由表。静态路由(登录页、首页、新闻模块)直接在router/index.js里注册;后台路由采用meta.requiresAuth和meta.roles字段做访问控制。
- 状态管理:用了Pinia。相比Vuex,Pinia的API更简单,store定义更符合直觉。项目里建了userStore,保存token、用户信息,并且通过persist插件把store数据持久化到localStorage,这个处理是必须的——否则刷新页面登录状态就丢了。
- 请求封装:axios实例统一配置baseURL和请求拦截器。请求拦截器里从localStorage取token并加到Authorization头,响应拦截器里统一处理后端返回的状态码,401时自动跳转到登录页。
前端这块最容易被忽略的是“接口路径统一管理”。项目里在src/api目录下按业务模块拆分了文件,比如news.js、project.js、feedback.js,每个文件就是一个模块的接口函数集合。这样后端接口变更时,前端只需要改一个文件,不会出现接口路径散落在组件里的情况。
3. 数据库设计与MyBatis核心实现
3.1 核心数据表关系设计
数据库是这个系统最值得花时间研究的部分。整个系统的核心表不算多,但关系清晰。我整理了一下核心表及其职责:
| 表名 | 核心字段 | 说明 |
|---|---|---|
| sys_user | id, username, password, role, status | 系统用户表,角色区分管理员和普通用户 |
| info_news | id, title, cover, content, category_id, status, view_count | 环保资讯表,关联分类表 |
| info_category | id, name, sort | 资讯分类表,如空气质量、水环境、政策法规 |
| env_project | id, project_name, company, type, progress, start_time | 环保项目管理表 |
| feedback | id, user_id, content, reply, status, create_time | 用户反馈表 |
| banner | id, image_url, link_url, sort | 首页轮播图表 |
| data_statistic | id, indicator_name, indicator_value, stat_date | 环保数据指标统计表 |
用户表和反馈表是一对多关系,资讯表和分类表是多对一关系,管理员可以对资讯维护多个分类,用户可以提交多条反馈,这是整个系统的核心表关系骨架。建表时需要注意外键约束不要滥用,项目里没有物理外键,而是通过逻辑外键维护——也就是说,业务表里的user_id只是普通索引字段,关联关系由SQL的JOIN语句而非数据库约束来实现。这样做的好处是高并发下写入更快,也不会有外键检查的性能损耗,代价是必须在应用层保证数据一致性。对于学习项目来说,这个选择是对的。
3.2 MyBatis高级查询与动态SQL
MyBatis是这个项目的一个核心部分。它在多条件组合查询场景下真的非常方便。以资讯列表为例,前端要传过来的筛选条件可能有:分类id、标题关键词、发布时间范围、状态,分页参数pageNum、pageSize。如果不做任何封装,至少需要写四种不同SQL,但是MyBatis的标签可以做这件事:
<select id="selectNewsList" resultType="com.example.entity.News"> SELECT * FROM info_news <where> <if test="categoryId != null"> AND category_id = #{categoryId} </if> <if test="keyword != null and keyword != ''"> AND title LIKE CONCAT('%', #{keyword}, '%') </if> <if test="status != null"> AND status = #{status} </if> </where> ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} </select>这个SQL的效果是:传入什么条件就拼接什么条件,不传就不拼。<where>标签会自动处理掉第一个AND,不会出现SQL语法错误。在项目里,这种动态SQL一般在资讯查询、用户查询、反馈查询等需要多种筛选项的场景都会大量出现,学会这个是MyBatis入门的重中之重。
还有一对多查询。以项目详情为例,一个项目可能包含多个进度节点(project_progress子表)。MyBatis里可以用collection标签一次性查出主表和从表数据:
<resultMap id="ProjectDetailMap" type="com.example.entity.Project"> <id property="id" column="id" /> <result property="projectName" column="project_name" /> <collection property="progressList" ofType="com.example.entity.ProjectProgress"> <id property="id" column="progress_id" /> <result property="progressDesc" column="progress_desc" /> </collection> </resultMap>注意,这个查询会有一个“N+1问题”的隐患——如果项目列表有10条,每条都要查进度列表,那么总查询次数是1+10=11次。数据量小无所谓,数据量大了性能就会劣化。项目里的做法是在详情页才查进度,列表页只查主表字段,这个思路是对的。
3.3 MySQL性能优化要点
既然是管理系统,数据量短期内不会太大,但基本的数据库规范还是要遵守。我把项目里用到的优化策略总结成以下几点:
一是索引设计。按照查询习惯,在category_id、create_time、status这些经常用于where条件或排序的字段上建普通索引(BTREE索引),在user_id、project_id这类外键字段上建索引加速关联查询。需要注意的是,索引不是越多越好——每次insert/update时,数据库都要同步更新索引,索引过多会导致写入变慢。区分度低的字段(如status只有0和1)尽量不单独建索引。
二是分页查询的优化。MySQL在深分页场景(比如LIMIT 100000, 10)下性能会急剧下降,因为offset越大,扫描的行数越多。项目里如果数据量增长到几十万条,建议改成“先查上一页最后一条记录的id,再用id > xxx AND ... LIMIT 10”这种方案。这个技术叫“延迟关联”或“走索引分页”。
三是SQL规范。所有SQL都要用parameterized query(#{}),一是防止SQL注入,二是能让MySQL走预编译缓存。注意${}不能用于拼接参数,项目里导出的Excel文件名因为要动态生成,不得已用了${},但必须限制输入内容为纯字母,不能带单引号。
4. 后端核心代码逐块拆解
4.1 SpringBoot分层架构与代码结构
后端代码采用的是标准的Controller-Service-Mapper三层架构。这个架构的核心约束是:Controller只负责接收参数和返回结果,Service负责业务逻辑编排,Mapper只负责数据库操作。遵守这个约束后,项目可维护性会明显提升。
项目包结构我拆出来是这样的:
com.example.epms ├── controller // 接口层 │ ├── PortalController.java │ ├── NewsController.java │ └── AdminNewsController.java ├── service // 业务逻辑层 │ ├── NewsService.java │ └── impl/NewsServiceImpl.java ├── mapper // 数据库访问层 │ ├── NewsMapper.java │ └── xml/NewsMapper.xml ├── entity // 数据库实体类 │ └── News.java ├── dto // 参数接收对象 │ └── NewsQueryDTO.java ├── vo // 视图返回对象 │ └── NewsVO.java ├── common // 通用类(Result、异常处理) └── config // 配置类(拦截器、跨域)一个值得学习的细节是entity、dto、vo的分离。entity对应数据库字段,dto用于接收前端查询参数,vo用于返回前端展示字段。很多初学者喜欢直接拿entity返回给前端,如果数据库表里刚好有password字段,前端接口直接就把密码返回了。项目里用户查询列表用的vo类只包含id、username、role、createTime这些安全字段,不包含password,这个习惯建议直接养成。
4.2 登录认证与拦截器实现
登录功能是系统安全的入口。项目里的实现方案是:用户提交用户名和密码,后端通过用户名查库,用BCrypt校验密码。BCrypt是一种加盐哈希算法——即便两个用户密码相同,由于随机盐的存在,存储的哈希串也不一样,安全性远高于MD5加固定盐。
项目里密码存储用的是BCrypt,这里必须强调:不要用MD5存密码,MD5已经被彩虹表破解得几乎形同虚设了。Spring Security的加密模块或jBCrypt库都可以实现BCrypt校验,代码上大概是:
// 注册时 String encodedPwd = BCrypt.hashpw(rawPassword, BCrypt.gensalt()); // 登录时 boolean match = BCrypt.checkpw(rawPassword, user.getPassword());登录成功后,后端生成一个简单的UUID token(学习项目可以不用JWT,但生产环境建议上JWT),把token存到Redis(或内存Map,项目里为了减少依赖用的ConcurrentHashMap)并设置过期时间,前端把token存到localStorage,后续请求在拦截器中带上token。这个方案的好处是简单、好理解,缺点是服务端需要维护token状态,不过以这个项目的规模完全够用。
实现LoginInterceptor的核心代码如下:
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口 if (request.getRequestURI().contains("/login")) { return true; } String token = request.getHeader("Authorization"); if (token != null && tokenService.checkToken(token)) { return true; } // 未登录,返回401 response.setStatus(401); return false; } }注意这里有个容易出问题的地方:拦截器如果直接对未登录请求返回401,前端axios响应拦截器需要接收这个状态码并跳转登录页。项目里前端在响应拦截器中对error.response.status === 401做了处理,这个前后端联动逻辑确保登录过期后用户会被自动导回登录页。
4.3 统一返回结果与全局异常处理
后端接口如果每次返回格式不统一,前端处理就会很崩溃。项目里统一封装了一个Result对象,结构如下:
{ "code": 200, "message": "操作成功", "data": { } }Controller所有接口都返回这个Result对象,成功code为200,业务失败code为500,未登录code为401。前端axios响应拦截器统一判断res.data.code,如果非200就弹出错误提示,不再用try/catch包裹每个请求。
配合统一返回结果的是全局异常处理器。项目里用@RestControllerAdvice + @ExceptionHandler处理异常,所有已知业务异常(比如用户名已存在、资讯不存在)抛出后用@ExceptionHandler捕获并返回错误信息。好处是Controller代码里不用写满try/catch,业务代码干净很多。
这里提一个经验:全局异常处理一定要区分“业务异常”和“未知异常”。业务异常(比如用户名重复)的message应该直接返回给前端展示;未知异常(比如NullPointerException)应该记录日志并返回“系统繁忙,请稍后重试”,不要直接把堆栈信息暴露给前端,避免信息泄露。
5. 前端页面实现与接口联调
5.1 Vue3工程结构和响应式页面的实现逻辑
前端项目是基于Vue 3 + Vite + Element Plus搭建的。相比Vue 2的Options API,Vue 3的Composition API在代码复用上的优势很明显。同一个页面里的数据、计算属性、方法,按功能聚合成一个逻辑块,相当于代码的“内聚性”更好。
拿新闻列表页举例,Composition API的核心代码如下:
const queryParams = reactive({ pageNum: 1, pageSize: 10, keyword: '', categoryId: null }); const newsList = ref([]); const total = ref(0); // 查询接口 const fetchNewsList = async () => { const res = await getNewsList(queryParams); newsList.value = res.data.rows; total.value = res.data.total; }; // 搜索按钮点击事件 const handleSearch = () => { queryParams.pageNum = 1; fetchNewsList(); }; onMounted(() => { fetchNewsList(); });这段代码的价值在于:queryParams是响应式对象,任何字段被修改后,只要调用fetchNewsList就会以最新参数重新请求。分页组件通过v-model:current-page和v-model:page-size绑定queryParams.pageNum和queryParams.pageSize,页码一变就会触发查询方法。这是后台管理系统非常标准的前端逻辑。
5.2 组件化开发思维:轮播图、卡片、表格如何复用
组件化是前端代码质量的分水岭。项目里比较好的实践是把轮播图封装成HomeBanner.vue组件,把分页表格封装成PaginationTable.vue组件。这样每个页面只用引用组件并传参,代码量少且维护方便。
一个具体的例子是数据卡片组件。首页的“统计卡片区域”(如累计项目数、本月反馈数、资讯总数)本质上是同一个组件被调用了三次,只是传入的icon、label、value不同。封装成StatCard.vue后:
<template> <div class="stat-card"> <el-icon><component :is="icon" /></el-icon> <div> <div class="stat-label">{{ label }}</div> <div class="stat-value">{{ value }}</div> </div> </div> </template> <script setup> defineProps({ icon: String, label: String, value: [String, Number] }); </script>这个组件可以用在首页仪表盘,也可以用在环保数据展示大屏,复用性极强。初学前端时最容易犯的错误是每个页面都复制一份HTML结构,后期需求一变要改十几个地方,所以养成组件化思维比背API更有价值。
5.3 前后端联调中的跨域与代理配置
前后端分离联调时,困扰大多数新手的问题就是跨域。前端开发服务器地址是localhost:5173,后端接口地址是localhost:8080,浏览器出于同源策略会拦截跨域请求。解决办法有两种:后端开启CORS,或前端配置代理。
项目里的方案是前端Vite代理。在vite.config.js中配置:
server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') } } }这样前端请求的URL以/api开头时,Vite开发服务器会把请求转发到后端8080端口。浏览器发出的请求是同源的,就不会触发跨域拦截。生产环境部署时,一般用Nginx做反向代理,把/api路径转发到Spring Boot应用,原理和Vite代理是一样的。
配置完代理后,前端axios的baseURL要设为/api而不是http://localhost:8080/api——很多新手纠结为什么代理不生效,其实多半是baseURL写死了真实地址,请求根本没有经过代理服务器。
6. 项目部署与上线实操
6.1 本地开发环境搭建步骤
如果你是第一次跑这个项目,本地环境准备有四个环节。首先确认JDK版本必须是17及以上,Spring Boot 3.x不兼容JDK 8。其次准备MySQL 8.x,执行项目中提供的SQL脚本初始化数据库。然后是Node.js 18+,用来跑前端Vite。最后是IDE,后端用IntelliJ IDEA,前端用VS Code即可。
依次启动后端和前端:后端Spring Boot主类直接运行,默认8080端口;前端在项目根目录执行npm install安装依赖,然后npm run dev启动,访问http://localhost:5173。MySQL连接配置在application.yml里,需要修改成你自己的数据库名、用户名和密码。
注意:如果MySQL是8.x版本,驱动类要写
com.mysql.cj.jdbc.Driver,URL需要拼接useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai。不要用旧版的com.mysql.jdbc.Driver,在新驱动下会直接启动报错。
6.2 服务器部署:从打包到Nginx反向代理
本地跑通之后,部署到云服务器是完整闭环的最后一环。部署分为三步:
第一步,后端打包。在项目根目录执行mvn clean package -DskipTests,生成jar包。启动命令建议写成:
nohup java -jar epms.jar --spring.profiles.active=prod > app.log 2>&1 &这里使用--spring.profiles.active=prod指定生产配置文件,生产环境的数据库密码、日志级别和本地环境不同,用profile隔离是最规范的做法。
第二步,前端打包。执行npm run build,生成dist目录,里面是纯静态文件。然后把dist目录上传到服务器,配合Nginx配置:
server { listen 80; server_name your_domain; # 前端静态资源 location / { root /opt/epms/dist; 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; } }try_files $uri $uri/ /index.html;这一行很关键,它保证Vue Router开启history模式时,刷新子路由页面不会404。
第三步,检查防火墙和端口安全组。云服务器控制台的入方向规则必须放行80端口,后端8080端口建议不要对外开放,只让Nginx通过内网访问,这样可以避免Spring Boot接口被直接外网调用。
6.3 环境差异化配置:开发与生产分离
很多项目跑不起来的根源在于配置混淆。开发时数据库可能是本地,生产用的是云数据库;开发时日志打印到控制台,生产要写入文件。这些差异用Spring profiles解决非常简单。
在application.yml主配置里放公共配置,然后在application-dev.yml和application-prod.yml里放各自环境的差异化配置:
# application-prod.yml spring: datasource: url: jdbc:mysql://生产数据库地址:3306/epms?... username: production_user password: 生产密码 logging: file: name: /opt/epms/logs/app.log启动时通过--spring.profiles.active=prod指定环境即可。项目默认使用dev,新手如果直接把生产配置写到主配置文件里,本地跑的时候连不上生产库,部署时又要改回来——用profile彻底根治这个问题。
7. 常见问题与排查技巧实录
7.1 启动类报错:javax不存在
这是Spring Boot 3.x最典型的迁移问题。老项目导入后编译报红,javax.servlet等包不存在,因为Spring Boot 3.x已经全面切换为jakarta.*命名空间。解决办法是把代码里所有javax开头的import改成jakarta开头。如果是自己从零写项目,直接在新版本下用Spring Initializr生成项目,就不会遇到这个问题。
7.2 分页查询不生效或页码错乱
分页问题出现频率极高。常见现象是前端传了pageNum=1、pageSize=10,但查询结果一直是所有数据。排查顺序是:核对前端请求参数是否完整传递,用浏览器的Network面板看请求Payload——确认后端是否接对了参数名。项目里分页参数名是pageNum/pageSize,如果你用page/pageLimit,默认就会传NULL,分页自然失效。
另一个隐蔽问题是PageHelper插件(或手写limit)的后端逻辑:如果查询方法中在Mapper之前又执行了其他SQL,会导致分页结果错乱。PageHelper的原理是基于线程局部变量的拦截器,它会拦截最近的一条查询语句。如果代码里先执行了别的查询,分页就会作用到那条语句上。解决办法就是确保分页拦截器紧挨着目标查询执行。
7.3 MySQL时区差八小时
项目里数据库时间字段显示比北京时间晚8个小时,在JDBC连接URL中配置serverTimezone=Asia/Shanghai可以解决存储端问题。如果已经出现数据写入后时间偏差,需要检查MySQL全局时区:
SET GLOBAL time_zone = '+08:00'; SET SESSION time_zone = '+08:00';如果用了Spring Boot的Jackson序列化,可能还需要在application.yml中配置spring.jackson.date-format和time-zone: GMT+8,否则接口返回的JSON时间字符串会按UTC序列化。
7.4 前后端联调时请求403或404
这种问题多数是跨域代理没有配好。先打开浏览器F12看网络请求,判断请求是发到了5173还是8080。请求落到8080则可能是跨域没处理;落到5173但报404,多半是代理rewrite路径写错,导致后端接口路径对不上。核对一遍vite.config.js的proxy配置,特别要注意rewrite是否把/api前缀替换干净。
如果后端已经开启CORS仍然报403,注意检查是否开启allowedHeaders或allowedOriginPatterns设置。项目里建议统一使用代理方案,避免在生产环境还要维护CORS配置。
7.5 MyBatis返回结果字段为null
前端拿到数据后,对象属性全是null,SQL单独跑也有结果,这类问题绝大多数是数据库下划线字段和Java驼峰属性映射失败导致的。MyBatis默认不会自动将create_time映射为createTime,需要在全局配置中开启驼峰映射:
mybatis: configuration: map-underscore-to-camel-case: true开启后,create_time字段就会自动映射到createTime属性,不需要手写几百行resultMap。
7.6 前端页面刷新后状态丢失
用Vuex或Pinia管理登录状态,刷新页面后数据被清空,因为store默认只在内存中。解决办法是用Pinia的持久化插件。项目里的方案是把userStore中的token字段配置了持久化存储到localStorage,刷新后从localStorage恢复登录状态。注意不要把所有数据都持久化——只持久化必须的token和用户信息,避免localStorage存储复杂对象导致性能问题。
7.7 后台管理页面报表加载慢
后台仪表盘要展示统计数据,如果统计SQL是全表扫描加聚合计算,数据量上来后会明显变慢。优化手段有两个:一是给stat_date字段建索引,二是把统计结果缓存到Redis,设置过期时间5分钟。项目里用了一个更简单的方案:维护了一张data_statistic表,每天通过定时任务生成一次统计数据,仪表盘和首页直接查这张表,而不是实时去聚合业务表。对于环保网站这种数据时效性要求不高的系统,定时统计的效果很好。
实操感悟与后续扩展建议
这套项目跑完一遍之后,我对“全栈管理系统”的理解比单纯看教程时清晰了很多。最大的收获在于:一个完整的业务系统,数据流从前端页面到后端Controller再到Mapper最终落库,中间跨域、代理、状态管理、权限拦截这些环节缺一不可,真正的难点往往不在于某个技术点的深度,而在于这些技术点如何串联协作。
如果要在这个项目基础上继续扩展,我个人建议优先考虑三个方向:一是接入Redis缓存热点资讯和统计结果,降低数据库压力;二是引入Spring Security或Sa-Token,把登录授权设计得更规范,配合OAuth2对接第三方登录;三是把环保数据部分做成大屏可视化,用ECharts展示区域环境质量指标,这样业务价值会更直观。技术框架不变的情况下,这些扩展都不会伤筋动骨。
最后再分享一个我在实践中养成的习惯:任何模块开发完成后,先把接口文档写出来(哪怕只是用一个Postman集合),再调前端。不要小看这一步,它能节省大量前后端联调的时间,也能让业务描述更明确。这套环保网站管理系统虽然规模不大,但麻雀虽小五脏俱全,认真撸一遍,从环境搭建到部署上线的整个流程你都会有一个全局的掌握。