You've already forked FrameTour-BE
feat(face): 添加人脸识别防重复调用机制
- 引入 FaceMatchDedupService 用于防止短时间内重复调用 - 在匹配前检查是否应跳过本次调用 - 匹配完成后标记已处理,避免重复执行 - 增强系统稳定性与性能,减少无效计算
This commit is contained in:
@@ -0,0 +1,121 @@
|
|||||||
|
package com.ycwl.basic.service.pc.helper;
|
||||||
|
|
||||||
|
import com.ycwl.basic.util.TtlCacheMap;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸匹配防重复服务
|
||||||
|
* 使用本地缓存实现2秒内的防重复调用机制
|
||||||
|
*
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-27
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class FaceMatchDedupService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 防重复TTL时间(毫秒)
|
||||||
|
*/
|
||||||
|
private static final long DEFAULT_TTL_MILLIS = 2000L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存Key前缀
|
||||||
|
*/
|
||||||
|
private static final String KEY_PREFIX = "face:match:dedup:";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 防重复缓存
|
||||||
|
* Key: face:match:dedup:{scenicId}:{userId}
|
||||||
|
* Value: 时间戳
|
||||||
|
*/
|
||||||
|
private final TtlCacheMap<String, Long> dedupCache;
|
||||||
|
|
||||||
|
public FaceMatchDedupService() {
|
||||||
|
this.dedupCache = new TtlCacheMap<>(DEFAULT_TTL_MILLIS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否应该跳过本次匹配(防重复)
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param scenicId 景区ID
|
||||||
|
* @return true-应该跳过,false-可以执行
|
||||||
|
*/
|
||||||
|
public boolean shouldSkip(Long userId, Long scenicId) {
|
||||||
|
if (userId == null || scenicId == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String key = buildKey(userId, scenicId);
|
||||||
|
Long timestamp = dedupCache.get(key);
|
||||||
|
|
||||||
|
if (timestamp != null) {
|
||||||
|
long elapsedMs = System.currentTimeMillis() - timestamp;
|
||||||
|
log.info("防重复检查:检测到{}秒内的重复调用(已过{}ms),跳过匹配。userId={}, scenicId={}, 上次调用时间={}",
|
||||||
|
DEFAULT_TTL_MILLIS / 1000.0, elapsedMs, userId, scenicId, new Date(timestamp));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记本次匹配已执行(用于防重复)
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param scenicId 景区ID
|
||||||
|
*/
|
||||||
|
public void markMatched(Long userId, Long scenicId) {
|
||||||
|
if (userId == null || scenicId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String key = buildKey(userId, scenicId);
|
||||||
|
long timestamp = System.currentTimeMillis();
|
||||||
|
dedupCache.put(key, timestamp, DEFAULT_TTL_MILLIS);
|
||||||
|
|
||||||
|
log.debug("防重复标记:记录匹配调用。userId={}, scenicId={}, TTL={}ms, timestamp={}",
|
||||||
|
userId, scenicId, DEFAULT_TTL_MILLIS, timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除指定用户的防重标记(用于测试或特殊场景)
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param scenicId 景区ID
|
||||||
|
*/
|
||||||
|
public void clearMark(Long userId, Long scenicId) {
|
||||||
|
if (userId == null || scenicId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String key = buildKey(userId, scenicId);
|
||||||
|
dedupCache.remove(key);
|
||||||
|
log.info("防重复标记清除:userId={}, scenicId={}", userId, scenicId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建缓存Key
|
||||||
|
* 格式:face:match:dedup:{scenicId}:{userId}
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param scenicId 景区ID
|
||||||
|
* @return 缓存Key
|
||||||
|
*/
|
||||||
|
private String buildKey(Long userId, Long scenicId) {
|
||||||
|
return KEY_PREFIX + scenicId + ":" + userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取缓存统计信息(用于监控)
|
||||||
|
*
|
||||||
|
* @return 统计信息字符串
|
||||||
|
*/
|
||||||
|
public String getStats() {
|
||||||
|
return dedupCache.getStats();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -65,6 +65,7 @@ import com.ycwl.basic.service.mobile.GoodsService;
|
|||||||
import com.ycwl.basic.service.pc.FaceService;
|
import com.ycwl.basic.service.pc.FaceService;
|
||||||
import com.ycwl.basic.service.pc.ScenicService;
|
import com.ycwl.basic.service.pc.ScenicService;
|
||||||
import com.ycwl.basic.constant.SourceType;
|
import com.ycwl.basic.constant.SourceType;
|
||||||
|
import com.ycwl.basic.service.pc.helper.FaceMatchDedupService;
|
||||||
import com.ycwl.basic.service.pc.helper.FaceMetricsRecorder;
|
import com.ycwl.basic.service.pc.helper.FaceMetricsRecorder;
|
||||||
import com.ycwl.basic.service.pc.helper.SearchResultMerger;
|
import com.ycwl.basic.service.pc.helper.SearchResultMerger;
|
||||||
import com.ycwl.basic.service.pc.helper.ScenicConfigFacade;
|
import com.ycwl.basic.service.pc.helper.ScenicConfigFacade;
|
||||||
@@ -175,6 +176,10 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private FaceMatchingOrchestrator faceMatchingOrchestrator;
|
private FaceMatchingOrchestrator faceMatchingOrchestrator;
|
||||||
|
|
||||||
|
// 防重复服务
|
||||||
|
@Autowired
|
||||||
|
private FaceMatchDedupService faceMatchDedupService;
|
||||||
|
|
||||||
// 第二阶段的处理器
|
// 第二阶段的处理器
|
||||||
@Autowired
|
@Autowired
|
||||||
private SourceRelationProcessor sourceRelationProcessor;
|
private SourceRelationProcessor sourceRelationProcessor;
|
||||||
@@ -326,7 +331,27 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SearchFaceRespVo matchFaceId(Long faceId) {
|
public SearchFaceRespVo matchFaceId(Long faceId) {
|
||||||
return faceMatchingOrchestrator.orchestrateMatching(faceId, false);
|
// 获取人脸信息用于防重检查
|
||||||
|
FaceEntity face = faceRepository.getFace(faceId);
|
||||||
|
if (face == null) {
|
||||||
|
log.warn("人脸不存在,无法执行匹配,faceId: {}", faceId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 防重复检查:如果2秒内已调用过,则跳过
|
||||||
|
if (faceMatchDedupService.shouldSkip(face.getMemberId(), face.getScenicId())) {
|
||||||
|
log.info("防重复:跳过人脸匹配。faceId={}, userId={}, scenicId={}",
|
||||||
|
faceId, face.getMemberId(), face.getScenicId());
|
||||||
|
return null; // 静默忽略,返回null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 正常执行人脸匹配
|
||||||
|
SearchFaceRespVo result = faceMatchingOrchestrator.orchestrateMatching(faceId, false);
|
||||||
|
|
||||||
|
// 执行完成后标记,防止2秒内重复调用
|
||||||
|
faceMatchDedupService.markMatched(face.getMemberId(), face.getScenicId());
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
Reference in New Issue
Block a user