feat(face-pipeline): 实现人脸匹配管线核心框架

- 新增Stage配置注解@StageConfig,支持Stage元数据和可选性控制
- 创建抽象基类AbstractFaceMatchingStage,提供Stage执行模板方法
- 实现FaceMatchingContext上下文类,用于Stage间状态和数据传递
- 构建Pipeline核心执行类,支持Stage动态添加和执行控制
- 添加PipelineBuilder构建器,支持链式组装管线
- 定义PipelineStage接口和StageResult结果类,规范Stage行为
- 新增人脸匹配场景枚举FaceMatchingScene和Stage可选模式枚举
- 创建管线异常类PipelineException和StageExecutionException
- 实现FaceMatchingPipelineFactory工厂类,支持多场景管线组装
- 添加拼图生成编排器PuzzleGenerationOrchestrator,支持异步批量生成
- 创建BuildSourceRelationStage等核心Stage实现类
This commit is contained in:
2025-12-03 18:06:43 +08:00
parent d2ad14175d
commit 96e75a458f
33 changed files with 3059 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
package com.ycwl.basic.face.pipeline.annotation;
import com.ycwl.basic.face.pipeline.enums.StageOptionalMode;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Stage配置注解
* 用于标注Stage的元数据和可选性控制
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface StageConfig {
/**
* Stage唯一标识
* 用于外部配置控制
*/
String stageId();
/**
* 可选模式
*/
StageOptionalMode optionalMode() default StageOptionalMode.UNSUPPORT;
/**
* 描述信息
*/
String description() default "";
/**
* 默认是否启用(当optionalMode=SUPPORT时生效)
*/
boolean defaultEnabled() default true;
}

View File

@@ -0,0 +1,97 @@
package com.ycwl.basic.face.pipeline.core;
import com.ycwl.basic.face.pipeline.annotation.StageConfig;
import com.ycwl.basic.face.pipeline.enums.StageOptionalMode;
import lombok.extern.slf4j.Slf4j;
/**
* 人脸匹配Pipeline Stage抽象基类
* 提供默认实现和通用逻辑
*
* @param <C> Context类型,必须继承FaceMatchingContext
*/
@Slf4j
public abstract class AbstractFaceMatchingStage<C extends FaceMatchingContext> implements PipelineStage<C> {
/**
* 最终的shouldExecute判断
* 整合了外部配置控制和业务逻辑判断
*/
@Override
public final boolean shouldExecute(C context) {
// 1. 检查Stage配置注解
StageConfig config = getStageConfig();
if (config != null) {
String stageId = config.stageId();
StageOptionalMode mode = config.optionalMode();
// FORCE_ON:强制执行,不检查外部配置
if (mode == StageOptionalMode.FORCE_ON) {
return shouldExecuteByBusinessLogic(context);
}
// SUPPORT:检查外部配置
if (mode == StageOptionalMode.SUPPORT) {
boolean externalEnabled = context.isStageEnabled(stageId, config.defaultEnabled());
if (!externalEnabled) {
log.debug("[{}] Stage被外部配置禁用", stageId);
return false;
}
}
// UNSUPPORT:不检查外部配置,直接走业务逻辑
}
// 2. 执行业务逻辑判断
return shouldExecuteByBusinessLogic(context);
}
/**
* 子类实现业务逻辑判断
* 默认总是执行
*
* 子类可以覆盖此方法实现条件性执行
* 例如: 只有新用户才设置任务状态, 只有匹配到样本才处理源文件关联等
*/
protected boolean shouldExecuteByBusinessLogic(C context) {
return true;
}
/**
* 模板方法:执行Stage前的准备工作
*/
protected void beforeExecute(C context) {
log.debug("[{}] 开始执行", getName());
}
/**
* 模板方法:执行Stage后的清理工作
*/
protected void afterExecute(C context, StageResult<C> result) {
if (result.isSuccess()) {
log.debug("[{}] 执行成功: {}", getName(), result.getMessage());
} else if (result.isSkipped()) {
log.debug("[{}] 已跳过: {}", getName(), result.getMessage());
} else if (result.isDegraded()) {
log.warn("[{}] 降级执行: {}", getName(), result.getMessage());
} else {
log.error("[{}] 执行失败: {}", getName(), result.getMessage(), result.getException());
}
}
/**
* 子类实现具体的处理逻辑
*/
protected abstract StageResult<C> doExecute(C context);
/**
* 最终执行方法(带钩子)
*/
@Override
public final StageResult<C> execute(C context) {
beforeExecute(context);
StageResult<C> result = doExecute(context);
afterExecute(context, result);
return result;
}
}

View File

@@ -0,0 +1,287 @@
package com.ycwl.basic.face.pipeline.core;
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
import com.ycwl.basic.face.pipeline.enums.FaceMatchingScene;
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
import com.ycwl.basic.model.task.resp.SearchFaceRespVo;
import lombok.Getter;
import lombok.Setter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 人脸匹配管线上下文
* 在各个Stage之间传递状态和数据
*/
@Getter
@Setter
public class FaceMatchingContext {
// ==================== 核心字段(构造时必填)====================
/**
* 人脸ID(必填)
*/
private final Long faceId;
/**
* 是否新用户
*/
private final boolean isNew;
// ==================== 场景标识 ====================
/**
* 场景标识
*/
private FaceMatchingScene scene;
/**
* 手动选择的样本ID(自定义匹配场景)
*/
private List<Long> faceSampleIds;
// ==================== 中间状态 ====================
/**
* 人脸实体
*/
private FaceEntity face;
/**
* 景区配置管理器
*/
private ScenicConfigManager scenicConfig;
/**
* 人脸识别适配器
*/
private IFaceBodyAdapter faceBodyAdapter;
/**
* 人脸搜索结果
*/
private SearchFaceRespVo searchResult;
/**
* 人脸样本列表(自定义匹配场景)
*/
private List<FaceSampleEntity> faceSamples;
/**
* 匹配到的样本ID列表
*/
private List<Long> sampleListIds;
/**
* 源文件关联列表
*/
private List<MemberSourceEntity> memberSourceList;
/**
* 免费源文件ID列表
*/
private List<Long> freeSourceIds;
/**
* 人脸选择后置模式配置(自定义匹配场景)
* 0: 并集, 1: 交集, 2: 直接使用
*/
private Integer faceSelectPostMode;
// ==================== 输出结果 ====================
/**
* 最终结果
*/
private SearchFaceRespVo finalResult;
// ==================== Stage配置 ====================
/**
* Stage开关配置表
* Key: stageId, Value: 是否启用
*/
private Map<String, Boolean> stageEnabledMap = new HashMap<>();
// ==================== 构造函数(私有)====================
private FaceMatchingContext(Builder builder) {
this.faceId = builder.faceId;
this.isNew = builder.isNew;
this.scene = builder.scene;
this.faceSampleIds = builder.faceSampleIds;
}
// ==================== 静态工厂方法 ====================
/**
* 获取 Builder
*/
public static Builder builder() {
return new Builder();
}
/**
* 快速创建自动匹配场景Context
*/
public static FaceMatchingContext forAutoMatching(Long faceId, boolean isNew) {
return FaceMatchingContext.builder()
.faceId(faceId)
.isNew(isNew)
.scene(FaceMatchingScene.AUTO_MATCHING)
.build();
}
/**
* 快速创建自定义匹配场景Context
*/
public static FaceMatchingContext forCustomMatching(Long faceId, List<Long> faceSampleIds) {
return FaceMatchingContext.builder()
.faceId(faceId)
.isNew(false)
.faceSampleIds(faceSampleIds)
.scene(FaceMatchingScene.CUSTOM_MATCHING)
.build();
}
/**
* 快速创建仅识别场景Context
*/
public static FaceMatchingContext forRecognitionOnly(Long faceId) {
return FaceMatchingContext.builder()
.faceId(faceId)
.isNew(false)
.scene(FaceMatchingScene.RECOGNITION_ONLY)
.build();
}
// ==================== 业务方法 ====================
/**
* 判断指定Stage是否启用
*
* @param stageId Stage唯一标识
* @param defaultEnabled 默认值(当配置未指定时使用)
* @return true-启用, false-禁用
*/
public boolean isStageEnabled(String stageId, boolean defaultEnabled) {
return stageEnabledMap.getOrDefault(stageId, defaultEnabled);
}
/**
* 判断指定Stage是否启用(默认为false)
*
* @param stageId Stage唯一标识
* @return true-启用, false-禁用
*/
public boolean isStageEnabled(String stageId) {
return stageEnabledMap.getOrDefault(stageId, false);
}
/**
* 设置指定Stage的启用状态
*
* @param stageId Stage唯一标识
* @param enabled 是否启用
* @return this(支持链式调用)
*/
public FaceMatchingContext setStageState(String stageId, boolean enabled) {
stageEnabledMap.put(stageId, enabled);
return this;
}
/**
* 启用指定Stage
*
* @param stageId Stage唯一标识
* @return this(支持链式调用)
*/
public FaceMatchingContext enableStage(String stageId) {
stageEnabledMap.put(stageId, true);
return this;
}
/**
* 禁用指定Stage
*
* @param stageId Stage唯一标识
* @return this(支持链式调用)
*/
public FaceMatchingContext disableStage(String stageId) {
stageEnabledMap.put(stageId, false);
return this;
}
/**
* 批量设置Stage启用状态
*
* @param stages Stage配置Map(stageId -> enabled)
* @return this(支持链式调用)
*/
public FaceMatchingContext setStages(Map<String, Boolean> stages) {
if (stages != null) {
stageEnabledMap.putAll(stages);
}
return this;
}
/**
* 清空所有Stage配置
*
* @return this(支持链式调用)
*/
public FaceMatchingContext clearStages() {
stageEnabledMap.clear();
return this;
}
// ==================== Builder ====================
public static class Builder {
private Long faceId;
private boolean isNew = false;
private FaceMatchingScene scene;
private List<Long> faceSampleIds;
public Builder faceId(Long faceId) {
this.faceId = faceId;
return this;
}
public Builder isNew(boolean isNew) {
this.isNew = isNew;
return this;
}
public Builder scene(FaceMatchingScene scene) {
this.scene = scene;
return this;
}
public Builder faceSampleIds(List<Long> faceSampleIds) {
this.faceSampleIds = faceSampleIds;
return this;
}
public FaceMatchingContext build() {
// 参数校验
if (faceId == null) {
throw new IllegalArgumentException("faceId is required");
}
if (scene == null) {
throw new IllegalArgumentException("scene is required");
}
// 自定义匹配场景必须提供faceSampleIds
if (scene == FaceMatchingScene.CUSTOM_MATCHING && (faceSampleIds == null || faceSampleIds.isEmpty())) {
throw new IllegalArgumentException("faceSampleIds is required for CUSTOM_MATCHING scene");
}
return new FaceMatchingContext(this);
}
}
}

View File

