Compare commits

..

50 Commits

Author SHA1 Message Date
09d142aa98 feat(coupon): 添加优惠券领取后有效期配置功能
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 在数据库插入和更新操作中添加 valid_days_after_claim 字段支持
- 在使用优惠券时增加对优惠券全局有效性和领取记录过期时间的验证逻辑
- 添加对已过期领取记录的筛选和错误提示处理
- 新增优惠券使用请求、状态枚举和异常类的测试用例
- 实现优惠券配置的完整有效期验证流程
2026-02-14 15:14:42 +08:00
143185926c feat(pricing): 添加全局配置查询功能并优化价格计算逻辑
- 在PriceProductConfigMapper中新增selectGlobalByProductTypeAndId方法,用于查询全局配置(排除景区级配置)
- 在PriceTierConfigMapper中新增selectGlobalByProductTypeAndQuantity方法,用于查询全局阶梯价格配置
- 移除价格计算服务中的default配置兜底逻辑,简化价格计算流程
- 更新ProductConfigServiceImpl中的配置查询逻辑,使用新的全局配置查询方法替代原有查询方式
- 优化配置查找顺序,提高全局配置的优先级和查询效率
2026-02-14 13:48:31 +08:00
cbbdd02003 feat(voucher): 优化券码领取接口返回结果
- 添加success和message字段到VoucherCodeResp响应类
- 修改券码领取逻辑,失败时不抛出异常而是返回失败结果
- 实现券码状态、用户权限和批次验证的统一响应格式
- 成功领取时设置success为true并返回成功消息
- 失败时设置success为false并返回具体失败原因
- 保持券码基础信息在失败情况下仍然返回给前端
2026-02-14 05:07:08 +08:00
1110b5409b refactor(voucher): 移除券码领取接口中的景区ID参数验证
- 删除 VoucherClaimReq 中的 scenicId 字段
- 移除券码领取接口中对景区ID的空值检查
- 更新查询条件,不再按景区ID过滤券码
- 修改错误提示信息为"券码不存在"
- 调整领券权限校验逻辑,使用券码关联的景区ID进行验证
2026-02-14 05:07:04 +08:00
4ac59b1f31 fix(puzzle): 解决拼图生成服务重复内容检测问题
- 添加对正在生成中记录的状态检查,避免并发重复写入
- 实现等待机制处理相同内容正在生成的情况
- 优化数据库查询逻辑,同时匹配成功和生成中的记录
- 仅对成功记录标记素材版本缓存,避免生成中记录失败时的错误标记
- 更新日志输出包含记录状态信息以便调试
- 添加超时机制确保系统稳定性
2026-02-14 05:07:00 +08:00
671cad4687 feat(goods): 添加摄影师拍照模式支持
- 在GoodsServiceImpl中集成景区配置管理器检查
- 添加摄影模式下的成员资源查询逻辑
- 实现视频任务状态成功设置和计数返回
- 更新FaceServiceImpl中的显示文本为更准确的描述
- 将"记录大片"改为"拍摄照片"以匹配实际功能
2026-02-14 05:06:54 +08:00
90fb0df69c feat(face): 添加摄影师拍照功能支持
- 在ContentPageVO中新增origUrl字段用于存储原始图片地址
- 集成DeviceV2DTO设备数据结构支持摄影师设备管理
- 添加SourceRepository依赖注入实现资源数据访问
- 实现景区模式2下的摄影师拍照内容展示逻辑
- 支持按设备分组显示摄影师拍摄的照片内容
- 添加摄影师拍照相关的购买状态和锁定类型控制
- 更新人脸识别页面查询返回摄影师拍摄的内容列表
- 优化景区配置管理器变量
2026-02-13 20:06:23 +08:00
383f9c4a31 refactor(pricing): 将券码系统中的faceId替换为userId
- 移除AppVoucherController中的人脸相关依赖和验证逻辑
- 修改VoucherClaimReq和VoucherCodeQueryReq数据传输对象,将faceId字段替换为userId
- 更新VoucherCodeResp和VoucherDetailResp响应对象中的用户标识字段
- 修改数据库实体PriceVoucherCode中领取人标识字段从faceId改为userId
- 更新PriceVoucherCodeMapper中所有SQL查询的人脸ID参数为用户ID参数
- 修改VoucherCodeServiceImpl中券码领取和查询逻辑使用用户ID进行操作
- 更新VoucherServiceImpl中券码验证和使用记录的相关用户标识处理
- 统一数据库表字段和代码中的命名规范,确保用户标识一致性
2026-02-13 20:06:13 +08:00
9a92a4943a feat(face): 根据景区模式动态设置人脸识别状态显示文本
- 获取景区配置管理器以判断景区模式
- 当景区模式为2时显示"去拍摄点免费拍照吧"
- 其他模式下显示"快去智能机位打卡吧"
- 保持原有业务逻辑不变
2026-02-13 16:11:29 +08:00
959eb6077e feat(printer): 添加批量创建虚拟订单功能
- 在PrinterTvController中新增printerService和orderService依赖注入
- 添加getPrinterListByScenicId接口获取景区下启用状态的打印机列表
- 新增createVirtualOrder接口支持批量创建虚拟用户订单
- 新增queryOrder接口用于查询订单支付状态
- 创建TvCreateVirtualOrderRequest请求参数类
- 在PrinterService中实现createBatchVirtualOrder批量创建订单逻辑
- 支持通过faceSampleIds自动查找关联照片素材聚合为一笔订单
- 支持是否需要实际支付的配置选项
- 实现订单价格计算和微信支付集成
2026-02-13 14:44:57 +08:00
b2012f9209 feat(cache): 添加缓存清理功能以同步数据变更
- 在设备默认配置的创建、更新和删除操作后清理相关缓存
- 在设备信息更新和删除操作后清理设备缓存
- 在分账规则的更新、删除、启用和禁用操作后清理规则缓存
- 实现了针对特定配置键的缓存清理方法
- 实现了清除所有默认配置缓存的方法
- 实现了针对特定规则ID的缓存清理方法
2026-02-13 12:07:41 +08:00
533fb306ca feat(questionnaire): 添加问卷操作后的缓存清理功能
- 在更新问卷后清理问卷相关缓存
- 在删除问卷后清理问卷相关缓存
- 在发布问卷后清理问卷相关缓存
- 在停止问卷后清理问卷相关缓存
- 在提交问卷答案后清理统计缓存
- 新增clearQuestionnaireCache方法统一处理缓存清理逻辑
- 提交答案时根据问卷ID清理对应的统计数据缓存
2026-02-13 12:06:37 +08:00
701d7879a8 feat(cache): 添加渲染服务缓存清理功能
- 在模板更新、删除、发布和创建版本操作后清理模板相关缓存
- 在模板片段创建、更新、删除和替换操作后清理片段缓存
- 在工作器配置创建、更新、删除和批量更新操作后清理配置缓存
- 在工作器更新和删除操作后清理工作器缓存
- 新增模板和片段缓存清理辅助方法
- 新增工作器配置缓存清理辅助方法
2026-02-13 12:06:22 +08:00
1f302aefd6 feat(scenic): 添加景区配置和服务缓存清理功能
- 在创建配置后清理配置列表缓存
- 在更新配置后清理配置相关缓存
- 在删除配置后清理整个列表缓存
- 在批量更新配置后清理所有相关缓存
- 在更新景区信息后清理单个景区缓存
- 在删除景区后清理对应缓存
- 添加清除指定配置缓存的私有方法
- 添加清除景区所有配置缓存的私有方法
2026-02-13 12:06:04 +08:00
78c45343d6 refactor(printer): 优化水印配置构建逻辑
- 将 WatermarkConfig 构建过程拆分为条件判断分支
- 针对 IPC 图像源增加特殊处理逻辑
- 统一构建器模式的调用流程
- 提高代码可读性和维护性
- 保持原有功能不变的情况下优化结构
2026-02-13 11:49:14 +08:00
0ed60f5200 refactor(printer): 修改水印配置URL处理逻辑
- 将水印图片URL配置从列表类型改为字符串类型
- 使用Collections.singletonList包装单个URL为列表
- 简化了水印配置的构建过程
- 保持了原有的水印功能实现
- 优化了配置项的数据结构设计
2026-02-13 11:48:05 +08:00
daa1436e55 feat(printer): 添加打印机指引图片管理功能
- 新增 PrinterGuideEntity 实体类定义数据库表结构
- 创建 PrinterGuideMapper 数据访问接口及实现方法
- 在 AppPrinterController 中添加移动端查询已启用指引图片接口
- 在 PrinterController 中添加 PC 端指引图片管理完整 CRUD 接口
- 扩展打印机服务层集成指引图片业务逻辑
- 调整订单支付完成后购买后逻辑触发机制
- 修改用户照片列表到打印机缓存时间从 60 秒延长至 24 小时
2026-02-13 10:20:38 +08:00
0cfa871e86 fix(notification): 解决下载通知任务中用户信息为空的问题
- 添加了对用户信息和微信openId的空值检查
- 当用户不存在或未绑定微信时跳过处理并记录调试日志
- 防止因空指针异常导致的通知发送失败
2026-02-12 21:01:57 +08:00
9cfb366839 feat(watermark): 添加打印水印功能支持
- 在WatermarkConfig中添加竖版和横版打印水印URL列表配置
- 将打印水印URL列表传递给WatermarkStage处理
- 在PrinterDefaultWatermarkTemplateBuilder中实现打印水印叠加层功能
- 添加resolvePrintWatermarkUrl方法根据图片方向选择对应URL
- 在WatermarkRequest和WatermarkInfo中添加打印水印相关字段
- 从配置文件读取打印水印URL列表并构建到WatermarkConfig中
2026-02-12 20:10:34 +08:00
dee504f7ed refactor(FaceChatService): 优化 GLM 客户端依赖注入配置
- 添加 Lazy 注解以延迟 GLM 客户端初始化
- 避免循环依赖问题
- 提升服务启动性能
2026-02-12 19:20:42 +08:00
55d3d36b81 feat(device): 添加设备拍摄统计数据接口
- 新增设备拍摄统计功能,支持查询拍摄总数、拍摄人数、售出张数等统计信息
- 实现设备拍摄时间线功能,按5分钟分桶统计type=2的拍摄数量
- 添加SourceMapper的数据访问方法,包括getDeviceSourceStats和getDeviceSourceTimeline
- 集成日期时间参数处理,支持自定义统计时间段
- 实现时间轴数据补零逻辑,确保时间线图表显示连续性
- 添加相应的响应对象DeviceSourceStatsVO和DeviceSourceTimelineVO
2026-02-12 17:05:15 +08:00
39bdd02566 feat(printer): 根据图片方向智能设置裁剪参数
- 在图片裁剪前增加图片方向检测功能
- 当检测到横图时才应用270度旋转裁剪参数
- 添加ImageUtils工具类isLandscape方法判断图片方向
- 完善图片方向检测异常处理和资源清理
- 优化打印服务中图片处理流程的条件判断逻辑
2026-02-12 12:08:10 +08:00
a4496db344 feat(printer): 扩展虚拟订单功能支持实际支付模式
- 修改CreateVirtualOrderRequest添加needActualPayment字段
- 更新SourceController接口方法签名以传递实际支付参数
- 在PrinterServiceImpl中实现两种订单模式:0元立即购买和待支付订单
- 添加价格计算逻辑,支持通过价格计算服务获取真实价格
- 实现微信Native支付集成,为待支付订单生成支付二维码
- 添加Redis临时存储机制,用于支付完成后恢复needEnhance配置
- 更新createVirtualOrder方法重载,支持完整的参数组合
- 添加详细的日志记录以便跟踪订单创建和支付状态变化
2026-02-11 20:12:42 +08:00
350df0fc28 refactor(tests): 重命名支付适配器测试类为集成测试类
- 将 CongMingPayAdapterTest 重命名为 CongMingPayAdapterIT
- 更新类名以符合集成测试命名规范
2026-02-11 17:55:55 +08:00
49094be1c5 feat(source): 添加管理员关联管理功能
- 新增管理员取消关联接口,实现软删除功能
- 新增管理员恢复关联接口,支持已取消记录的重新激活
- 新增查询已取消关联记录的分页接口
- 在MemberSourceEntity实体类中添加deleted和deletedAt字段
- 更新多个Mapper XML文件中的查询条件,过滤已删除记录
- 实现在删除和恢复操作后清除相关缓存的逻辑
- 添加对已删除记录的时间格式化显示支持
2026-02-11 17:31:53 +08:00
f80b15446a config(task): 限制渲染任务轮询服务仅在生产环境启用
- 修改 Profile 注解配置,将 dev 环境从启用列表中移除
- 确保 RenderJobPollingService 仅在 prod 环境下运行
- 避免开发环境下不必要的任务轮询执行
2026-02-11 16:38:42 +08:00
122d430dbb feat(source): 添加根据sourceId查询faceId和根据faceId分页查询source的功能
- 在SourceController中新增getFaceIdsBySourceIds接口,支持根据sourceId列表查询关联的faceId
- 在SourceController中新增pageByFaceId接口,支持根据faceId分页查询关联的source记录
- 在SourceMapper中新增listFaceIdsBySourceIds和pageByFaceId数据访问方法
- 在SourceService中实现getFaceIdsBySourceIds和pageByFaceId业务逻辑
- 在SourceMapper.xml中新增对应的SQL查询语句
- 添加MemberSourceEntity实体类引用和LinkedHashMap导入
- 实现空值处理和分页功能,确保查询结果准确性
2026-02-11 16:38:30 +08:00
13b1b37c8a feat(printer): 添加打印机管理相关接口
- 新增 detail 接口用于根据 accessKey 获取打印机详情
- 新增 scenic 接口用于获取打印机关联的景区基础信息
- 新增 open 接口用于打开打印机(设置状态为1)
- 新增 close 接口用于关闭打印机(设置状态为0)
- 实现了 getByAccessKey、getScenicBasicByAccessKey、openPrinter 和 closePrinter 服务方法
- 添加了 PrinterEntity 的导入和相关异常处理逻辑
2026-02-11 13:14:00 +08:00
a94154ad47 refactor(storage): 移除视频片段上传中的本地存储适配器直接操作
- 删除了 VptController 中的本地存储适配器设置 URL 和访问权限代码
- 删除了 WvpController 中的本地存储适配器设置 URL 和访问权限代码
- 将存储操作统一到被动存储操作器中处理
- 简化了控制器中的文件对象处理逻辑
2026-02-10 16:03:04 +08:00
d609fe8ac3 ```
fix(task): 调整任务渲染作业映射参数配置

- 将预览就绪所需的最小已发布片段数从2调整为3
- 将定时轮询间隔从2秒调整为1秒
```
2026-02-10 00:03:09 +08:00
7316591ebd ```
refactor(puzzle): 移除重复图片检测中的异常抛出逻辑

- 删除了当所有图片URL相同时抛出DuplicateImageException的检查代码
- 保留了URL去重和日志记录功能
- 简化了重复图片检测流程
```
2026-02-07 20:22:29 +08:00
d286ecb4da fix(task): 解决原位替换模式下旧映射残留问题
- 在插入新映射前先删除已存在的旧映射记录
- 添加日志记录以便追踪旧映射删除操作
- 确保轮询服务能够正确处理最新的任务渲染作业映射关系
2026-02-06 21:25:51 +08:00
a79cbe4f84 feat(render): 优化模板渲染状态管理逻辑
- 引入 TaskRenderJobMappingMapper 和 TaskRenderJobMappingEntity 处理渲染作业映射关系
- 重构 FaceStatusManager 中的模板渲染状态查询逻辑,基于任务渲染作业映射确定准确状态
- 在 TaskTaskServiceImpl 中完善视频复用场景下的状态标记机制
- 新增 RenderJobPollingService 中的模板渲染状态更新功能,在预览就绪时同步更新缓存状态
- 添加渲染失败时的状态重置机制,确保状态一致性
- 实现基于任务ID查询关联信息并更新模板渲染状态的通用方法
2026-02-06 21:07:55 +08:00
092c99d25d fix(goods): 修复人脸切片状态处理逻辑
- 合并 WAITING_USER_SELECT 和 COMPLETED 状态的处理分支
- 统一查询人脸关联视频信息的逻辑
- 修正前端状态返回的一致性问题
2026-02-06 20:37:44 +08:00
34839276cf refactor(statistics): 优化应用统计漏斗查询逻辑
- 实现跨日期范围查询时分离历史数据和实时数据的处理策略
- 添加包含今天日期的跨范围查询特殊处理逻辑
- 将实时数据查询提取为独立的 queryRealtimeData 方法
- 优化数据累加逻辑,支持历史数据和今日数据合并计算
- 修复 BigDecimal 安全相加方法中的空值处理问题
- 统一数值字段的安全累加操作,防止空指针异常
- 调整 Redis 缓存策略,仅对当天数据启用短期缓存
- 改进查询条件判断逻辑,提高多日查询性能表现
2026-02-06 13:38:16 +08:00
1e71add551 Revert "refactor(storage): 简化存储适配器配置逻辑并移除降级机制"
This reverts commit 95c82cfcf2.
2026-02-05 22:47:13 +08:00
ee2482a55a refactor(render): 优化渲染作业轮询配置
- 移除集成服务中的备用降级服务依赖
- 将轮询间隔从4秒调整为2秒以提高响应速度
- 将定时轮询频率从每3秒一次提升为每1秒一次
- 优化渲染作业状态检查的实时性
2026-02-05 18:48:19 +08:00
2489f5464a fix(task): 修改景区统计数据统计时间
- 移除旧的定时任务配置 (0 0 3 * * *)
- 更新为新的定时任务配置 (0 1 0 * * *),每天凌晨执行
2026-02-05 18:45:59 +08:00
b6141d9381 Merge branch 'latest' 2026-02-05 18:42:43 +08:00
95c82cfcf2 refactor(storage): 简化存储适配器配置逻辑并移除降级机制
- 移除默认存储配置常量 DEFAULT_STORAGE
- 简化 UploadStage 中的存储适配器获取逻辑,直接使用 StorageFactory.use()
- 移除降级到默认存储的处理机制
- 在 PuzzleGenerateServiceImpl 中复用存储适配器实例
- 移除 SourceRepository 中的 StorageUnsupportedException 导入
- 移除 GoodsServiceImpl 中的 StorageType 枚举导入
- 移除 SourceServiceImpl 中的 ScenicService 依赖注入
- 移除 PrinterServiceImpl 中的复杂存储适配器配置逻辑
- 在 TaskTaskServiceImpl 中统一使用景点存储适配器
- 在 FaceCleaner 中添加新的存储清理逻辑,使用独立的图片存储适配器
- 添加 sourceImageUrlMap 和 sourceScenicIdMap 来优化文件清理逻辑
2026-02-05 14:16:16 +08:00
a85d6b0ead feat(statistics): 添加当日数据Redis缓存并调整定时任务时间
- 在AppStatisticsServiceImpl中实现当日数据的Redis缓存机制
- 仅对实时查询且查询日期为当天的数据进行缓存
- 设置缓存时间为60秒以减少实时查询压力
- 将历史数据查询与实时数据查询分离
- 调整ScenicStatsTask定时任务执行时间
- 添加每日凌晨3点执行的任务配置
- 新增每天0点1分执行的统计任务调度
2026-02-05 01:05:56 +08:00
6c330764ea refactor(statistics): 重构应用统计漏斗服务的Redis缓存逻辑
- 移除固定的Redis缓存key,改为包含日期维度的动态key
- 修复日期范围检查逻辑中的时间顺序问题
- 统一多处相同的日期范围条件判断代码
- 移除实时模式下的数据持久化操作以避免缓存污染
2026-02-05 01:02:13 +08:00
3f4b02e617 Merge branch 'render_next' 2026-02-04 16:28:54 +08:00
ee1eb8cde9 feat(video): 添加视频时长和任务参数字段支持
- 在AdminVideoReviewLogRespDTO和VideoReviewRespDTO中新增duration和taskParams字段
- 添加BigDecimal类型导入用于视频时长数据
- 更新VideoReviewMapper.xml映射文件中的结果映射配置
- 新增数据库关联查询以获取视频时长和任务参数信息
- 完善数据传输对象的注释文档说明
2026-01-27 21:48:19 +08:00
93744510ec feat(video): 完善视频评价功能,增加问题机位和标签管理
- 新增VideoReviewSourceEnum枚举,定义评价来源类型(订单、渲染)
- 添加LongListTypeHandler和StringListTypeHandler,处理数据库JSON字段与Java列表转换
- 修改VideoReviewEntity实体类,将机位评价改为问题机位ID列表和问题标签列表
- 创建AdminVideoReviewLogReqDTO和AdminVideoReviewLogRespDTO,实现管理后台评价日志查询
- 在VideoReviewController中增加管理后台分页查询评价日志接口
- 更新视频评价添加逻辑,验证来源参数并记录问题机位和标签信息
- 修改
2026-01-27 21:28:33 +08:00
1c0a506238 test(pipeline): 更新人脸识别流水线集成测试配置
- 替换 Spring Boot 测试注解为 Mockito 扩展
- 添加所有流水线阶段的 Mock 对象注入
- 更新自动匹配旧版本流水线的阶段数量断言
- 在多个阶段测试中添加 FaceStatusManager 的 Mock 验证
- 修改价格计算服务升级检查测试的业务逻辑验证
- 修复产品类型能力服务中的类别常量值
2026-01-27 21:28:09 +08:00
1dc0754b7f refactor(render): 移除作业服务的降级功能并删除任务监控组件
- 移除了 RenderJobIntegrationService 中的 fallbackService 降级处理逻辑
- 直接调用 renderJobV2Client 客户端获取作业状态、播放列表信息、作业详情和作业片段
- 删除了 TaskWatchDog 组件及其相关的任务状态扫描和异常通知功能
- 移除了任务积压、失败任务和长时间运行任务的监控逻辑
- 清理了相关的通知计数器和异常恢复机制代码
2026-01-25 00:29:06 +08:00
7b4a2f3fe8 perf(task): 优化渲染任务轮询频率并修复重试计数逻辑
- 将定时轮询间隔从5秒调整为4秒
- 修改调度注解将执行频率从每5秒一次改为每3秒一次
- 移除异常处理中的incrementRetryCount调用避免事务回滚影响
- 添加注释说明外层handleProcessError负责重试次数增加
2026-01-24 22:28:19 +08:00
9d98ea31af feat(task): 优化任务调度和视频处理流程
- 移除渲染工作配置管理器相关逻辑
- 将任务列表设置为空集合,禁用任务分配功能
- 删除景点存储适配器的ACL设置代码
- 添加视频处理相关的mapper和repository依赖
- 在渲染轮询服务中添加视频记录处理逻辑
- 实现预览视频就绪时的video记录创建和更新
- 实现MP4合成完成时的video记录更新功能
- 添加缓存清理机制确保数据一致性
- 增加详细的日志记录便于问题排查
2026-01-24 22:10:18 +08:00
ad3741fd15 feat(render): 添加视频渲染作业轮询服务
- 在RenderJobV2Client中新增createFinalizeMP4Task接口用于创建MP4合成任务
- 在RenderJobIntegrationService中实现createFinalizeMP4Task方法
- 创建TaskRenderJobMappingEntity实体类用于跟踪任务与渲染作业关联
- 创建TaskRenderJobMappingMapper接口及对应XML映射文件
- 在TaskTaskServiceImpl中添加mapping表写入逻辑
- 新增RenderJobPollingService定时轮询服务处理渲染状态流转
- 实现从PENDING到PREVIEW_READY再到MP4_COMPOSING最后到COMPLETED的状态转换
- 添加MP4合成任务创建及状态更新功能
2026-01-24 21:20:09 +08:00
110 changed files with 3940 additions and 771 deletions

View File

@@ -6,7 +6,9 @@ import com.ycwl.basic.enums.FaceCutStatus;
import com.ycwl.basic.enums.FacePieceUpdateStatus; import com.ycwl.basic.enums.FacePieceUpdateStatus;
import com.ycwl.basic.enums.TemplateRenderStatus; import com.ycwl.basic.enums.TemplateRenderStatus;
import com.ycwl.basic.mapper.TaskMapper; import com.ycwl.basic.mapper.TaskMapper;
import com.ycwl.basic.mapper.task.TaskRenderJobMappingMapper;
import com.ycwl.basic.model.pc.task.entity.TaskEntity; import com.ycwl.basic.model.pc.task.entity.TaskEntity;
import com.ycwl.basic.model.task.entity.TaskRenderJobMappingEntity;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -51,6 +53,8 @@ public class FaceStatusManager {
@Autowired @Autowired
private TaskMapper taskMapper; private TaskMapper taskMapper;
@Autowired
private TaskRenderJobMappingMapper taskRenderJobMappingMapper;
public FaceStatusManager() { public FaceStatusManager() {
// 初始化三个独立的缓存实例 // 初始化三个独立的缓存实例
@@ -257,20 +261,27 @@ public class FaceStatusManager {
Integer code = templateRenderCache.getIfPresent(faceId + ":" + templateId); Integer code = templateRenderCache.getIfPresent(faceId + ":" + templateId);
if (code == null) { if (code == null) {
log.debug("模板渲染状态缓存不存在: faceId={}, templateId={}", faceId, templateId); log.debug("模板渲染状态缓存不存在: faceId={}, templateId={}", faceId, templateId);
// 查询数据库 // 查询数据库:通过 task_render_job_mapping 确定渲染状态
TaskEntity task = taskMapper.listLastFaceTemplateTask(faceId, templateId); TaskEntity task = taskMapper.listLastFaceTemplateTask(faceId, templateId);
if (task == null) { if (task == null) {
setTemplateRenderStatus(faceId, templateId, TemplateRenderStatus.NONE); setTemplateRenderStatus(faceId, templateId, TemplateRenderStatus.NONE);
return TemplateRenderStatus.NONE; return TemplateRenderStatus.NONE;
} }
if (Integer.valueOf(2).equals(task.getStatus())) { TaskRenderJobMappingEntity mapping = taskRenderJobMappingMapper.selectByTaskId(task.getId());
setTemplateRenderStatus(faceId, templateId, TemplateRenderStatus.RENDERING); if (mapping == null) {
} setTemplateRenderStatus(faceId, templateId, TemplateRenderStatus.NONE);
if (Integer.valueOf(1).equals(task.getStatus())) {
setTemplateRenderStatus(faceId, templateId, TemplateRenderStatus.RENDERED);
}
return TemplateRenderStatus.NONE; return TemplateRenderStatus.NONE;
} }
TemplateRenderStatus status = switch (mapping.getRenderStatus()) {
case TaskRenderJobMappingEntity.STATUS_PENDING -> TemplateRenderStatus.RENDERING;
case TaskRenderJobMappingEntity.STATUS_PREVIEW_READY,
TaskRenderJobMappingEntity.STATUS_MP4_COMPOSING,
TaskRenderJobMappingEntity.STATUS_COMPLETED -> TemplateRenderStatus.RENDERED;
default -> TemplateRenderStatus.NONE; // FAILED 等异常状态
};
setTemplateRenderStatus(faceId, templateId, status);
return status;
}
return TemplateRenderStatus.fromCode(code); return TemplateRenderStatus.fromCode(code);
} }

View File

@@ -249,6 +249,9 @@ public class OrderBiz {
case 13: // AI微单 case 13: // AI微单
sourceRepository.setUserIsBuyItem(order.getMemberId(), item.getGoodsType(), item.getGoodsId(), order.getId()); sourceRepository.setUserIsBuyItem(order.getMemberId(), item.getGoodsType(), item.getGoodsId(), order.getId());
break; break;
case 14: // 单张照片
sourceRepository.setUserIsBuyItemBySourceId(order.getMemberId(), item.getGoodsId(), order.getFaceId(), order.getId());
break;
case 3: case 3:
printerService.setUserIsBuyItem(order.getMemberId(), item.getGoodsId(), order.getId()); printerService.setUserIsBuyItem(order.getMemberId(), item.getGoodsId(), order.getId());
break; break;
@@ -287,6 +290,9 @@ public class OrderBiz {
case 2: // 照片原素材 case 2: // 照片原素材
sourceRepository.setUserNotBuyItem(order.getMemberId(), item.getGoodsType(), item.getGoodsId()); sourceRepository.setUserNotBuyItem(order.getMemberId(), item.getGoodsType(), item.getGoodsId());
break; break;
case 14: // 单张照片
sourceRepository.setUserNotBuyItemBySourceId(order.getMemberId(), item.getGoodsId(), order.getFaceId());
break;
} }
}); });
orderRepository.clearOrderCache(orderId); // 更新完了,清理下 orderRepository.clearOrderCache(orderId); // 更新完了,清理下
@@ -311,6 +317,9 @@ public class OrderBiz {
case 2: // 照片原素材 case 2: // 照片原素材
sourceRepository.setUserNotBuyItem(order.getMemberId(), item.getGoodsType(), item.getGoodsId()); sourceRepository.setUserNotBuyItem(order.getMemberId(), item.getGoodsType(), item.getGoodsId());
break; break;
case 14: // 单张照片
sourceRepository.setUserNotBuyItemBySourceId(order.getMemberId(), item.getGoodsId(), order.getFaceId());
break;
} }
}); });
orderRepository.clearOrderCache(orderId); // 更新完了,清理下 orderRepository.clearOrderCache(orderId); // 更新完了,清理下

View File

@@ -1,6 +1,8 @@
package com.ycwl.basic.controller; package com.ycwl.basic.controller;
import com.github.pagehelper.PageInfo; import com.github.pagehelper.PageInfo;
import com.ycwl.basic.model.pc.videoreview.dto.AdminVideoReviewLogReqDTO;
import com.ycwl.basic.model.pc.videoreview.dto.AdminVideoReviewLogRespDTO;
import com.ycwl.basic.model.pc.videoreview.dto.VideoPurchaseCheckReqDTO; import com.ycwl.basic.model.pc.videoreview.dto.VideoPurchaseCheckReqDTO;
import com.ycwl.basic.model.pc.videoreview.dto.VideoPurchaseCheckRespDTO; import com.ycwl.basic.model.pc.videoreview.dto.VideoPurchaseCheckRespDTO;
import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewAddReqDTO; import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewAddReqDTO;
@@ -55,6 +57,20 @@ public class VideoReviewController {
return ApiResponse.success(pageInfo); return ApiResponse.success(pageInfo);
} }
/**
* 管理后台分页查询评价日志
* 提供更详细的管理信息,包括评价人账号、机位评价统计等
*
* @param reqDTO 查询条件
* @return 分页结果
*/
@GetMapping("/admin/logs")
public ApiResponse<PageInfo<AdminVideoReviewLogRespDTO>> getAdminReviewLogList(AdminVideoReviewLogReqDTO reqDTO) {
log.info("管理后台查询评价日志,pageNum: {}, pageSize: {}", reqDTO.getPageNum(), reqDTO.getPageSize());
PageInfo<AdminVideoReviewLogRespDTO> pageInfo = videoReviewService.getAdminReviewLogList(reqDTO);
return ApiResponse.success(pageInfo);
}
/** /**
* 获取评价统计数据 * 获取评价统计数据
* *

View File

@@ -4,6 +4,7 @@ import com.github.pagehelper.PageInfo;
import com.ycwl.basic.constant.BaseContextHandler; import com.ycwl.basic.constant.BaseContextHandler;
import com.ycwl.basic.mapper.SourceMapper; import com.ycwl.basic.mapper.SourceMapper;
import com.ycwl.basic.mapper.VideoMapper; import com.ycwl.basic.mapper.VideoMapper;
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
import com.ycwl.basic.model.pc.source.req.SourceReqQuery; import com.ycwl.basic.model.pc.source.req.SourceReqQuery;
import com.ycwl.basic.model.pc.task.entity.TaskEntity; import com.ycwl.basic.model.pc.task.entity.TaskEntity;
import com.ycwl.basic.model.pc.video.entity.MemberVideoEntity; import com.ycwl.basic.model.pc.video.entity.MemberVideoEntity;
@@ -95,6 +96,12 @@ public class AppOrderV2Controller {
request.setFaceId(video.getFaceId()); request.setFaceId(video.getFaceId());
} }
case RECORDING_SET, PHOTO_SET, AI_CAM_PHOTO_SET -> request.setFaceId(Long.valueOf(productItem.getProductId())); case RECORDING_SET, PHOTO_SET, AI_CAM_PHOTO_SET -> request.setFaceId(Long.valueOf(productItem.getProductId()));
case PHOTO -> {
MemberSourceEntity ms = sourceMapper.getMemberSourceByMemberAndSourceId(currentUserId, Long.valueOf(productItem.getProductId()));
if (ms != null) {
request.setFaceId(ms.getFaceId());
}
}
} }
} }
@@ -141,6 +148,9 @@ public class AppOrderV2Controller {
Integer _count = sourceMapper.countUser(aiPhotoSetReqQuery); Integer _count = sourceMapper.countUser(aiPhotoSetReqQuery);
product.setQuantity(_count); product.setQuantity(_count);
break; break;
case PHOTO:
product.setQuantity(1);
break;
default: default:
log.warn("未知的商品类型,跳过重复购买检查: productType={}", product.getProductType()); log.warn("未知的商品类型,跳过重复购买检查: productType={}", product.getProductType());
break; break;

View File

@@ -1,8 +1,10 @@
package com.ycwl.basic.controller.mobile; package com.ycwl.basic.controller.mobile;
import com.ycwl.basic.annotation.IgnoreToken; import com.ycwl.basic.annotation.IgnoreToken;
import com.ycwl.basic.mapper.PrinterGuideMapper;
import com.ycwl.basic.model.jwt.JwtInfo; import com.ycwl.basic.model.jwt.JwtInfo;
import com.ycwl.basic.model.mobile.face.FaceRecognizeResp; import com.ycwl.basic.model.mobile.face.FaceRecognizeResp;
import com.ycwl.basic.model.pc.printer.entity.PrinterGuideEntity;
import com.ycwl.basic.model.pc.printer.resp.MemberPrintResp; import com.ycwl.basic.model.pc.printer.resp.MemberPrintResp;
import com.ycwl.basic.model.pc.printer.resp.PrinterResp; import com.ycwl.basic.model.pc.printer.resp.PrinterResp;
import com.ycwl.basic.model.printer.req.FromSourceReq; import com.ycwl.basic.model.printer.req.FromSourceReq;
@@ -31,12 +33,21 @@ import java.util.UUID;
public class AppPrinterController { public class AppPrinterController {
@Autowired @Autowired
private PrinterService printerService; private PrinterService printerService;
@Autowired
private PrinterGuideMapper printerGuideMapper;
@GetMapping("/listFor/{scenicId}") @GetMapping("/listFor/{scenicId}")
@IgnoreToken @IgnoreToken
public ApiResponse<List<PrinterResp>> listFor(@PathVariable("scenicId") Long scenicId) { public ApiResponse<List<PrinterResp>> listFor(@PathVariable("scenicId") Long scenicId) {
return ApiResponse.success(printerService.listByScenicId(scenicId)); return ApiResponse.success(printerService.listByScenicId(scenicId));
} }
// 查询打印机已启用的指引图片(按排序)
@GetMapping("/guide/{printerId}")
@IgnoreToken
public ApiResponse<List<PrinterGuideEntity>> guideList(@PathVariable("printerId") Integer printerId) {
return ApiResponse.success(printerGuideMapper.listEnabledByPrinterId(printerId));
}
@GetMapping("/useSample/{sampleId}") @GetMapping("/useSample/{sampleId}")
public ApiResponse<FaceRecognizeResp> useSample(@PathVariable("sampleId") Long sampleId) throws IOException { public ApiResponse<FaceRecognizeResp> useSample(@PathVariable("sampleId") Long sampleId) throws IOException {
JwtInfo worker = JwtTokenUtil.getWorker(); JwtInfo worker = JwtTokenUtil.getWorker();

View File

@@ -1,15 +1,12 @@
package com.ycwl.basic.controller.mobile; package com.ycwl.basic.controller.mobile;
import com.ycwl.basic.constant.BaseContextHandler; import com.ycwl.basic.constant.BaseContextHandler;
import com.ycwl.basic.exception.BaseException;
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
import com.ycwl.basic.pricing.dto.req.VoucherClaimReq; import com.ycwl.basic.pricing.dto.req.VoucherClaimReq;
import com.ycwl.basic.pricing.dto.req.VoucherPrintReq; import com.ycwl.basic.pricing.dto.req.VoucherPrintReq;
import com.ycwl.basic.pricing.dto.resp.VoucherCodeResp; import com.ycwl.basic.pricing.dto.resp.VoucherCodeResp;
import com.ycwl.basic.pricing.dto.resp.VoucherPrintResp; import com.ycwl.basic.pricing.dto.resp.VoucherPrintResp;
import com.ycwl.basic.pricing.service.VoucherCodeService; import com.ycwl.basic.pricing.service.VoucherCodeService;
import com.ycwl.basic.pricing.service.VoucherPrintService; import com.ycwl.basic.pricing.service.VoucherPrintService;
import com.ycwl.basic.repository.FaceRepository;
import com.ycwl.basic.utils.ApiResponse; import com.ycwl.basic.utils.ApiResponse;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -30,8 +27,6 @@ public class AppVoucherController {
private VoucherPrintService voucherPrintService; private VoucherPrintService voucherPrintService;
@Autowired @Autowired
private VoucherCodeService voucherCodeService; private VoucherCodeService voucherCodeService;
@Autowired
private FaceRepository faceRepository;
/** /**
* 打印小票 * 打印小票
@@ -60,11 +55,6 @@ public class AppVoucherController {
@PostMapping("/claim") @PostMapping("/claim")
public ApiResponse<VoucherCodeResp> claimVoucher(@RequestBody VoucherClaimReq req) { public ApiResponse<VoucherCodeResp> claimVoucher(@RequestBody VoucherClaimReq req) {
FaceEntity face = faceRepository.getFace(req.getFaceId());
if (face == null) {
throw new BaseException("请选择人脸");
}
req.setScenicId(face.getScenicId());
VoucherCodeResp result = voucherCodeService.claimVoucher(req); VoucherCodeResp result = voucherCodeService.claimVoucher(req);
return ApiResponse.success(result); return ApiResponse.success(result);
} }

View File

@@ -7,13 +7,19 @@ 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.integration.device.service.DeviceStatusIntegrationService;
import com.ycwl.basic.mapper.SourceMapper;
import com.ycwl.basic.model.pc.device.resp.DeviceSourceStatsVO;
import com.ycwl.basic.model.pc.device.resp.DeviceSourceTimelineVO;
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;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.HashMap; import java.util.HashMap;
import java.util.ArrayList;
import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -32,6 +38,7 @@ public class DeviceV2Controller {
private final DeviceIntegrationService deviceIntegrationService; private final DeviceIntegrationService deviceIntegrationService;
private final DeviceConfigIntegrationService deviceConfigIntegrationService; private final DeviceConfigIntegrationService deviceConfigIntegrationService;
private final DeviceStatusIntegrationService deviceStatusIntegrationService; private final DeviceStatusIntegrationService deviceStatusIntegrationService;
private final SourceMapper sourceMapper;
// ========== 设备基础 CRUD 操作 ========== // ========== 设备基础 CRUD 操作 ==========
@@ -387,4 +394,96 @@ public class DeviceV2Controller {
return ApiResponse.fail("获取景区所有设备列表失败: " + e.getMessage()); return ApiResponse.fail("获取景区所有设备列表失败: " + e.getMessage());
} }
} }
// ========== 设备拍摄统计 ==========
/**
* 设备拍摄统计:拍摄总数、拍摄人数、售出张数、赠送张数、售出人数
*/
@GetMapping("/{id}/source-stats")
public ApiResponse<DeviceSourceStatsVO> getDeviceSourceStats(
@PathVariable Long id,
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date startDate,
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date endDate) {
try {
java.util.Calendar cal = java.util.Calendar.getInstance();
// startDate:归到当天 00:00:00(未传则默认今天)
if (startDate != null) {
cal.setTime(startDate);
}
cal.set(java.util.Calendar.HOUR_OF_DAY, 0);
cal.set(java.util.Calendar.MINUTE, 0);
cal.set(java.util.Calendar.SECOND, 0);
cal.set(java.util.Calendar.MILLISECOND, 0);
startDate = cal.getTime();
// endDate:归到当天 23:59:59(未传则取 startDate 同一天)
if (endDate != null) {
cal.setTime(endDate);
}
cal.set(java.util.Calendar.HOUR_OF_DAY, 23);
cal.set(java.util.Calendar.MINUTE, 59);
cal.set(java.util.Calendar.SECOND, 59);
cal.set(java.util.Calendar.MILLISECOND, 999);
endDate = cal.getTime();
DeviceSourceStatsVO stats = sourceMapper.getDeviceSourceStats(id, startDate, endDate);
return ApiResponse.success(stats);
} catch (Exception e) {
log.error("获取设备拍摄统计失败, deviceId: {}", id, e);
return ApiResponse.fail("获取设备拍摄统计失败: " + e.getMessage());
}
}
/**
* 设备拍摄时间线:按 5 分钟分桶统计 type=2 的拍摄数量,空桶补 0
*/
@GetMapping("/{id}/source-timeline")
public ApiResponse<List<DeviceSourceTimelineVO>> getDeviceSourceTimeline(
@PathVariable Long id,
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date startDate,
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date endDate) {
try {
java.util.Calendar cal = java.util.Calendar.getInstance();
// startDate:归到当天 08:00:00(未传则默认今天)
if (startDate != null) {
cal.setTime(startDate);
}
cal.set(java.util.Calendar.HOUR_OF_DAY, 8);
cal.set(java.util.Calendar.MINUTE, 0);
cal.set(java.util.Calendar.SECOND, 0);
cal.set(java.util.Calendar.MILLISECOND, 0);
startDate = cal.getTime();
// endDate:归到当天 19:59:59(未传则取 startDate 同一天)
if (endDate != null) {
cal.setTime(endDate);
}
cal.set(java.util.Calendar.HOUR_OF_DAY, 19);
cal.set(java.util.Calendar.MINUTE, 59);
cal.set(java.util.Calendar.SECOND, 59);
cal.set(java.util.Calendar.MILLISECOND, 999);
endDate = cal.getTime();
// 查询有数据的桶
List<DeviceSourceTimelineVO> rawData = sourceMapper.getDeviceSourceTimeline(id, startDate, endDate);
// 将有数据的桶放入 Map,key 为对齐到 5 分钟的毫秒时间戳
Map<Long, Integer> dataMap = new HashMap<>();
for (DeviceSourceTimelineVO item : rawData) {
long aligned = (item.getTime().getTime() / 300_000) * 300_000;
dataMap.put(aligned, item.getCount());
}
// 生成完整时间轴并补零
long startMs = (startDate.getTime() / 300_000) * 300_000;
long endMs = endDate.getTime();
List<DeviceSourceTimelineVO> timeline = new ArrayList<>();
for (long ts = startMs; ts <= endMs; ts += 300_000) {
timeline.add(new DeviceSourceTimelineVO(new Date(ts), dataMap.getOrDefault(ts, 0)));
}
return ApiResponse.success(timeline);
} catch (Exception e) {
log.error("获取设备拍摄时间线失败, deviceId: {}", id, e);
return ApiResponse.fail("获取设备拍摄时间线失败: " + e.getMessage());
}
}
} }

View File

@@ -3,8 +3,10 @@ package com.ycwl.basic.controller.pc;
import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo; import com.github.pagehelper.PageInfo;
import com.ycwl.basic.mapper.PrintTaskMapper; import com.ycwl.basic.mapper.PrintTaskMapper;
import com.ycwl.basic.mapper.PrinterGuideMapper;
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.entity.PrinterGuideEntity;
import com.ycwl.basic.model.pc.printer.req.PrintTaskReqQuery; import com.ycwl.basic.model.pc.printer.req.PrintTaskReqQuery;
import com.ycwl.basic.model.pc.printer.req.ReprintRequest; import com.ycwl.basic.model.pc.printer.req.ReprintRequest;
import com.ycwl.basic.service.printer.PrinterService; import com.ycwl.basic.service.printer.PrinterService;
@@ -16,6 +18,7 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.List; import java.util.List;
@@ -29,6 +32,9 @@ public class PrinterController {
@Autowired @Autowired
private PrintTaskMapper printTaskMapper; private PrintTaskMapper printTaskMapper;
@Autowired
private PrinterGuideMapper printerGuideMapper;
// 查询列表 // 查询列表
@PostMapping("/list") @PostMapping("/list")
public ApiResponse<List<PrinterEntity>> list(@RequestBody PrinterEntity condition) { public ApiResponse<List<PrinterEntity>> list(@RequestBody PrinterEntity condition) {
@@ -119,4 +125,41 @@ public class PrinterController {
int count = printerService.rejectPrintTasks(taskIds); int count = printerService.rejectPrintTasks(taskIds);
return ApiResponse.success(count); return ApiResponse.success(count);
} }
// 查询打印机所有指引图片(含禁用)
@GetMapping("/guide/list/{printerId}")
public ApiResponse<List<PrinterGuideEntity>> guideList(@PathVariable("printerId") Integer printerId) {
return ApiResponse.success(printerGuideMapper.listByPrinterId(printerId));
}
// 添加指引图片
@PostMapping("/guide/add")
public ApiResponse<Integer> guideAdd(@RequestBody PrinterGuideEntity entity) {
if (entity.getSortOrder() == null) {
entity.setSortOrder(0);
}
if (entity.getEnabled() == null) {
entity.setEnabled(1);
}
printerGuideMapper.insertGuide(entity);
return ApiResponse.success(entity.getId());
}
// 删除指引图片
@DeleteMapping("/guide/delete/{id}")
public ApiResponse<Integer> guideDelete(@PathVariable("id") Integer id) {
return ApiResponse.success(printerGuideMapper.deleteById(id));
}
// 修改指引图片排序
@PostMapping("/guide/updateSort/{id}")
public ApiResponse<Integer> guideUpdateSort(@PathVariable("id") Integer id, @RequestParam("sortOrder") Integer sortOrder) {
return ApiResponse.success(printerGuideMapper.updateSortOrder(id, sortOrder));
}
// 切换指引图片启用/禁用
@PostMapping("/guide/toggleEnabled/{id}")
public ApiResponse<Integer> guideToggleEnabled(@PathVariable("id") Integer id) {
return ApiResponse.success(printerGuideMapper.toggleEnabled(id));
}
} }

View File

@@ -12,6 +12,7 @@ import com.ycwl.basic.utils.JwtTokenUtil;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map; import java.util.Map;
/** /**
@@ -52,8 +53,9 @@ public class SourceController {
} }
/** /**
* 创建虚拟用户0元订单 * 创建虚拟用户订单
* 用于后台直接从source创建订单,不需要真实用户 * 用于后台直接从source创建订单,不需要真实用户
* 支持立即0元购买或创建待支付订单(由needActualPayment控制)
* *
* @param request 请求参数 * @param request 请求参数
* @return 订单信息 * @return 订单信息
@@ -66,7 +68,8 @@ public class SourceController {
request.getScenicId(), request.getScenicId(),
request.getPrinterId(), request.getPrinterId(),
request.getNeedEnhance(), request.getNeedEnhance(),
request.getPrintImgUrl() request.getPrintImgUrl(),
request.getNeedActualPayment()
); );
return ApiResponse.success(result); return ApiResponse.success(result);
} catch (Exception e) { } catch (Exception e) {
@@ -74,5 +77,54 @@ public class SourceController {
} }
} }
/**
* 根据sourceId列表查询关联的faceId
* @param sourceIds sourceId列表
* @return sourceId -> faceId 的映射,无关联则value为null
*/
@PostMapping("/faceIds")
public ApiResponse<Map<Long, Long>> getFaceIdsBySourceIds(@RequestBody List<Long> sourceIds) {
return sourceService.getFaceIdsBySourceIds(sourceIds);
}
/**
* 根据faceId分页查询关联的source记录
* @param sourceReqQuery 查询参数(需设置faceId,可选type/scenicId/isBuy)
* @return 分页source列表
*/
@PostMapping("/pageByFaceId")
public ApiResponse pageByFaceId(@RequestBody SourceReqQuery sourceReqQuery) {
return sourceService.pageByFaceId(sourceReqQuery);
}
/**
* 管理员取消关联(软删除)
* @param id member_source 记录 ID
* @return 操作结果
*/
@PostMapping("/admin/cancel/{id}")
public ApiResponse cancelRelation(@PathVariable("id") Long id) {
return sourceService.cancelRelation(id);
}
/**
* 管理员恢复已取消的关联
* @param id member_source 记录 ID
* @return 操作结果
*/
@PostMapping("/admin/reactivate/{id}")
public ApiResponse reactivateRelation(@PathVariable("id") Long id) {
return sourceService.reactivateRelation(id);
}
/**
* 管理员查询已取消的关联记录
* @param sourceReqQuery 查询参数(需设置faceId,可选type/scenicId)
* @return 分页已取消关联列表
*/
@PostMapping("/admin/pageDeletedByFaceId")
public ApiResponse pageDeletedByFaceId(@RequestBody SourceReqQuery sourceReqQuery) {
return sourceService.pageDeletedByFaceId(sourceReqQuery);
}
} }

View File

@@ -1,6 +1,7 @@
package com.ycwl.basic.controller.printer; package com.ycwl.basic.controller.printer;
import com.ycwl.basic.annotation.IgnoreToken; import com.ycwl.basic.annotation.IgnoreToken;
import com.ycwl.basic.model.pc.printer.entity.PrinterEntity;
import com.ycwl.basic.model.printer.req.PrinterSyncReq; import com.ycwl.basic.model.printer.req.PrinterSyncReq;
import com.ycwl.basic.model.printer.req.WorkerAuthReqVo; import com.ycwl.basic.model.printer.req.WorkerAuthReqVo;
import com.ycwl.basic.model.printer.resp.PrintTaskResp; import com.ycwl.basic.model.printer.resp.PrintTaskResp;
@@ -40,4 +41,26 @@ public class PrinterTaskController {
printerService.taskFail(taskId, req); printerService.taskFail(taskId, req);
return ApiResponse.success("OK"); return ApiResponse.success("OK");
} }
@PostMapping("/detail")
public ApiResponse<PrinterEntity> detail(@RequestBody WorkerAuthReqVo req) {
return ApiResponse.success(printerService.getByAccessKey(req.getAccessKey()));
}
@PostMapping("/scenic")
public ApiResponse scenic(@RequestBody WorkerAuthReqVo req) {
return ApiResponse.success(printerService.getScenicBasicByAccessKey(req.getAccessKey()));
}
@PostMapping("/open")
public ApiResponse open(@RequestBody WorkerAuthReqVo req) {
printerService.openPrinter(req.getAccessKey());
return ApiResponse.success("OK");
}
@PostMapping("/close")
public ApiResponse close(@RequestBody WorkerAuthReqVo req) {
printerService.closePrinter(req.getAccessKey());
return ApiResponse.success("OK");
}
} }

View File

@@ -9,12 +9,17 @@ import com.ycwl.basic.model.mobile.face.FaceRecognizeResp;
import com.ycwl.basic.model.pc.face.entity.FaceEntity; import com.ycwl.basic.model.pc.face.entity.FaceEntity;
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity; import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
import com.ycwl.basic.model.pc.mp.MpConfigEntity; import com.ycwl.basic.model.pc.mp.MpConfigEntity;
import com.ycwl.basic.model.pc.printer.resp.PrinterResp;
import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery; import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery;
import com.ycwl.basic.model.pc.source.entity.SourceEntity; import com.ycwl.basic.model.pc.source.entity.SourceEntity;
import com.ycwl.basic.model.printer.req.TvCreateVirtualOrderRequest;
import com.ycwl.basic.pay.entity.PayResponse;
import com.ycwl.basic.repository.DeviceRepository; import com.ycwl.basic.repository.DeviceRepository;
import com.ycwl.basic.repository.FaceRepository; import com.ycwl.basic.repository.FaceRepository;
import com.ycwl.basic.repository.ScenicRepository; import com.ycwl.basic.repository.ScenicRepository;
import com.ycwl.basic.service.pc.FaceService; import com.ycwl.basic.service.pc.FaceService;
import com.ycwl.basic.service.pc.OrderService;
import com.ycwl.basic.service.printer.PrinterService;
import com.ycwl.basic.utils.ApiResponse; import com.ycwl.basic.utils.ApiResponse;
import com.ycwl.basic.utils.WxMpUtil; import com.ycwl.basic.utils.WxMpUtil;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
@@ -22,6 +27,7 @@ import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@@ -31,6 +37,7 @@ import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.List; import java.util.List;
import java.util.Map;
@IgnoreToken @IgnoreToken
// 打印机大屏对接接口 // 打印机大屏对接接口
@@ -44,6 +51,8 @@ public class PrinterTvController {
private final FaceRepository faceRepository; private final FaceRepository faceRepository;
private final FaceService pcFaceService; private final FaceService pcFaceService;
private final SourceMapper sourceMapper; private final SourceMapper sourceMapper;
private final PrinterService printerService;
private final OrderService orderService;
/** /**
* 获取景区列表 * 获取景区列表
@@ -191,4 +200,58 @@ public class PrinterTvController {
response.sendRedirect(face.getFaceUrl()); response.sendRedirect(face.getFaceUrl());
} }
/**
* 获取景区下的打印机列表
*
* @param scenicId 景区ID
* @return 启用状态的打印机列表
*/
@GetMapping("/printer/list")
public ApiResponse<List<PrinterResp>> getPrinterListByScenicId(@RequestParam Long scenicId) {
return ApiResponse.success(printerService.listByScenicId(scenicId));
}
/**
* 批量创建虚拟用户订单
* 传入faceSampleIds,自动查找关联的照片素材(type=2),聚合为一笔订单、一次支付
*
* @param request 请求参数(含faceSampleIds列表)
* @return 聚合订单结果
*/
@PostMapping("/createVirtualOrder")
public ApiResponse<Map<String, Object>> createVirtualOrder(@RequestBody TvCreateVirtualOrderRequest request) {
if (request.getFaceSampleIds() == null || request.getFaceSampleIds().isEmpty()) {
return ApiResponse.fail("faceSampleIds不能为空");
}
try {
List<SourceEntity> sources = sourceMapper.listByFaceSampleIdsAndType(request.getFaceSampleIds(), 2);
if (sources.isEmpty()) {
return ApiResponse.fail("未找到关联的照片素材");
}
List<Long> sourceIds = sources.stream().map(SourceEntity::getId).toList();
Map<String, Object> result = printerService.createBatchVirtualOrder(
sourceIds,
request.getScenicId(),
request.getPrinterId(),
request.getNeedEnhance(),
request.getPrintImgUrl(),
request.getNeedActualPayment()
);
return ApiResponse.success(result);
} catch (Exception e) {
return ApiResponse.fail(e.getMessage());
}
}
/**
* 查询订单支付状态
*
* @param orderId 订单ID
* @return 支付状态信息
*/
@GetMapping("/order/query")
public ApiResponse<PayResponse> queryOrder(@RequestParam("orderId") Long orderId) {
return ApiResponse.success(orderService.queryOrder(orderId));
}
} }

View File

@@ -47,10 +47,6 @@ public class VptController {
} }
@PostMapping("/scenic/{scenicId}/{taskId}/success") @PostMapping("/scenic/{scenicId}/{taskId}/success")
public ApiResponse<String> success(@PathVariable("scenicId") Long scenicId, @PathVariable("taskId") Long taskId, @RequestBody FileObject fileObject) { public ApiResponse<String> success(@PathVariable("scenicId") Long scenicId, @PathVariable("taskId") Long taskId, @RequestBody FileObject fileObject) {
IStorageAdapter adapter = scenicService.getScenicLocalStorageAdapter(scenicId);
String filename = StorageUtil.joinPath(StorageConstant.VIDEO_PIECE_PATH, taskId.toString() + ".mp4");
fileObject.setUrl(adapter.getUrl(filename));
adapter.setAcl(StorageAcl.PUBLIC_READ, filename);
VptPassiveStorageOperator.onReceiveResult(taskId, fileObject); VptPassiveStorageOperator.onReceiveResult(taskId, fileObject);
return ApiResponse.success("success"); return ApiResponse.success("success");
} }

View File

@@ -48,10 +48,6 @@ public class WvpController {
} }
@PostMapping("/scenic/{scenicId}/{taskId}/success") @PostMapping("/scenic/{scenicId}/{taskId}/success")
public ApiResponse<String> success(@PathVariable("scenicId") Long scenicId, @PathVariable("taskId") Long taskId, @RequestBody FileObject fileObject) { public ApiResponse<String> success(@PathVariable("scenicId") Long scenicId, @PathVariable("taskId") Long taskId, @RequestBody FileObject fileObject) {
IStorageAdapter adapter = scenicService.getScenicLocalStorageAdapter(scenicId);
String filename = StorageUtil.joinPath(StorageConstant.VIDEO_PIECE_PATH, taskId.toString() + ".mp4");
fileObject.setUrl(adapter.getUrl(filename));
adapter.setAcl(StorageAcl.PUBLIC_READ, filename);
WvpPassiveStorageOperator.onReceiveResult(taskId, fileObject); WvpPassiveStorageOperator.onReceiveResult(taskId, fileObject);
return ApiResponse.success("success"); return ApiResponse.success("success");
} }

View File

@@ -0,0 +1,63 @@
package com.ycwl.basic.enums;
import lombok.Getter;
/**
* 视频评价来源枚举
*/
@Getter
public enum VideoReviewSourceEnum {
/**
* 订单
*/
ORDER("ORDER", "订单"),
/**
* 渲染
*/
RENDER("RENDER", "渲染");
/**
* 枚举代码
*/
private final String code;
/**
* 枚举描述
*/
private final String description;
VideoReviewSourceEnum(String code, String description) {
this.code = code;
this.description = description;
}
/**
* 根据code获取枚举
*
* @param code 枚举代码
* @return 枚举对象,不存在则返回null
*/
public static VideoReviewSourceEnum fromCode(String code) {
if (code == null || code.isEmpty()) {
return null;
}
for (VideoReviewSourceEnum value : VideoReviewSourceEnum.values()) {
if (value.getCode().equals(code)) {
return value;
}
}
return null;
}
/**
* 验证code是否有效
*
* @param code 枚举代码
* @return true-有效, false-无效
*/
public static boolean isValid(String code) {
return fromCode(code) != null;
}
}

View File

@@ -0,0 +1,80 @@
package com.ycwl.basic.handler;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
/**
* Long类型列表的TypeHandler
* 用于处理数据库JSON字段与Java List<Long>之间的转换
*/
@Slf4j
@MappedTypes(List.class)
@MappedJdbcTypes(JdbcType.VARCHAR)
public class LongListTypeHandler extends BaseTypeHandler<List<Long>> {
private final ObjectMapper objectMapper = new ObjectMapper();
private final TypeReference<List<Long>> typeReference = new TypeReference<List<Long>>() {};
@Override
public void setNonNullParameter(PreparedStatement ps, int i, List<Long> parameter, JdbcType jdbcType) throws SQLException {
try {
String json = objectMapper.writeValueAsString(parameter);
ps.setString(i, json);
log.debug("序列化Long列表: {}", json);
} catch (JsonProcessingException e) {
log.error("序列化Long列表失败", e);
throw new SQLException("序列化Long列表失败", e);
}
}
@Override
public List<Long> getNullableResult(ResultSet rs, String columnName) throws SQLException {
String json = rs.getString(columnName);
return parseJson(json, columnName);
}
@Override
public List<Long> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String json = rs.getString(columnIndex);
return parseJson(json, "columnIndex:" + columnIndex);
}
@Override
public List<Long> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String json = cs.getString(columnIndex);
return parseJson(json, "columnIndex:" + columnIndex);
}
private List<Long> parseJson(String json, String source) {
if (json == null || json.trim().isEmpty() || "null".equals(json)) {
log.debug("从{}获取的JSON为空,返回空列表", source);
return new ArrayList<>();
}
try {
List<Long> result = objectMapper.readValue(json, typeReference);
if (result == null) {
log.debug("从{}反序列化得到null,返回空列表", source);
return new ArrayList<>();
}
log.debug("从{}反序列化Long列表成功,数量: {}", source, result.size());
return result;
} catch (JsonProcessingException e) {
log.error("从{}反序列化Long列表失败,JSON: {}", source, json, e);
return new ArrayList<>();
}
}
}

View File

@@ -0,0 +1,74 @@
package com.ycwl.basic.handler;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
/**
* String列表类型处理器
* 用于将数据库中的JSON字符串转换为Java的List<String>对象
*/
@Slf4j
@MappedTypes(List.class)
@MappedJdbcTypes(JdbcType.VARCHAR)
public class StringListTypeHandler extends BaseTypeHandler<List<String>> {
private final ObjectMapper objectMapper = new ObjectMapper();
private final TypeReference<List<String>> typeReference = new TypeReference<List<String>>() {};
@Override
public void setNonNullParameter(PreparedStatement ps, int i, List<String> parameter, JdbcType jdbcType) throws SQLException {
try {
String json = objectMapper.writeValueAsString(parameter);
ps.setString(i, json);
log.debug("序列化String列表: {}", json);
} catch (JsonProcessingException e) {
log.error("序列化String列表失败", e);
throw new SQLException("序列化String列表失败", e);
}
}
@Override
public List<String> getNullableResult(ResultSet rs, String columnName) throws SQLException {
return parseJson(rs.getString(columnName));
}
@Override
public List<String> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return parseJson(rs.getString(columnIndex));
}
@Override
public List<String> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return parseJson(cs.getString(columnIndex));
}
/**
* 解析JSON字符串为List<String>
*/
private List<String> parseJson(String json) {
if (json == null || json.isEmpty() || "null".equals(json)) {
return new ArrayList<>();
}
try {
List<String> result = objectMapper.readValue(json, typeReference);
log.debug("反序列化String列表: {}", result);
return result;
} catch (JsonProcessingException e) {
log.error("反序列化String列表失败: {}", json, e);
return new ArrayList<>();
}
}
}

View File

@@ -7,6 +7,7 @@ import lombok.Builder;
import lombok.Getter; import lombok.Getter;
import java.io.File; import java.io.File;
import java.util.List;
/** /**
* 水印Stage配置 * 水印Stage配置
@@ -67,4 +68,16 @@ public class WatermarkConfig {
*/ */
@Builder.Default @Builder.Default
private final long edgeTimeoutMs = 10_000L; private final long edgeTimeoutMs = 10_000L;
/**
* 打印水印竖版URL列表
* 用于在竖屏图片上添加全屏水印叠加层(在原图上方,文字/二维码下方)
*/
private final List<String> printWatermarkPUrlList;
/**
* 打印水印横版URL列表
* 用于在横屏图片上添加全屏水印叠加层(在原图上方,文字/二维码下方)
*/
private final List<String> printWatermarkLUrlList;
} }

View File

@@ -241,6 +241,10 @@ public class WatermarkStage extends AbstractPipelineStage<PhotoProcessContext> {
} }
} }
// 传递打印水印URL列表(横竖版由边缘端根据图片实际尺寸判断)
info.setPrintWatermarkPUrlList(config.getPrintWatermarkPUrlList());
info.setPrintWatermarkLUrlList(config.getPrintWatermarkLUrlList());
return info; return info;
} }
} }

View File

@@ -77,6 +77,18 @@ public class PrinterDefaultWatermarkTemplateBuilder extends AbstractWatermarkTem
elements.add(originalImageElement); elements.add(originalImageElement);
dynamicData.put("originalImage", request.getOriginalImageUrl()); dynamicData.put("originalImage", request.getOriginalImageUrl());
// 0.5 打印水印叠加层(z-index=5,全屏覆盖,在原图上方、文字/二维码下方)
if (request.getPrintWatermarkUrl() != null && !request.getPrintWatermarkUrl().isEmpty()) {
PuzzleElementEntity printWatermarkElement = createImageElement(
"printWatermark", "打印水印",
0, 0,
imageWidth, imageHeight, 5,
FIT_MODE_COVER, null, null
);
elements.add(printWatermarkElement);
dynamicData.put("printWatermark", request.getPrintWatermarkUrl());
}
// 计算二维码位置 // 计算二维码位置
int qrcodeWidth = scaledQrcodeSize; int qrcodeWidth = scaledQrcodeSize;
int qrcodeHeight = scaledQrcodeSize; int qrcodeHeight = scaledQrcodeSize;

View File

@@ -21,6 +21,7 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URL; import java.net.URL;
import java.util.Date; import java.util.Date;
import java.util.List;
import java.util.UUID; import java.util.UUID;
/** /**
@@ -237,6 +238,8 @@ public class WatermarkEdgeService {
.offsetRight(info.getOffsetRight()) .offsetRight(info.getOffsetRight())
.offsetTop(info.getOffsetTop()) .offsetTop(info.getOffsetTop())
.offsetBottom(info.getOffsetBottom()) .offsetBottom(info.getOffsetBottom())
.printWatermarkUrl(resolvePrintWatermarkUrl(imageWidth, imageHeight,
info.getPrintWatermarkPUrlList(), info.getPrintWatermarkLUrlList()))
.outputFormat(type.getPreferFileType().equalsIgnoreCase("png") ? "PNG" : "JPEG") .outputFormat(type.getPreferFileType().equalsIgnoreCase("png") ? "PNG" : "JPEG")
.outputQuality(90) .outputQuality(90)
.build(); .build();
@@ -294,6 +297,25 @@ public class WatermarkEdgeService {
}; };
} }
/**
* 根据图片方向从URL列表中选取对应的打印水印URL
*
* @param imageWidth 图片宽度
* @param imageHeight 图片高度
* @param pUrlList 竖版URL列表
* @param lUrlList 横版URL列表
* @return 选中的URL,无可用URL时返回null
*/
private String resolvePrintWatermarkUrl(int imageWidth, int imageHeight,
List<String> pUrlList, List<String> lUrlList) {
boolean isLandscape = imageWidth >= imageHeight;
List<String> urlList = isLandscape ? lUrlList : pUrlList;
if (urlList != null && !urlList.isEmpty()) {
return urlList.get(0);
}
return null;
}
/** /**
* 获取图片尺寸 * 获取图片尺寸
* *

View File

@@ -70,6 +70,12 @@ public class WatermarkRequest {
@Builder.Default @Builder.Default
private Integer outputQuality = 75; private Integer outputQuality = 75;
/**
* 打印水印URL(根据图片方向已解析的单个URL)
* 用于在原图上方、文字/二维码下方添加全屏水印叠加层
*/
private String printWatermarkUrl;
public double getScaleValue() { public double getScaleValue() {
return scale != null ? scale : 1.0; return scale != null ? scale : 1.0;
} }

View File

@@ -5,6 +5,7 @@ import lombok.Data;
import java.io.File; import java.io.File;
import java.util.Date; import java.util.Date;
import java.util.List;
@Data @Data
public class WatermarkInfo { public class WatermarkInfo {
@@ -40,6 +41,16 @@ public class WatermarkInfo {
*/ */
private Double scale; private Double scale;
/**
* 打印水印竖版URL列表
*/
private List<String> printWatermarkPUrlList;
/**
* 打印水印横版URL列表
*/
private List<String> printWatermarkLUrlList;
public String getDatetimeLine() { public String getDatetimeLine() {
if (datetimeLine == null) { if (datetimeLine == null) {
datetimeLine = DateUtil.format(datetime, dtFormat); datetimeLine = DateUtil.format(datetime, dtFormat);

View File

@@ -64,6 +64,8 @@ public class DeviceDefaultConfigIntegrationService {
log.info("创建默认配置, configKey: {}", request.getConfigKey()); log.info("创建默认配置, configKey: {}", request.getConfigKey());
CommonResponse<String> response = defaultConfigClient.createDefaultConfig(request); CommonResponse<String> response = defaultConfigClient.createDefaultConfig(request);
String result = handleResponse(response, "创建默认配置失败"); String result = handleResponse(response, "创建默认配置失败");
// 清理相关缓存
clearDefaultConfigCache(request.getConfigKey());
return result != null; return result != null;
} }
@@ -75,6 +77,9 @@ public class DeviceDefaultConfigIntegrationService {
CommonResponse<Map<String, Object>> response = defaultConfigClient.updateDefaultConfig(configKey, updates); CommonResponse<Map<String, Object>> response = defaultConfigClient.updateDefaultConfig(configKey, updates);
Map<String, Object> result = handleResponse(response, "更新默认配置失败"); Map<String, Object> result = handleResponse(response, "更新默认配置失败");
// 清理相关缓存
clearDefaultConfigCache(configKey);
// 检查是否有冲突信息 // 检查是否有冲突信息
if (result != null && result.containsKey("conflict")) { if (result != null && result.containsKey("conflict")) {
Object conflictObj = result.get("conflict"); Object conflictObj = result.get("conflict");
@@ -94,6 +99,8 @@ public class DeviceDefaultConfigIntegrationService {
log.info("删除默认配置, configKey: {}", configKey); log.info("删除默认配置, configKey: {}", configKey);
CommonResponse<String> response = defaultConfigClient.deleteDefaultConfig(configKey); CommonResponse<String> response = defaultConfigClient.deleteDefaultConfig(configKey);
String result = handleResponse(response, "删除默认配置失败"); String result = handleResponse(response, "删除默认配置失败");
// 清理相关缓存
clearDefaultConfigCache(configKey);
return result != null; return result != null;
} }
@@ -103,7 +110,26 @@ public class DeviceDefaultConfigIntegrationService {
public BatchDefaultConfigResponse batchUpdateDefaultConfigs(BatchDefaultConfigRequest request) { public BatchDefaultConfigResponse batchUpdateDefaultConfigs(BatchDefaultConfigRequest request) {
log.info("批量更新默认配置, configs count: {}", request.getConfigs().size()); log.info("批量更新默认配置, configs count: {}", request.getConfigs().size());
CommonResponse<BatchDefaultConfigResponse> response = defaultConfigClient.batchUpdateDefaultConfigs(request); CommonResponse<BatchDefaultConfigResponse> response = defaultConfigClient.batchUpdateDefaultConfigs(request);
return handleResponse(response, "批量更新默认配置失败"); BatchDefaultConfigResponse result = handleResponse(response, "批量更新默认配置失败");
// 清理所有默认配置相关缓存
clearAllDefaultConfigCache();
return result;
}
/**
* 清理指定配置的缓存
*/
private void clearDefaultConfigCache(String configKey) {
if (configKey != null) {
fallbackService.clearFallbackCache(SERVICE_NAME, "defaults:config:" + configKey);
}
}
/**
* 清理所有默认配置缓存
*/
private void clearAllDefaultConfigCache() {
fallbackService.clearAllFallbackCache(SERVICE_NAME);
} }
/** /**

View File

@@ -58,12 +58,16 @@ public class DeviceIntegrationService {
log.debug("更新设备信息, deviceId: {}", deviceId); log.debug("更新设备信息, deviceId: {}", deviceId);
CommonResponse<String> response = deviceV2Client.updateDevice(deviceId, request); CommonResponse<String> response = deviceV2Client.updateDevice(deviceId, request);
handleResponse(response, "更新设备信息失败"); handleResponse(response, "更新设备信息失败");
// 清理设备相关缓存
fallbackService.clearFallbackCache(SERVICE_NAME, "device:" + deviceId);
} }
public void deleteDevice(Long deviceId) { public void deleteDevice(Long deviceId) {
log.debug("删除设备, deviceId: {}", deviceId); log.debug("删除设备, deviceId: {}", deviceId);
CommonResponse<String> response = deviceV2Client.deleteDevice(deviceId); CommonResponse<String> response = deviceV2Client.deleteDevice(deviceId);
handleResponse(response, "删除设备失败"); handleResponse(response, "删除设备失败");
// 清理设备相关缓存
fallbackService.clearFallbackCache(SERVICE_NAME, "device:" + deviceId);
} }
public PageResponse<DeviceV2DTO> listDevices(Integer page, Integer pageSize, String name, String no, public PageResponse<DeviceV2DTO> listDevices(Integer page, Integer pageSize, String name, String no,

View File

@@ -83,7 +83,10 @@ public class ProfitShareIntegrationService {
public RuleVO updateRule(Long ruleId, CreateRuleRequest request) { public RuleVO updateRule(Long ruleId, CreateRuleRequest request) {
log.debug("更新分账规则, ruleId: {}", ruleId); log.debug("更新分账规则, ruleId: {}", ruleId);
CommonResponse<RuleVO> response = profitShareClient.updateRule(ruleId, request); CommonResponse<RuleVO> response = profitShareClient.updateRule(ruleId, request);
return handleResponse(response, "更新分账规则失败"); RuleVO result = handleResponse(response, "更新分账规则失败");
// 清理规则缓存
clearRuleCache(ruleId);
return result;
} }
/** /**
@@ -93,6 +96,8 @@ public class ProfitShareIntegrationService {
log.debug("删除分账规则, ruleId: {}", ruleId); log.debug("删除分账规则, ruleId: {}", ruleId);
CommonResponse<Void> response = profitShareClient.deleteRule(ruleId); CommonResponse<Void> response = profitShareClient.deleteRule(ruleId);
handleResponse(response, "删除分账规则失败"); handleResponse(response, "删除分账规则失败");
// 清理规则缓存
clearRuleCache(ruleId);
} }
/** /**
@@ -102,6 +107,8 @@ public class ProfitShareIntegrationService {
log.debug("启用分账规则, ruleId: {}", ruleId); log.debug("启用分账规则, ruleId: {}", ruleId);
CommonResponse<Void> response = profitShareClient.enableRule(ruleId); CommonResponse<Void> response = profitShareClient.enableRule(ruleId);
handleResponse(response, "启用分账规则失败"); handleResponse(response, "启用分账规则失败");
// 清理规则缓存
clearRuleCache(ruleId);
} }
/** /**
@@ -111,6 +118,8 @@ public class ProfitShareIntegrationService {
log.debug("禁用分账规则, ruleId: {}", ruleId); log.debug("禁用分账规则, ruleId: {}", ruleId);
CommonResponse<Void> response = profitShareClient.disableRule(ruleId); CommonResponse<Void> response = profitShareClient.disableRule(ruleId);
handleResponse(response, "禁用分账规则失败"); handleResponse(response, "禁用分账规则失败");
// 清理规则缓存
clearRuleCache(ruleId);
} }
// ==================== 分账记录查询 ==================== // ==================== 分账记录查询 ====================
@@ -211,6 +220,13 @@ public class ProfitShareIntegrationService {
// ==================== 私有方法 ==================== // ==================== 私有方法 ====================
/**
* 清理规则相关缓存
*/
private void clearRuleCache(Long ruleId) {
fallbackService.clearFallbackCache(SERVICE_NAME, "rule:" + ruleId);
}
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

@@ -94,31 +94,55 @@ public class QuestionnaireIntegrationService {
public QuestionnaireResponse updateQuestionnaire(Long id, CreateQuestionnaireRequest request, String userId) { public QuestionnaireResponse updateQuestionnaire(Long id, CreateQuestionnaireRequest request, String userId) {
log.info("更新问卷, id: {}, userId: {}", id, userId); log.info("更新问卷, id: {}, userId: {}", id, userId);
CommonResponse<QuestionnaireResponse> response = questionnaireClient.updateQuestionnaire(id, request, userId); CommonResponse<QuestionnaireResponse> response = questionnaireClient.updateQuestionnaire(id, request, userId);
return handleResponse(response, "更新问卷失败"); QuestionnaireResponse result = handleResponse(response, "更新问卷失败");
// 清理问卷相关缓存
clearQuestionnaireCache(id);
return result;
} }
public void deleteQuestionnaire(Long id, String userId) { public void deleteQuestionnaire(Long id, String userId) {
log.info("删除问卷, id: {}, userId: {}", id, userId); log.info("删除问卷, id: {}, userId: {}", id, userId);
CommonResponse<Void> response = questionnaireClient.deleteQuestionnaire(id, userId); CommonResponse<Void> response = questionnaireClient.deleteQuestionnaire(id, userId);
handleResponse(response, "删除问卷失败"); handleResponse(response, "删除问卷失败");
// 清理问卷相关缓存
clearQuestionnaireCache(id);
} }
public QuestionnaireResponse publishQuestionnaire(Long id, String userId) { public QuestionnaireResponse publishQuestionnaire(Long id, String userId) {
log.info("发布问卷, id: {}, userId: {}", id, userId); log.info("发布问卷, id: {}, userId: {}", id, userId);
CommonResponse<QuestionnaireResponse> response = questionnaireClient.publishQuestionnaire(id, userId); CommonResponse<QuestionnaireResponse> response = questionnaireClient.publishQuestionnaire(id, userId);
return handleResponse(response, "发布问卷失败"); QuestionnaireResponse result = handleResponse(response, "发布问卷失败");
// 清理问卷相关缓存(发布会改变状态)
clearQuestionnaireCache(id);
return result;
} }
public QuestionnaireResponse stopQuestionnaire(Long id, String userId) { public QuestionnaireResponse stopQuestionnaire(Long id, String userId) {
log.info("停止问卷, id: {}, userId: {}", id, userId); log.info("停止问卷, id: {}, userId: {}", id, userId);
CommonResponse<QuestionnaireResponse> response = questionnaireClient.stopQuestionnaire(id, userId); CommonResponse<QuestionnaireResponse> response = questionnaireClient.stopQuestionnaire(id, userId);
return handleResponse(response, "停止问卷失败"); QuestionnaireResponse result = handleResponse(response, "停止问卷失败");
// 清理问卷相关缓存
clearQuestionnaireCache(id);
return result;
} }
public ResponseDetailResponse submitAnswer(SubmitAnswerRequest request) { public ResponseDetailResponse submitAnswer(SubmitAnswerRequest request) {
log.info("提交问卷答案, questionnaireId: {}, userId: {}", request.getQuestionnaireId(), request.getUserId()); log.info("提交问卷答案, questionnaireId: {}, userId: {}", request.getQuestionnaireId(), request.getUserId());
CommonResponse<ResponseDetailResponse> response = questionnaireClient.submitAnswer(request); CommonResponse<ResponseDetailResponse> response = questionnaireClient.submitAnswer(request);
return handleResponse(response, "提交问卷答案失败"); ResponseDetailResponse result = handleResponse(response, "提交问卷答案失败");
// 清理统计缓存(提交答案会影响统计)
if (request.getQuestionnaireId() != null) {
fallbackService.clearFallbackCache(SERVICE_NAME, "questionnaire:statistics:" + request.getQuestionnaireId());
}
return result;
}
/**
* 清理问卷相关缓存
*/
private void clearQuestionnaireCache(Long questionnaireId) {
fallbackService.clearFallbackCache(SERVICE_NAME, "questionnaire:" + questionnaireId);
fallbackService.clearFallbackCache(SERVICE_NAME, "questionnaire:statistics:" + questionnaireId);
} }

View File

@@ -47,6 +47,21 @@ public interface RenderJobV2Client {
@PostMapping("/jobs/{jobId}/cancel") @PostMapping("/jobs/{jobId}/cancel")
CommonResponse<Void> cancelJob(@PathVariable("jobId") Long jobId); CommonResponse<Void> cancelJob(@PathVariable("jobId") Long jobId);
/**
* 创建FINALIZE_MP4任务
* 将所有已发布的TS片段合成为MP4文件
*
* 前置条件:
* 1. 作业存在且状态为RUNNING
* 2. 所有片段都已发布(PublishedCount == SegmentCount)
* 3. 不存在已有的FINALIZE_MP4任务
*
* @param jobId 作业ID
* @return 任务创建结果
*/
@PostMapping("/jobs/{jobId}/finalize-mp4")
CommonResponse<FinalizeMP4Response> createFinalizeMP4Task(@PathVariable("jobId") Long jobId);
// ==================== 管理端接口 ==================== // ==================== 管理端接口 ====================
/** /**

View File

@@ -0,0 +1,20 @@
package com.ycwl.basic.integration.render.dto.job;
import lombok.Data;
/**
* 创建FINALIZE_MP4任务响应
*/
@Data
public class FinalizeMP4Response {
/**
* 任务ID
*/
private Long taskId;
/**
* 任务状态
*/
private String status;
}

View File

@@ -21,7 +21,6 @@ import java.util.List;
public class RenderJobIntegrationService { public class RenderJobIntegrationService {
private final RenderJobV2Client renderJobV2Client; private final RenderJobV2Client renderJobV2Client;
private final IntegrationFallbackService fallbackService;
private static final String SERVICE_NAME = "zt-render-worker"; private static final String SERVICE_NAME = "zt-render-worker";
@@ -41,15 +40,8 @@ public class RenderJobIntegrationService {
*/ */
public JobStatusResponse getJobStatus(Long jobId) { public JobStatusResponse getJobStatus(Long jobId) {
log.debug("获取作业状态, jobId: {}", jobId); log.debug("获取作业状态, jobId: {}", jobId);
return fallbackService.executeWithFallback(
SERVICE_NAME,
"job:status:" + jobId,
() -> {
CommonResponse<JobStatusResponse> response = renderJobV2Client.getJobStatus(jobId); CommonResponse<JobStatusResponse> response = renderJobV2Client.getJobStatus(jobId);
return handleResponse(response, "获取作业状态失败"); return handleResponse(response, "获取作业状态失败");
},
JobStatusResponse.class
);
} }
/** /**
@@ -71,15 +63,8 @@ public class RenderJobIntegrationService {
*/ */
public PlaylistInfoDTO getPlaylistInfo(Long jobId) { public PlaylistInfoDTO getPlaylistInfo(Long jobId) {
log.debug("获取播放列表信息, jobId: {}", jobId); log.debug("获取播放列表信息, jobId: {}", jobId);
return fallbackService.executeWithFallback(
SERVICE_NAME,
"job:playlist-info:" + jobId,
() -> {
CommonResponse<PlaylistInfoDTO> response = renderJobV2Client.getPlaylistInfo(jobId); CommonResponse<PlaylistInfoDTO> response = renderJobV2Client.getPlaylistInfo(jobId);
return handleResponse(response, "获取播放列表信息失败"); return handleResponse(response, "获取播放列表信息失败");
},
PlaylistInfoDTO.class
);
} }
/** /**
@@ -91,6 +76,20 @@ public class RenderJobIntegrationService {
handleVoidResponse(response, "取消作业失败"); handleVoidResponse(response, "取消作业失败");
} }
/**
* 创建FINALIZE_MP4任务(直接调用,不降级)
* 将所有已发布的TS片段合成为MP4文件
*
* @param jobId 作业ID
* @return 任务创建结果
* @throws IntegrationException 当前置条件不满足时抛出异常
*/
public FinalizeMP4Response createFinalizeMP4Task(Long jobId) {
log.debug("创建FINALIZE_MP4任务, jobId: {}", jobId);
CommonResponse<FinalizeMP4Response> response = renderJobV2Client.createFinalizeMP4Task(jobId);
return handleResponse(response, "创建FINALIZE_MP4任务失败");
}
// ==================== 管理端接口 ==================== // ==================== 管理端接口 ====================
/** /**
@@ -110,15 +109,8 @@ public class RenderJobIntegrationService {
*/ */
public RenderJobV2DTO getJobDetail(Long jobId) { public RenderJobV2DTO getJobDetail(Long jobId) {
log.debug("获取作业详情, jobId: {}", jobId); log.debug("获取作业详情, jobId: {}", jobId);
return fallbackService.executeWithFallback(
SERVICE_NAME,
"job:detail:" + jobId,
() -> {
CommonResponse<RenderJobV2DTO> response = renderJobV2Client.getJobDetail(jobId); CommonResponse<RenderJobV2DTO> response = renderJobV2Client.getJobDetail(jobId);
return handleResponse(response, "获取作业详情失败"); return handleResponse(response, "获取作业详情失败");
},
RenderJobV2DTO.class
);
} }
/** /**
@@ -127,16 +119,9 @@ public class RenderJobIntegrationService {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public List<RenderJobSegmentV2DTO> getJobSegments(Long jobId) { public List<RenderJobSegmentV2DTO> getJobSegments(Long jobId) {
log.debug("获取作业片段列表, jobId: {}", jobId); log.debug("获取作业片段列表, jobId: {}", jobId);
return fallbackService.executeWithFallback(
SERVICE_NAME,
"job:segments:" + jobId,
() -> {
CommonResponse<List<RenderJobSegmentV2DTO>> response = CommonResponse<List<RenderJobSegmentV2DTO>> response =
renderJobV2Client.getJobSegments(jobId); renderJobV2Client.getJobSegments(jobId);
return handleResponse(response, "获取作业片段列表失败"); return handleResponse(response, "获取作业片段列表失败");
},
(Class<List<RenderJobSegmentV2DTO>>) (Class<?>) List.class
);
} }
// ==================== Helper Methods ==================== // ==================== Helper Methods ====================

View File

@@ -88,6 +88,8 @@ public class RenderTemplateIntegrationService {
log.debug("更新渲染模板, id: {}, name: {}", id, request.getName()); log.debug("更新渲染模板, id: {}, name: {}", id, request.getName());
CommonResponse<Void> response = renderTemplateV2Client.updateTemplate(id, request); CommonResponse<Void> response = renderTemplateV2Client.updateTemplate(id, request);
handleVoidResponse(response, "更新渲染模板失败"); handleVoidResponse(response, "更新渲染模板失败");
// 清理模板相关缓存
clearTemplateCache(id);
} }
/** /**
@@ -97,6 +99,8 @@ public class RenderTemplateIntegrationService {
log.debug("删除渲染模板, id: {}", id); log.debug("删除渲染模板, id: {}", id);
CommonResponse<Void> response = renderTemplateV2Client.deleteTemplate(id); CommonResponse<Void> response = renderTemplateV2Client.deleteTemplate(id);
handleVoidResponse(response, "删除渲染模板失败"); handleVoidResponse(response, "删除渲染模板失败");
// 清理模板相关缓存
clearTemplateCache(id);
} }
// ==================== Template Operations ==================== // ==================== Template Operations ====================
@@ -108,6 +112,8 @@ public class RenderTemplateIntegrationService {
log.debug("发布渲染模板, id: {}", id); log.debug("发布渲染模板, id: {}", id);
CommonResponse<Void> response = renderTemplateV2Client.publishTemplate(id); CommonResponse<Void> response = renderTemplateV2Client.publishTemplate(id);
handleVoidResponse(response, "发布渲染模板失败"); handleVoidResponse(response, "发布渲染模板失败");
// 清理模板相关缓存(发布会改变状态)
clearTemplateCache(id);
} }
/** /**
@@ -116,7 +122,10 @@ public class RenderTemplateIntegrationService {
public TemplateV2DTO createTemplateVersion(Long id) { public TemplateV2DTO createTemplateVersion(Long id) {
log.debug("创建渲染模板新版本, id: {}", id); log.debug("创建渲染模板新版本, id: {}", id);
CommonResponse<TemplateV2DTO> response = renderTemplateV2Client.createTemplateVersion(id); CommonResponse<TemplateV2DTO> response = renderTemplateV2Client.createTemplateVersion(id);
return handleResponse(response, "创建渲染模板新版本失败"); TemplateV2DTO result = handleResponse(response, "创建渲染模板新版本失败");
// 清理模板相关缓存
clearTemplateCache(id);
return result;
} }
// ==================== Segment Management ==================== // ==================== Segment Management ====================
@@ -146,7 +155,10 @@ public class RenderTemplateIntegrationService {
log.debug("创建模板片段, templateId: {}, segmentIndex: {}", templateId, request.getSegmentIndex()); log.debug("创建模板片段, templateId: {}, segmentIndex: {}", templateId, request.getSegmentIndex());
CommonResponse<TemplateV2SegmentDTO> response = CommonResponse<TemplateV2SegmentDTO> response =
renderTemplateV2Client.createSegment(templateId, request); renderTemplateV2Client.createSegment(templateId, request);
return handleResponse(response, "创建模板片段失败"); TemplateV2SegmentDTO result = handleResponse(response, "创建模板片段失败");
// 清理模板片段缓存
clearTemplateSegmentCache(templateId);
return result;
} }
/** /**
@@ -157,6 +169,8 @@ public class RenderTemplateIntegrationService {
CommonResponse<Void> response = CommonResponse<Void> response =
renderTemplateV2Client.updateSegment(templateId, segmentId, request); renderTemplateV2Client.updateSegment(templateId, segmentId, request);
handleVoidResponse(response, "更新模板片段失败"); handleVoidResponse(response, "更新模板片段失败");
// 清理模板片段缓存
clearTemplateSegmentCache(templateId);
} }
/** /**
@@ -166,6 +180,8 @@ public class RenderTemplateIntegrationService {
log.debug("删除模板片段, templateId: {}, segmentId: {}", templateId, segmentId); log.debug("删除模板片段, templateId: {}, segmentId: {}", templateId, segmentId);
CommonResponse<Void> response = renderTemplateV2Client.deleteSegment(templateId, segmentId); CommonResponse<Void> response = renderTemplateV2Client.deleteSegment(templateId, segmentId);
handleVoidResponse(response, "删除模板片段失败"); handleVoidResponse(response, "删除模板片段失败");
// 清理模板片段缓存
clearTemplateSegmentCache(templateId);
} }
/** /**
@@ -176,10 +192,29 @@ public class RenderTemplateIntegrationService {
templateId, request.getSegments() != null ? request.getSegments().size() : 0); templateId, request.getSegments() != null ? request.getSegments().size() : 0);
CommonResponse<Void> response = renderTemplateV2Client.replaceSegments(templateId, request); CommonResponse<Void> response = renderTemplateV2Client.replaceSegments(templateId, request);
handleVoidResponse(response, "替换所有模板片段失败"); handleVoidResponse(response, "替换所有模板片段失败");
// 清理模板及片段缓存
clearTemplateCache(templateId);
clearTemplateSegmentCache(templateId);
} }
// ==================== Helper Methods ==================== // ==================== Helper Methods ====================
/**
* 清理模板缓存
*/
private void clearTemplateCache(Long templateId) {
fallbackService.clearFallbackCache(SERVICE_NAME, "template:" + templateId);
fallbackService.clearFallbackCache(SERVICE_NAME, "template:with-segments:" + templateId);
}
/**
* 清理模板片段缓存
*/
private void clearTemplateSegmentCache(Long templateId) {
fallbackService.clearFallbackCache(SERVICE_NAME, "template:segments:" + templateId);
fallbackService.clearFallbackCache(SERVICE_NAME, "template:with-segments:" + templateId);
}
/** /**
* 处理通用响应 * 处理通用响应
*/ */

View File

@@ -87,7 +87,10 @@ public class RenderWorkerConfigIntegrationService {
log.debug("创建渲染工作器配置, workerId: {}, configKey: {}", workerId, config.getConfigKey()); log.debug("创建渲染工作器配置, workerId: {}, configKey: {}", workerId, config.getConfigKey());
CommonResponse<RenderWorkerConfigV2DTO> response = CommonResponse<RenderWorkerConfigV2DTO> response =
renderWorkerConfigV2Client.createWorkerConfig(workerId, config); renderWorkerConfigV2Client.createWorkerConfig(workerId, config);
return handleResponse(response, "创建渲染工作器配置失败"); RenderWorkerConfigV2DTO result = handleResponse(response, "创建渲染工作器配置失败");
// 清理配置相关缓存
clearWorkerConfigCache(workerId, config.getConfigKey());
return result;
} }
/** /**
@@ -98,6 +101,8 @@ public class RenderWorkerConfigIntegrationService {
CommonResponse<Void> response = CommonResponse<Void> response =
renderWorkerConfigV2Client.updateWorkerConfig(workerId, configId, updates); renderWorkerConfigV2Client.updateWorkerConfig(workerId, configId, updates);
handleVoidResponse(response, "更新渲染工作器配置失败"); handleVoidResponse(response, "更新渲染工作器配置失败");
// 清理所有配置缓存(无法确定具体key)
clearAllWorkerConfigCache(workerId);
} }
/** /**
@@ -108,6 +113,8 @@ public class RenderWorkerConfigIntegrationService {
CommonResponse<Void> response = CommonResponse<Void> response =
renderWorkerConfigV2Client.deleteWorkerConfig(workerId, configId); renderWorkerConfigV2Client.deleteWorkerConfig(workerId, configId);
handleVoidResponse(response, "删除渲染工作器配置失败"); handleVoidResponse(response, "删除渲染工作器配置失败");
// 清理所有配置缓存(无法确定具体key)
clearAllWorkerConfigCache(workerId);
} }
/** /**
@@ -119,6 +126,8 @@ public class RenderWorkerConfigIntegrationService {
CommonResponse<Void> response = CommonResponse<Void> response =
renderWorkerConfigV2Client.batchUpdateWorkerConfigs(workerId, request); renderWorkerConfigV2Client.batchUpdateWorkerConfigs(workerId, request);
handleVoidResponse(response, "批量更新渲染工作器配置失败"); handleVoidResponse(response, "批量更新渲染工作器配置失败");
// 清理所有配置缓存
clearAllWorkerConfigCache(workerId);
} }
/** /**
@@ -142,6 +151,26 @@ public class RenderWorkerConfigIntegrationService {
request.setConfigs(configs); request.setConfigs(configs);
batchUpdateWorkerConfigs(workerId, request); batchUpdateWorkerConfigs(workerId, request);
// 缓存清理已在 batchUpdateWorkerConfigs 中处理
}
/**
* 清理指定配置的缓存
*/
private void clearWorkerConfigCache(Long workerId, String configKey) {
fallbackService.clearFallbackCache(SERVICE_NAME, "worker:configs:" + workerId);
fallbackService.clearFallbackCache(SERVICE_NAME, "worker:flat:config:" + workerId);
if (configKey != null) {
fallbackService.clearFallbackCache(SERVICE_NAME, "worker:config:" + workerId + ":" + configKey);
}
}
/**
* 清理工作器所有配置缓存
*/
private void clearAllWorkerConfigCache(Long workerId) {
fallbackService.clearFallbackCache(SERVICE_NAME, "worker:configs:" + workerId);
fallbackService.clearFallbackCache(SERVICE_NAME, "worker:flat:config:" + workerId);
} }
/** /**

View File

@@ -58,6 +58,8 @@ public class RenderWorkerIntegrationService {
log.debug("更新渲染工作器, id: {}, name: {}", id, request.getName()); log.debug("更新渲染工作器, id: {}, name: {}", id, request.getName());
CommonResponse<Void> response = renderWorkerV2Client.updateWorker(id, request); CommonResponse<Void> response = renderWorkerV2Client.updateWorker(id, request);
handleVoidResponse(response, "更新渲染工作器失败"); handleVoidResponse(response, "更新渲染工作器失败");
// 清理工作器相关缓存
fallbackService.clearFallbackCache(SERVICE_NAME, "worker:" + id);
} }
/** /**
@@ -67,6 +69,8 @@ public class RenderWorkerIntegrationService {
log.debug("删除渲染工作器, id: {}", id); log.debug("删除渲染工作器, id: {}", id);
CommonResponse<Void> response = renderWorkerV2Client.deleteWorker(id); CommonResponse<Void> response = renderWorkerV2Client.deleteWorker(id);
handleVoidResponse(response, "删除渲染工作器失败"); handleVoidResponse(response, "删除渲染工作器失败");
// 清理工作器相关缓存
fallbackService.clearFallbackCache(SERVICE_NAME, "worker:" + id);
} }
/** /**

View File

@@ -55,25 +55,53 @@ public class ScenicConfigIntegrationService {
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);
return handleResponse(response, "创建景区配置失败"); ScenicConfigV2DTO result = handleResponse(response, "创建景区配置失败");
// 清理配置列表缓存
clearConfigCache(scenicId, request.getConfigKey());
return result;
} }
public ScenicConfigV2DTO updateConfig(Long scenicId, String id, UpdateConfigRequest request) { public ScenicConfigV2DTO updateConfig(Long scenicId, String id, UpdateConfigRequest request) {
log.debug("更新景区配置, scenicId: {}, id: {}", scenicId, id); log.debug("更新景区配置, scenicId: {}, id: {}", scenicId, id);
CommonResponse<ScenicConfigV2DTO> response = scenicConfigV2Client.updateConfig(scenicId, id, request); CommonResponse<ScenicConfigV2DTO> response = scenicConfigV2Client.updateConfig(scenicId, id, request);
return handleResponse(response, "更新景区配置失败"); ScenicConfigV2DTO result = handleResponse(response, "更新景区配置失败");
// 清理配置相关缓存
clearConfigCache(scenicId, request.getConfigKey());
return result;
} }
public void deleteConfig(Long scenicId, String id) { public void deleteConfig(Long scenicId, String id) {
log.debug("删除景区配置, scenicId: {}, id: {}", scenicId, id); log.debug("删除景区配置, scenicId: {}, id: {}", scenicId, id);
CommonResponse<Void> response = scenicConfigV2Client.deleteConfig(scenicId, id); CommonResponse<Void> response = scenicConfigV2Client.deleteConfig(scenicId, id);
handleResponse(response, "删除景区配置失败"); handleResponse(response, "删除景区配置失败");
// 清理配置列表缓存(无法确定具体key,清理整个列表缓存)
fallbackService.clearFallbackCache(SERVICE_NAME, "scenic:configs:" + scenicId);
} }
public BatchUpdateResponse batchUpdateConfigs(Long scenicId, BatchConfigRequest request) { public BatchUpdateResponse batchUpdateConfigs(Long scenicId, BatchConfigRequest request) {
log.debug("批量更新景区配置, scenicId: {}, configs count: {}", scenicId, request.getConfigs().size()); log.debug("批量更新景区配置, scenicId: {}, configs count: {}", scenicId, request.getConfigs().size());
CommonResponse<BatchUpdateResponse> response = scenicConfigV2Client.batchUpdateConfigs(scenicId, request); CommonResponse<BatchUpdateResponse> response = scenicConfigV2Client.batchUpdateConfigs(scenicId, request);
return handleResponse(response, "批量更新景区配置失败"); BatchUpdateResponse result = handleResponse(response, "批量更新景区配置失败");
// 清理所有相关缓存
clearAllConfigCache(scenicId);
return result;
}
/**
* 清理指定配置的缓存
*/
private void clearConfigCache(Long scenicId, String configKey) {
fallbackService.clearFallbackCache(SERVICE_NAME, "scenic:configs:" + scenicId);
if (configKey != null) {
fallbackService.clearFallbackCache(SERVICE_NAME, "scenic:config:" + scenicId + ":" + configKey);
}
}
/**
* 清理景区所有配置相关缓存
*/
private void clearAllConfigCache(Long scenicId) {
fallbackService.clearFallbackCache(SERVICE_NAME, "scenic:configs:" + scenicId);
} }

View File

@@ -48,13 +48,18 @@ public class ScenicIntegrationService {
public ScenicV2DTO updateScenic(Long scenicId, UpdateScenicRequest request) { public ScenicV2DTO updateScenic(Long scenicId, UpdateScenicRequest request) {
log.debug("更新景区信息, scenicId: {}", scenicId); log.debug("更新景区信息, scenicId: {}", scenicId);
CommonResponse<ScenicV2DTO> response = scenicV2Client.updateScenic(scenicId, request); CommonResponse<ScenicV2DTO> response = scenicV2Client.updateScenic(scenicId, request);
return handleResponse(response, "更新景区信息失败"); ScenicV2DTO result = handleResponse(response, "更新景区信息失败");
// 清理缓存,确保下次查询获取最新数据
fallbackService.clearFallbackCache(SERVICE_NAME, "scenic:" + scenicId);
return result;
} }
public void deleteScenic(Long scenicId) { public void deleteScenic(Long scenicId) {
log.debug("删除景区, scenicId: {}", scenicId); log.debug("删除景区, scenicId: {}", scenicId);
CommonResponse<Void> response = scenicV2Client.deleteScenic(scenicId); CommonResponse<Void> response = scenicV2Client.deleteScenic(scenicId);
handleResponse(response, "删除景区失败"); handleResponse(response, "删除景区失败");
// 清理缓存
fallbackService.clearFallbackCache(SERVICE_NAME, "scenic:" + scenicId);
} }
public ScenicFilterPageResponse filterScenics(ScenicFilterRequest request) { public ScenicFilterPageResponse filterScenics(ScenicFilterRequest request) {

View File

@@ -0,0 +1,23 @@
package com.ycwl.basic.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ycwl.basic.model.pc.printer.entity.PrinterGuideEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface PrinterGuideMapper extends BaseMapper<PrinterGuideEntity> {
List<PrinterGuideEntity> listByPrinterId(@Param("printerId") Integer printerId);
List<PrinterGuideEntity> listEnabledByPrinterId(@Param("printerId") Integer printerId);
int insertGuide(PrinterGuideEntity entity);
int deleteById(@Param("id") Integer id);
int updateSortOrder(@Param("id") Integer id, @Param("sortOrder") Integer sortOrder);
int toggleEnabled(@Param("id") Integer id);
}

View File

@@ -1,11 +1,14 @@
package com.ycwl.basic.mapper; package com.ycwl.basic.mapper;
import com.ycwl.basic.model.pc.device.resp.DeviceSourceStatsVO;
import com.ycwl.basic.model.pc.device.resp.DeviceSourceTimelineVO;
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity; import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
import com.ycwl.basic.model.pc.source.entity.SourceEntity; import com.ycwl.basic.model.pc.source.entity.SourceEntity;
import com.ycwl.basic.model.pc.source.entity.SourceWatermarkEntity; import com.ycwl.basic.model.pc.source.entity.SourceWatermarkEntity;
import com.ycwl.basic.model.pc.source.req.SourceReqQuery; import com.ycwl.basic.model.pc.source.req.SourceReqQuery;
import com.ycwl.basic.model.pc.source.resp.SourceRespVO; import com.ycwl.basic.model.pc.source.resp.SourceRespVO;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
@@ -181,4 +184,59 @@ public interface SourceMapper {
* @return source实体列表 * @return source实体列表
*/ */
List<SourceEntity> listSourceByFaceRelation(Long faceId, Integer type); List<SourceEntity> listSourceByFaceRelation(Long faceId, Integer type);
/**
* 根据sourceId列表查询关联的faceId
* @param sourceIds sourceId列表
* @return member_source记录列表(包含sourceId和faceId)
*/
List<MemberSourceEntity> listFaceIdsBySourceIds(List<Long> sourceIds);
/**
* 根据faceId分页查询关联的source记录
* @param sourceReqQuery 查询参数(需设置faceId)
* @return source响应列表
*/
List<SourceRespVO> pageByFaceId(SourceReqQuery sourceReqQuery);
int softDeleteRelation(Long id);
int reactivateRelation(Long id);
List<SourceRespVO> pageDeletedByFaceId(SourceReqQuery sourceReqQuery);
MemberSourceEntity getMemberSourceById(Long id);
/**
* 设备拍摄统计:拍摄总数、拍摄人数、售出张数、赠送张数、售出人数
* @param deviceId 设备ID
* @param startTime 开始时间
* @param endTime 结束时间
* @return 统计结果
*/
DeviceSourceStatsVO getDeviceSourceStats(Long deviceId, Date startTime, Date endTime);
/**
* 按 5 分钟分桶统计设备 type=2 的拍摄数量(仅返回有数据的桶)
* @param deviceId 设备ID
* @param startTime 开始时间
* @param endTime 结束时间
* @return 有数据的时间桶列表
*/
List<DeviceSourceTimelineVO> getDeviceSourceTimeline(Long deviceId, Date startTime, Date endTime);
/**
* 根据会员ID和素材ID查询 member_source 关联记录
* @param memberId 会员ID
* @param sourceId 素材ID
* @return 关联记录(含 faceId 等信息)
*/
MemberSourceEntity getMemberSourceByMemberAndSourceId(@Param("memberId") Long memberId, @Param("sourceId") Long sourceId);
/**
* 根据会员ID和素材ID更新 member_source 关联记录的购买状态
* @param memberSourceEntity 包含 memberId、sourceId、isBuy、orderId
* @return 影响行数
*/
int updateRelationBySourceId(MemberSourceEntity memberSourceEntity);
} }

View File

@@ -1,6 +1,8 @@
package com.ycwl.basic.mapper; package com.ycwl.basic.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ycwl.basic.model.pc.videoreview.dto.AdminVideoReviewLogReqDTO;
import com.ycwl.basic.model.pc.videoreview.dto.AdminVideoReviewLogRespDTO;
import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewListReqDTO; import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewListReqDTO;
import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewRespDTO; import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewRespDTO;
import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewStatisticsRespDTO; import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewStatisticsRespDTO;
@@ -25,6 +27,14 @@ public interface VideoReviewMapper extends BaseMapper<VideoReviewEntity> {
*/ */
List<VideoReviewRespDTO> selectReviewList(VideoReviewListReqDTO reqDTO); List<VideoReviewRespDTO> selectReviewList(VideoReviewListReqDTO reqDTO);
/**
* 管理后台分页查询评价日志(带更详细的管理信息)
*
* @param reqDTO 查询条件
* @return 评价日志列表
*/
List<AdminVideoReviewLogRespDTO> selectAdminReviewLogList(AdminVideoReviewLogReqDTO reqDTO);
/** /**
* 统计总评价数 * 统计总评价数
* *
@@ -63,9 +73,9 @@ public interface VideoReviewMapper extends BaseMapper<VideoReviewEntity> {
List<VideoReviewStatisticsRespDTO.ScenicReviewRank> countScenicRank(@Param("limit") int limit); List<VideoReviewStatisticsRespDTO.ScenicReviewRank> countScenicRank(@Param("limit") int limit);
/** /**
* 查询所有机位评价数据(用于后端计算平均值) * 查询所有问题机位ID列表(用于后端统计问题机位)
* *
* @return 机位评价列表(Map结构: 机位ID -> 评分) * @return 问题机位ID列表
*/ */
List<Map<String, Integer>> selectAllCameraPositionRatings(); List<List<Long>> selectAllProblemDeviceIds();
} }

View File

@@ -0,0 +1,64 @@
package com.ycwl.basic.mapper.task;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ycwl.basic.model.task.entity.TaskRenderJobMappingEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.Date;
import java.util.List;
/**
* Task与RenderJob关联Mapper
*/
@Mapper
public interface TaskRenderJobMappingMapper extends BaseMapper<TaskRenderJobMappingEntity> {
/**
* 根据taskId查询mapping
*/
TaskRenderJobMappingEntity selectByTaskId(@Param("taskId") Long taskId);
/**
* 根据renderJobId查询mapping
*/
TaskRenderJobMappingEntity selectByRenderJobId(@Param("renderJobId") Long renderJobId);
/**
* 查询需要轮询的记录
* 条件:状态为PENDING或PREVIEW_READY,且最后检查时间超过指定间隔
*/
List<TaskRenderJobMappingEntity> selectPendingForPolling(
@Param("statuses") List<String> statuses,
@Param("checkIntervalSeconds") int checkIntervalSeconds,
@Param("limit") int limit
);
/**
* 更新渲染状态和片段信息
*/
int updateRenderStatus(
@Param("id") Long id,
@Param("renderStatus") String renderStatus,
@Param("publishedCount") Integer publishedCount,
@Param("segmentCount") Integer segmentCount,
@Param("previewUrl") String previewUrl,
@Param("mp4Url") String mp4Url,
@Param("lastCheckTime") Date lastCheckTime
);
/**
* 更新为失败状态
*/
int updateToFailed(
@Param("id") Long id,
@Param("errorCode") String errorCode,
@Param("errorMessage") String errorMessage,
@Param("lastCheckTime") Date lastCheckTime
);
/**
* 增加重试次数
*/
int incrementRetryCount(@Param("id") Long id);
}

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 origUrl;
private String videoUrl; private String videoUrl;
// 模版id // 模版id
private Long templateId; private Long templateId;

View File

@@ -0,0 +1,20 @@
package com.ycwl.basic.model.pc.device.resp;
import lombok.Data;
/**
* 设备拍摄统计 VO
*/
@Data
public class DeviceSourceStatsVO {
/** 拍摄总数(source type=2 记录数) */
private Integer totalShots;
/** 拍摄人数(关联的不同 face_id 数) */
private Integer totalFaces;
/** 售出张数(is_buy=1 的 member_source 记录数) */
private Integer soldCount;
/** 赠送张数(is_free=1 的 member_source 记录数) */
private Integer freeCount;
/** 售出人数(is_buy=1 的不同 face_id 数) */
private Integer soldFaceCount;
}

View File

@@ -0,0 +1,22 @@
package com.ycwl.basic.model.pc.device.resp;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
/**
* 设备拍摄时间线数据点(5 分钟一个桶)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class DeviceSourceTimelineVO {
/** 时间桶起始时刻 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm", timezone = "GMT+8")
private Date time;
/** 该桶内 source type=2 的记录数 */
private Integer count;
}

View File

@@ -0,0 +1,21 @@
package com.ycwl.basic.model.pc.printer.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
@Data
@TableName("printer_guide")
public class PrinterGuideEntity {
@TableId(type = IdType.AUTO)
private Integer id;
private Integer printerId;
private String imageUrl;
private Integer sortOrder;
private Integer enabled;
private Date createTime;
private Date updateTime;
}

View File

@@ -3,6 +3,8 @@ package com.ycwl.basic.model.pc.source.entity;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data; import lombok.Data;
import java.util.Date;
@Data @Data
@TableName("member_source") @TableName("member_source")
public class MemberSourceEntity { public class MemberSourceEntity {
@@ -15,4 +17,6 @@ public class MemberSourceEntity {
private Integer isBuy; private Integer isBuy;
private Long orderId; private Long orderId;
private Integer isFree; private Integer isFree;
private Integer deleted;
private Date deletedAt;
} }

View File

@@ -46,4 +46,6 @@ public class SourceRespVO {
// 是否购买:0未购买,1已购买 // 是否购买:0未购买,1已购买
private int isBuy; private int isBuy;
private int isFree; private int isFree;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date deletedAt;
} }

View File

@@ -0,0 +1,123 @@
package com.ycwl.basic.model.pc.videoreview.dto;
import lombok.Data;
/**
* 管理后台视频评价日志查询请求DTO
*/
@Data
public class AdminVideoReviewLogReqDTO {
/**
* 评价ID(可选,精确查询)
*/
private Long id;
/**
* 视频ID(可选)
*/
private Long videoId;
/**
* 景区ID(可选)
*/
private Long scenicId;
/**
* 评价人ID(可选)
*/
private Long creator;
/**
* 评价人名称(可选,模糊查询)
*/
private String creatorName;
/**
* 评分(可选,精确匹配)
*/
private Integer rating;
/**
* 最小评分(可选,范围查询)
*/
private Integer minRating;
/**
* 最大评分(可选,范围查询)
*/
private Integer maxRating;
/**
* 开始时间(可选,格式: yyyy-MM-dd HH:mm:ss)
*/
private String startTime;
/**
* 结束时间(可选,格式: yyyy-MM-dd HH:mm:ss)
*/
private String endTime;
/**
* 关键词搜索(可选,搜索评价内容、景区名称、模板名称)
*/
private String keyword;
/**
* 模板ID(可选)
*/
private Long templateId;
/**
* 模板名称(可选,模糊查询)
*/
private String templateName;
/**
* 是否有机位评价(可选)
* true: 仅查询有机位评价的记录
* false: 仅查询无机位评价的记录
* null: 不限制
*/
private Boolean hasCameraRating;
/**
* 问题机位ID(可选,筛选包含该机位ID的评价)
* 任意一个问题机位匹配即可
*/
private Long problemDeviceId;
/**
* 问题标签(可选,筛选包含该标签的评价)
* 任意一个标签匹配即可
*/
private String problemTag;
/**
* 来源(可选,筛选指定来源的评价)
* 固定值: ORDER(订单), RENDER(渲染)
*/
private String source;
/**
* 页码(必填,默认1)
*/
private Integer pageNum = 1;
/**
* 每页数量(必填,默认20)
*/
private Integer pageSize = 20;
/**
* 排序字段(可选,默认create_time)
* 可选值: create_time, rating, update_time, id
*/
private String orderBy = "create_time";
/**
* 排序方向(可选,默认DESC)
* 可选值: ASC, DESC
*/
private String orderDirection = "DESC";
}

View File

@@ -0,0 +1,131 @@
package com.ycwl.basic.model.pc.videoreview.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
/**
* 管理后台视频评价日志响应DTO
*/
@Data
public class AdminVideoReviewLogRespDTO {
/**
* 评价ID
*/
private Long id;
/**
* 视频ID
*/
private Long videoId;
/**
* 视频URL(关联查询)
*/
private String videoUrl;
/**
* 视频时长(秒,关联查询video表)
*/
private BigDecimal duration;
/**
* 任务参数(JSON字符串,关联查询task表)
*/
private String taskParams;
/**
* 模板ID(关联查询video表)
*/
private Long templateId;
/**
* 模板名称(关联查询)
*/
private String templateName;
/**
* 景区ID
*/
private Long scenicId;
/**
* 景区名称(关联查询)
*/
private String scenicName;
/**
* 评价人ID(管理员ID)
*/
private Long creator;
/**
* 评价人名称(关联查询)
*/
private String creatorName;
/**
* 评价人账号(关联查询,管理后台显示)
*/
private String creatorAccount;
/**
* 购买评分 1-5
*/
private Integer rating;
/**
* 文字评价内容
*/
private String content;
/**
* 有问题的机位ID列表
* 格式: [12345, 12346, 12347]
*/
private List<Long> problemDeviceIds;
/**
* 问题机位数量(方便前端展示)
*/
private Integer problemDeviceCount;
/**
* 问题标签列表
* 格式: ["画面模糊", "抖动严重", "色彩异常"]
*/
private List<String> problemTags;
/**
* 来源
* 固定值: ORDER(订单), RENDER(渲染)
*/
private String source;
/**
* 来源ID
* 用于溯源,关联订单ID或渲染任务ID等
*/
private Long sourceId;
/**
* 创建时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;
/**
* 更新时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date updateTime;
/**
* 操作时长(创建到更新的时间差,单位:秒)
*/
private Long operationDuration;
}

View File

@@ -2,7 +2,7 @@ package com.ycwl.basic.model.pc.videoreview.dto;
import lombok.Data; import lombok.Data;
import java.util.Map; import java.util.List;
/** /**
* 新增视频评价请求DTO * 新增视频评价请求DTO
@@ -26,9 +26,28 @@ public class VideoReviewAddReqDTO {
private String content; private String content;
/** /**
* 机位评价JSON(可选) * 有问题的机位ID列表(可选)
* 格式: {"12345": 5, "12346": 4} * 格式: [12345, 12346, 12347]
* key为机位ID,value为该机位的评分(1-5) * 选择有问题的机位ID
*/ */
private Map<String, Integer> cameraPositionRating; private List<Long> problemDeviceIds;
/**
* 问题标签列表(可选)
* 格式: ["画面模糊", "抖动严重", "色彩异常"]
* 可多选问题标签
*/
private List<String> problemTags;
/**
* 来源(必填)
* 固定值: ORDER(订单), RENDER(渲染)
*/
private String source;
/**
* 来源ID(可选)
* 用于溯源,关联订单ID或渲染任务ID等
*/
private Long sourceId;
} }

View File

@@ -53,6 +53,24 @@ public class VideoReviewListReqDTO {
*/ */
private String keyword; private String keyword;
/**
* 问题机位ID(可选,筛选包含该机位ID的评价)
* 任意一个问题机位匹配即可
*/
private Long problemDeviceId;
/**
* 问题标签(可选,筛选包含该标签的评价)
* 任意一个标签匹配即可
*/
private String problemTag;
/**
* 来源(可选,筛选指定来源的评价)
* 固定值: ORDER(订单), RENDER(渲染)
*/
private String source;
/** /**
* 页码(必填,默认1) * 页码(必填,默认1)
*/ */

View File

@@ -3,8 +3,9 @@ package com.ycwl.basic.model.pc.videoreview.dto;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data; import lombok.Data;
import java.math.BigDecimal;
import java.util.Date; import java.util.Date;
import java.util.Map; import java.util.List;
/** /**
* 视频评价详情响应DTO * 视频评价详情响应DTO
@@ -27,6 +28,16 @@ public class VideoReviewRespDTO {
*/ */
private String videoUrl; private String videoUrl;
/**
* 视频时长(秒,关联查询video表)
*/
private BigDecimal duration;
/**
* 任务参数(JSON字符串,关联查询task表)
*/
private String taskParams;
/** /**
* 模板ID(关联查询video表) * 模板ID(关联查询video表)
*/ */
@@ -68,11 +79,28 @@ public class VideoReviewRespDTO {
private String content; private String content;
/** /**
* 机位评价JSON * 有问题的机位ID列表
* 格式: {"12345": 5, "12346": 4} * 格式: [12345, 12346, 12347]
* key为机位ID,value为该机位的评分(1-5)
*/ */
private Map<String, Integer> cameraPositionRating; private List<Long> problemDeviceIds;
/**
* 问题标签列表
* 格式: ["画面模糊", "抖动严重", "色彩异常"]
*/
private List<String> problemTags;
/**
* 来源
* 固定值: ORDER(订单), RENDER(渲染)
*/
private String source;
/**
* 来源ID
* 用于溯源,关联订单ID或渲染任务ID等
*/
private Long sourceId;
/** /**
* 创建时间 * 创建时间

View File

@@ -40,10 +40,10 @@ public class VideoReviewStatisticsRespDTO {
private List<ScenicReviewRank> scenicRankList; private List<ScenicReviewRank> scenicRankList;
/** /**
* 机位评价统计 * 问题机位统计
* key: 机位ID, value: 该机位的平均评分 * key: 机位ID, value: 该机位被标记为问题的次数
*/ */
private Map<String, BigDecimal> cameraPositionAverage; private Map<Long, Long> problemDeviceStatistics;
/** /**
* 景区评价排行内部类 * 景区评价排行内部类

View File

@@ -4,12 +4,13 @@ import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import com.ycwl.basic.handler.MapTypeHandler; import com.ycwl.basic.handler.LongListTypeHandler;
import com.ycwl.basic.handler.StringListTypeHandler;
import lombok.Data; import lombok.Data;
import org.apache.ibatis.type.JdbcType; import org.apache.ibatis.type.JdbcType;
import java.util.Date; import java.util.Date;
import java.util.Map; import java.util.List;
/** /**
* 视频评价实体类 * 视频评价实体类
@@ -50,12 +51,32 @@ public class VideoReviewEntity {
private String content; private String content;
/** /**
* 机位评价JSON * 有问题的机位ID列表
* 格式: {"12345": 5, "12346": 4} * 格式: [12345, 12346, 12347]
* key为机位ID,value为该机位的评分(1-5) * 存储被标记为有问题的机位ID
*/ */
@TableField(typeHandler = MapTypeHandler.class, jdbcType = JdbcType.VARCHAR) @TableField(typeHandler = LongListTypeHandler.class, jdbcType = JdbcType.VARCHAR)
private Map<String, Integer> cameraPositionRating; private List<Long> problemDeviceIds;
/**
* 问题标签列表
* 格式: ["画面模糊", "抖动严重", "色彩异常"]
* 存储视频或机位的问题标签,可多选
*/
@TableField(typeHandler = StringListTypeHandler.class, jdbcType = JdbcType.VARCHAR)
private List<String> problemTags;
/**
* 来源
* 固定值: ORDER(订单), RENDER(渲染)
*/
private String source;
/**
* 来源ID
* 用于溯源,关联订单ID或渲染任务ID等
*/
private Long sourceId;
/** /**
* 创建时间 * 创建时间

View File

@@ -31,4 +31,11 @@ public class CreateVirtualOrderRequest {
* 打印图片URL(可选,如果提供则使用此URL进行打印) * 打印图片URL(可选,如果提供则使用此URL进行打印)
*/ */
private String printImgUrl; private String printImgUrl;
/**
* 是否需要实际支付(可选,默认false)
* false/null: 创建0元虚拟订单,立即完成购买
* true: 创建待支付订单(计算实际价格),由前端处理支付流程
*/
private Boolean needActualPayment;
} }

View File

@@ -0,0 +1,44 @@
package com.ycwl.basic.model.printer.req;
import lombok.Data;
import java.util.List;
/**
* 打印机大屏创建虚拟订单请求参数
* 通过 faceSampleIds 自动查找关联的照片素材进行下单
*/
@Data
public class TvCreateVirtualOrderRequest {
/**
* 人脸样本ID列表,系统自动查找这些样本关联的所有照片素材(type=2)
*/
private List<Long> faceSampleIds;
/**
* 景区ID
*/
private Long scenicId;
/**
* 打印机ID(可选)
*/
private Integer printerId;
/**
* 是否需要图像增强(可选,默认不增强)
*/
private Boolean needEnhance;
/**
* 打印图片URL(可选,如果提供则使用此URL进行打印)
*/
private String printImgUrl;
/**
* 是否需要实际支付(可选,默认false)
* false/null: 创建0元虚拟订单,立即完成购买
* true: 创建待支付订单(计算实际价格)
*/
private Boolean needActualPayment;
}

View File

@@ -0,0 +1,98 @@
package com.ycwl.basic.model.task.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
/**
* Task与RenderJob关联实体
* 用于跟踪task和zt-render-worker服务中渲染作业的关联
*/
@Data
@TableName("task_render_job_mapping")
public class TaskRenderJobMappingEntity {
@TableId(type = IdType.AUTO)
private Long id;
/**
* 任务ID(task表的id)
*/
private Long taskId;
/**
* 渲染作业ID(zt-render-worker返回的jobId)
*/
private Long renderJobId;
/**
* 渲染状态
* PENDING-等待中, PREVIEW_READY-预览就绪, COMPLETED-已完成, FAILED-失败
*/
private String renderStatus;
/**
* 已发布片段数
*/
private Integer publishedCount;
/**
* 总片段数
*/
private Integer segmentCount;
/**
* 预览播放地址(HLS)
*/
private String previewUrl;
/**
* 最终MP4地址
*/
private String mp4Url;
/**
* 错误码
*/
private String errorCode;
/**
* 错误信息
*/
private String errorMessage;
/**
* 重试次数
*/
private Integer retryCount;
/**
* 最后检查时间
*/
private Date lastCheckTime;
private Date createTime;
private Date updateTime;
/**
* 渲染状态常量
*/
public static final String STATUS_PENDING = "PENDING";
public static final String STATUS_PREVIEW_READY = "PREVIEW_READY";
public static final String STATUS_MP4_COMPOSING = "MP4_COMPOSING";
public static final String STATUS_COMPLETED = "COMPLETED";
public static final String STATUS_FAILED = "FAILED";
/**
* 预览就绪所需的最小已发布片段数
*/
public static final int MIN_PUBLISHED_FOR_PREVIEW = 3;
/**
* 最大重试次数
*/
public static final int MAX_RETRY_COUNT = 10;
}

View File

@@ -1,6 +1,7 @@
package com.ycwl.basic.pricing.controller; package com.ycwl.basic.pricing.controller;
import com.github.pagehelper.PageInfo; import com.github.pagehelper.PageInfo;
import com.ycwl.basic.constant.BaseContextHandler;
import com.ycwl.basic.pricing.dto.req.VoucherBatchCreateReq; import com.ycwl.basic.pricing.dto.req.VoucherBatchCreateReq;
import com.ycwl.basic.pricing.dto.req.VoucherBatchCreateReqV2; import com.ycwl.basic.pricing.dto.req.VoucherBatchCreateReqV2;
import com.ycwl.basic.pricing.dto.req.VoucherBatchQueryReq; import com.ycwl.basic.pricing.dto.req.VoucherBatchQueryReq;
@@ -96,8 +97,9 @@ public class VoucherManagementController {
} }
@GetMapping("/mobile/my-codes") @GetMapping("/mobile/my-codes")
public ApiResponse<List<VoucherCodeResp>> getMyVoucherCodes(@RequestParam Long faceId) { public ApiResponse<List<VoucherCodeResp>> getMyVoucherCodes() {
List<VoucherCodeResp> codes = voucherCodeService.getMyVoucherCodes(faceId); Long userId = Long.valueOf(BaseContextHandler.getUserId());
List<VoucherCodeResp> codes = voucherCodeService.getMyVoucherCodes(userId);
return ApiResponse.success(codes); return ApiResponse.success(codes);
} }
} }

View File

@@ -4,7 +4,5 @@ import lombok.Data;
@Data @Data
public class VoucherClaimReq { public class VoucherClaimReq {
private Long scenicId;
private Long faceId;
private String code; private String code;
} }

View File

@@ -9,7 +9,7 @@ import lombok.EqualsAndHashCode;
public class VoucherCodeQueryReq extends BaseQueryParameterReq { public class VoucherCodeQueryReq extends BaseQueryParameterReq {
private Long batchId; private Long batchId;
private Long scenicId; private Long scenicId;
private Long faceId; private Long userId;
private Integer status; private Integer status;
private String code; private String code;
} }

View File

@@ -7,6 +7,15 @@ import java.util.Date;
@Data @Data
public class VoucherCodeResp { public class VoucherCodeResp {
/**
* 领取是否成功
*/
private Boolean success;
/**
* 结果描述(失败时为原因说明)
*/
private String message;
private Long id; private Long id;
private Long batchId; private Long batchId;
private String batchName; private String batchName;
@@ -14,7 +23,7 @@ public class VoucherCodeResp {
private String code; private String code;
private Integer status; private Integer status;
private String statusName; private String statusName;
private Long faceId; private Long userId;
private Date claimedTime; private Date claimedTime;
private Date usedTime; private Date usedTime;
private String remark; private String remark;

View File

@@ -102,9 +102,9 @@ public class VoucherDetailResp {
@Data @Data
public static class UserInfo { public static class UserInfo {
/** /**
* 用户人脸ID * 用户ID
*/ */
private Long faceId; private Long userId;
/** /**
* 该用户已使用此券码的次数 * 该用户已使用此券码的次数

View File

@@ -39,9 +39,9 @@ public class PriceVoucherCode {
private Integer status; private Integer status;
/** /**
* 领取人faceId * 领取人用户ID
*/ */
private Long faceId; private Long userId;
/** /**
* 领取时间 * 领取时间

View File

@@ -61,11 +61,11 @@ public interface PriceCouponConfigMapper extends BaseMapper<PriceCouponConfig> {
*/ */
@Insert("INSERT INTO price_coupon_config (coupon_name, coupon_type, discount_value, min_amount, " + @Insert("INSERT INTO price_coupon_config (coupon_name, coupon_type, discount_value, min_amount, " +
"max_discount, applicable_products, required_attribute_keys, total_quantity, used_quantity, " + "max_discount, applicable_products, required_attribute_keys, total_quantity, used_quantity, " +
"claimed_quantity, user_claim_limit, valid_from, valid_until, " + "claimed_quantity, user_claim_limit, valid_from, valid_until, valid_days_after_claim, " +
"is_active, scenic_id, create_time, update_time) VALUES " + "is_active, scenic_id, create_time, update_time) VALUES " +
"(#{couponName}, #{couponType}, #{discountValue}, #{minAmount}, #{maxDiscount}, " + "(#{couponName}, #{couponType}, #{discountValue}, #{minAmount}, #{maxDiscount}, " +
"#{applicableProducts}, #{requiredAttributeKeys}, #{totalQuantity}, #{usedQuantity}, " + "#{applicableProducts}, #{requiredAttributeKeys}, #{totalQuantity}, #{usedQuantity}, " +
"#{claimedQuantity}, #{userClaimLimit}, #{validFrom}, #{validUntil}, " + "#{claimedQuantity}, #{userClaimLimit}, #{validFrom}, #{validUntil}, #{validDaysAfterClaim}, " +
"#{isActive}, #{scenicId}, NOW(), NOW())") "#{isActive}, #{scenicId}, NOW(), NOW())")
int insertCoupon(PriceCouponConfig coupon); int insertCoupon(PriceCouponConfig coupon);
@@ -76,7 +76,8 @@ public interface PriceCouponConfigMapper extends BaseMapper<PriceCouponConfig> {
"discount_value = #{discountValue}, min_amount = #{minAmount}, max_discount = #{maxDiscount}, " + "discount_value = #{discountValue}, min_amount = #{minAmount}, max_discount = #{maxDiscount}, " +
"applicable_products = #{applicableProducts}, required_attribute_keys = #{requiredAttributeKeys}, " + "applicable_products = #{applicableProducts}, required_attribute_keys = #{requiredAttributeKeys}, " +
"total_quantity = #{totalQuantity}, user_claim_limit = #{userClaimLimit}, " + "total_quantity = #{totalQuantity}, user_claim_limit = #{userClaimLimit}, " +
"valid_from = #{validFrom}, valid_until = #{validUntil}, is_active = #{isActive}, " + "valid_from = #{validFrom}, valid_until = #{validUntil}, valid_days_after_claim = #{validDaysAfterClaim}, " +
"is_active = #{isActive}, " +
"scenic_id = #{scenicId}, update_time = NOW() WHERE id = #{id}") "scenic_id = #{scenicId}, update_time = NOW() WHERE id = #{id}")
int updateCoupon(PriceCouponConfig coupon); int updateCoupon(PriceCouponConfig coupon);

View File

@@ -34,6 +34,12 @@ public interface PriceProductConfigMapper extends BaseMapper<PriceProductConfig>
@Select("SELECT * FROM price_product_config WHERE product_type = #{productType} AND product_id = #{productId} AND is_active = 1") @Select("SELECT * FROM price_product_config WHERE product_type = #{productType} AND product_id = #{productId} AND is_active = 1")
PriceProductConfig selectByProductTypeAndId(String productType, String productId); PriceProductConfig selectByProductTypeAndId(String productType, String productId);
/**
* 根据商品类型和商品ID查询全局配置(排除景区级配置)
*/
@Select("SELECT * FROM price_product_config WHERE product_type = #{productType} AND product_id = #{productId} AND (scenic_id IS NULL OR scenic_id = '') AND is_active = 1")
PriceProductConfig selectGlobalByProductTypeAndId(String productType, String productId);
/** /**
* 根据商品类型、商品ID和景区ID查询配置(支持景区维度) * 根据商品类型、商品ID和景区ID查询配置(支持景区维度)
*/ */

View File

@@ -27,6 +27,17 @@ public interface PriceTierConfigMapper extends BaseMapper<PriceTierConfig> {
@Param("productId") String productId, @Param("productId") String productId,
@Param("quantity") Integer quantity); @Param("quantity") Integer quantity);
/**
* 根据商品类型、商品ID和数量查询全局匹配的阶梯价格(排除景区级配置)
*/
@Select("SELECT * FROM price_tier_config WHERE product_type = #{productType} " +
"AND product_id = #{productId} AND (scenic_id IS NULL OR scenic_id = '') " +
"AND #{quantity} >= min_quantity AND #{quantity} <= max_quantity " +
"AND is_active = 1 ORDER BY sort_order ASC LIMIT 1")
PriceTierConfig selectGlobalByProductTypeAndQuantity(@Param("productType") String productType,
@Param("productId") String productId,
@Param("quantity") Integer quantity);
/** /**
* 根据商品类型、商品ID、数量和景区ID查询匹配的阶梯价格(支持景区维度) * 根据商品类型、商品ID、数量和景区ID查询匹配的阶梯价格(支持景区维度)
*/ */

View File

@@ -21,31 +21,31 @@ public interface PriceVoucherCodeMapper extends BaseMapper<PriceVoucherCode> {
* @param code 券码 * @param code 券码
* @return 券码信息 * @return 券码信息
*/ */
@Select("SELECT id, batch_id, scenic_id, code, status, face_id, claimed_time, used_time, " + @Select("SELECT id, batch_id, scenic_id, code, status, user_id, claimed_time, used_time, " +
"current_use_count, last_used_time, remark, create_time, update_time, deleted, deleted_at " + "current_use_count, last_used_time, remark, create_time, update_time, deleted, deleted_at " +
"FROM price_voucher_code WHERE code = #{code} AND deleted = 0 LIMIT 1") "FROM price_voucher_code WHERE code = #{code} AND deleted = 0 LIMIT 1")
PriceVoucherCode selectByCode(@Param("code") String code); PriceVoucherCode selectByCode(@Param("code") String code);
/** /**
* 根据faceId和scenicId统计已领取的券码数量 * 根据userId和scenicId统计已领取的券码数量
* @param faceId 用户faceId * @param userId 用户ID
* @param scenicId 景区ID * @param scenicId 景区ID
* @return 数量 * @return 数量
*/ */
@Select("SELECT COUNT(1) FROM price_voucher_code WHERE face_id = #{faceId} AND scenic_id = #{scenicId} AND deleted = 0") @Select("SELECT COUNT(1) FROM price_voucher_code WHERE user_id = #{userId} AND scenic_id = #{scenicId} AND deleted = 0")
Integer countByFaceIdAndScenicId(@Param("faceId") Long faceId, @Param("scenicId") Long scenicId); Integer countByUserIdAndScenicId(@Param("userId") Long userId, @Param("scenicId") Long scenicId);
/** /**
* 查询用户在指定景区的可用券码列表 * 查询用户在指定景区的可用券码列表
* @param faceId 用户faceId * @param userId 用户ID
* @param scenicId 景区ID * @param scenicId 景区ID
* @return 券码列表 * @return 券码列表
*/ */
@Select("SELECT id, batch_id, scenic_id, code, status, face_id, claimed_time, used_time, " + @Select("SELECT id, batch_id, scenic_id, code, status, user_id, claimed_time, used_time, " +
"current_use_count, last_used_time, remark, create_time, update_time, deleted, deleted_at " + "current_use_count, last_used_time, remark, create_time, update_time, deleted, deleted_at " +
"FROM price_voucher_code WHERE face_id = #{faceId} AND scenic_id = #{scenicId} AND status = 1 AND deleted = 0 " + "FROM price_voucher_code WHERE user_id = #{userId} AND scenic_id = #{scenicId} AND status = 1 AND deleted = 0 " +
"ORDER BY claimed_time DESC") "ORDER BY claimed_time DESC")
List<PriceVoucherCode> selectAvailableVouchersByFaceIdAndScenicId(@Param("faceId") Long faceId, List<PriceVoucherCode> selectAvailableVouchersByUserIdAndScenicId(@Param("userId") Long userId,
@Param("scenicId") Long scenicId); @Param("scenicId") Long scenicId);
/** /**
@@ -54,7 +54,7 @@ public interface PriceVoucherCodeMapper extends BaseMapper<PriceVoucherCode> {
* @param limit 限制数量 * @param limit 限制数量
* @return 券码列表 * @return 券码列表
*/ */
@Select("SELECT id, batch_id, scenic_id, code, status, face_id, claimed_time, used_time, " + @Select("SELECT id, batch_id, scenic_id, code, status, user_id, claimed_time, used_time, " +
"current_use_count, last_used_time, remark, create_time, update_time, deleted, deleted_at " + "current_use_count, last_used_time, remark, create_time, update_time, deleted, deleted_at " +
"FROM price_voucher_code WHERE batch_id = #{batchId} AND status = 0 AND deleted = 0 LIMIT #{limit}") "FROM price_voucher_code WHERE batch_id = #{batchId} AND status = 0 AND deleted = 0 LIMIT #{limit}")
List<PriceVoucherCode> selectUnclaimedVouchersByBatchId(@Param("batchId") Long batchId, List<PriceVoucherCode> selectUnclaimedVouchersByBatchId(@Param("batchId") Long batchId,
@@ -63,14 +63,14 @@ public interface PriceVoucherCodeMapper extends BaseMapper<PriceVoucherCode> {
/** /**
* 领取券码(更新状态为已领取) * 领取券码(更新状态为已领取)
* @param id 券码ID * @param id 券码ID
* @param faceId 用户faceId * @param userId 用户ID
* @param claimedTime 领取时间 * @param claimedTime 领取时间
* @return 影响行数 * @return 影响行数
*/ */
@Update("UPDATE price_voucher_code SET status = 1, face_id = #{faceId}, claimed_time = #{claimedTime}, " + @Update("UPDATE price_voucher_code SET status = 1, user_id = #{userId}, claimed_time = #{claimedTime}, " +
"update_time = NOW() WHERE id = #{id} AND status = 0 AND deleted = 0") "update_time = NOW() WHERE id = #{id} AND status = 0 AND deleted = 0")
int claimVoucher(@Param("id") Long id, int claimVoucher(@Param("id") Long id,
@Param("faceId") Long faceId, @Param("userId") Long userId,
@Param("claimedTime") LocalDateTime claimedTime); @Param("claimedTime") LocalDateTime claimedTime);
/** /**
@@ -78,25 +78,25 @@ public interface PriceVoucherCodeMapper extends BaseMapper<PriceVoucherCode> {
* @param batchId 批次ID * @param batchId 批次ID
* @return 券码列表 * @return 券码列表
*/ */
@Select("SELECT id, batch_id, scenic_id, code, status, face_id, claimed_time, used_time, " + @Select("SELECT id, batch_id, scenic_id, code, status, user_id, claimed_time, used_time, " +
"current_use_count, last_used_time, remark, create_time, update_time, deleted, deleted_at " + "current_use_count, last_used_time, remark, create_time, update_time, deleted, deleted_at " +
"FROM price_voucher_code WHERE batch_id = #{batchId} AND deleted = 0 ORDER BY create_time DESC") "FROM price_voucher_code WHERE batch_id = #{batchId} AND deleted = 0 ORDER BY create_time DESC")
List<PriceVoucherCode> selectByBatchId(@Param("batchId") Long batchId); List<PriceVoucherCode> selectByBatchId(@Param("batchId") Long batchId);
/** /**
* 查询用户的券码列表 * 查询用户的券码列表
* @param faceId 用户faceId * @param userId 用户ID
* @param scenicId 景区ID(可选) * @param scenicId 景区ID(可选)
* @return 券码列表 * @return 券码列表
*/ */
@Select("<script>" + @Select("<script>" +
"SELECT id, batch_id, scenic_id, code, status, face_id, claimed_time, used_time, " + "SELECT id, batch_id, scenic_id, code, status, user_id, claimed_time, used_time, " +
"current_use_count, last_used_time, remark, create_time, update_time, deleted, deleted_at " + "current_use_count, last_used_time, remark, create_time, update_time, deleted, deleted_at " +
"FROM price_voucher_code WHERE face_id = #{faceId}" + "FROM price_voucher_code WHERE user_id = #{userId}" +
"<if test='scenicId != null'> AND scenic_id = #{scenicId}</if>" + "<if test='scenicId != null'> AND scenic_id = #{scenicId}</if>" +
" AND deleted = 0 ORDER BY claimed_time DESC" + " AND deleted = 0 ORDER BY claimed_time DESC" +
"</script>") "</script>")
List<PriceVoucherCode> selectUserVouchers(@Param("faceId") Long faceId, List<PriceVoucherCode> selectUserVouchers(@Param("userId") Long userId,
@Param("scenicId") Long scenicId); @Param("scenicId") Long scenicId);
/** /**
@@ -104,7 +104,7 @@ public interface PriceVoucherCodeMapper extends BaseMapper<PriceVoucherCode> {
* @param batchId 批次ID * @param batchId 批次ID
* @return 可用券码 * @return 可用券码
*/ */
@Select("SELECT id, batch_id, scenic_id, code, status, face_id, claimed_time, used_time, " + @Select("SELECT id, batch_id, scenic_id, code, status, user_id, claimed_time, used_time, " +
"current_use_count, last_used_time, remark, create_time, update_time, deleted, deleted_at " + "current_use_count, last_used_time, remark, create_time, update_time, deleted, deleted_at " +
"FROM price_voucher_code WHERE batch_id = #{batchId} AND status = 0 AND deleted = 0 LIMIT 1") "FROM price_voucher_code WHERE batch_id = #{batchId} AND status = 0 AND deleted = 0 LIMIT 1")
PriceVoucherCode findFirstAvailableByBatchId(@Param("batchId") Long batchId); PriceVoucherCode findFirstAvailableByBatchId(@Param("batchId") Long batchId);
@@ -114,7 +114,7 @@ public interface PriceVoucherCodeMapper extends BaseMapper<PriceVoucherCode> {
* @param scenicId 景区ID * @param scenicId 景区ID
* @return 可用券码 * @return 可用券码
*/ */
@Select("SELECT pvc.id, pvc.batch_id, pvc.scenic_id, pvc.code, pvc.status, pvc.face_id, pvc.claimed_time, pvc.used_time, " + @Select("SELECT pvc.id, pvc.batch_id, pvc.scenic_id, pvc.code, pvc.status, pvc.user_id, pvc.claimed_time, pvc.used_time, " +
"pvc.current_use_count, pvc.last_used_time, pvc.remark, pvc.create_time, pvc.update_time, pvc.deleted, pvc.deleted_at " + "pvc.current_use_count, pvc.last_used_time, pvc.remark, pvc.create_time, pvc.update_time, pvc.deleted, pvc.deleted_at " +
"FROM price_voucher_code pvc WHERE pvc.scenic_id = #{scenicId} AND pvc.status = 0 AND pvc.deleted = 0 " + "FROM price_voucher_code pvc WHERE pvc.scenic_id = #{scenicId} AND pvc.status = 0 AND pvc.deleted = 0 " +
"AND NOT EXISTS (SELECT 1 FROM voucher_print_record vpr WHERE vpr.voucher_code_id = pvc.id AND vpr.deleted = 0) " + "AND NOT EXISTS (SELECT 1 FROM voucher_print_record vpr WHERE vpr.voucher_code_id = pvc.id AND vpr.deleted = 0) " +

View File

@@ -15,9 +15,9 @@ public interface VoucherCodeService {
PageInfo<VoucherCodeResp> queryCodeList(VoucherCodeQueryReq req); PageInfo<VoucherCodeResp> queryCodeList(VoucherCodeQueryReq req);
List<VoucherCodeResp> getMyVoucherCodes(Long faceId); List<VoucherCodeResp> getMyVoucherCodes(Long userId);
void markCodeAsUsed(Long codeId, String remark); void markCodeAsUsed(Long codeId, String remark);
boolean canClaimVoucher(Long faceId, Long scenicId); boolean canClaimVoucher(Long userId, Long scenicId);
} }

View File

@@ -224,6 +224,18 @@ public class CouponServiceImpl implements ICouponService {
@Override @Override
@Transactional @Transactional
public CouponUseResult useCoupon(CouponUseRequest request) { public CouponUseResult useCoupon(CouponUseRequest request) {
Date now = new Date();
PriceCouponConfig coupon = couponConfigMapper.selectById(request.getCouponId());
if (coupon == null || coupon.getDeleted() == 1 || !Boolean.TRUE.equals(coupon.getIsActive())) {
throw new CouponInvalidException("优惠券不存在或已失效");
}
if (coupon.getValidFrom() != null && now.before(coupon.getValidFrom())) {
throw new CouponInvalidException("优惠券尚未生效");
}
if (coupon.getValidUntil() != null && !now.before(coupon.getValidUntil())) {
throw new CouponInvalidException("优惠券已过期");
}
List<PriceCouponClaimRecord> records = couponClaimRecordMapper.selectUserCouponRecords( List<PriceCouponClaimRecord> records = couponClaimRecordMapper.selectUserCouponRecords(
request.getUserId(), request.getCouponId()); request.getUserId(), request.getCouponId());
@@ -234,10 +246,18 @@ public class CouponServiceImpl implements ICouponService {
// 查找一张可用的优惠券(状态为CLAIMED) // 查找一张可用的优惠券(状态为CLAIMED)
PriceCouponClaimRecord record = records.stream() PriceCouponClaimRecord record = records.stream()
.filter(r -> r.getStatus() == CouponStatus.CLAIMED) .filter(r -> r.getStatus() == CouponStatus.CLAIMED)
.filter(r -> r.getExpireTime() == null || r.getExpireTime().after(now))
.findFirst() .findFirst()
.orElse(null); .orElse(null);
if (record == null) { if (record == null) {
boolean hasClaimedButExpired = records.stream()
.anyMatch(r -> r.getStatus() == CouponStatus.CLAIMED
&& r.getExpireTime() != null
&& !r.getExpireTime().after(now));
if (hasClaimedButExpired) {
throw new CouponInvalidException("优惠券已过期");
}
// 如果没有可用的,抛出异常。为了错误信息准确,可以检查最后一张的状态 // 如果没有可用的,抛出异常。为了错误信息准确,可以检查最后一张的状态
CouponStatus lastStatus = records.getFirst().getStatus(); CouponStatus lastStatus = records.getFirst().getStatus();
throw new CouponInvalidException("优惠券状态无效: " + lastStatus); throw new CouponInvalidException("优惠券状态无效: " + lastStatus);

View File

@@ -401,29 +401,10 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
log.warn("未找到具体商品配置: productType={}, productId={}, scenicId={}, 尝试使用通用配置", log.warn("未找到具体商品配置: productType={}, productId={}, scenicId={}, 尝试使用通用配置",
productType, productId, scenicId); productType, productId, scenicId);
// 兜底:使用default配置(带景区ID)
try {
PriceProductConfig defaultConfig = productConfigService.getProductConfig(productType.getCode(), "default", scenicId);
if (defaultConfig != null) {
actualPrice = defaultConfig.getBasePrice();
originalPrice = defaultConfig.getOriginalPrice();
if (isQuantityBasedPricing(capability)) {
actualPrice = actualPrice.multiply(BigDecimal.valueOf(product.getQuantity()));
if (originalPrice != null) {
originalPrice = originalPrice.multiply(BigDecimal.valueOf(product.getQuantity()));
}
}
} else {
throw new PriceCalculationException("无法找到default配置");
}
} catch (Exception defaultEx) {
log.warn("未找到default配置: productType={}, scenicId={}", productType.getCode(), scenicId);
// 最后兜底:使用通用配置(向后兼容) // 最后兜底:使用通用配置(向后兼容)
List<PriceProductConfig> configs = productConfigService.getProductConfig(productType.getCode()); List<PriceProductConfig> configs = productConfigService.getProductConfig(productType.getCode());
if (!configs.isEmpty()) { if (!configs.isEmpty()) {
PriceProductConfig baseConfig = configs.getFirst(); // 使用第一个配置作为默认 PriceProductConfig baseConfig = configs.getFirst();
actualPrice = baseConfig.getBasePrice(); actualPrice = baseConfig.getBasePrice();
originalPrice = baseConfig.getOriginalPrice(); originalPrice = baseConfig.getOriginalPrice();
@@ -438,7 +419,6 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
} }
} }
} }
}
return new ProductPriceInfo(actualPrice, originalPrice); return new ProductPriceInfo(actualPrice, originalPrice);
} }

View File

@@ -76,29 +76,41 @@ public class ProductConfigServiceImpl implements IProductConfigService {
return getProductConfig(productType, productId); return getProductConfig(productType, productId);
} }
String scenicIdStr = scenicId.toString();
// 查询优先级: // 查询优先级:
// 1. 景区+商品ID // 1. 景区+商品ID
PriceProductConfig config = productConfigMapper.selectByProductTypeIdAndScenic( PriceProductConfig config = productConfigMapper.selectByProductTypeIdAndScenic(
productType, productId, scenicId.toString()); productType, productId, scenicIdStr);
if (config != null) { if (config != null) {
log.debug("使用景区特定商品配置: productType={}, productId={}, scenicId={}", log.debug("使用景区特定商品配置: productType={}, productId={}, scenicId={}",
productType, productId, scenicId); productType, productId, scenicId);
return config; return config;
} }
// 2. 景区+默认 // 2. 景区+景区ID作为商品ID(productId未命中时回退)
if (!scenicIdStr.equals(productId)) {
config = productConfigMapper.selectByProductTypeIdAndScenic(
productType, scenicIdStr, scenicIdStr);
if (config != null) {
log.debug("使用景区ID作为商品ID的配置: productType={}, scenicId={}", productType, scenicId);
return config;
}
}
// 3. 景区+默认
if (!"default".equals(productId)) { if (!"default".equals(productId)) {
config = productConfigMapper.selectByProductTypeIdAndScenic( config = productConfigMapper.selectByProductTypeIdAndScenic(
productType, "default", scenicId.toString()); productType, "default", scenicIdStr);
if (config != null) { if (config != null) {
log.debug("使用景区默认配置: productType={}, scenicId={}", productType, scenicId); log.debug("使用景区默认配置: productType={}, scenicId={}", productType, scenicId);
return config; return config;
} }
} }
// 3. 全局+商品ID (兜底) // 4. 全局+商品ID (兜底)
try { try {
config = productConfigMapper.selectByProductTypeAndId(productType, productId); config = productConfigMapper.selectGlobalByProductTypeAndId(productType, productId);
if (config != null) { if (config != null) {
log.debug("使用全局商品配置: productType={}, productId={}", productType, productId); log.debug("使用全局商品配置: productType={}, productId={}", productType, productId);
return config; return config;
@@ -107,8 +119,8 @@ public class ProductConfigServiceImpl implements IProductConfigService {
log.debug("全局商品配置未找到: productType={}, productId={}", productType, productId); log.debug("全局商品配置未找到: productType={}, productId={}", productType, productId);
} }
// 4. 全局+默认 (最后兜底) // 5. 全局+默认 (最后兜底)
config = productConfigMapper.selectByProductTypeAndId(productType, "default"); config = productConfigMapper.selectGlobalByProductTypeAndId(productType, "default");
if (config != null) { if (config != null) {
log.debug("使用全局默认配置: productType={}", productType); log.debug("使用全局默认配置: productType={}", productType);
return config; return config;
@@ -130,20 +142,33 @@ public class ProductConfigServiceImpl implements IProductConfigService {
return getTierConfig(productType, productId, quantity); return getTierConfig(productType, productId, quantity);
} }
String scenicIdStr = scenicId.toString();
// 查询优先级: // 查询优先级:
// 1. 景区+商品ID // 1. 景区+商品ID
PriceTierConfig config = tierConfigMapper.selectByProductTypeQuantityAndScenic( PriceTierConfig config = tierConfigMapper.selectByProductTypeQuantityAndScenic(
productType, productId, quantity, scenicId.toString()); productType, productId, quantity, scenicIdStr);
if (config != null) { if (config != null) {
log.debug("使用景区特定阶梯定价: productType={}, productId={}, quantity={}, scenicId={}", log.debug("使用景区特定阶梯定价: productType={}, productId={}, quantity={}, scenicId={}",
productType, productId, quantity, scenicId); productType, productId, quantity, scenicId);
return config; return config;
} }
// 2. 景区+默认 // 2. 景区+景区ID作为商品ID(productId未命中时回退)
if (!scenicIdStr.equals(productId)) {
config = tierConfigMapper.selectByProductTypeQuantityAndScenic(
productType, scenicIdStr, quantity, scenicIdStr);
if (config != null) {
log.debug("使用景区ID作为商品ID的阶梯定价: productType={}, quantity={}, scenicId={}",
productType, quantity, scenicId);
return config;
}
}
// 3. 景区+默认
if (!"default".equals(productId)) { if (!"default".equals(productId)) {
config = tierConfigMapper.selectByProductTypeQuantityAndScenic( config = tierConfigMapper.selectByProductTypeQuantityAndScenic(
productType, "default", quantity, scenicId.toString()); productType, "default", quantity, scenicIdStr);
if (config != null) { if (config != null) {
log.debug("使用景区默认阶梯定价: productType={}, quantity={}, scenicId={}", log.debug("使用景区默认阶梯定价: productType={}, quantity={}, scenicId={}",
productType, quantity, scenicId); productType, quantity, scenicId);
@@ -151,16 +176,16 @@ public class ProductConfigServiceImpl implements IProductConfigService {
} }
} }
// 3. 全局+商品ID (兜底) // 4. 全局+商品ID (兜底)
config = tierConfigMapper.selectByProductTypeAndQuantity(productType, productId, quantity); config = tierConfigMapper.selectGlobalByProductTypeAndQuantity(productType, productId, quantity);
if (config != null) { if (config != null) {
log.debug("使用全局阶梯定价: productType={}, productId={}, quantity={}", log.debug("使用全局阶梯定价: productType={}, productId={}, quantity={}",
productType, productId, quantity); productType, productId, quantity);
return config; return config;
} }
// 4. 全局+默认 (最后兜底) // 5. 全局+默认 (最后兜底)
config = tierConfigMapper.selectByProductTypeAndQuantity(productType, "default", quantity); config = tierConfigMapper.selectGlobalByProductTypeAndQuantity(productType, "default", quantity);
if (config != null) { if (config != null) {
log.debug("使用全局默认阶梯定价: productType={}, quantity={}", productType, quantity); log.debug("使用全局默认阶梯定价: productType={}, quantity={}", productType, quantity);
} }

View File

@@ -3,6 +3,7 @@ package com.ycwl.basic.pricing.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo; import com.github.pagehelper.PageInfo;
import com.ycwl.basic.constant.BaseContextHandler;
import com.ycwl.basic.exception.BizException; import com.ycwl.basic.exception.BizException;
import com.ycwl.basic.pricing.dto.req.VoucherClaimReq; import com.ycwl.basic.pricing.dto.req.VoucherClaimReq;
import com.ycwl.basic.pricing.dto.req.VoucherCodeQueryReq; import com.ycwl.basic.pricing.dto.req.VoucherCodeQueryReq;
@@ -73,43 +74,49 @@ public void generateVoucherCodes(Long batchId, Long scenicId, Integer count) {
@Override @Override
@Transactional @Transactional
public VoucherCodeResp claimVoucher(VoucherClaimReq req) { public VoucherCodeResp claimVoucher(VoucherClaimReq req) {
if (req.getScenicId() == null) {
throw new BizException(400, "景区ID不能为空");
}
if (req.getFaceId() == null) {
throw new BizException(400, "用户faceId不能为空");
}
if (!StringUtils.hasText(req.getCode())) { if (!StringUtils.hasText(req.getCode())) {
throw new BizException(400, "券码不能为空"); throw new BizException(400, "券码不能为空");
} }
// 验证券码是否存在且未被领取 Long userId = Long.valueOf(BaseContextHandler.getUserId());
// 查询券码
LambdaQueryWrapper<PriceVoucherCode> wrapper = new LambdaQueryWrapper<>(); LambdaQueryWrapper<PriceVoucherCode> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(PriceVoucherCode::getCode, req.getCode()) wrapper.eq(PriceVoucherCode::getCode, req.getCode())
.eq(PriceVoucherCode::getScenicId, req.getScenicId())
.eq(PriceVoucherCode::getDeleted, 0); .eq(PriceVoucherCode::getDeleted, 0);
PriceVoucherCode voucherCode = voucherCodeMapper.selectOne(wrapper); PriceVoucherCode voucherCode = voucherCodeMapper.selectOne(wrapper);
if (voucherCode == null) { if (voucherCode == null) {
throw new BizException(400, "券码不存在或不属于该景区"); throw new BizException(400, "券码不存在");
} }
if (!Objects.equals(voucherCode.getStatus(), VoucherCodeStatus.UNCLAIMED.getCode())) { // 查询批次信息,用于构建响应
throw new BizException(400, "券码已被领取或已使用");
}
if (!canClaimVoucher(req.getFaceId(), req.getScenicId())) {
throw new BizException(400, "该用户在此景区已领取过券码");
}
// 获取券码所属批次
PriceVoucherBatchConfig batch = voucherBatchMapper.selectById(voucherCode.getBatchId()); PriceVoucherBatchConfig batch = voucherBatchMapper.selectById(voucherCode.getBatchId());
// 券码已找到,后续校验失败时仍返回 scenicId 等信息
if (!Objects.equals(voucherCode.getStatus(), VoucherCodeStatus.UNCLAIMED.getCode())) {
VoucherCodeResp resp = convertToResp(voucherCode, batch);
resp.setSuccess(false);
resp.setMessage("券码已被领取或已使用");
return resp;
}
if (!canClaimVoucher(userId, voucherCode.getScenicId())) {
VoucherCodeResp resp = convertToResp(voucherCode, batch);
resp.setSuccess(false);
resp.setMessage("该用户在此景区已领取过券码");
return resp;
}
if (batch == null || batch.getDeleted() == 1) { if (batch == null || batch.getDeleted() == 1) {
throw new BizException(400, "券码批次不存在"); VoucherCodeResp resp = convertToResp(voucherCode, batch);
resp.setSuccess(false);
resp.setMessage("券码批次不存在");
return resp;
} }
// 更新券码状态 // 更新券码状态
voucherCode.setFaceId(req.getFaceId()); voucherCode.setUserId(userId);
voucherCode.setStatus(VoucherCodeStatus.CLAIMED_UNUSED.getCode()); voucherCode.setStatus(VoucherCodeStatus.CLAIMED_UNUSED.getCode());
voucherCode.setClaimedTime(new Date()); voucherCode.setClaimedTime(new Date());
// 确保currentUseCount被初始化 // 确保currentUseCount被初始化
@@ -121,7 +128,10 @@ public VoucherCodeResp claimVoucher(VoucherClaimReq req) {
voucherBatchService.updateBatchClaimedCount(batch.getId()); voucherBatchService.updateBatchClaimedCount(batch.getId());
return convertToResp(voucherCode, batch); VoucherCodeResp resp = convertToResp(voucherCode, batch);
resp.setSuccess(true);
resp.setMessage("领取成功");
return resp;
} }
@Override @Override
@@ -132,7 +142,7 @@ public VoucherCodeResp claimVoucher(VoucherClaimReq req) {
wrapper.eq(PriceVoucherCode::getDeleted, 0) wrapper.eq(PriceVoucherCode::getDeleted, 0)
.eq(req.getBatchId() != null, PriceVoucherCode::getBatchId, req.getBatchId()) .eq(req.getBatchId() != null, PriceVoucherCode::getBatchId, req.getBatchId())
.eq(req.getScenicId() != null, PriceVoucherCode::getScenicId, req.getScenicId()) .eq(req.getScenicId() != null, PriceVoucherCode::getScenicId, req.getScenicId())
.eq(req.getFaceId() != null, PriceVoucherCode::getFaceId, req.getFaceId()) .eq(req.getUserId() != null, PriceVoucherCode::getUserId, req.getUserId())
.eq(req.getStatus() != null, PriceVoucherCode::getStatus, req.getStatus()) .eq(req.getStatus() != null, PriceVoucherCode::getStatus, req.getStatus())
.like(StringUtils.hasText(req.getCode()), PriceVoucherCode::getCode, req.getCode()) .like(StringUtils.hasText(req.getCode()), PriceVoucherCode::getCode, req.getCode())
.orderByDesc(PriceVoucherCode::getId); .orderByDesc(PriceVoucherCode::getId);
@@ -149,9 +159,9 @@ public VoucherCodeResp claimVoucher(VoucherClaimReq req) {
} }
@Override @Override
public List<VoucherCodeResp> getMyVoucherCodes(Long faceId) { public List<VoucherCodeResp> getMyVoucherCodes(Long userId) {
LambdaQueryWrapper<PriceVoucherCode> wrapper = new LambdaQueryWrapper<>(); LambdaQueryWrapper<PriceVoucherCode> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(PriceVoucherCode::getFaceId, faceId) wrapper.eq(PriceVoucherCode::getUserId, userId)
.eq(PriceVoucherCode::getDeleted, 0) .eq(PriceVoucherCode::getDeleted, 0)
.orderByDesc(PriceVoucherCode::getClaimedTime); .orderByDesc(PriceVoucherCode::getClaimedTime);
@@ -193,8 +203,8 @@ public void markCodeAsUsed(Long codeId, String remark) {
} }
@Override @Override
public boolean canClaimVoucher(Long faceId, Long scenicId) { public boolean canClaimVoucher(Long userId, Long scenicId) {
Integer count = voucherCodeMapper.countByFaceIdAndScenicId(faceId, scenicId); Integer count = voucherCodeMapper.countByUserIdAndScenicId(userId, scenicId);
return count == 0; return count == 0;
} }

View File

@@ -132,7 +132,7 @@ public class VoucherServiceImpl implements IVoucherService {
if (faceId == null) { if (faceId == null) {
voucherInfo.setAvailable(false); voucherInfo.setAvailable(false);
voucherInfo.setUnavailableReason("用户信息缺失,无法验证券码权限"); voucherInfo.setUnavailableReason("用户信息缺失,无法验证券码权限");
} else if (!faceId.equals(voucherCodeEntity.getFaceId())) { } else if (!faceId.equals(voucherCodeEntity.getUserId())) {
voucherInfo.setAvailable(false); voucherInfo.setAvailable(false);
voucherInfo.setUnavailableReason("券码已被其他用户领取"); voucherInfo.setUnavailableReason("券码已被其他用户领取");
} else { } else {
@@ -176,7 +176,7 @@ public class VoucherServiceImpl implements IVoucherService {
return new ArrayList<>(); return new ArrayList<>();
} }
List<PriceVoucherCode> voucherCodes = voucherCodeMapper.selectAvailableVouchersByFaceIdAndScenicId(faceId, scenicId); List<PriceVoucherCode> voucherCodes = voucherCodeMapper.selectAvailableVouchersByUserIdAndScenicId(faceId, scenicId);
List<VoucherInfo> voucherInfos = new ArrayList<>(); List<VoucherInfo> voucherInfos = new ArrayList<>();
for (PriceVoucherCode voucherCode : voucherCodes) { for (PriceVoucherCode voucherCode : voucherCodes) {
@@ -234,7 +234,7 @@ public void markVoucherAsUsed(String voucherCode, String remark, String orderId,
PriceVoucherUsageRecord usageRecord = new PriceVoucherUsageRecord(); PriceVoucherUsageRecord usageRecord = new PriceVoucherUsageRecord();
usageRecord.setVoucherCodeId(voucherCodeEntity.getId()); usageRecord.setVoucherCodeId(voucherCodeEntity.getId());
usageRecord.setVoucherCode(voucherCode); usageRecord.setVoucherCode(voucherCode);
usageRecord.setFaceId(faceId != null ? faceId : voucherCodeEntity.getFaceId()); usageRecord.setFaceId(faceId != null ? faceId : voucherCodeEntity.getUserId());
usageRecord.setScenicId(voucherCodeEntity.getScenicId()); usageRecord.setScenicId(voucherCodeEntity.getScenicId());
usageRecord.setBatchId(voucherCodeEntity.getBatchId()); usageRecord.setBatchId(voucherCodeEntity.getBatchId());
usageRecord.setUsageSequence(newUseCount); // 设置使用序号,表示这是该券码的第几次使用 usageRecord.setUsageSequence(newUseCount); // 设置使用序号,表示这是该券码的第几次使用
@@ -279,7 +279,7 @@ public void markVoucherAsUsed(String voucherCode, String remark, String orderId,
return false; return false;
} }
Integer count = voucherCodeMapper.countByFaceIdAndScenicId(faceId, scenicId); Integer count = voucherCodeMapper.countByUserIdAndScenicId(faceId, scenicId);
return count == 0; return count == 0;
} }
@@ -435,7 +435,7 @@ public void markVoucherAsUsed(String voucherCode, String remark, String orderId,
// 设置用户信息 // 设置用户信息
if (faceId != null) { if (faceId != null) {
VoucherDetailResp.UserInfo userInfo = new VoucherDetailResp.UserInfo(); VoucherDetailResp.UserInfo userInfo = new VoucherDetailResp.UserInfo();
userInfo.setFaceId(faceId); userInfo.setUserId(faceId);
// 计算该用户使用此券码的次数 // 计算该用户使用此券码的次数
List<PriceVoucherUsageRecord> userUsageRecords = usageRecordMapper.selectByVoucherCodeAndFaceId(voucherCodeEntity.getId(), faceId); List<PriceVoucherUsageRecord> userUsageRecords = usageRecordMapper.selectByVoucherCodeAndFaceId(voucherCodeEntity.getId(), faceId);

View File

@@ -141,6 +141,8 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
template.getId(), contentHash, resolvedScenicId template.getId(), contentHash, resolvedScenicId
); );
if (duplicateRecord != null) { if (duplicateRecord != null) {
if (duplicateRecord.getStatus() == 1) {
// 已有成功记录,直接复用
long duration = System.currentTimeMillis() - startTime; long duration = System.currentTimeMillis() - startTime;
log.info("检测到重复内容,复用历史记录: recordId={}, imageUrl={}, duration={}ms", log.info("检测到重复内容,复用历史记录: recordId={}, imageUrl={}, duration={}ms",
duplicateRecord.getId(), duplicateRecord.getResultImageUrl(), duration); duplicateRecord.getId(), duplicateRecord.getResultImageUrl(), duration);
@@ -158,6 +160,31 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
true, true,
duplicateRecord.getId() duplicateRecord.getId()
); );
} else if (duplicateRecord.getStatus() == 0) {
// 相同内容正在生成中,等待完成后复用
log.info("检测到相同内容正在生成中,等待完成: recordId={}", duplicateRecord.getId());
PuzzleGenerationRecordEntity completedRecord = waitForRecordCompletion(duplicateRecord.getId(), 30_000);
if (completedRecord != null && completedRecord.getStatus() == 1) {
long duration = System.currentTimeMillis() - startTime;
log.info("等待生成中记录完成,复用结果: recordId={}, imageUrl={}, duration={}ms",
completedRecord.getId(), completedRecord.getResultImageUrl(), duration);
if (request.getFaceId() != null) {
faceStatusManager.markPuzzleSourceVersion(request.getFaceId(), template.getId(), 0);
}
return PuzzleGenerateResponse.success(
completedRecord.getResultImageUrl(),
completedRecord.getResultFileSize(),
completedRecord.getResultWidth(),
completedRecord.getResultHeight(),
(int) duration,
completedRecord.getId(),
true,
completedRecord.getId()
);
}
// 超时或失败,兜底创建新记录
log.warn("等待生成中记录超时或失败,创建新记录: originalRecordId={}", duplicateRecord.getId());
}
} }
// 7. 创建生成记录 // 7. 创建生成记录
@@ -290,10 +317,10 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
); );
if (duplicateRecord != null) { if (duplicateRecord != null) {
long duration = System.currentTimeMillis() - startTime; long duration = System.currentTimeMillis() - startTime;
log.info("检测到重复内容,复用历史记录: recordId={}, imageUrl={}, duration={}ms", log.info("检测到重复内容,复用历史记录: recordId={}, status={}, imageUrl={}, duration={}ms",
duplicateRecord.getId(), duplicateRecord.getResultImageUrl(), duration); duplicateRecord.getId(), duplicateRecord.getStatus(), duplicateRecord.getResultImageUrl(), duration);
// 标记素材版本缓存 // 仅成功记录才标记素材版本缓存(生成中的记录可能会失败)
if (request.getFaceId() != null) { if (request.getFaceId() != null && duplicateRecord.getStatus() == 1) {
faceStatusManager.markPuzzleSourceVersion(request.getFaceId(), template.getId(), 0); faceStatusManager.markPuzzleSourceVersion(request.getFaceId(), template.getId(), 0);
} }
return duplicateRecord.getId(); return duplicateRecord.getId();
@@ -326,6 +353,33 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
return record.getId(); return record.getId();
} }
/**
* 等待生成中的记录完成
* 轮询数据库直到记录状态变为非生成中(成功或失败),或超时返回null
*
* @param recordId 记录ID
* @param timeoutMs 超时时间(毫秒)
* @return 完成后的记录,超时返回null
*/
private PuzzleGenerationRecordEntity waitForRecordCompletion(Long recordId, long timeoutMs) {
long deadline = System.currentTimeMillis() + timeoutMs;
while (System.currentTimeMillis() < deadline) {
PuzzleGenerationRecordEntity record = recordMapper.getById(recordId);
if (record == null || record.getStatus() != 0) {
return record;
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.warn("等待记录完成被中断: recordId={}", recordId);
return null;
}
}
log.warn("等待记录完成超时: recordId={}, timeoutMs={}", recordId, timeoutMs);
return null;
}
/** /**
* 校验请求参数 * 校验请求参数
*/ */

View File

@@ -129,13 +129,6 @@ public class PuzzleDuplicationDetector {
// 3. 对URL去重 // 3. 对URL去重
Set<String> uniqueUrls = new HashSet<>(imageUrls); Set<String> uniqueUrls = new HashSet<>(imageUrls);
// 4. 如果去重后只有1个URL,说明所有图片相同
if (uniqueUrls.size() == 1) {
String duplicateUrl = uniqueUrls.iterator().next();
log.warn("检测到重复图片: 所有{}个图片元素使用相同URL: {}", imageUrls.size(), duplicateUrl);
throw new DuplicateImageException(duplicateUrl, imageUrls.size());
}
log.debug("重复图片检测通过: 发现{}个不同的图片URL", uniqueUrls.size()); log.debug("重复图片检测通过: 发现{}个不同的图片URL", uniqueUrls.size());
} }

View File

@@ -235,6 +235,28 @@ public class SourceRepository {
faceStatusManager.invalidatePuzzleSourceVersion(faceId); faceStatusManager.invalidatePuzzleSourceVersion(faceId);
} }
public void setUserIsBuyItemBySourceId(Long memberId, Long sourceId, Long faceId, Long orderId) {
MemberSourceEntity memberSource = new MemberSourceEntity();
memberSource.setMemberId(memberId);
memberSource.setSourceId(sourceId);
memberSource.setOrderId(orderId);
memberSource.setIsBuy(1);
sourceMapper.updateRelationBySourceId(memberSource);
memberRelationRepository.clearSCacheByFace(faceId);
faceStatusManager.invalidatePuzzleSourceVersion(faceId);
}
public void setUserNotBuyItemBySourceId(Long memberId, Long sourceId, Long faceId) {
MemberSourceEntity memberSource = new MemberSourceEntity();
memberSource.setMemberId(memberId);
memberSource.setSourceId(sourceId);
memberSource.setOrderId(null);
memberSource.setIsBuy(0);
sourceMapper.updateRelationBySourceId(memberSource);
memberRelationRepository.clearSCacheByFace(faceId);
faceStatusManager.invalidatePuzzleSourceVersion(faceId);
}
public SourceEntity getSource(Long id) { public SourceEntity getSource(Long id) {
return sourceMapper.getEntity(id); return sourceMapper.getEntity(id);
} }

View File

@@ -1,6 +1,8 @@
package com.ycwl.basic.service; package com.ycwl.basic.service;
import com.github.pagehelper.PageInfo; import com.github.pagehelper.PageInfo;
import com.ycwl.basic.model.pc.videoreview.dto.AdminVideoReviewLogReqDTO;
import com.ycwl.basic.model.pc.videoreview.dto.AdminVideoReviewLogRespDTO;
import com.ycwl.basic.model.pc.videoreview.dto.VideoPurchaseCheckReqDTO; import com.ycwl.basic.model.pc.videoreview.dto.VideoPurchaseCheckReqDTO;
import com.ycwl.basic.model.pc.videoreview.dto.VideoPurchaseCheckRespDTO; import com.ycwl.basic.model.pc.videoreview.dto.VideoPurchaseCheckRespDTO;
import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewAddReqDTO; import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewAddReqDTO;
@@ -32,6 +34,14 @@ public interface VideoReviewService {
*/ */
PageInfo<VideoReviewRespDTO> getReviewList(VideoReviewListReqDTO reqDTO); PageInfo<VideoReviewRespDTO> getReviewList(VideoReviewListReqDTO reqDTO);
/**
* 管理后台分页查询评价日志
*
* @param reqDTO 查询条件
* @return 分页结果
*/
PageInfo<AdminVideoReviewLogRespDTO> getAdminReviewLogList(AdminVideoReviewLogReqDTO reqDTO);
/** /**
* 获取评价统计数据 * 获取评价统计数据
* *

View File

@@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo; import com.github.pagehelper.PageInfo;
import com.ycwl.basic.constant.BaseContextHandler; import com.ycwl.basic.constant.BaseContextHandler;
import com.ycwl.basic.enums.VideoReviewSourceEnum;
import com.ycwl.basic.exception.BaseException; import com.ycwl.basic.exception.BaseException;
import com.ycwl.basic.exception.BizException; import com.ycwl.basic.exception.BizException;
import com.ycwl.basic.mapper.OrderMapper; import com.ycwl.basic.mapper.OrderMapper;
@@ -14,6 +15,8 @@ import com.ycwl.basic.mapper.VideoReviewMapper;
import com.ycwl.basic.model.pc.order.entity.OrderEntity; import com.ycwl.basic.model.pc.order.entity.OrderEntity;
import com.ycwl.basic.model.pc.task.entity.TaskEntity; import com.ycwl.basic.model.pc.task.entity.TaskEntity;
import com.ycwl.basic.model.pc.video.entity.VideoEntity; import com.ycwl.basic.model.pc.video.entity.VideoEntity;
import com.ycwl.basic.model.pc.videoreview.dto.AdminVideoReviewLogReqDTO;
import com.ycwl.basic.model.pc.videoreview.dto.AdminVideoReviewLogRespDTO;
import com.ycwl.basic.model.pc.videoreview.dto.VideoPurchaseCheckReqDTO; import com.ycwl.basic.model.pc.videoreview.dto.VideoPurchaseCheckReqDTO;
import com.ycwl.basic.model.pc.videoreview.dto.VideoPurchaseCheckRespDTO; import com.ycwl.basic.model.pc.videoreview.dto.VideoPurchaseCheckRespDTO;
import com.ycwl.basic.repository.DeviceRepository; import com.ycwl.basic.repository.DeviceRepository;
@@ -72,6 +75,12 @@ public class VideoReviewServiceImpl implements VideoReviewService {
if (reqDTO.getRating() == null || reqDTO.getRating() < 1 || reqDTO.getRating() > 5) { if (reqDTO.getRating() == null || reqDTO.getRating() < 1 || reqDTO.getRating() > 5) {
throw new BaseException("评分必须在1-5之间"); throw new BaseException("评分必须在1-5之间");
} }
if (reqDTO.getSource() == null || reqDTO.getSource().isEmpty()) {
throw new BaseException("来源不能为空");
}
if (!VideoReviewSourceEnum.isValid(reqDTO.getSource())) {
throw new BaseException("来源值无效,仅支持: ORDER(订单), RENDER(渲染)");
}
// 2. 查询视频信息,获取景区ID // 2. 查询视频信息,获取景区ID
VideoEntity video = videoMapper.getEntity(reqDTO.getVideoId()); VideoEntity video = videoMapper.getEntity(reqDTO.getVideoId());
@@ -93,12 +102,16 @@ public class VideoReviewServiceImpl implements VideoReviewService {
entity.setCreator(creator); entity.setCreator(creator);
entity.setRating(reqDTO.getRating()); entity.setRating(reqDTO.getRating());
entity.setContent(reqDTO.getContent()); entity.setContent(reqDTO.getContent());
entity.setCameraPositionRating(reqDTO.getCameraPositionRating()); entity.setProblemDeviceIds(reqDTO.getProblemDeviceIds());
entity.setProblemTags(reqDTO.getProblemTags());
entity.setSource(reqDTO.getSource());
entity.setSourceId(reqDTO.getSourceId());
// 5. 插入数据库 // 5. 插入数据库
videoReviewMapper.insert(entity); videoReviewMapper.insert(entity);
log.info("管理员[{}]对视频[{}]添加评价成功,评价ID: {}", creator, reqDTO.getVideoId(), entity.getId()); log.info("管理员[{}]对视频[{}]添加评价成功,评价ID: {}, 来源: {}, 来源ID: {}",
creator, reqDTO.getVideoId(), entity.getId(), reqDTO.getSource(), reqDTO.getSourceId());
return entity.getId(); return entity.getId();
} }
@@ -114,6 +127,18 @@ public class VideoReviewServiceImpl implements VideoReviewService {
return new PageInfo<>(list); return new PageInfo<>(list);
} }
@Override
public PageInfo<AdminVideoReviewLogRespDTO> getAdminReviewLogList(AdminVideoReviewLogReqDTO reqDTO) {
// 设置分页参数
PageHelper.startPage(reqDTO.getPageNum(), reqDTO.getPageSize());
// 查询列表
List<AdminVideoReviewLogRespDTO> list = videoReviewMapper.selectAdminReviewLogList(reqDTO);
// 封装分页结果
return new PageInfo<>(list);
}
@Override @Override
public VideoReviewStatisticsRespDTO getStatistics() { public VideoReviewStatisticsRespDTO getStatistics() {
VideoReviewStatisticsRespDTO statistics = new VideoReviewStatisticsRespDTO(); VideoReviewStatisticsRespDTO statistics = new VideoReviewStatisticsRespDTO();
@@ -154,9 +179,9 @@ public class VideoReviewServiceImpl implements VideoReviewService {
List<VideoReviewStatisticsRespDTO.ScenicReviewRank> scenicRankList = videoReviewMapper.countScenicRank(10); List<VideoReviewStatisticsRespDTO.ScenicReviewRank> scenicRankList = videoReviewMapper.countScenicRank(10);
statistics.setScenicRankList(scenicRankList); statistics.setScenicRankList(scenicRankList);
// 6. 机位评价维度平均值 // 6. 问题机位统计
Map<String, BigDecimal> cameraPositionAverage = calculateCameraPositionAverage(); Map<Long, Long> problemDeviceStatistics = calculateProblemDeviceStatistics();
statistics.setCameraPositionAverage(cameraPositionAverage); statistics.setProblemDeviceStatistics(problemDeviceStatistics);
return statistics; return statistics;
} }
@@ -168,37 +193,11 @@ public class VideoReviewServiceImpl implements VideoReviewService {
reqDTO.setPageSize(Integer.MAX_VALUE); reqDTO.setPageSize(Integer.MAX_VALUE);
List<VideoReviewRespDTO> list = videoReviewMapper.selectReviewList(reqDTO); List<VideoReviewRespDTO> list = videoReviewMapper.selectReviewList(reqDTO);
// 2. 收集所有机位ID并批量查询机位名称 // 2. 创建Excel工作簿
Set<Long> allDeviceIds = new LinkedHashSet<>();
for (VideoReviewRespDTO review : list) {
Map<String, Integer> cameraRating = review.getCameraPositionRating();
if (cameraRating != null && !cameraRating.isEmpty()) {
// 收集机位ID (按顺序)
for (String deviceIdStr : cameraRating.keySet()) {
try {
allDeviceIds.add(Long.valueOf(deviceIdStr));
} catch (NumberFormatException e) {
log.warn("无效的机位ID: {}", deviceIdStr);
}
}
}
}
// 批量查询机位名称
Map<Long, String> deviceNames = new HashMap<>();
if (!allDeviceIds.isEmpty()) {
deviceNames = deviceRepository.batchGetDeviceNames(new ArrayList<>(allDeviceIds));
}
// 对机位ID按ID排序,保证表头顺序一致
List<Long> sortedDeviceIds = new ArrayList<>(allDeviceIds);
sortedDeviceIds.sort(Long::compareTo);
// 3. 创建Excel工作簿
Workbook workbook = new XSSFWorkbook(); Workbook workbook = new XSSFWorkbook();
Sheet sheet = workbook.createSheet("视频评价数据"); Sheet sheet = workbook.createSheet("视频评价数据");
// 4. 创建标题行样式 // 3. 创建标题行样式
CellStyle headerStyle = workbook.createCellStyle(); CellStyle headerStyle = workbook.createCellStyle();
Font headerFont = workbook.createFont(); Font headerFont = workbook.createFont();
headerFont.setBold(true); headerFont.setBold(true);
@@ -206,7 +205,7 @@ public class VideoReviewServiceImpl implements VideoReviewService {
headerStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex()); headerStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
// 5. 生成动态表头 - 使用机位名称作为表头 // 4. 生成表头
Row headerRow = sheet.createRow(0); Row headerRow = sheet.createRow(0);
List<String> headerList = new ArrayList<>(); List<String> headerList = new ArrayList<>();
headerList.add("评价ID"); headerList.add("评价ID");
@@ -216,14 +215,8 @@ public class VideoReviewServiceImpl implements VideoReviewService {
headerList.add("评价人名称"); headerList.add("评价人名称");
headerList.add("评分"); headerList.add("评分");
headerList.add("文字评价"); headerList.add("文字评价");
headerList.add("问题机位ID列表");
// 添加机位列 - 表头直接使用机位名称 headerList.add("问题标签");
Map<Long, String> finalDeviceNames = deviceNames;
for (Long deviceId : sortedDeviceIds) {
String deviceName = finalDeviceNames.getOrDefault(deviceId, "未知设备(ID:" + deviceId + ")");
headerList.add(deviceName);
}
headerList.add("创建时间"); headerList.add("创建时间");
headerList.add("更新时间"); headerList.add("更新时间");
@@ -234,7 +227,7 @@ public class VideoReviewServiceImpl implements VideoReviewService {
cell.setCellStyle(headerStyle); cell.setCellStyle(headerStyle);
} }
// 6. 填充数据 // 5. 填充数据
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
int rowNum = 1; int rowNum = 1;
@@ -249,41 +242,37 @@ public class VideoReviewServiceImpl implements VideoReviewService {
row.createCell(colIndex++).setCellValue(review.getScenicName()); row.createCell(colIndex++).setCellValue(review.getScenicName());
row.createCell(colIndex++).setCellValue(review.getCreatorName()); row.createCell(colIndex++).setCellValue(review.getCreatorName());
row.createCell(colIndex++).setCellValue(review.getRating()); row.createCell(colIndex++).setCellValue(review.getRating());
row.createCell(colIndex++).setCellValue(review.getContent()); row.createCell(colIndex++).setCellValue(review.getContent() != null ? review.getContent() : "");
// 机位评价列 - 按表头顺序填充 // 问题机位ID列表
Map<String, Integer> cameraRating = review.getCameraPositionRating(); List<Long> problemDeviceIds = review.getProblemDeviceIds();
for (Long deviceId : sortedDeviceIds) { String problemDeviceIdsStr = (problemDeviceIds != null && !problemDeviceIds.isEmpty())
String deviceIdStr = String.valueOf(deviceId); ? problemDeviceIds.toString()
Integer rating = null; : "";
row.createCell(colIndex++).setCellValue(problemDeviceIdsStr);
if (cameraRating != null && cameraRating.containsKey(deviceIdStr)) { // 问题标签
rating = cameraRating.get(deviceIdStr); List<String> problemTags = review.getProblemTags();
} String problemTagsStr = (problemTags != null && !problemTags.isEmpty())
? String.join(", ", problemTags)
Cell cell = row.createCell(colIndex++); : "";
if (rating != null) { row.createCell(colIndex++).setCellValue(problemTagsStr);
cell.setCellValue(rating);
} else {
cell.setCellValue("");
}
}
// 时间列 // 时间列
row.createCell(colIndex++).setCellValue(review.getCreateTime() != null ? sdf.format(review.getCreateTime()) : ""); row.createCell(colIndex++).setCellValue(review.getCreateTime() != null ? sdf.format(review.getCreateTime()) : "");
row.createCell(colIndex).setCellValue(review.getUpdateTime() != null ? sdf.format(review.getUpdateTime()) : ""); row.createCell(colIndex).setCellValue(review.getUpdateTime() != null ? sdf.format(review.getUpdateTime()) : "");
} }
// 7. 自动调整列宽 // 6. 自动调整列宽
for (int i = 0; i < headerList.size(); i++) { for (int i = 0; i < headerList.size(); i++) {
sheet.autoSizeColumn(i); sheet.autoSizeColumn(i);
} }
// 8. 写入输出流 // 7. 写入输出流
workbook.write(outputStream); workbook.write(outputStream);
workbook.close(); workbook.close();
log.info("导出视频评价数据成功,共{}条,机位数:{}", list.size(), sortedDeviceIds.size()); log.info("导出视频评价数据成功,共{}条", list.size());
} }
@Override @Override
@@ -338,32 +327,25 @@ public class VideoReviewServiceImpl implements VideoReviewService {
} }
/** /**
* 计算各机位的平均评分 * 统计问题机位
* 统计每个机位被标记为问题的次数
*/ */
private Map<String, BigDecimal> calculateCameraPositionAverage() { private Map<Long, Long> calculateProblemDeviceStatistics() {
List<Map<String, Integer>> allRatings = videoReviewMapper.selectAllCameraPositionRatings(); List<List<Long>> allProblemDeviceIds = videoReviewMapper.selectAllProblemDeviceIds();
if (allRatings == null || allRatings.isEmpty()) { if (allProblemDeviceIds == null || allProblemDeviceIds.isEmpty()) {
return new HashMap<>(); return new HashMap<>();
} }
// 统计各机位的总分和次数 // 统计各机位被标记为问题的次数
Map<String, List<Integer>> deviceScores = new HashMap<>(); Map<Long, Long> deviceProblemCount = new HashMap<>();
for (Map<String, Integer> rating : allRatings) { for (List<Long> problemDeviceIds : allProblemDeviceIds) {
if (rating == null) continue; if (problemDeviceIds == null || problemDeviceIds.isEmpty()) continue;
// 遍历每个机位的评分 for (Long deviceId : problemDeviceIds) {
for (Map.Entry<String, Integer> entry : rating.entrySet()) { deviceProblemCount.put(deviceId, deviceProblemCount.getOrDefault(deviceId, 0L) + 1);
deviceScores.computeIfAbsent(entry.getKey(), k -> new ArrayList<>()).add(entry.getValue());
} }
} }
// 计算平均值 return deviceProblemCount;
Map<String, BigDecimal> result = new HashMap<>();
for (Map.Entry<String, List<Integer>> entry : deviceScores.entrySet()) {
double avg = entry.getValue().stream().mapToInt(Integer::intValue).average().orElse(0.0);
result.put(entry.getKey(), BigDecimal.valueOf(avg).setScale(2, RoundingMode.HALF_UP));
}
return result;
} }
} }

View File

@@ -145,7 +145,6 @@ public class AppStatisticsServiceImpl implements AppStatisticsService {
@Override @Override
public ApiResponse<AppStatisticsFunnelVO> userConversionFunnel(CommonQueryReq query) { public ApiResponse<AppStatisticsFunnelVO> userConversionFunnel(CommonQueryReq query) {
String redisKey = "statistics:tmp_cache:"+query.getScenicId();
AppStatisticsFunnelVO vo = new AppStatisticsFunnelVO(); AppStatisticsFunnelVO vo = new AppStatisticsFunnelVO();
if(query.getEndTime()==null && query.getStartTime()==null){ if(query.getEndTime()==null && query.getStartTime()==null){
// 没有传时间,则代表用户没有自定义查询时间,使用standard来判断查询时间范围 // 没有传时间,则代表用户没有自定义查询时间,使用standard来判断查询时间范围
@@ -156,9 +155,79 @@ public class AppStatisticsServiceImpl implements AppStatisticsService {
//获取当前周期的具体时间范围 //获取当前周期的具体时间范围
standardToNewSpecificTime(query); standardToNewSpecificTime(query);
} }
// 构建包含日期维度的 Redis 缓存 key
String redisKey = String.format("statistics:tmp_cache:%s:%s",
query.getScenicId(),
query.getStartTime() != null ? DateUtil.formatDate(query.getStartTime()) : "null");
if (!query.isRealtime()) { if (!query.isRealtime()) {
if (!(DateUtil.isIn(query.getStartTime(), DateUtil.tomorrow(), DateUtil.yesterday()) && DateUtil.isIn(query.getEndTime(), DateUtil.tomorrow(), DateUtil.yesterday()))) { if (!(DateUtil.isIn(query.getStartTime(), DateUtil.yesterday(), DateUtil.tomorrow()) && DateUtil.isIn(query.getEndTime(), DateUtil.yesterday(), DateUtil.tomorrow()))) {
// 查询缓存数据 // 判断是否为跨范围查询且包含今天
Date today = DateUtil.beginOfDay(new Date());
boolean containsToday = query.getEndTime() != null && !query.getEndTime().before(today);
boolean isMultiDayQuery = query.getStartTime() != null && query.getStartTime().before(today);
if (containsToday && isMultiDayQuery) {
// 跨范围查询且包含今天:需要分别查询历史数据和今天数据
AppStatisticsFunnelVO result = new AppStatisticsFunnelVO();
// 1. 查询历史数据(从 startDate 到昨天结束)
Date yesterday = DateUtil.endOfDay(DateUtil.yesterday());
List<AppStatisticsFunnelVO> historyList = statisticsMapper.listStatByScenic(
query.getScenicId(),
query.getStartTime(),
yesterday
);
// 累加历史数据
if (historyList != null && !historyList.isEmpty()) {
for (AppStatisticsFunnelVO item : historyList) {
result.setCameraShotOfMemberNum(addIntSafely(result.getCameraShotOfMemberNum(), item.getCameraShotOfMemberNum()));
result.setScanCodeVisitorOfMemberNum(addIntSafely(result.getScanCodeVisitorOfMemberNum(), item.getScanCodeVisitorOfMemberNum()));
result.setUploadFaceOfMemberNum(addIntSafely(result.getUploadFaceOfMemberNum(), item.getUploadFaceOfMemberNum()));
result.setPushOfMemberNum(addIntSafely(result.getPushOfMemberNum(), item.getPushOfMemberNum()));
result.setCompleteVideoOfMemberNum(addIntSafely(result.getCompleteVideoOfMemberNum(), item.getCompleteVideoOfMemberNum()));
result.setPreviewVideoOfMemberNum(addIntSafely(result.getPreviewVideoOfMemberNum(), item.getPreviewVideoOfMemberNum()));
result.setClickOnPayOfMemberNum(addIntSafely(result.getClickOnPayOfMemberNum(), item.getClickOnPayOfMemberNum()));
result.setPayOfMemberNum(addIntSafely(result.getPayOfMemberNum(), item.getPayOfMemberNum()));
result.setTotalVisitorOfMemberNum(addIntSafely(result.getTotalVisitorOfMemberNum(), item.getTotalVisitorOfMemberNum()));
result.setCompleteOfVideoNum(addIntSafely(result.getCompleteOfVideoNum(), item.getCompleteOfVideoNum()));
result.setPreviewOfVideoNum(addIntSafely(result.getPreviewOfVideoNum(), item.getPreviewOfVideoNum()));
result.setPayOfOrderNum(addIntSafely(result.getPayOfOrderNum(), item.getPayOfOrderNum()));
result.setRefundOfOrderNum(addIntSafely(result.getRefundOfOrderNum(), item.getRefundOfOrderNum()));
result.setPayOfOrderAmount(addBigDecimalSafely(result.payOfOrderAmount(), item.payOfOrderAmount()));
result.setRefundOfOrderAmount(addBigDecimalSafely(result.refundOfOrderAmount(), item.refundOfOrderAmount()));
}
}
// 2. 查询今天的实时数据
CommonQueryReq todayQuery = new CommonQueryReq();
todayQuery.setScenicId(query.getScenicId());
todayQuery.setStartTime(today);
todayQuery.setEndTime(query.getEndTime());
// 执行今天的实时查询
AppStatisticsFunnelVO todayData = queryRealtimeData(todayQuery);
// 3. 合并今天的数据到结果中
result.setCameraShotOfMemberNum(addIntSafely(result.getCameraShotOfMemberNum(), todayData.getCameraShotOfMemberNum()));
result.setScanCodeVisitorOfMemberNum(addIntSafely(result.getScanCodeVisitorOfMemberNum(), todayData.getScanCodeVisitorOfMemberNum()));
result.setUploadFaceOfMemberNum(addIntSafely(result.getUploadFaceOfMemberNum(), todayData.getUploadFaceOfMemberNum()));
result.setPushOfMemberNum(addIntSafely(result.getPushOfMemberNum(), todayData.getPushOfMemberNum()));
result.setCompleteVideoOfMemberNum(addIntSafely(result.getCompleteVideoOfMemberNum(), todayData.getCompleteVideoOfMemberNum()));
result.setPreviewVideoOfMemberNum(addIntSafely(result.getPreviewVideoOfMemberNum(), todayData.getPreviewVideoOfMemberNum()));
result.setClickOnPayOfMemberNum(addIntSafely(result.getClickOnPayOfMemberNum(), todayData.getClickOnPayOfMemberNum()));
result.setPayOfMemberNum(addIntSafely(result.getPayOfMemberNum(), todayData.getPayOfMemberNum()));
result.setTotalVisitorOfMemberNum(addIntSafely(result.getTotalVisitorOfMemberNum(), todayData.getTotalVisitorOfMemberNum()));
result.setCompleteOfVideoNum(addIntSafely(result.getCompleteOfVideoNum(), todayData.getCompleteOfVideoNum()));
result.setPreviewOfVideoNum(addIntSafely(result.getPreviewOfVideoNum(), todayData.getPreviewOfVideoNum()));
result.setPayOfOrderNum(addIntSafely(result.getPayOfOrderNum(), todayData.getPayOfOrderNum()));
result.setRefundOfOrderNum(addIntSafely(result.getRefundOfOrderNum(), todayData.getRefundOfOrderNum()));
result.setPayOfOrderAmount(addBigDecimalSafely(result.payOfOrderAmount(), todayData.payOfOrderAmount()));
result.setRefundOfOrderAmount(addBigDecimalSafely(result.refundOfOrderAmount(), todayData.refundOfOrderAmount()));
return ApiResponse.success(result);
} else {
// 纯历史查询(不包含今天)
List<AppStatisticsFunnelVO> list = statisticsMapper.listStatByScenic(query.getScenicId(), query.getStartTime(), query.getEndTime()); List<AppStatisticsFunnelVO> list = statisticsMapper.listStatByScenic(query.getScenicId(), query.getStartTime(), query.getEndTime());
AppStatisticsFunnelVO resp = new AppStatisticsFunnelVO(); AppStatisticsFunnelVO resp = new AppStatisticsFunnelVO();
if (list != null && !list.isEmpty()) { if (list != null && !list.isEmpty()) {
@@ -188,8 +257,9 @@ public class AppStatisticsServiceImpl implements AppStatisticsService {
} }
} }
} }
}
if (!query.isRealtime()) { if (!query.isRealtime()) {
if (DateUtil.isIn(query.getStartTime(), DateUtil.tomorrow(), DateUtil.yesterday()) && DateUtil.isIn(query.getEndTime(), DateUtil.tomorrow(), DateUtil.yesterday())) { if (DateUtil.isIn(query.getStartTime(), DateUtil.yesterday(), DateUtil.tomorrow()) && DateUtil.isIn(query.getEndTime(), DateUtil.yesterday(), DateUtil.tomorrow())) {
// 缓存 // 缓存
if (redisTemplate.hasKey(redisKey)) { if (redisTemplate.hasKey(redisKey)) {
String s = redisTemplate.opsForValue().get(redisKey); String s = redisTemplate.opsForValue().get(redisKey);
@@ -203,7 +273,7 @@ public class AppStatisticsServiceImpl implements AppStatisticsService {
try { try {
// 缓存 // 缓存
if (!query.isRealtime()) { if (!query.isRealtime()) {
if (DateUtil.isIn(query.getStartTime(), DateUtil.tomorrow(), DateUtil.yesterday()) && DateUtil.isIn(query.getEndTime(), DateUtil.tomorrow(), DateUtil.yesterday())) { if (DateUtil.isIn(query.getStartTime(), DateUtil.yesterday(), DateUtil.tomorrow()) && DateUtil.isIn(query.getEndTime(), DateUtil.yesterday(), DateUtil.tomorrow())) {
// 缓存 // 缓存
if (redisTemplate.hasKey(redisKey)) { if (redisTemplate.hasKey(redisKey)) {
String s = redisTemplate.opsForValue().get(redisKey); String s = redisTemplate.opsForValue().get(redisKey);
@@ -212,72 +282,18 @@ public class AppStatisticsServiceImpl implements AppStatisticsService {
} }
} }
} }
//镜头检测游客数
// Integer cameraShotOfMemberNum=statisticsMapper.countCameraShotOfMember(query);
//扫码访问人数
// 扫小程序码或景区码进入访问的用户数,包括授权用户(使用OpenID进行精准统计)和未授权用户(使用 UUID统计访问)。但当用户授权时,获取OpenID并与UUID关联,删除本地UUID,避免重复记录。
Integer scanCodeVisitorOfMemberNum=statsQueryService.countScanCodeOfMember(query);
//上传头像(人脸)人数
// 上传了人脸的用户数(包括本地临时ID和获取到OpenID的,同一设备微信获取到OpenID要覆盖掉之前生成的临时ID),上传多张人脸都只算一个人。
Integer uploadFaceOfMemberNum=statsQueryService.countUploadFaceOfMember(query);
//推送订阅人数
// 只要点了允许通知,哪怕只勾选1条订阅都算
Integer pushOfMemberNum =statsQueryService.countPushOfMember(query);
//生成视频人数
// 生成过Vlog视频的用户ID数,要注意屏蔽掉以前没有片段也能生成的情况
Integer completeVideoOfMemberNum =statsQueryService.countCompleteVideoOfMember(query);
//预览视频人数
// 购买前播放了5秒的视频条数。
Integer previewVideoOfMemberNum =statsQueryService.countPreviewVideoOfMember(query);
if (previewVideoOfMemberNum==null){
previewVideoOfMemberNum=0;
}
//点击购买人数
// 点了立即购买按钮的用户ID就算,包括支付的和未支付的都算,只要点击了。
Integer clickOnPayOfMemberNum =statisticsMapper.countClickPayOfMember(query);
//支付订单人数
Integer payOfMemberNum =statisticsMapper.countPayOfMember(query);
//总访问人数
// 通过任何途径访问到小程序的总人数,包括授权用户和未授权用户。
Integer totalVisitorOfMemberNum =statsQueryService.countTotalVisitorOfMember(query);
// Integer totalVisitorOfMemberNum =scanCodeVisitorOfMemberNum;
//生成视频条数
// 仅指代生成的Vlog条数,不包含录像原片。
Integer completeOfVideoNum =statsQueryService.countCompleteOfVideo(query);
//预览视频条数
Integer previewOfVideoNum =statsQueryService.countPreviewOfVideo(query);
//支付订单数
Integer payOfOrderNum =statisticsMapper.countPayOfOrder(query);
//支付订单金额
BigDecimal payOfOrderAmount =statisticsMapper.countOrderAmount(query);
//退款订单数
Integer refundOfOrderNum =statisticsMapper.countRefundOfOrder(query);
//退款订单金额
BigDecimal refundOfOrderAmount =statisticsMapper.countRefundAmount(query);
vo.setScanCodeVisitorOfMemberNum(scanCodeVisitorOfMemberNum); // 执行实时查询
vo.setUploadFaceOfMemberNum(uploadFaceOfMemberNum); vo = queryRealtimeData(query);
vo.setPushOfMemberNum(pushOfMemberNum);
vo.setCompleteVideoOfMemberNum(completeVideoOfMemberNum);
vo.setPreviewVideoOfMemberNum(previewVideoOfMemberNum);
vo.setClickOnPayOfMemberNum(clickOnPayOfMemberNum);
vo.setPayOfMemberNum(payOfMemberNum);
vo.setTotalVisitorOfMemberNum(totalVisitorOfMemberNum); // 仅对当天数据启用 Redis 缓存(短期缓存,减少实时查询压力)
vo.setCompleteOfVideoNum(completeOfVideoNum); // 历史数据已在 scenic_stats 表中持久化,不需要 Redis 缓存
vo.setPreviewOfVideoNum(previewOfVideoNum); if (!query.isRealtime() && query.getStartTime() != null) {
vo.setPayOfOrderNum(payOfOrderNum); // 判断查询日期是否为今天
vo.setPayOfOrderAmount(payOfOrderAmount.setScale(2, RoundingMode.HALF_UP)); if (DateUtil.isSameDay(query.getStartTime(), new Date())) {
vo.setRefundOfOrderNum(refundOfOrderNum);
vo.setRefundOfOrderAmount(refundOfOrderAmount.setScale(2, RoundingMode.HALF_UP));
// 仅在非 realtime 模式下写入缓存
// realtime=true 时由调用方(如定时任务)自行控制写入目标日期,不污染当天缓存
if (!query.isRealtime()) {
if (query.getScenicId() != null) {
statisticsMapper.insertStat(query.getScenicId(), new Date(), vo);
}
redisTemplate.opsForValue().set(redisKey, JacksonUtil.toJSONString(vo), 60, TimeUnit.SECONDS); redisTemplate.opsForValue().set(redisKey, JacksonUtil.toJSONString(vo), 60, TimeUnit.SECONDS);
} }
}
return ApiResponse.success(vo); return ApiResponse.success(vo);
} finally { } finally {
lock.unlock(); lock.unlock();
@@ -298,6 +314,64 @@ public class AppStatisticsServiceImpl implements AppStatisticsService {
return int1 == null ? 0 : int1 + (int2 == null ? 0 : int2); return int1 == null ? 0 : int1 + (int2 == null ? 0 : int2);
} }
/**
* 执行实时数据查询(从 ClickHouse 和 MySQL 查询最新数据)
* @param query 查询条件
* @return 实时统计数据
*/
private AppStatisticsFunnelVO queryRealtimeData(CommonQueryReq query) {
AppStatisticsFunnelVO vo = new AppStatisticsFunnelVO();
//扫码访问人数
Integer scanCodeVisitorOfMemberNum = statsQueryService.countScanCodeOfMember(query);
//上传头像(人脸)人数
Integer uploadFaceOfMemberNum = statsQueryService.countUploadFaceOfMember(query);
//推送订阅人数
Integer pushOfMemberNum = statsQueryService.countPushOfMember(query);
//生成视频人数
Integer completeVideoOfMemberNum = statsQueryService.countCompleteVideoOfMember(query);
//预览视频人数
Integer previewVideoOfMemberNum = statsQueryService.countPreviewVideoOfMember(query);
if (previewVideoOfMemberNum == null) {
previewVideoOfMemberNum = 0;
}
//点击购买人数
Integer clickOnPayOfMemberNum = statisticsMapper.countClickPayOfMember(query);
//支付订单人数
Integer payOfMemberNum = statisticsMapper.countPayOfMember(query);
//总访问人数
Integer totalVisitorOfMemberNum = statsQueryService.countTotalVisitorOfMember(query);
//生成视频条数
Integer completeOfVideoNum = statsQueryService.countCompleteOfVideo(query);
//预览视频条数
Integer previewOfVideoNum = statsQueryService.countPreviewOfVideo(query);
//支付订单数
Integer payOfOrderNum = statisticsMapper.countPayOfOrder(query);
//支付订单金额
BigDecimal payOfOrderAmount = statisticsMapper.countOrderAmount(query);
//退款订单数
Integer refundOfOrderNum = statisticsMapper.countRefundOfOrder(query);
//退款订单金额
BigDecimal refundOfOrderAmount = statisticsMapper.countRefundAmount(query);
vo.setScanCodeVisitorOfMemberNum(scanCodeVisitorOfMemberNum);
vo.setUploadFaceOfMemberNum(uploadFaceOfMemberNum);
vo.setPushOfMemberNum(pushOfMemberNum);
vo.setCompleteVideoOfMemberNum(completeVideoOfMemberNum);
vo.setPreviewVideoOfMemberNum(previewVideoOfMemberNum);
vo.setClickOnPayOfMemberNum(clickOnPayOfMemberNum);
vo.setPayOfMemberNum(payOfMemberNum);
vo.setTotalVisitorOfMemberNum(totalVisitorOfMemberNum);
vo.setCompleteOfVideoNum(completeOfVideoNum);
vo.setPreviewOfVideoNum(previewOfVideoNum);
vo.setPayOfOrderNum(payOfOrderNum);
vo.setPayOfOrderAmount(payOfOrderAmount != null ? payOfOrderAmount.setScale(2, RoundingMode.HALF_UP) : BigDecimal.ZERO);
vo.setRefundOfOrderNum(refundOfOrderNum);
vo.setRefundOfOrderAmount(refundOfOrderAmount != null ? refundOfOrderAmount.setScale(2, RoundingMode.HALF_UP) : BigDecimal.ZERO);
return vo;
}
@Override @Override
public ApiResponse orderChart(CommonQueryReq query) { public ApiResponse orderChart(CommonQueryReq query) {
if(query.getEndTime()==null && query.getStartTime()==null){ if(query.getEndTime()==null && query.getStartTime()==null){

View File

@@ -16,6 +16,7 @@ import lombok.extern.slf4j.Slf4j;
import ai.z.openapi.service.model.ChatMessage; import ai.z.openapi.service.model.ChatMessage;
import ai.z.openapi.service.model.ChatMessageRole; import ai.z.openapi.service.model.ChatMessageRole;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -42,6 +43,7 @@ public class FaceChatServiceImpl implements FaceChatService {
private final FaceChatConversationMapper conversationMapper; private final FaceChatConversationMapper conversationMapper;
private final FaceChatMessageMapper messageMapper; private final FaceChatMessageMapper messageMapper;
private final FaceRepository faceRepository; private final FaceRepository faceRepository;
@Lazy
private final GlmClient glmClient; private final GlmClient glmClient;
@Override @Override

View File

@@ -296,6 +296,15 @@ public class GoodsServiceImpl implements GoodsService {
return response; return response;
} }
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(face.getScenicId());
if (Integer.valueOf(2).equals(scenicConfig.getInteger("scenic_mode", 0))) {
// 摄影师拍照
List<MemberSourceEntity> list = memberRelationRepository.listSourceByFaceRelation(faceId, 2);
response.setStatus(VideoTaskStatus.SUCCESS.getCode());
response.setCount(list.size());
return response;
}
// ==================== 第三步:检查模板渲染状态 ==================== // ==================== 第三步:检查模板渲染状态 ====================
// 获取该景区的所有视频模板 // 获取该景区的所有视频模板
List<TemplateRespVO> templateList = templateRepository.getTemplateListByScenicId(response.getScenicId()); List<TemplateRespVO> templateList = templateRepository.getTemplateListByScenicId(response.getScenicId());
@@ -332,16 +341,8 @@ public class GoodsServiceImpl implements GoodsService {
// ==================== 第四步:根据切片完成状态返回结果 ==================== // ==================== 第四步:根据切片完成状态返回结果 ====================
if (status == FaceCutStatus.WAITING_USER_SELECT) { if (status == FaceCutStatus.WAITING_USER_SELECT || status == FaceCutStatus.COMPLETED) {
// 切片已完成,但景区配置了 face_select_first=true // 切片已完成(或等待用户选择),查询该人脸关联的视频信息
// 需要等待用户手动选择模板后才开始渲染
// 前端展示:「专属视频合成中」
response.setStatus(VideoTaskStatus.PROCESSING.getCode());
return response;
}
if (status == FaceCutStatus.COMPLETED) {
// 切片已完成,查询该人脸关联的视频信息
List<MemberVideoEntity> taskList = videoMapper.listRelationByFace(faceId); List<MemberVideoEntity> taskList = videoMapper.listRelationByFace(faceId);
if (taskList == null || taskList.isEmpty()) { if (taskList == null || taskList.isEmpty()) {
response.setStatus(VideoTaskStatus.PENDING.getCode()); response.setStatus(VideoTaskStatus.PENDING.getCode());

View File

@@ -8,6 +8,7 @@ import com.ycwl.basic.utils.ApiResponse;
import java.io.File; import java.io.File;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* @Author:longbinbin * @Author:longbinbin
@@ -23,4 +24,39 @@ public interface SourceService {
ApiResponse cutVideo(Long id); ApiResponse cutVideo(Long id);
String uploadAndUpdateUrl(Long id, File file); String uploadAndUpdateUrl(Long id, File file);
/**
* 根据sourceId列表查询关联的faceId
* @param sourceIds sourceId列表
* @return sourceId -> faceId 的映射
*/
ApiResponse<Map<Long, Long>> getFaceIdsBySourceIds(List<Long> sourceIds);
/**
* 根据faceId分页查询关联的source记录
* @param sourceReqQuery 查询参数(需设置faceId)
* @return 分页结果
*/
ApiResponse<PageInfo<SourceRespVO>> pageByFaceId(SourceReqQuery sourceReqQuery);
/**
* 管理员软删除(取消)关联记录
* @param id member_source 记录 ID
* @return 操作结果
*/
ApiResponse<Void> cancelRelation(Long id);
/**
* 管理员恢复已取消的关联记录
* @param id member_source 记录 ID
* @return 操作结果
*/
ApiResponse<Void> reactivateRelation(Long id);
/**
* 分页查询已取消的关联记录(管理员用)
* @param sourceReqQuery 查询参数(需设置faceId)
* @return 分页结果
*/
ApiResponse<PageInfo<SourceRespVO>> pageDeletedByFaceId(SourceReqQuery sourceReqQuery);
} }

View File

@@ -12,6 +12,7 @@ import com.ycwl.basic.face.pipeline.enums.FaceMatchingScene;
import com.ycwl.basic.face.pipeline.factory.FaceMatchingPipelineFactory; import com.ycwl.basic.face.pipeline.factory.FaceMatchingPipelineFactory;
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter; import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
import com.ycwl.basic.facebody.entity.SearchFaceResultItem; import com.ycwl.basic.facebody.entity.SearchFaceResultItem;
import com.ycwl.basic.integration.device.dto.device.DeviceV2DTO;
import com.ycwl.basic.mapper.FaceSampleMapper; import com.ycwl.basic.mapper.FaceSampleMapper;
import com.ycwl.basic.mapper.ProjectMapper; import com.ycwl.basic.mapper.ProjectMapper;
import com.ycwl.basic.mapper.SourceMapper; import com.ycwl.basic.mapper.SourceMapper;
@@ -60,6 +61,7 @@ import com.ycwl.basic.repository.FaceRepository;
import com.ycwl.basic.repository.MemberRelationRepository; import com.ycwl.basic.repository.MemberRelationRepository;
import com.ycwl.basic.repository.OrderRepository; import com.ycwl.basic.repository.OrderRepository;
import com.ycwl.basic.repository.ScenicRepository; import com.ycwl.basic.repository.ScenicRepository;
import com.ycwl.basic.repository.SourceRepository;
import com.ycwl.basic.repository.TemplateRepository; import com.ycwl.basic.repository.TemplateRepository;
import com.ycwl.basic.repository.VideoRepository; import com.ycwl.basic.repository.VideoRepository;
import com.ycwl.basic.repository.VideoTaskRepository; import com.ycwl.basic.repository.VideoTaskRepository;
@@ -210,6 +212,8 @@ public class FaceServiceImpl implements FaceService {
private OrderRepository orderRepository; private OrderRepository orderRepository;
@Autowired @Autowired
private com.ycwl.basic.biz.FaceStatusManager faceStatusManager; private com.ycwl.basic.biz.FaceStatusManager faceStatusManager;
@Autowired
private SourceRepository sourceRepository;
@Override @Override
public ApiResponse<PageInfo<FaceRespVO>> pageQuery(FaceReqQuery faceReqQuery) { public ApiResponse<PageInfo<FaceRespVO>> pageQuery(FaceReqQuery faceReqQuery) {
@@ -472,7 +476,47 @@ public class FaceServiceImpl implements FaceService {
if (face == null) { if (face == null) {
return Collections.emptyList(); return Collections.emptyList();
} }
Long userId = face.getMemberId(); Long userId = Long.parseLong(BaseContextHandler.getUserId());
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(face.getScenicId());
if (Integer.valueOf(2).equals(scenicConfig.getInteger("scenic_mode", 0))) {
List<ContentPageVO> result = new ArrayList<>();
// 摄影师拍照
List<DeviceV2DTO> deviceList = deviceRepository.getAllDeviceByScenicId(face.getScenicId());
List<SourceEntity> sourceEntityList = sourceMapper.listSourceByFaceRelation(face.getId(), 2);
for (SourceEntity sourceEntity : sourceEntityList) {
ContentPageVO content = new ContentPageVO();
content.setName("摄影师拍照");
deviceList.stream().filter(device -> device.getId().equals(sourceEntity.getDeviceId())).findFirst().ifPresent(device -> {
content.setGroup(device.getName());
});
content.setContentId(sourceEntity.getId());
content.setGoodsType(2);
content.setContentType(2);
content.setScenicId(sourceEntity.getScenicId());
content.setSourceType(2);
content.setOrigUrl(sourceEntity.getUrl());
content.setTemplateCoverUrl(sourceEntity.getThumbUrl());
content.setIsBuy(sourceEntity.getIsBuy());
content.setLockType(-1);
result.add(content);
}
List<Long> containedDeviceId = sourceEntityList.stream().map(SourceEntity::getDeviceId).filter(Objects::nonNull).distinct().toList();
deviceList.stream().filter(device -> !containedDeviceId.contains(device.getId())).forEach(device -> {
ContentPageVO content = new ContentPageVO();
content.setName(device.getName());
content.setGroup(device.getName());
content.setContentId(device.getId());
content.setGoodsType(2);
content.setContentType(2);
content.setScenicId(face.getScenicId());
content.setSourceType(2);
content.setTemplateCoverUrl("");
content.setIsBuy(0);
content.setLockType(1);
result.add(content);
});
return result;
}
List<TemplateRespVO> templateList = templateRepository.getTemplateListByScenicId(face.getScenicId()); List<TemplateRespVO> templateList = templateRepository.getTemplateListByScenicId(face.getScenicId());
List<ContentPageVO> contentList = templateList.stream().map(template -> { List<ContentPageVO> contentList = templateList.stream().map(template -> {
/// select t.id templateId, t.scenic_id, t.`group`, t.`name`, pid, t.cover_url templateCoverUrl, /// select t.id templateId, t.scenic_id, t.`group`, t.`name`, pid, t.cover_url templateCoverUrl,
@@ -607,7 +651,6 @@ public class FaceServiceImpl implements FaceService {
sourceVideoContent.setGroup("直出原片"); sourceVideoContent.setGroup("直出原片");
sourceImageContent.setGroup("直出原片"); sourceImageContent.setGroup("直出原片");
sourceAiCamContent.setGroup("智能连连拍"); sourceAiCamContent.setGroup("智能连连拍");
ScenicConfigManager configManager = scenicRepository.getScenicConfigManager(face.getScenicId());
if (!scenicConfigFacade.isDisableSourceImage(face.getScenicId())) { if (!scenicConfigFacade.isDisableSourceImage(face.getScenicId())) {
IsBuyRespVO isBuyRespVO = orderBiz.isBuy(face.getScenicId(), userId, faceId, SourceType.IMAGE.getCode(), faceId); IsBuyRespVO isBuyRespVO = orderBiz.isBuy(face.getScenicId(), userId, faceId, SourceType.IMAGE.getCode(), faceId);
sourceImageContent.setSourceType(isBuyRespVO.getGoodsType()); sourceImageContent.setSourceType(isBuyRespVO.getGoodsType());
@@ -679,7 +722,7 @@ public class FaceServiceImpl implements FaceService {
} else if (type == 3) { } else if (type == 3) {
sourceAiCamContent.setSourceType(13); sourceAiCamContent.setSourceType(13);
sourceAiCamContent.setLockType(-1); sourceAiCamContent.setLockType(-1);
sourceAiCamContent.setTemplateCoverUrl(configManager.getString("ai_camera_cover_url")); sourceAiCamContent.setTemplateCoverUrl(scenicConfig.getString("ai_camera_cover_url"));
} }
}); });
return contentList; return contentList;
@@ -780,14 +823,31 @@ public class FaceServiceImpl implements FaceService {
sourceReqQuery.setMemberId(face.getMemberId()); sourceReqQuery.setMemberId(face.getMemberId());
sourceReqQuery.setFaceId(faceId); sourceReqQuery.setFaceId(faceId);
sourceReqQuery.setType(2); sourceReqQuery.setType(2);
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(face.getScenicId());
List<MemberSourceEntity> countUser = memberRelationRepository.listSourceByFaceRelation(faceId, 2); List<MemberSourceEntity> countUser = memberRelationRepository.listSourceByFaceRelation(faceId, 2);
if (countUser != null && !countUser.isEmpty()) { if (countUser != null && !countUser.isEmpty()) {
statusResp.setStep2Status(true); statusResp.setStep2Status(true);
} else { } else {
statusResp.setStep2Status(false); statusResp.setStep2Status(false);
if (Integer.valueOf(2).equals(scenicConfig.getInteger("scenic_mode", 0))) {
statusResp.setDisplayText("Hey,去拍摄点免费拍照吧");
} else {
statusResp.setDisplayText("Hey,快去智能机位打卡吧"); statusResp.setDisplayText("Hey,快去智能机位打卡吧");
}
return statusResp; return statusResp;
} }
if (Integer.valueOf(2).equals(scenicConfig.getInteger("scenic_mode", 0))) {
// 摄影模式
if (!countUser.isEmpty()) {
statusResp.setStep3Status(true);
statusResp.setDisplayText("已为您拍摄" + countUser.size() + "张照片");
return statusResp;
} else {
statusResp.setStep3Status(false);
statusResp.setDisplayText("Hey,去拍摄点免费拍照吧");
return statusResp;
}
} else {
VideoTaskStatusVO taskStatusByFaceId = goodsService.getTaskStatusByFaceId(faceId); VideoTaskStatusVO taskStatusByFaceId = goodsService.getTaskStatusByFaceId(faceId);
if (Integer.valueOf(1).equals(taskStatusByFaceId.getStatus())) { if (Integer.valueOf(1).equals(taskStatusByFaceId.getStatus())) {
if (taskStatusByFaceId.getCount() > 0) { if (taskStatusByFaceId.getCount() > 0) {
@@ -801,6 +861,7 @@ public class FaceServiceImpl implements FaceService {
statusResp.setStep3Status(false); statusResp.setStep3Status(false);
statusResp.setDisplayText("帧途AI正在为您渲染vlog,请稍候"); statusResp.setDisplayText("帧途AI正在为您渲染vlog,请稍候");
} }
}
return statusResp; return statusResp;
} }

View File

@@ -930,6 +930,7 @@ public class OrderServiceImpl implements OrderService {
Integer type = switch (productItem.getProductType()) { Integer type = switch (productItem.getProductType()) {
case PHOTO_LOG -> 5; case PHOTO_LOG -> 5;
case PHOTO_SET -> 2; case PHOTO_SET -> 2;
case PHOTO -> 14;
case VLOG_VIDEO -> 0; case VLOG_VIDEO -> 0;
case RECORDING_SET -> 1; case RECORDING_SET -> 1;
case AI_CAM_PHOTO_SET -> 13; case AI_CAM_PHOTO_SET -> 13;
@@ -937,6 +938,7 @@ public class OrderServiceImpl implements OrderService {
}; };
Long goodsId = switch (productItem.getProductType()) { Long goodsId = switch (productItem.getProductType()) {
case PHOTO_LOG -> Long.valueOf(productItem.getProductId()); case PHOTO_LOG -> Long.valueOf(productItem.getProductId());
case PHOTO -> Long.valueOf(productItem.getProductId());
case PHOTO_SET, RECORDING_SET -> face.getId(); case PHOTO_SET, RECORDING_SET -> face.getId();
case AI_CAM_PHOTO_SET -> face.getId(); case AI_CAM_PHOTO_SET -> face.getId();
case VLOG_VIDEO -> { case VLOG_VIDEO -> {

View File

@@ -3,14 +3,17 @@ package com.ycwl.basic.service.pc.impl;
import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo; import com.github.pagehelper.PageInfo;
import com.ycwl.basic.exception.BaseException; import com.ycwl.basic.exception.BaseException;
import com.ycwl.basic.biz.FaceStatusManager;
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO; import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO;
import com.ycwl.basic.mapper.SourceMapper; import com.ycwl.basic.mapper.SourceMapper;
import com.ycwl.basic.model.pc.device.entity.DeviceEntity; import com.ycwl.basic.model.pc.device.entity.DeviceEntity;
import com.ycwl.basic.model.pc.scenic.entity.ScenicEntity; import com.ycwl.basic.model.pc.scenic.entity.ScenicEntity;
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
import com.ycwl.basic.model.pc.source.entity.SourceEntity; import com.ycwl.basic.model.pc.source.entity.SourceEntity;
import com.ycwl.basic.model.pc.source.req.SourceReqQuery; import com.ycwl.basic.model.pc.source.req.SourceReqQuery;
import com.ycwl.basic.model.pc.source.resp.SourceRespVO; import com.ycwl.basic.model.pc.source.resp.SourceRespVO;
import com.ycwl.basic.repository.DeviceRepository; import com.ycwl.basic.repository.DeviceRepository;
import com.ycwl.basic.repository.MemberRelationRepository;
import com.ycwl.basic.repository.ScenicRepository; import com.ycwl.basic.repository.ScenicRepository;
import com.ycwl.basic.repository.SourceRepository; import com.ycwl.basic.repository.SourceRepository;
import com.ycwl.basic.service.pc.ScenicService; import com.ycwl.basic.service.pc.ScenicService;
@@ -29,6 +32,7 @@ import java.io.File;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
@@ -52,6 +56,10 @@ public class SourceServiceImpl implements SourceService {
private ScenicRepository scenicRepository; private ScenicRepository scenicRepository;
@Autowired @Autowired
private DeviceRepository deviceRepository; private DeviceRepository deviceRepository;
@Autowired
private MemberRelationRepository memberRelationRepository;
@Autowired
private FaceStatusManager faceStatusManager;
@Override @Override
public ApiResponse<PageInfo<SourceRespVO>> pageQuery(SourceReqQuery sourceReqQuery) { public ApiResponse<PageInfo<SourceRespVO>> pageQuery(SourceReqQuery sourceReqQuery) {
@@ -201,4 +209,76 @@ public class SourceServiceImpl implements SourceService {
throw new BaseException("文件上传失败: " + e.getMessage()); throw new BaseException("文件上传失败: " + e.getMessage());
} }
} }
@Override
public ApiResponse<Map<Long, Long>> getFaceIdsBySourceIds(List<Long> sourceIds) {
if (sourceIds == null || sourceIds.isEmpty()) {
return ApiResponse.success(Collections.emptyMap());
}
List<MemberSourceEntity> relations = sourceMapper.listFaceIdsBySourceIds(sourceIds);
Map<Long, Long> faceIdMap = relations.stream()
.collect(Collectors.toMap(
MemberSourceEntity::getSourceId,
MemberSourceEntity::getFaceId,
(existing, replacement) -> existing,
LinkedHashMap::new
));
// 对于没有关联的sourceId,填充null
Map<Long, Long> result = new LinkedHashMap<>();
for (Long sourceId : sourceIds) {
result.put(sourceId, faceIdMap.get(sourceId));
}
return ApiResponse.success(result);
}
@Override
public ApiResponse<PageInfo<SourceRespVO>> pageByFaceId(SourceReqQuery sourceReqQuery) {
PageHelper.startPage(sourceReqQuery.getPageNum(), sourceReqQuery.getPageSize());
List<SourceRespVO> list = sourceMapper.pageByFaceId(sourceReqQuery);
PageInfo<SourceRespVO> pageInfo = new PageInfo<>(list);
return ApiResponse.success(pageInfo);
}
@Override
public ApiResponse<Void> cancelRelation(Long id) {
MemberSourceEntity entity = sourceMapper.getMemberSourceById(id);
if (entity == null) {
return ApiResponse.fail("关联记录不存在");
}
int rows = sourceMapper.softDeleteRelation(id);
if (rows == 0) {
return ApiResponse.fail("记录已取消或不存在");
}
invalidateCacheByFace(entity.getFaceId());
return ApiResponse.success(null);
}
@Override
public ApiResponse<Void> reactivateRelation(Long id) {
MemberSourceEntity entity = sourceMapper.getMemberSourceById(id);
if (entity == null) {
return ApiResponse.fail("关联记录不存在");
}
int rows = sourceMapper.reactivateRelation(id);
if (rows == 0) {
return ApiResponse.fail("记录未处于取消状态");
}
invalidateCacheByFace(entity.getFaceId());
return ApiResponse.success(null);
}
@Override
public ApiResponse<PageInfo<SourceRespVO>> pageDeletedByFaceId(SourceReqQuery sourceReqQuery) {
PageHelper.startPage(sourceReqQuery.getPageNum(), sourceReqQuery.getPageSize());
List<SourceRespVO> list = sourceMapper.pageDeletedByFaceId(sourceReqQuery);
PageInfo<SourceRespVO> pageInfo = new PageInfo<>(list);
return ApiResponse.success(pageInfo);
}
private void invalidateCacheByFace(Long faceId) {
if (faceId != null) {
memberRelationRepository.clearSCacheByFace(faceId);
faceStatusManager.invalidatePuzzleSourceVersion(faceId);
}
}
} }

View File

@@ -143,4 +143,56 @@ public interface PrinterService {
* @return 订单信息 * @return 订单信息
*/ */
Map<String, Object> createVirtualOrder(Long sourceId, Long scenicId, Integer printerId, Boolean needEnhance, String printImgUrl); Map<String, Object> createVirtualOrder(Long sourceId, Long scenicId, Integer printerId, Boolean needEnhance, String printImgUrl);
/**
* 创建虚拟用户订单(支持实际支付模式)
*
* @param sourceId source记录ID
* @param scenicId 景区ID
* @param printerId 打印机ID(可选)
* @param needEnhance 是否需要图像增强(可选)
* @param printImgUrl 打印图片URL(可选)
* @param needActualPayment 是否需要实际支付(true: 创建待支付订单, false/null: 0元立即购买)
* @return 订单信息
*/
Map<String, Object> createVirtualOrder(Long sourceId, Long scenicId, Integer printerId, Boolean needEnhance, String printImgUrl, Boolean needActualPayment);
/**
* 批量创建虚拟用户订单(多个sourceId聚合为一笔订单、一次支付)
*
* @param sourceIds source记录ID列表
* @param scenicId 景区ID
* @param printerId 打印机ID(可选)
* @param needEnhance 是否需要图像增强(可选)
* @param printImgUrl 打印图片URL(可选)
* @param needActualPayment 是否需要实际支付
* @return 订单信息
*/
Map<String, Object> createBatchVirtualOrder(List<Long> sourceIds, Long scenicId, Integer printerId, Boolean needEnhance, String printImgUrl, Boolean needActualPayment);
/**
* 根据accessKey获取打印机详情
* @param accessKey 打印机accessKey
* @return 打印机实体
*/
PrinterEntity getByAccessKey(String accessKey);
/**
* 根据accessKey获取打印机对应的景区基础信息
* @param accessKey 打印机accessKey
* @return 景区基础信息
*/
Object getScenicBasicByAccessKey(String accessKey);
/**
* 打开打印机(设置status=1)
* @param accessKey 打印机accessKey
*/
void openPrinter(String accessKey);
/**
* 关闭打印机(设置status=0)
* @param accessKey 打印机accessKey
*/
void closePrinter(String accessKey);
} }

View File

@@ -1,5 +1,12 @@
package com.ycwl.basic.service.printer.impl; package com.ycwl.basic.service.printer.impl;
import com.wechat.pay.java.service.payments.nativepay.NativePayService;
import com.wechat.pay.java.service.payments.nativepay.model.Amount;
import com.wechat.pay.java.service.payments.nativepay.model.PrepayRequest;
import com.wechat.pay.java.service.payments.nativepay.model.PrepayResponse;
import com.ycwl.basic.pay.adapter.IPayAdapter;
import com.ycwl.basic.pay.adapter.WxMpPayAdapter;
import com.ycwl.basic.service.pc.ScenicService;
import com.ycwl.basic.biz.OrderBiz; import com.ycwl.basic.biz.OrderBiz;
import com.ycwl.basic.constant.NumberConstant; import com.ycwl.basic.constant.NumberConstant;
import com.ycwl.basic.enums.OrderStateEnum; import com.ycwl.basic.enums.OrderStateEnum;
@@ -162,6 +169,9 @@ public class PrinterServiceImpl implements PrinterService {
@Autowired @Autowired
@Lazy @Lazy
private WatermarkEdgeService watermarkEdgeService; private WatermarkEdgeService watermarkEdgeService;
@Autowired
@Lazy
private ScenicService scenicService;
@Override @Override
public List<PrinterResp> listByScenicId(Long scenicId) { public List<PrinterResp> listByScenicId(Long scenicId) {
@@ -343,6 +353,14 @@ public class PrinterServiceImpl implements PrinterService {
log.debug("打印机高度未配置或无效,使用默认值: height={}", printHeight); log.debug("打印机高度未配置或无效,使用默认值: height={}", printHeight);
} }
// 检测原图方向
boolean isLandscape = false;
try {
isLandscape = ImageUtils.isLandscape(url);
} catch (Exception e) {
log.warn("检测图片方向失败,默认为竖图: url={}", url, e);
}
// 使用smartCropAndFill裁剪图片 // 使用smartCropAndFill裁剪图片
File croppedFile = ImageUtils.smartCropAndFill(url, printWidth, printHeight); File croppedFile = ImageUtils.smartCropAndFill(url, printWidth, printHeight);
@@ -355,8 +373,9 @@ public class PrinterServiceImpl implements PrinterService {
log.info("照片裁剪成功: memberId={}, scenicId={}, 原图={}, 裁剪后={}, 尺寸={}x{}", log.info("照片裁剪成功: memberId={}, scenicId={}, 原图={}, 裁剪后={}, 尺寸={}x{}",
memberId, scenicId, url, cropUrl, printWidth, printHeight); memberId, scenicId, url, cropUrl, printWidth, printHeight);
String crop = JacksonUtil.toJSONString(new Crop(270)); if (isLandscape) {
entity.setCrop(crop); entity.setCrop(JacksonUtil.toJSONString(new Crop(270)));
}
} finally { } finally {
// 清理临时文件 // 清理临时文件
if (croppedFile != null && croppedFile.exists()) { if (croppedFile != null && croppedFile.exists()) {
@@ -627,9 +646,19 @@ public class PrinterServiceImpl implements PrinterService {
log.debug("打印机高度未配置或无效,使用默认值: height={}", printHeight); log.debug("打印机高度未配置或无效,使用默认值: height={}", printHeight);
} }
// 检测原图方向
boolean isLandscape = false;
try {
isLandscape = ImageUtils.isLandscape(url);
} catch (Exception e) {
log.warn("检测图片方向失败,默认为竖图: url={}", url, e);
}
// 使用smartCropAndFill裁剪图片 // 使用smartCropAndFill裁剪图片
File croppedFile = ImageUtils.smartCropAndFill(url, printWidth, printHeight); File croppedFile = ImageUtils.smartCropAndFill(url, printWidth, printHeight);
if (isLandscape) {
entity.setCrop(JacksonUtil.toJSONString(new Crop(270))); entity.setCrop(JacksonUtil.toJSONString(new Crop(270)));
}
try { try {
// 上传裁剪后的图片 // 上传裁剪后的图片
@@ -1055,8 +1084,10 @@ public class PrinterServiceImpl implements PrinterService {
String scenicText = scenicConfig.getString("print_watermark_scenic_text", ""); String scenicText = scenicConfig.getString("print_watermark_scenic_text", "");
String dateFormat = scenicConfig.getString("print_watermark_dt_format", "yyyy.MM.dd"); String dateFormat = scenicConfig.getString("print_watermark_dt_format", "yyyy.MM.dd");
String printWatermarkPUrl = scenicConfig.getString("print_watermark_p_url", null);
String printWatermarkLUrl = scenicConfig.getString("print_watermark_l_url", null);
return WatermarkConfig.builder() WatermarkConfig.WatermarkConfigBuilder builder = WatermarkConfig.builder()
.watermarkType(watermarkType) .watermarkType(watermarkType)
.scenicText(scenicText) .scenicText(scenicText)
.dateFormat(dateFormat) .dateFormat(dateFormat)
@@ -1064,7 +1095,14 @@ public class PrinterServiceImpl implements PrinterService {
.storageAdapter(StorageFactory.use()) .storageAdapter(StorageFactory.use())
.edgeEnabled(true) .edgeEnabled(true)
.qrcodeFile(qrCodeFile) .qrcodeFile(qrCodeFile)
.scale(scale) .scale(scale);
if (context.getSource() == ImageSource.IPC) {
return builder
.printWatermarkPUrlList(Collections.singletonList(printWatermarkPUrl))
.printWatermarkLUrlList(Collections.singletonList(printWatermarkLUrl))
.build();
}
return builder
.build(); .build();
} }
@@ -1126,7 +1164,14 @@ public class PrinterServiceImpl implements PrinterService {
@Override @Override
public void setUserIsBuyItem(Long memberId, Long id, Long orderId) { public void setUserIsBuyItem(Long memberId, Long id, Long orderId) {
setUserIsBuyItem(memberId, id, orderId, null); // 尝试从 Redis 读取虚拟订单存储的 needEnhance 配置
Boolean needEnhance = null;
String enhanceFlag = redisTemplate.opsForValue().get("virtual_order_enhance:" + orderId);
if (enhanceFlag != null) {
needEnhance = Boolean.parseBoolean(enhanceFlag);
redisTemplate.delete("virtual_order_enhance:" + orderId);
}
setUserIsBuyItem(memberId, id, orderId, needEnhance);
} }
@Override @Override
@@ -1134,7 +1179,7 @@ public class PrinterServiceImpl implements PrinterService {
if (redisTemplate.opsForValue().get(USER_PHOTO_LIST_TO_PRINTER + memberId + ":" + orderId) != null) { if (redisTemplate.opsForValue().get(USER_PHOTO_LIST_TO_PRINTER + memberId + ":" + orderId) != null) {
return; return;
} }
redisTemplate.opsForValue().set(USER_PHOTO_LIST_TO_PRINTER + memberId + ":" + orderId, "1", 60, TimeUnit.SECONDS); redisTemplate.opsForValue().set(USER_PHOTO_LIST_TO_PRINTER + memberId + ":" + orderId, "1", 24, TimeUnit.HOURS);
OrderEntity order = orderRepository.getOrder(orderId); OrderEntity order = orderRepository.getOrder(orderId);
List<OrderItemEntity> orderItems = orderMapper.getOrderItems(orderId); List<OrderItemEntity> orderItems = orderMapper.getOrderItems(orderId);
orderItems.forEach(item -> { orderItems.forEach(item -> {
@@ -1680,16 +1725,21 @@ public class PrinterServiceImpl implements PrinterService {
@Override @Override
public Map<String, Object> createVirtualOrder(Long sourceId, Long scenicId, Integer printerId) { public Map<String, Object> createVirtualOrder(Long sourceId, Long scenicId, Integer printerId) {
return createVirtualOrder(sourceId, scenicId, printerId, null, null); return createVirtualOrder(sourceId, scenicId, printerId, null, null, null);
} }
@Override @Override
public Map<String, Object> createVirtualOrder(Long sourceId, Long scenicId, Integer printerId, Boolean needEnhance) { public Map<String, Object> createVirtualOrder(Long sourceId, Long scenicId, Integer printerId, Boolean needEnhance) {
return createVirtualOrder(sourceId, scenicId, printerId, needEnhance, null); return createVirtualOrder(sourceId, scenicId, printerId, needEnhance, null, null);
} }
@Override @Override
public Map<String, Object> createVirtualOrder(Long sourceId, Long scenicId, Integer printerId, Boolean needEnhance, String printImgUrl) { public Map<String, Object> createVirtualOrder(Long sourceId, Long scenicId, Integer printerId, Boolean needEnhance, String printImgUrl) {
return createVirtualOrder(sourceId, scenicId, printerId, needEnhance, printImgUrl, null);
}
@Override
public Map<String, Object> createVirtualOrder(Long sourceId, Long scenicId, Integer printerId, Boolean needEnhance, String printImgUrl, Boolean needActualPayment) {
// 1. 查询source记录 // 1. 查询source记录
SourceEntity source = sourceMapper.getEntity(sourceId); SourceEntity source = sourceMapper.getEntity(sourceId);
FaceSampleEntity faceSample = faceSampleMapper.getEntity(source.getFaceSampleId()); FaceSampleEntity faceSample = faceSampleMapper.getEntity(source.getFaceSampleId());
@@ -1752,7 +1802,7 @@ public class PrinterServiceImpl implements PrinterService {
throw new BaseException("打印机不属于该景区"); throw new BaseException("打印机不属于该景区");
} }
// 6. 创建0元订单 // 6. 创建订单
OrderEntity order = new OrderEntity(); OrderEntity order = new OrderEntity();
Long orderId = SnowFlakeUtil.getLongId(); Long orderId = SnowFlakeUtil.getLongId();
redisTemplate.opsForValue().set("printer_size:" + orderId, printer.getPreferPaper(), 60, TimeUnit.SECONDS); redisTemplate.opsForValue().set("printer_size:" + orderId, printer.getPreferPaper(), 60, TimeUnit.SECONDS);
@@ -1776,13 +1826,52 @@ public class PrinterServiceImpl implements PrinterService {
return orderItem; return orderItem;
}).collect(Collectors.toList()); }).collect(Collectors.toList());
// 设置价格为0 boolean actualPayment = Boolean.TRUE.equals(needActualPayment);
if (actualPayment) {
// 需要实际支付:通过价格计算服务获取真实价格
PriceCalculationRequest priceRequest = new PriceCalculationRequest();
priceRequest.setUserId(virtualMemberId);
priceRequest.setScenicId(scenicId);
List<ProductItem> productItems = new ArrayList<>();
ProductItem photoItem = new ProductItem();
photoItem.setProductType(ProductType.PHOTO_PRINT);
photoItem.setProductId(scenicId.toString());
photoItem.setQuantity(1);
photoItem.setPurchaseCount(1);
photoItem.setScenicId(scenicId.toString());
// 通过 source 的 deviceId 设置 attributeKeys
SourceEntity priceSource = sourceRepository.getSource(sourceId);
if (priceSource != null && priceSource.getDeviceId() != null) {
photoItem.setAttributeKeys(List.of(String.valueOf(priceSource.getDeviceId())));
}
productItems.add(photoItem);
priceRequest.setProducts(productItems);
priceRequest.setAutoUseCoupon(false);
priceRequest.setPreviewOnly(false);
PriceCalculationResult priceResult = priceCalculationService.calculatePrice(priceRequest);
order.setPrice(priceResult.getFinalAmount());
order.setSlashPrice(priceResult.getOriginalAmount());
order.setPayPrice(priceResult.getFinalAmount());
order.setStatus(OrderStateEnum.UNPAID.getState());
log.info("创建待支付虚拟订单: orderId={}, price={}", orderId, priceResult.getFinalAmount());
// 将 needEnhance 存入 Redis,支付完成后 setUserIsBuyItem 可读取
if (needEnhance != null) {
redisTemplate.opsForValue().set("virtual_order_enhance:" + orderId, needEnhance.toString(), 24, TimeUnit.HOURS);
}
} else {
// 虚拟0元购买:价格为0,直接标记已支付
order.setPrice(BigDecimal.ZERO); order.setPrice(BigDecimal.ZERO);
order.setSlashPrice(BigDecimal.ZERO); order.setSlashPrice(BigDecimal.ZERO);
order.setPayPrice(BigDecimal.ZERO); order.setPayPrice(BigDecimal.ZERO);
order.setFaceId(faceId);
order.setStatus(OrderStateEnum.PAID.getState()); order.setStatus(OrderStateEnum.PAID.getState());
order.setPayAt(new Date()); order.setPayAt(new Date());
}
// 保存订单 // 保存订单
orderMapper.add(order); orderMapper.add(order);
@@ -1792,21 +1881,277 @@ public class PrinterServiceImpl implements PrinterService {
throw new BaseException("订单添加失败"); throw new BaseException("订单添加失败");
} }
log.info("创建0元订单成功: orderId={}, virtualMemberId={}, faceId={}", orderId, virtualMemberId, faceId); log.info("创建虚拟订单成功: orderId={}, virtualMemberId={}, faceId={}, actualPayment={}", orderId, virtualMemberId, faceId, actualPayment);
// 7. 触发购买后逻辑(调用setUserIsBuyItem)
setUserIsBuyItem(virtualMemberId, memberPrintId.longValue(), orderId, needEnhance);
log.info("触发购买后逻辑完成: orderId={}", orderId);
// 8. 返回结果
Map<String, Object> result = new HashMap<>(); Map<String, Object> result = new HashMap<>();
result.put("orderId", orderId); result.put("orderId", orderId);
result.put("faceId", faceId); result.put("faceId", faceId);
result.put("virtualMemberId", virtualMemberId); result.put("virtualMemberId", virtualMemberId);
result.put("memberPrintId", memberPrintId); result.put("memberPrintId", memberPrintId);
if (actualPayment) {
if (order.getPayPrice().compareTo(BigDecimal.ZERO) <= 0) {
// 计算后价格为0,直接走免费逻辑
order.setStatus(OrderStateEnum.PAID.getState());
order.setPayAt(new Date());
orderMapper.updateOrder(order);
log.info("待支付订单计算后价格为0,直接完成购买: orderId={}", orderId);
result.put("needPay", false); result.put("needPay", false);
} else {
// 通过 Native 支付生成二维码
IPayAdapter payAdapter = scenicService.getScenicPayAdapter(scenicId);
if (payAdapter instanceof WxMpPayAdapter adapter) {
NativePayService nativePayService = new NativePayService.Builder().config(adapter.getConfig()).build();
PrepayRequest prepayRequest = new PrepayRequest();
prepayRequest.setAppid(adapter._config().getAppId());
prepayRequest.setMchid(adapter._config().getMerchantId());
prepayRequest.setDescription("照片打印");
prepayRequest.setOutTradeNo(String.valueOf(orderId));
prepayRequest.setNotifyUrl("https://zhentuai.com/api/mobile/wx/pay/v1/" + scenicId + "/payNotify");
Amount amount = new Amount();
amount.setTotal(order.getPayPrice().multiply(new BigDecimal(100)).intValue());
prepayRequest.setAmount(amount);
PrepayResponse prepayResponse = nativePayService.prepay(prepayRequest);
result.put("payCode", prepayResponse.getCodeUrl());
} else {
throw new BaseException("该景区不支持 Native 支付");
}
result.put("needPay", true);
result.put("price", order.getPayPrice());
}
} else {
result.put("needPay", false);
}
// 无论是否需要支付,都立即触发购买后动作(打印等)
// setUserIsBuyItem 内部通过 Redis 去重,支付回调到达时不会重复触发
setUserIsBuyItem(virtualMemberId, memberPrintId.longValue(), orderId, needEnhance);
log.info("触发购买后逻辑完成: orderId={}", orderId);
return result; return result;
} }
@Override
public Map<String, Object> createBatchVirtualOrder(List<Long> sourceIds, Long scenicId, Integer printerId, Boolean needEnhance, String printImgUrl, Boolean needActualPayment) {
if (sourceIds == null || sourceIds.isEmpty()) {
throw new BaseException("sourceIds不能为空");
}
// 1. 校验所有source并收集faceSample
List<SourceEntity> sources = new ArrayList<>();
FaceSampleEntity firstFaceSample = null;
for (Long sourceId : sourceIds) {
SourceEntity source = sourceMapper.getEntity(sourceId);
if (source == null) {
throw new BaseException("Source记录不存在: " + sourceId);
}
if (!scenicId.equals(source.getScenicId())) {
throw new BaseException("Source记录不属于该景区: " + sourceId);
}
FaceSampleEntity faceSample = faceSampleMapper.getEntity(source.getFaceSampleId());
if (faceSample == null) {
throw new BaseException("人脸样本不存在, sourceId=" + sourceId);
}
if (firstFaceSample == null) {
firstFaceSample = faceSample;
}
sources.add(source);
}
// 2. 生成一个虚拟用户 + 一条人脸记录
Long virtualMemberId = SnowFlakeUtil.getLongId();
Long faceId = SnowFlakeUtil.getLongId();
FaceEntity face = new FaceEntity();
face.setId(faceId);
face.setScenicId(scenicId);
face.setMemberId(virtualMemberId);
face.setFaceUrl(firstFaceSample.getFaceUrl());
face.setCreateAt(new Date());
faceMapper.add(face);
log.info("批量下单 - 创建虚拟用户: virtualMemberId={}, faceId={}, sourceCount={}", virtualMemberId, faceId, sourceIds.size());
// 3. 为每个source创建member_print记录
List<Integer> memberPrintIds = new ArrayList<>();
for (SourceEntity source : sources) {
String photoUrl = (printImgUrl != null && !printImgUrl.isEmpty()) ? printImgUrl : source.getUrl();
Integer memberPrintId = addUserPhoto(virtualMemberId, scenicId, photoUrl, faceId, source.getId());
if (memberPrintId == null) {
throw new BaseException("创建member_print记录失败, sourceId=" + source.getId());
}
setPhotoQuantity(virtualMemberId, scenicId, memberPrintId.longValue(), 1);
memberPrintIds.add(memberPrintId);
}
// 4. 验证打印机
if (printerId == null) {
List<PrinterResp> printerList = printerMapper.listByScenicId(scenicId);
if (printerList.isEmpty()) {
throw new BaseException("该景区没有可用的打印机");
}
if (printerList.size() != 1) {
throw new BaseException("请选择打印机");
}
printerId = printerList.getFirst().getId();
}
PrinterEntity printer = printerMapper.getById(printerId);
if (printer == null) {
throw new BaseException("打印机不存在");
}
if (printer.getStatus() != 1) {
throw new BaseException("打印机已停用");
}
if (!printer.getScenicId().equals(scenicId)) {
throw new BaseException("打印机不属于该景区");
}
// 5. 创建订单
OrderEntity order = new OrderEntity();
Long orderId = SnowFlakeUtil.getLongId();
redisTemplate.opsForValue().set("printer_size:" + orderId, printer.getPreferPaper(), 60, TimeUnit.SECONDS);
order.setId(orderId);
order.setMemberId(virtualMemberId);
order.setFaceId(faceId);
order.setOpenId("");
order.setScenicId(scenicId);
order.setType(3);
batchSetUserPhotoListToPrinter(virtualMemberId, scenicId, printerId);
List<MemberPrintResp> userPhotoList = getUserPhotoList(virtualMemberId, scenicId, faceId);
List<OrderItemEntity> orderItems = userPhotoList.stream().map(goods -> {
OrderItemEntity orderItem = new OrderItemEntity();
orderItem.setOrderId(orderId);
orderItem.setGoodsId(Long.valueOf(goods.getId()));
orderItem.setGoodsType(3);
return orderItem;
}).collect(Collectors.toList());
boolean actualPayment = Boolean.TRUE.equals(needActualPayment);
if (actualPayment) {
PriceCalculationRequest priceRequest = new PriceCalculationRequest();
priceRequest.setUserId(virtualMemberId);
priceRequest.setScenicId(scenicId);
List<ProductItem> productItems = new ArrayList<>();
ProductItem photoItem = new ProductItem();
photoItem.setProductType(ProductType.PHOTO_PRINT);
photoItem.setProductId(scenicId.toString());
photoItem.setQuantity(sourceIds.size());
photoItem.setPurchaseCount(sourceIds.size());
photoItem.setScenicId(scenicId.toString());
productItems.add(photoItem);
priceRequest.setProducts(productItems);
priceRequest.setAutoUseCoupon(false);
priceRequest.setPreviewOnly(false);
PriceCalculationResult priceResult = priceCalculationService.calculatePrice(priceRequest);
order.setPrice(priceResult.getFinalAmount());
order.setSlashPrice(priceResult.getOriginalAmount());
order.setPayPrice(priceResult.getFinalAmount());
order.setStatus(OrderStateEnum.UNPAID.getState());
log.info("批量下单 - 待支付订单: orderId={}, price={}, count={}", orderId, priceResult.getFinalAmount(), sourceIds.size());
if (needEnhance != null) {
redisTemplate.opsForValue().set("virtual_order_enhance:" + orderId, needEnhance.toString(), 24, TimeUnit.HOURS);
}
} else {
order.setPrice(BigDecimal.ZERO);
order.setSlashPrice(BigDecimal.ZERO);
order.setPayPrice(BigDecimal.ZERO);
order.setStatus(OrderStateEnum.PAID.getState());
order.setPayAt(new Date());
}
orderMapper.add(order);
int addOrderItems = orderMapper.addOrderItems(orderItems);
if (addOrderItems == NumberConstant.ZERO) {
throw new BaseException("订单添加失败");
}
log.info("批量下单 - 订单创建成功: orderId={}, itemCount={}", orderId, orderItems.size());
Map<String, Object> result = new HashMap<>();
result.put("orderId", orderId);
result.put("faceId", faceId);
result.put("virtualMemberId", virtualMemberId);
result.put("memberPrintIds", memberPrintIds);
result.put("sourceIds", sourceIds);
if (actualPayment) {
if (order.getPayPrice().compareTo(BigDecimal.ZERO) <= 0) {
order.setStatus(OrderStateEnum.PAID.getState());
order.setPayAt(new Date());
orderMapper.updateOrder(order);
log.info("批量下单 - 价格为0直接完成: orderId={}", orderId);
result.put("needPay", false);
} else {
IPayAdapter payAdapter = scenicService.getScenicPayAdapter(scenicId);
if (payAdapter instanceof WxMpPayAdapter adapter) {
NativePayService nativePayService = new NativePayService.Builder().config(adapter.getConfig()).build();
PrepayRequest prepayRequest = new PrepayRequest();
prepayRequest.setAppid(adapter._config().getAppId());
prepayRequest.setMchid(adapter._config().getMerchantId());
prepayRequest.setDescription("照片打印 x" + sourceIds.size());
prepayRequest.setOutTradeNo(String.valueOf(orderId));
prepayRequest.setNotifyUrl("https://zhentuai.com/api/mobile/wx/pay/v1/" + scenicId + "/payNotify");
Amount amount = new Amount();
amount.setTotal(order.getPayPrice().multiply(new BigDecimal(100)).intValue());
prepayRequest.setAmount(amount);
PrepayResponse prepayResponse = nativePayService.prepay(prepayRequest);
result.put("payCode", prepayResponse.getCodeUrl());
} else {
throw new BaseException("该景区不支持 Native 支付");
}
result.put("needPay", true);
result.put("price", order.getPayPrice());
}
} else {
result.put("needPay", false);
}
// 触发购买后逻辑(setUserIsBuyItem 内部遍历 orderItems 处理所有 memberPrint)
setUserIsBuyItem(virtualMemberId, memberPrintIds.getFirst().longValue(), orderId, needEnhance);
log.info("批量下单 - 购买后逻辑完成: orderId={}", orderId);
return result;
}
@Override
public PrinterEntity getByAccessKey(String accessKey) {
if (accessKey == null) {
throw new BaseException("accessKey不能为空");
}
PrinterEntity printer = printerMapper.findByAccessKey(accessKey);
if (printer == null) {
throw new BaseException("打印机不存在");
}
return printer;
}
@Override
public Object getScenicBasicByAccessKey(String accessKey) {
PrinterEntity printer = getByAccessKey(accessKey);
if (printer.getScenicId() == null) {
throw new BaseException("打印机未关联景区");
}
return scenicRepository.getScenicBasic(printer.getScenicId());
}
@Override
public void openPrinter(String accessKey) {
PrinterEntity printer = getByAccessKey(accessKey);
printer.setStatus(1);
printerMapper.update(printer);
}
@Override
public void closePrinter(String accessKey) {
PrinterEntity printer = getByAccessKey(accessKey);
printer.setStatus(0);
printerMapper.update(printer);
}
} }

View File

@@ -0,0 +1,442 @@
package com.ycwl.basic.service.task;
import com.ycwl.basic.biz.FaceStatusManager;
import com.ycwl.basic.enums.TemplateRenderStatus;
import com.ycwl.basic.integration.render.dto.job.FinalizeMP4Response;
import com.ycwl.basic.integration.render.dto.job.JobStatusResponse;
import com.ycwl.basic.integration.render.service.RenderJobIntegrationService;
import com.ycwl.basic.mapper.TaskMapper;
import com.ycwl.basic.mapper.VideoMapper;
import com.ycwl.basic.mapper.task.TaskRenderJobMappingMapper;
import com.ycwl.basic.model.pc.task.entity.TaskEntity;
import com.ycwl.basic.model.pc.video.entity.VideoEntity;
import com.ycwl.basic.model.task.entity.TaskRenderJobMappingEntity;
import com.ycwl.basic.repository.MemberRelationRepository;
import com.ycwl.basic.repository.VideoRepository;
import com.ycwl.basic.repository.VideoTaskRepository;
import com.ycwl.basic.utils.SnowFlakeUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.annotation.Profile;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
/**
* 渲染作业轮询服务
* 定时查询zt-render-worker服务中的渲染作业状态,并更新本地task状态
*
* 状态流转:
* PENDING → PREVIEW_READY → MP4_COMPOSING → COMPLETED
* │ │ │ │
* └────────────┴──────────────┴──────────────┴──→ FAILED
*/
@Slf4j
@Service
@EnableScheduling
@RequiredArgsConstructor
@Profile({"prod"}) // 开发和生产环境启用
public class RenderJobPollingService {
private final TaskRenderJobMappingMapper mappingMapper;
private final RenderJobIntegrationService renderJobService;
private final TaskMapper taskMapper;
private final VideoMapper videoMapper;
private final VideoTaskRepository videoTaskRepository;
private final VideoRepository videoRepository;
private final MemberRelationRepository memberRelationRepository;
private final FaceStatusManager faceStatusManager;
/**
* 定时轮询间隔:1+1=2秒
*/
private static final int POLL_INTERVAL_SECONDS = 1;
/**
* 每次查询的最大记录数
*/
private static final int BATCH_SIZE = 50;
/**
* 定时轮询渲染作业状态
* 每1秒执行一次
*/
@Scheduled(fixedDelay = 1000)
public void pollRenderJobs() {
try {
log.debug("[渲染轮询] 开始轮询渲染作业状态");
// 查询待轮询的记录(包含MP4_COMPOSING状态)
List<String> pendingStatuses = Arrays.asList(
TaskRenderJobMappingEntity.STATUS_PENDING,
TaskRenderJobMappingEntity.STATUS_PREVIEW_READY,
TaskRenderJobMappingEntity.STATUS_MP4_COMPOSING
);
List<TaskRenderJobMappingEntity> mappings = mappingMapper.selectPendingForPolling(
pendingStatuses,
POLL_INTERVAL_SECONDS,
BATCH_SIZE
);
if (mappings.isEmpty()) {
log.debug("[渲染轮询] 无待处理记录");
return;
}
log.info("[渲染轮询] 查询到 {} 条待处理记录", mappings.size());
// 处理每条记录
for (TaskRenderJobMappingEntity mapping : mappings) {
try {
processMapping(mapping);
} catch (Exception e) {
log.error("[渲染轮询] 处理失败, mappingId: {}, taskId: {}, renderJobId: {}, error: {}",
mapping.getId(), mapping.getTaskId(), mapping.getRenderJobId(), e.getMessage(), e);
handleProcessError(mapping, e);
}
}
log.debug("[渲染轮询] 轮询完成");
} catch (Exception e) {
log.error("[渲染轮询] 轮询异常", e);
}
}
/**
* 处理单条mapping记录
*/
@Transactional(rollbackFor = Exception.class)
public void processMapping(TaskRenderJobMappingEntity mapping) {
Long renderJobId = mapping.getRenderJobId();
Long taskId = mapping.getTaskId();
String currentStatus = mapping.getRenderStatus();
log.debug("[渲染轮询] 处理记录: mappingId={}, taskId={}, renderJobId={}, currentStatus={}",
mapping.getId(), taskId, renderJobId, currentStatus);
// 查询渲染作业状态
JobStatusResponse jobStatus;
try {
jobStatus = renderJobService.getJobStatus(renderJobId);
} catch (Exception e) {
log.warn("[渲染轮询] 查询作业状态失败, renderJobId: {}, error: {}", renderJobId, e.getMessage());
// 注:此处不调用incrementRetryCount,因为@Transactional会回滚
// 外层handleProcessError会负责增加重试次数
throw e;
}
// 检查作业状态
String status = jobStatus.getStatus();
Integer publishedCount = jobStatus.getPublishedCount();
Integer segmentCount = jobStatus.getSegmentCount();
String playUrl = jobStatus.getPlayUrl();
String mp4Url = jobStatus.getMp4Url();
log.info("[渲染轮询] 作业状态: taskId={}, status={}, publishedCount={}/{}, playUrl={}, mp4Url={}",
taskId, status, publishedCount, segmentCount, playUrl, mp4Url);
// 处理失败状态
if ("FAILED".equals(status) || "CANCELED".equals(status)) {
handleJobFailed(mapping, jobStatus);
return;
}
// 状态流转处理
switch (currentStatus) {
case TaskRenderJobMappingEntity.STATUS_PENDING:
handlePendingStatus(mapping, jobStatus, taskId);
break;
case TaskRenderJobMappingEntity.STATUS_PREVIEW_READY:
handlePreviewReadyStatus(mapping, jobStatus, taskId);
break;
case TaskRenderJobMappingEntity.STATUS_MP4_COMPOSING:
handleMp4ComposingStatus(mapping, jobStatus, taskId);
break;
default:
log.warn("[渲染轮询] 未知状态: {}", currentStatus);
}
}
/**
* 处理PENDING状态
* PENDING → PREVIEW_READY:当publishedCount >= 2时
*/
private void handlePendingStatus(TaskRenderJobMappingEntity mapping, JobStatusResponse jobStatus, Long taskId) {
Integer publishedCount = jobStatus.getPublishedCount();
Integer segmentCount = jobStatus.getSegmentCount();
String playUrl = jobStatus.getPlayUrl();
if (publishedCount != null && publishedCount >= TaskRenderJobMappingEntity.MIN_PUBLISHED_FOR_PREVIEW) {
log.info("[渲染轮询] 预览就绪: taskId={}, publishedCount={}/{}, playUrl={}",
taskId, publishedCount, segmentCount, playUrl);
// 更新mapping状态为PREVIEW_READY
updateMappingStatus(mapping.getId(), TaskRenderJobMappingEntity.STATUS_PREVIEW_READY,
publishedCount, segmentCount, playUrl, null);
// 更新模板渲染状态缓存为已渲染(预览就绪即视为渲染成功)
updateTemplateRenderStatus(taskId, TemplateRenderStatus.RENDERED);
// 更新task的videoUrl为预览地址
if (StringUtils.isNotBlank(playUrl)) {
TaskEntity task = new TaskEntity();
task.setId(taskId);
task.setVideoUrl(playUrl);
task.setStatus(1); // 设置为完成状态
taskMapper.update(task);
videoTaskRepository.clearTaskCache(taskId);
log.info("[渲染轮询] 已更新task预览URL和状态: taskId={}, playUrl={}, status=1", taskId, playUrl);
// 处理video记录(类似taskSuccess逻辑)
try {
handleVideoRecordForPreview(taskId, playUrl);
} catch (Exception e) {
log.warn("[渲染轮询] 处理video记录失败: taskId={}, error={}", taskId, e.getMessage(), e);
}
// 异步发送视频生成通知(仅记录日志,实际通知可能需要在MP4完成后)
log.info("[渲染轮询] 预览视频已就绪,可发送通知: taskId={}", taskId);
}
} else {
// 更新片段信息
updateMappingStatus(mapping.getId(), TaskRenderJobMappingEntity.STATUS_PENDING,
publishedCount, segmentCount, null, null);
}
}
/**
* 处理PREVIEW_READY状态
* PREVIEW_READY → MP4_COMPOSING:当所有片段都已发布时,调用finalize-mp4接口
*/
private void handlePreviewReadyStatus(TaskRenderJobMappingEntity mapping, JobStatusResponse jobStatus, Long taskId) {
Integer publishedCount = jobStatus.getPublishedCount();
Integer segmentCount = jobStatus.getSegmentCount();
String playUrl = jobStatus.getPlayUrl();
Long renderJobId = mapping.getRenderJobId();
// 检查是否所有片段都已发布
if (publishedCount != null && segmentCount != null && publishedCount.equals(segmentCount) && segmentCount > 0) {
log.info("[渲染轮询] 所有片段已发布,开始创建MP4合成任务: taskId={}, renderJobId={}, publishedCount={}/{}",
taskId, renderJobId, publishedCount, segmentCount);
try {
// 调用finalize-mp4接口创建MP4合成任务
FinalizeMP4Response response = renderJobService.createFinalizeMP4Task(renderJobId);
log.info("[渲染轮询] MP4合成任务创建成功: taskId={}, renderJobId={}, mp4TaskId={}, status={}",
taskId, renderJobId, response.getTaskId(), response.getStatus());
// 更新mapping状态为MP4_COMPOSING
updateMappingStatus(mapping.getId(), TaskRenderJobMappingEntity.STATUS_MP4_COMPOSING,
publishedCount, segmentCount, playUrl, null);
} catch (Exception e) {
// 409表示任务已存在,直接进入MP4_COMPOSING状态
if (e.getMessage() != null && e.getMessage().contains("409")) {
log.info("[渲染轮询] MP4合成任务已存在,继续等待: taskId={}, renderJobId={}", taskId, renderJobId);
updateMappingStatus(mapping.getId(), TaskRenderJobMappingEntity.STATUS_MP4_COMPOSING,
publishedCount, segmentCount, playUrl, null);
} else {
log.warn("[渲染轮询] 创建MP4合成任务失败: taskId={}, renderJobId={}, error={}",
taskId, renderJobId, e.getMessage());
// 不改变状态,下次轮询重试
updateMappingStatus(mapping.getId(), TaskRenderJobMappingEntity.STATUS_PREVIEW_READY,
publishedCount, segmentCount, playUrl, null);
}
}
} else {
// 更新片段信息
updateMappingStatus(mapping.getId(), TaskRenderJobMappingEntity.STATUS_PREVIEW_READY,
publishedCount, segmentCount, playUrl, null);
}
}
/**
* 处理MP4_COMPOSING状态
* MP4_COMPOSING → COMPLETED:当mp4Url有值时
*/
private void handleMp4ComposingStatus(TaskRenderJobMappingEntity mapping, JobStatusResponse jobStatus, Long taskId) {
Integer publishedCount = jobStatus.getPublishedCount();
Integer segmentCount = jobStatus.getSegmentCount();
String playUrl = jobStatus.getPlayUrl();
String mp4Url = jobStatus.getMp4Url();
if (StringUtils.isNotBlank(mp4Url)) {
log.info("[渲染轮询] MP4合成完成: taskId={}, mp4Url={}", taskId, mp4Url);
// 更新mapping状态为COMPLETED
updateMappingStatus(mapping.getId(), TaskRenderJobMappingEntity.STATUS_COMPLETED,
publishedCount, segmentCount, playUrl, mp4Url);
// 更新task的videoUrl为最终MP4地址
TaskEntity task = new TaskEntity();
task.setId(taskId);
task.setVideoUrl(mp4Url);
taskMapper.update(task);
videoTaskRepository.clearTaskCache(taskId);
log.info("[渲染轮询] 已更新task最终MP4 URL: taskId={}, mp4Url={}", taskId, mp4Url);
// 更新video记录的videoUrl为最终MP4地址
try {
handleVideoRecordForMP4(taskId, mp4Url);
} catch (Exception e) {
log.warn("[渲染轮询] 更新video的MP4 URL失败: taskId={}, error={}", taskId, e.getMessage(), e);
}
} else {
// MP4还在合成中,更新片段信息
log.debug("[渲染轮询] MP4合成中: taskId={}, 等待下次轮询", taskId);
updateMappingStatus(mapping.getId(), TaskRenderJobMappingEntity.STATUS_MP4_COMPOSING,
publishedCount, segmentCount, playUrl, null);
}
}
/**
* 处理作业失败
*/
private void handleJobFailed(TaskRenderJobMappingEntity mapping, JobStatusResponse jobStatus) {
String errorCode = jobStatus.getErrorCode();
String errorMessage = jobStatus.getErrorMessage();
log.warn("[渲染轮询] 作业失败: taskId={}, status={}, errorCode={}, errorMessage={}",
mapping.getTaskId(), jobStatus.getStatus(), errorCode, errorMessage);
mappingMapper.updateToFailed(
mapping.getId(),
errorCode,
errorMessage,
new Date()
);
// 渲染失败,重置模板渲染状态
updateTemplateRenderStatus(mapping.getTaskId(), TemplateRenderStatus.NONE);
}
/**
* 更新模板渲染状态缓存
* 根据 taskId 查询关联的 faceId 和 templateId,更新 FaceStatusManager 中的渲染状态
*/
private void updateTemplateRenderStatus(Long taskId, TemplateRenderStatus status) {
try {
var taskInfo = taskMapper.getById(taskId);
if (taskInfo != null && taskInfo.getFaceId() != null && taskInfo.getTemplateId() != null) {
faceStatusManager.setTemplateRenderStatus(taskInfo.getFaceId(), taskInfo.getTemplateId(), status);
log.info("[渲染轮询] 已更新模板渲染状态: taskId={}, faceId={}, templateId={}, status={}",
taskId, taskInfo.getFaceId(), taskInfo.getTemplateId(), status.getDescription());
}
} catch (Exception e) {
log.warn("[渲染轮询] 更新模板渲染状态缓存失败: taskId={}, error={}", taskId, e.getMessage());
}
}
/**
* 更新mapping状态
*/
private void updateMappingStatus(Long id, String renderStatus, Integer publishedCount,
Integer segmentCount, String previewUrl, String mp4Url) {
mappingMapper.updateRenderStatus(
id,
renderStatus,
publishedCount,
segmentCount,
previewUrl,
mp4Url,
new Date()
);
}
/**
* 处理异常
*/
private void handleProcessError(TaskRenderJobMappingEntity mapping, Exception e) {
try {
mappingMapper.incrementRetryCount(mapping.getId());
// 超过最大重试次数,标记为失败
if (mapping.getRetryCount() != null &&
mapping.getRetryCount() >= TaskRenderJobMappingEntity.MAX_RETRY_COUNT - 1) {
mappingMapper.updateToFailed(
mapping.getId(),
"MAX_RETRY",
"超过最大重试次数: " + e.getMessage(),
new Date()
);
}
} catch (Exception ex) {
log.error("[渲染轮询] 处理错误失败", ex);
}
}
/**
* 处理video记录(预览就绪时)
* 类似taskSuccess的逻辑,但简化版本
*/
private void handleVideoRecordForPreview(Long taskId, String videoUrl) {
try {
var taskResp = taskMapper.getById(taskId);
if (taskResp == null) {
log.warn("[渲染轮询] task不存在: taskId={}", taskId);
return;
}
VideoEntity video = videoMapper.findByTaskId(taskId);
if (video != null) {
// 更新已有video记录
video.setVideoUrl(videoUrl);
videoMapper.update(video);
videoRepository.clearVideoCache(video.getId());
log.info("[渲染轮询] 已更新video预览URL: taskId={}, videoId={}, videoUrl={}",
taskId, video.getId(), videoUrl);
} else {
// 创建新video记录
video = new VideoEntity();
video.setId(SnowFlakeUtil.getLongId());
video.setScenicId(taskResp.getScenicId());
video.setTemplateId(taskResp.getTemplateId());
video.setTaskId(taskId);
video.setFaceId(taskResp.getFaceId());
video.setVideoUrl(videoUrl);
video.setCreateTime(new Date());
videoMapper.add(video);
log.info("[渲染轮询] 已创建video预览记录: taskId={}, videoId={}, videoUrl={}",
taskId, video.getId(), videoUrl);
}
// 更新member_video关联表(isBuy=0,预览阶段未购买)
videoMapper.updateRelationWhenTaskSuccess(taskId, video.getId(), 0);
memberRelationRepository.clearVCacheByFace(taskResp.getFaceId());
log.info("[渲染轮询] 已更新member_video关联: taskId={}, videoId={}", taskId, video.getId());
} catch (Exception e) {
log.error("[渲染轮询] 处理video记录失败: taskId={}", taskId, e);
throw e;
}
}
/**
* 更新video记录的MP4地址(MP4合成完成时)
*/
private void handleVideoRecordForMP4(Long taskId, String mp4Url) {
try {
VideoEntity video = videoMapper.findByTaskId(taskId);
if (video != null) {
video.setVideoUrl(mp4Url);
videoMapper.update(video);
videoRepository.clearVideoCache(video.getId());
log.info("[渲染轮询] 已更新video最终MP4 URL: taskId={}, videoId={}, mp4Url={}",
taskId, video.getId(), mp4Url);
} else {
log.warn("[渲染轮询] video不存在,无法更新MP4 URL: taskId={}", taskId);
}
} catch (Exception e) {
log.error("[渲染轮询] 更新video的MP4 URL失败: taskId={}", taskId, e);
throw e;
}
}
}

View File

@@ -13,6 +13,8 @@ import com.ycwl.basic.integration.render.dto.job.CreatePreviewRequest;
import com.ycwl.basic.integration.render.dto.job.CreatePreviewResponse; import com.ycwl.basic.integration.render.dto.job.CreatePreviewResponse;
import com.ycwl.basic.integration.render.dto.job.MaterialDTO; import com.ycwl.basic.integration.render.dto.job.MaterialDTO;
import com.ycwl.basic.integration.render.service.RenderJobIntegrationService; import com.ycwl.basic.integration.render.service.RenderJobIntegrationService;
import com.ycwl.basic.mapper.task.TaskRenderJobMappingMapper;
import com.ycwl.basic.model.task.entity.TaskRenderJobMappingEntity;
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;
@@ -69,6 +71,7 @@ import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.ArrayList; import java.util.ArrayList;
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.HashMap;
@@ -124,6 +127,8 @@ public class TaskTaskServiceImpl implements TaskService {
private FaceStatusManager faceStatusManager; private FaceStatusManager faceStatusManager;
@Autowired @Autowired
private RenderJobIntegrationService renderJobIntegrationService; private RenderJobIntegrationService renderJobIntegrationService;
@Autowired
private TaskRenderJobMappingMapper taskRenderJobMappingMapper;
private RenderWorkerEntity getWorker(@NonNull WorkerAuthReqVo req) { private RenderWorkerEntity getWorker(@NonNull WorkerAuthReqVo req) {
String accessKey = req.getAccessKey(); String accessKey = req.getAccessKey();
@@ -224,26 +229,11 @@ public class TaskTaskServiceImpl implements TaskService {
} else { } else {
updTemplateList = templateRepository.getAllEnabledTemplateList(); updTemplateList = templateRepository.getAllEnabledTemplateList();
} }
RenderWorkerConfigManager configManager = repository.getWorkerConfigManager(worker.getId());
try { try {
if (lock.tryLock(2, TimeUnit.SECONDS)) { if (lock.tryLock(2, TimeUnit.SECONDS)) {
try { try {
List<TaskRespVO> taskList; resp.setTasks(Collections.emptyList());
if (Strings.isNotBlank(configManager.getString("scenic_only"))) {
taskList = taskMapper.selectNotRunningByScenicList(configManager.getString("scenic_only"));
} else {
var _taskList = taskMapper.selectNotRunning();
taskList = _taskList.stream().filter(task -> {
boolean workerSelfHostedScenic = isWorkerSelfHostedScenic(task.getScenicId());
return !workerSelfHostedScenic;
}).limit(1).toList();
}
resp.setTasks(taskList);
resp.setTemplates(updTemplateList); resp.setTemplates(updTemplateList);
taskList.forEach(task -> {
taskMapper.assignToWorker(task.getId(), worker.getId());
videoTaskRepository.clearTaskCache(task.getId());
});
} finally { } finally {
lock.unlock(); lock.unlock();
} }
@@ -446,6 +436,7 @@ public class TaskTaskServiceImpl implements TaskService {
if (video != null) { if (video != null) {
log.info("自动创建任务:跳过(auto_replace_vlog=false), faceId:{}, templateId:{}, existingTaskId:{}, videoId:{}", log.info("自动创建任务:跳过(auto_replace_vlog=false), faceId:{}, templateId:{}, existingTaskId:{}, videoId:{}",
faceId, templateId, taskEntity.getId(), video.getId()); faceId, templateId, taskEntity.getId(), video.getId());
faceStatusManager.setTemplateRenderStatus(faceId, templateId, TemplateRenderStatus.RENDERED);
return; return;
} }
} }
@@ -502,7 +493,10 @@ public class TaskTaskServiceImpl implements TaskService {
} }
videoMapper.addRelation(memberVideoEntity); videoMapper.addRelation(memberVideoEntity);
memberRelationRepository.clearVCacheByFace(faceId); memberRelationRepository.clearVCacheByFace(faceId);
// 仅当复用已有视频时立即标记为已渲染,新任务由 RenderJobPollingService 在 PREVIEW_READY 时更新
if (memberVideoEntity.getVideoId() != null) {
faceStatusManager.setTemplateRenderStatus(faceId, templateId, TemplateRenderStatus.RENDERED); faceStatusManager.setTemplateRenderStatus(faceId, templateId, TemplateRenderStatus.RENDERED);
}
faceStatusManager.markNoNewPieces(faceId, templateId); faceStatusManager.markNoNewPieces(faceId, templateId);
}; };
VideoPieceGetter.addTask(task); VideoPieceGetter.addTask(task);
@@ -552,10 +546,6 @@ public class TaskTaskServiceImpl implements TaskService {
videoMapper.add(video); videoMapper.add(video);
} }
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(task.getScenicId()); ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(task.getScenicId());
IStorageAdapter adapter = scenicService.getScenicTmpStorageAdapter(task.getScenicId());
String hash = MD5.create().digestHex(task.getTaskParams() + task.getFaceId().toString());
String filename = StorageUtil.joinPath(StorageConstant.VLOG_PATH, task.getTemplateId().toString() + "_" + hash + "_" + task.getScenicId() + ".mp4");
adapter.setAcl(StorageAcl.PUBLIC_READ, filename);
int isBuy = 0; int isBuy = 0;
FaceEntity face = faceRepository.getFace(task.getFaceId()); FaceEntity face = faceRepository.getFace(task.getFaceId());
if (face != null) { if (face != null) {
@@ -729,6 +719,27 @@ public class TaskTaskServiceImpl implements TaskService {
CreatePreviewResponse response = renderJobIntegrationService.createPreview(request); CreatePreviewResponse response = renderJobIntegrationService.createPreview(request);
log.info("[灰度测试] 渲染预览任务创建成功, taskId: {}, renderJobId: {}, playUrl: {}", log.info("[灰度测试] 渲染预览任务创建成功, taskId: {}, renderJobId: {}, playUrl: {}",
taskId, response.getJobId(), response.getPlayUrl()); taskId, response.getJobId(), response.getPlayUrl());
// 写入mapping表,供轮询服务处理
try {
// 原位替换模式下可能已有旧映射,先删除再插入
TaskRenderJobMappingEntity existingMapping = taskRenderJobMappingMapper.selectByTaskId(taskId);
if (existingMapping != null) {
taskRenderJobMappingMapper.deleteById(existingMapping.getId());
log.info("[灰度测试] 已删除旧mapping, taskId: {}, oldRenderJobId: {}", taskId, existingMapping.getRenderJobId());
}
TaskRenderJobMappingEntity mapping = new TaskRenderJobMappingEntity();
mapping.setTaskId(taskId);
mapping.setRenderJobId(response.getJobId());
mapping.setRenderStatus(TaskRenderJobMappingEntity.STATUS_PENDING);
mapping.setPublishedCount(0);
mapping.setSegmentCount(0);
mapping.setRetryCount(0);
taskRenderJobMappingMapper.insert(mapping);
log.info("[灰度测试] 写入mapping成功, taskId: {}, renderJobId: {}", taskId, response.getJobId());
} catch (Exception ex) {
log.warn("[灰度测试] 写入mapping失败,不影响主流程, taskId: {}, error: {}", taskId, ex.getMessage());
}
} catch (Exception e) { } catch (Exception e) {
// 灰度测试:不管返回什么或者报错,都不影响现有流程 // 灰度测试:不管返回什么或者报错,都不影响现有流程
log.warn("[灰度测试] 渲染预览任务创建失败,不影响主流程, taskId: {}, templateId: {}, error: {}", log.warn("[灰度测试] 渲染预览任务创建失败,不影响主流程, taskId: {}, templateId: {}, error: {}",

View File

@@ -66,6 +66,10 @@ public class DownloadNotificationTasker {
} }
MemberRespVO member = memberMapper.getById(item.getMemberId()); MemberRespVO member = memberMapper.getById(item.getMemberId());
if (member == null || member.getOpenId() == null) {
log.debug("用户[memberId={}]不存在或未绑定微信,跳过", item.getMemberId());
return;
}
// 发送模板消息 // 发送模板消息
HashMap<String, Object> variables = new HashMap<>(); HashMap<String, Object> variables = new HashMap<>();
ScenicV2DTO scenic = scenicRepository.getScenicBasic(item.getScenicId()); ScenicV2DTO scenic = scenicRepository.getScenicBasic(item.getScenicId());
@@ -111,6 +115,10 @@ public class DownloadNotificationTasker {
sentMemberIds.add(item.getMemberId()); sentMemberIds.add(item.getMemberId());
MemberRespVO member = memberMapper.getById(item.getMemberId()); MemberRespVO member = memberMapper.getById(item.getMemberId());
if (member == null || member.getOpenId() == null) {
log.debug("用户[memberId={}]不存在或未绑定微信,跳过", item.getMemberId());
return;
}
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(item.getScenicId()); ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(item.getScenicId());
Integer videoStoreDay = scenicConfig.getInteger("video_store_day"); Integer videoStoreDay = scenicConfig.getInteger("video_store_day");
if (videoStoreDay == null) { if (videoStoreDay == null) {
@@ -161,6 +169,10 @@ public class DownloadNotificationTasker {
sentMemberIds.add(item.getMemberId()); sentMemberIds.add(item.getMemberId());
MemberRespVO member = memberMapper.getById(item.getMemberId()); MemberRespVO member = memberMapper.getById(item.getMemberId());
if (member == null || member.getOpenId() == null) {
log.debug("用户[memberId={}]不存在或未绑定微信,跳过", item.getMemberId());
return;
}
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(item.getScenicId()); ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(item.getScenicId());
Integer videoStoreDay = scenicConfig.getInteger("video_store_day"); Integer videoStoreDay = scenicConfig.getInteger("video_store_day");
if (videoStoreDay == null) { if (videoStoreDay == null) {
@@ -237,6 +249,10 @@ public class DownloadNotificationTasker {
} }
MemberRespVO member = memberMapper.getById(item.getMemberId()); MemberRespVO member = memberMapper.getById(item.getMemberId());
if (member == null || member.getOpenId() == null) {
log.debug("用户[memberId={}]不存在或未绑定微信,跳过", item.getMemberId());
return;
}
// 发送模板消息 // 发送模板消息
HashMap<String, Object> variables = new HashMap<>(); HashMap<String, Object> variables = new HashMap<>();
variables.put("scenicName", scenic.getName()); variables.put("scenicName", scenic.getName());

View File

@@ -60,7 +60,7 @@ public class ScenicStatsTask {
}); });
} }
} }
@Scheduled(cron = "0 0 2 * * *") @Scheduled(cron = "0 1 0 * * *")
public void countScenicStats() { public void countScenicStats() {
log.info("开始执行景区统计任务,统计前7天至昨天的数据"); log.info("开始执行景区统计任务,统计前7天至昨天的数据");
@@ -93,8 +93,8 @@ public class ScenicStatsTask {
// 写入数据库(REPLACE INTO 会自动更新已存在的记录) // 写入数据库(REPLACE INTO 会自动更新已存在的记录)
statisticsMapper.insertStat(scenicId, startTime, data); statisticsMapper.insertStat(scenicId, startTime, data);
// 删除该景区的缓存,确保下次查询时获取最新数据 // 删除该景区该日期的缓存,确保下次查询时获取最新数据
invalidateStatisticsCache(scenicId); invalidateStatisticsCache(scenicId, startTime);
} catch (Exception e) { } catch (Exception e) {
log.error("统计景区 {} 在日期 {} 的数据时发生错误", scenic.getId(), DateUtil.formatDate(startTime), e); log.error("统计景区 {} 在日期 {} 的数据时发生错误", scenic.getId(), DateUtil.formatDate(startTime), e);
} }
@@ -109,9 +109,12 @@ public class ScenicStatsTask {
/** /**
* 删除景区统计缓存 * 删除景区统计缓存
* @param scenicId 景区ID * @param scenicId 景区ID
* @param date 统计日期
*/ */
private void invalidateStatisticsCache(Long scenicId) { private void invalidateStatisticsCache(Long scenicId, Date date) {
String redisKey = "statistics:tmp_cache:" + scenicId; String redisKey = String.format("statistics:tmp_cache:%s:%s",
scenicId,
DateUtil.formatDate(date));
redisTemplate.delete(redisKey); redisTemplate.delete(redisKey);
} }
} }

View File

@@ -101,6 +101,29 @@ public class ImageUtils {
} }
} }
/**
* 判断图片是否为横图(宽度大于高度)
* 支持URL字符串或文件路径
*
* @param imageSource URL字符串或文件路径
* @return true表示横图,false表示竖图
* @throws IOException 读取图片失败
*/
public static boolean isLandscape(String imageSource) throws IOException {
BufferedImage image = null;
try {
image = loadImage(imageSource);
if (image == null) {
throw new IOException("无法读取图片: " + imageSource);
}
return image.getWidth() > image.getHeight();
} finally {
if (image != null) {
image.flush();
}
}
}
/** /**
* 旋转图片90度(顺时针) * 旋转图片90度(顺时针)
* *

View File

@@ -1,167 +0,0 @@
package com.ycwl.basic.watchdog;
import com.ycwl.basic.integration.message.dto.ZtMessage;
import com.ycwl.basic.integration.message.service.ZtMessageProducerService;
import com.ycwl.basic.mapper.TaskMapper;
import com.ycwl.basic.model.pc.task.entity.TaskEntity;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Profile;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@Component
@Profile("prod")
public class TaskWatchDog {
@Autowired
private TaskMapper taskMapper;
@Autowired
private ZtMessageProducerService ztMessageProducerService;
// 异常通知计数器
private final Map<String, Integer> notificationCounters = new HashMap<>();
// 配置参数
private static final int MAX_NOTIFICATION_COUNT = 3; // 每种异常最多通知3次
// 异常类型标识
private static final String TASK_BACKLOG = "task_backlog";
private static final String FAILED_TASKS = "failed_tasks";
private static final String LONG_RUNNING_TASK_PREFIX = "long_running_task_"; // 长时间运行任务前缀
@Scheduled(fixedDelay = 1000 * 60L)
public void scanTaskStatus() {
List<TaskEntity> allNotRunningTaskList = taskMapper.selectAllNotRunning();
List<TaskEntity> allFailedTaskList = taskMapper.selectAllFailed();
List<TaskEntity> allRunningTaskList = taskMapper.selectAllRunning();
// 检查任务积压
checkTaskBacklog(allNotRunningTaskList);
// 检查失败任务
checkFailedTasks(allFailedTaskList);
// 检查长时间运行任务
checkLongRunningTasks(allRunningTaskList);
}
/**
* 检查任务积压
*/
private void checkTaskBacklog(List<TaskEntity> notRunningTasks) {
if (notRunningTasks.size() > 10) {
if (shouldSendNotification(TASK_BACKLOG)) {
String content = String.format("当前任务队列中存在超过10个未运行任务,请及时处理!未运行任务数量:%d", notRunningTasks.size());
sendNotification("任务堆积警告", content, TASK_BACKLOG);
}
} else {
// 异常已恢复,重置计数器
resetNotificationCounter(TASK_BACKLOG);
}
}
/**
* 检查失败任务
*/
private void checkFailedTasks(List<TaskEntity> failedTasks) {
if (failedTasks.size() > 5) {
if (shouldSendNotification(FAILED_TASKS)) {
String content = String.format("当前存在超过5个失败任务(status=3),请及时检查和处理!失败任务数量:%d", failedTasks.size());
sendNotification("任务失败警告", content, FAILED_TASKS);
}
} else {
// 异常已恢复,重置计数器
resetNotificationCounter(FAILED_TASKS);
}
}
/**
* 检查长时间运行任务
*/
private void checkLongRunningTasks(List<TaskEntity> runningTasks) {
Set<String> currentLongRunningTasks = new HashSet<>();
for (TaskEntity taskEntity : runningTasks) {
if (taskEntity.getStartTime() == null) {
continue;
}
// startTime已经过去3分钟了
if (System.currentTimeMillis() - taskEntity.getStartTime().getTime() > 1000 * 60 * 3) {
String taskKey = LONG_RUNNING_TASK_PREFIX + taskEntity.getId();
currentLongRunningTasks.add(taskKey);
if (shouldSendNotification(taskKey)) {
String content = String.format("当前【%s】渲染机的【%d】任务已超过3分钟未完成!",
taskEntity.getWorkerId(), taskEntity.getId());
sendNotification("长时间运行任务警告", content, taskKey);
}
}
}
// 清理已恢复正常的长时运行任务的计数器
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

@@ -136,7 +136,7 @@
FROM `zt`.`face` FROM `zt`.`face`
WHERE `scenic_id` = #{scenicId} WHERE `scenic_id` = #{scenicId}
AND `create_at` &lt; #{endDate} AND `create_at` &lt; #{endDate}
and `id` not in (select face_id from member_source where is_buy = 1) and `id` not in (select face_id from member_source where is_buy = 1 AND deleted = 0)
and `id` not in (select face_id from member_video where is_buy = 1) and `id` not in (select face_id from member_video where is_buy = 1)
</select> </select>
</mapper> </mapper>

View File

@@ -94,14 +94,14 @@
FROM member_source ms FROM member_source ms
LEFT JOIN face f ON ms.face_id = f.id LEFT JOIN face f ON ms.face_id = f.id
LEFT JOIN source s ON ms.source_id = s.id LEFT JOIN source s ON ms.source_id = s.id
WHERE s.id IS NOT NULL WHERE s.id IS NOT NULL AND ms.deleted = 0
), ),
member_source_aicam_data AS ( member_source_aicam_data AS (
SELECT ms.member_id, ms.source_id, ms.face_id, f.face_url, s.url SELECT ms.member_id, ms.source_id, ms.face_id, f.face_url, s.url
FROM member_source ms FROM member_source ms
LEFT JOIN face f ON ms.face_id = f.id LEFT JOIN face f ON ms.face_id = f.id
LEFT JOIN source s ON ms.source_id = s.id LEFT JOIN source s ON ms.source_id = s.id
WHERE s.id IS NOT NULL AND ms.type = 3 WHERE s.id IS NOT NULL AND ms.type = 3 AND ms.deleted = 0
), ),
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

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.ycwl.basic.mapper.PrinterGuideMapper">
<select id="listByPrinterId" resultType="com.ycwl.basic.model.pc.printer.entity.PrinterGuideEntity">
select id, printer_id, image_url, sort_order, enabled, create_time, update_time
from printer_guide
where printer_id = #{printerId}
order by sort_order asc, id asc
</select>
<select id="listEnabledByPrinterId" resultType="com.ycwl.basic.model.pc.printer.entity.PrinterGuideEntity">
select id, printer_id, image_url, sort_order, enabled, create_time, update_time
from printer_guide
where printer_id = #{printerId} and enabled = 1
order by sort_order asc, id asc
</select>
<insert id="insertGuide" useGeneratedKeys="true" keyProperty="id">
insert into printer_guide(printer_id, image_url, sort_order, enabled, create_time)
values (#{printerId}, #{imageUrl}, #{sortOrder}, #{enabled}, NOW())
</insert>
<delete id="deleteById">
delete from printer_guide where id = #{id}
</delete>
<update id="updateSortOrder">
update printer_guide
set sort_order = #{sortOrder}, update_time = NOW()
where id = #{id}
</update>
<update id="toggleEnabled">
update printer_guide
set enabled = 1 - enabled, update_time = NOW()
where id = #{id}
</update>
</mapper>

View File

@@ -128,15 +128,15 @@
WHERE id = #{id} WHERE id = #{id}
</update> </update>
<!-- 根据内容哈希查询历史记录(用于去重) --> <!-- 根据内容哈希查询历史记录(用于去重,同时匹配成功和生成中的记录,防止并发重复写入) -->
<select id="findByContentHash" resultMap="BaseResultMap"> <select id="findByContentHash" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/> SELECT <include refid="Base_Column_List"/>
FROM puzzle_generation_record FROM puzzle_generation_record
WHERE template_id = #{templateId} WHERE template_id = #{templateId}
AND content_hash = #{contentHash} AND content_hash = #{contentHash}
AND scenic_id = #{scenicId} AND scenic_id = #{scenicId}
AND status = 1 AND (status = 1 OR (status = 0 AND create_time > DATE_SUB(NOW(), INTERVAL 5 MINUTE)))
ORDER BY create_time DESC ORDER BY status DESC, create_time DESC
LIMIT 1 LIMIT 1
</select> </select>

View File

@@ -22,7 +22,7 @@
select s.scenic_id, s.device_id select s.scenic_id, s.device_id
from member_source ms from member_source ms
left join source s on ms.source_id = s.id left join source s on ms.source_id = s.id
where ms.type = 1 and s.id is not null where ms.type = 1 and s.id is not null and ms.deleted = 0
and s.create_time >= #{start} and s.create_time >= #{start}
and s.create_time &lt;= #{end} and s.create_time &lt;= #{end}
group by s.scenic_id, s.device_id, ms.face_id group by s.scenic_id, s.device_id, ms.face_id
@@ -53,7 +53,7 @@
select s.scenic_id, s.device_id select s.scenic_id, s.device_id
from member_source ms from member_source ms
left join source s on ms.source_id = s.id left join source s on ms.source_id = s.id
where ms.type = 2 and s.id is not null where ms.type = 2 and s.id is not null and ms.deleted = 0
and s.create_time >= #{start} and s.create_time >= #{start}
and s.create_time &lt;= #{end} and s.create_time &lt;= #{end}
group by s.scenic_id, s.device_id, ms.face_id group by s.scenic_id, s.device_id, ms.face_id

View File

@@ -165,11 +165,11 @@
</delete> </delete>
<delete id="deleteNotBuyRelations"> <delete id="deleteNotBuyRelations">
delete from member_source delete from member_source
where scenic_id = #{scenicId} and is_buy = 0 and create_time &lt;= #{endDate} where scenic_id = #{scenicId} and is_buy = 0 and create_time &lt;= #{endDate} and deleted = 0
</delete> </delete>
<delete id="deleteNotBuyFaceRelation"> <delete id="deleteNotBuyFaceRelation">
delete from member_source delete from member_source
where member_id = #{userId} and face_id = #{faceId} and is_buy = 0 where member_id = #{userId} and face_id = #{faceId} and is_buy = 0 and deleted = 0
</delete> </delete>
<delete id="deleteUselessSource"> <delete id="deleteUselessSource">
delete from source where id not in (select source_id from member_source) and face_sample_id not in (select id from face_sample) delete from source where id not in (select source_id from member_source) and face_sample_id not in (select id from face_sample)
@@ -195,7 +195,7 @@
from member_source ms from member_source ms
left join source so on ms.source_id = so.id left join source so on ms.source_id = so.id
where so.id = #{id} and ms.member_id = #{userId} and so.id is not null where so.id = #{id} and ms.member_id = #{userId} and so.id is not null and ms.deleted = 0
</select> </select>
<select id="getById" resultType="com.ycwl.basic.model.pc.source.resp.SourceRespVO"> <select id="getById" resultType="com.ycwl.basic.model.pc.source.resp.SourceRespVO">
select so.id, scenic_id, device_id, thumb_url, type, url, video_url, so.create_time, so.update_time select so.id, scenic_id, device_id, thumb_url, type, url, video_url, so.create_time, so.update_time
@@ -207,6 +207,7 @@
select ms.type, ms.is_buy select ms.type, ms.is_buy
from member_source ms from member_source ms
<where> <where>
and ms.deleted = 0
<if test="scenicId!= null">and ms.scenic_id = #{scenicId} </if> <if test="scenicId!= null">and ms.scenic_id = #{scenicId} </if>
<if test="memberId!= null">and ms.member_id = #{memberId} </if> <if test="memberId!= null">and ms.member_id = #{memberId} </if>
<if test="isBuy!=null">and ms.is_buy = #{isBuy}</if> <if test="isBuy!=null">and ms.is_buy = #{isBuy}</if>
@@ -214,7 +215,7 @@
group by ms.type group by ms.type
</select> </select>
<select id="countByMemberId" resultType="java.lang.Integer"> <select id="countByMemberId" resultType="java.lang.Integer">
select count(1) from member_source where member_id = #{userId} select count(1) from member_source where member_id = #{userId} and deleted = 0
</select> </select>
<select id="listBySampleIds" resultType="com.ycwl.basic.model.pc.source.entity.SourceEntity"> <select id="listBySampleIds" resultType="com.ycwl.basic.model.pc.source.entity.SourceEntity">
select * select *
@@ -245,6 +246,7 @@
from member_source ms from member_source ms
left join source so on ms.source_id = so.id left join source so on ms.source_id = so.id
<where> <where>
and ms.deleted = 0
<if test="scenicId!= null">and ms.scenic_id = #{scenicId} </if> <if test="scenicId!= null">and ms.scenic_id = #{scenicId} </if>
<if test="isBuy!=null">and ms.is_buy = #{isBuy} </if> <if test="isBuy!=null">and ms.is_buy = #{isBuy} </if>
<if test="type!=null">and ms.type = #{type} </if> <if test="type!=null">and ms.type = #{type} </if>
@@ -258,7 +260,7 @@
from member_source ms from member_source ms
left join source so on ms.source_id = so.id left join source so on ms.source_id = so.id
where ms.member_id = #{userId} and ms.source_id = #{sourceId} and so.id is not null where ms.member_id = #{userId} and ms.source_id = #{sourceId} and so.id is not null and ms.deleted = 0
limit 1 limit 1
</select> </select>
<select id="queryByRelation" resultType="com.ycwl.basic.model.pc.source.resp.SourceRespVO"> <select id="queryByRelation" resultType="com.ycwl.basic.model.pc.source.resp.SourceRespVO">
@@ -267,7 +269,7 @@
left join source so on ms.source_id = so.id left join source so on ms.source_id = so.id
where where
ms.member_id = #{memberId} and so.id is not null ms.member_id = #{memberId} and so.id is not null and ms.deleted = 0
<if test="faceId!= null">and ms.face_id = #{faceId} </if> <if test="faceId!= null">and ms.face_id = #{faceId} </if>
<if test="type!=null">and ms.type = #{type} </if> <if test="type!=null">and ms.type = #{type} </if>
<if test="scenicId!= null">and ms.scenic_id = #{scenicId} </if> <if test="scenicId!= null">and ms.scenic_id = #{scenicId} </if>
@@ -302,7 +304,7 @@
ROW_NUMBER() OVER (PARTITION BY ms.face_id, ms.type ORDER BY so.create_time DESC) as rn ROW_NUMBER() OVER (PARTITION BY ms.face_id, ms.type ORDER BY so.create_time DESC) as rn
FROM member_source ms FROM member_source ms
LEFT JOIN source so ON ms.source_id = so.id LEFT JOIN source so ON ms.source_id = so.id
WHERE so.id IS NOT NULL WHERE so.id IS NOT NULL AND ms.deleted = 0
<if test="faceIds != null and faceIds.size() > 0"> <if test="faceIds != null and faceIds.size() > 0">
AND ms.face_id IN AND ms.face_id IN
<foreach collection="faceIds" item="id" open="(" separator="," close=")"> <foreach collection="faceIds" item="id" open="(" separator="," close=")">
@@ -326,33 +328,33 @@
<select id="hasRelationTo" resultType="java.lang.Integer"> <select id="hasRelationTo" resultType="java.lang.Integer">
select count(1) select count(1)
from member_source from member_source
where member_id = #{memberId} and source_id = #{sourceId} and type = #{type} where member_id = #{memberId} and source_id = #{sourceId} and type = #{type} and deleted = 0
</select> </select>
<select id="listVideoByFaceRelation" resultType="com.ycwl.basic.model.pc.source.entity.SourceEntity"> <select id="listVideoByFaceRelation" resultType="com.ycwl.basic.model.pc.source.entity.SourceEntity">
select s.*, ms.is_buy select s.*, ms.is_buy
from member_source ms from member_source ms
left join source s on ms.source_id = s.id left join source s on ms.source_id = s.id
where ms.face_id = #{faceId} and ms.type = 1 where ms.face_id = #{faceId} and ms.type = 1 and ms.deleted = 0
order by create_time desc order by create_time desc
</select> </select>
<select id="listVideoByScenicFaceRelation" resultType="com.ycwl.basic.model.pc.source.entity.SourceEntity"> <select id="listVideoByScenicFaceRelation" resultType="com.ycwl.basic.model.pc.source.entity.SourceEntity">
select s.*, ms.is_buy select s.*, ms.is_buy
from member_source ms from member_source ms
left join source s on ms.source_id = s.id left join source s on ms.source_id = s.id
where ms.face_id = #{faceId} and ms.type = 1 and ms.scenic_id = #{scenicId} where ms.face_id = #{faceId} and ms.type = 1 and ms.scenic_id = #{scenicId} and ms.deleted = 0
order by create_time desc order by create_time desc
</select> </select>
<select id="listImageByFaceRelation" resultType="com.ycwl.basic.model.pc.source.entity.SourceEntity"> <select id="listImageByFaceRelation" resultType="com.ycwl.basic.model.pc.source.entity.SourceEntity">
select s.*, ms.is_buy select s.*, ms.is_buy
from member_source ms from member_source ms
left join source s on ms.source_id = s.id left join source s on ms.source_id = s.id
where ms.face_id = #{faceId} and ms.type = 2 where ms.face_id = #{faceId} and ms.type = 2 and ms.deleted = 0
</select> </select>
<select id="listAiCamImageByFaceRelation" resultType="com.ycwl.basic.model.pc.source.entity.SourceEntity"> <select id="listAiCamImageByFaceRelation" resultType="com.ycwl.basic.model.pc.source.entity.SourceEntity">
select s.*, ms.is_buy select s.*, ms.is_buy
from member_source ms from member_source ms
left join source s on ms.source_id = s.id left join source s on ms.source_id = s.id
where ms.face_id = #{faceId} and ms.type = 3 where ms.face_id = #{faceId} and ms.type = 3 and ms.deleted = 0
</select> </select>
<select id="getEntity" resultType="com.ycwl.basic.model.pc.source.entity.SourceEntity"> <select id="getEntity" resultType="com.ycwl.basic.model.pc.source.entity.SourceEntity">
select * select *
@@ -379,7 +381,7 @@
from member_source ms from member_source ms
left join source so on ms.source_id = so.id left join source so on ms.source_id = so.id
where where
ms.member_id = #{memberId} and so.id is not null ms.member_id = #{memberId} and so.id is not null and ms.deleted = 0
<if test="scenicId!= null">and ms.scenic_id = #{scenicId} </if> <if test="scenicId!= null">and ms.scenic_id = #{scenicId} </if>
<if test="isBuy!=null">and ms.is_buy = #{isBuy} </if> <if test="isBuy!=null">and ms.is_buy = #{isBuy} </if>
<if test="type!=null">and ms.type = #{type} </if> <if test="type!=null">and ms.type = #{type} </if>
@@ -388,7 +390,7 @@
<select id="listByFaceRelation" resultType="com.ycwl.basic.model.pc.source.entity.MemberSourceEntity"> <select id="listByFaceRelation" resultType="com.ycwl.basic.model.pc.source.entity.MemberSourceEntity">
select * select *
from member_source ms from member_source ms
where ms.face_id = #{faceId} where ms.face_id = #{faceId} and ms.deleted = 0
<if test="type!=null">and ms.type = #{type} </if> <if test="type!=null">and ms.type = #{type} </if>
</select> </select>
<update id="updateMemberIdByFaceId"> <update id="updateMemberIdByFaceId">
@@ -400,7 +402,7 @@
select s.* select s.*
from source s from source s
inner join member_source ms on s.id = ms.source_id inner join member_source ms on s.id = ms.source_id
where ms.face_id = #{faceId} and s.type = 2 where ms.face_id = #{faceId} and s.type = 2 and ms.deleted = 0
</select> </select>
<select id="getBySampleIdAndType" resultType="com.ycwl.basic.model.pc.source.entity.SourceEntity"> <select id="getBySampleIdAndType" resultType="com.ycwl.basic.model.pc.source.entity.SourceEntity">
select * select *
@@ -416,6 +418,7 @@
INNER JOIN source s ON ms.source_id = s.id INNER JOIN source s ON ms.source_id = s.id
WHERE ms.face_id = #{faceId} WHERE ms.face_id = #{faceId}
AND s.type = 2 AND s.type = 2
AND ms.deleted = 0
</select> </select>
<select id="getSourceByFaceAndDeviceIndex" resultType="com.ycwl.basic.model.pc.source.entity.SourceEntity"> <select id="getSourceByFaceAndDeviceIndex" resultType="com.ycwl.basic.model.pc.source.entity.SourceEntity">
@@ -445,6 +448,7 @@
INNER JOIN member_source ms ON s.id = ms.source_id INNER JOIN member_source ms ON s.id = ms.source_id
WHERE ms.face_id = #{faceId} WHERE ms.face_id = #{faceId}
AND s.type = #{type} AND s.type = #{type}
AND ms.deleted = 0
) )
SELECT * SELECT *
FROM ranked_sources FROM ranked_sources
@@ -456,7 +460,7 @@
SELECT DISTINCT s.device_id SELECT DISTINCT s.device_id
FROM member_source ms FROM member_source ms
INNER JOIN source s ON ms.source_id = s.id INNER JOIN source s ON ms.source_id = s.id
WHERE ms.face_id = #{faceId} WHERE ms.face_id = #{faceId} AND ms.deleted = 0
ORDER BY s.device_id ASC ORDER BY s.device_id ASC
</select> </select>
@@ -467,6 +471,7 @@
WHERE ms.face_id = #{faceId} WHERE ms.face_id = #{faceId}
AND s.device_id = #{deviceId} AND s.device_id = #{deviceId}
AND s.type = #{type} AND s.type = #{type}
AND ms.deleted = 0
<choose> <choose>
<when test='sortStrategy == "LATEST"'> <when test='sortStrategy == "LATEST"'>
ORDER BY s.create_time DESC ORDER BY s.create_time DESC
@@ -505,22 +510,100 @@
<delete id="deleteRelationsByFaceIdAndType"> <delete id="deleteRelationsByFaceIdAndType">
DELETE FROM member_source DELETE FROM member_source
WHERE face_id = #{faceId} AND `type` = #{type} WHERE face_id = #{faceId} AND `type` = #{type} AND deleted = 0
</delete> </delete>
<select id="countFreeRelationsByFaceIdAndType" resultType="int"> <select id="countFreeRelationsByFaceIdAndType" resultType="int">
SELECT COUNT(*) FROM member_source SELECT COUNT(*) FROM member_source
WHERE face_id = #{faceId} AND `type` = #{type} AND is_free = 1 WHERE face_id = #{faceId} AND `type` = #{type} AND is_free = 1 AND deleted = 0
</select> </select>
<select id="listSourceByFaceRelation" resultType="com.ycwl.basic.model.pc.source.entity.SourceEntity"> <select id="listSourceByFaceRelation" resultType="com.ycwl.basic.model.pc.source.entity.SourceEntity">
SELECT s.* SELECT s.*
FROM member_source ms FROM member_source ms
INNER JOIN source s ON ms.source_id = s.id INNER JOIN source s ON ms.source_id = s.id
WHERE ms.face_id = #{faceId} WHERE ms.face_id = #{faceId} AND ms.deleted = 0
<if test="type != null"> <if test="type != null">
AND ms.type = #{type} AND ms.type = #{type}
</if> </if>
ORDER BY s.create_time DESC ORDER BY s.create_time DESC
</select> </select>
<select id="listFaceIdsBySourceIds" resultType="com.ycwl.basic.model.pc.source.entity.MemberSourceEntity">
SELECT source_id, face_id
FROM member_source
WHERE deleted = 0 AND source_id IN
<foreach collection="list" item="item" open="(" separator="," close=")">
#{item}
</foreach>
GROUP BY source_id
</select>
<select id="pageByFaceId" resultType="com.ycwl.basic.model.pc.source.resp.SourceRespVO">
SELECT so.id, ms.face_id, ms.scenic_id, ms.type, so.thumb_url, so.url, so.video_url,
ms.is_free, so.create_time, ms.is_buy, so.device_id
FROM member_source ms
LEFT JOIN source so ON ms.source_id = so.id
WHERE ms.face_id = #{faceId} AND so.id IS NOT NULL AND ms.deleted = 0
<if test="type != null">AND ms.type = #{type}</if>
<if test="scenicId != null">AND ms.scenic_id = #{scenicId}</if>
<if test="isBuy != null">AND ms.is_buy = #{isBuy}</if>
ORDER BY so.create_time DESC
</select>
<update id="softDeleteRelation">
UPDATE member_source SET deleted = 1, deleted_at = NOW()
WHERE id = #{id} AND deleted = 0
</update>
<update id="reactivateRelation">
UPDATE member_source SET deleted = 0, deleted_at = NULL
WHERE id = #{id} AND deleted = 1
</update>
<select id="pageDeletedByFaceId" resultType="com.ycwl.basic.model.pc.source.resp.SourceRespVO">
SELECT so.id, ms.face_id, ms.scenic_id, ms.type, so.thumb_url, so.url, so.video_url,
ms.is_free, so.create_time, ms.is_buy, so.device_id, ms.deleted_at
FROM member_source ms
LEFT JOIN source so ON ms.source_id = so.id
WHERE ms.face_id = #{faceId} AND so.id IS NOT NULL AND ms.deleted = 1
<if test="type != null">AND ms.type = #{type}</if>
<if test="scenicId != null">AND ms.scenic_id = #{scenicId}</if>
ORDER BY ms.deleted_at DESC
</select>
<select id="getMemberSourceById" resultType="com.ycwl.basic.model.pc.source.entity.MemberSourceEntity">
SELECT * FROM member_source WHERE id = #{id}
</select>
<select id="getDeviceSourceStats" resultType="com.ycwl.basic.model.pc.device.resp.DeviceSourceStatsVO">
SELECT
COUNT(DISTINCT so.id) AS totalShots,
COUNT(DISTINCT ms.face_id) AS totalFaces,
SUM(CASE WHEN ms.is_buy = 1 THEN 1 ELSE 0 END) AS soldCount,
SUM(CASE WHEN ms.is_free = 1 THEN 1 ELSE 0 END) AS freeCount,
COUNT(DISTINCT CASE WHEN ms.is_buy = 1 THEN ms.face_id END) AS soldFaceCount
FROM source so
LEFT JOIN member_source ms ON ms.source_id = so.id AND ms.deleted = 0
WHERE so.device_id = #{deviceId}
AND so.type = 2
AND so.create_time &gt;= #{startTime}
AND so.create_time &lt;= #{endTime}
</select>
<select id="getDeviceSourceTimeline" resultType="com.ycwl.basic.model.pc.device.resp.DeviceSourceTimelineVO">
SELECT
FROM_UNIXTIME(FLOOR(UNIX_TIMESTAMP(create_time) / 300) * 300) AS time,
COUNT(*) AS count
FROM source
WHERE device_id = #{deviceId}
AND type = 2
AND create_time &gt;= #{startTime}
AND create_time &lt;= #{endTime}
GROUP BY FLOOR(UNIX_TIMESTAMP(create_time) / 300)
ORDER BY time
</select>
<select id="getMemberSourceByMemberAndSourceId" resultType="com.ycwl.basic.model.pc.source.entity.MemberSourceEntity">
SELECT * FROM member_source WHERE member_id = #{memberId} AND source_id = #{sourceId} AND deleted = 0 LIMIT 1
</select>
<update id="updateRelationBySourceId">
update member_source
<set>
<if test="isBuy!=null">is_buy = #{isBuy}, </if>
<if test="orderId!=null">order_id = #{orderId}, </if>
</set>
where member_id = #{memberId} and source_id = #{sourceId}
</update>
</mapper> </mapper>

View File

@@ -0,0 +1,89 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ycwl.basic.mapper.task.TaskRenderJobMappingMapper">
<resultMap id="BaseResultMap" type="com.ycwl.basic.model.task.entity.TaskRenderJobMappingEntity">
<id column="id" property="id"/>
<result column="task_id" property="taskId"/>
<result column="render_job_id" property="renderJobId"/>
<result column="render_status" property="renderStatus"/>
<result column="published_count" property="publishedCount"/>
<result column="segment_count" property="segmentCount"/>
<result column="preview_url" property="previewUrl"/>
<result column="mp4_url" property="mp4Url"/>
<result column="error_code" property="errorCode"/>
<result column="error_message" property="errorMessage"/>
<result column="retry_count" property="retryCount"/>
<result column="last_check_time" property="lastCheckTime"/>
<result column="create_time" property="createTime"/>
<result column="update_time" property="updateTime"/>
</resultMap>
<sql id="Base_Column_List">
id, task_id, render_job_id, render_status, published_count, segment_count,
preview_url, mp4_url, error_code, error_message, retry_count, last_check_time,
create_time, update_time
</sql>
<select id="selectByTaskId" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM task_render_job_mapping
WHERE task_id = #{taskId}
</select>
<select id="selectByRenderJobId" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM task_render_job_mapping
WHERE render_job_id = #{renderJobId}
</select>
<select id="selectPendingForPolling" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM task_render_job_mapping
WHERE render_status IN
<foreach collection="statuses" item="status" open="(" separator="," close=")">
#{status}
</foreach>
AND (
last_check_time IS NULL
OR last_check_time &lt; DATE_SUB(NOW(), INTERVAL #{checkIntervalSeconds} SECOND)
)
AND retry_count &lt; 10
ORDER BY last_check_time ASC, create_time ASC
LIMIT #{limit}
</select>
<update id="updateRenderStatus">
UPDATE task_render_job_mapping
SET render_status = #{renderStatus},
published_count = #{publishedCount},
segment_count = #{segmentCount},
<if test="previewUrl != null">
preview_url = #{previewUrl},
</if>
<if test="mp4Url != null">
mp4_url = #{mp4Url},
</if>
last_check_time = #{lastCheckTime},
update_time = NOW()
WHERE id = #{id}
</update>
<update id="updateToFailed">
UPDATE task_render_job_mapping
SET render_status = 'FAILED',
error_code = #{errorCode},
error_message = #{errorMessage},
last_check_time = #{lastCheckTime},
update_time = NOW()
WHERE id = #{id}
</update>
<update id="incrementRetryCount">
UPDATE task_render_job_mapping
SET retry_count = retry_count + 1,
update_time = NOW()
WHERE id = #{id}
</update>
</mapper>

View File

@@ -7,6 +7,8 @@
<id property="id" column="id"/> <id property="id" column="id"/>
<result property="videoId" column="video_id"/> <result property="videoId" column="video_id"/>
<result property="videoUrl" column="video_url"/> <result property="videoUrl" column="video_url"/>
<result property="duration" column="duration"/>
<result property="taskParams" column="task_params"/>
<result property="templateId" column="template_id"/> <result property="templateId" column="template_id"/>
<result property="templateName" column="template_name"/> <result property="templateName" column="template_name"/>
<result property="scenicId" column="scenic_id"/> <result property="scenicId" column="scenic_id"/>
@@ -15,8 +17,12 @@
<result property="creatorName" column="creator_name"/> <result property="creatorName" column="creator_name"/>
<result property="rating" column="rating"/> <result property="rating" column="rating"/>
<result property="content" column="content"/> <result property="content" column="content"/>
<result property="cameraPositionRating" column="camera_position_rating" <result property="problemDeviceIds" column="problem_device_ids"
typeHandler="com.ycwl.basic.handler.MapTypeHandler"/> typeHandler="com.ycwl.basic.handler.LongListTypeHandler"/>
<result property="problemTags" column="problem_tags"
typeHandler="com.ycwl.basic.handler.StringListTypeHandler"/>
<result property="source" column="source"/>
<result property="sourceId" column="source_id"/>
<result property="createTime" column="create_time"/> <result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/> <result property="updateTime" column="update_time"/>
</resultMap> </resultMap>
@@ -31,16 +37,22 @@
vr.creator, vr.creator,
vr.rating, vr.rating,
vr.content, vr.content,
vr.camera_position_rating, vr.problem_device_ids,
vr.problem_tags,
vr.source,
vr.source_id,
vr.create_time, vr.create_time,
vr.update_time, vr.update_time,
v.video_url, v.video_url,
v.duration,
v.template_id, v.template_id,
tk.task_params,
t.name AS template_name, t.name AS template_name,
s.name AS scenic_name, s.name AS scenic_name,
u.name AS creator_name u.name AS creator_name
FROM video_review vr FROM video_review vr
LEFT JOIN video v ON vr.video_id = v.id LEFT JOIN video v ON vr.video_id = v.id
LEFT JOIN task tk ON v.task_id = tk.id
LEFT JOIN template t ON v.template_id = t.id LEFT JOIN template t ON v.template_id = t.id
LEFT JOIN scenic s ON vr.scenic_id = s.id LEFT JOIN scenic s ON vr.scenic_id = s.id
LEFT JOIN admin_user u ON vr.creator = u.id LEFT JOIN admin_user u ON vr.creator = u.id
@@ -72,6 +84,15 @@
<if test="keyword != null and keyword != ''"> <if test="keyword != null and keyword != ''">
AND vr.content LIKE CONCAT('%', #{keyword}, '%') AND vr.content LIKE CONCAT('%', #{keyword}, '%')
</if> </if>
<if test="problemDeviceId != null">
AND JSON_CONTAINS(vr.problem_device_ids, CAST(#{problemDeviceId} AS CHAR), '$')
</if>
<if test="problemTag != null and problemTag != ''">
AND JSON_CONTAINS(vr.problem_tags, JSON_QUOTE(#{problemTag}), '$')
</if>
<if test="source != null and source != ''">
AND vr.source = #{source}
</if>
</where> </where>
ORDER BY ORDER BY
<choose> <choose>
@@ -140,11 +161,161 @@
LIMIT #{limit} LIMIT #{limit}
</select> </select>
<!-- 查询所有机位评价数据 --> <!-- 查询所有问题机位ID列表 -->
<select id="selectAllCameraPositionRatings" resultType="java.util.Map"> <select id="selectAllProblemDeviceIds" resultType="java.util.List">
SELECT camera_position_rating SELECT problem_device_ids
FROM video_review FROM video_review
WHERE camera_position_rating IS NOT NULL AND camera_position_rating != '' WHERE problem_device_ids IS NOT NULL
AND problem_device_ids != ''
AND problem_device_ids != '[]'
</select>
<!-- 管理后台评价日志结果映射 -->
<resultMap id="AdminVideoReviewLogRespMap" type="com.ycwl.basic.model.pc.videoreview.dto.AdminVideoReviewLogRespDTO">
<id property="id" column="id"/>
<result property="videoId" column="video_id"/>
<result property="videoUrl" column="video_url"/>
<result property="duration" column="duration"/>
<result property="taskParams" column="task_params"/>
<result property="templateId" column="template_id"/>
<result property="templateName" column="template_name"/>
<result property="scenicId" column="scenic_id"/>
<result property="scenicName" column="scenic_name"/>
<result property="creator" column="creator"/>
<result property="creatorName" column="creator_name"/>
<result property="creatorAccount" column="creator_account"/>
<result property="rating" column="rating"/>
<result property="content" column="content"/>
<result property="problemDeviceIds" column="problem_device_ids"
typeHandler="com.ycwl.basic.handler.LongListTypeHandler"/>
<result property="problemDeviceCount" column="problem_device_count"/>
<result property="problemTags" column="problem_tags"
typeHandler="com.ycwl.basic.handler.StringListTypeHandler"/>
<result property="source" column="source"/>
<result property="sourceId" column="source_id"/>
<result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/>
<result property="operationDuration" column="operation_duration"/>
</resultMap>
<!-- 管理后台分页查询评价日志 -->
<select id="selectAdminReviewLogList" parameterType="com.ycwl.basic.model.pc.videoreview.dto.AdminVideoReviewLogReqDTO"
resultMap="AdminVideoReviewLogRespMap">
SELECT
vr.id,
vr.video_id,
vr.scenic_id,
vr.creator,
vr.rating,
vr.content,
vr.problem_device_ids,
vr.problem_tags,
vr.source,
vr.source_id,
vr.create_time,
vr.update_time,
v.video_url,
v.duration,
v.template_id,
tk.task_params,
t.name AS template_name,
s.name AS scenic_name,
u.name AS creator_name,
u.account AS creator_account,
<!-- 计算问题机位数量 -->
CASE
WHEN vr.problem_device_ids IS NOT NULL AND vr.problem_device_ids != '' AND vr.problem_device_ids != '[]'
THEN JSON_LENGTH(vr.problem_device_ids)
ELSE 0
END AS problem_device_count,
<!-- 计算操作时长(秒) -->
TIMESTAMPDIFF(SECOND, vr.create_time, vr.update_time) AS operation_duration
FROM video_review vr
LEFT JOIN video v ON vr.video_id = v.id
LEFT JOIN task tk ON v.task_id = tk.id
LEFT JOIN template t ON v.template_id = t.id
LEFT JOIN scenic s ON vr.scenic_id = s.id
LEFT JOIN admin_user u ON vr.creator = u.id
<where>
<if test="id != null">
AND vr.id = #{id}
</if>
<if test="videoId != null">
AND vr.video_id = #{videoId}
</if>
<if test="scenicId != null">
AND vr.scenic_id = #{scenicId}
</if>
<if test="creator != null">
AND vr.creator = #{creator}
</if>
<if test="creatorName != null and creatorName != ''">
AND u.name LIKE CONCAT('%', #{creatorName}, '%')
</if>
<if test="rating != null">
AND vr.rating = #{rating}
</if>
<if test="minRating != null">
AND vr.rating &gt;= #{minRating}
</if>
<if test="maxRating != null">
AND vr.rating &lt;= #{maxRating}
</if>
<if test="startTime != null and startTime != ''">
AND vr.create_time &gt;= #{startTime}
</if>
<if test="endTime != null and endTime != ''">
AND vr.create_time &lt;= #{endTime}
</if>
<if test="templateId != null">
AND v.template_id = #{templateId}
</if>
<if test="templateName != null and templateName != ''">
AND t.name LIKE CONCAT('%', #{templateName}, '%')
</if>
<if test="keyword != null and keyword != ''">
AND (
vr.content LIKE CONCAT('%', #{keyword}, '%')
OR s.name LIKE CONCAT('%', #{keyword}, '%')
OR t.name LIKE CONCAT('%', #{keyword}, '%')
)
</if>
<if test="hasCameraRating != null">
<!-- hasCameraRating 参数已废弃,保留以兼容旧接口 -->
</if>
<if test="problemDeviceId != null">
AND JSON_CONTAINS(vr.problem_device_ids, CAST(#{problemDeviceId} AS CHAR), '$')
</if>
<if test="problemTag != null and problemTag != ''">
AND JSON_CONTAINS(vr.problem_tags, JSON_QUOTE(#{problemTag}), '$')
</if>
<if test="source != null and source != ''">
AND vr.source = #{source}
</if>
</where>
ORDER BY
<choose>
<when test="orderBy == 'rating'">
vr.rating
</when>
<when test="orderBy == 'update_time'">
vr.update_time
</when>
<when test="orderBy == 'id'">
vr.id
</when>
<otherwise>
vr.create_time
</otherwise>
</choose>
<choose>
<when test="orderDirection == 'ASC'">
ASC
</when>
<otherwise>
DESC
</otherwise>
</choose>
</select> </select>
</mapper> </mapper>

View File

@@ -1,13 +1,35 @@
package com.ycwl.basic.face.pipeline.integration; package com.ycwl.basic.face.pipeline.integration;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext; import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.pipeline.core.Pipeline;
import com.ycwl.basic.face.pipeline.enums.FaceMatchingScene; import com.ycwl.basic.face.pipeline.enums.FaceMatchingScene;
import com.ycwl.basic.face.pipeline.factory.FaceMatchingPipelineFactory; import com.ycwl.basic.face.pipeline.factory.FaceMatchingPipelineFactory;
import com.ycwl.basic.face.pipeline.stages.BuildSourceRelationStage;
import com.ycwl.basic.face.pipeline.stages.CreateTaskStage;
import com.ycwl.basic.face.pipeline.stages.CustomFaceSearchStage;
import com.ycwl.basic.face.pipeline.stages.DeleteOldRelationsStage;
import com.ycwl.basic.face.pipeline.stages.FaceRecognitionStage;
import com.ycwl.basic.face.pipeline.stages.FaceRecoveryStage;
import com.ycwl.basic.face.pipeline.stages.FilterByDevicePhotoLimitStage;
import com.ycwl.basic.face.pipeline.stages.FilterByTimeRangeStage;
import com.ycwl.basic.face.pipeline.stages.GeneratePuzzleStage;
import com.ycwl.basic.face.pipeline.stages.HandleVideoRecreationStage;
import com.ycwl.basic.face.pipeline.stages.LoadFaceSamplesStage;
import com.ycwl.basic.face.pipeline.stages.LoadMatchedSamplesStage;
import com.ycwl.basic.face.pipeline.stages.PersistRelationsStage;
import com.ycwl.basic.face.pipeline.stages.PrepareContextStage;
import com.ycwl.basic.face.pipeline.stages.ProcessBuyStatusStage;
import com.ycwl.basic.face.pipeline.stages.ProcessFreeSourceStage;
import com.ycwl.basic.face.pipeline.stages.RecordCustomMatchMetricsStage;
import com.ycwl.basic.face.pipeline.stages.RecordMetricsStage;
import com.ycwl.basic.face.pipeline.stages.SetTaskStatusStage;
import com.ycwl.basic.face.pipeline.stages.UpdateFaceResultStage;
import com.ycwl.basic.pipeline.core.Pipeline;
import com.ycwl.basic.service.pc.helper.ScenicConfigFacade;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.context.SpringBootTest; import org.mockito.InjectMocks;
import org.springframework.test.context.ActiveProfiles; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Arrays; import java.util.Arrays;
@@ -17,13 +39,55 @@ import static org.junit.jupiter.api.Assertions.*;
* Pipeline集成测试 * Pipeline集成测试
* 测试Pipeline的完整流程和Stage协作 * 测试Pipeline的完整流程和Stage协作
*/ */
@SpringBootTest @ExtendWith(MockitoExtension.class)
@ActiveProfiles("test")
class FaceMatchingPipelineIntegrationTest { class FaceMatchingPipelineIntegrationTest {
@Autowired @InjectMocks
private FaceMatchingPipelineFactory pipelineFactory; private FaceMatchingPipelineFactory pipelineFactory;
@Mock
private PrepareContextStage prepareContextStage;
@Mock
private RecordMetricsStage recordMetricsStage;
@Mock
private FaceRecognitionStage faceRecognitionStage;
@Mock
private FaceRecoveryStage faceRecoveryStage;
@Mock
private UpdateFaceResultStage updateFaceResultStage;
@Mock
private BuildSourceRelationStage buildSourceRelationStage;
@Mock
private ProcessFreeSourceStage processFreeSourceStage;
@Mock
private ProcessBuyStatusStage processBuyStatusStage;
@Mock
private HandleVideoRecreationStage handleVideoRecreationStage;
@Mock
private PersistRelationsStage persistRelationsStage;
@Mock
private CreateTaskStage createTaskStage;
@Mock
private SetTaskStatusStage setTaskStatusStage;
@Mock
private GeneratePuzzleStage generatePuzzleStage;
@Mock
private RecordCustomMatchMetricsStage recordCustomMatchMetricsStage;
@Mock
private LoadFaceSamplesStage loadFaceSamplesStage;
@Mock
private CustomFaceSearchStage customFaceSearchStage;
@Mock
private LoadMatchedSamplesStage loadMatchedSamplesStage;
@Mock
private FilterByTimeRangeStage filterByTimeRangeStage;
@Mock
private FilterByDevicePhotoLimitStage filterByDevicePhotoLimitStage;
@Mock
private DeleteOldRelationsStage deleteOldRelationsStage;
@Mock
private ScenicConfigFacade scenicConfigFacade;
/** /**
* 测试Pipeline工厂能够成功创建Pipeline * 测试Pipeline工厂能够成功创建Pipeline
*/ */
@@ -43,7 +107,7 @@ class FaceMatchingPipelineIntegrationTest {
// 验证Stage数量符合预期 // 验证Stage数量符合预期
assertEquals(13, autoMatchingNew.getStageCount()); assertEquals(13, autoMatchingNew.getStageCount());
assertEquals(13, autoMatchingOld.getStageCount()); assertEquals(12, autoMatchingOld.getStageCount());
assertEquals(15, customMatching.getStageCount()); assertEquals(15, customMatching.getStageCount());
assertEquals(3, recognitionOnly.getStageCount()); assertEquals(3, recognitionOnly.getStageCount());
} }

View File

@@ -1,5 +1,7 @@
package com.ycwl.basic.face.pipeline.stages; package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.biz.FaceStatusManager;
import com.ycwl.basic.enums.FaceCutStatus;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext; import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.pipeline.core.StageResult; import com.ycwl.basic.pipeline.core.StageResult;
import com.ycwl.basic.model.pc.face.entity.FaceEntity; import com.ycwl.basic.model.pc.face.entity.FaceEntity;
@@ -27,6 +29,9 @@ class CreateTaskStageTest {
@Mock @Mock
private TaskService taskService; private TaskService taskService;
@Mock
private FaceStatusManager faceStatusManager;
@InjectMocks @InjectMocks
private CreateTaskStage stage; private CreateTaskStage stage;
@@ -60,7 +65,7 @@ class CreateTaskStageTest {
assertTrue(result.getMessage().contains("自动创建任务成功")); assertTrue(result.getMessage().contains("自动创建任务成功"));
verify(scenicConfigFacade, times(1)).isFaceSelectFirst(10L); verify(scenicConfigFacade, times(1)).isFaceSelectFirst(10L);
verify(taskService, times(1)).autoCreateTaskByFaceId(1L); verify(taskService, times(1)).autoCreateTaskByFaceId(1L);
// verify(taskStatusBiz, never()).setFaceCutStatus(anyLong(), anyInt()); verify(faceStatusManager, never()).setFaceCutStatus(anyLong(), any(FaceCutStatus.class));
} }
@Test @Test
@@ -76,7 +81,7 @@ class CreateTaskStageTest {
assertTrue(result.isSkipped()); assertTrue(result.isSkipped());
assertTrue(result.getMessage().contains("等待用户手动选择")); assertTrue(result.getMessage().contains("等待用户手动选择"));
verify(scenicConfigFacade, times(1)).isFaceSelectFirst(10L); verify(scenicConfigFacade, times(1)).isFaceSelectFirst(10L);
// verify(taskStatusBiz, times(1)).setFaceCutStatus(1L, 2); verify(faceStatusManager, times(1)).setFaceCutStatus(1L, FaceCutStatus.WAITING_USER_SELECT);
verify(taskService, never()).autoCreateTaskByFaceId(anyLong()); verify(taskService, never()).autoCreateTaskByFaceId(anyLong());
} }
@@ -94,7 +99,7 @@ class CreateTaskStageTest {
assertTrue(result.getMessage().contains("任务创建失败")); assertTrue(result.getMessage().contains("任务创建失败"));
verify(scenicConfigFacade, times(1)).isFaceSelectFirst(10L); verify(scenicConfigFacade, times(1)).isFaceSelectFirst(10L);
verify(taskService, never()).autoCreateTaskByFaceId(anyLong()); verify(taskService, never()).autoCreateTaskByFaceId(anyLong());
// verify(taskStatusBiz, never()).setFaceCutStatus(anyLong(), anyInt()); verify(faceStatusManager, never()).setFaceCutStatus(anyLong(), any(FaceCutStatus.class));
} }
@Test @Test
@@ -119,8 +124,8 @@ class CreateTaskStageTest {
// Given: 设置状态失败 // Given: 设置状态失败
when(scenicConfigFacade.isFaceSelectFirst(10L)) when(scenicConfigFacade.isFaceSelectFirst(10L))
.thenReturn(true); .thenReturn(true);
// doThrow(new RuntimeException("Status set error")) doThrow(new RuntimeException("Status set error"))
// .when(taskStatusBiz).setFaceCutStatus(1L, 2); .when(faceStatusManager).setFaceCutStatus(1L, FaceCutStatus.WAITING_USER_SELECT);
// When // When
StageResult<FaceMatchingContext> result = stage.execute(context); StageResult<FaceMatchingContext> result = stage.execute(context);
@@ -128,7 +133,7 @@ class CreateTaskStageTest {
// Then // Then
assertTrue(result.isDegraded()); assertTrue(result.isDegraded());
assertTrue(result.getMessage().contains("任务创建失败")); assertTrue(result.getMessage().contains("任务创建失败"));
// verify(taskStatusBiz, times(1)).setFaceCutStatus(1L, 2); verify(faceStatusManager, times(1)).setFaceCutStatus(1L, FaceCutStatus.WAITING_USER_SELECT);
} }
@Test @Test

Some files were not shown because too many files have changed in this diff Show More