Compare commits

..

11 Commits

Author SHA1 Message Date
47ae60b203 feat(task): 更新日期时间格式为包含时分显示
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 修改DownloadNotificationTasker中的videoShotTime格式为yyyy-MM-dd HH:mm
- 修改DownloadNotificationTasker中的expireDate格式为yyyy-MM-dd HH:mm
- 修改SourceNotificationTasker中的sourceVideoCreateTime格式为yyyy-MM-dd HH:mm
- 修改SourceNotificationTasker中的sourcePhotoCreateTime格式为yyyy-MM-dd HH:mm
2026-02-24 16:53:34 +08:00
703a5baf13 refactor(thread): 使用 threadId 替换 getId 方法
- 将 ai-cam-image-processor 线程命名中的 thread.getId() 替换为 thread.threadId()
- 将视频片段获取任务中的 Thread.currentThread().getId() 替换为 Thread.currentThread().threadId()
- 统一使用 threadId 方法提高代码一致性
- 保持线程标识符的唯一性和可读性
2026-02-24 14:30:31 +08:00
7454111615 feat(puzzle): 更新拼图生成中的日期显示逻辑
- 在PuzzleGenerationOrchestrator中注入SourceMapper依赖
- 新增getMaxCreateTimeByFaceId方法查询图片素材最大创建时间
- 修改拼图模板生成逻辑使用图片素材最大创建时间作为dateStr值
- 移除硬编码当前日期,改为基于实际素材时间
- 更新FaceMatchingOrchestrator中相同的日期处理逻辑
- 从跳过重复检测的元素键集合中移除dateStr字段
2026-02-20 20:11:11 +08:00
39c955b55c refactor(face): 重构人脸识别服务中的设备数据处理逻辑
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 按设备列表顺序重新排列结果以确保一致性
- 为每个设备创建对应的内容项,包括无源设备的占位符
- 过滤并按设备ID分组源实体数据
- 为无关联设备的源实体补充单独的处理逻辑
- 优化数据流处理提高代码可读性和维护性
2026-02-15 09:05:09 +08:00
9a18ffc167 feat(face): 添加人脸样本关联分组功能
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 新增 FaceSampleAssociationController 提供关联管理API
- 新增 ExpandSampleAssociationStage 扩展匹配结果中的关联样本
- 在人脸匹配流程中集成关联样本扩展逻辑
- 新增 FaceSampleAssociationMapper 和相关实体类
- 修改 UpdateFaceResultStage 使用扩展后的样本ID列表
- 添加关联样本扩展的日志记录和统计功能
2026-02-15 02:55:16 +08:00
062a128dcc fix(printer): 修正照片打印产品数量设置
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 将照片打印产品的数量固定为1
- 保留购买次数为源ID列表的大小
- 确保每个照片打印项只计算一次数量
2026-02-14 19:22:50 +08:00
f9c776b3ab feat(printer): 支持批量创建虚拟订单功能
- 修改CreateVirtualOrderRequest参数结构,将sourceId改为sourceIds列表
- 添加对sourceIds参数的空值校验
- 调用createBatchVirtualOrder方法替代原有单个订单创建逻辑
- 更新API接口支持多条source记录聚合为一笔订单
2026-02-14 19:07:12 +08:00
e5eea4c349 fix(face): 修复摄影师拍照内容购买状态显示问题
- 添加会员资源关系查询以获取正确的购买状态
- 修改内容设置逻辑使用会员资源关系中的购买状态
- 实现流式过滤匹配资源ID并设置对应的购买标识
2026-02-14 18:19:04 +08:00
0484c8077d fix(face): 修复摄影师拍照内容购买状态显示问题
- 添加会员资源关系查询以获取正确的购买状态
- 修改内容设置逻辑使用会员资源关系中的购买状态
- 实现流式过滤匹配资源ID并设置对应的购买标识
2026-02-14 18:05:46 +08:00
6a22fc87a7 feat(order): 添加单张照片订单类型支持
- 新增 member_single_photo_data CTE 查询单张照片数据
- 添加订单类型 14 对应单张照片类型的映射
- 在订单项目查询中增加对单张照片类型的支持
- 关联 member_single_photo_data 表获取单张照片的 face_id 和 URL
- 实现单张照片类型的 face_url 和 imgUrl 映射逻辑
2026-02-14 17:54:13 +08:00
b01056d829 feat(coupon): 添加景区ID过滤功能以查询用户可用优惠券
- 在getUserCoupons接口中添加scenicId参数支持
- 修改couponService实现以按景区ID过滤优惠券
- 添加空值检查跳过无效配置的优惠券
- 更新接口文档添加scenicId参数说明
2026-02-14 17:42:44 +08:00
27 changed files with 636 additions and 62 deletions

View File

@@ -0,0 +1,111 @@
package com.ycwl.basic.controller.pc;
import com.ycwl.basic.annotation.IgnoreToken;
import com.ycwl.basic.mapper.FaceSampleAssociationMapper;
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleAssociationEntity;
import com.ycwl.basic.model.pc.faceSample.req.FaceSampleAssociationReq;
import com.ycwl.basic.utils.ApiResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 人脸样本关联分组管理
*/
@Slf4j
@RestController
@RequestMapping("/api/faceSampleAssociation/v1")
@IgnoreToken
public class FaceSampleAssociationController {
@Autowired
private FaceSampleAssociationMapper faceSampleAssociationMapper;
/**
* 添加关联:将一批 faceSampleId 加入指定的 groupKey
*/
@PostMapping("/add")
public ApiResponse<String> add(@RequestBody FaceSampleAssociationReq req) {
if (req.getScenicId() == null || req.getGroupKey() == null || req.getGroupKey().isBlank()) {
return ApiResponse.fail("scenicId 和 groupKey 不能为空");
}
if (req.getFaceSampleIds() == null || req.getFaceSampleIds().isEmpty()) {
return ApiResponse.fail("faceSampleIds 不能为空");
}
List<FaceSampleAssociationEntity> entities = req.getFaceSampleIds().stream()
.map(sampleId -> {
FaceSampleAssociationEntity entity = new FaceSampleAssociationEntity();
entity.setScenicId(req.getScenicId());
entity.setGroupKey(req.getGroupKey());
entity.setFaceSampleId(sampleId);
return entity;
})
.collect(Collectors.toList());
faceSampleAssociationMapper.batchInsertIgnore(entities);
log.info("添加人脸样本关联: scenicId={}, groupKey={}, count={}",
req.getScenicId(), req.getGroupKey(), entities.size());
return ApiResponse.success("添加成功");
}
/**
* 移除关联:从指定 groupKey 中移除一批 faceSampleId
*/
@PostMapping("/remove")
public ApiResponse<String> remove(@RequestBody FaceSampleAssociationReq req) {
if (req.getScenicId() == null || req.getGroupKey() == null || req.getGroupKey().isBlank()) {
return ApiResponse.fail("scenicId 和 groupKey 不能为空");
}
if (req.getFaceSampleIds() == null || req.getFaceSampleIds().isEmpty()) {
return ApiResponse.fail("faceSampleIds 不能为空");
}
faceSampleAssociationMapper.deleteByGroupAndSampleIds(
req.getScenicId(), req.getGroupKey(), req.getFaceSampleIds());
log.info("移除人脸样本关联: scenicId={}, groupKey={}, count={}",
req.getScenicId(), req.getGroupKey(), req.getFaceSampleIds().size());
return ApiResponse.success("移除成功");
}
/**
* 删除整个组
*/
@PostMapping("/deleteGroup")
public ApiResponse<String> deleteGroup(@RequestParam Long scenicId, @RequestParam String groupKey) {
if (scenicId == null || groupKey == null || groupKey.isBlank()) {
return ApiResponse.fail("scenicId 和 groupKey 不能为空");
}
faceSampleAssociationMapper.deleteByGroup(scenicId, groupKey);
log.info("删除关联组: scenicId={}, groupKey={}", scenicId, groupKey);
return ApiResponse.success("删除成功");
}
/**
* 查询指定组的所有样本ID
*/
@GetMapping("/listByGroup")
public ApiResponse<List<Long>> listByGroup(@RequestParam Long scenicId, @RequestParam String groupKey) {
List<Long> sampleIds = faceSampleAssociationMapper.listSampleIdsByGroup(scenicId, groupKey);
return ApiResponse.success(sampleIds);
}
/**
* 查询指定景区下的所有分组及其样本ID
*/
@GetMapping("/listGroups")
public ApiResponse<Map<String, List<Long>>> listGroups(@RequestParam Long scenicId) {
List<String> groupKeys = faceSampleAssociationMapper.listGroupKeysByScenicId(scenicId);
Map<String, List<Long>> result = groupKeys.stream()
.collect(Collectors.toMap(
groupKey -> groupKey,
groupKey -> faceSampleAssociationMapper.listSampleIdsByGroup(scenicId, groupKey)
));
return ApiResponse.success(result);
}
}