@@ -0,0 +1,119 @@
package com.ycwl.basic.face.pipeline.core;
import com.ycwl.basic.face.pipeline.exception.PipelineException;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
/**
* 人脸匹配管线
* 按顺序执行一系列Stage
*
* @param <C> Context类型,必须继承FaceMatchingContext
*/
@Slf4j
public class Pipeline<C extends FaceMatchingContext> {
private final List<PipelineStage<C>> stages;
private final String name;
public Pipeline(String name, List<PipelineStage<C>> stages) {
this.name = name;
this.stages = new ArrayList<>(stages);
}
/**
* 执行管线
*
* @param context 管线上下文
* @return 执行成功返回true
* @throws PipelineException 管线执行异常
*/
public boolean execute(C context) {
log.info("[{}] 开始执行人脸匹配管线, Stage数量: {}", name, stages.size());
long startTime = System.currentTimeMillis();
int maxStages = 100; // 防止无限循环
int executedCount = 0;
try {
for (int i = 0; i < stages.size(); i++) {
if (executedCount >= maxStages) {
log.error("[{}] Stage执行数量超过最大限制({}),可能存在循环依赖", name, maxStages);
throw new PipelineException("Stage执行数量超过最大限制,可能存在循环依赖");
}
PipelineStage<C> stage = stages.get(i);
String stageName = stage.getName();
log.debug("[{}] [{}/{}] 准备执行Stage: {}", name, i + 1, stages.size(), stageName);
if (!stage.shouldExecute(context)) {
log.debug("[{}] Stage {} 条件不满足,跳过执行", name, stageName);
continue;
}
long stageStartTime = System.currentTimeMillis();
StageResult<C> result = stage.execute(context);
long stageDuration = System.currentTimeMillis() - stageStartTime;
executedCount++;
logStageResult(stageName, result, stageDuration);
// 动态添加后续Stage
if (result.getNextStages() != null && !result.getNextStages().isEmpty()) {
List<PipelineStage<C>> nextStages = result.getNextStages();
log.info("[{}] Stage {} 动态添加了 {} 个后续Stage", name, stageName, nextStages.size());
for (int j = 0; j < nextStages.size(); j++) {
PipelineStage<C> nextStage = nextStages.get(j);
stages.add(i + 1 + j, nextStage);
log.debug("[{}] - 插入Stage: {} 到位置 {}", name, nextStage.getName(), i + 1 + j);
}
}
if (result.isFailed()) {
log.error("[{}] Stage {} 执行失败,管线终止", name, stageName);
return false;
}
}
long totalDuration = System.currentTimeMillis() - startTime;
log.info("[{}] 人脸匹配管线执行完成, 总Stage数: {}, 实际执行: {}, 耗时: {}ms",
name, stages.size(), executedCount, totalDuration);
return true;
} catch (Exception e) {
log.error("[{}] 人脸匹配管线执行异常", name, e);
throw new PipelineException("管线执行失败: " + e.getMessage(), e);
}
}
private void logStageResult(String stageName, StageResult<C> result, long duration) {
String statusIcon = switch (result.getStatus()) {
case SUCCESS -> "";
case SKIPPED -> "";
case DEGRADED -> "";
case FAILED -> "";
};
log.info("[{}] {} Stage {} - {} (耗时: {}ms)",
name, statusIcon, stageName, result.getStatus(), duration);
if (result.getMessage() != null) {
log.debug("[{}] 详情: {}", name, result.getMessage());
}
}
public String getName() {
return name;
}
public int getStageCount() {
return stages.size();
}
public List<String> getStageNames() {
return stages.stream().map(PipelineStage::getName).toList();
}
}

View File

@@ -0,0 +1,80 @@
package com.ycwl.basic.face.pipeline.core;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
/**
* Pipeline构建器
* 使用Builder模式动态组装人脸匹配管线
*
* @param <C> Context类型,必须继承FaceMatchingContext
*/
public class PipelineBuilder<C extends FaceMatchingContext> {
private String name = "DefaultFaceMatchingPipeline";
private final List<PipelineStage<C>> stages = new ArrayList<>();
public PipelineBuilder() {
}
public PipelineBuilder(String name) {
this.name = name;
}
/**
* 设置管线名称
*/
public PipelineBuilder<C> name(String name) {
this.name = name;
return this;
}
/**
* 添加Stage
*/
public PipelineBuilder<C> addStage(PipelineStage<C> stage) {
if (stage != null) {
this.stages.add(stage);
}
return this;
}
/**
* 批量添加Stage
*/
public PipelineBuilder<C> addStages(List<PipelineStage<C>> stages) {
if (stages != null) {
this.stages.addAll(stages);
}
return this;
}
/**
* 条件性添加Stage
*/
public PipelineBuilder<C> addStageIf(boolean condition, PipelineStage<C> stage) {
if (condition && stage != null) {
this.stages.add(stage);
}
return this;
}
/**
* 按优先级排序Stage
*/
public PipelineBuilder<C> sortByPriority() {
this.stages.sort(Comparator.comparingInt(PipelineStage::getPriority));
return this;
}
/**
* 构建Pipeline
*/
public Pipeline<C> build() {
if (stages.isEmpty()) {
throw new IllegalStateException("人脸匹配管线至少需要一个Stage");
}
return new Pipeline<>(name, stages);
}
}

View File

@@ -0,0 +1,50 @@
package com.ycwl.basic.face.pipeline.core;
import com.ycwl.basic.face.pipeline.annotation.StageConfig;
/**
* 管线处理阶段接口
* 每个Stage负责一个独立的人脸匹配处理步骤
*
* @param <C> Context类型,必须继承FaceMatchingContext
*/
public interface PipelineStage<C extends FaceMatchingContext> {
/**
* 获取Stage名称(用于日志和监控)
*/
String getName();
/**
* 判断是否需要执行此Stage
* 支持条件性执行(如:只有新用户才设置任务状态)
*
* @param context 管线上下文
* @return true-执行, false-跳过
*/
boolean shouldExecute(C context);
/**
* 执行Stage处理逻辑
*
* @param context 管线上下文
* @return 执行结果
*/
StageResult<C> execute(C context);
/**
* 获取Stage的执行优先级(用于排序)
* 数值越小优先级越高,默认为100
*/
default int getPriority() {
return 100;
}
/**
* 获取Stage配置注解(用于反射读取可选性控制信息)
* @return Stage配置注解,如果未标注则返回null
*/
default StageConfig getStageConfig() {
return this.getClass().getAnnotation(StageConfig.class);
}
}

View File

@@ -0,0 +1,101 @@
package com.ycwl.basic.face.pipeline.core;
import lombok.Getter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* Stage执行结果
*
* @param <C> Context类型,必须继承FaceMatchingContext
*/
@Getter
public class StageResult<C extends FaceMatchingContext> {
public enum Status {
SUCCESS, // 执行成功
SKIPPED, // 跳过执行
FAILED, // 执行失败
DEGRADED // 降级执行
}
private final Status status;
private final String message;
private final Throwable exception;
private final List<PipelineStage<C>> nextStages;
private StageResult(Status status, String message, Throwable exception, List<PipelineStage<C>> nextStages) {
this.status = status;
this.message = message;
this.exception = exception;
this.nextStages = nextStages != null
? Collections.unmodifiableList(new ArrayList<>(nextStages))
: Collections.emptyList();
}
public static <C extends FaceMatchingContext> StageResult<C> success() {
return new StageResult<>(Status.SUCCESS, null, null, null);
}
public static <C extends FaceMatchingContext> StageResult<C> success(String message) {
return new StageResult<>(Status.SUCCESS, message, null, null);
}
/**
* 成功执行并动态添加后续Stage
*/
@SafeVarargs
public static <C extends FaceMatchingContext> StageResult<C> successWithNext(String message, PipelineStage<C>... stages) {
return new StageResult<>(Status.SUCCESS, message, null, Arrays.asList(stages));
}
/**
* 成功执行并动态添加后续Stage列表
*/
public static <C extends FaceMatchingContext> StageResult<C> successWithNext(String message, List<PipelineStage<C>> stages) {
return new StageResult<>(Status.SUCCESS, message, null, stages);
}
public static <C extends FaceMatchingContext> StageResult<C> skipped() {
return new StageResult<>(Status.SKIPPED, "条件不满足,跳过执行", null, null);
}
public static <C extends FaceMatchingContext> StageResult<C> skipped(String reason) {
return new StageResult<>(Status.SKIPPED, reason, null, null);
}
public static <C extends FaceMatchingContext> StageResult<C> failed(String message) {
return new StageResult<>(Status.FAILED, message, null, null);
}
public static <C extends FaceMatchingContext> StageResult<C> failed(String message, Throwable exception) {
return new StageResult<>(Status.FAILED, message, exception, null);
}
public static <C extends FaceMatchingContext> StageResult<C> degraded(String message) {
return new StageResult<>(Status.DEGRADED, message, null, null);
}
public boolean isSuccess() {
return status == Status.SUCCESS;
}
public boolean isSkipped() {
return status == Status.SKIPPED;
}
public boolean isFailed() {
return status == Status.FAILED;
}
public boolean isDegraded() {
return status == Status.DEGRADED;
}
public boolean canContinue() {
return status == Status.SUCCESS || status == Status.SKIPPED || status == Status.DEGRADED;
}
}

View File

@@ -0,0 +1,25 @@
package com.ycwl.basic.face.pipeline.enums;
/**
* 人脸匹配场景枚举
*/
public enum FaceMatchingScene {
/**
* 自动人脸匹配
* 新用户上传人脸后自动执行匹配,或老用户重新匹配
*/
AUTO_MATCHING,
/**
* 自定义人脸匹配
* 用户手动选择人脸样本进行匹配
*/
CUSTOM_MATCHING,
/**
* 仅识别
* 只执行人脸识别,不处理后续业务逻辑(源文件关联、任务创建等)
*/
RECOGNITION_ONLY
}

View File

@@ -0,0 +1,29 @@
package com.ycwl.basic.face.pipeline.enums;
/**
* Stage可选模式枚举
* 控制Stage是否支持外部配置
*/
public enum StageOptionalMode {
/**
* 强制执行
* 不检查外部配置,总是执行(除非业务逻辑判断跳过)
* 例如: PrepareContextStage、FaceRecognitionStage
*/
FORCE_ON,
/**
* 支持外部控制
* 检查外部配置来决定是否执行
* 例如: RecordMetricsStage、FaceRecoveryStage
*/
SUPPORT,
/**
* 不支持外部控制
* 完全由业务逻辑控制是否执行,不检查外部配置
* 例如: SourceRelationStage(需要sampleListIds不为空)
*/
UNSUPPORT
}

View File

@@ -0,0 +1,19 @@
package com.ycwl.basic.face.pipeline.exception;
/**
* Pipeline执行异常
*/
public class PipelineException extends RuntimeException {
public PipelineException(String message) {
super(message);
}
public PipelineException(String message, Throwable cause) {
super(message, cause);
}
public PipelineException(Throwable cause) {
super(cause);
}
}

View File

@@ -0,0 +1,23 @@
package com.ycwl.basic.face.pipeline.exception;
/**
* Stage执行异常
*/
public class StageExecutionException extends RuntimeException {
private final String stageName;
public StageExecutionException(String stageName, String message) {
super(String.format("[%s] %s", stageName, message));
this.stageName = stageName;
}
public StageExecutionException(String stageName, String message, Throwable cause) {
super(String.format("[%s] %s", stageName, message), cause);
this.stageName = stageName;
}
public String getStageName() {
return stageName;
}
}

View File

