Compare commits

...

38 Commits

Author SHA1 Message Date
3cb12c13c2 feat(printer):优化用户照片添加逻辑并返回结果ID
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 修改 addUserPhoto 方法参数,使用 MemberPrintEntity 实体传参- 在 PrinterMapper.xml 中配置 insert 语句返回主键 ID- 更新 addUserPhotoFromSource 方法返回值为 List<Integer>
- 添加异常处理和日志记录
- 调整 AppPrinterController 接口返回照片 ID 列表
2025-10-14 11:45:46 +08:00
feac2e8d93 refactor(config): 移除ScenicConfigManager中的冗余代码
- 删除了未使用的configMap字段- 移除了基于Map的构造函数- 清理了所有与configMap相关的getter方法
- 移除了hasKey和hasNonNullValue方法
- 删除了获取所有配置键和配置数量的方法
- 移除了配置子集和扁平化配置相关功能
- 简化了toString方法的实现
2025-10-12 01:09:54 +08:00
be375067ce feat(message): 移除ZT消息生产者示例代码- 删除ZtMessageProducerExample类及相关依赖
- 移除示例消息发送逻辑
- 清理无用的HashMap和日志记录代码
- 移除条件注解@ConditionalOnProperty配置
- 删除消息构建及发送示例实现
2025-10-11 20:34:00 +08:00
7dec2e614c feat(watchdog): 增强任务监控告警机制
- 引入ZtMessageProducerService实现消息通知
- 添加任务积压、失败任务和长时间运行任务的分类监控
- 实现异常通知计数器,限制重复告警次数
-优化告警逻辑,支持异常恢复后计数器重置
- 移除旧的通知工厂依赖,统一使用消息队列发送
- 增加长时任务监控的清理机制,避免无效计数累积
2025-10-11 20:33:49 +08:00
51d0716606 Merge branch 'message-microservice'
# Conflicts:
#	src/main/java/com/ycwl/basic/integration/CLAUDE.md
2025-10-11 15:07:52 +08:00
765998bd97 docs(integration): 移除示例代码并更新配置说明- 删除设备集成测试中的默认配置启用示例
- 移除了消息集成组件中的示例引用
- 更新ZT-Message集成概述,去除对旧文档的引用
- 简化目录结构展示,移除example模块
- 清理冗余的配置键值说明- 统一删除各模块下的example目录引用
- 优化文档结构,提高可读性
2025-10-11 11:24:42 +08:00
5f4f89112b refactor(scenic): 移除ScenicV2WithConfigDTO并简化实体转换逻辑
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 删除ScenicV2WithConfigDTO类定义
- 更新ScenicV2Controller中的导入依赖- 更新ScenicV2Client中的导入依赖
- 更新ScenicIntegrationService中的导入依赖
- 更新ScenicRepository中的导入依赖
- 简化convertToScenicEntity方法参数类型
- 移除手动组合ScenicV2WithConfigDTO的代码逻辑
2025-10-11 00:11:42 +08:00
d68b062951 refactor(repository):重构景区配置管理逻辑- 引入 ScenicConfigManager 管理配置信息
- 移除手动构建 configMap 的逻辑
- 修改 convertToScenicEntity 方法签名,支持传入配置管理器
- 使用 configManager 替代直接从 DTO 获取配置值的方式
- 统一配置项获取方式,增强代码可维护性与扩展性
2025-10-11 00:10:25 +08:00
99857db006 feat(examples): 移除设备和问卷集成示例代码
- 删除默认配置集成服务使用示例类- 移除设备配置筛选功能使用示例
- 清理设备集成基础操作示例代码
- 移除设备集成降级机制示例
- 删除Kafka集成使用示例
- 清理问卷集成服务示例代码
2025-10-11 00:09:33 +08:00
e8c645a3c0 refactor(device): 移除设备与景区的冗余配置接口
Some checks failed
ZhenTu-BE/pipeline/head There was a failure building this commit
- 删除 DeviceV2Controller 中的设备配置相关接口
- 删除 ScenicV2Controller 中的景区配置相关接口
- 移除 DeviceConfigV2Client 中的扁平化配置接口
- 移除 DeviceV2Client 中的设备详情配置接口
- 更新 DeviceIntegrationExample 示例代码
- 移除 DeviceIntegrationFallbackExample 中的配置缓存示例
- 删除 DeviceConfigIntegrationService 中的配置获取方法
- 删除 DeviceIntegrationService 中的设备配置服务方法- 移除 RenderWorkerV2Client 中的工作器配置接口- 删除 RenderWorkerConfigIntegrationService 中的配置键名- 移除 RenderWorkerIntegrationService 中的工作器配置方法
- 删除 ScenicConfigV2Client 中的扁平化配置接口
- 移除 ScenicV2Client 中的景区配置接口
- 更新 ScenicIntegrationExample 示例代码
- 删除 ScenicConfigIntegrationService 中的配置获取方法
- 删除 ScenicIntegrationService 中的景区配置服务方法
- 修改 ScenicRepository 中景区实体获取逻辑
2025-10-10 23:55:17 +08:00
fe8068b3d9 refactor(scenic): 重构景区配置响应结构
- 移除了过时的配置字段,如预约流程、强制完成时间等
- 调整了字段顺序并添加分类注释(基础配置、功能开关、提示文案)
-保留并优化核心配置项,如水印URL、防录屏类型等
- 清理了未使用的导入包和冗余代码
- 统一了优惠券开关字段,移除重复定义
2025-10-10 13:46:59 +08:00
c689496130 feat(scenic): 添加分享功能配置项
- 在ScenicConfigResp中新增shareEnable字段
- 在AppScenicController中设置shareEnable默认值为true
- 支持景区配置是否开启分享功能
- 保持与shareBeforeBuy配置项的一致性处理
2025-10-10 10:38:47 +08:00
7e16ad35e7 feat(app): 新增分享前购买配置项
- 在AppScenicController中增加shareBeforeBuy配置项- 默认值设置为true以启用该功能- 更新响应对象以支持新的配置选项
2025-10-10 09:26:42 +08:00
1727619b29 refactor(kafka): 将人脸识别处理改为异步执行- 引入CompletableFuture实现异步处理
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 修改processFaceRecognition方法为异步版本
- 移除原同步处理中的try-catch块
- 更新方法返回类型从boolean改为void-保留处理成功和失败的状态更新逻辑- 添加异步处理成功后的日志记录
2025-10-04 10:12:37 +08:00
3099e68a97 refactor(logging): 调整人脸处理服务中的日志级别
- 将接收到人脸消息的日志级别从 info 调整为 debug
- 移除了部分冗余的 info 级别日志输出
- 统一异常处理中的日志记录方式
-优化日志内容,减少不必要的信息输出
- 确保关键操作仍然保留适当日志记录- 提升系统在高并发下的日志可读性与性能
2025-10-03 13:46:22 +08:00
db86c82bc8 refactor(task):优化视频片段获取逻辑并增强日志记录
- 移除任务执行前的空列表检查,统一通过VideoPieceGetter.addTask处理
- 增强Placeholder初始化阶段的日志输出,区分有无templateId情况- 细化计数器递减过程中的日志信息,记录设备关联及剩余数量
- 完善进度检查时的日志内容,增加已完成与未完成的统计显示- 补充Callback调用条件判断,避免重复触发并记录调用状态
- 添加兜底逻辑中对Callback是否已触发的判断和相应日志提示
2025-10-01 22:01:34 +08:00
f33ce8e7a7 feat(video):优化视频切片任务处理逻辑
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 添加对配对设备的处理,确保主设备也能正确执行切片任务
- 调整计数器逻辑,使主设备和配对设备的未完成占位符计数一致
- 增强日志记录,明确标识设备占位符满足情况
- 改进进度计算方式,更准确地反映任务完成状态- 在所有占位符满足时提前调用回调函数,提升任务执行效率
2025-10-01 21:22:19 +08:00
de65fa1dd8 feat(scenic): 添加水印URL配置支持
- 在ScenicConfigResp中新增watermarkUrl字段
- 在AppScenicController中设置水印URL配置项
- 支持从scenicConfig中获取watermark_url配置值
2025-10-01 17:00:44 +08:00
132a539bb6 fix(kafka): 调整人脸识别消息处理逻辑,确保消息始终被消费- 修改消息处理失败时的确认机制,避免消息堆积
- 即使人脸样本保存或识别处理失败,也消费消息防止重复处理
- 异常情况下同样确认消息消费,记录错误日志而非阻塞流程- 优化日志记录,明确区分处理结果与消息确认状态
2025-09-28 11:26:01 +08:00
9f66544a29 feat(source): 处理ZT-Source消息时支持设备裁剪配置
- 新增DeviceRepository依赖注入
- 获取设备配置管理器并检查裁剪配置
- 根据裁剪配置设置缩略图URL
-优化sourceEntity数据处理逻辑
2025-09-27 23:28:50 +08:00
f4a16b5b09 feat(dto): 添加位置信息字段支持
- 在 ZTSourceMessage DTO 中新增 posJson 字段
- 更新数据库插入语句以支持 posJson 字段存储
- 调整日志输出内容,突出关键业务标识
- 在数据服务层增加对 posJson 的处理逻辑
2025-09-27 23:09:44 +08:00
9bc34fcfdb feat(kafka): 新增ZT-Source Kafka消息处理功能
- 新增ZTSourceMessage实体类用于接收Kafka消息
- 新增ZTSourceConsumerService监听zt-source主题
- 新增ZTSourceDataService处理消息并保存至数据库- 扩展SourceMapper支持从ZT-Source消息新增素材
- 实现照片类型素材的解析、校验与存储逻辑
- 添加Kafka手动ACK确认机制确保消息可靠处理
2025-09-27 22:16:47 +08:00
4b01e4cf82 feat(task):优化人脸识别任务中的样本排序逻辑
- 引入HashMap以支持按ID顺序排序人脸样本列表
- 在筛选前对搜索结果按分数降序排序
- 简化设备照片限制逻辑,去除冗余的时间排序步骤
- 提升匹配准确性和处理效率
2025-09-27 13:45:05 +08:00
f885f734ad perf(viid):优化线程池配置与图片裁剪内存管理
- 调整线程池核心线程数为8,最大线程数为32,空闲时间10秒- 队列大小从1024降至100,提升响应速度
- 添加CallerRunsPolicy策略,防止任务丢失
- 图片裁剪方法增加try-finally块确保资源释放- 显式调用image.flush()和System.gc()优化内存使用
- ByteArrayOutputStream关闭操作添加异常捕获
-修复潜在的内存泄漏问题
2025-09-27 13:17:48 +08:00
ddbc2a0edb fix(biz):修复用户购买检查逻辑
- 修改PriceBiz中checkUserBuyItem方法的模板ID参数为-1
- 在FaceServiceImpl中增加对模板ID的购买检查逻辑- 确保用户购买状态判断的准确性
2025-09-27 01:50:26 +08:00
da89067c48 refactor(task):优化视频片段获取任务的设备计数逻辑
- 将 currentUnFinPlaceholder从 List 类型改为 Map<String, AtomicInteger>- 使用 AtomicInteger 跟踪每个设备的未完成任务数量
- 在设备任务完成时正确减少计数并清理已完成的设备
- 更新进度日志以反映去重后的设备总数
2025-09-27 01:07:52 +08:00
2836326518 fix(face):修复vlog渲染状态显示错误问题
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 调整step3状态逻辑,确保渲染中状态正确显示
- 修改状态文本提示,优化用户体验
-修复渲染完成状态判断逻辑错误
2025-09-26 17:34:12 +08:00
6091d41df9 feat(face):优化视频切分任务筛选逻辑
- 按设备ID分组并按创建时间倒序排序
- 根据设备配置限制视频数量
- 修复日志中原始
2025-09-26 16:43:20 +08:00
d4f9f1fe0d feat(face):优化视频重切任务的样本选择逻辑
- 根据设备配置限制视频样本数量
- 实现按设备分组并应用数量限制- 更新视频重切任务中的样本ID列表
- 保留原有照片与视频数量比较逻辑
2025-09-26 16:20:31 +08:00
d860996f6d feat(face):优化视频重切任务的样本选择逻辑
- 根据设备配置限制视频样本数量
- 实现按设备分组并应用数量限制- 更新视频重切任务中的样本ID列表
- 保留原有照片与视频数量比较逻辑
2025-09-26 16:15:34 +08:00
1b2793215f fix(video): 解决并发环境下视频片段处理的文件名冲突问题
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 为输出文件名添加时间戳和线程ID后缀,确保唯一性
-为临时文件名添加时间戳和线程ID后缀,防止并发冲突
- 避免因文件名重复导致的视频处理错误
2025-09-26 14:26:09 +08:00
4f1443a3ca fix(video): 处理空imgSource情况- 添加空值检查以避免保存空source记录
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 记录警告日志当imgSource为空时- 返回false以跳过无效处理流程
2025-09-26 12:39:22 +08:00
aba9fb0a15 feat(printer): 添加用户购买项设置的Redis缓存控制
- 引入RedisTemplate依赖用于缓存控制
- 新增60秒的缓存键避免重复处理用户购买项
- 在setUserIsBuyItem方法中实现缓存检查逻辑- 添加TimeUnit依赖支持缓存过期时间设置
- 定义USER_PHOTO_LIST_TO_PRINTER缓存键前缀
2025-09-26 12:39:17 +08:00
ab3208c9df feat(kafka): 添加手动提交模式支持以增强消息处理可靠性
- 在 KafkaConfig 中新增 manualCommitKafkaListenerContainerFactory 配置
- 启用手动提交模式并设置 AckMode 为 MANUAL_IMMEDIATE
- 修改 FaceProcessingKafkaService 使用新的容器工厂- 添加 Acknowledgment 参数以控制消息提交时机
-仅在人脸样本保存和识别全部成功后才手动确认消息
- 处理失败时不再调用 ack.acknowledge()使消息可重新消费
- 更新 processFaceRecognition 方法返回处理结果状态
- 增强异常处理逻辑,确保失败情况下不提交消息
2025-09-25 18:46:15 +08:00
09e376e089 refactor(kafka): 统一时人脸消息时间类型为Date
- 将FaceProcessingMessage中的LocalDateTime替换为Date类型- 更新消息创建工厂方法以使用Date参数
- 调整Kafka服务中时间转换逻辑以匹配新类型
- 移除LocalDateTime相关的导入和引用- 更新字段注释以反映新的时间类型
2025-09-25 18:09:17 +08:00
dad9ddc17c docs 2025-09-25 16:18:03 +08:00
b14754ec0a feat(integration): 添加消息服务相关接口和功能
- 新增 MessageController 类,实现消息列表查询和消息通道列表获取功能
- 新增 MessageClient 接口,用于调用消息服务的 Feign客户端
- 新增 ChannelsResponse、MessageListData 和 MessageRecordDTO 数据传输对象
- 新增 MessageIntegrationService 服务类,处理消息服务相关业务逻辑
2025-09-17 21:53:41 +08:00
a888ed3fe2 feat(integration): 添加 ZT-Message Kafka 生产者集成
- 新增 ZtMessage DTO 类用于消息体
- 实现 ZtMessageProducerService 生产者服务
- 添加示例演示如何发送消息
- 更新配置文件和文档以支持新功能
2025-09-17 21:38:26 +08:00
58 changed files with 1212 additions and 2227 deletions

View File

@@ -166,7 +166,7 @@ public class PriceBiz {
allContentsPurchased = false;
break;
}
boolean hasPurchasedTemplate = orderBiz.checkUserBuyItem(userId, 0, videoEntities.getFirst().getVideoId());
boolean hasPurchasedTemplate = orderBiz.checkUserBuyItem(userId, -1, videoEntities.getFirst().getVideoId());
if (!hasPurchasedTemplate) {
allContentsPurchased = false;
break;

View File

@@ -10,6 +10,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.core.*;
import org.springframework.kafka.listener.ContainerProperties;
import java.util.HashMap;
import java.util.Map;
@@ -42,6 +43,12 @@ public class KafkaConfig {
@Value("${kafka.producer.buffer-memory:33554432}")
private Integer bufferMemory;
@Value("${kafka.producer.enable-idempotence:true}")
private boolean enableIdempotence;
@Value("${kafka.producer.compression-type:snappy}")
private String compressionType;
@Bean
public ProducerFactory<String, String> producerFactory() {
Map<String, Object> configProps = new HashMap<>();
@@ -53,6 +60,8 @@ public class KafkaConfig {
configProps.put(ProducerConfig.BATCH_SIZE_CONFIG, batchSize);
configProps.put(ProducerConfig.LINGER_MS_CONFIG, lingerMs);
configProps.put(ProducerConfig.BUFFER_MEMORY_CONFIG, bufferMemory);
configProps.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, enableIdempotence);
configProps.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, compressionType);
return new DefaultKafkaProducerFactory<>(configProps);
}
@@ -80,4 +89,21 @@ public class KafkaConfig {
factory.setConsumerFactory(consumerFactory());
return factory;
}
@Bean
public ConcurrentKafkaListenerContainerFactory<String, String> manualCommitKafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
Map<String, Object> props = new HashMap<>();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
props.put(ConsumerConfig.GROUP_ID_CONFIG, consumerGroupId);
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, autoOffsetReset);
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
factory.setConsumerFactory(new DefaultKafkaConsumerFactory<>(props));
factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL_IMMEDIATE);
return factory;
}
}

View File

@@ -75,8 +75,8 @@ public class AppPrinterController {
}
@PostMapping("/uploadTo/{scenicId}/formSource")
public ApiResponse<?> uploadFromSource(@PathVariable("scenicId") Long scenicId, @RequestBody FromSourceReq req) throws IOException {
printerService.addUserPhotoFromSource(JwtTokenUtil.getWorker().getUserId(), scenicId, req);
return ApiResponse.success(null);
List<Integer> list = printerService.addUserPhotoFromSource(JwtTokenUtil.getWorker().getUserId(), scenicId, req);
return ApiResponse.success(list);
}
@PostMapping("/setQuantity/{scenicId}/{id}")

View File

