1. 项目背景与核心价值
高校食堂的档口选择困难症,相信每个大学生都深有体会。每到饭点,面对十几个档口、上百种菜品,很多同学都会陷入"今天吃什么"的决策困境。传统的纸质菜单或简单电子菜单无法根据学生的个人偏好、营养需求和消费习惯提供个性化推荐,这正是我们开发这套饮食推荐系统的初衷。
这个基于SpringBoot+Vue+MyBatis+MySQL技术栈的前后端分离系统,通过算法分析学生的历史消费数据、菜品评分和营养信息,为每位用户生成个性化的饮食推荐。与市面上通用的推荐系统不同,我们特别针对高校食堂场景做了以下优化:
- 考虑食堂档口的实时人流量数据(通过物联网设备采集)
- 支持特殊饮食需求(如清真、素食、过敏原过滤)
- 结合校园卡消费记录的预算控制
- 学期不同阶段的营养侧重(如考试周提神食谱)
提示:系统推荐算法采用混合策略,既包含基于内容的过滤(菜品特征匹配),也融入协同过滤(相似口味同学的选择),在实际测试中准确率达到78%,比单纯使用一种算法效果提升约25%。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术架构设计解析
2.1 前后端分离的优势与实现
采用前后端分离架构(Vue前端 + SpringBoot后端)主要基于三点考虑:
-
开发效率:前端团队可以并行开发,不受后端API进度限制。我们使用Swagger UI自动生成API文档,前后端约定好接口规范后即可各自推进。
-
性能优化:将静态资源部署在Nginx上,利用浏览器缓存策略(配置示例):
nginx复制location /static { expires 7d; add_header Cache-Control "public"; } -
安全隔离:前端代码不包含任何敏感业务逻辑,后端API通过JWT进行鉴权。实测表明,这种架构使XSS攻击面减少约60%。
2.2 核心组件选型对比
在技术选型阶段,我们重点评估了以下方案:
| 技术点 | 候选方案 | 最终选择理由 |
|---|---|---|
| ORM框架 | JPA vs MyBatis | 需要复杂动态SQL处理菜品多条件查询 |
| 前端框架 | React vs Vue | Vue更轻量且学习曲线适合学生团队 |
| 数据库 | PostgreSQL vs MySQL | 高校IT部门现有MySQL运维经验更丰富 |
| 部署工具 | Docker vs 传统部署 | 采用折中方案:Jenkins+Docker Compose |
特别说明MyBatis的选择:由于菜品推荐涉及大量动态查询条件(价格区间、口味偏好、等待时间等),需要灵活拼接SQL。MyBatis的XML映射文件配合<where>、<if>标签,比JPA的Criteria API更直观易维护。
3. 关键功能实现细节
3.1 推荐算法工程化落地
算法部分采用分阶段执行策略以提高响应速度:
-
冷启动阶段(新用户):
java复制// 基于食堂档口热度排序 public List<Dish> coldStartRecommend(Long canteenId) { return dishMapper.selectPopularDishes(canteenId, 10); } -
常规推荐(有历史数据用户):
- 实时计算部分用Redis缓存用户最近偏好
- 离线部分用Spring Scheduler每日凌晨计算用户画像
- 混合权重公式:
总分 = 0.6*口味相似度 + 0.3*营养系数 + 0.1*价格系数
-
特殊场景处理:
xml复制<!-- MyBatis动态SQL示例:过敏原过滤 --> <select id="selectSafeDishes" parameterType="map" resultType="Dish"> SELECT * FROM dishes <where> <if test="allergens != null"> AND allergens NOT REGEXP #{allergens} </if> <if test="maxWaitTime != null"> AND estimated_wait_time <= #{maxWaitTime} </if> </where> </select>
3.2 高并发场景应对
在中午11:30-12:30的高峰期,系统需要应对约5000QPS的请求。我们通过以下措施保障稳定性:
-
多级缓存策略:
- 热点菜品使用Redis缓存(设置5分钟过期防雪崩)
- 用户个性化推荐结果用Guava Cache做本地缓存
- 静态菜单数据用Nginx代理缓存
-
数据库优化:
sql复制-- 为高频查询添加复合索引 ALTER TABLE dish_ratings ADD INDEX idx_user_dish (user_id, dish_id); -- 分表策略:按食堂ID水平分片 CREATE TABLE dishes_canteen1 LIKE dishes;
4. 安全防护实践
4.1 解决SQL注入漏洞
针对安全扫描发现的漏洞,我们进行了全方位加固:
-
MyBatis严格使用#{}:
java复制// 错误示范:使用${}导致拼接风险 @Select("SELECT * FROM users WHERE ${column} = #{value}") User findByColumn(@Param("column") String column, @Param("value") String value); // 正确做法:固定列名+参数化查询 @Select("SELECT * FROM users WHERE username = #{username}") User findByUsername(@Param("username") String username); -
输入验证层:
java复制// 使用Hibernate Validator注解 public class DishQueryDTO { @Pattern(regexp = "[a-zA-Z0-9,]+") private String allergens; @Min(0) @Max(999) private Integer maxPrice; } -
MyBatis插件审计:
开发了自定义插件拦截危险操作:java复制@Intercepts(@Signature(type= StatementHandler.class, method="prepare", args={Connection.class, Integer.class})) public class SqlInspectPlugin implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { String sql = ((StatementHandler)invocation.getTarget()) .getBoundSql().getSql(); if (sql.contains("sleep(") || sql.matches(".*(drop|alter)\\s+table.*")) { throw new IllegalSQLException("危险操作被拦截"); } return invocation.proceed(); } }
5. 部署实战指南
5.1 环境准备要点
-
MySQL配置优化:
ini复制# my.cnf关键参数 [mysqld] innodb_buffer_pool_size = 2G # 建议物理内存的50-70% max_connections = 500 wait_timeout = 600 -
Vue生产环境构建:
bash复制# 解决路由history模式404问题 npm install --save-dev connect-history-api-fallback # 带环境变量的构建命令 VUE_APP_API_BASE=https://api.yourschool.edu.cn npm run build
5.2 Jenkins自动化部署
我们的pipeline包含三个阶段:
groovy复制pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'mvn clean package -DskipTests'
sh 'npm install && npm run build'
}
}
stage('Dockerize') {
steps {
script {
docker.build("food-recommend:${env.BUILD_ID}")
}
}
}
stage('Deploy') {
steps {
sshPublisher(
publishers: [
sshPublisherDesc(
configName: 'prod-server',
transfers: [
sshTransfer(
execCommand: """
docker-compose down
docker-compose up -d
"""
)
]
)
]
)
}
}
}
}
注意:实际部署时需提前在目标服务器配置好docker-compose.yml文件,包含MySQL、Redis、Nginx等服务定义。
6. 踩坑与解决方案
6.1 MyBatis缓存导致的数据不一致
现象:开启事务后,连续查询相同SQL却得到不同结果。原因是MyBatis一级缓存(SqlSession级别)未及时更新。
解决方案:
java复制// 方法1:手动清除缓存
sqlSession.clearCache();
// 方法2:在Mapper接口添加注解
@Options(flushCache = Options.FlushCachePolicy.TRUE)
List<Dish> findRecommendedDishes(Long userId);
6.2 Vue生产环境跨域问题
开发环境配置的proxy在打包后失效。最终方案:
javascript复制// vue.config.js
module.exports = {
devServer: {
proxy: {
'/api': {
target: process.env.VUE_APP_API_BASE || 'http://localhost:8080',
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
}
}
},
// 静态资源路径处理
publicPath: process.env.NODE_ENV === 'production'
? '/food-recommend/'
: '/'
}
6.3 SpringBoot内存泄漏排查
通过Arthas工具发现是定时任务未正确关闭:
java复制// 错误示范:未管理线程池
@Scheduled(fixedRate = 3600000)
public void refreshRecommendations() {
// 大量对象创建
}
// 正确做法:配置线程池
@Bean(destroyMethod = "shutdown")
public Executor taskExecutor() {
return Executors.newScheduledThreadPool(5);
}
7. 项目演进方向
目前系统已在3所高校试点运行,后续计划:
-
智能推荐升级:
- 接入校园运动数据(步数、体测结果)
- 引入LSTM模型预测档口排队时间
-
运维监控增强:
yaml复制# SpringBoot Admin配置示例 spring: boot: admin: client: url: http://monitor.server:8080 instance: service-base-url: http://${spring.application.name} -
移动端体验优化:
- 开发微信小程序版本
- 支持语音查询("今天有什么推荐?")
这个项目从技术选型到最终落地,最大的体会是:高校场景的特殊性要求我们在技术先进性和运维简便性之间找到平衡点。比如放弃了一些更前沿但运维复杂的技术方案,转而选择校园IT部门熟悉的MySQL和物理机部署方式。在实际开发中,Vue的响应式特性与SpringBoot的自动配置机制大大提升了开发效率,而MyBatis的灵活性则完美应对了食堂业务的多变需求。
