Compare commits

..

804 Commits

Author SHA1 Message Date
0ed12af8c9 test(facebody): 更新人脸识别适配器集成测试
Some checks failed
ZhenTu-BE/pipeline/head There was a failure building this commit
- 重命名 AliFaceBodyAdapterTest 为 AliFaceBodyAdapterIT
- 重命名 BceFaceBodyAdapterTest 为 B
2026-01-27 11:00:01 +08:00
ecbdec4518 test(puzzle 2026-01-27 09:47:33 +08:00
bf6b866e67 refactor(member): 更新用户信息方法传递用户ID参数
- 在AppMemberController中从BaseContextHandler获取用户ID并传递给服务层
- 修改AppMemberServiceImpl中的update方法签名以接收用户ID参数
- 更新AppMemberService接口定义以包含用户ID参数
- 移除服务实现中重复的BaseContextHandler调用
- 确保用户信息更新时使用正确的用户上下文
2026-01-26 14:50:39 +08:00
93f9c1486f feat(app): 添加微信小程序内容安全检测功能
- 集成 WxMpUtil 工具类用于微信小程序消息安全检测
- 在用户更新昵称时添加内容安全校验逻辑
- 使用景区配置的微信小程序 AppId 和 AppSecret 进行检测
- 当昵称包含违规内容时抛出参数错误异常
- 实现 msgSecCheck 方法调用微信小程序内容安全接口
- 添加 MSG_SEC_CHECK_URL 常量定义检测接口地址
2026-01-26 14:09:54 +08:00
e87e38be03 feat(order): 添加商品重复购买检查功能
- 集成重复购买检查策略工厂和上下文管理
- 实现基于商品类型的重复购买验证机制
- 添加价格计算结果中是否已购买的标识字段
- 扩展商品项目DTO以支持已购买状态标记
- 实现异常捕获方式的购买状态检测逻辑
- 集成Redis缓存提升重复购买检查性能
2026-01-26 11:06:45 +08:00
85d0fc0996 fix(video): 解决视频数据获取时的空指针异常
- 添加了 contentPageVO 为 null 的检查并返回 null 避免后续操作
- 添加了 videoRespVO 为 null 的检查并返回 null 避免后续操作
- 在流处理后添加了非空过滤确保结果列表不包含 null 值
2026-01-26 10:47:06 +08:00
d25d09cb66 feat(task): 添加自动替换vlog配置控制功能
- 实现scenicConfig管理器获取景区配置
- 添加auto_replace_vlog配置项,默认值为true
- 当auto_replace_vlog为false时跳过自动创建任务
- 添加视频存在检查避免重复处理
- 记录跳过任务的详细日志信息
2026-01-23 21:07:58 +08:00
c40c6a0966 1 2026-01-23 19:25:52 +08:00
4fc0984994 feat(coupon): 优化优惠券领取结果返回逻辑
- 移除控制器中只返回首个错误的限制,改为返回完整的领取结果列表
- 在 CouponClaimResult DTO 中新增 claimedRecords 字段用于返回已领取记录
- 添加 failureWithClaimedRecords 静态方法支持携带已领取记录的失败结果
- 当用户达到领取上限时查询并返回其已领取的券记录供前端展示
- 实现无论成功或失败都向客户端返回完整结果数据的功能
2026-01-23 18:04:13 +08:00
918ff860c3 feat(pricing): 更新用户优惠券查询接口返回完整信息
- 修改 getUserCoupons 接口不再需要传入 userId 参数,从上下文获取当前登录用户
- 新增 UserCouponResp DTO 包含领取记录和优惠券配置的完整信息
- 更新 ICouponService 接口返回类型为 UserCouponResp 列表
- 在 Controller 层添加 getUserId 方法用于获取当前登录用户ID
- 实现完整的用户优惠券信息组装逻辑,包含领取时间、过期时间等记录信息
2026-01-22 15:55:05 +08:00
8b3bea8bed feat(AppTemplateController): 添加景区模板内容列表接口
- 新增 ScenicTemplateContentVO 数据传输对象
- 实现 /scenic/{scenicId}/contents 接口获取景区模板内容
- 支持获取普通模板和拼图模板的基础信息
- 返回模板名称、分组、ID和封面图片等信息
- 设置普通模板商品类型为0,拼图模板商品类型为3
- 拼图模板固定分组为"氛围拼图"
2026-01-22 15:54:54 +08:00
be54bbaa82 1 2026-01-22 14:05:29 +08:00
68a674ba51 feat(coupon): 添加优惠券领取后有效期功能
- 在 PriceCouponClaimRecord 实体中添加过期时间字段
- 在 PriceCouponConfig 实体中添加领取后有效天数配置
- 更新查询可用优惠券的 SQL 条件以过滤过期券
- 修改插入领用记录的 SQL 语句以包含过期时间
- 实现领取时根据配置计算过期时间的逻辑
2026-01-22 14:03:28 +08:00
80f8a6b56b feat(task): 添加景区ID到微信订阅通知触发请求
- 在CouponExpireNotificationTask任务中为微信订阅通知请求添加scenicId字段
- 确保通知请求包含正确的景区标识信息
2026-01-21 19:04:30 +08:00
973bd73e9a feat(task): 添加渲染预览任务创建功能
- 引入渲染相关DTO和服务类用于预览任务创建
- 移除未使用的消息生产者和通知认证工具依赖
- 在任务创建后异步触发渲染预览任务生成
- 实现虚拟线程异步处理渲染预览任务创建逻辑
- 添加素材映射转换支持视频和图片类型识别
- 实现异常捕获确保主流程不受渲染服务影响
2026-01-21 15:51:25 +08:00
819caab047 render_v2 2026-01-21 14:32:13 +08:00
00bf4b5a8b chore(task): 2026-01-20 20:33:10 +08:00
6c305f4cd1 fix(task): 优化下载通知任务的执行频率和时间范围
- 移除未使用的ZtMessageProducerService和NotificationAuthUtils依赖注入
- 将定时任务执行时间从每天21点调整为每天9点和21点
- 将查询时间范围从24小时缩短为12小时,提高查询效率
- 保持用户去重逻辑以避免重复发送通知
2026-01-20 20:26:51 +08:00
82e844a779 feat(notify): 添加微信订阅消息去重功能
- 在 WechatSubscribeTemplateConfigEntity 中新增 dedupSeconds 字段用于配置去重窗口
- 将去重配置从事件模板映射复制到通知配置实体中
- 集成 RedisTemplate 实现基于时间窗口的消息去重机制
- 支持三种去重模式:永久去重(0)、不设去重(负数)、窗口期去重(正数)
- 实现基于 Redis 分布式锁的重复消息过滤逻辑
- 为非永久去重场景生成唯一数据库幂等键以避免冲突
2026-01-20 20:19:44 +08:00
c3fcfdd633 ```
style(app): 调整日期格式显示

- 将日期时间格式从 yyyy-MM-dd HH:mm 修改为 yyyy-MM-dd
```
2026-01-20 18:51:52 +08:00
a8156976be feat(puzzle): 添加免费拼图通知任务功能
- 在MemberPuzzleMapper中新增listFreeUnpurchased方法用于查询指定时间范围内生成且未购买的免费拼图记录
- 新增FreePuzzleNotificationTask定时任务类,每天晚7点执行免费拼图通知
- 添加SQL映射配置实现免费拼图记录的查询逻辑
- 实现微信订阅通知触发机制,向符合条件的用户发送免费拼图领取通知
- 集成景区信息查询和会员信息获取功能用于通知内容构造
2026-01-20 18:35:57 +08:00
ce48bd00c9 feat(task): 添加优惠券领取和过半提醒定时任务
- 新增优惠券领取通知定时任务,每小时执行一次查询最近2小时内领取的优惠券
- 新增优惠券有效期过半提醒定时任务,每天18点执行
- 引入ScenicRepository和ScenicV2DTO用于获取景区基础信息
- 修改processNotification方法提取公共逻辑到processRecords方法
- 在微信订阅消息变量中增加景区ID和景区名称字段
- 优化优惠券相关查询逻辑和数据处理流程
2026-01-20 17:11:20 +08:00
c5df277e6c feat(task): 添加优惠券过期提醒定时任务
- 实现优惠券过期提醒功能,每天20点执行
- 实现优惠券临期提醒功能,每天8点执行
- 集成微信订阅消息通知服务
- 查询指定时间范围内已领取的优惠券记录
- 构建优惠券信息变量并触发通知
- 添加异常处理和日志记录机制
2026-01-20 16:58:36 +08:00
9a31e71e42 refactor(coupon): 移除优惠券相关模块代码
- 删除优惠券控制器相关类,包括 AppCouponController 和 CouponController
- 移除优惠券记录控制器 CouponRecordController
- 删除优惠券数据访问层接口及实现类
- 移除优惠券相关的实体类、请求响应对象
- 清理业务逻辑层中与优惠券相关的服务接口及实现
- 从 PriceBiz 中移除优惠券相关导入依赖
- 从任务类 DownloadNotificationTasker 中移除优惠券相关导入
- 删除优惠券相关的 MyBatis 映射文件
2026-01-20 16:30:47 +08:00
e268d236f4 fix(coupon): 修复优惠券重复领取和状态检查逻辑
- 修改数据库查询方法返回类型为List以支持多条记录查询
- 更新AutoCouponServiceImpl中的重复领取检查逻辑
- 在CouponServiceImpl中实现可用优惠券筛选功能
- 优化优惠券状态验证逻辑并改进错误信息提示
- 修复使用优惠券时的状态判断条件
2026-01-20 15:53:33 +08:00
143426db1f refactor(glm): 优化 GLM 客户端实现
- 添加 @Lazy 注解以延迟初始化 GLM 客户端
- 避免应用启动时不必要的资源消耗
- 提高系统启动性能和内存使用效率
2026-01-20 15:52:27 +08:00
fcc4b06295 refactor(goods): 优化视频片段更新状态检查逻辑
- 移除无新片段情况下的冗余日志输出
- 将视频未关联任务的日志级别从error调整为warn
- 保持原有的业务逻辑判断不变
2026-01-20 15:52:13 +08:00
f876dc59fa feat(task): 添加资源通知定时任务功能
- 实现了每日19点执行的资源通知定时任务
- 查询当日新增人脸数据并获取相关会员信息
- 整合景区、视频和照片资源统计数据
- 集成微信订阅消息推送服务
- 构建资源通知模板变量并触发消息发送
- 添加异常处理和日志记录机制
2026-01-20 15:34:46 +08:00
8e6d10ad95 feat(watermark): 调整水印模板布局为原图完整显示
- 将原图区域从90%高度调整为100%完整高度
- 添加底部扩展10%区域用于信息展示
- 更新PuzzleDefault和PuzzlePrint模板的画布尺寸计算逻辑
- 修改二维码尺寸计算基准为原始图片高度
- 调整布局参数常量命名以反映新的设计思路
2026-01-20 11:36:25 +08:00
42bf3d3d0a refactor(puzzle): 优化拼图记录查询逻辑
- 移除 BigDecimal 导入并修改拼图数量统计方式
- 使用关联表查询替换直接的数量统计方法
- 更新拼图记录查询逻辑,通过关联表获取数据
- 添加对空值的过滤处理确保数据完整性
- 修改内容页面转换方法,支持免费状态判断
- 删除价格计算相关依赖和服务调用
- 添加 MemberPuzzleEntity 和 FreeStatus 常量支持
- 从关联记录读取免费状态替代价格计算逻辑
2026-01-20 11:24:57 +08:00
679f2d3a79 feat(order): 添加退款单号幂等性支持并完善订单状态流转验证
- 在RefundRequest中新增refundNo字段作为退款单号幂等键
- 添加订单状态、支付状态、退款状态流转验证逻辑
- 完善updateRefundStatus方法支持退款单号和支付平台退款单号参数
- 优化createRefundRecord方法增加退款单号重复性检查和幂等处理
- 重构支付回调处理逻辑统一状态更新方式
- 增强订单状态流转的安全性和一致性校验
2026-01-19 21:28:24 +08:00
3084afc6a7 fix(pricing): 修复升级检查中的支付金额处理逻辑
- 移除 resolvePaidAmount 方法中对 purchasedDetails 的依赖
- 添加支付金额为空时的异常抛出机制
- 在升单结果中添加补差价金额的判断逻辑
- 更新文档中标注支付金额为必填字段
- 优化打包优惠计算中的补差价处理流程
2026-01-19 20:28:36 +08:00
91626626f4 feat(pricing): 优化升单价格计算逻辑支持补差价功能
- 修改 UpgradeBundleDiscountResult 和 UpgradeOnePriceResult 中 estimatedFinalAmount 字段含义为补差价金额
- 在 UpgradeCheckRequest 中新增 paidAmount 字段用于传递已支付金额
- 在 UpgradeCheckResult 中新增 bestUpgradeType 和 bestPayableAmount 字段提供最优升单建议
- 在 UpgradePriceSummary 中新增 paidAmount 字段记录已支付金额
- 更新价格计算服务实现,加入已支付金额处理逻辑
- 新增 normalizeAmount、calculateSupplementAmount 等工具方法确保金额计算精度
- 修复测试代码中的数据类型不匹配问题
2026-01-19 20:25:44 +08:00
b1cfef278d Merge branch 'order_update'
# Conflicts:
#	src/main/java/com/ycwl/basic/pricing/CLAUDE.md
2026-01-19 19:54:59 +08:00
c42474256e fix(LyCompatibleController): 修复视频列表获取逻辑
- 修改了ContentPageVO的获取方式,使用filter过滤掉contentId为null的记录
- 使用findFirst替换getFirst避免空指针异常
- 确保只有有效的contentId才会被用于后续的视频查询操作
2026-01-19 19:32:09 +08:00
63180159d2 feat(puzzle): 实现免费拼图下载时自动添加水印功能
- 在AppPuzzleController中新增水印相关依赖注入
- 添加免费拼图判断逻辑,区分付费与免费拼图下载流程
- 实现addWatermarkForFreePuzzle方法处理水印添加
- 集成景区信息、人脸信息、二维码等水印模板数据
- 添加水印任务创建与等待机制,支持30秒超时处理
- 增加水印操作的日志记录与异常处理机制
- 优化免费拼图下载的安全性保护措施
2026-01-19 18:58:08 +08:00
e647ad75c6 feat(clickhouse): 实现统计数据查询的时间序列填充功能
- 将日期时间处理从旧的 Date 和 SimpleDateFormat 迁移到新的 Java 8 时间 API
- 添加小时级别数据序列填充功能,确保每个小时都有数据记录
- 添加日期级别数据序列填充功能,确保每天都有数据记录
- 实现缺失时间段的数据自动补零机制
- 重构查询方法以支持连续时间序列数据返回
- 提高统计图表数据完整性和可视化效果
2026-01-17 16:58:23 +08:00
4a07f5bba9 fix(puzzle): 修复拼图生成服务中的打印队列关联ID问题
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 将硬编码的0L替换为实际的拼图记录ID
- 确保打印队列记录能正确关联到对应的拼图生成记录
- 更新代码注释以明确ID的用途和关联关系
2026-01-17 03:24:43 +08:00
1f7e6d69f4 fix(app): 修复拼图记录ID传递问题
- 将硬编码的0L替换为实际的recordId参数
- 确保拼图记录能够正确关联到puzzle_record表
- 移除打印特有标识注释,统一使用拼图记录ID逻辑
2026-01-17 03:05:07 +08:00
50aaf7cb1a refactor(puzzle): 移除边缘渲染任务数据访问层
- 删除了 PuzzleEdgeRenderTaskMapper 接口文件
- 移除了对应的 MyBatis XML 映射文件
- 清理了数据库操作相关的实体映射配置
- 移除了任务领取、成功标记、失败标记等数据库操作方法
- 删除了查询下一条可领取任务的业务逻辑实现
2026-01-17 02:50:10 +08:00
f2c739160a feat(printer): 添加图片类型字段支持不同来源图片处理
- 在 MemberPrintEntity 和 MemberPrintResp 中新增 imageType 字段
- 根据 sourceId 自动设置图片类型为移动上传或普通照片
- 拼图类型图片明确标记为 PUZZLE 类型
- 修改图片来源判断逻辑从 sourceId 改为 imageType 字段
- 更新数据库映射文件添加 image_type 字段映射
2026-01-17 02:45:14 +08:00
2efc66292e fix(order): 修复优惠券使用请求中的景区ID设置问题
- 将优惠券使用请求中的景区ID从缓存结果字符串改为订单实际景区ID
- 添加空值检查以避免潜在的空指针异常
- 确保景区ID正确传递为字符串格式
2026-01-17 01:57:01 +08:00
0eced869fa feat(pricing): 支持无限量优惠券功能
- 修改数据库更新逻辑以支持无限量优惠券
- 当 total_quantity 为 NULL 或 <= 0 时不限制使用数量
- 使用 COALESCE 函数处理空值情况
- 更新 SQL 条件判断逻辑以兼容无限量场景
2026-01-17 01:55:49 +08:00
aa2611d369 feat(printer): 添加拼图打印功能支持
- 在ImageWatermarkOperatorEnum中新增PUZZLE_PRINT类型
- 在WatermarkEdgeService中添加PuzzlePrint水印模板支持
- 修改ImageWatermarkFactory对PUZZLE_PRINT类型的处理逻辑
- 移除PuzzleBorderStage并创建专用的拼图打印处理管线
- 实现createPuzzlePrintPipeline方法用于拼图水印处理
- 添加preparePuzzleWatermarkConfig方法准备拼图专用水印配置
- 更新打印机服务中的拼图处理流程以使用新的水印配置
2026-01-17 01:55:37 +08:00
6a8f679540 feat(clickhouse): 添加打印样片页面访问统计功能
- 实现按小时统计访问打印样片页面人数的功能
- 实现按日期统计访问打印样片页面人数的功能
- 在ClickHouse查询服务中添加相应的SQL查询方法
- 在MySQL查询服务中添加接口实现
- 更新统计图表合并逻辑,支持打印样片访问数据展示
- 修改mergeChartData方法以支持三组数据合并
- 在MyBatis映射文件中添加对应的SQL查询语句
- 完善相关接口定义和文档注释
2026-01-16 20:15:41 +08:00
4fac129c3a feat(image): 启用边缘端水印处理并优化徕卡模板布局算法
- 将边缘端处理默认启用状态从false改为true
- 将边缘端处理超时时间从30秒调整为10秒
- 将徕卡水印模板的固定像素配置转换为基于1920x1080的百分比配置
- 新增多种百分比常量包括底部区域、Logo大小、边距、字体大小等
- 实现动态计算实际像素值的方法替代固定数值
- 在PrinterServiceImpl中注入WatermarkEdgeService依赖
- 配置水印处理流程启用边缘服务和存储适配器
2026-01-16 18:56:29 +08:00
830dd17071 feat(printer): 添加自定义打印图片URL功能
- 在CreateVirtualOrderRequest中新增printImgUrl字段
- 修改createVirtualOrder方法支持自定义打印图片URL参数
- 实现当提供printImgUrl时优先使用该URL进行打印
- 更新服务接口和实现类以支持新的参数传递
- 添加相应的文档注释说明新功能特性
2026-01-16 18:30:39 +08:00
83c831887e refactor(service): 移除视频URL内网代理逻辑
- 删除移动端商品服务中的视频URL内网地址代理转换代码
- 移除PC端资源服务中的视频URL内网代理处理逻辑
- 简化视频URL设置流程,直接使用原始URL地址
- 清理相关的异常处理和日志记录代码
2026-01-16 18:14:25 +08:00
5ab2882777 refactor(watermark): 将水印布局配置改为百分比方式
- 将固定像素配置改为基于1920x1080的百分比配置
- 添加底部距离、二维码大小、位置等百分比常量
- 修改二维码位置计算逻辑为基于百分比的方式
- 调整景区名和日期时间文字的布局和对齐方式
- 移除原有的固定偏移计算方法
- 优化文字区域的垂直居中对齐效果
2026-01-16 17:43:31 +08:00
a5a9ff09f2 feat(watermark): 添加边缘端水印处理功能
- 引入 WatermarkEdgeService 支持边缘端渲染
- 在 WatermarkConfig 中添加边缘端相关配置参数
- 在 WatermarkStage 中实现边缘端处理逻辑和降级机制
- 修改 ImageWatermarkOperatorEnum 的默认输出格式为 jpg
- 移除已废弃的 DefaultImageWatermarkOperator 类
- 更新 GoodsServiceImpl 使用边缘端处理水印
- 优化 PuzzleEdgeWorkerIpInterceptor 允许本地回环地址访问
- 修正 PrinterDefaultWatermarkTemplateBuilder 样式常量名称
2026-01-16 17:25:19 +08:00
83e47ed843 refactor(goods): 移除预览功能并优化水印处理逻辑
- 移除了 sourceGoodsListPreview 接口及相关实现
- 新增 WatermarkEdgeService 和 FaceService 依赖注入
- 实现边缘端水印处理支持,包含降级机制
- 优化二维码和头像文件处理流程
- 统一水印处理的异常处理和资源清理逻辑
2026-01-16 17:19:31 +08:00
e9a4c26a83 refactor(watermark): 调整徕卡水印模板构建器的画布布局策略
- 画布大小改为原图大小(不再扩展底部区域)
- 原图收缩后放置在画布上半部分,为底部留出空间
- 计算原图收缩后的区域高度和底部区域起始Y坐标
- 将原图元素从画布顶部调整为收缩后放在画布上半部分
- 调整Logo、帧途文字和二维码元素的Y坐标计算方式
- 更新布局说明文档以反映新的设计策略
2026-01-16 17:07:48 +08:00
8c76a4fb03 refactor(printer): 简化人脸二维码生成逻辑
- 移除原有的复杂二维码生成和文件操作代码
- 使用 pcFaceService.bindWxaCode 方法替代
- 直接重定向到生成的二维码 URL
- 消除临时文件创建和删除操作
- 简化 HTTP 响应处理流程
2026-01-16 16:26:47 +08:00
8198b0c537 feat(watermark): 添加拼图水印模板构建器
- 实现拼图默认水印模板构建器,支持原图区域和底部信息区域布局
- 实现拼图打印水印模板构建器,增加四周白边设计
- 配置二维码、头像、景区名和日期时间的文字布局
- 支持动态数据绑定和图片元素的COVER模式显示
- 提供可选的头像圆形裁剪功能和右对齐文字显示
2026-01-16 16:16:59 +08:00
0235d1d121 feat(watermark): 添加水印边缘渲染模板构建功能
- 实现抽象水印模板构建器基类提供通用构建工具方法
- 定义水印模板构建器接口规范模板构建契约
- 实现徕卡风格水印模板构建器支持底部扩展布局
- 实现普通风格水印模板构建器支持左下角布局
- 实现打印专用水印模板构建器支持缩放和偏移
- 创建水印边缘任务服务统一管理模板构建流程
- 添加水印请求参数类定义边缘渲染所需字段
- 实现水印模板构建结果类封装模板元素和动态数据
- 集成拼图边缘渲染任务服务实现异步渲染机制
2026-01-16 15:21:38 +08:00
8d5a10cce1 feat(puzzle): 添加水印拼图功能支持
- 在 PuzzleEdgeRenderTaskEntity 中新增 taskType 和 watermarkType 字段
- 添加 TASK_TYPE_PUZZLE 和 TASK_TYPE_WATERMARK 常量定义
- 新增 PuzzleWatermarkMapper 依赖注入
- 实现 handleWatermarkTaskSuccess 方法处理水印拼图任务成功逻辑
- 修改 taskSuccess 方法根据任务类型分别处理原始拼图和水印拼图
- 新增 createWatermarkRenderTask 方法创建水印拼图边缘渲染任务
- 为水印拼图任务添加独立的存储目录和文件命名规则
- 实现水印拼图结果写入 puzzle_watermark 表的功能
2026-01-16 13:56:29 +08:00
eba727b446 feat(puzzle): 添加拼图水印功能支持
- 创建 PuzzleWatermarkEntity 实体类用于存储拼图水印信息
- 定义水印类型、URL、关联记录ID等关键字段
- 实现 PuzzleWatermarkMapper 数据访问层接口
- 提供新增水印记录的 insert 方法
- 添加批量查询水印的 listByRecordIds 方法
- 实现按记录和类型查询单条水印的 getByRecordAndType 方法
- 支持按人脸ID和水印类型进行条件查询
- 为拼图不同场景下的水印版本管理提供数据支撑
2026-01-16 13:55:40 +08:00
27a18096b5 feat(face): 添加小程序码异步预生成功能
- 在人脸创建后异步预生成小程序码,提升后续获取速度
- 实现小程序码文件按日期目录存储优化文件管理
- 添加阿里云OSS内网域名替换为公网域名的逻辑
- 增加小程序码文件存在性检查避免重复生成
- 添加异步任务异常处理和日志记录机制
- 优化文件路径命名规则提高系统可维护性
2026-01-16 11:59:01 +08:00
d15d070cb4 refactor(puzzle): 重构拼图功能实现会员拼图关联管理
- 移除原有的图片裁切功能和userArea字段
- 删除originalImageUrl字段,统一使用resultImageUrl
- 添加MemberPuzzleEntity实体类管理会员拼图关联关系
- 创建MemberPuzzleMapper接口及XML映射文件
- 实现PuzzleRelationProcessor处理器负责关联记录创建
- 在拼图生成完成后自动创建会员拼图关联记录
- 添加景区配置中的免费拼图数量设置
- 实现免费拼图逻辑控制
- 更新拼图模板和生成记录的数据结构
- 修改AppPuzzleController中图片URL的获取方式
- 优化PuzzleEdgeRenderTaskService中的图片处理流程
2026-01-16 11:23:21 +08:00
fb4568721a feat(template): 添加景区模板封面接口的token忽略注解
- 在AppTemplateController中导入IgnoreToken注解
- 为getScenicTemplateCoverUrls方法添加@IgnoreToken注解以允许匿名访问
- 确保景区模板封面获取接口无需身份验证即可访问
2026-01-15 18:25:28 +08:00
63d31d69a9 feat(puzzle): 实现拼图模板缓存功能
- 集成 PuzzleRepository 缓存层替代直接数据库查询
- 在 PriceBiz 中使用缓存查询拼图模板数据
- 在 AppTemplateController 中添加景区模板封面URL批量获取接口
- 在 PuzzleTemplateServiceImpl 中实现模板增改时的缓存清理逻辑
- 在 FaceServiceImpl 中使用缓存查询拼图模板
- 优化模板查询性能并减少数据库压力
2026-01-15 17:01:17 +08:00
2fb6aa42cf feat(image): 添加图片叠加功能支持
- 新增 OverlayImageConfig 类用于配置叠加图片参数
- 支持通过 imageKey 从 dynamicData 动态获取图片 URL
- 提供默认图片 URL 配置选项
- 支持设置叠加图片宽高比例(0.0-1.0 范围)
- 实现图片适配模式配置(CONTAIN、COVER、FILL、SCALE_DOWN)
- 添加圆角半径配置,支持自动圆形效果
- 支持水平垂直对齐方式设置(CENTER、LEFT、RIGHT、TOP、BOTTOM)
- 提供水平垂直偏移量调节功能
- 更新配置验证逻辑,增加叠加图片配置校验
- 修改图片 URL 校验规则,支持动态数据填充
- 更新 JSON Schema 配置模板,包含叠加图片配置项
2026-01-15 13:36:13 +08:00
fed92c5445 feat(face): 添加人脸绑定功能
- 实现了 faceId 绑定接口
- 集成了人脸识别服务匹配功能
- 添加了绑定状态参数支持
2026-01-14 21:59:56 +08:00
6d774e4d76 fix(print): 修复打印队列添加逻辑
- 将打印记录ID参数
2026-01-14 10:11:07 +08:00
57b71c309e feat(task): 添加视频ID到下载通知任务
- 在DownloadNotificationTasker中添加videoId变量到模板参数
- 将item.getVideoId()方法调用结果存入variables映射中
- 确保视频ID信息能够在通知模板中正确渲染使用
2026-01-13 11:17:34 +08:00
93e28828ad fix(statistics): 修复统计数据合并中的类型转换问题
- 将订单数据转为 Map 时使用 String.valueOf 处理 Object 类型数值
- 在合并数据时对时间键和金额字段进行字符串类型转换
- 防止因数值类型不匹配导致的数据丢失问题
2026-01-12 22:36:00 +08:00
f8c6604a8a refactor(statistics): 切换数据查询服务并优化扫码统计功能
- 将 BrokerBiz 和 OrderBiz 中的数据查询从 StatisticsMapper 切换到 StatsQueryService
- 更新 StatisticsServiceImpl 使用 StatsQueryService 进行数据查询
- 添加订单数据合并功能到扫码统计图表中
- 重构扫码统计查询逻辑以支持统计数据和订单数据的合并显示
- 新增按小时和按日期统计订单数据的查询方法
- 优化 SQL 查询以分离统计数据和订单数据的查询逻辑
2026-01-12 18:30:27 +08:00
3bd658cc1f refactor(clickhouse): 优化景点统计数据查询逻辑
- 将原有的 scenicId 参数匹配条件替换为子查询方式
- 统一使用 enterScenicTraceIdSubQuery 方法处理景点访问轨迹ID筛选
- 移除重复的时间范围过滤条件以提高查询效率
- 保持 LOAD 和 FACE_UPLOAD 操作的数据统计一致性
- 简化 LAUNCH 操作的时间范围过滤逻辑
- 提高代码可维护性和查询性能
2026-01-12 17:43:43 +08:00
7b417aa4f1 fix(clickhouse): 修复查询时间范围条件处理逻辑
- 修改了小时统计查询中的时间范围条件,添加空值检查
- 修改了天统计查询中的时间范围条件,添加空值检查
- 将固定的时间范围查询改为可选的时间范围过滤
- 避免当开始或结束时间为空时的SQL语法错误
- 确保查询参数的灵活性和安全性
2026-01-12 13:46:51 +08:00
6ca7dceb0e feat(wechat): 支持微信订阅消息模板渲染嵌套数据结构
- 实现renderValue方法支持递归渲染Map类型的值
- 添加对非字符串类型值的直接返回处理
- 在任务服务中根据分组配置动态设置视频结果页面变量
- 为分组启用场景添加travelVideoCenter页面配置
- 为非分组场景保留videoSynthesis页面配置
2026-01-12 12:54:26 +08:00
0b3dd19de5 rm template v2 2026-01-11 22:25:51 +08:00
e56c2e6642 refactor(printer): 移除 WebSocket 任务推送功能
- 删除 PrinterTaskPushService 接口及其实现类
- 移除 WebSocketConfig 配置类及 PrinterWebSocketHandler 处理器
- 删除 WebSocket 相关模型类包括 WsMessage、WsMessageType、ErrorData、TaskAckData
- 移除 PrinterConnectionManager 连接管理器
- 从 PrinterServiceImpl 中删除 taskPushService 的依赖注入
- 移除创建打印任务时的 WebSocket 推送逻辑
- 移除审核通过任务的 WebSocket 推送逻辑
- 移除重新打印任务的 WebSocket 推送逻辑
2026-01-11 22:24:14 +08:00
482789b523 feat(task): 根据景区配置动态设置视频结果页面
- 获取景区配置管理器并检查分组功能是否启用
- 当分组功能启用时将视频结果页面设置
2026-01-11 00:04:04 +08:00
d902b480b8 fix(print): 修复打印队列添加功能
- 将打印记录ID参数修改为固定值0L
- 添加代码注释说明打印特有逻辑
2026-01-10 20:37:56 +08:00
fc0d5fed9b refactor(puzzle): 移除 worker 认证逻辑并简化任务处理
- 删除 PuzzleEdgeWorkerAuthRequest 认证请求类
- 移除 Controller 中的 accessKey 参数验证
- 删除 RenderWorkerEntity 和 RenderWorkerRepository 相关依赖
- 使用默认 workerId 替代动态 worker 验证逻辑
- 将 IP 验证职责移至拦截器层
- 简化客户端状态上报处理逻辑
- 统一任务处理流程中的 workerId 使用方式
2026-01-10 20:33:03 +08:00
31b9220a32 feat(notification): 添加视频任务统计信息到微信订阅通知
- 在任务服务中添加视频设备数量、镜头数量和拍摄时间变量
- 注入VideoTaskRepository依赖以获取任务统计数据
- 更新下载通知任务器中的变量映射逻辑
- 格式化日期时间为yyyy-MM-dd HH:mm格式
- 移除未使用的导入和重复的依赖注入
2026-01-10 20:30:15 +08:00
c4b78f1b09 fix(puzzle): 修复拼图记录图片URL获取错误
- 将resultImageUrl替换为originalImageUrl以正确获取原始图片URL
- 修正了拼图记录中图片URL为空时的错误提示逻辑
- 确保拼图功能能够正确处理原始图片URL而非结果图片URL
2026-01-10 20:29:10 +08:00
c9cc90c842 feat(notify): 添加批量查询用户授权余额功能
- 新增批量查询用户授权余额接口 /api/mobile/notify/auth/batch-remaining
- 实现批量检查用户对多个模板的授权记录功能
- 添加景区所有场景及模板列表查询接口并支持缓存
- 优化授权记录查询性能,使用批量查询替代逐个查询
- 新增批量查询请求对象 BatchRemainingCountReq 和响应对象 WechatSubscribeAllScenesResp
- 在数据层添加批量查询用户授权记录的 SQL 映射
- 实现缓存管理机制,支持所有场景模板配置的缓存读写与清理
2026-01-10 17:30:48 +08:00
02f1392355 feat(printer): 添加人脸图片URL重定向功能
- 实现通过人脸样本ID重定向到人脸图片URL的功能
- 实现通过人脸ID重定向到人脸图片URL的功能
- 添加404状态码处理当人脸数据不存在或URL为空的情况
- 使用response.sendRedirect实现URL重定向逻辑
2026-01-10 14:59:58 +08:00
d02aca9bf1 chore(AppFaceController): 移除人脸绑定功能的实现代码
- 注释掉 JWT 用户信息获取逻辑
- 移除人脸服务绑定调用
- 添加临时占位注释替代原有业务逻辑
2026-01-10 14:47:58 +08:00
05e269a305 fix(printer): 修复虚拟订单二维码生成问题
- 添加会员信息查询逻辑
- 实现虚拟订单使用无限二维码生成功能
- 非虚拟订单保持原有二维码生成方式
- 解决faceId绑定页面路径参数传递问题
2026-01-10 14:46:39 +08:00
74c146c104 feat(printer): 添加图像增强功能支持
- 在CreateVirtualOrderRequest中新增needEnhance字段
- 修改createVirtualOrder方法支持图像增强参数传递
- 更新setUserIsBuyItem方法以支持图像增强选项
- 在processPhotoWithPipeline调用中传入图像增强参数
- 为虚拟订单创建流程添加图像增强功能支持
2026-01-10 14:12:25 +08:00
42000df311 feat(order): 添加照片日记产品类型的价格计算支持
- 新增 case 5 分支处理照片日记产品类型
- 创建 PhotoLog 产品的价格计算请求对象
- 设置产品类型为 PHOTO_LOG 并配置相关参数
- 调用价格计算服务获取最终价格和原价
- 设置价格对象的 faceId 和 scenicId 字段
- 实现仅查询价格不使用优惠的预览模式
2026-01-09 22:58:29 +08:00
8b7f3d8eae fix(face-matching): 修复无人脸匹配结果时切片状态未更新问题
- 无匹配结果时将切片状态设置为已完成,避免前端一直显示"合成中"
- 确保人脸状态管理器正确更新切片状态为COMPLETED
2026-01-09 22:18:44 +08:00
6e345f2da4 refactor(pricing): 优化优惠券配置实体和领取逻辑
- 将时间字段类型从 LocalDateTime 改为 Date
- 为优惠券领取数量更新添加无条件增加方法
- 区分有限量和无限量优惠券的领取处理逻辑
- 实现有总量限制优惠券的库存检查机制
- 统一更新已领取数量的计数逻辑
2026-01-08 17:27:27 +08:00
d7c2c5b830 fix(coupon): 修复优惠券适用商品类型为空时的处理逻辑
- 添加空数组检查,当适用商品类型为空时不进行过滤
- 修复商品类型为空时直接返回全部商品总价的逻辑
- 保持原有商品类型过滤功能的完整性
2026-01-08 17:11:07 +08:00
07593694c8 feat(pricing): 添加优惠券配置中的申领数量和用户申领限制字段
- 在 PriceCouponConfigMapper 中新增 claimed_quantity 和 user_claim_limit 字段映射
- 更新 INSERT 语句以包含新的申领相关字段
- 修改 insertCoupon 方法以支持优惠券申领数量控制功能
2026-01-08 16:48:53 +08:00
3ff76a0bea fix(goods): 修复商品服务中视频任务状态返回逻辑
- 检查是否有完成的模板,如果没有则返回待处理状态
- 在计数为0时提前返回响应,避免后续处理逻辑执行
2026-01-08 14:37:31 +08:00
5952390093 fix(service): 修复视频任务状态和内容ID设置问题
- 修复当完成数量小于等于0时视频任务状态设置为待处理
- 调整内容页面VO中内容ID的设置顺序以确保正确赋值
2026-01-08 11:11:47 +08:00
e896f58d82 perf(notify): 优化微信订阅消息配置查询性能
- 为微信订阅消息配置接口添加 Redis 缓存支持
- 在 WechatSubscribeNotifyConfigRepository 中实现缓存读写和清除机制
- 修改 Controller 层接口添加 @IgnoreToken 注解支持匿名访问
- 优化查询逻辑,添加 memberId 为空时的提前返回处理
- 在管理服务中添加缓存清除逻辑,确保配置变更时缓存同步更新
- 实现批量缓存清除功能,支持按景区和全局范围清除缓存
2026-01-07 17:40:58 +08:00
3291371dd7 feat(puzzle): 添加景区模板列表缓存功能
- 新增景区模板列表缓存KEY常量PUZZLE_TEMPLATES_BY_SCENIC_KEY
- 在清除模板缓存时同步清除对应景区的模板列表缓存
- 实现listTemplateByScenic方法根据景区ID获取启用模板列表并缓存
- 实现clearTemplateByScenicCache方法清除景区模板列表缓存
- 重构人脸匹配编排器使用新的缓存方法替代原有数据库查询
- 移除过期的redisTemplate依赖
2026-01-07 17:40:58 +08:00
917668da0c refactor(puzzle): 优化拼图模板生成逻辑
- 移除 Redis 缓存检查机制
- 改用 PuzzleRepository 直接查询拼图模板数据
- 更新日志记录格式,使用 e.getMessage() 替代完整异常对象
- 调整依赖注入顺序,添加 PuzzleRepository 注入
- 简化模板列表查询逻辑,提升代码可读性
2026-01-07 17:40:58 +08:00
d3884c8aa2 refactor(facebody): 移除重复的日志记录
- 移除了重试成功时的冗余日志输出
- 保持了原有的重试逻辑和错误处理机制
- 优化了代码的可读性和日志输出的合理性
2026-01-07 17:40:58 +08:00
a652124a93 refactor(FaceMatchingOrchestrator): 重构人脸匹配拼图生成逻辑
- 移除外层异常捕获处理,将异常处理移到模板循环内部
- 将日志级别从 info 调整为 debug,减少不必要的日志输出
- 优化代码结构,移除多余的 try-catch 包装
- 保持原有的业务逻辑不变,仅调整代码组织方式
- 确保异常处理不影响主流程执行
2026-01-07 17:40:58 +08:00
54cdee333d feat(puzzle): 添加拼图素材版本缓存优化重复生成
- 新增 puzzleSourceVersionCache 缓存用于记录拼图素材版本
- 实现 isPuzzleSourceChanged 方法判断素材是否变化
- 添加 markPuzzleSourceVersion 方法标记当前素材版本
- 实现 invalidatePuzzleSourceVersion 方法清除指定人脸缓存
- 在人脸关系变更时自动清除相关拼图素材版本缓存
- 重构 AppPuzzleController 使用 PuzzleRepository 替代直接访问 Mapper
- 添加生成记录缓存机制,包括按人脸ID和记录ID的缓存
- 实现素材版本缓存命中时复用历史记录功能
- 优化重复内容检测逻辑,添加缓存标记机制
- 在各种生成流程中添加缓存清除逻辑确保数据一致性
2026-01-07 17:40:58 +08:00
286062a81a feat(app-statistics): 添加实时统计模式支持
- 在非实时模式下才写入当天统计缓存
- 实时模式由调用方自行控制写入目标日期
- 添加了实时模式查询的逻辑分支处理
2026-01-07 01:45:15 +08:00
e0856a1b9c feat(pricing): 添加场景优惠券功能
- 创建场景优惠券领取控制器,提供前端优惠券领取接口
- 创建场景优惠券配置管理控制器,提供后台管理端配置接口
- 定义场景优惠券领取和配置相关的请求响应DTO
- 创建场景优惠券配置实体和数据库表结构
- 实现场景优惠券配置的数据访问和业务逻辑处理
- 实现场景优惠券领取功能,支持景区隔离和默认配置回退
- 添加优惠券领取状态检查和用户限制验证逻辑
- 实现分页查询和配置管理功能
2026-01-06 18:30:23 +08:00
123a081eab refactor(notifications): 重构通知系统使用统一的微信订阅通知触发服务
- 移除 UserNotificationAuthController 中的 getScenicTemplatesWithAuth 方法
- 从 ScenicRepository 中删除微信模板ID相关方法和配置
- 重命名 WechatSubscribeNotifyTriggerService 为 notifyTriggerService
- 更新 TaskTaskServiceImpl 中的视频生成通知逻辑
- 重构 DownloadNotificationTasker 中的通知发送方式
- 统一使用 WechatSubscribeNotifyTriggerRequest 和 WechatSubscribeNotifyTriggerResult
- 移除 ZT 消息服务相关代码
- 简化变量传递和通知模板逻辑
2026-01-06 15:35:09 +08:00
95e86fb996 refactor(video): 移除设备视频连续性检查定时任务
- 删除了 DeviceVideoContinuityCheckTask 定时任务类
- 从 DeviceVideoContinuityController 中移除手动检查接口
- 从生产环境日志配置中移除相关日志记录器配置
- 移除了 RedisTemplate 和 ObjectMapper 的依赖注入
- 移除了设备视频连续性检查相关的定时任务逻辑
- 移除了手动触发检查的 API 接口实现
2026-01-06 14:57:11 +08:00
6c3a413778 refactor(task): 移除未使用的常量和依赖注入
- 移除未使用的 VIDEO_NOTIFICATION_CACHE_KEY 常量
- 移除未使用的 NOTIFICATION_CACHE_EXPIRE_MINUTES 常量
- 移除未使用的 faceMapper 依赖注入
- 移除未使用的 faceSampleMapper 依赖注入
- 移除未使用的 sourceMapper 依赖注入
2026-01-06 13:35:00 +08:00
da2286bc80 Merge branch 'notify_v2' 2026-01-06 11:30:52 +08:00
1df6a4bc23 refactor(order): 优化重复购买检查器的延迟初始化
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 在DuplicatePurchaseCheckerFactory类上添加@Lazy注解实现延迟加载
- 在NoCheckDuplicateChecker类上添加@Lazy注解实现延迟加载
- 在ParentResourceDuplicateChecker类上添加@Lazy注解实现延迟加载
- 在UniqueResourceDuplicateChecker类上添加@Lazy注解实现延迟加载
- 添加org.springframework.context.annotation.Lazy导入语句
- 通过延迟初始化提升应用启动性能
2026-01-05 18:12:25 +08:00
981a4ba7bd perf(logging): 移除视频切分任务中的冗余日志输出
- 移除了 VideoRecreationHandler 中的 info 级别日志
- 移除了 TaskFaceServiceImpl 中的 info 级别日志
- 移除了 TaskTaskServiceImpl 中的 info 级别日志
- 保留了 debug 级别的视频重切逻辑日志
- 减少了生产环境中的日志输出量
2026-01-05 18:04:05 +08:00
017ced34fa change(FaceMatchingOrchestrator): 修改图像输出格式和质量设置
- 将输出格式从PNG更改为JPEG
- 将图像质量从90调整为80
2026-01-05 18:03:57 +08:00
a9ae00d580 refactor(puzzle): 重构拼图同步生成逻辑
- 添加详细的方法执行日志记录
- 实现参数校验和模板查询验证
- 增加元素排序和动态数据构建
- 集成重复图片检测机制
- 添加内容去重检测和历史记录复用
- 实现边缘渲染任务创建和
2026-01-05 16:02:28 +08:00
99f75b6805 style(log): 移除日志输出语句
- 移除了 BceFaceBodyAdapter 中无法访问URL图片的警告日志
- 移除了 VideoPieceGetter 中计数器更新和进度检查的调试日志
- 清理了设备关联计数器相关的日志输出
- 移除了 placeholder 完成状态的日志记录
- 删除了进度检查相关的统计日志输出
2026-01-05 14:59:33 +08:00
295815f1fa feat(puzzle): 添加拼图渲染任务同步等待机制
- 引入 CompletableFuture 支持任务异步等待
- 创建 TaskWaitResult 类封装任务执行结果
- 实现 registerWait 和 waitForTask 方法支持同步等待
- 添加 waitFutures 缓存池管理等待任务
- 实现超时清理机制防止内存泄漏
- 提供 createAndWait 便捷方法一键创建并等待
- 在任务完成和失败时自动通知等待方
- 添加过期 future 清理机制优化内存使用
2026-01-05 14:59:25 +08:00
010bac1091 test(integration): 添加集成回退服务的单元测试
- 验证缓存存在时返回缓存值的功能
- 测试无缓存时调用远程服务并缓存结果
- 验证远程调用失败且无缓存时抛出异常
- 测试清除单个缓存项功能
- 验证清除服务所有缓存项功能
- 测试获取缓存统计信息功能
- 验证并发请求时只调用一次远程服务的互斥锁机制
2026-01-05 14:56:51 +08:00
eb9b781fd3 Merge branch 'puzzle_edge_w'
# Conflicts:
#	src/main/java/com/ycwl/basic/config/WebMvcConfig.java
2026-01-05 11:58:56 +08:00
8d3dae32f3 feat(task): 添加版本校验和任务重分配功能
- 实现版本号比较方法,支持版本号大小判断
- 添加客户端版本校验逻辑,防止低版本上报覆盖高版本缓存
- 增加任务重分配功能,在更新旧任务时解除任务分配
- 修复worker状态处理中的版本冲突问题
2026-01-05 11:54:07 +08:00
43775f550b refactor(clickhouse): 修复日期格式化器线程安全问题
- 移除静态 SimpleDateFormat 实例,避免线程安全问题
- 添加上海时区配置确保日期格式化一致性
- 创建新的日期和日期时间格式化器方法
- 修改格式化方法使用新创建的格式化器实例
- 更新每日扫描统计查询中的日期格式化逻辑
2026-01-04 14:47:37 +08:00
24f72091b3 fix(stats): 修复景点人脸识别统计数据查询逻辑
- 修正了人脸上传统计查询中景点ID的过滤方式,从子查询改为直接解析params字段
- 移除了应用统计服务中的过期缓存逻辑
- 修复了任务完成用户统计的表关联错误,从task表改为member_video表进行统计
2026-01-04 14:43:01 +08:00
cc62fb4c18 refactor(clickhouse): 优化统计查询SQL性能和代码结构
- 提取进入景区trace_id子查询逻辑到独立方法appendEnterScenicTraceIdSubQuery
- 将count函数替换为uniqExact以提高去重统计性能
- 优化视频预览统计查询,使用WITH子句提取JSON字段减少重复计算
- 简化经纪人ID列表查询,移除不必要的子查询包装
- 修复每日扫码统计查询的时间范围过滤条件
- 优化按小时和按日期的扫码会员图表查询,使用ClickHouse内置时间函数
- 在子查询中添加时间范围过滤以减少数据扫描量
2026-01-04 13:53:37 +08:00
d1962ed615 refactor(clickhouse): 将统计数据查询从 MyBatis 迁移到 JDBC 模板
- 移除 ClickHouseStatsMapper 接口及 XML 映射文件
- 使用 NamedParameterJdbcTemplate 替代 MyBatis 实现数据查询
- 添加日期格式化工具类处理 ClickHouse 时间格式
- 重构所有统计查询方法使用原生 SQL 字符串构建
- 添加 MySQL 主数据源配置确保多数据源正确配置
- 升级 ClickHouse JDBC 驱动版本到 0.8.5
- 解决 0.6.x 版本参数绑定问题通过手动 SQL 构建
- 保持原有查询逻辑不变仅改变实现方式
2026-01-04 13:17:01 +08:00
e1023b6ea8 refactor(stats): 移除统计追踪模块相关代码
- 删除 StatsBiz 业务类
- 移除 TraceController 控制器及其接口实现
- 删除 AddTraceReq 数据传输对象
- 移除 StatsEntity 和 StatsRecordEntity 实体类
- 移除 StatsInterceptor 拦截器
- 删除 StatsMapper 和 StatsRecordMapper 数据访问接口
- 移除 StatsService 服务接口及 StatsServiceImpl 实现类
- 删除 StatsUtil 工具类
2026-01-04 12:16:01 +08:00
aec5e57df7 feat(database): 迁移统计数据查询到ClickHouse
- 添加ClickHouse数据源配置和相关依赖
- 实现ClickHouse统计查询服务和MySQL兜底方案
- 新增扫码统计、订单统计等数据查询接口
- 重构分销员数据统计逻辑,整合MySQL和ClickHouse数据源
- 更新应用配置文件以支持ClickHouse启用开关
- 修改分布式任务统计以支持跨库查询场景
2026-01-04 10:34:17 +08:00
52ce26e630 feat(puzzle): 添加拼图边缘渲染功能
- 集成 PuzzleEdgeWorkerIpInterceptor 拦截器进行 IP 校验
- 添加 PuzzleEdgeWorkerSecurityProperties 配置类
- 创建 PuzzleEdgeRenderTaskController 提供边缘渲染接口
- 添加多种 DTO 类用于边缘渲染任务数据传输
- 创建 PuzzleEdgeRenderTaskEntity 实体和 Mapper 接口
- 实现 PuzzleEdgeRenderTaskService 核心服务逻辑
- 重构 PuzzleGenerateServiceImpl 使用边缘渲染服务
- 移除原有的线程池执行器和同步渲染逻辑
- 添加定时任务处理渲染超时和重试机制
- 实现自动打印队列添加功能
2026-01-03 23:47:37 +08:00
32297dc29c refactor(printer): 优化人脸素材查询逻辑
- 移除不必要的MemberSourceEntity和相关Repository依赖
- 将数据查询逻辑从Repository层迁移到Mapper层
- 添加type参数支持素材类型过滤
- 修复方法注释中的人脸ID描述错误
- 直接返回SourceEntity列表避免额外的转换操作
2026-01-03 23:46:58 +08:00
21d8c56e82 feat(puzzle): 添加拼图模块缓存仓库并集成缓存功能
- 新增 PuzzleRepository 缓存仓库类,提供模板和元素的 Redis 缓存功能
- 实现模板按 ID 和编码的双向缓存,减少数据库查询压力
- 实现元素列表按模板 ID 缓存,避免重复查询
- 在模板服务中集成缓存,更新和删除时自动清除相关缓存
- 在生成服务中使用缓存读取模板和元素数据
- 添加缓存过期机制,设置 24 小时自动过期
- 实现批量缓存清除功能,支持按模式删除缓存
2026-01-01 21:39:43 +08:00
f8374519c3 feat(puzzle): 添加拼图生成异步处理能力
- 移除 @RequiredArgsConstructor 注解,改用手动构造函数注入
- 添加 ThreadPoolExecutor 实现拼图生成异步处理
- 新增 generateAsync 方法支持异步生成拼图
- 新增 generateSync 方法支持同步生成拼图
- 重构核心生成逻辑为 doGenerateInternal 方法供同步异步共用
- 在 FaceMatchingOrchestrator 中优化拼图模板生成逻辑
- 支持根据场景选择同步或异步生成模式
- 添加线程池队列大小监控和日志记录
2026-01-01 21:26:34 +08:00
44f5008fd1 refactor(task): 优化重复任务处理逻辑
- 移除旧任务更新逻辑,简化重复任务处理流程
- 替换视频查询方法调用,使用新的存储库方法
- 保持任务缓存清除功能
- 简化日志输出信息
2026-01-01 19:55:33 +08:00
6e0ebcd1bd fix(puzzle): 优化拼图生成失败日志记录
- 修改日志记录格式,添加异常消息详情
- 保持错误响应信息的一致性
2026-01-01 19:43:13 +08:00
5caf9a0ebf refactor(face): 重构人脸服务接口和实现
- 修改 getById 方法返回类型为 FaceEntity 并直接调用仓库层
- 移除删除人脸时的用户权限检查逻辑
- 删除 contentListUseDefaultFace 方法的实现
- 从服务接口中移除 contentListUseDefaultFace 方法定义
2026-01-01 19:40:45 +08:00
06bc2c2020 refactor(scenic): 移除景区移动端控制器和服务中的分页查询功能
- 删除 AppScenicController 中的 pageQuery 接口方法
- 删除 AppScenicController 中的 deviceCountByScenicId 接口方法
- 删除 AppScenicController 中的 scenicListByLnLa 接口方法
- 从 AppScenicService 接口中移除 pageQuery 方法定义
- 从 AppScenicService 接口中移除 deviceCountByScenicId 方法定义
- 从 AppScenicService 接口中移除 scenicListByLnLa 方法定义
- 完全移除 AppScenicController 类文件
- 简化 AppScenicServiceImpl 中
2026-01-01 19:32:49 +08:00
f1a2958251 feat(notification): 添加微信订阅消息配置管理及幂等授权功能
- 新增微信订阅消息配置管理控制器,支持模板、场景、事件映射配置
- 实现用户通知授权服务的幂等控制,避免前端重试导致授权次数虚增
- 添加微信订阅消息发送日志记录,用于幂等与排障
- 新增视频生成完成时的订阅消息触发功能
- 实现场景模板查询接口,返回用户授权余额信息
- 添加模板V2相关数据表映射器和实体类
- 集成微信订阅消息触发服务到任务完成流程中
2026-01-01 17:53:59 +08:00
81dc2f1b86 feat(printer): 添加景区ID参数并优化用户照片打印去重逻辑
- 在价格计算请求中添加景区ID参数
- 实现用户照片按sourceId去重机制,避免重复添加相同照片
- 查询用户在景区的已有打印记录用于去重判断
- 优化普通照片打印商品项,添加设备ID属性信息
- 过滤无效数据并去重后生成设备ID属性列表
2025-12-31 23:37:00 +08:00
41e90bab9c fix(task): 修复任务重复处理中的日志和更新逻辑
- 移除了创建任务日志中的敏感参数信息
- 更新重复任务日志以包含任务ID信息
- 移除了workerId重置逻辑,改为显式清除方法
- 修复TaskMapper中status字段的SQL语法问题
- 优化了任务参数更新的处理流程
2025-12-31 20:36:31 +08:00
b4628bd3e8 refactor(task): 优化重复任务处理逻辑
- 修复重复任务时直接使用旧任务ID的问题
- 实现重复任务的更新机制:重置workerId为空,status为0
- 添加taskParams的更新功能
- 集成任务缓存清理机制
- 修正订单购买状态检查的参数传递
2025-12-31 20:03:58 +08:00
cfb4284b7c refactor(video): 优化视频片段获取任务的设备配对处理
- 添加 Caffeine 缓存优化景区设备配对关系查询性能
- 实现设备配对关系缓存机制,避免重复数据库查询
- 重构线程安全的回调调用逻辑,使用 compareAndSet 保证原子性
- 添加调试日志的条件判断,减少不必要的日志输出
- 优化任务执行流程,调整线程池关闭和资源清理逻辑
- 实现设备配对关系加载方法,返回不可变Map提高安全性
2025-12-31 19:50:44 +08:00
5a61432dc9 refactor(orchestrator): 优化人脸匹配拼图模板生成逻辑
- 引入线程同步机制确保打印机场景下拼图模板生成完成
- 修改 asyncGeneratePuzzleTemplate 方法返回 Thread 对象便于控制
- 使用虚拟线程池优化拼图模板并发生成性能
- 简化原子计数器和异步任务相关代码实现
- 添加线程 join 等待确保关键场景执行顺序
- 修复方法返回值类型和资源管理相关问题
2025-12-31 19:50:18 +08:00
91160a1adb fix(task): 修复任务重复创建和空指针问题
- 在原位替换模式下设置taskParams为null,避免按参数匹配
- 添加isReuseOldTask标识判断是否复用旧任务
- 复用旧任务时执行更新操作而非新增操作
- 添加member和item空值检查,防止空指针异常
- 优化日志记录,提供更准确的操作信息
2025-12-30 18:12:29 +08:00
991a8b10e3 fix(printer): 解决图片类型设置逻辑问题
- 添加source为空时的图片类型判断逻辑
- 当source为空时将图片类型设置为PUZZLE
- 保持原有source不为空时的IPC类型设置逻辑
- 确保PHONE类型的设置逻辑不受影响
2025-12-30 17:49:39 +08:00
ab1e8cf7ef fix(printer): 解决图片类型设置逻辑问题
- 添加source为空时的图片类型判断逻辑
- 当source为空时将图片类型设置为PUZZLE
- 保持原有source不为空时的IPC类型设置逻辑
- 确保PHONE类型的设置逻辑不受影响
2025-12-30 17:34:30 +08:00
2a3b4ca19f fix(video): 修复设备视频连续性检查缓存覆盖问题
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 为VideoContinuityReportReq的gaps字段添加空列表默认值
- 在设备不支持连续性检查时检查Redis中是否已存在外部上报的缓存记录
- 避免已有的外部上报缓存被内部检查结果覆盖
- 保持已有缓存记录的完整性,仅在无缓存时进行存储
2025-12-30 10:49:58 +08:00
85599aa84a feat(pc): 添加外部工具上报视频连续性检查结果接口
- 添加 @IgnoreToken 注解支持无需认证的上报功能
- 新增 reportContinuityResult 方法处理外部工具上报请求
- 实现根据设备编号查询设备ID的逻辑
- 添加视频连续性检查结果的缓存存储功能
- 支持间隙信息的转换和存储到Redis
- 设置24小时缓存TTL策略
- 完善日志记录和异常处理机制
2025-12-30 10:45:04 +08:00
fbd4cfa83c refactor(integration): 将降级缓存从Redis迁移到Caffeine内存缓存
- 移除RedisTemplate依赖,改用Caffeine作为缓存实现
- 添加缓存互斥锁机制,避免并发请求打崩下游服务
- 统一缓存策略:有缓存直接返回,无缓存调用远程并缓存结果
- 调整缓存TTL配置,从天单位改为分钟单位
- 更新缓存统计信息结构,TTL字段从天改为分钟
- 优化批量清除缓存逻辑,使用流式过滤处理
- 简化缓存操作API,移除无返回值的执行方法
2025-12-29 17:32:13 +08:00
4596a61ba8 feat(ExtraDevice): 添加外部设备管理功能
- 创建了 ExtraDeviceController 提供分页查询外部设备列表的API接口
- 新增 ExtraDeviceService 和 ExtraDeviceServiceImpl 实现设备查询逻辑
- 添加 ExtraDevicePageQueryReq 和 ExtraDeviceRespVO 请求响应数据模型
- 扩展 ExtraDeviceMapper 支持分页查询外部设备列表
- 实现景区名称填充和设备在线状态判断功能
- 集成 Redis 获取设备心跳时间判断在线状态
- 添加了完整的参数校验和异常处理机制
2025-12-29 16:06:32 +08:00
d6780ccb7a feat(face): 添加打印机大屏人脸识别试点功能
- 在PrinterTvController中添加tv场景参数,用于触发打印机大屏识别试点逻辑
- 在FaceMatchingScene枚举中新增PRINTER_TV_RECOGNIZE场景,支持仅执行识别补救落库建关系
- 在FaceMatchingPipelineFactory中创建打印机大屏识别试点Pipeline,包含识别补救落库建关系等阶段
- 在FaceServiceImpl中添加打印机大屏人脸识别的专门处理方法matchFaceIdForPrinterTv
- 通过scene参数区分普通人脸识别和打印机大屏识别试点流程
2025-12-29 11:34:10 +08:00
58e8189b13 perf(face): 提高人脸匹配严格度阈值
- 将人脸匹配的严格度分数从0.6调整为0.75
- 增强人脸识别的准确性要求
2025-12-28 17:07:07 +08:00
84cb5ad8f9 refactor(face): 优化人脸识别服务中的购买验证逻辑
- 移除多余的空格格式
- 删除重复的购买验证逻辑
- 移除临时解决方案代码
- 简化条件判断流程
2025-12-28 13:58:04 +08:00
71bfa00c25 refactor(member): 移除会员响应对象中的冗余字段
- 移除了推客优惠码、推客id和用户协议同意状态字段
- 移除了创建时间、更新时间和订单数量等不必要字段
- 移除了服务通知开关设置
- 清理了会员服务实现中相关的字段赋值逻辑
2025-12-26 20:03:50 +08:00
1916dd96a2 feat(video): 添加移动端HLS视频流播放列表生成功能
- 实现AppVideoStreamController提供HLS播放列表生成接口
- 添加HlsStreamRequest和HlsStreamResponse数据传输对象
- 实现HlsStreamService服务类处理视频流逻辑
- 支持生成JSON格式和m3u8文件格式的播放列表
- 提供视频片段查询和设备视频HLS播放列表生成功能
- 支持EVENT和VOD两种播放列表类型
- 集成设备存储操作器获取视频文件列表
- 实现播放列表内容构建和视频片段时长计算功能
2025-12-26 15:37:22 +08:00
c583d4b007 feat(video): 添加通过faceId查询最新视频记录功能
- 在AppVideoController中新增getLatestByFaceId接口
- 添加VideoRespVO响应对象导入
- 实现通过faceId和可选templateId查询最新视频记录的功能
- 在VideoMapper中定义queryLatestByFaceIdAndTemplateId方法
- 在VideoRepository中实现查询逻辑
- 在VideoMapper.xml中添加对应的SQL查询语句
- 支持根据faceId和templateId条件查询最新视频记录
- 添加相应的日志记录和异常处理机制
2025-12-26 15:35:27 +08:00
50ee14cf8f feat(mobile): 添加移动端模板接口
- 实现了 AppTemplateController 控制器
- 添加了根据模板ID获取封面URL的接口
- 集成了 TemplateRepository 数据访问层
- 实现了模板ID参数校验逻辑
- 添加了模板不存在的错误处理
- 实现了封面URL为空的验证机制
2025-12-24 10:31:13 +08:00
3f4d3cb7ac refactor(scenic): 替换ScenicConfigEntity为ScenicConfigManager
- 将所有使用ScenicConfigEntity的地方替换为ScenicConfigManager
- 更新获取景区配置的方法调用
- 修改属性访问方式为通过manager的getter方法
- 移除已废弃的ScenicConfigEntity类及相关方法
- 统一配置读取接口,提高代码一致性与可维护性
2025-12-19 19:02:37 +08:00
f2ba5ed65b refactor(task): 移除视频重新上传功能及相关依赖
- 删除 VideoReUploader 类及其相关注入依赖
- 移除 VideoReUploader 的调用逻辑
- 清理无用的导入语句及测试类文件
- 简化视频处理流程,去除冗余操作步骤
2025-12-19 17:21:41 +08:00
677893272a refactor(pricing): 移除冗余的产品属性键缓存逻辑
- 删除了产品属性键的缓存处理逻辑
- 简化了价格计算服务中的商品处理流程
- 移除了不再使用的产品属性键设置方法调用
2025-12-19 09:21:23 +08:00
956ace77a8 fix(pricing): 调整优惠券配置插入字段顺序
- 修改了 price_coupon_config 表的插入字段顺序
- 确保 SQL 插入语句字段与值一一对应
- 避免因字段顺序不一致导致的数据插入错误
2025-12-18 19:51:35 +08:00
08e2a4ebec fix(face): 更新拼图模板分组名称
- 将拼图模板分组名称从 "plog" 更改为 "氛围拼图"
- 确保与产品需求文档中的命名一致
- 避免因分组名称不明确导致的内容分类错误
2025-12-18 19:46:28 +08:00
3c8b3b0ace refactor(printer): 重构人脸素材查询接口
- 移除对SourceMapper的依赖
- 新增MemberRelationRepository和SourceRepository依赖
- 修改getSourceByFaceSampleId方法参数faceSampleId为faceId
- 方法返回类型由SourceEntity改为List<SourceEntity>
- 使用memberRelationRepository查询关联素材
- 当查询结果为空时返回空集合而非失败响应
- 通过sourceRepository获取具体资源信息并转换返回
2025-12-18 19:40:24 +08:00
94b37d47ec feat(printer): 添加照片属性键功能支持
- 引入SourceRepository依赖以获取设备ID
- 在打印服务中新增对照片来源属性的处理逻辑
- 根据用户照片列表生成唯一的设备ID集合
- 将生成的属性键列表关联到普通照片项中
- 优化打印数量统计逻辑,确保数据准确性
- 增强打印任务构建时的数据完整性校验
2025-12-18 11:18:45 +08:00
cd4422eb23 feat(printer): 优化自动发券逻辑并支持多种产品类型
- 移除了对source类型为3的检查逻辑
- 简化了自动发券的触发条件判断
- 新增对effectCount大于0时发放PHOTO_PRINT_FX类型优惠券的支持
- 保留了原有的异常处理机制确保不影响主流程
- 维持了mobileCount相关的业务逻辑不变
2025-12-18 11:13:38 +08:00
8dc0e993e1 refactor(utils): 替换雪花ID生成工具实现
- 移除自定义的雪花ID生成逻辑
- 引入Hutool的Snowflake工具类
- 简化ID生成方法,提高代码可维护性
- 移除相关的测试类文件
- 删除不再使用的UniqueId和UniqueIdMetaData模型类
2025-12-18 10:55:58 +08:00
2432cf496f test(puzzle): 更新测试用例以适配新的方法签名并增强唯一性验证
- 在 PuzzleGenerateServiceDeduplicationTest 中引入 CustomFaceSearchStage 类
- 添加 SpringBootTest 注解以支持完整的上下文加载
- 使用 InjectMocks 替代手动构造服务实例
- 修改 recordMapper.updateSuccess 方法调用,增加一个 String 参数
- 新增 SnowFlakeUtilTest 类用于测试雪花ID生成器的唯一性和性能
- 添加高并发环境下的ID唯一性校验逻辑
- 引入对潜在时间戳溢出问题的检测机制
- 增加单线程性能测试方法 testPerformanceSingleThread
2025-12-18 10:35:19 +08:00
95a5977ae2 feat(puzzle): 添加模板打印相关字段
- 在 PuzzleTemplateDTO 中新增 autoAddPrint 字段表示自动添加到打印队列
- 在 PuzzleTemplateDTO 中新增 canPrint 字段表示是否可以打印
- 在 PuzzleTemplateDTO 中新增 userArea 字段表示用户查看区域
- 在 TemplateCreateRequest 中新增 autoAddPrint 字段表示自动添加到打印队列
- 在 TemplateCreateRequest 中新增 canPrint 字段表示是否可以打印
- 在 TemplateCreateRequest 中新增 userArea 字段表示用户查看区域
2025-12-18 10:32:21 +08:00
7e157eaba9 feat(pricing): 新增优惠券属性门槛校验功能
- 在PriceCouponConfig实体中新增requiredAttributeKeys字段,用于配置优惠券使用门槛
- 修改MyBatis Mapper SQL语句,支持新字段的插入和更新操作
- 在CouponManagementServiceImpl中增加对requiredAttributeKeys的格式校验逻辑
- 更新CouponServiceImpl的优惠券适用性检查逻辑,增加属性门槛判断
- 在PriceCalculationServiceImpl中实现商品属性Key的自动计算与填充
- 优化价格计算服务中的能力缓存与属性Key构建逻辑
- 更新CLAUDE.md文档,补充属性门槛特性的说明
2025-12-17 23:49:20 +08:00
00dd6a16a3 refactor(face): 调整人脸识别匹配逻辑以支持场景参数
- 修改 PrinterTvController 中 faceUpload 方法的 scene 参数值从 print 改为 tv
- 在 FaceServiceImpl 中为人脸匹配方法增加 scene 参数支持
- 更新 FaceMatchingOrchestrator 的 orchestrateMatching 方法签名以接收 scene 参数
- 在 FaceService 接口中新增带 scene 参数的 matchFaceId 方法定义
- 更新 VideoTaskGenerator 中调用 matchFaceId 方法时传入 scene 参数
2025-12-17 23:16:48 +08:00
9e6b623b0e feat(printer): 优化打印机大屏人脸识别接口
- 移除多余依赖和字段声明
- 简化人脸识别逻辑,复用 faceUpload 方法
- 保留人脸去重检测和自动添加打印逻辑
- 修改返回结构为 FaceRecognizeResp
- 删除旧有的照片上传与素材查询流程
2025-12-17 23:12:49 +08:00
10b39ec4c1 feat(puzzle): 支持拼图原图保存与自动打印功能
- 在PuzzleGenerationRecordEntity中新增originalImageUrl字段用于存储未裁切的原图URL
- 在PuzzleTemplateEntity中新增autoAddPrint、canPrint和userArea字段支持打印配置
- 更新PuzzleGenerationRecordMapper.xml以支持新字段的读写操作
- 在PuzzleGenerateServiceImpl中实现原图上传、用户区域裁切以及自动添加到打印队列逻辑
- 新增cropImage方法处理图片按指定区域裁切
- 集成PrinterService实现拼图完成后自动添加到打印队列功能
- 优化生成流程日志记录,区分原图和最终图片的URL信息
2025-12-17 22:56:50 +08:00
3e938ad171 refactor(order): 移除优惠券相关业务逻辑
- 删除 CouponBiz 类及其所有方法调用
- 移除 OrderBiz 中对优惠券使用的处理逻辑
- 清理 PriceBiz 中与优惠券查询相关的代码
- 移除 GoodsServiceImpl 中的优惠券查询功能
- 删除 OrderServiceImpl 中创建订单时的优惠券计算逻辑
- 移除多个类中对 CouponBiz 的依赖注入
- 清理与优惠券记录和折扣价格计算相关的冗余代码
2025-12-17 22:16:16 +08:00
6c404e210e feat(source): 新增虚拟订单创建接口
- 添加 createVirtualOrder 接口用于后台创建0元订单
- 引入 PrinterService 和相关请求实体类
- 支持通过 sourceId、scenicId 和 printerId 创建订单
- 实现异常捕获并返回失败响应
- 扩展 SourceController 功能模块
2025-12-17 22:09:27 +08:00
25681806c3 feat(printer): 支持查询不同类型的图像素材
- 为getSourceByFaceSampleId接口添加type参数,默认值为2
- 修改sourceMapper调用,传入动态type参数
- 移除未使用的scenicService和faceService依赖
- 引入Map类但尚未使用,可能为后续功能预留
2025-12-17 22:08:58 +08:00
99d0b9c340 feat(printer): 实现虚拟用户0元订单创建功能
- 新增CreateVirtualOrderRequest请求参数类
- 实现createVirtualOrder服务方法,支持根据source创建虚拟用户订单
- 自动生成虚拟用户ID并创建对应的人脸记录
- 创建member_print记录并设置照片数量
- 支持指定或自动选择景区内可用打印机
- 创建0元订单并触发购买后逻辑
- 返回订单相关信息包括orderId、faceId等
- 添加详细的日志记录便于调试和追踪
- 实现完整的异常处理和参数校验
2025-12-17 22:07:42 +08:00
8e0990832b feat(ai-cam): 实现AI相机免费照片赠送逻辑
- 新增统计免费关联记录数的Mapper方法
- 在AppAiCamServiceImpl中注入ScenicRepository依赖
- 删除旧关系前查询已有免费记录避免重复赠送
- 根据景区配置获取免费照片数量
- 随机选择免费照片并标记为免费状态
- 保留已存在的免费照片记录
- 更新日志记录以区分普通和免费照片数量
2025-12-17 20:47:21 +08:00
144c338972 feat(profitshare): 添加接收人ID字段
- 在CreateRecipientRequest类中新增id字段
- 支持接收人唯一标识的传递
- 完善接收人创建请求的数据结构
2025-12-17 17:53:09 +08:00
2dcb736df5 fix(pay): 微信支付退款失败时记录错误信息
- 在退款响应中添加状态信息字段
- 当退款失败时设置并返回具体的错误状态
- 更新订单退款状态时增加对退款失败情况的日志记录
2025-12-17 17:52:58 +08:00
c8560e3aca fix(video): 修复任务状态查询逻辑并优化Redis过期策略
- 修改人脸模板渲染状态存储逻辑,增加默认过期时间
- 移除AppTaskController中冗余的JwtInfo获取代码
- 优化GoodsServiceImpl中任务状态判断逻辑,增强空值检查
- 修复视频任务状态返回不准确的问题,完善边界条件处理
2025-12-17 16:42:09 +08:00
171932c05c feat(face): 优化模板渲染状态查询逻辑
- 引入 TaskMapper 依赖以支持任务查询
- 移除带过期时间的模板渲染状态设置方法
- 在缓存缺失时查询最新任务记录以确定渲染状态
- 新增 listLastFaceTemplateTask 方法用于获取最新的人脸模板任务
- 实现根据任务状态自动设置模板渲染状态的逻辑
- 添加对任务状态为 1 和 2 时的渲染状态映射处理
2025-12-17 16:18:37 +08:00
6cd47649fc fix(wxpay): 修复 Kafka 生产者空指针异常
- 添加对 profitShareKafkaProducer 的空值检查
- 在发送消息前确保 Kafka 生产者已注入
- 使用 CompletableFuture 处理异步退款消息发送
- 设置 Kafka 生产者的注入模式为 required=false
- 避免因 Kafka 生产者缺失导致的服务启动失败
2025-12-17 15:52:41 +08:00
00890c764e feat(basic): 添加模板片段更新状态缓存支持
- 在FaceStatusManager中新增按模板ID区分的人脸片段更新状态缓存键
- 更新TaskTaskServiceImpl以设置模板渲染状态
- 在任务回调逻辑中增加对模板渲染状态的更新操作
- 修改任务删除逻辑为更新状态加10的临时解决方案
- 移除旧有的切割任务状态更新逻辑,统一使用模板渲染状态管理
2025-12-17 15:49:24 +08:00
a9c33352f7 feat(profit-share): 实现分账消息发送功能
- 修改 ProfitShareKafkaProducer 的 sendRefundMessage 方法返回 CompletableFuture
- 在 WxMpPayAdapter 中增加 transactionId 和 refundTransactionId 字段解析
- 在 PayResponse 和 RefundResponse 中新增 transactionId 相关字段
- 在 WxPayServiceImpl 中注入 ProfitShareKafkaProducer 并发送分账消息
- 调整退款逻辑以异步方式发送分账退款消息后再执行退款操作
2025-12-16 17:58:20 +08:00
a9555d612a feat(profitshare): 添加支付分账调用配置字段
- 在CreateRecipientRequest中新增needPaymentCall字段
- 用于控制是否需要调用支付分账接口
- 字段类型为Integer,支持空值处理
- 添加对应的JSON序列化注解
- 更新实体类文档注释说明用途
2025-12-16 17:23:38 +08:00
c1f35e1f3a Merge branch 'profitshare' 2025-12-16 10:45:30 +08:00
a5903a9831 feat(integration): 支持TypeReference泛型的降级缓存机制
- 在IntegrationFallbackService中新增支持TypeReference的executeWithFallback方法
- 新增getFallbackFromCache和parseFallbackCacheValue方法处理泛型缓存
- 更新DeviceStatusIntegrationService使用TypeReference保留泛型信息
- 更新RenderWorkerConfigIntegrationService使用TypeReference并修正缓存键
- 更新ScenicConfigIntegrationService使用TypeReference保留泛型信息
- 添加必要的Jackson TypeReference导入依赖
2025-12-16 10:00:49 +08:00
b207b5805a fix(face): 优化人脸匹配流程中的拼图模板生成逻辑
- 仅在新增人脸时异步生成拼图模板
- 避免重复生成已存在的人脸拼图模板
- 提升人脸匹配服务的执行效率
2025-12-15 18:32:15 +08:00
5d7fe1638e feat(integration): 优化降级缓存策略,支持优先使用1分钟内缓存
- 新增优先缓存判断逻辑,1分钟内的缓存优先返回
- 调整默认缓存TTL常量命名以避免混淆
- 重构缓存读取流程,优先解析已读取的缓存值
- 提取缓存值解析方法,增强代码复用性
- 完善缓存存储与读取的日志记录
- 修复缓存TTL单位使用不一致的问题
2025-12-15 18:32:15 +08:00
c0f07ee9f4 refactor(task): 重构任务拍摄时间获取逻辑
- 将 getTaskShotDate 方法从 TaskTaskServiceImpl 移至 VideoTaskRepository
- 删除对 TaskService 和 TaskTaskServiceImpl 的依赖注入
- 更新 LyCompatibleController 和 GoodsServiceImpl 中的调用方式
- 简化日期解析逻辑,提高代码可读性
- 移除冗余的 VideoMapper 和 TaskService 接口方法声明
- 统一使用 VideoTaskRepository 处理任务相关数据查询
2025-12-15 17:33:40 +08:00
832f6a2339 refactor(order): 简化faceId获取逻辑
- 移除通过task获取faceId的中间步骤
- 直接从video实体获取faceId
- 更新相关服务和控制器中的调用逻辑
- 优化日志记录中的faceId来源
- 提高代码可读性和执行效率
2025-12-15 17:29:53 +08:00
7348994427 refactor(video): 将视频实体中的workerId字段改为faceId
- 修改VideoEntity类中字段workerId为faceId,并更新注释
- 更新TaskTaskServiceImpl中设置视频信息的逻辑,使用faceId替代workerId
- 修改VideoMapper.xml中插入视频记录的SQL语句,字段由workerId改为faceId
- 调整VideoMapper.xml中更新视频记录的SQL条件,使用faceId进行筛选
- 更新VideoMapper.xml中查询视频列表和单个视频详情的SQL语句,字段名由workerId改为faceId
- 优化查询条件中对faceId的处理逻辑,直接关联video表的face_id字段
2025-12-15 16:51:04 +08:00
0665eef37d feat(videoreview): 添加视频购买状态检查功能
- 在VideoReviewController中新增/check-purchase接口用于检查视频是否被购买
- 扩展OrderMapper接口,增加根据视频ID和模板ID查询订单ID的方法
- 在VideoReviewServiceImpl中实现checkVideoPurchase方法,支持两种购买方式判断
- 完善相关DTO类引入及Mapper XML配置文件的SQL查询逻辑
- 实现直接购买视频和通过模板购买的双重购买状态检测机制
- 添加详细的日志记录便于后续追踪与调试
2025-12-15 16:49:20 +08:00
adabe88648 fix(video): 调整视频处理任务参数以优化性能
- 将探测大小从 32M 减小到 16M 以减少内存占用
- 修改线程池关闭前的等待时间从 5 分钟缩短至 3 分钟
- 添加注释说明批量定时停止的目的
2025-12-15 13:56:54 +08:00
3c838ec36e refactor(video): 优化视频切割逻辑,使用concat demuxer提升性能
- 引入concat demuxer方式替代原有转码流程,提高处理效率
- 新增PROBE_SIZE常量用于控制探测大小,优化文件解析
- 重构runFfmpegForMultipleFile1方法,简化多文件处理逻辑
- 添加quickVideoCutWithConcatDemuxer方法实现无转码快速切割
- 调整ffmpeg命令参数顺序及新增选项,如-probesize、-analyzeduration等
- 在多个ffmpeg调用中统一增加-genpts标志和避免负时间戳处理
- 完善临时文件清理机制,确保执行过程中的资源回收
- 更新相关ffmpeg命令构建逻辑以适配新的处理流程
2025-12-15 10:53:12 +08:00
5bef712b1c feat(face): 优化人脸内容购买逻辑
- 引入OrderRepository依赖以查询用户购买状态
- 修改内容购买检查逻辑,优先检查人脸项目购买情况
- 添加模板购买状态的二次校验机制
- 移除冗余的订单业务层调用逻辑
- 简化购买状态判断流程,提高代码可读性
2025-12-15 08:34:11 +08:00
f08d590a3d fix(price): 修复价格分享逻辑错误
- 移除了错误的价格分享判断条件
- 确保价格分享状态正确设置为false
- 优化了价格业务逻辑的代码结构
2025-12-15 08:33:58 +08:00
844bc318ae refactor(videoreview): 简化机位评价数据结构
- 修改机位评价数据结构从嵌套Map改为简单Map
- 更新数据库映射文件中的类型处理器配置
- 调整评价统计逻辑以适应新的数据结构
- 优化导出功能以支持新格式的机位评价展示
- 更新相关实体类、DTO类及Mapper接口定义
- 移除不再使用的嵌套Map相关代码和依赖
2025-12-15 08:33:48 +08:00
c9c4d9454a feat(goods): 优化商品详情购买状态判断逻辑
- 引入OrderRepository依赖以支持新的购买状态查询
- 替换原有的视频购买状态判断逻辑,使用更准确的checkUserBuyFaceItem方法
- 增加对模板ID的购买状态检查,提高判断准确性
- 简化价格查询前的条件判断流程
- 移除冗余的实体查询和复杂的嵌套判断逻辑
- 保持原有价格展示逻辑不变,确保前端显示一致
2025-12-14 12:51:51 +08:00
398a3750f8 todo: 逻辑弄反了 2025-12-14 12:20:48 +08:00
aceea9af18 feat(order): 添加拼图商品项到订单列表
- 在订单服务中新增拼图商品项实体
- 设置拼图商品的订单ID和景区ID
- 指定拼图商品类型为5
- 将拼图商品项加入订单项列表
- 保留原有价格配置逻辑不变
2025-12-14 12:11:59 +08:00
54088f46d9 fix(biz): 调整pLog图商品添加逻辑
- 将pLog图<景区打包>商品添加到列表末尾改为添加到列表开头
- 当puzzleList非空时才添加打包商品
- 确保打包商品始终显示在商品列表第一位
2025-12-14 00:07:45 +08:00
5cc32ddf61 feat(order): 优化订单查询逻辑以支持景区关联数据
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 在member_plog_data子查询中增加scenic_id字段
- 添加puzzle_template与puzzle_generation_record的左连接
- 修改member_plog_data与其他表的连接条件以兼容景区ID匹配
- 支持通过goods_id或scenic_id关联member_plog_data表
- 提升订单详情中图片资源定位准确性
2025-12-14 00:04:06 +08:00
07987835ec fix(face): 修复人脸购买逻辑判断问题
- 修改AppPuzzleController中的人脸购买判断逻辑
- 增加对景区是否购买的前置判断
- 优化FaceServiceImpl中的人脸购买状态设置逻辑
- 确保模板购买状态的准确判断
- 避免重复查询价格计算服务
2025-12-13 23:47:29 +08:00
0a3f4119d7 feat(price): 添加pLog图商品到景区打包列表
- 在PHOTO_LOG情况下增加SimpleGoodsRespVO对象
- 对象包含景区ID、名称和产品类型信息
- 确保pLog图模板能正确显示在商品列表中
2025-12-13 23:43:31 +08:00
51c7de2474 feat(fill): 新增设备缩略图数据源策略
- 实现DeviceThumbImageDataSourceStrategy类,支持根据deviceIndex获取设备缩略图
- 支持从过滤后的机位列表或直接通过deviceIndex两种方式查询数据
- 默认使用LATEST排序策略,可配置type类型(默认为图片类型2)
- 添加对filteredDeviceIds上下文参数的支持,提升数据筛选灵活性
- 增强日志记录,便于调试和问题追踪
- 在DataSourceType枚举中新增DEVICE_THUMB_IMAGE类型定义
2025-12-13 21:47:41 +08:00
773d7f2254 refactor(service): 优化拼图模板处理逻辑
- 将遍历所有拼图模板改为只处理第一个模板
- 简化内容页面对象创建流程
- 保留原有的购买状态检查和价格计算逻辑
- 提高代码执行效率,避免不必要的循环操作
2025-12-13 21:41:11 +08:00
af131131ed fix(task): 修改任务创建接口中的自动标志参数类型
- 将 createTaskByFaceIdAndTemplateId 方法的 automatic 参数从 int 改为 boolean
- 更新方法实现以适配新的布尔值参数
- 移除无用的导入类和未使用的代码
- 调整任务实体中 automatic 字段的赋值逻辑以匹配新类型
- 删除已弃用的旧版重载方法
- 确保所有调用点传递正确的布尔值而非整数
- 优化代码结构并提高可读性
2025-12-13 19:19:21 +08:00
3f6f1508c5 feat(order): 增加faceId校验的订单购买检查功能
- 新增checkUserBuyFaceItem方法,支持校验用户购买商品时的人脸ID匹配
- 修改PriceBiz中商品类型设置,从13改为5
- 更新FaceServiceImpl中的购买检查逻辑,使用新的带faceId校验的方法
- 调整OrderServiceImpl中订单项的goodsType和goodsId设置逻辑
- 移除旧的checkUserBuyItem方法及相关缓存逻辑
- 新增ORDER_USER_FACE_TYPE_BUY_ITEM_CACHE_KEY缓存键定义
2025-12-13 19:00:25 +08:00
dbee1d9709 feat(puzzle): 使用虚拟线程优化拼图模板批量生成性能
- 将原有的串行模板生成逻辑改为并行处理
- 使用虚拟线程池提升高并发场景下的执行效率
- 通过 CompletableFuture 异步执行每个模板的生成任务
- 保留原有日志记录和异常处理机制
- 统计成功与失败数量并输出汇总日志
2025-12-13 17:38:48 +08:00
83d1096fdb feat(order): 添加vlog视频模板购买逻辑
- 在订单业务中处理类型为-1的商品(vlog视频模板)
- 调用视频仓库方法设置用户已购买模板
- 新增setUserIsBuyTemplate方法实现模板购买状态更新
- 查询面部关联视频并更新购买状态及清理缓存

feat(price): 增加拼图商品列表查询功能

- 在价格服务中加入对拼图模板的查询
- 设置拼图商品类型为13
- 将拼图模板信息加入返回的商品列表中
2025-12-13 14:13:59 +08:00
82925d203c feat(config): 添加优雅关机配置和智谱API密钥
- 在开发环境配置中启用优雅关机
- 设置每个关机阶段超时时间为60秒
- 添加智谱AI服务的API密钥配置
- 统一开发和生产环境的基础配置结构
2025-12-12 17:19:29 +08:00
3b11ddef6a feat(chat): 实现人脸智能聊天核心功能
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 新增小程序人脸聊天控制器 AppChatController,支持会话创建、消息收发、历史查询及会话关闭
- 集成智谱 GLM 模型客户端 GlmClient,支持流式文本生成与回调
- 新增聊天会话与消息实体类及 MyBatis 映射,实现数据持久化
- 提供 FaceChatService 接口及实现,封装聊天业务逻辑包括同步/流式消息发送
- 引入 zai-sdk 依赖以支持调用智谱 AI 大模型能力
- 支持基于人脸 ID 的唯一会话管理与用户权限校验
- 消息记录包含角色、内容、追踪 ID 及延迟信息,便于调试与分析
2025-12-11 17:45:49 +08:00
6e7b4729a8 feat(ai-cam): 新增使用人脸样本创建或获取Face记录功能
- 在AppAiCamController中新增/useSample/{faceSampleId}接口
- 实现通过人脸样本ID查找或创建Face记录的业务逻辑
- 自动关联AI相机照片到用户人脸记录
- 支持AI_CAM设备类型的二维码路径配置
- 完善人脸匹配及日志记录功能
- 添加相关实体类和工具类导入依赖
2025-12-09 16:20:50 +08:00
917cb37ccf feat(device): 添加获取设备基本信息的方法
- 新增 getDeviceBasic 方法直接返回 DeviceV2DTO 实例
- 添加设备基本信息查询的日志记录
- 实现通过设备ID获取设备详情的功能集成调用
2025-12-09 15:59:29 +08:00
7c0a3a63bb fix(order): 兼容旧逻辑并清理Redis缓存
- 在订单类型为3时设置Redis标识
- 删除冗余的Redis键值对清理操作
- 统一订单内容不可下载的处理逻辑
2025-12-08 13:51:28 +08:00
478467e124 fix(order): 更新商品类型13的名称显示
- 将"AI相机照片集"更正为"打卡点拍照"
- 同步更新OrderServiceImpl中的商品名称和订单类型
- 修改OrderMapper.xml中对应的商品名称映射逻辑
2025-12-08 13:43:11 +08:00
d5befd75e1 fix(pricing): 修复优惠券查询条件拼接问题
- 在每个查询条件后添加空格,避免SQL语法错误
- 确保动态SQL片段正确连接
- 优化时间范围查询条件的格式处理
2025-12-08 10:58:33 +08:00
b2c55c9feb refactor(printer): 优化照片处理管线与自动发券逻辑
- 调整自动发券判断条件,仅当存在type=3的source记录时触发
- 修改普通照片与拼图处理流程中的图像增强控制逻辑
- 移除冗余的图像缩放阶段,优化处理效率
- 增加processPhotoWithPipeline重载方法支持图像增强选项
- 重构水印配置方法,新增scale参数控制缩放比例
- 异步处理打印任务创建与推送,提升响应速度
- 复用processPhotoWithPipeline方法简化重打印处理逻辑
2025-12-07 21:42:48 +08:00
fef616c837 feat(image): 添加水印缩放功能支持
- 在 WatermarkConfig 中新增 scale 字段用于控制整体缩放倍数
- 在 WatermarkStage 中读取并传递 scale 参数到 WatermarkInfo
- 在 PrinterDefaultWatermarkOperator 中实现所有位置和尺寸的缩放逻辑
- 对偏移量、边距、字体大小、二维码尺寸等应用缩放因子
- 更新图像绘制相关参数计算方式以支持动态缩放
- 优化二维码圆形背景和头像绘制的缩放处理
- 确保缩放后的水印元素保持相对位置和视觉一致性
2025-12-07 21:42:11 +08:00
a5fe00052d feat(pricing): 支持发放多个首次打印优惠券
- 修改自动发券逻辑,支持发放多个符合条件的首次优惠券
- 更新查找优惠券方法,返回所有匹配的优惠券ID列表
- 添加发券过程中的异常处理,确保部分失败不影响其他券发放
- 记录详细的发券日志,包括成功、跳过和失败的数量
- 优化日志输出,提供更清晰的调试信息
2025-12-07 21:41:54 +08:00
349b702fc3 1 2025-12-06 22:42:05 +08:00
9f5a61247b feat(printer): 增加对source.type=3的特殊图片处理流程
- 新增ImageResizeStage、ImageSRStage和UpdateMemberPrintStage处理阶段
- 对type=3的图片增加超分辨率和图像增强处理
- 构建新的处理管线,包含下载、方向校正、超分、增强、上传等12个阶段
- 兼容旧版URL处理逻辑,针对type=3替换缩略图为原图URL
- 优化图片来源判断逻辑,增加source实体查询
- 完善处理日志记录和阶段状态控制
2025-12-06 22:42:05 +08:00
9321422e56 fix(mobile): 修复商品名称显示问题
- 修正商品类型为3时的名称显示逻辑
- 拍摄时间格式化后添加到商品名称中
- 优化商品名称前缀拼接逻辑
2025-12-06 22:42:05 +08:00
1834fe3ddd feat(order): 添加订单可下载状态查询接口
- 在AppOrderV2Controller中引入RedisTemplate依赖
- 新增/downloadable/{orderId} GET接口
- 通过检查Redis键值判断订单是否可下载
- 返回ApiResponse包装的布尔值表示下载状态
2025-12-06 21:23:58 +08:00
fa8f92d38b refactor(order): 调整图像处理逻辑与订单兼容性设置
- 将图像处理逻辑移至事务提交后执行
- 添加订单内容不可下载标识兼容旧逻辑
- 移除冗余的券服务注入依赖
- 清理订单相关缓存以确保数据一致性
2025-12-06 21:21:52 +08:00
df33e7929f feat(repository): 优化AI相机照片处理性能
- 引入CompletableFuture实现照片处理并发执行
- 创建专用线程池IMAGE_PROCESS_EXECUTOR管理异步任务
- 将原有串行处理逻辑改为并行处理
- 更新默认存储适配器从assets到assets-ext
2025-12-06 21:14:29 +08:00
554f55a7c1 feat(storage): 集成动态存储配置管理
- 引入ScenicConfigManager以支持景区级别的存储配置
- 添加StorageFactory和IStorageAdapter以实现灵活的存储适配
- 在图像处理流程中集成存储适配器的初始化逻辑
- 支持从配置中加载存储类型和相关参数
- 提供降级机制,默认使用assets存储适配器
- 增强SourceRepository对存储配置的依赖注入支持
2025-12-06 21:06:22 +08:00
f71149fd06 feat(order): 新增AI相机拍照套餐价格计算逻辑
- 在OrderBiz中增加对AI相机拍照套餐的价格计算处理
- 针对产品类型为AI_CAM_PHOTO_SET的场景实现价格查询逻辑
- 设置仅查询价格标志,避免实际使用优惠
- 补充价格对象的景区ID设置逻辑
2025-12-06 21:06:09 +08:00
e8eb8d816b refactor(repository): 暂时禁用图像超分辨率处理阶段
- 注释掉图像超分辨率处理阶段以优化处理流程
- 保留其他图像处理阶段(下载、增强、上传、清理)
- 为后续重新启用超分辨率功能预留接口配置
2025-12-06 20:00:40 +08:00
576d87d113 refactor(logging): 重构任务日志配置
- 将 DeviceVideoContinuityCheckTask 的专用日志 appender 重命名为通用 TASK_LOG
- 更新日志文件路径从 device_video_continuity_check_task.log 到 task.log
- 为多个任务类添加共享的日志记录器配置
- 包括 FaceCleaner、VideoPieceCleaner、DynamicTaskGenerator 和 DownloadNotificationTasker 的日志设置
2025-12-06 17:43:56 +08:00
a2378053a8 feat(printer): 打印订单成功后自动发放优惠券
- 在打印订单成功后调用自动发券服务
- 添加对自动发券异常的捕获与日志记录
- 确保发券失败不影响主业务流程
2025-12-06 17:32:21 +08:00
c92ea20575 feat(logging): 为设备视频连续性检查任务添加专用日志配置
- 新增 DeviceVideoContinuityCheckTask 专用日志文件
- 新增 FaceProcessingKafkaService 专用日志文件
- 新增 DeviceStorageOperator 专用日志文件
- 配置独立的日志滚动策略和文件命名规则
- 设置日志级别为 INFO 并禁用继承
- 限制最大历史文件数量为 30 天
- 设置单个日志文件最大大小为 10MB
- 总日志文件容量上限设置为 5GB
2025-12-06 16:11:31 +08:00
bb71cf9458 feat(image): 新增AI相机照片增强处理功能
- 在PipelineScene枚举中新增AI_CAM_ENHANCE场景
- 修改setUserIsBuyItem方法,增加对AI相机照片的图像处理逻辑
- 新增processAiCamImages方法,批量处理AI相机照片
- 新增processSingleAiCamImage方法,处理单张AI相机照片
- 新增buildEnhancerConfig方法,构建图像增强配置
- 实现图像处理管线:下载->超分->增强->上传->清理
- 添加处理结果URL回调更新机制
- 增加异常处理和日志记录,确保处理失败不影响主流程
2025-12-06 16:11:07 +08:00
7749faf807 feat(order): 添加AI相机照片集商品类型支持
- 在OrderServiceImpl中增加对商品类型13(AI相机照片集)的处理逻辑
- 新增listAiCamImageByFaceRelation方法用于查询AI相机图片数据
- 扩展订单详情展示逻辑,支持AI相机照片集的封面和拍摄时间显示
- 更新OrderMapper.xml,新增member_source_aicam_data查询片段
- 修改SQL映射,增加对goods_type=13情况的字段匹配规则
- 完善商品名称和订单类型的设置逻辑,区分AI相机照片集与其他类型
2025-12-06 14:42:16 +08:00
c42b055d5f feat(printer): 添加图片裁剪信息字段并实现裁剪功能
- 在 MemberPrintEntity 中新增 crop 字段用于存储裁剪信息
- 创建 Crop 类并添加 Lombok 注解以支持构造函数和 getter/setter
- 在 PrinterServiceImpl 中调用 smartCropAndFill 方法进行图片裁剪
- 设置默认旋转角度为 270 并将裁剪信息序列化后保存到数据库
- 更新 PrinterMapper.xml 配置文件以支持新字段的插入和查询
2025-12-06 14:41:06 +08:00
fe3bda28b4 feat(ai-cam): 增强AI摄像头人脸检测逻辑
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 引入时间范围限制配置,支持按分钟设置检测窗口
- 创建DetectResult内部类,记录人脸检测的分数和时间信息
- 优化照片筛选逻辑,先按分数排序再应用时间范围过滤
- 更新设备类型名称,将"微单"改为"AI微单"
- 增强日志记录,提供更详细的调试信息
2025-12-05 21:41:30 +08:00
66775ea48b feat(ai-cam): 增强AI摄像头人脸检测逻辑
- 引入时间范围限制配置,支持按分钟设置检测窗口
- 创建DetectResult内部类,记录人脸检测的分数和时间信息
- 优化照片筛选逻辑,先按分数排序再应用时间范围过滤
- 更新设备类型名称,将"微单"改为"AI微单"
- 增强日志记录,提供更详细的调试信息
2025-12-05 20:03:54 +08:00
125fadd6c5 feat(basic): 新增AI微单类型支持
- 在SourceType枚举中新增AI_CAM类型及其判断方法
- 在ProductType枚举中新增AI_CAM_PHOTO_SET类型
- 扩展SourceMapper接口及XML实现删除指定faceId和type的关联记录功能
- 更新AppAiCamServiceImpl服务逻辑,在添加新关联前先删除旧记录
- 修改GoodsServiceImpl以识别并处理AI微单类型的商品名称前缀
- 在FaceServiceImpl中增加对AI微单内容的查询与展示逻辑
- 优化face相关素材分类展示,确保AI微单正确归类显示
2025-12-05 19:58:53 +08:00
1f4a16f0e6 feat(ai-cam): 实现AI相机商品识别与会员关联功能
- 新增AppAiCamController控制器,提供获取AI相机识别商品和添加会员素材关联接口
- 实现AppAiCamService服务,完成从人脸识别日志到商品详情的转换逻辑
- 扩展FaceDetectLogAiCamMapper,支持根据faceId查询识别记录
- 扩展SourceMapper,新增根据faceSampleIds和type查询source列表的方法
- 添加设备配置管理,支持按设备设置识别分数阈值和照片数量限制
- 实现人脸识别结果解析,提取匹配度高的faceSampleId并去重处理
- 完成商品详情VO转换,包含素材URL、视频URL及购买状态等信息
- 支持批量添加会员与素材的关联关系,确保数据一致性校验
2025-12-05 17:52:46 +08:00
e9916d6aca fix(service): 修复ZTSourceDataService中照片类型设置逻辑
- 将硬编码的照片类型值替换为从消息对象获取的动态类型值
- 确保entity.setType()方法正确反映实际的消息来源类型
- 维持原有缩略图URL和设备ID等其他属性的设置逻辑不变
2025-12-05 17:35:18 +08:00
b71452b3ed refactor(face): 替换Strings工具类引用以优化代码
- 将org.apache.logging.log4j.util.Strings替换为org.apache.commons.lang3.StringUtils
- 统一使用StringUtils处理字符串判空逻辑
- 优化线程join条件判断中的字符串比较方式
- 更新所有相关字符串工具方法调用以保持一致性
2025-12-05 17:08:16 +08:00
4a82ee6c4d feat(ai): 实现AI相机人脸识别日志记录功能
- 引入DeviceRepository以获取景区内所有AI相机设备
- 修改searchAndLog方法逻辑,遍历所有AI相机设备进行人脸搜索
- 新增searchDeviceAndLog私有方法处理单个设备的人脸识别与日志记录
- 更新FaceDetectLogAiCamService接口定义,移除deviceId参数
- 在FaceServiceImpl中调用新的日志记录服务
- 删除不再使用的DeviceConfigManager和FaceRecoveryStrategy依赖
- 调整日志记录中的字段名称及异常处理逻辑
2025-12-05 16:54:47 +08:00
24bbb63bf7 feat(config): 添加Mybatis Plus分页插件和Mapper扫描配置
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 配置MybatisPlusInterceptor分页插件
- 添加@MapperScan注解扫描多个mapper包
- 为多个service注入添加@Lazy注解解决循环依赖
- 在VoucherServiceImpl和PuzzleGenerateServiceImpl中启用懒加载
- 优化订单服务中的依赖注入配置
2025-12-05 16:14:43 +08:00
ee13ef09f7 refactor(pricing): 优化优惠提供者初始化和依赖注入
- 使用 @Lazy 注解解决循环依赖问题
- 重构 DiscountDetectionServiceImpl 以延迟加载优惠提供者
- 移除构造函数中的直接依赖注入,改用 ObjectProvider
- 添加线程安全的提供者初始化机制
- 移除不必要的缓存注释
2025-12-05 16:03:00 +08:00
33c3a194ca refactor(kafka): 修改人脸库分组命名规则
- 将人脸库分组名称从 "ai-cam-{deviceId}" 更改为 "AiCam{deviceId}"
- 更新 FaceProcessingKafkaService 中的数据库名称生成逻辑
- 同步修改 FaceDetectLogAiCamServiceImpl 中的数据库名称使用方式
2025-12-05 16:01:46 +08:00
71a8d3b539 refactor(core): 添加 Lazy 注解解决循环依赖问题
- 在多个 Service 类中为注入的依赖添加 @Lazy 注解
- 修改了微信支付服务实现类中的依赖注入方式
- 更新了打印机服务实现类中的依赖注入配置
- 调整了统计拦截器和服务类中的依赖注入策略
- 优化了 FaceService 和相关 Repository 的注入方式
- 防止应用启动时因循环依赖导致的初始化失败
2025-12-05 15:22:19 +08:00
82626f615b refactor(auth): 延迟加载RedisTemplate和Mapper依赖
- 为RedisTemplate添加@Lazy注解实现延迟加载
- 为scenicAccountMapper添加@Lazy注解实现延迟加载
- 为adminUserMapper添加@Lazy注解实现延迟加载
- 避免应用启动时不必要的依赖初始化
- 提升系统启动性能和资源利用率
2025-12-05 15:12:14 +08:00
de2eadf764 feat(biz): 新增AI微单照片集商品类型支持
- 在PriceBiz类中添加对"AI_CAM_PHOTO_SET"商品类型的处理逻辑
- 当商品类型为"AI_CAM_PHOTO_SET"时,返回固定的照片集条目
- 商品名称设置为"AI微单照片集"以区分普通照片集
2025-12-05 13:48:14 +08:00
fd143830d3 refactor(entity): 修改人脸检测日志实体字段名称
- 将 faceSampleId 字段重命名为 faceId
- 更新相关服务实现类中的字段赋值逻辑
- 保持数据库字段映射一致性
2025-12-05 13:39:01 +08:00
68916f3f53 feat(ai-cam): 新增AI相机人脸识别日志功能
- 创建人脸检测日志实体类FaceDetectLogAiCamEntity
- 实现对应的MyBatis Mapper接口FaceDetectLogAiCamMapper
- 添加服务接口及实现类FaceDetectLogAiCamService
- 支持调用适配器进行人脸搜索并记录日志
- 记录搜索结果、匹配分数及原始响应数据
- 处理异常情况并记录错误信息到日志表中
2025-12-05 12:35:21 +08:00
e27ed7d971 feat(kafka): 新增AI相机人脸处理消息消费逻辑
- 新增AI相机专用Kafka主题(zt-ai-cam-face)监听
- 新增FaceSampleAiCamMapper及对应XML映射文件
- 实现AI相机人脸数据入库及状态更新逻辑
- 实现基于设备ID的人脸库分组策略
- 添加异步人脸识别处理及评分更新功能
- 增加预订单任务触发机制
- 补充安全的状态更新与异常处理机制
2025-12-05 11:30:45 +08:00
7a19f18962 fix(dto): 扩展照片类型判断逻辑
- 修改 isPhoto 方法以支持类型 2 和 3 作为照片类型
- 保持原有的空值检查逻辑不变
- 确保向后兼容性的同时增加新的照片类型支持
2025-12-05 10:52:06 +08:00
eade5f8092 feat(printer): 优化自动添加照片到打印列表逻辑
- 修改autoAddPhotosToPreferPrint方法返回值为List<SourceEntity>
- 当自动添加成功时直接返回添加的照片列表
- 添加失败或无照片时返回空列表而非数量
- 控制器根据返回结果判断是否生成二维码URL
- 提升代码可读性和维护性
2025-12-04 21:23:23 +08:00
42540e2dc4 feat(printer): 新增人脸绑定二维码生成功能
- 添加获取人脸绑定二维码接口,支持生成小程序二维码
- 实现二维码文件流输出及临时文件清理
- 修改人脸识别流程,保存人脸数据并返回faceId
- 调整自动添加照片到打印列表逻辑,返回添加数量
- 更新响应模型,增加二维码URL字段
- 优化人脸匹配逻辑,使用memberRelationRepository查询关联照片
- 修复BCE适配器图片下载地址问题,去除内网地址替换逻辑
2025-12-04 18:21:18 +08:00
15dda645b9 test(pipeline): 添加管道构建器测试类的包导入
- 导入 AbstractPipelineStage 类
- 导入 Pipeline 接口
- 导入 PipelineBuilder 类
- 导入 StageResult 类
2025-12-04 16:17:43 +08:00
17419d83e7 docs(pipeline): 添加Pipeline通用管线框架设计文档
- 新增CLAUDE.md文件,详细描述Pipeline框架的设计理念与使用方法
- 介绍责任链模式、Builder模式在Pipeline中的应用
- 说明动态Stage添加、降级策略、配置驱动等核心特性
- 提供完整的业务实现示例(人脸匹配、图片处理)
- 详细阐述Pipeline、PipelineContext、PipelineStage等核心组件
- 描述StageResult状态管理及@StageConfig注解使用方式
- 展示PipelineBuilder构建器模式的灵活用法
- 提供从Context创建到Stage实现再到Pipeline组装的全流程指导
- 总结最佳实践,包括错误处理策略、性能优化建议和测试方法
- 回答常见问题,如跳过Stage、动态添加Stage及超时处理等场景
2025-12-04 10:50:02 +08:00
ae92ba10a7 refactor(pipeline): 统一导入StageResult类路径
- 将多个测试类中的StageResult导入路径从face.pipeline.core统一调整为pipeline.core
- 修复StageExecutionException类缺少PipelineException导入的问题
- 确保所有Stage相关测试类使用一致的包引用路径
2025-12-04 09:19:56 +08:00
af60cc1540 test(pipeline): 添加 Pipeline 核心功能测试
- 增加 Pipeline 执行流程测试,验证 Stage 和 Hook 的执行顺序
- 添加失败场景测试,确保 Pipeline 在失败时能正确停止并跳过后续阶段
- 实现 Stage 配置外部开关控制测试,验证可选 Stage 的启用与禁用逻辑
- 创建 RecordingContext 类用于记录执行事件,便于断言生命周期钩子调用
- 构建 SimpleStage 和 OptionalStage 测试辅助类,支持自定义执行逻辑与条件判断
2025-12-03 22:15:05 +08:00
60b4473279 refactor(pipeline): 重构人脸匹配管线为核心管线模块
- 移除专用人脸匹配管线实现,统一使用通用管线模块
- 更新所有Stage类继承自通用管线Stage基类
- 调整包路径引用从face.pipeline到pipeline.core
- 修改上下文类实现通用管线上下文接口
- 删除冗余的人脸匹配专用注解和枚举类
- 更新工厂类引用至新的通用管线构建器
- 保持Stage功能逻辑不变仅调整继承结构
2025-12-03 21:47:43 +08:00
ecd5378b26 fix(pipeline): 增加防御性检查避免空指针异常
- 在多个阶段中增加对 memberSourceList 和 searchResult 的空值检查
- 当 memberSourceList 为空时跳过视频重切和购买状态处理逻辑
- 当 searchResult 为空时跳过人脸补救逻辑
- 增加对自定义匹配场景的判断,非该场景则跳过指标记录
- 为各个阶段添加详细的单元测试覆盖各种边界条件
2025-12-03 19:23:54 +08:00
8c08c8947e feat(pipeline): 增强人脸匹配流水线的健壮性
- 在BuildSourceRelationStage中增加sampleListIds空值检查与降级处理
- 在PersistRelationsStage中增加memberSourceList空值检查与提前跳过逻辑
- 为BuildSourceRelationStage、DeleteOldRelationsStage和PersistRelationsStage添加完整的单元测试覆盖
- 实现异常情况下的优雅降级与错误日志记录
- 完善上下文状态管理与阶段跳过机制
2025-12-03 18:49:03 +08:00
b165840176 feat(face): 添加新人脸任务状态设置逻辑及单元测试
- 在SetTaskStatusStage中增加新人脸用户判断逻辑,非新用户跳过任务状态设置
- 新增LoadFaceSamplesStage、SetTaskStatusStage和UpdateFaceResultStage的完整单元测试
- 完善各阶段异常处理和边界条件测试,提升代码健壮性
- 添加大量测试用例覆盖成功、失败、异常等多种执行路径
- 验证任务状态设置、人脸样本加载和识别结果更新的核心功能
2025-12-03 18:41:24 +08:00
71d6400a1e test(pipeline): 添加人脸匹配流水线单元测试
- 为CustomFaceSearchStage添加完整单元测试覆盖各种匹配模式
- 为人脸识别阶段FaceRecognitionStage编写测试用例
- 为上下文准备阶段PrepareContextStage增加测试验证
- 包含成功、失败、异常等边界情况测试
- 验证不同匹配模式下的结果合并逻辑
- 测试人工选择和自动匹配场景的处理差异
2025-12-03 18:27:53 +08:00
b3fa10e8fd fix(pipeline): 增强人脸匹配流水线的健壮性
- 在FilterByTimeRangeStage中增加空值检查和配置验证
- 在LoadMatchedSamplesStage中增加sampleListIds空值检查
- 添加完整的集成测试覆盖Pipeline工厂和Context构建
- 为FilterByDevicePhotoLimitStage添加全面的单元测试
- 为FilterByTimeRangeStage添加边界条件和异常处理测试
- 为LoadMatchedSamplesStage添加异常路径测试
2025-12-03 18:17:34 +08:00
96e75a458f feat(face-pipeline): 实现人脸匹配管线核心框架
- 新增Stage配置注解@StageConfig,支持Stage元数据和可选性控制
- 创建抽象基类AbstractFaceMatchingStage,提供Stage执行模板方法
- 实现FaceMatchingContext上下文类,用于Stage间状态和数据传递
- 构建Pipeline核心执行类,支持Stage动态添加和执行控制
- 添加PipelineBuilder构建器,支持链式组装管线
- 定义PipelineStage接口和StageResult结果类,规范Stage行为
- 新增人脸匹配场景枚举FaceMatchingScene和Stage可选模式枚举
- 创建管线异常类PipelineException和StageExecutionException
- 实现FaceMatchingPipelineFactory工厂类,支持多场景管线组装
- 添加拼图生成编排器PuzzleGenerationOrchestrator,支持异步批量生成
- 创建BuildSourceRelationStage等核心Stage实现类
2025-12-03 18:11:31 +08:00
d2ad14175d feat(printer): 新增打印机大屏人脸识别功能
- 添加根据人脸样本ID查询图像素材接口
- 实现上传人脸照片进行景区人脸识别逻辑
- 集成存储服务用于保存临时人脸图片
- 调用景区人脸识别适配器搜索匹配人脸
- 查询并返回匹配的人脸样本关联图像素材
- 创建人脸识别结果含素材列表的响应对象
2025-12-03 16:44:32 +08:00
06c0ade9b4 feat(scenic): 添加我的页面订单显示配置项
- 在AppScenicController中新增showMyPagePaid和showMyPageUnpaid配置返回
- 在ScenicConfigResp中添加对应的布尔类型字段定义
- 支持控制我的页面是否展示已付款和未付款订单的开关功能
2025-12-03 15:20:40 +08:00
36f85dbb63 feat(device): 支持按多个景区ID查询设备列表
- 在 DeviceV2Client 中新增 scenicIds 查询参数
- 修改 DeviceIntegrationService.listDevices 方法以支持 scenicIds 参数
- 优化参数优先级逻辑:scenicId 优先于 scenicIds
- 更新所有调用点以传递新的 scenicIds 参数
- 保持向后兼容性,确保原有接口行为不变
- 增加日志记录以便调试和监控参数使用情况
2025-12-02 09:39:04 +08:00
9becd6bfa7 fix(order): 修复商品视频URL设置错误并增加pLog类型处理
- 修复商品视频URL字段赋值错误,使用setVideoUrl替代setUrl
- 增加对商品类型为5(pLog)的特殊处理逻辑
- 设置pLog类型商品的默认名称和封面URL
- 确保pLog类型商品信息完整性和一致性
2025-12-01 15:24:04 +08:00
788184fc04 Fix: Remove deprecated CachingConfigurerSupport in CustomRedisCacheManager 2025-12-01 14:54:09 +08:00
3cf7c81162 chore(log): 移除日志中的特殊符号以提升可读性
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 删除了所有日志语句中的  符号
- 删除了所有日志语句中的  符号
- 删除了所有日志语句中的 ⚠️ 符号
- 统一了日志输出格式,使其更简洁清晰
- 保留了关键错误信息的记录方式
- 确保日志信息在不同终端下的一致性显示
2025-12-01 10:55:13 +08:00
88d9463e25 Merge branch 'refs/heads/facebody_async'
Some checks are pending
ZhenTu-BE/pipeline/head Build queued...
2025-12-01 10:39:11 +08:00
590a7c6191 feat(printer): 打印任务更新后推送至打印机
- 在任务更新后调用推送服务将任务发送至指定打印机
- 记录任务更新完成的日志信息
2025-12-01 10:09:11 +08:00
d590286b13 feat(printer): 实现打印机任务WebSocket实时推送功能
- 新增PrinterTaskPushService接口及实现,负责任务推送逻辑
- 在PrinterServiceImpl中集成WebSocket推送,在任务创建和审核通过时主动推送
- 新增WebSocket配置类和处理器,支持打印机通过WebSocket连接接收任务
- 实现连接管理器,维护打印机在线状态并支持心跳保活
- 添加相关模型类如WsMessage、WsMessageType等,规范通信协议
- 在PrinterMapper中增加查询待处理任务列表的方法
- 完善异常处理和日志记录,确保推送可靠性
2025-12-01 09:59:27 +08:00
b92568b842 feat(face): 实现账号级人脸识别调度器
- 新增账号级别调度器管理器,支持多账号QPS隔离控制
- 为阿里云和百度云适配器添加配置getter方法
- 移除原有阻塞式限流逻辑,交由外层调度器统一管控
- 创建QPS调度器实现精确的任务频率控制
- 新增监控接口用于查询各账号调度器运行状态
- 重构人脸识别Kafka消费服务,集成账号调度机制
- 优化线程池资源配置,提升多账号并发处理效率
- 增强错误处理与状态更新的安全性
- 删除旧版全局线程池配置类
- 完善任务提交与状态流转的日志记录
2025-11-29 23:50:24 +08:00
1de760fc87 fix(image): 修复JPEG文件上传路径问题
- 从文件名中提取扩展名并标准化为小写
- 将.jpg扩展名统一转换为.jpeg
- 更新上传路径以包含正确的图像类型目录
- 保持原有公共读取权限设置
2025-11-29 19:41:25 +08:00
4e9aac4cf3 chore(threadpool): 调整人脸识别线程池配置
- 将核心线程数从32增加到128
- 将最大线程数从128增加到256
- 将空闲线程存活时间从60秒减少到10秒
- 将任务队列容量从1000调整为1024
2025-11-29 12:41:17 +08:00
aa43d14316 fix(printer): 处理空人脸样本导致的异常
- 添加空人脸样本检查,避免空指针异常
- 当人脸样本不存在时,设置响应状态为404
- 提前返回,防止后续逻辑执行
2025-11-29 12:07:27 +08:00
a2d87e7fdc refactor(product): 移除商品类型能力缓存配置
- 删除类级别的缓存配置注解
- 移除方法上的缓存注解
- 简化缓存刷新逻辑
- 更新相关方法签名
- 清理缓存策略文档注释
- 调整依赖注入方式以适应无缓存场景
2025-11-28 13:37:39 +08:00
57be6aa983 fix(redis): 配置Redis缓存管理器以防止ClassCastException
- 添加BasicPolymorphicTypeValidator以处理多态类型
- 在ObjectMapper中激活默认类型检查
- 更新Redis序列化配置以支持类型安全
- 防止因类型转换导致的运行时异常
- 确保JavaTimeModule与类型检查兼容
- 统一Redis缓存和模板的序列化配置
2025-11-28 13:37:26 +08:00
cacb22a7bd feat(cache): 配置Redis缓存管理器以支持Java时间序列化
- 添加Jackson ObjectMapper和JavaTimeModule依赖
- 配置RedisCacheConfiguration使用Jackson2JsonRedisSerializer序列化值
- 在RedisTemplate中设置值和哈希值的序列化器为Jackson2JsonRedisSerializer
- 启用对LocalDateTime等Java 8时间类型的序列化支持
2025-11-28 12:58:13 +08:00
300edbe582 refactor(device): 整合设备相关Feign客户端接口
- 将DefaultConfigClient、DeviceConfigV2Client、DeviceStatusClient的功能整合到DeviceV2Client
- 更新DeviceConfigIntegrationService、DeviceDefaultConfigIntegrationService和DeviceStatusIntegrationService依赖为DeviceV2Client
- 移除独立的设备配置与状态客户端接口文件
- 保留原有API路径结构并调整为统一前缀管理

refactor(render): 整合渲染工作器相关Feign客户端接口

- 将RenderWorkerConfigV2Client功能整合到RenderWorkerV2Client
- 更新RenderWorkerConfigIntegrationService依赖为RenderWorkerV2Client
- 移除独立的渲染工作器配置客户端接口文件
- 保留原有API路径结构并调整为统一前缀管理

refactor(scenic): 整合景区相关Feign客户端接口

- 将ScenicConfigV2Client和DefaultConfigClient的功能整合到ScenicV2Client
- 更新ScenicConfigIntegrationService和ScenicDefaultConfigIntegrationService依赖为ScenicV2Client
- 移除独立的景区配置与默认配置客户端接口文件
- 保留原有API路径结构并调整为统一前缀管理
2025-11-28 11:51:04 +08:00
9219ea4ab0 feat(price): 新增根据商品类型查询简化商品列表接口
- 在 PriceBiz 中新增 listSimpleGoodsByScenic 方法,支持按 productType 查询不同数据源
- 新增对多种商品类型的处理逻辑,包括 VLOG_VIDEO、PHOTO、PHOTO_SET 等
- 为兼容旧逻辑,增加 listAllSimpleGoods 方法轮询所有启用的商品类型
- 在 PriceConfigController 中修改 goodsList 接口,支持 productType 参数并返回简化商品列表
- 引入 SimpleGoodsRespVO 用于简化商品信息响应结构
- 注入 PuzzleTemplateMapper 和 IProductTypeCapabilityManagementService 依赖以支持新功能
2025-11-28 11:19:00 +08:00
e292a0798d refactor(order): 重构重复购买检查策略
- 移除SetIdDuplicateChecker和VideoIdDuplicateChecker两个具体策略类
- 更新DuplicateCheckStrategy枚举,将CHECK_BY_SET_ID和CHECK_BY_VIDEO_ID
  替换为更通用的UNIQUE_RESOURCE和PARENT_RESOURCE策略
- 修改ProductTypeCapabilityManagementServiceImpl中的策略分配逻辑
- UNIQUE_RESOURCE适用于照片、视频等独立资源的重复购买检查
- PARENT_RESOURCE适用于套餐类商品的重复购买检查
- 打印类商品现在正确设置为允许重复购买且不检查
- 其他类别商品默认设置为不检查重复购买
2025-11-28 00:56:41 +08:00
4244b42d4b Merge branch 'refs/heads/order_v2' 2025-11-28 00:35:33 +08:00
8058bc21f5 fix(utils): 修正图片旋转角度参数
- 将旋转角度从270度更正为90度
- 保持旋转后宽高的正确计算逻辑
- 确保测试场景覆盖正确的旋转角度
2025-11-27 22:25:17 +08:00
6dd08ac4e7 feat(product): 实现商品类型能力配置管理功能
- 新增商品类型能力配置的增删改查接口
- 实现分页查询、分类查询、状态筛选等功能
- 支持批量初始化默认配置和缓存刷新
- 提供定价模式、重复检查策略等枚举选项接口
- 实现完整的参数校验和业务逻辑处理
- 添加详细的日志记录和异常处理机制
2025-11-27 20:52:32 +08:00
610a183be1 feat(image): 添加图像超分处理功能
- 新增 ImageSRStage 类实现图像超分辨率处理
- 在 AioDeviceController 中启用图像超分和增强的 Stage
- 修改 ImageEnhanceStage 配置检查逻辑,增加空值和占位符检测
- 为图像处理 Pipeline 添加超分 Stage
- 增加 ImageSRStage 的单元测试覆盖各种配置和执行情况
- 实现百度云图像超分 API 的调用和结果处理逻辑
2025-11-27 18:45:10 +08:00
e9a59cd466 feat(pricing): 添加商品分类枚举并扩展商品类型枚举
- 新增 ProductCategory 枚举类,定义商品分类
- 为 ProductType 枚举增加分类关联字段
- 扩展 ProductType 枚举值并按分类分组注释
- 添加获取分类代码和描述的方法
- 实现根据代码查找枚举的静态方法
- 完善枚举类的文档注释和类型安全引用
2025-11-27 18:39:43 +08:00
d60d7d9ad8 feat(image): 增强图片处理流程并优化水印逻辑
- 在PhotoProcessContext中新增Stage管理相关方法,支持启用、禁用及批量设置Stage状态
- 新增ImageEnhanceStage并整合到图片处理流水线中
- 重构重打印流程,复用普通照片处理流水线
- 生成订单二维码并用于水印配置
- 移除冗余的水印配置和增强配置代码
- 优化Stage控制逻辑,支持动态启用或禁用特定处理阶段
2025-11-27 18:17:19 +08:00
d483c222d0 fix(face): 调整任务状态为正在生成时的锁定类型值
- 将任务状态为正在生成时的lockType从0修改为-9
- 确保正在生成状态能被正确识别和处理
2025-11-27 17:14:50 +08:00
a7ef2cb35a feat(printer): 实现带图像增强选项的重新打印功能
- 在 ReprintRequest 中新增 needEnhance 字段以支持图像增强
- 将 reprint 接口的实现从 controller 下移到 printerService
- 实现 handleReprint 方法,支持根据 needEnhance 条件性添加图像增强阶段
- 重构 reprint 流程,引入 Pipeline 处理图像下载、旋转、增强、水印等步骤
- 增强 reprint 异常处理,失败时回退到原始裁剪图
- 移除 ImageEnhanceStage 中对 TODO 占位符的判断逻辑
- 提供 updateTaskStatusAndPrinter 兜底方法用于无 MemberPrint 场景
2025-11-27 16:04:55 +08:00
cbc0584706 feat(face): 添加人脸识别防重复调用机制
- 引入 FaceMatchDedupService 用于防止短时间内重复调用
- 在匹配前检查是否应跳过本次调用
- 匹配完成后标记已处理,避免重复执行
- 增强系统稳定性与性能,减少无效计算
2025-11-27 16:04:23 +08:00
67932c374b fix(order): 修复订单商品类型处理逻辑
- 在视频和照片原素材处理后添加break语句
- 防止switch语句穿透导致重复执行
- 确保每种商品类型只处理一次
- 清理订单缓存前确保所有商品处理完成
2025-11-27 13:57:14 +08:00
8a88c74df2 feat(pricing): 支持景区维度的价格配置和优惠策略控制
- 新增按景区ID查询商品配置和阶梯价格配置的方法
- 扩展价格计算服务以支持景区级别的优惠策略
- 更新优惠券和代金券提供者以使用景区维度配置
- 修改商品配置服务实现多级查询优先级(景区特定->景区默认->全局特定->全局默认)
- 添加商品类型能力服务测试用例
- 增强价格计算逻辑的容错性和向后兼容性
2025-11-27 13:55:51 +08:00
3ce3972875 refactor(order): 重构重复购买检查和定价逻辑
- 引入商品类型能力配置,替代硬编码的商品类型判断
- 实现策略模式处理不同商品类型的重复购买检查
- 抽象定价模式,支持固定价格和数量计价等不同方式
- 新增策略工厂自动注册各类检查器实现
- 添加缓存机制提升商品类型配置查询性能
- 解耦订单服务与具体商品类型的紧耦合关系
- 提高代码可维护性和扩展性,便于新增商品类型
2025-11-27 09:34:10 +08:00
1945639f90 refactor(image): 重构图片旋转和恢复逻辑
- 将 needRotation 标志重命名为 rotationApplied
- 修改条件旋转阶段的执行逻辑,基于实际旋转角度判断
- 实现通用的图片恢复旋转功能,支持90/180/270度恢复
- 添加恢复旋转角度计算方法 getRestoreAngle
- 更新水印阶段的旋转状态检查逻辑
- 完善单元测试覆盖各种旋转场景
- 优化日志记录和错误处理流程
2025-11-26 20:15:02 +08:00
40d5874560 refactor(image): 重构图片旋转和恢复逻辑
- 将 needRotation 标志重命名为 rotationApplied
- 修改条件旋转阶段的执行逻辑,基于实际旋转角度判断
- 实现通用的图片恢复旋转功能,支持90/180/270度恢复
- 添加恢复旋转角度计算方法 getRestoreAngle
- 更新水印阶段的旋转状态检查逻辑
- 完善单元测试覆盖各种旋转场景
- 优化日志记录和错误处理流程
2025-11-26 16:05:12 +08:00
95419fee66 refactor(image): 调整水印偏移量处理逻辑
- 将 PORTRAIT 偏移量常量重命名为 PRINTER
- 根据图像旋转角度动态设置左右偏移量
- 优化旋转状态下水印位置计算逻辑
2025-11-26 14:58:44 +08:00
333c4d3ca7 refactor(image): 重构水印处理逻辑以提高可维护性
- 移除 PhotoProcessContext 中的水印相关字段
- 新增 WatermarkConfig 类封装水印配置
- 修改 WatermarkStage 通过构造函数注入配置
- 调整 PrinterServiceImpl 中水印配置的传递方式
- 更新单元测试以适应新的配置注入方式
- 统一从配置对象读取水印参数而非上下文
- 优化日志记录与偏移量计算逻辑
2025-11-26 14:56:37 +08:00
90efc908c5 feat(image): 支持多角度图片旋转及方向判断
- 在 PhotoProcessContext 中新增 imageRotation 字段用于存储旋转角度
- 修改 ConditionalRotateStage 支持 90、180、270 度旋转
- 优化 ImageOrientationStage 综合判断图片方向逻辑
- 新增 NoOpStage 作为空操作阶段占位符
- 解除 DeviceVideoContinuityCheckTask 的生产环境限制
- 添加完整的单元测试覆盖各种旋转场景和边界情况
2025-11-26 14:34:17 +08:00
d2846e6d8e fix(core): 修复 StageResult 中 nextStages 的不可变性问题
- 将 nextStages 初始化改为使用 Collections.unmodifiableList 包装
- 防止外部代码修改 nextStages 列表内容
- 保证 StageResult 的不可变性和线程安全性
- 添加完整的单元测试覆盖各种构造场景
2025-11-26 09:03:36 +08:00
7b18d7c2af feat(image): 实现源图片超分辨率增强流水线
- 引入Pipeline模式重构图片处理流程
- 新增SourcePhotoUpdateStage用于上传并更新源图片URL
- 扩展PhotoProcessContext支持超分场景配置
- 增加SOURCE_PHOTO_SUPER_RESOLUTION枚举值
- 修改各Stage判断逻辑适配新的图片类型系统
- 调整SourceService接口支持File类型参数
- 优化超分处理日志记录和异常处理机制
2025-11-25 19:17:55 +08:00
bcebe5defe feat(image): 实现图像增强与质量检测功能
- 新增ImageEnhancerFactory工厂类,支持创建不同类型的图像增强器
- 添加图像清晰度增强和超分辨率两种增强模式
- 实现ImageEnhanceStage图像增强处理阶段
- 新增ImageQualityCheckStage图像质量检测阶段
- 支持根据图片质量动态添加图像增强处理
- 完善Stage配置注解和可选性控制机制
- 优化Pipeline执行流程,支持动态插入Stage
- 增加Stage执行计数和循环依赖防护机制
- 改进StageResult结构,支持携带后续Stage列表
- 统一抽象Stage的执行条件判断逻辑
2025-11-25 11:21:03 +08:00
4a86849372 feat(image): 引入图片来源和处理场景枚举支持
- 新增 ImageSource 枚举定义图片来源类型(IPC、相机、手机等)
- 新增 PipelineScene 枚举定义管线处理场景(打印、增强等)
- 在 PhotoProcessContext 中添加 scenicConfigManager、scene 和 source 字段
- 在 PrinterServiceImpl 中根据 sourceId 判断并设置图片来源
- 在 PrinterServiceImpl 中设置默认管线场景为图片打印
- 修改 prepareNormalPhotoContext 和 prepareStorageAdapter 方法签名
- 优化配置获取逻辑,统一从 context 中获取 scenicConfigManager
2025-11-24 23:54:22 +08:00
e418a5ccdb feat(printer): 引入照片处理管线机制
- 新增Crop和PrinterOrderItem模型用于封装裁剪信息和打印订单项
- 实现基于Pipeline模式的照片处理流程,支持普通照片和拼图处理
- 添加多个处理阶段:下载、方向检测、条件旋转、水印、恢复方向、上传和清理
- 创建PipelineBuilder用于动态构建处理管线
- 实现抽象Stage基类和具体Stage实现类
- 添加Stage执行结果管理和异常处理机制
- 优化照片处理逻辑,使用管线替代原有复杂的嵌套处理代码
- 支持通过景区配置管理水印类型、存储适配器等参数
- 提供临时文件管理工具确保处理过程中文件及时清理
- 增强日志记录和错误处理能力,提升系统可维护性
2025-11-24 21:18:35 +08:00
4360ef1313 feat(device): 实现设备视频连续性检查功能
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 新增设备视频连续性检查控制器 DeviceVideoContinuityController
- 提供查询、手动触发和删除检查结果的 REST 接口
- 实现视频连续性检查核心逻辑,支持检测视频间隙
- 添加定时任务 DeviceVideoContinuityCheckTask 自动检查设备视频连续性
- 仅在生产环境(prod)启用,每天9点到18点间每5分钟执行一次
- 支持阿里云OSS和本地存储的视频连续性检查
- 检查结果缓存至 Redis,默认保留24小时
- 新增相关实体类: DeviceVideoContinuityCache、VideoContinuityGap、VideoContinuityResult
- 在存储操作接口中增加 checkVideoContinuity 和 checkRecentVideoContinuity 方法
- 为不支持的存储类型提供默认不支持连续性检查的实现
2025-11-24 14:02:53 +08:00
9278d4479f feat(printer): 优化拼图打印偏移处理逻辑
- 添加白边框并向上偏移内容以避免打印机偏移
- 替换原有的单纯向上偏移方法
- 弃用 shiftImageUp 方法,新增 addBorderAndShiftUp 方法
- 更新临时文件命名及清理逻辑
- 修改日志记录内容以反映新的处理方式
2025-11-22 00:07:18 +08:00
18bf51487d feat(printer): 优化拼图打印逻辑并调整日期格式
- 调整AppPuzzleController中recordId参数为固定值0L
- 修改FaceMatchingOrchestrator中的日期格式为"yyyy.MM.dd"
- 完善PrinterServiceImpl水印处理条件判断
- 新增针对sourceId为0时的拼图照片偏移处理逻辑
- 修复重复打印检查逻辑,使用resultImageUrl代替puzzleRecordId比较
- 增强异常处理和日志记录,提升系统稳定性
2025-11-21 23:25:34 +08:00
447e8799e8 refactor(repository): 移除冗余的用户购买状态查询逻辑
- 删除 SourceRepository 中的 getUserIsBuy 方法
- 删除 VideoRepository 中的 getUserIsBuy 方法
- 简化业务逻辑,减少重复代码
- 提高代码可维护性和清晰度
2025-11-21 22:24:32 +08:00
fd130c471f feat(order): 添加未来模板一口价购买逻辑
- 引入 IsBuyBatchRespVO 类以支持批量购买响应
- 实现视频模板买断逻辑,优先检查模板是否已购买
- 在商品类型为0时,查询视频模板并判断用户是否已购买
- 若已购买,直接返回订单ID及购买状态,跳过价格查询
- 保留原有价格查询逻辑作为兜底方案
2025-11-21 22:15:17 +08:00
c47c24a39a refactor(goods): 移除商品列表查询接口及关联逻辑
- 删除 GoodsService 中的 goodsList 接口定义
- 移除 GoodsServiceImpl 中 goodsList 方法的实现
- 清理相关导入语句和无用代码引用
- 简化商品服务模块,聚焦于源素材商品列表功能
2025-11-21 21:52:14 +08:00
97e3ab19a0 refactor(order): 重构订单购买逻辑并优化接口参数
- 调整 isBuy 方法参数顺序,增加 faceId 参数支持
- 删除冗余的购买检查方法和旧版 isBuy 重载方法
- 简化购买状态判断逻辑,移除重复代码
- 更新视频查看权限服务中的购买检查调用
- 修改人脸服务中景区 ID 类型为 Long
- 调整打印机服务中人脸查询方法参数类型
- 统一订单业务类中方法签名和调用方式
- 移除订单请求模型中无用字段注释
- 增加人脸 ID 列表字段支持批量查询
- 优化任务服务中购买状态检查逻辑
2025-11-21 21:45:26 +08:00
5b27cac6b0 feat(service): 优化商品查询逻辑并新增分组查询接口
- 在 SourceMapper 中新增 queryGroupedByFaceAndType 方法,支持按 faceId 和 type 分组查询
- 调整 orderBiz.isBuy 方法的参数顺序,统一调用格式
- 修改 GoodsServiceImpl 中源素材查询逻辑,使用新分组方法减少循环嵌套
- 简化源素材去重及过滤禁用类型的处理流程
- 提前获取景区配置信息,避免重复查询
- 优化代码结构,提升可读性和维护性
2025-11-21 21:43:37 +08:00
91f3632e2b fix(printer): 修复人脸照片统计逻辑
- 在统计人脸照片数量时增加状态过滤条件
- 仅统计状态为0的有效记录
- 避免已删除或无效数据影响统计结果
2025-11-21 21:06:28 +08:00
cd8ae491e2 feat(mobile): 实现基于人脸ID的商品列表查询功能
- 修改AppFaceController中list方法,将scenicId转换为Long类型传递
- 在AppGoodsController中注入FaceService,并在goodsList接口中调用faceService获取人脸列表
- 更新FaceMapper中的listByScenicAndUserId方法签名,统一scenicId参数类型为Long
- GoodsServiceImpl中新增listGoodsByFaceIdList方法,实现根据人脸ID列表查询相关商品逻辑
- 商品查询支持按成片vlog和源素材分类展示,并去重处理
- 优化GoodsService接口,增加listGoodsByFaceIdList方法定义
- OrderMapper.xml
2025-11-21 20:49:05 +08:00
d0d238d31d feat(order): 添加景区全免逻辑处理
- 引入ScenicConfigManager依赖
- 实现景区全免配置判断
- 设置全免订单价格为零
- 更新订单响应状态逻辑
2025-11-21 19:52:13 +08:00
2be30c6eb4 refactor(price): 重构价格购买方法命名以明确用途
- 将 isBuy 方法重命名为 isOnePriceBuy 以准确反映其功能
- 更新所有调用点以使用新的方法名
- 清理未使用的导入包和变量声明
- 移除与一口价购买无关的冗余代码引用
2025-11-21 19:48:54 +08:00
fb82329a88 fix(order): 修复订单购买状态判断逻辑
- 调整isBuy方法参数顺序,确保 memberId 和 scenicId 正确传递
- 在OrderBiz中设置默认buy状态为false,避免空指针异常
- 修改OrderMapper查询条件,增加refund_status=0过滤已退款订单
- 优化face服务中调用isBuy方法时的参数传递逻辑
2025-11-21 19:45:21 +08:00
4f0d6dc44f feat(order): 新增根据人脸ID查询购买记录功能
- 在OrderBiz中增加isBuy方法重载,支持通过人脸ID查询购买记录
- 修改AppPuzzleController中调用参数顺序,适配新方法签名
- 在OrderMapper接口中新增getUserBuyFaceItem方法定义
- 在OrderMapper.xml中实现getUserBuyFaceItem的SQL查询逻辑
- 调整FaceServiceImpl中相关调用逻辑,移除冗余配置获取代码
2025-11-21 19:27:53 +08:00
302b6811c4 feat(puzzle): 优化二维码生成与去重逻辑
- 避免重复上传已存在的微信小程序二维码
- 在去重检测中跳过 dateStr 字段以提高准确性
- 添加文件存在性检查,减少不必要的上传操作
- 记录并返回已存在文件的访问 URL
- 提升 puzzle 服务的性能与资源利用率
2025-11-21 18:11:41 +08:00
c0daa4d3b2 refactor(face): 优化拼图模板内容生成逻辑
- 修改拼图模板内容生成方式,支持多个模板内容生成
- 使用forEach循环处理每个模板,确保内容正确插入
- 保留原有价格计算和购买状态判断逻辑
- 确保contentId在记录存在时正确设置
- 维持原有的商品类型、分组和排序设置
2025-11-21 17:46:28 +08:00
83cfbc67e1 fix(repository): 修复人脸缓存获取逻辑
- 添加空值检查避免解析空字符串
- 提取缓存键避免重复格式化
- 优化缓存命中时的对象转换逻辑
2025-11-21 17:07:29 +08:00
8f918570d9 feat(puzzle): 动态设置拼图名称
- 注入PuzzleTemplateMapper依赖
- 根据模板ID获取拼图模板名称
- 使用模板名称替换硬编码的"三拼图"名称
2025-11-21 17:07:24 +08:00
f4a3dc9cae fix(order): 修复订单支付后商品创建时间获取逻辑
- 删除冗余的商品创建时间计算代码
- 优化订单支付后的统计记录逻辑
- 清理无用的日期比较操作
- 提升代码可读性和维护性
2025-11-21 17:04:12 +08:00
cd5ba23d59 feat(puzzle): 添加模板封面图片字段并更新相关逻辑
- 在PuzzleTemplateDTO和TemplateCreateRequest中新增coverImage字段
- 在PuzzleTemplateEntity中新增coverImage字段并映射到数据库
- 更新FaceServiceImpl以支持获取模板封面图片URL
- 修改Mapper XML文件以支持coverImage字段的读写操作
- 调整SQL查询和插入语句以包含新的coverImage字段
- 更新三拼图内容页面逻辑以使用模板封面图片URL
2025-11-21 16:04:59 +08:00
038b2e6f08 fix(order): 修复订单查询逻辑
- 在查询条件中添加了 goodsId 过滤
- 为避免多余数据返回,增加了 limit 1 限制
- 确保订单项查询的准确性与性能优化
2025-11-21 15:02:26 +08:00
caad0c2cf0 feat(order): 添加plog图商品类型支持
- 在OrderServiceImpl中增加对商品类型5的处理逻辑
- 设置商品名称和订单类型为"plog图"
- 在OrderMapper.xml中新增member_plog_data查询块
- 添加对goods_type为5时coverUrl和imgUrl的映射
- 增加对goods_type为5时商品名称的显示处理
- 新增member_plog_data表的左连接查询条件
2025-11-21 14:40:08 +08:00
259d99bde7 feat(face): 添加购买状态判断逻辑
- 在生成内容时增加对用户是否已购买的判断
- 根据购买状态设置内容的 isBuy 字段
- 调用 orderBiz.isBuy 方法检查购买状态
- 使用 scenicId 和 templateId 作为购买查询条件
2025-11-21 14:03:54 +08:00
0e2122910f feat(face): 新增人脸匹配编排流程中的任务状态管理
- 引入 TaskStatusBiz
2025-11-21 13:56:20 +08:00
e1a77a1614 feat(printer): 移除拼图照片自动裁剪功能
- 删除了从打印机配置获取打印尺寸的逻辑
- 移除了调用ImageUtils.smartCropAndFill进行图片裁剪的代码
- 去掉了裁剪后图片上传和临时文件清理的相关实现
- 简化了打印服务流程,直接使用原始图片URL
- 保留了cropUrl字段但不再进行实际裁剪操作
2025-11-21 11:47:52 +08:00
8791cf5910 fix(printer): 修复上传裁剪图片时的文件扩展名获取逻辑
- 将文件扩展名从resultImageUrl改为croppedFile.getName()中获取
- 确保上传裁剪后图片时能正确识别文件类型
- 避免因URL解析错误导致的文件扩展名丢失问题
2025-11-21 11:47:05 +08:00
a860319ea1 refactor(puzzle): 移除拼图生成记录中的复用逻辑
- 删除 PuzzleGenerationRecordEntity 中的 isDuplicate 和 originalRecordId 字段
- 移除插入记录时设置 isDuplicate 的逻辑
- 删除 FaceMatchingOrchestrator 中查询历史记录的逻辑
- 更新 Mapper XML 文件,移除相关字段和条件判断
- 简化生成流程,不再检查模板是否已生成
2025-11-21 11:41:11 +08:00
d5fc5c2565 feat(scenic): 添加新的景区ID到白名单
- 在白名单中新增景区ID: 4049850382325780480
- 完善景区列表分页查询功能的ID过滤逻辑
2025-11-21 11:02:56 +08:00
0db713b4a8 feat(puzzle): 实现拼图生成去重机制
- 新增内容哈希计算逻辑,基于元素内容生成SHA256哈希用于去重判断
- 添加重复图片检测功能,当所有IMAGE元素使用相同URL时抛出异常
- 实现历史记录查询接口,根据模板ID、内容哈希和景区ID查找重复记录
- 扩展生成响应对象,增加isDuplicate和originalRecordId字段标识复用情况
- 更新数据库实体和Mapper,新增content_hash、is_duplicate等字段支持去重
- 添加完整的单元测试和集成测试,覆盖去重检测、哈希计算等核心逻辑
- 引入DuplicateImageException和PuzzleBizException异常类完善错误处理
2025-11-21 11:02:43 +08:00
6ef710201c fix(order): 修正订单商品名称逻辑并更新购买检查参数
- 修改AppPuzzleController中isBuy方法的参数传递逻辑
- 在OrderServiceImpl中为未知类型添加默认商品名称
- 统一景区相关商品的命名规则
2025-11-21 10:03:10 +08:00
9123a1f6db feat(puzzle): 新增拼图功能模块
- 新增AppPuzzleController控制器,提供拼图相关接口
- 实现根据faceId查询拼图数量和记录列表功能
- 实现根据recordId查询拼图详情和下载拼图资源功能
- 实现拼图价格计算和导入打印列表功能
- 在FaceServiceImpl中集成拼图记录展示逻辑
- 在OrderServiceImpl中新增PHOTO_LOG产品类型处理
- 在PrinterService中实现从拼图添加到打印列表的功能
- 完善拼图记录转换为内容页面VO的逻辑处理
2025-11-20 23:11:04 +08:00
d458f918ed feat(text): 实现文本垂直居中对齐功能
- 修改TextElement类中的Y坐标计算逻辑
- 新增总文本高度计算和垂直偏移量
- 调整起始Y坐标以支持垂直居中对齐
- 保持原有逐行绘制逻辑不变
2025-11-20 23:10:50 +08:00
27e58d36d0 test(puzzle): 更新测试用例以适配新的执行结果结构
- 移除已弃用的 DeviceCountRangeConditionStrategy 策略注册
- 修改 PuzzleElementFillEngine 执行方法调用方式,使用 getDynamicData 获取动态数据
- 在 PuzzleGenerateServiceImplTest 中引入 FillResult 类型并更新 mock 返回值结构
- 统一调整所有相关测试断言逻辑以匹配新返回的数据格式
2025-11-20 23:10:27 +08:00
8c76c85ae2 feat(puzzle): 添加拼图生成记录检查逻辑
- 引入 PuzzleGenerationRecordEntity 和 PuzzleGenerationRecordMapper
- 在人脸匹配编排流程中查询已有拼图生成记录
- 增加模板重复生成判断逻辑,避免重复处理
- 跳过已生成模板并记录日志提示
2025-11-20 17:33:51 +08:00
8991d68673 docs(claude): 更新设备数量匹配策略的描述
- 修改模式1的匹配逻辑为实际机位数大于等于deviceCount
- 修改模式2的匹配逻辑为从指定列表过滤后数量大于等于deviceCount
- 保持配置顺序并只取前N个设备进行匹配
2025-11-20 17:27:31 +08:00
3b93e07a66 feat(fill): 更新机位数量匹配策略为大于等于匹配
- 修改策略注释说明匹配方式由精确匹配改为大于等于匹配
- 更新全局数量匹配逻辑,从 == 改为 >= 判断
- 更新列表数量匹配逻辑,从 == 改为 >= 判断
- 在列表匹配成功时,只取前 N 个机位存入 context.extra
- 调整日志描述,明确显示最小数量与实际数量的比较
- 更新单元测试用例以验证大于等于匹配逻辑
- 增加测试用例验证匹配成功时只取前 N 个机位的行为
- 调整测试用例名称和断言逻辑以适应新的匹配规则
2025-11-20 16:38:08 +08:00
c8054c60ab feat(puzzle): 启用规则匹配以增强拼图生成
- 在拼图生成请求中添加规则匹配选项
- 设置 requireRuleMatch 参数为 true 以启用高级验证
- 确保动态数据映射保持不变
- 保留现有质量与格式设置配置
2025-11-20 15:20:36 +08:00
2fd852c5c6 feat(puzzle): 增强拼图填充引擎功能
- 新增 requireRuleMatch 参数控制是否必须匹配规则
- 重构 DeviceCountConditionStrategy 支持两种匹配模式
- 移除已废弃的 DeviceCountRangeConditionStrategy
- 引入 FillResult 类封装填充结果信息
- 优化条件上下文和数据源上下文的 extra 字段类型
- 更新相关测试用例和文档说明
2025-11-20 15:11:13 +08:00
aaa8d8310a feat(source): 新增根据人脸和设备ID获取素材的功能
- 在SourceMapper接口中新增getSourceByFaceAndDeviceId方法
- 支持通过faceId、deviceId、type和排序策略查询特定素材
- 在XML映射文件中实现对应的SQL查询逻辑
- 支持多种排序策略:最新、最早、评分降序、评分升序、随机和已购买优先
- 查询结果限制为一条记录
2025-11-20 14:55:28 +08:00
8d2d0901fd feat(face): 添加景区名称和日期到动态数据
- 引入日期工具类以支持日期格式化
- 在基础动态数据中增加景区名称字段
- 添加当前日期字符串到基础动态数据
- 为后续模板生成提供更丰富的上下文信息
2025-11-20 13:51:04 +08:00
d1381c93b0 feat(puzzle): 更新拼图元素填充引擎执行方法参数
- 在engine.execute方法调用中增加scenicId参数
- 修改测试用例以适应新的方法签名
- 确保所有相关测试验证逻辑正确性
- 更新PuzzleGenerateServiceImplTest中的fillEngine调用参数
- 调整verify语句匹配新参数列表
- 保持原有功能逻辑不变仅扩展参数传递
2025-11-20 11:41:22 +08:00
536f2866f6 feat(puzzle): 添加统计人脸ID生成记录数量功能
- 在PuzzleGenerationRecordMapper接口中新增countByFaceId方法
- 在PuzzleGenerationRecordMapper.xml中实现对应的SQL查询
- 支持根据faceId统计生成记录的数量
2025-11-20 11:41:09 +08:00
4cbd0dc255 feat(puzzle): 新增微信小程序二维码生成功能
- 在DataSourceContext中新增scenicId字段用于景区关联
- 实现WechatQrcodeDataSourceStrategy策略类,支持生成并上传微信小程序码
- 扩展DataSourceType枚举,增加WECHAT_QRCODE类型
- 修改PuzzleElementFillEngine执行方法,支持传入scenicId参数
- 在PuzzleGenerateServiceImpl中集成二维码自动生成逻辑
- 新增generateWechatQrcode方法用于生成并上传小程序码到OSS
- 完善日志记录和异常处理机制
- 添加必要的工具类和存储服务依赖注入
2025-11-20 11:00:53 +08:00
90cf0d44c9 feat(video-review): 优化视频评价导出功能,支持机位名称动态表头
- 引入DeviceRepository用于批量查询机位名称
- 在导出逻辑中收集并排序机位ID,确保表头顺序一致
- 动态生成Excel表头,使用实际机位名称替代原始JSON字段
- 调整单元格样式以支持自动换行,提升可读性
- 更新mapper配置,关联template表获取模板名称
- 优化列宽自适应逻辑,为机位列设置最小宽度保障显示效果
- 日志记录中增加导出机位数量统计信息
2025-11-20 11:00:29 +08:00
d387f11173 feat(video): 添加视频模板ID和名称字段
- 在VideoReviewRespDTO中新增templateId字段
- 在VideoReviewRespDTO中新增templateName字段
- 添加相应字段的注释说明
- 支持关联查询video表获取模板信息
2025-11-20 10:48:08 +08:00
f6d6a63977 feat(puzzle): 修改生成记录查询逻辑以支持人脸ID
- 将查询条件从 orderId 更改为 faceId
- 更新 Mapper 接口方法名和参数
- 修改 XML 映射文件中的字段和查询条件
- 调整插入记录时使用的字段名称
- 更新基础列定义以反映新的字段结构
2025-11-20 10:48:08 +08:00
67aebd5770 refactor(puzzle): 移除填充规则中的景区ID依赖
- 删除 PuzzleFillRuleDTO、PuzzleFillRuleSaveRequest 和 PuzzleFillRuleEntity 中的 scenicId 字段
- 从 ConditionContext 和 DataSourceContext 中移除 scenicId 属性
- 更新 PuzzleElementFillEngine 的 execute 方法,不再接收和传递 scenicId 参数
- 修改 PuzzleGenerateServiceImpl 中调用填充引擎的逻辑,去除 scenicId 判断和传参
- 调整 PuzzleFillRuleMapper.xml 配置文件,移除 scenic_id 映射关系
- 更新所有相关单元测试用例,删除对 scenicId 的引用和验证
- 简化规则查询方法,由 listByTemplateAndScenic 改为 listByTemplateId
- 移除因缺少 scenicId 而产生的警告日志和特殊处理分支
2025-11-19 23:23:08 +08:00
6d18a770b8 feat(puzzle): 实现人脸匹配后异步生成拼图模板功能
- 移除查询规则时的景区ID参数,简化规则加载逻辑
- 为人脸匹配编排器添加拼图模板服务依赖
- 新增异步生成拼图模板方法,在人脸识别成功后触发
- 优化Mapper接口,添加@Mapper注解并移除冗余查询方法
- 更新文档说明,同步修改规则查询方式描述
- 清理SourceMapper中重复的deleted条件过滤逻辑
2025-11-19 22:48:01 +08:00
b6cbb18a7f docs(puzzle): 更新Claude模块文档结构
- 移除了联系方式和维护者信息
- 简化了文档结尾的元数据部分
- 优化了设备ID匹配策略文档的引用格式
2025-11-19 18:29:45 +08:00
cfb3625ac0 feat(puzzle): 实现智能自动填充引擎和安全增强
- 新增拼图元素自动填充引擎 PuzzleElementFillEngine
- 支持基于规则的条件匹配和数据源解析
- 实现机位数量、机位ID等多维度条件策略
- 添加 DEVICE_IMAGE、USER_AVATAR 等数据源类型支持
- 增加景区隔离校验确保模板使用安全性
- 强化图片下载安全校验,防范 SSRF 攻击
- 支持本地文件路径解析和公网 URL 安全检查
- 完善静态值数据源策略支持 localPath 配置
- 优化生成流程中 faceId 和 scenicId 的校验逻辑
- 补充相关单元测试覆盖核心功能点
2025-11-19 17:28:41 +08:00
cb17ea527b refactor(SourceMapper): 优化查询条件动态拼接逻辑
- 使用<where>标签替换原有静态where条件
- 添加对scenicId、isBuy、type、faceId参数的动态判断
- 确保只有非空参数参与SQL查询条件构建
- 提高SQL语句可读性和维护性
- 避免因缺少条件导致的语法错误风险
2025-11-19 15:08:59 +08:00
625ad910c9 feat(printer): 添加素材打印状态查询功能
- 在PrinterMapper中新增countFacePhoto方法用于统计用户打印素材数量
- 创建GoodsDetailPrintSceneVO类继承GoodsDetailVO并添加inList字段
- 修改GoodsReqQuery类添加scene字段用于标识打印场景
- 在GoodsServiceImpl中注入PrinterMapper并实现打印状态判断逻辑
- 在PrinterMapper.xml中添加对应的SQL查询语句
- 移除BaseContextHandler引入,优化代码依赖关系
2025-11-19 15:08:39 +08:00
778afaaa83 feat(puzzle): 实现拼图自动填充规则引擎及相关功能
- 新增拼图填充规则管理Controller、DTO、Entity等核心类
- 实现条件评估策略模式,支持多种匹配规则
- 实现数据源解析策略模式,支持多种数据来源
- 新增拼图元素自动填充引擎,支持优先级匹配和动态填充
- 在SourceMapper中增加设备统计和查询相关方法
- 在PuzzleGenerateRequest中新增faceId字段用于触发自动填充
- 完善相关枚举类和工具类,提升系统可维护性和扩展性
2025-11-19 11:10:23 +08:00
de421cf0d5 chore(build): 移除跳过测试编译的 Maven 插件配置
- 删除了 maven-compiler-plugin 中跳过测试编译的配置
- 移除了对测试文件排除的设置
- 清理了插件中不必要的 Java 21 预览功能启用参数
2025-11-19 10:16:24 +08:00
3ddf7bd0e9 feat(image): 添加图片180度旋转功能
- 新增rotateImage180方法实现图片180度旋转
- 支持源文件读取和目标文件写入
- 使用AffineTransform实现图像旋转变换
- 保持图片原始尺寸不变
- 添加详细的异常处理和资源释放
- 移除对270度旋转的限制检查
2025-11-18 17:32:04 +08:00
208202ba41 feat(image): 添加水印四边偏移支持
- 在 WatermarkInfo 中新增 offsetTop、offsetBottom、offsetLeft 和 offsetRight 字段
- 在 PrinterDefaultWatermarkOperator 中实现四边偏移逻辑,默认值为 0
- 根据图片方向设置不同的偏移值,横图左偏移 40 像素,竖图下偏移 30 像素
- 调整二维码和文字位置计算方式以应用偏移量
- 优化水印处理流程,确保偏移参数正确传递和使用
2025-11-18 16:27:19 +08:00
6e84a5fd43 fix(printer): 调整二维码边距和图片旋转逻辑
- 修改二维码距离左边缘的图片宽度比例从 0.075 为 0.05
- 修正图片旋转角度判断逻辑,确保横向处理正确
- 移除下载 URL 中的域名替换操作,使用原始地址直接下载
2025-11-18 16:06:19 +08:00
8e48bd92cc feat(pricing): 添加新的产品类型枚举值
- 新增 PHOTO_LOG 类型,表示 pLog 图
- 新增 PHOTO_VLOG 类型,表示 pLog 视频
2025-11-18 15:49:41 +08:00
23181e9f08 fix(video): 完善视频删除逻辑以排除被评价的视频
- 修改删除条件,增加对视频评价关联表的检查
- 确保已被评价的视频不会被误删
- 防止因外键约束导致的删除失败问题
2025-11-18 14:42:20 +08:00
42e806df76 feat(puzzle): 添加批量替换模板元素功能
- 在 PuzzleTemplateController 中新增 replaceElements 接口
- 在 PuzzleElementMapper 中新增 getByTemplateIdAndKey 查询方法
- 在 PuzzleTemplateServiceImpl 中实现 replaceElements 业务逻辑
- 在 IPuzzleTemplateService 接口中定义 replaceElements 方法
- 在 PuzzleElementMapper.xml 中添加对应 SQL 查询语句
2025-11-18 12:47:24 +08:00
a49e581915 fix(printer): 修复打印照片方向检测逻辑
- 修改图片方向判断方式,从文件检测改为读取crop配置中的rotation值
- 添加异常处理机制,确保旋转角度解析失败时能正确抛出异常
- 保持竖图自动旋转为横图的处理逻辑不变
2025-11-18 12:27:18 +08:00
af60e95529 feat(puzzle): 添加模板分页查询功能并优化DTO序列化
- 在PuzzleTemplateController中新增pageTemplates接口支持分页查询
- 为ElementCreateRequest和PuzzleElementDTO添加@JsonProperty注解优化JSON序列化
- 实现PuzzleTemplateServiceImpl中的pageTemplates分页逻辑
- 使用PageHelper实现分页查询并限制最大页面大小为100
- 在IPuzzleTemplateService接口中定义pageTemplates方法签名及文档说明
- 添加参数校验确保page和pageSize的有效性
- 返回PageResponse对象封装分页结果供前端使用
2025-11-18 12:05:21 +08:00
bb2367c5a6 feat(video-review): 支持机位多维度评价功能
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 新增NestedMapTypeHandler处理嵌套Map与JSON互转
- 修改VideoReview相关实体类和DTO以支持嵌套Map结构
- 更新数据库查询逻辑以适配新的评价数据结构
- 优化平均分计算方法以处理多机位多维度评分
- 完善MyBatis配置中的typeHandler引用
- 补充视频查询接口返回任务开始结束时间字段
- 修正SQL关联查询条件确保数据准确性
2025-11-18 10:14:42 +08:00
3d361200b0 refactor(puzzle): 重构元素DTO及新增元素基类
- 将ElementCreateRequest和PuzzleElementDTO中的elementType从Integer改为String
- 删除所有类型特定字段,新增config和configMap支持JSON配置
- 新增BaseElement抽象基类定义元素通用行为
- 添加ElementConfig接口和具体实现类ImageConfig、TextConfig
- 创建ElementFactory工厂类和ElementRegistrar注册器
- 新增ElementType枚举和ElementValidationException异常类
- 实现ImageElement和TextElement具体元素类
- 添加Position位置信息封装类
2025-11-18 08:13:38 +08:00
5c49a5af9e refactor(video): 重构视频审核服务异常处理
- 将 javax.servlet.http.HttpServletResponse 替换为 jakarta.servlet.http.HttpServletResponse
- 使用 BaseException 替换 BizException 处理业务异常
- 修改视频查询方法 selectById 为 getEntity
- 统一参数校验和用户登录状态检查的异常抛出方式
2025-11-18 00:54:11 +08:00
a5ffb86790 feat(pom): 添加Apache POI依赖以支持Excel文件处理
- 添加poi依赖版本5.4.0
- 添加poi-ooxml依赖版本5.4.0
- 支持Excel文件的读写操作
- 为后续Excel导出功能做准备
- 统一管理POI相关依赖版本
- 更新项目依赖结构
2025-11-18 00:53:55 +08:00
755ba1153e feat(video): 新增视频评价功能及购买状态查询
- 移除TaskController上的@Deprecated注解
- 在VideoController中新增/checkBuyStatus接口用于查询视频购买状态
- 新增VideoReviewController控制器,提供评价管理功能
- 新增MapTypeHandler用于处理Map类型与JSON字段的转换
- 在VideoMapper中增加countBuyRecordByVideoId方法查询视频购买记录
- 新增视频评价相关实体类、DTO及Mapper接口
- 实现VideoReviewService服务类,支持评价新增、分页查询、统计分析和Excel导出
- 在VideoServiceImpl中实现checkVideoBuyStatus方法
- 修改VideoMapper.xml,关联task表并查询task_params字段
- 新增VideoReviewMapper.xml配置文件,实现评价相关SQL查询
2025-11-17 23:37:04 +08:00
ebf05ab189 fix(pricing): 修复优惠券计算逻辑,防止优惠金额溢出
- 重构优惠券折扣计算方法,确保固定金额优惠券不超过适用商品总价
- 修改百分比优惠券计算逻辑,基于适用商品总价而非购物车总价
- 新增适用商品总价计算方法,支持按商品类型过滤
- 添加防止优惠金额超出适用商品总价的保护逻辑
- 完善无商品类型限制时的全商品适用逻辑
- 增加多种边界情况和多SKU场景的单元测试
- 修复百分比优惠券最大折扣限制的计算顺序问题
2025-11-17 17:38:46 +08:00
e2b450682b feat(puzzle): 优化拼图生成逻辑并新增完整测试套件
- 在PuzzleGenerateServiceImpl中改进图片上传逻辑,支持contentType指定
- 在PuzzleImageRenderer中优化背景图片缩放算法,使用原生Java方法提升性能
- 修改scaleImage方法实现,完善多种图片适配模式(COVER、CONTAIN、FILL等)
- 新增PuzzleRealScenarioIntegrationTest集成测试类,覆盖真实业务场景
- 添加PuzzleTemplateServiceImplTest单元测试,使用Mockito模拟数据库交互
- 创建MockImageUtil工具类,支持测试过程中生成各类模拟图片
- 构建PuzzleTestDataBuilder测试数据构造器,简化测试模板和元素创建
- 增加RealScenarioTestHelper辅助类,提升测试代码复用性
-
2025-11-17 16:50:53 +08:00
443f92ff92 feat(puzzle): 实现拼图生成功能模块
- 新增拼图生成控制器 PuzzleGenerateController,支持 /api/puzzle/generate 接口
- 新增拼图模板管理控制器 PuzzleTemplateController,提供完整的 CRUD 和元素管理功能
- 定义拼图相关 DTO 类,包括模板、元素、生成请求与响应等数据传输对象
- 创建拼图相关的实体类 PuzzleTemplateEntity、PuzzleElementEntity 和 PuzzleGenerationRecordEntity
- 实现 Mapper 接口用于数据库操作,支持模板和元素的增删改查及生成记录管理
- 开发 PuzzleGenerateServiceImpl 服务,完成从模板渲染到图片上传的完整流程
- 提供 PuzzleTemplateServiceImpl 服务,实现模板及其元素的全生命周期管理
- 引入 PuzzleImageRenderer 工具类负责图像合成渲染逻辑
- 支持将生成结果上传至 OSS 并记录生成过程的日志和元数据
2025-11-17 12:54:56 +08:00
630d344b5a refactor(order): 移除冗余的用户ID参数
- 删除了 OrderBiz 中 listImageByFaceRelation 方法调用时多余的 memberId 参数
- 更新了 SourceRepository 中 listVideoByFaceRelation 和 listImageByFaceRelation
  方法的参数列表,移除了不再需要的 userId 参数
- 简化了 AppScenicServiceImpl 中 ScenicAppVO 对象的属性设置逻辑,去除了部分字段赋值
2025-11-17 11:12:08 +08:00
9eb3fd3e58 refactor(controller): 调整 RenderWorkerWithStatusDTO 包路径
- 将 RenderWorkerWithStatusDTO 从 controller.dto 包移动到 dto 包
- 更新了相关的 import 路径引用
- 保持类的功能和接口不变
2025-11-17 11:07:02 +08:00
3463dcc9ae Merge branch 'remove_face_member_limit' 2025-11-17 10:42:53 +08:00
1b9bebf7e4 fix(facebody): 优化人脸搜索失败时的重试机制
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 提取error_code到局部变量避免重复调用
- 增加对错误码22204的处理逻辑,当URL图片无法访问时尝试下载并转换为base64后重试
- 添加重试成功后的完整响应处理流程
- 记录详细的重试过程日志便于问题排查
- 保留原始错误响应的日志记录
- 确保所有异常路径都返回有效的响应对象
2025-11-17 10:31:52 +08:00
fa287f36ae Revert "feat(facebody): 实现人脸识别搜索的重试机制"
This reverts commit 7c42c5c462.
2025-11-17 10:27:38 +08:00
0c3ada97f9 feat(facebody): 添加图片下载及base64重试机制
- 当添加人脸因无法访问URL图片失败时,自动下载图片并转换为base64后重试
- 新增downloadImageAsBase64方法用于图片下载与编码
- 替换阿里云OSS域名以支持内网访问
- 增加对png和jpg格式图片的支持
- 使用hutool库进行base64编码
- 添加详细的日志记录以便追踪重试过程
2025-11-17 10:21:10 +08:00
a7a7e30364 Merge branch 'coupon_user_limit' 2025-11-17 10:12:52 +08:00
17a33ada9f fix(printer): 修复打印机更新逻辑以使用传入的实体数据
- 在更新方法中先获取现有打印机实体
- 检查景区是否启用打印功能
- 使用传入的payload实体进行更新操作
- 保持原有的成功或失败响应结构
2025-11-17 10:12:43 +08:00
d408c47963 feat(mobile): 增强人脸数据访问控制并优化订单查询逻辑
- 在删除人脸数据前增加用户权限校验,确保操作安全
- 移除订单详情接口中的用户身份强制绑定,简化查询流程
- 更新视频与图片资源查询方法,去除冗余的用户ID参数
- 调整Mapper层SQL语句,解耦人脸关联数据对用户的依赖
- 优化服务层代码结构,提升数据获取效率与一致性
2025-11-17 10:06:32 +08:00
5d5643e7d7 feat(pricing): 新增照片打印SKU及价格计算逻辑
- 添加 PHOTO_PRINT_MU 和 PHOTO_PRINT_FX 枚举类型定义
- 实现手机照片打印和特效照片打印的基础价格计算(单价×数量)
- 支持景区特定配置的价格计算逻辑
- 验证新SKU与现有 PHOTO_PRINT 的行为一致性
- 添加相关单元测试确保价格计算准确性
2025-11-17 08:53:08 +08:00
8efd16ba56 fix(coupon): 优化事务回滚标记逻辑
- 添加对无事务上下文情况的处理
- 避免在非事务环境下抛出异常
- 提高优惠券领取失败时的系统稳定性
2025-11-17 08:52:00 +08:00
9d708ae20c fix(voucher): 移除人脸验证中的用户ID检查
- 删除了人脸验证中不必要的用户ID匹配检查
- 简化了自动领取凭证的逻辑流程
- 保留了景区ID的设置以确保业务连续性
2025-11-17 01:02:31 +08:00
7c906d5529 feat(pricing): 优化优惠券领取逻辑与并发控制
- 为 CouponInvalidException 添加错误码支持
- 在 countUserCouponClaims 查询中添加 FOR UPDATE 锁
- 新增 incrementClaimedQuantityIfAvailable 方法用于原子性增加已领取数量
- 移除重复的用户优惠券领取检查逻辑
- 调整领取流程步骤编号并优化事务回滚处理
- 增加对优惠券库存耗尽情况的业务异常处理
- 使用
2025-11-17 00:53:48 +08:00
88ad6d6b6f fix(pricing): 修复优惠券库存检查逻辑
- 修正totalQuantity为NULL或0时不限制总量的判断逻辑
- 优化claimedQuantity为空时的默认值处理
- 仅在totalQuantity大于0时更新已领取数量
- 完善MD文档中字段语义描述和配置示例
- 更新SQL表字段说明及典型配置组合示例
2025-11-17 00:30:58 +08:00
7835283f0f feat(pricing): 添加优惠券用户领取数量限制功能
- 新增用户领取数量限制字段 userClaimLimit
- 区分已领取数量 claimedQuantity 和已使用数量 usedQuantity
- 添加用户领取次数统计方法 countUserCouponClaims
- 实现领取上限检查逻辑和错误码 CLAIM_LIMIT_REACHED
- 更新数据库表结构和索引优化建议
- 完善文档说明和版本更新记录
2025-11-17 00:26:15 +08:00
9ee466bd5e fix(pricing): 移除自动发券中不必要的faceId校验
- 删除了对faceId参数的非空校验逻辑
- 保持其他参数校验不变
- 更新了日志记录中的参数列表
2025-11-16 13:17:53 +08:00
1a25848102 fix(pricing): 调整用户优惠券查询逻辑
- 修改查询条件,移除状态限制,支持查询所有状态的优惠券记录
- 更新注释说明,明确查询目的为检查用户是否领取过指定优惠券
2025-11-15 16:29:05 +08:00
c319398c58 feat(printer): 添加预览模式和订单ID支持
- 在价格计算请求中添加 previewOnly 参数以支持仅查询价格
- 为订单价格计算设置 orderId 参数
- 完善价格计算服务调用逻辑
2025-11-15 15:57:29 +08:00
afc589bb39 feat(order): 添加价格预览模式支持
- 在VLOG商品价格计算中添加previewOnly标志
- 在普通商品价格计算中添加previewOnly标志
- 确保价格查询时不实际使用优惠券
- 支持仅查询最终价格和原始价格的功能
2025-11-15 15:55:15 +08:00
4ccb563557 feat(pricing): 调整价格计算请求参数并完善优惠券使用逻辑
- 将 previewOnly 默认值从 false 改为 true
- 新增 orderId 字段用于实际使用优惠时的订单标识
- 实现优惠券使用标记逻辑,调用 couponService.useCoupon
- 添加优惠券使用日志记录,包括 couponId、userId 和 orderId
- 补充 scenicId 参数转换处理逻辑
2025-11-15 15:54:13 +08:00
11face7935 fix(printer): 修复支付价格比较逻辑和数据返回问题
- 将支付价格比较方式从 equals 修改为 compareTo 方法以确保精度正确性
- 调整 orderId 返回逻辑的位置,确保在所有情况下都能正确返回
2025-11-15 15:53:47 +08:00
515f68a6f4 feat(pricing): 移除已弃用的优惠券使用接口
- 删除 /coupons/use 接口的实现代码
- 清理相关注释和日志记录代码
- 保留查询用户可用优惠券接口不变
2025-11-15 15:53:23 +08:00
3f396b4cb8 Merge branch 'print_sku' 2025-11-15 15:30:57 +08:00
a1b0687526 feat(pricing): 添加折扣优先级设置并优化价格计算请求
- 在折扣信息中增加优先级字段,提升折扣策略的灵活性
- 为价格计算请求默认启用自动使用优惠券功能
- 设置价格预览模式为非预览状态,确保实际计算准确性
- 完善打印服务中的价格计算逻辑,提高系统稳定性
2025-11-15 14:52:53 +08:00
932081abf0 refactor(pricing): 重构自动发券服务方法命名及逻辑
- 将 autoGrantFirstPrintCoupon 方法重命名为 autoGrantCoupon
- 修改 findFirstPrintCouponId 方法名为 findFirstCouponId
- 调整优惠券名称匹配逻辑,移除对"first"关键字的检查
- 更新调用方 PrinterServiceImpl 中的方法引用
- 优化自动发券异常处理,确保不影响主流程
2025-11-15 14:28:56 +08:00
6462037dcd fix(printer): 优化人脸识别逻辑避免重复添加照片
- 在人脸匹配成功后,仅当不存在已匹配的人脸时才自动添加照片到预打印列表
- 更新了用户照片列表的获取条件,确保只在必要时执行此操作
- 保留了对源实体存在的检查,以维持原有业务流程的完整性
2025-11-15 14:11:35 +08:00
9b9e69cf52 feat(printer): add print task review and reprint functionality
- Add reprint endpoint with printer name update
- Implement pending review task query and management
- Add task URL update for pending review tasks
- Support bulk approve/reject of pending tasks
- Extend task status enum with review-related states
- Create ReprintRequest DTO for printer information
- Update mapper to handle status transitions and queries
- Modify service layer to support review workflow
- Adjust XML mapper for new database operations
2025-11-15 14:05:37 +08:00
6246d6ef46 feat(printer): add pagination to scenic list query
- Set default page number to 1
- Set default page size to 1000
- Enable pagination for scenic list retrieval
2025-11-15 14:04:45 +08:00
19fae4bd00 feat(pricing): 实现首次打印自动发券功能
- 新增自动发券服务接口 IAutoCouponService- 实现自动发券逻辑,包括参数校验、优惠券配置查询和发券记录检查
- 在打印服务中集成自动发券调用,确保首次打印时触发发券- 添加异常处理,避免发券失败影响主流程
- 支持通过优惠券名称和商品类型匹配规则查找目标优惠券
2025-11-14 09:10:06 +08:00
661aa4567f feat(print): 支持多种照片打印类型的价格计算
- 新增手机照片打印(PHOTO_PRINT_MU)和特效照片打印(PHOTO_PRINT_FX)枚举类型
- 在价格计算服务中增加isPrintProduct方法统一判断打印类商品
- 修改订单服务跳过打印类商品重复购买检查逻辑
-优化打印机服务根据sourceId分类统计不同照片类型数量
- 分别计算普通、手机、特效照片打印的价格和数量- 更新价格计算逻辑以支持多种打印类型商品项
2025-11-14 01:08:12 +08:00
ec34437e9d feat(scenic): 添加打印功能开关配置
- 在AppScenicController中新增printEnable字段返回
- 在ScenicConfigResp中添加printEnable属性
- 默认关闭打印功能,可通过配置开启
2025-11-11 17:02:14 +08:00
dde9f5d542 fix(printer): 更新二维码链接参数并优化水印配置读取
- 在生成微信小程序二维码的链接中添加 force=1 参数
- 从景区配置中读取水印景区文本,为空时使用空字符串
-从景区配置中读取日期格式,为空时使用默认格式 yyyy.MM.dd
2025-11-11 15:36:52 +08:00
72e60c95e0 fix(printer):优化水印处理逻辑并增强人脸匹配容错
- 限制水印处理仅在sourceId不为空时执行
- 调整水印处理代码结构,提高可读性
- 增加人脸匹配异常捕获,避免影响主流程
-保持原有打印任务创建逻辑不变
2025-11-09 23:16:09 +08:00
9df30a84e0 1 2025-11-08 18:01:52 +08:00
acfaebfffa fix(printer):修复打印机选择逻辑并优化查询
- 调整打印机选择逻辑,确保正确获取打印机ID
- 优化SourceMapper查询,按创建时间倒序并限制结果数量- 修复可能因逻辑错误导致的打印机选择异常问题
2025-11-08 17:38:44 +08:00
72e215c552 feat(printer): 添加sourceId字段并优化打印逻辑
- 在MemberPrintResp中新增sourceId字段
- 优化PrinterServiceImpl中的水印处理逻辑
- 添加sourceId为空时的返回判断,避免空指针异常
2025-11-08 17:09:10 +08:00
ee5cc81864 fix(printer): 修改二维码生成路径
- 将视频合成页面路径更改为打印机样本页面路径
- 确保二维码指向正确的页面处理逻辑
2025-11-08 17:07:17 +08:00
1bbfe8d092 feat(printer): 添加照片来源ID字段并更新相关逻辑
- 在MemberPrintEntity中新增sourceId字段用于记录照片来源
- 更新addUserPhoto方法签名,增加sourceId参数
- 修改照片上传接口,支持传递sourceId参数
- 完善自动添加照片到打印列表的逻辑,关联sourceId
- 更新数据库映射文件,添加source_id字段的读写配置- 优化重复照片检测逻辑,确保数据一致性
2025-11-08 15:12:15 +08:00
88c31d4fdc feat(printer):优化人脸样本使用逻辑并增强景区列表查询
- 修改 useSample 接口返回类型为 FaceRecognizeResp
- 增加根据样本ID和类型查询来源实体的逻辑
- 在景区列表查询中添加参数校验和异常处理
- 完善景区信息处理流程,增加设备数量统计
-优化景区距离计算与筛选逻辑
- 增加人脸匹配后自动添加照片到用户相册的功能
- 添加 XML 映射文件中新的查询语句实现
2025-11-08 15:04:50 +08:00
fb75cbf230 feat(printer):优化订单打印纸张尺寸获取逻辑
- 引入Redis缓存存储打印纸张尺寸信息
- 优先从Redis中获取纸张尺寸,减少数据库查询
- 在创建订单时将打印机偏好纸张存入Redis,有效期60秒
- 修复打印机对象作用域问题,避免空指针异常
- 统一打印机状态校验逻辑,提高代码可读性
2025-11-08 11:04:22 +08:00
eda4ed2955 1 2025-11-08 10:35:43 +08:00
c41611e5d0 feat(printer): 实现人脸照片自动添加到优先打印列表功能
- 引入DeviceConfigManager和SourceEntity依赖
- 替换TaskFaceService为FaceService并注入DeviceRepository
- 新增autoAddPhotosToPreferPrint方法实现自动添加逻辑
- 根据景区和设备配置筛选并添加符合条件的照片
- 支持按设备分组处理和优先打印数量控制
- 添加详细的日志记录和异常处理机制
2025-11-08 10:20:37 +08:00
747081901f feat(printer): 实现人脸样本使用功能
- 新增人脸样本使用逻辑,支持查询或创建face记录
- 集成人脸识别相关实体和映射器
- 添加人脸样本URL校验和重复检测机制
- 返回包含人脸URL、ID及景区ID的响应对象
- 引入雪花算法生成唯一face ID
- 记录创建新face或使用已有face的日志信息
2025-11-08 01:17:39 +08:00
616ab217e4 feat(printer): 新增打印机大屏对接接口
- 添加获取景区列表接口
- 实现根据景区ID查询设备列表功能
- 支持生成并返回二维码图片- 集成设备、景区和人脸样本数据查询- 提供微信小程序二维码生成功能
- 实现文件流输出与临时文件清理机制
2025-11-08 00:47:33 +08:00
00db16e7bd feat(watermark): 调整水印默认字体及位置参数
- 更改默认字体文件路径为 "/PingFang_SC_t.ttf"
-修正字体资源加载类为 PrinterDefaultWatermarkOperator
- 调整垂直偏移量 OFFSET_Y 从20 到15
- 微调二维码左边距比例从0.07 到 0.075
- 修改二维码垂直偏移量 QRCODE_OFFSET_Y从 -20 到 -35
- 设置景区名称和时间字体样式为粗体 (BOLD)
2025-11-07 23:01:58 +08:00
1821ba9f58 feat(image): 添加打印机默认水印操作器并优化图片处理逻辑- 新增 PrinterDefaultWatermarkOperator 实现自定义水印处理
- 在 ImageWatermarkOperatorEnum 中添加 PRINTER_DEFAULT 类型
- 更新 ImageWatermarkFactory 以支持新的水印操作器
- 调整日期格式为 yyyy.MM.dd 用于打印场景
-优化 ImageUtils 中的图片旋转逻辑,仅支持270度旋转
- 移除对90度旋转的支持以简化处理流程
2025-11-07 22:38:02 +08:00
ea48f03bbc fix(order):修复订单打印信息获取逻辑
- 当 printTaskResp 为空时,从 printerMapper 获取打印机信息
- 设置商品的打印机名称和纸张类型字段
- 确保即使任务响应为空也能正确显示打印机数据
2025-11-07 22:02:16 +08:00
6b2089a9bc feat(order): 添加商品数量字段并初始化
- 在 OrderItemVO 中新增 count 字段用于记录商品数量- 根据不同商品类型设置默认数量为1
- 针对打印订单类型从照片信息中获取实际数量进行赋值
- 完善订单项数据封装逻辑以支持数量统计需求
2025-11-07 21:58:19 +08:00
25c0e629c6 feat(image): 添加二维码圆形白色背景
- 创建比二维码大10像素的白色圆形背景
- 绘制白色圆形背景以突出显示二维码
- 裁剪二维码为圆形并绘制在白色背景上
2025-11-07 21:47:30 +08:00
48c8518ae6 feat(printer):优化打印任务逻辑并添加水印景区文案
- 根据订单项循环设置用户购买状态
- 添加水印信息中的景区文案配置
- 调整打印任务创建流程顺序
2025-11-07 21:08:33 +08:00
96d001dfc0 feat(printer): 新增样品使用功能并优化打印流程
- 在AppPrinterController中新增/useSample接口用于使用样品- PrinterServiceImpl中实现生成微信小程序二维码逻辑
- 更新setUserIsBuyItem方法签名以支持异常抛出- 添加useSample方法占位实现
- 引入相关依赖类如MpConfigEntity、OrderRepository和WxMpUtil
2025-11-07 20:43:15 +08:00
fb3a08fdcf feat(printer): 实现照片自动裁剪功能
- 添加打印尺寸获取逻辑,优先从打印机配置读取
- 实现默认尺寸 fallback 机制 (1020x1520)
- 集成 smartCropAndFill 图片裁剪算法
- 添加裁剪后图片上传和临时文件清理
- 增加异常处理,裁剪失败时回退到原图
-优化图片 URL 内部访问地址配置
2025-11-06 15:32:11 +08:00
dcc8cdeb6a fix(printer): 修改 faceId 参数类型为字符串并增加解析逻辑
- 将 getListFor、upload、uploadFromSource、queryPrice、createOrder 等接口中的 faceId 参数类型从 Long 改为 String
- 新增 parseFaceId 方法用于将字符串类型的 faceId 安全转换为 Long 类型
- 在所有涉及 faceId 的业务方法调用处使用 parseFaceId 进行参数传递
- 增加对无效 faceId 字符串的处理,避免 NumberFormatException 异常
- 保持原有功能不变,提升接口健壮性和兼容性
2025-11-05 17:00:26 +08:00
054958ebf5 fix(printer):修复用户照片列表查询逻辑
- 修正了当faceId为null时的过滤条件判断
- 调整了listRelation方法的过滤逻辑,确保正确返回无faceId关联的数据
-保证了getUserPhotoList接口在不同参数下的正确性
2025-11-05 15:26:09 +08:00
fa0c3a1a43 feat(printer): 支持按数量创建多个打印任务- 根据照片数量生成对应数量的打印任务- 默认至少创建1个打印任务
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 保留原有单任务逻辑并适配多任务场景
- 确保每个任务独立插入数据库
- 维持打印机轮询选择机制
2025-11-05 14:45:55 +08:00
779334a09e 1 2025-11-05 13:05:43 +08:00
3a3bdee296 feat(printer): 添加 faceId 参数支持照片打印功能
- 在多个接口中新增 faceId 请求参数,用于关联人脸识别信息
- 修改 getUserPhotoList 方法支持按 faceId 过滤照片列表
- 更新 addUserPhoto 和 addUserPhotoFromSource 方法保存 faceId信息
- 调整 queryPrice 和 createOrder 方法支持 faceId 查询条件- 新增 listRelationByFaceId Mapper 方法实现按 faceId 查询照片
- 在 MemberPrintEntity 和 MemberPrintResp 中添加 faceId 字段- 更新数据库插入语句,添加 face_id 字段写入支持
2025-11-05 11:38:04 +08:00
546ddfbb62 test(facebody): 添加删除人脸库的测试方法
- 新增测试方法 testDeleteDb 验证删除人脸库功能
- 调用 deleteFaceDb 方法测试删除指定人脸库
- 使用固定ID "test" 进行删除测试
2025-11-05 10:00:46 +08:00
58b642f356 feat(device): 更新景区设备查询接口
- 删除冗余的景区IPC设备和激活设备查询接口
- 合并设备查询逻辑到统一接口 /scenic/{scenicId}
- 新增设备名称、类型、编号作为可选查询参数
- 简化控制器代码结构,提升维护性
-保留分页功能支持大规模数据展示
- 统一异常处理流程增强系统健壮性
2025-11-05 10:00:28 +08:00
fa8a8ed711 feat(face):人脸上传接口增加scene参数
- 在AioDeviceController中调用faceUpload时添加空字符串scene参数
- 在LyCompatibleController中调用faceUpload时添加空字符串scene参数- 在AppFaceController中增加scene请求参数并传递给faceService
- 修改FaceService接口和实现类faceUpload方法签名,添加scene参数- 移除多个控制器和服务中未使用的导入依赖
- 调整代码格式以符合规范
2025-11-04 14:26:00 +08:00
6886f87fe9 Merge branch 'face_service_refactor'
# Conflicts:
#	src/main/java/com/ycwl/basic/service/pc/impl/FaceServiceImpl.java
2025-11-02 22:06:57 +08:00
78a2a74fa6 feat(print): 实现照片自动裁剪与优先打印功能
- 人脸上传后自动将关联照片添加到优先打印列表
- 根据景区和设备配置自动处理type=2的照片
- 支持按设备分组处理并限制打印数量
- 实现智能图片裁剪功能,支持自动旋转以减少裁切损失
- 添加图片尺寸配置读取和默认值处理
- 完善异常处理确保不影响主流程执行
-优化打印服务中照片上传和裁剪逻辑
- 增加详细的日志记录便于问题追踪
2025-11-02 09:13:10 +08:00
222f974ad5 feat(face): 添加人脸识别重试功能并优化得分筛选逻辑
- 在FaceSampleController中新增retryFaceRecognition接口用于手动重试失败的人脸识别任务- 集成人脸识别Kafka服务,支持异步处理重试请求- 在FaceServiceImpl中增加从景区配置读取人脸得分阈值的功能
- 根据配置的得分阈值对人脸识别结果进行筛选,过滤低分样本
- 添加详细的日志记录和异常处理机制- 优化线程池使用,确保重试任务能够正确提交和执行
2025-11-01 20:12:25 +08:00
96a4d3ffeb feat(storage): 更新照片存储路径常量
- 将 PHOTO_PATH 常量值从 "source_photo" 更改为 "viid"
- 保持其他存储路径常量不变- 确保与现有存储结构兼容
2025-11-01 19:56:27 +08:00
e99d75ba1b feat(app): 添加场景模式配置字段
- 在AppScenicController中新增scene_mode配置项
- 默认值设置为0
- 支持从scenicConfig获取场景模式配置
2025-11-01 19:55:27 +08:00
c1b9a42c73 3 2025-10-31 21:04:10 +08:00
4c10c1d939 2 2025-10-31 18:44:43 +08:00
3000e18cb7 refactor(face):重构人脸识别服务逻辑
- 将人脸识别补救逻辑提取到FaceRecoveryStrategy类中
- 将源文件关联处理逻辑提取到SourceRelationProcessor类中
- 将购买状态处理逻辑提取到BuyStatusProcessor类中
- 将视频重切处理逻辑提取到VideoRecreationHandler类中
- 在FaceServiceImpl中引入四个新的处理器组件
- 删除原有的冗长方法实现,改为调用对应处理器
- 更新方法调用方式以使用新的处理器实例
- 保留核心业务流程但解耦具体实现细节
2025-10-31 17:31:48 +08:00
bf014db7ff feat(face): 引入人脸识别指标记录与搜索结果合并功能
- 新增 FaceMetricsRecorder 类用于记录人脸识别、自定义匹配及低阈值检测次数
- 新增 SearchResultMerger 类用于合并多个人脸搜索结果,支持并集与交集模式- 在 FaceServiceImpl 中引入 metricsRecorder 和 resultMerger 辅助类
- 替换原有的 Redis 操作代码为 FaceMetricsRecorder 的方法调用- 将搜索结果合并逻辑从 FaceServiceImpl 提取至 SearchResultMerger- 新增策略模式相关类:RematchContext、RematchModeStrategy 接口及四种实现
- 使用策略工厂 Rematch
2025-10-31 17:11:02 +08:00
12cd9bd275 1 2025-10-31 16:41:15 +08:00
7c42c5c462 feat(facebody): 实现人脸识别搜索的重试机制
- 添加可重试和不可重试异常分类
- 集成百度云错误码分类器
- 实现搜索人脸接口的自动重试逻辑
- 支持根据错误码动态调整重试次数和延迟
- 添加详细的异常日志记录
- 保持与原有逻辑一致的空结果返回行为
2025-10-31 15:01:06 +08:00
631d5c175f feat(payment): 支付与退款后清除景区统计缓存
- 在支付成功、取消、退款回调后增加缓存删除逻辑
- 新增 `invalidateStatisticsCache` 方法用于删除 Redis 缓存
- 定时任务中统计景区数据后也调用缓存清除方法
- 调整景区统计任务时间并扩展统计周期为近7天
- 增强定时任务日志记录和异常处理机制
2025-10-31 13:46:17 +08:00
785de52780 feat(scenic): 添加打印相关配置项
- 新增智能抓拍打印开关配置
- 新增打印必须录入人脸开关配置
- 新增手机照片打印开关配置
- 在AppScenicController中设置打印相关配置项
- 在ScenicConfigResp中添加打印配置字段
2025-10-31 13:03:44 +08:00
2ee7e93201 refactor(order):优化订单业务逻辑中的景区信息获取方式
- 移除了对 ScenicEntity 的直接依赖
- 使用 scenicId 替代 scenic 对象以减少数据库查询
- 更新了 VLOG 和普通商品的价格计算逻辑
- 在下载通知任务中使用 ScenicV2DTO 替代 ScenicEntity
- 统一通过 scenicRepository 获取景区基本信息的方法调用
2025-10-31 11:29:48 +08:00
65ae23a956 refactor(scenic): 替换ScenicEntity为ScenicV2DTO以优化数据访问
- 将DeviceStatsServiceImpl中的ScenicEntity替换为ScenicV2DTO
- 将FaceSampleServiceImpl中的ScenicEntity替换为ScenicV2DTO
- 将TaskTaskServiceImpl中的ScenicEntity替换为ScenicV2DTO
- 更新相关方法调用以使用新的DTO结构
- 确保景点名称获取逻辑保持一致
-优化数据访问层以提高性能和可维护性
2025-10-31 11:12:38 +08:00
b9ade63e8e feat(wx): 移除微信消息模板通知控制器
- 删除了 AppWxNotifyController 类及相关接口实现
- 移除了 /api/mobile/wx/notify/v1 路径下的所有端点
- 清理了相关依赖注入和业务逻辑代码
2025-10-31 11:05:01 +08:00
cdeb2e4d5a refactor(statistics): 移除统计记录添加功能
- 删除 AppStatisticsController 中的 addStatistics 接口
- 移除 AppStatisticsServiceImpl 中 addStatistics 方法的实现
- 从 AppStatisticsService 接口中移除 addStatistics 方法声明- 清理 FaceServiceImpl 中调用统计记录添加的代码逻辑
2025-10-31 11:00:38 +08:00
cc38d6e095 feat(printer): 添加打印功能开关检查
- 在新增打印机接口中增加景区打印功能开关检查
- 在更新打印机接口中增加景区打印功能开关检查
- 打印功能未开启时返回失败响应及提示信息
2025-10-31 11:00:06 +08:00
82d86c8c3c fix(face):修复人脸匹配样本ID处理逻辑
- 移除旧数据合并逻辑,直接使用上传的样本ID列表
- 使用stream过滤和去重处理样本ID
- 简化样本列表变更检测逻辑
- 移除不必要的LinkedHashSet转换
- 优化最终样本列表的生成方式
2025-10-30 11:43:02 +08:00
5979b1a275 refactor(face): 调整人脸关系数据删除逻辑
- 将删除人脸旧关系数据的逻辑从匹配前移至保存新关系前- 确保在更新人脸关系时正确清理旧数据- 避免重复删除操作,优化数据处理流程
2025-10-30 10:40:33 +08:00
a7fe0d715d feat(face): 添加人工调整标记更新功能
- 在 FaceMapper 接口中新增 updateManualFlag 方法
- 实现根据 ID 更新 is_manual 字段的 SQL 语句
- 优化 FaceServiceImpl 中设置人工调整标记的逻辑
- 使用专门的更新方法替代原有的通用更新方式
- 清理相关缓存以确保数据一致性
2025-10-30 10:16:24 +08:00
ef8a913636 fix(face):修复人脸样本直接使用模式下搜索结果未设置的问题
- 在模式2下直接使用用户选择的人脸样本时,补充设置搜索结果JSON
- 保证检索结果在所有模式下都能正确返回
- 避免前端因缺少搜索结果数据而出现异常
2025-10-30 09:54:29 +08:00
73791a92d3 feat(face):重新匹配前删除人脸旧关系数据
- 在重新匹配前删除该人脸的旧数据关系
- 清理 member_source 和 member_video 中的关联记录
- 更新缓存清理逻辑以确保数据一致性
- 添加详细的日志记录以便追踪操作过程
2025-10-30 09:47:29 +08:00
f0ad0f58a9 fix(order):修复订单备注更新逻辑
- 移除了订单备注更新时的空字符串检查
- 允许将订单备注更新为空值
-保持了退款原因的非空检查逻辑不变
2025-10-30 09:24:28 +08:00
73825cd1d6 feat(face): 添加人工调整标记字段并优化匹配逻辑
- 在 FaceEntity 中新增 isManual 字段,用于标识是否经过人工调整
- 优化人脸识别匹配流程,若已人工调整则跳过自动匹配
- 更新 FaceMapper.xml,支持 isManual 字段的更新操作
- 在处理自定义人脸匹配时,设置人工调整标记并清除缓存
2025-10-30 00:18:03 +08:00
745943fc23 refactor(face): 移除样本筛选轨迹功能及相关枚举
- 删除 FaceRecognitionFilterReason 枚举类
- 移除 SampleFilterTrace 类及其相关逻辑
- 简化样本筛选方法,去除轨迹记录功能- 更新 FaceServiceImpl 和 TaskFaceServiceImpl 中的调用逻辑
- 移除 SearchFaceRespVo 中的 filterTrace 字段- 清理无用的 import语句和相关代码引用
2025-10-29 19:26:35 +08:00
b6bde4ad62 refactor(face):优化人脸识别更新接口及样本展示逻辑
- 修改 updateRecognition 接口返回类型为 void,简化响应内容
- 移除 FaceRecognitionSampleVO 中冗余的字段(sourceType、faceUrl 等)- 删除与过滤原因相关的属性和处理逻辑
- 简化 buildSampleVO 方法参数及内部实现- 调整 resolveSourceUrl 方法中 URL 获取优先级
- 优化样本列表构建逻辑,提升性能与可读性
2025-10-29 15:21:15 +08:00
07ebccad3c fix(video): 减少视频任务生成器的等待时间
- 将线程睡眠时间从5000毫秒减少到2000毫秒
- 提高视频任务处理效率
- 减少系统资源占用
2025-10-28 18:04:35 +08:00
028178605e fix(printer): 修改打印机列表分隔符
- 将打印机列表的分隔符从逗号(,)更改为竖线(|)
- 避免打印机名称中包含逗号导致解析错误
- 更新打印机信息时使用新的分隔符格式
2025-10-28 17:44:09 +08:00
03162dec44 feat(face): 移动人脸识别接口到移动端并优化请求参数
- 将人脸识别相关接口从PC端控制器迁移至移动端控制器
- 更新人脸识别详情和样本VO类的包路径至mobile.face
- 修改人脸识别更新请求参数默认值
- 删除PC端冗余的人脸识别接口实现
- 调整服务层依赖引用至新的mobile.face包路径
- 移除过时的FaceSampleRespVO引用依赖
2025-10-28 17:42:18 +08:00
85cdfe9ea1 feat(printer): 实现打印机轮询选择功能
- 新增 getNextPrinter 方法实现打印机轮询逻辑
- 添加 Redis 键前缀 PRINTER_INDEX_KEY_PREFIX 和过期时间常量
- 在创建打印任务时设置选中的打印机名称- 支持多个打印机按顺序轮流使用
- 使用 Redis 原子递增确保并发安全的索引获取
- 自动为 Redis 键设置 5 分钟过期时间以避免内存泄漏
2025-10-28 17:31:08 +08:00
5e2fe0329d refactor(task):优化设备照片限制筛选逻辑
- 使用LinkedHashMap和LinkedHashSet保持插入顺序
-重构筛选逻辑,提高代码可读性
- 优化设备样本分组处理流程
- 添加筛选原因追踪功能-保持原有筛选规则和日志记录- 提升代码执行效率和内存使用
2025-10-28 16:21:30 +08:00
6f8b3c8cdf chore(template): 删除空的模板工厂类
- 移除了无用的 TemplateFactory 类定义
- 清理了包声明和空的类结构
- 减少了代码库中的冗余文件
2025-10-28 15:52:08 +08:00
1efe4a1439 refactor(task): 移除过时的人脸清理功能
- 删除了 `cleanFaceSampleOss` 方法及相关调用
- 注释说明 VIID 相关功能已移除
-保留并继续使用 `cleanSourceOss` 和 `cleanVideoOss` 方法
2025-10-28 15:51:53 +08:00
e27f092f85 refactor(logging): 将部分info级别日志调整为debug级别- 将Placeholder初始化相关日志从info 2025-10-28 15:51:32 +08:00
215a7e87ae feat(face): 添加景区配置控制人脸任务自动创建
- 新增对景区配置中 face_select_first 参数的检查
- 当 face_select_first为 true 时跳过自动创建任务
- 添加相关日志记录以方便调试和追踪
-保留原有自动创建任务逻辑作为默认行为
2025-10-28 15:41:55 +08:00
636ab96e96 feat(scenic): 添加景区配置人脸优先选择功能
- 在AppScenicController中新增faceSelectFirst字段返回
- 在ScenicConfigResp中增加faceSelectFirst属性默认值为false- 支持景区配置中设置人脸识别优先级开关
2025-10-28 15:41:46 +08:00
cc68a8dbbd Merge branch 'refs/heads/result_edit_2'
# Conflicts:
#	src/main/java/com/ycwl/basic/service/pc/impl/FaceServiceImpl.java
#	src/main/java/com/ycwl/basic/service/task/impl/TaskFaceServiceImpl.java
2025-10-28 15:36:32 +08:00
e887fd47f2 feat(scenic): 添加景区配置分享购买前开关
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 在AppScenicController中新增shareBeforeBuy配置返回字段
- 在ScenicConfigResp中增加shareBeforeBuy布尔类型属性
- 默认值设置为true以保持功能开启状态
2025-10-27 19:00:10 +08:00
f07d808f3d feat(printer): 支持上传裁剪参数并更新照片裁剪信息
- 在 AppPrinterController 中新增 crop 参数用于接收裁剪数据
- 修改 PrinterMapper 和 PrinterService 接口及实现,支持保存 crop 字段
- 更新 MemberPrintResp 模型以包含 crop 属性
-优化 Mapper XML 查询语句,统一使用 SELECT p.* 提高可读性
- 数据库更新语句中添加 crop 字段的赋值操作
2025-10-27 16:59:31 +08:00
2a662ae86d feat(printer): 添加打印任务分页查询和重新打印功能- 引入 PageHelper 和 PageInfo 实现分页查询
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 新增 PrintTaskMapper 接口方法 queryByCondition 和 updateStatus
- 扩展 PrintTaskEntity 实体类,新增 mpId 和 paper 字段- 在 PrinterController 中新增 /task/page 和 /task/reprint/{id} 接口- 更新 PrintTaskMapper.xml,添加查询和更新状态的 SQL语句- 优化打印任务插入逻辑,补充 mpId 和 paper 字段赋值
2025-10-27 13:58:59 +08:00
e805fdac9a feat(template): 添加人脸样本和视频源为空时的日志提示
- 在视频源查询为空时添加日志记录- 在人脸样本查询为空时添加日志记录
- 提高代码调试和问题排查的便利性
2025-10-27 13:58:32 +08:00
0db411c2e4 feat(order): 添加打印纸张信息到订单项
- 在 OrderItemVO 中新增 printerPaper 字段
- 根据商品类型查询并设置打印任务的纸张信息
- 优化订单详情接口返回数据结构
2025-10-27 11:22:47 +08:00
27930b1dca feat(printer): 增加打印任务查询功能并完善商品详情展示
- 在 PrinterMapper 中新增 queryTaskByMpId 方法用于查询打印任务
- 扩展 GoodsDetailVO 类,增加打印机名称和纸张类型字段
- 修改 OrderServiceImpl,在商品列表中设置打印机相关信息
- 更新 PrintTaskResp 类,添加纸张类型属性
- 在 PrinterMapper.xml 中实现 queryTaskByMpId 的 SQL 查询逻辑
2025-10-27 10:24:59 +08:00
b3e2601758 添加字段 2025-10-27 09:25:17 +08:00
d9049b8a29 feat(printer): 修改用户照片上传接口返回照片ID
- 将 addUserPhoto 接口的返回值从布尔值改为照片ID
- 更新 AppPrinterController 中上传接口的返回值为照片ID
- 调整 PrinterServiceImpl 实现类中 addUserPhoto 方法返回实体ID- 修改 PrinterService 接口定义,统一返回类型为 Integer
2025-10-27 09:24:59 +08:00
0f0601e5eb feat(order): 根据订单类型设置商品名称
- 为类型3的订单设置商品名为"打印照片"
-为类型4的订单设置商品名为"一体机打印"
- 更新商品列表中的第一个商品名称以匹配订单商品名
2025-10-26 18:46:54 +08:00
8064c68b8b feat(profit-share): 调整分账与退款消息结构并优化接口
- 修改手动分账接口路径为 /manual 并支持请求体参数
- 更新计算分账结果接口路径为 /calculate
- 将退款消息主题从 zt-refund 更改为 zt-profitshare-refund
-重构退款消息对象字段,增加退款类型和原订单 ID
- 更新退款消息生产者逻辑以适配新字段和主题配置
- 强化退款消息校验规则,确保必要字段完整性
2025-10-24 20:10:44 +08:00
0a57eeaeef feat(face): 支持多种人脸搜索结果合并模式
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 新增face_select_post_mode配置,支持三种合并模式
    - 模式0:并集合并(默认),收集所有搜索结果样本ID
    - 模式1:交集合并,只保留所有结果中共有的样本ID
    - 模式2:直接使用用户选择的样本,跳过搜索过程
- 重构mergeSearchResults方法,增加mergeMode参数- 添加computeIntersection方法计算交集逻辑
- 添加createDirectResult方法处理模式2的直接结果
- 更新日志记录,便于追踪不同模式的执行情况
-保持向后兼容,旧调用方式默认使用并集模式
2025-10-24 18:21:17 +08:00
fb637bc7db feat(repository): 处理重复templatePlaceholder时倒序sourceMap列表
- 新增逻辑用于识别重复的templatePlaceholder
- 对于重复的占位符,将其对应的sourceMap列表进行倒序排列
- 保持原有去重逻辑不变
- 添加了对占位符前缀"P"的检查逻辑
2025-10-24 17:12:07 +08:00
ca2b812574 feat(task):优化设备照片数量限制逻辑
- 新增根据样本数量与限制关系的多种处理策略
- 实现去首尾和去尾部的样本筛选逻辑
- 添加按创建时间排序和恢复原始排序的功能
-重构代码结构,提取处理逻辑到独立方法
- 更新日志记录以反映新的处理流程
- 使用 toList() 替代 collect(Collectors.toList()) 提升可读性
2025-10-24 17:05:53 +08:00
841c89af04 refactor(task): 移除视频生成通知的Redis缓存逻辑
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 删除了检查Redis中memberId是否在3分钟内已发送过通知的代码
- 移除了发送成功后设置Redis缓存的逻辑
- 简化了通知发送流程,直接发送消息而不进行缓存控制- 更新了日志记录,移除了缓存相关的调试信息- 保留了核心的通知发送功能和必要的业务逻辑
2025-10-24 16:22:20 +08:00
bed3a4e3c9 feat(printer): 实现任务状态的原子性更新与同步锁机制
- 添加 compareAndSetTaskStatus 方法以支持基于期望状态的任务更新
- 引入 ReentrantLock 实现任务同步处理,防止并发冲突
- 在 XML 映射文件中定义 compareAndSetTaskStatus 的 SQL 更新语句
- 定义任务状态常量:TASK_STATUS_PENDING 和 TASK_STATUS_PROCESSING
- 优化任务获取逻辑,确保任务状态在处理前正确更新为 PROCESSING
2025-10-23 21:24:58 +08:00
3f8b911e6f feat(face): 增加自定义人脸匹配次数限制与记录功能
- 新增常量 FACE_CUSTOM_MATCH_COUNT_PFX 用于记录自定义匹配次数
- 在人脸识别逻辑中增加对自定义匹配次数的读取与限制判断
- 实现 recordCustomMatchCount 方法用于记录自定义匹配调用次数
- 优化原有识别次数获取逻辑,避免重复代码
- 增加 Redis 过期时间设置,确保计数数据自动清理
2025-10-23 18:17:07 +08:00
ea4adcdeb7 feat(kafka): 移除Kafka集成服务
- 删除了KafkaIntegrationService类及其相关逻辑
- 移除了Kafka消息发送和连接测试功能
- 清理了未实现的预留接口方法
- 移除了相关的配置属性获取方法
2025-10-23 16:11:57 +08:00
6242a346ce feat(device): 更新设备响应字段
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 将 deviceNo 和 channelNo 字段替换为 previewUrl 和 previewCoverUrl
- 修改 DeviceRespVO 类中的字段定义- 在 AppScenicServiceImpl 中更新设备配置信息的设置逻辑
2025-10-22 12:19:22 +08:00
2d2ed6fc1b Revert "feat(notification):临时绕过授权检查逻辑"
This reverts commit 927c4955f8.
2025-10-21 22:35:08 +08:00
1b312313b2 feat(face): 增加人脸识别详情与人工调整功能
- 新增人脸识别详情接口,返回系统采纳与被过滤的样本信息
- 新增人工调整识别结果接口,支持用户手动选择或排除样本
- 引入样本过滤原因枚举,用于记录和展示过滤原因
- 重构样本过滤逻辑,增加过滤轨迹追踪功能
- 优化时间范围与设备照片数量限制的过滤实现
- 在搜索结果中增加过滤轨迹信息,便于前端展示
- 添加人脸识别详情VO和样本VO,丰富返回数据结构
- 完善人脸识别相关的请求与响应模型定义
2025-10-21 21:35:06 +08:00
37033f1b16 fix(notify):优化视频通知描述文案
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 简化视频生成完成通知的描述内容
- 调整未购买视频通知的表述方式
- 更新视频即将过期通知的提示语- 统一各类通知的描述风格
2025-10-20 17:11:53 +08:00
7ea78e4a17 refactor(logging): 将视频任务更新检查日志级别从 info 调整为 debug
- 修改 VideoTaskRepository 中的日志记录级别
- 将任务更新检查完成的日志从 info 级别改为 debug 级别
- 保持日志内容和参数不变,仅调整日志级别以减少生产环境日志量
2025-10-20 15:05:28 +08:00
9fcb472717 feat(notify): 增加景区通知模板标题和描述字段
- 在记录用户通知授权接口中将日志级别从 info 调整为 debug
- 在获取景区通知模板及用户授权余额接口中将日志级别从 info 调整为 debug
- 根据不同模板 ID 设置对应的标题和描述信息:
  - 视频生成通知  - 视频下载通知- 视频即将过期通知
  -未知模板类型兜底处理
- 在 ScenicTemplateAuthResp 类中新增 title 和 description 字段用于返回模板信息
2025-10-20 14:58:49 +08:00
70ef4eb09c fix(mapper): 更新用户通知授权检查逻辑
- 将XML中的小于等于符号从<=更正为&lt;=
- 确保SQL语句在XML文件中正确转义
- 避免因特殊字符导致的解析错误
2025-10-20 13:18:45 +08:00
58224a03c5 refactor(logging): 调整日志级别并移除冗余日志
- 将人脸识别成功日志从 info 级别调整为 debug 级别- 将 ZT-Source 消息处理成功日志从 info 级别调整为 debug 级别
- 移除 ZT-Source 照片素材保存成功的冗余 info 日志输出
2025-10-20 13:15:03 +08:00
927c4955f8 feat(notification):临时绕过授权检查逻辑
- 注释掉原有的授权次数检查和消费逻辑
- 直接返回 true 用于测试目的
- 保留原逻辑代码以备后续恢复
2025-10-20 13:15:03 +08:00
27f356519e feat(notification): 引入通知授权检查机制
- 在任务服务中集成通知授权工具类
- 实现视频生成通知前的权限校验逻辑- 在下载通知任务中增加授权消费检查- 添加过期提醒及额外下载通知的权限控制
- 完善无授权时的日志记录与处理流程
2025-10-20 13:15:03 +08:00
c80086ba69 feat(notify): 支持批量授权记录及景区模板查询
- 新增批量授权记录接口,支持一次请求处理多个模板ID
- 新增查询景区通知模板及用户授权余额接口
- 修改授权记录请求体,将单个templateId改为templateIds列表
- 增加授权记录响应结构,区分成功与失败记录
- 新增通知授权工具类,封装常用授权检查与消费方法
- 使用JwtTokenUtil获取当前用户ID替代BaseContextHandler
- 移除过时的BaseContextHandler导入及相关代码依赖
2025-10-20 13:15:02 +08:00
86d5f8ceb1 refactor(notify): 重构用户通知授权控制器以适应新的上下文处理器
- 将 JwtInfo 替换为 BaseContextHandler 来获取用户ID
- 更新 ResponseData 类型为 ApiResponse
- 修改 member ID 的获取方式为从 BaseContextHandler 中解析
- 使用 Jakarta Validation 注解替换旧的校验注解
- 调整 import 包路径以匹配新架构
2025-10-20 13:15:02 +08:00
ff708a3fc3 refactor(notify):重构通知授权模块,移除外部接口
- 移除用户通知授权检查、消费和记录查询的外部接口
- 废弃相关请求和响应 DTO 类文件
- 将授权检查和统计功能迁移至内部服务调用
- 新增批量检查授权方法 batchCheckAuthorization- 新增获取用户授权统计信息方法 getAuthorizationStats
- 更新 UserNotificationAuthorizationService 接口定义- 优化 ServiceImpl 中的数据处理逻辑和引入 Collectors 工具类
2025-10-20 13:15:02 +08:00
44b20890d5 feat(notify): 新增用户通知授权管理功能
- 添加用户通知授权记录的完整CRUD操作
- 实现授权次数的记录与消费逻辑
- 提供授权状态检查与剩余次数查询接口
- 支持按用户、模板或景区维度查询授权记录
- 新增授权统计信息接口,包括总授权数、消费数等
- 完成移动端相关请求/响应DTO定义
- 集成MyBatis Mapper实现数据持久化操作
- 添加服务层事务控制确保操作一致性
2025-10-20 13:15:02 +08:00
19ca91778f fix(pricing): 调整一口价优惠默认支持逻辑
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 修改默认情况下不支持一口价优惠
- 更新异常处理逻辑,异常时默认返回不支持
-修复日志描述与实际逻辑不符的问题
2025-10-20 11:18:18 +08:00
e5a58ae757 feat(source): 添加缩略图URL字段支持
- 在SourceController中移除了@Deprecated注解
- 在SourceMapper.xml的多个查询SQL中增加了thumb_url字段
- 更新了list、userGetById、getById、listUserOne和queryByRelation等方法的SELECT语句- 确保所有相关接口能够返回thumb_url数据
- 改动涉及视频源分页查询及相关详情获取逻辑
2025-10-20 10:40:08 +08:00
602eb84f9c feat(video): 添加缩略图URL设置功能
- 在视频片段获取器中新增缩略图URL字段
- 完善图片源信息的处理逻辑- 提高视频资源信息的完整性
2025-10-20 10:29:44 +08:00
8d24e7f9e5 feat(source): 添加缩略图URL字段并优化商品详情展示逻辑
- 在 SourceRespVO 中新增 thumbUrl 字段用于存储缩略图地址- 修改 GoodsServiceImpl 中的商品详情封面图设置逻辑,优先使用缩略图
- 更新 SourceMapper.xml 查询语句,增加 thumb_url 字段的查询支持
2025-10-20 10:20:12 +08:00
8eed685373 feat(source): 添加缩略图字段支持
- 在SourceEntity中新增thumbUrl字段用于存储缩略图地址- 在ZTSourceDataService中设置缩略图URL并优化裁剪配置逻辑
- 更新SourceMapper.xml中的SQL语句以支持缩略图字段的插入和更新
- 移除不必要的URL查询条件以提高查询灵活性
2025-10-20 10:05:22 +08:00
2b79302874 feat(printer): 引入唯一处理标识符避免文件冲突
- 添加 UUID 依赖以生成唯一标识符
-为打印任务生成唯一处理 ID,防止多线程文件名冲突
- 更新原图、水印图及旋转图片的文件命名逻辑
- 确保每个打印任务使用独立的临时文件名
2025-10-16 15:37:51 +08:00
56e1081304 refactor(storage): 移除不再使用的人脸存储路径常量
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 删除 StorageConstant 中的 VIID_FACE 常量定义
- 移除 FaceCleaner 中对 VIID_FACE 的引用和相关文件清理逻辑
- 清理相关的导入语句和静态引用
2025-10-15 19:13:48 +08:00
658e741611 feat(printer): 添加图片方向检测与自动旋转功能
- 引入 ImageUtils 工具类处理图片旋转逻辑
- 实现打印前对竖图自动旋转为横图处理
- 完成水印处理后将图片旋转回原始方向-优化临时文件清理逻辑,确保所有中间文件被删除
- 添加图片方向判断方法 isLandscape- 新增图片旋转90度和270度的工具方法
2025-10-15 18:53:28 +08:00
d5cd1924f5 feat(task): 添加视频生成通知防重机制- 新增Redis缓存键VIDEO_NOTIFICATION_CACHE_KEY用于记录通知发送状态
- 设置通知发送间隔为2分钟,防止重复发送
- 在发送通知前检查缓存,若3分钟内已发送则跳过- 发送成功后更新Redis缓存并设置过期时间
- 添加相关日志记录以方便追踪通知发送情况
2025-10-15 18:43:54 +08:00
645afbaf0c feat(printer): 添加打印照片水印处理功能
- 引入图片水印处理相关依赖和工具类
- 实现根据景区配置动态添加水印逻辑
- 支持从配置中读取存储类型和水印类型
- 下载原始图片并应用水印处理
- 将处理后的水印图片上传至指定存储服务
- 打印任务使用水印图片URL替代原始URL
- 增加异常失败时回处理确保水印退到原始图片- 清理处理过程中产生的临时文件
2025-10-15 17:37:26 +08:00
a7ede3303d refactor(task): 移除重复的景区配置查询逻辑
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 删除了 DownloadNotificationTasker 中多次调用的 getScenicMpConfig 方法
- 简化了视频下载通知任务的执行流程- 提高代码可读性和维护性
- 避免不必要的数据库查询操作
2025-10-14 20:32:36 +08:00
aa7330000f fix(task): 避免重复发送下载和过期通知
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 在发送下载通知前检查用户是否已接收通知
- 在发送过期通知前检查用户是否已接收通知- 在发送额外下载通知前检查用户是否已接收通知
- 使用ConcurrentHashMap.newKeySet()确保线程安全- 添加调试日志以追踪重复通知的跳过情况- 优化通知逻辑以提升定时任务执行效率
2025-10-14 20:31:45 +08:00
29f4bbf2d8 feat(message): 添加ZT消息生产者空实现服务
- 创建 ZtMessageProducerNoOpService 类作为 Kafka 禁用时的替代实现- 实现 ConditionalOnProperty 注解,当 kafka.enabled=false 时激活该服务- 覆写 send 方法,仅记录日志而不实际发送消息
- 添加构造函数以满足父类依赖要求
- 提供详细注释说明服务用途和实现逻辑
2025-10-14 20:28:00 +08:00
ad42254ea0 refactor(task): 移除通知模块依赖
- 删除了对通知模块的包引用
- 移除了通知模块相关的类导入- 清理了与通知功能相关的代码依赖
-优化了任务服务实现类的依赖结构
- 简化了下载通知任务器的代码引用
- 解除了通知工厂类的直接依赖关系
2025-10-14 19:38:47 +08:00
0ceecf0488 fix(message): 将消息相关接口的日志级别从 info 调整为 debug
- 修改消息列表查询接口的日志级别- 修改获取消息通道列表接口的日志级别- 统一调整日志输出方式以减少生产环境日志量
2025-10-14 19:20:41 +08:00
311008cbf2 feat(message): 集成ZT消息服务发送通知
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 在TaskTaskServiceImpl中引入ZtMessageProducerService依赖
- 替换原有微信通知逻辑,使用ZT消息服务发送视频生成通知- 在DownloadNotificationTasker中引入ZtMessageProducerService依赖
- 修改视频下载通知发送逻辑,使用ZT消息服务
- 修改视频过期提醒通知逻辑,使用ZT消息服务
- 调整额外通知时间配置获取方式,从scenicConfigManager获取
- 统一构建通知消息参数格式,包含data和page信息
- 添加详细的日志记录,便于追踪消息发送过程
2025-10-14 19:06:30 +08:00
f54d40d026 feat(message):为消息添加唯一标识符支持
- 在 ZtMessage DTO 中新增 messageId 字段
- 发送消息前自动生成 UUID 作为默认 messageId
- 更新 Kafka 生产者日志,包含 messageId 以便追踪
- 增强错误日志记录,附带 messageId 提升调试效率
2025-10-14 18:27:15 +08:00
3cb12c13c2 feat(printer):优化用户照片添加逻辑并返回结果ID
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 修改 addUserPhoto 方法参数,使用 MemberPrintEntity 实体传参- 在 PrinterMapper.xml 中配置 insert 语句返回主键 ID- 更新 addUserPhotoFromSource 方法返回值为 List<Integer>
- 添加异常处理和日志记录
- 调整 AppPrinterController 接口返回照片 ID 列表
2025-10-14 11:45:46 +08:00
bdeb41bead feat(profit-share): 实现分账管理V2版本功能
- 新增分账规则的创建、查询、更新、启用、禁用和删除接口
- 新增分账记录的查询接口,支持按景区、订单ID等多种方式查询
- 新增手动触发分账和计算分账结果的功能接口
- 新增获取支持类型的接口,方便前端展示和选择- 集成分账服务Feign客户端,实现与zt-profitshare微服务通信
- 添加Kafka消息配置,支持分账和退款消息的发送
- 完善DTO结构定义,包括规则、记录、明细及消息相关实体类
- 实现集成服务层,封装对分账服务的操作并提供fallback机制
- 控制器层增加参数校验和异常处理逻辑,提高系统健壮性- 所有接口均遵循RESTful设计规范,并提供详细的日志记录
2025-10-13 20:30:46 +08:00
feac2e8d93 refactor(config): 移除ScenicConfigManager中的冗余代码
- 删除了未使用的configMap字段- 移除了基于Map的构造函数- 清理了所有与configMap相关的getter方法
- 移除了hasKey和hasNonNullValue方法
- 删除了获取所有配置键和配置数量的方法
- 移除了配置子集和扁平化配置相关功能
- 简化了toString方法的实现
2025-10-12 01:09:54 +08:00
dc4091e058 feat(pricing): 新增升单检测功能- 添加升单检测API端点 /api/pricing/upgrade-check
- 实现 `checkUpgrade` 核心方法,用于检测已购与待购商品组合优惠
- 支持一口价和打包优惠的综合评估逻辑- 提供详细的请求参数与响应结果结构定义
- 更新文档说明升单检测的业务价值与使用场景- 补充关键架构变更记录与兼容性注意事项
2025-10-11 21:09:36 +08:00
be375067ce feat(message): 移除ZT消息生产者示例代码- 删除ZtMessageProducerExample类及相关依赖
- 移除示例消息发送逻辑
- 清理无用的HashMap和日志记录代码
- 移除条件注解@ConditionalOnProperty配置
- 删除消息构建及发送示例实现
2025-10-11 20:34:00 +08:00
7dec2e614c feat(watchdog): 增强任务监控告警机制
- 引入ZtMessageProducerService实现消息通知
- 添加任务积压、失败任务和长时间运行任务的分类监控
- 实现异常通知计数器,限制重复告警次数
-优化告警逻辑,支持异常恢复后计数器重置
- 移除旧的通知工厂依赖,统一使用消息队列发送
- 增加长时任务监控的清理机制,避免无效计数累积
2025-10-11 20:33:49 +08:00
51d0716606 Merge branch 'message-microservice'
# Conflicts:
#	src/main/java/com/ycwl/basic/integration/CLAUDE.md
2025-10-11 15:07:52 +08:00
765998bd97 docs(integration): 移除示例代码并更新配置说明- 删除设备集成测试中的默认配置启用示例
- 移除了消息集成组件中的示例引用
- 更新ZT-Message集成概述,去除对旧文档的引用
- 简化目录结构展示,移除example模块
- 清理冗余的配置键值说明- 统一删除各模块下的example目录引用
- 优化文档结构,提高可读性
2025-10-11 11:24:42 +08:00
5f4f89112b refactor(scenic): 移除ScenicV2WithConfigDTO并简化实体转换逻辑
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 删除ScenicV2WithConfigDTO类定义
- 更新ScenicV2Controller中的导入依赖- 更新ScenicV2Client中的导入依赖
- 更新ScenicIntegrationService中的导入依赖
- 更新ScenicRepository中的导入依赖
- 简化convertToScenicEntity方法参数类型
- 移除手动组合ScenicV2WithConfigDTO的代码逻辑
2025-10-11 00:11:42 +08:00
d68b062951 refactor(repository):重构景区配置管理逻辑- 引入 ScenicConfigManager 管理配置信息
- 移除手动构建 configMap 的逻辑
- 修改 convertToScenicEntity 方法签名,支持传入配置管理器
- 使用 configManager 替代直接从 DTO 获取配置值的方式
- 统一配置项获取方式,增强代码可维护性与扩展性
2025-10-11 00:10:25 +08:00
99857db006 feat(examples): 移除设备和问卷集成示例代码
- 删除默认配置集成服务使用示例类- 移除设备配置筛选功能使用示例
- 清理设备集成基础操作示例代码
- 移除设备集成降级机制示例
- 删除Kafka集成使用示例
- 清理问卷集成服务示例代码
2025-10-11 00:09:33 +08:00
e8c645a3c0 refactor(device): 移除设备与景区的冗余配置接口
Some checks failed
ZhenTu-BE/pipeline/head There was a failure building this commit
- 删除 DeviceV2Controller 中的设备配置相关接口
- 删除 ScenicV2Controller 中的景区配置相关接口
- 移除 DeviceConfigV2Client 中的扁平化配置接口
- 移除 DeviceV2Client 中的设备详情配置接口
- 更新 DeviceIntegrationExample 示例代码
- 移除 DeviceIntegrationFallbackExample 中的配置缓存示例
- 删除 DeviceConfigIntegrationService 中的配置获取方法
- 删除 DeviceIntegrationService 中的设备配置服务方法- 移除 RenderWorkerV2Client 中的工作器配置接口- 删除 RenderWorkerConfigIntegrationService 中的配置键名- 移除 RenderWorkerIntegrationService 中的工作器配置方法
- 删除 ScenicConfigV2Client 中的扁平化配置接口
- 移除 ScenicV2Client 中的景区配置接口
- 更新 ScenicIntegrationExample 示例代码
- 删除 ScenicConfigIntegrationService 中的配置获取方法
- 删除 ScenicIntegrationService 中的景区配置服务方法
- 修改 ScenicRepository 中景区实体获取逻辑
2025-10-10 23:55:17 +08:00
fe8068b3d9 refactor(scenic): 重构景区配置响应结构
- 移除了过时的配置字段,如预约流程、强制完成时间等
- 调整了字段顺序并添加分类注释(基础配置、功能开关、提示文案)
-保留并优化核心配置项,如水印URL、防录屏类型等
- 清理了未使用的导入包和冗余代码
- 统一了优惠券开关字段,移除重复定义
2025-10-10 13:46:59 +08:00
c689496130 feat(scenic): 添加分享功能配置项
- 在ScenicConfigResp中新增shareEnable字段
- 在AppScenicController中设置shareEnable默认值为true
- 支持景区配置是否开启分享功能
- 保持与shareBeforeBuy配置项的一致性处理
2025-10-10 10:38:47 +08:00
7e16ad35e7 feat(app): 新增分享前购买配置项
- 在AppScenicController中增加shareBeforeBuy配置项- 默认值设置为true以启用该功能- 更新响应对象以支持新的配置选项
2025-10-10 09:26:42 +08:00
1727619b29 refactor(kafka): 将人脸识别处理改为异步执行- 引入CompletableFuture实现异步处理
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 修改processFaceRecognition方法为异步版本
- 移除原同步处理中的try-catch块
- 更新方法返回类型从boolean改为void-保留处理成功和失败的状态更新逻辑- 添加异步处理成功后的日志记录
2025-10-04 10:12:37 +08:00
3099e68a97 refactor(logging): 调整人脸处理服务中的日志级别
- 将接收到人脸消息的日志级别从 info 调整为 debug
- 移除了部分冗余的 info 级别日志输出
- 统一异常处理中的日志记录方式
-优化日志内容,减少不必要的信息输出
- 确保关键操作仍然保留适当日志记录- 提升系统在高并发下的日志可读性与性能
2025-10-03 13:46:22 +08:00
db86c82bc8 refactor(task):优化视频片段获取逻辑并增强日志记录
- 移除任务执行前的空列表检查,统一通过VideoPieceGetter.addTask处理
- 增强Placeholder初始化阶段的日志输出,区分有无templateId情况- 细化计数器递减过程中的日志信息,记录设备关联及剩余数量
- 完善进度检查时的日志内容,增加已完成与未完成的统计显示- 补充Callback调用条件判断,避免重复触发并记录调用状态
- 添加兜底逻辑中对Callback是否已触发的判断和相应日志提示
2025-10-01 22:01:34 +08:00
f33ce8e7a7 feat(video):优化视频切片任务处理逻辑
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 添加对配对设备的处理,确保主设备也能正确执行切片任务
- 调整计数器逻辑,使主设备和配对设备的未完成占位符计数一致
- 增强日志记录,明确标识设备占位符满足情况
- 改进进度计算方式,更准确地反映任务完成状态- 在所有占位符满足时提前调用回调函数,提升任务执行效率
2025-10-01 21:22:19 +08:00
de65fa1dd8 feat(scenic): 添加水印URL配置支持
- 在ScenicConfigResp中新增watermarkUrl字段
- 在AppScenicController中设置水印URL配置项
- 支持从scenicConfig中获取watermark_url配置值
2025-10-01 17:00:44 +08:00
132a539bb6 fix(kafka): 调整人脸识别消息处理逻辑,确保消息始终被消费- 修改消息处理失败时的确认机制,避免消息堆积
- 即使人脸样本保存或识别处理失败,也消费消息防止重复处理
- 异常情况下同样确认消息消费,记录错误日志而非阻塞流程- 优化日志记录,明确区分处理结果与消息确认状态
2025-09-28 11:26:01 +08:00
9f66544a29 feat(source): 处理ZT-Source消息时支持设备裁剪配置
- 新增DeviceRepository依赖注入
- 获取设备配置管理器并检查裁剪配置
- 根据裁剪配置设置缩略图URL
-优化sourceEntity数据处理逻辑
2025-09-27 23:28:50 +08:00
f4a16b5b09 feat(dto): 添加位置信息字段支持
- 在 ZTSourceMessage DTO 中新增 posJson 字段
- 更新数据库插入语句以支持 posJson 字段存储
- 调整日志输出内容,突出关键业务标识
- 在数据服务层增加对 posJson 的处理逻辑
2025-09-27 23:09:44 +08:00
9bc34fcfdb feat(kafka): 新增ZT-Source Kafka消息处理功能
- 新增ZTSourceMessage实体类用于接收Kafka消息
- 新增ZTSourceConsumerService监听zt-source主题
- 新增ZTSourceDataService处理消息并保存至数据库- 扩展SourceMapper支持从ZT-Source消息新增素材
- 实现照片类型素材的解析、校验与存储逻辑
- 添加Kafka手动ACK确认机制确保消息可靠处理
2025-09-27 22:16:47 +08:00
4b01e4cf82 feat(task):优化人脸识别任务中的样本排序逻辑
- 引入HashMap以支持按ID顺序排序人脸样本列表
- 在筛选前对搜索结果按分数降序排序
- 简化设备照片限制逻辑,去除冗余的时间排序步骤
- 提升匹配准确性和处理效率
2025-09-27 13:45:05 +08:00
f885f734ad perf(viid):优化线程池配置与图片裁剪内存管理
- 调整线程池核心线程数为8,最大线程数为32,空闲时间10秒- 队列大小从1024降至100,提升响应速度
- 添加CallerRunsPolicy策略,防止任务丢失
- 图片裁剪方法增加try-finally块确保资源释放- 显式调用image.flush()和System.gc()优化内存使用
- ByteArrayOutputStream关闭操作添加异常捕获
-修复潜在的内存泄漏问题
2025-09-27 13:17:48 +08:00
ddbc2a0edb fix(biz):修复用户购买检查逻辑
- 修改PriceBiz中checkUserBuyItem方法的模板ID参数为-1
- 在FaceServiceImpl中增加对模板ID的购买检查逻辑- 确保用户购买状态判断的准确性
2025-09-27 01:50:26 +08:00
da89067c48 refactor(task):优化视频片段获取任务的设备计数逻辑
- 将 currentUnFinPlaceholder从 List 类型改为 Map<String, AtomicInteger>- 使用 AtomicInteger 跟踪每个设备的未完成任务数量
- 在设备任务完成时正确减少计数并清理已完成的设备
- 更新进度日志以反映去重后的设备总数
2025-09-27 01:07:52 +08:00
2836326518 fix(face):修复vlog渲染状态显示错误问题
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 调整step3状态逻辑,确保渲染中状态正确显示
- 修改状态文本提示,优化用户体验
-修复渲染完成状态判断逻辑错误
2025-09-26 17:34:12 +08:00
6091d41df9 feat(face):优化视频切分任务筛选逻辑
- 按设备ID分组并按创建时间倒序排序
- 根据设备配置限制视频数量
- 修复日志中原始
2025-09-26 16:43:20 +08:00
d4f9f1fe0d feat(face):优化视频重切任务的样本选择逻辑
- 根据设备配置限制视频样本数量
- 实现按设备分组并应用数量限制- 更新视频重切任务中的样本ID列表
- 保留原有照片与视频数量比较逻辑
2025-09-26 16:20:31 +08:00
d860996f6d feat(face):优化视频重切任务的样本选择逻辑
- 根据设备配置限制视频样本数量
- 实现按设备分组并应用数量限制- 更新视频重切任务中的样本ID列表
- 保留原有照片与视频数量比较逻辑
2025-09-26 16:15:34 +08:00
1b2793215f fix(video): 解决并发环境下视频片段处理的文件名冲突问题
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 为输出文件名添加时间戳和线程ID后缀,确保唯一性
-为临时文件名添加时间戳和线程ID后缀,防止并发冲突
- 避免因文件名重复导致的视频处理错误
2025-09-26 14:26:09 +08:00
4f1443a3ca fix(video): 处理空imgSource情况- 添加空值检查以避免保存空source记录
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 记录警告日志当imgSource为空时- 返回false以跳过无效处理流程
2025-09-26 12:39:22 +08:00
aba9fb0a15 feat(printer): 添加用户购买项设置的Redis缓存控制
- 引入RedisTemplate依赖用于缓存控制
- 新增60秒的缓存键避免重复处理用户购买项
- 在setUserIsBuyItem方法中实现缓存检查逻辑- 添加TimeUnit依赖支持缓存过期时间设置
- 定义USER_PHOTO_LIST_TO_PRINTER缓存键前缀
2025-09-26 12:39:17 +08:00
ab3208c9df feat(kafka): 添加手动提交模式支持以增强消息处理可靠性
- 在 KafkaConfig 中新增 manualCommitKafkaListenerContainerFactory 配置
- 启用手动提交模式并设置 AckMode 为 MANUAL_IMMEDIATE
- 修改 FaceProcessingKafkaService 使用新的容器工厂- 添加 Acknowledgment 参数以控制消息提交时机
-仅在人脸样本保存和识别全部成功后才手动确认消息
- 处理失败时不再调用 ack.acknowledge()使消息可重新消费
- 更新 processFaceRecognition 方法返回处理结果状态
- 增强异常处理逻辑,确保失败情况下不提交消息
2025-09-25 18:46:15 +08:00
09e376e089 refactor(kafka): 统一时人脸消息时间类型为Date
- 将FaceProcessingMessage中的LocalDateTime替换为Date类型- 更新消息创建工厂方法以使用Date参数
- 调整Kafka服务中时间转换逻辑以匹配新类型
- 移除LocalDateTime相关的导入和引用- 更新字段注释以反映新的时间类型
2025-09-25 18:09:17 +08:00
dad9ddc17c docs 2025-09-25 16:18:03 +08:00
4a05773860 fix(device): 添加空值检查避免空指针异常- 在设置设备在线状态时添加对 lastActiveTime 和 clientIP 的空值检查
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 在判断设备是否在线时,增加对 keepaliveAt 时间的空值判断
- 防止因空值导致的 NullPointerException 异常- 提高代码健壮性和稳定性
2025-09-25 15:52:16 +08:00
3c700a42f9 feat(device): 添加设备在线状态查询功能- 在DeviceV2Controller中新增getDeviceOnlineStatus接口,用于根据设备ID查询设备在线状态
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 引入DeviceStatusDTO和DeviceStatusIntegrationService以支持设备状态查询- 修改DeviceStatusDTO中的时间字段类型为Date,并调整JSON序列化格式- 在DeviceRepository中增加convertToEntityWithStatus方法,用于合并设备信息与状态信息
- 优化DeviceRepository中的getOnlineStatus方法,增加异常处理和降级机制- 完善设备在线状态查询的日志记录和错误处理逻辑
2025-09-25 15:32:09 +08:00
47c6b2ca67 feat(device): 新增设备状态管理集成服务
- 添加设备状态客户端接口,支持设备在线状态查询与设置
- 创建设备状态相关 DTO,包括设备状态、在线状态和状态动作枚举
- 实现设备状态集成服务,封装设备状态操作与异常处理逻辑
- 支持单个及批量设备在线状态检查与设置功能
- 提供
2025-09-25 14:18:06 +08:00
59baf8811b feat(pricing): 添加商品一口价优惠支持检查
- 在 PriceProductConfig 实体中新增 canUseOnePrice 字段
- 更新数据库插入和更新语句,支持 canUseOnePrice 字段持久化- 在 OnePricePurchaseDiscountProvider 中实现商品一口价优惠支持检查逻辑
- 新增 areAllProductsSupportOnePrice 方法,验证购物车商品是否支持一口价优惠
- 支持查询具体商品配置和默认配置的一口价优惠设置
- 添加日志记录和异常处理,确保检查过程不影响主流程
2025-09-25 10:40:10 +08:00
019b9ffca6 refactor(video):优化视频关联关系处理逻辑
- 调整source记录插入时机,确保关联关系处理前数据已存在
- 移除冗余的source存在性检查逻辑- 统一关联关系处理流程,避免重复代码
- 添加日志记录以便追踪处理过程- 优化代码结构,提高可读性和维护性
2025-09-24 18:04:47 +08:00
30805f3e30 refactor(mapper):优化查询逻辑并处理空列表情况
- 将 filterExistingRelations 查询中的 if 判断替换为 choose-when 结构
- 在 otherwise 分支中添加空结果集查询,避免空列表时 SQL 异常- 统一 filterValidSourceRelations 查询结构,增强代码一致性
-修正 foreach 标签中 UNION ALL 前后的空格问题,确保 SQL 语法正确- 提升 XML 映射文件的可读性和健壮性
2025-09-24 17:50:53 +08:00
94d6b2f443 feat(source): 增强source关联关系的数据一致性校验
- 在SourceMapper中新增sourceExists方法,用于校验source是否存在
- 新增filterValidSourceRelations方法,过滤无效的source引用
- 在FaceServiceImpl中增强关联关系创建逻辑,防止重复和无效数据
- 在VideoPieceGetter任务中增加source存在性校验,避免创建孤立关联- 添加详细的日志记录,便于追踪关联关系创建过程
-优化XML映射文件,支持新的校验和过滤查询逻辑
2025-09-24 17:39:05 +08:00
b34f994298 feat(source): 添加过滤已存在关联关系功能
- 在SourceMapper中新增filterExistingRelations方法
- 修改FaceServiceImpl中的关联关系保存逻辑
- 修改TaskFaceServiceImpl中的关联关系保存逻辑
- 修改VideoPieceGetter中的关联关系检查逻辑
- 在SourceMapper.xml中添加filterExistingRelations的SQL实现
2025-09-24 17:16:12 +08:00
7728f4424f status 2025-09-24 13:45:48 +08:00
becbe5f6ab 允许重复
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
2025-09-24 05:03:47 +08:00
dc3a46362b Merge branch 'kafka_face_sample' 2025-09-24 05:03:04 +08:00
a361b59d74 配置
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
2025-09-23 20:57:01 +08:00
f779b0e040 计算 2025-09-23 20:53:22 +08:00
78c4548d02 文字 2025-09-23 17:54:49 +08:00
842310f73c ignore 2025-09-23 14:34:32 +08:00
cf235d38bb feat(模板): 为模板查找方法添加scanSource参数
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
在findFirstAvailableTemplate方法中新增scanSource参数,用于控制模板生成时的来源检查逻辑。调用方TaskTaskServiceImpl在强制创建vlog时传入false以跳过来源检查。
2025-09-23 13:50:26 +08:00
8903818cb0 订单详情 2025-09-23 12:21:34 +08:00
ae0cf56216 content返回url 2025-09-23 10:40:04 +08:00
90b6f53986 兜底1个 2025-09-23 10:38:23 +08:00
80b4508211 docs 2025-09-23 10:07:14 +08:00
57b8d90d5e 名称 2025-09-23 10:04:05 +08:00
59b481989c 避免文件过小损坏
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
2025-09-22 09:44:26 +08:00
61cf9383d0 修改 2025-09-22 09:33:17 +08:00
878dec2c55 npe 2025-09-21 21:18:43 +08:00
48bd9d2b0c 通知地址 2025-09-21 02:40:40 +08:00
b365d86796 显示逻辑 2025-09-21 02:38:30 +08:00
18cb459320 一口价查询 2025-09-21 00:16:45 +08:00
b7d3e20c46 C a c h e 2025-09-20 18:07:55 +08:00
d55c7a7769 Reapply "feat(PriceBiz): 新增商品类型字段并完善商品列表逻辑"
This reverts commit 638da8cd3d.
2025-09-20 16:53:55 +08:00
0432b99524 feat(OrderMapper): 查询订单时按支付时间倒序排列在queryTypeOrder查询中添加order by pay_at desc子句,
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
确保返回的订单记录按支付时间从新到旧排序,便于业务逻辑处理和展示最新订单信息。
2025-09-20 15:51:33 +08:00
da71e6d16f debug 2025-09-20 15:51:22 +08:00
f29217ac1f fix(FaceServiceImpl):修复视频模板封面URL设置问题当视频模板封面URL为空时,使用图片列表第一项的URL进行设置,避免空值导致的问题。 2025-09-20 15:51:19 +08:00
638da8cd3d Revert "feat(PriceBiz): 新增商品类型字段并完善商品列表逻辑"
This reverts commit 7ca59a1b0b.
2025-09-20 15:51:10 +08:00
7ca59a1b0b feat(PriceBiz): 新增商品类型字段并完善商品列表逻辑
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
新增 GoodsListRespVO 中的 goodsType 字段,用于区分不同商品类型。补充商品列表中“录像集”与“照片集”的类型标识。
在 PriceBiz 中注入 FaceService 并延迟加载,优化依赖关系。
根据内容购买状态判断是否整体购买,增强一口价商品的处理逻辑。
2025-09-20 04:51:51 +08:00
f10ede0d2c refactor(biz): 优化模板参数过滤逻辑
- 统计每个占位符在模板中出现的次数
- 根据占位符出现次数和实际可用源数量,选择合适的源进行过滤
- 优化日志输出,增加占位符统计信息
2025-09-19 18:50:01 +08:00
9226dfff1d feat(questionnaire): 添加下载问卷小程序二维码功能
- 新增 downloadQrCode 方法,用于生成和下载问卷小程序二维码
- 集成微信小程序配置和二维码生成工具
- 实现问卷二维码的生成、上传和访问控制
2025-09-19 18:50:01 +08:00
67f5c274f7 feat(ScenicConfigResp): 添加等待时显示照片的字段
在 ScenicConfigResp 类中添加了 showPhotoWhenWaiting 字段,用于控制在等待时是否显示照片。
2025-09-19 17:06:17 +08:00
ff8fe33eb0 Merge branch 'print-price' 2025-09-19 15:08:55 +08:00
292157885a refactor(questionnaire): 移除问卷模块中的健康检查接口
- 删除了 QuestionnaireClient 中的 health 方法
- 删除了 QuestionnaireIntegrationService 中的 health 方法
-移除了与健康检查相关的代码和注释
2025-09-19 13:33:20 +08:00
ad33b1abef feat(pc): 添加打印机管理功能- 新增 PrinterManageController 类实现打印机管理接口
- 添加打印机列表查询、详情、创建、更新、删除等功能
- 新增打印机状态、首选尺寸、当前使用设备更新接口
- 在 PrinterMapper.xml 中添加相关 SQL 语句
2025-09-19 12:32:58 +08:00
524627ea73 Merge branch 'refs/heads/questionnare-microservice'
# Conflicts:
#	src/main/java/com/ycwl/basic/controller/pc/DefaultConfigController.java
2025-09-19 12:29:54 +08:00
a5c815b6ed feat(pricing): 新增升单检测功能
- 添加升单检测接口和相关 DTO 类
- 实现升单检测逻辑,包括价格汇总、一口价评估和打包优惠评估
- 优化商品列表复制和规范化处理
- 新增 IBundleDiscountService 依赖
2025-09-18 19:51:13 +08:00
7a35551a7b feat(video): 添加视频查看权限控制功能
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 新增视频查看权限相关数据结构和接口
- 实现用户视频查看记录的创建和更新逻辑
- 添加视频查看权限的检查和记录功能
-优化分布式环境下的并发控制
2025-09-18 18:42:53 +08:00
7820a282d9 refactor(pc): 优化人脸服务中的模式匹配逻辑
- 在获取 re_match_mode 配置时,添加默认值 0,避免潜在的 NullPointerException
- 优化了 switch 表达式中的条件判断,提高代码可读性
2025-09-18 18:31:30 +08:00
864fc2c127 refactor(RenderWorkerV2Controller): 将日志级别从info更改为debug将RenderWorkerV2Controller中的多个日志记录级别从info更改为debug,以减少不必要的日志输出。
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 分页查询渲染工作器列表- 获取渲染工作器详情
- 创建渲染工作器
- 更新渲染工作器
2025-09-18 16:43:46 +08:00
ab7deb5c8f feat(RenderWorkerV2Controller): 添加带保活信息的渲染工作器DTO并更新控制器新增了RenderWorkerWithStatusDTO类,用于封装带有保活信息的渲染工作器数据。在RenderWorkerV2Controller中更新了listWorkers方法,使其返回包含保活信息的工作器列表。 2025-09-18 16:41:21 +08:00
079c5dc540 feat(视频更新): 添加视频片段更新检查功能
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 新增TaskUpdateResult类存储任务更新检查结果
- 在VideoTaskRepository中实现checkTaskUpdate方法检查任务更新状态
- 重构GoodsServiceImpl中的视频更新检查逻辑,使用VideoTaskRepository的统一实现
- 在ContentPageVO中添加newSegmentCount字段显示新增片段数
2025-09-18 15:11:50 +08:00
1220348bae refactor(PrinterServiceImpl):重构价格计算逻辑以使用统一的价格计算服务
在 `PrinterServiceImpl` 类中,移除了对 `PriceRepository` 的依赖,并引入了 `IPriceCalculationService` 接口来处理价格计算。主要改动包括:- 添加了新的导入语句,如 `PriceCalculationRequest`, `PriceCalculationResult`, `ProductItem`, `ProductType` 和 `IPriceCalculationService`。
- 在 `queryPrice` 方法中,通过 `IPriceCalculationService` 计算照片打印的总价格,替代了原有的直接从 `PriceRepository` 获取价格配置的方式。
- 更新了 `createOrder` 方法中的价格计算逻辑,同样采用 `IPriceCalculationService` 来确定订单的价格信息。
- 对于没有可打印照片的情况,现在会返回一个零价格的对象或抛出异常,具体取决于调用上下文。

这些更改旨在提高代码的模块化程度和可维护性,同时确保价格计算的一致性和准确性。
2025-09-18 13:36:25 +08:00
e9102e8e58 feat(pricing): 新增打包购买优惠功能
- 添加打包购买优惠信息类 BundleDiscountInfo
- 实现打包购买优惠提供者 BundleDiscountProvider
- 添加打包购买优惠服务接口 IBundleDiscountService 及其实现类 BundleDiscountServiceImpl
- 在 DiscountInfo 中添加 bundleDiscountInfo 字段以支持打包优惠
- 更新 CLAUDE.md 文档,详细说明打包购买优惠系统的设计和实现
2025-09-18 11:37:23 +08:00
e86dc85afe refactor(pricing): 调整优惠优先级顺序
-
2025-09-18 10:28:35 +08:00
b14754ec0a feat(integration): 添加消息服务相关接口和功能
- 新增 MessageController 类,实现消息列表查询和消息通道列表获取功能
- 新增 MessageClient 接口,用于调用消息服务的 Feign客户端
- 新增 ChannelsResponse、MessageListData 和 MessageRecordDTO 数据传输对象
- 新增 MessageIntegrationService 服务类,处理消息服务相关业务逻辑
2025-09-17 21:53:41 +08:00
a888ed3fe2 feat(integration): 添加 ZT-Message Kafka 生产者集成
- 新增 ZtMessage DTO 类用于消息体
- 实现 ZtMessageProducerService 生产者服务
- 添加示例演示如何发送消息
- 更新配置文件和文档以支持新功能
2025-09-17 21:38:26 +08:00
fde4deb370 Merge branch 'refs/heads/price_inquery'
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
2025-09-17 17:19:53 +08:00
5212547b3a docs 2025-09-17 17:18:48 +08:00
9a39592a98 Merge branch 'refs/heads/price_inquery' 2025-09-17 17:03:37 +08:00
f3fdb44742 refactor(mybatis): 移除 XML 配置,使用注解替代
- 在 PriceVoucherBatchConfigMapper、PriceVoucherCodeMapper 和 VoucherPrintRecordMapper 中添加了 @Select 和 @Update 注解
- 删除了对应的 XML 配置文件
- 优化了 SQL 查询,直接在 Java 接口中定义
2025-09-17 17:03:12 +08:00
ad111cdebb Merge branch 'page_info' into price_inquery 2025-09-17 16:54:59 +08:00
1c0c0393aa feat(pricing): 实现批次统计功能
- 查询批次配置和券码数据
- 统计每个券码的使用情况,包括状态、使用次数、剩余次数等信息
- 计算是否还能使用和剩余使用次数
- 获取使用记录数和最后使用时间
- 返回批次统计结果列表
2025-09-17 16:36:50 +08:00
04f7c79679 Merge branch 'refs/heads/page_info' 2025-09-17 16:22:26 +08:00
6d3fecc1c8 feat(AppClaimController): 优化优惠券领取结果展示
- 在 ClaimResp 中添加 couponType 字段,用于展示优惠券类型
-根据 CouponType 枚举值,设置不同的优惠券类型描述- 优化折扣优惠券和满减优惠券的描述生成逻辑- 保留原有的通用优惠券描述配置
2025-09-17 15:49:15 +08:00
5626498002 refactor(coupon): 重构优惠券领取结果封装
- 在 CouponClaimResult 类中添加 PriceCouponConfig 类型的 coupon 字段
- 修改 success 静态方法,接收 PriceCouponConfig 对象作为参数
- 更新方法内部逻辑,使用 coupon 对象替代单独的 couponName 字段
- 调整 CouponServiceImpl 中的代码,适应新的 CouponClaimResult 结构
2025-09-17 15:29:16 +08:00
8975ce404c feat(FaceServiceImpl): 实现人脸重复匹配逻辑
- 新增旅游时间和项目匹配逻辑
-增加识别次数、旅游匹配和项目匹配的规则判断
-根据不同匹配模式返回相应的结果
2025-09-17 15:13:39 +08:00
2a8bdaec28 feat(mapper): 添加获取用户项目 ID 列表的方法
- 在 StatisticsMapper 接口中新增 getProjectIdListForUser 方法
- 在 StatisticsMapper.xml 中添加对应的 SQL 查询语句
- 该方法用于获取用户在指定时间之前的项目 ID 列表
2025-09-17 15:13:17 +08:00
b323450708 refactor(paging): 重构分页查询使用 PageHelper
-将 MyBatis-Plus 的分页插件替换为 PageHelper
- 更新了相关控制器、服务接口和实现类中的分页查询方法
- 优化了分页查询的逻辑,提高了代码的可读性和维护性
2025-09-17 12:53:32 +08:00
a5e882e693 feat(basic): 添加视频更新检查功能
- 新增 VideoUpdateConfig 类用于配置视频更新检查参数
- 添加 VideoUpdateCheckVO 类作为视频更新检查响应模型
-功能包括检测片段变化、判断是否可更新以及统计片段数量等
2025-09-17 09:39:47 +08:00
a2348e3692 Merge branch 'refs/heads/price_inquery'
Some checks failed
ZhenTu-BE/pipeline/head There was a failure building this commit
2025-09-17 00:04:44 +08:00
6006fe460c feat(pricing): 优惠券增加有效期时间范围功能
- 在VoucherBatchCreateReqV2、VoucherBatchResp、VoucherInfo 和 PriceVoucherBatchConfig 类中添加有效期开始时间和结束时间字段
- 实现有效期时间范围的验证和检查逻辑
- 更新 VoucherBatchServiceImpl 和 VoucherServiceImpl 以支持有效期时间范围功能
2025-09-16 23:49:39 +08:00
1506ae95b8 feat(pricing): 增加券码重复使用功能并优化相关数据结构
- 在 PriceVoucherUsageRecord 和 VoucherUsageRecordResp 中添加 usageSequence 字段,用于记录券码的使用序号- 更新 PriceVoucherCode 实体和相关 mapper,增加 currentUseCount 和 lastUsedTime 字段
- 修改 VoucherCodeServiceImpl 和 VoucherServiceImpl 中的券码使用逻辑,支持重复使用
- 新增VoucherBatchOverviewResp、VoucherDetailResp、VoucherUsageSummaryResp 和 VoucherValidationResp 等新的响应 DTO 类,用于提供券码批次概览、详情、使用统计和验证等功能
2025-09-16 20:54:37 +08:00
8380b02fbb feat(pricing): 增加券码重复使用功能并优化相关接口
- 在 VoucherCodeResp 和 VoucherInfo 中添加可重复使用券码相关字段
- 新增 getVoucherDetail、getVoucherUsageSummary 和 getBatchOverview接口
- 优化 calculateVoucherDiscount 接口,支持重复使用券码的计算
- 在 PriceVoucherUsageRecordMapper 中添加按券码ID和用户ID查询使用记录的方法
2025-09-16 19:46:56 +08:00
c6681a249e Merge branch 'refs/heads/voucher_inf' 2025-09-16 17:57:34 +08:00
90a21c0933 fix(pricing): 完善券码验证逻辑并优化使用权限判断
- 新增用户在指定批次下的使用次数统计和最后使用时间获取功能
- 重构券码验证逻辑,支持未领取券码的使用权限判断
- 优化已领取券码的使用限制检查,包括使用次数和间隔时间- 改进日志记录,增加剩余使用次数信息
-修复一些潜在的逻辑问题和边界情况处理
2025-09-16 17:55:24 +08:00
57266eb535 refactor(order): 重构订单创建和价格计算逻辑
- 修改订单服务中的券码使用逻辑,增加人脸 ID 参数
- 优化价格计算控制器和服务中的预览模式
- 更新券码使用记录,支持人脸 ID 记录
- 修复零金额订单的处理逻辑
- 优化日志输出级别和内容
2025-09-16 17:54:31 +08:00
7cfcc44531 refactor(pricing): 重构券码使用记录相关接口和映射
- 移除了 VoucherUsageController 中的 Swagger 注解
- 更新了 PriceVoucherUsageRecordMapper 中的 SQL 查询
- 新增了 PriceVoucherUsageRecordMapper.xml 文件,用于定义分页查询
2025-09-16 17:54:31 +08:00
2f51470d43 Merge branch 'refs/heads/xmgl' 2025-09-16 15:03:33 +08:00
a61ecf7646 refactor(mobile): 重构商品页面逻辑
- 引入 ScenicConfigManager 接口以更好地处理景点配置
- 优化源素材查询和处理逻辑
-改进商品类型的处理方式,增加未知商品类型处理
- 优化商品封面图的获取逻辑,优先使用景点配置中的封面图
2025-09-16 14:39:43 +08:00
221f0175e6 feat(goods): 添加视频更新检查功能
Some checks failed
ZhenTu-BE/pipeline/head There was a failure building this commit
- 在 AppGoodsController 中添加视频更新检查接口
- 在 GoodsService 接口中添加 checkVideoUpdate 方法
- 在 GoodsServiceImpl 中实现视频更新检查逻辑
- 在 VideoGoodsDetailVO 中添加 templateId 字段
2025-09-16 11:14:24 +08:00
dcd5a8f930 feat(project): 增加项目模板关联功能
- 在 ProjectEntity 中添加 templateId 字段,用于绑定模板
- 在 ProjectReqQuery 和 ProjectRespVO 中添加 templateId 和 templateName 字段
- 修改 ProjectServiceImpl 中的查询方法,增加模板名称的查询和设置
- 更新 ProjectMapper.xml 中的 SQL语句,增加 template_id 相关操作
2025-09-16 01:27:40 +08:00
ce3f7aae1e feat(voucher): 支持券码重复使用
- 新增VoucherBatchCreateReqV2 请求对象,用于创建支持重复使用的券码批次
- 添加 VoucherUsageController 控制器,实现券码使用记录和统计功能
- 在VoucherInfo 对象中增加与重复使用相关的字段
- 修改 PriceVoucherBatchConfig 和 PriceVoucherCode 实体,支持重复使用相关属性
- 更新 VoucherBatchServiceImpl 和 VoucherServiceImpl,增加处理重复使用逻辑的方法
2025-09-16 01:08:54 +08:00
cce0b45e70 refactor(task): 重构任务参数处理逻辑
- 新增 filterTaskParams 方法,用于过滤模板所需的源数据
- 新增 getTaskParams 方法,用于获取任务参数并进行预处理
- 优化了视频源和图片源的处理逻辑,提高了代码可读性和可维护性
- 重构了任务回调中的源数据处理流程,使用新方法替代原有逻辑
2025-09-15 22:17:38 +08:00
f8c7cc2db6 Merge branch 'refs/heads/xmgl' 2025-09-15 17:17:35 +08:00
4b58c03ad2 feat(pc): 添加景区项目管理功能
- 新增项目管理相关的 Controller、Service、Mapper 及模型类
- 实现项目分页查询、列表查询、详情查询、新增、修改、删除等功能
- 添加项目状态更新和二维码下载功能
- 集成微信小程序二维码生成和存储服务
2025-09-15 17:17:06 +08:00
8ed38bd229 feat(biz): 修改模板未配置最小自动生成功能的默认行为
- 将日志信息从"默认不生成"修改为"默认生成!"
- 设置 minimalPlaceholderFill 为 1,表示开启最小自动生成功能
2025-09-15 16:14:26 +08:00
ccddab37ea feat(service): 实现自定义人脸匹配功能
- 新增 matchCustomFaceId 方法,实现自定义人脸匹配逻辑
- 优化 mergeSearchResults 方法,合并多个搜索结果
- 在 TaskFaceService 接口中添加 applySampleFilters 方法
- 在 TaskFaceServiceImpl 中实现 applySampleFilters 方法
2025-09-15 16:04:41 +08:00
8c37f2bf2f refactor(order): 优化订单相关代码
- 修改了多个模块中的方法名称,使其更加准确地反映功能
- 优化了部分代码逻辑,提高了可读性和维护性
- 增加了获取
2025-09-15 15:07:04 +08:00
89a2e19419 feat(order): 添加倒计时字段
- 在 IsBuyBatchRespVO 类中添加 countdown 属性,初始值为 3600秒(1小时)
- 用于表示订单倒计时时间,增强用户体验
2025-09-15 14:19:24 +08:00
63c2fdfece feat(pc): 更新人脸状态响应逻辑
- 引入 GoodsService接口,用于获取视频任务状态
- 修改 step3 状态判断逻辑,根据视频任务状态动态设置
- 优化显示文本,根据不同情况提供更准确的提示信息
2025-09-15 10:40:58 +08:00
048780071b feat(scenic): 添加景区配置分组功能并优化配置获取接口
- 在 ScenicConfigResp 中添加 groupingEnable 字段,用于表示是否开启分组功能
- 重构 getConfig 方法,使用 ScenicConfigManager 替代 ScenicConfigEntity- 优化配置参数的获取方式,使用 getString、getBoolean等方法替代直接获取字段值
2025-09-15 10:14:10 +08:00
c5f7003077 feat(face): 增加人脸状态查询功能
- 新增 FaceStatusResp 类用于人脸状态响应- 在 AppFaceController 中添加人脸状态查询相关接口
- 在 FaceService 接口中定义相关方法- 实现 FaceServiceImpl 中的人脸状态查询逻辑
- 优化 ContentPageVO 类,增加 group 字段
2025-09-15 10:13:41 +08:00
5531c576e0 refactor(basic): 移除景区缓存相关代码
- 删除了 ScenicRepository 中的缓存键常量
- 移除了 getScenicBasic 和 getScenic 方法中的缓存逻辑
- 删除了 clearCache 方法
- 移除了与缓存相关的工具方法
2025-09-13 23:44:15 +08:00
e43809593b refactor(basic): 移除 PriceRepository 中的缓存相关代码
- 删除了与 Redis 缓存相关的字段和方法
-移除了 clearPriceCache 和 clearPriceScenicCache 方法
-简化了 getPriceConfig 方法,移除缓存逻辑
2025-09-13 23:42:28 +08:00
bf672a8af7 feat(face): 添加低阈值检测功能
- 在 FaceConstant 中添加 FACE_LOW_THRESHOLD_PFX 常量
- 在 SearchFaceRespVo 中添加 lowThreshold 字段
- 在 FaceServiceImpl 中实现记录低阈值检测人脸的逻辑
- 在 TaskFaceServiceImpl 中添加低阈值检测的判断和结果设置
2025-09-13 15:04:06 +08:00
91e68c3272 feat(face): 增加人脸识别计数功能
- 在 FaceConstant 中添加 FACE_RECOGNITION_COUNT_PFX 常量
- 在 FaceServiceImpl 中实现记录人脸识别次数的方法
- 使用 Redis 进行计数,并设置过期时间
2025-09-13 14:42:49 +08:00
96c56bd8c1 feat(TaskFaceServiceImpl): 对搜索人脸结果进行排序
- 在处理搜索人脸结果时,按分数从高到低进行排序
- 这样可以确保分数较高的结果优先被处理
2025-09-13 14:24:14 +08:00
be2750c162 refactor(task): 优化任务服务中视频文件名生成逻辑
- 在生成文件名时加入 faceId 和 templateId,提高文件名的唯一性和可识别性
-修改 hash 值生成逻辑,增加 faceId 以进一步确保文件名的唯一性
2025-09-12 14:19:22 +08:00
b5b2c12a15 Merge branch 'refs/heads/notify-text' 2025-09-12 09:39:51 +08:00
dc2154c020 feat(integration): 添加 Kafka 消息系统集成
- 新增 Kafka 配置和连接测试功能- 实现人脸处理消息的消费逻辑
- 添加消息发送预留接口
- 优化人脸样本保存和处理流程
2025-09-12 06:38:44 +08:00
2f88699bb0 feat(task): 添加设备照片数量限制功能
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 在任务筛选逻辑中增加设备照片数量限制筛选
- 根据设备配置的 limit_photo 值限制每个设备的照片数量
- 对于未设置限制或限制为 0 的设备,不做筛选
- 对于设置了限制的设备,按创建时间倒序排序,取前 N 张照片- 记录筛选过程的日志信息
2025-09-12 05:38:52 +08:00
c194c169be refactor(task): 重构下载通知任务逻辑
- 引入 ScenicConfigManager 用于获取景区配置信息
- 根据景区配置动态生成通知标题和内容
- 优化了第二次和第三次通知的处理逻辑
- 移除了优惠券相关查询,简化了代码结构
2025-09-11 22:34:32 +08:00
39bd18497c Merge branch 'refs/heads/rem_unused_c' 2025-09-10 19:33:40 +08:00
1d666c076e Merge branch 'refs/heads/fix_n9e_err' 2025-09-10 19:33:25 +08:00
88974d7e9e refactor: 删除 DefaultConfigController 类
删除了 src/main/java/com/ycwl/basic/controller/pc/DefaultConfigController.java 文件。这个类提供了默认配置管理的增删查改
2025-09-10 17:13:36 +08:00
a956c54500 Merge branch '_fix' into fix_n9e_err 2025-09-10 17:09:09 +08:00
a7e5c8cd95 refactor(basic): 优化 N9eSyncTask 中的代码 2025-09-10 17:08:53 +08:00
eaf959e1b8 refactor(face): 重构人脸识别服务逻辑
- 优化了 faceId 参数校验和日志记录
- 重构了人脸识别主流程,增加了异常处理和日志记录
- 新增了人脸识别补救逻辑方法
- 优化了源文件关联、免费逻辑、购买状态处理等方法
- 重构了视频重切逻辑,使其更加清晰- 优化了时间范围筛选逻辑
2025-09-10 17:00:09 +08:00
0c56a7fa67 refactor(basic): 优化 N9E 信息获取逻辑
- 使用 try-with-resources 确保 HttpResponse 资源正确关闭- 添加异常捕获,当请求失败时记录日志并终止方法执行
2025-09-10 16:15:51 +08:00
7839082352 Merge branch 'scenic-microservice'
# Conflicts:
#	src/main/java/com/ycwl/basic/integration/scenic/service/ScenicConfigIntegrationService.java
#	src/main/java/com/ycwl/basic/repository/ScenicRepository.java
2025-09-09 14:04:43 +08:00
32b5b39ea3 refactor: 将日志级别从 info 改为 debug
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
修改了以下文件中的日志级别:- RenderWorkerConfigIntegrationService.java- RenderWorkerIntegrationService.java

将所有 info 级别的日志
2025-09-09 14:01:19 +08:00
670e37e7a6 fix(basic): 修正阿里云 OSS 文件存储域名
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 移除了之前在 DeviceFactory 类中对 AliOssStorageOperator 初始化时添加的 "-internal" 后缀- 此修改解决了因内部域名错误导致的文件存储失败问题
2025-09-09 13:43:03 +08:00
13640c88d3 Merge branch 'fix' 2025-09-09 13:40:50 +08:00
ac91921c28 refactor(device): 优化文件列表获取逻辑
- 移除了不必要的循环遍历,简化了代码结构
- 仅根据起始日期获取一次文件列表,提高了效率- 清除了无用的日历操作,减少了代码复杂性
2025-09-09 13:32:58 +08:00
9e9e245801 fix(task): 调整视频剪辑命令的参数顺序
- 将 '-ss' 参数及其值移动到 '-i' 参数之后
- 优化了 ffmpeg 命令的参数顺序,提高视频处理效率
2025-09-09 13:13:17 +08:00
9587354d0a fix(device): 修复阿里云 OSS 文件检索和连接问题
- 修改 AliOssStorageOperator 中的时间增量,从分钟改为天
- 在 DeviceFactory 中为阿里云 OSS操作器添加域名替换,以解决连接问题
2025-09-09 13:08:42 +08:00
3697093bed refactor(task): 移除视频上传相关代码
- 删除了 TaskTaskServiceImpl 中的视频上传调用
- 移除了 VideoReUploader 中的 addVideoTask 方法和相关代码
- 优化了代码结构,减少了不必要的导入
2025-09-09 12:59:55 +08:00
1e3d2e9e3a perf(task): 调整 FFmpeg 命令参数顺序以优化视频处理
- 将 -ss 参数移到 -i 参数之前,以提高视频处理效率
-这种修改可以减少 FFmpeg 在处理视频时的计算量,从而加快处理速度
2025-09-09 12:52:45 +08:00
c593e0c7e9 fix(task): 修复巡逻时长计算逻辑
- 将 endDate 的计算方式从 0 分钟偏移改为 1 分钟偏移
- 这样可以确保包含第一张人脸采样图片在内的时间区间为 [startDate, endDate)- 从而更准确地计算巡逻时长
2025-09-09 12:18:05 +08:00
a641acee88 refactor(TaskTaskServiceImpl): 添加 DeviceConfigManager 导入
- 在 TaskTaskServiceImpl 类中添加了 DeviceConfigManager 的导入
- 此
2025-09-09 11:41:18 +08:00
ef61ce9d63 Merge branch 'render-worker-microservice'
# Conflicts:
#	src/main/java/com/ycwl/basic/integration/scenic/service/ScenicIntegrationService.java
#	src/main/java/com/ycwl/basic/service/task/impl/TaskTaskServiceImpl.java
2025-09-09 11:00:10 +08:00
aa683a62c4 refactor(task): 优化任务获取逻辑
- 在 TaskTaskServiceImpl 中添加 limit(1) 以限制获取的任务数量为 1
- 在 TaskMapper.xml 中移除 selectNotRunning 查询中的 limit 1条件
2025-09-09 10:57:56 +08:00
5426f61328 fix(task): 修复获取工作者信息逻辑
- 在获取工作者信息时增加状态检查
- 确保只返回状态为 1 的
2025-09-09 10:03:16 +08:00
9ec222a155 Merge branch 'rem_old_price_config' 2025-09-09 09:47:45 +08:00
a8711f6d19 fix(viid): 修复人脸上传适配器为空时的处理逻辑
- 增加了对人脸上传适配器为空的检查,避免空指针异常
- 优化了人脸添加的逻辑,增加了异常捕获和日志记录
- 调整了任务调度的顺序,确保人脸添加成功后再添加任务
2025-09-09 09:46:45 +08:00
7bb2905462 refactor(mapper): 移除订单和资源相关 mapper 中的 scenicName 字段
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 从 OrderMapper.xml 中移除了多个 resultMap 中的 scenicName 字段
-从 SourceMapper.xml 中移除了多个 SQL 查询中的 scenicName 字段- 这些更改可能是为了减少冗余数据或优化数据库查询性能
2025-09-09 02:03:14 +08:00
fd4c708406 refactor mapper: 移除冗余的景点名称字段
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 在 BrokerMapper.xml 中移除了 scenicName 字段的查询
- 在 ExtraDeviceMapper.xml 中移除了 scenic_name 字段的查询
2025-09-09 01:42:12 +08:00
8504e29c80 Merge branch 'rem_scenic_device' 2025-09-09 01:37:52 +08:00
948bd0d2e2 feat(AppOrderV2Controller):兼容旧版本 faceId 参数缺失处理
- 在 faceId 参数缺失时,增加兼容旧版本的处理逻辑
- 根据产品类型和产品 ID 获取 faceId,确保系统能够正常计算价格
2025-09-08 21:37:23 +08:00
ad0888ccc7 feat(task): 添加视频数量限制功能
- 在处理人脸样本时,根据设备配置限制视频数量
- 新增 DeviceConfigManager 的使用,用于获取设备配置信息
- 使用流式处理对人脸样本进行分组和限制数量
2025-09-08 21:11:20 +08:00
48eff52a47 refactor(task): 优化人脸识别时间范围逻辑
- 修改了景区场景下的人脸识别时间范围计算方式
- 从 acceptFaceSampleIds 中筛选出在指定时间范围内的样本
- 优化了代码结构,提高了可读性和可维护性
2025-09-08 20:51:50 +08:00
7d171b5003 refactor(task): 修改任务服务中视频 URL 生成逻辑
-将视频文件名的生成方式从使用任务 ID 和创建时间改为使用任务参数进行 MD5 加密
- 这一改动可以确保即使在任务重新执行时,视频 URL 也能保持一致
2025-09-08 20:33:22 +08:00
bfe84546c6 fix(task): 修复 vlog视频文件名生成逻辑
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 使用 MD5 哈希算法生成唯一的文件名前缀,以避免文件名冲突
- 将任务创建时间加入哈希计算,确保即使任务 ID 相同也能生成不同的文件名
- 修改涉及两个代码块,分别处理视频上传和获取视频 URL 的场景
2025-09-08 20:31:57 +08:00
c28efbbb9c feat(profitsharing, mobile, pc): 为对象添加景区名称信息
- 在 ProfitSharingConfigController 中,为分页查询结果添加景区名称信息
- 在 AppScenicServiceImpl 中,为额外设备列表添加景区名称信息
- 在 SourceServiceImpl 中,为资源响应对象添加景区名称信息

这些修改增强了对象数据,使其包含相关的景区名称,提高了数据的完整性和可读性。
2025-09-08 14:37:53 +08:00
6fb3cb93a9 feat(pricing): 增加商品参数校验和默认值设置
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 为每个产品增加产品类型和产品 ID 的非空校验
- 为购买数量和数量设置默认值为 1,如果未提供
2025-09-08 14:03:15 +08:00
1fbe7c86d5 fix(mobile): 修复商品详情页面景区名称显示错误
- 在 GoodsServiceImpl 中添加 scenicRepository 以获取景区信息
- 在 goodsPageVO 和 goodsDetailVO 中使用 scenicRepository 获取正确的景区名称
- 添加异常处理,确保在获取景区信息失败时不会影响页面显示
2025-09-08 11:29:31 +08:00
c1ca4e8631 feat(template): 添加模板缩放裁剪功能
- 在 TemplateEntity 中添加 zoomCut 字段,用于控制模板的缩放裁剪
- 更新 TemplateMapper.xml,增加 zoom_cut 列的插入和更新逻辑
2025-09-08 10:54:21 +08:00
c1d61f4ed5 refactor(basic): 重构景区配置获取方法并优化配置管理初始化
- 在 ScenicConfigIntegrationService 中使用 fallback 机制封装景区配置获取方法
- 优化 ScenicRepository 中的 getScenicConfigManager 方法,提高代码健壮性
2025-09-08 10:47:30 +08:00
c3101ceb6b feat(task): 优化任务分发逻辑
- 新增自托管景点缓存机制,减少重复查询
- 修改任务分配逻辑,排除自托管景点的任务
- 优化景点唯一性配置的读取方式
2025-09-08 10:47:07 +08:00
29637bc5e5 refactor(biz): 重构Vlog 视频价格计算逻辑
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 移除 OrderBiz 中的固定价格设置,改为调用价格计算服务
- 删除 PriceBiz 中未使用的 IOnePricePurchaseService 注入
- 优化 AppOrderV2Controller 中的产品数量设置逻辑
- 新增 VideoTaskRepository 中的 getTaskDeviceCount 方法,用于获取任务设备数量
- 调整 GoodsServiceImpl 和 OrderServiceImpl 中的相关代码,使用新的设备数量获取方法
2025-09-08 10:11:19 +08:00
4ee79b5db8 refactor: 删除渲染机管理相关代码
- 移除 RenderWorkerController、RenderWorkerMapper、RenderWorkerEntity、RenderWorkerService 等类
- 删除相关的 XML 配置文件
- 清理数据库表结构
2025-09-08 00:12:18 +08:00
502eca10f6 refactor(integration): 重构渲染工作器相关 DTO 类的字段命名
- 将蛇形命名法(snake_case)改为驼峰命名法(camelCase)
- 更新了以下几个 DTO 类的字段名: - RenderWorkerConfigV2DTO  - CreateRenderWorkerRequest - RenderWorkerV2DTO - RenderWorkerV2WithConfigDTO
  - UpdateRenderWorkerRequest- 主要更改集中在配置键、值、类型以及时间相关字段上
2025-09-07 14:57:47 +08:00
e574f49177 refactor(integration): 重构设备和服务配置
- 修改 DefaultConfigIntegrationService 类名以更准确地反映其功能
- 移除不必要的导入和注解
- 统一命名规范

# Conflicts:
#	src/main/java/com/ycwl/basic/integration/questionnaire/config/QuestionnaireIntegrationConfig.java
2025-09-07 14:45:21 +08:00
7696c934b1 feat(pc): 重构渲染工作器管理接口并添加配置管理功能- 重新设计了渲染工作器管理接口,简化了操作流程- 添加了渲染工作器配置管理相关接口,包括创建、更新、删除等操作
- 优化了代码结构,提高了可维护性和可扩展性
2025-09-07 14:43:36 +08:00
c4acdc576a refactor(pc): 重构价格配置相关代码
- 移除了 PriceConfigController 中的冗余方法- 删除了 VideoController 中的 @Deprecated 注解
- 移除了 PriceConfigEntity 中的 @TableName 注解
- 重构了 PriceRepository 中的 getPriceByScenicTypeGoods 方法
- 删除了 PriceConfigServiceImpl 和 PriceConfigService 接口- 移除了 PriceConfigMapper接口和对应的 XML 文件
2025-09-07 14:31:35 +08:00
90dc7fea70 feat(VideoService): 视频查询接口增加景区名称
- 在 VideoServiceImpl 中添加景区名称查询功能
- 使用 ScenicRepository 批量获取景区名称
- 在视频列表中添加景区名称字段
2025-09-07 14:28:42 +08:00
e9e59cd33e feat(service): 批量设置景区名称
- 在多个服务实现类中添加批量获取景区名称的方法- 优化了景区名称的设置逻辑,提高了查询效率
-涉及的服务包括:
  - ProfitSharingRecordServiceImpl
  - BrokerServiceImpl
  - CouponServiceImpl - SourceServiceImpl
  - TemplateServiceImpl
2025-09-07 12:12:22 +08:00
5a89a7c60a feat(service): 批量获取景区和设备信息
- 在 DeviceRepository 中添加批量获取设备信息的方法
- 在 ScenicRepository 中添加批量获取景区信息的方法
- 修改 OrderServiceImpl,使用批量方法获取景区名称
- 移除多个 mapper 文件中冗余的景区信息查询
2025-09-07 01:42:38 +08:00
24f692b69a refactor(questionnaire): 调整问卷相关请求对象结构
- 移除 SubmitAnswerRequest 中的 @NotNull 注解
- 在 CreateQuestionRequest 中添加 id 字段
2025-09-06 21:46:06 +08:00
d9a2da49bb feat(template): 添加模板详情中的缩放裁剪功能
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 在 TemplateRespVO 类中添加 zoomCut 字段
- 用于表示模板是否支持缩放裁剪功能
2025-09-06 16:57:18 +08:00
0aa834bdfa refactor(log): 修改日志级别并优化日志输出
-将 info 日志级别改为 debug 日志级别
- 在 DefaultConfigIntegrationService 中添加了获取默认配置列表和指定默认配置的日志输出- 优化了部分日志信息的描述,使其更加详细
2025-09-06 15:46:30 +08:00
b9c65cf030 feat(mobile): 添加移动端问卷接口
- 新增 AppQuestionnaireController 控制器,提供移动端问卷相关接口
- 实现问卷详情获取和问卷答案提交两个主要功能
- 集成 QuestionnaireIntegrationService 服务进行问卷数据处理
- 使用 ApiResponse 统一接口返回格式
- 添加日志记录和异常处理,提高系统稳定性
2025-09-06 15:36:37 +08:00
58488d2cde refactor(integration): 重构设备和服务配置
- 修改 DefaultConfigIntegrationService 类名以更准确地反映其功能
- 移除不必要的导入和注解
- 统一命名规范
2025-09-06 15:35:42 +08:00
32f7660dc0 feat(questionnaire): 新增问卷管理 V2 版本接口
- 添加了新的 QuestionnaireV2Controller 类,实现了问卷管理的 CRUD操作
- 新增了问卷答案查看和统计功能相关接口
- 重构了 ResponseDetailResponse 类,将 AnswerDetailResponse 类独立出来- 简化了 CreateQuestionOptionRequest 类的结构
2025-09-06 01:09:17 +08:00
180f89042c refactor(questionnaire): 重构问卷列表和回答记录列表的响应结构
- 将 QuestionnaireListResponse 和 ResponseListResponse 类移除
- 使用泛型化的 PageResponse 类作为列表响应的基类
- 更新相关接口和方法的返回类型
- 调整示例代码和测试用例
2025-09-06 01:04:15 +08:00
a49450b795 feat(integration): 添加问卷服务集成模块
- 新增问卷服务配置和客户端接口
- 实现问卷创建、查询、提交答案和统计分析等功能
- 添加问卷集成示例,演示各项功能的使用- 设计并实现问卷服务的 fallback 缓存管理策略
2025-09-06 00:19:48 +08:00
d7c6ce9f40 refactor(basic): 重构渲染机相关代码
- 移除了 RenderWorkerMapper 中的未使用的接口
- 精简了 RenderWorkerEntity 中的字段
-重构了 RenderWorkerRepository 中的缓存逻辑
- 更新了 RenderWorkerService 接口和实现类,使用新的 RenderWorkerRespVO 响应对象
- 调整了 TaskTaskServiceImpl 中的渲染机相关代码,使用新的配置管理方式
2025-09-06 00:18:50 +08:00
ffad1c9f59 refactor(device): 重构默认配置接口返回类型
- 将 DefaultConfigClient 中的 listDefaultConfigs 方法返回类型由 PageResponse 改为 CommonResponse<PageResponse>
- 更新 DefaultConfigIntegrationService 中的调用方式
- 移除 handlePageResponse 方法,改为使用 handleResponse 方法处理响应
2025-09-05 16:51:47 +08:00
6039f337cb feat(price): 增加一价全包价格配置支持
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 在 PriceBiz 和 PriceRepository 中添加对一价全包价格配置的处理逻辑
- 通过 IOnePricePurchaseService 获取一价全包配置信息
- 在查询商品列表时,增加对一价全包配置的判断和处理
-优化分享逻辑,修复了部分情况下分享状态不正确的问题
2025-09-05 16:37:50 +08:00
c9f7080615 refactor(device): 优化默认配置示例代码
- 移除了不必要的 getData() 调用,直接使用 getList() 和 getTotal() 方法
- 使用 Java 8 Stream API 简化了部分代码,提高了可读性
- 优化了配置类型分布统计逻辑,使用更简洁的方式
2025-09-05 15:48:54 +08:00
7b22baeb66 refactor(device): 更新验证注解导入
- 将 javax.validation相关的导入语句替换为 jakarta.validation
- 此更改是为了适应 Jakarta EE 规范的最新版本
2025-09-05 15:48:03 +08:00
13bd60f24b feat(pricing): 新增景区一口价配置查询功能
- 在 IOnePricePurchaseService 接口中添加 getActiveConfigByScenic 方法- 在 OnePricePurchaseServiceImpl 类中实现该方法
- 方法用于查询指定景区的启用的一口价配置,通常每个景区只有一个生效配置
2025-09-05 15:27:28 +08:00
1b1e5f1690 refactor(pricing): 将 PriceOnePriceConfig 中的 LocalDateTime 类型替换为 Date 类型
-将 createTime、updateTime 和 deletedAt 字段的类型从 LocalDateTime 改为 Date
- 此修改统一了时间类型的使用,提高了代码的一致性和可维护性
2025-09-05 15:27:06 +08:00
b2a95ed862 feat(integration): 添加渲染工作器配置管理功能
- 新增 RenderWorkerConfigManager 类实现渲染工作器配置的管理功能
- 在 RenderWorkerRepository 中集成 RenderWorkerConfigManager
- 添加方法 getWorkerConfigManager 获取渲染工作器配置管理器实例
- 优化 getWorkerByAccessKey 和 getWorker 方法,使用集成服务获取工作器信息
2025-09-05 14:49:19 +08:00
933818d458 feat(device): 添加默认配置管理功能
- 新增 DefaultConfigClient接口,用于与设备微服务进行默认配置相关的操作
- 实现 DefaultConfigIntegrationService 类,提供默认配置管理的高阶服务- 添加批量配置请求构建器 BatchDefaultConfigRequestBuilder,简化批量操作
- 新增 DefaultConfigIntegrationExample 示例类,演示默认配置管理的使用方法
- 更新 CLAUDE.md 文档,增加默认配置管理的详细使用说明和示例代码
2025-09-05 14:49:06 +08:00
933a1209e7 refactor(render): 重构渲染工作器列表接口和 DTO
- 更新 RenderWorkerV2Client 中的 listWorkers 和 listWorkersWithConfig 方法返回类型
- 删除 RenderWorkerV2ListResponse 和 RenderWorkerV2WithConfigListResponse 类
- 更新 CreateRenderWorkerRequest 中的导入路径- 重构 RenderWorkerIntegrationService 中的 listWorkers 和 listWorkersWithConfig 方法
2025-09-05 12:17:44 +08:00
aa4a6c29c6 refactor(device): 重构设备列表接口返回类型
- 将 DeviceV2ListResponse 和 DeviceV2WithConfigListResponse 替换为通用的 PageResponse 类
- 更新相关控制器、服务和客户端接口以使用新的返回类型
- 删除冗余的 DeviceV2ListResponse 和 DeviceV2WithConfigListResponse 类
- 调整 FilterDevicesByConfigsResponse 中的 total 字段类型
2025-09-05 12:17:33 +08:00
60ce65f3e4 feat(integration): 添加渲染工作器服务集成
- 新增 RenderWorkerConfigV2Client 和 RenderWorkerV2Client 接口
- 实现 RenderWorkerConfigIntegrationService 和 RenderWorkerIntegrationService 服务类
- 添加相关 DTO 类和 BatchConfigBuilder 工具类
- 在 IntegrationProperties 中增加 render 相关配置
- 更新 CommonResponse 类,增加 success 字段
- 新增 RenderWorkerIntegrationConfig 配置类
2025-09-05 11:46:19 +08:00
50c84ac1c9 feat(pricing): 添加一口价购买功能
- 新增 OnePricePurchaseController 控制器
- 新增 OnePriceConfigFilterRequest、OnePriceConfigRequest、OnePriceInfo等 DTO 类
- 新增 PriceOnePriceConfig 实体类和对应的 Mapper 接口
- 实现 OnePricePurchaseDiscountProvider 优惠提供者
- 实现 OnePricePurchaseServiceImpl 服务实现类
-定义 IOnePricePurchaseService服务接口
- 优化 DiscountDetail 类,添加创建一口价折扣的方法
- 修改 CLAUDE.md,将 error 方法改为 fail 方法
2025-09-05 11:09:54 +08:00
5210b50adb feat(pricing): 增加商品和打包配置的优惠券及券码使用限制
- 在 PriceBundleConfig 和 PriceProductConfig 中添加是否可使用优惠券和券码的字段
- 修改 CouponDiscountProvider 和 VoucherDiscountProvider,增加对商品和打包配置的检查
- 更新 PriceCalculationServiceImpl 中的优惠计算逻辑,将一口价改为打包购买
- 调整 DiscountDetail 中的描述和排序顺序,以适应新的优惠方式
2025-09-05 11:09:28 +08:00
bd077b9252 Merge branch 'refs/heads/master' into price_inquery 2025-09-04 17:03:26 +08:00
0a13bd8b12 refactor(device): 将设备列表和景点筛选接口中的 total 类型从 Long 改为 Integer
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 修改了 DeviceV2ListResponse、DeviceV2WithConfigListResponse 和 ScenicFilterPageResponse 类中的 total 字段类型
- 从 Long 改为 Integer,以确保数据类型一致性并可能提高性能
2025-09-04 17:00:35 +08:00
5b757eda8d refactor(basic): 重构设备配置获取逻辑
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 修改 getDeviceConfig 方法,使用 DeviceConfigManager替代 List<DeviceConfigV2DTO>
- 更新 convertToDeviceConfigEntity 方法,使用 DeviceConfigManager 作为参数
- 在 convertToDeviceConfigEntity 方法中实现从 configManager 到 deviceConfigEntity 的字段映射
2025-09-04 16:33:29 +08:00
1cce9168c1 fix(basic): 修复获取景区配置时的空指针异常- 在 ScenicRepository 类中添加了对 scenicConfigManager 的空值检查
- 如果 scenicConfigManager为 null,则直接返回当前配置,避免空指针异常
2025-09-04 16:22:22 +08:00
b6efe7b7da fix(AppScenicServiceImpl): 修复查询景区列表时的状态筛选问题- 在查询景区列表时添加了状态筛选条件,只返回状态为"1"的景区
-这个修改确保了移动应用接口返回的景区列表中只包含有效(已发布)的景区
2025-09-04 16:18:57 +08:00
dbe0447987 refactor(pc): 移除日志记录并优化数据查询
- 移除了多个控制器和服务类中的冗余日志记录
- 在查询数据时,不再通过 SQL左连接直接获取景点和设备名称,而是使用 Repository 单独查询
- 更新了 FaceSampleMapper、
2025-09-04 15:57:18 +08:00
4427c7fde1 build(pom): 移除 Sonatype Nexus Staging 仓库配置
- 从 pom.xml 文件中删除了 Sonatype Nexus Staging 仓库的配置
- 此修改简化了仓库配置,可能影响项目的发布流程
2025-09-04 14:59:06 +08:00
480e40d78c Merge branch 'device-microservice'
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
2025-09-04 12:38:29 +08:00
4d70c33650 feat(device): 添加设备配置筛选功能
- 新增 DeviceV2Client 接口中的 filterDevicesByConfigs 方法
- 创建 ConfigFilter、FilterDevicesByConfigsRequest 和 FilterDevicesByConfigsResponse 类
- 实现 DeviceIntegrationService 中的 filterDevicesByConfigs 和 findDevicesWithMissingConfig 方法- 添加 DeviceFilterExample 类,展示设备配置筛选功能的使用示例
2025-09-04 12:38:13 +08:00
3b8a33c8eb refactor(device): 优化时间格式处理
- 移除了 DeviceFactory 和 DeviceRepository 中将 LocalDateTime转换为 Date 的代码
- 更新了 DeviceConfigV2DTO 和 DeviceV2DTO,将 createTime 和 updateTime 字段从 LocalDateTime 改为 Date
- 现在使用 @JsonFormat 注解来处理日期格式的序列化和反序列化
2025-09-04 12:36:44 +08:00
8ad999f779 Merge branch 'device-microservice'
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
# Conflicts:
#	src/main/java/com/ycwl/basic/integration/scenic/service/ScenicConfigIntegrationService.java
#	src/main/java/com/ycwl/basic/integration/scenic/service/ScenicIntegrationService.java
2025-09-04 12:28:32 +08:00
bbfc61d75a refactor: 删除 DeviceMapper 接口及其对应的 XML 文件
- 删除了 DeviceMapper.java 文件,包括所有与设备管理相关的接口方法
- 删除了 DeviceMapper.xml 文件,包括所有与设备管理相关的 SQL 语句
- 此次重构移除了设备管理功能模块的持久层代码
2025-09-04 10:03:45 +08:00
7779b84c81 feat(device): 集成 zt-device 服务
- 移除 DeviceController、DeviceService 相关代码
- 更新 ViidController、WvpController 使用 DeviceIntegrationService
- 修改 DeviceFactory 创建 DeviceEntity 的方式
- 更新 DeviceRepository 使用 DeviceV2DTO
-调整 CustomUploadTaskService、AppScenicServiceImpl 中的设备相关逻辑
- 移除 DeviceServiceImpl 类
- 更新 VideoPieceCleaner、VideoPieceGetter 任务类,使用 DeviceIntegrationService 获取设备信息
2025-09-04 10:03:00 +08:00
9a086fc86d refactor(device): 重构设备配置获取逻辑
- 在 ViidController 中添加获取 DeviceConfigEntity 的逻辑
- 在 VideoPieceGetter 中使用 DeviceConfigManager 替代 DeviceConfigEntity
- 优化设备配置参数的获取方式,使用 getBigDecimal 和 getString 方法
- 移除未使用的代码片段,提高代码可读性
2025-09-03 17:32:14 +08:00
657eb482bb feat(order): 添加face相关分享逻辑
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 在OrderBiz和PriceBiz中增加face相关分享逻辑判断
- 在IsBuyBatchRespVO和IsBuyRespVO中添加share字段用于表示是否可分享
2025-09-03 16:45:00 +08:00
7d2666128a refactor(pricing): 优化代金券可用性判断逻辑
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 移除了对 canClaimVoucher 方法的调用,暂时注释掉以简化逻辑
- 此修改可能会影响代金券领取功能
2025-09-03 14:35:10 +08:00
5c416f6c09 compatible 2025-09-03 14:35:00 +08:00
829ab50b03 feat(voucher): 优化优惠券打印逻辑
- 添加景区配置管理器验证逻辑,确保只有指定的代理可以打印优惠券
-优化代码结构,提高可读性和可维护性
2025-09-03 14:34:42 +08:00
03fd80a313 fix(order): 修复零元订单状态设置逻辑
- 将订单状态设置逻辑移出 promo code块
- 确保所有支付价格为零的订单都设置为已支付状态
2025-09-03 14:33:36 +08:00
cf829bec2f fix(voucher): 更新代金券状态逻辑
- 将更新条件从 status = 1 修改为 (status = 1 OR status =0)
- 这样可以同时更新未使用和已使用的代金券状态
2025-09-03 14:13:33 +08:00
cbeaee751f refactor(mobile): 移除 AppOrderV2Controller 中的缓存获取逻辑
- 删除了从 Redis 缓存获取价格计算结果的相关代码
- 优化了订单处理流程,不再依赖缓存数据
2025-09-03 14:12:50 +08:00
bea5255927 feat(order): 添加零金额订单处理逻辑
- 在 getPaymentParams 方法中增加对订单金额的检查- 如果订单金额为 0 或空,则自动将订单状态设置为已支付
- 更新数据库中的订单信息
- 返回一个特殊的 PaymentParamsResponse,表示免费订单
-非零金额订单则按原有流程处理
2025-09-03 08:31:45 +08:00
982e9180f1 feat(ConfigManager): 重构配置管理器功能
- 新增 getObject、getMap、getList 等方法,增强配置值获取和转换能力
- 支持 JSON 字符串自动反序列化为指定类型对象
- 优化配置值处理逻辑,提高代码复用性和可维护性
- 移除 ScenicConfigManager 中的冗余方法
2025-09-02 23:49:15 +08:00
0ff0b75910 feat(image): 新增图像超分辨率增强功能
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 新增 BceImageSR 类实现图像超分辨率增强
- 修改 BceImageEnhancer 类中的增强方法
-增加图像超分辨率增强的相关配置和接口
2025-09-02 19:24:00 +08:00
aa717d0c2a feat(pricing): 后端新增分页查询接口
- 新增了三个分页查询接口:商品配置、阶梯配置和一口价配置
- 实现了对应的筛选条件和排序功能
- 添加了新的请求DTO类:PricingFilterRequest、TierConfigFilterRequest和BundleConfigFilterRequest
- 修改了服务接口和实现类,增加了分页查询相关的方法
2025-09-02 19:23:22 +08:00
3efad90750 refactor(device-config): 重构设备配置获取方式
- 将 DeviceConfigEntity 替换为 DeviceConfigManager
- 优化设备配置的获取逻辑,使用 getInteger 和 getLong 方法
- 移除未使用的代码块,提高代码可读性
- 统一设备配置的处理方式,提高代码维护性
2025-09-02 16:15:35 +08:00
35b2e7c655 feat(ConfigManager): 添加获取长整型配置值的方法
- 新增 getLong 方法,用于获取长整型配置值
- 增加 getLong 方法的重载版本,支持返回默认值
-支持处理不同类型的配置值,包括 Long、Number 和 String
2025-09-02 16:14:01 +08:00
25b912c7af refactor(DeviceConfigManager): 移除未使用的设备配置快捷方法- 删除了多个未使用的设备配置获取方法,包括 IP 地址、分辨率、帧率等
- 简化了 DeviceConfigManager 类的结构,提高了代码的可维护性
2025-09-02 16:13:43 +08:00
8e770a5b97 refactor(integration): 重构配置管理功能
- 新增通用 ConfigManager 类,实现配置管理的通用功能
- 新增 DeviceConfigManager 和 ScenicConfigManager 类,分别实现设备和景区的配置管理- 更新相关控制器和服务,使用新的配置管理器类
-调整设备和景区的配置数据结构,以适应新的管理方式
2025-09-02 15:30:54 +08:00
2dee78247e refactor(repository): 移除设备和景点相关缓存逻辑
- 删除了 DeviceRepository 和 ScenicRepository 中的缓存相关代码
- 移除了成功结果缓存和错误降级逻辑
-简化了设备和景点信息获取方法,直接调用服务接口返回结果
2025-09-02 12:27:51 +08:00
8c8a6baa5e refactor(integration): 重构集成服务的降级机制
-移除各服务自定义的降级服务类,统一降级逻辑
- 新增 IntegrationFallbackService作为通用降级服务
- 更新设备和景区服务的降级处理方式
- 优化降级缓存管理,增加统计信息和批量清理功能
- 调整 API 接口,移除扁平化批量更新等相关方法
2025-09-02 12:24:55 +08:00
d35a1facbd refactor(device): 重构设备相关接口并添加缓存机制
- 为 DeviceConfigIntegrationService 和 DeviceIntegrationService 添加 fallback 服务
- 为设备配置和信息获取方法添加缓存逻辑
- 移除冗余的设备配置设置方法
- 优化设备信息和配置的获取流程
2025-09-02 11:35:15 +08:00
dac3b8d847 refactor(integration): 更新设备集成配置类注解
- 移除 @ConditionalOnProperty 注解
- 添加 @ConfigurationProperties 注解
2025-09-02 11:07:07 +08:00
ec24464cba feat(device): 新增设备管理V2 版本接口
- 添加设备基础 CRUD 操作接口
- 实现设备配置管理相关接口- 提供景区设备管理功能接口
- 优化参数验证和错误处理
2025-09-02 01:43:31 +08:00
b475e38018 feat(device): 添加设备排序功能并优化示例代码
- 在 CreateDeviceRequest 和 UpdateDeviceRequest 中添加 sort 字段
- 在 DeviceV2DTO 中添加 sort 属性- 更新 DeviceIntegrationExample 中的示例代码,演示设备排序功能- 新增设备排序相关的服务方法,如 createIpcDeviceWithSort 和 updateDeviceSort
- 优化 runAllExamples 方法,移除部分冗余示例
- 新增 runBasicExamples 方法,用于运行基础示例
2025-09-02 01:32:12 +08:00
ad7d1042f4 feat(device): 新增批量配置设备参数接口
- 新增 BatchUpdateResponse 和 ProcessedConfigItem 类用于批量更新响应
- 修改 DeviceConfigV2Client 接口返回类型为 BatchUpdateResponse
- 在 DeviceConfigIntegrationService 中实现新的批量更新逻辑
- 更新 DeviceIntegrationExample 和 CLAUDE.md 文档,添加新的批量配置示例
2025-09-01 23:15:02 +08:00
0bcf2aaccf refactor(device): 重构设备信息获取逻辑,增加缓存降级策略
- 新增 DeviceV2DTO 转换为 DeviceEntity 的方法
- 引入成功结果缓存,用于失败时降级
- 优化 getDevice 和 getDeviceByDeviceNo 方法,增加异常处理和缓存逻辑
- 清理缓存时增加成功结果缓存的清理
2025-09-01 23:14:45 +08:00
98e5c3dc39 fix(device): 移除未配置 deviceNo 时的被动存储查询
- 在 VptPassiveStorageOperator 和 WvpPassiveStorageOperator 中添加日志警告
- 返回空列表以避免使用未配置的 deviceNo 进行查询
- 移除 DeviceEntity 中的 no2 字段
- 更新 DeviceServiceImpl 中的缓存清除逻辑
2025-09-01 18:45:26 +08:00
e4da509964 perf(basic): 调整价格缓存过期时间为 10 分钟
- 将 PriceCacheService 类中的 CACHE_DURATION常量值从 5 分钟修改为 10 分钟
- 此修改旨在减少缓存刷新频率,提高系统性能
2025-09-01 17:39:26 +08:00
9426d9c712 refactor(scenic): 移除日志记录语句
移除了 DefaultConfigIntegrationService、ScenicConfigIntegrationService 和 ScenicIntegrationService 类中的日志记录语句。这些日志记录语句在每个方法开始处记录了方法的输入参数,为了简化代码结构和减少日志输出,决定移除这些日志记录语句。
2025-09-01 17:31:09 +08:00
8745cde2fb refactor(scenic): 重构批量配置请求参数结构
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 将 BatchConfigRequest 中的 BatchConfigItem 类移除
- 使用 UpdateConfigRequest 替代 BatchConfigItem- 在 UpdateConfigRequest 中添加 configType 字段
- 更新相关代码以适应新的请求参数结构
2025-09-01 17:19:22 +08:00
1d5e4562f2 fix(biz): 修复支付宝商品预览价格计算逻辑
-增加对 face 对象非空的判断,提高代码健壮性
- 修改 faceId 的设置逻辑,使用 goodsId 替代,确保正确传递参数
2025-09-01 17:11:23 +08:00
cdd434317f feat(pricing): 集成定价服务并优化价格查询逻辑- 在 OrderBiz 中添加 IPriceCalculationService 依赖,用于计算价格
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 重构 queryPrice 方法,使用定价服务计算价格而不是直接从数据库读取
- 移除 PriceObj 中未使用的 scenicAllPrice 字段
- 删除 ScenicEntity 和 ScenicAddOrUpdateReq 中的冗余价格字段
-优化 ProductConfigServiceImpl 中的 getTierConfig 方法,增加参数校验
2025-09-01 09:21:26 +08:00
f91b98c68e feat(order): 增加重复购买检查功能
- 为VLOG_VIDEO、RECORDING_SET 和 PHOTO_SET 类型的产品添加重复购买检查
- 使用 sourceMapper 和 videoMapper 查询用户已购买的产品数量- 根据查询结果设置产品数量,避免重复购买
-优化了价格计算流程,先检查缓存再进行重复购买检查
2025-08-31 14:36:24 +08:00
d34603062a feat(integration): 添加设备服务集成模块
- 新增设备服务配置和相关客户端接口
- 实现设备和设备配置的管理功能- 添加设备监控和状态管理示例
- 优化错误处理和故障恢复机制
2025-08-30 23:30:12 +08:00
a9d64402f2 refactor(scenic): 将时间戳类型改为 Date 类型
- 在 ScenicConfigV2DTO 和 ScenicV2DTO 类中,将 createTime 和 updateTime 字段的类型从 Long 改为 Date
- 这个改动可以更方便地进行时间操作和格式化,提高代码的可读性和易用性
2025-08-30 18:09:42 +08:00
ada7158a48 refactor(basic): 重构景区配置管理逻辑
- 移除 AppOrderV2Controller 中的价格缓存逻辑
- 修正 VoucherServiceImpl 中的购买数量计算方式- 重构 ScenicRepository 中的景区配置获取逻辑
-增加 ScenicConfigManager 的扁平化配置和驼峰转换功能
2025-08-30 16:21:40 +08:00
047feec045 refactor(pricing): 重构适用商品类型处理逻辑
- 移除 ProductTypeListTypeHandler,直接在实体类中处理 JSON转换
- 为 PriceVoucherBatchConfig 添加 ObjectMapper 静态实例和日志记录
- 实现 JSON 字符串与 ProductType 列表之间的转换方法- 更新数据库映射,将 applicableProducts 映射为 JSON 字符串
- 优化 VoucherServiceImpl 中的产品适用性检查逻辑
2025-08-30 15:55:26 +08:00
966568156c feat(voucher): 增加券码适用商品类型功能
- 在 VoucherBatchCreateReq、VoucherBatchResp 和 VoucherInfo 中添加适用商品类型列表字段
- 在 PriceVoucherBatchConfig 中添加适用商品类型列表字段,并使用 ProductTypeListTypeHandler 进行 JSON 序列化和反序列化
- 实现 ProductTypeListTypeHandler 以处理商品类型列表的 JSON 序列化和反序列化
- 更新 VoucherBatchServiceImpl 和 VoucherServiceImpl 以支持适用商品类型的筛选和计算
2025-08-30 15:31:35 +08:00
57b087a4fb refactor(order): 重构订单创建和支付参数获取逻辑
- 新增 createOrderCompact 方法实现旧版订单创建逻辑
- 新增 getPaymentParams 方法获取支付参数
- 更新 AppOrderV2Controller调用新的订单创建和支付参数获取方法
- 在 OrderMapper 中添加 getOrderItems 方法获取订单详情- 更新 VideoRepository 接口,增加根据人脸和模板 ID 获取视频列表的方法
- 在 OrderServiceImpl 中实现新的订单创建和支付参数获取逻辑
- 更新 OrderService 接口,添加新的方法声明
- 在 OrderMapper.xml 中添加新的 SQL 查询语句
2025-08-30 14:25:28 +08:00
607c5bc057 refactor(print): 优化门票打印逻辑
- 移除了 AppClaimController 中的重复代码
- 在 VoucherPrintServiceImpl 中增加了打印配置的判断和警告日志
- 保留了 FeiETicketPrinter 的调用逻辑,增加了配置判断
2025-08-30 13:18:26 +08:00
fc8818a595 feat(voucher): 电子凭证打印增加预约功能
- 在 AppClaimController 中添加了对 morphId 的非空判断,只有在 morphId 存在时才进行打印操作
- 在 VoucherPrintServiceImpl 中增加了景点配置的检查,包括预约功能是否启用和指定的经纪人 ID
2025-08-30 12:59:03 +08:00
b1deabc7c1 feat(pricing): 新增打印小票和查询券码批次配置功能
- 新增 AppClaimController 控制器处理移动设备端的领券请求
- 实现 ClaimReq 和 ClaimResp 模型类用于领券请求和响应
- 在 VoucherPrintService 接口中新增打印小票方法
- 在VoucherPrintServiceImpl 中实现打印小票和查询券码批次配置的逻辑
- 更新 PriceVoucherBatchConfigMapper 接口和 XML 文件,添加查询券码批次配置的方法
2025-08-30 12:52:08 +08:00
1ac375e491 refactor(pricing): 移除商品阶梯定价中的 default 配置逻辑
- 删除了尝试使用 default 配置的代码块
- 保留了缓存注释(已注释)
- 优化了日志输出,当找不到配置时直接记录警告日志
2025-08-30 11:31:26 +08:00
60af636639 Merge branch 'refs/heads/order_v2'
# Conflicts:
#	src/main/java/com/ycwl/basic/controller/mobile/manage/AppScenicAccountController.java
2025-08-30 10:53:11 +08:00
792deb5c4d feat(order): 添加重复购买检测功能
- 新增 DuplicatePurchaseException 类用于处理重复购买异常
- 在 OrderServiceImpl 中实现重复购买检查逻辑
- 更新 CustomExceptionHandle 以处理新的重复购买异常
-优化订单创建流程,在生成订单号前增加重复购买检查
2025-08-30 10:52:26 +08:00
93a424058a feat(order): 优化订单创建逻辑,增加商品价格和名称计算
- 新增 calculateProductItemPriceAndName 方法,用于重新计算商品价格信息并获取商品名称
- 更新订单创建流程,使用计算后的商品价格和名称信息
- 引入 IProductConfigService 接口,用于获取商品配置信息
- 优化异常处理,确保在价格计算失败时有兜底方案
2025-08-29 17:39:52 +08:00
98ae9f2930 refactor(order): 重构订单相关代码并优化商品哈希计算逻辑
- 修改 DiscountType 枚举,将 FLASH_SALE 改为 LIMITED_TIME
- 优化 OrderServiceImpl 中的商品信息设置逻辑,增加空值判断
- 更新 IDiscountProvider 接口和 FlashSaleDiscountProvider 类中的提供者类型标识- 优化 ScenicServiceImpl 中的字符串判空逻辑,使用 Strings.isNotBlank 方法
- 重构 PriceCacheService 中的商品列表哈希值计算逻辑,仅基于必传字段生成哈希
2025-08-29 16:54:46 +08:00
e2b760caab feat(order): 完善订单创建和支付流程
- 添加优惠券和券码的使用记录及状态更新
- 优化支付成功、取消和退款的处理逻辑
- 增加异常处理,确保事务一致性
2025-08-29 16:20:07 +08:00
5a66856e72 feat(order): 添加支付相关接口和功能
- 新增获取支付参数接口和处理支付回调接口
- 实现支付参数获取和支付回调处理的逻辑
- 添加支付相关数据传输对象(DTO)
- 修改订单服务接口和实现类,增加支付相关方法
2025-08-29 15:32:47 +08:00
bc2b2fb10f refactor(basic): 修改移动端下单接口路径
- 将 "/add-order" 路径修改为 "/add"
- 优化接口路径,使其更简洁
2025-08-29 14:51:24 +08:00
4dac46bb46 refactor(order): 调整优惠排序逻辑
- 将券码优惠的排序顺序从 1 调整为 2,使其显示顺序低于限时立减- 将限时立减优惠的排序顺序从 2 调整为 1,使其显示在最前面
2025-08-29 14:50:49 +08:00
3fbfb7df54 feat(coupon): 添加优惠券领取功能
- 新增 CouponClaimRequest 和 CouponClaimResult 类用于处理优惠券领取请求和结果
- 在 ICouponService 接口中添加 claimCoupon 方法
- 在 CouponServiceImpl 中实现 claimCoupon 方法,包括参数验证、优惠券查询、库存检查、记录创建等步骤
- 优化日志记录和异常处理
2025-08-29 13:49:30 +08:00
346c484cbc refactor(order): 将 OrderMapper 重命名为 OrderV2Mapper- 将 OrderMapper 接口重命名为 OrderV2Mapper
- 更新了相关服务类中的 Mapper引用
- 修改了 OrderServiceImpl 中的字段名从 orderMapper改为 orderV2Mapper
- 更新了与订单相关的所有方法中对 Mapper 的调用
2025-08-29 12:41:12 +08:00
e95e0a04ff feat(order): 新增订单管理功能 V2
- 新增订单创建、查询、备注更新、申请退款等接口
- 添加订单相关实体类和枚举类
- 实现订单事件监听器,处理支付、退款、订单状态变化
- 优化移动端订单创建逻辑,集成订单服务
2025-08-28 18:42:47 +08:00
af79a5ffa6 feat(basic): 新增移动端下单请求DTO和价格缓存服务- 创建 MobileOrderRequest 类用于移动端下单请求
- 实现 PriceCacheService 类提供价格缓存相关功能
- 使用 Redis 缓存价格计算结果,提高查询效率
2025-08-28 18:14:34 +08:00
5c2629237e feat(mobile): 新增移动端订单V2接口
- 添加 AppOrderV2Controller 控制器,实现移动端价格计算和下单功能
- 新增 MobilePriceCalculationRequest DTO 类,用于移动端价格计算请求- 集成 Redis 缓存机制,提升价格查询性能- 实现人脸权限验证和价格缓存验证逻辑
- 优化日志记录和异常处理
2025-08-28 18:13:59 +08:00
798ff3b9b5 feat(service): 使用带TTL的缓存Map替换静态Map
- 新增TtlCacheMap类,用于实现带生存时间的缓存
- 在ScenicServiceImpl中使用TtlCacheMap替换原有的ConcurrentHashMap
- 为不同类型的适配器创建了对应的缓存Map
- 优化了缓存获取逻辑,增加了TTL支持
- 添加了缓存清理和统计功能
2025-08-28 16:02:30 +08:00
46fb255e66 refactor(pc): 重构景区配置管理
- 引入 ScenicConfigManager 类替代 ScenicConfigEntity
- 优化景区存储、临时存储、本地存储、人脸身体识别和支付适配器的获取逻辑
- 使用 getString 和 getObject 方法替代直接解析 JSON 对象
2025-08-28 15:57:36 +08:00
c03678bd93 feat(mobile): 添加景区账号注册功能
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 新增注册接口和相关请求/响应对象
- 实现手机号格式验证、密码非空验证
- 检查手机号是否已注册
- 创建新账号并设置初始状态
- 返回注册结果及提示信息
2025-08-28 15:07:45 +08:00
f451b835b9 feat(pricing): 添加快速设置商品价格功能并集成到模板服务
- 在 PricingManagementServiceImpl 中实现 quickSetupProductPrice 方法,用于快速设置商品价格
- 在 IPricingManagementService 接口中添加 quickSetupProductPrice 方法的声明
- 在 TemplateServiceImpl 中调用 quickSetupProductPrice 方法,为模板设置价格
2025-08-28 12:09:47 +08:00
ce7e055ada feat(scenic-account): 添加景区账号激活/停用功能并优化登录验证
- 新增激活/停用景区账号的接口和相关逻辑
- 在登录时增加账号激活状态的验证
- 更新数据库表结构,添加 isActive 字段
- 优化 MyBatis 映射文件,支持新功能
2025-08-28 11:12:37 +08:00
c7d5399931 refactor(scenic): 将 scenic 模块列表接口中的 total 字段类型从 Long 改为 Integer
- 修改了 ScenicV2ListResponse 和 ScenicV2WithConfigListResponse 类中的 total 字段类型
- 此更改统一了 total 字段的类型,提高了代码的一致性和可维护性
2025-08-28 09:57:39 +08:00
5bb2bc1ac3 refactor(PageResponse): 将 total 字段类型从 Long 改为 Integer
- 修改了 PageResponse 类中 total 字段的类型,以更好地与前端交互
- 这个改动解决了后端与前端之间关于 total 类型不一致的问题
2025-08-28 09:55:40 +08:00
95d8b742ee feat(scenic): 添加景区配置管理器并集成缓存支持
- 新增 ScenicConfigManager 类,用于管理和获取景区配置
- 在 ScenicRepository 中添加获取景区配置管理器的方法
- 实现了带缓存支持的景区配置获取,提高性能
2025-08-28 09:52:43 +08:00
ff320ba3e8 feat(AppScenicServiceImpl): 添加景区设备数量字段
- 在 AppScenicServiceImpl 类中,为 scenicAppVO 对象添加 deviceNum 字段
- 通过 deviceRepository.getAllDeviceByScenicId 方法获取景区设备数量并设置到 scenicAppVO 中
2025-08-27 17:10:42 +08:00
98bbaccb3a refactor(biz): 优化代码中的条件判断逻辑
- 将 Integer 类型的比较改为 Boolean 类型的比较,提高代码可读性和性能
- 修改涉及 scenicConfig 的条件判断,使用 Boolean.TRUE进行比较
- 优化部分代码结构,保持逻辑一致性
2025-08-27 16:40:32 +08:00
f2ac6aaea0 refactor(scenic): 重构景区相关接口和缓存机制
- 移除 ScenicMapper 接口,将相关方法移至 ScenicRepository
- 修改景区列表查询逻辑,使用 ScenicRepository 的 list 方法
- 优化景区详情获取方式,使用 ScenicRepository 的 getScenicBasic 方法
- 重构缓存机制,增加对景区基本信息的缓存
- 优化 AppScenicService 和 ScenicService接口,使用 ScenicV2DTO 替代 ScenicRespV
2025-08-27 16:37:57 +08:00
21f76ff9c5 refactor(scenic): 重构景区相关接口和数据结构
-移除了 ScenicMapper 中的冗余方法
- 更新了 ScenicEntity 和 ScenicRespVO 的字段结构
- 重构了 ScenicRepository 中的缓存逻辑
- 优化了 AppScenicServiceImpl 中的景区详情获取方法
2025-08-27 10:25:51 +08:00
7d40b8043d feat(basic): 添加默认配置管理功能
- 实现了默认配置的列表获取、单个配置获取、创建、更新和删除功能- 使用日志记录操作信息- 异常处理确保错误信息返回给客户端
2025-08-27 10:12:08 +08:00
b67fb87989 refactor(basic): 移除景区控制器中的冗余代码
- 删除了 ScenicController 类中多个未使用的 API 方法
- 保留了下载小程序二维码的功能
-简化了代码结构,提高了代码可维护性
2025-08-27 10:07:30 +08:00
42e7b7da95 feat(AppScenicAccountController):修复并优化景区列表获取功能- 初始化 list 为 Collections.emptyList(),避免空指针异常
- 增加对 ADMIN 角色的处理,使其能够获取景区列表
- 优化代码结构,提高可读性和维护性
2025-08-27 10:07:14 +08:00
6bc94a65a6 feat(scenic): 优化景区信息获取与缓存机制
-移除景区信息查询相关冗余代码
- 增加缓存逻辑,提高景区信息获取效率
- 更新 ScenicRepository 中的 getScenic 和 getScenicConfig 方法
- 重构 ScenicServiceImpl 中的 list 方法
- 删除 ScenicService 接口中未使用的多个方法
2025-08-27 10:07:01 +08:00
7c2db2ad22 refactor(scenic): 重构景区管理接口并新增 V2 版本
- 新增 ScenicV2Controller 控制器,实现景区 V2 版本的 CRUD操作和配置管理
- 移除 ScenicConfigWithDefaultClient 和 ScenicMetaClient 接口- 更新 ScenicV2Client接口,添加分页查询方法
- 删除 ConfigWithDefaultResponse、BatchSetFieldEnabledRequest、EnabledFieldsResponse、FieldConfigDTO 和 SetFieldEnabledRequest 类
- 新增 ScenicV2ListResponse 和 ScenicV2WithConfigListResponse 类- 更新 ScenicConfigIntegrationService 和 ScenicIntegrationService,移除与配置相关的方法
- 删除 ScenicMetaIntegrationService 类
2025-08-27 00:11:00 +08:00
f6bd7e48a3 refactor(basic): 将 ScenicConfigEntity 中的 allFree 字段类型从 Integer 改为 Boolean
- 修改了 OrderBiz、PriceBiz 中的相关代码,使用 Boolean.TRUE 进行比较
- 更新了 ScenicConfigEntity 和 ScenicConfigResp 中 allFree 字段的类型
- 在 ScenicRepository 中使用 ConfigValueUtil.getBooleanValue 方法获取 allFree 的值
2025-08-26 14:29:45 +08:00
f0aeb27566 refactor(scenic): 重构景区配置相关代码
- 为 FeignClient 添加 contextId 属性,提高服务调用的可读性
- 更新 ScenicIntegrationService 中的接口调用方式
- 修改 ScenicConfigEntity 和 ScenicConfigResp 中的字段类型
-重构 ScenicRepository 中的配置解析逻辑,使用 ConfigValueUtil 工具类
2025-08-26 14:26:44 +08:00
5871beb84e refactor: 移除 FeignConfig 类
删除了 FeignConfig 类及相关配置,包括日志级别设置、请求拦截器、错误解码器等。这部分配置可能已经不再需要,或者已经被其他配置所替代。
2025-08-26 13:45:38 +08:00
291b3d620f refactor(basic): 重构景区相关接口调用
- 移除 Redis 缓存操作,改为直接调用 ScenicIntegrationService- 新增 convertToScenicEntity 和 convertToScenicConfigEntity 方法进行数据转换
- 优化异常处理,fallback 到数据库查询
2025-08-26 13:45:28 +08:00
32feaa9692 feat(integration): 添加 ZT-Scenic 集成服务模块
- 新增 FeignConfig、IntegrationProperties 等基础配置类
- 实现自定义 FeignErrorDecoder 和 IntegrationException
- 添加 CommonResponse 和 PageResponse 等通用响应模型
- 定义多个 Feign 客户端接口,用于调用 ZT-Scenic 服务
- 实现 DefaultConfigIntegrationService 和 ScenicConfigIntegrationService 服务类
- 添加 ScenicIntegrationExample 示例类,展示如何使用集成服务
2025-08-26 13:36:06 +08:00
e694aac928 refactor(pricing): 修正凭证打印服务中的 URL 链接参数
- 在生成 URL链接时,添加了 scenicId
2025-08-26 10:37:30 +08:00
985 changed files with 86671 additions and 9161 deletions

4
.gitignore vendored
View File

@@ -1,6 +1,8 @@
.idea/ .idea/
logs/ logs/
target/ target/
.*
.claude .claude
.vscode .vscode
*.jpg
!.gitignore

27
AGENTS.md Normal file
View File

@@ -0,0 +1,27 @@
# Repository Guidelines
## Build, Test, and Development Commands
- Build artifact: `mvn clean package` (tests are skipped by default via `pom.xml`).
- Run locally (dev): `mvn spring-boot:run -Dspring-boot.run.profiles=dev`.
- Run jar: `java -jar target/basic21-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev`.
- Execute all tests: `mvn -DskipTests=false test` (note: `pom.xml` excludes `**/*Test.java` from test-compile; temporarily remove/override that config if you need to compile and run tests).
- Run single test: `mvn -DskipTests=false test -Dtest=ClassNameTest` (after removing testExcludes from maven-compiler-plugin).
## Code Style Guidelines
- Java 21. Use 4-space indentation; UTF-8; no wildcard imports.
- Packages: `com.ycwl.basic.*`; classes PascalCase; methods/fields camelCase; constants UPPER_SNAKE_CASE.
- Controllers in `controller`, business logic in `service`, persistence in `mapper` + `resources/mapper/*.xml`.
- Prefer Lombok for boilerplate and constructor injection where applicable.
- Error handling: Use custom exceptions in `exception` package; proper logging with SLF4J.
- Testing: Spring Boot testing + JUnit; test names end with `Test` or `Tests` and mirror package structure.
## Project Structure
- Application code: `src/main/java/com/ycwl/basic/**` (controllers, services, mapper/repository, dto/model, config, util).
- Resources: `src/main/resources/**` (Spring configs, `mapper/*.xml`, static assets, logging).
- Tests: `src/test/java/**` mirrors main packages.
- Build output: `target/` (never commit).
## Agent-Specific Notes
- Keep changes minimal and within existing package boundaries.
- Do not reorganize MyBatis XML names or mapper interfaces without updating both sides.
- If altering APIs, update affected tests and documentation in the same PR.

209
CLAUDE.md
View File

@@ -1,190 +1,27 @@
# CLAUDE.md # Repository Guidelines
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Build, Test, and Development Commands
- Build artifact: `mvn clean package` (tests are skipped by default via `pom.xml`).
- Run locally (dev): `mvn spring-boot:run -Dspring-boot.run.profiles=dev`.
- Run jar: `java -jar target/basic21-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev`.
- Execute all tests: `mvn -DskipTests=false test` (note: `pom.xml` excludes `**/*Test.java` from test-compile; temporarily remove/override that config if you need to compile and run tests).
- Run single test: `mvn -DskipTests=false test -Dtest=ClassNameTest` (after removing testExcludes from maven-compiler-plugin).
## 构建和开发命令 ## Code Style Guidelines
- Java 21. Use 4-space indentation; UTF-8; no wildcard imports.
- Packages: `com.ycwl.basic.*`; classes PascalCase; methods/fields camelCase; constants UPPER_SNAKE_CASE.
- Controllers in `controller`, business logic in `service`, persistence in `mapper` + `resources/mapper/*.xml`.
- Prefer Lombok for boilerplate and constructor injection where applicable.
- Error handling: Use custom exceptions in `exception` package; proper logging with SLF4J.
- Testing: Spring Boot testing + JUnit; test names end with `Test` or `Tests` and mirror package structure.
### 构建应用程序 ## Project Structure
```bash - Application code: `src/main/java/com/ycwl/basic/**` (controllers, services, mapper/repository, dto/model, config, util).
# 清理构建(默认跳过测试) - Resources: `src/main/resources/**` (Spring configs, `mapper/*.xml`, static assets, logging).
mvn clean package - Tests: `src/test/java/**` mirrors main packages.
- Build output: `target/` (never commit).
# 清理构建并执行测试 ## Agent-Specific Notes
mvn clean package -DskipTests=false - Keep changes minimal and within existing package boundaries.
- Do not reorganize MyBatis XML names or mapper interfaces without updating both sides.
# 运行应用程序 - If altering APIs, update affected tests and documentation in the same PR.
mvn spring-boot:run
```
### 测试命令
```bash
# 运行特定测试类
mvn test -Dtest=FaceCleanerTest
# 运行特定包的测试
mvn test -Dtest="com.ycwl.basic.storage.adapters.*Test"
# 运行所有测试
mvn test -DskipTests=false
```
### 开发环境配置
应用程序使用 Spring 配置文件:
- 默认激活配置文件:`dev`
- 生产环境配置文件:`prod`(启用定时任务)
- 配置文件:`application-dev.yml``application-prod.yml`
## 架构概览
这是一个 Spring Boot 3.3.5 应用程序(Java 21),采用多租户架构,通过不同的 API 端点为不同的客户端类型提供服务。
### 控制器架构
- **移动端 APIs** (`/api/mobile/`):面向移动应用的客户端端点
- **PC 端 APIs** (`/api/`):Web 仪表板/管理面板端点
- **任务 APIs** (`/task/`):后台工作和渲染任务端点
- **外部 APIs**:专用集成(打印机、代理、viid、vpt、wvp)
### 核心业务模块
#### 工厂模式实现
三个主要工厂类管理第三方集成:
1. **StorageFactory** (`com.ycwl.basic.storage.StorageFactory`)
- 管理:本地存储、AWS S3、阿里云 OSS 存储适配器
- 配置节:`storage.configs[]`
2. **PayFactory** (`com.ycwl.basic.pay.PayFactory`)
- 管理:微信支付、聪明支付适配器
- 配置节:`pay.configs[]`
3. **FaceBodyFactory** (`com.ycwl.basic.facebody.FaceBodyFactory`)
- 管理:阿里云、百度人脸识别适配器
- 配置节:`facebody.configs[]`
#### 适配器模式
每个工厂使用标准化接口:
- `IStorageAdapter`:文件操作(上传/下载/删除/ACL)
- `IPayAdapter`:支付生命周期(创建/回调/退款)
- `IFaceBodyAdapter`:人脸识别操作
#### 定时任务系统
`com.ycwl.basic.task` 包中的后台任务(仅生产环境):
- `VideoTaskGenerator`:人脸识别和视频处理
- `FaceCleaner`:人脸和存储清理任务
- `DynamicTaskGenerator`:带延迟队列的动态任务创建
- `ScenicStatsTask`:统计数据聚合
### 数据库和持久化
- **MyBatis Plus**:具有自动 CRUD 操作的 ORM
- **MapperScan**:扫描 `com.ycwl.basic.mapper` 及子包
- **数据库**:MySQL 配合 HikariCP 连接池
- **Redis**:会话管理和缓存
### 主要库和依赖
- Spring Boot 3.3.5 启用 Java 21 虚拟线程
- MyBatis Plus 3.5.5 用于数据库操作
- JWT (jjwt 0.9.0) 用于身份验证
- 微信支付 SDK 用于支付处理
- 阿里云 OSS 和 AWS S3 用于文件存储
- 阿里云和百度 SDK 用于人脸识别
- OpenTelemetry 用于可观测性(开发环境中禁用)
### 业务逻辑组织
- **Service 层**:`service` 包中的业务逻辑实现
- **Biz 层**:`biz` 包中的高级业务编排
- **Repository 模式**:`repository` 包中的数据访问抽象
- **自定义异常**:特定领域的异常处理
### 配置管理
每个模块使用 Spring Boot 自动配置启动器:
- 支持多供应商的命名配置
- 通过配置进行默认供应商选择
- 针对不同环境的特定配置文件
## 常见开发模式
### 添加新的存储/支付/人脸识别供应商
1. 实现相应接口(`IStorageAdapter``IPayAdapter``IFaceBodyAdapter`
2. 在相应的类型枚举中添加枚举值
3. 更新工厂的 switch 表达式
4. 如需要,添加配置类
5. 在 application.yml 中更新新供应商配置
### 身份验证上下文
在整个应用程序中使用 `BaseContextHandler.getUserId()` 获取当前已认证用户 ID。
### API 响应模式
所有 API 都返回 `ApiResponse<T>` 包装器,通过 `CustomExceptionHandle` 进行一致的错误处理。
### 添加新的定时任务
1.`com.ycwl.basic.task` 包中创建类
2. 添加 `@Component``@Profile("prod")` 注解
3. 使用 `@Scheduled` 进行基于 cron 的执行
4. 遵循现有的错误处理和日志记录模式
## 价格查询系统 (Pricing Module)
### 核心架构
价格查询系统是一个独立的业务模块,位于 `com.ycwl.basic.pricing` 包中,提供商品定价、优惠券管理和价格计算功能。
#### 关键组件
- **PriceCalculationController** (`/api/pricing/calculate`):价格计算API
- **CouponManagementController** (`/api/pricing/admin/coupons/`):优惠券管理API
- **PricingConfigController** (`/api/pricing/config/`):价格配置管理API
#### 商品类型支持
```java
ProductType枚举定义了支持的商品类型
- VLOG_VIDEO: Vlog视频
- RECORDING_SET: 录像集
- PHOTO_SET: 照相集
- PHOTO_PRINT: 照片打印
- MACHINE_PRINT: 一体机打印
```
#### 价格计算流程
1. 接收PriceCalculationRequest(包含商品列表和用户ID)
2. 查找商品基础配置和分层定价
3. 处理套餐商品(BundleProductItem)
4. 自动应用最优优惠券
5. 返回PriceCalculationResult(包含原价、最终价格、优惠详情)
#### 优惠券系统
- **CouponType**: PERCENTAGE(百分比)、FIXED_AMOUNT(固定金额)
- **CouponStatus**: CLAIMED(已领取)、USED(已使用)、EXPIRED(已过期)
- 支持商品类型限制 (`applicableProducts` JSON字段)
- 最小消费金额和最大折扣限制
- 时间有效期控制
#### 分页查询功能
所有管理接口都支持分页查询,使用PageHelper实现:
- 优惠券配置分页:支持按状态、名称筛选
- 领取记录分页:支持按用户、优惠券、状态、时间范围筛选
#### 统计功能
- 基础统计:领取数、使用数、可用数
- 详细统计:使用率、平均使用天数
- 时间范围统计:指定时间段的整体数据分析
### 开发模式
#### 添加新商品类型
1. 在ProductType枚举中添加新类型
2. 在PriceProductConfig表中配置default配置
3. 根据需要添加分层定价(PriceTierConfig)
4. 更新前端产品类型映射
#### 添加新优惠券类型
1. 在CouponType枚举中添加类型
2. 在CouponServiceImpl中实现计算逻辑
3. 更新applicableProducts验证规则
#### 自定义TypeHandler使用
项目使用自定义TypeHandler处理复杂JSON字段:
- `BundleProductListTypeHandler`:处理套餐商品列表JSON序列化
### 测试策略
- 单元测试:每个服务类都有对应测试类
- 配置验证测试:DefaultConfigValidationTest验证default配置
- JSON序列化测试:验证复杂对象的数据库存储
- 分页功能测试:验证PageHelper集成

View File

66
pom.xml
View File

@@ -75,6 +75,12 @@
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-web</artifactId>
</dependency> </dependency>
<!-- WebSocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- Nacos服务发现 --> <!-- Nacos服务发现 -->
<dependency> <dependency>
<groupId>com.alibaba.cloud</groupId> <groupId>com.alibaba.cloud</groupId>
@@ -266,6 +272,45 @@
<artifactId>mts20140618</artifactId> <artifactId>mts20140618</artifactId>
<version>5.0.0</version> <version>5.0.0</version>
</dependency> </dependency>
<!-- 智谱AI SDK -->
<dependency>
<groupId>ai.z.openapi</groupId>
<artifactId>zai-sdk</artifactId>
<version>0.1.3</version>
</dependency>
<!-- Spring Kafka -->
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
<!-- Caffeine Cache -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<!-- Apache POI - 处理Excel文件 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>5.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.4.0</version>
</dependency>
<!-- ClickHouse JDBC Driver -->
<dependency>
<groupId>com.clickhouse</groupId>
<artifactId>clickhouse-jdbc</artifactId>
<version>0.8.5</version>
<classifier>all</classifier>
</dependency>
</dependencies> </dependencies>
<build> <build>
@@ -293,16 +338,6 @@
<skip>${skipTests}</skip> <skip>${skipTests}</skip>
</configuration> </configuration>
</plugin> </plugin>
<!-- 跳过测试编译 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<testExcludes>
<testExclude>**/*Test.java</testExclude>
</testExcludes>
</configuration>
</plugin>
</plugins> </plugins>
</build> </build>
@@ -325,17 +360,6 @@
<enabled>true</enabled> <enabled>true</enabled>
</snapshots> </snapshots>
</repository> </repository>
<repository>
<id>sonatype-nexus-staging</id>
<name>Sonatype Nexus Staging</name>
<url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories> </repositories>
</project> </project>

View File

@@ -1,6 +1,5 @@
package com.ycwl.basic; package com.ycwl.basic;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@@ -9,8 +8,6 @@ import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication @SpringBootApplication
@EnableDiscoveryClient @EnableDiscoveryClient
@EnableFeignClients @EnableFeignClients
@MapperScan(basePackages = "com.ycwl.basic.mapper")
@MapperScan(basePackages = "com.ycwl.basic.*.mapper")
public class Application { public class Application {
public static void main(String[] args) { public static void main(String[] args) {

View File

@@ -1,13 +1,14 @@
package com.ycwl.basic.biz; package com.ycwl.basic.biz;
import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.DateUtil;
import com.ycwl.basic.clickhouse.service.StatsQueryService;
import com.ycwl.basic.mapper.BrokerMapper; import com.ycwl.basic.mapper.BrokerMapper;
import com.ycwl.basic.mapper.BrokerRecordMapper; import com.ycwl.basic.mapper.BrokerRecordMapper;
import com.ycwl.basic.mapper.StatisticsMapper; import com.ycwl.basic.mapper.StatisticsMapper;
import com.ycwl.basic.model.pc.broker.entity.BrokerRecord; import com.ycwl.basic.model.pc.broker.entity.BrokerRecord;
import com.ycwl.basic.model.pc.broker.resp.BrokerRespVO; import com.ycwl.basic.model.pc.broker.resp.BrokerRespVO;
import com.ycwl.basic.model.pc.order.entity.OrderEntity; import com.ycwl.basic.model.pc.order.entity.OrderEntity;
import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity; import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
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 lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -34,7 +35,7 @@ public class BrokerBiz {
@Autowired @Autowired
private ScenicRepository scenicRepository; private ScenicRepository scenicRepository;
@Autowired @Autowired
private StatisticsMapper statisticsMapper; private StatsQueryService statsQueryService;
public void processOrder(Long orderId) { public void processOrder(Long orderId) {
log.info("开始处理订单分佣,订单ID:{}", orderId); log.info("开始处理订单分佣,订单ID:{}", orderId);
@@ -43,16 +44,16 @@ public class BrokerBiz {
log.info("订单不存在,订单ID:{}", orderId); log.info("订单不存在,订单ID:{}", orderId);
return; return;
} }
ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(order.getScenicId()); ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(order.getScenicId());
if (scenicConfig == null) { if (scenicConfig == null) {
log.info("景区不存在,订单ID:{}", orderId); log.info("景区不存在,订单ID:{}", orderId);
return; return;
} }
int expireDay = 3; int expireDay = 3;
if (scenicConfig.getSampleStoreDay() != null) { if (scenicConfig.getInteger("sample_store_day") != null) {
expireDay = scenicConfig.getSampleStoreDay(); expireDay = scenicConfig.getInteger("sample_store_day");
} }
List<Long> brokerIdList = statisticsMapper.getBrokerIdListForUser(order.getMemberId(), DateUtil.offsetDay(DateUtil.beginOfDay(order.getCreateAt()), -expireDay), order.getCreateAt()); List<Long> brokerIdList = statsQueryService.getBrokerIdListForUser(order.getMemberId(), DateUtil.offsetDay(DateUtil.beginOfDay(order.getCreateAt()), -expireDay), order.getCreateAt());
if (brokerIdList == null || brokerIdList.isEmpty()) { if (brokerIdList == null || brokerIdList.isEmpty()) {
log.info("用户与推客无关,订单ID:{}", orderId); log.info("用户与推客无关,订单ID:{}", orderId);
return; return;
@@ -103,7 +104,7 @@ public class BrokerBiz {
BigDecimal realRate = broker.getBrokerRate(); BigDecimal realRate = broker.getBrokerRate();
BigDecimal brokerPrice = order.getPayPrice().multiply(realRate).divide(BigDecimal.valueOf(100), 2, RoundingMode.DOWN); BigDecimal brokerPrice = order.getPayPrice().multiply(realRate).divide(BigDecimal.valueOf(100), 2, RoundingMode.DOWN);
// todo 需要计算实际提成比例 // todo 需要计算实际提成比例
BigDecimal firstRate = scenicConfig.getBrokerDirectRate(); BigDecimal firstRate = scenicConfig.getBigDecimal("broker_direct_rate");
if (firstRate == null) { if (firstRate == null) {
firstRate = BigDecimal.ZERO; firstRate = BigDecimal.ZERO;
} }

View File

@@ -1,91 +0,0 @@
package com.ycwl.basic.biz;
import com.ycwl.basic.mapper.CouponMapper;
import com.ycwl.basic.mapper.CouponRecordMapper;
import com.ycwl.basic.model.pc.coupon.entity.CouponEntity;
import com.ycwl.basic.model.pc.couponRecord.entity.CouponRecordEntity;
import com.ycwl.basic.model.pc.couponRecord.resp.CouponRecordQueryResp;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.List;
import java.util.Optional;
@Component
public class CouponBiz {
@Autowired
private CouponMapper couponMapper;
@Autowired
private CouponRecordMapper couponRecordMapper;
public CouponRecordQueryResp queryUserCouponRecord(Long scenicId, Long memberId, Long faceId, String goodsId) {
CouponRecordQueryResp resp = new CouponRecordQueryResp();
List<CouponRecordEntity> recordList = couponRecordMapper.queryByUserWithGoodsId(scenicId, memberId, goodsId);
if (recordList != null && !recordList.isEmpty()) {
Optional<CouponRecordEntity> record = recordList.stream().filter(item -> item.getStatus() == 0).filter(item -> item.getFaceId() == null || item.getFaceId().equals(faceId)).findAny();
if (record.isPresent()) {
CouponRecordEntity recordEntity = record.get();
resp.setExist(true);
resp.setId(recordEntity.getId());
resp.setCouponId(recordEntity.getCouponId());
CouponEntity coupon = couponMapper.getById(recordEntity.getCouponId());
if (coupon != null) {
resp.setMemberId(recordEntity.getMemberId());
resp.setFaceId(recordEntity.getFaceId());
resp.setStatus(recordEntity.getStatus());
resp.setCreateTime(recordEntity.getCreateTime());
resp.setUsedTime(recordEntity.getUsedTime());
resp.setUsedOrderId(recordEntity.getUsedOrderId());
resp.setCoupon(coupon);
} else {
resp.setExist(false);
}
} else {
Optional<CouponRecordEntity> usedRecord = recordList.stream().filter(item -> item.getStatus() != 0).filter(item -> item.getFaceId() == null || item.getFaceId().equals(faceId)).findAny();
if (usedRecord.isPresent()) {
CouponRecordEntity recordEntity = usedRecord.get();
resp.setExist(true);
resp.setId(recordEntity.getId());
resp.setCouponId(recordEntity.getCouponId());
CouponEntity coupon = couponMapper.getById(recordEntity.getCouponId());
if (coupon != null) {
resp.setMemberId(recordEntity.getMemberId());
resp.setFaceId(recordEntity.getFaceId());
resp.setStatus(recordEntity.getStatus());
resp.setCreateTime(recordEntity.getCreateTime());
resp.setUsedTime(recordEntity.getUsedTime());
resp.setUsedOrderId(recordEntity.getUsedOrderId());
resp.setCoupon(coupon);
} else {
resp.setExist(false);
}
}
}
}
return resp;
}
public boolean userGetCoupon(Long memberId, Long faceId, Integer couponId) {
CouponEntity coupon = couponMapper.getById(couponId);
if (coupon == null) {
return false;
}
CouponRecordEntity entity = new CouponRecordEntity();
entity.setCouponId(couponId);
entity.setFaceId(faceId);
entity.setMemberId(memberId);
entity.setStatus(0);
entity.setCreateTime(new Date());
return couponRecordMapper.insert(entity) > 0;
}
public boolean userUseCoupon(Long memberId, Long faceId, Integer couponRecordId, Long orderId) {
CouponRecordEntity entity = new CouponRecordEntity();
entity.setId(couponRecordId);
entity.setStatus(1);
entity.setUsedTime(new Date());
entity.setUsedOrderId(orderId);
return couponRecordMapper.updateById(entity) > 0;
}
}

View File

@@ -0,0 +1,384 @@
package com.ycwl.basic.biz;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.ycwl.basic.enums.FaceCutStatus;
import com.ycwl.basic.enums.FacePieceUpdateStatus;
import com.ycwl.basic.enums.TemplateRenderStatus;
import com.ycwl.basic.mapper.TaskMapper;
import com.ycwl.basic.model.pc.task.entity.TaskEntity;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* 人脸状态缓存管理器
* 统一管理人脸相关的内存缓存状态(使用Caffeine)
*/
@Slf4j
@Component
public class FaceStatusManager {
/**
* 默认过期时间:1小时
*/
private static final long DEFAULT_EXPIRE_SECONDS = 3600L;
/**
* 人脸切片状态缓存
*/
private final Cache<String, Integer> faceCutStatusCache;
/**
* 人脸片段更新状态缓存(全局和模板级)
* 键存在=无新片段,键不存在=有新片段
*/
private final Cache<String, Boolean> faceNoPieceUpdateCache;
/**
* 人脸模板渲染状态缓存
*/
private final Cache<String, Integer> templateRenderCache;
/**
* 拼图素材版本缓存
* 键:faceId:puzzleTemplateId -> 当时的图片源数量
* 用于判断拼图模板的素材是否发生变化,避免重复生成
*/
private final Cache<String, Integer> puzzleSourceVersionCache;
@Autowired
private TaskMapper taskMapper;
public FaceStatusManager() {
// 初始化三个独立的缓存实例
this.faceCutStatusCache = Caffeine.newBuilder()
.expireAfterWrite(DEFAULT_EXPIRE_SECONDS, TimeUnit.SECONDS)
.maximumSize(10000)
.build();
this.faceNoPieceUpdateCache = Caffeine.newBuilder()
.expireAfterWrite(DEFAULT_EXPIRE_SECONDS, TimeUnit.SECONDS)
.maximumSize(10000)
.build();
this.templateRenderCache = Caffeine.newBuilder()
.expireAfterWrite(DEFAULT_EXPIRE_SECONDS, TimeUnit.SECONDS)
.maximumSize(10000)
.build();
this.puzzleSourceVersionCache = Caffeine.newBuilder()
.expireAfterWrite(DEFAULT_EXPIRE_SECONDS, TimeUnit.SECONDS)
.maximumSize(10000)
.build();
}
// ==================== 切片状态相关方法 ====================
/**
* 设置人脸切片状态
* @param faceId 人脸ID
* @param status 切片状态
*/
public void setFaceCutStatus(Long faceId, FaceCutStatus status) {
if (faceId == null || status == null) {
log.warn("设置切片状态参数为空: faceId={}, status={}", faceId, status);
return;
}
faceCutStatusCache.put(String.valueOf(faceId), status.getCode());
log.debug("设置切片状态: faceId={}, status={}", faceId, status.getDescription());
}
/**
* 获取人脸切片状态
* @param faceId 人脸ID
* @return 切片状态,缓存不存在时返回 COMPLETED(已完成)
*/
public FaceCutStatus getFaceCutStatus(Long faceId) {
if (faceId == null) {
log.warn("获取切片状态参数为空: faceId={}", faceId);
return FaceCutStatus.COMPLETED;
}
Integer code = faceCutStatusCache.getIfPresent(String.valueOf(faceId));
if (code == null) {
log.debug("切片状态缓存不存在,返回默认值COMPLETED: faceId={}", faceId);
return FaceCutStatus.COMPLETED;
}
return FaceCutStatus.fromCodeOrDefault(code, FaceCutStatus.COMPLETED);
}
/**
* 删除人脸切片状态缓存
* @param faceId 人脸ID
*/
public void deleteFaceCutStatus(Long faceId) {
if (faceId == null) {
return;
}
faceCutStatusCache.invalidate(String.valueOf(faceId));
log.debug("删除切片状态缓存: faceId={}", faceId);
}
// ==================== 片段更新状态相关方法 ====================
/**
* 标记无新片段(设置缓存键)
* @param faceId 人脸ID
* @param templateId 模板ID(可选,为null时标记全局状态)
*/
public void markNoNewPieces(Long faceId, Long templateId) {
if (faceId == null) {
log.warn("标记无新片段参数为空: faceId={}", faceId);
return;
}
if (templateId == null) {
// 全局标记:该人脸的所有模板都无新片段
faceNoPieceUpdateCache.put(String.valueOf(faceId), Boolean.TRUE);
log.debug("标记无新片段(全局): faceId={}", faceId);
} else {
// 模板级标记:该人脸在该模板下无新片段
faceNoPieceUpdateCache.put(faceId + ":" + templateId, Boolean.TRUE);
log.debug("标记无新片段(模板): faceId={}, templateId={}", faceId, templateId);
}
}
/**
* 标记有新片段(删除缓存键)
* @param faceId 人脸ID
* @param templateId 模板ID(可选,为null时标记全局状态)
*/
public void markHasNewPieces(Long faceId, Long templateId) {
if (faceId == null) {
log.warn("标记有新片段参数为空: faceId={}", faceId);
return;
}
if (templateId == null) {
// 全局标记:该人脸有新片段
faceNoPieceUpdateCache.invalidate(String.valueOf(faceId));
log.debug("标记有新片段(全局): faceId={}", faceId);
} else {
// 模板级标记:该人脸在该模板下有新片段
faceNoPieceUpdateCache.invalidate(faceId + ":" + templateId);
log.debug("标记有新片段(模板): faceId={}, templateId={}", faceId, templateId);
}
}
/**
* 获取人脸片段更新状态
* @param faceId 人脸ID
* @param templateId 模板ID(可选,为null时查询全局状态)
* @return 片段更新状态,键存在=无新片段,键不存在=有新片段
*/
public FacePieceUpdateStatus getFacePieceUpdateStatus(Long faceId, Long templateId) {
if (faceId == null) {
log.warn("获取片段更新状态参数为空: faceId={}", faceId);
return FacePieceUpdateStatus.HAS_NEW_PIECES;
}
String key = templateId == null ? String.valueOf(faceId) : faceId + ":" + templateId;
boolean exists = faceNoPieceUpdateCache.getIfPresent(key) != null;
FacePieceUpdateStatus status = FacePieceUpdateStatus.fromKeyExists(exists);
if (templateId == null) {
log.debug("获取片段更新状态(全局): faceId={}, status={}", faceId, status.getDescription());
} else {
log.debug("获取片段更新状态(模板): faceId={}, templateId={}, status={}",
faceId, templateId, status.getDescription());
}
return status;
}
/**
* 获取人脸片段更新状态 - 全局版本
* @param faceId 人脸ID
* @return 片段更新状态,键存在=无新片段,键不存在=有新片段
*/
public FacePieceUpdateStatus getFacePieceUpdateStatus(Long faceId) {
return getFacePieceUpdateStatus(faceId, null);
}
/**
* 判断是否有新片段
* @param faceId 人脸ID
* @param templateId 模板ID(可选,为null时查询全局状态)
* @return true=有新片段,false=无新片段;如果templateId为null则默认返回true(有新片段)
*/
public boolean hasNewPieces(Long faceId, Long templateId) {
if (templateId == null) {
// 如果没有指定templateId,默认认为有新片段
log.debug("未指定templateId,默认返回有新片段: faceId={}", faceId);
return true;
}
return getFacePieceUpdateStatus(faceId, templateId).hasNewPieces();
}
/**
* 判断是否有新片段 - 全局版本
* @param faceId 人脸ID
* @return true=有新片段,false=无新片段
*/
public boolean hasNewPieces(Long faceId) {
return getFacePieceUpdateStatus(faceId, null).hasNewPieces();
}
// ==================== 模板渲染状态相关方法 ====================
/**
* 设置人脸模板渲染状态
* @param faceId 人脸ID
* @param templateId 模板ID
* @param status 渲染状态
*/
public void setTemplateRenderStatus(Long faceId, Long templateId, TemplateRenderStatus status) {
if (faceId == null || templateId == null || status == null) {
log.warn("设置模板渲染状态参数为空: faceId={}, templateId={}, status={}", faceId, templateId, status);
return;
}
templateRenderCache.put(faceId + ":" + templateId, status.getCode());
log.debug("设置模板渲染状态: faceId={}, templateId={}, status={}", faceId, templateId, status.getDescription());
}
/**
* 获取人脸模板渲染状态
* @param faceId 人脸ID
* @param templateId 模板ID
* @return 渲染状态,缓存不存在时返回 null
*/
public TemplateRenderStatus getTemplateRenderStatus(Long faceId, Long templateId) {
if (faceId == null || templateId == null) {
log.warn("获取模板渲染状态参数为空: faceId={}, templateId={}", faceId, templateId);
return null;
}
Integer code = templateRenderCache.getIfPresent(faceId + ":" + templateId);
if (code == null) {
log.debug("模板渲染状态缓存不存在: faceId={}, templateId={}", faceId, templateId);
// 查询数据库
TaskEntity task = taskMapper.listLastFaceTemplateTask(faceId, templateId);
if (task == null) {
setTemplateRenderStatus(faceId, templateId, TemplateRenderStatus.NONE);
return TemplateRenderStatus.NONE;
}
if (Integer.valueOf(2).equals(task.getStatus())) {
setTemplateRenderStatus(faceId, templateId, TemplateRenderStatus.RENDERING);
}
if (Integer.valueOf(1).equals(task.getStatus())) {
setTemplateRenderStatus(faceId, templateId, TemplateRenderStatus.RENDERED);
}
return TemplateRenderStatus.NONE;
}
return TemplateRenderStatus.fromCode(code);
}
/**
* 删除人脸模板渲染状态缓存
* @param faceId 人脸ID
* @param templateId 模板ID
*/
public void deleteTemplateRenderStatus(Long faceId, Long templateId) {
if (faceId == null || templateId == null) {
return;
}
templateRenderCache.invalidate(faceId + ":" + templateId);
log.debug("删除模板渲染状态缓存: faceId={}, templateId={}", faceId, templateId);
}
/**
* 删除人脸的所有模板渲染状态(使用模式匹配)
* 注意:此操作可能影响性能,谨慎使用
* @param faceId 人脸ID
*/
public void deleteAllTemplateRenderStatus(Long faceId) {
if (faceId == null) {
return;
}
String prefix = faceId + ":";
long count = templateRenderCache.asMap().keySet().stream()
.filter(key -> key.startsWith(prefix))
.peek(templateRenderCache::invalidate)
.count();
if (count > 0) {
log.debug("批量删除模板渲染状态缓存: faceId={}, count={}", faceId, count);
}
}
// ==================== 拼图素材版本相关方法 ====================
/**
* 标记拼图素材版本(记录当前的图片源数量)
* 在拼图生成成功后调用,用于后续判断素材是否变化
*
* @param faceId 人脸ID
* @param puzzleTemplateId 拼图模板ID(全局唯一)
* @param sourceCount 当前的图片源数量
*/
public void markPuzzleSourceVersion(Long faceId, Long puzzleTemplateId, int sourceCount) {
if (faceId == null || puzzleTemplateId == null) {
log.warn("标记拼图素材版本参数为空: faceId={}, puzzleTemplateId={}", faceId, puzzleTemplateId);
return;
}
String key = faceId + ":" + puzzleTemplateId;
puzzleSourceVersionCache.put(key, sourceCount);
log.debug("标记拼图素材版本: faceId={}, puzzleTemplateId={}, sourceCount={}", faceId, puzzleTemplateId, sourceCount);
}
/**
* 判断拼图素材是否发生变化
* 通过比较当前的图片源数量与缓存中记录的数量
*
* @param faceId 人脸ID
* @param puzzleTemplateId 拼图模板ID(全局唯一)
* @param currentSourceCount 当前的图片源数量
* @return true=素材已变化(需要重新生成),false=素材未变化(可以跳过生成)
*/
public boolean isPuzzleSourceChanged(Long faceId, Long puzzleTemplateId, int currentSourceCount) {
if (faceId == null || puzzleTemplateId == null) {
log.warn("判断拼图素材变化参数为空: faceId={}, puzzleTemplateId={}", faceId, puzzleTemplateId);
return true; // 参数不合法时默认认为有变化
}
String key = faceId + ":" + puzzleTemplateId;
Integer cachedCount = puzzleSourceVersionCache.getIfPresent(key);
if (cachedCount == null) {
// 缓存不存在,认为有变化(首次生成或缓存过期)
log.debug("拼图素材版本缓存不存在,需要生成: faceId={}, puzzleTemplateId={}", faceId, puzzleTemplateId);
return true;
}
boolean changed = !cachedCount.equals(currentSourceCount);
if (changed) {
log.debug("拼图素材已变化: faceId={}, puzzleTemplateId={}, cachedCount={}, currentCount={}",
faceId, puzzleTemplateId, cachedCount, currentSourceCount);
} else {
log.debug("拼图素材未变化,可跳过生成: faceId={}, puzzleTemplateId={}, sourceCount={}",
faceId, puzzleTemplateId, currentSourceCount);
}
return changed;
}
/**
* 使指定人脸的所有拼图素材版本缓存失效
* 当人脸的图片关联发生变化时调用(如人脸匹配后新增了关联)
*
* @param faceId 人脸ID
*/
public void invalidatePuzzleSourceVersion(Long faceId) {
if (faceId == null) {
return;
}
String prefix = faceId + ":";
long count = puzzleSourceVersionCache.asMap().keySet().stream()
.filter(key -> key.startsWith(prefix))
.peek(puzzleSourceVersionCache::invalidate)
.count();
if (count > 0) {
log.debug("批量使拼图素材版本缓存失效: faceId={}, count={}", faceId, count);
}
}
}

View File

@@ -1,29 +1,24 @@
package com.ycwl.basic.biz; package com.ycwl.basic.biz;
import com.ycwl.basic.clickhouse.service.StatsQueryService;
import com.ycwl.basic.enums.StatisticEnum; import com.ycwl.basic.enums.StatisticEnum;
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
import com.ycwl.basic.mapper.OrderMapper; import com.ycwl.basic.mapper.OrderMapper;
import com.ycwl.basic.mapper.SourceMapper;
import com.ycwl.basic.mapper.StatisticsMapper; import com.ycwl.basic.mapper.StatisticsMapper;
import com.ycwl.basic.mapper.VideoMapper;
import com.ycwl.basic.model.mobile.order.IsBuyRespVO; import com.ycwl.basic.model.mobile.order.IsBuyRespVO;
import com.ycwl.basic.model.mobile.order.PriceObj; import com.ycwl.basic.model.mobile.order.PriceObj;
import com.ycwl.basic.model.mobile.statistic.req.StatisticsRecordAddReq; import com.ycwl.basic.model.mobile.statistic.req.StatisticsRecordAddReq;
import com.ycwl.basic.model.pc.coupon.entity.CouponEntity;
import com.ycwl.basic.model.pc.couponRecord.resp.CouponRecordQueryResp;
import com.ycwl.basic.model.pc.face.entity.FaceEntity; import com.ycwl.basic.model.pc.face.entity.FaceEntity;
import com.ycwl.basic.model.pc.order.entity.OrderEntity; import com.ycwl.basic.model.pc.order.entity.OrderEntity;
import com.ycwl.basic.model.pc.order.entity.OrderItemEntity; import com.ycwl.basic.model.pc.order.entity.OrderItemEntity;
import com.ycwl.basic.model.pc.order.req.OrderUpdateReq; import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
import com.ycwl.basic.model.pc.order.resp.OrderAppRespVO;
import com.ycwl.basic.model.pc.order.resp.OrderItemVO;
import com.ycwl.basic.model.pc.order.resp.OrderRespVO;
import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity;
import com.ycwl.basic.model.pc.scenic.entity.ScenicEntity;
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
import com.ycwl.basic.model.pc.task.entity.TaskEntity;
import com.ycwl.basic.model.pc.template.resp.TemplateRespVO; import com.ycwl.basic.model.pc.template.resp.TemplateRespVO;
import com.ycwl.basic.model.pc.video.entity.VideoEntity; import com.ycwl.basic.model.pc.video.entity.VideoEntity;
import com.ycwl.basic.model.pc.video.resp.VideoRespVO; import com.ycwl.basic.pricing.dto.PriceCalculationRequest;
import com.ycwl.basic.pricing.dto.PriceCalculationResult;
import com.ycwl.basic.pricing.dto.ProductItem;
import com.ycwl.basic.pricing.enums.ProductType;
import com.ycwl.basic.pricing.service.IPriceCalculationService;
import com.ycwl.basic.profitsharing.biz.ProfitSharingBiz; import com.ycwl.basic.profitsharing.biz.ProfitSharingBiz;
import com.ycwl.basic.repository.FaceRepository; import com.ycwl.basic.repository.FaceRepository;
import com.ycwl.basic.repository.OrderRepository; import com.ycwl.basic.repository.OrderRepository;
@@ -33,23 +28,18 @@ 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;
import com.ycwl.basic.service.printer.PrinterService; import com.ycwl.basic.service.printer.PrinterService;
import com.ycwl.basic.utils.ApiResponse;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.Calendar; import java.util.Collections;
import java.util.Comparator;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Optional;
@Component @Component
public class OrderBiz { public class OrderBiz {
@Autowired
private VideoMapper videoMapper;
@Autowired @Autowired
private ScenicRepository scenicRepository; private ScenicRepository scenicRepository;
@Autowired @Autowired
@@ -67,28 +57,26 @@ public class OrderBiz {
@Autowired @Autowired
private OrderMapper orderMapper; private OrderMapper orderMapper;
@Autowired @Autowired
private SourceMapper sourceMapper;
@Autowired
private ProfitSharingBiz profitSharingBiz; private ProfitSharingBiz profitSharingBiz;
@Autowired @Autowired
private VideoTaskRepository videoTaskRepository; private VideoTaskRepository videoTaskRepository;
@Autowired @Autowired
private BrokerBiz brokerBiz; private BrokerBiz brokerBiz;
@Autowired @Autowired
private CouponBiz couponBiz;
@Autowired
@Lazy @Lazy
private PrinterService printerService; private PrinterService printerService;
@Autowired
private IPriceCalculationService iPriceCalculationService;
@Autowired
private StatsQueryService statsQueryService;
public PriceObj queryPrice(Long scenicId, int goodsType, Long goodsId) { public PriceObj queryPrice(Long scenicId, Long memberId, int goodsType, Long goodsId) {
PriceObj priceObj = new PriceObj(); PriceObj priceObj = new PriceObj();
priceObj.setGoodsType(goodsType); priceObj.setGoodsType(goodsType);
priceObj.setGoodsId(goodsId); priceObj.setGoodsId(goodsId);
ScenicEntity scenic = scenicRepository.getScenic(scenicId); ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(scenicId);
ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId);
priceObj.setScenicAllPrice(scenic.getPrice());
if (scenicConfig != null) { if (scenicConfig != null) {
if (Integer.valueOf(1).equals(scenicConfig.getAllFree())) { if (Boolean.TRUE.equals(scenicConfig.getBoolean("all_free"))) {
// 景区全免 // 景区全免
priceObj.setFree(true); priceObj.setFree(true);
priceObj.setPrice(BigDecimal.ZERO); priceObj.setPrice(BigDecimal.ZERO);
@@ -102,32 +90,85 @@ public class OrderBiz {
if (video == null) { if (video == null) {
return null; return null;
} }
TaskEntity task = videoTaskRepository.getTaskById(video.getTaskId()); priceObj.setFaceId(video.getFaceId());
if (task != null) {
priceObj.setFaceId(task.getFaceId());
}
TemplateRespVO template = templateRepository.getTemplate(video.getTemplateId()); TemplateRespVO template = templateRepository.getTemplate(video.getTemplateId());
if (template == null) { if (template == null) {
return priceObj; return priceObj;
} }
priceObj.setPrice(template.getPrice()); PriceCalculationRequest vlogCalculationRequest = new PriceCalculationRequest();
BigDecimal slashPrice = template.getSlashPrice(); ProductItem vlogProductItem = new ProductItem();
if (slashPrice == null) { vlogProductItem.setProductType(ProductType.VLOG_VIDEO);
priceObj.setSlashPrice(priceObj.getPrice()); vlogProductItem.setProductId(template.getId().toString());
} else { vlogProductItem.setQuantity(videoTaskRepository.getTaskLensNum(video.getTaskId()));
priceObj.setSlashPrice(slashPrice); vlogProductItem.setScenicId(scenicId.toString());
} vlogCalculationRequest.setProducts(Collections.singletonList(vlogProductItem));
vlogCalculationRequest.setUserId(memberId);
vlogCalculationRequest.setFaceId(priceObj.getFaceId());
vlogCalculationRequest.setPreviewOnly(true); // 仅查询价格,不实际使用优惠
vlogCalculationRequest.setAutoUseCoupon(true);
PriceCalculationResult vlogCalculationResult = iPriceCalculationService.calculatePrice(vlogCalculationRequest);
priceObj.setPrice(vlogCalculationResult.getFinalAmount());
priceObj.setSlashPrice(vlogCalculationResult.getOriginalAmount());
priceObj.setFaceId(goodsId);
priceObj.setScenicId(video.getScenicId()); priceObj.setScenicId(video.getScenicId());
break; break;
case 1: // source case 1: // source
priceObj.setPrice(scenic.getSourceVideoPrice()); case 2: // source
priceObj.setSlashPrice(scenic.getSourceVideoPrice()); FaceEntity face = faceRepository.getFace(goodsId);
PriceCalculationRequest calculationRequest = new PriceCalculationRequest();
ProductItem productItem = new ProductItem();
productItem.setProductType(goodsType == 1 ? ProductType.RECORDING_SET : ProductType.PHOTO_SET);
productItem.setProductId(scenicId.toString());
productItem.setPurchaseCount(1);
productItem.setScenicId(scenicId.toString());
calculationRequest.setProducts(Collections.singletonList(productItem));
if (face != null) {
calculationRequest.setUserId(face.getMemberId());
}
calculationRequest.setUserId(memberId);
calculationRequest.setFaceId(goodsId);
calculationRequest.setPreviewOnly(true); // 仅查询价格,不实际使用优惠
calculationRequest.setAutoUseCoupon(true);
PriceCalculationResult priceCalculationResult = iPriceCalculationService.calculatePrice(calculationRequest);
priceObj.setPrice(priceCalculationResult.getFinalAmount());
priceObj.setSlashPrice(priceCalculationResult.getOriginalAmount());
priceObj.setFaceId(goodsId); priceObj.setFaceId(goodsId);
break; break;
case 2: // source case 5:
priceObj.setPrice(scenic.getSourceImagePrice()); PriceCalculationRequest plogCalculationRequest = new PriceCalculationRequest();
priceObj.setSlashPrice(scenic.getSourceImagePrice()); ProductItem plogProductItem = new ProductItem();
plogProductItem.setProductType(ProductType.PHOTO_LOG);
plogProductItem.setProductId(scenicId.toString());
plogProductItem.setPurchaseCount(1);
plogProductItem.setScenicId(scenicId.toString());
plogCalculationRequest.setProducts(Collections.singletonList(plogProductItem));
plogCalculationRequest.setUserId(memberId);
plogCalculationRequest.setFaceId(goodsId);
plogCalculationRequest.setPreviewOnly(true); // 仅查询价格,不实际使用优惠
plogCalculationRequest.setAutoUseCoupon(true);
PriceCalculationResult plogPriceCalculationResult = iPriceCalculationService.calculatePrice(plogCalculationRequest);
priceObj.setPrice(plogPriceCalculationResult.getFinalAmount());
priceObj.setSlashPrice(plogPriceCalculationResult.getOriginalAmount());
priceObj.setFaceId(goodsId); priceObj.setFaceId(goodsId);
priceObj.setScenicId(scenicId);
break;
case 13:
PriceCalculationRequest aiCamCalculationRequest = new PriceCalculationRequest();
ProductItem aiCamProductItem = new ProductItem();
aiCamProductItem.setProductType(ProductType.AI_CAM_PHOTO_SET);
aiCamProductItem.setProductId(scenicId.toString());
aiCamProductItem.setPurchaseCount(1);
aiCamProductItem.setScenicId(scenicId.toString());
aiCamCalculationRequest.setProducts(Collections.singletonList(aiCamProductItem));
aiCamCalculationRequest.setUserId(memberId);
aiCamCalculationRequest.setFaceId(goodsId);
aiCamCalculationRequest.setPreviewOnly(true); // 仅查询价格,不实际使用优惠
aiCamCalculationRequest.setAutoUseCoupon(true);
PriceCalculationResult aiCamPriceCalculationResult = iPriceCalculationService.calculatePrice(aiCamCalculationRequest);
priceObj.setPrice(aiCamPriceCalculationResult.getFinalAmount());
priceObj.setSlashPrice(aiCamPriceCalculationResult.getOriginalAmount());
priceObj.setFaceId(goodsId);
priceObj.setScenicId(scenicId);
break; break;
} }
return priceObj; return priceObj;
@@ -144,85 +185,46 @@ public class OrderBiz {
return null; return null;
} }
} }
public IsBuyRespVO isBuy(Long scenicId, Long memberId, Long faceId, int goodsType, Long goodsId) {
public IsBuyRespVO isBuy(Long userId, Long scenicId, int goodsType, Long goodsId) {
IsBuyRespVO respVO = new IsBuyRespVO(); IsBuyRespVO respVO = new IsBuyRespVO();
boolean isBuy = orderRepository.checkUserBuyItem(userId, goodsType, goodsId); respVO.setGoodsType(goodsType);
// 模板购买逻辑 respVO.setGoodsId(goodsId);
if (!isBuy) { OrderEntity orderEntity = orderMapper.getUserBuyFaceItem(memberId, faceId, goodsType, goodsId);
if (goodsType == 0) {
VideoEntity video = videoRepository.getVideo(goodsId);
if (video == null) {
return respVO;
}
TaskEntity task = videoTaskRepository.getTaskById(video.getTaskId());
Long templateId = video.getTemplateId();
// -1为整个模板购买
OrderEntity orderEntity = orderRepository.getUserBuyItem(userId, -1, templateId);
if (orderEntity != null && task != null) {
respVO.setOrderId(orderEntity.getId());
if (orderEntity.getFaceId() != null && task.getFaceId() != null) {
isBuy = orderEntity.getFaceId().equals(task.getFaceId());
}
}
}
}
// 免费送逻辑,之前已经赠送了的
if (!isBuy) {
isBuy = switch (goodsType) {
case 0 -> videoRepository.getUserIsBuy(userId, goodsId);
case 1, 2 -> sourceRepository.getUserIsBuy(userId, goodsType, goodsId);
default -> false;
};
} else {
OrderEntity orderEntity = orderRepository.getUserBuyItem(userId, goodsType, goodsId);
if (orderEntity != null) { if (orderEntity != null) {
respVO.setOrderId(orderEntity.getId()); respVO.setOrderId(orderEntity.getId());
respVO.setBuy(true);
respVO.setFree(false);
return respVO;
}
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(scenicId);
if (Boolean.TRUE.equals(scenicConfig.getBoolean("all_free"))) {
// 景区全免
respVO.setFree(true);
respVO.setOrigPrice(BigDecimal.ZERO);
respVO.setSlashPrice(BigDecimal.ZERO);
return respVO;
}
// 未来模板一口价
if (goodsType == 0) {
// 视频,可以买断模板
VideoEntity video = videoRepository.getVideo(goodsId);
if (video != null && video.getTemplateId() != null) {
OrderEntity templateBuy = orderMapper.getUserBuyFaceItem(memberId, faceId, -1, video.getTemplateId());
if (templateBuy != null) {
respVO.setOrderId(templateBuy.getId());
respVO.setBuy(true);
respVO.setFree(false);
return respVO;
} }
} }
respVO.setBuy(isBuy); }
// 还是没买 PriceObj priceObj = queryPrice(scenicId, memberId, goodsType, goodsId);
if (!isBuy) {
PriceObj priceObj = queryPrice(scenicId, goodsType, goodsId);
if (priceObj == null) { if (priceObj == null) {
return respVO; return respVO;
} }
respVO.setFree(priceObj.isFree()); respVO.setBuy(false);
respVO.setGoodsType(goodsType);
respVO.setGoodsId(goodsId);
respVO.setOrigPrice(priceObj.getPrice()); respVO.setOrigPrice(priceObj.getPrice());
respVO.setSlashPrice(priceObj.getSlashPrice()); respVO.setSlashPrice(priceObj.getSlashPrice());
switch (goodsType) {
case 0: // vlog
VideoEntity video = videoRepository.getVideo(goodsId);
TaskEntity taskById = videoTaskRepository.getTaskById(video.getTaskId());
if (taskById != null) {
CouponRecordQueryResp recordQueryResp = couponBiz.queryUserCouponRecord(scenicId, userId, taskById.getFaceId(), taskById.getTemplateId().toString());
if (recordQueryResp.isUsable()) {
respVO.setCouponId(recordQueryResp.getCouponId());
respVO.setCouponRecordId(recordQueryResp.getId());
CouponEntity coupon = recordQueryResp.getCoupon();
if (coupon != null) {
respVO.setCouponPrice(coupon.calculateDiscountPrice(priceObj.getPrice()));
}
}
}
break;
case 1:
case 2:
CouponRecordQueryResp recordQueryResp = couponBiz.queryUserCouponRecord(scenicId, userId, goodsId, String.valueOf(goodsType));
if (recordQueryResp.isUsable()) {
respVO.setCouponId(recordQueryResp.getCouponId());
respVO.setCouponRecordId(recordQueryResp.getId());
CouponEntity coupon = recordQueryResp.getCoupon();
if (coupon != null) {
respVO.setCouponPrice(coupon.calculateDiscountPrice(priceObj.getPrice()));
}
}
break;
}
}
return respVO; return respVO;
} }
@@ -236,55 +238,26 @@ public class OrderBiz {
orderRepository.updateOrder(orderId, orderUpdate); orderRepository.updateOrder(orderId, orderUpdate);
orderItems.forEach(item -> { orderItems.forEach(item -> {
switch (item.getGoodsType()) { switch (item.getGoodsType()) {
case -1: // vlog视频模板
videoRepository.setUserIsBuyTemplate(order.getMemberId(), item.getGoodsId(), order.getId(), order.getFaceId());
break;
case 0: // vlog视频 case 0: // vlog视频
videoRepository.setUserIsBuyItem(order.getMemberId(), item.getGoodsId(), order.getId()); videoRepository.setUserIsBuyItem(order.getMemberId(), item.getGoodsId(), order.getId());
break;
case 1: // 视频原素材 case 1: // 视频原素材
case 2: // 照片原素材 case 2: // 照片原素材
case 13: // AI微单
sourceRepository.setUserIsBuyItem(order.getMemberId(), item.getGoodsType(), item.getGoodsId(), order.getId()); sourceRepository.setUserIsBuyItem(order.getMemberId(), item.getGoodsType(), item.getGoodsId(), order.getId());
break;
case 3: case 3:
printerService.setUserIsBuyItem(order.getMemberId(), item.getGoodsId(), order.getId()); printerService.setUserIsBuyItem(order.getMemberId(), item.getGoodsId(), order.getId());
break;
} }
}); });
orderRepository.clearOrderCache(orderId); // 更新完了,清理下 orderRepository.clearOrderCache(orderId); // 更新完了,清理下
Integer couponRecordId = order.getCouponRecordId();
if (couponRecordId != null) {
couponBiz.userUseCoupon(order.getMemberId(), order.getFaceId(), couponRecordId, orderId);
}
//支付时间
OrderAppRespVO orderDetail = orderMapper.appDetail(orderId);
Date payAt = orderDetail.getPayAt();
//商品创建时间
Date goodsCreateTime = new Date();
if (!orderDetail.getOrderItemList().isEmpty()) {
OrderItemVO orderItemVO = orderDetail.getOrderItemList().getFirst();
switch (orderItemVO.getGoodsType()) {
case 0:
VideoEntity video = videoRepository.getVideo(orderItemVO.getGoodsId());
if (video != null) {
goodsCreateTime = video.getCreateTime();
}
break;
case 1:
List<SourceEntity> imageSource = sourceMapper.listImageByFaceRelation(order.getMemberId(), orderItemVO.getGoodsId());
Optional<SourceEntity> min = imageSource.stream().min(Comparator.comparing(SourceEntity::getCreateTime));
if (min.isPresent()) {
goodsCreateTime = min.get().getCreateTime();
}
break;
case 2:
List<SourceEntity> videoSource = sourceMapper.listImageByFaceRelation(order.getMemberId(), orderItemVO.getGoodsId());
Optional<SourceEntity> minTime = videoSource.stream().min(Comparator.comparing(SourceEntity::getCreateTime));
if (minTime.isPresent()) {
goodsCreateTime = minTime.get().getCreateTime();
}
break;
}
}
StatisticsRecordAddReq statisticsRecordAddReq = new StatisticsRecordAddReq(); StatisticsRecordAddReq statisticsRecordAddReq = new StatisticsRecordAddReq();
statisticsRecordAddReq.setMemberId(order.getMemberId()); statisticsRecordAddReq.setMemberId(order.getMemberId());
Long enterType = statisticsMapper.getUserRecentEnterType(order.getMemberId(), order.getCreateAt()); Long enterType = statsQueryService.getUserRecentEnterType(order.getMemberId(), order.getCreateAt());
if(!Long.valueOf(1014).equals(enterType)){// if(!Long.valueOf(1014).equals(enterType)){//
statisticsRecordAddReq.setType(StatisticEnum.ON_SITE_PAYMENT.code); statisticsRecordAddReq.setType(StatisticEnum.ON_SITE_PAYMENT.code);
}else { }else {
@@ -309,9 +282,11 @@ public class OrderBiz {
switch (item.getGoodsType()) { switch (item.getGoodsType()) {
case 0: // vlog视频 case 0: // vlog视频
videoRepository.setUserNotBuyItem(order.getMemberId(), item.getGoodsId()); videoRepository.setUserNotBuyItem(order.getMemberId(), item.getGoodsId());
break;
case 1: // 视频原素材 case 1: // 视频原素材
case 2: // 照片原素材 case 2: // 照片原素材
sourceRepository.setUserNotBuyItem(order.getMemberId(), item.getGoodsType(), item.getGoodsId()); sourceRepository.setUserNotBuyItem(order.getMemberId(), item.getGoodsType(), item.getGoodsId());
break;
} }
}); });
orderRepository.clearOrderCache(orderId); // 更新完了,清理下 orderRepository.clearOrderCache(orderId); // 更新完了,清理下
@@ -331,12 +306,15 @@ public class OrderBiz {
switch (item.getGoodsType()) { switch (item.getGoodsType()) {
case 0: // vlog视频 case 0: // vlog视频
videoRepository.setUserNotBuyItem(order.getMemberId(), item.getGoodsId()); videoRepository.setUserNotBuyItem(order.getMemberId(), item.getGoodsId());
break;
case 1: // 视频原素材 case 1: // 视频原素材
case 2: // 照片原素材 case 2: // 照片原素材
sourceRepository.setUserNotBuyItem(order.getMemberId(), item.getGoodsType(), item.getGoodsId()); sourceRepository.setUserNotBuyItem(order.getMemberId(), item.getGoodsType(), item.getGoodsId());
break;
} }
}); });
orderRepository.clearOrderCache(orderId); // 更新完了,清理下 orderRepository.clearOrderCache(orderId); // 更新完了,清理下
profitSharingBiz.revokeProfitSharing(order.getScenicId(), orderId, "订单已退款"); profitSharingBiz.revokeProfitSharing(order.getScenicId(), orderId, "订单已退款");
} }
} }

View File

@@ -1,20 +1,29 @@
package com.ycwl.basic.biz; package com.ycwl.basic.biz;
import com.ycwl.basic.model.mobile.order.IsBuyBatchRespVO; import com.ycwl.basic.model.mobile.order.IsBuyBatchRespVO;
import com.ycwl.basic.model.pc.coupon.entity.CouponEntity;
import com.ycwl.basic.model.pc.couponRecord.resp.CouponRecordQueryResp;
import com.ycwl.basic.model.pc.face.entity.FaceEntity; import com.ycwl.basic.model.pc.face.entity.FaceEntity;
import com.ycwl.basic.model.pc.order.entity.OrderEntity; import com.ycwl.basic.model.pc.order.entity.OrderEntity;
import com.ycwl.basic.model.pc.price.entity.PriceConfigEntity; import com.ycwl.basic.model.pc.price.entity.PriceConfigEntity;
import com.ycwl.basic.model.pc.price.resp.GoodsListRespVO; import com.ycwl.basic.model.pc.price.resp.GoodsListRespVO;
import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity; import com.ycwl.basic.model.pc.price.resp.SimpleGoodsRespVO;
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
import com.ycwl.basic.model.pc.template.resp.TemplateRespVO; import com.ycwl.basic.model.pc.template.resp.TemplateRespVO;
import com.ycwl.basic.model.pc.video.entity.MemberVideoEntity;
import com.ycwl.basic.product.capability.ProductTypeCapability;
import com.ycwl.basic.product.service.IProductTypeCapabilityManagementService;
import com.ycwl.basic.puzzle.entity.PuzzleTemplateEntity;
import com.ycwl.basic.puzzle.mapper.PuzzleTemplateMapper;
import com.ycwl.basic.puzzle.repository.PuzzleRepository;
import com.ycwl.basic.repository.FaceRepository; import com.ycwl.basic.repository.FaceRepository;
import com.ycwl.basic.repository.MemberRelationRepository;
import com.ycwl.basic.repository.OrderRepository;
import com.ycwl.basic.repository.PriceRepository; import com.ycwl.basic.repository.PriceRepository;
import com.ycwl.basic.repository.ScenicRepository; import com.ycwl.basic.repository.ScenicRepository;
import com.ycwl.basic.repository.TemplateRepository; import com.ycwl.basic.repository.TemplateRepository;
import com.ycwl.basic.service.pc.FaceService;
import org.apache.commons.lang3.Strings; import org.apache.commons.lang3.Strings;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.math.BigDecimal; import java.math.BigDecimal;
@@ -36,7 +45,15 @@ public class PriceBiz {
@Autowired @Autowired
private FaceRepository faceRepository; private FaceRepository faceRepository;
@Autowired @Autowired
private CouponBiz couponBiz; private MemberRelationRepository memberRelationRepository;
@Autowired
private PuzzleTemplateMapper puzzleTemplateMapper;
@Autowired
private PuzzleRepository puzzleRepository;
@Autowired
private IProductTypeCapabilityManagementService productTypeCapabilityManagementService;
@Autowired
private OrderRepository orderRepository;
public List<GoodsListRespVO> listGoodsByScenic(Long scenicId) { public List<GoodsListRespVO> listGoodsByScenic(Long scenicId) {
List<GoodsListRespVO> goodsList = new ArrayList<>(); List<GoodsListRespVO> goodsList = new ArrayList<>();
@@ -46,17 +63,131 @@ public class PriceBiz {
GoodsListRespVO goods = new GoodsListRespVO(); GoodsListRespVO goods = new GoodsListRespVO();
goods.setGoodsId(template.getId()); goods.setGoodsId(template.getId());
goods.setGoodsName(template.getName()); goods.setGoodsName(template.getName());
goods.setGoodsType(0);
return goods; return goods;
}).forEach(goodsList::add); }).forEach(goodsList::add);
ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId); ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(scenicId);
if (scenicConfig != null) { if (scenicConfig != null) {
if (!Integer.valueOf(1).equals(scenicConfig.getDisableSourceVideo())) { if (!Boolean.TRUE.equals(scenicConfig.getBoolean("disable_source_video"))) {
goodsList.add(new GoodsListRespVO(1L, "录像集")); goodsList.add(new GoodsListRespVO(1L, "录像集", 1));
} }
if (!Integer.valueOf(1).equals(scenicConfig.getDisableSourceImage())) { if (!Boolean.TRUE.equals(scenicConfig.getBoolean("disable_source_image"))) {
goodsList.add(new GoodsListRespVO(2L, "照片集")); goodsList.add(new GoodsListRespVO(2L, "照片集", 2));
} }
} }
// 拼图(使用缓存)
puzzleRepository.listTemplateByScenic(scenicId).forEach(puzzleTemplate -> {
GoodsListRespVO goods = new GoodsListRespVO();
goods.setGoodsId(puzzleTemplate.getId());
goods.setGoodsName(puzzleTemplate.getName());
goods.setGoodsType(5);
goodsList.add(goods);
});
return goodsList;
}
/**
* 根据景区ID和商品类型查询简化的商品列表
*
* @param scenicId 景区ID
* @param productType 商品类型(可选,为空时返回所有商品)
* @return 简化的商品列表
*/
public List<SimpleGoodsRespVO> listSimpleGoodsByScenic(Long scenicId, String productType) {
List<SimpleGoodsRespVO> goodsList = new ArrayList<>();
// 如果 productType 为空,兼容旧逻辑
if (productType == null || productType.isEmpty()) {
return listAllSimpleGoods(scenicId);
}
// 根据 productType 查询不同数据源
switch (productType) {
case "VLOG_VIDEO":
// 从 template 表查询视频模板
List<TemplateRespVO> templateList = templateRepository.getTemplateListByScenicId(scenicId);
templateList.stream()
.map(template -> new SimpleGoodsRespVO(template.getId(), template.getName(), productType))
.forEach(goodsList::add);
break;
case "PHOTO_VLOG":
// TODO
goodsList.add(new SimpleGoodsRespVO(scenicId, "【待实现】pLog视频", productType));
break;
case "PHOTO":
goodsList.add(new SimpleGoodsRespVO(scenicId, "单张照片", productType));
break;
case "PHOTO_SET":
// 返回固定的照片集条目
goodsList.add(new SimpleGoodsRespVO(scenicId, "照片集", productType));
break;
case "AI_CAM_PHOTO_SET":
// 返回固定的照片集条目
goodsList.add(new SimpleGoodsRespVO(scenicId, "AI微单照片集", productType));
break;
case "PHOTO_LOG":
// 从 template 表查询pLog模板
List<PuzzleTemplateEntity> puzzleList = puzzleRepository.listTemplateByScenic(scenicId);
puzzleList.stream()
.map(template -> new SimpleGoodsRespVO(template.getId(), template.getName(), productType))
.forEach(goodsList::add);
if (!puzzleList.isEmpty()) {
goodsList.addFirst(new SimpleGoodsRespVO(scenicId, "pLog图<景区打包>", productType));
}
break;
case "RECORDING_SET":
// 返回固定的录像集条目
goodsList.add(new SimpleGoodsRespVO(scenicId, "录像集", productType));
break;
case "PHOTO_PRINT":
// 打印类返回单一通用条目
goodsList.add(new SimpleGoodsRespVO(scenicId, "照片打印", productType));
break;
case "PHOTO_PRINT_MU":
// 打印类返回单一通用条目
goodsList.add(new SimpleGoodsRespVO(scenicId, "手机照片打印", productType));
break;
case "PHOTO_PRINT_FX":
// 打印类返回单一通用条目
goodsList.add(new SimpleGoodsRespVO(scenicId, "效果图片打印", productType));
break;
case "MACHINE_PRINT":
// 打印类返回单一通用条目
goodsList.add(new SimpleGoodsRespVO(scenicId, "一体机打印", productType));
break;
default:
// 不支持的 productType,返回空列表
break;
}
return goodsList;
}
/**
* 兼容旧逻辑:返回所有商品
* 通过查询系统中所有已知的 productType,将结果综合到一起
*/
private List<SimpleGoodsRespVO> listAllSimpleGoods(Long scenicId) {
List<SimpleGoodsRespVO> goodsList = new ArrayList<>();
// 从 ProductTypeCapability 服务查询所有已知的商品类型(仅包含启用的)
List<ProductTypeCapability> capabilities = productTypeCapabilityManagementService.queryAll(false);
// 轮询每个商品类型,获取对应的商品列表
for (ProductTypeCapability capability : capabilities) {
String productType = capability.getProductType();
List<SimpleGoodsRespVO> typeGoodsList = listSimpleGoodsByScenic(scenicId, productType);
goodsList.addAll(typeGoodsList);
}
return goodsList; return goodsList;
} }
@@ -80,7 +211,7 @@ public class PriceBiz {
}).collect(Collectors.toList()); }).collect(Collectors.toList());
} }
public IsBuyBatchRespVO isBuy(Long userId, Long faceId, Long scenicId, Integer type, String goodsIds) { public IsBuyBatchRespVO isOnePriceBuy(Long userId, Long faceId, Long scenicId, Integer type, String goodsIds) {
IsBuyBatchRespVO respVO = new IsBuyBatchRespVO(); IsBuyBatchRespVO respVO = new IsBuyBatchRespVO();
PriceConfigEntity priceConfig = priceRepository.getPriceConfigByScenicTypeGoods(scenicId, type, goodsIds); PriceConfigEntity priceConfig = priceRepository.getPriceConfigByScenicTypeGoods(scenicId, type, goodsIds);
if (priceConfig == null) { if (priceConfig == null) {
@@ -90,39 +221,15 @@ public class PriceBiz {
if (face != null && !face.getMemberId().equals(userId)) { if (face != null && !face.getMemberId().equals(userId)) {
return null; return null;
} }
ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId); ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(scenicId);
if (scenicConfig != null) { if (scenicConfig != null) {
if (Integer.valueOf(1).equals(scenicConfig.getAllFree())) { if (Boolean.TRUE.equals(scenicConfig.getBoolean("all_free"))) {
// 景区全免 // 景区全免
respVO.setFree(true); respVO.setFree(true);
respVO.setSlashPrice(BigDecimal.ZERO); respVO.setSlashPrice(BigDecimal.ZERO);
return respVO; return respVO;
} }
} }
switch (type) {
case 0: // 单个定价
CouponRecordQueryResp recordQueryResp = couponBiz.queryUserCouponRecord(scenicId, userId, faceId, goodsIds);
if (recordQueryResp.isUsable()) {
respVO.setCouponId(recordQueryResp.getCouponId());
respVO.setCouponRecordId(recordQueryResp.getId());
CouponEntity coupon = recordQueryResp.getCoupon();
if (coupon != null) {
respVO.setCouponPrice(coupon.calculateDiscountPrice(priceConfig.getPrice()));
}
}
break;
case -1:
CouponRecordQueryResp oneCouponRecordQueryResp = couponBiz.queryUserCouponRecord(scenicId, userId, faceId, "-1");
if (oneCouponRecordQueryResp.isUsable()) {
respVO.setCouponId(oneCouponRecordQueryResp.getCouponId());
respVO.setCouponRecordId(oneCouponRecordQueryResp.getId());
CouponEntity coupon = oneCouponRecordQueryResp.getCoupon();
if (coupon != null) {
respVO.setCouponPrice(coupon.calculateDiscountPrice(priceConfig.getPrice()));
}
}
break;
}
respVO.setConfigId(priceConfig.getId()); respVO.setConfigId(priceConfig.getId());
respVO.setGoodsIds(goodsIds); respVO.setGoodsIds(goodsIds);
respVO.setType(type); respVO.setType(type);
@@ -138,6 +245,53 @@ public class PriceBiz {
respVO.setBuy(Integer.valueOf(1).equals(orderEntity.getStatus())); respVO.setBuy(Integer.valueOf(1).equals(orderEntity.getStatus()));
} }
} }
if (type == -1 && !respVO.isBuy()) {
// 直接查询用户购买状态,避免调用faceContentList造成循环调用
boolean allContentsPurchased = true;
// 检查视频模板购买状态
List<TemplateRespVO> templateList = templateRepository.getTemplateListByScenicId(scenicId);
for (TemplateRespVO template : templateList) {
// 使用OrderRepository直接检查是否购买了该模板下的内容
List<MemberVideoEntity> videoEntities = memberRelationRepository.listRelationByFaceAndTemplate(faceId, template.getId());
if (videoEntities == null || videoEntities.isEmpty()) {
allContentsPurchased = false;
break;
}
boolean hasPurchasedTemplate = orderRepository.checkUserBuyFaceItem(userId, faceId, -1, videoEntities.getFirst().getVideoId());
if (!hasPurchasedTemplate) {
allContentsPurchased = false;
break;
}
}
// 检查源文件购买状态(录像集和照片集)
if (allContentsPurchased) {
if (scenicConfig != null) {
// 检查录像集
if (!Boolean.TRUE.equals(scenicConfig.getBoolean("disable_source_video"))) {
boolean hasPurchasedRecording = orderRepository.checkUserBuyFaceItem(userId, faceId, 1, faceId);
if (!hasPurchasedRecording) {
allContentsPurchased = false;
}
}
// 检查照片集
if (allContentsPurchased && !Boolean.TRUE.equals(scenicConfig.getBoolean("disable_source_image"))) {
boolean hasPurchasedPhoto = orderRepository.checkUserBuyFaceItem(userId, faceId, 2, faceId);
if (!hasPurchasedPhoto) {
allContentsPurchased = false;
}
}
}
}
// 如果所有内容都已购买,则认为已购买套餐
if (allContentsPurchased) {
respVO.setBuy(true);
}
}
respVO.setShare(false);
return respVO; return respVO;
} }
} }

View File

@@ -1,192 +0,0 @@
package com.ycwl.basic.biz;
import com.ycwl.basic.mapper.FaceMapper;
import com.ycwl.basic.mapper.TaskMapper;
import com.ycwl.basic.mapper.VideoMapper;
import com.ycwl.basic.model.mobile.goods.VideoTaskStatusVO;
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
import com.ycwl.basic.model.pc.face.resp.FaceRespVO;
import com.ycwl.basic.model.pc.task.req.TaskReqQuery;
import com.ycwl.basic.model.pc.task.resp.TaskRespVO;
import com.ycwl.basic.model.pc.template.resp.TemplateRespVO;
import com.ycwl.basic.model.pc.video.entity.VideoEntity;
import com.ycwl.basic.repository.FaceRepository;
import com.ycwl.basic.repository.TemplateRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
@Component
public class TaskStatusBiz {
public static final String TASK_STATUS_USER_CACHE_KEY = "task:status:user:%s:face:%s";
public static final String TASK_STATUS_FACE_CACHE_KEY = "task:status:face:%s";
public static final String TASK_STATUS_FACE_CACHE_KEY_CUT = "task:status:face:%s:cut";
public static final String TASK_STATUS_FACE_CACHE_KEY_TEMPLATE = "task:status:face:%s:tpl:%s";
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private FaceRepository faceRepository;
@Autowired
private TemplateRepository templateRepository;
@Autowired
private FaceMapper faceMapper;
@Autowired
private TaskMapper taskMapper;
@Autowired
private VideoMapper videoMapper;
@Autowired
private TemplateBiz templateBiz;
public boolean getUserHaveFace(Long userId, Long faceId) {
if (userId == null || faceId == null) {
return false;
}
if (redisTemplate.hasKey(String.format(TASK_STATUS_USER_CACHE_KEY, userId, faceId))) {
return true;
}
FaceEntity face = faceRepository.getFace(faceId);
if (face == null) {
return false;
}
if (face.getMemberId().equals(userId)) {
redisTemplate.opsForValue().set(String.format(TASK_STATUS_USER_CACHE_KEY, userId, faceId), "1", 3600, TimeUnit.SECONDS);
return true;
} else {
return false;
}
}
public void setFaceCutStatus(Long faceId, int status) {
redisTemplate.opsForValue().set(String.format(TASK_STATUS_FACE_CACHE_KEY_CUT, faceId), String.valueOf(status), 3600, TimeUnit.SECONDS);
}
public void setFaceTemplateStatus(Long faceId, Long templateId, Long videoId) {
redisTemplate.opsForValue().set(String.format(TASK_STATUS_FACE_CACHE_KEY_TEMPLATE, faceId, templateId), String.valueOf(videoId), 3600, TimeUnit.SECONDS);
}
public VideoTaskStatusVO getScenicUserStatus(Long scenicId, Long userId) {
FaceRespVO lastFace = faceMapper.findLastFaceByScenicAndUserId(scenicId, userId);
VideoTaskStatusVO response = new VideoTaskStatusVO();
if (lastFace == null) {
response.setStatus(-1);
return response;
}
return getFaceStatus(lastFace.getId());
}
public VideoTaskStatusVO getFaceStatus(Long faceId) {
FaceEntity face = faceRepository.getFace(faceId);
VideoTaskStatusVO response = new VideoTaskStatusVO();
if (face == null) {
response.setStatus(-1);
return response;
}
response.setScenicId(face.getScenicId());
response.setFaceId(faceId);
List<TemplateRespVO> templateList = templateRepository.getTemplateListByScenicId(face.getScenicId());
response.setMaxCount(templateList.size());
int alreadyFinished = 0;
for (TemplateRespVO template : templateList) {
response.setTemplateId(template.getId());
long videoId = getFaceTemplateVideoId(faceId, template.getId());
if (videoId <= 0) {
response.setStatus(2);
} else {
response.setVideoId(videoId);
alreadyFinished++;
}
}
response.setCount(alreadyFinished);
if (alreadyFinished == 0) {
response.setStatus(0);
} else {
response.setStatus(1);
}
if (alreadyFinished == 0) {
int faceCutStatus = getFaceCutStatus(faceId);
if (faceCutStatus != 1) {
// 正在切片
if (templateBiz.determineTemplateCanGenerate(templateList.getFirst().getId(), faceId, false)) {
response.setStatus(2);
} else {
response.setStatus(0);
}
}
}
return response;
}
public VideoTaskStatusVO getFaceTemplateStatus(Long faceId, Long templateId) {
FaceEntity face = faceRepository.getFace(faceId);
VideoTaskStatusVO response = new VideoTaskStatusVO();
if (face == null) {
response.setStatus(-1);
return response;
}
response.setScenicId(face.getScenicId());
response.setFaceId(faceId);
response.setTemplateId(templateId);
long videoId = getFaceTemplateVideoId(faceId, templateId);
if (videoId < 0) {
int faceCutStatus = getFaceCutStatus(faceId);
if (faceCutStatus != 1) {
// 正在切片
response.setStatus(2);
return response;
}
} else if (videoId == 0) {
response.setStatus(2);
} else {
response.setVideoId(videoId);
response.setStatus(1);
}
return response;
}
public int getFaceCutStatus(Long faceId) {
if (redisTemplate.hasKey(String.format(TASK_STATUS_FACE_CACHE_KEY_CUT, faceId))) {
String status = redisTemplate.opsForValue().get(String.format(TASK_STATUS_FACE_CACHE_KEY_CUT, faceId));
if (status != null) {
return Integer.parseInt(status);
}
}
return 1;
}
public long getFaceTemplateVideoId(Long faceId, Long templateId) {
if (redisTemplate.hasKey(String.format(TASK_STATUS_FACE_CACHE_KEY_TEMPLATE, faceId, templateId))) {
String status = redisTemplate.opsForValue().get(String.format(TASK_STATUS_FACE_CACHE_KEY_TEMPLATE, faceId, templateId));
if (status != null) {
return Long.parseLong(status);
}
}
TaskReqQuery taskReqQuery = new TaskReqQuery();
taskReqQuery.setFaceId(faceId);
taskReqQuery.setTemplateId(templateId);
List<TaskRespVO> list = taskMapper.list(taskReqQuery);
Optional<TaskRespVO> min = list.stream().min(Comparator.comparing(TaskRespVO::getCreateTime));
if (min.isPresent()) {
TaskRespVO task = min.get();
long taskStatus = 0;
if (task.getStatus() == 1) {
// 已完成
VideoEntity video = videoMapper.findByTaskId(task.getId());
if (video != null) {
taskStatus = video.getId();
}
}
setFaceTemplateStatus(faceId, templateId, taskStatus);
} else {
// 从来没生成过
setFaceTemplateStatus(faceId, templateId, -1L);
return -1;
}
return 0;
}
}

View File

@@ -14,8 +14,11 @@ 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;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -29,8 +32,6 @@ public class TemplateBiz {
private FaceRepository faceRepository; private FaceRepository faceRepository;
@Autowired @Autowired
private SourceMapper sourceMapper; private SourceMapper sourceMapper;
@Autowired
private SourceRepository sourceRepository;
public boolean determineTemplateCanGenerate(Long templateId, Long faceId) { public boolean determineTemplateCanGenerate(Long templateId, Long faceId) {
return determineTemplateCanGenerate(templateId, faceId, true); return determineTemplateCanGenerate(templateId, faceId, true);
@@ -53,6 +54,7 @@ public class TemplateBiz {
if (scanSource) { if (scanSource) {
List<SourceEntity> sourceEntities = sourceMapper.listVideoByScenicFaceRelation(face.getScenicId(), faceId); List<SourceEntity> sourceEntities = sourceMapper.listVideoByScenicFaceRelation(face.getScenicId(), faceId);
if (sourceEntities == null || sourceEntities.isEmpty()) { if (sourceEntities == null || sourceEntities.isEmpty()) {
log.info("faceId:{} has no source", faceId);
return false; return false;
} }
count = sourceEntities.stream() count = sourceEntities.stream()
@@ -64,6 +66,7 @@ public class TemplateBiz {
} else { } else {
List<FaceSampleEntity> faceSampleList = faceRepository.getFaceSampleList(faceId); List<FaceSampleEntity> faceSampleList = faceRepository.getFaceSampleList(faceId);
if (faceSampleList == null || faceSampleList.isEmpty()) { if (faceSampleList == null || faceSampleList.isEmpty()) {
log.info("faceId:{} has no faceSample", faceId);
return false; return false;
} }
count = faceSampleList.stream() count = faceSampleList.stream()
@@ -91,8 +94,8 @@ public class TemplateBiz {
} }
if (minimalPlaceholderFill == null) { if (minimalPlaceholderFill == null) {
// 未开启 // 未开启
log.info("模板:{},未配置最小自动生成功能,默认生成", templateId); log.info("模板:{},未配置最小自动生成功能,默认生成", templateId);
return false; minimalPlaceholderFill = 1;
} }
if (minimalPlaceholderFill <= 0) { if (minimalPlaceholderFill <= 0) {
return true; return true;
@@ -121,4 +124,97 @@ public class TemplateBiz {
return count >= minimalPlaceholderFill; return count >= minimalPlaceholderFill;
} }
public Map<String, List<SourceEntity>> filterTaskParams(Long templateId, Map<String, List<SourceEntity>> allTaskParams) {
if (allTaskParams == null || allTaskParams.isEmpty()) {
return Map.of();
}
List<String> templatePlaceholders = templateRepository.getTemplatePlaceholder(templateId);
if (templatePlaceholders == null || templatePlaceholders.isEmpty()) {
log.info("filterTaskParams: templateId:{} has no placeholders", templateId);
return Map.of();
}
TemplateConfigEntity templateConfig = templateRepository.getTemplateConfig(templateId);
// 统计每个 placeholder 在模板中出现的次数
Map<String, Long> placeholderCounts = templatePlaceholders.stream()
.collect(Collectors.groupingBy(
placeholder -> placeholder,
Collectors.counting()
));
Map<String, List<SourceEntity>> filteredParams = new HashMap<>();
// 判断是否允许片段重复
boolean allowDuplicate = templateConfig != null && Integer.valueOf(1).equals(templateConfig.getDuplicateEnable());
for (Map.Entry<String, Long> entry : placeholderCounts.entrySet()) {
String placeholder = entry.getKey();
Long requiredCount = entry.getValue();
if (placeholder.startsWith("P")) {
// 图片源:占位符格式为 "P{deviceId}"
String imageKey = placeholder;
if (allTaskParams.containsKey(imageKey)) {
List<SourceEntity> allSources = allTaskParams.get(imageKey);
List<SourceEntity> selectedSources = selectSources(allSources, requiredCount.intValue(), allowDuplicate);
if (!selectedSources.isEmpty()) {
filteredParams.put(imageKey, selectedSources);
}
}
} else {
// 视频源:占位符直接对应设备ID
String videoKey = placeholder;
if (allTaskParams.containsKey(videoKey)) {
List<SourceEntity> allSources = allTaskParams.get(videoKey);
List<SourceEntity> selectedSources = selectSources(allSources, requiredCount.intValue(), allowDuplicate);
if (!selectedSources.isEmpty()) {
filteredParams.put(videoKey, selectedSources);
}
}
}
}
log.debug("filterTaskParams: templateId:{}, original keys:{}, filtered keys:{}, placeholder counts:{}, allowDuplicate:{}",
templateId, allTaskParams.keySet().size(), filteredParams.keySet().size(), placeholderCounts, allowDuplicate);
return filteredParams;
}
private List<SourceEntity> selectSources(List<SourceEntity> allSources, int requiredCount, boolean allowDuplicate) {
if (allSources == null || allSources.isEmpty()) {
return new ArrayList<>();
}
if (!allowDuplicate) {
// 不允许重复,使用原有逻辑
int actualCount = Math.min(requiredCount, allSources.size());
return new ArrayList<>(allSources.subList(0, actualCount));
}
// 允许重复,循环填充到所需数量
List<SourceEntity> selectedSources = new ArrayList<>();
int sourceIndex = 0;
for (int i = 0; i < requiredCount; i++) {
selectedSources.add(allSources.get(sourceIndex));
sourceIndex = (sourceIndex + 1) % allSources.size();
}
return selectedSources;
}
public Long findFirstAvailableTemplate(List<Long> templateIds, Long faceId, boolean scanSource) {
if (templateIds == null || templateIds.isEmpty() || faceId == null) {
return null;
}
for (Long templateId : templateIds) {
if (determineTemplateCanGenerate(templateId, faceId, scanSource)) {
return templateId;
}
}
return null;
}
} }

View File

@@ -0,0 +1,104 @@
package com.ycwl.basic.clickhouse.service;
import com.ycwl.basic.model.mobile.statistic.req.CommonQueryReq;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
/**
* 统计数据查询服务接口
* 用于抽象 t_stats 和 t_stats_record 表的查询
* 支持 MySQL 和 ClickHouse 两种实现
*/
public interface StatsQueryService {
/**
* 统计预览视频人数
*/
Integer countPreviewVideoOfMember(CommonQueryReq query);
/**
* 统计扫码访问人数
*/
Integer countScanCodeOfMember(CommonQueryReq query);
/**
* 统计推送订阅人数
*/
Integer countPushOfMember(CommonQueryReq query);
/**
* 统计上传头像人数
*/
Integer countUploadFaceOfMember(CommonQueryReq query);
/**
* 统计生成视频人数
*/
Integer countCompleteVideoOfMember(CommonQueryReq query);
/**
* 统计生成视频条数
*/
Integer countCompleteOfVideo(CommonQueryReq query);
/**
* 统计总访问人数
*/
Integer countTotalVisitorOfMember(CommonQueryReq query);
/**
* 统计预览视频条数
*/
Integer countPreviewOfVideo(CommonQueryReq query);
/**
* 获取用户分销员 ID 列表
*/
List<Long> getBrokerIdListForUser(Long memberId, Date startTime, Date endTime);
/**
* 获取用户最近进入类型
*/
Long getUserRecentEnterType(Long memberId, Date endTime);
/**
* 获取用户项目 ID 列表
*/
List<Long> getProjectIdListForUser(Long memberId, Date startTime, Date endTime);
/**
* 统计分销员扫码次数
*/
Integer countBrokerScanCount(Long brokerId);
/**
* 按日期统计分销员扫码数据
*/
List<HashMap<String, Object>> getDailyScanStats(Long brokerId, Date startTime, Date endTime);
/**
* 按小时统计扫码人数(仅返回统计数据,不含订单)
* 返回格式: [{t: "MM-dd HH", count: "xxx"}, ...]
*/
List<HashMap<String, String>> scanCodeMemberChartByHour(CommonQueryReq query);
/**
* 按日期统计扫码人数(仅返回统计数据,不含订单)
* 返回格式: [{t: "MM-dd", count: "xxx"}, ...]
*/
List<HashMap<String, String>> scanCodeMemberChartByDate(CommonQueryReq query);
/**
* 按小时统计访问打印样片页面人数
* 返回格式: [{t: "MM-dd HH", count: "xxx"}, ...]
*/
List<HashMap<String, String>> printerFromSampleChartByHour(CommonQueryReq query);
/**
* 按日期统计访问打印样片页面人数
* 返回格式: [{t: "MM-dd", count: "xxx"}, ...]
*/
List<HashMap<String, String>> printerFromSampleChartByDate(CommonQueryReq query);
}

View File

@@ -0,0 +1,536 @@
package com.ycwl.basic.clickhouse.service.impl;
import com.ycwl.basic.clickhouse.service.StatsQueryService;
import com.ycwl.basic.mapper.TaskMapper;
import com.ycwl.basic.model.mobile.statistic.req.CommonQueryReq;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Service;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.stream.Collectors;
/**
* ClickHouse 统计数据查询服务实现
* 当 clickhouse.enabled=true 时启用
*
* 注意:ClickHouse JDBC 驱动 0.6.x 对参数绑定支持有问题,
* 因此使用字符串格式化方式构建 SQL(参数均为内部生成的数值或日期,无 SQL 注入风险)
*/
@Slf4j
@Service
@ConditionalOnProperty(prefix = "clickhouse", name = "enabled", havingValue = "true")
public class ClickHouseStatsQueryServiceImpl implements StatsQueryService {
private static final TimeZone CLICKHOUSE_TIMEZONE = TimeZone.getTimeZone("Asia/Shanghai");
/**
* 创建日期格式化器(SimpleDateFormat 非线程安全,每次创建新实例)
*/
private SimpleDateFormat createDateFormat() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setTimeZone(CLICKHOUSE_TIMEZONE);
return sdf;
}
/**
* 创建日期时间格式化器
*/
private SimpleDateFormat createDateTimeFormat() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
sdf.setTimeZone(CLICKHOUSE_TIMEZONE);
return sdf;
}
@Autowired
@Qualifier("clickHouseJdbcTemplate")
private NamedParameterJdbcTemplate namedJdbcTemplate;
private JdbcTemplate jdbcTemplate;
@Autowired
private TaskMapper taskMapper;
private JdbcTemplate getJdbcTemplate() {
if (jdbcTemplate == null) {
jdbcTemplate = namedJdbcTemplate.getJdbcTemplate();
}
return jdbcTemplate;
}
/**
* 格式化日期时间为 ClickHouse 可识别的字符串
*/
private String formatDateTime(Date date) {
return date != null ? "'" + createDateTimeFormat().format(date) + "'" : null;
}
/**
* 格式化日期为 ClickHouse 可识别的字符串
*/
private String formatDate(Date date) {
return date != null ? "'" + createDateFormat().format(date) + "'" : null;
}
/**
* 拼接“进入景区”的 trace_id 子查询。
* <p>
* ClickHouse 上 t_stats_record 往往按时间分区/排序;给子查询补充时间范围可显著减少扫描量。
*/
private void appendEnterScenicTraceIdSubQuery(StringBuilder sql, Long scenicId, Date startTime, Date endTime) {
sql.append("SELECT DISTINCT trace_id FROM t_stats_record ");
sql.append("WHERE action = 'ENTER_SCENIC' ");
if (scenicId != null) {
sql.append("AND identifier = '").append(scenicId).append("' ");
}
if (startTime != null) {
sql.append("AND create_time >= ").append(formatDateTime(startTime)).append(" ");
}
if (endTime != null) {
sql.append("AND create_time <= ").append(formatDateTime(endTime)).append(" ");
}
}
@Override
public Integer countPreviewVideoOfMember(CommonQueryReq query) {
StringBuilder sql = new StringBuilder();
sql.append("SELECT toInt32(uniqExact(s.member_id)) AS count ");
sql.append("FROM t_stats_record r ");
sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
sql.append("WHERE r.action = 'LOAD' ");
sql.append("AND r.identifier = 'pages/videoSynthesis/buy' ");
sql.append("AND r.trace_id IN (");
appendEnterScenicTraceIdSubQuery(sql, query.getScenicId(), query.getStartTime(), query.getEndTime());
sql.append(") ");
sql.append("AND JSONExtractString(r.params, 'share') = '' ");
if (query.getStartTime() != null) {
sql.append("AND r.create_time >= ").append(formatDateTime(query.getStartTime())).append(" ");
}
if (query.getEndTime() != null) {
sql.append("AND r.create_time <= ").append(formatDateTime(query.getEndTime())).append(" ");
}
return getJdbcTemplate().queryForObject(sql.toString(), Integer.class);
}
@Override
public Integer countScanCodeOfMember(CommonQueryReq query) {
StringBuilder sql = new StringBuilder();
sql.append("SELECT toInt32(uniqExact(s.member_id)) AS count ");
sql.append("FROM t_stats_record r ");
sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
sql.append("WHERE r.trace_id IN (");
appendEnterScenicTraceIdSubQuery(sql, query.getScenicId(), query.getStartTime(), query.getEndTime());
sql.append(") ");
sql.append("AND r.action = 'LAUNCH' ");
sql.append("AND JSONExtractInt(r.params, 'scene') IN (1047, 1048, 1049) ");
if (query.getStartTime() != null) {
sql.append("AND s.create_time >= ").append(formatDateTime(query.getStartTime())).append(" ");
}
if (query.getEndTime() != null) {
sql.append("AND s.create_time <= ").append(formatDateTime(query.getEndTime())).append(" ");
}
return getJdbcTemplate().queryForObject(sql.toString(), Integer.class);
}
@Override
public Integer countPushOfMember(CommonQueryReq query) {
StringBuilder sql = new StringBuilder();
sql.append("SELECT toInt32(uniqExact(s.member_id)) AS count ");
sql.append("FROM t_stats_record r ");
sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
sql.append("WHERE r.trace_id IN (");
appendEnterScenicTraceIdSubQuery(sql, query.getScenicId(), query.getStartTime(), query.getEndTime());
sql.append(") ");
sql.append("AND r.action = 'PERM_REQ' ");
sql.append("AND r.identifier = 'NOTIFY' ");
if (query.getStartTime() != null) {
sql.append("AND r.create_time >= ").append(formatDateTime(query.getStartTime())).append(" ");
}
if (query.getEndTime() != null) {
sql.append("AND r.create_time <= ").append(formatDateTime(query.getEndTime())).append(" ");
}
return getJdbcTemplate().queryForObject(sql.toString(), Integer.class);
}
@Override
public Integer countUploadFaceOfMember(CommonQueryReq query) {
StringBuilder sql = new StringBuilder();
sql.append("SELECT toInt32(uniqExact(s.member_id)) AS count ");
sql.append("FROM t_stats_record r ");
sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
sql.append("WHERE r.action = 'FACE_UPLOAD' ");
sql.append("AND r.trace_id IN (");
appendEnterScenicTraceIdSubQuery(sql, query.getScenicId(), query.getStartTime(), query.getEndTime());
sql.append(") ");
if (query.getStartTime() != null) {
sql.append("AND s.create_time >= ").append(formatDateTime(query.getStartTime())).append(" ");
}
if (query.getEndTime() != null) {
sql.append("AND s.create_time <= ").append(formatDateTime(query.getEndTime())).append(" ");
}
return getJdbcTemplate().queryForObject(sql.toString(), Integer.class);
}
private List<String> listFaceIdsWithUpload(CommonQueryReq query) {
StringBuilder sql = new StringBuilder();
sql.append("SELECT DISTINCT r.identifier FROM t_stats_record r ");
sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
sql.append("WHERE r.action = 'FACE_UPLOAD' ");
sql.append("AND r.trace_id IN (");
appendEnterScenicTraceIdSubQuery(sql, query.getScenicId(), query.getStartTime(), query.getEndTime());
sql.append(") ");
if (query.getStartTime() != null) {
sql.append("AND s.create_time >= ").append(formatDateTime(query.getStartTime())).append(" ");
}
if (query.getEndTime() != null) {
sql.append("AND s.create_time <= ").append(formatDateTime(query.getEndTime())).append(" ");
}
return getJdbcTemplate().queryForList(sql.toString(), String.class);
}
@Override
public Integer countCompleteVideoOfMember(CommonQueryReq query) {
List<String> faceIds = listFaceIdsWithUpload(query);
if (faceIds == null || faceIds.isEmpty()) {
return 0;
}
return taskMapper.countCompletedTaskMembersByFaceIds(faceIds);
}
@Override
public Integer countCompleteOfVideo(CommonQueryReq query) {
List<String> faceIds = listFaceIdsWithUpload(query);
if (faceIds == null || faceIds.isEmpty()) {
return 0;
}
return taskMapper.countCompletedTasksByFaceIds(faceIds);
}
@Override
public Integer countTotalVisitorOfMember(CommonQueryReq query) {
StringBuilder sql = new StringBuilder();
sql.append("SELECT toInt32(uniqExact(s.member_id)) AS count ");
sql.append("FROM t_stats_record r ");
sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
sql.append("WHERE r.trace_id IN (");
appendEnterScenicTraceIdSubQuery(sql, query.getScenicId(), query.getStartTime(), query.getEndTime());
sql.append(") ");
sql.append("AND r.action = 'LAUNCH' ");
if (query.getStartTime() != null) {
sql.append("AND s.create_time >= ").append(formatDateTime(query.getStartTime())).append(" ");
}
if (query.getEndTime() != null) {
sql.append("AND s.create_time <= ").append(formatDateTime(query.getEndTime())).append(" ");
}
return getJdbcTemplate().queryForObject(sql.toString(), Integer.class);
}
@Override
public Integer countPreviewOfVideo(CommonQueryReq query) {
StringBuilder sql = new StringBuilder();
sql.append("WITH JSONExtractString(params, 'id') AS videoId, ");
sql.append(" JSONExtractString(params, 'share') AS share ");
sql.append("SELECT toInt32(uniqExact(videoId)) AS count ");
sql.append("FROM t_stats_record r ");
sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
sql.append("WHERE r.trace_id IN (");
appendEnterScenicTraceIdSubQuery(sql, query.getScenicId(), query.getStartTime(), query.getEndTime());
sql.append(") ");
sql.append("AND r.action = 'LOAD' ");
sql.append("AND r.identifier = 'pages/videoSynthesis/buy' ");
sql.append("AND videoId != '' ");
sql.append("AND share = '' ");
if (query.getStartTime() != null) {
sql.append("AND s.create_time >= ").append(formatDateTime(query.getStartTime())).append(" ");
}
if (query.getEndTime() != null) {
sql.append("AND s.create_time <= ").append(formatDateTime(query.getEndTime())).append(" ");
}
return getJdbcTemplate().queryForObject(sql.toString(), Integer.class);
}
@Override
public List<Long> getBrokerIdListForUser(Long memberId, Date startTime, Date endTime) {
StringBuilder sql = new StringBuilder();
sql.append("SELECT toInt64(r.identifier) AS identifier ");
sql.append("FROM t_stats_record r ");
sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
sql.append("WHERE r.action = 'CODE_SCAN' ");
sql.append(" AND s.member_id = ").append(memberId).append(" ");
if (startTime != null) {
sql.append(" AND r.create_time >= ").append(formatDateTime(startTime)).append(" ");
}
if (endTime != null) {
sql.append(" AND r.create_time <= ").append(formatDateTime(endTime)).append(" ");
}
sql.append("GROUP BY r.identifier ");
sql.append("ORDER BY max(r.create_time) DESC");
return getJdbcTemplate().queryForList(sql.toString(), Long.class);
}
@Override
public Long getUserRecentEnterType(Long memberId, Date endTime) {
StringBuilder sql = new StringBuilder();
sql.append("SELECT JSONExtractInt(r.params, 'scene') AS scene ");
sql.append("FROM t_stats_record r ");
sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
sql.append("WHERE r.action = 'LAUNCH' ");
sql.append(" AND s.member_id = ").append(memberId).append(" ");
if (endTime != null) {
sql.append(" AND r.create_time <= ").append(formatDateTime(endTime)).append(" ");
}
sql.append("ORDER BY r.create_time DESC LIMIT 1");
try {
return getJdbcTemplate().queryForObject(sql.toString(), Long.class);
} catch (Exception e) {
return null;
}
}
@Override
public List<Long> getProjectIdListForUser(Long memberId, Date startTime, Date endTime) {
StringBuilder sql = new StringBuilder();
sql.append("SELECT toInt64(r.identifier) AS identifier ");
sql.append("FROM t_stats_record r ");
sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
sql.append("WHERE s.member_id = ").append(memberId).append(" ");
sql.append(" AND r.action = 'ENTER_PROJECT' ");
sql.append(" AND r.create_time < ").append(formatDateTime(endTime)).append(" ");
sql.append(" AND r.create_time > ").append(formatDateTime(startTime)).append(" ");
sql.append("ORDER BY r.create_time DESC LIMIT 1");
return getJdbcTemplate().queryForList(sql.toString(), Long.class);
}
@Override
public Integer countBrokerScanCount(Long brokerId) {
String sql = "SELECT count(1) AS count FROM t_stats_record " +
"WHERE action = 'CODE_SCAN' AND identifier = '" + brokerId + "'";
return getJdbcTemplate().queryForObject(sql, Integer.class);
}
@Override
public List<HashMap<String, Object>> getDailyScanStats(Long brokerId, Date startTime, Date endTime) {
SimpleDateFormat dateFormat = createDateFormat();
String startDateStr = dateFormat.format(startTime);
String endDateStr = dateFormat.format(endTime);
String startDateTimeStr = "'" + startDateStr + " 00:00:00'";
String endDateTimeStr = "'" + endDateStr + " 23:59:59'";
String sql = "SELECT toDate(create_time) AS date, count(DISTINCT id) AS scanCount " +
"FROM t_stats_record " +
"WHERE action = 'CODE_SCAN' " +
" AND identifier = '" + brokerId + "' " +
" AND create_time >= " + startDateTimeStr + " " +
" AND create_time <= " + endDateTimeStr + " " +
"GROUP BY toDate(create_time) " +
"ORDER BY toDate(create_time)";
return getJdbcTemplate().query(sql, (rs, rowNum) -> {
HashMap<String, Object> map = new HashMap<>();
map.put("date", rs.getDate("date"));
map.put("scanCount", rs.getLong("scanCount"));
return map;
});
}
@Override
public List<HashMap<String, String>> scanCodeMemberChartByHour(CommonQueryReq query) {
StringBuilder sql = new StringBuilder();
sql.append("SELECT formatDateTime(toStartOfHour(s.create_time), '%m-%d %H') AS t, ");
sql.append(" uniqExact(s.member_id) AS count ");
sql.append("FROM t_stats_record r ");
sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
sql.append("WHERE r.trace_id IN (");
appendEnterScenicTraceIdSubQuery(sql, query.getScenicId(), query.getStartTime(), query.getEndTime());
sql.append(") ");
sql.append("AND r.action = 'LAUNCH' ");
sql.append("AND JSONExtractInt(r.params, 'scene') IN (1047, 1048, 1049) ");
sql.append("GROUP BY toStartOfHour(s.create_time) ");
sql.append("ORDER BY toStartOfHour(s.create_time)");
List<HashMap<String, String>> rawData = getJdbcTemplate().query(sql.toString(), (rs, rowNum) -> {
HashMap<String, String> map = new HashMap<>();
map.put("t", rs.getString("t"));
map.put("count", rs.getString("count"));
return map;
});
return fillHourSeries(rawData, query.getStartTime(), query.getEndTime());
}
@Override
public List<HashMap<String, String>> scanCodeMemberChartByDate(CommonQueryReq query) {
StringBuilder sql = new StringBuilder();
sql.append("SELECT formatDateTime(toStartOfDay(s.create_time), '%m-%d') AS t, ");
sql.append(" uniqExact(s.member_id) AS count ");
sql.append("FROM t_stats_record r ");
sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
sql.append("WHERE r.trace_id IN (");
appendEnterScenicTraceIdSubQuery(sql, query.getScenicId(), query.getStartTime(), query.getEndTime());
sql.append(") ");
sql.append("AND r.action = 'LAUNCH' ");
sql.append("AND JSONExtractInt(r.params, 'scene') IN (1047, 1048, 1049) ");
sql.append("GROUP BY toStartOfDay(s.create_time) ");
sql.append("ORDER BY toStartOfDay(s.create_time)");
List<HashMap<String, String>> rawData = getJdbcTemplate().query(sql.toString(), (rs, rowNum) -> {
HashMap<String, String> map = new HashMap<>();
map.put("t", rs.getString("t"));
map.put("count", rs.getString("count"));
return map;
});
return fillDateSeries(rawData, query.getStartTime(), query.getEndTime());
}
@Override
public List<HashMap<String, String>> printerFromSampleChartByHour(CommonQueryReq query) {
StringBuilder sql = new StringBuilder();
sql.append("SELECT formatDateTime(toStartOfHour(s.create_time), '%m-%d %H') AS t, ");
sql.append(" uniqExact(s.member_id) AS count ");
sql.append("FROM t_stats_record r ");
sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
sql.append("WHERE r.action = 'LOAD' ");
sql.append("AND r.identifier = 'pages/printer/hello' ");
if (query.getScenicId() != null) {
sql.append("AND JSONExtractString(r.params, 'scenicId') = '").append(query.getScenicId()).append("' ");
}
if (query.getStartTime() != null) {
sql.append("AND r.create_time >= ").append(formatDateTime(query.getStartTime())).append(" ");
}
if (query.getEndTime() != null) {
sql.append("AND r.create_time <= ").append(formatDateTime(query.getEndTime())).append(" ");
}
sql.append("GROUP BY toStartOfHour(s.create_time) ");
sql.append("ORDER BY toStartOfHour(s.create_time)");
List<HashMap<String, String>> rawData = getJdbcTemplate().query(sql.toString(), (rs, rowNum) -> {
HashMap<String, String> map = new HashMap<>();
map.put("t", rs.getString("t"));
map.put("count", rs.getString("count"));
return map;
});
return fillHourSeries(rawData, query.getStartTime(), query.getEndTime());
}
@Override
public List<HashMap<String, String>> printerFromSampleChartByDate(CommonQueryReq query) {
StringBuilder sql = new StringBuilder();
sql.append("SELECT formatDateTime(toStartOfDay(s.create_time), '%m-%d') AS t, ");
sql.append(" uniqExact(s.member_id) AS count ");
sql.append("FROM t_stats_record r ");
sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
sql.append("WHERE r.action = 'LOAD' ");
sql.append("AND r.identifier = 'pages/printer/hello' ");
if (query.getScenicId() != null) {
sql.append("AND JSONExtractString(r.params, 'scenicId') = '").append(query.getScenicId()).append("' ");
}
if (query.getStartTime() != null) {
sql.append("AND r.create_time >= ").append(formatDateTime(query.getStartTime())).append(" ");
}
if (query.getEndTime() != null) {
sql.append("AND r.create_time <= ").append(formatDateTime(query.getEndTime())).append(" ");
}
sql.append("GROUP BY toStartOfDay(s.create_time) ");
sql.append("ORDER BY toStartOfDay(s.create_time)");
List<HashMap<String, String>> rawData = getJdbcTemplate().query(sql.toString(), (rs, rowNum) -> {
HashMap<String, String> map = new HashMap<>();
map.put("t", rs.getString("t"));
map.put("count", rs.getString("count"));
return map;
});
return fillDateSeries(rawData, query.getStartTime(), query.getEndTime());
}
/**
* 填充小时序列,确保每个小时都有数据(缺失的填充为0)
*/
private List<HashMap<String, String>> fillHourSeries(List<HashMap<String, String>> rawData, Date startTime, Date endTime) {
if (startTime == null || endTime == null) {
return rawData;
}
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM-dd HH");
LocalDateTime start = startTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime().truncatedTo(ChronoUnit.HOURS);
LocalDateTime end = endTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime().truncatedTo(ChronoUnit.HOURS);
// 将原始数据转为 Map 以便快速查找
Map<String, String> dataMap = rawData.stream()
.collect(Collectors.toMap(
m -> m.get("t"),
m -> m.get("count"),
(existing, replacement) -> existing
));
List<HashMap<String, String>> result = new ArrayList<>();
LocalDateTime current = start;
while (!current.isAfter(end)) {
String timeKey = current.format(formatter);
HashMap<String, String> item = new HashMap<>();
item.put("t", timeKey);
item.put("count", dataMap.getOrDefault(timeKey, "0"));
result.add(item);
current = current.plusHours(1);
}
return result;
}
/**
* 填充日期序列,确保每天都有数据(缺失的填充为0)
*/
private List<HashMap<String, String>> fillDateSeries(List<HashMap<String, String>> rawData, Date startTime, Date endTime) {
if (startTime == null || endTime == null) {
return rawData;
}
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM-dd");
LocalDate start = startTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
LocalDate end = endTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
// 将原始数据转为 Map 以便快速查找
Map<String, String> dataMap = rawData.stream()
.collect(Collectors.toMap(
m -> m.get("t"),
m -> m.get("count"),
(existing, replacement) -> existing
));
List<HashMap<String, String>> result = new ArrayList<>();
LocalDate current = start;
while (!current.isAfter(end)) {
String timeKey = current.format(formatter);
HashMap<String, String> item = new HashMap<>();
item.put("t", timeKey);
item.put("count", dataMap.getOrDefault(timeKey, "0"));
result.add(item);
current = current.plusDays(1);
}
return result;
}
}

View File

@@ -0,0 +1,111 @@
package com.ycwl.basic.clickhouse.service.impl;
import com.ycwl.basic.clickhouse.service.StatsQueryService;
import com.ycwl.basic.mapper.StatisticsMapper;
import com.ycwl.basic.model.mobile.statistic.req.CommonQueryReq;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
/**
* MySQL 统计数据查询服务实现
* 当 clickhouse.enabled 未启用时使用此实现(兜底)
*/
@Slf4j
@Service
@ConditionalOnProperty(prefix = "clickhouse", name = "enabled", havingValue = "false", matchIfMissing = true)
public class MySqlStatsQueryServiceImpl implements StatsQueryService {
@Autowired
private StatisticsMapper statisticsMapper;
@Override
public Integer countPreviewVideoOfMember(CommonQueryReq query) {
return statisticsMapper.countPreviewVideoOfMember(query);
}
@Override
public Integer countScanCodeOfMember(CommonQueryReq query) {
return statisticsMapper.countScanCodeOfMember(query);
}
@Override
public Integer countPushOfMember(CommonQueryReq query) {
return statisticsMapper.countPushOfMember(query);
}
@Override
public Integer countUploadFaceOfMember(CommonQueryReq query) {
return statisticsMapper.countUploadFaceOfMember(query);
}
@Override
public Integer countCompleteVideoOfMember(CommonQueryReq query) {
return statisticsMapper.countCompleteVideoOfMember(query);
}
@Override
public Integer countCompleteOfVideo(CommonQueryReq query) {
return statisticsMapper.countCompleteOfVideo(query);
}
@Override
public Integer countTotalVisitorOfMember(CommonQueryReq query) {
return statisticsMapper.countTotalVisitorOfMember(query);
}
@Override
public Integer countPreviewOfVideo(CommonQueryReq query) {
return statisticsMapper.countPreviewOfVideo(query);
}
@Override
public List<Long> getBrokerIdListForUser(Long memberId, Date startTime, Date endTime) {
return statisticsMapper.getBrokerIdListForUser(memberId, startTime, endTime);
}
@Override
public Long getUserRecentEnterType(Long memberId, Date endTime) {
return statisticsMapper.getUserRecentEnterType(memberId, endTime);
}
@Override
public List<Long> getProjectIdListForUser(Long memberId, Date startTime, Date endTime) {
return statisticsMapper.getProjectIdListForUser(memberId, startTime, endTime);
}
@Override
public Integer countBrokerScanCount(Long brokerId) {
return statisticsMapper.countBrokerScanCount(brokerId);
}
@Override
public List<HashMap<String, Object>> getDailyScanStats(Long brokerId, Date startTime, Date endTime) {
return statisticsMapper.getDailyScanStats(brokerId, startTime, endTime);
}
@Override
public List<HashMap<String, String>> scanCodeMemberChartByHour(CommonQueryReq query) {
return statisticsMapper.scanCodeMemberChartByHour(query);
}
@Override
public List<HashMap<String, String>> scanCodeMemberChartByDate(CommonQueryReq query) {
return statisticsMapper.scanCodeMemberChartByDate(query);
}
@Override
public List<HashMap<String, String>> printerFromSampleChartByHour(CommonQueryReq query) {
return statisticsMapper.printerFromSampleChartByHour(query);
}
@Override
public List<HashMap<String, String>> printerFromSampleChartByDate(CommonQueryReq query) {
return statisticsMapper.printerFromSampleChartByDate(query);
}
}

View File

@@ -0,0 +1,37 @@
package com.ycwl.basic.config;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import javax.sql.DataSource;
/**
* ClickHouse 数据源配置
* 用于 t_stats 和 t_stats_record 表的查询
*
* 使用 NamedParameterJdbcTemplate 而非 MyBatis,以避免干扰 MyBatis-Plus 的自动配置
*/
@Configuration
@ConditionalOnProperty(prefix = "clickhouse", name = "enabled", havingValue = "true")
public class ClickHouseDataSourceConfig {
/**
* ClickHouse 数据源(非 Primary)
*/
@Bean(name = "clickHouseDataSource")
@ConfigurationProperties(prefix = "clickhouse.datasource")
public DataSource clickHouseDataSource() {
return new HikariDataSource();
}
@Bean(name = "clickHouseJdbcTemplate")
public NamedParameterJdbcTemplate clickHouseJdbcTemplate(
@Qualifier("clickHouseDataSource") DataSource dataSource) {
return new NamedParameterJdbcTemplate(dataSource);
}
}

View File

@@ -1,34 +1,26 @@
package com.ycwl.basic.config; package com.ycwl.basic.config;
import org.springframework.cache.annotation.CachingConfigurerSupport; import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.annotation.EnableCaching; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
/** /**
* @author wenshijia * @author wenshijia
* @date 2021年07月05日 18:34 * @date 2021年07月05日 18:34
* 修改redis缓存序列化器 * 修改redis缓存序列化器
*/ */
@Configuration @Configuration
@EnableCaching public class CustomRedisCacheManager {
public class CustomRedisCacheManager extends CachingConfigurerSupport { @Autowired
private ObjectMapper objectMapper;
@Bean
public RedisCacheConfiguration redisCacheConfiguration() {
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig();
configuration = configuration.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)).entryTtl(Duration.ofMinutes(1));
return configuration;
}
/** /**
* 处理redis连接工具显示redis key值显示乱码问题,value值没处理 * 处理redis连接工具显示redis key值显示乱码问题,value值没处理
@@ -45,10 +37,23 @@ public class CustomRedisCacheManager extends CachingConfigurerSupport {
final StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); final StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringRedisSerializer); redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setValueSerializer(stringRedisSerializer);
redisTemplate.setHashKeySerializer(stringRedisSerializer); redisTemplate.setHashKeySerializer(stringRedisSerializer);
// Configure Jackson2JsonRedisSerializer with JavaTimeModule for value serialization
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
// Configure type handling to prevent ClassCastException
PolymorphicTypeValidator typeValidator = BasicPolymorphicTypeValidator.builder()
.allowIfBaseType(Object.class)
.build();
objectMapper.activateDefaultTyping(typeValidator, ObjectMapper.DefaultTyping.NON_FINAL);
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(objectMapper, Object.class);
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet(); redisTemplate.afterPropertiesSet();
return redisTemplate; return redisTemplate;
} }

View File

@@ -1,63 +0,0 @@
package com.ycwl.basic.config;
import feign.Logger;
import feign.RequestInterceptor;
import feign.codec.ErrorDecoder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import jakarta.servlet.http.HttpServletRequest;
@Slf4j
@Configuration
public class FeignConfig {
@Bean
public Logger.Level feignLoggerLevel() {
return Logger.Level.BASIC;
}
@Bean
public RequestInterceptor requestInterceptor() {
return requestTemplate -> {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
// 传递认证头
String authorization = request.getHeader("Authorization");
if (authorization != null) {
requestTemplate.header("Authorization", authorization);
}
}
};
}
@Bean
public ErrorDecoder errorDecoder() {
return new FeignErrorDecoder();
}
public static class FeignErrorDecoder implements ErrorDecoder {
private final ErrorDecoder defaultErrorDecoder = new Default();
@Override
public Exception decode(String methodKey, feign.Response response) {
log.error("Feign调用失败: method={}, status={}, reason={}",
methodKey, response.status(), response.reason());
if (response.status() >= 400 && response.status() < 500) {
// 4xx错误,客户端错误
return new RuntimeException("客户端请求错误: " + response.reason());
} else if (response.status() >= 500) {
// 5xx错误,服务器错误
return new RuntimeException("服务器内部错误: " + response.reason());
}
return defaultErrorDecoder.decode(methodKey, response);
}
}
}

View File

@@ -0,0 +1,109 @@
package com.ycwl.basic.config;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.apache.kafka.common.serialization.StringSerializer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.core.*;
import org.springframework.kafka.listener.ContainerProperties;
import java.util.HashMap;
import java.util.Map;
@Configuration
@ConditionalOnProperty(name = "kafka.enabled", havingValue = "true", matchIfMissing = false)
public class KafkaConfig {
@Value("${kafka.bootstrap-servers:100.64.0.12:39092}")
private String bootstrapServers;
@Value("${kafka.consumer.group-id:liuying-microservice}")
private String consumerGroupId;
@Value("${kafka.consumer.auto-offset-reset:earliest}")
private String autoOffsetReset;
@Value("${kafka.producer.acks:all}")
private String acks;
@Value("${kafka.producer.retries:3}")
private Integer retries;
@Value("${kafka.producer.batch-size:16384}")
private Integer batchSize;
@Value("${kafka.producer.linger-ms:1}")
private Integer lingerMs;
@Value("${kafka.producer.buffer-memory:33554432}")
private Integer bufferMemory;
@Value("${kafka.producer.enable-idempotence:true}")
private boolean enableIdempotence;
@Value("${kafka.producer.compression-type:snappy}")
private String compressionType;
@Bean
public ProducerFactory<String, String> producerFactory() {
Map<String, Object> configProps = new HashMap<>();
configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
configProps.put(ProducerConfig.ACKS_CONFIG, acks);
configProps.put(ProducerConfig.RETRIES_CONFIG, retries);
configProps.put(ProducerConfig.BATCH_SIZE_CONFIG, batchSize);
configProps.put(ProducerConfig.LINGER_MS_CONFIG, lingerMs);
configProps.put(ProducerConfig.BUFFER_MEMORY_CONFIG, bufferMemory);
configProps.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, enableIdempotence);
configProps.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, compressionType);
return new DefaultKafkaProducerFactory<>(configProps);
}
@Bean
public KafkaTemplate<String, String> kafkaTemplate() {
return new KafkaTemplate<>(producerFactory());
}
@Bean
public ConsumerFactory<String, String> consumerFactory() {
Map<String, Object> props = new HashMap<>();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
props.put(ConsumerConfig.GROUP_ID_CONFIG, consumerGroupId);
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, autoOffsetReset);
return new DefaultKafkaConsumerFactory<>(props);
}
@Bean
public ConcurrentKafkaListenerContainerFactory<String, String> kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory());
return factory;
}
@Bean
public ConcurrentKafkaListenerContainerFactory<String, String> manualCommitKafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
Map<String, Object> props = new HashMap<>();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
props.put(ConsumerConfig.GROUP_ID_CONFIG, consumerGroupId);
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, autoOffsetReset);
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
factory.setConsumerFactory(new DefaultKafkaConsumerFactory<>(props));
factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL_IMMEDIATE);
return factory;
}
}

View File

@@ -0,0 +1,41 @@
package com.ycwl.basic.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import javax.sql.DataSource;
/**
* MySQL 主数据源配置
*
* 当 ClickHouse 启用时,需要显式配置 MySQL 数据源并标记为 @Primary,
* 以确保 MyBatis-Plus 和其他组件使用正确的数据源
*/
@Configuration
@ConditionalOnProperty(prefix = "clickhouse", name = "enabled", havingValue = "true")
public class MySqlPrimaryDataSourceConfig {
/**
* MySQL 数据源属性
*/
@Primary
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSourceProperties mysqlDataSourceProperties() {
return new DataSourceProperties();
}
/**
* MySQL 主数据源
* 使用 @Primary 确保这是默认数据源
*/
@Primary
@Bean(name = "dataSource")
public DataSource mysqlDataSource(DataSourceProperties properties) {
return properties.initializeDataSourceBuilder().build();
}
}

View File

@@ -3,6 +3,7 @@ package com.ycwl.basic.config;
import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@@ -11,6 +12,15 @@ import org.springframework.context.annotation.Configuration;
* @date 2021年06月04日 9:42 * @date 2021年06月04日 9:42
*/ */
@Configuration @Configuration
@MapperScan(basePackages = {
"com.ycwl.basic.mapper",
"com.ycwl.basic.order.mapper",
"com.ycwl.basic.pricing.mapper",
"com.ycwl.basic.product.mapper",
"com.ycwl.basic.profitsharing.mapper",
"com.ycwl.basic.puzzle.mapper",
"com.ycwl.basic.stats.mapper"
})
public class MybatisPlusPageConfig { public class MybatisPlusPageConfig {
/* 旧版本配置 /* 旧版本配置

View File

@@ -0,0 +1,32 @@
package com.ycwl.basic.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 视频更新检查配置
* @author Claude
*/
@Data
@Component
@ConfigurationProperties(prefix = "video.update")
public class VideoUpdateConfig {
/**
* 是否将片段变化检测为新增
* true: 任何变化都视为新增
* false: 只有数量增加才视为新增
*/
private boolean detectChangesAsNew = true;
/**
* 最小新增片段数量才认为可更新
*/
private int minNewSegmentCount = 1;
/**
* 是否启用视频更新检查功能
*/
private boolean enabled = true;
}

View File

@@ -3,7 +3,7 @@ package com.ycwl.basic.config;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.module.SimpleModule;
import com.ycwl.basic.interceptor.AuthInterceptor; import com.ycwl.basic.interceptor.AuthInterceptor;
import com.ycwl.basic.stats.interceptor.StatsInterceptor; import com.ycwl.basic.puzzle.edge.interceptor.PuzzleEdgeWorkerIpInterceptor;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@@ -25,20 +25,19 @@ import java.util.List;
*/ */
@Configuration @Configuration
public class WebMvcConfig implements WebMvcConfigurer { public class WebMvcConfig implements WebMvcConfigurer {
@Autowired @Autowired
private AuthInterceptor authInterceptor; private AuthInterceptor authInterceptor;
@Autowired @Autowired
private StatsInterceptor statsInterceptor; private PuzzleEdgeWorkerIpInterceptor puzzleEdgeWorkerIpInterceptor;
@Override @Override
public void addInterceptors(InterceptorRegistry registry) { public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(puzzleEdgeWorkerIpInterceptor)
.addPathPatterns("/puzzle/render/v1/**");
registry.addInterceptor(authInterceptor) registry.addInterceptor(authInterceptor)
// 拦截除指定接口外的所有请求,通过判断 注解 来决定是否需要做登录验证 // 拦截除指定接口外的所有请求,通过判断 注解 来决定是否需要做登录验证
.addPathPatterns("/**") .addPathPatterns("/**")
.excludePathPatterns("/swagger-resources/**", "/webjars/**", "/v2/api-docs", "/doc.html/**", "/error", "/"); .excludePathPatterns("/swagger-resources/**", "/webjars/**", "/v2/api-docs", "/doc.html/**", "/error", "/");
registry.addInterceptor(statsInterceptor)
.addPathPatterns("/api/mobile/**");
} }
/** /**

View File

@@ -0,0 +1,71 @@
package com.ycwl.basic.constant;
/**
* 购买状态枚举
* 定义源文件的已购买和未购买两种状态
*
* @author Claude
* @since 2025-10-31
*/
public enum BuyStatus {
/**
* 未购买状态
*/
NOT_BOUGHT(0, "未购买"),
/**
* 已购买状态
*/
BOUGHT(1, "已购买");
private final int code;
private final String description;
BuyStatus(int code, String description) {
this.code = code;
this.description = description;
}
public int getCode() {
return code;
}
public String getDescription() {
return description;
}
/**
* 根据代码值获取枚举
*
* @param code 状态代码
* @return 对应的枚举值,如果不存在返回 null
*/
public static BuyStatus fromCode(int code) {
for (BuyStatus status : values()) {
if (status.code == code) {
return status;
}
}
return null;
}
/**
* 判断给定的代码是否为已购买状态
*
* @param code 状态代码
* @return true-已购买,false-未购买
*/
public static boolean isBought(Integer code) {
return code != null && code == BOUGHT.code;
}
/**
* 判断给定的代码是否为未购买状态
*
* @param code 状态代码
* @return true-未购买,false-已购买
*/
public static boolean isNotBought(Integer code) {
return code != null && code == NOT_BOUGHT.code;
}
}

View File

@@ -4,4 +4,7 @@ public class FaceConstant {
public static final String FACE_DB_NAME_PFX="face:db:"; public static final String FACE_DB_NAME_PFX="face:db:";
public static final String USER_FACE_DB_NAME="userFace"; public static final String USER_FACE_DB_NAME="userFace";
public static final String FACE_USER_URL_PFX="face:user:url:"; public static final String FACE_USER_URL_PFX="face:user:url:";
public static final String FACE_RECOGNITION_COUNT_PFX="face:recognition:count:";
public static final String FACE_CUSTOM_MATCH_COUNT_PFX="face:custom:match:count:";
public static final String FACE_LOW_THRESHOLD_PFX="face:low:threshold:";
} }

View File

@@ -0,0 +1,71 @@
package com.ycwl.basic.constant;
/**
* 免费状态枚举
* 定义源文件的收费和免费两种状态
*
* @author Claude
* @since 2025-10-31
*/
public enum FreeStatus {
/**
* 收费状态
*/
PAID(0, "收费"),
/**
* 免费状态
*/
FREE(1, "免费");
private final int code;
private final String description;
FreeStatus(int code, String description) {
this.code = code;
this.description = description;
}
public int getCode() {
return code;
}
public String getDescription() {
return description;
}
/**
* 根据代码值获取枚举
*
* @param code 状态代码
* @return 对应的枚举值,如果不存在返回 null
*/
public static FreeStatus fromCode(int code) {
for (FreeStatus status : values()) {
if (status.code == code) {
return status;
}
}
return null;
}
/**
* 判断给定的代码是否为免费状态
*
* @param code 状态代码
* @return true-免费,false-收费
*/
public static boolean isFree(Integer code) {
return code != null && code == FREE.code;
}
/**
* 判断给定的代码是否为收费状态
*
* @param code 状态代码
* @return true-收费,false-免费
*/
public static boolean isPaid(Integer code) {
return code != null && code == PAID.code;
}
}

View File

@@ -1,18 +0,0 @@
package com.ycwl.basic.constant;
public class ShareParkingSpaceRedisKeyConstant {
// 更改数量时候的锁
public final static String UPDATE_NUMBER_LOCK_KEY="ShareParking:updateNumberLockKey";
// 地上车位
public final static String GROUND_PARKING_SPACE_NUMBER="ShareParking:groundParkingSpaceNumber";
// 地下车位数
public final static String UNDERGROUND_PARKING_SPACE_NUMBER="ShareParking:undergroundParkingSpaceNumber";
// 每日开放预约时间
public final static String OPEN_TIME="ShareParking:openTime";
// 预约后当日车辆最晚停留时间
public final static String RESIDENCE_TIME="ShareParking:residenceTime";
//取消时间
public final static String CANCEL_TIME="ShareParking:cancelTime";
//支付时间
public final static String PAY_TIME="ShareParking:payTime";
}

View File

@@ -0,0 +1,86 @@
package com.ycwl.basic.constant;
/**
* 源文件类型枚举
* 定义视频和图片两种源文件类型
*
* @author Claude
* @since 2025-10-31
*/
public enum SourceType {
/**
* 视频类型
*/
VIDEO(1, "视频"),
/**
* 图片类型
*/
IMAGE(2, "图片"),
/**
* AI微单类型
*/
AI_CAM(3, "AI微单");
private final int code;
private final String description;
SourceType(int code, String description) {
this.code = code;
this.description = description;
}
public int getCode() {
return code;
}
public String getDescription() {
return description;
}
/**
* 根据代码值获取枚举
*
* @param code 类型代码
* @return 对应的枚举值,如果不存在返回 null
*/
public static SourceType fromCode(int code) {
for (SourceType type : values()) {
if (type.code == code) {
return type;
}
}
return null;
}
/**
* 判断给定的代码是否为视频类型
*
* @param code 类型代码
* @return true-是视频,false-不是视频
*/
public static boolean isVideo(Integer code) {
return code != null && code == VIDEO.code;
}
/**
* 判断给定的代码是否为图片类型
*
* @param code 类型代码
* @return true-是图片,false-不是图片
*/
public static boolean isImage(Integer code) {
return code != null && code == IMAGE.code;
}
/**
* 判断给定的代码是否为AI微单类型
*
* @param code 类型代码
* @return true-是AI微单,false-不是AI微单
*/
public static boolean isAiCam(Integer code) {
return code != null && code == AI_CAM.code;
}
}

View File

@@ -3,8 +3,7 @@ package com.ycwl.basic.constant;
public class StorageConstant { public class StorageConstant {
public static final String VLOG_PATH = "vlog"; public static final String VLOG_PATH = "vlog";
public static final String VIDEO_PIECE_PATH = "source_video"; public static final String VIDEO_PIECE_PATH = "source_video";
public static final String PHOTO_PATH = "source_photo"; public static final String PHOTO_PATH = "viid";
public static final String PHOTO_WATERMARKED_PATH = "photo_w"; public static final String PHOTO_WATERMARKED_PATH = "photo_w";
public static final String VIID_FACE = "viid_face";
public static final String USER_FACE = "user_face"; public static final String USER_FACE = "user_face";
} }

View File

@@ -0,0 +1,112 @@
package com.ycwl.basic.controller;
import com.github.pagehelper.PageInfo;
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.VideoReviewAddReqDTO;
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.VideoReviewStatisticsRespDTO;
import com.ycwl.basic.service.VideoReviewService;
import com.ycwl.basic.utils.ApiResponse;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.net.URLEncoder;
/**
* 视频评价Controller
* 管理端使用,通过token角色控制权限
*/
@Slf4j
@RestController
@RequestMapping("/api/video-review/v1")
public class VideoReviewController {
@Autowired
private VideoReviewService videoReviewService;
/**
* 新增视频评价
*
* @param reqDTO 评价信息
* @return 评价ID
*/
@PostMapping("/add")
public ApiResponse<Long> addReview(@RequestBody VideoReviewAddReqDTO reqDTO) {
log.info("新增视频评价,videoId: {}", reqDTO.getVideoId());
Long reviewId = videoReviewService.addReview(reqDTO);
return ApiResponse.success(reviewId);
}
/**
* 分页查询评价列表
*
* @param reqDTO 查询条件
* @return 分页结果
*/
@GetMapping("/list")
public ApiResponse<PageInfo<VideoReviewRespDTO>> getReviewList(VideoReviewListReqDTO reqDTO) {
log.info("查询视频评价列表,pageNum: {}, pageSize: {}", reqDTO.getPageNum(), reqDTO.getPageSize());
PageInfo<VideoReviewRespDTO> pageInfo = videoReviewService.getReviewList(reqDTO);
return ApiResponse.success(pageInfo);
}
/**
* 获取评价统计数据
*
* @return 统计结果
*/
@GetMapping("/statistics")
public ApiResponse<VideoReviewStatisticsRespDTO> getStatistics() {
log.info("获取视频评价统计数据");
VideoReviewStatisticsRespDTO statistics = videoReviewService.getStatistics();
return ApiResponse.success(statistics);
}
/**
* 导出评价数据到Excel
*
* @param reqDTO 查询条件
* @param response HTTP响应
*/
@GetMapping("/export")
public void exportReviews(VideoReviewListReqDTO reqDTO, HttpServletResponse response) {
log.info("导出视频评价数据");
try {
// 设置响应头
String fileName = "video_reviews_" + System.currentTimeMillis() + ".xlsx";
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("UTF-8");
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"));
// 导出数据
videoReviewService.exportReviews(reqDTO, response.getOutputStream());
response.getOutputStream().flush();
} catch (IOException e) {
log.error("导出视频评价数据失败", e);
throw new RuntimeException("导出失败: " + e.getMessage());
}
}
/**
* 检查视频是否已被购买
* 购买条件:
* 1. 直接购买视频(order_item中goods_type=0且goods_id=视频id)
* 2. 购买整个模板(order的face_id与video关联的task的face_id相同,goods_type=-1,goods_id为video的templateId)
*
* @param reqDTO 查询条件
* @return 购买状态及订单ID列表
*/
@PostMapping("/check-purchase")
public ApiResponse<VideoPurchaseCheckRespDTO> checkVideoPurchase(@RequestBody VideoPurchaseCheckReqDTO reqDTO) {
log.info("检查视频购买状态,videoId: {}", reqDTO.getVideoId());
VideoPurchaseCheckRespDTO respDTO = videoReviewService.checkVideoPurchase(reqDTO);
return ApiResponse.success(respDTO);
}
}

View File

@@ -4,6 +4,14 @@ import cn.hutool.http.HttpUtil;
import com.ycwl.basic.annotation.IgnoreToken; import com.ycwl.basic.annotation.IgnoreToken;
import com.ycwl.basic.image.enhancer.adapter.BceImageEnhancer; import com.ycwl.basic.image.enhancer.adapter.BceImageEnhancer;
import com.ycwl.basic.image.enhancer.entity.BceEnhancerConfig; import com.ycwl.basic.image.enhancer.entity.BceEnhancerConfig;
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
import com.ycwl.basic.image.pipeline.stages.DownloadStage;
import com.ycwl.basic.image.pipeline.stages.ImageEnhanceStage;
import com.ycwl.basic.image.pipeline.stages.ImageSRStage;
import com.ycwl.basic.image.pipeline.stages.SourcePhotoUpdateStage;
import com.ycwl.basic.image.pipeline.stages.CleanupStage;
import com.ycwl.basic.pipeline.core.Pipeline;
import com.ycwl.basic.pipeline.core.PipelineBuilder;
import com.ycwl.basic.mapper.AioDeviceMapper; import com.ycwl.basic.mapper.AioDeviceMapper;
import com.ycwl.basic.mapper.MemberMapper; import com.ycwl.basic.mapper.MemberMapper;
import com.ycwl.basic.model.aio.entity.AioDeviceBannerEntity; import com.ycwl.basic.model.aio.entity.AioDeviceBannerEntity;
@@ -12,29 +20,22 @@ import com.ycwl.basic.model.aio.entity.AioDevicePriceConfigEntity;
import com.ycwl.basic.model.aio.req.AioDeviceCreateOrderReq; import com.ycwl.basic.model.aio.req.AioDeviceCreateOrderReq;
import com.ycwl.basic.model.aio.resp.AioDeviceCreateOrderResp; import com.ycwl.basic.model.aio.resp.AioDeviceCreateOrderResp;
import com.ycwl.basic.model.aio.resp.AioDeviceInfoResp; import com.ycwl.basic.model.aio.resp.AioDeviceInfoResp;
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.mobile.goods.GoodsDetailVO; import com.ycwl.basic.model.mobile.goods.GoodsDetailVO;
import com.ycwl.basic.model.mobile.goods.GoodsReqQuery; import com.ycwl.basic.model.mobile.goods.GoodsReqQuery;
import com.ycwl.basic.model.pc.face.resp.FaceRespVO; import com.ycwl.basic.model.pc.face.resp.FaceRespVO;
import com.ycwl.basic.model.pc.member.entity.MemberEntity; import com.ycwl.basic.model.pc.member.entity.MemberEntity;
import com.ycwl.basic.model.pc.member.resp.MemberRespVO;
import com.ycwl.basic.pay.entity.PayResponse; import com.ycwl.basic.pay.entity.PayResponse;
import com.ycwl.basic.service.aio.AioDeviceService; import com.ycwl.basic.service.aio.AioDeviceService;
import com.ycwl.basic.service.mobile.GoodsService; import com.ycwl.basic.service.mobile.GoodsService;
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.pc.OrderService;
import com.ycwl.basic.service.pc.ScenicService;
import com.ycwl.basic.service.pc.SourceService; import com.ycwl.basic.service.pc.SourceService;
import com.ycwl.basic.storage.adapters.IStorageAdapter;
import com.ycwl.basic.utils.ApiResponse; import com.ycwl.basic.utils.ApiResponse;
import com.ycwl.basic.utils.JwtTokenUtil;
import com.ycwl.basic.utils.SnowFlakeUtil; import com.ycwl.basic.utils.SnowFlakeUtil;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@@ -48,7 +49,9 @@ import org.springframework.web.multipart.MultipartFile;
import java.io.File; import java.io.File;
import java.util.Date; import java.util.Date;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@Slf4j @Slf4j
@@ -119,7 +122,7 @@ public class AioDeviceController {
memberEntity.setId(SnowFlakeUtil.getLongId()); memberEntity.setId(SnowFlakeUtil.getLongId());
memberEntity.setNickname("用户"); memberEntity.setNickname("用户");
memberMapper.add(memberEntity); memberMapper.add(memberEntity);
FaceRecognizeResp resp = faceService.faceUpload(file, aioDevice.getScenicId(), memberEntity.getId()); FaceRecognizeResp resp = faceService.faceUpload(file, aioDevice.getScenicId(), memberEntity.getId(), "");
// 尝试超分 // 尝试超分
new Thread(() -> { new Thread(() -> {
try { try {
@@ -136,27 +139,38 @@ public class AioDeviceController {
redisTemplate.opsForValue().set("aio:faceId:"+resp.getFaceId().toString()+":pass", "1", 1, TimeUnit.DAYS); redisTemplate.opsForValue().set("aio:faceId:"+resp.getFaceId().toString()+":pass", "1", 1, TimeUnit.DAYS);
return; return;
} }
log.info("超分开始!"); log.info("超分开始!共{}张图片待处理", sourcePhotoList.size());
sourcePhotoList.forEach(photo -> { sourcePhotoList.forEach(photo -> {
if (StringUtils.contains(photo.getUrl(), "_q_")) { if (StringUtils.contains(photo.getUrl(), "_q_")) {
log.debug("跳过已增强的图片: {}", photo.getUrl());
return; return;
} }
try { try {
File dstFile = new File(photo.getGoodsId()+".jpg"); // 创建超分Pipeline
long fileSize = HttpUtil.downloadFile(photo.getUrl(), dstFile); Pipeline<PhotoProcessContext> superResolutionPipeline = createSuperResolutionPipeline(photo.getGoodsId());
log.info("超分开始:{}", fileSize);
BceImageEnhancer enhancer = getEnhancer(); // 使用静态工厂方法创建Context
MultipartFile enhancedFile = enhancer.enhance(dstFile.getName()); PhotoProcessContext context = PhotoProcessContext.forSuperResolution(
log.info("超分结束:{}", photo.getUrl()); photo.getGoodsId(), photo.getUrl(), photo.getScenicId()
String url = sourceService.uploadAndUpdateUrl(photo.getGoodsId(), enhancedFile); );
log.info("上传结束:->{}", url);
// 启用图像增强和超分的Stage
context.enableStage("image_enhance");
context.enableStage("image_sr");
// 执行Pipeline
boolean success = superResolutionPipeline.execute(context);
if (success) {
log.info("超分成功: {} -> {}", photo.getUrl(), context.getResultUrl());
} else {
log.error("超分失败: {}", photo.getGoodsId());
}
} catch (Exception e) { } catch (Exception e) {
log.error("超分失败:{}", photo.getGoodsId(), e); log.error("超分失败:{}", photo.getGoodsId(), e);
} finally {
File _file = new File(photo.getGoodsId()+".jpg");
if (_file.exists()) {
_file.delete();
}
} }
}); });
redisTemplate.opsForValue().set("aio:faceId:"+sourcePhotoList.getFirst().getFaceId().toString()+":pass", "1", 1, TimeUnit.DAYS); redisTemplate.opsForValue().set("aio:faceId:"+sourcePhotoList.getFirst().getFaceId().toString()+":pass", "1", 1, TimeUnit.DAYS);
@@ -213,6 +227,28 @@ public class AioDeviceController {
return ApiResponse.success(orderService.queryOrder(orderId)); return ApiResponse.success(orderService.queryOrder(orderId));
} }
/**
* 创建源图片超分辨率增强Pipeline
*
* @param sourceId 源图片ID
* @return 超分Pipeline
*/
private Pipeline<PhotoProcessContext> createSuperResolutionPipeline(Long sourceId) {
// 创建带有百度云配置的ImageEnhanceStage
BceEnhancerConfig config = new BceEnhancerConfig();
config.setQps(1);
config.setAppId("119554288");
config.setApiKey("OX6QoijgKio3eVtA0PiUVf7f");
config.setSecretKey("dYatXReVriPeiktTjUblhfubpcmYfuMk");
return new PipelineBuilder<PhotoProcessContext>("SourcePhotoSuperResolutionPipeline")
.addStage(new DownloadStage()) // 1. 下载图片
.addStage(new ImageEnhanceStage(config)).addStage(new ImageSRStage(config)) // 2. 图像增强(超分)
.addStage(new SourcePhotoUpdateStage(sourceService, sourceId)) // 3. 上传并更新数据库
.addStage(new CleanupStage()) // 4. 清理临时文件
.build();
}
private BceImageEnhancer getEnhancer() { private BceImageEnhancer getEnhancer() {
BceImageEnhancer enhancer = new BceImageEnhancer(); BceImageEnhancer enhancer = new BceImageEnhancer();
BceEnhancerConfig config = new BceEnhancerConfig(); BceEnhancerConfig config = new BceEnhancerConfig();

View File

@@ -18,7 +18,6 @@ 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.repository.VideoRepository; import com.ycwl.basic.repository.VideoRepository;
import com.ycwl.basic.repository.VideoTaskRepository; import com.ycwl.basic.repository.VideoTaskRepository;
import com.ycwl.basic.service.mobile.AppScenicService;
import com.ycwl.basic.service.mobile.GoodsService; import com.ycwl.basic.service.mobile.GoodsService;
import com.ycwl.basic.service.pc.FaceService; import com.ycwl.basic.service.pc.FaceService;
import com.ycwl.basic.service.task.impl.TaskTaskServiceImpl; import com.ycwl.basic.service.task.impl.TaskTaskServiceImpl;
@@ -59,10 +58,6 @@ public class LyCompatibleController {
@Autowired @Autowired
private VideoRepository videoRepository; private VideoRepository videoRepository;
@Autowired @Autowired
private VideoMapper videoMapper;
@Autowired
private TaskTaskServiceImpl taskTaskServiceImpl;
@Autowired
private RedisTemplate<String, String> redisTemplate; private RedisTemplate<String, String> redisTemplate;
@Autowired @Autowired
private VideoTaskRepository videoTaskRepository; private VideoTaskRepository videoTaskRepository;
@@ -114,7 +109,7 @@ public class LyCompatibleController {
} }
FaceRecognizeResp resp; FaceRecognizeResp resp;
try { try {
resp = faceService.faceUpload(file, scenicId, member.getId()); resp = faceService.faceUpload(file, scenicId, member.getId(), "");
} catch (Exception e) { } catch (Exception e) {
return R.error("上传失败!报错:"+e.getMessage()); return R.error("上传失败!报错:"+e.getMessage());
} }
@@ -208,25 +203,31 @@ public class LyCompatibleController {
return response; return response;
} }
List<Map<String, Object>> videoList = collect.get(0).stream().collect(Collectors.groupingBy(ContentPageVO::getTemplateId)) List<Map<String, Object>> videoList = collect.get(0).stream().collect(Collectors.groupingBy(ContentPageVO::getTemplateId))
.values().stream().map(contentPageVOs -> { .values().stream()
ContentPageVO contentPageVO = contentPageVOs.getFirst(); .map(contentPageVOs -> {
Map<String, Object> map = new HashMap<>(); ContentPageVO contentPageVO = contentPageVOs.stream().filter(vo -> vo.getContentId() != null).findFirst().orElse(null);
if (contentPageVO == null) {
return null;
}
VideoEntity videoRespVO = videoRepository.getVideo(contentPageVO.getContentId()); VideoEntity videoRespVO = videoRepository.getVideo(contentPageVO.getContentId());
if (videoRespVO == null) {
return null;
}
Map<String, Object> map = new HashMap<>();
map.put("id", videoRespVO.getId().toString()); map.put("id", videoRespVO.getId().toString());
map.put("task_id", videoRespVO.getTaskId().toString()); map.put("task_id", videoRespVO.getTaskId().toString());
TaskEntity task = videoTaskRepository.getTaskById(videoRespVO.getTaskId()); if (videoRespVO.getFaceId() != null) {
if (task != null) { map.put("face_id", String.valueOf(videoRespVO.getFaceId()));
map.put("face_id", String.valueOf(task.getFaceId()));
} }
map.put("template_cover_image", contentPageVO.getTemplateCoverUrl()); map.put("template_cover_image", contentPageVO.getTemplateCoverUrl());
Date taskShotDate = taskTaskServiceImpl.getTaskShotDate(videoRespVO.getTaskId()); Date taskShotDate = videoTaskRepository.getTaskShotDate(videoRespVO.getTaskId());
map.put("shoottime", DateUtil.format(taskShotDate, "yyyy-MM-dd HH:mm")); map.put("shoottime", DateUtil.format(taskShotDate, "yyyy-MM-dd HH:mm"));
map.put("openid", openId); map.put("openid", openId);
map.put("scenicname", contentPageVO.getScenicName()); map.put("scenicname", contentPageVO.getScenicName());
map.put("title", contentPageVO.getName()); map.put("title", contentPageVO.getName());
map.put("ossurldm", videoRespVO.getVideoUrl()); map.put("ossurldm", videoRespVO.getVideoUrl());
return map; return map;
}).collect(Collectors.toList()); }).filter(java.util.Objects::nonNull).collect(Collectors.toList());
GoodsReqQuery goodsReqQuery = new GoodsReqQuery(); GoodsReqQuery goodsReqQuery = new GoodsReqQuery();
goodsReqQuery.setFaceId(faceVO.getId()); goodsReqQuery.setFaceId(faceVO.getId());
goodsReqQuery.setSourceType(1); goodsReqQuery.setSourceType(1);

View File

@@ -0,0 +1,81 @@
package com.ycwl.basic.controller.mobile;
import com.ycwl.basic.model.jwt.JwtInfo;
import com.ycwl.basic.model.mobile.face.FaceRecognizeResp;
import com.ycwl.basic.model.mobile.goods.GoodsDetailVO;
import com.ycwl.basic.service.mobile.AppAiCamService;
import com.ycwl.basic.utils.ApiResponse;
import com.ycwl.basic.utils.JwtTokenUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* AI相机相关接口
*/
@Slf4j
@RestController
@RequestMapping("/api/mobile/ai_cam/v1")
@RequiredArgsConstructor
public class AppAiCamController {
private final AppAiCamService appAiCamService;
/**
* 根据faceId获取AI相机识别到的商品列表
* @param faceId 人脸ID
* @return 商品详情列表
*/
@GetMapping("/{faceId}/content")
public ApiResponse<List<GoodsDetailVO>> getAiCamGoods(@PathVariable Long faceId) {
try {
List<GoodsDetailVO> goods = appAiCamService.getAiCamGoodsByFaceId(faceId);
return ApiResponse.success(goods);
} catch (Exception e) {
log.error("获取AI相机商品失败: faceId={}", faceId, e);
return ApiResponse.fail("获取商品列表失败");
}
}
/**
* 批量添加会员与source的关联关系
* @param faceId 人脸ID
* @param sourceIds source ID列表
* @return 添加结果
*/
@PostMapping("/{faceId}/relations")
public ApiResponse<String> addMemberSourceRelations(
@PathVariable Long faceId,
@RequestBody List<Long> sourceIds
) {
try {
int count = appAiCamService.addMemberSourceRelations(faceId, sourceIds);
return ApiResponse.success("成功添加" + count + "条关联记录");
} catch (IllegalArgumentException e) {
log.warn("添加关联失败: faceId={}, error={}", faceId, e.getMessage());
return ApiResponse.fail(e.getMessage());
} catch (Exception e) {
log.error("添加关联失败: faceId={}", faceId, e);
return ApiResponse.fail("添加关联失败");
}
}
/**
* 使用人脸样本创建或获取Face记录
* @param faceSampleId 人脸样本ID
* @return 人脸识别响应
*/
@GetMapping("/useSample/{faceSampleId}")
public ApiResponse<FaceRecognizeResp> useSample(@PathVariable Long faceSampleId) {
try {
JwtInfo worker = JwtTokenUtil.getWorker();
FaceRecognizeResp resp = appAiCamService.useSample(worker.getUserId(), faceSampleId);
return ApiResponse.success(resp);
} catch (Exception e) {
log.error("使用人脸样本失败: faceSampleId={}", faceSampleId, e);
return ApiResponse.fail("使用人脸样本失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,98 @@
package com.ycwl.basic.controller.mobile;
import com.ycwl.basic.model.jwt.JwtInfo;
import com.ycwl.basic.model.mobile.chat.*;
import com.ycwl.basic.service.mobile.FaceChatService;
import com.ycwl.basic.utils.ApiResponse;
import com.ycwl.basic.utils.JwtTokenUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
import java.util.concurrent.CompletableFuture;
/**
* 小程序人脸智能聊天接口。
*/
@Slf4j
@RestController
@RequestMapping("/api/mobile/chat/v1")
@RequiredArgsConstructor
public class AppChatController {
private final FaceChatService faceChatService;
/**
* 获取或创建会话(同一人脸只保留一条)。
*/
@PostMapping("/faces/{faceId}/conversation")
public ApiResponse<ChatConversationVO> createConversation(@PathVariable Long faceId) {
JwtInfo worker = JwtTokenUtil.getWorker();
ChatConversationVO vo = faceChatService.getOrCreateConversation(faceId, worker.getUserId());
return ApiResponse.success(vo);
}
/**
* 同步发送消息,适用于短回复或前端自行轮询。
*/
@PostMapping("/conversations/{conversationId}/messages")
public ApiResponse<ChatSendMessageResp> sendMessage(@PathVariable Long conversationId,
@RequestBody ChatSendMessageReq req) {
JwtInfo worker = JwtTokenUtil.getWorker();
ChatSendMessageResp resp = faceChatService.sendMessage(conversationId, worker.getUserId(),
req.getContent(), req.getTraceId());
return ApiResponse.success(resp);
}
/**
* 流式返回,使用 HTTP chunked。小程序侧用 wx.request 的 onChunkReceived 消费。
*/
@PostMapping(value = "/conversations/{conversationId}/messages/stream", produces = "text/plain;charset=UTF-8")
public ResponseBodyEmitter streamMessage(@PathVariable Long conversationId,
@RequestBody ChatSendMessageReq req) {
JwtInfo worker = JwtTokenUtil.getWorker();
ResponseBodyEmitter emitter = new ResponseBodyEmitter(30_000L);
CompletableFuture.runAsync(() -> {
try {
faceChatService.sendMessageStream(
conversationId,
worker.getUserId(),
req.getContent(),
req.getTraceId(),
chunk -> {
try {
emitter.send(chunk, new MediaType("text", "plain", java.nio.charset.StandardCharsets.UTF_8));
} catch (Exception e) {
emitter.completeWithError(e);
}
});
emitter.complete();
} catch (Exception e) {
log.error("streamMessage error", e);
emitter.completeWithError(e);
}
});
return emitter;
}
/**
* 查询历史消息,cursor 为最后一条 seq,limit 为条数。
*/
@GetMapping("/conversations/{conversationId}/messages")
public ApiResponse<ChatMessagePageResp> listMessages(@PathVariable Long conversationId,
@RequestParam(value = "cursor", required = false) Integer cursor,
@RequestParam(value = "limit", required = false) Integer limit) {
JwtInfo worker = JwtTokenUtil.getWorker();
ChatMessagePageResp resp = faceChatService.listMessages(conversationId, cursor, limit, worker.getUserId());
return ApiResponse.success(resp);
}
@PostMapping("/conversations/{conversationId}/close")
public ApiResponse<String> closeConversation(@PathVariable Long conversationId) {
JwtInfo worker = JwtTokenUtil.getWorker();
faceChatService.closeConversation(conversationId, worker.getUserId());
return ApiResponse.success("OK");
}
}

View File

@@ -0,0 +1,113 @@
package com.ycwl.basic.controller.mobile;
import com.ycwl.basic.model.mobile.claim.ClaimReq;
import com.ycwl.basic.model.mobile.claim.ClaimResp;
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
import com.ycwl.basic.pricing.dto.CouponClaimRequest;
import com.ycwl.basic.pricing.dto.CouponClaimResult;
import com.ycwl.basic.pricing.dto.req.VoucherPrintReq;
import com.ycwl.basic.pricing.dto.resp.VoucherPrintResp;
import com.ycwl.basic.pricing.enums.CouponType;
import com.ycwl.basic.pricing.service.ICouponService;
import com.ycwl.basic.pricing.service.VoucherPrintService;
import com.ycwl.basic.repository.FaceRepository;
import com.ycwl.basic.repository.ScenicRepository;
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
import com.ycwl.basic.utils.ApiResponse;
import lombok.AllArgsConstructor;
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.RestController;
import java.math.BigDecimal;
import java.math.RoundingMode;
@RestController
@RequestMapping("/api/mobile/claim/v1")
@AllArgsConstructor
public class AppClaimController {
private final FaceRepository faceRepository;
private final ScenicRepository scenicRepository;
private final VoucherPrintService voucherPrintService;
private final ICouponService couponService;
@PostMapping("tryClaim")
public ApiResponse<ClaimResp> tryClaim(@RequestBody ClaimReq req) {
FaceEntity face = faceRepository.getFace(req.getFaceId());
if (face == null) {
return ApiResponse.fail("请选择人脸");
}
ClaimResp claimResp = new ClaimResp();
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(face.getScenicId());
if (Boolean.TRUE.equals(scenicConfig.getBoolean("voucher_enable"))) {
// 可以领券
VoucherPrintResp voucherPrintResp = voucherPrintService.queryPrintedVoucher(face.getId());
if (voucherPrintResp == null) {
// 打印
if (req.getMorphId() != null) {
voucherPrintResp = voucherPrintService.printVoucherTicket(new VoucherPrintReq(face.getId(), req.getMorphId(), face.getScenicId()));
}
}
if (voucherPrintResp != null) {
claimResp.setHasCoupon(false);
claimResp.setHasPrint(true);
claimResp.setPrintCode(voucherPrintResp.getCode());
claimResp.setPrintType(voucherPrintResp.getType());
return ApiResponse.success(claimResp);
}
}
if (Boolean.TRUE.equals(scenicConfig.getBoolean("booking_enable"))) {
VoucherPrintResp voucherPrintResp = voucherPrintService.queryPrintedVoucher(face.getId());
if (voucherPrintResp == null) {
// 打印
if (req.getMorphId() != null) {
voucherPrintResp = voucherPrintService.printBookingTicket(new VoucherPrintReq(face.getId(), req.getMorphId(), face.getScenicId()));
}
}
if (voucherPrintResp != null) {
claimResp.setHasCoupon(false);
claimResp.setHasPrint(true);
claimResp.setPrintCode(voucherPrintResp.getCode());
claimResp.setPrintType(voucherPrintResp.getType());
return ApiResponse.success(claimResp);
}
}
if (req.getType() != null) {
// 第几次进入
Integer couponId = scenicConfig.getInteger("coupon_id_for_type_" + req.getType());
if (couponId != null) {
// 可以领券
CouponClaimRequest request = new CouponClaimRequest(face.getMemberId(), Long.valueOf(couponId));
CouponClaimResult claimResult = couponService.claimCoupon(request);
if (claimResult.isSuccess()) {
// 领到了
claimResp.setHasCoupon(true);
switch (claimResult.getCoupon().getCouponType()) {
case CouponType.PERCENTAGE:
claimResp.setCouponType("折扣优惠券");
claimResp.setCouponDesc("" + (BigDecimal.valueOf(1).setScale(2, RoundingMode.HALF_UP).subtract(claimResult.getCoupon().getDiscountValue())).multiply(BigDecimal.valueOf(10)) + "");
break;
case CouponType.FIXED_AMOUNT:
if (claimResult.getCoupon().getMinAmount().compareTo(BigDecimal.ZERO) > 0) {
claimResp.setCouponType("满减优惠券");
claimResp.setCouponDesc("" + claimResult.getCoupon().getMinAmount() + "" + claimResult.getCoupon().getDiscountValue());
} else {
claimResp.setCouponType("直减优惠券");
claimResp.setCouponDesc("直减" + claimResult.getCoupon().getDiscountValue());
}
break;
default:
claimResp.setCouponType("普通优惠券");
break;
}
claimResp.setCouponDesc(scenicConfig.getString("coupon_desc_for_type_" + req.getType(), "专属折扣券"));
claimResp.setCouponCountdown(scenicConfig.getString("coupon_countdown_for_type_" + req.getType(), "送你优惠,保存美好!"));
return ApiResponse.success(claimResp);
}
}
}
return ApiResponse.fail("异常");
}
}

View File

@@ -1,47 +0,0 @@
package com.ycwl.basic.controller.mobile;
import com.ycwl.basic.constant.BaseContextHandler;
import com.ycwl.basic.model.mobile.coupon.req.ClaimCouponReq;
import com.ycwl.basic.model.pc.coupon.entity.CouponEntity;
import com.ycwl.basic.model.pc.couponRecord.entity.CouponRecordEntity;
import com.ycwl.basic.service.mobile.AppCouponRecordService;
import com.ycwl.basic.utils.ApiResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/mobile/coupon/v1")
public class AppCouponController {
@Autowired
private AppCouponRecordService appCouponRecordService;
/**
* 根据memberId、faceId和type查找优惠券记录
*/
@GetMapping("/record")
public ApiResponse<CouponRecordEntity> getCouponRecords(
@RequestParam Long faceId,
@RequestParam Integer type) {
CouponRecordEntity record = appCouponRecordService.queryByMemberIdAndFaceIdAndType(Long.valueOf(BaseContextHandler.getUserId()), faceId, type);
return ApiResponse.success(record);
}
/**
* 领取优惠券
*/
@PostMapping("/claim")
public ApiResponse<CouponEntity> claimCoupon(@RequestBody ClaimCouponReq request) {
request.setMemberId(Long.valueOf(BaseContextHandler.getUserId()));
try {
CouponEntity coupon = appCouponRecordService.claimCoupon(
request.getMemberId(),
request.getFaceId(),
request.getType()
);
return ApiResponse.success(coupon);
} catch (RuntimeException e) {
return ApiResponse.fail(e.getMessage());
}
}
}

View File

@@ -1,9 +1,16 @@
package com.ycwl.basic.controller.mobile; package com.ycwl.basic.controller.mobile;
import com.ycwl.basic.exception.BaseException;
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.mobile.face.FaceStatusResp;
import com.ycwl.basic.model.mobile.scenic.content.ContentPageVO; import com.ycwl.basic.model.mobile.scenic.content.ContentPageVO;
import com.ycwl.basic.model.mobile.face.FaceRecognitionUpdateReq;
import com.ycwl.basic.model.mobile.face.FaceRecognitionDetailVO;
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
import com.ycwl.basic.model.pc.face.resp.FaceRespVO; import com.ycwl.basic.model.pc.face.resp.FaceRespVO;
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
import com.ycwl.basic.repository.FaceRepository;
import com.ycwl.basic.service.pc.FaceService; import com.ycwl.basic.service.pc.FaceService;
import com.ycwl.basic.utils.ApiResponse; import com.ycwl.basic.utils.ApiResponse;
import com.ycwl.basic.utils.JwtTokenUtil; import com.ycwl.basic.utils.JwtTokenUtil;
@@ -20,11 +27,12 @@ import java.util.List;
@RestController @RestController
@RequestMapping("/api/mobile/face/v1") @RequestMapping("/api/mobile/face/v1")
// 用户人脸相关接口 // 用户人脸相关接口
public class public class AppFaceController {
AppFaceController {
@Autowired @Autowired
private FaceService faceService; private FaceService faceService;
@Autowired
private FaceRepository faceRepository;
/** /**
* 1、上传人脸照片 * 1、上传人脸照片
@@ -37,11 +45,13 @@ AppFaceController {
*/ */
// 人脸照片上传 // 人脸照片上传
@PostMapping("/faceUPload") @PostMapping("/faceUPload")
public ApiResponse<FaceRecognizeResp> faceUpload(@RequestParam("file")MultipartFile file, @RequestParam("scenicId") Long scenicId) { public ApiResponse<FaceRecognizeResp> faceUpload(@RequestParam("file")MultipartFile file,
@RequestParam(value = "scene", defaultValue = "", required = false) String scene,
@RequestParam("scenicId") Long scenicId) {
//获取用户id //获取用户id
JwtInfo worker = JwtTokenUtil.getWorker(); JwtInfo worker = JwtTokenUtil.getWorker();
Long userId = worker.getUserId(); Long userId = worker.getUserId();
FaceRecognizeResp resp = faceService.faceUpload(file, scenicId, userId); FaceRecognizeResp resp = faceService.faceUpload(file, scenicId, userId, scene);
return ApiResponse.success(resp); return ApiResponse.success(resp);
} }
@@ -49,17 +59,23 @@ AppFaceController {
public ApiResponse<List<FaceRespVO>> list(@PathVariable("scenicId") String scenicId) { public ApiResponse<List<FaceRespVO>> list(@PathVariable("scenicId") String scenicId) {
JwtInfo worker = JwtTokenUtil.getWorker(); JwtInfo worker = JwtTokenUtil.getWorker();
Long userId = worker.getUserId(); Long userId = worker.getUserId();
List<FaceRespVO> list = faceService.listByUser(userId, scenicId); List<FaceRespVO> list = faceService.listByUser(userId, Long.parseLong(scenicId));
return ApiResponse.success(list); return ApiResponse.success(list);
} }
@GetMapping("/{faceId}") @GetMapping("/{faceId}")
public ApiResponse<FaceRespVO> getById(@PathVariable("faceId") Long faceId) { public ApiResponse<FaceEntity> getById(@PathVariable("faceId") Long faceId) {
return faceService.getById(faceId); FaceEntity face = faceRepository.getFace(faceId);
return ApiResponse.success(face);
} }
@DeleteMapping("/{faceId}") @DeleteMapping("/{faceId}")
public ApiResponse<String> deleteFace(@PathVariable("faceId") Long faceId) { public ApiResponse<String> deleteFace(@PathVariable("faceId") Long faceId) {
FaceEntity face = faceRepository.getFace(faceId);
if (face == null) {
throw new BaseException("人脸数据不存在");
}
return faceService.deleteFace(faceId); return faceService.deleteFace(faceId);
} }
@@ -80,9 +96,44 @@ AppFaceController {
// 绑定人脸 // 绑定人脸
@PostMapping("/{faceId}/bind") @PostMapping("/{faceId}/bind")
public ApiResponse<String> bind(@PathVariable Long faceId) { public ApiResponse<String> bind(@PathVariable Long faceId) {
JwtInfo worker = JwtTokenUtil.getWorker(); // dummy item
Long userId = worker.getUserId(); faceService.matchFaceId(faceId, true);
faceService.bindFace(faceId, userId);
return ApiResponse.success("OK"); return ApiResponse.success("OK");
} }
@GetMapping("/{faceId}/status")
public ApiResponse<FaceStatusResp> status(@PathVariable Long faceId) {
return ApiResponse.success(faceService.getFaceStatus(faceId));
}
@GetMapping("/{faceId}/extraCheck")
public ApiResponse<Boolean> hasExtraCheck(@PathVariable Long faceId) {
return ApiResponse.success(faceService.checkHasExtraCheck(faceId));
}
@GetMapping("/{faceId}/queryOtherFace")
public ApiResponse<List<FaceSampleEntity>> queryOtherFace(@PathVariable Long faceId) {
return ApiResponse.success(faceService.getLowMatchedFaceSamples(faceId));
}
@PostMapping("/{faceId}/queryOtherFace")
public ApiResponse<String> queryOtherFace(@PathVariable Long faceId, @RequestBody List<Long> faceIds) {
faceService.matchCustomFaceId(faceId, faceIds);
return ApiResponse.success("OK");
}
@PutMapping("/{faceId}/recognition")
public ApiResponse<?> updateRecognition(@PathVariable Long faceId,
@RequestBody FaceRecognitionUpdateReq req) {
req.setFaceId(faceId);
faceService.updateRecognition(req);
return ApiResponse.success("OK");
}
@GetMapping("/{faceId}/recognition/detail")
public ApiResponse<FaceRecognitionDetailVO> recognitionDetail(@PathVariable Long faceId) {
return ApiResponse.success(faceService.getRecognitionDetail(faceId));
}
} }

View File

@@ -4,7 +4,9 @@ import com.ycwl.basic.annotation.IgnoreToken;
import com.ycwl.basic.exception.CheckTokenException; import com.ycwl.basic.exception.CheckTokenException;
import com.ycwl.basic.model.jwt.JwtInfo; import com.ycwl.basic.model.jwt.JwtInfo;
import com.ycwl.basic.model.mobile.goods.*; import com.ycwl.basic.model.mobile.goods.*;
import com.ycwl.basic.model.pc.face.resp.FaceRespVO;
import com.ycwl.basic.service.mobile.GoodsService; import com.ycwl.basic.service.mobile.GoodsService;
import com.ycwl.basic.service.pc.FaceService;
import com.ycwl.basic.utils.ApiResponse; import com.ycwl.basic.utils.ApiResponse;
import com.ycwl.basic.utils.JwtTokenUtil; import com.ycwl.basic.utils.JwtTokenUtil;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -24,11 +26,17 @@ public class AppGoodsController {
@Autowired @Autowired
private GoodsService goodsService; private GoodsService goodsService;
@Autowired
private FaceService faceService;
// 商品列表 // 商品列表
@PostMapping("/goodsList") @PostMapping("/goodsList")
public ApiResponse<List<GoodsPageVO>> goodsList(@RequestBody GoodsReqQuery query) { public ApiResponse<List<GoodsPageVO>> goodsList(@RequestBody GoodsReqQuery query) {
return goodsService.goodsList(query); JwtInfo worker = JwtTokenUtil.getWorker();
Long userId = worker.getUserId();
List<FaceRespVO> faceRespVOS = faceService.listByUser(userId, query.getScenicId());
List<Long> faceIds = faceRespVOS.stream().map(FaceRespVO::getId).toList();
return goodsService.listGoodsByFaceIdList(faceIds, query.getIsBuy(), query.getScenicId());
} }
// 源素材(原片/照片)商品列表 // 源素材(原片/照片)商品列表
@@ -45,12 +53,6 @@ public class AppGoodsController {
return ApiResponse.success(count); return ApiResponse.success(count);
} }
@PostMapping("/sourceGoodsList/preview")
public ApiResponse<List<GoodsUrlVO>> sourceGoodsListPreview(@RequestBody GoodsReqQuery query) {
List<GoodsUrlVO> goodsUrlList = goodsService.sourceGoodsListPreview(query);
return ApiResponse.success(goodsUrlList);
}
@PostMapping("/sourceGoodsList/download") @PostMapping("/sourceGoodsList/download")
public ApiResponse<List<GoodsUrlVO>> sourceGoodsListDownload(@RequestBody GoodsReqQuery query) { public ApiResponse<List<GoodsUrlVO>> sourceGoodsListDownload(@RequestBody GoodsReqQuery query) {
List<GoodsUrlVO> goodsUrlList = goodsService.sourceGoodsListDownload(query); List<GoodsUrlVO> goodsUrlList = goodsService.sourceGoodsListDownload(query);
@@ -96,7 +98,18 @@ public class AppGoodsController {
// 查询用户当前景区的具体模版视频合成任务状态 1 合成中 2 合成成功 // 查询用户当前景区的具体模版视频合成任务状态 1 合成中 2 合成成功
@GetMapping("/task/face/{faceId}/template/{templateId}") @GetMapping("/task/face/{faceId}/template/{templateId}")
public ApiResponse<VideoTaskStatusVO> getTemplateTaskStatus(@PathVariable("faceId") Long faceId, @PathVariable("templateId") Long templateId) { public ApiResponse<VideoTaskStatusVO> getTemplateTaskStatus(@PathVariable("faceId") Long faceId, @PathVariable("templateId") Long templateId) {
JwtInfo worker = JwtTokenUtil.getWorker();
return ApiResponse.success(goodsService.getTaskStatusByTemplateId(faceId, templateId)); return ApiResponse.success(goodsService.getTaskStatusByTemplateId(faceId, templateId));
} }
/**
* 检查视频是否可更新
*
* @param videoId 视频ID
* @return 视频更新检查结果
*/
@GetMapping("/video/{videoId}/updateCheck")
public ApiResponse<VideoUpdateCheckVO> checkVideoUpdate(@PathVariable("videoId") Long videoId) {
VideoUpdateCheckVO result = goodsService.checkVideoUpdate(videoId);
return ApiResponse.success(result);
}
} }

View File

@@ -1,6 +1,7 @@
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.constant.BaseContextHandler;
import com.ycwl.basic.model.mobile.weChat.DTO.WeChatUserInfoDTO; import com.ycwl.basic.model.mobile.weChat.DTO.WeChatUserInfoDTO;
import com.ycwl.basic.model.mobile.weChat.DTO.WeChatUserInfoUpdateDTO; import com.ycwl.basic.model.mobile.weChat.DTO.WeChatUserInfoUpdateDTO;
import com.ycwl.basic.model.pc.member.resp.MemberRespVO; import com.ycwl.basic.model.pc.member.resp.MemberRespVO;
@@ -67,7 +68,8 @@ public class AppMemberController {
// 修改用户信息 // 修改用户信息
@PostMapping("/update") @PostMapping("/update")
public ApiResponse<?> update(@RequestBody WeChatUserInfoUpdateDTO userInfoUpdateDTO) { public ApiResponse<?> update(@RequestBody WeChatUserInfoUpdateDTO userInfoUpdateDTO) {
return memberService.update(userInfoUpdateDTO); Long userId = Long.parseLong(BaseContextHandler.getUserId());
return memberService.update(userId, userInfoUpdateDTO);
} }
// 新增或修改景区服务通知状态 // 新增或修改景区服务通知状态

View File

@@ -1,6 +1,7 @@
package com.ycwl.basic.controller.mobile; package com.ycwl.basic.controller.mobile;
import com.github.pagehelper.PageInfo; import com.github.pagehelper.PageInfo;
import com.ycwl.basic.annotation.IgnoreToken;
import com.ycwl.basic.biz.OrderBiz; import com.ycwl.basic.biz.OrderBiz;
import com.ycwl.basic.biz.PriceBiz; import com.ycwl.basic.biz.PriceBiz;
import com.ycwl.basic.constant.BaseContextHandler; import com.ycwl.basic.constant.BaseContextHandler;
@@ -51,6 +52,7 @@ public class AppOrderController {
// 用户端订单详情查询 // 用户端订单详情查询
@GetMapping("getOrderDetails/{id}") @GetMapping("getOrderDetails/{id}")
@IgnoreToken
public ApiResponse<OrderAppRespVO> getOrderDetails(@PathVariable("id") Long id) { public ApiResponse<OrderAppRespVO> getOrderDetails(@PathVariable("id") Long id) {
return orderService.appDetail(id); return orderService.appDetail(id);
} }
@@ -91,9 +93,9 @@ public class AppOrderController {
} }
@GetMapping("/scenic/{scenicId}/query") @GetMapping("/scenic/{scenicId}/query")
public ApiResponse<IsBuyRespVO> isBuy(@PathVariable("scenicId") Long scenicId, @RequestParam("type") Integer type, @RequestParam("goodsId") Long goodsId) { public ApiResponse<IsBuyRespVO> isBuy(@PathVariable("scenicId") Long scenicId, @RequestParam("type") Integer type, @RequestParam("goodsId") Long goodsId, @RequestParam(value = "faceId", required = false) Long faceId) {
Long userId = Long.parseLong(BaseContextHandler.getUserId()); Long userId = Long.parseLong(BaseContextHandler.getUserId());
return ApiResponse.success(orderBiz.isBuy(userId, scenicId, type, goodsId)); return ApiResponse.success(orderBiz.isBuy(scenicId, userId, faceId, type, goodsId));
} }
@GetMapping("/scenic/{scenicId}/queryBatchPrice") @GetMapping("/scenic/{scenicId}/queryBatchPrice")
@@ -106,7 +108,7 @@ public class AppOrderController {
} }
faceId = lastFaceByUserId.getId(); faceId = lastFaceByUserId.getId();
} }
IsBuyBatchRespVO buy = priceBiz.isBuy(userId, faceId, scenicId, type, goodsIds); IsBuyBatchRespVO buy = priceBiz.isOnePriceBuy(userId, faceId, scenicId, type, goodsIds);
if (buy == null) { if (buy == null) {
return ApiResponse.fail("该套餐暂未开放购买"); return ApiResponse.fail("该套餐暂未开放购买");
} }

View File

@@ -0,0 +1,435 @@
package com.ycwl.basic.controller.mobile;
import com.github.pagehelper.PageInfo;
import com.ycwl.basic.constant.BaseContextHandler;
import com.ycwl.basic.mapper.SourceMapper;
import com.ycwl.basic.mapper.VideoMapper;
import com.ycwl.basic.model.pc.source.req.SourceReqQuery;
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.VideoEntity;
import com.ycwl.basic.pricing.enums.ProductType;
import com.ycwl.basic.repository.TemplateRepository;
import com.ycwl.basic.repository.VideoRepository;
import com.ycwl.basic.repository.VideoTaskRepository;
import com.ycwl.basic.service.pc.OrderService;
import com.ycwl.basic.utils.ApiResponse;
import com.ycwl.basic.pricing.dto.*;
import com.ycwl.basic.pricing.service.IPriceCalculationService;
import com.ycwl.basic.service.pc.FaceService;
import com.ycwl.basic.service.PriceCacheService;
import com.ycwl.basic.model.pc.face.resp.FaceRespVO;
import com.ycwl.basic.dto.MobileOrderRequest;
import com.ycwl.basic.order.service.IOrderService;
import com.ycwl.basic.order.dto.OrderV2DetailResponse;
import com.ycwl.basic.order.dto.OrderV2ListResponse;
import com.ycwl.basic.order.dto.OrderV2PageRequest;
import com.ycwl.basic.order.dto.PaymentParamsRequest;
import com.ycwl.basic.order.dto.PaymentParamsResponse;
import com.ycwl.basic.order.dto.PaymentCallbackResponse;
import com.ycwl.basic.order.exception.DuplicatePurchaseException;
import com.ycwl.basic.order.factory.DuplicatePurchaseCheckerFactory;
import com.ycwl.basic.order.strategy.DuplicateCheckContext;
import com.ycwl.basic.order.strategy.IDuplicatePurchaseChecker;
import com.ycwl.basic.product.capability.DuplicateCheckStrategy;
import com.ycwl.basic.product.service.IProductTypeCapabilityService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;
import jakarta.servlet.http.HttpServletRequest;
import java.util.List;
/**
* 移动端订单控制器V2
* 包含价格查询和订单管理功能
*/
@Slf4j
@RestController
@RequestMapping("/api/mobile/order/v2")
@RequiredArgsConstructor
public class AppOrderV2Controller {
private final IPriceCalculationService priceCalculationService;
private final FaceService faceService;
private final PriceCacheService priceCacheService;
private final IOrderService orderService;
private final OrderService oldOrderService;
private final SourceMapper sourceMapper;
private final VideoMapper videoMapper;
private final VideoTaskRepository videoTaskRepository;
private final TemplateRepository templateRepository;
private final VideoRepository videoRepository;
private final RedisTemplate<String, Object> redisTemplate;
private final IProductTypeCapabilityService productTypeCapabilityService;
private final DuplicatePurchaseCheckerFactory duplicatePurchaseCheckerFactory;
/**
* 移动端价格计算
* 集成Redis缓存机制,提升查询性能
*/
@PostMapping("/calculate")
public ApiResponse<PriceCalculationResult> calculatePrice(@RequestBody MobilePriceCalculationRequest request) {
// 获取当前登录用户ID
String currentUserIdStr = BaseContextHandler.getUserId();
if (currentUserIdStr == null) {
log.warn("移动端价格计算:用户未登录");
return ApiResponse.fail("用户未登录");
}
Long currentUserId = Long.valueOf(currentUserIdStr);
log.info("移动端价格计算请求: userId={}, faceId={}, products={}",
currentUserId, request.getFaceId(), request.getProducts().size());
// 验证faceId参数
if (request.getFaceId() == null) {
log.warn("移动端价格计算:faceId参数缺失");
// return ApiResponse.fail("faceId参数不能为空");
// 兼容:兼容旧版本
ProductItem productItem = request.getProducts().getFirst();
switch (productItem.getProductType()) {
case VLOG_VIDEO -> {
VideoEntity video = videoRepository.getVideo(Long.valueOf(productItem.getProductId()));
request.setFaceId(video.getFaceId());
}
case RECORDING_SET, PHOTO_SET, AI_CAM_PHOTO_SET -> request.setFaceId(Long.valueOf(productItem.getProductId()));
}
}
// 查询人脸信息进行权限验证
ApiResponse<FaceRespVO> faceResponse = faceService.getById(request.getFaceId());
if (!faceResponse.isSuccess() || faceResponse.getData() == null) {
log.warn("移动端价格计算:人脸信息不存在, faceId={}", request.getFaceId());
return ApiResponse.fail("人脸信息不存在");
}
FaceRespVO face = faceResponse.getData();
Long scenicId = face.getScenicId();
request.getProducts().forEach(product -> {
// 获取商品的重复检查策略
DuplicateCheckStrategy strategy = productTypeCapabilityService
.getDuplicateCheckStrategy(product.getProductType().name());
boolean hasPurchasedFlag;
switch (product.getProductType()) {
case VLOG_VIDEO:
List<MemberVideoEntity> videoEntities = videoMapper.listRelationByFaceAndTemplate(face.getId(), Long.valueOf(product.getProductId()));
if (videoEntities != null && !videoEntities.isEmpty()) {
product.setQuantity(videoTaskRepository.getTaskLensNum(videoEntities.getFirst().getTaskId()));
} else {
product.setQuantity(1);
}
break;
case RECORDING_SET:
case PHOTO_SET:
SourceReqQuery sourceReqQuery = new SourceReqQuery();
sourceReqQuery.setMemberId(currentUserId);
sourceReqQuery.setType(product.getProductType() == ProductType.RECORDING_SET ? 1 : 2);
sourceReqQuery.setFaceId(face.getId());
Integer count = sourceMapper.countUser(sourceReqQuery);
product.setQuantity(count);
break;
case AI_CAM_PHOTO_SET:
SourceReqQuery aiPhotoSetReqQuery = new SourceReqQuery();
aiPhotoSetReqQuery.setMemberId(currentUserId);
aiPhotoSetReqQuery.setType(13);
aiPhotoSetReqQuery.setFaceId(face.getId());
Integer _count = sourceMapper.countUser(aiPhotoSetReqQuery);
product.setQuantity(_count);
break;
default:
log.warn("未知的商品类型,跳过重复购买检查: productType={}", product.getProductType());
break;
}
// 使用 DuplicatePurchaseChecker 检查是否已购买
hasPurchasedFlag = checkIfPurchased(strategy, currentUserId, String.valueOf(scenicId),
product.getProductType().name(), product.getProductId(), face.getId());
// 设置是否已购买标识
product.setHasPurchased(hasPurchasedFlag);
});
// 转换为标准价格计算请求
PriceCalculationRequest standardRequest = request.toStandardRequest(currentUserId, scenicId);
// 执行价格计算
PriceCalculationResult result = priceCalculationService.calculatePrice(standardRequest);
// 设置是否已购买标识(基于请求中的商品 hasPurchased 判断)
// 只要有一个商品 hasPurchased = true,则整体 isPurchased = true
boolean isPurchased = request.getProducts().stream()
.anyMatch(product -> Boolean.TRUE.equals(product.getHasPurchased()));
result.setIsPurchased(isPurchased);
// 将计算结果缓存到Redis
String cacheKey = priceCacheService.cachePriceResult(currentUserId, scenicId, request.getProducts(), result);
log.info("移动端价格计算完成: userId={}, scenicId={}, originalAmount={}, finalAmount={}, cacheKey={}",
currentUserId, scenicId, result.getOriginalAmount(), result.getFinalAmount(), cacheKey);
return ApiResponse.success(result);
}
/**
* 移动端下单接口
* 验证价格缓存有效性,确保5分钟内使用缓存价格下单
*/
@PostMapping("/add")
public ApiResponse<String> addOrder(@RequestBody MobileOrderRequest request) {
// 获取当前登录用户ID
String currentUserIdStr = BaseContextHandler.getUserId();
if (currentUserIdStr == null) {
log.warn("移动端下单:用户未登录");
return ApiResponse.fail("用户未登录");
}
Long currentUserId = Long.valueOf(currentUserIdStr);
log.info("移动端下单请求: userId={}, faceId={}, products={}, expectedFinalAmount={}",
currentUserId, request.getFaceId(), request.getProducts().size(), request.getExpectedFinalAmount());
// 验证必填参数
if (request.getFaceId() == null) {
log.warn("移动端下单:faceId参数缺失");
return ApiResponse.fail("faceId参数不能为空");
}
if (request.getProducts() == null || request.getProducts().isEmpty()) {
log.warn("移动端下单:商品列表为空");
return ApiResponse.fail("商品列表不能为空");
}
if (request.getExpectedFinalAmount() == null) {
log.warn("移动端下单:预期价格参数缺失");
return ApiResponse.fail("预期价格不能为空");
}
// 查询人脸信息进行权限验证
ApiResponse<FaceRespVO> faceResponse = faceService.getById(request.getFaceId());
if (!faceResponse.isSuccess() || faceResponse.getData() == null) {
log.warn("移动端下单:人脸信息不存在, faceId={}", request.getFaceId());
return ApiResponse.fail("人脸信息不存在");
}
FaceRespVO face = faceResponse.getData();
Long scenicId = face.getScenicId();
// 验证并消费价格缓存(一次性使用)
PriceCalculationResult cachedResult = priceCacheService.validateAndConsumePriceCache(
currentUserId, scenicId, request.getProducts());
if (cachedResult == null) {
log.warn("移动端下单:价格缓存已过期或不存在, userId={}, scenicId={}", currentUserId, scenicId);
return ApiResponse.fail("请重新下单!");
}
// 验证价格是否匹配
if (cachedResult.getFinalAmount().compareTo(request.getExpectedFinalAmount()) != 0) {
log.warn("移动端下单:价格不匹配, cached={}, expected={}, userId={}, scenicId={}",
cachedResult.getFinalAmount(), request.getExpectedFinalAmount(), currentUserId, scenicId);
return ApiResponse.fail("价格信息变化,请退出后重新查询价格!");
}
// 验证原价是否匹配(可选)
if (request.getExpectedOriginalAmount() != null &&
cachedResult.getOriginalAmount().compareTo(request.getExpectedOriginalAmount()) != 0) {
log.warn("移动端下单:原价不匹配, cached={}, expected={}, userId={}, scenicId={}",
cachedResult.getOriginalAmount(), request.getExpectedOriginalAmount(), currentUserId, scenicId);
return ApiResponse.fail("原价信息不匹配,请重新查询价格后再下单");
}
log.info("价格缓存验证通过: userId={}, scenicId={}, finalAmount={}",
currentUserId, scenicId, cachedResult.getFinalAmount());
// 使用旧版创建订单逻辑
try {
Long orderId = oldOrderService.createOrderCompact(currentUserId, request, cachedResult);
return ApiResponse.success(String.valueOf(orderId));
} catch (Exception e) {
log.warn("移动端下单:订单创建失败, userId={}, scenicId={}, error={}", currentUserId, scenicId, e.getMessage(), e);
return ApiResponse.fail("订单创建失败,请稍后重试");
}
// 创建订单
// try {
// Long orderId = orderService.createOrder(request, currentUserId, scenicId, cachedResult);
//
// log.info("移动端订单创建成功: orderId={}, userId={}, scenicId={}, finalAmount={}",
// orderId, currentUserId, scenicId, cachedResult.getFinalAmount());
//
// return ApiResponse.success(orderId.toString());
//
// } catch (Exception e) {
// log.error("订单创建失败: userId={}, scenicId={}, error={}", currentUserId, scenicId, e.getMessage(), e);
// return ApiResponse.fail("订单创建失败,请稍后重试");
// }
}
// ====== 新增移动端订单查询功能 ======
/**
* 用户分页查询自己的订单列表
*/
@PostMapping("/page")
public ApiResponse<PageInfo<OrderV2ListResponse>> pageUserOrders(@RequestBody OrderV2PageRequest request) {
String currentUserIdStr = BaseContextHandler.getUserId();
if (currentUserIdStr == null) {
log.warn("用户未登录");
return ApiResponse.fail("用户未登录");
}
Long currentUserId = Long.valueOf(currentUserIdStr);
request.setMemberId(currentUserId); // 设置当前用户ID,确保只查询自己的订单
log.info("用户查询订单列表: userId={}, request={}", currentUserId, request);
try {
PageInfo<OrderV2ListResponse> pageInfo = orderService.pageOrdersByUser(request);
return ApiResponse.success(pageInfo);
} catch (Exception e) {
log.error("查询用户订单列表失败: userId={}", currentUserId, e);
return ApiResponse.fail("查询失败:" + e.getMessage());
}
}
/**
* 查询订单详情
*/
@GetMapping("/detail/{orderId}")
public ApiResponse<OrderV2DetailResponse> getUserOrderDetail(@PathVariable("orderId") Long orderId) {
log.info("查询订单详情: orderId={}", orderId);
try {
OrderV2DetailResponse detail = orderService.getOrderDetail(orderId);
if (detail == null) {
return ApiResponse.fail("订单不存在");
}
return ApiResponse.success(detail);
} catch (Exception e) {
log.error("查询订单详情失败: orderId={}", orderId, e);
return ApiResponse.fail("查询失败:" + e.getMessage());
}
}
// ====== 支付相关接口 ======
/**
* 获取订单支付参数
* 用于小程序调起支付
*/
@PostMapping("/{orderId}/payment-params")
public ApiResponse<PaymentParamsResponse> getPaymentParams(
@PathVariable("orderId") Long orderId,
@RequestBody PaymentParamsRequest request) {
String currentUserIdStr = BaseContextHandler.getUserId();
if (currentUserIdStr == null) {
log.warn("用户未登录");
return ApiResponse.fail("用户未登录");
}
Long currentUserId = Long.valueOf(currentUserIdStr);
log.info("获取支付参数: userId={}, orderId={}", currentUserId, orderId);
return oldOrderService.getPaymentParams(orderId, currentUserId, request);
//
// try {
// PaymentParamsResponse response = orderService.getPaymentParams(orderId, currentUserId, request);
// return ApiResponse.success(response);
// } catch (Exception e) {
// log.error("获取支付参数失败: userId={}, orderId={}", currentUserId, orderId, e);
// return ApiResponse.fail(e.getMessage());
// }
}
/**
* 支付回调处理接口
* 供第三方支付平台回调使用
*/
@PostMapping("/payment/callback/{scenicId}")
public String handlePaymentCallback(
@PathVariable("scenicId") Long scenicId,
HttpServletRequest request) {
log.info("接收支付回调: scenicId={}", scenicId);
try {
PaymentCallbackResponse response = orderService.handlePaymentCallback(scenicId, request);
if (response.isSuccess()) {
log.info("支付回调处理成功: scenicId={}, orderId={}, statusChangeType={}",
scenicId, response.getOrderId(), response.getStatusChangeType());
return "SUCCESS"; // 返回给第三方支付平台的成功标识
} else {
log.error("支付回调处理失败: scenicId={}, message={}", scenicId, response.getMessage());
return "FAIL"; // 返回给第三方支付平台的失败标识
}
} catch (Exception e) {
log.error("支付回调异常: scenicId={}", scenicId, e);
return "FAIL";
}
}
@GetMapping("/downloadable/{orderId}")
public ApiResponse<Boolean> getDownloadableOrder(@PathVariable("orderId") Long orderId) {
return ApiResponse.success(!redisTemplate.hasKey("order_content_not_downloadable_" + orderId));
}
/**
* 检查商品是否已购买
* 使用 DuplicatePurchaseChecker 通过异常捕获判断
*
* @param strategy 重复检查策略
* @param userId 用户ID
* @param scenicId 景区ID
* @param productType 商品类型
* @param productId 商品ID
* @param faceId 人脸ID
* @return true-已购买, false-未购买
*/
private boolean checkIfPurchased(DuplicateCheckStrategy strategy, Long userId, String scenicId,
String productType, String productId, Long faceId) {
// NO_CHECK 策略表示允许重复购买,直接返回 false
if (strategy == DuplicateCheckStrategy.NO_CHECK) {
return false;
}
try {
// 获取对应的检查器
IDuplicatePurchaseChecker checker = duplicatePurchaseCheckerFactory.getChecker(strategy);
// 构建检查上下文
DuplicateCheckContext context = new DuplicateCheckContext();
context.setUserId(String.valueOf(userId));
context.setScenicId(scenicId);
context.setProductType(productType);
context.setProductId(productId);
context.addParam("faceId", faceId);
// 执行检查,如果抛出异常则表示已购买
checker.check(context);
// 没有抛出异常,表示未购买
return false;
} catch (DuplicatePurchaseException e) {
// 捕获到重复购买异常,表示已购买
log.debug("检测到已购买: userId={}, scenicId={}, productType={}, productId={}",
userId, scenicId, productType, productId);
return true;
} catch (Exception e) {
// 其他异常,记录日志并返回 false(保守处理)
log.warn("检查是否已购买时发生异常: userId={}, scenicId={}, productType={}, productId={}, error={}",
userId, scenicId, productType, productId, e.getMessage(), e);
return false;
}
}
}

View File

@@ -2,6 +2,7 @@ package com.ycwl.basic.controller.mobile;
import com.ycwl.basic.annotation.IgnoreToken; import com.ycwl.basic.annotation.IgnoreToken;
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.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;
@@ -16,6 +17,7 @@ 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.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
@@ -35,10 +37,16 @@ public class AppPrinterController {
return ApiResponse.success(printerService.listByScenicId(scenicId)); return ApiResponse.success(printerService.listByScenicId(scenicId));
} }
@GetMapping("/getListFor/{scenicId}") @GetMapping("/useSample/{sampleId}")
public ApiResponse<List<MemberPrintResp>> getListFor(@PathVariable("scenicId") Long scenicId) { public ApiResponse<FaceRecognizeResp> useSample(@PathVariable("sampleId") Long sampleId) throws IOException {
JwtInfo worker = JwtTokenUtil.getWorker(); JwtInfo worker = JwtTokenUtil.getWorker();
return ApiResponse.success(printerService.getUserPhotoList(worker.getUserId(), scenicId)); return ApiResponse.success(printerService.useSample(worker.getUserId(), sampleId));
}
@GetMapping("/getListFor/{scenicId}")
public ApiResponse<List<MemberPrintResp>> getListFor(@PathVariable("scenicId") Long scenicId, @RequestParam(required = false) String faceId) {
JwtInfo worker = JwtTokenUtil.getWorker();
return ApiResponse.success(printerService.getUserPhotoList(worker.getUserId(), scenicId, parseFaceId(faceId)));
} }
@GetMapping("/getItem/{scenicId}/{id}") @GetMapping("/getItem/{scenicId}/{id}")
@@ -52,31 +60,38 @@ public class AppPrinterController {
} }
@PostMapping("/deleteFrom/{scenicId}/{id}") @PostMapping("/deleteFrom/{scenicId}/{id}")
public ApiResponse<?> deleteFrom(@PathVariable("scenicId") Long scenicId, @PathVariable("id") Long id) throws IOException { public ApiResponse<?> deleteFrom(@PathVariable("scenicId") Long scenicId, @PathVariable("id") Long id) {
JwtInfo worker = JwtTokenUtil.getWorker(); JwtInfo worker = JwtTokenUtil.getWorker();
printerService.deleteUserPhoto(worker.getUserId(), scenicId, id); printerService.deleteUserPhoto(worker.getUserId(), scenicId, id);
return ApiResponse.success(null); return ApiResponse.success(null);
} }
@PostMapping("/uploadTo/{scenicId}") @PostMapping("/uploadTo/{scenicId}")
public ApiResponse<?> upload(@PathVariable("scenicId") Long scenicId, @RequestParam(value = "file") MultipartFile file) throws IOException { public ApiResponse<?> upload(@PathVariable("scenicId") Long scenicId,
@RequestParam(value = "file") MultipartFile file,
@RequestParam(value = "faceId", required = false) String faceId) {
String[] split = file.getOriginalFilename().split("\\."); String[] split = file.getOriginalFilename().split("\\.");
String ext = split[split.length - 1]; String ext = split[split.length - 1];
String url = StorageFactory.use().uploadFile(file, "printer", UUID.randomUUID() + "." + ext); String url = StorageFactory.use().uploadFile(file, "printer", UUID.randomUUID() + "." + ext);
printerService.addUserPhoto(JwtTokenUtil.getWorker().getUserId(), scenicId, url); Integer id = printerService.addUserPhoto(JwtTokenUtil.getWorker().getUserId(), scenicId, url, parseFaceId(faceId), null);
return ApiResponse.success(url); return ApiResponse.success(id);
} }
@PostMapping("/uploadTo/{scenicId}/cropped/{id}") @PostMapping(value = "/uploadTo/{scenicId}/cropped/{id}", consumes = "multipart/form-data")
public ApiResponse<?> uploadReplace(@PathVariable("scenicId") Long scenicId, @PathVariable("id") Long id, @RequestParam(value = "file") MultipartFile file) throws IOException { public ApiResponse<?> uploadReplace(@PathVariable("scenicId") Long scenicId,
@PathVariable("id") Long id,
@RequestPart(value = "crop", required = false) String crop,
@RequestPart(value = "file") MultipartFile file) {
String[] split = file.getOriginalFilename().split("\\."); String[] split = file.getOriginalFilename().split("\\.");
String ext = split[split.length - 1]; String ext = split[split.length - 1];
String url = StorageFactory.use().uploadFile(file, "printer", UUID.randomUUID() + "." + ext); String url = StorageFactory.use().uploadFile(file, "printer", UUID.randomUUID() + "." + ext);
printerService.setPhotoCropped(JwtTokenUtil.getWorker().getUserId(), scenicId, id, url); printerService.setPhotoCropped(JwtTokenUtil.getWorker().getUserId(), scenicId, id, url, crop);
return ApiResponse.success(url); return ApiResponse.success(url);
} }
@PostMapping("/uploadTo/{scenicId}/formSource") @PostMapping("/uploadTo/{scenicId}/formSource")
public ApiResponse<?> uploadFromSource(@PathVariable("scenicId") Long scenicId, @RequestBody FromSourceReq req) throws IOException { public ApiResponse<?> uploadFromSource(@PathVariable("scenicId") Long scenicId,
printerService.addUserPhotoFromSource(JwtTokenUtil.getWorker().getUserId(), scenicId, req); @RequestBody FromSourceReq req,
return ApiResponse.success(null); @RequestParam(value = "faceId", required = false) String faceId) {
List<Integer> list = printerService.addUserPhotoFromSource(JwtTokenUtil.getWorker().getUserId(), scenicId, req, parseFaceId(faceId));
return ApiResponse.success(list);
} }
@PostMapping("/setQuantity/{scenicId}/{id}") @PostMapping("/setQuantity/{scenicId}/{id}")
@@ -91,16 +106,35 @@ public class AppPrinterController {
return ApiResponse.success(null); return ApiResponse.success(null);
} }
@GetMapping("/price/{scenicId}") @GetMapping("/price/{scenicId}")
public ApiResponse<?> queryPrice(@PathVariable("scenicId") Long scenicId) { public ApiResponse<?> queryPrice(@PathVariable("scenicId") Long scenicId,
return ApiResponse.success(printerService.queryPrice(JwtTokenUtil.getWorker().getUserId(), scenicId)); @RequestParam(value = "faceId", required = false) String faceId) {
return ApiResponse.success(printerService.queryPrice(JwtTokenUtil.getWorker().getUserId(), scenicId, parseFaceId(faceId)));
} }
@PostMapping("/order/{scenicId}") @PostMapping("/order/{scenicId}")
public ApiResponse<Map<String, Object>> createOrder(@PathVariable("scenicId") Long scenicId) { public ApiResponse<Map<String, Object>> createOrder(@PathVariable("scenicId") Long scenicId,
return ApiResponse.success(printerService.createOrder(JwtTokenUtil.getWorker().getUserId(), scenicId, null)); @RequestParam(value = "faceId", required = false) String faceId) {
return ApiResponse.success(printerService.createOrder(JwtTokenUtil.getWorker().getUserId(), scenicId, null, parseFaceId(faceId)));
} }
@PostMapping("/order/{scenicId}/toPrinter/{printerId}") @PostMapping("/order/{scenicId}/toPrinter/{printerId}")
public ApiResponse<Map<String, Object>> createOrderToPrinter(@PathVariable("scenicId") Long scenicId, @PathVariable("printerId") Integer printerId) { public ApiResponse<Map<String, Object>> createOrderToPrinter(@PathVariable("scenicId") Long scenicId,
return ApiResponse.success(printerService.createOrder(JwtTokenUtil.getWorker().getUserId(), scenicId, printerId)); @PathVariable("printerId") Integer printerId,
@RequestParam(value = "faceId", required = false) String faceId) {
return ApiResponse.success(printerService.createOrder(JwtTokenUtil.getWorker().getUserId(), scenicId, printerId, parseFaceId(faceId)));
}
/**
* 解析 faceId 字符串为 Long 类型
* 如果字符串不是有效数字,则返回 null
*/
private Long parseFaceId(String faceId) {
if (faceId == null || faceId.trim().isEmpty()) {
return null;
}
try {
return Long.parseLong(faceId.trim());
} catch (NumberFormatException e) {
return null;
}
} }
} }

View File

@@ -0,0 +1,336 @@
package com.ycwl.basic.controller.mobile;
import cn.hutool.core.date.DateUtil;
import com.ycwl.basic.biz.OrderBiz;
import com.ycwl.basic.constant.FreeStatus;
import com.ycwl.basic.image.watermark.edge.PuzzleDefaultWatermarkTemplateBuilder;
import com.ycwl.basic.image.watermark.edge.WatermarkEdgeTaskCreator;
import com.ycwl.basic.image.watermark.edge.WatermarkRequest;
import com.ycwl.basic.model.mobile.order.IsBuyRespVO;
import com.ycwl.basic.model.mobile.scenic.content.ContentPageVO;
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
import com.ycwl.basic.model.pc.puzzle.entity.MemberPuzzleEntity;
import com.ycwl.basic.model.pc.scenic.entity.ScenicEntity;
import com.ycwl.basic.pricing.dto.PriceCalculationRequest;
import com.ycwl.basic.pricing.dto.PriceCalculationResult;
import com.ycwl.basic.pricing.dto.ProductItem;
import com.ycwl.basic.pricing.enums.ProductType;
import com.ycwl.basic.pricing.service.IPriceCalculationService;
import com.ycwl.basic.puzzle.edge.task.PuzzleEdgeRenderTaskService;
import com.ycwl.basic.puzzle.entity.PuzzleGenerationRecordEntity;
import com.ycwl.basic.puzzle.mapper.MemberPuzzleMapper;
import com.ycwl.basic.puzzle.repository.PuzzleRepository;
import com.ycwl.basic.repository.FaceRepository;
import com.ycwl.basic.repository.ScenicRepository;
import com.ycwl.basic.service.pc.FaceService;
import com.ycwl.basic.service.printer.PrinterService;
import com.ycwl.basic.utils.ApiResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@RestController
@RequestMapping("/api/mobile/puzzle/v1")
@RequiredArgsConstructor
public class AppPuzzleController {
private final PuzzleRepository puzzleRepository;
private final FaceRepository faceRepository;
private final IPriceCalculationService iPriceCalculationService;
private final PrinterService printerService;
private final OrderBiz orderBiz;
private final MemberPuzzleMapper memberPuzzleMapper;
private final WatermarkEdgeTaskCreator watermarkEdgeTaskCreator;
private final FaceService faceService;
private final ScenicRepository scenicRepository;
/**
* 根据faceId查询三拼图数量
*/
@GetMapping("/count/{faceId}")
public ApiResponse<Integer> countByFaceId(@PathVariable("faceId") Long faceId) {
if (faceId == null) {
return ApiResponse.fail("faceId不能为空");
}
// 通过关联表查询数量
List<MemberPuzzleEntity> relations = memberPuzzleMapper.listByFaceId(faceId);
return ApiResponse.success(relations.size());
}
/**
* 根据faceId查询所有三拼图记录
*/
@GetMapping("/list/{faceId}")
public ApiResponse<List<ContentPageVO>> listByFaceId(@PathVariable("faceId") Long faceId) {
if (faceId == null) {
return ApiResponse.fail("faceId不能为空");
}
// 通过关联表查询,获取关联的拼图记录
List<MemberPuzzleEntity> relations = memberPuzzleMapper.listByFaceId(faceId);
List<ContentPageVO> result = relations.stream()
.map(relation -> {
PuzzleGenerationRecordEntity record = puzzleRepository.getRecordById(relation.getRecordId());
if (record == null) {
return null;
}
return convertToContentPageVO(record, relation);
})
.filter(vo -> vo != null)
.collect(Collectors.toList());
return ApiResponse.success(result);
}
/**
* 根据recordId查询单个三拼图记录
*/
@GetMapping("/detail/{recordId}")
public ApiResponse<ContentPageVO> getByRecordId(@PathVariable("recordId") Long recordId) {
if (recordId == null) {
return ApiResponse.fail("recordId不能为空");
}
PuzzleGenerationRecordEntity record = puzzleRepository.getRecordById(recordId);
if (record == null) {
return ApiResponse.fail("未找到对应的拼图记录");
}
// 查询关联记录
MemberPuzzleEntity relation = memberPuzzleMapper.getByFaceAndRecord(record.getFaceId(), recordId);
ContentPageVO result = convertToContentPageVO(record, relation);
return ApiResponse.success(result);
}
/**
* 根据recordId下载拼图资源
* 如果是免费赠送的拼图,会添加水印后返回
*/
@GetMapping("/download/{recordId}")
public ApiResponse<List<String>> download(@PathVariable("recordId") Long recordId) {
if (recordId == null) {
return ApiResponse.fail("recordId不能为空");
}
PuzzleGenerationRecordEntity record = puzzleRepository.getRecordById(recordId);
if (record == null) {
return ApiResponse.fail("未找到对应的拼图记录");
}
String resultImageUrl = record.getResultImageUrl();
if (resultImageUrl == null || resultImageUrl.isEmpty()) {
return ApiResponse.fail("该拼图记录没有可用的图片URL");
}
// 查询该拼图的关联记录,判断是否为免费赠送
Long faceId = record.getFaceId();
if (faceId != null) {
MemberPuzzleEntity memberPuzzle = memberPuzzleMapper.getByFaceAndRecord(faceId, recordId);
if (memberPuzzle != null && FreeStatus.isFree(memberPuzzle.getIsFree())) {
// 免费赠送的拼图,需要添加水印
String watermarkedUrl = addWatermarkForFreePuzzle(record);
if (watermarkedUrl != null) {
return ApiResponse.success(Collections.singletonList(watermarkedUrl));
}
// 如果水印添加失败,记录日志并返回原图
log.warn("免费拼图水印添加失败,返回原图: recordId={}", recordId);
}
}
return ApiResponse.success(Collections.singletonList(resultImageUrl));
}
/**
* 为免费赠送的拼图添加水印
*
* @param record 拼图记录
* @return 带水印的图片URL,失败返回null
*/
private String addWatermarkForFreePuzzle(PuzzleGenerationRecordEntity record) {
try {
Long faceId = record.getFaceId();
FaceEntity face = faceRepository.getFace(faceId);
if (face == null) {
log.warn("添加水印失败:未找到人脸信息, faceId={}", faceId);
return null;
}
// 获取景区信息
ScenicEntity scenic = scenicRepository.getScenic(face.getScenicId());
String scenicLine = scenic != null ? scenic.getName() : "";
// 获取二维码URL
String qrcodeUrl = faceService.bindWxaCode(faceId);
// 格式化日期时间
String datetimeLine = record.getCreateTime() != null
? DateUtil.format(record.getCreateTime(), "yyyy-MM-dd")
: "";
// 构建水印请求
WatermarkRequest request = WatermarkRequest.builder()
.originalImageUrl(record.getResultImageUrl())
.imageWidth(record.getResultWidth() != null ? record.getResultWidth() : 0)
.imageHeight(record.getResultHeight() != null ? record.getResultHeight() : 0)
.qrcodeUrl(qrcodeUrl)
.faceUrl(face.getFaceUrl())
.scenicLine(scenicLine)
.datetimeLine(datetimeLine)
.outputFormat("JPEG")
.outputQuality(90)
.build();
// 创建水印任务并等待结果
PuzzleEdgeRenderTaskService.TaskWaitResult result = watermarkEdgeTaskCreator.createAndWait(
PuzzleDefaultWatermarkTemplateBuilder.STYLE,
request,
record.getId(),
faceId,
"free_puzzle_download",
30_000L // 30秒超时
);
if (result.isSuccess()) {
log.info("免费拼图水印添加成功: recordId={}, url={}", record.getId(), result.getImageUrl());
return result.getImageUrl();
} else {
log.error("免费拼图水印添加失败: recordId={}, error={}", record.getId(), result.getErrorMessage());
return null;
}
} catch (Exception e) {
log.error("免费拼图水印添加异常: recordId={}", record.getId(), e);
return null;
}
}
/**
* 根据recordId查询拼图价格
*/
@GetMapping("/price/{recordId}")
public ApiResponse<PriceCalculationResult> getPriceByRecordId(@PathVariable("recordId") Long recordId) {
if (recordId == null) {
return ApiResponse.fail("recordId不能为空");
}
PuzzleGenerationRecordEntity record = puzzleRepository.getRecordById(recordId);
if (record == null) {
return ApiResponse.fail("未找到对应的拼图记录");
}
FaceEntity face = faceRepository.getFace(record.getFaceId());
if (face == null) {
return ApiResponse.fail("未找到对应的人脸信息");
}
PriceCalculationRequest calculationRequest = new PriceCalculationRequest();
ProductItem productItem = new ProductItem();
productItem.setProductType(ProductType.PHOTO_LOG);
productItem.setProductId(record.getTemplateId().toString());
productItem.setPurchaseCount(1);
productItem.setScenicId(face.getScenicId().toString());
calculationRequest.setProducts(Collections.singletonList(productItem));
calculationRequest.setUserId(face.getMemberId());
calculationRequest.setFaceId(record.getFaceId());
calculationRequest.setPreviewOnly(true); // 仅查询价格,不实际使用优惠
PriceCalculationResult calculationResult = iPriceCalculationService.calculatePrice(calculationRequest);
return ApiResponse.success(calculationResult);
}
/**
* 将拼图导入到打印列表
*/
@PostMapping("/import-to-print/{recordId}")
public ApiResponse<Integer> importToPrint(@PathVariable("recordId") Long recordId) {
if (recordId == null) {
return ApiResponse.fail("recordId不能为空");
}
// 查询拼图记录
PuzzleGenerationRecordEntity record = puzzleRepository.getRecordById(recordId);
if (record == null) {
return ApiResponse.fail("未找到对应的拼图记录");
}
// 检查是否有图片URL
String imageUrl = record.getResultImageUrl();
if (imageUrl == null || imageUrl.isEmpty()) {
return ApiResponse.fail("该拼图记录没有可用的图片URL");
}
// 获取人脸信息
FaceEntity face = faceRepository.getFace(record.getFaceId());
if (face == null) {
return ApiResponse.fail("未找到对应的人脸信息");
}
// 调用服务添加到打印列表
Integer memberPrintId = printerService.addUserPhotoFromPuzzle(
face.getMemberId(),
face.getScenicId(),
record.getFaceId(),
imageUrl,
recordId // 拼图记录ID,用于关联 puzzle_record 表
);
if (memberPrintId == null) {
return ApiResponse.fail("添加到打印列表失败");
}
return ApiResponse.success(memberPrintId);
}
/**
* 将PuzzleGenerationRecordEntity转换为ContentPageVO
*
* @param record 拼图生成记录
* @param relation 会员拼图关联记录,用于获取免费状态
*/
private ContentPageVO convertToContentPageVO(PuzzleGenerationRecordEntity record, MemberPuzzleEntity relation) {
ContentPageVO vo = new ContentPageVO();
// 内容类型为3(拼图)
vo.setContentType(3);
// 源素材类型为3(拼图)
vo.setSourceType(3);
vo.setGroup("拼图");
// 只要存在记录,lockType不为0(设置为-1表示已生成)
vo.setLockType(-1);
// 通过faceId填充scenicId的信息
FaceEntity face = faceRepository.getFace(record.getFaceId());
if (record.getFaceId() != null) {
vo.setScenicId(face.getScenicId());
}
// contentId为生成记录id
vo.setContentId(record.getId());
// templateCoverUrl和生成的图是一致的
vo.setTemplateCoverUrl(record.getResultImageUrl());
// 设置模板ID
vo.setTemplateId(record.getTemplateId());
IsBuyRespVO isBuyScenic = orderBiz.isBuy(face.getScenicId(), face.getMemberId(), face.getId(), 5, face.getScenicId());
if (isBuyScenic.isBuy()) {
vo.setIsBuy(1);
} else {
IsBuyRespVO isBuyRespVO = orderBiz.isBuy(face.getScenicId(), face.getMemberId(), face.getId(), 5, record.getTemplateId());
if (isBuyRespVO.isBuy()) {
vo.setIsBuy(1);
} else {
vo.setIsBuy(0);
// 从关联记录读取免费状态
if (relation != null && FreeStatus.isFree(relation.getIsFree())) {
vo.setFreeCount(1);
} else {
vo.setFreeCount(0);
}
}
}
return vo;
}
}

View File

@@ -0,0 +1,76 @@
package com.ycwl.basic.controller.mobile;
import com.ycwl.basic.annotation.IgnoreToken;
import com.ycwl.basic.constant.BaseContextHandler;
import com.ycwl.basic.integration.questionnaire.dto.answer.ResponseDetailResponse;
import com.ycwl.basic.integration.questionnaire.dto.answer.SubmitAnswerRequest;
import com.ycwl.basic.integration.questionnaire.dto.questionnaire.QuestionnaireResponse;
import com.ycwl.basic.integration.questionnaire.service.QuestionnaireIntegrationService;
import com.ycwl.basic.utils.ApiResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
/**
* 移动端问卷接口控制器
*
* @author Claude Code
* @date 2025-09-05
*/
@Slf4j
@RestController
@RequestMapping("/api/mobile/questionnaire/v1")
@RequiredArgsConstructor
public class AppQuestionnaireController {
private final QuestionnaireIntegrationService questionnaireIntegrationService;
/**
* 获取问卷详情
* 包含问卷基本信息和所有题目
*/
@IgnoreToken
@GetMapping("/{id}")
public ApiResponse<QuestionnaireResponse> getQuestionnaire(@PathVariable Long id) {
log.info("移动端获取问卷详情, id: {}", id);
try {
QuestionnaireResponse questionnaire = questionnaireIntegrationService.getQuestionnaire(id);
// 检查问卷状态,只有已发布的问卷才能被移动端访问
if (questionnaire.getStatus() != 2) {
return ApiResponse.fail("问卷未发布或已停止");
}
return ApiResponse.success(questionnaire);
} catch (Exception e) {
log.error("移动端获取问卷详情失败, id: {}", id, e);
return ApiResponse.fail("获取问卷详情失败: " + e.getMessage());
}
}
/**
* 提交问卷答案
*/
@PostMapping("/{id}/submit")
public ApiResponse<ResponseDetailResponse> submitAnswer(
@PathVariable Long id,
@Valid @RequestBody SubmitAnswerRequest request) {
String userId = BaseContextHandler.getUserId();
log.info("移动端提交问卷答案, questionnaireId: {}, userId: {}, answers count: {}",
id, userId, request.getAnswers() != null ? request.getAnswers().size() : 0);
try {
// 设置问卷ID和用户ID
request.setQuestionnaireId(id);
request.setUserId(userId);
ResponseDetailResponse response = questionnaireIntegrationService.submitAnswer(request);
return ApiResponse.success(response);
} catch (Exception e) {
log.error("移动端提交问卷答案失败, questionnaireId: {}, userId: {}", id, userId, e);
return ApiResponse.fail("提交问卷答案失败: " + e.getMessage());
}
}
}

View File

@@ -1,116 +0,0 @@
package com.ycwl.basic.controller.mobile;
import com.github.pagehelper.PageInfo;
import com.ycwl.basic.annotation.IgnoreToken;
import com.ycwl.basic.constant.BaseContextHandler;
import com.ycwl.basic.model.jwt.JwtInfo;
import com.ycwl.basic.model.mobile.scenic.ScenicAppVO;
import com.ycwl.basic.model.mobile.scenic.ScenicDeviceCountVO;
import com.ycwl.basic.model.mobile.scenic.ScenicIndexVO;
import com.ycwl.basic.model.mobile.scenic.content.ContentPageVO;
import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity;
import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery;
import com.ycwl.basic.model.pc.scenic.resp.ScenicConfigResp;
import com.ycwl.basic.model.pc.scenic.resp.ScenicRespVO;
import com.ycwl.basic.repository.ScenicRepository;
import com.ycwl.basic.service.mobile.AppScenicService;
import com.ycwl.basic.service.pc.FaceService;
import com.ycwl.basic.utils.ApiResponse;
import com.ycwl.basic.utils.JwtTokenUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
/**
* @Author:longbinbin
* @Date:2024/12/5 10:22
*/
@Slf4j
@RestController
@RequestMapping("/api/mobile/scenic/v1")
// 景区相关接口
public class AppScenicController {
@Autowired
private FaceService faceService;
@Autowired
private AppScenicService appScenicService;
@Autowired
private ScenicRepository scenicRepository;
private static final List<String> ENABLED_USER_IDs = new ArrayList<>(){{
add("3932535453961555968");
add("3936121342868459520");
add("3936940597855784960");
}};
// 分页查询景区列表
@PostMapping("/page")
public ApiResponse<PageInfo<ScenicAppVO>> pageQuery(@RequestBody ScenicReqQuery scenicReqQuery){
String userId = BaseContextHandler.getUserId();
if (ENABLED_USER_IDs.contains(userId)) {
return appScenicService.pageQuery(scenicReqQuery);
} else {
return ApiResponse.success(new PageInfo<>(new ArrayList<>()));
}
}
// 根据id查询景区详情
@IgnoreToken
@GetMapping("/{id}")
public ApiResponse<ScenicRespVO> getDetails(@PathVariable Long id){
return appScenicService.getDetails(id);
}
@GetMapping("/{id}/config")
@IgnoreToken
public ApiResponse<ScenicConfigResp> getConfig(@PathVariable Long id){
ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(id);
ScenicConfigResp resp = new ScenicConfigResp();
resp.setBookRoutine(scenicConfig.getBookRoutine());
resp.setForceFinishTime(scenicConfig.getForceFinishTime());
resp.setTourTime(scenicConfig.getTourTime());
resp.setSampleStoreDay(scenicConfig.getSampleStoreDay());
resp.setFaceStoreDay(scenicConfig.getFaceStoreDay());
resp.setVideoStoreDay(scenicConfig.getVideoStoreDay());
resp.setAllFree(scenicConfig.getAllFree());
resp.setDisableSourceVideo(scenicConfig.getDisableSourceVideo());
resp.setDisableSourceImage(scenicConfig.getDisableSourceImage());
resp.setAntiScreenRecordType(scenicConfig.getAntiScreenRecordType());
resp.setVideoSourceStoreDay(scenicConfig.getVideoSourceStoreDay());
resp.setImageSourceStoreDay(scenicConfig.getImageSourceStoreDay());
resp.setUserSourceExpireDay(scenicConfig.getUserSourceExpireDay());
resp.setBrokerDirectRate(scenicConfig.getBrokerDirectRate());
resp.setVideoSourcePackHint(scenicConfig.getVideoSourcePackHint());
resp.setImageSourcePackHint(scenicConfig.getImageSourcePackHint());
resp.setVoucherEnable(scenicConfig.getVoucherEnable());
return ApiResponse.success(resp);
}
// 查询景区设备总数和拍到用户的机位数量
@GetMapping("/{scenicId}/deviceCount/")
public ApiResponse<ScenicDeviceCountVO> deviceCountByScenicId(@PathVariable Long scenicId){
return appScenicService.deviceCountByScenicId(scenicId);
}
// 景区视频源素材列表
@GetMapping("/contentList/")
public ApiResponse<List<ContentPageVO>> contentList() {
return faceService.contentListUseDefaultFace();
}
// 景区视频源素材列表
@GetMapping("/face/{faceId}/contentList")
public ApiResponse<List<ContentPageVO>> contentList(@PathVariable Long faceId) {
List<ContentPageVO> contentPageVOS = faceService.faceContentList(faceId);
return ApiResponse.success(contentPageVOS);
}
@PostMapping("/nearby")
public ApiResponse<List<ScenicAppVO>> nearby(@RequestBody ScenicIndexVO scenicIndexVO) {
List<ScenicAppVO> list = appScenicService.scenicListByLnLa(scenicIndexVO);
return ApiResponse.success(list);
}
}

View File

@@ -27,7 +27,6 @@ public class AppTaskController {
@GetMapping("/face/{faceId}") @GetMapping("/face/{faceId}")
@IgnoreLogReq @IgnoreLogReq
public ApiResponse<VideoTaskStatusVO> getTaskStatusByFaceId(@PathVariable("faceId") Long faceId) { public ApiResponse<VideoTaskStatusVO> getTaskStatusByFaceId(@PathVariable("faceId") Long faceId) {
JwtInfo worker = JwtTokenUtil.getWorker();
return ApiResponse.success(goodsService.getTaskStatusByFaceId(faceId)); return ApiResponse.success(goodsService.getTaskStatusByFaceId(faceId));
} }
@GetMapping("/scenic/{scenicId}") @GetMapping("/scenic/{scenicId}")
@@ -49,13 +48,12 @@ public class AppTaskController {
@GetMapping("/face/{faceId}/template/{templateId}") @GetMapping("/face/{faceId}/template/{templateId}")
@IgnoreLogReq @IgnoreLogReq
public ApiResponse<VideoTaskStatusVO> getTemplateTaskStatus(@PathVariable("faceId") Long faceId, @PathVariable("templateId") Long templateId) { public ApiResponse<VideoTaskStatusVO> getTemplateTaskStatus(@PathVariable("faceId") Long faceId, @PathVariable("templateId") Long templateId) {
JwtInfo worker = JwtTokenUtil.getWorker();
return ApiResponse.success(goodsService.getTaskStatusByTemplateId(faceId, templateId)); return ApiResponse.success(goodsService.getTaskStatusByTemplateId(faceId, templateId));
} }
@PostMapping("/submit") @PostMapping("/submit")
public ApiResponse<String> submitVideoTask(@RequestBody VideoTaskReq videoTaskReq) { public ApiResponse<String> submitVideoTask(@RequestBody VideoTaskReq videoTaskReq) {
taskService.createTaskByFaceIdAndTempalteId(videoTaskReq.getFaceId(),videoTaskReq.getTemplateId(),0); taskService.createTaskByFaceIdAndTemplateId(videoTaskReq.getFaceId(),videoTaskReq.getTemplateId(),false);
return ApiResponse.success("成功"); return ApiResponse.success("成功");
} }
} }

View File

@@ -0,0 +1,131 @@
package com.ycwl.basic.controller.mobile;
import com.ycwl.basic.annotation.IgnoreToken;
import com.ycwl.basic.model.mobile.scenic.content.ScenicTemplateContentVO;
import com.ycwl.basic.model.pc.template.resp.TemplateRespVO;
import com.ycwl.basic.puzzle.entity.PuzzleTemplateEntity;
import com.ycwl.basic.puzzle.repository.PuzzleRepository;
import com.ycwl.basic.repository.TemplateRepository;
import com.ycwl.basic.utils.ApiResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* 移动端模板接口
*/
@RestController
@RequestMapping("/api/mobile/template/v1")
@RequiredArgsConstructor
public class AppTemplateController {
private final TemplateRepository templateRepository;
private final PuzzleRepository puzzleRepository;
/**
* 根据模板ID获取封面URL
*
* @param templateId 模板ID
* @return 模板封面URL
*/
@GetMapping("/cover/{templateId}")
public ApiResponse<String> getTemplateCoverUrl(@PathVariable("templateId") Long templateId) {
if (templateId == null) {
return ApiResponse.fail("模板ID不能为空");
}
TemplateRespVO template = templateRepository.getTemplate(templateId);
if (template == null) {
return ApiResponse.fail("未找到对应的模板");
}
String coverUrl = template.getCoverUrl();
if (coverUrl == null || coverUrl.isEmpty()) {
return ApiResponse.fail("该模板没有封面地址");
}
return ApiResponse.success(coverUrl);
}
/**
* 根据景区ID获取所有模板封面URL列表(用于前端预缓存)
*
* @param scenicId 景区ID
* @return 模板封面URL列表
*/
@GetMapping("/scenic/{scenicId}/covers")
@IgnoreToken
public ApiResponse<List<String>> getScenicTemplateCoverUrls(@PathVariable("scenicId") Long scenicId) {
if (scenicId == null) {
return ApiResponse.fail("景区ID不能为空");
}
List<String> coverUrls = new ArrayList<>();
// 获取普通模板封面
List<TemplateRespVO> templateList = templateRepository.getTemplateListByScenicId(scenicId);
templateList.stream()
.map(TemplateRespVO::getCoverUrl)
.filter(Objects::nonNull)
.filter(url -> !url.isEmpty())
.forEach(coverUrls::add);
// 获取拼图模板封面(使用缓存)
List<PuzzleTemplateEntity> puzzleTemplateList = puzzleRepository.listTemplateByScenic(scenicId);
puzzleTemplateList.stream()
.map(PuzzleTemplateEntity::getCoverImage)
.filter(Objects::nonNull)
.filter(url -> !url.isEmpty())
.forEach(coverUrls::add);
return ApiResponse.success(coverUrls);
}
/**
* 根据景区ID获取所有模板内容列表(返回模板基础信息,与 faceId 无关)
*
* @param scenicId 景区ID
* @return 景区模板内容列表
*/
@GetMapping("/scenic/{scenicId}/contents")
@IgnoreToken
public ApiResponse<List<ScenicTemplateContentVO>> getScenicTemplateContents(@PathVariable("scenicId") Long scenicId) {
if (scenicId == null) {
return ApiResponse.fail("景区ID不能为空");
}
List<ScenicTemplateContentVO> contentList = new ArrayList<>();
// 获取普通模板
List<TemplateRespVO> templateList = templateRepository.getTemplateListByScenicId(scenicId);
for (TemplateRespVO template : templateList) {
ScenicTemplateContentVO content = new ScenicTemplateContentVO();
content.setGoodsType(0); // 普通模板默认商品类型为 0
content.setName(template.getName());
content.setGroup(template.getGroup());
content.setTemplateId(template.getId());
content.setTemplateCoverUrl(template.getCoverUrl());
contentList.add(content);
}
// 获取拼图模板
List<PuzzleTemplateEntity> puzzleTemplateList = puzzleRepository.listTemplateByScenic(scenicId);
for (PuzzleTemplateEntity puzzleTemplate : puzzleTemplateList) {
ScenicTemplateContentVO content = new ScenicTemplateContentVO();
content.setGoodsType(3); // 拼图模板商品类型为 3
content.setName(puzzleTemplate.getName());
content.setGroup("氛围拼图"); // 拼图模板固定分组
content.setTemplateId(puzzleTemplate.getId());
content.setTemplateCoverUrl(puzzleTemplate.getCoverImage());
contentList.add(content);
}
return ApiResponse.success(contentList);
}
}

View File

@@ -1,23 +1,124 @@
package com.ycwl.basic.controller.mobile; package com.ycwl.basic.controller.mobile;
import com.ycwl.basic.constant.BaseContextHandler;
import com.ycwl.basic.model.mobile.video.dto.VideoViewPermissionDTO;
import com.ycwl.basic.model.pc.video.resp.VideoRespVO;
import com.ycwl.basic.model.task.req.VideoInfoReq; import com.ycwl.basic.model.task.req.VideoInfoReq;
import com.ycwl.basic.repository.VideoRepository; import com.ycwl.basic.repository.VideoRepository;
import com.ycwl.basic.service.mobile.VideoViewPermissionService;
import com.ycwl.basic.utils.ApiResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
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.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;
@Deprecated @Slf4j
@RestController @RestController
@RequestMapping("/api/mobile/video/v1") @RequestMapping("/api/mobile/video/v1")
public class AppVideoController { public class AppVideoController {
@Autowired @Autowired
private VideoRepository videoRepository; private VideoRepository videoRepository;
@Autowired
private VideoViewPermissionService videoViewPermissionService;
@PostMapping("/{videoId}/updateMeta") @PostMapping("/{videoId}/updateMeta")
public void updateMeta(@PathVariable("videoId") Long videoId, @RequestBody VideoInfoReq req) { public void updateMeta(@PathVariable("videoId") Long videoId, @RequestBody VideoInfoReq req) {
videoRepository.updateMeta(videoId, req); videoRepository.updateMeta(videoId, req);
} }
/**
* 记录用户查看视频并返回权限信息
*
* @param videoId 视频ID
* @return 查看权限信息
*/
@PostMapping("/{videoId}/recordView")
public ApiResponse<VideoViewPermissionDTO> recordView(@PathVariable("videoId") Long videoId) {
try {
String userIdStr = BaseContextHandler.getUserId();
if (userIdStr == null || userIdStr.isEmpty()) {
log.warn("用户未登录,无法记录查看: videoId={}", videoId);
return ApiResponse.fail("用户未登录");
}
Long userId = Long.valueOf(userIdStr);
log.debug("记录用户查看视频: userId={}, videoId={}", userId, videoId);
VideoViewPermissionDTO permission = videoViewPermissionService.checkAndRecordView(userId, videoId);
return ApiResponse.success(permission);
} catch (NumberFormatException e) {
log.error("用户ID格式错误: userId={}, videoId={}", BaseContextHandler.getUserId(), videoId, e);
return ApiResponse.fail("用户信息无效");
} catch (Exception e) {
log.error("记录用户查看视频失败: videoId={}", videoId, e);
return ApiResponse.fail("记录查看失败,请稍后重试");
}
}
/**
* 检查用户查看权限(不记录查看次数)
*
* @param videoId 视频ID
* @return 查看权限信息
*/
@GetMapping("/{videoId}/checkPermission")
public ApiResponse<VideoViewPermissionDTO> checkPermission(@PathVariable("videoId") Long videoId) {
try {
String userIdStr = BaseContextHandler.getUserId();
if (userIdStr == null || userIdStr.isEmpty()) {
log.warn("用户未登录,无法查看权限: videoId={}", videoId);
return ApiResponse.fail("用户未登录");
}
Long userId = Long.valueOf(userIdStr);
log.debug("检查用户查看权限: userId={}, videoId={}", userId, videoId);
VideoViewPermissionDTO permission = videoViewPermissionService.checkViewPermission(userId, videoId);
return ApiResponse.success(permission);
} catch (NumberFormatException e) {
log.error("用户ID格式错误: userId={}, videoId={}", BaseContextHandler.getUserId(), videoId, e);
return ApiResponse.fail("用户信息无效");
} catch (Exception e) {
log.error("检查用户查看权限失败: videoId={}", videoId, e);
return ApiResponse.fail("权限检查失败,请稍后重试");
}
}
/**
* 通过faceId和templateId(可选)查询最新的视频记录
*
* @param faceId 人脸ID
* @param templateId 模板ID(可选)
* @return 最新的视频记录
*/
@GetMapping("/latest")
public ApiResponse<VideoRespVO> getLatestByFaceId(
@RequestParam("faceId") Long faceId,
@RequestParam(value = "templateId", required = false) Long templateId) {
try {
log.debug("查询最新视频记录: faceId={}, templateId={}", faceId, templateId);
VideoRespVO video = videoRepository.queryLatestByFaceIdAndTemplateId(faceId, templateId);
if (video == null) {
log.info("未找到视频记录: faceId={}, templateId={}", faceId, templateId);
return ApiResponse.fail("未找到视频记录");
}
return ApiResponse.success(video);
} catch (Exception e) {
log.error("查询最新视频记录失败: faceId={}, templateId={}", faceId, templateId, e);
return ApiResponse.fail("查询失败,请稍后重试");
}
}
} }

View File

@@ -64,9 +64,6 @@ public class AppVoucherController {
if (face == null) { if (face == null) {
throw new BaseException("请选择人脸"); throw new BaseException("请选择人脸");
} }
if (!face.getMemberId().equals(Long.valueOf(BaseContextHandler.getUserId()))) {
throw new BaseException("自动领取失败");
}
req.setScenicId(face.getScenicId()); req.setScenicId(face.getScenicId());
VoucherCodeResp result = voucherCodeService.claimVoucher(req); VoucherCodeResp result = voucherCodeService.claimVoucher(req);
return ApiResponse.success(result); return ApiResponse.success(result);

View File

@@ -1,67 +0,0 @@
package com.ycwl.basic.controller.mobile;
import com.ycwl.basic.annotation.IgnoreToken;
import com.ycwl.basic.repository.ScenicRepository;
import com.ycwl.basic.utils.ApiResponse;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
/**
* @Author: songmingsong
* @CreateTime: 2024-12-06
* @Description: 微信消息模板通知
* @Version: 1.0
*/
@RestController
@RequestMapping("/api/mobile/wx/notify/v1")
// 微信消息模板通知
public class AppWxNotifyController {
@Autowired
private ScenicRepository scenicRepository;
//
// @ApiOperation(value = "通知", notes = "通知")
// @PostMapping("/pushMessage")
// @IgnoreToken
// public ApiResponse<?> pushMessage(@RequestBody WechatMessageSubscribeForm req) {
// JSONObject resJson = wxNotifyService.pushMessage(req);
// return ApiResponse.success(resJson);
// }
@GetMapping({"/getIds", "/"})
@IgnoreToken
public ApiResponse<List<String>> getIds() {
return ApiResponse.success(new ArrayList<>() {{
add("5b8vTm7kvwYubqDxb3dxBs0BqxMsgVgGw573aahTEd8");
add("vPIzbkA0x4mMj-vdbWx6_45e8juWXzs3FGYnDsIPv3A");
add("HB1vp-0BXc2WyYeoYN3a3GuZV9HtPLXUTT7blCBq9eY");
}});
}
@GetMapping("/{scenicId}")
@IgnoreToken
public ApiResponse<List<String>> getIds(@PathVariable("scenicId") Long scenicId) {
return ApiResponse.success(new ArrayList<>() {{
String videoGeneratedTemplateId = scenicRepository.getVideoGeneratedTemplateId(scenicId);
if (StringUtils.isNotBlank(videoGeneratedTemplateId)) {
add(videoGeneratedTemplateId);
}
String videoDownloadTemplateId = scenicRepository.getVideoDownloadTemplateId(scenicId);
if (StringUtils.isNotBlank(videoDownloadTemplateId)) {
add(videoDownloadTemplateId);
}
String videoPreExpireTemplateId = scenicRepository.getVideoPreExpireTemplateId(scenicId);
if (StringUtils.isNotBlank(videoPreExpireTemplateId)) {
add(videoPreExpireTemplateId);
}
}});
}
}

View File

@@ -2,14 +2,18 @@ package com.ycwl.basic.controller.mobile.manage;
import com.ycwl.basic.annotation.IgnoreToken; import com.ycwl.basic.annotation.IgnoreToken;
import com.ycwl.basic.constant.BaseContextHandler; import com.ycwl.basic.constant.BaseContextHandler;
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO;
import com.ycwl.basic.model.mobile.scenic.account.ScenicLoginOldRespVO; import com.ycwl.basic.model.mobile.scenic.account.ScenicLoginOldRespVO;
import com.ycwl.basic.model.mobile.scenic.account.ScenicLoginReq; import com.ycwl.basic.model.mobile.scenic.account.ScenicLoginReq;
import com.ycwl.basic.model.mobile.scenic.account.ScenicLoginRespVO; import com.ycwl.basic.model.mobile.scenic.account.ScenicLoginRespVO;
import com.ycwl.basic.model.mobile.scenic.account.ScenicRegisterReq;
import com.ycwl.basic.model.mobile.scenic.account.ScenicRegisterRespVO;
import com.ycwl.basic.model.mobile.weChat.DTO.WeChatUserInfoDTO; import com.ycwl.basic.model.mobile.weChat.DTO.WeChatUserInfoDTO;
import com.ycwl.basic.model.pc.device.resp.DeviceRespVO; import com.ycwl.basic.model.pc.device.resp.DeviceRespVO;
import com.ycwl.basic.model.pc.scenic.entity.ScenicAccountEntity; import com.ycwl.basic.model.pc.scenic.entity.ScenicAccountEntity;
import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery; import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery;
import com.ycwl.basic.model.pc.scenic.resp.ScenicRespVO; import com.ycwl.basic.model.pc.scenic.resp.ScenicRespVO;
import com.ycwl.basic.repository.ScenicRepository;
import com.ycwl.basic.service.mobile.AppScenicService; import com.ycwl.basic.service.mobile.AppScenicService;
import com.ycwl.basic.service.pc.ScenicAccountService; import com.ycwl.basic.service.pc.ScenicAccountService;
import com.ycwl.basic.service.pc.ScenicService; import com.ycwl.basic.service.pc.ScenicService;
@@ -23,10 +27,10 @@ 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.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import static com.ycwl.basic.constant.JwtRoleConstant.ADMIN;
import static com.ycwl.basic.constant.JwtRoleConstant.MERCHANT; import static com.ycwl.basic.constant.JwtRoleConstant.MERCHANT;
/** /**
@@ -43,6 +47,8 @@ public class AppScenicAccountController {
private AppScenicService scenicService; private AppScenicService scenicService;
@Autowired @Autowired
private ScenicService adminScenicService; private ScenicService adminScenicService;
@Autowired
private ScenicRepository scenicRepository;
// 登录 // 登录
@PostMapping("/login") @PostMapping("/login")
@@ -63,9 +69,16 @@ public class AppScenicAccountController {
return ApiResponse.success(vo); return ApiResponse.success(vo);
} }
// 注册
@PostMapping("/register")
@IgnoreToken
public ApiResponse<ScenicRegisterRespVO> register(@RequestBody ScenicRegisterReq scenicRegisterReq) {
return scenicService.register(scenicRegisterReq);
}
@GetMapping("/myScenicList") @GetMapping("/myScenicList")
public ApiResponse<List<ScenicRespVO>> myScenicList() { public ApiResponse<List<ScenicV2DTO>> myScenicList() {
List<ScenicRespVO> list; List<ScenicV2DTO> list = Collections.emptyList();
if (Strings.CS.equals(BaseContextHandler.getRoleId(), MERCHANT.type)) { if (Strings.CS.equals(BaseContextHandler.getRoleId(), MERCHANT.type)) {
String userId = BaseContextHandler.getUserId(); String userId = BaseContextHandler.getUserId();
ScenicAccountEntity account = accountService.getScenicAccountById(Long.valueOf(userId)); ScenicAccountEntity account = accountService.getScenicAccountById(Long.valueOf(userId));
@@ -73,10 +86,12 @@ public class AppScenicAccountController {
return ApiResponse.fail("景区账号未绑定景区"); return ApiResponse.fail("景区账号未绑定景区");
} }
list = account.getScenicId().stream() list = account.getScenicId().stream()
.map(id -> scenicService.getDetails(id).getData()) .map(id -> scenicRepository.getScenicBasic(id))
.toList(); .toList();
} else { } else if (Strings.CS.equals(BaseContextHandler.getRoleId(), ADMIN.type)) {
list = adminScenicService.list(new ScenicReqQuery()).getData(); ScenicReqQuery query = new ScenicReqQuery();
query.setPageSize(1000);
list = scenicRepository.list(query);
} }
return ApiResponse.success(list); return ApiResponse.success(list);
} }

View File

@@ -56,12 +56,4 @@ public class AppStatisticsController {
return statisticsService.userConversionFunnel(query); return statisticsService.userConversionFunnel(query);
} }
// 统计数据记录
@PostMapping("/addStatistics")
@IgnoreToken
public ApiResponse<String> addStatistics(@RequestBody StatisticsRecordAddReq req) {
return statisticsService.addStatistics(req);
}
} }

View File

@@ -0,0 +1,150 @@
package com.ycwl.basic.controller.mobile.manage;
import com.ycwl.basic.annotation.IgnoreToken;
import com.ycwl.basic.model.mobile.video.dto.HlsStreamRequest;
import com.ycwl.basic.model.mobile.video.dto.HlsStreamResponse;
import com.ycwl.basic.service.mobile.HlsStreamService;
import com.ycwl.basic.utils.ApiResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
/**
* 移动端视频流控制器
* 提供HLS视频流播放列表生成功能
*
* @author Claude Code
* @date 2025-12-26
*/
@Slf4j
@RestController
@RequestMapping("/api/mobile/video-stream/v1")
@RequiredArgsConstructor
public class AppVideoStreamController {
private final HlsStreamService hlsStreamService;
/**
* 生成设备视频的HLS播放列表(JSON格式)
* 返回包含m3u8内容和视频片段信息的JSON对象
*
* @param request HLS流请求参数
* @return HLS播放列表响应
*/
@PostMapping("/hls/playlist")
public ApiResponse<HlsStreamResponse> generateHlsPlaylist(@Validated @RequestBody HlsStreamRequest request) {
log.info("收到HLS播放列表生成请求: deviceId={}, durationMinutes={}",
request.getDeviceId(), request.getDurationMinutes());
try {
HlsStreamResponse response = hlsStreamService.generateHlsPlaylist(request);
log.info("HLS播放列表生成成功: deviceId={}, segmentCount={}, totalDuration={}s",
response.getDeviceId(), response.getSegmentCount(), response.getTotalDurationSeconds());
return ApiResponse.success(response);
} catch (Exception e) {
log.error("生成HLS播放列表失败: deviceId={}", request.getDeviceId(), e);
return ApiResponse.buildResponse(500, null, "生成失败: " + e.getMessage());
}
}
/**
* 生成设备视频的HLS播放列表(m3u8文件格式)
* 直接返回m3u8文件内容,可被视频播放器直接使用
*
* @param deviceId 设备ID
* @param durationMinutes 视频时长(分钟),默认2分钟
* @param eventPlaylist 是否为Event播放列表,默认true
* @param response HTTP响应对象
*/
@GetMapping("/hls/playlist.m3u8")
@IgnoreToken
public void generateHlsPlaylistFile(
@RequestParam Long deviceId,
@RequestParam(defaultValue = "2") Integer durationMinutes,
@RequestParam(defaultValue = "true") Boolean eventPlaylist,
HttpServletResponse response) {
log.info("收到m3u8文件生成请求: deviceId={}, durationMinutes={}",
deviceId, durationMinutes);
try {
// 构建请求参数
HlsStreamRequest request = new HlsStreamRequest();
request.setDeviceId(deviceId);
request.setDurationMinutes(durationMinutes);
request.setEventPlaylist(eventPlaylist);
// 生成播放列表
HlsStreamResponse hlsResponse = hlsStreamService.generateHlsPlaylist(request);
log.info("m3u8文件生成成功: deviceId={}, segmentCount={}, totalDuration={}s",
deviceId, hlsResponse.getSegmentCount(), hlsResponse.getTotalDurationSeconds());
// 设置响应头
response.setContentType("application/vnd.apple.mpegurl");
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setHeader("Content-Disposition", "inline; filename=\"playlist.m3u8\"");
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
response.setHeader("Pragma", "no-cache");
response.setHeader("Expires", "0");
// 写入m3u8内容
response.getWriter().write(hlsResponse.getPlaylistContent());
response.getWriter().flush();
} catch (Exception e) {
log.error("生成m3u8文件失败: deviceId={}", deviceId, e);
try {
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.getWriter().write("{\"code\":500,\"message\":\"生成失败: " + e.getMessage() + "\"}");
response.getWriter().flush();
} catch (IOException ioException) {
log.error("写入错误响应失败", ioException);
}
}
}
/**
* 获取设备最近的视频片段信息
* 仅返回视频片段列表,不包含m3u8内容
*
* @param deviceId 设备ID
* @param durationMinutes 视频时长(分钟),默认2分钟
* @return 视频片段列表
*/
@GetMapping("/segments")
public ApiResponse<HlsStreamResponse> getVideoSegments(
@RequestParam Long deviceId,
@RequestParam(defaultValue = "2") Integer durationMinutes) {
log.info("收到视频片段查询请求: deviceId={}, durationMinutes={}",
deviceId, durationMinutes);
try {
HlsStreamRequest request = new HlsStreamRequest();
request.setDeviceId(deviceId);
request.setDurationMinutes(durationMinutes);
request.setEventPlaylist(true);
HlsStreamResponse response = hlsStreamService.generateHlsPlaylist(request);
log.info("视频片段查询成功: deviceId={}, segmentCount={}",
deviceId, response.getSegmentCount());
return ApiResponse.success(response);
} catch (Exception e) {
log.error("查询视频片段失败: deviceId={}", deviceId, e);
return ApiResponse.buildResponse(500, null, "查询失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,139 @@
package com.ycwl.basic.controller.mobile.notify;
import com.ycwl.basic.model.mobile.notify.req.BatchRemainingCountReq;
import com.ycwl.basic.model.mobile.notify.req.NotificationAuthRecordReq;
import com.ycwl.basic.model.mobile.notify.resp.NotificationAuthRecordResp;
import com.ycwl.basic.model.mobile.notify.resp.ScenicTemplateAuthResp;
import com.ycwl.basic.model.pc.notify.entity.UserNotificationAuthorizationEntity;
import com.ycwl.basic.service.UserNotificationAuthorizationService;
import com.ycwl.basic.utils.ApiResponse;
import com.ycwl.basic.utils.JwtTokenUtil;
import com.ycwl.basic.repository.ScenicRepository;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 用户通知授权记录Controller (移动端API)
* 只提供用户主动授权记录功能,支持批量授权,其他检查和消费功能由系统内部调用
*
* @Author: System
* @Date: 2024/12/28
*/
@RestController
@RequestMapping("/api/mobile/notify/auth")
@Slf4j
public class UserNotificationAuthController {
@Autowired
private UserNotificationAuthorizationService userNotificationAuthorizationService;
@Autowired
private ScenicRepository scenicRepository;
/**
* 记录用户通知授权 - 支持批量授权
* 用户主动同意通知授权时调用
*/
@PostMapping("/record")
public ApiResponse<NotificationAuthRecordResp> recordAuthorization(
@RequestBody NotificationAuthRecordReq req) {
log.debug("记录用户通知授权: templateIds={}, scenicId={}, requestId={}",
req.getTemplateIds(), req.getScenicId(), req.getRequestId());
try {
// 获取当前用户ID
Long memberId = JwtTokenUtil.getWorker().getUserId();
// 调用批量授权记录方法
List<UserNotificationAuthorizationService.AuthorizationRecord> records =
userNotificationAuthorizationService.batchRecordAuthorization(
memberId, req.getTemplateIds(), req.getScenicId(), req.getRequestId());
NotificationAuthRecordResp resp = new NotificationAuthRecordResp();
// 转换响应数据
List<NotificationAuthRecordResp.AuthorizationRecord> successRecords = new ArrayList<>();
List<String> failedTemplateIds = new ArrayList<>();
List<String> failureReasons = new ArrayList<>();
for (UserNotificationAuthorizationService.AuthorizationRecord record : records) {
if (record.isSuccess()) {
NotificationAuthRecordResp.AuthorizationRecord successRecord =
new NotificationAuthRecordResp.AuthorizationRecord();
successRecord.setId(record.getId());
successRecord.setTemplateId(record.getTemplateId());
successRecord.setScenicId(record.getScenicId());
successRecord.setAuthorizationCount(record.getAuthorizationCount());
successRecord.setConsumedCount(record.getConsumedCount());
successRecord.setRemainingCount(record.getRemainingCount());
successRecord.setLastAuthorizedTime(record.getLastAuthorizedTime());
successRecord.setLastConsumedTime(record.getLastConsumedTime());
successRecord.setStatus(record.getStatus());
successRecord.setCreateTime(record.getCreateTime());
successRecords.add(successRecord);
} else {
failedTemplateIds.add(record.getTemplateId());
failureReasons.add(record.getFailureReason());
}
}
resp.setAllSuccess(CollectionUtils.isEmpty(failedTemplateIds));
resp.setSuccessRecords(successRecords);
resp.setFailedTemplateIds(failedTemplateIds);
resp.setFailureReasons(failureReasons);
return ApiResponse.success(resp);
} catch (Exception e) {
log.error("记录用户通知授权失败", e);
return ApiResponse.fail("记录授权失败: " + e.getMessage());
}
}
/**
* 批量查询用户授权余额
* 返回 Map<wechatTemplateId, remainingCount>
*/
@PostMapping("/batch-remaining")
public ApiResponse<Map<String, Integer>> batchGetRemainingCount(
@RequestBody BatchRemainingCountReq req) {
log.debug("批量查询用户授权余额: templateIds={}, scenicId={}",
req.getTemplateIds(), req.getScenicId());
try {
Long memberId = JwtTokenUtil.getWorker().getUserId();
if (memberId == null) {
return ApiResponse.fail("用户未登录");
}
if (CollectionUtils.isEmpty(req.getTemplateIds())) {
return ApiResponse.success(new HashMap<>());
}
Map<String, UserNotificationAuthorizationEntity> authMap =
userNotificationAuthorizationService.batchCheckAuthorization(
memberId, req.getTemplateIds(), req.getScenicId());
// 转换为 templateId -> remainingCount
Map<String, Integer> result = new HashMap<>();
for (String templateId : req.getTemplateIds()) {
UserNotificationAuthorizationEntity entity = authMap.get(templateId);
int remaining = (entity != null && entity.getRemainingCount() != null)
? entity.getRemainingCount() : 0;
result.put(templateId, remaining);
}
return ApiResponse.success(result);
} catch (Exception e) {
log.error("批量查询用户授权余额失败", e);
return ApiResponse.fail("查询失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,109 @@
package com.ycwl.basic.controller.mobile.notify;
import com.ycwl.basic.annotation.IgnoreToken;
import com.ycwl.basic.model.mobile.notify.resp.WechatSubscribeAllScenesResp;
import com.ycwl.basic.model.mobile.notify.resp.WechatSubscribeSceneTemplatesResp;
import com.ycwl.basic.model.pc.notify.entity.WechatSubscribeTemplateConfigEntity;
import com.ycwl.basic.service.notify.WechatSubscribeNotifyConfigService;
import com.ycwl.basic.utils.ApiResponse;
import com.ycwl.basic.utils.JwtTokenUtil;
import com.ycwl.basic.utils.NotificationAuthUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* 微信小程序订阅消息:场景模板查询(移动端API)
*
* @Author: System
* @Date: 2025/12/31
*/
@RestController
@RequestMapping("/api/mobile/notify/subscribe")
@Slf4j
public class WechatSubscribeNotifyController {
private final WechatSubscribeNotifyConfigService configService;
private final NotificationAuthUtils notificationAuthUtils;
public WechatSubscribeNotifyController(WechatSubscribeNotifyConfigService configService,
NotificationAuthUtils notificationAuthUtils) {
this.configService = configService;
this.notificationAuthUtils = notificationAuthUtils;
}
/**
* 获取“场景”下可申请授权的模板列表(支持按 scenicId 覆盖模板ID/开关/文案)
*/
@GetMapping("/scenic/{scenicId}/scenes/{sceneKey}/templates")
@IgnoreToken
public ApiResponse<WechatSubscribeSceneTemplatesResp> listSceneTemplates(@PathVariable("scenicId") Long scenicId,
@PathVariable("sceneKey") String sceneKey) {
if (scenicId == null) {
return ApiResponse.fail("scenicId不能为空");
}
if (StringUtils.isBlank(sceneKey)) {
return ApiResponse.fail("sceneKey不能为空");
}
Long memberId = JwtTokenUtil.getWorker().getUserId();
List<WechatSubscribeTemplateConfigEntity> configs = configService.listSceneTemplateConfigs(scenicId, sceneKey);
WechatSubscribeSceneTemplatesResp resp = new WechatSubscribeSceneTemplatesResp();
resp.setScenicId(scenicId);
resp.setSceneKey(sceneKey);
if (memberId == null) {
return ApiResponse.success(resp);
}
List<WechatSubscribeSceneTemplatesResp.TemplateInfo> templates = new ArrayList<>();
for (WechatSubscribeTemplateConfigEntity cfg : configs) {
if (cfg == null || StringUtils.isBlank(cfg.getWechatTemplateId())) {
continue;
}
String title = StringUtils.isNotBlank(cfg.getTitleTemplate())
? cfg.getTitleTemplate()
: cfg.getTemplateKey();
int remaining = notificationAuthUtils.getRemainingCount(memberId, cfg.getWechatTemplateId(), scenicId);
WechatSubscribeSceneTemplatesResp.TemplateInfo info = new WechatSubscribeSceneTemplatesResp.TemplateInfo();
info.setTemplateKey(cfg.getTemplateKey());
info.setWechatTemplateId(cfg.getWechatTemplateId());
info.setTitle(title);
info.setDescription(cfg.getDescription());
info.setRemainingCount(remaining);
info.setHasAuthorization(remaining > 0);
templates.add(info);
}
resp.setTemplates(templates);
log.debug("场景模板查询: scenicId={}, sceneKey={}, memberId={}, templateCount={}",
scenicId, sceneKey, memberId, Objects.requireNonNullElse(templates.size(), 0));
return ApiResponse.success(resp);
}
/**
* 获取景区下所有场景及其模板列表(静态配置,带缓存)
* 不含用户授权信息,用户授权信息通过 /api/mobile/notify/auth/batch-remaining 接口获取
*/
@GetMapping("/scenic/{scenicId}/scenes")
@IgnoreToken
public ApiResponse<WechatSubscribeAllScenesResp> listAllSceneTemplates(@PathVariable("scenicId") Long scenicId) {
if (scenicId == null) {
return ApiResponse.fail("scenicId不能为空");
}
WechatSubscribeAllScenesResp resp = configService.getAllScenesWithTemplatesCached(scenicId);
log.debug("所有场景模板查询: scenicId={}, sceneCount={}",
scenicId, resp.getScenes() != null ? resp.getScenes().size() : 0);
return ApiResponse.success(resp);
}
}

View File

@@ -0,0 +1,50 @@
package com.ycwl.basic.controller.monitor;
import com.ycwl.basic.integration.kafka.scheduler.AccountFaceSchedulerManager;
import com.ycwl.basic.utils.ApiResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
/**
* 人脸识别监控接口
* 提供调度器状态查询功能
*/
@RestController
@RequestMapping("/api/monitor/face-recognition")
@RequiredArgsConstructor
public class FaceRecognitionMonitorController {
private final AccountFaceSchedulerManager schedulerManager;
/**
* 获取所有账号的调度器统计信息
* <p>
* 示例返回:
* {
* "LTAI5xxx": {
* "accountKey": "LTAI5xxx",
* "cloudType": "ALI",
* "activeThreads": 3,
* "executorQueueSize": 12,
* "schedulerQueueSize": 45
* },
* "245xxx": {
* "accountKey": "245xxx",
* "cloudType": "BAIDU",
* "activeThreads": 8,
* "executorQueueSize": 5,
* "schedulerQueueSize": 20
* }
* }
*
* @return 所有账号的调度器状态
*/
@GetMapping("/schedulers")
public ApiResponse<Map<String, AccountFaceSchedulerManager.AccountSchedulerStats>> getAllSchedulerStats() {
return ApiResponse.success(schedulerManager.getAllStats());
}
}

View File

@@ -1,70 +0,0 @@
package com.ycwl.basic.controller.pc;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.ycwl.basic.biz.PriceBiz;
import com.ycwl.basic.model.pc.coupon.entity.CouponEntity;
import com.ycwl.basic.model.pc.coupon.req.CouponQueryReq;
import com.ycwl.basic.model.pc.coupon.resp.CouponRespVO;
import com.ycwl.basic.model.pc.price.resp.GoodsListRespVO;
import com.ycwl.basic.service.pc.CouponService;
import com.ycwl.basic.utils.ApiResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/coupon/v1")
// 优惠券管理
public class CouponController {
@Autowired
private CouponService couponService;
@Autowired
private PriceBiz priceBiz;
@GetMapping("/{scenicId}/goodsList")
public ApiResponse<List<GoodsListRespVO>> scenicGoodsList(@PathVariable Long scenicId) {
List<GoodsListRespVO> data = priceBiz.listGoodsByScenic(scenicId);
data.add(new GoodsListRespVO(-1L, "一口价"));
return ApiResponse.success(data);
}
// 新增优惠券
@PostMapping("/add")
public ApiResponse<Integer> add(@RequestBody CouponEntity coupon) {
return ApiResponse.success(couponService.add(coupon));
}
// 更新优惠券
@PostMapping("/update/{id}")
public ApiResponse<Boolean> update(@PathVariable Integer id, @RequestBody CouponEntity coupon) {
coupon.setId(id);
return ApiResponse.success(couponService.update(coupon));
}
@PutMapping("/updateStatus/{id}")
public ApiResponse<Boolean> updateStatus(@PathVariable Integer id) {
return ApiResponse.success(couponService.updateStatus(id));
}
// 删除优惠券
@DeleteMapping("/delete/{id}")
public ApiResponse<Boolean> delete(@PathVariable Integer id) {
return ApiResponse.success(couponService.delete(id));
}
// 根据ID查询优惠券
@GetMapping("/get/{id}")
public ApiResponse<CouponEntity> getById(@PathVariable Integer id) {
return ApiResponse.success(couponService.getById(id));
}
// 分页查询优惠券列表
@PostMapping("/page")
public ApiResponse<PageInfo<CouponRespVO>> list(@RequestBody CouponQueryReq couponQuery) {
PageHelper.startPage(couponQuery.getPageNum(), couponQuery.getPageSize());
List<CouponRespVO> list = couponService.list(couponQuery);
PageInfo<CouponRespVO> pageInfo = new PageInfo<>(list);
return ApiResponse.success(pageInfo);
}
}

View File

@@ -1,22 +0,0 @@
package com.ycwl.basic.controller.pc;
import com.github.pagehelper.PageInfo;
import com.ycwl.basic.model.pc.couponRecord.req.CouponRecordPageQueryReq;
import com.ycwl.basic.model.pc.couponRecord.resp.CouponRecordPageResp;
import com.ycwl.basic.service.pc.CouponRecordService;
import com.ycwl.basic.utils.ApiResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/coupon/record/v1")
public class CouponRecordController {
@Autowired
private CouponRecordService couponRecordService;
@PostMapping("/page")
public ApiResponse<PageInfo<CouponRecordPageResp>> pageQuery(@RequestBody CouponRecordPageQueryReq query) {
return couponRecordService.pageQuery(query);
}
}

View File

@@ -1,79 +0,0 @@
package com.ycwl.basic.controller.pc;
import com.github.pagehelper.PageInfo;
import com.ycwl.basic.model.pc.device.entity.DeviceConfigEntity;
import com.ycwl.basic.model.pc.device.req.DeviceAddOrUpdateReq;
import com.ycwl.basic.model.pc.device.req.DeviceBatchSortRequest;
import com.ycwl.basic.model.pc.device.req.DeviceReqQuery;
import com.ycwl.basic.model.pc.device.req.DeviceSortRequest;
import com.ycwl.basic.model.pc.device.resp.DeviceRespVO;
import com.ycwl.basic.model.pc.template.req.TemplateSortRequest;
import com.ycwl.basic.service.pc.DeviceService;
import com.ycwl.basic.utils.ApiResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* @Author:longbinbin
* @Date:2024/12/2 16:13
*/
@RestController
@RequestMapping("/api/device/v1")
// 设备管理
public class DeviceController {
@Autowired
private DeviceService deviceService;
// 设备分页查询
@PostMapping("/page")
public ApiResponse<PageInfo<DeviceRespVO>> pageQuery(@RequestBody DeviceReqQuery deviceReqQuery) {
return deviceService.pageQuery(deviceReqQuery);
}
// 设备列表查询
@PostMapping("/list")
public ApiResponse list(@RequestBody DeviceReqQuery deviceReqQuery) {
return deviceService.list(deviceReqQuery);
}
// 设备详情查询
@GetMapping("/getDetails/{id}")
public ApiResponse<DeviceRespVO> getDetails(@PathVariable("id") Long id) {
return deviceService.getById(id);
}
// 新增或修改设备
@PostMapping("/addOrUpdate")
public ApiResponse addOrUpdate(@RequestBody DeviceAddOrUpdateReq deviceReqQuery) {
return deviceService.addOrUpdate(deviceReqQuery);
}
// 删除设备
@DeleteMapping("/delete/{id}")
public ApiResponse delete(@PathVariable("id") Long id) {
return deviceService.deleteById(id);
}
// 修改设备状态
@PutMapping("/updateStatus/{id}")
public ApiResponse updateStatus(@PathVariable("id") Long id) {
return deviceService.updateStatus(id);
}
// 排序设备
@PostMapping("/sort")
public ApiResponse<Boolean> sortDevice(@RequestBody DeviceSortRequest request) {
return deviceService.sortDevice(request.getDeviceId(), request.getAfterDeviceId());
}
@PostMapping("/scenic/{scenicId}/sortBatch")
public ApiResponse<Boolean> sortDeviceBatch(@PathVariable("scenicId") Long scenicId, @RequestBody DeviceBatchSortRequest request) {
return deviceService.batchSort(scenicId, request);
}
@GetMapping("/config/{id}")
public ApiResponse<DeviceConfigEntity> getConfig(@PathVariable("id") Long id) {
return ApiResponse.success(deviceService.getConfig(id));
}
@PostMapping("/saveConfig/{configId}")
public ApiResponse saveConfig(@PathVariable("configId") Long configId, @RequestBody DeviceConfigEntity deviceConfigEntity) {
deviceService.saveConfig(configId, deviceConfigEntity);
return ApiResponse.success(null);
}
}

View File

@@ -0,0 +1,390 @@
package com.ycwl.basic.controller.pc;
import com.ycwl.basic.integration.device.dto.config.*;
import com.ycwl.basic.integration.common.response.PageResponse;
import com.ycwl.basic.integration.device.dto.device.*;
import com.ycwl.basic.integration.device.dto.status.DeviceStatusDTO;
import com.ycwl.basic.integration.device.service.DeviceConfigIntegrationService;
import com.ycwl.basic.integration.device.service.DeviceIntegrationService;
import com.ycwl.basic.integration.device.service.DeviceStatusIntegrationService;
import com.ycwl.basic.utils.ApiResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 设备管理 V2 版本控制器 - 基于 zt-device 集成服务
*
* @author Claude Code
* @date 2025-09-01
*/
@Slf4j
@RestController
@RequestMapping("/api/device/v2")
@RequiredArgsConstructor
public class DeviceV2Controller {
private final DeviceIntegrationService deviceIntegrationService;
private final DeviceConfigIntegrationService deviceConfigIntegrationService;
private final DeviceStatusIntegrationService deviceStatusIntegrationService;
// ========== 设备基础 CRUD 操作 ==========
/**
* 设备V2核心信息分页列表
*/
@GetMapping("/")
public ApiResponse<PageResponse<DeviceV2DTO>> listDevices(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) String name,
@RequestParam(required = false) String no,
@RequestParam(required = false) String type,
@RequestParam(required = false) Integer isActive,
@RequestParam(required = false) Long scenicId) {
log.info("分页查询设备核心信息列表, page: {}, pageSize: {}, name: {}, no: {}, type: {}, isActive: {}, scenicId: {}",
page, pageSize, name, no, type, isActive, scenicId);
// 参数验证:限制pageSize最大值为100
if (pageSize > 100) {
pageSize = 100;
}
try {
PageResponse<DeviceV2DTO> response = deviceIntegrationService.listDevices(page, pageSize, name, no, type, isActive, scenicId, null);
return ApiResponse.success(response);
} catch (Exception e) {
log.error("分页查询设备核心信息列表失败", e);
return ApiResponse.fail("分页查询设备列表失败: " + e.getMessage());
}
}
/**
* 根据ID获取设备信息
*/
@GetMapping("/{id}")
public ApiResponse<DeviceV2DTO> getDevice(@PathVariable Long id) {
try {
DeviceV2DTO device = deviceIntegrationService.getDevice(id);
return ApiResponse.success(device);
} catch (Exception e) {
log.error("获取设备信息失败, id: {}", id, e);
return ApiResponse.fail("获取设备信息失败: " + e.getMessage());
}
}
/**
* 根据设备编号获取设备信息
*/
@GetMapping("/no/{no}")
public ApiResponse<DeviceV2DTO> getDeviceByNo(@PathVariable String no) {
try {
DeviceV2DTO device = deviceIntegrationService.getDeviceByNo(no);
return ApiResponse.success(device);
} catch (Exception e) {
log.error("根据设备编号获取设备信息失败, no: {}", no, e);
return ApiResponse.fail("根据设备编号获取设备信息失败: " + e.getMessage());
}
}
/**
* 根据设备ID获取设备在线状态
*/
@GetMapping("/{id}/status")
public ApiResponse<DeviceStatusDTO> getDeviceOnlineStatus(@PathVariable Long id) {
log.info("获取设备在线状态, deviceId: {}", id);
try {
// 首先获取设备信息以获得设备编号
DeviceV2DTO device = deviceIntegrationService.getDevice(id);
if (device == null) {
return ApiResponse.fail("设备不存在");
}
// 使用设备编号查询在线状态
DeviceStatusDTO onlineStatus = deviceStatusIntegrationService.getDeviceStatus(device.getNo());
return ApiResponse.success(onlineStatus);
} catch (Exception e) {
log.error("获取设备在线状态失败, deviceId: {}", id, e);
return ApiResponse.fail("获取设备在线状态失败: " + e.getMessage());
}
}
/**
* 创建设备
*/
@PostMapping("/")
public ApiResponse<DeviceV2DTO> createDevice(@Valid @RequestBody CreateDeviceRequest request) {
log.info("创建设备, name: {}, no: {}, type: {}, sort: {}",
request.getName(), request.getNo(), request.getType(), request.getSort());
try {
DeviceV2DTO device = deviceIntegrationService.createDevice(request);
return ApiResponse.success(device);
} catch (Exception e) {
log.error("创建设备失败", e);
return ApiResponse.fail("创建设备失败: " + e.getMessage());
}
}
/**
* 创建IPC摄像头设备(快捷方法)
*/
@PostMapping("/ipc")
public ApiResponse<DeviceV2DTO> createIpcDevice(@RequestBody Map<String, Object> request) {
String name = (String) request.get("name");
String deviceNo = (String) request.get("no");
Long scenicId = Long.valueOf(request.get("scenicId").toString());
Integer sort = request.get("sort") != null ? Integer.valueOf(request.get("sort").toString()) : null;
log.info("创建IPC摄像头设备, name: {}, no: {}, scenicId: {}, sort: {}", name, deviceNo, scenicId, sort);
try {
DeviceV2DTO device;
if (sort != null) {
device = deviceIntegrationService.createIpcDeviceWithSort(name, deviceNo, scenicId, sort);
} else {
device = deviceIntegrationService.createIpcDevice(name, deviceNo, scenicId);
}
return ApiResponse.success(device);
} catch (Exception e) {
log.error("创建IPC摄像头设备失败", e);
return ApiResponse.fail("创建IPC摄像头设备失败: " + e.getMessage());
}
}
/**
* 创建自定义设备(快捷方法)
*/
@PostMapping("/custom")
public ApiResponse<DeviceV2DTO> createCustomDevice(@RequestBody Map<String, Object> request) {
String name = (String) request.get("name");
String deviceNo = (String) request.get("no");
Long scenicId = Long.valueOf(request.get("scenicId").toString());
Integer sort = request.get("sort") != null ? Integer.valueOf(request.get("sort").toString()) : null;
log.info("创建自定义设备, name: {}, no: {}, scenicId: {}, sort: {}", name, deviceNo, scenicId, sort);
try {
DeviceV2DTO device;
if (sort != null) {
device = deviceIntegrationService.createCustomDeviceWithSort(name, deviceNo, scenicId, sort);
} else {
device = deviceIntegrationService.createCustomDevice(name, deviceNo, scenicId);
}
return ApiResponse.success(device);
} catch (Exception e) {
log.error("创建自定义设备失败", e);
return ApiResponse.fail("创建自定义设备失败: " + e.getMessage());
}
}
/**
* 更新设备信息
*/
@PutMapping("/{id}")
public ApiResponse<String> updateDevice(@PathVariable Long id, @Valid @RequestBody UpdateDeviceRequest request) {
log.info("更新设备信息, id: {}", id);
try {
deviceIntegrationService.updateDevice(id, request);
return ApiResponse.success("设备信息更新成功");
} catch (Exception e) {
log.error("更新设备信息失败, id: {}", id, e);
return ApiResponse.fail("更新设备信息失败: " + e.getMessage());
}
}
/**
* 更新设备排序
*/
@PutMapping("/{id}/sort")
public ApiResponse<String> updateDeviceSort(@PathVariable Long id, @RequestBody Map<String, Integer> request) {
Integer sort = request.get("sort");
log.info("更新设备排序, id: {}, sort: {}", id, sort);
try {
deviceIntegrationService.updateDeviceSort(id, sort);
return ApiResponse.success("设备排序更新成功");
} catch (Exception e) {
log.error("更新设备排序失败, id: {}, sort: {}", id, sort, e);
return ApiResponse.fail("更新设备排序失败: " + e.getMessage());
}
}
/**
* 启用设备
*/
@PutMapping("/{id}/enable")
public ApiResponse<String> enableDevice(@PathVariable Long id) {
log.info("启用设备, id: {}", id);
try {
deviceIntegrationService.enableDevice(id);
return ApiResponse.success("设备启用成功");
} catch (Exception e) {
log.error("启用设备失败, id: {}", id, e);
return ApiResponse.fail("启用设备失败: " + e.getMessage());
}
}
/**
* 禁用设备
*/
@PutMapping("/{id}/disable")
public ApiResponse<String> disableDevice(@PathVariable Long id) {
log.info("禁用设备, id: {}", id);
try {
deviceIntegrationService.disableDevice(id);
return ApiResponse.success("设备禁用成功");
} catch (Exception e) {
log.error("禁用设备失败, id: {}", id, e);
return ApiResponse.fail("禁用设备失败: " + e.getMessage());
}
}
/**
* 删除设备
*/
@DeleteMapping("/{id}")
public ApiResponse<String> deleteDevice(@PathVariable Long id) {
log.info("删除设备, id: {}", id);
try {
deviceIntegrationService.deleteDevice(id);
return ApiResponse.success("设备删除成功");
} catch (Exception e) {
log.error("删除设备失败, id: {}", id, e);
return ApiResponse.fail("删除设备失败: " + e.getMessage());
}
}
// ========== 设备配置管理操作 ==========
/**
* 获取设备配置列表
*/
@GetMapping("/{id}/config")
public ApiResponse<List<DeviceConfigV2DTO>> getDeviceConfigs(@PathVariable Long id) {
try {
List<DeviceConfigV2DTO> configs = deviceConfigIntegrationService.getDeviceConfigs(id);
return ApiResponse.success(configs);
} catch (Exception e) {
log.error("获取设备配置列表失败, deviceId: {}", id, e);
return ApiResponse.fail("获取设备配置列表失败: " + e.getMessage());
}
}
/**
* 根据配置键获取配置
*/
@GetMapping("/{id}/config/{configKey}")
public ApiResponse<DeviceConfigV2DTO> getDeviceConfigByKey(@PathVariable Long id,
@PathVariable String configKey) {
try {
DeviceConfigV2DTO config = deviceConfigIntegrationService.getDeviceConfigByKey(id, configKey);
return ApiResponse.success(config);
} catch (Exception e) {
log.error("根据键获取设备配置失败, deviceId: {}, configKey: {}", id, configKey, e);
return ApiResponse.fail("根据键获取设备配置失败: " + e.getMessage());
}
}
/**
* 根据设备编号获取配置列表
*/
@GetMapping("/no/{no}/config")
public ApiResponse<List<DeviceConfigV2DTO>> getDeviceConfigsByNo(@PathVariable String no) {
log.info("根据设备编号获取配置列表, deviceNo: {}", no);
try {
List<DeviceConfigV2DTO> configs = deviceConfigIntegrationService.getDeviceConfigsByNo(no);
return ApiResponse.success(configs);
} catch (Exception e) {
log.error("根据设备编号获取配置列表失败, deviceNo: {}", no, e);
return ApiResponse.fail("根据设备编号获取配置列表失败: " + e.getMessage());
}
}
/**
* 创建设备配置
*/
@PostMapping("/{id}/config")
public ApiResponse<DeviceConfigV2DTO> createDeviceConfig(@PathVariable Long id,
@Valid @RequestBody CreateDeviceConfigRequest request) {
log.info("创建设备配置, deviceId: {}, configKey: {}", id, request.getConfigKey());
try {
DeviceConfigV2DTO config = deviceConfigIntegrationService.createDeviceConfig(id, request);
return ApiResponse.success(config);
} catch (Exception e) {
log.error("创建设备配置失败, deviceId: {}, configKey: {}", id, request.getConfigKey(), e);
return ApiResponse.fail("创建设备配置失败: " + e.getMessage());
}
}
/**
* 批量创建/更新设备配置
*/
@PostMapping("/{id}/config/batch")
public ApiResponse<BatchUpdateResponse> batchUpdateDeviceConfig(@PathVariable Long id,
@Valid @RequestBody BatchDeviceConfigRequest request) {
log.info("批量更新设备配置, deviceId: {}, configs count: {}", id, request.getConfigs().size());
try {
BatchUpdateResponse result = deviceConfigIntegrationService.batchUpdateDeviceConfig(id, request);
return ApiResponse.success(result);
} catch (Exception e) {
log.error("批量更新设备配置失败, deviceId: {}", id, e);
return ApiResponse.fail("批量更新设备配置失败: " + e.getMessage());
}
}
/**
* 更新设备配置
*/
@PutMapping("/{id}/config/{configId}")
public ApiResponse<String> updateDeviceConfig(@PathVariable Long id, @PathVariable Long configId,
@Valid @RequestBody UpdateDeviceConfigRequest request) {
log.info("更新设备配置, deviceId: {}, configId: {}", id, configId);
try {
deviceConfigIntegrationService.updateDeviceConfig(id, configId, request);
return ApiResponse.success("设备配置更新成功");
} catch (Exception e) {
log.error("更新设备配置失败, deviceId: {}, configId: {}", id, configId, e);
return ApiResponse.fail("更新设备配置失败: " + e.getMessage());
}
}
/**
* 删除设备配置
*/
@DeleteMapping("/{id}/config/{configId}")
public ApiResponse<String> deleteDeviceConfig(@PathVariable Long id, @PathVariable Long configId) {
log.info("删除设备配置, deviceId: {}, configId: {}", id, configId);
try {
deviceConfigIntegrationService.deleteDeviceConfig(id, configId);
return ApiResponse.success("设备配置删除成功");
} catch (Exception e) {
log.error("删除设备配置失败, deviceId: {}, configId: {}", id, configId, e);
return ApiResponse.fail("删除设备配置失败: " + e.getMessage());
}
}
// ========== 景区设备管理操作 ==========
/**
* 获取景区所有设备列表
*/
@GetMapping("/scenic/{scenicId}")
public ApiResponse<PageResponse<DeviceV2DTO>> getScenicAllDevices(@PathVariable Long scenicId,
@RequestParam(required = false) String name,
@RequestParam(required = false) String type,
@RequestParam(required = false) String no,
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize) {
log.info("获取景区所有设备列表, scenicId: {}, page: {}, pageSize: {}", scenicId, page, pageSize);
try {
PageResponse<DeviceV2DTO> response = deviceIntegrationService.listDevices(page, pageSize, name, no, type, null, scenicId, null);
return ApiResponse.success(response);
} catch (Exception e) {
log.error("获取景区所有设备列表失败, scenicId: {}", scenicId, e);
return ApiResponse.fail("获取景区所有设备列表失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,176 @@
package com.ycwl.basic.controller.pc;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ycwl.basic.annotation.IgnoreToken;
import com.ycwl.basic.device.entity.common.DeviceVideoContinuityCache;
import com.ycwl.basic.model.pc.device.entity.DeviceEntity;
import com.ycwl.basic.model.pc.device.req.VideoContinuityReportReq;
import com.ycwl.basic.repository.DeviceRepository;
import com.ycwl.basic.utils.ApiResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* 设备视频连续性检查控制器
* 提供查询设备视频连续性检查结果的接口
*
* @author Claude Code
* @date 2025-09-01
*/
@Slf4j
@RestController
@RequestMapping("/api/device/video-continuity")
@RequiredArgsConstructor
public class DeviceVideoContinuityController {
private static final String REDIS_KEY_PREFIX = "device:video:continuity:";
private static final int CACHE_TTL_HOURS = 24; // 缓存24小时
private final RedisTemplate<String, String> redisTemplate;
private final ObjectMapper objectMapper;
private final DeviceRepository deviceRepository;
/**
* 查询设备最近的视频连续性检查结果
*
* @param deviceId 设备ID
* @return 检查结果
*/
@GetMapping("/{deviceId}")
public ApiResponse<DeviceVideoContinuityCache> getDeviceContinuityResult(@PathVariable Long deviceId) {
log.info("查询设备 {} 的视频连续性检查结果", deviceId);
try {
String redisKey = REDIS_KEY_PREFIX + deviceId;
String cacheJson = redisTemplate.opsForValue().get(redisKey);
if (cacheJson == null) {
log.warn("未找到设备 {} 的视频连续性检查结果", deviceId);
return ApiResponse.buildResponse(404, null, "未找到该设备的检查结果,可能设备未配置存储或尚未执行检查");
}
DeviceVideoContinuityCache cache = objectMapper.readValue(cacheJson, DeviceVideoContinuityCache.class);
return ApiResponse.success(cache);
} catch (Exception e) {
log.error("查询设备 {} 视频连续性检查结果失败", deviceId, e);
return ApiResponse.buildResponse(500, null, "查询失败: " + e.getMessage());
}
}
/**
* 手动触发设备视频连续性检查
* 注意:仅用于测试和紧急情况,正常情况下由定时任务自动执行
*
* @param deviceId 设备ID
* @return 检查结果
*/
@PostMapping("/{deviceId}/check")
public ApiResponse<DeviceVideoContinuityCache> manualCheck(@PathVariable Long deviceId) {
log.info("手动触发设备 {} 的视频连续性检查", deviceId);
return ApiResponse.success(null);
}
/**
* 删除设备的视频连续性检查缓存
* 用于清理过期或错误的缓存数据
*
* @param deviceId 设备ID
* @return 删除结果
*/
@DeleteMapping("/{deviceId}")
public ApiResponse<String> deleteContinuityCache(@PathVariable Long deviceId) {
log.info("删除设备 {} 的视频连续性检查缓存", deviceId);
try {
String redisKey = REDIS_KEY_PREFIX + deviceId;
Boolean deleted = redisTemplate.delete(redisKey);
if (deleted != null && deleted) {
return ApiResponse.success("缓存删除成功");
} else {
return ApiResponse.buildResponse(404, null, "未找到该设备的缓存数据");
}
} catch (Exception e) {
log.error("删除设备 {} 视频连续性检查缓存失败", deviceId, e);
return ApiResponse.buildResponse(500, null, "删除失败: " + e.getMessage());
}
}
/**
* 外部工具上报视频连续性检查结果
* 通过设备编号(deviceNo)上报检查结果,无需认证
*
* @param reportReq 上报请求
* @return 上报结果
*/
@PostMapping("/report")
@IgnoreToken
public ApiResponse<DeviceVideoContinuityCache> reportContinuityResult(
@Validated @RequestBody VideoContinuityReportReq reportReq) {
log.info("外部工具上报设备 {} 的视频连续性检查结果", reportReq.getDeviceNo());
try {
// 1. 根据设备编号查询设备ID
DeviceEntity device = deviceRepository.getDeviceByDeviceNo(reportReq.getDeviceNo());
if (device == null) {
log.warn("设备编号 {} 不存在", reportReq.getDeviceNo());
return ApiResponse.buildResponse(404, null, "设备不存在: " + reportReq.getDeviceNo());
}
Long deviceId = device.getId();
// 2. 构建缓存对象
DeviceVideoContinuityCache cache = new DeviceVideoContinuityCache();
cache.setDeviceId(deviceId);
cache.setCheckTime(new Date());
cache.setStartTime(reportReq.getStartTime());
cache.setEndTime(reportReq.getEndTime());
cache.setSupport(reportReq.getSupport());
cache.setContinuous(reportReq.getContinuous());
cache.setTotalVideos(reportReq.getTotalVideos());
cache.setTotalDurationMs(reportReq.getTotalDurationMs());
cache.setMaxAllowedGapMs(reportReq.getMaxAllowedGapMs() != null
? reportReq.getMaxAllowedGapMs() : 2000L);
cache.setGapCount(reportReq.getGaps() != null ? reportReq.getGaps().size() : 0);
// 3. 转换间隙信息
if (reportReq.getGaps() != null && !reportReq.getGaps().isEmpty()) {
List<DeviceVideoContinuityCache.GapInfo> gapInfos = reportReq.getGaps().stream()
.map(gap -> new DeviceVideoContinuityCache.GapInfo(
gap.getBeforeFileName(),
gap.getAfterFileName(),
gap.getGapMs(),
gap.getGapStartTime(),
gap.getGapEndTime()
))
.collect(Collectors.toList());
cache.setGaps(gapInfos);
}
// 4. 存储到Redis
String redisKey = REDIS_KEY_PREFIX + deviceId;
String cacheJson = objectMapper.writeValueAsString(cache);
redisTemplate.opsForValue().set(redisKey, cacheJson, CACHE_TTL_HOURS, TimeUnit.HOURS);
log.info("设备 {} (ID: {}) 视频连续性检查结果上报成功: continuous={}, videos={}, gaps={}",
reportReq.getDeviceNo(), deviceId, cache.getContinuous(),
cache.getTotalVideos(), cache.getGapCount());
return ApiResponse.success(cache);
} catch (Exception e) {
log.error("外部工具上报设备 {} 视频连续性检查结果失败", reportReq.getDeviceNo(), e);
return ApiResponse.buildResponse(500, null, "上报失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,44 @@
package com.ycwl.basic.controller.pc;
import com.github.pagehelper.PageInfo;
import com.ycwl.basic.model.pc.extraDevice.req.ExtraDevicePageQueryReq;
import com.ycwl.basic.model.pc.extraDevice.resp.ExtraDeviceRespVO;
import com.ycwl.basic.service.pc.ExtraDeviceService;
import com.ycwl.basic.utils.ApiResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.RestController;
/**
* 外部设备管理控制器
*/
@Slf4j
@RestController
@RequestMapping("/api/extra_device/v1")
@RequiredArgsConstructor
public class ExtraDeviceController {
private final ExtraDeviceService extraDeviceService;
/**
* 分页查询外部设备列表
*
* @param req 查询请求参数,包含scenicId(可选)、pageNum、pageSize
* @return 分页查询结果,包含设备ID、景区ID、景区名称、设备名称、标识、状态、心跳时间、在线状态
*/
@PostMapping("/page")
public ApiResponse<PageInfo<ExtraDeviceRespVO>> page(@RequestBody ExtraDevicePageQueryReq req) {
log.info("分页查询外部设备列表, scenicId: {}, pageNum: {}, pageSize: {}",
req.getScenicId(), req.getPageNum(), req.getPageSize());
PageInfo<ExtraDeviceRespVO> pageInfo = extraDeviceService.pageQuery(req);
log.info("外部设备列表查询完成, total: {}, pages: {}",
pageInfo.getTotal(), pageInfo.getPages());
return ApiResponse.success(pageInfo);
}
}

View File

@@ -53,5 +53,4 @@ public class FaceController {
return faceService.deleteByIds(ids); return faceService.deleteByIds(ids);
} }
} }

View File

@@ -1,4 +1,5 @@
package com.ycwl.basic.controller.pc; package com.ycwl.basic.controller.pc;
import com.ycwl.basic.integration.kafka.service.FaceProcessingKafkaService;
import com.github.pagehelper.PageInfo; import com.github.pagehelper.PageInfo;
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity; import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
@@ -15,13 +16,14 @@ import java.util.List;
* @Author:longbinbin * @Author:longbinbin
* @Date:2024/12/2 16:33 * @Date:2024/12/2 16:33
*/ */
@Deprecated
@RestController @RestController
@RequestMapping("/api/faceSample/v1") @RequestMapping("/api/faceSample/v1")
// 人脸样本管理 // 人脸样本管理
public class FaceSampleController { public class FaceSampleController {
@Autowired @Autowired
private FaceSampleService FaceSampleService; private FaceSampleService FaceSampleService;
@Autowired(required = false)
private FaceProcessingKafkaService faceProcessingKafkaService;
// 分页查询人脸样本 // 分页查询人脸样本
@PostMapping("/page") @PostMapping("/page")
@@ -39,4 +41,25 @@ public class FaceSampleController {
return FaceSampleService.getById(id); return FaceSampleService.getById(id);
} }
/**
* 重试失败的人脸识别
* 用于手动重试状态为-1的人脸样本
*
* @param id 人脸样本ID
* @return 重试结果
*/
@PostMapping("/retry/{id}")
public ApiResponse<String> retryFaceRecognition(@PathVariable("id") Long id) {
if (faceProcessingKafkaService == null) {
return ApiResponse.fail("Kafka服务未启用,无法重试人脸识别");
}
boolean success = faceProcessingKafkaService.retryFaceRecognition(id);
if (success) {
return ApiResponse.success("人脸识别重试任务已提交");
} else {
return ApiResponse.fail("提交重试任务失败,请检查人脸样本状态");
}
}
} }

View File

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

View File

@@ -1,13 +1,8 @@
package com.ycwl.basic.controller.pc; package com.ycwl.basic.controller.pc;
import com.github.pagehelper.PageHelper; import com.ycwl.basic.biz.PriceBiz;
import com.github.pagehelper.PageInfo;
import com.ycwl.basic.model.pc.price.entity.PriceConfigEntity;
import com.ycwl.basic.model.pc.price.req.PriceConfigListReq;
import com.ycwl.basic.model.pc.price.resp.GoodsListRespVO; import com.ycwl.basic.model.pc.price.resp.GoodsListRespVO;
import com.ycwl.basic.model.pc.price.resp.PriceConfigRespVO; import com.ycwl.basic.model.pc.price.resp.SimpleGoodsRespVO;
import com.ycwl.basic.repository.PriceRepository;
import com.ycwl.basic.service.pc.PriceConfigService;
import com.ycwl.basic.utils.ApiResponse; import com.ycwl.basic.utils.ApiResponse;
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.*;
@@ -19,61 +14,13 @@ import java.util.List;
public class PriceConfigController { public class PriceConfigController {
@Autowired @Autowired
private PriceConfigService priceConfigService; private PriceBiz priceBiz;
@Autowired
private PriceRepository priceRepository;
@GetMapping("/goodsList") @GetMapping("/goodsList")
public ApiResponse<List<GoodsListRespVO>> goodsList(@RequestParam Long scenicId) { public ApiResponse<List<SimpleGoodsRespVO>> goodsList(
return ApiResponse.success(priceConfigService.listGoodsByScenic(scenicId)); @RequestParam Long scenicId,
@RequestParam(required = false) String productType) {
return ApiResponse.success(priceBiz.listSimpleGoodsByScenic(scenicId, productType));
} }
@PostMapping("/add")
public ApiResponse<PriceConfigEntity> addPriceConfig(@RequestBody PriceConfigEntity priceConfig) {
priceConfig.setId(null);
priceConfigService.save(priceConfig);
return ApiResponse.success(priceConfig);
}
@PostMapping("/update")
public ApiResponse<PriceConfigEntity> updatePriceConfig(@RequestBody PriceConfigEntity priceConfig) {
priceRepository.clearPriceCache(priceConfig.getId());
priceConfigService.updateById(priceConfig);
priceRepository.clearPriceCache(priceConfig.getId());
return ApiResponse.success(priceConfig);
}
@DeleteMapping("/delete/{id}")
public ApiResponse<Boolean> deletePriceConfig(@PathVariable Integer id) {
priceRepository.clearPriceCache(id);
priceConfigService.removeById(id);
priceRepository.clearPriceCache(id);
return ApiResponse.success(true);
}
@PostMapping("/{id}/status")
public ApiResponse<Boolean> updateStatus(@PathVariable Integer id) {
priceRepository.clearPriceCache(id);
priceConfigService.updateStatus(id);
priceRepository.clearPriceCache(id);
return ApiResponse.success(true);
}
@GetMapping("/{id}")
public ApiResponse<PriceConfigRespVO> getPriceConfigById(@PathVariable Integer id) {
PriceConfigRespVO config = priceConfigService.findById(id);
priceConfigService.fillGoodsName(config);
return ApiResponse.success(config);
}
@GetMapping("/list")
public ApiResponse<PageInfo<PriceConfigRespVO>> list(@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "10") int pageSize,
@ModelAttribute PriceConfigListReq req) {
PageHelper.startPage(pageNum, pageSize);
List<PriceConfigRespVO> result = priceConfigService.listByCondition(req);
priceConfigService.fillGoodsName(result);
PageInfo<PriceConfigRespVO> pageInfo = new PageInfo<>(result);
return ApiResponse.success(pageInfo);
}
} }

View File

@@ -1,6 +1,12 @@
package com.ycwl.basic.controller.pc; package com.ycwl.basic.controller.pc;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.ycwl.basic.mapper.PrintTaskMapper;
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.req.PrintTaskReqQuery;
import com.ycwl.basic.model.pc.printer.req.ReprintRequest;
import com.ycwl.basic.service.printer.PrinterService; import com.ycwl.basic.service.printer.PrinterService;
import com.ycwl.basic.utils.ApiResponse; import com.ycwl.basic.utils.ApiResponse;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -20,6 +26,9 @@ public class PrinterController {
@Autowired @Autowired
private PrinterService printerService; private PrinterService printerService;
@Autowired
private PrintTaskMapper printTaskMapper;
// 查询列表 // 查询列表
@PostMapping("/list") @PostMapping("/list")
public ApiResponse<List<PrinterEntity>> list(@RequestBody PrinterEntity condition) { public ApiResponse<List<PrinterEntity>> list(@RequestBody PrinterEntity condition) {
@@ -49,4 +58,65 @@ public class PrinterController {
public ApiResponse<Integer> delete(@PathVariable("id") Integer id) { public ApiResponse<Integer> delete(@PathVariable("id") Integer id) {
return printerService.delete(id); return printerService.delete(id);
} }
// 分页查询打印任务
@PostMapping("/task/page")
public ApiResponse<PageInfo<PrintTaskEntity>> taskPage(@RequestBody PrintTaskReqQuery req) {
PageHelper.startPage(req.getPageNum(), req.getPageSize());
List<PrintTaskEntity> list = printTaskMapper.queryByCondition(req.getPrinterId(), req.getStatus());
PageInfo<PrintTaskEntity> pageInfo = new PageInfo<>(list);
return ApiResponse.success(pageInfo);
}
// 重新打印(将状态设置为0-未开始,并更新打印机名称)
@PostMapping("/task/reprint/{id}")
public ApiResponse<Integer> reprint(@PathVariable("id") Integer id, @RequestBody ReprintRequest request) {
int result = printerService.handleReprint(id, request);
return ApiResponse.success(result);
}
/**
* 查询待审核的打印任务
* @param printerId 打印机ID(可选)
* @return 待审核任务列表
*/
@GetMapping("/task/pending-review")
public ApiResponse<List<PrintTaskEntity>> getPendingReviewTasks(Integer printerId) {
List<PrintTaskEntity> tasks = printerService.getPendingReviewTasks(printerId);
return ApiResponse.success(tasks);
}
/**
* 更新待审核任务的URL(重新处理水印等)
* @param taskId 任务ID
* @param url 新的打印URL
* @return 操作结果
*/
@PostMapping("/task/{taskId}/url")
public ApiResponse<Boolean> updateTaskUrl(@PathVariable("taskId") Integer taskId, @RequestBody String url) {
boolean success = printerService.updatePendingReviewTaskUrl(taskId, url);
return ApiResponse.success(success);
}
/**
* 批准待审核任务,下发到打印队列
* @param taskIds 任务ID列表
* @return 成功数量
*/
@PostMapping("/task/approve")
public ApiResponse<Integer> approveTasks(@RequestBody List<Integer> taskIds) {
int count = printerService.approvePrintTasks(taskIds);
return ApiResponse.success(count);
}
/**
* 拒绝待审核任务
* @param taskIds 任务ID列表
* @return 成功数量
*/
@PostMapping("/task/reject")
public ApiResponse<Integer> rejectTasks(@RequestBody List<Integer> taskIds) {
int count = printerService.rejectPrintTasks(taskIds);
return ApiResponse.success(count);
}
} }

View File

@@ -0,0 +1,131 @@
package com.ycwl.basic.controller.pc;
import com.ycwl.basic.model.pc.printer.entity.PrinterEntity;
import com.ycwl.basic.model.pc.printer.req.PrinterPreferredSizeUpdateReq;
import com.ycwl.basic.model.pc.printer.req.PrinterStatusUpdateReq;
import com.ycwl.basic.model.pc.printer.req.PrinterUsePrinterUpdateReq;
import com.ycwl.basic.service.printer.PrinterService;
import com.ycwl.basic.utils.ApiConst;
import com.ycwl.basic.utils.ApiResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 打印机管理接口
*/
@RestController
@RequestMapping("/api/pc/printers/v1")
@RequiredArgsConstructor
public class PrinterManageController {
private final PrinterService printerService;
/**
* 打印机列表查询
*/
@GetMapping
public ApiResponse<List<PrinterEntity>> list(@RequestParam(value = "scenicId", required = false) Long scenicId,
@RequestParam(value = "status", required = false) Integer status,
@RequestParam(value = "name", required = false) String name) {
PrinterEntity condition = new PrinterEntity();
condition.setScenicId(scenicId);
condition.setStatus(status);
condition.setName(name);
return printerService.list(condition);
}
/**
* 打印机详情
*/
@GetMapping("/{id}")
public ApiResponse<PrinterEntity> detail(@PathVariable("id") Integer id) {
ApiResponse<PrinterEntity> response = printerService.get(id);
if (response.getData() == null) {
return ApiResponse.buildResponse(ApiConst.Code.CODE_NOT_EXIST, "打印机不存在");
}
return response;
}
/**
* 新增打印机
*/
@PostMapping
public ApiResponse<Integer> create(@RequestBody PrinterEntity request) {
request.setId(null);
return printerService.add(request);
}
/**
* 更新打印机信息
*/
@PutMapping("/{id}")
public ApiResponse<Integer> update(@PathVariable("id") Integer id, @RequestBody PrinterEntity request) {
request.setId(id);
return printerService.update(request);
}
/**
* 更新打印机状态
*/
@PatchMapping("/{id}/status")
public ApiResponse<Integer> updateStatus(@PathVariable("id") Integer id,
@RequestBody PrinterStatusUpdateReq req) {
if (req == null || req.getStatus() == null) {
return ApiResponse.buildResponse(ApiConst.Code.CODE_PARAM_ERROR, "状态不能为空");
}
PrinterEntity entity = new PrinterEntity();
entity.setId(id);
entity.setStatus(req.getStatus());
return printerService.update(entity);
}
/**
* 更新打印机首选尺寸
*/
@PatchMapping("/{id}/preferred-size")
public ApiResponse<Integer> updatePreferredSize(@PathVariable("id") Integer id,
@RequestBody PrinterPreferredSizeUpdateReq req) {
if (req == null || (req.getPreferW() == null && req.getPreferH() == null)) {
return ApiResponse.buildResponse(ApiConst.Code.CODE_PARAM_ERROR, "首选尺寸不能为空");
}
PrinterEntity entity = new PrinterEntity();
entity.setId(id);
entity.setPreferW(req.getPreferW());
entity.setPreferH(req.getPreferH());
return printerService.update(entity);
}
/**
* 更新当前使用的打印机
*/
@PatchMapping("/{id}/use-printer")
public ApiResponse<Integer> updateUsePrinter(@PathVariable("id") Integer id,
@RequestBody PrinterUsePrinterUpdateReq req) {
if (req == null) {
return ApiResponse.buildResponse(ApiConst.Code.CODE_PARAM_ERROR, "请求参数不能为空");
}
PrinterEntity entity = new PrinterEntity();
entity.setId(id);
entity.setUsePrinter(req.getUsePrinter());
return printerService.update(entity);
}
/**
* 删除打印机
*/
@DeleteMapping("/{id}")
public ApiResponse<Integer> delete(@PathVariable("id") Integer id) {
return printerService.delete(id);
}
}

View File

@@ -0,0 +1,257 @@
package com.ycwl.basic.controller.pc;
import com.ycwl.basic.integration.profitshare.dto.CalculateResultVO;
import com.ycwl.basic.integration.profitshare.dto.CalculateShareRequest;
import com.ycwl.basic.integration.profitshare.dto.ManualShareRequest;
import com.ycwl.basic.integration.profitshare.dto.TypesVO;
import com.ycwl.basic.integration.profitshare.dto.record.RecordDetailVO;
import com.ycwl.basic.integration.profitshare.dto.record.RecordVO;
import com.ycwl.basic.integration.profitshare.dto.rule.CreateRuleRequest;
import com.ycwl.basic.integration.profitshare.dto.rule.RuleVO;
import com.ycwl.basic.integration.profitshare.service.ProfitShareIntegrationService;
import com.ycwl.basic.integration.common.response.PageResponse;
import com.ycwl.basic.utils.ApiResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
/**
* 分账管理 V2 版本控制器 - 基于 zt-profitshare 集成服务
*
* @author Claude Code
* @date 2025-01-11
*/
@Slf4j
@RestController
@RequestMapping("/api/profit-share/v2")
@RequiredArgsConstructor
public class ProfitShareV2Controller {
private final ProfitShareIntegrationService profitShareIntegrationService;
// ========== 分账规则管理 ==========
/**
* 创建分账规则
*/
@PostMapping("/rules")
public ApiResponse<RuleVO> createRule(@Valid @RequestBody CreateRuleRequest request) {
log.info("创建分账规则, scenicId: {}, ruleName: {}, ruleType: {}",
request.getScenicId(), request.getRuleName(), request.getRuleType());
try {
RuleVO rule = profitShareIntegrationService.createRule(request);
return ApiResponse.success(rule);
} catch (Exception e) {
log.error("创建分账规则失败", e);
return ApiResponse.fail("创建分账规则失败: " + e.getMessage());
}
}
/**
* 查询分账规则列表
*/
@GetMapping("/rules")
public ApiResponse<PageResponse<RuleVO>> listRules(
@RequestParam(required = false) Long scenicId,
@RequestParam(required = false) String status,
@RequestParam(required = false) String ruleType,
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize) {
log.info("查询分账规则列表, scenicId: {}, status: {}, ruleType: {}, page: {}, pageSize: {}",
scenicId, status, ruleType, page, pageSize);
// 参数验证:限制pageSize最大值为100
if (pageSize > 100) {
pageSize = 100;
}
try {
PageResponse<RuleVO> response = profitShareIntegrationService.listRules(scenicId, status, ruleType, page, pageSize);
return ApiResponse.success(response);
} catch (Exception e) {
log.error("查询分账规则列表失败", e);
return ApiResponse.fail("查询分账规则列表失败: " + e.getMessage());
}
}
/**
* 获取分账规则详情
*/
@GetMapping("/rules/{id}")
public ApiResponse<RuleVO> getRule(@PathVariable Long id) {
log.info("获取分账规则详情, id: {}", id);
try {
RuleVO rule = profitShareIntegrationService.getRule(id);
return ApiResponse.success(rule);
} catch (Exception e) {
log.error("获取分账规则详情失败, id: {}", id, e);
return ApiResponse.fail("获取分账规则详情失败: " + e.getMessage());
}
}
/**
* 更新分账规则
*/
@PutMapping("/rules/{id}")
public ApiResponse<RuleVO> updateRule(@PathVariable Long id, @Valid @RequestBody CreateRuleRequest request) {
log.info("更新分账规则, id: {}", id);
try {
RuleVO rule = profitShareIntegrationService.updateRule(id, request);
return ApiResponse.success(rule);
} catch (Exception e) {
log.error("更新分账规则失败, id: {}", id, e);
return ApiResponse.fail("更新分账规则失败: " + e.getMessage());
}
}
/**
* 启用分账规则
*/
@PutMapping("/rules/{id}/enable")
public ApiResponse<String> enableRule(@PathVariable Long id) {
log.info("启用分账规则, id: {}", id);
try {
profitShareIntegrationService.enableRule(id);
return ApiResponse.success("规则已启用");
} catch (Exception e) {
log.error("启用分账规则失败, id: {}", id, e);
return ApiResponse.fail("启用分账规则失败: " + e.getMessage());
}
}
/**
* 禁用分账规则
*/
@PutMapping("/rules/{id}/disable")
public ApiResponse<String> disableRule(@PathVariable Long id) {
log.info("禁用分账规则, id: {}", id);
try {
profitShareIntegrationService.disableRule(id);
return ApiResponse.success("规则已禁用");
} catch (Exception e) {
log.error("禁用分账规则失败, id: {}", id, e);
return ApiResponse.fail("禁用分账规则失败: " + e.getMessage());
}
}
/**
* 删除分账规则
*/
@DeleteMapping("/rules/{id}")
public ApiResponse<String> deleteRule(@PathVariable Long id) {
log.info("删除分账规则, id: {}", id);
try {
profitShareIntegrationService.deleteRule(id);
return ApiResponse.success("规则已删除");
} catch (Exception e) {
log.error("删除分账规则失败, id: {}", id, e);
return ApiResponse.fail("删除分账规则失败: " + e.getMessage());
}
}
// ========== 分账记录查询 ==========
/**
* 查询景区分账记录
*/
@GetMapping("/records/scenic/{scenicId}")
public ApiResponse<PageResponse<RecordVO>> getRecordsByScenic(
@PathVariable Long scenicId,
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize) {
log.info("查询景区分账记录, scenicId: {}, page: {}, pageSize: {}", scenicId, page, pageSize);
// 参数验证:限制pageSize最大值为100
if (pageSize > 100) {
pageSize = 100;
}
try {
PageResponse<RecordVO> response = profitShareIntegrationService.getRecordsByScenic(scenicId, page, pageSize);
return ApiResponse.success(response);
} catch (Exception e) {
log.error("查询景区分账记录失败, scenicId: {}", scenicId, e);
return ApiResponse.fail("查询景区分账记录失败: " + e.getMessage());
}
}
/**
* 查询分账记录详情
*/
@GetMapping("/records/{id}")
public ApiResponse<RecordDetailVO> getRecordById(@PathVariable Long id) {
log.info("查询分账记录详情, id: {}", id);
try {
RecordDetailVO record = profitShareIntegrationService.getRecordById(id);
return ApiResponse.success(record);
} catch (Exception e) {
log.error("查询分账记录详情失败, id: {}", id, e);
return ApiResponse.fail("查询分账记录详情失败: " + e.getMessage());
}
}
/**
* 按订单ID查询分账记录
*/
@GetMapping("/records/order/{orderId}")
public ApiResponse<RecordDetailVO> getRecordByOrderId(@PathVariable String orderId) {
log.info("按订单ID查询分账记录, orderId: {}", orderId);
try {
RecordDetailVO record = profitShareIntegrationService.getRecordByOrderId(orderId);
return ApiResponse.success(record);
} catch (Exception e) {
log.error("按订单ID查询分账记录失败, orderId: {}", orderId, e);
return ApiResponse.fail("按订单ID查询分账记录失败: " + e.getMessage());
}
}
// ========== 手动分账与计算 ==========
/**
* 手动触发分账
*/
@PostMapping("/manual")
public ApiResponse<String> manualShare(@Valid @RequestBody ManualShareRequest request) {
log.info("手动触发分账, orderId: {}", request.getOrderId());
try {
profitShareIntegrationService.manualShare(request.getOrderId());
return ApiResponse.success("手动分账触发成功");
} catch (Exception e) {
log.error("手动触发分账失败, orderId: {}", request.getOrderId(), e);
return ApiResponse.fail("手动触发分账失败: " + e.getMessage());
}
}
/**
* 计算分账结果(不执行)
*/
@PostMapping("/calculate")
public ApiResponse<CalculateResultVO> calculateShare(@Valid @RequestBody CalculateShareRequest request) {
log.info("计算分账结果, scenicId: {}, totalAmount: {}", request.getScenicId(), request.getTotalAmount());
try {
CalculateResultVO result = profitShareIntegrationService.calculateShare(request);
return ApiResponse.success(result);
} catch (Exception e) {
log.error("计算分账结果失败", e);
return ApiResponse.fail("计算分账结果失败: " + e.getMessage());
}
}
// ========== 类型查询 ==========
/**
* 获取支持的类型列表
*/
@GetMapping("/types")
public ApiResponse<TypesVO> getSupportedTypes() {
log.info("获取支持的类型列表");
try {
TypesVO types = profitShareIntegrationService.getSupportedTypes();
return ApiResponse.success(types);
} catch (Exception e) {
log.error("获取支持的类型列表失败", e);
return ApiResponse.fail("获取支持的类型列表失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,102 @@
package com.ycwl.basic.controller.pc;
import com.ycwl.basic.model.pc.project.entity.ProjectEntity;
import com.ycwl.basic.model.pc.project.req.ProjectReqQuery;
import com.ycwl.basic.model.pc.project.resp.ProjectRespVO;
import com.ycwl.basic.service.pc.ProjectService;
import com.ycwl.basic.storage.enums.StorageAcl;
import com.ycwl.basic.utils.ApiResponse;
import com.ycwl.basic.utils.WxMpUtil;
import com.ycwl.basic.repository.ScenicRepository;
import com.ycwl.basic.storage.StorageFactory;
import com.ycwl.basic.storage.adapters.IStorageAdapter;
import com.ycwl.basic.model.pc.mp.MpConfigEntity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.io.File;
/**
* 景区项目管理控制器
*
* @Author: Claude
* @Date: 2025-01-15
*/
@RestController
@RequestMapping("/api/project/v1")
public class ProjectController {
@Autowired
private ProjectService projectService;
@Autowired
private ScenicRepository scenicRepository;
// 分页查询
@PostMapping("/page")
public ApiResponse page(@RequestBody ProjectReqQuery projectReqQuery) {
return ApiResponse.success(projectService.pageQuery(projectReqQuery));
}
// 列表查询
@PostMapping("/list")
public ApiResponse list(@RequestBody ProjectReqQuery projectReqQuery) {
return ApiResponse.success(projectService.list(projectReqQuery));
}
// 详情查询
@GetMapping("/getDetails/{id}")
public ApiResponse getDetails(@PathVariable("id") Long id) {
return ApiResponse.success(projectService.getById(id));
}
// 新增或修改
@PostMapping("/addOrUpdate")
public ApiResponse addOrUpdate(@RequestBody ProjectEntity project) {
return ApiResponse.success(projectService.addOrUpdate(project));
}
// 删除
@DeleteMapping("/delete/{id}")
public ApiResponse delete(@PathVariable("id") Long id) {
return ApiResponse.success(projectService.delete(id));
}
// 修改状态
@PutMapping("/updateStatus/{id}")
public ApiResponse updateStatus(@PathVariable("id") Long id) {
return ApiResponse.success(projectService.updateStatus(id));
}
// 根据项目ID下载小程序二维码
@GetMapping("/{id}/QRCode")
public ApiResponse<String> downloadQrCode(@PathVariable Long id) {
ProjectRespVO project = projectService.getById(id);
if (project == null) {
return ApiResponse.fail("项目不存在");
}
MpConfigEntity mpConfig = scenicRepository.getScenicMpConfig(project.getScenicId());
if (mpConfig == null) {
return ApiResponse.fail("小程序配置不存在");
}
String appId = mpConfig.getAppId();
String appSecret = mpConfig.getAppSecret();
String appState = mpConfig.getState();
String path = "pages/home/index?scenicId=" + project.getScenicId() + "&projectId=" + id;
String filePath = "qr_code_project_" + id + ".jpg";
IStorageAdapter adapter = StorageFactory.use();
if (adapter.isExists(filePath)) {
return ApiResponse.success(adapter.getUrl(filePath));
}
try {
WxMpUtil.generateWXAQRCode(appId, appSecret, appState, path, filePath);
File file = new File(filePath);
String s = adapter.uploadFile(null, file, filePath);
file.delete();
adapter.setAcl(StorageAcl.PUBLIC_READ, filePath);
return ApiResponse.success(s);
} catch (Exception e) {
return ApiResponse.fail("生成二维码失败");
}
}
}

View File

@@ -0,0 +1,313 @@
package com.ycwl.basic.controller.pc;
import com.ycwl.basic.integration.questionnaire.dto.answer.ResponseDetailResponse;
import com.ycwl.basic.integration.questionnaire.dto.questionnaire.CreateQuestionnaireRequest;
import com.ycwl.basic.integration.questionnaire.dto.questionnaire.QuestionnaireResponse;
import com.ycwl.basic.integration.questionnaire.dto.statistics.QuestionnaireStatistics;
import com.ycwl.basic.integration.questionnaire.service.QuestionnaireIntegrationService;
import com.ycwl.basic.integration.common.response.PageResponse;
import com.ycwl.basic.model.pc.mp.MpConfigEntity;
import com.ycwl.basic.repository.ScenicRepository;
import com.ycwl.basic.storage.StorageFactory;
import com.ycwl.basic.storage.adapters.IStorageAdapter;
import com.ycwl.basic.storage.enums.StorageAcl;
import com.ycwl.basic.utils.ApiResponse;
import com.ycwl.basic.utils.WxMpUtil;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.io.File;
import java.util.Map;
/**
* 问卷管理 V2 版本控制器 - 基于 zt-questionnaire 集成服务
*
* @author Claude Code
* @date 2025-09-05
*/
@Slf4j
@RestController
@RequestMapping("/api/questionnaire/v2")
@RequiredArgsConstructor
public class QuestionnaireV2Controller {
private final QuestionnaireIntegrationService questionnaireIntegrationService;
private final ScenicRepository scenicRepository;
// ========== 问卷管理 CRUD 操作 ==========
/**
* 分页查询问卷列表
*/
@GetMapping("/")
public ApiResponse<PageResponse<QuestionnaireResponse>> listQuestionnaires(
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) Integer status,
@RequestParam(required = false) String name) {
log.info("分页查询问卷列表, page: {}, pageSize: {}, status: {}, name: {}",
page, pageSize, status, name);
// 参数验证:限制pageSize最大值为100
if (pageSize > 100) {
pageSize = 100;
}
try {
PageResponse<QuestionnaireResponse> response =
questionnaireIntegrationService.getQuestionnaireList(page, pageSize, name, status, null);
return ApiResponse.success(response);
} catch (Exception e) {
log.error("分页查询问卷列表失败", e);
return ApiResponse.fail("分页查询问卷列表失败: " + e.getMessage());
}
}
/**
* 获取问卷详情
*/
@GetMapping("/{id}")
public ApiResponse<QuestionnaireResponse> getQuestionnaire(@PathVariable Long id) {
log.info("获取问卷详情, id: {}", id);
try {
QuestionnaireResponse questionnaire = questionnaireIntegrationService.getQuestionnaire(id);
return ApiResponse.success(questionnaire);
} catch (Exception e) {
log.error("获取问卷详情失败, id: {}", id, e);
return ApiResponse.fail("获取问卷详情失败: " + e.getMessage());
}
}
/**
* 创建问卷
*/
@PostMapping("/")
public ApiResponse<QuestionnaireResponse> createQuestionnaire(@Valid @RequestBody CreateQuestionnaireRequest request) {
log.info("创建问卷, name: {}, questions count: {}",
request.getName(), request.getQuestions() != null ? request.getQuestions().size() : 0);
try {
QuestionnaireResponse questionnaire = questionnaireIntegrationService.createQuestionnaire(request, "admin");
return ApiResponse.success(questionnaire);
} catch (Exception e) {
log.error("创建问卷失败", e);
return ApiResponse.fail("创建问卷失败: " + e.getMessage());
}
}
/**
* 更新问卷
*/
@PutMapping("/{id}")
public ApiResponse<QuestionnaireResponse> updateQuestionnaire(
@PathVariable Long id,
@Valid @RequestBody CreateQuestionnaireRequest request) {
log.info("更新问卷, id: {}", id);
try {
QuestionnaireResponse questionnaire = questionnaireIntegrationService.updateQuestionnaire(id, request, "admin");
return ApiResponse.success(questionnaire);
} catch (Exception e) {
log.error("更新问卷失败, id: {}", id, e);
return ApiResponse.fail("更新问卷失败: " + e.getMessage());
}
}
/**
* 更新问卷状态
*/
@PutMapping("/{id}/status")
public ApiResponse<String> updateQuestionnaireStatus(@PathVariable Long id, @RequestBody Map<String, Integer> request) {
Integer status = request.get("status");
log.info("更新问卷状态, id: {}, status: {}", id, status);
try {
// 根据状态调用不同的方法
if (status == 2) {
questionnaireIntegrationService.publishQuestionnaire(id, "admin");
} else if (status == 3) {
questionnaireIntegrationService.stopQuestionnaire(id, "admin");
}
return ApiResponse.success("问卷状态更新成功");
} catch (Exception e) {
log.error("更新问卷状态失败, id: {}, status: {}", id, status, e);
return ApiResponse.fail("更新问卷状态失败: " + e.getMessage());
}
}
/**
* 发布问卷
*/
@PutMapping("/{id}/publish")
public ApiResponse<String> publishQuestionnaire(@PathVariable Long id) {
log.info("发布问卷, id: {}", id);
try {
questionnaireIntegrationService.publishQuestionnaire(id, "admin");
return ApiResponse.success("问卷发布成功");
} catch (Exception e) {
log.error("发布问卷失败, id: {}", id, e);
return ApiResponse.fail("发布问卷失败: " + e.getMessage());
}
}
/**
* 停止问卷
*/
@PutMapping("/{id}/stop")
public ApiResponse<String> stopQuestionnaire(@PathVariable Long id) {
log.info("停止问卷, id: {}", id);
try {
questionnaireIntegrationService.stopQuestionnaire(id, "admin");
return ApiResponse.success("问卷停止成功");
} catch (Exception e) {
log.error("停止问卷失败, id: {}", id, e);
return ApiResponse.fail("停止问卷失败: " + e.getMessage());
}
}
/**
* 删除问卷
*/
@DeleteMapping("/{id}")
public ApiResponse<String> deleteQuestionnaire(@PathVariable Long id) {
log.info("删除问卷, id: {}", id);
try {
questionnaireIntegrationService.deleteQuestionnaire(id, "admin");
return ApiResponse.success("问卷删除成功");
} catch (Exception e) {
log.error("删除问卷失败, id: {}", id, e);
return ApiResponse.fail("删除问卷失败: " + e.getMessage());
}
}
// ========== 问卷答案查看操作 ==========
/**
* 分页查询问卷答案
*/
@GetMapping("/{id}/answers")
public ApiResponse<PageResponse<ResponseDetailResponse>> getQuestionnaireAnswers(
@PathVariable Long id,
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) String userId,
@RequestParam(required = false) String startTime,
@RequestParam(required = false) String endTime) {
log.info("分页查询问卷答案, questionnaireId: {}, page: {}, pageSize: {}, userId: {}",
id, page, pageSize, userId);
// 参数验证:限制pageSize最大值为100
if (pageSize > 100) {
pageSize = 100;
}
try {
PageResponse<ResponseDetailResponse> response =
questionnaireIntegrationService.getResponseList(page, pageSize, id, userId, startTime, endTime);
return ApiResponse.success(response);
} catch (Exception e) {
log.error("分页查询问卷答案失败, questionnaireId: {}", id, e);
return ApiResponse.fail("分页查询问卷答案失败: " + e.getMessage());
}
}
/**
* 获取特定答案详情
*/
@GetMapping("/{id}/answers/{answerId}")
public ApiResponse<ResponseDetailResponse> getQuestionnaireAnswer(@PathVariable Long id, @PathVariable Long answerId) {
log.info("获取问卷答案详情, questionnaireId: {}, answerId: {}", id, answerId);
try {
ResponseDetailResponse answer = questionnaireIntegrationService.getResponseDetail(answerId);
return ApiResponse.success(answer);
} catch (Exception e) {
log.error("获取问卷答案详情失败, questionnaireId: {}, answerId: {}", id, answerId, e);
return ApiResponse.fail("获取问卷答案详情失败: " + e.getMessage());
}
}
/**
* 查询用户答题记录
*/
@GetMapping("/answers/user/{userId}")
public ApiResponse<PageResponse<ResponseDetailResponse>> getUserAnswers(
@PathVariable String userId,
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) Long questionnaireId) {
log.info("查询用户答题记录, userId: {}, page: {}, pageSize: {}, questionnaireId: {}",
userId, page, pageSize, questionnaireId);
// 参数验证:限制pageSize最大值为100
if (pageSize > 100) {
pageSize = 100;
}
try {
PageResponse<ResponseDetailResponse> response =
questionnaireIntegrationService.getResponseList(page, pageSize, questionnaireId, userId, null, null);
return ApiResponse.success(response);
} catch (Exception e) {
log.error("查询用户答题记录失败, userId: {}", userId, e);
return ApiResponse.fail("查询用户答题记录失败: " + e.getMessage());
}
}
// ========== 统计功能 ==========
/**
* 获取问卷统计信息
*/
@GetMapping("/{id}/statistics")
public ApiResponse<QuestionnaireStatistics> getQuestionnaireStatistics(@PathVariable Long id) {
log.info("获取问卷统计信息, id: {}", id);
try {
QuestionnaireStatistics statistics = questionnaireIntegrationService.getStatistics(id);
return ApiResponse.success(statistics);
} catch (Exception e) {
log.error("获取问卷统计信息失败, id: {}", id, e);
return ApiResponse.fail("获取问卷统计信息失败: " + e.getMessage());
}
}
/**
* 下载问卷小程序二维码
*/
@GetMapping("/{id}/QRCode")
public ApiResponse<String> downloadQrCode(@PathVariable Long id) {
log.info("下载问卷小程序二维码, id: {}", id);
try {
// 获取问卷详情
QuestionnaireResponse questionnaire = questionnaireIntegrationService.getQuestionnaire(id);
if (questionnaire == null) {
return ApiResponse.fail("问卷不存在");
}
MpConfigEntity mpConfig = scenicRepository.getScenicMpConfig(3930324797233434624L);
if (mpConfig == null) {
return ApiResponse.fail("小程序配置不存在");
}
String appId = mpConfig.getAppId();
String appSecret = mpConfig.getAppSecret();
String appState = mpConfig.getState();
String path = "pages/questionnaire/index?id=" + id;
String filePath = "qr_code_questionnaire_" + id + ".jpg";
IStorageAdapter adapter = StorageFactory.use();
if (adapter.isExists(filePath)) {
return ApiResponse.success(adapter.getUrl(filePath));
}
WxMpUtil.generateWXAQRCode(appId, appSecret, appState, path, filePath);
File file = new File(filePath);
String s = adapter.uploadFile(null, file, filePath);
file.delete();
adapter.setAcl(StorageAcl.PUBLIC_READ, filePath);
return ApiResponse.success(s);
} catch (Exception e) {
log.error("生成问卷二维码失败, id: {}", id, e);
return ApiResponse.fail("生成二维码失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,196 @@
package com.ycwl.basic.controller.pc;
import com.ycwl.basic.integration.render.dto.config.BatchRenderWorkerConfigRequest;
import com.ycwl.basic.integration.render.dto.config.RenderWorkerConfigV2DTO;
import com.ycwl.basic.integration.render.service.RenderWorkerConfigIntegrationService;
import com.ycwl.basic.utils.ApiResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 渲染工作器配置管理 V2 版本控制器
* 基于 zt-render-worker 微服务标准接口实现
*
* @author Claude Code
* @date 2025-09-06
*/
@Slf4j
@RestController
@RequestMapping("/api/render/worker/config/v2")
@RequiredArgsConstructor
public class RenderWorkerConfigV2Controller {
private final RenderWorkerConfigIntegrationService configIntegrationService;
/**
* 获取工作器所有配置
*
* @param workerId 工作器ID
* @return 工作器配置列表
*/
@GetMapping("/{workerId}")
public ApiResponse<List<RenderWorkerConfigV2DTO>> getWorkerConfigs(@PathVariable Long workerId) {
log.info("获取渲染工作器配置列表, workerId: {}", workerId);
try {
List<RenderWorkerConfigV2DTO> configs = configIntegrationService.getWorkerConfigs(workerId);
return ApiResponse.success(configs);
} catch (Exception e) {
log.error("获取渲染工作器配置列表失败, workerId: {}", workerId, e);
return ApiResponse.fail("获取渲染工作器配置列表失败: " + e.getMessage());
}
}
/**
* 获取工作器平铺配置
*
* @param workerId 工作器ID
* @return 平铺配置Map
*/
@GetMapping("/{workerId}/flat")
public ApiResponse<Map<String, Object>> getWorkerFlatConfig(@PathVariable Long workerId) {
log.info("获取渲染工作器平铺配置, workerId: {}", workerId);
try {
Map<String, Object> flatConfig = configIntegrationService.getWorkerFlatConfig(workerId);
return ApiResponse.success(flatConfig);
} catch (Exception e) {
log.error("获取渲染工作器平铺配置失败, workerId: {}", workerId, e);
return ApiResponse.fail("获取渲染工作器平铺配置失败: " + e.getMessage());
}
}
/**
* 根据配置键获取特定配置
*
* @param workerId 工作器ID
* @param configKey 配置键
* @return 配置信息
*/
@GetMapping("/{workerId}/key/{configKey}")
public ApiResponse<RenderWorkerConfigV2DTO> getWorkerConfigByKey(@PathVariable Long workerId,
@PathVariable String configKey) {
log.info("根据配置键获取渲染工作器配置, workerId: {}, configKey: {}", workerId, configKey);
try {
RenderWorkerConfigV2DTO config = configIntegrationService.getWorkerConfigByKey(workerId, configKey);
return ApiResponse.success(config);
} catch (Exception e) {
log.error("根据配置键获取渲染工作器配置失败, workerId: {}, configKey: {}", workerId, configKey, e);
return ApiResponse.fail("根据配置键获取渲染工作器配置失败: " + e.getMessage());
}
}
/**
* 创建工作器配置
*
* @param workerId 工作器ID
* @param config 配置信息
* @return 创建的配置信息
*/
@PostMapping("/{workerId}")
public ApiResponse<RenderWorkerConfigV2DTO> createWorkerConfig(@PathVariable Long workerId,
@Valid @RequestBody RenderWorkerConfigV2DTO config) {
log.info("创建渲染工作器配置, workerId: {}, configKey: {}", workerId, config.getConfigKey());
try {
RenderWorkerConfigV2DTO result = configIntegrationService.createWorkerConfig(workerId, config);
return ApiResponse.success(result);
} catch (Exception e) {
log.error("创建渲染工作器配置失败, workerId: {}", workerId, e);
return ApiResponse.fail("创建渲染工作器配置失败: " + e.getMessage());
}
}
/**
* 更新工作器配置
*
* @param workerId 工作器ID
* @param configId 配置ID
* @param updates 更新内容
* @return 操作结果
*/
@PutMapping("/{workerId}/{configId}")
public ApiResponse<Void> updateWorkerConfig(@PathVariable Long workerId,
@PathVariable Long configId,
@Valid @RequestBody Map<String, Object> updates) {
log.info("更新渲染工作器配置, workerId: {}, configId: {}", workerId, configId);
try {
configIntegrationService.updateWorkerConfig(workerId, configId, updates);
return ApiResponse.success(null);
} catch (Exception e) {
log.error("更新渲染工作器配置失败, workerId: {}, configId: {}", workerId, configId, e);
return ApiResponse.fail("更新渲染工作器配置失败: " + e.getMessage());
}
}
/**
* 删除工作器配置
*
* @param workerId 工作器ID
* @param configId 配置ID
* @return 操作结果
*/
@DeleteMapping("/{workerId}/{configId}")
public ApiResponse<Void> deleteWorkerConfig(@PathVariable Long workerId,
@PathVariable Long configId) {
log.info("删除渲染工作器配置, workerId: {}, configId: {}", workerId, configId);
try {
configIntegrationService.deleteWorkerConfig(workerId, configId);
return ApiResponse.success(null);
} catch (Exception e) {
log.error("删除渲染工作器配置失败, workerId: {}, configId: {}", workerId, configId, e);
return ApiResponse.fail("删除渲染工作器配置失败: " + e.getMessage());
}
}
/**
* 批量更新工作器配置
*
* @param workerId 工作器ID
* @param request 批量配置请求
* @return 操作结果
*/
@PostMapping("/{workerId}/batch")
public ApiResponse<Void> batchUpdateWorkerConfigs(@PathVariable Long workerId,
@Valid @RequestBody BatchRenderWorkerConfigRequest request) {
log.info("批量更新渲染工作器配置, workerId: {}, configCount: {}",
workerId, request.getConfigs() != null ? request.getConfigs().size() : 0);
try {
configIntegrationService.batchUpdateWorkerConfigs(workerId, request);
return ApiResponse.success(null);
} catch (Exception e) {
log.error("批量更新渲染工作器配置失败, workerId: {}", workerId, e);
return ApiResponse.fail("批量更新渲染工作器配置失败: " + e.getMessage());
}
}
/**
* 批量平铺更新工作器配置
*
* @param workerId 工作器ID
* @param flatConfigs 平铺配置Map
* @return 操作结果
*/
@PostMapping("/{workerId}/flat-batch")
public ApiResponse<Void> batchFlatUpdateWorkerConfigs(@PathVariable Long workerId,
@Valid @RequestBody Map<String, Object> flatConfigs) {
log.info("批量平铺更新渲染工作器配置, workerId: {}, configCount: {}", workerId, flatConfigs.size());
try {
configIntegrationService.batchFlatUpdateWorkerConfigs(workerId, flatConfigs);
return ApiResponse.success(null);
} catch (Exception e) {
log.error("批量平铺更新渲染工作器配置失败, workerId: {}", workerId, e);
return ApiResponse.fail("批量平铺更新渲染工作器配置失败: " + e.getMessage());
}
}
}

View File

@@ -1,61 +0,0 @@
package com.ycwl.basic.controller.pc;
import com.ycwl.basic.model.pc.renderWorker.entity.RenderWorkerEntity;
import com.ycwl.basic.model.pc.renderWorker.req.RenderWorkerReqQuery;
import com.ycwl.basic.service.pc.RenderWorkerService;
import com.ycwl.basic.utils.ApiResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* @Author:longbinbin
* @Date:2024/12/3 14:59
*/
@RestController
@RequestMapping("/api/renderWorker/v1")
// 渲染机管理
public class RenderWorkerController {
@Autowired
private RenderWorkerService renderWorkerService;
// 分页查询渲染机
@PostMapping("/page")
public ApiResponse pageQuery(@RequestBody RenderWorkerReqQuery renderWorkerReqQuery){
return renderWorkerService.pageQuery(renderWorkerReqQuery);
}
// 渲染机列表查询
@PostMapping("/list")
public ApiResponse list(@RequestBody RenderWorkerReqQuery renderWorkerReqQuery){
return renderWorkerService.list(renderWorkerReqQuery);
}
// 渲染机详情查询
@GetMapping("/detail/{id}")
public ApiResponse detail(@PathVariable Long id){
return renderWorkerService.detail(id);
}
// 渲染机新增
@PostMapping("/add")
public ApiResponse add(@RequestBody RenderWorkerEntity renderWorker){
return renderWorkerService.add(renderWorker);
}
// 渲染机删除
@DeleteMapping("/delete/{id}")
public ApiResponse deleteById(@PathVariable Long id){
return renderWorkerService.deleteById(id);
}
// 渲染机修改
@PostMapping("/update")
public ApiResponse update(@RequestBody RenderWorkerEntity renderWorker){
return renderWorkerService.update(renderWorker);
}
// 渲染机修改状态
@PutMapping("/updateStatus/{id}")
public ApiResponse updateStatus(@PathVariable Long id) {
return renderWorkerService.updateStatus(id);
}
}

View File

@@ -0,0 +1,175 @@
package com.ycwl.basic.controller.pc;
import com.ycwl.basic.dto.RenderWorkerWithStatusDTO;
import com.ycwl.basic.integration.common.response.PageResponse;
import com.ycwl.basic.integration.render.dto.worker.CreateRenderWorkerRequest;
import com.ycwl.basic.integration.render.dto.worker.RenderWorkerV2DTO;
import com.ycwl.basic.integration.render.dto.worker.UpdateRenderWorkerRequest;
import com.ycwl.basic.integration.render.service.RenderWorkerIntegrationService;
import com.ycwl.basic.model.task.req.ClientStatusReqVo;
import com.ycwl.basic.repository.RenderWorkerRepository;
import com.ycwl.basic.utils.ApiResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
/**
* 渲染工作器管理 V2 版本控制器
* 基于 zt-render-worker 微服务标准接口实现
*
* @author Claude Code
* @date 2025-09-06
*/
@Slf4j
@RestController
@RequestMapping("/api/render/worker/v2")
@RequiredArgsConstructor
public class RenderWorkerV2Controller {
private final RenderWorkerIntegrationService renderWorkerIntegrationService;
private final RenderWorkerRepository renderWorkerRepository;
/**
* 分页查询渲染工作器列表(带保活信息)
*
* @param page 页码,从1开始
* @param pageSize 每页大小,默认10,最大100
* @param isEnabled 是否启用(0-禁用,1-启用)
* @param name 工作器名称(模糊搜索)
* @return 分页查询结果(包含保活信息)
*/
@GetMapping
public ApiResponse<PageResponse<RenderWorkerWithStatusDTO>> listWorkers(
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) Integer isEnabled,
@RequestParam(required = false) String name) {
log.debug("分页查询渲染工作器列表, page: {}, pageSize: {}, isEnabled: {}, name: {}",
page, pageSize, isEnabled, name);
// 参数验证:限制pageSize最大值为100
if (pageSize > 100) {
pageSize = 100;
}
try {
// 获取基础工作器列表
PageResponse<RenderWorkerV2DTO> basicResult = renderWorkerIntegrationService.listWorkers(
page, pageSize, isEnabled, name);
// 转换为带保活信息的DTO列表
List<RenderWorkerWithStatusDTO> workersWithStatus = new ArrayList<>();
for (RenderWorkerV2DTO worker : basicResult.getList()) {
RenderWorkerWithStatusDTO workerWithStatus = new RenderWorkerWithStatusDTO();
// 复制基础信息
BeanUtils.copyProperties(worker, workerWithStatus);
// 查询保活信息
ClientStatusReqVo hostStatus = renderWorkerRepository.getWorkerHostStatus(worker.getId());
workerWithStatus.setHostStatus(hostStatus);
workerWithStatus.setIsOnline(hostStatus != null);
workersWithStatus.add(workerWithStatus);
}
// 构建带保活信息的分页响应
PageResponse<RenderWorkerWithStatusDTO> result = new PageResponse<>();
result.setList(workersWithStatus);
result.setTotal(basicResult.getTotal());
result.setPage(basicResult.getPage());
result.setPageSize(basicResult.getPageSize());
return ApiResponse.success(result);
} catch (Exception e) {
log.error("分页查询渲染工作器列表失败", e);
return ApiResponse.fail("分页查询渲染工作器列表失败: " + e.getMessage());
}
}
/**
* 根据ID获取渲染工作器详情
*
* @param id 工作器ID
* @return 工作器详情
*/
@GetMapping("/{id}")
public ApiResponse<RenderWorkerV2DTO> getWorker(@PathVariable Long id) {
log.debug("获取渲染工作器详情, id: {}", id);
try {
RenderWorkerV2DTO worker = renderWorkerIntegrationService.getWorker(id);
return ApiResponse.success(worker);
} catch (Exception e) {
log.error("获取渲染工作器详情失败, id: {}", id, e);
return ApiResponse.fail("获取渲染工作器详情失败: " + e.getMessage());
}
}
/**
* 创建渲染工作器
*
* @param request 创建请求
* @return 创建的工作器信息
*/
@PostMapping
public ApiResponse<RenderWorkerV2DTO> createWorker(@Valid @RequestBody CreateRenderWorkerRequest request) {
log.debug("创建渲染工作器, name: {}, key: {}, isActive: {}",
request.getName(), request.getKey(), request.getIsActive());
try {
RenderWorkerV2DTO worker = renderWorkerIntegrationService.createWorker(request);
return ApiResponse.success(worker);
} catch (Exception e) {
log.error("创建渲染工作器失败", e);
return ApiResponse.fail("创建渲染工作器失败: " + e.getMessage());
}
}
/**
* 更新渲染工作器
*
* @param id 工作器ID
* @param request 更新请求
* @return 操作结果
*/
@PutMapping("/{id}")
public ApiResponse<Void> updateWorker(@PathVariable Long id,
@Valid @RequestBody UpdateRenderWorkerRequest request) {
log.debug("更新渲染工作器, id: {}, name: {}, isActive: {}",
id, request.getName(), request.getIsActive());
try {
renderWorkerIntegrationService.updateWorker(id, request);
return ApiResponse.success(null);
} catch (Exception e) {
log.error("更新渲染工作器失败, id: {}", id, e);
return ApiResponse.fail("更新渲染工作器失败: " + e.getMessage());
}
}
/**
* 删除渲染工作器
*
* @param id 工作器ID
* @return 操作结果
*/
@DeleteMapping("/{id}")
public ApiResponse<Void> deleteWorker(@PathVariable Long id) {
log.debug("删除渲染工作器, id: {}", id);
try {
renderWorkerIntegrationService.deleteWorker(id);
return ApiResponse.success(null);
} catch (Exception e) {
log.error("删除渲染工作器失败, id: {}", id, e);
return ApiResponse.fail("删除渲染工作器失败: " + e.getMessage());
}
}
}

View File

@@ -39,6 +39,13 @@ public class ScenicAccountController {
return result > 0 ? ApiResponse.success("更新成功") : ApiResponse.fail("更新失败"); return result > 0 ? ApiResponse.success("更新成功") : ApiResponse.fail("更新失败");
} }
// 激活/停用景区账号
@PostMapping("/updateActiveStatus/{id}")
public ApiResponse updateActiveStatus(@PathVariable Long id) {
int result = service.updateActiveStatus(id);
return result > 0 ? ApiResponse.success("操作成功") : ApiResponse.fail("操作失败");
}
// 更新景区账号 // 更新景区账号
@PostMapping("/update") @PostMapping("/update")
public ApiResponse updateScenicAccount(@RequestBody ScenicAccountEntity entity) { public ApiResponse updateScenicAccount(@RequestBody ScenicAccountEntity entity) {

View File

@@ -1,13 +1,12 @@
package com.ycwl.basic.controller.pc; package com.ycwl.basic.controller.pc;
import com.github.pagehelper.PageInfo;
import com.ycwl.basic.constant.BaseContextHandler; import com.ycwl.basic.constant.BaseContextHandler;
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO;
import com.ycwl.basic.model.mobile.statistic.req.CommonQueryReq; import com.ycwl.basic.model.mobile.statistic.req.CommonQueryReq;
import com.ycwl.basic.model.pc.mp.MpConfigEntity;
import com.ycwl.basic.model.pc.scenic.entity.ScenicAccountEntity; import com.ycwl.basic.model.pc.scenic.entity.ScenicAccountEntity;
import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity;
import com.ycwl.basic.model.pc.scenic.req.ScenicAddOrUpdateReq;
import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery; import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery;
import com.ycwl.basic.model.pc.scenic.resp.ScenicRespVO; import com.ycwl.basic.repository.ScenicRepository;
import com.ycwl.basic.service.mobile.AppScenicService; import com.ycwl.basic.service.mobile.AppScenicService;
import com.ycwl.basic.service.mobile.AppStatisticsService; import com.ycwl.basic.service.mobile.AppStatisticsService;
import com.ycwl.basic.service.pc.ScenicAccountService; import com.ycwl.basic.service.pc.ScenicAccountService;
@@ -17,16 +16,20 @@ import com.ycwl.basic.storage.adapters.IStorageAdapter;
import com.ycwl.basic.storage.enums.StorageAcl; import com.ycwl.basic.storage.enums.StorageAcl;
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 com.ycwl.basic.repository.ScenicRepository;
import com.ycwl.basic.model.pc.mp.MpConfigEntity;
import org.apache.commons.lang3.Strings; import org.apache.commons.lang3.Strings;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
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.RestController;
import java.io.File; import java.io.File;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import static com.ycwl.basic.constant.JwtRoleConstant.ADMIN;
import static com.ycwl.basic.constant.JwtRoleConstant.MERCHANT; import static com.ycwl.basic.constant.JwtRoleConstant.MERCHANT;
/** /**
@@ -50,68 +53,6 @@ public class ScenicController {
@Autowired @Autowired
private ScenicAccountService accountService; private ScenicAccountService accountService;
// 分页查询景区
@PostMapping("/page")
public ApiResponse<PageInfo<ScenicRespVO>> pageQuery(@RequestBody ScenicReqQuery scenicReqQuery) {
return scenicService.pageQuery(scenicReqQuery);
}
// 查询景区列表
@PostMapping("/list")
public ApiResponse<List<ScenicRespVO>> list(@RequestBody ScenicReqQuery scenicReqQuery) {
return scenicService.list(scenicReqQuery);
}
// 查询景区详情
@GetMapping("/getDetail/{id}")
public ApiResponse<ScenicRespVO> getDetail(@PathVariable Long id) {
return scenicService.getById(id);
}
// 新增景区
@PostMapping("/add")
public ApiResponse<Boolean> add(@RequestBody ScenicAddOrUpdateReq scenicAddReq) {
return scenicService.add(scenicAddReq);
}
// 删除景区
@GetMapping("/delete/{id}")
public ApiResponse<Boolean> delete(@PathVariable Long id) {
return scenicService.deleteById(id);
}
// 修改景区
@PostMapping("/update")
public ApiResponse<Boolean> update(@RequestBody ScenicAddOrUpdateReq scenicAddReq) {
return scenicService.update(scenicAddReq);
}
// 修改景区状态
@GetMapping("/updateStatus/{id}")
public ApiResponse<Boolean> updateStatus(@PathVariable Long id) {
return scenicService.updateStatus(id);
}
// 新增景区配置
@PostMapping("/addConfig")
public ApiResponse<Boolean> addConfig(@RequestBody ScenicConfigEntity scenicConfig) {
return scenicService.addConfig(scenicConfig);
}
// 修改景区配置
@PostMapping("/updateConfig")
public ApiResponse<Boolean> updateConfig(@RequestBody ScenicConfigEntity scenicConfig) {
return scenicService.updateConfigById(scenicConfig);
}
// 查询景区配置
@GetMapping("/config/{id}")
public ApiResponse<ScenicConfigEntity> getConfig(@PathVariable("id") Long id) {
return ApiResponse.success(scenicService.getConfig(id));
}
@PostMapping("/saveConfig/{id}")
public ApiResponse saveConfig(@PathVariable("id") Long id, @RequestBody ScenicConfigEntity config) {
scenicService.saveConfig(id, config);
return ApiResponse.success(null);
}
@PostMapping("/saveConfig/undefined")
public ApiResponse saveConfig(@RequestBody ScenicConfigEntity config) {
scenicService.addConfig(config);
return ApiResponse.success(null);
}
// 根据景区ID下载小程序二维码 // 根据景区ID下载小程序二维码
@GetMapping("/{id}/QRCode") @GetMapping("/{id}/QRCode")
public ApiResponse<String> downloadQrCode(@PathVariable Long id) { public ApiResponse<String> downloadQrCode(@PathVariable Long id) {
@@ -167,19 +108,19 @@ public class ScenicController {
} }
@GetMapping("/myScenicList") @GetMapping("/myScenicList")
public ApiResponse<List<ScenicRespVO>> myScenicList() { public ApiResponse<List<ScenicV2DTO>> myScenicList() {
List<ScenicRespVO> list = Collections.emptyList(); List<ScenicV2DTO> list = Collections.emptyList();
if (Strings.CS.equals(BaseContextHandler.getRoleId(), MERCHANT.type)) { if (Strings.CS.equals(BaseContextHandler.getRoleId(), MERCHANT.type)) {
String userId = BaseContextHandler.getUserId(); String userId = BaseContextHandler.getUserId();
ScenicAccountEntity account = accountService.getScenicAccountById(Long.valueOf(userId)); ScenicAccountEntity account = accountService.getScenicAccountById(Long.valueOf(userId));
if (account == null || account.getScenicId().isEmpty()) { if (account == null || account.getScenicId().isEmpty()) {
return ApiResponse.fail("景区账号未绑定景区"); return ApiResponse.fail("景区账号未绑定景区");
} }
list = account.getScenicId().stream().map(id -> { list = account.getScenicId().stream().map(id -> scenicRepository.getScenicBasic(id)).toList();
return appScenicService.getDetails(id).getData(); } else if (Strings.CS.equals(BaseContextHandler.getRoleId(), ADMIN.type)) {
}).toList(); ScenicReqQuery query = new ScenicReqQuery();
} else { query.setPageSize(1000);
list = scenicService.list(new ScenicReqQuery()).getData(); list = scenicRepository.list(query);
} }
return ApiResponse.success(list); return ApiResponse.success(list);
} }

View File

@@ -0,0 +1,264 @@
package com.ycwl.basic.controller.pc;
import com.ycwl.basic.integration.scenic.dto.config.BatchConfigRequest;
import com.ycwl.basic.integration.scenic.dto.config.BatchUpdateResponse;
import com.ycwl.basic.integration.scenic.dto.config.CreateConfigRequest;
import com.ycwl.basic.integration.scenic.dto.config.ScenicConfigV2DTO;
import com.ycwl.basic.integration.scenic.dto.config.UpdateConfigRequest;
import com.ycwl.basic.integration.scenic.dto.filter.ScenicFilterPageResponse;
import com.ycwl.basic.integration.scenic.dto.filter.ScenicFilterRequest;
import com.ycwl.basic.integration.scenic.dto.scenic.CreateScenicRequest;
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO;
import com.ycwl.basic.integration.common.response.PageResponse;
import com.ycwl.basic.integration.scenic.dto.scenic.UpdateScenicRequest;
import com.ycwl.basic.integration.scenic.service.ScenicConfigIntegrationService;
import com.ycwl.basic.integration.scenic.service.ScenicIntegrationService;
import com.ycwl.basic.utils.ApiResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
/**
* @Author:longbinbin
* @Date:2024/12/26
* 景区管理 V2 版本控制器 - 基于 zt-scenic 集成服务
*/
@Slf4j
@RestController
@RequestMapping("/api/scenic/v2")
@RequiredArgsConstructor
public class ScenicV2Controller {
private final ScenicIntegrationService scenicIntegrationService;
private final ScenicConfigIntegrationService scenicConfigIntegrationService;
// ========== 景区基础 CRUD 操作 ==========
/**
* 景区V2核心信息分页列表
*/
@GetMapping("/")
public ApiResponse<PageResponse<ScenicV2DTO>> listScenics(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) Integer status,
@RequestParam(required = false) String name) {
log.info("分页查询景区核心信息列表, page: {}, pageSize: {}, status: {}, name: {}", page, pageSize, status, name);
// 参数验证:限制pageSize最大值为100
if (pageSize > 100) {
pageSize = 100;
}
try {
PageResponse<ScenicV2DTO> response = scenicIntegrationService.listScenics(page, pageSize, status, name, null);
return ApiResponse.success(response);
} catch (Exception e) {
log.error("分页查询景区核心信息列表失败", e);
return ApiResponse.fail("分页查询景区列表失败: " + e.getMessage());
}
}
/**
* 查询单个景区详情
*/
@GetMapping("/{scenicId}")
public ApiResponse<ScenicV2DTO> getScenic(@PathVariable Long scenicId) {
log.info("查询景区详情, scenicId: {}", scenicId);
try {
ScenicV2DTO scenic = scenicIntegrationService.getScenic(scenicId);
return ApiResponse.success(scenic);
} catch (Exception e) {
log.error("查询景区详情失败, scenicId: {}", scenicId, e);
return ApiResponse.fail("查询景区详情失败: " + e.getMessage());
}
}
/**
* 查询景区列表(支持筛选和分页)- 高级筛选
*/
@PostMapping("/filter")
public ApiResponse<ScenicFilterPageResponse> filterScenics(@RequestBody @Valid ScenicFilterRequest request) {
log.info("高级筛选景区列表, 筛选条件数: {}, 页码: {}, 页大小: {}",
request.getFilters().size(), request.getPage(), request.getPageSize());
try {
ScenicFilterPageResponse response = scenicIntegrationService.filterScenics(request);
return ApiResponse.success(response);
} catch (Exception e) {
log.error("高级筛选景区列表失败", e);
return ApiResponse.fail("高级筛选景区列表失败: " + e.getMessage());
}
}
/**
* 新增景区
*/
@PostMapping("/create")
public ApiResponse<ScenicV2DTO> createScenic(@RequestBody @Valid CreateScenicRequest request) {
log.info("新增景区, name: {}, mpId: {}", request.getName(), request.getMpId());
try {
ScenicV2DTO scenic = scenicIntegrationService.createScenic(request);
return ApiResponse.success(scenic);
} catch (Exception e) {
log.error("新增景区失败, name: {}", request.getName(), e);
return ApiResponse.fail("新增景区失败: " + e.getMessage());
}
}
/**
* 修改景区
*/
@PutMapping("/{scenicId}")
public ApiResponse<ScenicV2DTO> updateScenic(@PathVariable Long scenicId,
@RequestBody @Valid UpdateScenicRequest request) {
log.info("修改景区, scenicId: {}", scenicId);
try {
ScenicV2DTO scenic = scenicIntegrationService.updateScenic(scenicId, request);
return ApiResponse.success(scenic);
} catch (Exception e) {
log.error("修改景区失败, scenicId: {}", scenicId, e);
return ApiResponse.fail("修改景区失败: " + e.getMessage());
}
}
/**
* 删除景区
*/
@DeleteMapping("/{scenicId}")
public ApiResponse<Void> deleteScenic(@PathVariable Long scenicId) {
log.info("删除景区, scenicId: {}", scenicId);
try {
scenicIntegrationService.deleteScenic(scenicId);
return ApiResponse.success(null);
} catch (Exception e) {
log.error("删除景区失败, scenicId: {}", scenicId, e);
return ApiResponse.fail("删除景区失败: " + e.getMessage());
}
}
/**
* 景区列表查询(默认1000条)
* 只支持根据状态筛选
*/
@GetMapping("/list")
public ApiResponse<PageResponse<ScenicV2DTO>> listScenicsByStatus(@RequestParam(required = false) Integer status) {
log.info("查询景区列表, status: {}", status);
try {
// 默认查询1000条数据,第1页
PageResponse<ScenicV2DTO> scenics = scenicIntegrationService.listScenics(1, 1000, status, null, null);
return ApiResponse.success(scenics);
} catch (Exception e) {
log.error("查询景区列表失败, status: {}", status, e);
return ApiResponse.fail("查询景区列表失败: " + e.getMessage());
}
}
// ========== 景区配置管理 ==========
/**
* 获取景区配置列表
*/
@GetMapping("/{scenicId}/config")
public ApiResponse<List<ScenicConfigV2DTO>> listConfigs(@PathVariable Long scenicId) {
log.info("获取景区配置列表, scenicId: {}", scenicId);
try {
List<ScenicConfigV2DTO> configs = scenicConfigIntegrationService.listConfigs(scenicId);
return ApiResponse.success(configs);
} catch (Exception e) {
log.error("获取景区配置列表失败, scenicId: {}", scenicId, e);
return ApiResponse.fail("获取景区配置列表失败: " + e.getMessage());
}
}
/**
* 根据配置键获取配置
*/
@GetMapping("/{scenicId}/config/{configKey}")
public ApiResponse<ScenicConfigV2DTO> getConfigByKey(@PathVariable Long scenicId,
@PathVariable String configKey) {
log.info("根据键获取景区配置, scenicId: {}, configKey: {}", scenicId, configKey);
try {
ScenicConfigV2DTO config = scenicConfigIntegrationService.getConfigByKey(scenicId, configKey);
return ApiResponse.success(config);
} catch (Exception e) {
log.error("根据键获取景区配置失败, scenicId: {}, configKey: {}", scenicId, configKey, e);
return ApiResponse.fail("获取配置失败: " + e.getMessage());
}
}
/**
* 创建景区配置
*/
@PostMapping("/{scenicId}/config")
public ApiResponse<ScenicConfigV2DTO> createConfig(@PathVariable Long scenicId,
@RequestBody @Valid CreateConfigRequest request) {
log.info("创建景区配置, scenicId: {}, configKey: {}", scenicId, request.getConfigKey());
try {
ScenicConfigV2DTO config = scenicConfigIntegrationService.createConfig(scenicId, request);
return ApiResponse.success(config);
} catch (Exception e) {
log.error("创建景区配置失败, scenicId: {}, configKey: {}", scenicId, request.getConfigKey(), e);
return ApiResponse.fail("创建配置失败: " + e.getMessage());
}
}
/**
* 更新景区配置
*/
@PutMapping("/{scenicId}/config/{configId}")
public ApiResponse<ScenicConfigV2DTO> updateConfig(@PathVariable Long scenicId,
@PathVariable String configId,
@RequestBody @Valid UpdateConfigRequest request) {
log.info("更新景区配置, scenicId: {}, configId: {}", scenicId, configId);
try {
ScenicConfigV2DTO config = scenicConfigIntegrationService.updateConfig(scenicId, configId, request);
return ApiResponse.success(config);
} catch (Exception e) {
log.error("更新景区配置失败, scenicId: {}, configId: {}", scenicId, configId, e);
return ApiResponse.fail("更新配置失败: " + e.getMessage());
}
}
/**
* 删除景区配置
*/
@DeleteMapping("/{scenicId}/config/{configId}")
public ApiResponse<Void> deleteConfig(@PathVariable Long scenicId, @PathVariable String configId) {
log.info("删除景区配置, scenicId: {}, configId: {}", scenicId, configId);
try {
scenicConfigIntegrationService.deleteConfig(scenicId, configId);
return ApiResponse.success(null);
} catch (Exception e) {
log.error("删除景区配置失败, scenicId: {}, configId: {}", scenicId, configId, e);
return ApiResponse.fail("删除配置失败: " + e.getMessage());
}
}
/**
* 批量更新景区配置
*/
@PutMapping("/{scenicId}/config/batch")
public ApiResponse<BatchUpdateResponse> batchUpdateConfigs(@PathVariable Long scenicId,
@RequestBody @Valid BatchConfigRequest request) {
log.info("批量更新景区配置, scenicId: {}, configs count: {}", scenicId, request.getConfigs().size());
try {
BatchUpdateResponse response = scenicConfigIntegrationService.batchUpdateConfigs(scenicId, request);
return ApiResponse.success(response);
} catch (Exception e) {
log.error("批量更新景区配置失败, scenicId: {}", scenicId, e);
return ApiResponse.fail("批量更新配置失败: " + e.getMessage());
}
}
}

View File

@@ -4,12 +4,16 @@ import com.ycwl.basic.annotation.IgnoreToken;
import com.ycwl.basic.model.jwt.JwtInfo; import com.ycwl.basic.model.jwt.JwtInfo;
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.printer.req.CreateVirtualOrderRequest;
import com.ycwl.basic.service.pc.SourceService; import com.ycwl.basic.service.pc.SourceService;
import com.ycwl.basic.service.printer.PrinterService;
import com.ycwl.basic.utils.ApiResponse; import com.ycwl.basic.utils.ApiResponse;
import com.ycwl.basic.utils.JwtTokenUtil; 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.Map;
/** /**
* @Author:longbinbin * @Author:longbinbin
* @Date:2024/12/3 15:45 * @Date:2024/12/3 15:45
@@ -21,8 +25,9 @@ public class SourceController {
@Autowired @Autowired
private SourceService sourceService; private SourceService sourceService;
@Autowired
private PrinterService printerService;
@Deprecated
// 分页查询视频源 // 分页查询视频源
@PostMapping("/page") @PostMapping("/page")
public ApiResponse pageQuery(@RequestBody SourceReqQuery sourceReqQuery) { public ApiResponse pageQuery(@RequestBody SourceReqQuery sourceReqQuery) {
@@ -46,5 +51,28 @@ public class SourceController {
return sourceService.deleteById(id); return sourceService.deleteById(id);
} }
/**
* 创建虚拟用户0元订单
* 用于后台直接从source创建订单,不需要真实用户
*
* @param request 请求参数
* @return 订单信息
*/
@PostMapping("/createVirtualOrder")
public ApiResponse<Map<String, Object>> createVirtualOrder(@RequestBody CreateVirtualOrderRequest request) {
try {
Map<String, Object> result = printerService.createVirtualOrder(
request.getSourceId(),
request.getScenicId(),
request.getPrinterId(),
request.getNeedEnhance(),
request.getPrintImgUrl()
);
return ApiResponse.success(result);
} catch (Exception e) {
return ApiResponse.fail(e.getMessage());
}
}
} }

View File

@@ -13,7 +13,6 @@ import org.springframework.web.bind.annotation.*;
*/ */
@RestController @RestController
@RequestMapping("/api/task/v1") @RequestMapping("/api/task/v1")
@Deprecated
// 任务列表管理 // 任务列表管理
public class TaskController { public class TaskController {

View File

@@ -18,7 +18,6 @@ import java.util.List;
@RestController @RestController
@RequestMapping("/api/video/v1") @RequestMapping("/api/video/v1")
@Deprecated
// 视频成片管理 // 视频成片管理
public class VideoController { public class VideoController {
@@ -41,4 +40,16 @@ public class VideoController {
return videoService.getById(id); return videoService.getById(id);
} }
/**
* 查询视频是否被购买
*
* @param videoId 视频ID
* @return 是否已购买
*/
@GetMapping("/checkBuyStatus")
public ApiResponse<Boolean> checkBuyStatus(@RequestParam("videoId") Long videoId) {
Boolean isBuy = videoService.checkVideoBuyStatus(videoId);
return ApiResponse.success(isBuy);
}
} }

View File

@@ -0,0 +1,122 @@
package com.ycwl.basic.controller.pc;
import com.github.pagehelper.PageInfo;
import com.ycwl.basic.model.pc.notify.entity.WechatSubscribeEventTemplateEntity;
import com.ycwl.basic.model.pc.notify.entity.WechatSubscribeSceneTemplateEntity;
import com.ycwl.basic.model.pc.notify.entity.WechatSubscribeSendLogEntity;
import com.ycwl.basic.model.pc.notify.entity.WechatSubscribeTemplateConfigEntity;
import com.ycwl.basic.model.pc.notify.req.WechatSubscribeEventTemplatePageReq;
import com.ycwl.basic.model.pc.notify.req.WechatSubscribeEventTemplateSaveReq;
import com.ycwl.basic.model.pc.notify.req.WechatSubscribeSceneTemplatePageReq;
import com.ycwl.basic.model.pc.notify.req.WechatSubscribeSceneTemplateSaveReq;
import com.ycwl.basic.model.pc.notify.req.WechatSubscribeSendLogPageReq;
import com.ycwl.basic.model.pc.notify.req.WechatSubscribeTemplateConfigPageReq;
import com.ycwl.basic.model.pc.notify.req.WechatSubscribeTemplateConfigSaveReq;
import com.ycwl.basic.service.pc.WechatSubscribeNotifyAdminService;
import com.ycwl.basic.utils.ApiResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
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.RestController;
/**
* 微信小程序订阅消息:配置管理(管理后台)
*
* @Author: System
* @Date: 2025/12/31
*/
@Slf4j
@RestController
@RequestMapping("/api/wechatSubscribeNotify/v1")
@RequiredArgsConstructor
public class WechatSubscribeNotifyAdminController {
private final WechatSubscribeNotifyAdminService adminService;
// ========================= 模板配置 =========================
@PostMapping("/templateConfig/page")
public ApiResponse<PageInfo<WechatSubscribeTemplateConfigEntity>> pageTemplateConfig(
@RequestBody WechatSubscribeTemplateConfigPageReq req) {
return adminService.pageTemplateConfig(req);
}
@GetMapping("/templateConfig/detail/{id}")
public ApiResponse<WechatSubscribeTemplateConfigEntity> getTemplateConfig(@PathVariable("id") Long id) {
return adminService.getTemplateConfig(id);
}
@PostMapping("/templateConfig/save")
public ApiResponse<Boolean> saveTemplateConfig(@RequestBody WechatSubscribeTemplateConfigSaveReq req) {
return adminService.saveTemplateConfig(req);
}
@DeleteMapping("/templateConfig/delete/{id}")
public ApiResponse<Boolean> deleteTemplateConfig(@PathVariable("id") Long id) {
return adminService.deleteTemplateConfig(id);
}
// ========================= 场景映射 =========================
@PostMapping("/sceneTemplate/page")
public ApiResponse<PageInfo<WechatSubscribeSceneTemplateEntity>> pageSceneTemplate(
@RequestBody WechatSubscribeSceneTemplatePageReq req) {
return adminService.pageSceneTemplate(req);
}
@GetMapping("/sceneTemplate/detail/{id}")
public ApiResponse<WechatSubscribeSceneTemplateEntity> getSceneTemplate(@PathVariable("id") Long id) {
return adminService.getSceneTemplate(id);
}
@PostMapping("/sceneTemplate/save")
public ApiResponse<Boolean> saveSceneTemplate(@RequestBody WechatSubscribeSceneTemplateSaveReq req) {
return adminService.saveSceneTemplate(req);
}
@DeleteMapping("/sceneTemplate/delete/{id}")
public ApiResponse<Boolean> deleteSceneTemplate(@PathVariable("id") Long id) {
return adminService.deleteSceneTemplate(id);
}
// ========================= 事件映射 =========================
@PostMapping("/eventTemplate/page")
public ApiResponse<PageInfo<WechatSubscribeEventTemplateEntity>> pageEventTemplate(
@RequestBody WechatSubscribeEventTemplatePageReq req) {
return adminService.pageEventTemplate(req);
}
@GetMapping("/eventTemplate/detail/{id}")
public ApiResponse<WechatSubscribeEventTemplateEntity> getEventTemplate(@PathVariable("id") Long id) {
return adminService.getEventTemplate(id);
}
@PostMapping("/eventTemplate/save")
public ApiResponse<Boolean> saveEventTemplate(@RequestBody WechatSubscribeEventTemplateSaveReq req) {
return adminService.saveEventTemplate(req);
}
@DeleteMapping("/eventTemplate/delete/{id}")
public ApiResponse<Boolean> deleteEventTemplate(@PathVariable("id") Long id) {
return adminService.deleteEventTemplate(id);
}
// ========================= 发送日志 =========================
@PostMapping("/sendLog/page")
public ApiResponse<PageInfo<WechatSubscribeSendLogEntity>> pageSendLog(@RequestBody WechatSubscribeSendLogPageReq req) {
return adminService.pageSendLog(req);
}
@GetMapping("/sendLog/detail/{id}")
public ApiResponse<WechatSubscribeSendLogEntity> getSendLog(@PathVariable("id") Long id) {
return adminService.getSendLog(id);
}
}

View File

@@ -0,0 +1,194 @@
package com.ycwl.basic.controller.printer;
import com.ycwl.basic.annotation.IgnoreToken;
import com.ycwl.basic.integration.device.dto.device.DeviceV2DTO;
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO;
import com.ycwl.basic.mapper.SourceMapper;
import com.ycwl.basic.model.mobile.face.FaceRecognizeResp;
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
import com.ycwl.basic.model.pc.mp.MpConfigEntity;
import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery;
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
import com.ycwl.basic.repository.DeviceRepository;
import com.ycwl.basic.repository.FaceRepository;
import com.ycwl.basic.repository.ScenicRepository;
import com.ycwl.basic.service.pc.FaceService;
import com.ycwl.basic.utils.ApiResponse;
import com.ycwl.basic.utils.WxMpUtil;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.FileInputStream;
import java.io.OutputStream;
import java.util.List;
@IgnoreToken
// 打印机大屏对接接口
@RestController
@RequestMapping("/printer/v1/tv")
@RequiredArgsConstructor
public class PrinterTvController {
private final DeviceRepository deviceRepository;
private final ScenicRepository scenicRepository;
private final FaceRepository faceRepository;
private final FaceService pcFaceService;
private final SourceMapper sourceMapper;
/**
* 获取景区列表
*
* @return 景区列表
*/
@GetMapping("/scenic/list")
public ApiResponse<List<ScenicV2DTO>> getScenicList() {
ScenicReqQuery query = new ScenicReqQuery();
query.setStatus("1"); // 只查询启用状态的景区
query.setPageNum(1);
query.setPageSize(1000);
return ApiResponse.success(scenicRepository.list(query));
}
/**
* 根据景区ID查询设备列表
*
* @param scenicId 景区ID
* @return 设备列表
*/
@GetMapping("/device/list")
public ApiResponse<List<DeviceV2DTO>> getDeviceListByScenicId(@RequestParam Long scenicId) {
List<DeviceV2DTO> result = deviceRepository.getAllDeviceByScenicId(scenicId);
return ApiResponse.success(result);
}
@GetMapping("/{sampleId}/qrcode")
public void getQrcode(@PathVariable("sampleId") Long sampleId, HttpServletResponse response) throws Exception {
File qrcode = new File("qrcode_"+sampleId+".jpg");
FaceSampleEntity faceSample = faceRepository.getFaceSample(sampleId);
if (faceSample == null) {
response.setStatus(404);
return;
}
String targetPath = "pages/printer/from_sample";
DeviceV2DTO device = deviceRepository.getDeviceBasic(faceSample.getDeviceId());
if (device.getType().equals("AI_CAM")) {
// AI_CAM,需要修改path
targetPath = "pages/ai-cam/from_sample";
}
try {
MpConfigEntity scenicMpConfig = scenicRepository.getScenicMpConfig(faceSample.getScenicId());
WxMpUtil.generateUnlimitedWXAQRCode(scenicMpConfig.getAppId(), scenicMpConfig.getAppSecret(), targetPath, sampleId.toString(), qrcode);
// 设置响应头
response.setContentType("image/jpeg");
response.setHeader("Content-Disposition", "inline; filename=\"" + qrcode.getName() + "\"");
// 将二维码文件写入响应输出流
try (FileInputStream fis = new FileInputStream(qrcode);
OutputStream os = response.getOutputStream()) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
os.write(buffer, 0, bytesRead);
}
os.flush();
}
} finally {
// 删除临时文件
if (qrcode.exists()) {
qrcode.delete();
}
}
}
/**
* 获取人脸绑定二维码
* 生成小程序二维码,用于绑定人脸到用户账号
*
* @param faceId 人脸ID
* @param response HTTP响应
*/
@GetMapping("/face/{faceId}/qrcode")
public void getFaceQrcode(@PathVariable("faceId") Long faceId, HttpServletResponse response) throws Exception {
String url = pcFaceService.bindWxaCode(faceId);
response.sendRedirect(url);
}
/**
* 根据人脸ID查询图像素材
*
* @param faceId 人脸ID
* @param type 素材类型(默认为2-图片)
* @return 匹配的source记录
*/
@GetMapping("/{faceId}/source")
public ApiResponse<List<SourceEntity>> getSourceByFaceId(@PathVariable Long faceId, @RequestParam(name = "type", required = false, defaultValue = "2") Integer type) {
List<SourceEntity> sources = sourceMapper.listSourceByFaceRelation(faceId, type);
return ApiResponse.success(sources);
}
/**
* 打印机大屏人脸识别
* 上传照片,在景区人脸库中搜索匹配的人脸样本,返回识别结果
*
* 使用 USER_FACE_DB_NAME+scenicId 对人脸进行去重检测:
* - 如果已存在相同人脸(打印机大屏用户,memberId=0),则返回已存在的 faceId
* - 否则创建新的人脸记录并添加到人脸库
*
* @param file 人脸照片文件
* @param scenicId 景区ID
* @return 人脸识别结果
*/
@PostMapping("/{scenicId}/faceRecognize")
public ApiResponse<FaceRecognizeResp> faceRecognize(
@RequestParam("file") MultipartFile file,
@PathVariable Long scenicId) {
// 复用 faceUpload 方法的去重逻辑
// memberId=0L 表示打印机大屏用户,scene="tv" 为试点场景:仅执行识别/补救/落库/建关系
FaceRecognizeResp resp = pcFaceService.faceUpload(file, scenicId, 0L, "tv");
return ApiResponse.success(resp);
}
/**
* 通过人脸样本ID重定向到人脸图片URL
*
* @param faceSampleId 人脸样本ID
* @param response HTTP响应
*/
@GetMapping("/faceSample/{faceSampleId}/url")
public void redirectToFaceSampleUrl(@PathVariable Long faceSampleId, HttpServletResponse response) throws Exception {
FaceSampleEntity faceSample = faceRepository.getFaceSample(faceSampleId);
if (faceSample == null || faceSample.getFaceUrl() == null) {
response.setStatus(404);
return;
}
response.sendRedirect(faceSample.getFaceUrl());
}
/**
* 通过人脸ID重定向到人脸图片URL
*
* @param faceId 人脸ID
* @param response HTTP响应
*/
@GetMapping("/face/{faceId}/url")
public void redirectToFaceUrl(@PathVariable Long faceId, HttpServletResponse response) throws Exception {
FaceEntity face = faceRepository.getFace(faceId);
if (face == null || face.getFaceUrl() == null) {
response.setStatus(404);
return;
}
response.sendRedirect(face.getFaceUrl());
}
}

View File

@@ -1,453 +0,0 @@
package com.ycwl.basic.controller.viid;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.thread.ThreadFactoryBuilder;
import cn.hutool.core.util.ObjectUtil;
import com.ycwl.basic.utils.JacksonUtil;
import com.ycwl.basic.annotation.IgnoreLogReq;
import com.ycwl.basic.annotation.IgnoreToken;
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
import com.ycwl.basic.facebody.entity.AddFaceResp;
import com.ycwl.basic.mapper.DeviceMapper;
import com.ycwl.basic.mapper.FaceSampleMapper;
import com.ycwl.basic.mapper.SourceMapper;
import com.ycwl.basic.model.pc.device.entity.DeviceConfigEntity;
import com.ycwl.basic.model.pc.device.entity.DeviceCropConfig;
import com.ycwl.basic.model.pc.device.entity.DeviceEntity;
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
import com.ycwl.basic.model.viid.entity.DeviceIdObject;
import com.ycwl.basic.model.viid.entity.FaceListObject;
import com.ycwl.basic.model.viid.entity.FaceObject;
import com.ycwl.basic.model.viid.entity.FacePositionObject;
import com.ycwl.basic.model.viid.entity.ResponseStatusObject;
import com.ycwl.basic.model.viid.entity.SubImageInfoObject;
import com.ycwl.basic.model.viid.entity.SubImageList;
import com.ycwl.basic.model.viid.entity.SystemTimeObject;
import com.ycwl.basic.model.viid.req.FaceUploadReq;
import com.ycwl.basic.model.viid.req.ImageUploadReq;
import com.ycwl.basic.model.viid.req.KeepaliveReq;
import com.ycwl.basic.model.viid.req.RegisterReq;
import com.ycwl.basic.model.viid.req.UnRegisterReq;
import com.ycwl.basic.model.viid.resp.SystemTimeResp;
import com.ycwl.basic.model.viid.resp.VIIDBaseResp;
import com.ycwl.basic.repository.DeviceRepository;
import com.ycwl.basic.repository.ScenicRepository;
import com.ycwl.basic.service.pc.ScenicService;
import com.ycwl.basic.service.task.TaskFaceService;
import com.ycwl.basic.storage.StorageFactory;
import com.ycwl.basic.storage.adapters.IStorageAdapter;
import com.ycwl.basic.storage.enums.StorageAcl;
import com.ycwl.basic.storage.utils.StorageUtil;
import com.ycwl.basic.task.DynamicTaskGenerator;
import com.ycwl.basic.utils.ImageUtils;
import com.ycwl.basic.utils.IpUtils;
import com.ycwl.basic.utils.SnowFlakeUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import jakarta.servlet.http.HttpServletRequest;
import java.awt.image.RasterFormatException;
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import static com.ycwl.basic.constant.StorageConstant.PHOTO_PATH;
import static com.ycwl.basic.constant.StorageConstant.VIID_FACE;
@IgnoreToken
@RestController
// 摄像头对接接口
@RequestMapping("/VIID")
@Slf4j
public class ViidController {
@Autowired
private DeviceMapper deviceMapper;
private static final String serverId = "00000000000000000001";
@Autowired
private SourceMapper sourceMapper;
@Autowired
private DeviceRepository deviceRepository;
@Autowired
private ScenicRepository scenicRepository;
@Autowired
private TaskFaceService taskFaceService;
private final Map<Long, ThreadPoolExecutor> executors = new ConcurrentHashMap<>();
@Autowired
private ScenicService scenicService;
private ThreadPoolExecutor getExecutor(Long scenicId) {
return executors.computeIfAbsent(scenicId, k -> {
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setNamePrefix("VIID-" + scenicId + "-t")
.build();
return new ThreadPoolExecutor(
4, 1024, 0L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(1024),
threadFactory);
});
}
// region 注册注销基础接口
/**
* 注册接口
*
* @param req 注册的信息
* @param request 请求
* @return 返回
*/
@RequestMapping(value = "/System/Register", method = RequestMethod.POST)
public VIIDBaseResp register(@RequestBody RegisterReq req, HttpServletRequest request) {
DeviceIdObject deviceIdObject = req.getRegisterObject();
log.info("注册的设备信息:{}", deviceIdObject);
// 保存设备注册时间
String deviceId = deviceIdObject.getDeviceId();
DeviceEntity device = deviceRepository.getDeviceByDeviceNo(deviceId);
if (device == null) {
device = new DeviceEntity();
device.setName("未配置设备");
device.setNo(deviceId);
device.setOnline(1);
}
device.setKeepaliveAt(new Date());
device.setIpAddr(IpUtils.getIpAddr(request));
if (device.getId() != null) {
deviceMapper.updateEntity(device);
} else {
device.setId(SnowFlakeUtil.getLongId());
deviceMapper.addEntity(device);
deviceRepository.clearDeviceCache(deviceId);
}
return new VIIDBaseResp(
new ResponseStatusObject(serverId, "/VIID/System/Register", "0", "注册成功", sdfTime.format(new Date()))
);
}
/**
* 保活接口
*
* @param req 保活的设备信息
* @param request 请求
* @return 返回
*/
@IgnoreLogReq
@RequestMapping(value = "/System/Keepalive", method = RequestMethod.POST)
public VIIDBaseResp keepalive(@RequestBody KeepaliveReq req, HttpServletRequest request) {
DeviceIdObject keepaliveObject = req.getKeepaliveObject();
// log.info("对方发送的心跳的信息:{}", keepaliveObject);
String deviceId = keepaliveObject.getDeviceId();
DeviceEntity device = deviceRepository.getDeviceByDeviceNo(deviceId);
// 判断设备状态
if (device == null) {
// 不存在设备就注册
device = new DeviceEntity();
device.setName("未配置设备");
device.setNo(deviceId);
device.setOnline(1);
device.setKeepaliveAt(new Date());
device.setIpAddr(IpUtils.getIpAddr(request));
device.setId(SnowFlakeUtil.getLongId());
deviceMapper.addEntity(device);
deviceRepository.clearDeviceCache(deviceId);
} else {
deviceRepository.updateOnlineStatus(device.getId(), IpUtils.getIpAddr(request), 1, new Date());
}
// log.info("已经解析过的心跳信息:{}", keepaliveObject);
return new VIIDBaseResp(
new ResponseStatusObject(deviceId, "/VIID/System/Keepalive", "0", "保活", sdfTime.format(new Date()))
);
}
/**
* 注销设备
*
* @param req 参数
* @return 返回
*/
@RequestMapping(value = "/System/UnRegister", method = RequestMethod.POST)
public VIIDBaseResp unRegister(@RequestBody UnRegisterReq req, HttpServletRequest request) {
// 获取设备id
DeviceIdObject unRegisterObject = req.getUnRegisterObject();
String deviceId = unRegisterObject.getDeviceId();
log.info("获取的注销的请求参数:{}", unRegisterObject);
// 首先查询该设备是否存在
DeviceEntity device = deviceRepository.getDeviceByDeviceNo(deviceId);
// 判断
if (device != null) {
deviceRepository.updateOnlineStatus(device.getId(), IpUtils.getIpAddr(request), 0, new Date());
}
return new VIIDBaseResp(
new ResponseStatusObject(deviceId, "/VIID/System/UnRegister", "0", "注销成功", sdfTime.format(new Date()))
);
}
/**
* 校时接口
*
* @return 返回
*/
@RequestMapping(value = "/System/Time", method = RequestMethod.GET)
public SystemTimeResp time() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
return new SystemTimeResp(
new SystemTimeObject(serverId, "2", sdf.format(new Date()), TimeZone.getTimeZone("Asia/Shanghai").toString())
);
}
// endregion
@Autowired
private FaceSampleMapper faceSampleMapper;
private final SimpleDateFormat sdfTime = new SimpleDateFormat("yyyyMMddHHmmss");
/**
* 批量新增人脸
*/
@RequestMapping(value = "/Faces", method = RequestMethod.POST)
@IgnoreLogReq
public VIIDBaseResp faces(@RequestBody FaceUploadReq req) {
FaceListObject faceListObject = req.getFaceListObject();
List<FaceObject> faceObject = faceListObject.getFaceObject();
String faceId = null;
// 遍历人脸列表
for (FaceObject face : faceObject) {
// 设置FaceId
faceId = face.getFaceID();
// 获取图片信息
SubImageList subImageList = face.getSubImageList();
// 判断人脸对象中的列表是否为空
String deviceID = face.getDeviceID();
DeviceEntity device = deviceRepository.getDeviceByDeviceNo(deviceID);
if (device == null) {
continue;
}
DeviceConfigEntity deviceConfig = deviceRepository.getDeviceConfig(device.getId());
if (deviceConfig == null) {
log.warn("设备配置不存在:" + deviceID);
return new VIIDBaseResp(
new ResponseStatusObject(faceId, "/VIID/Faces", "0", "OK", sdfTime.format(new Date()))
);
}
int viidMode = 0;
if (deviceConfig.getViidType() != null) {
viidMode = deviceConfig.getViidType();
}
Date shotTime = null;
if (StringUtils.isNotBlank(face.getShotTime())) {
try {
shotTime = sdfTime.parse(face.getShotTime());
} catch (ParseException e) {
log.warn("拍摄时间时间转换失败,使用当前时间。错误entity:{}", face);
}
}
if (shotTime == null) {
if (StringUtils.isNotBlank(face.getFaceAppearTime())) {
try {
shotTime = sdfTime.parse(face.getFaceAppearTime());
} catch (ParseException e) {
log.warn("拍摄时间时间转换失败,使用当前时间。错误entity:{}", face);
}
}
}
if (shotTime == null) {
shotTime = new Date();
} else if (!DateUtil.isSameDay(shotTime, new Date())) {
log.warn("时间不是今天,使用当前时间。错误entity:{}", face);
shotTime = new Date();
}
if (Math.abs(shotTime.getTime() - System.currentTimeMillis()) > 3600 * 1000) {
String jsonString = JacksonUtil.toJSONStringCompat(req);
log.warn("时间差超过1小时。device:{},错误entity:{}", device, jsonString);
}
Long scenicId = device.getScenicId();
if (scenicId == null) {
continue;
}
IStorageAdapter scenicStorageAdapter = scenicService.getScenicStorageAdapter(scenicId);
IFaceBodyAdapter faceBodyAdapter = scenicService.getScenicFaceBodyAdapter(scenicId);
FacePositionObject facePosition = new FacePositionObject();
facePosition.setLtY(face.getLeftTopY());
facePosition.setLtX(face.getLeftTopX());
facePosition.setRbY(face.getRightBtmY());
facePosition.setRbX(face.getRightBtmX());
if (ObjectUtil.isNotEmpty(subImageList) && CollUtil.isNotEmpty(subImageList.getSubImageInfoObject())) {
if (viidMode == 0) {
// 遍历每个图片对象
// 先找到type14的图片
List<SubImageInfoObject> type14ImageList = subImageList.getSubImageInfoObject().stream().filter(subImage -> "14".equals(subImage.getType())).toList();
for (SubImageInfoObject subImage : subImageList.getSubImageInfoObject()) {
// base64转换成MultipartFIle
MultipartFile file = ImageUtils.base64ToMultipartFile(subImage.getData());
String ext;
if (subImage.getFileFormat().equalsIgnoreCase("jpeg")) {
ext = "jpg";
} else {
ext = subImage.getFileFormat();
}
IStorageAdapter adapter = StorageFactory.use("faces");
// Type=11 人脸
if (subImage.getType().equals("11")) {
// 上传oss
Long newFaceSampleId = SnowFlakeUtil.getLongId();
if (Integer.valueOf(1).equals(device.getStatus())) {
FaceSampleEntity faceSample = new FaceSampleEntity();
faceSample.setId(newFaceSampleId);
faceSample.setScenicId(scenicId);
faceSample.setDeviceId(device.getId());
faceSample.setStatus(0);
faceSample.setCreateAt(shotTime);
String url = adapter.uploadFile(file, VIID_FACE, UUID.randomUUID() + "." + ext);
faceSample.setFaceUrl(url);
faceSampleMapper.add(faceSample);
ThreadPoolExecutor executor = getExecutor(scenicId);
executor.execute(() -> {
if (faceBodyAdapter != null) {
taskFaceService.assureFaceDb(faceBodyAdapter, scenicId.toString());
AddFaceResp addFaceResp = faceBodyAdapter.addFace(scenicId.toString(), faceSample.getId().toString(), url, newFaceSampleId.toString());
if (addFaceResp != null) {
faceSample.setScore(addFaceResp.getScore());
faceSampleMapper.updateScore(faceSample.getId(), addFaceResp.getScore());
}
}
if (Integer.valueOf(1).equals(deviceConfig.getEnablePreBook())) {
DynamicTaskGenerator.addTask(faceSample.getId());
}
});
}
for (SubImageInfoObject _subImage : type14ImageList) {
facePosition.setImgHeight(_subImage.getHeight());
facePosition.setImgWidth(_subImage.getWidth());
SourceEntity source = new SourceEntity();
source.setDeviceId(device.getId());
source.setScenicId(device.getScenicId());
source.setFaceSampleId(newFaceSampleId);
source.setCreateTime(shotTime);
source.setType(2);
// 上传oss
MultipartFile _file = ImageUtils.base64ToMultipartFile(_subImage.getData());
ThreadPoolExecutor executor = getExecutor(scenicId);
executor.execute(() -> {
List<DeviceCropConfig> cropConfigs = deviceConfig._getCropConfig();
for (DeviceCropConfig cropConfig : cropConfigs) {
source.setId(SnowFlakeUtil.getLongId());
String filename = StorageUtil.joinPath(PHOTO_PATH, UUID.randomUUID() + "." + ext);
MultipartFile _finalFile = _file;
if (cropConfig.getCropType() == 1) {
// 按固定位置截图
try {
_finalFile = ImageUtils.cropImage(_file, cropConfig.getTargetX(), cropConfig.getTargetY(), cropConfig.getTargetWidth(), cropConfig.getTargetHeight());
} catch (IOException e) {
log.error("裁切图片失败!", e);
} catch (RasterFormatException e) {
log.error("裁切图片出错!", e);
}
} else if (cropConfig.getCropType() == 2) {
// 按人脸位置
try {
int targetX = facePosition.getLtX() - (cropConfig.getTargetWidth() - facePosition.getWidth())/2;
int targetY = facePosition.getLtY() - (cropConfig.getTargetHeight() - facePosition.getHeight())/2;
_finalFile = ImageUtils.cropImage(_file, targetX, targetY, cropConfig.getTargetWidth(), cropConfig.getTargetHeight());
} catch (IOException e) {
log.error("裁切图片失败!", e);
} catch (RasterFormatException e) {
log.error("裁切图片出错!", e);
}
facePosition.setImgHeight(cropConfig.getTargetHeight());
facePosition.setImgWidth(cropConfig.getTargetWidth());
}
String _sourceUrl = scenicStorageAdapter.uploadFile(_finalFile, filename);
scenicStorageAdapter.setAcl(StorageAcl.PUBLIC_READ, filename);
source.setUrl(_sourceUrl);
source.setPosJson(JacksonUtil.toJSONString(facePosition));
sourceMapper.add(source);
}
});
}
log.info("人脸信息及原图{}张入库成功!设备ID:{}", type14ImageList.size(), deviceID);
}
}
} else if (viidMode == 1) {
for (SubImageInfoObject subImage : subImageList.getSubImageInfoObject()) {
// base64转换成MultipartFIle
MultipartFile file = ImageUtils.base64ToMultipartFile(subImage.getData());
String ext = subImage.getFileFormat();
if (ext.equalsIgnoreCase("jpeg")) {
ext = "jpg";
}
IStorageAdapter adapter = StorageFactory.use("faces");
// Type=14 人脸,传™的,有这么传的嘛
if (subImage.getType().equals("14")) {
// 上传oss
if (Integer.valueOf(1).equals(device.getStatus())) {
FaceSampleEntity faceSample = new FaceSampleEntity();
Long newFaceSampleId = SnowFlakeUtil.getLongId();
faceSample.setId(newFaceSampleId);
faceSample.setScenicId(scenicId);
faceSample.setDeviceId(device.getId());
faceSample.setStatus(0);
faceSample.setCreateAt(shotTime);
String url = adapter.uploadFile(file, VIID_FACE, UUID.randomUUID() + "." + ext);
faceSample.setFaceUrl(url);
faceSampleMapper.add(faceSample);
DynamicTaskGenerator.addTask(faceSample.getId());
ThreadPoolExecutor executor = getExecutor(scenicId);
executor.execute(() -> {
if (faceBodyAdapter != null) {
taskFaceService.assureFaceDb(faceBodyAdapter, scenicId.toString());
AddFaceResp addFaceResp = faceBodyAdapter.addFace(scenicId.toString(), faceSample.getId().toString(), url, newFaceSampleId.toString());
if (addFaceResp != null) {
faceSample.setScore(addFaceResp.getScore());
faceSampleMapper.updateScore(faceSample.getId(), addFaceResp.getScore());
}
}
if (Integer.valueOf(1).equals(deviceConfig.getEnablePreBook())) {
DynamicTaskGenerator.addTask(faceSample.getId());
}
});
log.info("模式1人脸信息入库成功!设备ID:{}", deviceID);
}
}
}
}
}
}
return new VIIDBaseResp(
new ResponseStatusObject(faceId, "/VIID/Faces", "0", "OK", sdfTime.format(new Date()))
);
}
@RequestMapping(value = "/Images", method = RequestMethod.POST)
@IgnoreLogReq
public VIIDBaseResp images(HttpServletRequest request, @RequestBody ImageUploadReq req) throws IOException {
return new VIIDBaseResp(
new ResponseStatusObject("1", "/VIID/Images", "0", "OK", sdfTime.format(new Date()))
);
}
}

View File

@@ -6,7 +6,6 @@ import com.ycwl.basic.constant.StorageConstant;
import com.ycwl.basic.device.entity.common.FileObject; import com.ycwl.basic.device.entity.common.FileObject;
import com.ycwl.basic.device.operator.WvpPassiveStorageOperator; import com.ycwl.basic.device.operator.WvpPassiveStorageOperator;
import com.ycwl.basic.model.wvp.WvpSyncReqVo; import com.ycwl.basic.model.wvp.WvpSyncReqVo;
import com.ycwl.basic.service.pc.DeviceService;
import com.ycwl.basic.service.pc.ScenicService; import com.ycwl.basic.service.pc.ScenicService;
import com.ycwl.basic.storage.adapters.IStorageAdapter; import com.ycwl.basic.storage.adapters.IStorageAdapter;
import com.ycwl.basic.storage.enums.StorageAcl; import com.ycwl.basic.storage.enums.StorageAcl;
@@ -30,15 +29,12 @@ import java.util.List;
@RequestMapping("/wvp/v1/") @RequestMapping("/wvp/v1/")
public class WvpController { public class WvpController {
@Autowired
private DeviceService deviceService;
@Autowired @Autowired
private ScenicService scenicService; private ScenicService scenicService;
@IgnoreLogReq @IgnoreLogReq
@PostMapping("/scenic/{scenicId}/sync") @PostMapping("/scenic/{scenicId}/sync")
public ApiResponse<List<WvpPassiveStorageOperator.Task>> sync(@PathVariable("scenicId") Long scenicId, @RequestBody WvpSyncReqVo reqVo) { public ApiResponse<List<WvpPassiveStorageOperator.Task>> sync(@PathVariable("scenicId") Long scenicId, @RequestBody WvpSyncReqVo reqVo) {
deviceService.updateDevices(scenicId, reqVo);
return ApiResponse.success(WvpPassiveStorageOperator.getTaskListByScenicId(scenicId)); return ApiResponse.success(WvpPassiveStorageOperator.getTaskListByScenicId(scenicId));
} }

View File

@@ -11,10 +11,13 @@ import com.ycwl.basic.device.operator.VptPassiveStorageOperator;
import com.ycwl.basic.device.operator.WvpActiveStorageOperator; import com.ycwl.basic.device.operator.WvpActiveStorageOperator;
import com.ycwl.basic.device.operator.WvpPassiveStorageOperator; import com.ycwl.basic.device.operator.WvpPassiveStorageOperator;
import com.ycwl.basic.model.pc.device.entity.DeviceConfigEntity; import com.ycwl.basic.model.pc.device.entity.DeviceConfigEntity;
import com.ycwl.basic.integration.device.dto.device.DeviceV2DTO;
import com.ycwl.basic.model.pc.device.entity.DeviceEntity; import com.ycwl.basic.model.pc.device.entity.DeviceEntity;
import java.time.ZoneId;
import java.util.Date;
public class DeviceFactory { public class DeviceFactory {
public static IDeviceStorageOperator getDeviceStorageOperator(DeviceEntity device, DeviceConfigEntity config) { public static IDeviceStorageOperator getDeviceStorageOperator(DeviceV2DTO device, DeviceConfigEntity config) {
IDeviceStorageOperator operator = null; IDeviceStorageOperator operator = null;
if (config == null) { if (config == null) {
return null; return null;
@@ -35,11 +38,33 @@ public class DeviceFactory {
if (operator == null) { if (operator == null) {
return null; return null;
} }
operator.setDevice(device); operator.setDevice(convertToEntity(device));
operator.setDeviceConfig(config); operator.setDeviceConfig(config);
return operator; return operator;
} }
/**
* 将DeviceV2DTO转换为DeviceEntity
*/
private static DeviceEntity convertToEntity(DeviceV2DTO dto) {
if (dto == null) {
return null;
}
DeviceEntity entity = new DeviceEntity();
entity.setId(dto.getId());
entity.setName(dto.getName());
entity.setNo(dto.getNo());
entity.setScenicId(dto.getScenicId());
entity.setStatus(dto.getIsActive());
if (dto.getCreateTime() != null) {
entity.setCreateAt(dto.getCreateTime());
}
if (dto.getUpdateTime() != null) {
entity.setUpdateAt(dto.getUpdateTime());
}
return entity;
}
public static IDeviceStatusChecker getDeviceStatusChecker(DeviceEntity device, DeviceConfigEntity config) { public static IDeviceStatusChecker getDeviceStatusChecker(DeviceEntity device, DeviceConfigEntity config) {
IDeviceStatusChecker checker = null; IDeviceStatusChecker checker = null;
if (config.getOnlineCheck() <= 0) { if (config.getOnlineCheck() <= 0) {

View File

@@ -0,0 +1,155 @@
package com.ycwl.basic.device.entity.common;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
/**
* 设备视频连续性检查缓存实体
* 用于存储在Redis中的检查结果
*
* @author Claude Code
* @date 2025-09-01
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class DeviceVideoContinuityCache implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 设备ID
*/
private Long deviceId;
/**
* 检查时间
*/
private Date checkTime;
/**
* 检查的开始时间
*/
private Date startTime;
/**
* 检查的结束时间
*/
private Date endTime;
/**
* 是否支持连续性检查
*/
private Boolean support;
/**
* 视频是否连续
*/
private Boolean continuous;
/**
* 视频总数
*/
private Integer totalVideos;
/**
* 总时长(毫秒)
*/
private Long totalDurationMs;
/**
* 允许的最大间隙(毫秒)
*/
private Long maxAllowedGapMs;
/**
* 间隙数量
*/
private Integer gapCount;
/**
* 间隙列表(简化版,只包含关键信息)
*/
private List<GapInfo> gaps;
/**
* 间隙信息简化类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class GapInfo implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 前一个文件名
*/
private String beforeFileName;
/**
* 后一个文件名
*/
private String afterFileName;
/**
* 间隙时长(毫秒)
*/
private Long gapMs;
/**
* 间隙开始时间
*/
private Date gapStartTime;
/**
* 间隙结束时间
*/
private Date gapEndTime;
}
/**
* 从VideoContinuityResult创建缓存对象
*
* @param deviceId 设备ID
* @param result 检查结果
* @param startTime 检查开始时间
* @param endTime 检查结束时间
* @return 缓存对象
*/
public static DeviceVideoContinuityCache fromResult(Long deviceId, VideoContinuityResult result,
Date startTime, Date endTime) {
DeviceVideoContinuityCache cache = new DeviceVideoContinuityCache();
cache.setDeviceId(deviceId);
cache.setCheckTime(new Date());
cache.setStartTime(startTime);
cache.setEndTime(endTime);
cache.setSupport(result.isSupport());
cache.setContinuous(result.isContinuous());
cache.setTotalVideos(result.getTotalVideos());
cache.setTotalDurationMs(result.getTotalDurationMs());
cache.setMaxAllowedGapMs(result.getMaxAllowedGapMs());
cache.setGapCount(result.getGapCount());
// 转换间隙列表
if (result.getGaps() != null && !result.getGaps().isEmpty()) {
List<GapInfo> gapInfos = result.getGaps().stream()
.map(gap -> new GapInfo(
gap.getBeforeFile() != null ? gap.getBeforeFile().getName() : null,
gap.getAfterFile() != null ? gap.getAfterFile().getName() : null,
gap.getGapMs(),
gap.getGapStartTime(),
gap.getGapEndTime()
))
.toList();
cache.setGaps(gapInfos);
}
return cache;
}
}

View File

@@ -0,0 +1,40 @@
package com.ycwl.basic.device.entity.common;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
/**
* 视频连续性检查中的间隙信息
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class VideoContinuityGap {
/**
* 间隙前的视频文件
*/
private FileObject beforeFile;
/**
* 间隙后的视频文件
*/
private FileObject afterFile;
/**
* 间隙时长(毫秒)
*/
private long gapMs;
/**
* 间隙开始时间(前一个视频的endTime)
*/
private Date gapStartTime;
/**
* 间隙结束时间(后一个视频的createTime)
*/
private Date gapEndTime;
}

View File

@@ -0,0 +1,56 @@
package com.ycwl.basic.device.entity.common;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 视频连续性检查结果
*/
@Data
public class VideoContinuityResult {
/**
* 是否支持连续性检查功能
*/
private boolean support;
/**
* 视频是否连续(所有间隙都在允许范围内)
*/
private boolean continuous;
/**
* 检测到的间隙列表
*/
private List<VideoContinuityGap> gaps = new ArrayList<>();
/**
* 视频文件总数
*/
private int totalVideos;
/**
* 总时长(毫秒)
*/
private long totalDurationMs;
/**
* 允许的最大间隙(毫秒)
*/
private long maxAllowedGapMs;
/**
* 添加一个间隙
*/
public void addGap(VideoContinuityGap gap) {
this.gaps.add(gap);
}
/**
* 获取间隙数量
*/
public int getGapCount() {
return gaps.size();
}
}

View File

@@ -1,12 +1,50 @@
package com.ycwl.basic.device.operator; package com.ycwl.basic.device.operator;
import com.ycwl.basic.device.entity.common.VideoContinuityResult;
import com.ycwl.basic.model.pc.device.entity.DeviceConfigEntity; import com.ycwl.basic.model.pc.device.entity.DeviceConfigEntity;
import com.ycwl.basic.model.pc.device.entity.DeviceEntity; import com.ycwl.basic.model.pc.device.entity.DeviceEntity;
import lombok.Setter; import lombok.Setter;
import java.util.Calendar;
import java.util.Date;
public abstract class ADeviceStorageOperator implements IDeviceStorageOperator { public abstract class ADeviceStorageOperator implements IDeviceStorageOperator {
@Setter @Setter
protected DeviceEntity device; protected DeviceEntity device;
@Setter @Setter
protected DeviceConfigEntity deviceConfig; protected DeviceConfigEntity deviceConfig;
/**
* 默认实现:不支持视频连续性检查
*
* @param startDate 开始时间
* @param endDate 结束时间
* @param maxGapMs 允许的最大间隔时间(毫秒)
* @return support=false的结果
*/
@Override
public VideoContinuityResult checkVideoContinuity(Date startDate, Date endDate, long maxGapMs) {
VideoContinuityResult result = new VideoContinuityResult();
result.setSupport(false);
result.setContinuous(false);
result.setTotalVideos(0);
result.setTotalDurationMs(0);
result.setMaxAllowedGapMs(maxGapMs);
return result;
}
/**
* 默认实现:不支持视频连续性检查
*
* @return support=false的结果
*/
@Override
public VideoContinuityResult checkRecentVideoContinuity() {
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.MINUTE, -2);
Date endDate = calendar.getTime();
calendar.add(Calendar.MINUTE, -5);
Date startDate = calendar.getTime();
return checkVideoContinuity(startDate, endDate, 2000L);
}
} }

View File

@@ -3,6 +3,8 @@ package com.ycwl.basic.device.operator;
import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.DateUtil;
import com.ycwl.basic.utils.JacksonUtil; import com.ycwl.basic.utils.JacksonUtil;
import com.ycwl.basic.device.entity.common.FileObject; import com.ycwl.basic.device.entity.common.FileObject;
import com.ycwl.basic.device.entity.common.VideoContinuityGap;
import com.ycwl.basic.device.entity.common.VideoContinuityResult;
import com.ycwl.basic.storage.StorageFactory; import com.ycwl.basic.storage.StorageFactory;
import com.ycwl.basic.storage.adapters.IStorageAdapter; import com.ycwl.basic.storage.adapters.IStorageAdapter;
import com.ycwl.basic.storage.entity.AliOssStorageConfig; import com.ycwl.basic.storage.entity.AliOssStorageConfig;
@@ -69,23 +71,15 @@ public class AliOssStorageOperator extends ADeviceStorageOperator {
if (startDate == null || endDate == null) { if (startDate == null || endDate == null) {
return null; return null;
} }
List<FileObject> fileList = new ArrayList<>();
if (startDate.after(endDate)) {
return fileList;
}
Calendar calendar = Calendar.getInstance(); Calendar calendar = Calendar.getInstance();
calendar.setTime(startDate); calendar.setTime(startDate);
calendar.set(Calendar.SECOND, 0); calendar.set(Calendar.SECOND, 0);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd"); SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");
while (calendar.getTime().before(endDate)) {
String prefix = dateFormat.format(calendar.getTime()); String prefix = dateFormat.format(calendar.getTime());
List<FileObject> fileListByPrefix = getOssFileListByPrefix(prefix); List<FileObject> fileList = getOssFileListByPrefix(prefix);
if (fileListByPrefix == null) { if (fileList == null) {
return null; return null;
} }
fileList.addAll(fileListByPrefix);
calendar.add(Calendar.MINUTE, 1);
}
calendar.clear(); calendar.clear();
return fileList.stream() return fileList.stream()
.sorted(Comparator.comparing(FileObject::getCreateTime)) .sorted(Comparator.comparing(FileObject::getCreateTime))
@@ -106,4 +100,104 @@ public class AliOssStorageOperator extends ADeviceStorageOperator {
String prefix = dateFormat.format(calendar.getTime()); String prefix = dateFormat.format(calendar.getTime());
return removeFilesByPrefix(prefix); return removeFilesByPrefix(prefix);
} }
/**
* 检查视频片段的连续性
*
* @param startDate 开始时间
* @param endDate 结束时间
* @param maxGapMs 允许的最大间隔时间(毫秒)
* @return 包含缺口信息的验证结果
*/
@Override
public VideoContinuityResult checkVideoContinuity(Date startDate, Date endDate, long maxGapMs) {
VideoContinuityResult result = new VideoContinuityResult();
result.setSupport(true);
result.setMaxAllowedGapMs(maxGapMs);
// 获取时间范围内的视频列表
List<FileObject> fileList = getFileListByDtRange(startDate, endDate);
if (fileList == null || fileList.isEmpty()) {
result.setContinuous(false);
result.setTotalVideos(0);
result.setTotalDurationMs(0);
log.warn("未找到指定时间范围内的视频文件: {} - {}", startDate, endDate);
return result;
}
result.setTotalVideos(fileList.size());
// 只有一个视频文件时,认为是连续的
if (fileList.size() == 1) {
FileObject file = fileList.get(0);
long duration = file.getEndTime().getTime() - file.getCreateTime().getTime();
result.setContinuous(true);
result.setTotalDurationMs(duration);
return result;
}
// 检查相邻视频之间的间隙
long totalDuration = 0;
for (int i = 0; i < fileList.size() - 1; i++) {
FileObject currentFile = fileList.get(i);
FileObject nextFile = fileList.get(i + 1);
// 计算当前视频的时长
totalDuration += currentFile.getEndTime().getTime() - currentFile.getCreateTime().getTime();
// 计算间隙: 后一个视频的开始时间 - 前一个视频的结束时间
long gapMs = nextFile.getCreateTime().getTime() - currentFile.getEndTime().getTime();
// 如果间隙超过允许值,记录该间隙
if (gapMs > maxGapMs) {
VideoContinuityGap gap = new VideoContinuityGap();
gap.setBeforeFile(currentFile);
gap.setAfterFile(nextFile);
gap.setGapMs(gapMs);
gap.setGapStartTime(currentFile.getEndTime());
gap.setGapEndTime(nextFile.getCreateTime());
result.addGap(gap);
log.debug("检测到视频间隙: {} -> {}, 间隙时长: {}ms",
currentFile.getName(), nextFile.getName(), gapMs);
}
}
// 加上最后一个视频的时长
FileObject lastFile = fileList.get(fileList.size() - 1);
totalDuration += lastFile.getEndTime().getTime() - lastFile.getCreateTime().getTime();
result.setTotalDurationMs(totalDuration);
result.setContinuous(result.getGapCount() == 0);
log.info("视频连续性检查完成: 总视频数={}, 总时长={}ms, 间隙数={}, 连续={}",
result.getTotalVideos(), result.getTotalDurationMs(), result.getGapCount(), result.isContinuous());
return result;
}
/**
* 检查近期视频的连续性(测试用)
* 时间范围: 当前时间向前2分钟后,再向前10分钟(即前12分钟到前2分钟)
* 允许的最大间隙: 2秒
*
* @return 包含缺口信息的验证结果
*/
@Override
public VideoContinuityResult checkRecentVideoContinuity() {
Calendar calendar = Calendar.getInstance();
// 结束时间: 当前时间 - 2分钟
calendar.add(Calendar.MINUTE, -2);
Date endDate = calendar.getTime();
// 开始时间: 当前时间 - 12分钟 (再向前10分钟)
calendar.add(Calendar.MINUTE, -10);
Date startDate = calendar.getTime();
log.info("检查近期视频连续性: {} - {}", startDate, endDate);
// 允许的最大间隙为2秒(2000毫秒)
return checkVideoContinuity(startDate, endDate, 2000L);
}
} }

View File

@@ -2,6 +2,7 @@ package com.ycwl.basic.device.operator;
import com.ycwl.basic.device.IDeviceCommon; import com.ycwl.basic.device.IDeviceCommon;
import com.ycwl.basic.device.entity.common.FileObject; import com.ycwl.basic.device.entity.common.FileObject;
import com.ycwl.basic.device.entity.common.VideoContinuityResult;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
@@ -19,10 +20,29 @@ public interface IDeviceStorageOperator extends IDeviceCommon {
List<FileObject> getFileListByDtRange(Date startDate, Date endDate); List<FileObject> getFileListByDtRange(Date startDate, Date endDate);
/** /**
* 删除指定日期之前的文件不包含指定的日期当天 * 删除指定日期之前的文件,不包含指定的日期当天
* *
* @param date 指定日期不包含指定日期当天 * @param date 指定日期,不包含指定日期当天
* @return * @return
*/ */
boolean removeFilesBeforeDate(Date date); boolean removeFilesBeforeDate(Date date);
/**
* 检查视频片段的连续性
*
* @param startDate 开始时间
* @param endDate 结束时间
* @param maxGapMs 允许的最大间隔时间(毫秒)
* @return 包含缺口信息的验证结果
*/
VideoContinuityResult checkVideoContinuity(Date startDate, Date endDate, long maxGapMs);
/**
* 检查近期视频的连续性(便捷方法)
* 时间范围: 当前时间向前2分钟后,再向前5分钟(即前7分钟到前2分钟)
* 允许的最大间隙: 2秒
*
* @return 包含缺口信息的验证结果
*/
VideoContinuityResult checkRecentVideoContinuity();
} }

View File

@@ -1,10 +1,12 @@
package com.ycwl.basic.device.operator; package com.ycwl.basic.device.operator;
import com.ycwl.basic.device.entity.common.FileObject; import com.ycwl.basic.device.entity.common.FileObject;
import com.ycwl.basic.device.entity.common.VideoContinuityResult;
import com.ycwl.basic.model.pc.device.entity.DeviceConfigEntity; import com.ycwl.basic.model.pc.device.entity.DeviceConfigEntity;
import com.ycwl.basic.model.pc.device.entity.DeviceEntity; import com.ycwl.basic.model.pc.device.entity.DeviceEntity;
import lombok.Setter; import lombok.Setter;
import java.util.Calendar;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
@@ -34,4 +36,24 @@ public class LocalStorageOperator implements IDeviceStorageOperator {
return false; return false;
} }
@Override
public VideoContinuityResult checkVideoContinuity(Date startDate, Date endDate, long maxGapMs) {
VideoContinuityResult result = new VideoContinuityResult();
result.setSupport(false);
result.setContinuous(false);
result.setTotalVideos(0);
result.setTotalDurationMs(0);
result.setMaxAllowedGapMs(maxGapMs);
return result;
}
@Override
public VideoContinuityResult checkRecentVideoContinuity() {
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.MINUTE, -2);
Date endDate = calendar.getTime();
calendar.add(Calendar.MINUTE, -5);
Date startDate = calendar.getTime();
return checkVideoContinuity(startDate, endDate, 2000L);
}
} }

View File

@@ -84,7 +84,8 @@ public class VptPassiveStorageOperator extends ADeviceStorageOperator {
if (StringUtils.isNotBlank(config.getDeviceNo())) { if (StringUtils.isNotBlank(config.getDeviceNo())) {
task.deviceNo = config.getDeviceNo(); task.deviceNo = config.getDeviceNo();
} else { } else {
task.deviceNo = device.getNo2(); log.warn("设备未配置deviceNo:{}", device);
return Collections.emptyList();
} }
task.startTime = startDate; task.startTime = startDate;
task.endTime = endDate; task.endTime = endDate;

View File

@@ -81,7 +81,8 @@ public class WvpPassiveStorageOperator extends ADeviceStorageOperator {
if (StringUtils.isNotBlank(config.getDeviceNo())) { if (StringUtils.isNotBlank(config.getDeviceNo())) {
task.deviceNo = config.getDeviceNo(); task.deviceNo = config.getDeviceNo();
} else { } else {
task.deviceNo = device.getNo2(); log.warn("设备未配置deviceNo:{}", device);
return Collections.emptyList();
} }
task.startTime = startDate; task.startTime = startDate;
task.endTime = endDate; task.endTime = endDate;

View File

@@ -0,0 +1,54 @@
package com.ycwl.basic.dto;
import com.ycwl.basic.pricing.dto.ProductItem;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
/**
* 移动端下单请求DTO
*/
@Data
public class MobileOrderRequest {
/**
* 商品列表
*/
private List<ProductItem> products;
/**
* 人脸ID(必填,用于权限验证)
*/
private Long faceId;
/**
* 预期原价(用于价格验证)
*/
private BigDecimal expectedOriginalAmount;
/**
* 预期最终价格(用于价格验证)
*/
private BigDecimal expectedFinalAmount;
/**
* 是否自动使用优惠券
*/
private Boolean autoUseCoupon = true;
/**
* 用户输入的券码
*/
private String voucherCode;
/**
* 是否自动使用券码优惠
*/
private Boolean autoUseVoucher = true;
/**
* 订单备注
*/
private String remarks;
}

View File

@@ -0,0 +1,60 @@
package com.ycwl.basic.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.ycwl.basic.model.task.req.ClientStatusReqVo;
import lombok.Data;
import java.util.Date;
/**
* 带保活信息的渲染工作器DTO
*/
@Data
public class RenderWorkerWithStatusDTO {
/**
* 工作器ID
*/
private Long id;
/**
* 工作器名称
*/
private String name;
/**
* 工作器标识
*/
private String key;
/**
* 是否启用 (0-禁用,1-启用)
*/
@JsonProperty("isActive")
private Integer isActive;
/**
* 创建时间
*/
@JsonProperty("createTime")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;
/**
* 更新时间
*/
@JsonProperty("updateTime")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date updateTime;
/**
* 主机保活状态信息
*/
private ClientStatusReqVo hostStatus;
/**
* 是否在线(基于保活信息判断)
*/
private Boolean isOnline;
}

View File

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

View File

@@ -0,0 +1,65 @@
package com.ycwl.basic.enums;
/**
* 人脸视频切片状态枚举
*/
public enum FaceCutStatus {
/**
* 正在切片中
*/
CUTTING(0, "正在切片中"),
/**
* 切片已完成
*/
COMPLETED(1, "切片已完成"),
/**
* 等待用户选择模板
*/
WAITING_USER_SELECT(2, "等待用户选择模板");
private final int code;
private final String description;
FaceCutStatus(int code, String description) {
this.code = code;
this.description = description;
}
public int getCode() {
return code;
}
public String getDescription() {
return description;
}
/**
* 根据code获取枚举
*/
public static FaceCutStatus fromCode(int code) {
for (FaceCutStatus status : values()) {
if (status.code == code) {
return status;
}
}
throw new IllegalArgumentException("Unknown FaceCutStatus code: " + code);
}
/**
* 根据code获取枚举,如果不存在则返回默认值
* @param code 状态码
* @param defaultStatus 默认状态
* @return 枚举值
*/
public static FaceCutStatus fromCodeOrDefault(int code, FaceCutStatus defaultStatus) {
for (FaceCutStatus status : values()) {
if (status.code == code) {
return status;
}
}
return defaultStatus;
}
}

View File

@@ -0,0 +1,64 @@
package com.ycwl.basic.enums;
/**
* 人脸片段更新状态枚举
* 用于标记人脸对应的视频片段是否有新增更新
*/
public enum FacePieceUpdateStatus {
/**
* 有新片段
* Redis键不存在时的默认状态,代表有新的视频片段产生
*/
HAS_NEW_PIECES(0, "有新片段"),
/**
* 无新片段
* Redis键存在时的状态,代表当前没有新的视频片段
*/
NO_NEW_PIECES(1, "无新片段");
private final int code;
private final String description;
FacePieceUpdateStatus(int code, String description) {
this.code = code;
this.description = description;
}
public int getCode() {
return code;
}
public String getDescription() {
return description;
}
/**
* 根据code获取枚举
*/
public static FacePieceUpdateStatus fromCode(int code) {
for (FacePieceUpdateStatus status : values()) {
if (status.code == code) {
return status;
}
}
throw new IllegalArgumentException("Unknown FacePieceUpdateStatus code: " + code);
}
/**
* 根据Redis键是否存在判断状态
* @param keyExists Redis键是否存在
* @return 键存在返回NO_NEW_PIECES,键不存在返回HAS_NEW_PIECES
*/
public static FacePieceUpdateStatus fromKeyExists(boolean keyExists) {
return keyExists ? NO_NEW_PIECES : HAS_NEW_PIECES;
}
/**
* 判断是否有新片段
*/
public boolean hasNewPieces() {
return this == HAS_NEW_PIECES;
}
}

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