Compare commits

..

67 Commits

Author SHA1 Message Date
a7ede3303d refactor(task): 移除重复的景区配置查询逻辑
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 删除了 DownloadNotificationTasker 中多次调用的 getScenicMpConfig 方法
- 简化了视频下载通知任务的执行流程- 提高代码可读性和维护性
- 避免不必要的数据库查询操作
2025-10-14 20:32:36 +08:00
aa7330000f fix(task): 避免重复发送下载和过期通知
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 在发送下载通知前检查用户是否已接收通知
- 在发送过期通知前检查用户是否已接收通知- 在发送额外下载通知前检查用户是否已接收通知
- 使用ConcurrentHashMap.newKeySet()确保线程安全- 添加调试日志以追踪重复通知的跳过情况- 优化通知逻辑以提升定时任务执行效率
2025-10-14 20:31:45 +08:00
29f4bbf2d8 feat(message): 添加ZT消息生产者空实现服务
- 创建 ZtMessageProducerNoOpService 类作为 Kafka 禁用时的替代实现- 实现 ConditionalOnProperty 注解,当 kafka.enabled=false 时激活该服务- 覆写 send 方法,仅记录日志而不实际发送消息
- 添加构造函数以满足父类依赖要求
- 提供详细注释说明服务用途和实现逻辑
2025-10-14 20:28:00 +08:00
ad42254ea0 refactor(task): 移除通知模块依赖
- 删除了对通知模块的包引用
- 移除了通知模块相关的类导入- 清理了与通知功能相关的代码依赖
-优化了任务服务实现类的依赖结构
- 简化了下载通知任务器的代码引用
- 解除了通知工厂类的直接依赖关系
2025-10-14 19:38:47 +08:00
0ceecf0488 fix(message): 将消息相关接口的日志级别从 info 调整为 debug
- 修改消息列表查询接口的日志级别- 修改获取消息通道列表接口的日志级别- 统一调整日志输出方式以减少生产环境日志量
2025-10-14 19:20:41 +08:00
311008cbf2 feat(message): 集成ZT消息服务发送通知
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 在TaskTaskServiceImpl中引入ZtMessageProducerService依赖
- 替换原有微信通知逻辑,使用ZT消息服务发送视频生成通知- 在DownloadNotificationTasker中引入ZtMessageProducerService依赖
- 修改视频下载通知发送逻辑,使用ZT消息服务
- 修改视频过期提醒通知逻辑,使用ZT消息服务
- 调整额外通知时间配置获取方式,从scenicConfigManager获取
- 统一构建通知消息参数格式,包含data和page信息
- 添加详细的日志记录,便于追踪消息发送过程
2025-10-14 19:06:30 +08:00
f54d40d026 feat(message):为消息添加唯一标识符支持
- 在 ZtMessage DTO 中新增 messageId 字段
- 发送消息前自动生成 UUID 作为默认 messageId
- 更新 Kafka 生产者日志,包含 messageId 以便追踪
- 增强错误日志记录,附带 messageId 提升调试效率
2025-10-14 18:27:15 +08:00
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
4a05773860 fix(device): 添加空值检查避免空指针异常- 在设置设备在线状态时添加对 lastActiveTime 和 clientIP 的空值检查
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 在判断设备是否在线时,增加对 keepaliveAt 时间的空值判断
- 防止因空值导致的 NullPointerException 异常- 提高代码健壮性和稳定性
2025-09-25 15:52:16 +08:00
3c700a42f9 feat(device): 添加设备在线状态查询功能- 在DeviceV2Controller中新增getDeviceOnlineStatus接口,用于根据设备ID查询设备在线状态
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 引入DeviceStatusDTO和DeviceStatusIntegrationService以支持设备状态查询- 修改DeviceStatusDTO中的时间字段类型为Date,并调整JSON序列化格式- 在DeviceRepository中增加convertToEntityWithStatus方法,用于合并设备信息与状态信息
- 优化DeviceRepository中的getOnlineStatus方法,增加异常处理和降级机制- 完善设备在线状态查询的日志记录和错误处理逻辑
2025-09-25 15:32:09 +08:00
47c6b2ca67 feat(device): 新增设备状态管理集成服务
- 添加设备状态客户端接口,支持设备在线状态查询与设置
- 创建设备状态相关 DTO,包括设备状态、在线状态和状态动作枚举
- 实现设备状态集成服务,封装设备状态操作与异常处理逻辑
- 支持单个及批量设备在线状态检查与设置功能
- 提供
2025-09-25 14:18:06 +08:00
59baf8811b feat(pricing): 添加商品一口价优惠支持检查
- 在 PriceProductConfig 实体中新增 canUseOnePrice 字段
- 更新数据库插入和更新语句,支持 canUseOnePrice 字段持久化- 在 OnePricePurchaseDiscountProvider 中实现商品一口价优惠支持检查逻辑
- 新增 areAllProductsSupportOnePrice 方法,验证购物车商品是否支持一口价优惠
- 支持查询具体商品配置和默认配置的一口价优惠设置
- 添加日志记录和异常处理,确保检查过程不影响主流程
2025-09-25 10:40:10 +08:00
019b9ffca6 refactor(video):优化视频关联关系处理逻辑
- 调整source记录插入时机,确保关联关系处理前数据已存在
- 移除冗余的source存在性检查逻辑- 统一关联关系处理流程,避免重复代码
- 添加日志记录以便追踪处理过程- 优化代码结构,提高可读性和维护性
2025-09-24 18:04:47 +08:00
30805f3e30 refactor(mapper):优化查询逻辑并处理空列表情况
- 将 filterExistingRelations 查询中的 if 判断替换为 choose-when 结构
- 在 otherwise 分支中添加空结果集查询,避免空列表时 SQL 异常- 统一 filterValidSourceRelations 查询结构,增强代码一致性
-修正 foreach 标签中 UNION ALL 前后的空格问题,确保 SQL 语法正确- 提升 XML 映射文件的可读性和健壮性
2025-09-24 17:50:53 +08:00
94d6b2f443 feat(source): 增强source关联关系的数据一致性校验
- 在SourceMapper中新增sourceExists方法,用于校验source是否存在
- 新增filterValidSourceRelations方法,过滤无效的source引用
- 在FaceServiceImpl中增强关联关系创建逻辑,防止重复和无效数据
- 在VideoPieceGetter任务中增加source存在性校验,避免创建孤立关联- 添加详细的日志记录,便于追踪关联关系创建过程
-优化XML映射文件,支持新的校验和过滤查询逻辑
2025-09-24 17:39:05 +08:00
b34f994298 feat(source): 添加过滤已存在关联关系功能
- 在SourceMapper中新增filterExistingRelations方法
- 修改FaceServiceImpl中的关联关系保存逻辑
- 修改TaskFaceServiceImpl中的关联关系保存逻辑
- 修改VideoPieceGetter中的关联关系检查逻辑
- 在SourceMapper.xml中添加filterExistingRelations的SQL实现
2025-09-24 17:16:12 +08:00
7728f4424f status 2025-09-24 13:45:48 +08:00
becbe5f6ab 允许重复
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
2025-09-24 05:03:47 +08:00
dc3a46362b Merge branch 'kafka_face_sample' 2025-09-24 05:03:04 +08:00
a361b59d74 配置
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
2025-09-23 20:57:01 +08:00
f779b0e040 计算 2025-09-23 20:53:22 +08:00
78c4548d02 文字 2025-09-23 17:54:49 +08:00
842310f73c ignore 2025-09-23 14:34:32 +08:00
cf235d38bb feat(模板): 为模板查找方法添加scanSource参数
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
在findFirstAvailableTemplate方法中新增scanSource参数,用于控制模板生成时的来源检查逻辑。调用方TaskTaskServiceImpl在强制创建vlog时传入false以跳过来源检查。
2025-09-23 13:50:26 +08:00
8903818cb0 订单详情 2025-09-23 12:21:34 +08:00
ae0cf56216 content返回url 2025-09-23 10:40:04 +08:00
90b6f53986 兜底1个 2025-09-23 10:38:23 +08:00
80b4508211 docs 2025-09-23 10:07:14 +08:00
57b8d90d5e 名称 2025-09-23 10:04:05 +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
dc2154c020 feat(integration): 添加 Kafka 消息系统集成
- 新增 Kafka 配置和连接测试功能- 实现人脸处理消息的消费逻辑
- 添加消息发送预留接口
- 优化人脸样本保存和处理流程
2025-09-12 06:38:44 +08:00
99 changed files with 2742 additions and 3108 deletions

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@ target/
.serena .serena
.claude .claude
.vscode .vscode
*.jpg

40
AGENTS.md Normal file
View File

@@ -0,0 +1,40 @@
# Repository Guidelines
## Project Structure & Module Organization
- Application code: `src/main/java/com/ycwl/basic/**` (controllers, services, mapper/repository, dto/model, config, util).
- Resources: `src/main/resources/**` (Spring configs, `mapper/*.xml`, static assets, logging).
- Tests: `src/test/java/**` mirrors main packages.
- Build output: `target/` (never commit).
## Build, Test, and Development Commands
- Build artifact: `mvn clean package` (tests are skipped by default via `pom.xml`).
- Run locally (dev): `mvn spring-boot:run -Dspring-boot.run.profiles=dev`.
- Run jar: `java -jar target/basic21-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev`.
- Execute tests: `mvn -DskipTests=false test` (note: `pom.xml` excludes `**/*Test.java` from test-compile; temporarily remove/override that config if you need to compile and run tests).
## Coding Style & Naming Conventions
- Java 21. Use 4-space indentation; UTF-8; no wildcard imports.
- Packages: `com.ycwl.basic.*`; classes PascalCase; methods/fields camelCase; constants UPPER_SNAKE_CASE.
- Controllers in `controller`, business logic in `service`, persistence in `mapper` + `resources/mapper/*.xml`.
- Prefer Lombok for boilerplate and constructor injection where applicable.
## Testing Guidelines
- Framework: Spring Boot testing + JUnit (see `spring-boot-starter-test`).
- Test names end with `Test` or `Tests` and mirror package structure.
- Aim to cover service/util layers and critical controllers. No enforced coverage target.
- To enable tests locally, remove/override the `maven-compiler-plugin` `testExcludes` in `pom.xml` and run `mvn -DskipTests=false test`.
## Commit & Pull Request Guidelines
- Follow Conventional Commits: `feat(scope): summary`, `fix(scope): summary`, `refactor: ...`.
- Reference issues (e.g., `#123`) and include brief rationale and screenshots for UI-facing changes.
- Keep PRs focused; include run/build instructions and any config changes.
## Security & Configuration Tips
- Profiles: `application.yml` and `bootstrap.yml` with `-dev`/`-prod` variants. Select via `--spring.profiles.active`.
- Do not commit secrets. Provide Nacos, Redis, MySQL, OSS/S3, and 3rd‑party keys via environment or secure config.
- Review `logback-spring*.xml` before raising log levels in production.
## Agent-Specific Notes
- Keep changes minimal and within existing package boundaries.
- Do not reorganize MyBatis XML names or mapper interfaces without updating both sides.
- If altering APIs, update affected tests and documentation in the same PR.

567
CLAUDE.md
View File

