Compare commits

..

522 Commits

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

5
.gitignore vendored
View File

@@ -1,6 +1,7 @@
.idea/ .idea/
logs/ logs/
target/ target/
.serena
.claude .claude
.vscode .vscode
*.jpg

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.

311
CLAUDE.md
View File

@@ -1,292 +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. 遵循现有的错误处理和日志记录模式
### 多端API架构
应用程序通过路径前缀区分不同的客户端:
- **移动端**: `/api/mobile/*` - 针对移动应用优化的接口
- **PC管理端**: `/api/*` - Web管理面板接口
- **任务处理**: `/task/*` - 后台任务和渲染服务接口
- **外部集成**: 专用集成接口(打印机、代理、viid、vpt、wvp等)
每个端点都有对应的Controller包结构,确保API的职责分离和维护性。
## 价格查询系统 (Pricing Module)
### 核心架构
价格查询系统是一个独立的业务模块,位于 `com.ycwl.basic.pricing` 包中,提供商品定价、优惠券管理和价格计算功能。
#### 关键组件
- **PriceCalculationController** (`/api/pricing/calculate`):价格计算API
- **CouponManagementController** (`/api/pricing/admin/coupons/`):优惠券管理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集成
## 关键架构模式
### Repository 层模式
项目使用Repository层抽象数据访问逻辑:
- Repository接口定义数据访问契约
- Mapper接口处理MyBatis Plus的数据库映射
- Service层通过Repository访问数据,避免直接依赖Mapper
### 异常处理架构
- **全局异常处理**: `CustomExceptionHandle` 提供统一的异常处理和响应格式
- **业务异常**: 自定义异常类继承RuntimeException,携带业务错误码
- **集成异常**: `IntegrationException` 专门处理外部服务调用异常
### 配置驱动的扩展性
通过配置文件驱动的多供应商支持:
- 存储:本地、AWS S3、阿里云 OSS
- 支付:微信支付、聪明支付
- 人脸识别:阿里云、百度
每个供应商通过统一接口访问,配置切换无需代码修改。
### 业务层架构
- **Service层**: 核心业务逻辑实现
- **Biz层**: 高级业务流程编排,组合多个Service
- **Controller层**: HTTP请求处理和响应转换
- **Repository层**: 数据访问抽象
### 认证和会话管理
- **JWT**: 使用jjwt库进行身份验证
- **Redis**: 存储会话信息和缓存
- **BaseContextHandler**: 提供当前用户上下文访问
## 微服务集成架构 (Integration Package)
### 核心架构
位于 `com.ycwl.basic.integration` 包,使用 Spring Cloud OpenFeign 和 Nacos 实现外部微服务集成。
#### 通用基础设施
- **IntegrationProperties**: 所有集成的集中配置管理
- **FeignErrorDecoder**: 自定义错误解码器,统一错误处理
- **IntegrationException**: 标准化集成异常
- **CommonResponse/PageResponse**: 外部服务响应包装器
#### 已实现的服务集成
- **Scenic Integration** (`integration.scenic`): ZT-Scenic 微服务集成
- **Device Integration** (`integration.device`): ZT-Device 微服务集成
#### 集成模式
每个外部服务按以下结构组织:
```
service/
├── client/ # Feign 客户端
├── config/ # 服务特定配置
├── dto/ # 数据传输对象
├── service/ # 业务逻辑层
└── example/ # 使用示例
```
### 配置管理
```yaml
integration:
scenic:
enabled: true
serviceName: zt-scenic
connectTimeout: 5000
readTimeout: 10000
device:
enabled: true
serviceName: zt-device
connectTimeout: 5000
readTimeout: 10000
```
### 使用模式
所有集成服务使用统一的 `handleResponse` 模式进行错误处理,确保一致的异常包装和日志记录。
### 测试集成服务
```bash
# 运行特定集成测试
mvn test -Dtest=ScenicIntegrationServiceTest
mvn test -Dtest=DeviceIntegrationServiceTest
# 运行所有集成测试
mvn test -Dtest="com.ycwl.basic.integration.*Test"
```
### 调试集成问题
启用 Feign 客户端日志:
```yaml
logging:
level:
com.ycwl.basic.integration: DEBUG
```

View File

52
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,31 @@
<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>
<!-- 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>
</dependencies> </dependencies>
<build> <build>
@@ -293,16 +324,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 +346,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,10 +1,12 @@
package com.ycwl.basic.biz; package com.ycwl.basic.biz;
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.SourceMapper;
import com.ycwl.basic.mapper.StatisticsMapper; import com.ycwl.basic.mapper.StatisticsMapper;
import com.ycwl.basic.mapper.VideoMapper; import com.ycwl.basic.mapper.VideoMapper;
import com.ycwl.basic.model.mobile.order.IsBuyBatchRespVO;
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;
@@ -13,17 +15,13 @@ 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.model.pc.order.resp.OrderAppRespVO; 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.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.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.source.entity.SourceEntity;
import com.ycwl.basic.model.pc.task.entity.TaskEntity; 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.PriceCalculationRequest;
import com.ycwl.basic.pricing.dto.PriceCalculationResult; import com.ycwl.basic.pricing.dto.PriceCalculationResult;
import com.ycwl.basic.pricing.dto.ProductItem; import com.ycwl.basic.pricing.dto.ProductItem;
@@ -38,13 +36,11 @@ 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.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.Date; import java.util.Date;
@@ -92,7 +88,6 @@ public class OrderBiz {
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);
ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId); ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId);
if (scenicConfig != null) { if (scenicConfig != null) {
if (Boolean.TRUE.equals(scenicConfig.getAllFree())) { if (Boolean.TRUE.equals(scenicConfig.getAllFree())) {
@@ -117,13 +112,19 @@ public class OrderBiz {
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.setFaceId(priceObj.getFaceId());
vlogCalculationRequest.setPreviewOnly(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
@@ -132,19 +133,35 @@ public class OrderBiz {
PriceCalculationRequest calculationRequest = new PriceCalculationRequest(); PriceCalculationRequest calculationRequest = new PriceCalculationRequest();
ProductItem productItem = new ProductItem(); ProductItem productItem = new ProductItem();
productItem.setProductType(goodsType == 1 ? ProductType.RECORDING_SET : ProductType.PHOTO_SET); productItem.setProductType(goodsType == 1 ? ProductType.RECORDING_SET : ProductType.PHOTO_SET);
productItem.setProductId(scenic.getId().toString()); productItem.setProductId(scenicId.toString());
productItem.setPurchaseCount(1); productItem.setPurchaseCount(1);
productItem.setScenicId(scenic.getId().toString()); productItem.setScenicId(scenicId.toString());
calculationRequest.setProducts(Collections.singletonList(productItem)); calculationRequest.setProducts(Collections.singletonList(productItem));
if (face != null) { if (face != null) {
calculationRequest.setUserId(face.getMemberId()); calculationRequest.setUserId(face.getMemberId());
} }
calculationRequest.setFaceId(goodsId); calculationRequest.setFaceId(goodsId);
calculationRequest.setPreviewOnly(true); // 仅查询价格,不实际使用优惠
PriceCalculationResult priceCalculationResult = iPriceCalculationService.calculatePrice(calculationRequest); PriceCalculationResult priceCalculationResult = iPriceCalculationService.calculatePrice(calculationRequest);
priceObj.setPrice(priceCalculationResult.getFinalAmount()); priceObj.setPrice(priceCalculationResult.getFinalAmount());
priceObj.setSlashPrice(priceCalculationResult.getOriginalAmount()); priceObj.setSlashPrice(priceCalculationResult.getOriginalAmount());
priceObj.setFaceId(goodsId); priceObj.setFaceId(goodsId);
break; 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.setPreviewOnly(true); // 仅查询价格,不实际使用优惠
PriceCalculationResult aiCamPriceCalculationResult = iPriceCalculationService.calculatePrice(aiCamCalculationRequest);
priceObj.setPrice(aiCamPriceCalculationResult.getFinalAmount());
priceObj.setSlashPrice(aiCamPriceCalculationResult.getOriginalAmount());
priceObj.setFaceId(goodsId);
priceObj.setScenicId(scenicId);
break;
} }
return priceObj; return priceObj;
} }
@@ -160,90 +177,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) { if (orderEntity != null) {
VideoEntity video = videoRepository.getVideo(goodsId); respVO.setOrderId(orderEntity.getId());
if (video == null) { 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; 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());
}
}
} }
} }
// 免费送逻辑,之前已经赠送了的 PriceObj priceObj = queryPrice(scenicId, goodsType, goodsId);
if (!isBuy) { if (priceObj == null) {
isBuy = switch (goodsType) { return respVO;
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) {
respVO.setOrderId(orderEntity.getId());
}
}
respVO.setBuy(isBuy);
// 还是没买
if (!isBuy) {
PriceObj priceObj = queryPrice(scenicId, goodsType, goodsId);
if (priceObj == null) {
return respVO;
}
FaceEntity face = faceRepository.getFace(priceObj.getFaceId());
respVO.setShare(true);
if (face != null && face.getMemberId().equals(userId)) {
respVO.setShare(false);
}
respVO.setFree(priceObj.isFree());
respVO.setGoodsType(goodsType);
respVO.setGoodsId(goodsId);
respVO.setOrigPrice(priceObj.getPrice());
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;
}
} }
respVO.setBuy(false);
respVO.setOrigPrice(priceObj.getPrice());
respVO.setSlashPrice(priceObj.getSlashPrice());
return respVO; return respVO;
} }
@@ -257,13 +230,20 @@ 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); // 更新完了,清理下
@@ -271,38 +251,6 @@ public class OrderBiz {
if (couponRecordId != null) { if (couponRecordId != null) {
couponBiz.userUseCoupon(order.getMemberId(), order.getFaceId(), couponRecordId, orderId); 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 = statisticsMapper.getUserRecentEnterType(order.getMemberId(), order.getCreateAt());
@@ -330,9 +278,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); // 更新完了,清理下
@@ -352,12 +302,26 @@ 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, "订单已退款");
} }
/**
* 检查用户是否购买了指定商品,并额外校验订单的faceId是否匹配
* @param userId 用户ID
* @param faceId 人脸ID
* @param goodsType 商品类型
* @param goodsId 商品ID
* @return 是否已购买且faceId匹配
*/
public boolean checkUserBuyFaceItem(Long userId, Long faceId, int goodsType, Long goodsId) {
return orderRepository.checkUserBuyFaceItem(userId, faceId, goodsType, goodsId);
}
} }

View File

@@ -7,14 +7,23 @@ 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.price.resp.SimpleGoodsRespVO;
import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity; import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity;
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.repository.FaceRepository; import com.ycwl.basic.repository.FaceRepository;
import com.ycwl.basic.repository.MemberRelationRepository;
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,16 @@ public class PriceBiz {
@Autowired @Autowired
private FaceRepository faceRepository; private FaceRepository faceRepository;
@Autowired @Autowired
@Lazy
private FaceService faceService;
@Autowired
private CouponBiz couponBiz; private CouponBiz couponBiz;
@Autowired
private MemberRelationRepository memberRelationRepository;
@Autowired
private PuzzleTemplateMapper puzzleTemplateMapper;
@Autowired
private IProductTypeCapabilityManagementService productTypeCapabilityManagementService;
public List<GoodsListRespVO> listGoodsByScenic(Long scenicId) { public List<GoodsListRespVO> listGoodsByScenic(Long scenicId) {
List<GoodsListRespVO> goodsList = new ArrayList<>(); List<GoodsListRespVO> goodsList = new ArrayList<>();
@@ -46,17 +64,129 @@ 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); ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId);
if (scenicConfig != null) { if (scenicConfig != null) {
if (!Boolean.TRUE.equals(scenicConfig.getDisableSourceVideo())) { if (!Boolean.TRUE.equals(scenicConfig.getDisableSourceVideo())) {
goodsList.add(new GoodsListRespVO(1L, "录像集")); goodsList.add(new GoodsListRespVO(1L, "录像集", 1));
} }
if (!Boolean.TRUE.equals(scenicConfig.getDisableSourceImage())) { if (!Boolean.TRUE.equals(scenicConfig.getDisableSourceImage())) {
goodsList.add(new GoodsListRespVO(2L, "照片集")); goodsList.add(new GoodsListRespVO(2L, "照片集", 2));
} }
} }
// 拼图
puzzleTemplateMapper.list(scenicId, null, 1).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模板
goodsList.add(new SimpleGoodsRespVO(scenicId, "pLog图<景区打包>", productType));
List<PuzzleTemplateEntity> puzzleList = puzzleTemplateMapper.list(scenicId, null, null);
puzzleList.stream()
.map(template -> new SimpleGoodsRespVO(template.getId(), template.getName(), productType))
.forEach(goodsList::add);
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 +210,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) {
@@ -138,8 +268,54 @@ 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 = orderBiz.checkUserBuyFaceItem(userId, faceId, -1, videoEntities.getFirst().getVideoId());
if (!hasPurchasedTemplate) {
allContentsPurchased = false;
break;
}
}
// 检查源文件购买状态(录像集和照片集)
if (allContentsPurchased) {
if (scenicConfig != null) {
// 检查录像集
if (!Boolean.TRUE.equals(scenicConfig.getDisableSourceVideo())) {
boolean hasPurchasedRecording = orderBiz.checkUserBuyFaceItem(userId, faceId, 1, faceId);
if (!hasPurchasedRecording) {
allContentsPurchased = false;
}
}
// 检查照片集
if (allContentsPurchased && !Boolean.TRUE.equals(scenicConfig.getDisableSourceImage())) {
boolean hasPurchasedPhoto = orderBiz.checkUserBuyFaceItem(userId, faceId, 2, faceId);
if (!hasPurchasedPhoto) {
allContentsPurchased = false;
}
}
}
}
// 如果所有内容都已购买,则认为已购买套餐
if (allContentsPurchased) {
respVO.setBuy(true);
}
}
respVO.setShare(false); respVO.setShare(false);
if (face != null && face.getMemberId().equals(userId)) { if (face == null || !face.getMemberId().equals(userId)) {
respVO.setShare(true); respVO.setShare(true);
} }
return respVO; return respVO;

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

@@ -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

@@ -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

@@ -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

@@ -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,94 @@
package com.ycwl.basic.controller;
import com.github.pagehelper.PageInfo;
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());
}
}
}

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;
@@ -114,7 +113,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());
} }

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

