Compare commits

...

7 Commits

Author SHA1 Message Date
fe3bda28b4 feat(ai-cam): 增强AI摄像头人脸检测逻辑
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 引入时间范围限制配置,支持按分钟设置检测窗口
- 创建DetectResult内部类,记录人脸检测的分数和时间信息
- 优化照片筛选逻辑,先按分数排序再应用时间范围过滤
- 更新设备类型名称,将"微单"改为"AI微单"
- 增强日志记录,提供更详细的调试信息
2025-12-05 21:41:30 +08:00
66775ea48b feat(ai-cam): 增强AI摄像头人脸检测逻辑
- 引入时间范围限制配置,支持按分钟设置检测窗口
- 创建DetectResult内部类,记录人脸检测的分数和时间信息
- 优化照片筛选逻辑,先按分数排序再应用时间范围过滤
- 更新设备类型名称,将"微单"改为"AI微单"
- 增强日志记录,提供更详细的调试信息
2025-12-05 20:03:54 +08:00
125fadd6c5 feat(basic): 新增AI微单类型支持
- 在SourceType枚举中新增AI_CAM类型及其判断方法
- 在ProductType枚举中新增AI_CAM_PHOTO_SET类型
- 扩展SourceMapper接口及XML实现删除指定faceId和type的关联记录功能
- 更新AppAiCamServiceImpl服务逻辑,在添加新关联前先删除旧记录
- 修改GoodsServiceImpl以识别并处理AI微单类型的商品名称前缀
- 在FaceServiceImpl中增加对AI微单内容的查询与展示逻辑
- 优化face相关素材分类展示,确保AI微单正确归类显示
2025-12-05 19:58:53 +08:00
1f4a16f0e6 feat(ai-cam): 实现AI相机商品识别与会员关联功能
- 新增AppAiCamController控制器,提供获取AI相机识别商品和添加会员素材关联接口
- 实现AppAiCamService服务,完成从人脸识别日志到商品详情的转换逻辑
- 扩展FaceDetectLogAiCamMapper,支持根据faceId查询识别记录
- 扩展SourceMapper,新增根据faceSampleIds和type查询source列表的方法
- 添加设备配置管理,支持按设备设置识别分数阈值和照片数量限制
- 实现人脸识别结果解析,提取匹配度高的faceSampleId并去重处理
- 完成商品详情VO转换,包含素材URL、视频URL及购买状态等信息
- 支持批量添加会员与素材的关联关系,确保数据一致性校验
2025-12-05 17:52:46 +08:00
e9916d6aca fix(service): 修复ZTSourceDataService中照片类型设置逻辑
- 将硬编码的照片类型值替换为从消息对象获取的动态类型值
- 确保entity.setType()方法正确反映实际的消息来源类型
- 维持原有缩略图URL和设备ID等其他属性的设置逻辑不变
2025-12-05 17:35:18 +08:00
b71452b3ed refactor(face): 替换Strings工具类引用以优化代码
- 将org.apache.logging.log4j.util.Strings替换为org.apache.commons.lang3.StringUtils
- 统一使用StringUtils处理字符串判空逻辑
- 优化线程join条件判断中的字符串比较方式
- 更新所有相关字符串工具方法调用以保持一致性
2025-12-05 17:08:16 +08:00
4a82ee6c4d feat(ai): 实现AI相机人脸识别日志记录功能
- 引入DeviceRepository以获取景区内所有AI相机设备
- 修改searchAndLog方法逻辑,遍历所有AI相机设备进行人脸搜索
- 新增searchDeviceAndLog私有方法处理单个设备的人脸识别与日志记录
- 更新FaceDetectLogAiCamService接口定义,移除deviceId参数
- 在FaceServiceImpl中调用新的日志记录服务
- 删除不再使用的DeviceConfigManager和FaceRecoveryStrategy依赖
- 调整日志记录中的字段名称及异常处理逻辑
2025-12-05 16:54:47 +08:00
18 changed files with 531 additions and 30 deletions

View File

