feat(face): 增加人脸状态查询功能

- 新增 FaceStatusResp 类用于人脸状态响应- 在 AppFaceController 中添加人脸状态查询相关接口
- 在 FaceService 接口中定义相关方法- 实现 FaceServiceImpl 中的人脸状态查询逻辑
- 优化 ContentPageVO 类,增加 group 字段
This commit is contained in:
2025-09-15 10:13:41 +08:00
parent bf672a8af7
commit c5f7003077
6 changed files with 198 additions and 3 deletions

View File

@@ -2,8 +2,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.face.FaceStatusResp;
import com.ycwl.basic.model.mobile.scenic.content.ContentPageVO;
import com.ycwl.basic.model.pc.face.resp.FaceRespVO;
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
import com.ycwl.basic.model.pc.faceSample.resp.FaceSampleRespVO;
import com.ycwl.basic.service.pc.FaceService;
import com.ycwl.basic.utils.ApiResponse;
import com.ycwl.basic.utils.JwtTokenUtil;
@@ -20,8 +23,7 @@ import java.util.List;
@RestController
@RequestMapping("/api/mobile/face/v1")
// 用户人脸相关接口
public class
AppFaceController {
public class AppFaceController {
@Autowired
private FaceService faceService;
@@ -85,4 +87,25 @@ AppFaceController {
faceService.bindFace(faceId, userId);
return ApiResponse.success("OK");
}
@GetMapping("/{faceId}/status")
public ApiResponse<FaceStatusResp> status(@PathVariable Long faceId) {
return ApiResponse.success(faceService.getFaceStatus(faceId));
}
@GetMapping("/{faceId}/extraCheck")
public ApiResponse<Boolean> hasExtraCheck(@PathVariable Long faceId) {
return ApiResponse.success(faceService.checkHasExtraCheck(faceId));
}
@GetMapping("/{faceId}/queryOtherFace")
public ApiResponse<List<FaceSampleEntity>> queryOtherFace(@PathVariable Long faceId) {
return ApiResponse.success(faceService.getLowMatchedFaceSamples(faceId));
}
@PostMapping("/{faceId}/queryOtherFace")
public ApiResponse<String> queryOtherFace(@PathVariable Long faceId, @RequestBody List<Long> faceIds) {
faceService.matchCustomFaceId(faceId, faceIds);
return ApiResponse.success("OK");
}
}

View File

@@ -0,0 +1,34 @@
package com.ycwl.basic.model.mobile.face;
import lombok.Data;
/**
* 人脸状态响应对象
*
* @author Claude Code
*/
@Data
public class FaceStatusResp {
/**
* 人脸ID
*/
private Long faceId;
private String faceUrl;
/**
* 识别次数,0表示未识别过
*/
private Long recognitionCount;
/**
* 是否触发过低阈值检测
*/
private Boolean hasLowThreshold;
private String displayText;
private boolean step1Status;
// private String step1Text;
private boolean step2Status;
// private String step2Text;
private boolean step3Status;
// private String step3Text;
}

View File

@@ -13,6 +13,7 @@ import java.math.BigDecimal;
public class ContentPageVO {
// 内容名称
private String name;
private String group;
// 景区id
private Long scenicId;
// 景区名称

View File

@@ -2,10 +2,13 @@ package com.ycwl.basic.service.pc;
import com.github.pagehelper.PageInfo;
import com.ycwl.basic.model.mobile.face.FaceRecognizeResp;
import com.ycwl.basic.model.mobile.face.FaceStatusResp;
import com.ycwl.basic.model.mobile.scenic.content.ContentPageVO;
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
import com.ycwl.basic.model.pc.face.req.FaceReqQuery;
import com.ycwl.basic.model.pc.face.resp.FaceRespVO;
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
import com.ycwl.basic.model.pc.faceSample.resp.FaceSampleRespVO;
import com.ycwl.basic.model.task.resp.SearchFaceRespVo;
import com.ycwl.basic.utils.ApiResponse;
import org.springframework.web.multipart.MultipartFile;
@@ -40,4 +43,12 @@ public interface FaceService {
void bindFace(Long faceId, Long memberId);
String bindWxaCode(Long faceId);
FaceStatusResp getFaceStatus(Long faceId);
Boolean checkHasExtraCheck(Long faceId);
List<FaceSampleEntity> getLowMatchedFaceSamples(Long faceId);
void matchCustomFaceId(Long faceId, List<Long> faceSampleIds);
}

View File

@@ -9,7 +9,9 @@ import com.ycwl.basic.constant.BaseContextHandler;
import com.ycwl.basic.enums.StatisticEnum;
import com.ycwl.basic.exception.BaseException;
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
import com.ycwl.basic.facebody.entity.SearchFaceResultItem;
import com.ycwl.basic.integration.common.manager.DeviceConfigManager;
import com.ycwl.basic.mapper.FaceSampleMapper;
import com.ycwl.basic.mapper.SourceMapper;
import com.ycwl.basic.mapper.StatisticsMapper;
import com.ycwl.basic.mapper.FaceMapper;
@@ -17,6 +19,7 @@ import com.ycwl.basic.mapper.TemplateMapper;
import com.ycwl.basic.mapper.VideoMapper;
import com.ycwl.basic.mapper.OrderMapper;
import com.ycwl.basic.model.mobile.face.FaceRecognizeResp;
import com.ycwl.basic.model.mobile.face.FaceStatusResp;
import com.ycwl.basic.model.mobile.order.IsBuyRespVO;
import com.ycwl.basic.model.mobile.scenic.content.ContentPageVO;
import com.ycwl.basic.model.mobile.statistic.req.StatisticsRecordAddReq;
@@ -25,6 +28,7 @@ import com.ycwl.basic.model.pc.face.entity.FaceEntity;
import com.ycwl.basic.model.pc.face.req.FaceReqQuery;
import com.ycwl.basic.model.pc.face.resp.FaceRespVO;
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
import com.ycwl.basic.model.pc.faceSample.resp.FaceSampleRespVO;
import com.ycwl.basic.model.pc.mp.MpConfigEntity;
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
@@ -52,6 +56,7 @@ import com.ycwl.basic.storage.utils.StorageUtil;
import com.ycwl.basic.task.VideoPieceGetter;
import com.ycwl.basic.utils.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.util.Strings;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
@@ -116,6 +121,8 @@ public class FaceServiceImpl implements FaceService {
private OrderMapper orderMapper;
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private FaceSampleMapper faceSampleMapper;
@Override
public ApiResponse<PageInfo<FaceRespVO>> pageQuery(FaceReqQuery faceReqQuery) {
@@ -698,6 +705,8 @@ public class FaceServiceImpl implements FaceService {
sourceImageContent.setContentType(2);
sourceVideoContent.setLockType(-1);
sourceImageContent.setLockType(-1);
sourceVideoContent.setGroup("直出原片");
sourceImageContent.setGroup("直出原片");
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(faceRespVO.getScenicId());
if (!Boolean.TRUE.equals(scenicConfig.getBoolean("disable_source_image"))) {
IsBuyRespVO isBuyRespVO = orderBiz.isBuy(userId, faceRespVO.getScenicId(), 2, faceId);
@@ -816,6 +825,123 @@ public class FaceServiceImpl implements FaceService {
}
}
@Override
public FaceStatusResp getFaceStatus(Long faceId) {
if (faceId == null) {
return null;
}
FaceStatusResp statusResp = new FaceStatusResp();
statusResp.setFaceId(faceId);
FaceEntity face = faceRepository.getFace(faceId);
if (face == null) {
statusResp.setStep1Status(false);
statusResp.setDisplayText("请重新录入人脸");
return statusResp;
}
statusResp.setStep1Status(true);
statusResp.setFaceUrl(face.getFaceUrl());
// 查询识别次数
String countKey = FACE_RECOGNITION_COUNT_PFX + 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);
// 查询是否触发过低阈值检测
String lowThresholdKey = FACE_LOW_THRESHOLD_PFX + faceId;
Boolean hasLowThreshold = redisTemplate.hasKey(lowThresholdKey);
statusResp.setHasLowThreshold(hasLowThreshold);
log.debug("查询人脸状态:faceId={}, recognitionCount={}, hasLowThreshold={}",
faceId, recognitionCount, hasLowThreshold);
SourceReqQuery sourceReqQuery = new SourceReqQuery();
sourceReqQuery.setMemberId(face.getMemberId());
sourceReqQuery.setFaceId(faceId);
sourceReqQuery.setType(2);
Integer countUser = sourceMapper.countUser(sourceReqQuery);
if (countUser != null && countUser > 0) {
statusResp.setStep2Status(true);
} else {
statusResp.setStep2Status(false);
statusResp.setDisplayText("Hey,快去智能机位打卡吧");
}
List<MemberVideoEntity> videoEntities = videoMapper.listRelationByFace(face.getMemberId(), faceId);
if (videoEntities != null && !videoEntities.isEmpty()) {
statusResp.setStep3Status(true);
statusResp.setDisplayText("帧途AI已为您渲染"+ videoEntities.size() +"个vlog");
} else {
statusResp.setStep3Status(false);
statusResp.setDisplayText("帧途AI将会为您渲染vlog,请稍后");
}
return statusResp;
}
@Override
public Boolean checkHasExtraCheck(Long faceId) {
FaceEntity face = faceRepository.getFace(faceId);
if (face == null) {
return false;
}
String countKey = FACE_RECOGNITION_COUNT_PFX + 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);
}
}
// 查询是否触发过低阈值检测
String lowThresholdKey = FACE_LOW_THRESHOLD_PFX + faceId;
Boolean hasLowThreshold = redisTemplate.hasKey(lowThresholdKey);
return true;
}
@Override
public List<FaceSampleEntity> getLowMatchedFaceSamples(Long faceId) {
FaceEntity face = faceMapper.get(faceId);
if (face == null) {
return List.of();
}
String matchResult = face.getMatchResult();
if (matchResult == null || Strings.isBlank(matchResult)) {
return List.of();
}
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(face.getScenicId());
if (scenicConfig == null) {
return List.of();
}
Float lowThreshold = scenicConfig.getFloat("face_score_low_threshold", 30.0F);
List<SearchFaceResultItem> resultItems = JacksonUtil.fromJsonToList(matchResult, SearchFaceResultItem.class);
if (resultItems == null || resultItems.isEmpty()) {
return List.of();
}
List<Long> idList = resultItems.stream().filter(item -> item.getScore() > lowThreshold/100F)
.map(SearchFaceResultItem::getExtData).limit(9).map(Long::valueOf).toList();
if (idList.isEmpty()) {
return List.of();
}
List<FaceSampleEntity> sampleEntities = faceSampleMapper.listByIds(idList);
return sampleEntities;
}
@Override
public void matchCustomFaceId(Long faceId, List<Long> faceSampleIds) {
}
/**
* 记录人脸识别次数到Redis
*

View File

@@ -114,7 +114,7 @@
order by sort
</select>
<select id="listFor" resultType="com.ycwl.basic.model.mobile.scenic.content.ContentPageVO">
select t.id templateId, t.scenic_id, t.`name`, pid, t.cover_url templateCoverUrl,
select t.id templateId, t.scenic_id, t.`group`, t.`name`, pid, t.cover_url templateCoverUrl,
0 as sourceType, sort,
t.create_time, t.price
from template t