You've already forked FrameTour-BE
Compare commits
50 Commits
0ed12af8c9
...
09d142aa98
| Author | SHA1 | Date | |
|---|---|---|---|
| 09d142aa98 | |||
| 143185926c | |||
| cbbdd02003 | |||
| 1110b5409b | |||
| 4ac59b1f31 | |||
| 671cad4687 | |||
| 90fb0df69c | |||
| 383f9c4a31 | |||
| 9a92a4943a | |||
| 959eb6077e | |||
| b2012f9209 | |||
| 533fb306ca | |||
| 701d7879a8 | |||
| 1f302aefd6 | |||
| 78c45343d6 | |||
| 0ed60f5200 | |||
| daa1436e55 | |||
| 0cfa871e86 | |||
| 9cfb366839 | |||
| dee504f7ed | |||
| 55d3d36b81 | |||
| 39bdd02566 | |||
| a4496db344 | |||
| 350df0fc28 | |||
| 49094be1c5 | |||
| f80b15446a | |||
| 122d430dbb | |||
| 13b1b37c8a | |||
| a94154ad47 | |||
| d609fe8ac3 | |||
| 7316591ebd | |||
| d286ecb4da | |||
| a79cbe4f84 | |||
| 092c99d25d | |||
| 34839276cf | |||
| 1e71add551 | |||
| ee2482a55a | |||
| 2489f5464a | |||
| b6141d9381 | |||
| 95c82cfcf2 | |||
| a85d6b0ead | |||
| 6c330764ea | |||
| 3f4b02e617 | |||
| ee1eb8cde9 | |||
| 93744510ec | |||
| 1c0a506238 | |||
| 1dc0754b7f | |||
| 7b4a2f3fe8 | |||
| 9d98ea31af | |||
| ad3741fd15 |
@@ -6,7 +6,9 @@ import com.ycwl.basic.enums.FaceCutStatus;
|
||||
import com.ycwl.basic.enums.FacePieceUpdateStatus;
|
||||
import com.ycwl.basic.enums.TemplateRenderStatus;
|
||||
import com.ycwl.basic.mapper.TaskMapper;
|
||||
import com.ycwl.basic.mapper.task.TaskRenderJobMappingMapper;
|
||||
import com.ycwl.basic.model.pc.task.entity.TaskEntity;
|
||||
import com.ycwl.basic.model.task.entity.TaskRenderJobMappingEntity;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -51,6 +53,8 @@ public class FaceStatusManager {
|
||||
|
||||
@Autowired
|
||||
private TaskMapper taskMapper;
|
||||
@Autowired
|
||||
private TaskRenderJobMappingMapper taskRenderJobMappingMapper;
|
||||
|
||||
public FaceStatusManager() {
|
||||
// 初始化三个独立的缓存实例
|
||||
@@ -257,19 +261,26 @@ public class FaceStatusManager {
|
||||
Integer code = templateRenderCache.getIfPresent(faceId + ":" + templateId);
|
||||
if (code == null) {
|
||||
log.debug("模板渲染状态缓存不存在: faceId={}, templateId={}", faceId, templateId);
|
||||
// 查询数据库
|
||||
// 查询数据库:通过 task_render_job_mapping 确定渲染状态
|
||||
TaskEntity task = taskMapper.listLastFaceTemplateTask(faceId, templateId);
|
||||
if (task == null) {
|
||||
setTemplateRenderStatus(faceId, templateId, TemplateRenderStatus.NONE);
|
||||
return TemplateRenderStatus.NONE;
|
||||
}
|
||||
if (Integer.valueOf(2).equals(task.getStatus())) {
|
||||
setTemplateRenderStatus(faceId, templateId, TemplateRenderStatus.RENDERING);
|
||||
TaskRenderJobMappingEntity mapping = taskRenderJobMappingMapper.selectByTaskId(task.getId());
|
||||
if (mapping == null) {
|
||||
setTemplateRenderStatus(faceId, templateId, TemplateRenderStatus.NONE);
|
||||
return TemplateRenderStatus.NONE;
|
||||
}
|
||||
if (Integer.valueOf(1).equals(task.getStatus())) {
|
||||
setTemplateRenderStatus(faceId, templateId, TemplateRenderStatus.RENDERED);
|
||||
}
|
||||
return TemplateRenderStatus.NONE;
|
||||
TemplateRenderStatus status = switch (mapping.getRenderStatus()) {
|
||||
case TaskRenderJobMappingEntity.STATUS_PENDING -> TemplateRenderStatus.RENDERING;
|
||||
case TaskRenderJobMappingEntity.STATUS_PREVIEW_READY,
|
||||
TaskRenderJobMappingEntity.STATUS_MP4_COMPOSING,
|
||||
TaskRenderJobMappingEntity.STATUS_COMPLETED -> TemplateRenderStatus.RENDERED;
|
||||
default -> TemplateRenderStatus.NONE; // FAILED 等异常状态
|
||||
};
|
||||
setTemplateRenderStatus(faceId, templateId, status);
|
||||
return status;
|
||||
}
|
||||
return TemplateRenderStatus.fromCode(code);
|
||||
}
|
||||
|
||||
@@ -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); // 更新完了,清理下
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.ycwl.basic.controller;
|
||||
|
||||
import com.github.pagehelper.PageInfo;
|
||||
import com.ycwl.basic.model.pc.videoreview.dto.AdminVideoReviewLogReqDTO;
|
||||
import com.ycwl.basic.model.pc.videoreview.dto.AdminVideoReviewLogRespDTO;
|
||||
import com.ycwl.basic.model.pc.videoreview.dto.VideoPurchaseCheckReqDTO;
|
||||
import com.ycwl.basic.model.pc.videoreview.dto.VideoPurchaseCheckRespDTO;
|
||||
import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewAddReqDTO;
|
||||
@@ -55,6 +57,20 @@ public class VideoReviewController {
|
||||
return ApiResponse.success(pageInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理后台分页查询评价日志
|
||||
* 提供更详细的管理信息,包括评价人账号、机位评价统计等
|
||||
*
|
||||
* @param reqDTO 查询条件
|
||||
* @return 分页结果
|
||||
*/
|
||||
@GetMapping("/admin/logs")
|
||||
public ApiResponse<PageInfo<AdminVideoReviewLogRespDTO>> getAdminReviewLogList(AdminVideoReviewLogReqDTO reqDTO) {
|
||||
log.info("管理后台查询评价日志,pageNum: {}, pageSize: {}", reqDTO.getPageNum(), reqDTO.getPageSize());
|
||||
PageInfo<AdminVideoReviewLogRespDTO> pageInfo = videoReviewService.getAdminReviewLogList(reqDTO);
|
||||
return ApiResponse.success(pageInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取评价统计数据
|
||||
*
|
||||
|
||||
@@ -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,8 +1,10 @@
|
||||
package com.ycwl.basic.controller.mobile;
|
||||
|
||||
import com.ycwl.basic.annotation.IgnoreToken;
|
||||
import com.ycwl.basic.mapper.PrinterGuideMapper;
|
||||
import com.ycwl.basic.model.jwt.JwtInfo;
|
||||
import com.ycwl.basic.model.mobile.face.FaceRecognizeResp;
|
||||
import com.ycwl.basic.model.pc.printer.entity.PrinterGuideEntity;
|
||||
import com.ycwl.basic.model.pc.printer.resp.MemberPrintResp;
|
||||
import com.ycwl.basic.model.pc.printer.resp.PrinterResp;
|
||||
import com.ycwl.basic.model.printer.req.FromSourceReq;
|
||||
@@ -31,12 +33,21 @@ import java.util.UUID;
|
||||
public class AppPrinterController {
|
||||
@Autowired
|
||||
private PrinterService printerService;
|
||||
@Autowired
|
||||
private PrinterGuideMapper printerGuideMapper;
|
||||
@GetMapping("/listFor/{scenicId}")
|
||||
@IgnoreToken
|
||||
public ApiResponse<List<PrinterResp>> listFor(@PathVariable("scenicId") Long scenicId) {
|
||||
return ApiResponse.success(printerService.listByScenicId(scenicId));
|
||||
}
|
||||
|
||||
// 查询打印机已启用的指引图片(按排序)
|
||||
@GetMapping("/guide/{printerId}")
|
||||
@IgnoreToken
|
||||
public ApiResponse<List<PrinterGuideEntity>> guideList(@PathVariable("printerId") Integer printerId) {
|
||||
return ApiResponse.success(printerGuideMapper.listEnabledByPrinterId(printerId));
|
||||
}
|
||||
|
||||
@GetMapping("/useSample/{sampleId}")
|
||||
public ApiResponse<FaceRecognizeResp> useSample(@PathVariable("sampleId") Long sampleId) throws IOException {
|
||||
JwtInfo worker = JwtTokenUtil.getWorker();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -7,13 +7,19 @@ import com.ycwl.basic.integration.device.dto.status.DeviceStatusDTO;
|
||||
import com.ycwl.basic.integration.device.service.DeviceConfigIntegrationService;
|
||||
import com.ycwl.basic.integration.device.service.DeviceIntegrationService;
|
||||
import com.ycwl.basic.integration.device.service.DeviceStatusIntegrationService;
|
||||
import com.ycwl.basic.mapper.SourceMapper;
|
||||
import com.ycwl.basic.model.pc.device.resp.DeviceSourceStatsVO;
|
||||
import com.ycwl.basic.model.pc.device.resp.DeviceSourceTimelineVO;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -32,6 +38,7 @@ public class DeviceV2Controller {
|
||||
private final DeviceIntegrationService deviceIntegrationService;
|
||||
private final DeviceConfigIntegrationService deviceConfigIntegrationService;
|
||||
private final DeviceStatusIntegrationService deviceStatusIntegrationService;
|
||||
private final SourceMapper sourceMapper;
|
||||
|
||||
// ========== 设备基础 CRUD 操作 ==========
|
||||
|
||||
@@ -387,4 +394,96 @@ public class DeviceV2Controller {
|
||||
return ApiResponse.fail("获取景区所有设备列表失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 设备拍摄统计 ==========
|
||||
|
||||
/**
|
||||
* 设备拍摄统计:拍摄总数、拍摄人数、售出张数、赠送张数、售出人数
|
||||
*/
|
||||
@GetMapping("/{id}/source-stats")
|
||||
public ApiResponse<DeviceSourceStatsVO> getDeviceSourceStats(
|
||||
@PathVariable Long id,
|
||||
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date startDate,
|
||||
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date endDate) {
|
||||
try {
|
||||
java.util.Calendar cal = java.util.Calendar.getInstance();
|
||||
// startDate:归到当天 00:00:00(未传则默认今天)
|
||||
if (startDate != null) {
|
||||
cal.setTime(startDate);
|
||||
}
|
||||
cal.set(java.util.Calendar.HOUR_OF_DAY, 0);
|
||||
cal.set(java.util.Calendar.MINUTE, 0);
|
||||
cal.set(java.util.Calendar.SECOND, 0);
|
||||
cal.set(java.util.Calendar.MILLISECOND, 0);
|
||||
startDate = cal.getTime();
|
||||
// endDate:归到当天 23:59:59(未传则取 startDate 同一天)
|
||||
if (endDate != null) {
|
||||
cal.setTime(endDate);
|
||||
}
|
||||
cal.set(java.util.Calendar.HOUR_OF_DAY, 23);
|
||||
cal.set(java.util.Calendar.MINUTE, 59);
|
||||
cal.set(java.util.Calendar.SECOND, 59);
|
||||
cal.set(java.util.Calendar.MILLISECOND, 999);
|
||||
endDate = cal.getTime();
|
||||
DeviceSourceStatsVO stats = sourceMapper.getDeviceSourceStats(id, startDate, endDate);
|
||||
return ApiResponse.success(stats);
|
||||
} catch (Exception e) {
|
||||
log.error("获取设备拍摄统计失败, deviceId: {}", id, e);
|
||||
return ApiResponse.fail("获取设备拍摄统计失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设备拍摄时间线:按 5 分钟分桶统计 type=2 的拍摄数量,空桶补 0
|
||||
*/
|
||||
@GetMapping("/{id}/source-timeline")
|
||||
public ApiResponse<List<DeviceSourceTimelineVO>> getDeviceSourceTimeline(
|
||||
@PathVariable Long id,
|
||||
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date startDate,
|
||||
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date endDate) {
|
||||
try {
|
||||
java.util.Calendar cal = java.util.Calendar.getInstance();
|
||||
// startDate:归到当天 08:00:00(未传则默认今天)
|
||||
if (startDate != null) {
|
||||
cal.setTime(startDate);
|
||||
}
|
||||
cal.set(java.util.Calendar.HOUR_OF_DAY, 8);
|
||||
cal.set(java.util.Calendar.MINUTE, 0);
|
||||
cal.set(java.util.Calendar.SECOND, 0);
|
||||
cal.set(java.util.Calendar.MILLISECOND, 0);
|
||||
startDate = cal.getTime();
|
||||
// endDate:归到当天 19:59:59(未传则取 startDate 同一天)
|
||||
if (endDate != null) {
|
||||
cal.setTime(endDate);
|
||||
}
|
||||
cal.set(java.util.Calendar.HOUR_OF_DAY, 19);
|
||||
cal.set(java.util.Calendar.MINUTE, 59);
|
||||
cal.set(java.util.Calendar.SECOND, 59);
|
||||
cal.set(java.util.Calendar.MILLISECOND, 999);
|
||||
endDate = cal.getTime();
|
||||
|
||||
// 查询有数据的桶
|
||||
List<DeviceSourceTimelineVO> rawData = sourceMapper.getDeviceSourceTimeline(id, startDate, endDate);
|
||||
|
||||
// 将有数据的桶放入 Map,key 为对齐到 5 分钟的毫秒时间戳
|
||||
Map<Long, Integer> dataMap = new HashMap<>();
|
||||
for (DeviceSourceTimelineVO item : rawData) {
|
||||
long aligned = (item.getTime().getTime() / 300_000) * 300_000;
|
||||
dataMap.put(aligned, item.getCount());
|
||||
}
|
||||
|
||||
// 生成完整时间轴并补零
|
||||
long startMs = (startDate.getTime() / 300_000) * 300_000;
|
||||
long endMs = endDate.getTime();
|
||||
List<DeviceSourceTimelineVO> timeline = new ArrayList<>();
|
||||
for (long ts = startMs; ts <= endMs; ts += 300_000) {
|
||||
timeline.add(new DeviceSourceTimelineVO(new Date(ts), dataMap.getOrDefault(ts, 0)));
|
||||
}
|
||||
|
||||
return ApiResponse.success(timeline);
|
||||
} catch (Exception e) {
|
||||
log.error("获取设备拍摄时间线失败, deviceId: {}", id, e);
|
||||
return ApiResponse.fail("获取设备拍摄时间线失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,10 @@ package com.ycwl.basic.controller.pc;
|
||||
import com.github.pagehelper.PageHelper;
|
||||
import com.github.pagehelper.PageInfo;
|
||||
import com.ycwl.basic.mapper.PrintTaskMapper;
|
||||
import com.ycwl.basic.mapper.PrinterGuideMapper;
|
||||
import com.ycwl.basic.model.pc.printer.entity.PrintTaskEntity;
|
||||
import com.ycwl.basic.model.pc.printer.entity.PrinterEntity;
|
||||
import com.ycwl.basic.model.pc.printer.entity.PrinterGuideEntity;
|
||||
import com.ycwl.basic.model.pc.printer.req.PrintTaskReqQuery;
|
||||
import com.ycwl.basic.model.pc.printer.req.ReprintRequest;
|
||||
import com.ycwl.basic.service.printer.PrinterService;
|
||||
@@ -16,6 +18,7 @@ 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;
|
||||
|
||||
import java.util.List;
|
||||
@@ -29,6 +32,9 @@ public class PrinterController {
|
||||
@Autowired
|
||||
private PrintTaskMapper printTaskMapper;
|
||||
|
||||
@Autowired
|
||||
private PrinterGuideMapper printerGuideMapper;
|
||||
|
||||
// 查询列表
|
||||
@PostMapping("/list")
|
||||
public ApiResponse<List<PrinterEntity>> list(@RequestBody PrinterEntity condition) {
|
||||
@@ -119,4 +125,41 @@ public class PrinterController {
|
||||
int count = printerService.rejectPrintTasks(taskIds);
|
||||
return ApiResponse.success(count);
|
||||
}
|
||||
|
||||
// 查询打印机所有指引图片(含禁用)
|
||||
@GetMapping("/guide/list/{printerId}")
|
||||
public ApiResponse<List<PrinterGuideEntity>> guideList(@PathVariable("printerId") Integer printerId) {
|
||||
return ApiResponse.success(printerGuideMapper.listByPrinterId(printerId));
|
||||
}
|
||||
|
||||
// 添加指引图片
|
||||
@PostMapping("/guide/add")
|
||||
public ApiResponse<Integer> guideAdd(@RequestBody PrinterGuideEntity entity) {
|
||||
if (entity.getSortOrder() == null) {
|
||||
entity.setSortOrder(0);
|
||||
}
|
||||
if (entity.getEnabled() == null) {
|
||||
entity.setEnabled(1);
|
||||
}
|
||||
printerGuideMapper.insertGuide(entity);
|
||||
return ApiResponse.success(entity.getId());
|
||||
}
|
||||
|
||||
// 删除指引图片
|
||||
@DeleteMapping("/guide/delete/{id}")
|
||||
public ApiResponse<Integer> guideDelete(@PathVariable("id") Integer id) {
|
||||
return ApiResponse.success(printerGuideMapper.deleteById(id));
|
||||
}
|
||||
|
||||
// 修改指引图片排序
|
||||
@PostMapping("/guide/updateSort/{id}")
|
||||
public ApiResponse<Integer> guideUpdateSort(@PathVariable("id") Integer id, @RequestParam("sortOrder") Integer sortOrder) {
|
||||
return ApiResponse.success(printerGuideMapper.updateSortOrder(id, sortOrder));
|
||||
}
|
||||
|
||||
// 切换指引图片启用/禁用
|
||||
@PostMapping("/guide/toggleEnabled/{id}")
|
||||
public ApiResponse<Integer> guideToggleEnabled(@PathVariable("id") Integer id) {
|
||||
return ApiResponse.success(printerGuideMapper.toggleEnabled(id));
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import com.ycwl.basic.utils.JwtTokenUtil;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
@@ -52,8 +53,9 @@ public class SourceController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建虚拟用户0元订单
|
||||
* 创建虚拟用户订单
|
||||
* 用于后台直接从source创建订单,不需要真实用户
|
||||
* 支持立即0元购买或创建待支付订单(由needActualPayment控制)
|
||||
*
|
||||
* @param request 请求参数
|
||||
* @return 订单信息
|
||||
@@ -66,7 +68,8 @@ public class SourceController {
|
||||
request.getScenicId(),
|
||||
request.getPrinterId(),
|
||||
request.getNeedEnhance(),
|
||||
request.getPrintImgUrl()
|
||||
request.getPrintImgUrl(),
|
||||
request.getNeedActualPayment()
|
||||
);
|
||||
return ApiResponse.success(result);
|
||||
} catch (Exception e) {
|
||||
@@ -74,5 +77,54 @@ public class SourceController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据sourceId列表查询关联的faceId
|
||||
* @param sourceIds sourceId列表
|
||||
* @return sourceId -> faceId 的映射,无关联则value为null
|
||||
*/
|
||||
@PostMapping("/faceIds")
|
||||
public ApiResponse<Map<Long, Long>> getFaceIdsBySourceIds(@RequestBody List<Long> sourceIds) {
|
||||
return sourceService.getFaceIdsBySourceIds(sourceIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据faceId分页查询关联的source记录
|
||||
* @param sourceReqQuery 查询参数(需设置faceId,可选type/scenicId/isBuy)
|
||||
* @return 分页source列表
|
||||
*/
|
||||
@PostMapping("/pageByFaceId")
|
||||
public ApiResponse pageByFaceId(@RequestBody SourceReqQuery sourceReqQuery) {
|
||||
return sourceService.pageByFaceId(sourceReqQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员取消关联(软删除)
|
||||
* @param id member_source 记录 ID
|
||||
* @return 操作结果
|
||||
*/
|
||||
@PostMapping("/admin/cancel/{id}")
|
||||
public ApiResponse cancelRelation(@PathVariable("id") Long id) {
|
||||
return sourceService.cancelRelation(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员恢复已取消的关联
|
||||
* @param id member_source 记录 ID
|
||||
* @return 操作结果
|
||||
*/
|
||||
@PostMapping("/admin/reactivate/{id}")
|
||||
public ApiResponse reactivateRelation(@PathVariable("id") Long id) {
|
||||
return sourceService.reactivateRelation(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员查询已取消的关联记录
|
||||
* @param sourceReqQuery 查询参数(需设置faceId,可选type/scenicId)
|
||||
* @return 分页已取消关联列表
|
||||
*/
|
||||
@PostMapping("/admin/pageDeletedByFaceId")
|
||||
public ApiResponse pageDeletedByFaceId(@RequestBody SourceReqQuery sourceReqQuery) {
|
||||
return sourceService.pageDeletedByFaceId(sourceReqQuery);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.ycwl.basic.controller.printer;
|
||||
|
||||
import com.ycwl.basic.annotation.IgnoreToken;
|
||||
import com.ycwl.basic.model.pc.printer.entity.PrinterEntity;
|
||||
import com.ycwl.basic.model.printer.req.PrinterSyncReq;
|
||||
import com.ycwl.basic.model.printer.req.WorkerAuthReqVo;
|
||||
import com.ycwl.basic.model.printer.resp.PrintTaskResp;
|
||||
@@ -40,4 +41,26 @@ public class PrinterTaskController {
|
||||
printerService.taskFail(taskId, req);
|
||||
return ApiResponse.success("OK");
|
||||
}
|
||||
|
||||
@PostMapping("/detail")
|
||||
public ApiResponse<PrinterEntity> detail(@RequestBody WorkerAuthReqVo req) {
|
||||
return ApiResponse.success(printerService.getByAccessKey(req.getAccessKey()));
|
||||
}
|
||||
|
||||
@PostMapping("/scenic")
|
||||
public ApiResponse scenic(@RequestBody WorkerAuthReqVo req) {
|
||||
return ApiResponse.success(printerService.getScenicBasicByAccessKey(req.getAccessKey()));
|
||||
}
|
||||
|
||||
@PostMapping("/open")
|
||||
public ApiResponse open(@RequestBody WorkerAuthReqVo req) {
|
||||
printerService.openPrinter(req.getAccessKey());
|
||||
return ApiResponse.success("OK");
|
||||
}
|
||||
|
||||
@PostMapping("/close")
|
||||
public ApiResponse close(@RequestBody WorkerAuthReqVo req) {
|
||||
printerService.closePrinter(req.getAccessKey());
|
||||
return ApiResponse.success("OK");
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -47,10 +47,6 @@ public class VptController {
|
||||
}
|
||||
@PostMapping("/scenic/{scenicId}/{taskId}/success")
|
||||
public ApiResponse<String> success(@PathVariable("scenicId") Long scenicId, @PathVariable("taskId") Long taskId, @RequestBody FileObject fileObject) {
|
||||
IStorageAdapter adapter = scenicService.getScenicLocalStorageAdapter(scenicId);
|
||||
String filename = StorageUtil.joinPath(StorageConstant.VIDEO_PIECE_PATH, taskId.toString() + ".mp4");
|
||||
fileObject.setUrl(adapter.getUrl(filename));
|
||||
adapter.setAcl(StorageAcl.PUBLIC_READ, filename);
|
||||
VptPassiveStorageOperator.onReceiveResult(taskId, fileObject);
|
||||
return ApiResponse.success("success");
|
||||
}
|
||||
|
||||
@@ -48,10 +48,6 @@ public class WvpController {
|
||||
}
|
||||
@PostMapping("/scenic/{scenicId}/{taskId}/success")
|
||||
public ApiResponse<String> success(@PathVariable("scenicId") Long scenicId, @PathVariable("taskId") Long taskId, @RequestBody FileObject fileObject) {
|
||||
IStorageAdapter adapter = scenicService.getScenicLocalStorageAdapter(scenicId);
|
||||
String filename = StorageUtil.joinPath(StorageConstant.VIDEO_PIECE_PATH, taskId.toString() + ".mp4");
|
||||
fileObject.setUrl(adapter.getUrl(filename));
|
||||
adapter.setAcl(StorageAcl.PUBLIC_READ, filename);
|
||||
WvpPassiveStorageOperator.onReceiveResult(taskId, fileObject);
|
||||
return ApiResponse.success("success");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.ycwl.basic.enums;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 视频评价来源枚举
|
||||
*/
|
||||
@Getter
|
||||
public enum VideoReviewSourceEnum {
|
||||
|
||||
/**
|
||||
* 订单
|
||||
*/
|
||||
ORDER("ORDER", "订单"),
|
||||
|
||||
/**
|
||||
* 渲染
|
||||
*/
|
||||
RENDER("RENDER", "渲染");
|
||||
|
||||
/**
|
||||
* 枚举代码
|
||||
*/
|
||||
private final String code;
|
||||
|
||||
/**
|
||||
* 枚举描述
|
||||
*/
|
||||
private final String description;
|
||||
|
||||
VideoReviewSourceEnum(String code, String description) {
|
||||
this.code = code;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据code获取枚举
|
||||
*
|
||||
* @param code 枚举代码
|
||||
* @return 枚举对象,不存在则返回null
|
||||
*/
|
||||
public static VideoReviewSourceEnum fromCode(String code) {
|
||||
if (code == null || code.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
for (VideoReviewSourceEnum value : VideoReviewSourceEnum.values()) {
|
||||
if (value.getCode().equals(code)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证code是否有效
|
||||
*
|
||||
* @param code 枚举代码
|
||||
* @return true-有效, false-无效
|
||||
*/
|
||||
public static boolean isValid(String code) {
|
||||
return fromCode(code) != null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.ycwl.basic.handler;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.ibatis.type.BaseTypeHandler;
|
||||
import org.apache.ibatis.type.JdbcType;
|
||||
import org.apache.ibatis.type.MappedJdbcTypes;
|
||||
import org.apache.ibatis.type.MappedTypes;
|
||||
|
||||
import java.sql.CallableStatement;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Long类型列表的TypeHandler
|
||||
* 用于处理数据库JSON字段与Java List<Long>之间的转换
|
||||
*/
|
||||
@Slf4j
|
||||
@MappedTypes(List.class)
|
||||
@MappedJdbcTypes(JdbcType.VARCHAR)
|
||||
public class LongListTypeHandler extends BaseTypeHandler<List<Long>> {
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private final TypeReference<List<Long>> typeReference = new TypeReference<List<Long>>() {};
|
||||
|
||||
@Override
|
||||
public void setNonNullParameter(PreparedStatement ps, int i, List<Long> parameter, JdbcType jdbcType) throws SQLException {
|
||||
try {
|
||||
String json = objectMapper.writeValueAsString(parameter);
|
||||
ps.setString(i, json);
|
||||
log.debug("序列化Long列表: {}", json);
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("序列化Long列表失败", e);
|
||||
throw new SQLException("序列化Long列表失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Long> getNullableResult(ResultSet rs, String columnName) throws SQLException {
|
||||
String json = rs.getString(columnName);
|
||||
return parseJson(json, columnName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Long> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
|
||||
String json = rs.getString(columnIndex);
|
||||
return parseJson(json, "columnIndex:" + columnIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Long> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
|
||||
String json = cs.getString(columnIndex);
|
||||
return parseJson(json, "columnIndex:" + columnIndex);
|
||||
}
|
||||
|
||||
private List<Long> parseJson(String json, String source) {
|
||||
if (json == null || json.trim().isEmpty() || "null".equals(json)) {
|
||||
log.debug("从{}获取的JSON为空,返回空列表", source);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
try {
|
||||
List<Long> result = objectMapper.readValue(json, typeReference);
|
||||
if (result == null) {
|
||||
log.debug("从{}反序列化得到null,返回空列表", source);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
log.debug("从{}反序列化Long列表成功,数量: {}", source, result.size());
|
||||
return result;
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("从{}反序列化Long列表失败,JSON: {}", source, json, e);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.ycwl.basic.handler;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.ibatis.type.BaseTypeHandler;
|
||||
import org.apache.ibatis.type.JdbcType;
|
||||
import org.apache.ibatis.type.MappedJdbcTypes;
|
||||
import org.apache.ibatis.type.MappedTypes;
|
||||
|
||||
import java.sql.CallableStatement;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* String列表类型处理器
|
||||
* 用于将数据库中的JSON字符串转换为Java的List<String>对象
|
||||
*/
|
||||
@Slf4j
|
||||
@MappedTypes(List.class)
|
||||
@MappedJdbcTypes(JdbcType.VARCHAR)
|
||||
public class StringListTypeHandler extends BaseTypeHandler<List<String>> {
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private final TypeReference<List<String>> typeReference = new TypeReference<List<String>>() {};
|
||||
|
||||
@Override
|
||||
public void setNonNullParameter(PreparedStatement ps, int i, List<String> parameter, JdbcType jdbcType) throws SQLException {
|
||||
try {
|
||||
String json = objectMapper.writeValueAsString(parameter);
|
||||
ps.setString(i, json);
|
||||
log.debug("序列化String列表: {}", json);
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("序列化String列表失败", e);
|
||||
throw new SQLException("序列化String列表失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getNullableResult(ResultSet rs, String columnName) throws SQLException {
|
||||
return parseJson(rs.getString(columnName));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
|
||||
return parseJson(rs.getString(columnIndex));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
|
||||
return parseJson(cs.getString(columnIndex));
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析JSON字符串为List<String>
|
||||
*/
|
||||
private List<String> parseJson(String json) {
|
||||
if (json == null || json.isEmpty() || "null".equals(json)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
try {
|
||||
List<String> result = objectMapper.readValue(json, typeReference);
|
||||
log.debug("反序列化String列表: {}", result);
|
||||
return result;
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("反序列化String列表失败: {}", json, e);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 水印Stage配置
|
||||
@@ -67,4 +68,16 @@ public class WatermarkConfig {
|
||||
*/
|
||||
@Builder.Default
|
||||
private final long edgeTimeoutMs = 10_000L;
|
||||
|
||||
/**
|
||||
* 打印水印竖版URL列表
|
||||
* 用于在竖屏图片上添加全屏水印叠加层(在原图上方,文字/二维码下方)
|
||||
*/
|
||||
private final List<String> printWatermarkPUrlList;
|
||||
|
||||
/**
|
||||
* 打印水印横版URL列表
|
||||
* 用于在横屏图片上添加全屏水印叠加层(在原图上方,文字/二维码下方)
|
||||
*/
|
||||
private final List<String> printWatermarkLUrlList;
|
||||
}
|
||||
|
||||
@@ -241,6 +241,10 @@ public class WatermarkStage extends AbstractPipelineStage<PhotoProcessContext> {
|
||||
}
|
||||
}
|
||||
|
||||
// 传递打印水印URL列表(横竖版由边缘端根据图片实际尺寸判断)
|
||||
info.setPrintWatermarkPUrlList(config.getPrintWatermarkPUrlList());
|
||||
info.setPrintWatermarkLUrlList(config.getPrintWatermarkLUrlList());
|
||||
|
||||
return info;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +77,18 @@ public class PrinterDefaultWatermarkTemplateBuilder extends AbstractWatermarkTem
|
||||
elements.add(originalImageElement);
|
||||
dynamicData.put("originalImage", request.getOriginalImageUrl());
|
||||
|
||||
// 0.5 打印水印叠加层(z-index=5,全屏覆盖,在原图上方、文字/二维码下方)
|
||||
if (request.getPrintWatermarkUrl() != null && !request.getPrintWatermarkUrl().isEmpty()) {
|
||||
PuzzleElementEntity printWatermarkElement = createImageElement(
|
||||
"printWatermark", "打印水印",
|
||||
0, 0,
|
||||
imageWidth, imageHeight, 5,
|
||||
FIT_MODE_COVER, null, null
|
||||
);
|
||||
elements.add(printWatermarkElement);
|
||||
dynamicData.put("printWatermark", request.getPrintWatermarkUrl());
|
||||
}
|
||||
|
||||
// 计算二维码位置
|
||||
int qrcodeWidth = scaledQrcodeSize;
|
||||
int qrcodeHeight = scaledQrcodeSize;
|
||||
|
||||
@@ -21,6 +21,7 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
@@ -237,6 +238,8 @@ public class WatermarkEdgeService {
|
||||
.offsetRight(info.getOffsetRight())
|
||||
.offsetTop(info.getOffsetTop())
|
||||
.offsetBottom(info.getOffsetBottom())
|
||||
.printWatermarkUrl(resolvePrintWatermarkUrl(imageWidth, imageHeight,
|
||||
info.getPrintWatermarkPUrlList(), info.getPrintWatermarkLUrlList()))
|
||||
.outputFormat(type.getPreferFileType().equalsIgnoreCase("png") ? "PNG" : "JPEG")
|
||||
.outputQuality(90)
|
||||
.build();
|
||||
@@ -294,6 +297,25 @@ public class WatermarkEdgeService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据图片方向从URL列表中选取对应的打印水印URL
|
||||
*
|
||||
* @param imageWidth 图片宽度
|
||||
* @param imageHeight 图片高度
|
||||
* @param pUrlList 竖版URL列表
|
||||
* @param lUrlList 横版URL列表
|
||||
* @return 选中的URL,无可用URL时返回null
|
||||
*/
|
||||
private String resolvePrintWatermarkUrl(int imageWidth, int imageHeight,
|
||||
List<String> pUrlList, List<String> lUrlList) {
|
||||
boolean isLandscape = imageWidth >= imageHeight;
|
||||
List<String> urlList = isLandscape ? lUrlList : pUrlList;
|
||||
if (urlList != null && !urlList.isEmpty()) {
|
||||
return urlList.get(0);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图片尺寸
|
||||
*
|
||||
|
||||
@@ -70,6 +70,12 @@ public class WatermarkRequest {
|
||||
@Builder.Default
|
||||
private Integer outputQuality = 75;
|
||||
|
||||
/**
|
||||
* 打印水印URL(根据图片方向已解析的单个URL)
|
||||
* 用于在原图上方、文字/二维码下方添加全屏水印叠加层
|
||||
*/
|
||||
private String printWatermarkUrl;
|
||||
|
||||
public double getScaleValue() {
|
||||
return scale != null ? scale : 1.0;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import lombok.Data;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class WatermarkInfo {
|
||||
@@ -40,6 +41,16 @@ public class WatermarkInfo {
|
||||
*/
|
||||
private Double scale;
|
||||
|
||||
/**
|
||||
* 打印水印竖版URL列表
|
||||
*/
|
||||
private List<String> printWatermarkPUrlList;
|
||||
|
||||
/**
|
||||
* 打印水印横版URL列表
|
||||
*/
|
||||
private List<String> printWatermarkLUrlList;
|
||||
|
||||
public String getDatetimeLine() {
|
||||
if (datetimeLine == null) {
|
||||
datetimeLine = DateUtil.format(datetime, dtFormat);
|
||||
|
||||
@@ -64,6 +64,8 @@ public class DeviceDefaultConfigIntegrationService {
|
||||
log.info("创建默认配置, configKey: {}", request.getConfigKey());
|
||||
CommonResponse<String> response = defaultConfigClient.createDefaultConfig(request);
|
||||
String result = handleResponse(response, "创建默认配置失败");
|
||||
// 清理相关缓存
|
||||
clearDefaultConfigCache(request.getConfigKey());
|
||||
return result != null;
|
||||
}
|
||||
|
||||
@@ -75,6 +77,9 @@ public class DeviceDefaultConfigIntegrationService {
|
||||
CommonResponse<Map<String, Object>> response = defaultConfigClient.updateDefaultConfig(configKey, updates);
|
||||
Map<String, Object> result = handleResponse(response, "更新默认配置失败");
|
||||
|
||||
// 清理相关缓存
|
||||
clearDefaultConfigCache(configKey);
|
||||
|
||||
// 检查是否有冲突信息
|
||||
if (result != null && result.containsKey("conflict")) {
|
||||
Object conflictObj = result.get("conflict");
|
||||
@@ -94,6 +99,8 @@ public class DeviceDefaultConfigIntegrationService {
|
||||
log.info("删除默认配置, configKey: {}", configKey);
|
||||
CommonResponse<String> response = defaultConfigClient.deleteDefaultConfig(configKey);
|
||||
String result = handleResponse(response, "删除默认配置失败");
|
||||
// 清理相关缓存
|
||||
clearDefaultConfigCache(configKey);
|
||||
return result != null;
|
||||
}
|
||||
|
||||
@@ -103,7 +110,26 @@ public class DeviceDefaultConfigIntegrationService {
|
||||
public BatchDefaultConfigResponse batchUpdateDefaultConfigs(BatchDefaultConfigRequest request) {
|
||||
log.info("批量更新默认配置, configs count: {}", request.getConfigs().size());
|
||||
CommonResponse<BatchDefaultConfigResponse> response = defaultConfigClient.batchUpdateDefaultConfigs(request);
|
||||
return handleResponse(response, "批量更新默认配置失败");
|
||||
BatchDefaultConfigResponse result = handleResponse(response, "批量更新默认配置失败");
|
||||
// 清理所有默认配置相关缓存
|
||||
clearAllDefaultConfigCache();
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理指定配置的缓存
|
||||
*/
|
||||
private void clearDefaultConfigCache(String configKey) {
|
||||
if (configKey != null) {
|
||||
fallbackService.clearFallbackCache(SERVICE_NAME, "defaults:config:" + configKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理所有默认配置缓存
|
||||
*/
|
||||
private void clearAllDefaultConfigCache() {
|
||||
fallbackService.clearAllFallbackCache(SERVICE_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -58,12 +58,16 @@ public class DeviceIntegrationService {
|
||||
log.debug("更新设备信息, deviceId: {}", deviceId);
|
||||
CommonResponse<String> response = deviceV2Client.updateDevice(deviceId, request);
|
||||
handleResponse(response, "更新设备信息失败");
|
||||
// 清理设备相关缓存
|
||||
fallbackService.clearFallbackCache(SERVICE_NAME, "device:" + deviceId);
|
||||
}
|
||||
|
||||
public void deleteDevice(Long deviceId) {
|
||||
log.debug("删除设备, deviceId: {}", deviceId);
|
||||
CommonResponse<String> response = deviceV2Client.deleteDevice(deviceId);
|
||||
handleResponse(response, "删除设备失败");
|
||||
// 清理设备相关缓存
|
||||
fallbackService.clearFallbackCache(SERVICE_NAME, "device:" + deviceId);
|
||||
}
|
||||
|
||||
public PageResponse<DeviceV2DTO> listDevices(Integer page, Integer pageSize, String name, String no,
|
||||
|
||||
@@ -83,7 +83,10 @@ public class ProfitShareIntegrationService {
|
||||
public RuleVO updateRule(Long ruleId, CreateRuleRequest request) {
|
||||
log.debug("更新分账规则, ruleId: {}", ruleId);
|
||||
CommonResponse<RuleVO> response = profitShareClient.updateRule(ruleId, request);
|
||||
return handleResponse(response, "更新分账规则失败");
|
||||
RuleVO result = handleResponse(response, "更新分账规则失败");
|
||||
// 清理规则缓存
|
||||
clearRuleCache(ruleId);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,6 +96,8 @@ public class ProfitShareIntegrationService {
|
||||
log.debug("删除分账规则, ruleId: {}", ruleId);
|
||||
CommonResponse<Void> response = profitShareClient.deleteRule(ruleId);
|
||||
handleResponse(response, "删除分账规则失败");
|
||||
// 清理规则缓存
|
||||
clearRuleCache(ruleId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -102,6 +107,8 @@ public class ProfitShareIntegrationService {
|
||||
log.debug("启用分账规则, ruleId: {}", ruleId);
|
||||
CommonResponse<Void> response = profitShareClient.enableRule(ruleId);
|
||||
handleResponse(response, "启用分账规则失败");
|
||||
// 清理规则缓存
|
||||
clearRuleCache(ruleId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -111,6 +118,8 @@ public class ProfitShareIntegrationService {
|
||||
log.debug("禁用分账规则, ruleId: {}", ruleId);
|
||||
CommonResponse<Void> response = profitShareClient.disableRule(ruleId);
|
||||
handleResponse(response, "禁用分账规则失败");
|
||||
// 清理规则缓存
|
||||
clearRuleCache(ruleId);
|
||||
}
|
||||
|
||||
// ==================== 分账记录查询 ====================
|
||||
@@ -211,6 +220,13 @@ public class ProfitShareIntegrationService {
|
||||
|
||||
// ==================== 私有方法 ====================
|
||||
|
||||
/**
|
||||
* 清理规则相关缓存
|
||||
*/
|
||||
private void clearRuleCache(Long ruleId) {
|
||||
fallbackService.clearFallbackCache(SERVICE_NAME, "rule:" + ruleId);
|
||||
}
|
||||
|
||||
private <T> T handleResponse(CommonResponse<T> response, String errorMessage) {
|
||||
if (response == null || !response.isSuccess()) {
|
||||
String msg = response != null && response.getMessage() != null
|
||||
|
||||
@@ -94,31 +94,55 @@ public class QuestionnaireIntegrationService {
|
||||
public QuestionnaireResponse updateQuestionnaire(Long id, CreateQuestionnaireRequest request, String userId) {
|
||||
log.info("更新问卷, id: {}, userId: {}", id, userId);
|
||||
CommonResponse<QuestionnaireResponse> response = questionnaireClient.updateQuestionnaire(id, request, userId);
|
||||
return handleResponse(response, "更新问卷失败");
|
||||
QuestionnaireResponse result = handleResponse(response, "更新问卷失败");
|
||||
// 清理问卷相关缓存
|
||||
clearQuestionnaireCache(id);
|
||||
return result;
|
||||
}
|
||||
|
||||
public void deleteQuestionnaire(Long id, String userId) {
|
||||
log.info("删除问卷, id: {}, userId: {}", id, userId);
|
||||
CommonResponse<Void> response = questionnaireClient.deleteQuestionnaire(id, userId);
|
||||
handleResponse(response, "删除问卷失败");
|
||||
// 清理问卷相关缓存
|
||||
clearQuestionnaireCache(id);
|
||||
}
|
||||
|
||||
public QuestionnaireResponse publishQuestionnaire(Long id, String userId) {
|
||||
log.info("发布问卷, id: {}, userId: {}", id, userId);
|
||||
CommonResponse<QuestionnaireResponse> response = questionnaireClient.publishQuestionnaire(id, userId);
|
||||
return handleResponse(response, "发布问卷失败");
|
||||
QuestionnaireResponse result = handleResponse(response, "发布问卷失败");
|
||||
// 清理问卷相关缓存(发布会改变状态)
|
||||
clearQuestionnaireCache(id);
|
||||
return result;
|
||||
}
|
||||
|
||||
public QuestionnaireResponse stopQuestionnaire(Long id, String userId) {
|
||||
log.info("停止问卷, id: {}, userId: {}", id, userId);
|
||||
CommonResponse<QuestionnaireResponse> response = questionnaireClient.stopQuestionnaire(id, userId);
|
||||
return handleResponse(response, "停止问卷失败");
|
||||
QuestionnaireResponse result = handleResponse(response, "停止问卷失败");
|
||||
// 清理问卷相关缓存
|
||||
clearQuestionnaireCache(id);
|
||||
return result;
|
||||
}
|
||||
|
||||
public ResponseDetailResponse submitAnswer(SubmitAnswerRequest request) {
|
||||
log.info("提交问卷答案, questionnaireId: {}, userId: {}", request.getQuestionnaireId(), request.getUserId());
|
||||
CommonResponse<ResponseDetailResponse> response = questionnaireClient.submitAnswer(request);
|
||||
return handleResponse(response, "提交问卷答案失败");
|
||||
ResponseDetailResponse result = handleResponse(response, "提交问卷答案失败");
|
||||
// 清理统计缓存(提交答案会影响统计)
|
||||
if (request.getQuestionnaireId() != null) {
|
||||
fallbackService.clearFallbackCache(SERVICE_NAME, "questionnaire:statistics:" + request.getQuestionnaireId());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理问卷相关缓存
|
||||
*/
|
||||
private void clearQuestionnaireCache(Long questionnaireId) {
|
||||
fallbackService.clearFallbackCache(SERVICE_NAME, "questionnaire:" + questionnaireId);
|
||||
fallbackService.clearFallbackCache(SERVICE_NAME, "questionnaire:statistics:" + questionnaireId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -47,6 +47,21 @@ public interface RenderJobV2Client {
|
||||
@PostMapping("/jobs/{jobId}/cancel")
|
||||
CommonResponse<Void> cancelJob(@PathVariable("jobId") Long jobId);
|
||||
|
||||
/**
|
||||
* 创建FINALIZE_MP4任务
|
||||
* 将所有已发布的TS片段合成为MP4文件
|
||||
*
|
||||
* 前置条件:
|
||||
* 1. 作业存在且状态为RUNNING
|
||||
* 2. 所有片段都已发布(PublishedCount == SegmentCount)
|
||||
* 3. 不存在已有的FINALIZE_MP4任务
|
||||
*
|
||||
* @param jobId 作业ID
|
||||
* @return 任务创建结果
|
||||
*/
|
||||
@PostMapping("/jobs/{jobId}/finalize-mp4")
|
||||
CommonResponse<FinalizeMP4Response> createFinalizeMP4Task(@PathVariable("jobId") Long jobId);
|
||||
|
||||
// ==================== 管理端接口 ====================
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.ycwl.basic.integration.render.dto.job;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 创建FINALIZE_MP4任务响应
|
||||
*/
|
||||
@Data
|
||||
public class FinalizeMP4Response {
|
||||
|
||||
/**
|
||||
* 任务ID
|
||||
*/
|
||||
private Long taskId;
|
||||
|
||||
/**
|
||||
* 任务状态
|
||||
*/
|
||||
private String status;
|
||||
}
|
||||
@@ -21,7 +21,6 @@ import java.util.List;
|
||||
public class RenderJobIntegrationService {
|
||||
|
||||
private final RenderJobV2Client renderJobV2Client;
|
||||
private final IntegrationFallbackService fallbackService;
|
||||
|
||||
private static final String SERVICE_NAME = "zt-render-worker";
|
||||
|
||||
@@ -41,15 +40,8 @@ public class RenderJobIntegrationService {
|
||||
*/
|
||||
public JobStatusResponse getJobStatus(Long jobId) {
|
||||
log.debug("获取作业状态, jobId: {}", jobId);
|
||||
return fallbackService.executeWithFallback(
|
||||
SERVICE_NAME,
|
||||
"job:status:" + jobId,
|
||||
() -> {
|
||||
CommonResponse<JobStatusResponse> response = renderJobV2Client.getJobStatus(jobId);
|
||||
return handleResponse(response, "获取作业状态失败");
|
||||
},
|
||||
JobStatusResponse.class
|
||||
);
|
||||
CommonResponse<JobStatusResponse> response = renderJobV2Client.getJobStatus(jobId);
|
||||
return handleResponse(response, "获取作业状态失败");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,15 +63,8 @@ public class RenderJobIntegrationService {
|
||||
*/
|
||||
public PlaylistInfoDTO getPlaylistInfo(Long jobId) {
|
||||
log.debug("获取播放列表信息, jobId: {}", jobId);
|
||||
return fallbackService.executeWithFallback(
|
||||
SERVICE_NAME,
|
||||
"job:playlist-info:" + jobId,
|
||||
() -> {
|
||||
CommonResponse<PlaylistInfoDTO> response = renderJobV2Client.getPlaylistInfo(jobId);
|
||||
return handleResponse(response, "获取播放列表信息失败");
|
||||
},
|
||||
PlaylistInfoDTO.class
|
||||
);
|
||||
CommonResponse<PlaylistInfoDTO> response = renderJobV2Client.getPlaylistInfo(jobId);
|
||||
return handleResponse(response, "获取播放列表信息失败");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,6 +76,20 @@ public class RenderJobIntegrationService {
|
||||
handleVoidResponse(response, "取消作业失败");
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建FINALIZE_MP4任务(直接调用,不降级)
|
||||
* 将所有已发布的TS片段合成为MP4文件
|
||||
*
|
||||
* @param jobId 作业ID
|
||||
* @return 任务创建结果
|
||||
* @throws IntegrationException 当前置条件不满足时抛出异常
|
||||
*/
|
||||
public FinalizeMP4Response createFinalizeMP4Task(Long jobId) {
|
||||
log.debug("创建FINALIZE_MP4任务, jobId: {}", jobId);
|
||||
CommonResponse<FinalizeMP4Response> response = renderJobV2Client.createFinalizeMP4Task(jobId);
|
||||
return handleResponse(response, "创建FINALIZE_MP4任务失败");
|
||||
}
|
||||
|
||||
// ==================== 管理端接口 ====================
|
||||
|
||||
/**
|
||||
@@ -110,15 +109,8 @@ public class RenderJobIntegrationService {
|
||||
*/
|
||||
public RenderJobV2DTO getJobDetail(Long jobId) {
|
||||
log.debug("获取作业详情, jobId: {}", jobId);
|
||||
return fallbackService.executeWithFallback(
|
||||
SERVICE_NAME,
|
||||
"job:detail:" + jobId,
|
||||
() -> {
|
||||
CommonResponse<RenderJobV2DTO> response = renderJobV2Client.getJobDetail(jobId);
|
||||
return handleResponse(response, "获取作业详情失败");
|
||||
},
|
||||
RenderJobV2DTO.class
|
||||
);
|
||||
CommonResponse<RenderJobV2DTO> response = renderJobV2Client.getJobDetail(jobId);
|
||||
return handleResponse(response, "获取作业详情失败");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -127,16 +119,9 @@ public class RenderJobIntegrationService {
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<RenderJobSegmentV2DTO> getJobSegments(Long jobId) {
|
||||
log.debug("获取作业片段列表, jobId: {}", jobId);
|
||||
return fallbackService.executeWithFallback(
|
||||
SERVICE_NAME,
|
||||
"job:segments:" + jobId,
|
||||
() -> {
|
||||
CommonResponse<List<RenderJobSegmentV2DTO>> response =
|
||||
renderJobV2Client.getJobSegments(jobId);
|
||||
return handleResponse(response, "获取作业片段列表失败");
|
||||
},
|
||||
(Class<List<RenderJobSegmentV2DTO>>) (Class<?>) List.class
|
||||
);
|
||||
CommonResponse<List<RenderJobSegmentV2DTO>> response =
|
||||
renderJobV2Client.getJobSegments(jobId);
|
||||
return handleResponse(response, "获取作业片段列表失败");
|
||||
}
|
||||
|
||||
// ==================== Helper Methods ====================
|
||||
|
||||
@@ -88,6 +88,8 @@ public class RenderTemplateIntegrationService {
|
||||
log.debug("更新渲染模板, id: {}, name: {}", id, request.getName());
|
||||
CommonResponse<Void> response = renderTemplateV2Client.updateTemplate(id, request);
|
||||
handleVoidResponse(response, "更新渲染模板失败");
|
||||
// 清理模板相关缓存
|
||||
clearTemplateCache(id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,6 +99,8 @@ public class RenderTemplateIntegrationService {
|
||||
log.debug("删除渲染模板, id: {}", id);
|
||||
CommonResponse<Void> response = renderTemplateV2Client.deleteTemplate(id);
|
||||
handleVoidResponse(response, "删除渲染模板失败");
|
||||
// 清理模板相关缓存
|
||||
clearTemplateCache(id);
|
||||
}
|
||||
|
||||
// ==================== Template Operations ====================
|
||||
@@ -108,6 +112,8 @@ public class RenderTemplateIntegrationService {
|
||||
log.debug("发布渲染模板, id: {}", id);
|
||||
CommonResponse<Void> response = renderTemplateV2Client.publishTemplate(id);
|
||||
handleVoidResponse(response, "发布渲染模板失败");
|
||||
// 清理模板相关缓存(发布会改变状态)
|
||||
clearTemplateCache(id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -116,7 +122,10 @@ public class RenderTemplateIntegrationService {
|
||||
public TemplateV2DTO createTemplateVersion(Long id) {
|
||||
log.debug("创建渲染模板新版本, id: {}", id);
|
||||
CommonResponse<TemplateV2DTO> response = renderTemplateV2Client.createTemplateVersion(id);
|
||||
return handleResponse(response, "创建渲染模板新版本失败");
|
||||
TemplateV2DTO result = handleResponse(response, "创建渲染模板新版本失败");
|
||||
// 清理模板相关缓存
|
||||
clearTemplateCache(id);
|
||||
return result;
|
||||
}
|
||||
|
||||
// ==================== Segment Management ====================
|
||||
@@ -146,7 +155,10 @@ public class RenderTemplateIntegrationService {
|
||||
log.debug("创建模板片段, templateId: {}, segmentIndex: {}", templateId, request.getSegmentIndex());
|
||||
CommonResponse<TemplateV2SegmentDTO> response =
|
||||
renderTemplateV2Client.createSegment(templateId, request);
|
||||
return handleResponse(response, "创建模板片段失败");
|
||||
TemplateV2SegmentDTO result = handleResponse(response, "创建模板片段失败");
|
||||
// 清理模板片段缓存
|
||||
clearTemplateSegmentCache(templateId);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -157,6 +169,8 @@ public class RenderTemplateIntegrationService {
|
||||
CommonResponse<Void> response =
|
||||
renderTemplateV2Client.updateSegment(templateId, segmentId, request);
|
||||
handleVoidResponse(response, "更新模板片段失败");
|
||||
// 清理模板片段缓存
|
||||
clearTemplateSegmentCache(templateId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -166,6 +180,8 @@ public class RenderTemplateIntegrationService {
|
||||
log.debug("删除模板片段, templateId: {}, segmentId: {}", templateId, segmentId);
|
||||
CommonResponse<Void> response = renderTemplateV2Client.deleteSegment(templateId, segmentId);
|
||||
handleVoidResponse(response, "删除模板片段失败");
|
||||
// 清理模板片段缓存
|
||||
clearTemplateSegmentCache(templateId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -176,10 +192,29 @@ public class RenderTemplateIntegrationService {
|
||||
templateId, request.getSegments() != null ? request.getSegments().size() : 0);
|
||||
CommonResponse<Void> response = renderTemplateV2Client.replaceSegments(templateId, request);
|
||||
handleVoidResponse(response, "替换所有模板片段失败");
|
||||
// 清理模板及片段缓存
|
||||
clearTemplateCache(templateId);
|
||||
clearTemplateSegmentCache(templateId);
|
||||
}
|
||||
|
||||
// ==================== Helper Methods ====================
|
||||
|
||||
/**
|
||||
* 清理模板缓存
|
||||
*/
|
||||
private void clearTemplateCache(Long templateId) {
|
||||
fallbackService.clearFallbackCache(SERVICE_NAME, "template:" + templateId);
|
||||
fallbackService.clearFallbackCache(SERVICE_NAME, "template:with-segments:" + templateId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理模板片段缓存
|
||||
*/
|
||||
private void clearTemplateSegmentCache(Long templateId) {
|
||||
fallbackService.clearFallbackCache(SERVICE_NAME, "template:segments:" + templateId);
|
||||
fallbackService.clearFallbackCache(SERVICE_NAME, "template:with-segments:" + templateId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理通用响应
|
||||
*/
|
||||
|
||||
@@ -87,7 +87,10 @@ public class RenderWorkerConfigIntegrationService {
|
||||
log.debug("创建渲染工作器配置, workerId: {}, configKey: {}", workerId, config.getConfigKey());
|
||||
CommonResponse<RenderWorkerConfigV2DTO> response =
|
||||
renderWorkerConfigV2Client.createWorkerConfig(workerId, config);
|
||||
return handleResponse(response, "创建渲染工作器配置失败");
|
||||
RenderWorkerConfigV2DTO result = handleResponse(response, "创建渲染工作器配置失败");
|
||||
// 清理配置相关缓存
|
||||
clearWorkerConfigCache(workerId, config.getConfigKey());
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -98,6 +101,8 @@ public class RenderWorkerConfigIntegrationService {
|
||||
CommonResponse<Void> response =
|
||||
renderWorkerConfigV2Client.updateWorkerConfig(workerId, configId, updates);
|
||||
handleVoidResponse(response, "更新渲染工作器配置失败");
|
||||
// 清理所有配置缓存(无法确定具体key)
|
||||
clearAllWorkerConfigCache(workerId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -108,6 +113,8 @@ public class RenderWorkerConfigIntegrationService {
|
||||
CommonResponse<Void> response =
|
||||
renderWorkerConfigV2Client.deleteWorkerConfig(workerId, configId);
|
||||
handleVoidResponse(response, "删除渲染工作器配置失败");
|
||||
// 清理所有配置缓存(无法确定具体key)
|
||||
clearAllWorkerConfigCache(workerId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,6 +126,8 @@ public class RenderWorkerConfigIntegrationService {
|
||||
CommonResponse<Void> response =
|
||||
renderWorkerConfigV2Client.batchUpdateWorkerConfigs(workerId, request);
|
||||
handleVoidResponse(response, "批量更新渲染工作器配置失败");
|
||||
// 清理所有配置缓存
|
||||
clearAllWorkerConfigCache(workerId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -142,6 +151,26 @@ public class RenderWorkerConfigIntegrationService {
|
||||
|
||||
request.setConfigs(configs);
|
||||
batchUpdateWorkerConfigs(workerId, request);
|
||||
// 缓存清理已在 batchUpdateWorkerConfigs 中处理
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理指定配置的缓存
|
||||
*/
|
||||
private void clearWorkerConfigCache(Long workerId, String configKey) {
|
||||
fallbackService.clearFallbackCache(SERVICE_NAME, "worker:configs:" + workerId);
|
||||
fallbackService.clearFallbackCache(SERVICE_NAME, "worker:flat:config:" + workerId);
|
||||
if (configKey != null) {
|
||||
fallbackService.clearFallbackCache(SERVICE_NAME, "worker:config:" + workerId + ":" + configKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理工作器所有配置缓存
|
||||
*/
|
||||
private void clearAllWorkerConfigCache(Long workerId) {
|
||||
fallbackService.clearFallbackCache(SERVICE_NAME, "worker:configs:" + workerId);
|
||||
fallbackService.clearFallbackCache(SERVICE_NAME, "worker:flat:config:" + workerId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -58,6 +58,8 @@ public class RenderWorkerIntegrationService {
|
||||
log.debug("更新渲染工作器, id: {}, name: {}", id, request.getName());
|
||||
CommonResponse<Void> response = renderWorkerV2Client.updateWorker(id, request);
|
||||
handleVoidResponse(response, "更新渲染工作器失败");
|
||||
// 清理工作器相关缓存
|
||||
fallbackService.clearFallbackCache(SERVICE_NAME, "worker:" + id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,6 +69,8 @@ public class RenderWorkerIntegrationService {
|
||||
log.debug("删除渲染工作器, id: {}", id);
|
||||
CommonResponse<Void> response = renderWorkerV2Client.deleteWorker(id);
|
||||
handleVoidResponse(response, "删除渲染工作器失败");
|
||||
// 清理工作器相关缓存
|
||||
fallbackService.clearFallbackCache(SERVICE_NAME, "worker:" + id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -55,25 +55,53 @@ public class ScenicConfigIntegrationService {
|
||||
public ScenicConfigV2DTO createConfig(Long scenicId, CreateConfigRequest request) {
|
||||
log.debug("创建景区配置, scenicId: {}, configKey: {}", scenicId, request.getConfigKey());
|
||||
CommonResponse<ScenicConfigV2DTO> response = scenicConfigV2Client.createConfig(scenicId, request);
|
||||
return handleResponse(response, "创建景区配置失败");
|
||||
ScenicConfigV2DTO result = handleResponse(response, "创建景区配置失败");
|
||||
// 清理配置列表缓存
|
||||
clearConfigCache(scenicId, request.getConfigKey());
|
||||
return result;
|
||||
}
|
||||
|
||||
public ScenicConfigV2DTO updateConfig(Long scenicId, String id, UpdateConfigRequest request) {
|
||||
log.debug("更新景区配置, scenicId: {}, id: {}", scenicId, id);
|
||||
CommonResponse<ScenicConfigV2DTO> response = scenicConfigV2Client.updateConfig(scenicId, id, request);
|
||||
return handleResponse(response, "更新景区配置失败");
|
||||
ScenicConfigV2DTO result = handleResponse(response, "更新景区配置失败");
|
||||
// 清理配置相关缓存
|
||||
clearConfigCache(scenicId, request.getConfigKey());
|
||||
return result;
|
||||
}
|
||||
|
||||
public void deleteConfig(Long scenicId, String id) {
|
||||
log.debug("删除景区配置, scenicId: {}, id: {}", scenicId, id);
|
||||
CommonResponse<Void> response = scenicConfigV2Client.deleteConfig(scenicId, id);
|
||||
handleResponse(response, "删除景区配置失败");
|
||||
// 清理配置列表缓存(无法确定具体key,清理整个列表缓存)
|
||||
fallbackService.clearFallbackCache(SERVICE_NAME, "scenic:configs:" + scenicId);
|
||||
}
|
||||
|
||||
public BatchUpdateResponse batchUpdateConfigs(Long scenicId, BatchConfigRequest request) {
|
||||
log.debug("批量更新景区配置, scenicId: {}, configs count: {}", scenicId, request.getConfigs().size());
|
||||
CommonResponse<BatchUpdateResponse> response = scenicConfigV2Client.batchUpdateConfigs(scenicId, request);
|
||||
return handleResponse(response, "批量更新景区配置失败");
|
||||
BatchUpdateResponse result = handleResponse(response, "批量更新景区配置失败");
|
||||
// 清理所有相关缓存
|
||||
clearAllConfigCache(scenicId);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理指定配置的缓存
|
||||
*/
|
||||
private void clearConfigCache(Long scenicId, String configKey) {
|
||||
fallbackService.clearFallbackCache(SERVICE_NAME, "scenic:configs:" + scenicId);
|
||||
if (configKey != null) {
|
||||
fallbackService.clearFallbackCache(SERVICE_NAME, "scenic:config:" + scenicId + ":" + configKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理景区所有配置相关缓存
|
||||
*/
|
||||
private void clearAllConfigCache(Long scenicId) {
|
||||
fallbackService.clearFallbackCache(SERVICE_NAME, "scenic:configs:" + scenicId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -48,13 +48,18 @@ public class ScenicIntegrationService {
|
||||
public ScenicV2DTO updateScenic(Long scenicId, UpdateScenicRequest request) {
|
||||
log.debug("更新景区信息, scenicId: {}", scenicId);
|
||||
CommonResponse<ScenicV2DTO> response = scenicV2Client.updateScenic(scenicId, request);
|
||||
return handleResponse(response, "更新景区信息失败");
|
||||
ScenicV2DTO result = handleResponse(response, "更新景区信息失败");
|
||||
// 清理缓存,确保下次查询获取最新数据
|
||||
fallbackService.clearFallbackCache(SERVICE_NAME, "scenic:" + scenicId);
|
||||
return result;
|
||||
}
|
||||
|
||||
public void deleteScenic(Long scenicId) {
|
||||
log.debug("删除景区, scenicId: {}", scenicId);
|
||||
CommonResponse<Void> response = scenicV2Client.deleteScenic(scenicId);
|
||||
handleResponse(response, "删除景区失败");
|
||||
// 清理缓存
|
||||
fallbackService.clearFallbackCache(SERVICE_NAME, "scenic:" + scenicId);
|
||||
}
|
||||
|
||||
public ScenicFilterPageResponse filterScenics(ScenicFilterRequest request) {
|
||||
|
||||
23
src/main/java/com/ycwl/basic/mapper/PrinterGuideMapper.java
Normal file
23
src/main/java/com/ycwl/basic/mapper/PrinterGuideMapper.java
Normal file
@@ -0,0 +1,23 @@
|
||||
package com.ycwl.basic.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.ycwl.basic.model.pc.printer.entity.PrinterGuideEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface PrinterGuideMapper extends BaseMapper<PrinterGuideEntity> {
|
||||
List<PrinterGuideEntity> listByPrinterId(@Param("printerId") Integer printerId);
|
||||
|
||||
List<PrinterGuideEntity> listEnabledByPrinterId(@Param("printerId") Integer printerId);
|
||||
|
||||
int insertGuide(PrinterGuideEntity entity);
|
||||
|
||||
int deleteById(@Param("id") Integer id);
|
||||
|
||||
int updateSortOrder(@Param("id") Integer id, @Param("sortOrder") Integer sortOrder);
|
||||
|
||||
int toggleEnabled(@Param("id") Integer id);
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
package com.ycwl.basic.mapper;
|
||||
|
||||
import com.ycwl.basic.model.pc.device.resp.DeviceSourceStatsVO;
|
||||
import com.ycwl.basic.model.pc.device.resp.DeviceSourceTimelineVO;
|
||||
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
|
||||
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
|
||||
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;
|
||||
@@ -181,4 +184,59 @@ public interface SourceMapper {
|
||||
* @return source实体列表
|
||||
*/
|
||||
List<SourceEntity> listSourceByFaceRelation(Long faceId, Integer type);
|
||||
|
||||
/**
|
||||
* 根据sourceId列表查询关联的faceId
|
||||
* @param sourceIds sourceId列表
|
||||
* @return member_source记录列表(包含sourceId和faceId)
|
||||
*/
|
||||
List<MemberSourceEntity> listFaceIdsBySourceIds(List<Long> sourceIds);
|
||||
|
||||
/**
|
||||
* 根据faceId分页查询关联的source记录
|
||||
* @param sourceReqQuery 查询参数(需设置faceId)
|
||||
* @return source响应列表
|
||||
*/
|
||||
List<SourceRespVO> pageByFaceId(SourceReqQuery sourceReqQuery);
|
||||
|
||||
int softDeleteRelation(Long id);
|
||||
|
||||
int reactivateRelation(Long id);
|
||||
|
||||
List<SourceRespVO> pageDeletedByFaceId(SourceReqQuery sourceReqQuery);
|
||||
|
||||
MemberSourceEntity getMemberSourceById(Long id);
|
||||
|
||||
/**
|
||||
* 设备拍摄统计:拍摄总数、拍摄人数、售出张数、赠送张数、售出人数
|
||||
* @param deviceId 设备ID
|
||||
* @param startTime 开始时间
|
||||
* @param endTime 结束时间
|
||||
* @return 统计结果
|
||||
*/
|
||||
DeviceSourceStatsVO getDeviceSourceStats(Long deviceId, Date startTime, Date endTime);
|
||||
|
||||
/**
|
||||
* 按 5 分钟分桶统计设备 type=2 的拍摄数量(仅返回有数据的桶)
|
||||
* @param deviceId 设备ID
|
||||
* @param startTime 开始时间
|
||||
* @param endTime 结束时间
|
||||
* @return 有数据的时间桶列表
|
||||
*/
|
||||
List<DeviceSourceTimelineVO> getDeviceSourceTimeline(Long deviceId, Date startTime, Date endTime);
|
||||
|
||||
/**
|
||||
* 根据会员ID和素材ID查询 member_source 关联记录
|
||||
* @param memberId 会员ID
|
||||
* @param sourceId 素材ID
|
||||
* @return 关联记录(含 faceId 等信息)
|
||||
*/
|
||||
MemberSourceEntity getMemberSourceByMemberAndSourceId(@Param("memberId") Long memberId, @Param("sourceId") Long sourceId);
|
||||
|
||||
/**
|
||||
* 根据会员ID和素材ID更新 member_source 关联记录的购买状态
|
||||
* @param memberSourceEntity 包含 memberId、sourceId、isBuy、orderId
|
||||
* @return 影响行数
|
||||
*/
|
||||
int updateRelationBySourceId(MemberSourceEntity memberSourceEntity);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.ycwl.basic.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.ycwl.basic.model.pc.videoreview.dto.AdminVideoReviewLogReqDTO;
|
||||
import com.ycwl.basic.model.pc.videoreview.dto.AdminVideoReviewLogRespDTO;
|
||||
import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewListReqDTO;
|
||||
import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewRespDTO;
|
||||
import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewStatisticsRespDTO;
|
||||
@@ -25,6 +27,14 @@ public interface VideoReviewMapper extends BaseMapper<VideoReviewEntity> {
|
||||
*/
|
||||
List<VideoReviewRespDTO> selectReviewList(VideoReviewListReqDTO reqDTO);
|
||||
|
||||
/**
|
||||
* 管理后台分页查询评价日志(带更详细的管理信息)
|
||||
*
|
||||
* @param reqDTO 查询条件
|
||||
* @return 评价日志列表
|
||||
*/
|
||||
List<AdminVideoReviewLogRespDTO> selectAdminReviewLogList(AdminVideoReviewLogReqDTO reqDTO);
|
||||
|
||||
/**
|
||||
* 统计总评价数
|
||||
*
|
||||
@@ -63,9 +73,9 @@ public interface VideoReviewMapper extends BaseMapper<VideoReviewEntity> {
|
||||
List<VideoReviewStatisticsRespDTO.ScenicReviewRank> countScenicRank(@Param("limit") int limit);
|
||||
|
||||
/**
|
||||
* 查询所有机位评价数据(用于后端计算平均值)
|
||||
* 查询所有问题机位ID列表(用于后端统计问题机位)
|
||||
*
|
||||
* @return 机位评价列表(Map结构: 机位ID -> 评分)
|
||||
* @return 问题机位ID列表
|
||||
*/
|
||||
List<Map<String, Integer>> selectAllCameraPositionRatings();
|
||||
List<List<Long>> selectAllProblemDeviceIds();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.ycwl.basic.mapper.task;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.ycwl.basic.model.task.entity.TaskRenderJobMappingEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Task与RenderJob关联Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface TaskRenderJobMappingMapper extends BaseMapper<TaskRenderJobMappingEntity> {
|
||||
|
||||
/**
|
||||
* 根据taskId查询mapping
|
||||
*/
|
||||
TaskRenderJobMappingEntity selectByTaskId(@Param("taskId") Long taskId);
|
||||
|
||||
/**
|
||||
* 根据renderJobId查询mapping
|
||||
*/
|
||||
TaskRenderJobMappingEntity selectByRenderJobId(@Param("renderJobId") Long renderJobId);
|
||||
|
||||
/**
|
||||
* 查询需要轮询的记录
|
||||
* 条件:状态为PENDING或PREVIEW_READY,且最后检查时间超过指定间隔
|
||||
*/
|
||||
List<TaskRenderJobMappingEntity> selectPendingForPolling(
|
||||
@Param("statuses") List<String> statuses,
|
||||
@Param("checkIntervalSeconds") int checkIntervalSeconds,
|
||||
@Param("limit") int limit
|
||||
);
|
||||
|
||||
/**
|
||||
* 更新渲染状态和片段信息
|
||||
*/
|
||||
int updateRenderStatus(
|
||||
@Param("id") Long id,
|
||||
@Param("renderStatus") String renderStatus,
|
||||
@Param("publishedCount") Integer publishedCount,
|
||||
@Param("segmentCount") Integer segmentCount,
|
||||
@Param("previewUrl") String previewUrl,
|
||||
@Param("mp4Url") String mp4Url,
|
||||
@Param("lastCheckTime") Date lastCheckTime
|
||||
);
|
||||
|
||||
/**
|
||||
* 更新为失败状态
|
||||
*/
|
||||
int updateToFailed(
|
||||
@Param("id") Long id,
|
||||
@Param("errorCode") String errorCode,
|
||||
@Param("errorMessage") String errorMessage,
|
||||
@Param("lastCheckTime") Date lastCheckTime
|
||||
);
|
||||
|
||||
/**
|
||||
* 增加重试次数
|
||||
*/
|
||||
int incrementRetryCount(@Param("id") Long id);
|
||||
}
|
||||
@@ -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,20 @@
|
||||
package com.ycwl.basic.model.pc.device.resp;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 设备拍摄统计 VO
|
||||
*/
|
||||
@Data
|
||||
public class DeviceSourceStatsVO {
|
||||
/** 拍摄总数(source type=2 记录数) */
|
||||
private Integer totalShots;
|
||||
/** 拍摄人数(关联的不同 face_id 数) */
|
||||
private Integer totalFaces;
|
||||
/** 售出张数(is_buy=1 的 member_source 记录数) */
|
||||
private Integer soldCount;
|
||||
/** 赠送张数(is_free=1 的 member_source 记录数) */
|
||||
private Integer freeCount;
|
||||
/** 售出人数(is_buy=1 的不同 face_id 数) */
|
||||
private Integer soldFaceCount;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.ycwl.basic.model.pc.device.resp;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 设备拍摄时间线数据点(5 分钟一个桶)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class DeviceSourceTimelineVO {
|
||||
/** 时间桶起始时刻 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm", timezone = "GMT+8")
|
||||
private Date time;
|
||||
/** 该桶内 source type=2 的记录数 */
|
||||
private Integer count;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.ycwl.basic.model.pc.printer.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
@TableName("printer_guide")
|
||||
public class PrinterGuideEntity {
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Integer id;
|
||||
private Integer printerId;
|
||||
private String imageUrl;
|
||||
private Integer sortOrder;
|
||||
private Integer enabled;
|
||||
private Date createTime;
|
||||
private Date updateTime;
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package com.ycwl.basic.model.pc.source.entity;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
@TableName("member_source")
|
||||
public class MemberSourceEntity {
|
||||
@@ -15,4 +17,6 @@ public class MemberSourceEntity {
|
||||
private Integer isBuy;
|
||||
private Long orderId;
|
||||
private Integer isFree;
|
||||
private Integer deleted;
|
||||
private Date deletedAt;
|
||||
}
|
||||
|
||||
@@ -46,4 +46,6 @@ public class SourceRespVO {
|
||||
// 是否购买:0未购买,1已购买
|
||||
private int isBuy;
|
||||
private int isFree;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date deletedAt;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package com.ycwl.basic.model.pc.videoreview.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 管理后台视频评价日志查询请求DTO
|
||||
*/
|
||||
@Data
|
||||
public class AdminVideoReviewLogReqDTO {
|
||||
|
||||
/**
|
||||
* 评价ID(可选,精确查询)
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 视频ID(可选)
|
||||
*/
|
||||
private Long videoId;
|
||||
|
||||
/**
|
||||
* 景区ID(可选)
|
||||
*/
|
||||
private Long scenicId;
|
||||
|
||||
/**
|
||||
* 评价人ID(可选)
|
||||
*/
|
||||
private Long creator;
|
||||
|
||||
/**
|
||||
* 评价人名称(可选,模糊查询)
|
||||
*/
|
||||
private String creatorName;
|
||||
|
||||
/**
|
||||
* 评分(可选,精确匹配)
|
||||
*/
|
||||
private Integer rating;
|
||||
|
||||
/**
|
||||
* 最小评分(可选,范围查询)
|
||||
*/
|
||||
private Integer minRating;
|
||||
|
||||
/**
|
||||
* 最大评分(可选,范围查询)
|
||||
*/
|
||||
private Integer maxRating;
|
||||
|
||||
/**
|
||||
* 开始时间(可选,格式: yyyy-MM-dd HH:mm:ss)
|
||||
*/
|
||||
private String startTime;
|
||||
|
||||
/**
|
||||
* 结束时间(可选,格式: yyyy-MM-dd HH:mm:ss)
|
||||
*/
|
||||
private String endTime;
|
||||
|
||||
/**
|
||||
* 关键词搜索(可选,搜索评价内容、景区名称、模板名称)
|
||||
*/
|
||||
private String keyword;
|
||||
|
||||
/**
|
||||
* 模板ID(可选)
|
||||
*/
|
||||
private Long templateId;
|
||||
|
||||
/**
|
||||
* 模板名称(可选,模糊查询)
|
||||
*/
|
||||
private String templateName;
|
||||
|
||||
/**
|
||||
* 是否有机位评价(可选)
|
||||
* true: 仅查询有机位评价的记录
|
||||
* false: 仅查询无机位评价的记录
|
||||
* null: 不限制
|
||||
*/
|
||||
private Boolean hasCameraRating;
|
||||
|
||||
/**
|
||||
* 问题机位ID(可选,筛选包含该机位ID的评价)
|
||||
* 任意一个问题机位匹配即可
|
||||
*/
|
||||
private Long problemDeviceId;
|
||||
|
||||
/**
|
||||
* 问题标签(可选,筛选包含该标签的评价)
|
||||
* 任意一个标签匹配即可
|
||||
*/
|
||||
private String problemTag;
|
||||
|
||||
/**
|
||||
* 来源(可选,筛选指定来源的评价)
|
||||
* 固定值: ORDER(订单), RENDER(渲染)
|
||||
*/
|
||||
private String source;
|
||||
|
||||
/**
|
||||
* 页码(必填,默认1)
|
||||
*/
|
||||
private Integer pageNum = 1;
|
||||
|
||||
/**
|
||||
* 每页数量(必填,默认20)
|
||||
*/
|
||||
private Integer pageSize = 20;
|
||||
|
||||
/**
|
||||
* 排序字段(可选,默认create_time)
|
||||
* 可选值: create_time, rating, update_time, id
|
||||
*/
|
||||
private String orderBy = "create_time";
|
||||
|
||||
/**
|
||||
* 排序方向(可选,默认DESC)
|
||||
* 可选值: ASC, DESC
|
||||
*/
|
||||
private String orderDirection = "DESC";
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package com.ycwl.basic.model.pc.videoreview.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 管理后台视频评价日志响应DTO
|
||||
*/
|
||||
@Data
|
||||
public class AdminVideoReviewLogRespDTO {
|
||||
|
||||
/**
|
||||
* 评价ID
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 视频ID
|
||||
*/
|
||||
private Long videoId;
|
||||
|
||||
/**
|
||||
* 视频URL(关联查询)
|
||||
*/
|
||||
private String videoUrl;
|
||||
|
||||
/**
|
||||
* 视频时长(秒,关联查询video表)
|
||||
*/
|
||||
private BigDecimal duration;
|
||||
|
||||
/**
|
||||
* 任务参数(JSON字符串,关联查询task表)
|
||||
*/
|
||||
private String taskParams;
|
||||
|
||||
/**
|
||||
* 模板ID(关联查询video表)
|
||||
*/
|
||||
private Long templateId;
|
||||
|
||||
/**
|
||||
* 模板名称(关联查询)
|
||||
*/
|
||||
private String templateName;
|
||||
|
||||
/**
|
||||
* 景区ID
|
||||
*/
|
||||
private Long scenicId;
|
||||
|
||||
/**
|
||||
* 景区名称(关联查询)
|
||||
*/
|
||||
private String scenicName;
|
||||
|
||||
/**
|
||||
* 评价人ID(管理员ID)
|
||||
*/
|
||||
private Long creator;
|
||||
|
||||
/**
|
||||
* 评价人名称(关联查询)
|
||||
*/
|
||||
private String creatorName;
|
||||
|
||||
/**
|
||||
* 评价人账号(关联查询,管理后台显示)
|
||||
*/
|
||||
private String creatorAccount;
|
||||
|
||||
/**
|
||||
* 购买评分 1-5
|
||||
*/
|
||||
private Integer rating;
|
||||
|
||||
/**
|
||||
* 文字评价内容
|
||||
*/
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 有问题的机位ID列表
|
||||
* 格式: [12345, 12346, 12347]
|
||||
*/
|
||||
private List<Long> problemDeviceIds;
|
||||
|
||||
/**
|
||||
* 问题机位数量(方便前端展示)
|
||||
*/
|
||||
private Integer problemDeviceCount;
|
||||
|
||||
/**
|
||||
* 问题标签列表
|
||||
* 格式: ["画面模糊", "抖动严重", "色彩异常"]
|
||||
*/
|
||||
private List<String> problemTags;
|
||||
|
||||
/**
|
||||
* 来源
|
||||
* 固定值: ORDER(订单), RENDER(渲染)
|
||||
*/
|
||||
private String source;
|
||||
|
||||
/**
|
||||
* 来源ID
|
||||
* 用于溯源,关联订单ID或渲染任务ID等
|
||||
*/
|
||||
private Long sourceId;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date updateTime;
|
||||
|
||||
/**
|
||||
* 操作时长(创建到更新的时间差,单位:秒)
|
||||
*/
|
||||
private Long operationDuration;
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package com.ycwl.basic.model.pc.videoreview.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 新增视频评价请求DTO
|
||||
@@ -26,9 +26,28 @@ public class VideoReviewAddReqDTO {
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 机位评价JSON(可选)
|
||||
* 格式: {"12345": 5, "12346": 4}
|
||||
* key为机位ID,value为该机位的评分(1-5)
|
||||
* 有问题的机位ID列表(可选)
|
||||
* 格式: [12345, 12346, 12347]
|
||||
* 选择有问题的机位ID
|
||||
*/
|
||||
private Map<String, Integer> cameraPositionRating;
|
||||
private List<Long> problemDeviceIds;
|
||||
|
||||
/**
|
||||
* 问题标签列表(可选)
|
||||
* 格式: ["画面模糊", "抖动严重", "色彩异常"]
|
||||
* 可多选问题标签
|
||||
*/
|
||||
private List<String> problemTags;
|
||||
|
||||
/**
|
||||
* 来源(必填)
|
||||
* 固定值: ORDER(订单), RENDER(渲染)
|
||||
*/
|
||||
private String source;
|
||||
|
||||
/**
|
||||
* 来源ID(可选)
|
||||
* 用于溯源,关联订单ID或渲染任务ID等
|
||||
*/
|
||||
private Long sourceId;
|
||||
}
|
||||
|
||||
@@ -53,6 +53,24 @@ public class VideoReviewListReqDTO {
|
||||
*/
|
||||
private String keyword;
|
||||
|
||||
/**
|
||||
* 问题机位ID(可选,筛选包含该机位ID的评价)
|
||||
* 任意一个问题机位匹配即可
|
||||
*/
|
||||
private Long problemDeviceId;
|
||||
|
||||
/**
|
||||
* 问题标签(可选,筛选包含该标签的评价)
|
||||
* 任意一个标签匹配即可
|
||||
*/
|
||||
private String problemTag;
|
||||
|
||||
/**
|
||||
* 来源(可选,筛选指定来源的评价)
|
||||
* 固定值: ORDER(订单), RENDER(渲染)
|
||||
*/
|
||||
private String source;
|
||||
|
||||
/**
|
||||
* 页码(必填,默认1)
|
||||
*/
|
||||
|
||||
@@ -3,8 +3,9 @@ package com.ycwl.basic.model.pc.videoreview.dto;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 视频评价详情响应DTO
|
||||
@@ -27,6 +28,16 @@ public class VideoReviewRespDTO {
|
||||
*/
|
||||
private String videoUrl;
|
||||
|
||||
/**
|
||||
* 视频时长(秒,关联查询video表)
|
||||
*/
|
||||
private BigDecimal duration;
|
||||
|
||||
/**
|
||||
* 任务参数(JSON字符串,关联查询task表)
|
||||
*/
|
||||
private String taskParams;
|
||||
|
||||
/**
|
||||
* 模板ID(关联查询video表)
|
||||
*/
|
||||
@@ -68,11 +79,28 @@ public class VideoReviewRespDTO {
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 机位评价JSON
|
||||
* 格式: {"12345": 5, "12346": 4}
|
||||
* key为机位ID,value为该机位的评分(1-5)
|
||||
* 有问题的机位ID列表
|
||||
* 格式: [12345, 12346, 12347]
|
||||
*/
|
||||
private Map<String, Integer> cameraPositionRating;
|
||||
private List<Long> problemDeviceIds;
|
||||
|
||||
/**
|
||||
* 问题标签列表
|
||||
* 格式: ["画面模糊", "抖动严重", "色彩异常"]
|
||||
*/
|
||||
private List<String> problemTags;
|
||||
|
||||
/**
|
||||
* 来源
|
||||
* 固定值: ORDER(订单), RENDER(渲染)
|
||||
*/
|
||||
private String source;
|
||||
|
||||
/**
|
||||
* 来源ID
|
||||
* 用于溯源,关联订单ID或渲染任务ID等
|
||||
*/
|
||||
private Long sourceId;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
|
||||
@@ -40,10 +40,10 @@ public class VideoReviewStatisticsRespDTO {
|
||||
private List<ScenicReviewRank> scenicRankList;
|
||||
|
||||
/**
|
||||
* 机位评价统计
|
||||
* key: 机位ID, value: 该机位的平均评分
|
||||
* 问题机位统计
|
||||
* key: 机位ID, value: 该机位被标记为问题的次数
|
||||
*/
|
||||
private Map<String, BigDecimal> cameraPositionAverage;
|
||||
private Map<Long, Long> problemDeviceStatistics;
|
||||
|
||||
/**
|
||||
* 景区评价排行内部类
|
||||
|
||||
@@ -4,12 +4,13 @@ import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.ycwl.basic.handler.MapTypeHandler;
|
||||
import com.ycwl.basic.handler.LongListTypeHandler;
|
||||
import com.ycwl.basic.handler.StringListTypeHandler;
|
||||
import lombok.Data;
|
||||
import org.apache.ibatis.type.JdbcType;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 视频评价实体类
|
||||
@@ -50,12 +51,32 @@ public class VideoReviewEntity {
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 机位评价JSON
|
||||
* 格式: {"12345": 5, "12346": 4}
|
||||
* key为机位ID,value为该机位的评分(1-5)
|
||||
* 有问题的机位ID列表
|
||||
* 格式: [12345, 12346, 12347]
|
||||
* 存储被标记为有问题的机位ID
|
||||
*/
|
||||
@TableField(typeHandler = MapTypeHandler.class, jdbcType = JdbcType.VARCHAR)
|
||||
private Map<String, Integer> cameraPositionRating;
|
||||
@TableField(typeHandler = LongListTypeHandler.class, jdbcType = JdbcType.VARCHAR)
|
||||
private List<Long> problemDeviceIds;
|
||||
|
||||
/**
|
||||
* 问题标签列表
|
||||
* 格式: ["画面模糊", "抖动严重", "色彩异常"]
|
||||
* 存储视频或机位的问题标签,可多选
|
||||
*/
|
||||
@TableField(typeHandler = StringListTypeHandler.class, jdbcType = JdbcType.VARCHAR)
|
||||
private List<String> problemTags;
|
||||
|
||||
/**
|
||||
* 来源
|
||||
* 固定值: ORDER(订单), RENDER(渲染)
|
||||
*/
|
||||
private String source;
|
||||
|
||||
/**
|
||||
* 来源ID
|
||||
* 用于溯源,关联订单ID或渲染任务ID等
|
||||
*/
|
||||
private Long sourceId;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
|
||||
@@ -31,4 +31,11 @@ public class CreateVirtualOrderRequest {
|
||||
* 打印图片URL(可选,如果提供则使用此URL进行打印)
|
||||
*/
|
||||
private String printImgUrl;
|
||||
|
||||
/**
|
||||
* 是否需要实际支付(可选,默认false)
|
||||
* false/null: 创建0元虚拟订单,立即完成购买
|
||||
* true: 创建待支付订单(计算实际价格),由前端处理支付流程
|
||||
*/
|
||||
private Boolean needActualPayment;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.ycwl.basic.model.task.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Task与RenderJob关联实体
|
||||
* 用于跟踪task和zt-render-worker服务中渲染作业的关联
|
||||
*/
|
||||
@Data
|
||||
@TableName("task_render_job_mapping")
|
||||
public class TaskRenderJobMappingEntity {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 任务ID(task表的id)
|
||||
*/
|
||||
private Long taskId;
|
||||
|
||||
/**
|
||||
* 渲染作业ID(zt-render-worker返回的jobId)
|
||||
*/
|
||||
private Long renderJobId;
|
||||
|
||||
/**
|
||||
* 渲染状态
|
||||
* PENDING-等待中, PREVIEW_READY-预览就绪, COMPLETED-已完成, FAILED-失败
|
||||
*/
|
||||
private String renderStatus;
|
||||
|
||||
/**
|
||||
* 已发布片段数
|
||||
*/
|
||||
private Integer publishedCount;
|
||||
|
||||
/**
|
||||
* 总片段数
|
||||
*/
|
||||
private Integer segmentCount;
|
||||
|
||||
/**
|
||||
* 预览播放地址(HLS)
|
||||
*/
|
||||
private String previewUrl;
|
||||
|
||||
/**
|
||||
* 最终MP4地址
|
||||
*/
|
||||
private String mp4Url;
|
||||
|
||||
/**
|
||||
* 错误码
|
||||
*/
|
||||
private String errorCode;
|
||||
|
||||
/**
|
||||
* 错误信息
|
||||
*/
|
||||
private String errorMessage;
|
||||
|
||||
/**
|
||||
* 重试次数
|
||||
*/
|
||||
private Integer retryCount;
|
||||
|
||||
/**
|
||||
* 最后检查时间
|
||||
*/
|
||||
private Date lastCheckTime;
|
||||
|
||||
private Date createTime;
|
||||
private Date updateTime;
|
||||
|
||||
/**
|
||||
* 渲染状态常量
|
||||
*/
|
||||
public static final String STATUS_PENDING = "PENDING";
|
||||
public static final String STATUS_PREVIEW_READY = "PREVIEW_READY";
|
||||
public static final String STATUS_MP4_COMPOSING = "MP4_COMPOSING";
|
||||
public static final String STATUS_COMPLETED = "COMPLETED";
|
||||
public static final String STATUS_FAILED = "FAILED";
|
||||
|
||||
/**
|
||||
* 预览就绪所需的最小已发布片段数
|
||||
*/
|
||||
public static final int MIN_PUBLISHED_FOR_PREVIEW = 3;
|
||||
|
||||
/**
|
||||
* 最大重试次数
|
||||
*/
|
||||
public static final int MAX_RETRY_COUNT = 10;
|
||||
}
|
||||
@@ -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查询匹配的阶梯价格(支持景区维度)
|
||||
*/
|
||||
|
||||
@@ -15,109 +15,109 @@ import java.util.List;
|
||||
*/
|
||||
@Mapper
|
||||
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);
|
||||
|
||||
|
||||
/**
|
||||
* 根据批次ID获取可领取的券码(未领取状态)
|
||||
* @param batchId 批次ID
|
||||
* @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,
|
||||
@Param("limit") Integer limit);
|
||||
|
||||
|
||||
/**
|
||||
* 领取券码(更新状态为已领取)
|
||||
* @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);
|
||||
|
||||
|
||||
/**
|
||||
* 根据批次ID查询券码列表(支持分页)
|
||||
* @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);
|
||||
|
||||
|
||||
/**
|
||||
* 根据批次ID查询第一个可用的券码
|
||||
* @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);
|
||||
|
||||
|
||||
/**
|
||||
* 随机获取一个未被打印过的可用券码
|
||||
* @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) " +
|
||||
"ORDER BY RAND() LIMIT 1")
|
||||
PriceVoucherCode findRandomUnprintedVoucher(@Param("scenicId") Long 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);
|
||||
|
||||
@@ -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,55 +74,64 @@ 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被初始化
|
||||
if (voucherCode.getCurrentUseCount() == null) {
|
||||
voucherCode.setCurrentUseCount(0);
|
||||
}
|
||||
|
||||
|
||||
voucherCodeMapper.updateById(voucherCode);
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验请求参数
|
||||
*/
|
||||
|
||||
@@ -129,13 +129,6 @@ public class PuzzleDuplicationDetector {
|
||||
// 3. 对URL去重
|
||||
Set<String> uniqueUrls = new HashSet<>(imageUrls);
|
||||
|
||||
// 4. 如果去重后只有1个URL,说明所有图片相同
|
||||
if (uniqueUrls.size() == 1) {
|
||||
String duplicateUrl = uniqueUrls.iterator().next();
|
||||
log.warn("检测到重复图片: 所有{}个图片元素使用相同URL: {}", imageUrls.size(), duplicateUrl);
|
||||
throw new DuplicateImageException(duplicateUrl, imageUrls.size());
|
||||
}
|
||||
|
||||
log.debug("重复图片检测通过: 发现{}个不同的图片URL", uniqueUrls.size());
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.ycwl.basic.service;
|
||||
|
||||
import com.github.pagehelper.PageInfo;
|
||||
import com.ycwl.basic.model.pc.videoreview.dto.AdminVideoReviewLogReqDTO;
|
||||
import com.ycwl.basic.model.pc.videoreview.dto.AdminVideoReviewLogRespDTO;
|
||||
import com.ycwl.basic.model.pc.videoreview.dto.VideoPurchaseCheckReqDTO;
|
||||
import com.ycwl.basic.model.pc.videoreview.dto.VideoPurchaseCheckRespDTO;
|
||||
import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewAddReqDTO;
|
||||
@@ -32,6 +34,14 @@ public interface VideoReviewService {
|
||||
*/
|
||||
PageInfo<VideoReviewRespDTO> getReviewList(VideoReviewListReqDTO reqDTO);
|
||||
|
||||
/**
|
||||
* 管理后台分页查询评价日志
|
||||
*
|
||||
* @param reqDTO 查询条件
|
||||
* @return 分页结果
|
||||
*/
|
||||
PageInfo<AdminVideoReviewLogRespDTO> getAdminReviewLogList(AdminVideoReviewLogReqDTO reqDTO);
|
||||
|
||||
/**
|
||||
* 获取评价统计数据
|
||||
*
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.github.pagehelper.PageHelper;
|
||||
import com.github.pagehelper.PageInfo;
|
||||
import com.ycwl.basic.constant.BaseContextHandler;
|
||||
import com.ycwl.basic.enums.VideoReviewSourceEnum;
|
||||
import com.ycwl.basic.exception.BaseException;
|
||||
import com.ycwl.basic.exception.BizException;
|
||||
import com.ycwl.basic.mapper.OrderMapper;
|
||||
@@ -14,6 +15,8 @@ import com.ycwl.basic.mapper.VideoReviewMapper;
|
||||
import com.ycwl.basic.model.pc.order.entity.OrderEntity;
|
||||
import com.ycwl.basic.model.pc.task.entity.TaskEntity;
|
||||
import com.ycwl.basic.model.pc.video.entity.VideoEntity;
|
||||
import com.ycwl.basic.model.pc.videoreview.dto.AdminVideoReviewLogReqDTO;
|
||||
import com.ycwl.basic.model.pc.videoreview.dto.AdminVideoReviewLogRespDTO;
|
||||
import com.ycwl.basic.model.pc.videoreview.dto.VideoPurchaseCheckReqDTO;
|
||||
import com.ycwl.basic.model.pc.videoreview.dto.VideoPurchaseCheckRespDTO;
|
||||
import com.ycwl.basic.repository.DeviceRepository;
|
||||
@@ -72,6 +75,12 @@ public class VideoReviewServiceImpl implements VideoReviewService {
|
||||
if (reqDTO.getRating() == null || reqDTO.getRating() < 1 || reqDTO.getRating() > 5) {
|
||||
throw new BaseException("评分必须在1-5之间");
|
||||
}
|
||||
if (reqDTO.getSource() == null || reqDTO.getSource().isEmpty()) {
|
||||
throw new BaseException("来源不能为空");
|
||||
}
|
||||
if (!VideoReviewSourceEnum.isValid(reqDTO.getSource())) {
|
||||
throw new BaseException("来源值无效,仅支持: ORDER(订单), RENDER(渲染)");
|
||||
}
|
||||
|
||||
// 2. 查询视频信息,获取景区ID
|
||||
VideoEntity video = videoMapper.getEntity(reqDTO.getVideoId());
|
||||
@@ -93,12 +102,16 @@ public class VideoReviewServiceImpl implements VideoReviewService {
|
||||
entity.setCreator(creator);
|
||||
entity.setRating(reqDTO.getRating());
|
||||
entity.setContent(reqDTO.getContent());
|
||||
entity.setCameraPositionRating(reqDTO.getCameraPositionRating());
|
||||
entity.setProblemDeviceIds(reqDTO.getProblemDeviceIds());
|
||||
entity.setProblemTags(reqDTO.getProblemTags());
|
||||
entity.setSource(reqDTO.getSource());
|
||||
entity.setSourceId(reqDTO.getSourceId());
|
||||
|
||||
// 5. 插入数据库
|
||||
videoReviewMapper.insert(entity);
|
||||
|
||||
log.info("管理员[{}]对视频[{}]添加评价成功,评价ID: {}", creator, reqDTO.getVideoId(), entity.getId());
|
||||
log.info("管理员[{}]对视频[{}]添加评价成功,评价ID: {}, 来源: {}, 来源ID: {}",
|
||||
creator, reqDTO.getVideoId(), entity.getId(), reqDTO.getSource(), reqDTO.getSourceId());
|
||||
return entity.getId();
|
||||
}
|
||||
|
||||
@@ -114,6 +127,18 @@ public class VideoReviewServiceImpl implements VideoReviewService {
|
||||
return new PageInfo<>(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageInfo<AdminVideoReviewLogRespDTO> getAdminReviewLogList(AdminVideoReviewLogReqDTO reqDTO) {
|
||||
// 设置分页参数
|
||||
PageHelper.startPage(reqDTO.getPageNum(), reqDTO.getPageSize());
|
||||
|
||||
// 查询列表
|
||||
List<AdminVideoReviewLogRespDTO> list = videoReviewMapper.selectAdminReviewLogList(reqDTO);
|
||||
|
||||
// 封装分页结果
|
||||
return new PageInfo<>(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VideoReviewStatisticsRespDTO getStatistics() {
|
||||
VideoReviewStatisticsRespDTO statistics = new VideoReviewStatisticsRespDTO();
|
||||
@@ -154,9 +179,9 @@ public class VideoReviewServiceImpl implements VideoReviewService {
|
||||
List<VideoReviewStatisticsRespDTO.ScenicReviewRank> scenicRankList = videoReviewMapper.countScenicRank(10);
|
||||
statistics.setScenicRankList(scenicRankList);
|
||||
|
||||
// 6. 机位评价维度平均值
|
||||
Map<String, BigDecimal> cameraPositionAverage = calculateCameraPositionAverage();
|
||||
statistics.setCameraPositionAverage(cameraPositionAverage);
|
||||
// 6. 问题机位统计
|
||||
Map<Long, Long> problemDeviceStatistics = calculateProblemDeviceStatistics();
|
||||
statistics.setProblemDeviceStatistics(problemDeviceStatistics);
|
||||
|
||||
return statistics;
|
||||
}
|
||||
@@ -168,37 +193,11 @@ public class VideoReviewServiceImpl implements VideoReviewService {
|
||||
reqDTO.setPageSize(Integer.MAX_VALUE);
|
||||
List<VideoReviewRespDTO> list = videoReviewMapper.selectReviewList(reqDTO);
|
||||
|
||||
// 2. 收集所有机位ID并批量查询机位名称
|
||||
Set<Long> allDeviceIds = new LinkedHashSet<>();
|
||||
for (VideoReviewRespDTO review : list) {
|
||||
Map<String, Integer> cameraRating = review.getCameraPositionRating();
|
||||
if (cameraRating != null && !cameraRating.isEmpty()) {
|
||||
// 收集机位ID (按顺序)
|
||||
for (String deviceIdStr : cameraRating.keySet()) {
|
||||
try {
|
||||
allDeviceIds.add(Long.valueOf(deviceIdStr));
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn("无效的机位ID: {}", deviceIdStr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 批量查询机位名称
|
||||
Map<Long, String> deviceNames = new HashMap<>();
|
||||
if (!allDeviceIds.isEmpty()) {
|
||||
deviceNames = deviceRepository.batchGetDeviceNames(new ArrayList<>(allDeviceIds));
|
||||
}
|
||||
|
||||
// 对机位ID按ID排序,保证表头顺序一致
|
||||
List<Long> sortedDeviceIds = new ArrayList<>(allDeviceIds);
|
||||
sortedDeviceIds.sort(Long::compareTo);
|
||||
|
||||
// 3. 创建Excel工作簿
|
||||
// 2. 创建Excel工作簿
|
||||
Workbook workbook = new XSSFWorkbook();
|
||||
Sheet sheet = workbook.createSheet("视频评价数据");
|
||||
|
||||
// 4. 创建标题行样式
|
||||
// 3. 创建标题行样式
|
||||
CellStyle headerStyle = workbook.createCellStyle();
|
||||
Font headerFont = workbook.createFont();
|
||||
headerFont.setBold(true);
|
||||
@@ -206,7 +205,7 @@ public class VideoReviewServiceImpl implements VideoReviewService {
|
||||
headerStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
|
||||
headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
|
||||
|
||||
// 5. 生成动态表头 - 使用机位名称作为表头
|
||||
// 4. 生成表头
|
||||
Row headerRow = sheet.createRow(0);
|
||||
List<String> headerList = new ArrayList<>();
|
||||
headerList.add("评价ID");
|
||||
@@ -216,14 +215,8 @@ public class VideoReviewServiceImpl implements VideoReviewService {
|
||||
headerList.add("评价人名称");
|
||||
headerList.add("评分");
|
||||
headerList.add("文字评价");
|
||||
|
||||
// 添加机位列 - 表头直接使用机位名称
|
||||
Map<Long, String> finalDeviceNames = deviceNames;
|
||||
for (Long deviceId : sortedDeviceIds) {
|
||||
String deviceName = finalDeviceNames.getOrDefault(deviceId, "未知设备(ID:" + deviceId + ")");
|
||||
headerList.add(deviceName);
|
||||
}
|
||||
|
||||
headerList.add("问题机位ID列表");
|
||||
headerList.add("问题标签");
|
||||
headerList.add("创建时间");
|
||||
headerList.add("更新时间");
|
||||
|
||||
@@ -234,7 +227,7 @@ public class VideoReviewServiceImpl implements VideoReviewService {
|
||||
cell.setCellStyle(headerStyle);
|
||||
}
|
||||
|
||||
// 6. 填充数据
|
||||
// 5. 填充数据
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
int rowNum = 1;
|
||||
|
||||
@@ -249,41 +242,37 @@ public class VideoReviewServiceImpl implements VideoReviewService {
|
||||
row.createCell(colIndex++).setCellValue(review.getScenicName());
|
||||
row.createCell(colIndex++).setCellValue(review.getCreatorName());
|
||||
row.createCell(colIndex++).setCellValue(review.getRating());
|
||||
row.createCell(colIndex++).setCellValue(review.getContent());
|
||||
row.createCell(colIndex++).setCellValue(review.getContent() != null ? review.getContent() : "");
|
||||
|
||||
// 机位评价列 - 按表头顺序填充
|
||||
Map<String, Integer> cameraRating = review.getCameraPositionRating();
|
||||
for (Long deviceId : sortedDeviceIds) {
|
||||
String deviceIdStr = String.valueOf(deviceId);
|
||||
Integer rating = null;
|
||||
// 问题机位ID列表
|
||||
List<Long> problemDeviceIds = review.getProblemDeviceIds();
|
||||
String problemDeviceIdsStr = (problemDeviceIds != null && !problemDeviceIds.isEmpty())
|
||||
? problemDeviceIds.toString()
|
||||
: "";
|
||||
row.createCell(colIndex++).setCellValue(problemDeviceIdsStr);
|
||||
|
||||
if (cameraRating != null && cameraRating.containsKey(deviceIdStr)) {
|
||||
rating = cameraRating.get(deviceIdStr);
|
||||
}
|
||||
|
||||
Cell cell = row.createCell(colIndex++);
|
||||
if (rating != null) {
|
||||
cell.setCellValue(rating);
|
||||
} else {
|
||||
cell.setCellValue("");
|
||||
}
|
||||
}
|
||||
// 问题标签
|
||||
List<String> problemTags = review.getProblemTags();
|
||||
String problemTagsStr = (problemTags != null && !problemTags.isEmpty())
|
||||
? String.join(", ", problemTags)
|
||||
: "";
|
||||
row.createCell(colIndex++).setCellValue(problemTagsStr);
|
||||
|
||||
// 时间列
|
||||
row.createCell(colIndex++).setCellValue(review.getCreateTime() != null ? sdf.format(review.getCreateTime()) : "");
|
||||
row.createCell(colIndex).setCellValue(review.getUpdateTime() != null ? sdf.format(review.getUpdateTime()) : "");
|
||||
}
|
||||
|
||||
// 7. 自动调整列宽
|
||||
// 6. 自动调整列宽
|
||||
for (int i = 0; i < headerList.size(); i++) {
|
||||
sheet.autoSizeColumn(i);
|
||||
}
|
||||
|
||||
// 8. 写入输出流
|
||||
// 7. 写入输出流
|
||||
workbook.write(outputStream);
|
||||
workbook.close();
|
||||
|
||||
log.info("导出视频评价数据成功,共{}条,机位数:{}", list.size(), sortedDeviceIds.size());
|
||||
log.info("导出视频评价数据成功,共{}条", list.size());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -338,32 +327,25 @@ public class VideoReviewServiceImpl implements VideoReviewService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算各机位的平均评分
|
||||
* 统计问题机位
|
||||
* 统计每个机位被标记为问题的次数
|
||||
*/
|
||||
private Map<String, BigDecimal> calculateCameraPositionAverage() {
|
||||
List<Map<String, Integer>> allRatings = videoReviewMapper.selectAllCameraPositionRatings();
|
||||
private Map<Long, Long> calculateProblemDeviceStatistics() {
|
||||
List<List<Long>> allProblemDeviceIds = videoReviewMapper.selectAllProblemDeviceIds();
|
||||
|
||||
if (allRatings == null || allRatings.isEmpty()) {
|
||||
if (allProblemDeviceIds == null || allProblemDeviceIds.isEmpty()) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
|
||||
// 统计各机位的总分和次数
|
||||
Map<String, List<Integer>> deviceScores = new HashMap<>();
|
||||
for (Map<String, Integer> rating : allRatings) {
|
||||
if (rating == null) continue;
|
||||
// 遍历每个机位的评分
|
||||
for (Map.Entry<String, Integer> entry : rating.entrySet()) {
|
||||
deviceScores.computeIfAbsent(entry.getKey(), k -> new ArrayList<>()).add(entry.getValue());
|
||||
// 统计各机位被标记为问题的次数
|
||||
Map<Long, Long> deviceProblemCount = new HashMap<>();
|
||||
for (List<Long> problemDeviceIds : allProblemDeviceIds) {
|
||||
if (problemDeviceIds == null || problemDeviceIds.isEmpty()) continue;
|
||||
for (Long deviceId : problemDeviceIds) {
|
||||
deviceProblemCount.put(deviceId, deviceProblemCount.getOrDefault(deviceId, 0L) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 计算平均值
|
||||
Map<String, BigDecimal> result = new HashMap<>();
|
||||
for (Map.Entry<String, List<Integer>> entry : deviceScores.entrySet()) {
|
||||
double avg = entry.getValue().stream().mapToInt(Integer::intValue).average().orElse(0.0);
|
||||
result.put(entry.getKey(), BigDecimal.valueOf(avg).setScale(2, RoundingMode.HALF_UP));
|
||||
}
|
||||
|
||||
return result;
|
||||
return deviceProblemCount;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,7 +145,6 @@ public class AppStatisticsServiceImpl implements AppStatisticsService {
|
||||
|
||||
@Override
|
||||
public ApiResponse<AppStatisticsFunnelVO> userConversionFunnel(CommonQueryReq query) {
|
||||
String redisKey = "statistics:tmp_cache:"+query.getScenicId();
|
||||
AppStatisticsFunnelVO vo = new AppStatisticsFunnelVO();
|
||||
if(query.getEndTime()==null && query.getStartTime()==null){
|
||||
// 没有传时间,则代表用户没有自定义查询时间,使用standard来判断查询时间范围
|
||||
@@ -156,40 +155,111 @@ public class AppStatisticsServiceImpl implements AppStatisticsService {
|
||||
//获取当前周期的具体时间范围
|
||||
standardToNewSpecificTime(query);
|
||||
}
|
||||
// 构建包含日期维度的 Redis 缓存 key
|
||||
String redisKey = String.format("statistics:tmp_cache:%s:%s",
|
||||
query.getScenicId(),
|
||||
query.getStartTime() != null ? DateUtil.formatDate(query.getStartTime()) : "null");
|
||||
if (!query.isRealtime()) {
|
||||
if (!(DateUtil.isIn(query.getStartTime(), DateUtil.tomorrow(), DateUtil.yesterday()) && DateUtil.isIn(query.getEndTime(), DateUtil.tomorrow(), DateUtil.yesterday()))) {
|
||||
// 查询缓存数据
|
||||
List<AppStatisticsFunnelVO> list = statisticsMapper.listStatByScenic(query.getScenicId(), query.getStartTime(), query.getEndTime());
|
||||
AppStatisticsFunnelVO resp = new AppStatisticsFunnelVO();
|
||||
if (list != null && !list.isEmpty()) {
|
||||
for (AppStatisticsFunnelVO item : list) {
|
||||
// Integer fields
|
||||
resp.setCameraShotOfMemberNum(addIntSafely(resp.getCameraShotOfMemberNum(), item.getCameraShotOfMemberNum()));
|
||||
resp.setScanCodeVisitorOfMemberNum(addIntSafely(resp.getScanCodeVisitorOfMemberNum(), item.getScanCodeVisitorOfMemberNum()));
|
||||
resp.setUploadFaceOfMemberNum(addIntSafely(resp.getUploadFaceOfMemberNum(), item.getUploadFaceOfMemberNum()));
|
||||
resp.setPushOfMemberNum(addIntSafely(resp.getPushOfMemberNum(), item.getPushOfMemberNum()));
|
||||
resp.setCompleteVideoOfMemberNum(addIntSafely(resp.getCompleteVideoOfMemberNum(), item.getCompleteVideoOfMemberNum()));
|
||||
resp.setPreviewVideoOfMemberNum(addIntSafely(resp.getPreviewVideoOfMemberNum(), item.getPreviewVideoOfMemberNum()));
|
||||
resp.setClickOnPayOfMemberNum(addIntSafely(resp.getClickOnPayOfMemberNum(), item.getClickOnPayOfMemberNum()));
|
||||
resp.setPayOfMemberNum(addIntSafely(resp.getPayOfMemberNum(), item.getPayOfMemberNum()));
|
||||
resp.setTotalVisitorOfMemberNum(addIntSafely(resp.getTotalVisitorOfMemberNum(), item.getTotalVisitorOfMemberNum()));
|
||||
resp.setCompleteOfVideoNum(addIntSafely(resp.getCompleteOfVideoNum(), item.getCompleteOfVideoNum()));
|
||||
resp.setPreviewOfVideoNum(addIntSafely(resp.getPreviewOfVideoNum(), item.getPreviewOfVideoNum()));
|
||||
resp.setPayOfOrderNum(addIntSafely(resp.getPayOfOrderNum(), item.getPayOfOrderNum()));
|
||||
resp.setRefundOfOrderNum(addIntSafely(resp.getRefundOfOrderNum(), item.getRefundOfOrderNum()));
|
||||
if (!(DateUtil.isIn(query.getStartTime(), DateUtil.yesterday(), DateUtil.tomorrow()) && DateUtil.isIn(query.getEndTime(), DateUtil.yesterday(), DateUtil.tomorrow()))) {
|
||||
// 判断是否为跨范围查询且包含今天
|
||||
Date today = DateUtil.beginOfDay(new Date());
|
||||
boolean containsToday = query.getEndTime() != null && !query.getEndTime().before(today);
|
||||
boolean isMultiDayQuery = query.getStartTime() != null && query.getStartTime().before(today);
|
||||
|
||||
// BigDecimal fields
|
||||
resp.setPayOfOrderAmount(addBigDecimalSafely(resp.payOfOrderAmount(), item.payOfOrderAmount()));
|
||||
resp.setRefundOfOrderAmount(addBigDecimalSafely(resp.refundOfOrderAmount(), item.refundOfOrderAmount()));
|
||||
if (containsToday && isMultiDayQuery) {
|
||||
// 跨范围查询且包含今天:需要分别查询历史数据和今天数据
|
||||
AppStatisticsFunnelVO result = new AppStatisticsFunnelVO();
|
||||
|
||||
// 1. 查询历史数据(从 startDate 到昨天结束)
|
||||
Date yesterday = DateUtil.endOfDay(DateUtil.yesterday());
|
||||
List<AppStatisticsFunnelVO> historyList = statisticsMapper.listStatByScenic(
|
||||
query.getScenicId(),
|
||||
query.getStartTime(),
|
||||
yesterday
|
||||
);
|
||||
|
||||
// 累加历史数据
|
||||
if (historyList != null && !historyList.isEmpty()) {
|
||||
for (AppStatisticsFunnelVO item : historyList) {
|
||||
result.setCameraShotOfMemberNum(addIntSafely(result.getCameraShotOfMemberNum(), item.getCameraShotOfMemberNum()));
|
||||
result.setScanCodeVisitorOfMemberNum(addIntSafely(result.getScanCodeVisitorOfMemberNum(), item.getScanCodeVisitorOfMemberNum()));
|
||||
result.setUploadFaceOfMemberNum(addIntSafely(result.getUploadFaceOfMemberNum(), item.getUploadFaceOfMemberNum()));
|
||||
result.setPushOfMemberNum(addIntSafely(result.getPushOfMemberNum(), item.getPushOfMemberNum()));
|
||||
result.setCompleteVideoOfMemberNum(addIntSafely(result.getCompleteVideoOfMemberNum(), item.getCompleteVideoOfMemberNum()));
|
||||
result.setPreviewVideoOfMemberNum(addIntSafely(result.getPreviewVideoOfMemberNum(), item.getPreviewVideoOfMemberNum()));
|
||||
result.setClickOnPayOfMemberNum(addIntSafely(result.getClickOnPayOfMemberNum(), item.getClickOnPayOfMemberNum()));
|
||||
result.setPayOfMemberNum(addIntSafely(result.getPayOfMemberNum(), item.getPayOfMemberNum()));
|
||||
result.setTotalVisitorOfMemberNum(addIntSafely(result.getTotalVisitorOfMemberNum(), item.getTotalVisitorOfMemberNum()));
|
||||
result.setCompleteOfVideoNum(addIntSafely(result.getCompleteOfVideoNum(), item.getCompleteOfVideoNum()));
|
||||
result.setPreviewOfVideoNum(addIntSafely(result.getPreviewOfVideoNum(), item.getPreviewOfVideoNum()));
|
||||
result.setPayOfOrderNum(addIntSafely(result.getPayOfOrderNum(), item.getPayOfOrderNum()));
|
||||
result.setRefundOfOrderNum(addIntSafely(result.getRefundOfOrderNum(), item.getRefundOfOrderNum()));
|
||||
result.setPayOfOrderAmount(addBigDecimalSafely(result.payOfOrderAmount(), item.payOfOrderAmount()));
|
||||
result.setRefundOfOrderAmount(addBigDecimalSafely(result.refundOfOrderAmount(), item.refundOfOrderAmount()));
|
||||
}
|
||||
}
|
||||
return ApiResponse.success(resp);
|
||||
|
||||
// 2. 查询今天的实时数据
|
||||
CommonQueryReq todayQuery = new CommonQueryReq();
|
||||
todayQuery.setScenicId(query.getScenicId());
|
||||
todayQuery.setStartTime(today);
|
||||
todayQuery.setEndTime(query.getEndTime());
|
||||
|
||||
// 执行今天的实时查询
|
||||
AppStatisticsFunnelVO todayData = queryRealtimeData(todayQuery);
|
||||
|
||||
// 3. 合并今天的数据到结果中
|
||||
result.setCameraShotOfMemberNum(addIntSafely(result.getCameraShotOfMemberNum(), todayData.getCameraShotOfMemberNum()));
|
||||
result.setScanCodeVisitorOfMemberNum(addIntSafely(result.getScanCodeVisitorOfMemberNum(), todayData.getScanCodeVisitorOfMemberNum()));
|
||||
result.setUploadFaceOfMemberNum(addIntSafely(result.getUploadFaceOfMemberNum(), todayData.getUploadFaceOfMemberNum()));
|
||||
result.setPushOfMemberNum(addIntSafely(result.getPushOfMemberNum(), todayData.getPushOfMemberNum()));
|
||||
result.setCompleteVideoOfMemberNum(addIntSafely(result.getCompleteVideoOfMemberNum(), todayData.getCompleteVideoOfMemberNum()));
|
||||
result.setPreviewVideoOfMemberNum(addIntSafely(result.getPreviewVideoOfMemberNum(), todayData.getPreviewVideoOfMemberNum()));
|
||||
result.setClickOnPayOfMemberNum(addIntSafely(result.getClickOnPayOfMemberNum(), todayData.getClickOnPayOfMemberNum()));
|
||||
result.setPayOfMemberNum(addIntSafely(result.getPayOfMemberNum(), todayData.getPayOfMemberNum()));
|
||||
result.setTotalVisitorOfMemberNum(addIntSafely(result.getTotalVisitorOfMemberNum(), todayData.getTotalVisitorOfMemberNum()));
|
||||
result.setCompleteOfVideoNum(addIntSafely(result.getCompleteOfVideoNum(), todayData.getCompleteOfVideoNum()));
|
||||
result.setPreviewOfVideoNum(addIntSafely(result.getPreviewOfVideoNum(), todayData.getPreviewOfVideoNum()));
|
||||
result.setPayOfOrderNum(addIntSafely(result.getPayOfOrderNum(), todayData.getPayOfOrderNum()));
|
||||
result.setRefundOfOrderNum(addIntSafely(result.getRefundOfOrderNum(), todayData.getRefundOfOrderNum()));
|
||||
result.setPayOfOrderAmount(addBigDecimalSafely(result.payOfOrderAmount(), todayData.payOfOrderAmount()));
|
||||
result.setRefundOfOrderAmount(addBigDecimalSafely(result.refundOfOrderAmount(), todayData.refundOfOrderAmount()));
|
||||
|
||||
return ApiResponse.success(result);
|
||||
} else {
|
||||
query.setRealtime(true);
|
||||
// 纯历史查询(不包含今天)
|
||||
List<AppStatisticsFunnelVO> list = statisticsMapper.listStatByScenic(query.getScenicId(), query.getStartTime(), query.getEndTime());
|
||||
AppStatisticsFunnelVO resp = new AppStatisticsFunnelVO();
|
||||
if (list != null && !list.isEmpty()) {
|
||||
for (AppStatisticsFunnelVO item : list) {
|
||||
// Integer fields
|
||||
resp.setCameraShotOfMemberNum(addIntSafely(resp.getCameraShotOfMemberNum(), item.getCameraShotOfMemberNum()));
|
||||
resp.setScanCodeVisitorOfMemberNum(addIntSafely(resp.getScanCodeVisitorOfMemberNum(), item.getScanCodeVisitorOfMemberNum()));
|
||||
resp.setUploadFaceOfMemberNum(addIntSafely(resp.getUploadFaceOfMemberNum(), item.getUploadFaceOfMemberNum()));
|
||||
resp.setPushOfMemberNum(addIntSafely(resp.getPushOfMemberNum(), item.getPushOfMemberNum()));
|
||||
resp.setCompleteVideoOfMemberNum(addIntSafely(resp.getCompleteVideoOfMemberNum(), item.getCompleteVideoOfMemberNum()));
|
||||
resp.setPreviewVideoOfMemberNum(addIntSafely(resp.getPreviewVideoOfMemberNum(), item.getPreviewVideoOfMemberNum()));
|
||||
resp.setClickOnPayOfMemberNum(addIntSafely(resp.getClickOnPayOfMemberNum(), item.getClickOnPayOfMemberNum()));
|
||||
resp.setPayOfMemberNum(addIntSafely(resp.getPayOfMemberNum(), item.getPayOfMemberNum()));
|
||||
resp.setTotalVisitorOfMemberNum(addIntSafely(resp.getTotalVisitorOfMemberNum(), item.getTotalVisitorOfMemberNum()));
|
||||
resp.setCompleteOfVideoNum(addIntSafely(resp.getCompleteOfVideoNum(), item.getCompleteOfVideoNum()));
|
||||
resp.setPreviewOfVideoNum(addIntSafely(resp.getPreviewOfVideoNum(), item.getPreviewOfVideoNum()));
|
||||
resp.setPayOfOrderNum(addIntSafely(resp.getPayOfOrderNum(), item.getPayOfOrderNum()));
|
||||
resp.setRefundOfOrderNum(addIntSafely(resp.getRefundOfOrderNum(), item.getRefundOfOrderNum()));
|
||||
|
||||
// BigDecimal fields
|
||||
resp.setPayOfOrderAmount(addBigDecimalSafely(resp.payOfOrderAmount(), item.payOfOrderAmount()));
|
||||
resp.setRefundOfOrderAmount(addBigDecimalSafely(resp.refundOfOrderAmount(), item.refundOfOrderAmount()));
|
||||
}
|
||||
return ApiResponse.success(resp);
|
||||
} else {
|
||||
query.setRealtime(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!query.isRealtime()) {
|
||||
if (DateUtil.isIn(query.getStartTime(), DateUtil.tomorrow(), DateUtil.yesterday()) && DateUtil.isIn(query.getEndTime(), DateUtil.tomorrow(), DateUtil.yesterday())) {
|
||||
if (DateUtil.isIn(query.getStartTime(), DateUtil.yesterday(), DateUtil.tomorrow()) && DateUtil.isIn(query.getEndTime(), DateUtil.yesterday(), DateUtil.tomorrow())) {
|
||||
// 缓存
|
||||
if (redisTemplate.hasKey(redisKey)) {
|
||||
String s = redisTemplate.opsForValue().get(redisKey);
|
||||
@@ -203,7 +273,7 @@ public class AppStatisticsServiceImpl implements AppStatisticsService {
|
||||
try {
|
||||
// 缓存
|
||||
if (!query.isRealtime()) {
|
||||
if (DateUtil.isIn(query.getStartTime(), DateUtil.tomorrow(), DateUtil.yesterday()) && DateUtil.isIn(query.getEndTime(), DateUtil.tomorrow(), DateUtil.yesterday())) {
|
||||
if (DateUtil.isIn(query.getStartTime(), DateUtil.yesterday(), DateUtil.tomorrow()) && DateUtil.isIn(query.getEndTime(), DateUtil.yesterday(), DateUtil.tomorrow())) {
|
||||
// 缓存
|
||||
if (redisTemplate.hasKey(redisKey)) {
|
||||
String s = redisTemplate.opsForValue().get(redisKey);
|
||||
@@ -212,71 +282,17 @@ public class AppStatisticsServiceImpl implements AppStatisticsService {
|
||||
}
|
||||
}
|
||||
}
|
||||
//镜头检测游客数
|
||||
// Integer cameraShotOfMemberNum=statisticsMapper.countCameraShotOfMember(query);
|
||||
//扫码访问人数
|
||||
// 扫小程序码或景区码进入访问的用户数,包括授权用户(使用OpenID进行精准统计)和未授权用户(使用 UUID统计访问)。但当用户授权时,获取OpenID并与UUID关联,删除本地UUID,避免重复记录。
|
||||
Integer scanCodeVisitorOfMemberNum=statsQueryService.countScanCodeOfMember(query);
|
||||
//上传头像(人脸)人数
|
||||
// 上传了人脸的用户数(包括本地临时ID和获取到OpenID的,同一设备微信获取到OpenID要覆盖掉之前生成的临时ID),上传多张人脸都只算一个人。
|
||||
Integer uploadFaceOfMemberNum=statsQueryService.countUploadFaceOfMember(query);
|
||||
//推送订阅人数
|
||||
// 只要点了允许通知,哪怕只勾选1条订阅都算
|
||||
Integer pushOfMemberNum =statsQueryService.countPushOfMember(query);
|
||||
//生成视频人数
|
||||
// 生成过Vlog视频的用户ID数,要注意屏蔽掉以前没有片段也能生成的情况
|
||||
Integer completeVideoOfMemberNum =statsQueryService.countCompleteVideoOfMember(query);
|
||||
//预览视频人数
|
||||
// 购买前播放了5秒的视频条数。
|
||||
Integer previewVideoOfMemberNum =statsQueryService.countPreviewVideoOfMember(query);
|
||||
if (previewVideoOfMemberNum==null){
|
||||
previewVideoOfMemberNum=0;
|
||||
}
|
||||
//点击购买人数
|
||||
// 点了立即购买按钮的用户ID就算,包括支付的和未支付的都算,只要点击了。
|
||||
Integer clickOnPayOfMemberNum =statisticsMapper.countClickPayOfMember(query);
|
||||
//支付订单人数
|
||||
Integer payOfMemberNum =statisticsMapper.countPayOfMember(query);
|
||||
//总访问人数
|
||||
// 通过任何途径访问到小程序的总人数,包括授权用户和未授权用户。
|
||||
Integer totalVisitorOfMemberNum =statsQueryService.countTotalVisitorOfMember(query);
|
||||
// Integer totalVisitorOfMemberNum =scanCodeVisitorOfMemberNum;
|
||||
//生成视频条数
|
||||
// 仅指代生成的Vlog条数,不包含录像原片。
|
||||
Integer completeOfVideoNum =statsQueryService.countCompleteOfVideo(query);
|
||||
//预览视频条数
|
||||
Integer previewOfVideoNum =statsQueryService.countPreviewOfVideo(query);
|
||||
//支付订单数
|
||||
Integer payOfOrderNum =statisticsMapper.countPayOfOrder(query);
|
||||
//支付订单金额
|
||||
BigDecimal payOfOrderAmount =statisticsMapper.countOrderAmount(query);
|
||||
//退款订单数
|
||||
Integer refundOfOrderNum =statisticsMapper.countRefundOfOrder(query);
|
||||
//退款订单金额
|
||||
BigDecimal refundOfOrderAmount =statisticsMapper.countRefundAmount(query);
|
||||
|
||||
vo.setScanCodeVisitorOfMemberNum(scanCodeVisitorOfMemberNum);
|
||||
vo.setUploadFaceOfMemberNum(uploadFaceOfMemberNum);
|
||||
vo.setPushOfMemberNum(pushOfMemberNum);
|
||||
vo.setCompleteVideoOfMemberNum(completeVideoOfMemberNum);
|
||||
vo.setPreviewVideoOfMemberNum(previewVideoOfMemberNum);
|
||||
vo.setClickOnPayOfMemberNum(clickOnPayOfMemberNum);
|
||||
vo.setPayOfMemberNum(payOfMemberNum);
|
||||
// 执行实时查询
|
||||
vo = queryRealtimeData(query);
|
||||
|
||||
vo.setTotalVisitorOfMemberNum(totalVisitorOfMemberNum);
|
||||
vo.setCompleteOfVideoNum(completeOfVideoNum);
|
||||
vo.setPreviewOfVideoNum(previewOfVideoNum);
|
||||
vo.setPayOfOrderNum(payOfOrderNum);
|
||||
vo.setPayOfOrderAmount(payOfOrderAmount.setScale(2, RoundingMode.HALF_UP));
|
||||
vo.setRefundOfOrderNum(refundOfOrderNum);
|
||||
vo.setRefundOfOrderAmount(refundOfOrderAmount.setScale(2, RoundingMode.HALF_UP));
|
||||
// 仅在非 realtime 模式下写入缓存
|
||||
// realtime=true 时由调用方(如定时任务)自行控制写入目标日期,不污染当天缓存
|
||||
if (!query.isRealtime()) {
|
||||
if (query.getScenicId() != null) {
|
||||
statisticsMapper.insertStat(query.getScenicId(), new Date(), vo);
|
||||
// 仅对当天数据启用 Redis 缓存(短期缓存,减少实时查询压力)
|
||||
// 历史数据已在 scenic_stats 表中持久化,不需要 Redis 缓存
|
||||
if (!query.isRealtime() && query.getStartTime() != null) {
|
||||
// 判断查询日期是否为今天
|
||||
if (DateUtil.isSameDay(query.getStartTime(), new Date())) {
|
||||
redisTemplate.opsForValue().set(redisKey, JacksonUtil.toJSONString(vo), 60, TimeUnit.SECONDS);
|
||||
}
|
||||
redisTemplate.opsForValue().set(redisKey, JacksonUtil.toJSONString(vo), 60, TimeUnit.SECONDS);
|
||||
}
|
||||
return ApiResponse.success(vo);
|
||||
} finally {
|
||||
@@ -298,6 +314,64 @@ public class AppStatisticsServiceImpl implements AppStatisticsService {
|
||||
return int1 == null ? 0 : int1 + (int2 == null ? 0 : int2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行实时数据查询(从 ClickHouse 和 MySQL 查询最新数据)
|
||||
* @param query 查询条件
|
||||
* @return 实时统计数据
|
||||
*/
|
||||
private AppStatisticsFunnelVO queryRealtimeData(CommonQueryReq query) {
|
||||
AppStatisticsFunnelVO vo = new AppStatisticsFunnelVO();
|
||||
|
||||
//扫码访问人数
|
||||
Integer scanCodeVisitorOfMemberNum = statsQueryService.countScanCodeOfMember(query);
|
||||
//上传头像(人脸)人数
|
||||
Integer uploadFaceOfMemberNum = statsQueryService.countUploadFaceOfMember(query);
|
||||
//推送订阅人数
|
||||
Integer pushOfMemberNum = statsQueryService.countPushOfMember(query);
|
||||
//生成视频人数
|
||||
Integer completeVideoOfMemberNum = statsQueryService.countCompleteVideoOfMember(query);
|
||||
//预览视频人数
|
||||
Integer previewVideoOfMemberNum = statsQueryService.countPreviewVideoOfMember(query);
|
||||
if (previewVideoOfMemberNum == null) {
|
||||
previewVideoOfMemberNum = 0;
|
||||
}
|
||||
//点击购买人数
|
||||
Integer clickOnPayOfMemberNum = statisticsMapper.countClickPayOfMember(query);
|
||||
//支付订单人数
|
||||
Integer payOfMemberNum = statisticsMapper.countPayOfMember(query);
|
||||
//总访问人数
|
||||
Integer totalVisitorOfMemberNum = statsQueryService.countTotalVisitorOfMember(query);
|
||||
//生成视频条数
|
||||
Integer completeOfVideoNum = statsQueryService.countCompleteOfVideo(query);
|
||||
//预览视频条数
|
||||
Integer previewOfVideoNum = statsQueryService.countPreviewOfVideo(query);
|
||||
//支付订单数
|
||||
Integer payOfOrderNum = statisticsMapper.countPayOfOrder(query);
|
||||
//支付订单金额
|
||||
BigDecimal payOfOrderAmount = statisticsMapper.countOrderAmount(query);
|
||||
//退款订单数
|
||||
Integer refundOfOrderNum = statisticsMapper.countRefundOfOrder(query);
|
||||
//退款订单金额
|
||||
BigDecimal refundOfOrderAmount = statisticsMapper.countRefundAmount(query);
|
||||
|
||||
vo.setScanCodeVisitorOfMemberNum(scanCodeVisitorOfMemberNum);
|
||||
vo.setUploadFaceOfMemberNum(uploadFaceOfMemberNum);
|
||||
vo.setPushOfMemberNum(pushOfMemberNum);
|
||||
vo.setCompleteVideoOfMemberNum(completeVideoOfMemberNum);
|
||||
vo.setPreviewVideoOfMemberNum(previewVideoOfMemberNum);
|
||||
vo.setClickOnPayOfMemberNum(clickOnPayOfMemberNum);
|
||||
vo.setPayOfMemberNum(payOfMemberNum);
|
||||
vo.setTotalVisitorOfMemberNum(totalVisitorOfMemberNum);
|
||||
vo.setCompleteOfVideoNum(completeOfVideoNum);
|
||||
vo.setPreviewOfVideoNum(previewOfVideoNum);
|
||||
vo.setPayOfOrderNum(payOfOrderNum);
|
||||
vo.setPayOfOrderAmount(payOfOrderAmount != null ? payOfOrderAmount.setScale(2, RoundingMode.HALF_UP) : BigDecimal.ZERO);
|
||||
vo.setRefundOfOrderNum(refundOfOrderNum);
|
||||
vo.setRefundOfOrderAmount(refundOfOrderAmount != null ? refundOfOrderAmount.setScale(2, RoundingMode.HALF_UP) : BigDecimal.ZERO);
|
||||
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiResponse orderChart(CommonQueryReq query) {
|
||||
if(query.getEndTime()==null && query.getStartTime()==null){
|
||||
|
||||
@@ -16,6 +16,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import ai.z.openapi.service.model.ChatMessage;
|
||||
import ai.z.openapi.service.model.ChatMessageRole;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -42,6 +43,7 @@ public class FaceChatServiceImpl implements FaceChatService {
|
||||
private final FaceChatConversationMapper conversationMapper;
|
||||
private final FaceChatMessageMapper messageMapper;
|
||||
private final FaceRepository faceRepository;
|
||||
@Lazy
|
||||
private final GlmClient glmClient;
|
||||
|
||||
@Override
|
||||
|
||||
@@ -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());
|
||||
@@ -332,16 +341,8 @@ public class GoodsServiceImpl implements GoodsService {
|
||||
|
||||
// ==================== 第四步:根据切片完成状态返回结果 ====================
|
||||
|
||||
if (status == FaceCutStatus.WAITING_USER_SELECT) {
|
||||
// 切片已完成,但景区配置了 face_select_first=true
|
||||
// 需要等待用户手动选择模板后才开始渲染
|
||||
// 前端展示:「专属视频合成中」
|
||||
response.setStatus(VideoTaskStatus.PROCESSING.getCode());
|
||||
return response;
|
||||
}
|
||||
|
||||
if (status == FaceCutStatus.COMPLETED) {
|
||||
// 切片已完成,查询该人脸关联的视频信息
|
||||
if (status == FaceCutStatus.WAITING_USER_SELECT || status == FaceCutStatus.COMPLETED) {
|
||||
// 切片已完成(或等待用户选择),查询该人脸关联的视频信息
|
||||
List<MemberVideoEntity> taskList = videoMapper.listRelationByFace(faceId);
|
||||
if (taskList == null || taskList.isEmpty()) {
|
||||
response.setStatus(VideoTaskStatus.PENDING.getCode());
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.ycwl.basic.utils.ApiResponse;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @Author:longbinbin
|
||||
@@ -23,4 +24,39 @@ public interface SourceService {
|
||||
|
||||
ApiResponse cutVideo(Long id);
|
||||
String uploadAndUpdateUrl(Long id, File file);
|
||||
|
||||
/**
|
||||
* 根据sourceId列表查询关联的faceId
|
||||
* @param sourceIds sourceId列表
|
||||
* @return sourceId -> faceId 的映射
|
||||
*/
|
||||
ApiResponse<Map<Long, Long>> getFaceIdsBySourceIds(List<Long> sourceIds);
|
||||
|
||||
/**
|
||||
* 根据faceId分页查询关联的source记录
|
||||
* @param sourceReqQuery 查询参数(需设置faceId)
|
||||
* @return 分页结果
|
||||
*/
|
||||
ApiResponse<PageInfo<SourceRespVO>> pageByFaceId(SourceReqQuery sourceReqQuery);
|
||||
|
||||
/**
|
||||
* 管理员软删除(取消)关联记录
|
||||
* @param id member_source 记录 ID
|
||||
* @return 操作结果
|
||||
*/
|
||||
ApiResponse<Void> cancelRelation(Long id);
|
||||
|
||||
/**
|
||||
* 管理员恢复已取消的关联记录
|
||||
* @param id member_source 记录 ID
|
||||
* @return 操作结果
|
||||
*/
|
||||
ApiResponse<Void> reactivateRelation(Long id);
|
||||
|
||||
/**
|
||||
* 分页查询已取消的关联记录(管理员用)
|
||||
* @param sourceReqQuery 查询参数(需设置faceId)
|
||||
* @return 分页结果
|
||||
*/
|
||||
ApiResponse<PageInfo<SourceRespVO>> pageDeletedByFaceId(SourceReqQuery sourceReqQuery);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ 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.FaceSampleMapper;
|
||||
import com.ycwl.basic.mapper.ProjectMapper;
|
||||
import com.ycwl.basic.mapper.SourceMapper;
|
||||
@@ -60,6 +61,7 @@ import com.ycwl.basic.repository.FaceRepository;
|
||||
import com.ycwl.basic.repository.MemberRelationRepository;
|
||||
import com.ycwl.basic.repository.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;
|
||||
@@ -210,6 +212,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) {
|
||||
@@ -472,7 +476,47 @@ 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);
|
||||
for (SourceEntity sourceEntity : sourceEntityList) {
|
||||
ContentPageVO content = new ContentPageVO();
|
||||
content.setName("摄影师拍照");
|
||||
deviceList.stream().filter(device -> device.getId().equals(sourceEntity.getDeviceId())).findFirst().ifPresent(device -> {
|
||||
content.setGroup(device.getName());
|
||||
});
|
||||
content.setContentId(sourceEntity.getId());
|
||||
content.setGoodsType(2);
|
||||
content.setContentType(2);
|
||||
content.setScenicId(sourceEntity.getScenicId());
|
||||
content.setSourceType(2);
|
||||
content.setOrigUrl(sourceEntity.getUrl());
|
||||
content.setTemplateCoverUrl(sourceEntity.getThumbUrl());
|
||||
content.setIsBuy(sourceEntity.getIsBuy());
|
||||
content.setLockType(-1);
|
||||
result.add(content);
|
||||
}
|
||||
List<Long> containedDeviceId = sourceEntityList.stream().map(SourceEntity::getDeviceId).filter(Objects::nonNull).distinct().toList();
|
||||
deviceList.stream().filter(device -> !containedDeviceId.contains(device.getId())).forEach(device -> {
|
||||
ContentPageVO content = new ContentPageVO();
|
||||
content.setName(device.getName());
|
||||
content.setGroup(device.getName());
|
||||
content.setContentId(device.getId());
|
||||
content.setGoodsType(2);
|
||||
content.setContentType(2);
|
||||
content.setScenicId(face.getScenicId());
|
||||
content.setSourceType(2);
|
||||
content.setTemplateCoverUrl("");
|
||||
content.setIsBuy(0);
|
||||
content.setLockType(1);
|
||||
result.add(content);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
List<TemplateRespVO> templateList = templateRepository.getTemplateListByScenicId(face.getScenicId());
|
||||
List<ContentPageVO> contentList = templateList.stream().map(template -> {
|
||||
/// select t.id templateId, t.scenic_id, t.`group`, t.`name`, pid, t.cover_url templateCoverUrl,
|
||||
@@ -607,7 +651,6 @@ public class FaceServiceImpl implements FaceService {
|
||||
sourceVideoContent.setGroup("直出原片");
|
||||
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 +722,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 +823,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;
|
||||
}
|
||||
|
||||
@@ -930,6 +930,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 +938,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 -> {
|
||||
|
||||
@@ -3,14 +3,17 @@ package com.ycwl.basic.service.pc.impl;
|
||||
import com.github.pagehelper.PageHelper;
|
||||
import com.github.pagehelper.PageInfo;
|
||||
import com.ycwl.basic.exception.BaseException;
|
||||
import com.ycwl.basic.biz.FaceStatusManager;
|
||||
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO;
|
||||
import com.ycwl.basic.mapper.SourceMapper;
|
||||
import com.ycwl.basic.model.pc.device.entity.DeviceEntity;
|
||||
import com.ycwl.basic.model.pc.scenic.entity.ScenicEntity;
|
||||
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
|
||||
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
|
||||
import com.ycwl.basic.model.pc.source.req.SourceReqQuery;
|
||||
import com.ycwl.basic.model.pc.source.resp.SourceRespVO;
|
||||
import com.ycwl.basic.repository.DeviceRepository;
|
||||
import com.ycwl.basic.repository.MemberRelationRepository;
|
||||
import com.ycwl.basic.repository.ScenicRepository;
|
||||
import com.ycwl.basic.repository.SourceRepository;
|
||||
import com.ycwl.basic.service.pc.ScenicService;
|
||||
@@ -29,6 +32,7 @@ import java.io.File;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
@@ -52,6 +56,10 @@ public class SourceServiceImpl implements SourceService {
|
||||
private ScenicRepository scenicRepository;
|
||||
@Autowired
|
||||
private DeviceRepository deviceRepository;
|
||||
@Autowired
|
||||
private MemberRelationRepository memberRelationRepository;
|
||||
@Autowired
|
||||
private FaceStatusManager faceStatusManager;
|
||||
|
||||
@Override
|
||||
public ApiResponse<PageInfo<SourceRespVO>> pageQuery(SourceReqQuery sourceReqQuery) {
|
||||
@@ -201,4 +209,76 @@ public class SourceServiceImpl implements SourceService {
|
||||
throw new BaseException("文件上传失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiResponse<Map<Long, Long>> getFaceIdsBySourceIds(List<Long> sourceIds) {
|
||||
if (sourceIds == null || sourceIds.isEmpty()) {
|
||||
return ApiResponse.success(Collections.emptyMap());
|
||||
}
|
||||
List<MemberSourceEntity> relations = sourceMapper.listFaceIdsBySourceIds(sourceIds);
|
||||
Map<Long, Long> faceIdMap = relations.stream()
|
||||
.collect(Collectors.toMap(
|
||||
MemberSourceEntity::getSourceId,
|
||||
MemberSourceEntity::getFaceId,
|
||||
(existing, replacement) -> existing,
|
||||
LinkedHashMap::new
|
||||
));
|
||||
// 对于没有关联的sourceId,填充null
|
||||
Map<Long, Long> result = new LinkedHashMap<>();
|
||||
for (Long sourceId : sourceIds) {
|
||||
result.put(sourceId, faceIdMap.get(sourceId));
|
||||
}
|
||||
return ApiResponse.success(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiResponse<PageInfo<SourceRespVO>> pageByFaceId(SourceReqQuery sourceReqQuery) {
|
||||
PageHelper.startPage(sourceReqQuery.getPageNum(), sourceReqQuery.getPageSize());
|
||||
List<SourceRespVO> list = sourceMapper.pageByFaceId(sourceReqQuery);
|
||||
PageInfo<SourceRespVO> pageInfo = new PageInfo<>(list);
|
||||
return ApiResponse.success(pageInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiResponse<Void> cancelRelation(Long id) {
|
||||
MemberSourceEntity entity = sourceMapper.getMemberSourceById(id);
|
||||
if (entity == null) {
|
||||
return ApiResponse.fail("关联记录不存在");
|
||||
}
|
||||
int rows = sourceMapper.softDeleteRelation(id);
|
||||
if (rows == 0) {
|
||||
return ApiResponse.fail("记录已取消或不存在");
|
||||
}
|
||||
invalidateCacheByFace(entity.getFaceId());
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiResponse<Void> reactivateRelation(Long id) {
|
||||
MemberSourceEntity entity = sourceMapper.getMemberSourceById(id);
|
||||
if (entity == null) {
|
||||
return ApiResponse.fail("关联记录不存在");
|
||||
}
|
||||
int rows = sourceMapper.reactivateRelation(id);
|
||||
if (rows == 0) {
|
||||
return ApiResponse.fail("记录未处于取消状态");
|
||||
}
|
||||
invalidateCacheByFace(entity.getFaceId());
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiResponse<PageInfo<SourceRespVO>> pageDeletedByFaceId(SourceReqQuery sourceReqQuery) {
|
||||
PageHelper.startPage(sourceReqQuery.getPageNum(), sourceReqQuery.getPageSize());
|
||||
List<SourceRespVO> list = sourceMapper.pageDeletedByFaceId(sourceReqQuery);
|
||||
PageInfo<SourceRespVO> pageInfo = new PageInfo<>(list);
|
||||
return ApiResponse.success(pageInfo);
|
||||
}
|
||||
|
||||
private void invalidateCacheByFace(Long faceId) {
|
||||
if (faceId != null) {
|
||||
memberRelationRepository.clearSCacheByFace(faceId);
|
||||
faceStatusManager.invalidatePuzzleSourceVersion(faceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,4 +143,56 @@ public interface PrinterService {
|
||||
* @return 订单信息
|
||||
*/
|
||||
Map<String, Object> createVirtualOrder(Long sourceId, Long scenicId, Integer printerId, Boolean needEnhance, String printImgUrl);
|
||||
|
||||
/**
|
||||
* 创建虚拟用户订单(支持实际支付模式)
|
||||
*
|
||||
* @param sourceId source记录ID
|
||||
* @param scenicId 景区ID
|
||||
* @param printerId 打印机ID(可选)
|
||||
* @param needEnhance 是否需要图像增强(可选)
|
||||
* @param printImgUrl 打印图片URL(可选)
|
||||
* @param needActualPayment 是否需要实际支付(true: 创建待支付订单, false/null: 0元立即购买)
|
||||
* @return 订单信息
|
||||
*/
|
||||
Map<String, Object> createVirtualOrder(Long sourceId, Long scenicId, Integer printerId, Boolean needEnhance, String printImgUrl, Boolean needActualPayment);
|
||||
|
||||
/**
|
||||
* 批量创建虚拟用户订单(多个sourceId聚合为一笔订单、一次支付)
|
||||
*
|
||||
* @param sourceIds source记录ID列表
|
||||
* @param scenicId 景区ID
|
||||
* @param printerId 打印机ID(可选)
|
||||
* @param needEnhance 是否需要图像增强(可选)
|
||||
* @param printImgUrl 打印图片URL(可选)
|
||||
* @param needActualPayment 是否需要实际支付
|
||||
* @return 订单信息
|
||||
*/
|
||||
Map<String, Object> createBatchVirtualOrder(List<Long> sourceIds, Long scenicId, Integer printerId, Boolean needEnhance, String printImgUrl, Boolean needActualPayment);
|
||||
|
||||
/**
|
||||
* 根据accessKey获取打印机详情
|
||||
* @param accessKey 打印机accessKey
|
||||
* @return 打印机实体
|
||||
*/
|
||||
PrinterEntity getByAccessKey(String accessKey);
|
||||
|
||||
/**
|
||||
* 根据accessKey获取打印机对应的景区基础信息
|
||||
* @param accessKey 打印机accessKey
|
||||
* @return 景区基础信息
|
||||
*/
|
||||
Object getScenicBasicByAccessKey(String accessKey);
|
||||
|
||||
/**
|
||||
* 打开打印机(设置status=1)
|
||||
* @param accessKey 打印机accessKey
|
||||
*/
|
||||
void openPrinter(String accessKey);
|
||||
|
||||
/**
|
||||
* 关闭打印机(设置status=0)
|
||||
* @param accessKey 打印机accessKey
|
||||
*/
|
||||
void closePrinter(String accessKey);
|
||||
}
|
||||
@@ -1,5 +1,12 @@
|
||||
package com.ycwl.basic.service.printer.impl;
|
||||
|
||||
import com.wechat.pay.java.service.payments.nativepay.NativePayService;
|
||||
import com.wechat.pay.java.service.payments.nativepay.model.Amount;
|
||||
import com.wechat.pay.java.service.payments.nativepay.model.PrepayRequest;
|
||||
import com.wechat.pay.java.service.payments.nativepay.model.PrepayResponse;
|
||||
import com.ycwl.basic.pay.adapter.IPayAdapter;
|
||||
import com.ycwl.basic.pay.adapter.WxMpPayAdapter;
|
||||
import com.ycwl.basic.service.pc.ScenicService;
|
||||
import com.ycwl.basic.biz.OrderBiz;
|
||||
import com.ycwl.basic.constant.NumberConstant;
|
||||
import com.ycwl.basic.enums.OrderStateEnum;
|
||||
@@ -162,6 +169,9 @@ public class PrinterServiceImpl implements PrinterService {
|
||||
@Autowired
|
||||
@Lazy
|
||||
private WatermarkEdgeService watermarkEdgeService;
|
||||
@Autowired
|
||||
@Lazy
|
||||
private ScenicService scenicService;
|
||||
|
||||
@Override
|
||||
public List<PrinterResp> listByScenicId(Long scenicId) {
|
||||
@@ -343,6 +353,14 @@ public class PrinterServiceImpl implements PrinterService {
|
||||
log.debug("打印机高度未配置或无效,使用默认值: height={}", printHeight);
|
||||
}
|
||||
|
||||
// 检测原图方向
|
||||
boolean isLandscape = false;
|
||||
try {
|
||||
isLandscape = ImageUtils.isLandscape(url);
|
||||
} catch (Exception e) {
|
||||
log.warn("检测图片方向失败,默认为竖图: url={}", url, e);
|
||||
}
|
||||
|
||||
// 使用smartCropAndFill裁剪图片
|
||||
File croppedFile = ImageUtils.smartCropAndFill(url, printWidth, printHeight);
|
||||
|
||||
@@ -355,8 +373,9 @@ public class PrinterServiceImpl implements PrinterService {
|
||||
|
||||
log.info("照片裁剪成功: memberId={}, scenicId={}, 原图={}, 裁剪后={}, 尺寸={}x{}",
|
||||
memberId, scenicId, url, cropUrl, printWidth, printHeight);
|
||||
String crop = JacksonUtil.toJSONString(new Crop(270));
|
||||
entity.setCrop(crop);
|
||||
if (isLandscape) {
|
||||
entity.setCrop(JacksonUtil.toJSONString(new Crop(270)));
|
||||
}
|
||||
} finally {
|
||||
// 清理临时文件
|
||||
if (croppedFile != null && croppedFile.exists()) {
|
||||
@@ -627,9 +646,19 @@ public class PrinterServiceImpl implements PrinterService {
|
||||
log.debug("打印机高度未配置或无效,使用默认值: height={}", printHeight);
|
||||
}
|
||||
|
||||
// 检测原图方向
|
||||
boolean isLandscape = false;
|
||||
try {
|
||||
isLandscape = ImageUtils.isLandscape(url);
|
||||
} catch (Exception e) {
|
||||
log.warn("检测图片方向失败,默认为竖图: url={}", url, e);
|
||||
}
|
||||
|
||||
// 使用smartCropAndFill裁剪图片
|
||||
File croppedFile = ImageUtils.smartCropAndFill(url, printWidth, printHeight);
|
||||
entity.setCrop(JacksonUtil.toJSONString(new Crop(270)));
|
||||
if (isLandscape) {
|
||||
entity.setCrop(JacksonUtil.toJSONString(new Crop(270)));
|
||||
}
|
||||
|
||||
try {
|
||||
// 上传裁剪后的图片
|
||||
@@ -1055,8 +1084,10 @@ public class PrinterServiceImpl implements PrinterService {
|
||||
|
||||
String scenicText = scenicConfig.getString("print_watermark_scenic_text", "");
|
||||
String dateFormat = scenicConfig.getString("print_watermark_dt_format", "yyyy.MM.dd");
|
||||
String printWatermarkPUrl = scenicConfig.getString("print_watermark_p_url", null);
|
||||
String printWatermarkLUrl = scenicConfig.getString("print_watermark_l_url", null);
|
||||
|
||||
return WatermarkConfig.builder()
|
||||
WatermarkConfig.WatermarkConfigBuilder builder = WatermarkConfig.builder()
|
||||
.watermarkType(watermarkType)
|
||||
.scenicText(scenicText)
|
||||
.dateFormat(dateFormat)
|
||||
@@ -1064,7 +1095,14 @@ public class PrinterServiceImpl implements PrinterService {
|
||||
.storageAdapter(StorageFactory.use())
|
||||
.edgeEnabled(true)
|
||||
.qrcodeFile(qrCodeFile)
|
||||
.scale(scale)
|
||||
.scale(scale);
|
||||
if (context.getSource() == ImageSource.IPC) {
|
||||
return builder
|
||||
.printWatermarkPUrlList(Collections.singletonList(printWatermarkPUrl))
|
||||
.printWatermarkLUrlList(Collections.singletonList(printWatermarkLUrl))
|
||||
.build();
|
||||
}
|
||||
return builder
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -1126,7 +1164,14 @@ public class PrinterServiceImpl implements PrinterService {
|
||||
|
||||
@Override
|
||||
public void setUserIsBuyItem(Long memberId, Long id, Long orderId) {
|
||||
setUserIsBuyItem(memberId, id, orderId, null);
|
||||
// 尝试从 Redis 读取虚拟订单存储的 needEnhance 配置
|
||||
Boolean needEnhance = null;
|
||||
String enhanceFlag = redisTemplate.opsForValue().get("virtual_order_enhance:" + orderId);
|
||||
if (enhanceFlag != null) {
|
||||
needEnhance = Boolean.parseBoolean(enhanceFlag);
|
||||
redisTemplate.delete("virtual_order_enhance:" + orderId);
|
||||
}
|
||||
setUserIsBuyItem(memberId, id, orderId, needEnhance);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1134,7 +1179,7 @@ public class PrinterServiceImpl implements PrinterService {
|
||||
if (redisTemplate.opsForValue().get(USER_PHOTO_LIST_TO_PRINTER + memberId + ":" + orderId) != null) {
|
||||
return;
|
||||
}
|
||||
redisTemplate.opsForValue().set(USER_PHOTO_LIST_TO_PRINTER + memberId + ":" + orderId, "1", 60, TimeUnit.SECONDS);
|
||||
redisTemplate.opsForValue().set(USER_PHOTO_LIST_TO_PRINTER + memberId + ":" + orderId, "1", 24, TimeUnit.HOURS);
|
||||
OrderEntity order = orderRepository.getOrder(orderId);
|
||||
List<OrderItemEntity> orderItems = orderMapper.getOrderItems(orderId);
|
||||
orderItems.forEach(item -> {
|
||||
@@ -1680,16 +1725,21 @@ public class PrinterServiceImpl implements PrinterService {
|
||||
|
||||
@Override
|
||||
public Map<String, Object> createVirtualOrder(Long sourceId, Long scenicId, Integer printerId) {
|
||||
return createVirtualOrder(sourceId, scenicId, printerId, null, null);
|
||||
return createVirtualOrder(sourceId, scenicId, printerId, null, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> createVirtualOrder(Long sourceId, Long scenicId, Integer printerId, Boolean needEnhance) {
|
||||
return createVirtualOrder(sourceId, scenicId, printerId, needEnhance, null);
|
||||
return createVirtualOrder(sourceId, scenicId, printerId, needEnhance, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> createVirtualOrder(Long sourceId, Long scenicId, Integer printerId, Boolean needEnhance, String printImgUrl) {
|
||||
return createVirtualOrder(sourceId, scenicId, printerId, needEnhance, printImgUrl, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> createVirtualOrder(Long sourceId, Long scenicId, Integer printerId, Boolean needEnhance, String printImgUrl, Boolean needActualPayment) {
|
||||
// 1. 查询source记录
|
||||
SourceEntity source = sourceMapper.getEntity(sourceId);
|
||||
FaceSampleEntity faceSample = faceSampleMapper.getEntity(source.getFaceSampleId());
|
||||
@@ -1752,7 +1802,7 @@ public class PrinterServiceImpl implements PrinterService {
|
||||
throw new BaseException("打印机不属于该景区");
|
||||
}
|
||||
|
||||
// 6. 创建0元订单
|
||||
// 6. 创建订单
|
||||
OrderEntity order = new OrderEntity();
|
||||
Long orderId = SnowFlakeUtil.getLongId();
|
||||
redisTemplate.opsForValue().set("printer_size:" + orderId, printer.getPreferPaper(), 60, TimeUnit.SECONDS);
|
||||
@@ -1776,13 +1826,52 @@ public class PrinterServiceImpl implements PrinterService {
|
||||
return orderItem;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
// 设置价格为0
|
||||
order.setPrice(BigDecimal.ZERO);
|
||||
order.setSlashPrice(BigDecimal.ZERO);
|
||||
order.setPayPrice(BigDecimal.ZERO);
|
||||
order.setFaceId(faceId);
|
||||
order.setStatus(OrderStateEnum.PAID.getState());
|
||||
order.setPayAt(new Date());
|
||||
boolean actualPayment = Boolean.TRUE.equals(needActualPayment);
|
||||
|
||||
if (actualPayment) {
|
||||
// 需要实际支付:通过价格计算服务获取真实价格
|
||||
PriceCalculationRequest priceRequest = new PriceCalculationRequest();
|
||||
priceRequest.setUserId(virtualMemberId);
|
||||
priceRequest.setScenicId(scenicId);
|
||||
|
||||
List<ProductItem> productItems = new ArrayList<>();
|
||||
ProductItem photoItem = new ProductItem();
|
||||
photoItem.setProductType(ProductType.PHOTO_PRINT);
|
||||
photoItem.setProductId(scenicId.toString());
|
||||
photoItem.setQuantity(1);
|
||||
photoItem.setPurchaseCount(1);
|
||||
photoItem.setScenicId(scenicId.toString());
|
||||
// 通过 source 的 deviceId 设置 attributeKeys
|
||||
SourceEntity priceSource = sourceRepository.getSource(sourceId);
|
||||
if (priceSource != null && priceSource.getDeviceId() != null) {
|
||||
photoItem.setAttributeKeys(List.of(String.valueOf(priceSource.getDeviceId())));
|
||||
}
|
||||
productItems.add(photoItem);
|
||||
|
||||
priceRequest.setProducts(productItems);
|
||||
priceRequest.setAutoUseCoupon(false);
|
||||
priceRequest.setPreviewOnly(false);
|
||||
|
||||
PriceCalculationResult priceResult = priceCalculationService.calculatePrice(priceRequest);
|
||||
|
||||
order.setPrice(priceResult.getFinalAmount());
|
||||
order.setSlashPrice(priceResult.getOriginalAmount());
|
||||
order.setPayPrice(priceResult.getFinalAmount());
|
||||
order.setStatus(OrderStateEnum.UNPAID.getState());
|
||||
log.info("创建待支付虚拟订单: orderId={}, price={}", orderId, priceResult.getFinalAmount());
|
||||
|
||||
// 将 needEnhance 存入 Redis,支付完成后 setUserIsBuyItem 可读取
|
||||
if (needEnhance != null) {
|
||||
redisTemplate.opsForValue().set("virtual_order_enhance:" + orderId, needEnhance.toString(), 24, TimeUnit.HOURS);
|
||||
}
|
||||
} else {
|
||||
// 虚拟0元购买:价格为0,直接标记已支付
|
||||
order.setPrice(BigDecimal.ZERO);
|
||||
order.setSlashPrice(BigDecimal.ZERO);
|
||||
order.setPayPrice(BigDecimal.ZERO);
|
||||
order.setStatus(OrderStateEnum.PAID.getState());
|
||||
order.setPayAt(new Date());
|
||||
}
|
||||
|
||||
// 保存订单
|
||||
orderMapper.add(order);
|
||||
@@ -1792,21 +1881,277 @@ public class PrinterServiceImpl implements PrinterService {
|
||||
throw new BaseException("订单添加失败");
|
||||
}
|
||||
|
||||
log.info("创建0元订单成功: orderId={}, virtualMemberId={}, faceId={}", orderId, virtualMemberId, faceId);
|
||||
log.info("创建虚拟订单成功: orderId={}, virtualMemberId={}, faceId={}, actualPayment={}", orderId, virtualMemberId, faceId, actualPayment);
|
||||
|
||||
// 7. 触发购买后逻辑(调用setUserIsBuyItem)
|
||||
setUserIsBuyItem(virtualMemberId, memberPrintId.longValue(), orderId, needEnhance);
|
||||
log.info("触发购买后逻辑完成: orderId={}", orderId);
|
||||
|
||||
// 8. 返回结果
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("orderId", orderId);
|
||||
result.put("faceId", faceId);
|
||||
result.put("virtualMemberId", virtualMemberId);
|
||||
result.put("memberPrintId", memberPrintId);
|
||||
result.put("needPay", false);
|
||||
|
||||
if (actualPayment) {
|
||||
if (order.getPayPrice().compareTo(BigDecimal.ZERO) <= 0) {
|
||||
// 计算后价格为0,直接走免费逻辑
|
||||
order.setStatus(OrderStateEnum.PAID.getState());
|
||||
order.setPayAt(new Date());
|
||||
orderMapper.updateOrder(order);
|
||||
log.info("待支付订单计算后价格为0,直接完成购买: orderId={}", orderId);
|
||||
result.put("needPay", false);
|
||||
} else {
|
||||
// 通过 Native 支付生成二维码
|
||||
IPayAdapter payAdapter = scenicService.getScenicPayAdapter(scenicId);
|
||||
if (payAdapter instanceof WxMpPayAdapter adapter) {
|
||||
NativePayService nativePayService = new NativePayService.Builder().config(adapter.getConfig()).build();
|
||||
PrepayRequest prepayRequest = new PrepayRequest();
|
||||
prepayRequest.setAppid(adapter._config().getAppId());
|
||||
prepayRequest.setMchid(adapter._config().getMerchantId());
|
||||
prepayRequest.setDescription("照片打印");
|
||||
prepayRequest.setOutTradeNo(String.valueOf(orderId));
|
||||
prepayRequest.setNotifyUrl("https://zhentuai.com/api/mobile/wx/pay/v1/" + scenicId + "/payNotify");
|
||||
Amount amount = new Amount();
|
||||
amount.setTotal(order.getPayPrice().multiply(new BigDecimal(100)).intValue());
|
||||
prepayRequest.setAmount(amount);
|
||||
PrepayResponse prepayResponse = nativePayService.prepay(prepayRequest);
|
||||
result.put("payCode", prepayResponse.getCodeUrl());
|
||||
} else {
|
||||
throw new BaseException("该景区不支持 Native 支付");
|
||||
}
|
||||
result.put("needPay", true);
|
||||
result.put("price", order.getPayPrice());
|
||||
}
|
||||
} else {
|
||||
result.put("needPay", false);
|
||||
}
|
||||
|
||||
// 无论是否需要支付,都立即触发购买后动作(打印等)
|
||||
// setUserIsBuyItem 内部通过 Redis 去重,支付回调到达时不会重复触发
|
||||
setUserIsBuyItem(virtualMemberId, memberPrintId.longValue(), orderId, needEnhance);
|
||||
log.info("触发购买后逻辑完成: orderId={}", orderId);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> createBatchVirtualOrder(List<Long> sourceIds, Long scenicId, Integer printerId, Boolean needEnhance, String printImgUrl, Boolean needActualPayment) {
|
||||
if (sourceIds == null || sourceIds.isEmpty()) {
|
||||
throw new BaseException("sourceIds不能为空");
|
||||
}
|
||||
|
||||
// 1. 校验所有source并收集faceSample
|
||||
List<SourceEntity> sources = new ArrayList<>();
|
||||
FaceSampleEntity firstFaceSample = null;
|
||||
for (Long sourceId : sourceIds) {
|
||||
SourceEntity source = sourceMapper.getEntity(sourceId);
|
||||
if (source == null) {
|
||||
throw new BaseException("Source记录不存在: " + sourceId);
|
||||
}
|
||||
if (!scenicId.equals(source.getScenicId())) {
|
||||
throw new BaseException("Source记录不属于该景区: " + sourceId);
|
||||
}
|
||||
FaceSampleEntity faceSample = faceSampleMapper.getEntity(source.getFaceSampleId());
|
||||
if (faceSample == null) {
|
||||
throw new BaseException("人脸样本不存在, sourceId=" + sourceId);
|
||||
}
|
||||
if (firstFaceSample == null) {
|
||||
firstFaceSample = faceSample;
|
||||
}
|
||||
sources.add(source);
|
||||
}
|
||||
|
||||
// 2. 生成一个虚拟用户 + 一条人脸记录
|
||||
Long virtualMemberId = SnowFlakeUtil.getLongId();
|
||||
Long faceId = SnowFlakeUtil.getLongId();
|
||||
FaceEntity face = new FaceEntity();
|
||||
face.setId(faceId);
|
||||
face.setScenicId(scenicId);
|
||||
face.setMemberId(virtualMemberId);
|
||||
face.setFaceUrl(firstFaceSample.getFaceUrl());
|
||||
face.setCreateAt(new Date());
|
||||
faceMapper.add(face);
|
||||
log.info("批量下单 - 创建虚拟用户: virtualMemberId={}, faceId={}, sourceCount={}", virtualMemberId, faceId, sourceIds.size());
|
||||
|
||||
// 3. 为每个source创建member_print记录
|
||||
List<Integer> memberPrintIds = new ArrayList<>();
|
||||
for (SourceEntity source : sources) {
|
||||
String photoUrl = (printImgUrl != null && !printImgUrl.isEmpty()) ? printImgUrl : source.getUrl();
|
||||
Integer memberPrintId = addUserPhoto(virtualMemberId, scenicId, photoUrl, faceId, source.getId());
|
||||
if (memberPrintId == null) {
|
||||
throw new BaseException("创建member_print记录失败, sourceId=" + source.getId());
|
||||
}
|
||||
setPhotoQuantity(virtualMemberId, scenicId, memberPrintId.longValue(), 1);
|
||||
memberPrintIds.add(memberPrintId);
|
||||
}
|
||||
|
||||
// 4. 验证打印机
|
||||
if (printerId == null) {
|
||||
List<PrinterResp> printerList = printerMapper.listByScenicId(scenicId);
|
||||
if (printerList.isEmpty()) {
|
||||
throw new BaseException("该景区没有可用的打印机");
|
||||
}
|
||||
if (printerList.size() != 1) {
|
||||
throw new BaseException("请选择打印机");
|
||||
}
|
||||
printerId = printerList.getFirst().getId();
|
||||
}
|
||||
PrinterEntity printer = printerMapper.getById(printerId);
|
||||
if (printer == null) {
|
||||
throw new BaseException("打印机不存在");
|
||||
}
|
||||
if (printer.getStatus() != 1) {
|
||||
throw new BaseException("打印机已停用");
|
||||
}
|
||||
if (!printer.getScenicId().equals(scenicId)) {
|
||||
throw new BaseException("打印机不属于该景区");
|
||||
}
|
||||
|
||||
// 5. 创建订单
|
||||
OrderEntity order = new OrderEntity();
|
||||
Long orderId = SnowFlakeUtil.getLongId();
|
||||
redisTemplate.opsForValue().set("printer_size:" + orderId, printer.getPreferPaper(), 60, TimeUnit.SECONDS);
|
||||
order.setId(orderId);
|
||||
order.setMemberId(virtualMemberId);
|
||||
order.setFaceId(faceId);
|
||||
order.setOpenId("");
|
||||
order.setScenicId(scenicId);
|
||||
order.setType(3);
|
||||
|
||||
batchSetUserPhotoListToPrinter(virtualMemberId, scenicId, printerId);
|
||||
|
||||
List<MemberPrintResp> userPhotoList = getUserPhotoList(virtualMemberId, scenicId, faceId);
|
||||
List<OrderItemEntity> orderItems = userPhotoList.stream().map(goods -> {
|
||||
OrderItemEntity orderItem = new OrderItemEntity();
|
||||
orderItem.setOrderId(orderId);
|
||||
orderItem.setGoodsId(Long.valueOf(goods.getId()));
|
||||
orderItem.setGoodsType(3);
|
||||
return orderItem;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
boolean actualPayment = Boolean.TRUE.equals(needActualPayment);
|
||||
|
||||
if (actualPayment) {
|
||||
PriceCalculationRequest priceRequest = new PriceCalculationRequest();
|
||||
priceRequest.setUserId(virtualMemberId);
|
||||
priceRequest.setScenicId(scenicId);
|
||||
|
||||
List<ProductItem> productItems = new ArrayList<>();
|
||||
ProductItem photoItem = new ProductItem();
|
||||
photoItem.setProductType(ProductType.PHOTO_PRINT);
|
||||
photoItem.setProductId(scenicId.toString());
|
||||
photoItem.setQuantity(sourceIds.size());
|
||||
photoItem.setPurchaseCount(sourceIds.size());
|
||||
photoItem.setScenicId(scenicId.toString());
|
||||
productItems.add(photoItem);
|
||||
|
||||
priceRequest.setProducts(productItems);
|
||||
priceRequest.setAutoUseCoupon(false);
|
||||
priceRequest.setPreviewOnly(false);
|
||||
|
||||
PriceCalculationResult priceResult = priceCalculationService.calculatePrice(priceRequest);
|
||||
|
||||
order.setPrice(priceResult.getFinalAmount());
|
||||
order.setSlashPrice(priceResult.getOriginalAmount());
|
||||
order.setPayPrice(priceResult.getFinalAmount());
|
||||
order.setStatus(OrderStateEnum.UNPAID.getState());
|
||||
log.info("批量下单 - 待支付订单: orderId={}, price={}, count={}", orderId, priceResult.getFinalAmount(), sourceIds.size());
|
||||
|
||||
if (needEnhance != null) {
|
||||
redisTemplate.opsForValue().set("virtual_order_enhance:" + orderId, needEnhance.toString(), 24, TimeUnit.HOURS);
|
||||
}
|
||||
} else {
|
||||
order.setPrice(BigDecimal.ZERO);
|
||||
order.setSlashPrice(BigDecimal.ZERO);
|
||||
order.setPayPrice(BigDecimal.ZERO);
|
||||
order.setStatus(OrderStateEnum.PAID.getState());
|
||||
order.setPayAt(new Date());
|
||||
}
|
||||
|
||||
orderMapper.add(order);
|
||||
int addOrderItems = orderMapper.addOrderItems(orderItems);
|
||||
if (addOrderItems == NumberConstant.ZERO) {
|
||||
throw new BaseException("订单添加失败");
|
||||
}
|
||||
|
||||
log.info("批量下单 - 订单创建成功: orderId={}, itemCount={}", orderId, orderItems.size());
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("orderId", orderId);
|
||||
result.put("faceId", faceId);
|
||||
result.put("virtualMemberId", virtualMemberId);
|
||||
result.put("memberPrintIds", memberPrintIds);
|
||||
result.put("sourceIds", sourceIds);
|
||||
|
||||
if (actualPayment) {
|
||||
if (order.getPayPrice().compareTo(BigDecimal.ZERO) <= 0) {
|
||||
order.setStatus(OrderStateEnum.PAID.getState());
|
||||
order.setPayAt(new Date());
|
||||
orderMapper.updateOrder(order);
|
||||
log.info("批量下单 - 价格为0直接完成: orderId={}", orderId);
|
||||
result.put("needPay", false);
|
||||
} else {
|
||||
IPayAdapter payAdapter = scenicService.getScenicPayAdapter(scenicId);
|
||||
if (payAdapter instanceof WxMpPayAdapter adapter) {
|
||||
NativePayService nativePayService = new NativePayService.Builder().config(adapter.getConfig()).build();
|
||||
PrepayRequest prepayRequest = new PrepayRequest();
|
||||
prepayRequest.setAppid(adapter._config().getAppId());
|
||||
prepayRequest.setMchid(adapter._config().getMerchantId());
|
||||
prepayRequest.setDescription("照片打印 x" + sourceIds.size());
|
||||
prepayRequest.setOutTradeNo(String.valueOf(orderId));
|
||||
prepayRequest.setNotifyUrl("https://zhentuai.com/api/mobile/wx/pay/v1/" + scenicId + "/payNotify");
|
||||
Amount amount = new Amount();
|
||||
amount.setTotal(order.getPayPrice().multiply(new BigDecimal(100)).intValue());
|
||||
prepayRequest.setAmount(amount);
|
||||
PrepayResponse prepayResponse = nativePayService.prepay(prepayRequest);
|
||||
result.put("payCode", prepayResponse.getCodeUrl());
|
||||
} else {
|
||||
throw new BaseException("该景区不支持 Native 支付");
|
||||
}
|
||||
result.put("needPay", true);
|
||||
result.put("price", order.getPayPrice());
|
||||
}
|
||||
} else {
|
||||
result.put("needPay", false);
|
||||
}
|
||||
|
||||
// 触发购买后逻辑(setUserIsBuyItem 内部遍历 orderItems 处理所有 memberPrint)
|
||||
setUserIsBuyItem(virtualMemberId, memberPrintIds.getFirst().longValue(), orderId, needEnhance);
|
||||
log.info("批量下单 - 购买后逻辑完成: orderId={}", orderId);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrinterEntity getByAccessKey(String accessKey) {
|
||||
if (accessKey == null) {
|
||||
throw new BaseException("accessKey不能为空");
|
||||
}
|
||||
PrinterEntity printer = printerMapper.findByAccessKey(accessKey);
|
||||
if (printer == null) {
|
||||
throw new BaseException("打印机不存在");
|
||||
}
|
||||
return printer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getScenicBasicByAccessKey(String accessKey) {
|
||||
PrinterEntity printer = getByAccessKey(accessKey);
|
||||
if (printer.getScenicId() == null) {
|
||||
throw new BaseException("打印机未关联景区");
|
||||
}
|
||||
return scenicRepository.getScenicBasic(printer.getScenicId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void openPrinter(String accessKey) {
|
||||
PrinterEntity printer = getByAccessKey(accessKey);
|
||||
printer.setStatus(1);
|
||||
printerMapper.update(printer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void closePrinter(String accessKey) {
|
||||
PrinterEntity printer = getByAccessKey(accessKey);
|
||||
printer.setStatus(0);
|
||||
printerMapper.update(printer);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,442 @@
|
||||
package com.ycwl.basic.service.task;
|
||||
|
||||
import com.ycwl.basic.biz.FaceStatusManager;
|
||||
import com.ycwl.basic.enums.TemplateRenderStatus;
|
||||
import com.ycwl.basic.integration.render.dto.job.FinalizeMP4Response;
|
||||
import com.ycwl.basic.integration.render.dto.job.JobStatusResponse;
|
||||
import com.ycwl.basic.integration.render.service.RenderJobIntegrationService;
|
||||
import com.ycwl.basic.mapper.TaskMapper;
|
||||
import com.ycwl.basic.mapper.VideoMapper;
|
||||
import com.ycwl.basic.mapper.task.TaskRenderJobMappingMapper;
|
||||
import com.ycwl.basic.model.pc.task.entity.TaskEntity;
|
||||
import com.ycwl.basic.model.pc.video.entity.VideoEntity;
|
||||
import com.ycwl.basic.model.task.entity.TaskRenderJobMappingEntity;
|
||||
import com.ycwl.basic.repository.MemberRelationRepository;
|
||||
import com.ycwl.basic.repository.VideoRepository;
|
||||
import com.ycwl.basic.repository.VideoTaskRepository;
|
||||
import com.ycwl.basic.utils.SnowFlakeUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 渲染作业轮询服务
|
||||
* 定时查询zt-render-worker服务中的渲染作业状态,并更新本地task状态
|
||||
*
|
||||
* 状态流转:
|
||||
* PENDING → PREVIEW_READY → MP4_COMPOSING → COMPLETED
|
||||
* │ │ │ │
|
||||
* └────────────┴──────────────┴──────────────┴──→ FAILED
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@EnableScheduling
|
||||
@RequiredArgsConstructor
|
||||
@Profile({"prod"}) // 开发和生产环境启用
|
||||
public class RenderJobPollingService {
|
||||
|
||||
private final TaskRenderJobMappingMapper mappingMapper;
|
||||
private final RenderJobIntegrationService renderJobService;
|
||||
private final TaskMapper taskMapper;
|
||||
private final VideoMapper videoMapper;
|
||||
private final VideoTaskRepository videoTaskRepository;
|
||||
private final VideoRepository videoRepository;
|
||||
private final MemberRelationRepository memberRelationRepository;
|
||||
private final FaceStatusManager faceStatusManager;
|
||||
|
||||
/**
|
||||
* 定时轮询间隔:1+1=2秒
|
||||
*/
|
||||
private static final int POLL_INTERVAL_SECONDS = 1;
|
||||
|
||||
/**
|
||||
* 每次查询的最大记录数
|
||||
*/
|
||||
private static final int BATCH_SIZE = 50;
|
||||
|
||||
/**
|
||||
* 定时轮询渲染作业状态
|
||||
* 每1秒执行一次
|
||||
*/
|
||||
@Scheduled(fixedDelay = 1000)
|
||||
public void pollRenderJobs() {
|
||||
try {
|
||||
log.debug("[渲染轮询] 开始轮询渲染作业状态");
|
||||
|
||||
// 查询待轮询的记录(包含MP4_COMPOSING状态)
|
||||
List<String> pendingStatuses = Arrays.asList(
|
||||
TaskRenderJobMappingEntity.STATUS_PENDING,
|
||||
TaskRenderJobMappingEntity.STATUS_PREVIEW_READY,
|
||||
TaskRenderJobMappingEntity.STATUS_MP4_COMPOSING
|
||||
);
|
||||
|
||||
List<TaskRenderJobMappingEntity> mappings = mappingMapper.selectPendingForPolling(
|
||||
pendingStatuses,
|
||||
POLL_INTERVAL_SECONDS,
|
||||
BATCH_SIZE
|
||||
);
|
||||
|
||||
if (mappings.isEmpty()) {
|
||||
log.debug("[渲染轮询] 无待处理记录");
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("[渲染轮询] 查询到 {} 条待处理记录", mappings.size());
|
||||
|
||||
// 处理每条记录
|
||||
for (TaskRenderJobMappingEntity mapping : mappings) {
|
||||
try {
|
||||
processMapping(mapping);
|
||||
} catch (Exception e) {
|
||||
log.error("[渲染轮询] 处理失败, mappingId: {}, taskId: {}, renderJobId: {}, error: {}",
|
||||
mapping.getId(), mapping.getTaskId(), mapping.getRenderJobId(), e.getMessage(), e);
|
||||
handleProcessError(mapping, e);
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("[渲染轮询] 轮询完成");
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[渲染轮询] 轮询异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理单条mapping记录
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void processMapping(TaskRenderJobMappingEntity mapping) {
|
||||
Long renderJobId = mapping.getRenderJobId();
|
||||
Long taskId = mapping.getTaskId();
|
||||
String currentStatus = mapping.getRenderStatus();
|
||||
|
||||
log.debug("[渲染轮询] 处理记录: mappingId={}, taskId={}, renderJobId={}, currentStatus={}",
|
||||
mapping.getId(), taskId, renderJobId, currentStatus);
|
||||
|
||||
// 查询渲染作业状态
|
||||
JobStatusResponse jobStatus;
|
||||
try {
|
||||
jobStatus = renderJobService.getJobStatus(renderJobId);
|
||||
} catch (Exception e) {
|
||||
log.warn("[渲染轮询] 查询作业状态失败, renderJobId: {}, error: {}", renderJobId, e.getMessage());
|
||||
// 注:此处不调用incrementRetryCount,因为@Transactional会回滚
|
||||
// 外层handleProcessError会负责增加重试次数
|
||||
throw e;
|
||||
}
|
||||
|
||||
// 检查作业状态
|
||||
String status = jobStatus.getStatus();
|
||||
Integer publishedCount = jobStatus.getPublishedCount();
|
||||
Integer segmentCount = jobStatus.getSegmentCount();
|
||||
String playUrl = jobStatus.getPlayUrl();
|
||||
String mp4Url = jobStatus.getMp4Url();
|
||||
|
||||
log.info("[渲染轮询] 作业状态: taskId={}, status={}, publishedCount={}/{}, playUrl={}, mp4Url={}",
|
||||
taskId, status, publishedCount, segmentCount, playUrl, mp4Url);
|
||||
|
||||
// 处理失败状态
|
||||
if ("FAILED".equals(status) || "CANCELED".equals(status)) {
|
||||
handleJobFailed(mapping, jobStatus);
|
||||
return;
|
||||
}
|
||||
|
||||
// 状态流转处理
|
||||
switch (currentStatus) {
|
||||
case TaskRenderJobMappingEntity.STATUS_PENDING:
|
||||
handlePendingStatus(mapping, jobStatus, taskId);
|
||||
break;
|
||||
case TaskRenderJobMappingEntity.STATUS_PREVIEW_READY:
|
||||
handlePreviewReadyStatus(mapping, jobStatus, taskId);
|
||||
break;
|
||||
case TaskRenderJobMappingEntity.STATUS_MP4_COMPOSING:
|
||||
handleMp4ComposingStatus(mapping, jobStatus, taskId);
|
||||
break;
|
||||
default:
|
||||
log.warn("[渲染轮询] 未知状态: {}", currentStatus);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理PENDING状态
|
||||
* PENDING → PREVIEW_READY:当publishedCount >= 2时
|
||||
*/
|
||||
private void handlePendingStatus(TaskRenderJobMappingEntity mapping, JobStatusResponse jobStatus, Long taskId) {
|
||||
Integer publishedCount = jobStatus.getPublishedCount();
|
||||
Integer segmentCount = jobStatus.getSegmentCount();
|
||||
String playUrl = jobStatus.getPlayUrl();
|
||||
|
||||
if (publishedCount != null && publishedCount >= TaskRenderJobMappingEntity.MIN_PUBLISHED_FOR_PREVIEW) {
|
||||
log.info("[渲染轮询] 预览就绪: taskId={}, publishedCount={}/{}, playUrl={}",
|
||||
taskId, publishedCount, segmentCount, playUrl);
|
||||
|
||||
// 更新mapping状态为PREVIEW_READY
|
||||
updateMappingStatus(mapping.getId(), TaskRenderJobMappingEntity.STATUS_PREVIEW_READY,
|
||||
publishedCount, segmentCount, playUrl, null);
|
||||
|
||||
// 更新模板渲染状态缓存为已渲染(预览就绪即视为渲染成功)
|
||||
updateTemplateRenderStatus(taskId, TemplateRenderStatus.RENDERED);
|
||||
|
||||
// 更新task的videoUrl为预览地址
|
||||
if (StringUtils.isNotBlank(playUrl)) {
|
||||
TaskEntity task = new TaskEntity();
|
||||
task.setId(taskId);
|
||||
task.setVideoUrl(playUrl);
|
||||
task.setStatus(1); // 设置为完成状态
|
||||
taskMapper.update(task);
|
||||
videoTaskRepository.clearTaskCache(taskId);
|
||||
log.info("[渲染轮询] 已更新task预览URL和状态: taskId={}, playUrl={}, status=1", taskId, playUrl);
|
||||
|
||||
// 处理video记录(类似taskSuccess逻辑)
|
||||
try {
|
||||
handleVideoRecordForPreview(taskId, playUrl);
|
||||
} catch (Exception e) {
|
||||
log.warn("[渲染轮询] 处理video记录失败: taskId={}, error={}", taskId, e.getMessage(), e);
|
||||
}
|
||||
|
||||
// 异步发送视频生成通知(仅记录日志,实际通知可能需要在MP4完成后)
|
||||
log.info("[渲染轮询] 预览视频已就绪,可发送通知: taskId={}", taskId);
|
||||
}
|
||||
} else {
|
||||
// 更新片段信息
|
||||
updateMappingStatus(mapping.getId(), TaskRenderJobMappingEntity.STATUS_PENDING,
|
||||
publishedCount, segmentCount, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理PREVIEW_READY状态
|
||||
* PREVIEW_READY → MP4_COMPOSING:当所有片段都已发布时,调用finalize-mp4接口
|
||||
*/
|
||||
private void handlePreviewReadyStatus(TaskRenderJobMappingEntity mapping, JobStatusResponse jobStatus, Long taskId) {
|
||||
Integer publishedCount = jobStatus.getPublishedCount();
|
||||
Integer segmentCount = jobStatus.getSegmentCount();
|
||||
String playUrl = jobStatus.getPlayUrl();
|
||||
Long renderJobId = mapping.getRenderJobId();
|
||||
|
||||
// 检查是否所有片段都已发布
|
||||
if (publishedCount != null && segmentCount != null && publishedCount.equals(segmentCount) && segmentCount > 0) {
|
||||
log.info("[渲染轮询] 所有片段已发布,开始创建MP4合成任务: taskId={}, renderJobId={}, publishedCount={}/{}",
|
||||
taskId, renderJobId, publishedCount, segmentCount);
|
||||
|
||||
try {
|
||||
// 调用finalize-mp4接口创建MP4合成任务
|
||||
FinalizeMP4Response response = renderJobService.createFinalizeMP4Task(renderJobId);
|
||||
log.info("[渲染轮询] MP4合成任务创建成功: taskId={}, renderJobId={}, mp4TaskId={}, status={}",
|
||||
taskId, renderJobId, response.getTaskId(), response.getStatus());
|
||||
|
||||
// 更新mapping状态为MP4_COMPOSING
|
||||
updateMappingStatus(mapping.getId(), TaskRenderJobMappingEntity.STATUS_MP4_COMPOSING,
|
||||
publishedCount, segmentCount, playUrl, null);
|
||||
|
||||
} catch (Exception e) {
|
||||
// 409表示任务已存在,直接进入MP4_COMPOSING状态
|
||||
if (e.getMessage() != null && e.getMessage().contains("409")) {
|
||||
log.info("[渲染轮询] MP4合成任务已存在,继续等待: taskId={}, renderJobId={}", taskId, renderJobId);
|
||||
updateMappingStatus(mapping.getId(), TaskRenderJobMappingEntity.STATUS_MP4_COMPOSING,
|
||||
publishedCount, segmentCount, playUrl, null);
|
||||
} else {
|
||||
log.warn("[渲染轮询] 创建MP4合成任务失败: taskId={}, renderJobId={}, error={}",
|
||||
taskId, renderJobId, e.getMessage());
|
||||
// 不改变状态,下次轮询重试
|
||||
updateMappingStatus(mapping.getId(), TaskRenderJobMappingEntity.STATUS_PREVIEW_READY,
|
||||
publishedCount, segmentCount, playUrl, null);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 更新片段信息
|
||||
updateMappingStatus(mapping.getId(), TaskRenderJobMappingEntity.STATUS_PREVIEW_READY,
|
||||
publishedCount, segmentCount, playUrl, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理MP4_COMPOSING状态
|
||||
* MP4_COMPOSING → COMPLETED:当mp4Url有值时
|
||||
*/
|
||||
private void handleMp4ComposingStatus(TaskRenderJobMappingEntity mapping, JobStatusResponse jobStatus, Long taskId) {
|
||||
Integer publishedCount = jobStatus.getPublishedCount();
|
||||
Integer segmentCount = jobStatus.getSegmentCount();
|
||||
String playUrl = jobStatus.getPlayUrl();
|
||||
String mp4Url = jobStatus.getMp4Url();
|
||||
|
||||
if (StringUtils.isNotBlank(mp4Url)) {
|
||||
log.info("[渲染轮询] MP4合成完成: taskId={}, mp4Url={}", taskId, mp4Url);
|
||||
|
||||
// 更新mapping状态为COMPLETED
|
||||
updateMappingStatus(mapping.getId(), TaskRenderJobMappingEntity.STATUS_COMPLETED,
|
||||
publishedCount, segmentCount, playUrl, mp4Url);
|
||||
|
||||
// 更新task的videoUrl为最终MP4地址
|
||||
TaskEntity task = new TaskEntity();
|
||||
task.setId(taskId);
|
||||
task.setVideoUrl(mp4Url);
|
||||
taskMapper.update(task);
|
||||
videoTaskRepository.clearTaskCache(taskId);
|
||||
log.info("[渲染轮询] 已更新task最终MP4 URL: taskId={}, mp4Url={}", taskId, mp4Url);
|
||||
|
||||
// 更新video记录的videoUrl为最终MP4地址
|
||||
try {
|
||||
handleVideoRecordForMP4(taskId, mp4Url);
|
||||
} catch (Exception e) {
|
||||
log.warn("[渲染轮询] 更新video的MP4 URL失败: taskId={}, error={}", taskId, e.getMessage(), e);
|
||||
}
|
||||
} else {
|
||||
// MP4还在合成中,更新片段信息
|
||||
log.debug("[渲染轮询] MP4合成中: taskId={}, 等待下次轮询", taskId);
|
||||
updateMappingStatus(mapping.getId(), TaskRenderJobMappingEntity.STATUS_MP4_COMPOSING,
|
||||
publishedCount, segmentCount, playUrl, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理作业失败
|
||||
*/
|
||||
private void handleJobFailed(TaskRenderJobMappingEntity mapping, JobStatusResponse jobStatus) {
|
||||
String errorCode = jobStatus.getErrorCode();
|
||||
String errorMessage = jobStatus.getErrorMessage();
|
||||
|
||||
log.warn("[渲染轮询] 作业失败: taskId={}, status={}, errorCode={}, errorMessage={}",
|
||||
mapping.getTaskId(), jobStatus.getStatus(), errorCode, errorMessage);
|
||||
|
||||
mappingMapper.updateToFailed(
|
||||
mapping.getId(),
|
||||
errorCode,
|
||||
errorMessage,
|
||||
new Date()
|
||||
);
|
||||
|
||||
// 渲染失败,重置模板渲染状态
|
||||
updateTemplateRenderStatus(mapping.getTaskId(), TemplateRenderStatus.NONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新模板渲染状态缓存
|
||||
* 根据 taskId 查询关联的 faceId 和 templateId,更新 FaceStatusManager 中的渲染状态
|
||||
*/
|
||||
private void updateTemplateRenderStatus(Long taskId, TemplateRenderStatus status) {
|
||||
try {
|
||||
var taskInfo = taskMapper.getById(taskId);
|
||||
if (taskInfo != null && taskInfo.getFaceId() != null && taskInfo.getTemplateId() != null) {
|
||||
faceStatusManager.setTemplateRenderStatus(taskInfo.getFaceId(), taskInfo.getTemplateId(), status);
|
||||
log.info("[渲染轮询] 已更新模板渲染状态: taskId={}, faceId={}, templateId={}, status={}",
|
||||
taskId, taskInfo.getFaceId(), taskInfo.getTemplateId(), status.getDescription());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[渲染轮询] 更新模板渲染状态缓存失败: taskId={}, error={}", taskId, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新mapping状态
|
||||
*/
|
||||
private void updateMappingStatus(Long id, String renderStatus, Integer publishedCount,
|
||||
Integer segmentCount, String previewUrl, String mp4Url) {
|
||||
mappingMapper.updateRenderStatus(
|
||||
id,
|
||||
renderStatus,
|
||||
publishedCount,
|
||||
segmentCount,
|
||||
previewUrl,
|
||||
mp4Url,
|
||||
new Date()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理异常
|
||||
*/
|
||||
private void handleProcessError(TaskRenderJobMappingEntity mapping, Exception e) {
|
||||
try {
|
||||
mappingMapper.incrementRetryCount(mapping.getId());
|
||||
|
||||
// 超过最大重试次数,标记为失败
|
||||
if (mapping.getRetryCount() != null &&
|
||||
mapping.getRetryCount() >= TaskRenderJobMappingEntity.MAX_RETRY_COUNT - 1) {
|
||||
mappingMapper.updateToFailed(
|
||||
mapping.getId(),
|
||||
"MAX_RETRY",
|
||||
"超过最大重试次数: " + e.getMessage(),
|
||||
new Date()
|
||||
);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.error("[渲染轮询] 处理错误失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理video记录(预览就绪时)
|
||||
* 类似taskSuccess的逻辑,但简化版本
|
||||
*/
|
||||
private void handleVideoRecordForPreview(Long taskId, String videoUrl) {
|
||||
try {
|
||||
var taskResp = taskMapper.getById(taskId);
|
||||
if (taskResp == null) {
|
||||
log.warn("[渲染轮询] task不存在: taskId={}", taskId);
|
||||
return;
|
||||
}
|
||||
|
||||
VideoEntity video = videoMapper.findByTaskId(taskId);
|
||||
if (video != null) {
|
||||
// 更新已有video记录
|
||||
video.setVideoUrl(videoUrl);
|
||||
videoMapper.update(video);
|
||||
videoRepository.clearVideoCache(video.getId());
|
||||
log.info("[渲染轮询] 已更新video预览URL: taskId={}, videoId={}, videoUrl={}",
|
||||
taskId, video.getId(), videoUrl);
|
||||
} else {
|
||||
// 创建新video记录
|
||||
video = new VideoEntity();
|
||||
video.setId(SnowFlakeUtil.getLongId());
|
||||
video.setScenicId(taskResp.getScenicId());
|
||||
video.setTemplateId(taskResp.getTemplateId());
|
||||
video.setTaskId(taskId);
|
||||
video.setFaceId(taskResp.getFaceId());
|
||||
video.setVideoUrl(videoUrl);
|
||||
video.setCreateTime(new Date());
|
||||
videoMapper.add(video);
|
||||
log.info("[渲染轮询] 已创建video预览记录: taskId={}, videoId={}, videoUrl={}",
|
||||
taskId, video.getId(), videoUrl);
|
||||
}
|
||||
|
||||
// 更新member_video关联表(isBuy=0,预览阶段未购买)
|
||||
videoMapper.updateRelationWhenTaskSuccess(taskId, video.getId(), 0);
|
||||
memberRelationRepository.clearVCacheByFace(taskResp.getFaceId());
|
||||
log.info("[渲染轮询] 已更新member_video关联: taskId={}, videoId={}", taskId, video.getId());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[渲染轮询] 处理video记录失败: taskId={}", taskId, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新video记录的MP4地址(MP4合成完成时)
|
||||
*/
|
||||
private void handleVideoRecordForMP4(Long taskId, String mp4Url) {
|
||||
try {
|
||||
VideoEntity video = videoMapper.findByTaskId(taskId);
|
||||
if (video != null) {
|
||||
video.setVideoUrl(mp4Url);
|
||||
videoMapper.update(video);
|
||||
videoRepository.clearVideoCache(video.getId());
|
||||
log.info("[渲染轮询] 已更新video最终MP4 URL: taskId={}, videoId={}, mp4Url={}",
|
||||
taskId, video.getId(), mp4Url);
|
||||
} else {
|
||||
log.warn("[渲染轮询] video不存在,无法更新MP4 URL: taskId={}", taskId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[渲染轮询] 更新video的MP4 URL失败: taskId={}", taskId, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,8 @@ import com.ycwl.basic.integration.render.dto.job.CreatePreviewRequest;
|
||||
import com.ycwl.basic.integration.render.dto.job.CreatePreviewResponse;
|
||||
import com.ycwl.basic.integration.render.dto.job.MaterialDTO;
|
||||
import com.ycwl.basic.integration.render.service.RenderJobIntegrationService;
|
||||
import com.ycwl.basic.mapper.task.TaskRenderJobMappingMapper;
|
||||
import com.ycwl.basic.model.task.entity.TaskRenderJobMappingEntity;
|
||||
import com.ycwl.basic.repository.MemberRelationRepository;
|
||||
import com.ycwl.basic.repository.SourceRepository;
|
||||
import com.ycwl.basic.utils.JacksonUtil;
|
||||
@@ -69,6 +71,7 @@ import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
@@ -124,6 +127,8 @@ public class TaskTaskServiceImpl implements TaskService {
|
||||
private FaceStatusManager faceStatusManager;
|
||||
@Autowired
|
||||
private RenderJobIntegrationService renderJobIntegrationService;
|
||||
@Autowired
|
||||
private TaskRenderJobMappingMapper taskRenderJobMappingMapper;
|
||||
|
||||
private RenderWorkerEntity getWorker(@NonNull WorkerAuthReqVo req) {
|
||||
String accessKey = req.getAccessKey();
|
||||
@@ -224,26 +229,11 @@ public class TaskTaskServiceImpl implements TaskService {
|
||||
} else {
|
||||
updTemplateList = templateRepository.getAllEnabledTemplateList();
|
||||
}
|
||||
RenderWorkerConfigManager configManager = repository.getWorkerConfigManager(worker.getId());
|
||||
try {
|
||||
if (lock.tryLock(2, TimeUnit.SECONDS)) {
|
||||
try {
|
||||
List<TaskRespVO> taskList;
|
||||
if (Strings.isNotBlank(configManager.getString("scenic_only"))) {
|
||||
taskList = taskMapper.selectNotRunningByScenicList(configManager.getString("scenic_only"));
|
||||
} else {
|
||||
var _taskList = taskMapper.selectNotRunning();
|
||||
taskList = _taskList.stream().filter(task -> {
|
||||
boolean workerSelfHostedScenic = isWorkerSelfHostedScenic(task.getScenicId());
|
||||
return !workerSelfHostedScenic;
|
||||
}).limit(1).toList();
|
||||
}
|
||||
resp.setTasks(taskList);
|
||||
resp.setTasks(Collections.emptyList());
|
||||
resp.setTemplates(updTemplateList);
|
||||
taskList.forEach(task -> {
|
||||
taskMapper.assignToWorker(task.getId(), worker.getId());
|
||||
videoTaskRepository.clearTaskCache(task.getId());
|
||||
});
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
@@ -446,6 +436,7 @@ public class TaskTaskServiceImpl implements TaskService {
|
||||
if (video != null) {
|
||||
log.info("自动创建任务:跳过(auto_replace_vlog=false), faceId:{}, templateId:{}, existingTaskId:{}, videoId:{}",
|
||||
faceId, templateId, taskEntity.getId(), video.getId());
|
||||
faceStatusManager.setTemplateRenderStatus(faceId, templateId, TemplateRenderStatus.RENDERED);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -502,7 +493,10 @@ public class TaskTaskServiceImpl implements TaskService {
|
||||
}
|
||||
videoMapper.addRelation(memberVideoEntity);
|
||||
memberRelationRepository.clearVCacheByFace(faceId);
|
||||
faceStatusManager.setTemplateRenderStatus(faceId, templateId, TemplateRenderStatus.RENDERED);
|
||||
// 仅当复用已有视频时立即标记为已渲染,新任务由 RenderJobPollingService 在 PREVIEW_READY 时更新
|
||||
if (memberVideoEntity.getVideoId() != null) {
|
||||
faceStatusManager.setTemplateRenderStatus(faceId, templateId, TemplateRenderStatus.RENDERED);
|
||||
}
|
||||
faceStatusManager.markNoNewPieces(faceId, templateId);
|
||||
};
|
||||
VideoPieceGetter.addTask(task);
|
||||
@@ -552,10 +546,6 @@ public class TaskTaskServiceImpl implements TaskService {
|
||||
videoMapper.add(video);
|
||||
}
|
||||
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(task.getScenicId());
|
||||
IStorageAdapter adapter = scenicService.getScenicTmpStorageAdapter(task.getScenicId());
|
||||
String hash = MD5.create().digestHex(task.getTaskParams() + task.getFaceId().toString());
|
||||
String filename = StorageUtil.joinPath(StorageConstant.VLOG_PATH, task.getTemplateId().toString() + "_" + hash + "_" + task.getScenicId() + ".mp4");
|
||||
adapter.setAcl(StorageAcl.PUBLIC_READ, filename);
|
||||
int isBuy = 0;
|
||||
FaceEntity face = faceRepository.getFace(task.getFaceId());
|
||||
if (face != null) {
|
||||
@@ -729,6 +719,27 @@ public class TaskTaskServiceImpl implements TaskService {
|
||||
CreatePreviewResponse response = renderJobIntegrationService.createPreview(request);
|
||||
log.info("[灰度测试] 渲染预览任务创建成功, taskId: {}, renderJobId: {}, playUrl: {}",
|
||||
taskId, response.getJobId(), response.getPlayUrl());
|
||||
|
||||
// 写入mapping表,供轮询服务处理
|
||||
try {
|
||||
// 原位替换模式下可能已有旧映射,先删除再插入
|
||||
TaskRenderJobMappingEntity existingMapping = taskRenderJobMappingMapper.selectByTaskId(taskId);
|
||||
if (existingMapping != null) {
|
||||
taskRenderJobMappingMapper.deleteById(existingMapping.getId());
|
||||
log.info("[灰度测试] 已删除旧mapping, taskId: {}, oldRenderJobId: {}", taskId, existingMapping.getRenderJobId());
|
||||
}
|
||||
TaskRenderJobMappingEntity mapping = new TaskRenderJobMappingEntity();
|
||||
mapping.setTaskId(taskId);
|
||||
mapping.setRenderJobId(response.getJobId());
|
||||
mapping.setRenderStatus(TaskRenderJobMappingEntity.STATUS_PENDING);
|
||||
mapping.setPublishedCount(0);
|
||||
mapping.setSegmentCount(0);
|
||||
mapping.setRetryCount(0);
|
||||
taskRenderJobMappingMapper.insert(mapping);
|
||||
log.info("[灰度测试] 写入mapping成功, taskId: {}, renderJobId: {}", taskId, response.getJobId());
|
||||
} catch (Exception ex) {
|
||||
log.warn("[灰度测试] 写入mapping失败,不影响主流程, taskId: {}, error: {}", taskId, ex.getMessage());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 灰度测试:不管返回什么或者报错,都不影响现有流程
|
||||
log.warn("[灰度测试] 渲染预览任务创建失败,不影响主流程, taskId: {}, templateId: {}, error: {}",
|
||||
|
||||
@@ -66,6 +66,10 @@ public class DownloadNotificationTasker {
|
||||
}
|
||||
|
||||
MemberRespVO member = memberMapper.getById(item.getMemberId());
|
||||
if (member == null || member.getOpenId() == null) {
|
||||
log.debug("用户[memberId={}]不存在或未绑定微信,跳过", item.getMemberId());
|
||||
return;
|
||||
}
|
||||
// 发送模板消息
|
||||
HashMap<String, Object> variables = new HashMap<>();
|
||||
ScenicV2DTO scenic = scenicRepository.getScenicBasic(item.getScenicId());
|
||||
@@ -111,6 +115,10 @@ public class DownloadNotificationTasker {
|
||||
sentMemberIds.add(item.getMemberId());
|
||||
|
||||
MemberRespVO member = memberMapper.getById(item.getMemberId());
|
||||
if (member == null || member.getOpenId() == null) {
|
||||
log.debug("用户[memberId={}]不存在或未绑定微信,跳过", item.getMemberId());
|
||||
return;
|
||||
}
|
||||
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(item.getScenicId());
|
||||
Integer videoStoreDay = scenicConfig.getInteger("video_store_day");
|
||||
if (videoStoreDay == null) {
|
||||
@@ -161,6 +169,10 @@ public class DownloadNotificationTasker {
|
||||
sentMemberIds.add(item.getMemberId());
|
||||
|
||||
MemberRespVO member = memberMapper.getById(item.getMemberId());
|
||||
if (member == null || member.getOpenId() == null) {
|
||||
log.debug("用户[memberId={}]不存在或未绑定微信,跳过", item.getMemberId());
|
||||
return;
|
||||
}
|
||||
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(item.getScenicId());
|
||||
Integer videoStoreDay = scenicConfig.getInteger("video_store_day");
|
||||
if (videoStoreDay == null) {
|
||||
@@ -237,6 +249,10 @@ public class DownloadNotificationTasker {
|
||||
}
|
||||
|
||||
MemberRespVO member = memberMapper.getById(item.getMemberId());
|
||||
if (member == null || member.getOpenId() == null) {
|
||||
log.debug("用户[memberId={}]不存在或未绑定微信,跳过", item.getMemberId());
|
||||
return;
|
||||
}
|
||||
// 发送模板消息
|
||||
HashMap<String, Object> variables = new HashMap<>();
|
||||
variables.put("scenicName", scenic.getName());
|
||||
|
||||
@@ -60,7 +60,7 @@ public class ScenicStatsTask {
|
||||
});
|
||||
}
|
||||
}
|
||||
@Scheduled(cron = "0 0 2 * * *")
|
||||
@Scheduled(cron = "0 1 0 * * *")
|
||||
public void countScenicStats() {
|
||||
log.info("开始执行景区统计任务,统计前7天至昨天的数据");
|
||||
|
||||
@@ -93,8 +93,8 @@ public class ScenicStatsTask {
|
||||
// 写入数据库(REPLACE INTO 会自动更新已存在的记录)
|
||||
statisticsMapper.insertStat(scenicId, startTime, data);
|
||||
|
||||
// 删除该景区的缓存,确保下次查询时获取最新数据
|
||||
invalidateStatisticsCache(scenicId);
|
||||
// 删除该景区该日期的缓存,确保下次查询时获取最新数据
|
||||
invalidateStatisticsCache(scenicId, startTime);
|
||||
} catch (Exception e) {
|
||||
log.error("统计景区 {} 在日期 {} 的数据时发生错误", scenic.getId(), DateUtil.formatDate(startTime), e);
|
||||
}
|
||||
@@ -109,9 +109,12 @@ public class ScenicStatsTask {
|
||||
/**
|
||||
* 删除景区统计缓存
|
||||
* @param scenicId 景区ID
|
||||
* @param date 统计日期
|
||||
*/
|
||||
private void invalidateStatisticsCache(Long scenicId) {
|
||||
String redisKey = "statistics:tmp_cache:" + scenicId;
|
||||
private void invalidateStatisticsCache(Long scenicId, Date date) {
|
||||
String redisKey = String.format("statistics:tmp_cache:%s:%s",
|
||||
scenicId,
|
||||
DateUtil.formatDate(date));
|
||||
redisTemplate.delete(redisKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,6 +101,29 @@ public class ImageUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断图片是否为横图(宽度大于高度)
|
||||
* 支持URL字符串或文件路径
|
||||
*
|
||||
* @param imageSource URL字符串或文件路径
|
||||
* @return true表示横图,false表示竖图
|
||||
* @throws IOException 读取图片失败
|
||||
*/
|
||||
public static boolean isLandscape(String imageSource) throws IOException {
|
||||
BufferedImage image = null;
|
||||
try {
|
||||
image = loadImage(imageSource);
|
||||
if (image == null) {
|
||||
throw new IOException("无法读取图片: " + imageSource);
|
||||
}
|
||||
return image.getWidth() > image.getHeight();
|
||||
} finally {
|
||||
if (image != null) {
|
||||
image.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 旋转图片90度(顺时针)
|
||||
*
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
package com.ycwl.basic.watchdog;
|
||||
|
||||
import com.ycwl.basic.integration.message.dto.ZtMessage;
|
||||
import com.ycwl.basic.integration.message.service.ZtMessageProducerService;
|
||||
import com.ycwl.basic.mapper.TaskMapper;
|
||||
import com.ycwl.basic.model.pc.task.entity.TaskEntity;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@Component
|
||||
@Profile("prod")
|
||||
public class TaskWatchDog {
|
||||
|
||||
@Autowired
|
||||
private TaskMapper taskMapper;
|
||||
|
||||
@Autowired
|
||||
private ZtMessageProducerService ztMessageProducerService;
|
||||
|
||||
// 异常通知计数器
|
||||
private final Map<String, Integer> notificationCounters = new HashMap<>();
|
||||
|
||||
// 配置参数
|
||||
private static final int MAX_NOTIFICATION_COUNT = 3; // 每种异常最多通知3次
|
||||
|
||||
// 异常类型标识
|
||||
private static final String TASK_BACKLOG = "task_backlog";
|
||||
private static final String FAILED_TASKS = "failed_tasks";
|
||||
private static final String LONG_RUNNING_TASK_PREFIX = "long_running_task_"; // 长时间运行任务前缀
|
||||
|
||||
@Scheduled(fixedDelay = 1000 * 60L)
|
||||
public void scanTaskStatus() {
|
||||
List<TaskEntity> allNotRunningTaskList = taskMapper.selectAllNotRunning();
|
||||
List<TaskEntity> allFailedTaskList = taskMapper.selectAllFailed();
|
||||
List<TaskEntity> allRunningTaskList = taskMapper.selectAllRunning();
|
||||
|
||||
// 检查任务积压
|
||||
checkTaskBacklog(allNotRunningTaskList);
|
||||
|
||||
// 检查失败任务
|
||||
checkFailedTasks(allFailedTaskList);
|
||||
|
||||
// 检查长时间运行任务
|
||||
checkLongRunningTasks(allRunningTaskList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查任务积压
|
||||
*/
|
||||
private void checkTaskBacklog(List<TaskEntity> notRunningTasks) {
|
||||
if (notRunningTasks.size() > 10) {
|
||||
if (shouldSendNotification(TASK_BACKLOG)) {
|
||||
String content = String.format("当前任务队列中存在超过10个未运行任务,请及时处理!未运行任务数量:%d", notRunningTasks.size());
|
||||
sendNotification("任务堆积警告", content, TASK_BACKLOG);
|
||||
}
|
||||
} else {
|
||||
// 异常已恢复,重置计数器
|
||||
resetNotificationCounter(TASK_BACKLOG);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查失败任务
|
||||
*/
|
||||
private void checkFailedTasks(List<TaskEntity> failedTasks) {
|
||||
if (failedTasks.size() > 5) {
|
||||
if (shouldSendNotification(FAILED_TASKS)) {
|
||||
String content = String.format("当前存在超过5个失败任务(status=3),请及时检查和处理!失败任务数量:%d", failedTasks.size());
|
||||
sendNotification("任务失败警告", content, FAILED_TASKS);
|
||||
}
|
||||
} else {
|
||||
// 异常已恢复,重置计数器
|
||||
resetNotificationCounter(FAILED_TASKS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查长时间运行任务
|
||||
*/
|
||||
private void checkLongRunningTasks(List<TaskEntity> runningTasks) {
|
||||
Set<String> currentLongRunningTasks = new HashSet<>();
|
||||
|
||||
for (TaskEntity taskEntity : runningTasks) {
|
||||
if (taskEntity.getStartTime() == null) {
|
||||
continue;
|
||||
}
|
||||
// startTime已经过去3分钟了
|
||||
if (System.currentTimeMillis() - taskEntity.getStartTime().getTime() > 1000 * 60 * 3) {
|
||||
String taskKey = LONG_RUNNING_TASK_PREFIX + taskEntity.getId();
|
||||
currentLongRunningTasks.add(taskKey);
|
||||
|
||||
if (shouldSendNotification(taskKey)) {
|
||||
String content = String.format("当前【%s】渲染机的【%d】任务已超过3分钟未完成!",
|
||||
taskEntity.getWorkerId(), taskEntity.getId());
|
||||
sendNotification("长时间运行任务警告", content, taskKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 清理已恢复正常的长时运行任务的计数器
|
||||
cleanupLongRunningTaskCounters(currentLongRunningTasks);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理已恢复正常的长时运行任务的计数器
|
||||
*/
|
||||
private void cleanupLongRunningTaskCounters(Set<String> currentLongRunningTasks) {
|
||||
Set<String> keysToRemove = new HashSet<>();
|
||||
|
||||
for (String key : notificationCounters.keySet()) {
|
||||
if (key.startsWith(LONG_RUNNING_TASK_PREFIX)) {
|
||||
if (!currentLongRunningTasks.contains(key)) {
|
||||
keysToRemove.add(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移除已恢复任务的计数器
|
||||
for (String key : keysToRemove) {
|
||||
notificationCounters.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否应该发送通知
|
||||
*/
|
||||
private boolean shouldSendNotification(String abnormalType) {
|
||||
int count = notificationCounters.getOrDefault(abnormalType, 0);
|
||||
return count < MAX_NOTIFICATION_COUNT;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送通知并更新计数器
|
||||
*/
|
||||
private void sendNotification(String title, String content, String abnormalType) {
|
||||
ZtMessage ztMessage = ZtMessage.of(
|
||||
"serverchan",
|
||||
title,
|
||||
content,
|
||||
"system"
|
||||
);
|
||||
ztMessage.setSendReason("任务监控");
|
||||
ztMessage.setSendBiz("系统监控");
|
||||
|
||||
ztMessageProducerService.send(ztMessage);
|
||||
|
||||
// 更新通知计数器
|
||||
int currentCount = notificationCounters.getOrDefault(abnormalType, 0);
|
||||
notificationCounters.put(abnormalType, currentCount + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置通知计数器(异常恢复时调用)
|
||||
*/
|
||||
private void resetNotificationCounter(String abnormalType) {
|
||||
notificationCounters.remove(abnormalType);
|
||||
}
|
||||
}
|
||||
@@ -136,7 +136,7 @@
|
||||
FROM `zt`.`face`
|
||||
WHERE `scenic_id` = #{scenicId}
|
||||
AND `create_at` < #{endDate}
|
||||
and `id` not in (select face_id from member_source where is_buy = 1)
|
||||
and `id` not in (select face_id from member_source where is_buy = 1 AND deleted = 0)
|
||||
and `id` not in (select face_id from member_video where is_buy = 1)
|
||||
</select>
|
||||
</mapper>
|
||||
|
||||
@@ -94,14 +94,14 @@
|
||||
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
|
||||
WHERE s.id IS NOT NULL AND ms.deleted = 0
|
||||
),
|
||||
member_source_aicam_data AS (
|
||||
SELECT ms.member_id, ms.source_id, ms.face_id, f.face_url, s.url
|
||||
FROM member_source ms
|
||||
LEFT JOIN face f ON ms.face_id = f.id
|
||||
LEFT JOIN source s ON ms.source_id = s.id
|
||||
WHERE s.id IS NOT NULL AND ms.type = 3
|
||||
WHERE s.id IS NOT NULL AND ms.type = 3 AND ms.deleted = 0
|
||||
),
|
||||
member_photo_data AS (
|
||||
SELECT mp.member_id, 3 as type, mp.id, mp.crop_url as url, mp.quantity, mp.status, mp.create_time
|
||||
|
||||
38
src/main/resources/mapper/PrinterGuideMapper.xml
Normal file
38
src/main/resources/mapper/PrinterGuideMapper.xml
Normal file
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
|
||||
<mapper namespace="com.ycwl.basic.mapper.PrinterGuideMapper">
|
||||
<select id="listByPrinterId" resultType="com.ycwl.basic.model.pc.printer.entity.PrinterGuideEntity">
|
||||
select id, printer_id, image_url, sort_order, enabled, create_time, update_time
|
||||
from printer_guide
|
||||
where printer_id = #{printerId}
|
||||
order by sort_order asc, id asc
|
||||
</select>
|
||||
|
||||
<select id="listEnabledByPrinterId" resultType="com.ycwl.basic.model.pc.printer.entity.PrinterGuideEntity">
|
||||
select id, printer_id, image_url, sort_order, enabled, create_time, update_time
|
||||
from printer_guide
|
||||
where printer_id = #{printerId} and enabled = 1
|
||||
order by sort_order asc, id asc
|
||||
</select>
|
||||
|
||||
<insert id="insertGuide" useGeneratedKeys="true" keyProperty="id">
|
||||
insert into printer_guide(printer_id, image_url, sort_order, enabled, create_time)
|
||||
values (#{printerId}, #{imageUrl}, #{sortOrder}, #{enabled}, NOW())
|
||||
</insert>
|
||||
|
||||
<delete id="deleteById">
|
||||
delete from printer_guide where id = #{id}
|
||||
</delete>
|
||||
|
||||
<update id="updateSortOrder">
|
||||
update printer_guide
|
||||
set sort_order = #{sortOrder}, update_time = NOW()
|
||||
where id = #{id}
|
||||
</update>
|
||||
|
||||
<update id="toggleEnabled">
|
||||
update printer_guide
|
||||
set enabled = 1 - enabled, update_time = NOW()
|
||||
where id = #{id}
|
||||
</update>
|
||||
</mapper>
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
select s.scenic_id, s.device_id
|
||||
from member_source ms
|
||||
left join source s on ms.source_id = s.id
|
||||
where ms.type = 1 and s.id is not null
|
||||
where ms.type = 1 and s.id is not null and ms.deleted = 0
|
||||
and s.create_time >= #{start}
|
||||
and s.create_time <= #{end}
|
||||
group by s.scenic_id, s.device_id, ms.face_id
|
||||
@@ -53,7 +53,7 @@
|
||||
select s.scenic_id, s.device_id
|
||||
from member_source ms
|
||||
left join source s on ms.source_id = s.id
|
||||
where ms.type = 2 and s.id is not null
|
||||
where ms.type = 2 and s.id is not null and ms.deleted = 0
|
||||
and s.create_time >= #{start}
|
||||
and s.create_time <= #{end}
|
||||
group by s.scenic_id, s.device_id, ms.face_id
|
||||
|
||||
@@ -165,11 +165,11 @@
|
||||
</delete>
|
||||
<delete id="deleteNotBuyRelations">
|
||||
delete from member_source
|
||||
where scenic_id = #{scenicId} and is_buy = 0 and create_time <= #{endDate}
|
||||
where scenic_id = #{scenicId} and is_buy = 0 and create_time <= #{endDate} and deleted = 0
|
||||
</delete>
|
||||
<delete id="deleteNotBuyFaceRelation">
|
||||
delete from member_source
|
||||
where member_id = #{userId} and face_id = #{faceId} and is_buy = 0
|
||||
where member_id = #{userId} and face_id = #{faceId} and is_buy = 0 and deleted = 0
|
||||
</delete>
|
||||
<delete id="deleteUselessSource">
|
||||
delete from source where id not in (select source_id from member_source) and face_sample_id not in (select id from face_sample)
|
||||
@@ -195,7 +195,7 @@
|
||||
from member_source ms
|
||||
left join source so on ms.source_id = so.id
|
||||
|
||||
where so.id = #{id} and ms.member_id = #{userId} and so.id is not null
|
||||
where so.id = #{id} and ms.member_id = #{userId} and so.id is not null and ms.deleted = 0
|
||||
</select>
|
||||
<select id="getById" resultType="com.ycwl.basic.model.pc.source.resp.SourceRespVO">
|
||||
select so.id, scenic_id, device_id, thumb_url, type, url, video_url, so.create_time, so.update_time
|
||||
@@ -207,6 +207,7 @@
|
||||
select ms.type, ms.is_buy
|
||||
from member_source ms
|
||||
<where>
|
||||
and ms.deleted = 0
|
||||
<if test="scenicId!= null">and ms.scenic_id = #{scenicId} </if>
|
||||
<if test="memberId!= null">and ms.member_id = #{memberId} </if>
|
||||
<if test="isBuy!=null">and ms.is_buy = #{isBuy}</if>
|
||||
@@ -214,7 +215,7 @@
|
||||
group by ms.type
|
||||
</select>
|
||||
<select id="countByMemberId" resultType="java.lang.Integer">
|
||||
select count(1) from member_source where member_id = #{userId}
|
||||
select count(1) from member_source where member_id = #{userId} and deleted = 0
|
||||
</select>
|
||||
<select id="listBySampleIds" resultType="com.ycwl.basic.model.pc.source.entity.SourceEntity">
|
||||
select *
|
||||
@@ -245,6 +246,7 @@
|
||||
from member_source ms
|
||||
left join source so on ms.source_id = so.id
|
||||
<where>
|
||||
and ms.deleted = 0
|
||||
<if test="scenicId!= null">and ms.scenic_id = #{scenicId} </if>
|
||||
<if test="isBuy!=null">and ms.is_buy = #{isBuy} </if>
|
||||
<if test="type!=null">and ms.type = #{type} </if>
|
||||
@@ -258,7 +260,7 @@
|
||||
from member_source ms
|
||||
left join source so on ms.source_id = so.id
|
||||
|
||||
where ms.member_id = #{userId} and ms.source_id = #{sourceId} and so.id is not null
|
||||
where ms.member_id = #{userId} and ms.source_id = #{sourceId} and so.id is not null and ms.deleted = 0
|
||||
limit 1
|
||||
</select>
|
||||
<select id="queryByRelation" resultType="com.ycwl.basic.model.pc.source.resp.SourceRespVO">
|
||||
@@ -267,7 +269,7 @@
|
||||
left join source so on ms.source_id = so.id
|
||||
|
||||
where
|
||||
ms.member_id = #{memberId} and so.id is not null
|
||||
ms.member_id = #{memberId} and so.id is not null and ms.deleted = 0
|
||||
<if test="faceId!= null">and ms.face_id = #{faceId} </if>
|
||||
<if test="type!=null">and ms.type = #{type} </if>
|
||||
<if test="scenicId!= null">and ms.scenic_id = #{scenicId} </if>
|
||||
@@ -302,7 +304,7 @@
|
||||
ROW_NUMBER() OVER (PARTITION BY ms.face_id, ms.type ORDER BY so.create_time DESC) as rn
|
||||
FROM member_source ms
|
||||
LEFT JOIN source so ON ms.source_id = so.id
|
||||
WHERE so.id IS NOT NULL
|
||||
WHERE so.id IS NOT NULL AND ms.deleted = 0
|
||||
<if test="faceIds != null and faceIds.size() > 0">
|
||||
AND ms.face_id IN
|
||||
<foreach collection="faceIds" item="id" open="(" separator="," close=")">
|
||||
@@ -326,33 +328,33 @@
|
||||
<select id="hasRelationTo" resultType="java.lang.Integer">
|
||||
select count(1)
|
||||
from member_source
|
||||
where member_id = #{memberId} and source_id = #{sourceId} and type = #{type}
|
||||
where member_id = #{memberId} and source_id = #{sourceId} and type = #{type} and deleted = 0
|
||||
</select>
|
||||
<select id="listVideoByFaceRelation" resultType="com.ycwl.basic.model.pc.source.entity.SourceEntity">
|
||||
select s.*, ms.is_buy
|
||||
from member_source ms
|
||||
left join source s on ms.source_id = s.id
|
||||
where ms.face_id = #{faceId} and ms.type = 1
|
||||
where ms.face_id = #{faceId} and ms.type = 1 and ms.deleted = 0
|
||||
order by create_time desc
|
||||
</select>
|
||||
<select id="listVideoByScenicFaceRelation" resultType="com.ycwl.basic.model.pc.source.entity.SourceEntity">
|
||||
select s.*, ms.is_buy
|
||||
from member_source ms
|
||||
left join source s on ms.source_id = s.id
|
||||
where ms.face_id = #{faceId} and ms.type = 1 and ms.scenic_id = #{scenicId}
|
||||
where ms.face_id = #{faceId} and ms.type = 1 and ms.scenic_id = #{scenicId} and ms.deleted = 0
|
||||
order by create_time desc
|
||||
</select>
|
||||
<select id="listImageByFaceRelation" resultType="com.ycwl.basic.model.pc.source.entity.SourceEntity">
|
||||
select s.*, ms.is_buy
|
||||
from member_source ms
|
||||
left join source s on ms.source_id = s.id
|
||||
where ms.face_id = #{faceId} and ms.type = 2
|
||||
where ms.face_id = #{faceId} and ms.type = 2 and ms.deleted = 0
|
||||
</select>
|
||||
<select id="listAiCamImageByFaceRelation" resultType="com.ycwl.basic.model.pc.source.entity.SourceEntity">
|
||||
select s.*, ms.is_buy
|
||||
from member_source ms
|
||||
left join source s on ms.source_id = s.id
|
||||
where ms.face_id = #{faceId} and ms.type = 3
|
||||
where ms.face_id = #{faceId} and ms.type = 3 and ms.deleted = 0
|
||||
</select>
|
||||
<select id="getEntity" resultType="com.ycwl.basic.model.pc.source.entity.SourceEntity">
|
||||
select *
|
||||
@@ -379,7 +381,7 @@
|
||||
from member_source ms
|
||||
left join source so on ms.source_id = so.id
|
||||
where
|
||||
ms.member_id = #{memberId} and so.id is not null
|
||||
ms.member_id = #{memberId} and so.id is not null and ms.deleted = 0
|
||||
<if test="scenicId!= null">and ms.scenic_id = #{scenicId} </if>
|
||||
<if test="isBuy!=null">and ms.is_buy = #{isBuy} </if>
|
||||
<if test="type!=null">and ms.type = #{type} </if>
|
||||
@@ -388,7 +390,7 @@
|
||||
<select id="listByFaceRelation" resultType="com.ycwl.basic.model.pc.source.entity.MemberSourceEntity">
|
||||
select *
|
||||
from member_source ms
|
||||
where ms.face_id = #{faceId}
|
||||
where ms.face_id = #{faceId} and ms.deleted = 0
|
||||
<if test="type!=null">and ms.type = #{type} </if>
|
||||
</select>
|
||||
<update id="updateMemberIdByFaceId">
|
||||
@@ -400,7 +402,7 @@
|
||||
select s.*
|
||||
from source s
|
||||
inner join member_source ms on s.id = ms.source_id
|
||||
where ms.face_id = #{faceId} and s.type = 2
|
||||
where ms.face_id = #{faceId} and s.type = 2 and ms.deleted = 0
|
||||
</select>
|
||||
<select id="getBySampleIdAndType" resultType="com.ycwl.basic.model.pc.source.entity.SourceEntity">
|
||||
select *
|
||||
@@ -416,6 +418,7 @@
|
||||
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="getSourceByFaceAndDeviceIndex" resultType="com.ycwl.basic.model.pc.source.entity.SourceEntity">
|
||||
@@ -445,6 +448,7 @@
|
||||
INNER JOIN member_source ms ON s.id = ms.source_id
|
||||
WHERE ms.face_id = #{faceId}
|
||||
AND s.type = #{type}
|
||||
AND ms.deleted = 0
|
||||
)
|
||||
SELECT *
|
||||
FROM ranked_sources
|
||||
@@ -456,7 +460,7 @@
|
||||
SELECT DISTINCT s.device_id
|
||||
FROM member_source ms
|
||||
INNER JOIN source s ON ms.source_id = s.id
|
||||
WHERE ms.face_id = #{faceId}
|
||||
WHERE ms.face_id = #{faceId} AND ms.deleted = 0
|
||||
ORDER BY s.device_id ASC
|
||||
</select>
|
||||
|
||||
@@ -467,6 +471,7 @@
|
||||
WHERE ms.face_id = #{faceId}
|
||||
AND s.device_id = #{deviceId}
|
||||
AND s.type = #{type}
|
||||
AND ms.deleted = 0
|
||||
<choose>
|
||||
<when test='sortStrategy == "LATEST"'>
|
||||
ORDER BY s.create_time DESC
|
||||
@@ -505,22 +510,100 @@
|
||||
|
||||
<delete id="deleteRelationsByFaceIdAndType">
|
||||
DELETE FROM member_source
|
||||
WHERE face_id = #{faceId} AND `type` = #{type}
|
||||
WHERE face_id = #{faceId} AND `type` = #{type} AND deleted = 0
|
||||
</delete>
|
||||
|
||||
<select id="countFreeRelationsByFaceIdAndType" resultType="int">
|
||||
SELECT COUNT(*) FROM member_source
|
||||
WHERE face_id = #{faceId} AND `type` = #{type} AND is_free = 1
|
||||
WHERE face_id = #{faceId} AND `type` = #{type} AND is_free = 1 AND deleted = 0
|
||||
</select>
|
||||
|
||||
<select id="listSourceByFaceRelation" resultType="com.ycwl.basic.model.pc.source.entity.SourceEntity">
|
||||
SELECT s.*
|
||||
FROM member_source ms
|
||||
INNER JOIN source s ON ms.source_id = s.id
|
||||
WHERE ms.face_id = #{faceId}
|
||||
WHERE ms.face_id = #{faceId} AND ms.deleted = 0
|
||||
<if test="type != null">
|
||||
AND ms.type = #{type}
|
||||
</if>
|
||||
ORDER BY s.create_time DESC
|
||||
</select>
|
||||
<select id="listFaceIdsBySourceIds" resultType="com.ycwl.basic.model.pc.source.entity.MemberSourceEntity">
|
||||
SELECT source_id, face_id
|
||||
FROM member_source
|
||||
WHERE deleted = 0 AND source_id IN
|
||||
<foreach collection="list" item="item" open="(" separator="," close=")">
|
||||
#{item}
|
||||
</foreach>
|
||||
GROUP BY source_id
|
||||
</select>
|
||||
<select id="pageByFaceId" resultType="com.ycwl.basic.model.pc.source.resp.SourceRespVO">
|
||||
SELECT so.id, ms.face_id, ms.scenic_id, ms.type, so.thumb_url, so.url, so.video_url,
|
||||
ms.is_free, so.create_time, ms.is_buy, so.device_id
|
||||
FROM member_source ms
|
||||
LEFT JOIN source so ON ms.source_id = so.id
|
||||
WHERE ms.face_id = #{faceId} AND so.id IS NOT NULL AND ms.deleted = 0
|
||||
<if test="type != null">AND ms.type = #{type}</if>
|
||||
<if test="scenicId != null">AND ms.scenic_id = #{scenicId}</if>
|
||||
<if test="isBuy != null">AND ms.is_buy = #{isBuy}</if>
|
||||
ORDER BY so.create_time DESC
|
||||
</select>
|
||||
<update id="softDeleteRelation">
|
||||
UPDATE member_source SET deleted = 1, deleted_at = NOW()
|
||||
WHERE id = #{id} AND deleted = 0
|
||||
</update>
|
||||
<update id="reactivateRelation">
|
||||
UPDATE member_source SET deleted = 0, deleted_at = NULL
|
||||
WHERE id = #{id} AND deleted = 1
|
||||
</update>
|
||||
<select id="pageDeletedByFaceId" resultType="com.ycwl.basic.model.pc.source.resp.SourceRespVO">
|
||||
SELECT so.id, ms.face_id, ms.scenic_id, ms.type, so.thumb_url, so.url, so.video_url,
|
||||
ms.is_free, so.create_time, ms.is_buy, so.device_id, ms.deleted_at
|
||||
FROM member_source ms
|
||||
LEFT JOIN source so ON ms.source_id = so.id
|
||||
WHERE ms.face_id = #{faceId} AND so.id IS NOT NULL AND ms.deleted = 1
|
||||
<if test="type != null">AND ms.type = #{type}</if>
|
||||
<if test="scenicId != null">AND ms.scenic_id = #{scenicId}</if>
|
||||
ORDER BY ms.deleted_at DESC
|
||||
</select>
|
||||
<select id="getMemberSourceById" resultType="com.ycwl.basic.model.pc.source.entity.MemberSourceEntity">
|
||||
SELECT * FROM member_source WHERE id = #{id}
|
||||
</select>
|
||||
<select id="getDeviceSourceStats" resultType="com.ycwl.basic.model.pc.device.resp.DeviceSourceStatsVO">
|
||||
SELECT
|
||||
COUNT(DISTINCT so.id) AS totalShots,
|
||||
COUNT(DISTINCT ms.face_id) AS totalFaces,
|
||||
SUM(CASE WHEN ms.is_buy = 1 THEN 1 ELSE 0 END) AS soldCount,
|
||||
SUM(CASE WHEN ms.is_free = 1 THEN 1 ELSE 0 END) AS freeCount,
|
||||
COUNT(DISTINCT CASE WHEN ms.is_buy = 1 THEN ms.face_id END) AS soldFaceCount
|
||||
FROM source so
|
||||
LEFT JOIN member_source ms ON ms.source_id = so.id AND ms.deleted = 0
|
||||
WHERE so.device_id = #{deviceId}
|
||||
AND so.type = 2
|
||||
AND so.create_time >= #{startTime}
|
||||
AND so.create_time <= #{endTime}
|
||||
</select>
|
||||
<select id="getDeviceSourceTimeline" resultType="com.ycwl.basic.model.pc.device.resp.DeviceSourceTimelineVO">
|
||||
SELECT
|
||||
FROM_UNIXTIME(FLOOR(UNIX_TIMESTAMP(create_time) / 300) * 300) AS time,
|
||||
COUNT(*) AS count
|
||||
FROM source
|
||||
WHERE device_id = #{deviceId}
|
||||
AND type = 2
|
||||
AND create_time >= #{startTime}
|
||||
AND create_time <= #{endTime}
|
||||
GROUP BY FLOOR(UNIX_TIMESTAMP(create_time) / 300)
|
||||
ORDER BY time
|
||||
</select>
|
||||
<select id="getMemberSourceByMemberAndSourceId" resultType="com.ycwl.basic.model.pc.source.entity.MemberSourceEntity">
|
||||
SELECT * FROM member_source WHERE member_id = #{memberId} AND source_id = #{sourceId} AND deleted = 0 LIMIT 1
|
||||
</select>
|
||||
<update id="updateRelationBySourceId">
|
||||
update member_source
|
||||
<set>
|
||||
<if test="isBuy!=null">is_buy = #{isBuy}, </if>
|
||||
<if test="orderId!=null">order_id = #{orderId}, </if>
|
||||
</set>
|
||||
where member_id = #{memberId} and source_id = #{sourceId}
|
||||
</update>
|
||||
</mapper>
|
||||
|
||||
89
src/main/resources/mapper/TaskRenderJobMappingMapper.xml
Normal file
89
src/main/resources/mapper/TaskRenderJobMappingMapper.xml
Normal file
@@ -0,0 +1,89 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.ycwl.basic.mapper.task.TaskRenderJobMappingMapper">
|
||||
|
||||
<resultMap id="BaseResultMap" type="com.ycwl.basic.model.task.entity.TaskRenderJobMappingEntity">
|
||||
<id column="id" property="id"/>
|
||||
<result column="task_id" property="taskId"/>
|
||||
<result column="render_job_id" property="renderJobId"/>
|
||||
<result column="render_status" property="renderStatus"/>
|
||||
<result column="published_count" property="publishedCount"/>
|
||||
<result column="segment_count" property="segmentCount"/>
|
||||
<result column="preview_url" property="previewUrl"/>
|
||||
<result column="mp4_url" property="mp4Url"/>
|
||||
<result column="error_code" property="errorCode"/>
|
||||
<result column="error_message" property="errorMessage"/>
|
||||
<result column="retry_count" property="retryCount"/>
|
||||
<result column="last_check_time" property="lastCheckTime"/>
|
||||
<result column="create_time" property="createTime"/>
|
||||
<result column="update_time" property="updateTime"/>
|
||||
</resultMap>
|
||||
|
||||
<sql id="Base_Column_List">
|
||||
id, task_id, render_job_id, render_status, published_count, segment_count,
|
||||
preview_url, mp4_url, error_code, error_message, retry_count, last_check_time,
|
||||
create_time, update_time
|
||||
</sql>
|
||||
|
||||
<select id="selectByTaskId" resultMap="BaseResultMap">
|
||||
SELECT <include refid="Base_Column_List"/>
|
||||
FROM task_render_job_mapping
|
||||
WHERE task_id = #{taskId}
|
||||
</select>
|
||||
|
||||
<select id="selectByRenderJobId" resultMap="BaseResultMap">
|
||||
SELECT <include refid="Base_Column_List"/>
|
||||
FROM task_render_job_mapping
|
||||
WHERE render_job_id = #{renderJobId}
|
||||
</select>
|
||||
|
||||
<select id="selectPendingForPolling" resultMap="BaseResultMap">
|
||||
SELECT <include refid="Base_Column_List"/>
|
||||
FROM task_render_job_mapping
|
||||
WHERE render_status IN
|
||||
<foreach collection="statuses" item="status" open="(" separator="," close=")">
|
||||
#{status}
|
||||
</foreach>
|
||||
AND (
|
||||
last_check_time IS NULL
|
||||
OR last_check_time < DATE_SUB(NOW(), INTERVAL #{checkIntervalSeconds} SECOND)
|
||||
)
|
||||
AND retry_count < 10
|
||||
ORDER BY last_check_time ASC, create_time ASC
|
||||
LIMIT #{limit}
|
||||
</select>
|
||||
|
||||
<update id="updateRenderStatus">
|
||||
UPDATE task_render_job_mapping
|
||||
SET render_status = #{renderStatus},
|
||||
published_count = #{publishedCount},
|
||||
segment_count = #{segmentCount},
|
||||
<if test="previewUrl != null">
|
||||
preview_url = #{previewUrl},
|
||||
</if>
|
||||
<if test="mp4Url != null">
|
||||
mp4_url = #{mp4Url},
|
||||
</if>
|
||||
last_check_time = #{lastCheckTime},
|
||||
update_time = NOW()
|
||||
WHERE id = #{id}
|
||||
</update>
|
||||
|
||||
<update id="updateToFailed">
|
||||
UPDATE task_render_job_mapping
|
||||
SET render_status = 'FAILED',
|
||||
error_code = #{errorCode},
|
||||
error_message = #{errorMessage},
|
||||
last_check_time = #{lastCheckTime},
|
||||
update_time = NOW()
|
||||
WHERE id = #{id}
|
||||
</update>
|
||||
|
||||
<update id="incrementRetryCount">
|
||||
UPDATE task_render_job_mapping
|
||||
SET retry_count = retry_count + 1,
|
||||
update_time = NOW()
|
||||
WHERE id = #{id}
|
||||
</update>
|
||||
|
||||
</mapper>
|
||||
@@ -7,6 +7,8 @@
|
||||
<id property="id" column="id"/>
|
||||
<result property="videoId" column="video_id"/>
|
||||
<result property="videoUrl" column="video_url"/>
|
||||
<result property="duration" column="duration"/>
|
||||
<result property="taskParams" column="task_params"/>
|
||||
<result property="templateId" column="template_id"/>
|
||||
<result property="templateName" column="template_name"/>
|
||||
<result property="scenicId" column="scenic_id"/>
|
||||
@@ -15,8 +17,12 @@
|
||||
<result property="creatorName" column="creator_name"/>
|
||||
<result property="rating" column="rating"/>
|
||||
<result property="content" column="content"/>
|
||||
<result property="cameraPositionRating" column="camera_position_rating"
|
||||
typeHandler="com.ycwl.basic.handler.MapTypeHandler"/>
|
||||
<result property="problemDeviceIds" column="problem_device_ids"
|
||||
typeHandler="com.ycwl.basic.handler.LongListTypeHandler"/>
|
||||
<result property="problemTags" column="problem_tags"
|
||||
typeHandler="com.ycwl.basic.handler.StringListTypeHandler"/>
|
||||
<result property="source" column="source"/>
|
||||
<result property="sourceId" column="source_id"/>
|
||||
<result property="createTime" column="create_time"/>
|
||||
<result property="updateTime" column="update_time"/>
|
||||
</resultMap>
|
||||
@@ -31,16 +37,22 @@
|
||||
vr.creator,
|
||||
vr.rating,
|
||||
vr.content,
|
||||
vr.camera_position_rating,
|
||||
vr.problem_device_ids,
|
||||
vr.problem_tags,
|
||||
vr.source,
|
||||
vr.source_id,
|
||||
vr.create_time,
|
||||
vr.update_time,
|
||||
v.video_url,
|
||||
v.duration,
|
||||
v.template_id,
|
||||
tk.task_params,
|
||||
t.name AS template_name,
|
||||
s.name AS scenic_name,
|
||||
u.name AS creator_name
|
||||
FROM video_review vr
|
||||
LEFT JOIN video v ON vr.video_id = v.id
|
||||
LEFT JOIN task tk ON v.task_id = tk.id
|
||||
LEFT JOIN template t ON v.template_id = t.id
|
||||
LEFT JOIN scenic s ON vr.scenic_id = s.id
|
||||
LEFT JOIN admin_user u ON vr.creator = u.id
|
||||
@@ -72,6 +84,15 @@
|
||||
<if test="keyword != null and keyword != ''">
|
||||
AND vr.content LIKE CONCAT('%', #{keyword}, '%')
|
||||
</if>
|
||||
<if test="problemDeviceId != null">
|
||||
AND JSON_CONTAINS(vr.problem_device_ids, CAST(#{problemDeviceId} AS CHAR), '$')
|
||||
</if>
|
||||
<if test="problemTag != null and problemTag != ''">
|
||||
AND JSON_CONTAINS(vr.problem_tags, JSON_QUOTE(#{problemTag}), '$')
|
||||
</if>
|
||||
<if test="source != null and source != ''">
|
||||
AND vr.source = #{source}
|
||||
</if>
|
||||
</where>
|
||||
ORDER BY
|
||||
<choose>
|
||||
@@ -140,11 +161,161 @@
|
||||
LIMIT #{limit}
|
||||
</select>
|
||||
|
||||
<!-- 查询所有机位评价数据 -->
|
||||
<select id="selectAllCameraPositionRatings" resultType="java.util.Map">
|
||||
SELECT camera_position_rating
|
||||
<!-- 查询所有问题机位ID列表 -->
|
||||
<select id="selectAllProblemDeviceIds" resultType="java.util.List">
|
||||
SELECT problem_device_ids
|
||||
FROM video_review
|
||||
WHERE camera_position_rating IS NOT NULL AND camera_position_rating != ''
|
||||
WHERE problem_device_ids IS NOT NULL
|
||||
AND problem_device_ids != ''
|
||||
AND problem_device_ids != '[]'
|
||||
</select>
|
||||
|
||||
<!-- 管理后台评价日志结果映射 -->
|
||||
<resultMap id="AdminVideoReviewLogRespMap" type="com.ycwl.basic.model.pc.videoreview.dto.AdminVideoReviewLogRespDTO">
|
||||
<id property="id" column="id"/>
|
||||
<result property="videoId" column="video_id"/>
|
||||
<result property="videoUrl" column="video_url"/>
|
||||
<result property="duration" column="duration"/>
|
||||
<result property="taskParams" column="task_params"/>
|
||||
<result property="templateId" column="template_id"/>
|
||||
<result property="templateName" column="template_name"/>
|
||||
<result property="scenicId" column="scenic_id"/>
|
||||
<result property="scenicName" column="scenic_name"/>
|
||||
<result property="creator" column="creator"/>
|
||||
<result property="creatorName" column="creator_name"/>
|
||||
<result property="creatorAccount" column="creator_account"/>
|
||||
<result property="rating" column="rating"/>
|
||||
<result property="content" column="content"/>
|
||||
<result property="problemDeviceIds" column="problem_device_ids"
|
||||
typeHandler="com.ycwl.basic.handler.LongListTypeHandler"/>
|
||||
<result property="problemDeviceCount" column="problem_device_count"/>
|
||||
<result property="problemTags" column="problem_tags"
|
||||
typeHandler="com.ycwl.basic.handler.StringListTypeHandler"/>
|
||||
<result property="source" column="source"/>
|
||||
<result property="sourceId" column="source_id"/>
|
||||
<result property="createTime" column="create_time"/>
|
||||
<result property="updateTime" column="update_time"/>
|
||||
<result property="operationDuration" column="operation_duration"/>
|
||||
</resultMap>
|
||||
|
||||
<!-- 管理后台分页查询评价日志 -->
|
||||
<select id="selectAdminReviewLogList" parameterType="com.ycwl.basic.model.pc.videoreview.dto.AdminVideoReviewLogReqDTO"
|
||||
resultMap="AdminVideoReviewLogRespMap">
|
||||
SELECT
|
||||
vr.id,
|
||||
vr.video_id,
|
||||
vr.scenic_id,
|
||||
vr.creator,
|
||||
vr.rating,
|
||||
vr.content,
|
||||
vr.problem_device_ids,
|
||||
vr.problem_tags,
|
||||
vr.source,
|
||||
vr.source_id,
|
||||
vr.create_time,
|
||||
vr.update_time,
|
||||
v.video_url,
|
||||
v.duration,
|
||||
v.template_id,
|
||||
tk.task_params,
|
||||
t.name AS template_name,
|
||||
s.name AS scenic_name,
|
||||
u.name AS creator_name,
|
||||
u.account AS creator_account,
|
||||
<!-- 计算问题机位数量 -->
|
||||
CASE
|
||||
WHEN vr.problem_device_ids IS NOT NULL AND vr.problem_device_ids != '' AND vr.problem_device_ids != '[]'
|
||||
THEN JSON_LENGTH(vr.problem_device_ids)
|
||||
ELSE 0
|
||||
END AS problem_device_count,
|
||||
<!-- 计算操作时长(秒) -->
|
||||
TIMESTAMPDIFF(SECOND, vr.create_time, vr.update_time) AS operation_duration
|
||||
FROM video_review vr
|
||||
LEFT JOIN video v ON vr.video_id = v.id
|
||||
LEFT JOIN task tk ON v.task_id = tk.id
|
||||
LEFT JOIN template t ON v.template_id = t.id
|
||||
LEFT JOIN scenic s ON vr.scenic_id = s.id
|
||||
LEFT JOIN admin_user u ON vr.creator = u.id
|
||||
<where>
|
||||
<if test="id != null">
|
||||
AND vr.id = #{id}
|
||||
</if>
|
||||
<if test="videoId != null">
|
||||
AND vr.video_id = #{videoId}
|
||||
</if>
|
||||
<if test="scenicId != null">
|
||||
AND vr.scenic_id = #{scenicId}
|
||||
</if>
|
||||
<if test="creator != null">
|
||||
AND vr.creator = #{creator}
|
||||
</if>
|
||||
<if test="creatorName != null and creatorName != ''">
|
||||
AND u.name LIKE CONCAT('%', #{creatorName}, '%')
|
||||
</if>
|
||||
<if test="rating != null">
|
||||
AND vr.rating = #{rating}
|
||||
</if>
|
||||
<if test="minRating != null">
|
||||
AND vr.rating >= #{minRating}
|
||||
</if>
|
||||
<if test="maxRating != null">
|
||||
AND vr.rating <= #{maxRating}
|
||||
</if>
|
||||
<if test="startTime != null and startTime != ''">
|
||||
AND vr.create_time >= #{startTime}
|
||||
</if>
|
||||
<if test="endTime != null and endTime != ''">
|
||||
AND vr.create_time <= #{endTime}
|
||||
</if>
|
||||
<if test="templateId != null">
|
||||
AND v.template_id = #{templateId}
|
||||
</if>
|
||||
<if test="templateName != null and templateName != ''">
|
||||
AND t.name LIKE CONCAT('%', #{templateName}, '%')
|
||||
</if>
|
||||
<if test="keyword != null and keyword != ''">
|
||||
AND (
|
||||
vr.content LIKE CONCAT('%', #{keyword}, '%')
|
||||
OR s.name LIKE CONCAT('%', #{keyword}, '%')
|
||||
OR t.name LIKE CONCAT('%', #{keyword}, '%')
|
||||
)
|
||||
</if>
|
||||
<if test="hasCameraRating != null">
|
||||
<!-- hasCameraRating 参数已废弃,保留以兼容旧接口 -->
|
||||
</if>
|
||||
<if test="problemDeviceId != null">
|
||||
AND JSON_CONTAINS(vr.problem_device_ids, CAST(#{problemDeviceId} AS CHAR), '$')
|
||||
</if>
|
||||
<if test="problemTag != null and problemTag != ''">
|
||||
AND JSON_CONTAINS(vr.problem_tags, JSON_QUOTE(#{problemTag}), '$')
|
||||
</if>
|
||||
<if test="source != null and source != ''">
|
||||
AND vr.source = #{source}
|
||||
</if>
|
||||
</where>
|
||||
ORDER BY
|
||||
<choose>
|
||||
<when test="orderBy == 'rating'">
|
||||
vr.rating
|
||||
</when>
|
||||
<when test="orderBy == 'update_time'">
|
||||
vr.update_time
|
||||
</when>
|
||||
<when test="orderBy == 'id'">
|
||||
vr.id
|
||||
</when>
|
||||
<otherwise>
|
||||
vr.create_time
|
||||
</otherwise>
|
||||
</choose>
|
||||
<choose>
|
||||
<when test="orderDirection == 'ASC'">
|
||||
ASC
|
||||
</when>
|
||||
<otherwise>
|
||||
DESC
|
||||
</otherwise>
|
||||
</choose>
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
||||
@@ -1,13 +1,35 @@
|
||||
package com.ycwl.basic.face.pipeline.integration;
|
||||
|
||||
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||
import com.ycwl.basic.pipeline.core.Pipeline;
|
||||
import com.ycwl.basic.face.pipeline.enums.FaceMatchingScene;
|
||||
import com.ycwl.basic.face.pipeline.factory.FaceMatchingPipelineFactory;
|
||||
import com.ycwl.basic.face.pipeline.stages.BuildSourceRelationStage;
|
||||
import com.ycwl.basic.face.pipeline.stages.CreateTaskStage;
|
||||
import com.ycwl.basic.face.pipeline.stages.CustomFaceSearchStage;
|
||||
import com.ycwl.basic.face.pipeline.stages.DeleteOldRelationsStage;
|
||||
import com.ycwl.basic.face.pipeline.stages.FaceRecognitionStage;
|
||||
import com.ycwl.basic.face.pipeline.stages.FaceRecoveryStage;
|
||||
import com.ycwl.basic.face.pipeline.stages.FilterByDevicePhotoLimitStage;
|
||||
import com.ycwl.basic.face.pipeline.stages.FilterByTimeRangeStage;
|
||||
import com.ycwl.basic.face.pipeline.stages.GeneratePuzzleStage;
|
||||
import com.ycwl.basic.face.pipeline.stages.HandleVideoRecreationStage;
|
||||
import com.ycwl.basic.face.pipeline.stages.LoadFaceSamplesStage;
|
||||
import com.ycwl.basic.face.pipeline.stages.LoadMatchedSamplesStage;
|
||||
import com.ycwl.basic.face.pipeline.stages.PersistRelationsStage;
|
||||
import com.ycwl.basic.face.pipeline.stages.PrepareContextStage;
|
||||
import com.ycwl.basic.face.pipeline.stages.ProcessBuyStatusStage;
|
||||
import com.ycwl.basic.face.pipeline.stages.ProcessFreeSourceStage;
|
||||
import com.ycwl.basic.face.pipeline.stages.RecordCustomMatchMetricsStage;
|
||||
import com.ycwl.basic.face.pipeline.stages.RecordMetricsStage;
|
||||
import com.ycwl.basic.face.pipeline.stages.SetTaskStatusStage;
|
||||
import com.ycwl.basic.face.pipeline.stages.UpdateFaceResultStage;
|
||||
import com.ycwl.basic.pipeline.core.Pipeline;
|
||||
import com.ycwl.basic.service.pc.helper.ScenicConfigFacade;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
@@ -17,13 +39,55 @@ import static org.junit.jupiter.api.Assertions.*;
|
||||
* Pipeline集成测试
|
||||
* 测试Pipeline的完整流程和Stage协作
|
||||
*/
|
||||
@SpringBootTest
|
||||
@ActiveProfiles("test")
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class FaceMatchingPipelineIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
@InjectMocks
|
||||
private FaceMatchingPipelineFactory pipelineFactory;
|
||||
|
||||
@Mock
|
||||
private PrepareContextStage prepareContextStage;
|
||||
@Mock
|
||||
private RecordMetricsStage recordMetricsStage;
|
||||
@Mock
|
||||
private FaceRecognitionStage faceRecognitionStage;
|
||||
@Mock
|
||||
private FaceRecoveryStage faceRecoveryStage;
|
||||
@Mock
|
||||
private UpdateFaceResultStage updateFaceResultStage;
|
||||
@Mock
|
||||
private BuildSourceRelationStage buildSourceRelationStage;
|
||||
@Mock
|
||||
private ProcessFreeSourceStage processFreeSourceStage;
|
||||
@Mock
|
||||
private ProcessBuyStatusStage processBuyStatusStage;
|
||||
@Mock
|
||||
private HandleVideoRecreationStage handleVideoRecreationStage;
|
||||
@Mock
|
||||
private PersistRelationsStage persistRelationsStage;
|
||||
@Mock
|
||||
private CreateTaskStage createTaskStage;
|
||||
@Mock
|
||||
private SetTaskStatusStage setTaskStatusStage;
|
||||
@Mock
|
||||
private GeneratePuzzleStage generatePuzzleStage;
|
||||
@Mock
|
||||
private RecordCustomMatchMetricsStage recordCustomMatchMetricsStage;
|
||||
@Mock
|
||||
private LoadFaceSamplesStage loadFaceSamplesStage;
|
||||
@Mock
|
||||
private CustomFaceSearchStage customFaceSearchStage;
|
||||
@Mock
|
||||
private LoadMatchedSamplesStage loadMatchedSamplesStage;
|
||||
@Mock
|
||||
private FilterByTimeRangeStage filterByTimeRangeStage;
|
||||
@Mock
|
||||
private FilterByDevicePhotoLimitStage filterByDevicePhotoLimitStage;
|
||||
@Mock
|
||||
private DeleteOldRelationsStage deleteOldRelationsStage;
|
||||
@Mock
|
||||
private ScenicConfigFacade scenicConfigFacade;
|
||||
|
||||
/**
|
||||
* 测试Pipeline工厂能够成功创建Pipeline
|
||||
*/
|
||||
@@ -43,7 +107,7 @@ class FaceMatchingPipelineIntegrationTest {
|
||||
|
||||
// 验证Stage数量符合预期
|
||||
assertEquals(13, autoMatchingNew.getStageCount());
|
||||
assertEquals(13, autoMatchingOld.getStageCount());
|
||||
assertEquals(12, autoMatchingOld.getStageCount());
|
||||
assertEquals(15, customMatching.getStageCount());
|
||||
assertEquals(3, recognitionOnly.getStageCount());
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.ycwl.basic.face.pipeline.stages;
|
||||
|
||||
import com.ycwl.basic.biz.FaceStatusManager;
|
||||
import com.ycwl.basic.enums.FaceCutStatus;
|
||||
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||
import com.ycwl.basic.pipeline.core.StageResult;
|
||||
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
||||
@@ -27,6 +29,9 @@ class CreateTaskStageTest {
|
||||
@Mock
|
||||
private TaskService taskService;
|
||||
|
||||
@Mock
|
||||
private FaceStatusManager faceStatusManager;
|
||||
|
||||
|
||||
@InjectMocks
|
||||
private CreateTaskStage stage;
|
||||
@@ -60,7 +65,7 @@ class CreateTaskStageTest {
|
||||
assertTrue(result.getMessage().contains("自动创建任务成功"));
|
||||
verify(scenicConfigFacade, times(1)).isFaceSelectFirst(10L);
|
||||
verify(taskService, times(1)).autoCreateTaskByFaceId(1L);
|
||||
// verify(taskStatusBiz, never()).setFaceCutStatus(anyLong(), anyInt());
|
||||
verify(faceStatusManager, never()).setFaceCutStatus(anyLong(), any(FaceCutStatus.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -76,7 +81,7 @@ class CreateTaskStageTest {
|
||||
assertTrue(result.isSkipped());
|
||||
assertTrue(result.getMessage().contains("等待用户手动选择"));
|
||||
verify(scenicConfigFacade, times(1)).isFaceSelectFirst(10L);
|
||||
// verify(taskStatusBiz, times(1)).setFaceCutStatus(1L, 2);
|
||||
verify(faceStatusManager, times(1)).setFaceCutStatus(1L, FaceCutStatus.WAITING_USER_SELECT);
|
||||
verify(taskService, never()).autoCreateTaskByFaceId(anyLong());
|
||||
}
|
||||
|
||||
@@ -94,7 +99,7 @@ class CreateTaskStageTest {
|
||||
assertTrue(result.getMessage().contains("任务创建失败"));
|
||||
verify(scenicConfigFacade, times(1)).isFaceSelectFirst(10L);
|
||||
verify(taskService, never()).autoCreateTaskByFaceId(anyLong());
|
||||
// verify(taskStatusBiz, never()).setFaceCutStatus(anyLong(), anyInt());
|
||||
verify(faceStatusManager, never()).setFaceCutStatus(anyLong(), any(FaceCutStatus.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -119,8 +124,8 @@ class CreateTaskStageTest {
|
||||
// Given: 设置状态失败
|
||||
when(scenicConfigFacade.isFaceSelectFirst(10L))
|
||||
.thenReturn(true);
|
||||
// doThrow(new RuntimeException("Status set error"))
|
||||
// .when(taskStatusBiz).setFaceCutStatus(1L, 2);
|
||||
doThrow(new RuntimeException("Status set error"))
|
||||
.when(faceStatusManager).setFaceCutStatus(1L, FaceCutStatus.WAITING_USER_SELECT);
|
||||
|
||||
// When
|
||||
StageResult<FaceMatchingContext> result = stage.execute(context);
|
||||
@@ -128,7 +133,7 @@ class CreateTaskStageTest {
|
||||
// Then
|
||||
assertTrue(result.isDegraded());
|
||||
assertTrue(result.getMessage().contains("任务创建失败"));
|
||||
// verify(taskStatusBiz, times(1)).setFaceCutStatus(1L, 2);
|
||||
verify(faceStatusManager, times(1)).setFaceCutStatus(1L, FaceCutStatus.WAITING_USER_SELECT);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user