From 6e7b4729a8edcb317cd7c280e74b75cf048e0cf0 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Tue, 9 Dec 2025 16:20:50 +0800 Subject: [PATCH] =?UTF-8?q?feat(ai-cam):=20=E6=96=B0=E5=A2=9E=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E4=BA=BA=E8=84=B8=E6=A0=B7=E6=9C=AC=E5=88=9B=E5=BB=BA?= =?UTF-8?q?=E6=88=96=E8=8E=B7=E5=8F=96Face=E8=AE=B0=E5=BD=95=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在AppAiCamController中新增/useSample/{faceSampleId}接口 - 实现通过人脸样本ID查找或创建Face记录的业务逻辑 - 自动关联AI相机照片到用户人脸记录 - 支持AI_CAM设备类型的二维码路径配置 - 完善人脸匹配及日志记录功能 - 添加相关实体类和工具类导入依赖 --- .../controller/mobile/AppAiCamController.java | 20 ++++ .../printer/PrinterTvController.java | 19 ++- .../basic/service/mobile/AppAiCamService.java | 9 ++ .../mobile/impl/AppAiCamServiceImpl.java | 108 ++++++++++++++++++ 4 files changed, 150 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/ycwl/basic/controller/mobile/AppAiCamController.java b/src/main/java/com/ycwl/basic/controller/mobile/AppAiCamController.java index 0d3f8e3c..24fbe95c 100644 --- a/src/main/java/com/ycwl/basic/controller/mobile/AppAiCamController.java +++ b/src/main/java/com/ycwl/basic/controller/mobile/AppAiCamController.java @@ -1,8 +1,11 @@ package com.ycwl.basic.controller.mobile; +import com.ycwl.basic.model.jwt.JwtInfo; +import com.ycwl.basic.model.mobile.face.FaceRecognizeResp; import com.ycwl.basic.model.mobile.goods.GoodsDetailVO; import com.ycwl.basic.service.mobile.AppAiCamService; import com.ycwl.basic.utils.ApiResponse; +import com.ycwl.basic.utils.JwtTokenUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; @@ -58,4 +61,21 @@ public class AppAiCamController { return ApiResponse.fail("添加关联失败"); } } + + /** + * 使用人脸样本创建或获取Face记录 + * @param faceSampleId 人脸样本ID + * @return 人脸识别响应 + */ + @GetMapping("/useSample/{faceSampleId}") + public ApiResponse useSample(@PathVariable Long faceSampleId) { + try { + JwtInfo worker = JwtTokenUtil.getWorker(); + FaceRecognizeResp resp = appAiCamService.useSample(worker.getUserId(), faceSampleId); + return ApiResponse.success(resp); + } catch (Exception e) { + log.error("使用人脸样本失败: faceSampleId={}", faceSampleId, e); + return ApiResponse.fail("使用人脸样本失败: " + e.getMessage()); + } + } } diff --git a/src/main/java/com/ycwl/basic/controller/printer/PrinterTvController.java b/src/main/java/com/ycwl/basic/controller/printer/PrinterTvController.java index 6245b541..ff61bae9 100644 --- a/src/main/java/com/ycwl/basic/controller/printer/PrinterTvController.java +++ b/src/main/java/com/ycwl/basic/controller/printer/PrinterTvController.java @@ -4,6 +4,7 @@ package com.ycwl.basic.controller.printer; import cn.hutool.core.date.DateUtil; import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter; import com.ycwl.basic.mapper.FaceMapper; +import com.ycwl.basic.model.pc.device.entity.DeviceEntity; 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; @@ -99,14 +100,20 @@ public class PrinterTvController { @GetMapping("/{sampleId}/qrcode") public void getQrcode(@PathVariable("sampleId") Long sampleId, HttpServletResponse response) throws Exception { File qrcode = new File("qrcode_"+sampleId+".jpg"); + FaceSampleEntity faceSample = faceRepository.getFaceSample(sampleId); + if (faceSample == null) { + response.setStatus(404); + return; + } + String targetPath = "pages/printer/from_sample"; + DeviceV2DTO device = deviceRepository.getDeviceBasic(faceSample.getDeviceId()); + if (device.getType().equals("AI_CAM")) { + // AI_CAM,需要修改path + targetPath = "pages/ai-cam/from_sample"; + } try { - FaceSampleEntity faceSample = faceRepository.getFaceSample(sampleId); - if (faceSample == null) { - response.setStatus(404); - return; - } MpConfigEntity scenicMpConfig = scenicRepository.getScenicMpConfig(faceSample.getScenicId()); - WxMpUtil.generateUnlimitedWXAQRCode(scenicMpConfig.getAppId(), scenicMpConfig.getAppSecret(), "pages/printer/from_sample", sampleId.toString(), qrcode); + WxMpUtil.generateUnlimitedWXAQRCode(scenicMpConfig.getAppId(), scenicMpConfig.getAppSecret(), targetPath, sampleId.toString(), qrcode); // 设置响应头 response.setContentType("image/jpeg"); diff --git a/src/main/java/com/ycwl/basic/service/mobile/AppAiCamService.java b/src/main/java/com/ycwl/basic/service/mobile/AppAiCamService.java index 4aa8abde..e43f4232 100644 --- a/src/main/java/com/ycwl/basic/service/mobile/AppAiCamService.java +++ b/src/main/java/com/ycwl/basic/service/mobile/AppAiCamService.java @@ -1,5 +1,6 @@ package com.ycwl.basic.service.mobile; +import com.ycwl.basic.model.mobile.face.FaceRecognizeResp; import com.ycwl.basic.model.mobile.goods.GoodsDetailVO; import java.util.List; @@ -23,4 +24,12 @@ public interface AppAiCamService { * @return 添加成功的数量 */ int addMemberSourceRelations(Long faceId, List sourceIds); + + /** + * 使用人脸样本创建或获取Face记录 + * @param userId 用户ID + * @param faceSampleId 人脸样本ID + * @return 人脸识别响应 + */ + FaceRecognizeResp useSample(Long userId, Long faceSampleId); } 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 index 224233aa..fcfe8f4c 100644 --- a/src/main/java/com/ycwl/basic/service/mobile/impl/AppAiCamServiceImpl.java +++ b/src/main/java/com/ycwl/basic/service/mobile/impl/AppAiCamServiceImpl.java @@ -1,21 +1,36 @@ package com.ycwl.basic.service.mobile.impl; +import com.ycwl.basic.exception.BaseException; +import com.ycwl.basic.facebody.FaceBodyFactory; +import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter; 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.integration.common.manager.ScenicConfigManager; import com.ycwl.basic.mapper.FaceDetectLogAiCamMapper; import com.ycwl.basic.mapper.FaceMapper; +import com.ycwl.basic.mapper.FaceSampleMapper; import com.ycwl.basic.mapper.SourceMapper; +import com.ycwl.basic.model.mobile.face.FaceRecognizeResp; import com.ycwl.basic.model.mobile.goods.GoodsDetailVO; import com.ycwl.basic.model.pc.face.entity.FaceEntity; +import com.ycwl.basic.model.pc.face.resp.FaceRespVO; import com.ycwl.basic.model.pc.faceDetectLog.entity.FaceDetectLogAiCamEntity; +import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity; +import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity; 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.repository.ScenicRepository; import com.ycwl.basic.service.mobile.AppAiCamService; +import com.ycwl.basic.service.pc.FaceDetectLogAiCamService; +import com.ycwl.basic.service.pc.FaceService; +import com.ycwl.basic.service.pc.ScenicService; import com.ycwl.basic.utils.JacksonUtil; +import com.ycwl.basic.utils.SnowFlakeUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; import java.util.*; @@ -33,10 +48,14 @@ public class AppAiCamServiceImpl implements AppAiCamService { private final SourceMapper sourceMapper; private final FaceMapper faceMapper; private final DeviceRepository deviceRepository; + private final FaceSampleMapper faceSampleMapper; 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; + private final FaceService faceService; + private final FaceDetectLogAiCamService faceDetectLogAiCamService; + private final ScenicService scenicService; @Override public List getAiCamGoodsByFaceId(Long faceId) { @@ -272,4 +291,93 @@ public class AppAiCamServiceImpl implements AppAiCamService { return inserted; } + + @Override + public FaceRecognizeResp useSample(Long userId, Long faceSampleId) { + // 1. 查询 faceSample 获取其 URL + FaceSampleEntity faceSample = faceSampleMapper.getEntity(faceSampleId); + if (faceSample == null) { + throw new BaseException("人脸样本不存在"); + } + + String faceUrl = faceSample.getFaceUrl(); + if (StringUtils.isBlank(faceUrl)) { + throw new BaseException("人脸样本URL为空"); + } + + Long scenicId = faceSample.getScenicId(); + + // 2. 检查face数据库中有没有同用户、同URL的face记录 + FaceEntity existingFace = null; + Long faceId = null; + + // 查询该用户在该景区的所有人脸记录 + List userFaces = faceMapper.listByScenicAndUserId(scenicId, userId); + + // 查找是否存在相同URL的记录 + for (FaceRespVO faceResp : userFaces) { + if (faceUrl.equals(faceResp.getFaceUrl())) { + existingFace = faceMapper.get(faceResp.getId()); + faceId = existingFace.getId(); + break; + } + } + + // 3. 如果不存在,则新建一个face记录 + if (existingFace == null) { + faceId = SnowFlakeUtil.getLongId(); + FaceEntity newFace = new FaceEntity(); + newFace.setId(faceId); + newFace.setCreateAt(new Date()); + newFace.setScenicId(scenicId); + newFace.setMemberId(userId); + newFace.setFaceUrl(faceUrl); + faceMapper.add(newFace); + + log.info("创建新的face记录, userId: {}, faceSampleId: {}, faceId: {}, faceUrl: {}", + userId, faceSampleId, faceId, faceUrl); + } else { + log.info("使用已存在的face记录, userId: {}, faceSampleId: {}, faceId: {}, faceUrl: {}", + userId, faceSampleId, faceId, faceUrl); + } + + // 4. 查询对应的 type=3 的 source 记录并自动添加关联 + SourceEntity sourceEntity = sourceMapper.getBySampleIdAndType(faceSampleId, AI_CAM_SOURCE_TYPE); + if (sourceEntity != null && existingFace == null) { + // 检查是否已存在该source的关联 + List existingGoods = getAiCamGoodsByFaceId(faceId); + boolean alreadyExists = existingGoods.stream() + .anyMatch(item -> Objects.equals(item.getGoodsId(), sourceEntity.getId())); + + if (!alreadyExists) { + // 添加关联 + MemberSourceEntity relation = new MemberSourceEntity(); + relation.setMemberId(userId); + relation.setScenicId(scenicId); + relation.setFaceId(faceId); + relation.setSourceId(sourceEntity.getId()); + relation.setType(AI_CAM_SOURCE_TYPE); + relation.setIsBuy(0); + relation.setIsFree(0); + sourceMapper.addRelations(Collections.singletonList(relation)); + log.info("自动添加AI相机照片关联: userId={}, faceId={}, sourceId={}", + userId, faceId, sourceEntity.getId()); + } + } + + // 5. 返回结果 + FaceRecognizeResp resp = new FaceRecognizeResp(); + resp.setUrl(faceUrl); + resp.setFaceId(faceId); + resp.setScenicId(scenicId); + IFaceBodyAdapter faceBodyAdapter = scenicService.getScenicFaceBodyAdapter(scenicId); + try { + faceService.matchFaceId(faceId); + faceDetectLogAiCamService.searchAndLog(scenicId, faceId, faceUrl, faceBodyAdapter); + } catch (Exception e) { + // 人脸匹配失败不可以阻止正常流程 + log.error("人脸匹配失败", e); + } + return resp; + } }