diff --git a/src/main/java/com/ycwl/basic/service/pc/impl/FaceServiceImpl.java b/src/main/java/com/ycwl/basic/service/pc/impl/FaceServiceImpl.java index eca47006..d230fb90 100644 --- a/src/main/java/com/ycwl/basic/service/pc/impl/FaceServiceImpl.java +++ b/src/main/java/com/ycwl/basic/service/pc/impl/FaceServiceImpl.java @@ -60,6 +60,10 @@ import com.ycwl.basic.service.pc.FaceService; import com.ycwl.basic.service.pc.ScenicService; import com.ycwl.basic.service.pc.helper.FaceMetricsRecorder; import com.ycwl.basic.service.pc.helper.SearchResultMerger; +import com.ycwl.basic.service.pc.processor.BuyStatusProcessor; +import com.ycwl.basic.service.pc.processor.FaceRecoveryStrategy; +import com.ycwl.basic.service.pc.processor.SourceRelationProcessor; +import com.ycwl.basic.service.pc.processor.VideoRecreationHandler; import com.ycwl.basic.service.pc.strategy.RematchContext; import com.ycwl.basic.service.pc.strategy.RematchModeStrategy; import com.ycwl.basic.service.pc.strategy.RematchStrategyFactory; @@ -158,7 +162,7 @@ public class FaceServiceImpl implements FaceService { @Autowired private TemplateRepository templateRepository; - // 新增的辅助类 + // 第一阶段的辅助类 @Autowired private FaceMetricsRecorder metricsRecorder; @Autowired @@ -166,6 +170,16 @@ public class FaceServiceImpl implements FaceService { @Autowired private RematchStrategyFactory rematchStrategyFactory; + // 第二阶段的处理器 + @Autowired + private SourceRelationProcessor sourceRelationProcessor; + @Autowired + private BuyStatusProcessor buyStatusProcessor; + @Autowired + private VideoRecreationHandler videoRecreationHandler; + @Autowired + private FaceRecoveryStrategy faceRecoveryStrategy; + @Override public ApiResponse> pageQuery(FaceReqQuery faceReqQuery) { PageHelper.startPage(faceReqQuery.getPageNum(),faceReqQuery.getPageSize()); @@ -341,7 +355,7 @@ public class FaceServiceImpl implements FaceService { } // 执行补救逻辑(如需要) - scenicDbSearchResult = executeFaceRecoveryLogic(scenicDbSearchResult, scenicConfig, + scenicDbSearchResult = faceRecoveryStrategy.executeFaceRecoveryLogic(scenicDbSearchResult, scenicConfig, faceBodyAdapter, face.getScenicId()); // 3. 结果处理:更新人脸实体信息 @@ -356,16 +370,16 @@ public class FaceServiceImpl implements FaceService { if (sampleListIds != null && !sampleListIds.isEmpty()) { try { // 4. 源文件关联:处理匹配到的源文件 - List memberSourceEntityList = processMemberSources(sampleListIds, face); - + List memberSourceEntityList = sourceRelationProcessor.processMemberSources(sampleListIds, face); + if (!memberSourceEntityList.isEmpty()) { // 5. 业务逻辑处理:免费逻辑、购买状态、任务创建 - List freeSourceIds = processFreeSourceLogic(memberSourceEntityList, scenicConfig, isNew); - processBuyStatus(memberSourceEntityList, freeSourceIds, face.getMemberId(), + List freeSourceIds = sourceRelationProcessor.processFreeSourceLogic(memberSourceEntityList, scenicConfig, isNew); + buyStatusProcessor.processBuyStatus(memberSourceEntityList, freeSourceIds, face.getMemberId(), face.getScenicId(), faceId); - + // 处理视频重切逻辑 - handleVideoRecreation(scenicConfig, memberSourceEntityList, faceId, + videoRecreationHandler.handleVideoRecreation(scenicConfig, memberSourceEntityList, faceId, face.getMemberId(), sampleListIds, isNew); // 过滤已存在的关联关系和无效的source引用,防止数据不一致 @@ -448,269 +462,6 @@ public class FaceServiceImpl implements FaceService { searchResult.getSampleListIds() != null ? searchResult.getSampleListIds().size() : 0); } - /** - * 执行人脸识别补救逻辑 - * 当匹配结果数量少于阈值时,使用第一个匹配结果重新进行人脸搜索 - */ - private SearchFaceRespVo executeFaceRecoveryLogic(SearchFaceRespVo originalResult, - ScenicConfigManager scenicConfig, - IFaceBodyAdapter faceBodyAdapter, - Long scenicId) { - if (originalResult == null || originalResult.getSampleListIds() == null || - originalResult.getFirstMatchRate() == null || originalResult.getSampleListIds().isEmpty()) { - return originalResult; - } - - if (scenicConfig == null) { - return originalResult; - } - - // 检查是否需要执行补救逻辑 - Integer helperThreshold = scenicConfig.getInteger("face_detect_helper_threshold", 0); - if (helperThreshold == null || helperThreshold <= 0) { - return originalResult; - } - - // 检查匹配结果数量是否少于阈值 - if (originalResult.getSampleListIds().size() >= helperThreshold) { - return originalResult; - } - - log.info("执行人脸识别补救逻辑,原匹配数量: {}, 阈值: {}", - originalResult.getSampleListIds().size(), helperThreshold); - - // 获取第一个匹配结果 - Long firstResultId = originalResult.getSampleListIds().getFirst(); - FaceSampleEntity faceSample = faceRepository.getFaceSample(firstResultId); - - if (faceSample == null) { - log.warn("补救逻辑失败:无法找到人脸样本, sampleId: {}", firstResultId); - return originalResult; - } - - // 使用人脸样本重新进行搜索 - try { - SearchFaceRespVo recoveryResult = faceService.searchFace(faceBodyAdapter, - String.valueOf(scenicId), - faceSample.getFaceUrl(), - "人脸补救措施1"); - - if (recoveryResult != null && recoveryResult.getSampleListIds() != null && - !recoveryResult.getSampleListIds().isEmpty()) { - log.info("补救逻辑成功,新匹配数量: {}", recoveryResult.getSampleListIds().size()); - return recoveryResult; - } - } catch (Exception e) { - log.warn("补救逻辑执行失败", e); - } - - return originalResult; - } - - /** - * 处理源文件关联逻辑 - * 根据匹配的样本ID创建MemberSourceEntity列表 - */ - private List processMemberSources(List sampleListIds, FaceEntity face) { - if (sampleListIds == null || sampleListIds.isEmpty()) { - return Collections.emptyList(); - } - - List sourceEntities = sourceMapper.listBySampleIds(sampleListIds); - if (sourceEntities.isEmpty()) { - return Collections.emptyList(); - } - - List filteredSourceEntities = sourceEntities.stream() - .sorted(Comparator.comparing(SourceEntity::getCreateTime).reversed()) - .collect(Collectors.groupingBy(SourceEntity::getDeviceId)) - .entrySet() - .stream().flatMap(entry -> { - DeviceConfigManager configManager = deviceRepository.getDeviceConfigManager(entry.getKey()); - if (configManager.getInteger("limit_video", 0) > 0) { - return Stream.concat( - entry.getValue().stream().filter(item -> item.getType() == 2), - entry.getValue().stream().filter(item -> item.getType() == 1).limit(Math.min(entry.getValue().size(), configManager.getInteger("limit_video", 0))) - ); - } - return entry.getValue().stream(); - }).toList(); - return filteredSourceEntities.stream().map(sourceEntity -> { - DeviceConfigManager deviceConfig = deviceRepository.getDeviceConfigManager(sourceEntity.getDeviceId()); - MemberSourceEntity memberSourceEntity = new MemberSourceEntity(); - memberSourceEntity.setScenicId(face.getScenicId()); - memberSourceEntity.setFaceId(face.getId()); - memberSourceEntity.setMemberId(face.getMemberId()); - memberSourceEntity.setSourceId(sourceEntity.getId()); - memberSourceEntity.setType(sourceEntity.getType()); - - // 设置免费状态 - 默认收费 - memberSourceEntity.setIsFree(0); - - if (deviceConfig != null) { - // 视频类型检查 - if (sourceEntity.getType() == 1) { - if (Integer.valueOf(1).equals(deviceConfig.getInteger("video_free"))) { - memberSourceEntity.setIsFree(1); - } - } - // 图片类型检查 - else if (sourceEntity.getType() == 2) { - if (Integer.valueOf(1).equals(deviceConfig.getInteger("image_free"))) { - memberSourceEntity.setIsFree(1); - } - } - } - - return memberSourceEntity; - }).collect(Collectors.toList()); - } - - /** - * 处理免费源文件逻辑 - * 根据景区配置和是否新用户决定哪些照片可以免费 - */ - private List processFreeSourceLogic(List memberSourceEntityList, - ScenicConfigManager scenicConfig, - boolean isNew) { - List freeSourceIds = new ArrayList<>(); - - if (memberSourceEntityList.isEmpty()) { - return freeSourceIds; - } - - if (isNew) { - // 新用户送照片逻辑 - List photoSource = memberSourceEntityList.stream() - .filter(item -> item.getIsFree() == 0) // 只考虑收费的 - .filter(item -> Integer.valueOf(2).equals(item.getType())) // 只考虑照片类型 - .toList(); - - Integer photoFreeNum = scenicConfig != null ? scenicConfig.getInteger("photo_free_num") : null; - if (scenicConfig != null && photoFreeNum != null && photoFreeNum > 0) { - - int freePhotoCount = Math.min(photoFreeNum, photoSource.size()); - freeSourceIds.addAll(photoSource.stream() - .limit(freePhotoCount) - .map(MemberSourceEntity::getSourceId) - .toList()); - - log.debug("新用户免费照片逻辑:配置免费数量 {}, 实际可用 {}, 赠送 {} 张", - photoFreeNum, photoSource.size(), freePhotoCount); - } - } - - return freeSourceIds; - } - - /** - * 处理购买状态逻辑 - * 设置每个源文件的购买状态和免费状态 - */ - private void processBuyStatus(List memberSourceEntityList, - List freeSourceIds, - Long memberId, - Long scenicId, - Long faceId) { - if (memberSourceEntityList.isEmpty()) { - return; - } - - // 获取用户购买状态 - IsBuyRespVO isBuy = orderBiz.isBuy(memberId, scenicId, - memberSourceEntityList.getFirst().getType(), - faceId); - - for (MemberSourceEntity memberSourceEntity : memberSourceEntityList) { - // 设置购买状态 - if (isBuy.isBuy()) { - // 如果用户买过 - memberSourceEntity.setIsBuy(1); - } else if (isBuy.isFree()) { - // 全免费逻辑 - memberSourceEntity.setIsBuy(1); - } else { - memberSourceEntity.setIsBuy(0); - } - - // 设置免费状态 - if (freeSourceIds.contains(memberSourceEntity.getSourceId())) { - memberSourceEntity.setIsFree(1); - } - } - - log.debug("购买状态处理完成:用户购买状态 isBuy={}, isFree={}, 免费源文件数量={}", - isBuy.isBuy(), isBuy.isFree(), freeSourceIds.size()); - } - - /** - * 处理视频重切逻辑 - * 当非新用户且照片数量大于视频数量时,创建视频重切任务 - */ - private void handleVideoRecreation(ScenicConfigManager scenicConfig, - List memberSourceEntityList, - Long faceId, - Long memberId, - List sampleListIds, - boolean isNew) { - // 新用户不执行视频重切逻辑 - if (isNew) { - return; - } - - // 检查景区是否禁用源视频功能 - Boolean disableSourceVideo = scenicConfig != null ? scenicConfig.getBoolean("disable_source_video") : null; - if (scenicConfig == null || Boolean.TRUE.equals(disableSourceVideo)) { - log.debug("视频重切逻辑跳过:景区禁用了源视频功能"); - return; - } - - // 统计视频和照片数量 - long videoCount = memberSourceEntityList.stream() - .filter(item -> Integer.valueOf(1).equals(item.getType())) - .count(); - long photoCount = memberSourceEntityList.stream() - .filter(item -> Integer.valueOf(2).equals(item.getType())) - .count(); - - List faceSampleList = faceRepository.getFaceSampleList(faceId); - if (faceSampleList.isEmpty()) { - log.info("faceId:{} sample list not exist", faceId); - return; - } - List faceSampleIds = faceSampleList.stream() - .sorted(Comparator.comparing(FaceSampleEntity::getCreateAt).reversed()) - .collect(Collectors.groupingBy(FaceSampleEntity::getDeviceId)) - .entrySet() - .stream().flatMap(entry -> { - DeviceConfigManager configManager = deviceRepository.getDeviceConfigManager(entry.getKey()); - if (configManager.getInteger("limit_video", 0) > 0) { - return entry.getValue().subList(0, Math.min(entry.getValue().size(), configManager.getInteger("limit_video", 0))).stream(); - } - return entry.getValue().stream(); - }).toList() - .stream().map(FaceSampleEntity::getId).toList(); - log.info("视频切分任务: faceId={}, 原始数量={}, 筛选后数量={}", faceId, faceSampleList.size(), faceSampleIds.size()); - log.debug("视频重切逻辑:视频数量 {}, 照片数量 {}", videoCount, photoCount); - - // 只有照片数量大于视频数量时才创建重切任务 - if (photoCount > videoCount) { - VideoPieceGetter.Task task = new VideoPieceGetter.Task(); - task.faceId = faceId; - task.faceSampleIds = faceSampleIds; - task.templateId = null; - task.memberId = memberId; - task.callback = () -> { - log.info("视频重切任务回调: {}", task); - }; - - VideoPieceGetter.addTask(task); - log.debug("视频重切任务已创建:faceId={}, memberId={}, sampleIds={}", - faceId, memberId, sampleListIds.size()); - } else { - log.debug("视频重切逻辑跳过:照片数量({})未超过视频数量({})", photoCount, videoCount); - } - } @Override public ApiResponse deleteFace(Long faceId) { @@ -1204,14 +955,14 @@ public class FaceServiceImpl implements FaceService { memberRelationRepository.clearSCacheByFace(faceId); log.debug("人脸旧关系数据删除完成:faceId={}", faceId); - List memberSourceEntityList = processMemberSources(sampleListIds, face); + List memberSourceEntityList = sourceRelationProcessor.processMemberSources(sampleListIds, face); if (!memberSourceEntityList.isEmpty()) { - List freeSourceIds = processFreeSourceLogic(memberSourceEntityList, scenicConfig, false); - processBuyStatus(memberSourceEntityList, freeSourceIds, face.getMemberId(), + List freeSourceIds = sourceRelationProcessor.processFreeSourceLogic(memberSourceEntityList, scenicConfig, false); + buyStatusProcessor.processBuyStatus(memberSourceEntityList, freeSourceIds, face.getMemberId(), face.getScenicId(), faceId); - handleVideoRecreation(scenicConfig, memberSourceEntityList, faceId, + videoRecreationHandler.handleVideoRecreation(scenicConfig, memberSourceEntityList, faceId, face.getMemberId(), sampleListIds, false); List existingFiltered = sourceMapper.filterExistingRelations(memberSourceEntityList); diff --git a/src/main/java/com/ycwl/basic/service/pc/processor/BuyStatusProcessor.java b/src/main/java/com/ycwl/basic/service/pc/processor/BuyStatusProcessor.java new file mode 100644 index 00000000..39394f5d --- /dev/null +++ b/src/main/java/com/ycwl/basic/service/pc/processor/BuyStatusProcessor.java @@ -0,0 +1,71 @@ +package com.ycwl.basic.service.pc.processor; + +import com.ycwl.basic.biz.OrderBiz; +import com.ycwl.basic.model.mobile.order.IsBuyRespVO; +import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * 购买状态处理器 + * 负责处理源文件的购买状态和免费状态 + * + * @author longbinbin + * @date 2025-01-31 + */ +@Slf4j +@Component +public class BuyStatusProcessor { + + @Autowired + private OrderBiz orderBiz; + + /** + * 处理购买状态逻辑 + * 设置每个源文件的购买状态和免费状态 + * + * @param memberSourceEntityList 源文件关联列表 + * @param freeSourceIds 免费的源文件ID列表 + * @param memberId 会员ID + * @param scenicId 景区ID + * @param faceId 人脸ID + */ + public void processBuyStatus(List memberSourceEntityList, + List freeSourceIds, + Long memberId, + Long scenicId, + Long faceId) { + if (memberSourceEntityList.isEmpty()) { + return; + } + + // 获取用户购买状态 + IsBuyRespVO isBuy = orderBiz.isBuy(memberId, scenicId, + memberSourceEntityList.getFirst().getType(), + faceId); + + for (MemberSourceEntity memberSourceEntity : memberSourceEntityList) { + // 设置购买状态 + if (isBuy.isBuy()) { + // 如果用户买过 + memberSourceEntity.setIsBuy(1); + } else if (isBuy.isFree()) { + // 全免费逻辑 + memberSourceEntity.setIsBuy(1); + } else { + memberSourceEntity.setIsBuy(0); + } + + // 设置免费状态 + if (freeSourceIds.contains(memberSourceEntity.getSourceId())) { + memberSourceEntity.setIsFree(1); + } + } + + log.debug("购买状态处理完成:用户购买状态 isBuy={}, isFree={}, 免费源文件数量={}", + isBuy.isBuy(), isBuy.isFree(), freeSourceIds.size()); + } +} diff --git a/src/main/java/com/ycwl/basic/service/pc/processor/FaceRecoveryStrategy.java b/src/main/java/com/ycwl/basic/service/pc/processor/FaceRecoveryStrategy.java new file mode 100644 index 00000000..5a9c610c --- /dev/null +++ b/src/main/java/com/ycwl/basic/service/pc/processor/FaceRecoveryStrategy.java @@ -0,0 +1,94 @@ +package com.ycwl.basic.service.pc.processor; + +import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter; +import com.ycwl.basic.integration.common.manager.ScenicConfigManager; +import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity; +import com.ycwl.basic.model.task.resp.SearchFaceRespVo; +import com.ycwl.basic.repository.FaceRepository; +import com.ycwl.basic.service.task.TaskFaceService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * 人脸识别补救策略 + * 负责执行人脸识别的补救逻辑 + * + * @author longbinbin + * @date 2025-01-31 + */ +@Slf4j +@Component +public class FaceRecoveryStrategy { + + @Autowired + private FaceRepository faceRepository; + + @Autowired + private TaskFaceService faceService; + + /** + * 执行人脸识别补救逻辑 + * 当匹配结果数量少于阈值时,使用第一个匹配结果重新进行人脸搜索 + * + * @param originalResult 原始搜索结果 + * @param scenicConfig 景区配置 + * @param faceBodyAdapter 人脸识别适配器 + * @param scenicId 景区ID + * @return 补救后的搜索结果(如果不需要补救或补救失败则返回原始结果) + */ + public SearchFaceRespVo executeFaceRecoveryLogic(SearchFaceRespVo originalResult, + ScenicConfigManager scenicConfig, + IFaceBodyAdapter faceBodyAdapter, + Long scenicId) { + if (originalResult == null || originalResult.getSampleListIds() == null || + originalResult.getFirstMatchRate() == null || originalResult.getSampleListIds().isEmpty()) { + return originalResult; + } + + if (scenicConfig == null) { + return originalResult; + } + + // 检查是否需要执行补救逻辑 + Integer helperThreshold = scenicConfig.getInteger("face_detect_helper_threshold", 0); + if (helperThreshold == null || helperThreshold <= 0) { + return originalResult; + } + + // 检查匹配结果数量是否少于阈值 + if (originalResult.getSampleListIds().size() >= helperThreshold) { + return originalResult; + } + + log.info("执行人脸识别补救逻辑,原匹配数量: {}, 阈值: {}", + originalResult.getSampleListIds().size(), helperThreshold); + + // 获取第一个匹配结果 + Long firstResultId = originalResult.getSampleListIds().getFirst(); + FaceSampleEntity faceSample = faceRepository.getFaceSample(firstResultId); + + if (faceSample == null) { + log.warn("补救逻辑失败:无法找到人脸样本, sampleId: {}", firstResultId); + return originalResult; + } + + // 使用人脸样本重新进行搜索 + try { + SearchFaceRespVo recoveryResult = faceService.searchFace(faceBodyAdapter, + String.valueOf(scenicId), + faceSample.getFaceUrl(), + "人脸补救措施1"); + + if (recoveryResult != null && recoveryResult.getSampleListIds() != null && + !recoveryResult.getSampleListIds().isEmpty()) { + log.info("补救逻辑成功,新匹配数量: {}", recoveryResult.getSampleListIds().size()); + return recoveryResult; + } + } catch (Exception e) { + log.warn("补救逻辑执行失败", e); + } + + return originalResult; + } +} diff --git a/src/main/java/com/ycwl/basic/service/pc/processor/SourceRelationProcessor.java b/src/main/java/com/ycwl/basic/service/pc/processor/SourceRelationProcessor.java new file mode 100644 index 00000000..e33c6f08 --- /dev/null +++ b/src/main/java/com/ycwl/basic/service/pc/processor/SourceRelationProcessor.java @@ -0,0 +1,144 @@ +package com.ycwl.basic.service.pc.processor; + +import com.ycwl.basic.integration.common.manager.DeviceConfigManager; +import com.ycwl.basic.integration.common.manager.ScenicConfigManager; +import com.ycwl.basic.mapper.SourceMapper; +import com.ycwl.basic.model.pc.face.entity.FaceEntity; +import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity; +import com.ycwl.basic.model.pc.source.entity.SourceEntity; +import com.ycwl.basic.repository.DeviceRepository; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * 源文件关联处理器 + * 负责处理人脸匹配后的源文件关联逻辑 + * + * @author longbinbin + * @date 2025-01-31 + */ +@Slf4j +@Component +public class SourceRelationProcessor { + + @Autowired + private SourceMapper sourceMapper; + + @Autowired + private DeviceRepository deviceRepository; + + /** + * 处理源文件关联逻辑 + * 根据匹配的样本ID创建MemberSourceEntity列表 + * + * @param sampleListIds 匹配的样本ID列表 + * @param face 人脸实体 + * @return MemberSourceEntity列表 + */ + public List processMemberSources(List sampleListIds, FaceEntity face) { + if (sampleListIds == null || sampleListIds.isEmpty()) { + return Collections.emptyList(); + } + + List sourceEntities = sourceMapper.listBySampleIds(sampleListIds); + if (sourceEntities.isEmpty()) { + return Collections.emptyList(); + } + + // 按设备分组并应用限制 + List filteredSourceEntities = sourceEntities.stream() + .sorted(Comparator.comparing(SourceEntity::getCreateTime).reversed()) + .collect(Collectors.groupingBy(SourceEntity::getDeviceId)) + .entrySet() + .stream().flatMap(entry -> { + DeviceConfigManager configManager = deviceRepository.getDeviceConfigManager(entry.getKey()); + if (configManager.getInteger("limit_video", 0) > 0) { + // 优先保留所有图片,然后限制视频数量 + return Stream.concat( + entry.getValue().stream().filter(item -> item.getType() == 2), + entry.getValue().stream().filter(item -> item.getType() == 1) + .limit(Math.min(entry.getValue().size(), configManager.getInteger("limit_video", 0))) + ); + } + return entry.getValue().stream(); + }).toList(); + + // 创建MemberSourceEntity列表 + return filteredSourceEntities.stream().map(sourceEntity -> { + DeviceConfigManager deviceConfig = deviceRepository.getDeviceConfigManager(sourceEntity.getDeviceId()); + MemberSourceEntity memberSourceEntity = new MemberSourceEntity(); + memberSourceEntity.setScenicId(face.getScenicId()); + memberSourceEntity.setFaceId(face.getId()); + memberSourceEntity.setMemberId(face.getMemberId()); + memberSourceEntity.setSourceId(sourceEntity.getId()); + memberSourceEntity.setType(sourceEntity.getType()); + + // 设置免费状态 - 默认收费 + memberSourceEntity.setIsFree(0); + + if (deviceConfig != null) { + // 视频类型检查 + if (sourceEntity.getType() == 1) { + if (Integer.valueOf(1).equals(deviceConfig.getInteger("video_free"))) { + memberSourceEntity.setIsFree(1); + } + } + // 图片类型检查 + else if (sourceEntity.getType() == 2) { + if (Integer.valueOf(1).equals(deviceConfig.getInteger("image_free"))) { + memberSourceEntity.setIsFree(1); + } + } + } + + return memberSourceEntity; + }).collect(Collectors.toList()); + } + + /** + * 处理免费源文件逻辑 + * 根据景区配置和是否新用户决定哪些照片可以免费 + * + * @param memberSourceEntityList 源文件关联列表 + * @param scenicConfig 景区配置 + * @param isNew 是否新用户 + * @return 免费的源文件ID列表 + */ + public List processFreeSourceLogic(List memberSourceEntityList, + ScenicConfigManager scenicConfig, + boolean isNew) { + List freeSourceIds = new ArrayList<>(); + + if (memberSourceEntityList.isEmpty()) { + return freeSourceIds; + } + + if (isNew) { + // 新用户送照片逻辑 + List photoSource = memberSourceEntityList.stream() + .filter(item -> item.getIsFree() == 0) // 只考虑收费的 + .filter(item -> Integer.valueOf(2).equals(item.getType())) // 只考虑照片类型 + .toList(); + + Integer photoFreeNum = scenicConfig != null ? scenicConfig.getInteger("photo_free_num") : null; + if (scenicConfig != null && photoFreeNum != null && photoFreeNum > 0) { + + int freePhotoCount = Math.min(photoFreeNum, photoSource.size()); + freeSourceIds.addAll(photoSource.stream() + .limit(freePhotoCount) + .map(MemberSourceEntity::getSourceId) + .toList()); + + log.debug("新用户免费照片逻辑:配置免费数量 {}, 实际可用 {}, 赠送 {} 张", + photoFreeNum, photoSource.size(), freePhotoCount); + } + } + + return freeSourceIds; + } +} diff --git a/src/main/java/com/ycwl/basic/service/pc/processor/VideoRecreationHandler.java b/src/main/java/com/ycwl/basic/service/pc/processor/VideoRecreationHandler.java new file mode 100644 index 00000000..2787370a --- /dev/null +++ b/src/main/java/com/ycwl/basic/service/pc/processor/VideoRecreationHandler.java @@ -0,0 +1,113 @@ +package com.ycwl.basic.service.pc.processor; + +import com.ycwl.basic.integration.common.manager.DeviceConfigManager; +import com.ycwl.basic.integration.common.manager.ScenicConfigManager; +import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity; +import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity; +import com.ycwl.basic.repository.DeviceRepository; +import com.ycwl.basic.repository.FaceRepository; +import com.ycwl.basic.task.VideoPieceGetter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 视频重切处理器 + * 负责处理视频重切任务的创建逻辑 + * + * @author longbinbin + * @date 2025-01-31 + */ +@Slf4j +@Component +public class VideoRecreationHandler { + + @Autowired + private FaceRepository faceRepository; + + @Autowired + private DeviceRepository deviceRepository; + + /** + * 处理视频重切逻辑 + * 当非新用户且照片数量大于视频数量时,创建视频重切任务 + * + * @param scenicConfig 景区配置 + * @param memberSourceEntityList 源文件关联列表 + * @param faceId 人脸ID + * @param memberId 会员ID + * @param sampleListIds 样本ID列表(用于日志) + * @param isNew 是否新用户 + */ + public void handleVideoRecreation(ScenicConfigManager scenicConfig, + List memberSourceEntityList, + Long faceId, + Long memberId, + List sampleListIds, + boolean isNew) { + // 新用户不执行视频重切逻辑 + if (isNew) { + return; + } + + // 检查景区是否禁用源视频功能 + Boolean disableSourceVideo = scenicConfig != null ? scenicConfig.getBoolean("disable_source_video") : null; + if (scenicConfig == null || Boolean.TRUE.equals(disableSourceVideo)) { + log.debug("视频重切逻辑跳过:景区禁用了源视频功能"); + return; + } + + // 统计视频和照片数量 + long videoCount = memberSourceEntityList.stream() + .filter(item -> Integer.valueOf(1).equals(item.getType())) + .count(); + long photoCount = memberSourceEntityList.stream() + .filter(item -> Integer.valueOf(2).equals(item.getType())) + .count(); + + List faceSampleList = faceRepository.getFaceSampleList(faceId); + if (faceSampleList.isEmpty()) { + log.info("faceId:{} sample list not exist", faceId); + return; + } + + // 筛选样本ID + List faceSampleIds = faceSampleList.stream() + .sorted(Comparator.comparing(FaceSampleEntity::getCreateAt).reversed()) + .collect(Collectors.groupingBy(FaceSampleEntity::getDeviceId)) + .entrySet() + .stream().flatMap(entry -> { + DeviceConfigManager configManager = deviceRepository.getDeviceConfigManager(entry.getKey()); + if (configManager.getInteger("limit_video", 0) > 0) { + return entry.getValue().subList(0, Math.min(entry.getValue().size(), configManager.getInteger("limit_video", 0))).stream(); + } + return entry.getValue().stream(); + }).toList() + .stream().map(FaceSampleEntity::getId).toList(); + + log.info("视频切分任务: faceId={}, 原始数量={}, 筛选后数量={}", faceId, faceSampleList.size(), faceSampleIds.size()); + log.debug("视频重切逻辑:视频数量 {}, 照片数量 {}", videoCount, photoCount); + + // 只有照片数量大于视频数量时才创建重切任务 + if (photoCount > videoCount) { + VideoPieceGetter.Task task = new VideoPieceGetter.Task(); + task.faceId = faceId; + task.faceSampleIds = faceSampleIds; + task.templateId = null; + task.memberId = memberId; + task.callback = () -> { + log.info("视频重切任务回调: {}", task); + }; + + VideoPieceGetter.addTask(task); + log.debug("视频重切任务已创建:faceId={}, memberId={}, sampleIds={}", + faceId, memberId, sampleListIds.size()); + } else { + log.debug("视频重切逻辑跳过:照片数量({})未超过视频数量({})", photoCount, videoCount); + } + } +}