@@ -0,0 +1,234 @@
package com.ycwl.basic.face.pipeline.factory;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.face.pipeline.core.Pipeline;
import com.ycwl.basic.face.pipeline.core.PipelineBuilder;
import com.ycwl.basic.face.pipeline.enums.FaceMatchingScene;
import com.ycwl.basic.face.pipeline.stages.*;
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;
/**
* 人脸匹配Pipeline工厂
* 负责为不同场景组装Pipeline
*
* 支持的场景:
* 1. 自动人脸匹配(新用户/老用户)
* 2. 自定义人脸匹配
* 3. 仅识别
*/
@Slf4j
@Component
public class FaceMatchingPipelineFactory {
// ==================== 通用 Stage(13个)====================
@Autowired
private PrepareContextStage prepareContextStage;
@Autowired
private RecordMetricsStage recordMetricsStage;
@Autowired
private FaceRecognitionStage faceRecognitionStage;
@Autowired
private FaceRecoveryStage faceRecoveryStage;
@Autowired
private UpdateFaceResultStage updateFaceResultStage;
@Autowired
private BuildSourceRelationStage buildSourceRelationStage;
@Autowired
private ProcessFreeSourceStage processFreeSourceStage;
@Autowired
private ProcessBuyStatusStage processBuyStatusStage;
@Autowired
private HandleVideoRecreationStage handleVideoRecreationStage;
@Autowired
private PersistRelationsStage persistRelationsStage;
@Autowired
private CreateTaskStage createTaskStage;
@Autowired
private SetTaskStatusStage setTaskStatusStage;
@Autowired
private GeneratePuzzleStage generatePuzzleStage;
// ==================== 自定义匹配专属 Stage(6个)====================
@Autowired
private RecordCustomMatchMetricsStage recordCustomMatchMetricsStage;
@Autowired
private LoadFaceSamplesStage loadFaceSamplesStage;
@Autowired
private CustomFaceSearchStage customFaceSearchStage;
@Autowired
private LoadMatchedSamplesStage loadMatchedSamplesStage;
@Autowired
private FilterByTimeRangeStage filterByTimeRangeStage;
@Autowired
private FilterByDevicePhotoLimitStage filterByDevicePhotoLimitStage;
@Autowired
private DeleteOldRelationsStage deleteOldRelationsStage;
// ==================== 辅助服务 ====================
@Autowired
private ScenicConfigFacade scenicConfigFacade;
/**
* 创建自动人脸匹配Pipeline
*
* @param isNew 是否新用户
* @return Pipeline
*/
public Pipeline<FaceMatchingContext> createAutoMatchingPipeline(boolean isNew) {
PipelineBuilder<FaceMatchingContext> builder = new PipelineBuilder<>("AutoMatching-" + (isNew ? "New" : "Old"));
// 1. 准备上下文
builder.addStage(prepareContextStage);
// 2. 新用户设置任务状态
if (isNew) {
builder.addStage(setTaskStatusStage);
}
// 3. 记录识别次数
builder.addStage(recordMetricsStage);
// 4. 执行人脸识别
builder.addStage(faceRecognitionStage);
// 5. 人脸识别补救
builder.addStage(faceRecoveryStage);
// 6. 更新人脸结果
builder.addStage(updateFaceResultStage);
// 7. 构建源文件关联
builder.addStage(buildSourceRelationStage);
// 8. 处理免费源文件逻辑
builder.addStage(processFreeSourceStage);
// 9. 处理购买状态
builder.addStage(processBuyStatusStage);
// 10. 处理视频重切
builder.addStage(handleVideoRecreationStage);
// 11. 持久化关联关系
builder.addStage(persistRelationsStage);
// 12. 创建任务
builder.addStage(createTaskStage);
// 13. 异步生成拼图模板
builder.addStage(generatePuzzleStage);
log.debug("创建自动人脸匹配Pipeline: isNew={}, stageCount={}", isNew, builder.build().getStageCount());
return builder.build();
}
/**
* 创建自定义人脸匹配Pipeline
*
* @return Pipeline
*/
public Pipeline<FaceMatchingContext> createCustomMatchingPipeline() {
PipelineBuilder<FaceMatchingContext> builder = new PipelineBuilder<>("CustomMatching");
// 1. 准备上下文
builder.addStage(prepareContextStage);
// 2. 记录自定义匹配次数
builder.addStage(recordCustomMatchMetricsStage);
// 3. 加载用户选择的人脸样本
builder.addStage(loadFaceSamplesStage);
// 4. 根据配置执行自定义人脸搜索
builder.addStage(customFaceSearchStage);
// 5. 加载匹配样本实体到缓存
builder.addStage(loadMatchedSamplesStage);
// 6. 应用时间范围筛选
builder.addStage(filterByTimeRangeStage);
// 7. 应用设备照片数量限制筛选
builder.addStage(filterByDevicePhotoLimitStage);
// 8. 更新人脸结果
builder.addStage(updateFaceResultStage);
// 9. 删除旧关系数据
builder.addStage(deleteOldRelationsStage);
// 10. 构建源文件关联
builder.addStage(buildSourceRelationStage);
// 11. 处理免费源文件逻辑
builder.addStage(processFreeSourceStage);
// 12. 处理购买状态
builder.addStage(processBuyStatusStage);
// 13. 处理视频重切
builder.addStage(handleVideoRecreationStage);
// 14. 持久化关联关系
builder.addStage(persistRelationsStage);
// 15. 创建任务
builder.addStage(createTaskStage);
log.debug("创建自定义人脸匹配Pipeline: stageCount={}", builder.build().getStageCount());
return builder.build();
}
/**
* 创建仅识别Pipeline
* 只执行人脸识别,不处理后续业务逻辑
*
* @return Pipeline
*/
public Pipeline<FaceMatchingContext> createRecognitionOnlyPipeline() {
PipelineBuilder<FaceMatchingContext> builder = new PipelineBuilder<>("RecognitionOnly");
// 1. 准备上下文
builder.addStage(prepareContextStage);
// 2. 执行人脸识别
builder.addStage(faceRecognitionStage);
// 3. 人脸识别补救
builder.addStage(faceRecoveryStage);
log.debug("创建仅识别Pipeline: stageCount={}", builder.build().getStageCount());
return builder.build();
}
/**
* 根据场景创建Pipeline
*
* @param scene 场景
* @param isNew 是否新用户(仅AUTO_MATCHING场景需要)
* @return Pipeline
*/
public Pipeline<FaceMatchingContext> createPipeline(FaceMatchingScene scene, boolean isNew) {
return switch (scene) {
case AUTO_MATCHING -> createAutoMatchingPipeline(isNew);
case CUSTOM_MATCHING -> createCustomMatchingPipeline();
case RECOGNITION_ONLY -> createRecognitionOnlyPipeline();
};
}
/**
* 根据Context创建Pipeline
*
* @param context 上下文
* @return Pipeline
*/
public Pipeline<FaceMatchingContext> createPipeline(FaceMatchingContext context) {
return createPipeline(context.getScene(), context.isNew());
}
}

View File

@@ -0,0 +1,147 @@
package com.ycwl.basic.face.pipeline.helper;
import cn.hutool.core.date.DateUtil;
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO;
import com.ycwl.basic.puzzle.dto.PuzzleGenerateRequest;
import com.ycwl.basic.puzzle.dto.PuzzleGenerateResponse;
import com.ycwl.basic.puzzle.dto.PuzzleTemplateDTO;
import com.ycwl.basic.puzzle.service.IPuzzleGenerateService;
import com.ycwl.basic.puzzle.service.IPuzzleTemplateService;
import com.ycwl.basic.repository.ScenicRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 拼图生成编排器
* 负责编排拼图模板的批量生成逻辑
*
* 职责:
* 1. 查询景区的所有启用拼图模板
* 2. 构建动态数据
* 3. 逐个生成拼图图片
* 4. 记录统计信息
*
* 设计说明:
* - 从GeneratePuzzleStage中抽离出来,符合"薄Stage,厚Service"原则
* - Stage只负责触发异步任务,业务逻辑由此Orchestrator承担
*/
@Slf4j
@Service
public class PuzzleGenerationOrchestrator {
@Autowired
private IPuzzleTemplateService puzzleTemplateService;
@Autowired
private IPuzzleGenerateService puzzleGenerateService;
@Autowired
private ScenicRepository scenicRepository;
/**
* 异步生成景区所有启用的拼图模板
*
* @param scenicId 景区ID
* @param faceId 人脸ID
* @param memberId 会员ID
* @param faceUrl 人脸URL
*/
public void generateAllTemplatesAsync(Long scenicId, Long faceId, Long memberId, String faceUrl) {
new Thread(() -> {
try {
log.info("开始异步生成景区拼图模板: scenicId={}, faceId={}", scenicId, faceId);
// 1. 查询该景区所有启用状态的拼图模板
List<PuzzleTemplateDTO> templateList = puzzleTemplateService.listTemplates(
scenicId, null, 1); // 查询启用状态的模板
if (templateList == null || templateList.isEmpty()) {
log.info("景区不存在启用的拼图模板,跳过生成: scenicId={}", scenicId);
return;
}
log.info("景区存在 {} 个启用的拼图模板,开始逐个生成: scenicId={}", templateList.size(), scenicId);
// 2. 获取景区信息用于动态数据
ScenicV2DTO scenicBasic = scenicRepository.getScenicBasic(scenicId);
// 3. 准备公共动态数据
Map<String, String> baseDynamicData = buildBaseDynamicData(faceId, faceUrl, scenicBasic);
// 4. 遍历所有模板,逐个生成
int successCount = 0;
int failCount = 0;
for (PuzzleTemplateDTO template : templateList) {
try {
generateSingleTemplate(scenicId, faceId, memberId, template, baseDynamicData);
successCount++;
} catch (Exception e) {
log.error("拼图生成失败: scenicId={}, templateCode={}, templateName={}",
scenicId, template.getCode(), template.getName(), e);
failCount++;
}
}
log.info("景区拼图模板批量生成完成: scenicId={}, 总数={}, 成功={}, 失败={}",
scenicId, templateList.size(), successCount, failCount);
} catch (Exception e) {
// 异步任务失败不影响主流程,仅记录日志
log.error("异步生成拼图模板失败: scenicId={}, faceId={}", scenicId, faceId, e);
}
}, "PuzzleTemplateGenerator-" + scenicId + "-" + faceId).start();
}
/**
* 构建基础动态数据
*/
private Map<String, String> buildBaseDynamicData(Long faceId, String faceUrl, ScenicV2DTO scenicBasic) {
Map<String, String> baseDynamicData = new HashMap<>();
if (faceUrl != null) {
baseDynamicData.put("faceImage", faceUrl);
baseDynamicData.put("userAvatar", faceUrl);
}
baseDynamicData.put("faceId", String.valueOf(faceId));
baseDynamicData.put("scenicName", scenicBasic.getName());
baseDynamicData.put("scenicText", scenicBasic.getName());
baseDynamicData.put("dateStr", DateUtil.format(new Date(), "yyyy.MM.dd"));
return baseDynamicData;
}
/**
* 生成单个拼图模板
*/
private void generateSingleTemplate(Long scenicId, Long faceId, Long memberId,
PuzzleTemplateDTO template,
Map<String, String> baseDynamicData) {
log.info("开始生成拼图: scenicId={}, templateCode={}, templateName={}",
scenicId, template.getCode(), template.getName());
// 构建生成请求
PuzzleGenerateRequest generateRequest = new PuzzleGenerateRequest();
generateRequest.setScenicId(scenicId);
generateRequest.setUserId(memberId);
generateRequest.setFaceId(faceId);
generateRequest.setBusinessType("face_matching");
generateRequest.setTemplateCode(template.getCode());
generateRequest.setOutputFormat("PNG");
generateRequest.setQuality(90);
generateRequest.setDynamicData(new HashMap<>(baseDynamicData));
generateRequest.setRequireRuleMatch(true);
// 调用拼图生成服务
PuzzleGenerateResponse response = puzzleGenerateService.generate(generateRequest);
log.info("拼图生成成功: scenicId={}, templateCode={}, imageUrl={}",
scenicId, template.getCode(), response.getImageUrl());
}
}