@@ -7,6 +7,7 @@ import com.ycwl.basic.pricing.dto.CouponClaimRequest;
import com.ycwl.basic.pricing.dto.CouponClaimResult; import com.ycwl.basic.pricing.dto.CouponClaimResult;
import com.ycwl.basic.pricing.dto.req.VoucherPrintReq; import com.ycwl.basic.pricing.dto.req.VoucherPrintReq;
import com.ycwl.basic.pricing.dto.resp.VoucherPrintResp; 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.ICouponService;
import com.ycwl.basic.pricing.service.VoucherPrintService; import com.ycwl.basic.pricing.service.VoucherPrintService;
import com.ycwl.basic.repository.FaceRepository; import com.ycwl.basic.repository.FaceRepository;
@@ -19,6 +20,9 @@ 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.math.BigDecimal;
import java.math.RoundingMode;
@RestController @RestController
@RequestMapping("/api/mobile/claim/v1") @RequestMapping("/api/mobile/claim/v1")
@AllArgsConstructor @AllArgsConstructor
@@ -80,6 +84,24 @@ public class AppClaimController {
if (claimResult.isSuccess()) { if (claimResult.isSuccess()) {
// 领到了 // 领到了
claimResp.setHasCoupon(true); 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.setCouponDesc(scenicConfig.getString("coupon_desc_for_type_" + req.getType(), "专属折扣券"));
claimResp.setCouponCountdown(scenicConfig.getString("coupon_countdown_for_type_" + req.getType(), "送你优惠,保存美好!")); claimResp.setCouponCountdown(scenicConfig.getString("coupon_countdown_for_type_" + req.getType(), "送你优惠,保存美好!"));
return ApiResponse.success(claimResp); return ApiResponse.success(claimResp);

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,7 +59,7 @@ 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);
} }
@@ -60,6 +70,18 @@ AppFaceController {
@DeleteMapping("/{faceId}") @DeleteMapping("/{faceId}")
public ApiResponse<String> deleteFace(@PathVariable("faceId") Long faceId) { public ApiResponse<String> deleteFace(@PathVariable("faceId") Long faceId) {
// 添加权限检查:验证当前用户是否拥有该 face
JwtInfo worker = JwtTokenUtil.getWorker();
Long userId = worker.getUserId();
FaceEntity face = faceRepository.getFace(faceId);
if (face == null) {
throw new BaseException("人脸数据不存在");
}
if (!face.getMemberId().equals(userId)) {
throw new BaseException("无权删除此人脸");
}
return faceService.deleteFace(faceId); return faceService.deleteFace(faceId);
} }
@@ -85,4 +107,40 @@ AppFaceController {
faceService.bindFace(faceId, userId); 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());
} }
// 源素材(原片/照片)商品列表 // 源素材(原片/照片)商品列表
@@ -99,4 +107,16 @@ public class AppGoodsController {
JwtInfo worker = JwtTokenUtil.getWorker(); 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.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

@@ -4,13 +4,13 @@ import com.github.pagehelper.PageInfo;
import com.ycwl.basic.constant.BaseContextHandler; import com.ycwl.basic.constant.BaseContextHandler;
import com.ycwl.basic.mapper.SourceMapper; import com.ycwl.basic.mapper.SourceMapper;
import com.ycwl.basic.mapper.VideoMapper; import com.ycwl.basic.mapper.VideoMapper;
import com.ycwl.basic.model.pc.order.req.CreateOrderReqVO;
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
import com.ycwl.basic.model.pc.source.req.SourceReqQuery; import com.ycwl.basic.model.pc.source.req.SourceReqQuery;
import com.ycwl.basic.model.pc.task.entity.TaskEntity; import com.ycwl.basic.model.pc.task.entity.TaskEntity;
import com.ycwl.basic.model.pc.video.entity.MemberVideoEntity; import com.ycwl.basic.model.pc.video.entity.MemberVideoEntity;
import com.ycwl.basic.model.pc.video.entity.VideoEntity;
import com.ycwl.basic.pricing.enums.ProductType; import com.ycwl.basic.pricing.enums.ProductType;
import com.ycwl.basic.repository.TemplateRepository; import com.ycwl.basic.repository.TemplateRepository;
import com.ycwl.basic.repository.VideoRepository;
import com.ycwl.basic.repository.VideoTaskRepository; import com.ycwl.basic.repository.VideoTaskRepository;
import com.ycwl.basic.service.pc.OrderService; import com.ycwl.basic.service.pc.OrderService;
import com.ycwl.basic.utils.ApiResponse; import com.ycwl.basic.utils.ApiResponse;
@@ -27,17 +27,14 @@ import com.ycwl.basic.order.dto.OrderV2PageRequest;
import com.ycwl.basic.order.dto.PaymentParamsRequest; import com.ycwl.basic.order.dto.PaymentParamsRequest;
import com.ycwl.basic.order.dto.PaymentParamsResponse; import com.ycwl.basic.order.dto.PaymentParamsResponse;
import com.ycwl.basic.order.dto.PaymentCallbackResponse; import com.ycwl.basic.order.dto.PaymentCallbackResponse;
import com.ycwl.basic.utils.JacksonUtil;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import java.math.BigDecimal;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
/** /**
* 移动端订单控制器V2 * 移动端订单控制器V2
@@ -58,6 +55,8 @@ public class AppOrderV2Controller {
private final VideoMapper videoMapper; private final VideoMapper videoMapper;
private final VideoTaskRepository videoTaskRepository; private final VideoTaskRepository videoTaskRepository;
private final TemplateRepository templateRepository; private final TemplateRepository templateRepository;
private final VideoRepository videoRepository;
private final RedisTemplate<String, Object> redisTemplate;
/** /**
* 移动端价格计算 * 移动端价格计算
@@ -80,7 +79,17 @@ public class AppOrderV2Controller {
// 验证faceId参数 // 验证faceId参数
if (request.getFaceId() == null) { if (request.getFaceId() == null) {
log.warn("移动端价格计算:faceId参数缺失"); log.warn("移动端价格计算:faceId参数缺失");
return ApiResponse.fail("faceId参数不能为空"); // return ApiResponse.fail("faceId参数不能为空");
// 兼容:兼容旧版本
ProductItem productItem = request.getProducts().getFirst();
switch (productItem.getProductType()) {
case VLOG_VIDEO -> {
VideoEntity video = videoRepository.getVideo(Long.valueOf(productItem.getProductId()));
TaskEntity task = videoTaskRepository.getTaskById(video.getTaskId());
request.setFaceId(task.getFaceId());
}
case RECORDING_SET, PHOTO_SET, AI_CAM_PHOTO_SET -> request.setFaceId(Long.valueOf(productItem.getProductId()));
}
} }
// 查询人脸信息进行权限验证 // 查询人脸信息进行权限验证
@@ -96,32 +105,11 @@ public class AppOrderV2Controller {
request.getProducts().forEach(product -> { request.getProducts().forEach(product -> {
switch (product.getProductType()) { switch (product.getProductType()) {
case VLOG_VIDEO: case VLOG_VIDEO:
AtomicInteger deviceCount = new AtomicInteger();
List<MemberVideoEntity> videoEntities = videoMapper.listRelationByFaceAndTemplate(face.getId(), Long.valueOf(product.getProductId())); List<MemberVideoEntity> videoEntities = videoMapper.listRelationByFaceAndTemplate(face.getId(), Long.valueOf(product.getProductId()));
if (videoEntities != null && !videoEntities.isEmpty()) { if (videoEntities != null && !videoEntities.isEmpty()) {
TaskEntity task = videoTaskRepository.getTaskById(videoEntities.getFirst().getTaskId()); product.setQuantity(videoTaskRepository.getTaskLensNum(videoEntities.getFirst().getTaskId()));
if (task != null) { } else {
Map<String, Object> paramJson = JacksonUtil.parseObject(task.getTaskParams(), Map.class); product.setQuantity(1);
if (paramJson == null) {
deviceCount.set(1);
} else {
List<String> templatePlaceholder = templateRepository.getTemplatePlaceholder(task.getTemplateId());
paramJson.entrySet().stream()
.filter(entry -> StringUtils.isNumeric(entry.getKey()))
.forEach(entry -> {
List<Object> jsonArray = JacksonUtil.parseArray(JacksonUtil.toJSONString(entry.getValue()), Object.class);
if (jsonArray != null && !jsonArray.isEmpty()) {
for (Object ignored : jsonArray) {
if (templatePlaceholder.contains(entry.getKey())) {
deviceCount.getAndIncrement();
templatePlaceholder.remove(entry.getKey());
}
}
}
});
}
product.setQuantity(deviceCount.get());
}
} }
break; break;
case RECORDING_SET: case RECORDING_SET:
@@ -133,6 +121,14 @@ public class AppOrderV2Controller {
Integer count = sourceMapper.countUser(sourceReqQuery); Integer count = sourceMapper.countUser(sourceReqQuery);
product.setQuantity(count); product.setQuantity(count);
break; 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: default:
log.warn("未知的商品类型,跳过重复购买检查: productType={}", product.getProductType()); log.warn("未知的商品类型,跳过重复购买检查: productType={}", product.getProductType());
break; break;
@@ -210,7 +206,7 @@ public class AppOrderV2Controller {
if (cachedResult.getFinalAmount().compareTo(request.getExpectedFinalAmount()) != 0) { if (cachedResult.getFinalAmount().compareTo(request.getExpectedFinalAmount()) != 0) {
log.warn("移动端下单:价格不匹配, cached={}, expected={}, userId={}, scenicId={}", log.warn("移动端下单:价格不匹配, cached={}, expected={}, userId={}, scenicId={}",
cachedResult.getFinalAmount(), request.getExpectedFinalAmount(), currentUserId, scenicId); cachedResult.getFinalAmount(), request.getExpectedFinalAmount(), currentUserId, scenicId);
return ApiResponse.fail("请重新下单"); return ApiResponse.fail("价格信息变化,请退出后重新查询价格");
} }
// 验证原价是否匹配(可选) // 验证原价是否匹配(可选)
@@ -229,6 +225,7 @@ public class AppOrderV2Controller {
Long orderId = oldOrderService.createOrderCompact(currentUserId, request, cachedResult); Long orderId = oldOrderService.createOrderCompact(currentUserId, request, cachedResult);
return ApiResponse.success(String.valueOf(orderId)); return ApiResponse.success(String.valueOf(orderId));
} catch (Exception e) { } catch (Exception e) {
log.warn("移动端下单:订单创建失败, userId={}, scenicId={}, error={}", currentUserId, scenicId, e.getMessage(), e);
return ApiResponse.fail("订单创建失败,请稍后重试"); return ApiResponse.fail("订单创建失败,请稍后重试");
} }
@@ -275,36 +272,21 @@ public class AppOrderV2Controller {
} }
/** /**
* 用户查询自己的订单详情 * 查询订单详情
*/ */
@GetMapping("/detail/{orderId}") @GetMapping("/detail/{orderId}")
public ApiResponse<OrderV2DetailResponse> getUserOrderDetail(@PathVariable("orderId") Long orderId) { public ApiResponse<OrderV2DetailResponse> getUserOrderDetail(@PathVariable("orderId") Long orderId) {
String currentUserIdStr = BaseContextHandler.getUserId(); log.info("查询订单详情: orderId={}", orderId);
if (currentUserIdStr == null) {
log.warn("用户未登录");
return ApiResponse.fail("用户未登录");
}
Long currentUserId = Long.valueOf(currentUserIdStr);
log.info("用户查询订单详情: userId={}, orderId={}", currentUserId, orderId);
try { try {
OrderV2DetailResponse detail = orderService.getOrderDetail(orderId); OrderV2DetailResponse detail = orderService.getOrderDetail(orderId);
if (detail == null) { if (detail == null) {
return ApiResponse.fail("订单不存在"); return ApiResponse.fail("订单不存在");
} }
// 验证订单是否属于当前用户
if (!currentUserId.equals(detail.getMemberId())) {
log.warn("用户尝试访问他人订单: userId={}, orderId={}, orderOwner={}",
currentUserId, orderId, detail.getMemberId());
return ApiResponse.fail("无权访问该订单");
}
return ApiResponse.success(detail); return ApiResponse.success(detail);
} catch (Exception e) { } catch (Exception e) {
log.error("查询用户订单详情失败: userId={}, orderId={}", currentUserId, orderId, e); log.error("查询订单详情失败: orderId={}", orderId, e);
return ApiResponse.fail("查询失败:" + e.getMessage()); return ApiResponse.fail("查询失败:" + e.getMessage());
} }
} }
@@ -369,4 +351,9 @@ public class AppOrderV2Controller {
return "FAIL"; return "FAIL";
} }
} }
@GetMapping("/downloadable/{orderId}")
public ApiResponse<Boolean> getDownloadableOrder(@PathVariable("orderId") Long orderId) {
return ApiResponse.success(!redisTemplate.hasKey("order_content_not_downloadable_" + orderId));
}
} }

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,237 @@
package com.ycwl.basic.controller.mobile;
import com.ycwl.basic.biz.OrderBiz;
import com.ycwl.basic.constant.SourceType;
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.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.entity.PuzzleGenerationRecordEntity;
import com.ycwl.basic.puzzle.mapper.PuzzleGenerationRecordMapper;
import com.ycwl.basic.repository.FaceRepository;
import com.ycwl.basic.service.printer.PrinterService;
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.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/mobile/puzzle/v1")
@RequiredArgsConstructor
public class AppPuzzleController {
private final PuzzleGenerationRecordMapper recordMapper;
private final FaceRepository faceRepository;
private final IPriceCalculationService iPriceCalculationService;
private final PrinterService printerService;
private final OrderBiz orderBiz;
/**
* 根据faceId查询三拼图数量
*/
@GetMapping("/count/{faceId}")
public ApiResponse<Integer> countByFaceId(@PathVariable("faceId") Long faceId) {
if (faceId == null) {
return ApiResponse.fail("faceId不能为空");
}
int count = recordMapper.countByFaceId(faceId);
return ApiResponse.success(count);
}
/**
* 根据faceId查询所有三拼图记录
*/
@GetMapping("/list/{faceId}")
public ApiResponse<List<ContentPageVO>> listByFaceId(@PathVariable("faceId") Long faceId) {
if (faceId == null) {
return ApiResponse.fail("faceId不能为空");
}
List<PuzzleGenerationRecordEntity> records = recordMapper.listByFaceId(faceId);
List<ContentPageVO> result = records.stream()
.map(this::convertToContentPageVO)
.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 = recordMapper.getById(recordId);
if (record == null) {
return ApiResponse.fail("未找到对应的拼图记录");
}
ContentPageVO result = convertToContentPageVO(record);
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 = recordMapper.getById(recordId);
if (record == null) {
return ApiResponse.fail("未找到对应的拼图记录");
}
String resultImageUrl = record.getResultImageUrl();
if (resultImageUrl == null || resultImageUrl.isEmpty()) {
return ApiResponse.fail("该拼图记录没有可用的图片URL");
}
return ApiResponse.success(Collections.singletonList(resultImageUrl));
}
/**
* 根据recordId查询拼图价格
*/
@GetMapping("/price/{recordId}")
public ApiResponse<PriceCalculationResult> getPriceByRecordId(@PathVariable("recordId") Long recordId) {
if (recordId == null) {
return ApiResponse.fail("recordId不能为空");
}
PuzzleGenerationRecordEntity record = recordMapper.getById(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 = recordMapper.getById(recordId);
if (record == null) {
return ApiResponse.fail("未找到对应的拼图记录");
}
// 检查是否有图片URL
String resultImageUrl = record.getResultImageUrl();
if (resultImageUrl == null || resultImageUrl.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(),
resultImageUrl,
0L // 打印特有
);
if (memberPrintId == null) {
return ApiResponse.fail("添加到打印列表失败");
}
return ApiResponse.success(memberPrintId);
}
/**
* 将PuzzleGenerationRecordEntity转换为ContentPageVO
*/
private ContentPageVO convertToContentPageVO(PuzzleGenerationRecordEntity record) {
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);
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);
if (calculationResult.getFinalAmount().compareTo(BigDecimal.ZERO) > 0) {
vo.setFreeCount(0);
} else {
vo.setFreeCount(1);
}
}
}
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

@@ -7,7 +7,7 @@ import com.ycwl.basic.model.mobile.scenic.ScenicAppVO;
import com.ycwl.basic.model.mobile.scenic.ScenicDeviceCountVO; import com.ycwl.basic.model.mobile.scenic.ScenicDeviceCountVO;
import com.ycwl.basic.model.mobile.scenic.ScenicIndexVO; import com.ycwl.basic.model.mobile.scenic.ScenicIndexVO;
import com.ycwl.basic.model.mobile.scenic.content.ContentPageVO; import com.ycwl.basic.model.mobile.scenic.content.ContentPageVO;
import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity; import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
import com.ycwl.basic.model.pc.scenic.entity.ScenicEntity; import com.ycwl.basic.model.pc.scenic.entity.ScenicEntity;
import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery; import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery;
import com.ycwl.basic.model.pc.scenic.resp.ScenicConfigResp; import com.ycwl.basic.model.pc.scenic.resp.ScenicConfigResp;
@@ -48,6 +48,7 @@ public class AppScenicController {
add("3932535453961555968"); add("3932535453961555968");
add("3936121342868459520"); add("3936121342868459520");
add("3936940597855784960"); add("3936940597855784960");
add("4049850382325780480");
}}; }};
// 分页查询景区列表 // 分页查询景区列表
@@ -70,26 +71,25 @@ public class AppScenicController {
@GetMapping("/{id}/config") @GetMapping("/{id}/config")
@IgnoreToken @IgnoreToken
public ApiResponse<ScenicConfigResp> getConfig(@PathVariable Long id){ public ApiResponse<ScenicConfigResp> getConfig(@PathVariable Long id){
ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(id); ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(id);
ScenicConfigResp resp = new ScenicConfigResp(); ScenicConfigResp resp = new ScenicConfigResp();
resp.setBookRoutine(scenicConfig.getBookRoutine()); resp.setWatermarkUrl(scenicConfig.getString("watermark_url"));
resp.setForceFinishTime(scenicConfig.getForceFinishTime()); resp.setVideoStoreDay(scenicConfig.getInteger("video_store_day"));
resp.setTourTime(scenicConfig.getTourTime()); resp.setAntiScreenRecordType(scenicConfig.getInteger("anti_screen_record_type"));
resp.setSampleStoreDay(scenicConfig.getSampleStoreDay()); resp.setGroupingEnable(scenicConfig.getBoolean("grouping_enable", false));
resp.setFaceStoreDay(scenicConfig.getFaceStoreDay()); resp.setVoucherEnable(scenicConfig.getBoolean("voucher_enable", false));
resp.setVideoStoreDay(scenicConfig.getVideoStoreDay()); resp.setShowPhotoWhenWaiting(scenicConfig.getBoolean("show_photo_when_waiting", false));
resp.setAllFree(scenicConfig.getAllFree()); resp.setImageSourcePackHint(scenicConfig.getString("image_source_pack_hint"));
resp.setDisableSourceVideo(scenicConfig.getDisableSourceVideo()); resp.setVideoSourcePackHint(scenicConfig.getString("video_source_pack_hint"));
resp.setDisableSourceImage(scenicConfig.getDisableSourceImage()); resp.setShareBeforeBuy(scenicConfig.getBoolean("share_before_buy"));
resp.setAntiScreenRecordType(scenicConfig.getAntiScreenRecordType()); resp.setFaceSelectFirst(scenicConfig.getBoolean("face_select_first", false));
resp.setVideoSourceStoreDay(scenicConfig.getVideoSourceStoreDay()); resp.setPrintEnableSource(scenicConfig.getBoolean("print_enable_source", true));
resp.setImageSourceStoreDay(scenicConfig.getImageSourceStoreDay()); resp.setPrintForceFaceUpload(scenicConfig.getBoolean("print_force_face_upload", false));
resp.setUserSourceExpireDay(scenicConfig.getUserSourceExpireDay()); resp.setPrintEnableManual(scenicConfig.getBoolean("print_enable_manual", true));
resp.setBrokerDirectRate(scenicConfig.getBrokerDirectRate()); resp.setSceneMode(scenicConfig.getInteger("scene_mode", 0));
resp.setVideoSourcePackHint(scenicConfig.getVideoSourcePackHint()); resp.setPrintEnable(scenicConfig.getBoolean("print_enable", false));
resp.setImageSourcePackHint(scenicConfig.getImageSourcePackHint()); resp.setShowMyPagePaid(scenicConfig.getBoolean("show_my_page_paid", true));
resp.setVoucherEnable(scenicConfig.getVoucherEnable()); resp.setShowMyPageUnpaid(scenicConfig.getBoolean("show_my_page_unpaid", true));
resp.setEnableVoucher(scenicConfig.getVoucherEnable()); // compactible
return ApiResponse.success(resp); return ApiResponse.success(resp);
} }

View File

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

View File

@@ -1,23 +1,93 @@
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.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.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("权限检查失败,请稍后重试");
}
}
} }

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

@@ -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,190 @@
package com.ycwl.basic.controller.mobile.notify;
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.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.List;
/**
* 用户通知授权记录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={}", req.getTemplateIds(), req.getScenicId());
try {
// 获取当前用户ID
Long memberId = JwtTokenUtil.getWorker().getUserId();
// 调用批量授权记录方法
List<UserNotificationAuthorizationService.AuthorizationRecord> records =
userNotificationAuthorizationService.batchRecordAuthorization(
memberId, req.getTemplateIds(), req.getScenicId());
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());
}
}
/**
* 获取景区通知模板ID及用户授权余额
* 复制AppWxNotifyController中的逻辑,并额外返回用户对应的授权余额
*/
@GetMapping("/{scenicId}/templates")
public ApiResponse<ScenicTemplateAuthResp> getScenicTemplatesWithAuth(@PathVariable("scenicId") Long scenicId) {
log.debug("获取景区通知模板ID及用户授权余额: scenicId={}", scenicId);
try {
// 获取当前用户ID
Long memberId = JwtTokenUtil.getWorker().getUserId();
// 获取景区的所有模板ID(复制自AppWxNotifyController的逻辑)
List<String> templateIds = 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);
}
}};
// 构建响应对象
ScenicTemplateAuthResp resp = new ScenicTemplateAuthResp();
resp.setScenicId(scenicId);
// 查询每个模板的授权余额信息
List<ScenicTemplateAuthResp.TemplateAuthInfo> templateAuthInfos = new ArrayList<>();
for (String templateId : templateIds) {
ScenicTemplateAuthResp.TemplateAuthInfo templateAuthInfo =
new ScenicTemplateAuthResp.TemplateAuthInfo();
templateAuthInfo.setTemplateId(templateId);
if (templateId.equals(scenicRepository.getVideoGeneratedTemplateId(scenicId))) {
templateAuthInfo.setTitle("视频生成通知");
templateAuthInfo.setDescription("当视频生成完成时,我们将提醒您");
} else if (templateId.equals(scenicRepository.getVideoDownloadTemplateId(scenicId))) {
templateAuthInfo.setTitle("视频下载通知");
templateAuthInfo.setDescription("当您的视频未购买时,我们将提醒您");
} else if (templateId.equals(scenicRepository.getVideoPreExpireTemplateId(scenicId))) {
templateAuthInfo.setTitle("视频即将过期通知");
templateAuthInfo.setDescription("当您的视频即将过期时,我们将提醒您及时下载");
} else {
templateAuthInfo.setTitle("未知模板类型");
templateAuthInfo.setDescription("未知的模板类型");
}
// 获取授权详情
try {
com.ycwl.basic.model.pc.notify.entity.UserNotificationAuthorizationEntity authEntity =
userNotificationAuthorizationService.checkAuthorization(memberId, templateId, scenicId);
if (authEntity != null) {
templateAuthInfo.setAuthorizationCount(authEntity.getAuthorizationCount());
templateAuthInfo.setConsumedCount(authEntity.getConsumedCount());
templateAuthInfo.setRemainingCount(authEntity.getRemainingCount());
templateAuthInfo.setHasAuthorization(authEntity.getRemainingCount() != null && authEntity.getRemainingCount() > 0);
} else {
// 没有授权记录
templateAuthInfo.setAuthorizationCount(0);
templateAuthInfo.setConsumedCount(0);
templateAuthInfo.setRemainingCount(0);
templateAuthInfo.setHasAuthorization(false);
}
} catch (Exception e) {
log.warn("获取模板授权信息失败: templateId={}, scenicId={}, memberId={}, error={}",
templateId, scenicId, memberId, e.getMessage());
// 获取失败时设置为无授权
templateAuthInfo.setAuthorizationCount(0);
templateAuthInfo.setConsumedCount(0);
templateAuthInfo.setRemainingCount(0);
templateAuthInfo.setHasAuthorization(false);
}
templateAuthInfos.add(templateAuthInfo);
}
resp.setTemplates(templateAuthInfos);
log.debug("成功获取景区通知模板ID及用户授权余额: scenicId={}, templateCount={}, memberId={}",
scenicId, templateIds.size(), memberId);
return ApiResponse.success(resp);
} catch (Exception e) {
log.error("获取景区通知模板ID及用户授权余额失败: scenicId={}", scenicId, e);
return ApiResponse.fail("获取授权信息失败: " + e.getMessage());
}
}
}

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