@@ -1,527 +1,40 @@
# CLAUDE.md # Repository Guidelines
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project Structure & Module Organization
- Application code: `src/main/java/com/ycwl/basic/**` (controllers, services, mapper/repository, dto/model, config, util).
## 构建和开发命令 - Resources: `src/main/resources/**` (Spring configs, `mapper/*.xml`, static assets, logging).
- Tests: `src/test/java/**` mirrors main packages.
### 构建应用程序 - Build output: `target/` (never commit).
```bash
# 清理构建(默认跳过测试) ## Build, Test, and Development Commands
mvn clean package - Build artifact: `mvn clean package` (tests are skipped by default via `pom.xml`).
- Run locally (dev): `mvn spring-boot:run -Dspring-boot.run.profiles=dev`.
# 清理构建并执行测试 - Run jar: `java -jar target/basic21-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev`.
mvn clean package -DskipTests=false - Execute tests: `mvn -DskipTests=false test` (note: `pom.xml` excludes `**/*Test.java` from test-compile; temporarily remove/override that config if you need to compile and run tests).
# 运行应用程序 ## Coding Style & Naming Conventions
mvn spring-boot:run - Java 21. Use 4-space indentation; UTF-8; no wildcard imports.
``` - Packages: `com.ycwl.basic.*`; classes PascalCase; methods/fields camelCase; constants UPPER_SNAKE_CASE.
- Controllers in `controller`, business logic in `service`, persistence in `mapper` + `resources/mapper/*.xml`.
### 测试命令 - Prefer Lombok for boilerplate and constructor injection where applicable.
```bash
# 运行特定测试类 ## Testing Guidelines
mvn test -Dtest=FaceCleanerTest - Framework: Spring Boot testing + JUnit (see `spring-boot-starter-test`).
- Test names end with `Test` or `Tests` and mirror package structure.
# 运行特定测试方法 - Aim to cover service/util layers and critical controllers. No enforced coverage target.
mvn test -Dtest=FaceCleanerTest#testSpecificMethod - To enable tests locally, remove/override the `maven-compiler-plugin` `testExcludes` in `pom.xml` and run `mvn -DskipTests=false test`.
# 运行特定包的测试 ## Commit & Pull Request Guidelines
mvn test -Dtest="com.ycwl.basic.storage.adapters.*Test" - Follow Conventional Commits: `feat(scope): summary`, `fix(scope): summary`, `refactor: ...`.
- Reference issues (e.g., `#123`) and include brief rationale and screenshots for UI-facing changes.
# 运行pricing模块测试 - Keep PRs focused; include run/build instructions and any config changes.
mvn test -Dtest="com.ycwl.basic.pricing.*Test"
## Security & Configuration Tips
# 运行所有测试 - Profiles: `application.yml` and `bootstrap.yml` with `-dev`/`-prod` variants. Select via `--spring.profiles.active`.
mvn test -DskipTests=false - Do not commit secrets. Provide Nacos, Redis, MySQL, OSS/S3, and 3rd‑party keys via environment or secure config.
- Review `logback-spring*.xml` before raising log levels in production.
# 运行测试并生成详细报告
mvn test -DskipTests=false -Dsurefire.printSummary=true ## Agent-Specific Notes
``` - Keep changes minimal and within existing package boundaries.
- Do not reorganize MyBatis XML names or mapper interfaces without updating both sides.
### 开发环境配置 - If altering APIs, update affected tests and documentation in the same PR.
应用程序使用 Spring 配置文件:
- 默认激活配置文件:`dev`
- 生产环境配置文件:`prod`(启用定时任务)
- 配置文件:`application-dev.yml``application-prod.yml`
## 架构概览
这是一个 Spring Boot 3.3.5 应用程序(Java 21),采用多租户架构,通过不同的 API 端点为不同的客户端类型提供服务。
### 控制器架构
- **移动端 APIs** (`/api/mobile/`):面向移动应用的客户端端点
- **PC 端 APIs** (`/api/`):Web 仪表板/管理面板端点
- **任务 APIs** (`/task/`):后台工作和渲染任务端点
- **外部 APIs**:专用集成(打印机、代理、viid、vpt、wvp)
### 核心业务模块
#### 工厂模式实现
三个主要工厂类管理第三方集成:
1. **StorageFactory** (`com.ycwl.basic.storage.StorageFactory`)
- 管理:本地存储、AWS S3、阿里云 OSS 存储适配器
- 配置节:`storage.configs[]`
2. **PayFactory** (`com.ycwl.basic.pay.PayFactory`)
- 管理:微信支付、聪明支付适配器
- 配置节:`pay.configs[]`
3. **FaceBodyFactory** (`com.ycwl.basic.facebody.FaceBodyFactory`)
- 管理:阿里云、百度人脸识别适配器
- 配置节:`facebody.configs[]`
#### 适配器模式
每个工厂使用标准化接口:
- `IStorageAdapter`:文件操作(上传/下载/删除/ACL)
- `IPayAdapter`:支付生命周期(创建/回调/退款)
- `IFaceBodyAdapter`:人脸识别操作
#### 定时任务系统
`com.ycwl.basic.task` 包中的后台任务(仅生产环境):
- `VideoTaskGenerator`:人脸识别和视频处理
- `FaceCleaner`:人脸和存储清理任务
- `DynamicTaskGenerator`:带延迟队列的动态任务创建
- `ScenicStatsTask`:统计数据聚合
### 数据库和持久化
- **MyBatis Plus**:具有自动 CRUD 操作的 ORM
- **MapperScan**:扫描 `com.ycwl.basic.mapper` 及子包
- **数据库**:MySQL 配合 HikariCP 连接池
- **Redis**:会话管理和缓存
### 主要库和依赖
- Spring Boot 3.3.5 启用 Java 21 虚拟线程
- MyBatis Plus 3.5.5 用于数据库操作
- JWT (jjwt 0.9.0) 用于身份验证
- 微信支付 SDK 用于支付处理
- 阿里云 OSS 和 AWS S3 用于文件存储
- 阿里云和百度 SDK 用于人脸识别
- OpenTelemetry 用于可观测性(开发环境中禁用)
### 业务逻辑组织
- **Service 层**:`service` 包中的业务逻辑实现
- **Biz 层**:`biz` 包中的高级业务编排
- **Repository 模式**:`repository` 包中的数据访问抽象
- **自定义异常**:特定领域的异常处理
### 配置管理
每个模块使用 Spring Boot 自动配置启动器:
- 支持多供应商的命名配置
- 通过配置进行默认供应商选择
- 针对不同环境的特定配置文件
## 常见开发模式
### 添加新的存储/支付/人脸识别供应商
1. 实现相应接口(`IStorageAdapter``IPayAdapter``IFaceBodyAdapter`
2. 在相应的类型枚举中添加枚举值
3. 更新工厂的 switch 表达式
4. 如需要,添加配置类
5. 在 application.yml 中更新新供应商配置
### 身份验证上下文
在整个应用程序中使用 `BaseContextHandler.getUserId()` 获取当前已认证用户 ID。
### API 响应模式
所有 API 都返回 `ApiResponse<T>` 包装器,通过 `CustomExceptionHandle` 进行一致的错误处理。
### 添加新的定时任务
1.`com.ycwl.basic.task` 包中创建类
2. 添加 `@Component``@Profile("prod")` 注解
3. 使用 `@Scheduled` 进行基于 cron 的执行
4. 遵循现有的错误处理和日志记录模式
### 多端API架构
应用程序通过路径前缀区分不同的客户端:
- **移动端**: `/api/mobile/*` - 针对移动应用优化的接口
- **PC管理端**: `/api/*` - Web管理面板接口
- **任务处理**: `/task/*` - 后台任务和渲染服务接口
- **外部集成**: 专用集成接口(打印机、代理、viid、vpt、wvp等)
每个端点都有对应的Controller包结构,确保API的职责分离和维护性。
## 价格查询系统 (Pricing Module)
### 核心架构
价格查询系统是一个独立的业务模块,位于 `com.ycwl.basic.pricing` 包中,提供商品定价、优惠券管理、券码管理和统一优惠检测功能。
#### 关键组件
- **PriceCalculationController** (`/api/pricing/calculate`):统一价格计算API,支持自动优惠组合
- **CouponManagementController** (`/api/pricing/admin/coupons/`):优惠券配置和统计管理
- **VoucherManagementController** (`/api/pricing/voucher/`):券码批次和券码管理
- **VoucherUsageController** (`/api/pricing/voucher/usage/`):券码使用记录和统计
- **PricingConfigController** (`/api/pricing/config/`):商品价格配置管理
- **OnePricePurchaseController** (`/api/pricing/admin/one-price/`):一口价配置管理
#### 商品类型支持
```java
ProductType枚举定义了支持的商品类型
- VLOG_VIDEO: Vlog视频
- RECORDING_SET: 录像集
- PHOTO_SET: 照相集
- PHOTO_PRINT: 照片打印
- MACHINE_PRINT: 一体机打印
```
#### 价格计算流程(统一优惠检测)
1. 接收PriceCalculationRequest(包含商品列表、用户ID、券码等)
2. 查找商品基础配置和分层定价
3. 处理套餐商品(BundleProductItem)
4. **统一优惠检测**:通过IDiscountDetectionService自动检测所有可用优惠
- 券码优惠(VoucherDiscountProvider,优先级100)
- 优惠券优惠(CouponDiscountProvider,优先级80)
- 一口价优惠(OnePricePurchaseDiscountProvider,优先级60)
5. **智能优惠组合**:按优先级和叠加规则应用最优优惠组合
6. 返回PriceCalculationResult(包含原价、最终价格、使用的优惠详情、可用优惠列表)
#### 优惠券系统
- **CouponType**: PERCENTAGE(百分比)、FIXED_AMOUNT(固定金额)
- **CouponStatus**: CLAIMED(已领取)、USED(已使用)、EXPIRED(已过期)
- 支持商品类型限制 (`applicableProducts` JSON字段)
- 最小消费金额和最大折扣限制
- 时间有效期控制
#### 分页查询功能
所有管理接口都支持分页查询:
- **优惠券系统**:使用PageHelper实现
- 优惠券配置分页:支持按状态、名称筛选
- 领取记录分页:支持按用户、优惠券、状态、时间范围筛选
- **券码系统**:使用MyBatis-Plus Page实现
- 券码批次分页:支持按景区、批次名称、状态筛选
- 券码列表分页:支持按批次、状态、用户筛选
- 使用记录分页:支持按券码、用户、时间范围筛选
#### 统计功能
- **优惠券统计**:基础统计(领取数、使用数、可用数)、详细统计(使用率、平均使用天数)
- **券码统计**:支持可重复使用的统计(使用率、重复使用率、平均使用次数)
- 时间范围统计:指定时间段的整体数据分析
## 券码管理系统 (Voucher System)
### 核心特性
券码系统支持**可重复使用**的优惠券管理,与传统优惠券系统并行工作。
#### 关键优势
- **可重复使用**:支持单个券码多次使用,通过`maxUseCount`配置最大使用次数
- **用户使用限制**:支持单个用户对券码的使用次数限制(`maxUsePerUser`)
- **使用间隔控制**:支持设置使用时间间隔(`useIntervalHours`)
- **时间范围控制**:支持设置券码的有效期开始和结束时间
#### 券码优惠类型
```java
public enum VoucherDiscountType {
FREE_ALL(0, "全场免费"), // 优先级最高,且不可叠加
REDUCE_PRICE(1, "商品降价"), // 每个商品减免固定金额
DISCOUNT(2, "商品打折"); // 每个商品按百分比打折
}
```
#### 数据库表结构
- **price_voucher_batch_config**:券码批次配置表,支持按景区、推客创建券码批次
- **price_voucher_code**:券码表,每个券码全局唯一,支持同一用户在同一景区只能领取一次
- **price_voucher_usage_record**:券码使用记录表,记录每次使用的完整信息
- **voucher_print_record**:券码打印记录表,用于移动端打印功能
## 统一优惠检测系统 (Unified Discount Detection)
### 设计模式
采用**策略模式**的可扩展优惠检测系统,统一管理并自动组合多种优惠类型。
#### 核心接口
```java
// 优惠提供者接口
public interface IDiscountProvider {
String getProviderType(); // 提供者类型
int getPriority(); // 优先级(数字越大越高)
List<DiscountInfo> detectAvailableDiscounts(); // 检测可用优惠
DiscountResult applyDiscount(); // 应用优惠
}
// 优惠检测服务接口
public interface IDiscountDetectionService {
DiscountCombinationResult calculateOptimalCombination(); // 计算最优组合
DiscountCombinationResult previewOptimalCombination(); // 预览优惠组合
}
```
#### 优惠提供者实现(按优先级排序)
1. **VoucherDiscountProvider** (优先级: 100)
- 处理券码优惠逻辑
- 支持用户主动输入券码或自动选择最优券码
- 全场免费券码不可与其他优惠叠加
2. **CouponDiscountProvider** (优先级: 80)
- 处理优惠券优惠逻辑
- 自动选择最优优惠券
- 可与券码叠加使用(除全场免费券码外)
3. **OnePricePurchaseDiscountProvider** (优先级: 60)
- 处理一口价优惠逻辑(景区级统一价格)
- 仅当一口价小于当前金额时产生优惠
- 叠加性由配置`canUseCoupon/canUseVoucher`控制
#### 优惠应用策略
```java
原价 券码 优惠券 一口价 最终价格
特殊情况
- 全场免费券码直接最终价=0停止后续优惠
- 一口价可叠加性由配置 canUseCoupon / canUseVoucher 控制
```
### 开发模式
#### 添加新商品类型
1. 在ProductType枚举中添加新类型
2. 在PriceProductConfig表中配置default配置
3. 根据需要添加分层定价(PriceTierConfig)
4. 更新前端产品类型映射
#### 添加新优惠券类型
1. 在CouponType枚举中添加类型
2. 在CouponServiceImpl中实现计算逻辑
3. 更新applicableProducts验证规则
#### 添加新优惠提供者(策略扩展)
```java
@Component
public class FlashSaleDiscountProvider implements IDiscountProvider {
@Override
public String getProviderType() { return "FLASH_SALE"; }
@Override
public int getPriority() { return 90; } // 介于券码和优惠券之间
@Override
public List<DiscountInfo> detectAvailableDiscounts(DiscountDetectionContext context) {
// 实现限时抢购优惠检测逻辑
return discountInfoList;
}
@Override
public DiscountResult applyDiscount(DiscountDetectionContext context, DiscountInfo discount) {
// 实现优惠应用逻辑
return discountResult;
}
}
```
#### 创建可重复使用券码批次
```java
VoucherBatchCreateReqV2 request = new VoucherBatchCreateReqV2();
request.setBatchName("限时活动券码");
request.setMaxUseCount(3); // 每个券码最多使用三次
request.setMaxUsePerUser(2); // 每个用户最多使用两次
request.setUseIntervalHours(12); // 使用间隔12小时
request.setValidStartTime(startTime); // 有效期开始时间
request.setValidEndTime(endTime); // 有效期结束时间
```
#### 自定义TypeHandler使用
项目使用自定义TypeHandler处理复杂JSON字段:
- `BundleProductListTypeHandler`:处理套餐商品列表JSON序列化
### 测试策略
针对pricing模块的全面测试策略:
#### 单元测试类型
- **服务层测试**:每个服务类都有对应测试类
- `PriceBundleServiceTest` - 套餐价格计算测试
- `ReusableVoucherServiceTest` - 可重复使用券码测试
- `VoucherTimeRangeTest` - 券码时间范围功能测试
- `VoucherPrintServiceCodeGenerationTest` - 券码生成测试
- **实体映射测试**:验证数据库映射和JSON序列化
- `PriceBundleConfigStructureTest` - 实体结构测试
- `PriceBundleConfigJsonTest` - JSON序列化测试
- `CouponSwitchFieldsMappingTest` - 字段映射测试
- **类型处理器测试**:验证自定义TypeHandler
- `BundleProductListTypeHandlerTest` - 套餐商品列表序列化测试
- **配置验证测试**:验证系统配置完整性
- `DefaultConfigValidationTest` - 验证所有ProductType的default配置
- `CodeGenerationStandaloneTest` - 独立代码生成测试
#### 测试执行命令
```bash
# 运行单个测试类
mvn test -Dtest=VoucherTimeRangeTest
mvn test -Dtest=ReusableVoucherServiceTest
mvn test -Dtest=BundleProductListTypeHandlerTest
# 运行整个pricing模块测试
mvn test -Dtest="com.ycwl.basic.pricing.*Test"
# 运行特定分类的测试
mvn test -Dtest="com.ycwl.basic.pricing.service.*Test" # 服务层测试
mvn test -Dtest="com.ycwl.basic.pricing.handler.*Test" # TypeHandler测试
mvn test -Dtest="com.ycwl.basic.pricing.entity.*Test" # 实体测试
mvn test -Dtest="com.ycwl.basic.pricing.mapper.*Test" # Mapper测试
# 运行带详细报告的测试
mvn test -Dtest="com.ycwl.basic.pricing.*Test" -Dsurefire.printSummary=true
```
#### 重点测试场景
- **价格计算核心流程**:验证统一优惠检测和组合逻辑
- **可重复使用券码**:验证多次使用、时间间隔、用户限制逻辑
- **时间范围控制**:验证券码有效期开始和结束时间
- **优惠叠加规则**:验证券码、优惠券、一口价的叠加逻辑
- **JSON序列化**:验证复杂对象在数据库中的存储和读取
- **分页功能**:验证PageHelper和MyBatis-Plus分页集成
- **异常处理**:验证业务异常和全局异常处理器
## 关键架构模式
### Repository 层模式
项目使用Repository层抽象数据访问逻辑:
- Repository接口定义数据访问契约
- Mapper接口处理MyBatis Plus的数据库映射
- Service层通过Repository访问数据,避免直接依赖Mapper
### 异常处理架构
- **全局异常处理**: `CustomExceptionHandle` 提供统一的异常处理和响应格式
- **业务异常**: 自定义异常类继承RuntimeException,携带业务错误码
- **集成异常**: `IntegrationException` 专门处理外部服务调用异常
### 配置驱动的扩展性
通过配置文件驱动的多供应商支持:
- 存储:本地、AWS S3、阿里云 OSS
- 支付:微信支付、聪明支付
- 人脸识别:阿里云、百度
每个供应商通过统一接口访问,配置切换无需代码修改。
### 业务层架构
- **Service层**: 核心业务逻辑实现
- **Biz层**: 高级业务流程编排,组合多个Service
- **Controller层**: HTTP请求处理和响应转换
- **Repository层**: 数据访问抽象
### 认证和会话管理
- **JWT**: 使用jjwt库进行身份验证
- **Redis**: 存储会话信息和缓存
- **BaseContextHandler**: 提供当前用户上下文访问
## 微服务集成架构 (Integration Package)
### 核心架构
位于 `com.ycwl.basic.integration` 包,使用 Spring Cloud OpenFeign 和 Nacos 实现外部微服务集成。
#### 通用基础设施
- **IntegrationProperties**: 所有集成的集中配置管理
- **FeignErrorDecoder**: 自定义错误解码器,统一错误处理
- **IntegrationException**: 标准化集成异常
- **CommonResponse/PageResponse**: 外部服务响应包装器
#### 已实现的服务集成
- **Scenic Integration** (`integration.scenic`): ZT-Scenic 微服务集成
- **Device Integration** (`integration.device`): ZT-Device 微服务集成
#### 集成模式
每个外部服务按以下结构组织:
```
service/
├── client/ # Feign 客户端
├── config/ # 服务特定配置
├── dto/ # 数据传输对象
├── service/ # 业务逻辑层
└── example/ # 使用示例
```
### 配置管理
```yaml
integration:
scenic:
enabled: true
serviceName: zt-scenic
connectTimeout: 5000
readTimeout: 10000
device:
enabled: true
serviceName: zt-device
connectTimeout: 5000
readTimeout: 10000
```
### 使用模式
所有集成服务使用统一的 `handleResponse` 模式进行错误处理,确保一致的异常包装和日志记录。
### 测试集成服务
```bash
# 运行特定集成测试
mvn test -Dtest=ScenicIntegrationServiceTest
mvn test -Dtest=DeviceIntegrationServiceTest
# 运行所有集成测试
mvn test -Dtest="com.ycwl.basic.integration.*Test"
```
### 调试集成问题
启用 Feign 客户端日志:
```yaml
logging:
level:
com.ycwl.basic.integration: DEBUG
```
## 开发环境和调试
### 端口配置
- **开发环境端口**: 8030
- **应用名称**: zt
### 日志配置
开发环境默认启用详细的集成服务日志,便于调试外部服务调用问题。
### CI/CD 配置
项目使用 Jenkins 进行持续集成:
- **JDK 版本**: OpenJDK 21
- **构建命令**: `mvn clean package -DskipTests=true`
- **构建产物**: 自动归档和发布 JAR 文件
## 重要开发约定
### 测试文件组织
测试按功能模块组织,包括:
- **适配器测试**: `*AdapterTest.java` 测试第三方集成
- **实体测试**: 验证数据库映射和JSON序列化
- **Mapper测试**: 验证数据访问层逻辑
- **Handler测试**: 测试自定义TypeHandler
### 模块化架构
每个业务模块(如 `pricing``integration``order`)都有完整的分层结构:
```
module/
├── controller/ # REST API控制器
├── service/ # 业务逻辑层
├── repository/ # 数据访问抽象
├── mapper/ # MyBatis数据映射
├── entity/ # JPA/MyBatis实体
├── dto/ # 数据传输对象
├── enums/ # 枚举定义
└── exception/ # 模块特定异常
```
### 外部服务集成
集成服务统一使用以下模式:
- **Feign客户端**: 声明式HTTP客户端调用
- **错误处理**: 统一的`handleResponse`模式
- **配置管理**: 通过`IntegrationProperties`集中配置
- **超时配置**: 连接超时5秒,读取超时10秒
## Windows 开发环境注意事项
### 路径处理
- 项目在Windows系统上运行,注意路径分隔符使用反斜杠 `\`
- 配置文件中的资源路径已适配Windows环境
- 日志文件和临时文件路径会自动适配系统环境
### 开发工具兼容性
- 确保使用Java 21兼容的IDE
- Maven命令在Windows Command Prompt和PowerShell中均可使用
- 建议使用UTF-8编码避免中文字符问题
### 端口占用检查
开发时如遇端口冲突,使用以下命令检查:
```cmd
netstat -ano | findstr :8030
taskkill /f /pid <PID>
```

View File

View File

@@ -266,6 +266,12 @@
<artifactId>mts20140618</artifactId> <artifactId>mts20140618</artifactId>
<version>5.0.0</version> <version>5.0.0</version>
</dependency> </dependency>
<!-- Spring Kafka -->
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

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

View File

@@ -32,8 +32,6 @@ public class TemplateBiz {
private FaceRepository faceRepository; private FaceRepository faceRepository;
@Autowired @Autowired
private SourceMapper sourceMapper; private SourceMapper sourceMapper;
@Autowired
private SourceRepository sourceRepository;
public boolean determineTemplateCanGenerate(Long templateId, Long faceId) { public boolean determineTemplateCanGenerate(Long templateId, Long faceId) {
return determineTemplateCanGenerate(templateId, faceId, true); return determineTemplateCanGenerate(templateId, faceId, true);
@@ -134,6 +132,7 @@ public class TemplateBiz {
log.info("filterTaskParams: templateId:{} has no placeholders", templateId); log.info("filterTaskParams: templateId:{} has no placeholders", templateId);
return Map.of(); return Map.of();
} }
TemplateConfigEntity templateConfig = templateRepository.getTemplateConfig(templateId);
// 统计每个 placeholder 在模板中出现的次数 // 统计每个 placeholder 在模板中出现的次数
Map<String, Long> placeholderCounts = templatePlaceholders.stream() Map<String, Long> placeholderCounts = templatePlaceholders.stream()
@@ -144,6 +143,9 @@ public class TemplateBiz {
Map<String, List<SourceEntity>> filteredParams = new HashMap<>(); Map<String, List<SourceEntity>> filteredParams = new HashMap<>();
// 判断是否允许片段重复
boolean allowDuplicate = templateConfig != null && Integer.valueOf(1).equals(templateConfig.getDuplicateEnable());
for (Map.Entry<String, Long> entry : placeholderCounts.entrySet()) { for (Map.Entry<String, Long> entry : placeholderCounts.entrySet()) {
String placeholder = entry.getKey(); String placeholder = entry.getKey();
Long requiredCount = entry.getValue(); Long requiredCount = entry.getValue();
@@ -153,26 +155,64 @@ public class TemplateBiz {
String imageKey = placeholder; String imageKey = placeholder;
if (allTaskParams.containsKey(imageKey)) { if (allTaskParams.containsKey(imageKey)) {
List<SourceEntity> allSources = allTaskParams.get(imageKey); List<SourceEntity> allSources = allTaskParams.get(imageKey);
int actualCount = Math.min(requiredCount.intValue(), allSources.size()); List<SourceEntity> selectedSources = selectSources(allSources, requiredCount.intValue(), allowDuplicate);
List<SourceEntity> selectedSources = allSources.subList(0, actualCount); if (!selectedSources.isEmpty()) {
filteredParams.put(imageKey, new ArrayList<>(selectedSources)); filteredParams.put(imageKey, selectedSources);
}
} }
} else { } else {
// 视频源:占位符直接对应设备ID // 视频源:占位符直接对应设备ID
String videoKey = placeholder; String videoKey = placeholder;
if (allTaskParams.containsKey(videoKey)) { if (allTaskParams.containsKey(videoKey)) {
List<SourceEntity> allSources = allTaskParams.get(videoKey); List<SourceEntity> allSources = allTaskParams.get(videoKey);
int actualCount = Math.min(requiredCount.intValue(), allSources.size()); List<SourceEntity> selectedSources = selectSources(allSources, requiredCount.intValue(), allowDuplicate);
List<SourceEntity> selectedSources = allSources.subList(0, actualCount); if (!selectedSources.isEmpty()) {
filteredParams.put(videoKey, new ArrayList<>(selectedSources)); filteredParams.put(videoKey, selectedSources);
}
} }
} }
} }
log.debug("filterTaskParams: templateId:{}, original keys:{}, filtered keys:{}, placeholder counts:{}", log.debug("filterTaskParams: templateId:{}, original keys:{}, filtered keys:{}, placeholder counts:{}, allowDuplicate:{}",
templateId, allTaskParams.keySet().size(), filteredParams.keySet().size(), placeholderCounts); templateId, allTaskParams.keySet().size(), filteredParams.keySet().size(), placeholderCounts, allowDuplicate);
return filteredParams; return filteredParams;
} }
private List<SourceEntity> selectSources(List<SourceEntity> allSources, int requiredCount, boolean allowDuplicate) {
if (allSources == null || allSources.isEmpty()) {
return new ArrayList<>();
}
if (!allowDuplicate) {
// 不允许重复,使用原有逻辑
int actualCount = Math.min(requiredCount, allSources.size());
return new ArrayList<>(allSources.subList(0, actualCount));
}
// 允许重复,循环填充到所需数量
List<SourceEntity> selectedSources = new ArrayList<>();
int sourceIndex = 0;
for (int i = 0; i < requiredCount; i++) {
selectedSources.add(allSources.get(sourceIndex));
sourceIndex = (sourceIndex + 1) % allSources.size();
}
return selectedSources;
}
public Long findFirstAvailableTemplate(List<Long> templateIds, Long faceId, boolean scanSource) {
if (templateIds == null || templateIds.isEmpty() || faceId == null) {
return null;
}
for (Long templateId : templateIds) {
if (determineTemplateCanGenerate(templateId, faceId, scanSource)) {
return templateId;
}
}
return null;
}
} }

View File

@@ -0,0 +1,109 @@
package com.ycwl.basic.config;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.apache.kafka.common.serialization.StringSerializer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
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;
@Configuration
@ConditionalOnProperty(name = "kafka.enabled", havingValue = "true", matchIfMissing = false)
public class KafkaConfig {
@Value("${kafka.bootstrap-servers:100.64.0.12:39092}")
private String bootstrapServers;
@Value("${kafka.consumer.group-id:liuying-microservice}")
private String consumerGroupId;
@Value("${kafka.consumer.auto-offset-reset:earliest}")
private String autoOffsetReset;
@Value("${kafka.producer.acks:all}")
private String acks;
@Value("${kafka.producer.retries:3}")
private Integer retries;
@Value("${kafka.producer.batch-size:16384}")
private Integer batchSize;
@Value("${kafka.producer.linger-ms:1}")
private Integer lingerMs;
@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<>();
configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
configProps.put(ProducerConfig.ACKS_CONFIG, acks);
configProps.put(ProducerConfig.RETRIES_CONFIG, retries);
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);
}
@Bean
public KafkaTemplate<String, String> kafkaTemplate() {
return new KafkaTemplate<>(producerFactory());
}
@Bean
public ConsumerFactory<String, String> consumerFactory() {
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);
return new DefaultKafkaConsumerFactory<>(props);
}
@Bean
public ConcurrentKafkaListenerContainerFactory<String, String> kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
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

@@ -196,7 +196,7 @@ public class AppOrderV2Controller {
if (cachedResult.getFinalAmount().compareTo(request.getExpectedFinalAmount()) != 0) { if (cachedResult.getFinalAmount().compareTo(request.getExpectedFinalAmount()) != 0) {
log.warn("移动端下单:价格不匹配, cached={}, expected={}, userId={}, scenicId={}", log.warn("移动端下单:价格不匹配, cached={}, expected={}, userId={}, scenicId={}",
cachedResult.getFinalAmount(), request.getExpectedFinalAmount(), currentUserId, scenicId); cachedResult.getFinalAmount(), request.getExpectedFinalAmount(), currentUserId, scenicId);
return ApiResponse.fail("请重新下单"); return ApiResponse.fail("价格信息变化,请退出后重新查询价格");
} }
// 验证原价是否匹配(可选) // 验证原价是否匹配(可选)
@@ -215,6 +215,7 @@ public class AppOrderV2Controller {
Long orderId = oldOrderService.createOrderCompact(currentUserId, request, cachedResult); Long orderId = oldOrderService.createOrderCompact(currentUserId, request, cachedResult);
return ApiResponse.success(String.valueOf(orderId)); return ApiResponse.success(String.valueOf(orderId));
} catch (Exception e) { } catch (Exception e) {
log.warn("移动端下单:订单创建失败, userId={}, scenicId={}, error={}", currentUserId, scenicId, e.getMessage(), e);
return ApiResponse.fail("订单创建失败,请稍后重试"); return ApiResponse.fail("订单创建失败,请稍后重试");
} }

View File

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

View File

@@ -72,26 +72,14 @@ public class AppScenicController {
public ApiResponse<ScenicConfigResp> getConfig(@PathVariable Long id){ public ApiResponse<ScenicConfigResp> getConfig(@PathVariable Long id){
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(id); ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(id);
ScenicConfigResp resp = new ScenicConfigResp(); ScenicConfigResp resp = new ScenicConfigResp();
resp.setBookRoutine(scenicConfig.getInteger("book_routine")); resp.setWatermarkUrl(scenicConfig.getString("watermark_url"));
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.setVideoStoreDay(scenicConfig.getInteger("video_store_day")); 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.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.setGroupingEnable(scenicConfig.getBoolean("grouping_enable", false));
resp.setVoucherEnable(scenicConfig.getBoolean("voucher_enable", false));
resp.setShowPhotoWhenWaiting(scenicConfig.getBoolean("show_photo_when_waiting", 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); return ApiResponse.success(resp);
} }

View File

@@ -55,7 +55,7 @@ public class AppTaskController {
@PostMapping("/submit") @PostMapping("/submit")
public ApiResponse<String> submitVideoTask(@RequestBody VideoTaskReq videoTaskReq) { public ApiResponse<String> submitVideoTask(@RequestBody VideoTaskReq videoTaskReq) {
taskService.createTaskByFaceIdAndTempalteId(videoTaskReq.getFaceId(),videoTaskReq.getTemplateId(),0); taskService.createTaskByFaceIdAndTemplateId(videoTaskReq.getFaceId(),videoTaskReq.getTemplateId(),0);
return ApiResponse.success("成功"); return ApiResponse.success("成功");
} }
} }

View File

@@ -3,8 +3,10 @@ package com.ycwl.basic.controller.pc;
import com.ycwl.basic.integration.device.dto.config.*; import com.ycwl.basic.integration.device.dto.config.*;
import com.ycwl.basic.integration.common.response.PageResponse; import com.ycwl.basic.integration.common.response.PageResponse;
import com.ycwl.basic.integration.device.dto.device.*; import com.ycwl.basic.integration.device.dto.device.*;
import com.ycwl.basic.integration.device.dto.status.DeviceStatusDTO;
import com.ycwl.basic.integration.device.service.DeviceConfigIntegrationService; import com.ycwl.basic.integration.device.service.DeviceConfigIntegrationService;
import com.ycwl.basic.integration.device.service.DeviceIntegrationService; import com.ycwl.basic.integration.device.service.DeviceIntegrationService;
import com.ycwl.basic.integration.device.service.DeviceStatusIntegrationService;
import com.ycwl.basic.utils.ApiResponse; import com.ycwl.basic.utils.ApiResponse;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@@ -29,6 +31,7 @@ public class DeviceV2Controller {
private final DeviceIntegrationService deviceIntegrationService; private final DeviceIntegrationService deviceIntegrationService;
private final DeviceConfigIntegrationService deviceConfigIntegrationService; private final DeviceConfigIntegrationService deviceConfigIntegrationService;
private final DeviceStatusIntegrationService deviceStatusIntegrationService;
// ========== 设备基础 CRUD 操作 ========== // ========== 设备基础 CRUD 操作 ==========
@@ -60,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获取设备信息 * 根据ID获取设备信息
*/ */
@@ -102,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());
}
}
/** /**
* 根据设备编号获取设备信息 * 根据设备编号获取设备信息
*/ */
@@ -131,16 +92,24 @@ public class DeviceV2Controller {
} }
/** /**
* 根据设备编号获取设备带配置信息 * 根据设备ID获取设备在线状态
*/ */
@GetMapping("/no/{no}/with-config") @GetMapping("/{id}/status")
public ApiResponse<DeviceV2WithConfigDTO> getDeviceWithConfigByNo(@PathVariable String no) { public ApiResponse<DeviceStatusDTO> getDeviceOnlineStatus(@PathVariable Long id) {
log.info("获取设备在线状态, deviceId: {}", id);
try { try {
DeviceV2WithConfigDTO device = deviceIntegrationService.getDeviceWithConfigByNo(no); // 首先获取设备信息以获得设备编号
return ApiResponse.success(device); DeviceV2DTO device = deviceIntegrationService.getDevice(id);
if (device == null) {
return ApiResponse.fail("设备不存在");
}
// 使用设备编号查询在线状态
DeviceStatusDTO onlineStatus = deviceStatusIntegrationService.getDeviceStatus(device.getNo());
return ApiResponse.success(onlineStatus);
} catch (Exception e) { } catch (Exception e) {
log.error("根据设备编号获取设备配置信息失败, no: {}", no, e); log.error("获取设备在线状态失败, deviceId: {}", id, e);
return ApiResponse.fail("根据设备编号获取设备配置信息失败: " + e.getMessage()); return ApiResponse.fail("获取设备在线状态失败: " + e.getMessage());
} }
} }
@@ -302,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());
}
}
/** /**
* 根据配置键获取配置 * 根据配置键获取配置
*/ */
@@ -346,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.debug("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.debug("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.CreateScenicRequest;
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO; import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO;
import com.ycwl.basic.integration.common.response.PageResponse; 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.dto.scenic.UpdateScenicRequest;
import com.ycwl.basic.integration.scenic.service.ScenicConfigIntegrationService; import com.ycwl.basic.integration.scenic.service.ScenicConfigIntegrationService;
import com.ycwl.basic.integration.scenic.service.ScenicIntegrationService; 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()); 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") .setNamePrefix("VIID-" + scenicId + "-t")
.build(); .build();
return new ThreadPoolExecutor( return new ThreadPoolExecutor(
4, 1024, 0L, TimeUnit.MILLISECONDS, 8, 32, 10L, TimeUnit.SECONDS, // 核心2个线程,最大20个线程,空闲60秒回收
new ArrayBlockingQueue<>(1024), new ArrayBlockingQueue<>(1024), // 队列大小从1024降至100
threadFactory); 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 - **Device Integration** (`com.ycwl.basic.integration.device`): ZT-Device microservice integration
- **Render Worker Integration** (`com.ycwl.basic.integration.render`): ZT-Render-Worker 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 - **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 ### Integration Pattern
@@ -34,8 +35,7 @@ service/
├── client/ # Feign clients for HTTP calls ├── client/ # Feign clients for HTTP calls
├── config/ # Service-specific configuration ├── config/ # Service-specific configuration
├── dto/ # Data transfer objects ├── dto/ # Data transfer objects
── service/ # Service layer with business logic ── service/ # Service layer with business logic
└── example/ # Usage examples
``` ```
## Integration Fallback Mechanism ## Integration Fallback Mechanism
@@ -792,13 +792,6 @@ mvn test -Dtest=DefaultConfigIntegrationServiceTest
# Run all device integration tests (including default configs) # Run all device integration tests (including default configs)
mvn test -Dtest="com.ycwl.basic.integration.device.*Test" 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 ### Common Configuration Keys
@@ -820,8 +813,7 @@ com.ycwl.basic.integration.{service-name}/
├── client/ ├── client/
├── config/ ├── config/
├── dto/ ├── dto/
── service/ ── service/
└── example/
``` ```
### 2. Add Configuration Properties ### 2. Add Configuration Properties
@@ -1168,6 +1160,57 @@ fallbackService.clearAllFallbackCache("zt-render-worker");
- **Active (isActive=1)**: Worker is available for tasks - **Active (isActive=1)**: Worker is available for tasks
- **Inactive (isActive=0)**: Worker is disabled - **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 ## Common Development Tasks
### Running Integration Tests ### Running Integration Tests

View File

@@ -16,8 +16,6 @@ import java.util.stream.Collectors;
*/ */
public class ScenicConfigManager extends ConfigManager<ScenicConfigV2DTO> { 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) { public ScenicConfigManager(List<ScenicConfigV2DTO> configList) {
super(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 @Override
protected String getConfigKey(ScenicConfigV2DTO config) { protected String getConfigKey(ScenicConfigV2DTO config) {
return config != null ? config.getConfigKey() : null; return config != null ? config.getConfigKey() : null;
@@ -55,276 +34,4 @@ public class ScenicConfigManager extends ConfigManager<ScenicConfigV2DTO> {
return config != null ? config.getConfigValue() : null; 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, CommonResponse<DeviceConfigV2DTO> getDeviceConfigByKey(@PathVariable("deviceId") Long deviceId,
@PathVariable("configKey") String configKey); @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

@@ -0,0 +1,51 @@
package com.ycwl.basic.integration.device.client;
import com.ycwl.basic.integration.common.response.CommonResponse;
import com.ycwl.basic.integration.device.dto.status.DeviceStatusDTO;
import com.ycwl.basic.integration.device.dto.status.OnlineStatusResponseDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import java.util.List;
@FeignClient(name = "zt-device", contextId = "deviceStatusClient", path = "/api/device/status")
public interface DeviceStatusClient {
/**
* 获取设备状态
*/
@GetMapping("/{deviceNo}")
CommonResponse<DeviceStatusDTO> getDeviceStatus(@PathVariable("deviceNo") String deviceNo);
/**
* 检查设备是否在线
*/
@GetMapping("/{deviceNo}/online")
CommonResponse<OnlineStatusResponseDTO> isDeviceOnline(@PathVariable("deviceNo") String deviceNo);
/**
* 获取所有在线设备
*/
@GetMapping("/online")
CommonResponse<List<DeviceStatusDTO>> getAllOnlineDevices();
/**
* 设置设备离线
*/
@PostMapping("/{deviceNo}/offline")
CommonResponse<String> setDeviceOffline(@PathVariable("deviceNo") String deviceNo);
/**
* 设置设备在线
*/
@PostMapping("/{deviceNo}/online")
CommonResponse<String> setDeviceOnline(@PathVariable("deviceNo") String deviceNo);
/**
* 清理过期设备状态
*/
@PostMapping("/clean")
CommonResponse<String> cleanExpiredDevices();
}

View File

@@ -21,18 +21,6 @@ public interface DeviceV2Client {
@GetMapping("/no/{no}") @GetMapping("/no/{no}")
CommonResponse<DeviceV2DTO> getDeviceByNo(@PathVariable("no") String 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 = "isActive", required = false) Integer isActive,
@RequestParam(value = "scenicId", required = false) Long scenicId); @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

@@ -0,0 +1,41 @@
package com.ycwl.basic.integration.device.dto.status;
/**
* 设备状态动作枚举
*/
public enum DeviceStatusActionEnum {
/**
* 设备注册
*/
REGISTER("register"),
/**
* 设备保活
*/
KEEPALIVE("keepalive"),
/**
* 设备注销
*/
UNREGISTER("unregister");
private final String action;
DeviceStatusActionEnum(String action) {
this.action = action;
}
public String getAction() {
return action;
}
public static DeviceStatusActionEnum fromString(String action) {
for (DeviceStatusActionEnum statusAction : values()) {
if (statusAction.action.equalsIgnoreCase(action)) {
return statusAction;
}
}
throw new IllegalArgumentException("Unknown device status action: " + action);
}
}

View File

@@ -0,0 +1,50 @@
package com.ycwl.basic.integration.device.dto.status;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.Date;
/**
* 设备状态信息
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class DeviceStatusDTO {
/**
* 设备编号
*/
private String deviceNo;
/**
* 是否在线
*/
private Boolean isOnline;
/**
* 最后活动时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date lastActiveTime;
/**
* 最后动作(register/keepalive/unregister)
*/
private String lastAction;
/**
* 客户端IP
*/
private String clientIP;
/**
* 状态更新时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
}

View File

@@ -0,0 +1,24 @@
package com.ycwl.basic.integration.device.dto.status;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 在线状态响应
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OnlineStatusResponseDTO {
/**
* 设备编号
*/
private String deviceNo;
/**
* 是否在线
*/
private Boolean isOnline;
}

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, "根据键获取设备配置失败"); 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) { public DeviceConfigV2DTO createDeviceConfig(Long deviceId, CreateDeviceConfigRequest request) {
log.debug("创建设备配置, deviceId: {}, configKey: {}", deviceId, request.getConfigKey()); log.debug("创建设备配置, deviceId: {}, configKey: {}", deviceId, request.getConfigKey());
CommonResponse<DeviceConfigV2DTO> response = deviceConfigV2Client.createDeviceConfig(deviceId, request); 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) { public DeviceV2DTO createDevice(CreateDeviceRequest request) {
log.debug("创建设备, name: {}, no: {}, type: {}", request.getName(), request.getNo(), request.getType()); log.debug("创建设备, name: {}, no: {}, type: {}", request.getName(), request.getNo(), request.getType());
CommonResponse<DeviceV2DTO> response = deviceV2Client.createDevice(request); CommonResponse<DeviceV2DTO> response = deviceV2Client.createDevice(request);
@@ -101,15 +75,6 @@ public class DeviceIntegrationService {
return handleResponse(response, "分页查询设备列表失败"); 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摄像头设备 * 创建IPC摄像头设备
*/ */

View File

@@ -0,0 +1,211 @@
package com.ycwl.basic.integration.device.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.device.client.DeviceStatusClient;
import com.ycwl.basic.integration.device.dto.status.DeviceStatusDTO;
import com.ycwl.basic.integration.device.dto.status.OnlineStatusResponseDTO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Slf4j
@Service
@RequiredArgsConstructor
public class DeviceStatusIntegrationService {
private final DeviceStatusClient deviceStatusClient;
private final IntegrationFallbackService fallbackService;
private static final String SERVICE_NAME = "zt-device";
public DeviceStatusDTO getDeviceStatus(String deviceNo) {
log.debug("获取设备状态, deviceNo: {}", deviceNo);
return fallbackService.executeWithFallback(
SERVICE_NAME,
"device:status:" + deviceNo,
() -> {
CommonResponse<DeviceStatusDTO> response = deviceStatusClient.getDeviceStatus(deviceNo);
return handleResponse(response, "获取设备状态失败");
},
DeviceStatusDTO.class
);
}
public OnlineStatusResponseDTO isDeviceOnline(String deviceNo) {
log.debug("检查设备是否在线, deviceNo: {}", deviceNo);
return fallbackService.executeWithFallback(
SERVICE_NAME,
"device:online:" + deviceNo,
() -> {
CommonResponse<OnlineStatusResponseDTO> response = deviceStatusClient.isDeviceOnline(deviceNo);
return handleResponse(response, "检查设备在线状态失败");
},
OnlineStatusResponseDTO.class
);
}
public List<DeviceStatusDTO> getAllOnlineDevices() {
log.debug("获取所有在线设备");
return fallbackService.executeWithFallback(
SERVICE_NAME,
"devices:online:all",
() -> {
CommonResponse<List<DeviceStatusDTO>> response = deviceStatusClient.getAllOnlineDevices();
return handleResponse(response, "获取所有在线设备失败");
},
List.class
);
}
public void setDeviceOffline(String deviceNo) {
log.debug("设置设备离线, deviceNo: {}", deviceNo);
CommonResponse<String> response = deviceStatusClient.setDeviceOffline(deviceNo);
handleResponse(response, "设置设备离线失败");
}
public void setDeviceOnline(String deviceNo) {
log.debug("设置设备在线, deviceNo: {}", deviceNo);
CommonResponse<String> response = deviceStatusClient.setDeviceOnline(deviceNo);
handleResponse(response, "设置设备在线失败");
}
public void cleanExpiredDevices() {
log.debug("清理过期设备状态");
CommonResponse<String> response = deviceStatusClient.cleanExpiredDevices();
handleResponse(response, "清理过期设备状态失败");
}
/**
* 安全地获取设备状态
*/
public Optional<DeviceStatusDTO> getDeviceStatusSafely(String deviceNo) {
try {
DeviceStatusDTO status = getDeviceStatus(deviceNo);
return Optional.ofNullable(status);
} catch (Exception e) {
log.warn("获取设备状态异常: deviceNo={}, error={}", deviceNo, e.getMessage());
return Optional.empty();
}
}
/**
* 安全地检查设备是否在线
*/
public boolean isDeviceOnlineSafely(String deviceNo) {
try {
OnlineStatusResponseDTO response = isDeviceOnline(deviceNo);
return response != null && Boolean.TRUE.equals(response.getIsOnline());
} catch (Exception e) {
log.warn("检查设备在线状态异常: deviceNo={}, error={}", deviceNo, e.getMessage());
return false;
}
}
/**
* 批量检查设备是否在线
*/
public boolean areAllDevicesOnline(List<String> deviceNos) {
if (deviceNos == null || deviceNos.isEmpty()) {
return true;
}
log.debug("批量检查设备在线状态, deviceNos: {}", deviceNos);
for (String deviceNo : deviceNos) {
if (!isDeviceOnlineSafely(deviceNo)) {
return false;
}
}
return true;
}
/**
* 批量设置设备离线
*/
public void setDevicesOffline(List<String> deviceNos) {
if (deviceNos == null || deviceNos.isEmpty()) {
return;
}
log.debug("批量设置设备离线, deviceNos: {}", deviceNos);
for (String deviceNo : deviceNos) {
try {
setDeviceOffline(deviceNo);
} catch (Exception e) {
log.error("设置设备离线失败: deviceNo={}, error={}", deviceNo, e.getMessage());
}
}
}
/**
* 批量设置设备在线
*/
public void setDevicesOnline(List<String> deviceNos) {
if (deviceNos == null || deviceNos.isEmpty()) {
return;
}
log.debug("批量设置设备在线, deviceNos: {}", deviceNos);
for (String deviceNo : deviceNos) {
try {
setDeviceOnline(deviceNo);
} catch (Exception e) {
log.error("设置设备在线失败: deviceNo={}, error={}", deviceNo, e.getMessage());
}
}
}
/**
* 获取在线设备数量
*/
public int getOnlineDeviceCount() {
try {
List<DeviceStatusDTO> onlineDevices = getAllOnlineDevices();
return onlineDevices != null ? onlineDevices.size() : 0;
} catch (Exception e) {
log.warn("获取在线设备数量失败: {}", e.getMessage());
return 0;
}
}
/**
* 获取指定设备编号列表中的在线设备
*/
public List<String> getOnlineDeviceNos(List<String> deviceNos) {
if (deviceNos == null || deviceNos.isEmpty()) {
return List.of();
}
return deviceNos.stream()
.filter(this::isDeviceOnlineSafely)
.toList();
}
/**
* 获取指定设备编号列表中的离线设备
*/
public List<String> getOfflineDeviceNos(List<String> deviceNos) {
if (deviceNos == null || deviceNos.isEmpty()) {
return List.of();
}
return deviceNos.stream()
.filter(deviceNo -> !isDeviceOnlineSafely(deviceNo))
.toList();
}
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,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

@@ -0,0 +1,36 @@
package com.ycwl.basic.integration.kafka.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "kafka")
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();
@Data
public static class Consumer {
private String groupId = "liuying-microservice";
private String autoOffsetReset = "earliest";
private String keyDeserializer = "org.apache.kafka.common.serialization.StringDeserializer";
private String valueDeserializer = "org.apache.kafka.common.serialization.StringDeserializer";
}
@Data
public static class Producer {
private String acks = "all";
private Integer retries = 3;
private Integer batchSize = 16384;
private Integer lingerMs = 1;
private Integer bufferMemory = 33554432;
private String keySerializer = "org.apache.kafka.common.serialization.StringSerializer";
private String valueSerializer = "org.apache.kafka.common.serialization.StringSerializer";
}
}

View File

@@ -0,0 +1,83 @@
package com.ycwl.basic.integration.kafka.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
/**
* zt-face topic消息结构
* 用于人脸处理任务的异步消息传递
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FaceProcessingMessage {
/**
* 人脸样本ID(外部系统传入)
*/
private Long faceSampleId;
/**
* 景区ID
*/
private Long scenicId;
/**
* 设备ID
*/
private Long deviceId;
/**
* 人脸图片URL
*/
private String faceUrl;
// 不再需要faceUniqueId,直接使用faceSampleId作为唯一标识
/**
* 拍摄时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date shotTime;
// status字段已移除,由系统内部管理状态
// deviceConfig字段已移除,由系统内部通过deviceRepository查询
/**
* 消息创建时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
/**
* 消息来源
*/
private String source;
// retryCount字段已移除,重试机制由系统内部管理
// DeviceConfig内部类已移除,不再需要在消息中传递设备配置
/**
* 创建人脸处理消息的工厂方法(使用外部传入的faceId)
*/
public static FaceProcessingMessage create(Long externalFaceId, Long scenicId, Long deviceId,
String faceUrl, Date shotTime) {
return FaceProcessingMessage.builder()
.faceSampleId(externalFaceId) // 使用外部传入的ID作为唯一标识
.scenicId(scenicId)
.deviceId(deviceId)
.faceUrl(faceUrl)
.shotTime(shotTime)
.createTime(new Date())
.source("external-system")
.build();
}
}

View File

@@ -0,0 +1,38 @@
package com.ycwl.basic.integration.kafka.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class KafkaMessage<T> {
private String messageId;
private String topic;
private String eventType;
private T payload;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime timestamp;
private String source;
private String version;
public static <T> KafkaMessage<T> of(String topic, String eventType, T payload) {
return KafkaMessage.<T>builder()
.topic(topic)
.eventType(eventType)
.payload(payload)
.timestamp(LocalDateTime.now())
.source("liuying-microservice")
.version("1.0")
.build();
}
}

View File

@@ -0,0 +1,194 @@
package com.ycwl.basic.integration.kafka.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
import com.ycwl.basic.facebody.entity.AddFaceResp;
import com.ycwl.basic.integration.kafka.dto.FaceProcessingMessage;
import com.ycwl.basic.mapper.FaceSampleMapper;
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
import com.ycwl.basic.repository.DeviceRepository;
import com.ycwl.basic.service.pc.ScenicService;
import com.ycwl.basic.service.task.TaskFaceService;
import com.ycwl.basic.task.DynamicTaskGenerator;
import com.ycwl.basic.integration.common.manager.DeviceConfigManager;
// 不再需要SnowFlakeUtil,使用外部传入的ID
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;
import java.time.ZoneId;
import java.util.Date;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 人脸处理Kafka消费服务
* 消费外部系统发送到zt-face topic的消息
*/
@Slf4j
@Service
@RequiredArgsConstructor
@ConditionalOnProperty(name = "kafka.enabled", havingValue = "true")
public class FaceProcessingKafkaService {
private static final String ZT_FACE_TOPIC = "zt-face";
private final FaceSampleMapper faceSampleMapper;
private final TaskFaceService taskFaceService;
private final ScenicService scenicService;
private final DeviceRepository deviceRepository;
private final ThreadPoolExecutor faceRecognitionExecutor;
/**
* 消费外部系统发送的人脸处理消息
* 先保存人脸样本数据,再进行异步人脸识别处理
*/
@KafkaListener(topics = ZT_FACE_TOPIC, containerFactory = "manualCommitKafkaListenerContainerFactory")
public void processFaceMessage(String message, Acknowledgment ack) {
try {
FaceProcessingMessage faceMessage = JacksonUtil.parseObject(message, FaceProcessingMessage.class);
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) {
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);
// 即使发生异常也消费消息,避免消息堆积
ack.acknowledge();
}
}
/**
* 保存人脸样本数据到数据库
* @param faceMessage 人脸处理消息
* @param externalFaceId 外部传入的人脸ID
* @return 是否保存成功
*/
private boolean saveFaceSample(FaceProcessingMessage faceMessage, Long externalFaceId) {
try {
FaceSampleEntity faceSample = new FaceSampleEntity();
faceSample.setId(externalFaceId); // 使用外部传入的ID
faceSample.setScenicId(faceMessage.getScenicId());
faceSample.setDeviceId(faceMessage.getDeviceId());
faceSample.setStatus(0); // 初始状态
faceSample.setFaceUrl(faceMessage.getFaceUrl());
// 转换时间格式
if (faceMessage.getShotTime() != null) {
Date shotTime = faceMessage.getShotTime();
faceSample.setCreateAt(shotTime);
} else {
faceSample.setCreateAt(new Date());
}
// 保存到数据库
faceSampleMapper.add(faceSample);
return true;
} catch (Exception e) {
log.error("保存人脸样本数据失败, 外部faceId: {}, scenicId: {}, deviceId: {}",
externalFaceId, faceMessage.getScenicId(), faceMessage.getDeviceId(), e);
return false;
}
}
/**
* 异步执行人脸识别处理逻辑
* 对已保存的人脸样本进行识别处理
*/
private void processFaceRecognitionAsync(FaceProcessingMessage message) {
Long faceSampleId = message.getFaceSampleId();
Long scenicId = message.getScenicId();
String faceUrl = message.getFaceUrl();
// 直接使用faceSampleId作为唯一标识
String faceUniqueId = faceSampleId.toString();
// 获取人脸识别适配器
IFaceBodyAdapter faceBodyAdapter = scenicService.getScenicFaceBodyAdapter(scenicId);
if (faceBodyAdapter == null) {
log.error("人脸识别适配器不存在, scenicId: {}", scenicId);
updateFaceSampleStatus(faceSampleId, -1);
return;
}
try {
// 更新状态为处理中
updateFaceSampleStatus(faceSampleId, 1);
// 确保人脸数据库存在
taskFaceService.assureFaceDb(faceBodyAdapter, scenicId.toString());
// 添加人脸到识别服务(使用faceSampleId作为唯一标识)
AddFaceResp addFaceResp = faceBodyAdapter.addFace(
scenicId.toString(),
faceSampleId.toString(),
faceUrl,
faceUniqueId // 即faceSampleId.toString()
);
if (addFaceResp != null) {
// 更新人脸样本得分和状态
faceSampleMapper.updateScore(faceSampleId, addFaceResp.getScore());
updateFaceSampleStatus(faceSampleId, 2);
log.info("人脸识别处理成功, faceSampleId: {}", faceSampleId);
// 查询设备配置,判断是否启用预订功能
Long deviceId = message.getDeviceId();
DeviceConfigManager deviceConfig = deviceRepository.getDeviceConfigManager(deviceId);
if (deviceConfig != null &&
Integer.valueOf(1).equals(deviceConfig.getInteger("enable_pre_book"))) {
DynamicTaskGenerator.addTask(faceSampleId);
}
} else {
log.warn("人脸添加返回空结果, faceSampleId: {}", faceSampleId);
updateFaceSampleStatus(faceSampleId, -1);
}
} catch (Exception e) {
log.error("人脸识别处理失败, faceSampleId: {}, error: {}",
faceSampleId, e.getMessage(), e);
// 标记人脸样本为处理失败状态
updateFaceSampleStatus(faceSampleId, -1);
}
}
/**
* 更新人脸样本状态
*/
private void updateFaceSampleStatus(Long faceSampleId, Integer status) {
try {
faceSampleMapper.updateStatus(faceSampleId, status);
} catch (Exception e) {
log.error("更新人脸样本状态失败, faceSampleId: {}", faceSampleId, e);
}
}
}

View File

@@ -0,0 +1,51 @@
package com.ycwl.basic.integration.kafka.service;
import com.ycwl.basic.integration.kafka.config.KafkaIntegrationProperties;
import com.ycwl.basic.integration.kafka.dto.KafkaMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;
@Slf4j
@Service
@RequiredArgsConstructor
@ConditionalOnProperty(name = "kafka.enabled", havingValue = "true")
public class KafkaIntegrationService {
private final KafkaTemplate<String, String> kafkaTemplate;
private final KafkaIntegrationProperties kafkaProperties;
/**
* 测试Kafka连接
*/
public boolean testConnection() {
try {
log.info("Testing Kafka connection to: {}", kafkaProperties.getBootstrapServers());
// 尝试获取元数据以测试连接
var metadata = kafkaTemplate.getProducerFactory().createProducer().partitionsFor("test-topic");
log.info("Kafka connection test successful");
return true;
} catch (Exception e) {
log.error("Kafka connection test failed", e);
return false;
}
}
/**
* 发送消息(预留接口,暂不实现具体逻辑)
*/
public void sendMessage(String topic, String key, KafkaMessage<?> message) {
log.info("Kafka message sending is not implemented yet. Topic: {}, Key: {}", topic, key);
// TODO: 后续实现具体的消息发送逻辑
}
/**
* 获取Kafka配置信息
*/
public KafkaIntegrationProperties getKafkaProperties() {
return kafkaProperties;
}
}

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,36 @@
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 messageId; // unique message identifier
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,52 @@
package com.ycwl.basic.integration.message.service;
import com.ycwl.basic.integration.message.dto.ZtMessage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
/**
* ZT消息生产者空实现服务
* <p>
* 当 kafka.enabled=false 时,该服务会被激活,作为 ZtMessageProducerService 的替代。
* 所有消息发送操作都会被忽略,只记录日志。
* </p>
*
* @see ZtMessageProducerService
*/
@Slf4j
@Service
@ConditionalOnProperty(name = "kafka.enabled", havingValue = "false", matchIfMissing = true)
public class ZtMessageProducerNoOpService extends ZtMessageProducerService {
/**
* 空构造函数
* 由于父类需要依赖项,但在此实现中不会使用,因此传入 null
*/
public ZtMessageProducerNoOpService() {
super(null, null, null);
}
/**
* 消息发送的空操作实现
* <p>
* 当 Kafka 未启用时,此方法会被调用。
* 它不会实际发送消息,只会记录一条 debug 日志。
* </p>
*
* @param msg 待发送的消息(会被验证基本字段)
*/
@Override
public void send(ZtMessage msg) {
if (msg == null) {
log.debug("[ZT-MESSAGE] Kafka未启用,跳过消息发送(消息为null)");
return;
}
log.debug("[ZT-MESSAGE] Kafka未启用,跳过消息发送: channelId={}, title={}, target={}, messageId={}",
msg.getChannelId(),
msg.getTitle(),
msg.getTarget(),
msg.getMessageId());
}
}

View File

@@ -0,0 +1,70 @@
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);
// Generate messageId if not present
if (StringUtils.isBlank(msg.getMessageId())) {
msg.setMessageId(java.util.UUID.randomUUID().toString());
}
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={}, messageId={}, title={}", topic, key, msg.getMessageId(), msg.getTitle());
kafkaTemplate.send(topic, key, payload).whenComplete((metadata, ex) -> {
if (ex != null) {
log.error("[ZT-MESSAGE] produce failed: messageId={}, error={}", msg.getMessageId(), ex.getMessage(), ex);
} else if (metadata != null) {
log.info("[ZT-MESSAGE] produced: messageId={}, partition={}, offset={}", msg.getMessageId(), 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}") @GetMapping("/{id}")
CommonResponse<RenderWorkerV2DTO> getWorker(@PathVariable("id") Long 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) Integer isEnabled,
@RequestParam(required = false) String name); @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获取工作器核心信息 * 根据key获取工作器核心信息
*/ */
@GetMapping("/key/{key}") @GetMapping("/key/{key}")
CommonResponse<RenderWorkerV2DTO> getWorkerByKey(@PathVariable("key") String 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); log.debug("获取渲染工作器平铺配置, workerId: {}", workerId);
return fallbackService.executeWithFallback( return fallbackService.executeWithFallback(
SERVICE_NAME, SERVICE_NAME,
"worker:flat:config:" + workerId, "worker:config:" + workerId,
() -> { () -> {
List<RenderWorkerConfigV2DTO> configs = getWorkerConfigsInternal(workerId); List<RenderWorkerConfigV2DTO> configs = getWorkerConfigsInternal(workerId);
return flattenConfigs(configs); 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, "查询渲染工作器列表失败"); 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获取工作器核心信息(带降级) * 根据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, CommonResponse<ScenicConfigV2DTO> getConfigByKey(@PathVariable("scenicId") Long scenicId,
@PathVariable("configKey") String configKey); @PathVariable("configKey") String configKey);
@GetMapping("/{scenicId}/keys")
CommonResponse<Map<String, Object>> getFlatConfigs(@PathVariable("scenicId") Long scenicId);
@PostMapping("/{scenicId}") @PostMapping("/{scenicId}")
CommonResponse<ScenicConfigV2DTO> createConfig(@PathVariable("scenicId") Long scenicId, CommonResponse<ScenicConfigV2DTO> createConfig(@PathVariable("scenicId") Long scenicId,
@RequestBody CreateConfigRequest request); @RequestBody CreateConfigRequest request);
@@ -37,8 +34,4 @@ public interface ScenicConfigV2Client {
@PostMapping("/{scenicId}/batch") @PostMapping("/{scenicId}/batch")
CommonResponse<BatchUpdateResponse> batchUpdateConfigs(@PathVariable("scenicId") Long scenicId, CommonResponse<BatchUpdateResponse> batchUpdateConfigs(@PathVariable("scenicId") Long scenicId,
@RequestBody BatchConfigRequest request); @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.filter.ScenicFilterRequest;
import com.ycwl.basic.integration.scenic.dto.scenic.CreateScenicRequest; 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.ScenicV2DTO;
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2WithConfigDTO;
import com.ycwl.basic.integration.common.response.PageResponse; import com.ycwl.basic.integration.common.response.PageResponse;
import com.ycwl.basic.integration.scenic.dto.scenic.UpdateScenicRequest; import com.ycwl.basic.integration.scenic.dto.scenic.UpdateScenicRequest;
import org.springframework.cloud.openfeign.FeignClient; import org.springframework.cloud.openfeign.FeignClient;
@@ -19,10 +18,6 @@ public interface ScenicV2Client {
@GetMapping("/{scenicId}") @GetMapping("/{scenicId}")
CommonResponse<ScenicV2DTO> getScenic(@PathVariable("scenicId") Long scenicId); CommonResponse<ScenicV2DTO> getScenic(@PathVariable("scenicId") Long scenicId);
@GetMapping("/{scenicId}/with-config")
CommonResponse<ScenicV2WithConfigDTO> getScenicWithConfig(@PathVariable("scenicId") Long scenicId);
@PostMapping("/") @PostMapping("/")
CommonResponse<ScenicV2DTO> createScenic(@RequestBody CreateScenicRequest request); CommonResponse<ScenicV2DTO> createScenic(@RequestBody CreateScenicRequest request);
@@ -41,10 +36,4 @@ public interface ScenicV2Client {
@RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) Integer status, @RequestParam(required = false) Integer status,
@RequestParam(required = false) String name); @RequestParam(required = false) String name);
@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) { public ScenicConfigV2DTO createConfig(Long scenicId, CreateConfigRequest request) {
log.debug("创建景区配置, scenicId: {}, configKey: {}", scenicId, request.getConfigKey()); log.debug("创建景区配置, scenicId: {}, configKey: {}", scenicId, request.getConfigKey());
CommonResponse<ScenicConfigV2DTO> response = scenicConfigV2Client.createConfig(scenicId, request); CommonResponse<ScenicConfigV2DTO> response = scenicConfigV2Client.createConfig(scenicId, request);
@@ -85,12 +72,6 @@ public class ScenicConfigIntegrationService {
return handleResponse(response, "批量更新景区配置失败"); 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) { private <T> T handleResponse(CommonResponse<T> response, String errorMessage) {
if (response == null || !response.isSuccess()) { 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.filter.ScenicFilterRequest;
import com.ycwl.basic.integration.scenic.dto.scenic.CreateScenicRequest; 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.ScenicV2DTO;
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2WithConfigDTO;
import com.ycwl.basic.integration.common.response.PageResponse; import com.ycwl.basic.integration.common.response.PageResponse;
import com.ycwl.basic.integration.scenic.dto.scenic.UpdateScenicRequest; import com.ycwl.basic.integration.scenic.dto.scenic.UpdateScenicRequest;
import lombok.RequiredArgsConstructor; 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) { public ScenicV2DTO createScenic(CreateScenicRequest request) {
log.debug("创建景区, name: {}", request.getName()); log.debug("创建景区, name: {}", request.getName());
CommonResponse<ScenicV2DTO> response = scenicV2Client.createScenic(request); CommonResponse<ScenicV2DTO> response = scenicV2Client.createScenic(request);
@@ -98,12 +71,6 @@ public class ScenicIntegrationService {
return handleResponse(response, "分页查询景区列表失败"); 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) { private <T> T handleResponse(CommonResponse<T> response, String errorMessage) {
if (response == null || !response.isSuccess()) { if (response == null || !response.isSuccess()) {
String msg = response != null && response.getMessage() != null String msg = response != null && response.getMessage() != null

View File

@@ -29,4 +29,6 @@ public interface FaceSampleMapper {
List<FaceSampleEntity> listEntityBeforeDate(Long scenicId, Date endDate); List<FaceSampleEntity> listEntityBeforeDate(Long scenicId, Date endDate);
void updateScore(Long id, Float score); void updateScore(Long id, Float score);
void updateStatus(Long id, Integer status);
} }

View File

@@ -1,5 +1,6 @@
package com.ycwl.basic.mapper; 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.PrintTaskEntity;
import com.ycwl.basic.model.pc.printer.entity.PrinterEntity; import com.ycwl.basic.model.pc.printer.entity.PrinterEntity;
import com.ycwl.basic.model.pc.printer.resp.MemberPrintResp; 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 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); MemberPrintResp getUserPhoto(Long memberId, Long scenicId, Long id);

View File

@@ -56,6 +56,12 @@ public interface SourceMapper {
int addRelations(List<MemberSourceEntity> list); int addRelations(List<MemberSourceEntity> list);
List<MemberSourceEntity> filterExistingRelations(List<MemberSourceEntity> list);
boolean sourceExists(Long sourceId);
List<MemberSourceEntity> filterValidSourceRelations(List<MemberSourceEntity> list);
int updateRelation(MemberSourceEntity memberSourceEntity); int updateRelation(MemberSourceEntity memberSourceEntity);
int freeRelations(List<Long> ids, int type); int freeRelations(List<Long> ids, int type);
@@ -92,4 +98,11 @@ public interface SourceMapper {
* @return type=2的source列表 * @return type=2的source列表
*/ */
List<SourceEntity> listImageSourcesByFaceId(Long faceId); List<SourceEntity> listImageSourcesByFaceId(Long faceId);
/**
* 从ZT-Source消息添加素材
* @param source 素材实体
* @return 影响行数
*/
int addFromZTSource(SourceEntity source);
} }

View File

@@ -25,6 +25,7 @@ public class ContentPageVO {
private int lockType; private int lockType;
// 内容id contentType为0或1时才有值 // 内容id contentType为0或1时才有值
private Long contentId; private Long contentId;
private String videoUrl;
// 模版id // 模版id
private Long templateId; private Long templateId;
private String templateCoverUrl; private String templateCoverUrl;

View File

@@ -51,18 +51,18 @@ public class AppStatisticsFunnelVO {
return "-"; // TODO: REAL return "-"; // TODO: REAL
} }
// 扫码访问人数_上传头像人数_转化率 // 扫码访问人数_推送订阅人数_转化率
@JsonProperty("scaom_ufom") @JsonProperty("scaom_ufom")
public String getScaom_ufom() { public String getScaom_ufom() {
if (uploadFaceOfMemberNum == 0 || scanCodeVisitorOfMemberNum == 0) { if (scanCodeVisitorOfMemberNum == 0 || pushOfMemberNum == 0) {
return "0.00"; return "0.00";
} }
return new BigDecimal(uploadFaceOfMemberNum) return new BigDecimal(pushOfMemberNum)
.multiply(new BigDecimal(100)) .multiply(new BigDecimal(100))
.divide(new BigDecimal(scanCodeVisitorOfMemberNum), 2, RoundingMode.HALF_UP) .divide(new BigDecimal(scanCodeVisitorOfMemberNum), 2, RoundingMode.HALF_UP)
.toString(); .toString();
} }
// 上传头像人数_推送订阅人数_转化率 // 推送订阅人数_上传头像人数_转化率
@JsonProperty("ufom_pom") @JsonProperty("ufom_pom")
public String getUfom_pom() { public String getUfom_pom() {
if (pushOfMemberNum == 0 || uploadFaceOfMemberNum == 0) { if (pushOfMemberNum == 0 || uploadFaceOfMemberNum == 0) {
@@ -76,12 +76,12 @@ public class AppStatisticsFunnelVO {
// 上传头像人数_生成视频人数_转化率 // 上传头像人数_生成视频人数_转化率
@JsonProperty("pom_cvom") @JsonProperty("pom_cvom")
public String getPom_cvom() { public String getPom_cvom() {
if (uploadFaceOfMemberNum == 0 || pushOfMemberNum == 0) { if (uploadFaceOfMemberNum == 0 || completeVideoOfMemberNum == 0) {
return "0.00"; return "0.00";
} }
return new BigDecimal(completeVideoOfMemberNum) return new BigDecimal(completeVideoOfMemberNum)
.multiply(new BigDecimal(100)) .multiply(new BigDecimal(100))
.divide(new BigDecimal(pushOfMemberNum), 2, RoundingMode.HALF_UP) .divide(new BigDecimal(uploadFaceOfMemberNum), 2, RoundingMode.HALF_UP)
.toString(); .toString();
} }
// 生成视频人数_预览视频人数_转化率 // 生成视频人数_预览视频人数_转化率

View File

@@ -1,16 +1,7 @@
package com.ycwl.basic.model.pc.scenic.resp; 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 lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
/** /**
* @Author:longbinbin * @Author:longbinbin
* @Date:2024/12/2 10:53 * @Date:2024/12/2 10:53
@@ -19,34 +10,49 @@ import java.util.Date;
@Data @Data
public class ScenicConfigResp { public class ScenicConfigResp {
// ========== 基础配置 ==========
/** /**
* 预约流程,1-预约,2-在线,3-全部 * 水印URL
*/ */
private Integer bookRoutine; private String watermarkUrl;
private Integer forceFinishTime;
private Integer tourTime;
/** /**
* 样本保存时间 * 视频存储天数
*/
private Integer sampleStoreDay;
private Integer faceStoreDay;
/**
* 视频保存时间
*/ */
private Integer videoStoreDay; 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 groupingEnable;
/**
* 优惠券功能开关
*/
private Boolean voucherEnable;
/**
* 等待时显示照片开关
*/
private Boolean showPhotoWhenWaiting; private Boolean showPhotoWhenWaiting;
// ========== 提示文案 ==========
/**
* 图片素材包提示文案
*/
private String imageSourcePackHint = "";
/**
* 视频素材包提示文案
*/
private String videoSourcePackHint = "";
} }

View File

@@ -14,4 +14,5 @@ public class TemplateConfigEntity {
private Date createDate; private Date createDate;
private Integer minimalPlaceholderFill; private Integer minimalPlaceholderFill;
private Integer automaticPlaceholderFill; private Integer automaticPlaceholderFill;
private Integer duplicateEnable;
} }

View File

@@ -1,51 +0,0 @@
package com.ycwl.basic.notify;
import com.ycwl.basic.notify.adapters.INotifyAdapter;
import com.ycwl.basic.notify.adapters.ServerChanNotifyAdapter;
import com.ycwl.basic.notify.adapters.WxMpSrvNotifyAdapter;
import com.ycwl.basic.notify.enums.NotifyType;
import java.util.HashMap;
import java.util.Map;
public class NotifyFactory {
public static INotifyAdapter get(NotifyType type) {
return switch (type) {
case SERVER_CHAN -> new ServerChanNotifyAdapter();
case WX_MP_SRV -> new WxMpSrvNotifyAdapter();
default -> throw new RuntimeException("不支持的通知类型");
};
}
public static INotifyAdapter get(NotifyType type, Map<String, String> config) {
INotifyAdapter adapter = get(type);
adapter.loadConfig(config);
return adapter;
}
protected static Map<String, INotifyAdapter> namedNotifier = new HashMap<>();
protected static INotifyAdapter defaultNotifier = null;
public static void register(String name, INotifyAdapter adapter) {
namedNotifier.put(name, adapter);
}
public static INotifyAdapter via(String name) {
INotifyAdapter adapter = namedNotifier.get(name);
if (adapter == null) {
throw new RuntimeException("未定义的通知方式:"+name);
}
return adapter;
}
public static INotifyAdapter via() {
if (defaultNotifier == null) {
throw new RuntimeException("未定义默认通知方式");
}
return defaultNotifier;
}
public static void setDefault(String defaultStorage) {
NotifyFactory.defaultNotifier = via(defaultStorage);
}
}

View File

@@ -1,11 +0,0 @@
package com.ycwl.basic.notify.adapters;
import com.ycwl.basic.notify.entity.NotifyContent;
import java.util.Map;
public interface INotifyAdapter {
void loadConfig(Map<String, String> _config);
void sendTo(NotifyContent notifyContent, String to);
}

View File

@@ -1,54 +0,0 @@
package com.ycwl.basic.notify.adapters;
import cn.hutool.http.HttpUtil;
import com.ycwl.basic.notify.entity.NotifyContent;
import com.ycwl.basic.notify.entity.ServerChanConfig;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class ServerChanNotifyAdapter implements INotifyAdapter {
ServerChanConfig config;
@Override
public void loadConfig(Map<String, String> _config) {
ServerChanConfig config = new ServerChanConfig();
config.setKey(_config.get("key"));
config.checkEverythingOK();
this.config = config;
}
@Override
public void sendTo(NotifyContent notifyContent, String to) {
scSend(notifyContent.getTitle(), notifyContent.getContent(), config.getKey());
}
public static String scSend(String title, String content, String key) {
try {
String api;
// 判断 sendkey 是否以 "sctp" 开头,并提取数字部分拼接 URL
if (key.startsWith("sctp")) {
Pattern pattern = Pattern.compile("sctp(\\d+)t");
Matcher matcher = pattern.matcher(key);
if (matcher.find()) {
String num = matcher.group(1);
api = "https://" + num + ".push.ft07.com/send/" + key +".send";
} else {
throw new IllegalArgumentException("Invalid sendkey format for sctp");
}
} else {
api = "https://sctapi.ftqq.com/" + key + ".send";
}
Map<String, Object> body = new HashMap<>();
body.put("title", title);
body.put("desp", content);
return HttpUtil.post(api, body);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}

View File

@@ -1,60 +0,0 @@
package com.ycwl.basic.notify.adapters;
import cn.hutool.http.HttpUtil;
import com.ycwl.basic.utils.JacksonUtil;
import com.ycwl.basic.notify.entity.NotifyContent;
import com.ycwl.basic.notify.entity.WxMpSrvConfig;
import java.util.Date;
import java.util.Map;
public class WxMpSrvNotifyAdapter implements INotifyAdapter{
private WxMpSrvConfig config;
@Override
public void loadConfig(Map<String, String> _config) {
WxMpSrvConfig config = new WxMpSrvConfig();
config.setAppId(_config.get("appId"));
config.setAppSecret(_config.get("appSecret"));
if (_config.containsKey("state")) {
config.setState(_config.get("state"));
}
config.checkEverythingOK();
this.config = config;
}
@Override
public void sendTo(NotifyContent notifyContent, String openId) {
Map<String, Object> params = notifyContent.getParams();
params.put("touser", openId);
params.put("miniprogram_state", config.getState());
sendServiceNotification(params);
}
private static final String SEND_TEMPLATE_MESSAGE_URL = "https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=%s";
private static final String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s";
private String ACCESS_TOKEN = "";
private Date expireTime = new Date();
private String getAccessToken() {
if (ACCESS_TOKEN != null && !ACCESS_TOKEN.isEmpty()) {
if (expireTime.getTime() > System.currentTimeMillis()) {
return ACCESS_TOKEN;
}
}
String url = String.format(ACCESS_TOKEN_URL, config.getAppId(), config.getAppSecret());
String response = HttpUtil.get(url);
Map<String, Object> jsonObject = JacksonUtil.parseObject(response, Map.class);
ACCESS_TOKEN = (String) jsonObject.get("access_token");
Integer expiresIn = (Integer) jsonObject.get("expires_in");
expireTime = new Date(System.currentTimeMillis() + (expiresIn != null ? expiresIn : 7200) * 1000);
return ACCESS_TOKEN;
}
public void sendServiceNotification(Map<String, Object> params) {
String url = String.format(SEND_TEMPLATE_MESSAGE_URL, getAccessToken());
String response = HttpUtil.post(url, JacksonUtil.toJSONString(params));
System.out.println(response);
}
}

View File

@@ -1,22 +0,0 @@
package com.ycwl.basic.notify.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.HashMap;
import java.util.Map;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class NotifyContent {
private String title;
private String content;
private Map<String, Object> params = new HashMap<>();
public NotifyContent(String title, String content) {
this.title = title;
this.content = content;
}
}

View File

@@ -1,11 +0,0 @@
package com.ycwl.basic.notify.entity;
import lombok.Data;
@Data
public class ServerChanConfig {
private String key;
public void checkEverythingOK() {
}
}

View File

@@ -1,15 +0,0 @@
package com.ycwl.basic.notify.entity;
import lombok.Data;
@Data
public class WxMpSrvConfig {
private String appId;
private String appSecret;
private String state = "formal";
private String templateId;
public void checkEverythingOK() {
}
}

View File

@@ -1,15 +0,0 @@
package com.ycwl.basic.notify.enums;
import lombok.Getter;
@Getter
public enum NotifyType {
WX_MP_SRV("WX_MP_SRV"),
SERVER_CHAN("SERVER_CHAN");
private final String type;
NotifyType(String type) {
this.type = type;
}
}

View File

@@ -1,32 +0,0 @@
package com.ycwl.basic.notify.starter;
import com.ycwl.basic.notify.NotifyFactory;
import com.ycwl.basic.notify.adapters.INotifyAdapter;
import com.ycwl.basic.notify.starter.config.NotifyConfigItem;
import com.ycwl.basic.notify.starter.config.OverallNotifyConfig;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.annotation.Configuration;
@Configuration
public class NotifyAutoConfigurator {
private final OverallNotifyConfig config;
public NotifyAutoConfigurator(OverallNotifyConfig config) {
this.config = config;
if (config != null) {
if (config.getConfigs() != null) {
loadConfig();
}
if (StringUtils.isNotBlank(config.getDefaultUse())) {
NotifyFactory.setDefault(config.getDefaultUse());
}
}
}
private void loadConfig() {
for (NotifyConfigItem item : config.getConfigs()) {
INotifyAdapter adapter = NotifyFactory.get(item.getType());
adapter.loadConfig(item.getConfig());
NotifyFactory.register(item.getName(), adapter);
}
}
}

View File

@@ -1,13 +0,0 @@
package com.ycwl.basic.notify.starter.config;
import com.ycwl.basic.notify.enums.NotifyType;
import lombok.Data;
import java.util.Map;
@Data
public class NotifyConfigItem {
private String name;
private NotifyType type;
private Map<String, String> config;
}

View File

@@ -1,15 +0,0 @@
package com.ycwl.basic.notify.starter.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
@ConfigurationProperties(prefix = "notify")
@Data
public class OverallNotifyConfig {
private String defaultUse;
private List<NotifyConfigItem> configs;
}

View File

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

View File

@@ -69,6 +69,11 @@ public class PriceProductConfig {
*/ */
private Boolean canUseVoucher; private Boolean canUseVoucher;
/**
* 是否可使用一口价优惠
*/
private Boolean canUseOnePrice;
@TableField("create_time") @TableField("create_time")
private Date createTime; private Date createTime;

View File

@@ -57,15 +57,15 @@ public interface PriceProductConfigMapper extends BaseMapper<PriceProductConfig>
/** /**
* 插入商品价格配置 * 插入商品价格配置
*/ */
@Insert("INSERT INTO price_product_config (product_type, product_id, scenic_id, product_name, base_price, original_price, unit, is_active, can_use_coupon, can_use_voucher, create_time, update_time) " + @Insert("INSERT INTO price_product_config (product_type, product_id, scenic_id, product_name, base_price, original_price, unit, is_active, can_use_coupon, can_use_voucher, can_use_one_price, create_time, update_time) " +
"VALUES (#{productType}, #{productId}, #{scenicId}, #{productName}, #{basePrice}, #{originalPrice}, #{unit}, #{isActive}, #{canUseCoupon}, #{canUseVoucher}, NOW(), NOW())") "VALUES (#{productType}, #{productId}, #{scenicId}, #{productName}, #{basePrice}, #{originalPrice}, #{unit}, #{isActive}, #{canUseCoupon}, #{canUseVoucher}, #{canUseOnePrice}, NOW(), NOW())")
int insertProductConfig(PriceProductConfig config); int insertProductConfig(PriceProductConfig config);
/** /**
* 更新商品价格配置 * 更新商品价格配置
*/ */
@Update("UPDATE price_product_config SET product_id = #{productId}, scenic_id = #{scenicId}, product_name = #{productName}, base_price = #{basePrice}, " + @Update("UPDATE price_product_config SET product_id = #{productId}, scenic_id = #{scenicId}, product_name = #{productName}, base_price = #{basePrice}, " +
"original_price = #{originalPrice}, unit = #{unit}, is_active = #{isActive}, can_use_coupon = #{canUseCoupon}, can_use_voucher = #{canUseVoucher}, update_time = NOW() WHERE id = #{id}") "original_price = #{originalPrice}, unit = #{unit}, is_active = #{isActive}, can_use_coupon = #{canUseCoupon}, can_use_voucher = #{canUseVoucher}, can_use_one_price = #{canUseOnePrice}, update_time = NOW() WHERE id = #{id}")
int updateProductConfig(PriceProductConfig config); int updateProductConfig(PriceProductConfig config);
/** /**

View File

@@ -4,8 +4,11 @@ import com.ycwl.basic.pricing.dto.DiscountDetectionContext;
import com.ycwl.basic.pricing.dto.DiscountInfo; import com.ycwl.basic.pricing.dto.DiscountInfo;
import com.ycwl.basic.pricing.dto.DiscountResult; import com.ycwl.basic.pricing.dto.DiscountResult;
import com.ycwl.basic.pricing.dto.OnePriceInfo; import com.ycwl.basic.pricing.dto.OnePriceInfo;
import com.ycwl.basic.pricing.dto.ProductItem;
import com.ycwl.basic.pricing.entity.PriceProductConfig;
import com.ycwl.basic.pricing.service.IDiscountProvider; import com.ycwl.basic.pricing.service.IDiscountProvider;
import com.ycwl.basic.pricing.service.IOnePricePurchaseService; import com.ycwl.basic.pricing.service.IOnePricePurchaseService;
import com.ycwl.basic.pricing.service.IProductConfigService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -23,6 +26,7 @@ import java.util.List;
public class OnePricePurchaseDiscountProvider implements IDiscountProvider { public class OnePricePurchaseDiscountProvider implements IDiscountProvider {
private final IOnePricePurchaseService onePricePurchaseService; private final IOnePricePurchaseService onePricePurchaseService;
private final IProductConfigService productConfigService;
@Override @Override
public String getProviderType() { public String getProviderType() {
@@ -50,6 +54,12 @@ public class OnePricePurchaseDiscountProvider implements IDiscountProvider {
return discounts; return discounts;
} }
// 检查商品是否支持一口价优惠
if (!areAllProductsSupportOnePrice(context.getProducts())) {
log.debug("存在不支持一口价优惠的商品,跳过一口价检测");
return discounts;
}
// 获取一口价信息 // 获取一口价信息
OnePriceInfo onePriceInfo = onePricePurchaseService.getOnePriceInfo( OnePriceInfo onePriceInfo = onePricePurchaseService.getOnePriceInfo(
context.getScenicId(), context.getCurrentAmount()); context.getScenicId(), context.getCurrentAmount());
@@ -170,6 +180,54 @@ public class OnePricePurchaseDiscountProvider implements IDiscountProvider {
return BigDecimal.ZERO; return BigDecimal.ZERO;
} }
/**
* 检查购物车中的所有商品是否都支持一口价优惠
*/
private boolean areAllProductsSupportOnePrice(List<ProductItem> products) {
if (products == null || products.isEmpty()) {
return true; // 空购物车时默认支持
}
for (ProductItem product : products) {
try {
// 查询商品配置
PriceProductConfig productConfig = productConfigService.getProductConfig(
product.getProductType().getCode(), product.getProductId());
if (productConfig != null) {
// 检查商品是否支持一口价优惠
if (Boolean.FALSE.equals(productConfig.getCanUseOnePrice())) {
log.debug("商品 {}({}) 不支持一口价优惠",
product.getProductType().getCode(), product.getProductId());
return false;
}
} else {
// 如果找不到具体商品配置,尝试查询 default 配置
PriceProductConfig defaultConfig = productConfigService.getProductConfig(
product.getProductType().getCode(), "default");
if (defaultConfig != null) {
if (Boolean.FALSE.equals(defaultConfig.getCanUseOnePrice())) {
log.debug("商品类型 {} 的默认配置不支持一口价优惠",
product.getProductType().getCode());
return false;
}
} else {
// 如果既没有具体配置也没有默认配置,默认支持一口价优惠
log.debug("商品 {}({}) 未找到价格配置,默认支持一口价优惠",
product.getProductType().getCode(), product.getProductId());
}
}
} catch (Exception e) {
log.warn("检查商品 {}({}) 一口价优惠支持情况时发生异常,默认支持",
product.getProductType().getCode(), product.getProductId(), e);
// 异常情况下默认支持,避免影响正常业务流程
}
}
return true;
}
/** /**
* 检查优惠叠加规则 * 检查优惠叠加规则
*/ */

View File

@@ -7,7 +7,9 @@ import com.ycwl.basic.model.pc.device.entity.DeviceConfigEntity;
import com.ycwl.basic.model.pc.device.entity.DeviceEntity; import com.ycwl.basic.model.pc.device.entity.DeviceEntity;
import com.ycwl.basic.utils.SnowFlakeUtil; import com.ycwl.basic.utils.SnowFlakeUtil;
import com.ycwl.basic.integration.device.service.DeviceIntegrationService; import com.ycwl.basic.integration.device.service.DeviceIntegrationService;
import com.ycwl.basic.integration.device.service.DeviceStatusIntegrationService;
import com.ycwl.basic.integration.device.dto.device.DeviceV2DTO; import com.ycwl.basic.integration.device.dto.device.DeviceV2DTO;
import com.ycwl.basic.integration.device.dto.status.DeviceStatusDTO;
import com.ycwl.basic.integration.common.manager.DeviceConfigManager; import com.ycwl.basic.integration.common.manager.DeviceConfigManager;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
@@ -33,6 +35,8 @@ public class DeviceRepository {
public static final String DEVICE_CACHE_KEY = "device:%s"; public static final String DEVICE_CACHE_KEY = "device:%s";
@Autowired @Autowired
private DeviceConfigIntegrationService deviceConfigIntegrationService; private DeviceConfigIntegrationService deviceConfigIntegrationService;
@Autowired
private DeviceStatusIntegrationService deviceStatusIntegrationService;
/** /**
* 将DeviceV2DTO转换为DeviceEntity * 将DeviceV2DTO转换为DeviceEntity
@@ -57,6 +61,35 @@ public class DeviceRepository {
return entity; return entity;
} }
/**
* 将DeviceV2DTO和DeviceStatusDTO合并转换为DeviceEntity
*/
private DeviceEntity convertToEntityWithStatus(DeviceV2DTO deviceDto, DeviceStatusDTO statusDto) {
if (deviceDto == null) {
return null;
}
DeviceEntity entity = convertToEntity(deviceDto);
// 合并状态信息
if (statusDto != null) {
// Boolean转Integer: true→1, false→0
entity.setOnline(statusDto.getIsOnline() != null && statusDto.getIsOnline() ? 1 : 0);
// 添加空值检查,避免NullPointerException
if (statusDto.getLastActiveTime() != null) {
entity.setKeepaliveAt(statusDto.getLastActiveTime());
}
if (statusDto.getClientIP() != null) {
entity.setIpAddr(statusDto.getClientIP());
}
} else {
// 默认离线状态
entity.setOnline(0);
}
return entity;
}
public DeviceEntity getDevice(Long deviceId) { public DeviceEntity getDevice(Long deviceId) {
log.debug("获取设备信息, deviceId: {}", deviceId); log.debug("获取设备信息, deviceId: {}", deviceId);
DeviceV2DTO deviceDto = deviceIntegrationService.getDevice(deviceId); DeviceV2DTO deviceDto = deviceIntegrationService.getDevice(deviceId);
@@ -144,11 +177,32 @@ public class DeviceRepository {
} }
public DeviceEntity getOnlineStatus(Long deviceId) { public DeviceEntity getOnlineStatus(Long deviceId) {
if (redisTemplate.hasKey(String.format(DEVICE_ONLINE_CACHE_KEY, deviceId))) { log.debug("获取设备在线状态, deviceId: {}", deviceId);
return JacksonUtil.parseObject(redisTemplate.opsForValue().get(String.format(DEVICE_ONLINE_CACHE_KEY, deviceId)), DeviceEntity.class); try {
} else { // 首先获取设备基本信息
DeviceV2DTO deviceDto = deviceIntegrationService.getDevice(deviceId);
if (deviceDto == null) {
log.warn("设备不存在, deviceId: {}", deviceId);
return null; return null;
} }
// 通过设备编号获取设备状态
DeviceStatusDTO statusDto = deviceStatusIntegrationService.getDeviceStatus(deviceDto.getNo());
// 合并设备信息和状态信息
return convertToEntityWithStatus(deviceDto, statusDto);
} catch (Exception e) {
log.error("获取设备在线状态异常, deviceId: {}", deviceId, e);
// 降级处理:尝试仅返回设备基本信息
try {
DeviceV2DTO deviceDto = deviceIntegrationService.getDevice(deviceId);
return convertToEntityWithStatus(deviceDto, null);
} catch (Exception fallbackException) {
log.error("降级获取设备信息也失败, deviceId: {}", deviceId, fallbackException);
return null;
}
}
} }
private void updateDeviceCache(DeviceEntity device) { private void updateDeviceCache(DeviceEntity device) {

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.common.util.ConfigValueUtil;
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO; import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO;
import com.ycwl.basic.integration.common.response.PageResponse; 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.ScenicIntegrationService;
import com.ycwl.basic.integration.scenic.service.ScenicConfigIntegrationService; import com.ycwl.basic.integration.scenic.service.ScenicConfigIntegrationService;
import com.ycwl.basic.integration.scenic.dto.config.ScenicConfigV2DTO; import com.ycwl.basic.integration.scenic.dto.config.ScenicConfigV2DTO;
@@ -52,8 +51,11 @@ public class ScenicRepository {
} }
public ScenicEntity getScenic(Long id) { 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; return scenicEntity;
} }
@@ -220,7 +222,7 @@ public class ScenicRepository {
} }
} }
private ScenicEntity convertToScenicEntity(ScenicV2WithConfigDTO dto) { private ScenicEntity convertToScenicEntity(ScenicV2DTO dto, ScenicConfigManager configManager) {
if (dto == null) { if (dto == null) {
return null; return null;
} }
@@ -229,19 +231,17 @@ public class ScenicRepository {
entity.setName(dto.getName()); entity.setName(dto.getName());
entity.setMpId(dto.getMpId()); entity.setMpId(dto.getMpId());
entity.setStatus(dto.getStatus().toString()); entity.setStatus(dto.getStatus().toString());
if (dto.getConfig() != null) { entity.setAddress(configManager.getString("address"));
entity.setAddress(ConfigValueUtil.getStringValue(dto.getConfig(), "address")); entity.setArea(configManager.getString("area"));
entity.setArea(ConfigValueUtil.getStringValue(dto.getConfig(), "area")); entity.setCity(configManager.getString("city"));
entity.setCity(ConfigValueUtil.getStringValue(dto.getConfig(), "city")); entity.setProvince(configManager.getString("province"));
entity.setProvince(ConfigValueUtil.getStringValue(dto.getConfig(), "province")); entity.setLatitude(configManager.getBigDecimal("latitude"));
entity.setLatitude(ConfigValueUtil.getBigDecimalValue(dto.getConfig(), "latitude")); entity.setLongitude(configManager.getBigDecimal("longitude"));
entity.setLongitude(ConfigValueUtil.getBigDecimalValue(dto.getConfig(), "longitude")); entity.setRadius(configManager.getBigDecimal("radius"));
entity.setRadius(ConfigValueUtil.getBigDecimalValue(dto.getConfig(), "radius")); entity.setPhone(configManager.getString("phone"));
entity.setPhone(ConfigValueUtil.getStringValue(dto.getConfig(), "phone")); entity.setLogoUrl(configManager.getString("logo_url"));
entity.setLogoUrl(ConfigValueUtil.getStringValue(dto.getConfig(), "logoUrl")); entity.setCoverUrl(configManager.getString("cover_url"));
entity.setCoverUrl(ConfigValueUtil.getStringValue(dto.getConfig(), "coverUrl")); entity.setKfCodeUrl(configManager.getString("kf_code_url"));
entity.setKfCodeUrl(ConfigValueUtil.getStringValue(dto.getConfig(), "kfCodeUrl"));
}
return entity; 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

@@ -294,7 +294,9 @@ public class AppScenicServiceImpl implements AppScenicService {
if (onlineStatus != null) { if (onlineStatus != null) {
deviceRespVO.setUpdateAt(onlineStatus.getKeepaliveAt()); deviceRespVO.setUpdateAt(onlineStatus.getKeepaliveAt());
deviceRespVO.setKeepaliveAt(onlineStatus.getKeepaliveAt()); deviceRespVO.setKeepaliveAt(onlineStatus.getKeepaliveAt());
if (new Date().getTime() - onlineStatus.getKeepaliveAt().getTime() > 300000) { if (onlineStatus.getKeepaliveAt() == null) {
deviceRespVO.setOnline(0);
} else if (new Date().getTime() - onlineStatus.getKeepaliveAt().getTime() > 300000) {
deviceRespVO.setOnline(0); deviceRespVO.setOnline(0);
} else { } else {
deviceRespVO.setOnline(onlineStatus.getOnline()); deviceRespVO.setOnline(onlineStatus.getOnline());

View File

@@ -72,6 +72,7 @@ import java.io.File;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator;
import java.util.Date; import java.util.Date;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
@@ -80,6 +81,7 @@ import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; 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_LOW_THRESHOLD_PFX;
import static com.ycwl.basic.constant.FaceConstant.FACE_RECOGNITION_COUNT_PFX; import static com.ycwl.basic.constant.FaceConstant.FACE_RECOGNITION_COUNT_PFX;
@@ -343,8 +345,16 @@ public class FaceServiceImpl implements FaceService {
handleVideoRecreation(scenicConfig, memberSourceEntityList, faceId, handleVideoRecreation(scenicConfig, memberSourceEntityList, faceId,
face.getMemberId(), sampleListIds, isNew); face.getMemberId(), sampleListIds, isNew);
// 保存关联关系并创建任务 // 过滤已存在的关联关系和无效的source引用,防止数据不一致
sourceMapper.addRelations(memberSourceEntityList); List<MemberSourceEntity> existingFiltered = sourceMapper.filterExistingRelations(memberSourceEntityList);
List<MemberSourceEntity> validFiltered = sourceMapper.filterValidSourceRelations(existingFiltered);
if (!validFiltered.isEmpty()) {
sourceMapper.addRelations(validFiltered);
log.debug("创建关联关系: faceId={}, 原始数量={}, 过滤后数量={}",
faceId, memberSourceEntityList.size(), validFiltered.size());
} else {
log.warn("没有有效的关联关系可创建: faceId={}, 原始数量={}", faceId, memberSourceEntityList.size());
}
memberRelationRepository.clearSCacheByFace(faceId); memberRelationRepository.clearSCacheByFace(faceId);
taskTaskService.autoCreateTaskByFaceId(faceId); taskTaskService.autoCreateTaskByFaceId(faceId);
@@ -481,7 +491,21 @@ public class FaceServiceImpl implements FaceService {
return Collections.emptyList(); 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()); DeviceConfigManager deviceConfig = deviceRepository.getDeviceConfigManager(sourceEntity.getDeviceId());
MemberSourceEntity memberSourceEntity = new MemberSourceEntity(); MemberSourceEntity memberSourceEntity = new MemberSourceEntity();
memberSourceEntity.setScenicId(face.getScenicId()); memberSourceEntity.setScenicId(face.getScenicId());
@@ -619,13 +643,31 @@ public class FaceServiceImpl implements FaceService {
.filter(item -> Integer.valueOf(2).equals(item.getType())) .filter(item -> Integer.valueOf(2).equals(item.getType()))
.count(); .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); log.debug("视频重切逻辑:视频数量 {}, 照片数量 {}", videoCount, photoCount);
// 只有照片数量大于视频数量时才创建重切任务 // 只有照片数量大于视频数量时才创建重切任务
if (photoCount > videoCount) { if (photoCount > videoCount) {
VideoPieceGetter.Task task = new VideoPieceGetter.Task(); VideoPieceGetter.Task task = new VideoPieceGetter.Task();
task.faceId = faceId; task.faceId = faceId;
task.faceSampleIds = sampleListIds; task.faceSampleIds = faceSampleIds;
task.templateId = null; task.templateId = null;
task.memberId = memberId; task.memberId = memberId;
task.callback = () -> { task.callback = () -> {
@@ -691,6 +733,7 @@ public class FaceServiceImpl implements FaceService {
contentPageVO.setContentId(memberVideoEntityList.getFirst().getVideoId()); contentPageVO.setContentId(memberVideoEntityList.getFirst().getVideoId());
VideoEntity video = videoRepository.getVideo(contentPageVO.getContentId()); VideoEntity video = videoRepository.getVideo(contentPageVO.getContentId());
if (video != null) { if (video != null) {
contentPageVO.setVideoUrl(video.getVideoUrl());
contentPageVO.setDuration(video.getDuration()); contentPageVO.setDuration(video.getDuration());
contentPageVO.setLockType(-1); contentPageVO.setLockType(-1);
TaskUpdateResult updResult = videoTaskRepository.checkTaskUpdate(video.getTaskId()); TaskUpdateResult updResult = videoTaskRepository.checkTaskUpdate(video.getTaskId());
@@ -716,6 +759,9 @@ public class FaceServiceImpl implements FaceService {
} }
} }
boolean buy = orderBiz.checkUserBuyItem(userId, contentPageVO.getGoodsType(), contentPageVO.getContentId()); boolean buy = orderBiz.checkUserBuyItem(userId, contentPageVO.getGoodsType(), contentPageVO.getContentId());
if (!buy) {
buy = orderBiz.checkUserBuyItem(userId, -1, contentPageVO.getTemplateId());
}
if (buy) { if (buy) {
contentPageVO.setIsBuy(1); contentPageVO.setIsBuy(1);
} else { } else {
@@ -924,11 +970,11 @@ public class FaceServiceImpl implements FaceService {
statusResp.setStep3Status(true); statusResp.setStep3Status(true);
statusResp.setDisplayText("帧途AI已为您渲染"+ taskStatusByFaceId.getCount() +"个vlog"); statusResp.setDisplayText("帧途AI已为您渲染"+ taskStatusByFaceId.getCount() +"个vlog");
} else { } else {
statusResp.setStep3Status(true); statusResp.setStep3Status(false);
statusResp.setDisplayText("帧途AI将会为您渲染vlog,请稍候"); statusResp.setDisplayText("帧途AI将会为您渲染vlog,请稍候");
} }
} else { } else {
statusResp.setStep3Status(true); statusResp.setStep3Status(false);
statusResp.setDisplayText("帧途AI正在为您渲染vlog,请稍候"); statusResp.setDisplayText("帧途AI正在为您渲染vlog,请稍候");
} }
return statusResp; return statusResp;
@@ -1114,7 +1160,16 @@ public class FaceServiceImpl implements FaceService {
handleVideoRecreation(scenicConfig, memberSourceEntityList, faceId, handleVideoRecreation(scenicConfig, memberSourceEntityList, faceId,
face.getMemberId(), sampleListIds, false); face.getMemberId(), sampleListIds, false);
sourceMapper.addRelations(memberSourceEntityList); // 过滤已存在的关联关系和无效的source引用,防止数据不一致
List<MemberSourceEntity> existingFiltered = sourceMapper.filterExistingRelations(memberSourceEntityList);
List<MemberSourceEntity> validFiltered = sourceMapper.filterValidSourceRelations(existingFiltered);
if (!validFiltered.isEmpty()) {
sourceMapper.addRelations(validFiltered);
log.debug("创建关联关系: faceId={}, 原始数量={}, 过滤后数量={}",
faceId, memberSourceEntityList.size(), validFiltered.size());
} else {
log.warn("没有有效的关联关系可创建: faceId={}, 原始数量={}", faceId, memberSourceEntityList.size());
}
memberRelationRepository.clearSCacheByFace(faceId); memberRelationRepository.clearSCacheByFace(faceId);
taskTaskService.autoCreateTaskByFaceId(faceId); taskTaskService.autoCreateTaskByFaceId(faceId);

View File

@@ -48,7 +48,7 @@ public interface PrinterService {
PriceObj queryPrice(Long memberId, Long scenicId); 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); 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.dto.ProductItem;
import com.ycwl.basic.pricing.enums.ProductType; import com.ycwl.basic.pricing.enums.ProductType;
import com.ycwl.basic.pricing.service.IPriceCalculationService; 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.PrintTaskEntity;
import com.ycwl.basic.model.pc.printer.entity.PrinterEntity; import com.ycwl.basic.model.pc.printer.entity.PrinterEntity;
import com.ycwl.basic.model.pc.printer.resp.MemberPrintResp; 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.apache.commons.lang3.Strings;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Slf4j @Slf4j
@@ -188,7 +192,13 @@ public class PrinterServiceImpl implements PrinterService {
@Override @Override
public boolean addUserPhoto(Long memberId, Long scenicId, String url) { 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; return true;
} }
@@ -257,15 +267,34 @@ public class PrinterServiceImpl implements PrinterService {
} }
@Override @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 -> { req.getIds().forEach(id -> {
SourceRespVO byId = sourceMapper.getById(id); SourceRespVO byId = sourceMapper.getById(id);
if (byId == null) { if (byId == null) {
resultIds.add(null);
return; 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 @Override
@@ -377,8 +406,16 @@ public class PrinterServiceImpl implements PrinterService {
printerMapper.updateUserPhotoListToPrinter(memberId, scenicId, printerId); 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 @Override
public void setUserIsBuyItem(Long memberId, Long id, Long orderId) { 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); printerMapper.setUserIsBuyItem(memberId, id, orderId);
// 创建打印任务 // 创建打印任务
List<MemberPrintResp> userPhotoListByOrderId = getUserPhotoListByOrderId(orderId); List<MemberPrintResp> userPhotoListByOrderId = getUserPhotoListByOrderId(orderId);

View File

@@ -14,9 +14,9 @@ public interface TaskService {
TemplateRespVO workerGetTemplate(Long templateId, WorkerAuthReqVo req); TemplateRespVO workerGetTemplate(Long templateId, WorkerAuthReqVo req);
void createTaskByFaceIdAndTempalteId(Long faceId, Long templateId); void createTaskByFaceIdAndTemplateId(Long faceId, Long templateId);
void createTaskByFaceIdAndTempalteId(Long faceId, Long templateId, int automatic); void createTaskByFaceIdAndTemplateId(Long faceId, Long templateId, int automatic);
void taskSuccess(Long taskId, TaskSuccessReqVo req); void taskSuccess(Long taskId, TaskSuccessReqVo req);
@@ -28,7 +28,7 @@ public interface TaskService {
void forceCreateTaskByFaceIdAndTempalteId(Long faceId, Long templateId); void forceCreateTaskByFaceIdAndTempalteId(Long faceId, Long templateId);
void autoCreateTaskByFaceId(Long id); void autoCreateTaskByFaceId(Long faceId);
Date getTaskShotDate(Long taskId); Date getTaskShotDate(Long taskId);

View File

@@ -49,6 +49,7 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.Date; import java.util.Date;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
@@ -153,11 +154,34 @@ public class TaskFaceServiceImpl implements TaskFaceService {
memberSourceEntity.setIsBuy(0); memberSourceEntity.setIsBuy(0);
} }
} }
sourceMapper.addRelations(memberSourceEntityList); // 过滤已存在的关联关系和无效的source引用,防止数据不一致
List<MemberSourceEntity> existingFiltered = sourceMapper.filterExistingRelations(memberSourceEntityList);
List<MemberSourceEntity> validFiltered = sourceMapper.filterValidSourceRelations(existingFiltered);
if (!validFiltered.isEmpty()) {
sourceMapper.addRelations(validFiltered);
log.debug("创建关联关系: faceId={}, 原始数量={}, 过滤后数量={}",
faceId, memberSourceEntityList.size(), validFiltered.size());
} else {
log.warn("没有有效的关联关系可创建: faceId={}, 原始数量={}", faceId, memberSourceEntityList.size());
}
memberRelationRepository.clearSCacheByFace(faceId); 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(); VideoPieceGetter.Task task = new VideoPieceGetter.Task();
task.faceId = faceEntity.getId(); task.faceId = faceEntity.getId();
task.faceSampleIds = sampleListIds; task.faceSampleIds = faceSampleIds;
task.memberId = face.getMemberId(); task.memberId = face.getMemberId();
VideoPieceGetter.addTask(task); VideoPieceGetter.addTask(task);
} }
@@ -228,11 +252,18 @@ public class TaskFaceServiceImpl implements TaskFaceService {
.anyMatch(record -> record.getScore() > _lowThreshold); .anyMatch(record -> record.getScore() > _lowThreshold);
respVo.setLowThreshold(isLowThreshold); respVo.setLowThreshold(isLowThreshold);
allFaceSampleIds = records.stream() allFaceSampleIds = records.stream()
.sorted(Comparator.comparing(SearchFaceResultItem::getScore).reversed())
.map(SearchFaceResultItem::getExtData) .map(SearchFaceResultItem::getExtData)
.filter(StringUtils::isNumeric) .filter(StringUtils::isNumeric)
.map(Long::valueOf) .map(Long::valueOf)
.collect(Collectors.toList()); .collect(Collectors.toList());
List<FaceSampleEntity> allFaceSampleList = faceSampleMapper.listByIds(allFaceSampleIds); 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); acceptFaceSampleIds = applySampleFilters(acceptFaceSampleIds, allFaceSampleList, scenicConfig);
List<MatchLocalRecord> collect = new ArrayList<>(); List<MatchLocalRecord> collect = new ArrayList<>();
for (SearchFaceResultItem item : records) { for (SearchFaceResultItem item : records) {
@@ -492,9 +523,8 @@ public class TaskFaceServiceImpl implements TaskFaceService {
log.debug("设备照片限制:设备ID={}, 无限制,保留{}张照片", log.debug("设备照片限制:设备ID={}, 无限制,保留{}张照片",
deviceId, deviceSampleIds.size()); deviceId, deviceSampleIds.size());
} else { } else {
// 按创建时间倒序排序,取前N张 // 取前N张
List<FaceSampleEntity> limitedSamples = deviceSamples.stream() List<FaceSampleEntity> limitedSamples = deviceSamples.stream()
.sorted(Comparator.comparing(FaceSampleEntity::getCreateAt).reversed())
.limit(limitPhoto) .limit(limitPhoto)
.collect(Collectors.toList()); .collect(Collectors.toList());

View File

@@ -5,6 +5,8 @@ import cn.hutool.crypto.digest.MD5;
import com.ycwl.basic.integration.common.manager.DeviceConfigManager; import com.ycwl.basic.integration.common.manager.DeviceConfigManager;
import com.ycwl.basic.integration.common.manager.RenderWorkerConfigManager; import com.ycwl.basic.integration.common.manager.RenderWorkerConfigManager;
import com.ycwl.basic.integration.common.manager.ScenicConfigManager; import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
import com.ycwl.basic.integration.message.dto.ZtMessage;
import com.ycwl.basic.integration.message.service.ZtMessageProducerService;
import com.ycwl.basic.repository.MemberRelationRepository; import com.ycwl.basic.repository.MemberRelationRepository;
import com.ycwl.basic.repository.SourceRepository; import com.ycwl.basic.repository.SourceRepository;
import com.ycwl.basic.utils.JacksonUtil; import com.ycwl.basic.utils.JacksonUtil;
@@ -39,10 +41,6 @@ import com.ycwl.basic.model.task.req.TaskReqVo;
import com.ycwl.basic.model.task.req.TaskSuccessReqVo; import com.ycwl.basic.model.task.req.TaskSuccessReqVo;
import com.ycwl.basic.model.task.req.WorkerAuthReqVo; import com.ycwl.basic.model.task.req.WorkerAuthReqVo;
import com.ycwl.basic.model.task.resp.TaskSyncRespVo; import com.ycwl.basic.model.task.resp.TaskSyncRespVo;
import com.ycwl.basic.notify.NotifyFactory;
import com.ycwl.basic.notify.adapters.INotifyAdapter;
import com.ycwl.basic.notify.entity.NotifyContent;
import com.ycwl.basic.notify.enums.NotifyType;
import com.ycwl.basic.repository.DeviceRepository; import com.ycwl.basic.repository.DeviceRepository;
import com.ycwl.basic.repository.FaceRepository; import com.ycwl.basic.repository.FaceRepository;
import com.ycwl.basic.repository.RenderWorkerRepository; import com.ycwl.basic.repository.RenderWorkerRepository;
@@ -69,6 +67,7 @@ import org.springframework.stereotype.Service;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Comparator;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@@ -127,6 +126,8 @@ public class TaskTaskServiceImpl implements TaskService {
private SourceRepository sourceRepository; private SourceRepository sourceRepository;
@Autowired @Autowired
private MemberRelationRepository memberRelationRepository; private MemberRelationRepository memberRelationRepository;
@Autowired
private ZtMessageProducerService ztMessageProducerService;
private RenderWorkerEntity getWorker(@NonNull WorkerAuthReqVo req) { private RenderWorkerEntity getWorker(@NonNull WorkerAuthReqVo req) {
String accessKey = req.getAccessKey(); String accessKey = req.getAccessKey();
@@ -250,12 +251,12 @@ public class TaskTaskServiceImpl implements TaskService {
@Override @Override
public void forceCreateTaskByFaceIdAndTempalteId(Long faceId, Long templateId) { public void forceCreateTaskByFaceIdAndTempalteId(Long faceId, Long templateId) {
createTaskByFaceIdAndTempalteIdInternal(faceId, templateId, 0, true); createTaskByFaceIdAndTemplateIdInternal(faceId, templateId, 0, true);
} }
@Override @Override
public void autoCreateTaskByFaceId(Long faceId) { public void autoCreateTaskByFaceId(Long faceId) {
FaceRespVO faceRespVO = faceMapper.getById(faceId); FaceEntity faceRespVO = faceRepository.getFace(faceId);
if (faceRespVO == null) { if (faceRespVO == null) {
log.info("faceId:{} is not exist", faceId); log.info("faceId:{} is not exist", faceId);
return; return;
@@ -264,18 +265,30 @@ public class TaskTaskServiceImpl implements TaskService {
log.info("faceId:{} matchSampleIds is empty", faceId); log.info("faceId:{} matchSampleIds is empty", faceId);
return; 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()) { if (faceSampleList.isEmpty()) {
log.info("faceId:{} faceSampleList is empty", faceId); log.info("faceId:{} faceSampleList is empty", faceId);
return; return;
} }
ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(faceRespVO.getScenicId()); 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()); List<TemplateRespVO> templateList = templateRepository.getTemplateListByScenicId(faceRespVO.getScenicId());
if (templateList == null || templateList.isEmpty()) { if (templateList == null || templateList.isEmpty()) {
// 没有vlog视频的情况下 // 没有vlog视频的情况下
VideoPieceGetter.Task task = new VideoPieceGetter.Task(); VideoPieceGetter.Task task = new VideoPieceGetter.Task();
task.faceId = faceId; task.faceId = faceId;
task.faceSampleIds = faceSampleList.stream().map(FaceSampleEntity::getId).toList(); task.faceSampleIds = faceSampleIds;
task.templateId = null; task.templateId = null;
task.memberId = faceRespVO.getMemberId(); task.memberId = faceRespVO.getMemberId();
task.callback = () -> { task.callback = () -> {
@@ -284,24 +297,36 @@ public class TaskTaskServiceImpl implements TaskService {
VideoPieceGetter.addTask(task); VideoPieceGetter.addTask(task);
return; return;
} }
if (Integer.valueOf(3).equals(scenicConfig.getBookRoutine()) || Integer.valueOf(4).equals(scenicConfig.getBookRoutine())) { if (Integer.valueOf(3).equals(scenicConfig.getInteger("book_routine")) || Integer.valueOf(4).equals(scenicConfig.getInteger("book_routine"))) {
// 生成全部视频的逻辑 // 生成全部视频的逻辑
templateList.forEach(template -> createTaskByFaceIdAndTempalteId(faceId, template.getId(), 1)); templateList.forEach(template -> createTaskByFaceIdAndTemplateId(faceId, template.getId(), 1));
} else { } else {
createTaskByFaceIdAndTempalteId(faceId, templateList.getFirst().getId(), 1); if (Boolean.TRUE.equals(scenicConfig.getBoolean("force_create_vlog"))) {
Long availableTemplateId = templateBiz.findFirstAvailableTemplate(templateList.stream().map(TemplateRespVO::getId).toList(), faceId, false);
if (availableTemplateId != null) {
createTaskByFaceIdAndTemplateId(faceId, availableTemplateId, 1);
} else {
log.info("faceId:{} available template is not exist", faceId);
}
} else {
// 非强制创建,只创建第一个可用模板
if (!templateList.isEmpty()) {
createTaskByFaceIdAndTemplateId(faceId, templateList.getFirst().getId(), 1);
}
}
} }
} }
@Override @Override
public void createTaskByFaceIdAndTempalteId(Long faceId, Long templateId) { public void createTaskByFaceIdAndTemplateId(Long faceId, Long templateId) {
createTaskByFaceIdAndTempalteId(faceId, templateId, 0); createTaskByFaceIdAndTemplateId(faceId, templateId, 0);
} }
@Override @Override
public void createTaskByFaceIdAndTempalteId(Long faceId, Long templateId, int automatic) { public void createTaskByFaceIdAndTemplateId(Long faceId, Long templateId, int automatic) {
createTaskByFaceIdAndTempalteIdInternal(faceId, templateId, automatic, false); createTaskByFaceIdAndTemplateIdInternal(faceId, templateId, automatic, false);
} }
private void createTaskByFaceIdAndTempalteIdInternal(Long faceId, Long templateId, int automatic, boolean forceCreate) { private void createTaskByFaceIdAndTemplateIdInternal(Long faceId, Long templateId, int automatic, boolean forceCreate) {
FaceEntity face = faceRepository.getFace(faceId); FaceEntity face = faceRepository.getFace(faceId);
if (face == null) { if (face == null) {
log.info("faceId:{} is not exist", faceId); log.info("faceId:{} is not exist", faceId);
@@ -322,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 -> { .stream().flatMap(entry -> {
DeviceConfigManager configManager = deviceRepository.getDeviceConfigManager(entry.getKey()); DeviceConfigManager configManager = deviceRepository.getDeviceConfigManager(entry.getKey());
if (configManager.getInteger("limit_video", 0) > 0) { if (configManager.getInteger("limit_video", 0) > 0) {
@@ -331,6 +358,7 @@ public class TaskTaskServiceImpl implements TaskService {
return entry.getValue().stream(); return entry.getValue().stream();
}).toList() }).toList()
.stream().map(FaceSampleEntity::getId).collect(Collectors.toList()); .stream().map(FaceSampleEntity::getId).collect(Collectors.toList());
log.info("视频切分任务: faceId={}, 原始数量={}, 筛选后数量={}", faceId, faceSampleList.size(), faceSampleIds.size());
List<SourceEntity> sourceList = sourceMapper.listVideoByScenicFaceRelation(face.getScenicId(), faceId); List<SourceEntity> sourceList = sourceMapper.listVideoByScenicFaceRelation(face.getScenicId(), faceId);
VideoPieceGetter.Task task = new VideoPieceGetter.Task(); VideoPieceGetter.Task task = new VideoPieceGetter.Task();
task.faceId = faceId; task.faceId = faceId;
@@ -429,12 +457,8 @@ public class TaskTaskServiceImpl implements TaskService {
taskStatusBiz.setFaceCutStatus(faceId, 2); taskStatusBiz.setFaceCutStatus(faceId, 2);
} }
}; };
if (!sourceList.isEmpty()) {
task.callback.onInvoke();
} else {
VideoPieceGetter.addTask(task); VideoPieceGetter.addTask(task);
} }
}
@Override @Override
public void taskSuccess(@NonNull Long taskId, @NonNull TaskSuccessReqVo req) { public void taskSuccess(@NonNull Long taskId, @NonNull TaskSuccessReqVo req) {
@@ -629,23 +653,26 @@ public class TaskTaskServiceImpl implements TaskService {
* 生成时间 {{time4.DATA}} * 生成时间 {{time4.DATA}}
* 备注 {{thing3.DATA}} * 备注 {{thing3.DATA}}
*/ */
Map<String, Object> params = new HashMap<>();
Map<String, Object> dataParam = new HashMap<>(); Map<String, Object> dataParam = new HashMap<>();
Map<String, String> videoMap = new HashMap<>(); dataParam.put("thing1", title);
videoMap.put("value", title); dataParam.put("time4", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm"));
dataParam.put("thing1", videoMap); dataParam.put("thing3", configContent);
Map<String, String> timeMap2 = new HashMap<>();
timeMap2.put("value", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm")); // 构建extra,只包含data和page
dataParam.put("time4", timeMap2); Map<String, Object> extra = new HashMap<>();
Map<String, String> remarkMap = new HashMap<>(); extra.put("data", dataParam);
remarkMap.put("value", configContent); extra.put("page", page);
dataParam.put("thing3", remarkMap);
params.put("data", dataParam); // 使用ZT消息服务发送通知(第一次通知)
params.put("page", page); ZtMessage msg = new ZtMessage();
params.put("template_id", templateId); msg.setChannelId(templateId);
log.info("视频生成通知模板参数:{},用户ID:{}", params, openId); msg.setTitle(title);
INotifyAdapter adapter = NotifyFactory.get(NotifyType.WX_MP_SRV, scenicMp.toMap()); msg.setContent("" + item.getFaceId() + "/" + item.getVideoId() + ""+configContent);
adapter.sendTo(new NotifyContent(title, page, params), openId); msg.setTarget(openId);
msg.setExtra(extra);
msg.setSendReason("视频生成通知");
msg.setSendBiz("视频生成");
ztMessageProducerService.send(msg);
} }
} }

View File

@@ -1,6 +1,8 @@
package com.ycwl.basic.task; package com.ycwl.basic.task;
import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.DateUtil;
import com.ycwl.basic.integration.message.dto.ZtMessage;
import com.ycwl.basic.integration.message.service.ZtMessageProducerService;
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO; import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO;
import com.ycwl.basic.mapper.CouponMapper; import com.ycwl.basic.mapper.CouponMapper;
import com.ycwl.basic.mapper.MemberMapper; import com.ycwl.basic.mapper.MemberMapper;
@@ -12,10 +14,6 @@ import com.ycwl.basic.model.pc.mp.MpConfigEntity;
import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity; import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity;
import com.ycwl.basic.model.pc.scenic.entity.ScenicEntity; import com.ycwl.basic.model.pc.scenic.entity.ScenicEntity;
import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery; import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery;
import com.ycwl.basic.notify.NotifyFactory;
import com.ycwl.basic.notify.adapters.INotifyAdapter;
import com.ycwl.basic.notify.entity.NotifyContent;
import com.ycwl.basic.notify.enums.NotifyType;
import com.ycwl.basic.repository.ScenicRepository; import com.ycwl.basic.repository.ScenicRepository;
import com.ycwl.basic.repository.TemplateRepository; import com.ycwl.basic.repository.TemplateRepository;
import com.ycwl.basic.integration.common.manager.ScenicConfigManager; import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
@@ -31,8 +29,11 @@ import java.util.Arrays;
import java.util.Calendar; import java.util.Calendar;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@Component @Component
@EnableScheduling @EnableScheduling
@@ -47,24 +48,33 @@ public class DownloadNotificationTasker {
private MemberMapper memberMapper; private MemberMapper memberMapper;
@Autowired @Autowired
private CouponMapper couponMapper; private CouponMapper couponMapper;
@Autowired
private ZtMessageProducerService ztMessageProducerService;
@Scheduled(cron = "0 0 21 * * *") @Scheduled(cron = "0 0 21 * * *")
public void sendDownloadNotification() { public void sendDownloadNotification() {
log.info("开始执行定时任务"); log.info("开始执行定时任务");
// 用于记录已发送通知的用户ID,避免重复发送
Set<Long> sentMemberIds = ConcurrentHashMap.newKeySet();
videoMapper.listRelationByCreateTime(new Date(System.currentTimeMillis() - 24 * 60 * 60 * 1000), new Date()) videoMapper.listRelationByCreateTime(new Date(System.currentTimeMillis() - 24 * 60 * 60 * 1000), new Date())
.forEach(item -> { .forEach(item -> {
if (item.getIsBuy() == 1) { if (item.getIsBuy() == 1) {
return; return;
} }
// 检查该用户是否已经发送过通知,避免重复发送
if (sentMemberIds.contains(item.getMemberId())) {
log.debug("用户[memberId={}]已发送过下载通知,跳过", item.getMemberId());
return;
}
sentMemberIds.add(item.getMemberId());
MemberRespVO member = memberMapper.getById(item.getMemberId()); MemberRespVO member = memberMapper.getById(item.getMemberId());
MpConfigEntity scenicMp = scenicRepository.getScenicMpConfig(member.getScenicId());
// 发送模板消息 // 发送模板消息
String templateId = scenicRepository.getVideoDownloadTemplateId(item.getScenicId()); String templateId = scenicRepository.getVideoDownloadTemplateId(item.getScenicId());
if (StringUtils.isBlank(templateId)) { if (StringUtils.isBlank(templateId)) {
log.info("模板消息为空"); log.info("模板消息为空");
return; return;
} }
log.info("发送模板消息");
ScenicEntity scenic = scenicRepository.getScenic(item.getScenicId()); ScenicEntity scenic = scenicRepository.getScenic(item.getScenicId());
ScenicConfigManager configManager = scenicRepository.getScenicConfigManager(item.getScenicId()); ScenicConfigManager configManager = scenicRepository.getScenicConfigManager(item.getScenicId());
String configTitle = configManager.getString("second_notification_title"); String configTitle = configManager.getString("second_notification_title");
@@ -86,33 +96,46 @@ public class DownloadNotificationTasker {
* 景区 {{thing1.DATA}} * 景区 {{thing1.DATA}}
* 备注 {{thing3.DATA}} * 备注 {{thing3.DATA}}
*/ */
Map<String, Object> params = new HashMap<>();
Map<String, Object> dataParam = new HashMap<>(); Map<String, Object> dataParam = new HashMap<>();
Map<String, String> videoMap = new HashMap<>(); dataParam.put("thing1", title);
videoMap.put("value", title); dataParam.put("thing3", configContent);
dataParam.put("thing1", videoMap);
Map<String, String> remarkMap = new HashMap<>(); // 构建extra,只包含data和page
remarkMap.put("value", configContent); Map<String, Object> extra = new HashMap<>();
dataParam.put("thing3", remarkMap); extra.put("data", dataParam);
params.put("data", dataParam); extra.put("page", page);
params.put("page", page);
params.put("template_id", templateId); // 使用ZT消息服务发送通知(第二次通知)
log.info("视频下载通知模板参数:{},用户ID:{}", params, member.getOpenId()); ZtMessage msg = new ZtMessage();
INotifyAdapter adapter = NotifyFactory.get(NotifyType.WX_MP_SRV, scenicMp.toMap()); msg.setChannelId(templateId);
adapter.sendTo(new NotifyContent(title, page, params), member.getOpenId()); msg.setTitle(title);
msg.setContent("" + item.getFaceId() + ""+configContent);
msg.setTarget(member.getOpenId());
msg.setExtra(extra);
msg.setSendReason("第二次通知");
msg.setSendBiz("定时通知");
ztMessageProducerService.send(msg);
}); });
} }
@Scheduled(cron = "0 0 20 * * *") @Scheduled(cron = "0 0 20 * * *")
public void sendExpireNotification() { public void sendExpireNotification() {
log.info("开始执行定时任务"); log.info("开始执行定时任务");
// 用于记录已发送通知的用户ID,避免重复发送
Set<Long> sentMemberIds = ConcurrentHashMap.newKeySet();
videoMapper.listRelationByCreateTime(new Date(System.currentTimeMillis() - 2 * 24 * 60 * 60 * 1000), new Date(System.currentTimeMillis() - 24 * 60 * 60 * 1000)) videoMapper.listRelationByCreateTime(new Date(System.currentTimeMillis() - 2 * 24 * 60 * 60 * 1000), new Date(System.currentTimeMillis() - 24 * 60 * 60 * 1000))
.forEach(item -> { .forEach(item -> {
if (item.getIsBuy() == 1) { if (item.getIsBuy() == 1) {
return; return;
} }
// 检查该用户是否已经发送过通知,避免重复发送
if (sentMemberIds.contains(item.getMemberId())) {
log.debug("用户[memberId={}]已发送过过期提醒通知,跳过", item.getMemberId());
return;
}
sentMemberIds.add(item.getMemberId());
MemberRespVO member = memberMapper.getById(item.getMemberId()); MemberRespVO member = memberMapper.getById(item.getMemberId());
MpConfigEntity scenicMp = scenicRepository.getScenicMpConfig(member.getScenicId());
ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(item.getScenicId()); ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(item.getScenicId());
Integer videoStoreDay = scenicConfig.getVideoStoreDay(); Integer videoStoreDay = scenicConfig.getVideoStoreDay();
if (videoStoreDay == null) { if (videoStoreDay == null) {
@@ -124,7 +147,6 @@ public class DownloadNotificationTasker {
log.info("模板消息为空"); log.info("模板消息为空");
return; return;
} }
log.info("发送模板消息");
ScenicEntity scenic = scenicRepository.getScenic(item.getScenicId()); ScenicEntity scenic = scenicRepository.getScenic(item.getScenicId());
ScenicConfigManager configManager = scenicRepository.getScenicConfigManager(item.getScenicId()); ScenicConfigManager configManager = scenicRepository.getScenicConfigManager(item.getScenicId());
String configTitle = configManager.getString("third_notification_title"); String configTitle = configManager.getString("third_notification_title");
@@ -147,24 +169,27 @@ public class DownloadNotificationTasker {
* 过期时间 {{time2.DATA}} * 过期时间 {{time2.DATA}}
* 备注 {{thing3.DATA}} * 备注 {{thing3.DATA}}
*/ */
Map<String, Object> params = new HashMap<>();
Map<String, Object> dataParam = new HashMap<>();
Map<String, String> videoMap = new HashMap<>();
videoMap.put("value", title);
dataParam.put("thing1", videoMap);
Map<String, String> dateMap = new HashMap<>();
Date expireDate = new Date(item.getCreateTime().getTime() + videoStoreDay * 24 * 60 * 60 * 1000); Date expireDate = new Date(item.getCreateTime().getTime() + videoStoreDay * 24 * 60 * 60 * 1000);
dateMap.put("value", DateUtil.format(expireDate, "yyyy-MM-dd HH:mm")); Map<String, Object> dataParam = new HashMap<>();
dataParam.put("time2", dateMap); dataParam.put("thing1", title);
Map<String, String> remarkMap = new HashMap<>(); dataParam.put("time2", DateUtil.format(expireDate, "yyyy-MM-dd HH:mm"));
remarkMap.put("value", configContent); dataParam.put("thing3", configContent);
dataParam.put("thing3", remarkMap);
params.put("data", dataParam); // 构建extra,只包含data和page
params.put("page", page); Map<String, Object> extra = new HashMap<>();
params.put("template_id", templateId); extra.put("data", dataParam);
log.info("视频下载通知模板参数:{},用户ID:{}", params, member.getOpenId()); extra.put("page", page);
INotifyAdapter adapter = NotifyFactory.get(NotifyType.WX_MP_SRV, scenicMp.toMap());
adapter.sendTo(new NotifyContent(title, page, params), member.getOpenId()); // 使用ZT消息服务发送通知(第三次通知 - 过期提醒)
ZtMessage msg = new ZtMessage();
msg.setChannelId(templateId);
msg.setTitle(title);
msg.setContent("" + item.getFaceId() + ""+configContent);
msg.setTarget(member.getOpenId());
msg.setExtra(extra);
msg.setSendReason("第三次通知");
msg.setSendBiz("定时通知");
ztMessageProducerService.send(msg);
}); });
} }
@@ -183,27 +208,34 @@ public class DownloadNotificationTasker {
calendar.clear(); calendar.clear();
scenicList.parallelStream().forEach(scenic -> { scenicList.parallelStream().forEach(scenic -> {
Long scenicId = Long.parseLong(scenic.getId()); Long scenicId = Long.parseLong(scenic.getId());
ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId); ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(scenicId);
if (scenicConfig == null) { if (scenicConfig == null) {
return; return;
} }
if (StringUtils.isEmpty(scenicConfig.getExtraNotificationTime())) { if (StringUtils.isEmpty(scenicConfig.getString("extra_notification_time"))) {
return; return;
} }
List<String> timeList = Arrays.asList(StringUtils.split(scenicConfig.getExtraNotificationTime(), ",")); List<String> timeList = Arrays.asList(StringUtils.split(scenicConfig.getString("extra_notification_time"), ","));
if (!timeList.contains(String.valueOf(currentHour))) { if (!timeList.contains(String.valueOf(currentHour))) {
return; return;
} }
log.info("当前景区{},配置了{}", scenic.getName(), scenicConfig.getExtraNotificationTime()); log.info("当前景区{},配置了{}", scenic.getName(), scenicConfig.getString("extra_notification_time"));
// 使用线程安全的Set记录已发送通知的用户ID,避免重复发送
Set<Long> sentMemberIds = ConcurrentHashMap.newKeySet();
videoMapper.listRelationByCreateTime(DateUtil.beginOfDay(new Date()), new Date()) videoMapper.listRelationByCreateTime(DateUtil.beginOfDay(new Date()), new Date())
.stream() .stream()
.filter(item -> item.getIsBuy() == 0) .filter(item -> item.getIsBuy() == 0)
.filter(item -> item.getScenicId().equals(scenicId)) .filter(item -> item.getScenicId().equals(scenicId))
.parallel() .parallel()
.forEach(item -> { .forEach(item -> {
// 检查该用户是否已经发送过通知,避免重复发送
if (!sentMemberIds.add(item.getMemberId())) {
log.debug("用户[memberId={}]已发送过额外下载通知,跳过", item.getMemberId());
return;
}
MemberRespVO member = memberMapper.getById(item.getMemberId()); MemberRespVO member = memberMapper.getById(item.getMemberId());
MpConfigEntity scenicMp = scenicRepository.getScenicMpConfig(member.getScenicId());
// 发送模板消息 // 发送模板消息
String templateId = scenicRepository.getVideoDownloadTemplateId(item.getScenicId()); String templateId = scenicRepository.getVideoDownloadTemplateId(item.getScenicId());
if (StringUtils.isBlank(templateId)) { if (StringUtils.isBlank(templateId)) {
@@ -219,7 +251,6 @@ public class DownloadNotificationTasker {
return; return;
} }
log.info("发送模板消息");
String title = configTitle.replace("【景区】", scenic.getName()); String title = configTitle.replace("【景区】", scenic.getName());
String page; String page;
if (configManager.getBoolean("grouping_enable", false)) { if (configManager.getBoolean("grouping_enable", false)) {
@@ -231,20 +262,25 @@ public class DownloadNotificationTasker {
* 景区 {{thing1.DATA}} * 景区 {{thing1.DATA}}
* 备注 {{thing3.DATA}} * 备注 {{thing3.DATA}}
*/ */
Map<String, Object> params = new HashMap<>();
Map<String, Object> dataParam = new HashMap<>(); Map<String, Object> dataParam = new HashMap<>();
Map<String, String> videoMap = new HashMap<>(); dataParam.put("thing1", title);
videoMap.put("value", title); dataParam.put("thing3", configContent);
dataParam.put("thing1", videoMap);
Map<String, String> remarkMap = new HashMap<>(); // 构建extra,只包含data和page
remarkMap.put("value", configContent); Map<String, Object> extra = new HashMap<>();
dataParam.put("thing3", remarkMap); extra.put("data", dataParam);
params.put("data", dataParam); extra.put("page", page);
params.put("page", page);
params.put("template_id", templateId); // 使用ZT消息服务发送通知(额外下载通知)
log.info("视频下载通知模板参数:{},用户ID:{}", params, member.getOpenId()); ZtMessage msg = new ZtMessage();
INotifyAdapter adapter = NotifyFactory.get(NotifyType.WX_MP_SRV, scenicMp.toMap()); msg.setChannelId(templateId);
adapter.sendTo(new NotifyContent(title, page, params), member.getOpenId()); msg.setTitle(title);
msg.setContent("" + item.getFaceId() + ""+configContent);
msg.setTarget(member.getOpenId());
msg.setExtra(extra);
msg.setSendReason("景区额外配置:" + scenicConfig.getString("extra_notification_time"));
msg.setSendBiz("定时通知");
ztMessageProducerService.send(msg);
}); });
}); });
} }

View File

@@ -53,6 +53,7 @@ import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.IntStream; import java.util.stream.IntStream;
@@ -145,7 +146,7 @@ public class VideoPieceGetter {
new ArrayBlockingQueue<>(128), new ArrayBlockingQueue<>(128),
threadFactory threadFactory
); );
List<String> currentUnFinPlaceholder = new ArrayList<>(); Map<String, AtomicInteger> currentUnFinPlaceholder = new ConcurrentHashMap<>();
List<FaceSampleEntity> list = faceSampleMapper.listByIds(task.getFaceSampleIds()); List<FaceSampleEntity> list = faceSampleMapper.listByIds(task.getFaceSampleIds());
Map<Long, Long> pairDeviceMap = new ConcurrentHashMap<>(); Map<Long, Long> pairDeviceMap = new ConcurrentHashMap<>();
if (!list.isEmpty()) { if (!list.isEmpty()) {
@@ -169,13 +170,21 @@ public class VideoPieceGetter {
}) })
.collect(Collectors.groupingBy(FaceSampleEntity::getDeviceId)); .collect(Collectors.groupingBy(FaceSampleEntity::getDeviceId));
if (templatePlaceholder != null) { if (templatePlaceholder != null) {
IntStream.range(0, templatePlaceholder.size()).forEach(i -> { templatePlaceholder.forEach(deviceId -> {
currentUnFinPlaceholder.add(templatePlaceholder.get(i)); 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 { } else {
collection.keySet().forEach(i -> { collection.keySet().forEach(deviceId -> {
currentUnFinPlaceholder.add(i.toString()); currentUnFinPlaceholder.put(deviceId.toString(), new AtomicInteger(1));
}); });
log.info("[Placeholder初始化] 无templateId,初始化完成:设备数={}",
currentUnFinPlaceholder.size());
} }
collection.values().forEach(faceSampleList -> { collection.values().forEach(faceSampleList -> {
executor.execute(() -> { executor.execute(() -> {
@@ -188,27 +197,61 @@ public class VideoPieceGetter {
} }
} }
isFirst.set(false); isFirst.set(false);
// 处理关联设备:如果当前设备是某个主设备的配对设备,也处理主设备
if (pairDeviceMap.containsValue(faceSample.getDeviceId())) { if (pairDeviceMap.containsValue(faceSample.getDeviceId())) {
// 有关联设备!
// 找到对应的deviceId
pairDeviceMap.entrySet().stream() pairDeviceMap.entrySet().stream()
.filter(entry -> entry.getValue().equals(faceSample.getDeviceId())) .filter(entry -> entry.getValue().equals(faceSample.getDeviceId()))
.map(Map.Entry::getKey).forEach(pairDeviceId -> { .map(Map.Entry::getKey).forEach(pairDeviceId -> {
log.info("找到同景区关联设备:{} -> {}", pairDeviceId, faceSample.getDeviceId()); log.info("找到同景区关联设备:{} -> {}", pairDeviceId, faceSample.getDeviceId());
if (pairDeviceId != null) { if (pairDeviceId != null) {
doCut(pairDeviceId, faceSample.getId(), faceSample.getCreateAt(), task); 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); doCut(faceSample.getDeviceId(), faceSample.getId(), faceSample.getCreateAt(), task);
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()); currentUnFinPlaceholder.remove(faceSample.getDeviceId().toString());
log.info("[Placeholder完成] 设备 {} 的placeholder已满足并移除,剩余设备数={}",
faceSample.getDeviceId(), currentUnFinPlaceholder.size());
}
}
// 如果有templateId,检查是否所有placeholder都已满足
if (templatePlaceholder != null) { 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 (currentUnFinPlaceholder.isEmpty()) {
if (!invoke.get()) { if (!invoke.get()) {
invoke.set(true); invoke.set(true);
log.info("[Callback调用] 所有placeholder已满足,currentUnFinPlaceholder为空,提前调用callback");
task.getCallback().onInvoke(); task.getCallback().onInvoke();
} else {
log.warn("[Callback跳过] 所有placeholder已满足,但callback已被调用过");
} }
} }
} }
@@ -232,7 +275,11 @@ public class VideoPieceGetter {
if (null != task.getCallback()) { if (null != task.getCallback()) {
if (!invoke.get()) { if (!invoke.get()) {
invoke.set(true); invoke.set(true);
log.info("[Callback调用] 兜底调用callback,currentUnFinPlaceholder剩余设备数={}",
currentUnFinPlaceholder.size());
task.getCallback().onInvoke(); task.getCallback().onInvoke();
} else {
log.info("[Callback跳过] 兜底检查,callback已被调用过");
} }
} }
if (task.getFaceId() != null) { if (task.getFaceId() != null) {
@@ -299,7 +346,9 @@ public class VideoPieceGetter {
ffmpegTask.setFileList(listByDtRange); ffmpegTask.setFileList(listByDtRange);
ffmpegTask.setDuration(duration); ffmpegTask.setDuration(duration);
ffmpegTask.setOffsetStart(BigDecimal.valueOf(offset, 3)); 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()); ffmpegTask.setOutputFile(outFile.getAbsolutePath());
boolean result = startFfmpegTask(ffmpegTask); boolean result = startFfmpegTask(ffmpegTask);
if (!result) { if (!result) {
@@ -318,6 +367,10 @@ public class VideoPieceGetter {
} }
if (source == null) { if (source == null) {
SourceEntity imgSource = sourceMapper.findBySampleId(faceSampleId); SourceEntity imgSource = sourceMapper.findBySampleId(faceSampleId);
if (imgSource == null) {
log.warn("imgSource为null,跳过保存source记录, faceSampleId: {}", faceSampleId);
return false;
}
SourceEntity sourceEntity = new SourceEntity(); SourceEntity sourceEntity = new SourceEntity();
sourceEntity.setId(SnowFlakeUtil.getLongId()); sourceEntity.setId(SnowFlakeUtil.getLongId());
sourceEntity.setCreateTime(baseTime); sourceEntity.setCreateTime(baseTime);
@@ -333,7 +386,14 @@ public class VideoPieceGetter {
sourceEntity.setScenicId(deviceV2.getScenicId()); sourceEntity.setScenicId(deviceV2.getScenicId());
sourceEntity.setDeviceId(deviceId); sourceEntity.setDeviceId(deviceId);
sourceEntity.setType(1); sourceEntity.setType(1);
// 先插入source记录
sourceMapper.add(sourceEntity);
videoReUploader.addTask(sourceEntity.getId());
// 然后处理关联关系
if (task.memberId != null && task.faceId != null) { if (task.memberId != null && task.faceId != null) {
List<MemberSourceEntity> memberSourceEntities = memberRelationRepository.listSourceByFaceRelation(task.faceId, 1);
MemberSourceEntity videoSource = new MemberSourceEntity(); MemberSourceEntity videoSource = new MemberSourceEntity();
videoSource.setMemberId(task.getMemberId()); videoSource.setMemberId(task.getMemberId());
videoSource.setType(1); videoSource.setType(1);
@@ -348,11 +408,17 @@ public class VideoPieceGetter {
} else { } else {
videoSource.setIsBuy(0); videoSource.setIsBuy(0);
} }
boolean anyMatch = memberSourceEntities.stream().anyMatch(memberSourceEntity -> {
return memberSourceEntity.getSourceId().equals(videoSource.getSourceId())
&& memberSourceEntity.getType().equals(videoSource.getType())
&& memberSourceEntity.getFaceId().equals(videoSource.getFaceId());
});
if (!anyMatch) {
// source已插入,可以直接添加关联关系
sourceMapper.addRelation(videoSource); sourceMapper.addRelation(videoSource);
}
memberRelationRepository.clearSCacheByFace(task.faceId); memberRelationRepository.clearSCacheByFace(task.faceId);
} }
sourceMapper.add(sourceEntity);
videoReUploader.addTask(sourceEntity.getId());
} else { } else {
source.setVideoUrl(url); source.setVideoUrl(url);
if (StringUtils.isNotBlank(config.getString("video_crop"))) { if (StringUtils.isNotBlank(config.getString("video_crop"))) {
@@ -362,11 +428,9 @@ public class VideoPieceGetter {
videoReUploader.addTask(source.getId()); videoReUploader.addTask(source.getId());
} }
} else { } else {
// 有原视频 // 有原视频,source已存在,可以直接添加关联关系
if (task.memberId != null && task.faceId != null) { if (task.memberId != null && task.faceId != null) {
int count = sourceMapper.hasRelationTo(task.getMemberId(), source.getId(), 1); List<MemberSourceEntity> memberSourceEntities = memberRelationRepository.listSourceByFaceRelation(task.faceId, 1);
if (count <= 0) {
// 没有关联
IsBuyRespVO isBuy = orderBiz.isBuy(task.getMemberId(), deviceV2.getScenicId(), 1, task.getFaceId()); IsBuyRespVO isBuy = orderBiz.isBuy(task.getMemberId(), deviceV2.getScenicId(), 1, task.getFaceId());
MemberSourceEntity videoSource = new MemberSourceEntity(); MemberSourceEntity videoSource = new MemberSourceEntity();
videoSource.setId(SnowFlakeUtil.getLongId()); videoSource.setId(SnowFlakeUtil.getLongId());
@@ -382,9 +446,17 @@ public class VideoPieceGetter {
videoSource.setIsBuy(0); videoSource.setIsBuy(0);
} }
videoSource.setSourceId(source.getId()); videoSource.setSourceId(source.getId());
// 没有关联
boolean anyMatch = memberSourceEntities.stream().anyMatch(memberSourceEntity -> {
return memberSourceEntity.getSourceId().equals(videoSource.getSourceId())
&& memberSourceEntity.getType().equals(videoSource.getType())
&& memberSourceEntity.getFaceId().equals(videoSource.getFaceId());
});
if (!anyMatch) {
// source已存在,可以直接添加关联关系
sourceMapper.addRelation(videoSource); sourceMapper.addRelation(videoSource);
memberRelationRepository.clearSCacheByFace(task.faceId);
} }
memberRelationRepository.clearSCacheByFace(task.faceId);
} }
} }
return true; return true;
@@ -414,7 +486,9 @@ public class VideoPieceGetter {
boolean notOk = task.getFileList().stream().map(file -> { boolean notOk = task.getFileList().stream().map(file -> {
try { try {
if (file.isNeedDownload() || (!file.getName().endsWith(".ts"))) { 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); boolean result = convertMp4ToTs(file, tmpFile);
// 因为是并行转换,没法保证顺序,就直接存里面 // 因为是并行转换,没法保证顺序,就直接存里面
if (result) { if (result) {

View File

@@ -28,7 +28,11 @@ public class ImageUtils {
} }
public static MultipartFile cropImage(MultipartFile file, int x, int y, int w, int h) throws IOException { public static MultipartFile cropImage(MultipartFile file, int x, int y, int w, int h) throws IOException {
BufferedImage image = ImageIO.read(file.getInputStream()); BufferedImage image = null;
BufferedImage targetImage = null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
image = ImageIO.read(file.getInputStream());
log.info("图片宽高:{}", image.getWidth() + "x" + image.getHeight()); log.info("图片宽高:{}", image.getWidth() + "x" + image.getHeight());
log.info("图片裁切:{}@{}", w + "x" + h, x + "," + y); log.info("图片裁切:{}@{}", w + "x" + h, x + "," + y);
if (image.getWidth() < w) { if (image.getWidth() < w) {
@@ -50,11 +54,27 @@ public class ImageUtils {
targetY = image.getHeight() - h; targetY = image.getHeight() - h;
} }
log.info("图片实际裁切:{}@{}", w + "x" + h, targetX + "," + targetY); log.info("图片实际裁切:{}@{}", w + "x" + h, targetX + "," + targetY);
BufferedImage targetImage = image.getSubimage(targetX, targetY, w, h); targetImage = image.getSubimage(targetX, targetY, w, h);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(targetImage, "jpg", baos); ImageIO.write(targetImage, "jpg", baos);
baos.close();
return new Base64DecodedMultipartFile(baos.toByteArray(), "image/jpeg"); 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 { public static class Base64DecodedMultipartFile implements MultipartFile {

View File

@@ -1,16 +1,20 @@
package com.ycwl.basic.watchdog; 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.mapper.TaskMapper;
import com.ycwl.basic.model.pc.task.entity.TaskEntity; 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.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set;
@Component @Component
@Profile("prod") @Profile("prod")
@@ -19,41 +23,145 @@ public class TaskWatchDog {
@Autowired @Autowired
private TaskMapper taskMapper; 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) @Scheduled(fixedDelay = 1000 * 60L)
public void scanTaskStatus() { public void scanTaskStatus() {
List<TaskEntity> allNotRunningTaskList = taskMapper.selectAllNotRunning(); 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(); List<TaskEntity> allFailedTaskList = taskMapper.selectAllFailed();
if (allFailedTaskList.size() > 5) { List<TaskEntity> allRunningTaskList = taskMapper.selectAllRunning();
if (content.length() > 0) {
content.append("\n"); // 检查任务积压
} checkTaskBacklog(allNotRunningTaskList);
content.append("当前存在超过5个失败任务(status=3),请及时检查和处理!失败任务数量:").append(allFailedTaskList.size());
// 检查失败任务
checkFailedTasks(allFailedTaskList);
// 检查长时间运行任务
checkLongRunningTasks(allRunningTaskList);
} }
List<TaskEntity> allRunningTaskList = taskMapper.selectAllRunning(); /**
for (TaskEntity taskEntity : 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) { if (taskEntity.getStartTime() == null) {
continue; continue;
} }
// startTime已经过去3分钟了 // startTime已经过去3分钟了
if (System.currentTimeMillis() - taskEntity.getStartTime().getTime() > 1000 * 60 * 3) { if (System.currentTimeMillis() - taskEntity.getStartTime().getTime() > 1000 * 60 * 3) {
if (content.length() > 0) { String taskKey = LONG_RUNNING_TASK_PREFIX + taskEntity.getId();
content.append("\n"); currentLongRunningTasks.add(taskKey);
}
content.append("当前【").append(taskEntity.getWorkerId()).append("】渲染机的【").append(taskEntity.getId()).append("】任务已超过3分钟未完成!"); if (shouldSendNotification(taskKey)) {
String content = String.format("当前【%s】渲染机的【%d】任务已超过3分钟未完成!",
taskEntity.getWorkerId(), taskEntity.getId());
sendNotification("长时间运行任务警告", content, taskKey);
} }
} }
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

@@ -17,6 +17,24 @@ feign:
okhttp: okhttp:
enabled: true enabled: true
# Kafka配置
kafka:
enabled: false # 默认关闭,需要时手动开启
bootstrap-servers: 100.64.0.12:39092
consumer:
group-id: liuying-microservice-dev
auto-offset-reset: earliest
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
producer:
acks: all
retries: 3
batch-size: 16384
linger-ms: 1
buffer-memory: 33554432
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
# 开发环境日志配置 # 开发环境日志配置
logging: logging:
level: level:

View File

@@ -41,6 +41,11 @@
set score = #{score} set score = #{score}
where id = #{id} where id = #{id}
</update> </update>
<update id="updateStatus">
update face_sample
set `status` = #{status}
where id = #{id}
</update>
<delete id="deleteById"> <delete id="deleteById">
delete from face_sample where id = #{id} delete from face_sample where id = #{id}
</delete> </delete>

View File

@@ -99,6 +99,10 @@
member_photo_data AS ( member_photo_data AS (
SELECT mp.member_id, 3 as type, mp.id, mp.crop_url as url, mp.quantity, mp.status, mp.create_time SELECT mp.member_id, 3 as type, mp.id, mp.crop_url as url, mp.quantity, mp.status, mp.create_time
FROM member_print mp FROM member_print mp
),
member_aio_photo_data AS (
SELECT 4 as type, s.id, s.url as url
FROM source s
) )
SELECT SELECT
oi.id AS oiId, oi.id AS oiId,
@@ -137,13 +141,14 @@
WHEN '1' THEN msd.url WHEN '1' THEN msd.url
WHEN '2' THEN msd.url WHEN '2' THEN msd.url
WHEN '3' THEN mpd.url WHEN '3' THEN mpd.url
WHEN '4' THEN msd.url WHEN '4' THEN mpa.url
END AS imgUrl END AS imgUrl
FROM order_item oi FROM order_item oi
LEFT JOIN `order` o ON oi.order_id = o.id LEFT JOIN `order` o ON oi.order_id = o.id
LEFT JOIN member_video_data mvd ON o.face_id = mvd.face_id AND oi.goods_id = mvd.video_id LEFT JOIN member_video_data mvd ON o.face_id = mvd.face_id AND oi.goods_id = mvd.video_id
LEFT JOIN member_source_data msd ON o.face_id = msd.face_id AND oi.goods_id = msd.face_id AND msd.type = oi.goods_type LEFT JOIN member_source_data msd ON o.face_id = msd.face_id AND oi.goods_id = msd.face_id AND msd.type = oi.goods_type
LEFT JOIN member_photo_data mpd ON oi.goods_id = mpd.id AND mpd.type = oi.goods_type LEFT JOIN member_photo_data mpd ON oi.goods_id = mpd.id AND mpd.type = oi.goods_type
LEFT JOIN member_aio_photo_data mpa ON oi.goods_id = mpa.id AND mpa.type = oi.goods_type
WHERE oi.order_id = #{id}; WHERE oi.order_id = #{id};
</select> </select>

View File

@@ -95,7 +95,7 @@
NOW() NOW()
) )
</insert> </insert>
<insert id="addUserPhoto"> <insert id="addUserPhoto" useGeneratedKeys="true" keyProperty="id">
INSERT INTO member_print ( INSERT INTO member_print (
member_id, member_id,
scenic_id, scenic_id,
@@ -108,8 +108,8 @@
) VALUES ( ) VALUES (
#{memberId}, #{memberId},
#{scenicId}, #{scenicId},
#{url}, #{origUrl},
#{url}, #{cropUrl},
1, 1,
0, 0,
NOW(), 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) 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}) values (#{id}, #{scenicId}, #{deviceId}, #{url}, #{videoUrl}, #{type}, #{faceSampleId}, #{posJson}, #{createTime})
</insert> </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"> <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>) 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>) values (#{scenicId}, #{faceId}, #{memberId}, #{sourceId}, #{isBuy}, #{type}, #{orderId}<if test="isFree">, #{isFree}</if>)
@@ -16,6 +20,105 @@
(#{item.scenicId}, #{item.faceId}, #{item.memberId}, #{item.sourceId}, #{item.isBuy}, #{item.type}, #{item.orderId}, #{item.isFree}) (#{item.scenicId}, #{item.faceId}, #{item.memberId}, #{item.sourceId}, #{item.isBuy}, #{item.type}, #{item.orderId}, #{item.isFree})
</foreach> </foreach>
</insert> </insert>
<select id="filterExistingRelations" resultType="com.ycwl.basic.model.pc.source.entity.MemberSourceEntity">
<choose>
<when test="list != null and list.size() > 0">
SELECT
r.memberId as memberId,
r.sourceId as sourceId,
r.type as type,
r.faceId as faceId,
r.scenicId as scenicId,
r.isBuy as isBuy,
r.orderId as orderId,
r.isFree as isFree,
r.id as id
FROM (
<foreach collection="list" item="item" separator=" UNION ALL ">
SELECT
#{item.memberId} as memberId,
#{item.sourceId} as sourceId,
#{item.type} as type,
#{item.faceId} as faceId,
#{item.scenicId} as scenicId,
#{item.isBuy} as isBuy,
#{item.orderId} as orderId,
#{item.isFree} as isFree,
#{item.id} as id
</foreach>
) r
WHERE NOT EXISTS (
SELECT 1 FROM member_source ms
WHERE ms.member_id = r.memberId
AND ms.source_id = r.sourceId
AND ms.type = r.type
AND ms.face_id = r.faceId
)
</when>
<otherwise>
SELECT
NULL as memberId,
NULL as sourceId,
NULL as type,
NULL as faceId,
NULL as scenicId,
NULL as isBuy,
NULL as orderId,
NULL as isFree,
NULL as id
WHERE 1 = 0
</otherwise>
</choose>
</select>
<select id="sourceExists" resultType="boolean">
SELECT COUNT(1) > 0 FROM source WHERE id = #{sourceId}
</select>
<select id="filterValidSourceRelations" resultType="com.ycwl.basic.model.pc.source.entity.MemberSourceEntity">
<choose>
<when test="list != null and list.size() > 0">
SELECT
r.memberId as memberId,
r.sourceId as sourceId,
r.type as type,
r.faceId as faceId,
r.scenicId as scenicId,
r.isBuy as isBuy,
r.orderId as orderId,
r.isFree as isFree,
r.id as id
FROM (
<foreach collection="list" item="item" separator=" UNION ALL ">
SELECT
#{item.memberId} as memberId,
#{item.sourceId} as sourceId,
#{item.type} as type,
#{item.faceId} as faceId,
#{item.scenicId} as scenicId,
#{item.isBuy} as isBuy,
#{item.orderId} as orderId,
#{item.isFree} as isFree,
#{item.id} as id
</foreach>
) r
WHERE EXISTS (
SELECT 1 FROM source s WHERE s.id = r.sourceId
)
</when>
<otherwise>
SELECT
NULL as memberId,
NULL as sourceId,
NULL as type,
NULL as faceId,
NULL as scenicId,
NULL as isBuy,
NULL as orderId,
NULL as isFree,
NULL as id
WHERE 1 = 0
</otherwise>
</choose>
</select>
<insert id="addSourceWatermark"> <insert id="addSourceWatermark">
insert source_watermark(source_id, face_id, watermark_type, watermark_url) insert source_watermark(source_id, face_id, watermark_type, watermark_url)
values (#{sourceId}, #{faceId}, #{type}, #{url}) values (#{sourceId}, #{faceId}, #{type}, #{url})

View File

@@ -6,8 +6,8 @@
values (#{id}, #{scenicId}, #{name}, #{pid}, #{isPlaceholder}, #{sourceUrl}, #{effects}, #{luts}, #{overlays}, #{audios}, #{coverUrl}, #{frameRate}, #{speed}, #{price}, #{slashPrice}, #{sort}, #{cropEnable}, #{zoomCut}, #{onlyIf}, #{resolution}, now()) values (#{id}, #{scenicId}, #{name}, #{pid}, #{isPlaceholder}, #{sourceUrl}, #{effects}, #{luts}, #{overlays}, #{audios}, #{coverUrl}, #{frameRate}, #{speed}, #{price}, #{slashPrice}, #{sort}, #{cropEnable}, #{zoomCut}, #{onlyIf}, #{resolution}, now())
</insert> </insert>
<insert id="addConfig"> <insert id="addConfig">
insert into template_config(id, template_id, create_time) insert into template_config(id, template_id, duplicate_enable, create_time)
values (#{id}, #{templateId}, now()) values (#{id}, #{templateId}, #{duplicateEnable}, now())
</insert> </insert>
<update id="update"> <update id="update">
update template update template
@@ -52,7 +52,8 @@
<set> <set>
<if test="isDefault!= null">is_default = #{isDefault}, </if> <if test="isDefault!= null">is_default = #{isDefault}, </if>
minimal_placeholder_fill = #{minimalPlaceholderFill}, minimal_placeholder_fill = #{minimalPlaceholderFill},
automatic_placeholder_fill = #{automaticPlaceholderFill} automatic_placeholder_fill = #{automaticPlaceholderFill},
duplicate_enable = #{duplicateEnable}
</set> </set>
where id = #{id} where id = #{id}
</update> </update>