Compare commits

...

17 Commits

Author SHA1 Message Date
221f0175e6 feat(goods): 添加视频更新检查功能
Some checks failed
ZhenTu-BE/pipeline/head There was a failure building this commit
- 在 AppGoodsController 中添加视频更新检查接口
- 在 GoodsService 接口中添加 checkVideoUpdate 方法
- 在 GoodsServiceImpl 中实现视频更新检查逻辑
- 在 VideoGoodsDetailVO 中添加 templateId 字段
2025-09-16 11:14:24 +08:00
cce0b45e70 refactor(task): 重构任务参数处理逻辑
- 新增 filterTaskParams 方法,用于过滤模板所需的源数据
- 新增 getTaskParams 方法,用于获取任务参数并进行预处理
- 优化了视频源和图片源的处理逻辑,提高了代码可读性和可维护性
- 重构了任务回调中的源数据处理流程,使用新方法替代原有逻辑
2025-09-15 22:17:38 +08:00
f8c7cc2db6 Merge branch 'refs/heads/xmgl' 2025-09-15 17:17:35 +08:00
4b58c03ad2 feat(pc): 添加景区项目管理功能
- 新增项目管理相关的 Controller、Service、Mapper 及模型类
- 实现项目分页查询、列表查询、详情查询、新增、修改、删除等功能
- 添加项目状态更新和二维码下载功能
- 集成微信小程序二维码生成和存储服务
2025-09-15 17:17:06 +08:00
8ed38bd229 feat(biz): 修改模板未配置最小自动生成功能的默认行为
- 将日志信息从"默认不生成"修改为"默认生成!"
- 设置 minimalPlaceholderFill 为 1,表示开启最小自动生成功能
2025-09-15 16:14:26 +08:00
ccddab37ea feat(service): 实现自定义人脸匹配功能
- 新增 matchCustomFaceId 方法,实现自定义人脸匹配逻辑
- 优化 mergeSearchResults 方法,合并多个搜索结果
- 在 TaskFaceService 接口中添加 applySampleFilters 方法
- 在 TaskFaceServiceImpl 中实现 applySampleFilters 方法
2025-09-15 16:04:41 +08:00
8c37f2bf2f refactor(order): 优化订单相关代码
- 修改了多个模块中的方法名称,使其更加准确地反映功能
- 优化了部分代码逻辑,提高了可读性和维护性
- 增加了获取
2025-09-15 15:07:04 +08:00
89a2e19419 feat(order): 添加倒计时字段
- 在 IsBuyBatchRespVO 类中添加 countdown 属性,初始值为 3600秒(1小时)
- 用于表示订单倒计时时间,增强用户体验
2025-09-15 14:19:24 +08:00
63c2fdfece feat(pc): 更新人脸状态响应逻辑
- 引入 GoodsService接口,用于获取视频任务状态
- 修改 step3 状态判断逻辑,根据视频任务状态动态设置
- 优化显示文本,根据不同情况提供更准确的提示信息
2025-09-15 10:40:58 +08:00
048780071b feat(scenic): 添加景区配置分组功能并优化配置获取接口
- 在 ScenicConfigResp 中添加 groupingEnable 字段,用于表示是否开启分组功能
- 重构 getConfig 方法,使用 ScenicConfigManager 替代 ScenicConfigEntity- 优化配置参数的获取方式,使用 getString、getBoolean等方法替代直接获取字段值
2025-09-15 10:14:10 +08:00
c5f7003077 feat(face): 增加人脸状态查询功能
- 新增 FaceStatusResp 类用于人脸状态响应- 在 AppFaceController 中添加人脸状态查询相关接口
- 在 FaceService 接口中定义相关方法- 实现 FaceServiceImpl 中的人脸状态查询逻辑
- 优化 ContentPageVO 类,增加 group 字段
2025-09-15 10:13:41 +08:00
bf672a8af7 feat(face): 添加低阈值检测功能
- 在 FaceConstant 中添加 FACE_LOW_THRESHOLD_PFX 常量
- 在 SearchFaceRespVo 中添加 lowThreshold 字段
- 在 FaceServiceImpl 中实现记录低阈值检测人脸的逻辑
- 在 TaskFaceServiceImpl 中添加低阈值检测的判断和结果设置
2025-09-13 15:04:06 +08:00
91e68c3272 feat(face): 增加人脸识别计数功能
- 在 FaceConstant 中添加 FACE_RECOGNITION_COUNT_PFX 常量
- 在 FaceServiceImpl 中实现记录人脸识别次数的方法
- 使用 Redis 进行计数,并设置过期时间
2025-09-13 14:42:49 +08:00
96c56bd8c1 feat(TaskFaceServiceImpl): 对搜索人脸结果进行排序
- 在处理搜索人脸结果时,按分数从高到低进行排序
- 这样可以确保分数较高的结果优先被处理
2025-09-13 14:24:14 +08:00
be2750c162 refactor(task): 优化任务服务中视频文件名生成逻辑
- 在生成文件名时加入 faceId 和 templateId,提高文件名的唯一性和可识别性
-修改 hash 值生成逻辑,增加 faceId 以进一步确保文件名的唯一性
2025-09-12 14:19:22 +08:00
b5b2c12a15 Merge branch 'refs/heads/notify-text' 2025-09-12 09:39:51 +08:00
c194c169be refactor(task): 重构下载通知任务逻辑
- 引入 ScenicConfigManager 用于获取景区配置信息
- 根据景区配置动态生成通知标题和内容
- 优化了第二次和第三次通知的处理逻辑
- 移除了优惠券相关查询,简化了代码结构
2025-09-11 22:34:32 +08:00
33 changed files with 1343 additions and 133 deletions

View File

