Compare commits

..

41 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
66 changed files with 2656 additions and 360 deletions

View File

@@ -273,6 +273,13 @@
<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 --> <!-- Spring Kafka -->
<dependency> <dependency>
<groupId>org.springframework.kafka</groupId> <groupId>org.springframework.kafka</groupId>

View File

@@ -147,6 +147,21 @@ public class OrderBiz {
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;
} }
@@ -215,11 +230,15 @@ 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; 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; break;
case 3: case 3:
@@ -295,10 +314,14 @@ public class OrderBiz {
} }
/** /**
* 检查用户是否购买了指定商品 * 检查用户是否购买了指定商品,并额外校验订单的faceId是否匹配
* 提供给PriceBiz使用,避免循环调用 * @param userId 用户ID
* @param faceId 人脸ID
* @param goodsType 商品类型
* @param goodsId 商品ID
* @return 是否已购买且faceId匹配
*/ */
public boolean checkUserBuyItem(Long userId, int goodsType, Long goodsId) { public boolean checkUserBuyFaceItem(Long userId, Long faceId, int goodsType, Long goodsId) {
return orderRepository.checkUserBuyItem(userId, goodsType, goodsId); return orderRepository.checkUserBuyFaceItem(userId, faceId, goodsType, goodsId);
} }
} }

View File