@@ -25,7 +25,7 @@ public class CouponController {
@GetMapping("/{scenicId}/goodsList") @GetMapping("/{scenicId}/goodsList")
public ApiResponse<List<GoodsListRespVO>> scenicGoodsList(@PathVariable Long scenicId) { public ApiResponse<List<GoodsListRespVO>> scenicGoodsList(@PathVariable Long scenicId) {
List<GoodsListRespVO> data = priceBiz.listGoodsByScenic(scenicId); List<GoodsListRespVO> data = priceBiz.listGoodsByScenic(scenicId);
data.add(new GoodsListRespVO(-1L, "一口价")); data.add(new GoodsListRespVO(-1L, "一口价", -1));
return ApiResponse.success(data); return ApiResponse.success(data);
} }

View File

@@ -1,100 +0,0 @@
package com.ycwl.basic.controller.pc;
import com.ycwl.basic.integration.scenic.dto.config.DefaultConfigDTO;
import com.ycwl.basic.integration.scenic.service.DefaultConfigIntegrationService;
import com.ycwl.basic.utils.ApiConst;
import com.ycwl.basic.utils.ApiResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 默认配置管理控制器
* 提供默认配置的增删查改功能
*/
@Slf4j
@RestController
@RequestMapping("/api/default-config")
@RequiredArgsConstructor
public class DefaultConfigController {
private final DefaultConfigIntegrationService defaultConfigIntegrationService;
/**
* 获取默认配置列表
*/
@GetMapping("/")
public ApiResponse<List<DefaultConfigDTO>> listDefaultConfigs() {
log.info("获取默认配置列表");
try {
List<DefaultConfigDTO> configs = defaultConfigIntegrationService.listDefaultConfigs();
return ApiResponse.success(configs);
} catch (Exception e) {
log.error("获取默认配置列表失败", e);
return ApiResponse.fail("获取默认配置列表失败: " + e.getMessage());
}
}
/**
* 根据配置键获取默认配置
*/
@GetMapping("/{configKey}")
public ApiResponse<DefaultConfigDTO> getDefaultConfig(@PathVariable String configKey) {
log.info("获取默认配置, configKey: {}", configKey);
try {
DefaultConfigDTO config = defaultConfigIntegrationService.getDefaultConfig(configKey);
return ApiResponse.success(config);
} catch (Exception e) {
log.error("获取默认配置失败, configKey: {}", configKey, e);
return ApiResponse.fail("获取默认配置失败: " + e.getMessage());
}
}
/**
* 创建默认配置
*/
@PostMapping("/")
public ApiResponse<DefaultConfigDTO> createDefaultConfig(@RequestBody DefaultConfigDTO request) {
log.info("创建默认配置, configKey: {}", request.getConfigKey());
try {
DefaultConfigDTO config = defaultConfigIntegrationService.createDefaultConfig(request);
return ApiResponse.success(config);
} catch (Exception e) {
log.error("创建默认配置失败, configKey: {}", request.getConfigKey(), e);
return ApiResponse.fail("创建默认配置失败: " + e.getMessage());
}
}
/**
* 更新默认配置
*/
@PutMapping("/{configKey}")
public ApiResponse<DefaultConfigDTO> updateDefaultConfig(@PathVariable String configKey,
@RequestBody DefaultConfigDTO request) {
log.info("更新默认配置, configKey: {}", configKey);
try {
DefaultConfigDTO config = defaultConfigIntegrationService.updateDefaultConfig(configKey, request);
return ApiResponse.success(config);
} catch (Exception e) {
log.error("更新默认配置失败, configKey: {}", configKey, e);
return ApiResponse.fail("更新默认配置失败: " + e.getMessage());
}
}
/**
* 删除默认配置
*/
@DeleteMapping("/{configKey}")
public ApiResponse<Void> deleteDefaultConfig(@PathVariable String configKey) {
log.info("删除默认配置, configKey: {}", configKey);
try {
defaultConfigIntegrationService.deleteDefaultConfig(configKey);
return ApiResponse.buildResponse(ApiConst.Code.CODE_SUCCESS.code(), null, "删除成功");
} catch (Exception e) {
log.error("删除默认配置失败, configKey: {}", configKey, e);
return ApiResponse.fail("删除默认配置失败: " + e.getMessage());
}
}
}

View File

@@ -1,9 +1,12 @@
package com.ycwl.basic.controller.pc; package com.ycwl.basic.controller.pc;
import com.ycwl.basic.integration.device.dto.config.*; import com.ycwl.basic.integration.device.dto.config.*;
import com.ycwl.basic.integration.common.response.PageResponse;
import com.ycwl.basic.integration.device.dto.device.*; import com.ycwl.basic.integration.device.dto.device.*;
import com.ycwl.basic.integration.device.dto.status.DeviceStatusDTO;
import com.ycwl.basic.integration.device.service.DeviceConfigIntegrationService; import com.ycwl.basic.integration.device.service.DeviceConfigIntegrationService;
import com.ycwl.basic.integration.device.service.DeviceIntegrationService; import com.ycwl.basic.integration.device.service.DeviceIntegrationService;
import com.ycwl.basic.integration.device.service.DeviceStatusIntegrationService;
import com.ycwl.basic.utils.ApiResponse; import com.ycwl.basic.utils.ApiResponse;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@@ -28,6 +31,7 @@ public class DeviceV2Controller {
private final DeviceIntegrationService deviceIntegrationService; private final DeviceIntegrationService deviceIntegrationService;
private final DeviceConfigIntegrationService deviceConfigIntegrationService; private final DeviceConfigIntegrationService deviceConfigIntegrationService;
private final DeviceStatusIntegrationService deviceStatusIntegrationService;
// ========== 设备基础 CRUD 操作 ========== // ========== 设备基础 CRUD 操作 ==========
@@ -35,7 +39,7 @@ public class DeviceV2Controller {
* 设备V2核心信息分页列表 * 设备V2核心信息分页列表
*/ */
@GetMapping("/") @GetMapping("/")
public ApiResponse<DeviceV2ListResponse> listDevices(@RequestParam(defaultValue = "1") Integer page, public ApiResponse<PageResponse<DeviceV2DTO>> listDevices(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) String name, @RequestParam(required = false) String name,
@RequestParam(required = false) String no, @RequestParam(required = false) String no,
@@ -49,9 +53,9 @@ public class DeviceV2Controller {
if (pageSize > 100) { if (pageSize > 100) {
pageSize = 100; pageSize = 100;
} }
try { try {
DeviceV2ListResponse response = deviceIntegrationService.listDevices(page, pageSize, name, no, type, isActive, scenicId); PageResponse<DeviceV2DTO> response = deviceIntegrationService.listDevices(page, pageSize, name, no, type, isActive, scenicId, null);
return ApiResponse.success(response); return ApiResponse.success(response);
} catch (Exception e) { } catch (Exception e) {
log.error("分页查询设备核心信息列表失败", e); log.error("分页查询设备核心信息列表失败", e);
@@ -59,34 +63,6 @@ public class DeviceV2Controller {
} }
} }
/**
* 设备V2带配置信息分页列表
*/
@GetMapping("/with-config")
public ApiResponse<DeviceV2WithConfigListResponse> listDevicesWithConfig(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) String name,
@RequestParam(required = false) String no,
@RequestParam(required = false) String type,
@RequestParam(required = false) Integer isActive,
@RequestParam(required = false) Long scenicId) {
log.info("分页查询设备带配置信息列表, page: {}, pageSize: {}, name: {}, no: {}, type: {}, isActive: {}, scenicId: {}",
page, pageSize, name, no, type, isActive, scenicId);
// 参数验证:限制pageSize最大值为100
if (pageSize > 100) {
pageSize = 100;
}
try {
DeviceV2WithConfigListResponse response = deviceIntegrationService.listDevicesWithConfig(page, pageSize, name, no, type, isActive, scenicId);
return ApiResponse.success(response);
} catch (Exception e) {
log.error("分页查询设备带配置信息列表失败", e);
return ApiResponse.fail("分页查询设备带配置信息列表失败: " + e.getMessage());
}
}
/** /**
* 根据ID获取设备信息 * 根据ID获取设备信息
*/ */
@@ -101,20 +77,6 @@ public class DeviceV2Controller {
} }
} }
/**
* 根据ID获取设备带配置信息
*/
@GetMapping("/{id}/with-config")
public ApiResponse<DeviceV2WithConfigDTO> getDeviceWithConfig(@PathVariable Long id) {
try {
DeviceV2WithConfigDTO device = deviceIntegrationService.getDeviceWithConfig(id);
return ApiResponse.success(device);
} catch (Exception e) {
log.error("获取设备配置信息失败, id: {}", id, e);
return ApiResponse.fail("获取设备配置信息失败: " + e.getMessage());
}
}
/** /**
* 根据设备编号获取设备信息 * 根据设备编号获取设备信息
*/ */
@@ -130,16 +92,24 @@ public class DeviceV2Controller {
} }
/** /**
* 根据设备编号获取设备带配置信息 * 根据设备ID获取设备在线状态
*/ */
@GetMapping("/no/{no}/with-config") @GetMapping("/{id}/status")
public ApiResponse<DeviceV2WithConfigDTO> getDeviceWithConfigByNo(@PathVariable String no) { public ApiResponse<DeviceStatusDTO> getDeviceOnlineStatus(@PathVariable Long id) {
log.info("获取设备在线状态, deviceId: {}", id);
try { try {
DeviceV2WithConfigDTO device = deviceIntegrationService.getDeviceWithConfigByNo(no); // 首先获取设备信息以获得设备编号
return ApiResponse.success(device); DeviceV2DTO device = deviceIntegrationService.getDevice(id);
if (device == null) {
return ApiResponse.fail("设备不存在");
}
// 使用设备编号查询在线状态
DeviceStatusDTO onlineStatus = deviceStatusIntegrationService.getDeviceStatus(device.getNo());
return ApiResponse.success(onlineStatus);
} catch (Exception e) { } catch (Exception e) {
log.error("根据设备编号获取设备配置信息失败, no: {}", no, e); log.error("获取设备在线状态失败, deviceId: {}", id, e);
return ApiResponse.fail("根据设备编号获取设备配置信息失败: " + e.getMessage()); return ApiResponse.fail("获取设备在线状态失败: " + e.getMessage());
} }
} }
@@ -301,20 +271,6 @@ public class DeviceV2Controller {
} }
} }
/**
* 获取设备扁平化配置
*/
@GetMapping("/{id}/flat-config")
public ApiResponse<Map<String, Object>> getDeviceFlatConfig(@PathVariable Long id) {
try {
Map<String, Object> config = deviceConfigIntegrationService.getDeviceFlatConfig(id);
return ApiResponse.success(config);
} catch (Exception e) {
log.error("获取设备扁平化配置失败, deviceId: {}", id, e);
return ApiResponse.fail("获取设备扁平化配置失败: " + e.getMessage());
}
}
/** /**
* 根据配置键获取配置 * 根据配置键获取配置
*/ */
@@ -345,21 +301,6 @@ public class DeviceV2Controller {
} }
} }
/**
* 根据设备编号获取扁平化配置
*/
@GetMapping("/no/{no}/flat-config")
public ApiResponse<Map<String, Object>> getDeviceFlatConfigByNo(@PathVariable String no) {
log.info("根据设备编号获取扁平化配置, deviceNo: {}", no);
try {
Map<String, Object> config = deviceConfigIntegrationService.getDeviceFlatConfigByNo(no);
return ApiResponse.success(config);
} catch (Exception e) {
log.error("根据设备编号获取扁平化配置失败, deviceNo: {}", no, e);
return ApiResponse.fail("根据设备编号获取扁平化配置失败: " + e.getMessage());
}
}
/** /**
* 创建设备配置 * 创建设备配置
*/ */
@@ -427,50 +368,19 @@ public class DeviceV2Controller {
// ========== 景区设备管理操作 ========== // ========== 景区设备管理操作 ==========
/**
* 获取景区IPC设备列表
*/
@GetMapping("/scenic/{scenicId}/ipc")
public ApiResponse<DeviceV2ListResponse> getScenicIpcDevices(@PathVariable Long scenicId,
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize) {
log.info("获取景区IPC设备列表, scenicId: {}, page: {}, pageSize: {}", scenicId, page, pageSize);
try {
DeviceV2ListResponse response = deviceIntegrationService.getScenicIpcDevices(scenicId, page, pageSize);
return ApiResponse.success(response);
} catch (Exception e) {
log.error("获取景区IPC设备列表失败, scenicId: {}", scenicId, e);
return ApiResponse.fail("获取景区IPC设备列表失败: " + e.getMessage());
}
}
/**
* 获取景区激活设备列表
*/
@GetMapping("/scenic/{scenicId}/active")
public ApiResponse<DeviceV2ListResponse> getScenicActiveDevices(@PathVariable Long scenicId,
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize) {
log.info("获取景区激活设备列表, scenicId: {}, page: {}, pageSize: {}", scenicId, page, pageSize);
try {
DeviceV2ListResponse response = deviceIntegrationService.getScenicActiveDevices(scenicId, page, pageSize);
return ApiResponse.success(response);
} catch (Exception e) {
log.error("获取景区激活设备列表失败, scenicId: {}", scenicId, e);
return ApiResponse.fail("获取景区激活设备列表失败: " + e.getMessage());
}
}
/** /**
* 获取景区所有设备列表 * 获取景区所有设备列表
*/ */
@GetMapping("/scenic/{scenicId}/all") @GetMapping("/scenic/{scenicId}")
public ApiResponse<DeviceV2ListResponse> getScenicAllDevices(@PathVariable Long 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 = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize) { @RequestParam(defaultValue = "10") Integer pageSize) {
log.info("获取景区所有设备列表, scenicId: {}, page: {}, pageSize: {}", scenicId, page, pageSize); log.info("获取景区所有设备列表, scenicId: {}, page: {}, pageSize: {}", scenicId, page, pageSize);
try { try {
DeviceV2ListResponse response = deviceIntegrationService.listDevices(page, pageSize, null, null, null, null, scenicId); PageResponse<DeviceV2DTO> response = deviceIntegrationService.listDevices(page, pageSize, name, no, type, null, scenicId, null);
return ApiResponse.success(response); return ApiResponse.success(response);
} catch (Exception e) { } catch (Exception e) {
log.error("获取景区所有设备列表失败, scenicId: {}", scenicId, e); log.error("获取景区所有设备列表失败, scenicId: {}", scenicId, e);

View File

@@ -0,0 +1,106 @@
package com.ycwl.basic.controller.pc;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ycwl.basic.device.entity.common.DeviceVideoContinuityCache;
import com.ycwl.basic.task.DeviceVideoContinuityCheckTask;
import com.ycwl.basic.utils.ApiResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;
/**
* 设备视频连续性检查控制器
* 提供查询设备视频连续性检查结果的接口
*
* @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 final RedisTemplate<String, String> redisTemplate;
private final ObjectMapper objectMapper;
private final DeviceVideoContinuityCheckTask checkTask;
/**
* 查询设备最近的视频连续性检查结果
*
* @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);
try {
DeviceVideoContinuityCache result = checkTask.manualCheck(deviceId);
return ApiResponse.success(result);
} catch (Exception e) {
log.error("手动检查设备 {} 视频连续性失败", deviceId, e);
return ApiResponse.buildResponse(500, null, "检查失败: " + e.getMessage());
}
}
/**
* 删除设备的视频连续性检查缓存
* 用于清理过期或错误的缓存数据
*
* @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());
}
}
}

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,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

@@ -9,9 +9,7 @@ import com.ycwl.basic.integration.scenic.dto.filter.ScenicFilterPageResponse;
import com.ycwl.basic.integration.scenic.dto.filter.ScenicFilterRequest; import com.ycwl.basic.integration.scenic.dto.filter.ScenicFilterRequest;
import com.ycwl.basic.integration.scenic.dto.scenic.CreateScenicRequest; import com.ycwl.basic.integration.scenic.dto.scenic.CreateScenicRequest;
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO; import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO;
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2ListResponse; import com.ycwl.basic.integration.common.response.PageResponse;
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2WithConfigDTO;
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2WithConfigListResponse;
import com.ycwl.basic.integration.scenic.dto.scenic.UpdateScenicRequest; import com.ycwl.basic.integration.scenic.dto.scenic.UpdateScenicRequest;
import com.ycwl.basic.integration.scenic.service.ScenicConfigIntegrationService; import com.ycwl.basic.integration.scenic.service.ScenicConfigIntegrationService;
import com.ycwl.basic.integration.scenic.service.ScenicIntegrationService; import com.ycwl.basic.integration.scenic.service.ScenicIntegrationService;
@@ -52,7 +50,7 @@ public class ScenicV2Controller {
* 景区V2核心信息分页列表 * 景区V2核心信息分页列表
*/ */
@GetMapping("/") @GetMapping("/")
public ApiResponse<ScenicV2ListResponse> listScenics(@RequestParam(defaultValue = "1") Integer page, public ApiResponse<PageResponse<ScenicV2DTO>> listScenics(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) Integer status, @RequestParam(required = false) Integer status,
@RequestParam(required = false) String name) { @RequestParam(required = false) String name) {
@@ -62,9 +60,9 @@ public class ScenicV2Controller {
if (pageSize > 100) { if (pageSize > 100) {
pageSize = 100; pageSize = 100;
} }
try { try {
ScenicV2ListResponse response = scenicIntegrationService.listScenics(page, pageSize, status, name); PageResponse<ScenicV2DTO> response = scenicIntegrationService.listScenics(page, pageSize, status, name, null);
return ApiResponse.success(response); return ApiResponse.success(response);
} catch (Exception e) { } catch (Exception e) {
log.error("分页查询景区核心信息列表失败", e); log.error("分页查询景区核心信息列表失败", e);
@@ -72,30 +70,6 @@ public class ScenicV2Controller {
} }
} }
/**
* 景区V2带配置信息分页列表
*/
@GetMapping("/with-config")
public ApiResponse<ScenicV2WithConfigListResponse> listScenicsWithConfig(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) Integer status,
@RequestParam(required = false) String name) {
log.info("分页查询景区带配置信息列表, page: {}, pageSize: {}, status: {}, name: {}", page, pageSize, status, name);
// 参数验证:限制pageSize最大值为100
if (pageSize > 100) {
pageSize = 100;
}
try {
ScenicV2WithConfigListResponse response = scenicIntegrationService.listScenicsWithConfig(page, pageSize, status, name);
return ApiResponse.success(response);
} catch (Exception e) {
log.error("分页查询景区带配置信息列表失败", e);
return ApiResponse.fail("分页查询景区带配置信息列表失败: " + e.getMessage());
}
}
/** /**
* 查询单个景区详情 * 查询单个景区详情
*/ */
@@ -178,11 +152,11 @@ public class ScenicV2Controller {
* 只支持根据状态筛选 * 只支持根据状态筛选
*/ */
@GetMapping("/list") @GetMapping("/list")
public ApiResponse<ScenicV2ListResponse> listScenicsByStatus(@RequestParam(required = false) Integer status) { public ApiResponse<PageResponse<ScenicV2DTO>> listScenicsByStatus(@RequestParam(required = false) Integer status) {
log.info("查询景区列表, status: {}", status); log.info("查询景区列表, status: {}", status);
try { try {
// 默认查询1000条数据,第1页 // 默认查询1000条数据,第1页
ScenicV2ListResponse scenics = scenicIntegrationService.listScenics(1, 1000, status, null); PageResponse<ScenicV2DTO> scenics = scenicIntegrationService.listScenics(1, 1000, status, null, null);
return ApiResponse.success(scenics); return ApiResponse.success(scenics);
} catch (Exception e) { } catch (Exception e) {
log.error("查询景区列表失败, status: {}", status, e); log.error("查询景区列表失败, status: {}", status, e);
@@ -193,36 +167,6 @@ public class ScenicV2Controller {
// ========== 景区配置管理 ========== // ========== 景区配置管理 ==========
/**
* 获取景区及其配置信息
*/
@GetMapping("/{scenicId}/with-config")
public ApiResponse<ScenicV2WithConfigDTO> getScenicWithConfig(@PathVariable Long scenicId) {
log.info("获取景区配置信息, scenicId: {}", scenicId);
try {
ScenicV2WithConfigDTO scenic = scenicIntegrationService.getScenicWithConfig(scenicId);
return ApiResponse.success(scenic);
} catch (Exception e) {
log.error("获取景区配置信息失败, scenicId: {}", scenicId, e);
return ApiResponse.fail("获取景区配置信息失败: " + e.getMessage());
}
}
/**
* 获取景区扁平化配置
*/
@GetMapping("/{scenicId}/flat-config")
public ApiResponse<Map<String, Object>> getScenicFlatConfig(@PathVariable Long scenicId) {
log.info("获取景区扁平化配置, scenicId: {}", scenicId);
try {
Map<String, Object> config = scenicIntegrationService.getScenicFlatConfig(scenicId);
return ApiResponse.success(config);
} catch (Exception e) {
log.error("获取景区扁平化配置失败, scenicId: {}", scenicId, e);
return ApiResponse.fail("获取景区扁平化配置失败: " + e.getMessage());
}
}
/** /**
* 获取景区配置列表 * 获取景区配置列表
*/ */
@@ -317,20 +261,4 @@ public class ScenicV2Controller {
return ApiResponse.fail("批量更新配置失败: " + e.getMessage()); return ApiResponse.fail("批量更新配置失败: " + e.getMessage());
} }
} }
/**
* 扁平化批量更新景区配置
*/
@PutMapping("/{scenicId}/flat-config")
public ApiResponse<BatchUpdateResponse> batchFlatUpdateConfigs(@PathVariable Long scenicId,
@RequestBody Map<String, Object> configs) {
log.info("扁平化批量更新景区配置, scenicId: {}, configs count: {}", scenicId, configs.size());
try {
BatchUpdateResponse response = scenicConfigIntegrationService.batchFlatUpdateConfigs(scenicId, configs);
return ApiResponse.success(response);
} catch (Exception e) {
log.error("扁平化批量更新景区配置失败, scenicId: {}", scenicId, e);
return ApiResponse.fail("扁平化批量更新配置失败: " + e.getMessage());
}
}
} }

View File

@@ -22,7 +22,6 @@ public class SourceController {
@Autowired @Autowired
private SourceService sourceService; private SourceService sourceService;
@Deprecated
// 分页查询视频源 // 分页查询视频源
@PostMapping("/page") @PostMapping("/page")
public ApiResponse pageQuery(@RequestBody SourceReqQuery sourceReqQuery) { public ApiResponse pageQuery(@RequestBody SourceReqQuery sourceReqQuery) {

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,277 @@
package com.ycwl.basic.controller.printer;
import cn.hutool.core.date.DateUtil;
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
import com.ycwl.basic.mapper.FaceMapper;
import com.ycwl.basic.model.pc.device.entity.DeviceEntity;
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
import com.ycwl.basic.model.task.resp.SearchFaceRespVo;
import com.ycwl.basic.repository.MemberRelationRepository;
import com.ycwl.basic.repository.SourceRepository;
import com.ycwl.basic.service.printer.PrinterService;
import com.ycwl.basic.service.task.TaskFaceService;
import com.ycwl.basic.storage.adapters.IStorageAdapter;
import com.ycwl.basic.annotation.IgnoreToken;
import com.ycwl.basic.storage.StorageFactory;
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.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.model.printer.FaceRecognizeWithSourcesResp;
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.service.pc.ScenicService;
import com.ycwl.basic.storage.utils.StorageUtil;
import com.ycwl.basic.utils.ApiResponse;
import com.ycwl.basic.utils.SnowFlakeUtil;
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.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import static com.ycwl.basic.constant.StorageConstant.USER_FACE;
@IgnoreToken
// 打印机大屏对接接口
@RestController
@RequestMapping("/printer/v1/tv")
@RequiredArgsConstructor
public class PrinterTvController {
private final DeviceRepository deviceRepository;
private final ScenicRepository scenicRepository;
private final FaceRepository faceRepository;
private final TaskFaceService faceService;
private final FaceService pcFaceService;
private final ScenicService scenicService;
private final SourceMapper sourceMapper;
private final FaceMapper faceMapper;
private final MemberRelationRepository memberRelationRepository;
private final SourceRepository sourceRepository;
private final PrinterService printerService;
/**
* 获取景区列表
*
* @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 {
File qrcode = new File("qrcode_face_" + faceId + ".jpg");
try {
FaceEntity face = faceRepository.getFace(faceId);
if (face == null) {
response.setStatus(404);
return;
}
MpConfigEntity scenicMpConfig = scenicRepository.getScenicMpConfig(face.getScenicId());
if (scenicMpConfig == null) {
response.setStatus(500);
return;
}
WxMpUtil.generateUnlimitedWXAQRCode(
scenicMpConfig.getAppId(),
scenicMpConfig.getAppSecret(),
"pages/videoSynthesis/bind_face",
faceId.toString(),
qrcode
);
// 设置响应头
response.setContentType("image/jpeg");
response.setHeader("Content-Disposition", "inline; filename=\"" + qrcode.getName() + "\"");
// 将二维码文件写入响应输出流
try (FileInputStream fis = new FileInputStream(qrcode);
OutputStream os = response.getOutputStream()) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
os.write(buffer, 0, bytesRead);
}
os.flush();
}
} finally {
// 删除临时文件
if (qrcode.exists()) {
qrcode.delete();
}
}
}
/**
* 根据人脸样本ID查询图像素材
*
* @param faceSampleId 人脸样本ID
* @return type=2且face_sample_id匹配的source记录
*/
@GetMapping("/{faceSampleId}/source")
public ApiResponse<SourceEntity> getSourceByFaceSampleId(@PathVariable Long faceSampleId) {
SourceEntity source = sourceMapper.getBySampleIdAndType(faceSampleId, 2);
if (source == null) {
return ApiResponse.fail("未找到对应的图像素材");
}
return ApiResponse.success(source);
}
/**
* 打印机大屏人脸识别
* 上传照片,在景区人脸库中搜索匹配的人脸样本,返回识别结果和匹配到的图像素材
*
* @param file 人脸照片文件
* @param scenicId 景区ID
* @return 人脸识别结果和匹配的source列表
*/
@PostMapping("/{scenicId}/faceRecognize")
public ApiResponse<FaceRecognizeWithSourcesResp> faceRecognize(
@RequestParam("file") MultipartFile file,
@PathVariable Long scenicId) throws Exception {
// 1. 上传人脸照片到存储
IStorageAdapter adapter = StorageFactory.use("faces");
String filePath = StorageUtil.joinPath(USER_FACE, DateUtil.format(new Date(), "yyyy-MM-dd"));
String originalFilename = file.getOriginalFilename();
String suffix = originalFilename.split("\\.", 2)[1];
String fileName = UUID.randomUUID() + "." + suffix;
String faceUrl = adapter.uploadFile(file, filePath, fileName);
// 2. 保存人脸数据到数据库
Long faceId = SnowFlakeUtil.getLongId();
FaceEntity faceEntity = new FaceEntity();
faceEntity.setId(faceId);
faceEntity.setScenicId(scenicId);
faceEntity.setFaceUrl(faceUrl);
faceEntity.setCreateAt(new Date());
faceEntity.setMemberId(0L); // 打印机大屏端没有用户ID
faceMapper.add(faceEntity);
// 3. 在景区人脸库中搜索(注意:这里使用scenicId作为数据库名,搜索的是景区内的人脸样本)
pcFaceService.matchFaceId(faceId);
// 4. 自动添加照片到打印列表,并获取添加成功的照片列表
List<SourceEntity> addedSources = printerService.autoAddPhotosToPreferPrint(faceId);
// 5. 根据自动添加结果决定返回的sources
List<SourceEntity> sources;
if (addedSources != null && !addedSources.isEmpty()) {
// 如果自动添加成功,返回添加的照片列表
sources = addedSources;
} else {
// 如果自动添加为空,按原逻辑查询匹配到的图像素材(type=2)
sources = new ArrayList<>();
List<MemberSourceEntity> memberSourceEntities = memberRelationRepository.listSourceByFaceRelation(faceId, 2);
for (MemberSourceEntity memberSourceEntity : memberSourceEntities) {
SourceEntity source = sourceRepository.getSource(memberSourceEntity.getSourceId());
if (source != null) {
sources.add(source);
}
}
}
// 6. 构造响应
FaceRecognizeWithSourcesResp resp = new FaceRecognizeWithSourcesResp();
resp.setUrl(faceUrl);
resp.setFaceId(faceId);
resp.setScenicId(scenicId);
resp.setSources(sources);
// 只有当添加了照片时才返回二维码URL
if (addedSources != null && !addedSources.isEmpty()) {
resp.setQrcodeUrl("https://zhentuai.com/printer/v1/tv/face/" + faceId + "/qrcode");
} else {
resp.setQrcodeUrl(null);
}
return ApiResponse.success(resp);
}
}

View File

@@ -1,472 +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.integration.common.manager.DeviceConfigManager;
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.integration.device.service.DeviceIntegrationService;
import com.ycwl.basic.integration.device.dto.device.CreateDeviceRequest;
import com.ycwl.basic.integration.device.dto.device.UpdateDeviceRequest;
import com.ycwl.basic.integration.device.dto.device.DeviceV2DTO;
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.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 DeviceIntegrationService deviceIntegrationService;
private static final String serverId = "00000000000000000001";
@Autowired
private SourceMapper sourceMapper;
@Autowired
private DeviceRepository deviceRepository;
@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) {
// 通过zt-device服务创建新设备
CreateDeviceRequest createRequest = new CreateDeviceRequest();
createRequest.setName(device.getName());
createRequest.setNo(device.getNo());
createRequest.setType("IPC"); // 默认类型为IPC
createRequest.setIsActive(0);
createRequest.setScenicId(0L);
createRequest.setSort(0);
try {
DeviceV2DTO createdDevice = deviceIntegrationService.createDevice(createRequest);
device.setId(createdDevice.getId());
} catch (Exception e) {
log.warn("创建设备失败,设备编号: {}, 错误: {}", deviceId, e.getMessage());
}
}
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));
// 通过zt-device服务创建新设备
CreateDeviceRequest createRequest = new CreateDeviceRequest();
createRequest.setName(device.getName());
createRequest.setNo(device.getNo());
createRequest.setType("IPC"); // 默认类型为IPC
createRequest.setIsActive(0);
createRequest.setScenicId(0L);
createRequest.setSort(0);
try {
DeviceV2DTO createdDevice = deviceIntegrationService.createDevice(createRequest);
device.setId(createdDevice.getId());
} catch (Exception e) {
log.warn("创建设备失败,设备编号: {}, 错误: {}", deviceId, e.getMessage());
}
} 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;
}
DeviceConfigManager deviceConfig = deviceRepository.getDeviceConfigManager(device.getId());
DeviceConfigEntity deviceConfigEntity = deviceRepository.getDeviceConfig(device.getId());
if (deviceConfig == null) {
log.warn("设备配置不存在:" + deviceID);
return new VIIDBaseResp(
new ResponseStatusObject(faceId, "/VIID/Faces", "0", "OK", sdfTime.format(new Date()))
);
}
Integer viidMode = deviceConfig.getInteger("viid_mode", 0);
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.getInteger("enable_pre_book"))) {
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 = deviceConfigEntity._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.getInteger("enable_pre_book"))) {
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

@@ -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,22 +71,14 @@ 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> fileList = getOssFileListByPrefix(prefix);
List<FileObject> fileListByPrefix = getOssFileListByPrefix(prefix); if (fileList == null) {
if (fileListByPrefix == null) { return null;
return null;
}
fileList.addAll(fileListByPrefix);
calendar.add(Calendar.MINUTE, 1);
} }
calendar.clear(); calendar.clear();
return fileList.stream() return fileList.stream()
@@ -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