@@ -220,6 +220,7 @@ public class OrderBiz {
break;
case 1: // 视频原素材
case 2: // 照片原素材
case 13: // AI微单
sourceRepository.setUserIsBuyItem(order.getMemberId(), item.getGoodsType(), item.getGoodsId(), order.getId());
break;
case 3:

View File

@@ -16,7 +16,12 @@ public enum SourceType {
/**
* 图片类型
*/
IMAGE(2, "图片");
IMAGE(2, "图片"),
/**
* AI微单类型
*/
AI_CAM(3, "AI微单");
private final int code;
private final String description;
@@ -68,4 +73,14 @@ public enum SourceType {
public static boolean isImage(Integer code) {
return code != null && code == IMAGE.code;
}
/**
* 判断给定的代码是否为AI微单类型
*
* @param code 类型代码
* @return true-是AI微单,false-不是AI微单
*/
public static boolean isAiCam(Integer code) {
return code != null && code == AI_CAM.code;
}
}

View File

@@ -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("添加关联失败");
}
}
}

View File

@@ -86,7 +86,7 @@ public class AppOrderV2Controller {
TaskEntity task = videoTaskRepository.getTaskById(video.getTaskId());
request.setFaceId(task.getFaceId());
}
case RECORDING_SET, PHOTO_SET -> request.setFaceId(Long.valueOf(productItem.getProductId()));
case RECORDING_SET, PHOTO_SET, AI_CAM_PHOTO_SET -> request.setFaceId(Long.valueOf(productItem.getProductId()));
}
}
@@ -119,6 +119,14 @@ public class AppOrderV2Controller {
Integer count = sourceMapper.countUser(sourceReqQuery);
product.setQuantity(count);
break;
case AI_CAM_PHOTO_SET:
SourceReqQuery aiPhotoSetReqQuery = new SourceReqQuery();
aiPhotoSetReqQuery.setMemberId(currentUserId);
aiPhotoSetReqQuery.setType(13);
aiPhotoSetReqQuery.setFaceId(face.getId());
Integer _count = sourceMapper.countUser(aiPhotoSetReqQuery);
product.setQuantity(_count);
break;
default:
log.warn("未知的商品类型,跳过重复购买检查: productType={}", product.getProductType());
break;

View File

@@ -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<FaceDetectLogAiCamEntity> listByFaceId(Long faceId);
}

View File

@@ -148,4 +148,20 @@ 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<SourceEntity> listByFaceSampleIdsAndType(List<Long> faceSampleIds, Integer type);
/**
* 删除指定faceId和type的member_source关联记录
* @param faceId 人脸ID
* @param type 素材类型
* @return 删除的记录数
*/
int deleteRelationsByFaceIdAndType(Long faceId, Integer type);
}

View File

