You've already forked FrameTour-BE
3
This commit is contained in:
71
src/main/java/com/ycwl/basic/constant/BuyStatus.java
Normal file
71
src/main/java/com/ycwl/basic/constant/BuyStatus.java
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
71
src/main/java/com/ycwl/basic/constant/FreeStatus.java
Normal file
71
src/main/java/com/ycwl/basic/constant/FreeStatus.java
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
71
src/main/java/com/ycwl/basic/constant/SourceType.java
Normal file
71
src/main/java/com/ycwl/basic/constant/SourceType.java
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<Long> sampleListIds = scenicDbSearchResult.getSampleListIds();
|
||||
if (sampleListIds != null && !sampleListIds.isEmpty()) {
|
||||
try {
|
||||
// 4. 源文件关联:处理匹配到的源文件
|
||||
List<MemberSourceEntity> memberSourceEntityList = sourceRelationProcessor.processMemberSources(sampleListIds, face);
|
||||
|
||||
if (!memberSourceEntityList.isEmpty()) {
|
||||
// 5. 业务逻辑处理:免费逻辑、购买状态、任务创建
|
||||
List<Long> 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<MemberSourceEntity> existingFiltered = sourceMapper.filterExistingRelations(memberSourceEntityList);
|
||||
List<MemberSourceEntity> 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<String> deleteFace(Long faceId) {
|
||||
|
||||
@@ -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<Long> 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<MemberSourceEntity> 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<MemberSourceEntity> memberSourceEntityList,
|
||||
Long faceId, List<Long> sampleListIds, boolean isNew) {
|
||||
// 免费逻辑
|
||||
List<Long> 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<MemberSourceEntity> memberSourceEntityList) {
|
||||
// 过滤已存在的关联关系和无效的source引用
|
||||
List<MemberSourceEntity> existingFiltered = sourceMapper.filterExistingRelations(memberSourceEntityList);
|
||||
List<MemberSourceEntity> 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user