feat(face): 添加人脸识别防重复调用机制

- 引入 FaceMatchDedupService 用于防止短时间内重复调用
- 在匹配前检查是否应跳过本次调用
- 匹配完成后标记已处理,避免重复执行
- 增强系统稳定性与性能,减少无效计算
This commit is contained in:
2025-11-27 13:58:26 +08:00
parent 67932c374b
commit cbc0584706
2 changed files with 147 additions and 1 deletions

View File

@@ -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();
}
}

View File

@@ -65,6 +65,7 @@ import com.ycwl.basic.service.mobile.GoodsService;
import com.ycwl.basic.service.pc.FaceService;
import com.ycwl.basic.service.pc.ScenicService;
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.SearchResultMerger;
import com.ycwl.basic.service.pc.helper.ScenicConfigFacade;
@@ -175,6 +176,10 @@ public class FaceServiceImpl implements FaceService {
@Autowired
private FaceMatchingOrchestrator faceMatchingOrchestrator;
// 防重复服务
@Autowired
private FaceMatchDedupService faceMatchDedupService;
// 第二阶段的处理器
@Autowired
private SourceRelationProcessor sourceRelationProcessor;
@@ -326,7 +331,27 @@ public class FaceServiceImpl implements FaceService {
@Override
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