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")
public ApiResponse<Map<String, Object>> createVirtualOrder(@RequestBody CreateVirtualOrderRequest request) {
if (request.getSourceIds() == null || request.getSourceIds().isEmpty()) {
return ApiResponse.fail("sourceIds不能为空");
}
try {
Map<String, Object> result = printerService.createVirtualOrder(
request.getSourceId(),
Map<String, Object> result = printerService.createBatchVirtualOrder(
request.getSourceIds(),
request.getScenicId(),
request.getPrinterId(),
request.getNeedEnhance(),

View File

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

View File

@@ -66,6 +66,8 @@ public class FaceMatchingPipelineFactory {
private FilterByDevicePhotoLimitStage filterByDevicePhotoLimitStage;
@Autowired
private DeleteOldRelationsStage deleteOldRelationsStage;
@Autowired
private ExpandSampleAssociationStage expandSampleAssociationStage;
// ==================== 辅助服务 ====================
@Autowired
@@ -91,13 +93,16 @@ public class FaceMatchingPipelineFactory {
// 3. 人脸识别补救
builder.addStage(faceRecoveryStage);
// 4. 更新人脸结果(落库)
// 4. 关联样本扩展
builder.addStage(expandSampleAssociationStage);
// 5. 更新人脸结果(落库)
builder.addStage(updateFaceResultStage);
// 5. 构建源文件关联(建关系)
// 6. 构建源文件关联(建关系)
builder.addStage(buildSourceRelationStage);
// 6. 持久化关联关系(建关系)
// 7. 持久化关联关系(建关系)
builder.addStage(persistRelationsStage);
log.debug("创建打印机大屏识别试点Pipeline: stageCount={}", builder.build().getStageCount());
@@ -131,28 +136,31 @@ public class FaceMatchingPipelineFactory {
// 5. 人脸识别补救
builder.addStage(faceRecoveryStage);
// 6. 更新人脸结果
// 6. 关联样本扩展
builder.addStage(expandSampleAssociationStage);
// 7. 更新人脸结果
builder.addStage(updateFaceResultStage);
// 7. 构建源文件关联
// 8. 构建源文件关联
builder.addStage(buildSourceRelationStage);
// 8. 处理免费源文件逻辑
// 9. 处理免费源文件逻辑
builder.addStage(processFreeSourceStage);
// 9. 处理购买状态
// 10. 处理购买状态
builder.addStage(processBuyStatusStage);
// 10. 处理视频重切
// 11. 处理视频重切
builder.addStage(handleVideoRecreationStage);
// 11. 持久化关联关系
// 12. 持久化关联关系
builder.addStage(persistRelationsStage);
// 12. 创建任务
// 13. 创建任务
builder.addStage(createTaskStage);
// 13. 异步生成拼图模板
// 14. 异步生成拼图模板
builder.addStage(generatePuzzleStage);
log.debug("创建自动人脸匹配Pipeline: isNew={}, stageCount={}", isNew, builder.build().getStageCount());
@@ -189,28 +197,31 @@ public class FaceMatchingPipelineFactory {
// 7. 应用设备照片数量限制筛选
builder.addStage(filterByDevicePhotoLimitStage);
// 8. 更新人脸结果
// 8. 关联样本扩展
builder.addStage(expandSampleAssociationStage);
// 9. 更新人脸结果
builder.addStage(updateFaceResultStage);
// 9. 删除旧关系数据
// 10. 删除旧关系数据
builder.addStage(deleteOldRelationsStage);
// 10. 构建源文件关联
// 11. 构建源文件关联
builder.addStage(buildSourceRelationStage);
// 11. 处理免费源文件逻辑
// 12. 处理免费源文件逻辑
builder.addStage(processFreeSourceStage);
// 12. 处理购买状态
// 13. 处理购买状态
builder.addStage(processBuyStatusStage);
// 13. 处理视频重切
// 14. 处理视频重切
builder.addStage(handleVideoRecreationStage);
// 14. 持久化关联关系
// 15. 持久化关联关系
builder.addStage(persistRelationsStage);
// 15. 创建任务
// 16. 创建任务
builder.addStage(createTaskStage);
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 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.PuzzleGenerateResponse;
import com.ycwl.basic.puzzle.dto.PuzzleTemplateDTO;
@@ -44,6 +45,9 @@ public class PuzzleGenerationOrchestrator {
@Autowired
private ScenicRepository scenicRepository;
@Autowired
private SourceMapper sourceMapper;
/**
* 异步生成景区所有启用的拼图模板
*
@@ -74,6 +78,11 @@ public class PuzzleGenerationOrchestrator {
// 3. 准备公共动态数据
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. 使用虚拟线程池并行生成所有模板
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);
@@ -121,7 +130,6 @@ public class PuzzleGenerationOrchestrator {
baseDynamicData.put("faceId", String.valueOf(faceId));
baseDynamicData.put("scenicName", scenicBasic.getName());
baseDynamicData.put("scenicText", scenicBasic.getName());
baseDynamicData.put("dateStr", DateUtil.format(new Date(), "yyyy.MM.dd"));
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.util.Date;
import java.util.List;
import java.util.stream.Collectors;
/**
@@ -67,8 +68,14 @@ public class UpdateFaceResultStage extends AbstractPipelineStage<FaceMatchingCon
faceEntity.setFirstMatchRate(BigDecimal.valueOf(searchResult.getFirstMatchRate()));
}
if (searchResult.getSampleListIds() != null) {
faceEntity.setMatchSampleIds(searchResult.getSampleListIds().stream()
// 优先使用 context.sampleListIds(可能包含关联扩展的ID),
// 回退到 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)
.collect(Collectors.joining(",")));
}
@@ -83,7 +90,7 @@ public class UpdateFaceResultStage extends AbstractPipelineStage<FaceMatchingCon
log.debug("人脸结果更新成功:faceId={}, score={}, sampleCount={}",
faceId, searchResult.getScore(),
searchResult.getSampleListIds() != null ? searchResult.getSampleListIds().size() : 0);
finalSampleIds != null ? finalSampleIds.size() : 0);
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);
/**
* 获取faceId关联的图片素材的最大创建时间
* @param faceId 人脸ID
* @return 最大创建时间,无记录时返回null
*/
Date getMaxCreateTimeByFaceId(Long faceId);
/**
* 根据faceId和设备ID获取source
* @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 java.util.List;
/**
* 创建虚拟用户0元订单请求参数
*/
@Data
public class CreateVirtualOrderRequest {
/**
* source记录ID
* source记录ID列表(支持单个或多个sourceId聚合为一笔订单)
*/
private Long sourceId;
private List<Long> sourceIds;
/**
* 景区ID

View File

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

View File

@@ -59,9 +59,10 @@ public interface ICouponService {
* 查询用户可用优惠券(包含领取记录信息)
*
* @param userId 用户ID
* @param scenicId 景区ID,传入时仅返回该景区可用的优惠券,NULL时返回全部
* @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
public List<UserCouponResp> getUserAvailableCoupons(Long userId) {
public List<UserCouponResp> getUserAvailableCoupons(Long userId, String scenicId) {
List<PriceCouponClaimRecord> records = couponClaimRecordMapper.selectUserAvailableCoupons(userId);
List<UserCouponResp> coupons = new ArrayList<>();
for (PriceCouponClaimRecord record : records) {
PriceCouponConfig config = couponConfigMapper.selectById(record.getCouponId());
if (config != null) {
coupons.add(buildUserCouponResp(record, config));
if (config == null) {
continue;
}
if (scenicId != null && config.getScenicId() != null && !scenicId.equals(config.getScenicId())) {
continue;
}
coupons.add(buildUserCouponResp(record, config));
}
return coupons;

View File

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

View File

@@ -40,7 +40,7 @@ public class SourceRepository {
Runtime.getRuntime().availableProcessors(),
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);
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.entity.SearchFaceResultItem;
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.ProjectMapper;
import com.ycwl.basic.mapper.SourceMapper;
@@ -101,6 +102,7 @@ import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
@@ -161,6 +163,8 @@ public class FaceServiceImpl implements FaceService {
@Autowired
private FaceSampleMapper faceSampleMapper;
@Autowired
private FaceSampleAssociationMapper faceSampleAssociationMapper;
@Autowired
private GoodsService goodsService;
@Autowired
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 方法
@@ -483,25 +518,14 @@ public class FaceServiceImpl implements FaceService {
// 摄影师拍照
List<DeviceV2DTO> deviceList = deviceRepository.getAllDeviceByScenicId(face.getScenicId());
List<SourceEntity> sourceEntityList = sourceMapper.listSourceByFaceRelation(face.getId(), 2);
for (SourceEntity sourceEntity : sourceEntityList) {
ContentPageVO content = new ContentPageVO();
content.setName("摄影师拍照");
deviceList.stream().filter(device -> device.getId().equals(sourceEntity.getDeviceId())).findFirst().ifPresent(device -> {
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());
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 -> {
List<MemberSourceEntity> memberSourceRelations = memberRelationRepository.listSourceByFaceRelation(face.getId(), 2);
// 按 deviceList 顺序排列结果
Set<Long> deviceIds = deviceList.stream().map(DeviceV2DTO::getId).collect(Collectors.toSet());
for (DeviceV2DTO device : deviceList) {
List<SourceEntity> deviceSources = sourceEntityList.stream()
.filter(s -> device.getId().equals(s.getDeviceId()))
.toList();
if (deviceSources.isEmpty()) {
ContentPageVO content = new ContentPageVO();
content.setName(device.getName());
content.setGroup(device.getName());
@@ -514,7 +538,46 @@ public class FaceServiceImpl implements FaceService {
content.setIsBuy(0);
content.setLockType(1);
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;
}
List<TemplateRespVO> templateList = templateRepository.getTemplateListByScenicId(face.getScenicId());
@@ -1057,6 +1120,9 @@ public class FaceServiceImpl implements FaceService {
allFaceSampleList.size(), filteredSampleIds.size());
}
// 4. 关联样本扩展
expandSampleAssociation(mergedResult, face.getScenicId(), faceId);
updateFaceEntityResult(face, mergedResult, faceId);
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.printer.resp.MemberPrintResp;
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.template.resp.TemplateRespVO;
import com.ycwl.basic.model.pc.video.entity.VideoEntity;
@@ -423,6 +424,21 @@ public class OrderServiceImpl implements OrderService {
goods.setTemplateCoverUrl(item.getCoverUrl());
goods.setScenicId(order.getScenicId());
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 {
item.setCoverList(Collections.singletonList(item.getCoverUrl()));
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.integration.common.manager.ScenicConfigManager;
import com.ycwl.basic.mapper.FaceMapper;
import com.ycwl.basic.mapper.FaceSampleAssociationMapper;
import com.ycwl.basic.mapper.SourceMapper;
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
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 java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -107,6 +111,8 @@ public class FaceMatchingOrchestrator {
private FaceStatusManager faceStatusManager;
@Autowired
private PuzzleRepository puzzleRepository;
@Autowired
private FaceSampleAssociationMapper faceSampleAssociationMapper;
/**
* 编排人脸匹配的完整流程
@@ -146,6 +152,9 @@ public class FaceMatchingOrchestrator {
searchResult = faceRecoveryStrategy.executeFaceRecoveryLogic(
searchResult, context.scenicConfig, context.faceBodyAdapter, context.face.getScenicId());
// 关联样本扩展
expandSampleAssociation(searchResult, context.face.getScenicId(), faceId);
// 步骤3: 更新人脸结果
updateFaceResult(context.face, searchResult, faceId);
@@ -396,7 +405,9 @@ public class FaceMatchingOrchestrator {
baseDynamicData.put("faceId", String.valueOf(faceId));
baseDynamicData.put("scenicName", 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
.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();
photoItem.setProductType(ProductType.PHOTO_PRINT);
photoItem.setProductId(scenicId.toString());
photoItem.setQuantity(sourceIds.size());
photoItem.setQuantity(1);
photoItem.setPurchaseCount(sourceIds.size());
photoItem.setScenicId(scenicId.toString());
productItems.add(photoItem);

View File

@@ -85,7 +85,7 @@ public class DownloadNotificationTasker {
}
variables.put("videoDeviceCount", videoTaskRepository.getTaskDeviceNum(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()
.scenicId(item.getScenicId())
.memberId(item.getMemberId())
@@ -137,7 +137,7 @@ public class DownloadNotificationTasker {
} else {
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("videoLensCount", videoTaskRepository.getTaskLensNum(item.getTaskId()));
variables.put("videoShotTime", DateUtil.format(videoTaskRepository.getTaskShotDate(item.getTaskId()), "yyyy-MM-dd HH:mm"));
@@ -191,7 +191,7 @@ public class DownloadNotificationTasker {
} else {
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("videoLensCount", videoTaskRepository.getTaskLensNum(item.getTaskId()));
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());
List<MemberSourceEntity> sourceVideoList = memberRelationRepository.listSourceByFaceRelation(item.getId(), 1);
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);
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()
.scenicId(item.getScenicId())

View File

@@ -342,7 +342,7 @@ public class VideoPieceGetter {
ffmpegTask.setDuration(duration);
ffmpegTask.setOffsetStart(BigDecimal.valueOf(offset, 3));
// 使用时间戳和线程ID确保输出文件名唯一性,避免并发冲突
String uniqueSuffix = System.currentTimeMillis() + "_" + Thread.currentThread().getId();
String uniqueSuffix = System.currentTimeMillis() + "_" + Thread.currentThread().threadId();
File outFile = new File(deviceId.toString() + "_" + faceSampleId + "_" + uniqueSuffix + ".mp4");
ffmpegTask.setOutputFile(outFile.getAbsolutePath());
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
FROM puzzle_generation_record gr
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
oi.id AS oiId,
@@ -136,6 +143,7 @@
WHEN '4' THEN '一体机照片打印'
WHEN '5' THEN 'pLog'
WHEN '13' THEN '打卡点拍照'
WHEN '14' THEN '单张照片'
ELSE '其他'
END AS goods_name,
CASE oi.goods_type
@@ -143,12 +151,14 @@
WHEN '1' THEN oi.goods_id
WHEN '2' THEN oi.goods_id
WHEN '13' THEN oi.goods_id
WHEN '14' THEN mspd.face_id
END AS face_id,
CASE oi.goods_type
WHEN '0' THEN mvd.face_url
WHEN '1' THEN msd.face_url
WHEN '2' THEN msd.face_url
WHEN '13' THEN msac.face_url
WHEN '14' THEN mspd.face_url
END AS face_url,
CASE oi.goods_type
WHEN '0' THEN mvd.video_url
@@ -161,6 +171,7 @@
WHEN '4' THEN mpa.url
WHEN '5' THEN mpl.url
WHEN '13' THEN msac.url
WHEN '14' THEN mspd.url
END AS imgUrl
FROM order_item oi
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_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_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};
</select>

View File

@@ -464,6 +464,15 @@
ORDER BY s.device_id ASC
</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 s.*
FROM source s