1. 项目整体设计与技术选型分析
1.1 为什么选 Vue + Spring Boot 做减肥网站
先说结论:这个组合放到今天依然是中小型管理系统和个人实习项目最稳妥的选择,没有之一。
我在实习期间接手这个减肥网站项目时,第一件事不是写代码,而是把技术栈明确下来。当时备选方案有三个:纯 JSP + Servlet、Vue + Spring Boot、Vue + Node.js。JSP 那套早就该淘汰了,前后端耦合严重,改一个页面样式都要重启服务器,效率低到没法看。Node.js 做后端虽然轻量,但公司现有服务器环境、运维习惯都是 Java 体系,而且要对接后续的权限框架、工作流引擎,Java 生态明显更占优势。
Vue + Spring Boot 的组合能站住脚,核心原因是前后端彻底分离。前端只需要关心页面渲染和用户交互,后端只需要关心业务逻辑和数据接口,两边通过 JSON 通信,互不干扰。实习期间带我的组长说了一句话我到现在都记得:“团队协作的本质是减少沟通成本,前后端分离就是在架构层面减少沟通成本。”你在公司里写代码,很少是一个人从头写到尾的,前端组、后端组、测试组各管一摊,技术栈不分离,协作就是灾难。
这个项目本身的需求也不复杂:用户注册登录、个人资料管理、饮食记录、运动记录、体重曲线、减肥计划推荐、社区帖子互动。说白了就是一个垂直领域的业务管理系统,加上一点内容社交属性。这类场景对技术架构的要求不是“高并发、高可用”,而是“结构清晰、易维护、能快速迭代”。Vue 的组件化开发方式非常适合这种业务模块较多的项目,Spring Boot 的自动配置和 Starter 机制又极大减少了后端样板代码。
另外一个很现实的因素是学习资源密度。Vue 和 Spring Boot 的社区资料太丰富了,遇到任何问题基本都能搜到现成答案。对于实习生来说,这意味着你可以把精力放在业务逻辑和功能实现上,而不是被困在环境配置和框架使用上。
1.2 宏观功能拆解:减肥网站到底有哪些模块
实习报告的框架不能只写技术栈,功能模块的拆解才是体现你对业务理解的地方。我最终把整个系统分成六大模块,每个模块都对应一个业务场景:
- 用户模块:注册、登录、个人信息维护、密码修改。这是所有系统的基础,最简单也最容易忽略细节。
- 饮食管理模块:记录一日三餐的热量摄入,维护常见食物数据库,支持自定义食物。
- 运动管理模块:记录有氧运动和无氧运动的时长、消耗热量,生成运动打卡记录。
- 体重趋势模块:按天记录体重数据,用折线图展示变化趋势,支持不同时间范围的筛选。
- 计划推荐模块:根据用户的基础代谢率(BMR)和每日消耗,给出减脂期的热量缺口建议和饮食运动计划。
- 社区互动模块:发布减肥心得帖子、评论、点赞。这块是为了提升用户粘性,也让实习报告看起来更有层次。
从难度系数来说,前四个模块属于标准 CRUD,第五个模块涉及一点简单的算法逻辑,第六个模块涉及表关联和列表分页。这个难度梯度刚好踩在实习生的能力区间——太简单了学不到东西,太难了做不完。
体重趋势模块是整站的亮点功能,我用到了 ECharts 来绘制折线图,前端只负责把后端返回的体重数据组扔给图表组件,后端负责数据的聚合查询。这个功能演示效果非常好,实习答辩的时候评委老师特意多看了两眼,因为图表比表格直观太多了。
2. 前端 Vue 核心细节拆解与实操要点
2.1 Vue 工程化搭建与目录结构设计
现在做 Vue 项目基本都是从 Vue CLI 或者 Vite 起步。我这里用的 Vue 3 + Vite 组合。很多人纠结选 Vue 2 还是 Vue 3,我的建议非常直接:新项目一律 Vue 3,除非公司老项目维护需要 Vue 2。Vue 3 的组合式 API(Composition API)在逻辑复用层面比选项式 API(Options API)舒服太多,尤其是一个组件里涉及多个业务状态的时候,选项式会把 data、methods、computed 拆得七零八落,而组合式可以按功能点把逻辑收拢在一起。
环境搭建流程其实很简单,装 Node.js 的时候顺手把 npm 也装了,然后用npm create vite@latest初始化项目。这里有个坑我一定要说:国内网络环境下,npm 直接装依赖经常超时,我实习第一天就栽在这里。解决办法是换淘宝镜像源,一行命令搞定:
npm config set registry https://registry.npmmirror.com没用镜像之前,一个 vue-router 装了快二十分钟最后还给报 ETIMEDOUT,换了镜像之后全程不到三十秒。这是面试里经常被问到的点,也是实习工作里的基础常识,写进报告里绝对是加分项。
工程目录我是按照业务模块来划分的:
src/ ├── api/ # 接口请求封装 ├── assets/ # 静态资源 ├── components/ # 公共组件 ├── router/ # 路由配置 ├── store/ # Pinia 状态管理 ├── utils/ # 工具函数 ├── views/ # 页面组件 │ ├── diet/ # 饮食管理页面 │ ├── exercise/ # 运动管理页面 │ ├── weight/ # 体重趋势页面 │ ├── plan/ # 计划推荐页面 │ └── community/ # 社区页面 └── App.vue这样的目录结构在答辩和日后面试复盘时都能讲出道理:按业务域划分目录是在为项目的可扩展性做铺垫,以后每增加一个模块,直接在 views 下建一个文件夹,不会把项目搞成一锅粥。
2.2 前端路由与权限控制的落地方式
路由是 Vue 项目里最容易出问题的部分,也是面试必考。我这个项目用的是 Vue Router 4,配合 Pinia 做登录态的全局管理。
路由表我分成了两部分:基础路由(登录页、注册页、首页)和需要登录后才能访问的业务路由。请求拦截器在上一个步骤里已经统一带上了 token,但页面级别的跳转还需要在前端做一层守卫判断。Vue Router 的全局前置守卫是这么写的:
// router/index.js router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') const whiteList = ['/login', '/register'] if (token) { next() } else { if (whiteList.includes(to.path)) { next() } else { next('/login') } } })没有 token 就直接送回登录页,这个逻辑很直白。但实习生容易漏掉一个细节:token 过期的情况。如果 localStorage 里的 token 是过期的,前端仍然放行,请求发到后端被拦截,接口返回 401,界面就白屏了。所以我在响应拦截器里也加了一重处理:
// utils/request.js axios.interceptors.response.use( response => response, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') router.push('/login') } return Promise.reject(error) } )前端做权限控制只是用户体验层面的事,真正的安全防线一定在后端。实习报告里要体现这种认知:前端的路由守卫可以被绕过,后端的接口鉴权才是安全底线。
2.3 播放器、图表等组件的选择与踩坑记录
减肥网站里我嵌入了两个比较有分量的第三方组件:一个是 ECharts 体重折线图,一个是西瓜播放器(xgplayer)用于播放运动教学视频。
ECharts 在 Vue 3 项目里推荐用vue-echarts封装组件,支持按需引入,能明显减小打包体积。一开始我图省事直接全量引入,Vite 打包后 chunk 文件有 800 多 KB,页面首屏加载那个慢啊,后来改成按需引入,直接缩到 200 KB 以内。这就是实习报告里可以写的性能优化点,面试官很吃这一套。
播放器这里有个跟热词相关的需求:播放 m3u8 格式的视频流。m3u8 是 HLS 直播/点播常用的格式,浏览器原生不支持直接播放,需要借助 hls.js。西瓜播放器恰好内置了 hls 支持,使用起来非常简单:
import Player from 'xgplayer' const player = new Player({ id: 'video-container', url: 'https://example.com/video/index.m3u8', isLive: false, autoplay: false })踩过的坑是跨域问题。视频文件放在服务器的静态资源目录里,播放请求被同源策略拦截,折腾了半天发现后端加一段 CORS 配置就解决了。这个我在后面后端章节展开讲。
3. 后端 Spring Boot 设计与环境配置经验
3.1 版本选型与环境配置的坑
Spring Boot 的版本问题在热词里出现了好多次,比如“springboot版本太高”“springboot配置”之类,说明这是很多新手共同的痛点。我实习那会儿用 IntelliJ IDEA 创建项目,默认拉到最新的 Spring Boot 版本,结果跟公司内部的依赖一整合,各种兼容性问题就冒出来了。
我最终用的是 Spring Boot 2.7.x 版本。说句实话,Spring Boot 3.x 虽然已经发布很久了,但它基于 Jakarta EE,很多老教程和老依赖还是用 javax 命名空间,对实习生来说遇到问题搜索成本太高。2.7.x 是 2.x 系列的最后一个维护版本,资料最多,踩过的坑几乎都有现成的解决方案。
数据库用了 MySQL 8.0,持久层框架用的是 MyBatis Plus 而不是原生 MyBatis。原因很简单:MyBatis Plus 内置了单表 CRUD 的通用方法,不需要自己写 XML 映射文件,开发效率高一大截。虽然面试的时候有人会杠“MyBatis Plus 不尊重 SQL”,但实习项目就是要快速出活,MyBatis Plus 完全够用,复杂的多表查询照样可以写自定义 SQL。
热词里有一个非常实用的问题:“springboot + mybatis 当表不存在自动建表”。实习期间我刚好遇到了这个需求:部署到新环境时,数据库里表结构还没初始化,每次都手动导入 SQL 太痛苦。MyBatis Plus 提供了一个自动建表的扩展组件mybatis-plus-extension,配合initialization-mode: always配置,可以在应用启动时自动执行建表脚本:
# application.yml spring: sql: init: mode: always schema-locations: classpath:sql/schema.sql前提是schema.sql里的语句要写得足够健壮,最好都带上IF NOT EXISTS,否则每次启动都会尝试建表,抛出“表已存在”的异常。
3.2 核心业务后端的接口设计与实现
后端接口我遵循的是 RESTful 风格,用/api作为统一前缀。每个业务模块设计对应的 Controller、Service、Mapper 三层结构。
拿饮食管理模块举例,接口设计是这样:
| 方法 | 路径 | 功能 |
|---|---|---|
| GET | /api/diet/list | 获取当前用户的饮食记录列表 |
| POST | /api/diet/add | 新增一条饮食记录 |
| PUT | /api/diet/update | 修改饮食记录 |
| DELETE | /api/diet/delete/{id} | 删除饮食记录 |
代码示例里最关键的其实是 Service 层的事务控制。比如用户新增一条饮食记录时,不仅要插入记录本身,还要同步更新“今日总摄入热量”这个冗余字段。这两个操作必须放在同一个事务里,否则数据会不一致:
@Service public class DietServiceImpl extends ServiceImpl<DietMapper, Diet> implements DietService { @Override @Transactional(rollbackFor = Exception.class) public boolean addDietRecord(DietDTO dto) { // 1. 插入饮食记录 Diet diet = new Diet(); BeanUtils.copyProperties(dto, diet); this.save(diet); // 2. 更新当日热量统计 return calorieStatisticsService.updateDailyIntake(dto.getUserId(), dto.getIntakeCalorie()); } }注意@Transactional注解里rollbackFor = Exception.class这个参数。默认情况下 Spring 的事务管理只对 RuntimeException 生效,如果方法抛的是 checked exception,事务不会回滚,这个细节面试经常问。
3.3 Spring Boot 配置文件与安全加固配置
Spring Boot 的配置文件是application.yml,里面最核心的几块是数据源、服务器端口、MyBatis Plus 配置、JWT 配置。
热词里有人搜“springboot yml 密文”,说的是配置文件里的密码不能明文存储的问题。我在实习项目里实践了一把用 Jasypt 做配置项加密:
# application.yml spring: datasource: url: ENC(EncryptedDatabaseUrl) username: ENC(EncryptedUsername) password: ENC(EncryptedPassword) jasypt: encryptor: password: ${JASYPT_SECRET}Jasypt 的原理是用一个外部传入的主密钥去解密配置文件里的 ENC 密文,即使配置文件泄露了,攻击者也拿不到真正的数据库密码。当然这个方案只是增加了一层保护,真正生产环境里更推荐用配置中心来管理敏感信息。
热词里还有“springboot heapdump 敏感信息泄露漏洞”,这个知识点值得特别注意。Spring Boot Actuator 的 heapdump 端点如果暴露在外网,攻击者可以直接下载内存快照,从中提取密码、token、数据库连接信息。实习的时候我竟然把这个风险忽略了,后来被组长点名批评。解决方式很粗暴:
# 生产环境禁用不必要的端点 management: endpoints: web: exposure: include: health,info只暴露health和info端点,其他的一律关闭。如果业务上确实需要 metrics 端点,也要放到内网并通过权限控制访问。
4. 前后端联调与核心功能实现
4.1 接口联调中的 Token 处理方案
前后端分离项目里,Token 处理是联调阶段最磨人的环节。我参考了网上 vben admin 这类成熟模板的做法,统一在 axios 请求拦截器里处理:
// utils/request.js const request = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 10000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config })所有请求自动带上Authorization头,后端通过拦截器解析并校验 token。这个方案的好处非常明显:业务代码里不需要关心 token 在哪、怎么传,全部在基础设施层处理干净了。
后端的 JWT 校验我用的是 HandlerInterceptor + ThreadLocal 的组合:
public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token = request.getHeader("Authorization"); if (StringUtils.hasText(token) && token.startsWith("Bearer ")) { token = token.substring(7); // 解析 token,将用户信息存入 ThreadLocal } // 校验失败则返回 401 response.setStatus(401); return false; } }Token 校验通过后,把当前用户 ID 存到 ThreadLocal 里,后续的 Controller 和 Service 层直接通过UserContext.getUserId()获取操作人,不用在每个接口里反复解析 token。这个小工具类写起来不难,但能让代码优雅很多。
4.2 体重趋势模块的前后端实现过程
体重趋势模块是前后端联调最典型的例子。前端需要根据用户选择的时间范围(近 7 天、近 30 天、近 90 天)展示折线图。
前端逻辑:在WeightTrend.vue里监听时间范围切换,重新调用接口获取数据:
const loadWeightData = async () => { const res = await getWeightTrend(selectedRange.value) if (res.data.code === 200) { chartData.value = res.data.data } }后端接口接收days参数,按日期聚合查询:
@GetMapping("/trend") public Result<List<WeightTrendVO>> getTrend(@RequestParam Integer days) { LocalDate startDate = LocalDate.now().minusDays(days - 1); List<WeightRecord> records = weightMapper.selectList( new LambdaQueryWrapper<WeightRecord>() .ge(WeightRecord::getRecordDate, startDate) .eq(WeightRecord::getUserId, UserContext.getUserId()) .orderByAsc(WeightRecord::getRecordDate) ); return Result.success(convertToVO(records)); }这里有个设计上的小细节:减肥期间体重数据不可能每天都有记录,如果直接查出来什么画什么,折线图会缺很多点。我在 VO 转换层做了日期补全,把没有记录的日子用null填充,前端 ECharts 的connectNulls: false配置会让断点处断开,图表看起来更真实。
4.3 部署上线:从本地到服务器的完整路径
实习报告的另一个亮点是部署经历。热词里有人搜“xshell 部署 vue 打完包前端”,这确实是一个劳动密集但收获很大的环节。
前端打包很简单:
npm run build打包完成后的dist目录是一个纯静态文件集合,里面只有一个index.html和若干 JS/CSS 资源。我在服务器上用的是 Nginx 来托管前端文件,后端用的是java -jar直接跑 Spring Boot jar 包。
Nginx 配置里最关键的是一段反向代理:
server { listen 80; server_name your-domain.com; # 前端静态文件 root /data/web/dist; index index.html; # 解决 Vue Router history 模式刷新 404 的问题 location / { 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; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }第三行的try_files指令是 Vue Router history 模式部署的关键。如果不加这一行,用户在/weight这种路径下刷新页面,Nginx 找不到对应的物理文件会返回 404,加了之后 Nginx 会把路由重定向到index.html,由前端路由接管后续的页面渲染。
后端启动命令我习惯写成脚本,避免每次手敲长串参数:
#!/bin/bash nohup java -jar weight-fat-loss-server.jar \ --spring.profiles.active=prod \ --server.port=8080 \ > logs/app.log 2>&1 &nohup保证即使 SSH 窗口关闭,进程也能继续运行。--spring.profiles.active=prod指定生产环境配置,保持环境隔离。
5. 常见问题与排查技巧实录
5.1 前端高频问题速查
实习期间前端踩过的坑,我整理成了一张问题排查表:
| 问题现象 | 根本原因 | 解决方案 |
|---|---|---|
| Vue 对象赋值后页面不更新 | Vue 3 中用obj.xxx = value新增属性,非响应式 | 使用reactive或ref包裹,或利用Object.assign重新赋值 |
| 路由跳转后页面不刷新 | Vue Router 复用组件实例导致 created 钩子不执行 | 使用onBeforeRouteUpdate监听路由参数变化 |
| npm 安装依赖超时 | 网络原因 | 切换淘宝镜像源 |
| 打包后文件太大 | 全量引入了 ECharts | 改为按需引入 |
| Nginx 部署后刷新 404 | 缺少try_files配置 | 添加try_files $uri $uri/ /index.html; |
第一行那个“Vue 对象赋值页面不变”是热词里被频繁搜索的问题,我实际也遇到过。原因是 Vue 3 的响应式系统是基于 Proxy 实现的,如果给一个已经创建好的 reactive 对象动态添加新属性,Proxy 是可以感知到的;但如果用的是普通对象(比如ref({})里的对象),直接添加属性就不会触发视图更新。解决办法是预先定义好所有字段,或者在赋值时把整个对象换掉。
5.2 后端高频问题速查
| 问题现象 | 根本原因 | 解决方案 |
|---|---|---|
java -jar启动失败但本地 IDEA 正常 | 生产环境依赖的 MySQL 连接不上 | 检查防火墙、数据库账号权限、连接串写法 |
| 存在 CORS 跨域报错 | 前端请求未经允许跨域访问后端 | 后端配置 CORS 或使用 Nginx 反向代理解决同源问题 |
| MyBatis Plus 自动建表不生效 | 数据源初始化模式配错 | 确认spring.sql.init.mode配置为always |
| heapdump 端点暴露 | 安全基线不达标 | 关闭多余 Actuator 端点 |
| 接口返回 401 且前端白屏 | token 过期后未做拦截处理 | 拦截器捕获 401 并跳转登录页 |
5.3 我最想强调的两个经验教训
实习里学到的东西很多,但最想分享的是这两个。
第一,日志是你最好的调试工具。前后端联调出问题时,新手第一反应是打 UI,看接口返回了什么。但实际上问题经常出现在调用链的中间层。我排查过一个问题:前端明明传了正确的userId,后端查到的记录却是空的。后来看日志发现,数据库表里的user_id字段是 bigint 类型,而我传入的是一个 Integer 类型的uid,MyBatis 类型转换直接失败了。如果没有日志,这种问题能查一天。
Spring Boot 里使用@Slf4j注解配上 Lombok,然后直接用log.info()、log.error()打日志,断点调试反而没有这个高效,特别是生产环境没法打断点。
第二,写实习报告和做项目一样重要。如果一个功能你自己都没想清楚为什么这么做,答辩的时候被问两句就露馅了。我的做法是每完成一个模块,就在项目文档里记录三件事:这个模块解决了什么问题、技术上踩了什么坑、还有什么可改进的空间。最后实习报告其实就是把这份文档结构化整理一遍,工作量小很多,而且内容真诚,不像是拼凑课设出来的。
6. 实习项目总结与后续可扩展方向
严格来说我建议你把这部分当成项目的复盘问答来用,而不是单纯的大段总结。写报告的时候其实就是回答三个问题:项目做了什么、你怎么做的、如果没有你它会怎么样。
减肥网站这个项目本身并不复杂,但它的价值在于完整覆盖了一个前后端分离项目从开发到上线的全部流程。如果你在写自己的实习报告,可以试着把视角拔高一点:不只是写“我实现了十个接口”,而是写“我通过这个项目理解了系统设计、团队协作、代码规范、部署运维的完整链路”。面试官真正想听的是这个。
后续可扩展的方向我列几个,都是真实业务里出现过或可能有用的:
- 引入 Redis 做缓存,优化体重排行榜和高频热帖的查询效率
- 引入 RabbitMQ 做消息队列,支持社区帖子的异步通知
- 接入第三方登录(微信或飞书),降低注册门槛
- 把计划推荐模块的简单公式替换为机器学习算法(用户画像 + 饮食偏好)
- 前端加上移动端适配,做一套响应式界面
最后再分享一个实际体会:实习报告不是写给人看的,是写给三个月前的自己看的。如果读完报告你能想起来当时为什么踩坑、怎么把坑填平,这个项目就真正的属于你了。祝你答辩顺利。