diff --git a/src/main/java/com/ycwl/basic/service/pc/helper/FaceMatchDedupService.java b/src/main/java/com/ycwl/basic/service/pc/helper/FaceMatchDedupService.java new file mode 100644 index 00000000..4f53e654 --- /dev/null +++ b/src/main/java/com/ycwl/basic/service/pc/helper/FaceMatchDedupService.java @@ -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 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(); + } +} diff --git a/src/main/java/com/ycwl/basic/service/pc/impl/FaceServiceImpl.java b/src/main/java/com/ycwl/basic/service/pc/impl/FaceServiceImpl.java index de16622f..d8ebdd5c 100644 --- a/src/main/java/com/ycwl/basic/service/pc/impl/FaceServiceImpl.java +++ b/src/main/java/com/ycwl/basic/service/pc/impl/FaceServiceImpl.java @@ -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