You've already forked FrameTour-BE
Compare commits
28 Commits
3cf7c81162
...
24bbb63bf7
| Author | SHA1 | Date | |
|---|---|---|---|
| 24bbb63bf7 | |||
| ee13ef09f7 | |||
| 33c3a194ca | |||
| 71a8d3b539 | |||
| 82626f615b | |||
| de2eadf764 | |||
| fd143830d3 | |||
| 68916f3f53 | |||
| e27ed7d971 | |||
| 7a19f18962 | |||
| eade5f8092 | |||
| 42540e2dc4 | |||
| 15dda645b9 | |||
| 17419d83e7 | |||
| ae92ba10a7 | |||
| af60cc1540 | |||
| 60b4473279 | |||
| ecd5378b26 | |||
| 8c08c8947e | |||
| b165840176 | |||
| 71d6400a1e | |||
| b3fa10e8fd | |||
| 96e75a458f | |||
| d2ad14175d | |||
| 06c0ade9b4 | |||
| 36f85dbb63 | |||
| 9becd6bfa7 | |||
| 788184fc04 |
@@ -1,6 +1,5 @@
|
|||||||
package com.ycwl.basic;
|
package com.ycwl.basic;
|
||||||
|
|
||||||
import org.mybatis.spring.annotation.MapperScan;
|
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
|
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
|
||||||
@@ -9,8 +8,6 @@ import org.springframework.cloud.openfeign.EnableFeignClients;
|
|||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
@EnableDiscoveryClient
|
@EnableDiscoveryClient
|
||||||
@EnableFeignClients
|
@EnableFeignClients
|
||||||
@MapperScan(basePackages = "com.ycwl.basic.mapper")
|
|
||||||
@MapperScan(basePackages = "com.ycwl.basic.*.mapper")
|
|
||||||
public class Application {
|
public class Application {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
|
|||||||
@@ -118,6 +118,11 @@ public class PriceBiz {
|
|||||||
goodsList.add(new SimpleGoodsRespVO(scenicId, "照片集", productType));
|
goodsList.add(new SimpleGoodsRespVO(scenicId, "照片集", productType));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "AI_CAM_PHOTO_SET":
|
||||||
|
// 返回固定的照片集条目
|
||||||
|
goodsList.add(new SimpleGoodsRespVO(scenicId, "AI微单照片集", productType));
|
||||||
|
break;
|
||||||
|
|
||||||
case "PHOTO_LOG":
|
case "PHOTO_LOG":
|
||||||
// 从 template 表查询pLog模板
|
// 从 template 表查询pLog模板
|
||||||
List<PuzzleTemplateEntity> puzzleList = puzzleTemplateMapper.list(scenicId, null, null);
|
List<PuzzleTemplateEntity> puzzleList = puzzleTemplateMapper.list(scenicId, null, null);
|
||||||
|
|||||||
@@ -4,45 +4,23 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||||
import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
|
import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
|
||||||
import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator;
|
import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator;
|
||||||
import org.springframework.cache.annotation.CachingConfigurerSupport;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.cache.annotation.EnableCaching;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.data.redis.cache.RedisCacheConfiguration;
|
|
||||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
|
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
|
||||||
import org.springframework.data.redis.serializer.RedisSerializationContext;
|
|
||||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||||
|
|
||||||
import java.time.Duration;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author wenshijia
|
* @author wenshijia
|
||||||
* @date 2021年07月05日 18:34
|
* @date 2021年07月05日 18:34
|
||||||
* 修改redis缓存序列化器
|
* 修改redis缓存序列化器
|
||||||
*/
|
*/
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableCaching
|
public class CustomRedisCacheManager {
|
||||||
public class CustomRedisCacheManager extends CachingConfigurerSupport {
|
@Autowired
|
||||||
|
private ObjectMapper objectMapper;
|
||||||
@Bean
|
|
||||||
public RedisCacheConfiguration redisCacheConfiguration() {
|
|
||||||
ObjectMapper objectMapper = new ObjectMapper();
|
|
||||||
objectMapper.registerModule(new JavaTimeModule());
|
|
||||||
|
|
||||||
// Configure type handling to prevent ClassCastException
|
|
||||||
PolymorphicTypeValidator typeValidator = BasicPolymorphicTypeValidator.builder()
|
|
||||||
.allowIfBaseType(Object.class)
|
|
||||||
.build();
|
|
||||||
objectMapper.activateDefaultTyping(typeValidator, ObjectMapper.DefaultTyping.NON_FINAL);
|
|
||||||
|
|
||||||
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(objectMapper, Object.class);
|
|
||||||
|
|
||||||
RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig();
|
|
||||||
configuration = configuration.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)).entryTtl(Duration.ofMinutes(1));
|
|
||||||
return configuration;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理redis连接工具显示redis key值显示乱码问题,value值没处理
|
* 处理redis连接工具显示redis key值显示乱码问题,value值没处理
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.ycwl.basic.config;
|
|||||||
import com.baomidou.mybatisplus.annotation.DbType;
|
import com.baomidou.mybatisplus.annotation.DbType;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||||
|
import org.mybatis.spring.annotation.MapperScan;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
@@ -11,6 +12,15 @@ import org.springframework.context.annotation.Configuration;
|
|||||||
* @date 2021年06月04日 9:42
|
* @date 2021年06月04日 9:42
|
||||||
*/
|
*/
|
||||||
@Configuration
|
@Configuration
|
||||||
|
@MapperScan(basePackages = {
|
||||||
|
"com.ycwl.basic.mapper",
|
||||||
|
"com.ycwl.basic.order.mapper",
|
||||||
|
"com.ycwl.basic.pricing.mapper",
|
||||||
|
"com.ycwl.basic.product.mapper",
|
||||||
|
"com.ycwl.basic.profitsharing.mapper",
|
||||||
|
"com.ycwl.basic.puzzle.mapper",
|
||||||
|
"com.ycwl.basic.stats.mapper"
|
||||||
|
})
|
||||||
public class MybatisPlusPageConfig {
|
public class MybatisPlusPageConfig {
|
||||||
|
|
||||||
/* 旧版本配置
|
/* 旧版本配置
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ import cn.hutool.http.HttpUtil;
|
|||||||
import com.ycwl.basic.annotation.IgnoreToken;
|
import com.ycwl.basic.annotation.IgnoreToken;
|
||||||
import com.ycwl.basic.image.enhancer.adapter.BceImageEnhancer;
|
import com.ycwl.basic.image.enhancer.adapter.BceImageEnhancer;
|
||||||
import com.ycwl.basic.image.enhancer.entity.BceEnhancerConfig;
|
import com.ycwl.basic.image.enhancer.entity.BceEnhancerConfig;
|
||||||
import com.ycwl.basic.image.pipeline.core.Pipeline;
|
|
||||||
import com.ycwl.basic.image.pipeline.core.PipelineBuilder;
|
|
||||||
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
||||||
import com.ycwl.basic.image.pipeline.stages.DownloadStage;
|
import com.ycwl.basic.image.pipeline.stages.DownloadStage;
|
||||||
import com.ycwl.basic.image.pipeline.stages.ImageEnhanceStage;
|
import com.ycwl.basic.image.pipeline.stages.ImageEnhanceStage;
|
||||||
import com.ycwl.basic.image.pipeline.stages.ImageSRStage;
|
import com.ycwl.basic.image.pipeline.stages.ImageSRStage;
|
||||||
import com.ycwl.basic.image.pipeline.stages.SourcePhotoUpdateStage;
|
import com.ycwl.basic.image.pipeline.stages.SourcePhotoUpdateStage;
|
||||||
import com.ycwl.basic.image.pipeline.stages.CleanupStage;
|
import com.ycwl.basic.image.pipeline.stages.CleanupStage;
|
||||||
|
import com.ycwl.basic.pipeline.core.Pipeline;
|
||||||
|
import com.ycwl.basic.pipeline.core.PipelineBuilder;
|
||||||
import com.ycwl.basic.mapper.AioDeviceMapper;
|
import com.ycwl.basic.mapper.AioDeviceMapper;
|
||||||
import com.ycwl.basic.mapper.MemberMapper;
|
import com.ycwl.basic.mapper.MemberMapper;
|
||||||
import com.ycwl.basic.model.aio.entity.AioDeviceBannerEntity;
|
import com.ycwl.basic.model.aio.entity.AioDeviceBannerEntity;
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ public class AppScenicController {
|
|||||||
resp.setPrintEnableManual(scenicConfig.getBoolean("print_enable_manual", true));
|
resp.setPrintEnableManual(scenicConfig.getBoolean("print_enable_manual", true));
|
||||||
resp.setSceneMode(scenicConfig.getInteger("scene_mode", 0));
|
resp.setSceneMode(scenicConfig.getInteger("scene_mode", 0));
|
||||||
resp.setPrintEnable(scenicConfig.getBoolean("print_enable", false));
|
resp.setPrintEnable(scenicConfig.getBoolean("print_enable", false));
|
||||||
|
resp.setShowMyPagePaid(scenicConfig.getBoolean("show_my_page_paid", true));
|
||||||
|
resp.setShowMyPageUnpaid(scenicConfig.getBoolean("show_my_page_unpaid", true));
|
||||||
return ApiResponse.success(resp);
|
return ApiResponse.success(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,9 +53,9 @@ public class DeviceV2Controller {
|
|||||||
if (pageSize > 100) {
|
if (pageSize > 100) {
|
||||||
pageSize = 100;
|
pageSize = 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
PageResponse<DeviceV2DTO> response = deviceIntegrationService.listDevices(page, pageSize, name, no, type, isActive, scenicId);
|
PageResponse<DeviceV2DTO> response = deviceIntegrationService.listDevices(page, pageSize, name, no, type, isActive, scenicId, null);
|
||||||
return ApiResponse.success(response);
|
return ApiResponse.success(response);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("分页查询设备核心信息列表失败", e);
|
log.error("分页查询设备核心信息列表失败", e);
|
||||||
@@ -380,7 +380,7 @@ public class DeviceV2Controller {
|
|||||||
@RequestParam(defaultValue = "10") Integer pageSize) {
|
@RequestParam(defaultValue = "10") Integer pageSize) {
|
||||||
log.info("获取景区所有设备列表, scenicId: {}, page: {}, pageSize: {}", scenicId, page, pageSize);
|
log.info("获取景区所有设备列表, scenicId: {}, page: {}, pageSize: {}", scenicId, page, pageSize);
|
||||||
try {
|
try {
|
||||||
PageResponse<DeviceV2DTO> response = deviceIntegrationService.listDevices(page, pageSize, name, no, type, null, scenicId);
|
PageResponse<DeviceV2DTO> response = deviceIntegrationService.listDevices(page, pageSize, name, no, type, null, scenicId, null);
|
||||||
return ApiResponse.success(response);
|
return ApiResponse.success(response);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("获取景区所有设备列表失败, scenicId: {}", scenicId, e);
|
log.error("获取景区所有设备列表失败, scenicId: {}", scenicId, e);
|
||||||
|
|||||||
@@ -60,9 +60,9 @@ public class ScenicV2Controller {
|
|||||||
if (pageSize > 100) {
|
if (pageSize > 100) {
|
||||||
pageSize = 100;
|
pageSize = 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
PageResponse<ScenicV2DTO> response = scenicIntegrationService.listScenics(page, pageSize, status, name);
|
PageResponse<ScenicV2DTO> response = scenicIntegrationService.listScenics(page, pageSize, status, name, null);
|
||||||
return ApiResponse.success(response);
|
return ApiResponse.success(response);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("分页查询景区核心信息列表失败", e);
|
log.error("分页查询景区核心信息列表失败", e);
|
||||||
@@ -156,7 +156,7 @@ public class ScenicV2Controller {
|
|||||||
log.info("查询景区列表, status: {}", status);
|
log.info("查询景区列表, status: {}", status);
|
||||||
try {
|
try {
|
||||||
// 默认查询1000条数据,第1页
|
// 默认查询1000条数据,第1页
|
||||||
PageResponse<ScenicV2DTO> scenics = scenicIntegrationService.listScenics(1, 1000, status, null);
|
PageResponse<ScenicV2DTO> scenics = scenicIntegrationService.listScenics(1, 1000, status, null, null);
|
||||||
return ApiResponse.success(scenics);
|
return ApiResponse.success(scenics);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("查询景区列表失败, status: {}", status, e);
|
log.error("查询景区列表失败, status: {}", status, e);
|
||||||
|
|||||||
@@ -1,34 +1,55 @@
|
|||||||
package com.ycwl.basic.controller.printer;
|
package com.ycwl.basic.controller.printer;
|
||||||
|
|
||||||
|
|
||||||
|
import cn.hutool.core.date.DateUtil;
|
||||||
|
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
|
||||||
|
import com.ycwl.basic.mapper.FaceMapper;
|
||||||
|
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
||||||
|
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
|
||||||
|
import com.ycwl.basic.model.task.resp.SearchFaceRespVo;
|
||||||
|
import com.ycwl.basic.repository.MemberRelationRepository;
|
||||||
|
import com.ycwl.basic.repository.SourceRepository;
|
||||||
|
import com.ycwl.basic.service.printer.PrinterService;
|
||||||
|
import com.ycwl.basic.service.task.TaskFaceService;
|
||||||
|
import com.ycwl.basic.storage.adapters.IStorageAdapter;
|
||||||
import com.ycwl.basic.annotation.IgnoreToken;
|
import com.ycwl.basic.annotation.IgnoreToken;
|
||||||
|
import com.ycwl.basic.storage.StorageFactory;
|
||||||
import com.ycwl.basic.integration.device.dto.device.DeviceV2DTO;
|
import com.ycwl.basic.integration.device.dto.device.DeviceV2DTO;
|
||||||
import com.ycwl.basic.integration.device.service.DeviceIntegrationService;
|
|
||||||
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.model.pc.faceSample.entity.FaceSampleEntity;
|
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
|
||||||
import com.ycwl.basic.model.pc.mp.MpConfigEntity;
|
import com.ycwl.basic.model.pc.mp.MpConfigEntity;
|
||||||
import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery;
|
import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery;
|
||||||
|
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
|
||||||
|
import com.ycwl.basic.model.printer.FaceRecognizeWithSourcesResp;
|
||||||
import com.ycwl.basic.repository.DeviceRepository;
|
import com.ycwl.basic.repository.DeviceRepository;
|
||||||
import com.ycwl.basic.repository.FaceRepository;
|
import com.ycwl.basic.repository.FaceRepository;
|
||||||
import com.ycwl.basic.repository.ScenicRepository;
|
import com.ycwl.basic.repository.ScenicRepository;
|
||||||
|
import com.ycwl.basic.service.pc.FaceService;
|
||||||
import com.ycwl.basic.service.pc.ScenicService;
|
import com.ycwl.basic.service.pc.ScenicService;
|
||||||
|
import com.ycwl.basic.storage.utils.StorageUtil;
|
||||||
import com.ycwl.basic.utils.ApiResponse;
|
import com.ycwl.basic.utils.ApiResponse;
|
||||||
|
import com.ycwl.basic.utils.SnowFlakeUtil;
|
||||||
import com.ycwl.basic.utils.WxMpUtil;
|
import com.ycwl.basic.utils.WxMpUtil;
|
||||||
import jakarta.websocket.server.PathParam;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static com.ycwl.basic.constant.StorageConstant.USER_FACE;
|
||||||
|
|
||||||
@IgnoreToken
|
@IgnoreToken
|
||||||
// 打印机大屏对接接口
|
// 打印机大屏对接接口
|
||||||
@@ -40,6 +61,14 @@ public class PrinterTvController {
|
|||||||
private final DeviceRepository deviceRepository;
|
private final DeviceRepository deviceRepository;
|
||||||
private final ScenicRepository scenicRepository;
|
private final ScenicRepository scenicRepository;
|
||||||
private final FaceRepository faceRepository;
|
private final FaceRepository faceRepository;
|
||||||
|
private final TaskFaceService faceService;
|
||||||
|
private final FaceService pcFaceService;
|
||||||
|
private final ScenicService scenicService;
|
||||||
|
private final SourceMapper sourceMapper;
|
||||||
|
private final FaceMapper faceMapper;
|
||||||
|
private final MemberRelationRepository memberRelationRepository;
|
||||||
|
private final SourceRepository sourceRepository;
|
||||||
|
private final PrinterService printerService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取景区列表
|
* 获取景区列表
|
||||||
@@ -100,4 +129,142 @@ public class PrinterTvController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取人脸绑定二维码
|
||||||
|
* 生成小程序二维码,用于绑定人脸到用户账号
|
||||||
|
*
|
||||||
|
* @param faceId 人脸ID
|
||||||
|
* @param response HTTP响应
|
||||||
|
*/
|
||||||
|
@GetMapping("/face/{faceId}/qrcode")
|
||||||
|
public void getFaceQrcode(@PathVariable("faceId") Long faceId, HttpServletResponse response) throws Exception {
|
||||||
|
File qrcode = new File("qrcode_face_" + faceId + ".jpg");
|
||||||
|
try {
|
||||||
|
FaceEntity face = faceRepository.getFace(faceId);
|
||||||
|
if (face == null) {
|
||||||
|
response.setStatus(404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
MpConfigEntity scenicMpConfig = scenicRepository.getScenicMpConfig(face.getScenicId());
|
||||||
|
if (scenicMpConfig == null) {
|
||||||
|
response.setStatus(500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
WxMpUtil.generateUnlimitedWXAQRCode(
|
||||||
|
scenicMpConfig.getAppId(),
|
||||||
|
scenicMpConfig.getAppSecret(),
|
||||||
|
"pages/videoSynthesis/bind_face",
|
||||||
|
faceId.toString(),
|
||||||
|
qrcode
|
||||||
|
);
|
||||||
|
|
||||||
|
// 设置响应头
|
||||||
|
response.setContentType("image/jpeg");
|
||||||
|
response.setHeader("Content-Disposition", "inline; filename=\"" + qrcode.getName() + "\"");
|
||||||
|
|
||||||
|
// 将二维码文件写入响应输出流
|
||||||
|
try (FileInputStream fis = new FileInputStream(qrcode);
|
||||||
|
OutputStream os = response.getOutputStream()) {
|
||||||
|
byte[] buffer = new byte[1024];
|
||||||
|
int bytesRead;
|
||||||
|
while ((bytesRead = fis.read(buffer)) != -1) {
|
||||||
|
os.write(buffer, 0, bytesRead);
|
||||||
|
}
|
||||||
|
os.flush();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// 删除临时文件
|
||||||
|
if (qrcode.exists()) {
|
||||||
|
qrcode.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据人脸样本ID查询图像素材
|
||||||
|
*
|
||||||
|
* @param faceSampleId 人脸样本ID
|
||||||
|
* @return type=2且face_sample_id匹配的source记录
|
||||||
|
*/
|
||||||
|
@GetMapping("/{faceSampleId}/source")
|
||||||
|
public ApiResponse<SourceEntity> getSourceByFaceSampleId(@PathVariable Long faceSampleId) {
|
||||||
|
SourceEntity source = sourceMapper.getBySampleIdAndType(faceSampleId, 2);
|
||||||
|
if (source == null) {
|
||||||
|
return ApiResponse.fail("未找到对应的图像素材");
|
||||||
|
}
|
||||||
|
return ApiResponse.success(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打印机大屏人脸识别
|
||||||
|
* 上传照片,在景区人脸库中搜索匹配的人脸样本,返回识别结果和匹配到的图像素材
|
||||||
|
*
|
||||||
|
* @param file 人脸照片文件
|
||||||
|
* @param scenicId 景区ID
|
||||||
|
* @return 人脸识别结果和匹配的source列表
|
||||||
|
*/
|
||||||
|
@PostMapping("/{scenicId}/faceRecognize")
|
||||||
|
public ApiResponse<FaceRecognizeWithSourcesResp> faceRecognize(
|
||||||
|
@RequestParam("file") MultipartFile file,
|
||||||
|
@PathVariable Long scenicId) throws Exception {
|
||||||
|
|
||||||
|
// 1. 上传人脸照片到存储
|
||||||
|
IStorageAdapter adapter = StorageFactory.use("faces");
|
||||||
|
String filePath = StorageUtil.joinPath(USER_FACE, DateUtil.format(new Date(), "yyyy-MM-dd"));
|
||||||
|
String originalFilename = file.getOriginalFilename();
|
||||||
|
String suffix = originalFilename.split("\\.", 2)[1];
|
||||||
|
String fileName = UUID.randomUUID() + "." + suffix;
|
||||||
|
String faceUrl = adapter.uploadFile(file, filePath, fileName);
|
||||||
|
|
||||||
|
// 2. 保存人脸数据到数据库
|
||||||
|
Long faceId = SnowFlakeUtil.getLongId();
|
||||||
|
FaceEntity faceEntity = new FaceEntity();
|
||||||
|
faceEntity.setId(faceId);
|
||||||
|
faceEntity.setScenicId(scenicId);
|
||||||
|
faceEntity.setFaceUrl(faceUrl);
|
||||||
|
faceEntity.setCreateAt(new Date());
|
||||||
|
faceEntity.setMemberId(0L); // 打印机大屏端没有用户ID
|
||||||
|
faceMapper.add(faceEntity);
|
||||||
|
|
||||||
|
// 3. 在景区人脸库中搜索(注意:这里使用scenicId作为数据库名,搜索的是景区内的人脸样本)
|
||||||
|
pcFaceService.matchFaceId(faceId);
|
||||||
|
|
||||||
|
// 4. 自动添加照片到打印列表,并获取添加成功的照片列表
|
||||||
|
List<SourceEntity> addedSources = printerService.autoAddPhotosToPreferPrint(faceId);
|
||||||
|
|
||||||
|
// 5. 根据自动添加结果决定返回的sources
|
||||||
|
List<SourceEntity> sources;
|
||||||
|
if (addedSources != null && !addedSources.isEmpty()) {
|
||||||
|
// 如果自动添加成功,返回添加的照片列表
|
||||||
|
sources = addedSources;
|
||||||
|
} else {
|
||||||
|
// 如果自动添加为空,按原逻辑查询匹配到的图像素材(type=2)
|
||||||
|
sources = new ArrayList<>();
|
||||||
|
List<MemberSourceEntity> memberSourceEntities = memberRelationRepository.listSourceByFaceRelation(faceId, 2);
|
||||||
|
for (MemberSourceEntity memberSourceEntity : memberSourceEntities) {
|
||||||
|
SourceEntity source = sourceRepository.getSource(memberSourceEntity.getSourceId());
|
||||||
|
if (source != null) {
|
||||||
|
sources.add(source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 构造响应
|
||||||
|
FaceRecognizeWithSourcesResp resp = new FaceRecognizeWithSourcesResp();
|
||||||
|
resp.setUrl(faceUrl);
|
||||||
|
resp.setFaceId(faceId);
|
||||||
|
resp.setScenicId(scenicId);
|
||||||
|
resp.setSources(sources);
|
||||||
|
// 只有当添加了照片时才返回二维码URL
|
||||||
|
if (addedSources != null && !addedSources.isEmpty()) {
|
||||||
|
resp.setQrcodeUrl("https://zhentuai.com/printer/v1/tv/face/" + faceId + "/qrcode");
|
||||||
|
} else {
|
||||||
|
resp.setQrcodeUrl(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiResponse.success(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ public class ZTSourceMessage {
|
|||||||
* 判断是否为照片
|
* 判断是否为照片
|
||||||
*/
|
*/
|
||||||
public boolean isPhoto() {
|
public boolean isPhoto() {
|
||||||
return sourceType != null && sourceType == 2;
|
return sourceType != null && (sourceType == 2 || sourceType == 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,290 @@
|
|||||||
|
package com.ycwl.basic.face.pipeline.core;
|
||||||
|
|
||||||
|
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
|
||||||
|
import com.ycwl.basic.face.pipeline.enums.FaceMatchingScene;
|
||||||
|
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
|
||||||
|
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
||||||
|
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
|
||||||
|
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
|
||||||
|
import com.ycwl.basic.model.task.resp.SearchFaceRespVo;
|
||||||
|
import com.ycwl.basic.pipeline.core.PipelineContext;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸匹配管线上下文
|
||||||
|
* 在各个Stage之间传递状态和数据
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class FaceMatchingContext implements PipelineContext {
|
||||||
|
|
||||||
|
// ==================== 核心字段(构造时必填)====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸ID(必填)
|
||||||
|
*/
|
||||||
|
private final Long faceId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否新用户
|
||||||
|
*/
|
||||||
|
private final boolean isNew;
|
||||||
|
|
||||||
|
// ==================== 场景标识 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 场景标识
|
||||||
|
*/
|
||||||
|
private FaceMatchingScene scene;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动选择的样本ID(自定义匹配场景)
|
||||||
|
*/
|
||||||
|
private List<Long> faceSampleIds;
|
||||||
|
|
||||||
|
// ==================== 中间状态 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸实体
|
||||||
|
*/
|
||||||
|
private FaceEntity face;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 景区配置管理器
|
||||||
|
*/
|
||||||
|
private ScenicConfigManager scenicConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸识别适配器
|
||||||
|
*/
|
||||||
|
private IFaceBodyAdapter faceBodyAdapter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸搜索结果
|
||||||
|
*/
|
||||||
|
private SearchFaceRespVo searchResult;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸样本列表(自定义匹配场景)
|
||||||
|
*/
|
||||||
|
private List<FaceSampleEntity> faceSamples;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 匹配到的样本ID列表
|
||||||
|
*/
|
||||||
|
private List<Long> sampleListIds;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 源文件关联列表
|
||||||
|
*/
|
||||||
|
private List<MemberSourceEntity> memberSourceList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 免费源文件ID列表
|
||||||
|
*/
|
||||||
|
private List<Long> freeSourceIds;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸选择后置模式配置(自定义匹配场景)
|
||||||
|
* 0: 并集, 1: 交集, 2: 直接使用
|
||||||
|
*/
|
||||||
|
private Integer faceSelectPostMode;
|
||||||
|
|
||||||
|
// ==================== 输出结果 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最终结果
|
||||||
|
*/
|
||||||
|
private SearchFaceRespVo finalResult;
|
||||||
|
|
||||||
|
// ==================== Stage配置 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stage开关配置表
|
||||||
|
* Key: stageId, Value: 是否启用
|
||||||
|
*/
|
||||||
|
private Map<String, Boolean> stageEnabledMap = new HashMap<>();
|
||||||
|
|
||||||
|
// ==================== 构造函数(私有)====================
|
||||||
|
|
||||||
|
private FaceMatchingContext(Builder builder) {
|
||||||
|
this.faceId = builder.faceId;
|
||||||
|
this.isNew = builder.isNew;
|
||||||
|
this.scene = builder.scene;
|
||||||
|
this.faceSampleIds = builder.faceSampleIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 静态工厂方法 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 Builder
|
||||||
|
*/
|
||||||
|
public static Builder builder() {
|
||||||
|
return new Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快速创建自动匹配场景Context
|
||||||
|
*/
|
||||||
|
public static FaceMatchingContext forAutoMatching(Long faceId, boolean isNew) {
|
||||||
|
return FaceMatchingContext.builder()
|
||||||
|
.faceId(faceId)
|
||||||
|
.isNew(isNew)
|
||||||
|
.scene(FaceMatchingScene.AUTO_MATCHING)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快速创建自定义匹配场景Context
|
||||||
|
*/
|
||||||
|
public static FaceMatchingContext forCustomMatching(Long faceId, List<Long> faceSampleIds) {
|
||||||
|
return FaceMatchingContext.builder()
|
||||||
|
.faceId(faceId)
|
||||||
|
.isNew(false)
|
||||||
|
.faceSampleIds(faceSampleIds)
|
||||||
|
.scene(FaceMatchingScene.CUSTOM_MATCHING)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快速创建仅识别场景Context
|
||||||
|
*/
|
||||||
|
public static FaceMatchingContext forRecognitionOnly(Long faceId) {
|
||||||
|
return FaceMatchingContext.builder()
|
||||||
|
.faceId(faceId)
|
||||||
|
.isNew(false)
|
||||||
|
.scene(FaceMatchingScene.RECOGNITION_ONLY)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 业务方法 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断指定Stage是否启用
|
||||||
|
*
|
||||||
|
* @param stageId Stage唯一标识
|
||||||
|
* @param defaultEnabled 默认值(当配置未指定时使用)
|
||||||
|
* @return true-启用, false-禁用
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean isStageEnabled(String stageId, boolean defaultEnabled) {
|
||||||
|
return stageEnabledMap.getOrDefault(stageId, defaultEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断指定Stage是否启用(默认为false)
|
||||||
|
*
|
||||||
|
* @param stageId Stage唯一标识
|
||||||
|
* @return true-启用, false-禁用
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean isStageEnabled(String stageId) {
|
||||||
|
return stageEnabledMap.getOrDefault(stageId, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置指定Stage的启用状态
|
||||||
|
*
|
||||||
|
* @param stageId Stage唯一标识
|
||||||
|
* @param enabled 是否启用
|
||||||
|
* @return this(支持链式调用)
|
||||||
|
*/
|
||||||
|
public FaceMatchingContext setStageState(String stageId, boolean enabled) {
|
||||||
|
stageEnabledMap.put(stageId, enabled);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启用指定Stage
|
||||||
|
*
|
||||||
|
* @param stageId Stage唯一标识
|
||||||
|
* @return this(支持链式调用)
|
||||||
|
*/
|
||||||
|
public FaceMatchingContext enableStage(String stageId) {
|
||||||
|
stageEnabledMap.put(stageId, true);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 禁用指定Stage
|
||||||
|
*
|
||||||
|
* @param stageId Stage唯一标识
|
||||||
|
* @return this(支持链式调用)
|
||||||
|
*/
|
||||||
|
public FaceMatchingContext disableStage(String stageId) {
|
||||||
|
stageEnabledMap.put(stageId, false);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量设置Stage启用状态
|
||||||
|
*
|
||||||
|
* @param stages Stage配置Map(stageId -> enabled)
|
||||||
|
* @return this(支持链式调用)
|
||||||
|
*/
|
||||||
|
public FaceMatchingContext setStages(Map<String, Boolean> stages) {
|
||||||
|
if (stages != null) {
|
||||||
|
stageEnabledMap.putAll(stages);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空所有Stage配置
|
||||||
|
*
|
||||||
|
* @return this(支持链式调用)
|
||||||
|
*/
|
||||||
|
public FaceMatchingContext clearStages() {
|
||||||
|
stageEnabledMap.clear();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Builder ====================
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
private Long faceId;
|
||||||
|
private boolean isNew = false;
|
||||||
|
private FaceMatchingScene scene;
|
||||||
|
private List<Long> faceSampleIds;
|
||||||
|
|
||||||
|
public Builder faceId(Long faceId) {
|
||||||
|
this.faceId = faceId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder isNew(boolean isNew) {
|
||||||
|
this.isNew = isNew;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder scene(FaceMatchingScene scene) {
|
||||||
|
this.scene = scene;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder faceSampleIds(List<Long> faceSampleIds) {
|
||||||
|
this.faceSampleIds = faceSampleIds;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FaceMatchingContext build() {
|
||||||
|
// 参数校验
|
||||||
|
if (faceId == null) {
|
||||||
|
throw new IllegalArgumentException("faceId is required");
|
||||||
|
}
|
||||||
|
if (scene == null) {
|
||||||
|
throw new IllegalArgumentException("scene is required");
|
||||||
|
}
|
||||||
|
// 自定义匹配场景必须提供faceSampleIds
|
||||||
|
if (scene == FaceMatchingScene.CUSTOM_MATCHING && (faceSampleIds == null || faceSampleIds.isEmpty())) {
|
||||||
|
throw new IllegalArgumentException("faceSampleIds is required for CUSTOM_MATCHING scene");
|
||||||
|
}
|
||||||
|
return new FaceMatchingContext(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.ycwl.basic.face.pipeline.enums;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸匹配场景枚举
|
||||||
|
*/
|
||||||
|
public enum FaceMatchingScene {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动人脸匹配
|
||||||
|
* 新用户上传人脸后自动执行匹配,或老用户重新匹配
|
||||||
|
*/
|
||||||
|
AUTO_MATCHING,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义人脸匹配
|
||||||
|
* 用户手动选择人脸样本进行匹配
|
||||||
|
*/
|
||||||
|
CUSTOM_MATCHING,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仅识别
|
||||||
|
* 只执行人脸识别,不处理后续业务逻辑(源文件关联、任务创建等)
|
||||||
|
*/
|
||||||
|
RECOGNITION_ONLY
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.ycwl.basic.face.pipeline.exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stage执行异常
|
||||||
|
*/
|
||||||
|
public class StageExecutionException extends RuntimeException {
|
||||||
|
|
||||||
|
private final String stageName;
|
||||||
|
|
||||||
|
public StageExecutionException(String stageName, String message) {
|
||||||
|
super(String.format("[%s] %s", stageName, message));
|
||||||
|
this.stageName = stageName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StageExecutionException(String stageName, String message, Throwable cause) {
|
||||||
|
super(String.format("[%s] %s", stageName, message), cause);
|
||||||
|
this.stageName = stageName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStageName() {
|
||||||
|
return stageName;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
package com.ycwl.basic.face.pipeline.factory;
|
||||||
|
|
||||||
|
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||||
|
import com.ycwl.basic.pipeline.core.Pipeline;
|
||||||
|
import com.ycwl.basic.pipeline.core.PipelineBuilder;
|
||||||
|
import com.ycwl.basic.face.pipeline.enums.FaceMatchingScene;
|
||||||
|
import com.ycwl.basic.face.pipeline.stages.*;
|
||||||
|
import com.ycwl.basic.service.pc.helper.ScenicConfigFacade;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸匹配Pipeline工厂
|
||||||
|
* 负责为不同场景组装Pipeline
|
||||||
|
*
|
||||||
|
* 支持的场景:
|
||||||
|
* 1. 自动人脸匹配(新用户/老用户)
|
||||||
|
* 2. 自定义人脸匹配
|
||||||
|
* 3. 仅识别
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class FaceMatchingPipelineFactory {
|
||||||
|
|
||||||
|
// ==================== 通用 Stage(13个)====================
|
||||||
|
@Autowired
|
||||||
|
private PrepareContextStage prepareContextStage;
|
||||||
|
@Autowired
|
||||||
|
private RecordMetricsStage recordMetricsStage;
|
||||||
|
@Autowired
|
||||||
|
private FaceRecognitionStage faceRecognitionStage;
|
||||||
|
@Autowired
|
||||||
|
private FaceRecoveryStage faceRecoveryStage;
|
||||||
|
@Autowired
|
||||||
|
private UpdateFaceResultStage updateFaceResultStage;
|
||||||
|
@Autowired
|
||||||
|
private BuildSourceRelationStage buildSourceRelationStage;
|
||||||
|
@Autowired
|
||||||
|
private ProcessFreeSourceStage processFreeSourceStage;
|
||||||
|
@Autowired
|
||||||
|
private ProcessBuyStatusStage processBuyStatusStage;
|
||||||
|
@Autowired
|
||||||
|
private HandleVideoRecreationStage handleVideoRecreationStage;
|
||||||
|
@Autowired
|
||||||
|
private PersistRelationsStage persistRelationsStage;
|
||||||
|
@Autowired
|
||||||
|
private CreateTaskStage createTaskStage;
|
||||||
|
@Autowired
|
||||||
|
private SetTaskStatusStage setTaskStatusStage;
|
||||||
|
@Autowired
|
||||||
|
private GeneratePuzzleStage generatePuzzleStage;
|
||||||
|
|
||||||
|
// ==================== 自定义匹配专属 Stage(6个)====================
|
||||||
|
@Autowired
|
||||||
|
private RecordCustomMatchMetricsStage recordCustomMatchMetricsStage;
|
||||||
|
@Autowired
|
||||||
|
private LoadFaceSamplesStage loadFaceSamplesStage;
|
||||||
|
@Autowired
|
||||||
|
private CustomFaceSearchStage customFaceSearchStage;
|
||||||
|
@Autowired
|
||||||
|
private LoadMatchedSamplesStage loadMatchedSamplesStage;
|
||||||
|
@Autowired
|
||||||
|
private FilterByTimeRangeStage filterByTimeRangeStage;
|
||||||
|
@Autowired
|
||||||
|
private FilterByDevicePhotoLimitStage filterByDevicePhotoLimitStage;
|
||||||
|
@Autowired
|
||||||
|
private DeleteOldRelationsStage deleteOldRelationsStage;
|
||||||
|
|
||||||
|
// ==================== 辅助服务 ====================
|
||||||
|
@Autowired
|
||||||
|
private ScenicConfigFacade scenicConfigFacade;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建自动人脸匹配Pipeline
|
||||||
|
*
|
||||||
|
* @param isNew 是否新用户
|
||||||
|
* @return Pipeline
|
||||||
|
*/
|
||||||
|
public Pipeline<FaceMatchingContext> createAutoMatchingPipeline(boolean isNew) {
|
||||||
|
PipelineBuilder<FaceMatchingContext> builder = new PipelineBuilder<>("AutoMatching-" + (isNew ? "New" : "Old"));
|
||||||
|
|
||||||
|
// 1. 准备上下文
|
||||||
|
builder.addStage(prepareContextStage);
|
||||||
|
|
||||||
|
// 2. 新用户设置任务状态
|
||||||
|
if (isNew) {
|
||||||
|
builder.addStage(setTaskStatusStage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 记录识别次数
|
||||||
|
builder.addStage(recordMetricsStage);
|
||||||
|
|
||||||
|
// 4. 执行人脸识别
|
||||||
|
builder.addStage(faceRecognitionStage);
|
||||||
|
|
||||||
|
// 5. 人脸识别补救
|
||||||
|
builder.addStage(faceRecoveryStage);
|
||||||
|
|
||||||
|
// 6. 更新人脸结果
|
||||||
|
builder.addStage(updateFaceResultStage);
|
||||||
|
|
||||||
|
// 7. 构建源文件关联
|
||||||
|
builder.addStage(buildSourceRelationStage);
|
||||||
|
|
||||||
|
// 8. 处理免费源文件逻辑
|
||||||
|
builder.addStage(processFreeSourceStage);
|
||||||
|
|
||||||
|
// 9. 处理购买状态
|
||||||
|
builder.addStage(processBuyStatusStage);
|
||||||
|
|
||||||
|
// 10. 处理视频重切
|
||||||
|
builder.addStage(handleVideoRecreationStage);
|
||||||
|
|
||||||
|
// 11. 持久化关联关系
|
||||||
|
builder.addStage(persistRelationsStage);
|
||||||
|
|
||||||
|
// 12. 创建任务
|
||||||
|
builder.addStage(createTaskStage);
|
||||||
|
|
||||||
|
// 13. 异步生成拼图模板
|
||||||
|
builder.addStage(generatePuzzleStage);
|
||||||
|
|
||||||
|
log.debug("创建自动人脸匹配Pipeline: isNew={}, stageCount={}", isNew, builder.build().getStageCount());
|
||||||
|
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建自定义人脸匹配Pipeline
|
||||||
|
*
|
||||||
|
* @return Pipeline
|
||||||
|
*/
|
||||||
|
public Pipeline<FaceMatchingContext> createCustomMatchingPipeline() {
|
||||||
|
PipelineBuilder<FaceMatchingContext> builder = new PipelineBuilder<>("CustomMatching");
|
||||||
|
|
||||||
|
// 1. 准备上下文
|
||||||
|
builder.addStage(prepareContextStage);
|
||||||
|
|
||||||
|
// 2. 记录自定义匹配次数
|
||||||
|
builder.addStage(recordCustomMatchMetricsStage);
|
||||||
|
|
||||||
|
// 3. 加载用户选择的人脸样本
|
||||||
|
builder.addStage(loadFaceSamplesStage);
|
||||||
|
|
||||||
|
// 4. 根据配置执行自定义人脸搜索
|
||||||
|
builder.addStage(customFaceSearchStage);
|
||||||
|
|
||||||
|
// 5. 加载匹配样本实体到缓存
|
||||||
|
builder.addStage(loadMatchedSamplesStage);
|
||||||
|
|
||||||
|
// 6. 应用时间范围筛选
|
||||||
|
builder.addStage(filterByTimeRangeStage);
|
||||||
|
|
||||||
|
// 7. 应用设备照片数量限制筛选
|
||||||
|
builder.addStage(filterByDevicePhotoLimitStage);
|
||||||
|
|
||||||
|
// 8. 更新人脸结果
|
||||||
|
builder.addStage(updateFaceResultStage);
|
||||||
|
|
||||||
|
// 9. 删除旧关系数据
|
||||||
|
builder.addStage(deleteOldRelationsStage);
|
||||||
|
|
||||||
|
// 10. 构建源文件关联
|
||||||
|
builder.addStage(buildSourceRelationStage);
|
||||||
|
|
||||||
|
// 11. 处理免费源文件逻辑
|
||||||
|
builder.addStage(processFreeSourceStage);
|
||||||
|
|
||||||
|
// 12. 处理购买状态
|
||||||
|
builder.addStage(processBuyStatusStage);
|
||||||
|
|
||||||
|
// 13. 处理视频重切
|
||||||
|
builder.addStage(handleVideoRecreationStage);
|
||||||
|
|
||||||
|
// 14. 持久化关联关系
|
||||||
|
builder.addStage(persistRelationsStage);
|
||||||
|
|
||||||
|
// 15. 创建任务
|
||||||
|
builder.addStage(createTaskStage);
|
||||||
|
|
||||||
|
log.debug("创建自定义人脸匹配Pipeline: stageCount={}", builder.build().getStageCount());
|
||||||
|
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建仅识别Pipeline
|
||||||
|
* 只执行人脸识别,不处理后续业务逻辑
|
||||||
|
*
|
||||||
|
* @return Pipeline
|
||||||
|
*/
|
||||||
|
public Pipeline<FaceMatchingContext> createRecognitionOnlyPipeline() {
|
||||||
|
PipelineBuilder<FaceMatchingContext> builder = new PipelineBuilder<>("RecognitionOnly");
|
||||||
|
|
||||||
|
// 1. 准备上下文
|
||||||
|
builder.addStage(prepareContextStage);
|
||||||
|
|
||||||
|
// 2. 执行人脸识别
|
||||||
|
builder.addStage(faceRecognitionStage);
|
||||||
|
|
||||||
|
// 3. 人脸识别补救
|
||||||
|
builder.addStage(faceRecoveryStage);
|
||||||
|
|
||||||
|
log.debug("创建仅识别Pipeline: stageCount={}", builder.build().getStageCount());
|
||||||
|
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据场景创建Pipeline
|
||||||
|
*
|
||||||
|
* @param scene 场景
|
||||||
|
* @param isNew 是否新用户(仅AUTO_MATCHING场景需要)
|
||||||
|
* @return Pipeline
|
||||||
|
*/
|
||||||
|
public Pipeline<FaceMatchingContext> createPipeline(FaceMatchingScene scene, boolean isNew) {
|
||||||
|
return switch (scene) {
|
||||||
|
case AUTO_MATCHING -> createAutoMatchingPipeline(isNew);
|
||||||
|
case CUSTOM_MATCHING -> createCustomMatchingPipeline();
|
||||||
|
case RECOGNITION_ONLY -> createRecognitionOnlyPipeline();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据Context创建Pipeline
|
||||||
|
*
|
||||||
|
* @param context 上下文
|
||||||
|
* @return Pipeline
|
||||||
|
*/
|
||||||
|
public Pipeline<FaceMatchingContext> createPipeline(FaceMatchingContext context) {
|
||||||
|
return createPipeline(context.getScene(), context.isNew());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
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.puzzle.dto.PuzzleGenerateRequest;
|
||||||
|
import com.ycwl.basic.puzzle.dto.PuzzleGenerateResponse;
|
||||||
|
import com.ycwl.basic.puzzle.dto.PuzzleTemplateDTO;
|
||||||
|
import com.ycwl.basic.puzzle.service.IPuzzleGenerateService;
|
||||||
|
import com.ycwl.basic.puzzle.service.IPuzzleTemplateService;
|
||||||
|
import com.ycwl.basic.repository.ScenicRepository;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拼图生成编排器
|
||||||
|
* 负责编排拼图模板的批量生成逻辑
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* 1. 查询景区的所有启用拼图模板
|
||||||
|
* 2. 构建动态数据
|
||||||
|
* 3. 逐个生成拼图图片
|
||||||
|
* 4. 记录统计信息
|
||||||
|
*
|
||||||
|
* 设计说明:
|
||||||
|
* - 从GeneratePuzzleStage中抽离出来,符合"薄Stage,厚Service"原则
|
||||||
|
* - Stage只负责触发异步任务,业务逻辑由此Orchestrator承担
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class PuzzleGenerationOrchestrator {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IPuzzleTemplateService puzzleTemplateService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IPuzzleGenerateService puzzleGenerateService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ScenicRepository scenicRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步生成景区所有启用的拼图模板
|
||||||
|
*
|
||||||
|
* @param scenicId 景区ID
|
||||||
|
* @param faceId 人脸ID
|
||||||
|
* @param memberId 会员ID
|
||||||
|
* @param faceUrl 人脸URL
|
||||||
|
*/
|
||||||
|
public void generateAllTemplatesAsync(Long scenicId, Long faceId, Long memberId, String faceUrl) {
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
log.info("开始异步生成景区拼图模板: scenicId={}, faceId={}", scenicId, faceId);
|
||||||
|
|
||||||
|
// 1. 查询该景区所有启用状态的拼图模板
|
||||||
|
List<PuzzleTemplateDTO> templateList = puzzleTemplateService.listTemplates(
|
||||||
|
scenicId, null, 1); // 查询启用状态的模板
|
||||||
|
|
||||||
|
if (templateList == null || templateList.isEmpty()) {
|
||||||
|
log.info("景区不存在启用的拼图模板,跳过生成: scenicId={}", scenicId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("景区存在 {} 个启用的拼图模板,开始逐个生成: scenicId={}", templateList.size(), scenicId);
|
||||||
|
|
||||||
|
// 2. 获取景区信息用于动态数据
|
||||||
|
ScenicV2DTO scenicBasic = scenicRepository.getScenicBasic(scenicId);
|
||||||
|
|
||||||
|
// 3. 准备公共动态数据
|
||||||
|
Map<String, String> baseDynamicData = buildBaseDynamicData(faceId, faceUrl, scenicBasic);
|
||||||
|
|
||||||
|
// 4. 遍历所有模板,逐个生成
|
||||||
|
int successCount = 0;
|
||||||
|
int failCount = 0;
|
||||||
|
for (PuzzleTemplateDTO template : templateList) {
|
||||||
|
try {
|
||||||
|
generateSingleTemplate(scenicId, faceId, memberId, template, baseDynamicData);
|
||||||
|
successCount++;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("拼图生成失败: scenicId={}, templateCode={}, templateName={}",
|
||||||
|
scenicId, template.getCode(), template.getName(), e);
|
||||||
|
failCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("景区拼图模板批量生成完成: scenicId={}, 总数={}, 成功={}, 失败={}",
|
||||||
|
scenicId, templateList.size(), successCount, failCount);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 异步任务失败不影响主流程,仅记录日志
|
||||||
|
log.error("异步生成拼图模板失败: scenicId={}, faceId={}", scenicId, faceId, e);
|
||||||
|
}
|
||||||
|
}, "PuzzleTemplateGenerator-" + scenicId + "-" + faceId).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建基础动态数据
|
||||||
|
*/
|
||||||
|
private Map<String, String> buildBaseDynamicData(Long faceId, String faceUrl, ScenicV2DTO scenicBasic) {
|
||||||
|
Map<String, String> baseDynamicData = new HashMap<>();
|
||||||
|
|
||||||
|
if (faceUrl != null) {
|
||||||
|
baseDynamicData.put("faceImage", faceUrl);
|
||||||
|
baseDynamicData.put("userAvatar", faceUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成单个拼图模板
|
||||||
|
*/
|
||||||
|
private void generateSingleTemplate(Long scenicId, Long faceId, Long memberId,
|
||||||
|
PuzzleTemplateDTO template,
|
||||||
|
Map<String, String> baseDynamicData) {
|
||||||
|
log.info("开始生成拼图: scenicId={}, templateCode={}, templateName={}",
|
||||||
|
scenicId, template.getCode(), template.getName());
|
||||||
|
|
||||||
|
// 构建生成请求
|
||||||
|
PuzzleGenerateRequest generateRequest = new PuzzleGenerateRequest();
|
||||||
|
generateRequest.setScenicId(scenicId);
|
||||||
|
generateRequest.setUserId(memberId);
|
||||||
|
generateRequest.setFaceId(faceId);
|
||||||
|
generateRequest.setBusinessType("face_matching");
|
||||||
|
generateRequest.setTemplateCode(template.getCode());
|
||||||
|
generateRequest.setOutputFormat("PNG");
|
||||||
|
generateRequest.setQuality(90);
|
||||||
|
generateRequest.setDynamicData(new HashMap<>(baseDynamicData));
|
||||||
|
generateRequest.setRequireRuleMatch(true);
|
||||||
|
|
||||||
|
// 调用拼图生成服务
|
||||||
|
PuzzleGenerateResponse response = puzzleGenerateService.generate(generateRequest);
|
||||||
|
|
||||||
|
log.info("拼图生成成功: scenicId={}, templateCode={}, imageUrl={}",
|
||||||
|
scenicId, template.getCode(), response.getImageUrl());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package com.ycwl.basic.face.pipeline.stages;
|
||||||
|
|
||||||
|
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||||
|
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 com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
|
||||||
|
import com.ycwl.basic.service.pc.processor.SourceRelationProcessor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建源文件关联Stage
|
||||||
|
* 负责根据匹配到的样本ID构建member_source关联关系
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* 1. 从context.sampleListIds读取匹配的样本ID列表
|
||||||
|
* 2. 调用sourceRelationProcessor.processMemberSources()生成MemberSourceEntity列表
|
||||||
|
* 3. 更新context.memberSourceList
|
||||||
|
*
|
||||||
|
* 前置条件: context.sampleListIds不为空
|
||||||
|
* 后置条件: context.memberSourceList已设置
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@StageConfig(
|
||||||
|
stageId = "build_source_relation",
|
||||||
|
optionalMode = StageOptionalMode.UNSUPPORT,
|
||||||
|
description = "构建源文件关联关系"
|
||||||
|
)
|
||||||
|
public class BuildSourceRelationStage extends AbstractPipelineStage<FaceMatchingContext> {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SourceRelationProcessor sourceRelationProcessor;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "BuildSourceRelation";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) {
|
||||||
|
// 只有当sampleListIds不为空时才执行
|
||||||
|
List<Long> sampleListIds = context.getSampleListIds();
|
||||||
|
if (sampleListIds == null || sampleListIds.isEmpty()) {
|
||||||
|
// 从searchResult中获取
|
||||||
|
if (context.getSearchResult() != null) {
|
||||||
|
sampleListIds = context.getSearchResult().getSampleListIds();
|
||||||
|
context.setSampleListIds(sampleListIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sampleListIds == null || sampleListIds.isEmpty()) {
|
||||||
|
log.debug("sampleListIds为空,跳过源文件关联,faceId={}", context.getFaceId());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
|
||||||
|
List<Long> sampleListIds = context.getSampleListIds();
|
||||||
|
Long faceId = context.getFaceId();
|
||||||
|
|
||||||
|
// 防御性检查:sampleListIds为空
|
||||||
|
if (sampleListIds == null || sampleListIds.isEmpty()) {
|
||||||
|
// 尝试从searchResult中获取
|
||||||
|
if (context.getSearchResult() != null) {
|
||||||
|
sampleListIds = context.getSearchResult().getSampleListIds();
|
||||||
|
if (sampleListIds != null && !sampleListIds.isEmpty()) {
|
||||||
|
context.setSampleListIds(sampleListIds);
|
||||||
|
} else {
|
||||||
|
log.debug("sampleListIds为空,跳过源文件关联,faceId={}", faceId);
|
||||||
|
return StageResult.skipped("sampleListIds为空");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.debug("sampleListIds为空,跳过源文件关联,faceId={}", faceId);
|
||||||
|
return StageResult.skipped("sampleListIds为空");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 处理源文件关联
|
||||||
|
List<MemberSourceEntity> memberSourceEntityList =
|
||||||
|
sourceRelationProcessor.processMemberSources(sampleListIds, context.getFace());
|
||||||
|
|
||||||
|
if (memberSourceEntityList == null || memberSourceEntityList.isEmpty()) {
|
||||||
|
log.warn("未找到有效的源文件,faceId={}, sampleListIds={}", faceId, sampleListIds);
|
||||||
|
return StageResult.skipped("未找到有效的源文件");
|
||||||
|
}
|
||||||
|
|
||||||
|
context.setMemberSourceList(memberSourceEntityList);
|
||||||
|
|
||||||
|
log.info("构建源文件关联成功: faceId={}, 关联源文件数={}", faceId, memberSourceEntityList.size());
|
||||||
|
|
||||||
|
return StageResult.success(String.format("构建了%d个源文件关联", memberSourceEntityList.size()));
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("构建源文件关联失败,faceId={}, sampleListIds={}", faceId, sampleListIds, e);
|
||||||
|
// 源文件关联失败不影响主流程,返回降级
|
||||||
|
return StageResult.degraded("构建源文件关联失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package com.ycwl.basic.face.pipeline.stages;
|
||||||
|
|
||||||
|
import com.ycwl.basic.biz.TaskStatusBiz;
|
||||||
|
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||||
|
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 com.ycwl.basic.service.pc.helper.ScenicConfigFacade;
|
||||||
|
import com.ycwl.basic.service.task.TaskService;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建任务Stage
|
||||||
|
* 负责根据配置决定是否自动创建任务
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* 1. 检查face_select_first配置
|
||||||
|
* 2. 如果配置为false,则调用taskService.autoCreateTaskByFaceId()
|
||||||
|
* 3. 如果配置为true,则设置任务状态为2(等待用户选择)
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@StageConfig(
|
||||||
|
stageId = "create_task",
|
||||||
|
optionalMode = StageOptionalMode.UNSUPPORT,
|
||||||
|
description = "根据配置创建视频任务"
|
||||||
|
)
|
||||||
|
public class CreateTaskStage extends AbstractPipelineStage<FaceMatchingContext> {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ScenicConfigFacade scenicConfigFacade;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TaskService taskService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TaskStatusBiz taskStatusBiz;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "CreateTask";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
|
||||||
|
Long scenicId = context.getFace().getScenicId();
|
||||||
|
Long faceId = context.getFaceId();
|
||||||
|
|
||||||
|
try {
|
||||||
|
boolean faceSelectFirst = scenicConfigFacade.isFaceSelectFirst(scenicId);
|
||||||
|
|
||||||
|
if (!faceSelectFirst) {
|
||||||
|
// 配置为自动创建任务
|
||||||
|
taskService.autoCreateTaskByFaceId(faceId);
|
||||||
|
log.info("自动创建任务成功: faceId={}", faceId);
|
||||||
|
return StageResult.success("自动创建任务成功");
|
||||||
|
} else {
|
||||||
|
// 配置为等待用户选择
|
||||||
|
taskStatusBiz.setFaceCutStatus(faceId, 2);
|
||||||
|
log.debug("景区配置 face_select_first=true,跳过自动创建任务: faceId={}", faceId);
|
||||||
|
return StageResult.skipped("等待用户手动选择");
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("创建任务失败,faceId={}", faceId, e);
|
||||||
|
// 任务创建失败不影响主流程,返回降级而不是失败
|
||||||
|
return StageResult.degraded("任务创建失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
package com.ycwl.basic.face.pipeline.stages;
|
||||||
|
|
||||||
|
import com.ycwl.basic.exception.BaseException;
|
||||||
|
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||||
|
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 com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
|
||||||
|
import com.ycwl.basic.model.task.resp.SearchFaceRespVo;
|
||||||
|
import com.ycwl.basic.service.pc.helper.SearchResultMerger;
|
||||||
|
import com.ycwl.basic.service.task.TaskFaceService;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义人脸搜索Stage
|
||||||
|
* 负责根据faceSelectPostMode执行不同的搜索策略
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* 1. 从context.faceSelectPostMode读取配置
|
||||||
|
* 2. 模式2: 直接使用用户选择的样本,不搜索
|
||||||
|
* 3. 模式0/1: 对每个样本搜索,然后合并结果
|
||||||
|
* 4. 更新context.searchResult
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@StageConfig(
|
||||||
|
stageId = "custom_face_search",
|
||||||
|
optionalMode = StageOptionalMode.FORCE_ON,
|
||||||
|
description = "根据配置执行自定义人脸搜索"
|
||||||
|
)
|
||||||
|
public class CustomFaceSearchStage extends AbstractPipelineStage<FaceMatchingContext> {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TaskFaceService taskFaceService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SearchResultMerger resultMerger;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "CustomFaceSearch";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
|
||||||
|
Integer faceSelectPostMode = context.getFaceSelectPostMode();
|
||||||
|
List<FaceSampleEntity> faceSamples = context.getFaceSamples();
|
||||||
|
List<Long> faceSampleIds = context.getFaceSampleIds();
|
||||||
|
Long faceId = context.getFaceId();
|
||||||
|
|
||||||
|
if (faceSelectPostMode == null) {
|
||||||
|
faceSelectPostMode = 0; // 默认为并集模式
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("face_select_post_mode配置值: {}, faceId={}", faceSelectPostMode, faceId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
SearchFaceRespVo mergedResult;
|
||||||
|
|
||||||
|
// 模式2:不搜索,直接使用用户选择的faceSampleIds
|
||||||
|
if (Integer.valueOf(2).equals(faceSelectPostMode)) {
|
||||||
|
log.debug("使用模式2:直接使用用户选择的人脸样本,不进行搜索,faceId={}", faceId);
|
||||||
|
mergedResult = resultMerger.createDirectResult(faceSampleIds);
|
||||||
|
// 保留原始matchResult
|
||||||
|
if (context.getFace().getMatchResult() != null) {
|
||||||
|
mergedResult.setSearchResultJson(context.getFace().getMatchResult());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 模式0(并集)和模式1(交集):需要进行搜索
|
||||||
|
List<SearchFaceRespVo> searchResults = new ArrayList<>();
|
||||||
|
|
||||||
|
for (FaceSampleEntity faceSample : faceSamples) {
|
||||||
|
try {
|
||||||
|
SearchFaceRespVo result = taskFaceService.searchFace(
|
||||||
|
context.getFaceBodyAdapter(),
|
||||||
|
String.valueOf(context.getFace().getScenicId()),
|
||||||
|
faceSample.getFaceUrl(),
|
||||||
|
"自定义人脸匹配");
|
||||||
|
|
||||||
|
if (result != null) {
|
||||||
|
searchResults.add(result);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("人脸样本搜索失败,faceSampleId={}, faceUrl={}, faceId={}",
|
||||||
|
faceSample.getId(), faceSample.getFaceUrl(), faceId, e);
|
||||||
|
// 继续处理其他样本,不中断整个流程
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchResults.isEmpty()) {
|
||||||
|
log.warn("所有人脸样本搜索都失败,faceId={}, faceSampleIds={}", faceId, faceSampleIds);
|
||||||
|
throw new BaseException("人脸识别失败,请重试");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据模式整合多个搜索结果
|
||||||
|
mergedResult = resultMerger.merge(searchResults, faceSelectPostMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.setSearchResult(mergedResult);
|
||||||
|
context.setSampleListIds(mergedResult.getSampleListIds());
|
||||||
|
|
||||||
|
log.info("自定义人脸搜索完成: faceId={}, mode={}, 匹配数={}",
|
||||||
|
faceId, faceSelectPostMode,
|
||||||
|
mergedResult.getSampleListIds() != null ? mergedResult.getSampleListIds().size() : 0);
|
||||||
|
|
||||||
|
return StageResult.success(String.format("自定义搜索完成,模式=%d", faceSelectPostMode));
|
||||||
|
|
||||||
|
} catch (BaseException e) {
|
||||||
|
throw e;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("自定义人脸搜索失败,faceId={}, faceSampleIds={}", faceId, faceSampleIds, e);
|
||||||
|
return StageResult.failed("自定义人脸搜索失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package com.ycwl.basic.face.pipeline.stages;
|
||||||
|
|
||||||
|
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||||
|
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 com.ycwl.basic.mapper.SourceMapper;
|
||||||
|
import com.ycwl.basic.mapper.VideoMapper;
|
||||||
|
import com.ycwl.basic.repository.MemberRelationRepository;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除旧关系Stage
|
||||||
|
* 负责在保存新关系前,删除该人脸的旧数据关系
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* 1. 删除member_source中该人脸的未购买关系
|
||||||
|
* 2. 删除member_video中该人脸的未购买关系
|
||||||
|
* 3. 清除缓存
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@StageConfig(
|
||||||
|
stageId = "delete_old_relations",
|
||||||
|
optionalMode = StageOptionalMode.FORCE_ON,
|
||||||
|
description = "删除人脸旧关系数据"
|
||||||
|
)
|
||||||
|
public class DeleteOldRelationsStage extends AbstractPipelineStage<FaceMatchingContext> {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SourceMapper sourceMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private VideoMapper videoMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MemberRelationRepository memberRelationRepository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "DeleteOldRelations";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
|
||||||
|
Long faceId = context.getFaceId();
|
||||||
|
Long memberId = context.getFace().getMemberId();
|
||||||
|
|
||||||
|
try {
|
||||||
|
log.debug("删除人脸旧关系数据:faceId={}, memberId={}", faceId, memberId);
|
||||||
|
|
||||||
|
// 1. 删除member_source中的未购买关系
|
||||||
|
sourceMapper.deleteNotBuyFaceRelation(memberId, faceId);
|
||||||
|
|
||||||
|
// 2. 删除member_video中的未购买关系
|
||||||
|
videoMapper.deleteNotBuyFaceRelations(memberId, faceId);
|
||||||
|
|
||||||
|
// 3. 清除缓存
|
||||||
|
memberRelationRepository.clearSCacheByFace(faceId);
|
||||||
|
|
||||||
|
log.debug("人脸旧关系数据删除完成:faceId={}", faceId);
|
||||||
|
|
||||||
|
return StageResult.success("旧关系数据已删除");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("删除旧关系数据失败,faceId={}", faceId, e);
|
||||||
|
// 删除失败不影响主流程,返回降级
|
||||||
|
return StageResult.degraded("删除旧关系数据失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package com.ycwl.basic.face.pipeline.stages;
|
||||||
|
|
||||||
|
import com.ycwl.basic.exception.BaseException;
|
||||||
|
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||||
|
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 com.ycwl.basic.model.task.resp.SearchFaceRespVo;
|
||||||
|
import com.ycwl.basic.service.task.TaskFaceService;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸识别Stage
|
||||||
|
* 负责执行核心的人脸识别搜索
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* 1. 调用taskFaceService.searchFace()执行人脸搜索
|
||||||
|
* 2. 将结果存入context.searchResult
|
||||||
|
* 3. 识别失败则返回FAILED
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@StageConfig(
|
||||||
|
stageId = "face_recognition",
|
||||||
|
optionalMode = StageOptionalMode.FORCE_ON,
|
||||||
|
description = "执行人脸识别搜索"
|
||||||
|
)
|
||||||
|
public class FaceRecognitionStage extends AbstractPipelineStage<FaceMatchingContext> {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TaskFaceService taskFaceService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "FaceRecognition";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
|
||||||
|
try {
|
||||||
|
SearchFaceRespVo searchResult = taskFaceService.searchFace(
|
||||||
|
context.getFaceBodyAdapter(),
|
||||||
|
String.valueOf(context.getFace().getScenicId()),
|
||||||
|
context.getFace().getFaceUrl(),
|
||||||
|
"人脸识别");
|
||||||
|
|
||||||
|
if (searchResult == null) {
|
||||||
|
log.warn("人脸识别返回结果为空,faceId={}", context.getFaceId());
|
||||||
|
return StageResult.failed("人脸识别失败,请换一张试试把~");
|
||||||
|
}
|
||||||
|
|
||||||
|
context.setSearchResult(searchResult);
|
||||||
|
|
||||||
|
log.info("人脸识别完成: faceId={}, score={}, 匹配数={}",
|
||||||
|
context.getFaceId(),
|
||||||
|
searchResult.getScore(),
|
||||||
|
searchResult.getSampleListIds() != null ? searchResult.getSampleListIds().size() : 0);
|
||||||
|
|
||||||
|
return StageResult.success(String.format("识别成功,匹配数=%d",
|
||||||
|
searchResult.getSampleListIds() != null ? searchResult.getSampleListIds().size() : 0));
|
||||||
|
|
||||||
|
} catch (BaseException e) {
|
||||||
|
throw e;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("人脸识别服务调用失败,faceId={}, scenicId={}",
|
||||||
|
context.getFaceId(), context.getFace().getScenicId(), e);
|
||||||
|
return StageResult.failed("人脸识别失败,请换一张试试把~", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package com.ycwl.basic.face.pipeline.stages;
|
||||||
|
|
||||||
|
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||||
|
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 com.ycwl.basic.model.task.resp.SearchFaceRespVo;
|
||||||
|
import com.ycwl.basic.service.pc.processor.FaceRecoveryStrategy;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸识别补救Stage
|
||||||
|
* 负责执行人脸识别的补救逻辑(降级)
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* 1. 从context.searchResult读取识别结果
|
||||||
|
* 2. 调用faceRecoveryStrategy.executeFaceRecoveryLogic()执行补救
|
||||||
|
* 3. 如果触发补救,更新searchResult并返回DEGRADED
|
||||||
|
* 4. 否则返回SUCCESS
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@StageConfig(
|
||||||
|
stageId = "face_recovery",
|
||||||
|
optionalMode = StageOptionalMode.SUPPORT,
|
||||||
|
description = "执行人脸识别补救逻辑",
|
||||||
|
defaultEnabled = true
|
||||||
|
)
|
||||||
|
public class FaceRecoveryStage extends AbstractPipelineStage<FaceMatchingContext> {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private FaceRecoveryStrategy faceRecoveryStrategy;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "FaceRecovery";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) {
|
||||||
|
// 只有当searchResult不为空时才执行
|
||||||
|
if (context.getSearchResult() == null) {
|
||||||
|
log.debug("searchResult为空,跳过补救逻辑,faceId={}", context.getFaceId());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
|
||||||
|
SearchFaceRespVo searchResult = context.getSearchResult();
|
||||||
|
Long faceId = context.getFaceId();
|
||||||
|
|
||||||
|
// 防御性检查:searchResult为空
|
||||||
|
if (searchResult == null) {
|
||||||
|
log.debug("searchResult为空,跳过补救逻辑,faceId={}", faceId);
|
||||||
|
return StageResult.skipped("searchResult为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 执行补救逻辑(补救逻辑内部会判断是否需要触发)
|
||||||
|
SearchFaceRespVo recoveredResult = faceRecoveryStrategy.executeFaceRecoveryLogic(
|
||||||
|
searchResult,
|
||||||
|
context.getScenicConfig(),
|
||||||
|
context.getFaceBodyAdapter(),
|
||||||
|
context.getFace().getScenicId());
|
||||||
|
|
||||||
|
// 如果结果发生变化,说明触发了补救
|
||||||
|
if (recoveredResult != searchResult) {
|
||||||
|
context.setSearchResult(recoveredResult);
|
||||||
|
log.info("触发补救逻辑,重新搜索: faceId={}", faceId);
|
||||||
|
return StageResult.degraded("触发补救逻辑,重新搜索");
|
||||||
|
}
|
||||||
|
|
||||||
|
return StageResult.success("无需补救");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("补救逻辑执行失败,faceId={}", faceId, e);
|
||||||
|
// 补救失败不影响主流程,返回降级
|
||||||
|
return StageResult.degraded("补救逻辑执行失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
package com.ycwl.basic.face.pipeline.stages;
|
||||||
|
|
||||||
|
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||||
|
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 com.ycwl.basic.integration.common.manager.DeviceConfigManager;
|
||||||
|
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
|
||||||
|
import com.ycwl.basic.repository.DeviceRepository;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按设备照片数量限制筛选样本Stage
|
||||||
|
* 负责根据设备配置的照片数量限制(limit_photo)筛选匹配样本
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* 1. 从context.faceSamples读取样本实体缓存
|
||||||
|
* 2. 按设备ID分组
|
||||||
|
* 3. 对每个设备,根据其limit_photo配置筛选样本:
|
||||||
|
* - 如果样本数 > limit_photo + 2: 按时间排序,去掉首尾,保留中间limit_photo张
|
||||||
|
* - 如果样本数 > limit_photo + 1: 按时间排序,去掉尾部,保留前limit_photo张
|
||||||
|
* - 如果样本数 > limit_photo: 保留前limit_photo张
|
||||||
|
* - 否则: 保留全部
|
||||||
|
* 4. 更新context.sampleListIds
|
||||||
|
*
|
||||||
|
* 前置条件: context.faceSamples不为空 (由LoadMatchedSamplesStage加载)
|
||||||
|
* 配置说明: limit_photo=0或null表示不限制数量
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@StageConfig(
|
||||||
|
stageId = "filter_by_device_photo_limit",
|
||||||
|
optionalMode = StageOptionalMode.SUPPORT,
|
||||||
|
description = "按设备照片数量限制筛选样本",
|
||||||
|
defaultEnabled = true
|
||||||
|
)
|
||||||
|
public class FilterByDevicePhotoLimitStage extends AbstractPipelineStage<FaceMatchingContext> {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private DeviceRepository deviceRepository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "FilterByDevicePhotoLimit";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) {
|
||||||
|
// 检查faceSamples是否为空
|
||||||
|
List<FaceSampleEntity> faceSamples = context.getFaceSamples();
|
||||||
|
if (faceSamples == null || faceSamples.isEmpty()) {
|
||||||
|
log.debug("faceSamples为空,跳过设备照片限制筛选,faceId={}", context.getFaceId());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
|
||||||
|
List<FaceSampleEntity> faceSamples = context.getFaceSamples();
|
||||||
|
List<Long> sampleListIds = context.getSampleListIds();
|
||||||
|
Long faceId = context.getFaceId();
|
||||||
|
|
||||||
|
// 防御性检查:faceSamples为空
|
||||||
|
if (faceSamples == null || faceSamples.isEmpty()) {
|
||||||
|
log.debug("faceSamples为空,跳过设备照片限制筛选,faceId={}", faceId);
|
||||||
|
return StageResult.skipped("faceSamples为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 构建样本ID到实体的映射
|
||||||
|
Map<Long, FaceSampleEntity> sampleMap = faceSamples.stream()
|
||||||
|
.collect(Collectors.toMap(FaceSampleEntity::getId, sample -> sample, (a, b) -> a));
|
||||||
|
|
||||||
|
// 2. 按设备ID分组
|
||||||
|
Map<Long, List<FaceSampleEntity>> deviceSamplesMap = new LinkedHashMap<>();
|
||||||
|
Set<Long> passthroughSampleIds = new LinkedHashSet<>();
|
||||||
|
|
||||||
|
for (Long sampleId : sampleListIds) {
|
||||||
|
FaceSampleEntity sample = sampleMap.get(sampleId);
|
||||||
|
if (sample == null || sample.getDeviceId() == null) {
|
||||||
|
passthroughSampleIds.add(sampleId); // 无设备ID的样本直接保留
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
deviceSamplesMap
|
||||||
|
.computeIfAbsent(sample.getDeviceId(), key -> new ArrayList<>())
|
||||||
|
.add(sample);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 对每个设备应用照片数量限制
|
||||||
|
Map<Long, Integer> limitCache = new HashMap<>();
|
||||||
|
Set<Long> retainedSampleIds = new LinkedHashSet<>(passthroughSampleIds);
|
||||||
|
|
||||||
|
for (Map.Entry<Long, List<FaceSampleEntity>> entry : deviceSamplesMap.entrySet()) {
|
||||||
|
Long deviceId = entry.getKey();
|
||||||
|
List<FaceSampleEntity> deviceSamples = entry.getValue();
|
||||||
|
|
||||||
|
// 读取设备配置
|
||||||
|
Integer limitPhoto = limitCache.computeIfAbsent(deviceId, id -> {
|
||||||
|
DeviceConfigManager deviceConfig = deviceRepository.getDeviceConfigManager(id);
|
||||||
|
return deviceConfig != null ? deviceConfig.getInteger("limit_photo") : null;
|
||||||
|
});
|
||||||
|
|
||||||
|
List<Long> retainedForDevice = applyLimitForDevice(deviceId, deviceSamples, limitPhoto);
|
||||||
|
retainedSampleIds.addAll(retainedForDevice);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 按原始顺序保留筛选后的样本ID
|
||||||
|
List<Long> resultIds = sampleListIds.stream()
|
||||||
|
.filter(retainedSampleIds::contains)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
// 5. 更新context
|
||||||
|
context.setSampleListIds(resultIds);
|
||||||
|
|
||||||
|
log.info("设备照片数量限制筛选完成: faceId={}, 原始样本数={}, 筛选后数={}",
|
||||||
|
faceId, sampleListIds.size(), resultIds.size());
|
||||||
|
|
||||||
|
return StageResult.success(String.format("设备限制筛选: %d → %d",
|
||||||
|
sampleListIds.size(), resultIds.size()));
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("设备照片数量限制筛选失败,faceId={}", faceId, e);
|
||||||
|
// 筛选失败不影响主流程,返回降级
|
||||||
|
return StageResult.degraded("设备照片数量限制筛选失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对单个设备的样本应用照片数量限制
|
||||||
|
*/
|
||||||
|
private List<Long> applyLimitForDevice(Long deviceId, List<FaceSampleEntity> deviceSamples, Integer limitPhoto) {
|
||||||
|
List<Long> deviceSampleIds = deviceSamples.stream()
|
||||||
|
.map(FaceSampleEntity::getId)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
// 无限制或限制数量<=0,保留全部
|
||||||
|
if (limitPhoto == null || limitPhoto <= 0) {
|
||||||
|
log.debug("设备照片限制: 设备ID={}, 无限制, 保留{}张照片", deviceId, deviceSampleIds.size());
|
||||||
|
return deviceSampleIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sampleCount = deviceSamples.size();
|
||||||
|
|
||||||
|
// 样本数 > limit_photo + 2: 按时间排序,去掉首尾
|
||||||
|
if (sampleCount > (limitPhoto + 2)) {
|
||||||
|
List<Long> retained = processDeviceSamples(deviceSamples, limitPhoto, true);
|
||||||
|
log.debug("设备照片限制: 设备ID={}, 限制={}张, 原始{}张, 去首尾后最终{}张",
|
||||||
|
deviceId, limitPhoto, sampleCount, retained.size());
|
||||||
|
return retained;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 样本数 > limit_photo + 1: 按时间排序,去掉尾部
|
||||||
|
if (sampleCount > (limitPhoto + 1)) {
|
||||||
|
List<Long> retained = processDeviceSamples(deviceSamples, limitPhoto, false);
|
||||||
|
log.debug("设备照片限制: 设备ID={}, 限制={}张, 原始{}张, 去尾部后最终{}张",
|
||||||
|
deviceId, limitPhoto, sampleCount, retained.size());
|
||||||
|
return retained;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 样本数 > limit_photo: 保留前limit_photo张
|
||||||
|
if (sampleCount > limitPhoto) {
|
||||||
|
List<Long> retained = deviceSamples.stream()
|
||||||
|
.limit(limitPhoto)
|
||||||
|
.map(FaceSampleEntity::getId)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
log.debug("设备照片限制: 设备ID={}, 限制={}张, 原始{}张, 取前{}张",
|
||||||
|
deviceId, limitPhoto, sampleCount, retained.size());
|
||||||
|
return retained;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 样本数 <= limit_photo: 保留全部
|
||||||
|
log.debug("设备照片限制: 设备ID={}, 限制={}张, 原始{}张, 无需筛选, 保留全部",
|
||||||
|
deviceId, limitPhoto, sampleCount);
|
||||||
|
return deviceSampleIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理设备样本,根据参数决定是否去掉首尾
|
||||||
|
*
|
||||||
|
* @param deviceSamples 设备样本列表
|
||||||
|
* @param limitPhoto 限制数量
|
||||||
|
* @param removeBoth 是否去掉首尾,true去掉首尾,false只去掉尾部
|
||||||
|
* @return 处理后的样本ID列表
|
||||||
|
*/
|
||||||
|
private List<Long> processDeviceSamples(List<FaceSampleEntity> deviceSamples, int limitPhoto, boolean removeBoth) {
|
||||||
|
// 创建原始排序的索引映射,用于后续恢复排序
|
||||||
|
Map<Long, Integer> originalIndexMap = new HashMap<>();
|
||||||
|
for (int i = 0; i < deviceSamples.size(); i++) {
|
||||||
|
originalIndexMap.put(deviceSamples.get(i).getId(), i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按创建时间排序
|
||||||
|
List<FaceSampleEntity> sortedByCreateTime = deviceSamples.stream()
|
||||||
|
.sorted(Comparator.comparing(FaceSampleEntity::getCreateAt))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
// 根据参数决定去掉首尾还是只去掉尾部
|
||||||
|
List<FaceSampleEntity> filteredSamples;
|
||||||
|
if (removeBoth && sortedByCreateTime.size() > 2) {
|
||||||
|
// 去掉首尾
|
||||||
|
filteredSamples = sortedByCreateTime.subList(1, sortedByCreateTime.size() - 1);
|
||||||
|
} else if (!removeBoth && sortedByCreateTime.size() > 1) {
|
||||||
|
// 只去掉尾部
|
||||||
|
filteredSamples = sortedByCreateTime.subList(0, sortedByCreateTime.size() - 1);
|
||||||
|
} else {
|
||||||
|
filteredSamples = sortedByCreateTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取前limitPhoto个
|
||||||
|
List<FaceSampleEntity> limitedSamples = filteredSamples.stream()
|
||||||
|
.limit(limitPhoto)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
// 按原始顺序排序
|
||||||
|
List<Long> resultIds = limitedSamples.stream()
|
||||||
|
.sorted(Comparator.comparing(sample -> originalIndexMap.get(sample.getId())))
|
||||||
|
.map(FaceSampleEntity::getId)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
return resultIds;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package com.ycwl.basic.face.pipeline.stages;
|
||||||
|
|
||||||
|
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||||
|
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 com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按时间范围筛选样本Stage
|
||||||
|
* 负责根据景区配置的游览时间(tour_time)筛选匹配样本
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* 1. 从context.scenicConfig读取tour_time配置(分钟)
|
||||||
|
* 2. 从context.faceSamples读取样本实体缓存
|
||||||
|
* 3. 找到最新的样本,以其拍摄时间为基准
|
||||||
|
* 4. 筛选出时间范围内(最新样本时间 ± tour_time分钟)的样本
|
||||||
|
* 5. 更新context.sampleListIds
|
||||||
|
*
|
||||||
|
* 前置条件:
|
||||||
|
* - context.faceSamples不为空 (由LoadMatchedSamplesStage加载)
|
||||||
|
* - context.scenicConfig配置了tour_time
|
||||||
|
*
|
||||||
|
* 配置说明: tour_time=0或null表示不限制时间范围
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@StageConfig(
|
||||||
|
stageId = "filter_by_time_range",
|
||||||
|
optionalMode = StageOptionalMode.SUPPORT,
|
||||||
|
description = "按游览时间范围筛选样本",
|
||||||
|
defaultEnabled = true
|
||||||
|
)
|
||||||
|
public class FilterByTimeRangeStage extends AbstractPipelineStage<FaceMatchingContext> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "FilterByTimeRange";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) {
|
||||||
|
// 检查faceSamples是否为空
|
||||||
|
List<FaceSampleEntity> faceSamples = context.getFaceSamples();
|
||||||
|
if (faceSamples == null || faceSamples.isEmpty()) {
|
||||||
|
log.debug("faceSamples为空,跳过时间范围筛选,faceId={}", context.getFaceId());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否配置了tour_time
|
||||||
|
Integer tourMinutes = context.getScenicConfig() != null
|
||||||
|
? context.getScenicConfig().getInteger("tour_time")
|
||||||
|
: null;
|
||||||
|
if (tourMinutes == null || tourMinutes <= 0) {
|
||||||
|
log.debug("景区未配置tour_time或配置为0,跳过时间范围筛选,faceId={}", context.getFaceId());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
|
||||||
|
List<FaceSampleEntity> faceSamples = context.getFaceSamples();
|
||||||
|
List<Long> sampleListIds = context.getSampleListIds();
|
||||||
|
Long faceId = context.getFaceId();
|
||||||
|
|
||||||
|
// 防御性检查:faceSamples为空
|
||||||
|
if (faceSamples == null || faceSamples.isEmpty()) {
|
||||||
|
log.debug("faceSamples为空,跳过时间范围筛选,faceId={}", faceId);
|
||||||
|
return StageResult.skipped("faceSamples为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 防御性检查:tour_time配置
|
||||||
|
Integer tourMinutes = context.getScenicConfig() != null
|
||||||
|
? context.getScenicConfig().getInteger("tour_time")
|
||||||
|
: null;
|
||||||
|
if (tourMinutes == null || tourMinutes <= 0) {
|
||||||
|
log.debug("景区未配置tour_time或配置为0,跳过时间范围筛选,faceId={}", faceId);
|
||||||
|
return StageResult.skipped("未配置tour_time");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 构建样本ID到实体的映射
|
||||||
|
Map<Long, FaceSampleEntity> sampleMap = faceSamples.stream()
|
||||||
|
.collect(Collectors.toMap(FaceSampleEntity::getId, sample -> sample, (a, b) -> a));
|
||||||
|
|
||||||
|
// 2. 找到最新的样本(拍摄时间最晚)
|
||||||
|
FaceSampleEntity topMatchSample = faceSamples.stream()
|
||||||
|
.filter(sample -> sample.getCreateAt() != null)
|
||||||
|
.max(Comparator.comparing(FaceSampleEntity::getCreateAt))
|
||||||
|
.orElse(null);
|
||||||
|
|
||||||
|
if (topMatchSample == null || topMatchSample.getCreateAt() == null) {
|
||||||
|
log.warn("未找到有效的样本拍摄时间,保留所有样本,faceId={}", faceId);
|
||||||
|
return StageResult.success("样本无拍摄时间,保留所有");
|
||||||
|
}
|
||||||
|
|
||||||
|
Date referenceTime = topMatchSample.getCreateAt();
|
||||||
|
long referenceMillis = referenceTime.getTime();
|
||||||
|
long tourMillis = tourMinutes * 60 * 1000L;
|
||||||
|
|
||||||
|
// 3. 筛选时间范围内的样本
|
||||||
|
List<Long> filteredIds = sampleListIds.stream()
|
||||||
|
.filter(sampleId -> {
|
||||||
|
FaceSampleEntity sample = sampleMap.get(sampleId);
|
||||||
|
if (sample == null || sample.getCreateAt() == null) {
|
||||||
|
return false; // 无时间信息的样本被过滤
|
||||||
|
}
|
||||||
|
long timeDiff = Math.abs(sample.getCreateAt().getTime() - referenceMillis);
|
||||||
|
return timeDiff <= tourMillis;
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
// 4. 更新context
|
||||||
|
context.setSampleListIds(filteredIds);
|
||||||
|
|
||||||
|
log.info("时间范围筛选完成: faceId={}, tour_time={}分钟, 原始样本数={}, 筛选后数={}",
|
||||||
|
faceId, tourMinutes, sampleListIds.size(), filteredIds.size());
|
||||||
|
|
||||||
|
return StageResult.success(String.format("时间筛选: %d → %d",
|
||||||
|
sampleListIds.size(), filteredIds.size()));
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("时间范围筛选失败,faceId={}", faceId, e);
|
||||||
|
// 筛选失败不影响主流程,返回降级
|
||||||
|
return StageResult.degraded("时间范围筛选失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package com.ycwl.basic.face.pipeline.stages;
|
||||||
|
|
||||||
|
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||||
|
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 com.ycwl.basic.face.pipeline.helper.PuzzleGenerationOrchestrator;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成拼图模板Stage
|
||||||
|
* 负责触发景区拼图模板的异步生成任务
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* 1. 从context读取必要参数(scenicId, faceId, memberId, faceUrl)
|
||||||
|
* 2. 调用puzzleOrchestrator.generateAllTemplatesAsync()触发异步生成
|
||||||
|
* 3. 立即返回,不等待生成完成
|
||||||
|
*
|
||||||
|
* 业务说明:
|
||||||
|
* - 拼图生成是异步的,不影响主流程
|
||||||
|
* - 具体的拼图生成逻辑由PuzzleGenerationOrchestrator负责
|
||||||
|
* - Stage只负责触发任务,符合"薄Stage,厚Service"原则
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@StageConfig(
|
||||||
|
stageId = "generate_puzzle",
|
||||||
|
optionalMode = StageOptionalMode.SUPPORT,
|
||||||
|
description = "异步生成拼图模板",
|
||||||
|
defaultEnabled = true
|
||||||
|
)
|
||||||
|
public class GeneratePuzzleStage extends AbstractPipelineStage<FaceMatchingContext> {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private PuzzleGenerationOrchestrator puzzleOrchestrator;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "GeneratePuzzle";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
|
||||||
|
Long scenicId = context.getFace().getScenicId();
|
||||||
|
Long faceId = context.getFaceId();
|
||||||
|
Long memberId = context.getFace().getMemberId();
|
||||||
|
String faceUrl = context.getFace().getFaceUrl();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 触发异步生成拼图模板
|
||||||
|
puzzleOrchestrator.generateAllTemplatesAsync(scenicId, faceId, memberId, faceUrl);
|
||||||
|
|
||||||
|
log.debug("拼图模板异步生成任务已提交: scenicId={}, faceId={}", scenicId, faceId);
|
||||||
|
|
||||||
|
return StageResult.success("拼图模板已提交异步生成");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("提交拼图生成任务失败: scenicId={}, faceId={}", scenicId, faceId, e);
|
||||||
|
// 拼图生成失败不影响主流程,返回降级
|
||||||
|
return StageResult.degraded("提交拼图生成任务失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package com.ycwl.basic.face.pipeline.stages;
|
||||||
|
|
||||||
|
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||||
|
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 com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
|
||||||
|
import com.ycwl.basic.service.pc.processor.VideoRecreationHandler;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理视频重切Stage
|
||||||
|
* 负责触发视频重新切片处理
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* 1. 从context读取必要参数(scenicId, memberSourceList, faceId, memberId, sampleListIds, isNew)
|
||||||
|
* 2. 调用videoRecreationHandler.handleVideoRecreation()触发视频重切
|
||||||
|
*
|
||||||
|
* 前置条件: context.memberSourceList不为空
|
||||||
|
* 业务说明: 视频重切用于根据人脸识别结果重新生成个性化视频片段
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@StageConfig(
|
||||||
|
stageId = "handle_video_recreation",
|
||||||
|
optionalMode = StageOptionalMode.SUPPORT,
|
||||||
|
description = "处理视频重切逻辑",
|
||||||
|
defaultEnabled = true
|
||||||
|
)
|
||||||
|
public class HandleVideoRecreationStage extends AbstractPipelineStage<FaceMatchingContext> {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private VideoRecreationHandler videoRecreationHandler;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "HandleVideoRecreation";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) {
|
||||||
|
// 只有当memberSourceList不为空时才执行
|
||||||
|
List<MemberSourceEntity> memberSourceList = context.getMemberSourceList();
|
||||||
|
if (memberSourceList == null || memberSourceList.isEmpty()) {
|
||||||
|
log.debug("memberSourceList为空,跳过视频重切,faceId={}", context.getFaceId());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
|
||||||
|
Long scenicId = context.getFace().getScenicId();
|
||||||
|
List<MemberSourceEntity> memberSourceEntityList = context.getMemberSourceList();
|
||||||
|
Long faceId = context.getFaceId();
|
||||||
|
Long memberId = context.getFace().getMemberId();
|
||||||
|
List<Long> sampleListIds = context.getSampleListIds();
|
||||||
|
boolean isNew = context.isNew();
|
||||||
|
|
||||||
|
// 防御性检查:memberSourceList为空
|
||||||
|
if (memberSourceEntityList == null || memberSourceEntityList.isEmpty()) {
|
||||||
|
log.debug("memberSourceList为空,跳过视频重切,faceId={}", faceId);
|
||||||
|
return StageResult.skipped("memberSourceList为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 处理视频重切
|
||||||
|
videoRecreationHandler.handleVideoRecreation(
|
||||||
|
scenicId,
|
||||||
|
memberSourceEntityList,
|
||||||
|
faceId,
|
||||||
|
memberId,
|
||||||
|
sampleListIds,
|
||||||
|
isNew);
|
||||||
|
|
||||||
|
log.info("视频重切处理完成: faceId={}, scenicId={}, 源文件数={}",
|
||||||
|
faceId, scenicId, memberSourceEntityList.size());
|
||||||
|
|
||||||
|
return StageResult.success("视频重切处理完成");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("处理视频重切失败,faceId={}", faceId, e);
|
||||||
|
// 视频重切失败不影响主流程,返回降级
|
||||||
|
return StageResult.degraded("视频重切处理失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package com.ycwl.basic.face.pipeline.stages;
|
||||||
|
|
||||||
|
import com.ycwl.basic.exception.BaseException;
|
||||||
|
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||||
|
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 com.ycwl.basic.mapper.FaceSampleMapper;
|
||||||
|
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载人脸样本Stage
|
||||||
|
* 负责加载用户选择的人脸样本数据
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* 1. 从context.faceSampleIds读取用户选择的样本ID列表
|
||||||
|
* 2. 调用faceSampleMapper.listByIds()加载样本实体
|
||||||
|
* 3. 更新context.faceSamples
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@StageConfig(
|
||||||
|
stageId = "load_face_samples",
|
||||||
|
optionalMode = StageOptionalMode.FORCE_ON,
|
||||||
|
description = "加载用户选择的人脸样本"
|
||||||
|
)
|
||||||
|
public class LoadFaceSamplesStage extends AbstractPipelineStage<FaceMatchingContext> {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private FaceSampleMapper faceSampleMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "LoadFaceSamples";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
|
||||||
|
List<Long> faceSampleIds = context.getFaceSampleIds();
|
||||||
|
Long faceId = context.getFaceId();
|
||||||
|
|
||||||
|
if (faceSampleIds == null || faceSampleIds.isEmpty()) {
|
||||||
|
log.warn("faceSampleIds为空,faceId={}", faceId);
|
||||||
|
return StageResult.failed("faceSampleIds不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
List<FaceSampleEntity> faceSamples = faceSampleMapper.listByIds(faceSampleIds);
|
||||||
|
|
||||||
|
if (faceSamples.isEmpty()) {
|
||||||
|
log.warn("未找到指定的人脸样本,faceSampleIds: {}, faceId={}", faceSampleIds, faceId);
|
||||||
|
throw new BaseException("未找到指定的人脸样本");
|
||||||
|
}
|
||||||
|
|
||||||
|
context.setFaceSamples(faceSamples);
|
||||||
|
|
||||||
|
log.info("加载人脸样本成功: faceId={}, sampleCount={}", faceId, faceSamples.size());
|
||||||
|
return StageResult.success(String.format("加载了%d个人脸样本", faceSamples.size()));
|
||||||
|
|
||||||
|
} catch (BaseException e) {
|
||||||
|
throw e;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("加载人脸样本失败,faceId={}, faceSampleIds={}", faceId, faceSampleIds, e);
|
||||||
|
return StageResult.failed("加载人脸样本失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package com.ycwl.basic.face.pipeline.stages;
|
||||||
|
|
||||||
|
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||||
|
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 com.ycwl.basic.mapper.FaceSampleMapper;
|
||||||
|
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载匹配样本实体Stage
|
||||||
|
* 负责将sampleListIds对应的样本实体加载到context.faceSamples,供后续Stage使用
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* 1. 从context.sampleListIds读取匹配到的样本ID列表
|
||||||
|
* 2. 调用faceSampleMapper.listByIds()批量加载样本实体
|
||||||
|
* 3. 更新context.faceSamples作为样本实体缓存
|
||||||
|
*
|
||||||
|
* 设计目的:
|
||||||
|
* - 避免后续多个Stage重复调用faceSampleMapper.listByIds()
|
||||||
|
* - 统一加载时机,提高性能
|
||||||
|
* - 为后续筛选Stage提供样本实体数据源
|
||||||
|
*
|
||||||
|
* 前置条件: context.sampleListIds不为空
|
||||||
|
*
|
||||||
|
* 应用场景: 自定义匹配场景,在CustomFaceSearchStage之后
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@StageConfig(
|
||||||
|
stageId = "load_matched_samples",
|
||||||
|
optionalMode = StageOptionalMode.UNSUPPORT,
|
||||||
|
description = "加载匹配样本实体到缓存"
|
||||||
|
)
|
||||||
|
public class LoadMatchedSamplesStage extends AbstractPipelineStage<FaceMatchingContext> {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private FaceSampleMapper faceSampleMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "LoadMatchedSamples";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) {
|
||||||
|
// 检查sampleListIds是否为空
|
||||||
|
List<Long> sampleListIds = context.getSampleListIds();
|
||||||
|
if (sampleListIds == null || sampleListIds.isEmpty()) {
|
||||||
|
log.debug("sampleListIds为空,跳过加载匹配样本,faceId={}", context.getFaceId());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
|
||||||
|
List<Long> sampleListIds = context.getSampleListIds();
|
||||||
|
Long faceId = context.getFaceId();
|
||||||
|
|
||||||
|
// 防御性检查:如果sampleListIds为空,直接跳过
|
||||||
|
if (sampleListIds == null || sampleListIds.isEmpty()) {
|
||||||
|
log.debug("sampleListIds为空,跳过加载匹配样本,faceId={}", faceId);
|
||||||
|
return StageResult.skipped("sampleListIds为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 批量加载样本实体
|
||||||
|
List<FaceSampleEntity> faceSamples = faceSampleMapper.listByIds(sampleListIds);
|
||||||
|
|
||||||
|
if (faceSamples == null || faceSamples.isEmpty()) {
|
||||||
|
log.warn("未找到任何匹配样本实体,faceId={}, sampleListIds={}", faceId, sampleListIds);
|
||||||
|
return StageResult.skipped("未找到匹配样本实体");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 存入context缓存,供后续Stage使用
|
||||||
|
context.setFaceSamples(faceSamples);
|
||||||
|
|
||||||
|
log.info("加载匹配样本实体完成: faceId={}, 样本数={}", faceId, faceSamples.size());
|
||||||
|
|
||||||
|
return StageResult.success(String.format("已加载%d个样本实体", faceSamples.size()));
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("加载匹配样本实体失败,faceId={}", faceId, e);
|
||||||
|
// 加载失败影响后续流程,返回失败
|
||||||
|
return StageResult.failed("加载匹配样本实体失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package com.ycwl.basic.face.pipeline.stages;
|
||||||
|
|
||||||
|
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||||
|
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 com.ycwl.basic.mapper.SourceMapper;
|
||||||
|
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
|
||||||
|
import com.ycwl.basic.repository.MemberRelationRepository;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 持久化关联关系Stage
|
||||||
|
* 负责过滤并保存源文件关联关系到数据库
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* 1. 从context.memberSourceList读取关联关系
|
||||||
|
* 2. 过滤已存在的关联关系和无效的source引用
|
||||||
|
* 3. 保存到数据库
|
||||||
|
* 4. 清除缓存
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@StageConfig(
|
||||||
|
stageId = "persist_relations",
|
||||||
|
optionalMode = StageOptionalMode.FORCE_ON,
|
||||||
|
description = "持久化源文件关联关系"
|
||||||
|
)
|
||||||
|
public class PersistRelationsStage extends AbstractPipelineStage<FaceMatchingContext> {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SourceMapper sourceMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MemberRelationRepository memberRelationRepository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "PersistRelations";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) {
|
||||||
|
// 只有当memberSourceList不为空时才执行
|
||||||
|
List<MemberSourceEntity> memberSourceList = context.getMemberSourceList();
|
||||||
|
if (memberSourceList == null || memberSourceList.isEmpty()) {
|
||||||
|
log.debug("memberSourceList为空,跳过持久化,faceId={}", context.getFaceId());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
|
||||||
|
List<MemberSourceEntity> memberSourceEntityList = context.getMemberSourceList();
|
||||||
|
Long faceId = context.getFaceId();
|
||||||
|
|
||||||
|
// 防御性检查:memberSourceList为空
|
||||||
|
if (memberSourceEntityList == null || memberSourceEntityList.isEmpty()) {
|
||||||
|
log.debug("memberSourceList为空,跳过持久化,faceId={}", faceId);
|
||||||
|
return StageResult.skipped("memberSourceList为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 过滤已存在的关联关系
|
||||||
|
List<MemberSourceEntity> existingFiltered = sourceMapper.filterExistingRelations(memberSourceEntityList);
|
||||||
|
|
||||||
|
// 2. 过滤无效的source引用
|
||||||
|
List<MemberSourceEntity> validFiltered = sourceMapper.filterValidSourceRelations(existingFiltered);
|
||||||
|
|
||||||
|
if (!validFiltered.isEmpty()) {
|
||||||
|
// 3. 保存到数据库
|
||||||
|
sourceMapper.addRelations(validFiltered);
|
||||||
|
|
||||||
|
log.debug("创建关联关系: faceId={}, 原始数量={}, 过滤后数量={}",
|
||||||
|
faceId, memberSourceEntityList.size(), validFiltered.size());
|
||||||
|
} else {
|
||||||
|
log.warn("没有有效的关联关系可创建: faceId={}, 原始数量={}",
|
||||||
|
faceId, memberSourceEntityList.size());
|
||||||
|
return StageResult.skipped("没有有效的关联关系可创建");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 清除缓存
|
||||||
|
memberRelationRepository.clearSCacheByFace(faceId);
|
||||||
|
|
||||||
|
return StageResult.success(String.format("持久化了%d条关联关系", validFiltered.size()));
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("持久化关联关系失败,faceId={}", faceId, e);
|
||||||
|
return StageResult.failed("保存关联关系失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package com.ycwl.basic.face.pipeline.stages;
|
||||||
|
|
||||||
|
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||||
|
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 com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
|
||||||
|
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
|
||||||
|
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
||||||
|
import com.ycwl.basic.repository.FaceRepository;
|
||||||
|
import com.ycwl.basic.repository.ScenicRepository;
|
||||||
|
import com.ycwl.basic.service.pc.ScenicService;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 准备上下文Stage
|
||||||
|
* 负责加载人脸实体、景区配置、识别适配器等必要数据
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* 1. 加载FaceEntity(如不存在则失败)
|
||||||
|
* 2. 检查是否人工选择(是则跳过,除非isNew=true)
|
||||||
|
* 3. 加载ScenicConfigManager和IFaceBodyAdapter
|
||||||
|
* 4. 更新Context
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@StageConfig(
|
||||||
|
stageId = "prepare_context",
|
||||||
|
optionalMode = StageOptionalMode.FORCE_ON,
|
||||||
|
description = "准备人脸匹配上下文数据"
|
||||||
|
)
|
||||||
|
public class PrepareContextStage extends AbstractPipelineStage<FaceMatchingContext> {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private FaceRepository faceRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ScenicRepository scenicRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ScenicService scenicService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "PrepareContext";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
|
||||||
|
Long faceId = context.getFaceId();
|
||||||
|
boolean isNew = context.isNew();
|
||||||
|
|
||||||
|
// 1. 加载人脸实体
|
||||||
|
FaceEntity face = faceRepository.getFace(faceId);
|
||||||
|
if (face == null) {
|
||||||
|
log.warn("人脸不存在,faceId: {}", faceId);
|
||||||
|
return StageResult.failed("人脸不存在,faceId: " + faceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.setFace(face);
|
||||||
|
log.debug("加载人脸实体成功: faceId={}, memberId={}, scenicId={}",
|
||||||
|
faceId, face.getMemberId(), face.getScenicId());
|
||||||
|
|
||||||
|
// 2. 检查是否人工选择
|
||||||
|
// 人工选择的无需重新匹配(新用户除外)
|
||||||
|
if (!isNew && Integer.valueOf(1).equals(face.getIsManual())) {
|
||||||
|
log.info("人工选择的人脸,无需匹配,faceId: {}", faceId);
|
||||||
|
return StageResult.skipped("人工选择的人脸,无需重新匹配");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 加载景区配置
|
||||||
|
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(face.getScenicId());
|
||||||
|
context.setScenicConfig(scenicConfig);
|
||||||
|
log.debug("加载景区配置成功: scenicId={}", face.getScenicId());
|
||||||
|
|
||||||
|
// 4. 加载人脸识别适配器
|
||||||
|
IFaceBodyAdapter faceBodyAdapter = scenicService.getScenicFaceBodyAdapter(face.getScenicId());
|
||||||
|
if (faceBodyAdapter == null) {
|
||||||
|
log.error("无法获取人脸识别适配器,scenicId: {}", face.getScenicId());
|
||||||
|
return StageResult.failed("人脸识别服务不可用,请稍后再试");
|
||||||
|
}
|
||||||
|
|
||||||
|
context.setFaceBodyAdapter(faceBodyAdapter);
|
||||||
|
log.debug("加载人脸识别适配器成功: scenicId={}", face.getScenicId());
|
||||||
|
|
||||||
|
return StageResult.success("上下文准备完成");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package com.ycwl.basic.face.pipeline.stages;
|
||||||
|
|
||||||
|
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||||
|
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 com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
|
||||||
|
import com.ycwl.basic.service.pc.processor.BuyStatusProcessor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理购买状态Stage
|
||||||
|
* 负责更新源文件的购买状态标记
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* 1. 从context.memberSourceList读取源文件关联列表
|
||||||
|
* 2. 从context.freeSourceIds读取免费源文件ID列表
|
||||||
|
* 3. 调用buyStatusProcessor.processBuyStatus()更新购买状态
|
||||||
|
*
|
||||||
|
* 前置条件: context.memberSourceList不为空
|
||||||
|
* 业务说明: 购买状态影响前端显示和用户下载权限
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@StageConfig(
|
||||||
|
stageId = "process_buy_status",
|
||||||
|
optionalMode = StageOptionalMode.SUPPORT,
|
||||||
|
description = "处理源文件购买状态",
|
||||||
|
defaultEnabled = true
|
||||||
|
)
|
||||||
|
public class ProcessBuyStatusStage extends AbstractPipelineStage<FaceMatchingContext> {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private BuyStatusProcessor buyStatusProcessor;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "ProcessBuyStatus";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) {
|
||||||
|
// 只有当memberSourceList不为空时才执行
|
||||||
|
List<MemberSourceEntity> memberSourceList = context.getMemberSourceList();
|
||||||
|
if (memberSourceList == null || memberSourceList.isEmpty()) {
|
||||||
|
log.debug("memberSourceList为空,跳过购买状态处理,faceId={}", context.getFaceId());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
|
||||||
|
List<MemberSourceEntity> memberSourceEntityList = context.getMemberSourceList();
|
||||||
|
List<Long> freeSourceIds = context.getFreeSourceIds();
|
||||||
|
Long memberId = context.getFace().getMemberId();
|
||||||
|
Long scenicId = context.getFace().getScenicId();
|
||||||
|
Long faceId = context.getFaceId();
|
||||||
|
|
||||||
|
// 防御性检查:memberSourceList为空
|
||||||
|
if (memberSourceEntityList == null || memberSourceEntityList.isEmpty()) {
|
||||||
|
log.debug("memberSourceList为空,跳过购买状态处理,faceId={}", faceId);
|
||||||
|
return StageResult.skipped("memberSourceList为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 处理购买状态
|
||||||
|
buyStatusProcessor.processBuyStatus(
|
||||||
|
memberSourceEntityList,
|
||||||
|
freeSourceIds,
|
||||||
|
memberId,
|
||||||
|
scenicId,
|
||||||
|
faceId);
|
||||||
|
|
||||||
|
log.info("购买状态处理完成: faceId={}, 源文件数={}, 免费数={}",
|
||||||
|
faceId, memberSourceEntityList.size(),
|
||||||
|
freeSourceIds != null ? freeSourceIds.size() : 0);
|
||||||
|
|
||||||
|
return StageResult.success("购买状态处理完成");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("处理购买状态失败,faceId={}", faceId, e);
|
||||||
|
// 购买状态处理失败不影响主流程,返回降级
|
||||||
|
return StageResult.degraded("购买状态处理失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package com.ycwl.basic.face.pipeline.stages;
|
||||||
|
|
||||||
|
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||||
|
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 com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
|
||||||
|
import com.ycwl.basic.service.pc.processor.SourceRelationProcessor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理免费源文件Stage
|
||||||
|
* 负责根据业务规则确定哪些源文件可以免费访问
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* 1. 从context.memberSourceList读取源文件关联列表
|
||||||
|
* 2. 调用sourceRelationProcessor.processFreeSourceLogic()确定免费源文件
|
||||||
|
* 3. 更新context.freeSourceIds
|
||||||
|
*
|
||||||
|
* 前置条件: context.memberSourceList不为空
|
||||||
|
* 后置条件: context.freeSourceIds已设置
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@StageConfig(
|
||||||
|
stageId = "process_free_source",
|
||||||
|
optionalMode = StageOptionalMode.SUPPORT,
|
||||||
|
description = "处理免费源文件逻辑",
|
||||||
|
defaultEnabled = true
|
||||||
|
)
|
||||||
|
public class ProcessFreeSourceStage extends AbstractPipelineStage<FaceMatchingContext> {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SourceRelationProcessor sourceRelationProcessor;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "ProcessFreeSource";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) {
|
||||||
|
// 只有当memberSourceList不为空时才执行
|
||||||
|
List<MemberSourceEntity> memberSourceList = context.getMemberSourceList();
|
||||||
|
if (memberSourceList == null || memberSourceList.isEmpty()) {
|
||||||
|
log.debug("memberSourceList为空,跳过免费逻辑,faceId={}", context.getFaceId());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
|
||||||
|
List<MemberSourceEntity> memberSourceEntityList = context.getMemberSourceList();
|
||||||
|
Long scenicId = context.getFace().getScenicId();
|
||||||
|
boolean isNew = context.isNew();
|
||||||
|
Long faceId = context.getFaceId();
|
||||||
|
|
||||||
|
// 防御性检查:memberSourceList为空
|
||||||
|
if (memberSourceEntityList == null || memberSourceEntityList.isEmpty()) {
|
||||||
|
log.debug("memberSourceList为空,跳过免费逻辑,faceId={}", faceId);
|
||||||
|
return StageResult.skipped("memberSourceList为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 处理免费逻辑
|
||||||
|
List<Long> freeSourceIds = sourceRelationProcessor.processFreeSourceLogic(
|
||||||
|
memberSourceEntityList, scenicId, isNew);
|
||||||
|
|
||||||
|
context.setFreeSourceIds(freeSourceIds);
|
||||||
|
|
||||||
|
log.info("免费源文件处理完成: faceId={}, 总源文件数={}, 免费数={}",
|
||||||
|
faceId, memberSourceEntityList.size(), freeSourceIds != null ? freeSourceIds.size() : 0);
|
||||||
|
|
||||||
|
return StageResult.success(String.format("确定了%d个免费源文件",
|
||||||
|
freeSourceIds != null ? freeSourceIds.size() : 0));
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("处理免费源文件失败,faceId={}", faceId, e);
|
||||||
|
// 免费逻辑失败不影响主流程,返回降级
|
||||||
|
return StageResult.degraded("免费源文件处理失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package com.ycwl.basic.face.pipeline.stages;
|
||||||
|
|
||||||
|
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||||
|
import com.ycwl.basic.face.pipeline.enums.FaceMatchingScene;
|
||||||
|
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 com.ycwl.basic.service.pc.helper.FaceMetricsRecorder;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录自定义匹配次数Stage
|
||||||
|
* 负责记录自定义人脸匹配调用次数,用于监控
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* 1. 仅在CUSTOM_MATCHING场景执行
|
||||||
|
* 2. 调用metricsRecorder.recordCustomMatchCount()记录次数
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@StageConfig(
|
||||||
|
stageId = "record_custom_match_metrics",
|
||||||
|
optionalMode = StageOptionalMode.SUPPORT,
|
||||||
|
description = "记录自定义匹配指标",
|
||||||
|
defaultEnabled = true
|
||||||
|
)
|
||||||
|
public class RecordCustomMatchMetricsStage extends AbstractPipelineStage<FaceMatchingContext> {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private FaceMetricsRecorder metricsRecorder;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "RecordCustomMatchMetrics";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) {
|
||||||
|
// 只有自定义匹配场景才执行
|
||||||
|
if (context.getScene() != FaceMatchingScene.CUSTOM_MATCHING) {
|
||||||
|
log.debug("非自定义匹配场景,跳过记录,faceId={}", context.getFaceId());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
|
||||||
|
Long faceId = context.getFaceId();
|
||||||
|
|
||||||
|
// 防御性检查:只有自定义匹配场景才执行
|
||||||
|
if (context.getScene() != FaceMatchingScene.CUSTOM_MATCHING) {
|
||||||
|
log.debug("非自定义匹配场景,跳过记录,faceId={}", faceId);
|
||||||
|
return StageResult.skipped("非自定义匹配场景");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
metricsRecorder.recordCustomMatchCount(faceId);
|
||||||
|
log.debug("记录自定义匹配次数: faceId={}", faceId);
|
||||||
|
return StageResult.success("自定义匹配指标记录完成");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("记录自定义匹配指标失败,faceId={}", faceId, e);
|
||||||
|
// 指标记录失败不影响主流程,返回降级
|
||||||
|
return StageResult.degraded("指标记录失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package com.ycwl.basic.face.pipeline.stages;
|
||||||
|
|
||||||
|
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||||
|
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 com.ycwl.basic.service.pc.helper.FaceMetricsRecorder;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录识别次数Stage
|
||||||
|
* 负责记录人脸识别调用次数,用于监控和防重复检查
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* 1. 调用metricsRecorder.recordRecognitionCount()记录识别次数
|
||||||
|
* 2. 检查searchResult是否触发低阈值检测
|
||||||
|
* 3. 如果是,调用metricsRecorder.recordLowThreshold()记录
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@StageConfig(
|
||||||
|
stageId = "record_metrics",
|
||||||
|
optionalMode = StageOptionalMode.SUPPORT,
|
||||||
|
description = "记录人脸识别指标",
|
||||||
|
defaultEnabled = true
|
||||||
|
)
|
||||||
|
public class RecordMetricsStage extends AbstractPipelineStage<FaceMatchingContext> {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private FaceMetricsRecorder metricsRecorder;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "RecordMetrics";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
|
||||||
|
Long faceId = context.getFaceId();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 记录识别次数
|
||||||
|
metricsRecorder.recordRecognitionCount(faceId);
|
||||||
|
log.debug("记录识别次数: faceId={}", faceId);
|
||||||
|
|
||||||
|
// 2. 检查是否触发低阈值检测
|
||||||
|
if (context.getSearchResult() != null && context.getSearchResult().isLowThreshold()) {
|
||||||
|
metricsRecorder.recordLowThreshold(faceId);
|
||||||
|
log.debug("触发低阈值检测,记录faceId: {}", faceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return StageResult.success("识别指标记录完成");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("记录识别指标失败,faceId={}", faceId, e);
|
||||||
|
// 指标记录失败不影响主流程,返回降级
|
||||||
|
return StageResult.degraded("指标记录失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package com.ycwl.basic.face.pipeline.stages;
|
||||||
|
|
||||||
|
import com.ycwl.basic.biz.TaskStatusBiz;
|
||||||
|
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置任务状态Stage
|
||||||
|
* 负责为新用户设置任务状态为待处理
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* 1. 仅在isNew=true时执行
|
||||||
|
* 2. 调用taskStatusBiz.setFaceCutStatus(faceId, 0)
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@StageConfig(
|
||||||
|
stageId = "set_task_status",
|
||||||
|
optionalMode = StageOptionalMode.FORCE_ON,
|
||||||
|
description = "设置新用户任务状态"
|
||||||
|
)
|
||||||
|
public class SetTaskStatusStage extends AbstractPipelineStage<FaceMatchingContext> {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TaskStatusBiz taskStatusBiz;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "SetTaskStatus";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) {
|
||||||
|
// 只有新用户才执行
|
||||||
|
if (!context.isNew()) {
|
||||||
|
log.debug("非新用户,跳过设置任务状态,faceId={}", context.getFaceId());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
|
||||||
|
Long faceId = context.getFaceId();
|
||||||
|
|
||||||
|
// 防御性检查:只有新用户才执行
|
||||||
|
if (!context.isNew()) {
|
||||||
|
log.debug("非新用户,跳过设置任务状态,faceId={}", faceId);
|
||||||
|
return StageResult.skipped("非新用户");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
taskStatusBiz.setFaceCutStatus(faceId, 0);
|
||||||
|
log.debug("设置新用户任务状态: faceId={}, status=0", faceId);
|
||||||
|
return StageResult.success("任务状态已设置");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("设置任务状态失败,faceId={}", faceId, e);
|
||||||
|
// 任务状态设置失败不影响主流程,返回降级
|
||||||
|
return StageResult.degraded("任务状态设置失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package com.ycwl.basic.face.pipeline.stages;
|
||||||
|
|
||||||
|
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||||
|
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 com.ycwl.basic.mapper.FaceMapper;
|
||||||
|
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
||||||
|
import com.ycwl.basic.model.task.resp.SearchFaceRespVo;
|
||||||
|
import com.ycwl.basic.repository.FaceRepository;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新人脸结果Stage
|
||||||
|
* 负责将人脸识别结果保存到数据库
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* 1. 从context.searchResult读取识别结果
|
||||||
|
* 2. 更新FaceEntity(score、matchResult、firstMatchRate、matchSampleIds)
|
||||||
|
* 3. 清除缓存
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@StageConfig(
|
||||||
|
stageId = "update_face_result",
|
||||||
|
optionalMode = StageOptionalMode.FORCE_ON,
|
||||||
|
description = "更新人脸识别结果到数据库"
|
||||||
|
)
|
||||||
|
public class UpdateFaceResultStage extends AbstractPipelineStage<FaceMatchingContext> {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private FaceMapper faceMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private FaceRepository faceRepository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "UpdateFaceResult";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
|
||||||
|
SearchFaceRespVo searchResult = context.getSearchResult();
|
||||||
|
if (searchResult == null) {
|
||||||
|
log.warn("searchResult为空,跳过更新人脸结果,faceId={}", context.getFaceId());
|
||||||
|
return StageResult.skipped("searchResult为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
FaceEntity originalFace = context.getFace();
|
||||||
|
Long faceId = context.getFaceId();
|
||||||
|
|
||||||
|
FaceEntity faceEntity = new FaceEntity();
|
||||||
|
faceEntity.setId(faceId);
|
||||||
|
faceEntity.setScore(searchResult.getScore());
|
||||||
|
faceEntity.setMatchResult(searchResult.getSearchResultJson());
|
||||||
|
|
||||||
|
if (searchResult.getFirstMatchRate() != null) {
|
||||||
|
faceEntity.setFirstMatchRate(BigDecimal.valueOf(searchResult.getFirstMatchRate()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchResult.getSampleListIds() != null) {
|
||||||
|
faceEntity.setMatchSampleIds(searchResult.getSampleListIds().stream()
|
||||||
|
.map(String::valueOf)
|
||||||
|
.collect(Collectors.joining(",")));
|
||||||
|
}
|
||||||
|
|
||||||
|
faceEntity.setCreateAt(new Date());
|
||||||
|
faceEntity.setScenicId(originalFace.getScenicId());
|
||||||
|
faceEntity.setMemberId(originalFace.getMemberId());
|
||||||
|
faceEntity.setFaceUrl(originalFace.getFaceUrl());
|
||||||
|
|
||||||
|
faceMapper.update(faceEntity);
|
||||||
|
faceRepository.clearFaceCache(faceId);
|
||||||
|
|
||||||
|
log.debug("人脸结果更新成功:faceId={}, score={}, sampleCount={}",
|
||||||
|
faceId, searchResult.getScore(),
|
||||||
|
searchResult.getSampleListIds() != null ? searchResult.getSampleListIds().size() : 0);
|
||||||
|
|
||||||
|
return StageResult.success("人脸结果更新成功");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("更新人脸结果失败,faceId={}", context.getFaceId(), e);
|
||||||
|
return StageResult.failed("保存人脸识别结果失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -418,7 +418,7 @@ public class BceFaceBodyAdapter implements IFaceBodyAdapter {
|
|||||||
ByteArrayOutputStream baos = null;
|
ByteArrayOutputStream baos = null;
|
||||||
try {
|
try {
|
||||||
// 下载图片
|
// 下载图片
|
||||||
URL url = new URL(imageUrl.replace("oss-cn-shanghai.aliyuncs.com", "oss-cn-shanghai-internal.aliyuncs.com"));
|
URL url = new URL(imageUrl);
|
||||||
image = ImageIO.read(url);
|
image = ImageIO.read(url);
|
||||||
if (image == null) {
|
if (image == null) {
|
||||||
log.error("无法读取图片,URL: {}", imageUrl);
|
log.error("无法读取图片,URL: {}", imageUrl);
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
package com.ycwl.basic.image.pipeline.annotation;
|
|
||||||
|
|
||||||
import com.ycwl.basic.image.pipeline.enums.StageOptionalMode;
|
|
||||||
|
|
||||||
import java.lang.annotation.Documented;
|
|
||||||
import java.lang.annotation.ElementType;
|
|
||||||
import java.lang.annotation.Retention;
|
|
||||||
import java.lang.annotation.RetentionPolicy;
|
|
||||||
import java.lang.annotation.Target;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stage配置注解
|
|
||||||
* 用于声明Stage的元数据和可选性控制信息
|
|
||||||
*/
|
|
||||||
@Target(ElementType.TYPE)
|
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
|
||||||
@Documented
|
|
||||||
public @interface StageConfig {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stage的唯一标识
|
|
||||||
* 用于外部配置引用该Stage
|
|
||||||
* 例如: "watermark", "download", "upload"
|
|
||||||
*/
|
|
||||||
String stageId();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 可选性模式
|
|
||||||
* 默认为UNSUPPORT(不支持外部控制)
|
|
||||||
*/
|
|
||||||
StageOptionalMode optionalMode() default StageOptionalMode.UNSUPPORT;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stage描述信息
|
|
||||||
* 用于文档和日志说明
|
|
||||||
*/
|
|
||||||
String description() default "";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 默认是否启用
|
|
||||||
* 仅当optionalMode=SUPPORT时有效
|
|
||||||
* 当外部配置未明确指定时,使用此默认值
|
|
||||||
*/
|
|
||||||
boolean defaultEnabled() default true;
|
|
||||||
}
|
|
||||||
@@ -8,6 +8,7 @@ import com.ycwl.basic.model.Crop;
|
|||||||
import com.ycwl.basic.model.PrinterOrderItem;
|
import com.ycwl.basic.model.PrinterOrderItem;
|
||||||
import com.ycwl.basic.image.pipeline.util.TempFileManager;
|
import com.ycwl.basic.image.pipeline.util.TempFileManager;
|
||||||
import com.ycwl.basic.storage.adapters.IStorageAdapter;
|
import com.ycwl.basic.storage.adapters.IStorageAdapter;
|
||||||
|
import com.ycwl.basic.pipeline.core.PipelineContext;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
@@ -23,7 +24,7 @@ import java.util.function.Consumer;
|
|||||||
*/
|
*/
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
public class PhotoProcessContext {
|
public class PhotoProcessContext implements PipelineContext {
|
||||||
|
|
||||||
// ==================== 核心字段(构造时必填)====================
|
// ==================== 核心字段(构造时必填)====================
|
||||||
|
|
||||||
@@ -192,6 +193,7 @@ public class PhotoProcessContext {
|
|||||||
* @param defaultEnabled 默认值(当配置未指定时使用)
|
* @param defaultEnabled 默认值(当配置未指定时使用)
|
||||||
* @return true-启用, false-禁用
|
* @return true-启用, false-禁用
|
||||||
*/
|
*/
|
||||||
|
@Override
|
||||||
public boolean isStageEnabled(String stageId, boolean defaultEnabled) {
|
public boolean isStageEnabled(String stageId, boolean defaultEnabled) {
|
||||||
return stageEnabledMap.getOrDefault(stageId, defaultEnabled);
|
return stageEnabledMap.getOrDefault(stageId, defaultEnabled);
|
||||||
}
|
}
|
||||||
@@ -202,6 +204,7 @@ public class PhotoProcessContext {
|
|||||||
* @param stageId Stage唯一标识
|
* @param stageId Stage唯一标识
|
||||||
* @return true-启用, false-禁用
|
* @return true-启用, false-禁用
|
||||||
*/
|
*/
|
||||||
|
@Override
|
||||||
public boolean isStageEnabled(String stageId) {
|
public boolean isStageEnabled(String stageId) {
|
||||||
return stageEnabledMap.getOrDefault(stageId, false);
|
return stageEnabledMap.getOrDefault(stageId, false);
|
||||||
}
|
}
|
||||||
@@ -292,6 +295,7 @@ public class PhotoProcessContext {
|
|||||||
/**
|
/**
|
||||||
* 清理所有临时文件
|
* 清理所有临时文件
|
||||||
*/
|
*/
|
||||||
|
@Override
|
||||||
public void cleanup() {
|
public void cleanup() {
|
||||||
if (cleaned) {
|
if (cleaned) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
package com.ycwl.basic.image.pipeline.core;
|
|
||||||
|
|
||||||
import com.ycwl.basic.image.pipeline.annotation.StageConfig;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 管线处理阶段接口
|
|
||||||
* 每个Stage负责一个独立的图片处理步骤
|
|
||||||
*
|
|
||||||
* @param <C> Context类型
|
|
||||||
*/
|
|
||||||
public interface PipelineStage<C extends PhotoProcessContext> {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取Stage名称(用于日志和监控)
|
|
||||||
*/
|
|
||||||
String getName();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断是否需要执行此Stage
|
|
||||||
* 支持条件性执行(如:只有竖图才需要旋转)
|
|
||||||
*
|
|
||||||
* @param context 管线上下文
|
|
||||||
* @return true-执行, false-跳过
|
|
||||||
*/
|
|
||||||
boolean shouldExecute(C context);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 执行Stage处理逻辑
|
|
||||||
*
|
|
||||||
* @param context 管线上下文
|
|
||||||
* @return 执行结果
|
|
||||||
*/
|
|
||||||
StageResult<C> execute(C context);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取Stage的执行优先级(用于排序)
|
|
||||||
* 数值越小优先级越高,默认为100
|
|
||||||
*/
|
|
||||||
default int getPriority() {
|
|
||||||
return 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取Stage配置注解(用于反射读取可选性控制信息)
|
|
||||||
* @return Stage配置注解,如果未标注则返回null
|
|
||||||
*/
|
|
||||||
default StageConfig getStageConfig() {
|
|
||||||
return this.getClass().getAnnotation(StageConfig.class);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
package com.ycwl.basic.image.pipeline.enums;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stage可选性模式枚举
|
|
||||||
* 定义Stage是否支持外部配置控制
|
|
||||||
*/
|
|
||||||
@Getter
|
|
||||||
public enum StageOptionalMode {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 不支持外部控制
|
|
||||||
* Stage的执行完全由代码中的业务逻辑决定
|
|
||||||
*/
|
|
||||||
UNSUPPORT("不支持外部控制"),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 支持外部控制
|
|
||||||
* Stage可以通过景区配置或请求参数进行开启/关闭
|
|
||||||
*/
|
|
||||||
SUPPORT("支持外部控制"),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 强制开启
|
|
||||||
* Stage必须执行,不允许外部配置关闭
|
|
||||||
*/
|
|
||||||
FORCE_ON("强制开启");
|
|
||||||
|
|
||||||
private final String description;
|
|
||||||
|
|
||||||
StageOptionalMode(String description) {
|
|
||||||
this.description = description;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.ycwl.basic.image.pipeline.exception;
|
package com.ycwl.basic.image.pipeline.exception;
|
||||||
|
|
||||||
|
import com.ycwl.basic.pipeline.exception.PipelineException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stage执行异常
|
* Stage执行异常
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
package com.ycwl.basic.image.pipeline.stages;
|
package com.ycwl.basic.image.pipeline.stages;
|
||||||
|
|
||||||
import com.ycwl.basic.image.pipeline.annotation.StageConfig;
|
|
||||||
import com.ycwl.basic.image.pipeline.core.AbstractPipelineStage;
|
|
||||||
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
||||||
import com.ycwl.basic.image.pipeline.core.StageResult;
|
import com.ycwl.basic.pipeline.annotation.StageConfig;
|
||||||
import com.ycwl.basic.image.pipeline.enums.StageOptionalMode;
|
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 lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package com.ycwl.basic.image.pipeline.stages;
|
package com.ycwl.basic.image.pipeline.stages;
|
||||||
|
|
||||||
import com.ycwl.basic.image.pipeline.annotation.StageConfig;
|
|
||||||
import com.ycwl.basic.image.pipeline.core.AbstractPipelineStage;
|
|
||||||
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
||||||
import com.ycwl.basic.image.pipeline.core.StageResult;
|
|
||||||
import com.ycwl.basic.image.pipeline.enums.ImageType;
|
import com.ycwl.basic.image.pipeline.enums.ImageType;
|
||||||
import com.ycwl.basic.image.pipeline.enums.StageOptionalMode;
|
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 com.ycwl.basic.utils.ImageUtils;
|
import com.ycwl.basic.utils.ImageUtils;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
package com.ycwl.basic.image.pipeline.stages;
|
package com.ycwl.basic.image.pipeline.stages;
|
||||||
|
|
||||||
import cn.hutool.http.HttpUtil;
|
import cn.hutool.http.HttpUtil;
|
||||||
import com.ycwl.basic.image.pipeline.annotation.StageConfig;
|
|
||||||
import com.ycwl.basic.image.pipeline.core.AbstractPipelineStage;
|
|
||||||
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
||||||
import com.ycwl.basic.image.pipeline.core.StageResult;
|
|
||||||
import com.ycwl.basic.image.pipeline.enums.ImageType;
|
import com.ycwl.basic.image.pipeline.enums.ImageType;
|
||||||
import com.ycwl.basic.image.pipeline.enums.StageOptionalMode;
|
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 lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ package com.ycwl.basic.image.pipeline.stages;
|
|||||||
|
|
||||||
import com.ycwl.basic.image.enhancer.adapter.BceImageEnhancer;
|
import com.ycwl.basic.image.enhancer.adapter.BceImageEnhancer;
|
||||||
import com.ycwl.basic.image.enhancer.entity.BceEnhancerConfig;
|
import com.ycwl.basic.image.enhancer.entity.BceEnhancerConfig;
|
||||||
import com.ycwl.basic.image.pipeline.annotation.StageConfig;
|
|
||||||
import com.ycwl.basic.image.pipeline.core.AbstractPipelineStage;
|
|
||||||
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
||||||
import com.ycwl.basic.image.pipeline.core.StageResult;
|
|
||||||
import com.ycwl.basic.image.pipeline.enums.ImageSource;
|
import com.ycwl.basic.image.pipeline.enums.ImageSource;
|
||||||
import com.ycwl.basic.image.pipeline.enums.StageOptionalMode;
|
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 lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package com.ycwl.basic.image.pipeline.stages;
|
package com.ycwl.basic.image.pipeline.stages;
|
||||||
|
|
||||||
import com.ycwl.basic.image.pipeline.enums.ImageSource;
|
|
||||||
import com.ycwl.basic.model.Crop;
|
|
||||||
import com.ycwl.basic.image.pipeline.annotation.StageConfig;
|
|
||||||
import com.ycwl.basic.image.pipeline.core.AbstractPipelineStage;
|
|
||||||
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
||||||
import com.ycwl.basic.image.pipeline.core.StageResult;
|
import com.ycwl.basic.image.pipeline.enums.ImageSource;
|
||||||
import com.ycwl.basic.image.pipeline.enums.ImageType;
|
import com.ycwl.basic.image.pipeline.enums.ImageType;
|
||||||
import com.ycwl.basic.image.pipeline.enums.StageOptionalMode;
|
import com.ycwl.basic.model.Crop;
|
||||||
|
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 com.ycwl.basic.utils.ImageUtils;
|
import com.ycwl.basic.utils.ImageUtils;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package com.ycwl.basic.image.pipeline.stages;
|
package com.ycwl.basic.image.pipeline.stages;
|
||||||
|
|
||||||
import com.ycwl.basic.image.pipeline.annotation.StageConfig;
|
|
||||||
import com.ycwl.basic.image.pipeline.core.AbstractPipelineStage;
|
|
||||||
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
||||||
import com.ycwl.basic.image.pipeline.core.StageResult;
|
|
||||||
import com.ycwl.basic.image.pipeline.enums.ImageType;
|
import com.ycwl.basic.image.pipeline.enums.ImageType;
|
||||||
import com.ycwl.basic.image.pipeline.enums.StageOptionalMode;
|
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 com.ycwl.basic.utils.ImageUtils;
|
import com.ycwl.basic.utils.ImageUtils;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ package com.ycwl.basic.image.pipeline.stages;
|
|||||||
|
|
||||||
import com.ycwl.basic.image.enhancer.adapter.BceImageSR;
|
import com.ycwl.basic.image.enhancer.adapter.BceImageSR;
|
||||||
import com.ycwl.basic.image.enhancer.entity.BceEnhancerConfig;
|
import com.ycwl.basic.image.enhancer.entity.BceEnhancerConfig;
|
||||||
import com.ycwl.basic.image.pipeline.annotation.StageConfig;
|
|
||||||
import com.ycwl.basic.image.pipeline.core.AbstractPipelineStage;
|
|
||||||
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
||||||
import com.ycwl.basic.image.pipeline.core.StageResult;
|
|
||||||
import com.ycwl.basic.image.pipeline.enums.ImageSource;
|
import com.ycwl.basic.image.pipeline.enums.ImageSource;
|
||||||
import com.ycwl.basic.image.pipeline.enums.StageOptionalMode;
|
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 lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package com.ycwl.basic.image.pipeline.stages;
|
package com.ycwl.basic.image.pipeline.stages;
|
||||||
|
|
||||||
import com.ycwl.basic.image.pipeline.core.AbstractPipelineStage;
|
|
||||||
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
||||||
import com.ycwl.basic.image.pipeline.core.StageResult;
|
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
|
||||||
|
import com.ycwl.basic.pipeline.core.StageResult;
|
||||||
|
|
||||||
public class NoOpStage extends AbstractPipelineStage<PhotoProcessContext> {
|
public class NoOpStage extends AbstractPipelineStage<PhotoProcessContext> {
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package com.ycwl.basic.image.pipeline.stages;
|
package com.ycwl.basic.image.pipeline.stages;
|
||||||
|
|
||||||
import com.ycwl.basic.image.pipeline.annotation.StageConfig;
|
|
||||||
import com.ycwl.basic.image.pipeline.core.AbstractPipelineStage;
|
|
||||||
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
||||||
import com.ycwl.basic.image.pipeline.core.StageResult;
|
|
||||||
import com.ycwl.basic.image.pipeline.enums.ImageType;
|
import com.ycwl.basic.image.pipeline.enums.ImageType;
|
||||||
import com.ycwl.basic.image.pipeline.enums.StageOptionalMode;
|
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 com.ycwl.basic.utils.ImageUtils;
|
import com.ycwl.basic.utils.ImageUtils;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package com.ycwl.basic.image.pipeline.stages;
|
package com.ycwl.basic.image.pipeline.stages;
|
||||||
|
|
||||||
import com.ycwl.basic.image.pipeline.annotation.StageConfig;
|
|
||||||
import com.ycwl.basic.image.pipeline.core.AbstractPipelineStage;
|
|
||||||
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
||||||
import com.ycwl.basic.image.pipeline.core.StageResult;
|
|
||||||
import com.ycwl.basic.image.pipeline.enums.ImageType;
|
import com.ycwl.basic.image.pipeline.enums.ImageType;
|
||||||
import com.ycwl.basic.image.pipeline.enums.StageOptionalMode;
|
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 com.ycwl.basic.utils.ImageUtils;
|
import com.ycwl.basic.utils.ImageUtils;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
package com.ycwl.basic.image.pipeline.stages;
|
package com.ycwl.basic.image.pipeline.stages;
|
||||||
|
|
||||||
import com.ycwl.basic.image.pipeline.annotation.StageConfig;
|
|
||||||
import com.ycwl.basic.image.pipeline.core.AbstractPipelineStage;
|
|
||||||
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
||||||
import com.ycwl.basic.image.pipeline.core.StageResult;
|
import com.ycwl.basic.pipeline.annotation.StageConfig;
|
||||||
import com.ycwl.basic.image.pipeline.enums.StageOptionalMode;
|
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
|
||||||
|
import com.ycwl.basic.pipeline.core.StageResult;
|
||||||
|
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
|
||||||
import com.ycwl.basic.service.pc.SourceService;
|
import com.ycwl.basic.service.pc.SourceService;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
package com.ycwl.basic.image.pipeline.stages;
|
package com.ycwl.basic.image.pipeline.stages;
|
||||||
|
|
||||||
import com.ycwl.basic.image.pipeline.annotation.StageConfig;
|
|
||||||
import com.ycwl.basic.image.pipeline.core.AbstractPipelineStage;
|
|
||||||
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
||||||
import com.ycwl.basic.image.pipeline.core.StageResult;
|
import com.ycwl.basic.pipeline.annotation.StageConfig;
|
||||||
import com.ycwl.basic.image.pipeline.enums.StageOptionalMode;
|
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
|
||||||
|
import com.ycwl.basic.pipeline.core.StageResult;
|
||||||
|
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
|
||||||
import com.ycwl.basic.storage.StorageFactory;
|
import com.ycwl.basic.storage.StorageFactory;
|
||||||
import com.ycwl.basic.storage.adapters.IStorageAdapter;
|
import com.ycwl.basic.storage.adapters.IStorageAdapter;
|
||||||
import com.ycwl.basic.storage.enums.StorageAcl;
|
import com.ycwl.basic.storage.enums.StorageAcl;
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
package com.ycwl.basic.image.pipeline.stages;
|
package com.ycwl.basic.image.pipeline.stages;
|
||||||
|
|
||||||
|
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
||||||
|
import com.ycwl.basic.image.pipeline.enums.ImageType;
|
||||||
import com.ycwl.basic.image.watermark.ImageWatermarkFactory;
|
import com.ycwl.basic.image.watermark.ImageWatermarkFactory;
|
||||||
import com.ycwl.basic.image.watermark.entity.WatermarkInfo;
|
import com.ycwl.basic.image.watermark.entity.WatermarkInfo;
|
||||||
import com.ycwl.basic.image.watermark.enums.ImageWatermarkOperatorEnum;
|
import com.ycwl.basic.image.watermark.enums.ImageWatermarkOperatorEnum;
|
||||||
import com.ycwl.basic.image.watermark.operator.IOperator;
|
import com.ycwl.basic.image.watermark.operator.IOperator;
|
||||||
import com.ycwl.basic.image.pipeline.annotation.StageConfig;
|
import com.ycwl.basic.pipeline.annotation.StageConfig;
|
||||||
import com.ycwl.basic.image.pipeline.core.AbstractPipelineStage;
|
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
|
||||||
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
import com.ycwl.basic.pipeline.core.StageResult;
|
||||||
import com.ycwl.basic.image.pipeline.core.StageResult;
|
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
|
||||||
import com.ycwl.basic.image.pipeline.enums.ImageType;
|
|
||||||
import com.ycwl.basic.image.pipeline.enums.StageOptionalMode;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,8 @@ public interface DeviceV2Client {
|
|||||||
@RequestParam(value = "no", required = false) String no,
|
@RequestParam(value = "no", required = false) String no,
|
||||||
@RequestParam(value = "type", required = false) String type,
|
@RequestParam(value = "type", required = false) String type,
|
||||||
@RequestParam(value = "isActive", required = false) Integer isActive,
|
@RequestParam(value = "isActive", required = false) Integer isActive,
|
||||||
@RequestParam(value = "scenicId", required = false) Long scenicId);
|
@RequestParam(value = "scenicId", required = false) Long scenicId,
|
||||||
|
@RequestParam(value = "scenicIds", required = false) String scenicIds);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据配置条件筛选设备
|
* 根据配置条件筛选设备
|
||||||
|
|||||||
@@ -67,11 +67,28 @@ public class DeviceIntegrationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public PageResponse<DeviceV2DTO> listDevices(Integer page, Integer pageSize, String name, String no,
|
public PageResponse<DeviceV2DTO> listDevices(Integer page, Integer pageSize, String name, String no,
|
||||||
String type, Integer isActive, Long scenicId) {
|
String type, Integer isActive, Long scenicId, String scenicIds) {
|
||||||
log.debug("分页查询设备列表, page: {}, pageSize: {}, name: {}, no: {}, type: {}, isActive: {}, scenicId: {}",
|
log.debug("分页查询设备列表, page: {}, pageSize: {}, name: {}, no: {}, type: {}, isActive: {}, scenicId: {}, scenicIds: {}",
|
||||||
page, pageSize, name, no, type, isActive, scenicId);
|
page, pageSize, name, no, type, isActive, scenicId, scenicIds);
|
||||||
|
|
||||||
|
// 参数优先级处理:scenicId 优先于 scenicIds
|
||||||
|
Long finalScenicId = null;
|
||||||
|
String finalScenicIds = null;
|
||||||
|
|
||||||
|
if (scenicId != null) {
|
||||||
|
// 优先使用单个 scenicId(向后兼容)
|
||||||
|
finalScenicId = scenicId;
|
||||||
|
finalScenicIds = null;
|
||||||
|
log.debug("使用单个 scenicId 参数: {}", finalScenicId);
|
||||||
|
} else if (scenicIds != null && !scenicIds.trim().isEmpty()) {
|
||||||
|
// 使用 scenicIds
|
||||||
|
finalScenicId = null;
|
||||||
|
finalScenicIds = scenicIds;
|
||||||
|
log.debug("使用 scenicIds 参数: {}", finalScenicIds);
|
||||||
|
}
|
||||||
|
|
||||||
CommonResponse<PageResponse<DeviceV2DTO>> response = deviceV2Client.listDevices(
|
CommonResponse<PageResponse<DeviceV2DTO>> response = deviceV2Client.listDevices(
|
||||||
page, pageSize, name, no, type, isActive, scenicId);
|
page, pageSize, name, no, type, isActive, finalScenicId, finalScenicIds);
|
||||||
return handleResponse(response, "分页查询设备列表失败");
|
return handleResponse(response, "分页查询设备列表失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,14 +180,14 @@ public class DeviceIntegrationService {
|
|||||||
* 获取景区的IPC设备列表
|
* 获取景区的IPC设备列表
|
||||||
*/
|
*/
|
||||||
public PageResponse<DeviceV2DTO> getScenicIpcDevices(Long scenicId, Integer page, Integer pageSize) {
|
public PageResponse<DeviceV2DTO> getScenicIpcDevices(Long scenicId, Integer page, Integer pageSize) {
|
||||||
return listDevices(page, pageSize, null, null, "IPC", 1, scenicId);
|
return listDevices(page, pageSize, null, null, "IPC", 1, scenicId, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取景区的所有激活设备
|
* 获取景区的所有激活设备
|
||||||
*/
|
*/
|
||||||
public PageResponse<DeviceV2DTO> getScenicActiveDevices(Long scenicId, Integer page, Integer pageSize) {
|
public PageResponse<DeviceV2DTO> getScenicActiveDevices(Long scenicId, Integer page, Integer pageSize) {
|
||||||
return listDevices(page, pageSize, null, null, null, 1, scenicId);
|
return listDevices(page, pageSize, null, null, null, 1, scenicId, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import com.ycwl.basic.integration.common.manager.DeviceConfigManager;
|
|||||||
import com.ycwl.basic.integration.kafka.dto.FaceProcessingMessage;
|
import com.ycwl.basic.integration.kafka.dto.FaceProcessingMessage;
|
||||||
import com.ycwl.basic.integration.kafka.scheduler.AccountFaceSchedulerManager;
|
import com.ycwl.basic.integration.kafka.scheduler.AccountFaceSchedulerManager;
|
||||||
import com.ycwl.basic.integration.kafka.scheduler.AccountSchedulerContext;
|
import com.ycwl.basic.integration.kafka.scheduler.AccountSchedulerContext;
|
||||||
|
import com.ycwl.basic.mapper.FaceSampleAiCamMapper;
|
||||||
import com.ycwl.basic.mapper.FaceSampleMapper;
|
import com.ycwl.basic.mapper.FaceSampleMapper;
|
||||||
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
|
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
|
||||||
import com.ycwl.basic.repository.DeviceRepository;
|
import com.ycwl.basic.repository.DeviceRepository;
|
||||||
@@ -42,8 +43,10 @@ import java.util.Date;
|
|||||||
public class FaceProcessingKafkaService {
|
public class FaceProcessingKafkaService {
|
||||||
|
|
||||||
private static final String ZT_FACE_TOPIC = "zt-face";
|
private static final String ZT_FACE_TOPIC = "zt-face";
|
||||||
|
private static final String ZT_AI_CAM_FACE_TOPIC = "zt-ai-cam-face";
|
||||||
|
|
||||||
private final FaceSampleMapper faceSampleMapper;
|
private final FaceSampleMapper faceSampleMapper;
|
||||||
|
private final FaceSampleAiCamMapper faceSampleAiCamMapper;
|
||||||
private final TaskFaceService taskFaceService;
|
private final TaskFaceService taskFaceService;
|
||||||
private final ScenicService scenicService;
|
private final ScenicService scenicService;
|
||||||
private final DeviceRepository deviceRepository;
|
private final DeviceRepository deviceRepository;
|
||||||
@@ -115,6 +118,67 @@ public class FaceProcessingKafkaService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消费AI相机发送的人脸处理消息 (zt-ai-cam-face)
|
||||||
|
* 逻辑与 zt-face 类似,但写入不同的表,且人脸库分组依据不同
|
||||||
|
*/
|
||||||
|
@KafkaListener(topics = ZT_AI_CAM_FACE_TOPIC, containerFactory = "manualCommitKafkaListenerContainerFactory")
|
||||||
|
public void processAiCamFaceMessage(String message, Acknowledgment ack) {
|
||||||
|
Long faceSampleId = null;
|
||||||
|
try {
|
||||||
|
FaceProcessingMessage faceMessage = JacksonUtil.parseObject(message, FaceProcessingMessage.class);
|
||||||
|
faceSampleId = faceMessage.getFaceSampleId();
|
||||||
|
|
||||||
|
log.debug("接收AI相机人脸消息: scenicId={}, deviceId={}, faceSampleId={}",
|
||||||
|
faceMessage.getScenicId(), faceMessage.getDeviceId(), faceSampleId);
|
||||||
|
|
||||||
|
// ========== 第一步: 同步写入数据库 (FaceSampleAiCam) ==========
|
||||||
|
boolean saved = saveAiCamFaceSample(faceMessage, faceSampleId);
|
||||||
|
if (!saved) {
|
||||||
|
log.error("AI相机数据库写入失败, 不提交识别任务, faceSampleId={}", faceSampleId);
|
||||||
|
ack.acknowledge();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("AI相机数据库写入成功, faceSampleId={}, status=0", faceSampleId);
|
||||||
|
|
||||||
|
// ========== 第二步: 获取账号调度器上下文 ==========
|
||||||
|
AccountSchedulerContext schedulerCtx = getSchedulerContextForScenic(faceMessage.getScenicId());
|
||||||
|
if (schedulerCtx == null) {
|
||||||
|
log.error("无法获取调度器上下文, faceSampleId={}", faceSampleId);
|
||||||
|
updateAiCamFaceSampleStatusSafely(faceSampleId, -1);
|
||||||
|
ack.acknowledge();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 第三步: 提交到账号专属调度器 ==========
|
||||||
|
boolean submitted = schedulerCtx.getScheduler().submit(() -> {
|
||||||
|
processAiCamFaceRecognitionAsync(faceMessage);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (submitted) {
|
||||||
|
log.debug("AI相机任务已提交到调度器, account={}, cloudType={}, faceSampleId={}, schedulerQueue={}",
|
||||||
|
schedulerCtx.getAccountKey(),
|
||||||
|
schedulerCtx.getCloudType(),
|
||||||
|
faceSampleId,
|
||||||
|
schedulerCtx.getScheduler().getQueueSize());
|
||||||
|
} else {
|
||||||
|
log.error("调度器队列已满, account={}, faceSampleId={}",
|
||||||
|
schedulerCtx.getAccountKey(), faceSampleId);
|
||||||
|
updateAiCamFaceSampleStatusSafely(faceSampleId, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
ack.acknowledge();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("处理AI相机人脸消息异常, faceSampleId={}", faceSampleId, e);
|
||||||
|
if (faceSampleId != null) {
|
||||||
|
updateAiCamFaceSampleStatusSafely(faceSampleId, -1);
|
||||||
|
}
|
||||||
|
ack.acknowledge();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据景区获取对应的账号调度器上下文
|
* 根据景区获取对应的账号调度器上下文
|
||||||
* 关键: 按 accessKeyId/appId 隔离,而非按云类型
|
* 关键: 按 accessKeyId/appId 隔离,而非按云类型
|
||||||
@@ -343,4 +407,105 @@ public class FaceProcessingKafkaService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安全地更新AI相机人脸样本状态
|
||||||
|
*/
|
||||||
|
private void updateAiCamFaceSampleStatusSafely(Long faceSampleId, Integer status) {
|
||||||
|
if (faceSampleId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
faceSampleAiCamMapper.updateStatus(faceSampleId, status);
|
||||||
|
log.debug("AI相机样本状态更新成功: faceSampleId={}, status={}", faceSampleId, status);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("AI相机样本状态更新失败(非致命): faceSampleId={}, status={}", faceSampleId, status, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存AI相机人脸样本数据到数据库
|
||||||
|
*/
|
||||||
|
private boolean saveAiCamFaceSample(FaceProcessingMessage faceMessage, Long externalFaceId) {
|
||||||
|
try {
|
||||||
|
FaceSampleEntity faceSample = new FaceSampleEntity();
|
||||||
|
faceSample.setId(externalFaceId); // 使用外部传入的ID
|
||||||
|
faceSample.setScenicId(faceMessage.getScenicId());
|
||||||
|
faceSample.setDeviceId(faceMessage.getDeviceId());
|
||||||
|
faceSample.setStatus(0); // 初始状态
|
||||||
|
faceSample.setFaceUrl(faceMessage.getFaceUrl());
|
||||||
|
|
||||||
|
if (faceMessage.getShotTime() != null) {
|
||||||
|
faceSample.setCreateAt(faceMessage.getShotTime());
|
||||||
|
} else {
|
||||||
|
faceSample.setCreateAt(new Date());
|
||||||
|
}
|
||||||
|
|
||||||
|
faceSampleAiCamMapper.add(faceSample);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("保存AI相机人脸样本数据失败, 外部faceId: {}, scenicId: {}, deviceId: {}",
|
||||||
|
externalFaceId, faceMessage.getScenicId(), faceMessage.getDeviceId(), e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步执行AI相机人脸识别处理逻辑
|
||||||
|
* 区别: 使用 deviceId 作为人脸库分组
|
||||||
|
*/
|
||||||
|
private void processAiCamFaceRecognitionAsync(FaceProcessingMessage message) {
|
||||||
|
Long faceSampleId = message.getFaceSampleId();
|
||||||
|
Long scenicId = message.getScenicId();
|
||||||
|
Long deviceId = message.getDeviceId();
|
||||||
|
|
||||||
|
try {
|
||||||
|
updateAiCamFaceSampleStatusSafely(faceSampleId, 1);
|
||||||
|
log.debug("开始AI相机人脸识别, faceSampleId={}, status=1", faceSampleId);
|
||||||
|
|
||||||
|
IFaceBodyAdapter adapter = scenicService.getScenicFaceBodyAdapter(scenicId);
|
||||||
|
if (adapter == null) {
|
||||||
|
log.error("adapter 不存在, scenicId={}, faceSampleId={}", scenicId, faceSampleId);
|
||||||
|
updateAiCamFaceSampleStatusSafely(faceSampleId, -1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 "AiCam" + deviceId 作为人脸库分组
|
||||||
|
String dbName = "AiCam" + deviceId;
|
||||||
|
taskFaceService.assureFaceDb(adapter, dbName);
|
||||||
|
|
||||||
|
String faceUniqueId = faceSampleId.toString();
|
||||||
|
// groupName 使用 deviceId
|
||||||
|
AddFaceResp addFaceResp = adapter.addFace(
|
||||||
|
dbName,
|
||||||
|
faceSampleId.toString(),
|
||||||
|
message.getFaceUrl(),
|
||||||
|
faceUniqueId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (addFaceResp != null) {
|
||||||
|
faceSampleAiCamMapper.updateScore(faceSampleId, addFaceResp.getScore());
|
||||||
|
updateAiCamFaceSampleStatusSafely(faceSampleId, 2);
|
||||||
|
|
||||||
|
log.info("AI相机人脸识别成功, faceSampleId={}, score={}, status=2",
|
||||||
|
faceSampleId, addFaceResp.getScore());
|
||||||
|
|
||||||
|
// 预订任务逻辑与原逻辑保持一致 (如果需要)
|
||||||
|
DeviceConfigManager deviceConfig = deviceRepository.getDeviceConfigManager(deviceId);
|
||||||
|
if (deviceConfig != null &&
|
||||||
|
Integer.valueOf(1).equals(deviceConfig.getInteger("enable_pre_book"))) {
|
||||||
|
DynamicTaskGenerator.addTask(faceSampleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
log.warn("addFace 返回 null, faceSampleId={}", faceSampleId);
|
||||||
|
updateAiCamFaceSampleStatusSafely(faceSampleId, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("AI相机人脸识别异常, faceSampleId={}", faceSampleId, e);
|
||||||
|
updateAiCamFaceSampleStatusSafely(faceSampleId, -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -39,7 +39,8 @@ public interface ScenicV2Client {
|
|||||||
CommonResponse<PageResponse<ScenicV2DTO>> listScenics(@RequestParam(defaultValue = "1") Integer page,
|
CommonResponse<PageResponse<ScenicV2DTO>> listScenics(@RequestParam(defaultValue = "1") Integer page,
|
||||||
@RequestParam(defaultValue = "10") Integer pageSize,
|
@RequestParam(defaultValue = "10") Integer pageSize,
|
||||||
@RequestParam(required = false) Integer status,
|
@RequestParam(required = false) Integer status,
|
||||||
@RequestParam(required = false) String name);
|
@RequestParam(required = false) String name,
|
||||||
|
@RequestParam(required = false) String scenicIds);
|
||||||
|
|
||||||
// ==================== Scenic Config V2 Operations ====================
|
// ==================== Scenic Config V2 Operations ====================
|
||||||
|
|
||||||
|
|||||||
@@ -63,9 +63,10 @@ public class ScenicIntegrationService {
|
|||||||
return handleResponse(response, "筛选景区失败");
|
return handleResponse(response, "筛选景区失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
public PageResponse<ScenicV2DTO> listScenics(Integer page, Integer pageSize, Integer status, String name) {
|
public PageResponse<ScenicV2DTO> listScenics(Integer page, Integer pageSize, Integer status, String name, String scenicIds) {
|
||||||
log.debug("分页查询景区列表, page: {}, pageSize: {}, status: {}, name: {}", page, pageSize, status, name);
|
log.debug("分页查询景区列表, page: {}, pageSize: {}, status: {}, name: {}, scenicIds: {}",
|
||||||
CommonResponse<PageResponse<ScenicV2DTO>> response = scenicV2Client.listScenics(page, pageSize, status, name);
|
page, pageSize, status, name, scenicIds);
|
||||||
|
CommonResponse<PageResponse<ScenicV2DTO>> response = scenicV2Client.listScenics(page, pageSize, status, name, scenicIds);
|
||||||
return handleResponse(response, "分页查询景区列表失败");
|
return handleResponse(response, "分页查询景区列表失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.commons.lang3.Strings;
|
import org.apache.commons.lang3.Strings;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.method.HandlerMethod;
|
import org.springframework.web.method.HandlerMethod;
|
||||||
@@ -41,10 +42,13 @@ import static com.ycwl.basic.constant.JwtRoleConstant.MERCHANT;
|
|||||||
@Component
|
@Component
|
||||||
public class AuthInterceptor implements HandlerInterceptor {
|
public class AuthInterceptor implements HandlerInterceptor {
|
||||||
|
|
||||||
|
@Lazy
|
||||||
@Autowired
|
@Autowired
|
||||||
RedisTemplate redisTemplate;
|
RedisTemplate redisTemplate;
|
||||||
|
@Lazy
|
||||||
@Autowired
|
@Autowired
|
||||||
private ScenicAccountMapper scenicAccountMapper;
|
private ScenicAccountMapper scenicAccountMapper;
|
||||||
|
@Lazy
|
||||||
@Autowired
|
@Autowired
|
||||||
private AdminUserMapper adminUserMapper;
|
private AdminUserMapper adminUserMapper;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.ycwl.basic.mapper;
|
||||||
|
|
||||||
|
import com.ycwl.basic.model.pc.faceDetectLog.entity.FaceDetectLogAiCamEntity;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI相机人脸识别日志Mapper
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface FaceDetectLogAiCamMapper {
|
||||||
|
int add(FaceDetectLogAiCamEntity entity);
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.ycwl.basic.mapper;
|
||||||
|
|
||||||
|
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
|
||||||
|
import com.ycwl.basic.model.pc.faceSample.req.FaceSampleReqQuery;
|
||||||
|
import com.ycwl.basic.model.pc.faceSample.resp.FaceSampleRespVO;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AiCam人脸样本Mapper
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface FaceSampleAiCamMapper {
|
||||||
|
List<FaceSampleRespVO> list(FaceSampleReqQuery faceSampleReqQuery);
|
||||||
|
FaceSampleRespVO getById(Long id);
|
||||||
|
int add(FaceSampleEntity faceSample);
|
||||||
|
int deleteById(Long id);
|
||||||
|
int deleteByIds(@Param("list") List<Long> ids);
|
||||||
|
int update(FaceSampleEntity faceSample);
|
||||||
|
|
||||||
|
List<FaceSampleEntity> listByIds(List<Long> list);
|
||||||
|
|
||||||
|
FaceSampleEntity getEntity(Long faceSampleId);
|
||||||
|
List<FaceSampleEntity> listEntityBeforeDate(Long scenicId, Date endDate);
|
||||||
|
|
||||||
|
void updateScore(Long id, Float score);
|
||||||
|
|
||||||
|
void updateStatus(Long id, Integer status);
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.ycwl.basic.model.pc.faceDetectLog.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;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@TableName("face_detect_log_ai_cam")
|
||||||
|
public class FaceDetectLogAiCamEntity {
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private Long scenicId;
|
||||||
|
|
||||||
|
private Long deviceId;
|
||||||
|
|
||||||
|
private Long faceId;
|
||||||
|
|
||||||
|
private String dbName;
|
||||||
|
|
||||||
|
private String faceUrl;
|
||||||
|
|
||||||
|
private Float score;
|
||||||
|
|
||||||
|
private String matchRawResult;
|
||||||
|
|
||||||
|
private Date createTime;
|
||||||
|
}
|
||||||
@@ -60,6 +60,16 @@ public class ScenicConfigResp {
|
|||||||
*/
|
*/
|
||||||
private Boolean printEnableManual;
|
private Boolean printEnableManual;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 我的页面显示已付款订单开关
|
||||||
|
*/
|
||||||
|
private Boolean showMyPagePaid;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 我的页面显示未付款订单开关
|
||||||
|
*/
|
||||||
|
private Boolean showMyPageUnpaid;
|
||||||
|
|
||||||
// ========== 提示文案 ==========
|
// ========== 提示文案 ==========
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package com.ycwl.basic.model.printer;
|
||||||
|
|
||||||
|
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author:longbinbin
|
||||||
|
* @Date:2024/12/3
|
||||||
|
* 打印机大屏人脸识别响应(包含匹配的source列表)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class FaceRecognizeWithSourcesResp {
|
||||||
|
/**
|
||||||
|
* 人脸照片URL
|
||||||
|
*/
|
||||||
|
private String url;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸ID
|
||||||
|
*/
|
||||||
|
private Long faceId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 景区ID
|
||||||
|
*/
|
||||||
|
private Long scenicId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 匹配到的图像素材列表(type=2)
|
||||||
|
*/
|
||||||
|
private List<SourceEntity> sources;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 二维码URL
|
||||||
|
*/
|
||||||
|
private String qrcodeUrl;
|
||||||
|
}
|
||||||
@@ -47,6 +47,7 @@ import com.ycwl.basic.repository.FaceRepository;
|
|||||||
import com.ycwl.basic.service.pc.ScenicService;
|
import com.ycwl.basic.service.pc.ScenicService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@@ -70,13 +71,20 @@ public class OrderServiceImpl implements IOrderService {
|
|||||||
private final OrderItemMapper orderItemMapper;
|
private final OrderItemMapper orderItemMapper;
|
||||||
private final OrderDiscountMapper orderDiscountMapper;
|
private final OrderDiscountMapper orderDiscountMapper;
|
||||||
private final OrderRefundMapper orderRefundMapper;
|
private final OrderRefundMapper orderRefundMapper;
|
||||||
|
@Lazy
|
||||||
private final OrderEventManager orderEventManager;
|
private final OrderEventManager orderEventManager;
|
||||||
|
@Lazy
|
||||||
private final ScenicService scenicService;
|
private final ScenicService scenicService;
|
||||||
private final MemberMapper memberMapper;
|
private final MemberMapper memberMapper;
|
||||||
|
@Lazy
|
||||||
private final ICouponService couponService;
|
private final ICouponService couponService;
|
||||||
|
@Lazy
|
||||||
private final IVoucherService voucherService;
|
private final IVoucherService voucherService;
|
||||||
|
@Lazy
|
||||||
private final IProductConfigService productConfigService;
|
private final IProductConfigService productConfigService;
|
||||||
|
@Lazy
|
||||||
private final IProductTypeCapabilityService productTypeCapabilityService;
|
private final IProductTypeCapabilityService productTypeCapabilityService;
|
||||||
|
@Lazy
|
||||||
private final DuplicatePurchaseCheckerFactory duplicatePurchaseCheckerFactory;
|
private final DuplicatePurchaseCheckerFactory duplicatePurchaseCheckerFactory;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
1611
src/main/java/com/ycwl/basic/pipeline/CLAUDE.md
Normal file
1611
src/main/java/com/ycwl/basic/pipeline/CLAUDE.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,38 @@
|
|||||||
|
package com.ycwl.basic.pipeline.annotation;
|
||||||
|
|
||||||
|
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
|
||||||
|
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stage 配置注解,统一声明 Stage 的元信息与可选性控制。
|
||||||
|
*/
|
||||||
|
@Target(ElementType.TYPE)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Documented
|
||||||
|
public @interface StageConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stage 唯一标识,用于外部配置或监控引用。
|
||||||
|
*/
|
||||||
|
String stageId();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 可选性模式,默认不受外部控制。
|
||||||
|
*/
|
||||||
|
StageOptionalMode optionalMode() default StageOptionalMode.UNSUPPORT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 描述信息,便于文档与日志。
|
||||||
|
*/
|
||||||
|
String description() default "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认是否启用,仅在 optionalMode=SUPPORT 时生效。
|
||||||
|
*/
|
||||||
|
boolean defaultEnabled() default true;
|
||||||
|
}
|
||||||
@@ -1,70 +1,46 @@
|
|||||||
package com.ycwl.basic.image.pipeline.core;
|
package com.ycwl.basic.pipeline.core;
|
||||||
|
|
||||||
import com.ycwl.basic.image.pipeline.annotation.StageConfig;
|
import com.ycwl.basic.pipeline.annotation.StageConfig;
|
||||||
import com.ycwl.basic.image.pipeline.enums.StageOptionalMode;
|
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pipeline Stage抽象基类
|
* 通用 Stage 抽象基类,封装可选性判断与钩子逻辑。
|
||||||
* 提供默认实现和通用逻辑
|
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public abstract class AbstractPipelineStage<C extends PhotoProcessContext> implements PipelineStage<C> {
|
public abstract class AbstractPipelineStage<C extends PipelineContext> implements PipelineStage<C> {
|
||||||
|
|
||||||
/**
|
|
||||||
* 最终的shouldExecute判断
|
|
||||||
* 整合了外部配置控制和业务逻辑判断
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
public final boolean shouldExecute(C context) {
|
public final boolean shouldExecute(C context) {
|
||||||
// 1. 检查Stage配置注解
|
|
||||||
StageConfig config = getStageConfig();
|
StageConfig config = getStageConfig();
|
||||||
if (config != null) {
|
if (config != null) {
|
||||||
String stageId = config.stageId();
|
String stageId = config.stageId();
|
||||||
StageOptionalMode mode = config.optionalMode();
|
StageOptionalMode mode = config.optionalMode();
|
||||||
|
|
||||||
// FORCE_ON:强制执行,不检查外部配置
|
|
||||||
if (mode == StageOptionalMode.FORCE_ON) {
|
if (mode == StageOptionalMode.FORCE_ON) {
|
||||||
return shouldExecuteByBusinessLogic(context);
|
return shouldExecuteByBusinessLogic(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
// SUPPORT:检查外部配置
|
|
||||||
if (mode == StageOptionalMode.SUPPORT) {
|
if (mode == StageOptionalMode.SUPPORT) {
|
||||||
boolean externalEnabled = context.isStageEnabled(stageId, config.defaultEnabled());
|
boolean externalEnabled = context.isStageEnabled(stageId, config.defaultEnabled());
|
||||||
if (!externalEnabled) {
|
if (!externalEnabled) {
|
||||||
log.debug("[{}] Stage被外部配置禁用", stageId);
|
log.debug("[{}] Stage 被外部配置禁用", stageId);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// UNSUPPORT:不检查外部配置,直接走业务逻辑
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 执行业务逻辑判断
|
|
||||||
return shouldExecuteByBusinessLogic(context);
|
return shouldExecuteByBusinessLogic(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 子类实现业务逻辑判断
|
|
||||||
* 默认总是执行
|
|
||||||
*
|
|
||||||
* 子类可以覆盖此方法实现条件性执行
|
|
||||||
* 例如: 只有竖图才旋转, 只有普通照片才加水印等
|
|
||||||
*/
|
|
||||||
protected boolean shouldExecuteByBusinessLogic(C context) {
|
protected boolean shouldExecuteByBusinessLogic(C context) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 模板方法:执行Stage前的准备工作
|
|
||||||
*/
|
|
||||||
protected void beforeExecute(C context) {
|
protected void beforeExecute(C context) {
|
||||||
log.debug("[{}] 开始执行", getName());
|
log.debug("[{}] 开始执行", getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 模板方法:执行Stage后的清理工作
|
|
||||||
*/
|
|
||||||
protected void afterExecute(C context, StageResult<C> result) {
|
protected void afterExecute(C context, StageResult<C> result) {
|
||||||
if (result.isSuccess()) {
|
if (result.isSuccess()) {
|
||||||
log.debug("[{}] 执行成功: {}", getName(), result.getMessage());
|
log.debug("[{}] 执行成功: {}", getName(), result.getMessage());
|
||||||
@@ -77,14 +53,8 @@ public abstract class AbstractPipelineStage<C extends PhotoProcessContext> imple
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 子类实现具体的处理逻辑
|
|
||||||
*/
|
|
||||||
protected abstract StageResult<C> doExecute(C context);
|
protected abstract StageResult<C> doExecute(C context);
|
||||||
|
|
||||||
/**
|
|
||||||
* 最终执行方法(带钩子)
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
public final StageResult<C> execute(C context) {
|
public final StageResult<C> execute(C context) {
|
||||||
beforeExecute(context);
|
beforeExecute(context);
|
||||||
@@ -1,17 +1,16 @@
|
|||||||
package com.ycwl.basic.image.pipeline.core;
|
package com.ycwl.basic.pipeline.core;
|
||||||
|
|
||||||
import com.ycwl.basic.image.pipeline.exception.PipelineException;
|
import com.ycwl.basic.pipeline.exception.PipelineException;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 图片处理管线
|
* 通用 Pipeline 实现,负责顺序执行 Stage 并支持动态插入。
|
||||||
* 按顺序执行一系列Stage
|
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class Pipeline<C extends PhotoProcessContext> {
|
public class Pipeline<C extends PipelineContext> {
|
||||||
|
|
||||||
private final List<PipelineStage<C>> stages;
|
private final List<PipelineStage<C>> stages;
|
||||||
private final String name;
|
private final String name;
|
||||||
@@ -21,30 +20,27 @@ public class Pipeline<C extends PhotoProcessContext> {
|
|||||||
this.stages = new ArrayList<>(stages);
|
this.stages = new ArrayList<>(stages);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 执行管线
|
|
||||||
*
|
|
||||||
* @param context 管线上下文
|
|
||||||
* @return 执行成功返回true
|
|
||||||
* @throws PipelineException 管线执行异常
|
|
||||||
*/
|
|
||||||
public boolean execute(C context) {
|
public boolean execute(C context) {
|
||||||
log.info("[{}] 开始执行管线, Stage数量: {}", name, stages.size());
|
log.info("[{}] Pipeline 开始执行, Stage 数量: {}", name, stages.size());
|
||||||
long startTime = System.currentTimeMillis();
|
long startTime = System.currentTimeMillis();
|
||||||
int maxStages = 100; // 防止无限循环
|
int maxStages = 100;
|
||||||
int executedCount = 0;
|
int executedCount = 0;
|
||||||
|
|
||||||
|
if (context != null) {
|
||||||
|
context.beforePipeline();
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (int i = 0; i < stages.size(); i++) {
|
for (int i = 0; i < stages.size(); i++) {
|
||||||
if (executedCount >= maxStages) {
|
if (executedCount >= maxStages) {
|
||||||
log.error("[{}] Stage执行数量超过最大限制({}),可能存在循环依赖", name, maxStages);
|
log.error("[{}] Stage 执行数量超过最大限制({}), 可能存在循环依赖", name, maxStages);
|
||||||
throw new PipelineException("Stage执行数量超过最大限制,可能存在循环依赖");
|
throw new PipelineException("Stage执行数量超过最大限制,可能存在循环依赖");
|
||||||
}
|
}
|
||||||
|
|
||||||
PipelineStage<C> stage = stages.get(i);
|
PipelineStage<C> stage = stages.get(i);
|
||||||
String stageName = stage.getName();
|
String stageName = stage.getName();
|
||||||
|
|
||||||
log.debug("[{}] [{}/{}] 准备执行Stage: {}", name, i + 1, stages.size(), stageName);
|
log.debug("[{}] [{}/{}] 准备执行 Stage: {}", name, i + 1, stages.size(), stageName);
|
||||||
|
|
||||||
if (!stage.shouldExecute(context)) {
|
if (!stage.shouldExecute(context)) {
|
||||||
log.debug("[{}] Stage {} 条件不满足,跳过执行", name, stageName);
|
log.debug("[{}] Stage {} 条件不满足,跳过执行", name, stageName);
|
||||||
@@ -58,31 +54,34 @@ public class Pipeline<C extends PhotoProcessContext> {
|
|||||||
|
|
||||||
logStageResult(stageName, result, stageDuration);
|
logStageResult(stageName, result, stageDuration);
|
||||||
|
|
||||||
// 动态添加后续Stage
|
|
||||||
if (result.getNextStages() != null && !result.getNextStages().isEmpty()) {
|
if (result.getNextStages() != null && !result.getNextStages().isEmpty()) {
|
||||||
List<PipelineStage<C>> nextStages = result.getNextStages();
|
List<PipelineStage<C>> nextStages = result.getNextStages();
|
||||||
log.info("[{}] Stage {} 动态添加了 {} 个后续Stage", name, stageName, nextStages.size());
|
log.info("[{}] Stage {} 动态添加了 {} 个后续 Stage", name, stageName, nextStages.size());
|
||||||
|
|
||||||
for (int j = 0; j < nextStages.size(); j++) {
|
for (int j = 0; j < nextStages.size(); j++) {
|
||||||
PipelineStage<C> nextStage = nextStages.get(j);
|
PipelineStage<C> nextStage = nextStages.get(j);
|
||||||
stages.add(i + 1 + j, nextStage);
|
stages.add(i + 1 + j, nextStage);
|
||||||
log.debug("[{}] - 插入Stage: {} 到位置 {}", name, nextStage.getName(), i + 1 + j);
|
log.debug("[{}] - 插入 Stage: {} 到位置 {}", name, nextStage.getName(), i + 1 + j);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.isFailed()) {
|
if (result.isFailed()) {
|
||||||
log.error("[{}] Stage {} 执行失败,管线终止", name, stageName);
|
log.error("[{}] Stage {} 执行失败, Pipeline 终止", name, stageName);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
long totalDuration = System.currentTimeMillis() - startTime;
|
long totalDuration = System.currentTimeMillis() - startTime;
|
||||||
log.info("[{}] 管线执行完成, 总Stage数: {}, 实际执行: {}, 耗时: {}ms",
|
log.info("[{}] Pipeline 执行完成, 总 Stage 数: {}, 实际执行: {}, 耗时: {}ms",
|
||||||
name, stages.size(), executedCount, totalDuration);
|
name, stages.size(), executedCount, totalDuration);
|
||||||
|
|
||||||
|
if (context != null) {
|
||||||
|
context.afterPipeline();
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("[{}] 管线执行异常", name, e);
|
log.error("[{}] Pipeline 执行异常", name, e);
|
||||||
throw new PipelineException("管线执行失败: " + e.getMessage(), e);
|
throw new PipelineException("管线执行失败: " + e.getMessage(), e);
|
||||||
} finally {
|
} finally {
|
||||||
safeCleanup(context);
|
safeCleanup(context);
|
||||||
@@ -124,7 +123,7 @@ public class Pipeline<C extends PhotoProcessContext> {
|
|||||||
try {
|
try {
|
||||||
context.cleanup();
|
context.cleanup();
|
||||||
} catch (Exception cleanupError) {
|
} catch (Exception cleanupError) {
|
||||||
log.warn("[{}] 管线清理失败", name, cleanupError);
|
log.warn("[{}] Pipeline 清理失败", name, cleanupError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
package com.ycwl.basic.image.pipeline.core;
|
package com.ycwl.basic.pipeline.core;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pipeline构建器
|
* 通用 Pipeline 构建器。
|
||||||
* 使用Builder模式动态组装管线
|
|
||||||
*/
|
*/
|
||||||
public class PipelineBuilder<C extends PhotoProcessContext> {
|
public class PipelineBuilder<C extends PipelineContext> {
|
||||||
|
|
||||||
private String name = "DefaultPipeline";
|
private String name = "DefaultPipeline";
|
||||||
private final List<PipelineStage<C>> stages = new ArrayList<>();
|
private final List<PipelineStage<C>> stages = new ArrayList<>();
|
||||||
@@ -20,17 +19,11 @@ public class PipelineBuilder<C extends PhotoProcessContext> {
|
|||||||
this.name = name;
|
this.name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置管线名称
|
|
||||||
*/
|
|
||||||
public PipelineBuilder<C> name(String name) {
|
public PipelineBuilder<C> name(String name) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加Stage
|
|
||||||
*/
|
|
||||||
public PipelineBuilder<C> addStage(PipelineStage<C> stage) {
|
public PipelineBuilder<C> addStage(PipelineStage<C> stage) {
|
||||||
if (stage != null) {
|
if (stage != null) {
|
||||||
this.stages.add(stage);
|
this.stages.add(stage);
|
||||||
@@ -38,9 +31,6 @@ public class PipelineBuilder<C extends PhotoProcessContext> {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量添加Stage
|
|
||||||
*/
|
|
||||||
public PipelineBuilder<C> addStages(List<PipelineStage<C>> stages) {
|
public PipelineBuilder<C> addStages(List<PipelineStage<C>> stages) {
|
||||||
if (stages != null) {
|
if (stages != null) {
|
||||||
this.stages.addAll(stages);
|
this.stages.addAll(stages);
|
||||||
@@ -48,9 +38,6 @@ public class PipelineBuilder<C extends PhotoProcessContext> {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 条件性添加Stage
|
|
||||||
*/
|
|
||||||
public PipelineBuilder<C> addStageIf(boolean condition, PipelineStage<C> stage) {
|
public PipelineBuilder<C> addStageIf(boolean condition, PipelineStage<C> stage) {
|
||||||
if (condition && stage != null) {
|
if (condition && stage != null) {
|
||||||
this.stages.add(stage);
|
this.stages.add(stage);
|
||||||
@@ -58,20 +45,14 @@ public class PipelineBuilder<C extends PhotoProcessContext> {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 按优先级排序Stage
|
|
||||||
*/
|
|
||||||
public PipelineBuilder<C> sortByPriority() {
|
public PipelineBuilder<C> sortByPriority() {
|
||||||
this.stages.sort(Comparator.comparingInt(PipelineStage::getPriority));
|
this.stages.sort(Comparator.comparingInt(PipelineStage::getPriority));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 构建Pipeline
|
|
||||||
*/
|
|
||||||
public Pipeline<C> build() {
|
public Pipeline<C> build() {
|
||||||
if (stages.isEmpty()) {
|
if (stages.isEmpty()) {
|
||||||
throw new IllegalStateException("管线至少需要一个Stage");
|
throw new IllegalStateException("Pipeline 至少需要一个 Stage");
|
||||||
}
|
}
|
||||||
return new Pipeline<>(name, stages);
|
return new Pipeline<>(name, stages);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package com.ycwl.basic.pipeline.core;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用的 Pipeline 上下文接口。
|
||||||
|
* 提供 Stage 开关控制与生命周期钩子,供不同业务场景复用。
|
||||||
|
*/
|
||||||
|
public interface PipelineContext {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在 Pipeline 开始执行前调用,可用于埋点或预检查。
|
||||||
|
*/
|
||||||
|
default void beforePipeline() {
|
||||||
|
// 默认无操作
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在 Pipeline 全部 Stage 执行完成且未抛出异常后调用。
|
||||||
|
*/
|
||||||
|
default void afterPipeline() {
|
||||||
|
// 默认无操作
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pipeline 结束时的清理钩子,无论是否异常都会调用。
|
||||||
|
*/
|
||||||
|
default void cleanup() {
|
||||||
|
// 默认无操作
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断指定 Stage 是否启用。
|
||||||
|
*
|
||||||
|
* @param stageId Stage 唯一标识
|
||||||
|
* @param defaultEnabled 配置缺失时的默认值
|
||||||
|
* @return 是否启用
|
||||||
|
*/
|
||||||
|
default boolean isStageEnabled(String stageId, boolean defaultEnabled) {
|
||||||
|
return defaultEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断指定 Stage 是否启用,默认关闭。
|
||||||
|
*
|
||||||
|
* @param stageId Stage 唯一标识
|
||||||
|
* @return 是否启用
|
||||||
|
*/
|
||||||
|
default boolean isStageEnabled(String stageId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.ycwl.basic.pipeline.core;
|
||||||
|
|
||||||
|
import com.ycwl.basic.pipeline.annotation.StageConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用 Pipeline Stage 接口,每个 Stage 负责独立处理步骤。
|
||||||
|
*/
|
||||||
|
public interface PipelineStage<C extends PipelineContext> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stage 名称,用于日志与监控。
|
||||||
|
*/
|
||||||
|
String getName();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否需要执行本 Stage。
|
||||||
|
*/
|
||||||
|
boolean shouldExecute(C context);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行处理逻辑。
|
||||||
|
*/
|
||||||
|
StageResult<C> execute(C context);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stage 执行优先级,值越小优先级越高。
|
||||||
|
*/
|
||||||
|
default int getPriority() {
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stage 配置注解,便于读取元信息。
|
||||||
|
*/
|
||||||
|
default StageConfig getStageConfig() {
|
||||||
|
return this.getClass().getAnnotation(StageConfig.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.ycwl.basic.image.pipeline.core;
|
package com.ycwl.basic.pipeline.core;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
|
||||||
@@ -8,16 +8,16 @@ import java.util.Collections;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stage执行结果
|
* Stage 执行结果对象。
|
||||||
*/
|
*/
|
||||||
@Getter
|
@Getter
|
||||||
public class StageResult<C extends PhotoProcessContext> {
|
public class StageResult<C extends PipelineContext> {
|
||||||
|
|
||||||
public enum Status {
|
public enum Status {
|
||||||
SUCCESS, // 执行成功
|
SUCCESS,
|
||||||
SKIPPED, // 跳过执行
|
SKIPPED,
|
||||||
FAILED, // 执行失败
|
FAILED,
|
||||||
DEGRADED // 降级执行
|
DEGRADED
|
||||||
}
|
}
|
||||||
|
|
||||||
private final Status status;
|
private final Status status;
|
||||||
@@ -34,46 +34,40 @@ public class StageResult<C extends PhotoProcessContext> {
|
|||||||
: Collections.emptyList();
|
: Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static <C extends PhotoProcessContext> StageResult<C> success() {
|
public static <C extends PipelineContext> StageResult<C> success() {
|
||||||
return new StageResult<>(Status.SUCCESS, null, null, null);
|
return new StageResult<>(Status.SUCCESS, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static <C extends PhotoProcessContext> StageResult<C> success(String message) {
|
public static <C extends PipelineContext> StageResult<C> success(String message) {
|
||||||
return new StageResult<>(Status.SUCCESS, message, null, null);
|
return new StageResult<>(Status.SUCCESS, message, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 成功执行并动态添加后续Stage
|
|
||||||
*/
|
|
||||||
@SafeVarargs
|
@SafeVarargs
|
||||||
public static <C extends PhotoProcessContext> StageResult<C> successWithNext(String message, PipelineStage<C>... stages) {
|
public static <C extends PipelineContext> StageResult<C> successWithNext(String message, PipelineStage<C>... stages) {
|
||||||
return new StageResult<>(Status.SUCCESS, message, null, Arrays.asList(stages));
|
return new StageResult<>(Status.SUCCESS, message, null, Arrays.asList(stages));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public static <C extends PipelineContext> StageResult<C> successWithNext(String message, List<PipelineStage<C>> stages) {
|
||||||
* 成功执行并动态添加后续Stage列表
|
|
||||||
*/
|
|
||||||
public static <C extends PhotoProcessContext> StageResult<C> successWithNext(String message, List<PipelineStage<C>> stages) {
|
|
||||||
return new StageResult<>(Status.SUCCESS, message, null, stages);
|
return new StageResult<>(Status.SUCCESS, message, null, stages);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static <C extends PhotoProcessContext> StageResult<C> skipped() {
|
public static <C extends PipelineContext> StageResult<C> skipped() {
|
||||||
return new StageResult<>(Status.SKIPPED, "条件不满足,跳过执行", null, null);
|
return new StageResult<>(Status.SKIPPED, "条件不满足,跳过执行", null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static <C extends PhotoProcessContext> StageResult<C> skipped(String reason) {
|
public static <C extends PipelineContext> StageResult<C> skipped(String reason) {
|
||||||
return new StageResult<>(Status.SKIPPED, reason, null, null);
|
return new StageResult<>(Status.SKIPPED, reason, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static <C extends PhotoProcessContext> StageResult<C> failed(String message) {
|
public static <C extends PipelineContext> StageResult<C> failed(String message) {
|
||||||
return new StageResult<>(Status.FAILED, message, null, null);
|
return new StageResult<>(Status.FAILED, message, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static <C extends PhotoProcessContext> StageResult<C> failed(String message, Throwable exception) {
|
public static <C extends PipelineContext> StageResult<C> failed(String message, Throwable exception) {
|
||||||
return new StageResult<>(Status.FAILED, message, exception, null);
|
return new StageResult<>(Status.FAILED, message, exception, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static <C extends PhotoProcessContext> StageResult<C> degraded(String message) {
|
public static <C extends PipelineContext> StageResult<C> degraded(String message) {
|
||||||
return new StageResult<>(Status.DEGRADED, message, null, null);
|
return new StageResult<>(Status.DEGRADED, message, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.ycwl.basic.pipeline.enums;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stage 可选性模式,定义 Stage 是否允许受外部配置控制。
|
||||||
|
*/
|
||||||
|
public enum StageOptionalMode {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 不支持外部配置,完全由代码控制。
|
||||||
|
*/
|
||||||
|
UNSUPPORT,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支持外部配置,可以通过开关控制启用或禁用。
|
||||||
|
*/
|
||||||
|
SUPPORT,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 强制开启,不允许被外部禁用。
|
||||||
|
*/
|
||||||
|
FORCE_ON
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.ycwl.basic.image.pipeline.exception;
|
package com.ycwl.basic.pipeline.exception;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 管线处理异常基类
|
* 通用 Pipeline 执行异常。
|
||||||
*/
|
*/
|
||||||
public class PipelineException extends RuntimeException {
|
public class PipelineException extends RuntimeException {
|
||||||
|
|
||||||
@@ -12,4 +12,8 @@ public class PipelineException extends RuntimeException {
|
|||||||
public PipelineException(String message, Throwable cause) {
|
public PipelineException(String message, Throwable cause) {
|
||||||
super(message, cause);
|
super(message, cause);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PipelineException(Throwable cause) {
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -7,8 +7,8 @@ import com.ycwl.basic.pricing.service.ICouponService;
|
|||||||
import com.ycwl.basic.pricing.service.IDiscountProvider;
|
import com.ycwl.basic.pricing.service.IDiscountProvider;
|
||||||
import com.ycwl.basic.pricing.service.IPriceBundleService;
|
import com.ycwl.basic.pricing.service.IPriceBundleService;
|
||||||
import com.ycwl.basic.pricing.service.IProductConfigService;
|
import com.ycwl.basic.pricing.service.IProductConfigService;
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
@@ -20,13 +20,20 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class CouponDiscountProvider implements IDiscountProvider {
|
public class CouponDiscountProvider implements IDiscountProvider {
|
||||||
|
|
||||||
private final ICouponService couponService;
|
private final ICouponService couponService;
|
||||||
private final IProductConfigService productConfigService;
|
private final IProductConfigService productConfigService;
|
||||||
private final IPriceBundleService bundleService;
|
private final IPriceBundleService bundleService;
|
||||||
|
|
||||||
|
public CouponDiscountProvider(@Lazy ICouponService couponService,
|
||||||
|
@Lazy IProductConfigService productConfigService,
|
||||||
|
@Lazy IPriceBundleService bundleService) {
|
||||||
|
this.couponService = couponService;
|
||||||
|
this.productConfigService = productConfigService;
|
||||||
|
this.bundleService = bundleService;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getProviderType() {
|
public String getProviderType() {
|
||||||
return "COUPON";
|
return "COUPON";
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import com.ycwl.basic.pricing.dto.*;
|
|||||||
import com.ycwl.basic.pricing.service.IDiscountDetectionService;
|
import com.ycwl.basic.pricing.service.IDiscountDetectionService;
|
||||||
import com.ycwl.basic.pricing.service.IDiscountProvider;
|
import com.ycwl.basic.pricing.service.IDiscountProvider;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@@ -19,24 +20,22 @@ import java.util.stream.Collectors;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
public class DiscountDetectionServiceImpl implements IDiscountDetectionService {
|
public class DiscountDetectionServiceImpl implements IDiscountDetectionService {
|
||||||
|
|
||||||
|
private final ObjectProvider<IDiscountProvider> discountProviderProvider;
|
||||||
private final List<IDiscountProvider> discountProviders = new ArrayList<>();
|
private final List<IDiscountProvider> discountProviders = new ArrayList<>();
|
||||||
|
private final Object providerInitLock = new Object();
|
||||||
|
private volatile boolean providersInitialized;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public DiscountDetectionServiceImpl(List<IDiscountProvider> providers) {
|
public DiscountDetectionServiceImpl(ObjectProvider<IDiscountProvider> discountProviderProvider) {
|
||||||
this.discountProviders.addAll(providers);
|
this.discountProviderProvider = discountProviderProvider;
|
||||||
// 按优先级排序(优先级高的在前)
|
|
||||||
this.discountProviders.sort(Comparator.comparing(IDiscountProvider::getPriority).reversed());
|
|
||||||
|
|
||||||
log.info("注册了 {} 个优惠提供者: {}",
|
|
||||||
providers.size(),
|
|
||||||
providers.stream().map(IDiscountProvider::getProviderType).collect(Collectors.toList()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<DiscountInfo> detectAllAvailableDiscounts(DiscountDetectionContext context) {
|
public List<DiscountInfo> detectAllAvailableDiscounts(DiscountDetectionContext context) {
|
||||||
|
initializeProvidersIfNecessary();
|
||||||
List<DiscountInfo> allDiscounts = new ArrayList<>();
|
List<DiscountInfo> allDiscounts = new ArrayList<>();
|
||||||
|
|
||||||
for (IDiscountProvider provider : discountProviders) {
|
for (IDiscountProvider provider : discountProviders) {
|
||||||
try {
|
try {
|
||||||
List<DiscountInfo> providerDiscounts = provider.detectAvailableDiscounts(context);
|
List<DiscountInfo> providerDiscounts = provider.detectAvailableDiscounts(context);
|
||||||
@@ -48,22 +47,22 @@ public class DiscountDetectionServiceImpl implements IDiscountDetectionService {
|
|||||||
log.error("优惠提供者 {} 检测失败", provider.getProviderType(), e);
|
log.error("优惠提供者 {} 检测失败", provider.getProviderType(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按优先级排序
|
// 按优先级排序
|
||||||
allDiscounts.sort(Comparator.comparing(DiscountInfo::getPriority).reversed());
|
allDiscounts.sort(Comparator.comparing(DiscountInfo::getPriority).reversed());
|
||||||
|
|
||||||
return allDiscounts;
|
return allDiscounts;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DiscountCombinationResult calculateOptimalCombination(DiscountDetectionContext context) {
|
public DiscountCombinationResult calculateOptimalCombination(DiscountDetectionContext context) {
|
||||||
DiscountCombinationResult result = new DiscountCombinationResult();
|
DiscountCombinationResult result = new DiscountCombinationResult();
|
||||||
result.setOriginalAmount(context.getCurrentAmount());
|
result.setOriginalAmount(context.getCurrentAmount());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
List<DiscountInfo> availableDiscounts = detectAllAvailableDiscounts(context);
|
List<DiscountInfo> availableDiscounts = detectAllAvailableDiscounts(context);
|
||||||
result.setAvailableDiscounts(availableDiscounts);
|
result.setAvailableDiscounts(availableDiscounts);
|
||||||
|
|
||||||
if (availableDiscounts.isEmpty()) {
|
if (availableDiscounts.isEmpty()) {
|
||||||
result.setFinalAmount(context.getCurrentAmount());
|
result.setFinalAmount(context.getCurrentAmount());
|
||||||
result.setTotalDiscountAmount(BigDecimal.ZERO);
|
result.setTotalDiscountAmount(BigDecimal.ZERO);
|
||||||
@@ -72,66 +71,66 @@ public class DiscountDetectionServiceImpl implements IDiscountDetectionService {
|
|||||||
result.setSuccess(true);
|
result.setSuccess(true);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<DiscountResult> appliedDiscounts = new ArrayList<>();
|
List<DiscountResult> appliedDiscounts = new ArrayList<>();
|
||||||
List<DiscountDetail> discountDetails = new ArrayList<>();
|
List<DiscountDetail> discountDetails = new ArrayList<>();
|
||||||
BigDecimal currentAmount = context.getCurrentAmount();
|
BigDecimal currentAmount = context.getCurrentAmount();
|
||||||
|
|
||||||
// 按优先级应用优惠
|
// 按优先级应用优惠
|
||||||
for (DiscountInfo discountInfo : availableDiscounts) {
|
for (DiscountInfo discountInfo : availableDiscounts) {
|
||||||
IDiscountProvider provider = findProvider(discountInfo.getProviderType());
|
IDiscountProvider provider = findProvider(discountInfo.getProviderType());
|
||||||
if (provider == null || !provider.canApply(discountInfo, context)) {
|
if (provider == null || !provider.canApply(discountInfo, context)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新上下文中的当前金额
|
// 更新上下文中的当前金额
|
||||||
context.setCurrentAmount(currentAmount);
|
context.setCurrentAmount(currentAmount);
|
||||||
|
|
||||||
DiscountResult discountResult = provider.applyDiscount(discountInfo, context);
|
DiscountResult discountResult = provider.applyDiscount(discountInfo, context);
|
||||||
if (Boolean.TRUE.equals(discountResult.getSuccess())) {
|
if (Boolean.TRUE.equals(discountResult.getSuccess())) {
|
||||||
appliedDiscounts.add(discountResult);
|
appliedDiscounts.add(discountResult);
|
||||||
|
|
||||||
// 创建显示用的优惠详情
|
// 创建显示用的优惠详情
|
||||||
DiscountDetail detail = createDiscountDetail(discountResult);
|
DiscountDetail detail = createDiscountDetail(discountResult);
|
||||||
if (detail != null) {
|
if (detail != null) {
|
||||||
discountDetails.add(detail);
|
discountDetails.add(detail);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新当前金额
|
// 更新当前金额
|
||||||
currentAmount = discountResult.getFinalAmount();
|
currentAmount = discountResult.getFinalAmount();
|
||||||
|
|
||||||
log.info("成功应用优惠: {} - {}, 优惠金额: {}",
|
log.info("成功应用优惠: {} - {}, 优惠金额: {}",
|
||||||
discountInfo.getProviderType(),
|
discountInfo.getProviderType(),
|
||||||
discountInfo.getDiscountName(),
|
discountInfo.getDiscountName(),
|
||||||
discountResult.getActualDiscountAmount());
|
discountResult.getActualDiscountAmount());
|
||||||
|
|
||||||
// 如果是不可叠加的优惠(如全场免费),则停止应用其他优惠
|
// 如果是不可叠加的优惠(如全场免费),则停止应用其他优惠
|
||||||
if (!Boolean.TRUE.equals(discountInfo.getStackable())) {
|
if (!Boolean.TRUE.equals(discountInfo.getStackable())) {
|
||||||
log.info("遇到不可叠加优惠,停止应用其他优惠: {}", discountInfo.getDiscountName());
|
log.info("遇到不可叠加优惠,停止应用其他优惠: {}", discountInfo.getDiscountName());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.warn("优惠应用失败: {} - {}, 原因: {}",
|
log.warn("优惠应用失败: {} - {}, 原因: {}",
|
||||||
discountInfo.getProviderType(),
|
discountInfo.getProviderType(),
|
||||||
discountInfo.getDiscountName(),
|
discountInfo.getDiscountName(),
|
||||||
discountResult.getFailureReason());
|
discountResult.getFailureReason());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算总优惠金额
|
// 计算总优惠金额
|
||||||
BigDecimal totalDiscountAmount = appliedDiscounts.stream()
|
BigDecimal totalDiscountAmount = appliedDiscounts.stream()
|
||||||
.map(DiscountResult::getActualDiscountAmount)
|
.map(DiscountResult::getActualDiscountAmount)
|
||||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
|
||||||
// 按显示顺序排序折扣详情
|
// 按显示顺序排序折扣详情
|
||||||
discountDetails.sort(Comparator.comparing(DiscountDetail::getSortOrder));
|
discountDetails.sort(Comparator.comparing(DiscountDetail::getSortOrder));
|
||||||
|
|
||||||
result.setFinalAmount(currentAmount);
|
result.setFinalAmount(currentAmount);
|
||||||
result.setTotalDiscountAmount(totalDiscountAmount);
|
result.setTotalDiscountAmount(totalDiscountAmount);
|
||||||
result.setAppliedDiscounts(appliedDiscounts);
|
result.setAppliedDiscounts(appliedDiscounts);
|
||||||
result.setDiscountDetails(discountDetails);
|
result.setDiscountDetails(discountDetails);
|
||||||
result.setSuccess(true);
|
result.setSuccess(true);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("计算最优优惠组合失败", e);
|
log.error("计算最优优惠组合失败", e);
|
||||||
result.setSuccess(false);
|
result.setSuccess(false);
|
||||||
@@ -139,48 +138,55 @@ public class DiscountDetectionServiceImpl implements IDiscountDetectionService {
|
|||||||
result.setFinalAmount(context.getCurrentAmount());
|
result.setFinalAmount(context.getCurrentAmount());
|
||||||
result.setTotalDiscountAmount(BigDecimal.ZERO);
|
result.setTotalDiscountAmount(BigDecimal.ZERO);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DiscountCombinationResult previewOptimalCombination(DiscountDetectionContext context) {
|
public DiscountCombinationResult previewOptimalCombination(DiscountDetectionContext context) {
|
||||||
// 预览模式与正常计算相同,但不会实际标记优惠为已使用
|
// 预览模式与正常计算相同,但不会实际标记优惠为已使用
|
||||||
return calculateOptimalCombination(context);
|
return calculateOptimalCombination(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void registerProvider(IDiscountProvider provider) {
|
public void registerProvider(IDiscountProvider provider) {
|
||||||
if (provider != null && !discountProviders.contains(provider)) {
|
if (provider == null) {
|
||||||
discountProviders.add(provider);
|
return;
|
||||||
// 重新排序
|
}
|
||||||
discountProviders.sort(Comparator.comparing(IDiscountProvider::getPriority).reversed());
|
synchronized (providerInitLock) {
|
||||||
log.info("注册新的优惠提供者: {}", provider.getProviderType());
|
initializeProvidersIfNecessary();
|
||||||
|
if (!discountProviders.contains(provider)) {
|
||||||
|
discountProviders.add(provider);
|
||||||
|
discountProviders.sort(Comparator.comparing(IDiscountProvider::getPriority).reversed());
|
||||||
|
log.info("注册新的优惠提供者: {}", provider.getProviderType());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<IDiscountProvider> getAllProviders() {
|
public List<IDiscountProvider> getAllProviders() {
|
||||||
|
initializeProvidersIfNecessary();
|
||||||
return new ArrayList<>(discountProviders);
|
return new ArrayList<>(discountProviders);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查找指定类型的优惠提供者
|
* 查找指定类型的优惠提供者
|
||||||
*/
|
*/
|
||||||
private IDiscountProvider findProvider(String providerType) {
|
private IDiscountProvider findProvider(String providerType) {
|
||||||
|
initializeProvidersIfNecessary();
|
||||||
return discountProviders.stream()
|
return discountProviders.stream()
|
||||||
.filter(provider -> providerType.equals(provider.getProviderType()))
|
.filter(provider -> providerType.equals(provider.getProviderType()))
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建显示用的优惠详情
|
* 创建显示用的优惠详情
|
||||||
*/
|
*/
|
||||||
private DiscountDetail createDiscountDetail(DiscountResult discountResult) {
|
private DiscountDetail createDiscountDetail(DiscountResult discountResult) {
|
||||||
DiscountInfo discountInfo = discountResult.getDiscountInfo();
|
DiscountInfo discountInfo = discountResult.getDiscountInfo();
|
||||||
String providerType = discountInfo.getProviderType();
|
String providerType = discountInfo.getProviderType();
|
||||||
|
|
||||||
return switch (providerType) {
|
return switch (providerType) {
|
||||||
case "VOUCHER" -> DiscountDetail.createVoucherDiscount(
|
case "VOUCHER" -> DiscountDetail.createVoucherDiscount(
|
||||||
discountInfo.getVoucherCode(),
|
discountInfo.getVoucherCode(),
|
||||||
@@ -203,4 +209,22 @@ public class DiscountDetectionServiceImpl implements IDiscountDetectionService {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private void initializeProvidersIfNecessary() {
|
||||||
|
if (providersInitialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
synchronized (providerInitLock) {
|
||||||
|
if (providersInitialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
discountProviders.clear();
|
||||||
|
discountProviderProvider.stream().forEach(discountProviders::add);
|
||||||
|
discountProviders.sort(Comparator.comparing(IDiscountProvider::getPriority).reversed());
|
||||||
|
log.info("注册了 {} 个优惠提供者: {}",
|
||||||
|
discountProviders.size(),
|
||||||
|
discountProviders.stream().map(IDiscountProvider::getProviderType).collect(Collectors.toList()));
|
||||||
|
providersInitialized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -97,13 +97,11 @@ public class PriceBundleServiceImpl implements IPriceBundleService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
// @Cacheable(value = "active-bundles")
|
|
||||||
public List<PriceBundleConfig> getActiveBundles() {
|
public List<PriceBundleConfig> getActiveBundles() {
|
||||||
return bundleConfigMapper.selectActiveBundles();
|
return bundleConfigMapper.selectActiveBundles();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
// @Cacheable(value = "all-bundles")
|
|
||||||
public List<PriceBundleConfig> getAllBundles() {
|
public List<PriceBundleConfig> getAllBundles() {
|
||||||
return bundleConfigMapper.selectActiveBundles();
|
return bundleConfigMapper.selectActiveBundles();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,13 +30,11 @@ public class ProductConfigServiceImpl implements IProductConfigService {
|
|||||||
private final PriceTierConfigMapper tierConfigMapper;
|
private final PriceTierConfigMapper tierConfigMapper;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
// @Cacheable(value = "product-config", key = "#productType")
|
|
||||||
public List<PriceProductConfig> getProductConfig(String productType) {
|
public List<PriceProductConfig> getProductConfig(String productType) {
|
||||||
return productConfigMapper.selectByProductType(productType);
|
return productConfigMapper.selectByProductType(productType);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
// @Cacheable(value = "product-config", key = "#productType + '_' + #productId")
|
|
||||||
public PriceProductConfig getProductConfig(String productType, String productId) {
|
public PriceProductConfig getProductConfig(String productType, String productId) {
|
||||||
PriceProductConfig config = productConfigMapper.selectByProductTypeAndId(productType, productId);
|
PriceProductConfig config = productConfigMapper.selectByProductTypeAndId(productType, productId);
|
||||||
if (config == null) {
|
if (config == null) {
|
||||||
@@ -46,7 +44,6 @@ public class ProductConfigServiceImpl implements IProductConfigService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
// @Cacheable(value = "tier-config", key = "#productType + '_' + #productId + '_' + #quantity")
|
|
||||||
public PriceTierConfig getTierConfig(String productType, String productId, Integer quantity) {
|
public PriceTierConfig getTierConfig(String productType, String productId, Integer quantity) {
|
||||||
if (quantity == null || quantity <= 0) {
|
if (quantity == null || quantity <= 0) {
|
||||||
return null;
|
return null;
|
||||||
@@ -73,7 +70,6 @@ public class ProductConfigServiceImpl implements IProductConfigService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
// @Cacheable(value = "product-config", key = "#productType + '_' + #productId + '_' + #scenicId")
|
|
||||||
public PriceProductConfig getProductConfig(String productType, String productId, Long scenicId) {
|
public PriceProductConfig getProductConfig(String productType, String productId, Long scenicId) {
|
||||||
if (scenicId == null) {
|
if (scenicId == null) {
|
||||||
// 如果没有景区ID,使用原有逻辑
|
// 如果没有景区ID,使用原有逻辑
|
||||||
@@ -124,7 +120,6 @@ public class ProductConfigServiceImpl implements IProductConfigService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
// @Cacheable(value = "tier-config", key = "#productType + '_' + #productId + '_' + #quantity + '_' + #scenicId")
|
|
||||||
public PriceTierConfig getTierConfig(String productType, String productId, Integer quantity, Long scenicId) {
|
public PriceTierConfig getTierConfig(String productType, String productId, Integer quantity, Long scenicId) {
|
||||||
if (quantity == null || quantity <= 0) {
|
if (quantity == null || quantity <= 0) {
|
||||||
return null;
|
return null;
|
||||||
@@ -174,31 +169,26 @@ public class ProductConfigServiceImpl implements IProductConfigService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
// @Cacheable(value = "active-product-configs")
|
|
||||||
public List<PriceProductConfig> getActiveProductConfigs() {
|
public List<PriceProductConfig> getActiveProductConfigs() {
|
||||||
return productConfigMapper.selectActiveConfigs();
|
return productConfigMapper.selectActiveConfigs();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
// @Cacheable(value = "all-product-configs")
|
|
||||||
public List<PriceProductConfig> getAllProductConfigs() {
|
public List<PriceProductConfig> getAllProductConfigs() {
|
||||||
return productConfigMapper.selectActiveConfigs();
|
return productConfigMapper.selectActiveConfigs();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
// @Cacheable(value = "tier-configs", key = "#productType")
|
|
||||||
public List<PriceTierConfig> getTierConfigs(String productType) {
|
public List<PriceTierConfig> getTierConfigs(String productType) {
|
||||||
return tierConfigMapper.selectByProductType(productType);
|
return tierConfigMapper.selectByProductType(productType);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
// @Cacheable(value = "tier-configs", key = "#productType + '_' + #productId")
|
|
||||||
public List<PriceTierConfig> getTierConfigs(String productType, String productId) {
|
public List<PriceTierConfig> getTierConfigs(String productType, String productId) {
|
||||||
return tierConfigMapper.selectByProductTypeAndId(productType, productId);
|
return tierConfigMapper.selectByProductTypeAndId(productType, productId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
// @Cacheable(value = "all-tier-configs")
|
|
||||||
public List<PriceTierConfig> getAllTierConfigs() {
|
public List<PriceTierConfig> getAllTierConfigs() {
|
||||||
return tierConfigMapper.selectAllActiveConfigs();
|
return tierConfigMapper.selectAllActiveConfigs();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import com.ycwl.basic.pricing.entity.PriceVoucherUsageRecord;
|
|||||||
import com.ycwl.basic.pricing.service.IVoucherService;
|
import com.ycwl.basic.pricing.service.IVoucherService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
@@ -60,6 +61,7 @@ import java.util.stream.Collectors;
|
|||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
|
@Lazy
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class VoucherServiceImpl implements IVoucherService {
|
public class VoucherServiceImpl implements IVoucherService {
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import com.ycwl.basic.storage.StorageFactory;
|
|||||||
import com.ycwl.basic.utils.WxMpUtil;
|
import com.ycwl.basic.utils.WxMpUtil;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
@@ -51,9 +52,13 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
|
|||||||
private final PuzzleTemplateMapper templateMapper;
|
private final PuzzleTemplateMapper templateMapper;
|
||||||
private final PuzzleElementMapper elementMapper;
|
private final PuzzleElementMapper elementMapper;
|
||||||
private final PuzzleGenerationRecordMapper recordMapper;
|
private final PuzzleGenerationRecordMapper recordMapper;
|
||||||
|
@Lazy
|
||||||
private final PuzzleImageRenderer imageRenderer;
|
private final PuzzleImageRenderer imageRenderer;
|
||||||
|
@Lazy
|
||||||
private final PuzzleElementFillEngine fillEngine;
|
private final PuzzleElementFillEngine fillEngine;
|
||||||
|
@Lazy
|
||||||
private final ScenicRepository scenicRepository;
|
private final ScenicRepository scenicRepository;
|
||||||
|
@Lazy
|
||||||
private final PuzzleDuplicationDetector duplicationDetector;
|
private final PuzzleDuplicationDetector duplicationDetector;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -208,10 +208,10 @@ public class ScenicRepository {
|
|||||||
status = Integer.valueOf(scenicReqQuery.getStatus());
|
status = Integer.valueOf(scenicReqQuery.getStatus());
|
||||||
}
|
}
|
||||||
String name = scenicReqQuery.getName();
|
String name = scenicReqQuery.getName();
|
||||||
|
|
||||||
// 调用 zt-scenic 服务的 list 方法
|
// 调用 zt-scenic 服务的 list 方法
|
||||||
PageResponse<ScenicV2DTO> response = scenicIntegrationService.listScenics(page, pageSize, status, name);
|
PageResponse<ScenicV2DTO> response = scenicIntegrationService.listScenics(page, pageSize, status, name, null);
|
||||||
|
|
||||||
// 将 ScenicV2DTO 列表转换为 ScenicEntity 列表
|
// 将 ScenicV2DTO 列表转换为 ScenicEntity 列表
|
||||||
if (response != null && response.getList() != null) {
|
if (response != null && response.getList() != null) {
|
||||||
return response.getList();
|
return response.getList();
|
||||||
|
|||||||
@@ -309,7 +309,7 @@ public class AppScenicServiceImpl implements AppScenicService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ApiResponse<List<DeviceRespVO>> getDevices(Long scenicId) {
|
public ApiResponse<List<DeviceRespVO>> getDevices(Long scenicId) {
|
||||||
PageResponse<DeviceV2DTO> deviceV2ListResponse = deviceIntegrationService.listDevices(1, 1000, null, null, null, 1, scenicId);
|
PageResponse<DeviceV2DTO> deviceV2ListResponse = deviceIntegrationService.listDevices(1, 1000, null, null, null, 1, scenicId, null);
|
||||||
List<DeviceRespVO> deviceRespVOList = deviceV2ListResponse.getList().stream().map(device -> {
|
List<DeviceRespVO> deviceRespVOList = deviceV2ListResponse.getList().stream().map(device -> {
|
||||||
DeviceRespVO deviceRespVO = new DeviceRespVO();
|
DeviceRespVO deviceRespVO = new DeviceRespVO();
|
||||||
deviceRespVO.setId(device.getId());
|
deviceRespVO.setId(device.getId());
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import com.ycwl.basic.service.pc.ScenicService;
|
|||||||
import com.ycwl.basic.utils.SnowFlakeUtil;
|
import com.ycwl.basic.utils.SnowFlakeUtil;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@@ -50,18 +51,25 @@ import java.util.*;
|
|||||||
public class WxPayServiceImpl implements WxPayService {
|
public class WxPayServiceImpl implements WxPayService {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
|
@Lazy
|
||||||
private PaymentMapper paymentMapper;
|
private PaymentMapper paymentMapper;
|
||||||
@Autowired
|
@Autowired
|
||||||
|
@Lazy
|
||||||
private StatisticsMapper statisticsMapper;
|
private StatisticsMapper statisticsMapper;
|
||||||
@Autowired
|
@Autowired
|
||||||
|
@Lazy
|
||||||
private OrderRepository orderRepository;
|
private OrderRepository orderRepository;
|
||||||
@Autowired
|
@Autowired
|
||||||
|
@Lazy
|
||||||
private OrderBiz orderBiz;
|
private OrderBiz orderBiz;
|
||||||
@Autowired
|
@Autowired
|
||||||
|
@Lazy
|
||||||
private OrderMapper orderMapper;
|
private OrderMapper orderMapper;
|
||||||
@Autowired
|
@Autowired
|
||||||
|
@Lazy
|
||||||
private ScenicService scenicService;
|
private ScenicService scenicService;
|
||||||
@Autowired
|
@Autowired
|
||||||
|
@Lazy
|
||||||
private RedisTemplate<String, String> redisTemplate;
|
private RedisTemplate<String, String> redisTemplate;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.ycwl.basic.service.pc;
|
||||||
|
|
||||||
|
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
|
||||||
|
import com.ycwl.basic.facebody.entity.SearchFaceResp;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI相机人脸识别日志服务
|
||||||
|
*/
|
||||||
|
public interface FaceDetectLogAiCamService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索人脸库并保存日志
|
||||||
|
* @param scenicId 景区ID
|
||||||
|
* @param deviceId 设备ID
|
||||||
|
* @param faceSampleId 人脸样本ID
|
||||||
|
* @param faceUrl 人脸URL
|
||||||
|
* @param adapter 人脸适配器
|
||||||
|
* @return 搜索结果
|
||||||
|
*/
|
||||||
|
SearchFaceResp searchAndLog(Long scenicId, Long deviceId, Long faceSampleId, String faceUrl, IFaceBodyAdapter adapter);
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
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.mapper.FaceDetectLogAiCamMapper;
|
||||||
|
import com.ycwl.basic.model.pc.faceDetectLog.entity.FaceDetectLogAiCamEntity;
|
||||||
|
import com.ycwl.basic.service.pc.FaceDetectLogAiCamService;
|
||||||
|
import com.ycwl.basic.utils.JacksonUtil;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class FaceDetectLogAiCamServiceImpl implements FaceDetectLogAiCamService {
|
||||||
|
|
||||||
|
private final FaceDetectLogAiCamMapper faceDetectLogAiCamMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SearchFaceResp searchAndLog(Long scenicId, Long deviceId, Long faceSampleId, String faceUrl, IFaceBodyAdapter adapter) {
|
||||||
|
String dbName = "AiCam" + deviceId;
|
||||||
|
|
||||||
|
SearchFaceResp resp = null;
|
||||||
|
try {
|
||||||
|
// 调用适配器搜索人脸
|
||||||
|
resp = adapter.searchFace(dbName, faceUrl);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("AI相机人脸搜索异常: scenicId={}, deviceId={}, faceSampleId={}", scenicId, deviceId, faceSampleId, e);
|
||||||
|
// 发生异常时记录空结果或错误信息,视业务需求而定。这里暂不中断流程,继续记录日志
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 记录日志
|
||||||
|
FaceDetectLogAiCamEntity logEntity = new FaceDetectLogAiCamEntity();
|
||||||
|
logEntity.setScenicId(scenicId);
|
||||||
|
logEntity.setDeviceId(deviceId);
|
||||||
|
logEntity.setFaceId(faceSampleId);
|
||||||
|
logEntity.setDbName(dbName);
|
||||||
|
logEntity.setFaceUrl(faceUrl);
|
||||||
|
logEntity.setCreateTime(new Date());
|
||||||
|
|
||||||
|
if (resp != null) {
|
||||||
|
logEntity.setScore(resp.getOriginalFaceScore()); // 记录图片中检测到的人脸质量分或首位匹配分?
|
||||||
|
// SearchFaceResp 的 getOriginalFaceScore 通常是图片质量分,getFirstMatchRate 是最佳匹配分
|
||||||
|
// 需根据 SearchFaceResp 定义确认。假设 getFirstMatchRate() 是匹配分
|
||||||
|
// 实际上 searchFace 返回的是匹配列表。
|
||||||
|
|
||||||
|
// 记录原始响应
|
||||||
|
logEntity.setMatchRawResult(JacksonUtil.toJSONString(resp));
|
||||||
|
} else {
|
||||||
|
logEntity.setMatchRawResult("{\"error\": \"search failed or exception\"}");
|
||||||
|
}
|
||||||
|
|
||||||
|
faceDetectLogAiCamMapper.add(logEntity);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("保存AI相机人脸识别日志失败: faceSampleId={}", faceSampleId, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -311,10 +311,10 @@ public class OrderServiceImpl implements OrderService {
|
|||||||
GoodsDetailVO goods = new GoodsDetailVO();
|
GoodsDetailVO goods = new GoodsDetailVO();
|
||||||
goods.setGoodsId(sourceEntity.getId());
|
goods.setGoodsId(sourceEntity.getId());
|
||||||
goods.setGoodsName("录像集录像");
|
goods.setGoodsName("录像集录像");
|
||||||
goods.setUrl(sourceEntity.getUrl());
|
goods.setUrl(sourceEntity.getVideoUrl());
|
||||||
goods.setGoodsType(sourceEntity.getType());
|
goods.setGoodsType(sourceEntity.getType());
|
||||||
goods.setScenicId(sourceEntity.getScenicId());
|
goods.setScenicId(sourceEntity.getScenicId());
|
||||||
goods.setUrl(sourceEntity.getVideoUrl());
|
goods.setVideoUrl(sourceEntity.getVideoUrl());
|
||||||
goods.setTemplateCoverUrl(sourceEntity.getUrl());
|
goods.setTemplateCoverUrl(sourceEntity.getUrl());
|
||||||
goods.setCreateTime(sourceEntity.getCreateTime());
|
goods.setCreateTime(sourceEntity.getCreateTime());
|
||||||
goodsList.add(goods);
|
goodsList.add(goods);
|
||||||
@@ -394,6 +394,15 @@ public class OrderServiceImpl implements OrderService {
|
|||||||
goodsList.add(goods);
|
goodsList.add(goods);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (Integer.valueOf(5).equals(item.getGoodsType())) { // pLog
|
||||||
|
GoodsDetailVO goods = new GoodsDetailVO();
|
||||||
|
goods.setGoodsId(item.getGoodsId());
|
||||||
|
goods.setGoodsName("pLog");
|
||||||
|
goods.setGoodsType(5);
|
||||||
|
goods.setUrl(item.getCoverUrl());
|
||||||
|
goods.setTemplateCoverUrl(item.getCoverUrl());
|
||||||
|
goods.setScenicId(order.getScenicId());
|
||||||
|
goodsList.add(goods);
|
||||||
} 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());
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import com.ycwl.basic.model.pc.printer.entity.PrinterEntity;
|
|||||||
import com.ycwl.basic.model.pc.printer.req.ReprintRequest;
|
import com.ycwl.basic.model.pc.printer.req.ReprintRequest;
|
||||||
import com.ycwl.basic.model.pc.printer.resp.MemberPrintResp;
|
import com.ycwl.basic.model.pc.printer.resp.MemberPrintResp;
|
||||||
import com.ycwl.basic.model.pc.printer.resp.PrinterResp;
|
import com.ycwl.basic.model.pc.printer.resp.PrinterResp;
|
||||||
|
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
|
||||||
import com.ycwl.basic.model.printer.req.FromSourceReq;
|
import com.ycwl.basic.model.printer.req.FromSourceReq;
|
||||||
import com.ycwl.basic.model.printer.req.PrinterSyncReq;
|
import com.ycwl.basic.model.printer.req.PrinterSyncReq;
|
||||||
import com.ycwl.basic.model.printer.resp.PrintTaskResp;
|
import com.ycwl.basic.model.printer.resp.PrintTaskResp;
|
||||||
@@ -61,7 +62,7 @@ public interface PrinterService {
|
|||||||
|
|
||||||
FaceRecognizeResp useSample(Long userId, Long sampleId);
|
FaceRecognizeResp useSample(Long userId, Long sampleId);
|
||||||
|
|
||||||
void autoAddPhotosToPreferPrint(Long faceId);
|
List<SourceEntity> autoAddPhotosToPreferPrint(Long faceId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询待审核的打印任务
|
* 查询待审核的打印任务
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import com.ycwl.basic.enums.OrderStateEnum;
|
|||||||
import com.ycwl.basic.exception.BaseException;
|
import com.ycwl.basic.exception.BaseException;
|
||||||
import com.ycwl.basic.image.enhancer.entity.BceEnhancerConfig;
|
import com.ycwl.basic.image.enhancer.entity.BceEnhancerConfig;
|
||||||
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
||||||
import com.ycwl.basic.image.pipeline.core.Pipeline;
|
|
||||||
import com.ycwl.basic.image.pipeline.core.PipelineBuilder;
|
|
||||||
import com.ycwl.basic.image.pipeline.enums.ImageSource;
|
import com.ycwl.basic.image.pipeline.enums.ImageSource;
|
||||||
import com.ycwl.basic.image.pipeline.enums.ImageType;
|
import com.ycwl.basic.image.pipeline.enums.ImageType;
|
||||||
import com.ycwl.basic.image.pipeline.enums.PipelineScene;
|
import com.ycwl.basic.image.pipeline.enums.PipelineScene;
|
||||||
@@ -70,6 +68,8 @@ import com.ycwl.basic.service.printer.PrinterService;
|
|||||||
import com.ycwl.basic.service.printer.PrinterTaskPushService;
|
import com.ycwl.basic.service.printer.PrinterTaskPushService;
|
||||||
import com.ycwl.basic.storage.StorageFactory;
|
import com.ycwl.basic.storage.StorageFactory;
|
||||||
import com.ycwl.basic.storage.adapters.IStorageAdapter;
|
import com.ycwl.basic.storage.adapters.IStorageAdapter;
|
||||||
|
import com.ycwl.basic.pipeline.core.Pipeline;
|
||||||
|
import com.ycwl.basic.pipeline.core.PipelineBuilder;
|
||||||
import com.ycwl.basic.utils.ApiResponse;
|
import com.ycwl.basic.utils.ApiResponse;
|
||||||
import com.ycwl.basic.utils.ImageUtils;
|
import com.ycwl.basic.utils.ImageUtils;
|
||||||
import com.ycwl.basic.utils.JacksonUtil;
|
import com.ycwl.basic.utils.JacksonUtil;
|
||||||
@@ -119,29 +119,37 @@ public class PrinterServiceImpl implements PrinterService {
|
|||||||
@Lazy
|
@Lazy
|
||||||
private OrderBiz orderBiz;
|
private OrderBiz orderBiz;
|
||||||
@Autowired
|
@Autowired
|
||||||
|
@Lazy
|
||||||
private WxPayService wxPayService;
|
private WxPayService wxPayService;
|
||||||
@Autowired
|
@Autowired
|
||||||
private PrintTaskMapper printTaskMapper;
|
private PrintTaskMapper printTaskMapper;
|
||||||
@Autowired
|
@Autowired
|
||||||
|
@Lazy
|
||||||
private IPriceCalculationService priceCalculationService;
|
private IPriceCalculationService priceCalculationService;
|
||||||
@Autowired
|
@Autowired
|
||||||
|
@Lazy
|
||||||
private IAutoCouponService autoCouponService;
|
private IAutoCouponService autoCouponService;
|
||||||
@Autowired
|
@Autowired
|
||||||
|
@Lazy
|
||||||
private ScenicRepository scenicRepository;
|
private ScenicRepository scenicRepository;
|
||||||
@Autowired
|
@Autowired
|
||||||
|
@Lazy
|
||||||
private OrderRepository orderRepository;
|
private OrderRepository orderRepository;
|
||||||
@Autowired
|
@Autowired
|
||||||
private FaceSampleMapper faceSampleMapper;
|
private FaceSampleMapper faceSampleMapper;
|
||||||
@Autowired
|
@Autowired
|
||||||
private FaceMapper faceMapper;
|
private FaceMapper faceMapper;
|
||||||
@Autowired
|
@Autowired
|
||||||
|
@Lazy
|
||||||
private FaceRepository faceRepository;
|
private FaceRepository faceRepository;
|
||||||
@Lazy
|
@Lazy
|
||||||
@Autowired
|
@Autowired
|
||||||
private FaceService faceService;
|
private FaceService faceService;
|
||||||
@Autowired
|
@Autowired
|
||||||
|
@Lazy
|
||||||
private DeviceRepository deviceRepository;
|
private DeviceRepository deviceRepository;
|
||||||
@Autowired
|
@Autowired
|
||||||
|
@Lazy
|
||||||
private PrinterTaskPushService taskPushService;
|
private PrinterTaskPushService taskPushService;
|
||||||
|
|
||||||
// 用于优先打印的线程池,核心线程数根据实际情况调整
|
// 用于优先打印的线程池,核心线程数根据实际情况调整
|
||||||
@@ -1126,15 +1134,19 @@ public class PrinterServiceImpl implements PrinterService {
|
|||||||
* 根据景区和设备配置自动添加type=2的照片到用户打印列表
|
* 根据景区和设备配置自动添加type=2的照片到用户打印列表
|
||||||
*
|
*
|
||||||
* @param faceId 人脸ID
|
* @param faceId 人脸ID
|
||||||
|
* @return 成功添加的照片列表
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void autoAddPhotosToPreferPrint(Long faceId) {
|
public List<SourceEntity> autoAddPhotosToPreferPrint(Long faceId) {
|
||||||
|
// 使用线程安全的List收集成功添加的SourceEntity
|
||||||
|
List<SourceEntity> addedSources = Collections.synchronizedList(new ArrayList<>());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. 获取人脸信息
|
// 1. 获取人脸信息
|
||||||
FaceEntity face = faceRepository.getFace(faceId);
|
FaceEntity face = faceRepository.getFace(faceId);
|
||||||
if (face == null) {
|
if (face == null) {
|
||||||
log.warn("人脸不存在,无法自动添加打印: faceId={}", faceId);
|
log.warn("人脸不存在,无法自动添加打印: faceId={}", faceId);
|
||||||
return;
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
Long scenicId = face.getScenicId();
|
Long scenicId = face.getScenicId();
|
||||||
@@ -1144,21 +1156,21 @@ public class PrinterServiceImpl implements PrinterService {
|
|||||||
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(scenicId);
|
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(scenicId);
|
||||||
if (scenicConfig == null) {
|
if (scenicConfig == null) {
|
||||||
log.warn("景区配置不存在,跳过自动添加打印: scenicId={}", scenicId);
|
log.warn("景区配置不存在,跳过自动添加打印: scenicId={}", scenicId);
|
||||||
return;
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 检查景区是否启用打印功能
|
// 3. 检查景区是否启用打印功能
|
||||||
Boolean printEnable = scenicConfig.getBoolean("print_enable");
|
Boolean printEnable = scenicConfig.getBoolean("print_enable");
|
||||||
if (printEnable == null || !printEnable) {
|
if (printEnable == null || !printEnable) {
|
||||||
log.debug("景区未启用打印功能,跳过自动添加: scenicId={}", scenicId);
|
log.debug("景区未启用打印功能,跳过自动添加: scenicId={}", scenicId);
|
||||||
return;
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 查询该faceId关联的所有type=2的照片
|
// 4. 查询该faceId关联的所有type=2的照片
|
||||||
List<SourceEntity> imageSources = sourceMapper.listImageSourcesByFaceId(faceId);
|
List<SourceEntity> imageSources = sourceMapper.listImageSourcesByFaceId(faceId);
|
||||||
if (imageSources == null || imageSources.isEmpty()) {
|
if (imageSources == null || imageSources.isEmpty()) {
|
||||||
log.debug("该人脸没有关联的照片,跳过自动添加: faceId={}", faceId);
|
log.debug("该人脸没有关联的照片,跳过自动添加: faceId={}", faceId);
|
||||||
return;
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 按照deviceId分组处理
|
// 5. 按照deviceId分组处理
|
||||||
@@ -1222,6 +1234,7 @@ public class PrinterServiceImpl implements PrinterService {
|
|||||||
try {
|
try {
|
||||||
addUserPhoto(memberId, scenicId, source.getUrl(), faceId, source.getId());
|
addUserPhoto(memberId, scenicId, source.getUrl(), faceId, source.getId());
|
||||||
deviceAdded++;
|
deviceAdded++;
|
||||||
|
addedSources.add(source); // 添加成功的source
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("添加照片到打印列表失败: sourceId={}, url={}, error={}",
|
log.warn("添加照片到打印列表失败: sourceId={}, url={}, error={}",
|
||||||
source.getId(), source.getUrl(), e.getMessage());
|
source.getId(), source.getUrl(), e.getMessage());
|
||||||
@@ -1246,15 +1259,19 @@ public class PrinterServiceImpl implements PrinterService {
|
|||||||
log.error("等待照片添加任务完成时发生异常: faceId={}", faceId, e);
|
log.error("等待照片添加任务完成时发生异常: faceId={}", faceId, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (totalAdded.get() > 0) {
|
int added = totalAdded.get();
|
||||||
log.info("自动添加打印完成: faceId={}, 成功添加{}张照片", faceId, totalAdded.get());
|
if (added > 0) {
|
||||||
|
log.info("自动添加打印完成: faceId={}, 成功添加{}张照片", faceId, added);
|
||||||
} else {
|
} else {
|
||||||
log.debug("自动添加打印完成: faceId={}, 无符合条件的照片", faceId);
|
log.debug("自动添加打印完成: faceId={}, 无符合条件的照片", faceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return addedSources;
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// 出现异常则放弃,不影响主流程
|
// 出现异常则放弃,不影响主流程
|
||||||
log.error("自动添加打印失败,已忽略: faceId={}", faceId, e);
|
log.error("自动添加打印失败,已忽略: faceId={}", faceId, e);
|
||||||
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.ycwl.basic.stats.service.StatsService;
|
|||||||
import com.ycwl.basic.stats.util.StatsUtil;
|
import com.ycwl.basic.stats.util.StatsUtil;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.method.HandlerMethod;
|
import org.springframework.web.method.HandlerMethod;
|
||||||
import org.springframework.web.servlet.HandlerInterceptor;
|
import org.springframework.web.servlet.HandlerInterceptor;
|
||||||
@@ -18,6 +19,7 @@ import java.util.HashSet;
|
|||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class StatsInterceptor implements HandlerInterceptor {
|
public class StatsInterceptor implements HandlerInterceptor {
|
||||||
|
@Lazy
|
||||||
@Autowired
|
@Autowired
|
||||||
private StatsService statsService;
|
private StatsService statsService;
|
||||||
|
|
||||||
|
|||||||
@@ -7,15 +7,18 @@ import com.ycwl.basic.stats.mapper.StatsMapper;
|
|||||||
import com.ycwl.basic.stats.mapper.StatsRecordMapper;
|
import com.ycwl.basic.stats.mapper.StatsRecordMapper;
|
||||||
import com.ycwl.basic.stats.service.StatsService;
|
import com.ycwl.basic.stats.service.StatsService;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class StatsServiceImpl implements StatsService {
|
public class StatsServiceImpl implements StatsService {
|
||||||
|
@Lazy
|
||||||
@Autowired
|
@Autowired
|
||||||
private StatsMapper statsMapper;
|
private StatsMapper statsMapper;
|
||||||
|
|
||||||
|
@Lazy
|
||||||
@Autowired
|
@Autowired
|
||||||
private StatsRecordMapper statsRecordMapper;
|
private StatsRecordMapper statsRecordMapper;
|
||||||
|
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ public class DeviceVideoContinuityCheckTask {
|
|||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
PageResponse<DeviceV2DTO> pageResponse = deviceIntegrationService.listDevices(
|
PageResponse<DeviceV2DTO> pageResponse = deviceIntegrationService.listDevices(
|
||||||
currentPage, pageSize, null, null, null, 1, null
|
currentPage, pageSize, null, null, null, 1, null, null
|
||||||
);
|
);
|
||||||
|
|
||||||
if (pageResponse == null || pageResponse.getList() == null
|
if (pageResponse == null || pageResponse.getList() == null
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ public class VideoPieceCleaner {
|
|||||||
public void clean() {
|
public void clean() {
|
||||||
log.info("开始删除视频文件");
|
log.info("开始删除视频文件");
|
||||||
// 通过zt-device服务获取所有激活设备
|
// 通过zt-device服务获取所有激活设备
|
||||||
PageResponse<DeviceV2DTO> deviceListResponse = deviceIntegrationService.listDevices(1, 10000, null, null, null, 1, null);
|
PageResponse<DeviceV2DTO> deviceListResponse = deviceIntegrationService.listDevices(1, 10000, null, null, null, 1, null, null);
|
||||||
if (deviceListResponse == null || deviceListResponse.getList() == null) {
|
if (deviceListResponse == null || deviceListResponse.getList() == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
8
src/main/resources/mapper/FaceDetectLogAiCamMapper.xml
Normal file
8
src/main/resources/mapper/FaceDetectLogAiCamMapper.xml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?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.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>
|
||||||
|
</mapper>
|
||||||
111
src/main/resources/mapper/FaceSampleAiCamMapper.xml
Normal file
111
src/main/resources/mapper/FaceSampleAiCamMapper.xml
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
<?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.FaceSampleAiCamMapper">
|
||||||
|
<insert id="add">
|
||||||
|
insert into face_sample_ai_cam(id, scenic_id, device_id, face_url, match_sample_ids, first_match_rate, match_result,`status`, create_at)
|
||||||
|
values (#{id}, #{scenicId}, #{deviceId}, #{faceUrl}, #{matchSampleIds}, #{firstMatchRate}, #{matchResult},#{status},#{createAt})
|
||||||
|
</insert>
|
||||||
|
<update id="update">
|
||||||
|
update face_sample_ai_cam
|
||||||
|
<set>
|
||||||
|
<if test="scenicId!= null ">
|
||||||
|
scenic_id = #{scenicId},
|
||||||
|
</if>
|
||||||
|
<if test="deviceId!= null ">
|
||||||
|
device_id = #{deviceId},
|
||||||
|
</if>
|
||||||
|
<if test="faceUrl!= null and faceUrl!= ''">
|
||||||
|
face_url = #{faceUrl},
|
||||||
|
</if>
|
||||||
|
<if test="matchSampleIds!= null and matchSampleIds!= ''">
|
||||||
|
match_sample_ids = #{matchSampleIds},
|
||||||
|
</if>
|
||||||
|
<if test="firstMatchRate!= null ">
|
||||||
|
first_match_rate = #{firstMatchRate},
|
||||||
|
</if>
|
||||||
|
<if test="matchResult!= null and matchResult!= ''">
|
||||||
|
match_result = #{matchResult},
|
||||||
|
</if>
|
||||||
|
<if test="status!= null ">
|
||||||
|
`status` = #{status},
|
||||||
|
</if>
|
||||||
|
<if test="score!= null ">
|
||||||
|
`score` = #{score},
|
||||||
|
</if>
|
||||||
|
update_at = now(),
|
||||||
|
</set>
|
||||||
|
where id = #{id}
|
||||||
|
</update>
|
||||||
|
<update id="updateScore">
|
||||||
|
update face_sample_ai_cam
|
||||||
|
set score = #{score}
|
||||||
|
where id = #{id}
|
||||||
|
</update>
|
||||||
|
<update id="updateStatus">
|
||||||
|
update face_sample_ai_cam
|
||||||
|
set `status` = #{status}
|
||||||
|
where id = #{id}
|
||||||
|
</update>
|
||||||
|
<delete id="deleteById">
|
||||||
|
delete from face_sample_ai_cam where id = #{id}
|
||||||
|
</delete>
|
||||||
|
<delete id="deleteByIds">
|
||||||
|
<if test="list!= null and list.size() > 0">
|
||||||
|
delete from face_sample_ai_cam where id in (
|
||||||
|
<foreach collection="list" item="id" separator=",">
|
||||||
|
#{id}
|
||||||
|
</foreach>
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
</delete>
|
||||||
|
<select id="list" resultType="com.ycwl.basic.model.pc.faceSample.resp.FaceSampleRespVO">
|
||||||
|
select f.id, f.scenic_id, device_id, face_url, f.score, match_sample_ids, first_match_rate, match_result, f.`status`, f.create_at
|
||||||
|
from face_sample_ai_cam f
|
||||||
|
<where>
|
||||||
|
<if test="scenicId!= null and scenicId!= ''">
|
||||||
|
and f.scenic_id = #{scenicId}
|
||||||
|
</if>
|
||||||
|
<if test="deviceId!= null and deviceId!= ''">
|
||||||
|
and device_id = #{deviceId}
|
||||||
|
</if>
|
||||||
|
<if test="matchSampleIds!= null and matchSampleIds!= ''">
|
||||||
|
and match_sample_ids like concat('%', #{matchSampleIds}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="startTime!=null">
|
||||||
|
and f.create_at >= #{startTime}
|
||||||
|
</if>
|
||||||
|
<if test="endTime!=null">
|
||||||
|
and f.create_at <= #{endTime}
|
||||||
|
</if>
|
||||||
|
<if test="status!= null ">
|
||||||
|
and f.`status` = #{status}
|
||||||
|
</if>
|
||||||
|
</where>
|
||||||
|
ORDER BY f.create_at desc
|
||||||
|
</select>
|
||||||
|
<select id="getById" resultType="com.ycwl.basic.model.pc.faceSample.resp.FaceSampleRespVO">
|
||||||
|
select id, scenic_id, device_id, face_url, match_sample_ids, first_match_rate, match_result,`status`, create_at
|
||||||
|
from face_sample_ai_cam
|
||||||
|
where id = #{id}
|
||||||
|
</select>
|
||||||
|
<select id="listByIds" resultType="com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity">
|
||||||
|
select *
|
||||||
|
from face_sample_ai_cam
|
||||||
|
where id in (
|
||||||
|
<foreach collection="list" item="id" separator=",">
|
||||||
|
#{id}
|
||||||
|
</foreach>
|
||||||
|
)
|
||||||
|
order by create_at desc
|
||||||
|
</select>
|
||||||
|
<select id="getEntity" resultType="com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity">
|
||||||
|
select *
|
||||||
|
from face_sample_ai_cam
|
||||||
|
where id = #{id}
|
||||||
|
</select>
|
||||||
|
<select id="listEntityBeforeDate" resultType="com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity">
|
||||||
|
select *
|
||||||
|
from face_sample_ai_cam
|
||||||
|
where scenic_id = #{scenicId} and create_at <= #{endDate}
|
||||||
|
</select>
|
||||||
|
</mapper>
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
package com.ycwl.basic.face.pipeline.integration;
|
||||||
|
|
||||||
|
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||||
|
import com.ycwl.basic.pipeline.core.Pipeline;
|
||||||
|
import com.ycwl.basic.face.pipeline.enums.FaceMatchingScene;
|
||||||
|
import com.ycwl.basic.face.pipeline.factory.FaceMatchingPipelineFactory;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pipeline集成测试
|
||||||
|
* 测试Pipeline的完整流程和Stage协作
|
||||||
|
*/
|
||||||
|
@SpringBootTest
|
||||||
|
@ActiveProfiles("test")
|
||||||
|
class FaceMatchingPipelineIntegrationTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private FaceMatchingPipelineFactory pipelineFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试Pipeline工厂能够成功创建Pipeline
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testCreatePipelines() {
|
||||||
|
// When: 创建各种场景的Pipeline
|
||||||
|
Pipeline<FaceMatchingContext> autoMatchingNew = pipelineFactory.createAutoMatchingPipeline(true);
|
||||||
|
Pipeline<FaceMatchingContext> autoMatchingOld = pipelineFactory.createAutoMatchingPipeline(false);
|
||||||
|
Pipeline<FaceMatchingContext> customMatching = pipelineFactory.createCustomMatchingPipeline();
|
||||||
|
Pipeline<FaceMatchingContext> recognitionOnly = pipelineFactory.createRecognitionOnlyPipeline();
|
||||||
|
|
||||||
|
// Then: 验证Pipeline创建成功
|
||||||
|
assertNotNull(autoMatchingNew);
|
||||||
|
assertNotNull(autoMatchingOld);
|
||||||
|
assertNotNull(customMatching);
|
||||||
|
assertNotNull(recognitionOnly);
|
||||||
|
|
||||||
|
// 验证Stage数量符合预期
|
||||||
|
assertEquals(13, autoMatchingNew.getStageCount());
|
||||||
|
assertEquals(13, autoMatchingOld.getStageCount());
|
||||||
|
assertEquals(15, customMatching.getStageCount());
|
||||||
|
assertEquals(3, recognitionOnly.getStageCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试通过场景和isNew参数创建Pipeline
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testCreatePipelineByScene() {
|
||||||
|
// When
|
||||||
|
Pipeline<FaceMatchingContext> autoNew = pipelineFactory.createPipeline(FaceMatchingScene.AUTO_MATCHING, true);
|
||||||
|
Pipeline<FaceMatchingContext> autoOld = pipelineFactory.createPipeline(FaceMatchingScene.AUTO_MATCHING, false);
|
||||||
|
Pipeline<FaceMatchingContext> custom = pipelineFactory.createPipeline(FaceMatchingScene.CUSTOM_MATCHING, false);
|
||||||
|
Pipeline<FaceMatchingContext> recognition = pipelineFactory.createPipeline(FaceMatchingScene.RECOGNITION_ONLY, false);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertNotNull(autoNew);
|
||||||
|
assertNotNull(autoOld);
|
||||||
|
assertNotNull(custom);
|
||||||
|
assertNotNull(recognition);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试通过Context创建Pipeline
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testCreatePipelineByContext() {
|
||||||
|
// Given
|
||||||
|
FaceMatchingContext autoContext = FaceMatchingContext.forAutoMatching(1L, true);
|
||||||
|
FaceMatchingContext customContext = FaceMatchingContext.forCustomMatching(2L, Arrays.asList(101L, 102L));
|
||||||
|
FaceMatchingContext recognitionContext = FaceMatchingContext.forRecognitionOnly(3L);
|
||||||
|
|
||||||
|
// When
|
||||||
|
Pipeline<FaceMatchingContext> autoPipeline = pipelineFactory.createPipeline(autoContext);
|
||||||
|
Pipeline<FaceMatchingContext> customPipeline = pipelineFactory.createPipeline(customContext);
|
||||||
|
Pipeline<FaceMatchingContext> recognitionPipeline = pipelineFactory.createPipeline(recognitionContext);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertNotNull(autoPipeline);
|
||||||
|
assertNotNull(customPipeline);
|
||||||
|
assertNotNull(recognitionPipeline);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试Pipeline名称
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testPipelineNames() {
|
||||||
|
// When
|
||||||
|
Pipeline<FaceMatchingContext> autoNew = pipelineFactory.createAutoMatchingPipeline(true);
|
||||||
|
Pipeline<FaceMatchingContext> autoOld = pipelineFactory.createAutoMatchingPipeline(false);
|
||||||
|
Pipeline<FaceMatchingContext> custom = pipelineFactory.createCustomMatchingPipeline();
|
||||||
|
Pipeline<FaceMatchingContext> recognition = pipelineFactory.createRecognitionOnlyPipeline();
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertTrue(autoNew.getName().contains("AutoMatching"));
|
||||||
|
assertTrue(autoNew.getName().contains("New"));
|
||||||
|
assertTrue(autoOld.getName().contains("AutoMatching"));
|
||||||
|
assertTrue(autoOld.getName().contains("Old"));
|
||||||
|
assertTrue(custom.getName().contains("CustomMatching"));
|
||||||
|
assertTrue(recognition.getName().contains("RecognitionOnly"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试Context构建
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testContextCreation() {
|
||||||
|
// When: 使用不同的工厂方法创建Context
|
||||||
|
FaceMatchingContext autoContext = FaceMatchingContext.forAutoMatching(100L, true);
|
||||||
|
FaceMatchingContext customContext = FaceMatchingContext.forCustomMatching(200L, Arrays.asList(1L, 2L, 3L));
|
||||||
|
FaceMatchingContext recognitionContext = FaceMatchingContext.forRecognitionOnly(300L);
|
||||||
|
|
||||||
|
// Then: 验证Context属性
|
||||||
|
assertEquals(100L, autoContext.getFaceId());
|
||||||
|
assertTrue(autoContext.isNew());
|
||||||
|
assertEquals(FaceMatchingScene.AUTO_MATCHING, autoContext.getScene());
|
||||||
|
|
||||||
|
assertEquals(200L, customContext.getFaceId());
|
||||||
|
assertFalse(customContext.isNew());
|
||||||
|
assertEquals(FaceMatchingScene.CUSTOM_MATCHING, customContext.getScene());
|
||||||
|
assertEquals(3, customContext.getFaceSampleIds().size());
|
||||||
|
|
||||||
|
assertEquals(300L, recognitionContext.getFaceId());
|
||||||
|
assertFalse(recognitionContext.isNew());
|
||||||
|
assertEquals(FaceMatchingScene.RECOGNITION_ONLY, recognitionContext.getScene());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试Context的Stage开关配置
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testContextStageConfiguration() {
|
||||||
|
// Given
|
||||||
|
FaceMatchingContext context = FaceMatchingContext.forAutoMatching(1L, true);
|
||||||
|
|
||||||
|
// When: 配置Stage开关
|
||||||
|
context.enableStage("stage1");
|
||||||
|
context.disableStage("stage2");
|
||||||
|
context.setStageState("stage3", true);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertTrue(context.isStageEnabled("stage1"));
|
||||||
|
assertFalse(context.isStageEnabled("stage2"));
|
||||||
|
assertTrue(context.isStageEnabled("stage3"));
|
||||||
|
assertFalse(context.isStageEnabled("non_exist_stage")); // 默认false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试Context Builder
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testContextBuilder() {
|
||||||
|
// When
|
||||||
|
FaceMatchingContext context = FaceMatchingContext.builder()
|
||||||
|
.faceId(999L)
|
||||||
|
.isNew(true)
|
||||||
|
.scene(FaceMatchingScene.AUTO_MATCHING)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertEquals(999L, context.getFaceId());
|
||||||
|
assertTrue(context.isNew());
|
||||||
|
assertEquals(FaceMatchingScene.AUTO_MATCHING, context.getScene());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试Builder参数校验
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testContextBuilderValidation() {
|
||||||
|
// When & Then: faceId为null应该抛异常
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> {
|
||||||
|
FaceMatchingContext.builder()
|
||||||
|
.scene(FaceMatchingScene.AUTO_MATCHING)
|
||||||
|
.build();
|
||||||
|
});
|
||||||
|
|
||||||
|
// When & Then: scene为null应该抛异常
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> {
|
||||||
|
FaceMatchingContext.builder()
|
||||||
|
.faceId(1L)
|
||||||
|
.build();
|
||||||
|
});
|
||||||
|
|
||||||
|
// When & Then: CUSTOM_MATCHING场景必须提供faceSampleIds
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> {
|
||||||
|
FaceMatchingContext.builder()
|
||||||
|
.faceId(1L)
|
||||||
|
.scene(FaceMatchingScene.CUSTOM_MATCHING)
|
||||||
|
.build();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
package com.ycwl.basic.face.pipeline.stages;
|
||||||
|
|
||||||
|
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||||
|
import com.ycwl.basic.pipeline.core.StageResult;
|
||||||
|
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
||||||
|
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
|
||||||
|
import com.ycwl.basic.model.task.resp.SearchFaceRespVo;
|
||||||
|
import com.ycwl.basic.service.pc.processor.SourceRelationProcessor;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyList;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BuildSourceRelationStage 单元测试
|
||||||
|
*/
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class BuildSourceRelationStageTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private SourceRelationProcessor sourceRelationProcessor;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private BuildSourceRelationStage stage;
|
||||||
|
|
||||||
|
private FaceMatchingContext context;
|
||||||
|
private FaceEntity face;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
context = FaceMatchingContext.forAutoMatching(1L, true);
|
||||||
|
|
||||||
|
face = new FaceEntity();
|
||||||
|
face.setId(1L);
|
||||||
|
face.setMemberId(100L);
|
||||||
|
face.setScenicId(10L);
|
||||||
|
|
||||||
|
context.setFace(face);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecute_Success() {
|
||||||
|
// Given
|
||||||
|
List<Long> sampleListIds = Arrays.asList(101L, 102L, 103L);
|
||||||
|
context.setSampleListIds(sampleListIds);
|
||||||
|
|
||||||
|
List<MemberSourceEntity> memberSourceList = createMemberSourceList(3);
|
||||||
|
when(sourceRelationProcessor.processMemberSources(sampleListIds, face))
|
||||||
|
.thenReturn(memberSourceList);
|
||||||
|
|
||||||
|
// When
|
||||||
|
StageResult<FaceMatchingContext> result = stage.execute(context);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertTrue(result.isSuccess());
|
||||||
|
assertTrue(result.getMessage().contains("构建了3个源文件关联"));
|
||||||
|
assertEquals(memberSourceList, context.getMemberSourceList());
|
||||||
|
verify(sourceRelationProcessor, times(1)).processMemberSources(sampleListIds, face);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecute_SampleListIdsNull_FromSearchResult() {
|
||||||
|
// Given: sampleListIds为null,但searchResult有值
|
||||||
|
context.setSampleListIds(null);
|
||||||
|
|
||||||
|
SearchFaceRespVo searchResult = new SearchFaceRespVo();
|
||||||
|
searchResult.setSampleListIds(Arrays.asList(101L, 102L));
|
||||||
|
context.setSearchResult(searchResult);
|
||||||
|
|
||||||
|
List<MemberSourceEntity> memberSourceList = createMemberSourceList(2);
|
||||||
|
when(sourceRelationProcessor.processMemberSources(anyList(), any()))
|
||||||
|
.thenReturn(memberSourceList);
|
||||||
|
|
||||||
|
// When
|
||||||
|
StageResult<FaceMatchingContext> result = stage.execute(context);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertTrue(result.isSuccess());
|
||||||
|
assertEquals(Arrays.asList(101L, 102L), context.getSampleListIds()); // 从searchResult复制过来
|
||||||
|
verify(sourceRelationProcessor, times(1)).processMemberSources(anyList(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecute_SampleListIdsEmpty_Skipped() {
|
||||||
|
// Given
|
||||||
|
context.setSampleListIds(new ArrayList<>());
|
||||||
|
context.setSearchResult(null);
|
||||||
|
|
||||||
|
// When
|
||||||
|
StageResult<FaceMatchingContext> result = stage.execute(context);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertTrue(result.isSkipped());
|
||||||
|
assertTrue(result.getMessage().contains("sampleListIds为空"));
|
||||||
|
verify(sourceRelationProcessor, never()).processMemberSources(anyList(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecute_BothSampleListIdsAndSearchResultEmpty_Skipped() {
|
||||||
|
// Given
|
||||||
|
context.setSampleListIds(null);
|
||||||
|
|
||||||
|
SearchFaceRespVo searchResult = new SearchFaceRespVo();
|
||||||
|
searchResult.setSampleListIds(null);
|
||||||
|
context.setSearchResult(searchResult);
|
||||||
|
|
||||||
|
// When
|
||||||
|
StageResult<FaceMatchingContext> result = stage.execute(context);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertTrue(result.isSkipped());
|
||||||
|
verify(sourceRelationProcessor, never()).processMemberSources(anyList(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecute_ProcessorReturnsNull_Skipped() {
|
||||||
|
// Given
|
||||||
|
List<Long> sampleListIds = Arrays.asList(101L, 102L);
|
||||||
|
context.setSampleListIds(sampleListIds);
|
||||||
|
|
||||||
|
when(sourceRelationProcessor.processMemberSources(sampleListIds, face))
|
||||||
|
.thenReturn(null);
|
||||||
|
|
||||||
|
// When
|
||||||
|
StageResult<FaceMatchingContext> result = stage.execute(context);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertTrue(result.isSkipped());
|
||||||
|
assertTrue(result.getMessage().contains("未找到有效的源文件"));
|
||||||
|
assertNull(context.getMemberSourceList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecute_ProcessorReturnsEmpty_Skipped() {
|
||||||
|
// Given
|
||||||
|
List<Long> sampleListIds = Arrays.asList(101L, 102L);
|
||||||
|
context.setSampleListIds(sampleListIds);
|
||||||
|
|
||||||
|
when(sourceRelationProcessor.processMemberSources(sampleListIds, face))
|
||||||
|
.thenReturn(new ArrayList<>());
|
||||||
|
|
||||||
|
// When
|
||||||
|
StageResult<FaceMatchingContext> result = stage.execute(context);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertTrue(result.isSkipped());
|
||||||
|
assertTrue(result.getMessage().contains("未找到有效的源文件"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecute_ProcessorThrowsException_Degraded() {
|
||||||
|
// Given
|
||||||
|
List<Long> sampleListIds = Arrays.asList(101L, 102L);
|
||||||
|
context.setSampleListIds(sampleListIds);
|
||||||
|
|
||||||
|
when(sourceRelationProcessor.processMemberSources(sampleListIds, face))
|
||||||
|
.thenThrow(new RuntimeException("Processing error"));
|
||||||
|
|
||||||
|
// When
|
||||||
|
StageResult<FaceMatchingContext> result = stage.execute(context);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertTrue(result.isDegraded()); // 降级处理
|
||||||
|
assertTrue(result.getMessage().contains("构建源文件关联失败"));
|
||||||
|
verify(sourceRelationProcessor, times(1)).processMemberSources(sampleListIds, face);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecute_SingleSample() {
|
||||||
|
// Given: 只有1个样本
|
||||||
|
List<Long> sampleListIds = Arrays.asList(101L);
|
||||||
|
context.setSampleListIds(sampleListIds);
|
||||||
|
|
||||||
|
List<MemberSourceEntity> memberSourceList = createMemberSourceList(1);
|
||||||
|
when(sourceRelationProcessor.processMemberSources(sampleListIds, face))
|
||||||
|
.thenReturn(memberSourceList);
|
||||||
|
|
||||||
|
// When
|
||||||
|
StageResult<FaceMatchingContext> result = stage.execute(context);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertTrue(result.isSuccess());
|
||||||
|
assertTrue(result.getMessage().contains("构建了1个源文件关联"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecute_ManySamples() {
|
||||||
|
// Given: 大量样本
|
||||||
|
List<Long> sampleListIds = Arrays.asList(101L, 102L, 103L, 104L, 105L, 106L, 107L, 108L, 109L, 110L);
|
||||||
|
context.setSampleListIds(sampleListIds);
|
||||||
|
|
||||||
|
List<MemberSourceEntity> memberSourceList = createMemberSourceList(10);
|
||||||
|
when(sourceRelationProcessor.processMemberSources(sampleListIds, face))
|
||||||
|
.thenReturn(memberSourceList);
|
||||||
|
|
||||||
|
// When
|
||||||
|
StageResult<FaceMatchingContext> result = stage.execute(context);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertTrue(result.isSuccess());
|
||||||
|
assertTrue(result.getMessage().contains("构建了10个源文件关联"));
|
||||||
|
assertEquals(10, context.getMemberSourceList().size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecute_SampleListIdsDifferentFromResult() {
|
||||||
|
// Given: sampleListIds与processMemberSources返回的数量不同(部分无效)
|
||||||
|
List<Long> sampleListIds = Arrays.asList(101L, 102L, 103L, 104L, 105L);
|
||||||
|
context.setSampleListIds(sampleListIds);
|
||||||
|
|
||||||
|
List<MemberSourceEntity> memberSourceList = createMemberSourceList(3); // 只有3个有效
|
||||||
|
when(sourceRelationProcessor.processMemberSources(sampleListIds, face))
|
||||||
|
.thenReturn(memberSourceList);
|
||||||
|
|
||||||
|
// When
|
||||||
|
StageResult<FaceMatchingContext> result = stage.execute(context);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertTrue(result.isSuccess());
|
||||||
|
assertTrue(result.getMessage().contains("构建了3个源文件关联"));
|
||||||
|
assertEquals(3, context.getMemberSourceList().size());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<MemberSourceEntity> createMemberSourceList(int count) {
|
||||||
|
List<MemberSourceEntity> list = new ArrayList<>();
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
MemberSourceEntity entity = new MemberSourceEntity();
|
||||||
|
entity.setMemberId(100L);
|
||||||
|
entity.setSourceId((long) (i + 1));
|
||||||
|
list.add(entity);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
package com.ycwl.basic.face.pipeline.stages;
|
||||||
|
|
||||||
|
import com.ycwl.basic.biz.TaskStatusBiz;
|
||||||
|
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||||
|
import com.ycwl.basic.pipeline.core.StageResult;
|
||||||
|
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
||||||
|
import com.ycwl.basic.service.pc.helper.ScenicConfigFacade;
|
||||||
|
import com.ycwl.basic.service.task.TaskService;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CreateTaskStage 单元测试
|
||||||
|
*/
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class CreateTaskStageTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ScenicConfigFacade scenicConfigFacade;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private TaskService taskService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private TaskStatusBiz taskStatusBiz;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private CreateTaskStage stage;
|
||||||
|
|
||||||
|
private FaceMatchingContext context;
|
||||||
|
private FaceEntity face;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
context = FaceMatchingContext.forAutoMatching(1L, true);
|
||||||
|
|
||||||
|
face = new FaceEntity();
|
||||||
|
face.setId(1L);
|
||||||
|
face.setMemberId(100L);
|
||||||
|
face.setScenicId(10L);
|
||||||
|
|
||||||
|
context.setFace(face);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecute_AutoCreateTask_Success() {
|
||||||
|
// Given: 配置为自动创建任务(faceSelectFirst = false)
|
||||||
|
when(scenicConfigFacade.isFaceSelectFirst(10L))
|
||||||
|
.thenReturn(false);
|
||||||
|
|
||||||
|
// When
|
||||||
|
StageResult<FaceMatchingContext> result = stage.execute(context);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertTrue(result.isSuccess());
|
||||||
|
assertTrue(result.getMessage().contains("自动创建任务成功"));
|
||||||
|
verify(scenicConfigFacade, times(1)).isFaceSelectFirst(10L);
|
||||||
|
verify(taskService, times(1)).autoCreateTaskByFaceId(1L);
|
||||||
|
verify(taskStatusBiz, never()).setFaceCutStatus(anyLong(), anyInt());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecute_WaitForUserSelection_Skipped() {
|
||||||
|
// Given: 配置为等待用户选择(faceSelectFirst = true)
|
||||||
|
when(scenicConfigFacade.isFaceSelectFirst(10L))
|
||||||
|
.thenReturn(true);
|
||||||
|
|
||||||
|
// When
|
||||||
|
StageResult<FaceMatchingContext> result = stage.execute(context);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertTrue(result.isSkipped());
|
||||||
|
assertTrue(result.getMessage().contains("等待用户手动选择"));
|
||||||
|
verify(scenicConfigFacade, times(1)).isFaceSelectFirst(10L);
|
||||||
|
verify(taskStatusBiz, times(1)).setFaceCutStatus(1L, 2);
|
||||||
|
verify(taskService, never()).autoCreateTaskByFaceId(anyLong());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecute_CheckConfigFailed_Degraded() {
|
||||||
|
// Given: 查询配置失败
|
||||||
|
when(scenicConfigFacade.isFaceSelectFirst(10L))
|
||||||
|
.thenThrow(new RuntimeException("Config service error"));
|
||||||
|
|
||||||
|
// When
|
||||||
|
StageResult<FaceMatchingContext> result = stage.execute(context);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertTrue(result.isDegraded());
|
||||||
|
assertTrue(result.getMessage().contains("任务创建失败"));
|
||||||
|
verify(scenicConfigFacade, times(1)).isFaceSelectFirst(10L);
|
||||||
|
verify(taskService, never()).autoCreateTaskByFaceId(anyLong());
|
||||||
|
verify(taskStatusBiz, never()).setFaceCutStatus(anyLong(), anyInt());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecute_AutoCreateTaskFailed_Degraded() {
|
||||||
|
// Given: 自动创建任务失败
|
||||||
|
when(scenicConfigFacade.isFaceSelectFirst(10L))
|
||||||
|
.thenReturn(false);
|
||||||
|
doThrow(new RuntimeException("Task creation error"))
|
||||||
|
.when(taskService).autoCreateTaskByFaceId(1L);
|
||||||
|
|
||||||
|
// When
|
||||||
|
StageResult<FaceMatchingContext> result = stage.execute(context);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertTrue(result.isDegraded());
|
||||||
|
assertTrue(result.getMessage().contains("任务创建失败"));
|
||||||
|
verify(taskService, times(1)).autoCreateTaskByFaceId(1L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecute_SetStatusFailed_Degraded() {
|
||||||
|
// Given: 设置状态失败
|
||||||
|
when(scenicConfigFacade.isFaceSelectFirst(10L))
|
||||||
|
.thenReturn(true);
|
||||||
|
doThrow(new RuntimeException("Status set error"))
|
||||||
|
.when(taskStatusBiz).setFaceCutStatus(1L, 2);
|
||||||
|
|
||||||
|
// When
|
||||||
|
StageResult<FaceMatchingContext> result = stage.execute(context);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertTrue(result.isDegraded());
|
||||||
|
assertTrue(result.getMessage().contains("任务创建失败"));
|
||||||
|
verify(taskStatusBiz, times(1)).setFaceCutStatus(1L, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecute_DifferentScenicId() {
|
||||||
|
// Given: 不同的景区ID
|
||||||
|
face.setScenicId(999L);
|
||||||
|
|
||||||
|
when(scenicConfigFacade.isFaceSelectFirst(999L))
|
||||||
|
.thenReturn(false);
|
||||||
|
|
||||||
|
// When
|
||||||
|
StageResult<FaceMatchingContext> result = stage.execute(context);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertTrue(result.isSuccess());
|
||||||
|
verify(scenicConfigFacade, times(1)).isFaceSelectFirst(999L);
|
||||||
|
verify(taskService, times(1)).autoCreateTaskByFaceId(1L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecute_DifferentFaceId() {
|
||||||
|
// Given: 不同的faceId
|
||||||
|
context = FaceMatchingContext.forAutoMatching(888L, true);
|
||||||
|
face.setId(888L);
|
||||||
|
context.setFace(face);
|
||||||
|
|
||||||
|
when(scenicConfigFacade.isFaceSelectFirst(10L))
|
||||||
|
.thenReturn(false);
|
||||||
|
|
||||||
|
// When
|
||||||
|
StageResult<FaceMatchingContext> result = stage.execute(context);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertTrue(result.isSuccess());
|
||||||
|
verify(taskService, times(1)).autoCreateTaskByFaceId(888L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecute_NullPointerException_Degraded() {
|
||||||
|
// Given: 空指针异常
|
||||||
|
when(scenicConfigFacade.isFaceSelectFirst(10L))
|
||||||
|
.thenThrow(new NullPointerException("Null scenic config"));
|
||||||
|
|
||||||
|
// When
|
||||||
|
StageResult<FaceMatchingContext> result = stage.execute(context);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertTrue(result.isDegraded());
|
||||||
|
assertTrue(result.getMessage().contains("任务创建失败"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,299 @@
|
|||||||
|
package com.ycwl.basic.face.pipeline.stages;
|
||||||
|
|
||||||
|
import com.ycwl.basic.exception.BaseException;
|
||||||
|
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||||
|
import com.ycwl.basic.pipeline.core.StageResult;
|
||||||
|
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
|
||||||
|
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
||||||
|
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
|
||||||
|
import com.ycwl.basic.model.task.resp.SearchFaceRespVo;
|
||||||
|
import com.ycwl.basic.service.pc.helper.SearchResultMerger;
|
||||||
|
import com.ycwl.basic.service.task.TaskFaceService;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.ArgumentMatchers.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CustomFaceSearchStage 单元测试
|
||||||
|
*/
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class CustomFaceSearchStageTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private TaskFaceService taskFaceService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private SearchResultMerger resultMerger;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private IFaceBodyAdapter faceBodyAdapter;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private CustomFaceSearchStage stage;
|
||||||
|
|
||||||
|
private FaceMatchingContext context;
|
||||||
|
private FaceEntity face;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
context = FaceMatchingContext.forCustomMatching(1L, Arrays.asList(101L, 102L));
|
||||||
|
face = new FaceEntity();
|
||||||
|
face.setId(1L);
|
||||||
|
face.setScenicId(10L);
|
||||||
|
face.setFaceUrl("http://example.com/face.jpg");
|
||||||
|
context.setFace(face);
|
||||||
|
context.setFaceBodyAdapter(faceBodyAdapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecute_Mode2_DirectUse() {
|
||||||
|
// Given: 模式2,直接使用用户选择的样本
|
||||||
|
context.setFaceSelectPostMode(2);
|
||||||
|
context.setFaceSampleIds(Arrays.asList(101L, 102L, 103L));
|
||||||
|
|
||||||
|
SearchFaceRespVo directResult = new SearchFaceRespVo();
|
||||||
|
directResult.setSampleListIds(Arrays.asList(101L, 102L, 103L));
|
||||||
|
|
||||||
|
when(resultMerger.createDirectResult(Arrays.asList(101L, 102L, 103L)))
|
||||||
|
.thenReturn(directResult);
|
||||||
|
|
||||||
|
// When
|
||||||
|
StageResult<FaceMatchingContext> result = stage.execute(context);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertTrue(result.isSuccess());
|
||||||
|
assertEquals(directResult, context.getSearchResult());
|
||||||
|
assertEquals(3, context.getSampleListIds().size());
|
||||||
|
verify(resultMerger, times(1)).createDirectResult(anyList());
|
||||||
|
verify(taskFaceService, never()).searchFace(any(), anyString(), anyString(), anyString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecute_Mode2_WithMatchResult() {
|
||||||
|
// Given: 模式2,保留原始matchResult
|
||||||
|
context.setFaceSelectPostMode(2);
|
||||||
|
context.setFaceSampleIds(Arrays.asList(101L, 102L));
|
||||||
|
face.setMatchResult("{\"score\":0.85}");
|
||||||
|
|
||||||
|
SearchFaceRespVo directResult = new SearchFaceRespVo();
|
||||||
|
directResult.setSampleListIds(Arrays.asList(101L, 102L));
|
||||||
|
|
||||||
|
when(resultMerger.createDirectResult(anyList())).thenReturn(directResult);
|
||||||
|
|
||||||
|
// When
|
||||||
|
StageResult<FaceMatchingContext> result = stage.execute(context);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertTrue(result.isSuccess());
|
||||||
|
assertEquals("{\"score\":0.85}", directResult.getSearchResultJson());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecute_Mode0_Union() throws Exception {
|
||||||
|
// Given: 模式0(并集),搜索多个样本并合并
|
||||||
|
context.setFaceSelectPostMode(0);
|
||||||
|
|
||||||
|
FaceSampleEntity sample1 = createSample(101L, "http://example.com/s1.jpg");
|
||||||
|
FaceSampleEntity sample2 = createSample(102L, "http://example.com/s2.jpg");
|
||||||
|
context.setFaceSamples(Arrays.asList(sample1, sample2));
|
||||||
|
context.setFaceSampleIds(Arrays.asList(101L, 102L));
|
||||||
|
|
||||||
|
SearchFaceRespVo searchResult1 = new SearchFaceRespVo();
|
||||||
|
searchResult1.setSampleListIds(Arrays.asList(201L, 202L));
|
||||||
|
|
||||||
|
SearchFaceRespVo searchResult2 = new SearchFaceRespVo();
|
||||||
|
searchResult2.setSampleListIds(Arrays.asList(202L, 203L));
|
||||||
|
|
||||||
|
when(taskFaceService.searchFace(eq(faceBodyAdapter), eq("10"), eq("http://example.com/s1.jpg"), anyString()))
|
||||||
|
.thenReturn(searchResult1);
|
||||||
|
when(taskFaceService.searchFace(eq(faceBodyAdapter), eq("10"), eq("http://example.com/s2.jpg"), anyString()))
|
||||||
|
.thenReturn(searchResult2);
|
||||||
|
|
||||||
|
SearchFaceRespVo mergedResult = new SearchFaceRespVo();
|
||||||
|
mergedResult.setSampleListIds(Arrays.asList(201L, 202L, 203L)); // 并集
|
||||||
|
|
||||||
|
when(resultMerger.merge(anyList(), eq(0))).thenReturn(mergedResult);
|
||||||
|
|
||||||
|
// When
|
||||||
|
StageResult<FaceMatchingContext> result = stage.execute(context);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertTrue(result.isSuccess());
|
||||||
|
assertEquals(mergedResult, context.getSearchResult());
|
||||||
|
assertEquals(3, context.getSampleListIds().size());
|
||||||
|
verify(taskFaceService, times(2)).searchFace(any(), anyString(), anyString(), anyString());
|
||||||
|
verify(resultMerger, times(1)).merge(anyList(), eq(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecute_Mode1_Intersection() throws Exception {
|
||||||
|
// Given: 模式1(交集)
|
||||||
|
context.setFaceSelectPostMode(1);
|
||||||
|
|
||||||
|
FaceSampleEntity sample1 = createSample(101L, "http://example.com/s1.jpg");
|
||||||
|
FaceSampleEntity sample2 = createSample(102L, "http://example.com/s2.jpg");
|
||||||
|
context.setFaceSamples(Arrays.asList(sample1, sample2));
|
||||||
|
context.setFaceSampleIds(Arrays.asList(101L, 102L));
|
||||||
|
|
||||||
|
SearchFaceRespVo searchResult1 = new SearchFaceRespVo();
|
||||||
|
searchResult1.setSampleListIds(Arrays.asList(201L, 202L, 203L));
|
||||||
|
|
||||||
|
SearchFaceRespVo searchResult2 = new SearchFaceRespVo();
|
||||||
|
searchResult2.setSampleListIds(Arrays.asList(202L, 203L, 204L));
|
||||||
|
|
||||||
|
when(taskFaceService.searchFace(any(), anyString(), anyString(), anyString()))
|
||||||
|
.thenReturn(searchResult1, searchResult2);
|
||||||
|
|
||||||
|
SearchFaceRespVo mergedResult = new SearchFaceRespVo();
|
||||||
|
mergedResult.setSampleListIds(Arrays.asList(202L, 203L)); // 交集
|
||||||
|
|
||||||
|
when(resultMerger.merge(anyList(), eq(1))).thenReturn(mergedResult);
|
||||||
|
|
||||||
|
// When
|
||||||
|
StageResult<FaceMatchingContext> result = stage.execute(context);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertTrue(result.isSuccess());
|
||||||
|
assertEquals(2, context.getSampleListIds().size());
|
||||||
|
verify(resultMerger, times(1)).merge(anyList(), eq(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecute_DefaultMode() throws Exception {
|
||||||
|
// Given: 未设置faceSelectPostMode,默认为0
|
||||||
|
context.setFaceSelectPostMode(null);
|
||||||
|
|
||||||
|
FaceSampleEntity sample1 = createSample(101L, "http://example.com/s1.jpg");
|
||||||
|
context.setFaceSamples(Arrays.asList(sample1));
|
||||||
|
context.setFaceSampleIds(Arrays.asList(101L));
|
||||||
|
|
||||||
|
SearchFaceRespVo searchResult = new SearchFaceRespVo();
|
||||||
|
searchResult.setSampleListIds(Arrays.asList(201L));
|
||||||
|
|
||||||
|
when(taskFaceService.searchFace(any(), anyString(), anyString(), anyString()))
|
||||||
|
.thenReturn(searchResult);
|
||||||
|
|
||||||
|
SearchFaceRespVo mergedResult = new SearchFaceRespVo();
|
||||||
|
mergedResult.setSampleListIds(Arrays.asList(201L));
|
||||||
|
|
||||||
|
when(resultMerger.merge(anyList(), eq(0))).thenReturn(mergedResult);
|
||||||
|
|
||||||
|
// When
|
||||||
|
StageResult<FaceMatchingContext> result = stage.execute(context);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertTrue(result.isSuccess());
|
||||||
|
verify(resultMerger, times(1)).merge(anyList(), eq(0)); // 默认模式0
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecute_PartialSearchFailure_Success() throws Exception {
|
||||||
|
// Given: 部分样本搜索失败,但至少有一个成功
|
||||||
|
context.setFaceSelectPostMode(0);
|
||||||
|
|
||||||
|
FaceSampleEntity sample1 = createSample(101L, "http://example.com/s1.jpg");
|
||||||
|
FaceSampleEntity sample2 = createSample(102L, "http://example.com/s2.jpg");
|
||||||
|
FaceSampleEntity sample3 = createSample(103L, "http://example.com/s3.jpg");
|
||||||
|
context.setFaceSamples(Arrays.asList(sample1, sample2, sample3));
|
||||||
|
|
||||||
|
SearchFaceRespVo searchResult1 = new SearchFaceRespVo();
|
||||||
|
searchResult1.setSampleListIds(Arrays.asList(201L));
|
||||||
|
|
||||||
|
when(taskFaceService.searchFace(eq(faceBodyAdapter), eq("10"), eq("http://example.com/s1.jpg"), anyString()))
|
||||||
|
.thenReturn(searchResult1);
|
||||||
|
when(taskFaceService.searchFace(eq(faceBodyAdapter), eq("10"), eq("http://example.com/s2.jpg"), anyString()))
|
||||||
|
.thenThrow(new RuntimeException("Network error"));
|
||||||
|
when(taskFaceService.searchFace(eq(faceBodyAdapter), eq("10"), eq("http://example.com/s3.jpg"), anyString()))
|
||||||
|
.thenReturn(null);
|
||||||
|
|
||||||
|
SearchFaceRespVo mergedResult = new SearchFaceRespVo();
|
||||||
|
mergedResult.setSampleListIds(Arrays.asList(201L));
|
||||||
|
|
||||||
|
when(resultMerger.merge(anyList(), eq(0))).thenReturn(mergedResult);
|
||||||
|
|
||||||
|
// When
|
||||||
|
StageResult<FaceMatchingContext> result = stage.execute(context);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertTrue(result.isSuccess());
|
||||||
|
verify(taskFaceService, times(3)).searchFace(any(), anyString(), anyString(), anyString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecute_AllSearchFailure_ThrowException() throws Exception {
|
||||||
|
// Given: 所有样本搜索都失败
|
||||||
|
context.setFaceSelectPostMode(0);
|
||||||
|
|
||||||
|
FaceSampleEntity sample1 = createSample(101L, "http://example.com/s1.jpg");
|
||||||
|
FaceSampleEntity sample2 = createSample(102L, "http://example.com/s2.jpg");
|
||||||
|
context.setFaceSamples(Arrays.asList(sample1, sample2));
|
||||||
|
|
||||||
|
when(taskFaceService.searchFace(any(), anyString(), anyString(), anyString()))
|
||||||
|
.thenThrow(new RuntimeException("Network error"));
|
||||||
|
|
||||||
|
// When & Then
|
||||||
|
assertThrows(BaseException.class, () -> {
|
||||||
|
stage.execute(context);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecute_BaseException_Rethrow() throws Exception {
|
||||||
|
// Given
|
||||||
|
context.setFaceSelectPostMode(0);
|
||||||
|
|
||||||
|
FaceSampleEntity sample1 = createSample(101L, "http://example.com/s1.jpg");
|
||||||
|
context.setFaceSamples(Arrays.asList(sample1));
|
||||||
|
|
||||||
|
SearchFaceRespVo searchResult = new SearchFaceRespVo();
|
||||||
|
when(taskFaceService.searchFace(any(), anyString(), anyString(), anyString()))
|
||||||
|
.thenReturn(searchResult);
|
||||||
|
|
||||||
|
BaseException baseException = new BaseException("服务不可用");
|
||||||
|
when(resultMerger.merge(anyList(), anyInt())).thenThrow(baseException);
|
||||||
|
|
||||||
|
// When & Then
|
||||||
|
assertThrows(BaseException.class, () -> {
|
||||||
|
stage.execute(context);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecute_GenericException_Failed() throws Exception {
|
||||||
|
// Given
|
||||||
|
context.setFaceSelectPostMode(0);
|
||||||
|
|
||||||
|
FaceSampleEntity sample1 = createSample(101L, "http://example.com/s1.jpg");
|
||||||
|
context.setFaceSamples(Arrays.asList(sample1));
|
||||||
|
|
||||||
|
SearchFaceRespVo searchResult = new SearchFaceRespVo();
|
||||||
|
when(taskFaceService.searchFace(any(), anyString(), anyString(), anyString()))
|
||||||
|
.thenReturn(searchResult);
|
||||||
|
|
||||||
|
when(resultMerger.merge(anyList(), anyInt())).thenThrow(new RuntimeException("Merge error"));
|
||||||
|
|
||||||
|
// When
|
||||||
|
StageResult<FaceMatchingContext> result = stage.execute(context);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertTrue(result.isFailed());
|
||||||
|
assertTrue(result.getMessage().contains("自定义人脸搜索失败"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private FaceSampleEntity createSample(Long id, String faceUrl) {
|
||||||
|
FaceSampleEntity sample = new FaceSampleEntity();
|
||||||
|
sample.setId(id);
|
||||||
|
sample.setFaceUrl(faceUrl);
|
||||||
|
return sample;
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user