From 9a18ffc167c85520afcb058b16356b3f3e35cc7c Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Sun, 15 Feb 2026 02:48:13 +0800 Subject: [PATCH] =?UTF-8?q?feat(face):=20=E6=B7=BB=E5=8A=A0=E4=BA=BA?= =?UTF-8?q?=E8=84=B8=E6=A0=B7=E6=9C=AC=E5=85=B3=E8=81=94=E5=88=86=E7=BB=84?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 FaceSampleAssociationController 提供关联管理API - 新增 ExpandSampleAssociationStage 扩展匹配结果中的关联样本 - 在人脸匹配流程中集成关联样本扩展逻辑 - 新增 FaceSampleAssociationMapper 和相关实体类 - 修改 UpdateFaceResultStage 使用扩展后的样本ID列表 - 添加关联样本扩展的日志记录和统计功能 --- .../pc/FaceSampleAssociationController.java | 111 +++++++++++++++++ .../pipeline/core/FaceMatchingContext.java | 6 + .../factory/FaceMatchingPipelineFactory.java | 49 +++++--- .../stages/ExpandSampleAssociationStage.java | 116 ++++++++++++++++++ .../stages/UpdateFaceResultStage.java | 13 +- .../mapper/FaceSampleAssociationMapper.java | 48 ++++++++ .../entity/FaceSampleAssociationEntity.java | 26 ++++ .../req/FaceSampleAssociationReq.java | 18 +++ .../service/pc/impl/FaceServiceImpl.java | 38 ++++++ .../FaceMatchingOrchestrator.java | 43 +++++++ .../mapper/FaceSampleAssociationMapper.xml | 58 +++++++++ 11 files changed, 504 insertions(+), 22 deletions(-) create mode 100644 src/main/java/com/ycwl/basic/controller/pc/FaceSampleAssociationController.java create mode 100644 src/main/java/com/ycwl/basic/face/pipeline/stages/ExpandSampleAssociationStage.java create mode 100644 src/main/java/com/ycwl/basic/mapper/FaceSampleAssociationMapper.java create mode 100644 src/main/java/com/ycwl/basic/model/pc/faceSample/entity/FaceSampleAssociationEntity.java create mode 100644 src/main/java/com/ycwl/basic/model/pc/faceSample/req/FaceSampleAssociationReq.java create mode 100644 src/main/resources/mapper/FaceSampleAssociationMapper.xml diff --git a/src/main/java/com/ycwl/basic/controller/pc/FaceSampleAssociationController.java b/src/main/java/com/ycwl/basic/controller/pc/FaceSampleAssociationController.java new file mode 100644 index 00000000..1c57a70d --- /dev/null +++ b/src/main/java/com/ycwl/basic/controller/pc/FaceSampleAssociationController.java @@ -0,0 +1,111 @@ +package com.ycwl.basic.controller.pc; + +import com.ycwl.basic.annotation.IgnoreToken; +import com.ycwl.basic.mapper.FaceSampleAssociationMapper; +import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleAssociationEntity; +import com.ycwl.basic.model.pc.faceSample.req.FaceSampleAssociationReq; +import com.ycwl.basic.utils.ApiResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 人脸样本关联分组管理 + */ +@Slf4j +@RestController +@RequestMapping("/api/faceSampleAssociation/v1") +@IgnoreToken +public class FaceSampleAssociationController { + + @Autowired + private FaceSampleAssociationMapper faceSampleAssociationMapper; + + /** + * 添加关联:将一批 faceSampleId 加入指定的 groupKey + */ + @PostMapping("/add") + public ApiResponse add(@RequestBody FaceSampleAssociationReq req) { + if (req.getScenicId() == null || req.getGroupKey() == null || req.getGroupKey().isBlank()) { + return ApiResponse.fail("scenicId 和 groupKey 不能为空"); + } + if (req.getFaceSampleIds() == null || req.getFaceSampleIds().isEmpty()) { + return ApiResponse.fail("faceSampleIds 不能为空"); + } + + List entities = req.getFaceSampleIds().stream() + .map(sampleId -> { + FaceSampleAssociationEntity entity = new FaceSampleAssociationEntity(); + entity.setScenicId(req.getScenicId()); + entity.setGroupKey(req.getGroupKey()); + entity.setFaceSampleId(sampleId); + return entity; + }) + .collect(Collectors.toList()); + + faceSampleAssociationMapper.batchInsertIgnore(entities); + log.info("添加人脸样本关联: scenicId={}, groupKey={}, count={}", + req.getScenicId(), req.getGroupKey(), entities.size()); + return ApiResponse.success("添加成功"); + } + + /** + * 移除关联:从指定 groupKey 中移除一批 faceSampleId + */ + @PostMapping("/remove") + public ApiResponse remove(@RequestBody FaceSampleAssociationReq req) { + if (req.getScenicId() == null || req.getGroupKey() == null || req.getGroupKey().isBlank()) { + return ApiResponse.fail("scenicId 和 groupKey 不能为空"); + } + if (req.getFaceSampleIds() == null || req.getFaceSampleIds().isEmpty()) { + return ApiResponse.fail("faceSampleIds 不能为空"); + } + + faceSampleAssociationMapper.deleteByGroupAndSampleIds( + req.getScenicId(), req.getGroupKey(), req.getFaceSampleIds()); + log.info("移除人脸样本关联: scenicId={}, groupKey={}, count={}", + req.getScenicId(), req.getGroupKey(), req.getFaceSampleIds().size()); + return ApiResponse.success("移除成功"); + } + + /** + * 删除整个组 + */ + @PostMapping("/deleteGroup") + public ApiResponse deleteGroup(@RequestParam Long scenicId, @RequestParam String groupKey) { + if (scenicId == null || groupKey == null || groupKey.isBlank()) { + return ApiResponse.fail("scenicId 和 groupKey 不能为空"); + } + + faceSampleAssociationMapper.deleteByGroup(scenicId, groupKey); + log.info("删除关联组: scenicId={}, groupKey={}", scenicId, groupKey); + return ApiResponse.success("删除成功"); + } + + /** + * 查询指定组的所有样本ID + */ + @GetMapping("/listByGroup") + public ApiResponse> listByGroup(@RequestParam Long scenicId, @RequestParam String groupKey) { + List sampleIds = faceSampleAssociationMapper.listSampleIdsByGroup(scenicId, groupKey); + return ApiResponse.success(sampleIds); + } + + /** + * 查询指定景区下的所有分组及其样本ID + */ + @GetMapping("/listGroups") + public ApiResponse>> listGroups(@RequestParam Long scenicId) { + List groupKeys = faceSampleAssociationMapper.listGroupKeysByScenicId(scenicId); + Map> result = groupKeys.stream() + .collect(Collectors.toMap( + groupKey -> groupKey, + groupKey -> faceSampleAssociationMapper.listSampleIdsByGroup(scenicId, groupKey) + )); + return ApiResponse.success(result); + } +} diff --git a/src/main/java/com/ycwl/basic/face/pipeline/core/FaceMatchingContext.java b/src/main/java/com/ycwl/basic/face/pipeline/core/FaceMatchingContext.java index dfb190f9..5abcabb1 100644 --- a/src/main/java/com/ycwl/basic/face/pipeline/core/FaceMatchingContext.java +++ b/src/main/java/com/ycwl/basic/face/pipeline/core/FaceMatchingContext.java @@ -89,6 +89,12 @@ public class FaceMatchingContext implements PipelineContext { */ private List freeSourceIds; + /** + * 关联扩展的样本ID列表(由 ExpandSampleAssociationStage 设置) + * 用于标识哪些 sampleId 是通过关联分组扩展得到的,而非直接匹配 + */ + private List associatedSampleIds; + /** * 人脸选择后置模式配置(自定义匹配场景) * 0: 并集, 1: 交集, 2: 直接使用 diff --git a/src/main/java/com/ycwl/basic/face/pipeline/factory/FaceMatchingPipelineFactory.java b/src/main/java/com/ycwl/basic/face/pipeline/factory/FaceMatchingPipelineFactory.java index 33c10927..67bd75eb 100644 --- a/src/main/java/com/ycwl/basic/face/pipeline/factory/FaceMatchingPipelineFactory.java +++ b/src/main/java/com/ycwl/basic/face/pipeline/factory/FaceMatchingPipelineFactory.java @@ -66,6 +66,8 @@ public class FaceMatchingPipelineFactory { private FilterByDevicePhotoLimitStage filterByDevicePhotoLimitStage; @Autowired private DeleteOldRelationsStage deleteOldRelationsStage; + @Autowired + private ExpandSampleAssociationStage expandSampleAssociationStage; // ==================== 辅助服务 ==================== @Autowired @@ -91,13 +93,16 @@ public class FaceMatchingPipelineFactory { // 3. 人脸识别补救 builder.addStage(faceRecoveryStage); - // 4. 更新人脸结果(落库) + // 4. 关联样本扩展 + builder.addStage(expandSampleAssociationStage); + + // 5. 更新人脸结果(落库) builder.addStage(updateFaceResultStage); - // 5. 构建源文件关联(建关系) + // 6. 构建源文件关联(建关系) builder.addStage(buildSourceRelationStage); - // 6. 持久化关联关系(建关系) + // 7. 持久化关联关系(建关系) builder.addStage(persistRelationsStage); log.debug("创建打印机大屏识别试点Pipeline: stageCount={}", builder.build().getStageCount()); @@ -131,28 +136,31 @@ public class FaceMatchingPipelineFactory { // 5. 人脸识别补救 builder.addStage(faceRecoveryStage); - // 6. 更新人脸结果 + // 6. 关联样本扩展 + builder.addStage(expandSampleAssociationStage); + + // 7. 更新人脸结果 builder.addStage(updateFaceResultStage); - // 7. 构建源文件关联 + // 8. 构建源文件关联 builder.addStage(buildSourceRelationStage); - // 8. 处理免费源文件逻辑 + // 9. 处理免费源文件逻辑 builder.addStage(processFreeSourceStage); - // 9. 处理购买状态 + // 10. 处理购买状态 builder.addStage(processBuyStatusStage); - // 10. 处理视频重切 + // 11. 处理视频重切 builder.addStage(handleVideoRecreationStage); - // 11. 持久化关联关系 + // 12. 持久化关联关系 builder.addStage(persistRelationsStage); - // 12. 创建任务 + // 13. 创建任务 builder.addStage(createTaskStage); - // 13. 异步生成拼图模板 + // 14. 异步生成拼图模板 builder.addStage(generatePuzzleStage); log.debug("创建自动人脸匹配Pipeline: isNew={}, stageCount={}", isNew, builder.build().getStageCount()); @@ -189,28 +197,31 @@ public class FaceMatchingPipelineFactory { // 7. 应用设备照片数量限制筛选 builder.addStage(filterByDevicePhotoLimitStage); - // 8. 更新人脸结果 + // 8. 关联样本扩展 + builder.addStage(expandSampleAssociationStage); + + // 9. 更新人脸结果 builder.addStage(updateFaceResultStage); - // 9. 删除旧关系数据 + // 10. 删除旧关系数据 builder.addStage(deleteOldRelationsStage); - // 10. 构建源文件关联 + // 11. 构建源文件关联 builder.addStage(buildSourceRelationStage); - // 11. 处理免费源文件逻辑 + // 12. 处理免费源文件逻辑 builder.addStage(processFreeSourceStage); - // 12. 处理购买状态 + // 13. 处理购买状态 builder.addStage(processBuyStatusStage); - // 13. 处理视频重切 + // 14. 处理视频重切 builder.addStage(handleVideoRecreationStage); - // 14. 持久化关联关系 + // 15. 持久化关联关系 builder.addStage(persistRelationsStage); - // 15. 创建任务 + // 16. 创建任务 builder.addStage(createTaskStage); log.debug("创建自定义人脸匹配Pipeline: stageCount={}", builder.build().getStageCount()); diff --git a/src/main/java/com/ycwl/basic/face/pipeline/stages/ExpandSampleAssociationStage.java b/src/main/java/com/ycwl/basic/face/pipeline/stages/ExpandSampleAssociationStage.java new file mode 100644 index 00000000..cd4e304f --- /dev/null +++ b/src/main/java/com/ycwl/basic/face/pipeline/stages/ExpandSampleAssociationStage.java @@ -0,0 +1,116 @@ +package com.ycwl.basic.face.pipeline.stages; + +import com.ycwl.basic.face.pipeline.core.FaceMatchingContext; +import com.ycwl.basic.mapper.FaceSampleAssociationMapper; +import com.ycwl.basic.model.task.resp.SearchFaceRespVo; +import com.ycwl.basic.pipeline.annotation.StageConfig; +import com.ycwl.basic.pipeline.core.AbstractPipelineStage; +import com.ycwl.basic.pipeline.core.StageResult; +import com.ycwl.basic.pipeline.enums.StageOptionalMode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * 根据人脸样本关联分组扩展匹配结果 + *

+ * 当匹配结果中包含某分组的任一 faceSampleId 时, + * 将该分组内所有 faceSampleId 加入匹配结果。 + * 关联扩展的样本ID跳过时间范围和设备限制筛选(通过Stage执行顺序保证)。 + */ +@Slf4j +@Component +@StageConfig( + stageId = "expand_sample_association", + optionalMode = StageOptionalMode.SUPPORT, + description = "根据人脸样本关联分组扩展匹配结果", + defaultEnabled = true +) +public class ExpandSampleAssociationStage extends AbstractPipelineStage { + + @Autowired + private FaceSampleAssociationMapper faceSampleAssociationMapper; + + @Override + public String getName() { + return "ExpandSampleAssociation"; + } + + @Override + protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) { + SearchFaceRespVo searchResult = context.getSearchResult(); + if (searchResult == null || searchResult.getSampleListIds() == null + || searchResult.getSampleListIds().isEmpty()) { + return false; + } + return context.getFace() != null && context.getFace().getScenicId() != null; + } + + @Override + protected StageResult doExecute(FaceMatchingContext context) { + // 防御性检查 + SearchFaceRespVo searchResult = context.getSearchResult(); + if (searchResult == null || searchResult.getSampleListIds() == null + || searchResult.getSampleListIds().isEmpty()) { + return StageResult.skipped("searchResult或sampleListIds为空"); + } + + Long scenicId = context.getFace().getScenicId(); + List originalMatchedIds = searchResult.getSampleListIds(); + + try { + List associatedIds = faceSampleAssociationMapper + .findAssociatedSampleIds(scenicId, originalMatchedIds); + + if (associatedIds == null || associatedIds.isEmpty()) { + log.debug("未找到关联样本, faceId={}, scenicId={}", context.getFaceId(), scenicId); + return StageResult.success("无关联样本"); + } + + // 获取当前的 sampleListIds(可能已经过筛选,也可能与 searchResult 相同) + List currentSampleIds = context.getSampleListIds(); + if (currentSampleIds == null || currentSampleIds.isEmpty()) { + currentSampleIds = new ArrayList<>(originalMatchedIds); + } + + // 计算净新增的关联ID + Set currentSet = new HashSet<>(currentSampleIds); + List netNewIds = associatedIds.stream() + .filter(id -> !currentSet.contains(id)) + .distinct() + .collect(Collectors.toList()); + + if (netNewIds.isEmpty()) { + log.debug("关联样本均已存在于当前结果中, faceId={}", context.getFaceId()); + return StageResult.success("关联样本均已存在"); + } + + // 合并:当前筛选后的ID + 净新增关联ID + List expandedIds = Stream.concat( + currentSampleIds.stream(), + netNewIds.stream() + ).distinct().collect(Collectors.toList()); + + context.setSampleListIds(expandedIds); + context.setAssociatedSampleIds(netNewIds); + + log.info("关联样本扩展完成: faceId={}, scenicId={}, 原始匹配数={}, 扩展前={}, 净新增={}, 扩展后={}", + context.getFaceId(), scenicId, + originalMatchedIds.size(), currentSampleIds.size(), + netNewIds.size(), expandedIds.size()); + + return StageResult.success(String.format("关联扩展: +%d个样本", netNewIds.size())); + + } catch (Exception e) { + log.error("关联样本扩展失败, faceId={}, scenicId={}", context.getFaceId(), scenicId, e); + return StageResult.degraded("关联样本扩展失败: " + e.getMessage()); + } + } +} diff --git a/src/main/java/com/ycwl/basic/face/pipeline/stages/UpdateFaceResultStage.java b/src/main/java/com/ycwl/basic/face/pipeline/stages/UpdateFaceResultStage.java index c07fd4cf..0db9093c 100644 --- a/src/main/java/com/ycwl/basic/face/pipeline/stages/UpdateFaceResultStage.java +++ b/src/main/java/com/ycwl/basic/face/pipeline/stages/UpdateFaceResultStage.java @@ -15,6 +15,7 @@ import org.springframework.stereotype.Component; import java.math.BigDecimal; import java.util.Date; +import java.util.List; import java.util.stream.Collectors; /** @@ -67,8 +68,14 @@ public class UpdateFaceResultStage extends AbstractPipelineStage finalSampleIds = context.getSampleListIds(); + if (finalSampleIds == null || finalSampleIds.isEmpty()) { + finalSampleIds = searchResult.getSampleListIds(); + } + if (finalSampleIds != null && !finalSampleIds.isEmpty()) { + faceEntity.setMatchSampleIds(finalSampleIds.stream() .map(String::valueOf) .collect(Collectors.joining(","))); } @@ -83,7 +90,7 @@ public class UpdateFaceResultStage extends AbstractPipelineStage findAssociatedSampleIds(@Param("scenicId") Long scenicId, + @Param("sampleIds") List sampleIds); + + /** + * 批量插入关联记录(忽略重复) + */ + void batchInsertIgnore(@Param("list") List list); + + /** + * 删除指定组内的指定样本关联 + */ + void deleteByGroupAndSampleIds(@Param("scenicId") Long scenicId, + @Param("groupKey") String groupKey, + @Param("sampleIds") List sampleIds); + + /** + * 删除整个组的所有关联 + */ + void deleteByGroup(@Param("scenicId") Long scenicId, + @Param("groupKey") String groupKey); + + /** + * 查询指定组的所有样本ID + */ + List listSampleIdsByGroup(@Param("scenicId") Long scenicId, + @Param("groupKey") String groupKey); + + /** + * 查询指定景区下的所有分组标识 + */ + List listGroupKeysByScenicId(@Param("scenicId") Long scenicId); +} diff --git a/src/main/java/com/ycwl/basic/model/pc/faceSample/entity/FaceSampleAssociationEntity.java b/src/main/java/com/ycwl/basic/model/pc/faceSample/entity/FaceSampleAssociationEntity.java new file mode 100644 index 00000000..e2b1ee92 --- /dev/null +++ b/src/main/java/com/ycwl/basic/model/pc/faceSample/entity/FaceSampleAssociationEntity.java @@ -0,0 +1,26 @@ +package com.ycwl.basic.model.pc.faceSample.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.util.Date; + +/** + * 人脸样本关联分组实体 + * 同一景区下相同 groupKey 的记录互为关联 + */ +@Data +@TableName("face_sample_association") +public class FaceSampleAssociationEntity { + @TableId(type = IdType.AUTO) + private Long id; + /** 景区ID */ + private Long scenicId; + /** 关联分组标识 */ + private String groupKey; + /** 人脸样本ID */ + private Long faceSampleId; + private Date createAt; +} diff --git a/src/main/java/com/ycwl/basic/model/pc/faceSample/req/FaceSampleAssociationReq.java b/src/main/java/com/ycwl/basic/model/pc/faceSample/req/FaceSampleAssociationReq.java new file mode 100644 index 00000000..6850f6e5 --- /dev/null +++ b/src/main/java/com/ycwl/basic/model/pc/faceSample/req/FaceSampleAssociationReq.java @@ -0,0 +1,18 @@ +package com.ycwl.basic.model.pc.faceSample.req; + +import lombok.Data; + +import java.util.List; + +/** + * 人脸样本关联分组请求 + */ +@Data +public class FaceSampleAssociationReq { + /** 景区ID */ + private Long scenicId; + /** 关联分组标识 */ + private String groupKey; + /** 人脸样本ID列表 */ + private List faceSampleIds; +} 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 f395cbdf..79fb5cec 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 @@ -13,6 +13,7 @@ import com.ycwl.basic.face.pipeline.factory.FaceMatchingPipelineFactory; import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter; import com.ycwl.basic.facebody.entity.SearchFaceResultItem; import com.ycwl.basic.integration.device.dto.device.DeviceV2DTO; +import com.ycwl.basic.mapper.FaceSampleAssociationMapper; import com.ycwl.basic.mapper.FaceSampleMapper; import com.ycwl.basic.mapper.ProjectMapper; import com.ycwl.basic.mapper.SourceMapper; @@ -101,6 +102,7 @@ import org.springframework.web.multipart.MultipartFile; import java.io.File; import java.math.BigDecimal; import java.util.ArrayList; +import java.util.HashSet; import java.util.Collections; import java.util.Date; import java.util.HashMap; @@ -161,6 +163,8 @@ public class FaceServiceImpl implements FaceService { @Autowired private FaceSampleMapper faceSampleMapper; @Autowired + private FaceSampleAssociationMapper faceSampleAssociationMapper; + @Autowired private GoodsService goodsService; @Autowired private ProjectMapper projectMapper; @@ -422,6 +426,37 @@ public class FaceServiceImpl implements FaceService { } } + /** + * 关联样本扩展 + * 根据 face_sample_association 表,将同组样本ID加入匹配结果 + */ + private void expandSampleAssociation(SearchFaceRespVo searchResult, Long scenicId, Long faceId) { + List sampleListIds = searchResult.getSampleListIds(); + if (sampleListIds == null || sampleListIds.isEmpty()) { + return; + } + try { + List associatedIds = faceSampleAssociationMapper.findAssociatedSampleIds(scenicId, sampleListIds); + if (associatedIds == null || associatedIds.isEmpty()) { + return; + } + Set currentSet = new HashSet<>(sampleListIds); + List netNewIds = associatedIds.stream() + .filter(id -> !currentSet.contains(id)) + .distinct() + .collect(Collectors.toList()); + if (!netNewIds.isEmpty()) { + List expandedIds = new ArrayList<>(sampleListIds); + expandedIds.addAll(netNewIds); + searchResult.setSampleListIds(expandedIds); + log.info("关联样本扩展完成: faceId={}, scenicId={}, 原始数={}, 新增数={}, 扩展后={}", + faceId, scenicId, sampleListIds.size(), netNewIds.size(), expandedIds.size()); + } + } catch (Exception e) { + log.error("关联样本扩展失败, faceId={}, scenicId={}", faceId, scenicId, e); + } + } + /** * 更新人脸实体结果信息 * 仅用于 handleCustomFaceMatching 方法 @@ -1060,6 +1095,9 @@ public class FaceServiceImpl implements FaceService { allFaceSampleList.size(), filteredSampleIds.size()); } + // 4. 关联样本扩展 + expandSampleAssociation(mergedResult, face.getScenicId(), faceId); + updateFaceEntityResult(face, mergedResult, faceId); List sampleListIds = mergedResult.getSampleListIds(); diff --git a/src/main/java/com/ycwl/basic/service/pc/orchestrator/FaceMatchingOrchestrator.java b/src/main/java/com/ycwl/basic/service/pc/orchestrator/FaceMatchingOrchestrator.java index 6d3bb602..7c0f12a0 100644 --- a/src/main/java/com/ycwl/basic/service/pc/orchestrator/FaceMatchingOrchestrator.java +++ b/src/main/java/com/ycwl/basic/service/pc/orchestrator/FaceMatchingOrchestrator.java @@ -18,6 +18,7 @@ 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.FaceSampleAssociationMapper; import com.ycwl.basic.mapper.SourceMapper; import com.ycwl.basic.model.pc.face.entity.FaceEntity; import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity; @@ -41,8 +42,11 @@ import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import java.math.BigDecimal; +import java.util.ArrayList; import java.util.Date; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -107,6 +111,8 @@ public class FaceMatchingOrchestrator { private FaceStatusManager faceStatusManager; @Autowired private PuzzleRepository puzzleRepository; + @Autowired + private FaceSampleAssociationMapper faceSampleAssociationMapper; /** * 编排人脸匹配的完整流程 @@ -146,6 +152,9 @@ public class FaceMatchingOrchestrator { searchResult = faceRecoveryStrategy.executeFaceRecoveryLogic( searchResult, context.scenicConfig, context.faceBodyAdapter, context.face.getScenicId()); + // 关联样本扩展 + expandSampleAssociation(searchResult, context.face.getScenicId(), faceId); + // 步骤3: 更新人脸结果 updateFaceResult(context.face, searchResult, faceId); @@ -428,6 +437,40 @@ public class FaceMatchingOrchestrator { }); } + /** + * 关联样本扩展 + * 根据 face_sample_association 表,将同组样本ID加入匹配结果 + */ + private void expandSampleAssociation(SearchFaceRespVo searchResult, Long scenicId, Long faceId) { + List sampleListIds = searchResult.getSampleListIds(); + if (sampleListIds == null || sampleListIds.isEmpty()) { + return; + } + try { + List associatedIds = faceSampleAssociationMapper.findAssociatedSampleIds(scenicId, sampleListIds); + if (associatedIds == null || associatedIds.isEmpty()) { + return; + } + + Set currentSet = new HashSet<>(sampleListIds); + List netNewIds = associatedIds.stream() + .filter(id -> !currentSet.contains(id)) + .distinct() + .collect(Collectors.toList()); + + if (!netNewIds.isEmpty()) { + List expandedIds = new ArrayList<>(sampleListIds); + expandedIds.addAll(netNewIds); + searchResult.setSampleListIds(expandedIds); + log.info("关联样本扩展完成: faceId={}, scenicId={}, 原始数={}, 新增数={}, 扩展后={}", + faceId, scenicId, sampleListIds.size(), netNewIds.size(), expandedIds.size()); + } + } catch (Exception e) { + log.error("关联样本扩展失败, faceId={}, scenicId={}", faceId, scenicId, e); + // 扩展失败不影响主流程 + } + } + /** * 匹配上下文 * 封装匹配过程中需要的所有上下文信息 diff --git a/src/main/resources/mapper/FaceSampleAssociationMapper.xml b/src/main/resources/mapper/FaceSampleAssociationMapper.xml new file mode 100644 index 00000000..caa64b27 --- /dev/null +++ b/src/main/resources/mapper/FaceSampleAssociationMapper.xml @@ -0,0 +1,58 @@ + + + + + + + + INSERT IGNORE INTO face_sample_association (scenic_id, group_key, face_sample_id) + VALUES + + (#{item.scenicId}, #{item.groupKey}, #{item.faceSampleId}) + + + + + DELETE FROM face_sample_association + WHERE scenic_id = #{scenicId} + AND group_key = #{groupKey} + AND face_sample_id IN + + #{sampleId} + + + + + DELETE FROM face_sample_association + WHERE scenic_id = #{scenicId} + AND group_key = #{groupKey} + + + + + + +