View File

@@ -62,9 +62,12 @@ public class SourceController {
*/ */
@PostMapping("/createVirtualOrder") @PostMapping("/createVirtualOrder")
public ApiResponse<Map<String, Object>> createVirtualOrder(@RequestBody CreateVirtualOrderRequest request) { public ApiResponse<Map<String, Object>> createVirtualOrder(@RequestBody CreateVirtualOrderRequest request) {
if (request.getSourceIds() == null || request.getSourceIds().isEmpty()) {
return ApiResponse.fail("sourceIds不能为空");
}
try { try {
Map<String, Object> result = printerService.createVirtualOrder( Map<String, Object> result = printerService.createBatchVirtualOrder(
request.getSourceId(), request.getSourceIds(),
request.getScenicId(), request.getScenicId(),
request.getPrinterId(), request.getPrinterId(),
request.getNeedEnhance(), request.getNeedEnhance(),

View File

@@ -89,6 +89,12 @@ public class FaceMatchingContext implements PipelineContext {
*/ */
private List<Long> freeSourceIds; private List<Long> freeSourceIds;
/**
* 关联扩展的样本ID列表(由 ExpandSampleAssociationStage 设置)
* 用于标识哪些 sampleId 是通过关联分组扩展得到的,而非直接匹配
*/
private List<Long> associatedSampleIds;
/** /**
* 人脸选择后置模式配置(自定义匹配场景) * 人脸选择后置模式配置(自定义匹配场景)
* 0: 并集, 1: 交集, 2: 直接使用 * 0: 并集, 1: 交集, 2: 直接使用

View File

@@ -66,6 +66,8 @@ public class FaceMatchingPipelineFactory {
private FilterByDevicePhotoLimitStage filterByDevicePhotoLimitStage; private FilterByDevicePhotoLimitStage filterByDevicePhotoLimitStage;
@Autowired @Autowired
private DeleteOldRelationsStage deleteOldRelationsStage; private DeleteOldRelationsStage deleteOldRelationsStage;
@Autowired
private ExpandSampleAssociationStage expandSampleAssociationStage;
// ==================== 辅助服务 ==================== // ==================== 辅助服务 ====================
@Autowired @Autowired
@@ -91,13 +93,16 @@ public class FaceMatchingPipelineFactory {
// 3. 人脸识别补救 // 3. 人脸识别补救
builder.addStage(faceRecoveryStage); builder.addStage(faceRecoveryStage);
// 4. 更新人脸结果(落库) // 4. 关联样本扩展
builder.addStage(expandSampleAssociationStage);
// 5. 更新人脸结果(落库)
builder.addStage(updateFaceResultStage); builder.addStage(updateFaceResultStage);
// 5. 构建源文件关联(建关系) // 6. 构建源文件关联(建关系)
builder.addStage(buildSourceRelationStage); builder.addStage(buildSourceRelationStage);
// 6. 持久化关联关系(建关系) // 7. 持久化关联关系(建关系)
builder.addStage(persistRelationsStage); builder.addStage(persistRelationsStage);
log.debug("创建打印机大屏识别试点Pipeline: stageCount={}", builder.build().getStageCount()); log.debug("创建打印机大屏识别试点Pipeline: stageCount={}", builder.build().getStageCount());
@@ -131,28 +136,31 @@ public class FaceMatchingPipelineFactory {
// 5. 人脸识别补救 // 5. 人脸识别补救
builder.addStage(faceRecoveryStage); builder.addStage(faceRecoveryStage);
// 6. 更新人脸结果 // 6. 关联样本扩展
builder.addStage(expandSampleAssociationStage);
// 7. 更新人脸结果
builder.addStage(updateFaceResultStage); builder.addStage(updateFaceResultStage);
// 7. 构建源文件关联 // 8. 构建源文件关联
builder.addStage(buildSourceRelationStage); builder.addStage(buildSourceRelationStage);
// 8. 处理免费源文件逻辑 // 9. 处理免费源文件逻辑
builder.addStage(processFreeSourceStage); builder.addStage(processFreeSourceStage);
// 9. 处理购买状态 // 10. 处理购买状态
builder.addStage(processBuyStatusStage); builder.addStage(processBuyStatusStage);
// 10. 处理视频重切 // 11. 处理视频重切
builder.addStage(handleVideoRecreationStage); builder.addStage(handleVideoRecreationStage);
// 11. 持久化关联关系 // 12. 持久化关联关系
builder.addStage(persistRelationsStage); builder.addStage(persistRelationsStage);
// 12. 创建任务 // 13. 创建任务
builder.addStage(createTaskStage); builder.addStage(createTaskStage);
// 13. 异步生成拼图模板 // 14. 异步生成拼图模板
builder.addStage(generatePuzzleStage); builder.addStage(generatePuzzleStage);
log.debug("创建自动人脸匹配Pipeline: isNew={}, stageCount={}", isNew, builder.build().getStageCount()); log.debug("创建自动人脸匹配Pipeline: isNew={}, stageCount={}", isNew, builder.build().getStageCount());
@@ -189,28 +197,31 @@ public class FaceMatchingPipelineFactory {
// 7. 应用设备照片数量限制筛选 // 7. 应用设备照片数量限制筛选
builder.addStage(filterByDevicePhotoLimitStage); builder.addStage(filterByDevicePhotoLimitStage);
// 8. 更新人脸结果 // 8. 关联样本扩展
builder.addStage(expandSampleAssociationStage);
// 9. 更新人脸结果
builder.addStage(updateFaceResultStage); builder.addStage(updateFaceResultStage);
// 9. 删除旧关系数据 // 10. 删除旧关系数据
builder.addStage(deleteOldRelationsStage); builder.addStage(deleteOldRelationsStage);
// 10. 构建源文件关联 // 11. 构建源文件关联
builder.addStage(buildSourceRelationStage); builder.addStage(buildSourceRelationStage);
// 11. 处理免费源文件逻辑 // 12. 处理免费源文件逻辑
builder.addStage(processFreeSourceStage); builder.addStage(processFreeSourceStage);
// 12. 处理购买状态 // 13. 处理购买状态
builder.addStage(processBuyStatusStage); builder.addStage(processBuyStatusStage);
// 13. 处理视频重切 // 14. 处理视频重切
builder.addStage(handleVideoRecreationStage); builder.addStage(handleVideoRecreationStage);
// 14. 持久化关联关系 // 15. 持久化关联关系
builder.addStage(persistRelationsStage); builder.addStage(persistRelationsStage);
// 15. 创建任务 // 16. 创建任务
builder.addStage(createTaskStage); builder.addStage(createTaskStage);
log.debug("创建自定义人脸匹配Pipeline: stageCount={}", builder.build().getStageCount()); log.debug("创建自定义人脸匹配Pipeline: stageCount={}", builder.build().getStageCount());

View File

@@ -2,6 +2,7 @@ package com.ycwl.basic.face.pipeline.helper;
import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.DateUtil;
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO; import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO;
import com.ycwl.basic.mapper.SourceMapper;
import com.ycwl.basic.puzzle.dto.PuzzleGenerateRequest; import com.ycwl.basic.puzzle.dto.PuzzleGenerateRequest;
import com.ycwl.basic.puzzle.dto.PuzzleGenerateResponse; import com.ycwl.basic.puzzle.dto.PuzzleGenerateResponse;
import com.ycwl.basic.puzzle.dto.PuzzleTemplateDTO; import com.ycwl.basic.puzzle.dto.PuzzleTemplateDTO;
@@ -44,6 +45,9 @@ public class PuzzleGenerationOrchestrator {
@Autowired @Autowired
private ScenicRepository scenicRepository; private ScenicRepository scenicRepository;
@Autowired
private SourceMapper sourceMapper;
/** /**
* 异步生成景区所有启用的拼图模板 * 异步生成景区所有启用的拼图模板
* *
@@ -74,6 +78,11 @@ public class PuzzleGenerationOrchestrator {
// 3. 准备公共动态数据 // 3. 准备公共动态数据
Map<String, String> baseDynamicData = buildBaseDynamicData(faceId, faceUrl, scenicBasic); Map<String, String> baseDynamicData = buildBaseDynamicData(faceId, faceUrl, scenicBasic);
// 4. 设置dateStr为图片素材的最大创建时间
Date maxCreateTime = sourceMapper.getMaxCreateTimeByFaceId(faceId);
baseDynamicData.put("dateStr", DateUtil.format(
maxCreateTime != null ? maxCreateTime : new Date(), "yyyy.MM.dd"));
// 4. 使用虚拟线程池并行生成所有模板 // 4. 使用虚拟线程池并行生成所有模板
java.util.concurrent.atomic.AtomicInteger successCount = new java.util.concurrent.atomic.AtomicInteger(0); java.util.concurrent.atomic.AtomicInteger successCount = new java.util.concurrent.atomic.AtomicInteger(0);
java.util.concurrent.atomic.AtomicInteger failCount = new java.util.concurrent.atomic.AtomicInteger(0); java.util.concurrent.atomic.AtomicInteger failCount = new java.util.concurrent.atomic.AtomicInteger(0);
@@ -121,7 +130,6 @@ public class PuzzleGenerationOrchestrator {
baseDynamicData.put("faceId", String.valueOf(faceId)); baseDynamicData.put("faceId", String.valueOf(faceId));
baseDynamicData.put("scenicName", scenicBasic.getName()); baseDynamicData.put("scenicName", scenicBasic.getName());
baseDynamicData.put("scenicText", scenicBasic.getName()); baseDynamicData.put("scenicText", scenicBasic.getName());
baseDynamicData.put("dateStr", DateUtil.format(new Date(), "yyyy.MM.dd"));
return baseDynamicData; return baseDynamicData;
} }

View File

@@ -0,0 +1,116 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.mapper.FaceSampleAssociationMapper;
import com.ycwl.basic.model.task.resp.SearchFaceRespVo;
import com.ycwl.basic.pipeline.annotation.StageConfig;
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
import com.ycwl.basic.pipeline.core.StageResult;
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 根据人脸样本关联分组扩展匹配结果
* <p>
* 当匹配结果中包含某分组的任一 faceSampleId 时,
* 将该分组内所有 faceSampleId 加入匹配结果。
* 关联扩展的样本ID跳过时间范围和设备限制筛选(通过Stage执行顺序保证)。
*/
@Slf4j
@Component
@StageConfig(
stageId = "expand_sample_association",
optionalMode = StageOptionalMode.SUPPORT,
description = "根据人脸样本关联分组扩展匹配结果",
defaultEnabled = true
)
public class ExpandSampleAssociationStage extends AbstractPipelineStage<FaceMatchingContext> {
@Autowired
private FaceSampleAssociationMapper faceSampleAssociationMapper;
@Override
public String getName() {
return "ExpandSampleAssociation";
}
@Override
protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) {
SearchFaceRespVo searchResult = context.getSearchResult();
if (searchResult == null || searchResult.getSampleListIds() == null
|| searchResult.getSampleListIds().isEmpty()) {
return false;
}
return context.getFace() != null && context.getFace().getScenicId() != null;
}
@Override
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
// 防御性检查
SearchFaceRespVo searchResult = context.getSearchResult();
if (searchResult == null || searchResult.getSampleListIds() == null
|| searchResult.getSampleListIds().isEmpty()) {
return StageResult.skipped("searchResult或sampleListIds为空");
}
Long scenicId = context.getFace().getScenicId();
List<Long> originalMatchedIds = searchResult.getSampleListIds();
try {
List<Long> associatedIds = faceSampleAssociationMapper
.findAssociatedSampleIds(scenicId, originalMatchedIds);
if (associatedIds == null || associatedIds.isEmpty()) {
log.debug("未找到关联样本, faceId={}, scenicId={}", context.getFaceId(), scenicId);
return StageResult.success("无关联样本");
}
// 获取当前的 sampleListIds(可能已经过筛选,也可能与 searchResult 相同)
List<Long> currentSampleIds = context.getSampleListIds();
if (currentSampleIds == null || currentSampleIds.isEmpty()) {
currentSampleIds = new ArrayList<>(originalMatchedIds);
}
// 计算净新增的关联ID
Set<Long> currentSet = new HashSet<>(currentSampleIds);
List<Long> netNewIds = associatedIds.stream()
.filter(id -> !currentSet.contains(id))
.distinct()
.collect(Collectors.toList());
if (netNewIds.isEmpty()) {
log.debug("关联样本均已存在于当前结果中, faceId={}", context.getFaceId());
return StageResult.success("关联样本均已存在");
}
// 合并:当前筛选后的ID + 净新增关联ID
List<Long> expandedIds = Stream.concat(
currentSampleIds.stream(),
netNewIds.stream()
).distinct().collect(Collectors.toList());
context.setSampleListIds(expandedIds);
context.setAssociatedSampleIds(netNewIds);
log.info("关联样本扩展完成: faceId={}, scenicId={}, 原始匹配数={}, 扩展前={}, 净新增={}, 扩展后={}",
context.getFaceId(), scenicId,
originalMatchedIds.size(), currentSampleIds.size(),
netNewIds.size(), expandedIds.size());
return StageResult.success(String.format("关联扩展: +%d个样本", netNewIds.size()));
} catch (Exception e) {
log.error("关联样本扩展失败, faceId={}, scenicId={}", context.getFaceId(), scenicId, e);
return StageResult.degraded("关联样本扩展失败: " + e.getMessage());
}
}
}

View File

@@ -15,6 +15,7 @@ import org.springframework.stereotype.Component;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.Date; import java.util.Date;
import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@@ -67,8 +68,14 @@ public class UpdateFaceResultStage extends AbstractPipelineStage<FaceMatchingCon
faceEntity.setFirstMatchRate(BigDecimal.valueOf(searchResult.getFirstMatchRate())); faceEntity.setFirstMatchRate(BigDecimal.valueOf(searchResult.getFirstMatchRate()));
} }
if (searchResult.getSampleListIds() != null) { // 优先使用 context.sampleListIds(可能包含关联扩展的ID),
faceEntity.setMatchSampleIds(searchResult.getSampleListIds().stream() // 回退到 searchResult.sampleListIds
List<Long> finalSampleIds = context.getSampleListIds();
if (finalSampleIds == null || finalSampleIds.isEmpty()) {
finalSampleIds = searchResult.getSampleListIds();
}
if (finalSampleIds != null && !finalSampleIds.isEmpty()) {
faceEntity.setMatchSampleIds(finalSampleIds.stream()
.map(String::valueOf) .map(String::valueOf)
.collect(Collectors.joining(","))); .collect(Collectors.joining(",")));
} }
@@ -83,7 +90,7 @@ public class UpdateFaceResultStage extends AbstractPipelineStage<FaceMatchingCon
log.debug("人脸结果更新成功:faceId={}, score={}, sampleCount={}", log.debug("人脸结果更新成功:faceId={}, score={}, sampleCount={}",
faceId, searchResult.getScore(), faceId, searchResult.getScore(),
searchResult.getSampleListIds() != null ? searchResult.getSampleListIds().size() : 0); finalSampleIds != null ? finalSampleIds.size() : 0);
return StageResult.success("人脸结果更新成功"); return StageResult.success("人脸结果更新成功");

View File

@@ -0,0 +1,48 @@
package com.ycwl.basic.mapper;
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleAssociationEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 人脸样本关联分组 Mapper
*/
@Mapper
public interface FaceSampleAssociationMapper {
/**
* 根据景区ID和样本ID列表,查找同组的所有关联样本ID
*/
List<Long> findAssociatedSampleIds(@Param("scenicId") Long scenicId,
@Param("sampleIds") List<Long> sampleIds);
/**
* 批量插入关联记录(忽略重复)
*/
void batchInsertIgnore(@Param("list") List<FaceSampleAssociationEntity> list);
/**
* 删除指定组内的指定样本关联
*/
void deleteByGroupAndSampleIds(@Param("scenicId") Long scenicId,
@Param("groupKey") String groupKey,
@Param("sampleIds") List<Long> sampleIds);
/**
* 删除整个组的所有关联
*/
void deleteByGroup(@Param("scenicId") Long scenicId,
@Param("groupKey") String groupKey);
/**
* 查询指定组的所有样本ID
*/
List<Long> listSampleIdsByGroup(@Param("scenicId") Long scenicId,
@Param("groupKey") String groupKey);
/**
* 查询指定景区下的所有分组标识
*/
List<String> listGroupKeysByScenicId(@Param("scenicId") Long scenicId);
}

View File

@@ -143,6 +143,13 @@ public interface SourceMapper {
*/ */
List<Long> getDeviceIdsByFaceId(Long faceId); List<Long> getDeviceIdsByFaceId(Long faceId);
/**
* 获取faceId关联的图片素材的最大创建时间
* @param faceId 人脸ID
* @return 最大创建时间,无记录时返回null
*/
Date getMaxCreateTimeByFaceId(Long faceId);
/** /**
* 根据faceId和设备ID获取source * 根据faceId和设备ID获取source
* @param faceId 人脸ID * @param faceId 人脸ID

View File

@@ -0,0 +1,26 @@
package com.ycwl.basic.model.pc.faceSample.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
/**
* 人脸样本关联分组实体
* 同一景区下相同 groupKey 的记录互为关联
*/
@Data
@TableName("face_sample_association")
public class FaceSampleAssociationEntity {
@TableId(type = IdType.AUTO)
private Long id;
/** 景区ID */
private Long scenicId;
/** 关联分组标识 */
private String groupKey;
/** 人脸样本ID */
private Long faceSampleId;
private Date createAt;
}

View File

@@ -0,0 +1,18 @@
package com.ycwl.basic.model.pc.faceSample.req;
import lombok.Data;
import java.util.List;
/**
* 人脸样本关联分组请求
*/
@Data
public class FaceSampleAssociationReq {
/** 景区ID */
private Long scenicId;
/** 关联分组标识 */
private String groupKey;
/** 人脸样本ID列表 */
private List<Long> faceSampleIds;
}

View File

@@ -2,15 +2,17 @@ package com.ycwl.basic.model.printer.req;
import lombok.Data; import lombok.Data;
import java.util.List;
/** /**
* 创建虚拟用户0元订单请求参数 * 创建虚拟用户0元订单请求参数
*/ */
@Data @Data
public class CreateVirtualOrderRequest { public class CreateVirtualOrderRequest {
/** /**
* source记录ID * source记录ID列表(支持单个或多个sourceId聚合为一笔订单)
*/ */
private Long sourceId; private List<Long> sourceIds;
/** /**
* 景区ID * 景区ID

View File

@@ -58,13 +58,13 @@ public class PriceCalculationController {
* 查询用户可用优惠券(包含领取记录信息) * 查询用户可用优惠券(包含领取记录信息)
*/ */
@GetMapping("/coupons/my-coupons") @GetMapping("/coupons/my-coupons")
public ApiResponse<List<UserCouponResp>> getUserCoupons() { public ApiResponse<List<UserCouponResp>> getUserCoupons(@RequestParam(required = false) String scenicId) {
Long userId = getUserId(); Long userId = getUserId();
if (userId == null) { if (userId == null) {
return ApiResponse.fail("用户未登录"); return ApiResponse.fail("用户未登录");
} }
List<UserCouponResp> coupons = couponService.getUserAvailableCoupons(userId); List<UserCouponResp> coupons = couponService.getUserAvailableCoupons(userId, scenicId);
return ApiResponse.success(coupons); return ApiResponse.success(coupons);
} }

View File

@@ -59,9 +59,10 @@ public interface ICouponService {
* 查询用户可用优惠券(包含领取记录信息) * 查询用户可用优惠券(包含领取记录信息)
* *
* @param userId 用户ID * @param userId 用户ID
* @param scenicId 景区ID,传入时仅返回该景区可用的优惠券,NULL时返回全部
* @return 用户优惠券列表(包含领取记录+优惠券配置) * @return 用户优惠券列表(包含领取记录+优惠券配置)
*/ */
List<UserCouponResp> getUserAvailableCoupons(Long userId); List<UserCouponResp> getUserAvailableCoupons(Long userId, String scenicId);
/** /**
* 领取优惠券(内部调用方法) * 领取优惠券(内部调用方法)

View File

@@ -295,15 +295,19 @@ public class CouponServiceImpl implements ICouponService {
} }
@Override @Override
public List<UserCouponResp> getUserAvailableCoupons(Long userId) { public List<UserCouponResp> getUserAvailableCoupons(Long userId, String scenicId) {
List<PriceCouponClaimRecord> records = couponClaimRecordMapper.selectUserAvailableCoupons(userId); List<PriceCouponClaimRecord> records = couponClaimRecordMapper.selectUserAvailableCoupons(userId);
List<UserCouponResp> coupons = new ArrayList<>(); List<UserCouponResp> coupons = new ArrayList<>();
for (PriceCouponClaimRecord record : records) { for (PriceCouponClaimRecord record : records) {
PriceCouponConfig config = couponConfigMapper.selectById(record.getCouponId()); PriceCouponConfig config = couponConfigMapper.selectById(record.getCouponId());
if (config != null) { if (config == null) {
coupons.add(buildUserCouponResp(record, config)); continue;
} }
if (scenicId != null && config.getScenicId() != null && !scenicId.equals(config.getScenicId())) {
continue;
}
coupons.add(buildUserCouponResp(record, config));
} }
return coupons; return coupons;

View File

@@ -25,7 +25,7 @@ import java.util.stream.Collectors;
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
public class PuzzleDuplicationDetector { public class PuzzleDuplicationDetector {
private final Set<String> skippedElementKeys = Set.of("dateStr"); private final Set<String> skippedElementKeys = Set.of();
private final PuzzleGenerationRecordMapper recordMapper; private final PuzzleGenerationRecordMapper recordMapper;
/** /**

View File

@@ -40,7 +40,7 @@ public class SourceRepository {
Runtime.getRuntime().availableProcessors(), Runtime.getRuntime().availableProcessors(),
runnable -> { runnable -> {
Thread thread = new Thread(runnable); Thread thread = new Thread(runnable);
thread.setName("ai-cam-image-processor-" + thread.getId()); thread.setName("ai-cam-image-processor-" + thread.threadId());
thread.setDaemon(true); thread.setDaemon(true);
return thread; return thread;
} }

View File

@@ -13,6 +13,7 @@ import com.ycwl.basic.face.pipeline.factory.FaceMatchingPipelineFactory;
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter; import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
import com.ycwl.basic.facebody.entity.SearchFaceResultItem; import com.ycwl.basic.facebody.entity.SearchFaceResultItem;
import com.ycwl.basic.integration.device.dto.device.DeviceV2DTO; import com.ycwl.basic.integration.device.dto.device.DeviceV2DTO;
import com.ycwl.basic.mapper.FaceSampleAssociationMapper;
import com.ycwl.basic.mapper.FaceSampleMapper; import com.ycwl.basic.mapper.FaceSampleMapper;
import com.ycwl.basic.mapper.ProjectMapper; import com.ycwl.basic.mapper.ProjectMapper;
import com.ycwl.basic.mapper.SourceMapper; import com.ycwl.basic.mapper.SourceMapper;
@@ -101,6 +102,7 @@ import org.springframework.web.multipart.MultipartFile;
import java.io.File; import java.io.File;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
@@ -161,6 +163,8 @@ public class FaceServiceImpl implements FaceService {
@Autowired @Autowired
private FaceSampleMapper faceSampleMapper; private FaceSampleMapper faceSampleMapper;
@Autowired @Autowired
private FaceSampleAssociationMapper faceSampleAssociationMapper;
@Autowired
private GoodsService goodsService; private GoodsService goodsService;
@Autowired @Autowired
private ProjectMapper projectMapper; private ProjectMapper projectMapper;
@@ -422,6 +426,37 @@ public class FaceServiceImpl implements FaceService {
} }
} }
/**
* 关联样本扩展
* 根据 face_sample_association 表,将同组样本ID加入匹配结果
*/
private void expandSampleAssociation(SearchFaceRespVo searchResult, Long scenicId, Long faceId) {
List<Long> sampleListIds = searchResult.getSampleListIds();
if (sampleListIds == null || sampleListIds.isEmpty()) {
return;
}
try {
List<Long> associatedIds = faceSampleAssociationMapper.findAssociatedSampleIds(scenicId, sampleListIds);
if (associatedIds == null || associatedIds.isEmpty()) {
return;
}
Set<Long> currentSet = new HashSet<>(sampleListIds);
List<Long> netNewIds = associatedIds.stream()
.filter(id -> !currentSet.contains(id))
.distinct()
.collect(Collectors.toList());
if (!netNewIds.isEmpty()) {
List<Long> expandedIds = new ArrayList<>(sampleListIds);
expandedIds.addAll(netNewIds);
searchResult.setSampleListIds(expandedIds);
log.info("关联样本扩展完成: faceId={}, scenicId={}, 原始数={}, 新增数={}, 扩展后={}",
faceId, scenicId, sampleListIds.size(), netNewIds.size(), expandedIds.size());
}
} catch (Exception e) {
log.error("关联样本扩展失败, faceId={}, scenicId={}", faceId, scenicId, e);
}
}
/** /**
* 更新人脸实体结果信息 * 更新人脸实体结果信息
* 仅用于 handleCustomFaceMatching 方法 * 仅用于 handleCustomFaceMatching 方法
@@ -483,25 +518,14 @@ public class FaceServiceImpl implements FaceService {
// 摄影师拍照 // 摄影师拍照
List<DeviceV2DTO> deviceList = deviceRepository.getAllDeviceByScenicId(face.getScenicId()); List<DeviceV2DTO> deviceList = deviceRepository.getAllDeviceByScenicId(face.getScenicId());
List<SourceEntity> sourceEntityList = sourceMapper.listSourceByFaceRelation(face.getId(), 2); List<SourceEntity> sourceEntityList = sourceMapper.listSourceByFaceRelation(face.getId(), 2);
for (SourceEntity sourceEntity : sourceEntityList) { List<MemberSourceEntity> memberSourceRelations = memberRelationRepository.listSourceByFaceRelation(face.getId(), 2);
ContentPageVO content = new ContentPageVO(); // 按 deviceList 顺序排列结果
content.setName("摄影师拍照"); Set<Long> deviceIds = deviceList.stream().map(DeviceV2DTO::getId).collect(Collectors.toSet());
deviceList.stream().filter(device -> device.getId().equals(sourceEntity.getDeviceId())).findFirst().ifPresent(device -> { for (DeviceV2DTO device : deviceList) {
content.setGroup(device.getName()); List<SourceEntity> deviceSources = sourceEntityList.stream()
}); .filter(s -> device.getId().equals(s.getDeviceId()))
content.setContentId(sourceEntity.getId()); .toList();
content.setGoodsType(2); if (deviceSources.isEmpty()) {
content.setContentType(2);
content.setScenicId(sourceEntity.getScenicId());
content.setSourceType(2);
content.setOrigUrl(sourceEntity.getUrl());
content.setTemplateCoverUrl(sourceEntity.getThumbUrl());
content.setIsBuy(sourceEntity.getIsBuy());
content.setLockType(-1);
result.add(content);
}
List<Long> containedDeviceId = sourceEntityList.stream().map(SourceEntity::getDeviceId).filter(Objects::nonNull).distinct().toList();
deviceList.stream().filter(device -> !containedDeviceId.contains(device.getId())).forEach(device -> {
ContentPageVO content = new ContentPageVO(); ContentPageVO content = new ContentPageVO();
content.setName(device.getName()); content.setName(device.getName());
content.setGroup(device.getName()); content.setGroup(device.getName());
@@ -514,7 +538,46 @@ public class FaceServiceImpl implements FaceService {
content.setIsBuy(0); content.setIsBuy(0);
content.setLockType(1); content.setLockType(1);
result.add(content); result.add(content);
} else {
for (SourceEntity sourceEntity : deviceSources) {
ContentPageVO content = new ContentPageVO();
content.setName("摄影师拍照");
content.setGroup(device.getName());
content.setContentId(sourceEntity.getId());
content.setGoodsType(2);
content.setContentType(2);
content.setScenicId(sourceEntity.getScenicId());
content.setSourceType(2);
content.setOrigUrl(sourceEntity.getUrl());
content.setTemplateCoverUrl(sourceEntity.getThumbUrl());
memberSourceRelations.stream().filter(relation -> relation.getSourceId().equals(sourceEntity.getId())).findAny().ifPresent(relation -> {
content.setIsBuy(relation.getIsBuy());
}); });
content.setLockType(-1);
result.add(content);
}
}
}
// 补充不属于任何设备的源
for (SourceEntity sourceEntity : sourceEntityList) {
if (sourceEntity.getDeviceId() != null && deviceIds.contains(sourceEntity.getDeviceId())) {
continue;
}
ContentPageVO content = new ContentPageVO();
content.setName("摄影师拍照");
content.setContentId(sourceEntity.getId());
content.setGoodsType(2);
content.setContentType(2);
content.setScenicId(sourceEntity.getScenicId());
content.setSourceType(2);
content.setOrigUrl(sourceEntity.getUrl());
content.setTemplateCoverUrl(sourceEntity.getThumbUrl());
memberSourceRelations.stream().filter(relation -> relation.getSourceId().equals(sourceEntity.getId())).findAny().ifPresent(relation -> {
content.setIsBuy(relation.getIsBuy());
});
content.setLockType(-1);
result.add(content);
}
return result; return result;
} }
List<TemplateRespVO> templateList = templateRepository.getTemplateListByScenicId(face.getScenicId()); List<TemplateRespVO> templateList = templateRepository.getTemplateListByScenicId(face.getScenicId());
@@ -1057,6 +1120,9 @@ public class FaceServiceImpl implements FaceService {
allFaceSampleList.size(), filteredSampleIds.size()); allFaceSampleList.size(), filteredSampleIds.size());
} }
// 4. 关联样本扩展
expandSampleAssociation(mergedResult, face.getScenicId(), faceId);
updateFaceEntityResult(face, mergedResult, faceId); updateFaceEntityResult(face, mergedResult, faceId);
List<Long> sampleListIds = mergedResult.getSampleListIds(); List<Long> sampleListIds = mergedResult.getSampleListIds();

View File

@@ -44,6 +44,7 @@ import com.ycwl.basic.model.pc.price.entity.PriceConfigEntity;
import com.ycwl.basic.model.pc.price.resp.GoodsListRespVO; import com.ycwl.basic.model.pc.price.resp.GoodsListRespVO;
import com.ycwl.basic.model.pc.printer.resp.MemberPrintResp; import com.ycwl.basic.model.pc.printer.resp.MemberPrintResp;
import com.ycwl.basic.model.pc.source.entity.SourceEntity; import com.ycwl.basic.model.pc.source.entity.SourceEntity;
import com.ycwl.basic.model.pc.source.resp.SourceRespVO;
import com.ycwl.basic.model.pc.task.entity.TaskEntity; import com.ycwl.basic.model.pc.task.entity.TaskEntity;
import com.ycwl.basic.model.pc.template.resp.TemplateRespVO; import com.ycwl.basic.model.pc.template.resp.TemplateRespVO;
import com.ycwl.basic.model.pc.video.entity.VideoEntity; import com.ycwl.basic.model.pc.video.entity.VideoEntity;
@@ -423,6 +424,21 @@ public class OrderServiceImpl implements OrderService {
goods.setTemplateCoverUrl(item.getCoverUrl()); goods.setTemplateCoverUrl(item.getCoverUrl());
goods.setScenicId(order.getScenicId()); goods.setScenicId(order.getScenicId());
goodsList.add(goods); goodsList.add(goods);
} else if (Integer.valueOf(14).equals(item.getGoodsType())) { // 单张照片 goodsId就是sourceId
SourceRespVO source = sourceMapper.getById(item.getGoodsId());
if (source != null) {
item.setCoverList(Collections.singletonList(source.getUrl()));
GoodsDetailVO goods = new GoodsDetailVO();
goods.setGoodsId(source.getId());
goods.setGoodsName("单张照片");
goods.setUrl(source.getUrl());
goods.setGoodsType(14);
goods.setScenicId(source.getScenicId());
goods.setTemplateCoverUrl(source.getUrl());
goods.setCreateTime(source.getCreateTime());
goodsList.add(goods);
item.setShootingTime(source.getCreateTime());
}
} else { } else {
item.setCoverList(Collections.singletonList(item.getCoverUrl())); item.setCoverList(Collections.singletonList(item.getCoverUrl()));
VideoEntity videoMapperById = videoRepository.getVideo(item.getGoodsId()); VideoEntity videoMapperById = videoRepository.getVideo(item.getGoodsId());

View File

@@ -18,6 +18,7 @@ import com.ycwl.basic.exception.BaseException;
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter; import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
import com.ycwl.basic.integration.common.manager.ScenicConfigManager; import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
import com.ycwl.basic.mapper.FaceMapper; import com.ycwl.basic.mapper.FaceMapper;
import com.ycwl.basic.mapper.FaceSampleAssociationMapper;
import com.ycwl.basic.mapper.SourceMapper; import com.ycwl.basic.mapper.SourceMapper;
import com.ycwl.basic.model.pc.face.entity.FaceEntity; import com.ycwl.basic.model.pc.face.entity.FaceEntity;
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity; import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
@@ -41,8 +42,11 @@ import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
@@ -107,6 +111,8 @@ public class FaceMatchingOrchestrator {
private FaceStatusManager faceStatusManager; private FaceStatusManager faceStatusManager;
@Autowired @Autowired
private PuzzleRepository puzzleRepository; private PuzzleRepository puzzleRepository;
@Autowired
private FaceSampleAssociationMapper faceSampleAssociationMapper;
/** /**
* 编排人脸匹配的完整流程 * 编排人脸匹配的完整流程
@@ -146,6 +152,9 @@ public class FaceMatchingOrchestrator {
searchResult = faceRecoveryStrategy.executeFaceRecoveryLogic( searchResult = faceRecoveryStrategy.executeFaceRecoveryLogic(
searchResult, context.scenicConfig, context.faceBodyAdapter, context.face.getScenicId()); searchResult, context.scenicConfig, context.faceBodyAdapter, context.face.getScenicId());
// 关联样本扩展
expandSampleAssociation(searchResult, context.face.getScenicId(), faceId);
// 步骤3: 更新人脸结果 // 步骤3: 更新人脸结果
updateFaceResult(context.face, searchResult, faceId); updateFaceResult(context.face, searchResult, faceId);
@@ -396,7 +405,9 @@ public class FaceMatchingOrchestrator {
baseDynamicData.put("faceId", String.valueOf(faceId)); baseDynamicData.put("faceId", String.valueOf(faceId));
baseDynamicData.put("scenicName", scenicBasic.getName()); baseDynamicData.put("scenicName", scenicBasic.getName());
baseDynamicData.put("scenicText", scenicBasic.getName()); baseDynamicData.put("scenicText", scenicBasic.getName());
baseDynamicData.put("dateStr", DateUtil.format(new Date(), "yyyy.MM.dd")); Date maxCreateTime = sourceMapper.getMaxCreateTimeByFaceId(faceId);
baseDynamicData.put("dateStr", DateUtil.format(
maxCreateTime != null ? maxCreateTime : new Date(), "yyyy.MM.dd"));
templateList templateList
.forEach(template -> { .forEach(template -> {
@@ -428,6 +439,40 @@ public class FaceMatchingOrchestrator {
}); });
} }
/**
* 关联样本扩展
* 根据 face_sample_association 表,将同组样本ID加入匹配结果
*/
private void expandSampleAssociation(SearchFaceRespVo searchResult, Long scenicId, Long faceId) {
List<Long> sampleListIds = searchResult.getSampleListIds();
if (sampleListIds == null || sampleListIds.isEmpty()) {
return;
}
try {
List<Long> associatedIds = faceSampleAssociationMapper.findAssociatedSampleIds(scenicId, sampleListIds);
if (associatedIds == null || associatedIds.isEmpty()) {
return;
}
Set<Long> currentSet = new HashSet<>(sampleListIds);
List<Long> netNewIds = associatedIds.stream()
.filter(id -> !currentSet.contains(id))
.distinct()
.collect(Collectors.toList());
if (!netNewIds.isEmpty()) {
List<Long> expandedIds = new ArrayList<>(sampleListIds);
expandedIds.addAll(netNewIds);
searchResult.setSampleListIds(expandedIds);
log.info("关联样本扩展完成: faceId={}, scenicId={}, 原始数={}, 新增数={}, 扩展后={}",
faceId, scenicId, sampleListIds.size(), netNewIds.size(), expandedIds.size());
}
} catch (Exception e) {
log.error("关联样本扩展失败, faceId={}, scenicId={}", faceId, scenicId, e);
// 扩展失败不影响主流程
}
}
/** /**
* 匹配上下文 * 匹配上下文
* 封装匹配过程中需要的所有上下文信息 * 封装匹配过程中需要的所有上下文信息

View File

@@ -2037,7 +2037,7 @@ public class PrinterServiceImpl implements PrinterService {
ProductItem photoItem = new ProductItem(); ProductItem photoItem = new ProductItem();
photoItem.setProductType(ProductType.PHOTO_PRINT); photoItem.setProductType(ProductType.PHOTO_PRINT);
photoItem.setProductId(scenicId.toString()); photoItem.setProductId(scenicId.toString());
photoItem.setQuantity(sourceIds.size()); photoItem.setQuantity(1);
photoItem.setPurchaseCount(sourceIds.size()); photoItem.setPurchaseCount(sourceIds.size());
photoItem.setScenicId(scenicId.toString()); photoItem.setScenicId(scenicId.toString());
productItems.add(photoItem); productItems.add(photoItem);

View File

@@ -85,7 +85,7 @@ public class DownloadNotificationTasker {
} }
variables.put("videoDeviceCount", videoTaskRepository.getTaskDeviceNum(item.getTaskId())); variables.put("videoDeviceCount", videoTaskRepository.getTaskDeviceNum(item.getTaskId()));
variables.put("videoLensCount", videoTaskRepository.getTaskLensNum(item.getTaskId())); variables.put("videoLensCount", videoTaskRepository.getTaskLensNum(item.getTaskId()));
variables.put("videoShotTime", DateUtil.format(videoTaskRepository.getTaskShotDate(item.getTaskId()), "yyyy-MM-dd")); variables.put("videoShotTime", DateUtil.format(videoTaskRepository.getTaskShotDate(item.getTaskId()), "yyyy-MM-dd HH:mm"));
WechatSubscribeNotifyTriggerRequest request = WechatSubscribeNotifyTriggerRequest.builder() WechatSubscribeNotifyTriggerRequest request = WechatSubscribeNotifyTriggerRequest.builder()
.scenicId(item.getScenicId()) .scenicId(item.getScenicId())
.memberId(item.getMemberId()) .memberId(item.getMemberId())
@@ -137,7 +137,7 @@ public class DownloadNotificationTasker {
} else { } else {
variables.put("videoResultPage", "videoSynthesis"); variables.put("videoResultPage", "videoSynthesis");
} }
variables.put("expireDate", DateUtil.format(expireDate, "yyyy-MM-dd")); variables.put("expireDate", DateUtil.format(expireDate, "yyyy-MM-dd HH:mm"));
variables.put("videoDeviceCount", videoTaskRepository.getTaskDeviceNum(item.getTaskId())); variables.put("videoDeviceCount", videoTaskRepository.getTaskDeviceNum(item.getTaskId()));
variables.put("videoLensCount", videoTaskRepository.getTaskLensNum(item.getTaskId())); variables.put("videoLensCount", videoTaskRepository.getTaskLensNum(item.getTaskId()));
variables.put("videoShotTime", DateUtil.format(videoTaskRepository.getTaskShotDate(item.getTaskId()), "yyyy-MM-dd HH:mm")); variables.put("videoShotTime", DateUtil.format(videoTaskRepository.getTaskShotDate(item.getTaskId()), "yyyy-MM-dd HH:mm"));
@@ -191,7 +191,7 @@ public class DownloadNotificationTasker {
} else { } else {
variables.put("videoResultPage", "videoSynthesis"); variables.put("videoResultPage", "videoSynthesis");
} }
variables.put("expireDate", DateUtil.format(expireDate, "yyyy-MM-dd")); variables.put("expireDate", DateUtil.format(expireDate, "yyyy-MM-dd HH:mm"));
variables.put("videoDeviceCount", videoTaskRepository.getTaskDeviceNum(item.getTaskId())); variables.put("videoDeviceCount", videoTaskRepository.getTaskDeviceNum(item.getTaskId()));
variables.put("videoLensCount", videoTaskRepository.getTaskLensNum(item.getTaskId())); variables.put("videoLensCount", videoTaskRepository.getTaskLensNum(item.getTaskId()));
variables.put("videoShotTime", DateUtil.format(videoTaskRepository.getTaskShotDate(item.getTaskId()), "yyyy-MM-dd HH:mm")); variables.put("videoShotTime", DateUtil.format(videoTaskRepository.getTaskShotDate(item.getTaskId()), "yyyy-MM-dd HH:mm"));

View File

@@ -64,10 +64,10 @@ public class SourceNotificationTasker {
variables.put("faceId", item.getId()); variables.put("faceId", item.getId());
List<MemberSourceEntity> sourceVideoList = memberRelationRepository.listSourceByFaceRelation(item.getId(), 1); List<MemberSourceEntity> sourceVideoList = memberRelationRepository.listSourceByFaceRelation(item.getId(), 1);
variables.put("sourceVideoCount", sourceVideoList.size()); variables.put("sourceVideoCount", sourceVideoList.size());
variables.put("sourceVideoCreateTime", DateUtil.format(item.getCreateAt(), "yyyy-MM-dd")); variables.put("sourceVideoCreateTime", DateUtil.format(item.getCreateAt(), "yyyy-MM-dd HH:mm"));
List<MemberSourceEntity> sourcePhotoList = memberRelationRepository.listSourceByFaceRelation(item.getId(), 2); List<MemberSourceEntity> sourcePhotoList = memberRelationRepository.listSourceByFaceRelation(item.getId(), 2);
variables.put("sourcePhotoCount", sourcePhotoList.size()); variables.put("sourcePhotoCount", sourcePhotoList.size());
variables.put("sourcePhotoCreateTime", DateUtil.format(item.getCreateAt(), "yyyy-MM-dd")); variables.put("sourcePhotoCreateTime", DateUtil.format(item.getCreateAt(), "yyyy-MM-dd HH:mm"));
WechatSubscribeNotifyTriggerRequest request = WechatSubscribeNotifyTriggerRequest.builder() WechatSubscribeNotifyTriggerRequest request = WechatSubscribeNotifyTriggerRequest.builder()
.scenicId(item.getScenicId()) .scenicId(item.getScenicId())

View File

@@ -342,7 +342,7 @@ public class VideoPieceGetter {
ffmpegTask.setDuration(duration); ffmpegTask.setDuration(duration);
ffmpegTask.setOffsetStart(BigDecimal.valueOf(offset, 3)); ffmpegTask.setOffsetStart(BigDecimal.valueOf(offset, 3));
// 使用时间戳和线程ID确保输出文件名唯一性,避免并发冲突 // 使用时间戳和线程ID确保输出文件名唯一性,避免并发冲突
String uniqueSuffix = System.currentTimeMillis() + "_" + Thread.currentThread().getId(); String uniqueSuffix = System.currentTimeMillis() + "_" + Thread.currentThread().threadId();
File outFile = new File(deviceId.toString() + "_" + faceSampleId + "_" + uniqueSuffix + ".mp4"); File outFile = new File(deviceId.toString() + "_" + faceSampleId + "_" + uniqueSuffix + ".mp4");
ffmpegTask.setOutputFile(outFile.getAbsolutePath()); ffmpegTask.setOutputFile(outFile.getAbsolutePath());
boolean result = startFfmpegTask(ffmpegTask); boolean result = startFfmpegTask(ffmpegTask);

View File

@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ycwl.basic.mapper.FaceSampleAssociationMapper">
<select id="findAssociatedSampleIds" resultType="java.lang.Long">
SELECT DISTINCT a2.face_sample_id
FROM face_sample_association a1
INNER JOIN face_sample_association a2
ON a1.scenic_id = a2.scenic_id
AND a1.group_key = a2.group_key
WHERE a1.scenic_id = #{scenicId}
AND a1.face_sample_id IN
<foreach collection="sampleIds" item="sampleId" open="(" separator="," close=")">
#{sampleId}
</foreach>
</select>
<insert id="batchInsertIgnore">
INSERT IGNORE INTO face_sample_association (scenic_id, group_key, face_sample_id)
VALUES
<foreach collection="list" item="item" separator=",">
(#{item.scenicId}, #{item.groupKey}, #{item.faceSampleId})
</foreach>
</insert>
<delete id="deleteByGroupAndSampleIds">
DELETE FROM face_sample_association
WHERE scenic_id = #{scenicId}
AND group_key = #{groupKey}
AND face_sample_id IN
<foreach collection="sampleIds" item="sampleId" open="(" separator="," close=")">
#{sampleId}
</foreach>
</delete>
<delete id="deleteByGroup">
DELETE FROM face_sample_association
WHERE scenic_id = #{scenicId}
AND group_key = #{groupKey}
</delete>
<select id="listSampleIdsByGroup" resultType="java.lang.Long">
SELECT face_sample_id
FROM face_sample_association
WHERE scenic_id = #{scenicId}
AND group_key = #{groupKey}
ORDER BY create_at ASC
</select>
<select id="listGroupKeysByScenicId" resultType="java.lang.String">
SELECT DISTINCT group_key
FROM face_sample_association
WHERE scenic_id = #{scenicId}
ORDER BY group_key ASC
</select>
</mapper>

View File

@@ -115,6 +115,13 @@
SELECT 5 as type, gr.template_id as id, pt.scenic_id as scenic_id, gr.result_image_url as url, gr.face_id SELECT 5 as type, gr.template_id as id, pt.scenic_id as scenic_id, gr.result_image_url as url, gr.face_id
FROM puzzle_generation_record gr FROM puzzle_generation_record gr
left join puzzle_template pt on gr.template_id = pt.id left join puzzle_template pt on gr.template_id = pt.id
),
member_single_photo_data AS (
SELECT ms.source_id, ms.face_id, f.face_url, s.url
FROM member_source ms
LEFT JOIN face f ON ms.face_id = f.id
LEFT JOIN source s ON ms.source_id = s.id
WHERE s.id IS NOT NULL AND ms.deleted = 0
) )
SELECT SELECT
oi.id AS oiId, oi.id AS oiId,
@@ -136,6 +143,7 @@
WHEN '4' THEN '一体机照片打印' WHEN '4' THEN '一体机照片打印'
WHEN '5' THEN 'pLog' WHEN '5' THEN 'pLog'
WHEN '13' THEN '打卡点拍照' WHEN '13' THEN '打卡点拍照'
WHEN '14' THEN '单张照片'
ELSE '其他' ELSE '其他'
END AS goods_name, END AS goods_name,
CASE oi.goods_type CASE oi.goods_type
@@ -143,12 +151,14 @@
WHEN '1' THEN oi.goods_id WHEN '1' THEN oi.goods_id
WHEN '2' THEN oi.goods_id WHEN '2' THEN oi.goods_id
WHEN '13' THEN oi.goods_id WHEN '13' THEN oi.goods_id
WHEN '14' THEN mspd.face_id
END AS face_id, END AS face_id,
CASE oi.goods_type CASE oi.goods_type
WHEN '0' THEN mvd.face_url WHEN '0' THEN mvd.face_url
WHEN '1' THEN msd.face_url WHEN '1' THEN msd.face_url
WHEN '2' THEN msd.face_url WHEN '2' THEN msd.face_url
WHEN '13' THEN msac.face_url WHEN '13' THEN msac.face_url
WHEN '14' THEN mspd.face_url
END AS face_url, END AS face_url,
CASE oi.goods_type CASE oi.goods_type
WHEN '0' THEN mvd.video_url WHEN '0' THEN mvd.video_url
@@ -161,6 +171,7 @@
WHEN '4' THEN mpa.url WHEN '4' THEN mpa.url
WHEN '5' THEN mpl.url WHEN '5' THEN mpl.url
WHEN '13' THEN msac.url WHEN '13' THEN msac.url
WHEN '14' THEN mspd.url
END AS imgUrl END AS imgUrl
FROM order_item oi FROM order_item oi
LEFT JOIN `order` o ON oi.order_id = o.id LEFT JOIN `order` o ON oi.order_id = o.id
@@ -170,6 +181,7 @@
LEFT JOIN member_photo_data mpd ON oi.goods_id = mpd.id AND mpd.type = oi.goods_type LEFT JOIN member_photo_data mpd ON oi.goods_id = mpd.id AND mpd.type = oi.goods_type
LEFT JOIN member_aio_photo_data mpa ON oi.goods_id = mpa.id AND mpa.type = oi.goods_type LEFT JOIN member_aio_photo_data mpa ON oi.goods_id = mpa.id AND mpa.type = oi.goods_type
LEFT JOIN member_plog_data mpl ON (oi.goods_id = mpl.id OR oi.goods_id = mpl.scenic_id) AND mpl.type = oi.goods_type AND o.face_id = mpl.face_id LEFT JOIN member_plog_data mpl ON (oi.goods_id = mpl.id OR oi.goods_id = mpl.scenic_id) AND mpl.type = oi.goods_type AND o.face_id = mpl.face_id
LEFT JOIN member_single_photo_data mspd ON oi.goods_id = mspd.source_id AND o.face_id = mspd.face_id AND oi.goods_type = 14
WHERE oi.order_id = #{id}; WHERE oi.order_id = #{id};
</select> </select>

View File

@@ -464,6 +464,15 @@
ORDER BY s.device_id ASC ORDER BY s.device_id ASC
</select> </select>
<select id="getMaxCreateTimeByFaceId" resultType="java.util.Date">
SELECT MAX(s.create_time)
FROM member_source ms
INNER JOIN source s ON ms.source_id = s.id
WHERE ms.face_id = #{faceId}
AND s.type = 2
AND ms.deleted = 0
</select>
<select id="getSourceByFaceAndDeviceId" resultType="com.ycwl.basic.model.pc.source.entity.SourceEntity"> <select id="getSourceByFaceAndDeviceId" resultType="com.ycwl.basic.model.pc.source.entity.SourceEntity">
SELECT s.* SELECT s.*
FROM source s FROM source s