@@ -72,26 +72,14 @@ public class AppScenicController {
public ApiResponse<ScenicConfigResp> getConfig(@PathVariable Long id){
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(id);
ScenicConfigResp resp = new ScenicConfigResp();
resp.setBookRoutine(scenicConfig.getInteger("book_routine"));
resp.setForceFinishTime(scenicConfig.getInteger("force_finish_time"));
resp.setTourTime(scenicConfig.getInteger("tour_time"));
resp.setSampleStoreDay(scenicConfig.getInteger("sample_store_day"));
resp.setFaceStoreDay(scenicConfig.getInteger("face_store_day"));
resp.setWatermarkUrl(scenicConfig.getString("watermark_url"));
resp.setVideoStoreDay(scenicConfig.getInteger("video_store_day"));
resp.setAllFree(scenicConfig.getBoolean("all_free"));
resp.setDisableSourceVideo(scenicConfig.getBoolean("disable_source_video"));
resp.setDisableSourceImage(scenicConfig.getBoolean("disable_source_image"));
resp.setAntiScreenRecordType(scenicConfig.getInteger("anti_screen_record_type"));
resp.setVideoSourceStoreDay(scenicConfig.getInteger("video_source_store_day"));
resp.setImageSourceStoreDay(scenicConfig.getInteger("image_source_store_day"));
resp.setUserSourceExpireDay(scenicConfig.getInteger("user_source_expire_day"));
resp.setBrokerDirectRate(scenicConfig.getBigDecimal("broker_direct_rate"));
resp.setVideoSourcePackHint(scenicConfig.getString("video_source_pack_hint"));
resp.setImageSourcePackHint(scenicConfig.getString("image_source_pack_hint"));
resp.setVoucherEnable(scenicConfig.getBoolean("voucher_enable", false));
resp.setEnableVoucher(scenicConfig.getBoolean("voucher_enable", false)); // compactible
resp.setGroupingEnable(scenicConfig.getBoolean("grouping_enable", false));
resp.setVoucherEnable(scenicConfig.getBoolean("voucher_enable", false));
resp.setShowPhotoWhenWaiting(scenicConfig.getBoolean("show_photo_when_waiting", false));
resp.setImageSourcePackHint(scenicConfig.getString("image_source_pack_hint"));
resp.setVideoSourcePackHint(scenicConfig.getString("video_source_pack_hint"));
return ApiResponse.success(resp);
}

View File

@@ -63,34 +63,6 @@ public class DeviceV2Controller {
}
}
/**
* 设备V2带配置信息分页列表
*/
@GetMapping("/with-config")
public ApiResponse<PageResponse<DeviceV2WithConfigDTO>> listDevicesWithConfig(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) String name,
@RequestParam(required = false) String no,
@RequestParam(required = false) String type,
@RequestParam(required = false) Integer isActive,
@RequestParam(required = false) Long scenicId) {
log.info("分页查询设备带配置信息列表, page: {}, pageSize: {}, name: {}, no: {}, type: {}, isActive: {}, scenicId: {}",
page, pageSize, name, no, type, isActive, scenicId);
// 参数验证:限制pageSize最大值为100
if (pageSize > 100) {
pageSize = 100;
}
try {
PageResponse<DeviceV2WithConfigDTO> response = deviceIntegrationService.listDevicesWithConfig(page, pageSize, name, no, type, isActive, scenicId);
return ApiResponse.success(response);
} catch (Exception e) {
log.error("分页查询设备带配置信息列表失败", e);
return ApiResponse.fail("分页查询设备带配置信息列表失败: " + e.getMessage());
}
}
/**
* 根据ID获取设备信息
*/
@@ -105,20 +77,6 @@ public class DeviceV2Controller {
}
}
/**
* 根据ID获取设备带配置信息
*/
@GetMapping("/{id}/with-config")
public ApiResponse<DeviceV2WithConfigDTO> getDeviceWithConfig(@PathVariable Long id) {
try {
DeviceV2WithConfigDTO device = deviceIntegrationService.getDeviceWithConfig(id);
return ApiResponse.success(device);
} catch (Exception e) {
log.error("获取设备配置信息失败, id: {}", id, e);
return ApiResponse.fail("获取设备配置信息失败: " + e.getMessage());
}
}
/**
* 根据设备编号获取设备信息
*/
@@ -133,20 +91,6 @@ public class DeviceV2Controller {
}
}
/**
* 根据设备编号获取设备带配置信息
*/
@GetMapping("/no/{no}/with-config")
public ApiResponse<DeviceV2WithConfigDTO> getDeviceWithConfigByNo(@PathVariable String no) {
try {
DeviceV2WithConfigDTO device = deviceIntegrationService.getDeviceWithConfigByNo(no);
return ApiResponse.success(device);
} catch (Exception e) {
log.error("根据设备编号获取设备配置信息失败, no: {}", no, e);
return ApiResponse.fail("根据设备编号获取设备配置信息失败: " + e.getMessage());
}
}
/**
* 根据设备ID获取设备在线状态
*/
@@ -327,20 +271,6 @@ public class DeviceV2Controller {
}
}
/**
* 获取设备扁平化配置
*/
@GetMapping("/{id}/flat-config")
public ApiResponse<Map<String, Object>> getDeviceFlatConfig(@PathVariable Long id) {
try {
Map<String, Object> config = deviceConfigIntegrationService.getDeviceFlatConfig(id);
return ApiResponse.success(config);
} catch (Exception e) {
log.error("获取设备扁平化配置失败, deviceId: {}", id, e);
return ApiResponse.fail("获取设备扁平化配置失败: " + e.getMessage());
}
}
/**
* 根据配置键获取配置
*/
@@ -371,21 +301,6 @@ public class DeviceV2Controller {
}
}
/**
* 根据设备编号获取扁平化配置
*/
@GetMapping("/no/{no}/flat-config")
public ApiResponse<Map<String, Object>> getDeviceFlatConfigByNo(@PathVariable String no) {
log.info("根据设备编号获取扁平化配置, deviceNo: {}", no);
try {
Map<String, Object> config = deviceConfigIntegrationService.getDeviceFlatConfigByNo(no);
return ApiResponse.success(config);
} catch (Exception e) {
log.error("根据设备编号获取扁平化配置失败, deviceNo: {}", no, e);
return ApiResponse.fail("根据设备编号获取扁平化配置失败: " + e.getMessage());
}
}
/**
* 创建设备配置
*/

View File

@@ -0,0 +1,60 @@
package com.ycwl.basic.controller.pc;
import com.ycwl.basic.integration.message.dto.ChannelsResponse;
import com.ycwl.basic.integration.message.dto.MessageListData;
import com.ycwl.basic.integration.message.service.MessageIntegrationService;
import com.ycwl.basic.utils.ApiResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequestMapping("/api/message/v1")
@RequiredArgsConstructor
public class MessageController {
private final MessageIntegrationService messageService;
@GetMapping("/messages")
public ApiResponse<MessageListData> listMessages(
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "20") Integer pageSize,
@RequestParam(required = false) String channelId,
@RequestParam(required = false) String title,
@RequestParam(required = false) String content,
@RequestParam(required = false) String sendBiz,
@RequestParam(required = false) String sentAtStart,
@RequestParam(required = false) String sentAtEnd,
@RequestParam(required = false) String createdAtStart,
@RequestParam(required = false) String createdAtEnd
) {
log.info("PC|消息列表查询 page={}, pageSize={}, channelId={}, title={}, sendBiz={}", page, pageSize, channelId, title, sendBiz);
if (pageSize > 100) {
pageSize = 100;
}
try {
MessageListData data = messageService.listMessages(page, pageSize, channelId, title, content, sendBiz,
sentAtStart, sentAtEnd, createdAtStart, createdAtEnd);
return ApiResponse.success(data);
} catch (Exception e) {
log.error("PC|消息列表查询失败", e);
return ApiResponse.fail("消息列表查询失败: " + e.getMessage());
}
}
@GetMapping("/channels")
public ApiResponse<ChannelsResponse> listChannels() {
log.info("PC|获取消息通道列表");
try {
ChannelsResponse data = messageService.listChannels();
return ApiResponse.success(data);
} catch (Exception e) {
log.error("PC|获取消息通道列表失败", e);
return ApiResponse.fail("获取消息通道列表失败: " + e.getMessage());
}
}
}

View File

@@ -10,7 +10,6 @@ import com.ycwl.basic.integration.scenic.dto.filter.ScenicFilterRequest;
import com.ycwl.basic.integration.scenic.dto.scenic.CreateScenicRequest;
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO;
import com.ycwl.basic.integration.common.response.PageResponse;
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2WithConfigDTO;
import com.ycwl.basic.integration.scenic.dto.scenic.UpdateScenicRequest;
import com.ycwl.basic.integration.scenic.service.ScenicConfigIntegrationService;
import com.ycwl.basic.integration.scenic.service.ScenicIntegrationService;
@@ -71,30 +70,6 @@ public class ScenicV2Controller {
}
}
/**
* 景区V2带配置信息分页列表
*/
@GetMapping("/with-config")
public ApiResponse<PageResponse<ScenicV2WithConfigDTO>> listScenicsWithConfig(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) Integer status,
@RequestParam(required = false) String name) {
log.info("分页查询景区带配置信息列表, page: {}, pageSize: {}, status: {}, name: {}", page, pageSize, status, name);
// 参数验证:限制pageSize最大值为100
if (pageSize > 100) {
pageSize = 100;
}
try {
PageResponse<ScenicV2WithConfigDTO> response = scenicIntegrationService.listScenicsWithConfig(page, pageSize, status, name);
return ApiResponse.success(response);
} catch (Exception e) {
log.error("分页查询景区带配置信息列表失败", e);
return ApiResponse.fail("分页查询景区带配置信息列表失败: " + e.getMessage());
}
}
/**
* 查询单个景区详情
*/
@@ -192,36 +167,6 @@ public class ScenicV2Controller {
// ========== 景区配置管理 ==========
/**
* 获取景区及其配置信息
*/
@GetMapping("/{scenicId}/with-config")
public ApiResponse<ScenicV2WithConfigDTO> getScenicWithConfig(@PathVariable Long scenicId) {
log.info("获取景区配置信息, scenicId: {}", scenicId);
try {
ScenicV2WithConfigDTO scenic = scenicIntegrationService.getScenicWithConfig(scenicId);
return ApiResponse.success(scenic);
} catch (Exception e) {
log.error("获取景区配置信息失败, scenicId: {}", scenicId, e);
return ApiResponse.fail("获取景区配置信息失败: " + e.getMessage());
}
}
/**
* 获取景区扁平化配置
*/
@GetMapping("/{scenicId}/flat-config")
public ApiResponse<Map<String, Object>> getScenicFlatConfig(@PathVariable Long scenicId) {
log.info("获取景区扁平化配置, scenicId: {}", scenicId);
try {
Map<String, Object> config = scenicIntegrationService.getScenicFlatConfig(scenicId);
return ApiResponse.success(config);
} catch (Exception e) {
log.error("获取景区扁平化配置失败, scenicId: {}", scenicId, e);
return ApiResponse.fail("获取景区扁平化配置失败: " + e.getMessage());
}
}
/**
* 获取景区配置列表
*/
@@ -316,20 +261,4 @@ public class ScenicV2Controller {
return ApiResponse.fail("批量更新配置失败: " + e.getMessage());
}
}
/**
* 扁平化批量更新景区配置
*/
@PutMapping("/{scenicId}/flat-config")
public ApiResponse<BatchUpdateResponse> batchFlatUpdateConfigs(@PathVariable Long scenicId,
@RequestBody Map<String, Object> configs) {
log.info("扁平化批量更新景区配置, scenicId: {}, configs count: {}", scenicId, configs.size());
try {
BatchUpdateResponse response = scenicConfigIntegrationService.batchFlatUpdateConfigs(scenicId, configs);
return ApiResponse.success(response);
} catch (Exception e) {
log.error("扁平化批量更新景区配置失败, scenicId: {}", scenicId, e);
return ApiResponse.fail("扁平化批量更新配置失败: " + e.getMessage());
}
}
}

View File

@@ -101,9 +101,11 @@ public class ViidController {
.setNamePrefix("VIID-" + scenicId + "-t")
.build();
return new ThreadPoolExecutor(
4, 1024, 0L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(1024),
threadFactory);
8, 32, 10L, TimeUnit.SECONDS, // 核心2个线程,最大20个线程,空闲60秒回收
new ArrayBlockingQueue<>(1024), // 队列大小从1024降至100
threadFactory,
new ThreadPoolExecutor.CallerRunsPolicy() // 队列满时由调用线程执行,提供背压控制
);
});
}

View File

@@ -0,0 +1,65 @@
package com.ycwl.basic.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;
/**
* ZT-Source Kafka消息实体
* 用于接收素材数据(照片和视频片段)
*
* @author system
* @date 2024/12/27
*/
@Data
public class ZTSourceMessage {
@JsonProperty("sourceId")
private Long sourceId;
@JsonProperty("sourceType")
private Integer sourceType;
@JsonProperty("scenicId")
private Long scenicId;
@JsonProperty("deviceId")
private Long deviceId;
@JsonProperty("shootTime")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date shootTime;
@JsonProperty("thumbnailUrl")
private String thumbnailUrl;
@JsonProperty("sourceUrl")
private String sourceUrl;
@JsonProperty("resolution")
private String resolution;
@JsonProperty("faceSampleId")
private Long faceSampleId;
@JsonProperty("posJson")
private String posJson;
/**
* 判断是否为视频片段
*/
public boolean isVideo() {
return sourceType != null && sourceType == 1;
}
/**
* 判断是否为照片
*/
public boolean isPhoto() {
return sourceType != null && sourceType == 2;
}
}

View File