@@ -13,17 +13,14 @@ import com.ycwl.basic.model.pc.couponRecord.resp.CouponRecordQueryResp;
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
import com.ycwl.basic.model.pc.order.entity.OrderEntity;
import com.ycwl.basic.model.pc.order.entity.OrderItemEntity;
import com.ycwl.basic.model.pc.order.req.OrderUpdateReq;
import com.ycwl.basic.model.pc.order.resp.OrderAppRespVO;
import com.ycwl.basic.model.pc.order.resp.OrderItemVO;
import com.ycwl.basic.model.pc.order.resp.OrderRespVO;
import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity;
import com.ycwl.basic.model.pc.scenic.entity.ScenicEntity;
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
import com.ycwl.basic.model.pc.task.entity.TaskEntity;
import com.ycwl.basic.model.pc.template.resp.TemplateRespVO;
import com.ycwl.basic.model.pc.video.entity.VideoEntity;
import com.ycwl.basic.model.pc.video.resp.VideoRespVO;
import com.ycwl.basic.pricing.dto.PriceCalculationRequest;
import com.ycwl.basic.pricing.dto.PriceCalculationResult;
import com.ycwl.basic.pricing.dto.ProductItem;
@@ -38,13 +35,11 @@ import com.ycwl.basic.repository.TemplateRepository;
import com.ycwl.basic.repository.VideoRepository;
import com.ycwl.basic.repository.VideoTaskRepository;
import com.ycwl.basic.service.printer.PrinterService;
import com.ycwl.basic.utils.ApiResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
@@ -121,7 +116,7 @@ public class OrderBiz {
ProductItem vlogProductItem = new ProductItem();
vlogProductItem.setProductType(ProductType.VLOG_VIDEO);
vlogProductItem.setProductId(template.getId().toString());
vlogProductItem.setQuantity(videoTaskRepository.getTaskDeviceCount(video.getTaskId()));
vlogProductItem.setQuantity(videoTaskRepository.getTaskLensNum(video.getTaskId()));
vlogProductItem.setScenicId(scenic.getId().toString());
vlogCalculationRequest.setProducts(Collections.singletonList(vlogProductItem));
vlogCalculationRequest.setFaceId(priceObj.getFaceId());

View File

@@ -15,7 +15,9 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
@@ -91,8 +93,8 @@ public class TemplateBiz {
}
if (minimalPlaceholderFill == null) {
// 未开启
log.info("模板:{},未配置最小自动生成功能,默认生成", templateId);
return false;
log.info("模板:{},未配置最小自动生成功能,默认生成", templateId);
minimalPlaceholderFill = 1;
}
if (minimalPlaceholderFill <= 0) {
return true;
@@ -121,4 +123,39 @@ public class TemplateBiz {
return count >= minimalPlaceholderFill;
}
public Map<String, List<SourceEntity>> filterTaskParams(Long templateId, Map<String, List<SourceEntity>> allTaskParams) {
if (allTaskParams == null || allTaskParams.isEmpty()) {
return Map.of();
}
List<String> templatePlaceholders = templateRepository.getTemplatePlaceholder(templateId);
if (templatePlaceholders == null || templatePlaceholders.isEmpty()) {
log.info("filterTaskParams: templateId:{} has no placeholders", templateId);
return Map.of();
}
Map<String, List<SourceEntity>> filteredParams = new HashMap<>();
for (String placeholder : templatePlaceholders) {
if (placeholder.startsWith("P")) {
// 图片源:占位符格式为 "P{deviceId}"
String imageKey = placeholder;
if (allTaskParams.containsKey(imageKey)) {
filteredParams.put(imageKey, allTaskParams.get(imageKey));
}
} else {
// 视频源:占位符直接对应设备ID
String videoKey = placeholder;
if (allTaskParams.containsKey(videoKey)) {
filteredParams.put(videoKey, allTaskParams.get(videoKey));
}
}
}
log.info("filterTaskParams: templateId:{}, original keys:{}, filtered keys:{}",
templateId, allTaskParams.keySet().size(), filteredParams.keySet().size());
return filteredParams;
}
}

View File

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

View File

@@ -2,8 +2,11 @@ package com.ycwl.basic.controller.mobile;
import com.ycwl.basic.model.jwt.JwtInfo;
import com.ycwl.basic.model.mobile.face.FaceRecognizeResp;
import com.ycwl.basic.model.mobile.face.FaceStatusResp;
import com.ycwl.basic.model.mobile.scenic.content.ContentPageVO;
import com.ycwl.basic.model.pc.face.resp.FaceRespVO;
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
import com.ycwl.basic.model.pc.faceSample.resp.FaceSampleRespVO;
import com.ycwl.basic.service.pc.FaceService;
import com.ycwl.basic.utils.ApiResponse;
import com.ycwl.basic.utils.JwtTokenUtil;
@@ -20,8 +23,7 @@ import java.util.List;
@RestController
@RequestMapping("/api/mobile/face/v1")
// 用户人脸相关接口
public class
AppFaceController {
public class AppFaceController {
@Autowired
private FaceService faceService;
@@ -85,4 +87,25 @@ AppFaceController {
faceService.bindFace(faceId, userId);
return ApiResponse.success("OK");
}
@GetMapping("/{faceId}/status")
public ApiResponse<FaceStatusResp> status(@PathVariable Long faceId) {
return ApiResponse.success(faceService.getFaceStatus(faceId));
}
@GetMapping("/{faceId}/extraCheck")
public ApiResponse<Boolean> hasExtraCheck(@PathVariable Long faceId) {
return ApiResponse.success(faceService.checkHasExtraCheck(faceId));
}
@GetMapping("/{faceId}/queryOtherFace")
public ApiResponse<List<FaceSampleEntity>> queryOtherFace(@PathVariable Long faceId) {
return ApiResponse.success(faceService.getLowMatchedFaceSamples(faceId));
}
@PostMapping("/{faceId}/queryOtherFace")
public ApiResponse<String> queryOtherFace(@PathVariable Long faceId, @RequestBody List<Long> faceIds) {
faceService.matchCustomFaceId(faceId, faceIds);
return ApiResponse.success("OK");
}
}

View File

@@ -99,4 +99,16 @@ public class AppGoodsController {
JwtInfo worker = JwtTokenUtil.getWorker();
return ApiResponse.success(goodsService.getTaskStatusByTemplateId(faceId, templateId));
}
/**
* 检查视频是否可更新
*
* @param videoId 视频ID
* @return 视频更新检查结果
*/
@GetMapping("/video/{videoId}/updateCheck")
public ApiResponse<VideoUpdateCheckVO> checkVideoUpdate(@PathVariable("videoId") Long videoId) {
VideoUpdateCheckVO result = goodsService.checkVideoUpdate(videoId);
return ApiResponse.success(result);
}
}

View File

@@ -4,8 +4,6 @@ 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.order.req.CreateOrderReqVO;
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
import com.ycwl.basic.model.pc.source.req.SourceReqQuery;
import com.ycwl.basic.model.pc.task.entity.TaskEntity;
import com.ycwl.basic.model.pc.video.entity.MemberVideoEntity;
@@ -29,17 +27,13 @@ import com.ycwl.basic.order.dto.OrderV2PageRequest;
import com.ycwl.basic.order.dto.PaymentParamsRequest;
import com.ycwl.basic.order.dto.PaymentParamsResponse;
import com.ycwl.basic.order.dto.PaymentCallbackResponse;
import com.ycwl.basic.utils.JacksonUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.*;
import jakarta.servlet.http.HttpServletRequest;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 移动端订单控制器V2
@@ -111,7 +105,7 @@ public class AppOrderV2Controller {
case VLOG_VIDEO:
List<MemberVideoEntity> videoEntities = videoMapper.listRelationByFaceAndTemplate(face.getId(), Long.valueOf(product.getProductId()));
if (videoEntities != null && !videoEntities.isEmpty()) {
product.setQuantity(videoTaskRepository.getTaskDeviceCount(videoEntities.getFirst().getTaskId()));
product.setQuantity(videoTaskRepository.getTaskLensNum(videoEntities.getFirst().getTaskId()));
} else {
product.setQuantity(1);
}

View File

@@ -7,7 +7,7 @@ import com.ycwl.basic.model.mobile.scenic.ScenicAppVO;
import com.ycwl.basic.model.mobile.scenic.ScenicDeviceCountVO;
import com.ycwl.basic.model.mobile.scenic.ScenicIndexVO;
import com.ycwl.basic.model.mobile.scenic.content.ContentPageVO;
import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity;
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
import com.ycwl.basic.model.pc.scenic.entity.ScenicEntity;
import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery;
import com.ycwl.basic.model.pc.scenic.resp.ScenicConfigResp;
@@ -70,26 +70,27 @@ public class AppScenicController {
@GetMapping("/{id}/config")
@IgnoreToken
public ApiResponse<ScenicConfigResp> getConfig(@PathVariable Long id){
ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(id);
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(id);
ScenicConfigResp resp = new ScenicConfigResp();
resp.setBookRoutine(scenicConfig.getBookRoutine());
resp.setForceFinishTime(scenicConfig.getForceFinishTime());
resp.setTourTime(scenicConfig.getTourTime());
resp.setSampleStoreDay(scenicConfig.getSampleStoreDay());
resp.setFaceStoreDay(scenicConfig.getFaceStoreDay());
resp.setVideoStoreDay(scenicConfig.getVideoStoreDay());
resp.setAllFree(scenicConfig.getAllFree());
resp.setDisableSourceVideo(scenicConfig.getDisableSourceVideo());
resp.setDisableSourceImage(scenicConfig.getDisableSourceImage());
resp.setAntiScreenRecordType(scenicConfig.getAntiScreenRecordType());
resp.setVideoSourceStoreDay(scenicConfig.getVideoSourceStoreDay());
resp.setImageSourceStoreDay(scenicConfig.getImageSourceStoreDay());
resp.setUserSourceExpireDay(scenicConfig.getUserSourceExpireDay());
resp.setBrokerDirectRate(scenicConfig.getBrokerDirectRate());
resp.setVideoSourcePackHint(scenicConfig.getVideoSourcePackHint());
resp.setImageSourcePackHint(scenicConfig.getImageSourcePackHint());
resp.setVoucherEnable(scenicConfig.getVoucherEnable());
resp.setEnableVoucher(scenicConfig.getVoucherEnable()); // compactible
resp.setBookRoutine(scenicConfig.getInteger("book_routine"));
resp.setForceFinishTime(scenicConfig.getInteger("force_finish_time"));
resp.setTourTime(scenicConfig.getInteger("tour_time"));
resp.setSampleStoreDay(scenicConfig.getInteger("sample_store_day"));
resp.setFaceStoreDay(scenicConfig.getInteger("face_store_day"));
resp.setVideoStoreDay(scenicConfig.getInteger("video_store_day"));
resp.setAllFree(scenicConfig.getBoolean("all_free"));
resp.setDisableSourceVideo(scenicConfig.getBoolean("disable_source_video"));
resp.setDisableSourceImage(scenicConfig.getBoolean("disable_source_image"));
resp.setAntiScreenRecordType(scenicConfig.getInteger("anti_screen_record_type"));
resp.setVideoSourceStoreDay(scenicConfig.getInteger("video_source_store_day"));
resp.setImageSourceStoreDay(scenicConfig.getInteger("image_source_store_day"));
resp.setUserSourceExpireDay(scenicConfig.getInteger("user_source_expire_day"));
resp.setBrokerDirectRate(scenicConfig.getBigDecimal("broker_direct_rate"));
resp.setVideoSourcePackHint(scenicConfig.getString("video_source_pack_hint"));
resp.setImageSourcePackHint(scenicConfig.getString("image_source_pack_hint"));
resp.setVoucherEnable(scenicConfig.getBoolean("voucher_enable"));
resp.setEnableVoucher(scenicConfig.getBoolean("voucher_enable")); // compactible
resp.setGroupingEnable(scenicConfig.getBoolean("grouping_enable"));
return ApiResponse.success(resp);
}

View File

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

View File

@@ -0,0 +1,48 @@
package com.ycwl.basic.mapper;
import com.ycwl.basic.model.pc.project.entity.ProjectEntity;
import com.ycwl.basic.model.pc.project.req.ProjectReqQuery;
import com.ycwl.basic.model.pc.project.resp.ProjectRespVO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* 景区项目管理Mapper
*
* @Author: Claude
* @Date: 2025-01-15
*/
@Mapper
public interface ProjectMapper {
/**
* 分页查询项目列表
*/
List<ProjectRespVO> list(ProjectReqQuery projectReqQuery);
/**
* 根据ID查询项目详情
*/
ProjectRespVO getById(Long id);
/**
* 新增项目
*/
int add(ProjectEntity project);
/**
* 根据ID删除项目
*/
int deleteById(Long id);
/**
* 更新项目信息
*/
int update(ProjectEntity project);
/**
* 更新项目状态
*/
int updateStatus(Long id);
}

View File

@@ -0,0 +1,34 @@
package com.ycwl.basic.model.mobile.face;
import lombok.Data;
/**
* 人脸状态响应对象
*
* @author Claude Code
*/
@Data
public class FaceStatusResp {
/**
* 人脸ID
*/
private Long faceId;
private String faceUrl;
/**
* 识别次数,0表示未识别过
*/
private Long recognitionCount;
/**
* 是否触发过低阈值检测
*/
private Boolean hasLowThreshold;
private String displayText;
private boolean step1Status;
// private String step1Text;
private boolean step2Status;
// private String step2Text;
private boolean step3Status;
// private String step3Text;
}

View File

@@ -30,6 +30,7 @@ public class VideoGoodsDetailVO {
private Integer sourceType;
// 商品id goodsType=1时为videoId,goodsType=2时为sourceId
private Long goodsId;
private Long templateId;
// 模版封面图片
private String templateCoverUrl;
// 图片文件存储地址
@@ -50,6 +51,7 @@ public class VideoGoodsDetailVO {
// 是否已购买 0否 1是
private Integer isBuy;
// 镜头数
private Integer devicesNum;
private Integer lensNum;
private Long faceId;
private boolean share = false;

View File

@@ -17,6 +17,7 @@ public class IsBuyBatchRespVO {
private BigDecimal origPrice = BigDecimal.ZERO;
private Integer couponId;
private Integer couponRecordId;
private Integer countdown = 3600;
private BigDecimal couponPrice = BigDecimal.ZERO;
private BigDecimal slashPrice;

View File

@@ -13,6 +13,7 @@ import java.math.BigDecimal;
public class ContentPageVO {
// 内容名称
private String name;
private String group;
// 景区id
private Long scenicId;
// 景区名称

View File

@@ -0,0 +1,49 @@
package com.ycwl.basic.model.pc.project.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;
/**
* 景区项目管理实体类
*
* @Author: Claude
* @Date: 2025-01-15
*/
@Data
@TableName("project")
public class ProjectEntity {
@TableId(type = IdType.AUTO)
private Long id;
/**
* 景区ID
*/
private Long scenicId;
/**
* 项目名称
*/
private String name;
/**
* 最少游玩时间(分钟)
*/
private Integer minPlayTime;
/**
* 最长游玩时间(分钟)
*/
private Integer maxPlayTime;
/**
* 状态,0禁用,1启用
*/
private Integer status;
private Date createAt;
private Date updateAt;
}

View File

@@ -0,0 +1,31 @@
package com.ycwl.basic.model.pc.project.req;
import com.ycwl.basic.model.common.BaseQueryParameterReq;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 项目查询请求参数
*
* @Author: Claude
* @Date: 2025-01-15
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class ProjectReqQuery extends BaseQueryParameterReq {
/**
* 景区ID
*/
private Long scenicId;
/**
* 项目名称(模糊查询)
*/
private String name;
/**
* 状态,0禁用,1启用
*/
private Integer status;
}

View File

@@ -0,0 +1,50 @@
package com.ycwl.basic.model.pc.project.resp;
import lombok.Data;
import java.util.Date;
/**
* 项目响应数据
*
* @Author: Claude
* @Date: 2025-01-15
*/
@Data
public class ProjectRespVO {
private Long id;
/**
* 景区ID
*/
private Long scenicId;
/**
* 景区名称
*/
private String scenicName;
/**
* 项目名称
*/
private String name;
/**
* 最少游玩时间(分钟)
*/
private Integer minPlayTime;
/**
* 最长游玩时间(分钟)
*/
private Integer maxPlayTime;
/**
* 状态,0禁用,1启用
*/
private Integer status;
private Date createAt;
private Date updateAt;
}

View File

@@ -47,4 +47,5 @@ public class ScenicConfigResp {
private String videoSourcePackHint = "";
private Boolean voucherEnable;
private Boolean enableVoucher;
private Boolean groupingEnable;
}

View File

@@ -10,4 +10,5 @@ public class SearchFaceRespVo {
private List<Long> sampleListIds;
private String searchResultJson;
private Float firstMatchRate;
private boolean lowThreshold;
}

View File

@@ -4,6 +4,7 @@ import com.ycwl.basic.mapper.SourceMapper;
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
import com.ycwl.basic.model.pc.device.entity.DeviceEntity;
import com.ycwl.basic.pricing.dto.VoucherInfo;
import com.ycwl.basic.pricing.enums.VoucherDiscountType;
import com.ycwl.basic.pricing.service.IVoucherService;
@@ -13,7 +14,9 @@ import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
@Slf4j
@Component
@@ -26,6 +29,10 @@ public class SourceRepository {
private IVoucherService iVoucherService;
@Autowired
private FaceRepository faceRepository;
@Autowired
private TemplateRepository templateRepository;
@Autowired
private DeviceRepository deviceRepository;
public void addSource(SourceEntity source) {
sourceMapper.add(source);
@@ -86,4 +93,65 @@ public class SourceRepository {
public SourceEntity getSource(Long id) {
return sourceMapper.getEntity(id);
}
public Map<String, List<SourceEntity>> getTaskParams(Long faceId, Long templateId) {
FaceEntity face = faceRepository.getFace(faceId);
if (face == null) {
log.info("getTaskParams: faceId:{} is not exist", faceId);
return Map.of();
}
List<SourceEntity> videoSourceList = sourceMapper.listVideoByScenicFaceRelation(face.getScenicId(), faceId);
Map<String, List<SourceEntity>> sourcesMap = videoSourceList.stream()
.peek(item -> item.setUrl(item.getVideoUrl()))
.filter(item -> item.getDeviceId() != null)
.filter(item -> {
DeviceEntity device = deviceRepository.getDevice(item.getDeviceId());
if (device == null) {
log.info("getTaskParams: deviceId:{} is not exist", item.getDeviceId());
return false;
}
return Integer.valueOf(1).equals(device.getStatus());
})
.collect(Collectors.groupingBy(item -> item.getDeviceId().toString()));
if (sourcesMap.isEmpty()) {
log.info("getTaskParams: 没有视频源,templateId: {}", templateId);
return Map.of();
}
List<String> templatePlaceholder = templateRepository.getTemplatePlaceholder(templateId);
if (templatePlaceholder.stream().distinct().count() == templatePlaceholder.size()) {
sourcesMap.forEach((key, value) -> {
value.removeIf(item -> !value.getFirst().equals(item));
});
}
boolean hasPPlaceholder = templatePlaceholder.stream().anyMatch(placeholder -> placeholder.startsWith("P"));
if (hasPPlaceholder) {
List<SourceEntity> imageSourceList = sourceMapper.listImageSourcesByFaceId(faceId);
if (!imageSourceList.isEmpty()) {
Map<String, List<SourceEntity>> imageSourceMap = imageSourceList.stream()
.peek(item -> item.setUrl(item.getUrl()))
.filter(item -> item.getDeviceId() != null)
.filter(item -> {
DeviceEntity device = deviceRepository.getDevice(item.getDeviceId());
if (device == null) {
return false;
}
return Integer.valueOf(1).equals(device.getStatus());
})
.collect(Collectors.groupingBy(item -> "P" + item.getDeviceId().toString()));
imageSourceMap.forEach((key, value) -> {
sourcesMap.merge(key, value, (existing, replacement) -> {
existing.addAll(replacement);
return existing;
});
});
}
}
return sourcesMap;
}
}

View File

@@ -3,7 +3,6 @@ package com.ycwl.basic.repository;
import cn.hutool.core.date.DateUtil;
import com.ycwl.basic.utils.JacksonUtil;
import com.ycwl.basic.mapper.TaskMapper;
import com.ycwl.basic.mapper.VideoMapper;
import com.ycwl.basic.model.pc.task.entity.TaskEntity;
import com.ycwl.basic.model.pc.task.resp.TaskRespVO;
import org.apache.commons.lang3.StringUtils;
@@ -11,6 +10,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
@@ -68,7 +68,35 @@ public class VideoTaskRepository {
return shotTime;
}
public Integer getTaskDeviceCount(Long taskId) {
public Integer getTaskDeviceNum(Long taskId) {
TaskEntity task = getTaskById(taskId);
if (task == null) {
return 1;
}
Map<String, Object> paramJson = JacksonUtil.parseObject(task.getTaskParams(), Map.class);
if (paramJson == null) {
return 1;
}
List<String> deviceIds = new ArrayList<>();
List<String> templatePlaceholder = templateRepository.getTemplatePlaceholder(task.getTemplateId());
paramJson.entrySet().stream()
.filter(entry -> StringUtils.isNumeric(entry.getKey()))
.forEach(entry -> {
List<Object> jsonArray = JacksonUtil.parseArray(JacksonUtil.toJSONString(entry.getValue()), Object.class);
if (jsonArray != null && !jsonArray.isEmpty()) {
for (Object ignored : jsonArray) {
if (templatePlaceholder.contains(entry.getKey())) {
if (!deviceIds.contains(entry.getKey())) {
deviceIds.add(entry.getKey());
}
}
}
}
});
return deviceIds.size();
}
public Integer getTaskLensNum(Long taskId) {
TaskEntity task = getTaskById(taskId);
if (task == null) {
return 1;

View File

@@ -50,4 +50,11 @@ public interface GoodsService {
List<GoodsUrlVO> sourceGoodsListDownload(GoodsReqQuery query);
Integer sourceGoodsCount(GoodsReqQuery query);
/**
* 检查视频是否可更新
* @param videoId 视频ID
* @return 视频更新检查结果
*/
VideoUpdateCheckVO checkVideoUpdate(Long videoId);
}

View File

@@ -1,10 +1,8 @@
package com.ycwl.basic.service.mobile.impl;
import cn.hutool.core.date.DateUtil;
import cn.hutool.extra.qrcode.QrCodeUtil;
import cn.hutool.http.HttpUtil;
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO;
import com.ycwl.basic.model.pc.scenic.entity.ScenicEntity;
import com.ycwl.basic.utils.JacksonUtil;
import com.ycwl.basic.biz.CouponBiz;
import com.ycwl.basic.biz.OrderBiz;
@@ -33,15 +31,19 @@ import com.ycwl.basic.model.pc.source.resp.SourceRespVO;
import com.ycwl.basic.model.pc.task.entity.TaskEntity;
import com.ycwl.basic.model.pc.template.resp.TemplateRespVO;
import com.ycwl.basic.model.pc.video.entity.MemberVideoEntity;
import com.ycwl.basic.model.pc.video.entity.VideoEntity;
import com.ycwl.basic.model.pc.video.req.VideoReqQuery;
import com.ycwl.basic.model.pc.video.resp.VideoRespVO;
import com.ycwl.basic.repository.DeviceRepository;
import com.ycwl.basic.repository.FaceRepository;
import com.ycwl.basic.repository.OrderRepository;
import com.ycwl.basic.repository.ScenicRepository;
import com.ycwl.basic.repository.VideoRepository;
import com.ycwl.basic.repository.VideoTaskRepository;
import com.ycwl.basic.service.mobile.GoodsService;
import com.ycwl.basic.repository.TemplateRepository;
import com.ycwl.basic.repository.SourceRepository;
import com.ycwl.basic.biz.TemplateBiz;
import com.ycwl.basic.config.VideoUpdateConfig;
import com.ycwl.basic.service.task.TaskService;
import com.ycwl.basic.storage.StorageFactory;
import com.ycwl.basic.storage.adapters.IStorageAdapter;
@@ -54,15 +56,12 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
/**
@@ -81,6 +80,8 @@ public class GoodsServiceImpl implements GoodsService {
@Autowired
private TemplateRepository templateRepository;
@Autowired
private VideoRepository videoRepository;
@Autowired
private VideoTaskRepository videoTaskRepository;
@Autowired
private TaskService taskTaskService;
@@ -96,6 +97,12 @@ public class GoodsServiceImpl implements GoodsService {
private DeviceRepository deviceRepository;
@Autowired
private CouponBiz couponBiz;
@Autowired
private SourceRepository sourceRepository;
@Autowired
private TemplateBiz templateBiz;
@Autowired
private VideoUpdateConfig videoUpdateConfig;
public ApiResponse<List<GoodsPageVO>> goodsList(GoodsReqQuery query) {
Long scenicId = query.getScenicId();
@@ -266,6 +273,7 @@ public class GoodsServiceImpl implements GoodsService {
goodsDetailVO.setGoodsType(0);
goodsDetailVO.setGoodsId(videoRespVO.getId());
goodsDetailVO.setVideoUrl(videoRespVO.getVideoUrl());
goodsDetailVO.setTemplateId(videoRespVO.getTemplateId());
goodsDetailVO.setTemplateCoverUrl(videoRespVO.getTemplateCoverUrl());
goodsDetailVO.setCreateTime(videoRespVO.getCreateTime());
goodsDetailVO.setHeight(videoRespVO.getHeight());
@@ -308,7 +316,8 @@ public class GoodsServiceImpl implements GoodsService {
return ApiResponse.fail("该vlog不存在或已失效");
}
goodsDetailVO.setShotTime(taskTaskService.getTaskShotDate(task.getId()));
goodsDetailVO.setLensNum(videoTaskRepository.getTaskDeviceCount(task.getId()));
goodsDetailVO.setLensNum(videoTaskRepository.getTaskLensNum(task.getId()));
goodsDetailVO.setDevicesNum(videoTaskRepository.getTaskDeviceNum(task.getId()));
CouponRecordQueryResp couponRecord = couponBiz.queryUserCouponRecord(task.getScenicId(), userId, task.getFaceId(), task.getTemplateId().toString());
if (couponRecord != null) {
if (couponRecord.isUsable()) {
@@ -772,4 +781,147 @@ public class GoodsServiceImpl implements GoodsService {
sourceReqQuery.setFaceId(query.getFaceId());
return sourceMapper.countUser(sourceReqQuery);
}
@Override
public VideoUpdateCheckVO checkVideoUpdate(Long videoId) {
VideoUpdateCheckVO result = new VideoUpdateCheckVO();
result.setVideoId(videoId);
if (!videoUpdateConfig.isEnabled()) {
log.info("视频更新检查功能已禁用");
result.setCanUpdate(false);
return result;
}
VideoEntity video = videoRepository.getVideo(videoId);
if (video == null) {
log.error("视频不存在: videoId={}", videoId);
result.setCanUpdate(false);
return result;
}
Long taskId = video.getTaskId();
TaskEntity task = videoTaskRepository.getTaskById(taskId);
if (task == null) {
log.error("关联任务不存在: videoId={}, taskId={}", videoId, taskId);
result.setCanUpdate(false);
return result;
}
result.setTaskId(taskId);
result.setFaceId(task.getFaceId());
result.setTemplateId(task.getTemplateId());
try {
Map<String, Object> originalTaskParams = JacksonUtil.parseObject(task.getTaskParams(), Map.class);
if (originalTaskParams == null) {
log.error("原始任务参数解析失败: taskId={}", taskId);
result.setCanUpdate(false);
return result;
}
int originalSegmentCount = calculateSegmentCount(originalTaskParams);
result.setOriginalSegmentCount(originalSegmentCount);
Map<String, List<SourceEntity>> currentTaskParams = sourceRepository.getTaskParams(task.getFaceId(), task.getTemplateId());
if (currentTaskParams.isEmpty()) {
log.info("当前没有可用的任务参数: faceId={}, templateId={}", task.getFaceId(), task.getTemplateId());
result.setCanUpdate(false);
result.setTotalSegmentCount(0);
result.setNewSegmentCount(0);
return result;
}
Map<String, List<SourceEntity>> filteredTaskParams = templateBiz.filterTaskParams(task.getTemplateId(), currentTaskParams);
int currentSegmentCount = calculateSegmentCount(filteredTaskParams);
result.setTotalSegmentCount(currentSegmentCount);
boolean hasNewSegments = videoUpdateConfig.isDetectChangesAsNew()
? hasNewSegments(originalTaskParams, filteredTaskParams)
: currentSegmentCount > originalSegmentCount;
int newSegmentCount = Math.max(0, currentSegmentCount - originalSegmentCount);
boolean canUpdate = hasNewSegments && newSegmentCount >= videoUpdateConfig.getMinNewSegmentCount();
result.setCanUpdate(canUpdate);
result.setNewSegmentCount(newSegmentCount);
log.info("视频更新检查完成: videoId={}, taskId={}, canUpdate={}, newSegmentCount={}, originalCount={}, currentCount={}",
videoId, taskId, result.isCanUpdate(), newSegmentCount, originalSegmentCount, currentSegmentCount);
} catch (Exception e) {
log.error("检查视频更新失败: videoId={}, taskId={}", videoId, taskId, e);
result.setCanUpdate(false);
}
return result;
}
private int calculateSegmentCount(Map<String, ?> taskParams) {
if (taskParams == null || taskParams.isEmpty()) {
return 0;
}
int totalCount = 0;
for (Map.Entry<String, ?> entry : taskParams.entrySet()) {
Object value = entry.getValue();
if (value instanceof List) {
totalCount += ((List<?>) value).size();
} else if (value instanceof String && StringUtils.isNumeric(entry.getKey())) {
try {
List<?> jsonArray = JacksonUtil.parseArray((String) value, Object.class);
if (jsonArray != null) {
totalCount += jsonArray.size();
}
} catch (Exception e) {
log.warn("解析任务参数失败: key={}, value={}", entry.getKey(), value);
}
}
}
return totalCount;
}
private boolean hasNewSegments(Map<String, ?> originalParams, Map<String, List<SourceEntity>> currentParams) {
if (currentParams == null || currentParams.isEmpty()) {
return false;
}
if (originalParams == null || originalParams.isEmpty()) {
return !currentParams.isEmpty();
}
for (Map.Entry<String, List<SourceEntity>> entry : currentParams.entrySet()) {
String deviceKey = entry.getKey();
List<SourceEntity> currentSources = entry.getValue();
if (!originalParams.containsKey(deviceKey)) {
if (currentSources != null && !currentSources.isEmpty()) {
return true;
}
} else {
Object originalValue = originalParams.get(deviceKey);
int originalCount = 0;
if (originalValue instanceof List) {
originalCount = ((List<?>) originalValue).size();
} else if (originalValue instanceof String) {
try {
List<?> jsonArray = JacksonUtil.parseArray((String) originalValue, Object.class);
if (jsonArray != null) {
originalCount = jsonArray.size();
}
} catch (Exception e) {
log.warn("解析原始参数失败: key={}, value={}", deviceKey, originalValue);
}
}
int currentCount = currentSources != null ? currentSources.size() : 0;
if (currentCount > originalCount) {
return true;
}
}
}
return false;
}
}

View File

@@ -2,10 +2,13 @@ package com.ycwl.basic.service.pc;
import com.github.pagehelper.PageInfo;
import com.ycwl.basic.model.mobile.face.FaceRecognizeResp;
import com.ycwl.basic.model.mobile.face.FaceStatusResp;
import com.ycwl.basic.model.mobile.scenic.content.ContentPageVO;
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
import com.ycwl.basic.model.pc.face.req.FaceReqQuery;
import com.ycwl.basic.model.pc.face.resp.FaceRespVO;
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
import com.ycwl.basic.model.pc.faceSample.resp.FaceSampleRespVO;
import com.ycwl.basic.model.task.resp.SearchFaceRespVo;
import com.ycwl.basic.utils.ApiResponse;
import org.springframework.web.multipart.MultipartFile;
@@ -40,4 +43,12 @@ public interface FaceService {
void bindFace(Long faceId, Long memberId);
String bindWxaCode(Long faceId);
FaceStatusResp getFaceStatus(Long faceId);
Boolean checkHasExtraCheck(Long faceId);
List<FaceSampleEntity> getLowMatchedFaceSamples(Long faceId);
void matchCustomFaceId(Long faceId, List<Long> faceSampleIds);
}

View File

@@ -0,0 +1,47 @@
package com.ycwl.basic.service.pc;
import com.github.pagehelper.PageInfo;
import com.ycwl.basic.model.pc.project.entity.ProjectEntity;
import com.ycwl.basic.model.pc.project.req.ProjectReqQuery;
import com.ycwl.basic.model.pc.project.resp.ProjectRespVO;
import java.util.List;
/**
* 景区项目管理服务接口
*
* @Author: Claude
* @Date: 2025-01-15
*/
public interface ProjectService {
/**
* 分页查询项目列表
*/
PageInfo<ProjectRespVO> pageQuery(ProjectReqQuery projectReqQuery);
/**
* 查询项目列表
*/
List<ProjectRespVO> list(ProjectReqQuery projectReqQuery);
/**
* 根据ID查询项目详情
*/
ProjectRespVO getById(Long id);
/**
* 新增或修改项目
*/
int addOrUpdate(ProjectEntity project);
/**
* 删除项目
*/
int delete(Long id);
/**
* 更新项目状态
*/
int updateStatus(Long id);
}

View File

@@ -9,7 +9,9 @@ import com.ycwl.basic.constant.BaseContextHandler;
import com.ycwl.basic.enums.StatisticEnum;
import com.ycwl.basic.exception.BaseException;
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
import com.ycwl.basic.facebody.entity.SearchFaceResultItem;
import com.ycwl.basic.integration.common.manager.DeviceConfigManager;
import com.ycwl.basic.mapper.FaceSampleMapper;
import com.ycwl.basic.mapper.SourceMapper;
import com.ycwl.basic.mapper.StatisticsMapper;
import com.ycwl.basic.mapper.FaceMapper;
@@ -17,6 +19,8 @@ import com.ycwl.basic.mapper.TemplateMapper;
import com.ycwl.basic.mapper.VideoMapper;
import com.ycwl.basic.mapper.OrderMapper;
import com.ycwl.basic.model.mobile.face.FaceRecognizeResp;
import com.ycwl.basic.model.mobile.face.FaceStatusResp;
import com.ycwl.basic.model.mobile.goods.VideoTaskStatusVO;
import com.ycwl.basic.model.mobile.order.IsBuyRespVO;
import com.ycwl.basic.model.mobile.scenic.content.ContentPageVO;
import com.ycwl.basic.model.mobile.statistic.req.StatisticsRecordAddReq;
@@ -25,6 +29,7 @@ import com.ycwl.basic.model.pc.face.entity.FaceEntity;
import com.ycwl.basic.model.pc.face.req.FaceReqQuery;
import com.ycwl.basic.model.pc.face.resp.FaceRespVO;
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
import com.ycwl.basic.model.pc.faceSample.resp.FaceSampleRespVO;
import com.ycwl.basic.model.pc.mp.MpConfigEntity;
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
@@ -41,6 +46,7 @@ import com.ycwl.basic.repository.FaceRepository;
import com.ycwl.basic.repository.ScenicRepository;
import com.ycwl.basic.repository.VideoRepository;
import com.ycwl.basic.repository.VideoTaskRepository;
import com.ycwl.basic.service.mobile.GoodsService;
import com.ycwl.basic.service.pc.FaceService;
import com.ycwl.basic.service.pc.ScenicService;
import com.ycwl.basic.service.task.TaskFaceService;
@@ -52,7 +58,9 @@ import com.ycwl.basic.storage.utils.StorageUtil;
import com.ycwl.basic.task.VideoPieceGetter;
import com.ycwl.basic.utils.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.util.Strings;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
@@ -61,12 +69,17 @@ import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import java.util.Random;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static com.ycwl.basic.constant.FaceConstant.FACE_LOW_THRESHOLD_PFX;
import static com.ycwl.basic.constant.FaceConstant.FACE_RECOGNITION_COUNT_PFX;
import static com.ycwl.basic.constant.FaceConstant.USER_FACE_DB_NAME;
import static com.ycwl.basic.constant.StorageConstant.USER_FACE;
@@ -110,6 +123,12 @@ public class FaceServiceImpl implements FaceService {
private DeviceRepository deviceRepository;
@Autowired
private OrderMapper orderMapper;
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private FaceSampleMapper faceSampleMapper;
@Autowired
private GoodsService goodsService;
@Override
public ApiResponse<PageInfo<FaceRespVO>> pageQuery(FaceReqQuery faceReqQuery) {
@@ -251,6 +270,9 @@ public class FaceServiceImpl implements FaceService {
log.debug("开始人脸匹配:faceId={}, isNew={}", faceId, isNew);
// 记录识别次数到Redis,设置2天过期时间
recordFaceRecognitionCount(faceId);
try {
// 1. 数据准备:获取人脸信息、景区配置、适配器等
FaceEntity face = faceRepository.getFace(faceId);
@@ -325,6 +347,12 @@ public class FaceServiceImpl implements FaceService {
}
} else {
log.warn("人脸匹配无结果:faceId={}", faceId);
// 检查低阈值检测结果,如果为true则记录该人脸ID到Redis
if (scenicDbSearchResult != null && scenicDbSearchResult.isLowThreshold()) {
recordLowThresholdFace(faceId);
log.debug("触发低阈值检测,记录faceId: {}", faceId);
}
}
return scenicDbSearchResult;
@@ -683,6 +711,8 @@ public class FaceServiceImpl implements FaceService {
sourceImageContent.setContentType(2);
sourceVideoContent.setLockType(-1);
sourceImageContent.setLockType(-1);
sourceVideoContent.setGroup("直出原片");
sourceImageContent.setGroup("直出原片");
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(faceRespVO.getScenicId());
if (!Boolean.TRUE.equals(scenicConfig.getBoolean("disable_source_image"))) {
IsBuyRespVO isBuyRespVO = orderBiz.isBuy(userId, faceRespVO.getScenicId(), 2, faceId);
@@ -800,4 +830,321 @@ public class FaceServiceImpl implements FaceService {
throw new BaseException("生成二维码失败");
}
}
@Override
public FaceStatusResp getFaceStatus(Long faceId) {
if (faceId == null) {
return null;
}
FaceStatusResp statusResp = new FaceStatusResp();
statusResp.setFaceId(faceId);
FaceEntity face = faceRepository.getFace(faceId);
if (face == null) {
statusResp.setStep1Status(false);
statusResp.setDisplayText("请重新录入人脸");
return statusResp;
}
statusResp.setStep1Status(true);
statusResp.setFaceUrl(face.getFaceUrl());
// 查询识别次数
String countKey = FACE_RECOGNITION_COUNT_PFX + faceId;
String countStr = redisTemplate.opsForValue().get(countKey);
long recognitionCount = 0L;
if (countStr != null) {
try {
recognitionCount = Long.parseLong(countStr);
} catch (NumberFormatException e) {
log.warn("识别次数解析失败,faceId={}, count={}", faceId, countStr);
}
}
statusResp.setRecognitionCount(recognitionCount);
// 查询是否触发过低阈值检测
String lowThresholdKey = FACE_LOW_THRESHOLD_PFX + faceId;
Boolean hasLowThreshold = redisTemplate.hasKey(lowThresholdKey);
statusResp.setHasLowThreshold(hasLowThreshold);
log.debug("查询人脸状态:faceId={}, recognitionCount={}, hasLowThreshold={}",
faceId, recognitionCount, hasLowThreshold);
SourceReqQuery sourceReqQuery = new SourceReqQuery();
sourceReqQuery.setMemberId(face.getMemberId());
sourceReqQuery.setFaceId(faceId);
sourceReqQuery.setType(2);
Integer countUser = sourceMapper.countUser(sourceReqQuery);
if (countUser != null && countUser > 0) {
statusResp.setStep2Status(true);
} else {
statusResp.setStep2Status(false);
statusResp.setDisplayText("Hey,快去智能机位打卡吧");
}
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;
}
@Override
public Boolean checkHasExtraCheck(Long faceId) {
FaceEntity face = faceRepository.getFace(faceId);
if (face == null) {
return false;
}
String countKey = FACE_RECOGNITION_COUNT_PFX + faceId;
String countStr = redisTemplate.opsForValue().get(countKey);
long recognitionCount = 0L;
if (countStr != null) {
try {
recognitionCount = Long.parseLong(countStr);
} catch (NumberFormatException e) {
log.warn("识别次数解析失败,faceId={}, count={}", faceId, countStr);
}
}
// 查询是否触发过低阈值检测
String lowThresholdKey = FACE_LOW_THRESHOLD_PFX + faceId;
Boolean hasLowThreshold = redisTemplate.hasKey(lowThresholdKey);
return true;
}
@Override
public List<FaceSampleEntity> getLowMatchedFaceSamples(Long faceId) {
FaceEntity face = faceMapper.get(faceId);
if (face == null) {
return List.of();
}
String matchResult = face.getMatchResult();
if (matchResult == null || Strings.isBlank(matchResult)) {
return List.of();
}
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(face.getScenicId());
if (scenicConfig == null) {
return List.of();
}
Float lowThreshold = scenicConfig.getFloat("face_score_low_threshold", 30.0F);
List<SearchFaceResultItem> resultItems = JacksonUtil.fromJsonToList(matchResult, SearchFaceResultItem.class);
if (resultItems == null || resultItems.isEmpty()) {
return List.of();
}
List<Long> idList = resultItems.stream().filter(item -> item.getScore() > lowThreshold/100F)
.map(SearchFaceResultItem::getExtData).limit(9).map(Long::valueOf).toList();
if (idList.isEmpty()) {
return List.of();
}
List<FaceSampleEntity> sampleEntities = faceSampleMapper.listByIds(idList);
return sampleEntities;
}
@Override
public void matchCustomFaceId(Long faceId, List<Long> faceSampleIds) {
// 参数验证
if (faceId == null) {
throw new IllegalArgumentException("faceId 不能为空");
}
if (faceSampleIds == null || faceSampleIds.isEmpty()) {
throw new IllegalArgumentException("faceSampleIds 不能为空");
}
log.debug("开始自定义人脸匹配:faceId={}, faceSampleIds={}", faceId, faceSampleIds);
try {
// 1. 获取基础数据
FaceEntity face = faceRepository.getFace(faceId);
if (face == null) {
log.warn("人脸不存在,faceId: {}", faceId);
throw new BaseException("人脸不存在");
}
List<FaceSampleEntity> faceSamples = faceSampleMapper.listByIds(faceSampleIds);
if (faceSamples.isEmpty()) {
log.warn("未找到指定的人脸样本,faceSampleIds: {}", faceSampleIds);
throw new BaseException("未找到指定的人脸样本");
}
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(face.getScenicId());
IFaceBodyAdapter faceBodyAdapter = scenicService.getScenicFaceBodyAdapter(face.getScenicId());
if (faceBodyAdapter == null) {
log.error("无法获取人脸识别适配器,scenicId: {}", face.getScenicId());
throw new BaseException("人脸识别服务不可用,请稍后再试");
}
// 2. 对每个faceSample进行人脸搜索
List<SearchFaceRespVo> searchResults = new ArrayList<>();
for (FaceSampleEntity faceSample : faceSamples) {
try {
SearchFaceRespVo result = faceService.searchFace(faceBodyAdapter,
String.valueOf(face.getScenicId()),
faceSample.getFaceUrl(),
"自定义人脸匹配");
if (result != null) {
searchResults.add(result);
}
} catch (Exception e) {
log.warn("人脸样本搜索失败,faceSampleId={}, faceUrl={}",
faceSample.getId(), faceSample.getFaceUrl(), e);
// 继续处理其他样本,不中断整个流程
}
}
if (searchResults.isEmpty()) {
log.warn("所有人脸样本搜索都失败,faceId={}, faceSampleIds={}", faceId, faceSampleIds);
throw new BaseException("人脸识别失败,请重试");
}
// 3. 整合多个搜索结果
SearchFaceRespVo mergedResult = mergeSearchResults(searchResults);
// 4. 应用后置筛选逻辑
if (mergedResult.getSampleListIds() != null && !mergedResult.getSampleListIds().isEmpty()) {
List<FaceSampleEntity> allFaceSampleList = faceSampleMapper.listByIds(mergedResult.getSampleListIds());
List<Long> filteredSampleIds = faceService.applySampleFilters(mergedResult.getSampleListIds(), allFaceSampleList, scenicConfig);
mergedResult.setSampleListIds(filteredSampleIds);
log.debug("应用后置筛选:原始样本数={}, 筛选后样本数={}", allFaceSampleList.size(), filteredSampleIds.size());
}
// 5. 更新人脸实体结果
updateFaceEntityResult(face, mergedResult, faceId);
// 6. 执行后续业务逻辑
List<Long> sampleListIds = mergedResult.getSampleListIds();
if (sampleListIds != null && !sampleListIds.isEmpty()) {
try {
List<MemberSourceEntity> memberSourceEntityList = processMemberSources(sampleListIds, face);
if (!memberSourceEntityList.isEmpty()) {
List<Long> freeSourceIds = processFreeSourceLogic(memberSourceEntityList, scenicConfig, false);
processBuyStatus(memberSourceEntityList, freeSourceIds, face.getMemberId(),
face.getScenicId(), faceId);
handleVideoRecreation(scenicConfig, memberSourceEntityList, faceId,
face.getMemberId(), sampleListIds, false);
sourceMapper.addRelations(memberSourceEntityList);
taskTaskService.autoCreateTaskByFaceId(face.getId());
log.info("自定义人脸匹配完成:faceId={}, 匹配样本数={}, 关联源文件数={}, 免费数={}",
faceId, sampleListIds.size(), memberSourceEntityList.size(), freeSourceIds.size());
}
} catch (Exception e) {
log.error("处理源文件关联失败,faceId={}", faceId, e);
// 源文件关联失败不影响主流程
}
} else {
log.warn("自定义人脸匹配无结果:faceId={}, faceSampleIds={}", faceId, faceSampleIds);
}
} catch (BaseException e) {
throw e;
} catch (Exception e) {
log.error("自定义人脸匹配处理异常,faceId={}, faceSampleIds={}", faceId, faceSampleIds, e);
throw new BaseException("自定义人脸匹配处理失败,请稍后重试");
}
}
/**
* 合并多个搜索结果
*/
private SearchFaceRespVo mergeSearchResults(List<SearchFaceRespVo> searchResults) {
SearchFaceRespVo mergedResult = new SearchFaceRespVo();
// 收集所有样本ID并去重
Set<Long> allSampleIds = new LinkedHashSet<>();
List<String> allSearchJsons = new ArrayList<>();
float maxScore = 0f;
float maxFirstMatchRate = 0f;
boolean hasLowThreshold = false;
for (SearchFaceRespVo result : searchResults) {
if (result.getSampleListIds() != null) {
allSampleIds.addAll(result.getSampleListIds());
}
if (result.getSearchResultJson() != null) {
allSearchJsons.add(result.getSearchResultJson());
}
if (result.getScore() > maxScore) {
maxScore = result.getScore();
}
if (result.getFirstMatchRate() > maxFirstMatchRate) {
maxFirstMatchRate = result.getFirstMatchRate();
}
if (result.isLowThreshold()) {
hasLowThreshold = true;
}
}
mergedResult.setSampleListIds(new ArrayList<>(allSampleIds));
mergedResult.setSearchResultJson(String.join("|", allSearchJsons));
mergedResult.setScore(maxScore);
mergedResult.setFirstMatchRate(maxFirstMatchRate);
mergedResult.setLowThreshold(hasLowThreshold);
log.debug("合并搜索结果完成,总样本数: {}", allSampleIds.size());
return mergedResult;
}
/**
* 记录人脸识别次数到Redis
*
* @param faceId 人脸ID
*/
private void recordFaceRecognitionCount(Long faceId) {
if (faceId == null) {
return;
}
try {
String redisKey = FACE_RECOGNITION_COUNT_PFX + faceId;
// 使用Redis原子操作INCR增加计数
Long count = redisTemplate.opsForValue().increment(redisKey);
// 设置2天过期时间(48小时)
redisTemplate.expire(redisKey, 2, TimeUnit.DAYS);
log.debug("人脸识别计数更新:faceId={}, count={}", faceId, count);
} catch (Exception e) {
// 计数失败不应影响主要业务逻辑,只记录错误日志
log.error("记录人脸识别次数失败:faceId={}", faceId, e);
}
}
/**
* 记录低阈值检测的人脸ID到Redis
*
* @param faceId 人脸ID
*/
private void recordLowThresholdFace(Long faceId) {
if (faceId == null) {
return;
}
try {
String redisKey = FACE_LOW_THRESHOLD_PFX + faceId;
// 设置标记,表示该人脸ID触发了低阈值检测
redisTemplate.opsForValue().set(redisKey, "1", 2, TimeUnit.DAYS);
log.debug("记录低阈值检测人脸:faceId={}", faceId);
} catch (Exception e) {
// 记录失败不应影响主要业务逻辑,只记录错误日志
log.error("记录低阈值检测人脸失败:faceId={}", faceId, e);
}
}
}

View File

@@ -11,7 +11,6 @@ import com.ycwl.basic.pricing.dto.PriceCalculationResult;
import com.ycwl.basic.pricing.dto.ProductItem;
import com.ycwl.basic.pricing.service.ICouponService;
import com.ycwl.basic.pricing.service.IVoucherService;
import com.ycwl.basic.utils.JacksonUtil;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.ycwl.basic.biz.CouponBiz;
@@ -64,7 +63,6 @@ import com.ycwl.basic.service.task.impl.TaskTaskServiceImpl;
import com.ycwl.basic.utils.ApiResponse;
import com.ycwl.basic.utils.SnowFlakeUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Scope;
@@ -342,7 +340,7 @@ public class OrderServiceImpl implements OrderService {
goods.setGoodsType(0);
goods.setTemplateCoverUrl(template.getCoverUrl());
goods.setCreateTime(videoTaskRepository.getTaskShotDate(task.getId()));
goods.setParts(videoTaskRepository.getTaskDeviceCount(task.getId()));
goods.setParts(videoTaskRepository.getTaskLensNum(task.getId()));
goodsList.add(goods);
item.setShootingTime(videoTaskRepository.getTaskShotDate(videoMapperById.getTaskId()));
item.setVideoUrl(videoMapperById.getVideoUrl());

View File

@@ -0,0 +1,104 @@
package com.ycwl.basic.service.pc.impl;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.ycwl.basic.mapper.ProjectMapper;
import com.ycwl.basic.model.pc.project.entity.ProjectEntity;
import com.ycwl.basic.model.pc.project.req.ProjectReqQuery;
import com.ycwl.basic.model.pc.project.resp.ProjectRespVO;
import com.ycwl.basic.repository.ScenicRepository;
import com.ycwl.basic.service.pc.ProjectService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* 景区项目管理服务实现
*
* @Author: Claude
* @Date: 2025-01-15
*/
@Service
public class ProjectServiceImpl implements ProjectService {
@Autowired
private ProjectMapper projectMapper;
@Autowired
private ScenicRepository scenicRepository;
@Override
public PageInfo<ProjectRespVO> pageQuery(ProjectReqQuery projectReqQuery) {
PageHelper.startPage(projectReqQuery.getPageNum(), projectReqQuery.getPageSize());
List<ProjectRespVO> list = projectMapper.list(projectReqQuery);
// 批量获取景区名称
List<Long> scenicIds = list.stream()
.map(ProjectRespVO::getScenicId)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
Map<Long, String> scenicNames = scenicRepository.batchGetScenicNames(scenicIds);
// 设置景区名称
list.forEach(item -> {
if (item.getScenicId() != null) {
item.setScenicName(scenicNames.get(item.getScenicId()));
}
});
PageInfo<ProjectRespVO> pageInfo = new PageInfo(list);
return pageInfo;
}
@Override
public List<ProjectRespVO> list(ProjectReqQuery projectReqQuery) {
List<ProjectRespVO> list = projectMapper.list(projectReqQuery);
// 批量获取景区名称
List<Long> scenicIds = list.stream()
.map(ProjectRespVO::getScenicId)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
Map<Long, String> scenicNames = scenicRepository.batchGetScenicNames(scenicIds);
// 设置景区名称
list.forEach(item -> {
if (item.getScenicId() != null) {
item.setScenicName(scenicNames.get(item.getScenicId()));
}
});
return list;
}
@Override
public ProjectRespVO getById(Long id) {
return projectMapper.getById(id);
}
@Override
public int addOrUpdate(ProjectEntity project) {
Long id = project.getId();
if (id == null) {
return projectMapper.add(project);
} else {
return projectMapper.update(project);
}
}
@Override
public int delete(Long id) {
return projectMapper.deleteById(id);
}
@Override
public int updateStatus(Long id) {
return projectMapper.updateStatus(id);
}
}

View File

@@ -1,9 +1,13 @@
package com.ycwl.basic.service.task;
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
import com.ycwl.basic.model.task.resp.SearchFaceRespVo;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
public interface TaskFaceService {
SearchFaceRespVo searchFace(Long faceId);
@@ -13,4 +17,15 @@ public interface TaskFaceService {
boolean deleteFaceSample(Long scenicId, String dbName, String entityId);
boolean assureFaceDb(IFaceBodyAdapter faceBodyAdapter, String dbName);
/**
* 应用样本筛选逻辑
* @param acceptedSampleIds 已接受的样本ID列表
* @param allFaceSampleList 所有人脸样本实体列表
* @param scenicConfig 景区配置管理器
* @return 筛选后的样本ID列表
*/
List<Long> applySampleFilters(List<Long> acceptedSampleIds,
List<FaceSampleEntity> allFaceSampleList,
ScenicConfigManager scenicConfig);
}

View File

@@ -171,6 +171,7 @@ public class TaskFaceServiceImpl implements TaskFaceService {
request.setLimit(200);
FaceDetectLog logEntity = FaceDetectLog.quickCreate(reason, request);
float threshold = 0;
float lowThreshold = 100;
ScenicConfigManager scenicConfig = null;
if (StringUtils.isNumeric(dbName)) {
scenicConfig = scenicRepository.getScenicConfigManager(Long.valueOf(dbName));
@@ -178,6 +179,9 @@ public class TaskFaceServiceImpl implements TaskFaceService {
if (scenicConfig.getFloat("face_score_threshold") != null) {
threshold = scenicConfig.getFloat("face_score_threshold") / 100F;
}
if (scenicConfig.getFloat("face_score_low_threshold") != null) {
lowThreshold = scenicConfig.getFloat("face_score_low_threshold") / 100F;
}
}
} else if (StringUtils.isNumeric(dbName.replace(USER_FACE_DB_NAME, ""))) {
Long scenicId = Long.valueOf(dbName.replace(USER_FACE_DB_NAME, ""));
@@ -186,6 +190,9 @@ public class TaskFaceServiceImpl implements TaskFaceService {
if (scenicConfig.getFloat("face_score_threshold") != null) {
threshold = scenicConfig.getFloat("face_score_threshold") / 100F;
}
if (scenicConfig.getFloat("face_score_low_threshold") != null) {
lowThreshold = scenicConfig.getFloat("face_score_low_threshold") / 100F;
}
}
}
final float _threshold = threshold;
@@ -206,11 +213,16 @@ public class TaskFaceServiceImpl implements TaskFaceService {
return respVo;
}
acceptFaceSampleIds = records.stream()
.sorted(Comparator.comparing(SearchFaceResultItem::getScore).reversed())
.filter(record -> record.getScore() > _threshold)
.map(SearchFaceResultItem::getExtData)
.filter(StringUtils::isNumeric)
.map(Long::valueOf)
.collect(Collectors.toList());
float _lowThreshold = lowThreshold;
boolean isLowThreshold = records.stream()
.anyMatch(record -> record.getScore() > _lowThreshold);
respVo.setLowThreshold(isLowThreshold);
allFaceSampleIds = records.stream()
.map(SearchFaceResultItem::getExtData)
.filter(StringUtils::isNumeric)
@@ -340,7 +352,8 @@ public class TaskFaceServiceImpl implements TaskFaceService {
* @param scenicConfig 景区配置,包含各种筛选策略的参数
* @return 筛选后的样本ID列表
*/
private List<Long> applySampleFilters(List<Long> acceptedSampleIds,
@Override
public List<Long> applySampleFilters(List<Long> acceptedSampleIds,
List<FaceSampleEntity> allFaceSampleList,
ScenicConfigManager scenicConfig) {
if (acceptedSampleIds == null || acceptedSampleIds.isEmpty()) {

View File

@@ -5,6 +5,7 @@ import cn.hutool.crypto.digest.MD5;
import com.ycwl.basic.integration.common.manager.DeviceConfigManager;
import com.ycwl.basic.integration.common.manager.RenderWorkerConfigManager;
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
import com.ycwl.basic.repository.SourceRepository;
import com.ycwl.basic.utils.JacksonUtil;
import com.ycwl.basic.biz.OrderBiz;
import com.ycwl.basic.biz.TaskStatusBiz;
@@ -126,6 +127,8 @@ public class TaskTaskServiceImpl implements TaskService {
private VideoReUploader videoReUploader;
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private SourceRepository sourceRepository;
private RenderWorkerEntity getWorker(@NonNull WorkerAuthReqVo req) {
String accessKey = req.getAccessKey();
@@ -348,58 +351,15 @@ public class TaskTaskServiceImpl implements TaskService {
}
}
List<SourceEntity> videoSourceList = sourceMapper.listVideoByScenicFaceRelation(face.getScenicId(), faceId);
Map<String, List<SourceEntity>> sourcesMap = videoSourceList.stream()
.peek(item -> item.setUrl(item.getVideoUrl()))
.filter(item -> item.getDeviceId() != null) // 添加对 deviceId 为 null 的检查
.filter(item -> {
DeviceEntity device = deviceRepository.getDevice(item.getDeviceId());
if (device == null) {
log.info("task callback: deviceId:{} is not exist", item.getDeviceId());
return false;
}
return Integer.valueOf(1).equals(device.getStatus());
})
.collect(Collectors.groupingBy(item -> item.getDeviceId().toString()));
if (sourcesMap.isEmpty()) {
// 主动禁止没有视频源视频生成
log.info("task callback: 没有视频源,templateId: {}", templateId);
Map<String, List<SourceEntity>> allTaskParams = sourceRepository.getTaskParams(faceId, templateId);
if (allTaskParams.isEmpty()) {
return;
}
List<String> templatePlaceholder = templateRepository.getTemplatePlaceholder(templateId);
if (templatePlaceholder.stream().distinct().count() == templatePlaceholder.size()) {
sourcesMap.forEach((key, value) -> {
// 每个value只保留第一个
value.removeIf(item -> !value.getFirst().equals(item));
});
}
// 处理以P开头的templatePlaceHolder,添加type=2的source
boolean hasPPlaceholder = templatePlaceholder.stream().anyMatch(placeholder -> placeholder.startsWith("P"));
if (hasPPlaceholder) {
List<SourceEntity> imageSourceList = sourceMapper.listImageSourcesByFaceId(faceId);
if (!imageSourceList.isEmpty()) {
// 将图片source按设备ID分组并添加到sourcesMap中
Map<String, List<SourceEntity>> imageSourceMap = imageSourceList.stream()
.peek(item -> item.setUrl(item.getUrl())) // 图片使用url字段
.filter(item -> item.getDeviceId() != null)
.filter(item -> {
DeviceEntity device = deviceRepository.getDevice(item.getDeviceId());
if (device == null) {
return false;
}
return Integer.valueOf(1).equals(device.getStatus());
})
.collect(Collectors.groupingBy(item -> Strings.concat("P", item.getDeviceId().toString())));
// 合并到现有的sourcesMap中
imageSourceMap.forEach((key, value) -> {
sourcesMap.merge(key, value, (existing, replacement) -> {
existing.addAll(replacement);
return existing;
});
});
}
Map<String, List<SourceEntity>> sourcesMap = templateBiz.filterTaskParams(templateId, allTaskParams);
if (sourcesMap.isEmpty()) {
log.info("task callback: 筛选后无有效源数据,templateId: {}", templateId);
return;
}
TaskReqQuery taskReqQuery = new TaskReqQuery();
taskReqQuery.setFaceId(faceId);
@@ -522,8 +482,8 @@ public class TaskTaskServiceImpl implements TaskService {
}
ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(task.getScenicId());
IStorageAdapter adapter = scenicService.getScenicTmpStorageAdapter(task.getScenicId());
String hash = MD5.create().digestHex(task.getTaskParams());
String filename = StorageUtil.joinPath(StorageConstant.VLOG_PATH, hash + "_" + task.getScenicId() + ".mp4");
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());
@@ -591,8 +551,8 @@ public class TaskTaskServiceImpl implements TaskService {
} catch (Exception e) {
adapter = scenicService.getScenicStorageAdapter(task.getScenicId());
}
String hash = MD5.create().digestHex(task.getTaskParams());
String filename = StorageUtil.joinPath(StorageConstant.VLOG_PATH, hash + "_" + task.getScenicId() + ".mp4");
String hash = MD5.create().digestHex(task.getTaskParams() + task.getFaceId().toString());
String filename = StorageUtil.joinPath(StorageConstant.VLOG_PATH, task.getTemplateId().toString() + "_" + hash + "_" + task.getScenicId() + ".mp4");
// 生成
String url = adapter.getUrl(filename);
TaskEntity updateTask = new TaskEntity();
@@ -648,7 +608,16 @@ public class TaskTaskServiceImpl implements TaskService {
return;
}
ScenicEntity scenic = scenicRepository.getScenic(item.getScenicId());
String title = "您在【" + scenic.getName() + "】的专属影像";
ScenicConfigManager configManager = scenicRepository.getScenicConfigManager(item.getScenicId());
String configTitle = configManager.getString("first_notification_title");
String configContent = configManager.getString("first_notification_content");
if (StringUtils.isBlank(configTitle) || StringUtils.isBlank(configContent)) {
log.info("景区[{}]未配置第一次通知内容,跳过发送通知", scenic.getName());
return;
}
String title = configTitle.replace("【景区】", scenic.getName());
String page = "pages/videoSynthesis/index?type=1&scenicId=" + item.getScenicId() + "&faceId=" + item.getFaceId();
/**
* 视频名称 {{thing1.DATA}}
@@ -664,7 +633,7 @@ public class TaskTaskServiceImpl implements TaskService {
timeMap2.put("value", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm"));
dataParam.put("time4", timeMap2);
Map<String, String> remarkMap = new HashMap<>();
remarkMap.put("value", "请及时查看视频,48小时后系统删除");
remarkMap.put("value", configContent);
dataParam.put("thing3", remarkMap);
params.put("data", dataParam);
params.put("page", page);

View File

@@ -18,6 +18,7 @@ import com.ycwl.basic.notify.entity.NotifyContent;
import com.ycwl.basic.notify.enums.NotifyType;
import com.ycwl.basic.repository.ScenicRepository;
import com.ycwl.basic.repository.TemplateRepository;
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
@@ -65,7 +66,16 @@ public class DownloadNotificationTasker {
}
log.info("发送模板消息");
ScenicEntity scenic = scenicRepository.getScenic(item.getScenicId());
String title = "您在【" + scenic.getName() + "】的专属影像";
ScenicConfigManager configManager = scenicRepository.getScenicConfigManager(item.getScenicId());
String configTitle = configManager.getString("second_notification_title");
String configContent = configManager.getString("second_notification_content");
if (StringUtils.isBlank(configTitle) || StringUtils.isBlank(configContent)) {
log.info("景区[{}]未配置第二次通知内容,跳过发送通知", scenic.getName());
return;
}
String title = configTitle.replace("【景区】", scenic.getName());
String page = "pages/videoSynthesis/index?type=2&scenicId=" + item.getScenicId() + "&faceId=" + item.getFaceId();
/**
* 景区 {{thing1.DATA}}
@@ -77,17 +87,7 @@ public class DownloadNotificationTasker {
videoMap.put("value", title);
dataParam.put("thing1", videoMap);
Map<String, String> remarkMap = new HashMap<>();
// 查询是否有优惠券可用,如果有,则显示优惠券配置的内容
CouponQueryReq query = new CouponQueryReq();
query.setScenicId(item.getScenicId());
query.setType(2);
query.setStatus(1);
List<CouponRespVO> coupons = couponMapper.selectByQuery(query);
if (coupons.isEmpty() || StringUtils.isBlank(coupons.getFirst().getBroadcast())) {
remarkMap.put("value", "系统删除前,请下载好您的旅行视频");
} else {
remarkMap.put("value", coupons.getFirst().getBroadcast());
}
remarkMap.put("value", configContent);
dataParam.put("thing3", remarkMap);
params.put("data", dataParam);
params.put("page", page);
@@ -121,7 +121,16 @@ public class DownloadNotificationTasker {
}
log.info("发送模板消息");
ScenicEntity scenic = scenicRepository.getScenic(item.getScenicId());
String title = "您在【" + scenic.getName() + "】的专属影像";
ScenicConfigManager configManager = scenicRepository.getScenicConfigManager(item.getScenicId());
String configTitle = configManager.getString("third_notification_title");
String configContent = configManager.getString("third_notification_content");
if (StringUtils.isBlank(configTitle) || StringUtils.isBlank(configContent)) {
log.info("景区[{}]未配置第三次通知内容,跳过发送通知", scenic.getName());
return;
}
String title = configTitle.replace("【景区】", scenic.getName());
String page = "pages/videoSynthesis/index?type=3&scenicId=" + item.getScenicId() + "&faceId=" + item.getFaceId();
/**
* 影像名称 {{thing1.DATA}}
@@ -138,17 +147,7 @@ public class DownloadNotificationTasker {
dateMap.put("value", DateUtil.format(expireDate, "yyyy-MM-dd HH:mm"));
dataParam.put("time2", dateMap);
Map<String, String> remarkMap = new HashMap<>();
// 查询是否有优惠券可用,如果有,则显示优惠券配置的内容
CouponQueryReq query = new CouponQueryReq();
query.setScenicId(item.getScenicId());
query.setType(3);
query.setStatus(1);
List<CouponRespVO> coupons = couponMapper.selectByQuery(query);
if (coupons.isEmpty() || StringUtils.isBlank(coupons.getFirst().getBroadcast())) {
remarkMap.put("value", "视频即将删除,花点小钱买下回忆");
} else {
remarkMap.put("value", coupons.getFirst().getBroadcast());
}
remarkMap.put("value", configContent);
dataParam.put("thing3", remarkMap);
params.put("data", dataParam);
params.put("page", page);
@@ -201,8 +200,17 @@ public class DownloadNotificationTasker {
log.info("模板消息为空");
return;
}
ScenicConfigManager configManager = scenicRepository.getScenicConfigManager(scenicId);
String configTitle = configManager.getString("second_notification_title");
String configContent = configManager.getString("second_notification_content");
if (StringUtils.isBlank(configTitle) || StringUtils.isBlank(configContent)) {
log.info("景区[{}]未配置第一次通知内容,跳过发送通知", scenic.getName());
return;
}
log.info("发送模板消息");
String title = "您在【" + scenic.getName() + "】的专属影像";
String title = configTitle.replace("【景区】", scenic.getName());
String page = "pages/videoSynthesis/index?type=2&scenicId=" + item.getScenicId() + "&faceId=" + item.getFaceId();
/**
* 景区 {{thing1.DATA}}
@@ -214,7 +222,7 @@ public class DownloadNotificationTasker {
videoMap.put("value", title);
dataParam.put("thing1", videoMap);
Map<String, String> remarkMap = new HashMap<>();
remarkMap.put("value", "请及时购买并下载好您的旅行视频");
remarkMap.put("value", configContent);
dataParam.put("thing3", remarkMap);
params.put("data", dataParam);
params.put("page", page);

View File

@@ -0,0 +1,59 @@
<?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.ProjectMapper">
<insert id="add">
insert into project(scenic_id, `name`, min_play_time, max_play_time, status, create_at, update_at)
values (#{scenicId}, #{name}, #{minPlayTime}, #{maxPlayTime}, #{status}, now(), now())
</insert>
<update id="update">
update project set
`name` = #{name},
min_play_time = #{minPlayTime},
max_play_time = #{maxPlayTime},
status = #{status},
update_at = now()
where id = #{id}
</update>
<update id="updateStatus">
update project
set status = (CASE
status
WHEN 1 THEN
0
WHEN 0 THEN
1
ELSE 1
END),
update_at = now()
where id = #{id}
</update>
<delete id="deleteById">
delete from project where id = #{id}
</delete>
<select id="list" resultType="com.ycwl.basic.model.pc.project.resp.ProjectRespVO">
select p.id, p.scenic_id, p.`name`, p.min_play_time, p.max_play_time, p.status, p.create_at, p.update_at
from project p
<where>
<if test="scenicId != null">
and p.scenic_id = #{scenicId}
</if>
<if test="name != null and name != ''">
and p.`name` like concat('%', #{name}, '%')
</if>
<if test="status != null">
and p.`status` = #{status}
</if>
</where>
order by p.create_at desc
</select>
<select id="getById" resultType="com.ycwl.basic.model.pc.project.resp.ProjectRespVO">
select p.id, p.scenic_id, p.`name`, p.min_play_time, p.max_play_time, p.status, p.create_at, p.update_at
from project p
where p.id = #{id}
</select>
</mapper>

View File

@@ -114,7 +114,7 @@
order by sort
</select>
<select id="listFor" resultType="com.ycwl.basic.model.mobile.scenic.content.ContentPageVO">
select t.id templateId, t.scenic_id, t.`name`, pid, t.cover_url templateCoverUrl,
select t.id templateId, t.scenic_id, t.`group`, t.`name`, pid, t.cover_url templateCoverUrl,
0 as sourceType, sort,
t.create_time, t.price
from template t