@@ -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,290 @@
package com.ycwl.basic.face.pipeline.core;
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
import com.ycwl.basic.face.pipeline.enums.FaceMatchingScene;
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
import com.ycwl.basic.model.task.resp.SearchFaceRespVo;
import com.ycwl.basic.pipeline.core.PipelineContext;
import lombok.Getter;
import lombok.Setter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 人脸匹配管线上下文
* 在各个Stage之间传递状态和数据
*/
@Getter
@Setter
public class FaceMatchingContext implements PipelineContext {
// ==================== 核心字段(构造时必填)====================
/**
* 人脸ID(必填)
*/
private final Long faceId;
/**
* 是否新用户
*/
private final boolean isNew;
// ==================== 场景标识 ====================
/**
* 场景标识
*/
private FaceMatchingScene scene;
/**
* 手动选择的样本ID(自定义匹配场景)
*/
private List<Long> faceSampleIds;
// ==================== 中间状态 ====================
/**
* 人脸实体
*/
private FaceEntity face;
/**
* 景区配置管理器
*/
private ScenicConfigManager scenicConfig;
/**
* 人脸识别适配器
*/
private IFaceBodyAdapter faceBodyAdapter;
/**
* 人脸搜索结果
*/
private SearchFaceRespVo searchResult;
/**
* 人脸样本列表(自定义匹配场景)
*/
private List<FaceSampleEntity> faceSamples;
/**
* 匹配到的样本ID列表
*/
private List<Long> sampleListIds;
/**
* 源文件关联列表
*/
private List<MemberSourceEntity> memberSourceList;
/**
* 免费源文件ID列表
*/
private List<Long> freeSourceIds;
/**
* 人脸选择后置模式配置(自定义匹配场景)
* 0: 并集, 1: 交集, 2: 直接使用
*/
private Integer faceSelectPostMode;
// ==================== 输出结果 ====================
/**
* 最终结果
*/
private SearchFaceRespVo finalResult;
// ==================== Stage配置 ====================
/**
* Stage开关配置表
* Key: stageId, Value: 是否启用
*/
private Map<String, Boolean> stageEnabledMap = new HashMap<>();
// ==================== 构造函数(私有)====================
private FaceMatchingContext(Builder builder) {
this.faceId = builder.faceId;
this.isNew = builder.isNew;
this.scene = builder.scene;
this.faceSampleIds = builder.faceSampleIds;
}
// ==================== 静态工厂方法 ====================
/**
* 获取 Builder
*/
public static Builder builder() {
return new Builder();
}
/**
* 快速创建自动匹配场景Context
*/
public static FaceMatchingContext forAutoMatching(Long faceId, boolean isNew) {
return FaceMatchingContext.builder()
.faceId(faceId)
.isNew(isNew)
.scene(FaceMatchingScene.AUTO_MATCHING)
.build();
}
/**
* 快速创建自定义匹配场景Context
*/
public static FaceMatchingContext forCustomMatching(Long faceId, List<Long> faceSampleIds) {
return FaceMatchingContext.builder()
.faceId(faceId)
.isNew(false)
.faceSampleIds(faceSampleIds)
.scene(FaceMatchingScene.CUSTOM_MATCHING)
.build();
}
/**
* 快速创建仅识别场景Context
*/
public static FaceMatchingContext forRecognitionOnly(Long faceId) {
return FaceMatchingContext.builder()
.faceId(faceId)
.isNew(false)
.scene(FaceMatchingScene.RECOGNITION_ONLY)
.build();
}
// ==================== 业务方法 ====================
/**
* 判断指定Stage是否启用
*
* @param stageId Stage唯一标识
* @param defaultEnabled 默认值(当配置未指定时使用)
* @return true-启用, false-禁用
*/
@Override
public boolean isStageEnabled(String stageId, boolean defaultEnabled) {
return stageEnabledMap.getOrDefault(stageId, defaultEnabled);
}
/**
* 判断指定Stage是否启用(默认为false)
*
* @param stageId Stage唯一标识
* @return true-启用, false-禁用
*/
@Override
public boolean isStageEnabled(String stageId) {
return stageEnabledMap.getOrDefault(stageId, false);
}
/**
* 设置指定Stage的启用状态
*
* @param stageId Stage唯一标识
* @param enabled 是否启用
* @return this(支持链式调用)
*/
public FaceMatchingContext setStageState(String stageId, boolean enabled) {
stageEnabledMap.put(stageId, enabled);
return this;
}
/**
* 启用指定Stage
*
* @param stageId Stage唯一标识
* @return this(支持链式调用)
*/
public FaceMatchingContext enableStage(String stageId) {
stageEnabledMap.put(stageId, true);
return this;
}
/**
* 禁用指定Stage
*
* @param stageId Stage唯一标识
* @return this(支持链式调用)
*/
public FaceMatchingContext disableStage(String stageId) {
stageEnabledMap.put(stageId, false);
return this;
}
/**
* 批量设置Stage启用状态
*
* @param stages Stage配置Map(stageId -> enabled)
* @return this(支持链式调用)
*/
public FaceMatchingContext setStages(Map<String, Boolean> stages) {
if (stages != null) {
stageEnabledMap.putAll(stages);
}
return this;
}
/**
* 清空所有Stage配置
*
* @return this(支持链式调用)
*/
public FaceMatchingContext clearStages() {
stageEnabledMap.clear();
return this;
}
// ==================== Builder ====================
public static class Builder {
private Long faceId;
private boolean isNew = false;
private FaceMatchingScene scene;
private List<Long> faceSampleIds;
public Builder faceId(Long faceId) {
this.faceId = faceId;
return this;
}
public Builder isNew(boolean isNew) {
this.isNew = isNew;
return this;
}
public Builder scene(FaceMatchingScene scene) {
this.scene = scene;
return this;
}
public Builder faceSampleIds(List<Long> faceSampleIds) {
this.faceSampleIds = faceSampleIds;
return this;
}
public FaceMatchingContext build() {
// 参数校验
if (faceId == null) {
throw new IllegalArgumentException("faceId is required");
}
if (scene == null) {
throw new IllegalArgumentException("scene is required");
}
// 自定义匹配场景必须提供faceSampleIds
if (scene == FaceMatchingScene.CUSTOM_MATCHING && (faceSampleIds == null || faceSampleIds.isEmpty())) {
throw new IllegalArgumentException("faceSampleIds is required for CUSTOM_MATCHING scene");
}
return new FaceMatchingContext(this);
}
}
}

View File

@@ -0,0 +1,25 @@
package com.ycwl.basic.face.pipeline.enums;
/**
* 人脸匹配场景枚举
*/
public enum FaceMatchingScene {
/**
* 自动人脸匹配
* 新用户上传人脸后自动执行匹配,或老用户重新匹配
*/
AUTO_MATCHING,
/**
* 自定义人脸匹配
* 用户手动选择人脸样本进行匹配
*/
CUSTOM_MATCHING,
/**
* 仅识别
* 只执行人脸识别,不处理后续业务逻辑(源文件关联、任务创建等)
*/
RECOGNITION_ONLY
}

View File

@@ -0,0 +1,23 @@
package com.ycwl.basic.face.pipeline.exception;
/**
* Stage执行异常
*/
public class StageExecutionException extends RuntimeException {
private final String stageName;
public StageExecutionException(String stageName, String message) {
super(String.format("[%s] %s", stageName, message));
this.stageName = stageName;
}
public StageExecutionException(String stageName, String message, Throwable cause) {
super(String.format("[%s] %s", stageName, message), cause);
this.stageName = stageName;
}
public String getStageName() {
return stageName;
}
}

View File