@@ -17,6 +17,7 @@ public enum ProductType {
// 照片类
PHOTO("PHOTO", "照片", ProductCategory.PHOTO),
PHOTO_SET("PHOTO_SET", "照片集", ProductCategory.PHOTO),
AI_CAM_PHOTO_SET("AI_CAM_PHOTO_SET", "照片集", ProductCategory.PHOTO),
PHOTO_LOG("PHOTO_LOG", "pLog图", ProductCategory.PHOTO),
// 视频类(素材视频)

View File

@@ -41,6 +41,9 @@ public class SourceRepository {
}
public void setUserIsBuyItem(Long memberId, int type, Long faceId, Long orderId) {
if (type == 13) {
type = 3; // compact
}
MemberSourceEntity memberSource = new MemberSourceEntity();
memberSource.setMemberId(memberId);
memberSource.setFaceId(faceId);

View File

@@ -114,7 +114,7 @@ public class ZTSourceDataService {
entity.setDeviceId(message.getDeviceId());
entity.setUrl(message.getSourceUrl()); // 使用sourceUrl,不使用缩略图
entity.setThumbUrl(message.getThumbnailUrl()); // 设置缩略图URL
entity.setType(2); // 照片类型
entity.setType(message.getSourceType()); // 照片类型
// 人脸样本ID处理
entity.setFaceSampleId(message.getFaceSampleId());

View File

@@ -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);
}

View File

@@ -0,0 +1,275 @@
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);
}
}
// 获取该设备的时间范围限制(分钟)
Integer timeRangeMin = null;
if (configManager != null) {
Integer range = configManager.getInteger("ai_cam_time_range_min");
if (range != null && range > 0) {
timeRangeMin = range;
log.debug("设备{}使用配置的时间范围: {}分钟", deviceId, timeRangeMin);
}
}
// 收集该设备符合阈值的faceSampleId,同时记录分数和时间信息用于后续过滤
class DetectResult {
Long faceSampleId;
Float score;
Date detectTime;
DetectResult(Long faceSampleId, Float score, Date detectTime) {
this.faceSampleId = faceSampleId;
this.score = score;
this.detectTime = detectTime;
}
}
List<DetectResult> detectResults = 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());
detectResults.add(new DetectResult(
faceSampleId,
item.getScore(),
detectLog.getCreateTime()
));
} catch (NumberFormatException e) {
log.warn("解析faceSampleId失败: extData={}", item.getExtData());
}
}
}
}
} catch (Exception e) {
log.error("解析matchRawResult失败: logId={}", detectLog.getId(), e);
}
}
// 应用照片数量限制(保留前N个)
if (detectResults.size() > photoLimit) {
log.debug("设备{}的照片数量{}超过限制{},截取前{}张",
deviceId, detectResults.size(), photoLimit, photoLimit);
detectResults = detectResults.subList(0, photoLimit);
}
// 应用时间范围限制
List<Long> deviceFaceSampleIds;
if (timeRangeMin != null && !detectResults.isEmpty()) {
// 找到分数最高的照片
DetectResult highestScoreResult = detectResults.stream()
.max(Comparator.comparing(r -> r.score))
.orElse(null);
if (highestScoreResult != null && highestScoreResult.detectTime != null) {
Date baseTime = highestScoreResult.detectTime;
long halfRangeMillis = (long) timeRangeMin * 60 * 1000 / 2;
Date startTime = new Date(baseTime.getTime() - halfRangeMillis);
Date endTime = new Date(baseTime.getTime() + halfRangeMillis);
// 过滤出时间范围内的照片
List<DetectResult> filteredResults = detectResults.stream()
.filter(r -> r.detectTime != null
&& !r.detectTime.before(startTime)
&& !r.detectTime.after(endTime))
.collect(Collectors.toList());
log.debug("设备{}应用时间范围{}分钟过滤: {}张 -> {}张 (基准时间: {})",
deviceId, timeRangeMin, detectResults.size(), filteredResults.size(), baseTime);
deviceFaceSampleIds = filteredResults.stream()
.map(r -> r.faceSampleId)
.collect(Collectors.toList());
} else {
// 没有时间信息,不过滤
deviceFaceSampleIds = detectResults.stream()
.map(r -> r.faceSampleId)
.collect(Collectors.toList());
}
} else {
// 不限制时间范围
deviceFaceSampleIds = detectResults.stream()
.map(r -> r.faceSampleId)
.collect(Collectors.toList());
}
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);
}
// 删除该faceId对应的旧的type=3关系
int deleted = sourceMapper.deleteRelationsByFaceIdAndType(faceId, AI_CAM_SOURCE_TYPE);
log.info("删除faceId={}的旧AI相机关联记录: {}条", faceId, deleted);
// 构建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());
// 批量插入
int inserted = sourceMapper.addRelations(relations);
log.info("为faceId={}添加新AI相机关联记录: {}条", faceId, inserted);
return inserted;
}
}

View File