View File

@@ -0,0 +1,92 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.face.pipeline.annotation.StageConfig;
import com.ycwl.basic.face.pipeline.core.AbstractFaceMatchingStage;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.face.pipeline.core.StageResult;
import com.ycwl.basic.face.pipeline.enums.StageOptionalMode;
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
import com.ycwl.basic.service.pc.processor.SourceRelationProcessor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 构建源文件关联Stage
* 负责根据匹配到的样本ID构建member_source关联关系
*
* 职责:
* 1. 从context.sampleListIds读取匹配的样本ID列表
* 2. 调用sourceRelationProcessor.processMemberSources()生成MemberSourceEntity列表
* 3. 更新context.memberSourceList
*
* 前置条件: context.sampleListIds不为空
* 后置条件: context.memberSourceList已设置
*/
@Slf4j
@Component
@StageConfig(
stageId = "build_source_relation",
optionalMode = StageOptionalMode.UNSUPPORT,
description = "构建源文件关联关系"
)
public class BuildSourceRelationStage extends AbstractFaceMatchingStage<FaceMatchingContext> {
@Autowired
private SourceRelationProcessor sourceRelationProcessor;
@Override
public String getName() {
return "BuildSourceRelation";
}
@Override
protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) {
// 只有当sampleListIds不为空时才执行
List<Long> sampleListIds = context.getSampleListIds();
if (sampleListIds == null || sampleListIds.isEmpty()) {
// 从searchResult中获取
if (context.getSearchResult() != null) {
sampleListIds = context.getSearchResult().getSampleListIds();
context.setSampleListIds(sampleListIds);
}
}
if (sampleListIds == null || sampleListIds.isEmpty()) {
log.debug("sampleListIds为空,跳过源文件关联,faceId={}", context.getFaceId());
return false;
}
return true;
}
@Override
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
List<Long> sampleListIds = context.getSampleListIds();
Long faceId = context.getFaceId();
try {
// 处理源文件关联
List<MemberSourceEntity> memberSourceEntityList =
sourceRelationProcessor.processMemberSources(sampleListIds, context.getFace());
if (memberSourceEntityList == null || memberSourceEntityList.isEmpty()) {
log.warn("未找到有效的源文件,faceId={}, sampleListIds={}", faceId, sampleListIds);
return StageResult.skipped("未找到有效的源文件");
}
context.setMemberSourceList(memberSourceEntityList);
log.info("构建源文件关联成功: faceId={}, 关联源文件数={}", faceId, memberSourceEntityList.size());
return StageResult.success(String.format("构建了%d个源文件关联", memberSourceEntityList.size()));
} catch (Exception e) {
log.error("构建源文件关联失败,faceId={}, sampleListIds={}", faceId, sampleListIds, e);
// 源文件关联失败不影响主流程,返回降级
return StageResult.degraded("构建源文件关联失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,73 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.biz.TaskStatusBiz;
import com.ycwl.basic.face.pipeline.annotation.StageConfig;
import com.ycwl.basic.face.pipeline.core.AbstractFaceMatchingStage;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.face.pipeline.core.StageResult;
import com.ycwl.basic.face.pipeline.enums.StageOptionalMode;
import com.ycwl.basic.service.pc.helper.ScenicConfigFacade;
import com.ycwl.basic.service.task.TaskService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 创建任务Stage
* 负责根据配置决定是否自动创建任务
*
* 职责:
* 1. 检查face_select_first配置
* 2. 如果配置为false,则调用taskService.autoCreateTaskByFaceId()
* 3. 如果配置为true,则设置任务状态为2(等待用户选择)
*/
@Slf4j
@Component
@StageConfig(
stageId = "create_task",
optionalMode = StageOptionalMode.UNSUPPORT,
description = "根据配置创建视频任务"
)
public class CreateTaskStage extends AbstractFaceMatchingStage<FaceMatchingContext> {
@Autowired
private ScenicConfigFacade scenicConfigFacade;
@Autowired
private TaskService taskService;
@Autowired
private TaskStatusBiz taskStatusBiz;
@Override
public String getName() {
return "CreateTask";
}
@Override
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
Long scenicId = context.getFace().getScenicId();
Long faceId = context.getFaceId();
try {
boolean faceSelectFirst = scenicConfigFacade.isFaceSelectFirst(scenicId);
if (!faceSelectFirst) {
// 配置为自动创建任务
taskService.autoCreateTaskByFaceId(faceId);
log.info("自动创建任务成功: faceId={}", faceId);
return StageResult.success("自动创建任务成功");
} else {
// 配置为等待用户选择
taskStatusBiz.setFaceCutStatus(faceId, 2);
log.debug("景区配置 face_select_first=true,跳过自动创建任务: faceId={}", faceId);
return StageResult.skipped("等待用户手动选择");
}
} catch (Exception e) {
log.error("创建任务失败,faceId={}", faceId, e);
// 任务创建失败不影响主流程,返回降级而不是失败
return StageResult.degraded("任务创建失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,121 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.exception.BaseException;
import com.ycwl.basic.face.pipeline.annotation.StageConfig;
import com.ycwl.basic.face.pipeline.core.AbstractFaceMatchingStage;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.face.pipeline.core.StageResult;
import com.ycwl.basic.face.pipeline.enums.StageOptionalMode;
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
import com.ycwl.basic.model.task.resp.SearchFaceRespVo;
import com.ycwl.basic.service.pc.helper.SearchResultMerger;
import com.ycwl.basic.service.task.TaskFaceService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* 自定义人脸搜索Stage
* 负责根据faceSelectPostMode执行不同的搜索策略
*
* 职责:
* 1. 从context.faceSelectPostMode读取配置
* 2. 模式2: 直接使用用户选择的样本,不搜索
* 3. 模式0/1: 对每个样本搜索,然后合并结果
* 4. 更新context.searchResult
*/
@Slf4j
@Component
@StageConfig(
stageId = "custom_face_search",
optionalMode = StageOptionalMode.FORCE_ON,
description = "根据配置执行自定义人脸搜索"
)
public class CustomFaceSearchStage extends AbstractFaceMatchingStage<FaceMatchingContext> {
@Autowired
private TaskFaceService taskFaceService;
@Autowired
private SearchResultMerger resultMerger;
@Override
public String getName() {
return "CustomFaceSearch";
}
@Override
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
Integer faceSelectPostMode = context.getFaceSelectPostMode();
List<FaceSampleEntity> faceSamples = context.getFaceSamples();
List<Long> faceSampleIds = context.getFaceSampleIds();
Long faceId = context.getFaceId();
if (faceSelectPostMode == null) {
faceSelectPostMode = 0; // 默认为并集模式
}
log.debug("face_select_post_mode配置值: {}, faceId={}", faceSelectPostMode, faceId);
try {
SearchFaceRespVo mergedResult;
// 模式2:不搜索,直接使用用户选择的faceSampleIds
if (Integer.valueOf(2).equals(faceSelectPostMode)) {
log.debug("使用模式2:直接使用用户选择的人脸样本,不进行搜索,faceId={}", faceId);
mergedResult = resultMerger.createDirectResult(faceSampleIds);
// 保留原始matchResult
if (context.getFace().getMatchResult() != null) {
mergedResult.setSearchResultJson(context.getFace().getMatchResult());
}
} else {
// 模式0(并集)和模式1(交集):需要进行搜索
List<SearchFaceRespVo> searchResults = new ArrayList<>();
for (FaceSampleEntity faceSample : faceSamples) {
try {
SearchFaceRespVo result = taskFaceService.searchFace(
context.getFaceBodyAdapter(),
String.valueOf(context.getFace().getScenicId()),
faceSample.getFaceUrl(),
"自定义人脸匹配");
if (result != null) {
searchResults.add(result);
}
} catch (Exception e) {
log.warn("人脸样本搜索失败,faceSampleId={}, faceUrl={}, faceId={}",
faceSample.getId(), faceSample.getFaceUrl(), faceId, e);
// 继续处理其他样本,不中断整个流程
}
}
if (searchResults.isEmpty()) {
log.warn("所有人脸样本搜索都失败,faceId={}, faceSampleIds={}", faceId, faceSampleIds);
throw new BaseException("人脸识别失败,请重试");
}
// 根据模式整合多个搜索结果
mergedResult = resultMerger.merge(searchResults, faceSelectPostMode);
}
context.setSearchResult(mergedResult);
context.setSampleListIds(mergedResult.getSampleListIds());
log.info("自定义人脸搜索完成: faceId={}, mode={}, 匹配数={}",
faceId, faceSelectPostMode,
mergedResult.getSampleListIds() != null ? mergedResult.getSampleListIds().size() : 0);
return StageResult.success(String.format("自定义搜索完成,模式=%d", faceSelectPostMode));
} catch (BaseException e) {
throw e;
} catch (Exception e) {
log.error("自定义人脸搜索失败,faceId={}, faceSampleIds={}", faceId, faceSampleIds, e);
return StageResult.failed("自定义人脸搜索失败", e);
}
}
}

View File

@@ -0,0 +1,74 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.face.pipeline.annotation.StageConfig;
import com.ycwl.basic.face.pipeline.core.AbstractFaceMatchingStage;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.face.pipeline.core.StageResult;
import com.ycwl.basic.face.pipeline.enums.StageOptionalMode;
import com.ycwl.basic.mapper.SourceMapper;
import com.ycwl.basic.mapper.VideoMapper;
import com.ycwl.basic.repository.MemberRelationRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 删除旧关系Stage
* 负责在保存新关系前,删除该人脸的旧数据关系
*
* 职责:
* 1. 删除member_source中该人脸的未购买关系
* 2. 删除member_video中该人脸的未购买关系
* 3. 清除缓存
*/
@Slf4j
@Component
@StageConfig(
stageId = "delete_old_relations",
optionalMode = StageOptionalMode.FORCE_ON,
description = "删除人脸旧关系数据"
)
public class DeleteOldRelationsStage extends AbstractFaceMatchingStage<FaceMatchingContext> {
@Autowired
private SourceMapper sourceMapper;
@Autowired
private VideoMapper videoMapper;
@Autowired
private MemberRelationRepository memberRelationRepository;
@Override
public String getName() {
return "DeleteOldRelations";
}
@Override
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
Long faceId = context.getFaceId();
Long memberId = context.getFace().getMemberId();
try {
log.debug("删除人脸旧关系数据:faceId={}, memberId={}", faceId, memberId);
// 1. 删除member_source中的未购买关系
sourceMapper.deleteNotBuyFaceRelation(memberId, faceId);
// 2. 删除member_video中的未购买关系
videoMapper.deleteNotBuyFaceRelations(memberId, faceId);
// 3. 清除缓存
memberRelationRepository.clearSCacheByFace(faceId);
log.debug("人脸旧关系数据删除完成:faceId={}", faceId);
return StageResult.success("旧关系数据已删除");
} catch (Exception e) {
log.error("删除旧关系数据失败,faceId={}", faceId, e);
// 删除失败不影响主流程,返回降级
return StageResult.degraded("删除旧关系数据失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,73 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.exception.BaseException;
import com.ycwl.basic.face.pipeline.annotation.StageConfig;
import com.ycwl.basic.face.pipeline.core.AbstractFaceMatchingStage;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.face.pipeline.core.StageResult;
import com.ycwl.basic.face.pipeline.enums.StageOptionalMode;
import com.ycwl.basic.model.task.resp.SearchFaceRespVo;
import com.ycwl.basic.service.task.TaskFaceService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 人脸识别Stage
* 负责执行核心的人脸识别搜索
*
* 职责:
* 1. 调用taskFaceService.searchFace()执行人脸搜索
* 2. 将结果存入context.searchResult
* 3. 识别失败则返回FAILED
*/
@Slf4j
@Component
@StageConfig(
stageId = "face_recognition",
optionalMode = StageOptionalMode.FORCE_ON,
description = "执行人脸识别搜索"
)
public class FaceRecognitionStage extends AbstractFaceMatchingStage<FaceMatchingContext> {
@Autowired
private TaskFaceService taskFaceService;
@Override
public String getName() {
return "FaceRecognition";
}
@Override
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
try {
SearchFaceRespVo searchResult = taskFaceService.searchFace(
context.getFaceBodyAdapter(),
String.valueOf(context.getFace().getScenicId()),
context.getFace().getFaceUrl(),
"人脸识别");
if (searchResult == null) {
log.warn("人脸识别返回结果为空,faceId={}", context.getFaceId());
return StageResult.failed("人脸识别失败,请换一张试试把~");
}
context.setSearchResult(searchResult);
log.info("人脸识别完成: faceId={}, score={}, 匹配数={}",
context.getFaceId(),
searchResult.getScore(),
searchResult.getSampleListIds() != null ? searchResult.getSampleListIds().size() : 0);
return StageResult.success(String.format("识别成功,匹配数=%d",
searchResult.getSampleListIds() != null ? searchResult.getSampleListIds().size() : 0));
} catch (BaseException e) {
throw e;
} catch (Exception e) {
log.error("人脸识别服务调用失败,faceId={}, scenicId={}",
context.getFaceId(), context.getFace().getScenicId(), e);
return StageResult.failed("人脸识别失败,请换一张试试把~", e);
}
}
}

View File

@@ -0,0 +1,80 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.face.pipeline.annotation.StageConfig;
import com.ycwl.basic.face.pipeline.core.AbstractFaceMatchingStage;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.face.pipeline.core.StageResult;
import com.ycwl.basic.face.pipeline.enums.StageOptionalMode;
import com.ycwl.basic.model.task.resp.SearchFaceRespVo;
import com.ycwl.basic.service.pc.processor.FaceRecoveryStrategy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 人脸识别补救Stage
* 负责执行人脸识别的补救逻辑(降级)
*
* 职责:
* 1. 从context.searchResult读取识别结果
* 2. 调用faceRecoveryStrategy.executeFaceRecoveryLogic()执行补救
* 3. 如果触发补救,更新searchResult并返回DEGRADED
* 4. 否则返回SUCCESS
*/
@Slf4j
@Component
@StageConfig(
stageId = "face_recovery",
optionalMode = StageOptionalMode.SUPPORT,
description = "执行人脸识别补救逻辑",
defaultEnabled = true
)
public class FaceRecoveryStage extends AbstractFaceMatchingStage<FaceMatchingContext> {
@Autowired
private FaceRecoveryStrategy faceRecoveryStrategy;
@Override
public String getName() {
return "FaceRecovery";
}
@Override
protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) {
// 只有当searchResult不为空时才执行
if (context.getSearchResult() == null) {
log.debug("searchResult为空,跳过补救逻辑,faceId={}", context.getFaceId());
return false;
}
return true;
}
@Override
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
SearchFaceRespVo searchResult = context.getSearchResult();
Long faceId = context.getFaceId();
try {
// 执行补救逻辑(补救逻辑内部会判断是否需要触发)
SearchFaceRespVo recoveredResult = faceRecoveryStrategy.executeFaceRecoveryLogic(
searchResult,
context.getScenicConfig(),
context.getFaceBodyAdapter(),
context.getFace().getScenicId());
// 如果结果发生变化,说明触发了补救
if (recoveredResult != searchResult) {
context.setSearchResult(recoveredResult);
log.info("触发补救逻辑,重新搜索: faceId={}", faceId);
return StageResult.degraded("触发补救逻辑,重新搜索");
}
return StageResult.success("无需补救");
} catch (Exception e) {
log.error("补救逻辑执行失败,faceId={}", faceId, e);
// 补救失败不影响主流程,返回降级
return StageResult.degraded("补救逻辑执行失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,223 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.face.pipeline.annotation.StageConfig;
import com.ycwl.basic.face.pipeline.core.AbstractFaceMatchingStage;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.face.pipeline.core.StageResult;
import com.ycwl.basic.face.pipeline.enums.StageOptionalMode;
import com.ycwl.basic.integration.common.manager.DeviceConfigManager;
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
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;
/**
* 按设备照片数量限制筛选样本Stage
* 负责根据设备配置的照片数量限制(limit_photo)筛选匹配样本
*
* 职责:
* 1. 从context.faceSamples读取样本实体缓存
* 2. 按设备ID分组
* 3. 对每个设备,根据其limit_photo配置筛选样本:
* - 如果样本数 > limit_photo + 2: 按时间排序,去掉首尾,保留中间limit_photo张
* - 如果样本数 > limit_photo + 1: 按时间排序,去掉尾部,保留前limit_photo张
* - 如果样本数 > limit_photo: 保留前limit_photo张
* - 否则: 保留全部
* 4. 更新context.sampleListIds
*
* 前置条件: context.faceSamples不为空 (由LoadMatchedSamplesStage加载)
* 配置说明: limit_photo=0或null表示不限制数量
*/
@Slf4j
@Component
@StageConfig(
stageId = "filter_by_device_photo_limit",
optionalMode = StageOptionalMode.SUPPORT,
description = "按设备照片数量限制筛选样本",
defaultEnabled = true
)
public class FilterByDevicePhotoLimitStage extends AbstractFaceMatchingStage<FaceMatchingContext> {
@Autowired
private DeviceRepository deviceRepository;
@Override
public String getName() {
return "FilterByDevicePhotoLimit";
}
@Override
protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) {
// 检查faceSamples是否为空
List<FaceSampleEntity> faceSamples = context.getFaceSamples();
if (faceSamples == null || faceSamples.isEmpty()) {
log.debug("faceSamples为空,跳过设备照片限制筛选,faceId={}", context.getFaceId());
return false;
}
return true;
}
@Override
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
List<FaceSampleEntity> faceSamples = context.getFaceSamples();
List<Long> sampleListIds = context.getSampleListIds();
Long faceId = context.getFaceId();
try {
// 1. 构建样本ID到实体的映射
Map<Long, FaceSampleEntity> sampleMap = faceSamples.stream()
.collect(Collectors.toMap(FaceSampleEntity::getId, sample -> sample, (a, b) -> a));
// 2. 按设备ID分组
Map<Long, List<FaceSampleEntity>> deviceSamplesMap = new LinkedHashMap<>();
Set<Long> passthroughSampleIds = new LinkedHashSet<>();
for (Long sampleId : sampleListIds) {
FaceSampleEntity sample = sampleMap.get(sampleId);
if (sample == null || sample.getDeviceId() == null) {
passthroughSampleIds.add(sampleId); // 无设备ID的样本直接保留
continue;
}
deviceSamplesMap
.computeIfAbsent(sample.getDeviceId(), key -> new ArrayList<>())
.add(sample);
}
// 3. 对每个设备应用照片数量限制
Map<Long, Integer> limitCache = new HashMap<>();
Set<Long> retainedSampleIds = new LinkedHashSet<>(passthroughSampleIds);
for (Map.Entry<Long, List<FaceSampleEntity>> entry : deviceSamplesMap.entrySet()) {
Long deviceId = entry.getKey();
List<FaceSampleEntity> deviceSamples = entry.getValue();
// 读取设备配置
Integer limitPhoto = limitCache.computeIfAbsent(deviceId, id -> {
DeviceConfigManager deviceConfig = deviceRepository.getDeviceConfigManager(id);
return deviceConfig != null ? deviceConfig.getInteger("limit_photo") : null;
});
List<Long> retainedForDevice = applyLimitForDevice(deviceId, deviceSamples, limitPhoto);
retainedSampleIds.addAll(retainedForDevice);
}
// 4. 按原始顺序保留筛选后的样本ID
List<Long> resultIds = sampleListIds.stream()
.filter(retainedSampleIds::contains)
.collect(Collectors.toList());
// 5. 更新context
context.setSampleListIds(resultIds);
log.info("设备照片数量限制筛选完成: faceId={}, 原始样本数={}, 筛选后数={}",
faceId, sampleListIds.size(), resultIds.size());
return StageResult.success(String.format("设备限制筛选: %d → %d",
sampleListIds.size(), resultIds.size()));
} catch (Exception e) {
log.error("设备照片数量限制筛选失败,faceId={}", faceId, e);
// 筛选失败不影响主流程,返回降级
return StageResult.degraded("设备照片数量限制筛选失败: " + e.getMessage());
}
}
/**
* 对单个设备的样本应用照片数量限制
*/
private List<Long> applyLimitForDevice(Long deviceId, List<FaceSampleEntity> deviceSamples, Integer limitPhoto) {
List<Long> deviceSampleIds = deviceSamples.stream()
.map(FaceSampleEntity::getId)
.collect(Collectors.toList());
// 无限制或限制数量<=0,保留全部
if (limitPhoto == null || limitPhoto <= 0) {
log.debug("设备照片限制: 设备ID={}, 无限制, 保留{}张照片", deviceId, deviceSampleIds.size());
return deviceSampleIds;
}
int sampleCount = deviceSamples.size();
// 样本数 > limit_photo + 2: 按时间排序,去掉首尾
if (sampleCount > (limitPhoto + 2)) {
List<Long> retained = processDeviceSamples(deviceSamples, limitPhoto, true);
log.debug("设备照片限制: 设备ID={}, 限制={}张, 原始{}张, 去首尾后最终{}张",
deviceId, limitPhoto, sampleCount, retained.size());
return retained;
}
// 样本数 > limit_photo + 1: 按时间排序,去掉尾部
if (sampleCount > (limitPhoto + 1)) {
List<Long> retained = processDeviceSamples(deviceSamples, limitPhoto, false);
log.debug("设备照片限制: 设备ID={}, 限制={}张, 原始{}张, 去尾部后最终{}张",
deviceId, limitPhoto, sampleCount, retained.size());
return retained;
}
// 样本数 > limit_photo: 保留前limit_photo张
if (sampleCount > limitPhoto) {
List<Long> retained = deviceSamples.stream()
.limit(limitPhoto)
.map(FaceSampleEntity::getId)
.collect(Collectors.toList());
log.debug("设备照片限制: 设备ID={}, 限制={}张, 原始{}张, 取前{}张",
deviceId, limitPhoto, sampleCount, retained.size());
return retained;
}
// 样本数 <= limit_photo: 保留全部
log.debug("设备照片限制: 设备ID={}, 限制={}张, 原始{}张, 无需筛选, 保留全部",
deviceId, limitPhoto, sampleCount);
return deviceSampleIds;
}
/**
* 处理设备样本,根据参数决定是否去掉首尾
*
* @param deviceSamples 设备样本列表
* @param limitPhoto 限制数量
* @param removeBoth 是否去掉首尾,true去掉首尾,false只去掉尾部
* @return 处理后的样本ID列表
*/
private List<Long> processDeviceSamples(List<FaceSampleEntity> deviceSamples, int limitPhoto, boolean removeBoth) {
// 创建原始排序的索引映射,用于后续恢复排序
Map<Long, Integer> originalIndexMap = new HashMap<>();
for (int i = 0; i < deviceSamples.size(); i++) {
originalIndexMap.put(deviceSamples.get(i).getId(), i);
}
// 按创建时间排序
List<FaceSampleEntity> sortedByCreateTime = deviceSamples.stream()
.sorted(Comparator.comparing(FaceSampleEntity::getCreateAt))
.collect(Collectors.toList());
// 根据参数决定去掉首尾还是只去掉尾部
List<FaceSampleEntity> filteredSamples;
if (removeBoth && sortedByCreateTime.size() > 2) {
// 去掉首尾
filteredSamples = sortedByCreateTime.subList(1, sortedByCreateTime.size() - 1);
} else if (!removeBoth && sortedByCreateTime.size() > 1) {
// 只去掉尾部
filteredSamples = sortedByCreateTime.subList(0, sortedByCreateTime.size() - 1);
} else {
filteredSamples = sortedByCreateTime;
}
// 取前limitPhoto个
List<FaceSampleEntity> limitedSamples = filteredSamples.stream()
.limit(limitPhoto)
.collect(Collectors.toList());
// 按原始顺序排序
List<Long> resultIds = limitedSamples.stream()
.sorted(Comparator.comparing(sample -> originalIndexMap.get(sample.getId())))
.map(FaceSampleEntity::getId)
.collect(Collectors.toList());
return resultIds;
}
}

View File

@@ -0,0 +1,122 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.face.pipeline.annotation.StageConfig;
import com.ycwl.basic.face.pipeline.core.AbstractFaceMatchingStage;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.face.pipeline.core.StageResult;
import com.ycwl.basic.face.pipeline.enums.StageOptionalMode;
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.stream.Collectors;
/**
* 按时间范围筛选样本Stage
* 负责根据景区配置的游览时间(tour_time)筛选匹配样本
*
* 职责:
* 1. 从context.scenicConfig读取tour_time配置(分钟)
* 2. 从context.faceSamples读取样本实体缓存
* 3. 找到最新的样本,以其拍摄时间为基准
* 4. 筛选出时间范围内(最新样本时间 ± tour_time分钟)的样本
* 5. 更新context.sampleListIds
*
* 前置条件:
* - context.faceSamples不为空 (由LoadMatchedSamplesStage加载)
* - context.scenicConfig配置了tour_time
*
* 配置说明: tour_time=0或null表示不限制时间范围
*/
@Slf4j
@Component
@StageConfig(
stageId = "filter_by_time_range",
optionalMode = StageOptionalMode.SUPPORT,
description = "按游览时间范围筛选样本",
defaultEnabled = true
)
public class FilterByTimeRangeStage extends AbstractFaceMatchingStage<FaceMatchingContext> {
@Override
public String getName() {
return "FilterByTimeRange";
}
@Override
protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) {
// 检查faceSamples是否为空
List<FaceSampleEntity> faceSamples = context.getFaceSamples();
if (faceSamples == null || faceSamples.isEmpty()) {
log.debug("faceSamples为空,跳过时间范围筛选,faceId={}", context.getFaceId());
return false;
}
// 检查是否配置了tour_time
Integer tourMinutes = context.getScenicConfig() != null
? context.getScenicConfig().getInteger("tour_time")
: null;
if (tourMinutes == null || tourMinutes <= 0) {
log.debug("景区未配置tour_time或配置为0,跳过时间范围筛选,faceId={}", context.getFaceId());
return false;
}
return true;
}
@Override
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
List<FaceSampleEntity> faceSamples = context.getFaceSamples();
List<Long> sampleListIds = context.getSampleListIds();
Integer tourMinutes = context.getScenicConfig().getInteger("tour_time");
Long faceId = context.getFaceId();
try {
// 1. 构建样本ID到实体的映射
Map<Long, FaceSampleEntity> sampleMap = faceSamples.stream()
.collect(Collectors.toMap(FaceSampleEntity::getId, sample -> sample, (a, b) -> a));
// 2. 找到最新的样本(拍摄时间最晚)
FaceSampleEntity topMatchSample = faceSamples.stream()
.filter(sample -> sample.getCreateAt() != null)
.max(Comparator.comparing(FaceSampleEntity::getCreateAt))
.orElse(null);
if (topMatchSample == null || topMatchSample.getCreateAt() == null) {
log.warn("未找到有效的样本拍摄时间,保留所有样本,faceId={}", faceId);
return StageResult.success("样本无拍摄时间,保留所有");
}
Date referenceTime = topMatchSample.getCreateAt();
long referenceMillis = referenceTime.getTime();
long tourMillis = tourMinutes * 60 * 1000L;
// 3. 筛选时间范围内的样本
List<Long> filteredIds = sampleListIds.stream()
.filter(sampleId -> {
FaceSampleEntity sample = sampleMap.get(sampleId);
if (sample == null || sample.getCreateAt() == null) {
return false; // 无时间信息的样本被过滤
}
long timeDiff = Math.abs(sample.getCreateAt().getTime() - referenceMillis);
return timeDiff <= tourMillis;
})
.collect(Collectors.toList());
// 4. 更新context
context.setSampleListIds(filteredIds);
log.info("时间范围筛选完成: faceId={}, tour_time={}分钟, 原始样本数={}, 筛选后数={}",
faceId, tourMinutes, sampleListIds.size(), filteredIds.size());
return StageResult.success(String.format("时间筛选: %d → %d",
sampleListIds.size(), filteredIds.size()));
} catch (Exception e) {
log.error("时间范围筛选失败,faceId={}", faceId, e);
// 筛选失败不影响主流程,返回降级
return StageResult.degraded("时间范围筛选失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,66 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.face.pipeline.annotation.StageConfig;
import com.ycwl.basic.face.pipeline.core.AbstractFaceMatchingStage;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.face.pipeline.core.StageResult;
import com.ycwl.basic.face.pipeline.enums.StageOptionalMode;
import com.ycwl.basic.face.pipeline.helper.PuzzleGenerationOrchestrator;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 生成拼图模板Stage
* 负责触发景区拼图模板的异步生成任务
*
* 职责:
* 1. 从context读取必要参数(scenicId, faceId, memberId, faceUrl)
* 2. 调用puzzleOrchestrator.generateAllTemplatesAsync()触发异步生成
* 3. 立即返回,不等待生成完成
*
* 业务说明:
* - 拼图生成是异步的,不影响主流程
* - 具体的拼图生成逻辑由PuzzleGenerationOrchestrator负责
* - Stage只负责触发任务,符合"薄Stage,厚Service"原则
*/
@Slf4j
@Component
@StageConfig(
stageId = "generate_puzzle",
optionalMode = StageOptionalMode.SUPPORT,
description = "异步生成拼图模板",
defaultEnabled = true
)
public class GeneratePuzzleStage extends AbstractFaceMatchingStage<FaceMatchingContext> {
@Autowired
private PuzzleGenerationOrchestrator puzzleOrchestrator;
@Override
public String getName() {
return "GeneratePuzzle";
}
@Override
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
Long scenicId = context.getFace().getScenicId();
Long faceId = context.getFaceId();
Long memberId = context.getFace().getMemberId();
String faceUrl = context.getFace().getFaceUrl();
try {
// 触发异步生成拼图模板
puzzleOrchestrator.generateAllTemplatesAsync(scenicId, faceId, memberId, faceUrl);
log.debug("拼图模板异步生成任务已提交: scenicId={}, faceId={}", scenicId, faceId);
return StageResult.success("拼图模板已提交异步生成");
} catch (Exception e) {
log.error("提交拼图生成任务失败: scenicId={}, faceId={}", scenicId, faceId, e);
// 拼图生成失败不影响主流程,返回降级
return StageResult.degraded("提交拼图生成任务失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,86 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.face.pipeline.annotation.StageConfig;
import com.ycwl.basic.face.pipeline.core.AbstractFaceMatchingStage;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.face.pipeline.core.StageResult;
import com.ycwl.basic.face.pipeline.enums.StageOptionalMode;
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
import com.ycwl.basic.service.pc.processor.VideoRecreationHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 处理视频重切Stage
* 负责触发视频重新切片处理
*
* 职责:
* 1. 从context读取必要参数(scenicId, memberSourceList, faceId, memberId, sampleListIds, isNew)
* 2. 调用videoRecreationHandler.handleVideoRecreation()触发视频重切
*
* 前置条件: context.memberSourceList不为空
* 业务说明: 视频重切用于根据人脸识别结果重新生成个性化视频片段
*/
@Slf4j
@Component
@StageConfig(
stageId = "handle_video_recreation",
optionalMode = StageOptionalMode.SUPPORT,
description = "处理视频重切逻辑",
defaultEnabled = true
)
public class HandleVideoRecreationStage extends AbstractFaceMatchingStage<FaceMatchingContext> {
@Autowired
private VideoRecreationHandler videoRecreationHandler;
@Override
public String getName() {
return "HandleVideoRecreation";
}
@Override
protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) {
// 只有当memberSourceList不为空时才执行
List<MemberSourceEntity> memberSourceList = context.getMemberSourceList();
if (memberSourceList == null || memberSourceList.isEmpty()) {
log.debug("memberSourceList为空,跳过视频重切,faceId={}", context.getFaceId());
return false;
}
return true;
}
@Override
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
Long scenicId = context.getFace().getScenicId();
List<MemberSourceEntity> memberSourceEntityList = context.getMemberSourceList();
Long faceId = context.getFaceId();
Long memberId = context.getFace().getMemberId();
List<Long> sampleListIds = context.getSampleListIds();
boolean isNew = context.isNew();
try {
// 处理视频重切
videoRecreationHandler.handleVideoRecreation(
scenicId,
memberSourceEntityList,
faceId,
memberId,
sampleListIds,
isNew);
log.info("视频重切处理完成: faceId={}, scenicId={}, 源文件数={}",
faceId, scenicId, memberSourceEntityList.size());
return StageResult.success("视频重切处理完成");
} catch (Exception e) {
log.error("处理视频重切失败,faceId={}", faceId, e);
// 视频重切失败不影响主流程,返回降级
return StageResult.degraded("视频重切处理失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,73 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.exception.BaseException;
import com.ycwl.basic.face.pipeline.annotation.StageConfig;
import com.ycwl.basic.face.pipeline.core.AbstractFaceMatchingStage;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.face.pipeline.core.StageResult;
import com.ycwl.basic.face.pipeline.enums.StageOptionalMode;
import com.ycwl.basic.mapper.FaceSampleMapper;
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 加载人脸样本Stage
* 负责加载用户选择的人脸样本数据
*
* 职责:
* 1. 从context.faceSampleIds读取用户选择的样本ID列表
* 2. 调用faceSampleMapper.listByIds()加载样本实体
* 3. 更新context.faceSamples
*/
@Slf4j
@Component
@StageConfig(
stageId = "load_face_samples",
optionalMode = StageOptionalMode.FORCE_ON,
description = "加载用户选择的人脸样本"
)
public class LoadFaceSamplesStage extends AbstractFaceMatchingStage<FaceMatchingContext> {
@Autowired
private FaceSampleMapper faceSampleMapper;
@Override
public String getName() {
return "LoadFaceSamples";
}
@Override
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
List<Long> faceSampleIds = context.getFaceSampleIds();
Long faceId = context.getFaceId();
if (faceSampleIds == null || faceSampleIds.isEmpty()) {
log.warn("faceSampleIds为空,faceId={}", faceId);
return StageResult.failed("faceSampleIds不能为空");
}
try {
List<FaceSampleEntity> faceSamples = faceSampleMapper.listByIds(faceSampleIds);
if (faceSamples.isEmpty()) {
log.warn("未找到指定的人脸样本,faceSampleIds: {}, faceId={}", faceSampleIds, faceId);
throw new BaseException("未找到指定的人脸样本");
}
context.setFaceSamples(faceSamples);
log.info("加载人脸样本成功: faceId={}, sampleCount={}", faceId, faceSamples.size());
return StageResult.success(String.format("加载了%d个人脸样本", faceSamples.size()));
} catch (BaseException e) {
throw e;
} catch (Exception e) {
log.error("加载人脸样本失败,faceId={}, faceSampleIds={}", faceId, faceSampleIds, e);
return StageResult.failed("加载人脸样本失败", e);
}
}
}

View File

@@ -0,0 +1,89 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.face.pipeline.annotation.StageConfig;
import com.ycwl.basic.face.pipeline.core.AbstractFaceMatchingStage;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.face.pipeline.core.StageResult;
import com.ycwl.basic.face.pipeline.enums.StageOptionalMode;
import com.ycwl.basic.mapper.FaceSampleMapper;
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 加载匹配样本实体Stage
* 负责将sampleListIds对应的样本实体加载到context.faceSamples,供后续Stage使用
*
* 职责:
* 1. 从context.sampleListIds读取匹配到的样本ID列表
* 2. 调用faceSampleMapper.listByIds()批量加载样本实体
* 3. 更新context.faceSamples作为样本实体缓存
*
* 设计目的:
* - 避免后续多个Stage重复调用faceSampleMapper.listByIds()
* - 统一加载时机,提高性能
* - 为后续筛选Stage提供样本实体数据源
*
* 前置条件: context.sampleListIds不为空
*
* 应用场景: 自定义匹配场景,在CustomFaceSearchStage之后
*/
@Slf4j
@Component
@StageConfig(
stageId = "load_matched_samples",
optionalMode = StageOptionalMode.UNSUPPORT,
description = "加载匹配样本实体到缓存"
)
public class LoadMatchedSamplesStage extends AbstractFaceMatchingStage<FaceMatchingContext> {
@Autowired
private FaceSampleMapper faceSampleMapper;
@Override
public String getName() {
return "LoadMatchedSamples";
}
@Override
protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) {
// 检查sampleListIds是否为空
List<Long> sampleListIds = context.getSampleListIds();
if (sampleListIds == null || sampleListIds.isEmpty()) {
log.debug("sampleListIds为空,跳过加载匹配样本,faceId={}", context.getFaceId());
return false;
}
return true;
}
@Override
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
List<Long> sampleListIds = context.getSampleListIds();
Long faceId = context.getFaceId();
try {
// 批量加载样本实体
List<FaceSampleEntity> faceSamples = faceSampleMapper.listByIds(sampleListIds);
if (faceSamples == null || faceSamples.isEmpty()) {
log.warn("未找到任何匹配样本实体,faceId={}, sampleListIds={}", faceId, sampleListIds);
return StageResult.skipped("未找到匹配样本实体");
}
// 存入context缓存,供后续Stage使用
context.setFaceSamples(faceSamples);
log.info("加载匹配样本实体完成: faceId={}, 样本数={}", faceId, faceSamples.size());
return StageResult.success(String.format("已加载%d个样本实体", faceSamples.size()));
} catch (Exception e) {
log.error("加载匹配样本实体失败,faceId={}", faceId, e);
// 加载失败影响后续流程,返回失败
return StageResult.failed("加载匹配样本实体失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,92 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.face.pipeline.annotation.StageConfig;
import com.ycwl.basic.face.pipeline.core.AbstractFaceMatchingStage;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.face.pipeline.core.StageResult;
import com.ycwl.basic.face.pipeline.enums.StageOptionalMode;
import com.ycwl.basic.mapper.SourceMapper;
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
import com.ycwl.basic.repository.MemberRelationRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 持久化关联关系Stage
* 负责过滤并保存源文件关联关系到数据库
*
* 职责:
* 1. 从context.memberSourceList读取关联关系
* 2. 过滤已存在的关联关系和无效的source引用
* 3. 保存到数据库
* 4. 清除缓存
*/
@Slf4j
@Component
@StageConfig(
stageId = "persist_relations",
optionalMode = StageOptionalMode.FORCE_ON,
description = "持久化源文件关联关系"
)
public class PersistRelationsStage extends AbstractFaceMatchingStage<FaceMatchingContext> {
@Autowired
private SourceMapper sourceMapper;
@Autowired
private MemberRelationRepository memberRelationRepository;
@Override
public String getName() {
return "PersistRelations";
}
@Override
protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) {
// 只有当memberSourceList不为空时才执行
List<MemberSourceEntity> memberSourceList = context.getMemberSourceList();
if (memberSourceList == null || memberSourceList.isEmpty()) {
log.debug("memberSourceList为空,跳过持久化,faceId={}", context.getFaceId());
return false;
}
return true;
}
@Override
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
List<MemberSourceEntity> memberSourceEntityList = context.getMemberSourceList();
Long faceId = context.getFaceId();
try {
// 1. 过滤已存在的关联关系
List<MemberSourceEntity> existingFiltered = sourceMapper.filterExistingRelations(memberSourceEntityList);
// 2. 过滤无效的source引用
List<MemberSourceEntity> validFiltered = sourceMapper.filterValidSourceRelations(existingFiltered);
if (!validFiltered.isEmpty()) {
// 3. 保存到数据库
sourceMapper.addRelations(validFiltered);
log.debug("创建关联关系: faceId={}, 原始数量={}, 过滤后数量={}",
faceId, memberSourceEntityList.size(), validFiltered.size());
} else {
log.warn("没有有效的关联关系可创建: faceId={}, 原始数量={}",
faceId, memberSourceEntityList.size());
return StageResult.skipped("没有有效的关联关系可创建");
}
// 4. 清除缓存
memberRelationRepository.clearSCacheByFace(faceId);
return StageResult.success(String.format("持久化了%d条关联关系", validFiltered.size()));
} catch (Exception e) {
log.error("持久化关联关系失败,faceId={}", faceId, e);
return StageResult.failed("保存关联关系失败", e);
}
}
}

View File

@@ -0,0 +1,91 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.face.pipeline.annotation.StageConfig;
import com.ycwl.basic.face.pipeline.core.AbstractFaceMatchingStage;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.face.pipeline.core.StageResult;
import com.ycwl.basic.face.pipeline.enums.StageOptionalMode;
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
import com.ycwl.basic.repository.FaceRepository;
import com.ycwl.basic.repository.ScenicRepository;
import com.ycwl.basic.service.pc.ScenicService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 准备上下文Stage
* 负责加载人脸实体、景区配置、识别适配器等必要数据
*
* 职责:
* 1. 加载FaceEntity(如不存在则失败)
* 2. 检查是否人工选择(是则跳过,除非isNew=true)
* 3. 加载ScenicConfigManager和IFaceBodyAdapter
* 4. 更新Context
*/
@Slf4j
@Component
@StageConfig(
stageId = "prepare_context",
optionalMode = StageOptionalMode.FORCE_ON,
description = "准备人脸匹配上下文数据"
)
public class PrepareContextStage extends AbstractFaceMatchingStage<FaceMatchingContext> {
@Autowired
private FaceRepository faceRepository;
@Autowired
private ScenicRepository scenicRepository;
@Autowired
private ScenicService scenicService;
@Override
public String getName() {
return "PrepareContext";
}
@Override
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
Long faceId = context.getFaceId();
boolean isNew = context.isNew();
// 1. 加载人脸实体
FaceEntity face = faceRepository.getFace(faceId);
if (face == null) {
log.warn("人脸不存在,faceId: {}", faceId);
return StageResult.failed("人脸不存在,faceId: " + faceId);
}
context.setFace(face);
log.debug("加载人脸实体成功: faceId={}, memberId={}, scenicId={}",
faceId, face.getMemberId(), face.getScenicId());
// 2. 检查是否人工选择
// 人工选择的无需重新匹配(新用户除外)
if (!isNew && Integer.valueOf(1).equals(face.getIsManual())) {
log.info("人工选择的人脸,无需匹配,faceId: {}", faceId);
return StageResult.skipped("人工选择的人脸,无需重新匹配");
}
// 3. 加载景区配置
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(face.getScenicId());
context.setScenicConfig(scenicConfig);
log.debug("加载景区配置成功: scenicId={}", face.getScenicId());
// 4. 加载人脸识别适配器
IFaceBodyAdapter faceBodyAdapter = scenicService.getScenicFaceBodyAdapter(face.getScenicId());
if (faceBodyAdapter == null) {
log.error("无法获取人脸识别适配器,scenicId: {}", face.getScenicId());
return StageResult.failed("人脸识别服务不可用,请稍后再试");
}
context.setFaceBodyAdapter(faceBodyAdapter);
log.debug("加载人脸识别适配器成功: scenicId={}", face.getScenicId());
return StageResult.success("上下文准备完成");
}
}

View File

@@ -0,0 +1,86 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.face.pipeline.annotation.StageConfig;
import com.ycwl.basic.face.pipeline.core.AbstractFaceMatchingStage;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.face.pipeline.core.StageResult;
import com.ycwl.basic.face.pipeline.enums.StageOptionalMode;
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
import com.ycwl.basic.service.pc.processor.BuyStatusProcessor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 处理购买状态Stage
* 负责更新源文件的购买状态标记
*
* 职责:
* 1. 从context.memberSourceList读取源文件关联列表
* 2. 从context.freeSourceIds读取免费源文件ID列表
* 3. 调用buyStatusProcessor.processBuyStatus()更新购买状态
*
* 前置条件: context.memberSourceList不为空
* 业务说明: 购买状态影响前端显示和用户下载权限
*/
@Slf4j
@Component
@StageConfig(
stageId = "process_buy_status",
optionalMode = StageOptionalMode.SUPPORT,
description = "处理源文件购买状态",
defaultEnabled = true
)
public class ProcessBuyStatusStage extends AbstractFaceMatchingStage<FaceMatchingContext> {
@Autowired
private BuyStatusProcessor buyStatusProcessor;
@Override
public String getName() {
return "ProcessBuyStatus";
}
@Override
protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) {
// 只有当memberSourceList不为空时才执行
List<MemberSourceEntity> memberSourceList = context.getMemberSourceList();
if (memberSourceList == null || memberSourceList.isEmpty()) {
log.debug("memberSourceList为空,跳过购买状态处理,faceId={}", context.getFaceId());
return false;
}
return true;
}
@Override
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
List<MemberSourceEntity> memberSourceEntityList = context.getMemberSourceList();
List<Long> freeSourceIds = context.getFreeSourceIds();
Long memberId = context.getFace().getMemberId();
Long scenicId = context.getFace().getScenicId();
Long faceId = context.getFaceId();
try {
// 处理购买状态
buyStatusProcessor.processBuyStatus(
memberSourceEntityList,
freeSourceIds,
memberId,
scenicId,
faceId);
log.info("购买状态处理完成: faceId={}, 源文件数={}, 免费数={}",
faceId, memberSourceEntityList.size(),
freeSourceIds != null ? freeSourceIds.size() : 0);
return StageResult.success("购买状态处理完成");
} catch (Exception e) {
log.error("处理购买状态失败,faceId={}", faceId, e);
// 购买状态处理失败不影响主流程,返回降级
return StageResult.degraded("购买状态处理失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,83 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.face.pipeline.annotation.StageConfig;
import com.ycwl.basic.face.pipeline.core.AbstractFaceMatchingStage;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.face.pipeline.core.StageResult;
import com.ycwl.basic.face.pipeline.enums.StageOptionalMode;
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
import com.ycwl.basic.service.pc.processor.SourceRelationProcessor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 处理免费源文件Stage
* 负责根据业务规则确定哪些源文件可以免费访问
*
* 职责:
* 1. 从context.memberSourceList读取源文件关联列表
* 2. 调用sourceRelationProcessor.processFreeSourceLogic()确定免费源文件
* 3. 更新context.freeSourceIds
*
* 前置条件: context.memberSourceList不为空
* 后置条件: context.freeSourceIds已设置
*/
@Slf4j
@Component
@StageConfig(
stageId = "process_free_source",
optionalMode = StageOptionalMode.SUPPORT,
description = "处理免费源文件逻辑",
defaultEnabled = true
)
public class ProcessFreeSourceStage extends AbstractFaceMatchingStage<FaceMatchingContext> {
@Autowired
private SourceRelationProcessor sourceRelationProcessor;
@Override
public String getName() {
return "ProcessFreeSource";
}
@Override
protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) {
// 只有当memberSourceList不为空时才执行
List<MemberSourceEntity> memberSourceList = context.getMemberSourceList();
if (memberSourceList == null || memberSourceList.isEmpty()) {
log.debug("memberSourceList为空,跳过免费逻辑,faceId={}", context.getFaceId());
return false;
}
return true;
}
@Override
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
List<MemberSourceEntity> memberSourceEntityList = context.getMemberSourceList();
Long scenicId = context.getFace().getScenicId();
boolean isNew = context.isNew();
Long faceId = context.getFaceId();
try {
// 处理免费逻辑
List<Long> freeSourceIds = sourceRelationProcessor.processFreeSourceLogic(
memberSourceEntityList, scenicId, isNew);
context.setFreeSourceIds(freeSourceIds);
log.info("免费源文件处理完成: faceId={}, 总源文件数={}, 免费数={}",
faceId, memberSourceEntityList.size(), freeSourceIds != null ? freeSourceIds.size() : 0);
return StageResult.success(String.format("确定了%d个免费源文件",
freeSourceIds != null ? freeSourceIds.size() : 0));
} catch (Exception e) {
log.error("处理免费源文件失败,faceId={}", faceId, e);
// 免费逻辑失败不影响主流程,返回降级
return StageResult.degraded("免费源文件处理失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,65 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.face.pipeline.annotation.StageConfig;
import com.ycwl.basic.face.pipeline.core.AbstractFaceMatchingStage;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.face.pipeline.core.StageResult;
import com.ycwl.basic.face.pipeline.enums.FaceMatchingScene;
import com.ycwl.basic.face.pipeline.enums.StageOptionalMode;
import com.ycwl.basic.service.pc.helper.FaceMetricsRecorder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 记录自定义匹配次数Stage
* 负责记录自定义人脸匹配调用次数,用于监控
*
* 职责:
* 1. 仅在CUSTOM_MATCHING场景执行
* 2. 调用metricsRecorder.recordCustomMatchCount()记录次数
*/
@Slf4j
@Component
@StageConfig(
stageId = "record_custom_match_metrics",
optionalMode = StageOptionalMode.SUPPORT,
description = "记录自定义匹配指标",
defaultEnabled = true
)
public class RecordCustomMatchMetricsStage extends AbstractFaceMatchingStage<FaceMatchingContext> {
@Autowired
private FaceMetricsRecorder metricsRecorder;
@Override
public String getName() {
return "RecordCustomMatchMetrics";
}
@Override
protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) {
// 只有自定义匹配场景才执行
if (context.getScene() != FaceMatchingScene.CUSTOM_MATCHING) {
log.debug("非自定义匹配场景,跳过记录,faceId={}", context.getFaceId());
return false;
}
return true;
}
@Override
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
Long faceId = context.getFaceId();
try {
metricsRecorder.recordCustomMatchCount(faceId);
log.debug("记录自定义匹配次数: faceId={}", faceId);
return StageResult.success("自定义匹配指标记录完成");
} catch (Exception e) {
log.error("记录自定义匹配指标失败,faceId={}", faceId, e);
// 指标记录失败不影响主流程,返回降级
return StageResult.degraded("指标记录失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,63 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.face.pipeline.annotation.StageConfig;
import com.ycwl.basic.face.pipeline.core.AbstractFaceMatchingStage;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.face.pipeline.core.StageResult;
import com.ycwl.basic.face.pipeline.enums.StageOptionalMode;
import com.ycwl.basic.service.pc.helper.FaceMetricsRecorder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 记录识别次数Stage
* 负责记录人脸识别调用次数,用于监控和防重复检查
*
* 职责:
* 1. 调用metricsRecorder.recordRecognitionCount()记录识别次数
* 2. 检查searchResult是否触发低阈值检测
* 3. 如果是,调用metricsRecorder.recordLowThreshold()记录
*/
@Slf4j
@Component
@StageConfig(
stageId = "record_metrics",
optionalMode = StageOptionalMode.SUPPORT,
description = "记录人脸识别指标",
defaultEnabled = true
)
public class RecordMetricsStage extends AbstractFaceMatchingStage<FaceMatchingContext> {
@Autowired
private FaceMetricsRecorder metricsRecorder;
@Override
public String getName() {
return "RecordMetrics";
}
@Override
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
Long faceId = context.getFaceId();
try {
// 1. 记录识别次数
metricsRecorder.recordRecognitionCount(faceId);
log.debug("记录识别次数: faceId={}", faceId);
// 2. 检查是否触发低阈值检测
if (context.getSearchResult() != null && context.getSearchResult().isLowThreshold()) {
metricsRecorder.recordLowThreshold(faceId);
log.debug("触发低阈值检测,记录faceId: {}", faceId);
}
return StageResult.success("识别指标记录完成");
} catch (Exception e) {
log.error("记录识别指标失败,faceId={}", faceId, e);
// 指标记录失败不影响主流程,返回降级
return StageResult.degraded("指标记录失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,63 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.biz.TaskStatusBiz;
import com.ycwl.basic.face.pipeline.annotation.StageConfig;
import com.ycwl.basic.face.pipeline.core.AbstractFaceMatchingStage;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.face.pipeline.core.StageResult;
import com.ycwl.basic.face.pipeline.enums.StageOptionalMode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 设置任务状态Stage
* 负责为新用户设置任务状态为待处理
*
* 职责:
* 1. 仅在isNew=true时执行
* 2. 调用taskStatusBiz.setFaceCutStatus(faceId, 0)
*/
@Slf4j
@Component
@StageConfig(
stageId = "set_task_status",
optionalMode = StageOptionalMode.FORCE_ON,
description = "设置新用户任务状态"
)
public class SetTaskStatusStage extends AbstractFaceMatchingStage<FaceMatchingContext> {
@Autowired
private TaskStatusBiz taskStatusBiz;
@Override
public String getName() {
return "SetTaskStatus";
}
@Override
protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) {
// 只有新用户才执行
if (!context.isNew()) {
log.debug("非新用户,跳过设置任务状态,faceId={}", context.getFaceId());
return false;
}
return true;
}
@Override
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
Long faceId = context.getFaceId();
try {
taskStatusBiz.setFaceCutStatus(faceId, 0);
log.debug("设置新用户任务状态: faceId={}, status=0", faceId);
return StageResult.success("任务状态已设置");
} catch (Exception e) {
log.error("设置任务状态失败,faceId={}", faceId, e);
// 任务状态设置失败不影响主流程,返回降级
return StageResult.degraded("任务状态设置失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,95 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.face.pipeline.annotation.StageConfig;
import com.ycwl.basic.face.pipeline.core.AbstractFaceMatchingStage;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.face.pipeline.core.StageResult;
import com.ycwl.basic.face.pipeline.enums.StageOptionalMode;
import com.ycwl.basic.mapper.FaceMapper;
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
import com.ycwl.basic.model.task.resp.SearchFaceRespVo;
import com.ycwl.basic.repository.FaceRepository;
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.stream.Collectors;
/**
* 更新人脸结果Stage
* 负责将人脸识别结果保存到数据库
*
* 职责:
* 1. 从context.searchResult读取识别结果
* 2. 更新FaceEntity(score、matchResult、firstMatchRate、matchSampleIds)
* 3. 清除缓存
*/
@Slf4j
@Component
@StageConfig(
stageId = "update_face_result",
optionalMode = StageOptionalMode.FORCE_ON,
description = "更新人脸识别结果到数据库"
)
public class UpdateFaceResultStage extends AbstractFaceMatchingStage<FaceMatchingContext> {
@Autowired
private FaceMapper faceMapper;
@Autowired
private FaceRepository faceRepository;
@Override
public String getName() {
return "UpdateFaceResult";
}
@Override
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
SearchFaceRespVo searchResult = context.getSearchResult();
if (searchResult == null) {
log.warn("searchResult为空,跳过更新人脸结果,faceId={}", context.getFaceId());
return StageResult.skipped("searchResult为空");
}
try {
FaceEntity originalFace = context.getFace();
Long faceId = context.getFaceId();
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(faceId);
log.debug("人脸结果更新成功:faceId={}, score={}, sampleCount={}",
faceId, searchResult.getScore(),
searchResult.getSampleListIds() != null ? searchResult.getSampleListIds().size() : 0);
return StageResult.success("人脸结果更新成功");
} catch (Exception e) {
log.error("更新人脸结果失败,faceId={}", context.getFaceId(), e);
return StageResult.failed("保存人脸识别结果失败", e);
}
}
}