diff --git a/src/main/java/com/ycwl/basic/controller/mobile/AppAiCamController.java b/src/main/java/com/ycwl/basic/controller/mobile/AppAiCamController.java new file mode 100644 index 00000000..0d3f8e3c --- /dev/null +++ b/src/main/java/com/ycwl/basic/controller/mobile/AppAiCamController.java @@ -0,0 +1,61 @@ +package com.ycwl.basic.controller.mobile; + +import com.ycwl.basic.model.mobile.goods.GoodsDetailVO; +import com.ycwl.basic.service.mobile.AppAiCamService; +import com.ycwl.basic.utils.ApiResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * AI相机相关接口 + */ +@Slf4j +@RestController +@RequestMapping("/api/mobile/ai_cam/v1") +@RequiredArgsConstructor +public class AppAiCamController { + + private final AppAiCamService appAiCamService; + + /** + * 根据faceId获取AI相机识别到的商品列表 + * @param faceId 人脸ID + * @return 商品详情列表 + */ + @GetMapping("/{faceId}/content") + public ApiResponse> getAiCamGoods(@PathVariable Long faceId) { + try { + List goods = appAiCamService.getAiCamGoodsByFaceId(faceId); + return ApiResponse.success(goods); + } catch (Exception e) { + log.error("获取AI相机商品失败: faceId={}", faceId, e); + return ApiResponse.fail("获取商品列表失败"); + } + } + + /** + * 批量添加会员与source的关联关系 + * @param faceId 人脸ID + * @param sourceIds source ID列表 + * @return 添加结果 + */ + @PostMapping("/{faceId}/relations") + public ApiResponse addMemberSourceRelations( + @PathVariable Long faceId, + @RequestBody List sourceIds + ) { + try { + int count = appAiCamService.addMemberSourceRelations(faceId, sourceIds); + return ApiResponse.success("成功添加" + count + "条关联记录"); + } catch (IllegalArgumentException e) { + log.warn("添加关联失败: faceId={}, error={}", faceId, e.getMessage()); + return ApiResponse.fail(e.getMessage()); + } catch (Exception e) { + log.error("添加关联失败: faceId={}", faceId, e); + return ApiResponse.fail("添加关联失败"); + } + } +} diff --git a/src/main/java/com/ycwl/basic/mapper/FaceDetectLogAiCamMapper.java b/src/main/java/com/ycwl/basic/mapper/FaceDetectLogAiCamMapper.java index 5c5188c8..006be691 100644 --- a/src/main/java/com/ycwl/basic/mapper/FaceDetectLogAiCamMapper.java +++ b/src/main/java/com/ycwl/basic/mapper/FaceDetectLogAiCamMapper.java @@ -3,10 +3,19 @@ package com.ycwl.basic.mapper; import com.ycwl.basic.model.pc.faceDetectLog.entity.FaceDetectLogAiCamEntity; import org.apache.ibatis.annotations.Mapper; +import java.util.List; + /** * AI相机人脸识别日志Mapper */ @Mapper public interface FaceDetectLogAiCamMapper { int add(FaceDetectLogAiCamEntity entity); + + /** + * 根据faceId查询所有识别记录 + * @param faceId 人脸ID + * @return 识别记录列表 + */ + List listByFaceId(Long faceId); } diff --git a/src/main/java/com/ycwl/basic/mapper/SourceMapper.java b/src/main/java/com/ycwl/basic/mapper/SourceMapper.java index 065da456..ca7b90e5 100644 --- a/src/main/java/com/ycwl/basic/mapper/SourceMapper.java +++ b/src/main/java/com/ycwl/basic/mapper/SourceMapper.java @@ -148,4 +148,12 @@ public interface SourceMapper { * @return source实体 */ SourceEntity getSourceByFaceAndDeviceId(Long faceId, Long deviceId, Integer type, String sortStrategy); + + /** + * 根据faceSampleId列表和type查询source列表 + * @param faceSampleIds faceSampleId列表 + * @param type 素材类型 + * @return source实体列表 + */ + List listByFaceSampleIdsAndType(List faceSampleIds, Integer type); } diff --git a/src/main/java/com/ycwl/basic/service/mobile/AppAiCamService.java b/src/main/java/com/ycwl/basic/service/mobile/AppAiCamService.java new file mode 100644 index 00000000..4aa8abde --- /dev/null +++ b/src/main/java/com/ycwl/basic/service/mobile/AppAiCamService.java @@ -0,0 +1,26 @@ +package com.ycwl.basic.service.mobile; + +import com.ycwl.basic.model.mobile.goods.GoodsDetailVO; + +import java.util.List; + +/** + * AI相机相关服务 + */ +public interface AppAiCamService { + + /** + * 根据faceId获取AI相机识别到的商品列表 + * @param faceId 人脸ID + * @return 商品详情列表 + */ + List getAiCamGoodsByFaceId(Long faceId); + + /** + * 批量添加会员与source的关联关系 + * @param faceId 人脸ID + * @param sourceIds source ID列表 + * @return 添加成功的数量 + */ + int addMemberSourceRelations(Long faceId, List sourceIds); +} diff --git a/src/main/java/com/ycwl/basic/service/mobile/impl/AppAiCamServiceImpl.java b/src/main/java/com/ycwl/basic/service/mobile/impl/AppAiCamServiceImpl.java new file mode 100644 index 00000000..f77aba7f --- /dev/null +++ b/src/main/java/com/ycwl/basic/service/mobile/impl/AppAiCamServiceImpl.java @@ -0,0 +1,202 @@ +package com.ycwl.basic.service.mobile.impl; + +import com.ycwl.basic.facebody.entity.SearchFaceResp; +import com.ycwl.basic.facebody.entity.SearchFaceResultItem; +import com.ycwl.basic.integration.common.manager.DeviceConfigManager; +import com.ycwl.basic.mapper.FaceDetectLogAiCamMapper; +import com.ycwl.basic.mapper.FaceMapper; +import com.ycwl.basic.mapper.SourceMapper; +import com.ycwl.basic.model.mobile.goods.GoodsDetailVO; +import com.ycwl.basic.model.pc.face.entity.FaceEntity; +import com.ycwl.basic.model.pc.faceDetectLog.entity.FaceDetectLogAiCamEntity; +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.mobile.AppAiCamService; +import com.ycwl.basic.utils.JacksonUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * AI相机相关服务实现 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class AppAiCamServiceImpl implements AppAiCamService { + + private final FaceDetectLogAiCamMapper faceDetectLogAiCamMapper; + private final SourceMapper sourceMapper; + private final FaceMapper faceMapper; + private final DeviceRepository deviceRepository; + + private static final float DEFAULT_SCORE_THRESHOLD = 0.8f; + private static final int DEFAULT_PHOTO_LIMIT = 10; + private static final int AI_CAM_SOURCE_TYPE = 3; + + @Override + public List getAiCamGoodsByFaceId(Long faceId) { + // 1. 查询该faceId的所有识别记录 + List detectLogs = faceDetectLogAiCamMapper.listByFaceId(faceId); + if (detectLogs == null || detectLogs.isEmpty()) { + return Collections.emptyList(); + } + + // 2. 按设备分组并根据设备配置过滤faceSampleId + Map> deviceFaceSampleMap = new LinkedHashMap<>(); + + // 按设备分组识别记录 + Map> deviceLogsMap = detectLogs.stream() + .collect(Collectors.groupingBy(FaceDetectLogAiCamEntity::getDeviceId)); + + // 遍历每个设备的识别记录 + for (Map.Entry> entry : deviceLogsMap.entrySet()) { + Long deviceId = entry.getKey(); + List deviceLogs = entry.getValue(); + + // 获取设备配置 + DeviceConfigManager configManager = deviceRepository.getDeviceConfigManager(deviceId); + + // 获取该设备的分数阈值(百分制,需要转换为0-1) + float scoreThreshold = DEFAULT_SCORE_THRESHOLD; + if (configManager != null) { + Float thresholdPercent = configManager.getFloat("ai_cam_face_score_threshold"); + if (thresholdPercent != null) { + scoreThreshold = thresholdPercent / 100.0f; + log.debug("设备{}使用配置的分数阈值: {}% ({})", deviceId, thresholdPercent, scoreThreshold); + } + } + + // 获取该设备的照片数量限制 + int photoLimit = DEFAULT_PHOTO_LIMIT; + if (configManager != null) { + Integer limit = configManager.getInteger("ai_cam_photo_limit"); + if (limit != null && limit > 0) { + photoLimit = limit; + log.debug("设备{}使用配置的照片限制: {}", deviceId, photoLimit); + } + } + + // 收集该设备符合阈值的faceSampleId + List deviceFaceSampleIds = new ArrayList<>(); + for (FaceDetectLogAiCamEntity detectLog : deviceLogs) { + if (detectLog.getMatchRawResult() == null) { + continue; + } + try { + SearchFaceResp resp = JacksonUtil.parseObject(detectLog.getMatchRawResult(), SearchFaceResp.class); + if (resp != null && resp.getResult() != null) { + for (SearchFaceResultItem item : resp.getResult()) { + // 使用设备配置的分数阈值 + if (item.getScore() != null && item.getScore() >= scoreThreshold && item.getExtData() != null) { + try { + Long faceSampleId = Long.parseLong(item.getExtData()); + deviceFaceSampleIds.add(faceSampleId); + } catch (NumberFormatException e) { + log.warn("解析faceSampleId失败: extData={}", item.getExtData()); + } + } + } + } + } catch (Exception e) { + log.error("解析matchRawResult失败: logId={}", detectLog.getId(), e); + } + } + + // 应用照片数量限制(保留前N个) + if (deviceFaceSampleIds.size() > photoLimit) { + log.debug("设备{}的照片数量{}超过限制{},截取前{}张", + deviceId, deviceFaceSampleIds.size(), photoLimit, photoLimit); + deviceFaceSampleIds = deviceFaceSampleIds.subList(0, photoLimit); + } + + if (!deviceFaceSampleIds.isEmpty()) { + deviceFaceSampleMap.put(deviceId, deviceFaceSampleIds); + } + } + + // 3. 合并所有设备的faceSampleId(去重) + Set faceSampleIds = new HashSet<>(); + for (List ids : deviceFaceSampleMap.values()) { + faceSampleIds.addAll(ids); + } + + if (faceSampleIds.isEmpty()) { + log.debug("没有符合条件的faceSampleId, faceId={}", faceId); + return Collections.emptyList(); + } + + log.info("人脸{}在{}个设备上识别到{}个不重复的faceSampleId", + faceId, deviceFaceSampleMap.size(), faceSampleIds.size()); + + // 4. 根据faceSampleId列表查询type=3的source记录 + List sources = sourceMapper.listByFaceSampleIdsAndType( + new ArrayList<>(faceSampleIds), AI_CAM_SOURCE_TYPE + ); + + if (sources == null || sources.isEmpty()) { + log.debug("未找到type=3的source记录, faceId={}", faceId); + return Collections.emptyList(); + } + + log.info("查询到{}条AI相机图像记录, faceId={}", sources.size(), faceId); + + // 5. 查询Face信息以获取scenicId + FaceEntity face = faceMapper.get(faceId); + if (face == null) { + log.warn("Face不存在: faceId={}", faceId); + return Collections.emptyList(); + } + + // 6. 转换为GoodsDetailVO + return sources.stream().map(source -> { + GoodsDetailVO vo = new GoodsDetailVO(); + vo.setFaceId(faceId); + vo.setScenicId(face.getScenicId()); + vo.setGoodsType(2); // 2表示原素材 + vo.setGoodsId(source.getId()); + vo.setUrl(source.getUrl()); + vo.setVideoUrl(source.getVideoUrl()); + vo.setCreateTime(source.getCreateTime()); + vo.setIsBuy(source.getIsBuy() != null ? source.getIsBuy() : 0); + return vo; + }).collect(Collectors.toList()); + } + + @Override + public int addMemberSourceRelations(Long faceId, List sourceIds) { + if (sourceIds == null || sourceIds.isEmpty()) { + return 0; + } + + // 查询Face信息 + FaceEntity face = faceMapper.get(faceId); + if (face == null) { + throw new IllegalArgumentException("Face不存在: faceId=" + faceId); + } + + if (face.getMemberId() == null) { + throw new IllegalArgumentException("Face未关联会员: faceId=" + faceId); + } + + // 构建MemberSourceEntity列表 + List relations = sourceIds.stream().map(sourceId -> { + MemberSourceEntity entity = new MemberSourceEntity(); + entity.setMemberId(face.getMemberId()); + entity.setScenicId(face.getScenicId()); + entity.setFaceId(faceId); + entity.setSourceId(sourceId); + entity.setType(AI_CAM_SOURCE_TYPE); + entity.setIsBuy(0); + entity.setIsFree(0); + return entity; + }).collect(Collectors.toList()); + + // 批量插入 + return sourceMapper.addRelations(relations); + } +} diff --git a/src/main/java/com/ycwl/basic/service/pc/impl/FaceDetectLogAiCamServiceImpl.java b/src/main/java/com/ycwl/basic/service/pc/impl/FaceDetectLogAiCamServiceImpl.java index 1531358e..d77fa538 100644 --- a/src/main/java/com/ycwl/basic/service/pc/impl/FaceDetectLogAiCamServiceImpl.java +++ b/src/main/java/com/ycwl/basic/service/pc/impl/FaceDetectLogAiCamServiceImpl.java @@ -42,7 +42,7 @@ public class FaceDetectLogAiCamServiceImpl implements FaceDetectLogAiCamService // 调用适配器搜索人脸 resp = adapter.searchFace(dbName, faceUrl); } catch (Exception e) { - log.error("AI相机人脸搜索异常: scenicId={}, deviceId={}, faceSampleId={}", scenicId, deviceId, faceId, e); + log.error("AI相机人脸搜索异常: scenicId={}, deviceId={}, faceId={}", scenicId, deviceId, faceId, e); // 发生异常时记录空结果或错误信息,视业务需求而定。这里暂不中断流程,继续记录日志 } @@ -71,7 +71,7 @@ public class FaceDetectLogAiCamServiceImpl implements FaceDetectLogAiCamService faceDetectLogAiCamMapper.add(logEntity); } catch (Exception e) { - log.error("保存AI相机人脸识别日志失败: faceSampleId={}", faceId, e); + log.error("保存AI相机人脸识别日志失败: faceId={}", faceId, e); } return resp; diff --git a/src/main/resources/mapper/FaceDetectLogAiCamMapper.xml b/src/main/resources/mapper/FaceDetectLogAiCamMapper.xml index e8fea611..b9fcfc95 100644 --- a/src/main/resources/mapper/FaceDetectLogAiCamMapper.xml +++ b/src/main/resources/mapper/FaceDetectLogAiCamMapper.xml @@ -2,7 +2,16 @@ - insert into face_detect_log_ai_cam(scenic_id, device_id, face_sample_id, db_name, face_url, score, match_raw_result, create_time) - values (#{scenicId}, #{deviceId}, #{faceSampleId}, #{dbName}, #{faceUrl}, #{score}, #{matchRawResult}, #{createTime}) + insert into face_detect_log_ai_cam(scenic_id, device_id, face_id, db_name, face_url, score, match_raw_result, create_time) + values (#{scenicId}, #{deviceId}, #{faceId}, #{dbName}, #{faceUrl}, #{score}, #{matchRawResult}, #{createTime}) + + diff --git a/src/main/resources/mapper/SourceMapper.xml b/src/main/resources/mapper/SourceMapper.xml index 5442a530..7df2f4d5 100644 --- a/src/main/resources/mapper/SourceMapper.xml +++ b/src/main/resources/mapper/SourceMapper.xml @@ -486,4 +486,14 @@ LIMIT 1 + +