@@ -76,6 +76,14 @@ public class PriceBiz {
goodsList.add(new GoodsListRespVO(2L, "照片集", 2)); 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; return goodsList;
} }
@@ -125,6 +133,7 @@ public class PriceBiz {
case "PHOTO_LOG": case "PHOTO_LOG":
// 从 template 表查询pLog模板 // 从 template 表查询pLog模板
goodsList.add(new SimpleGoodsRespVO(scenicId, "pLog图<景区打包>", productType));
List<PuzzleTemplateEntity> puzzleList = puzzleTemplateMapper.list(scenicId, null, null); List<PuzzleTemplateEntity> puzzleList = puzzleTemplateMapper.list(scenicId, null, null);
puzzleList.stream() puzzleList.stream()
.map(template -> new SimpleGoodsRespVO(template.getId(), template.getName(), productType)) .map(template -> new SimpleGoodsRespVO(template.getId(), template.getName(), productType))
@@ -272,7 +281,7 @@ public class PriceBiz {
allContentsPurchased = false; allContentsPurchased = false;
break; break;
} }
boolean hasPurchasedTemplate = orderBiz.checkUserBuyItem(userId, -1, videoEntities.getFirst().getVideoId()); boolean hasPurchasedTemplate = orderBiz.checkUserBuyFaceItem(userId, faceId, -1, videoEntities.getFirst().getVideoId());
if (!hasPurchasedTemplate) { if (!hasPurchasedTemplate) {
allContentsPurchased = false; allContentsPurchased = false;
break; break;
@@ -284,7 +293,7 @@ public class PriceBiz {
if (scenicConfig != null) { if (scenicConfig != null) {
// 检查录像集 // 检查录像集
if (!Boolean.TRUE.equals(scenicConfig.getDisableSourceVideo())) { if (!Boolean.TRUE.equals(scenicConfig.getDisableSourceVideo())) {
boolean hasPurchasedRecording = orderBiz.checkUserBuyItem(userId, 1, faceId); boolean hasPurchasedRecording = orderBiz.checkUserBuyFaceItem(userId, faceId, 1, faceId);
if (!hasPurchasedRecording) { if (!hasPurchasedRecording) {
allContentsPurchased = false; allContentsPurchased = false;
} }
@@ -292,7 +301,7 @@ public class PriceBiz {
// 检查照片集 // 检查照片集
if (allContentsPurchased && !Boolean.TRUE.equals(scenicConfig.getDisableSourceImage())) { if (allContentsPurchased && !Boolean.TRUE.equals(scenicConfig.getDisableSourceImage())) {
boolean hasPurchasedPhoto = orderBiz.checkUserBuyItem(userId, 2, faceId); boolean hasPurchasedPhoto = orderBiz.checkUserBuyFaceItem(userId, faceId, 2, faceId);
if (!hasPurchasedPhoto) { if (!hasPurchasedPhoto) {
allContentsPurchased = false; allContentsPurchased = false;
} }

View File

@@ -16,7 +16,12 @@ public enum SourceType {
/** /**
* 图片类型 * 图片类型
*/ */
IMAGE(2, "图片"); IMAGE(2, "图片"),
/**
* AI微单类型
*/
AI_CAM(3, "AI微单");
private final int code; private final int code;
private final String description; private final String description;
@@ -68,4 +73,14 @@ public enum SourceType {
public static boolean isImage(Integer code) { public static boolean isImage(Integer code) {
return code != null && code == IMAGE.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

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

@@ -29,6 +29,7 @@ import com.ycwl.basic.order.dto.PaymentParamsResponse;
import com.ycwl.basic.order.dto.PaymentCallbackResponse; import com.ycwl.basic.order.dto.PaymentCallbackResponse;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
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;
@@ -55,6 +56,7 @@ public class AppOrderV2Controller {
private final VideoTaskRepository videoTaskRepository; private final VideoTaskRepository videoTaskRepository;
private final TemplateRepository templateRepository; private final TemplateRepository templateRepository;
private final VideoRepository videoRepository; private final VideoRepository videoRepository;
private final RedisTemplate<String, Object> redisTemplate;
/** /**
* 移动端价格计算 * 移动端价格计算
@@ -86,7 +88,7 @@ public class AppOrderV2Controller {
TaskEntity task = videoTaskRepository.getTaskById(video.getTaskId()); TaskEntity task = videoTaskRepository.getTaskById(video.getTaskId());
request.setFaceId(task.getFaceId()); request.setFaceId(task.getFaceId());
} }
case RECORDING_SET, PHOTO_SET -> request.setFaceId(Long.valueOf(productItem.getProductId())); case RECORDING_SET, PHOTO_SET, AI_CAM_PHOTO_SET -> request.setFaceId(Long.valueOf(productItem.getProductId()));
} }
} }
@@ -119,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;
@@ -341,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

@@ -205,6 +205,10 @@ public class AppPuzzleController {
// 设置模板ID // 设置模板ID
vo.setTemplateId(record.getTemplateId()); 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()); IsBuyRespVO isBuyRespVO = orderBiz.isBuy(face.getScenicId(), face.getMemberId(), face.getId(), 5, record.getTemplateId());
if (isBuyRespVO.isBuy()) { if (isBuyRespVO.isBuy()) {
vo.setIsBuy(1); vo.setIsBuy(1);
@@ -227,6 +231,7 @@ public class AppPuzzleController {
vo.setFreeCount(1); vo.setFreeCount(1);
} }
} }
}
return vo; return vo;
} }
} }

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.createTaskByFaceIdAndTemplateId(videoTaskReq.getFaceId(),videoTaskReq.getTemplateId(),0); taskService.createTaskByFaceIdAndTemplateId(videoTaskReq.getFaceId(),videoTaskReq.getTemplateId(),false);
return ApiResponse.success("成功"); return ApiResponse.success("成功");
} }
} }

View File

@@ -4,6 +4,7 @@ package com.ycwl.basic.controller.printer;
import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.DateUtil;
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter; import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
import com.ycwl.basic.mapper.FaceMapper; 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.face.entity.FaceEntity;
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity; import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
import com.ycwl.basic.model.task.resp.SearchFaceRespVo; import com.ycwl.basic.model.task.resp.SearchFaceRespVo;
@@ -99,14 +100,20 @@ public class PrinterTvController {
@GetMapping("/{sampleId}/qrcode") @GetMapping("/{sampleId}/qrcode")
public void getQrcode(@PathVariable("sampleId") Long sampleId, HttpServletResponse response) throws Exception { public void getQrcode(@PathVariable("sampleId") Long sampleId, HttpServletResponse response) throws Exception {
File qrcode = new File("qrcode_"+sampleId+".jpg"); File qrcode = new File("qrcode_"+sampleId+".jpg");
try {
FaceSampleEntity faceSample = faceRepository.getFaceSample(sampleId); FaceSampleEntity faceSample = faceRepository.getFaceSample(sampleId);
if (faceSample == null) { if (faceSample == null) {
response.setStatus(404); response.setStatus(404);
return; 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()); MpConfigEntity scenicMpConfig = scenicRepository.getScenicMpConfig(faceSample.getScenicId());
WxMpUtil.generateUnlimitedWXAQRCode(scenicMpConfig.getAppId(), scenicMpConfig.getAppSecret(), "pages/printer/from_sample", sampleId.toString(), qrcode); WxMpUtil.generateUnlimitedWXAQRCode(scenicMpConfig.getAppId(), scenicMpConfig.getAppSecret(), targetPath, sampleId.toString(), qrcode);
// 设置响应头 // 设置响应头
response.setContentType("image/jpeg"); response.setContentType("image/jpeg");

View File

@@ -74,22 +74,31 @@ public class PuzzleGenerationOrchestrator {
// 3. 准备公共动态数据 // 3. 准备公共动态数据
Map<String, String> baseDynamicData = buildBaseDynamicData(faceId, faceUrl, scenicBasic); Map<String, String> baseDynamicData = buildBaseDynamicData(faceId, faceUrl, scenicBasic);
// 4. 遍历所有模板,逐个生成 // 4. 使用虚拟线程池并行生成所有模板
int successCount = 0; java.util.concurrent.atomic.AtomicInteger successCount = new java.util.concurrent.atomic.AtomicInteger(0);
int failCount = 0; java.util.concurrent.atomic.AtomicInteger failCount = new java.util.concurrent.atomic.AtomicInteger(0);
for (PuzzleTemplateDTO template : templateList) {
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 { try {
generateSingleTemplate(scenicId, faceId, memberId, template, baseDynamicData); generateSingleTemplate(scenicId, faceId, memberId, template, baseDynamicData);
successCount++; successCount.incrementAndGet();
} catch (Exception e) { } catch (Exception e) {
log.error("拼图生成失败: scenicId={}, templateCode={}, templateName={}", log.error("拼图生成失败: scenicId={}, templateCode={}, templateName={}",
scenicId, template.getCode(), template.getName(), e); scenicId, template.getCode(), template.getName(), e);
failCount++; failCount.incrementAndGet();
} }
}, executor))
.toList();
// 等待所有任务完成
java.util.concurrent.CompletableFuture.allOf(futures.toArray(new java.util.concurrent.CompletableFuture[0])).join();
} }
log.info("景区拼图模板批量生成完成: scenicId={}, 总数={}, 成功={}, 失败={}", log.info("景区拼图模板批量生成完成: scenicId={}, 总数={}, 成功={}, 失败={}",
scenicId, templateList.size(), successCount, failCount); scenicId, templateList.size(), successCount.get(), failCount.get());
} catch (Exception e) { } catch (Exception e) {
// 异步任务失败不影响主流程,仅记录日志 // 异步任务失败不影响主流程,仅记录日志

View File

@@ -22,7 +22,13 @@ public enum PipelineScene {
* 源图片超分辨率增强场景 * 源图片超分辨率增强场景
* IPC设备拍摄的源图片进行质量提升 * IPC设备拍摄的源图片进行质量提升
*/ */
SOURCE_PHOTO_SUPER_RESOLUTION("source_photo_sr", "源图片超分辨率增强"); SOURCE_PHOTO_SUPER_RESOLUTION("source_photo_sr", "源图片超分辨率增强"),
/**
* AI相机照片增强场景
* AI相机拍摄的照片进行超分辨率和质量增强
*/
AI_CAM_ENHANCE("ai_cam_enhance", "AI相机照片增强");
private final String code; private final String code;
private final String description; private final String description;

View File

@@ -0,0 +1,126 @@
package com.ycwl.basic.image.pipeline.stages;
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
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 javax.imageio.ImageIO;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.File;
/**
* 图像缩放Stage
* 支持按比例放大或缩小图片
*/
@Slf4j
@StageConfig(
stageId = "image_resize",
optionalMode = StageOptionalMode.SUPPORT,
description = "图像缩放处理",
defaultEnabled = true
)
public class ImageResizeStage extends AbstractPipelineStage<PhotoProcessContext> {
private final double scaleFactor;
/**
* 构造函数
* @param scaleFactor 缩放比例(例如: 1.5表示放大1.5倍, 0.333表示缩小到1/3)
*/
public ImageResizeStage(double scaleFactor) {
if (scaleFactor <= 0) {
throw new IllegalArgumentException("scaleFactor must be positive");
}
this.scaleFactor = scaleFactor;
}
@Override
public String getName() {
return "ImageResizeStage";
}
@Override
protected StageResult<PhotoProcessContext> doExecute(PhotoProcessContext context) {
File currentFile = context.getCurrentFile();
if (currentFile == null || !currentFile.exists()) {
return StageResult.skipped("当前文件不存在");
}
BufferedImage originalImage = null;
BufferedImage resizedImage = null;
try {
log.debug("开始图像缩放处理: file={}, scaleFactor={}", currentFile.getName(), scaleFactor);
// 读取原图
originalImage = ImageIO.read(currentFile);
if (originalImage == null) {
return StageResult.failed("无法读取图片文件");
}
int originalWidth = originalImage.getWidth();
int originalHeight = originalImage.getHeight();
// 计算新尺寸
int newWidth = (int) Math.round(originalWidth * scaleFactor);
int newHeight = (int) Math.round(originalHeight * scaleFactor);
// 检查尺寸是否合理
if (newWidth <= 0 || newHeight <= 0) {
return StageResult.failed("缩放后尺寸无效: " + newWidth + "x" + newHeight);
}
// 创建缩放后的图像
resizedImage = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = resizedImage.createGraphics();
try {
// 设置高质量渲染选项
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// 执行缩放
g2d.drawImage(originalImage, 0, 0, newWidth, newHeight, null);
} finally {
g2d.dispose();
}
// 保存缩放后的图片
File resizedFile = context.getTempFileManager().createTempFile("resized", ".jpg");
ImageIO.write(resizedImage, "jpg", resizedFile);
if (!resizedFile.exists() || resizedFile.length() == 0) {
return StageResult.failed("缩放后图片保存失败");
}
// 更新处理后的文件
context.updateProcessedFile(resizedFile);
log.info("图像缩放完成: {}x{} -> {}x{} (比例: {})",
originalWidth, originalHeight,
newWidth, newHeight,
scaleFactor);
return StageResult.success(String.format("缩放完成 (%dx%d -> %dx%d)",
originalWidth, originalHeight, newWidth, newHeight));
} catch (Exception e) {
log.error("图像缩放失败: {}", e.getMessage(), e);
return StageResult.failed("缩放失败: " + e.getMessage(), e);
} finally {
// 释放图像资源
if (originalImage != null) {
originalImage.flush();
}
if (resizedImage != null) {
resizedImage.flush();
}
}
}
}

View File

@@ -0,0 +1,82 @@
package com.ycwl.basic.image.pipeline.stages;
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
import com.ycwl.basic.mapper.PrinterMapper;
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;
/**
* 更新MemberPrint记录Stage
* 用于更新member_print表中的cropUrl字段
*/
@Slf4j
@StageConfig(
stageId = "update_member_print",
optionalMode = StageOptionalMode.UNSUPPORT,
description = "更新MemberPrint记录",
defaultEnabled = true
)
public class UpdateMemberPrintStage extends AbstractPipelineStage<PhotoProcessContext> {
private final PrinterMapper printerMapper;
private final Integer memberPrintId;
private final Long memberId;
private final Long scenicId;
/**
* 构造函数
* @param printerMapper PrinterMapper实例
* @param memberPrintId MemberPrint记录ID
* @param memberId 用户ID
* @param scenicId 景区ID
*/
public UpdateMemberPrintStage(PrinterMapper printerMapper, Integer memberPrintId, Long memberId, Long scenicId) {
this.printerMapper = printerMapper;
this.memberPrintId = memberPrintId;
this.memberId = memberId;
this.scenicId = scenicId;
}
@Override
public String getName() {
return "UpdateMemberPrintStage";
}
@Override
protected StageResult<PhotoProcessContext> doExecute(PhotoProcessContext context) {
String resultUrl = context.getResultUrl();
if (resultUrl == null || resultUrl.trim().isEmpty()) {
return StageResult.skipped("结果URL为空,跳过更新");
}
if (memberPrintId == null || memberId == null || scenicId == null) {
log.warn("MemberPrint更新参数不完整: memberPrintId={}, memberId={}, scenicId={}",
memberPrintId, memberId, scenicId);
return StageResult.skipped("更新参数不完整");
}
try {
log.debug("开始更新MemberPrint记录: id={}, newCropUrl={}", memberPrintId, resultUrl);
// 更新cropUrl字段
int rows = printerMapper.setPhotoCropped(memberId, scenicId, memberPrintId.longValue(), resultUrl, null);
if (rows > 0) {
log.info("MemberPrint记录更新成功: id={}, cropUrl已更新", memberPrintId);
return StageResult.success("更新成功");
} else {
log.warn("MemberPrint记录更新失败: 可能记录不存在, id={}", memberPrintId);
return StageResult.degraded("更新失败,记录可能不存在");
}
} catch (Exception e) {
log.error("更新MemberPrint记录异常: id={}", memberPrintId, e);
// 更新失败不影响整个流程,使用降级状态
return StageResult.degraded("更新异常: " + e.getMessage());
}
}
}

View File

@@ -34,4 +34,11 @@ public class WatermarkConfig {
* 二维码文件 * 二维码文件
*/ */
private final File qrcodeFile; private final File qrcodeFile;
/**
* 缩放倍数,用于将所有定位和大小乘以该倍数
* 默认值为 1.0(不缩放)
*/
@Builder.Default
private final Double scale = 1.0;
} }

View File

@@ -170,6 +170,12 @@ public class WatermarkStage extends AbstractPipelineStage<PhotoProcessContext> {
info.setQrcodeFile(qrcodeFile); info.setQrcodeFile(qrcodeFile);
} }
// 从 config 读取缩放倍数
Double scale = config.getScale();
if (scale != null) {
info.setScale(scale);
}
// 根据旋转状态自己处理 offsetLeft // 根据旋转状态自己处理 offsetLeft
if (context.isRotationApplied()) { if (context.isRotationApplied()) {
if (context.getImageRotation() == 90) { if (context.getImageRotation() == 90) {

View File

@@ -33,6 +33,13 @@ public class WatermarkInfo {
private Integer offsetLeft; private Integer offsetLeft;
private Integer offsetRight; private Integer offsetRight;
/**
* 缩放倍数,用于将所有定位和大小乘以该倍数
* 例如: scale=2.0 表示所有尺寸和位置都放大2倍
* null 表示使用默认值1.0(不缩放)
*/
private Double scale;
public String getDatetimeLine() { public String getDatetimeLine() {
if (datetimeLine == null) { if (datetimeLine == null) {
datetimeLine = DateUtil.format(datetime, dtFormat); datetimeLine = DateUtil.format(datetime, dtFormat);

View File

@@ -64,11 +64,14 @@ public class PrinterDefaultWatermarkOperator implements IOperator {
@Override @Override
public File process(WatermarkInfo info) throws ImageWatermarkException { public File process(WatermarkInfo info) throws ImageWatermarkException {
// 获取四边偏移值,优先使用传入的值,否则使用默认值 // 获取缩放倍数,默认为1.0(不缩放)
int offsetTop = info.getOffsetTop() != null ? info.getOffsetTop() : DEFAULT_OFFSET_TOP; double scale = info.getScale() != null ? info.getScale() : 1.0;
int offsetBottom = info.getOffsetBottom() != null ? info.getOffsetBottom() : DEFAULT_OFFSET_BOTTOM;
int offsetLeft = info.getOffsetLeft() != null ? info.getOffsetLeft() : DEFAULT_OFFSET_LEFT; // 获取四边偏移值,优先使用传入的值,否则使用默认值,并应用缩放
int offsetRight = info.getOffsetRight() != null ? info.getOffsetRight() : DEFAULT_OFFSET_RIGHT; int offsetTop = (int) ((info.getOffsetTop() != null ? info.getOffsetTop() : DEFAULT_OFFSET_TOP) * scale);
int offsetBottom = (int) ((info.getOffsetBottom() != null ? info.getOffsetBottom() : DEFAULT_OFFSET_BOTTOM) * scale);
int offsetLeft = (int) ((info.getOffsetLeft() != null ? info.getOffsetLeft() : DEFAULT_OFFSET_LEFT) * scale);
int offsetRight = (int) ((info.getOffsetRight() != null ? info.getOffsetRight() : DEFAULT_OFFSET_RIGHT) * scale);
BufferedImage baseImage; BufferedImage baseImage;
BufferedImage qrcodeImage; BufferedImage qrcodeImage;
@@ -86,17 +89,26 @@ public class PrinterDefaultWatermarkOperator implements IOperator {
} catch (IOException e) { } catch (IOException e) {
throw new ImageWatermarkException("图片打开失败"); throw new ImageWatermarkException("图片打开失败");
} }
// 应用缩放到所有常量
int scaledExtraBorder = (int) (EXTRA_BORDER_PX * scale);
int scaledOffsetY = (int) (OFFSET_Y * scale);
int scaledQrcodeSize = (int) (QRCODE_SIZE * scale);
int scaledQrcodeOffsetY = (int) (QRCODE_OFFSET_Y * scale);
int scaledScenicFontSize = (int) (SCENIC_FONT_SIZE * scale);
int scaledDatetimeFontSize = (int) (DATETIME_FONT_SIZE * scale);
// 新图像画布 // 新图像画布
BufferedImage newImage = new BufferedImage(baseImage.getWidth() + 2 * EXTRA_BORDER_PX, baseImage.getHeight() + 2 * EXTRA_BORDER_PX, BufferedImage.TYPE_INT_RGB); BufferedImage newImage = new BufferedImage(baseImage.getWidth() + 2 * scaledExtraBorder, baseImage.getHeight() + 2 * scaledExtraBorder, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = newImage.createGraphics(); Graphics2D g2d = newImage.createGraphics();
g2d.setColor(BG_COLOR); g2d.setColor(BG_COLOR);
g2d.fillRect(0, 0, newImage.getWidth(), newImage.getHeight()); g2d.fillRect(0, 0, newImage.getWidth(), newImage.getHeight());
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.drawImage(baseImage, EXTRA_BORDER_PX, EXTRA_BORDER_PX, null); g2d.drawImage(baseImage, scaledExtraBorder, scaledExtraBorder, null);
int newQrcodeHeight = QRCODE_SIZE; int newQrcodeHeight = scaledQrcodeSize;
int newQrcodeWidth = (int) (newQrcodeHeight * 1.0 / qrcodeImage.getHeight() * qrcodeImage.getWidth()); int newQrcodeWidth = (int) (newQrcodeHeight * 1.0 / qrcodeImage.getHeight() * qrcodeImage.getWidth());
Font scenicFont = new Font(defaultFontName, Font.BOLD, SCENIC_FONT_SIZE); Font scenicFont = new Font(defaultFontName, Font.BOLD, scaledScenicFontSize);
Font datetimeFont = new Font(defaultFontName, Font.BOLD, DATETIME_FONT_SIZE); Font datetimeFont = new Font(defaultFontName, Font.BOLD, scaledDatetimeFontSize);
FontMetrics scenicFontMetrics = g2d.getFontMetrics(scenicFont); FontMetrics scenicFontMetrics = g2d.getFontMetrics(scenicFont);
FontMetrics datetimeFontMetrics = g2d.getFontMetrics(datetimeFont); FontMetrics datetimeFontMetrics = g2d.getFontMetrics(datetimeFont);
int scenicLineHeight = scenicFontMetrics.getHeight(); int scenicLineHeight = scenicFontMetrics.getHeight();
@@ -106,13 +118,14 @@ public class PrinterDefaultWatermarkOperator implements IOperator {
// 二维码放置在左下角,距离左边缘图片宽度的5%,再加上左侧偏移 // 二维码放置在左下角,距离左边缘图片宽度的5%,再加上左侧偏移
int qrcodeOffsetX = (int) (newImage.getWidth() * QRCODE_LEFT_MARGIN_RATIO) + offsetLeft; int qrcodeOffsetX = (int) (newImage.getWidth() * QRCODE_LEFT_MARGIN_RATIO) + offsetLeft;
int qrcodeOffsetY = EXTRA_BORDER_PX + baseImage.getHeight() - OFFSET_Y - newQrcodeHeight - offsetBottom; int qrcodeOffsetY = scaledExtraBorder + baseImage.getHeight() - scaledOffsetY - newQrcodeHeight - offsetBottom;
Shape originalClip = g2d.getClip(); Shape originalClip = g2d.getClip();
// 创建比二维码大10像素的白色圆形背景 // 创建比二维码大10像素的白色圆形背景(10像素也要缩放)
int whiteCircleSize = Math.max(newQrcodeWidth, newQrcodeHeight) + 10; int whiteCirclePadding = (int) (10 * scale);
int whiteCircleSize = Math.max(newQrcodeWidth, newQrcodeHeight) + whiteCirclePadding;
int whiteCircleX = qrcodeOffsetX - (whiteCircleSize - newQrcodeWidth) / 2; int whiteCircleX = qrcodeOffsetX - (whiteCircleSize - newQrcodeWidth) / 2;
int whiteCircleY = qrcodeOffsetY + QRCODE_OFFSET_Y - (whiteCircleSize - newQrcodeHeight) / 2; int whiteCircleY = qrcodeOffsetY + scaledQrcodeOffsetY - (whiteCircleSize - newQrcodeHeight) / 2;
// 绘制白色圆形背景 // 绘制白色圆形背景
g2d.setColor(Color.WHITE); g2d.setColor(Color.WHITE);
@@ -122,7 +135,7 @@ public class PrinterDefaultWatermarkOperator implements IOperator {
// 用白色圆形尺寸裁切二维码(保持二维码原始尺寸,但用大圆裁切) // 用白色圆形尺寸裁切二维码(保持二维码原始尺寸,但用大圆裁切)
Ellipse2D qrcodeCircle = new Ellipse2D.Double(whiteCircleX, whiteCircleY, whiteCircleSize, whiteCircleSize); Ellipse2D qrcodeCircle = new Ellipse2D.Double(whiteCircleX, whiteCircleY, whiteCircleSize, whiteCircleSize);
g2d.setClip(qrcodeCircle); g2d.setClip(qrcodeCircle);
g2d.drawImage(qrcodeImage, qrcodeOffsetX, qrcodeOffsetY + QRCODE_OFFSET_Y, newQrcodeWidth, newQrcodeHeight, null); g2d.drawImage(qrcodeImage, qrcodeOffsetX, qrcodeOffsetY + scaledQrcodeOffsetY, newQrcodeWidth, newQrcodeHeight, null);
g2d.setClip(originalClip); g2d.setClip(originalClip);
// 在圆形二维码中央绘制圆形头像 // 在圆形二维码中央绘制圆形头像
@@ -130,7 +143,7 @@ public class PrinterDefaultWatermarkOperator implements IOperator {
// 计算圆形头像的尺寸和位置 // 计算圆形头像的尺寸和位置
int avatarDiameter = (int) (newQrcodeHeight * 0.45); int avatarDiameter = (int) (newQrcodeHeight * 0.45);
int avatarX = qrcodeOffsetX + (newQrcodeWidth - avatarDiameter) / 2; int avatarX = qrcodeOffsetX + (newQrcodeWidth - avatarDiameter) / 2;
int avatarY = qrcodeOffsetY + QRCODE_OFFSET_Y + (newQrcodeHeight - avatarDiameter) / 2; int avatarY = qrcodeOffsetY + scaledQrcodeOffsetY + (newQrcodeHeight - avatarDiameter) / 2;
// 保存当前的渲染设置 // 保存当前的渲染设置
RenderingHints originalHints = g2d.getRenderingHints(); RenderingHints originalHints = g2d.getRenderingHints();
@@ -149,10 +162,10 @@ public class PrinterDefaultWatermarkOperator implements IOperator {
double faceHeight = faceImage.getHeight(); double faceHeight = faceImage.getHeight();
double scaleX = avatarDiameter / faceWidth; double scaleX = avatarDiameter / faceWidth;
double scaleY = avatarDiameter / faceHeight; double scaleY = avatarDiameter / faceHeight;
double scale = Math.max(scaleX, scaleY); // 使用较大的缩放比例以填满圆形 double faceScale = Math.max(scaleX, scaleY); // 使用较大的缩放比例以填满圆形
int scaledWidth = (int) (faceWidth * scale); int scaledWidth = (int) (faceWidth * faceScale);
int scaledHeight = (int) (faceHeight * scale); int scaledHeight = (int) (faceHeight * faceScale);
// 计算居中位置 // 计算居中位置
int faceDrawX = avatarX + (avatarDiameter - scaledWidth) / 2; int faceDrawX = avatarX + (avatarDiameter - scaledWidth) / 2;
@@ -167,7 +180,7 @@ public class PrinterDefaultWatermarkOperator implements IOperator {
} }
// 计算文字与二维码垂直居中对齐的Y坐标 // 计算文字与二维码垂直居中对齐的Y坐标
int qrcodeTop = qrcodeOffsetY + QRCODE_OFFSET_Y; int qrcodeTop = qrcodeOffsetY + scaledQrcodeOffsetY;
int qrcodeBottom = qrcodeTop + newQrcodeHeight; int qrcodeBottom = qrcodeTop + newQrcodeHeight;
int qrcodeCenter = (qrcodeTop + qrcodeBottom) / 2; int qrcodeCenter = (qrcodeTop + qrcodeBottom) / 2;

View File

@@ -0,0 +1,17 @@
package com.ycwl.basic.integration.glm;
import java.util.List;
/**
* 智谱 GLM 模型调用抽象。
*/
public interface GlmClient {
/**
* 流式回复,实时回调分片,同时返回完整文本。
*/
String streamReply(Long faceId,
Long memberId,
String traceId,
List<ai.z.openapi.service.model.ChatMessage> messages,
java.util.function.Consumer<String> chunkConsumer);
}

View File

@@ -0,0 +1,118 @@
package com.ycwl.basic.integration.glm;
import ai.z.openapi.ZhipuAiClient;
import ai.z.openapi.service.model.ChatCompletionCreateParams;
import ai.z.openapi.service.model.ChatCompletionResponse;
import ai.z.openapi.service.model.ChatMessage;
import ai.z.openapi.service.model.ChatMessageRole;
import ai.z.openapi.service.model.ChatThinking;
import ai.z.openapi.service.model.Delta;
import ai.z.openapi.service.model.ModelData;
import io.reactivex.rxjava3.core.Flowable;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
/**
* 智谱 GLM 官方 SDK 调用实现,流式拆分文本。
*/
@Slf4j
@Component
public class GlmClientImpl implements GlmClient {
private static final String DEFAULT_MODEL = "glm-4.5-airx";
private final ZhipuAiClient client;
public GlmClientImpl(@Value("${zhipu.api-key:}") String apiKeyFromCfg) {
String apiKey = StringUtils.isNotBlank(apiKeyFromCfg) ? apiKeyFromCfg : System.getenv("ZHIPU_API_KEY");
if (StringUtils.isBlank(apiKey)) {
log.warn("未配置 ZHIPU_API_KEY,GLM 调用将返回占位结果");
client = null;
} else {
client = ZhipuAiClient.builder().apiKey(apiKey).build();
}
}
@Override
public String streamReply(Long faceId,
Long memberId,
String traceId,
List<ChatMessage> messages,
Consumer<String> chunkConsumer) {
if (client == null) {
String fallback = "GLM 未配置 API KEY,无法调用。";
if (chunkConsumer != null) {
chunkConsumer.accept(fallback);
}
return fallback;
}
List<ChatMessage> finalMessages = messages == null ? new ArrayList<>() : new ArrayList<>(messages);
if (finalMessages.isEmpty()) {
finalMessages.add(ChatMessage.builder()
.role(ChatMessageRole.USER.value())
.content("你好")
.build());
}
finalMessages.addFirst(ChatMessage.builder()
.role(ChatMessageRole.SYSTEM.value())
.content("你是一个智能销售,然后用户是实际想购买的人。你需要尽可能推广景区生成的Vlog、视频、照片等内容给用户,鼓励购买。注意,返回内容需要为普通文本格式,禁止出现Markdown标记。")
.build());
finalMessages.addFirst(ChatMessage.builder()
.role(ChatMessageRole.SYSTEM.value())
.content("注意,返回内容需要为普通文本格式,禁止使用Markdown格式进行返回。")
.build());
ChatCompletionCreateParams request = ChatCompletionCreateParams.builder()
.model(DEFAULT_MODEL)
.messages(finalMessages)
.thinking(ChatThinking.builder().type("enabled").build())
.stream(true)
.maxTokens(4096)
.temperature(0.8f)
.build();
ChatCompletionResponse response = client.chat().createChatCompletion(request);
if (!response.isSuccess()) {
String msg = "GLM 调用失败: " + response.getMsg();
log.warn(msg);
if (chunkConsumer != null) {
chunkConsumer.accept(msg);
}
return msg;
}
StringBuilder sb = new StringBuilder();
Flowable<ModelData> flowable = response.getFlowable();
flowable.blockingSubscribe(
data -> {
if (data.getChoices() == null || data.getChoices().isEmpty()) {
return;
}
Delta delta = data.getChoices().getFirst().getDelta();
if (delta == null) {
return;
}
String piece = delta.getContent();
if (StringUtils.isNotBlank(piece)) {
sb.append(piece);
if (chunkConsumer != null) {
chunkConsumer.accept(piece);
}
}
},
error -> {
log.error("GLM 流式调用异常", error);
String err = "GLM 调用异常:" + error.getMessage();
sb.append(err);
if (chunkConsumer != null) {
chunkConsumer.accept(err);
}
}
);
return sb.toString();
}
}

View File

@@ -0,0 +1,16 @@
package com.ycwl.basic.mapper;
import com.ycwl.basic.model.mobile.chat.entity.FaceChatConversationEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface FaceChatConversationMapper {
FaceChatConversationEntity findByFaceId(@Param("faceId") Long faceId);
FaceChatConversationEntity getById(@Param("id") Long id);
int insert(FaceChatConversationEntity entity);
int updateStatus(@Param("id") Long id, @Param("status") String status);
}

View File

@@ -0,0 +1,24 @@
package com.ycwl.basic.mapper;
import com.ycwl.basic.model.mobile.chat.entity.FaceChatMessageEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface FaceChatMessageMapper {
Integer maxSeqForUpdate(@Param("conversationId") Long conversationId);
int insert(FaceChatMessageEntity entity);
List<FaceChatMessageEntity> listByConversation(@Param("conversationId") Long conversationId,
@Param("cursor") Integer cursor,
@Param("limit") Integer limit);
/**
* 按 seq 倒序获取最近若干条消息,用于拼接上下文。
*/
List<FaceChatMessageEntity> listRecentByConversation(@Param("conversationId") Long conversationId,
@Param("limit") Integer limit);
}

View File

@@ -3,10 +3,19 @@ package com.ycwl.basic.mapper;
import com.ycwl.basic.model.pc.faceDetectLog.entity.FaceDetectLogAiCamEntity; import com.ycwl.basic.model.pc.faceDetectLog.entity.FaceDetectLogAiCamEntity;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/** /**
* AI相机人脸识别日志Mapper * AI相机人脸识别日志Mapper
*/ */
@Mapper @Mapper
public interface FaceDetectLogAiCamMapper { public interface FaceDetectLogAiCamMapper {
int add(FaceDetectLogAiCamEntity entity); int add(FaceDetectLogAiCamEntity entity);
/**
* 根据faceId查询所有识别记录
* @param faceId 人脸ID
* @return 识别记录列表
*/
List<FaceDetectLogAiCamEntity> listByFaceId(Long faceId);
} }

View File

@@ -82,6 +82,7 @@ public interface SourceMapper {
List<SourceEntity> listVideoByFaceRelation(Long faceId); List<SourceEntity> listVideoByFaceRelation(Long faceId);
List<SourceEntity> listImageByFaceRelation(Long faceId); List<SourceEntity> listImageByFaceRelation(Long faceId);
List<SourceEntity> listAiCamImageByFaceRelation(Long faceId);
List<MemberSourceEntity> listByFaceRelation(Long faceId, Integer type); List<MemberSourceEntity> listByFaceRelation(Long faceId, Integer type);
SourceEntity getEntity(Long id); SourceEntity getEntity(Long id);
@@ -148,4 +149,20 @@ public interface SourceMapper {
* @return source实体 * @return source实体
*/ */
SourceEntity getSourceByFaceAndDeviceId(Long faceId, Long deviceId, Integer type, String sortStrategy); SourceEntity getSourceByFaceAndDeviceId(Long faceId, Long deviceId, Integer type, String sortStrategy);
/**
* 根据faceSampleId列表和type查询source列表
* @param faceSampleIds faceSampleId列表
* @param type 素材类型
* @return source实体列表
*/
List<SourceEntity> listByFaceSampleIdsAndType(List<Long> faceSampleIds, Integer type);
/**
* 删除指定faceId和type的member_source关联记录
* @param faceId 人脸ID
* @param type 素材类型
* @return 删除的记录数
*/
int deleteRelationsByFaceIdAndType(Long faceId, Integer type);
} }

View File

@@ -1,11 +1,15 @@
package com.ycwl.basic.model; package com.ycwl.basic.model;
import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor;
/** /**
* 裁剪信息 * 裁剪信息
*/ */
@Data @Data
@AllArgsConstructor
@NoArgsConstructor
public class Crop { public class Crop {
private Integer rotation; private Integer rotation;
} }

View File

@@ -0,0 +1,14 @@
package com.ycwl.basic.model.mobile.chat;
import lombok.Data;
/**
* 会话信息返回对象。
*/
@Data
public class ChatConversationVO {
private Long conversationId;
private Long faceId;
private String status;
private String model;
}

View File

@@ -0,0 +1,17 @@
package com.ycwl.basic.model.mobile.chat;
import lombok.Data;
import java.util.List;
/**
* 消息列表响应。
*/
@Data
public class ChatMessagePageResp {
private List<ChatMessageVO> messages;
/**
* 下一条游标(返回最后一条 seq)。
*/
private Integer nextCursor;
}

View File

@@ -0,0 +1,18 @@
package com.ycwl.basic.model.mobile.chat;
import lombok.Data;
import java.util.Date;
/**
* 聊天消息视图对象。
*/
@Data
public class ChatMessageVO {
private Long id;
private Integer seq;
private String role;
private String content;
private String traceId;
private Date createdAt;
}

View File

@@ -0,0 +1,22 @@
package com.ycwl.basic.model.mobile.chat;
import lombok.Data;
/**
* 发送消息请求体。
*/
@Data
public class ChatSendMessageReq {
/**
* 用户输入的文本内容。
*/
private String content;
/**
* 链路追踪ID,前端可透传,没有则服务端生成。
*/
private String traceId;
/**
* 是否期望流式返回。
*/
private Boolean stream;
}

View File

@@ -0,0 +1,13 @@
package com.ycwl.basic.model.mobile.chat;
import lombok.Data;
/**
* 发送消息同步响应。
*/
@Data
public class ChatSendMessageResp {
private ChatMessageVO userMessage;
private ChatMessageVO assistantMessage;
private String traceId;
}

View File

@@ -0,0 +1,16 @@
package com.ycwl.basic.model.mobile.chat;
import lombok.Data;
import java.util.List;
/**
* 流式发送消息的服务结果。
*/
@Data
public class ChatSendMessageStreamResp {
private ChatMessageVO userMessage;
private ChatMessageVO assistantMessage;
private String traceId;
private List<String> chunks;
}

View File

@@ -0,0 +1,35 @@
package com.ycwl.basic.model.mobile.chat.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
/**
* 小程序人脸聊天会话,一脸一会话。
*/
@Data
@TableName("face_chat_conversation")
public class FaceChatConversationEntity {
@TableId
private Long id;
/**
* 对应的人脸ID。
*/
private Long faceId;
/**
* 归属用户ID,冗余校验越权。
*/
private Long memberId;
/**
* 会话状态 active/closed。
*/
private String status;
/**
* 使用的模型名称,例如 glm-v。
*/
private String model;
private Date createdAt;
private Date updatedAt;
}

View File

@@ -0,0 +1,28 @@
package com.ycwl.basic.model.mobile.chat.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
/**
* 小程序人脸聊天消息,只保存文本。
*/
@Data
@TableName("face_chat_message")
public class FaceChatMessageEntity {
@TableId
private Long id;
private Long conversationId;
private Long faceId;
private Integer seq;
/**
* user / assistant / system。
*/
private String role;
private String content;
private String traceId;
private Integer latencyMs;
private Date createdAt;
}

View File

@@ -24,4 +24,5 @@ public class MemberPrintEntity {
private Integer status; private Integer status;
private Date createTime; private Date createTime;
private Date updateTime; private Date updateTime;
private String crop;
} }

View File

@@ -17,6 +17,7 @@ public enum ProductType {
// 照片类 // 照片类
PHOTO("PHOTO", "照片", ProductCategory.PHOTO), PHOTO("PHOTO", "照片", ProductCategory.PHOTO),
PHOTO_SET("PHOTO_SET", "照片集", ProductCategory.PHOTO), PHOTO_SET("PHOTO_SET", "照片集", ProductCategory.PHOTO),
AI_CAM_PHOTO_SET("AI_CAM_PHOTO_SET", "照片集", ProductCategory.PHOTO),
PHOTO_LOG("PHOTO_LOG", "pLog图", ProductCategory.PHOTO), PHOTO_LOG("PHOTO_LOG", "pLog图", ProductCategory.PHOTO),
// 视频类(素材视频) // 视频类(素材视频)

View File

@@ -89,22 +89,22 @@ public interface PriceCouponClaimRecordMapper extends BaseMapper<PriceCouponClai
"LEFT JOIN price_coupon_config c ON r.coupon_id = c.id " + "LEFT JOIN price_coupon_config c ON r.coupon_id = c.id " +
"<where>" + "<where>" +
"<if test='userId != null'>" + "<if test='userId != null'>" +
"AND r.user_id = #{userId}" + "AND r.user_id = #{userId} " +
"</if>" + "</if>" +
"<if test='couponId != null'>" + "<if test='couponId != null'>" +
"AND r.coupon_id = #{couponId}" + "AND r.coupon_id = #{couponId} " +
"</if>" + "</if>" +
"<if test='status != null'>" + "<if test='status != null'>" +
"AND r.status = #{status}" + "AND r.status = #{status} " +
"</if>" + "</if>" +
"<if test='startTime != null and startTime != \"\"'>" + "<if test='startTime != null and startTime != \"\"'>" +
"AND r.claim_time >= #{startTime}" + "AND r.claim_time >= #{startTime} " +
"</if>" + "</if>" +
"<if test='endTime != null and endTime != \"\"'>" + "<if test='endTime != null and endTime != \"\"'>" +
"AND r.claim_time &lt;= #{endTime}" + "AND r.claim_time &lt;= #{endTime} " +
"</if>" + "</if>" +
"<if test='scenicId != null and scenicId != \"\"'>" + "<if test='scenicId != null and scenicId != \"\"'>" +
"AND r.scenic_id = #{scenicId}" + "AND r.scenic_id = #{scenicId} " +
"</if>" + "</if>" +
"</where>" + "</where>" +
"ORDER BY r.create_time DESC" + "ORDER BY r.create_time DESC" +

View File

@@ -36,26 +36,37 @@ public class AutoCouponServiceImpl implements IAutoCouponService {
return false; return false;
} }
// 2. 查找该景区、该商品类型的首次打印优惠券配置 // 2. 查找该景区、该商品类型的所有首次打印优惠券配置
Long couponId = findFirstCouponId(scenicId, productType); List<Long> couponIds = findFirstCouponIds(scenicId, productType);
if (couponId == null) { if (couponIds == null || couponIds.isEmpty()) {
log.debug("景区未配置首次打印优惠券: scenicId={}, productType={}", scenicId, productType); log.debug("景区未配置首次打印优惠券: scenicId={}, productType={}", scenicId, productType);
return false; return false;
} }
// 3. 检查用户是否已领取过该券(领券即消耗首次资格) log.info("找到{}张首次优惠券待发放: scenicId={}, productType={}, couponIds={}",
couponIds.size(), scenicId, productType, couponIds);
// 3. 遍历所有优惠券,逐一检查并发放
int successCount = 0;
int skipCount = 0;
int failCount = 0;
for (Long couponId : couponIds) {
try {
// 检查用户是否已领取过该券(领券即消耗首次资格)
PriceCouponClaimRecord existingRecord = couponClaimRecordMapper.selectUserCouponRecord( PriceCouponClaimRecord existingRecord = couponClaimRecordMapper.selectUserCouponRecord(
memberId, memberId,
couponId couponId
); );
if (existingRecord != null) { if (existingRecord != null) {
log.debug("用户已领取过首次优惠券,不重复发券: memberId={}, couponId={}, claimTime={}", log.debug("用户已领取过优惠券,跳过: memberId={}, couponId={}, claimTime={}",
memberId, couponId, existingRecord.getClaimTime()); memberId, couponId, existingRecord.getClaimTime());
return false; skipCount++;
continue;
} }
// 4. 自动发券 // 自动发券
CouponClaimRequest request = new CouponClaimRequest( CouponClaimRequest request = new CouponClaimRequest(
memberId, memberId,
couponId, couponId,
@@ -64,11 +75,23 @@ public class AutoCouponServiceImpl implements IAutoCouponService {
); );
couponService.claimCoupon(request); couponService.claimCoupon(request);
successCount++;
log.info("成功自动发放首次打印优惠券: memberId={}, faceId={}, scenicId={}, productType={}, couponId={}", log.info("成功自动发放首次优惠券: memberId={}, faceId={}, scenicId={}, productType={}, couponId={}",
memberId, faceId, scenicId, productType, couponId); memberId, faceId, scenicId, productType, couponId);
return true; } catch (Exception e) {
failCount++;
log.error("单张优惠券发放失败,继续处理其他券: memberId={}, couponId={}, error={}",
memberId, couponId, e.getMessage());
}
}
log.info("自动发券完成: memberId={}, 成功{}张, 跳过{}张, 失败{}张",
memberId, successCount, skipCount, failCount);
// 只要有一张成功就返回true
return successCount > 0;
} catch (Exception e) { } catch (Exception e) {
log.error("自动发券失败: memberId={}, faceId={}, scenicId={}, productType={}", log.error("自动发券失败: memberId={}, faceId={}, scenicId={}, productType={}",
@@ -78,14 +101,15 @@ public class AutoCouponServiceImpl implements IAutoCouponService {
} }
/** /**
* 查找指定景区、指定商品类型的首次打印优惠券ID * 查找指定景区、指定商品类型的所有首次打印优惠券ID
* 规则:优惠券名称包含 "首次" 且 适用商品类型包含目标类型 * 规则:优惠券名称包含 "首次" 且 适用商品类型包含目标类型
* *
* @param scenicId 景区ID * @param scenicId 景区ID
* @param productType 商品类型 * @param productType 商品类型
* @return 优惠券ID,未找到返回null * @return 优惠券ID列表,未找到返回空列表
*/ */
private Long findFirstCouponId(Long scenicId, ProductType productType) { private List<Long> findFirstCouponIds(Long scenicId, ProductType productType) {
List<Long> couponIds = new java.util.ArrayList<>();
try { try {
// 查询该景区的有效优惠券 // 查询该景区的有效优惠券
List<PriceCouponConfig> coupons = couponConfigMapper.selectValidCouponsByScenicId( List<PriceCouponConfig> coupons = couponConfigMapper.selectValidCouponsByScenicId(
@@ -100,17 +124,22 @@ public class AutoCouponServiceImpl implements IAutoCouponService {
String applicableProducts = coupon.getApplicableProducts(); String applicableProducts = coupon.getApplicableProducts();
if (applicableProducts != null && if (applicableProducts != null &&
applicableProducts.contains(productType.getCode())) { applicableProducts.contains(productType.getCode())) {
return coupon.getId(); couponIds.add(coupon.getId());
log.debug("找到匹配的首次优惠券: couponId={}, couponName={}, scenicId={}, productType={}",
coupon.getId(), coupon.getCouponName(), scenicId, productType);
} }
} }
} }
if (couponIds.isEmpty()) {
log.debug("未找到匹配的首次打印优惠券: scenicId={}, productType={}", scenicId, productType); log.debug("未找到匹配的首次打印优惠券: scenicId={}, productType={}", scenicId, productType);
return null; }
return couponIds;
} catch (Exception e) { } catch (Exception e) {
log.error("查找首次打印优惠券失败: scenicId={}, productType={}", scenicId, productType, e); log.error("查找首次打印优惠券失败: scenicId={}, productType={}", scenicId, productType, e);
return null; return couponIds;
} }
} }
} }

View File

@@ -0,0 +1,104 @@
package com.ycwl.basic.puzzle.fill.datasource;
import com.fasterxml.jackson.databind.JsonNode;
import com.ycwl.basic.mapper.SourceMapper;
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
import com.ycwl.basic.puzzle.fill.enums.DataSourceType;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
/**
* 设备缩略图数据源策略
* 根据deviceIndex指定第N个设备的缩略图
*/
@Slf4j
@Component
public class DeviceThumbImageDataSourceStrategy implements DataSourceStrategy {
@Autowired
private SourceMapper sourceMapper;
@Override
public String resolve(JsonNode sourceFilter, String sortStrategy, DataSourceContext context) {
try {
// 默认type=2(图片)
Integer type = 2;
if (sourceFilter != null && sourceFilter.has("type")) {
type = sourceFilter.get("type").asInt();
}
// 获取deviceIndex
Integer deviceIndex = 0;
if (sourceFilter != null && sourceFilter.has("deviceIndex")) {
deviceIndex = sourceFilter.get("deviceIndex").asInt();
}
// 使用默认策略
if (sortStrategy == null || sortStrategy.isEmpty()) {
sortStrategy = "LATEST";
}
// 1. 检查是否有过滤后的机位列表
Map<String, Object> extra = context.getExtra();
if (extra != null && extra.containsKey("filteredDeviceIds")) {
@SuppressWarnings("unchecked")
List<Long> filteredDeviceIds = (List<Long>) extra.get("filteredDeviceIds");
if (filteredDeviceIds != null && !filteredDeviceIds.isEmpty()) {
// 使用过滤后的机位列表
if (deviceIndex >= filteredDeviceIds.size()) {
log.warn("deviceIndex[{}]超出过滤后的机位列表范围, 最大索引={}",
deviceIndex, filteredDeviceIds.size() - 1);
return null;
}
Long targetDeviceId = filteredDeviceIds.get(deviceIndex);
log.debug("使用过滤后的机位列表, deviceIndex={}, targetDeviceId={}",
deviceIndex, targetDeviceId);
SourceEntity source = sourceMapper.getSourceByFaceAndDeviceId(
context.getFaceId(),
targetDeviceId,
type,
sortStrategy
);
if (source != null) {
String thumbUrl = source.getThumbUrl();
log.debug("解析DEVICE_THUMB_IMAGE成功(过滤模式), faceId={}, deviceId={}, type={}, thumbUrl={}",
context.getFaceId(), targetDeviceId, type, thumbUrl);
return thumbUrl;
}
return null;
}
}
// 2. 降级到原有逻辑(使用deviceIndex直接查询)
SourceEntity source = sourceMapper.getSourceByFaceAndDeviceIndex(
context.getFaceId(),
deviceIndex,
type,
sortStrategy
);
if (source != null) {
String thumbUrl = source.getThumbUrl();
log.debug("解析DEVICE_THUMB_IMAGE成功(索引模式), faceId={}, deviceIndex={}, type={}, thumbUrl={}",
context.getFaceId(), deviceIndex, type, thumbUrl);
return thumbUrl;
}
} catch (Exception e) {
log.error("解析DEVICE_THUMB_IMAGE异常, faceId={}", context.getFaceId(), e);
}
return null;
}
@Override
public String getSupportedType() {
return DataSourceType.DEVICE_THUMB_IMAGE.getCode();
}
}

View File

@@ -27,6 +27,7 @@ public enum DataSourceType {
* 设备图片(根据deviceIndex指定第N个设备的图片) * 设备图片(根据deviceIndex指定第N个设备的图片)
*/ */
DEVICE_IMAGE("DEVICE_IMAGE", "设备图片"), DEVICE_IMAGE("DEVICE_IMAGE", "设备图片"),
DEVICE_THUMB_IMAGE("DEVICE_THUMB_IMAGE", "设备缩略图片"),
/** /**
* 静态值(直接使用fallbackValue) * 静态值(直接使用fallbackValue)

View File

@@ -97,6 +97,17 @@ public class DeviceRepository {
return device; return device;
} }
/**
* 获取设备基本信息(直接返回DeviceV2DTO)
*
* @param deviceId 设备ID
* @return DeviceV2DTO实例
*/
public DeviceV2DTO getDeviceBasic(Long deviceId) {
log.debug("获取设备基本信息, deviceId: {}", deviceId);
return deviceIntegrationService.getDevice(deviceId);
}
public DeviceEntity getDeviceByDeviceNo(String deviceNo) { public DeviceEntity getDeviceByDeviceNo(String deviceNo) {
log.debug("根据设备编号获取设备信息, deviceNo: {}", deviceNo); log.debug("根据设备编号获取设备信息, deviceNo: {}", deviceNo);
DeviceV2DTO deviceDto = deviceIntegrationService.getDeviceByNo(deviceNo); DeviceV2DTO deviceDto = deviceIntegrationService.getDeviceByNo(deviceNo);

View File

@@ -23,7 +23,7 @@ public class OrderRepository {
public static final String ORDER_CACHE_KEY = "order:%s"; public static final String ORDER_CACHE_KEY = "order:%s";
public static final String ORDER_ITEMS_CACHE_KEY = "order:%s:items"; public static final String ORDER_ITEMS_CACHE_KEY = "order:%s:items";
public static final String ORDER_ITEM_CACHE_KEY = "order:item:%s"; public static final String ORDER_ITEM_CACHE_KEY = "order:item:%s";
public static final String ORDER_USER_TYPE_BUY_ITEM_CACHE_KEY = "order:user:%s:type:%s:id:%s"; public static final String ORDER_USER_FACE_TYPE_BUY_ITEM_CACHE_KEY = "order:user:%s:face:%s:type:%s:id:%s";
public OrderEntity getOrder(Long orderId) { public OrderEntity getOrder(Long orderId) {
if (redisTemplate.hasKey(String.format(ORDER_CACHE_KEY, orderId))) { if (redisTemplate.hasKey(String.format(ORDER_CACHE_KEY, orderId))) {
@@ -62,40 +62,12 @@ public class OrderRepository {
return orderItemEntity; return orderItemEntity;
} }
public boolean checkUserBuyItem(Long userId, int goodsType, Long goodsId) {
synchronized (this) {
if (redisTemplate.hasKey(String.format(ORDER_USER_TYPE_BUY_ITEM_CACHE_KEY, userId, goodsType, goodsId))) {
return "1".equals(redisTemplate.opsForValue().get(String.format(ORDER_USER_TYPE_BUY_ITEM_CACHE_KEY, userId, goodsType, goodsId)));
}
OrderEntity orderEntity = orderMapper.getUserBuyItem(userId, goodsType, goodsId);
if (orderEntity == null) {
redisTemplate.opsForValue().set(String.format(ORDER_USER_TYPE_BUY_ITEM_CACHE_KEY, userId, goodsType, goodsId), "0", 60, TimeUnit.SECONDS);
return false;
}
if (Integer.valueOf(1).equals(orderEntity.getStatus())) {
redisTemplate.opsForValue().set(String.format(ORDER_USER_TYPE_BUY_ITEM_CACHE_KEY, userId, goodsType, goodsId), "1");
return true;
} else {
redisTemplate.opsForValue().set(String.format(ORDER_USER_TYPE_BUY_ITEM_CACHE_KEY, userId, goodsType, goodsId), "0", 60, TimeUnit.SECONDS);
return false;
}
}
}
public OrderEntity getUserBuyItem(Long userId, int goodsType, Long goodsId) {
return orderMapper.getUserBuyItem(userId, goodsType, goodsId);
}
public void clearUserBuyItemCache(Long userId, int goodsType, Long goodsId) {
redisTemplate.delete(String.format(ORDER_USER_TYPE_BUY_ITEM_CACHE_KEY, userId, goodsType, goodsId));
}
public void clearOrderCache(Long orderId) { public void clearOrderCache(Long orderId) {
OrderEntity order = getOrder(orderId); OrderEntity order = getOrder(orderId);
redisTemplate.delete(String.format(ORDER_CACHE_KEY, orderId)); redisTemplate.delete(String.format(ORDER_CACHE_KEY, orderId));
getOrderItems(orderId).forEach(orderItem -> { getOrderItems(orderId).forEach(orderItem -> {
redisTemplate.delete(String.format(ORDER_ITEM_CACHE_KEY, orderItem.getId())); redisTemplate.delete(String.format(ORDER_ITEM_CACHE_KEY, orderItem.getId()));
clearUserBuyItemCache(order.getMemberId(), orderItem.getGoodsType(), orderItem.getGoodsId()); clearUserBuyFaceItemCache(order.getMemberId(), order.getFaceId(), orderItem.getGoodsType(), orderItem.getGoodsId());
}); });
redisTemplate.delete(String.format(ORDER_ITEMS_CACHE_KEY, orderId)); redisTemplate.delete(String.format(ORDER_ITEMS_CACHE_KEY, orderId));
} }
@@ -105,4 +77,37 @@ public class OrderRepository {
orderMapper.updateOrder(updateEntity); orderMapper.updateOrder(updateEntity);
clearOrderCache(orderId); clearOrderCache(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) {
synchronized (this) {
String cacheKey = String.format(ORDER_USER_FACE_TYPE_BUY_ITEM_CACHE_KEY, userId, faceId, goodsType, goodsId);
if (redisTemplate.hasKey(cacheKey)) {
return "1".equals(redisTemplate.opsForValue().get(cacheKey));
}
OrderEntity orderEntity = orderMapper.getUserBuyFaceItem(userId, faceId, goodsType, goodsId);
if (orderEntity == null) {
redisTemplate.opsForValue().set(cacheKey, "0", 60, TimeUnit.SECONDS);
return false;
}
if (Integer.valueOf(1).equals(orderEntity.getStatus())) {
redisTemplate.opsForValue().set(cacheKey, "1");
return true;
} else {
redisTemplate.opsForValue().set(cacheKey, "0", 60, TimeUnit.SECONDS);
return false;
}
}
}
public void clearUserBuyFaceItemCache(Long userId, Long faceId, int goodsType, Long goodsId) {
redisTemplate.delete(String.format(ORDER_USER_FACE_TYPE_BUY_ITEM_CACHE_KEY, userId, faceId, goodsType, goodsId));
}
} }

View File

@@ -1,13 +1,25 @@
package com.ycwl.basic.repository; package com.ycwl.basic.repository;
import com.ycwl.basic.image.enhancer.entity.BceEnhancerConfig;
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
import com.ycwl.basic.image.pipeline.enums.ImageSource;
import com.ycwl.basic.image.pipeline.enums.ImageType;
import com.ycwl.basic.image.pipeline.enums.PipelineScene;
import com.ycwl.basic.image.pipeline.stages.*;
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
import com.ycwl.basic.mapper.SourceMapper; import com.ycwl.basic.mapper.SourceMapper;
import com.ycwl.basic.model.pc.face.entity.FaceEntity; import com.ycwl.basic.model.pc.face.entity.FaceEntity;
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity; import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
import com.ycwl.basic.model.pc.source.entity.SourceEntity; import com.ycwl.basic.model.pc.source.entity.SourceEntity;
import com.ycwl.basic.model.pc.device.entity.DeviceEntity; import com.ycwl.basic.model.pc.device.entity.DeviceEntity;
import com.ycwl.basic.pipeline.core.Pipeline;
import com.ycwl.basic.pipeline.core.PipelineBuilder;
import com.ycwl.basic.pricing.dto.VoucherInfo; import com.ycwl.basic.pricing.dto.VoucherInfo;
import com.ycwl.basic.pricing.enums.VoucherDiscountType; import com.ycwl.basic.pricing.enums.VoucherDiscountType;
import com.ycwl.basic.pricing.service.IVoucherService; import com.ycwl.basic.pricing.service.IVoucherService;
import com.ycwl.basic.storage.StorageFactory;
import com.ycwl.basic.storage.adapters.IStorageAdapter;
import com.ycwl.basic.storage.exceptions.StorageUnsupportedException;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
@@ -16,18 +28,29 @@ import org.springframework.stereotype.Component;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Slf4j @Slf4j
@Component @Component
public class SourceRepository { public class SourceRepository {
private static final ExecutorService IMAGE_PROCESS_EXECUTOR = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors(),
runnable -> {
Thread thread = new Thread(runnable);
thread.setName("ai-cam-image-processor-" + thread.getId());
thread.setDaemon(true);
return thread;
}
);
@Autowired @Autowired
private SourceMapper sourceMapper; private SourceMapper sourceMapper;
@Autowired @Autowired
private RedisTemplate<String, String> redisTemplate; private RedisTemplate<String, String> redisTemplate;
@Autowired @Autowired
private IVoucherService iVoucherService;
@Autowired
private FaceRepository faceRepository; private FaceRepository faceRepository;
@Autowired @Autowired
private TemplateRepository templateRepository; private TemplateRepository templateRepository;
@@ -35,12 +58,21 @@ public class SourceRepository {
private DeviceRepository deviceRepository; private DeviceRepository deviceRepository;
@Autowired @Autowired
private MemberRelationRepository memberRelationRepository; private MemberRelationRepository memberRelationRepository;
@Autowired
private ScenicRepository scenicRepository;
public void addSource(SourceEntity source) { public void addSource(SourceEntity source) {
sourceMapper.add(source); sourceMapper.add(source);
} }
public void setUserIsBuyItem(Long memberId, int type, Long faceId, Long orderId) { public void setUserIsBuyItem(Long memberId, int type, Long faceId, Long orderId) {
// 如果是AI相机照片类型(type=13),需要进行图像超分和增强处理
boolean needsImageProcessing = (type == 13 || type == 3);
if (type == 13) {
type = 3; // compact
}
MemberSourceEntity memberSource = new MemberSourceEntity(); MemberSourceEntity memberSource = new MemberSourceEntity();
memberSource.setMemberId(memberId); memberSource.setMemberId(memberId);
memberSource.setFaceId(faceId); memberSource.setFaceId(faceId);
@@ -49,6 +81,143 @@ public class SourceRepository {
memberSource.setIsBuy(1); memberSource.setIsBuy(1);
sourceMapper.updateRelation(memberSource); sourceMapper.updateRelation(memberSource);
memberRelationRepository.clearSCacheByFace(faceId); memberRelationRepository.clearSCacheByFace(faceId);
// 如果需要图像处理,对该faceId下的所有type=3的照片进行处理
if (needsImageProcessing) {
processAiCamImages(faceId);
}
redisTemplate.delete("order_content_not_downloadable_" + orderId);
}
/**
* 处理AI相机照片 - 对照片进行超分辨率和增强处理
*
* @param faceId 人脸ID
*/
private void processAiCamImages(Long faceId) {
try {
// 1. 获取该faceId下所有type=3的照片
List<SourceEntity> aiCamImages = sourceMapper.listAiCamImageByFaceRelation(faceId);
if (aiCamImages == null || aiCamImages.isEmpty()) {
log.info("没有找到需要处理的AI相机照片, faceId: {}", faceId);
return;
}
log.info("开始处理AI相机照片, faceId: {}, 照片数量: {}", faceId, aiCamImages.size());
// 2. 构建图像处理配置
BceEnhancerConfig config = buildEnhancerConfig();
// 3. 并发处理所有照片
List<CompletableFuture<Void>> futures = aiCamImages.stream()
.map(source -> CompletableFuture.runAsync(() -> {
try {
processSingleAiCamImage(source, config);
} catch (Exception e) {
log.error("处理AI相机照片失败, sourceId: {}, error: {}", source.getId(), e.getMessage(), e);
// 继续处理下一张照片,不中断整个流程
}
}, IMAGE_PROCESS_EXECUTOR))
.collect(Collectors.toList());
// 4. 等待所有任务完成
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
log.info("AI相机照片处理完成, faceId: {}", faceId);
} catch (Exception e) {
log.error("处理AI相机照片整体流程失败, faceId: {}, error: {}", faceId, e.getMessage(), e);
// 即使处理失败也不抛出异常,不影响订单购买流程
}
}
/**
* 处理单张AI相机照片
*
* @param source 原始照片
* @param config 增强配置
*/
private void processSingleAiCamImage(SourceEntity source, BceEnhancerConfig config) {
// 1. 创建处理上下文
PhotoProcessContext context = PhotoProcessContext.builder()
.processId("aicam-" + source.getId())
.originalUrl(source.getUrl())
.scenicId(source.getScenicId())
.imageType(ImageType.NORMAL_PHOTO)
.source(ImageSource.IPC)
.scene(PipelineScene.AI_CAM_ENHANCE)
.build();
context.enableStage("image_sr");
context.enableStage("image_enhance");
ScenicConfigManager configManager = scenicRepository.getScenicConfigManager(source.getScenicId());
IStorageAdapter adapter;
try {
adapter = StorageFactory.get(configManager.getString("store_type"));
adapter.loadConfig(configManager.getObject("store_config_json", Map.class));
} catch (StorageUnsupportedException ignored) {
adapter = StorageFactory.use("assets-ext");
}
context.setStorageAdapter(adapter);
// 2. 设置结果URL回调 - 更新source记录
context.setResultUrlCallback(newUrl -> {
SourceEntity updateEntity = new SourceEntity();
updateEntity.setId(source.getId());
updateEntity.setUrl(newUrl);
sourceMapper.update(updateEntity);
log.info("已更新AI相机照片URL, sourceId: {}, oldUrl: {}, newUrl: {}",
source.getId(), source.getUrl(), newUrl);
});
// 3. 构建处理管线: 下载 -> 超分 -> 增强 -> 上传 -> 清理
Pipeline<PhotoProcessContext> pipeline = new PipelineBuilder<PhotoProcessContext>("AiCamEnhancePipeline")
.addStage(new DownloadStage()) // 下载原图
// .addStage(new ImageSRStage(config)) // 图像超分辨率
.addStage(new ImageEnhanceStage(config)) // 图像增强
.addStage(new UploadStage()) // 上传处理后的图片
.addStage(new CleanupStage()) // 清理临时文件
.build();
// 4. 执行管线
boolean success = pipeline.execute(context);
if (!success) {
log.warn("AI相机照片处理管线执行失败, sourceId: {}", source.getId());
}
}
/**
* 构建图像增强配置
*
* @return 增强配置
*/
private BceEnhancerConfig buildEnhancerConfig() {
BceEnhancerConfig config = new BceEnhancerConfig();
// 尝试从环境变量读取
String appId = System.getenv("BCE_IMAGE_APP_ID");
String apiKey = System.getenv("BCE_IMAGE_API_KEY");
String secretKey = System.getenv("BCE_IMAGE_SECRET_KEY");
// 如果环境变量没有配置,使用默认值(与PrinterServiceImpl保持一致)
if (appId == null || appId.isBlank()) {
appId = "119554288";
}
if (apiKey == null || apiKey.isBlank()) {
apiKey = "OX6QoijgKio3eVtA0PiUVf7f";
}
if (secretKey == null || secretKey.isBlank()) {
secretKey = "dYatXReVriPeiktTjUblhfubpcmYfuMk";
}
config.setAppId(appId);
config.setApiKey(apiKey);
config.setSecretKey(secretKey);
config.setQps(1.0f);
return config;
} }
public void setUserNotBuyItem(Long memberId, int type, Long faceId) { public void setUserNotBuyItem(Long memberId, int type, Long faceId) {

View File

@@ -1,9 +1,6 @@
package com.ycwl.basic.repository; package com.ycwl.basic.repository;
import com.ycwl.basic.biz.PriceBiz; import com.ycwl.basic.biz.PriceBiz;
import com.ycwl.basic.model.mobile.order.IsBuyBatchRespVO;
import com.ycwl.basic.pricing.dto.VoucherInfo;
import com.ycwl.basic.pricing.enums.VoucherDiscountType;
import com.ycwl.basic.pricing.service.IVoucherService; import com.ycwl.basic.pricing.service.IVoucherService;
import com.ycwl.basic.utils.JacksonUtil; import com.ycwl.basic.utils.JacksonUtil;
import com.ycwl.basic.mapper.VideoMapper; import com.ycwl.basic.mapper.VideoMapper;
@@ -116,4 +113,25 @@ public class VideoRepository {
return true; return true;
} }
public void setUserIsBuyTemplate(Long memberId, Long templateId, Long orderId, Long faceId) {
List<MemberVideoEntity> videoEntities = memberRelationRepository.listRelationByFace(faceId);
for (MemberVideoEntity videoEntity : videoEntities) {
if (videoEntity.getTemplateId() != null && videoEntity.getTemplateId().equals(templateId)) {
MemberVideoEntity memberVideo = new MemberVideoEntity();
memberVideo.setVideoId(videoEntity.getVideoId());
memberVideo.setMemberId(memberId);
memberVideo.setIsBuy(1);
memberVideo.setOrderId(orderId);
videoMapper.updateRelation(memberVideo);
// 清理视频关系缓存
MemberVideoEntity existingVideo = videoMapper.queryUserVideo(memberId, videoEntity.getVideoId());
if (existingVideo != null && existingVideo.getFaceId() != null) {
memberRelationRepository.clearVCacheByFace(existingVideo.getFaceId());
}
}
}
}
} }

View File

@@ -114,7 +114,7 @@ public class ZTSourceDataService {
entity.setDeviceId(message.getDeviceId()); entity.setDeviceId(message.getDeviceId());
entity.setUrl(message.getSourceUrl()); // 使用sourceUrl,不使用缩略图 entity.setUrl(message.getSourceUrl()); // 使用sourceUrl,不使用缩略图
entity.setThumbUrl(message.getThumbnailUrl()); // 设置缩略图URL entity.setThumbUrl(message.getThumbnailUrl()); // 设置缩略图URL
entity.setType(2); // 照片类型 entity.setType(message.getSourceType()); // 照片类型
// 人脸样本ID处理 // 人脸样本ID处理
entity.setFaceSampleId(message.getFaceSampleId()); entity.setFaceSampleId(message.getFaceSampleId());

View File

@@ -0,0 +1,35 @@
package com.ycwl.basic.service.mobile;
import com.ycwl.basic.model.mobile.face.FaceRecognizeResp;
import com.ycwl.basic.model.mobile.goods.GoodsDetailVO;
import java.util.List;
/**
* AI相机相关服务
*/
public interface AppAiCamService {
/**
* 根据faceId获取AI相机识别到的商品列表
* @param faceId 人脸ID
* @return 商品详情列表
*/
List<GoodsDetailVO> getAiCamGoodsByFaceId(Long faceId);
/**
* 批量添加会员与source的关联关系
* @param faceId 人脸ID
* @param sourceIds source ID列表
* @return 添加成功的数量
*/
int addMemberSourceRelations(Long faceId, List<Long> sourceIds);
/**
* 使用人脸样本创建或获取Face记录
* @param userId 用户ID
* @param faceSampleId 人脸样本ID
* @return 人脸识别响应
*/
FaceRecognizeResp useSample(Long userId, Long faceSampleId);
}

View File

@@ -0,0 +1,36 @@
package com.ycwl.basic.service.mobile;
import com.ycwl.basic.model.mobile.chat.*;
import java.util.List;
public interface FaceChatService {
/**
* 获取或创建人脸会话,一脸一会话。
*/
ChatConversationVO getOrCreateConversation(Long faceId, Long memberId);
/**
* 同步发送消息并保存助手回复。
*/
ChatSendMessageResp sendMessage(Long conversationId, Long memberId, String content, String traceId);
/**
* 流式发送消息,支持实时分片回调,仍返回完整结果。
*/
ChatSendMessageStreamResp sendMessageStream(Long conversationId,
Long memberId,
String content,
String traceId,
java.util.function.Consumer<String> chunkConsumer);
/**
* 拉取历史消息,cursor 为最后一条 seq,limit 为条数。
*/
ChatMessagePageResp listMessages(Long conversationId, Integer cursor, Integer limit, Long memberId);
/**
* 关闭会话。
*/
void closeConversation(Long conversationId, Long memberId);
}

View File

@@ -0,0 +1,383 @@
package com.ycwl.basic.service.mobile.impl;
import com.ycwl.basic.exception.BaseException;
import com.ycwl.basic.facebody.FaceBodyFactory;
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
import com.ycwl.basic.facebody.entity.SearchFaceResp;
import com.ycwl.basic.facebody.entity.SearchFaceResultItem;
import com.ycwl.basic.integration.common.manager.DeviceConfigManager;
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
import com.ycwl.basic.mapper.FaceDetectLogAiCamMapper;
import com.ycwl.basic.mapper.FaceMapper;
import com.ycwl.basic.mapper.FaceSampleMapper;
import com.ycwl.basic.mapper.SourceMapper;
import com.ycwl.basic.model.mobile.face.FaceRecognizeResp;
import com.ycwl.basic.model.mobile.goods.GoodsDetailVO;
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
import com.ycwl.basic.model.pc.face.resp.FaceRespVO;
import com.ycwl.basic.model.pc.faceDetectLog.entity.FaceDetectLogAiCamEntity;
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity;
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
import com.ycwl.basic.repository.DeviceRepository;
import com.ycwl.basic.repository.ScenicRepository;
import com.ycwl.basic.service.mobile.AppAiCamService;
import com.ycwl.basic.service.pc.FaceDetectLogAiCamService;
import com.ycwl.basic.service.pc.FaceService;
import com.ycwl.basic.service.pc.ScenicService;
import com.ycwl.basic.utils.JacksonUtil;
import com.ycwl.basic.utils.SnowFlakeUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.Collectors;
/**
* AI相机相关服务实现
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class AppAiCamServiceImpl implements AppAiCamService {
private final FaceDetectLogAiCamMapper faceDetectLogAiCamMapper;
private final SourceMapper sourceMapper;
private final FaceMapper faceMapper;
private final DeviceRepository deviceRepository;
private final FaceSampleMapper faceSampleMapper;
private static final float DEFAULT_SCORE_THRESHOLD = 0.8f;
private static final int DEFAULT_PHOTO_LIMIT = 10;
private static final int AI_CAM_SOURCE_TYPE = 3;
private final FaceService faceService;
private final FaceDetectLogAiCamService faceDetectLogAiCamService;
private final ScenicService scenicService;
@Override
public List<GoodsDetailVO> getAiCamGoodsByFaceId(Long faceId) {
// 1. 查询该faceId的所有识别记录
List<FaceDetectLogAiCamEntity> detectLogs = faceDetectLogAiCamMapper.listByFaceId(faceId);
if (detectLogs == null || detectLogs.isEmpty()) {
return Collections.emptyList();
}
// 2. 按设备分组并根据设备配置过滤faceSampleId
Map<Long, List<Long>> deviceFaceSampleMap = new LinkedHashMap<>();
// 按设备分组识别记录
Map<Long, List<FaceDetectLogAiCamEntity>> deviceLogsMap = detectLogs.stream()
.collect(Collectors.groupingBy(FaceDetectLogAiCamEntity::getDeviceId));
// 遍历每个设备的识别记录
for (Map.Entry<Long, List<FaceDetectLogAiCamEntity>> entry : deviceLogsMap.entrySet()) {
Long deviceId = entry.getKey();
List<FaceDetectLogAiCamEntity> deviceLogs = entry.getValue();
// 获取设备配置
DeviceConfigManager configManager = deviceRepository.getDeviceConfigManager(deviceId);
// 获取该设备的分数阈值(百分制,需要转换为0-1)
float scoreThreshold = DEFAULT_SCORE_THRESHOLD;
if (configManager != null) {
Float thresholdPercent = configManager.getFloat("ai_cam_face_score_threshold");
if (thresholdPercent != null) {
scoreThreshold = thresholdPercent / 100.0f;
log.debug("设备{}使用配置的分数阈值: {}% ({})", deviceId, thresholdPercent, scoreThreshold);
}
}
// 获取该设备的照片数量限制
int photoLimit = DEFAULT_PHOTO_LIMIT;
if (configManager != null) {
Integer limit = configManager.getInteger("ai_cam_photo_limit");
if (limit != null && limit > 0) {
photoLimit = limit;
log.debug("设备{}使用配置的照片限制: {}", deviceId, photoLimit);
}
}
// 获取该设备的时间范围限制(分钟)
Integer timeRangeMin = null;
if (configManager != null) {
Integer range = configManager.getInteger("ai_cam_time_range_min");
if (range != null && range > 0) {
timeRangeMin = range;
log.debug("设备{}使用配置的时间范围: {}分钟", deviceId, timeRangeMin);
}
}
// 收集该设备符合阈值的faceSampleId,同时记录分数和时间信息用于后续过滤
class DetectResult {
Long faceSampleId;
Float score;
Date detectTime;
DetectResult(Long faceSampleId, Float score, Date detectTime) {
this.faceSampleId = faceSampleId;
this.score = score;
this.detectTime = detectTime;
}
}
List<DetectResult> detectResults = new ArrayList<>();
for (FaceDetectLogAiCamEntity detectLog : deviceLogs) {
if (detectLog.getMatchRawResult() == null) {
continue;
}
try {
SearchFaceResp resp = JacksonUtil.parseObject(detectLog.getMatchRawResult(), SearchFaceResp.class);
if (resp != null && resp.getResult() != null) {
for (SearchFaceResultItem item : resp.getResult()) {
// 使用设备配置的分数阈值
if (item.getScore() != null && item.getScore() >= scoreThreshold && item.getExtData() != null) {
try {
Long faceSampleId = Long.parseLong(item.getExtData());
detectResults.add(new DetectResult(
faceSampleId,
item.getScore(),
detectLog.getCreateTime()
));
} catch (NumberFormatException e) {
log.warn("解析faceSampleId失败: extData={}", item.getExtData());
}
}
}
}
} catch (Exception e) {
log.error("解析matchRawResult失败: logId={}", detectLog.getId(), e);
}
}
// 应用照片数量限制(保留前N个)
if (detectResults.size() > photoLimit) {
log.debug("设备{}的照片数量{}超过限制{},截取前{}张",
deviceId, detectResults.size(), photoLimit, photoLimit);
detectResults = detectResults.subList(0, photoLimit);
}
// 应用时间范围限制
List<Long> deviceFaceSampleIds;
if (timeRangeMin != null && !detectResults.isEmpty()) {
// 找到分数最高的照片
DetectResult highestScoreResult = detectResults.stream()
.max(Comparator.comparing(r -> r.score))
.orElse(null);
if (highestScoreResult != null && highestScoreResult.detectTime != null) {
Date baseTime = highestScoreResult.detectTime;
long halfRangeMillis = (long) timeRangeMin * 60 * 1000 / 2;
Date startTime = new Date(baseTime.getTime() - halfRangeMillis);
Date endTime = new Date(baseTime.getTime() + halfRangeMillis);
// 过滤出时间范围内的照片
List<DetectResult> filteredResults = detectResults.stream()
.filter(r -> r.detectTime != null
&& !r.detectTime.before(startTime)
&& !r.detectTime.after(endTime))
.collect(Collectors.toList());
log.debug("设备{}应用时间范围{}分钟过滤: {}张 -> {}张 (基准时间: {})",
deviceId, timeRangeMin, detectResults.size(), filteredResults.size(), baseTime);
deviceFaceSampleIds = filteredResults.stream()
.map(r -> r.faceSampleId)
.collect(Collectors.toList());
} else {
// 没有时间信息,不过滤
deviceFaceSampleIds = detectResults.stream()
.map(r -> r.faceSampleId)
.collect(Collectors.toList());
}
} else {
// 不限制时间范围
deviceFaceSampleIds = detectResults.stream()
.map(r -> r.faceSampleId)
.collect(Collectors.toList());
}
if (!deviceFaceSampleIds.isEmpty()) {
deviceFaceSampleMap.put(deviceId, deviceFaceSampleIds);
}
}
// 3. 合并所有设备的faceSampleId(去重)
Set<Long> faceSampleIds = new HashSet<>();
for (List<Long> ids : deviceFaceSampleMap.values()) {
faceSampleIds.addAll(ids);
}
if (faceSampleIds.isEmpty()) {
log.debug("没有符合条件的faceSampleId, faceId={}", faceId);
return Collections.emptyList();
}
log.info("人脸{}在{}个设备上识别到{}个不重复的faceSampleId",
faceId, deviceFaceSampleMap.size(), faceSampleIds.size());
// 4. 根据faceSampleId列表查询type=3的source记录
List<SourceEntity> sources = sourceMapper.listByFaceSampleIdsAndType(
new ArrayList<>(faceSampleIds), AI_CAM_SOURCE_TYPE
);
if (sources == null || sources.isEmpty()) {
log.debug("未找到type=3的source记录, faceId={}", faceId);
return Collections.emptyList();
}
log.info("查询到{}条AI相机图像记录, faceId={}", sources.size(), faceId);
// 5. 查询Face信息以获取scenicId
FaceEntity face = faceMapper.get(faceId);
if (face == null) {
log.warn("Face不存在: faceId={}", faceId);
return Collections.emptyList();
}
// 6. 转换为GoodsDetailVO
return sources.stream().map(source -> {
GoodsDetailVO vo = new GoodsDetailVO();
vo.setFaceId(faceId);
vo.setScenicId(face.getScenicId());
vo.setGoodsType(2); // 2表示原素材
vo.setGoodsId(source.getId());
vo.setUrl(source.getUrl());
vo.setVideoUrl(source.getVideoUrl());
vo.setCreateTime(source.getCreateTime());
vo.setIsBuy(source.getIsBuy() != null ? source.getIsBuy() : 0);
return vo;
}).collect(Collectors.toList());
}
@Override
public int addMemberSourceRelations(Long faceId, List<Long> sourceIds) {
if (sourceIds == null || sourceIds.isEmpty()) {
return 0;
}
// 查询Face信息
FaceEntity face = faceMapper.get(faceId);
if (face == null) {
throw new IllegalArgumentException("Face不存在: faceId=" + faceId);
}
if (face.getMemberId() == null) {
throw new IllegalArgumentException("Face未关联会员: faceId=" + faceId);
}
// 删除该faceId对应的旧的type=3关系
int deleted = sourceMapper.deleteRelationsByFaceIdAndType(faceId, AI_CAM_SOURCE_TYPE);
log.info("删除faceId={}的旧AI相机关联记录: {}条", faceId, deleted);
// 构建MemberSourceEntity列表
List<MemberSourceEntity> relations = sourceIds.stream().map(sourceId -> {
MemberSourceEntity entity = new MemberSourceEntity();
entity.setMemberId(face.getMemberId());
entity.setScenicId(face.getScenicId());
entity.setFaceId(faceId);
entity.setSourceId(sourceId);
entity.setType(AI_CAM_SOURCE_TYPE);
entity.setIsBuy(0);
entity.setIsFree(0);
return entity;
}).collect(Collectors.toList());
// 批量插入
int inserted = sourceMapper.addRelations(relations);
log.info("为faceId={}添加新AI相机关联记录: {}条", faceId, inserted);
return inserted;
}
@Override
public FaceRecognizeResp useSample(Long userId, Long faceSampleId) {
// 1. 查询 faceSample 获取其 URL
FaceSampleEntity faceSample = faceSampleMapper.getEntity(faceSampleId);
if (faceSample == null) {
throw new BaseException("人脸样本不存在");
}
String faceUrl = faceSample.getFaceUrl();
if (StringUtils.isBlank(faceUrl)) {
throw new BaseException("人脸样本URL为空");
}
Long scenicId = faceSample.getScenicId();
// 2. 检查face数据库中有没有同用户、同URL的face记录
FaceEntity existingFace = null;
Long faceId = null;
// 查询该用户在该景区的所有人脸记录
List<FaceRespVO> userFaces = faceMapper.listByScenicAndUserId(scenicId, userId);
// 查找是否存在相同URL的记录
for (FaceRespVO faceResp : userFaces) {
if (faceUrl.equals(faceResp.getFaceUrl())) {
existingFace = faceMapper.get(faceResp.getId());
faceId = existingFace.getId();
break;
}
}
// 3. 如果不存在,则新建一个face记录
if (existingFace == null) {
faceId = SnowFlakeUtil.getLongId();
FaceEntity newFace = new FaceEntity();
newFace.setId(faceId);
newFace.setCreateAt(new Date());
newFace.setScenicId(scenicId);
newFace.setMemberId(userId);
newFace.setFaceUrl(faceUrl);
faceMapper.add(newFace);
log.info("创建新的face记录, userId: {}, faceSampleId: {}, faceId: {}, faceUrl: {}",
userId, faceSampleId, faceId, faceUrl);
} else {
log.info("使用已存在的face记录, userId: {}, faceSampleId: {}, faceId: {}, faceUrl: {}",
userId, faceSampleId, faceId, faceUrl);
}
// 4. 查询对应的 type=3 的 source 记录并自动添加关联
SourceEntity sourceEntity = sourceMapper.getBySampleIdAndType(faceSampleId, AI_CAM_SOURCE_TYPE);
if (sourceEntity != null && existingFace == null) {
// 检查是否已存在该source的关联
List<GoodsDetailVO> existingGoods = getAiCamGoodsByFaceId(faceId);
boolean alreadyExists = existingGoods.stream()
.anyMatch(item -> Objects.equals(item.getGoodsId(), sourceEntity.getId()));
if (!alreadyExists) {
// 添加关联
MemberSourceEntity relation = new MemberSourceEntity();
relation.setMemberId(userId);
relation.setScenicId(scenicId);
relation.setFaceId(faceId);
relation.setSourceId(sourceEntity.getId());
relation.setType(AI_CAM_SOURCE_TYPE);
relation.setIsBuy(0);
relation.setIsFree(0);
sourceMapper.addRelations(Collections.singletonList(relation));
log.info("自动添加AI相机照片关联: userId={}, faceId={}, sourceId={}",
userId, faceId, sourceEntity.getId());
}
}
// 5. 返回结果
FaceRecognizeResp resp = new FaceRecognizeResp();
resp.setUrl(faceUrl);
resp.setFaceId(faceId);
resp.setScenicId(scenicId);
IFaceBodyAdapter faceBodyAdapter = scenicService.getScenicFaceBodyAdapter(scenicId);
try {
faceService.matchFaceId(faceId);
faceDetectLogAiCamService.searchAndLog(scenicId, faceId, faceUrl, faceBodyAdapter);
} catch (Exception e) {
// 人脸匹配失败不可以阻止正常流程
log.error("人脸匹配失败", e);
}
return resp;
}
}

View File

@@ -0,0 +1,234 @@
package com.ycwl.basic.service.mobile.impl;
import com.ycwl.basic.exception.BaseException;
import com.ycwl.basic.integration.glm.GlmClient;
import com.ycwl.basic.mapper.FaceChatConversationMapper;
import com.ycwl.basic.mapper.FaceChatMessageMapper;
import com.ycwl.basic.model.mobile.chat.*;
import com.ycwl.basic.model.mobile.chat.entity.FaceChatConversationEntity;
import com.ycwl.basic.model.mobile.chat.entity.FaceChatMessageEntity;
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
import com.ycwl.basic.repository.FaceRepository;
import com.ycwl.basic.service.mobile.FaceChatService;
import com.ycwl.basic.utils.SnowFlakeUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import ai.z.openapi.service.model.ChatMessage;
import ai.z.openapi.service.model.ChatMessageRole;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;
@Slf4j
@Service
@RequiredArgsConstructor
public class FaceChatServiceImpl implements FaceChatService {
private static final String STATUS_ACTIVE = "active";
private static final String STATUS_CLOSED = "closed";
private static final String ROLE_USER = "user";
private static final String ROLE_ASSISTANT = "assistant";
private static final String DEFAULT_MODEL = "glm-4.5-airx";
private static final int HISTORY_LIMIT = 50;
private final FaceChatConversationMapper conversationMapper;
private final FaceChatMessageMapper messageMapper;
private final FaceRepository faceRepository;
private final GlmClient glmClient;
@Override
public ChatConversationVO getOrCreateConversation(Long faceId, Long memberId) {
FaceChatConversationEntity exist = conversationMapper.findByFaceId(faceId);
if (exist != null) {
assertOwner(exist, memberId);
return toConversationVO(exist);
}
// DEBUG阶段,暂时不检查
// FaceEntity face = faceRepository.getFace(faceId);
// if (face == null) {
// throw new BaseException("人脸不存在");
// }
// if (!Objects.equals(face.getMemberId(), memberId)) {
// throw new BaseException("无权访问该人脸");
// }
FaceChatConversationEntity entity = new FaceChatConversationEntity();
entity.setId(SnowFlakeUtil.getLongId());
entity.setFaceId(faceId);
entity.setMemberId(memberId);
entity.setStatus(STATUS_ACTIVE);
entity.setModel(DEFAULT_MODEL);
conversationMapper.insert(entity);
return toConversationVO(entity);
}
@Override
@Transactional(rollbackFor = Exception.class)
public ChatSendMessageResp sendMessage(Long conversationId, Long memberId, String content, String traceId) {
ChatSendMessageStreamResp result = doSend(conversationId, memberId, content, traceId, null);
ChatSendMessageResp resp = new ChatSendMessageResp();
resp.setUserMessage(result.getUserMessage());
resp.setAssistantMessage(result.getAssistantMessage());
resp.setTraceId(result.getTraceId());
return resp;
}
@Override
@Transactional(rollbackFor = Exception.class)
public ChatSendMessageStreamResp sendMessageStream(Long conversationId, Long memberId, String content, String traceId,
java.util.function.Consumer<String> chunkConsumer) {
return doSend(conversationId, memberId, content, traceId, chunkConsumer);
}
@Override
public ChatMessagePageResp listMessages(Long conversationId, Integer cursor, Integer limit, Long memberId) {
FaceChatConversationEntity conv = conversationMapper.getById(conversationId);
if (conv == null) {
throw new BaseException("会话不存在");
}
assertOwner(conv, memberId);
int pageSize = limit == null ? 50 : Math.max(1, Math.min(limit, 100));
List<FaceChatMessageEntity> list = messageMapper.listByConversation(conversationId, cursor, pageSize);
List<ChatMessageVO> vos = list.stream().map(this::toMessageVO).collect(Collectors.toList());
ChatMessagePageResp resp = new ChatMessagePageResp();
resp.setMessages(vos);
if (!list.isEmpty()) {
resp.setNextCursor(list.getLast().getSeq());
} else {
resp.setNextCursor(cursor == null ? 0 : cursor);
}
return resp;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void closeConversation(Long conversationId, Long memberId) {
FaceChatConversationEntity conv = conversationMapper.getById(conversationId);
if (conv == null) {
throw new BaseException("会话不存在");
}
assertOwner(conv, memberId);
if (STATUS_CLOSED.equals(conv.getStatus())) {
return;
}
conversationMapper.updateStatus(conversationId, STATUS_CLOSED);
}
private ChatSendMessageStreamResp doSend(Long conversationId, Long memberId, String content, String traceId,
java.util.function.Consumer<String> liveConsumer) {
if (StringUtils.isBlank(content)) {
throw new BaseException("消息内容不能为空");
}
FaceChatConversationEntity conv = conversationMapper.getById(conversationId);
if (conv == null) {
throw new BaseException("会话不存在");
}
assertOwner(conv, memberId);
if (STATUS_CLOSED.equals(conv.getStatus())) {
throw new BaseException("会话已关闭,请重新创建");
}
String resolvedTraceId = StringUtils.isBlank(traceId) ? UUID.randomUUID().toString() : traceId;
Integer maxSeq = messageMapper.maxSeqForUpdate(conversationId);
int baseSeq = maxSeq == null ? 0 : maxSeq;
int userSeq = baseSeq + 1;
FaceChatMessageEntity userMsg = buildMessage(conv, userSeq, ROLE_USER, content, resolvedTraceId, null);
messageMapper.insert(userMsg);
long start = System.currentTimeMillis();
List<FaceChatMessageEntity> recentDesc = messageMapper.listRecentByConversation(conversationId, HISTORY_LIMIT);
Collections.reverse(recentDesc); // 按时间升序
List<ChatMessage> chatMessages = recentDesc.stream()
.map(this::toChatMessage)
.collect(Collectors.toList());
CopyOnWriteArrayList<String> chunks = new CopyOnWriteArrayList<>();
java.util.function.Consumer<String> chunkConsumer = piece -> {
if (StringUtils.isNotBlank(piece)) {
chunks.add(piece);
if (liveConsumer != null) {
liveConsumer.accept(piece);
}
}
};
String assistantText = glmClient.streamReply(conv.getFaceId(), memberId, resolvedTraceId, chatMessages, chunkConsumer);
if (StringUtils.isBlank(assistantText)) {
assistantText = "GLM 暂未接入,稍后再试。";
chunkConsumer.accept(assistantText);
}
int latency = (int) (System.currentTimeMillis() - start);
FaceChatMessageEntity assistantMsg = buildMessage(conv, userSeq + 1, ROLE_ASSISTANT, assistantText, resolvedTraceId, latency);
messageMapper.insert(assistantMsg);
ChatSendMessageStreamResp resp = new ChatSendMessageStreamResp();
resp.setUserMessage(toMessageVO(userMsg));
resp.setAssistantMessage(toMessageVO(assistantMsg));
resp.setTraceId(resolvedTraceId);
resp.setChunks(chunks);
return resp;
}
private FaceChatMessageEntity buildMessage(FaceChatConversationEntity conv, int seq, String role, String content, String traceId, Integer latencyMs) {
FaceChatMessageEntity msg = new FaceChatMessageEntity();
msg.setId(SnowFlakeUtil.getLongId());
msg.setConversationId(conv.getId());
msg.setFaceId(conv.getFaceId());
msg.setSeq(seq);
msg.setRole(role);
msg.setContent(content);
msg.setTraceId(traceId);
msg.setLatencyMs(latencyMs);
msg.setCreatedAt(new Date());
return msg;
}
private void assertOwner(FaceChatConversationEntity conv, Long memberId) {
if (!Objects.equals(conv.getMemberId(), memberId)) {
throw new BaseException("无权访问该会话");
}
}
private ChatConversationVO toConversationVO(FaceChatConversationEntity entity) {
ChatConversationVO vo = new ChatConversationVO();
vo.setConversationId(entity.getId());
vo.setFaceId(entity.getFaceId());
vo.setStatus(entity.getStatus());
vo.setModel(entity.getModel());
return vo;
}
private ChatMessageVO toMessageVO(FaceChatMessageEntity entity) {
ChatMessageVO vo = new ChatMessageVO();
vo.setId(entity.getId());
vo.setSeq(entity.getSeq());
vo.setRole(entity.getRole());
vo.setContent(entity.getContent());
vo.setTraceId(entity.getTraceId());
vo.setCreatedAt(entity.getCreatedAt());
return vo;
}
private ChatMessage toChatMessage(FaceChatMessageEntity entity) {
String role = entity.getRole();
String mappedRole = ChatMessageRole.USER.value();
if (ROLE_ASSISTANT.equalsIgnoreCase(role)) {
mappedRole = ChatMessageRole.ASSISTANT.value();
} else if ("system".equalsIgnoreCase(role)) {
mappedRole = ChatMessageRole.SYSTEM.value();
}
return ChatMessage.builder()
.role(mappedRole)
.content(entity.getContent())
.build();
}
}

View File

@@ -137,11 +137,15 @@ public class GoodsServiceImpl implements GoodsService {
goodsDetailVO.setFaceId(sourceRespVO.getFaceId()); goodsDetailVO.setFaceId(sourceRespVO.getFaceId());
goodsDetailVO.setGoodsId(sourceRespVO.getId()); goodsDetailVO.setGoodsId(sourceRespVO.getId());
String shootingTime = DateUtil.format(sourceRespVO.getCreateTime(), "yyyy.MM.dd HH:mm:ss"); String shootingTime = DateUtil.format(sourceRespVO.getCreateTime(), "yyyy.MM.dd HH:mm:ss");
if (Integer.valueOf(3).equals(sourceType)) {
goodsDetailVO.setGoodsName("拍摄时间:" + shootingTime);
} else {
if (i < 10) { if (i < 10) {
goodsDetailVO.setGoodsName(goodsNamePrefix + "0" + i + " " + shootingTime); goodsDetailVO.setGoodsName(goodsNamePrefix + "0" + i + " " + shootingTime);
} else { } else {
goodsDetailVO.setGoodsName(goodsNamePrefix + i + " " + shootingTime); goodsDetailVO.setGoodsName(goodsNamePrefix + i + " " + shootingTime);
} }
}
goodsDetailVO.setScenicId(sourceRespVO.getScenicId()); goodsDetailVO.setScenicId(sourceRespVO.getScenicId());
try { try {
ScenicV2DTO scenic = scenicRepository.getScenicBasic(sourceRespVO.getScenicId()); ScenicV2DTO scenic = scenicRepository.getScenicBasic(sourceRespVO.getScenicId());
@@ -573,7 +577,7 @@ public class GoodsServiceImpl implements GoodsService {
if (query.getGoodsId() != null) { if (query.getGoodsId() != null) {
list = list.stream().filter(source -> source.getId().equals(query.getGoodsId())).toList(); list = list.stream().filter(source -> source.getId().equals(query.getGoodsId())).toList();
} }
if (!Integer.valueOf(2).equals(query.getSourceType())) { if (Integer.valueOf(1).equals(query.getSourceType())) {
return list.stream().map(source -> { return list.stream().map(source -> {
GoodsUrlVO goodsUrlVO = new GoodsUrlVO(); GoodsUrlVO goodsUrlVO = new GoodsUrlVO();
goodsUrlVO.setGoodsType(source.getType()); goodsUrlVO.setGoodsType(source.getType());
@@ -592,7 +596,7 @@ public class GoodsServiceImpl implements GoodsService {
} }
return true; return true;
}).count(); }).count();
IsBuyRespVO isBuy = orderBiz.isBuy(face.getScenicId(), face.getMemberId(), face.getId(), query.getSourceType(), face.getId()); IsBuyRespVO isBuy = orderBiz.isBuy(face.getScenicId(), face.getMemberId(), face.getId(), sourceType == 3 ? 13 : sourceType, face.getId());
if (count > 0) { if (count > 0) {
if (!isBuy.isBuy()) { if (!isBuy.isBuy()) {
return Collections.emptyList(); return Collections.emptyList();
@@ -829,6 +833,9 @@ public class GoodsServiceImpl implements GoodsService {
} else if (type == 2) { } else if (type == 2) {
goodsPageVO.setGoodsName("照片集"); goodsPageVO.setGoodsName("照片集");
goodsPageVO.setTemplateCoverUrl(scenicConfig.getString("photo_cover_url")); goodsPageVO.setTemplateCoverUrl(scenicConfig.getString("photo_cover_url"));
} else if (type == 3) {
goodsPageVO.setGoodsName("AI微单");
goodsPageVO.setTemplateCoverUrl(scenicConfig.getString("ai_camera_cover_url"));
} else { } else {
goodsPageVO.setGoodsName("未知商品"); goodsPageVO.setGoodsName("未知商品");
} }

View File

@@ -11,11 +11,10 @@ public interface FaceDetectLogAiCamService {
/** /**
* 搜索人脸库并保存日志 * 搜索人脸库并保存日志
* @param scenicId 景区ID * @param scenicId 景区ID
* @param deviceId 设备ID * @param faceId 人脸样本ID
* @param faceSampleId 人脸样本ID
* @param faceUrl 人脸URL * @param faceUrl 人脸URL
* @param adapter 人脸适配器 * @param adapter 人脸适配器
* @return 搜索结果 * @return 搜索结果
*/ */
SearchFaceResp searchAndLog(Long scenicId, Long deviceId, Long faceSampleId, String faceUrl, IFaceBodyAdapter adapter); void searchAndLog(Long scenicId, Long faceId, String faceUrl, IFaceBodyAdapter adapter);
} }

View File

@@ -2,8 +2,10 @@ package com.ycwl.basic.service.pc.impl;
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter; import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
import com.ycwl.basic.facebody.entity.SearchFaceResp; import com.ycwl.basic.facebody.entity.SearchFaceResp;
import com.ycwl.basic.integration.device.dto.device.DeviceV2DTO;
import com.ycwl.basic.mapper.FaceDetectLogAiCamMapper; import com.ycwl.basic.mapper.FaceDetectLogAiCamMapper;
import com.ycwl.basic.model.pc.faceDetectLog.entity.FaceDetectLogAiCamEntity; import com.ycwl.basic.model.pc.faceDetectLog.entity.FaceDetectLogAiCamEntity;
import com.ycwl.basic.repository.DeviceRepository;
import com.ycwl.basic.service.pc.FaceDetectLogAiCamService; import com.ycwl.basic.service.pc.FaceDetectLogAiCamService;
import com.ycwl.basic.utils.JacksonUtil; import com.ycwl.basic.utils.JacksonUtil;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@@ -11,6 +13,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.Date; import java.util.Date;
import java.util.List;
@Slf4j @Slf4j
@Service @Service
@@ -18,9 +21,20 @@ import java.util.Date;
public class FaceDetectLogAiCamServiceImpl implements FaceDetectLogAiCamService { public class FaceDetectLogAiCamServiceImpl implements FaceDetectLogAiCamService {
private final FaceDetectLogAiCamMapper faceDetectLogAiCamMapper; private final FaceDetectLogAiCamMapper faceDetectLogAiCamMapper;
private final DeviceRepository deviceRepository;
@Override @Override
public SearchFaceResp searchAndLog(Long scenicId, Long deviceId, Long faceSampleId, String faceUrl, IFaceBodyAdapter adapter) { public void searchAndLog(Long scenicId, Long faceId, String faceUrl, IFaceBodyAdapter adapter) {
List<DeviceV2DTO> devices = deviceRepository.getAllDeviceByScenicId(scenicId);
for (DeviceV2DTO device : devices) {
if (!device.getType().equals("AI_CAM")) {
continue;
}
searchDeviceAndLog(scenicId, device.getId(), faceId, faceUrl, adapter);
}
}
private SearchFaceResp searchDeviceAndLog(Long scenicId, Long deviceId, Long faceId, String faceUrl, IFaceBodyAdapter adapter) {
String dbName = "AiCam" + deviceId; String dbName = "AiCam" + deviceId;
SearchFaceResp resp = null; SearchFaceResp resp = null;
@@ -28,7 +42,7 @@ public class FaceDetectLogAiCamServiceImpl implements FaceDetectLogAiCamService
// 调用适配器搜索人脸 // 调用适配器搜索人脸
resp = adapter.searchFace(dbName, faceUrl); resp = adapter.searchFace(dbName, faceUrl);
} catch (Exception e) { } catch (Exception e) {
log.error("AI相机人脸搜索异常: scenicId={}, deviceId={}, faceSampleId={}", scenicId, deviceId, faceSampleId, e); log.error("AI相机人脸搜索异常: scenicId={}, deviceId={}, faceId={}", scenicId, deviceId, faceId, e);
// 发生异常时记录空结果或错误信息,视业务需求而定。这里暂不中断流程,继续记录日志 // 发生异常时记录空结果或错误信息,视业务需求而定。这里暂不中断流程,继续记录日志
} }
@@ -37,7 +51,7 @@ public class FaceDetectLogAiCamServiceImpl implements FaceDetectLogAiCamService
FaceDetectLogAiCamEntity logEntity = new FaceDetectLogAiCamEntity(); FaceDetectLogAiCamEntity logEntity = new FaceDetectLogAiCamEntity();
logEntity.setScenicId(scenicId); logEntity.setScenicId(scenicId);
logEntity.setDeviceId(deviceId); logEntity.setDeviceId(deviceId);
logEntity.setFaceId(faceSampleId); logEntity.setFaceId(faceId);
logEntity.setDbName(dbName); logEntity.setDbName(dbName);
logEntity.setFaceUrl(faceUrl); logEntity.setFaceUrl(faceUrl);
logEntity.setCreateTime(new Date()); logEntity.setCreateTime(new Date());
@@ -57,7 +71,7 @@ public class FaceDetectLogAiCamServiceImpl implements FaceDetectLogAiCamService
faceDetectLogAiCamMapper.add(logEntity); faceDetectLogAiCamMapper.add(logEntity);
} catch (Exception e) { } catch (Exception e) {
log.error("保存AI相机人脸识别日志失败: faceSampleId={}", faceSampleId, e); log.error("保存AI相机人脸识别日志失败: faceId={}", faceId, e);
} }
return resp; return resp;

View File

@@ -9,7 +9,6 @@ import com.ycwl.basic.constant.BaseContextHandler;
import com.ycwl.basic.exception.BaseException; import com.ycwl.basic.exception.BaseException;
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter; import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
import com.ycwl.basic.facebody.entity.SearchFaceResultItem; import com.ycwl.basic.facebody.entity.SearchFaceResultItem;
import com.ycwl.basic.integration.common.manager.DeviceConfigManager;
import com.ycwl.basic.mapper.FaceSampleMapper; import com.ycwl.basic.mapper.FaceSampleMapper;
import com.ycwl.basic.mapper.ProjectMapper; import com.ycwl.basic.mapper.ProjectMapper;
import com.ycwl.basic.mapper.SourceMapper; import com.ycwl.basic.mapper.SourceMapper;
@@ -62,6 +61,7 @@ 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.mobile.GoodsService; import com.ycwl.basic.service.mobile.GoodsService;
import com.ycwl.basic.service.pc.FaceDetectLogAiCamService;
import com.ycwl.basic.service.pc.FaceService; import com.ycwl.basic.service.pc.FaceService;
import com.ycwl.basic.service.pc.ScenicService; import com.ycwl.basic.service.pc.ScenicService;
import com.ycwl.basic.constant.SourceType; import com.ycwl.basic.constant.SourceType;
@@ -71,7 +71,6 @@ import com.ycwl.basic.service.pc.helper.SearchResultMerger;
import com.ycwl.basic.service.pc.helper.ScenicConfigFacade; import com.ycwl.basic.service.pc.helper.ScenicConfigFacade;
import com.ycwl.basic.service.pc.orchestrator.FaceMatchingOrchestrator; import com.ycwl.basic.service.pc.orchestrator.FaceMatchingOrchestrator;
import com.ycwl.basic.service.pc.processor.BuyStatusProcessor; import com.ycwl.basic.service.pc.processor.BuyStatusProcessor;
import com.ycwl.basic.service.pc.processor.FaceRecoveryStrategy;
import com.ycwl.basic.service.pc.processor.SourceRelationProcessor; import com.ycwl.basic.service.pc.processor.SourceRelationProcessor;
import com.ycwl.basic.service.pc.processor.VideoRecreationHandler; import com.ycwl.basic.service.pc.processor.VideoRecreationHandler;
import com.ycwl.basic.service.pc.strategy.RematchContext; import com.ycwl.basic.service.pc.strategy.RematchContext;
@@ -86,7 +85,8 @@ import com.ycwl.basic.storage.enums.StorageAcl;
import com.ycwl.basic.storage.utils.StorageUtil; import com.ycwl.basic.storage.utils.StorageUtil;
import com.ycwl.basic.utils.*; import com.ycwl.basic.utils.*;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.util.Strings; 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.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
@@ -193,6 +193,8 @@ public class FaceServiceImpl implements FaceService {
private IPriceCalculationService iPriceCalculationService; private IPriceCalculationService iPriceCalculationService;
@Autowired @Autowired
private PuzzleTemplateMapper puzzleTemplateMapper; private PuzzleTemplateMapper puzzleTemplateMapper;
@Autowired
private FaceDetectLogAiCamService faceDetectLogAiCamService;
@Override @Override
public ApiResponse<PageInfo<FaceRespVO>> pageQuery(FaceReqQuery faceReqQuery) { public ApiResponse<PageInfo<FaceRespVO>> pageQuery(FaceReqQuery faceReqQuery) {
@@ -312,13 +314,16 @@ public class FaceServiceImpl implements FaceService {
Long finalFaceId = newFaceId; Long finalFaceId = newFaceId;
Thread thread = new Thread(() -> printerService.autoAddPhotosToPreferPrint(finalFaceId), "auto-add-print-" + newFaceId); Thread thread = new Thread(() -> printerService.autoAddPhotosToPreferPrint(finalFaceId), "auto-add-print-" + newFaceId);
thread.start(); thread.start();
if (org.apache.commons.lang3.Strings.CI.equals("print", scene)) { if (Strings.CI.equals("print", scene)) {
try { try {
thread.join(); thread.join();
} catch (InterruptedException ignore) { } catch (InterruptedException ignore) {
} }
} }
if (Strings.CI.equals("aiCam", scene)) {
faceDetectLogAiCamService.searchAndLog(scenicId, newFaceId, faceUrl, faceBodyAdapter);
}
return resp; return resp;
} }
@@ -468,9 +473,9 @@ public class FaceServiceImpl implements FaceService {
contentPageVO.setLockType(1); contentPageVO.setLockType(1);
} }
} }
boolean buy = orderBiz.checkUserBuyItem(userId, contentPageVO.getGoodsType(), contentPageVO.getContentId()); boolean buy = orderBiz.checkUserBuyFaceItem(userId, faceId, contentPageVO.getGoodsType(), contentPageVO.getContentId());
if (!buy) { if (!buy) {
buy = orderBiz.checkUserBuyItem(userId, -1, contentPageVO.getTemplateId()); buy = orderBiz.checkUserBuyFaceItem(userId, faceId, -1, contentPageVO.getTemplateId());
} }
if (buy) { if (buy) {
contentPageVO.setIsBuy(1); contentPageVO.setIsBuy(1);
@@ -482,7 +487,7 @@ public class FaceServiceImpl implements FaceService {
List<PuzzleTemplateEntity> puzzleTemplateEntityList = puzzleTemplateMapper.list(face.getScenicId(), null, 1); List<PuzzleTemplateEntity> puzzleTemplateEntityList = puzzleTemplateMapper.list(face.getScenicId(), null, 1);
if (!puzzleTemplateEntityList.isEmpty()) { if (!puzzleTemplateEntityList.isEmpty()) {
List<PuzzleGenerationRecordEntity> records = puzzleGenerationRecordMapper.listByFaceId(faceId); List<PuzzleGenerationRecordEntity> records = puzzleGenerationRecordMapper.listByFaceId(faceId);
puzzleTemplateEntityList.forEach(template -> { PuzzleTemplateEntity template = puzzleTemplateEntityList.getFirst();
Optional<PuzzleGenerationRecordEntity> optionalRecord = records.stream().filter(r -> r.getTemplateId().equals(template.getId())).findFirst(); Optional<PuzzleGenerationRecordEntity> optionalRecord = records.stream().filter(r -> r.getTemplateId().equals(template.getId())).findFirst();
ContentPageVO sfpContent = new ContentPageVO(); ContentPageVO sfpContent = new ContentPageVO();
sfpContent.setName(template.getName()); sfpContent.setName(template.getName());
@@ -497,6 +502,10 @@ public class FaceServiceImpl implements FaceService {
sfpContent.setGoodsType(3); sfpContent.setGoodsType(3);
sfpContent.setSort(0); sfpContent.setSort(0);
if (optionalRecord.isPresent()) { if (optionalRecord.isPresent()) {
IsBuyRespVO isBuyScenic = orderBiz.isBuy(face.getScenicId(), face.getMemberId(), faceId, 5, face.getScenicId());
if (isBuyScenic.isBuy()) {
sfpContent.setIsBuy(1);
} else {
IsBuyRespVO isBuyRespVO = orderBiz.isBuy(face.getScenicId(), face.getMemberId(), faceId, 5, optionalRecord.get().getTemplateId()); IsBuyRespVO isBuyRespVO = orderBiz.isBuy(face.getScenicId(), face.getMemberId(), faceId, 5, optionalRecord.get().getTemplateId());
if (isBuyRespVO.isBuy()) { if (isBuyRespVO.isBuy()) {
sfpContent.setIsBuy(1); sfpContent.setIsBuy(1);
@@ -504,6 +513,7 @@ public class FaceServiceImpl implements FaceService {
sfpContent.setIsBuy(0); sfpContent.setIsBuy(0);
} }
} }
}
PriceCalculationRequest calculationRequest = new PriceCalculationRequest(); PriceCalculationRequest calculationRequest = new PriceCalculationRequest();
ProductItem productItem = new ProductItem(); ProductItem productItem = new ProductItem();
productItem.setProductType(ProductType.PHOTO_LOG); productItem.setProductType(ProductType.PHOTO_LOG);
@@ -521,7 +531,6 @@ public class FaceServiceImpl implements FaceService {
sfpContent.setFreeCount(1); sfpContent.setFreeCount(1);
} }
contentList.add(1, sfpContent); contentList.add(1, sfpContent);
});
} }
SourceReqQuery sourceReqQuery = new SourceReqQuery(); SourceReqQuery sourceReqQuery = new SourceReqQuery();
sourceReqQuery.setScenicId(face.getScenicId()); sourceReqQuery.setScenicId(face.getScenicId());
@@ -531,20 +540,29 @@ public class FaceServiceImpl implements FaceService {
List<SourceRespVO> sourceList = sourceMapper.queryByRelation(sourceReqQuery); List<SourceRespVO> sourceList = sourceMapper.queryByRelation(sourceReqQuery);
ContentPageVO sourceVideoContent = new ContentPageVO(); ContentPageVO sourceVideoContent = new ContentPageVO();
ContentPageVO sourceImageContent = new ContentPageVO(); ContentPageVO sourceImageContent = new ContentPageVO();
ContentPageVO sourceAiCamContent = new ContentPageVO();
sourceVideoContent.setName("录像集"); sourceVideoContent.setName("录像集");
sourceImageContent.setName("照片集"); sourceImageContent.setName("照片集");
sourceAiCamContent.setName("AI微单");
sourceVideoContent.setSort(9999); sourceVideoContent.setSort(9999);
sourceImageContent.setSort(9999); sourceImageContent.setSort(9999);
sourceAiCamContent.setSort(9999);
sourceVideoContent.setScenicId(face.getScenicId()); sourceVideoContent.setScenicId(face.getScenicId());
sourceImageContent.setScenicId(face.getScenicId()); sourceImageContent.setScenicId(face.getScenicId());
sourceAiCamContent.setScenicId(face.getScenicId());
sourceVideoContent.setGoodsType(1); sourceVideoContent.setGoodsType(1);
sourceImageContent.setGoodsType(2); sourceImageContent.setGoodsType(2);
sourceAiCamContent.setGoodsType(3);
sourceVideoContent.setContentType(2); sourceVideoContent.setContentType(2);
sourceImageContent.setContentType(2); sourceImageContent.setContentType(2);
sourceAiCamContent.setContentType(2);
sourceVideoContent.setLockType(-1); sourceVideoContent.setLockType(-1);
sourceImageContent.setLockType(-1); sourceImageContent.setLockType(-1);
sourceAiCamContent.setLockType(-1);
sourceVideoContent.setGroup("直出原片"); sourceVideoContent.setGroup("直出原片");
sourceImageContent.setGroup("直出原片"); sourceImageContent.setGroup("直出原片");
sourceAiCamContent.setGroup("直出原片");
ScenicConfigManager configManager = scenicRepository.getScenicConfigManager(face.getScenicId());
if (!scenicConfigFacade.isDisableSourceImage(face.getScenicId())) { if (!scenicConfigFacade.isDisableSourceImage(face.getScenicId())) {
IsBuyRespVO isBuyRespVO = orderBiz.isBuy(face.getScenicId(), userId, faceId, SourceType.IMAGE.getCode(), faceId); IsBuyRespVO isBuyRespVO = orderBiz.isBuy(face.getScenicId(), userId, faceId, SourceType.IMAGE.getCode(), faceId);
sourceImageContent.setSourceType(isBuyRespVO.getGoodsType()); sourceImageContent.setSourceType(isBuyRespVO.getGoodsType());
@@ -583,18 +601,40 @@ public class FaceServiceImpl implements FaceService {
sourceVideoContent.setFreeCount((int) freeCount); sourceVideoContent.setFreeCount((int) freeCount);
contentList.add(sourceVideoContent); contentList.add(sourceVideoContent);
} }
// AI微单:只有存在type=3的数据时才添加
boolean hasAiCam = sourceList.stream().anyMatch(source -> source.getType() == 3);
if (hasAiCam) {
IsBuyRespVO isBuyRespVO = orderBiz.isBuy(face.getScenicId(), userId, faceId, 13, faceId);
sourceAiCamContent.setSourceType(isBuyRespVO.getGoodsType());
sourceAiCamContent.setContentId(isBuyRespVO.getGoodsId());
if (isBuyRespVO.isBuy()) {
sourceAiCamContent.setIsBuy(1);
} else {
sourceAiCamContent.setIsBuy(0);
}
// AI微单有数据才显示,所以lockType固定为-1
sourceAiCamContent.setLockType(-1);
List<MemberSourceEntity> relations = memberRelationRepository.listSourceByFaceRelation(faceId, 3);
long freeCount = relations.stream().filter(entity -> Integer.valueOf(1).equals(entity.getIsFree())).count();
sourceAiCamContent.setFreeCount((int) freeCount);
contentList.add(sourceAiCamContent);
}
sourceList.stream().collect(Collectors.groupingBy(SourceRespVO::getType)).forEach((type, list) -> { sourceList.stream().collect(Collectors.groupingBy(SourceRespVO::getType)).forEach((type, list) -> {
if (type == 1) { if (type == 1) {
sourceVideoContent.setSourceType(1); sourceVideoContent.setSourceType(1);
sourceVideoContent.setLockType(-1); sourceVideoContent.setLockType(-1);
sourceVideoContent.setTemplateCoverUrl(list.getFirst().getUrl()); sourceVideoContent.setTemplateCoverUrl(list.getFirst().getUrl());
} else { } else if (type == 2) {
sourceImageContent.setSourceType(2); sourceImageContent.setSourceType(2);
sourceImageContent.setLockType(-1); sourceImageContent.setLockType(-1);
sourceImageContent.setTemplateCoverUrl(list.getFirst().getUrl()); sourceImageContent.setTemplateCoverUrl(list.getFirst().getUrl());
if (Strings.isBlank(sourceVideoContent.getTemplateCoverUrl())) { if (StringUtils.isBlank(sourceVideoContent.getTemplateCoverUrl())) {
sourceVideoContent.setTemplateCoverUrl(list.getFirst().getUrl()); sourceVideoContent.setTemplateCoverUrl(list.getFirst().getUrl());
} }
} else if (type == 3) {
sourceAiCamContent.setSourceType(13);
sourceAiCamContent.setLockType(-1);
sourceAiCamContent.setTemplateCoverUrl(configManager.getString("ai_camera_cover_url"));
} }
}); });
return contentList; return contentList;
@@ -803,7 +843,7 @@ public class FaceServiceImpl implements FaceService {
return List.of(); return List.of();
} }
String matchResult = face.getMatchResult(); String matchResult = face.getMatchResult();
if (matchResult == null || Strings.isBlank(matchResult)) { if (matchResult == null || StringUtils.isBlank(matchResult)) {
return List.of(); return List.of();
} }
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(face.getScenicId()); ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(face.getScenicId());
@@ -1009,7 +1049,7 @@ public class FaceServiceImpl implements FaceService {
handleCustomFaceMatching(faceId, finalSampleList); handleCustomFaceMatching(faceId, finalSampleList);
} }
if (Strings.isNotBlank(req.getRemark())) { if (StringUtils.isNotBlank(req.getRemark())) {
log.info("人脸识别人工调整备注:faceId={}, remark={}", faceId, req.getRemark()); log.info("人脸识别人工调整备注:faceId={}, remark={}", faceId, req.getRemark());
} }
} }
@@ -1035,7 +1075,7 @@ public class FaceServiceImpl implements FaceService {
detail.setLastMatchedAt(face.getUpdateAt() != null ? face.getUpdateAt() : face.getCreateAt()); detail.setLastMatchedAt(face.getUpdateAt() != null ? face.getUpdateAt() : face.getCreateAt());
String matchResultJson = face.getMatchResult(); String matchResultJson = face.getMatchResult();
if (Strings.isBlank(matchResultJson)) { if (StringUtils.isBlank(matchResultJson)) {
detail.setAcceptedSamples(Collections.emptyList()); detail.setAcceptedSamples(Collections.emptyList());
detail.setFilteredSamples(Collections.emptyList()); detail.setFilteredSamples(Collections.emptyList());
return detail; return detail;
@@ -1155,7 +1195,7 @@ public class FaceServiceImpl implements FaceService {
} }
private List<Long> parseMatchSampleIds(String matchSampleIds) { private List<Long> parseMatchSampleIds(String matchSampleIds) {
if (Strings.isBlank(matchSampleIds)) { if (StringUtils.isBlank(matchSampleIds)) {
return Collections.emptyList(); return Collections.emptyList();
} }
String[] segments = matchSampleIds.split(","); String[] segments = matchSampleIds.split(",");
@@ -1170,7 +1210,7 @@ public class FaceServiceImpl implements FaceService {
} }
private Long parseLongSilently(String value) { private Long parseLongSilently(String value) {
if (Strings.isBlank(value)) { if (StringUtils.isBlank(value)) {
return null; return null;
} }
try { try {
@@ -1222,10 +1262,10 @@ public class FaceServiceImpl implements FaceService {
if (sourceEntity == null) { if (sourceEntity == null) {
return null; return null;
} }
if (!Strings.isBlank(sourceEntity.getThumbUrl())) { if (!StringUtils.isBlank(sourceEntity.getThumbUrl())) {
return sourceEntity.getThumbUrl(); return sourceEntity.getThumbUrl();
} }
if (!Strings.isBlank(sourceEntity.getUrl())) { if (!StringUtils.isBlank(sourceEntity.getUrl())) {
return sourceEntity.getUrl(); return sourceEntity.getUrl();
} }
return null; return null;

View File

@@ -177,6 +177,9 @@ public class OrderServiceImpl implements OrderService {
} else if (Integer.valueOf(2).equals(orderItemList.getFirst().getGoodsType())) { } else if (Integer.valueOf(2).equals(orderItemList.getFirst().getGoodsType())) {
item.setGoodsName("照片集"); item.setGoodsName("照片集");
item.setOrderType("照片集"); item.setOrderType("照片集");
} else if (Integer.valueOf(13).equals(orderItemList.getFirst().getGoodsType())) {
item.setGoodsName("AI微单");
item.setOrderType("AI微单");
} else if (Integer.valueOf(0).equals(orderItemList.getFirst().getGoodsType())) { } else if (Integer.valueOf(0).equals(orderItemList.getFirst().getGoodsType())) {
item.setOrderType("旅行Vlog"); item.setOrderType("旅行Vlog");
item.setGoodsName(orderItemList.getFirst().getGoodsName()); item.setGoodsName(orderItemList.getFirst().getGoodsName());
@@ -237,6 +240,9 @@ public class OrderServiceImpl implements OrderService {
} else if (Integer.valueOf(2).equals(orderItemList.getFirst().getGoodsType())) { } else if (Integer.valueOf(2).equals(orderItemList.getFirst().getGoodsType())) {
item.setGoodsName("照片集"); item.setGoodsName("照片集");
item.setOrderType("照片集"); item.setOrderType("照片集");
} else if (Integer.valueOf(13).equals(orderItemList.getFirst().getGoodsType())) {
item.setGoodsName("AI微单");
item.setOrderType("AI微单");
} else if (Integer.valueOf(0).equals(orderItemList.getFirst().getGoodsType())) { } else if (Integer.valueOf(0).equals(orderItemList.getFirst().getGoodsType())) {
item.setOrderType("旅行Vlog"); item.setOrderType("旅行Vlog");
item.setGoodsName(orderItemList.getFirst().getGoodsName()); item.setGoodsName(orderItemList.getFirst().getGoodsName());
@@ -344,6 +350,26 @@ public class OrderServiceImpl implements OrderService {
item.setShootingTime(memberVideoEntityList.getFirst().getCreateTime()); item.setShootingTime(memberVideoEntityList.getFirst().getCreateTime());
} }
} }
} else if (Integer.valueOf(13).equals(item.getGoodsType())) { // AI相机照片 goodsId就是人脸ID
List<SourceEntity> aiCamImageList = sourceMapper.listAiCamImageByFaceRelation(item.getGoodsId());
item.setCoverList(aiCamImageList.stream().map(SourceEntity::getUrl).collect(Collectors.toList()));
if (!_f.contains(13)) {
_f.add(13);
if (!aiCamImageList.isEmpty()) {
for (SourceEntity sourceEntity : aiCamImageList) {
GoodsDetailVO goods = new GoodsDetailVO();
goods.setGoodsId(sourceEntity.getId());
goods.setGoodsName("AI相机照片");
goods.setUrl(sourceEntity.getUrl());
goods.setGoodsType(sourceEntity.getType());
goods.setScenicId(sourceEntity.getScenicId());
goods.setTemplateCoverUrl(sourceEntity.getUrl());
goods.setCreateTime(sourceEntity.getCreateTime());
goodsList.add(goods);
}
item.setShootingTime(aiCamImageList.getFirst().getCreateTime());
}
}
} else if (Integer.valueOf(3).equals(item.getGoodsType())) { // 打印照片 goodsId就是memberPrintId } else if (Integer.valueOf(3).equals(item.getGoodsType())) { // 打印照片 goodsId就是memberPrintId
List<MemberPrintResp> list = printerMapper.getUserPhotoByIds(orderItemList.stream().map(OrderItemVO::getGoodsId).collect(Collectors.toList())); List<MemberPrintResp> list = printerMapper.getUserPhotoByIds(orderItemList.stream().map(OrderItemVO::getGoodsId).collect(Collectors.toList()));
item.setCoverList(orderItemList.stream().map(OrderItemVO::getCoverUrl).collect(Collectors.toList())); item.setCoverList(orderItemList.stream().map(OrderItemVO::getCoverUrl).collect(Collectors.toList()));
@@ -546,6 +572,13 @@ public class OrderServiceImpl implements OrderService {
item.setShootingTime(memberVideoEntityList.getFirst().getCreateTime()); item.setShootingTime(memberVideoEntityList.getFirst().getCreateTime());
item.setCount(1); item.setCount(1);
} }
} else if (Integer.valueOf(13).equals(item.getGoodsType())) {
List<SourceEntity> aiCamImageList = sourceMapper.listAiCamImageByFaceRelation(item.getFaceId());
item.setCoverList(aiCamImageList.stream().map(SourceEntity::getUrl).collect(Collectors.toList()));
if (!aiCamImageList.isEmpty()) {
item.setShootingTime(aiCamImageList.getFirst().getCreateTime());
item.setCount(1);
}
} else if (Integer.valueOf(0).equals(item.getGoodsType())) { } else if (Integer.valueOf(0).equals(item.getGoodsType())) {
item.setCoverList(Collections.singletonList(item.getCoverUrl())); item.setCoverList(Collections.singletonList(item.getCoverUrl()));
VideoEntity video = videoRepository.getVideo(item.getGoodsId()); VideoEntity video = videoRepository.getVideo(item.getGoodsId());
@@ -682,6 +715,9 @@ public class OrderServiceImpl implements OrderService {
} else if (Integer.valueOf(2).equals(orderItemList.getFirst().getGoodsType())) { } else if (Integer.valueOf(2).equals(orderItemList.getFirst().getGoodsType())) {
item.setGoodsName("照片集"); item.setGoodsName("照片集");
item.setOrderType("照片集"); item.setOrderType("照片集");
} else if (Integer.valueOf(13).equals(orderItemList.getFirst().getGoodsType())) {
item.setGoodsName("打卡点拍照");
item.setOrderType("打卡点拍照");
} else if (Integer.valueOf(0).equals(orderItemList.getFirst().getGoodsType())) { } else if (Integer.valueOf(0).equals(orderItemList.getFirst().getGoodsType())) {
item.setOrderType("旅行Vlog"); item.setOrderType("旅行Vlog");
item.setGoodsName(orderItemList.getFirst().getGoodsName()); item.setGoodsName(orderItemList.getFirst().getGoodsName());
@@ -880,15 +916,13 @@ public class OrderServiceImpl implements OrderService {
List<OrderItemEntity> orderItems = goodsList.stream().map(goods -> { List<OrderItemEntity> orderItems = goodsList.stream().map(goods -> {
OrderItemEntity orderItem = new OrderItemEntity(); OrderItemEntity orderItem = new OrderItemEntity();
orderItem.setOrderId(orderId); orderItem.setOrderId(orderId);
if (Long.valueOf(1L).equals(goods.getGoodsId())) {
orderItem.setGoodsId(batchOrderReqVO.getFaceId());
orderItem.setGoodsType(1);
} else if (Long.valueOf(2L).equals(goods.getGoodsId())) {
orderItem.setGoodsId(batchOrderReqVO.getFaceId());
orderItem.setGoodsType(2);
} else {
// templateId
orderItem.setGoodsId(goods.getGoodsId()); orderItem.setGoodsId(goods.getGoodsId());
orderItem.setGoodsType(goods.getGoodsType());
if (Integer.valueOf(1).equals(goods.getGoodsType())) {
orderItem.setGoodsId(batchOrderReqVO.getFaceId());
} else if (Integer.valueOf(2).equals(goods.getGoodsType())) {
orderItem.setGoodsId(batchOrderReqVO.getFaceId());
} else if (Integer.valueOf(0).equals(goods.getGoodsType())) {
orderItem.setGoodsType(-1); orderItem.setGoodsType(-1);
} }
return orderItem; return orderItem;
@@ -940,11 +974,13 @@ public class OrderServiceImpl implements OrderService {
case PHOTO_SET -> 2; case PHOTO_SET -> 2;
case VLOG_VIDEO -> 0; case VLOG_VIDEO -> 0;
case RECORDING_SET -> 1; case RECORDING_SET -> 1;
case AI_CAM_PHOTO_SET -> 13;
default -> 0; default -> 0;
}; };
Long goodsId = switch (productItem.getProductType()) { Long goodsId = switch (productItem.getProductType()) {
case PHOTO_LOG -> Long.valueOf(productItem.getProductId()); case PHOTO_LOG -> Long.valueOf(productItem.getProductId());
case PHOTO_SET, RECORDING_SET -> face.getId(); case PHOTO_SET, RECORDING_SET -> face.getId();
case AI_CAM_PHOTO_SET -> face.getId();
case VLOG_VIDEO -> { case VLOG_VIDEO -> {
List<MemberVideoEntity> videos = memberRelationRepository.listRelationByFaceAndTemplate(face.getId(), Long.valueOf(productItem.getProductId())); List<MemberVideoEntity> videos = memberRelationRepository.listRelationByFaceAndTemplate(face.getId(), Long.valueOf(productItem.getProductId()));
yield videos.getFirst().getVideoId(); yield videos.getFirst().getVideoId();
@@ -1017,6 +1053,13 @@ public class OrderServiceImpl implements OrderService {
orderItem.setGoodsType(type); orderItem.setGoodsType(type);
orderItem.setOrderId(order.getId()); orderItem.setOrderId(order.getId());
orderItems.add(orderItem); orderItems.add(orderItem);
// ======== 兼容旧逻辑 ==========
if (order.getType() == 3) {
redisTemplate.opsForValue().set("order_content_not_downloadable_" + order.getId(), "1");
}
if (type == 13) {
redisTemplate.opsForValue().set("order_content_not_downloadable_" + order.getId(), "1");
}
// 在事务中保存订单数据 // 在事务中保存订单数据
try { try {
self.saveOrderInTransaction(order, orderItems, haveOldOrder); self.saveOrderInTransaction(order, orderItems, haveOldOrder);

View File

@@ -390,10 +390,14 @@ public class FaceMatchingOrchestrator {
baseDynamicData.put("scenicText", scenicBasic.getName()); baseDynamicData.put("scenicText", scenicBasic.getName());
baseDynamicData.put("dateStr", DateUtil.format(new Date(), "yyyy.MM.dd")); baseDynamicData.put("dateStr", DateUtil.format(new Date(), "yyyy.MM.dd"));
// 遍历所有模板,逐个生成 // 使用虚拟线程池并行生成所有模板
int successCount = 0; java.util.concurrent.atomic.AtomicInteger successCount = new java.util.concurrent.atomic.AtomicInteger(0);
int failCount = 0; java.util.concurrent.atomic.AtomicInteger failCount = new java.util.concurrent.atomic.AtomicInteger(0);
for (PuzzleTemplateDTO template : templateList) {
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 { try {
log.info("开始生成拼图: scenicId={}, templateCode={}, templateName={}", log.info("开始生成拼图: scenicId={}, templateCode={}, templateName={}",
scenicId, template.getCode(), template.getName()); scenicId, template.getCode(), template.getName());
@@ -415,17 +419,22 @@ public class FaceMatchingOrchestrator {
log.info("拼图生成成功: scenicId={}, templateCode={}, imageUrl={}", log.info("拼图生成成功: scenicId={}, templateCode={}, imageUrl={}",
scenicId, template.getCode(), response.getImageUrl()); scenicId, template.getCode(), response.getImageUrl());
successCount++; successCount.incrementAndGet();
} catch (Exception e) { } catch (Exception e) {
log.error("拼图生成失败: scenicId={}, templateCode={}, templateName={}", log.error("拼图生成失败: scenicId={}, templateCode={}, templateName={}",
scenicId, template.getCode(), template.getName(), e); scenicId, template.getCode(), template.getName(), e);
failCount++; failCount.incrementAndGet();
} }
}, executor))
.toList();
// 等待所有任务完成
java.util.concurrent.CompletableFuture.allOf(futures.toArray(new java.util.concurrent.CompletableFuture[0])).join();
} }
log.info("景区拼图模板批量生成完成: scenicId={}, 总数={}, 成功={}, 失败={}", log.info("景区拼图模板批量生成完成: scenicId={}, 总数={}, 成功={}, 失败={}",
scenicId, templateList.size(), successCount, failCount); scenicId, templateList.size(), successCount.get(), failCount.get());
} catch (Exception e) { } catch (Exception e) {
// 异步任务失败不影响主流程,仅记录日志 // 异步任务失败不影响主流程,仅记录日志

View File

@@ -14,8 +14,11 @@ import com.ycwl.basic.image.pipeline.stages.ConditionalRotateStage;
import com.ycwl.basic.image.pipeline.stages.DownloadStage; import com.ycwl.basic.image.pipeline.stages.DownloadStage;
import com.ycwl.basic.image.pipeline.stages.ImageEnhanceStage; import com.ycwl.basic.image.pipeline.stages.ImageEnhanceStage;
import com.ycwl.basic.image.pipeline.stages.ImageOrientationStage; import com.ycwl.basic.image.pipeline.stages.ImageOrientationStage;
import com.ycwl.basic.image.pipeline.stages.ImageResizeStage;
import com.ycwl.basic.image.pipeline.stages.ImageSRStage;
import com.ycwl.basic.image.pipeline.stages.PuzzleBorderStage; import com.ycwl.basic.image.pipeline.stages.PuzzleBorderStage;
import com.ycwl.basic.image.pipeline.stages.RestoreOrientationStage; import com.ycwl.basic.image.pipeline.stages.RestoreOrientationStage;
import com.ycwl.basic.image.pipeline.stages.UpdateMemberPrintStage;
import com.ycwl.basic.image.pipeline.stages.UploadStage; import com.ycwl.basic.image.pipeline.stages.UploadStage;
import com.ycwl.basic.image.pipeline.stages.WatermarkConfig; import com.ycwl.basic.image.pipeline.stages.WatermarkConfig;
import com.ycwl.basic.image.pipeline.stages.WatermarkStage; import com.ycwl.basic.image.pipeline.stages.WatermarkStage;
@@ -29,6 +32,7 @@ import com.ycwl.basic.mapper.OrderMapper;
import com.ycwl.basic.mapper.PrintTaskMapper; import com.ycwl.basic.mapper.PrintTaskMapper;
import com.ycwl.basic.mapper.PrinterMapper; import com.ycwl.basic.mapper.PrinterMapper;
import com.ycwl.basic.mapper.SourceMapper; import com.ycwl.basic.mapper.SourceMapper;
import com.ycwl.basic.model.Crop;
import com.ycwl.basic.model.PrinterOrderItem; import com.ycwl.basic.model.PrinterOrderItem;
import com.ycwl.basic.model.mobile.face.FaceRecognizeResp; import com.ycwl.basic.model.mobile.face.FaceRecognizeResp;
import com.ycwl.basic.model.mobile.order.PriceObj; import com.ycwl.basic.model.mobile.order.PriceObj;
@@ -347,6 +351,8 @@ public class PrinterServiceImpl implements PrinterService {
log.info("照片裁剪成功: memberId={}, scenicId={}, 原图={}, 裁剪后={}, 尺寸={}x{}", log.info("照片裁剪成功: memberId={}, scenicId={}, 原图={}, 裁剪后={}, 尺寸={}x{}",
memberId, scenicId, url, cropUrl, printWidth, printHeight); memberId, scenicId, url, cropUrl, printWidth, printHeight);
String crop = JacksonUtil.toJSONString(new Crop(270));
entity.setCrop(crop);
} finally { } finally {
// 清理临时文件 // 清理临时文件
if (croppedFile != null && croppedFile.exists()) { if (croppedFile != null && croppedFile.exists()) {
@@ -467,6 +473,34 @@ public class PrinterServiceImpl implements PrinterService {
request.setProducts(productItems); request.setProducts(productItems);
// 检查是否存在type=3的source记录,存在才自动发券
boolean hasType3Source = userPhotoList.stream()
.filter(item -> item.getSourceId() != null && item.getSourceId() > 0)
.anyMatch(item -> {
try {
SourceEntity source = sourceMapper.getEntity(item.getSourceId());
return source != null && Integer.valueOf(3).equals(source.getType());
} catch (Exception e) {
log.warn("查询source失败: sourceId={}, error={}", item.getSourceId(), e.getMessage());
return false;
}
});
if (hasType3Source) {
if (normalCount > 0) {
try {
autoCouponService.autoGrantCoupon(
memberId,
faceId,
scenicId,
ProductType.PHOTO_PRINT
);
} catch (Exception e) {
log.warn("自动发券失败,不影响下单流程: memberId={}, faceId={}, scenicId={}, error={}",
memberId, faceId, scenicId, e.getMessage());
}
}
}
if (mobileCount > 0) { if (mobileCount > 0) {
try { try {
autoCouponService.autoGrantCoupon( autoCouponService.autoGrantCoupon(
@@ -505,6 +539,10 @@ public class PrinterServiceImpl implements PrinterService {
} }
String url = byId.getUrl(); String url = byId.getUrl();
// 特殊兼容处理
if (Integer.valueOf(3).equals(byId.getType())) {
url = byId.getThumbUrl().replace("_t.", "_o.");
}
MemberPrintEntity entity = new MemberPrintEntity(); MemberPrintEntity entity = new MemberPrintEntity();
entity.setMemberId(memberId); entity.setMemberId(memberId);
entity.setScenicId(scenicId); entity.setScenicId(scenicId);
@@ -540,6 +578,7 @@ public class PrinterServiceImpl implements PrinterService {
// 使用smartCropAndFill裁剪图片 // 使用smartCropAndFill裁剪图片
File croppedFile = ImageUtils.smartCropAndFill(url, printWidth, printHeight); File croppedFile = ImageUtils.smartCropAndFill(url, printWidth, printHeight);
entity.setCrop(JacksonUtil.toJSONString(new Crop(270)));
try { try {
// 上传裁剪后的图片 // 上传裁剪后的图片
@@ -800,6 +839,18 @@ public class PrinterServiceImpl implements PrinterService {
* @return 处理后的URL,失败返回原URL * @return 处理后的URL,失败返回原URL
*/ */
private String processPhotoWithPipeline(MemberPrintResp item, Long scenicId, File qrCodeFile) { private String processPhotoWithPipeline(MemberPrintResp item, Long scenicId, File qrCodeFile) {
return processPhotoWithPipeline(item, scenicId, qrCodeFile, null);
}
/**
* 使用管线处理照片(支持增强选项)
* @param item 打印项
* @param scenicId 景区ID
* @param qrCodeFile 二维码文件
* @param needEnhance 是否需要图像增强(null 或 false 表示不增强)
* @return 处理后的URL,失败返回原URL
*/
private String processPhotoWithPipeline(MemberPrintResp item, Long scenicId, File qrCodeFile, Boolean needEnhance) {
PrinterOrderItem orderItem = PrinterOrderItem.fromMemberPrintResp(item); PrinterOrderItem orderItem = PrinterOrderItem.fromMemberPrintResp(item);
PhotoProcessContext context = PhotoProcessContext.fromPrinterOrderItem(orderItem, scenicId); PhotoProcessContext context = PhotoProcessContext.fromPrinterOrderItem(orderItem, scenicId);
@@ -812,11 +863,15 @@ public class PrinterServiceImpl implements PrinterService {
// 设置管线场景为图片打印 // 设置管线场景为图片打印
context.setScene(PipelineScene.IMAGE_PRINT); context.setScene(PipelineScene.IMAGE_PRINT);
// 根据sourceId判断图片来源 // 处理图像增强选项
// sourceId > 0: IPC设备拍摄 if (needEnhance != null && needEnhance) {
// sourceId == null: 手机上传 context.setStageState("image_enhance", true);
// sourceId == 0: 拼图(暂定为UNKNOWN) }
// 根据sourceId判断图片来源和source类型
SourceEntity source = null;
if (item.getSourceId() != null && item.getSourceId() > 0) { if (item.getSourceId() != null && item.getSourceId() > 0) {
source = sourceMapper.getEntity(item.getSourceId());
context.setSource(ImageSource.IPC); context.setSource(ImageSource.IPC);
} else if (item.getSourceId() == null) { } else if (item.getSourceId() == null) {
context.setSource(ImageSource.PHONE); context.setSource(ImageSource.PHONE);
@@ -825,15 +880,49 @@ public class PrinterServiceImpl implements PrinterService {
} }
Pipeline<PhotoProcessContext> pipeline; Pipeline<PhotoProcessContext> pipeline;
if (context.getImageType() == ImageType.NORMAL_PHOTO) {
// 特殊处理: sourceId > 0 且 source.type == 3
if (source != null && source.getType() != null && source.getType() == 3) {
// Type=3的特殊处理流程
log.info("检测到source.type=3, 使用特殊处理管线: sourceId={}", item.getSourceId());
// 准备百度云配置
BceEnhancerConfig bceConfig = new BceEnhancerConfig();
bceConfig.setQps(1);
bceConfig.setAppId("119554288");
bceConfig.setApiKey("OX6QoijgKio3eVtA0PiUVf7f");
bceConfig.setSecretKey("dYatXReVriPeiktTjUblhfubpcmYfuMk");
// 准备水印配置 // 准备水印配置
WatermarkConfig watermarkConfig = prepareWatermarkConfig(context, qrCodeFile); WatermarkConfig watermarkConfig = prepareWatermarkConfig(context, qrCodeFile, 3.0);
// 准备存储适配器 // 准备存储适配器
prepareStorageAdapter(context); prepareStorageAdapter(context);
// 创建普通照片管线 context.enableStage("image_sr");
context.enableStage("image_enhance");
// 构建特殊管线: 超分(2倍) -> 增强 -> 更新MemberPrint -> 缩小2倍 -> 水印 -> 上传
pipeline = new PipelineBuilder<PhotoProcessContext>("Type3Pipeline")
.addStage(new DownloadStage()) // 1. 下载图片
.addStage(new ImageOrientationStage()) // 2. 检测方向
.addStage(new ConditionalRotateStage()) // 3. 条件性旋转
.addStage(new ImageSRStage(bceConfig)) // 4. 超分辨率(2倍放大)
.addStage(new ImageEnhanceStage(bceConfig)) // 5. 图像增强
.addStage(new UploadStage()) // 6. 上传(用于更新MemberPrint)
.addStage(new UpdateMemberPrintStage(printerMapper, // 7. 更新MemberPrint的cropUrl
item.getId(), item.getMemberId(), scenicId))
.addStage(new WatermarkStage(watermarkConfig)) // 9. 添加水印
.addStage(new RestoreOrientationStage()) // 10. 恢复方向
.addStage(new UploadStage()) // 11. 最终上传
.addStage(new CleanupStage()) // 12. 清理
.build();
} else if (context.getImageType() == ImageType.NORMAL_PHOTO) {
// 普通照片处理流程
WatermarkConfig watermarkConfig = prepareWatermarkConfig(context, qrCodeFile, 1.5);
prepareStorageAdapter(context);
pipeline = createNormalPhotoPipeline(watermarkConfig); pipeline = createNormalPhotoPipeline(watermarkConfig);
} else { } else {
// 拼图 // 拼图处理流程
prepareStorageAdapter(context); prepareStorageAdapter(context);
pipeline = createPuzzlePipeline(); pipeline = createPuzzlePipeline();
} }
@@ -867,7 +956,7 @@ public class PrinterServiceImpl implements PrinterService {
* @param qrCodeFile 二维码文件 * @param qrCodeFile 二维码文件
* @return WatermarkConfig * @return WatermarkConfig
*/ */
private WatermarkConfig prepareWatermarkConfig(PhotoProcessContext context, File qrCodeFile) { private WatermarkConfig prepareWatermarkConfig(PhotoProcessContext context, File qrCodeFile, Double scale) {
ScenicConfigManager scenicConfig = context.getScenicConfigManager(); ScenicConfigManager scenicConfig = context.getScenicConfigManager();
if (scenicConfig == null) { if (scenicConfig == null) {
log.warn("scenicConfigManager未设置,返回空水印配置"); log.warn("scenicConfigManager未设置,返回空水印配置");
@@ -888,6 +977,7 @@ public class PrinterServiceImpl implements PrinterService {
.scenicText(scenicText) .scenicText(scenicText)
.dateFormat(dateFormat) .dateFormat(dateFormat)
.qrcodeFile(qrCodeFile) .qrcodeFile(qrCodeFile)
.scale(scale)
.build(); .build();
} }
@@ -937,6 +1027,7 @@ public class PrinterServiceImpl implements PrinterService {
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
Thread.ofVirtual().start(() -> {
userPhotoListByOrderId.forEach(item -> { userPhotoListByOrderId.forEach(item -> {
PrinterEntity printer = printerMapper.getById(item.getPrinterId()); PrinterEntity printer = printerMapper.getById(item.getPrinterId());
@@ -985,6 +1076,8 @@ public class PrinterServiceImpl implements PrinterService {
} }
} }
}); });
redisTemplate.delete("order_content_not_downloadable_" + orderId);
});
} }
/** /**
@@ -1418,58 +1511,15 @@ public class PrinterServiceImpl implements PrinterService {
needEnhance = false; // 默认不增强 needEnhance = false; // 默认不增强
} }
// 3.1 创建图片处理上下文 // 3.1 使用管线处理照片(复用 processPhotoWithPipeline)
PrinterOrderItem orderItem = PrinterOrderItem.fromMemberPrintResp(memberPrint); String newPrintUrl;
PhotoProcessContext context = PhotoProcessContext.fromPrinterOrderItem(orderItem, memberPrint.getScenicId());
context.setStageState("image_enhance", needEnhance); // 通过setStageState方法设置是否启用
// 3.2 设置景区配置和场景
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(memberPrint.getScenicId());
context.setScenicConfigManager(scenicConfig);
context.setScene(PipelineScene.IMAGE_PRINT);
// 3.3 判断图片来源
if (memberPrint.getSourceId() != null && memberPrint.getSourceId() > 0) {
context.setSource(ImageSource.IPC);
} else if (memberPrint.getSourceId() == null) {
context.setSource(ImageSource.PHONE);
} else {
context.setSource(ImageSource.UNKNOWN);
}
// 3.4 构建管线(关键:条件性添加 ImageEnhanceStage)
Pipeline<PhotoProcessContext> pipeline;
String newPrintUrl = null;
try { try {
if (context.getImageType() == ImageType.NORMAL_PHOTO) { newPrintUrl = processPhotoWithPipeline(memberPrint, memberPrint.getScenicId(), qrCodeFile, needEnhance);
// 准备水印配置(重打印需要二维码)
WatermarkConfig watermarkConfig = prepareWatermarkConfig(context, qrCodeFile);
prepareStorageAdapter(context);
// 创建管线,条件性添加增强 Stage
pipeline = createNormalPhotoPipeline(watermarkConfig);
} else {
// 拼图
prepareStorageAdapter(context);
pipeline = createPuzzlePipeline();
}
// 3.5 执行管线
boolean success = pipeline.execute(context);
if (success && context.getResultUrl() != null) {
newPrintUrl = context.getResultUrl();
log.info("handleReprint: 照片重新处理成功, taskId={}, mpId={}, enhance={}, newUrl={}", log.info("handleReprint: 照片重新处理成功, taskId={}, mpId={}, enhance={}, newUrl={}",
id, mpId, needEnhance, newPrintUrl); id, mpId, needEnhance, newPrintUrl);
} else {
log.warn("handleReprint: 照片重新处理失败, taskId={}, 使用原图", id);
newPrintUrl = memberPrint.getCropUrl(); // 使用原裁剪图
}
} catch (Exception e) { } catch (Exception e) {
log.error("handleReprint: 照片重新处理异常, taskId={}, 使用原图", id, e); log.error("handleReprint: 照片重新处理异常, taskId={}, 使用原图", id, e);
newPrintUrl = memberPrint.getCropUrl(); newPrintUrl = memberPrint.getCropUrl();
} finally {
context.cleanup();
} }
// 4. 更新打印任务 // 4. 更新打印任务

View File

@@ -14,9 +14,7 @@ public interface TaskService {
TemplateRespVO workerGetTemplate(Long templateId, WorkerAuthReqVo req); TemplateRespVO workerGetTemplate(Long templateId, WorkerAuthReqVo req);
void createTaskByFaceIdAndTemplateId(Long faceId, Long templateId); void createTaskByFaceIdAndTemplateId(Long faceId, Long templateId, boolean automatic);
void createTaskByFaceIdAndTemplateId(Long faceId, Long templateId, int automatic);
void taskSuccess(Long taskId, TaskSuccessReqVo req); void taskSuccess(Long taskId, TaskSuccessReqVo req);

View File

@@ -22,7 +22,6 @@ import com.ycwl.basic.mapper.TaskMapper;
import com.ycwl.basic.mapper.VideoMapper; import com.ycwl.basic.mapper.VideoMapper;
import com.ycwl.basic.model.mobile.order.IsBuyRespVO; import com.ycwl.basic.model.mobile.order.IsBuyRespVO;
import com.ycwl.basic.model.pc.face.entity.FaceEntity; import com.ycwl.basic.model.pc.face.entity.FaceEntity;
import com.ycwl.basic.model.pc.face.resp.FaceRespVO;
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity; import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
import com.ycwl.basic.model.pc.member.resp.MemberRespVO; import com.ycwl.basic.model.pc.member.resp.MemberRespVO;
import com.ycwl.basic.model.pc.mp.MpConfigEntity; import com.ycwl.basic.model.pc.mp.MpConfigEntity;
@@ -67,7 +66,6 @@ import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator; import java.util.Comparator;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
@@ -256,7 +254,7 @@ public class TaskTaskServiceImpl implements TaskService {
@Override @Override
public void forceCreateTaskByFaceIdAndTempalteId(Long faceId, Long templateId) { public void forceCreateTaskByFaceIdAndTempalteId(Long faceId, Long templateId) {
createTaskByFaceIdAndTemplateIdInternal(faceId, templateId, 0, true); createTaskByFaceIdAndTemplateIdInternal(faceId, templateId, false, true);
} }
@Override @Override
@@ -304,34 +302,30 @@ public class TaskTaskServiceImpl implements TaskService {
} }
if (Integer.valueOf(3).equals(scenicConfig.getInteger("book_routine")) || Integer.valueOf(4).equals(scenicConfig.getInteger("book_routine"))) { if (Integer.valueOf(3).equals(scenicConfig.getInteger("book_routine")) || Integer.valueOf(4).equals(scenicConfig.getInteger("book_routine"))) {
// 生成全部视频的逻辑 // 生成全部视频的逻辑
templateList.forEach(template -> createTaskByFaceIdAndTemplateId(faceId, template.getId(), 1)); templateList.forEach(template -> createTaskByFaceIdAndTemplateId(faceId, template.getId(), true));
} else { } else {
if (Boolean.TRUE.equals(scenicConfig.getBoolean("force_create_vlog"))) { if (Boolean.TRUE.equals(scenicConfig.getBoolean("force_create_vlog"))) {
Long availableTemplateId = templateBiz.findFirstAvailableTemplate(templateList.stream().map(TemplateRespVO::getId).toList(), faceId, false); Long availableTemplateId = templateBiz.findFirstAvailableTemplate(templateList.stream().map(TemplateRespVO::getId).toList(), faceId, false);
if (availableTemplateId != null) { if (availableTemplateId != null) {
createTaskByFaceIdAndTemplateId(faceId, availableTemplateId, 1); createTaskByFaceIdAndTemplateId(faceId, availableTemplateId, true);
} else { } else {
log.info("faceId:{} available template is not exist", faceId); log.info("faceId:{} available template is not exist", faceId);
} }
} else { } else {
// 非强制创建,只创建第一个可用模板 // 非强制创建,只创建第一个可用模板
if (!templateList.isEmpty()) { if (!templateList.isEmpty()) {
createTaskByFaceIdAndTemplateId(faceId, templateList.getFirst().getId(), 1); createTaskByFaceIdAndTemplateId(faceId, templateList.getFirst().getId(), true);
} }
} }
} }
} }
@Override
public void createTaskByFaceIdAndTemplateId(Long faceId, Long templateId) {
createTaskByFaceIdAndTemplateId(faceId, templateId, 0);
}
@Override @Override
public void createTaskByFaceIdAndTemplateId(Long faceId, Long templateId, int automatic) { public void createTaskByFaceIdAndTemplateId(Long faceId, Long templateId, boolean automatic) {
createTaskByFaceIdAndTemplateIdInternal(faceId, templateId, automatic, false); createTaskByFaceIdAndTemplateIdInternal(faceId, templateId, automatic, false);
} }
private void createTaskByFaceIdAndTemplateIdInternal(Long faceId, Long templateId, int automatic, boolean forceCreate) { private void createTaskByFaceIdAndTemplateIdInternal(Long faceId, Long templateId, boolean automatic, boolean forceCreate) {
FaceEntity face = faceRepository.getFace(faceId); FaceEntity face = faceRepository.getFace(faceId);
if (face == null) { if (face == null) {
log.info("faceId:{} is not exist", faceId); log.info("faceId:{} is not exist", faceId);
@@ -430,7 +424,7 @@ public class TaskTaskServiceImpl implements TaskService {
taskEntity.setScenicId(face.getScenicId()); taskEntity.setScenicId(face.getScenicId());
taskEntity.setFaceId(faceId); taskEntity.setFaceId(faceId);
taskEntity.setTemplateId(templateId); taskEntity.setTemplateId(templateId);
taskEntity.setAutomatic(automatic); taskEntity.setAutomatic(automatic ? 1 : 0);
} }
taskEntity.setWorkerId(null); taskEntity.setWorkerId(null);
taskEntity.setStatus(0); taskEntity.setStatus(0);

View File

@@ -1,9 +1,12 @@
server: server:
port: 8030 port: 8030
shutdown: graceful
spring: spring:
application: application:
name: zt name: zt
lifecycle:
timeout-per-shutdown-phase: 60s
# Feign配置(简化版,基于Nacos服务发现) # Feign配置(简化版,基于Nacos服务发现)
feign: feign:
@@ -39,3 +42,6 @@ kafka:
logging: logging:
level: level:
com.ycwl.basic.integration.scenic.client: DEBUG com.ycwl.basic.integration.scenic.client: DEBUG
zhipu:
api-key: a331e0fcf3f74518818b8e5129b79058.RXuUxUUjKdcxbF4L

View File

@@ -1,11 +1,17 @@
server: server:
port: 8031 port: 8031
shutdown: graceful
spring: spring:
application: application:
name: zt name: zt
lifecycle:
timeout-per-shutdown-phase: 60s
# 生产环境日志级别 # 生产环境日志级别
logging: logging:
level: level:
com.ycwl.basic.integration.scenic.client: WARN com.ycwl.basic.integration.scenic.client: WARN
zhipu:
api-key: a331e0fcf3f74518818b8e5129b79058.RXuUxUUjKdcxbF4L

View File

@@ -46,6 +46,82 @@
</encoder> </encoder>
</appender> </appender>
<!-- Task specific log -->
<appender name="TASK_LOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
<File>logs/task.log</File>
<append>true</append>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>logs/task.%d.%i.log</fileNamePattern>
<maxHistory>30</maxHistory>
<totalSizeCap>5GB</totalSizeCap>
<maxFileSize>10MB</maxFileSize>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level --- [%15.15(%thread)] %-40.40(%logger{40}) : %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- FaceProcessingKafkaService specific log -->
<appender name="FACE_PROCESSING_KAFKA_SERVICE_LOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
<File>logs/face_processing_kafka_service.log</File>
<append>true</append>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>logs/face_processing_kafka_service.%d.%i.log</fileNamePattern>
<maxHistory>30</maxHistory>
<totalSizeCap>5GB</totalSizeCap>
<maxFileSize>10MB</maxFileSize>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level --- [%15.15(%thread)] %-40.40(%logger{40}) : %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- DeviceStorageOperator specific log -->
<appender name="DEVICE_STORAGE_OPERATOR_LOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
<File>logs/device_storage_operator.log</File>
<append>true</append>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>logs/device_storage_operator.%d.%i.log</fileNamePattern>
<maxHistory>30</maxHistory>
<totalSizeCap>5GB</totalSizeCap>
<maxFileSize>10MB</maxFileSize>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level --- [%15.15(%thread)] %-40.40(%logger{40}) : %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<logger name="com.ycwl.basic.device.operator" level="INFO" additivity="false">
<appender-ref ref="DEVICE_STORAGE_OPERATOR_LOG"/>
</logger>
<logger name="com.ycwl.basic.task.DeviceVideoContinuityCheckTask" level="INFO" additivity="false">
<appender-ref ref="TASK_LOG"/>
</logger>
<logger name="com.ycwl.basic.task.FaceCleaner" level="INFO" additivity="false">
<appender-ref ref="TASK_LOG"/>
</logger>
<logger name="com.ycwl.basic.task.VideoPieceCleaner" level="INFO" additivity="false">
<appender-ref ref="TASK_LOG"/>
</logger>
<logger name="com.ycwl.basic.task.DynamicTaskGenerator" level="INFO" additivity="false">
<appender-ref ref="TASK_LOG"/>
</logger>
<logger name="com.ycwl.basic.task.DownloadNotificationTasker" level="INFO" additivity="false">
<appender-ref ref="TASK_LOG"/>
</logger>
<logger name="com.ycwl.basic.integration.kafka.service.FaceProcessingKafkaService" level="INFO" additivity="false">
<appender-ref ref="FACE_PROCESSING_KAFKA_SERVICE_LOG"/>
</logger>
<root level="ERROR"> <root level="ERROR">
<appender-ref ref="error_log" /> <appender-ref ref="error_log" />
<appender-ref ref="STDOUT" /> <appender-ref ref="STDOUT" />

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ycwl.basic.mapper.FaceChatConversationMapper">
<resultMap id="BaseResultMap" type="com.ycwl.basic.model.mobile.chat.entity.FaceChatConversationEntity">
<id column="id" property="id"/>
<result column="face_id" property="faceId"/>
<result column="member_id" property="memberId"/>
<result column="status" property="status"/>
<result column="model" property="model"/>
<result column="created_at" property="createdAt"/>
<result column="updated_at" property="updatedAt"/>
</resultMap>
<select id="findByFaceId" resultMap="BaseResultMap">
select id, face_id, member_id, status, model, created_at, updated_at
from face_chat_conversation
where face_id = #{faceId}
limit 1
</select>
<select id="getById" resultMap="BaseResultMap">
select id, face_id, member_id, status, model, created_at, updated_at
from face_chat_conversation
where id = #{id}
limit 1
</select>
<insert id="insert">
insert into face_chat_conversation
(id, face_id, member_id, status, model, created_at, updated_at)
values
(#{id}, #{faceId}, #{memberId}, #{status}, #{model}, now(), now())
</insert>
<update id="updateStatus">
update face_chat_conversation
set status = #{status}, updated_at = now()
where id = #{id}
</update>
</mapper>

View File

@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ycwl.basic.mapper.FaceChatMessageMapper">
<resultMap id="BaseResultMap" type="com.ycwl.basic.model.mobile.chat.entity.FaceChatMessageEntity">
<id column="id" property="id"/>
<result column="conversation_id" property="conversationId"/>
<result column="face_id" property="faceId"/>
<result column="seq" property="seq"/>
<result column="role" property="role"/>
<result column="content" property="content"/>
<result column="trace_id" property="traceId"/>
<result column="latency_ms" property="latencyMs"/>
<result column="created_at" property="createdAt"/>
</resultMap>
<select id="maxSeqForUpdate" resultType="java.lang.Integer">
select ifnull(max(seq), 0)
from face_chat_message
where conversation_id = #{conversationId}
for update
</select>
<insert id="insert">
insert into face_chat_message
(id, conversation_id, face_id, seq, role, content, trace_id, latency_ms, created_at)
values
(#{id}, #{conversationId}, #{faceId}, #{seq}, #{role}, #{content}, #{traceId}, #{latencyMs}, now())
</insert>
<select id="listByConversation" resultMap="BaseResultMap">
select id, conversation_id, face_id, seq, role, content, trace_id, latency_ms, created_at
from face_chat_message
where conversation_id = #{conversationId}
<if test="cursor != null">
and seq &gt; #{cursor}
</if>
order by seq asc
<if test="limit != null">
limit #{limit}
</if>
</select>
<select id="listRecentByConversation" resultMap="BaseResultMap">
select id, conversation_id, face_id, seq, role, content, trace_id, latency_ms, created_at
from face_chat_message
where conversation_id = #{conversationId}
order by seq desc
<if test="limit != null">
limit #{limit}
</if>
</select>
</mapper>

View File

@@ -2,7 +2,16 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ycwl.basic.mapper.FaceDetectLogAiCamMapper"> <mapper namespace="com.ycwl.basic.mapper.FaceDetectLogAiCamMapper">
<insert id="add" useGeneratedKeys="true" keyProperty="id"> <insert id="add" useGeneratedKeys="true" keyProperty="id">
insert into face_detect_log_ai_cam(scenic_id, device_id, face_sample_id, db_name, face_url, score, match_raw_result, create_time) insert into face_detect_log_ai_cam(scenic_id, device_id, face_id, db_name, face_url, score, match_raw_result, create_time)
values (#{scenicId}, #{deviceId}, #{faceSampleId}, #{dbName}, #{faceUrl}, #{score}, #{matchRawResult}, #{createTime}) values (#{scenicId}, #{deviceId}, #{faceId}, #{dbName}, #{faceUrl}, #{score}, #{matchRawResult}, #{createTime})
</insert> </insert>
<select id="listByFaceId" resultType="com.ycwl.basic.model.pc.faceDetectLog.entity.FaceDetectLogAiCamEntity">
select id, scenic_id as scenicId, device_id as deviceId, face_id as faceId,
db_name as dbName, face_url as faceUrl, score, match_raw_result as matchRawResult,
create_time as createTime
from face_detect_log_ai_cam
where face_id = #{faceId}
order by create_time desc
</select>
</mapper> </mapper>

View File

@@ -95,6 +95,13 @@
LEFT JOIN face f ON ms.face_id = f.id LEFT JOIN face f ON ms.face_id = f.id
LEFT JOIN source s ON ms.source_id = s.id LEFT JOIN source s ON ms.source_id = s.id
WHERE s.id IS NOT NULL WHERE s.id IS NOT NULL
),
member_source_aicam_data AS (
SELECT ms.member_id, ms.source_id, ms.face_id, f.face_url, s.url
FROM member_source ms
LEFT JOIN face f ON ms.face_id = f.id
LEFT JOIN source s ON ms.source_id = s.id
WHERE s.id IS NOT NULL AND ms.type = 3
), ),
member_photo_data AS ( member_photo_data AS (
SELECT mp.member_id, 3 as type, mp.id, mp.crop_url as url, mp.quantity, mp.status, mp.create_time SELECT mp.member_id, 3 as type, mp.id, mp.crop_url as url, mp.quantity, mp.status, mp.create_time
@@ -105,8 +112,9 @@
FROM source s FROM source s
), ),
member_plog_data AS ( member_plog_data AS (
SELECT 5 as type, gr.template_id as id, gr.result_image_url as url, gr.face_id SELECT 5 as type, gr.template_id as id, pt.scenic_id as scenic_id, gr.result_image_url as url, gr.face_id
FROM puzzle_generation_record gr FROM puzzle_generation_record gr
left join puzzle_template pt on gr.template_id = pt.id
) )
SELECT SELECT
oi.id AS oiId, oi.id AS oiId,
@@ -127,17 +135,20 @@
WHEN '3' THEN '照片打印' WHEN '3' THEN '照片打印'
WHEN '4' THEN '一体机照片打印' WHEN '4' THEN '一体机照片打印'
WHEN '5' THEN 'pLog' WHEN '5' THEN 'pLog'
WHEN '13' THEN '打卡点拍照'
ELSE '其他' ELSE '其他'
END AS goods_name, END AS goods_name,
CASE oi.goods_type CASE oi.goods_type
WHEN '0' THEN mvd.face_id WHEN '0' THEN mvd.face_id
WHEN '1' THEN oi.goods_id WHEN '1' THEN oi.goods_id
WHEN '2' THEN oi.goods_id WHEN '2' THEN oi.goods_id
WHEN '13' THEN oi.goods_id
END AS face_id, END AS face_id,
CASE oi.goods_type CASE oi.goods_type
WHEN '0' THEN mvd.face_url WHEN '0' THEN mvd.face_url
WHEN '1' THEN msd.face_url WHEN '1' THEN msd.face_url
WHEN '2' THEN msd.face_url WHEN '2' THEN msd.face_url
WHEN '13' THEN msac.face_url
END AS face_url, END AS face_url,
CASE oi.goods_type CASE oi.goods_type
WHEN '0' THEN mvd.video_url WHEN '0' THEN mvd.video_url
@@ -149,14 +160,16 @@
WHEN '3' THEN mpd.url WHEN '3' THEN mpd.url
WHEN '4' THEN mpa.url WHEN '4' THEN mpa.url
WHEN '5' THEN mpl.url WHEN '5' THEN mpl.url
WHEN '13' THEN msac.url
END AS imgUrl END AS imgUrl
FROM order_item oi FROM order_item oi
LEFT JOIN `order` o ON oi.order_id = o.id LEFT JOIN `order` o ON oi.order_id = o.id
LEFT JOIN member_video_data mvd ON o.face_id = mvd.face_id AND oi.goods_id = mvd.video_id LEFT JOIN member_video_data mvd ON o.face_id = mvd.face_id AND oi.goods_id = mvd.video_id
LEFT JOIN member_source_data msd ON o.face_id = msd.face_id AND oi.goods_id = msd.face_id AND msd.type = oi.goods_type LEFT JOIN member_source_data msd ON o.face_id = msd.face_id AND oi.goods_id = msd.face_id AND msd.type = oi.goods_type
LEFT JOIN member_source_aicam_data msac ON o.face_id = msac.face_id AND oi.goods_id = msac.face_id AND oi.goods_type = 13
LEFT JOIN member_photo_data mpd ON oi.goods_id = mpd.id AND mpd.type = oi.goods_type LEFT JOIN member_photo_data mpd ON oi.goods_id = mpd.id AND mpd.type = oi.goods_type
LEFT JOIN member_aio_photo_data mpa ON oi.goods_id = mpa.id AND mpa.type = oi.goods_type LEFT JOIN member_aio_photo_data mpa ON oi.goods_id = mpa.id AND mpa.type = oi.goods_type
LEFT JOIN member_plog_data mpl ON oi.goods_id = mpl.id AND mpl.type = oi.goods_type AND o.face_id = mpl.face_id LEFT JOIN member_plog_data mpl ON (oi.goods_id = mpl.id OR oi.goods_id = mpl.scenic_id) AND mpl.type = oi.goods_type AND o.face_id = mpl.face_id
WHERE oi.order_id = #{id}; WHERE oi.order_id = #{id};
</select> </select>

View File

@@ -116,6 +116,7 @@
source_id, source_id,
orig_url, orig_url,
crop_url, crop_url,
crop,
quantity, quantity,
status, status,
create_time, create_time,
@@ -127,6 +128,7 @@
#{sourceId}, #{sourceId},
#{origUrl}, #{origUrl},
#{cropUrl}, #{cropUrl},
#{crop},
1, 1,
0, 0,
NOW(), NOW(),

View File

@@ -198,7 +198,7 @@
where so.id = #{id} and ms.member_id = #{userId} and so.id is not null where so.id = #{id} and ms.member_id = #{userId} and so.id is not null
</select> </select>
<select id="getById" resultType="com.ycwl.basic.model.pc.source.resp.SourceRespVO"> <select id="getById" resultType="com.ycwl.basic.model.pc.source.resp.SourceRespVO">
select so.id, scenic_id, device_id, thumb_url, url, video_url, so.create_time, so.update_time select so.id, scenic_id, device_id, thumb_url, type, url, video_url, so.create_time, so.update_time
from source so from source so
where so.id = #{id} where so.id = #{id}
@@ -348,6 +348,12 @@
left join source s on ms.source_id = s.id left join source s on ms.source_id = s.id
where ms.face_id = #{faceId} and ms.type = 2 where ms.face_id = #{faceId} and ms.type = 2
</select> </select>
<select id="listAiCamImageByFaceRelation" resultType="com.ycwl.basic.model.pc.source.entity.SourceEntity">
select s.*, ms.is_buy
from member_source ms
left join source s on ms.source_id = s.id
where ms.face_id = #{faceId} and ms.type = 3
</select>
<select id="getEntity" resultType="com.ycwl.basic.model.pc.source.entity.SourceEntity"> <select id="getEntity" resultType="com.ycwl.basic.model.pc.source.entity.SourceEntity">
select * select *
from source from source
@@ -486,4 +492,19 @@
</choose> </choose>
LIMIT 1 LIMIT 1
</select> </select>
<select id="listByFaceSampleIdsAndType" resultType="com.ycwl.basic.model.pc.source.entity.SourceEntity">
SELECT * FROM source
WHERE face_sample_id IN
<foreach collection="faceSampleIds" item="item" open="(" separator="," close=")">
#{item}
</foreach>
AND `type` = #{type}
ORDER BY create_time DESC
</select>
<delete id="deleteRelationsByFaceIdAndType">
DELETE FROM member_source
WHERE face_id = #{faceId} AND `type` = #{type}
</delete>
</mapper> </mapper>