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] 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; + } +}