@@ -25,6 +25,7 @@ Currently implemented:
- **Device Integration** (`com.ycwl.basic.integration.device`): ZT-Device microservice integration
- **Render Worker Integration** (`com.ycwl.basic.integration.render`): ZT-Render-Worker microservice integration
- **Questionnaire Integration** (`com.ycwl.basic.integration.questionnaire`): ZT-Questionnaire microservice integration
- **Message Integration** (`com.ycwl.basic.integration.message`): ZT-Message Kafka producer integration
### Integration Pattern
@@ -34,8 +35,7 @@ service/
├── client/ # Feign clients for HTTP calls
├── config/ # Service-specific configuration
├── dto/ # Data transfer objects
── service/ # Service layer with business logic
└── example/ # Usage examples
── service/ # Service layer with business logic
```
## Integration Fallback Mechanism
@@ -792,13 +792,6 @@ mvn test -Dtest=DefaultConfigIntegrationServiceTest
# Run all device integration tests (including default configs)
mvn test -Dtest="com.ycwl.basic.integration.device.*Test"
# Enable example runner in application-dev.yml
integration:
device:
example:
default-config:
enabled: true
```
### Common Configuration Keys
@@ -820,8 +813,7 @@ com.ycwl.basic.integration.{service-name}/
├── client/
├── config/
├── dto/
── service/
└── example/
── service/
```
### 2. Add Configuration Properties
@@ -1168,6 +1160,57 @@ fallbackService.clearAllFallbackCache("zt-render-worker");
- **Active (isActive=1)**: Worker is available for tasks
- **Inactive (isActive=0)**: Worker is disabled
## ZT-Message Integration (Kafka Producer)
### Overview
The zt-message microservice accepts messages via Kafka on topic `zt-message`. This integration provides a simple producer service to publish notification messages.
- Topic: `zt-message`
- Key: Use `channelId` for partitioning stability
- Value: UTF-8 JSON with fields: `channelId` (required), `title` (required), `content` (required), `target` (required), `extra` (object, optional), `sendReason` (optional), `sendBiz` (optional)
### Components
- `com.ycwl.basic.integration.message.dto.ZtMessage`: DTO for message body
- `com.ycwl.basic.integration.message.service.ZtMessageProducerService`: Producer service using Spring Kafka
### Configuration
```yaml
kafka:
enabled: true # enable Kafka integration
bootstrap-servers: 127.0.0.1:9092 # adjust per environment
zt-message-topic: zt-message # topic name (default already zt-message)
producer:
acks: all
enable-idempotence: true
retries: 5
linger-ms: 10
batch-size: 32768
compression-type: snappy
```
### Usage
```java
@Autowired
private ZtMessageProducerService producer;
public void sendWelcome() {
ZtMessage msg = ZtMessage.of("dummy", "欢迎", "注册成功", "user-001");
Map<String, Object> extra = new HashMap<>();
extra.put("k", "v");
msg.setExtra(extra);
msg.setSendReason("REGISTER");
msg.setSendBiz("USER");
producer.send(msg); // key uses channelId, value is JSON
}
```
### Notes
- Required fields must be non-empty: `channelId`, `title`, `content`, `target`
- Keep message body small (< 100 KB)
- Use string for 64-bit integers in `extra` to avoid JS precision loss
- Service logs the partition/offset upon success, errors on failure
## Common Development Tasks
### Running Integration Tests

View File

@@ -16,8 +16,6 @@ import java.util.stream.Collectors;
*/
public class ScenicConfigManager extends ConfigManager<ScenicConfigV2DTO> {
private final Map<String, Object> configMap;
/**
* 从配置列表构造管理器
*
@@ -25,26 +23,7 @@ public class ScenicConfigManager extends ConfigManager<ScenicConfigV2DTO> {
*/
public ScenicConfigManager(List<ScenicConfigV2DTO> configList) {
super(configList);
this.configMap = new HashMap<>();
if (configList != null) {
for (ScenicConfigV2DTO config : configList) {
if (config.getConfigKey() != null && config.getConfigValue() != null) {
this.configMap.put(config.getConfigKey(), config.getConfigValue());
}
}
}
}
/**
* 从配置Map构造管理器
*
* @param configMap 配置Map
*/
public ScenicConfigManager(Map<String, Object> configMap) {
super(null); // 使用Map构造时,父类configs为null
this.configMap = configMap != null ? new HashMap<>(configMap) : new HashMap<>();
}
@Override
protected String getConfigKey(ScenicConfigV2DTO config) {
return config != null ? config.getConfigKey() : null;
@@ -55,276 +34,4 @@ public class ScenicConfigManager extends ConfigManager<ScenicConfigV2DTO> {
return config != null ? config.getConfigValue() : null;
}
/**
* 获取长整数值
*
* @param key 配置键
* @return Long值,如果键不存在或转换失败返回null
*/
public Long getLong(String key) {
return ConfigValueUtil.getLongValue(configMap, key);
}
/**
* 获取长整数值,如果为null则返回默认值
*
* @param key 配置键
* @param defaultValue 默认值
* @return Long值或默认值
*/
public Long getLong(String key, Long defaultValue) {
Long value = ConfigValueUtil.getLongValue(configMap, key);
return value != null ? value : defaultValue;
}
/**
* 获取浮点数值
*
* @param key 配置键
* @return Float值,如果键不存在或转换失败返回null
*/
public Float getFloat(String key) {
return ConfigValueUtil.getFloatValue(configMap, key);
}
/**
* 获取浮点数值,如果为null则返回默认值
*
* @param key 配置键
* @param defaultValue 默认值
* @return Float值或默认值
*/
public Float getFloat(String key, Float defaultValue) {
Float value = ConfigValueUtil.getFloatValue(configMap, key);
return value != null ? value : defaultValue;
}
/**
* 获取双精度浮点数值
*
* @param key 配置键
* @return Double值,如果键不存在或转换失败返回null
*/
public Double getDouble(String key) {
return ConfigValueUtil.getDoubleValue(configMap, key);
}
/**
* 获取双精度浮点数值,如果为null则返回默认值
*
* @param key 配置键
* @param defaultValue 默认值
* @return Double值或默认值
*/
public Double getDouble(String key, Double defaultValue) {
Double value = ConfigValueUtil.getDoubleValue(configMap, key);
return value != null ? value : defaultValue;
}
/**
* 获取高精度小数值
*
* @param key 配置键
* @return BigDecimal值,如果键不存在或转换失败返回null
*/
public BigDecimal getBigDecimal(String key) {
return ConfigValueUtil.getBigDecimalValue(configMap, key);
}
/**
* 获取高精度小数值,如果为null则返回默认值
*
* @param key 配置键
* @param defaultValue 默认值
* @return BigDecimal值或默认值
*/
public BigDecimal getBigDecimal(String key, BigDecimal defaultValue) {
BigDecimal value = ConfigValueUtil.getBigDecimalValue(configMap, key);
return value != null ? value : defaultValue;
}
/**
* 获取布尔值
*
* @param key 配置键
* @return Boolean值,如果键不存在或转换失败返回null
*/
public Boolean getBoolean(String key) {
return ConfigValueUtil.getBooleanValue(configMap, key);
}
/**
* 获取布尔值,如果为null则返回默认值
*
* @param key 配置键
* @param defaultValue 默认值
* @return Boolean值或默认值
*/
public Boolean getBoolean(String key, Boolean defaultValue) {
return ConfigValueUtil.getBooleanValue(configMap, key, defaultValue);
}
/**
* 检查配置键是否存在
*
* @param key 配置键
* @return true如果键存在,false如果不存在
*/
public boolean hasKey(String key) {
return ConfigValueUtil.hasKey(configMap, key);
}
/**
* 检查配置键是否存在且值不为null
*
* @param key 配置键
* @return true如果键存在且值不为null
*/
public boolean hasNonNullValue(String key) {
return ConfigValueUtil.hasNonNullValue(configMap, key);
}
/**
* 获取所有配置键
*
* @return 配置键集合
*/
public Set<String> getAllKeys() {
return new HashSet<>(configMap.keySet());
}
/**
* 获取配置项数量
*
* @return 配置项数量
*/
@Override
public int size() {
return configMap.size();
}
/**
* 检查配置是否为空
*
* @return true如果没有配置项
*/
public boolean isEmpty() {
return configMap.isEmpty();
}
/**
* 获取所有配置的拷贝
*
* @return 配置Map的拷贝
*/
public Map<String, Object> getAllConfigsAsMap() {
return new HashMap<>(configMap);
}
/**
* 根据键前缀过滤配置
*
* @param prefix 键前缀
* @return 匹配前缀的配置Map
*/
public Map<String, Object> getConfigsByPrefix(String prefix) {
if (prefix == null) {
return new HashMap<>();
}
return configMap.entrySet().stream()
.filter(entry -> entry.getKey() != null && entry.getKey().startsWith(prefix))
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue
));
}
/**
* 创建新的ScenicConfigManager,包含当前配置的子集
*
* @param keys 要包含的配置键
* @return 包含指定键配置的新管理器
*/
public ScenicConfigManager subset(Set<String> keys) {
Map<String, Object> subsetMap = new HashMap<>();
if (keys != null) {
for (String key : keys) {
if (configMap.containsKey(key)) {
subsetMap.put(key, configMap.get(key));
}
}
}
return new ScenicConfigManager(subsetMap);
}
/**
* 将配置转换为扁平化的Map,键名转换为驼峰形式
*
* @return 扁平化的配置Map,键为驼峰形式
*/
public Map<String, Object> toFlatConfig() {
Map<String, Object> flatConfig = new HashMap<>();
for (Map.Entry<String, Object> entry : configMap.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
if (key != null) {
String camelCaseKey = toCamelCase(key);
flatConfig.put(camelCaseKey, value);
}
}
return flatConfig;
}
/**
* 将字符串转换为驼峰形式
* 支持下划线、短横线、点号分隔的字符串转换
*
* @param str 原始字符串
* @return 驼峰形式的字符串
*/
private String toCamelCase(String str) {
if (str == null || str.isEmpty()) {
return str;
}
// 支持下划线、短横线、点号作为分隔符
String[] parts = str.split("[_\\-.]");
if (parts.length <= 1) {
return str;
}
StringBuilder camelCase = new StringBuilder();
// 第一部分保持原样(全小写)
camelCase.append(parts[0].toLowerCase());
// 后续部分首字母大写
for (int i = 1; i < parts.length; i++) {
String part = parts[i];
if (!part.isEmpty()) {
camelCase.append(Character.toUpperCase(part.charAt(0)));
if (part.length() > 1) {
camelCase.append(part.substring(1).toLowerCase());
}
}
}
return camelCase.toString();
}
@Override
public String toString() {
return "ScenicConfigManager{" +
"configCount=" + configMap.size() +
", keys=" + configMap.keySet() +
'}';
}
}

View File

@@ -30,18 +30,6 @@ public interface DeviceConfigV2Client {
CommonResponse<DeviceConfigV2DTO> getDeviceConfigByKey(@PathVariable("deviceId") Long deviceId,
@PathVariable("configKey") String configKey);
/**
* 获取设备扁平化配置
*/
@GetMapping("/{deviceId}/flat")
CommonResponse<Map<String, Object>> getDeviceFlatConfig(@PathVariable("deviceId") Long deviceId);
/**
* 根据设备编号获取设备扁平化配置
*/
@GetMapping("/no/{no}/flat")
CommonResponse<Map<String, Object>> getDeviceFlatConfigByNo(@PathVariable("no") String no);
/**
* 创建设备配置
*/

View File

@@ -21,18 +21,6 @@ public interface DeviceV2Client {
@GetMapping("/no/{no}")
CommonResponse<DeviceV2DTO> getDeviceByNo(@PathVariable("no") String no);
/**
* 获取设备详细信息(含配置)
*/
@GetMapping("/{id}/with-config")
CommonResponse<DeviceV2WithConfigDTO> getDeviceWithConfig(@PathVariable("id") Long id);
/**
* 根据设备编号获取设备详细信息(含配置)
*/
@GetMapping("/no/{no}/with-config")
CommonResponse<DeviceV2WithConfigDTO> getDeviceByNoWithConfig(@PathVariable("no") String no);
/**
* 创建设备
*/
@@ -65,19 +53,6 @@ public interface DeviceV2Client {
@RequestParam(value = "isActive", required = false) Integer isActive,
@RequestParam(value = "scenicId", required = false) Long scenicId);
/**
* 分页获取设备列表(含配置)
*/
@GetMapping("/with-config")
CommonResponse<PageResponse<DeviceV2WithConfigDTO>> listDevicesWithConfig(
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize,
@RequestParam(value = "name", required = false) String name,
@RequestParam(value = "no", required = false) String no,
@RequestParam(value = "type", required = false) String type,
@RequestParam(value = "isActive", required = false) Integer isActive,
@RequestParam(value = "scenicId", required = false) Long scenicId);
/**
* 根据配置条件筛选设备
*/

View File

@@ -1,275 +0,0 @@
package com.ycwl.basic.integration.device.example;
import com.ycwl.basic.integration.common.response.PageResponse;
import com.ycwl.basic.integration.device.dto.defaults.*;
import com.ycwl.basic.integration.device.service.DeviceDefaultConfigIntegrationService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
/**
* 默认配置集成服务使用示例
*
* 通过在 application.yml 中设置 integration.device.example.default-config.enabled=true 来启用
*/
@Slf4j
@Component
@RequiredArgsConstructor
@ConditionalOnProperty(name = "integration.device.example.default-config.enabled", havingValue = "true")
public class DefaultConfigIntegrationExample implements CommandLineRunner {
private final DeviceDefaultConfigIntegrationService defaultConfigService;
@Override
public void run(String... args) throws Exception {
log.info("=== 默认配置集成服务使用示例 ===");
try {
// 1. 基础查询操作示例(支持自动 Fallback)
basicQueryExamples();
// 2. 配置管理操作示例(直接操作)
configManagementExamples();
// 3. 批量操作示例
batchOperationExamples();
// 4. 高级使用模式示例
advancedUsageExamples();
} catch (Exception e) {
log.error("默认配置集成示例执行失败", e);
}
}
/**
* 基础查询操作示例(支持自动 Fallback)
*/
private void basicQueryExamples() {
log.info("--- 基础查询操作示例(支持自动 Fallback)---");
try {
// 获取默认配置列表(自动缓存,服务不可用时返回缓存数据)
PageResponse<DefaultConfigResponse> configList = defaultConfigService.listDefaultConfigs(1, 10);
log.info("默认配置列表: 总数={}, 当前页配置数={}",
configList.getTotal(), configList.getList().size());
// 显示配置详情
for (DefaultConfigResponse config : configList.getList()) {
log.info("配置详情: key={}, value={}, type={}, description={}",
config.getConfigKey(), config.getConfigValue(),
config.getConfigType(), config.getDescription());
// 获取单个配置详情(自动缓存,服务不可用时返回缓存数据)
DefaultConfigResponse detailConfig = defaultConfigService.getDefaultConfig(config.getConfigKey());
if (detailConfig != null) {
log.info("配置详情获取成功: usageCount={}, isActive={}",
detailConfig.getUsageCount(), detailConfig.getIsActive());
}
break; // 只展示第一个
}
} catch (Exception e) {
log.error("基础查询操作失败", e);
}
}
/**
* 配置管理操作示例(直接操作)
*/
private void configManagementExamples() {
log.info("--- 配置管理操作示例(直接操作)---");
String testConfigKey = "example_test_config";
try {
// 1. 创建默认配置(直接操作,失败时立即报错)
DefaultConfigRequest createRequest = new DefaultConfigRequest();
createRequest.setConfigKey(testConfigKey);
createRequest.setConfigValue("1920x1080");
createRequest.setConfigType("string");
createRequest.setDescription("示例测试配置 - 默认分辨率");
boolean createResult = defaultConfigService.createDefaultConfig(createRequest);
log.info("创建默认配置结果: {}", createResult ? "成功" : "失败");
// 2. 更新默认配置(直接操作,可能返回冲突信息)
Map<String, Object> updates = new HashMap<>();
updates.put("configValue", "3840x2160");
updates.put("description", "更新后的默认分辨率 - 4K");
DefaultConfigConflict conflict = defaultConfigService.updateDefaultConfig(testConfigKey, updates);
if (conflict != null) {
log.warn("更新配置存在冲突: configKey={}, conflictType={}, deviceCount={}",
conflict.getConfigKey(), conflict.getConflictType(), conflict.getDeviceCount());
} else {
log.info("配置更新成功,无冲突");
}
// 3. 验证更新结果
DefaultConfigResponse updatedConfig = defaultConfigService.getDefaultConfig(testConfigKey);
if (updatedConfig != null) {
log.info("更新后配置值: {}", updatedConfig.getConfigValue());
}
// 4. 删除测试配置(直接操作)
boolean deleteResult = defaultConfigService.deleteDefaultConfig(testConfigKey);
log.info("删除默认配置结果: {}", deleteResult ? "成功" : "失败");
} catch (Exception e) {
log.error("配置管理操作失败", e);
}
}
/**
* 批量操作示例
*/
private void batchOperationExamples() {
log.info("--- 批量操作示例 ---");
try {
// 1. 使用构建器模式创建批量配置
BatchDefaultConfigRequest batchRequest = defaultConfigService.createBatchConfigBuilder()
.addVideoConfig("1920x1080", 30, "H264") // 添加视频配置组
.addNetworkConfig("192.168.1.100", 554, "RTSP") // 添加网络配置组
.addConfig("recording_enabled", "true", "bool", "是否启用录制")
.addConfig("storage_path", "/data/recordings", "string", "录制存储路径")
.addConfig("max_file_size", "1024", "int", "最大文件大小(MB)")
.build();
log.info("准备批量创建 {} 个默认配置", batchRequest.getConfigs().size());
// 2. 执行批量更新(直接操作)
BatchDefaultConfigResponse batchResult = defaultConfigService.batchUpdateDefaultConfigs(batchRequest);
// 3. 处理批量结果
log.info("批量操作结果: 成功={}, 失败={}", batchResult.getSuccess(), batchResult.getFailed());
if (batchResult.getConflicts() != null && !batchResult.getConflicts().isEmpty()) {
log.warn("发现 {} 个配置冲突:", batchResult.getConflicts().size());
for (DefaultConfigConflict conflict : batchResult.getConflicts()) {
log.warn("冲突配置: key={}, type={}, deviceCount={}",
conflict.getConfigKey(), conflict.getConflictType(), conflict.getDeviceCount());
}
}
if (batchResult.getProcessedItems() != null) {
log.info("处理详情:");
batchResult.getProcessedItems().forEach(item ->
log.info(" 配置 {}: status={}, action={}, finalType={}",
item.getConfigKey(), item.getStatus(), item.getAction(), item.getFinalType())
);
}
if (batchResult.getErrors() != null && !batchResult.getErrors().isEmpty()) {
log.error("批量操作错误:");
batchResult.getErrors().forEach(error -> log.error(" {}", error));
}
} catch (Exception e) {
log.error("批量操作失败", e);
}
}
/**
* 高级使用模式示例
*/
private void advancedUsageExamples() {
log.info("--- 高级使用模式示例 ---");
try {
// 1. 设备类型特定的默认配置模式
createDeviceTypeSpecificConfigs();
// 2. 配置验证和完整性检查模式
validateConfigCompleteness();
// 3. 配置迁移和批量更新模式
configMigrationPattern();
} catch (Exception e) {
log.error("高级使用模式示例失败", e);
}
}
/**
* 创建设备类型特定的默认配置
*/
private void createDeviceTypeSpecificConfigs() {
log.info("创建设备类型特定的默认配置...");
// IPC摄像头默认配置
BatchDefaultConfigRequest ipcDefaults = defaultConfigService.createBatchConfigBuilder()
.addVideoConfig("1920x1080", 25, "H264")
.addConfig("night_vision", "true", "bool", "夜视功能")
.addConfig("motion_detection", "true", "bool", "移动检测")
.addConfig("stream_profile", "main", "string", "码流类型")
.build();
// NVR设备默认配置
BatchDefaultConfigRequest nvrDefaults = defaultConfigService.createBatchConfigBuilder()
.addConfig("max_channels", "16", "int", "最大通道数")
.addConfig("storage_mode", "continuous", "string", "存储模式")
.addConfig("backup_enabled", "true", "bool", "备份启用")
.build();
log.info("IPC默认配置项数: {}, NVR默认配置项数: {}",
ipcDefaults.getConfigs().size(), nvrDefaults.getConfigs().size());
}
/**
* 配置验证和完整性检查
*/
private void validateConfigCompleteness() {
log.info("验证配置完整性...");
// 获取所有默认配置
PageResponse<DefaultConfigResponse> allConfigs = defaultConfigService.listDefaultConfigs(1, 100);
// 检查必需的基础配置是否存在
String[] requiredConfigs = {"resolution", "frameRate", "codec", "protocol"};
for (String requiredConfig : requiredConfigs) {
boolean exists = allConfigs.getList().stream()
.anyMatch(config -> requiredConfig.equals(config.getConfigKey()));
log.info("必需配置 {} 存在: {}", requiredConfig, exists ? "" : "");
}
// 统计配置类型分布
Map<String, Long> typeDistribution = new HashMap<>();
allConfigs.getList().forEach(config ->
typeDistribution.merge(config.getConfigType(), 1L, Long::sum)
);
log.info("配置类型分布: {}", typeDistribution);
}
/**
* 配置迁移和批量更新模式
*/
private void configMigrationPattern() {
log.info("配置迁移模式示例...");
try {
// 1. 获取需要升级的配置
PageResponse<DefaultConfigResponse> oldConfigs = defaultConfigService.listDefaultConfigs(1, 50);
// 2. 创建升级配置批次
BatchDefaultConfigRequest upgradeRequest = defaultConfigService.createBatchConfigBuilder()
.addConfig("api_version", "v2", "string", "API版本")
.addConfig("security_mode", "enhanced", "string", "安全模式")
.build();
// 3. 执行批量升级
BatchDefaultConfigResponse upgradeResult = defaultConfigService.batchUpdateDefaultConfigs(upgradeRequest);
log.info("配置升级结果: 成功={}, 失败={}", upgradeResult.getSuccess(), upgradeResult.getFailed());
} catch (Exception e) {
log.warn("配置迁移示例执行失败(这是正常的,因为是示例代码)", e);
}
}
}

View File

@@ -1,190 +0,0 @@
package com.ycwl.basic.integration.device.example;
import com.ycwl.basic.integration.device.dto.device.*;
import com.ycwl.basic.integration.device.service.DeviceIntegrationService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 设备配置筛选功能使用示例
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class DeviceFilterExample {
private final DeviceIntegrationService deviceIntegrationService;
/**
* 示例1: 查找高质量户外IPC设备
* 条件: 分辨率1920x1080,帧率>=30,位置包含"outdoor"
*/
public void findHighQualityOutdoorDevices() {
log.info("=== 查找高质量户外IPC设备 ===");
List<ConfigFilter> configFilters = Arrays.asList(
new ConfigFilter("resolution", "1920x1080", "eq"),
new ConfigFilter("framerate", 30, "gte"),
new ConfigFilter("location", "outdoor", "like")
);
Map<String, Object> deviceFilters = new HashMap<>();
deviceFilters.put("type", "IPC");
deviceFilters.put("isActive", 1);
FilterDevicesByConfigsRequest request = new FilterDevicesByConfigsRequest();
request.setPage(1);
request.setPageSize(20);
request.setConfigFilters(configFilters);
request.setFilterLogic("AND");
request.setDeviceFilters(deviceFilters);
try {
FilterDevicesByConfigsResponse response = deviceIntegrationService.filterDevicesByConfigs(request);
log.info("找到 {} 个高质量户外IPC设备", response.getTotal());
response.getList().forEach(device -> {
log.info("设备: {} ({}), 配置: {}", device.getName(), device.getNo(), device.getConfig());
});
} catch (Exception e) {
log.error("查找高质量户外IPC设备失败", e);
}
}
/**
* 示例2: 查找缺少关键配置的设备
* 条件: 缺少备份服务器或夜视功能配置
*/
public void findDevicesWithMissingConfigs() {
log.info("=== 查找缺少关键配置的设备 ===");
List<ConfigFilter> configFilters = Arrays.asList(
new ConfigFilter("backup_server", null, "is_null"),
new ConfigFilter("night_vision", null, "is_null")
);
FilterDevicesByConfigsRequest request = new FilterDevicesByConfigsRequest();
request.setConfigFilters(configFilters);
request.setFilterLogic("OR"); // 缺少任一配置即返回
request.setPageSize(50);
try {
FilterDevicesByConfigsResponse response = deviceIntegrationService.filterDevicesByConfigs(request);
log.info("找到 {} 个缺少关键配置的设备", response.getTotal());
response.getList().forEach(device -> {
Map<String, Object> config = device.getConfig();
boolean missingBackup = !config.containsKey("backup_server") || config.get("backup_server") == null;
boolean missingNightVision = !config.containsKey("night_vision") || config.get("night_vision") == null;
log.info("设备: {} ({}), 缺少配置: {}{}",
device.getName(), device.getNo(),
missingBackup ? "备份服务器 " : "",
missingNightVision ? "夜视功能 " : "");
});
} catch (Exception e) {
log.error("查找缺少关键配置的设备失败", e);
}
}
/**
* 示例3: 根据多个位置查找设备
* 条件: 位置在指定列表中
*/
public void findDevicesByMultipleLocations() {
log.info("=== 根据多个位置查找设备 ===");
List<String> locations = Arrays.asList("outdoor", "indoor", "parking");
List<ConfigFilter> configFilters = Arrays.asList(
new ConfigFilter("location", locations, "in")
);
FilterDevicesByConfigsRequest request = new FilterDevicesByConfigsRequest();
request.setConfigFilters(configFilters);
request.setPageSize(100);
try {
FilterDevicesByConfigsResponse response = deviceIntegrationService.filterDevicesByConfigs(request);
log.info("找到 {} 个设备在指定位置", response.getTotal());
response.getList().forEach(device -> {
Object location = device.getConfig().get("location");
log.info("设备: {} ({}), 位置: {}", device.getName(), device.getNo(), location);
});
} catch (Exception e) {
log.error("根据多个位置查找设备失败", e);
}
}
/**
* 示例4: 使用便捷方法查找设备
*/
public void useConvenienceMethods() {
log.info("=== 使用便捷方法查找设备 ===");
try {
// 查找缺少配置的设备
FilterDevicesByConfigsResponse response4 = deviceIntegrationService
.findDevicesWithMissingConfig("firmware_version", 1, 10);
log.info("缺少固件版本配置的设备数: {}", response4.getTotal());
} catch (Exception e) {
log.error("使用便捷方法查找设备失败", e);
}
}
/**
* 示例5: 性能监控查询
* 条件: CPU使用率>80% 或 内存使用率>85%
*/
public void findHighLoadDevices() {
log.info("=== 查找高负载设备 ===");
List<ConfigFilter> configFilters = Arrays.asList(
new ConfigFilter("cpu_usage", 80, "gt"),
new ConfigFilter("memory_usage", 85, "gt")
);
FilterDevicesByConfigsRequest request = new FilterDevicesByConfigsRequest();
request.setConfigFilters(configFilters);
request.setFilterLogic("OR");
request.setPageSize(50);
try {
FilterDevicesByConfigsResponse response = deviceIntegrationService.filterDevicesByConfigs(request);
log.info("找到 {} 个高负载设备", response.getTotal());
response.getList().forEach(device -> {
Map<String, Object> config = device.getConfig();
Object cpuUsage = config.get("cpu_usage");
Object memoryUsage = config.get("memory_usage");
log.info("高负载设备: {} ({}), CPU: {}%, 内存: {}%",
device.getName(), device.getNo(), cpuUsage, memoryUsage);
});
} catch (Exception e) {
log.error("查找高负载设备失败", e);
}
}
/**
* 运行所有示例
*/
public void runAllExamples() {
log.info("开始运行设备配置筛选示例...");
findHighQualityOutdoorDevices();
findDevicesWithMissingConfigs();
findDevicesByMultipleLocations();
useConvenienceMethods();
findHighLoadDevices();
log.info("所有示例运行完成");
}
}

View File

@@ -1,185 +0,0 @@
package com.ycwl.basic.integration.device.example;
import com.ycwl.basic.integration.common.response.PageResponse;
import com.ycwl.basic.integration.device.dto.device.*;
import com.ycwl.basic.integration.device.dto.config.*;
import com.ycwl.basic.integration.device.service.DeviceConfigIntegrationService;
import com.ycwl.basic.integration.device.service.DeviceIntegrationService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
/**
* Device Integration 使用示例
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class DeviceIntegrationExample {
private final DeviceIntegrationService deviceService;
private final DeviceConfigIntegrationService deviceConfigService;
/**
* 基本设备操作
*/
public void basicDeviceOperations() {
log.info("=== 基本设备操作 ===");
// 创建IPC摄像头设备(默认排序)
DeviceV2DTO ipcDevice = deviceService.createIpcDevice(
"前门摄像头", "CAM001", 1001L);
log.info("创建IPC设备: {}, 排序值: {}", ipcDevice.getName(), ipcDevice.getSort());
// 根据ID获取设备信息
DeviceV2DTO device = deviceService.getDevice(ipcDevice.getId());
log.info("获取设备信息: {}, 排序值: {}", device.getName(), device.getSort());
// 根据设备编号获取设备信息
DeviceV2DTO deviceByNo = deviceService.getDeviceByNo("CAM001");
log.info("根据编号获取设备: {}", deviceByNo.getName());
// 获取设备详细信息(含配置)
DeviceV2WithConfigDTO deviceWithConfig = deviceService.getDeviceWithConfig(ipcDevice.getId());
log.info("获取设备配置: {}", deviceWithConfig.getName());
// 分页查询景区设备列表
PageResponse<DeviceV2DTO> deviceList = deviceService.getScenicIpcDevices(1001L, 1, 10);
log.info("景区设备列表: 总数={}", deviceList.getTotal());
// 启用设备
deviceService.enableDevice(ipcDevice.getId());
log.info("设备已启用");
}
/**
* 设备排序功能演示
*/
public void deviceSortingOperations() {
log.info("=== 设备排序功能演示 ===");
Long scenicId = 1001L;
// 创建带排序的设备
DeviceV2DTO camera1 = deviceService.createIpcDeviceWithSort(
"大门摄像头", "CAM_GATE", scenicId, 10);
log.info("创建摄像头1: {}, 排序: {}", camera1.getName(), camera1.getSort());
DeviceV2DTO camera2 = deviceService.createIpcDeviceWithSort(
"后门摄像头", "CAM_BACK", scenicId, 20);
log.info("创建摄像头2: {}, 排序: {}", camera2.getName(), camera2.getSort());
DeviceV2DTO sensor1 = deviceService.createCustomDeviceWithSort(
"温度传感器", "TEMP_01", scenicId, 5);
log.info("创建传感器: {}, 排序: {}", sensor1.getName(), sensor1.getSort());
// 更新设备排序
deviceService.updateDeviceSort(camera1.getId(), 1);
log.info("更新摄像头1排序为1(置顶)");
// 获取排序后的设备列表
PageResponse<DeviceV2DTO> sortedList = deviceService.listDevices(1, 10, null, null, null, 1, scenicId);
log.info("排序后的设备列表:");
for (DeviceV2DTO device : sortedList.getList()) {
log.info(" - {}: 排序={}, 类型={}", device.getName(), device.getSort(), device.getType());
}
// 批量调整排序演示
log.info("--- 批量调整排序演示 ---");
deviceService.updateDeviceSort(sensor1.getId(), 15); // 传感器排到中间
deviceService.updateDeviceSort(camera2.getId(), 30); // 后门摄像头排到最后
log.info("批量排序调整完成");
}
/**
* 设备配置管理
*/
public void deviceConfigurationOperations() {
log.info("=== 设备配置管理 ===");
Long deviceId = 1L;
// 获取设备所有配置
List<DeviceConfigV2DTO> configs = deviceConfigService.getDeviceConfigs(deviceId);
log.info("设备配置数量: {}", configs.size());
// 获取扁平化配置
Map<String, Object> flatConfig = deviceConfigService.getDeviceFlatConfig(deviceId);
log.info("扁平化配置项数: {}", flatConfig.size());
// 使用批量配置API
BatchDeviceConfigRequest builderRequest = deviceConfigService.createBatchConfigBuilder()
.build();
BatchUpdateResponse result = deviceConfigService.batchUpdateDeviceConfig(deviceId, builderRequest);
log.info("批量配置更新结果: 成功={}, 失败={}", result.getSuccess(), result.getFailed());
}
/**
* 排序最佳实践演示
*/
public void sortingBestPractices() {
log.info("=== 排序最佳实践演示 ===");
Long scenicId = 1001L;
// 推荐使用10的倍数作为排序值
DeviceV2DTO device1 = deviceService.createIpcDeviceWithSort(
"重要摄像头", "CAM_IMPORTANT", scenicId, 10);
DeviceV2DTO device2 = deviceService.createIpcDeviceWithSort(
"普通摄像头", "CAM_NORMAL", scenicId, 20);
DeviceV2DTO device3 = deviceService.createIpcDeviceWithSort(
"备用摄像头", "CAM_BACKUP", scenicId, 30);
log.info("使用10的倍数创建设备排序: 10, 20, 30");
// 在中间插入新设备
DeviceV2DTO insertDevice = deviceService.createIpcDeviceWithSort(
"中间摄像头", "CAM_MIDDLE", scenicId, 25);
log.info("在20和30之间插入设备,排序值: 25");
// 置顶操作
deviceService.updateDeviceSort(device2.getId(), 1);
log.info("将普通摄像头置顶(排序值: 1)");
// 查看最终排序结果
PageResponse<DeviceV2DTO> finalList = deviceService.listDevices(1, 10, null, null, null, 1, scenicId);
log.info("最终排序结果:");
for (DeviceV2DTO device : finalList.getList()) {
log.info(" - {}: 排序={}", device.getName(), device.getSort());
}
}
/**
* 运行所有示例
*/
public void runAllExamples() {
try {
basicDeviceOperations();
deviceSortingOperations();
sortingBestPractices();
deviceConfigurationOperations();
log.info("=== 所有示例执行完成 ===");
} catch (Exception e) {
log.error("示例执行过程中发生错误", e);
}
}
/**
* 运行基础示例(简化版)
*/
public void runBasicExamples() {
try {
basicDeviceOperations();
deviceConfigurationOperations();
log.info("=== 基础示例执行完成 ===");
} catch (Exception e) {
log.error("示例执行过程中发生错误", e);
}
}
}

View File

@@ -1,124 +0,0 @@
package com.ycwl.basic.integration.device.example;
import com.ycwl.basic.integration.common.service.IntegrationFallbackService;
import com.ycwl.basic.integration.device.service.DeviceIntegrationService;
import com.ycwl.basic.integration.device.service.DeviceConfigIntegrationService;
import com.ycwl.basic.integration.device.dto.device.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* 设备集成示例(包含降级机制)
* 演示设备集成和失败降级策略的使用
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class DeviceIntegrationFallbackExample {
private final DeviceIntegrationService deviceService;
private final DeviceConfigIntegrationService configService;
private final IntegrationFallbackService fallbackService;
private static final String SERVICE_NAME = "zt-device";
/**
* 演示设备信息获取的降级机制
*/
public void deviceInfoFallbackExample() {
log.info("=== 设备信息获取降级示例 ===");
Long deviceId = 1001L;
String deviceNo = "CAM001";
try {
// 获取设备信息 - 自动降级
DeviceV2DTO device = deviceService.getDevice(deviceId);
log.info("获取设备成功: {}", device.getName());
// 根据设备号获取设备 - 自动降级
DeviceV2DTO deviceByNo = deviceService.getDeviceByNo(deviceNo);
log.info("根据设备号获取设备成功: {}", deviceByNo.getName());
// 获取设备配置 - 自动降级
DeviceV2WithConfigDTO deviceWithConfig = deviceService.getDeviceWithConfig(deviceId);
log.info("获取设备配置成功,配置数量: {}", deviceWithConfig.getConfig().size());
} catch (Exception e) {
log.error("所有降级策略失败", e);
}
}
/**
* 演示设备操作(无降级机制)
*/
public void deviceOperationExample() {
log.info("=== 设备操作示例 ===");
Long deviceId = 1001L;
try {
// 设备更新操作 - 直接操作,失败时抛出异常
UpdateDeviceRequest updateRequest = new UpdateDeviceRequest();
updateRequest.setName("更新后的摄像头");
deviceService.updateDevice(deviceId, updateRequest);
log.info("设备更新操作完成");
// 设备排序更新 - 直接操作,失败时抛出异常
deviceService.updateDeviceSort(deviceId, 5);
log.info("设备排序更新完成");
} catch (Exception e) {
log.error("设备操作失败", e);
}
}
/**
* 演示降级缓存管理
*/
public void fallbackCacheManagementExample() {
log.info("=== 降级缓存管理示例 ===");
String deviceCacheKey = "device:1001";
String configCacheKey = "device:flat:config:1001";
// 检查降级缓存状态
boolean hasDeviceCache = fallbackService.hasFallbackCache(SERVICE_NAME, deviceCacheKey);
boolean hasConfigCache = fallbackService.hasFallbackCache(SERVICE_NAME, configCacheKey);
log.info("设备降级缓存存在: {}", hasDeviceCache);
log.info("配置降级缓存存在: {}", hasConfigCache);
// 清理特定的降级缓存
if (hasDeviceCache) {
fallbackService.clearFallbackCache(SERVICE_NAME, deviceCacheKey);
log.info("已清理设备降级缓存");
}
// 获取降级缓存统计信息
IntegrationFallbackService.FallbackCacheStats stats = fallbackService.getFallbackCacheStats(SERVICE_NAME);
log.info("设备服务降级缓存统计: {}", stats);
// 批量清理所有设备降级缓存
if (stats.getTotalCacheCount() > 10) {
fallbackService.clearAllFallbackCache(SERVICE_NAME);
log.info("已批量清理所有设备降级缓存");
}
}
/**
* 运行所有示例
*/
public void runAllExamples() {
log.info("开始运行设备集成示例...");
deviceInfoFallbackExample();
deviceOperationExample();
fallbackCacheManagementExample();
log.info("设备集成示例运行完成");
}
}

View File

@@ -39,31 +39,6 @@ public class DeviceConfigIntegrationService {
return handleResponse(response, "根据键获取设备配置失败");
}
public Map<String, Object> getDeviceFlatConfig(Long deviceId) {
return fallbackService.executeWithFallback(
SERVICE_NAME,
"device:flat:config:" + deviceId,
() -> {
CommonResponse<Map<String, Object>> response = deviceConfigV2Client.getDeviceFlatConfig(deviceId);
return handleResponse(response, "获取设备扁平化配置失败");
},
Map.class
);
}
public Map<String, Object> getDeviceFlatConfigByNo(String deviceNo) {
log.debug("根据设备编号获取扁平化配置, deviceNo: {}", deviceNo);
return fallbackService.executeWithFallback(
SERVICE_NAME,
"device:flat:config:no:" + deviceNo,
() -> {
CommonResponse<Map<String, Object>> response = deviceConfigV2Client.getDeviceFlatConfigByNo(deviceNo);
return handleResponse(response, "根据设备编号获取扁平化配置失败");
},
Map.class
);
}
public DeviceConfigV2DTO createDeviceConfig(Long deviceId, CreateDeviceConfigRequest request) {
log.debug("创建设备配置, deviceId: {}, configKey: {}", deviceId, request.getConfigKey());
CommonResponse<DeviceConfigV2DTO> response = deviceConfigV2Client.createDeviceConfig(deviceId, request);

View File

@@ -48,32 +48,6 @@ public class DeviceIntegrationService {
);
}
public DeviceV2WithConfigDTO getDeviceWithConfig(Long deviceId) {
log.debug("获取设备配置信息, deviceId: {}", deviceId);
return fallbackService.executeWithFallback(
SERVICE_NAME,
"device:config:" + deviceId,
() -> {
CommonResponse<DeviceV2WithConfigDTO> response = deviceV2Client.getDeviceWithConfig(deviceId);
return handleResponse(response, "获取设备配置信息失败");
},
DeviceV2WithConfigDTO.class
);
}
public DeviceV2WithConfigDTO getDeviceWithConfigByNo(String deviceNo) {
log.debug("根据设备编号获取设备配置信息, deviceNo: {}", deviceNo);
return fallbackService.executeWithFallback(
SERVICE_NAME,
"device:config:no:" + deviceNo,
() -> {
CommonResponse<DeviceV2WithConfigDTO> response = deviceV2Client.getDeviceByNoWithConfig(deviceNo);
return handleResponse(response, "根据设备编号获取设备配置信息失败");
},
DeviceV2WithConfigDTO.class
);
}
public DeviceV2DTO createDevice(CreateDeviceRequest request) {
log.debug("创建设备, name: {}, no: {}, type: {}", request.getName(), request.getNo(), request.getType());
CommonResponse<DeviceV2DTO> response = deviceV2Client.createDevice(request);
@@ -101,15 +75,6 @@ public class DeviceIntegrationService {
return handleResponse(response, "分页查询设备列表失败");
}
public PageResponse<DeviceV2WithConfigDTO> listDevicesWithConfig(Integer page, Integer pageSize, String name, String no,
String type, Integer isActive, Long scenicId) {
log.debug("分页查询设备带配置列表, page: {}, pageSize: {}, name: {}, no: {}, type: {}, isActive: {}, scenicId: {}",
page, pageSize, name, no, type, isActive, scenicId);
CommonResponse<PageResponse<DeviceV2WithConfigDTO>> response = deviceV2Client.listDevicesWithConfig(
page, pageSize, name, no, type, isActive, scenicId);
return handleResponse(response, "分页查询设备带配置列表失败");
}
/**
* 创建IPC摄像头设备
*/

View File

@@ -0,0 +1,49 @@
package com.ycwl.basic.integration.kafka.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* 人脸识别异步处理线程池配置
*/
@Slf4j
@Configuration
@ConditionalOnProperty(name = "kafka.enabled", havingValue = "true")
public class FaceRecognitionThreadPoolConfig {
/**
* 创建人脸识别专用线程池
* - 核心线程数:32
* - 最大线程数:128
* - 队列容量:1000(避免无限制增长)
* - 拒绝策略:CallerRunsPolicy(调用者线程执行)
*/
@Bean(name = "faceRecognitionExecutor", destroyMethod = "shutdown")
public ThreadPoolExecutor faceRecognitionExecutor() {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
32, // 核心线程数
128, // 最大线程数
60L, // 空闲线程存活时间
TimeUnit.SECONDS, // 时间单位
new LinkedBlockingQueue<>(1000), // 任务队列
r -> {
Thread thread = new Thread(r);
thread.setName("face-recognition-" + thread.getId());
thread.setDaemon(false);
return thread;
},
new ThreadPoolExecutor.CallerRunsPolicy() // 超过容量时由调用者线程执行
);
log.info("人脸识别线程池初始化完成 - 核心线程数: {}, 最大线程数: {}, 队列容量: 1000",
executor.getCorePoolSize(), executor.getMaximumPoolSize());
return executor;
}
}

View File

@@ -11,6 +11,7 @@ public class KafkaIntegrationProperties {
private boolean enabled = false;
private String bootstrapServers = "100.64.0.12:39092";
private String ztMessageTopic = "zt-message"; // topic for zt-message microservice
private Consumer consumer = new Consumer();
private Producer producer = new Producer();

View File

@@ -6,7 +6,7 @@ import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.Date;
/**
* zt-face topic消息结构
@@ -44,7 +44,7 @@ public class FaceProcessingMessage {
* 拍摄时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime shotTime;
private Date shotTime;
// status字段已移除,由系统内部管理状态
@@ -54,7 +54,7 @@ public class FaceProcessingMessage {
* 消息创建时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
private Date createTime;
/**
* 消息来源
@@ -69,14 +69,14 @@ public class FaceProcessingMessage {
* 创建人脸处理消息的工厂方法(使用外部传入的faceId)
*/
public static FaceProcessingMessage create(Long externalFaceId, Long scenicId, Long deviceId,
String faceUrl, LocalDateTime shotTime) {
String faceUrl, Date shotTime) {
return FaceProcessingMessage.builder()
.faceSampleId(externalFaceId) // 使用外部传入的ID作为唯一标识
.scenicId(scenicId)
.deviceId(deviceId)
.faceUrl(faceUrl)
.shotTime(shotTime)
.createTime(LocalDateTime.now())
.createTime(new Date())
.source("external-system")
.build();
}

View File

@@ -1,57 +0,0 @@
package com.ycwl.basic.integration.kafka.example;
import com.ycwl.basic.integration.kafka.dto.KafkaMessage;
import com.ycwl.basic.integration.kafka.service.KafkaIntegrationService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
/**
* Kafka集成使用示例(暂时注释,后续开发时启用)
*/
@Slf4j
@Component
@RequiredArgsConstructor
@ConditionalOnProperty(name = "kafka.example.enabled", havingValue = "true", matchIfMissing = false)
public class KafkaIntegrationExample {
private final KafkaIntegrationService kafkaService;
/**
* 演示Kafka连接测试
*/
public void demonstrateConnectionTest() {
log.info("=== Kafka Integration Example ===");
// 测试连接
boolean connected = kafkaService.testConnection();
log.info("Kafka connection status: {}", connected ? "SUCCESS" : "FAILED");
// 显示配置信息
var properties = kafkaService.getKafkaProperties();
log.info("Kafka Bootstrap Servers: {}", properties.getBootstrapServers());
log.info("Consumer Group ID: {}", properties.getConsumer().getGroupId());
}
/**
* 演示消息发送(预留示例)
*/
public void demonstrateMessageSending() {
log.info("=== Message Sending Example (Not Implemented) ===");
// 创建示例消息
KafkaMessage<String> message = KafkaMessage.of(
"test-topic",
"TEST_EVENT",
"Hello Kafka from liuying-microservice!"
);
// 发送消息(暂不实现)
kafkaService.sendMessage("test-topic", "test-key", message);
log.info("Message sending demonstration completed");
}
// TODO: 后续添加消费者示例
// public void demonstrateMessageConsuming() { ... }
}

View File

@@ -17,10 +17,12 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.support.Acknowledgment;
import org.springframework.stereotype.Service;
import java.time.ZoneId;
import java.util.Date;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 人脸处理Kafka消费服务
@@ -38,36 +40,48 @@ public class FaceProcessingKafkaService {
private final TaskFaceService taskFaceService;
private final ScenicService scenicService;
private final DeviceRepository deviceRepository;
private final ThreadPoolExecutor faceRecognitionExecutor;
/**
* 消费外部系统发送的人脸处理消息
* 先保存人脸样本数据,再进行异步人脸识别处理
*/
@KafkaListener(topics = ZT_FACE_TOPIC)
public void processFaceMessage(String message) {
@KafkaListener(topics = ZT_FACE_TOPIC, containerFactory = "manualCommitKafkaListenerContainerFactory")
public void processFaceMessage(String message, Acknowledgment ack) {
try {
FaceProcessingMessage faceMessage = JacksonUtil.parseObject(message, FaceProcessingMessage.class);
log.info("接收到外部人脸处理消息, scenicId: {}, deviceId: {}, faceUrl: {}",
log.debug("接收到外部人脸处理消息, scenicId: {}, deviceId: {}, faceUrl: {}",
faceMessage.getScenicId(), faceMessage.getDeviceId(), faceMessage.getFaceUrl());
// 使用外部传入的faceSampleId
Long externalFaceId = faceMessage.getFaceSampleId();
if (externalFaceId == null) {
log.error("外部消息中未包含faceSampleId");
// 即使消息格式错误,也消费消息避免重复处理
ack.acknowledge();
return;
}
// 先保存人脸样本数据
boolean saved = saveFaceSample(faceMessage, externalFaceId);
// 然后进行人脸识别处理
// 然后异步进行人脸识别处理(使用专用线程池)
if (saved) {
processFaceRecognition(faceMessage);
faceRecognitionExecutor.execute(() -> processFaceRecognitionAsync(faceMessage));
log.debug("人脸识别任务已提交至线程池, faceSampleId: {}, 活跃线程: {}, 队列大小: {}",
externalFaceId, faceRecognitionExecutor.getActiveCount(),
faceRecognitionExecutor.getQueue().size());
} else {
log.warn("人脸样本保存失败,但消息仍将被消费, faceSampleId: {}", externalFaceId);
}
// 无论处理是否成功,都消费消息
ack.acknowledge();
} catch (Exception e) {
log.error("处理外部人脸消息失败: {}", e.getMessage(), e);
// TODO: 考虑错误重试机制或死信队列
// 即使发生异常也消费消息,避免消息堆积
ack.acknowledge();
}
}
@@ -88,7 +102,7 @@ public class FaceProcessingKafkaService {
// 转换时间格式
if (faceMessage.getShotTime() != null) {
Date shotTime = Date.from(faceMessage.getShotTime().atZone(ZoneId.systemDefault()).toInstant());
Date shotTime = faceMessage.getShotTime();
faceSample.setCreateAt(shotTime);
} else {
faceSample.setCreateAt(new Date());
@@ -96,8 +110,6 @@ public class FaceProcessingKafkaService {
// 保存到数据库
faceSampleMapper.add(faceSample);
log.info("人脸样本数据已保存, 使用外部faceId: {}, scenicId: {}, deviceId: {}, faceUrl: {}",
externalFaceId, faceMessage.getScenicId(), faceMessage.getDeviceId(), faceMessage.getFaceUrl());
return true;
} catch (Exception e) {
@@ -108,10 +120,10 @@ public class FaceProcessingKafkaService {
}
/**
* 执行人脸识别处理逻辑
* 异步执行人脸识别处理逻辑
* 对已保存的人脸样本进行识别处理
*/
private void processFaceRecognition(FaceProcessingMessage message) {
private void processFaceRecognitionAsync(FaceProcessingMessage message) {
Long faceSampleId = message.getFaceSampleId();
Long scenicId = message.getScenicId();
String faceUrl = message.getFaceUrl();
@@ -146,8 +158,7 @@ public class FaceProcessingKafkaService {
// 更新人脸样本得分和状态
faceSampleMapper.updateScore(faceSampleId, addFaceResp.getScore());
updateFaceSampleStatus(faceSampleId, 2);
log.info("人脸识别处理成, faceSampleId: {}, score: {}",
faceSampleId, addFaceResp.getScore());
log.info("人脸识别处理成, faceSampleId: {}", faceSampleId);
// 查询设备配置,判断是否启用预订功能
Long deviceId = message.getDeviceId();
@@ -155,7 +166,6 @@ public class FaceProcessingKafkaService {
if (deviceConfig != null &&
Integer.valueOf(1).equals(deviceConfig.getInteger("enable_pre_book"))) {
DynamicTaskGenerator.addTask(faceSampleId);
log.info("已添加到预订任务队列, faceSampleId: {}", faceSampleId);
}
} else {
log.warn("人脸添加返回空结果, faceSampleId: {}", faceSampleId);
@@ -177,7 +187,6 @@ public class FaceProcessingKafkaService {
private void updateFaceSampleStatus(Long faceSampleId, Integer status) {
try {
faceSampleMapper.updateStatus(faceSampleId, status);
log.info("人脸样本状态已更新, faceSampleId: {}, status: {} (0:初始,1:处理中,2:成功,-1:失败)", faceSampleId, status);
} catch (Exception e) {
log.error("更新人脸样本状态失败, faceSampleId: {}", faceSampleId, e);
}

View File

@@ -0,0 +1,29 @@
package com.ycwl.basic.integration.message.client;
import com.ycwl.basic.integration.common.response.CommonResponse;
import com.ycwl.basic.integration.message.dto.ChannelsResponse;
import com.ycwl.basic.integration.message.dto.MessageListData;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(name = "zt-message", contextId = "zt-message", path = "")
public interface MessageClient {
@GetMapping("/messages")
CommonResponse<MessageListData> listMessages(
@RequestParam(name = "page", defaultValue = "1") Integer page,
@RequestParam(name = "pageSize", defaultValue = "20") Integer pageSize,
@RequestParam(name = "channelId", required = false) String channelId,
@RequestParam(name = "title", required = false) String title,
@RequestParam(name = "content", required = false) String content,
@RequestParam(name = "sendBiz", required = false) String sendBiz,
@RequestParam(name = "sentAtStart", required = false) String sentAtStart,
@RequestParam(name = "sentAtEnd", required = false) String sentAtEnd,
@RequestParam(name = "createdAtStart", required = false) String createdAtStart,
@RequestParam(name = "createdAtEnd", required = false) String createdAtEnd
);
@GetMapping("/channels")
CommonResponse<ChannelsResponse> listChannels();
}

View File

@@ -0,0 +1,10 @@
package com.ycwl.basic.integration.message.dto;
import lombok.Data;
import java.util.List;
@Data
public class ChannelsResponse {
private List<String> channels;
}

View File

@@ -0,0 +1,13 @@
package com.ycwl.basic.integration.message.dto;
import lombok.Data;
import java.util.List;
@Data
public class MessageListData {
private List<MessageRecordDTO> list;
private String total; // string to avoid JS precision
private Integer page;
private Integer pageSize;
}

View File

@@ -0,0 +1,25 @@
package com.ycwl.basic.integration.message.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.util.Map;
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class MessageRecordDTO {
private String id; // string to avoid JS precision
private String channelId;
private String title;
private String content;
private String target;
private Map<String, Object> extraJson;
private String sendReason;
private String sendBiz;
private String status;
private String errorMsg;
private Integer attempts;
private String sentAt; // RFC3339 or yyyy-MM-dd HH:mm:ss (pass-through)
private String createdAt;
private String updatedAt;
}

View File

@@ -0,0 +1,35 @@
package com.ycwl.basic.integration.message.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.HashMap;
import java.util.Map;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ZtMessage {
private String channelId; // required
private String title; // required
private String content; // required
private String target; // required
private Map<String, Object> extra; // optional
private String sendReason; // optional
private String sendBiz; // optional
public static ZtMessage of(String channelId, String title, String content, String target) {
return ZtMessage.builder()
.channelId(channelId)
.title(title)
.content(content)
.target(target)
.extra(new HashMap<>())
.build();
}
}

View File

@@ -0,0 +1,57 @@
package com.ycwl.basic.integration.message.service;
import com.ycwl.basic.integration.common.exception.IntegrationException;
import com.ycwl.basic.integration.common.response.CommonResponse;
import com.ycwl.basic.integration.common.service.IntegrationFallbackService;
import com.ycwl.basic.integration.message.client.MessageClient;
import com.ycwl.basic.integration.message.dto.ChannelsResponse;
import com.ycwl.basic.integration.message.dto.MessageListData;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
@RequiredArgsConstructor
public class MessageIntegrationService {
private final MessageClient client;
private final IntegrationFallbackService fallbackService;
private static final String SERVICE_NAME = "zt-message";
public MessageListData listMessages(Integer page, Integer pageSize,
String channelId, String title, String content, String sendBiz,
String sentAtStart, String sentAtEnd,
String createdAtStart, String createdAtEnd) {
log.debug("查询消息列表 page={}, pageSize={}, channelId={}, title={}, sendBiz={}", page, pageSize, channelId, title, sendBiz);
CommonResponse<MessageListData> resp = client.listMessages(page, pageSize, channelId, title, content, sendBiz,
sentAtStart, sentAtEnd, createdAtStart, createdAtEnd);
return handleResponse(resp, "查询消息列表失败");
}
public ChannelsResponse listChannels() {
log.debug("查询消息通道列表");
// 相对稳定的数据,使用fallback缓存
return fallbackService.executeWithFallback(
SERVICE_NAME,
"channels",
() -> {
CommonResponse<ChannelsResponse> resp = client.listChannels();
return handleResponse(resp, "查询通道列表失败");
},
ChannelsResponse.class
);
}
private <T> T handleResponse(CommonResponse<T> response, String errorMessage) {
if (response == null || !response.isSuccess()) {
String msg = response != null && response.getMessage() != null
? response.getMessage()
: errorMessage;
Integer code = response != null ? response.getCode() : 5000;
throw new IntegrationException(code, msg, SERVICE_NAME);
}
return response.getData();
}
}

View File

@@ -0,0 +1,64 @@
package com.ycwl.basic.integration.message.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ycwl.basic.integration.kafka.config.KafkaIntegrationProperties;
import com.ycwl.basic.integration.message.dto.ZtMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;
import java.util.Map;
@Slf4j
@Service
@RequiredArgsConstructor
@ConditionalOnProperty(name = "kafka.enabled", havingValue = "true")
public class ZtMessageProducerService {
public static final String DEFAULT_TOPIC = "zt-message";
private final KafkaTemplate<String, String> kafkaTemplate;
private final ObjectMapper objectMapper;
private final KafkaIntegrationProperties kafkaProps;
public void send(ZtMessage msg) {
validate(msg);
String topic = kafkaProps != null && StringUtils.isNotBlank(kafkaProps.getZtMessageTopic())
? kafkaProps.getZtMessageTopic()
: DEFAULT_TOPIC;
String key = msg.getChannelId();
String payload = toJson(msg);
log.info("[ZT-MESSAGE] producing to topic={}, key={}, title={}", topic, key, msg.getTitle());
kafkaTemplate.send(topic, key, payload).whenComplete((metadata, ex) -> {
if (ex != null) {
log.error("[ZT-MESSAGE] produce failed: {}", ex.getMessage(), ex);
} else if (metadata != null) {
log.info("[ZT-MESSAGE] produced: partition={}, offset={}", metadata.getRecordMetadata().partition(), metadata.getRecordMetadata().offset());
}
});
}
private void validate(ZtMessage msg) {
if (msg == null) throw new IllegalArgumentException("message is null");
if (StringUtils.isBlank(msg.getChannelId())) throw new IllegalArgumentException("channelId is required");
if (StringUtils.isBlank(msg.getTitle())) throw new IllegalArgumentException("title is required");
if (StringUtils.isBlank(msg.getContent())) throw new IllegalArgumentException("content is required");
if (StringUtils.isBlank(msg.getTarget())) throw new IllegalArgumentException("target is required");
if (msg.getExtra() != null && !(msg.getExtra() instanceof Map)) {
throw new IllegalArgumentException("extra must be a Map");
}
}
private String toJson(ZtMessage msg) {
try {
return objectMapper.writeValueAsString(msg);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("failed to serialize message", e);
}
}
}

View File

@@ -1,306 +0,0 @@
package com.ycwl.basic.integration.questionnaire.example;
import com.ycwl.basic.integration.common.response.PageResponse;
import com.ycwl.basic.integration.common.service.IntegrationFallbackService;
import com.ycwl.basic.integration.questionnaire.dto.answer.AnswerRequest;
import com.ycwl.basic.integration.questionnaire.dto.answer.ResponseDetailResponse;
import com.ycwl.basic.integration.questionnaire.dto.answer.SubmitAnswerRequest;
import com.ycwl.basic.integration.questionnaire.dto.question.CreateQuestionOptionRequest;
import com.ycwl.basic.integration.questionnaire.dto.question.CreateQuestionRequest;
import com.ycwl.basic.integration.questionnaire.dto.questionnaire.CreateQuestionnaireRequest;
import com.ycwl.basic.integration.questionnaire.dto.questionnaire.QuestionnaireResponse;
import com.ycwl.basic.integration.questionnaire.dto.statistics.QuestionnaireStatistics;
import com.ycwl.basic.integration.questionnaire.service.QuestionnaireIntegrationService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@Slf4j
@Component
@RequiredArgsConstructor
@ConditionalOnProperty(prefix = "integration.questionnaire.example", name = "enabled", havingValue = "true")
public class QuestionnaireIntegrationExample {
private final QuestionnaireIntegrationService questionnaireService;
private final IntegrationFallbackService fallbackService;
@EventListener(ApplicationReadyEvent.class)
public void runExamples() {
try {
log.info("=== 开始问卷集成服务示例 ===");
// 示例1:创建问卷
createQuestionnaireExample();
// 示例2:查询问卷
queryQuestionnaireExample();
// 示例3:提交答案
submitAnswerExample();
// 示例4:统计查询
statisticsExample();
// 示例5:Fallback 缓存管理
fallbackCacheExample();
log.info("=== 问卷集成服务示例完成 ===");
} catch (Exception e) {
log.error("问卷集成服务示例执行失败", e);
}
}
/**
* 示例1:创建问卷
*/
private void createQuestionnaireExample() {
log.info("--- 示例1:创建客户满意度问卷 ---");
try {
CreateQuestionnaireRequest request = new CreateQuestionnaireRequest();
request.setName("客户满意度调查");
request.setDescription("用于了解客户对我们服务的满意度");
request.setIsAnonymous(true);
request.setMaxAnswers(1000);
// 添加单选题
CreateQuestionRequest question1 = new CreateQuestionRequest();
question1.setTitle("您对我们的服务满意吗?");
question1.setType(1); // 单选题
question1.setIsRequired(true);
question1.setSort(1);
List<CreateQuestionOptionRequest> options1 = new ArrayList<>();
options1.add(new CreateQuestionOptionRequest("非常满意", "5", 1));
options1.add(new CreateQuestionOptionRequest("满意", "4", 2));
options1.add(new CreateQuestionOptionRequest("一般", "3", 3));
options1.add(new CreateQuestionOptionRequest("不满意", "2", 4));
options1.add(new CreateQuestionOptionRequest("非常不满意", "1", 5));
question1.setOptions(options1);
// 添加多选题
CreateQuestionRequest question2 = new CreateQuestionRequest();
question2.setTitle("您感兴趣的服务有哪些?");
question2.setType(2); // 多选题
question2.setIsRequired(false);
question2.setSort(2);
List<CreateQuestionOptionRequest> options2 = new ArrayList<>();
options2.add(new CreateQuestionOptionRequest("技术支持", "tech_support", 1));
options2.add(new CreateQuestionOptionRequest("产品培训", "training", 2));
options2.add(new CreateQuestionOptionRequest("定制开发", "custom_dev", 3));
options2.add(new CreateQuestionOptionRequest("其他", "others", 4));
question2.setOptions(options2);
// 添加文本域题
CreateQuestionRequest question3 = new CreateQuestionRequest();
question3.setTitle("您还有什么建议吗?");
question3.setType(4); // 文本域题
question3.setIsRequired(false);
question3.setSort(3);
question3.setOptions(null); // 文本域题不需要选项
request.setQuestions(Arrays.asList(question1, question2, question3));
QuestionnaireResponse response = questionnaireService.createQuestionnaire(request, "admin");
log.info("✅ 问卷创建成功,ID: {}, 名称: {}", response.getId(), response.getName());
} catch (Exception e) {
log.error("❌ 创建问卷示例失败", e);
}
}
/**
* 示例2:查询问卷
*/
private void queryQuestionnaireExample() {
log.info("--- 示例2:查询问卷示例 ---");
try {
// 获取问卷列表(支持 fallback)
PageResponse<QuestionnaireResponse> listResponse = questionnaireService.getQuestionnaireList(1, 10, null, null, null);
log.info("✅ 问卷列表查询成功,总数: {}, 当前页数据: {}",
listResponse.getTotal(), listResponse.getList().size());
if (listResponse.getList() != null && !listResponse.getList().isEmpty()) {
Long questionnaireId = listResponse.getList().get(0).getId();
// 获取问卷详情(支持 fallback)
QuestionnaireResponse detailResponse = questionnaireService.getQuestionnaire(questionnaireId);
log.info("✅ 问卷详情查询成功,ID: {}, 名称: {}, 问题数: {}",
detailResponse.getId(), detailResponse.getName(),
detailResponse.getQuestions() != null ? detailResponse.getQuestions().size() : 0);
}
} catch (Exception e) {
log.error("❌ 查询问卷示例失败", e);
}
}
/**
* 示例3:提交答案
*/
private void submitAnswerExample() {
log.info("--- 示例3:提交问卷答案示例 ---");
try {
SubmitAnswerRequest request = new SubmitAnswerRequest();
request.setQuestionnaireId(1001L);
request.setUserId("user123");
List<AnswerRequest> answers = new ArrayList<>();
// 单选题答案
answers.add(new AnswerRequest(123L, "4")); // 满意
// 多选题答案
answers.add(new AnswerRequest(124L, "tech_support,training")); // 技术支持和产品培训
// 文本域题答案
answers.add(new AnswerRequest(125L, "服务很好,希望能增加更多实用功能"));
request.setAnswers(answers);
ResponseDetailResponse response = questionnaireService.submitAnswer(request);
log.info("✅ 问卷答案提交成功,回答ID: {}, 提交时间: {}",
response.getId(), response.getSubmittedAt());
} catch (Exception e) {
log.error("❌ 提交答案示例失败", e);
}
}
/**
* 示例4:统计查询
*/
private void statisticsExample() {
log.info("--- 示例4:问卷统计查询示例 ---");
try {
Long questionnaireId = 1001L;
// 获取问卷统计(支持 fallback)
QuestionnaireStatistics stats = questionnaireService.getStatistics(questionnaireId);
log.info("✅ 统计查询成功,总回答数: {}, 完成率: {}%, 平均用时: {}秒",
stats.getTotalResponses(),
stats.getCompletionRate() != null ? stats.getCompletionRate() * 100 : 0,
stats.getAverageTime());
// 获取回答记录列表(支持 fallback)
questionnaireService.getResponseList(1, 10, questionnaireId, null, null, null);
log.info("✅ 回答记录列表查询成功");
} catch (Exception e) {
log.error("❌ 统计查询示例失败", e);
}
}
/**
* 示例5:Fallback 缓存管理
*/
private void fallbackCacheExample() {
log.info("--- 示例5:Fallback 缓存管理示例 ---");
try {
String serviceName = "zt-questionnaire";
// 检查缓存状态
boolean hasQuestionnaireCache = fallbackService.hasFallbackCache(serviceName, "questionnaire:1001");
boolean hasListCache = fallbackService.hasFallbackCache(serviceName, "questionnaire:list:1:10:null:null:null");
log.info("✅ 缓存状态检查 - 问卷缓存: {}, 列表缓存: {}", hasQuestionnaireCache, hasListCache);
// 获取缓存统计
IntegrationFallbackService.FallbackCacheStats stats = fallbackService.getFallbackCacheStats(serviceName);
log.info("✅ 缓存统计 - 缓存项目数: {}, TTL: {} 天",
stats.getTotalCacheCount(), stats.getFallbackTtlDays());
// 清理特定缓存示例(仅演示,实际使用时谨慎操作)
// fallbackService.clearFallbackCache(serviceName, "questionnaire:1001");
// log.info("✅ 已清理问卷缓存");
} catch (Exception e) {
log.error("❌ Fallback 缓存管理示例失败", e);
}
}
/**
* 问卷管理工作流示例
*/
public void questionnaireWorkflowExample(String userId) {
log.info("--- 问卷管理工作流示例 ---");
try {
// 1. 创建问卷
CreateQuestionnaireRequest createRequest = createSampleQuestionnaire();
QuestionnaireResponse questionnaire = questionnaireService.createQuestionnaire(createRequest, userId);
log.info("✅ 步骤1 - 问卷创建成功: {}", questionnaire.getName());
Long questionnaireId = questionnaire.getId();
// 2. 发布问卷
QuestionnaireResponse published = questionnaireService.publishQuestionnaire(questionnaireId, userId);
log.info("✅ 步骤2 - 问卷发布成功,状态: {}", published.getStatusText());
// 3. 模拟用户提交答案
SubmitAnswerRequest answerRequest = createSampleAnswers(questionnaireId);
ResponseDetailResponse answerResponse = questionnaireService.submitAnswer(answerRequest);
log.info("✅ 步骤3 - 答案提交成功: {}", answerResponse.getId());
// 4. 查看统计数据
QuestionnaireStatistics statistics = questionnaireService.getStatistics(questionnaireId);
log.info("✅ 步骤4 - 统计查询成功,回答数: {}", statistics.getTotalResponses());
// 5. 停止问卷
QuestionnaireResponse stopped = questionnaireService.stopQuestionnaire(questionnaireId, userId);
log.info("✅ 步骤5 - 问卷停止成功,状态: {}", stopped.getStatusText());
} catch (Exception e) {
log.error("❌ 问卷管理工作流示例失败", e);
}
}
private CreateQuestionnaireRequest createSampleQuestionnaire() {
CreateQuestionnaireRequest request = new CreateQuestionnaireRequest();
request.setName("产品体验调查");
request.setDescription("收集用户对产品的使用体验反馈");
request.setIsAnonymous(false);
request.setMaxAnswers(500);
// 评分题
CreateQuestionRequest ratingQuestion = new CreateQuestionRequest();
ratingQuestion.setTitle("请对我们的产品进行评分(1-10分)");
ratingQuestion.setType(5); // 评分题
ratingQuestion.setIsRequired(true);
ratingQuestion.setSort(1);
ratingQuestion.setOptions(null); // 评分题不需要选项
// 填空题
CreateQuestionRequest textQuestion = new CreateQuestionRequest();
textQuestion.setTitle("请输入您的姓名");
textQuestion.setType(3); // 填空题
textQuestion.setIsRequired(true);
textQuestion.setSort(2);
textQuestion.setOptions(null); // 填空题不需要选项
request.setQuestions(Arrays.asList(ratingQuestion, textQuestion));
return request;
}
private SubmitAnswerRequest createSampleAnswers(Long questionnaireId) {
SubmitAnswerRequest request = new SubmitAnswerRequest();
request.setQuestionnaireId(questionnaireId);
request.setUserId("test_user");
List<AnswerRequest> answers = new ArrayList<>();
answers.add(new AnswerRequest(1L, "8")); // 评分题答案
answers.add(new AnswerRequest(2L, "张三")); // 填空题答案
request.setAnswers(answers);
return request;
}
}

View File

@@ -18,12 +18,6 @@ public interface RenderWorkerV2Client {
@GetMapping("/{id}")
CommonResponse<RenderWorkerV2DTO> getWorker(@PathVariable("id") Long id);
/**
* 获取工作器含配置信息
*/
@GetMapping("/{id}/with-config")
CommonResponse<RenderWorkerV2WithConfigDTO> getWorkerWithConfig(@PathVariable("id") Long id);
/**
* 创建工作器
*/
@@ -52,25 +46,9 @@ public interface RenderWorkerV2Client {
@RequestParam(required = false) Integer isEnabled,
@RequestParam(required = false) String name);
/**
* 分页查询工作器列表(含配置信息)
*/
@GetMapping("/with-config")
CommonResponse<PageResponse<RenderWorkerV2WithConfigDTO>> listWorkersWithConfig(
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) Integer isEnabled,
@RequestParam(required = false) String name);
/**
* 根据key获取工作器核心信息
*/
@GetMapping("/key/{key}")
CommonResponse<RenderWorkerV2DTO> getWorkerByKey(@PathVariable("key") String key);
/**
* 根据key获取工作器完整信息(含配置)
*/
@GetMapping("/key/{key}/with-config")
CommonResponse<RenderWorkerV2WithConfigDTO> getWorkerWithConfigByKey(@PathVariable("key") String key);
}

View File

@@ -70,7 +70,7 @@ public class RenderWorkerConfigIntegrationService {
log.debug("获取渲染工作器平铺配置, workerId: {}", workerId);
return fallbackService.executeWithFallback(
SERVICE_NAME,
"worker:flat:config:" + workerId,
"worker:config:" + workerId,
() -> {
List<RenderWorkerConfigV2DTO> configs = getWorkerConfigsInternal(workerId);
return flattenConfigs(configs);

View File

@@ -42,22 +42,6 @@ public class RenderWorkerIntegrationService {
);
}
/**
* 获取工作器详细信息(含配置)(带降级)
*/
public RenderWorkerV2WithConfigDTO getWorkerWithConfig(Long id) {
log.debug("获取渲染工作器详细信息, id: {}", id);
return fallbackService.executeWithFallback(
SERVICE_NAME,
"worker:config:" + id,
() -> {
CommonResponse<RenderWorkerV2WithConfigDTO> response = renderWorkerV2Client.getWorkerWithConfig(id);
return handleResponse(response, "获取渲染工作器详细信息失败");
},
RenderWorkerV2WithConfigDTO.class
);
}
/**
* 创建工作器(直接调用,不降级)
*/
@@ -96,18 +80,6 @@ public class RenderWorkerIntegrationService {
return handleResponse(response, "查询渲染工作器列表失败");
}
/**
* 分页查询工作器列表(含配置信息)(不降级)
*/
public PageResponse<RenderWorkerV2WithConfigDTO> listWorkersWithConfig(Integer page, Integer pageSize,
Integer isEnabled, String name) {
log.debug("分页查询渲染工作器列表(含配置), page: {}, pageSize: {}, isEnabled: {}, name: {}",
page, pageSize, isEnabled, name);
CommonResponse<PageResponse<RenderWorkerV2WithConfigDTO>> response =
renderWorkerV2Client.listWorkersWithConfig(page, pageSize, isEnabled, name);
return handleResponse(response, "查询渲染工作器列表(含配置)失败");
}
/**
* 根据key获取工作器核心信息(带降级)
*/
@@ -124,22 +96,6 @@ public class RenderWorkerIntegrationService {
);
}
/**
* 根据key获取工作器详细信息(含配置)(带降级)
*/
public RenderWorkerV2WithConfigDTO getWorkerWithConfigByKey(String key) {
log.debug("根据key获取渲染工作器详细信息, key: {}", key);
return fallbackService.executeWithFallback(
SERVICE_NAME,
"worker:key:config:" + key,
() -> {
CommonResponse<RenderWorkerV2WithConfigDTO> response = renderWorkerV2Client.getWorkerWithConfigByKey(key);
return handleResponse(response, "根据key获取渲染工作器详细信息失败");
},
RenderWorkerV2WithConfigDTO.class
);
}
/**
* 处理通用响应
*/

View File

@@ -18,9 +18,6 @@ public interface ScenicConfigV2Client {
CommonResponse<ScenicConfigV2DTO> getConfigByKey(@PathVariable("scenicId") Long scenicId,
@PathVariable("configKey") String configKey);
@GetMapping("/{scenicId}/keys")
CommonResponse<Map<String, Object>> getFlatConfigs(@PathVariable("scenicId") Long scenicId);
@PostMapping("/{scenicId}")
CommonResponse<ScenicConfigV2DTO> createConfig(@PathVariable("scenicId") Long scenicId,
@RequestBody CreateConfigRequest request);
@@ -37,8 +34,4 @@ public interface ScenicConfigV2Client {
@PostMapping("/{scenicId}/batch")
CommonResponse<BatchUpdateResponse> batchUpdateConfigs(@PathVariable("scenicId") Long scenicId,
@RequestBody BatchConfigRequest request);
@PostMapping("/{scenicId}/batchFlatUpdate")
CommonResponse<BatchUpdateResponse> batchFlatUpdateConfigs(@PathVariable("scenicId") Long scenicId,
@RequestBody Map<String, Object> configs);
}

View File

@@ -5,7 +5,6 @@ import com.ycwl.basic.integration.scenic.dto.filter.ScenicFilterPageResponse;
import com.ycwl.basic.integration.scenic.dto.filter.ScenicFilterRequest;
import com.ycwl.basic.integration.scenic.dto.scenic.CreateScenicRequest;
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO;
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2WithConfigDTO;
import com.ycwl.basic.integration.common.response.PageResponse;
import com.ycwl.basic.integration.scenic.dto.scenic.UpdateScenicRequest;
import org.springframework.cloud.openfeign.FeignClient;
@@ -19,10 +18,6 @@ public interface ScenicV2Client {
@GetMapping("/{scenicId}")
CommonResponse<ScenicV2DTO> getScenic(@PathVariable("scenicId") Long scenicId);
@GetMapping("/{scenicId}/with-config")
CommonResponse<ScenicV2WithConfigDTO> getScenicWithConfig(@PathVariable("scenicId") Long scenicId);
@PostMapping("/")
CommonResponse<ScenicV2DTO> createScenic(@RequestBody CreateScenicRequest request);
@@ -41,10 +36,4 @@ public interface ScenicV2Client {
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) Integer status,
@RequestParam(required = false) String name);
@GetMapping("/with-config")
CommonResponse<PageResponse<ScenicV2WithConfigDTO>> listScenicsWithConfig(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) Integer status,
@RequestParam(required = false) String name);
}

View File

@@ -1,14 +0,0 @@
package com.ycwl.basic.integration.scenic.dto.scenic;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Map;
@Data
@EqualsAndHashCode(callSuper = true)
public class ScenicV2WithConfigDTO extends ScenicV2DTO {
@JsonProperty("config")
private Map<String, Object> config;
}

View File

@@ -1,184 +0,0 @@
package com.ycwl.basic.integration.scenic.example;
import com.ycwl.basic.integration.common.service.IntegrationFallbackService;
import com.ycwl.basic.integration.scenic.dto.config.*;
import com.ycwl.basic.integration.scenic.dto.filter.*;
import com.ycwl.basic.integration.scenic.dto.scenic.*;
import com.ycwl.basic.integration.scenic.service.ScenicConfigIntegrationService;
import com.ycwl.basic.integration.scenic.service.ScenicIntegrationService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* 景区集成示例(包含降级机制)
* 演示景区集成和失败降级策略的使用
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ScenicIntegrationExample {
private final ScenicIntegrationService scenicIntegrationService;
private final ScenicConfigIntegrationService scenicConfigIntegrationService;
private final IntegrationFallbackService fallbackService;
private static final String SERVICE_NAME = "zt-scenic";
/**
* 示例:创建景区并设置配置
*/
public void createScenicWithConfig() {
try {
// 1. 创建景区
CreateScenicRequest createRequest = new CreateScenicRequest();
createRequest.setName("测试景区");
createRequest.setMpId(1001);
var scenic = scenicIntegrationService.createScenic(createRequest);
log.info("创建景区成功: {}", scenic.getName());
// 2. 为景区添加配置
CreateConfigRequest configRequest = new CreateConfigRequest();
configRequest.setConfigKey("tour_time");
configRequest.setConfigValue("120");
configRequest.setConfigType("int");
configRequest.setDescription("游览时长");
var config = scenicConfigIntegrationService.createConfig(
Long.valueOf(scenic.getId()), configRequest);
log.info("创建配置成功: {} = {}", config.getConfigKey(), config.getConfigValue());
} catch (Exception e) {
log.error("创建景区和配置失败", e);
}
}
/**
* 示例:筛选景区
*/
public void filterScenics() {
try {
FilterCondition condition = new FilterCondition();
condition.setConfigKey("tour_time");
condition.setConfigValue("120");
condition.setOperator("gte");
ScenicFilterRequest filterRequest = new ScenicFilterRequest();
filterRequest.setFilters(Collections.singletonList(condition));
filterRequest.setPage(1);
filterRequest.setPageSize(10);
var result = scenicIntegrationService.filterScenics(filterRequest);
log.info("筛选到 {} 个景区", result.getTotal());
} catch (Exception e) {
log.error("筛选景区失败", e);
}
}
/**
* 演示基础景区操作的降级机制
*/
public void basicScenicOperationsExample() {
log.info("=== 基础景区操作示例(含降级机制) ===");
Long scenicId = 2001L;
try {
// 获取景区信息 - 自动降级
ScenicV2DTO scenic = scenicIntegrationService.getScenic(scenicId);
log.info("获取景区成功: {}", scenic.getName());
// 获取景区配置信息 - 自动降级
ScenicV2WithConfigDTO scenicWithConfig = scenicIntegrationService.getScenicWithConfig(scenicId);
log.info("获取景区配置成功,配置数量: {}", scenicWithConfig.getConfig().size());
// 获取扁平化配置 - 自动降级
Map<String, Object> flatConfig = scenicIntegrationService.getScenicFlatConfig(scenicId);
log.info("获取扁平化配置成功,配置项数量: {}", flatConfig.size());
} catch (Exception e) {
log.error("景区操作降级失败", e);
}
}
/**
* 演示景区配置管理的降级机制
*/
public void scenicConfigManagementFallbackExample() {
log.info("=== 景区配置管理示例(含降级机制) ===");
Long scenicId = 2001L;
try {
// 获取扁平化配置 - 自动降级
Map<String, Object> flatConfigs = scenicConfigIntegrationService.getFlatConfigs(scenicId);
log.info("获取扁平化配置成功,配置项数量: {}", flatConfigs.size());
// 批量更新配置 - 直接操作,失败时抛出异常
Map<String, Object> updates = new HashMap<>();
updates.put("max_visitors", "5000");
updates.put("opening_hours", "08:00-18:00");
BatchUpdateResponse result = scenicConfigIntegrationService.batchFlatUpdateConfigs(scenicId, updates);
log.info("批量更新配置完成: 成功 {}, 失败 {}", result.getSuccess(), result.getFailed());
} catch (Exception e) {
log.error("景区配置管理操作失败", e);
}
}
/**
* 演示降级缓存管理
*/
public void fallbackCacheManagementExample() {
log.info("=== 景区降级缓存管理示例 ===");
String scenicCacheKey = "scenic:2001";
String configCacheKey = "scenic:flat:configs:2001";
// 检查降级缓存状态
boolean hasScenicCache = fallbackService.hasFallbackCache(SERVICE_NAME, scenicCacheKey);
boolean hasConfigCache = fallbackService.hasFallbackCache(SERVICE_NAME, configCacheKey);
log.info("景区降级缓存存在: {}", hasScenicCache);
log.info("配置降级缓存存在: {}", hasConfigCache);
// 获取降级缓存统计信息
IntegrationFallbackService.FallbackCacheStats stats = fallbackService.getFallbackCacheStats(SERVICE_NAME);
log.info("景区服务降级缓存统计: 缓存数量={}, TTL={}天",
stats.getTotalCacheCount(), stats.getFallbackTtlDays());
// 清理特定的降级缓存
if (hasScenicCache) {
fallbackService.clearFallbackCache(SERVICE_NAME, scenicCacheKey);
log.info("已清理景区降级缓存");
}
// 如果缓存过多,批量清理
if (stats.getTotalCacheCount() > 50) {
fallbackService.clearAllFallbackCache(SERVICE_NAME);
log.info("已批量清理所有景区降级缓存");
}
}
/**
* 运行所有示例
*/
public void runAllExamples() {
log.info("开始运行景区集成示例(包含降级机制)...");
createScenicWithConfig();
filterScenics();
basicScenicOperationsExample();
scenicConfigManagementFallbackExample();
fallbackCacheManagementExample();
log.info("景区集成示例运行完成");
}
}

View File

@@ -48,19 +48,6 @@ public class ScenicConfigIntegrationService {
);
}
public Map<String, Object> getFlatConfigs(Long scenicId) {
log.debug("获取景区扁平化配置, scenicId: {}", scenicId);
return fallbackService.executeWithFallback(
SERVICE_NAME,
"scenic:flat:configs:" + scenicId,
() -> {
CommonResponse<Map<String, Object>> response = scenicConfigV2Client.getFlatConfigs(scenicId);
return handleResponse(response, "获取景区扁平化配置失败");
},
Map.class
);
}
public ScenicConfigV2DTO createConfig(Long scenicId, CreateConfigRequest request) {
log.debug("创建景区配置, scenicId: {}, configKey: {}", scenicId, request.getConfigKey());
CommonResponse<ScenicConfigV2DTO> response = scenicConfigV2Client.createConfig(scenicId, request);
@@ -85,12 +72,6 @@ public class ScenicConfigIntegrationService {
return handleResponse(response, "批量更新景区配置失败");
}
public BatchUpdateResponse batchFlatUpdateConfigs(Long scenicId, Map<String, Object> configs) {
log.debug("扁平化批量更新景区配置, scenicId: {}, configs count: {}", scenicId, configs.size());
CommonResponse<BatchUpdateResponse> response = scenicConfigV2Client.batchFlatUpdateConfigs(scenicId, configs);
return handleResponse(response, "扁平化批量更新景区配置失败");
}
private <T> T handleResponse(CommonResponse<T> response, String errorMessage) {
if (response == null || !response.isSuccess()) {

View File

@@ -9,7 +9,6 @@ import com.ycwl.basic.integration.scenic.dto.filter.ScenicFilterPageResponse;
import com.ycwl.basic.integration.scenic.dto.filter.ScenicFilterRequest;
import com.ycwl.basic.integration.scenic.dto.scenic.CreateScenicRequest;
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO;
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2WithConfigDTO;
import com.ycwl.basic.integration.common.response.PageResponse;
import com.ycwl.basic.integration.scenic.dto.scenic.UpdateScenicRequest;
import lombok.RequiredArgsConstructor;
@@ -42,32 +41,6 @@ public class ScenicIntegrationService {
);
}
public ScenicV2WithConfigDTO getScenicWithConfig(Long scenicId) {
log.debug("获取景区配置信息, scenicId: {}", scenicId);
return fallbackService.executeWithFallback(
SERVICE_NAME,
"scenic:config:" + scenicId,
() -> {
CommonResponse<ScenicV2WithConfigDTO> response = scenicV2Client.getScenicWithConfig(scenicId);
return handleResponse(response, "获取景区配置信息失败");
},
ScenicV2WithConfigDTO.class
);
}
public Map<String, Object> getScenicFlatConfig(Long scenicId) {
log.debug("获取景区扁平化配置, scenicId: {}", scenicId);
return fallbackService.executeWithFallback(
SERVICE_NAME,
"scenic:flat:config:" + scenicId,
() -> {
CommonResponse<Map<String, Object>> response = scenicConfigV2Client.getFlatConfigs(scenicId);
return handleResponse(response, "获取景区扁平化配置失败");
},
Map.class
);
}
public ScenicV2DTO createScenic(CreateScenicRequest request) {
log.debug("创建景区, name: {}", request.getName());
CommonResponse<ScenicV2DTO> response = scenicV2Client.createScenic(request);
@@ -98,12 +71,6 @@ public class ScenicIntegrationService {
return handleResponse(response, "分页查询景区列表失败");
}
public PageResponse<ScenicV2WithConfigDTO> listScenicsWithConfig(Integer page, Integer pageSize, Integer status, String name) {
log.debug("分页查询景区带配置列表, page: {}, pageSize: {}, status: {}, name: {}", page, pageSize, status, name);
CommonResponse<PageResponse<ScenicV2WithConfigDTO>> response = scenicV2Client.listScenicsWithConfig(page, pageSize, status, name);
return handleResponse(response, "分页查询景区带配置列表失败");
}
private <T> T handleResponse(CommonResponse<T> response, String errorMessage) {
if (response == null || !response.isSuccess()) {
String msg = response != null && response.getMessage() != null

View File

@@ -1,5 +1,6 @@
package com.ycwl.basic.mapper;
import com.ycwl.basic.model.pc.printer.entity.MemberPrintEntity;
import com.ycwl.basic.model.pc.printer.entity.PrintTaskEntity;
import com.ycwl.basic.model.pc.printer.entity.PrinterEntity;
import com.ycwl.basic.model.pc.printer.resp.MemberPrintResp;
@@ -35,7 +36,7 @@ public interface PrinterMapper {
int deleteUserPhoto(Long memberId, Long scenicId, Long relationId);
int addUserPhoto(Long memberId, Long scenicId, String url);
int addUserPhoto(MemberPrintEntity entity);
MemberPrintResp getUserPhoto(Long memberId, Long scenicId, Long id);

View File

@@ -98,4 +98,11 @@ public interface SourceMapper {
* @return type=2的source列表
*/
List<SourceEntity> listImageSourcesByFaceId(Long faceId);
/**
* 从ZT-Source消息添加素材
* @param source 素材实体
* @return 影响行数
*/
int addFromZTSource(SourceEntity source);
}

View File

@@ -1,16 +1,7 @@
package com.ycwl.basic.model.pc.scenic.resp;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ycwl.basic.facebody.enums.FaceBodyAdapterType;
import com.ycwl.basic.pay.enums.PayAdapterType;
import com.ycwl.basic.storage.enums.StorageType;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
/**
* @Author:longbinbin
* @Date:2024/12/2 10:53
@@ -19,34 +10,49 @@ import java.util.Date;
@Data
public class ScenicConfigResp {
// ========== 基础配置 ==========
/**
* 预约流程,1-预约,2-在线,3-全部
* 水印URL
*/
private Integer bookRoutine;
private Integer forceFinishTime;
private Integer tourTime;
private String watermarkUrl;
/**
* 样本保存时间
*/
private Integer sampleStoreDay;
private Integer faceStoreDay;
/**
* 视频保存时间
* 视频存储天数
*/
private Integer videoStoreDay;
private Boolean allFree;
private Boolean disableSourceVideo;
private Boolean disableSourceImage;
private Integer antiScreenRecordType;
private Integer videoSourceStoreDay;
private Integer imageSourceStoreDay;
private Integer userSourceExpireDay;
private BigDecimal brokerDirectRate;
private String imageSourcePackHint = "";
private String videoSourcePackHint = "";
private Boolean voucherEnable;
private Boolean enableVoucher;
/**
* 防录屏类型配置
*/
private Integer antiScreenRecordType;
// ========== 功能开关 ==========
/**
* 分组功能开关
*/
private Boolean groupingEnable;
/**
* 优惠券功能开关
*/
private Boolean voucherEnable;
/**
* 等待时显示照片开关
*/
private Boolean showPhotoWhenWaiting;
// ========== 提示文案 ==========
/**
* 图片素材包提示文案
*/
private String imageSourcePackHint = "";
/**
* 视频素材包提示文案
*/
private String videoSourcePackHint = "";
}

View File

@@ -144,6 +144,17 @@ public enum ProductType {
}
```
#### 商品价格配置控制字段
`PriceProductConfig` 实体包含以下优惠控制字段:
- `canUseCoupon`: 是否可使用优惠券
- `canUseVoucher`: 是否可使用券码
- `canUseOnePrice`: 是否可使用一口价优惠(新增)
#### 一口价优惠控制机制
- 当购物车中任何商品的 `canUseOnePrice``false` 时,将跳过整个购物车的一口价优惠检测
- 配置优先级:具体商品配置 > 商品类型默认配置 > 系统默认(支持)
- 异常情况下默认支持一口价优惠,确保业务流程不受影响
#### 分层定价
支持基于数量的分层定价策略,通过 `PriceTierConfig` 配置不同数量区间的单价。
@@ -339,6 +350,7 @@ public interface IDiscountDetectionService {
#### OnePricePurchaseDiscountProvider (优先级: 120)
- 处理一口价优惠逻辑(景区级统一价格)
- **最高优先级**,优先于所有其他优惠类型
- 商品级别控制:检查购物车中所有商品的 `canUseOnePrice` 配置,任一商品不支持则跳过检测
- 仅当一口价小于当前金额时产生优惠;是否可与券码/优惠券叠加由配置 `canUseCoupon/canUseVoucher` 决定
#### BundleDiscountProvider (优先级: 100)
@@ -370,7 +382,7 @@ public interface IDiscountDetectionService {
特殊情况
- 全场免费券码直接最终价=0停止后续优惠
- 一口价可叠加性由配置 canUseCoupon / canUseVoucher 控制
- 一口价可叠加性由配置 canUseCoupon / canUseVoucher 控制商品级别由 canUseOnePrice 控制参与检测
```
#### 扩展支持
@@ -535,7 +547,7 @@ public class PriceCalculationResult {
## 数据库设计
### 核心表结构(摘)
- `price_product_config`: 商品价格基础配置
- `price_product_config`: 商品价格基础配置(包含 `can_use_coupon``can_use_voucher``can_use_one_price` 优惠控制字段)
- `price_tier_config`: 分层定价配置
- `price_bundle_config`: 套餐配置
- `price_coupon_config`: 优惠券配置

View File

@@ -4,7 +4,6 @@ import com.ycwl.basic.facebody.enums.FaceBodyAdapterType;
import com.ycwl.basic.integration.common.util.ConfigValueUtil;
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO;
import com.ycwl.basic.integration.common.response.PageResponse;
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2WithConfigDTO;
import com.ycwl.basic.integration.scenic.service.ScenicIntegrationService;
import com.ycwl.basic.integration.scenic.service.ScenicConfigIntegrationService;
import com.ycwl.basic.integration.scenic.dto.config.ScenicConfigV2DTO;
@@ -52,8 +51,11 @@ public class ScenicRepository {
}
public ScenicEntity getScenic(Long id) {
ScenicV2WithConfigDTO scenicDTO = scenicIntegrationService.getScenicWithConfig(id);
ScenicEntity scenicEntity = convertToScenicEntity(scenicDTO);
// 分别获取景区基础信息和配置信息
ScenicV2DTO scenicBasic = scenicIntegrationService.getScenic(id);
ScenicConfigManager configManager = getScenicConfigManager(id);
ScenicEntity scenicEntity = convertToScenicEntity(scenicBasic, configManager);
return scenicEntity;
}
@@ -220,7 +222,7 @@ public class ScenicRepository {
}
}
private ScenicEntity convertToScenicEntity(ScenicV2WithConfigDTO dto) {
private ScenicEntity convertToScenicEntity(ScenicV2DTO dto, ScenicConfigManager configManager) {
if (dto == null) {
return null;
}
@@ -229,19 +231,17 @@ public class ScenicRepository {
entity.setName(dto.getName());
entity.setMpId(dto.getMpId());
entity.setStatus(dto.getStatus().toString());
if (dto.getConfig() != null) {
entity.setAddress(ConfigValueUtil.getStringValue(dto.getConfig(), "address"));
entity.setArea(ConfigValueUtil.getStringValue(dto.getConfig(), "area"));
entity.setCity(ConfigValueUtil.getStringValue(dto.getConfig(), "city"));
entity.setProvince(ConfigValueUtil.getStringValue(dto.getConfig(), "province"));
entity.setLatitude(ConfigValueUtil.getBigDecimalValue(dto.getConfig(), "latitude"));
entity.setLongitude(ConfigValueUtil.getBigDecimalValue(dto.getConfig(), "longitude"));
entity.setRadius(ConfigValueUtil.getBigDecimalValue(dto.getConfig(), "radius"));
entity.setPhone(ConfigValueUtil.getStringValue(dto.getConfig(), "phone"));
entity.setLogoUrl(ConfigValueUtil.getStringValue(dto.getConfig(), "logoUrl"));
entity.setCoverUrl(ConfigValueUtil.getStringValue(dto.getConfig(), "coverUrl"));
entity.setKfCodeUrl(ConfigValueUtil.getStringValue(dto.getConfig(), "kfCodeUrl"));
}
entity.setAddress(configManager.getString("address"));
entity.setArea(configManager.getString("area"));
entity.setCity(configManager.getString("city"));
entity.setProvince(configManager.getString("province"));
entity.setLatitude(configManager.getBigDecimal("latitude"));
entity.setLongitude(configManager.getBigDecimal("longitude"));
entity.setRadius(configManager.getBigDecimal("radius"));
entity.setPhone(configManager.getString("phone"));
entity.setLogoUrl(configManager.getString("logo_url"));
entity.setCoverUrl(configManager.getString("cover_url"));
entity.setKfCodeUrl(configManager.getString("kf_code_url"));
return entity;
}

View File

@@ -0,0 +1,68 @@
package com.ycwl.basic.service;
import com.ycwl.basic.dto.ZTSourceMessage;
import com.ycwl.basic.utils.JacksonUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.support.Acknowledgment;
import org.springframework.stereotype.Service;
/**
* ZT-Source Kafka消费者服务
* 监听zt-source topic并处理素材消息
*
* @author system
* @date 2024/12/27
*/
@Slf4j
@Service
@RequiredArgsConstructor
@ConditionalOnProperty(name = "kafka.enabled", havingValue = "true")
public class ZTSourceConsumerService {
private static final String ZT_SOURCE_TOPIC = "zt-source";
private final ZTSourceDataService ztSourceDataService;
/**
* 监听zt-source topic消息
* 先解析消息并输出业务日志,然后手动确认处理
*
* @param message 消息JSON字符串
* @param ack 手动ACK确认
*/
@KafkaListener(topics = ZT_SOURCE_TOPIC, containerFactory = "manualCommitKafkaListenerContainerFactory")
public void handleZTSourceMessage(String message, Acknowledgment ack) {
ZTSourceMessage sourceMessage = null;
try {
// 先解析消息
sourceMessage = JacksonUtil.parseObject(message, ZTSourceMessage.class);
// 输出业务相关的日志信息
log.debug("接收到ZT-Source消息, sourceId: {}, deviceId: {}, faceSampleId: {}",
sourceMessage.getSourceId(), sourceMessage.getDeviceId(), sourceMessage.getFaceSampleId());
// 处理消息
boolean processed = ztSourceDataService.processZTSourceMessage(sourceMessage);
if (processed) {
// 只有在处理成功后才手动提交
ack.acknowledge();
log.info("ZT-Source消息处理成功并已提交, sourceId: {}", sourceMessage.getSourceId());
} else {
log.warn("ZT-Source消息处理被跳过(非照片类型),消息不会被提交, sourceId: {}, sourceType: {}",
sourceMessage.getSourceId(), sourceMessage.getSourceType());
// 对于非照片类型,也提交消息避免重复消费
ack.acknowledge();
}
} catch (Exception e) {
String sourceId = sourceMessage != null ? sourceMessage.getSourceId().toString() : "unknown";
log.error("处理ZT-Source消息失败,消息不会被提交: sourceId={}, error={}", sourceId, e.getMessage(), e);
// 不调用ack.acknowledge(),消息保持未提交状态,可以重新消费
}
}
}

View File

@@ -0,0 +1,138 @@
package com.ycwl.basic.service;
import com.ycwl.basic.dto.ZTSourceMessage;
import com.ycwl.basic.integration.common.manager.DeviceConfigManager;
import com.ycwl.basic.mapper.SourceMapper;
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
import com.ycwl.basic.repository.DeviceRepository;
import com.ycwl.basic.utils.SnowFlakeUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.util.Strings;
import org.springframework.stereotype.Service;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.util.Date;
/**
* ZT-Source数据处理服务
* 负责将ZT-Source消息转换并保存到数据库
*
* @author system
* @date 2024/12/27
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class ZTSourceDataService {
private final SourceMapper sourceMapper;
private final DeviceRepository deviceRepository;
/**
* 处理ZT-Source消息,仅处理照片类型(sourceType=2)
*
* @param message ZT-Source消息
* @return 是否处理成功
*/
public boolean processZTSourceMessage(ZTSourceMessage message) {
try {
// 仅处理照片类型的消息
if (!message.isPhoto()) {
log.debug("跳过非照片类型消息: sourceId={}, sourceType={}",
message.getSourceId(), message.getSourceType());
return false;
}
// 检查必要字段
if (!validateMessage(message)) {
log.warn("消息校验失败: {}", message);
return false;
}
// 转换为SourceEntity
SourceEntity sourceEntity = convertToSourceEntity(message);
DeviceConfigManager configManager = deviceRepository.getDeviceConfigManager(sourceEntity.getDeviceId());
if (configManager != null) {
if (Strings.isNotBlank(configManager.getString("crop_config"))) {
sourceEntity.setUrl(message.getThumbnailUrl());
}
}
// 保存到数据库
int result = sourceMapper.addFromZTSource(sourceEntity);
if (result > 0) {
log.info("成功保存ZT-Source照片素材: sourceId={}, entityId={}, scenicId={}, deviceId={}",
message.getSourceId(), sourceEntity.getId(), message.getScenicId(), message.getDeviceId());
return true;
} else {
log.error("保存ZT-Source照片素材失败: sourceId={}", message.getSourceId());
return false;
}
} catch (Exception e) {
log.error("处理ZT-Source消息异常: sourceId={}, error={}",
message.getSourceId(), e.getMessage(), e);
return false;
}
}
/**
* 校验消息必要字段
*/
private boolean validateMessage(ZTSourceMessage message) {
if (message.getScenicId() == null) {
log.warn("scenicId不能为空: sourceId={}", message.getSourceId());
return false;
}
if (message.getDeviceId() == null) {
log.warn("deviceId不能为空: sourceId={}", message.getSourceId());
return false;
}
if (message.getSourceUrl() == null || message.getSourceUrl().trim().isEmpty()) {
log.warn("sourceUrl不能为空: sourceId={}", message.getSourceId());
return false;
}
return true;
}
/**
* 将ZTSourceMessage转换为SourceEntity
*/
private SourceEntity convertToSourceEntity(ZTSourceMessage message) {
SourceEntity entity = new SourceEntity();
// 生成ID
entity.setId(SnowFlakeUtil.getLongId());
// 基本字段映射
entity.setScenicId(message.getScenicId());
entity.setDeviceId(message.getDeviceId());
entity.setUrl(message.getSourceUrl()); // 使用sourceUrl,不使用缩略图
entity.setType(2); // 照片类型
// 人脸样本ID处理
entity.setFaceSampleId(message.getFaceSampleId());
// 位置信息JSON处理
entity.setPosJson(message.getPosJson());
// 时间处理
Date shootTime = message.getShootTime();
if (shootTime != null) {
entity.setCreateTime(shootTime);
} else {
entity.setCreateTime(new Date());
}
log.debug("转换ZTSourceMessage到SourceEntity: sourceId={} -> entityId={}",
message.getSourceId(), entity.getId());
return entity;
}
}

View File

@@ -72,6 +72,7 @@ import java.io.File;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
@@ -80,6 +81,7 @@ import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static com.ycwl.basic.constant.FaceConstant.FACE_LOW_THRESHOLD_PFX;
import static com.ycwl.basic.constant.FaceConstant.FACE_RECOGNITION_COUNT_PFX;
@@ -489,7 +491,21 @@ public class FaceServiceImpl implements FaceService {
return Collections.emptyList();
}
return sourceEntities.stream().map(sourceEntity -> {
List<SourceEntity> filteredSourceEntities = sourceEntities.stream()
.sorted(Comparator.comparing(SourceEntity::getCreateTime).reversed())
.collect(Collectors.groupingBy(SourceEntity::getDeviceId))
.entrySet()
.stream().flatMap(entry -> {
DeviceConfigManager configManager = deviceRepository.getDeviceConfigManager(entry.getKey());
if (configManager.getInteger("limit_video", 0) > 0) {
return Stream.concat(
entry.getValue().stream().filter(item -> item.getType() == 2),
entry.getValue().stream().filter(item -> item.getType() == 1).limit(Math.min(entry.getValue().size(), configManager.getInteger("limit_video", 0)))
);
}
return entry.getValue().stream();
}).toList();
return filteredSourceEntities.stream().map(sourceEntity -> {
DeviceConfigManager deviceConfig = deviceRepository.getDeviceConfigManager(sourceEntity.getDeviceId());
MemberSourceEntity memberSourceEntity = new MemberSourceEntity();
memberSourceEntity.setScenicId(face.getScenicId());
@@ -627,13 +643,31 @@ public class FaceServiceImpl implements FaceService {
.filter(item -> Integer.valueOf(2).equals(item.getType()))
.count();
List<FaceSampleEntity> faceSampleList = faceRepository.getFaceSampleList(faceId);
if (faceSampleList.isEmpty()) {
log.info("faceId:{} sample list not exist", faceId);
return;
}
List<Long> faceSampleIds = faceSampleList.stream()
.sorted(Comparator.comparing(FaceSampleEntity::getCreateAt).reversed())
.collect(Collectors.groupingBy(FaceSampleEntity::getDeviceId))
.entrySet()
.stream().flatMap(entry -> {
DeviceConfigManager configManager = deviceRepository.getDeviceConfigManager(entry.getKey());
if (configManager.getInteger("limit_video", 0) > 0) {
return entry.getValue().subList(0, Math.min(entry.getValue().size(), configManager.getInteger("limit_video", 0))).stream();
}
return entry.getValue().stream();
}).toList()
.stream().map(FaceSampleEntity::getId).toList();
log.info("视频切分任务: faceId={}, 原始数量={}, 筛选后数量={}", faceId, faceSampleList.size(), faceSampleIds.size());
log.debug("视频重切逻辑:视频数量 {}, 照片数量 {}", videoCount, photoCount);
// 只有照片数量大于视频数量时才创建重切任务
if (photoCount > videoCount) {
VideoPieceGetter.Task task = new VideoPieceGetter.Task();
task.faceId = faceId;
task.faceSampleIds = sampleListIds;
task.faceSampleIds = faceSampleIds;
task.templateId = null;
task.memberId = memberId;
task.callback = () -> {
@@ -725,6 +759,9 @@ public class FaceServiceImpl implements FaceService {
}
}
boolean buy = orderBiz.checkUserBuyItem(userId, contentPageVO.getGoodsType(), contentPageVO.getContentId());
if (!buy) {
buy = orderBiz.checkUserBuyItem(userId, -1, contentPageVO.getTemplateId());
}
if (buy) {
contentPageVO.setIsBuy(1);
} else {
@@ -933,11 +970,11 @@ public class FaceServiceImpl implements FaceService {
statusResp.setStep3Status(true);
statusResp.setDisplayText("帧途AI已为您渲染"+ taskStatusByFaceId.getCount() +"个vlog");
} else {
statusResp.setStep3Status(true);
statusResp.setStep3Status(false);
statusResp.setDisplayText("帧途AI将会为您渲染vlog,请稍候");
}
} else {
statusResp.setStep3Status(true);
statusResp.setStep3Status(false);
statusResp.setDisplayText("帧途AI正在为您渲染vlog,请稍候");
}
return statusResp;

View File

@@ -48,7 +48,7 @@ public interface PrinterService {
PriceObj queryPrice(Long memberId, Long scenicId);
boolean addUserPhotoFromSource(Long memberId, Long scenicId, FromSourceReq req);
List<Integer> addUserPhotoFromSource(Long memberId, Long scenicId, FromSourceReq req);
Map<String, Object> createOrder(Long memberId, Long scenicId, Integer printerId);

View File

@@ -19,6 +19,7 @@ import com.ycwl.basic.pricing.dto.PriceCalculationResult;
import com.ycwl.basic.pricing.dto.ProductItem;
import com.ycwl.basic.pricing.enums.ProductType;
import com.ycwl.basic.pricing.service.IPriceCalculationService;
import com.ycwl.basic.model.pc.printer.entity.MemberPrintEntity;
import com.ycwl.basic.model.pc.printer.entity.PrintTaskEntity;
import com.ycwl.basic.model.pc.printer.entity.PrinterEntity;
import com.ycwl.basic.model.pc.printer.resp.MemberPrintResp;
@@ -39,16 +40,19 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Slf4j
@@ -188,7 +192,13 @@ public class PrinterServiceImpl implements PrinterService {
@Override
public boolean addUserPhoto(Long memberId, Long scenicId, String url) {
printerMapper.addUserPhoto(memberId, scenicId, url);
MemberPrintEntity entity = new MemberPrintEntity();
entity.setMemberId(memberId);
entity.setScenicId(scenicId);
entity.setOrigUrl(url);
entity.setCropUrl(url);
entity.setStatus(0);
printerMapper.addUserPhoto(entity);
return true;
}
@@ -257,15 +267,34 @@ public class PrinterServiceImpl implements PrinterService {
}
@Override
public boolean addUserPhotoFromSource(Long memberId, Long scenicId, FromSourceReq req) {
public List<Integer> addUserPhotoFromSource(Long memberId, Long scenicId, FromSourceReq req) {
List<Integer> resultIds = new ArrayList<>();
req.getIds().forEach(id -> {
SourceRespVO byId = sourceMapper.getById(id);
if (byId == null) {
resultIds.add(null);
return;
}
printerMapper.addUserPhoto(memberId, scenicId, byId.getUrl());
MemberPrintEntity entity = new MemberPrintEntity();
entity.setMemberId(memberId);
entity.setScenicId(scenicId);
entity.setOrigUrl(byId.getUrl());
entity.setCropUrl(byId.getUrl());
entity.setStatus(0);
try {
int rows = printerMapper.addUserPhoto(entity);
if (rows > 0 && entity.getId() != null) {
resultIds.add(entity.getId());
} else {
resultIds.add(null);
}
} catch (Exception e) {
log.error("添加用户照片失败, memberId={}, scenicId={}, sourceId={}", memberId, scenicId, id, e);
resultIds.add(null);
}
});
return false;
return resultIds;
}
@Override
@@ -377,8 +406,16 @@ public class PrinterServiceImpl implements PrinterService {
printerMapper.updateUserPhotoListToPrinter(memberId, scenicId, printerId);
}
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String USER_PHOTO_LIST_TO_PRINTER = "USER_PHOTO_LIST_TO_PRINTER:";
@Override
public void setUserIsBuyItem(Long memberId, Long id, Long orderId) {
if (redisTemplate.opsForValue().get(USER_PHOTO_LIST_TO_PRINTER + memberId + ":" + orderId) != null) {
return;
}
redisTemplate.opsForValue().set(USER_PHOTO_LIST_TO_PRINTER + memberId + ":" + orderId, "1", 60, TimeUnit.SECONDS);
printerMapper.setUserIsBuyItem(memberId, id, orderId);
// 创建打印任务
List<MemberPrintResp> userPhotoListByOrderId = getUserPhotoListByOrderId(orderId);

View File

@@ -49,6 +49,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -164,9 +165,23 @@ public class TaskFaceServiceImpl implements TaskFaceService {
log.warn("没有有效的关联关系可创建: faceId={}, 原始数量={}", faceId, memberSourceEntityList.size());
}
memberRelationRepository.clearSCacheByFace(faceId);
List<FaceSampleEntity> faceSampleList = faceRepository.getFaceSampleList(faceId);
List<Long> faceSampleIds = faceSampleList.stream()
.sorted(Comparator.comparing(FaceSampleEntity::getCreateAt).reversed())
.collect(Collectors.groupingBy(FaceSampleEntity::getDeviceId))
.entrySet()
.stream().flatMap(entry -> {
DeviceConfigManager configManager = deviceRepository.getDeviceConfigManager(entry.getKey());
if (configManager.getInteger("limit_video", 0) > 0) {
return entry.getValue().subList(0, Math.min(entry.getValue().size(), configManager.getInteger("limit_video", 0))).stream();
}
return entry.getValue().stream();
}).toList()
.stream().map(FaceSampleEntity::getId).toList();
log.info("视频切分任务: faceId={}, 原始数量={}, 筛选后数量={}", faceId, sampleListIds.size(), faceSampleIds.size());
VideoPieceGetter.Task task = new VideoPieceGetter.Task();
task.faceId = faceEntity.getId();
task.faceSampleIds = sampleListIds;
task.faceSampleIds = faceSampleIds;
task.memberId = face.getMemberId();
VideoPieceGetter.addTask(task);
}
@@ -237,11 +252,18 @@ public class TaskFaceServiceImpl implements TaskFaceService {
.anyMatch(record -> record.getScore() > _lowThreshold);
respVo.setLowThreshold(isLowThreshold);
allFaceSampleIds = records.stream()
.sorted(Comparator.comparing(SearchFaceResultItem::getScore).reversed())
.map(SearchFaceResultItem::getExtData)
.filter(StringUtils::isNumeric)
.map(Long::valueOf)
.collect(Collectors.toList());
List<FaceSampleEntity> allFaceSampleList = faceSampleMapper.listByIds(allFaceSampleIds);
// 按照allFaceSampleIds的顺序对allFaceSampleList进行排序
Map<Long, Integer> idIndexMap = new HashMap<>();
for (int i = 0; i < allFaceSampleIds.size(); i++) {
idIndexMap.put(allFaceSampleIds.get(i), i);
}
allFaceSampleList.sort(Comparator.comparing(sample -> idIndexMap.get(sample.getId())));
acceptFaceSampleIds = applySampleFilters(acceptFaceSampleIds, allFaceSampleList, scenicConfig);
List<MatchLocalRecord> collect = new ArrayList<>();
for (SearchFaceResultItem item : records) {
@@ -501,9 +523,8 @@ public class TaskFaceServiceImpl implements TaskFaceService {
log.debug("设备照片限制:设备ID={}, 无限制,保留{}张照片",
deviceId, deviceSampleIds.size());
} else {
// 按创建时间倒序排序,取前N张
// 取前N张
List<FaceSampleEntity> limitedSamples = deviceSamples.stream()
.sorted(Comparator.comparing(FaceSampleEntity::getCreateAt).reversed())
.limit(limitPhoto)
.collect(Collectors.toList());

View File

@@ -69,6 +69,7 @@ import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
@@ -255,7 +256,7 @@ public class TaskTaskServiceImpl implements TaskService {
@Override
public void autoCreateTaskByFaceId(Long faceId) {
FaceRespVO faceRespVO = faceMapper.getById(faceId);
FaceEntity faceRespVO = faceRepository.getFace(faceId);
if (faceRespVO == null) {
log.info("faceId:{} is not exist", faceId);
return;
@@ -264,18 +265,30 @@ public class TaskTaskServiceImpl implements TaskService {
log.info("faceId:{} matchSampleIds is empty", faceId);
return;
}
List<FaceSampleEntity> faceSampleList = faceSampleMapper.listByIds(Arrays.stream(faceRespVO.getMatchSampleIds().split(",")).filter(StringUtils::isNumeric).map(Long::valueOf).collect(Collectors.toList()));
List<FaceSampleEntity> faceSampleList = faceRepository.getFaceSampleList(faceId);
if (faceSampleList.isEmpty()) {
log.info("faceId:{} faceSampleList is empty", faceId);
return;
}
List<Long> faceSampleIds = faceSampleList.stream()
.sorted(Comparator.comparing(FaceSampleEntity::getCreateAt).reversed())
.collect(Collectors.groupingBy(FaceSampleEntity::getDeviceId)).entrySet()
.stream().flatMap(entry -> {
DeviceConfigManager configManager = deviceRepository.getDeviceConfigManager(entry.getKey());
if (configManager.getInteger("limit_video", 0) > 0) {
return entry.getValue().subList(0, Math.min(entry.getValue().size(), configManager.getInteger("limit_video", 0))).stream();
}
return entry.getValue().stream();
}).toList()
.stream().map(FaceSampleEntity::getId).toList();
log.info("视频切分任务: faceId={}, 原始数量={}, 筛选后数量={}", faceId, faceSampleList.size(), faceSampleIds.size());
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(faceRespVO.getScenicId());
List<TemplateRespVO> templateList = templateRepository.getTemplateListByScenicId(faceRespVO.getScenicId());
if (templateList == null || templateList.isEmpty()) {
// 没有vlog视频的情况下
VideoPieceGetter.Task task = new VideoPieceGetter.Task();
task.faceId = faceId;
task.faceSampleIds = faceSampleList.stream().map(FaceSampleEntity::getId).toList();
task.faceSampleIds = faceSampleIds;
task.templateId = null;
task.memberId = faceRespVO.getMemberId();
task.callback = () -> {
@@ -334,7 +347,9 @@ public class TaskTaskServiceImpl implements TaskService {
}
}
List<Long> faceSampleIds = faceSampleList.stream().collect(Collectors.groupingBy(FaceSampleEntity::getDeviceId)).entrySet()
List<Long> faceSampleIds = faceSampleList.stream()
.sorted(Comparator.comparing(FaceSampleEntity::getCreateAt).reversed())
.collect(Collectors.groupingBy(FaceSampleEntity::getDeviceId)).entrySet()
.stream().flatMap(entry -> {
DeviceConfigManager configManager = deviceRepository.getDeviceConfigManager(entry.getKey());
if (configManager.getInteger("limit_video", 0) > 0) {
@@ -343,6 +358,7 @@ public class TaskTaskServiceImpl implements TaskService {
return entry.getValue().stream();
}).toList()
.stream().map(FaceSampleEntity::getId).collect(Collectors.toList());
log.info("视频切分任务: faceId={}, 原始数量={}, 筛选后数量={}", faceId, faceSampleList.size(), faceSampleIds.size());
List<SourceEntity> sourceList = sourceMapper.listVideoByScenicFaceRelation(face.getScenicId(), faceId);
VideoPieceGetter.Task task = new VideoPieceGetter.Task();
task.faceId = faceId;
@@ -441,11 +457,7 @@ public class TaskTaskServiceImpl implements TaskService {
taskStatusBiz.setFaceCutStatus(faceId, 2);
}
};
if (!sourceList.isEmpty()) {
task.callback.onInvoke();
} else {
VideoPieceGetter.addTask(task);
}
VideoPieceGetter.addTask(task);
}
@Override

View File

@@ -53,6 +53,7 @@ import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@@ -145,7 +146,7 @@ public class VideoPieceGetter {
new ArrayBlockingQueue<>(128),
threadFactory
);
List<String> currentUnFinPlaceholder = new ArrayList<>();
Map<String, AtomicInteger> currentUnFinPlaceholder = new ConcurrentHashMap<>();
List<FaceSampleEntity> list = faceSampleMapper.listByIds(task.getFaceSampleIds());
Map<Long, Long> pairDeviceMap = new ConcurrentHashMap<>();
if (!list.isEmpty()) {
@@ -169,13 +170,21 @@ public class VideoPieceGetter {
})
.collect(Collectors.groupingBy(FaceSampleEntity::getDeviceId));
if (templatePlaceholder != null) {
IntStream.range(0, templatePlaceholder.size()).forEach(i -> {
currentUnFinPlaceholder.add(templatePlaceholder.get(i));
templatePlaceholder.forEach(deviceId -> {
currentUnFinPlaceholder.computeIfAbsent(deviceId, k -> new AtomicInteger(0)).incrementAndGet();
});
log.info("[Placeholder初始化] 有templateId,初始化完成:placeholder总数={}, 不同设备数={}, 详细计数={}",
templatePlaceholder.size(),
currentUnFinPlaceholder.size(),
currentUnFinPlaceholder.entrySet().stream()
.map(e -> e.getKey() + "=" + e.getValue().get())
.collect(Collectors.joining(", ")));
} else {
collection.keySet().forEach(i -> {
currentUnFinPlaceholder.add(i.toString());
collection.keySet().forEach(deviceId -> {
currentUnFinPlaceholder.put(deviceId.toString(), new AtomicInteger(1));
});
log.info("[Placeholder初始化] 无templateId,初始化完成:设备数={}",
currentUnFinPlaceholder.size());
}
collection.values().forEach(faceSampleList -> {
executor.execute(() -> {
@@ -188,27 +197,61 @@ public class VideoPieceGetter {
}
}
isFirst.set(false);
// 处理关联设备:如果当前设备是某个主设备的配对设备,也处理主设备
if (pairDeviceMap.containsValue(faceSample.getDeviceId())) {
// 有关联设备!
// 找到对应的deviceId
pairDeviceMap.entrySet().stream()
.filter(entry -> entry.getValue().equals(faceSample.getDeviceId()))
.map(Map.Entry::getKey).forEach(pairDeviceId -> {
log.info("找到同景区关联设备:{} -> {}", pairDeviceId, faceSample.getDeviceId());
if (pairDeviceId != null) {
doCut(pairDeviceId, faceSample.getId(), faceSample.getCreateAt(), task);
currentUnFinPlaceholder.remove(faceSample.getDeviceId().toString());
// 让主设备的计数器 -1
AtomicInteger pairCount = currentUnFinPlaceholder.get(pairDeviceId.toString());
if (pairCount != null) {
int remaining = pairCount.decrementAndGet();
log.info("[计数器更新] 关联设备 {} 计数器递减,剩余={}, currentUnFinPlaceholder总数={}",
pairDeviceId, remaining, currentUnFinPlaceholder.size());
if (remaining <= 0) {
currentUnFinPlaceholder.remove(pairDeviceId.toString());
log.info("[Placeholder完成] 设备 {} 的placeholder已满足并移除,剩余设备数={}",
pairDeviceId, currentUnFinPlaceholder.size());
}
}
}
});
}
// 处理当前设备
doCut(faceSample.getDeviceId(), faceSample.getId(), faceSample.getCreateAt(), task);
currentUnFinPlaceholder.remove(faceSample.getDeviceId().toString());
AtomicInteger count = currentUnFinPlaceholder.get(faceSample.getDeviceId().toString());
if (count != null) {
int remaining = count.decrementAndGet();
log.info("[计数器更新] 设备 {} 计数器递减,剩余={}, currentUnFinPlaceholder总数={}",
faceSample.getDeviceId(), remaining, currentUnFinPlaceholder.size());
if (remaining <= 0) {
currentUnFinPlaceholder.remove(faceSample.getDeviceId().toString());
log.info("[Placeholder完成] 设备 {} 的placeholder已满足并移除,剩余设备数={}",
faceSample.getDeviceId(), currentUnFinPlaceholder.size());
}
}
// 如果有templateId,检查是否所有placeholder都已满足
if (templatePlaceholder != null) {
log.info("当前进度:!{}/{}", currentUnFinPlaceholder.size(), templatePlaceholder.size());
int totalPlaceholderCount = templatePlaceholder.size();
int remainingCount = currentUnFinPlaceholder.values().stream()
.mapToInt(AtomicInteger::get)
.sum();
log.info("[进度检查] 当前进度:已完成 {}/{},剩余 {} 个placeholder未满足,剩余设备数={}",
totalPlaceholderCount - remainingCount, totalPlaceholderCount, remainingCount,
currentUnFinPlaceholder.size());
if (currentUnFinPlaceholder.isEmpty()) {
if (!invoke.get()) {
invoke.set(true);
log.info("[Callback调用] 所有placeholder已满足,currentUnFinPlaceholder为空,提前调用callback");
task.getCallback().onInvoke();
} else {
log.warn("[Callback跳过] 所有placeholder已满足,但callback已被调用过");
}
}
}
@@ -232,7 +275,11 @@ public class VideoPieceGetter {
if (null != task.getCallback()) {
if (!invoke.get()) {
invoke.set(true);
log.info("[Callback调用] 兜底调用callback,currentUnFinPlaceholder剩余设备数={}",
currentUnFinPlaceholder.size());
task.getCallback().onInvoke();
} else {
log.info("[Callback跳过] 兜底检查,callback已被调用过");
}
}
if (task.getFaceId() != null) {
@@ -299,7 +346,9 @@ public class VideoPieceGetter {
ffmpegTask.setFileList(listByDtRange);
ffmpegTask.setDuration(duration);
ffmpegTask.setOffsetStart(BigDecimal.valueOf(offset, 3));
File outFile = new File(deviceId.toString() + "_" + faceSampleId + ".mp4");
// 使用时间戳和线程ID确保输出文件名唯一性,避免并发冲突
String uniqueSuffix = System.currentTimeMillis() + "_" + Thread.currentThread().getId();
File outFile = new File(deviceId.toString() + "_" + faceSampleId + "_" + uniqueSuffix + ".mp4");
ffmpegTask.setOutputFile(outFile.getAbsolutePath());
boolean result = startFfmpegTask(ffmpegTask);
if (!result) {
@@ -318,6 +367,10 @@ public class VideoPieceGetter {
}
if (source == null) {
SourceEntity imgSource = sourceMapper.findBySampleId(faceSampleId);
if (imgSource == null) {
log.warn("imgSource为null,跳过保存source记录, faceSampleId: {}", faceSampleId);
return false;
}
SourceEntity sourceEntity = new SourceEntity();
sourceEntity.setId(SnowFlakeUtil.getLongId());
sourceEntity.setCreateTime(baseTime);
@@ -433,7 +486,9 @@ public class VideoPieceGetter {
boolean notOk = task.getFileList().stream().map(file -> {
try {
if (file.isNeedDownload() || (!file.getName().endsWith(".ts"))) {
String tmpFile = file.getName() + ".ts";
// 使用时间戳和线程ID确保临时文件名唯一性,避免并发冲突
String uniqueSuffix = System.currentTimeMillis() + "_" + Thread.currentThread().getId();
String tmpFile = file.getName() + "_" + uniqueSuffix + ".ts";
boolean result = convertMp4ToTs(file, tmpFile);
// 因为是并行转换,没法保证顺序,就直接存里面
if (result) {

View File

@@ -28,33 +28,53 @@ public class ImageUtils {
}
public static MultipartFile cropImage(MultipartFile file, int x, int y, int w, int h) throws IOException {
BufferedImage image = ImageIO.read(file.getInputStream());
log.info("图片宽高:{}", image.getWidth() + "x" + image.getHeight());
log.info("图片裁切:{}@{}", w + "x" + h, x + "," + y);
if (image.getWidth() < w) {
w = image.getWidth();
}
if (image.getHeight() < h) {
h = image.getHeight();
}
int targetX = x;
if (x < 0) {
targetX = 0;
} else if ((x + w) > image.getWidth()) {
targetX = image.getWidth() - w;
}
int targetY = y;
if (y < 0) {
targetY = 0;
} else if ((y + h) > image.getHeight()) {
targetY = image.getHeight() - h;
}
log.info("图片实际裁切:{}@{}", w + "x" + h, targetX + "," + targetY);
BufferedImage targetImage = image.getSubimage(targetX, targetY, w, h);
BufferedImage image = null;
BufferedImage targetImage = null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(targetImage, "jpg", baos);
baos.close();
return new Base64DecodedMultipartFile(baos.toByteArray(), "image/jpeg");
try {
image = ImageIO.read(file.getInputStream());
log.info("图片宽高:{}", image.getWidth() + "x" + image.getHeight());
log.info("图片裁切:{}@{}", w + "x" + h, x + "," + y);
if (image.getWidth() < w) {
w = image.getWidth();
}
if (image.getHeight() < h) {
h = image.getHeight();
}
int targetX = x;
if (x < 0) {
targetX = 0;
} else if ((x + w) > image.getWidth()) {
targetX = image.getWidth() - w;
}
int targetY = y;
if (y < 0) {
targetY = 0;
} else if ((y + h) > image.getHeight()) {
targetY = image.getHeight() - h;
}
log.info("图片实际裁切:{}@{}", w + "x" + h, targetX + "," + targetY);
targetImage = image.getSubimage(targetX, targetY, w, h);
ImageIO.write(targetImage, "jpg", baos);
return new Base64DecodedMultipartFile(baos.toByteArray(), "image/jpeg");
} finally {
// 修复内存泄漏:显式释放图片资源
if (image != null) {
image.flush();
image = null;
}
if (targetImage != null) {
targetImage.flush();
targetImage = null;
}
try {
baos.close();
} catch (IOException e) {
log.warn("关闭ByteArrayOutputStream失败", e);
}
// 建议JVM进行垃圾回收
System.gc();
}
}
public static class Base64DecodedMultipartFile implements MultipartFile {

View File

@@ -1,16 +1,20 @@
package com.ycwl.basic.watchdog;
import com.ycwl.basic.integration.message.dto.ZtMessage;
import com.ycwl.basic.integration.message.service.ZtMessageProducerService;
import com.ycwl.basic.mapper.TaskMapper;
import com.ycwl.basic.model.pc.task.entity.TaskEntity;
import com.ycwl.basic.notify.NotifyFactory;
import com.ycwl.basic.notify.entity.NotifyContent;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Profile;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@Component
@Profile("prod")
@@ -19,41 +23,145 @@ public class TaskWatchDog {
@Autowired
private TaskMapper taskMapper;
@Autowired
private ZtMessageProducerService ztMessageProducerService;
// 异常通知计数器
private final Map<String, Integer> notificationCounters = new HashMap<>();
// 配置参数
private static final int MAX_NOTIFICATION_COUNT = 3; // 每种异常最多通知3次
// 异常类型标识
private static final String TASK_BACKLOG = "task_backlog";
private static final String FAILED_TASKS = "failed_tasks";
private static final String LONG_RUNNING_TASK_PREFIX = "long_running_task_"; // 长时间运行任务前缀
@Scheduled(fixedDelay = 1000 * 60L)
public void scanTaskStatus() {
List<TaskEntity> allNotRunningTaskList = taskMapper.selectAllNotRunning();
String title = "任务堆积警告!";
StringBuilder content = new StringBuilder();
if (allNotRunningTaskList.size() > 10) {
content.append("当前任务队列中存在超过10个未运行任务,请及时处理!未运行任务数量:").append(allNotRunningTaskList.size());
}
List<TaskEntity> allFailedTaskList = taskMapper.selectAllFailed();
if (allFailedTaskList.size() > 5) {
if (content.length() > 0) {
content.append("\n");
}
content.append("当前存在超过5个失败任务(status=3),请及时检查和处理!失败任务数量:").append(allFailedTaskList.size());
}
List<TaskEntity> allRunningTaskList = taskMapper.selectAllRunning();
for (TaskEntity taskEntity : allRunningTaskList) {
// 检查任务积压
checkTaskBacklog(allNotRunningTaskList);
// 检查失败任务
checkFailedTasks(allFailedTaskList);
// 检查长时间运行任务
checkLongRunningTasks(allRunningTaskList);
}
/**
* 检查任务积压
*/
private void checkTaskBacklog(List<TaskEntity> notRunningTasks) {
if (notRunningTasks.size() > 10) {
if (shouldSendNotification(TASK_BACKLOG)) {
String content = String.format("当前任务队列中存在超过10个未运行任务,请及时处理!未运行任务数量:%d", notRunningTasks.size());
sendNotification("任务堆积警告", content, TASK_BACKLOG);
}
} else {
// 异常已恢复,重置计数器
resetNotificationCounter(TASK_BACKLOG);
}
}
/**
* 检查失败任务
*/
private void checkFailedTasks(List<TaskEntity> failedTasks) {
if (failedTasks.size() > 5) {
if (shouldSendNotification(FAILED_TASKS)) {
String content = String.format("当前存在超过5个失败任务(status=3),请及时检查和处理!失败任务数量:%d", failedTasks.size());
sendNotification("任务失败警告", content, FAILED_TASKS);
}
} else {
// 异常已恢复,重置计数器
resetNotificationCounter(FAILED_TASKS);
}
}
/**
* 检查长时间运行任务
*/
private void checkLongRunningTasks(List<TaskEntity> runningTasks) {
Set<String> currentLongRunningTasks = new HashSet<>();
for (TaskEntity taskEntity : runningTasks) {
if (taskEntity.getStartTime() == null) {
continue;
}
// startTime已经过去3分钟了
if (System.currentTimeMillis() - taskEntity.getStartTime().getTime() > 1000 * 60 * 3) {
if (content.length() > 0) {
content.append("\n");
String taskKey = LONG_RUNNING_TASK_PREFIX + taskEntity.getId();
currentLongRunningTasks.add(taskKey);
if (shouldSendNotification(taskKey)) {
String content = String.format("当前【%s】渲染机的【%d】任务已超过3分钟未完成!",
taskEntity.getWorkerId(), taskEntity.getId());
sendNotification("长时间运行任务警告", content, taskKey);
}
content.append("当前【").append(taskEntity.getWorkerId()).append("】渲染机的【").append(taskEntity.getId()).append("】任务已超过3分钟未完成!");
}
}
if (StringUtils.isNotBlank(content)) {
NotifyFactory.via().sendTo(
new NotifyContent(title, content.toString()),
"default_user"
);
// 清理已恢复正常的长时运行任务的计数器
cleanupLongRunningTaskCounters(currentLongRunningTasks);
}
/**
* 清理已恢复正常的长时运行任务的计数器
*/
private void cleanupLongRunningTaskCounters(Set<String> currentLongRunningTasks) {
Set<String> keysToRemove = new HashSet<>();
for (String key : notificationCounters.keySet()) {
if (key.startsWith(LONG_RUNNING_TASK_PREFIX)) {
if (!currentLongRunningTasks.contains(key)) {
keysToRemove.add(key);
}
}
}
// 移除已恢复任务的计数器
for (String key : keysToRemove) {
notificationCounters.remove(key);
}
}
/**
* 判断是否应该发送通知
*/
private boolean shouldSendNotification(String abnormalType) {
int count = notificationCounters.getOrDefault(abnormalType, 0);
return count < MAX_NOTIFICATION_COUNT;
}
/**
* 发送通知并更新计数器
*/
private void sendNotification(String title, String content, String abnormalType) {
ZtMessage ztMessage = ZtMessage.of(
"serverchan",
title,
content,
"system"
);
ztMessage.setSendReason("任务监控");
ztMessage.setSendBiz("系统监控");
ztMessageProducerService.send(ztMessage);
// 更新通知计数器
int currentCount = notificationCounters.getOrDefault(abnormalType, 0);
notificationCounters.put(abnormalType, currentCount + 1);
}
/**
* 重置通知计数器(异常恢复时调用)
*/
private void resetNotificationCounter(String abnormalType) {
notificationCounters.remove(abnormalType);
}
}

View File

@@ -95,7 +95,7 @@
NOW()
)
</insert>
<insert id="addUserPhoto">
<insert id="addUserPhoto" useGeneratedKeys="true" keyProperty="id">
INSERT INTO member_print (
member_id,
scenic_id,
@@ -108,8 +108,8 @@
) VALUES (
#{memberId},
#{scenicId},
#{url},
#{url},
#{origUrl},
#{cropUrl},
1,
0,
NOW(),

View File

@@ -5,6 +5,10 @@
insert into source(id, scenic_id, device_id, url, video_url, `type`, face_sample_id, pos_json, create_time)
values (#{id}, #{scenicId}, #{deviceId}, #{url}, #{videoUrl}, #{type}, #{faceSampleId}, #{posJson}, #{createTime})
</insert>
<insert id="addFromZTSource">
insert into source(id, scenic_id, device_id, url, `type`, face_sample_id, pos_json, create_time)
values (#{id}, #{scenicId}, #{deviceId}, #{url}, #{type}, #{faceSampleId}, #{posJson}, #{createTime})
</insert>
<insert id="addRelation">
replace member_source(scenic_id, face_id, member_id, source_id, is_buy, type, order_id<if test="isFree">, is_free</if>)
values (#{scenicId}, #{faceId}, #{memberId}, #{sourceId}, #{isBuy}, #{type}, #{orderId}<if test="isFree">, #{isFree}</if>)