You've already forked FrameTour-BE
Merge branch 'face_service_refactor'
# Conflicts: # src/main/java/com/ycwl/basic/service/pc/impl/FaceServiceImpl.java
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
package com.ycwl.basic.service.pc.helper;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import static com.ycwl.basic.constant.FaceConstant.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸识别指标记录器
|
||||||
|
* 负责记录人脸识别相关的计数指标到Redis
|
||||||
|
*
|
||||||
|
* @author longbinbin
|
||||||
|
* @date 2025-01-31
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class FaceMetricsRecorder {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RedisTemplate<String, String> redisTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录人脸识别次数到Redis
|
||||||
|
* 设置2天过期时间
|
||||||
|
*
|
||||||
|
* @param faceId 人脸ID
|
||||||
|
*/
|
||||||
|
public void recordRecognitionCount(Long faceId) {
|
||||||
|
if (faceId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
String redisKey = FACE_RECOGNITION_COUNT_PFX + faceId;
|
||||||
|
|
||||||
|
// 使用Redis原子操作INCR增加计数
|
||||||
|
Long count = redisTemplate.opsForValue().increment(redisKey);
|
||||||
|
|
||||||
|
// 设置2天过期时间(48小时)
|
||||||
|
redisTemplate.expire(redisKey, 2, TimeUnit.DAYS);
|
||||||
|
|
||||||
|
log.debug("人脸识别计数更新:faceId={}, count={}", faceId, count);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 计数失败不应影响主要业务逻辑,只记录错误日志
|
||||||
|
log.error("记录人脸识别次数失败:faceId={}", faceId, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录自定义人脸匹配次数到Redis
|
||||||
|
* 设置2天过期时间
|
||||||
|
*
|
||||||
|
* @param faceId 人脸ID
|
||||||
|
*/
|
||||||
|
public void recordCustomMatchCount(Long faceId) {
|
||||||
|
if (faceId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
String redisKey = FACE_CUSTOM_MATCH_COUNT_PFX + faceId;
|
||||||
|
|
||||||
|
Long count = redisTemplate.opsForValue().increment(redisKey);
|
||||||
|
redisTemplate.expire(redisKey, 2, TimeUnit.DAYS);
|
||||||
|
|
||||||
|
log.debug("自定义人脸匹配计数更新:faceId={}, count={}", faceId, count);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("记录自定义人脸匹配次数失败:faceId={}", faceId, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录低阈值检测的人脸ID到Redis
|
||||||
|
* 设置2天过期时间
|
||||||
|
*
|
||||||
|
* @param faceId 人脸ID
|
||||||
|
*/
|
||||||
|
public void recordLowThreshold(Long faceId) {
|
||||||
|
if (faceId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
String redisKey = FACE_LOW_THRESHOLD_PFX + faceId;
|
||||||
|
|
||||||
|
// 设置标记,表示该人脸ID触发了低阈值检测
|
||||||
|
redisTemplate.opsForValue().set(redisKey, "1", 2, TimeUnit.DAYS);
|
||||||
|
|
||||||
|
log.debug("记录低阈值检测人脸:faceId={}", faceId);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 记录失败不应影响主要业务逻辑,只记录错误日志
|
||||||
|
log.error("记录低阈值检测人脸失败:faceId={}", faceId, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取人脸识别次数
|
||||||
|
*
|
||||||
|
* @param faceId 人脸ID
|
||||||
|
* @return 识别次数
|
||||||
|
*/
|
||||||
|
public long getRecognitionCount(Long faceId) {
|
||||||
|
if (faceId == null) {
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
String countKey = FACE_RECOGNITION_COUNT_PFX + faceId;
|
||||||
|
String countStr = redisTemplate.opsForValue().get(countKey);
|
||||||
|
if (countStr != null) {
|
||||||
|
return Long.parseLong(countStr);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("获取识别次数失败:faceId={}", faceId, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取自定义匹配次数
|
||||||
|
*
|
||||||
|
* @param faceId 人脸ID
|
||||||
|
* @return 自定义匹配次数
|
||||||
|
*/
|
||||||
|
public long getCustomMatchCount(Long faceId) {
|
||||||
|
if (faceId == null) {
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
String customMatchKey = FACE_CUSTOM_MATCH_COUNT_PFX + faceId;
|
||||||
|
String customMatchCountStr = redisTemplate.opsForValue().get(customMatchKey);
|
||||||
|
if (customMatchCountStr != null) {
|
||||||
|
return Long.parseLong(customMatchCountStr);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("获取自定义匹配次数失败:faceId={}", faceId, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否触发过低阈值检测
|
||||||
|
*
|
||||||
|
* @param faceId 人脸ID
|
||||||
|
* @return 是否触发过低阈值检测
|
||||||
|
*/
|
||||||
|
public boolean hasLowThreshold(Long faceId) {
|
||||||
|
if (faceId == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
String lowThresholdKey = FACE_LOW_THRESHOLD_PFX + faceId;
|
||||||
|
return Boolean.TRUE.equals(redisTemplate.hasKey(lowThresholdKey));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("检查低阈值状态失败:faceId={}", faceId, e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
package com.ycwl.basic.service.pc.helper;
|
||||||
|
|
||||||
|
import com.ycwl.basic.repository.ScenicRepository;
|
||||||
|
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 景区配置门面类
|
||||||
|
* 提供类型安全的配置访问方法,避免配置键的字符串硬编码
|
||||||
|
* 集中管理所有景区配置项的访问逻辑
|
||||||
|
*
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-10-31
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class ScenicConfigFacade {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ScenicRepository scenicRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取景区配置管理器
|
||||||
|
*/
|
||||||
|
private ScenicConfigManager getConfig(Long scenicId) {
|
||||||
|
return scenicRepository.getScenicConfigManager(scenicId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 人脸识别相关配置 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否优先手动选择人脸
|
||||||
|
* 如果为 true,则不自动创建任务
|
||||||
|
*
|
||||||
|
* @param scenicId 景区ID
|
||||||
|
* @return true-需要手动选择,false-自动创建任务
|
||||||
|
*/
|
||||||
|
public boolean isFaceSelectFirst(Long scenicId) {
|
||||||
|
ScenicConfigManager config = getConfig(scenicId);
|
||||||
|
if (config == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Boolean value = config.getBoolean("face_select_first");
|
||||||
|
return Boolean.TRUE.equals(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取自定义人脸匹配的最大次数
|
||||||
|
* 用于限制用户手动重新匹配的次数
|
||||||
|
*
|
||||||
|
* @param scenicId 景区ID
|
||||||
|
* @return 最大匹配次数,null 或 0 表示不限制
|
||||||
|
*/
|
||||||
|
public Integer getFaceSelectMaxCount(Long scenicId) {
|
||||||
|
ScenicConfigManager config = getConfig(scenicId);
|
||||||
|
if (config == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return config.getInteger("face_select_max_count");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取人脸分数低阈值
|
||||||
|
* 低于此分数的匹配结果会被标记为低置信度
|
||||||
|
*
|
||||||
|
* @param scenicId 景区ID
|
||||||
|
* @return 分数阈值,默认 30.0
|
||||||
|
*/
|
||||||
|
public Float getFaceScoreLowThreshold(Long scenicId) {
|
||||||
|
ScenicConfigManager config = getConfig(scenicId);
|
||||||
|
if (config == null) {
|
||||||
|
return 30.0F;
|
||||||
|
}
|
||||||
|
return config.getFloat("face_score_low_threshold", 30.0F);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取人脸检测辅助阈值
|
||||||
|
* 当匹配结果数量少于此阈值时,会触发补救逻辑
|
||||||
|
*
|
||||||
|
* @param scenicId 景区ID
|
||||||
|
* @return 辅助阈值,默认 0(不启用)
|
||||||
|
*/
|
||||||
|
public Integer getFaceDetectHelperThreshold(Long scenicId) {
|
||||||
|
ScenicConfigManager config = getConfig(scenicId);
|
||||||
|
if (config == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return config.getInteger("face_detect_helper_threshold", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取人脸选择后处理模式
|
||||||
|
* 0-并集模式,1-交集模式
|
||||||
|
*
|
||||||
|
* @param scenicId 景区ID
|
||||||
|
* @return 后处理模式,默认 0
|
||||||
|
*/
|
||||||
|
public Integer getFaceSelectPostMode(Long scenicId) {
|
||||||
|
ScenicConfigManager config = getConfig(scenicId);
|
||||||
|
if (config == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return config.getInteger("face_select_post_mode", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取重新匹配模式
|
||||||
|
* 用于决定在什么条件下需要重新进行人脸匹配
|
||||||
|
*
|
||||||
|
* @param scenicId 景区ID
|
||||||
|
* @return 匹配模式,默认 0
|
||||||
|
*/
|
||||||
|
public Integer getRematchMode(Long scenicId) {
|
||||||
|
ScenicConfigManager config = getConfig(scenicId);
|
||||||
|
if (config == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return config.getInteger("re_match_mode", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 源文件相关配置 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否禁用源图片功能
|
||||||
|
*
|
||||||
|
* @param scenicId 景区ID
|
||||||
|
* @return true-禁用,false-启用
|
||||||
|
*/
|
||||||
|
public boolean isDisableSourceImage(Long scenicId) {
|
||||||
|
ScenicConfigManager config = getConfig(scenicId);
|
||||||
|
if (config == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Boolean value = config.getBoolean("disable_source_image");
|
||||||
|
return Boolean.TRUE.equals(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否禁用源视频功能
|
||||||
|
*
|
||||||
|
* @param scenicId 景区ID
|
||||||
|
* @return true-禁用,false-启用
|
||||||
|
*/
|
||||||
|
public boolean isDisableSourceVideo(Long scenicId) {
|
||||||
|
ScenicConfigManager config = getConfig(scenicId);
|
||||||
|
if (config == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Boolean value = config.getBoolean("disable_source_video");
|
||||||
|
return Boolean.TRUE.equals(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取免费照片数量
|
||||||
|
* 新用户首次识别时赠送的免费照片数量
|
||||||
|
*
|
||||||
|
* @param scenicId 景区ID
|
||||||
|
* @return 免费照片数量,null 或 0 表示不赠送
|
||||||
|
*/
|
||||||
|
public Integer getPhotoFreeNum(Long scenicId) {
|
||||||
|
ScenicConfigManager config = getConfig(scenicId);
|
||||||
|
if (config == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return config.getInteger("photo_free_num");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 游玩时间相关配置 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最大游玩时间(分钟)
|
||||||
|
* 用于判断照片是否在合理的游玩时间范围内
|
||||||
|
*
|
||||||
|
* @param scenicId 景区ID
|
||||||
|
* @return 最大游玩时间(分钟),null 表示不限制
|
||||||
|
*/
|
||||||
|
public Integer getTourTime(Long scenicId) {
|
||||||
|
ScenicConfigManager config = getConfig(scenicId);
|
||||||
|
if (config == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return config.getInteger("tour_time");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最小游玩时间(分钟)
|
||||||
|
* 用于判断照片是否在合理的游玩时间范围内
|
||||||
|
*
|
||||||
|
* @param scenicId 景区ID
|
||||||
|
* @return 最小游玩时间(分钟),null 表示不限制
|
||||||
|
*/
|
||||||
|
public Integer getTourMinTime(Long scenicId) {
|
||||||
|
ScenicConfigManager config = getConfig(scenicId);
|
||||||
|
if (config == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return config.getInteger("tour_min_time");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否配置了游玩时间限制
|
||||||
|
*
|
||||||
|
* @param scenicId 景区ID
|
||||||
|
* @return true-已配置,false-未配置
|
||||||
|
*/
|
||||||
|
public boolean hasTourTimeConfig(Long scenicId) {
|
||||||
|
Integer maxTime = getTourTime(scenicId);
|
||||||
|
Integer minTime = getTourMinTime(scenicId);
|
||||||
|
return maxTime != null && minTime != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
package com.ycwl.basic.service.pc.helper;
|
||||||
|
|
||||||
|
import com.ycwl.basic.model.task.resp.SearchFaceRespVo;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索结果合并器
|
||||||
|
* 负责合并多个人脸搜索结果
|
||||||
|
*
|
||||||
|
* @author longbinbin
|
||||||
|
* @date 2025-01-31
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class SearchResultMerger {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 合并多个搜索结果(默认使用并集模式)
|
||||||
|
*
|
||||||
|
* @param searchResults 搜索结果列表
|
||||||
|
* @return 合并后的结果
|
||||||
|
*/
|
||||||
|
public SearchFaceRespVo merge(List<SearchFaceRespVo> searchResults) {
|
||||||
|
return merge(searchResults, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 合并多个搜索结果
|
||||||
|
*
|
||||||
|
* @param searchResults 搜索结果列表
|
||||||
|
* @param mergeMode 合并模式:0-并集,1-交集
|
||||||
|
* @return 合并后的结果
|
||||||
|
*/
|
||||||
|
public SearchFaceRespVo merge(List<SearchFaceRespVo> searchResults, Integer mergeMode) {
|
||||||
|
SearchFaceRespVo mergedResult = new SearchFaceRespVo();
|
||||||
|
|
||||||
|
if (searchResults == null || searchResults.isEmpty()) {
|
||||||
|
return mergedResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> allSearchJsons = new ArrayList<>();
|
||||||
|
float maxScore = 0f;
|
||||||
|
float maxFirstMatchRate = 0f;
|
||||||
|
boolean hasLowThreshold = false;
|
||||||
|
|
||||||
|
// 收集基础信息
|
||||||
|
for (SearchFaceRespVo result : searchResults) {
|
||||||
|
if (result.getSearchResultJson() != null) {
|
||||||
|
allSearchJsons.add(result.getSearchResultJson());
|
||||||
|
}
|
||||||
|
if (result.getScore() > maxScore) {
|
||||||
|
maxScore = result.getScore();
|
||||||
|
}
|
||||||
|
if (result.getFirstMatchRate() > maxFirstMatchRate) {
|
||||||
|
maxFirstMatchRate = result.getFirstMatchRate();
|
||||||
|
}
|
||||||
|
if (result.isLowThreshold()) {
|
||||||
|
hasLowThreshold = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据合并模式处理样本ID
|
||||||
|
List<Long> finalSampleIds;
|
||||||
|
if (Integer.valueOf(1).equals(mergeMode)) {
|
||||||
|
// 模式1:交集 - 只保留所有搜索结果中都出现的样本ID
|
||||||
|
finalSampleIds = computeIntersection(searchResults);
|
||||||
|
log.debug("使用交集模式合并搜索结果,交集样本数: {}", finalSampleIds.size());
|
||||||
|
} else {
|
||||||
|
// 模式0:并集(默认) - 收集所有样本ID并去重
|
||||||
|
Set<Long> allSampleIds = new LinkedHashSet<>();
|
||||||
|
for (SearchFaceRespVo result : searchResults) {
|
||||||
|
if (result.getSampleListIds() != null) {
|
||||||
|
allSampleIds.addAll(result.getSampleListIds());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finalSampleIds = new ArrayList<>(allSampleIds);
|
||||||
|
log.debug("使用并集模式合并搜索结果,并集样本数: {}", finalSampleIds.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
mergedResult.setSampleListIds(finalSampleIds);
|
||||||
|
mergedResult.setSearchResultJson(String.join("|", allSearchJsons));
|
||||||
|
mergedResult.setScore(maxScore);
|
||||||
|
mergedResult.setFirstMatchRate(maxFirstMatchRate);
|
||||||
|
mergedResult.setLowThreshold(hasLowThreshold);
|
||||||
|
|
||||||
|
log.debug("合并搜索结果完成,模式={}, 最终样本数: {}", mergeMode, finalSampleIds.size());
|
||||||
|
|
||||||
|
return mergedResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算多个搜索结果的交集
|
||||||
|
* 返回在所有搜索结果中都出现的样本ID
|
||||||
|
*
|
||||||
|
* @param searchResults 搜索结果列表
|
||||||
|
* @return 交集样本ID列表
|
||||||
|
*/
|
||||||
|
public List<Long> computeIntersection(List<SearchFaceRespVo> searchResults) {
|
||||||
|
if (searchResults == null || searchResults.isEmpty()) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤掉空结果
|
||||||
|
List<List<Long>> validSampleLists = searchResults.stream()
|
||||||
|
.filter(result -> result.getSampleListIds() != null && !result.getSampleListIds().isEmpty())
|
||||||
|
.map(SearchFaceRespVo::getSampleListIds)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (validSampleLists.isEmpty()) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果只有一个有效结果,直接返回
|
||||||
|
if (validSampleLists.size() == 1) {
|
||||||
|
return new ArrayList<>(validSampleLists.getFirst());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算交集:从第一个列表开始,保留在所有其他列表中都出现的ID
|
||||||
|
Set<Long> intersection = new LinkedHashSet<>(validSampleLists.getFirst());
|
||||||
|
|
||||||
|
for (int i = 1; i < validSampleLists.size(); i++) {
|
||||||
|
intersection.retainAll(validSampleLists.get(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ArrayList<>(intersection);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建直接结果(模式2:不搜索,直接使用用户选择的faceSampleIds)
|
||||||
|
*
|
||||||
|
* @param faceSampleIds 用户选择的人脸样本ID列表
|
||||||
|
* @return 搜索结果对象
|
||||||
|
*/
|
||||||
|
public SearchFaceRespVo createDirectResult(List<Long> faceSampleIds) {
|
||||||
|
SearchFaceRespVo result = new SearchFaceRespVo();
|
||||||
|
|
||||||
|
// 直接使用用户选择的faceSampleIds作为结果
|
||||||
|
result.setSampleListIds(new ArrayList<>(faceSampleIds));
|
||||||
|
|
||||||
|
// 设置默认值
|
||||||
|
result.setScore(1.0f);
|
||||||
|
result.setFirstMatchRate(1.0f);
|
||||||
|
result.setLowThreshold(false);
|
||||||
|
result.setSearchResultJson("");
|
||||||
|
|
||||||
|
log.debug("创建直接结果,样本数: {}", faceSampleIds.size());
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,6 @@ import com.github.pagehelper.PageInfo;
|
|||||||
import com.ycwl.basic.biz.OrderBiz;
|
import com.ycwl.basic.biz.OrderBiz;
|
||||||
import com.ycwl.basic.biz.TemplateBiz;
|
import com.ycwl.basic.biz.TemplateBiz;
|
||||||
import com.ycwl.basic.constant.BaseContextHandler;
|
import com.ycwl.basic.constant.BaseContextHandler;
|
||||||
import com.ycwl.basic.enums.StatisticEnum;
|
|
||||||
import com.ycwl.basic.exception.BaseException;
|
import com.ycwl.basic.exception.BaseException;
|
||||||
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
|
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
|
||||||
import com.ycwl.basic.facebody.entity.SearchFaceResultItem;
|
import com.ycwl.basic.facebody.entity.SearchFaceResultItem;
|
||||||
@@ -24,7 +23,6 @@ import com.ycwl.basic.model.mobile.face.FaceStatusResp;
|
|||||||
import com.ycwl.basic.model.mobile.goods.VideoTaskStatusVO;
|
import com.ycwl.basic.model.mobile.goods.VideoTaskStatusVO;
|
||||||
import com.ycwl.basic.model.mobile.order.IsBuyRespVO;
|
import com.ycwl.basic.model.mobile.order.IsBuyRespVO;
|
||||||
import com.ycwl.basic.model.mobile.scenic.content.ContentPageVO;
|
import com.ycwl.basic.model.mobile.scenic.content.ContentPageVO;
|
||||||
import com.ycwl.basic.model.mobile.statistic.req.StatisticsRecordAddReq;
|
|
||||||
import com.ycwl.basic.model.pc.device.entity.DeviceEntity;
|
import com.ycwl.basic.model.pc.device.entity.DeviceEntity;
|
||||||
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
||||||
import com.ycwl.basic.model.mobile.face.FaceRecognitionUpdateReq;
|
import com.ycwl.basic.model.mobile.face.FaceRecognitionUpdateReq;
|
||||||
@@ -58,6 +56,18 @@ import com.ycwl.basic.repository.VideoTaskRepository;
|
|||||||
import com.ycwl.basic.service.mobile.GoodsService;
|
import com.ycwl.basic.service.mobile.GoodsService;
|
||||||
import com.ycwl.basic.service.pc.FaceService;
|
import com.ycwl.basic.service.pc.FaceService;
|
||||||
import com.ycwl.basic.service.pc.ScenicService;
|
import com.ycwl.basic.service.pc.ScenicService;
|
||||||
|
import com.ycwl.basic.constant.SourceType;
|
||||||
|
import com.ycwl.basic.service.pc.helper.FaceMetricsRecorder;
|
||||||
|
import com.ycwl.basic.service.pc.helper.SearchResultMerger;
|
||||||
|
import com.ycwl.basic.service.pc.helper.ScenicConfigFacade;
|
||||||
|
import com.ycwl.basic.service.pc.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;
|
||||||
|
import com.ycwl.basic.service.pc.processor.VideoRecreationHandler;
|
||||||
|
import com.ycwl.basic.service.pc.strategy.RematchContext;
|
||||||
|
import com.ycwl.basic.service.pc.strategy.RematchModeStrategy;
|
||||||
|
import com.ycwl.basic.service.pc.strategy.RematchStrategyFactory;
|
||||||
import com.ycwl.basic.service.printer.PrinterService;
|
import com.ycwl.basic.service.printer.PrinterService;
|
||||||
import com.ycwl.basic.service.task.TaskFaceService;
|
import com.ycwl.basic.service.task.TaskFaceService;
|
||||||
import com.ycwl.basic.service.task.TaskService;
|
import com.ycwl.basic.service.task.TaskService;
|
||||||
@@ -65,12 +75,10 @@ import com.ycwl.basic.storage.StorageFactory;
|
|||||||
import com.ycwl.basic.storage.adapters.IStorageAdapter;
|
import com.ycwl.basic.storage.adapters.IStorageAdapter;
|
||||||
import com.ycwl.basic.storage.enums.StorageAcl;
|
import com.ycwl.basic.storage.enums.StorageAcl;
|
||||||
import com.ycwl.basic.storage.utils.StorageUtil;
|
import com.ycwl.basic.storage.utils.StorageUtil;
|
||||||
import com.ycwl.basic.task.VideoPieceGetter;
|
|
||||||
import com.ycwl.basic.utils.*;
|
import com.ycwl.basic.utils.*;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.logging.log4j.util.Strings;
|
import org.apache.logging.log4j.util.Strings;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
@@ -78,10 +86,8 @@ import java.io.File;
|
|||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -90,14 +96,9 @@ import java.util.Objects;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
import static com.ycwl.basic.constant.FaceConstant.FACE_CUSTOM_MATCH_COUNT_PFX;
|
|
||||||
import static com.ycwl.basic.constant.FaceConstant.FACE_LOW_THRESHOLD_PFX;
|
|
||||||
import static com.ycwl.basic.constant.FaceConstant.FACE_RECOGNITION_COUNT_PFX;
|
|
||||||
import static com.ycwl.basic.constant.FaceConstant.USER_FACE_DB_NAME;
|
import static com.ycwl.basic.constant.FaceConstant.USER_FACE_DB_NAME;
|
||||||
import static com.ycwl.basic.constant.StorageConstant.USER_FACE;
|
import static com.ycwl.basic.constant.StorageConstant.USER_FACE;
|
||||||
|
|
||||||
@@ -130,8 +131,6 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private ScenicService scenicService;
|
private ScenicService scenicService;
|
||||||
@Autowired
|
@Autowired
|
||||||
private TemplateMapper templateMapper;
|
|
||||||
@Autowired
|
|
||||||
private VideoRepository videoRepository;
|
private VideoRepository videoRepository;
|
||||||
@Autowired
|
@Autowired
|
||||||
private VideoTaskRepository videoTaskRepository;
|
private VideoTaskRepository videoTaskRepository;
|
||||||
@@ -142,8 +141,6 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private OrderMapper orderMapper;
|
private OrderMapper orderMapper;
|
||||||
@Autowired
|
@Autowired
|
||||||
private RedisTemplate<String, String> redisTemplate;
|
|
||||||
@Autowired
|
|
||||||
private FaceSampleMapper faceSampleMapper;
|
private FaceSampleMapper faceSampleMapper;
|
||||||
@Autowired
|
@Autowired
|
||||||
private GoodsService goodsService;
|
private GoodsService goodsService;
|
||||||
@@ -156,6 +153,30 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private PrinterService printerService;
|
private PrinterService printerService;
|
||||||
|
|
||||||
|
// 第一阶段的辅助类
|
||||||
|
@Autowired
|
||||||
|
private FaceMetricsRecorder metricsRecorder;
|
||||||
|
@Autowired
|
||||||
|
private SearchResultMerger resultMerger;
|
||||||
|
@Autowired
|
||||||
|
private RematchStrategyFactory rematchStrategyFactory;
|
||||||
|
@Autowired
|
||||||
|
private ScenicConfigFacade scenicConfigFacade;
|
||||||
|
|
||||||
|
// 编排器
|
||||||
|
@Autowired
|
||||||
|
private FaceMatchingOrchestrator faceMatchingOrchestrator;
|
||||||
|
|
||||||
|
// 第二阶段的处理器
|
||||||
|
@Autowired
|
||||||
|
private SourceRelationProcessor sourceRelationProcessor;
|
||||||
|
@Autowired
|
||||||
|
private BuyStatusProcessor buyStatusProcessor;
|
||||||
|
@Autowired
|
||||||
|
private VideoRecreationHandler videoRecreationHandler;
|
||||||
|
@Autowired
|
||||||
|
private FaceRecoveryStrategy faceRecoveryStrategy;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ApiResponse<PageInfo<FaceRespVO>> pageQuery(FaceReqQuery faceReqQuery) {
|
public ApiResponse<PageInfo<FaceRespVO>> pageQuery(FaceReqQuery faceReqQuery) {
|
||||||
PageHelper.startPage(faceReqQuery.getPageNum(),faceReqQuery.getPageSize());
|
PageHelper.startPage(faceReqQuery.getPageNum(),faceReqQuery.getPageSize());
|
||||||
@@ -268,11 +289,11 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
resp.setUrl(faceUrl);
|
resp.setUrl(faceUrl);
|
||||||
resp.setFaceId(newFaceId);
|
resp.setFaceId(newFaceId);
|
||||||
matchFaceId(newFaceId, oldFaceId == null);
|
matchFaceId(newFaceId, oldFaceId == null);
|
||||||
|
|
||||||
// 异步执行自动添加打印
|
// 异步执行自动添加打印
|
||||||
Long finalFaceId = newFaceId;
|
Long finalFaceId = newFaceId;
|
||||||
new Thread(() -> autoAddPhotosToPreferPrint(finalFaceId), "auto-add-print-" + newFaceId).start();
|
new Thread(() -> autoAddPhotosToPreferPrint(finalFaceId), "auto-add-print-" + newFaceId).start();
|
||||||
|
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,427 +305,45 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SearchFaceRespVo matchFaceId(Long faceId) {
|
public SearchFaceRespVo matchFaceId(Long faceId) {
|
||||||
return matchFaceId(faceId, false);
|
return faceMatchingOrchestrator.orchestrateMatching(faceId, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SearchFaceRespVo matchFaceId(Long faceId, boolean isNew) {
|
public SearchFaceRespVo matchFaceId(Long faceId, boolean isNew) {
|
||||||
if (faceId == null) {
|
return faceMatchingOrchestrator.orchestrateMatching(faceId, isNew);
|
||||||
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天过期时间
|
|
||||||
recordFaceRecognitionCount(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 = 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 = processMemberSources(sampleListIds, face);
|
|
||||||
|
|
||||||
if (!memberSourceEntityList.isEmpty()) {
|
|
||||||
// 5. 业务逻辑处理:免费逻辑、购买状态、任务创建
|
|
||||||
List<Long> freeSourceIds = processFreeSourceLogic(memberSourceEntityList, scenicConfig, isNew);
|
|
||||||
processBuyStatus(memberSourceEntityList, freeSourceIds, face.getMemberId(),
|
|
||||||
face.getScenicId(), faceId);
|
|
||||||
|
|
||||||
// 处理视频重切逻辑
|
|
||||||
handleVideoRecreation(scenicConfig, 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 则不自动创建任务
|
|
||||||
Boolean faceSelectFirst = scenicConfig != null ? scenicConfig.getBoolean("face_select_first") : null;
|
|
||||||
if (!Boolean.TRUE.equals(faceSelectFirst)) {
|
|
||||||
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()) {
|
|
||||||
recordLowThresholdFace(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("人脸匹配处理失败,请稍后重试");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新人脸实体结果信息
|
* 更新人脸实体结果信息
|
||||||
|
* 仅用于 handleCustomFaceMatching 方法
|
||||||
*/
|
*/
|
||||||
private void updateFaceEntityResult(FaceEntity originalFace, SearchFaceRespVo searchResult, Long faceId) {
|
private void updateFaceEntityResult(FaceEntity originalFace, SearchFaceRespVo searchResult, Long faceId) {
|
||||||
FaceEntity faceEntity = new FaceEntity();
|
FaceEntity faceEntity = new FaceEntity();
|
||||||
faceEntity.setId(faceId);
|
faceEntity.setId(faceId);
|
||||||
faceEntity.setScore(searchResult.getScore());
|
faceEntity.setScore(searchResult.getScore());
|
||||||
faceEntity.setMatchResult(searchResult.getSearchResultJson());
|
faceEntity.setMatchResult(searchResult.getSearchResultJson());
|
||||||
|
|
||||||
if (searchResult.getFirstMatchRate() != null) {
|
if (searchResult.getFirstMatchRate() != null) {
|
||||||
faceEntity.setFirstMatchRate(BigDecimal.valueOf(searchResult.getFirstMatchRate()));
|
faceEntity.setFirstMatchRate(BigDecimal.valueOf(searchResult.getFirstMatchRate()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (searchResult.getSampleListIds() != null) {
|
if (searchResult.getSampleListIds() != null) {
|
||||||
faceEntity.setMatchSampleIds(searchResult.getSampleListIds().stream()
|
faceEntity.setMatchSampleIds(searchResult.getSampleListIds().stream()
|
||||||
.map(String::valueOf)
|
.map(String::valueOf)
|
||||||
.collect(Collectors.joining(",")));
|
.collect(Collectors.joining(",")));
|
||||||
}
|
}
|
||||||
|
|
||||||
faceEntity.setCreateAt(new Date());
|
faceEntity.setCreateAt(new Date());
|
||||||
faceEntity.setScenicId(originalFace.getScenicId());
|
faceEntity.setScenicId(originalFace.getScenicId());
|
||||||
faceEntity.setMemberId(originalFace.getMemberId());
|
faceEntity.setMemberId(originalFace.getMemberId());
|
||||||
faceEntity.setFaceUrl(originalFace.getFaceUrl());
|
faceEntity.setFaceUrl(originalFace.getFaceUrl());
|
||||||
|
|
||||||
faceMapper.update(faceEntity);
|
faceMapper.update(faceEntity);
|
||||||
faceRepository.clearFaceCache(faceEntity.getId());
|
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);
|
||||||
*/
|
|
||||||
private SearchFaceRespVo executeFaceRecoveryLogic(SearchFaceRespVo originalResult,
|
|
||||||
ScenicConfigManager scenicConfig,
|
|
||||||
IFaceBodyAdapter faceBodyAdapter,
|
|
||||||
Long scenicId) {
|
|
||||||
if (originalResult == null || originalResult.getSampleListIds() == null ||
|
|
||||||
originalResult.getFirstMatchRate() == null || originalResult.getSampleListIds().isEmpty()) {
|
|
||||||
return originalResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scenicConfig == null) {
|
|
||||||
return originalResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否需要执行补救逻辑
|
|
||||||
Integer helperThreshold = scenicConfig.getInteger("face_detect_helper_threshold", 0);
|
|
||||||
if (helperThreshold == null || helperThreshold <= 0) {
|
|
||||||
return originalResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查匹配结果数量是否少于阈值
|
|
||||||
if (originalResult.getSampleListIds().size() >= helperThreshold) {
|
|
||||||
return originalResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("执行人脸识别补救逻辑,原匹配数量: {}, 阈值: {}",
|
|
||||||
originalResult.getSampleListIds().size(), helperThreshold);
|
|
||||||
|
|
||||||
// 获取第一个匹配结果
|
|
||||||
Long firstResultId = originalResult.getSampleListIds().getFirst();
|
|
||||||
FaceSampleEntity faceSample = faceRepository.getFaceSample(firstResultId);
|
|
||||||
|
|
||||||
if (faceSample == null) {
|
|
||||||
log.warn("补救逻辑失败:无法找到人脸样本, sampleId: {}", firstResultId);
|
|
||||||
return originalResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用人脸样本重新进行搜索
|
|
||||||
try {
|
|
||||||
SearchFaceRespVo recoveryResult = faceService.searchFace(faceBodyAdapter,
|
|
||||||
String.valueOf(scenicId),
|
|
||||||
faceSample.getFaceUrl(),
|
|
||||||
"人脸补救措施1");
|
|
||||||
|
|
||||||
if (recoveryResult != null && recoveryResult.getSampleListIds() != null &&
|
|
||||||
!recoveryResult.getSampleListIds().isEmpty()) {
|
|
||||||
log.info("补救逻辑成功,新匹配数量: {}", recoveryResult.getSampleListIds().size());
|
|
||||||
return recoveryResult;
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("补救逻辑执行失败", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return originalResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理源文件关联逻辑
|
|
||||||
* 根据匹配的样本ID创建MemberSourceEntity列表
|
|
||||||
*/
|
|
||||||
private List<MemberSourceEntity> processMemberSources(List<Long> sampleListIds, FaceEntity face) {
|
|
||||||
if (sampleListIds == null || sampleListIds.isEmpty()) {
|
|
||||||
return Collections.emptyList();
|
|
||||||
}
|
|
||||||
|
|
||||||
List<SourceEntity> sourceEntities = sourceMapper.listBySampleIds(sampleListIds);
|
|
||||||
if (sourceEntities.isEmpty()) {
|
|
||||||
return Collections.emptyList();
|
|
||||||
}
|
|
||||||
|
|
||||||
List<SourceEntity> filteredSourceEntities = sourceEntities.stream()
|
|
||||||
.sorted(Comparator.comparing(SourceEntity::getCreateTime).reversed())
|
|
||||||
.collect(Collectors.groupingBy(SourceEntity::getDeviceId))
|
|
||||||
.entrySet()
|
|
||||||
.stream().flatMap(entry -> {
|
|
||||||
DeviceConfigManager configManager = deviceRepository.getDeviceConfigManager(entry.getKey());
|
|
||||||
if (configManager.getInteger("limit_video", 0) > 0) {
|
|
||||||
return Stream.concat(
|
|
||||||
entry.getValue().stream().filter(item -> item.getType() == 2),
|
|
||||||
entry.getValue().stream().filter(item -> item.getType() == 1).limit(Math.min(entry.getValue().size(), configManager.getInteger("limit_video", 0)))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return entry.getValue().stream();
|
|
||||||
}).toList();
|
|
||||||
return filteredSourceEntities.stream().map(sourceEntity -> {
|
|
||||||
DeviceConfigManager deviceConfig = deviceRepository.getDeviceConfigManager(sourceEntity.getDeviceId());
|
|
||||||
MemberSourceEntity memberSourceEntity = new MemberSourceEntity();
|
|
||||||
memberSourceEntity.setScenicId(face.getScenicId());
|
|
||||||
memberSourceEntity.setFaceId(face.getId());
|
|
||||||
memberSourceEntity.setMemberId(face.getMemberId());
|
|
||||||
memberSourceEntity.setSourceId(sourceEntity.getId());
|
|
||||||
memberSourceEntity.setType(sourceEntity.getType());
|
|
||||||
|
|
||||||
// 设置免费状态 - 默认收费
|
|
||||||
memberSourceEntity.setIsFree(0);
|
|
||||||
|
|
||||||
if (deviceConfig != null) {
|
|
||||||
// 视频类型检查
|
|
||||||
if (sourceEntity.getType() == 1) {
|
|
||||||
if (Integer.valueOf(1).equals(deviceConfig.getInteger("video_free"))) {
|
|
||||||
memberSourceEntity.setIsFree(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 图片类型检查
|
|
||||||
else if (sourceEntity.getType() == 2) {
|
|
||||||
if (Integer.valueOf(1).equals(deviceConfig.getInteger("image_free"))) {
|
|
||||||
memberSourceEntity.setIsFree(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return memberSourceEntity;
|
|
||||||
}).collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理免费源文件逻辑
|
|
||||||
* 根据景区配置和是否新用户决定哪些照片可以免费
|
|
||||||
*/
|
|
||||||
private List<Long> processFreeSourceLogic(List<MemberSourceEntity> memberSourceEntityList,
|
|
||||||
ScenicConfigManager scenicConfig,
|
|
||||||
boolean isNew) {
|
|
||||||
List<Long> freeSourceIds = new ArrayList<>();
|
|
||||||
|
|
||||||
if (memberSourceEntityList.isEmpty()) {
|
|
||||||
return freeSourceIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isNew) {
|
|
||||||
// 新用户送照片逻辑
|
|
||||||
List<MemberSourceEntity> photoSource = memberSourceEntityList.stream()
|
|
||||||
.filter(item -> item.getIsFree() == 0) // 只考虑收费的
|
|
||||||
.filter(item -> Integer.valueOf(2).equals(item.getType())) // 只考虑照片类型
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
Integer photoFreeNum = scenicConfig != null ? scenicConfig.getInteger("photo_free_num") : null;
|
|
||||||
if (scenicConfig != null && photoFreeNum != null && photoFreeNum > 0) {
|
|
||||||
|
|
||||||
int freePhotoCount = Math.min(photoFreeNum, photoSource.size());
|
|
||||||
freeSourceIds.addAll(photoSource.stream()
|
|
||||||
.limit(freePhotoCount)
|
|
||||||
.map(MemberSourceEntity::getSourceId)
|
|
||||||
.toList());
|
|
||||||
|
|
||||||
log.debug("新用户免费照片逻辑:配置免费数量 {}, 实际可用 {}, 赠送 {} 张",
|
|
||||||
photoFreeNum, photoSource.size(), freePhotoCount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return freeSourceIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理购买状态逻辑
|
|
||||||
* 设置每个源文件的购买状态和免费状态
|
|
||||||
*/
|
|
||||||
private void processBuyStatus(List<MemberSourceEntity> memberSourceEntityList,
|
|
||||||
List<Long> freeSourceIds,
|
|
||||||
Long memberId,
|
|
||||||
Long scenicId,
|
|
||||||
Long faceId) {
|
|
||||||
if (memberSourceEntityList.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取用户购买状态
|
|
||||||
IsBuyRespVO isBuy = orderBiz.isBuy(memberId, scenicId,
|
|
||||||
memberSourceEntityList.getFirst().getType(),
|
|
||||||
faceId);
|
|
||||||
|
|
||||||
for (MemberSourceEntity memberSourceEntity : memberSourceEntityList) {
|
|
||||||
// 设置购买状态
|
|
||||||
if (isBuy.isBuy()) {
|
|
||||||
// 如果用户买过
|
|
||||||
memberSourceEntity.setIsBuy(1);
|
|
||||||
} else if (isBuy.isFree()) {
|
|
||||||
// 全免费逻辑
|
|
||||||
memberSourceEntity.setIsBuy(1);
|
|
||||||
} else {
|
|
||||||
memberSourceEntity.setIsBuy(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置免费状态
|
|
||||||
if (freeSourceIds.contains(memberSourceEntity.getSourceId())) {
|
|
||||||
memberSourceEntity.setIsFree(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug("购买状态处理完成:用户购买状态 isBuy={}, isFree={}, 免费源文件数量={}",
|
|
||||||
isBuy.isBuy(), isBuy.isFree(), freeSourceIds.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理视频重切逻辑
|
|
||||||
* 当非新用户且照片数量大于视频数量时,创建视频重切任务
|
|
||||||
*/
|
|
||||||
private void handleVideoRecreation(ScenicConfigManager scenicConfig,
|
|
||||||
List<MemberSourceEntity> memberSourceEntityList,
|
|
||||||
Long faceId,
|
|
||||||
Long memberId,
|
|
||||||
List<Long> sampleListIds,
|
|
||||||
boolean isNew) {
|
|
||||||
// 新用户不执行视频重切逻辑
|
|
||||||
if (isNew) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查景区是否禁用源视频功能
|
|
||||||
Boolean disableSourceVideo = scenicConfig != null ? scenicConfig.getBoolean("disable_source_video") : null;
|
|
||||||
if (scenicConfig == null || Boolean.TRUE.equals(disableSourceVideo)) {
|
|
||||||
log.debug("视频重切逻辑跳过:景区禁用了源视频功能");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 统计视频和照片数量
|
|
||||||
long videoCount = memberSourceEntityList.stream()
|
|
||||||
.filter(item -> Integer.valueOf(1).equals(item.getType()))
|
|
||||||
.count();
|
|
||||||
long photoCount = memberSourceEntityList.stream()
|
|
||||||
.filter(item -> Integer.valueOf(2).equals(item.getType()))
|
|
||||||
.count();
|
|
||||||
|
|
||||||
List<FaceSampleEntity> faceSampleList = faceRepository.getFaceSampleList(faceId);
|
|
||||||
if (faceSampleList.isEmpty()) {
|
|
||||||
log.info("faceId:{} sample list not exist", faceId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
List<Long> faceSampleIds = faceSampleList.stream()
|
|
||||||
.sorted(Comparator.comparing(FaceSampleEntity::getCreateAt).reversed())
|
|
||||||
.collect(Collectors.groupingBy(FaceSampleEntity::getDeviceId))
|
|
||||||
.entrySet()
|
|
||||||
.stream().flatMap(entry -> {
|
|
||||||
DeviceConfigManager configManager = deviceRepository.getDeviceConfigManager(entry.getKey());
|
|
||||||
if (configManager.getInteger("limit_video", 0) > 0) {
|
|
||||||
return entry.getValue().subList(0, Math.min(entry.getValue().size(), configManager.getInteger("limit_video", 0))).stream();
|
|
||||||
}
|
|
||||||
return entry.getValue().stream();
|
|
||||||
}).toList()
|
|
||||||
.stream().map(FaceSampleEntity::getId).toList();
|
|
||||||
log.info("视频切分任务: faceId={}, 原始数量={}, 筛选后数量={}", faceId, faceSampleList.size(), faceSampleIds.size());
|
|
||||||
log.debug("视频重切逻辑:视频数量 {}, 照片数量 {}", videoCount, photoCount);
|
|
||||||
|
|
||||||
// 只有照片数量大于视频数量时才创建重切任务
|
|
||||||
if (photoCount > videoCount) {
|
|
||||||
VideoPieceGetter.Task task = new VideoPieceGetter.Task();
|
|
||||||
task.faceId = faceId;
|
|
||||||
task.faceSampleIds = faceSampleIds;
|
|
||||||
task.templateId = null;
|
|
||||||
task.memberId = memberId;
|
|
||||||
task.callback = () -> {
|
|
||||||
log.info("视频重切任务回调: {}", task);
|
|
||||||
};
|
|
||||||
|
|
||||||
VideoPieceGetter.addTask(task);
|
|
||||||
log.debug("视频重切任务已创建:faceId={}, memberId={}, sampleIds={}",
|
|
||||||
faceId, memberId, sampleListIds.size());
|
|
||||||
} else {
|
|
||||||
log.debug("视频重切逻辑跳过:照片数量({})未超过视频数量({})", photoCount, videoCount);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -817,8 +456,8 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
sourceVideoContent.setGroup("直出原片");
|
sourceVideoContent.setGroup("直出原片");
|
||||||
sourceImageContent.setGroup("直出原片");
|
sourceImageContent.setGroup("直出原片");
|
||||||
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(face.getScenicId());
|
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(face.getScenicId());
|
||||||
if (!Boolean.TRUE.equals(scenicConfig.getBoolean("disable_source_image"))) {
|
if (!scenicConfigFacade.isDisableSourceImage(face.getScenicId())) {
|
||||||
IsBuyRespVO isBuyRespVO = orderBiz.isBuy(userId, face.getScenicId(), 2, faceId);
|
IsBuyRespVO isBuyRespVO = orderBiz.isBuy(userId, face.getScenicId(), SourceType.IMAGE.getCode(), faceId);
|
||||||
sourceImageContent.setSourceType(isBuyRespVO.getGoodsType());
|
sourceImageContent.setSourceType(isBuyRespVO.getGoodsType());
|
||||||
sourceImageContent.setContentId(isBuyRespVO.getGoodsId());
|
sourceImageContent.setContentId(isBuyRespVO.getGoodsId());
|
||||||
if (isBuyRespVO.isBuy()) {
|
if (isBuyRespVO.isBuy()) {
|
||||||
@@ -836,8 +475,8 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
sourceImageContent.setFreeCount((int) freeCount);
|
sourceImageContent.setFreeCount((int) freeCount);
|
||||||
contentList.add(sourceImageContent);
|
contentList.add(sourceImageContent);
|
||||||
}
|
}
|
||||||
if (!Boolean.TRUE.equals(scenicConfig.getBoolean("disable_source_video"))) {
|
if (!scenicConfigFacade.isDisableSourceVideo(face.getScenicId())) {
|
||||||
IsBuyRespVO isBuyRespVO = orderBiz.isBuy(userId, face.getScenicId(), 1, faceId);
|
IsBuyRespVO isBuyRespVO = orderBiz.isBuy(userId, face.getScenicId(), SourceType.VIDEO.getCode(), faceId);
|
||||||
sourceVideoContent.setSourceType(isBuyRespVO.getGoodsType());
|
sourceVideoContent.setSourceType(isBuyRespVO.getGoodsType());
|
||||||
sourceVideoContent.setContentId(isBuyRespVO.getGoodsId());
|
sourceVideoContent.setContentId(isBuyRespVO.getGoodsId());
|
||||||
if (isBuyRespVO.isBuy()) {
|
if (isBuyRespVO.isBuy()) {
|
||||||
@@ -955,23 +594,13 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
statusResp.setStep1Status(true);
|
statusResp.setStep1Status(true);
|
||||||
|
|
||||||
statusResp.setFaceUrl(face.getFaceUrl());
|
statusResp.setFaceUrl(face.getFaceUrl());
|
||||||
|
|
||||||
// 查询识别次数
|
// 查询识别次数
|
||||||
String countKey = FACE_RECOGNITION_COUNT_PFX + faceId;
|
long recognitionCount = metricsRecorder.getRecognitionCount(faceId);
|
||||||
String countStr = redisTemplate.opsForValue().get(countKey);
|
|
||||||
long recognitionCount = 0L;
|
|
||||||
if (countStr != null) {
|
|
||||||
try {
|
|
||||||
recognitionCount = Long.parseLong(countStr);
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
log.warn("识别次数解析失败,faceId={}, count={}", faceId, countStr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
statusResp.setRecognitionCount(recognitionCount);
|
statusResp.setRecognitionCount(recognitionCount);
|
||||||
|
|
||||||
// 查询是否触发过低阈值检测
|
// 查询是否触发过低阈值检测
|
||||||
String lowThresholdKey = FACE_LOW_THRESHOLD_PFX + faceId;
|
Boolean hasLowThreshold = metricsRecorder.hasLowThreshold(faceId);
|
||||||
Boolean hasLowThreshold = redisTemplate.hasKey(lowThresholdKey);
|
|
||||||
statusResp.setHasLowThreshold(hasLowThreshold);
|
statusResp.setHasLowThreshold(hasLowThreshold);
|
||||||
|
|
||||||
log.debug("查询人脸状态:faceId={}, recognitionCount={}, hasLowThreshold={}",
|
log.debug("查询人脸状态:faceId={}, recognitionCount={}, hasLowThreshold={}",
|
||||||
@@ -1013,37 +642,20 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
}
|
}
|
||||||
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(face.getScenicId());
|
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(face.getScenicId());
|
||||||
|
|
||||||
String recognitionKey = FACE_RECOGNITION_COUNT_PFX + faceId;
|
// 使用FaceMetricsRecorder获取计数信息
|
||||||
String recognitionCountStr = redisTemplate.opsForValue().get(recognitionKey);
|
long recognitionCount = metricsRecorder.getRecognitionCount(faceId);
|
||||||
long recognitionCount = 0L;
|
long customMatchCount = metricsRecorder.getCustomMatchCount(faceId);
|
||||||
if (recognitionCountStr != null) {
|
boolean hasLowThreshold = metricsRecorder.hasLowThreshold(faceId);
|
||||||
try {
|
|
||||||
recognitionCount = Long.parseLong(recognitionCountStr);
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
log.warn("识别次数解析失败,faceId={}, count={}", faceId, recognitionCountStr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String customMatchKey = FACE_CUSTOM_MATCH_COUNT_PFX + faceId;
|
Integer faceSelectMaxCount = scenicConfigFacade.getFaceSelectMaxCount(face.getScenicId());
|
||||||
String customMatchCountStr = redisTemplate.opsForValue().get(customMatchKey);
|
|
||||||
long customMatchCount = 0L;
|
|
||||||
if (customMatchCountStr != null) {
|
|
||||||
try {
|
|
||||||
customMatchCount = Long.parseLong(customMatchCountStr);
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
log.warn("自定义匹配次数解析失败,faceId={}, count={}", faceId, customMatchCountStr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Integer faceSelectMaxCount = scenicConfig.getInteger("face_select_max_count");
|
|
||||||
if (faceSelectMaxCount != null && faceSelectMaxCount > 0 && customMatchCount > faceSelectMaxCount) {
|
if (faceSelectMaxCount != null && faceSelectMaxCount > 0 && customMatchCount > faceSelectMaxCount) {
|
||||||
log.debug("自定义人脸匹配次数超过限制:faceId={}, customMatchCount={}, limit={}",
|
log.debug("自定义人脸匹配次数超过限制:faceId={}, customMatchCount={}, limit={}",
|
||||||
faceId, customMatchCount, faceSelectMaxCount);
|
faceId, customMatchCount, faceSelectMaxCount);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Integer maxTourTime = scenicConfig.getInteger("tour_time");
|
Integer maxTourTime = scenicConfigFacade.getTourTime(face.getScenicId());
|
||||||
Integer minTourTime = scenicConfig.getInteger("tour_min_time");
|
Integer minTourTime = scenicConfigFacade.getTourMinTime(face.getScenicId());
|
||||||
boolean tourMatch = false;
|
boolean tourMatch = false;
|
||||||
if (maxTourTime != null && minTourTime != null) {
|
if (maxTourTime != null && minTourTime != null) {
|
||||||
if ((new Date().getTime()) - face.getCreateAt().getTime() < maxTourTime * 60 * 1000
|
if ((new Date().getTime()) - face.getCreateAt().getTime() < maxTourTime * 60 * 1000
|
||||||
@@ -1080,16 +692,20 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
if (projectMatch) {
|
if (projectMatch) {
|
||||||
ruleMatched++;
|
ruleMatched++;
|
||||||
}
|
}
|
||||||
// 查询是否触发过低阈值检测
|
|
||||||
String lowThresholdKey = FACE_LOW_THRESHOLD_PFX + faceId;
|
// 使用策略模式替换switch语句
|
||||||
boolean hasLowThreshold = redisTemplate.hasKey(lowThresholdKey);
|
Integer mode = scenicConfigFacade.getRematchMode(face.getScenicId());
|
||||||
Integer mode = scenicConfig.getInteger("re_match_mode", 0);
|
RematchContext context = RematchContext.builder()
|
||||||
return switch (mode) {
|
.recognitionCount(recognitionCount)
|
||||||
case 1 -> tourMatch || recognitionCount > 1 || hasLowThreshold;
|
.hasLowThreshold(hasLowThreshold)
|
||||||
case 5 -> hasLowThreshold || (ruleMatched >= 2);
|
.tourMatch(tourMatch)
|
||||||
case 9 -> hasLowThreshold && ruleMatched >= 2;
|
.projectMatch(projectMatch)
|
||||||
default -> false;
|
.ruleMatched(ruleMatched)
|
||||||
};
|
.faceCreateAt(face.getCreateAt())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
RematchModeStrategy strategy = rematchStrategyFactory.getStrategy(mode);
|
||||||
|
return strategy.shouldRematch(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -1106,7 +722,7 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
if (scenicConfig == null) {
|
if (scenicConfig == null) {
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
Float lowThreshold = scenicConfig.getFloat("face_score_low_threshold", 30.0F);
|
Float lowThreshold = scenicConfigFacade.getFaceScoreLowThreshold(face.getScenicId());
|
||||||
List<SearchFaceResultItem> resultItems = JacksonUtil.fromJsonToList(matchResult, SearchFaceResultItem.class);
|
List<SearchFaceResultItem> resultItems = JacksonUtil.fromJsonToList(matchResult, SearchFaceResultItem.class);
|
||||||
if (resultItems == null || resultItems.isEmpty()) {
|
if (resultItems == null || resultItems.isEmpty()) {
|
||||||
return List.of();
|
return List.of();
|
||||||
@@ -1136,7 +752,7 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
log.debug("开始自定义人脸匹配:faceId={}, faceSampleIds={}", faceId, faceSampleIds);
|
log.debug("开始自定义人脸匹配:faceId={}, faceSampleIds={}", faceId, faceSampleIds);
|
||||||
|
|
||||||
// 记录自定义匹配调用次数,便于监控调用频率
|
// 记录自定义匹配调用次数,便于监控调用频率
|
||||||
recordCustomMatchCount(faceId);
|
metricsRecorder.recordCustomMatchCount(faceId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
FaceEntity face = faceRepository.getFace(faceId);
|
FaceEntity face = faceRepository.getFace(faceId);
|
||||||
@@ -1160,7 +776,7 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取face_select_post_mode配置,默认为0(并集)
|
// 获取face_select_post_mode配置,默认为0(并集)
|
||||||
Integer faceSelectPostMode = scenicConfig != null ? scenicConfig.getInteger("face_select_post_mode", 0) : 0;
|
Integer faceSelectPostMode = scenicConfigFacade.getFaceSelectPostMode(face.getScenicId());
|
||||||
log.debug("face_select_post_mode配置值: {}", faceSelectPostMode);
|
log.debug("face_select_post_mode配置值: {}", faceSelectPostMode);
|
||||||
|
|
||||||
SearchFaceRespVo mergedResult;
|
SearchFaceRespVo mergedResult;
|
||||||
@@ -1169,7 +785,7 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
if (Integer.valueOf(2).equals(faceSelectPostMode)) {
|
if (Integer.valueOf(2).equals(faceSelectPostMode)) {
|
||||||
// 模式2:不搜索,直接使用用户选择的faceSampleIds
|
// 模式2:不搜索,直接使用用户选择的faceSampleIds
|
||||||
log.debug("使用模式2:直接使用用户选择的人脸样本,不进行搜索");
|
log.debug("使用模式2:直接使用用户选择的人脸样本,不进行搜索");
|
||||||
mergedResult = createDirectResult(faceSampleIds);
|
mergedResult = resultMerger.createDirectResult(faceSampleIds);
|
||||||
mergedResult.setSearchResultJson(face.getMatchResult()); // 没有检索
|
mergedResult.setSearchResultJson(face.getMatchResult()); // 没有检索
|
||||||
} else {
|
} else {
|
||||||
// 模式0(并集)和模式1(交集):需要进行搜索
|
// 模式0(并集)和模式1(交集):需要进行搜索
|
||||||
@@ -1197,7 +813,7 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2.2 根据模式整合多个搜索结果
|
// 2.2 根据模式整合多个搜索结果
|
||||||
mergedResult = mergeSearchResults(searchResults, faceSelectPostMode);
|
mergedResult = resultMerger.merge(searchResults, faceSelectPostMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 应用后置筛选逻辑
|
// 3. 应用后置筛选逻辑
|
||||||
@@ -1222,14 +838,14 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
memberRelationRepository.clearSCacheByFace(faceId);
|
memberRelationRepository.clearSCacheByFace(faceId);
|
||||||
log.debug("人脸旧关系数据删除完成:faceId={}", faceId);
|
log.debug("人脸旧关系数据删除完成:faceId={}", faceId);
|
||||||
|
|
||||||
List<MemberSourceEntity> memberSourceEntityList = processMemberSources(sampleListIds, face);
|
List<MemberSourceEntity> memberSourceEntityList = sourceRelationProcessor.processMemberSources(sampleListIds, face);
|
||||||
|
|
||||||
if (!memberSourceEntityList.isEmpty()) {
|
if (!memberSourceEntityList.isEmpty()) {
|
||||||
List<Long> freeSourceIds = processFreeSourceLogic(memberSourceEntityList, scenicConfig, false);
|
List<Long> freeSourceIds = sourceRelationProcessor.processFreeSourceLogic(memberSourceEntityList, face.getScenicId(), false);
|
||||||
processBuyStatus(memberSourceEntityList, freeSourceIds, face.getMemberId(),
|
buyStatusProcessor.processBuyStatus(memberSourceEntityList, freeSourceIds, face.getMemberId(),
|
||||||
face.getScenicId(), faceId);
|
face.getScenicId(), faceId);
|
||||||
|
|
||||||
handleVideoRecreation(scenicConfig, memberSourceEntityList, faceId,
|
videoRecreationHandler.handleVideoRecreation(face.getScenicId(), memberSourceEntityList, faceId,
|
||||||
face.getMemberId(), sampleListIds, false);
|
face.getMemberId(), sampleListIds, false);
|
||||||
|
|
||||||
List<MemberSourceEntity> existingFiltered = sourceMapper.filterExistingRelations(memberSourceEntityList);
|
List<MemberSourceEntity> existingFiltered = sourceMapper.filterExistingRelations(memberSourceEntityList);
|
||||||
@@ -1264,13 +880,6 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 合并多个搜索结果(兼容老版本,默认使用并集模式)
|
|
||||||
*/
|
|
||||||
private SearchFaceRespVo mergeSearchResults(List<SearchFaceRespVo> searchResults) {
|
|
||||||
return mergeSearchResults(searchResults, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateRecognition(FaceRecognitionUpdateReq req) {
|
public void updateRecognition(FaceRecognitionUpdateReq req) {
|
||||||
if (req == null || req.getFaceId() == null) {
|
if (req == null || req.getFaceId() == null) {
|
||||||
@@ -1334,7 +943,7 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
detail.setFaceUrl(face.getFaceUrl());
|
detail.setFaceUrl(face.getFaceUrl());
|
||||||
detail.setScore(face.getScore());
|
detail.setScore(face.getScore());
|
||||||
detail.setFirstMatchRate(face.getFirstMatchRate() != null ? face.getFirstMatchRate().floatValue() : null);
|
detail.setFirstMatchRate(face.getFirstMatchRate() != null ? face.getFirstMatchRate().floatValue() : null);
|
||||||
detail.setLowThreshold(redisTemplate.hasKey(FACE_LOW_THRESHOLD_PFX + faceId));
|
detail.setLowThreshold(metricsRecorder.hasLowThreshold(faceId));
|
||||||
detail.setLastMatchedAt(face.getUpdateAt() != null ? face.getUpdateAt() : face.getCreateAt());
|
detail.setLastMatchedAt(face.getUpdateAt() != null ? face.getUpdateAt() : face.getCreateAt());
|
||||||
|
|
||||||
String matchResultJson = face.getMatchResult();
|
String matchResultJson = face.getMatchResult();
|
||||||
@@ -1534,204 +1143,10 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 合并多个搜索结果
|
|
||||||
*
|
|
||||||
* @param searchResults 搜索结果列表
|
|
||||||
* @param mergeMode 合并模式:0-并集,1-交集
|
|
||||||
* @return 合并后的结果
|
|
||||||
*/
|
|
||||||
private SearchFaceRespVo mergeSearchResults(List<SearchFaceRespVo> searchResults, Integer mergeMode) {
|
|
||||||
SearchFaceRespVo mergedResult = new SearchFaceRespVo();
|
|
||||||
|
|
||||||
if (searchResults == null || searchResults.isEmpty()) {
|
|
||||||
return mergedResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> allSearchJsons = new ArrayList<>();
|
|
||||||
float maxScore = 0f;
|
|
||||||
float maxFirstMatchRate = 0f;
|
|
||||||
boolean hasLowThreshold = false;
|
|
||||||
|
|
||||||
// 收集基础信息
|
|
||||||
for (SearchFaceRespVo result : searchResults) {
|
|
||||||
if (result.getSearchResultJson() != null) {
|
|
||||||
allSearchJsons.add(result.getSearchResultJson());
|
|
||||||
}
|
|
||||||
if (result.getScore() > maxScore) {
|
|
||||||
maxScore = result.getScore();
|
|
||||||
}
|
|
||||||
if (result.getFirstMatchRate() > maxFirstMatchRate) {
|
|
||||||
maxFirstMatchRate = result.getFirstMatchRate();
|
|
||||||
}
|
|
||||||
if (result.isLowThreshold()) {
|
|
||||||
hasLowThreshold = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据合并模式处理样本ID
|
|
||||||
List<Long> finalSampleIds;
|
|
||||||
if (Integer.valueOf(1).equals(mergeMode)) {
|
|
||||||
// 模式1:交集 - 只保留所有搜索结果中都出现的样本ID
|
|
||||||
finalSampleIds = computeIntersection(searchResults);
|
|
||||||
log.debug("使用交集模式合并搜索结果,交集样本数: {}", finalSampleIds.size());
|
|
||||||
} else {
|
|
||||||
// 模式0:并集(默认) - 收集所有样本ID并去重
|
|
||||||
Set<Long> allSampleIds = new LinkedHashSet<>();
|
|
||||||
for (SearchFaceRespVo result : searchResults) {
|
|
||||||
if (result.getSampleListIds() != null) {
|
|
||||||
allSampleIds.addAll(result.getSampleListIds());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finalSampleIds = new ArrayList<>(allSampleIds);
|
|
||||||
log.debug("使用并集模式合并搜索结果,并集样本数: {}", finalSampleIds.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
mergedResult.setSampleListIds(finalSampleIds);
|
|
||||||
mergedResult.setSearchResultJson(String.join("|", allSearchJsons));
|
|
||||||
mergedResult.setScore(maxScore);
|
|
||||||
mergedResult.setFirstMatchRate(maxFirstMatchRate);
|
|
||||||
mergedResult.setLowThreshold(hasLowThreshold);
|
|
||||||
|
|
||||||
log.debug("合并搜索结果完成,模式={}, 最终样本数: {}", mergeMode, finalSampleIds.size());
|
|
||||||
|
|
||||||
return mergedResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 计算多个搜索结果的交集
|
|
||||||
* 返回在所有搜索结果中都出现的样本ID
|
|
||||||
*/
|
|
||||||
private List<Long> computeIntersection(List<SearchFaceRespVo> searchResults) {
|
|
||||||
if (searchResults == null || searchResults.isEmpty()) {
|
|
||||||
return new ArrayList<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 过滤掉空结果
|
|
||||||
List<List<Long>> validSampleLists = searchResults.stream()
|
|
||||||
.filter(result -> result.getSampleListIds() != null && !result.getSampleListIds().isEmpty())
|
|
||||||
.map(SearchFaceRespVo::getSampleListIds)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
if (validSampleLists.isEmpty()) {
|
|
||||||
return new ArrayList<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果只有一个有效结果,直接返回
|
|
||||||
if (validSampleLists.size() == 1) {
|
|
||||||
return new ArrayList<>(validSampleLists.getFirst());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算交集:从第一个列表开始,保留在所有其他列表中都出现的ID
|
|
||||||
Set<Long> intersection = new LinkedHashSet<>(validSampleLists.getFirst());
|
|
||||||
|
|
||||||
for (int i = 1; i < validSampleLists.size(); i++) {
|
|
||||||
intersection.retainAll(validSampleLists.get(i));
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ArrayList<>(intersection);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建直接结果(模式2:不搜索,直接使用用户选择的faceSampleIds)
|
|
||||||
*
|
|
||||||
* @param faceSampleIds 用户选择的人脸样本ID列表
|
|
||||||
* @return 搜索结果对象
|
|
||||||
*/
|
|
||||||
private SearchFaceRespVo createDirectResult(List<Long> faceSampleIds) {
|
|
||||||
SearchFaceRespVo result = new SearchFaceRespVo();
|
|
||||||
|
|
||||||
// 直接使用用户选择的faceSampleIds作为结果
|
|
||||||
result.setSampleListIds(new ArrayList<>(faceSampleIds));
|
|
||||||
|
|
||||||
// 设置默认值
|
|
||||||
result.setScore(1.0f);
|
|
||||||
result.setFirstMatchRate(1.0f);
|
|
||||||
result.setLowThreshold(false);
|
|
||||||
result.setSearchResultJson("");
|
|
||||||
|
|
||||||
log.debug("创建直接结果,样本数: {}", faceSampleIds.size());
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 记录自定义人脸匹配次数到Redis
|
|
||||||
*
|
|
||||||
* @param faceId 人脸ID
|
|
||||||
*/
|
|
||||||
private void recordCustomMatchCount(Long faceId) {
|
|
||||||
if (faceId == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
String redisKey = FACE_CUSTOM_MATCH_COUNT_PFX + faceId;
|
|
||||||
|
|
||||||
Long count = redisTemplate.opsForValue().increment(redisKey);
|
|
||||||
redisTemplate.expire(redisKey, 2, TimeUnit.DAYS);
|
|
||||||
|
|
||||||
log.debug("自定义人脸匹配计数更新:faceId={}, count={}", faceId, count);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("记录自定义人脸匹配次数失败:faceId={}", faceId, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 记录人脸识别次数到Redis
|
|
||||||
*
|
|
||||||
* @param faceId 人脸ID
|
|
||||||
*/
|
|
||||||
private void recordFaceRecognitionCount(Long faceId) {
|
|
||||||
if (faceId == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
String redisKey = FACE_RECOGNITION_COUNT_PFX + faceId;
|
|
||||||
|
|
||||||
// 使用Redis原子操作INCR增加计数
|
|
||||||
Long count = redisTemplate.opsForValue().increment(redisKey);
|
|
||||||
|
|
||||||
// 设置2天过期时间(48小时)
|
|
||||||
redisTemplate.expire(redisKey, 2, TimeUnit.DAYS);
|
|
||||||
|
|
||||||
log.debug("人脸识别计数更新:faceId={}, count={}", faceId, count);
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
// 计数失败不应影响主要业务逻辑,只记录错误日志
|
|
||||||
log.error("记录人脸识别次数失败:faceId={}", faceId, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 记录低阈值检测的人脸ID到Redis
|
|
||||||
*
|
|
||||||
* @param faceId 人脸ID
|
|
||||||
*/
|
|
||||||
private void recordLowThresholdFace(Long faceId) {
|
|
||||||
if (faceId == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
String redisKey = FACE_LOW_THRESHOLD_PFX + faceId;
|
|
||||||
|
|
||||||
// 设置标记,表示该人脸ID触发了低阈值检测
|
|
||||||
redisTemplate.opsForValue().set(redisKey, "1", 2, TimeUnit.DAYS);
|
|
||||||
|
|
||||||
log.debug("记录低阈值检测人脸:faceId={}", faceId);
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
// 记录失败不应影响主要业务逻辑,只记录错误日志
|
|
||||||
log.error("记录低阈值检测人脸失败:faceId={}", faceId, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 自动将人脸关联的照片添加到优先打印列表
|
* 自动将人脸关联的照片添加到优先打印列表
|
||||||
* 根据景区和设备配置自动添加type=2的照片到用户打印列表
|
* 根据景区和设备配置自动添加type=2的照片到用户打印列表
|
||||||
*
|
*
|
||||||
* @param faceId 人脸ID
|
* @param faceId 人脸ID
|
||||||
*/
|
*/
|
||||||
private void autoAddPhotosToPreferPrint(Long faceId) {
|
private void autoAddPhotosToPreferPrint(Long faceId) {
|
||||||
@@ -1805,12 +1220,12 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
sourcesToAdd = deviceSources.stream()
|
sourcesToAdd = deviceSources.stream()
|
||||||
.limit(preferPrintCount)
|
.limit(preferPrintCount)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
log.info("设备{}配置优先打印{}张,实际添加{}张",
|
log.info("设备{}配置优先打印{}张,实际添加{}张",
|
||||||
deviceId, preferPrintCount, sourcesToAdd.size());
|
deviceId, preferPrintCount, sourcesToAdd.size());
|
||||||
} else {
|
} else {
|
||||||
// 如果小于等于0,添加该设备的所有照片
|
// 如果小于等于0,添加该设备的所有照片
|
||||||
sourcesToAdd = deviceSources;
|
sourcesToAdd = deviceSources;
|
||||||
log.info("设备{}配置优先打印所有照片,实际添加{}张",
|
log.info("设备{}配置优先打印所有照片,实际添加{}张",
|
||||||
deviceId, sourcesToAdd.size());
|
deviceId, sourcesToAdd.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1820,7 +1235,7 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
printerService.addUserPhoto(memberId, scenicId, source.getUrl());
|
printerService.addUserPhoto(memberId, scenicId, source.getUrl());
|
||||||
totalAdded++;
|
totalAdded++;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("添加照片到打印列表失败: sourceId={}, url={}, error={}",
|
log.warn("添加照片到打印列表失败: sourceId={}, url={}, error={}",
|
||||||
source.getId(), source.getUrl(), e.getMessage());
|
source.getId(), source.getUrl(), e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package com.ycwl.basic.service.pc.processor;
|
||||||
|
|
||||||
|
import com.ycwl.basic.biz.OrderBiz;
|
||||||
|
import com.ycwl.basic.constant.BuyStatus;
|
||||||
|
import com.ycwl.basic.constant.FreeStatus;
|
||||||
|
import com.ycwl.basic.model.mobile.order.IsBuyRespVO;
|
||||||
|
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 购买状态处理器
|
||||||
|
* 负责处理源文件的购买状态和免费状态
|
||||||
|
*
|
||||||
|
* @author longbinbin
|
||||||
|
* @date 2025-01-31
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class BuyStatusProcessor {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private OrderBiz orderBiz;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理购买状态逻辑
|
||||||
|
* 设置每个源文件的购买状态和免费状态
|
||||||
|
*
|
||||||
|
* @param memberSourceEntityList 源文件关联列表
|
||||||
|
* @param freeSourceIds 免费的源文件ID列表
|
||||||
|
* @param memberId 会员ID
|
||||||
|
* @param scenicId 景区ID
|
||||||
|
* @param faceId 人脸ID
|
||||||
|
*/
|
||||||
|
public void processBuyStatus(List<MemberSourceEntity> memberSourceEntityList,
|
||||||
|
List<Long> freeSourceIds,
|
||||||
|
Long memberId,
|
||||||
|
Long scenicId,
|
||||||
|
Long faceId) {
|
||||||
|
if (memberSourceEntityList.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户购买状态
|
||||||
|
IsBuyRespVO isBuy = orderBiz.isBuy(memberId, scenicId,
|
||||||
|
memberSourceEntityList.getFirst().getType(),
|
||||||
|
faceId);
|
||||||
|
|
||||||
|
for (MemberSourceEntity memberSourceEntity : memberSourceEntityList) {
|
||||||
|
// 设置购买状态
|
||||||
|
if (isBuy.isBuy()) {
|
||||||
|
// 如果用户买过
|
||||||
|
memberSourceEntity.setIsBuy(BuyStatus.BOUGHT.getCode());
|
||||||
|
} else if (isBuy.isFree()) {
|
||||||
|
// 全免费逻辑
|
||||||
|
memberSourceEntity.setIsBuy(BuyStatus.BOUGHT.getCode());
|
||||||
|
} else {
|
||||||
|
memberSourceEntity.setIsBuy(BuyStatus.NOT_BOUGHT.getCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置免费状态
|
||||||
|
if (freeSourceIds.contains(memberSourceEntity.getSourceId())) {
|
||||||
|
memberSourceEntity.setIsFree(FreeStatus.FREE.getCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("购买状态处理完成:用户购买状态 isBuy={}, isFree={}, 免费源文件数量={}",
|
||||||
|
isBuy.isBuy(), isBuy.isFree(), freeSourceIds.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package com.ycwl.basic.service.pc.processor;
|
||||||
|
|
||||||
|
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
|
||||||
|
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
|
||||||
|
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
|
||||||
|
import com.ycwl.basic.model.task.resp.SearchFaceRespVo;
|
||||||
|
import com.ycwl.basic.repository.FaceRepository;
|
||||||
|
import com.ycwl.basic.service.pc.helper.ScenicConfigFacade;
|
||||||
|
import com.ycwl.basic.service.task.TaskFaceService;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸识别补救策略
|
||||||
|
* 负责执行人脸识别的补救逻辑
|
||||||
|
*
|
||||||
|
* @author longbinbin
|
||||||
|
* @date 2025-01-31
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class FaceRecoveryStrategy {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private FaceRepository faceRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TaskFaceService faceService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ScenicConfigFacade scenicConfigFacade;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行人脸识别补救逻辑
|
||||||
|
* 当匹配结果数量少于阈值时,使用第一个匹配结果重新进行人脸搜索
|
||||||
|
*
|
||||||
|
* @param originalResult 原始搜索结果
|
||||||
|
* @param scenicConfig 景区配置
|
||||||
|
* @param faceBodyAdapter 人脸识别适配器
|
||||||
|
* @param scenicId 景区ID
|
||||||
|
* @return 补救后的搜索结果(如果不需要补救或补救失败则返回原始结果)
|
||||||
|
*/
|
||||||
|
public SearchFaceRespVo executeFaceRecoveryLogic(SearchFaceRespVo originalResult,
|
||||||
|
ScenicConfigManager scenicConfig,
|
||||||
|
IFaceBodyAdapter faceBodyAdapter,
|
||||||
|
Long scenicId) {
|
||||||
|
if (originalResult == null || originalResult.getSampleListIds() == null ||
|
||||||
|
originalResult.getFirstMatchRate() == null || originalResult.getSampleListIds().isEmpty()) {
|
||||||
|
return originalResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scenicConfig == null) {
|
||||||
|
return originalResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否需要执行补救逻辑
|
||||||
|
Integer helperThreshold = scenicConfigFacade.getFaceDetectHelperThreshold(scenicId);
|
||||||
|
if (helperThreshold == null || helperThreshold <= 0) {
|
||||||
|
return originalResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查匹配结果数量是否少于阈值
|
||||||
|
if (originalResult.getSampleListIds().size() >= helperThreshold) {
|
||||||
|
return originalResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("执行人脸识别补救逻辑,原匹配数量: {}, 阈值: {}",
|
||||||
|
originalResult.getSampleListIds().size(), helperThreshold);
|
||||||
|
|
||||||
|
// 获取第一个匹配结果
|
||||||
|
Long firstResultId = originalResult.getSampleListIds().getFirst();
|
||||||
|
FaceSampleEntity faceSample = faceRepository.getFaceSample(firstResultId);
|
||||||
|
|
||||||
|
if (faceSample == null) {
|
||||||
|
log.warn("补救逻辑失败:无法找到人脸样本, sampleId: {}", firstResultId);
|
||||||
|
return originalResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用人脸样本重新进行搜索
|
||||||
|
try {
|
||||||
|
SearchFaceRespVo recoveryResult = faceService.searchFace(faceBodyAdapter,
|
||||||
|
String.valueOf(scenicId),
|
||||||
|
faceSample.getFaceUrl(),
|
||||||
|
"人脸补救措施1");
|
||||||
|
|
||||||
|
if (recoveryResult != null && recoveryResult.getSampleListIds() != null &&
|
||||||
|
!recoveryResult.getSampleListIds().isEmpty()) {
|
||||||
|
log.info("补救逻辑成功,新匹配数量: {}", recoveryResult.getSampleListIds().size());
|
||||||
|
return recoveryResult;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("补救逻辑执行失败", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
package com.ycwl.basic.service.pc.processor;
|
||||||
|
|
||||||
|
import com.ycwl.basic.constant.FreeStatus;
|
||||||
|
import com.ycwl.basic.constant.SourceType;
|
||||||
|
import com.ycwl.basic.integration.common.manager.DeviceConfigManager;
|
||||||
|
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
|
||||||
|
import com.ycwl.basic.mapper.SourceMapper;
|
||||||
|
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
||||||
|
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
|
||||||
|
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
|
||||||
|
import com.ycwl.basic.repository.DeviceRepository;
|
||||||
|
import com.ycwl.basic.service.pc.helper.ScenicConfigFacade;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 源文件关联处理器
|
||||||
|
* 负责处理人脸匹配后的源文件关联逻辑
|
||||||
|
*
|
||||||
|
* @author longbinbin
|
||||||
|
* @date 2025-01-31
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class SourceRelationProcessor {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SourceMapper sourceMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private DeviceRepository deviceRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ScenicConfigFacade scenicConfigFacade;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理源文件关联逻辑
|
||||||
|
* 根据匹配的样本ID创建MemberSourceEntity列表
|
||||||
|
*
|
||||||
|
* @param sampleListIds 匹配的样本ID列表
|
||||||
|
* @param face 人脸实体
|
||||||
|
* @return MemberSourceEntity列表
|
||||||
|
*/
|
||||||
|
public List<MemberSourceEntity> processMemberSources(List<Long> sampleListIds, FaceEntity face) {
|
||||||
|
if (sampleListIds == null || sampleListIds.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<SourceEntity> sourceEntities = sourceMapper.listBySampleIds(sampleListIds);
|
||||||
|
if (sourceEntities.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按设备分组并应用限制
|
||||||
|
List<SourceEntity> filteredSourceEntities = sourceEntities.stream()
|
||||||
|
.sorted(Comparator.comparing(SourceEntity::getCreateTime).reversed())
|
||||||
|
.collect(Collectors.groupingBy(SourceEntity::getDeviceId))
|
||||||
|
.entrySet()
|
||||||
|
.stream().flatMap(entry -> {
|
||||||
|
DeviceConfigManager configManager = deviceRepository.getDeviceConfigManager(entry.getKey());
|
||||||
|
if (configManager.getInteger("limit_video", 0) > 0) {
|
||||||
|
// 优先保留所有图片,然后限制视频数量
|
||||||
|
return Stream.concat(
|
||||||
|
entry.getValue().stream().filter(item -> SourceType.isImage(item.getType())),
|
||||||
|
entry.getValue().stream().filter(item -> SourceType.isVideo(item.getType()))
|
||||||
|
.limit(Math.min(entry.getValue().size(), configManager.getInteger("limit_video", 0)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return entry.getValue().stream();
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
// 创建MemberSourceEntity列表
|
||||||
|
return filteredSourceEntities.stream().map(sourceEntity -> {
|
||||||
|
DeviceConfigManager deviceConfig = deviceRepository.getDeviceConfigManager(sourceEntity.getDeviceId());
|
||||||
|
MemberSourceEntity memberSourceEntity = new MemberSourceEntity();
|
||||||
|
memberSourceEntity.setScenicId(face.getScenicId());
|
||||||
|
memberSourceEntity.setFaceId(face.getId());
|
||||||
|
memberSourceEntity.setMemberId(face.getMemberId());
|
||||||
|
memberSourceEntity.setSourceId(sourceEntity.getId());
|
||||||
|
memberSourceEntity.setType(sourceEntity.getType());
|
||||||
|
|
||||||
|
// 设置免费状态 - 默认收费
|
||||||
|
memberSourceEntity.setIsFree(FreeStatus.PAID.getCode());
|
||||||
|
|
||||||
|
if (deviceConfig != null) {
|
||||||
|
// 视频类型检查
|
||||||
|
if (SourceType.isVideo(sourceEntity.getType())) {
|
||||||
|
if (Integer.valueOf(1).equals(deviceConfig.getInteger("video_free"))) {
|
||||||
|
memberSourceEntity.setIsFree(FreeStatus.FREE.getCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 图片类型检查
|
||||||
|
else if (SourceType.isImage(sourceEntity.getType())) {
|
||||||
|
if (Integer.valueOf(1).equals(deviceConfig.getInteger("image_free"))) {
|
||||||
|
memberSourceEntity.setIsFree(FreeStatus.FREE.getCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return memberSourceEntity;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理免费源文件逻辑
|
||||||
|
* 根据景区配置和是否新用户决定哪些照片可以免费
|
||||||
|
*
|
||||||
|
* @param memberSourceEntityList 源文件关联列表
|
||||||
|
* @param scenicId 景区ID
|
||||||
|
* @param isNew 是否新用户
|
||||||
|
* @return 免费的源文件ID列表
|
||||||
|
*/
|
||||||
|
public List<Long> processFreeSourceLogic(List<MemberSourceEntity> memberSourceEntityList,
|
||||||
|
Long scenicId,
|
||||||
|
boolean isNew) {
|
||||||
|
List<Long> freeSourceIds = new ArrayList<>();
|
||||||
|
|
||||||
|
if (memberSourceEntityList.isEmpty()) {
|
||||||
|
return freeSourceIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNew) {
|
||||||
|
// 新用户送照片逻辑
|
||||||
|
List<MemberSourceEntity> photoSource = memberSourceEntityList.stream()
|
||||||
|
.filter(item -> FreeStatus.isPaid(item.getIsFree())) // 只考虑收费的
|
||||||
|
.filter(item -> SourceType.isImage(item.getType())) // 只考虑照片类型
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
Integer photoFreeNum = scenicConfigFacade.getPhotoFreeNum(scenicId);
|
||||||
|
if (photoFreeNum != null && photoFreeNum > 0) {
|
||||||
|
|
||||||
|
int freePhotoCount = Math.min(photoFreeNum, photoSource.size());
|
||||||
|
freeSourceIds.addAll(photoSource.stream()
|
||||||
|
.limit(freePhotoCount)
|
||||||
|
.map(MemberSourceEntity::getSourceId)
|
||||||
|
.toList());
|
||||||
|
|
||||||
|
log.debug("新用户免费照片逻辑:配置免费数量 {}, 实际可用 {}, 赠送 {} 张",
|
||||||
|
photoFreeNum, photoSource.size(), freePhotoCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return freeSourceIds;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
package com.ycwl.basic.service.pc.processor;
|
||||||
|
|
||||||
|
import com.ycwl.basic.constant.SourceType;
|
||||||
|
import com.ycwl.basic.integration.common.manager.DeviceConfigManager;
|
||||||
|
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
|
||||||
|
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
|
||||||
|
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
|
||||||
|
import com.ycwl.basic.repository.DeviceRepository;
|
||||||
|
import com.ycwl.basic.repository.FaceRepository;
|
||||||
|
import com.ycwl.basic.service.pc.helper.ScenicConfigFacade;
|
||||||
|
import com.ycwl.basic.task.VideoPieceGetter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视频重切处理器
|
||||||
|
* 负责处理视频重切任务的创建逻辑
|
||||||
|
*
|
||||||
|
* @author longbinbin
|
||||||
|
* @date 2025-01-31
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class VideoRecreationHandler {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private FaceRepository faceRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private DeviceRepository deviceRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ScenicConfigFacade scenicConfigFacade;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理视频重切逻辑
|
||||||
|
* 当非新用户且照片数量大于视频数量时,创建视频重切任务
|
||||||
|
*
|
||||||
|
* @param scenicId 景区ID
|
||||||
|
* @param memberSourceEntityList 源文件关联列表
|
||||||
|
* @param faceId 人脸ID
|
||||||
|
* @param memberId 会员ID
|
||||||
|
* @param sampleListIds 样本ID列表(用于日志)
|
||||||
|
* @param isNew 是否新用户
|
||||||
|
*/
|
||||||
|
public void handleVideoRecreation(Long scenicId,
|
||||||
|
List<MemberSourceEntity> memberSourceEntityList,
|
||||||
|
Long faceId,
|
||||||
|
Long memberId,
|
||||||
|
List<Long> sampleListIds,
|
||||||
|
boolean isNew) {
|
||||||
|
// 新用户不执行视频重切逻辑
|
||||||
|
if (isNew) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查景区是否禁用源视频功能
|
||||||
|
if (scenicConfigFacade.isDisableSourceVideo(scenicId)) {
|
||||||
|
log.debug("视频重切逻辑跳过:景区禁用了源视频功能");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统计视频和照片数量
|
||||||
|
long videoCount = memberSourceEntityList.stream()
|
||||||
|
.filter(item -> SourceType.isVideo(item.getType()))
|
||||||
|
.count();
|
||||||
|
long photoCount = memberSourceEntityList.stream()
|
||||||
|
.filter(item -> SourceType.isImage(item.getType()))
|
||||||
|
.count();
|
||||||
|
|
||||||
|
List<FaceSampleEntity> faceSampleList = faceRepository.getFaceSampleList(faceId);
|
||||||
|
if (faceSampleList.isEmpty()) {
|
||||||
|
log.info("faceId:{} sample list not exist", faceId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 筛选样本ID
|
||||||
|
List<Long> faceSampleIds = faceSampleList.stream()
|
||||||
|
.sorted(Comparator.comparing(FaceSampleEntity::getCreateAt).reversed())
|
||||||
|
.collect(Collectors.groupingBy(FaceSampleEntity::getDeviceId))
|
||||||
|
.entrySet()
|
||||||
|
.stream().flatMap(entry -> {
|
||||||
|
DeviceConfigManager configManager = deviceRepository.getDeviceConfigManager(entry.getKey());
|
||||||
|
if (configManager.getInteger("limit_video", 0) > 0) {
|
||||||
|
return entry.getValue().subList(0, Math.min(entry.getValue().size(), configManager.getInteger("limit_video", 0))).stream();
|
||||||
|
}
|
||||||
|
return entry.getValue().stream();
|
||||||
|
}).toList()
|
||||||
|
.stream().map(FaceSampleEntity::getId).toList();
|
||||||
|
|
||||||
|
log.info("视频切分任务: faceId={}, 原始数量={}, 筛选后数量={}", faceId, faceSampleList.size(), faceSampleIds.size());
|
||||||
|
log.debug("视频重切逻辑:视频数量 {}, 照片数量 {}", videoCount, photoCount);
|
||||||
|
|
||||||
|
// 只有照片数量大于视频数量时才创建重切任务
|
||||||
|
if (photoCount > videoCount) {
|
||||||
|
VideoPieceGetter.Task task = new VideoPieceGetter.Task();
|
||||||
|
task.faceId = faceId;
|
||||||
|
task.faceSampleIds = faceSampleIds;
|
||||||
|
task.templateId = null;
|
||||||
|
task.memberId = memberId;
|
||||||
|
task.callback = () -> {
|
||||||
|
log.info("视频重切任务回调: {}", task);
|
||||||
|
};
|
||||||
|
|
||||||
|
VideoPieceGetter.addTask(task);
|
||||||
|
log.debug("视频重切任务已创建:faceId={}, memberId={}, sampleIds={}",
|
||||||
|
faceId, memberId, sampleListIds.size());
|
||||||
|
} else {
|
||||||
|
log.debug("视频重切逻辑跳过:照片数量({})未超过视频数量({})", photoCount, videoCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package com.ycwl.basic.service.pc.strategy;
|
||||||
|
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重匹配上下文
|
||||||
|
* 包含判断是否需要重匹配的所有必要信息
|
||||||
|
*
|
||||||
|
* @author longbinbin
|
||||||
|
* @date 2025-01-31
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
public class RematchContext {
|
||||||
|
/**
|
||||||
|
* 人脸识别次数
|
||||||
|
*/
|
||||||
|
private long recognitionCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否触发低阈值检测
|
||||||
|
*/
|
||||||
|
private boolean hasLowThreshold;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否符合游览时间匹配
|
||||||
|
*/
|
||||||
|
private boolean tourMatch;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否符合项目时间匹配
|
||||||
|
*/
|
||||||
|
private boolean projectMatch;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规则匹配数量
|
||||||
|
*/
|
||||||
|
private int ruleMatched;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸创建时间
|
||||||
|
*/
|
||||||
|
private Date faceCreateAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.ycwl.basic.service.pc.strategy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重匹配模式策略接口
|
||||||
|
* 用于判断是否需要进行人脸重新匹配
|
||||||
|
*
|
||||||
|
* @author longbinbin
|
||||||
|
* @date 2025-01-31
|
||||||
|
*/
|
||||||
|
public interface RematchModeStrategy {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否应该重新匹配
|
||||||
|
*
|
||||||
|
* @param context 重匹配上下文
|
||||||
|
* @return true-需要重匹配, false-不需要重匹配
|
||||||
|
*/
|
||||||
|
boolean shouldRematch(RematchContext context);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取策略对应的模式值
|
||||||
|
*
|
||||||
|
* @return 模式值
|
||||||
|
*/
|
||||||
|
int getMode();
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package com.ycwl.basic.service.pc.strategy;
|
||||||
|
|
||||||
|
import com.ycwl.basic.service.pc.strategy.impl.DefaultRematchStrategy;
|
||||||
|
import com.ycwl.basic.service.pc.strategy.impl.RematchMode1Strategy;
|
||||||
|
import com.ycwl.basic.service.pc.strategy.impl.RematchMode5Strategy;
|
||||||
|
import com.ycwl.basic.service.pc.strategy.impl.RematchMode9Strategy;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重匹配策略工厂
|
||||||
|
* 根据模式值获取对应的策略实例
|
||||||
|
*
|
||||||
|
* @author longbinbin
|
||||||
|
* @date 2025-01-31
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class RematchStrategyFactory {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private List<RematchModeStrategy> strategies;
|
||||||
|
|
||||||
|
private final Map<Integer, RematchModeStrategy> strategyMap = new HashMap<>();
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void init() {
|
||||||
|
for (RematchModeStrategy strategy : strategies) {
|
||||||
|
strategyMap.put(strategy.getMode(), strategy);
|
||||||
|
log.debug("注册重匹配策略: mode={}, class={}",
|
||||||
|
strategy.getMode(), strategy.getClass().getSimpleName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据模式值获取对应的策略
|
||||||
|
*
|
||||||
|
* @param mode 模式值(0-默认, 1-模式1, 5-模式5, 9-模式9)
|
||||||
|
* @return 对应的策略实例,如果没有找到则返回默认策略
|
||||||
|
*/
|
||||||
|
public RematchModeStrategy getStrategy(Integer mode) {
|
||||||
|
if (mode == null) {
|
||||||
|
return strategyMap.getOrDefault(0, new DefaultRematchStrategy());
|
||||||
|
}
|
||||||
|
|
||||||
|
RematchModeStrategy strategy = strategyMap.get(mode);
|
||||||
|
if (strategy == null) {
|
||||||
|
log.warn("未找到重匹配模式{}对应的策略,使用默认策略", mode);
|
||||||
|
return strategyMap.getOrDefault(0, new DefaultRematchStrategy());
|
||||||
|
}
|
||||||
|
|
||||||
|
return strategy;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.ycwl.basic.service.pc.strategy.impl;
|
||||||
|
|
||||||
|
import com.ycwl.basic.service.pc.strategy.RematchContext;
|
||||||
|
import com.ycwl.basic.service.pc.strategy.RematchModeStrategy;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认重匹配策略(模式0或其他未定义模式)
|
||||||
|
* 条件: 不触发重匹配
|
||||||
|
*
|
||||||
|
* @author longbinbin
|
||||||
|
* @date 2025-01-31
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class DefaultRematchStrategy implements RematchModeStrategy {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldRematch(RematchContext context) {
|
||||||
|
log.debug("DefaultRematchStrategy判断: 默认不重匹配");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getMode() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.ycwl.basic.service.pc.strategy.impl;
|
||||||
|
|
||||||
|
import com.ycwl.basic.service.pc.strategy.RematchContext;
|
||||||
|
import com.ycwl.basic.service.pc.strategy.RematchModeStrategy;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重匹配模式1策略
|
||||||
|
* 条件: tourMatch || recognitionCount > 1 || hasLowThreshold
|
||||||
|
* 满足任一条件即可重匹配
|
||||||
|
*
|
||||||
|
* @author longbinbin
|
||||||
|
* @date 2025-01-31
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class RematchMode1Strategy implements RematchModeStrategy {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldRematch(RematchContext context) {
|
||||||
|
boolean result = context.isTourMatch()
|
||||||
|
|| context.getRecognitionCount() > 1
|
||||||
|
|| context.isHasLowThreshold();
|
||||||
|
|
||||||
|
log.debug("RematchMode1Strategy判断: tourMatch={}, recognitionCount={}, hasLowThreshold={}, result={}",
|
||||||
|
context.isTourMatch(), context.getRecognitionCount(),
|
||||||
|
context.isHasLowThreshold(), result);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getMode() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.ycwl.basic.service.pc.strategy.impl;
|
||||||
|
|
||||||
|
import com.ycwl.basic.service.pc.strategy.RematchContext;
|
||||||
|
import com.ycwl.basic.service.pc.strategy.RematchModeStrategy;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重匹配模式5策略
|
||||||
|
* 条件: hasLowThreshold || (ruleMatched >= 2)
|
||||||
|
* 触发低阈值或匹配2个及以上规则即可重匹配
|
||||||
|
*
|
||||||
|
* @author longbinbin
|
||||||
|
* @date 2025-01-31
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class RematchMode5Strategy implements RematchModeStrategy {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldRematch(RematchContext context) {
|
||||||
|
boolean result = context.isHasLowThreshold()
|
||||||
|
|| context.getRuleMatched() >= 2;
|
||||||
|
|
||||||
|
log.debug("RematchMode5Strategy判断: hasLowThreshold={}, ruleMatched={}, result={}",
|
||||||
|
context.isHasLowThreshold(), context.getRuleMatched(), result);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getMode() {
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.ycwl.basic.service.pc.strategy.impl;
|
||||||
|
|
||||||
|
import com.ycwl.basic.service.pc.strategy.RematchContext;
|
||||||
|
import com.ycwl.basic.service.pc.strategy.RematchModeStrategy;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重匹配模式9策略
|
||||||
|
* 条件: hasLowThreshold && (ruleMatched >= 2)
|
||||||
|
* 必须同时触发低阈值且匹配2个及以上规则才可重匹配
|
||||||
|
*
|
||||||
|
* @author longbinbin
|
||||||
|
* @date 2025-01-31
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class RematchMode9Strategy implements RematchModeStrategy {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldRematch(RematchContext context) {
|
||||||
|
boolean result = context.isHasLowThreshold()
|
||||||
|
&& context.getRuleMatched() >= 2;
|
||||||
|
|
||||||
|
log.debug("RematchMode9Strategy判断: hasLowThreshold={}, ruleMatched={}, result={}",
|
||||||
|
context.isHasLowThreshold(), context.getRuleMatched(), result);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getMode() {
|
||||||
|
return 9;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user