1. 项目概述与技术选型
这个前后端分离的美食信息推荐系统采用了当前企业级开发中最主流的Java+Vue技术栈组合。后端基于SpringBoot 2.7.x构建,前端使用Vue3+TypeScript实现,数据持久层采用MyBatis-Plus增强框架,数据库选择了MySQL 8.0。整套架构设计充分考虑了高并发场景下的性能表现和扩展性需求。
技术选型背后的考量值得深入探讨。SpringBoot作为后端框架,其自动配置特性大幅减少了XML配置工作量,内嵌Tomcat容器简化了部署流程,而Starter机制则让第三方组件集成变得异常简单。Vue3作为前端框架,其Composition API相比Options API在逻辑复用和类型推导方面有明显优势,特别适合复杂交互场景。MyBatis-Plus在原生MyBatis基础上增强了CRUD操作,内置的分页插件和代码生成器能显著提升开发效率。
提示:实际项目中建议锁定具体版本号,如SpringBoot 2.7.15避免踩到新版本的兼容性问题
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 系统架构设计与核心模块
2.1 前后端分离架构实现
系统采用典型的前后端分离架构,通过RESTful API进行数据交互。前端部署在Nginx服务器,后端采用SpringBoot内置容器,两者通过JSON格式进行通信。跨域问题通过@CrossOrigin注解配合Nginx反向代理解决,生产环境建议启用HTTPS加密传输。
后端按功能划分为以下几个核心模块:
- 用户中心:处理注册登录、权限认证(JWT)
- 美食数据:菜品信息的CRUD管理
- 推荐引擎:基于用户行为的推荐算法
- 评价系统:用户反馈的收集与分析
- 管理员后台:数据统计与系统配置
2.2 数据库设计要点
MySQL数据库设计遵循第三范式,主要表结构包括:
sql复制CREATE TABLE `dish_info` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(50) NOT NULL COMMENT '菜品名称',
`category_id` int NOT NULL COMMENT '分类ID',
`price` decimal(10,2) DEFAULT NULL COMMENT '价格',
`description` text COMMENT '描述',
`cover_image` varchar(255) DEFAULT NULL COMMENT '封面图',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_category` (`category_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
索引设计上,除了主键索引外,对category_id等高频查询字段建立了普通索引,对复合查询场景使用了组合索引。考虑到美食数据的文本搜索需求,额外增加了Elasticsearch作为全文检索引擎。
3. 核心功能实现细节
3.1 推荐算法实现
系统采用混合推荐策略:
- 基于内容的推荐:通过菜品标签计算余弦相似度
- 协同过滤推荐:使用用户-菜品评分矩阵
- 热门推荐:近期浏览/收藏TopN
- 冷启动策略:新用户默认推荐地域特色美食
算法核心代码片段:
java复制public List<DishVO> recommend(Long userId) {
// 获取用户特征向量
double[] userVector = userFeatureService.getUserVector(userId);
// 混合推荐结果
List<DishVO> contentBased = contentBasedRecommend(userVector);
List<DishVO> cf = cfRecommend(userId);
List<DishVO> hot = hotRecommend();
// 结果融合与去重
return mergeRecommendations(contentBased, cf, hot);
}
3.2 前后端交互关键点
Axios封装示例(前端):
typescript复制const service = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API,
timeout: 10000,
headers: { 'Content-Type': 'application/json' }
})
// 请求拦截器
service.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) {
config.headers['Authorization'] = `Bearer ${token}`
}
return config
})
SpringBoot统一响应封装(后端):
java复制@RestControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType,
MediaType selectedContentType, Class selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
if (body instanceof Result) {
return body;
}
return Result.success(body);
}
}
4. 开发环境搭建与调试技巧
4.1 本地开发环境配置
后端开发环境:
- JDK17(注意与Maven编译器版本一致)
- IntelliJ IDEA安装Lombok插件
- 配置SpringBoot DevTools实现热部署
- 使用MyBatis Log Plugin打印SQL语句
前端开发环境:
- Node.js 16+
- VSCode安装Volar插件
- 配置Vite代理解决跨域
javascript复制server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
4.2 常见问题排查指南
问题1:Vue3项目在Edge浏览器中最小化按钮异常
解决方案:检查CSS样式是否被浏览器默认样式覆盖,建议重置基础样式:
css复制button {
-webkit-appearance: none;
margin: 0;
padding: 0;
border: none;
background: none;
}
问题2:MyBatis动态表名实现
xml复制<insert id="insertDynamic">
INSERT INTO ${tableName}
<foreach collection="fields" item="field" open="(" separator="," close=")">
${field.column}
</foreach>
VALUES
<foreach collection="fields" item="field" open="(" separator="," close=")">
#{field.value}
</foreach>
</insert>
问题3:SpringBoot处理PDF XSS攻击
java复制@Bean
public FilterRegistrationBean<XssFilter> xssFilter() {
FilterRegistrationBean<XssFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new XssFilter());
registration.addUrlPatterns("/*");
return registration;
}
5. 性能优化与安全实践
5.1 数据库优化策略
- 查询优化:
- 使用EXPLAIN分析慢查询
- 避免SELECT * 只查询必要字段
- 大数据量表采用分库分表策略
- 缓存方案:
java复制@Cacheable(value = "dish", key = "#id")
public Dish getById(Long id) {
return dishMapper.selectById(id);
}
@CacheEvict(value = "dish", key = "#dish.id")
public void updateDish(Dish dish) {
dishMapper.updateById(dish);
}
- 连接池配置:
yaml复制spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
5.2 安全防护措施
- 接口安全:
- JWT过期时间设置为2小时
- 敏感接口增加@PreAuthorize注解
- 密码加密存储使用BCryptPasswordEncoder
- 日志审计:
java复制@Aspect
@Component
public class LogAspect {
@AfterReturning(pointcut = "@annotation(operLog)", returning = "result")
public void afterReturning(JoinPoint joinPoint, OperLog operLog, Object result) {
// 记录操作日志
}
}
- XSS防护:
- 前端使用DOMPurify过滤HTML
- 后端统一处理特殊字符转义
- 文件上传限制MIME类型
6. 部署方案与监控体系
6.1 生产环境部署
后端部署方案:
- 使用Docker打包SpringBoot应用
dockerfile复制FROM openjdk:17-jdk
COPY target/*.jar app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
- Nginx配置示例:
nginx复制server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
}
}
前端部署注意事项:
- 构建时开启压缩:
bash复制vite build --mode production
- 配置Gzip压缩:
nginx复制gzip on;
gzip_types text/plain application/xml text/css application/javascript;
6.2 系统监控方案
- SpringBoot Actuator健康检查:
yaml复制management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
- Prometheus监控指标采集:
java复制@Bean
public MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() {
return registry -> registry.config().commonTags("application", "food-recommend");
}
- ELK日志收集方案:
- Logstash配置Grok模式解析日志
- Kibana创建可视化仪表盘
- 设置异常日志告警规则
7. 项目扩展与演进方向
7.1 功能扩展建议
- 社交化功能:
- 用户关注关系
- 菜品收藏分享
- 好友口味对比
- 智能推荐增强:
- 实时点击流分析
- 季节时令推荐
- 健康饮食建议
- 运营功能:
- 优惠券系统
- 限时秒杀活动
- 数据看板
7.2 技术演进路线
- 架构升级:
- 微服务化改造
- 引入Service Mesh
- 多云部署方案
- 新技术集成:
- 图像识别自动标注菜品
- 语音搜索功能
- AR菜单展示
- 性能优化:
- 引入Redis集群
- 尝试GraalVM原生镜像
- 实现Serverless架构
在实际开发中,我们发现TypeScript的类型系统能有效减少前后端接口对接的错误。通过定义统一的DTO类型,前后端开发可以并行进行。例如定义推荐结果的类型约束:
typescript复制interface DishVO {
id: number
name: string
price: number
rating?: number
imageUrl: string
tags: string[]
}
另一个实用技巧是使用MyBatis-Plus的自动填充功能处理公共字段:
java复制@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", Date.class, new Date());
}
}
