diff --git a/src/main/java/com/ycwl/basic/service/pc/helper/FaceMetricsRecorder.java b/src/main/java/com/ycwl/basic/service/pc/helper/FaceMetricsRecorder.java new file mode 100644 index 00000000..83f84959 --- /dev/null +++ b/src/main/java/com/ycwl/basic/service/pc/helper/FaceMetricsRecorder.java @@ -0,0 +1,169 @@ +package com.ycwl.basic.service.pc.helper; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +import static com.ycwl.basic.constant.FaceConstant.*; + +/** + * 人脸识别指标记录器 + * 负责记录人脸识别相关的计数指标到Redis + * + * @author longbinbin + * @date 2025-01-31 + */ +@Slf4j +@Component +public class FaceMetricsRecorder { + + @Autowired + private RedisTemplate redisTemplate; + + /** + * 记录人脸识别次数到Redis + * 设置2天过期时间 + * + * @param faceId 人脸ID + */ + public void recordRecognitionCount(Long faceId) { + if (faceId == null) { + return; + } + + try { + String redisKey = FACE_RECOGNITION_COUNT_PFX + faceId; + + // 使用Redis原子操作INCR增加计数 + Long count = redisTemplate.opsForValue().increment(redisKey); + + // 设置2天过期时间(48小时) + redisTemplate.expire(redisKey, 2, TimeUnit.DAYS); + + log.debug("人脸识别计数更新:faceId={}, count={}", faceId, count); + + } catch (Exception e) { + // 计数失败不应影响主要业务逻辑,只记录错误日志 + log.error("记录人脸识别次数失败:faceId={}", faceId, e); + } + } + + /** + * 记录自定义人脸匹配次数到Redis + * 设置2天过期时间 + * + * @param faceId 人脸ID + */ + public void recordCustomMatchCount(Long faceId) { + if (faceId == null) { + return; + } + + try { + String redisKey = FACE_CUSTOM_MATCH_COUNT_PFX + faceId; + + Long count = redisTemplate.opsForValue().increment(redisKey); + redisTemplate.expire(redisKey, 2, TimeUnit.DAYS); + + log.debug("自定义人脸匹配计数更新:faceId={}, count={}", faceId, count); + } catch (Exception e) { + log.error("记录自定义人脸匹配次数失败:faceId={}", faceId, e); + } + } + + /** + * 记录低阈值检测的人脸ID到Redis + * 设置2天过期时间 + * + * @param faceId 人脸ID + */ + public void recordLowThreshold(Long faceId) { + if (faceId == null) { + return; + } + + try { + String redisKey = FACE_LOW_THRESHOLD_PFX + faceId; + + // 设置标记,表示该人脸ID触发了低阈值检测 + redisTemplate.opsForValue().set(redisKey, "1", 2, TimeUnit.DAYS); + + log.debug("记录低阈值检测人脸:faceId={}", faceId); + + } catch (Exception e) { + // 记录失败不应影响主要业务逻辑,只记录错误日志 + log.error("记录低阈值检测人脸失败:faceId={}", faceId, e); + } + } + + /** + * 获取人脸识别次数 + * + * @param faceId 人脸ID + * @return 识别次数 + */ + public long getRecognitionCount(Long faceId) { + if (faceId == null) { + return 0L; + } + + try { + String countKey = FACE_RECOGNITION_COUNT_PFX + faceId; + String countStr = redisTemplate.opsForValue().get(countKey); + if (countStr != null) { + return Long.parseLong(countStr); + } + } catch (Exception e) { + log.warn("获取识别次数失败:faceId={}", faceId, e); + } + + return 0L; + } + + /** + * 获取自定义匹配次数 + * + * @param faceId 人脸ID + * @return 自定义匹配次数 + */ + public long getCustomMatchCount(Long faceId) { + if (faceId == null) { + return 0L; + } + + try { + String customMatchKey = FACE_CUSTOM_MATCH_COUNT_PFX + faceId; + String customMatchCountStr = redisTemplate.opsForValue().get(customMatchKey); + if (customMatchCountStr != null) { + return Long.parseLong(customMatchCountStr); + } + } catch (Exception e) { + log.warn("获取自定义匹配次数失败:faceId={}", faceId, e); + } + + return 0L; + } + + /** + * 检查是否触发过低阈值检测 + * + * @param faceId 人脸ID + * @return 是否触发过低阈值检测 + */ + public boolean hasLowThreshold(Long faceId) { + if (faceId == null) { + return false; + } + + try { + String lowThresholdKey = FACE_LOW_THRESHOLD_PFX + faceId; + return Boolean.TRUE.equals(redisTemplate.hasKey(lowThresholdKey)); + } catch (Exception e) { + log.warn("检查低阈值状态失败:faceId={}", faceId, e); + return false; + } + } +} diff --git a/src/main/java/com/ycwl/basic/service/pc/helper/SearchResultMerger.java b/src/main/java/com/ycwl/basic/service/pc/helper/SearchResultMerger.java new file mode 100644 index 00000000..7741712f --- /dev/null +++ b/src/main/java/com/ycwl/basic/service/pc/helper/SearchResultMerger.java @@ -0,0 +1,154 @@ +package com.ycwl.basic.service.pc.helper; + +import com.ycwl.basic.model.task.resp.SearchFaceRespVo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 搜索结果合并器 + * 负责合并多个人脸搜索结果 + * + * @author longbinbin + * @date 2025-01-31 + */ +@Slf4j +@Component +public class SearchResultMerger { + + /** + * 合并多个搜索结果(默认使用并集模式) + * + * @param searchResults 搜索结果列表 + * @return 合并后的结果 + */ + public SearchFaceRespVo merge(List searchResults) { + return merge(searchResults, 0); + } + + /** + * 合并多个搜索结果 + * + * @param searchResults 搜索结果列表 + * @param mergeMode 合并模式:0-并集,1-交集 + * @return 合并后的结果 + */ + public SearchFaceRespVo merge(List searchResults, Integer mergeMode) { + SearchFaceRespVo mergedResult = new SearchFaceRespVo(); + + if (searchResults == null || searchResults.isEmpty()) { + return mergedResult; + } + + List allSearchJsons = new ArrayList<>(); + float maxScore = 0f; + float maxFirstMatchRate = 0f; + boolean hasLowThreshold = false; + + // 收集基础信息 + for (SearchFaceRespVo result : searchResults) { + if (result.getSearchResultJson() != null) { + allSearchJsons.add(result.getSearchResultJson()); + } + if (result.getScore() > maxScore) { + maxScore = result.getScore(); + } + if (result.getFirstMatchRate() > maxFirstMatchRate) { + maxFirstMatchRate = result.getFirstMatchRate(); + } + if (result.isLowThreshold()) { + hasLowThreshold = true; + } + } + + // 根据合并模式处理样本ID + List finalSampleIds; + if (Integer.valueOf(1).equals(mergeMode)) { + // 模式1:交集 - 只保留所有搜索结果中都出现的样本ID + finalSampleIds = computeIntersection(searchResults); + log.debug("使用交集模式合并搜索结果,交集样本数: {}", finalSampleIds.size()); + } else { + // 模式0:并集(默认) - 收集所有样本ID并去重 + Set allSampleIds = new LinkedHashSet<>(); + for (SearchFaceRespVo result : searchResults) { + if (result.getSampleListIds() != null) { + allSampleIds.addAll(result.getSampleListIds()); + } + } + finalSampleIds = new ArrayList<>(allSampleIds); + log.debug("使用并集模式合并搜索结果,并集样本数: {}", finalSampleIds.size()); + } + + mergedResult.setSampleListIds(finalSampleIds); + mergedResult.setSearchResultJson(String.join("|", allSearchJsons)); + mergedResult.setScore(maxScore); + mergedResult.setFirstMatchRate(maxFirstMatchRate); + mergedResult.setLowThreshold(hasLowThreshold); + + log.debug("合并搜索结果完成,模式={}, 最终样本数: {}", mergeMode, finalSampleIds.size()); + + return mergedResult; + } + + /** + * 计算多个搜索结果的交集 + * 返回在所有搜索结果中都出现的样本ID + * + * @param searchResults 搜索结果列表 + * @return 交集样本ID列表 + */ + public List computeIntersection(List searchResults) { + if (searchResults == null || searchResults.isEmpty()) { + return new ArrayList<>(); + } + + // 过滤掉空结果 + List> validSampleLists = searchResults.stream() + .filter(result -> result.getSampleListIds() != null && !result.getSampleListIds().isEmpty()) + .map(SearchFaceRespVo::getSampleListIds) + .toList(); + + if (validSampleLists.isEmpty()) { + return new ArrayList<>(); + } + + // 如果只有一个有效结果,直接返回 + if (validSampleLists.size() == 1) { + return new ArrayList<>(validSampleLists.getFirst()); + } + + // 计算交集:从第一个列表开始,保留在所有其他列表中都出现的ID + Set intersection = new LinkedHashSet<>(validSampleLists.getFirst()); + + for (int i = 1; i < validSampleLists.size(); i++) { + intersection.retainAll(validSampleLists.get(i)); + } + + return new ArrayList<>(intersection); + } + + /** + * 创建直接结果(模式2:不搜索,直接使用用户选择的faceSampleIds) + * + * @param faceSampleIds 用户选择的人脸样本ID列表 + * @return 搜索结果对象 + */ + public SearchFaceRespVo createDirectResult(List faceSampleIds) { + SearchFaceRespVo result = new SearchFaceRespVo(); + + // 直接使用用户选择的faceSampleIds作为结果 + result.setSampleListIds(new ArrayList<>(faceSampleIds)); + + // 设置默认值 + result.setScore(1.0f); + result.setFirstMatchRate(1.0f); + result.setLowThreshold(false); + result.setSearchResultJson(""); + + log.debug("创建直接结果,样本数: {}", faceSampleIds.size()); + + return result; + } +} 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 ad3e07ef..eca47006 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 @@ -58,6 +58,11 @@ import com.ycwl.basic.repository.VideoTaskRepository; 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.service.pc.helper.FaceMetricsRecorder; +import com.ycwl.basic.service.pc.helper.SearchResultMerger; +import com.ycwl.basic.service.pc.strategy.RematchContext; +import com.ycwl.basic.service.pc.strategy.RematchModeStrategy; +import com.ycwl.basic.service.pc.strategy.RematchStrategyFactory; import com.ycwl.basic.service.task.TaskFaceService; import com.ycwl.basic.service.task.TaskService; import com.ycwl.basic.storage.StorageFactory; @@ -153,6 +158,14 @@ public class FaceServiceImpl implements FaceService { @Autowired private TemplateRepository templateRepository; + // 新增的辅助类 + @Autowired + private FaceMetricsRecorder metricsRecorder; + @Autowired + private SearchResultMerger resultMerger; + @Autowired + private RematchStrategyFactory rematchStrategyFactory; + @Override public ApiResponse> pageQuery(FaceReqQuery faceReqQuery) { PageHelper.startPage(faceReqQuery.getPageNum(),faceReqQuery.getPageSize()); @@ -296,9 +309,9 @@ public class FaceServiceImpl implements FaceService { return null; } log.debug("开始人脸匹配:faceId={}, isNew={}", faceId, isNew); - + // 记录识别次数到Redis,设置2天过期时间 - recordFaceRecognitionCount(faceId); + metricsRecorder.recordRecognitionCount(faceId); try { @@ -387,7 +400,7 @@ public class FaceServiceImpl implements FaceService { // 检查低阈值检测结果,如果为true则记录该人脸ID到Redis if (scenicDbSearchResult != null && scenicDbSearchResult.isLowThreshold()) { - recordLowThresholdFace(faceId); + metricsRecorder.recordLowThreshold(faceId); log.debug("触发低阈值检测,记录faceId: {}", faceId); } } @@ -947,23 +960,13 @@ public class FaceServiceImpl implements FaceService { statusResp.setStep1Status(true); statusResp.setFaceUrl(face.getFaceUrl()); - + // 查询识别次数 - String countKey = FACE_RECOGNITION_COUNT_PFX + faceId; - String countStr = redisTemplate.opsForValue().get(countKey); - long recognitionCount = 0L; - if (countStr != null) { - try { - recognitionCount = Long.parseLong(countStr); - } catch (NumberFormatException e) { - log.warn("识别次数解析失败,faceId={}, count={}", faceId, countStr); - } - } + long recognitionCount = metricsRecorder.getRecognitionCount(faceId); statusResp.setRecognitionCount(recognitionCount); // 查询是否触发过低阈值检测 - String lowThresholdKey = FACE_LOW_THRESHOLD_PFX + faceId; - Boolean hasLowThreshold = redisTemplate.hasKey(lowThresholdKey); + Boolean hasLowThreshold = metricsRecorder.hasLowThreshold(faceId); statusResp.setHasLowThreshold(hasLowThreshold); log.debug("查询人脸状态:faceId={}, recognitionCount={}, hasLowThreshold={}", @@ -1005,27 +1008,10 @@ public class FaceServiceImpl implements FaceService { } ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(face.getScenicId()); - String recognitionKey = FACE_RECOGNITION_COUNT_PFX + faceId; - String recognitionCountStr = redisTemplate.opsForValue().get(recognitionKey); - long recognitionCount = 0L; - if (recognitionCountStr != null) { - try { - recognitionCount = Long.parseLong(recognitionCountStr); - } catch (NumberFormatException e) { - log.warn("识别次数解析失败,faceId={}, count={}", faceId, recognitionCountStr); - } - } - - String customMatchKey = FACE_CUSTOM_MATCH_COUNT_PFX + faceId; - String customMatchCountStr = redisTemplate.opsForValue().get(customMatchKey); - long customMatchCount = 0L; - if (customMatchCountStr != null) { - try { - customMatchCount = Long.parseLong(customMatchCountStr); - } catch (NumberFormatException e) { - log.warn("自定义匹配次数解析失败,faceId={}, count={}", faceId, customMatchCountStr); - } - } + // 使用FaceMetricsRecorder获取计数信息 + long recognitionCount = metricsRecorder.getRecognitionCount(faceId); + long customMatchCount = metricsRecorder.getCustomMatchCount(faceId); + boolean hasLowThreshold = metricsRecorder.hasLowThreshold(faceId); Integer faceSelectMaxCount = scenicConfig.getInteger("face_select_max_count"); if (faceSelectMaxCount != null && faceSelectMaxCount > 0 && customMatchCount > faceSelectMaxCount) { @@ -1072,16 +1058,20 @@ public class FaceServiceImpl implements FaceService { if (projectMatch) { ruleMatched++; } - // 查询是否触发过低阈值检测 - String lowThresholdKey = FACE_LOW_THRESHOLD_PFX + faceId; - boolean hasLowThreshold = redisTemplate.hasKey(lowThresholdKey); + + // 使用策略模式替换switch语句 Integer mode = scenicConfig.getInteger("re_match_mode", 0); - return switch (mode) { - case 1 -> tourMatch || recognitionCount > 1 || hasLowThreshold; - case 5 -> hasLowThreshold || (ruleMatched >= 2); - case 9 -> hasLowThreshold && ruleMatched >= 2; - default -> false; - }; + RematchContext context = RematchContext.builder() + .recognitionCount(recognitionCount) + .hasLowThreshold(hasLowThreshold) + .tourMatch(tourMatch) + .projectMatch(projectMatch) + .ruleMatched(ruleMatched) + .faceCreateAt(face.getCreateAt()) + .build(); + + RematchModeStrategy strategy = rematchStrategyFactory.getStrategy(mode); + return strategy.shouldRematch(context); } @Override @@ -1128,7 +1118,7 @@ public class FaceServiceImpl implements FaceService { log.debug("开始自定义人脸匹配:faceId={}, faceSampleIds={}", faceId, faceSampleIds); // 记录自定义匹配调用次数,便于监控调用频率 - recordCustomMatchCount(faceId); + metricsRecorder.recordCustomMatchCount(faceId); try { FaceEntity face = faceRepository.getFace(faceId); @@ -1161,7 +1151,7 @@ public class FaceServiceImpl implements FaceService { if (Integer.valueOf(2).equals(faceSelectPostMode)) { // 模式2:不搜索,直接使用用户选择的faceSampleIds log.debug("使用模式2:直接使用用户选择的人脸样本,不进行搜索"); - mergedResult = createDirectResult(faceSampleIds); + mergedResult = resultMerger.createDirectResult(faceSampleIds); mergedResult.setSearchResultJson(face.getMatchResult()); // 没有检索 } else { // 模式0(并集)和模式1(交集):需要进行搜索 @@ -1189,7 +1179,7 @@ public class FaceServiceImpl implements FaceService { } // 2.2 根据模式整合多个搜索结果 - mergedResult = mergeSearchResults(searchResults, faceSelectPostMode); + mergedResult = resultMerger.merge(searchResults, faceSelectPostMode); } // 3. 应用后置筛选逻辑 @@ -1256,13 +1246,6 @@ public class FaceServiceImpl implements FaceService { } } - /** - * 合并多个搜索结果(兼容老版本,默认使用并集模式) - */ - private SearchFaceRespVo mergeSearchResults(List searchResults) { - return mergeSearchResults(searchResults, 0); - } - @Override public void updateRecognition(FaceRecognitionUpdateReq req) { if (req == null || req.getFaceId() == null) { @@ -1326,7 +1309,7 @@ public class FaceServiceImpl implements FaceService { detail.setFaceUrl(face.getFaceUrl()); detail.setScore(face.getScore()); detail.setFirstMatchRate(face.getFirstMatchRate() != null ? face.getFirstMatchRate().floatValue() : null); - detail.setLowThreshold(redisTemplate.hasKey(FACE_LOW_THRESHOLD_PFX + faceId)); + detail.setLowThreshold(metricsRecorder.hasLowThreshold(faceId)); detail.setLastMatchedAt(face.getUpdateAt() != null ? face.getUpdateAt() : face.getCreateAt()); String matchResultJson = face.getMatchResult(); @@ -1505,198 +1488,4 @@ public class FaceServiceImpl implements FaceService { } return null; } - - /** - * 合并多个搜索结果 - * - * @param searchResults 搜索结果列表 - * @param mergeMode 合并模式:0-并集,1-交集 - * @return 合并后的结果 - */ - private SearchFaceRespVo mergeSearchResults(List searchResults, Integer mergeMode) { - SearchFaceRespVo mergedResult = new SearchFaceRespVo(); - - if (searchResults == null || searchResults.isEmpty()) { - return mergedResult; - } - - List allSearchJsons = new ArrayList<>(); - float maxScore = 0f; - float maxFirstMatchRate = 0f; - boolean hasLowThreshold = false; - - // 收集基础信息 - for (SearchFaceRespVo result : searchResults) { - if (result.getSearchResultJson() != null) { - allSearchJsons.add(result.getSearchResultJson()); - } - if (result.getScore() > maxScore) { - maxScore = result.getScore(); - } - if (result.getFirstMatchRate() > maxFirstMatchRate) { - maxFirstMatchRate = result.getFirstMatchRate(); - } - if (result.isLowThreshold()) { - hasLowThreshold = true; - } - } - - // 根据合并模式处理样本ID - List finalSampleIds; - if (Integer.valueOf(1).equals(mergeMode)) { - // 模式1:交集 - 只保留所有搜索结果中都出现的样本ID - finalSampleIds = computeIntersection(searchResults); - log.debug("使用交集模式合并搜索结果,交集样本数: {}", finalSampleIds.size()); - } else { - // 模式0:并集(默认) - 收集所有样本ID并去重 - Set allSampleIds = new LinkedHashSet<>(); - for (SearchFaceRespVo result : searchResults) { - if (result.getSampleListIds() != null) { - allSampleIds.addAll(result.getSampleListIds()); - } - } - finalSampleIds = new ArrayList<>(allSampleIds); - log.debug("使用并集模式合并搜索结果,并集样本数: {}", finalSampleIds.size()); - } - - mergedResult.setSampleListIds(finalSampleIds); - mergedResult.setSearchResultJson(String.join("|", allSearchJsons)); - mergedResult.setScore(maxScore); - mergedResult.setFirstMatchRate(maxFirstMatchRate); - mergedResult.setLowThreshold(hasLowThreshold); - - log.debug("合并搜索结果完成,模式={}, 最终样本数: {}", mergeMode, finalSampleIds.size()); - - return mergedResult; - } - - /** - * 计算多个搜索结果的交集 - * 返回在所有搜索结果中都出现的样本ID - */ - private List computeIntersection(List searchResults) { - if (searchResults == null || searchResults.isEmpty()) { - return new ArrayList<>(); - } - - // 过滤掉空结果 - List> validSampleLists = searchResults.stream() - .filter(result -> result.getSampleListIds() != null && !result.getSampleListIds().isEmpty()) - .map(SearchFaceRespVo::getSampleListIds) - .toList(); - - if (validSampleLists.isEmpty()) { - return new ArrayList<>(); - } - - // 如果只有一个有效结果,直接返回 - if (validSampleLists.size() == 1) { - return new ArrayList<>(validSampleLists.getFirst()); - } - - // 计算交集:从第一个列表开始,保留在所有其他列表中都出现的ID - Set intersection = new LinkedHashSet<>(validSampleLists.getFirst()); - - for (int i = 1; i < validSampleLists.size(); i++) { - intersection.retainAll(validSampleLists.get(i)); - } - - return new ArrayList<>(intersection); - } - - /** - * 创建直接结果(模式2:不搜索,直接使用用户选择的faceSampleIds) - * - * @param faceSampleIds 用户选择的人脸样本ID列表 - * @return 搜索结果对象 - */ - private SearchFaceRespVo createDirectResult(List faceSampleIds) { - SearchFaceRespVo result = new SearchFaceRespVo(); - - // 直接使用用户选择的faceSampleIds作为结果 - result.setSampleListIds(new ArrayList<>(faceSampleIds)); - - // 设置默认值 - result.setScore(1.0f); - result.setFirstMatchRate(1.0f); - result.setLowThreshold(false); - result.setSearchResultJson(""); - - log.debug("创建直接结果,样本数: {}", faceSampleIds.size()); - - return result; - } - - /** - * 记录自定义人脸匹配次数到Redis - * - * @param faceId 人脸ID - */ - private void recordCustomMatchCount(Long faceId) { - if (faceId == null) { - return; - } - - try { - String redisKey = FACE_CUSTOM_MATCH_COUNT_PFX + faceId; - - Long count = redisTemplate.opsForValue().increment(redisKey); - redisTemplate.expire(redisKey, 2, TimeUnit.DAYS); - - log.debug("自定义人脸匹配计数更新:faceId={}, count={}", faceId, count); - } catch (Exception e) { - log.error("记录自定义人脸匹配次数失败:faceId={}", faceId, e); - } - } - - /** - * 记录人脸识别次数到Redis - * - * @param faceId 人脸ID - */ - private void recordFaceRecognitionCount(Long faceId) { - if (faceId == null) { - return; - } - - try { - String redisKey = FACE_RECOGNITION_COUNT_PFX + faceId; - - // 使用Redis原子操作INCR增加计数 - Long count = redisTemplate.opsForValue().increment(redisKey); - - // 设置2天过期时间(48小时) - redisTemplate.expire(redisKey, 2, TimeUnit.DAYS); - - log.debug("人脸识别计数更新:faceId={}, count={}", faceId, count); - - } catch (Exception e) { - // 计数失败不应影响主要业务逻辑,只记录错误日志 - log.error("记录人脸识别次数失败:faceId={}", faceId, e); - } - } - - /** - * 记录低阈值检测的人脸ID到Redis - * - * @param faceId 人脸ID - */ - private void recordLowThresholdFace(Long faceId) { - if (faceId == null) { - return; - } - - try { - String redisKey = FACE_LOW_THRESHOLD_PFX + faceId; - - // 设置标记,表示该人脸ID触发了低阈值检测 - redisTemplate.opsForValue().set(redisKey, "1", 2, TimeUnit.DAYS); - - log.debug("记录低阈值检测人脸:faceId={}", faceId); - - } catch (Exception e) { - // 记录失败不应影响主要业务逻辑,只记录错误日志 - log.error("记录低阈值检测人脸失败:faceId={}", faceId, e); - } - } } diff --git a/src/main/java/com/ycwl/basic/service/pc/strategy/RematchContext.java b/src/main/java/com/ycwl/basic/service/pc/strategy/RematchContext.java new file mode 100644 index 00000000..1523e2e2 --- /dev/null +++ b/src/main/java/com/ycwl/basic/service/pc/strategy/RematchContext.java @@ -0,0 +1,47 @@ +package com.ycwl.basic.service.pc.strategy; + +import lombok.Builder; +import lombok.Data; + +import java.util.Date; + +/** + * 重匹配上下文 + * 包含判断是否需要重匹配的所有必要信息 + * + * @author longbinbin + * @date 2025-01-31 + */ +@Data +@Builder +public class RematchContext { + /** + * 人脸识别次数 + */ + private long recognitionCount; + + /** + * 是否触发低阈值检测 + */ + private boolean hasLowThreshold; + + /** + * 是否符合游览时间匹配 + */ + private boolean tourMatch; + + /** + * 是否符合项目时间匹配 + */ + private boolean projectMatch; + + /** + * 规则匹配数量 + */ + private int ruleMatched; + + /** + * 人脸创建时间 + */ + private Date faceCreateAt; +} diff --git a/src/main/java/com/ycwl/basic/service/pc/strategy/RematchModeStrategy.java b/src/main/java/com/ycwl/basic/service/pc/strategy/RematchModeStrategy.java new file mode 100644 index 00000000..1539cecd --- /dev/null +++ b/src/main/java/com/ycwl/basic/service/pc/strategy/RematchModeStrategy.java @@ -0,0 +1,26 @@ +package com.ycwl.basic.service.pc.strategy; + +/** + * 重匹配模式策略接口 + * 用于判断是否需要进行人脸重新匹配 + * + * @author longbinbin + * @date 2025-01-31 + */ +public interface RematchModeStrategy { + + /** + * 判断是否应该重新匹配 + * + * @param context 重匹配上下文 + * @return true-需要重匹配, false-不需要重匹配 + */ + boolean shouldRematch(RematchContext context); + + /** + * 获取策略对应的模式值 + * + * @return 模式值 + */ + int getMode(); +} diff --git a/src/main/java/com/ycwl/basic/service/pc/strategy/RematchStrategyFactory.java b/src/main/java/com/ycwl/basic/service/pc/strategy/RematchStrategyFactory.java new file mode 100644 index 00000000..bfba7f19 --- /dev/null +++ b/src/main/java/com/ycwl/basic/service/pc/strategy/RematchStrategyFactory.java @@ -0,0 +1,60 @@ +package com.ycwl.basic.service.pc.strategy; + +import com.ycwl.basic.service.pc.strategy.impl.DefaultRematchStrategy; +import com.ycwl.basic.service.pc.strategy.impl.RematchMode1Strategy; +import com.ycwl.basic.service.pc.strategy.impl.RematchMode5Strategy; +import com.ycwl.basic.service.pc.strategy.impl.RematchMode9Strategy; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import jakarta.annotation.PostConstruct; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 重匹配策略工厂 + * 根据模式值获取对应的策略实例 + * + * @author longbinbin + * @date 2025-01-31 + */ +@Slf4j +@Component +public class RematchStrategyFactory { + + @Autowired + private List strategies; + + private final Map strategyMap = new HashMap<>(); + + @PostConstruct + public void init() { + for (RematchModeStrategy strategy : strategies) { + strategyMap.put(strategy.getMode(), strategy); + log.debug("注册重匹配策略: mode={}, class={}", + strategy.getMode(), strategy.getClass().getSimpleName()); + } + } + + /** + * 根据模式值获取对应的策略 + * + * @param mode 模式值(0-默认, 1-模式1, 5-模式5, 9-模式9) + * @return 对应的策略实例,如果没有找到则返回默认策略 + */ + public RematchModeStrategy getStrategy(Integer mode) { + if (mode == null) { + return strategyMap.getOrDefault(0, new DefaultRematchStrategy()); + } + + RematchModeStrategy strategy = strategyMap.get(mode); + if (strategy == null) { + log.warn("未找到重匹配模式{}对应的策略,使用默认策略", mode); + return strategyMap.getOrDefault(0, new DefaultRematchStrategy()); + } + + return strategy; + } +} diff --git a/src/main/java/com/ycwl/basic/service/pc/strategy/impl/DefaultRematchStrategy.java b/src/main/java/com/ycwl/basic/service/pc/strategy/impl/DefaultRematchStrategy.java new file mode 100644 index 00000000..75a853dd --- /dev/null +++ b/src/main/java/com/ycwl/basic/service/pc/strategy/impl/DefaultRematchStrategy.java @@ -0,0 +1,29 @@ +package com.ycwl.basic.service.pc.strategy.impl; + +import com.ycwl.basic.service.pc.strategy.RematchContext; +import com.ycwl.basic.service.pc.strategy.RematchModeStrategy; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 默认重匹配策略(模式0或其他未定义模式) + * 条件: 不触发重匹配 + * + * @author longbinbin + * @date 2025-01-31 + */ +@Slf4j +@Component +public class DefaultRematchStrategy implements RematchModeStrategy { + + @Override + public boolean shouldRematch(RematchContext context) { + log.debug("DefaultRematchStrategy判断: 默认不重匹配"); + return false; + } + + @Override + public int getMode() { + return 0; + } +} diff --git a/src/main/java/com/ycwl/basic/service/pc/strategy/impl/RematchMode1Strategy.java b/src/main/java/com/ycwl/basic/service/pc/strategy/impl/RematchMode1Strategy.java new file mode 100644 index 00000000..1b0f6aac --- /dev/null +++ b/src/main/java/com/ycwl/basic/service/pc/strategy/impl/RematchMode1Strategy.java @@ -0,0 +1,37 @@ +package com.ycwl.basic.service.pc.strategy.impl; + +import com.ycwl.basic.service.pc.strategy.RematchContext; +import com.ycwl.basic.service.pc.strategy.RematchModeStrategy; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 重匹配模式1策略 + * 条件: tourMatch || recognitionCount > 1 || hasLowThreshold + * 满足任一条件即可重匹配 + * + * @author longbinbin + * @date 2025-01-31 + */ +@Slf4j +@Component +public class RematchMode1Strategy implements RematchModeStrategy { + + @Override + public boolean shouldRematch(RematchContext context) { + boolean result = context.isTourMatch() + || context.getRecognitionCount() > 1 + || context.isHasLowThreshold(); + + log.debug("RematchMode1Strategy判断: tourMatch={}, recognitionCount={}, hasLowThreshold={}, result={}", + context.isTourMatch(), context.getRecognitionCount(), + context.isHasLowThreshold(), result); + + return result; + } + + @Override + public int getMode() { + return 1; + } +} diff --git a/src/main/java/com/ycwl/basic/service/pc/strategy/impl/RematchMode5Strategy.java b/src/main/java/com/ycwl/basic/service/pc/strategy/impl/RematchMode5Strategy.java new file mode 100644 index 00000000..3c1250af --- /dev/null +++ b/src/main/java/com/ycwl/basic/service/pc/strategy/impl/RematchMode5Strategy.java @@ -0,0 +1,35 @@ +package com.ycwl.basic.service.pc.strategy.impl; + +import com.ycwl.basic.service.pc.strategy.RematchContext; +import com.ycwl.basic.service.pc.strategy.RematchModeStrategy; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 重匹配模式5策略 + * 条件: hasLowThreshold || (ruleMatched >= 2) + * 触发低阈值或匹配2个及以上规则即可重匹配 + * + * @author longbinbin + * @date 2025-01-31 + */ +@Slf4j +@Component +public class RematchMode5Strategy implements RematchModeStrategy { + + @Override + public boolean shouldRematch(RematchContext context) { + boolean result = context.isHasLowThreshold() + || context.getRuleMatched() >= 2; + + log.debug("RematchMode5Strategy判断: hasLowThreshold={}, ruleMatched={}, result={}", + context.isHasLowThreshold(), context.getRuleMatched(), result); + + return result; + } + + @Override + public int getMode() { + return 5; + } +} diff --git a/src/main/java/com/ycwl/basic/service/pc/strategy/impl/RematchMode9Strategy.java b/src/main/java/com/ycwl/basic/service/pc/strategy/impl/RematchMode9Strategy.java new file mode 100644 index 00000000..cb34fbda --- /dev/null +++ b/src/main/java/com/ycwl/basic/service/pc/strategy/impl/RematchMode9Strategy.java @@ -0,0 +1,35 @@ +package com.ycwl.basic.service.pc.strategy.impl; + +import com.ycwl.basic.service.pc.strategy.RematchContext; +import com.ycwl.basic.service.pc.strategy.RematchModeStrategy; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 重匹配模式9策略 + * 条件: hasLowThreshold && (ruleMatched >= 2) + * 必须同时触发低阈值且匹配2个及以上规则才可重匹配 + * + * @author longbinbin + * @date 2025-01-31 + */ +@Slf4j +@Component +public class RematchMode9Strategy implements RematchModeStrategy { + + @Override + public boolean shouldRematch(RematchContext context) { + boolean result = context.isHasLowThreshold() + && context.getRuleMatched() >= 2; + + log.debug("RematchMode9Strategy判断: hasLowThreshold={}, ruleMatched={}, result={}", + context.isHasLowThreshold(), context.getRuleMatched(), result); + + return result; + } + + @Override + public int getMode() { + return 9; + } +}