You've already forked FrameTour-BE
Compare commits
21 Commits
b2012f9209
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 47ae60b203 | |||
| 703a5baf13 | |||
| 7454111615 | |||
| 39c955b55c | |||
| 9a18ffc167 | |||
| 062a128dcc | |||
| f9c776b3ab | |||
| e5eea4c349 | |||
| 0484c8077d | |||
| 6a22fc87a7 | |||
| b01056d829 | |||
| 09d142aa98 | |||
| 143185926c | |||
| cbbdd02003 | |||
| 1110b5409b | |||
| 4ac59b1f31 | |||
| 671cad4687 | |||
| 90fb0df69c | |||
| 383f9c4a31 | |||
| 9a92a4943a | |||
| 959eb6077e |
@@ -249,6 +249,9 @@ public class OrderBiz {
|
||||
case 13: // AI微单
|
||||
sourceRepository.setUserIsBuyItem(order.getMemberId(), item.getGoodsType(), item.getGoodsId(), order.getId());
|
||||
break;
|
||||
case 14: // 单张照片
|
||||
sourceRepository.setUserIsBuyItemBySourceId(order.getMemberId(), item.getGoodsId(), order.getFaceId(), order.getId());
|
||||
break;
|
||||
case 3:
|
||||
printerService.setUserIsBuyItem(order.getMemberId(), item.getGoodsId(), order.getId());
|
||||
break;
|
||||
@@ -287,6 +290,9 @@ public class OrderBiz {
|
||||
case 2: // 照片原素材
|
||||
sourceRepository.setUserNotBuyItem(order.getMemberId(), item.getGoodsType(), item.getGoodsId());
|
||||
break;
|
||||
case 14: // 单张照片
|
||||
sourceRepository.setUserNotBuyItemBySourceId(order.getMemberId(), item.getGoodsId(), order.getFaceId());
|
||||
break;
|
||||
}
|
||||
});
|
||||
orderRepository.clearOrderCache(orderId); // 更新完了,清理下
|
||||
@@ -311,6 +317,9 @@ public class OrderBiz {
|
||||
case 2: // 照片原素材
|
||||
sourceRepository.setUserNotBuyItem(order.getMemberId(), item.getGoodsType(), item.getGoodsId());
|
||||
break;
|
||||
case 14: // 单张照片
|
||||
sourceRepository.setUserNotBuyItemBySourceId(order.getMemberId(), item.getGoodsId(), order.getFaceId());
|
||||
break;
|
||||
}
|
||||
});
|
||||
orderRepository.clearOrderCache(orderId); // 更新完了,清理下
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.github.pagehelper.PageInfo;
|
||||
import com.ycwl.basic.constant.BaseContextHandler;
|
||||
import com.ycwl.basic.mapper.SourceMapper;
|
||||
import com.ycwl.basic.mapper.VideoMapper;
|
||||
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
|
||||
import com.ycwl.basic.model.pc.source.req.SourceReqQuery;
|
||||
import com.ycwl.basic.model.pc.task.entity.TaskEntity;
|
||||
import com.ycwl.basic.model.pc.video.entity.MemberVideoEntity;
|
||||
@@ -95,6 +96,12 @@ public class AppOrderV2Controller {
|
||||
request.setFaceId(video.getFaceId());
|
||||
}
|
||||
case RECORDING_SET, PHOTO_SET, AI_CAM_PHOTO_SET -> request.setFaceId(Long.valueOf(productItem.getProductId()));
|
||||
case PHOTO -> {
|
||||
MemberSourceEntity ms = sourceMapper.getMemberSourceByMemberAndSourceId(currentUserId, Long.valueOf(productItem.getProductId()));
|
||||
if (ms != null) {
|
||||
request.setFaceId(ms.getFaceId());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,6 +148,9 @@ public class AppOrderV2Controller {
|
||||
Integer _count = sourceMapper.countUser(aiPhotoSetReqQuery);
|
||||
product.setQuantity(_count);
|
||||
break;
|
||||
case PHOTO:
|
||||
product.setQuantity(1);
|
||||
break;
|
||||
default:
|
||||
log.warn("未知的商品类型,跳过重复购买检查: productType={}", product.getProductType());
|
||||
break;
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
package com.ycwl.basic.controller.mobile;
|
||||
|
||||
import com.ycwl.basic.constant.BaseContextHandler;
|
||||
import com.ycwl.basic.exception.BaseException;
|
||||
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
||||
import com.ycwl.basic.pricing.dto.req.VoucherClaimReq;
|
||||
import com.ycwl.basic.pricing.dto.req.VoucherPrintReq;
|
||||
import com.ycwl.basic.pricing.dto.resp.VoucherCodeResp;
|
||||
import com.ycwl.basic.pricing.dto.resp.VoucherPrintResp;
|
||||
import com.ycwl.basic.pricing.service.VoucherCodeService;
|
||||
import com.ycwl.basic.pricing.service.VoucherPrintService;
|
||||
import com.ycwl.basic.repository.FaceRepository;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -30,8 +27,6 @@ public class AppVoucherController {
|
||||
private VoucherPrintService voucherPrintService;
|
||||
@Autowired
|
||||
private VoucherCodeService voucherCodeService;
|
||||
@Autowired
|
||||
private FaceRepository faceRepository;
|
||||
|
||||
/**
|
||||
* 打印小票
|
||||
@@ -60,11 +55,6 @@ public class AppVoucherController {
|
||||
|
||||
@PostMapping("/claim")
|
||||
public ApiResponse<VoucherCodeResp> claimVoucher(@RequestBody VoucherClaimReq req) {
|
||||
FaceEntity face = faceRepository.getFace(req.getFaceId());
|
||||
if (face == null) {
|
||||
throw new BaseException("请选择人脸");
|
||||
}
|
||||
req.setScenicId(face.getScenicId());
|
||||
VoucherCodeResp result = voucherCodeService.claimVoucher(req);
|
||||
return ApiResponse.success(result);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
package com.ycwl.basic.controller.pc;
|
||||
|
||||
import com.ycwl.basic.annotation.IgnoreToken;
|
||||
import com.ycwl.basic.mapper.FaceSampleAssociationMapper;
|
||||
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleAssociationEntity;
|
||||
import com.ycwl.basic.model.pc.faceSample.req.FaceSampleAssociationReq;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 人脸样本关联分组管理
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/faceSampleAssociation/v1")
|
||||
@IgnoreToken
|
||||
public class FaceSampleAssociationController {
|
||||
|
||||
@Autowired
|
||||
private FaceSampleAssociationMapper faceSampleAssociationMapper;
|
||||
|
||||
/**
|
||||
* 添加关联:将一批 faceSampleId 加入指定的 groupKey
|
||||
*/
|
||||
@PostMapping("/add")
|
||||
public ApiResponse<String> add(@RequestBody FaceSampleAssociationReq req) {
|
||||
if (req.getScenicId() == null || req.getGroupKey() == null || req.getGroupKey().isBlank()) {
|
||||
return ApiResponse.fail("scenicId 和 groupKey 不能为空");
|
||||
}
|
||||
if (req.getFaceSampleIds() == null || req.getFaceSampleIds().isEmpty()) {
|
||||
return ApiResponse.fail("faceSampleIds 不能为空");
|
||||
}
|
||||
|
||||
List<FaceSampleAssociationEntity> entities = req.getFaceSampleIds().stream()
|
||||
.map(sampleId -> {
|
||||
FaceSampleAssociationEntity entity = new FaceSampleAssociationEntity();
|
||||
entity.setScenicId(req.getScenicId());
|
||||
entity.setGroupKey(req.getGroupKey());
|
||||
entity.setFaceSampleId(sampleId);
|
||||
return entity;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
faceSampleAssociationMapper.batchInsertIgnore(entities);
|
||||
log.info("添加人脸样本关联: scenicId={}, groupKey={}, count={}",
|
||||
req.getScenicId(), req.getGroupKey(), entities.size());
|
||||
return ApiResponse.success("添加成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除关联:从指定 groupKey 中移除一批 faceSampleId
|
||||
*/
|
||||
@PostMapping("/remove")
|
||||
public ApiResponse<String> remove(@RequestBody FaceSampleAssociationReq req) {
|
||||
if (req.getScenicId() == null || req.getGroupKey() == null || req.getGroupKey().isBlank()) {
|
||||
return ApiResponse.fail("scenicId 和 groupKey 不能为空");
|
||||
}
|
||||
if (req.getFaceSampleIds() == null || req.getFaceSampleIds().isEmpty()) {
|
||||
return ApiResponse.fail("faceSampleIds 不能为空");
|
||||
}
|
||||
|
||||
faceSampleAssociationMapper.deleteByGroupAndSampleIds(
|
||||
req.getScenicId(), req.getGroupKey(), req.getFaceSampleIds());
|
||||
log.info("移除人脸样本关联: scenicId={}, groupKey={}, count={}",
|
||||
req.getScenicId(), req.getGroupKey(), req.getFaceSampleIds().size());
|
||||
return ApiResponse.success("移除成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除整个组
|
||||
*/
|
||||
@PostMapping("/deleteGroup")
|
||||
public ApiResponse<String> deleteGroup(@RequestParam Long scenicId, @RequestParam String groupKey) {
|
||||
if (scenicId == null || groupKey == null || groupKey.isBlank()) {
|
||||
return ApiResponse.fail("scenicId 和 groupKey 不能为空");
|
||||
}
|
||||
|
||||
faceSampleAssociationMapper.deleteByGroup(scenicId, groupKey);
|
||||
log.info("删除关联组: scenicId={}, groupKey={}", scenicId, groupKey);
|
||||
return ApiResponse.success("删除成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询指定组的所有样本ID
|
||||
*/
|
||||
@GetMapping("/listByGroup")
|
||||
public ApiResponse<List<Long>> listByGroup(@RequestParam Long scenicId, @RequestParam String groupKey) {
|
||||
List<Long> sampleIds = faceSampleAssociationMapper.listSampleIdsByGroup(scenicId, groupKey);
|
||||
return ApiResponse.success(sampleIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询指定景区下的所有分组及其样本ID
|
||||
*/
|
||||
@GetMapping("/listGroups")
|
||||
public ApiResponse<Map<String, List<Long>>> listGroups(@RequestParam Long scenicId) {
|
||||
List<String> groupKeys = faceSampleAssociationMapper.listGroupKeysByScenicId(scenicId);
|
||||
Map<String, List<Long>> result = groupKeys.stream()
|
||||
.collect(Collectors.toMap(
|
||||
groupKey -> groupKey,
|
||||
groupKey -> faceSampleAssociationMapper.listSampleIdsByGroup(scenicId, groupKey)
|
||||
));
|
||||
return ApiResponse.success(result);
|
||||
}
|
||||
}
|
||||
@@ -62,9 +62,12 @@ public class SourceController {
|
||||
*/
|
||||
@PostMapping("/createVirtualOrder")
|
||||
public ApiResponse<Map<String, Object>> createVirtualOrder(@RequestBody CreateVirtualOrderRequest request) {
|
||||
if (request.getSourceIds() == null || request.getSourceIds().isEmpty()) {
|
||||
return ApiResponse.fail("sourceIds不能为空");
|
||||
}
|
||||
try {
|
||||
Map<String, Object> result = printerService.createVirtualOrder(
|
||||
request.getSourceId(),
|
||||
Map<String, Object> result = printerService.createBatchVirtualOrder(
|
||||
request.getSourceIds(),
|
||||
request.getScenicId(),
|
||||
request.getPrinterId(),
|
||||
request.getNeedEnhance(),
|
||||
|
||||
@@ -9,12 +9,17 @@ import com.ycwl.basic.model.mobile.face.FaceRecognizeResp;
|
||||
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
||||
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
|
||||
import com.ycwl.basic.model.pc.mp.MpConfigEntity;
|
||||
import com.ycwl.basic.model.pc.printer.resp.PrinterResp;
|
||||
import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery;
|
||||
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
|
||||
import com.ycwl.basic.model.printer.req.TvCreateVirtualOrderRequest;
|
||||
import com.ycwl.basic.pay.entity.PayResponse;
|
||||
import com.ycwl.basic.repository.DeviceRepository;
|
||||
import com.ycwl.basic.repository.FaceRepository;
|
||||
import com.ycwl.basic.repository.ScenicRepository;
|
||||
import com.ycwl.basic.service.pc.FaceService;
|
||||
import com.ycwl.basic.service.pc.OrderService;
|
||||
import com.ycwl.basic.service.printer.PrinterService;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import com.ycwl.basic.utils.WxMpUtil;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
@@ -22,6 +27,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
@@ -31,6 +37,7 @@ import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@IgnoreToken
|
||||
// 打印机大屏对接接口
|
||||
@@ -44,6 +51,8 @@ public class PrinterTvController {
|
||||
private final FaceRepository faceRepository;
|
||||
private final FaceService pcFaceService;
|
||||
private final SourceMapper sourceMapper;
|
||||
private final PrinterService printerService;
|
||||
private final OrderService orderService;
|
||||
|
||||
/**
|
||||
* 获取景区列表
|
||||
@@ -191,4 +200,58 @@ public class PrinterTvController {
|
||||
response.sendRedirect(face.getFaceUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取景区下的打印机列表
|
||||
*
|
||||
* @param scenicId 景区ID
|
||||
* @return 启用状态的打印机列表
|
||||
*/
|
||||
@GetMapping("/printer/list")
|
||||
public ApiResponse<List<PrinterResp>> getPrinterListByScenicId(@RequestParam Long scenicId) {
|
||||
return ApiResponse.success(printerService.listByScenicId(scenicId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量创建虚拟用户订单
|
||||
* 传入faceSampleIds,自动查找关联的照片素材(type=2),聚合为一笔订单、一次支付
|
||||
*
|
||||
* @param request 请求参数(含faceSampleIds列表)
|
||||
* @return 聚合订单结果
|
||||
*/
|
||||
@PostMapping("/createVirtualOrder")
|
||||
public ApiResponse<Map<String, Object>> createVirtualOrder(@RequestBody TvCreateVirtualOrderRequest request) {
|
||||
if (request.getFaceSampleIds() == null || request.getFaceSampleIds().isEmpty()) {
|
||||
return ApiResponse.fail("faceSampleIds不能为空");
|
||||
}
|
||||
try {
|
||||
List<SourceEntity> sources = sourceMapper.listByFaceSampleIdsAndType(request.getFaceSampleIds(), 2);
|
||||
if (sources.isEmpty()) {
|
||||
return ApiResponse.fail("未找到关联的照片素材");
|
||||
}
|
||||
List<Long> sourceIds = sources.stream().map(SourceEntity::getId).toList();
|
||||
Map<String, Object> result = printerService.createBatchVirtualOrder(
|
||||
sourceIds,
|
||||
request.getScenicId(),
|
||||
request.getPrinterId(),
|
||||
request.getNeedEnhance(),
|
||||
request.getPrintImgUrl(),
|
||||
request.getNeedActualPayment()
|
||||
);
|
||||
return ApiResponse.success(result);
|
||||
} catch (Exception e) {
|
||||
return ApiResponse.fail(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询订单支付状态
|
||||
*
|
||||
* @param orderId 订单ID
|
||||
* @return 支付状态信息
|
||||
*/
|
||||
@GetMapping("/order/query")
|
||||
public ApiResponse<PayResponse> queryOrder(@RequestParam("orderId") Long orderId) {
|
||||
return ApiResponse.success(orderService.queryOrder(orderId));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -89,6 +89,12 @@ public class FaceMatchingContext implements PipelineContext {
|
||||
*/
|
||||
private List<Long> freeSourceIds;
|
||||
|
||||
/**
|
||||
* 关联扩展的样本ID列表(由 ExpandSampleAssociationStage 设置)
|
||||
* 用于标识哪些 sampleId 是通过关联分组扩展得到的,而非直接匹配
|
||||
*/
|
||||
private List<Long> associatedSampleIds;
|
||||
|
||||
/**
|
||||
* 人脸选择后置模式配置(自定义匹配场景)
|
||||
* 0: 并集, 1: 交集, 2: 直接使用
|
||||
|
||||
@@ -66,6 +66,8 @@ public class FaceMatchingPipelineFactory {
|
||||
private FilterByDevicePhotoLimitStage filterByDevicePhotoLimitStage;
|
||||
@Autowired
|
||||
private DeleteOldRelationsStage deleteOldRelationsStage;
|
||||
@Autowired
|
||||
private ExpandSampleAssociationStage expandSampleAssociationStage;
|
||||
|
||||
// ==================== 辅助服务 ====================
|
||||
@Autowired
|
||||
@@ -91,13 +93,16 @@ public class FaceMatchingPipelineFactory {
|
||||
// 3. 人脸识别补救
|
||||
builder.addStage(faceRecoveryStage);
|
||||
|
||||
// 4. 更新人脸结果(落库)
|
||||
// 4. 关联样本扩展
|
||||
builder.addStage(expandSampleAssociationStage);
|
||||
|
||||
// 5. 更新人脸结果(落库)
|
||||
builder.addStage(updateFaceResultStage);
|
||||
|
||||
// 5. 构建源文件关联(建关系)
|
||||
// 6. 构建源文件关联(建关系)
|
||||
builder.addStage(buildSourceRelationStage);
|
||||
|
||||
// 6. 持久化关联关系(建关系)
|
||||
// 7. 持久化关联关系(建关系)
|
||||
builder.addStage(persistRelationsStage);
|
||||
|
||||
log.debug("创建打印机大屏识别试点Pipeline: stageCount={}", builder.build().getStageCount());
|
||||
@@ -131,28 +136,31 @@ public class FaceMatchingPipelineFactory {
|
||||
// 5. 人脸识别补救
|
||||
builder.addStage(faceRecoveryStage);
|
||||
|
||||
// 6. 更新人脸结果
|
||||
// 6. 关联样本扩展
|
||||
builder.addStage(expandSampleAssociationStage);
|
||||
|
||||
// 7. 更新人脸结果
|
||||
builder.addStage(updateFaceResultStage);
|
||||
|
||||
// 7. 构建源文件关联
|
||||
// 8. 构建源文件关联
|
||||
builder.addStage(buildSourceRelationStage);
|
||||
|
||||
// 8. 处理免费源文件逻辑
|
||||
// 9. 处理免费源文件逻辑
|
||||
builder.addStage(processFreeSourceStage);
|
||||
|
||||
// 9. 处理购买状态
|
||||
// 10. 处理购买状态
|
||||
builder.addStage(processBuyStatusStage);
|
||||
|
||||
// 10. 处理视频重切
|
||||
// 11. 处理视频重切
|
||||
builder.addStage(handleVideoRecreationStage);
|
||||
|
||||
// 11. 持久化关联关系
|
||||
// 12. 持久化关联关系
|
||||
builder.addStage(persistRelationsStage);
|
||||
|
||||
// 12. 创建任务
|
||||
// 13. 创建任务
|
||||
builder.addStage(createTaskStage);
|
||||
|
||||
// 13. 异步生成拼图模板
|
||||
// 14. 异步生成拼图模板
|
||||
builder.addStage(generatePuzzleStage);
|
||||
|
||||
log.debug("创建自动人脸匹配Pipeline: isNew={}, stageCount={}", isNew, builder.build().getStageCount());
|
||||
@@ -189,28 +197,31 @@ public class FaceMatchingPipelineFactory {
|
||||
// 7. 应用设备照片数量限制筛选
|
||||
builder.addStage(filterByDevicePhotoLimitStage);
|
||||
|
||||
// 8. 更新人脸结果
|
||||
// 8. 关联样本扩展
|
||||
builder.addStage(expandSampleAssociationStage);
|
||||
|
||||
// 9. 更新人脸结果
|
||||
builder.addStage(updateFaceResultStage);
|
||||
|
||||
// 9. 删除旧关系数据
|
||||
// 10. 删除旧关系数据
|
||||
builder.addStage(deleteOldRelationsStage);
|
||||
|
||||
// 10. 构建源文件关联
|
||||
// 11. 构建源文件关联
|
||||
builder.addStage(buildSourceRelationStage);
|
||||
|
||||
// 11. 处理免费源文件逻辑
|
||||
// 12. 处理免费源文件逻辑
|
||||
builder.addStage(processFreeSourceStage);
|
||||
|
||||
// 12. 处理购买状态
|
||||
// 13. 处理购买状态
|
||||
builder.addStage(processBuyStatusStage);
|
||||
|
||||
// 13. 处理视频重切
|
||||
// 14. 处理视频重切
|
||||
builder.addStage(handleVideoRecreationStage);
|
||||
|
||||
// 14. 持久化关联关系
|
||||
// 15. 持久化关联关系
|
||||
builder.addStage(persistRelationsStage);
|
||||
|
||||
// 15. 创建任务
|
||||
// 16. 创建任务
|
||||
builder.addStage(createTaskStage);
|
||||
|
||||
log.debug("创建自定义人脸匹配Pipeline: stageCount={}", builder.build().getStageCount());
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.ycwl.basic.face.pipeline.helper;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO;
|
||||
import com.ycwl.basic.mapper.SourceMapper;
|
||||
import com.ycwl.basic.puzzle.dto.PuzzleGenerateRequest;
|
||||
import com.ycwl.basic.puzzle.dto.PuzzleGenerateResponse;
|
||||
import com.ycwl.basic.puzzle.dto.PuzzleTemplateDTO;
|
||||
@@ -44,6 +45,9 @@ public class PuzzleGenerationOrchestrator {
|
||||
@Autowired
|
||||
private ScenicRepository scenicRepository;
|
||||
|
||||
@Autowired
|
||||
private SourceMapper sourceMapper;
|
||||
|
||||
/**
|
||||
* 异步生成景区所有启用的拼图模板
|
||||
*
|
||||
@@ -74,6 +78,11 @@ public class PuzzleGenerationOrchestrator {
|
||||
// 3. 准备公共动态数据
|
||||
Map<String, String> baseDynamicData = buildBaseDynamicData(faceId, faceUrl, scenicBasic);
|
||||
|
||||
// 4. 设置dateStr为图片素材的最大创建时间
|
||||
Date maxCreateTime = sourceMapper.getMaxCreateTimeByFaceId(faceId);
|
||||
baseDynamicData.put("dateStr", DateUtil.format(
|
||||
maxCreateTime != null ? maxCreateTime : new Date(), "yyyy.MM.dd"));
|
||||
|
||||
// 4. 使用虚拟线程池并行生成所有模板
|
||||
java.util.concurrent.atomic.AtomicInteger successCount = new java.util.concurrent.atomic.AtomicInteger(0);
|
||||
java.util.concurrent.atomic.AtomicInteger failCount = new java.util.concurrent.atomic.AtomicInteger(0);
|
||||
@@ -121,7 +130,6 @@ public class PuzzleGenerationOrchestrator {
|
||||
baseDynamicData.put("faceId", String.valueOf(faceId));
|
||||
baseDynamicData.put("scenicName", scenicBasic.getName());
|
||||
baseDynamicData.put("scenicText", scenicBasic.getName());
|
||||
baseDynamicData.put("dateStr", DateUtil.format(new Date(), "yyyy.MM.dd"));
|
||||
|
||||
return baseDynamicData;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
package com.ycwl.basic.face.pipeline.stages;
|
||||
|
||||
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||
import com.ycwl.basic.mapper.FaceSampleAssociationMapper;
|
||||
import com.ycwl.basic.model.task.resp.SearchFaceRespVo;
|
||||
import com.ycwl.basic.pipeline.annotation.StageConfig;
|
||||
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
|
||||
import com.ycwl.basic.pipeline.core.StageResult;
|
||||
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* 根据人脸样本关联分组扩展匹配结果
|
||||
* <p>
|
||||
* 当匹配结果中包含某分组的任一 faceSampleId 时,
|
||||
* 将该分组内所有 faceSampleId 加入匹配结果。
|
||||
* 关联扩展的样本ID跳过时间范围和设备限制筛选(通过Stage执行顺序保证)。
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@StageConfig(
|
||||
stageId = "expand_sample_association",
|
||||
optionalMode = StageOptionalMode.SUPPORT,
|
||||
description = "根据人脸样本关联分组扩展匹配结果",
|
||||
defaultEnabled = true
|
||||
)
|
||||
public class ExpandSampleAssociationStage extends AbstractPipelineStage<FaceMatchingContext> {
|
||||
|
||||
@Autowired
|
||||
private FaceSampleAssociationMapper faceSampleAssociationMapper;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "ExpandSampleAssociation";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldExecuteByBusinessLogic(FaceMatchingContext context) {
|
||||
SearchFaceRespVo searchResult = context.getSearchResult();
|
||||
if (searchResult == null || searchResult.getSampleListIds() == null
|
||||
|| searchResult.getSampleListIds().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return context.getFace() != null && context.getFace().getScenicId() != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
|
||||
// 防御性检查
|
||||
SearchFaceRespVo searchResult = context.getSearchResult();
|
||||
if (searchResult == null || searchResult.getSampleListIds() == null
|
||||
|| searchResult.getSampleListIds().isEmpty()) {
|
||||
return StageResult.skipped("searchResult或sampleListIds为空");
|
||||
}
|
||||
|
||||
Long scenicId = context.getFace().getScenicId();
|
||||
List<Long> originalMatchedIds = searchResult.getSampleListIds();
|
||||
|
||||
try {
|
||||
List<Long> associatedIds = faceSampleAssociationMapper
|
||||
.findAssociatedSampleIds(scenicId, originalMatchedIds);
|
||||
|
||||
if (associatedIds == null || associatedIds.isEmpty()) {
|
||||
log.debug("未找到关联样本, faceId={}, scenicId={}", context.getFaceId(), scenicId);
|
||||
return StageResult.success("无关联样本");
|
||||
}
|
||||
|
||||
// 获取当前的 sampleListIds(可能已经过筛选,也可能与 searchResult 相同)
|
||||
List<Long> currentSampleIds = context.getSampleListIds();
|
||||
if (currentSampleIds == null || currentSampleIds.isEmpty()) {
|
||||
currentSampleIds = new ArrayList<>(originalMatchedIds);
|
||||
}
|
||||
|
||||
// 计算净新增的关联ID
|
||||
Set<Long> currentSet = new HashSet<>(currentSampleIds);
|
||||
List<Long> netNewIds = associatedIds.stream()
|
||||
.filter(id -> !currentSet.contains(id))
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (netNewIds.isEmpty()) {
|
||||
log.debug("关联样本均已存在于当前结果中, faceId={}", context.getFaceId());
|
||||
return StageResult.success("关联样本均已存在");
|
||||
}
|
||||
|
||||
// 合并:当前筛选后的ID + 净新增关联ID
|
||||
List<Long> expandedIds = Stream.concat(
|
||||
currentSampleIds.stream(),
|
||||
netNewIds.stream()
|
||||
).distinct().collect(Collectors.toList());
|
||||
|
||||
context.setSampleListIds(expandedIds);
|
||||
context.setAssociatedSampleIds(netNewIds);
|
||||
|
||||
log.info("关联样本扩展完成: faceId={}, scenicId={}, 原始匹配数={}, 扩展前={}, 净新增={}, 扩展后={}",
|
||||
context.getFaceId(), scenicId,
|
||||
originalMatchedIds.size(), currentSampleIds.size(),
|
||||
netNewIds.size(), expandedIds.size());
|
||||
|
||||
return StageResult.success(String.format("关联扩展: +%d个样本", netNewIds.size()));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("关联样本扩展失败, faceId={}, scenicId={}", context.getFaceId(), scenicId, e);
|
||||
return StageResult.degraded("关联样本扩展失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import org.springframework.stereotype.Component;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
@@ -67,8 +68,14 @@ public class UpdateFaceResultStage extends AbstractPipelineStage<FaceMatchingCon
|
||||
faceEntity.setFirstMatchRate(BigDecimal.valueOf(searchResult.getFirstMatchRate()));
|
||||
}
|
||||
|
||||
if (searchResult.getSampleListIds() != null) {
|
||||
faceEntity.setMatchSampleIds(searchResult.getSampleListIds().stream()
|
||||
// 优先使用 context.sampleListIds(可能包含关联扩展的ID),
|
||||
// 回退到 searchResult.sampleListIds
|
||||
List<Long> finalSampleIds = context.getSampleListIds();
|
||||
if (finalSampleIds == null || finalSampleIds.isEmpty()) {
|
||||
finalSampleIds = searchResult.getSampleListIds();
|
||||
}
|
||||
if (finalSampleIds != null && !finalSampleIds.isEmpty()) {
|
||||
faceEntity.setMatchSampleIds(finalSampleIds.stream()
|
||||
.map(String::valueOf)
|
||||
.collect(Collectors.joining(",")));
|
||||
}
|
||||
@@ -83,7 +90,7 @@ public class UpdateFaceResultStage extends AbstractPipelineStage<FaceMatchingCon
|
||||
|
||||
log.debug("人脸结果更新成功:faceId={}, score={}, sampleCount={}",
|
||||
faceId, searchResult.getScore(),
|
||||
searchResult.getSampleListIds() != null ? searchResult.getSampleListIds().size() : 0);
|
||||
finalSampleIds != null ? finalSampleIds.size() : 0);
|
||||
|
||||
return StageResult.success("人脸结果更新成功");
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.ycwl.basic.mapper;
|
||||
|
||||
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleAssociationEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 人脸样本关联分组 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface FaceSampleAssociationMapper {
|
||||
/**
|
||||
* 根据景区ID和样本ID列表,查找同组的所有关联样本ID
|
||||
*/
|
||||
List<Long> findAssociatedSampleIds(@Param("scenicId") Long scenicId,
|
||||
@Param("sampleIds") List<Long> sampleIds);
|
||||
|
||||
/**
|
||||
* 批量插入关联记录(忽略重复)
|
||||
*/
|
||||
void batchInsertIgnore(@Param("list") List<FaceSampleAssociationEntity> list);
|
||||
|
||||
/**
|
||||
* 删除指定组内的指定样本关联
|
||||
*/
|
||||
void deleteByGroupAndSampleIds(@Param("scenicId") Long scenicId,
|
||||
@Param("groupKey") String groupKey,
|
||||
@Param("sampleIds") List<Long> sampleIds);
|
||||
|
||||
/**
|
||||
* 删除整个组的所有关联
|
||||
*/
|
||||
void deleteByGroup(@Param("scenicId") Long scenicId,
|
||||
@Param("groupKey") String groupKey);
|
||||
|
||||
/**
|
||||
* 查询指定组的所有样本ID
|
||||
*/
|
||||
List<Long> listSampleIdsByGroup(@Param("scenicId") Long scenicId,
|
||||
@Param("groupKey") String groupKey);
|
||||
|
||||
/**
|
||||
* 查询指定景区下的所有分组标识
|
||||
*/
|
||||
List<String> listGroupKeysByScenicId(@Param("scenicId") Long scenicId);
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import com.ycwl.basic.model.pc.source.entity.SourceWatermarkEntity;
|
||||
import com.ycwl.basic.model.pc.source.req.SourceReqQuery;
|
||||
import com.ycwl.basic.model.pc.source.resp.SourceRespVO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
@@ -142,6 +143,13 @@ public interface SourceMapper {
|
||||
*/
|
||||
List<Long> getDeviceIdsByFaceId(Long faceId);
|
||||
|
||||
/**
|
||||
* 获取faceId关联的图片素材的最大创建时间
|
||||
* @param faceId 人脸ID
|
||||
* @return 最大创建时间,无记录时返回null
|
||||
*/
|
||||
Date getMaxCreateTimeByFaceId(Long faceId);
|
||||
|
||||
/**
|
||||
* 根据faceId和设备ID获取source
|
||||
* @param faceId 人脸ID
|
||||
@@ -223,4 +231,19 @@ public interface SourceMapper {
|
||||
* @return 有数据的时间桶列表
|
||||
*/
|
||||
List<DeviceSourceTimelineVO> getDeviceSourceTimeline(Long deviceId, Date startTime, Date endTime);
|
||||
|
||||
/**
|
||||
* 根据会员ID和素材ID查询 member_source 关联记录
|
||||
* @param memberId 会员ID
|
||||
* @param sourceId 素材ID
|
||||
* @return 关联记录(含 faceId 等信息)
|
||||
*/
|
||||
MemberSourceEntity getMemberSourceByMemberAndSourceId(@Param("memberId") Long memberId, @Param("sourceId") Long sourceId);
|
||||
|
||||
/**
|
||||
* 根据会员ID和素材ID更新 member_source 关联记录的购买状态
|
||||
* @param memberSourceEntity 包含 memberId、sourceId、isBuy、orderId
|
||||
* @return 影响行数
|
||||
*/
|
||||
int updateRelationBySourceId(MemberSourceEntity memberSourceEntity);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ public class ContentPageVO {
|
||||
private int lockType;
|
||||
// 内容id contentType为0或1时才有值
|
||||
private Long contentId;
|
||||
private String origUrl;
|
||||
private String videoUrl;
|
||||
// 模版id
|
||||
private Long templateId;
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.ycwl.basic.model.pc.faceSample.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 人脸样本关联分组实体
|
||||
* 同一景区下相同 groupKey 的记录互为关联
|
||||
*/
|
||||
@Data
|
||||
@TableName("face_sample_association")
|
||||
public class FaceSampleAssociationEntity {
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
/** 景区ID */
|
||||
private Long scenicId;
|
||||
/** 关联分组标识 */
|
||||
private String groupKey;
|
||||
/** 人脸样本ID */
|
||||
private Long faceSampleId;
|
||||
private Date createAt;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.ycwl.basic.model.pc.faceSample.req;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 人脸样本关联分组请求
|
||||
*/
|
||||
@Data
|
||||
public class FaceSampleAssociationReq {
|
||||
/** 景区ID */
|
||||
private Long scenicId;
|
||||
/** 关联分组标识 */
|
||||
private String groupKey;
|
||||
/** 人脸样本ID列表 */
|
||||
private List<Long> faceSampleIds;
|
||||
}
|
||||
@@ -2,15 +2,17 @@ package com.ycwl.basic.model.printer.req;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 创建虚拟用户0元订单请求参数
|
||||
*/
|
||||
@Data
|
||||
public class CreateVirtualOrderRequest {
|
||||
/**
|
||||
* source记录ID
|
||||
* source记录ID列表(支持单个或多个sourceId聚合为一笔订单)
|
||||
*/
|
||||
private Long sourceId;
|
||||
private List<Long> sourceIds;
|
||||
|
||||
/**
|
||||
* 景区ID
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.ycwl.basic.model.printer.req;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 打印机大屏创建虚拟订单请求参数
|
||||
* 通过 faceSampleIds 自动查找关联的照片素材进行下单
|
||||
*/
|
||||
@Data
|
||||
public class TvCreateVirtualOrderRequest {
|
||||
/**
|
||||
* 人脸样本ID列表,系统自动查找这些样本关联的所有照片素材(type=2)
|
||||
*/
|
||||
private List<Long> faceSampleIds;
|
||||
|
||||
/**
|
||||
* 景区ID
|
||||
*/
|
||||
private Long scenicId;
|
||||
|
||||
/**
|
||||
* 打印机ID(可选)
|
||||
*/
|
||||
private Integer printerId;
|
||||
|
||||
/**
|
||||
* 是否需要图像增强(可选,默认不增强)
|
||||
*/
|
||||
private Boolean needEnhance;
|
||||
|
||||
/**
|
||||
* 打印图片URL(可选,如果提供则使用此URL进行打印)
|
||||
*/
|
||||
private String printImgUrl;
|
||||
|
||||
/**
|
||||
* 是否需要实际支付(可选,默认false)
|
||||
* false/null: 创建0元虚拟订单,立即完成购买
|
||||
* true: 创建待支付订单(计算实际价格)
|
||||
*/
|
||||
private Boolean needActualPayment;
|
||||
}
|
||||
@@ -58,13 +58,13 @@ public class PriceCalculationController {
|
||||
* 查询用户可用优惠券(包含领取记录信息)
|
||||
*/
|
||||
@GetMapping("/coupons/my-coupons")
|
||||
public ApiResponse<List<UserCouponResp>> getUserCoupons() {
|
||||
public ApiResponse<List<UserCouponResp>> getUserCoupons(@RequestParam(required = false) String scenicId) {
|
||||
Long userId = getUserId();
|
||||
if (userId == null) {
|
||||
return ApiResponse.fail("用户未登录");
|
||||
}
|
||||
|
||||
List<UserCouponResp> coupons = couponService.getUserAvailableCoupons(userId);
|
||||
List<UserCouponResp> coupons = couponService.getUserAvailableCoupons(userId, scenicId);
|
||||
|
||||
return ApiResponse.success(coupons);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.ycwl.basic.pricing.controller;
|
||||
|
||||
import com.github.pagehelper.PageInfo;
|
||||
import com.ycwl.basic.constant.BaseContextHandler;
|
||||
import com.ycwl.basic.pricing.dto.req.VoucherBatchCreateReq;
|
||||
import com.ycwl.basic.pricing.dto.req.VoucherBatchCreateReqV2;
|
||||
import com.ycwl.basic.pricing.dto.req.VoucherBatchQueryReq;
|
||||
@@ -96,8 +97,9 @@ public class VoucherManagementController {
|
||||
}
|
||||
|
||||
@GetMapping("/mobile/my-codes")
|
||||
public ApiResponse<List<VoucherCodeResp>> getMyVoucherCodes(@RequestParam Long faceId) {
|
||||
List<VoucherCodeResp> codes = voucherCodeService.getMyVoucherCodes(faceId);
|
||||
public ApiResponse<List<VoucherCodeResp>> getMyVoucherCodes() {
|
||||
Long userId = Long.valueOf(BaseContextHandler.getUserId());
|
||||
List<VoucherCodeResp> codes = voucherCodeService.getMyVoucherCodes(userId);
|
||||
return ApiResponse.success(codes);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,5 @@ import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class VoucherClaimReq {
|
||||
private Long scenicId;
|
||||
private Long faceId;
|
||||
private String code;
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import lombok.EqualsAndHashCode;
|
||||
public class VoucherCodeQueryReq extends BaseQueryParameterReq {
|
||||
private Long batchId;
|
||||
private Long scenicId;
|
||||
private Long faceId;
|
||||
private Long userId;
|
||||
private Integer status;
|
||||
private String code;
|
||||
}
|
||||
@@ -7,6 +7,15 @@ import java.util.Date;
|
||||
|
||||
@Data
|
||||
public class VoucherCodeResp {
|
||||
/**
|
||||
* 领取是否成功
|
||||
*/
|
||||
private Boolean success;
|
||||
/**
|
||||
* 结果描述(失败时为原因说明)
|
||||
*/
|
||||
private String message;
|
||||
|
||||
private Long id;
|
||||
private Long batchId;
|
||||
private String batchName;
|
||||
@@ -14,7 +23,7 @@ public class VoucherCodeResp {
|
||||
private String code;
|
||||
private Integer status;
|
||||
private String statusName;
|
||||
private Long faceId;
|
||||
private Long userId;
|
||||
private Date claimedTime;
|
||||
private Date usedTime;
|
||||
private String remark;
|
||||
|
||||
@@ -102,9 +102,9 @@ public class VoucherDetailResp {
|
||||
@Data
|
||||
public static class UserInfo {
|
||||
/**
|
||||
* 用户人脸ID
|
||||
* 用户ID
|
||||
*/
|
||||
private Long faceId;
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 该用户已使用此券码的次数
|
||||
|
||||
@@ -39,9 +39,9 @@ public class PriceVoucherCode {
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 领取人faceId
|
||||
* 领取人用户ID
|
||||
*/
|
||||
private Long faceId;
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 领取时间
|
||||
|
||||
@@ -61,11 +61,11 @@ public interface PriceCouponConfigMapper extends BaseMapper<PriceCouponConfig> {
|
||||
*/
|
||||
@Insert("INSERT INTO price_coupon_config (coupon_name, coupon_type, discount_value, min_amount, " +
|
||||
"max_discount, applicable_products, required_attribute_keys, total_quantity, used_quantity, " +
|
||||
"claimed_quantity, user_claim_limit, valid_from, valid_until, " +
|
||||
"claimed_quantity, user_claim_limit, valid_from, valid_until, valid_days_after_claim, " +
|
||||
"is_active, scenic_id, create_time, update_time) VALUES " +
|
||||
"(#{couponName}, #{couponType}, #{discountValue}, #{minAmount}, #{maxDiscount}, " +
|
||||
"#{applicableProducts}, #{requiredAttributeKeys}, #{totalQuantity}, #{usedQuantity}, " +
|
||||
"#{claimedQuantity}, #{userClaimLimit}, #{validFrom}, #{validUntil}, " +
|
||||
"#{claimedQuantity}, #{userClaimLimit}, #{validFrom}, #{validUntil}, #{validDaysAfterClaim}, " +
|
||||
"#{isActive}, #{scenicId}, NOW(), NOW())")
|
||||
int insertCoupon(PriceCouponConfig coupon);
|
||||
|
||||
@@ -76,7 +76,8 @@ public interface PriceCouponConfigMapper extends BaseMapper<PriceCouponConfig> {
|
||||
"discount_value = #{discountValue}, min_amount = #{minAmount}, max_discount = #{maxDiscount}, " +
|
||||
"applicable_products = #{applicableProducts}, required_attribute_keys = #{requiredAttributeKeys}, " +
|
||||
"total_quantity = #{totalQuantity}, user_claim_limit = #{userClaimLimit}, " +
|
||||
"valid_from = #{validFrom}, valid_until = #{validUntil}, is_active = #{isActive}, " +
|
||||
"valid_from = #{validFrom}, valid_until = #{validUntil}, valid_days_after_claim = #{validDaysAfterClaim}, " +
|
||||
"is_active = #{isActive}, " +
|
||||
"scenic_id = #{scenicId}, update_time = NOW() WHERE id = #{id}")
|
||||
int updateCoupon(PriceCouponConfig coupon);
|
||||
|
||||
|
||||
@@ -34,6 +34,12 @@ public interface PriceProductConfigMapper extends BaseMapper<PriceProductConfig>
|
||||
@Select("SELECT * FROM price_product_config WHERE product_type = #{productType} AND product_id = #{productId} AND is_active = 1")
|
||||
PriceProductConfig selectByProductTypeAndId(String productType, String productId);
|
||||
|
||||
/**
|
||||
* 根据商品类型和商品ID查询全局配置(排除景区级配置)
|
||||
*/
|
||||
@Select("SELECT * FROM price_product_config WHERE product_type = #{productType} AND product_id = #{productId} AND (scenic_id IS NULL OR scenic_id = '') AND is_active = 1")
|
||||
PriceProductConfig selectGlobalByProductTypeAndId(String productType, String productId);
|
||||
|
||||
/**
|
||||
* 根据商品类型、商品ID和景区ID查询配置(支持景区维度)
|
||||
*/
|
||||
|
||||
@@ -27,6 +27,17 @@ public interface PriceTierConfigMapper extends BaseMapper<PriceTierConfig> {
|
||||
@Param("productId") String productId,
|
||||
@Param("quantity") Integer quantity);
|
||||
|
||||
/**
|
||||
* 根据商品类型、商品ID和数量查询全局匹配的阶梯价格(排除景区级配置)
|
||||
*/
|
||||
@Select("SELECT * FROM price_tier_config WHERE product_type = #{productType} " +
|
||||
"AND product_id = #{productId} AND (scenic_id IS NULL OR scenic_id = '') " +
|
||||
"AND #{quantity} >= min_quantity AND #{quantity} <= max_quantity " +
|
||||
"AND is_active = 1 ORDER BY sort_order ASC LIMIT 1")
|
||||
PriceTierConfig selectGlobalByProductTypeAndQuantity(@Param("productType") String productType,
|
||||
@Param("productId") String productId,
|
||||
@Param("quantity") Integer quantity);
|
||||
|
||||
/**
|
||||
* 根据商品类型、商品ID、数量和景区ID查询匹配的阶梯价格(支持景区维度)
|
||||
*/
|
||||
|
||||
@@ -21,31 +21,31 @@ public interface PriceVoucherCodeMapper extends BaseMapper<PriceVoucherCode> {
|
||||
* @param code 券码
|
||||
* @return 券码信息
|
||||
*/
|
||||
@Select("SELECT id, batch_id, scenic_id, code, status, face_id, claimed_time, used_time, " +
|
||||
@Select("SELECT id, batch_id, scenic_id, code, status, user_id, claimed_time, used_time, " +
|
||||
"current_use_count, last_used_time, remark, create_time, update_time, deleted, deleted_at " +
|
||||
"FROM price_voucher_code WHERE code = #{code} AND deleted = 0 LIMIT 1")
|
||||
PriceVoucherCode selectByCode(@Param("code") String code);
|
||||
|
||||
/**
|
||||
* 根据faceId和scenicId统计已领取的券码数量
|
||||
* @param faceId 用户faceId
|
||||
* 根据userId和scenicId统计已领取的券码数量
|
||||
* @param userId 用户ID
|
||||
* @param scenicId 景区ID
|
||||
* @return 数量
|
||||
*/
|
||||
@Select("SELECT COUNT(1) FROM price_voucher_code WHERE face_id = #{faceId} AND scenic_id = #{scenicId} AND deleted = 0")
|
||||
Integer countByFaceIdAndScenicId(@Param("faceId") Long faceId, @Param("scenicId") Long scenicId);
|
||||
@Select("SELECT COUNT(1) FROM price_voucher_code WHERE user_id = #{userId} AND scenic_id = #{scenicId} AND deleted = 0")
|
||||
Integer countByUserIdAndScenicId(@Param("userId") Long userId, @Param("scenicId") Long scenicId);
|
||||
|
||||
/**
|
||||
* 查询用户在指定景区的可用券码列表
|
||||
* @param faceId 用户faceId
|
||||
* @param userId 用户ID
|
||||
* @param scenicId 景区ID
|
||||
* @return 券码列表
|
||||
*/
|
||||
@Select("SELECT id, batch_id, scenic_id, code, status, face_id, claimed_time, used_time, " +
|
||||
@Select("SELECT id, batch_id, scenic_id, code, status, user_id, claimed_time, used_time, " +
|
||||
"current_use_count, last_used_time, remark, create_time, update_time, deleted, deleted_at " +
|
||||
"FROM price_voucher_code WHERE face_id = #{faceId} AND scenic_id = #{scenicId} AND status = 1 AND deleted = 0 " +
|
||||
"FROM price_voucher_code WHERE user_id = #{userId} AND scenic_id = #{scenicId} AND status = 1 AND deleted = 0 " +
|
||||
"ORDER BY claimed_time DESC")
|
||||
List<PriceVoucherCode> selectAvailableVouchersByFaceIdAndScenicId(@Param("faceId") Long faceId,
|
||||
List<PriceVoucherCode> selectAvailableVouchersByUserIdAndScenicId(@Param("userId") Long userId,
|
||||
@Param("scenicId") Long scenicId);
|
||||
|
||||
/**
|
||||
@@ -54,7 +54,7 @@ public interface PriceVoucherCodeMapper extends BaseMapper<PriceVoucherCode> {
|
||||
* @param limit 限制数量
|
||||
* @return 券码列表
|
||||
*/
|
||||
@Select("SELECT id, batch_id, scenic_id, code, status, face_id, claimed_time, used_time, " +
|
||||
@Select("SELECT id, batch_id, scenic_id, code, status, user_id, claimed_time, used_time, " +
|
||||
"current_use_count, last_used_time, remark, create_time, update_time, deleted, deleted_at " +
|
||||
"FROM price_voucher_code WHERE batch_id = #{batchId} AND status = 0 AND deleted = 0 LIMIT #{limit}")
|
||||
List<PriceVoucherCode> selectUnclaimedVouchersByBatchId(@Param("batchId") Long batchId,
|
||||
@@ -63,14 +63,14 @@ public interface PriceVoucherCodeMapper extends BaseMapper<PriceVoucherCode> {
|
||||
/**
|
||||
* 领取券码(更新状态为已领取)
|
||||
* @param id 券码ID
|
||||
* @param faceId 用户faceId
|
||||
* @param userId 用户ID
|
||||
* @param claimedTime 领取时间
|
||||
* @return 影响行数
|
||||
*/
|
||||
@Update("UPDATE price_voucher_code SET status = 1, face_id = #{faceId}, claimed_time = #{claimedTime}, " +
|
||||
@Update("UPDATE price_voucher_code SET status = 1, user_id = #{userId}, claimed_time = #{claimedTime}, " +
|
||||
"update_time = NOW() WHERE id = #{id} AND status = 0 AND deleted = 0")
|
||||
int claimVoucher(@Param("id") Long id,
|
||||
@Param("faceId") Long faceId,
|
||||
@Param("userId") Long userId,
|
||||
@Param("claimedTime") LocalDateTime claimedTime);
|
||||
|
||||
/**
|
||||
@@ -78,25 +78,25 @@ public interface PriceVoucherCodeMapper extends BaseMapper<PriceVoucherCode> {
|
||||
* @param batchId 批次ID
|
||||
* @return 券码列表
|
||||
*/
|
||||
@Select("SELECT id, batch_id, scenic_id, code, status, face_id, claimed_time, used_time, " +
|
||||
@Select("SELECT id, batch_id, scenic_id, code, status, user_id, claimed_time, used_time, " +
|
||||
"current_use_count, last_used_time, remark, create_time, update_time, deleted, deleted_at " +
|
||||
"FROM price_voucher_code WHERE batch_id = #{batchId} AND deleted = 0 ORDER BY create_time DESC")
|
||||
List<PriceVoucherCode> selectByBatchId(@Param("batchId") Long batchId);
|
||||
|
||||
/**
|
||||
* 查询用户的券码列表
|
||||
* @param faceId 用户faceId
|
||||
* @param userId 用户ID
|
||||
* @param scenicId 景区ID(可选)
|
||||
* @return 券码列表
|
||||
*/
|
||||
@Select("<script>" +
|
||||
"SELECT id, batch_id, scenic_id, code, status, face_id, claimed_time, used_time, " +
|
||||
"SELECT id, batch_id, scenic_id, code, status, user_id, claimed_time, used_time, " +
|
||||
"current_use_count, last_used_time, remark, create_time, update_time, deleted, deleted_at " +
|
||||
"FROM price_voucher_code WHERE face_id = #{faceId}" +
|
||||
"FROM price_voucher_code WHERE user_id = #{userId}" +
|
||||
"<if test='scenicId != null'> AND scenic_id = #{scenicId}</if>" +
|
||||
" AND deleted = 0 ORDER BY claimed_time DESC" +
|
||||
"</script>")
|
||||
List<PriceVoucherCode> selectUserVouchers(@Param("faceId") Long faceId,
|
||||
List<PriceVoucherCode> selectUserVouchers(@Param("userId") Long userId,
|
||||
@Param("scenicId") Long scenicId);
|
||||
|
||||
/**
|
||||
@@ -104,7 +104,7 @@ public interface PriceVoucherCodeMapper extends BaseMapper<PriceVoucherCode> {
|
||||
* @param batchId 批次ID
|
||||
* @return 可用券码
|
||||
*/
|
||||
@Select("SELECT id, batch_id, scenic_id, code, status, face_id, claimed_time, used_time, " +
|
||||
@Select("SELECT id, batch_id, scenic_id, code, status, user_id, claimed_time, used_time, " +
|
||||
"current_use_count, last_used_time, remark, create_time, update_time, deleted, deleted_at " +
|
||||
"FROM price_voucher_code WHERE batch_id = #{batchId} AND status = 0 AND deleted = 0 LIMIT 1")
|
||||
PriceVoucherCode findFirstAvailableByBatchId(@Param("batchId") Long batchId);
|
||||
@@ -114,7 +114,7 @@ public interface PriceVoucherCodeMapper extends BaseMapper<PriceVoucherCode> {
|
||||
* @param scenicId 景区ID
|
||||
* @return 可用券码
|
||||
*/
|
||||
@Select("SELECT pvc.id, pvc.batch_id, pvc.scenic_id, pvc.code, pvc.status, pvc.face_id, pvc.claimed_time, pvc.used_time, " +
|
||||
@Select("SELECT pvc.id, pvc.batch_id, pvc.scenic_id, pvc.code, pvc.status, pvc.user_id, pvc.claimed_time, pvc.used_time, " +
|
||||
"pvc.current_use_count, pvc.last_used_time, pvc.remark, pvc.create_time, pvc.update_time, pvc.deleted, pvc.deleted_at " +
|
||||
"FROM price_voucher_code pvc WHERE pvc.scenic_id = #{scenicId} AND pvc.status = 0 AND pvc.deleted = 0 " +
|
||||
"AND NOT EXISTS (SELECT 1 FROM voucher_print_record vpr WHERE vpr.voucher_code_id = pvc.id AND vpr.deleted = 0) " +
|
||||
|
||||
@@ -59,9 +59,10 @@ public interface ICouponService {
|
||||
* 查询用户可用优惠券(包含领取记录信息)
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param scenicId 景区ID,传入时仅返回该景区可用的优惠券,NULL时返回全部
|
||||
* @return 用户优惠券列表(包含领取记录+优惠券配置)
|
||||
*/
|
||||
List<UserCouponResp> getUserAvailableCoupons(Long userId);
|
||||
List<UserCouponResp> getUserAvailableCoupons(Long userId, String scenicId);
|
||||
|
||||
/**
|
||||
* 领取优惠券(内部调用方法)
|
||||
|
||||
@@ -15,9 +15,9 @@ public interface VoucherCodeService {
|
||||
|
||||
PageInfo<VoucherCodeResp> queryCodeList(VoucherCodeQueryReq req);
|
||||
|
||||
List<VoucherCodeResp> getMyVoucherCodes(Long faceId);
|
||||
List<VoucherCodeResp> getMyVoucherCodes(Long userId);
|
||||
|
||||
void markCodeAsUsed(Long codeId, String remark);
|
||||
|
||||
boolean canClaimVoucher(Long faceId, Long scenicId);
|
||||
boolean canClaimVoucher(Long userId, Long scenicId);
|
||||
}
|
||||
@@ -224,6 +224,18 @@ public class CouponServiceImpl implements ICouponService {
|
||||
@Override
|
||||
@Transactional
|
||||
public CouponUseResult useCoupon(CouponUseRequest request) {
|
||||
Date now = new Date();
|
||||
PriceCouponConfig coupon = couponConfigMapper.selectById(request.getCouponId());
|
||||
if (coupon == null || coupon.getDeleted() == 1 || !Boolean.TRUE.equals(coupon.getIsActive())) {
|
||||
throw new CouponInvalidException("优惠券不存在或已失效");
|
||||
}
|
||||
if (coupon.getValidFrom() != null && now.before(coupon.getValidFrom())) {
|
||||
throw new CouponInvalidException("优惠券尚未生效");
|
||||
}
|
||||
if (coupon.getValidUntil() != null && !now.before(coupon.getValidUntil())) {
|
||||
throw new CouponInvalidException("优惠券已过期");
|
||||
}
|
||||
|
||||
List<PriceCouponClaimRecord> records = couponClaimRecordMapper.selectUserCouponRecords(
|
||||
request.getUserId(), request.getCouponId());
|
||||
|
||||
@@ -234,10 +246,18 @@ public class CouponServiceImpl implements ICouponService {
|
||||
// 查找一张可用的优惠券(状态为CLAIMED)
|
||||
PriceCouponClaimRecord record = records.stream()
|
||||
.filter(r -> r.getStatus() == CouponStatus.CLAIMED)
|
||||
.filter(r -> r.getExpireTime() == null || r.getExpireTime().after(now))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
if (record == null) {
|
||||
boolean hasClaimedButExpired = records.stream()
|
||||
.anyMatch(r -> r.getStatus() == CouponStatus.CLAIMED
|
||||
&& r.getExpireTime() != null
|
||||
&& !r.getExpireTime().after(now));
|
||||
if (hasClaimedButExpired) {
|
||||
throw new CouponInvalidException("优惠券已过期");
|
||||
}
|
||||
// 如果没有可用的,抛出异常。为了错误信息准确,可以检查最后一张的状态
|
||||
CouponStatus lastStatus = records.getFirst().getStatus();
|
||||
throw new CouponInvalidException("优惠券状态无效: " + lastStatus);
|
||||
@@ -275,15 +295,19 @@ public class CouponServiceImpl implements ICouponService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<UserCouponResp> getUserAvailableCoupons(Long userId) {
|
||||
public List<UserCouponResp> getUserAvailableCoupons(Long userId, String scenicId) {
|
||||
List<PriceCouponClaimRecord> records = couponClaimRecordMapper.selectUserAvailableCoupons(userId);
|
||||
List<UserCouponResp> coupons = new ArrayList<>();
|
||||
|
||||
for (PriceCouponClaimRecord record : records) {
|
||||
PriceCouponConfig config = couponConfigMapper.selectById(record.getCouponId());
|
||||
if (config != null) {
|
||||
coupons.add(buildUserCouponResp(record, config));
|
||||
if (config == null) {
|
||||
continue;
|
||||
}
|
||||
if (scenicId != null && config.getScenicId() != null && !scenicId.equals(config.getScenicId())) {
|
||||
continue;
|
||||
}
|
||||
coupons.add(buildUserCouponResp(record, config));
|
||||
}
|
||||
|
||||
return coupons;
|
||||
|
||||
@@ -401,41 +401,21 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
|
||||
log.warn("未找到具体商品配置: productType={}, productId={}, scenicId={}, 尝试使用通用配置",
|
||||
productType, productId, scenicId);
|
||||
|
||||
// 兜底:使用default配置(带景区ID)
|
||||
try {
|
||||
PriceProductConfig defaultConfig = productConfigService.getProductConfig(productType.getCode(), "default", scenicId);
|
||||
if (defaultConfig != null) {
|
||||
actualPrice = defaultConfig.getBasePrice();
|
||||
originalPrice = defaultConfig.getOriginalPrice();
|
||||
// 最后兜底:使用通用配置(向后兼容)
|
||||
List<PriceProductConfig> configs = productConfigService.getProductConfig(productType.getCode());
|
||||
if (!configs.isEmpty()) {
|
||||
PriceProductConfig baseConfig = configs.getFirst();
|
||||
actualPrice = baseConfig.getBasePrice();
|
||||
originalPrice = baseConfig.getOriginalPrice();
|
||||
|
||||
if (isQuantityBasedPricing(capability)) {
|
||||
actualPrice = actualPrice.multiply(BigDecimal.valueOf(product.getQuantity()));
|
||||
if (originalPrice != null) {
|
||||
originalPrice = originalPrice.multiply(BigDecimal.valueOf(product.getQuantity()));
|
||||
}
|
||||
if (isQuantityBasedPricing(capability)) {
|
||||
actualPrice = actualPrice.multiply(BigDecimal.valueOf(product.getQuantity()));
|
||||
if (originalPrice != null) {
|
||||
originalPrice = originalPrice.multiply(BigDecimal.valueOf(product.getQuantity()));
|
||||
}
|
||||
} else {
|
||||
throw new PriceCalculationException("无法找到default配置");
|
||||
}
|
||||
} catch (Exception defaultEx) {
|
||||
log.warn("未找到default配置: productType={}, scenicId={}", productType.getCode(), scenicId);
|
||||
|
||||
// 最后兜底:使用通用配置(向后兼容)
|
||||
List<PriceProductConfig> configs = productConfigService.getProductConfig(productType.getCode());
|
||||
if (!configs.isEmpty()) {
|
||||
PriceProductConfig baseConfig = configs.getFirst(); // 使用第一个配置作为默认
|
||||
actualPrice = baseConfig.getBasePrice();
|
||||
originalPrice = baseConfig.getOriginalPrice();
|
||||
|
||||
if (isQuantityBasedPricing(capability)) {
|
||||
actualPrice = actualPrice.multiply(BigDecimal.valueOf(product.getQuantity()));
|
||||
if (originalPrice != null) {
|
||||
originalPrice = originalPrice.multiply(BigDecimal.valueOf(product.getQuantity()));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new PriceCalculationException("无法计算商品价格: " + productType.getDescription() + ", productId: " + productId);
|
||||
}
|
||||
} else {
|
||||
throw new PriceCalculationException("无法计算商品价格: " + productType.getDescription() + ", productId: " + productId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,29 +76,41 @@ public class ProductConfigServiceImpl implements IProductConfigService {
|
||||
return getProductConfig(productType, productId);
|
||||
}
|
||||
|
||||
String scenicIdStr = scenicId.toString();
|
||||
|
||||
// 查询优先级:
|
||||
// 1. 景区+商品ID
|
||||
PriceProductConfig config = productConfigMapper.selectByProductTypeIdAndScenic(
|
||||
productType, productId, scenicId.toString());
|
||||
productType, productId, scenicIdStr);
|
||||
if (config != null) {
|
||||
log.debug("使用景区特定商品配置: productType={}, productId={}, scenicId={}",
|
||||
productType, productId, scenicId);
|
||||
return config;
|
||||
}
|
||||
|
||||
// 2. 景区+默认
|
||||
// 2. 景区+景区ID作为商品ID(productId未命中时回退)
|
||||
if (!scenicIdStr.equals(productId)) {
|
||||
config = productConfigMapper.selectByProductTypeIdAndScenic(
|
||||
productType, scenicIdStr, scenicIdStr);
|
||||
if (config != null) {
|
||||
log.debug("使用景区ID作为商品ID的配置: productType={}, scenicId={}", productType, scenicId);
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 景区+默认
|
||||
if (!"default".equals(productId)) {
|
||||
config = productConfigMapper.selectByProductTypeIdAndScenic(
|
||||
productType, "default", scenicId.toString());
|
||||
productType, "default", scenicIdStr);
|
||||
if (config != null) {
|
||||
log.debug("使用景区默认配置: productType={}, scenicId={}", productType, scenicId);
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 全局+商品ID (兜底)
|
||||
// 4. 全局+商品ID (兜底)
|
||||
try {
|
||||
config = productConfigMapper.selectByProductTypeAndId(productType, productId);
|
||||
config = productConfigMapper.selectGlobalByProductTypeAndId(productType, productId);
|
||||
if (config != null) {
|
||||
log.debug("使用全局商品配置: productType={}, productId={}", productType, productId);
|
||||
return config;
|
||||
@@ -107,8 +119,8 @@ public class ProductConfigServiceImpl implements IProductConfigService {
|
||||
log.debug("全局商品配置未找到: productType={}, productId={}", productType, productId);
|
||||
}
|
||||
|
||||
// 4. 全局+默认 (最后兜底)
|
||||
config = productConfigMapper.selectByProductTypeAndId(productType, "default");
|
||||
// 5. 全局+默认 (最后兜底)
|
||||
config = productConfigMapper.selectGlobalByProductTypeAndId(productType, "default");
|
||||
if (config != null) {
|
||||
log.debug("使用全局默认配置: productType={}", productType);
|
||||
return config;
|
||||
@@ -130,20 +142,33 @@ public class ProductConfigServiceImpl implements IProductConfigService {
|
||||
return getTierConfig(productType, productId, quantity);
|
||||
}
|
||||
|
||||
String scenicIdStr = scenicId.toString();
|
||||
|
||||
// 查询优先级:
|
||||
// 1. 景区+商品ID
|
||||
PriceTierConfig config = tierConfigMapper.selectByProductTypeQuantityAndScenic(
|
||||
productType, productId, quantity, scenicId.toString());
|
||||
productType, productId, quantity, scenicIdStr);
|
||||
if (config != null) {
|
||||
log.debug("使用景区特定阶梯定价: productType={}, productId={}, quantity={}, scenicId={}",
|
||||
productType, productId, quantity, scenicId);
|
||||
return config;
|
||||
}
|
||||
|
||||
// 2. 景区+默认
|
||||
// 2. 景区+景区ID作为商品ID(productId未命中时回退)
|
||||
if (!scenicIdStr.equals(productId)) {
|
||||
config = tierConfigMapper.selectByProductTypeQuantityAndScenic(
|
||||
productType, scenicIdStr, quantity, scenicIdStr);
|
||||
if (config != null) {
|
||||
log.debug("使用景区ID作为商品ID的阶梯定价: productType={}, quantity={}, scenicId={}",
|
||||
productType, quantity, scenicId);
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 景区+默认
|
||||
if (!"default".equals(productId)) {
|
||||
config = tierConfigMapper.selectByProductTypeQuantityAndScenic(
|
||||
productType, "default", quantity, scenicId.toString());
|
||||
productType, "default", quantity, scenicIdStr);
|
||||
if (config != null) {
|
||||
log.debug("使用景区默认阶梯定价: productType={}, quantity={}, scenicId={}",
|
||||
productType, quantity, scenicId);
|
||||
@@ -151,16 +176,16 @@ public class ProductConfigServiceImpl implements IProductConfigService {
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 全局+商品ID (兜底)
|
||||
config = tierConfigMapper.selectByProductTypeAndQuantity(productType, productId, quantity);
|
||||
// 4. 全局+商品ID (兜底)
|
||||
config = tierConfigMapper.selectGlobalByProductTypeAndQuantity(productType, productId, quantity);
|
||||
if (config != null) {
|
||||
log.debug("使用全局阶梯定价: productType={}, productId={}, quantity={}",
|
||||
productType, productId, quantity);
|
||||
return config;
|
||||
}
|
||||
|
||||
// 4. 全局+默认 (最后兜底)
|
||||
config = tierConfigMapper.selectByProductTypeAndQuantity(productType, "default", quantity);
|
||||
// 5. 全局+默认 (最后兜底)
|
||||
config = tierConfigMapper.selectGlobalByProductTypeAndQuantity(productType, "default", quantity);
|
||||
if (config != null) {
|
||||
log.debug("使用全局默认阶梯定价: productType={}, quantity={}", productType, quantity);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.ycwl.basic.pricing.service.impl;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.github.pagehelper.PageHelper;
|
||||
import com.github.pagehelper.PageInfo;
|
||||
import com.ycwl.basic.constant.BaseContextHandler;
|
||||
import com.ycwl.basic.exception.BizException;
|
||||
import com.ycwl.basic.pricing.dto.req.VoucherClaimReq;
|
||||
import com.ycwl.basic.pricing.dto.req.VoucherCodeQueryReq;
|
||||
@@ -73,43 +74,49 @@ public void generateVoucherCodes(Long batchId, Long scenicId, Integer count) {
|
||||
@Override
|
||||
@Transactional
|
||||
public VoucherCodeResp claimVoucher(VoucherClaimReq req) {
|
||||
if (req.getScenicId() == null) {
|
||||
throw new BizException(400, "景区ID不能为空");
|
||||
}
|
||||
if (req.getFaceId() == null) {
|
||||
throw new BizException(400, "用户faceId不能为空");
|
||||
}
|
||||
if (!StringUtils.hasText(req.getCode())) {
|
||||
throw new BizException(400, "券码不能为空");
|
||||
}
|
||||
|
||||
// 验证券码是否存在且未被领取
|
||||
Long userId = Long.valueOf(BaseContextHandler.getUserId());
|
||||
|
||||
// 查询券码
|
||||
LambdaQueryWrapper<PriceVoucherCode> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(PriceVoucherCode::getCode, req.getCode())
|
||||
.eq(PriceVoucherCode::getScenicId, req.getScenicId())
|
||||
.eq(PriceVoucherCode::getDeleted, 0);
|
||||
|
||||
PriceVoucherCode voucherCode = voucherCodeMapper.selectOne(wrapper);
|
||||
if (voucherCode == null) {
|
||||
throw new BizException(400, "券码不存在或不属于该景区");
|
||||
throw new BizException(400, "券码不存在");
|
||||
}
|
||||
|
||||
if (!Objects.equals(voucherCode.getStatus(), VoucherCodeStatus.UNCLAIMED.getCode())) {
|
||||
throw new BizException(400, "券码已被领取或已使用");
|
||||
}
|
||||
|
||||
if (!canClaimVoucher(req.getFaceId(), req.getScenicId())) {
|
||||
throw new BizException(400, "该用户在此景区已领取过券码");
|
||||
}
|
||||
|
||||
// 获取券码所属批次
|
||||
// 查询批次信息,用于构建响应
|
||||
PriceVoucherBatchConfig batch = voucherBatchMapper.selectById(voucherCode.getBatchId());
|
||||
|
||||
// 券码已找到,后续校验失败时仍返回 scenicId 等信息
|
||||
if (!Objects.equals(voucherCode.getStatus(), VoucherCodeStatus.UNCLAIMED.getCode())) {
|
||||
VoucherCodeResp resp = convertToResp(voucherCode, batch);
|
||||
resp.setSuccess(false);
|
||||
resp.setMessage("券码已被领取或已使用");
|
||||
return resp;
|
||||
}
|
||||
|
||||
if (!canClaimVoucher(userId, voucherCode.getScenicId())) {
|
||||
VoucherCodeResp resp = convertToResp(voucherCode, batch);
|
||||
resp.setSuccess(false);
|
||||
resp.setMessage("该用户在此景区已领取过券码");
|
||||
return resp;
|
||||
}
|
||||
|
||||
if (batch == null || batch.getDeleted() == 1) {
|
||||
throw new BizException(400, "券码批次不存在");
|
||||
VoucherCodeResp resp = convertToResp(voucherCode, batch);
|
||||
resp.setSuccess(false);
|
||||
resp.setMessage("券码批次不存在");
|
||||
return resp;
|
||||
}
|
||||
|
||||
// 更新券码状态
|
||||
voucherCode.setFaceId(req.getFaceId());
|
||||
voucherCode.setUserId(userId);
|
||||
voucherCode.setStatus(VoucherCodeStatus.CLAIMED_UNUSED.getCode());
|
||||
voucherCode.setClaimedTime(new Date());
|
||||
// 确保currentUseCount被初始化
|
||||
@@ -121,7 +128,10 @@ public VoucherCodeResp claimVoucher(VoucherClaimReq req) {
|
||||
|
||||
voucherBatchService.updateBatchClaimedCount(batch.getId());
|
||||
|
||||
return convertToResp(voucherCode, batch);
|
||||
VoucherCodeResp resp = convertToResp(voucherCode, batch);
|
||||
resp.setSuccess(true);
|
||||
resp.setMessage("领取成功");
|
||||
return resp;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -132,7 +142,7 @@ public VoucherCodeResp claimVoucher(VoucherClaimReq req) {
|
||||
wrapper.eq(PriceVoucherCode::getDeleted, 0)
|
||||
.eq(req.getBatchId() != null, PriceVoucherCode::getBatchId, req.getBatchId())
|
||||
.eq(req.getScenicId() != null, PriceVoucherCode::getScenicId, req.getScenicId())
|
||||
.eq(req.getFaceId() != null, PriceVoucherCode::getFaceId, req.getFaceId())
|
||||
.eq(req.getUserId() != null, PriceVoucherCode::getUserId, req.getUserId())
|
||||
.eq(req.getStatus() != null, PriceVoucherCode::getStatus, req.getStatus())
|
||||
.like(StringUtils.hasText(req.getCode()), PriceVoucherCode::getCode, req.getCode())
|
||||
.orderByDesc(PriceVoucherCode::getId);
|
||||
@@ -149,9 +159,9 @@ public VoucherCodeResp claimVoucher(VoucherClaimReq req) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VoucherCodeResp> getMyVoucherCodes(Long faceId) {
|
||||
public List<VoucherCodeResp> getMyVoucherCodes(Long userId) {
|
||||
LambdaQueryWrapper<PriceVoucherCode> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(PriceVoucherCode::getFaceId, faceId)
|
||||
wrapper.eq(PriceVoucherCode::getUserId, userId)
|
||||
.eq(PriceVoucherCode::getDeleted, 0)
|
||||
.orderByDesc(PriceVoucherCode::getClaimedTime);
|
||||
|
||||
@@ -193,8 +203,8 @@ public void markCodeAsUsed(Long codeId, String remark) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canClaimVoucher(Long faceId, Long scenicId) {
|
||||
Integer count = voucherCodeMapper.countByFaceIdAndScenicId(faceId, scenicId);
|
||||
public boolean canClaimVoucher(Long userId, Long scenicId) {
|
||||
Integer count = voucherCodeMapper.countByUserIdAndScenicId(userId, scenicId);
|
||||
return count == 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -132,7 +132,7 @@ public class VoucherServiceImpl implements IVoucherService {
|
||||
if (faceId == null) {
|
||||
voucherInfo.setAvailable(false);
|
||||
voucherInfo.setUnavailableReason("用户信息缺失,无法验证券码权限");
|
||||
} else if (!faceId.equals(voucherCodeEntity.getFaceId())) {
|
||||
} else if (!faceId.equals(voucherCodeEntity.getUserId())) {
|
||||
voucherInfo.setAvailable(false);
|
||||
voucherInfo.setUnavailableReason("券码已被其他用户领取");
|
||||
} else {
|
||||
@@ -176,7 +176,7 @@ public class VoucherServiceImpl implements IVoucherService {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
List<PriceVoucherCode> voucherCodes = voucherCodeMapper.selectAvailableVouchersByFaceIdAndScenicId(faceId, scenicId);
|
||||
List<PriceVoucherCode> voucherCodes = voucherCodeMapper.selectAvailableVouchersByUserIdAndScenicId(faceId, scenicId);
|
||||
List<VoucherInfo> voucherInfos = new ArrayList<>();
|
||||
|
||||
for (PriceVoucherCode voucherCode : voucherCodes) {
|
||||
@@ -234,7 +234,7 @@ public void markVoucherAsUsed(String voucherCode, String remark, String orderId,
|
||||
PriceVoucherUsageRecord usageRecord = new PriceVoucherUsageRecord();
|
||||
usageRecord.setVoucherCodeId(voucherCodeEntity.getId());
|
||||
usageRecord.setVoucherCode(voucherCode);
|
||||
usageRecord.setFaceId(faceId != null ? faceId : voucherCodeEntity.getFaceId());
|
||||
usageRecord.setFaceId(faceId != null ? faceId : voucherCodeEntity.getUserId());
|
||||
usageRecord.setScenicId(voucherCodeEntity.getScenicId());
|
||||
usageRecord.setBatchId(voucherCodeEntity.getBatchId());
|
||||
usageRecord.setUsageSequence(newUseCount); // 设置使用序号,表示这是该券码的第几次使用
|
||||
@@ -279,7 +279,7 @@ public void markVoucherAsUsed(String voucherCode, String remark, String orderId,
|
||||
return false;
|
||||
}
|
||||
|
||||
Integer count = voucherCodeMapper.countByFaceIdAndScenicId(faceId, scenicId);
|
||||
Integer count = voucherCodeMapper.countByUserIdAndScenicId(faceId, scenicId);
|
||||
return count == 0;
|
||||
}
|
||||
|
||||
@@ -435,7 +435,7 @@ public void markVoucherAsUsed(String voucherCode, String remark, String orderId,
|
||||
// 设置用户信息
|
||||
if (faceId != null) {
|
||||
VoucherDetailResp.UserInfo userInfo = new VoucherDetailResp.UserInfo();
|
||||
userInfo.setFaceId(faceId);
|
||||
userInfo.setUserId(faceId);
|
||||
|
||||
// 计算该用户使用此券码的次数
|
||||
List<PriceVoucherUsageRecord> userUsageRecords = usageRecordMapper.selectByVoucherCodeAndFaceId(voucherCodeEntity.getId(), faceId);
|
||||
|
||||
@@ -141,23 +141,50 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
|
||||
template.getId(), contentHash, resolvedScenicId
|
||||
);
|
||||
if (duplicateRecord != null) {
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
log.info("检测到重复内容,复用历史记录: recordId={}, imageUrl={}, duration={}ms",
|
||||
duplicateRecord.getId(), duplicateRecord.getResultImageUrl(), duration);
|
||||
// 标记素材版本缓存
|
||||
if (request.getFaceId() != null) {
|
||||
faceStatusManager.markPuzzleSourceVersion(request.getFaceId(), template.getId(), 0);
|
||||
if (duplicateRecord.getStatus() == 1) {
|
||||
// 已有成功记录,直接复用
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
log.info("检测到重复内容,复用历史记录: recordId={}, imageUrl={}, duration={}ms",
|
||||
duplicateRecord.getId(), duplicateRecord.getResultImageUrl(), duration);
|
||||
// 标记素材版本缓存
|
||||
if (request.getFaceId() != null) {
|
||||
faceStatusManager.markPuzzleSourceVersion(request.getFaceId(), template.getId(), 0);
|
||||
}
|
||||
return PuzzleGenerateResponse.success(
|
||||
duplicateRecord.getResultImageUrl(),
|
||||
duplicateRecord.getResultFileSize(),
|
||||
duplicateRecord.getResultWidth(),
|
||||
duplicateRecord.getResultHeight(),
|
||||
(int) duration,
|
||||
duplicateRecord.getId(),
|
||||
true,
|
||||
duplicateRecord.getId()
|
||||
);
|
||||
} else if (duplicateRecord.getStatus() == 0) {
|
||||
// 相同内容正在生成中,等待完成后复用
|
||||
log.info("检测到相同内容正在生成中,等待完成: recordId={}", duplicateRecord.getId());
|
||||
PuzzleGenerationRecordEntity completedRecord = waitForRecordCompletion(duplicateRecord.getId(), 30_000);
|
||||
if (completedRecord != null && completedRecord.getStatus() == 1) {
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
log.info("等待生成中记录完成,复用结果: recordId={}, imageUrl={}, duration={}ms",
|
||||
completedRecord.getId(), completedRecord.getResultImageUrl(), duration);
|
||||
if (request.getFaceId() != null) {
|
||||
faceStatusManager.markPuzzleSourceVersion(request.getFaceId(), template.getId(), 0);
|
||||
}
|
||||
return PuzzleGenerateResponse.success(
|
||||
completedRecord.getResultImageUrl(),
|
||||
completedRecord.getResultFileSize(),
|
||||
completedRecord.getResultWidth(),
|
||||
completedRecord.getResultHeight(),
|
||||
(int) duration,
|
||||
completedRecord.getId(),
|
||||
true,
|
||||
completedRecord.getId()
|
||||
);
|
||||
}
|
||||
// 超时或失败,兜底创建新记录
|
||||
log.warn("等待生成中记录超时或失败,创建新记录: originalRecordId={}", duplicateRecord.getId());
|
||||
}
|
||||
return PuzzleGenerateResponse.success(
|
||||
duplicateRecord.getResultImageUrl(),
|
||||
duplicateRecord.getResultFileSize(),
|
||||
duplicateRecord.getResultWidth(),
|
||||
duplicateRecord.getResultHeight(),
|
||||
(int) duration,
|
||||
duplicateRecord.getId(),
|
||||
true,
|
||||
duplicateRecord.getId()
|
||||
);
|
||||
}
|
||||
|
||||
// 7. 创建生成记录
|
||||
@@ -290,10 +317,10 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
|
||||
);
|
||||
if (duplicateRecord != null) {
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
log.info("检测到重复内容,复用历史记录: recordId={}, imageUrl={}, duration={}ms",
|
||||
duplicateRecord.getId(), duplicateRecord.getResultImageUrl(), duration);
|
||||
// 标记素材版本缓存
|
||||
if (request.getFaceId() != null) {
|
||||
log.info("检测到重复内容,复用历史记录: recordId={}, status={}, imageUrl={}, duration={}ms",
|
||||
duplicateRecord.getId(), duplicateRecord.getStatus(), duplicateRecord.getResultImageUrl(), duration);
|
||||
// 仅成功记录才标记素材版本缓存(生成中的记录可能会失败)
|
||||
if (request.getFaceId() != null && duplicateRecord.getStatus() == 1) {
|
||||
faceStatusManager.markPuzzleSourceVersion(request.getFaceId(), template.getId(), 0);
|
||||
}
|
||||
return duplicateRecord.getId();
|
||||
@@ -326,6 +353,33 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
|
||||
return record.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待生成中的记录完成
|
||||
* 轮询数据库直到记录状态变为非生成中(成功或失败),或超时返回null
|
||||
*
|
||||
* @param recordId 记录ID
|
||||
* @param timeoutMs 超时时间(毫秒)
|
||||
* @return 完成后的记录,超时返回null
|
||||
*/
|
||||
private PuzzleGenerationRecordEntity waitForRecordCompletion(Long recordId, long timeoutMs) {
|
||||
long deadline = System.currentTimeMillis() + timeoutMs;
|
||||
while (System.currentTimeMillis() < deadline) {
|
||||
PuzzleGenerationRecordEntity record = recordMapper.getById(recordId);
|
||||
if (record == null || record.getStatus() != 0) {
|
||||
return record;
|
||||
}
|
||||
try {
|
||||
Thread.sleep(500);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
log.warn("等待记录完成被中断: recordId={}", recordId);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
log.warn("等待记录完成超时: recordId={}, timeoutMs={}", recordId, timeoutMs);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验请求参数
|
||||
*/
|
||||
|
||||
@@ -25,7 +25,7 @@ import java.util.stream.Collectors;
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class PuzzleDuplicationDetector {
|
||||
private final Set<String> skippedElementKeys = Set.of("dateStr");
|
||||
private final Set<String> skippedElementKeys = Set.of();
|
||||
private final PuzzleGenerationRecordMapper recordMapper;
|
||||
|
||||
/**
|
||||
|
||||
@@ -40,7 +40,7 @@ public class SourceRepository {
|
||||
Runtime.getRuntime().availableProcessors(),
|
||||
runnable -> {
|
||||
Thread thread = new Thread(runnable);
|
||||
thread.setName("ai-cam-image-processor-" + thread.getId());
|
||||
thread.setName("ai-cam-image-processor-" + thread.threadId());
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
}
|
||||
@@ -235,6 +235,28 @@ public class SourceRepository {
|
||||
faceStatusManager.invalidatePuzzleSourceVersion(faceId);
|
||||
}
|
||||
|
||||
public void setUserIsBuyItemBySourceId(Long memberId, Long sourceId, Long faceId, Long orderId) {
|
||||
MemberSourceEntity memberSource = new MemberSourceEntity();
|
||||
memberSource.setMemberId(memberId);
|
||||
memberSource.setSourceId(sourceId);
|
||||
memberSource.setOrderId(orderId);
|
||||
memberSource.setIsBuy(1);
|
||||
sourceMapper.updateRelationBySourceId(memberSource);
|
||||
memberRelationRepository.clearSCacheByFace(faceId);
|
||||
faceStatusManager.invalidatePuzzleSourceVersion(faceId);
|
||||
}
|
||||
|
||||
public void setUserNotBuyItemBySourceId(Long memberId, Long sourceId, Long faceId) {
|
||||
MemberSourceEntity memberSource = new MemberSourceEntity();
|
||||
memberSource.setMemberId(memberId);
|
||||
memberSource.setSourceId(sourceId);
|
||||
memberSource.setOrderId(null);
|
||||
memberSource.setIsBuy(0);
|
||||
sourceMapper.updateRelationBySourceId(memberSource);
|
||||
memberRelationRepository.clearSCacheByFace(faceId);
|
||||
faceStatusManager.invalidatePuzzleSourceVersion(faceId);
|
||||
}
|
||||
|
||||
public SourceEntity getSource(Long id) {
|
||||
return sourceMapper.getEntity(id);
|
||||
}
|
||||
|
||||
@@ -296,6 +296,15 @@ public class GoodsServiceImpl implements GoodsService {
|
||||
return response;
|
||||
}
|
||||
|
||||
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(face.getScenicId());
|
||||
if (Integer.valueOf(2).equals(scenicConfig.getInteger("scenic_mode", 0))) {
|
||||
// 摄影师拍照
|
||||
List<MemberSourceEntity> list = memberRelationRepository.listSourceByFaceRelation(faceId, 2);
|
||||
response.setStatus(VideoTaskStatus.SUCCESS.getCode());
|
||||
response.setCount(list.size());
|
||||
return response;
|
||||
}
|
||||
|
||||
// ==================== 第三步:检查模板渲染状态 ====================
|
||||
// 获取该景区的所有视频模板
|
||||
List<TemplateRespVO> templateList = templateRepository.getTemplateListByScenicId(response.getScenicId());
|
||||
|
||||
@@ -12,6 +12,8 @@ import com.ycwl.basic.face.pipeline.enums.FaceMatchingScene;
|
||||
import com.ycwl.basic.face.pipeline.factory.FaceMatchingPipelineFactory;
|
||||
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
|
||||
import com.ycwl.basic.facebody.entity.SearchFaceResultItem;
|
||||
import com.ycwl.basic.integration.device.dto.device.DeviceV2DTO;
|
||||
import com.ycwl.basic.mapper.FaceSampleAssociationMapper;
|
||||
import com.ycwl.basic.mapper.FaceSampleMapper;
|
||||
import com.ycwl.basic.mapper.ProjectMapper;
|
||||
import com.ycwl.basic.mapper.SourceMapper;
|
||||
@@ -60,6 +62,7 @@ import com.ycwl.basic.repository.FaceRepository;
|
||||
import com.ycwl.basic.repository.MemberRelationRepository;
|
||||
import com.ycwl.basic.repository.OrderRepository;
|
||||
import com.ycwl.basic.repository.ScenicRepository;
|
||||
import com.ycwl.basic.repository.SourceRepository;
|
||||
import com.ycwl.basic.repository.TemplateRepository;
|
||||
import com.ycwl.basic.repository.VideoRepository;
|
||||
import com.ycwl.basic.repository.VideoTaskRepository;
|
||||
@@ -99,6 +102,7 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
import java.io.File;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
@@ -159,6 +163,8 @@ public class FaceServiceImpl implements FaceService {
|
||||
@Autowired
|
||||
private FaceSampleMapper faceSampleMapper;
|
||||
@Autowired
|
||||
private FaceSampleAssociationMapper faceSampleAssociationMapper;
|
||||
@Autowired
|
||||
private GoodsService goodsService;
|
||||
@Autowired
|
||||
private ProjectMapper projectMapper;
|
||||
@@ -210,6 +216,8 @@ public class FaceServiceImpl implements FaceService {
|
||||
private OrderRepository orderRepository;
|
||||
@Autowired
|
||||
private com.ycwl.basic.biz.FaceStatusManager faceStatusManager;
|
||||
@Autowired
|
||||
private SourceRepository sourceRepository;
|
||||
|
||||
@Override
|
||||
public ApiResponse<PageInfo<FaceRespVO>> pageQuery(FaceReqQuery faceReqQuery) {
|
||||
@@ -418,6 +426,37 @@ public class FaceServiceImpl implements FaceService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联样本扩展
|
||||
* 根据 face_sample_association 表,将同组样本ID加入匹配结果
|
||||
*/
|
||||
private void expandSampleAssociation(SearchFaceRespVo searchResult, Long scenicId, Long faceId) {
|
||||
List<Long> sampleListIds = searchResult.getSampleListIds();
|
||||
if (sampleListIds == null || sampleListIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
List<Long> associatedIds = faceSampleAssociationMapper.findAssociatedSampleIds(scenicId, sampleListIds);
|
||||
if (associatedIds == null || associatedIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Set<Long> currentSet = new HashSet<>(sampleListIds);
|
||||
List<Long> netNewIds = associatedIds.stream()
|
||||
.filter(id -> !currentSet.contains(id))
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
if (!netNewIds.isEmpty()) {
|
||||
List<Long> expandedIds = new ArrayList<>(sampleListIds);
|
||||
expandedIds.addAll(netNewIds);
|
||||
searchResult.setSampleListIds(expandedIds);
|
||||
log.info("关联样本扩展完成: faceId={}, scenicId={}, 原始数={}, 新增数={}, 扩展后={}",
|
||||
faceId, scenicId, sampleListIds.size(), netNewIds.size(), expandedIds.size());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("关联样本扩展失败, faceId={}, scenicId={}", faceId, scenicId, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新人脸实体结果信息
|
||||
* 仅用于 handleCustomFaceMatching 方法
|
||||
@@ -472,7 +511,75 @@ public class FaceServiceImpl implements FaceService {
|
||||
if (face == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
Long userId = face.getMemberId();
|
||||
Long userId = Long.parseLong(BaseContextHandler.getUserId());
|
||||
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(face.getScenicId());
|
||||
if (Integer.valueOf(2).equals(scenicConfig.getInteger("scenic_mode", 0))) {
|
||||
List<ContentPageVO> result = new ArrayList<>();
|
||||
// 摄影师拍照
|
||||
List<DeviceV2DTO> deviceList = deviceRepository.getAllDeviceByScenicId(face.getScenicId());
|
||||
List<SourceEntity> sourceEntityList = sourceMapper.listSourceByFaceRelation(face.getId(), 2);
|
||||
List<MemberSourceEntity> memberSourceRelations = memberRelationRepository.listSourceByFaceRelation(face.getId(), 2);
|
||||
// 按 deviceList 顺序排列结果
|
||||
Set<Long> deviceIds = deviceList.stream().map(DeviceV2DTO::getId).collect(Collectors.toSet());
|
||||
for (DeviceV2DTO device : deviceList) {
|
||||
List<SourceEntity> deviceSources = sourceEntityList.stream()
|
||||
.filter(s -> device.getId().equals(s.getDeviceId()))
|
||||
.toList();
|
||||
if (deviceSources.isEmpty()) {
|
||||
ContentPageVO content = new ContentPageVO();
|
||||
content.setName(device.getName());
|
||||
content.setGroup(device.getName());
|
||||
content.setContentId(device.getId());
|
||||
content.setGoodsType(2);
|
||||
content.setContentType(2);
|
||||
content.setScenicId(face.getScenicId());
|
||||
content.setSourceType(2);
|
||||
content.setTemplateCoverUrl("");
|
||||
content.setIsBuy(0);
|
||||
content.setLockType(1);
|
||||
result.add(content);
|
||||
} else {
|
||||
for (SourceEntity sourceEntity : deviceSources) {
|
||||
ContentPageVO content = new ContentPageVO();
|
||||
content.setName("摄影师拍照");
|
||||
content.setGroup(device.getName());
|
||||
content.setContentId(sourceEntity.getId());
|
||||
content.setGoodsType(2);
|
||||
content.setContentType(2);
|
||||
content.setScenicId(sourceEntity.getScenicId());
|
||||
content.setSourceType(2);
|
||||
content.setOrigUrl(sourceEntity.getUrl());
|
||||
content.setTemplateCoverUrl(sourceEntity.getThumbUrl());
|
||||
memberSourceRelations.stream().filter(relation -> relation.getSourceId().equals(sourceEntity.getId())).findAny().ifPresent(relation -> {
|
||||
content.setIsBuy(relation.getIsBuy());
|
||||
});
|
||||
content.setLockType(-1);
|
||||
result.add(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 补充不属于任何设备的源
|
||||
for (SourceEntity sourceEntity : sourceEntityList) {
|
||||
if (sourceEntity.getDeviceId() != null && deviceIds.contains(sourceEntity.getDeviceId())) {
|
||||
continue;
|
||||
}
|
||||
ContentPageVO content = new ContentPageVO();
|
||||
content.setName("摄影师拍照");
|
||||
content.setContentId(sourceEntity.getId());
|
||||
content.setGoodsType(2);
|
||||
content.setContentType(2);
|
||||
content.setScenicId(sourceEntity.getScenicId());
|
||||
content.setSourceType(2);
|
||||
content.setOrigUrl(sourceEntity.getUrl());
|
||||
content.setTemplateCoverUrl(sourceEntity.getThumbUrl());
|
||||
memberSourceRelations.stream().filter(relation -> relation.getSourceId().equals(sourceEntity.getId())).findAny().ifPresent(relation -> {
|
||||
content.setIsBuy(relation.getIsBuy());
|
||||
});
|
||||
content.setLockType(-1);
|
||||
result.add(content);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
List<TemplateRespVO> templateList = templateRepository.getTemplateListByScenicId(face.getScenicId());
|
||||
List<ContentPageVO> contentList = templateList.stream().map(template -> {
|
||||
/// select t.id templateId, t.scenic_id, t.`group`, t.`name`, pid, t.cover_url templateCoverUrl,
|
||||
@@ -607,7 +714,6 @@ public class FaceServiceImpl implements FaceService {
|
||||
sourceVideoContent.setGroup("直出原片");
|
||||
sourceImageContent.setGroup("直出原片");
|
||||
sourceAiCamContent.setGroup("智能连连拍");
|
||||
ScenicConfigManager configManager = scenicRepository.getScenicConfigManager(face.getScenicId());
|
||||
if (!scenicConfigFacade.isDisableSourceImage(face.getScenicId())) {
|
||||
IsBuyRespVO isBuyRespVO = orderBiz.isBuy(face.getScenicId(), userId, faceId, SourceType.IMAGE.getCode(), faceId);
|
||||
sourceImageContent.setSourceType(isBuyRespVO.getGoodsType());
|
||||
@@ -679,7 +785,7 @@ public class FaceServiceImpl implements FaceService {
|
||||
} else if (type == 3) {
|
||||
sourceAiCamContent.setSourceType(13);
|
||||
sourceAiCamContent.setLockType(-1);
|
||||
sourceAiCamContent.setTemplateCoverUrl(configManager.getString("ai_camera_cover_url"));
|
||||
sourceAiCamContent.setTemplateCoverUrl(scenicConfig.getString("ai_camera_cover_url"));
|
||||
}
|
||||
});
|
||||
return contentList;
|
||||
@@ -780,26 +886,44 @@ public class FaceServiceImpl implements FaceService {
|
||||
sourceReqQuery.setMemberId(face.getMemberId());
|
||||
sourceReqQuery.setFaceId(faceId);
|
||||
sourceReqQuery.setType(2);
|
||||
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(face.getScenicId());
|
||||
List<MemberSourceEntity> countUser = memberRelationRepository.listSourceByFaceRelation(faceId, 2);
|
||||
if (countUser != null && !countUser.isEmpty()) {
|
||||
statusResp.setStep2Status(true);
|
||||
} else {
|
||||
statusResp.setStep2Status(false);
|
||||
statusResp.setDisplayText("Hey,快去智能机位打卡吧");
|
||||
if (Integer.valueOf(2).equals(scenicConfig.getInteger("scenic_mode", 0))) {
|
||||
statusResp.setDisplayText("Hey,去拍摄点免费拍照吧");
|
||||
} else {
|
||||
statusResp.setDisplayText("Hey,快去智能机位打卡吧");
|
||||
}
|
||||
return statusResp;
|
||||
}
|
||||
VideoTaskStatusVO taskStatusByFaceId = goodsService.getTaskStatusByFaceId(faceId);
|
||||
if (Integer.valueOf(1).equals(taskStatusByFaceId.getStatus())) {
|
||||
if (taskStatusByFaceId.getCount() > 0) {
|
||||
if (Integer.valueOf(2).equals(scenicConfig.getInteger("scenic_mode", 0))) {
|
||||
// 摄影模式
|
||||
if (!countUser.isEmpty()) {
|
||||
statusResp.setStep3Status(true);
|
||||
statusResp.setDisplayText("帧途AI已为您渲染"+ taskStatusByFaceId.getCount() +"个vlog");
|
||||
statusResp.setDisplayText("已为您拍摄" + countUser.size() + "张照片");
|
||||
return statusResp;
|
||||
} else {
|
||||
statusResp.setStep3Status(false);
|
||||
statusResp.setDisplayText("帧途AI将会为您渲染vlog,请稍候");
|
||||
statusResp.setDisplayText("Hey,去拍摄点免费拍照吧");
|
||||
return statusResp;
|
||||
}
|
||||
} else {
|
||||
statusResp.setStep3Status(false);
|
||||
statusResp.setDisplayText("帧途AI正在为您渲染vlog,请稍候");
|
||||
VideoTaskStatusVO taskStatusByFaceId = goodsService.getTaskStatusByFaceId(faceId);
|
||||
if (Integer.valueOf(1).equals(taskStatusByFaceId.getStatus())) {
|
||||
if (taskStatusByFaceId.getCount() > 0) {
|
||||
statusResp.setStep3Status(true);
|
||||
statusResp.setDisplayText("帧途AI已为您渲染"+ taskStatusByFaceId.getCount() +"个vlog");
|
||||
} else {
|
||||
statusResp.setStep3Status(false);
|
||||
statusResp.setDisplayText("帧途AI将会为您渲染vlog,请稍候");
|
||||
}
|
||||
} else {
|
||||
statusResp.setStep3Status(false);
|
||||
statusResp.setDisplayText("帧途AI正在为您渲染vlog,请稍候");
|
||||
}
|
||||
}
|
||||
return statusResp;
|
||||
}
|
||||
@@ -996,6 +1120,9 @@ public class FaceServiceImpl implements FaceService {
|
||||
allFaceSampleList.size(), filteredSampleIds.size());
|
||||
}
|
||||
|
||||
// 4. 关联样本扩展
|
||||
expandSampleAssociation(mergedResult, face.getScenicId(), faceId);
|
||||
|
||||
updateFaceEntityResult(face, mergedResult, faceId);
|
||||
|
||||
List<Long> sampleListIds = mergedResult.getSampleListIds();
|
||||
|
||||
@@ -44,6 +44,7 @@ import com.ycwl.basic.model.pc.price.entity.PriceConfigEntity;
|
||||
import com.ycwl.basic.model.pc.price.resp.GoodsListRespVO;
|
||||
import com.ycwl.basic.model.pc.printer.resp.MemberPrintResp;
|
||||
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
|
||||
import com.ycwl.basic.model.pc.source.resp.SourceRespVO;
|
||||
import com.ycwl.basic.model.pc.task.entity.TaskEntity;
|
||||
import com.ycwl.basic.model.pc.template.resp.TemplateRespVO;
|
||||
import com.ycwl.basic.model.pc.video.entity.VideoEntity;
|
||||
@@ -423,6 +424,21 @@ public class OrderServiceImpl implements OrderService {
|
||||
goods.setTemplateCoverUrl(item.getCoverUrl());
|
||||
goods.setScenicId(order.getScenicId());
|
||||
goodsList.add(goods);
|
||||
} else if (Integer.valueOf(14).equals(item.getGoodsType())) { // 单张照片 goodsId就是sourceId
|
||||
SourceRespVO source = sourceMapper.getById(item.getGoodsId());
|
||||
if (source != null) {
|
||||
item.setCoverList(Collections.singletonList(source.getUrl()));
|
||||
GoodsDetailVO goods = new GoodsDetailVO();
|
||||
goods.setGoodsId(source.getId());
|
||||
goods.setGoodsName("单张照片");
|
||||
goods.setUrl(source.getUrl());
|
||||
goods.setGoodsType(14);
|
||||
goods.setScenicId(source.getScenicId());
|
||||
goods.setTemplateCoverUrl(source.getUrl());
|
||||
goods.setCreateTime(source.getCreateTime());
|
||||
goodsList.add(goods);
|
||||
item.setShootingTime(source.getCreateTime());
|
||||
}
|
||||
} else {
|
||||
item.setCoverList(Collections.singletonList(item.getCoverUrl()));
|
||||
VideoEntity videoMapperById = videoRepository.getVideo(item.getGoodsId());
|
||||
@@ -930,6 +946,7 @@ public class OrderServiceImpl implements OrderService {
|
||||
Integer type = switch (productItem.getProductType()) {
|
||||
case PHOTO_LOG -> 5;
|
||||
case PHOTO_SET -> 2;
|
||||
case PHOTO -> 14;
|
||||
case VLOG_VIDEO -> 0;
|
||||
case RECORDING_SET -> 1;
|
||||
case AI_CAM_PHOTO_SET -> 13;
|
||||
@@ -937,6 +954,7 @@ public class OrderServiceImpl implements OrderService {
|
||||
};
|
||||
Long goodsId = switch (productItem.getProductType()) {
|
||||
case PHOTO_LOG -> Long.valueOf(productItem.getProductId());
|
||||
case PHOTO -> Long.valueOf(productItem.getProductId());
|
||||
case PHOTO_SET, RECORDING_SET -> face.getId();
|
||||
case AI_CAM_PHOTO_SET -> face.getId();
|
||||
case VLOG_VIDEO -> {
|
||||
|
||||
@@ -18,6 +18,7 @@ import com.ycwl.basic.exception.BaseException;
|
||||
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
|
||||
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
|
||||
import com.ycwl.basic.mapper.FaceMapper;
|
||||
import com.ycwl.basic.mapper.FaceSampleAssociationMapper;
|
||||
import com.ycwl.basic.mapper.SourceMapper;
|
||||
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
||||
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
|
||||
@@ -41,8 +42,11 @@ import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
@@ -107,6 +111,8 @@ public class FaceMatchingOrchestrator {
|
||||
private FaceStatusManager faceStatusManager;
|
||||
@Autowired
|
||||
private PuzzleRepository puzzleRepository;
|
||||
@Autowired
|
||||
private FaceSampleAssociationMapper faceSampleAssociationMapper;
|
||||
|
||||
/**
|
||||
* 编排人脸匹配的完整流程
|
||||
@@ -146,6 +152,9 @@ public class FaceMatchingOrchestrator {
|
||||
searchResult = faceRecoveryStrategy.executeFaceRecoveryLogic(
|
||||
searchResult, context.scenicConfig, context.faceBodyAdapter, context.face.getScenicId());
|
||||
|
||||
// 关联样本扩展
|
||||
expandSampleAssociation(searchResult, context.face.getScenicId(), faceId);
|
||||
|
||||
// 步骤3: 更新人脸结果
|
||||
updateFaceResult(context.face, searchResult, faceId);
|
||||
|
||||
@@ -396,7 +405,9 @@ public class FaceMatchingOrchestrator {
|
||||
baseDynamicData.put("faceId", String.valueOf(faceId));
|
||||
baseDynamicData.put("scenicName", scenicBasic.getName());
|
||||
baseDynamicData.put("scenicText", scenicBasic.getName());
|
||||
baseDynamicData.put("dateStr", DateUtil.format(new Date(), "yyyy.MM.dd"));
|
||||
Date maxCreateTime = sourceMapper.getMaxCreateTimeByFaceId(faceId);
|
||||
baseDynamicData.put("dateStr", DateUtil.format(
|
||||
maxCreateTime != null ? maxCreateTime : new Date(), "yyyy.MM.dd"));
|
||||
|
||||
templateList
|
||||
.forEach(template -> {
|
||||
@@ -428,6 +439,40 @@ public class FaceMatchingOrchestrator {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联样本扩展
|
||||
* 根据 face_sample_association 表,将同组样本ID加入匹配结果
|
||||
*/
|
||||
private void expandSampleAssociation(SearchFaceRespVo searchResult, Long scenicId, Long faceId) {
|
||||
List<Long> sampleListIds = searchResult.getSampleListIds();
|
||||
if (sampleListIds == null || sampleListIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
List<Long> associatedIds = faceSampleAssociationMapper.findAssociatedSampleIds(scenicId, sampleListIds);
|
||||
if (associatedIds == null || associatedIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Set<Long> currentSet = new HashSet<>(sampleListIds);
|
||||
List<Long> netNewIds = associatedIds.stream()
|
||||
.filter(id -> !currentSet.contains(id))
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (!netNewIds.isEmpty()) {
|
||||
List<Long> expandedIds = new ArrayList<>(sampleListIds);
|
||||
expandedIds.addAll(netNewIds);
|
||||
searchResult.setSampleListIds(expandedIds);
|
||||
log.info("关联样本扩展完成: faceId={}, scenicId={}, 原始数={}, 新增数={}, 扩展后={}",
|
||||
faceId, scenicId, sampleListIds.size(), netNewIds.size(), expandedIds.size());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("关联样本扩展失败, faceId={}, scenicId={}", faceId, scenicId, e);
|
||||
// 扩展失败不影响主流程
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 匹配上下文
|
||||
* 封装匹配过程中需要的所有上下文信息
|
||||
|
||||
@@ -157,6 +157,19 @@ public interface PrinterService {
|
||||
*/
|
||||
Map<String, Object> createVirtualOrder(Long sourceId, Long scenicId, Integer printerId, Boolean needEnhance, String printImgUrl, Boolean needActualPayment);
|
||||
|
||||
/**
|
||||
* 批量创建虚拟用户订单(多个sourceId聚合为一笔订单、一次支付)
|
||||
*
|
||||
* @param sourceIds source记录ID列表
|
||||
* @param scenicId 景区ID
|
||||
* @param printerId 打印机ID(可选)
|
||||
* @param needEnhance 是否需要图像增强(可选)
|
||||
* @param printImgUrl 打印图片URL(可选)
|
||||
* @param needActualPayment 是否需要实际支付
|
||||
* @return 订单信息
|
||||
*/
|
||||
Map<String, Object> createBatchVirtualOrder(List<Long> sourceIds, Long scenicId, Integer printerId, Boolean needEnhance, String printImgUrl, Boolean needActualPayment);
|
||||
|
||||
/**
|
||||
* 根据accessKey获取打印机详情
|
||||
* @param accessKey 打印机accessKey
|
||||
|
||||
@@ -1931,6 +1931,194 @@ public class PrinterServiceImpl implements PrinterService {
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> createBatchVirtualOrder(List<Long> sourceIds, Long scenicId, Integer printerId, Boolean needEnhance, String printImgUrl, Boolean needActualPayment) {
|
||||
if (sourceIds == null || sourceIds.isEmpty()) {
|
||||
throw new BaseException("sourceIds不能为空");
|
||||
}
|
||||
|
||||
// 1. 校验所有source并收集faceSample
|
||||
List<SourceEntity> sources = new ArrayList<>();
|
||||
FaceSampleEntity firstFaceSample = null;
|
||||
for (Long sourceId : sourceIds) {
|
||||
SourceEntity source = sourceMapper.getEntity(sourceId);
|
||||
if (source == null) {
|
||||
throw new BaseException("Source记录不存在: " + sourceId);
|
||||
}
|
||||
if (!scenicId.equals(source.getScenicId())) {
|
||||
throw new BaseException("Source记录不属于该景区: " + sourceId);
|
||||
}
|
||||
FaceSampleEntity faceSample = faceSampleMapper.getEntity(source.getFaceSampleId());
|
||||
if (faceSample == null) {
|
||||
throw new BaseException("人脸样本不存在, sourceId=" + sourceId);
|
||||
}
|
||||
if (firstFaceSample == null) {
|
||||
firstFaceSample = faceSample;
|
||||
}
|
||||
sources.add(source);
|
||||
}
|
||||
|
||||
// 2. 生成一个虚拟用户 + 一条人脸记录
|
||||
Long virtualMemberId = SnowFlakeUtil.getLongId();
|
||||
Long faceId = SnowFlakeUtil.getLongId();
|
||||
FaceEntity face = new FaceEntity();
|
||||
face.setId(faceId);
|
||||
face.setScenicId(scenicId);
|
||||
face.setMemberId(virtualMemberId);
|
||||
face.setFaceUrl(firstFaceSample.getFaceUrl());
|
||||
face.setCreateAt(new Date());
|
||||
faceMapper.add(face);
|
||||
log.info("批量下单 - 创建虚拟用户: virtualMemberId={}, faceId={}, sourceCount={}", virtualMemberId, faceId, sourceIds.size());
|
||||
|
||||
// 3. 为每个source创建member_print记录
|
||||
List<Integer> memberPrintIds = new ArrayList<>();
|
||||
for (SourceEntity source : sources) {
|
||||
String photoUrl = (printImgUrl != null && !printImgUrl.isEmpty()) ? printImgUrl : source.getUrl();
|
||||
Integer memberPrintId = addUserPhoto(virtualMemberId, scenicId, photoUrl, faceId, source.getId());
|
||||
if (memberPrintId == null) {
|
||||
throw new BaseException("创建member_print记录失败, sourceId=" + source.getId());
|
||||
}
|
||||
setPhotoQuantity(virtualMemberId, scenicId, memberPrintId.longValue(), 1);
|
||||
memberPrintIds.add(memberPrintId);
|
||||
}
|
||||
|
||||
// 4. 验证打印机
|
||||
if (printerId == null) {
|
||||
List<PrinterResp> printerList = printerMapper.listByScenicId(scenicId);
|
||||
if (printerList.isEmpty()) {
|
||||
throw new BaseException("该景区没有可用的打印机");
|
||||
}
|
||||
if (printerList.size() != 1) {
|
||||
throw new BaseException("请选择打印机");
|
||||
}
|
||||
printerId = printerList.getFirst().getId();
|
||||
}
|
||||
PrinterEntity printer = printerMapper.getById(printerId);
|
||||
if (printer == null) {
|
||||
throw new BaseException("打印机不存在");
|
||||
}
|
||||
if (printer.getStatus() != 1) {
|
||||
throw new BaseException("打印机已停用");
|
||||
}
|
||||
if (!printer.getScenicId().equals(scenicId)) {
|
||||
throw new BaseException("打印机不属于该景区");
|
||||
}
|
||||
|
||||
// 5. 创建订单
|
||||
OrderEntity order = new OrderEntity();
|
||||
Long orderId = SnowFlakeUtil.getLongId();
|
||||
redisTemplate.opsForValue().set("printer_size:" + orderId, printer.getPreferPaper(), 60, TimeUnit.SECONDS);
|
||||
order.setId(orderId);
|
||||
order.setMemberId(virtualMemberId);
|
||||
order.setFaceId(faceId);
|
||||
order.setOpenId("");
|
||||
order.setScenicId(scenicId);
|
||||
order.setType(3);
|
||||
|
||||
batchSetUserPhotoListToPrinter(virtualMemberId, scenicId, printerId);
|
||||
|
||||
List<MemberPrintResp> userPhotoList = getUserPhotoList(virtualMemberId, scenicId, faceId);
|
||||
List<OrderItemEntity> orderItems = userPhotoList.stream().map(goods -> {
|
||||
OrderItemEntity orderItem = new OrderItemEntity();
|
||||
orderItem.setOrderId(orderId);
|
||||
orderItem.setGoodsId(Long.valueOf(goods.getId()));
|
||||
orderItem.setGoodsType(3);
|
||||
return orderItem;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
boolean actualPayment = Boolean.TRUE.equals(needActualPayment);
|
||||
|
||||
if (actualPayment) {
|
||||
PriceCalculationRequest priceRequest = new PriceCalculationRequest();
|
||||
priceRequest.setUserId(virtualMemberId);
|
||||
priceRequest.setScenicId(scenicId);
|
||||
|
||||
List<ProductItem> productItems = new ArrayList<>();
|
||||
ProductItem photoItem = new ProductItem();
|
||||
photoItem.setProductType(ProductType.PHOTO_PRINT);
|
||||
photoItem.setProductId(scenicId.toString());
|
||||
photoItem.setQuantity(1);
|
||||
photoItem.setPurchaseCount(sourceIds.size());
|
||||
photoItem.setScenicId(scenicId.toString());
|
||||
productItems.add(photoItem);
|
||||
|
||||
priceRequest.setProducts(productItems);
|
||||
priceRequest.setAutoUseCoupon(false);
|
||||
priceRequest.setPreviewOnly(false);
|
||||
|
||||
PriceCalculationResult priceResult = priceCalculationService.calculatePrice(priceRequest);
|
||||
|
||||
order.setPrice(priceResult.getFinalAmount());
|
||||
order.setSlashPrice(priceResult.getOriginalAmount());
|
||||
order.setPayPrice(priceResult.getFinalAmount());
|
||||
order.setStatus(OrderStateEnum.UNPAID.getState());
|
||||
log.info("批量下单 - 待支付订单: orderId={}, price={}, count={}", orderId, priceResult.getFinalAmount(), sourceIds.size());
|
||||
|
||||
if (needEnhance != null) {
|
||||
redisTemplate.opsForValue().set("virtual_order_enhance:" + orderId, needEnhance.toString(), 24, TimeUnit.HOURS);
|
||||
}
|
||||
} else {
|
||||
order.setPrice(BigDecimal.ZERO);
|
||||
order.setSlashPrice(BigDecimal.ZERO);
|
||||
order.setPayPrice(BigDecimal.ZERO);
|
||||
order.setStatus(OrderStateEnum.PAID.getState());
|
||||
order.setPayAt(new Date());
|
||||
}
|
||||
|
||||
orderMapper.add(order);
|
||||
int addOrderItems = orderMapper.addOrderItems(orderItems);
|
||||
if (addOrderItems == NumberConstant.ZERO) {
|
||||
throw new BaseException("订单添加失败");
|
||||
}
|
||||
|
||||
log.info("批量下单 - 订单创建成功: orderId={}, itemCount={}", orderId, orderItems.size());
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("orderId", orderId);
|
||||
result.put("faceId", faceId);
|
||||
result.put("virtualMemberId", virtualMemberId);
|
||||
result.put("memberPrintIds", memberPrintIds);
|
||||
result.put("sourceIds", sourceIds);
|
||||
|
||||
if (actualPayment) {
|
||||
if (order.getPayPrice().compareTo(BigDecimal.ZERO) <= 0) {
|
||||
order.setStatus(OrderStateEnum.PAID.getState());
|
||||
order.setPayAt(new Date());
|
||||
orderMapper.updateOrder(order);
|
||||
log.info("批量下单 - 价格为0直接完成: orderId={}", orderId);
|
||||
result.put("needPay", false);
|
||||
} else {
|
||||
IPayAdapter payAdapter = scenicService.getScenicPayAdapter(scenicId);
|
||||
if (payAdapter instanceof WxMpPayAdapter adapter) {
|
||||
NativePayService nativePayService = new NativePayService.Builder().config(adapter.getConfig()).build();
|
||||
PrepayRequest prepayRequest = new PrepayRequest();
|
||||
prepayRequest.setAppid(adapter._config().getAppId());
|
||||
prepayRequest.setMchid(adapter._config().getMerchantId());
|
||||
prepayRequest.setDescription("照片打印 x" + sourceIds.size());
|
||||
prepayRequest.setOutTradeNo(String.valueOf(orderId));
|
||||
prepayRequest.setNotifyUrl("https://zhentuai.com/api/mobile/wx/pay/v1/" + scenicId + "/payNotify");
|
||||
Amount amount = new Amount();
|
||||
amount.setTotal(order.getPayPrice().multiply(new BigDecimal(100)).intValue());
|
||||
prepayRequest.setAmount(amount);
|
||||
PrepayResponse prepayResponse = nativePayService.prepay(prepayRequest);
|
||||
result.put("payCode", prepayResponse.getCodeUrl());
|
||||
} else {
|
||||
throw new BaseException("该景区不支持 Native 支付");
|
||||
}
|
||||
result.put("needPay", true);
|
||||
result.put("price", order.getPayPrice());
|
||||
}
|
||||
} else {
|
||||
result.put("needPay", false);
|
||||
}
|
||||
|
||||
// 触发购买后逻辑(setUserIsBuyItem 内部遍历 orderItems 处理所有 memberPrint)
|
||||
setUserIsBuyItem(virtualMemberId, memberPrintIds.getFirst().longValue(), orderId, needEnhance);
|
||||
log.info("批量下单 - 购买后逻辑完成: orderId={}", orderId);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrinterEntity getByAccessKey(String accessKey) {
|
||||
if (accessKey == null) {
|
||||
|
||||
@@ -85,7 +85,7 @@ public class DownloadNotificationTasker {
|
||||
}
|
||||
variables.put("videoDeviceCount", videoTaskRepository.getTaskDeviceNum(item.getTaskId()));
|
||||
variables.put("videoLensCount", videoTaskRepository.getTaskLensNum(item.getTaskId()));
|
||||
variables.put("videoShotTime", DateUtil.format(videoTaskRepository.getTaskShotDate(item.getTaskId()), "yyyy-MM-dd"));
|
||||
variables.put("videoShotTime", DateUtil.format(videoTaskRepository.getTaskShotDate(item.getTaskId()), "yyyy-MM-dd HH:mm"));
|
||||
WechatSubscribeNotifyTriggerRequest request = WechatSubscribeNotifyTriggerRequest.builder()
|
||||
.scenicId(item.getScenicId())
|
||||
.memberId(item.getMemberId())
|
||||
@@ -137,7 +137,7 @@ public class DownloadNotificationTasker {
|
||||
} else {
|
||||
variables.put("videoResultPage", "videoSynthesis");
|
||||
}
|
||||
variables.put("expireDate", DateUtil.format(expireDate, "yyyy-MM-dd"));
|
||||
variables.put("expireDate", DateUtil.format(expireDate, "yyyy-MM-dd HH:mm"));
|
||||
variables.put("videoDeviceCount", videoTaskRepository.getTaskDeviceNum(item.getTaskId()));
|
||||
variables.put("videoLensCount", videoTaskRepository.getTaskLensNum(item.getTaskId()));
|
||||
variables.put("videoShotTime", DateUtil.format(videoTaskRepository.getTaskShotDate(item.getTaskId()), "yyyy-MM-dd HH:mm"));
|
||||
@@ -191,7 +191,7 @@ public class DownloadNotificationTasker {
|
||||
} else {
|
||||
variables.put("videoResultPage", "videoSynthesis");
|
||||
}
|
||||
variables.put("expireDate", DateUtil.format(expireDate, "yyyy-MM-dd"));
|
||||
variables.put("expireDate", DateUtil.format(expireDate, "yyyy-MM-dd HH:mm"));
|
||||
variables.put("videoDeviceCount", videoTaskRepository.getTaskDeviceNum(item.getTaskId()));
|
||||
variables.put("videoLensCount", videoTaskRepository.getTaskLensNum(item.getTaskId()));
|
||||
variables.put("videoShotTime", DateUtil.format(videoTaskRepository.getTaskShotDate(item.getTaskId()), "yyyy-MM-dd HH:mm"));
|
||||
|
||||
@@ -64,10 +64,10 @@ public class SourceNotificationTasker {
|
||||
variables.put("faceId", item.getId());
|
||||
List<MemberSourceEntity> sourceVideoList = memberRelationRepository.listSourceByFaceRelation(item.getId(), 1);
|
||||
variables.put("sourceVideoCount", sourceVideoList.size());
|
||||
variables.put("sourceVideoCreateTime", DateUtil.format(item.getCreateAt(), "yyyy-MM-dd"));
|
||||
variables.put("sourceVideoCreateTime", DateUtil.format(item.getCreateAt(), "yyyy-MM-dd HH:mm"));
|
||||
List<MemberSourceEntity> sourcePhotoList = memberRelationRepository.listSourceByFaceRelation(item.getId(), 2);
|
||||
variables.put("sourcePhotoCount", sourcePhotoList.size());
|
||||
variables.put("sourcePhotoCreateTime", DateUtil.format(item.getCreateAt(), "yyyy-MM-dd"));
|
||||
variables.put("sourcePhotoCreateTime", DateUtil.format(item.getCreateAt(), "yyyy-MM-dd HH:mm"));
|
||||
|
||||
WechatSubscribeNotifyTriggerRequest request = WechatSubscribeNotifyTriggerRequest.builder()
|
||||
.scenicId(item.getScenicId())
|
||||
|
||||
@@ -342,7 +342,7 @@ public class VideoPieceGetter {
|
||||
ffmpegTask.setDuration(duration);
|
||||
ffmpegTask.setOffsetStart(BigDecimal.valueOf(offset, 3));
|
||||
// 使用时间戳和线程ID确保输出文件名唯一性,避免并发冲突
|
||||
String uniqueSuffix = System.currentTimeMillis() + "_" + Thread.currentThread().getId();
|
||||
String uniqueSuffix = System.currentTimeMillis() + "_" + Thread.currentThread().threadId();
|
||||
File outFile = new File(deviceId.toString() + "_" + faceSampleId + "_" + uniqueSuffix + ".mp4");
|
||||
ffmpegTask.setOutputFile(outFile.getAbsolutePath());
|
||||
boolean result = startFfmpegTask(ffmpegTask);
|
||||
|
||||
58
src/main/resources/mapper/FaceSampleAssociationMapper.xml
Normal file
58
src/main/resources/mapper/FaceSampleAssociationMapper.xml
Normal file
@@ -0,0 +1,58 @@
|
||||
<?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.FaceSampleAssociationMapper">
|
||||
|
||||
<select id="findAssociatedSampleIds" resultType="java.lang.Long">
|
||||
SELECT DISTINCT a2.face_sample_id
|
||||
FROM face_sample_association a1
|
||||
INNER JOIN face_sample_association a2
|
||||
ON a1.scenic_id = a2.scenic_id
|
||||
AND a1.group_key = a2.group_key
|
||||
WHERE a1.scenic_id = #{scenicId}
|
||||
AND a1.face_sample_id IN
|
||||
<foreach collection="sampleIds" item="sampleId" open="(" separator="," close=")">
|
||||
#{sampleId}
|
||||
</foreach>
|
||||
</select>
|
||||
|
||||
<insert id="batchInsertIgnore">
|
||||
INSERT IGNORE INTO face_sample_association (scenic_id, group_key, face_sample_id)
|
||||
VALUES
|
||||
<foreach collection="list" item="item" separator=",">
|
||||
(#{item.scenicId}, #{item.groupKey}, #{item.faceSampleId})
|
||||
</foreach>
|
||||
</insert>
|
||||
|
||||
<delete id="deleteByGroupAndSampleIds">
|
||||
DELETE FROM face_sample_association
|
||||
WHERE scenic_id = #{scenicId}
|
||||
AND group_key = #{groupKey}
|
||||
AND face_sample_id IN
|
||||
<foreach collection="sampleIds" item="sampleId" open="(" separator="," close=")">
|
||||
#{sampleId}
|
||||
</foreach>
|
||||
</delete>
|
||||
|
||||
<delete id="deleteByGroup">
|
||||
DELETE FROM face_sample_association
|
||||
WHERE scenic_id = #{scenicId}
|
||||
AND group_key = #{groupKey}
|
||||
</delete>
|
||||
|
||||
<select id="listSampleIdsByGroup" resultType="java.lang.Long">
|
||||
SELECT face_sample_id
|
||||
FROM face_sample_association
|
||||
WHERE scenic_id = #{scenicId}
|
||||
AND group_key = #{groupKey}
|
||||
ORDER BY create_at ASC
|
||||
</select>
|
||||
|
||||
<select id="listGroupKeysByScenicId" resultType="java.lang.String">
|
||||
SELECT DISTINCT group_key
|
||||
FROM face_sample_association
|
||||
WHERE scenic_id = #{scenicId}
|
||||
ORDER BY group_key ASC
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -115,6 +115,13 @@
|
||||
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
|
||||
left join puzzle_template pt on gr.template_id = pt.id
|
||||
),
|
||||
member_single_photo_data AS (
|
||||
SELECT 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.deleted = 0
|
||||
)
|
||||
SELECT
|
||||
oi.id AS oiId,
|
||||
@@ -136,6 +143,7 @@
|
||||
WHEN '4' THEN '一体机照片打印'
|
||||
WHEN '5' THEN 'pLog'
|
||||
WHEN '13' THEN '打卡点拍照'
|
||||
WHEN '14' THEN '单张照片'
|
||||
ELSE '其他'
|
||||
END AS goods_name,
|
||||
CASE oi.goods_type
|
||||
@@ -143,12 +151,14 @@
|
||||
WHEN '1' THEN oi.goods_id
|
||||
WHEN '2' THEN oi.goods_id
|
||||
WHEN '13' THEN oi.goods_id
|
||||
WHEN '14' THEN mspd.face_id
|
||||
END AS face_id,
|
||||
CASE oi.goods_type
|
||||
WHEN '0' THEN mvd.face_url
|
||||
WHEN '1' THEN msd.face_url
|
||||
WHEN '2' THEN msd.face_url
|
||||
WHEN '13' THEN msac.face_url
|
||||
WHEN '14' THEN mspd.face_url
|
||||
END AS face_url,
|
||||
CASE oi.goods_type
|
||||
WHEN '0' THEN mvd.video_url
|
||||
@@ -161,6 +171,7 @@
|
||||
WHEN '4' THEN mpa.url
|
||||
WHEN '5' THEN mpl.url
|
||||
WHEN '13' THEN msac.url
|
||||
WHEN '14' THEN mspd.url
|
||||
END AS imgUrl
|
||||
FROM order_item oi
|
||||
LEFT JOIN `order` o ON oi.order_id = o.id
|
||||
@@ -170,6 +181,7 @@
|
||||
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_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
|
||||
LEFT JOIN member_single_photo_data mspd ON oi.goods_id = mspd.source_id AND o.face_id = mspd.face_id AND oi.goods_type = 14
|
||||
WHERE oi.order_id = #{id};
|
||||
</select>
|
||||
|
||||
|
||||
@@ -128,15 +128,15 @@
|
||||
WHERE id = #{id}
|
||||
</update>
|
||||
|
||||
<!-- 根据内容哈希查询历史记录(用于去重) -->
|
||||
<!-- 根据内容哈希查询历史记录(用于去重,同时匹配成功和生成中的记录,防止并发重复写入) -->
|
||||
<select id="findByContentHash" resultMap="BaseResultMap">
|
||||
SELECT <include refid="Base_Column_List"/>
|
||||
FROM puzzle_generation_record
|
||||
WHERE template_id = #{templateId}
|
||||
AND content_hash = #{contentHash}
|
||||
AND scenic_id = #{scenicId}
|
||||
AND status = 1
|
||||
ORDER BY create_time DESC
|
||||
AND (status = 1 OR (status = 0 AND create_time > DATE_SUB(NOW(), INTERVAL 5 MINUTE)))
|
||||
ORDER BY status DESC, create_time DESC
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
|
||||
@@ -464,6 +464,15 @@
|
||||
ORDER BY s.device_id ASC
|
||||
</select>
|
||||
|
||||
<select id="getMaxCreateTimeByFaceId" resultType="java.util.Date">
|
||||
SELECT MAX(s.create_time)
|
||||
FROM member_source ms
|
||||
INNER JOIN source s ON ms.source_id = s.id
|
||||
WHERE ms.face_id = #{faceId}
|
||||
AND s.type = 2
|
||||
AND ms.deleted = 0
|
||||
</select>
|
||||
|
||||
<select id="getSourceByFaceAndDeviceId" resultType="com.ycwl.basic.model.pc.source.entity.SourceEntity">
|
||||
SELECT s.*
|
||||
FROM source s
|
||||
@@ -595,4 +604,15 @@
|
||||
GROUP BY FLOOR(UNIX_TIMESTAMP(create_time) / 300)
|
||||
ORDER BY time
|
||||
</select>
|
||||
<select id="getMemberSourceByMemberAndSourceId" resultType="com.ycwl.basic.model.pc.source.entity.MemberSourceEntity">
|
||||
SELECT * FROM member_source WHERE member_id = #{memberId} AND source_id = #{sourceId} AND deleted = 0 LIMIT 1
|
||||
</select>
|
||||
<update id="updateRelationBySourceId">
|
||||
update member_source
|
||||
<set>
|
||||
<if test="isBuy!=null">is_buy = #{isBuy}, </if>
|
||||
<if test="orderId!=null">order_id = #{orderId}, </if>
|
||||
</set>
|
||||
where member_id = #{memberId} and source_id = #{sourceId}
|
||||
</update>
|
||||
</mapper>
|
||||
|
||||
@@ -3,8 +3,11 @@ package com.ycwl.basic.pricing.service;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ycwl.basic.pricing.dto.CouponClaimRequest;
|
||||
import com.ycwl.basic.pricing.dto.CouponClaimResult;
|
||||
import com.ycwl.basic.pricing.dto.CouponUseRequest;
|
||||
import com.ycwl.basic.pricing.entity.PriceCouponClaimRecord;
|
||||
import com.ycwl.basic.pricing.entity.PriceCouponConfig;
|
||||
import com.ycwl.basic.pricing.enums.CouponStatus;
|
||||
import com.ycwl.basic.pricing.exception.CouponInvalidException;
|
||||
import com.ycwl.basic.pricing.mapper.PriceCouponClaimRecordMapper;
|
||||
import com.ycwl.basic.pricing.mapper.PriceCouponConfigMapper;
|
||||
import com.ycwl.basic.pricing.service.impl.CouponServiceImpl;
|
||||
@@ -96,6 +99,61 @@ class CouponServiceImplTest {
|
||||
assertEquals(CouponClaimResult.ERROR_COUPON_OUT_OF_STOCK, result.getErrorCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailToUseCouponWhenClaimRecordExpiredByClaimWindow() {
|
||||
PriceCouponConfig coupon = baseCoupon();
|
||||
when(couponConfigMapper.selectById(1L)).thenReturn(coupon);
|
||||
|
||||
PriceCouponClaimRecord expiredRecord = new PriceCouponClaimRecord();
|
||||
expiredRecord.setId(99L);
|
||||
expiredRecord.setStatus(CouponStatus.CLAIMED);
|
||||
expiredRecord.setExpireTime(new Date(System.currentTimeMillis() - 1_000L));
|
||||
when(couponClaimRecordMapper.selectUserCouponRecords(10L, 1L)).thenReturn(java.util.List.of(expiredRecord));
|
||||
|
||||
CouponUseRequest request = buildUseRequest();
|
||||
CouponInvalidException exception = assertThrows(CouponInvalidException.class, () -> couponService.useCoupon(request));
|
||||
|
||||
assertEquals("优惠券已过期", exception.getMessage());
|
||||
verify(couponConfigMapper, never()).incrementUsedQuantity(anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailToUseCouponWhenCouponGlobalValidityExpired() {
|
||||
PriceCouponConfig coupon = baseCoupon();
|
||||
coupon.setValidUntil(new Date(System.currentTimeMillis() - 1_000L));
|
||||
when(couponConfigMapper.selectById(1L)).thenReturn(coupon);
|
||||
|
||||
CouponUseRequest request = buildUseRequest();
|
||||
CouponInvalidException exception = assertThrows(CouponInvalidException.class, () -> couponService.useCoupon(request));
|
||||
|
||||
assertEquals("优惠券已过期", exception.getMessage());
|
||||
verify(couponConfigMapper, never()).incrementUsedQuantity(anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseCouponWhenClaimRecordAndGlobalWindowAreValid() {
|
||||
PriceCouponConfig coupon = baseCoupon();
|
||||
when(couponConfigMapper.selectById(1L)).thenReturn(coupon);
|
||||
when(couponConfigMapper.incrementUsedQuantity(1L)).thenReturn(1);
|
||||
|
||||
PriceCouponClaimRecord validClaimRecord = new PriceCouponClaimRecord();
|
||||
validClaimRecord.setId(99L);
|
||||
validClaimRecord.setStatus(CouponStatus.CLAIMED);
|
||||
validClaimRecord.setExpireTime(new Date(System.currentTimeMillis() + 10_000L));
|
||||
when(couponClaimRecordMapper.selectUserCouponRecords(10L, 1L)).thenReturn(java.util.List.of(validClaimRecord));
|
||||
|
||||
CouponUseRequest request = buildUseRequest();
|
||||
couponService.useCoupon(request);
|
||||
|
||||
verify(couponConfigMapper).incrementUsedQuantity(1L);
|
||||
verify(couponClaimRecordMapper).updateCouponStatus(
|
||||
eq(99L),
|
||||
eq(CouponStatus.USED),
|
||||
any(Date.class),
|
||||
eq("ORDER-1"),
|
||||
eq("SCENIC-1"));
|
||||
}
|
||||
|
||||
private CouponClaimRequest buildRequest() {
|
||||
CouponClaimRequest request = new CouponClaimRequest();
|
||||
request.setUserId(10L);
|
||||
@@ -104,6 +162,15 @@ class CouponServiceImplTest {
|
||||
return request;
|
||||
}
|
||||
|
||||
private CouponUseRequest buildUseRequest() {
|
||||
CouponUseRequest request = new CouponUseRequest();
|
||||
request.setUserId(10L);
|
||||
request.setCouponId(1L);
|
||||
request.setOrderId("ORDER-1");
|
||||
request.setScenicId("SCENIC-1");
|
||||
return request;
|
||||
}
|
||||
|
||||
private PriceCouponConfig baseCoupon() {
|
||||
PriceCouponConfig coupon = new PriceCouponConfig();
|
||||
coupon.setId(1L);
|
||||
|
||||
@@ -53,7 +53,7 @@ public class ReusableVoucherServiceTest {
|
||||
testVoucherCode.setCode("TEST123");
|
||||
testVoucherCode.setBatchId(1L);
|
||||
testVoucherCode.setScenicId(1L);
|
||||
testVoucherCode.setFaceId(1001L);
|
||||
testVoucherCode.setUserId(1001L);
|
||||
testVoucherCode.setStatus(VoucherCodeStatus.CLAIMED_AVAILABLE.getCode());
|
||||
testVoucherCode.setCurrentUseCount(1);
|
||||
testVoucherCode.setLastUsedTime(new Date());
|
||||
|
||||
Reference in New Issue
Block a user