1. 项目背景与核心需求
河南作为中原文化的发源地,拥有丰富多样的特色美食文化。从烩面到胡辣汤,从道口烧鸡到开封灌汤包,这些美食不仅承载着地方饮食文化的精髓,更是当地人民生活智慧的结晶。然而,目前市面上缺乏一个专门展示河南特色美食的数字化平台,导致许多外地游客甚至本地年轻一代对这些传统美食了解有限。
这个基于SpringBoot+Vue的河南特色美食分享系统,正是为了解决以下核心痛点:
- 文化传承需求:通过数字化手段保存和传播正在消失的传统美食制作工艺
- 旅游推广需求:为游客提供权威、全面的河南美食指南
- 社交分享需求:建立美食爱好者交流社区,分享探店体验和自制心得
系统采用前后端分离架构,后端使用SpringBoot提供RESTful API,前端采用Vue.js构建用户界面,数据库选用MySQL存储结构化数据。这种技术组合既保证了系统的稳定性和扩展性,又能提供流畅的用户体验。
2. 系统架构设计
2.1 技术栈选型分析
选择SpringBoot作为后端框架主要基于以下考虑:
- 内嵌Tomcat服务器,简化部署流程
- 自动配置特性大幅减少XML配置
- 丰富的Starter依赖,快速集成MyBatis、Redis等常用组件
- 完善的监控机制(Actuator)便于后期运维
前端选用Vue.js而非React或Angular,主要因为:
- 渐进式框架特性,适合中小型项目快速迭代
- 单文件组件(SFC)模式提升开发效率
- 丰富的生态系统(Vuex、Vue Router等)
- 更低的学习曲线,便于团队协作
数据库选择MySQL 8.0版本,因其:
- 完善的JSON支持,便于存储美食的多样化属性
- 窗口函数等高级特性方便实现复杂统计
- 开源免费且社区活跃
2.2 系统模块划分
系统主要分为以下核心模块:
code复制美食信息模块
├── 基础信息管理
├── 分类体系管理
├── 地理位置标注
└── 时令推荐系统
用户交互模块
├── 用户认证授权
├── 内容发布系统
├── 收藏点赞功能
└── 评论交流区
运营管理模块
├── 内容审核流程
├── 数据统计分析
└── 系统监控告警
2.3 数据库设计要点
核心表结构设计考虑:
- 美食主表(food)
sql复制CREATE TABLE `food` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL COMMENT '美食名称',
`cover_img` varchar(255) NOT NULL COMMENT '封面图URL',
`origin` varchar(100) DEFAULT NULL COMMENT '发源地',
`history` text COMMENT '历史渊源',
`ingredients` json DEFAULT NULL COMMENT '主要食材(JSON数组)',
`steps` json DEFAULT NULL COMMENT '制作步骤',
`video_url` varchar(255) DEFAULT NULL COMMENT '制作视频',
`seasonal` tinyint DEFAULT '0' COMMENT '是否时令美食',
`status` tinyint DEFAULT '1' COMMENT '状态(0下架1上架)',
PRIMARY KEY (`id`),
FULLTEXT KEY `ft_idx` (`name`,`origin`) WITH PARSER `ngram`
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
- 用户收藏关联表(user_favorite)
sql复制CREATE TABLE `user_favorite` (
`user_id` bigint NOT NULL,
`food_id` bigint NOT NULL,
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`user_id`,`food_id`),
KEY `idx_food` (`food_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
特别注意:使用JSON类型存储可变属性(如食材、步骤),既保持结构又避免过度范式化。同时为搜索字段添加全文索引,使用ngram分词器支持中文检索。
3. 关键功能实现
3.1 基于地理位置的智能推荐
结合腾讯地图API实现:
- 前端获取用户当前位置坐标
- 后端使用MySQL空间函数计算距离
java复制@Query(value = "SELECT f.*, ST_Distance_Sphere(point(:lng, :lat), point(lng, lat)) as distance " +
"FROM food f WHERE f.status = 1 ORDER BY distance ASC LIMIT 10",
nativeQuery = true)
List<Object[]> findNearbyFoods(@Param("lng") double lng, @Param("lat") double lat);
- 结果缓存到Redis,设置15分钟过期
java复制public List<FoodVO> getNearbyFoods(double lng, double lat) {
String key = "nearby:" + lng + ":" + lat;
ValueOperations<String, String> ops = redisTemplate.opsForValue();
if (redisTemplate.hasKey(key)) {
return JSON.parseArray(ops.get(key), FoodVO.class);
}
List<FoodVO> foods = foodRepository.findNearbyFoods(lng, lat)
.stream().map(this::convertToVO).collect(Collectors.toList());
ops.set(key, JSON.toJSONString(foods), 15, TimeUnit.MINUTES);
return foods;
}
3.2 多维度搜索功能
实现组合搜索策略:
- 基础搜索:MySQL全文索引
- 高级筛选:动态SQL构建
xml复制<select id="searchFoods" resultType="Food">
SELECT * FROM food
<where>
<if test="keyword != null">
AND MATCH(name,origin) AGAINST(#{keyword} IN BOOLEAN MODE)
</if>
<if test="seasonal != null">
AND seasonal = #{seasonal}
</if>
<if test="origin != null">
AND origin LIKE CONCAT('%',#{origin},'%')
</if>
</where>
ORDER BY
<choose>
<when test="sort == 'hot'">view_count DESC</when>
<otherwise>create_time DESC</otherwise>
</choose>
</select>
- 搜索建议:使用Elasticsearch实现即时提示
java复制public List<String> suggest(String prefix) {
NativeSearchQuery query = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.prefixQuery("name.pinyin", prefix))
.withSourceFilter(new FetchSourceFilter(new String[]{"name"}, null))
.withPageable(PageRequest.of(0, 5))
.build();
return elasticsearchRestTemplate.search(query, Food.class)
.getSearchHits()
.stream()
.map(hit -> hit.getContent().getName())
.collect(Collectors.toList());
}
3.3 用户内容发布审核流程
采用状态机模式设计审核流程:
java复制public enum ContentStatus {
DRAFT(0), // 草稿
PENDING(1), // 待审核
APPROVED(2), // 审核通过
REJECTED(3), // 审核驳回
DELETED(4); // 已删除
// 状态转换校验逻辑
public boolean canTransferTo(ContentStatus target) {
switch (this) {
case DRAFT: return target == PENDING;
case PENDING: return target == APPROVED || target == REJECTED;
case APPROVED: return target == DELETED;
default: return false;
}
}
}
结合Spring StateMachine实现:
java复制@Configuration
@EnableStateMachineFactory
public class StateMachineConfig extends EnumStateMachineConfigurerAdapter<ContentStatus, ContentEvent> {
@Override
public void configure(StateMachineStateConfigurer<ContentStatus, ContentEvent> states)
throws Exception {
states.withStates()
.initial(ContentStatus.DRAFT)
.states(EnumSet.allOf(ContentStatus.class));
}
@Override
public void configure(StateMachineTransitionConfigurer<ContentStatus, ContentEvent> transitions)
throws Exception {
transitions
.withExternal()
.source(ContentStatus.DRAFT)
.target(ContentStatus.PENDING)
.event(ContentEvent.SUBMIT)
.and()
.withExternal()
.source(ContentStatus.PENDING)
.target(ContentStatus.APPROVED)
.event(ContentEvent.APPROVE)
// 其他转换规则...
}
}
4. 开发中的典型问题与解决方案
4.1 跨域问题处理
前后端分离开发时遇到的典型跨域问题,通过以下方式解决:
- SpringBoot配置全局CORS
java复制@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:8080")
.allowedMethods("*")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
- Vue开发环境代理配置
javascript复制// vue.config.js
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
}
}
}
}
4.2 文件上传优化
美食图片上传的常见问题及解决方案:
- 前端压缩处理:使用compressorjs在客户端压缩图片
javascript复制new Compressor(file, {
quality: 0.6,
maxWidth: 1920,
success(result) {
const formData = new FormData();
formData.append('file', result, file.name);
// 上传逻辑...
}
});
- 后端分块上传:应对大文件上传
java复制@PostMapping("/upload")
public ResponseEntity<String> chunkUpload(
@RequestParam("file") MultipartFile file,
@RequestParam("chunkNumber") int chunkNumber,
@RequestParam("totalChunks") int totalChunks,
@RequestParam("identifier") String identifier) {
String tempDir = System.getProperty("java.io.tmpdir") + "/upload/" + identifier;
File dir = new File(tempDir);
if (!dir.exists()) dir.mkdirs();
File chunk = new File(dir, String.valueOf(chunkNumber));
try {
file.transferTo(chunk);
if (chunkNumber == totalChunks - 1) {
// 合并分块
mergeFiles(dir, identifier);
}
return ResponseEntity.ok("Chunk uploaded");
} catch (IOException e) {
return ResponseEntity.status(500).body("Upload failed");
}
}
4.3 性能优化实践
- Nginx动静分离配置
nginx复制server {
listen 80;
server_name example.com;
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://backend:8080;
proxy_set_header Host $host;
}
location /uploads {
alias /data/uploads;
expires 30d;
access_log off;
}
}
- SpringBoot缓存配置
java复制@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.disableCachingNullValues()
.serializeValuesWith(SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.withInitialCacheConfigurations(Map.of(
"foodDetail", config.entryTtl(Duration.ofHours(1)),
"nearbyFoods", config.entryTtl(Duration.ofMinutes(15))
))
.transactionAware()
.build();
}
}
5. 项目部署与运维
5.1 容器化部署方案
使用Docker Compose编排服务:
yaml复制version: '3.8'
services:
backend:
build: ./backend
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
- DB_URL=jdbc:mysql://db:3306/food?useSSL=false
depends_on:
- db
- redis
frontend:
build: ./frontend
ports:
- "80:80"
depends_on:
- backend
db:
image: mysql:8.0
environment:
- MYSQL_ROOT_PASSWORD=rootpass
- MYSQL_DATABASE=food
- MYSQL_USER=appuser
- MYSQL_PASSWORD=apppass
volumes:
- db_data:/var/lib/mysql
redis:
image: redis:6
ports:
- "6379:6379"
volumes:
- redis_data:/data
volumes:
db_data:
redis_data:
5.2 监控与日志收集
- SpringBoot Actuator配置
yaml复制management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
metrics:
enabled: true
metrics:
export:
prometheus:
enabled: true
- ELK日志收集方案
java复制@Configuration
public class LogbackConfig {
@Bean
public LogstashTcpSocketAppender logstashAppender() {
LogstashTcpSocketAppender appender = new LogstashTcpSocketAppender();
appender.setName("LOGSTASH");
appender.setContext(loggerContext());
appender.addDestination("logstash:5044");
appender.setEncoder(logstashEncoder());
return appender;
}
private LoggerContext loggerContext() {
return (LoggerContext) LoggerFactory.getILoggerFactory();
}
private LogstashEncoder logstashEncoder() {
LogstashEncoder encoder = new LogstashEncoder();
encoder.setCustomFields("{\"app\":\"food-share\",\"env\":\"prod\"}");
return encoder;
}
}
6. 项目扩展方向
在实际开发过程中,我们发现以下几个有价值的扩展点:
-
小程序端开发:基于uni-app框架开发微信小程序版本,利用微信的地理位置、扫码等功能增强用户体验
-
美食制作AR指导:集成AR SDK,用户扫描美食图片即可观看立体制作演示
-
智能推荐算法优化:收集用户行为数据,采用协同过滤算法实现个性化推荐
-
供应链对接:与本地农户合作,提供特色食材直购服务
-
多语言支持:为国际游客提供英文版界面,助力河南美食文化走向世界
在技术实现上,这些扩展都需要注意保持系统核心架构的稳定性。比如AR功能可以采用单独微服务实现,通过API网关与主系统集成;推荐算法初期可以使用简单的基于内容的推荐,后期再逐步引入机器学习模型。