@@ -127,6 +127,8 @@ public class GoodsServiceImpl implements GoodsService {
goodsNamePrefix = "录像";
} else if (sourceType == 2) {
goodsNamePrefix = "图片";
} else if (sourceType == 3) {
goodsNamePrefix = "AI微单";
} else {
goodsNamePrefix = "其他类型";
}
@@ -573,7 +575,7 @@ public class GoodsServiceImpl implements GoodsService {
if (query.getGoodsId() != null) {
list = list.stream().filter(source -> source.getId().equals(query.getGoodsId())).toList();
}
if (!Integer.valueOf(2).equals(query.getSourceType())) {
if (Integer.valueOf(1).equals(query.getSourceType())) {
return list.stream().map(source -> {
GoodsUrlVO goodsUrlVO = new GoodsUrlVO();
goodsUrlVO.setGoodsType(source.getType());
@@ -592,7 +594,7 @@ public class GoodsServiceImpl implements GoodsService {
}
return true;
}).count();
IsBuyRespVO isBuy = orderBiz.isBuy(face.getScenicId(), face.getMemberId(), face.getId(), query.getSourceType(), face.getId());
IsBuyRespVO isBuy = orderBiz.isBuy(face.getScenicId(), face.getMemberId(), face.getId(), sourceType == 3 ? 13 : sourceType, face.getId());
if (count > 0) {
if (!isBuy.isBuy()) {
return Collections.emptyList();
@@ -829,6 +831,9 @@ public class GoodsServiceImpl implements GoodsService {
} else if (type == 2) {
goodsPageVO.setGoodsName("照片集");
goodsPageVO.setTemplateCoverUrl(scenicConfig.getString("photo_cover_url"));
} else if (type == 3) {
goodsPageVO.setGoodsName("AI微单");
goodsPageVO.setTemplateCoverUrl(scenicConfig.getString("ai_camera_cover_url"));
} else {
goodsPageVO.setGoodsName("未知商品");
}

View File

@@ -11,11 +11,10 @@ public interface FaceDetectLogAiCamService {
/**
* 搜索人脸库并保存日志
* @param scenicId 景区ID
* @param deviceId 设备ID
* @param faceSampleId 人脸样本ID
* @param faceId 人脸样本ID
* @param faceUrl 人脸URL
* @param adapter 人脸适配器
* @return 搜索结果
*/
SearchFaceResp searchAndLog(Long scenicId, Long deviceId, Long faceSampleId, String faceUrl, IFaceBodyAdapter adapter);
void searchAndLog(Long scenicId, Long faceId, String faceUrl, IFaceBodyAdapter adapter);
}

View File

@@ -2,8 +2,10 @@ package com.ycwl.basic.service.pc.impl;
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
import com.ycwl.basic.facebody.entity.SearchFaceResp;
import com.ycwl.basic.integration.device.dto.device.DeviceV2DTO;
import com.ycwl.basic.mapper.FaceDetectLogAiCamMapper;
import com.ycwl.basic.model.pc.faceDetectLog.entity.FaceDetectLogAiCamEntity;
import com.ycwl.basic.repository.DeviceRepository;
import com.ycwl.basic.service.pc.FaceDetectLogAiCamService;
import com.ycwl.basic.utils.JacksonUtil;
import lombok.RequiredArgsConstructor;
@@ -11,6 +13,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
@Slf4j
@Service
@@ -18,9 +21,20 @@ import java.util.Date;
public class FaceDetectLogAiCamServiceImpl implements FaceDetectLogAiCamService {
private final FaceDetectLogAiCamMapper faceDetectLogAiCamMapper;
private final DeviceRepository deviceRepository;
@Override
public SearchFaceResp searchAndLog(Long scenicId, Long deviceId, Long faceSampleId, String faceUrl, IFaceBodyAdapter adapter) {
public void searchAndLog(Long scenicId, Long faceId, String faceUrl, IFaceBodyAdapter adapter) {
List<DeviceV2DTO> devices = deviceRepository.getAllDeviceByScenicId(scenicId);
for (DeviceV2DTO device : devices) {
if (!device.getType().equals("AI_CAM")) {
continue;
}
searchDeviceAndLog(scenicId, device.getId(), faceId, faceUrl, adapter);
}
}
private SearchFaceResp searchDeviceAndLog(Long scenicId, Long deviceId, Long faceId, String faceUrl, IFaceBodyAdapter adapter) {
String dbName = "AiCam" + deviceId;
SearchFaceResp resp = null;
@@ -28,7 +42,7 @@ public class FaceDetectLogAiCamServiceImpl implements FaceDetectLogAiCamService
// 调用适配器搜索人脸
resp = adapter.searchFace(dbName, faceUrl);
} catch (Exception e) {
log.error("AI相机人脸搜索异常: scenicId={}, deviceId={}, faceSampleId={}", scenicId, deviceId, faceSampleId, e);
log.error("AI相机人脸搜索异常: scenicId={}, deviceId={}, faceId={}", scenicId, deviceId, faceId, e);
// 发生异常时记录空结果或错误信息,视业务需求而定。这里暂不中断流程,继续记录日志
}
@@ -37,7 +51,7 @@ public class FaceDetectLogAiCamServiceImpl implements FaceDetectLogAiCamService
FaceDetectLogAiCamEntity logEntity = new FaceDetectLogAiCamEntity();
logEntity.setScenicId(scenicId);
logEntity.setDeviceId(deviceId);
logEntity.setFaceId(faceSampleId);
logEntity.setFaceId(faceId);
logEntity.setDbName(dbName);
logEntity.setFaceUrl(faceUrl);
logEntity.setCreateTime(new Date());
@@ -57,7 +71,7 @@ public class FaceDetectLogAiCamServiceImpl implements FaceDetectLogAiCamService
faceDetectLogAiCamMapper.add(logEntity);
} catch (Exception e) {
log.error("保存AI相机人脸识别日志失败: faceSampleId={}", faceSampleId, e);
log.error("保存AI相机人脸识别日志失败: faceId={}", faceId, e);
}
return resp;

View File

@@ -9,7 +9,6 @@ import com.ycwl.basic.constant.BaseContextHandler;
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.ProjectMapper;
import com.ycwl.basic.mapper.SourceMapper;
@@ -62,6 +61,7 @@ import com.ycwl.basic.repository.TemplateRepository;
import com.ycwl.basic.repository.VideoRepository;
import com.ycwl.basic.repository.VideoTaskRepository;
import com.ycwl.basic.service.mobile.GoodsService;
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.constant.SourceType;
@@ -71,7 +71,6 @@ import com.ycwl.basic.service.pc.helper.SearchResultMerger;
import com.ycwl.basic.service.pc.helper.ScenicConfigFacade;
import com.ycwl.basic.service.pc.orchestrator.FaceMatchingOrchestrator;
import com.ycwl.basic.service.pc.processor.BuyStatusProcessor;
import com.ycwl.basic.service.pc.processor.FaceRecoveryStrategy;
import com.ycwl.basic.service.pc.processor.SourceRelationProcessor;
import com.ycwl.basic.service.pc.processor.VideoRecreationHandler;
import com.ycwl.basic.service.pc.strategy.RematchContext;
@@ -86,7 +85,8 @@ import com.ycwl.basic.storage.enums.StorageAcl;
import com.ycwl.basic.storage.utils.StorageUtil;
import com.ycwl.basic.utils.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.util.Strings;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
@@ -193,6 +193,8 @@ public class FaceServiceImpl implements FaceService {
private IPriceCalculationService iPriceCalculationService;
@Autowired
private PuzzleTemplateMapper puzzleTemplateMapper;
@Autowired
private FaceDetectLogAiCamService faceDetectLogAiCamService;
@Override
public ApiResponse<PageInfo<FaceRespVO>> pageQuery(FaceReqQuery faceReqQuery) {
@@ -312,13 +314,16 @@ public class FaceServiceImpl implements FaceService {
Long finalFaceId = newFaceId;
Thread thread = new Thread(() -> printerService.autoAddPhotosToPreferPrint(finalFaceId), "auto-add-print-" + newFaceId);
thread.start();
if (org.apache.commons.lang3.Strings.CI.equals("print", scene)) {
if (Strings.CI.equals("print", scene)) {
try {
thread.join();
} catch (InterruptedException ignore) {
}
}
if (Strings.CI.equals("aiCam", scene)) {
faceDetectLogAiCamService.searchAndLog(scenicId, newFaceId, faceUrl, faceBodyAdapter);
}
return resp;
}
@@ -531,20 +536,29 @@ public class FaceServiceImpl implements FaceService {
List<SourceRespVO> sourceList = sourceMapper.queryByRelation(sourceReqQuery);
ContentPageVO sourceVideoContent = new ContentPageVO();
ContentPageVO sourceImageContent = new ContentPageVO();
ContentPageVO sourceAiCamContent = new ContentPageVO();
sourceVideoContent.setName("录像集");
sourceImageContent.setName("照片集");
sourceAiCamContent.setName("AI微单");
sourceVideoContent.setSort(9999);
sourceImageContent.setSort(9999);
sourceAiCamContent.setSort(9999);
sourceVideoContent.setScenicId(face.getScenicId());
sourceImageContent.setScenicId(face.getScenicId());
sourceAiCamContent.setScenicId(face.getScenicId());
sourceVideoContent.setGoodsType(1);
sourceImageContent.setGoodsType(2);
sourceAiCamContent.setGoodsType(3);
sourceVideoContent.setContentType(2);
sourceImageContent.setContentType(2);
sourceAiCamContent.setContentType(2);
sourceVideoContent.setLockType(-1);
sourceImageContent.setLockType(-1);
sourceAiCamContent.setLockType(-1);
sourceVideoContent.setGroup("直出原片");
sourceImageContent.setGroup("直出原片");
sourceAiCamContent.setGroup("直出原片");
ScenicConfigManager configManager = scenicRepository.getScenicConfigManager(face.getScenicId());
if (!scenicConfigFacade.isDisableSourceImage(face.getScenicId())) {
IsBuyRespVO isBuyRespVO = orderBiz.isBuy(face.getScenicId(), userId, faceId, SourceType.IMAGE.getCode(), faceId);
sourceImageContent.setSourceType(isBuyRespVO.getGoodsType());
@@ -583,18 +597,40 @@ public class FaceServiceImpl implements FaceService {
sourceVideoContent.setFreeCount((int) freeCount);
contentList.add(sourceVideoContent);
}
// AI微单:只有存在type=3的数据时才添加
boolean hasAiCam = sourceList.stream().anyMatch(source -> source.getType() == 3);
if (hasAiCam) {
IsBuyRespVO isBuyRespVO = orderBiz.isBuy(face.getScenicId(), userId, faceId, 13, faceId);
sourceAiCamContent.setSourceType(isBuyRespVO.getGoodsType());
sourceAiCamContent.setContentId(isBuyRespVO.getGoodsId());
if (isBuyRespVO.isBuy()) {
sourceAiCamContent.setIsBuy(1);
} else {
sourceAiCamContent.setIsBuy(0);
}
// AI微单有数据才显示,所以lockType固定为-1
sourceAiCamContent.setLockType(-1);
List<MemberSourceEntity> relations = memberRelationRepository.listSourceByFaceRelation(faceId, 3);
long freeCount = relations.stream().filter(entity -> Integer.valueOf(1).equals(entity.getIsFree())).count();
sourceAiCamContent.setFreeCount((int) freeCount);
contentList.add(sourceAiCamContent);
}
sourceList.stream().collect(Collectors.groupingBy(SourceRespVO::getType)).forEach((type, list) -> {
if (type == 1) {
sourceVideoContent.setSourceType(1);
sourceVideoContent.setLockType(-1);
sourceVideoContent.setTemplateCoverUrl(list.getFirst().getUrl());
} else {
} else if (type == 2) {
sourceImageContent.setSourceType(2);
sourceImageContent.setLockType(-1);
sourceImageContent.setTemplateCoverUrl(list.getFirst().getUrl());
if (Strings.isBlank(sourceVideoContent.getTemplateCoverUrl())) {
if (StringUtils.isBlank(sourceVideoContent.getTemplateCoverUrl())) {
sourceVideoContent.setTemplateCoverUrl(list.getFirst().getUrl());
}
} else if (type == 3) {
sourceAiCamContent.setSourceType(13);
sourceAiCamContent.setLockType(-1);
sourceAiCamContent.setTemplateCoverUrl(configManager.getString("ai_camera_cover_url"));
}
});
return contentList;
@@ -803,7 +839,7 @@ public class FaceServiceImpl implements FaceService {
return List.of();
}
String matchResult = face.getMatchResult();
if (matchResult == null || Strings.isBlank(matchResult)) {
if (matchResult == null || StringUtils.isBlank(matchResult)) {
return List.of();
}
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(face.getScenicId());
@@ -1009,7 +1045,7 @@ public class FaceServiceImpl implements FaceService {
handleCustomFaceMatching(faceId, finalSampleList);
}
if (Strings.isNotBlank(req.getRemark())) {
if (StringUtils.isNotBlank(req.getRemark())) {
log.info("人脸识别人工调整备注:faceId={}, remark={}", faceId, req.getRemark());
}
}
@@ -1035,7 +1071,7 @@ public class FaceServiceImpl implements FaceService {
detail.setLastMatchedAt(face.getUpdateAt() != null ? face.getUpdateAt() : face.getCreateAt());
String matchResultJson = face.getMatchResult();
if (Strings.isBlank(matchResultJson)) {
if (StringUtils.isBlank(matchResultJson)) {
detail.setAcceptedSamples(Collections.emptyList());
detail.setFilteredSamples(Collections.emptyList());
return detail;
@@ -1155,7 +1191,7 @@ public class FaceServiceImpl implements FaceService {
}
private List<Long> parseMatchSampleIds(String matchSampleIds) {
if (Strings.isBlank(matchSampleIds)) {
if (StringUtils.isBlank(matchSampleIds)) {
return Collections.emptyList();
}
String[] segments = matchSampleIds.split(",");
@@ -1170,7 +1206,7 @@ public class FaceServiceImpl implements FaceService {
}
private Long parseLongSilently(String value) {
if (Strings.isBlank(value)) {
if (StringUtils.isBlank(value)) {
return null;
}
try {
@@ -1222,10 +1258,10 @@ public class FaceServiceImpl implements FaceService {
if (sourceEntity == null) {
return null;
}
if (!Strings.isBlank(sourceEntity.getThumbUrl())) {
if (!StringUtils.isBlank(sourceEntity.getThumbUrl())) {
return sourceEntity.getThumbUrl();
}
if (!Strings.isBlank(sourceEntity.getUrl())) {
if (!StringUtils.isBlank(sourceEntity.getUrl())) {
return sourceEntity.getUrl();
}
return null;

View File

@@ -177,6 +177,9 @@ public class OrderServiceImpl implements OrderService {
} else if (Integer.valueOf(2).equals(orderItemList.getFirst().getGoodsType())) {
item.setGoodsName("照片集");
item.setOrderType("照片集");
} else if (Integer.valueOf(3).equals(orderItemList.getFirst().getGoodsType())) {
item.setGoodsName("AI微单");
item.setOrderType("AI微单");
} else if (Integer.valueOf(0).equals(orderItemList.getFirst().getGoodsType())) {
item.setOrderType("旅行Vlog");
item.setGoodsName(orderItemList.getFirst().getGoodsName());
@@ -237,6 +240,9 @@ public class OrderServiceImpl implements OrderService {
} else if (Integer.valueOf(2).equals(orderItemList.getFirst().getGoodsType())) {
item.setGoodsName("照片集");
item.setOrderType("照片集");
} else if (Integer.valueOf(3).equals(orderItemList.getFirst().getGoodsType())) {
item.setGoodsName("AI微单");
item.setOrderType("AI微单");
} else if (Integer.valueOf(0).equals(orderItemList.getFirst().getGoodsType())) {
item.setOrderType("旅行Vlog");
item.setGoodsName(orderItemList.getFirst().getGoodsName());
@@ -940,11 +946,13 @@ public class OrderServiceImpl implements OrderService {
case PHOTO_SET -> 2;
case VLOG_VIDEO -> 0;
case RECORDING_SET -> 1;
case AI_CAM_PHOTO_SET -> 13;
default -> 0;
};
Long goodsId = switch (productItem.getProductType()) {
case PHOTO_LOG -> Long.valueOf(productItem.getProductId());
case PHOTO_SET, RECORDING_SET -> face.getId();
case AI_CAM_PHOTO_SET -> face.getId();
case VLOG_VIDEO -> {
List<MemberVideoEntity> videos = memberRelationRepository.listRelationByFaceAndTemplate(face.getId(), Long.valueOf(productItem.getProductId()));
yield videos.getFirst().getVideoId();

View File

@@ -2,7 +2,16 @@
<!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">
<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)
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})
</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>

View File

@@ -486,4 +486,19 @@
</choose>
LIMIT 1
</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>
<delete id="deleteRelationsByFaceIdAndType">
DELETE FROM member_source
WHERE face_id = #{faceId} AND `type` = #{type}
</delete>
</mapper>