You've already forked FrameTour-BE
feat(ai-cam): 实现AI相机商品识别与会员关联功能
- 新增AppAiCamController控制器,提供获取AI相机识别商品和添加会员素材关联接口 - 实现AppAiCamService服务,完成从人脸识别日志到商品详情的转换逻辑 - 扩展FaceDetectLogAiCamMapper,支持根据faceId查询识别记录 - 扩展SourceMapper,新增根据faceSampleIds和type查询source列表的方法 - 添加设备配置管理,支持按设备设置识别分数阈值和照片数量限制 - 实现人脸识别结果解析,提取匹配度高的faceSampleId并去重处理 - 完成商品详情VO转换,包含素材URL、视频URL及购买状态等信息 - 支持批量添加会员与素材的关联关系,确保数据一致性校验
This commit is contained in:
@@ -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<List<GoodsDetailVO>> getAiCamGoods(@PathVariable Long faceId) {
|
||||||
|
try {
|
||||||
|
List<GoodsDetailVO> 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<String> addMemberSourceRelations(
|
||||||
|
@PathVariable Long faceId,
|
||||||
|
@RequestBody List<Long> 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("添加关联失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,10 +3,19 @@ package com.ycwl.basic.mapper;
|
|||||||
import com.ycwl.basic.model.pc.faceDetectLog.entity.FaceDetectLogAiCamEntity;
|
import com.ycwl.basic.model.pc.faceDetectLog.entity.FaceDetectLogAiCamEntity;
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AI相机人脸识别日志Mapper
|
* AI相机人脸识别日志Mapper
|
||||||
*/
|
*/
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface FaceDetectLogAiCamMapper {
|
public interface FaceDetectLogAiCamMapper {
|
||||||
int add(FaceDetectLogAiCamEntity entity);
|
int add(FaceDetectLogAiCamEntity entity);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据faceId查询所有识别记录
|
||||||
|
* @param faceId 人脸ID
|
||||||
|
* @return 识别记录列表
|
||||||
|
*/
|
||||||
|
List<FaceDetectLogAiCamEntity> listByFaceId(Long faceId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,4 +148,12 @@ public interface SourceMapper {
|
|||||||
* @return source实体
|
* @return source实体
|
||||||
*/
|
*/
|
||||||
SourceEntity getSourceByFaceAndDeviceId(Long faceId, Long deviceId, Integer type, String sortStrategy);
|
SourceEntity getSourceByFaceAndDeviceId(Long faceId, Long deviceId, Integer type, String sortStrategy);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据faceSampleId列表和type查询source列表
|
||||||
|
* @param faceSampleIds faceSampleId列表
|
||||||
|
* @param type 素材类型
|
||||||
|
* @return source实体列表
|
||||||
|
*/
|
||||||
|
List<SourceEntity> listByFaceSampleIdsAndType(List<Long> faceSampleIds, Integer type);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<GoodsDetailVO> getAiCamGoodsByFaceId(Long faceId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量添加会员与source的关联关系
|
||||||
|
* @param faceId 人脸ID
|
||||||
|
* @param sourceIds source ID列表
|
||||||
|
* @return 添加成功的数量
|
||||||
|
*/
|
||||||
|
int addMemberSourceRelations(Long faceId, List<Long> sourceIds);
|
||||||
|
}
|
||||||
@@ -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<GoodsDetailVO> getAiCamGoodsByFaceId(Long faceId) {
|
||||||
|
// 1. 查询该faceId的所有识别记录
|
||||||
|
List<FaceDetectLogAiCamEntity> detectLogs = faceDetectLogAiCamMapper.listByFaceId(faceId);
|
||||||
|
if (detectLogs == null || detectLogs.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 按设备分组并根据设备配置过滤faceSampleId
|
||||||
|
Map<Long, List<Long>> deviceFaceSampleMap = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
// 按设备分组识别记录
|
||||||
|
Map<Long, List<FaceDetectLogAiCamEntity>> deviceLogsMap = detectLogs.stream()
|
||||||
|
.collect(Collectors.groupingBy(FaceDetectLogAiCamEntity::getDeviceId));
|
||||||
|
|
||||||
|
// 遍历每个设备的识别记录
|
||||||
|
for (Map.Entry<Long, List<FaceDetectLogAiCamEntity>> entry : deviceLogsMap.entrySet()) {
|
||||||
|
Long deviceId = entry.getKey();
|
||||||
|
List<FaceDetectLogAiCamEntity> 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<Long> 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<Long> faceSampleIds = new HashSet<>();
|
||||||
|
for (List<Long> 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<SourceEntity> 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<Long> 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<MemberSourceEntity> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,7 +42,7 @@ public class FaceDetectLogAiCamServiceImpl implements FaceDetectLogAiCamService
|
|||||||
// 调用适配器搜索人脸
|
// 调用适配器搜索人脸
|
||||||
resp = adapter.searchFace(dbName, faceUrl);
|
resp = adapter.searchFace(dbName, faceUrl);
|
||||||
} catch (Exception e) {
|
} 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);
|
faceDetectLogAiCamMapper.add(logEntity);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("保存AI相机人脸识别日志失败: faceSampleId={}", faceId, e);
|
log.error("保存AI相机人脸识别日志失败: faceId={}", faceId, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return resp;
|
return resp;
|
||||||
|
|||||||
@@ -2,7 +2,16 @@
|
|||||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
<mapper namespace="com.ycwl.basic.mapper.FaceDetectLogAiCamMapper">
|
<mapper namespace="com.ycwl.basic.mapper.FaceDetectLogAiCamMapper">
|
||||||
<insert id="add" useGeneratedKeys="true" keyProperty="id">
|
<insert id="add" useGeneratedKeys="true" keyProperty="id">
|
||||||
insert into face_detect_log_ai_cam(scenic_id, device_id, face_sample_id, db_name, face_url, score, match_raw_result, create_time)
|
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}, #{faceSampleId}, #{dbName}, #{faceUrl}, #{score}, #{matchRawResult}, #{createTime})
|
values (#{scenicId}, #{deviceId}, #{faceId}, #{dbName}, #{faceUrl}, #{score}, #{matchRawResult}, #{createTime})
|
||||||
</insert>
|
</insert>
|
||||||
|
|
||||||
|
<select id="listByFaceId" resultType="com.ycwl.basic.model.pc.faceDetectLog.entity.FaceDetectLogAiCamEntity">
|
||||||
|
select id, scenic_id as scenicId, device_id as deviceId, face_id as faceId,
|
||||||
|
db_name as dbName, face_url as faceUrl, score, match_raw_result as matchRawResult,
|
||||||
|
create_time as createTime
|
||||||
|
from face_detect_log_ai_cam
|
||||||
|
where face_id = #{faceId}
|
||||||
|
order by create_time desc
|
||||||
|
</select>
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
@@ -486,4 +486,14 @@
|
|||||||
</choose>
|
</choose>
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="listByFaceSampleIdsAndType" resultType="com.ycwl.basic.model.pc.source.entity.SourceEntity">
|
||||||
|
SELECT * FROM source
|
||||||
|
WHERE face_sample_id IN
|
||||||
|
<foreach collection="faceSampleIds" item="item" open="(" separator="," close=")">
|
||||||
|
#{item}
|
||||||
|
</foreach>
|
||||||
|
AND `type` = #{type}
|
||||||
|
ORDER BY create_time DESC
|
||||||
|
</select>
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
Reference in New Issue
Block a user