作为一个长期深耕Java技术栈的开发者,最近完成了一个颇具地方特色的美食分享平台项目。这个基于SpringBoot+MyBatis的河南美食推荐系统,不仅实现了传统美食文化的数字化展示,更通过技术手段解决了美食爱好者"找不到正宗店铺"、"不了解地方特色"的痛点。系统上线三个月内,已收录超过200家河南本土美食店铺信息,用户活跃度保持在日均300+人次。
从技术架构来看,项目采用了当前企业级开发的主流技术组合:
提示:选择MyBatis-Plus而非原生MyBatis,主要考虑到其对单表CRUD的零SQL编写支持,这在美食数据的基础维护场景中能提升30%以上的开发效率。
作为系统的核心模块,采用三层架构设计:
code复制Controller层(Web)
↓
Service层(业务逻辑)
↓
Mapper层(数据持久化)
特色功能实现要点:
xml复制<select id="selectByCondition" resultMap="BaseResultMap">
SELECT * FROM food
<where>
<if test="type != null">AND type = #{type}</if>
<if test="region != null">AND region LIKE CONCAT('%',#{region},'%')</if>
<if test="priceRange != null">
AND price BETWEEN #{priceRange[0]} AND #{priceRange[1]}
</if>
</where>
ORDER BY create_time DESC
</select>
java复制@Query(nativeQuery = true,
value = "SELECT *, ST_Distance_Sphere(point(longitude, latitude), " +
"point(:lng, :lat)) AS distance FROM food " +
"WHERE ST_Distance_Sphere(point(longitude, latitude), " +
"point(:lng, :lat)) < :radius ORDER BY distance")
List<Food> findNearby(@Param("lng") double lng,
@Param("lat") double lat,
@Param("radius") int radius);
采用RBAC(基于角色的访问控制)模型,核心表关系:
code复制user - user_role - role - role_permission - permission
关键实现技巧:
java复制public boolean addFavorite(Long userId, Long foodId) {
String lockKey = "fav:" + userId + ":" + foodId;
// 使用Redis分布式锁防止重复提交
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);
if (locked != null && locked) {
try {
// 业务处理...
} finally {
redisTemplate.delete(lockKey);
}
}
return false;
}
java复制// 简化的推荐逻辑
public List<Food> recommendFoods(Long userId) {
// 1. 获取用户历史行为
List<UserBehavior> behaviors = behaviorMapper.selectByUser(userId);
// 2. 提取特征向量(此处简化处理)
Map<String, Integer> tagWeights = behaviors.stream()
.flatMap(b -> b.getTags().stream())
.collect(Collectors.groupingBy(Function.identity(),
Collectors.summingInt(e -> 1)));
// 3. 匹配相似美食
return foodMapper.selectSimilar(tagWeights.keySet())
.stream()
.sorted(Comparator.comparingInt(food ->
-food.getTags().stream()
.filter(tagWeights::containsKey)
.mapToInt(tagWeights::get)
.sum()))
.limit(10)
.collect(Collectors.toList());
}
缓存策略设计:
yaml复制spring:
redis:
cluster:
nodes: 192.168.1.101:6379,192.168.1.102:6379
max-redirects: 3
lettuce:
pool:
max-active: 8
max-wait: -1ms
max-idle: 8
min-idle: 0
数据库分库分表:
java复制// 分片策略配置
@Bean
public ShardingRuleConfiguration shardingRule() {
ShardingRuleConfiguration rule = new ShardingRuleConfiguration();
rule.getTableRuleConfigs().add(
new TableRuleConfiguration("food",
"ds${0..1}.food_${0..15}"));
rule.setDefaultDatabaseShardingStrategy(
new StandardShardingStrategyConfiguration(
"region_code",
new PreciseShardingAlgorithm() {
@Override
public String doSharding(Collection availableTargetNames,
PreciseShardingValue shardingValue) {
// 分片逻辑...
}
}));
return rule;
}
防XSS攻击:
java复制String safeHtml = Jsoup.clean(rawInput,
Whitelist.basicWithImages());
java复制http.headers()
.contentSecurityPolicy("default-src 'self'");
数据脱敏处理:
java复制@DataMasking(maskFunc = MaskType.PHONE)
private String phone;
public class DataMaskingSerializer extends JsonSerializer<String> {
@Override
public void serialize(String value, JsonGenerator gen,
SerializerProvider provider) {
// 脱敏逻辑...
}
}
采用Docker Compose编排方案:
docker-compose复制version: '3'
services:
app:
image: openjdk:11-jre
ports:
- "8080:8080"
volumes:
- ./app.jar:/app.jar
command: java -jar /app.jar
depends_on:
- redis
- mysql
redis:
image: redis:6
ports:
- "6379:6379"
volumes:
- redis_data:/data
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: rootpass
volumes:
- mysql_data:/var/lib/mysql
volumes:
redis_data:
mysql_data:
Prometheus + Grafana监控:
java复制@Bean
MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() {
return registry -> registry.config()
.commonTags("application", "henan-food");
}
日志收集方案:
xml复制<!-- logback-spring.xml -->
<appender name="LOGSTASH" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<destination>logstash:5044</destination>
<encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
</appender>
现象:频繁请求不存在的foodId导致数据库压力剧增
解决方案:
java复制@PostConstruct
public void initBloomFilter() {
List<Long> allIds = foodMapper.selectAllIds();
bloomFilter.putAll(allIds);
}
public Food getById(Long id) {
if (!bloomFilter.mightContain(id)) {
return null;
}
// 正常查询逻辑...
}
java复制String cacheKey = "food:" + id;
Food food = redisTemplate.opsForValue().get(cacheKey);
if (food == null) {
food = foodMapper.selectById(id);
redisTemplate.opsForValue().set(cacheKey,
food != null ? food : new NullValue(),
5, TimeUnit.MINUTES);
}
场景:秒杀优惠券时出现超发
最终方案:Redisson分布式锁
java复制RLock lock = redissonClient.getLock("coupon:" + couponId);
try {
if (lock.tryLock(1, 10, TimeUnit.SECONDS)) {
// 业务处理
}
} finally {
lock.unlock();
}
智能化升级:
生态扩展:
技术架构演进:
在实际开发过程中,最大的收获是对领域驱动设计(DDD)的实践理解。例如将"美食"这个核心领域划分为:
这种清晰的领域划分使得后续功能扩展更加有序。建议在类似项目中,前期花足够时间进行领域建模,这比急于编码更能保证项目的长期可维护性。