@@ -0,0 +1,234 @@
package com.ycwl.basic.face.pipeline.factory;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.pipeline.core.Pipeline;
import com.ycwl.basic.pipeline.core.PipelineBuilder;
import com.ycwl.basic.face.pipeline.enums.FaceMatchingScene;
import com.ycwl.basic.face.pipeline.stages.*;
import com.ycwl.basic.service.pc.helper.ScenicConfigFacade;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 人脸匹配Pipeline工厂
* 负责为不同场景组装Pipeline
*
* 支持的场景:
* 1. 自动人脸匹配(新用户/老用户)
* 2. 自定义人脸匹配
* 3. 仅识别
*/
@Slf4j
@Component
public class FaceMatchingPipelineFactory {
// ==================== 通用 Stage(13个)====================
@Autowired
private PrepareContextStage prepareContextStage;
@Autowired
private RecordMetricsStage recordMetricsStage;
@Autowired
private FaceRecognitionStage faceRecognitionStage;
@Autowired
private FaceRecoveryStage faceRecoveryStage;
@Autowired
private UpdateFaceResultStage updateFaceResultStage;
@Autowired
private BuildSourceRelationStage buildSourceRelationStage;
@Autowired
private ProcessFreeSourceStage processFreeSourceStage;
@Autowired
private ProcessBuyStatusStage processBuyStatusStage;
@Autowired
private HandleVideoRecreationStage handleVideoRecreationStage;
@Autowired
private PersistRelationsStage persistRelationsStage;
@Autowired
private CreateTaskStage createTaskStage;
@Autowired
private SetTaskStatusStage setTaskStatusStage;
@Autowired
private GeneratePuzzleStage generatePuzzleStage;
// ==================== 自定义匹配专属 Stage(6个)====================
@Autowired
private RecordCustomMatchMetricsStage recordCustomMatchMetricsStage;
@Autowired
private LoadFaceSamplesStage loadFaceSamplesStage;
@Autowired
private CustomFaceSearchStage customFaceSearchStage;
@Autowired
private LoadMatchedSamplesStage loadMatchedSamplesStage;
@Autowired
private FilterByTimeRangeStage filterByTimeRangeStage;
@Autowired
private FilterByDevicePhotoLimitStage filterByDevicePhotoLimitStage;
@Autowired
private DeleteOldRelationsStage deleteOldRelationsStage;
// ==================== 辅助服务 ====================
@Autowired
private ScenicConfigFacade scenicConfigFacade;
/**
* 创建自动人脸匹配Pipeline
*
* @param isNew 是否新用户
* @return Pipeline
*/
public Pipeline<FaceMatchingContext> createAutoMatchingPipeline(boolean isNew) {
PipelineBuilder<FaceMatchingContext> builder = new PipelineBuilder<>("AutoMatching-" + (isNew ? "New" : "Old"));
// 1. 准备上下文
builder.addStage(prepareContextStage);
// 2. 新用户设置任务状态
if (isNew) {
builder.addStage(setTaskStatusStage);
}
// 3. 记录识别次数
builder.addStage(recordMetricsStage);
// 4. 执行人脸识别
builder.addStage(faceRecognitionStage);
// 5. 人脸识别补救
builder.addStage(faceRecoveryStage);
// 6. 更新人脸结果
builder.addStage(updateFaceResultStage);
// 7. 构建源文件关联
builder.addStage(buildSourceRelationStage);
// 8. 处理免费源文件逻辑
builder.addStage(processFreeSourceStage);
// 9. 处理购买状态
builder.addStage(processBuyStatusStage);
// 10. 处理视频重切
builder.addStage(handleVideoRecreationStage);
// 11. 持久化关联关系
builder.addStage(persistRelationsStage);
// 12. 创建任务
builder.addStage(createTaskStage);
// 13. 异步生成拼图模板
builder.addStage(generatePuzzleStage);
log.debug("创建自动人脸匹配Pipeline: isNew={}, stageCount={}", isNew, builder.build().getStageCount());
return builder.build();
}
/**
* 创建自定义人脸匹配Pipeline
*
* @return Pipeline
*/
public Pipeline<FaceMatchingContext> createCustomMatchingPipeline() {
PipelineBuilder<FaceMatchingContext> builder = new PipelineBuilder<>("CustomMatching");
// 1. 准备上下文
builder.addStage(prepareContextStage);
// 2. 记录自定义匹配次数
builder.addStage(recordCustomMatchMetricsStage);
// 3. 加载用户选择的人脸样本
builder.addStage(loadFaceSamplesStage);
// 4. 根据配置执行自定义人脸搜索
builder.addStage(customFaceSearchStage);
// 5. 加载匹配样本实体到缓存
builder.addStage(loadMatchedSamplesStage);
// 6. 应用时间范围筛选
builder.addStage(filterByTimeRangeStage);
// 7. 应用设备照片数量限制筛选
builder.addStage(filterByDevicePhotoLimitStage);
// 8. 更新人脸结果
builder.addStage(updateFaceResultStage);
// 9. 删除旧关系数据
builder.addStage(deleteOldRelationsStage);
// 10. 构建源文件关联
builder.addStage(buildSourceRelationStage);
// 11. 处理免费源文件逻辑
builder.addStage(processFreeSourceStage);
// 12. 处理购买状态
builder.addStage(processBuyStatusStage);
// 13. 处理视频重切
builder.addStage(handleVideoRecreationStage);
// 14. 持久化关联关系
builder.addStage(persistRelationsStage);
// 15. 创建任务
builder.addStage(createTaskStage);
log.debug("创建自定义人脸匹配Pipeline: stageCount={}", builder.build().getStageCount());
return builder.build();
}
/**
* 创建仅识别Pipeline
* 只执行人脸识别,不处理后续业务逻辑
*
* @return Pipeline
*/
public Pipeline<FaceMatchingContext> createRecognitionOnlyPipeline() {
PipelineBuilder<FaceMatchingContext> builder = new PipelineBuilder<>("RecognitionOnly");
// 1. 准备上下文
builder.addStage(prepareContextStage);
// 2. 执行人脸识别
builder.addStage(faceRecognitionStage);
// 3. 人脸识别补救
builder.addStage(faceRecoveryStage);
log.debug("创建仅识别Pipeline: stageCount={}", builder.build().getStageCount());
return builder.build();
}
/**
* 根据场景创建Pipeline
*
* @param scene 场景
* @param isNew 是否新用户(仅AUTO_MATCHING场景需要)
* @return Pipeline
*/
public Pipeline<FaceMatchingContext> createPipeline(FaceMatchingScene scene, boolean isNew) {
return switch (scene) {
case AUTO_MATCHING -> createAutoMatchingPipeline(isNew);
case CUSTOM_MATCHING -> createCustomMatchingPipeline();
case RECOGNITION_ONLY -> createRecognitionOnlyPipeline();
};
}
/**
* 根据Context创建Pipeline
*
* @param context 上下文
* @return Pipeline
*/
public Pipeline<FaceMatchingContext> createPipeline(FaceMatchingContext context) {
return createPipeline(context.getScene(), context.isNew());
}
}

View File

@@ -0,0 +1,156 @@
package com.ycwl.basic.face.pipeline.helper;
import cn.hutool.core.date.DateUtil;
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO;
import com.ycwl.basic.puzzle.dto.PuzzleGenerateRequest;
import com.ycwl.basic.puzzle.dto.PuzzleGenerateResponse;
import com.ycwl.basic.puzzle.dto.PuzzleTemplateDTO;
import com.ycwl.basic.puzzle.service.IPuzzleGenerateService;
import com.ycwl.basic.puzzle.service.IPuzzleTemplateService;
import com.ycwl.basic.repository.ScenicRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 拼图生成编排器
* 负责编排拼图模板的批量生成逻辑
*
* 职责:
* 1. 查询景区的所有启用拼图模板
* 2. 构建动态数据
* 3. 逐个生成拼图图片
* 4. 记录统计信息
*
* 设计说明:
* - 从GeneratePuzzleStage中抽离出来,符合"薄Stage,厚Service"原则
* - Stage只负责触发异步任务,业务逻辑由此Orchestrator承担
*/
@Slf4j
@Service
public class PuzzleGenerationOrchestrator {
@Autowired
private IPuzzleTemplateService puzzleTemplateService;
@Autowired
private IPuzzleGenerateService puzzleGenerateService;
@Autowired
private ScenicRepository scenicRepository;
/**
* 异步生成景区所有启用的拼图模板
*
* @param scenicId 景区ID
* @param faceId 人脸ID
* @param memberId 会员ID
* @param faceUrl 人脸URL
*/
public void generateAllTemplatesAsync(Long scenicId, Long faceId, Long memberId, String faceUrl) {
new Thread(() -> {
try {
log.info("开始异步生成景区拼图模板: scenicId={}, faceId={}", scenicId, faceId);
// 1. 查询该景区所有启用状态的拼图模板
List<PuzzleTemplateDTO> templateList = puzzleTemplateService.listTemplates(
scenicId, null, 1); // 查询启用状态的模板
if (templateList == null || templateList.isEmpty()) {
log.info("景区不存在启用的拼图模板,跳过生成: scenicId={}", scenicId);
return;
}
log.info("景区存在 {} 个启用的拼图模板,开始逐个生成: scenicId={}", templateList.size(), scenicId);
// 2. 获取景区信息用于动态数据
ScenicV2DTO scenicBasic = scenicRepository.getScenicBasic(scenicId);
// 3. 准备公共动态数据
Map<String, String> baseDynamicData = buildBaseDynamicData(faceId, faceUrl, scenicBasic);
// 4. 使用虚拟线程池并行生成所有模板
java.util.concurrent.atomic.AtomicInteger successCount = new java.util.concurrent.atomic.AtomicInteger(0);
java.util.concurrent.atomic.AtomicInteger failCount = new java.util.concurrent.atomic.AtomicInteger(0);
try (java.util.concurrent.ExecutorService executor = java.util.concurrent.Executors.newVirtualThreadPerTaskExecutor()) {
// 为每个模板创建一个异步任务
List<java.util.concurrent.CompletableFuture<Void>> futures = templateList.stream()
.map(template -> java.util.concurrent.CompletableFuture.runAsync(() -> {
try {
generateSingleTemplate(scenicId, faceId, memberId, template, baseDynamicData);
successCount.incrementAndGet();
} catch (Exception e) {
log.error("拼图生成失败: scenicId={}, templateCode={}, templateName={}",
scenicId, template.getCode(), template.getName(), e);
failCount.incrementAndGet();
}
}, executor))
.toList();
// 等待所有任务完成
java.util.concurrent.CompletableFuture.allOf(futures.toArray(new java.util.concurrent.CompletableFuture[0])).join();
}
log.info("景区拼图模板批量生成完成: scenicId={}, 总数={}, 成功={}, 失败={}",
scenicId, templateList.size(), successCount.get(), failCount.get());
} catch (Exception e) {
// 异步任务失败不影响主流程,仅记录日志
log.error("异步生成拼图模板失败: scenicId={}, faceId={}", scenicId, faceId, e);
}
}, "PuzzleTemplateGenerator-" + scenicId + "-" + faceId).start();
}
/**
* 构建基础动态数据
*/
private Map<String, String> buildBaseDynamicData(Long faceId, String faceUrl, ScenicV2DTO scenicBasic) {
Map<String, String> baseDynamicData = new HashMap<>();
if (faceUrl != null) {
baseDynamicData.put("faceImage", faceUrl);
baseDynamicData.put("userAvatar", faceUrl);
}
baseDynamicData.put("faceId", String.valueOf(faceId));
baseDynamicData.put("scenicName", scenicBasic.getName());
baseDynamicData.put("scenicText", scenicBasic.getName());
baseDynamicData.put("dateStr", DateUtil.format(new Date(), "yyyy.MM.dd"));
return baseDynamicData;
}
/**
* 生成单个拼图模板
*/
private void generateSingleTemplate(Long scenicId, Long faceId, Long memberId,
PuzzleTemplateDTO template,
Map<String, String> baseDynamicData) {
log.info("开始生成拼图: scenicId={}, templateCode={}, templateName={}",
scenicId, template.getCode(), template.getName());
// 构建生成请求
PuzzleGenerateRequest generateRequest = new PuzzleGenerateRequest();
generateRequest.setScenicId(scenicId);
generateRequest.setUserId(memberId);
generateRequest.setFaceId(faceId);
generateRequest.setBusinessType("face_matching");
generateRequest.setTemplateCode(template.getCode());
generateRequest.setOutputFormat("PNG");
generateRequest.setQuality(90);
generateRequest.setDynamicData(new HashMap<>(baseDynamicData));
generateRequest.setRequireRuleMatch(true);
// 调用拼图生成服务
PuzzleGenerateResponse response = puzzleGenerateService.generate(generateRequest);
log.info("拼图生成成功: scenicId={}, templateCode={}, imageUrl={}",
scenicId, template.getCode(), response.getImageUrl());
}
}

View File

