From bf014db7ff6f525180a598df2e3ec83ac63524fc Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Fri, 31 Oct 2025 17:11:02 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat(face):=20=E5=BC=95=E5=85=A5=E4=BA=BA?= =?UTF-8?q?=E8=84=B8=E8=AF=86=E5=88=AB=E6=8C=87=E6=A0=87=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E4=B8=8E=E6=90=9C=E7=B4=A2=E7=BB=93=E6=9E=9C=E5=90=88=E5=B9=B6?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 FaceMetricsRecorder 类用于记录人脸识别、自定义匹配及低阈值检测次数 - 新增 SearchResultMerger 类用于合并多个人脸搜索结果,支持并集与交集模式- 在 FaceServiceImpl 中引入 metricsRecorder 和 resultMerger 辅助类 - 替换原有的 Redis 操作代码为 FaceMetricsRecorder 的方法调用- 将搜索结果合并逻辑从 FaceServiceImpl 提取至 SearchResultMerger- 新增策略模式相关类:RematchContext、RematchModeStrategy 接口及四种实现 - 使用策略工厂 Rematch --- .../pc/helper/FaceMetricsRecorder.java | 169 ++++++++++ .../service/pc/helper/SearchResultMerger.java | 154 +++++++++ .../service/pc/impl/FaceServiceImpl.java | 291 +++--------------- .../service/pc/strategy/RematchContext.java | 47 +++ .../pc/strategy/RematchModeStrategy.java | 26 ++ .../pc/strategy/RematchStrategyFactory.java | 60 ++++ .../strategy/impl/DefaultRematchStrategy.java | 29 ++ .../strategy/impl/RematchMode1Strategy.java | 37 +++ .../strategy/impl/RematchMode5Strategy.java | 35 +++ .../strategy/impl/RematchMode9Strategy.java | 35 +++ 10 files changed, 632 insertions(+), 251 deletions(-) create mode 100644 src/main/java/com/ycwl/basic/service/pc/helper/FaceMetricsRecorder.java create mode 100644 src/main/java/com/ycwl/basic/service/pc/helper/SearchResultMerger.java create mode 100644 src/main/java/com/ycwl/basic/service/pc/strategy/RematchContext.java create mode 100644 src/main/java/com/ycwl/basic/service/pc/strategy/RematchModeStrategy.java create mode 100644 src/main/java/com/ycwl/basic/service/pc/strategy/RematchStrategyFactory.java create mode 100644 src/main/java/com/ycwl/basic/service/pc/strategy/impl/DefaultRematchStrategy.java create mode 100644 src/main/java/com/ycwl/basic/service/pc/strategy/impl/RematchMode1Strategy.java create mode 100644 src/main/java/com/ycwl/basic/service/pc/strategy/impl/RematchMode5Strategy.java create mode 100644 src/main/java/com/ycwl/basic/service/pc/strategy/impl/RematchMode9Strategy.java 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; + } +} From 3000e18cb7da239f03426941890eeec1fc094144 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Fri, 31 Oct 2025 17:31:48 +0800 Subject: [PATCH 2/4] =?UTF-8?q?refactor(face):=E9=87=8D=E6=9E=84=E4=BA=BA?= =?UTF-8?q?=E8=84=B8=E8=AF=86=E5=88=AB=E6=9C=8D=E5=8A=A1=E9=80=BB=E8=BE=91?= =?UTF-8?q?=20-=20=E5=B0=86=E4=BA=BA=E8=84=B8=E8=AF=86=E5=88=AB=E8=A1=A5?= =?UTF-8?q?=E6=95=91=E9=80=BB=E8=BE=91=E6=8F=90=E5=8F=96=E5=88=B0FaceRecov?= =?UTF-8?q?eryStrategy=E7=B1=BB=E4=B8=AD=20-=20=E5=B0=86=E6=BA=90=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=85=B3=E8=81=94=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E6=8F=90=E5=8F=96=E5=88=B0SourceRelationProcessor=E7=B1=BB?= =?UTF-8?q?=E4=B8=AD=20-=20=E5=B0=86=E8=B4=AD=E4=B9=B0=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91=E6=8F=90=E5=8F=96=E5=88=B0?= =?UTF-8?q?BuyStatusProcessor=E7=B1=BB=E4=B8=AD=20-=20=E5=B0=86=E8=A7=86?= =?UTF-8?q?=E9=A2=91=E9=87=8D=E5=88=87=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E6=8F=90=E5=8F=96=E5=88=B0VideoRecreationHandler=E7=B1=BB?= =?UTF-8?q?=E4=B8=AD=20-=20=E5=9C=A8FaceServiceImpl=E4=B8=AD=E5=BC=95?= =?UTF-8?q?=E5=85=A5=E5=9B=9B=E4=B8=AA=E6=96=B0=E7=9A=84=E5=A4=84=E7=90=86?= =?UTF-8?q?=E5=99=A8=E7=BB=84=E4=BB=B6=20-=20=E5=88=A0=E9=99=A4=E5=8E=9F?= =?UTF-8?q?=E6=9C=89=E7=9A=84=E5=86=97=E9=95=BF=E6=96=B9=E6=B3=95=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=EF=BC=8C=E6=94=B9=E4=B8=BA=E8=B0=83=E7=94=A8=E5=AF=B9?= =?UTF-8?q?=E5=BA=94=E5=A4=84=E7=90=86=E5=99=A8=20-=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E6=96=B9=E6=B3=95=E8=B0=83=E7=94=A8=E6=96=B9=E5=BC=8F=E4=BB=A5?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E6=96=B0=E7=9A=84=E5=A4=84=E7=90=86=E5=99=A8?= =?UTF-8?q?=E5=AE=9E=E4=BE=8B=20-=20=E4=BF=9D=E7=95=99=E6=A0=B8=E5=BF=83?= =?UTF-8?q?=E4=B8=9A=E5=8A=A1=E6=B5=81=E7=A8=8B=E4=BD=86=E8=A7=A3=E8=80=A6?= =?UTF-8?q?=E5=85=B7=E4=BD=93=E5=AE=9E=E7=8E=B0=E7=BB=86=E8=8A=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/pc/impl/FaceServiceImpl.java | 301 ++---------------- .../pc/processor/BuyStatusProcessor.java | 71 +++++ .../pc/processor/FaceRecoveryStrategy.java | 94 ++++++ .../pc/processor/SourceRelationProcessor.java | 144 +++++++++ .../pc/processor/VideoRecreationHandler.java | 113 +++++++ 5 files changed, 448 insertions(+), 275 deletions(-) create mode 100644 src/main/java/com/ycwl/basic/service/pc/processor/BuyStatusProcessor.java create mode 100644 src/main/java/com/ycwl/basic/service/pc/processor/FaceRecoveryStrategy.java create mode 100644 src/main/java/com/ycwl/basic/service/pc/processor/SourceRelationProcessor.java create mode 100644 src/main/java/com/ycwl/basic/service/pc/processor/VideoRecreationHandler.java 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 eca47006..d230fb90 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 @@ -60,6 +60,10 @@ 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.processor.BuyStatusProcessor; +import com.ycwl.basic.service.pc.processor.FaceRecoveryStrategy; +import com.ycwl.basic.service.pc.processor.SourceRelationProcessor; +import com.ycwl.basic.service.pc.processor.VideoRecreationHandler; import com.ycwl.basic.service.pc.strategy.RematchContext; import com.ycwl.basic.service.pc.strategy.RematchModeStrategy; import com.ycwl.basic.service.pc.strategy.RematchStrategyFactory; @@ -158,7 +162,7 @@ public class FaceServiceImpl implements FaceService { @Autowired private TemplateRepository templateRepository; - // 新增的辅助类 + // 第一阶段的辅助类 @Autowired private FaceMetricsRecorder metricsRecorder; @Autowired @@ -166,6 +170,16 @@ public class FaceServiceImpl implements FaceService { @Autowired private RematchStrategyFactory rematchStrategyFactory; + // 第二阶段的处理器 + @Autowired + private SourceRelationProcessor sourceRelationProcessor; + @Autowired + private BuyStatusProcessor buyStatusProcessor; + @Autowired + private VideoRecreationHandler videoRecreationHandler; + @Autowired + private FaceRecoveryStrategy faceRecoveryStrategy; + @Override public ApiResponse> pageQuery(FaceReqQuery faceReqQuery) { PageHelper.startPage(faceReqQuery.getPageNum(),faceReqQuery.getPageSize()); @@ -341,7 +355,7 @@ public class FaceServiceImpl implements FaceService { } // 执行补救逻辑(如需要) - scenicDbSearchResult = executeFaceRecoveryLogic(scenicDbSearchResult, scenicConfig, + scenicDbSearchResult = faceRecoveryStrategy.executeFaceRecoveryLogic(scenicDbSearchResult, scenicConfig, faceBodyAdapter, face.getScenicId()); // 3. 结果处理:更新人脸实体信息 @@ -356,16 +370,16 @@ public class FaceServiceImpl implements FaceService { if (sampleListIds != null && !sampleListIds.isEmpty()) { try { // 4. 源文件关联:处理匹配到的源文件 - List memberSourceEntityList = processMemberSources(sampleListIds, face); - + List memberSourceEntityList = sourceRelationProcessor.processMemberSources(sampleListIds, face); + if (!memberSourceEntityList.isEmpty()) { // 5. 业务逻辑处理:免费逻辑、购买状态、任务创建 - List freeSourceIds = processFreeSourceLogic(memberSourceEntityList, scenicConfig, isNew); - processBuyStatus(memberSourceEntityList, freeSourceIds, face.getMemberId(), + List freeSourceIds = sourceRelationProcessor.processFreeSourceLogic(memberSourceEntityList, scenicConfig, isNew); + buyStatusProcessor.processBuyStatus(memberSourceEntityList, freeSourceIds, face.getMemberId(), face.getScenicId(), faceId); - + // 处理视频重切逻辑 - handleVideoRecreation(scenicConfig, memberSourceEntityList, faceId, + videoRecreationHandler.handleVideoRecreation(scenicConfig, memberSourceEntityList, faceId, face.getMemberId(), sampleListIds, isNew); // 过滤已存在的关联关系和无效的source引用,防止数据不一致 @@ -448,269 +462,6 @@ public class FaceServiceImpl implements FaceService { searchResult.getSampleListIds() != null ? searchResult.getSampleListIds().size() : 0); } - /** - * 执行人脸识别补救逻辑 - * 当匹配结果数量少于阈值时,使用第一个匹配结果重新进行人脸搜索 - */ - private SearchFaceRespVo executeFaceRecoveryLogic(SearchFaceRespVo originalResult, - ScenicConfigManager scenicConfig, - IFaceBodyAdapter faceBodyAdapter, - Long scenicId) { - if (originalResult == null || originalResult.getSampleListIds() == null || - originalResult.getFirstMatchRate() == null || originalResult.getSampleListIds().isEmpty()) { - return originalResult; - } - - if (scenicConfig == null) { - return originalResult; - } - - // 检查是否需要执行补救逻辑 - Integer helperThreshold = scenicConfig.getInteger("face_detect_helper_threshold", 0); - if (helperThreshold == null || helperThreshold <= 0) { - return originalResult; - } - - // 检查匹配结果数量是否少于阈值 - if (originalResult.getSampleListIds().size() >= helperThreshold) { - return originalResult; - } - - log.info("执行人脸识别补救逻辑,原匹配数量: {}, 阈值: {}", - originalResult.getSampleListIds().size(), helperThreshold); - - // 获取第一个匹配结果 - Long firstResultId = originalResult.getSampleListIds().getFirst(); - FaceSampleEntity faceSample = faceRepository.getFaceSample(firstResultId); - - if (faceSample == null) { - log.warn("补救逻辑失败:无法找到人脸样本, sampleId: {}", firstResultId); - return originalResult; - } - - // 使用人脸样本重新进行搜索 - try { - SearchFaceRespVo recoveryResult = faceService.searchFace(faceBodyAdapter, - String.valueOf(scenicId), - faceSample.getFaceUrl(), - "人脸补救措施1"); - - if (recoveryResult != null && recoveryResult.getSampleListIds() != null && - !recoveryResult.getSampleListIds().isEmpty()) { - log.info("补救逻辑成功,新匹配数量: {}", recoveryResult.getSampleListIds().size()); - return recoveryResult; - } - } catch (Exception e) { - log.warn("补救逻辑执行失败", e); - } - - return originalResult; - } - - /** - * 处理源文件关联逻辑 - * 根据匹配的样本ID创建MemberSourceEntity列表 - */ - private List processMemberSources(List sampleListIds, FaceEntity face) { - if (sampleListIds == null || sampleListIds.isEmpty()) { - return Collections.emptyList(); - } - - List sourceEntities = sourceMapper.listBySampleIds(sampleListIds); - if (sourceEntities.isEmpty()) { - return Collections.emptyList(); - } - - List filteredSourceEntities = sourceEntities.stream() - .sorted(Comparator.comparing(SourceEntity::getCreateTime).reversed()) - .collect(Collectors.groupingBy(SourceEntity::getDeviceId)) - .entrySet() - .stream().flatMap(entry -> { - DeviceConfigManager configManager = deviceRepository.getDeviceConfigManager(entry.getKey()); - if (configManager.getInteger("limit_video", 0) > 0) { - return Stream.concat( - entry.getValue().stream().filter(item -> item.getType() == 2), - entry.getValue().stream().filter(item -> item.getType() == 1).limit(Math.min(entry.getValue().size(), configManager.getInteger("limit_video", 0))) - ); - } - return entry.getValue().stream(); - }).toList(); - return filteredSourceEntities.stream().map(sourceEntity -> { - DeviceConfigManager deviceConfig = deviceRepository.getDeviceConfigManager(sourceEntity.getDeviceId()); - MemberSourceEntity memberSourceEntity = new MemberSourceEntity(); - memberSourceEntity.setScenicId(face.getScenicId()); - memberSourceEntity.setFaceId(face.getId()); - memberSourceEntity.setMemberId(face.getMemberId()); - memberSourceEntity.setSourceId(sourceEntity.getId()); - memberSourceEntity.setType(sourceEntity.getType()); - - // 设置免费状态 - 默认收费 - memberSourceEntity.setIsFree(0); - - if (deviceConfig != null) { - // 视频类型检查 - if (sourceEntity.getType() == 1) { - if (Integer.valueOf(1).equals(deviceConfig.getInteger("video_free"))) { - memberSourceEntity.setIsFree(1); - } - } - // 图片类型检查 - else if (sourceEntity.getType() == 2) { - if (Integer.valueOf(1).equals(deviceConfig.getInteger("image_free"))) { - memberSourceEntity.setIsFree(1); - } - } - } - - return memberSourceEntity; - }).collect(Collectors.toList()); - } - - /** - * 处理免费源文件逻辑 - * 根据景区配置和是否新用户决定哪些照片可以免费 - */ - private List processFreeSourceLogic(List memberSourceEntityList, - ScenicConfigManager scenicConfig, - boolean isNew) { - List freeSourceIds = new ArrayList<>(); - - if (memberSourceEntityList.isEmpty()) { - return freeSourceIds; - } - - if (isNew) { - // 新用户送照片逻辑 - List photoSource = memberSourceEntityList.stream() - .filter(item -> item.getIsFree() == 0) // 只考虑收费的 - .filter(item -> Integer.valueOf(2).equals(item.getType())) // 只考虑照片类型 - .toList(); - - Integer photoFreeNum = scenicConfig != null ? scenicConfig.getInteger("photo_free_num") : null; - if (scenicConfig != null && photoFreeNum != null && photoFreeNum > 0) { - - int freePhotoCount = Math.min(photoFreeNum, photoSource.size()); - freeSourceIds.addAll(photoSource.stream() - .limit(freePhotoCount) - .map(MemberSourceEntity::getSourceId) - .toList()); - - log.debug("新用户免费照片逻辑:配置免费数量 {}, 实际可用 {}, 赠送 {} 张", - photoFreeNum, photoSource.size(), freePhotoCount); - } - } - - return freeSourceIds; - } - - /** - * 处理购买状态逻辑 - * 设置每个源文件的购买状态和免费状态 - */ - private void processBuyStatus(List memberSourceEntityList, - List freeSourceIds, - Long memberId, - Long scenicId, - Long faceId) { - if (memberSourceEntityList.isEmpty()) { - return; - } - - // 获取用户购买状态 - IsBuyRespVO isBuy = orderBiz.isBuy(memberId, scenicId, - memberSourceEntityList.getFirst().getType(), - faceId); - - for (MemberSourceEntity memberSourceEntity : memberSourceEntityList) { - // 设置购买状态 - if (isBuy.isBuy()) { - // 如果用户买过 - memberSourceEntity.setIsBuy(1); - } else if (isBuy.isFree()) { - // 全免费逻辑 - memberSourceEntity.setIsBuy(1); - } else { - memberSourceEntity.setIsBuy(0); - } - - // 设置免费状态 - if (freeSourceIds.contains(memberSourceEntity.getSourceId())) { - memberSourceEntity.setIsFree(1); - } - } - - log.debug("购买状态处理完成:用户购买状态 isBuy={}, isFree={}, 免费源文件数量={}", - isBuy.isBuy(), isBuy.isFree(), freeSourceIds.size()); - } - - /** - * 处理视频重切逻辑 - * 当非新用户且照片数量大于视频数量时,创建视频重切任务 - */ - private void handleVideoRecreation(ScenicConfigManager scenicConfig, - List memberSourceEntityList, - Long faceId, - Long memberId, - List sampleListIds, - boolean isNew) { - // 新用户不执行视频重切逻辑 - if (isNew) { - return; - } - - // 检查景区是否禁用源视频功能 - Boolean disableSourceVideo = scenicConfig != null ? scenicConfig.getBoolean("disable_source_video") : null; - if (scenicConfig == null || Boolean.TRUE.equals(disableSourceVideo)) { - log.debug("视频重切逻辑跳过:景区禁用了源视频功能"); - return; - } - - // 统计视频和照片数量 - long videoCount = memberSourceEntityList.stream() - .filter(item -> Integer.valueOf(1).equals(item.getType())) - .count(); - long photoCount = memberSourceEntityList.stream() - .filter(item -> Integer.valueOf(2).equals(item.getType())) - .count(); - - List faceSampleList = faceRepository.getFaceSampleList(faceId); - if (faceSampleList.isEmpty()) { - log.info("faceId:{} sample list not exist", faceId); - return; - } - List faceSampleIds = faceSampleList.stream() - .sorted(Comparator.comparing(FaceSampleEntity::getCreateAt).reversed()) - .collect(Collectors.groupingBy(FaceSampleEntity::getDeviceId)) - .entrySet() - .stream().flatMap(entry -> { - DeviceConfigManager configManager = deviceRepository.getDeviceConfigManager(entry.getKey()); - if (configManager.getInteger("limit_video", 0) > 0) { - return entry.getValue().subList(0, Math.min(entry.getValue().size(), configManager.getInteger("limit_video", 0))).stream(); - } - return entry.getValue().stream(); - }).toList() - .stream().map(FaceSampleEntity::getId).toList(); - log.info("视频切分任务: faceId={}, 原始数量={}, 筛选后数量={}", faceId, faceSampleList.size(), faceSampleIds.size()); - log.debug("视频重切逻辑:视频数量 {}, 照片数量 {}", videoCount, photoCount); - - // 只有照片数量大于视频数量时才创建重切任务 - if (photoCount > videoCount) { - VideoPieceGetter.Task task = new VideoPieceGetter.Task(); - task.faceId = faceId; - task.faceSampleIds = faceSampleIds; - task.templateId = null; - task.memberId = memberId; - task.callback = () -> { - log.info("视频重切任务回调: {}", task); - }; - - VideoPieceGetter.addTask(task); - log.debug("视频重切任务已创建:faceId={}, memberId={}, sampleIds={}", - faceId, memberId, sampleListIds.size()); - } else { - log.debug("视频重切逻辑跳过:照片数量({})未超过视频数量({})", photoCount, videoCount); - } - } @Override public ApiResponse deleteFace(Long faceId) { @@ -1204,14 +955,14 @@ public class FaceServiceImpl implements FaceService { memberRelationRepository.clearSCacheByFace(faceId); log.debug("人脸旧关系数据删除完成:faceId={}", faceId); - List memberSourceEntityList = processMemberSources(sampleListIds, face); + List memberSourceEntityList = sourceRelationProcessor.processMemberSources(sampleListIds, face); if (!memberSourceEntityList.isEmpty()) { - List freeSourceIds = processFreeSourceLogic(memberSourceEntityList, scenicConfig, false); - processBuyStatus(memberSourceEntityList, freeSourceIds, face.getMemberId(), + List freeSourceIds = sourceRelationProcessor.processFreeSourceLogic(memberSourceEntityList, scenicConfig, false); + buyStatusProcessor.processBuyStatus(memberSourceEntityList, freeSourceIds, face.getMemberId(), face.getScenicId(), faceId); - handleVideoRecreation(scenicConfig, memberSourceEntityList, faceId, + videoRecreationHandler.handleVideoRecreation(scenicConfig, memberSourceEntityList, faceId, face.getMemberId(), sampleListIds, false); List existingFiltered = sourceMapper.filterExistingRelations(memberSourceEntityList); diff --git a/src/main/java/com/ycwl/basic/service/pc/processor/BuyStatusProcessor.java b/src/main/java/com/ycwl/basic/service/pc/processor/BuyStatusProcessor.java new file mode 100644 index 00000000..39394f5d --- /dev/null +++ b/src/main/java/com/ycwl/basic/service/pc/processor/BuyStatusProcessor.java @@ -0,0 +1,71 @@ +package com.ycwl.basic.service.pc.processor; + +import com.ycwl.basic.biz.OrderBiz; +import com.ycwl.basic.model.mobile.order.IsBuyRespVO; +import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * 购买状态处理器 + * 负责处理源文件的购买状态和免费状态 + * + * @author longbinbin + * @date 2025-01-31 + */ +@Slf4j +@Component +public class BuyStatusProcessor { + + @Autowired + private OrderBiz orderBiz; + + /** + * 处理购买状态逻辑 + * 设置每个源文件的购买状态和免费状态 + * + * @param memberSourceEntityList 源文件关联列表 + * @param freeSourceIds 免费的源文件ID列表 + * @param memberId 会员ID + * @param scenicId 景区ID + * @param faceId 人脸ID + */ + public void processBuyStatus(List memberSourceEntityList, + List freeSourceIds, + Long memberId, + Long scenicId, + Long faceId) { + if (memberSourceEntityList.isEmpty()) { + return; + } + + // 获取用户购买状态 + IsBuyRespVO isBuy = orderBiz.isBuy(memberId, scenicId, + memberSourceEntityList.getFirst().getType(), + faceId); + + for (MemberSourceEntity memberSourceEntity : memberSourceEntityList) { + // 设置购买状态 + if (isBuy.isBuy()) { + // 如果用户买过 + memberSourceEntity.setIsBuy(1); + } else if (isBuy.isFree()) { + // 全免费逻辑 + memberSourceEntity.setIsBuy(1); + } else { + memberSourceEntity.setIsBuy(0); + } + + // 设置免费状态 + if (freeSourceIds.contains(memberSourceEntity.getSourceId())) { + memberSourceEntity.setIsFree(1); + } + } + + log.debug("购买状态处理完成:用户购买状态 isBuy={}, isFree={}, 免费源文件数量={}", + isBuy.isBuy(), isBuy.isFree(), freeSourceIds.size()); + } +} diff --git a/src/main/java/com/ycwl/basic/service/pc/processor/FaceRecoveryStrategy.java b/src/main/java/com/ycwl/basic/service/pc/processor/FaceRecoveryStrategy.java new file mode 100644 index 00000000..5a9c610c --- /dev/null +++ b/src/main/java/com/ycwl/basic/service/pc/processor/FaceRecoveryStrategy.java @@ -0,0 +1,94 @@ +package com.ycwl.basic.service.pc.processor; + +import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter; +import com.ycwl.basic.integration.common.manager.ScenicConfigManager; +import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity; +import com.ycwl.basic.model.task.resp.SearchFaceRespVo; +import com.ycwl.basic.repository.FaceRepository; +import com.ycwl.basic.service.task.TaskFaceService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * 人脸识别补救策略 + * 负责执行人脸识别的补救逻辑 + * + * @author longbinbin + * @date 2025-01-31 + */ +@Slf4j +@Component +public class FaceRecoveryStrategy { + + @Autowired + private FaceRepository faceRepository; + + @Autowired + private TaskFaceService faceService; + + /** + * 执行人脸识别补救逻辑 + * 当匹配结果数量少于阈值时,使用第一个匹配结果重新进行人脸搜索 + * + * @param originalResult 原始搜索结果 + * @param scenicConfig 景区配置 + * @param faceBodyAdapter 人脸识别适配器 + * @param scenicId 景区ID + * @return 补救后的搜索结果(如果不需要补救或补救失败则返回原始结果) + */ + public SearchFaceRespVo executeFaceRecoveryLogic(SearchFaceRespVo originalResult, + ScenicConfigManager scenicConfig, + IFaceBodyAdapter faceBodyAdapter, + Long scenicId) { + if (originalResult == null || originalResult.getSampleListIds() == null || + originalResult.getFirstMatchRate() == null || originalResult.getSampleListIds().isEmpty()) { + return originalResult; + } + + if (scenicConfig == null) { + return originalResult; + } + + // 检查是否需要执行补救逻辑 + Integer helperThreshold = scenicConfig.getInteger("face_detect_helper_threshold", 0); + if (helperThreshold == null || helperThreshold <= 0) { + return originalResult; + } + + // 检查匹配结果数量是否少于阈值 + if (originalResult.getSampleListIds().size() >= helperThreshold) { + return originalResult; + } + + log.info("执行人脸识别补救逻辑,原匹配数量: {}, 阈值: {}", + originalResult.getSampleListIds().size(), helperThreshold); + + // 获取第一个匹配结果 + Long firstResultId = originalResult.getSampleListIds().getFirst(); + FaceSampleEntity faceSample = faceRepository.getFaceSample(firstResultId); + + if (faceSample == null) { + log.warn("补救逻辑失败:无法找到人脸样本, sampleId: {}", firstResultId); + return originalResult; + } + + // 使用人脸样本重新进行搜索 + try { + SearchFaceRespVo recoveryResult = faceService.searchFace(faceBodyAdapter, + String.valueOf(scenicId), + faceSample.getFaceUrl(), + "人脸补救措施1"); + + if (recoveryResult != null && recoveryResult.getSampleListIds() != null && + !recoveryResult.getSampleListIds().isEmpty()) { + log.info("补救逻辑成功,新匹配数量: {}", recoveryResult.getSampleListIds().size()); + return recoveryResult; + } + } catch (Exception e) { + log.warn("补救逻辑执行失败", e); + } + + return originalResult; + } +} diff --git a/src/main/java/com/ycwl/basic/service/pc/processor/SourceRelationProcessor.java b/src/main/java/com/ycwl/basic/service/pc/processor/SourceRelationProcessor.java new file mode 100644 index 00000000..e33c6f08 --- /dev/null +++ b/src/main/java/com/ycwl/basic/service/pc/processor/SourceRelationProcessor.java @@ -0,0 +1,144 @@ +package com.ycwl.basic.service.pc.processor; + +import com.ycwl.basic.integration.common.manager.DeviceConfigManager; +import com.ycwl.basic.integration.common.manager.ScenicConfigManager; +import com.ycwl.basic.mapper.SourceMapper; +import com.ycwl.basic.model.pc.face.entity.FaceEntity; +import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity; +import com.ycwl.basic.model.pc.source.entity.SourceEntity; +import com.ycwl.basic.repository.DeviceRepository; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * 源文件关联处理器 + * 负责处理人脸匹配后的源文件关联逻辑 + * + * @author longbinbin + * @date 2025-01-31 + */ +@Slf4j +@Component +public class SourceRelationProcessor { + + @Autowired + private SourceMapper sourceMapper; + + @Autowired + private DeviceRepository deviceRepository; + + /** + * 处理源文件关联逻辑 + * 根据匹配的样本ID创建MemberSourceEntity列表 + * + * @param sampleListIds 匹配的样本ID列表 + * @param face 人脸实体 + * @return MemberSourceEntity列表 + */ + public List processMemberSources(List sampleListIds, FaceEntity face) { + if (sampleListIds == null || sampleListIds.isEmpty()) { + return Collections.emptyList(); + } + + List sourceEntities = sourceMapper.listBySampleIds(sampleListIds); + if (sourceEntities.isEmpty()) { + return Collections.emptyList(); + } + + // 按设备分组并应用限制 + List filteredSourceEntities = sourceEntities.stream() + .sorted(Comparator.comparing(SourceEntity::getCreateTime).reversed()) + .collect(Collectors.groupingBy(SourceEntity::getDeviceId)) + .entrySet() + .stream().flatMap(entry -> { + DeviceConfigManager configManager = deviceRepository.getDeviceConfigManager(entry.getKey()); + if (configManager.getInteger("limit_video", 0) > 0) { + // 优先保留所有图片,然后限制视频数量 + return Stream.concat( + entry.getValue().stream().filter(item -> item.getType() == 2), + entry.getValue().stream().filter(item -> item.getType() == 1) + .limit(Math.min(entry.getValue().size(), configManager.getInteger("limit_video", 0))) + ); + } + return entry.getValue().stream(); + }).toList(); + + // 创建MemberSourceEntity列表 + return filteredSourceEntities.stream().map(sourceEntity -> { + DeviceConfigManager deviceConfig = deviceRepository.getDeviceConfigManager(sourceEntity.getDeviceId()); + MemberSourceEntity memberSourceEntity = new MemberSourceEntity(); + memberSourceEntity.setScenicId(face.getScenicId()); + memberSourceEntity.setFaceId(face.getId()); + memberSourceEntity.setMemberId(face.getMemberId()); + memberSourceEntity.setSourceId(sourceEntity.getId()); + memberSourceEntity.setType(sourceEntity.getType()); + + // 设置免费状态 - 默认收费 + memberSourceEntity.setIsFree(0); + + if (deviceConfig != null) { + // 视频类型检查 + if (sourceEntity.getType() == 1) { + if (Integer.valueOf(1).equals(deviceConfig.getInteger("video_free"))) { + memberSourceEntity.setIsFree(1); + } + } + // 图片类型检查 + else if (sourceEntity.getType() == 2) { + if (Integer.valueOf(1).equals(deviceConfig.getInteger("image_free"))) { + memberSourceEntity.setIsFree(1); + } + } + } + + return memberSourceEntity; + }).collect(Collectors.toList()); + } + + /** + * 处理免费源文件逻辑 + * 根据景区配置和是否新用户决定哪些照片可以免费 + * + * @param memberSourceEntityList 源文件关联列表 + * @param scenicConfig 景区配置 + * @param isNew 是否新用户 + * @return 免费的源文件ID列表 + */ + public List processFreeSourceLogic(List memberSourceEntityList, + ScenicConfigManager scenicConfig, + boolean isNew) { + List freeSourceIds = new ArrayList<>(); + + if (memberSourceEntityList.isEmpty()) { + return freeSourceIds; + } + + if (isNew) { + // 新用户送照片逻辑 + List photoSource = memberSourceEntityList.stream() + .filter(item -> item.getIsFree() == 0) // 只考虑收费的 + .filter(item -> Integer.valueOf(2).equals(item.getType())) // 只考虑照片类型 + .toList(); + + Integer photoFreeNum = scenicConfig != null ? scenicConfig.getInteger("photo_free_num") : null; + if (scenicConfig != null && photoFreeNum != null && photoFreeNum > 0) { + + int freePhotoCount = Math.min(photoFreeNum, photoSource.size()); + freeSourceIds.addAll(photoSource.stream() + .limit(freePhotoCount) + .map(MemberSourceEntity::getSourceId) + .toList()); + + log.debug("新用户免费照片逻辑:配置免费数量 {}, 实际可用 {}, 赠送 {} 张", + photoFreeNum, photoSource.size(), freePhotoCount); + } + } + + return freeSourceIds; + } +} diff --git a/src/main/java/com/ycwl/basic/service/pc/processor/VideoRecreationHandler.java b/src/main/java/com/ycwl/basic/service/pc/processor/VideoRecreationHandler.java new file mode 100644 index 00000000..2787370a --- /dev/null +++ b/src/main/java/com/ycwl/basic/service/pc/processor/VideoRecreationHandler.java @@ -0,0 +1,113 @@ +package com.ycwl.basic.service.pc.processor; + +import com.ycwl.basic.integration.common.manager.DeviceConfigManager; +import com.ycwl.basic.integration.common.manager.ScenicConfigManager; +import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity; +import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity; +import com.ycwl.basic.repository.DeviceRepository; +import com.ycwl.basic.repository.FaceRepository; +import com.ycwl.basic.task.VideoPieceGetter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 视频重切处理器 + * 负责处理视频重切任务的创建逻辑 + * + * @author longbinbin + * @date 2025-01-31 + */ +@Slf4j +@Component +public class VideoRecreationHandler { + + @Autowired + private FaceRepository faceRepository; + + @Autowired + private DeviceRepository deviceRepository; + + /** + * 处理视频重切逻辑 + * 当非新用户且照片数量大于视频数量时,创建视频重切任务 + * + * @param scenicConfig 景区配置 + * @param memberSourceEntityList 源文件关联列表 + * @param faceId 人脸ID + * @param memberId 会员ID + * @param sampleListIds 样本ID列表(用于日志) + * @param isNew 是否新用户 + */ + public void handleVideoRecreation(ScenicConfigManager scenicConfig, + List memberSourceEntityList, + Long faceId, + Long memberId, + List sampleListIds, + boolean isNew) { + // 新用户不执行视频重切逻辑 + if (isNew) { + return; + } + + // 检查景区是否禁用源视频功能 + Boolean disableSourceVideo = scenicConfig != null ? scenicConfig.getBoolean("disable_source_video") : null; + if (scenicConfig == null || Boolean.TRUE.equals(disableSourceVideo)) { + log.debug("视频重切逻辑跳过:景区禁用了源视频功能"); + return; + } + + // 统计视频和照片数量 + long videoCount = memberSourceEntityList.stream() + .filter(item -> Integer.valueOf(1).equals(item.getType())) + .count(); + long photoCount = memberSourceEntityList.stream() + .filter(item -> Integer.valueOf(2).equals(item.getType())) + .count(); + + List faceSampleList = faceRepository.getFaceSampleList(faceId); + if (faceSampleList.isEmpty()) { + log.info("faceId:{} sample list not exist", faceId); + return; + } + + // 筛选样本ID + List faceSampleIds = faceSampleList.stream() + .sorted(Comparator.comparing(FaceSampleEntity::getCreateAt).reversed()) + .collect(Collectors.groupingBy(FaceSampleEntity::getDeviceId)) + .entrySet() + .stream().flatMap(entry -> { + DeviceConfigManager configManager = deviceRepository.getDeviceConfigManager(entry.getKey()); + if (configManager.getInteger("limit_video", 0) > 0) { + return entry.getValue().subList(0, Math.min(entry.getValue().size(), configManager.getInteger("limit_video", 0))).stream(); + } + return entry.getValue().stream(); + }).toList() + .stream().map(FaceSampleEntity::getId).toList(); + + log.info("视频切分任务: faceId={}, 原始数量={}, 筛选后数量={}", faceId, faceSampleList.size(), faceSampleIds.size()); + log.debug("视频重切逻辑:视频数量 {}, 照片数量 {}", videoCount, photoCount); + + // 只有照片数量大于视频数量时才创建重切任务 + if (photoCount > videoCount) { + VideoPieceGetter.Task task = new VideoPieceGetter.Task(); + task.faceId = faceId; + task.faceSampleIds = faceSampleIds; + task.templateId = null; + task.memberId = memberId; + task.callback = () -> { + log.info("视频重切任务回调: {}", task); + }; + + VideoPieceGetter.addTask(task); + log.debug("视频重切任务已创建:faceId={}, memberId={}, sampleIds={}", + faceId, memberId, sampleListIds.size()); + } else { + log.debug("视频重切逻辑跳过:照片数量({})未超过视频数量({})", photoCount, videoCount); + } + } +} From 4c10c1d9396fd949e0fba0e410e04fc2364af2eb Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Fri, 31 Oct 2025 18:44:43 +0800 Subject: [PATCH 3/4] 2 --- .../service/pc/helper/ScenicConfigFacade.java | 214 ++++++++++++++++++ .../service/pc/impl/FaceServiceImpl.java | 51 ++--- .../pc/processor/BuyStatusProcessor.java | 10 +- .../pc/processor/FaceRecoveryStrategy.java | 6 +- .../pc/processor/SourceRelationProcessor.java | 32 +-- .../pc/processor/VideoRecreationHandler.java | 16 +- 6 files changed, 273 insertions(+), 56 deletions(-) create mode 100644 src/main/java/com/ycwl/basic/service/pc/helper/ScenicConfigFacade.java diff --git a/src/main/java/com/ycwl/basic/service/pc/helper/ScenicConfigFacade.java b/src/main/java/com/ycwl/basic/service/pc/helper/ScenicConfigFacade.java new file mode 100644 index 00000000..7b429aad --- /dev/null +++ b/src/main/java/com/ycwl/basic/service/pc/helper/ScenicConfigFacade.java @@ -0,0 +1,214 @@ +package com.ycwl.basic.service.pc.helper; + +import com.ycwl.basic.repository.ScenicRepository; +import com.ycwl.basic.integration.common.manager.ScenicConfigManager; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * 景区配置门面类 + * 提供类型安全的配置访问方法,避免配置键的字符串硬编码 + * 集中管理所有景区配置项的访问逻辑 + * + * @author Claude + * @since 2025-10-31 + */ +@Slf4j +@Component +public class ScenicConfigFacade { + + @Autowired + private ScenicRepository scenicRepository; + + /** + * 获取景区配置管理器 + */ + private ScenicConfigManager getConfig(Long scenicId) { + return scenicRepository.getScenicConfigManager(scenicId); + } + + // ==================== 人脸识别相关配置 ==================== + + /** + * 是否优先手动选择人脸 + * 如果为 true,则不自动创建任务 + * + * @param scenicId 景区ID + * @return true-需要手动选择,false-自动创建任务 + */ + public boolean isFaceSelectFirst(Long scenicId) { + ScenicConfigManager config = getConfig(scenicId); + if (config == null) { + return false; + } + Boolean value = config.getBoolean("face_select_first"); + return Boolean.TRUE.equals(value); + } + + /** + * 获取自定义人脸匹配的最大次数 + * 用于限制用户手动重新匹配的次数 + * + * @param scenicId 景区ID + * @return 最大匹配次数,null 或 0 表示不限制 + */ + public Integer getFaceSelectMaxCount(Long scenicId) { + ScenicConfigManager config = getConfig(scenicId); + if (config == null) { + return null; + } + return config.getInteger("face_select_max_count"); + } + + /** + * 获取人脸分数低阈值 + * 低于此分数的匹配结果会被标记为低置信度 + * + * @param scenicId 景区ID + * @return 分数阈值,默认 30.0 + */ + public Float getFaceScoreLowThreshold(Long scenicId) { + ScenicConfigManager config = getConfig(scenicId); + if (config == null) { + return 30.0F; + } + return config.getFloat("face_score_low_threshold", 30.0F); + } + + /** + * 获取人脸检测辅助阈值 + * 当匹配结果数量少于此阈值时,会触发补救逻辑 + * + * @param scenicId 景区ID + * @return 辅助阈值,默认 0(不启用) + */ + public Integer getFaceDetectHelperThreshold(Long scenicId) { + ScenicConfigManager config = getConfig(scenicId); + if (config == null) { + return 0; + } + return config.getInteger("face_detect_helper_threshold", 0); + } + + /** + * 获取人脸选择后处理模式 + * 0-并集模式,1-交集模式 + * + * @param scenicId 景区ID + * @return 后处理模式,默认 0 + */ + public Integer getFaceSelectPostMode(Long scenicId) { + ScenicConfigManager config = getConfig(scenicId); + if (config == null) { + return 0; + } + return config.getInteger("face_select_post_mode", 0); + } + + /** + * 获取重新匹配模式 + * 用于决定在什么条件下需要重新进行人脸匹配 + * + * @param scenicId 景区ID + * @return 匹配模式,默认 0 + */ + public Integer getRematchMode(Long scenicId) { + ScenicConfigManager config = getConfig(scenicId); + if (config == null) { + return 0; + } + return config.getInteger("re_match_mode", 0); + } + + // ==================== 源文件相关配置 ==================== + + /** + * 是否禁用源图片功能 + * + * @param scenicId 景区ID + * @return true-禁用,false-启用 + */ + public boolean isDisableSourceImage(Long scenicId) { + ScenicConfigManager config = getConfig(scenicId); + if (config == null) { + return false; + } + Boolean value = config.getBoolean("disable_source_image"); + return Boolean.TRUE.equals(value); + } + + /** + * 是否禁用源视频功能 + * + * @param scenicId 景区ID + * @return true-禁用,false-启用 + */ + public boolean isDisableSourceVideo(Long scenicId) { + ScenicConfigManager config = getConfig(scenicId); + if (config == null) { + return false; + } + Boolean value = config.getBoolean("disable_source_video"); + return Boolean.TRUE.equals(value); + } + + /** + * 获取免费照片数量 + * 新用户首次识别时赠送的免费照片数量 + * + * @param scenicId 景区ID + * @return 免费照片数量,null 或 0 表示不赠送 + */ + public Integer getPhotoFreeNum(Long scenicId) { + ScenicConfigManager config = getConfig(scenicId); + if (config == null) { + return null; + } + return config.getInteger("photo_free_num"); + } + + // ==================== 游玩时间相关配置 ==================== + + /** + * 获取最大游玩时间(分钟) + * 用于判断照片是否在合理的游玩时间范围内 + * + * @param scenicId 景区ID + * @return 最大游玩时间(分钟),null 表示不限制 + */ + public Integer getTourTime(Long scenicId) { + ScenicConfigManager config = getConfig(scenicId); + if (config == null) { + return null; + } + return config.getInteger("tour_time"); + } + + /** + * 获取最小游玩时间(分钟) + * 用于判断照片是否在合理的游玩时间范围内 + * + * @param scenicId 景区ID + * @return 最小游玩时间(分钟),null 表示不限制 + */ + public Integer getTourMinTime(Long scenicId) { + ScenicConfigManager config = getConfig(scenicId); + if (config == null) { + return null; + } + return config.getInteger("tour_min_time"); + } + + /** + * 检查是否配置了游玩时间限制 + * + * @param scenicId 景区ID + * @return true-已配置,false-未配置 + */ + public boolean hasTourTimeConfig(Long scenicId) { + Integer maxTime = getTourTime(scenicId); + Integer minTime = getTourMinTime(scenicId); + return maxTime != null && minTime != null; + } +} 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 d230fb90..edf31df3 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 @@ -6,11 +6,9 @@ import com.github.pagehelper.PageInfo; import com.ycwl.basic.biz.OrderBiz; import com.ycwl.basic.biz.TemplateBiz; import com.ycwl.basic.constant.BaseContextHandler; -import com.ycwl.basic.enums.StatisticEnum; import com.ycwl.basic.exception.BaseException; import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter; import com.ycwl.basic.facebody.entity.SearchFaceResultItem; -import com.ycwl.basic.integration.common.manager.DeviceConfigManager; import com.ycwl.basic.mapper.FaceSampleMapper; import com.ycwl.basic.mapper.ProjectMapper; import com.ycwl.basic.mapper.SourceMapper; @@ -24,7 +22,6 @@ import com.ycwl.basic.model.mobile.face.FaceStatusResp; import com.ycwl.basic.model.mobile.goods.VideoTaskStatusVO; import com.ycwl.basic.model.mobile.order.IsBuyRespVO; import com.ycwl.basic.model.mobile.scenic.content.ContentPageVO; -import com.ycwl.basic.model.mobile.statistic.req.StatisticsRecordAddReq; import com.ycwl.basic.model.pc.device.entity.DeviceEntity; import com.ycwl.basic.model.pc.face.entity.FaceEntity; import com.ycwl.basic.model.mobile.face.FaceRecognitionUpdateReq; @@ -58,8 +55,10 @@ 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.constant.SourceType; import com.ycwl.basic.service.pc.helper.FaceMetricsRecorder; import com.ycwl.basic.service.pc.helper.SearchResultMerger; +import com.ycwl.basic.service.pc.helper.ScenicConfigFacade; import com.ycwl.basic.service.pc.processor.BuyStatusProcessor; import com.ycwl.basic.service.pc.processor.FaceRecoveryStrategy; import com.ycwl.basic.service.pc.processor.SourceRelationProcessor; @@ -73,12 +72,10 @@ import com.ycwl.basic.storage.StorageFactory; import com.ycwl.basic.storage.adapters.IStorageAdapter; import com.ycwl.basic.storage.enums.StorageAcl; import com.ycwl.basic.storage.utils.StorageUtil; -import com.ycwl.basic.task.VideoPieceGetter; import com.ycwl.basic.utils.*; import lombok.extern.slf4j.Slf4j; import org.apache.logging.log4j.util.Strings; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; @@ -86,10 +83,8 @@ import java.io.File; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collections; -import java.util.Comparator; import java.util.Date; import java.util.HashMap; -import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -98,14 +93,9 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.UUID; -import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.stream.Collectors; -import java.util.stream.Stream; -import static com.ycwl.basic.constant.FaceConstant.FACE_CUSTOM_MATCH_COUNT_PFX; -import static com.ycwl.basic.constant.FaceConstant.FACE_LOW_THRESHOLD_PFX; -import static com.ycwl.basic.constant.FaceConstant.FACE_RECOGNITION_COUNT_PFX; import static com.ycwl.basic.constant.FaceConstant.USER_FACE_DB_NAME; import static com.ycwl.basic.constant.StorageConstant.USER_FACE; @@ -138,8 +128,6 @@ public class FaceServiceImpl implements FaceService { @Autowired private ScenicService scenicService; @Autowired - private TemplateMapper templateMapper; - @Autowired private VideoRepository videoRepository; @Autowired private VideoTaskRepository videoTaskRepository; @@ -150,8 +138,6 @@ public class FaceServiceImpl implements FaceService { @Autowired private OrderMapper orderMapper; @Autowired - private RedisTemplate redisTemplate; - @Autowired private FaceSampleMapper faceSampleMapper; @Autowired private GoodsService goodsService; @@ -169,6 +155,8 @@ public class FaceServiceImpl implements FaceService { private SearchResultMerger resultMerger; @Autowired private RematchStrategyFactory rematchStrategyFactory; + @Autowired + private ScenicConfigFacade scenicConfigFacade; // 第二阶段的处理器 @Autowired @@ -374,12 +362,12 @@ public class FaceServiceImpl implements FaceService { if (!memberSourceEntityList.isEmpty()) { // 5. 业务逻辑处理:免费逻辑、购买状态、任务创建 - List freeSourceIds = sourceRelationProcessor.processFreeSourceLogic(memberSourceEntityList, scenicConfig, isNew); + List freeSourceIds = sourceRelationProcessor.processFreeSourceLogic(memberSourceEntityList, face.getScenicId(), isNew); buyStatusProcessor.processBuyStatus(memberSourceEntityList, freeSourceIds, face.getMemberId(), face.getScenicId(), faceId); // 处理视频重切逻辑 - videoRecreationHandler.handleVideoRecreation(scenicConfig, memberSourceEntityList, faceId, + videoRecreationHandler.handleVideoRecreation(face.getScenicId(), memberSourceEntityList, faceId, face.getMemberId(), sampleListIds, isNew); // 过滤已存在的关联关系和无效的source引用,防止数据不一致 @@ -395,8 +383,7 @@ public class FaceServiceImpl implements FaceService { memberRelationRepository.clearSCacheByFace(faceId); // 检查景区配置中的 face_select_first,如果为 true 则不自动创建任务 - Boolean faceSelectFirst = scenicConfig != null ? scenicConfig.getBoolean("face_select_first") : null; - if (!Boolean.TRUE.equals(faceSelectFirst)) { + if (!scenicConfigFacade.isFaceSelectFirst(face.getScenicId())) { taskTaskService.autoCreateTaskByFaceId(faceId); } else { log.debug("景区配置 face_select_first=true,跳过自动创建任务:faceId={}", faceId); @@ -573,8 +560,8 @@ public class FaceServiceImpl implements FaceService { sourceVideoContent.setGroup("直出原片"); sourceImageContent.setGroup("直出原片"); ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(face.getScenicId()); - if (!Boolean.TRUE.equals(scenicConfig.getBoolean("disable_source_image"))) { - IsBuyRespVO isBuyRespVO = orderBiz.isBuy(userId, face.getScenicId(), 2, faceId); + if (!scenicConfigFacade.isDisableSourceImage(face.getScenicId())) { + IsBuyRespVO isBuyRespVO = orderBiz.isBuy(userId, face.getScenicId(), SourceType.IMAGE.getCode(), faceId); sourceImageContent.setSourceType(isBuyRespVO.getGoodsType()); sourceImageContent.setContentId(isBuyRespVO.getGoodsId()); if (isBuyRespVO.isBuy()) { @@ -592,8 +579,8 @@ public class FaceServiceImpl implements FaceService { sourceImageContent.setFreeCount((int) freeCount); contentList.add(sourceImageContent); } - if (!Boolean.TRUE.equals(scenicConfig.getBoolean("disable_source_video"))) { - IsBuyRespVO isBuyRespVO = orderBiz.isBuy(userId, face.getScenicId(), 1, faceId); + if (!scenicConfigFacade.isDisableSourceVideo(face.getScenicId())) { + IsBuyRespVO isBuyRespVO = orderBiz.isBuy(userId, face.getScenicId(), SourceType.VIDEO.getCode(), faceId); sourceVideoContent.setSourceType(isBuyRespVO.getGoodsType()); sourceVideoContent.setContentId(isBuyRespVO.getGoodsId()); if (isBuyRespVO.isBuy()) { @@ -764,15 +751,15 @@ public class FaceServiceImpl implements FaceService { long customMatchCount = metricsRecorder.getCustomMatchCount(faceId); boolean hasLowThreshold = metricsRecorder.hasLowThreshold(faceId); - Integer faceSelectMaxCount = scenicConfig.getInteger("face_select_max_count"); + Integer faceSelectMaxCount = scenicConfigFacade.getFaceSelectMaxCount(face.getScenicId()); if (faceSelectMaxCount != null && faceSelectMaxCount > 0 && customMatchCount > faceSelectMaxCount) { log.debug("自定义人脸匹配次数超过限制:faceId={}, customMatchCount={}, limit={}", faceId, customMatchCount, faceSelectMaxCount); return false; } - Integer maxTourTime = scenicConfig.getInteger("tour_time"); - Integer minTourTime = scenicConfig.getInteger("tour_min_time"); + Integer maxTourTime = scenicConfigFacade.getTourTime(face.getScenicId()); + Integer minTourTime = scenicConfigFacade.getTourMinTime(face.getScenicId()); boolean tourMatch = false; if (maxTourTime != null && minTourTime != null) { if ((new Date().getTime()) - face.getCreateAt().getTime() < maxTourTime * 60 * 1000 @@ -811,7 +798,7 @@ public class FaceServiceImpl implements FaceService { } // 使用策略模式替换switch语句 - Integer mode = scenicConfig.getInteger("re_match_mode", 0); + Integer mode = scenicConfigFacade.getRematchMode(face.getScenicId()); RematchContext context = RematchContext.builder() .recognitionCount(recognitionCount) .hasLowThreshold(hasLowThreshold) @@ -839,7 +826,7 @@ public class FaceServiceImpl implements FaceService { if (scenicConfig == null) { return List.of(); } - Float lowThreshold = scenicConfig.getFloat("face_score_low_threshold", 30.0F); + Float lowThreshold = scenicConfigFacade.getFaceScoreLowThreshold(face.getScenicId()); List resultItems = JacksonUtil.fromJsonToList(matchResult, SearchFaceResultItem.class); if (resultItems == null || resultItems.isEmpty()) { return List.of(); @@ -893,7 +880,7 @@ public class FaceServiceImpl implements FaceService { } // 获取face_select_post_mode配置,默认为0(并集) - Integer faceSelectPostMode = scenicConfig != null ? scenicConfig.getInteger("face_select_post_mode", 0) : 0; + Integer faceSelectPostMode = scenicConfigFacade.getFaceSelectPostMode(face.getScenicId()); log.debug("face_select_post_mode配置值: {}", faceSelectPostMode); SearchFaceRespVo mergedResult; @@ -958,11 +945,11 @@ public class FaceServiceImpl implements FaceService { List memberSourceEntityList = sourceRelationProcessor.processMemberSources(sampleListIds, face); if (!memberSourceEntityList.isEmpty()) { - List freeSourceIds = sourceRelationProcessor.processFreeSourceLogic(memberSourceEntityList, scenicConfig, false); + List freeSourceIds = sourceRelationProcessor.processFreeSourceLogic(memberSourceEntityList, face.getScenicId(), false); buyStatusProcessor.processBuyStatus(memberSourceEntityList, freeSourceIds, face.getMemberId(), face.getScenicId(), faceId); - videoRecreationHandler.handleVideoRecreation(scenicConfig, memberSourceEntityList, faceId, + videoRecreationHandler.handleVideoRecreation(face.getScenicId(), memberSourceEntityList, faceId, face.getMemberId(), sampleListIds, false); List existingFiltered = sourceMapper.filterExistingRelations(memberSourceEntityList); diff --git a/src/main/java/com/ycwl/basic/service/pc/processor/BuyStatusProcessor.java b/src/main/java/com/ycwl/basic/service/pc/processor/BuyStatusProcessor.java index 39394f5d..811c8afb 100644 --- a/src/main/java/com/ycwl/basic/service/pc/processor/BuyStatusProcessor.java +++ b/src/main/java/com/ycwl/basic/service/pc/processor/BuyStatusProcessor.java @@ -1,6 +1,8 @@ package com.ycwl.basic.service.pc.processor; import com.ycwl.basic.biz.OrderBiz; +import com.ycwl.basic.constant.BuyStatus; +import com.ycwl.basic.constant.FreeStatus; import com.ycwl.basic.model.mobile.order.IsBuyRespVO; import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity; import lombok.extern.slf4j.Slf4j; @@ -51,17 +53,17 @@ public class BuyStatusProcessor { // 设置购买状态 if (isBuy.isBuy()) { // 如果用户买过 - memberSourceEntity.setIsBuy(1); + memberSourceEntity.setIsBuy(BuyStatus.BOUGHT.getCode()); } else if (isBuy.isFree()) { // 全免费逻辑 - memberSourceEntity.setIsBuy(1); + memberSourceEntity.setIsBuy(BuyStatus.BOUGHT.getCode()); } else { - memberSourceEntity.setIsBuy(0); + memberSourceEntity.setIsBuy(BuyStatus.NOT_BOUGHT.getCode()); } // 设置免费状态 if (freeSourceIds.contains(memberSourceEntity.getSourceId())) { - memberSourceEntity.setIsFree(1); + memberSourceEntity.setIsFree(FreeStatus.FREE.getCode()); } } diff --git a/src/main/java/com/ycwl/basic/service/pc/processor/FaceRecoveryStrategy.java b/src/main/java/com/ycwl/basic/service/pc/processor/FaceRecoveryStrategy.java index 5a9c610c..e288da0c 100644 --- a/src/main/java/com/ycwl/basic/service/pc/processor/FaceRecoveryStrategy.java +++ b/src/main/java/com/ycwl/basic/service/pc/processor/FaceRecoveryStrategy.java @@ -5,6 +5,7 @@ import com.ycwl.basic.integration.common.manager.ScenicConfigManager; import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity; import com.ycwl.basic.model.task.resp.SearchFaceRespVo; import com.ycwl.basic.repository.FaceRepository; +import com.ycwl.basic.service.pc.helper.ScenicConfigFacade; import com.ycwl.basic.service.task.TaskFaceService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -27,6 +28,9 @@ public class FaceRecoveryStrategy { @Autowired private TaskFaceService faceService; + @Autowired + private ScenicConfigFacade scenicConfigFacade; + /** * 执行人脸识别补救逻辑 * 当匹配结果数量少于阈值时,使用第一个匹配结果重新进行人脸搜索 @@ -51,7 +55,7 @@ public class FaceRecoveryStrategy { } // 检查是否需要执行补救逻辑 - Integer helperThreshold = scenicConfig.getInteger("face_detect_helper_threshold", 0); + Integer helperThreshold = scenicConfigFacade.getFaceDetectHelperThreshold(scenicId); if (helperThreshold == null || helperThreshold <= 0) { return originalResult; } diff --git a/src/main/java/com/ycwl/basic/service/pc/processor/SourceRelationProcessor.java b/src/main/java/com/ycwl/basic/service/pc/processor/SourceRelationProcessor.java index e33c6f08..bb02fa08 100644 --- a/src/main/java/com/ycwl/basic/service/pc/processor/SourceRelationProcessor.java +++ b/src/main/java/com/ycwl/basic/service/pc/processor/SourceRelationProcessor.java @@ -1,5 +1,7 @@ package com.ycwl.basic.service.pc.processor; +import com.ycwl.basic.constant.FreeStatus; +import com.ycwl.basic.constant.SourceType; import com.ycwl.basic.integration.common.manager.DeviceConfigManager; import com.ycwl.basic.integration.common.manager.ScenicConfigManager; import com.ycwl.basic.mapper.SourceMapper; @@ -7,6 +9,7 @@ import com.ycwl.basic.model.pc.face.entity.FaceEntity; import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity; import com.ycwl.basic.model.pc.source.entity.SourceEntity; import com.ycwl.basic.repository.DeviceRepository; +import com.ycwl.basic.service.pc.helper.ScenicConfigFacade; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -32,6 +35,9 @@ public class SourceRelationProcessor { @Autowired private DeviceRepository deviceRepository; + @Autowired + private ScenicConfigFacade scenicConfigFacade; + /** * 处理源文件关联逻辑 * 根据匹配的样本ID创建MemberSourceEntity列表 @@ -60,8 +66,8 @@ public class SourceRelationProcessor { if (configManager.getInteger("limit_video", 0) > 0) { // 优先保留所有图片,然后限制视频数量 return Stream.concat( - entry.getValue().stream().filter(item -> item.getType() == 2), - entry.getValue().stream().filter(item -> item.getType() == 1) + entry.getValue().stream().filter(item -> SourceType.isImage(item.getType())), + entry.getValue().stream().filter(item -> SourceType.isVideo(item.getType())) .limit(Math.min(entry.getValue().size(), configManager.getInteger("limit_video", 0))) ); } @@ -79,19 +85,19 @@ public class SourceRelationProcessor { memberSourceEntity.setType(sourceEntity.getType()); // 设置免费状态 - 默认收费 - memberSourceEntity.setIsFree(0); + memberSourceEntity.setIsFree(FreeStatus.PAID.getCode()); if (deviceConfig != null) { // 视频类型检查 - if (sourceEntity.getType() == 1) { + if (SourceType.isVideo(sourceEntity.getType())) { if (Integer.valueOf(1).equals(deviceConfig.getInteger("video_free"))) { - memberSourceEntity.setIsFree(1); + memberSourceEntity.setIsFree(FreeStatus.FREE.getCode()); } } // 图片类型检查 - else if (sourceEntity.getType() == 2) { + else if (SourceType.isImage(sourceEntity.getType())) { if (Integer.valueOf(1).equals(deviceConfig.getInteger("image_free"))) { - memberSourceEntity.setIsFree(1); + memberSourceEntity.setIsFree(FreeStatus.FREE.getCode()); } } } @@ -105,12 +111,12 @@ public class SourceRelationProcessor { * 根据景区配置和是否新用户决定哪些照片可以免费 * * @param memberSourceEntityList 源文件关联列表 - * @param scenicConfig 景区配置 + * @param scenicId 景区ID * @param isNew 是否新用户 * @return 免费的源文件ID列表 */ public List processFreeSourceLogic(List memberSourceEntityList, - ScenicConfigManager scenicConfig, + Long scenicId, boolean isNew) { List freeSourceIds = new ArrayList<>(); @@ -121,12 +127,12 @@ public class SourceRelationProcessor { if (isNew) { // 新用户送照片逻辑 List photoSource = memberSourceEntityList.stream() - .filter(item -> item.getIsFree() == 0) // 只考虑收费的 - .filter(item -> Integer.valueOf(2).equals(item.getType())) // 只考虑照片类型 + .filter(item -> FreeStatus.isPaid(item.getIsFree())) // 只考虑收费的 + .filter(item -> SourceType.isImage(item.getType())) // 只考虑照片类型 .toList(); - Integer photoFreeNum = scenicConfig != null ? scenicConfig.getInteger("photo_free_num") : null; - if (scenicConfig != null && photoFreeNum != null && photoFreeNum > 0) { + Integer photoFreeNum = scenicConfigFacade.getPhotoFreeNum(scenicId); + if (photoFreeNum != null && photoFreeNum > 0) { int freePhotoCount = Math.min(photoFreeNum, photoSource.size()); freeSourceIds.addAll(photoSource.stream() diff --git a/src/main/java/com/ycwl/basic/service/pc/processor/VideoRecreationHandler.java b/src/main/java/com/ycwl/basic/service/pc/processor/VideoRecreationHandler.java index 2787370a..c34593ed 100644 --- a/src/main/java/com/ycwl/basic/service/pc/processor/VideoRecreationHandler.java +++ b/src/main/java/com/ycwl/basic/service/pc/processor/VideoRecreationHandler.java @@ -1,11 +1,13 @@ package com.ycwl.basic.service.pc.processor; +import com.ycwl.basic.constant.SourceType; import com.ycwl.basic.integration.common.manager.DeviceConfigManager; import com.ycwl.basic.integration.common.manager.ScenicConfigManager; import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity; import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity; import com.ycwl.basic.repository.DeviceRepository; import com.ycwl.basic.repository.FaceRepository; +import com.ycwl.basic.service.pc.helper.ScenicConfigFacade; import com.ycwl.basic.task.VideoPieceGetter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -32,18 +34,21 @@ public class VideoRecreationHandler { @Autowired private DeviceRepository deviceRepository; + @Autowired + private ScenicConfigFacade scenicConfigFacade; + /** * 处理视频重切逻辑 * 当非新用户且照片数量大于视频数量时,创建视频重切任务 * - * @param scenicConfig 景区配置 + * @param scenicId 景区ID * @param memberSourceEntityList 源文件关联列表 * @param faceId 人脸ID * @param memberId 会员ID * @param sampleListIds 样本ID列表(用于日志) * @param isNew 是否新用户 */ - public void handleVideoRecreation(ScenicConfigManager scenicConfig, + public void handleVideoRecreation(Long scenicId, List memberSourceEntityList, Long faceId, Long memberId, @@ -55,18 +60,17 @@ public class VideoRecreationHandler { } // 检查景区是否禁用源视频功能 - Boolean disableSourceVideo = scenicConfig != null ? scenicConfig.getBoolean("disable_source_video") : null; - if (scenicConfig == null || Boolean.TRUE.equals(disableSourceVideo)) { + if (scenicConfigFacade.isDisableSourceVideo(scenicId)) { log.debug("视频重切逻辑跳过:景区禁用了源视频功能"); return; } // 统计视频和照片数量 long videoCount = memberSourceEntityList.stream() - .filter(item -> Integer.valueOf(1).equals(item.getType())) + .filter(item -> SourceType.isVideo(item.getType())) .count(); long photoCount = memberSourceEntityList.stream() - .filter(item -> Integer.valueOf(2).equals(item.getType())) + .filter(item -> SourceType.isImage(item.getType())) .count(); List faceSampleList = faceRepository.getFaceSampleList(faceId); From c1b9a42c73b3a0ffcdaccc8ed31b4a232bcbeaed Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Fri, 31 Oct 2025 21:04:10 +0800 Subject: [PATCH 4/4] 3 --- .../com/ycwl/basic/constant/BuyStatus.java | 71 ++++ .../com/ycwl/basic/constant/FreeStatus.java | 71 ++++ .../com/ycwl/basic/constant/SourceType.java | 71 ++++ .../service/pc/impl/FaceServiceImpl.java | 145 +------- .../FaceMatchingOrchestrator.java | 333 ++++++++++++++++++ 5 files changed, 562 insertions(+), 129 deletions(-) create mode 100644 src/main/java/com/ycwl/basic/constant/BuyStatus.java create mode 100644 src/main/java/com/ycwl/basic/constant/FreeStatus.java create mode 100644 src/main/java/com/ycwl/basic/constant/SourceType.java create mode 100644 src/main/java/com/ycwl/basic/service/pc/orchestrator/FaceMatchingOrchestrator.java diff --git a/src/main/java/com/ycwl/basic/constant/BuyStatus.java b/src/main/java/com/ycwl/basic/constant/BuyStatus.java new file mode 100644 index 00000000..bb609bda --- /dev/null +++ b/src/main/java/com/ycwl/basic/constant/BuyStatus.java @@ -0,0 +1,71 @@ +package com.ycwl.basic.constant; + +/** + * 购买状态枚举 + * 定义源文件的已购买和未购买两种状态 + * + * @author Claude + * @since 2025-10-31 + */ +public enum BuyStatus { + /** + * 未购买状态 + */ + NOT_BOUGHT(0, "未购买"), + + /** + * 已购买状态 + */ + BOUGHT(1, "已购买"); + + private final int code; + private final String description; + + BuyStatus(int code, String description) { + this.code = code; + this.description = description; + } + + public int getCode() { + return code; + } + + public String getDescription() { + return description; + } + + /** + * 根据代码值获取枚举 + * + * @param code 状态代码 + * @return 对应的枚举值,如果不存在返回 null + */ + public static BuyStatus fromCode(int code) { + for (BuyStatus status : values()) { + if (status.code == code) { + return status; + } + } + return null; + } + + /** + * 判断给定的代码是否为已购买状态 + * + * @param code 状态代码 + * @return true-已购买,false-未购买 + */ + public static boolean isBought(Integer code) { + return code != null && code == BOUGHT.code; + } + + /** + * 判断给定的代码是否为未购买状态 + * + * @param code 状态代码 + * @return true-未购买,false-已购买 + */ + public static boolean isNotBought(Integer code) { + return code != null && code == NOT_BOUGHT.code; + } +} diff --git a/src/main/java/com/ycwl/basic/constant/FreeStatus.java b/src/main/java/com/ycwl/basic/constant/FreeStatus.java new file mode 100644 index 00000000..803d48b5 --- /dev/null +++ b/src/main/java/com/ycwl/basic/constant/FreeStatus.java @@ -0,0 +1,71 @@ +package com.ycwl.basic.constant; + +/** + * 免费状态枚举 + * 定义源文件的收费和免费两种状态 + * + * @author Claude + * @since 2025-10-31 + */ +public enum FreeStatus { + /** + * 收费状态 + */ + PAID(0, "收费"), + + /** + * 免费状态 + */ + FREE(1, "免费"); + + private final int code; + private final String description; + + FreeStatus(int code, String description) { + this.code = code; + this.description = description; + } + + public int getCode() { + return code; + } + + public String getDescription() { + return description; + } + + /** + * 根据代码值获取枚举 + * + * @param code 状态代码 + * @return 对应的枚举值,如果不存在返回 null + */ + public static FreeStatus fromCode(int code) { + for (FreeStatus status : values()) { + if (status.code == code) { + return status; + } + } + return null; + } + + /** + * 判断给定的代码是否为免费状态 + * + * @param code 状态代码 + * @return true-免费,false-收费 + */ + public static boolean isFree(Integer code) { + return code != null && code == FREE.code; + } + + /** + * 判断给定的代码是否为收费状态 + * + * @param code 状态代码 + * @return true-收费,false-免费 + */ + public static boolean isPaid(Integer code) { + return code != null && code == PAID.code; + } +} diff --git a/src/main/java/com/ycwl/basic/constant/SourceType.java b/src/main/java/com/ycwl/basic/constant/SourceType.java new file mode 100644 index 00000000..5e0cae14 --- /dev/null +++ b/src/main/java/com/ycwl/basic/constant/SourceType.java @@ -0,0 +1,71 @@ +package com.ycwl.basic.constant; + +/** + * 源文件类型枚举 + * 定义视频和图片两种源文件类型 + * + * @author Claude + * @since 2025-10-31 + */ +public enum SourceType { + /** + * 视频类型 + */ + VIDEO(1, "视频"), + + /** + * 图片类型 + */ + IMAGE(2, "图片"); + + private final int code; + private final String description; + + SourceType(int code, String description) { + this.code = code; + this.description = description; + } + + public int getCode() { + return code; + } + + public String getDescription() { + return description; + } + + /** + * 根据代码值获取枚举 + * + * @param code 类型代码 + * @return 对应的枚举值,如果不存在返回 null + */ + public static SourceType fromCode(int code) { + for (SourceType type : values()) { + if (type.code == code) { + return type; + } + } + return null; + } + + /** + * 判断给定的代码是否为视频类型 + * + * @param code 类型代码 + * @return true-是视频,false-不是视频 + */ + public static boolean isVideo(Integer code) { + return code != null && code == VIDEO.code; + } + + /** + * 判断给定的代码是否为图片类型 + * + * @param code 类型代码 + * @return true-是图片,false-不是图片 + */ + public static boolean isImage(Integer code) { + return code != null && code == IMAGE.code; + } +} 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 edf31df3..584485ec 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 @@ -59,6 +59,7 @@ import com.ycwl.basic.constant.SourceType; import com.ycwl.basic.service.pc.helper.FaceMetricsRecorder; import com.ycwl.basic.service.pc.helper.SearchResultMerger; import com.ycwl.basic.service.pc.helper.ScenicConfigFacade; +import com.ycwl.basic.service.pc.orchestrator.FaceMatchingOrchestrator; import com.ycwl.basic.service.pc.processor.BuyStatusProcessor; import com.ycwl.basic.service.pc.processor.FaceRecoveryStrategy; import com.ycwl.basic.service.pc.processor.SourceRelationProcessor; @@ -158,6 +159,10 @@ public class FaceServiceImpl implements FaceService { @Autowired private ScenicConfigFacade scenicConfigFacade; + // 编排器 + @Autowired + private FaceMatchingOrchestrator faceMatchingOrchestrator; + // 第二阶段的处理器 @Autowired private SourceRelationProcessor sourceRelationProcessor; @@ -291,164 +296,46 @@ public class FaceServiceImpl implements FaceService { @Override public SearchFaceRespVo matchFaceId(Long faceId) { - return matchFaceId(faceId, false); + return faceMatchingOrchestrator.orchestrateMatching(faceId, false); } @Override public SearchFaceRespVo matchFaceId(Long faceId, boolean isNew) { - if (faceId == null) { - throw new IllegalArgumentException("faceId 不能为空"); - } - // 1. 数据准备:获取人脸信息、景区配置、适配器等 - FaceEntity face = faceRepository.getFace(faceId); - if (face == null) { - log.warn("人脸不存在,faceId: {}", faceId); - return null; - } - - if (!isNew && Integer.valueOf(1).equals(face.getIsManual())) { - log.info("人工选择的,无需匹配,faceId: {}", faceId); - return null; - } - log.debug("开始人脸匹配:faceId={}, isNew={}", faceId, isNew); - - // 记录识别次数到Redis,设置2天过期时间 - metricsRecorder.recordRecognitionCount(faceId); - - try { - - ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(face.getScenicId()); - IFaceBodyAdapter faceBodyAdapter = scenicService.getScenicFaceBodyAdapter(face.getScenicId()); - - if (faceBodyAdapter == null) { - log.error("无法获取人脸识别适配器,scenicId: {}", face.getScenicId()); - throw new BaseException("人脸识别服务不可用,请稍后再试"); - } - - // 2. 人脸识别:执行人脸搜索和补救逻辑 - SearchFaceRespVo scenicDbSearchResult; - try { - scenicDbSearchResult = faceService.searchFace(faceBodyAdapter, - String.valueOf(face.getScenicId()), - face.getFaceUrl(), - "人脸识别"); - } catch (Exception e) { - log.error("人脸识别服务调用失败,faceId={}, scenicId={}", faceId, face.getScenicId(), e); - throw new BaseException("人脸识别失败,请换一张试试把~"); - } - - if (scenicDbSearchResult == null) { - log.warn("人脸识别返回结果为空,faceId={}", faceId); - throw new BaseException("人脸识别失败,请换一张试试把~"); - } - - // 执行补救逻辑(如需要) - scenicDbSearchResult = faceRecoveryStrategy.executeFaceRecoveryLogic(scenicDbSearchResult, scenicConfig, - faceBodyAdapter, face.getScenicId()); - - // 3. 结果处理:更新人脸实体信息 - try { - updateFaceEntityResult(face, scenicDbSearchResult, faceId); - } catch (Exception e) { - log.error("更新人脸结果失败,faceId={}", faceId, e); - throw new BaseException("保存人脸识别结果失败"); - } - - List sampleListIds = scenicDbSearchResult.getSampleListIds(); - if (sampleListIds != null && !sampleListIds.isEmpty()) { - try { - // 4. 源文件关联:处理匹配到的源文件 - List memberSourceEntityList = sourceRelationProcessor.processMemberSources(sampleListIds, face); - - if (!memberSourceEntityList.isEmpty()) { - // 5. 业务逻辑处理:免费逻辑、购买状态、任务创建 - List freeSourceIds = sourceRelationProcessor.processFreeSourceLogic(memberSourceEntityList, face.getScenicId(), isNew); - buyStatusProcessor.processBuyStatus(memberSourceEntityList, freeSourceIds, face.getMemberId(), - face.getScenicId(), faceId); - - // 处理视频重切逻辑 - videoRecreationHandler.handleVideoRecreation(face.getScenicId(), memberSourceEntityList, faceId, - face.getMemberId(), sampleListIds, isNew); - - // 过滤已存在的关联关系和无效的source引用,防止数据不一致 - List existingFiltered = sourceMapper.filterExistingRelations(memberSourceEntityList); - List validFiltered = sourceMapper.filterValidSourceRelations(existingFiltered); - if (!validFiltered.isEmpty()) { - sourceMapper.addRelations(validFiltered); - log.debug("创建关联关系: faceId={}, 原始数量={}, 过滤后数量={}", - faceId, memberSourceEntityList.size(), validFiltered.size()); - } else { - log.warn("没有有效的关联关系可创建: faceId={}, 原始数量={}", faceId, memberSourceEntityList.size()); - } - memberRelationRepository.clearSCacheByFace(faceId); - - // 检查景区配置中的 face_select_first,如果为 true 则不自动创建任务 - if (!scenicConfigFacade.isFaceSelectFirst(face.getScenicId())) { - taskTaskService.autoCreateTaskByFaceId(faceId); - } else { - log.debug("景区配置 face_select_first=true,跳过自动创建任务:faceId={}", faceId); - } - - log.info("人脸匹配完成:faceId={}, 匹配样本数={}, 关联源文件数={}, 免费数={}", - faceId, sampleListIds.size(), memberSourceEntityList.size(), freeSourceIds.size()); - } - } catch (Exception e) { - log.error("处理源文件关联失败,faceId={}", faceId, e); - // 源文件关联失败不影响主流程,记录错误但不抛出异常 - } - } else { - log.warn("人脸匹配无结果:faceId={}", faceId); - - // 检查低阈值检测结果,如果为true则记录该人脸ID到Redis - if (scenicDbSearchResult != null && scenicDbSearchResult.isLowThreshold()) { - metricsRecorder.recordLowThreshold(faceId); - log.debug("触发低阈值检测,记录faceId: {}", faceId); - } - } - - return scenicDbSearchResult; - - } catch (BaseException e) { - // 业务异常直接抛出 - throw e; - } catch (Exception e) { - log.error("人脸匹配处理异常,faceId={}, isNew={}", faceId, isNew, e); - throw new BaseException("人脸匹配处理失败,请稍后重试"); - } + return faceMatchingOrchestrator.orchestrateMatching(faceId, isNew); } /** * 更新人脸实体结果信息 + * 仅用于 handleCustomFaceMatching 方法 */ private void updateFaceEntityResult(FaceEntity originalFace, SearchFaceRespVo searchResult, Long faceId) { FaceEntity faceEntity = new FaceEntity(); faceEntity.setId(faceId); faceEntity.setScore(searchResult.getScore()); faceEntity.setMatchResult(searchResult.getSearchResultJson()); - + if (searchResult.getFirstMatchRate() != null) { faceEntity.setFirstMatchRate(BigDecimal.valueOf(searchResult.getFirstMatchRate())); } - + if (searchResult.getSampleListIds() != null) { faceEntity.setMatchSampleIds(searchResult.getSampleListIds().stream() .map(String::valueOf) .collect(Collectors.joining(","))); } - + faceEntity.setCreateAt(new Date()); faceEntity.setScenicId(originalFace.getScenicId()); faceEntity.setMemberId(originalFace.getMemberId()); faceEntity.setFaceUrl(originalFace.getFaceUrl()); - + faceMapper.update(faceEntity); faceRepository.clearFaceCache(faceEntity.getId()); - - log.debug("人脸结果更新完成:faceId={}, score={}, 匹配数={}", - faceId, searchResult.getScore(), - searchResult.getSampleListIds() != null ? searchResult.getSampleListIds().size() : 0); - } + log.debug("人脸结果更新完成:faceId={}, score={}, 匹配数={}", + faceId, searchResult.getScore(), + searchResult.getSampleListIds() != null ? searchResult.getSampleListIds().size() : 0); + } @Override public ApiResponse deleteFace(Long faceId) { diff --git a/src/main/java/com/ycwl/basic/service/pc/orchestrator/FaceMatchingOrchestrator.java b/src/main/java/com/ycwl/basic/service/pc/orchestrator/FaceMatchingOrchestrator.java new file mode 100644 index 00000000..f6a19337 --- /dev/null +++ b/src/main/java/com/ycwl/basic/service/pc/orchestrator/FaceMatchingOrchestrator.java @@ -0,0 +1,333 @@ +package com.ycwl.basic.service.pc.orchestrator; + +import com.ycwl.basic.biz.OrderBiz; +import com.ycwl.basic.exception.BaseException; +import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter; +import com.ycwl.basic.integration.common.manager.ScenicConfigManager; +import com.ycwl.basic.mapper.FaceMapper; +import com.ycwl.basic.mapper.SourceMapper; +import com.ycwl.basic.model.pc.face.entity.FaceEntity; +import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity; +import com.ycwl.basic.model.task.resp.SearchFaceRespVo; +import com.ycwl.basic.repository.FaceRepository; +import com.ycwl.basic.repository.MemberRelationRepository; +import com.ycwl.basic.repository.ScenicRepository; +import com.ycwl.basic.service.pc.ScenicService; +import com.ycwl.basic.service.pc.helper.FaceMetricsRecorder; +import com.ycwl.basic.service.pc.helper.ScenicConfigFacade; +import com.ycwl.basic.service.pc.processor.BuyStatusProcessor; +import com.ycwl.basic.service.pc.processor.FaceRecoveryStrategy; +import com.ycwl.basic.service.pc.processor.SourceRelationProcessor; +import com.ycwl.basic.service.pc.processor.VideoRecreationHandler; +import com.ycwl.basic.service.task.TaskFaceService; +import com.ycwl.basic.service.task.TaskService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 人脸匹配流程编排器 + * 将 matchFaceId 的复杂流程拆分为清晰的步骤,每个步骤负责单一职责 + * + * 主要步骤: + * 1. 数据准备 - 获取人脸信息、配置、适配器 + * 2. 人脸识别 - 执行人脸搜索和补救逻辑 + * 3. 结果更新 - 更新人脸实体信息 + * 4. 源文件关联 - 处理匹配到的源文件 + * 5. 业务处理 - 免费逻辑、购买状态、任务创建 + * 6. 数据持久化 - 保存关联关系 + * + * @author Claude + * @since 2025-10-31 + */ +@Slf4j +@Component +public class FaceMatchingOrchestrator { + + @Autowired + private FaceRepository faceRepository; + @Autowired + private FaceMapper faceMapper; + @Autowired + private ScenicRepository scenicRepository; + @Autowired + private ScenicService scenicService; + @Autowired + private TaskFaceService taskFaceService; + @Autowired + private TaskService taskService; + @Autowired + private SourceMapper sourceMapper; + @Autowired + private MemberRelationRepository memberRelationRepository; + + // 辅助类和处理器 + @Autowired + private FaceMetricsRecorder metricsRecorder; + @Autowired + private ScenicConfigFacade scenicConfigFacade; + @Autowired + private FaceRecoveryStrategy faceRecoveryStrategy; + @Autowired + private SourceRelationProcessor sourceRelationProcessor; + @Autowired + private BuyStatusProcessor buyStatusProcessor; + @Autowired + private VideoRecreationHandler videoRecreationHandler; + + /** + * 编排人脸匹配的完整流程 + * + * @param faceId 人脸ID + * @param isNew 是否新用户 + * @return 人脸搜索结果 + */ + public SearchFaceRespVo orchestrateMatching(Long faceId, boolean isNew) { + if (faceId == null) { + throw new IllegalArgumentException("faceId 不能为空"); + } + + // 步骤1: 数据准备 + MatchingContext context = prepareMatchingContext(faceId, isNew); + if (context == null) { + return null; + } + + // 记录识别次数 + metricsRecorder.recordRecognitionCount(faceId); + + try { + // 步骤2: 人脸识别 + SearchFaceRespVo searchResult = executeFaceRecognition(context); + if (searchResult == null) { + log.warn("人脸识别返回结果为空,faceId={}", faceId); + throw new BaseException("人脸识别失败,请换一张试试把~"); + } + + // 执行补救逻辑 + searchResult = faceRecoveryStrategy.executeFaceRecoveryLogic( + searchResult, context.scenicConfig, context.faceBodyAdapter, context.face.getScenicId()); + + // 步骤3: 更新人脸结果 + updateFaceResult(context.face, searchResult, faceId); + + // 步骤4-6: 处理源文件关联和业务逻辑 + processSourceRelations(context, searchResult, faceId, isNew); + + return searchResult; + + } catch (BaseException e) { + throw e; + } catch (Exception e) { + log.error("人脸匹配流程异常,faceId={}", faceId, e); + throw new BaseException("人脸匹配处理失败"); + } + } + + /** + * 步骤1: 准备匹配上下文 + * 获取人脸信息、景区配置、人脸识别适配器等必要数据 + */ + private MatchingContext prepareMatchingContext(Long faceId, boolean isNew) { + FaceEntity face = faceRepository.getFace(faceId); + if (face == null) { + log.warn("人脸不存在,faceId: {}", faceId); + return null; + } + + // 人工选择的无需重新匹配(新用户除外) + if (!isNew && Integer.valueOf(1).equals(face.getIsManual())) { + log.info("人工选择的,无需匹配,faceId: {}", faceId); + return null; + } + + log.debug("开始人脸匹配:faceId={}, isNew={}", faceId, isNew); + + ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(face.getScenicId()); + IFaceBodyAdapter faceBodyAdapter = scenicService.getScenicFaceBodyAdapter(face.getScenicId()); + + if (faceBodyAdapter == null) { + log.error("无法获取人脸识别适配器,scenicId: {}", face.getScenicId()); + throw new BaseException("人脸识别服务不可用,请稍后再试"); + } + + MatchingContext context = new MatchingContext(); + context.face = face; + context.scenicConfig = scenicConfig; + context.faceBodyAdapter = faceBodyAdapter; + return context; + } + + /** + * 步骤2: 执行人脸识别 + * 调用人脸识别服务进行人脸搜索 + */ + private SearchFaceRespVo executeFaceRecognition(MatchingContext context) { + try { + return taskFaceService.searchFace( + context.faceBodyAdapter, + String.valueOf(context.face.getScenicId()), + context.face.getFaceUrl(), + "人脸识别"); + } catch (Exception e) { + log.error("人脸识别服务调用失败,faceId={}, scenicId={}", + context.face.getId(), context.face.getScenicId(), e); + throw new BaseException("人脸识别失败,请换一张试试把~"); + } + } + + /** + * 步骤3: 更新人脸结果 + * 保存人脸匹配结果到数据库 + */ + private void updateFaceResult(FaceEntity originalFace, SearchFaceRespVo searchResult, Long faceId) { + try { + FaceEntity faceEntity = new FaceEntity(); + faceEntity.setId(faceId); + faceEntity.setScore(searchResult.getScore()); + faceEntity.setMatchResult(searchResult.getSearchResultJson()); + + if (searchResult.getFirstMatchRate() != null) { + faceEntity.setFirstMatchRate(BigDecimal.valueOf(searchResult.getFirstMatchRate())); + } + + if (searchResult.getSampleListIds() != null) { + faceEntity.setMatchSampleIds(searchResult.getSampleListIds().stream() + .map(String::valueOf) + .collect(Collectors.joining(","))); + } + + faceEntity.setCreateAt(new Date()); + faceEntity.setScenicId(originalFace.getScenicId()); + faceEntity.setMemberId(originalFace.getMemberId()); + faceEntity.setFaceUrl(originalFace.getFaceUrl()); + + faceMapper.update(faceEntity); + faceRepository.clearFaceCache(faceEntity.getId()); + + log.debug("人脸结果更新成功:faceId={}, score={}, sampleCount={}", + faceId, searchResult.getScore(), + searchResult.getSampleListIds() != null ? searchResult.getSampleListIds().size() : 0); + } catch (Exception e) { + log.error("更新人脸结果失败,faceId={}", faceId, e); + throw new BaseException("保存人脸识别结果失败"); + } + } + + /** + * 步骤4-6: 处理源文件关联和业务逻辑 + * 包括:源文件关联、免费逻辑、购买状态、视频重切、任务创建、数据持久化 + */ + private void processSourceRelations(MatchingContext context, SearchFaceRespVo searchResult, + Long faceId, boolean isNew) { + List sampleListIds = searchResult.getSampleListIds(); + + if (sampleListIds == null || sampleListIds.isEmpty()) { + log.warn("人脸匹配无结果:faceId={}", faceId); + + // 记录低阈值检测 + if (searchResult.isLowThreshold()) { + metricsRecorder.recordLowThreshold(faceId); + log.debug("触发低阈值检测,记录faceId: {}", faceId); + } + return; + } + + try { + // 4. 源文件关联:处理匹配到的源文件 + List memberSourceEntityList = + sourceRelationProcessor.processMemberSources(sampleListIds, context.face); + + if (memberSourceEntityList.isEmpty()) { + log.warn("未找到有效的源文件,faceId={}", faceId); + return; + } + + // 5. 业务逻辑处理 + processBusinessLogic(context, memberSourceEntityList, faceId, sampleListIds, isNew); + + // 6. 数据持久化 + persistSourceRelations(faceId, memberSourceEntityList); + + // 7. 任务创建 + createTaskIfNeeded(context.face.getScenicId(), faceId); + + log.info("人脸匹配完成:faceId={}, 匹配样本数={}, 关联源文件数={}", + faceId, sampleListIds.size(), memberSourceEntityList.size()); + + } catch (Exception e) { + log.error("处理源文件关联失败,faceId={}", faceId, e); + // 源文件关联失败不影响主流程,记录错误但不抛出异常 + } + } + + /** + * 步骤5: 业务逻辑处理 + * 处理免费逻辑、购买状态、视频重切 + */ + private void processBusinessLogic(MatchingContext context, List memberSourceEntityList, + Long faceId, List sampleListIds, boolean isNew) { + // 免费逻辑 + List freeSourceIds = sourceRelationProcessor.processFreeSourceLogic( + memberSourceEntityList, context.face.getScenicId(), isNew); + + // 购买状态 + buyStatusProcessor.processBuyStatus( + memberSourceEntityList, freeSourceIds, + context.face.getMemberId(), context.face.getScenicId(), faceId); + + // 视频重切 + videoRecreationHandler.handleVideoRecreation( + context.face.getScenicId(), memberSourceEntityList, + faceId, context.face.getMemberId(), sampleListIds, isNew); + } + + /** + * 步骤6: 数据持久化 + * 过滤并保存源文件关联关系 + */ + private void persistSourceRelations(Long faceId, List memberSourceEntityList) { + // 过滤已存在的关联关系和无效的source引用 + List existingFiltered = sourceMapper.filterExistingRelations(memberSourceEntityList); + List validFiltered = sourceMapper.filterValidSourceRelations(existingFiltered); + + if (!validFiltered.isEmpty()) { + sourceMapper.addRelations(validFiltered); + log.debug("创建关联关系: faceId={}, 原始数量={}, 过滤后数量={}", + faceId, memberSourceEntityList.size(), validFiltered.size()); + } else { + log.warn("没有有效的关联关系可创建: faceId={}, 原始数量={}", + faceId, memberSourceEntityList.size()); + } + + // 清除缓存 + memberRelationRepository.clearSCacheByFace(faceId); + } + + /** + * 步骤7: 任务创建 + * 根据配置决定是否自动创建任务 + */ + private void createTaskIfNeeded(Long scenicId, Long faceId) { + if (!scenicConfigFacade.isFaceSelectFirst(scenicId)) { + taskService.autoCreateTaskByFaceId(faceId); + } else { + log.debug("景区配置 face_select_first=true,跳过自动创建任务:faceId={}", faceId); + } + } + + /** + * 匹配上下文 + * 封装匹配过程中需要的所有上下文信息 + */ + private static class MatchingContext { + FaceEntity face; + ScenicConfigManager scenicConfig; + IFaceBodyAdapter faceBodyAdapter; + } +}