@@ -0,0 +1,109 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.pipeline.annotation.StageConfig;
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
import com.ycwl.basic.pipeline.core.StageResult;
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
import com.ycwl.basic.service.pc.processor.SourceRelationProcessor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 构建源文件关联Stage
* 负责根据匹配到的样本ID构建member_source关联关系
*
* 职责:
* 1. 从context.sampleListIds读取匹配的样本ID列表
* 2. 调用sourceRelationProcessor.processMemberSources()生成MemberSourceEntity列表
* 3. 更新context.memberSourceList
*
* 前置条件: context.sampleListIds不为空
* 后置条件: context.memberSourceList已设置
*/
@Slf4j
@Component
@StageConfig(
stageId = "build_source_relation",
optionalMode = StageOptionalMode.UNSUPPORT,
description = "构建源文件关联关系"
)
public class BuildSourceRelationStage extends AbstractPipelineStage<FaceMatchingContext> {
@Autowired
private SourceRelationProcessor sourceRelationProcessor;
@Override
public String getName() {
return "BuildSourceRelation";
}
@Override
protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) {
// 只有当sampleListIds不为空时才执行
List<Long> sampleListIds = context.getSampleListIds();
if (sampleListIds == null || sampleListIds.isEmpty()) {
// 从searchResult中获取
if (context.getSearchResult() != null) {
sampleListIds = context.getSearchResult().getSampleListIds();
context.setSampleListIds(sampleListIds);
}
}
if (sampleListIds == null || sampleListIds.isEmpty()) {
log.debug("sampleListIds为空,跳过源文件关联,faceId={}", context.getFaceId());
return false;
}
return true;
}
@Override
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
List<Long> sampleListIds = context.getSampleListIds();
Long faceId = context.getFaceId();
// 防御性检查:sampleListIds为空
if (sampleListIds == null || sampleListIds.isEmpty()) {
// 尝试从searchResult中获取
if (context.getSearchResult() != null) {
sampleListIds = context.getSearchResult().getSampleListIds();
if (sampleListIds != null && !sampleListIds.isEmpty()) {
context.setSampleListIds(sampleListIds);
} else {
log.debug("sampleListIds为空,跳过源文件关联,faceId={}", faceId);
return StageResult.skipped("sampleListIds为空");
}
} else {
log.debug("sampleListIds为空,跳过源文件关联,faceId={}", faceId);
return StageResult.skipped("sampleListIds为空");
}
}
try {
// 处理源文件关联
List<MemberSourceEntity> memberSourceEntityList =
sourceRelationProcessor.processMemberSources(sampleListIds, context.getFace());
if (memberSourceEntityList == null || memberSourceEntityList.isEmpty()) {
log.warn("未找到有效的源文件,faceId={}, sampleListIds={}", faceId, sampleListIds);
return StageResult.skipped("未找到有效的源文件");
}
context.setMemberSourceList(memberSourceEntityList);
log.info("构建源文件关联成功: faceId={}, 关联源文件数={}", faceId, memberSourceEntityList.size());
return StageResult.success(String.format("构建了%d个源文件关联", memberSourceEntityList.size()));
} catch (Exception e) {
log.error("构建源文件关联失败,faceId={}, sampleListIds={}", faceId, sampleListIds, e);
// 源文件关联失败不影响主流程,返回降级
return StageResult.degraded("构建源文件关联失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,73 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.biz.TaskStatusBiz;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.pipeline.annotation.StageConfig;
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
import com.ycwl.basic.pipeline.core.StageResult;
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
import com.ycwl.basic.service.pc.helper.ScenicConfigFacade;
import com.ycwl.basic.service.task.TaskService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 创建任务Stage
* 负责根据配置决定是否自动创建任务
*
* 职责:
* 1. 检查face_select_first配置
* 2. 如果配置为false,则调用taskService.autoCreateTaskByFaceId()
* 3. 如果配置为true,则设置任务状态为2(等待用户选择)
*/
@Slf4j
@Component
@StageConfig(
stageId = "create_task",
optionalMode = StageOptionalMode.UNSUPPORT,
description = "根据配置创建视频任务"
)
public class CreateTaskStage extends AbstractPipelineStage<FaceMatchingContext> {
@Autowired
private ScenicConfigFacade scenicConfigFacade;
@Autowired
private TaskService taskService;
@Autowired
private TaskStatusBiz taskStatusBiz;
@Override
public String getName() {
return "CreateTask";
}
@Override
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
Long scenicId = context.getFace().getScenicId();
Long faceId = context.getFaceId();
try {
boolean faceSelectFirst = scenicConfigFacade.isFaceSelectFirst(scenicId);
if (!faceSelectFirst) {
// 配置为自动创建任务
taskService.autoCreateTaskByFaceId(faceId);
log.info("自动创建任务成功: faceId={}", faceId);
return StageResult.success("自动创建任务成功");
} else {
// 配置为等待用户选择
taskStatusBiz.setFaceCutStatus(faceId, 2);
log.debug("景区配置 face_select_first=true,跳过自动创建任务: faceId={}", faceId);
return StageResult.skipped("等待用户手动选择");
}
} catch (Exception e) {
log.error("创建任务失败,faceId={}", faceId, e);
// 任务创建失败不影响主流程,返回降级而不是失败
return StageResult.degraded("任务创建失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,121 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.exception.BaseException;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.pipeline.annotation.StageConfig;
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
import com.ycwl.basic.pipeline.core.StageResult;
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
import com.ycwl.basic.model.task.resp.SearchFaceRespVo;
import com.ycwl.basic.service.pc.helper.SearchResultMerger;
import com.ycwl.basic.service.task.TaskFaceService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* 自定义人脸搜索Stage
* 负责根据faceSelectPostMode执行不同的搜索策略
*
* 职责:
* 1. 从context.faceSelectPostMode读取配置
* 2. 模式2: 直接使用用户选择的样本,不搜索
* 3. 模式0/1: 对每个样本搜索,然后合并结果
* 4. 更新context.searchResult
*/
@Slf4j
@Component
@StageConfig(
stageId = "custom_face_search",
optionalMode = StageOptionalMode.FORCE_ON,
description = "根据配置执行自定义人脸搜索"
)
public class CustomFaceSearchStage extends AbstractPipelineStage<FaceMatchingContext> {
@Autowired
private TaskFaceService taskFaceService;
@Autowired
private SearchResultMerger resultMerger;
@Override
public String getName() {
return "CustomFaceSearch";
}
@Override
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
Integer faceSelectPostMode = context.getFaceSelectPostMode();
List<FaceSampleEntity> faceSamples = context.getFaceSamples();
List<Long> faceSampleIds = context.getFaceSampleIds();
Long faceId = context.getFaceId();
if (faceSelectPostMode == null) {
faceSelectPostMode = 0; // 默认为并集模式
}
log.debug("face_select_post_mode配置值: {}, faceId={}", faceSelectPostMode, faceId);
try {
SearchFaceRespVo mergedResult;
// 模式2:不搜索,直接使用用户选择的faceSampleIds
if (Integer.valueOf(2).equals(faceSelectPostMode)) {
log.debug("使用模式2:直接使用用户选择的人脸样本,不进行搜索,faceId={}", faceId);
mergedResult = resultMerger.createDirectResult(faceSampleIds);
// 保留原始matchResult
if (context.getFace().getMatchResult() != null) {
mergedResult.setSearchResultJson(context.getFace().getMatchResult());
}
} else {
// 模式0(并集)和模式1(交集):需要进行搜索
List<SearchFaceRespVo> searchResults = new ArrayList<>();
for (FaceSampleEntity faceSample : faceSamples) {
try {
SearchFaceRespVo result = taskFaceService.searchFace(
context.getFaceBodyAdapter(),
String.valueOf(context.getFace().getScenicId()),
faceSample.getFaceUrl(),
"自定义人脸匹配");
if (result != null) {
searchResults.add(result);
}
} catch (Exception e) {
log.warn("人脸样本搜索失败,faceSampleId={}, faceUrl={}, faceId={}",
faceSample.getId(), faceSample.getFaceUrl(), faceId, e);
// 继续处理其他样本,不中断整个流程
}
}
if (searchResults.isEmpty()) {
log.warn("所有人脸样本搜索都失败,faceId={}, faceSampleIds={}", faceId, faceSampleIds);
throw new BaseException("人脸识别失败,请重试");
}
// 根据模式整合多个搜索结果
mergedResult = resultMerger.merge(searchResults, faceSelectPostMode);
}
context.setSearchResult(mergedResult);
context.setSampleListIds(mergedResult.getSampleListIds());
log.info("自定义人脸搜索完成: faceId={}, mode={}, 匹配数={}",
faceId, faceSelectPostMode,
mergedResult.getSampleListIds() != null ? mergedResult.getSampleListIds().size() : 0);
return StageResult.success(String.format("自定义搜索完成,模式=%d", faceSelectPostMode));
} catch (BaseException e) {
throw e;
} catch (Exception e) {
log.error("自定义人脸搜索失败,faceId={}, faceSampleIds={}", faceId, faceSampleIds, e);
return StageResult.failed("自定义人脸搜索失败", e);
}
}
}

View File

@@ -0,0 +1,74 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.pipeline.annotation.StageConfig;
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
import com.ycwl.basic.pipeline.core.StageResult;
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
import com.ycwl.basic.mapper.SourceMapper;
import com.ycwl.basic.mapper.VideoMapper;
import com.ycwl.basic.repository.MemberRelationRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 删除旧关系Stage
* 负责在保存新关系前,删除该人脸的旧数据关系
*
* 职责:
* 1. 删除member_source中该人脸的未购买关系
* 2. 删除member_video中该人脸的未购买关系
* 3. 清除缓存
*/
@Slf4j
@Component
@StageConfig(
stageId = "delete_old_relations",
optionalMode = StageOptionalMode.FORCE_ON,
description = "删除人脸旧关系数据"
)
public class DeleteOldRelationsStage extends AbstractPipelineStage<FaceMatchingContext> {
@Autowired
private SourceMapper sourceMapper;
@Autowired
private VideoMapper videoMapper;
@Autowired
private MemberRelationRepository memberRelationRepository;
@Override
public String getName() {
return "DeleteOldRelations";
}
@Override
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
Long faceId = context.getFaceId();
Long memberId = context.getFace().getMemberId();
try {
log.debug("删除人脸旧关系数据:faceId={}, memberId={}", faceId, memberId);
// 1. 删除member_source中的未购买关系
sourceMapper.deleteNotBuyFaceRelation(memberId, faceId);
// 2. 删除member_video中的未购买关系
videoMapper.deleteNotBuyFaceRelations(memberId, faceId);
// 3. 清除缓存
memberRelationRepository.clearSCacheByFace(faceId);
log.debug("人脸旧关系数据删除完成:faceId={}", faceId);
return StageResult.success("旧关系数据已删除");
} catch (Exception e) {
log.error("删除旧关系数据失败,faceId={}", faceId, e);
// 删除失败不影响主流程,返回降级
return StageResult.degraded("删除旧关系数据失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,73 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.exception.BaseException;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.pipeline.annotation.StageConfig;
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
import com.ycwl.basic.pipeline.core.StageResult;
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
import com.ycwl.basic.model.task.resp.SearchFaceRespVo;
import com.ycwl.basic.service.task.TaskFaceService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 人脸识别Stage
* 负责执行核心的人脸识别搜索
*
* 职责:
* 1. 调用taskFaceService.searchFace()执行人脸搜索
* 2. 将结果存入context.searchResult
* 3. 识别失败则返回FAILED
*/
@Slf4j
@Component
@StageConfig(
stageId = "face_recognition",
optionalMode = StageOptionalMode.FORCE_ON,
description = "执行人脸识别搜索"
)
public class FaceRecognitionStage extends AbstractPipelineStage<FaceMatchingContext> {
@Autowired
private TaskFaceService taskFaceService;
@Override
public String getName() {
return "FaceRecognition";
}
@Override
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
try {
SearchFaceRespVo searchResult = taskFaceService.searchFace(
context.getFaceBodyAdapter(),
String.valueOf(context.getFace().getScenicId()),
context.getFace().getFaceUrl(),
"人脸识别");
if (searchResult == null) {
log.warn("人脸识别返回结果为空,faceId={}", context.getFaceId());
return StageResult.failed("人脸识别失败,请换一张试试把~");
}
context.setSearchResult(searchResult);
log.info("人脸识别完成: faceId={}, score={}, 匹配数={}",
context.getFaceId(),
searchResult.getScore(),
searchResult.getSampleListIds() != null ? searchResult.getSampleListIds().size() : 0);
return StageResult.success(String.format("识别成功,匹配数=%d",
searchResult.getSampleListIds() != null ? searchResult.getSampleListIds().size() : 0));
} catch (BaseException e) {
throw e;
} catch (Exception e) {
log.error("人脸识别服务调用失败,faceId={}, scenicId={}",
context.getFaceId(), context.getFace().getScenicId(), e);
return StageResult.failed("人脸识别失败,请换一张试试把~", e);
}
}
}

View File

@@ -0,0 +1,86 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.pipeline.annotation.StageConfig;
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
import com.ycwl.basic.pipeline.core.StageResult;
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
import com.ycwl.basic.model.task.resp.SearchFaceRespVo;
import com.ycwl.basic.service.pc.processor.FaceRecoveryStrategy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 人脸识别补救Stage
* 负责执行人脸识别的补救逻辑(降级)
*
* 职责:
* 1. 从context.searchResult读取识别结果
* 2. 调用faceRecoveryStrategy.executeFaceRecoveryLogic()执行补救
* 3. 如果触发补救,更新searchResult并返回DEGRADED
* 4. 否则返回SUCCESS
*/
@Slf4j
@Component
@StageConfig(
stageId = "face_recovery",
optionalMode = StageOptionalMode.SUPPORT,
description = "执行人脸识别补救逻辑",
defaultEnabled = true
)
public class FaceRecoveryStage extends AbstractPipelineStage<FaceMatchingContext> {
@Autowired
private FaceRecoveryStrategy faceRecoveryStrategy;
@Override
public String getName() {
return "FaceRecovery";
}
@Override
protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) {
// 只有当searchResult不为空时才执行
if (context.getSearchResult() == null) {
log.debug("searchResult为空,跳过补救逻辑,faceId={}", context.getFaceId());
return false;
}
return true;
}
@Override
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
SearchFaceRespVo searchResult = context.getSearchResult();
Long faceId = context.getFaceId();
// 防御性检查:searchResult为空
if (searchResult == null) {
log.debug("searchResult为空,跳过补救逻辑,faceId={}", faceId);
return StageResult.skipped("searchResult为空");
}
try {
// 执行补救逻辑(补救逻辑内部会判断是否需要触发)
SearchFaceRespVo recoveredResult = faceRecoveryStrategy.executeFaceRecoveryLogic(
searchResult,
context.getScenicConfig(),
context.getFaceBodyAdapter(),
context.getFace().getScenicId());
// 如果结果发生变化,说明触发了补救
if (recoveredResult != searchResult) {
context.setSearchResult(recoveredResult);
log.info("触发补救逻辑,重新搜索: faceId={}", faceId);
return StageResult.degraded("触发补救逻辑,重新搜索");
}
return StageResult.success("无需补救");
} catch (Exception e) {
log.error("补救逻辑执行失败,faceId={}", faceId, e);
// 补救失败不影响主流程,返回降级
return StageResult.degraded("补救逻辑执行失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,229 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.pipeline.annotation.StageConfig;
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
import com.ycwl.basic.pipeline.core.StageResult;
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
import com.ycwl.basic.integration.common.manager.DeviceConfigManager;
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
import com.ycwl.basic.repository.DeviceRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.stream.Collectors;
/**
* 按设备照片数量限制筛选样本Stage
* 负责根据设备配置的照片数量限制(limit_photo)筛选匹配样本
*
* 职责:
* 1. 从context.faceSamples读取样本实体缓存
* 2. 按设备ID分组
* 3. 对每个设备,根据其limit_photo配置筛选样本:
* - 如果样本数 > limit_photo + 2: 按时间排序,去掉首尾,保留中间limit_photo张
* - 如果样本数 > limit_photo + 1: 按时间排序,去掉尾部,保留前limit_photo张
* - 如果样本数 > limit_photo: 保留前limit_photo张
* - 否则: 保留全部
* 4. 更新context.sampleListIds
*
* 前置条件: context.faceSamples不为空 (由LoadMatchedSamplesStage加载)
* 配置说明: limit_photo=0或null表示不限制数量
*/
@Slf4j
@Component
@StageConfig(
stageId = "filter_by_device_photo_limit",
optionalMode = StageOptionalMode.SUPPORT,
description = "按设备照片数量限制筛选样本",
defaultEnabled = true
)
public class FilterByDevicePhotoLimitStage extends AbstractPipelineStage<FaceMatchingContext> {
@Autowired
private DeviceRepository deviceRepository;
@Override
public String getName() {
return "FilterByDevicePhotoLimit";
}
@Override
protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) {
// 检查faceSamples是否为空
List<FaceSampleEntity> faceSamples = context.getFaceSamples();
if (faceSamples == null || faceSamples.isEmpty()) {
log.debug("faceSamples为空,跳过设备照片限制筛选,faceId={}", context.getFaceId());
return false;
}
return true;
}
@Override
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
List<FaceSampleEntity> faceSamples = context.getFaceSamples();
List<Long> sampleListIds = context.getSampleListIds();
Long faceId = context.getFaceId();
// 防御性检查:faceSamples为空
if (faceSamples == null || faceSamples.isEmpty()) {
log.debug("faceSamples为空,跳过设备照片限制筛选,faceId={}", faceId);
return StageResult.skipped("faceSamples为空");
}
try {
// 1. 构建样本ID到实体的映射
Map<Long, FaceSampleEntity> sampleMap = faceSamples.stream()
.collect(Collectors.toMap(FaceSampleEntity::getId, sample -> sample, (a, b) -> a));
// 2. 按设备ID分组
Map<Long, List<FaceSampleEntity>> deviceSamplesMap = new LinkedHashMap<>();
Set<Long> passthroughSampleIds = new LinkedHashSet<>();
for (Long sampleId : sampleListIds) {
FaceSampleEntity sample = sampleMap.get(sampleId);
if (sample == null || sample.getDeviceId() == null) {
passthroughSampleIds.add(sampleId); // 无设备ID的样本直接保留
continue;
}
deviceSamplesMap
.computeIfAbsent(sample.getDeviceId(), key -> new ArrayList<>())
.add(sample);
}
// 3. 对每个设备应用照片数量限制
Map<Long, Integer> limitCache = new HashMap<>();
Set<Long> retainedSampleIds = new LinkedHashSet<>(passthroughSampleIds);
for (Map.Entry<Long, List<FaceSampleEntity>> entry : deviceSamplesMap.entrySet()) {
Long deviceId = entry.getKey();
List<FaceSampleEntity> deviceSamples = entry.getValue();
// 读取设备配置
Integer limitPhoto = limitCache.computeIfAbsent(deviceId, id -> {
DeviceConfigManager deviceConfig = deviceRepository.getDeviceConfigManager(id);
return deviceConfig != null ? deviceConfig.getInteger("limit_photo") : null;
});
List<Long> retainedForDevice = applyLimitForDevice(deviceId, deviceSamples, limitPhoto);
retainedSampleIds.addAll(retainedForDevice);
}
// 4. 按原始顺序保留筛选后的样本ID
List<Long> resultIds = sampleListIds.stream()
.filter(retainedSampleIds::contains)
.collect(Collectors.toList());
// 5. 更新context
context.setSampleListIds(resultIds);
log.info("设备照片数量限制筛选完成: faceId={}, 原始样本数={}, 筛选后数={}",
faceId, sampleListIds.size(), resultIds.size());
return StageResult.success(String.format("设备限制筛选: %d → %d",
sampleListIds.size(), resultIds.size()));
} catch (Exception e) {
log.error("设备照片数量限制筛选失败,faceId={}", faceId, e);
// 筛选失败不影响主流程,返回降级
return StageResult.degraded("设备照片数量限制筛选失败: " + e.getMessage());
}
}
/**
* 对单个设备的样本应用照片数量限制
*/
private List<Long> applyLimitForDevice(Long deviceId, List<FaceSampleEntity> deviceSamples, Integer limitPhoto) {
List<Long> deviceSampleIds = deviceSamples.stream()
.map(FaceSampleEntity::getId)
.collect(Collectors.toList());
// 无限制或限制数量<=0,保留全部
if (limitPhoto == null || limitPhoto <= 0) {
log.debug("设备照片限制: 设备ID={}, 无限制, 保留{}张照片", deviceId, deviceSampleIds.size());
return deviceSampleIds;
}
int sampleCount = deviceSamples.size();
// 样本数 > limit_photo + 2: 按时间排序,去掉首尾
if (sampleCount > (limitPhoto + 2)) {
List<Long> retained = processDeviceSamples(deviceSamples, limitPhoto, true);
log.debug("设备照片限制: 设备ID={}, 限制={}张, 原始{}张, 去首尾后最终{}张",
deviceId, limitPhoto, sampleCount, retained.size());
return retained;
}
// 样本数 > limit_photo + 1: 按时间排序,去掉尾部
if (sampleCount > (limitPhoto + 1)) {
List<Long> retained = processDeviceSamples(deviceSamples, limitPhoto, false);
log.debug("设备照片限制: 设备ID={}, 限制={}张, 原始{}张, 去尾部后最终{}张",
deviceId, limitPhoto, sampleCount, retained.size());
return retained;
}
// 样本数 > limit_photo: 保留前limit_photo张
if (sampleCount > limitPhoto) {
List<Long> retained = deviceSamples.stream()
.limit(limitPhoto)
.map(FaceSampleEntity::getId)
.collect(Collectors.toList());
log.debug("设备照片限制: 设备ID={}, 限制={}张, 原始{}张, 取前{}张",
deviceId, limitPhoto, sampleCount, retained.size());
return retained;
}
// 样本数 <= limit_photo: 保留全部
log.debug("设备照片限制: 设备ID={}, 限制={}张, 原始{}张, 无需筛选, 保留全部",
deviceId, limitPhoto, sampleCount);
return deviceSampleIds;
}
/**
* 处理设备样本,根据参数决定是否去掉首尾
*
* @param deviceSamples 设备样本列表
* @param limitPhoto 限制数量
* @param removeBoth 是否去掉首尾,true去掉首尾,false只去掉尾部
* @return 处理后的样本ID列表
*/
private List<Long> processDeviceSamples(List<FaceSampleEntity> deviceSamples, int limitPhoto, boolean removeBoth) {
// 创建原始排序的索引映射,用于后续恢复排序
Map<Long, Integer> originalIndexMap = new HashMap<>();
for (int i = 0; i < deviceSamples.size(); i++) {
originalIndexMap.put(deviceSamples.get(i).getId(), i);
}
// 按创建时间排序
List<FaceSampleEntity> sortedByCreateTime = deviceSamples.stream()
.sorted(Comparator.comparing(FaceSampleEntity::getCreateAt))
.collect(Collectors.toList());
// 根据参数决定去掉首尾还是只去掉尾部
List<FaceSampleEntity> filteredSamples;
if (removeBoth && sortedByCreateTime.size() > 2) {
// 去掉首尾
filteredSamples = sortedByCreateTime.subList(1, sortedByCreateTime.size() - 1);
} else if (!removeBoth && sortedByCreateTime.size() > 1) {
// 只去掉尾部
filteredSamples = sortedByCreateTime.subList(0, sortedByCreateTime.size() - 1);
} else {
filteredSamples = sortedByCreateTime;
}
// 取前limitPhoto个
List<FaceSampleEntity> limitedSamples = filteredSamples.stream()
.limit(limitPhoto)
.collect(Collectors.toList());
// 按原始顺序排序
List<Long> resultIds = limitedSamples.stream()
.sorted(Comparator.comparing(sample -> originalIndexMap.get(sample.getId())))
.map(FaceSampleEntity::getId)
.collect(Collectors.toList());
return resultIds;
}
}

View File

@@ -0,0 +1,136 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.pipeline.annotation.StageConfig;
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
import com.ycwl.basic.pipeline.core.StageResult;
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.stream.Collectors;
/**
* 按时间范围筛选样本Stage
* 负责根据景区配置的游览时间(tour_time)筛选匹配样本
*
* 职责:
* 1. 从context.scenicConfig读取tour_time配置(分钟)
* 2. 从context.faceSamples读取样本实体缓存
* 3. 找到最新的样本,以其拍摄时间为基准
* 4. 筛选出时间范围内(最新样本时间 ± tour_time分钟)的样本
* 5. 更新context.sampleListIds
*
* 前置条件:
* - context.faceSamples不为空 (由LoadMatchedSamplesStage加载)
* - context.scenicConfig配置了tour_time
*
* 配置说明: tour_time=0或null表示不限制时间范围
*/
@Slf4j
@Component
@StageConfig(
stageId = "filter_by_time_range",
optionalMode = StageOptionalMode.SUPPORT,
description = "按游览时间范围筛选样本",
defaultEnabled = true
)
public class FilterByTimeRangeStage extends AbstractPipelineStage<FaceMatchingContext> {
@Override
public String getName() {
return "FilterByTimeRange";
}
@Override
protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) {
// 检查faceSamples是否为空
List<FaceSampleEntity> faceSamples = context.getFaceSamples();
if (faceSamples == null || faceSamples.isEmpty()) {
log.debug("faceSamples为空,跳过时间范围筛选,faceId={}", context.getFaceId());
return false;
}
// 检查是否配置了tour_time
Integer tourMinutes = context.getScenicConfig() != null
? context.getScenicConfig().getInteger("tour_time")
: null;
if (tourMinutes == null || tourMinutes <= 0) {
log.debug("景区未配置tour_time或配置为0,跳过时间范围筛选,faceId={}", context.getFaceId());
return false;
}
return true;
}
@Override
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
List<FaceSampleEntity> faceSamples = context.getFaceSamples();
List<Long> sampleListIds = context.getSampleListIds();
Long faceId = context.getFaceId();
// 防御性检查:faceSamples为空
if (faceSamples == null || faceSamples.isEmpty()) {
log.debug("faceSamples为空,跳过时间范围筛选,faceId={}", faceId);
return StageResult.skipped("faceSamples为空");
}
// 防御性检查:tour_time配置
Integer tourMinutes = context.getScenicConfig() != null
? context.getScenicConfig().getInteger("tour_time")
: null;
if (tourMinutes == null || tourMinutes <= 0) {
log.debug("景区未配置tour_time或配置为0,跳过时间范围筛选,faceId={}", faceId);
return StageResult.skipped("未配置tour_time");
}
try {
// 1. 构建样本ID到实体的映射
Map<Long, FaceSampleEntity> sampleMap = faceSamples.stream()
.collect(Collectors.toMap(FaceSampleEntity::getId, sample -> sample, (a, b) -> a));
// 2. 找到最新的样本(拍摄时间最晚)
FaceSampleEntity topMatchSample = faceSamples.stream()
.filter(sample -> sample.getCreateAt() != null)
.max(Comparator.comparing(FaceSampleEntity::getCreateAt))
.orElse(null);
if (topMatchSample == null || topMatchSample.getCreateAt() == null) {
log.warn("未找到有效的样本拍摄时间,保留所有样本,faceId={}", faceId);
return StageResult.success("样本无拍摄时间,保留所有");
}
Date referenceTime = topMatchSample.getCreateAt();
long referenceMillis = referenceTime.getTime();
long tourMillis = tourMinutes * 60 * 1000L;
// 3. 筛选时间范围内的样本
List<Long> filteredIds = sampleListIds.stream()
.filter(sampleId -> {
FaceSampleEntity sample = sampleMap.get(sampleId);
if (sample == null || sample.getCreateAt() == null) {
return false; // 无时间信息的样本被过滤
}
long timeDiff = Math.abs(sample.getCreateAt().getTime() - referenceMillis);
return timeDiff <= tourMillis;
})
.collect(Collectors.toList());
// 4. 更新context
context.setSampleListIds(filteredIds);
log.info("时间范围筛选完成: faceId={}, tour_time={}分钟, 原始样本数={}, 筛选后数={}",
faceId, tourMinutes, sampleListIds.size(), filteredIds.size());
return StageResult.success(String.format("时间筛选: %d → %d",
sampleListIds.size(), filteredIds.size()));
} catch (Exception e) {
log.error("时间范围筛选失败,faceId={}", faceId, e);
// 筛选失败不影响主流程,返回降级
return StageResult.degraded("时间范围筛选失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,66 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.pipeline.annotation.StageConfig;
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
import com.ycwl.basic.pipeline.core.StageResult;
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
import com.ycwl.basic.face.pipeline.helper.PuzzleGenerationOrchestrator;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 生成拼图模板Stage
* 负责触发景区拼图模板的异步生成任务
*
* 职责:
* 1. 从context读取必要参数(scenicId, faceId, memberId, faceUrl)
* 2. 调用puzzleOrchestrator.generateAllTemplatesAsync()触发异步生成
* 3. 立即返回,不等待生成完成
*
* 业务说明:
* - 拼图生成是异步的,不影响主流程
* - 具体的拼图生成逻辑由PuzzleGenerationOrchestrator负责
* - Stage只负责触发任务,符合"薄Stage,厚Service"原则
*/
@Slf4j
@Component
@StageConfig(
stageId = "generate_puzzle",
optionalMode = StageOptionalMode.SUPPORT,
description = "异步生成拼图模板",
defaultEnabled = true
)
public class GeneratePuzzleStage extends AbstractPipelineStage<FaceMatchingContext> {
@Autowired
private PuzzleGenerationOrchestrator puzzleOrchestrator;
@Override
public String getName() {
return "GeneratePuzzle";
}
@Override
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
Long scenicId = context.getFace().getScenicId();
Long faceId = context.getFaceId();
Long memberId = context.getFace().getMemberId();
String faceUrl = context.getFace().getFaceUrl();
try {
// 触发异步生成拼图模板
puzzleOrchestrator.generateAllTemplatesAsync(scenicId, faceId, memberId, faceUrl);
log.debug("拼图模板异步生成任务已提交: scenicId={}, faceId={}", scenicId, faceId);
return StageResult.success("拼图模板已提交异步生成");
} catch (Exception e) {
log.error("提交拼图生成任务失败: scenicId={}, faceId={}", scenicId, faceId, e);
// 拼图生成失败不影响主流程,返回降级
return StageResult.degraded("提交拼图生成任务失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,92 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.pipeline.annotation.StageConfig;
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
import com.ycwl.basic.pipeline.core.StageResult;
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
import com.ycwl.basic.service.pc.processor.VideoRecreationHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 处理视频重切Stage
* 负责触发视频重新切片处理
*
* 职责:
* 1. 从context读取必要参数(scenicId, memberSourceList, faceId, memberId, sampleListIds, isNew)
* 2. 调用videoRecreationHandler.handleVideoRecreation()触发视频重切
*
* 前置条件: context.memberSourceList不为空
* 业务说明: 视频重切用于根据人脸识别结果重新生成个性化视频片段
*/
@Slf4j
@Component
@StageConfig(
stageId = "handle_video_recreation",
optionalMode = StageOptionalMode.SUPPORT,
description = "处理视频重切逻辑",
defaultEnabled = true
)
public class HandleVideoRecreationStage extends AbstractPipelineStage<FaceMatchingContext> {
@Autowired
private VideoRecreationHandler videoRecreationHandler;
@Override
public String getName() {
return "HandleVideoRecreation";
}
@Override
protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) {
// 只有当memberSourceList不为空时才执行
List<MemberSourceEntity> memberSourceList = context.getMemberSourceList();
if (memberSourceList == null || memberSourceList.isEmpty()) {
log.debug("memberSourceList为空,跳过视频重切,faceId={}", context.getFaceId());
return false;
}
return true;
}
@Override
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
Long scenicId = context.getFace().getScenicId();
List<MemberSourceEntity> memberSourceEntityList = context.getMemberSourceList();
Long faceId = context.getFaceId();
Long memberId = context.getFace().getMemberId();
List<Long> sampleListIds = context.getSampleListIds();
boolean isNew = context.isNew();
// 防御性检查:memberSourceList为空
if (memberSourceEntityList == null || memberSourceEntityList.isEmpty()) {
log.debug("memberSourceList为空,跳过视频重切,faceId={}", faceId);
return StageResult.skipped("memberSourceList为空");
}
try {
// 处理视频重切
videoRecreationHandler.handleVideoRecreation(
scenicId,
memberSourceEntityList,
faceId,
memberId,
sampleListIds,
isNew);
log.info("视频重切处理完成: faceId={}, scenicId={}, 源文件数={}",
faceId, scenicId, memberSourceEntityList.size());
return StageResult.success("视频重切处理完成");
} catch (Exception e) {
log.error("处理视频重切失败,faceId={}", faceId, e);
// 视频重切失败不影响主流程,返回降级
return StageResult.degraded("视频重切处理失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,73 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.exception.BaseException;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.pipeline.annotation.StageConfig;
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
import com.ycwl.basic.pipeline.core.StageResult;
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
import com.ycwl.basic.mapper.FaceSampleMapper;
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 加载人脸样本Stage
* 负责加载用户选择的人脸样本数据
*
* 职责:
* 1. 从context.faceSampleIds读取用户选择的样本ID列表
* 2. 调用faceSampleMapper.listByIds()加载样本实体
* 3. 更新context.faceSamples
*/
@Slf4j
@Component
@StageConfig(
stageId = "load_face_samples",
optionalMode = StageOptionalMode.FORCE_ON,
description = "加载用户选择的人脸样本"
)
public class LoadFaceSamplesStage extends AbstractPipelineStage<FaceMatchingContext> {
@Autowired
private FaceSampleMapper faceSampleMapper;
@Override
public String getName() {
return "LoadFaceSamples";
}
@Override
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
List<Long> faceSampleIds = context.getFaceSampleIds();
Long faceId = context.getFaceId();
if (faceSampleIds == null || faceSampleIds.isEmpty()) {
log.warn("faceSampleIds为空,faceId={}", faceId);
return StageResult.failed("faceSampleIds不能为空");
}
try {
List<FaceSampleEntity> faceSamples = faceSampleMapper.listByIds(faceSampleIds);
if (faceSamples.isEmpty()) {
log.warn("未找到指定的人脸样本,faceSampleIds: {}, faceId={}", faceSampleIds, faceId);
throw new BaseException("未找到指定的人脸样本");
}
context.setFaceSamples(faceSamples);
log.info("加载人脸样本成功: faceId={}, sampleCount={}", faceId, faceSamples.size());
return StageResult.success(String.format("加载了%d个人脸样本", faceSamples.size()));
} catch (BaseException e) {
throw e;
} catch (Exception e) {
log.error("加载人脸样本失败,faceId={}, faceSampleIds={}", faceId, faceSampleIds, e);
return StageResult.failed("加载人脸样本失败", e);
}
}
}

View File

@@ -0,0 +1,95 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.pipeline.annotation.StageConfig;
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
import com.ycwl.basic.pipeline.core.StageResult;
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
import com.ycwl.basic.mapper.FaceSampleMapper;
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 加载匹配样本实体Stage
* 负责将sampleListIds对应的样本实体加载到context.faceSamples,供后续Stage使用
*
* 职责:
* 1. 从context.sampleListIds读取匹配到的样本ID列表
* 2. 调用faceSampleMapper.listByIds()批量加载样本实体
* 3. 更新context.faceSamples作为样本实体缓存
*
* 设计目的:
* - 避免后续多个Stage重复调用faceSampleMapper.listByIds()
* - 统一加载时机,提高性能
* - 为后续筛选Stage提供样本实体数据源
*
* 前置条件: context.sampleListIds不为空
*
* 应用场景: 自定义匹配场景,在CustomFaceSearchStage之后
*/
@Slf4j
@Component
@StageConfig(
stageId = "load_matched_samples",
optionalMode = StageOptionalMode.UNSUPPORT,
description = "加载匹配样本实体到缓存"
)
public class LoadMatchedSamplesStage extends AbstractPipelineStage<FaceMatchingContext> {
@Autowired
private FaceSampleMapper faceSampleMapper;
@Override
public String getName() {
return "LoadMatchedSamples";
}
@Override
protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) {
// 检查sampleListIds是否为空
List<Long> sampleListIds = context.getSampleListIds();
if (sampleListIds == null || sampleListIds.isEmpty()) {
log.debug("sampleListIds为空,跳过加载匹配样本,faceId={}", context.getFaceId());
return false;
}
return true;
}
@Override
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
List<Long> sampleListIds = context.getSampleListIds();
Long faceId = context.getFaceId();
// 防御性检查:如果sampleListIds为空,直接跳过
if (sampleListIds == null || sampleListIds.isEmpty()) {
log.debug("sampleListIds为空,跳过加载匹配样本,faceId={}", faceId);
return StageResult.skipped("sampleListIds为空");
}
try {
// 批量加载样本实体
List<FaceSampleEntity> faceSamples = faceSampleMapper.listByIds(sampleListIds);
if (faceSamples == null || faceSamples.isEmpty()) {
log.warn("未找到任何匹配样本实体,faceId={}, sampleListIds={}", faceId, sampleListIds);
return StageResult.skipped("未找到匹配样本实体");
}
// 存入context缓存,供后续Stage使用
context.setFaceSamples(faceSamples);
log.info("加载匹配样本实体完成: faceId={}, 样本数={}", faceId, faceSamples.size());
return StageResult.success(String.format("已加载%d个样本实体", faceSamples.size()));
} catch (Exception e) {
log.error("加载匹配样本实体失败,faceId={}", faceId, e);
// 加载失败影响后续流程,返回失败
return StageResult.failed("加载匹配样本实体失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,98 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.pipeline.annotation.StageConfig;
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
import com.ycwl.basic.pipeline.core.StageResult;
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
import com.ycwl.basic.mapper.SourceMapper;
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
import com.ycwl.basic.repository.MemberRelationRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 持久化关联关系Stage
* 负责过滤并保存源文件关联关系到数据库
*
* 职责:
* 1. 从context.memberSourceList读取关联关系
* 2. 过滤已存在的关联关系和无效的source引用
* 3. 保存到数据库
* 4. 清除缓存
*/
@Slf4j
@Component
@StageConfig(
stageId = "persist_relations",
optionalMode = StageOptionalMode.FORCE_ON,
description = "持久化源文件关联关系"
)
public class PersistRelationsStage extends AbstractPipelineStage<FaceMatchingContext> {
@Autowired
private SourceMapper sourceMapper;
@Autowired
private MemberRelationRepository memberRelationRepository;
@Override
public String getName() {
return "PersistRelations";
}
@Override
protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) {
// 只有当memberSourceList不为空时才执行
List<MemberSourceEntity> memberSourceList = context.getMemberSourceList();
if (memberSourceList == null || memberSourceList.isEmpty()) {
log.debug("memberSourceList为空,跳过持久化,faceId={}", context.getFaceId());
return false;
}
return true;
}
@Override
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
List<MemberSourceEntity> memberSourceEntityList = context.getMemberSourceList();
Long faceId = context.getFaceId();
// 防御性检查:memberSourceList为空
if (memberSourceEntityList == null || memberSourceEntityList.isEmpty()) {
log.debug("memberSourceList为空,跳过持久化,faceId={}", faceId);
return StageResult.skipped("memberSourceList为空");
}
try {
// 1. 过滤已存在的关联关系
List<MemberSourceEntity> existingFiltered = sourceMapper.filterExistingRelations(memberSourceEntityList);
// 2. 过滤无效的source引用
List<MemberSourceEntity> validFiltered = sourceMapper.filterValidSourceRelations(existingFiltered);
if (!validFiltered.isEmpty()) {
// 3. 保存到数据库
sourceMapper.addRelations(validFiltered);
log.debug("创建关联关系: faceId={}, 原始数量={}, 过滤后数量={}",
faceId, memberSourceEntityList.size(), validFiltered.size());
} else {
log.warn("没有有效的关联关系可创建: faceId={}, 原始数量={}",
faceId, memberSourceEntityList.size());
return StageResult.skipped("没有有效的关联关系可创建");
}
// 4. 清除缓存
memberRelationRepository.clearSCacheByFace(faceId);
return StageResult.success(String.format("持久化了%d条关联关系", validFiltered.size()));
} catch (Exception e) {
log.error("持久化关联关系失败,faceId={}", faceId, e);
return StageResult.failed("保存关联关系失败", e);
}
}
}

View File

@@ -0,0 +1,91 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.pipeline.annotation.StageConfig;
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
import com.ycwl.basic.pipeline.core.StageResult;
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
import com.ycwl.basic.repository.FaceRepository;
import com.ycwl.basic.repository.ScenicRepository;
import com.ycwl.basic.service.pc.ScenicService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 准备上下文Stage
* 负责加载人脸实体、景区配置、识别适配器等必要数据
*
* 职责:
* 1. 加载FaceEntity(如不存在则失败)
* 2. 检查是否人工选择(是则跳过,除非isNew=true)
* 3. 加载ScenicConfigManager和IFaceBodyAdapter
* 4. 更新Context
*/
@Slf4j
@Component
@StageConfig(
stageId = "prepare_context",
optionalMode = StageOptionalMode.FORCE_ON,
description = "准备人脸匹配上下文数据"
)
public class PrepareContextStage extends AbstractPipelineStage<FaceMatchingContext> {
@Autowired
private FaceRepository faceRepository;
@Autowired
private ScenicRepository scenicRepository;
@Autowired
private ScenicService scenicService;
@Override
public String getName() {
return "PrepareContext";
}
@Override
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
Long faceId = context.getFaceId();
boolean isNew = context.isNew();
// 1. 加载人脸实体
FaceEntity face = faceRepository.getFace(faceId);
if (face == null) {
log.warn("人脸不存在,faceId: {}", faceId);
return StageResult.failed("人脸不存在,faceId: " + faceId);
}
context.setFace(face);
log.debug("加载人脸实体成功: faceId={}, memberId={}, scenicId={}",
faceId, face.getMemberId(), face.getScenicId());
// 2. 检查是否人工选择
// 人工选择的无需重新匹配(新用户除外)
if (!isNew && Integer.valueOf(1).equals(face.getIsManual())) {
log.info("人工选择的人脸,无需匹配,faceId: {}", faceId);
return StageResult.skipped("人工选择的人脸,无需重新匹配");
}
// 3. 加载景区配置
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(face.getScenicId());
context.setScenicConfig(scenicConfig);
log.debug("加载景区配置成功: scenicId={}", face.getScenicId());
// 4. 加载人脸识别适配器
IFaceBodyAdapter faceBodyAdapter = scenicService.getScenicFaceBodyAdapter(face.getScenicId());
if (faceBodyAdapter == null) {
log.error("无法获取人脸识别适配器,scenicId: {}", face.getScenicId());
return StageResult.failed("人脸识别服务不可用,请稍后再试");
}
context.setFaceBodyAdapter(faceBodyAdapter);
log.debug("加载人脸识别适配器成功: scenicId={}", face.getScenicId());
return StageResult.success("上下文准备完成");
}
}

View File

@@ -0,0 +1,92 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.pipeline.annotation.StageConfig;
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
import com.ycwl.basic.pipeline.core.StageResult;
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
import com.ycwl.basic.service.pc.processor.BuyStatusProcessor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 处理购买状态Stage
* 负责更新源文件的购买状态标记
*
* 职责:
* 1. 从context.memberSourceList读取源文件关联列表
* 2. 从context.freeSourceIds读取免费源文件ID列表
* 3. 调用buyStatusProcessor.processBuyStatus()更新购买状态
*
* 前置条件: context.memberSourceList不为空
* 业务说明: 购买状态影响前端显示和用户下载权限
*/
@Slf4j
@Component
@StageConfig(
stageId = "process_buy_status",
optionalMode = StageOptionalMode.SUPPORT,
description = "处理源文件购买状态",
defaultEnabled = true
)
public class ProcessBuyStatusStage extends AbstractPipelineStage<FaceMatchingContext> {
@Autowired
private BuyStatusProcessor buyStatusProcessor;
@Override
public String getName() {
return "ProcessBuyStatus";
}
@Override
protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) {
// 只有当memberSourceList不为空时才执行
List<MemberSourceEntity> memberSourceList = context.getMemberSourceList();
if (memberSourceList == null || memberSourceList.isEmpty()) {
log.debug("memberSourceList为空,跳过购买状态处理,faceId={}", context.getFaceId());
return false;
}
return true;
}
@Override
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
List<MemberSourceEntity> memberSourceEntityList = context.getMemberSourceList();
List<Long> freeSourceIds = context.getFreeSourceIds();
Long memberId = context.getFace().getMemberId();
Long scenicId = context.getFace().getScenicId();
Long faceId = context.getFaceId();
// 防御性检查:memberSourceList为空
if (memberSourceEntityList == null || memberSourceEntityList.isEmpty()) {
log.debug("memberSourceList为空,跳过购买状态处理,faceId={}", faceId);
return StageResult.skipped("memberSourceList为空");
}
try {
// 处理购买状态
buyStatusProcessor.processBuyStatus(
memberSourceEntityList,
freeSourceIds,
memberId,
scenicId,
faceId);
log.info("购买状态处理完成: faceId={}, 源文件数={}, 免费数={}",
faceId, memberSourceEntityList.size(),
freeSourceIds != null ? freeSourceIds.size() : 0);
return StageResult.success("购买状态处理完成");
} catch (Exception e) {
log.error("处理购买状态失败,faceId={}", faceId, e);
// 购买状态处理失败不影响主流程,返回降级
return StageResult.degraded("购买状态处理失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,89 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.pipeline.annotation.StageConfig;
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
import com.ycwl.basic.pipeline.core.StageResult;
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
import com.ycwl.basic.service.pc.processor.SourceRelationProcessor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 处理免费源文件Stage
* 负责根据业务规则确定哪些源文件可以免费访问
*
* 职责:
* 1. 从context.memberSourceList读取源文件关联列表
* 2. 调用sourceRelationProcessor.processFreeSourceLogic()确定免费源文件
* 3. 更新context.freeSourceIds
*
* 前置条件: context.memberSourceList不为空
* 后置条件: context.freeSourceIds已设置
*/
@Slf4j
@Component
@StageConfig(
stageId = "process_free_source",
optionalMode = StageOptionalMode.SUPPORT,
description = "处理免费源文件逻辑",
defaultEnabled = true
)
public class ProcessFreeSourceStage extends AbstractPipelineStage<FaceMatchingContext> {
@Autowired
private SourceRelationProcessor sourceRelationProcessor;
@Override
public String getName() {
return "ProcessFreeSource";
}
@Override
protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) {
// 只有当memberSourceList不为空时才执行
List<MemberSourceEntity> memberSourceList = context.getMemberSourceList();
if (memberSourceList == null || memberSourceList.isEmpty()) {
log.debug("memberSourceList为空,跳过免费逻辑,faceId={}", context.getFaceId());
return false;
}
return true;
}
@Override
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
List<MemberSourceEntity> memberSourceEntityList = context.getMemberSourceList();
Long scenicId = context.getFace().getScenicId();
boolean isNew = context.isNew();
Long faceId = context.getFaceId();
// 防御性检查:memberSourceList为空
if (memberSourceEntityList == null || memberSourceEntityList.isEmpty()) {
log.debug("memberSourceList为空,跳过免费逻辑,faceId={}", faceId);
return StageResult.skipped("memberSourceList为空");
}
try {
// 处理免费逻辑
List<Long> freeSourceIds = sourceRelationProcessor.processFreeSourceLogic(
memberSourceEntityList, scenicId, isNew);
context.setFreeSourceIds(freeSourceIds);
log.info("免费源文件处理完成: faceId={}, 总源文件数={}, 免费数={}",
faceId, memberSourceEntityList.size(), freeSourceIds != null ? freeSourceIds.size() : 0);
return StageResult.success(String.format("确定了%d个免费源文件",
freeSourceIds != null ? freeSourceIds.size() : 0));
} catch (Exception e) {
log.error("处理免费源文件失败,faceId={}", faceId, e);
// 免费逻辑失败不影响主流程,返回降级
return StageResult.degraded("免费源文件处理失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,71 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.face.pipeline.enums.FaceMatchingScene;
import com.ycwl.basic.pipeline.annotation.StageConfig;
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
import com.ycwl.basic.pipeline.core.StageResult;
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
import com.ycwl.basic.service.pc.helper.FaceMetricsRecorder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 记录自定义匹配次数Stage
* 负责记录自定义人脸匹配调用次数,用于监控
*
* 职责:
* 1. 仅在CUSTOM_MATCHING场景执行
* 2. 调用metricsRecorder.recordCustomMatchCount()记录次数
*/
@Slf4j
@Component
@StageConfig(
stageId = "record_custom_match_metrics",
optionalMode = StageOptionalMode.SUPPORT,
description = "记录自定义匹配指标",
defaultEnabled = true
)
public class RecordCustomMatchMetricsStage extends AbstractPipelineStage<FaceMatchingContext> {
@Autowired
private FaceMetricsRecorder metricsRecorder;
@Override
public String getName() {
return "RecordCustomMatchMetrics";
}
@Override
protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) {
// 只有自定义匹配场景才执行
if (context.getScene() != FaceMatchingScene.CUSTOM_MATCHING) {
log.debug("非自定义匹配场景,跳过记录,faceId={}", context.getFaceId());
return false;
}
return true;
}
@Override
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
Long faceId = context.getFaceId();
// 防御性检查:只有自定义匹配场景才执行
if (context.getScene() != FaceMatchingScene.CUSTOM_MATCHING) {
log.debug("非自定义匹配场景,跳过记录,faceId={}", faceId);
return StageResult.skipped("非自定义匹配场景");
}
try {
metricsRecorder.recordCustomMatchCount(faceId);
log.debug("记录自定义匹配次数: faceId={}", faceId);
return StageResult.success("自定义匹配指标记录完成");
} catch (Exception e) {
log.error("记录自定义匹配指标失败,faceId={}", faceId, e);
// 指标记录失败不影响主流程,返回降级
return StageResult.degraded("指标记录失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,63 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.pipeline.annotation.StageConfig;
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
import com.ycwl.basic.pipeline.core.StageResult;
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
import com.ycwl.basic.service.pc.helper.FaceMetricsRecorder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 记录识别次数Stage
* 负责记录人脸识别调用次数,用于监控和防重复检查
*
* 职责:
* 1. 调用metricsRecorder.recordRecognitionCount()记录识别次数
* 2. 检查searchResult是否触发低阈值检测
* 3. 如果是,调用metricsRecorder.recordLowThreshold()记录
*/
@Slf4j
@Component
@StageConfig(
stageId = "record_metrics",
optionalMode = StageOptionalMode.SUPPORT,
description = "记录人脸识别指标",
defaultEnabled = true
)
public class RecordMetricsStage extends AbstractPipelineStage<FaceMatchingContext> {
@Autowired
private FaceMetricsRecorder metricsRecorder;
@Override
public String getName() {
return "RecordMetrics";
}
@Override
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
Long faceId = context.getFaceId();
try {
// 1. 记录识别次数
metricsRecorder.recordRecognitionCount(faceId);
log.debug("记录识别次数: faceId={}", faceId);
// 2. 检查是否触发低阈值检测
if (context.getSearchResult() != null && context.getSearchResult().isLowThreshold()) {
metricsRecorder.recordLowThreshold(faceId);
log.debug("触发低阈值检测,记录faceId: {}", faceId);
}
return StageResult.success("识别指标记录完成");
} catch (Exception e) {
log.error("记录识别指标失败,faceId={}", faceId, e);
// 指标记录失败不影响主流程,返回降级
return StageResult.degraded("指标记录失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,69 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.biz.TaskStatusBiz;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.pipeline.annotation.StageConfig;
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
import com.ycwl.basic.pipeline.core.StageResult;
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 设置任务状态Stage
* 负责为新用户设置任务状态为待处理
*
* 职责:
* 1. 仅在isNew=true时执行
* 2. 调用taskStatusBiz.setFaceCutStatus(faceId, 0)
*/
@Slf4j
@Component
@StageConfig(
stageId = "set_task_status",
optionalMode = StageOptionalMode.FORCE_ON,
description = "设置新用户任务状态"
)
public class SetTaskStatusStage extends AbstractPipelineStage<FaceMatchingContext> {
@Autowired
private TaskStatusBiz taskStatusBiz;
@Override
public String getName() {
return "SetTaskStatus";
}
@Override
protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) {
// 只有新用户才执行
if (!context.isNew()) {
log.debug("非新用户,跳过设置任务状态,faceId={}", context.getFaceId());
return false;
}
return true;
}
@Override
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
Long faceId = context.getFaceId();
// 防御性检查:只有新用户才执行
if (!context.isNew()) {
log.debug("非新用户,跳过设置任务状态,faceId={}", faceId);
return StageResult.skipped("非新用户");
}
try {
taskStatusBiz.setFaceCutStatus(faceId, 0);
log.debug("设置新用户任务状态: faceId={}, status=0", faceId);
return StageResult.success("任务状态已设置");
} catch (Exception e) {
log.error("设置任务状态失败,faceId={}", faceId, e);
// 任务状态设置失败不影响主流程,返回降级
return StageResult.degraded("任务状态设置失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,95 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.pipeline.annotation.StageConfig;
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
import com.ycwl.basic.pipeline.core.StageResult;
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
import com.ycwl.basic.mapper.FaceMapper;
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
import com.ycwl.basic.model.task.resp.SearchFaceRespVo;
import com.ycwl.basic.repository.FaceRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.util.Date;
import java.util.stream.Collectors;
/**
* 更新人脸结果Stage
* 负责将人脸识别结果保存到数据库
*
* 职责:
* 1. 从context.searchResult读取识别结果
* 2. 更新FaceEntity(score、matchResult、firstMatchRate、matchSampleIds)
* 3. 清除缓存
*/
@Slf4j
@Component
@StageConfig(
stageId = "update_face_result",
optionalMode = StageOptionalMode.FORCE_ON,
description = "更新人脸识别结果到数据库"
)
public class UpdateFaceResultStage extends AbstractPipelineStage<FaceMatchingContext> {
@Autowired
private FaceMapper faceMapper;
@Autowired
private FaceRepository faceRepository;
@Override
public String getName() {
return "UpdateFaceResult";
}
@Override
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
SearchFaceRespVo searchResult = context.getSearchResult();
if (searchResult == null) {
log.warn("searchResult为空,跳过更新人脸结果,faceId={}", context.getFaceId());
return StageResult.skipped("searchResult为空");
}
try {
FaceEntity originalFace = context.getFace();
Long faceId = context.getFaceId();
FaceEntity faceEntity = new FaceEntity();
faceEntity.setId(faceId);
faceEntity.setScore(searchResult.getScore());
faceEntity.setMatchResult(searchResult.getSearchResultJson());
if (searchResult.getFirstMatchRate() != null) {
faceEntity.setFirstMatchRate(BigDecimal.valueOf(searchResult.getFirstMatchRate()));
}
if (searchResult.getSampleListIds() != null) {
faceEntity.setMatchSampleIds(searchResult.getSampleListIds().stream()
.map(String::valueOf)
.collect(Collectors.joining(",")));
}
faceEntity.setCreateAt(new Date());
faceEntity.setScenicId(originalFace.getScenicId());
faceEntity.setMemberId(originalFace.getMemberId());
faceEntity.setFaceUrl(originalFace.getFaceUrl());
faceMapper.update(faceEntity);
faceRepository.clearFaceCache(faceId);
log.debug("人脸结果更新成功:faceId={}, score={}, sampleCount={}",
faceId, searchResult.getScore(),
searchResult.getSampleListIds() != null ? searchResult.getSampleListIds().size() : 0);
return StageResult.success("人脸结果更新成功");
} catch (Exception e) {
log.error("更新人脸结果失败,faceId={}", context.getFaceId(), e);
return StageResult.failed("保存人脸识别结果失败", e);
}
}
}

View File

@@ -45,6 +45,7 @@ public class AliFaceBodyAdapter implements IFaceBodyAdapter {
private static final Map<String, IRateLimiter> deleteDbLimiters = new ConcurrentHashMap<>(); private static final Map<String, IRateLimiter> deleteDbLimiters = new ConcurrentHashMap<>();
private static final Map<String, IRateLimiter> deleteEntityLimiters = new ConcurrentHashMap<>(); private static final Map<String, IRateLimiter> deleteEntityLimiters = new ConcurrentHashMap<>();
@Getter // 添加getter,支持获取accessKeyId
private AliFaceBodyConfig config; private AliFaceBodyConfig config;
public boolean setConfig(AliFaceBodyConfig config) { public boolean setConfig(AliFaceBodyConfig config) {
@@ -184,10 +185,8 @@ public class AliFaceBodyAdapter implements IFaceBodyAdapter {
addFaceRequest.setImageUrl(faceUrl); addFaceRequest.setImageUrl(faceUrl);
addFaceRequest.setExtraData(extData); addFaceRequest.setExtraData(extData);
AddFaceResp respVo = new AddFaceResp(); AddFaceResp respVo = new AddFaceResp();
try { // QPS控制已由外层调度器管理,这里不再需要限流
addFaceLimiter.acquire(); // 移除阻塞等待: addFaceLimiter.acquire()
} catch (InterruptedException ignored) {
}
try { try {
AddFaceResponse acsResponse = client.getAcsResponse(addFaceRequest); AddFaceResponse acsResponse = client.getAcsResponse(addFaceRequest);
respVo.setScore(acsResponse.getData().getQualitieScore()); respVo.setScore(acsResponse.getData().getQualitieScore());

View File

@@ -1,5 +1,6 @@
package com.ycwl.basic.facebody.adapter; package com.ycwl.basic.facebody.adapter;
import cn.hutool.core.codec.Base64;
import com.baidu.aip.face.AipFace; import com.baidu.aip.face.AipFace;
import com.ycwl.basic.facebody.entity.AddFaceResp; import com.ycwl.basic.facebody.entity.AddFaceResp;
import com.ycwl.basic.facebody.entity.BceFaceBodyConfig; import com.ycwl.basic.facebody.entity.BceFaceBodyConfig;
@@ -7,12 +8,18 @@ import com.ycwl.basic.facebody.entity.SearchFaceResp;
import com.ycwl.basic.facebody.entity.SearchFaceResultItem; import com.ycwl.basic.facebody.entity.SearchFaceResultItem;
import com.ycwl.basic.utils.ratelimiter.FixedRateLimiter; import com.ycwl.basic.utils.ratelimiter.FixedRateLimiter;
import com.ycwl.basic.utils.ratelimiter.IRateLimiter; import com.ycwl.basic.utils.ratelimiter.IRateLimiter;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONObject; import org.json.JSONObject;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
import java.net.URL;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
@@ -34,6 +41,8 @@ public class BceFaceBodyAdapter implements IFaceBodyAdapter {
private static final Map<String, IRateLimiter> deleteDbLimiters = new ConcurrentHashMap<>(); private static final Map<String, IRateLimiter> deleteDbLimiters = new ConcurrentHashMap<>();
private static final Map<String, IRateLimiter> deleteEntityLimiters = new ConcurrentHashMap<>(); private static final Map<String, IRateLimiter> deleteEntityLimiters = new ConcurrentHashMap<>();
private static final Map<String, IRateLimiter> deleteFaceLimiters = new ConcurrentHashMap<>(); private static final Map<String, IRateLimiter> deleteFaceLimiters = new ConcurrentHashMap<>();
@Getter // 添加getter,支持获取appId和addQps
private BceFaceBodyConfig config; private BceFaceBodyConfig config;
public boolean setConfig(BceFaceBodyConfig config) { public boolean setConfig(BceFaceBodyConfig config) {
@@ -143,15 +152,34 @@ public class BceFaceBodyAdapter implements IFaceBodyAdapter {
options.put("user_info", extData); options.put("user_info", extData);
// options.put("quality_control", "LOW"); // options.put("quality_control", "LOW");
options.put("action_type", "REPLACE"); options.put("action_type", "REPLACE");
try { // QPS控制已由外层调度器管理,这里不再需要限流
addEntityLimiter.acquire(); // 移除阻塞等待: addEntityLimiter.acquire()
} catch (InterruptedException ignored) {
}
JSONObject response = client.addUser(faceUrl, "URL", dbName, entityId, options); JSONObject response = client.addUser(faceUrl, "URL", dbName, entityId, options);
if (response.getInt("error_code") == 0) { int errorCode = response.getInt("error_code");
if (errorCode == 0) {
AddFaceResp resp = new AddFaceResp(); AddFaceResp resp = new AddFaceResp();
resp.setScore(100f); resp.setScore(100f);
return resp; return resp;
} else if (errorCode == 222204) {
// error_code: 222204 表示无法正常访问URL图片,尝试下载并转换为base64后重试
log.warn("无法正常访问URL图片,错误码: 222204,尝试下载图片转base64后重试,URL: {}", faceUrl);
String base64Image = downloadImageAsBase64(faceUrl);
if (base64Image != null) {
// 重试时也不需要限流,由外层调度器控制
JSONObject retryResponse = client.addUser(base64Image, "BASE64", dbName, entityId, options);
if (retryResponse.getInt("error_code") == 0) {
log.info("使用base64重试添加人脸成功,entityId: {}", entityId);
AddFaceResp resp = new AddFaceResp();
resp.setScore(100f);
return resp;
} else {
log.warn("使用base64重试添加人脸仍失败!{}", retryResponse);
return null;
}
} else {
log.error("下载图片转base64失败,无法重试,URL: {}", faceUrl);
return null;
}
} else { } else {
log.warn("创建人脸失败!{}", response); log.warn("创建人脸失败!{}", response);
return null; return null;
@@ -284,7 +312,8 @@ public class BceFaceBodyAdapter implements IFaceBodyAdapter {
} catch (InterruptedException ignored) { } catch (InterruptedException ignored) {
} }
JSONObject response = client.search(faceUrl, "URL", dbName, options); JSONObject response = client.search(faceUrl, "URL", dbName, options);
if (response.getInt("error_code") == 0) { int errorCode = response.getInt("error_code");
if (errorCode == 0) {
resp.setOriginalFaceScore(100f); resp.setOriginalFaceScore(100f);
JSONObject resultObj = response.getJSONObject("result"); JSONObject resultObj = response.getJSONObject("result");
if (resultObj == null) { if (resultObj == null) {
@@ -307,7 +336,52 @@ public class BceFaceBodyAdapter implements IFaceBodyAdapter {
resp.setFirstMatchRate(result.getFirst().getScore()); resp.setFirstMatchRate(result.getFirst().getScore());
} }
return resp; return resp;
} else if (errorCode == 222204) {
// error_code: 222204 表示无法正常访问URL图片,尝试下载并转换为base64后重试
log.warn("搜索人脸时无法正常访问URL图片,错误码: 222204,尝试下载图片转base64后重试,URL: {}", faceUrl);
String base64Image = downloadImageAsBase64(faceUrl);
if (base64Image != null) {
try {
searchFaceLimiter.acquire();
} catch (InterruptedException ignored) {
}
JSONObject retryResponse = client.search(base64Image, "BASE64", dbName, options);
if (retryResponse.getInt("error_code") == 0) {
log.info("使用base64重试搜索人脸成功");
resp.setOriginalFaceScore(100f);
JSONObject resultObj = retryResponse.getJSONObject("result");
if (resultObj == null) {
resp.setFirstMatchRate(0f);
return resp;
}
JSONArray userList = resultObj.getJSONArray("user_list");
List<SearchFaceResultItem> result = new ArrayList<>();
for (int i = 0; i < userList.length(); i++) {
JSONObject user = userList.getJSONObject(i);
SearchFaceResultItem item = new SearchFaceResultItem();
item.setDbName(dbName);
item.setFaceId(user.getString("user_id"));
item.setExtData(user.getString("user_info"));
item.setScore(user.getBigDecimal("score").divide(BigDecimal.valueOf(100), 6, RoundingMode.HALF_UP).floatValue());
result.add(item);
}
resp.setResult(result);
if (!result.isEmpty()) {
resp.setFirstMatchRate(result.getFirst().getScore());
}
return resp;
} else {
log.warn("使用base64重试搜索人脸仍失败!{}", retryResponse);
resp.setOriginalFaceScore(0f);
return resp;
}
} else {
log.error("下载图片转base64失败,无法重试,URL: {}", faceUrl);
resp.setOriginalFaceScore(0f);
return resp;
}
} else { } else {
log.warn("搜索人脸失败,错误码: {}, 响应: {}", errorCode, response);
resp.setOriginalFaceScore(0f); resp.setOriginalFaceScore(0f);
return resp; return resp;
} }
@@ -332,7 +406,53 @@ public class BceFaceBodyAdapter implements IFaceBodyAdapter {
return client; return client;
} }
} }
/**
* 下载图片并转换为base64字符串
*
* @param imageUrl 图片URL
* @return base64编码的图片字符串,失败返回null
*/
private String downloadImageAsBase64(String imageUrl) {
BufferedImage image = null;
ByteArrayOutputStream baos = null;
try {
// 下载图片
URL url = new URL(imageUrl);
image = ImageIO.read(url);
if (image == null) {
log.error("无法读取图片,URL: {}", imageUrl);
return null;
}
// 转换为字节数组
baos = new ByteArrayOutputStream();
String format = "jpg";
if (imageUrl.toLowerCase().endsWith(".png")) {
format = "png";
}
ImageIO.write(image, format, baos);
byte[] imageBytes = baos.toByteArray();
// 编码为base64
return Base64.encode(imageBytes);
} catch (IOException e) {
log.error("下载图片或转换base64失败,URL: {}", imageUrl, e);
return null;
} finally {
if (image != null) {
image.flush();
}
if (baos != null) {
try {
baos.close();
} catch (IOException e) {
log.warn("关闭ByteArrayOutputStream失败", e);
}
}
}
}
private IRateLimiter getLimiter(LOCK_TYPE type) { private IRateLimiter getLimiter(LOCK_TYPE type) {
return switch (type) { return switch (type) {
case ADD_DB -> case ADD_DB ->

View File

@@ -0,0 +1,70 @@
package com.ycwl.basic.handler;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
/**
* Map类型的TypeHandler,用于处理JSON字段与Map<String, Integer>的互转
* 主要用于机位快速评价等自由格式的JSON存储
*/
@Slf4j
public class MapTypeHandler extends BaseTypeHandler<Map<String, Integer>> {
private final ObjectMapper objectMapper = new ObjectMapper();
private final TypeReference<Map<String, Integer>> typeReference = new TypeReference<Map<String, Integer>>() {};
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Map<String, Integer> parameter, JdbcType jdbcType) throws SQLException {
try {
String json = objectMapper.writeValueAsString(parameter);
ps.setString(i, json);
} catch (JsonProcessingException e) {
log.error("序列化Map为JSON失败", e);
throw new SQLException("序列化Map为JSON失败", e);
}
}
@Override
public Map<String, Integer> getNullableResult(ResultSet rs, String columnName) throws SQLException {
String json = rs.getString(columnName);
return parseJson(json);
}
@Override
public Map<String, Integer> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String json = rs.getString(columnIndex);
return parseJson(json);
}
@Override
public Map<String, Integer> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String json = cs.getString(columnIndex);
return parseJson(json);
}
/**
* 解析JSON字符串为Map
*/
private Map<String, Integer> parseJson(String json) {
if (json == null || json.trim().isEmpty()) {
return new HashMap<>();
}
try {
return objectMapper.readValue(json, typeReference);
} catch (JsonProcessingException e) {
log.error("解析JSON为Map失败, json={}", json, e);
return new HashMap<>();
}
}
}

View File

@@ -0,0 +1,77 @@
package com.ycwl.basic.handler;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
/**
* 嵌套Map类型的TypeHandler,用于处理JSON字段与Map<String, Map<String, Integer>>的互转
* 主要用于机位评价功能:外层key为机位ID,内层Map为该机位的各维度评分
*
* 数据格式示例:
* {
* "12345": {"清晰度": 5, "构图": 4, "色彩": 5, "整体效果": 4},
* "12346": {"清晰度": 4, "构图": 5, "色彩": 4, "整体效果": 5}
* }
*/
@Slf4j
public class NestedMapTypeHandler extends BaseTypeHandler<Map<String, Map<String, Integer>>> {
private final ObjectMapper objectMapper = new ObjectMapper();
private final TypeReference<Map<String, Map<String, Integer>>> typeReference =
new TypeReference<Map<String, Map<String, Integer>>>() {};
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Map<String, Map<String, Integer>> parameter, JdbcType jdbcType) throws SQLException {
try {
String json = objectMapper.writeValueAsString(parameter);
ps.setString(i, json);
} catch (JsonProcessingException e) {
log.error("序列化嵌套Map为JSON失败", e);
throw new SQLException("序列化嵌套Map为JSON失败", e);
}
}
@Override
public Map<String, Map<String, Integer>> getNullableResult(ResultSet rs, String columnName) throws SQLException {
String json = rs.getString(columnName);
return parseJson(json);
}
@Override
public Map<String, Map<String, Integer>> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String json = rs.getString(columnIndex);
return parseJson(json);
}
@Override
public Map<String, Map<String, Integer>> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String json = cs.getString(columnIndex);
return parseJson(json);
}
/**
* 解析JSON字符串为嵌套Map
*/
private Map<String, Map<String, Integer>> parseJson(String json) {
if (json == null || json.trim().isEmpty()) {
return new HashMap<>();
}
try {
return objectMapper.readValue(json, typeReference);
} catch (JsonProcessingException e) {
log.error("解析JSON为嵌套Map失败, json={}", json, e);
return new HashMap<>();
}
}
}

View File

@@ -1,4 +1,62 @@
package com.ycwl.basic.image.enhancer; package com.ycwl.basic.image.enhancer;
import com.ycwl.basic.image.enhancer.adapter.BceImageEnhancer;
import com.ycwl.basic.image.enhancer.adapter.BceImageSR;
import com.ycwl.basic.image.enhancer.adapter.IEnhancer;
import com.ycwl.basic.image.enhancer.entity.BceEnhancerConfig;
/**
* 图像增强器工厂
* 用于创建不同类型的图像增强器实例
*/
public class ImageEnhancerFactory { public class ImageEnhancerFactory {
/**
* 增强器类型枚举
*/
public enum EnhancerType {
/**
* 图像清晰度增强
* 使用imageDefinitionEnhance接口,适合提升整体清晰度
*/
DEFINITION_ENHANCE,
/**
* 图像超分辨率
* 使用imageQualityEnhance接口,适合放大图片同时保持质量
*/
SUPER_RESOLUTION
}
/**
* 创建图像增强器
*
* @param type 增强器类型
* @param config 百度云配置
* @return 图像增强器实例
*/
public static IEnhancer createEnhancer(EnhancerType type, BceEnhancerConfig config) {
IEnhancer enhancer = switch (type) {
case DEFINITION_ENHANCE -> new BceImageEnhancer();
case SUPER_RESOLUTION -> new BceImageSR();
};
if (enhancer instanceof BceImageEnhancer) {
((BceImageEnhancer) enhancer).setConfig(config);
} else if (enhancer instanceof BceImageSR) {
((BceImageSR) enhancer).setConfig(config);
}
return enhancer;
}
/**
* 创建默认的图像增强器(清晰度增强)
*
* @param config 百度云配置
* @return 图像增强器实例
*/
public static IEnhancer createDefaultEnhancer(BceEnhancerConfig config) {
return createEnhancer(EnhancerType.DEFINITION_ENHANCE, config);
}
} }

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