You've already forked FrameTour-BE
Compare commits
41 Commits
24bbb63bf7
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 5cc32ddf61 | |||
| 07987835ec | |||
| 0a3f4119d7 | |||
| 51c7de2474 | |||
| 773d7f2254 | |||
| af131131ed | |||
| 3f6f1508c5 | |||
| dbee1d9709 | |||
| 83d1096fdb | |||
| 82925d203c | |||
| 3b11ddef6a | |||
| 6e7b4729a8 | |||
| 917cb37ccf | |||
| 7c0a3a63bb | |||
| 478467e124 | |||
| d5befd75e1 | |||
| b2c55c9feb | |||
| fef616c837 | |||
| a5fe00052d | |||
| 349b702fc3 | |||
| 9f5a61247b | |||
| 9321422e56 | |||
| 1834fe3ddd | |||
| fa8f92d38b | |||
| df33e7929f | |||
| 554f55a7c1 | |||
| f71149fd06 | |||
| e8eb8d816b | |||
| 576d87d113 | |||
| a2378053a8 | |||
| c92ea20575 | |||
| bb71cf9458 | |||
| 7749faf807 | |||
| c42b055d5f | |||
| fe3bda28b4 | |||
| 66775ea48b | |||
| 125fadd6c5 | |||
| 1f4a16f0e6 | |||
| e9916d6aca | |||
| b71452b3ed | |||
| 4a82ee6c4d |
7
pom.xml
7
pom.xml
@@ -273,6 +273,13 @@
|
|||||||
<version>5.0.0</version>
|
<version>5.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 智谱AI SDK -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>ai.z.openapi</groupId>
|
||||||
|
<artifactId>zai-sdk</artifactId>
|
||||||
|
<version>0.1.3</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- Spring Kafka -->
|
<!-- Spring Kafka -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.kafka</groupId>
|
<groupId>org.springframework.kafka</groupId>
|
||||||
|
|||||||
@@ -147,6 +147,21 @@ public class OrderBiz {
|
|||||||
priceObj.setSlashPrice(priceCalculationResult.getOriginalAmount());
|
priceObj.setSlashPrice(priceCalculationResult.getOriginalAmount());
|
||||||
priceObj.setFaceId(goodsId);
|
priceObj.setFaceId(goodsId);
|
||||||
break;
|
break;
|
||||||
|
case 13:
|
||||||
|
PriceCalculationRequest aiCamCalculationRequest = new PriceCalculationRequest();
|
||||||
|
ProductItem aiCamProductItem = new ProductItem();
|
||||||
|
aiCamProductItem.setProductType(ProductType.AI_CAM_PHOTO_SET);
|
||||||
|
aiCamProductItem.setProductId(scenicId.toString());
|
||||||
|
aiCamProductItem.setPurchaseCount(1);
|
||||||
|
aiCamProductItem.setScenicId(scenicId.toString());
|
||||||
|
aiCamCalculationRequest.setProducts(Collections.singletonList(aiCamProductItem));
|
||||||
|
aiCamCalculationRequest.setPreviewOnly(true); // 仅查询价格,不实际使用优惠
|
||||||
|
PriceCalculationResult aiCamPriceCalculationResult = iPriceCalculationService.calculatePrice(aiCamCalculationRequest);
|
||||||
|
priceObj.setPrice(aiCamPriceCalculationResult.getFinalAmount());
|
||||||
|
priceObj.setSlashPrice(aiCamPriceCalculationResult.getOriginalAmount());
|
||||||
|
priceObj.setFaceId(goodsId);
|
||||||
|
priceObj.setScenicId(scenicId);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return priceObj;
|
return priceObj;
|
||||||
}
|
}
|
||||||
@@ -215,11 +230,15 @@ public class OrderBiz {
|
|||||||
orderRepository.updateOrder(orderId, orderUpdate);
|
orderRepository.updateOrder(orderId, orderUpdate);
|
||||||
orderItems.forEach(item -> {
|
orderItems.forEach(item -> {
|
||||||
switch (item.getGoodsType()) {
|
switch (item.getGoodsType()) {
|
||||||
|
case -1: // vlog视频模板
|
||||||
|
videoRepository.setUserIsBuyTemplate(order.getMemberId(), item.getGoodsId(), order.getId(), order.getFaceId());
|
||||||
|
break;
|
||||||
case 0: // vlog视频
|
case 0: // vlog视频
|
||||||
videoRepository.setUserIsBuyItem(order.getMemberId(), item.getGoodsId(), order.getId());
|
videoRepository.setUserIsBuyItem(order.getMemberId(), item.getGoodsId(), order.getId());
|
||||||
break;
|
break;
|
||||||
case 1: // 视频原素材
|
case 1: // 视频原素材
|
||||||
case 2: // 照片原素材
|
case 2: // 照片原素材
|
||||||
|
case 13: // AI微单
|
||||||
sourceRepository.setUserIsBuyItem(order.getMemberId(), item.getGoodsType(), item.getGoodsId(), order.getId());
|
sourceRepository.setUserIsBuyItem(order.getMemberId(), item.getGoodsType(), item.getGoodsId(), order.getId());
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
@@ -295,10 +314,14 @@ public class OrderBiz {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查用户是否购买了指定商品
|
* 检查用户是否购买了指定商品,并额外校验订单的faceId是否匹配
|
||||||
* 提供给PriceBiz使用,避免循环调用
|
* @param userId 用户ID
|
||||||
|
* @param faceId 人脸ID
|
||||||
|
* @param goodsType 商品类型
|
||||||
|
* @param goodsId 商品ID
|
||||||
|
* @return 是否已购买且faceId匹配
|
||||||
*/
|
*/
|
||||||
public boolean checkUserBuyItem(Long userId, int goodsType, Long goodsId) {
|
public boolean checkUserBuyFaceItem(Long userId, Long faceId, int goodsType, Long goodsId) {
|
||||||
return orderRepository.checkUserBuyItem(userId, goodsType, goodsId);
|
return orderRepository.checkUserBuyFaceItem(userId, faceId, goodsType, goodsId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,14 @@ public class PriceBiz {
|
|||||||
goodsList.add(new GoodsListRespVO(2L, "照片集", 2));
|
goodsList.add(new GoodsListRespVO(2L, "照片集", 2));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 拼图
|
||||||
|
puzzleTemplateMapper.list(scenicId, null, 1).forEach(puzzleTemplate -> {
|
||||||
|
GoodsListRespVO goods = new GoodsListRespVO();
|
||||||
|
goods.setGoodsId(puzzleTemplate.getId());
|
||||||
|
goods.setGoodsName(puzzleTemplate.getName());
|
||||||
|
goods.setGoodsType(5);
|
||||||
|
goodsList.add(goods);
|
||||||
|
});
|
||||||
return goodsList;
|
return goodsList;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,6 +133,7 @@ public class PriceBiz {
|
|||||||
|
|
||||||
case "PHOTO_LOG":
|
case "PHOTO_LOG":
|
||||||
// 从 template 表查询pLog模板
|
// 从 template 表查询pLog模板
|
||||||
|
goodsList.add(new SimpleGoodsRespVO(scenicId, "pLog图<景区打包>", productType));
|
||||||
List<PuzzleTemplateEntity> puzzleList = puzzleTemplateMapper.list(scenicId, null, null);
|
List<PuzzleTemplateEntity> puzzleList = puzzleTemplateMapper.list(scenicId, null, null);
|
||||||
puzzleList.stream()
|
puzzleList.stream()
|
||||||
.map(template -> new SimpleGoodsRespVO(template.getId(), template.getName(), productType))
|
.map(template -> new SimpleGoodsRespVO(template.getId(), template.getName(), productType))
|
||||||
@@ -272,7 +281,7 @@ public class PriceBiz {
|
|||||||
allContentsPurchased = false;
|
allContentsPurchased = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
boolean hasPurchasedTemplate = orderBiz.checkUserBuyItem(userId, -1, videoEntities.getFirst().getVideoId());
|
boolean hasPurchasedTemplate = orderBiz.checkUserBuyFaceItem(userId, faceId, -1, videoEntities.getFirst().getVideoId());
|
||||||
if (!hasPurchasedTemplate) {
|
if (!hasPurchasedTemplate) {
|
||||||
allContentsPurchased = false;
|
allContentsPurchased = false;
|
||||||
break;
|
break;
|
||||||
@@ -284,7 +293,7 @@ public class PriceBiz {
|
|||||||
if (scenicConfig != null) {
|
if (scenicConfig != null) {
|
||||||
// 检查录像集
|
// 检查录像集
|
||||||
if (!Boolean.TRUE.equals(scenicConfig.getDisableSourceVideo())) {
|
if (!Boolean.TRUE.equals(scenicConfig.getDisableSourceVideo())) {
|
||||||
boolean hasPurchasedRecording = orderBiz.checkUserBuyItem(userId, 1, faceId);
|
boolean hasPurchasedRecording = orderBiz.checkUserBuyFaceItem(userId, faceId, 1, faceId);
|
||||||
if (!hasPurchasedRecording) {
|
if (!hasPurchasedRecording) {
|
||||||
allContentsPurchased = false;
|
allContentsPurchased = false;
|
||||||
}
|
}
|
||||||
@@ -292,7 +301,7 @@ public class PriceBiz {
|
|||||||
|
|
||||||
// 检查照片集
|
// 检查照片集
|
||||||
if (allContentsPurchased && !Boolean.TRUE.equals(scenicConfig.getDisableSourceImage())) {
|
if (allContentsPurchased && !Boolean.TRUE.equals(scenicConfig.getDisableSourceImage())) {
|
||||||
boolean hasPurchasedPhoto = orderBiz.checkUserBuyItem(userId, 2, faceId);
|
boolean hasPurchasedPhoto = orderBiz.checkUserBuyFaceItem(userId, faceId, 2, faceId);
|
||||||
if (!hasPurchasedPhoto) {
|
if (!hasPurchasedPhoto) {
|
||||||
allContentsPurchased = false;
|
allContentsPurchased = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,12 @@ public enum SourceType {
|
|||||||
/**
|
/**
|
||||||
* 图片类型
|
* 图片类型
|
||||||
*/
|
*/
|
||||||
IMAGE(2, "图片");
|
IMAGE(2, "图片"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI微单类型
|
||||||
|
*/
|
||||||
|
AI_CAM(3, "AI微单");
|
||||||
|
|
||||||
private final int code;
|
private final int code;
|
||||||
private final String description;
|
private final String description;
|
||||||
@@ -68,4 +73,14 @@ public enum SourceType {
|
|||||||
public static boolean isImage(Integer code) {
|
public static boolean isImage(Integer code) {
|
||||||
return code != null && code == IMAGE.code;
|
return code != null && code == IMAGE.code;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断给定的代码是否为AI微单类型
|
||||||
|
*
|
||||||
|
* @param code 类型代码
|
||||||
|
* @return true-是AI微单,false-不是AI微单
|
||||||
|
*/
|
||||||
|
public static boolean isAiCam(Integer code) {
|
||||||
|
return code != null && code == AI_CAM.code;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package com.ycwl.basic.controller.mobile;
|
||||||
|
|
||||||
|
import com.ycwl.basic.model.jwt.JwtInfo;
|
||||||
|
import com.ycwl.basic.model.mobile.face.FaceRecognizeResp;
|
||||||
|
import com.ycwl.basic.model.mobile.goods.GoodsDetailVO;
|
||||||
|
import com.ycwl.basic.service.mobile.AppAiCamService;
|
||||||
|
import com.ycwl.basic.utils.ApiResponse;
|
||||||
|
import com.ycwl.basic.utils.JwtTokenUtil;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI相机相关接口
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/mobile/ai_cam/v1")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AppAiCamController {
|
||||||
|
|
||||||
|
private final AppAiCamService appAiCamService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据faceId获取AI相机识别到的商品列表
|
||||||
|
* @param faceId 人脸ID
|
||||||
|
* @return 商品详情列表
|
||||||
|
*/
|
||||||
|
@GetMapping("/{faceId}/content")
|
||||||
|
public ApiResponse<List<GoodsDetailVO>> getAiCamGoods(@PathVariable Long faceId) {
|
||||||
|
try {
|
||||||
|
List<GoodsDetailVO> goods = appAiCamService.getAiCamGoodsByFaceId(faceId);
|
||||||
|
return ApiResponse.success(goods);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("获取AI相机商品失败: faceId={}", faceId, e);
|
||||||
|
return ApiResponse.fail("获取商品列表失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量添加会员与source的关联关系
|
||||||
|
* @param faceId 人脸ID
|
||||||
|
* @param sourceIds source ID列表
|
||||||
|
* @return 添加结果
|
||||||
|
*/
|
||||||
|
@PostMapping("/{faceId}/relations")
|
||||||
|
public ApiResponse<String> addMemberSourceRelations(
|
||||||
|
@PathVariable Long faceId,
|
||||||
|
@RequestBody List<Long> sourceIds
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
int count = appAiCamService.addMemberSourceRelations(faceId, sourceIds);
|
||||||
|
return ApiResponse.success("成功添加" + count + "条关联记录");
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
log.warn("添加关联失败: faceId={}, error={}", faceId, e.getMessage());
|
||||||
|
return ApiResponse.fail(e.getMessage());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("添加关联失败: faceId={}", faceId, e);
|
||||||
|
return ApiResponse.fail("添加关联失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用人脸样本创建或获取Face记录
|
||||||
|
* @param faceSampleId 人脸样本ID
|
||||||
|
* @return 人脸识别响应
|
||||||
|
*/
|
||||||
|
@GetMapping("/useSample/{faceSampleId}")
|
||||||
|
public ApiResponse<FaceRecognizeResp> useSample(@PathVariable Long faceSampleId) {
|
||||||
|
try {
|
||||||
|
JwtInfo worker = JwtTokenUtil.getWorker();
|
||||||
|
FaceRecognizeResp resp = appAiCamService.useSample(worker.getUserId(), faceSampleId);
|
||||||
|
return ApiResponse.success(resp);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("使用人脸样本失败: faceSampleId={}", faceSampleId, e);
|
||||||
|
return ApiResponse.fail("使用人脸样本失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package com.ycwl.basic.controller.mobile;
|
||||||
|
|
||||||
|
import com.ycwl.basic.model.jwt.JwtInfo;
|
||||||
|
import com.ycwl.basic.model.mobile.chat.*;
|
||||||
|
import com.ycwl.basic.service.mobile.FaceChatService;
|
||||||
|
import com.ycwl.basic.utils.ApiResponse;
|
||||||
|
import com.ycwl.basic.utils.JwtTokenUtil;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
|
||||||
|
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小程序人脸智能聊天接口。
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/mobile/chat/v1")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AppChatController {
|
||||||
|
|
||||||
|
private final FaceChatService faceChatService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取或创建会话(同一人脸只保留一条)。
|
||||||
|
*/
|
||||||
|
@PostMapping("/faces/{faceId}/conversation")
|
||||||
|
public ApiResponse<ChatConversationVO> createConversation(@PathVariable Long faceId) {
|
||||||
|
JwtInfo worker = JwtTokenUtil.getWorker();
|
||||||
|
ChatConversationVO vo = faceChatService.getOrCreateConversation(faceId, worker.getUserId());
|
||||||
|
return ApiResponse.success(vo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步发送消息,适用于短回复或前端自行轮询。
|
||||||
|
*/
|
||||||
|
@PostMapping("/conversations/{conversationId}/messages")
|
||||||
|
public ApiResponse<ChatSendMessageResp> sendMessage(@PathVariable Long conversationId,
|
||||||
|
@RequestBody ChatSendMessageReq req) {
|
||||||
|
JwtInfo worker = JwtTokenUtil.getWorker();
|
||||||
|
ChatSendMessageResp resp = faceChatService.sendMessage(conversationId, worker.getUserId(),
|
||||||
|
req.getContent(), req.getTraceId());
|
||||||
|
return ApiResponse.success(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流式返回,使用 HTTP chunked。小程序侧用 wx.request 的 onChunkReceived 消费。
|
||||||
|
*/
|
||||||
|
@PostMapping(value = "/conversations/{conversationId}/messages/stream", produces = "text/plain;charset=UTF-8")
|
||||||
|
public ResponseBodyEmitter streamMessage(@PathVariable Long conversationId,
|
||||||
|
@RequestBody ChatSendMessageReq req) {
|
||||||
|
JwtInfo worker = JwtTokenUtil.getWorker();
|
||||||
|
ResponseBodyEmitter emitter = new ResponseBodyEmitter(30_000L);
|
||||||
|
CompletableFuture.runAsync(() -> {
|
||||||
|
try {
|
||||||
|
faceChatService.sendMessageStream(
|
||||||
|
conversationId,
|
||||||
|
worker.getUserId(),
|
||||||
|
req.getContent(),
|
||||||
|
req.getTraceId(),
|
||||||
|
chunk -> {
|
||||||
|
try {
|
||||||
|
emitter.send(chunk, new MediaType("text", "plain", java.nio.charset.StandardCharsets.UTF_8));
|
||||||
|
} catch (Exception e) {
|
||||||
|
emitter.completeWithError(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
emitter.complete();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("streamMessage error", e);
|
||||||
|
emitter.completeWithError(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return emitter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询历史消息,cursor 为最后一条 seq,limit 为条数。
|
||||||
|
*/
|
||||||
|
@GetMapping("/conversations/{conversationId}/messages")
|
||||||
|
public ApiResponse<ChatMessagePageResp> listMessages(@PathVariable Long conversationId,
|
||||||
|
@RequestParam(value = "cursor", required = false) Integer cursor,
|
||||||
|
@RequestParam(value = "limit", required = false) Integer limit) {
|
||||||
|
JwtInfo worker = JwtTokenUtil.getWorker();
|
||||||
|
ChatMessagePageResp resp = faceChatService.listMessages(conversationId, cursor, limit, worker.getUserId());
|
||||||
|
return ApiResponse.success(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/conversations/{conversationId}/close")
|
||||||
|
public ApiResponse<String> closeConversation(@PathVariable Long conversationId) {
|
||||||
|
JwtInfo worker = JwtTokenUtil.getWorker();
|
||||||
|
faceChatService.closeConversation(conversationId, worker.getUserId());
|
||||||
|
return ApiResponse.success("OK");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ import com.ycwl.basic.order.dto.PaymentParamsResponse;
|
|||||||
import com.ycwl.basic.order.dto.PaymentCallbackResponse;
|
import com.ycwl.basic.order.dto.PaymentCallbackResponse;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
@@ -55,6 +56,7 @@ public class AppOrderV2Controller {
|
|||||||
private final VideoTaskRepository videoTaskRepository;
|
private final VideoTaskRepository videoTaskRepository;
|
||||||
private final TemplateRepository templateRepository;
|
private final TemplateRepository templateRepository;
|
||||||
private final VideoRepository videoRepository;
|
private final VideoRepository videoRepository;
|
||||||
|
private final RedisTemplate<String, Object> redisTemplate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 移动端价格计算
|
* 移动端价格计算
|
||||||
@@ -86,7 +88,7 @@ public class AppOrderV2Controller {
|
|||||||
TaskEntity task = videoTaskRepository.getTaskById(video.getTaskId());
|
TaskEntity task = videoTaskRepository.getTaskById(video.getTaskId());
|
||||||
request.setFaceId(task.getFaceId());
|
request.setFaceId(task.getFaceId());
|
||||||
}
|
}
|
||||||
case RECORDING_SET, PHOTO_SET -> request.setFaceId(Long.valueOf(productItem.getProductId()));
|
case RECORDING_SET, PHOTO_SET, AI_CAM_PHOTO_SET -> request.setFaceId(Long.valueOf(productItem.getProductId()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,6 +121,14 @@ public class AppOrderV2Controller {
|
|||||||
Integer count = sourceMapper.countUser(sourceReqQuery);
|
Integer count = sourceMapper.countUser(sourceReqQuery);
|
||||||
product.setQuantity(count);
|
product.setQuantity(count);
|
||||||
break;
|
break;
|
||||||
|
case AI_CAM_PHOTO_SET:
|
||||||
|
SourceReqQuery aiPhotoSetReqQuery = new SourceReqQuery();
|
||||||
|
aiPhotoSetReqQuery.setMemberId(currentUserId);
|
||||||
|
aiPhotoSetReqQuery.setType(13);
|
||||||
|
aiPhotoSetReqQuery.setFaceId(face.getId());
|
||||||
|
Integer _count = sourceMapper.countUser(aiPhotoSetReqQuery);
|
||||||
|
product.setQuantity(_count);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
log.warn("未知的商品类型,跳过重复购买检查: productType={}", product.getProductType());
|
log.warn("未知的商品类型,跳过重复购买检查: productType={}", product.getProductType());
|
||||||
break;
|
break;
|
||||||
@@ -341,4 +351,9 @@ public class AppOrderV2Controller {
|
|||||||
return "FAIL";
|
return "FAIL";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/downloadable/{orderId}")
|
||||||
|
public ApiResponse<Boolean> getDownloadableOrder(@PathVariable("orderId") Long orderId) {
|
||||||
|
return ApiResponse.success(!redisTemplate.hasKey("order_content_not_downloadable_" + orderId));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -205,26 +205,31 @@ public class AppPuzzleController {
|
|||||||
|
|
||||||
// 设置模板ID
|
// 设置模板ID
|
||||||
vo.setTemplateId(record.getTemplateId());
|
vo.setTemplateId(record.getTemplateId());
|
||||||
IsBuyRespVO isBuyRespVO = orderBiz.isBuy(face.getScenicId(), face.getMemberId(), face.getId(), 5, record.getTemplateId());
|
IsBuyRespVO isBuyScenic = orderBiz.isBuy(face.getScenicId(), face.getMemberId(), face.getId(), 5, face.getScenicId());
|
||||||
if (isBuyRespVO.isBuy()) {
|
if (isBuyScenic.isBuy()) {
|
||||||
vo.setIsBuy(1);
|
vo.setIsBuy(1);
|
||||||
} else {
|
} else {
|
||||||
vo.setIsBuy(0);
|
IsBuyRespVO isBuyRespVO = orderBiz.isBuy(face.getScenicId(), face.getMemberId(), face.getId(), 5, record.getTemplateId());
|
||||||
PriceCalculationRequest calculationRequest = new PriceCalculationRequest();
|
if (isBuyRespVO.isBuy()) {
|
||||||
ProductItem productItem = new ProductItem();
|
vo.setIsBuy(1);
|
||||||
productItem.setProductType(ProductType.PHOTO_LOG);
|
|
||||||
productItem.setProductId(record.getTemplateId().toString());
|
|
||||||
productItem.setPurchaseCount(1);
|
|
||||||
productItem.setScenicId(face.getScenicId().toString());
|
|
||||||
calculationRequest.setProducts(Collections.singletonList(productItem));
|
|
||||||
calculationRequest.setUserId(face.getMemberId());
|
|
||||||
calculationRequest.setFaceId(record.getFaceId());
|
|
||||||
calculationRequest.setPreviewOnly(true); // 仅查询价格,不实际使用优惠
|
|
||||||
PriceCalculationResult calculationResult = iPriceCalculationService.calculatePrice(calculationRequest);
|
|
||||||
if (calculationResult.getFinalAmount().compareTo(BigDecimal.ZERO) > 0) {
|
|
||||||
vo.setFreeCount(0);
|
|
||||||
} else {
|
} else {
|
||||||
vo.setFreeCount(1);
|
vo.setIsBuy(0);
|
||||||
|
PriceCalculationRequest calculationRequest = new PriceCalculationRequest();
|
||||||
|
ProductItem productItem = new ProductItem();
|
||||||
|
productItem.setProductType(ProductType.PHOTO_LOG);
|
||||||
|
productItem.setProductId(record.getTemplateId().toString());
|
||||||
|
productItem.setPurchaseCount(1);
|
||||||
|
productItem.setScenicId(face.getScenicId().toString());
|
||||||
|
calculationRequest.setProducts(Collections.singletonList(productItem));
|
||||||
|
calculationRequest.setUserId(face.getMemberId());
|
||||||
|
calculationRequest.setFaceId(record.getFaceId());
|
||||||
|
calculationRequest.setPreviewOnly(true); // 仅查询价格,不实际使用优惠
|
||||||
|
PriceCalculationResult calculationResult = iPriceCalculationService.calculatePrice(calculationRequest);
|
||||||
|
if (calculationResult.getFinalAmount().compareTo(BigDecimal.ZERO) > 0) {
|
||||||
|
vo.setFreeCount(0);
|
||||||
|
} else {
|
||||||
|
vo.setFreeCount(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return vo;
|
return vo;
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ public class AppTaskController {
|
|||||||
|
|
||||||
@PostMapping("/submit")
|
@PostMapping("/submit")
|
||||||
public ApiResponse<String> submitVideoTask(@RequestBody VideoTaskReq videoTaskReq) {
|
public ApiResponse<String> submitVideoTask(@RequestBody VideoTaskReq videoTaskReq) {
|
||||||
taskService.createTaskByFaceIdAndTemplateId(videoTaskReq.getFaceId(),videoTaskReq.getTemplateId(),0);
|
taskService.createTaskByFaceIdAndTemplateId(videoTaskReq.getFaceId(),videoTaskReq.getTemplateId(),false);
|
||||||
return ApiResponse.success("成功");
|
return ApiResponse.success("成功");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ package com.ycwl.basic.controller.printer;
|
|||||||
import cn.hutool.core.date.DateUtil;
|
import cn.hutool.core.date.DateUtil;
|
||||||
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
|
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
|
||||||
import com.ycwl.basic.mapper.FaceMapper;
|
import com.ycwl.basic.mapper.FaceMapper;
|
||||||
|
import com.ycwl.basic.model.pc.device.entity.DeviceEntity;
|
||||||
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
||||||
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
|
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
|
||||||
import com.ycwl.basic.model.task.resp.SearchFaceRespVo;
|
import com.ycwl.basic.model.task.resp.SearchFaceRespVo;
|
||||||
@@ -99,14 +100,20 @@ public class PrinterTvController {
|
|||||||
@GetMapping("/{sampleId}/qrcode")
|
@GetMapping("/{sampleId}/qrcode")
|
||||||
public void getQrcode(@PathVariable("sampleId") Long sampleId, HttpServletResponse response) throws Exception {
|
public void getQrcode(@PathVariable("sampleId") Long sampleId, HttpServletResponse response) throws Exception {
|
||||||
File qrcode = new File("qrcode_"+sampleId+".jpg");
|
File qrcode = new File("qrcode_"+sampleId+".jpg");
|
||||||
|
FaceSampleEntity faceSample = faceRepository.getFaceSample(sampleId);
|
||||||
|
if (faceSample == null) {
|
||||||
|
response.setStatus(404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String targetPath = "pages/printer/from_sample";
|
||||||
|
DeviceV2DTO device = deviceRepository.getDeviceBasic(faceSample.getDeviceId());
|
||||||
|
if (device.getType().equals("AI_CAM")) {
|
||||||
|
// AI_CAM,需要修改path
|
||||||
|
targetPath = "pages/ai-cam/from_sample";
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
FaceSampleEntity faceSample = faceRepository.getFaceSample(sampleId);
|
|
||||||
if (faceSample == null) {
|
|
||||||
response.setStatus(404);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
MpConfigEntity scenicMpConfig = scenicRepository.getScenicMpConfig(faceSample.getScenicId());
|
MpConfigEntity scenicMpConfig = scenicRepository.getScenicMpConfig(faceSample.getScenicId());
|
||||||
WxMpUtil.generateUnlimitedWXAQRCode(scenicMpConfig.getAppId(), scenicMpConfig.getAppSecret(), "pages/printer/from_sample", sampleId.toString(), qrcode);
|
WxMpUtil.generateUnlimitedWXAQRCode(scenicMpConfig.getAppId(), scenicMpConfig.getAppSecret(), targetPath, sampleId.toString(), qrcode);
|
||||||
|
|
||||||
// 设置响应头
|
// 设置响应头
|
||||||
response.setContentType("image/jpeg");
|
response.setContentType("image/jpeg");
|
||||||
|
|||||||
@@ -74,22 +74,31 @@ public class PuzzleGenerationOrchestrator {
|
|||||||
// 3. 准备公共动态数据
|
// 3. 准备公共动态数据
|
||||||
Map<String, String> baseDynamicData = buildBaseDynamicData(faceId, faceUrl, scenicBasic);
|
Map<String, String> baseDynamicData = buildBaseDynamicData(faceId, faceUrl, scenicBasic);
|
||||||
|
|
||||||
// 4. 遍历所有模板,逐个生成
|
// 4. 使用虚拟线程池并行生成所有模板
|
||||||
int successCount = 0;
|
java.util.concurrent.atomic.AtomicInteger successCount = new java.util.concurrent.atomic.AtomicInteger(0);
|
||||||
int failCount = 0;
|
java.util.concurrent.atomic.AtomicInteger failCount = new java.util.concurrent.atomic.AtomicInteger(0);
|
||||||
for (PuzzleTemplateDTO template : templateList) {
|
|
||||||
try {
|
try (java.util.concurrent.ExecutorService executor = java.util.concurrent.Executors.newVirtualThreadPerTaskExecutor()) {
|
||||||
generateSingleTemplate(scenicId, faceId, memberId, template, baseDynamicData);
|
// 为每个模板创建一个异步任务
|
||||||
successCount++;
|
List<java.util.concurrent.CompletableFuture<Void>> futures = templateList.stream()
|
||||||
} catch (Exception e) {
|
.map(template -> java.util.concurrent.CompletableFuture.runAsync(() -> {
|
||||||
log.error("拼图生成失败: scenicId={}, templateCode={}, templateName={}",
|
try {
|
||||||
scenicId, template.getCode(), template.getName(), e);
|
generateSingleTemplate(scenicId, faceId, memberId, template, baseDynamicData);
|
||||||
failCount++;
|
successCount.incrementAndGet();
|
||||||
}
|
} catch (Exception e) {
|
||||||
|
log.error("拼图生成失败: scenicId={}, templateCode={}, templateName={}",
|
||||||
|
scenicId, template.getCode(), template.getName(), e);
|
||||||
|
failCount.incrementAndGet();
|
||||||
|
}
|
||||||
|
}, executor))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
// 等待所有任务完成
|
||||||
|
java.util.concurrent.CompletableFuture.allOf(futures.toArray(new java.util.concurrent.CompletableFuture[0])).join();
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("景区拼图模板批量生成完成: scenicId={}, 总数={}, 成功={}, 失败={}",
|
log.info("景区拼图模板批量生成完成: scenicId={}, 总数={}, 成功={}, 失败={}",
|
||||||
scenicId, templateList.size(), successCount, failCount);
|
scenicId, templateList.size(), successCount.get(), failCount.get());
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// 异步任务失败不影响主流程,仅记录日志
|
// 异步任务失败不影响主流程,仅记录日志
|
||||||
|
|||||||
@@ -22,7 +22,13 @@ public enum PipelineScene {
|
|||||||
* 源图片超分辨率增强场景
|
* 源图片超分辨率增强场景
|
||||||
* IPC设备拍摄的源图片进行质量提升
|
* IPC设备拍摄的源图片进行质量提升
|
||||||
*/
|
*/
|
||||||
SOURCE_PHOTO_SUPER_RESOLUTION("source_photo_sr", "源图片超分辨率增强");
|
SOURCE_PHOTO_SUPER_RESOLUTION("source_photo_sr", "源图片超分辨率增强"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI相机照片增强场景
|
||||||
|
* AI相机拍摄的照片进行超分辨率和质量增强
|
||||||
|
*/
|
||||||
|
AI_CAM_ENHANCE("ai_cam_enhance", "AI相机照片增强");
|
||||||
|
|
||||||
private final String code;
|
private final String code;
|
||||||
private final String description;
|
private final String description;
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
package com.ycwl.basic.image.pipeline.stages;
|
||||||
|
|
||||||
|
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
||||||
|
import com.ycwl.basic.pipeline.annotation.StageConfig;
|
||||||
|
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
|
||||||
|
import com.ycwl.basic.pipeline.core.StageResult;
|
||||||
|
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import java.awt.Graphics2D;
|
||||||
|
import java.awt.RenderingHints;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图像缩放Stage
|
||||||
|
* 支持按比例放大或缩小图片
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@StageConfig(
|
||||||
|
stageId = "image_resize",
|
||||||
|
optionalMode = StageOptionalMode.SUPPORT,
|
||||||
|
description = "图像缩放处理",
|
||||||
|
defaultEnabled = true
|
||||||
|
)
|
||||||
|
public class ImageResizeStage extends AbstractPipelineStage<PhotoProcessContext> {
|
||||||
|
|
||||||
|
private final double scaleFactor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
* @param scaleFactor 缩放比例(例如: 1.5表示放大1.5倍, 0.333表示缩小到1/3)
|
||||||
|
*/
|
||||||
|
public ImageResizeStage(double scaleFactor) {
|
||||||
|
if (scaleFactor <= 0) {
|
||||||
|
throw new IllegalArgumentException("scaleFactor must be positive");
|
||||||
|
}
|
||||||
|
this.scaleFactor = scaleFactor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "ImageResizeStage";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected StageResult<PhotoProcessContext> doExecute(PhotoProcessContext context) {
|
||||||
|
File currentFile = context.getCurrentFile();
|
||||||
|
if (currentFile == null || !currentFile.exists()) {
|
||||||
|
return StageResult.skipped("当前文件不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
BufferedImage originalImage = null;
|
||||||
|
BufferedImage resizedImage = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
log.debug("开始图像缩放处理: file={}, scaleFactor={}", currentFile.getName(), scaleFactor);
|
||||||
|
|
||||||
|
// 读取原图
|
||||||
|
originalImage = ImageIO.read(currentFile);
|
||||||
|
if (originalImage == null) {
|
||||||
|
return StageResult.failed("无法读取图片文件");
|
||||||
|
}
|
||||||
|
|
||||||
|
int originalWidth = originalImage.getWidth();
|
||||||
|
int originalHeight = originalImage.getHeight();
|
||||||
|
|
||||||
|
// 计算新尺寸
|
||||||
|
int newWidth = (int) Math.round(originalWidth * scaleFactor);
|
||||||
|
int newHeight = (int) Math.round(originalHeight * scaleFactor);
|
||||||
|
|
||||||
|
// 检查尺寸是否合理
|
||||||
|
if (newWidth <= 0 || newHeight <= 0) {
|
||||||
|
return StageResult.failed("缩放后尺寸无效: " + newWidth + "x" + newHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建缩放后的图像
|
||||||
|
resizedImage = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB);
|
||||||
|
Graphics2D g2d = resizedImage.createGraphics();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 设置高质量渲染选项
|
||||||
|
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
|
||||||
|
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
|
||||||
|
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
|
|
||||||
|
// 执行缩放
|
||||||
|
g2d.drawImage(originalImage, 0, 0, newWidth, newHeight, null);
|
||||||
|
} finally {
|
||||||
|
g2d.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存缩放后的图片
|
||||||
|
File resizedFile = context.getTempFileManager().createTempFile("resized", ".jpg");
|
||||||
|
ImageIO.write(resizedImage, "jpg", resizedFile);
|
||||||
|
|
||||||
|
if (!resizedFile.exists() || resizedFile.length() == 0) {
|
||||||
|
return StageResult.failed("缩放后图片保存失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新处理后的文件
|
||||||
|
context.updateProcessedFile(resizedFile);
|
||||||
|
|
||||||
|
log.info("图像缩放完成: {}x{} -> {}x{} (比例: {})",
|
||||||
|
originalWidth, originalHeight,
|
||||||
|
newWidth, newHeight,
|
||||||
|
scaleFactor);
|
||||||
|
|
||||||
|
return StageResult.success(String.format("缩放完成 (%dx%d -> %dx%d)",
|
||||||
|
originalWidth, originalHeight, newWidth, newHeight));
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("图像缩放失败: {}", e.getMessage(), e);
|
||||||
|
return StageResult.failed("缩放失败: " + e.getMessage(), e);
|
||||||
|
} finally {
|
||||||
|
// 释放图像资源
|
||||||
|
if (originalImage != null) {
|
||||||
|
originalImage.flush();
|
||||||
|
}
|
||||||
|
if (resizedImage != null) {
|
||||||
|
resizedImage.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package com.ycwl.basic.image.pipeline.stages;
|
||||||
|
|
||||||
|
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
||||||
|
import com.ycwl.basic.mapper.PrinterMapper;
|
||||||
|
import com.ycwl.basic.pipeline.annotation.StageConfig;
|
||||||
|
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
|
||||||
|
import com.ycwl.basic.pipeline.core.StageResult;
|
||||||
|
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新MemberPrint记录Stage
|
||||||
|
* 用于更新member_print表中的cropUrl字段
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@StageConfig(
|
||||||
|
stageId = "update_member_print",
|
||||||
|
optionalMode = StageOptionalMode.UNSUPPORT,
|
||||||
|
description = "更新MemberPrint记录",
|
||||||
|
defaultEnabled = true
|
||||||
|
)
|
||||||
|
public class UpdateMemberPrintStage extends AbstractPipelineStage<PhotoProcessContext> {
|
||||||
|
|
||||||
|
private final PrinterMapper printerMapper;
|
||||||
|
private final Integer memberPrintId;
|
||||||
|
private final Long memberId;
|
||||||
|
private final Long scenicId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
* @param printerMapper PrinterMapper实例
|
||||||
|
* @param memberPrintId MemberPrint记录ID
|
||||||
|
* @param memberId 用户ID
|
||||||
|
* @param scenicId 景区ID
|
||||||
|
*/
|
||||||
|
public UpdateMemberPrintStage(PrinterMapper printerMapper, Integer memberPrintId, Long memberId, Long scenicId) {
|
||||||
|
this.printerMapper = printerMapper;
|
||||||
|
this.memberPrintId = memberPrintId;
|
||||||
|
this.memberId = memberId;
|
||||||
|
this.scenicId = scenicId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "UpdateMemberPrintStage";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected StageResult<PhotoProcessContext> doExecute(PhotoProcessContext context) {
|
||||||
|
String resultUrl = context.getResultUrl();
|
||||||
|
|
||||||
|
if (resultUrl == null || resultUrl.trim().isEmpty()) {
|
||||||
|
return StageResult.skipped("结果URL为空,跳过更新");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (memberPrintId == null || memberId == null || scenicId == null) {
|
||||||
|
log.warn("MemberPrint更新参数不完整: memberPrintId={}, memberId={}, scenicId={}",
|
||||||
|
memberPrintId, memberId, scenicId);
|
||||||
|
return StageResult.skipped("更新参数不完整");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
log.debug("开始更新MemberPrint记录: id={}, newCropUrl={}", memberPrintId, resultUrl);
|
||||||
|
|
||||||
|
// 更新cropUrl字段
|
||||||
|
int rows = printerMapper.setPhotoCropped(memberId, scenicId, memberPrintId.longValue(), resultUrl, null);
|
||||||
|
|
||||||
|
if (rows > 0) {
|
||||||
|
log.info("MemberPrint记录更新成功: id={}, cropUrl已更新", memberPrintId);
|
||||||
|
return StageResult.success("更新成功");
|
||||||
|
} else {
|
||||||
|
log.warn("MemberPrint记录更新失败: 可能记录不存在, id={}", memberPrintId);
|
||||||
|
return StageResult.degraded("更新失败,记录可能不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("更新MemberPrint记录异常: id={}", memberPrintId, e);
|
||||||
|
// 更新失败不影响整个流程,使用降级状态
|
||||||
|
return StageResult.degraded("更新异常: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,4 +34,11 @@ public class WatermarkConfig {
|
|||||||
* 二维码文件
|
* 二维码文件
|
||||||
*/
|
*/
|
||||||
private final File qrcodeFile;
|
private final File qrcodeFile;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缩放倍数,用于将所有定位和大小乘以该倍数
|
||||||
|
* 默认值为 1.0(不缩放)
|
||||||
|
*/
|
||||||
|
@Builder.Default
|
||||||
|
private final Double scale = 1.0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,6 +170,12 @@ public class WatermarkStage extends AbstractPipelineStage<PhotoProcessContext> {
|
|||||||
info.setQrcodeFile(qrcodeFile);
|
info.setQrcodeFile(qrcodeFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 从 config 读取缩放倍数
|
||||||
|
Double scale = config.getScale();
|
||||||
|
if (scale != null) {
|
||||||
|
info.setScale(scale);
|
||||||
|
}
|
||||||
|
|
||||||
// 根据旋转状态自己处理 offsetLeft
|
// 根据旋转状态自己处理 offsetLeft
|
||||||
if (context.isRotationApplied()) {
|
if (context.isRotationApplied()) {
|
||||||
if (context.getImageRotation() == 90) {
|
if (context.getImageRotation() == 90) {
|
||||||
|
|||||||
@@ -33,6 +33,13 @@ public class WatermarkInfo {
|
|||||||
private Integer offsetLeft;
|
private Integer offsetLeft;
|
||||||
private Integer offsetRight;
|
private Integer offsetRight;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缩放倍数,用于将所有定位和大小乘以该倍数
|
||||||
|
* 例如: scale=2.0 表示所有尺寸和位置都放大2倍
|
||||||
|
* null 表示使用默认值1.0(不缩放)
|
||||||
|
*/
|
||||||
|
private Double scale;
|
||||||
|
|
||||||
public String getDatetimeLine() {
|
public String getDatetimeLine() {
|
||||||
if (datetimeLine == null) {
|
if (datetimeLine == null) {
|
||||||
datetimeLine = DateUtil.format(datetime, dtFormat);
|
datetimeLine = DateUtil.format(datetime, dtFormat);
|
||||||
|
|||||||
@@ -64,11 +64,14 @@ public class PrinterDefaultWatermarkOperator implements IOperator {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public File process(WatermarkInfo info) throws ImageWatermarkException {
|
public File process(WatermarkInfo info) throws ImageWatermarkException {
|
||||||
// 获取四边偏移值,优先使用传入的值,否则使用默认值
|
// 获取缩放倍数,默认为1.0(不缩放)
|
||||||
int offsetTop = info.getOffsetTop() != null ? info.getOffsetTop() : DEFAULT_OFFSET_TOP;
|
double scale = info.getScale() != null ? info.getScale() : 1.0;
|
||||||
int offsetBottom = info.getOffsetBottom() != null ? info.getOffsetBottom() : DEFAULT_OFFSET_BOTTOM;
|
|
||||||
int offsetLeft = info.getOffsetLeft() != null ? info.getOffsetLeft() : DEFAULT_OFFSET_LEFT;
|
// 获取四边偏移值,优先使用传入的值,否则使用默认值,并应用缩放
|
||||||
int offsetRight = info.getOffsetRight() != null ? info.getOffsetRight() : DEFAULT_OFFSET_RIGHT;
|
int offsetTop = (int) ((info.getOffsetTop() != null ? info.getOffsetTop() : DEFAULT_OFFSET_TOP) * scale);
|
||||||
|
int offsetBottom = (int) ((info.getOffsetBottom() != null ? info.getOffsetBottom() : DEFAULT_OFFSET_BOTTOM) * scale);
|
||||||
|
int offsetLeft = (int) ((info.getOffsetLeft() != null ? info.getOffsetLeft() : DEFAULT_OFFSET_LEFT) * scale);
|
||||||
|
int offsetRight = (int) ((info.getOffsetRight() != null ? info.getOffsetRight() : DEFAULT_OFFSET_RIGHT) * scale);
|
||||||
|
|
||||||
BufferedImage baseImage;
|
BufferedImage baseImage;
|
||||||
BufferedImage qrcodeImage;
|
BufferedImage qrcodeImage;
|
||||||
@@ -86,17 +89,26 @@ public class PrinterDefaultWatermarkOperator implements IOperator {
|
|||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new ImageWatermarkException("图片打开失败");
|
throw new ImageWatermarkException("图片打开失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 应用缩放到所有常量
|
||||||
|
int scaledExtraBorder = (int) (EXTRA_BORDER_PX * scale);
|
||||||
|
int scaledOffsetY = (int) (OFFSET_Y * scale);
|
||||||
|
int scaledQrcodeSize = (int) (QRCODE_SIZE * scale);
|
||||||
|
int scaledQrcodeOffsetY = (int) (QRCODE_OFFSET_Y * scale);
|
||||||
|
int scaledScenicFontSize = (int) (SCENIC_FONT_SIZE * scale);
|
||||||
|
int scaledDatetimeFontSize = (int) (DATETIME_FONT_SIZE * scale);
|
||||||
|
|
||||||
// 新图像画布
|
// 新图像画布
|
||||||
BufferedImage newImage = new BufferedImage(baseImage.getWidth() + 2 * EXTRA_BORDER_PX, baseImage.getHeight() + 2 * EXTRA_BORDER_PX, BufferedImage.TYPE_INT_RGB);
|
BufferedImage newImage = new BufferedImage(baseImage.getWidth() + 2 * scaledExtraBorder, baseImage.getHeight() + 2 * scaledExtraBorder, BufferedImage.TYPE_INT_RGB);
|
||||||
Graphics2D g2d = newImage.createGraphics();
|
Graphics2D g2d = newImage.createGraphics();
|
||||||
g2d.setColor(BG_COLOR);
|
g2d.setColor(BG_COLOR);
|
||||||
g2d.fillRect(0, 0, newImage.getWidth(), newImage.getHeight());
|
g2d.fillRect(0, 0, newImage.getWidth(), newImage.getHeight());
|
||||||
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
g2d.drawImage(baseImage, EXTRA_BORDER_PX, EXTRA_BORDER_PX, null);
|
g2d.drawImage(baseImage, scaledExtraBorder, scaledExtraBorder, null);
|
||||||
int newQrcodeHeight = QRCODE_SIZE;
|
int newQrcodeHeight = scaledQrcodeSize;
|
||||||
int newQrcodeWidth = (int) (newQrcodeHeight * 1.0 / qrcodeImage.getHeight() * qrcodeImage.getWidth());
|
int newQrcodeWidth = (int) (newQrcodeHeight * 1.0 / qrcodeImage.getHeight() * qrcodeImage.getWidth());
|
||||||
Font scenicFont = new Font(defaultFontName, Font.BOLD, SCENIC_FONT_SIZE);
|
Font scenicFont = new Font(defaultFontName, Font.BOLD, scaledScenicFontSize);
|
||||||
Font datetimeFont = new Font(defaultFontName, Font.BOLD, DATETIME_FONT_SIZE);
|
Font datetimeFont = new Font(defaultFontName, Font.BOLD, scaledDatetimeFontSize);
|
||||||
FontMetrics scenicFontMetrics = g2d.getFontMetrics(scenicFont);
|
FontMetrics scenicFontMetrics = g2d.getFontMetrics(scenicFont);
|
||||||
FontMetrics datetimeFontMetrics = g2d.getFontMetrics(datetimeFont);
|
FontMetrics datetimeFontMetrics = g2d.getFontMetrics(datetimeFont);
|
||||||
int scenicLineHeight = scenicFontMetrics.getHeight();
|
int scenicLineHeight = scenicFontMetrics.getHeight();
|
||||||
@@ -106,13 +118,14 @@ public class PrinterDefaultWatermarkOperator implements IOperator {
|
|||||||
|
|
||||||
// 二维码放置在左下角,距离左边缘图片宽度的5%,再加上左侧偏移
|
// 二维码放置在左下角,距离左边缘图片宽度的5%,再加上左侧偏移
|
||||||
int qrcodeOffsetX = (int) (newImage.getWidth() * QRCODE_LEFT_MARGIN_RATIO) + offsetLeft;
|
int qrcodeOffsetX = (int) (newImage.getWidth() * QRCODE_LEFT_MARGIN_RATIO) + offsetLeft;
|
||||||
int qrcodeOffsetY = EXTRA_BORDER_PX + baseImage.getHeight() - OFFSET_Y - newQrcodeHeight - offsetBottom;
|
int qrcodeOffsetY = scaledExtraBorder + baseImage.getHeight() - scaledOffsetY - newQrcodeHeight - offsetBottom;
|
||||||
Shape originalClip = g2d.getClip();
|
Shape originalClip = g2d.getClip();
|
||||||
|
|
||||||
// 创建比二维码大10像素的白色圆形背景
|
// 创建比二维码大10像素的白色圆形背景(10像素也要缩放)
|
||||||
int whiteCircleSize = Math.max(newQrcodeWidth, newQrcodeHeight) + 10;
|
int whiteCirclePadding = (int) (10 * scale);
|
||||||
|
int whiteCircleSize = Math.max(newQrcodeWidth, newQrcodeHeight) + whiteCirclePadding;
|
||||||
int whiteCircleX = qrcodeOffsetX - (whiteCircleSize - newQrcodeWidth) / 2;
|
int whiteCircleX = qrcodeOffsetX - (whiteCircleSize - newQrcodeWidth) / 2;
|
||||||
int whiteCircleY = qrcodeOffsetY + QRCODE_OFFSET_Y - (whiteCircleSize - newQrcodeHeight) / 2;
|
int whiteCircleY = qrcodeOffsetY + scaledQrcodeOffsetY - (whiteCircleSize - newQrcodeHeight) / 2;
|
||||||
|
|
||||||
// 绘制白色圆形背景
|
// 绘制白色圆形背景
|
||||||
g2d.setColor(Color.WHITE);
|
g2d.setColor(Color.WHITE);
|
||||||
@@ -122,7 +135,7 @@ public class PrinterDefaultWatermarkOperator implements IOperator {
|
|||||||
// 用白色圆形尺寸裁切二维码(保持二维码原始尺寸,但用大圆裁切)
|
// 用白色圆形尺寸裁切二维码(保持二维码原始尺寸,但用大圆裁切)
|
||||||
Ellipse2D qrcodeCircle = new Ellipse2D.Double(whiteCircleX, whiteCircleY, whiteCircleSize, whiteCircleSize);
|
Ellipse2D qrcodeCircle = new Ellipse2D.Double(whiteCircleX, whiteCircleY, whiteCircleSize, whiteCircleSize);
|
||||||
g2d.setClip(qrcodeCircle);
|
g2d.setClip(qrcodeCircle);
|
||||||
g2d.drawImage(qrcodeImage, qrcodeOffsetX, qrcodeOffsetY + QRCODE_OFFSET_Y, newQrcodeWidth, newQrcodeHeight, null);
|
g2d.drawImage(qrcodeImage, qrcodeOffsetX, qrcodeOffsetY + scaledQrcodeOffsetY, newQrcodeWidth, newQrcodeHeight, null);
|
||||||
g2d.setClip(originalClip);
|
g2d.setClip(originalClip);
|
||||||
|
|
||||||
// 在圆形二维码中央绘制圆形头像
|
// 在圆形二维码中央绘制圆形头像
|
||||||
@@ -130,7 +143,7 @@ public class PrinterDefaultWatermarkOperator implements IOperator {
|
|||||||
// 计算圆形头像的尺寸和位置
|
// 计算圆形头像的尺寸和位置
|
||||||
int avatarDiameter = (int) (newQrcodeHeight * 0.45);
|
int avatarDiameter = (int) (newQrcodeHeight * 0.45);
|
||||||
int avatarX = qrcodeOffsetX + (newQrcodeWidth - avatarDiameter) / 2;
|
int avatarX = qrcodeOffsetX + (newQrcodeWidth - avatarDiameter) / 2;
|
||||||
int avatarY = qrcodeOffsetY + QRCODE_OFFSET_Y + (newQrcodeHeight - avatarDiameter) / 2;
|
int avatarY = qrcodeOffsetY + scaledQrcodeOffsetY + (newQrcodeHeight - avatarDiameter) / 2;
|
||||||
|
|
||||||
// 保存当前的渲染设置
|
// 保存当前的渲染设置
|
||||||
RenderingHints originalHints = g2d.getRenderingHints();
|
RenderingHints originalHints = g2d.getRenderingHints();
|
||||||
@@ -149,10 +162,10 @@ public class PrinterDefaultWatermarkOperator implements IOperator {
|
|||||||
double faceHeight = faceImage.getHeight();
|
double faceHeight = faceImage.getHeight();
|
||||||
double scaleX = avatarDiameter / faceWidth;
|
double scaleX = avatarDiameter / faceWidth;
|
||||||
double scaleY = avatarDiameter / faceHeight;
|
double scaleY = avatarDiameter / faceHeight;
|
||||||
double scale = Math.max(scaleX, scaleY); // 使用较大的缩放比例以填满圆形
|
double faceScale = Math.max(scaleX, scaleY); // 使用较大的缩放比例以填满圆形
|
||||||
|
|
||||||
int scaledWidth = (int) (faceWidth * scale);
|
int scaledWidth = (int) (faceWidth * faceScale);
|
||||||
int scaledHeight = (int) (faceHeight * scale);
|
int scaledHeight = (int) (faceHeight * faceScale);
|
||||||
|
|
||||||
// 计算居中位置
|
// 计算居中位置
|
||||||
int faceDrawX = avatarX + (avatarDiameter - scaledWidth) / 2;
|
int faceDrawX = avatarX + (avatarDiameter - scaledWidth) / 2;
|
||||||
@@ -167,7 +180,7 @@ public class PrinterDefaultWatermarkOperator implements IOperator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 计算文字与二维码垂直居中对齐的Y坐标
|
// 计算文字与二维码垂直居中对齐的Y坐标
|
||||||
int qrcodeTop = qrcodeOffsetY + QRCODE_OFFSET_Y;
|
int qrcodeTop = qrcodeOffsetY + scaledQrcodeOffsetY;
|
||||||
int qrcodeBottom = qrcodeTop + newQrcodeHeight;
|
int qrcodeBottom = qrcodeTop + newQrcodeHeight;
|
||||||
int qrcodeCenter = (qrcodeTop + qrcodeBottom) / 2;
|
int qrcodeCenter = (qrcodeTop + qrcodeBottom) / 2;
|
||||||
|
|
||||||
|
|||||||
17
src/main/java/com/ycwl/basic/integration/glm/GlmClient.java
Normal file
17
src/main/java/com/ycwl/basic/integration/glm/GlmClient.java
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package com.ycwl.basic.integration.glm;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智谱 GLM 模型调用抽象。
|
||||||
|
*/
|
||||||
|
public interface GlmClient {
|
||||||
|
/**
|
||||||
|
* 流式回复,实时回调分片,同时返回完整文本。
|
||||||
|
*/
|
||||||
|
String streamReply(Long faceId,
|
||||||
|
Long memberId,
|
||||||
|
String traceId,
|
||||||
|
List<ai.z.openapi.service.model.ChatMessage> messages,
|
||||||
|
java.util.function.Consumer<String> chunkConsumer);
|
||||||
|
}
|
||||||
118
src/main/java/com/ycwl/basic/integration/glm/GlmClientImpl.java
Normal file
118
src/main/java/com/ycwl/basic/integration/glm/GlmClientImpl.java
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
package com.ycwl.basic.integration.glm;
|
||||||
|
|
||||||
|
import ai.z.openapi.ZhipuAiClient;
|
||||||
|
import ai.z.openapi.service.model.ChatCompletionCreateParams;
|
||||||
|
import ai.z.openapi.service.model.ChatCompletionResponse;
|
||||||
|
import ai.z.openapi.service.model.ChatMessage;
|
||||||
|
import ai.z.openapi.service.model.ChatMessageRole;
|
||||||
|
import ai.z.openapi.service.model.ChatThinking;
|
||||||
|
import ai.z.openapi.service.model.Delta;
|
||||||
|
import ai.z.openapi.service.model.ModelData;
|
||||||
|
import io.reactivex.rxjava3.core.Flowable;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智谱 GLM 官方 SDK 调用实现,流式拆分文本。
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class GlmClientImpl implements GlmClient {
|
||||||
|
|
||||||
|
private static final String DEFAULT_MODEL = "glm-4.5-airx";
|
||||||
|
|
||||||
|
private final ZhipuAiClient client;
|
||||||
|
|
||||||
|
public GlmClientImpl(@Value("${zhipu.api-key:}") String apiKeyFromCfg) {
|
||||||
|
String apiKey = StringUtils.isNotBlank(apiKeyFromCfg) ? apiKeyFromCfg : System.getenv("ZHIPU_API_KEY");
|
||||||
|
if (StringUtils.isBlank(apiKey)) {
|
||||||
|
log.warn("未配置 ZHIPU_API_KEY,GLM 调用将返回占位结果");
|
||||||
|
client = null;
|
||||||
|
} else {
|
||||||
|
client = ZhipuAiClient.builder().apiKey(apiKey).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String streamReply(Long faceId,
|
||||||
|
Long memberId,
|
||||||
|
String traceId,
|
||||||
|
List<ChatMessage> messages,
|
||||||
|
Consumer<String> chunkConsumer) {
|
||||||
|
if (client == null) {
|
||||||
|
String fallback = "GLM 未配置 API KEY,无法调用。";
|
||||||
|
if (chunkConsumer != null) {
|
||||||
|
chunkConsumer.accept(fallback);
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
List<ChatMessage> finalMessages = messages == null ? new ArrayList<>() : new ArrayList<>(messages);
|
||||||
|
if (finalMessages.isEmpty()) {
|
||||||
|
finalMessages.add(ChatMessage.builder()
|
||||||
|
.role(ChatMessageRole.USER.value())
|
||||||
|
.content("你好")
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
finalMessages.addFirst(ChatMessage.builder()
|
||||||
|
.role(ChatMessageRole.SYSTEM.value())
|
||||||
|
.content("你是一个智能销售,然后用户是实际想购买的人。你需要尽可能推广景区生成的Vlog、视频、照片等内容给用户,鼓励购买。注意,返回内容需要为普通文本格式,禁止出现Markdown标记。")
|
||||||
|
.build());
|
||||||
|
finalMessages.addFirst(ChatMessage.builder()
|
||||||
|
.role(ChatMessageRole.SYSTEM.value())
|
||||||
|
.content("注意,返回内容需要为普通文本格式,禁止使用Markdown格式进行返回。")
|
||||||
|
.build());
|
||||||
|
ChatCompletionCreateParams request = ChatCompletionCreateParams.builder()
|
||||||
|
.model(DEFAULT_MODEL)
|
||||||
|
.messages(finalMessages)
|
||||||
|
.thinking(ChatThinking.builder().type("enabled").build())
|
||||||
|
.stream(true)
|
||||||
|
.maxTokens(4096)
|
||||||
|
.temperature(0.8f)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
ChatCompletionResponse response = client.chat().createChatCompletion(request);
|
||||||
|
if (!response.isSuccess()) {
|
||||||
|
String msg = "GLM 调用失败: " + response.getMsg();
|
||||||
|
log.warn(msg);
|
||||||
|
if (chunkConsumer != null) {
|
||||||
|
chunkConsumer.accept(msg);
|
||||||
|
}
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
Flowable<ModelData> flowable = response.getFlowable();
|
||||||
|
flowable.blockingSubscribe(
|
||||||
|
data -> {
|
||||||
|
if (data.getChoices() == null || data.getChoices().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Delta delta = data.getChoices().getFirst().getDelta();
|
||||||
|
if (delta == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String piece = delta.getContent();
|
||||||
|
if (StringUtils.isNotBlank(piece)) {
|
||||||
|
sb.append(piece);
|
||||||
|
if (chunkConsumer != null) {
|
||||||
|
chunkConsumer.accept(piece);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error -> {
|
||||||
|
log.error("GLM 流式调用异常", error);
|
||||||
|
String err = "GLM 调用异常:" + error.getMessage();
|
||||||
|
sb.append(err);
|
||||||
|
if (chunkConsumer != null) {
|
||||||
|
chunkConsumer.accept(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.ycwl.basic.mapper;
|
||||||
|
|
||||||
|
import com.ycwl.basic.model.mobile.chat.entity.FaceChatConversationEntity;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface FaceChatConversationMapper {
|
||||||
|
FaceChatConversationEntity findByFaceId(@Param("faceId") Long faceId);
|
||||||
|
|
||||||
|
FaceChatConversationEntity getById(@Param("id") Long id);
|
||||||
|
|
||||||
|
int insert(FaceChatConversationEntity entity);
|
||||||
|
|
||||||
|
int updateStatus(@Param("id") Long id, @Param("status") String status);
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.ycwl.basic.mapper;
|
||||||
|
|
||||||
|
import com.ycwl.basic.model.mobile.chat.entity.FaceChatMessageEntity;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface FaceChatMessageMapper {
|
||||||
|
Integer maxSeqForUpdate(@Param("conversationId") Long conversationId);
|
||||||
|
|
||||||
|
int insert(FaceChatMessageEntity entity);
|
||||||
|
|
||||||
|
List<FaceChatMessageEntity> listByConversation(@Param("conversationId") Long conversationId,
|
||||||
|
@Param("cursor") Integer cursor,
|
||||||
|
@Param("limit") Integer limit);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按 seq 倒序获取最近若干条消息,用于拼接上下文。
|
||||||
|
*/
|
||||||
|
List<FaceChatMessageEntity> listRecentByConversation(@Param("conversationId") Long conversationId,
|
||||||
|
@Param("limit") Integer limit);
|
||||||
|
}
|
||||||
@@ -3,10 +3,19 @@ package com.ycwl.basic.mapper;
|
|||||||
import com.ycwl.basic.model.pc.faceDetectLog.entity.FaceDetectLogAiCamEntity;
|
import com.ycwl.basic.model.pc.faceDetectLog.entity.FaceDetectLogAiCamEntity;
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AI相机人脸识别日志Mapper
|
* AI相机人脸识别日志Mapper
|
||||||
*/
|
*/
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface FaceDetectLogAiCamMapper {
|
public interface FaceDetectLogAiCamMapper {
|
||||||
int add(FaceDetectLogAiCamEntity entity);
|
int add(FaceDetectLogAiCamEntity entity);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据faceId查询所有识别记录
|
||||||
|
* @param faceId 人脸ID
|
||||||
|
* @return 识别记录列表
|
||||||
|
*/
|
||||||
|
List<FaceDetectLogAiCamEntity> listByFaceId(Long faceId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ public interface SourceMapper {
|
|||||||
List<SourceEntity> listVideoByFaceRelation(Long faceId);
|
List<SourceEntity> listVideoByFaceRelation(Long faceId);
|
||||||
|
|
||||||
List<SourceEntity> listImageByFaceRelation(Long faceId);
|
List<SourceEntity> listImageByFaceRelation(Long faceId);
|
||||||
|
List<SourceEntity> listAiCamImageByFaceRelation(Long faceId);
|
||||||
List<MemberSourceEntity> listByFaceRelation(Long faceId, Integer type);
|
List<MemberSourceEntity> listByFaceRelation(Long faceId, Integer type);
|
||||||
|
|
||||||
SourceEntity getEntity(Long id);
|
SourceEntity getEntity(Long id);
|
||||||
@@ -148,4 +149,20 @@ public interface SourceMapper {
|
|||||||
* @return source实体
|
* @return source实体
|
||||||
*/
|
*/
|
||||||
SourceEntity getSourceByFaceAndDeviceId(Long faceId, Long deviceId, Integer type, String sortStrategy);
|
SourceEntity getSourceByFaceAndDeviceId(Long faceId, Long deviceId, Integer type, String sortStrategy);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据faceSampleId列表和type查询source列表
|
||||||
|
* @param faceSampleIds faceSampleId列表
|
||||||
|
* @param type 素材类型
|
||||||
|
* @return source实体列表
|
||||||
|
*/
|
||||||
|
List<SourceEntity> listByFaceSampleIdsAndType(List<Long> faceSampleIds, Integer type);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除指定faceId和type的member_source关联记录
|
||||||
|
* @param faceId 人脸ID
|
||||||
|
* @param type 素材类型
|
||||||
|
* @return 删除的记录数
|
||||||
|
*/
|
||||||
|
int deleteRelationsByFaceIdAndType(Long faceId, Integer type);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
package com.ycwl.basic.model;
|
package com.ycwl.basic.model;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 裁剪信息
|
* 裁剪信息
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
public class Crop {
|
public class Crop {
|
||||||
private Integer rotation;
|
private Integer rotation;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.ycwl.basic.model.mobile.chat;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会话信息返回对象。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ChatConversationVO {
|
||||||
|
private Long conversationId;
|
||||||
|
private Long faceId;
|
||||||
|
private String status;
|
||||||
|
private String model;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.ycwl.basic.model.mobile.chat;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息列表响应。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ChatMessagePageResp {
|
||||||
|
private List<ChatMessageVO> messages;
|
||||||
|
/**
|
||||||
|
* 下一条游标(返回最后一条 seq)。
|
||||||
|
*/
|
||||||
|
private Integer nextCursor;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.ycwl.basic.model.mobile.chat;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 聊天消息视图对象。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ChatMessageVO {
|
||||||
|
private Long id;
|
||||||
|
private Integer seq;
|
||||||
|
private String role;
|
||||||
|
private String content;
|
||||||
|
private String traceId;
|
||||||
|
private Date createdAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.ycwl.basic.model.mobile.chat;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送消息请求体。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ChatSendMessageReq {
|
||||||
|
/**
|
||||||
|
* 用户输入的文本内容。
|
||||||
|
*/
|
||||||
|
private String content;
|
||||||
|
/**
|
||||||
|
* 链路追踪ID,前端可透传,没有则服务端生成。
|
||||||
|
*/
|
||||||
|
private String traceId;
|
||||||
|
/**
|
||||||
|
* 是否期望流式返回。
|
||||||
|
*/
|
||||||
|
private Boolean stream;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.ycwl.basic.model.mobile.chat;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送消息同步响应。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ChatSendMessageResp {
|
||||||
|
private ChatMessageVO userMessage;
|
||||||
|
private ChatMessageVO assistantMessage;
|
||||||
|
private String traceId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.ycwl.basic.model.mobile.chat;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流式发送消息的服务结果。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ChatSendMessageStreamResp {
|
||||||
|
private ChatMessageVO userMessage;
|
||||||
|
private ChatMessageVO assistantMessage;
|
||||||
|
private String traceId;
|
||||||
|
private List<String> chunks;
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.ycwl.basic.model.mobile.chat.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小程序人脸聊天会话,一脸一会话。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@TableName("face_chat_conversation")
|
||||||
|
public class FaceChatConversationEntity {
|
||||||
|
@TableId
|
||||||
|
private Long id;
|
||||||
|
/**
|
||||||
|
* 对应的人脸ID。
|
||||||
|
*/
|
||||||
|
private Long faceId;
|
||||||
|
/**
|
||||||
|
* 归属用户ID,冗余校验越权。
|
||||||
|
*/
|
||||||
|
private Long memberId;
|
||||||
|
/**
|
||||||
|
* 会话状态 active/closed。
|
||||||
|
*/
|
||||||
|
private String status;
|
||||||
|
/**
|
||||||
|
* 使用的模型名称,例如 glm-v。
|
||||||
|
*/
|
||||||
|
private String model;
|
||||||
|
private Date createdAt;
|
||||||
|
private Date updatedAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.ycwl.basic.model.mobile.chat.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小程序人脸聊天消息,只保存文本。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@TableName("face_chat_message")
|
||||||
|
public class FaceChatMessageEntity {
|
||||||
|
@TableId
|
||||||
|
private Long id;
|
||||||
|
private Long conversationId;
|
||||||
|
private Long faceId;
|
||||||
|
private Integer seq;
|
||||||
|
/**
|
||||||
|
* user / assistant / system。
|
||||||
|
*/
|
||||||
|
private String role;
|
||||||
|
private String content;
|
||||||
|
private String traceId;
|
||||||
|
private Integer latencyMs;
|
||||||
|
private Date createdAt;
|
||||||
|
}
|
||||||
@@ -24,4 +24,5 @@ public class MemberPrintEntity {
|
|||||||
private Integer status;
|
private Integer status;
|
||||||
private Date createTime;
|
private Date createTime;
|
||||||
private Date updateTime;
|
private Date updateTime;
|
||||||
|
private String crop;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ public enum ProductType {
|
|||||||
// 照片类
|
// 照片类
|
||||||
PHOTO("PHOTO", "照片", ProductCategory.PHOTO),
|
PHOTO("PHOTO", "照片", ProductCategory.PHOTO),
|
||||||
PHOTO_SET("PHOTO_SET", "照片集", ProductCategory.PHOTO),
|
PHOTO_SET("PHOTO_SET", "照片集", ProductCategory.PHOTO),
|
||||||
|
AI_CAM_PHOTO_SET("AI_CAM_PHOTO_SET", "照片集", ProductCategory.PHOTO),
|
||||||
PHOTO_LOG("PHOTO_LOG", "pLog图", ProductCategory.PHOTO),
|
PHOTO_LOG("PHOTO_LOG", "pLog图", ProductCategory.PHOTO),
|
||||||
|
|
||||||
// 视频类(素材视频)
|
// 视频类(素材视频)
|
||||||
|
|||||||
@@ -89,22 +89,22 @@ public interface PriceCouponClaimRecordMapper extends BaseMapper<PriceCouponClai
|
|||||||
"LEFT JOIN price_coupon_config c ON r.coupon_id = c.id " +
|
"LEFT JOIN price_coupon_config c ON r.coupon_id = c.id " +
|
||||||
"<where>" +
|
"<where>" +
|
||||||
"<if test='userId != null'>" +
|
"<if test='userId != null'>" +
|
||||||
"AND r.user_id = #{userId}" +
|
"AND r.user_id = #{userId} " +
|
||||||
"</if>" +
|
"</if>" +
|
||||||
"<if test='couponId != null'>" +
|
"<if test='couponId != null'>" +
|
||||||
"AND r.coupon_id = #{couponId}" +
|
"AND r.coupon_id = #{couponId} " +
|
||||||
"</if>" +
|
"</if>" +
|
||||||
"<if test='status != null'>" +
|
"<if test='status != null'>" +
|
||||||
"AND r.status = #{status}" +
|
"AND r.status = #{status} " +
|
||||||
"</if>" +
|
"</if>" +
|
||||||
"<if test='startTime != null and startTime != \"\"'>" +
|
"<if test='startTime != null and startTime != \"\"'>" +
|
||||||
"AND r.claim_time >= #{startTime}" +
|
"AND r.claim_time >= #{startTime} " +
|
||||||
"</if>" +
|
"</if>" +
|
||||||
"<if test='endTime != null and endTime != \"\"'>" +
|
"<if test='endTime != null and endTime != \"\"'>" +
|
||||||
"AND r.claim_time <= #{endTime}" +
|
"AND r.claim_time <= #{endTime} " +
|
||||||
"</if>" +
|
"</if>" +
|
||||||
"<if test='scenicId != null and scenicId != \"\"'>" +
|
"<if test='scenicId != null and scenicId != \"\"'>" +
|
||||||
"AND r.scenic_id = #{scenicId}" +
|
"AND r.scenic_id = #{scenicId} " +
|
||||||
"</if>" +
|
"</if>" +
|
||||||
"</where>" +
|
"</where>" +
|
||||||
"ORDER BY r.create_time DESC" +
|
"ORDER BY r.create_time DESC" +
|
||||||
|
|||||||
@@ -36,39 +36,62 @@ public class AutoCouponServiceImpl implements IAutoCouponService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 查找该景区、该商品类型的首次打印优惠券配置
|
// 2. 查找该景区、该商品类型的所有首次打印优惠券配置
|
||||||
Long couponId = findFirstCouponId(scenicId, productType);
|
List<Long> couponIds = findFirstCouponIds(scenicId, productType);
|
||||||
if (couponId == null) {
|
if (couponIds == null || couponIds.isEmpty()) {
|
||||||
log.debug("景区未配置首次打印优惠券: scenicId={}, productType={}", scenicId, productType);
|
log.debug("景区未配置首次打印优惠券: scenicId={}, productType={}", scenicId, productType);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 检查用户是否已领取过该券(领券即消耗首次资格)
|
log.info("找到{}张首次优惠券待发放: scenicId={}, productType={}, couponIds={}",
|
||||||
PriceCouponClaimRecord existingRecord = couponClaimRecordMapper.selectUserCouponRecord(
|
couponIds.size(), scenicId, productType, couponIds);
|
||||||
memberId,
|
|
||||||
couponId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (existingRecord != null) {
|
// 3. 遍历所有优惠券,逐一检查并发放
|
||||||
log.debug("用户已领取过首次优惠券,不重复发券: memberId={}, couponId={}, claimTime={}",
|
int successCount = 0;
|
||||||
memberId, couponId, existingRecord.getClaimTime());
|
int skipCount = 0;
|
||||||
return false;
|
int failCount = 0;
|
||||||
|
|
||||||
|
for (Long couponId : couponIds) {
|
||||||
|
try {
|
||||||
|
// 检查用户是否已领取过该券(领券即消耗首次资格)
|
||||||
|
PriceCouponClaimRecord existingRecord = couponClaimRecordMapper.selectUserCouponRecord(
|
||||||
|
memberId,
|
||||||
|
couponId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingRecord != null) {
|
||||||
|
log.debug("用户已领取过优惠券,跳过: memberId={}, couponId={}, claimTime={}",
|
||||||
|
memberId, couponId, existingRecord.getClaimTime());
|
||||||
|
skipCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动发券
|
||||||
|
CouponClaimRequest request = new CouponClaimRequest(
|
||||||
|
memberId,
|
||||||
|
couponId,
|
||||||
|
scenicId.toString(),
|
||||||
|
"AUTO_GRANT" // 标记为自动发券来源
|
||||||
|
);
|
||||||
|
|
||||||
|
couponService.claimCoupon(request);
|
||||||
|
successCount++;
|
||||||
|
|
||||||
|
log.info("成功自动发放首次优惠券: memberId={}, faceId={}, scenicId={}, productType={}, couponId={}",
|
||||||
|
memberId, faceId, scenicId, productType, couponId);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
failCount++;
|
||||||
|
log.error("单张优惠券发放失败,继续处理其他券: memberId={}, couponId={}, error={}",
|
||||||
|
memberId, couponId, e.getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 自动发券
|
log.info("自动发券完成: memberId={}, 成功{}张, 跳过{}张, 失败{}张",
|
||||||
CouponClaimRequest request = new CouponClaimRequest(
|
memberId, successCount, skipCount, failCount);
|
||||||
memberId,
|
|
||||||
couponId,
|
|
||||||
scenicId.toString(),
|
|
||||||
"AUTO_GRANT" // 标记为自动发券来源
|
|
||||||
);
|
|
||||||
|
|
||||||
couponService.claimCoupon(request);
|
// 只要有一张成功就返回true
|
||||||
|
return successCount > 0;
|
||||||
log.info("成功自动发放首次打印优惠券: memberId={}, faceId={}, scenicId={}, productType={}, couponId={}",
|
|
||||||
memberId, faceId, scenicId, productType, couponId);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("自动发券失败: memberId={}, faceId={}, scenicId={}, productType={}",
|
log.error("自动发券失败: memberId={}, faceId={}, scenicId={}, productType={}",
|
||||||
@@ -78,14 +101,15 @@ public class AutoCouponServiceImpl implements IAutoCouponService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查找指定景区、指定商品类型的首次打印优惠券ID
|
* 查找指定景区、指定商品类型的所有首次打印优惠券ID
|
||||||
* 规则:优惠券名称包含 "首次" 且 适用商品类型包含目标类型
|
* 规则:优惠券名称包含 "首次" 且 适用商品类型包含目标类型
|
||||||
*
|
*
|
||||||
* @param scenicId 景区ID
|
* @param scenicId 景区ID
|
||||||
* @param productType 商品类型
|
* @param productType 商品类型
|
||||||
* @return 优惠券ID,未找到返回null
|
* @return 优惠券ID列表,未找到返回空列表
|
||||||
*/
|
*/
|
||||||
private Long findFirstCouponId(Long scenicId, ProductType productType) {
|
private List<Long> findFirstCouponIds(Long scenicId, ProductType productType) {
|
||||||
|
List<Long> couponIds = new java.util.ArrayList<>();
|
||||||
try {
|
try {
|
||||||
// 查询该景区的有效优惠券
|
// 查询该景区的有效优惠券
|
||||||
List<PriceCouponConfig> coupons = couponConfigMapper.selectValidCouponsByScenicId(
|
List<PriceCouponConfig> coupons = couponConfigMapper.selectValidCouponsByScenicId(
|
||||||
@@ -100,17 +124,22 @@ public class AutoCouponServiceImpl implements IAutoCouponService {
|
|||||||
String applicableProducts = coupon.getApplicableProducts();
|
String applicableProducts = coupon.getApplicableProducts();
|
||||||
if (applicableProducts != null &&
|
if (applicableProducts != null &&
|
||||||
applicableProducts.contains(productType.getCode())) {
|
applicableProducts.contains(productType.getCode())) {
|
||||||
return coupon.getId();
|
couponIds.add(coupon.getId());
|
||||||
|
log.debug("找到匹配的首次优惠券: couponId={}, couponName={}, scenicId={}, productType={}",
|
||||||
|
coupon.getId(), coupon.getCouponName(), scenicId, productType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug("未找到匹配的首次打印优惠券: scenicId={}, productType={}", scenicId, productType);
|
if (couponIds.isEmpty()) {
|
||||||
return null;
|
log.debug("未找到匹配的首次打印优惠券: scenicId={}, productType={}", scenicId, productType);
|
||||||
|
}
|
||||||
|
|
||||||
|
return couponIds;
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("查找首次打印优惠券失败: scenicId={}, productType={}", scenicId, productType, e);
|
log.error("查找首次打印优惠券失败: scenicId={}, productType={}", scenicId, productType, e);
|
||||||
return null;
|
return couponIds;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
package com.ycwl.basic.puzzle.fill.datasource;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.ycwl.basic.mapper.SourceMapper;
|
||||||
|
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
|
||||||
|
import com.ycwl.basic.puzzle.fill.enums.DataSourceType;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备缩略图数据源策略
|
||||||
|
* 根据deviceIndex指定第N个设备的缩略图
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class DeviceThumbImageDataSourceStrategy implements DataSourceStrategy {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SourceMapper sourceMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String resolve(JsonNode sourceFilter, String sortStrategy, DataSourceContext context) {
|
||||||
|
try {
|
||||||
|
// 默认type=2(图片)
|
||||||
|
Integer type = 2;
|
||||||
|
if (sourceFilter != null && sourceFilter.has("type")) {
|
||||||
|
type = sourceFilter.get("type").asInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取deviceIndex
|
||||||
|
Integer deviceIndex = 0;
|
||||||
|
if (sourceFilter != null && sourceFilter.has("deviceIndex")) {
|
||||||
|
deviceIndex = sourceFilter.get("deviceIndex").asInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用默认策略
|
||||||
|
if (sortStrategy == null || sortStrategy.isEmpty()) {
|
||||||
|
sortStrategy = "LATEST";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 检查是否有过滤后的机位列表
|
||||||
|
Map<String, Object> extra = context.getExtra();
|
||||||
|
if (extra != null && extra.containsKey("filteredDeviceIds")) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<Long> filteredDeviceIds = (List<Long>) extra.get("filteredDeviceIds");
|
||||||
|
|
||||||
|
if (filteredDeviceIds != null && !filteredDeviceIds.isEmpty()) {
|
||||||
|
// 使用过滤后的机位列表
|
||||||
|
if (deviceIndex >= filteredDeviceIds.size()) {
|
||||||
|
log.warn("deviceIndex[{}]超出过滤后的机位列表范围, 最大索引={}",
|
||||||
|
deviceIndex, filteredDeviceIds.size() - 1);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Long targetDeviceId = filteredDeviceIds.get(deviceIndex);
|
||||||
|
log.debug("使用过滤后的机位列表, deviceIndex={}, targetDeviceId={}",
|
||||||
|
deviceIndex, targetDeviceId);
|
||||||
|
|
||||||
|
SourceEntity source = sourceMapper.getSourceByFaceAndDeviceId(
|
||||||
|
context.getFaceId(),
|
||||||
|
targetDeviceId,
|
||||||
|
type,
|
||||||
|
sortStrategy
|
||||||
|
);
|
||||||
|
|
||||||
|
if (source != null) {
|
||||||
|
String thumbUrl = source.getThumbUrl();
|
||||||
|
log.debug("解析DEVICE_THUMB_IMAGE成功(过滤模式), faceId={}, deviceId={}, type={}, thumbUrl={}",
|
||||||
|
context.getFaceId(), targetDeviceId, type, thumbUrl);
|
||||||
|
return thumbUrl;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 降级到原有逻辑(使用deviceIndex直接查询)
|
||||||
|
SourceEntity source = sourceMapper.getSourceByFaceAndDeviceIndex(
|
||||||
|
context.getFaceId(),
|
||||||
|
deviceIndex,
|
||||||
|
type,
|
||||||
|
sortStrategy
|
||||||
|
);
|
||||||
|
|
||||||
|
if (source != null) {
|
||||||
|
String thumbUrl = source.getThumbUrl();
|
||||||
|
log.debug("解析DEVICE_THUMB_IMAGE成功(索引模式), faceId={}, deviceIndex={}, type={}, thumbUrl={}",
|
||||||
|
context.getFaceId(), deviceIndex, type, thumbUrl);
|
||||||
|
return thumbUrl;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("解析DEVICE_THUMB_IMAGE异常, faceId={}", context.getFaceId(), e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getSupportedType() {
|
||||||
|
return DataSourceType.DEVICE_THUMB_IMAGE.getCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@ public enum DataSourceType {
|
|||||||
* 设备图片(根据deviceIndex指定第N个设备的图片)
|
* 设备图片(根据deviceIndex指定第N个设备的图片)
|
||||||
*/
|
*/
|
||||||
DEVICE_IMAGE("DEVICE_IMAGE", "设备图片"),
|
DEVICE_IMAGE("DEVICE_IMAGE", "设备图片"),
|
||||||
|
DEVICE_THUMB_IMAGE("DEVICE_THUMB_IMAGE", "设备缩略图片"),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 静态值(直接使用fallbackValue)
|
* 静态值(直接使用fallbackValue)
|
||||||
|
|||||||
@@ -97,6 +97,17 @@ public class DeviceRepository {
|
|||||||
return device;
|
return device;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取设备基本信息(直接返回DeviceV2DTO)
|
||||||
|
*
|
||||||
|
* @param deviceId 设备ID
|
||||||
|
* @return DeviceV2DTO实例
|
||||||
|
*/
|
||||||
|
public DeviceV2DTO getDeviceBasic(Long deviceId) {
|
||||||
|
log.debug("获取设备基本信息, deviceId: {}", deviceId);
|
||||||
|
return deviceIntegrationService.getDevice(deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
public DeviceEntity getDeviceByDeviceNo(String deviceNo) {
|
public DeviceEntity getDeviceByDeviceNo(String deviceNo) {
|
||||||
log.debug("根据设备编号获取设备信息, deviceNo: {}", deviceNo);
|
log.debug("根据设备编号获取设备信息, deviceNo: {}", deviceNo);
|
||||||
DeviceV2DTO deviceDto = deviceIntegrationService.getDeviceByNo(deviceNo);
|
DeviceV2DTO deviceDto = deviceIntegrationService.getDeviceByNo(deviceNo);
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ public class OrderRepository {
|
|||||||
public static final String ORDER_CACHE_KEY = "order:%s";
|
public static final String ORDER_CACHE_KEY = "order:%s";
|
||||||
public static final String ORDER_ITEMS_CACHE_KEY = "order:%s:items";
|
public static final String ORDER_ITEMS_CACHE_KEY = "order:%s:items";
|
||||||
public static final String ORDER_ITEM_CACHE_KEY = "order:item:%s";
|
public static final String ORDER_ITEM_CACHE_KEY = "order:item:%s";
|
||||||
public static final String ORDER_USER_TYPE_BUY_ITEM_CACHE_KEY = "order:user:%s:type:%s:id:%s";
|
public static final String ORDER_USER_FACE_TYPE_BUY_ITEM_CACHE_KEY = "order:user:%s:face:%s:type:%s:id:%s";
|
||||||
|
|
||||||
public OrderEntity getOrder(Long orderId) {
|
public OrderEntity getOrder(Long orderId) {
|
||||||
if (redisTemplate.hasKey(String.format(ORDER_CACHE_KEY, orderId))) {
|
if (redisTemplate.hasKey(String.format(ORDER_CACHE_KEY, orderId))) {
|
||||||
@@ -62,40 +62,12 @@ public class OrderRepository {
|
|||||||
return orderItemEntity;
|
return orderItemEntity;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean checkUserBuyItem(Long userId, int goodsType, Long goodsId) {
|
|
||||||
synchronized (this) {
|
|
||||||
if (redisTemplate.hasKey(String.format(ORDER_USER_TYPE_BUY_ITEM_CACHE_KEY, userId, goodsType, goodsId))) {
|
|
||||||
return "1".equals(redisTemplate.opsForValue().get(String.format(ORDER_USER_TYPE_BUY_ITEM_CACHE_KEY, userId, goodsType, goodsId)));
|
|
||||||
}
|
|
||||||
OrderEntity orderEntity = orderMapper.getUserBuyItem(userId, goodsType, goodsId);
|
|
||||||
if (orderEntity == null) {
|
|
||||||
redisTemplate.opsForValue().set(String.format(ORDER_USER_TYPE_BUY_ITEM_CACHE_KEY, userId, goodsType, goodsId), "0", 60, TimeUnit.SECONDS);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (Integer.valueOf(1).equals(orderEntity.getStatus())) {
|
|
||||||
redisTemplate.opsForValue().set(String.format(ORDER_USER_TYPE_BUY_ITEM_CACHE_KEY, userId, goodsType, goodsId), "1");
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
redisTemplate.opsForValue().set(String.format(ORDER_USER_TYPE_BUY_ITEM_CACHE_KEY, userId, goodsType, goodsId), "0", 60, TimeUnit.SECONDS);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public OrderEntity getUserBuyItem(Long userId, int goodsType, Long goodsId) {
|
|
||||||
return orderMapper.getUserBuyItem(userId, goodsType, goodsId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void clearUserBuyItemCache(Long userId, int goodsType, Long goodsId) {
|
|
||||||
redisTemplate.delete(String.format(ORDER_USER_TYPE_BUY_ITEM_CACHE_KEY, userId, goodsType, goodsId));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void clearOrderCache(Long orderId) {
|
public void clearOrderCache(Long orderId) {
|
||||||
OrderEntity order = getOrder(orderId);
|
OrderEntity order = getOrder(orderId);
|
||||||
redisTemplate.delete(String.format(ORDER_CACHE_KEY, orderId));
|
redisTemplate.delete(String.format(ORDER_CACHE_KEY, orderId));
|
||||||
getOrderItems(orderId).forEach(orderItem -> {
|
getOrderItems(orderId).forEach(orderItem -> {
|
||||||
redisTemplate.delete(String.format(ORDER_ITEM_CACHE_KEY, orderItem.getId()));
|
redisTemplate.delete(String.format(ORDER_ITEM_CACHE_KEY, orderItem.getId()));
|
||||||
clearUserBuyItemCache(order.getMemberId(), orderItem.getGoodsType(), orderItem.getGoodsId());
|
clearUserBuyFaceItemCache(order.getMemberId(), order.getFaceId(), orderItem.getGoodsType(), orderItem.getGoodsId());
|
||||||
});
|
});
|
||||||
redisTemplate.delete(String.format(ORDER_ITEMS_CACHE_KEY, orderId));
|
redisTemplate.delete(String.format(ORDER_ITEMS_CACHE_KEY, orderId));
|
||||||
}
|
}
|
||||||
@@ -105,4 +77,37 @@ public class OrderRepository {
|
|||||||
orderMapper.updateOrder(updateEntity);
|
orderMapper.updateOrder(updateEntity);
|
||||||
clearOrderCache(orderId);
|
clearOrderCache(orderId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查用户是否购买了指定商品,并额外校验订单的faceId是否匹配
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param faceId 人脸ID
|
||||||
|
* @param goodsType 商品类型
|
||||||
|
* @param goodsId 商品ID
|
||||||
|
* @return 是否已购买且faceId匹配
|
||||||
|
*/
|
||||||
|
public boolean checkUserBuyFaceItem(Long userId, Long faceId, int goodsType, Long goodsId) {
|
||||||
|
synchronized (this) {
|
||||||
|
String cacheKey = String.format(ORDER_USER_FACE_TYPE_BUY_ITEM_CACHE_KEY, userId, faceId, goodsType, goodsId);
|
||||||
|
if (redisTemplate.hasKey(cacheKey)) {
|
||||||
|
return "1".equals(redisTemplate.opsForValue().get(cacheKey));
|
||||||
|
}
|
||||||
|
OrderEntity orderEntity = orderMapper.getUserBuyFaceItem(userId, faceId, goodsType, goodsId);
|
||||||
|
if (orderEntity == null) {
|
||||||
|
redisTemplate.opsForValue().set(cacheKey, "0", 60, TimeUnit.SECONDS);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (Integer.valueOf(1).equals(orderEntity.getStatus())) {
|
||||||
|
redisTemplate.opsForValue().set(cacheKey, "1");
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
redisTemplate.opsForValue().set(cacheKey, "0", 60, TimeUnit.SECONDS);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearUserBuyFaceItemCache(Long userId, Long faceId, int goodsType, Long goodsId) {
|
||||||
|
redisTemplate.delete(String.format(ORDER_USER_FACE_TYPE_BUY_ITEM_CACHE_KEY, userId, faceId, goodsType, goodsId));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,25 @@
|
|||||||
package com.ycwl.basic.repository;
|
package com.ycwl.basic.repository;
|
||||||
|
|
||||||
|
import com.ycwl.basic.image.enhancer.entity.BceEnhancerConfig;
|
||||||
|
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
||||||
|
import com.ycwl.basic.image.pipeline.enums.ImageSource;
|
||||||
|
import com.ycwl.basic.image.pipeline.enums.ImageType;
|
||||||
|
import com.ycwl.basic.image.pipeline.enums.PipelineScene;
|
||||||
|
import com.ycwl.basic.image.pipeline.stages.*;
|
||||||
|
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
|
||||||
import com.ycwl.basic.mapper.SourceMapper;
|
import com.ycwl.basic.mapper.SourceMapper;
|
||||||
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
||||||
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
|
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
|
||||||
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
|
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
|
||||||
import com.ycwl.basic.model.pc.device.entity.DeviceEntity;
|
import com.ycwl.basic.model.pc.device.entity.DeviceEntity;
|
||||||
|
import com.ycwl.basic.pipeline.core.Pipeline;
|
||||||
|
import com.ycwl.basic.pipeline.core.PipelineBuilder;
|
||||||
import com.ycwl.basic.pricing.dto.VoucherInfo;
|
import com.ycwl.basic.pricing.dto.VoucherInfo;
|
||||||
import com.ycwl.basic.pricing.enums.VoucherDiscountType;
|
import com.ycwl.basic.pricing.enums.VoucherDiscountType;
|
||||||
import com.ycwl.basic.pricing.service.IVoucherService;
|
import com.ycwl.basic.pricing.service.IVoucherService;
|
||||||
|
import com.ycwl.basic.storage.StorageFactory;
|
||||||
|
import com.ycwl.basic.storage.adapters.IStorageAdapter;
|
||||||
|
import com.ycwl.basic.storage.exceptions.StorageUnsupportedException;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
@@ -16,18 +28,29 @@ import org.springframework.stereotype.Component;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
public class SourceRepository {
|
public class SourceRepository {
|
||||||
|
private static final ExecutorService IMAGE_PROCESS_EXECUTOR = Executors.newFixedThreadPool(
|
||||||
|
Runtime.getRuntime().availableProcessors(),
|
||||||
|
runnable -> {
|
||||||
|
Thread thread = new Thread(runnable);
|
||||||
|
thread.setName("ai-cam-image-processor-" + thread.getId());
|
||||||
|
thread.setDaemon(true);
|
||||||
|
return thread;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private SourceMapper sourceMapper;
|
private SourceMapper sourceMapper;
|
||||||
@Autowired
|
@Autowired
|
||||||
private RedisTemplate<String, String> redisTemplate;
|
private RedisTemplate<String, String> redisTemplate;
|
||||||
@Autowired
|
@Autowired
|
||||||
private IVoucherService iVoucherService;
|
|
||||||
@Autowired
|
|
||||||
private FaceRepository faceRepository;
|
private FaceRepository faceRepository;
|
||||||
@Autowired
|
@Autowired
|
||||||
private TemplateRepository templateRepository;
|
private TemplateRepository templateRepository;
|
||||||
@@ -35,12 +58,21 @@ public class SourceRepository {
|
|||||||
private DeviceRepository deviceRepository;
|
private DeviceRepository deviceRepository;
|
||||||
@Autowired
|
@Autowired
|
||||||
private MemberRelationRepository memberRelationRepository;
|
private MemberRelationRepository memberRelationRepository;
|
||||||
|
@Autowired
|
||||||
|
private ScenicRepository scenicRepository;
|
||||||
|
|
||||||
public void addSource(SourceEntity source) {
|
public void addSource(SourceEntity source) {
|
||||||
sourceMapper.add(source);
|
sourceMapper.add(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setUserIsBuyItem(Long memberId, int type, Long faceId, Long orderId) {
|
public void setUserIsBuyItem(Long memberId, int type, Long faceId, Long orderId) {
|
||||||
|
// 如果是AI相机照片类型(type=13),需要进行图像超分和增强处理
|
||||||
|
boolean needsImageProcessing = (type == 13 || type == 3);
|
||||||
|
|
||||||
|
if (type == 13) {
|
||||||
|
type = 3; // compact
|
||||||
|
}
|
||||||
|
|
||||||
MemberSourceEntity memberSource = new MemberSourceEntity();
|
MemberSourceEntity memberSource = new MemberSourceEntity();
|
||||||
memberSource.setMemberId(memberId);
|
memberSource.setMemberId(memberId);
|
||||||
memberSource.setFaceId(faceId);
|
memberSource.setFaceId(faceId);
|
||||||
@@ -49,6 +81,143 @@ public class SourceRepository {
|
|||||||
memberSource.setIsBuy(1);
|
memberSource.setIsBuy(1);
|
||||||
sourceMapper.updateRelation(memberSource);
|
sourceMapper.updateRelation(memberSource);
|
||||||
memberRelationRepository.clearSCacheByFace(faceId);
|
memberRelationRepository.clearSCacheByFace(faceId);
|
||||||
|
|
||||||
|
// 如果需要图像处理,对该faceId下的所有type=3的照片进行处理
|
||||||
|
if (needsImageProcessing) {
|
||||||
|
processAiCamImages(faceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
redisTemplate.delete("order_content_not_downloadable_" + orderId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理AI相机照片 - 对照片进行超分辨率和增强处理
|
||||||
|
*
|
||||||
|
* @param faceId 人脸ID
|
||||||
|
*/
|
||||||
|
private void processAiCamImages(Long faceId) {
|
||||||
|
try {
|
||||||
|
// 1. 获取该faceId下所有type=3的照片
|
||||||
|
List<SourceEntity> aiCamImages = sourceMapper.listAiCamImageByFaceRelation(faceId);
|
||||||
|
|
||||||
|
if (aiCamImages == null || aiCamImages.isEmpty()) {
|
||||||
|
log.info("没有找到需要处理的AI相机照片, faceId: {}", faceId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("开始处理AI相机照片, faceId: {}, 照片数量: {}", faceId, aiCamImages.size());
|
||||||
|
|
||||||
|
// 2. 构建图像处理配置
|
||||||
|
BceEnhancerConfig config = buildEnhancerConfig();
|
||||||
|
|
||||||
|
// 3. 并发处理所有照片
|
||||||
|
List<CompletableFuture<Void>> futures = aiCamImages.stream()
|
||||||
|
.map(source -> CompletableFuture.runAsync(() -> {
|
||||||
|
try {
|
||||||
|
processSingleAiCamImage(source, config);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("处理AI相机照片失败, sourceId: {}, error: {}", source.getId(), e.getMessage(), e);
|
||||||
|
// 继续处理下一张照片,不中断整个流程
|
||||||
|
}
|
||||||
|
}, IMAGE_PROCESS_EXECUTOR))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
// 4. 等待所有任务完成
|
||||||
|
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
|
||||||
|
|
||||||
|
log.info("AI相机照片处理完成, faceId: {}", faceId);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("处理AI相机照片整体流程失败, faceId: {}, error: {}", faceId, e.getMessage(), e);
|
||||||
|
// 即使处理失败也不抛出异常,不影响订单购买流程
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理单张AI相机照片
|
||||||
|
*
|
||||||
|
* @param source 原始照片
|
||||||
|
* @param config 增强配置
|
||||||
|
*/
|
||||||
|
private void processSingleAiCamImage(SourceEntity source, BceEnhancerConfig config) {
|
||||||
|
// 1. 创建处理上下文
|
||||||
|
PhotoProcessContext context = PhotoProcessContext.builder()
|
||||||
|
.processId("aicam-" + source.getId())
|
||||||
|
.originalUrl(source.getUrl())
|
||||||
|
.scenicId(source.getScenicId())
|
||||||
|
.imageType(ImageType.NORMAL_PHOTO)
|
||||||
|
.source(ImageSource.IPC)
|
||||||
|
.scene(PipelineScene.AI_CAM_ENHANCE)
|
||||||
|
.build();
|
||||||
|
context.enableStage("image_sr");
|
||||||
|
context.enableStage("image_enhance");
|
||||||
|
ScenicConfigManager configManager = scenicRepository.getScenicConfigManager(source.getScenicId());
|
||||||
|
IStorageAdapter adapter;
|
||||||
|
try {
|
||||||
|
adapter = StorageFactory.get(configManager.getString("store_type"));
|
||||||
|
adapter.loadConfig(configManager.getObject("store_config_json", Map.class));
|
||||||
|
} catch (StorageUnsupportedException ignored) {
|
||||||
|
adapter = StorageFactory.use("assets-ext");
|
||||||
|
}
|
||||||
|
context.setStorageAdapter(adapter);
|
||||||
|
|
||||||
|
// 2. 设置结果URL回调 - 更新source记录
|
||||||
|
context.setResultUrlCallback(newUrl -> {
|
||||||
|
SourceEntity updateEntity = new SourceEntity();
|
||||||
|
updateEntity.setId(source.getId());
|
||||||
|
updateEntity.setUrl(newUrl);
|
||||||
|
sourceMapper.update(updateEntity);
|
||||||
|
log.info("已更新AI相机照片URL, sourceId: {}, oldUrl: {}, newUrl: {}",
|
||||||
|
source.getId(), source.getUrl(), newUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. 构建处理管线: 下载 -> 超分 -> 增强 -> 上传 -> 清理
|
||||||
|
Pipeline<PhotoProcessContext> pipeline = new PipelineBuilder<PhotoProcessContext>("AiCamEnhancePipeline")
|
||||||
|
.addStage(new DownloadStage()) // 下载原图
|
||||||
|
// .addStage(new ImageSRStage(config)) // 图像超分辨率
|
||||||
|
.addStage(new ImageEnhanceStage(config)) // 图像增强
|
||||||
|
.addStage(new UploadStage()) // 上传处理后的图片
|
||||||
|
.addStage(new CleanupStage()) // 清理临时文件
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 4. 执行管线
|
||||||
|
boolean success = pipeline.execute(context);
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
log.warn("AI相机照片处理管线执行失败, sourceId: {}", source.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建图像增强配置
|
||||||
|
*
|
||||||
|
* @return 增强配置
|
||||||
|
*/
|
||||||
|
private BceEnhancerConfig buildEnhancerConfig() {
|
||||||
|
BceEnhancerConfig config = new BceEnhancerConfig();
|
||||||
|
|
||||||
|
// 尝试从环境变量读取
|
||||||
|
String appId = System.getenv("BCE_IMAGE_APP_ID");
|
||||||
|
String apiKey = System.getenv("BCE_IMAGE_API_KEY");
|
||||||
|
String secretKey = System.getenv("BCE_IMAGE_SECRET_KEY");
|
||||||
|
|
||||||
|
// 如果环境变量没有配置,使用默认值(与PrinterServiceImpl保持一致)
|
||||||
|
if (appId == null || appId.isBlank()) {
|
||||||
|
appId = "119554288";
|
||||||
|
}
|
||||||
|
if (apiKey == null || apiKey.isBlank()) {
|
||||||
|
apiKey = "OX6QoijgKio3eVtA0PiUVf7f";
|
||||||
|
}
|
||||||
|
if (secretKey == null || secretKey.isBlank()) {
|
||||||
|
secretKey = "dYatXReVriPeiktTjUblhfubpcmYfuMk";
|
||||||
|
}
|
||||||
|
|
||||||
|
config.setAppId(appId);
|
||||||
|
config.setApiKey(apiKey);
|
||||||
|
config.setSecretKey(secretKey);
|
||||||
|
config.setQps(1.0f);
|
||||||
|
|
||||||
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setUserNotBuyItem(Long memberId, int type, Long faceId) {
|
public void setUserNotBuyItem(Long memberId, int type, Long faceId) {
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
package com.ycwl.basic.repository;
|
package com.ycwl.basic.repository;
|
||||||
|
|
||||||
import com.ycwl.basic.biz.PriceBiz;
|
import com.ycwl.basic.biz.PriceBiz;
|
||||||
import com.ycwl.basic.model.mobile.order.IsBuyBatchRespVO;
|
|
||||||
import com.ycwl.basic.pricing.dto.VoucherInfo;
|
|
||||||
import com.ycwl.basic.pricing.enums.VoucherDiscountType;
|
|
||||||
import com.ycwl.basic.pricing.service.IVoucherService;
|
import com.ycwl.basic.pricing.service.IVoucherService;
|
||||||
import com.ycwl.basic.utils.JacksonUtil;
|
import com.ycwl.basic.utils.JacksonUtil;
|
||||||
import com.ycwl.basic.mapper.VideoMapper;
|
import com.ycwl.basic.mapper.VideoMapper;
|
||||||
@@ -116,4 +113,25 @@ public class VideoRepository {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setUserIsBuyTemplate(Long memberId, Long templateId, Long orderId, Long faceId) {
|
||||||
|
List<MemberVideoEntity> videoEntities = memberRelationRepository.listRelationByFace(faceId);
|
||||||
|
for (MemberVideoEntity videoEntity : videoEntities) {
|
||||||
|
if (videoEntity.getTemplateId() != null && videoEntity.getTemplateId().equals(templateId)) {
|
||||||
|
MemberVideoEntity memberVideo = new MemberVideoEntity();
|
||||||
|
memberVideo.setVideoId(videoEntity.getVideoId());
|
||||||
|
memberVideo.setMemberId(memberId);
|
||||||
|
memberVideo.setIsBuy(1);
|
||||||
|
memberVideo.setOrderId(orderId);
|
||||||
|
videoMapper.updateRelation(memberVideo);
|
||||||
|
|
||||||
|
// 清理视频关系缓存
|
||||||
|
MemberVideoEntity existingVideo = videoMapper.queryUserVideo(memberId, videoEntity.getVideoId());
|
||||||
|
if (existingVideo != null && existingVideo.getFaceId() != null) {
|
||||||
|
memberRelationRepository.clearVCacheByFace(existingVideo.getFaceId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ public class ZTSourceDataService {
|
|||||||
entity.setDeviceId(message.getDeviceId());
|
entity.setDeviceId(message.getDeviceId());
|
||||||
entity.setUrl(message.getSourceUrl()); // 使用sourceUrl,不使用缩略图
|
entity.setUrl(message.getSourceUrl()); // 使用sourceUrl,不使用缩略图
|
||||||
entity.setThumbUrl(message.getThumbnailUrl()); // 设置缩略图URL
|
entity.setThumbUrl(message.getThumbnailUrl()); // 设置缩略图URL
|
||||||
entity.setType(2); // 照片类型
|
entity.setType(message.getSourceType()); // 照片类型
|
||||||
|
|
||||||
// 人脸样本ID处理
|
// 人脸样本ID处理
|
||||||
entity.setFaceSampleId(message.getFaceSampleId());
|
entity.setFaceSampleId(message.getFaceSampleId());
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.ycwl.basic.service.mobile;
|
||||||
|
|
||||||
|
import com.ycwl.basic.model.mobile.face.FaceRecognizeResp;
|
||||||
|
import com.ycwl.basic.model.mobile.goods.GoodsDetailVO;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI相机相关服务
|
||||||
|
*/
|
||||||
|
public interface AppAiCamService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据faceId获取AI相机识别到的商品列表
|
||||||
|
* @param faceId 人脸ID
|
||||||
|
* @return 商品详情列表
|
||||||
|
*/
|
||||||
|
List<GoodsDetailVO> getAiCamGoodsByFaceId(Long faceId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量添加会员与source的关联关系
|
||||||
|
* @param faceId 人脸ID
|
||||||
|
* @param sourceIds source ID列表
|
||||||
|
* @return 添加成功的数量
|
||||||
|
*/
|
||||||
|
int addMemberSourceRelations(Long faceId, List<Long> sourceIds);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用人脸样本创建或获取Face记录
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param faceSampleId 人脸样本ID
|
||||||
|
* @return 人脸识别响应
|
||||||
|
*/
|
||||||
|
FaceRecognizeResp useSample(Long userId, Long faceSampleId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.ycwl.basic.service.mobile;
|
||||||
|
|
||||||
|
import com.ycwl.basic.model.mobile.chat.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface FaceChatService {
|
||||||
|
/**
|
||||||
|
* 获取或创建人脸会话,一脸一会话。
|
||||||
|
*/
|
||||||
|
ChatConversationVO getOrCreateConversation(Long faceId, Long memberId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步发送消息并保存助手回复。
|
||||||
|
*/
|
||||||
|
ChatSendMessageResp sendMessage(Long conversationId, Long memberId, String content, String traceId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流式发送消息,支持实时分片回调,仍返回完整结果。
|
||||||
|
*/
|
||||||
|
ChatSendMessageStreamResp sendMessageStream(Long conversationId,
|
||||||
|
Long memberId,
|
||||||
|
String content,
|
||||||
|
String traceId,
|
||||||
|
java.util.function.Consumer<String> chunkConsumer);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拉取历史消息,cursor 为最后一条 seq,limit 为条数。
|
||||||
|
*/
|
||||||
|
ChatMessagePageResp listMessages(Long conversationId, Integer cursor, Integer limit, Long memberId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭会话。
|
||||||
|
*/
|
||||||
|
void closeConversation(Long conversationId, Long memberId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,383 @@
|
|||||||
|
package com.ycwl.basic.service.mobile.impl;
|
||||||
|
|
||||||
|
import com.ycwl.basic.exception.BaseException;
|
||||||
|
import com.ycwl.basic.facebody.FaceBodyFactory;
|
||||||
|
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
|
||||||
|
import com.ycwl.basic.facebody.entity.SearchFaceResp;
|
||||||
|
import com.ycwl.basic.facebody.entity.SearchFaceResultItem;
|
||||||
|
import com.ycwl.basic.integration.common.manager.DeviceConfigManager;
|
||||||
|
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
|
||||||
|
import com.ycwl.basic.mapper.FaceDetectLogAiCamMapper;
|
||||||
|
import com.ycwl.basic.mapper.FaceMapper;
|
||||||
|
import com.ycwl.basic.mapper.FaceSampleMapper;
|
||||||
|
import com.ycwl.basic.mapper.SourceMapper;
|
||||||
|
import com.ycwl.basic.model.mobile.face.FaceRecognizeResp;
|
||||||
|
import com.ycwl.basic.model.mobile.goods.GoodsDetailVO;
|
||||||
|
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
||||||
|
import com.ycwl.basic.model.pc.face.resp.FaceRespVO;
|
||||||
|
import com.ycwl.basic.model.pc.faceDetectLog.entity.FaceDetectLogAiCamEntity;
|
||||||
|
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
|
||||||
|
import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity;
|
||||||
|
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
|
||||||
|
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
|
||||||
|
import com.ycwl.basic.repository.DeviceRepository;
|
||||||
|
import com.ycwl.basic.repository.ScenicRepository;
|
||||||
|
import com.ycwl.basic.service.mobile.AppAiCamService;
|
||||||
|
import com.ycwl.basic.service.pc.FaceDetectLogAiCamService;
|
||||||
|
import com.ycwl.basic.service.pc.FaceService;
|
||||||
|
import com.ycwl.basic.service.pc.ScenicService;
|
||||||
|
import com.ycwl.basic.utils.JacksonUtil;
|
||||||
|
import com.ycwl.basic.utils.SnowFlakeUtil;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI相机相关服务实现
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AppAiCamServiceImpl implements AppAiCamService {
|
||||||
|
|
||||||
|
private final FaceDetectLogAiCamMapper faceDetectLogAiCamMapper;
|
||||||
|
private final SourceMapper sourceMapper;
|
||||||
|
private final FaceMapper faceMapper;
|
||||||
|
private final DeviceRepository deviceRepository;
|
||||||
|
private final FaceSampleMapper faceSampleMapper;
|
||||||
|
|
||||||
|
private static final float DEFAULT_SCORE_THRESHOLD = 0.8f;
|
||||||
|
private static final int DEFAULT_PHOTO_LIMIT = 10;
|
||||||
|
private static final int AI_CAM_SOURCE_TYPE = 3;
|
||||||
|
private final FaceService faceService;
|
||||||
|
private final FaceDetectLogAiCamService faceDetectLogAiCamService;
|
||||||
|
private final ScenicService scenicService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<GoodsDetailVO> getAiCamGoodsByFaceId(Long faceId) {
|
||||||
|
// 1. 查询该faceId的所有识别记录
|
||||||
|
List<FaceDetectLogAiCamEntity> detectLogs = faceDetectLogAiCamMapper.listByFaceId(faceId);
|
||||||
|
if (detectLogs == null || detectLogs.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 按设备分组并根据设备配置过滤faceSampleId
|
||||||
|
Map<Long, List<Long>> deviceFaceSampleMap = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
// 按设备分组识别记录
|
||||||
|
Map<Long, List<FaceDetectLogAiCamEntity>> deviceLogsMap = detectLogs.stream()
|
||||||
|
.collect(Collectors.groupingBy(FaceDetectLogAiCamEntity::getDeviceId));
|
||||||
|
|
||||||
|
// 遍历每个设备的识别记录
|
||||||
|
for (Map.Entry<Long, List<FaceDetectLogAiCamEntity>> entry : deviceLogsMap.entrySet()) {
|
||||||
|
Long deviceId = entry.getKey();
|
||||||
|
List<FaceDetectLogAiCamEntity> deviceLogs = entry.getValue();
|
||||||
|
|
||||||
|
// 获取设备配置
|
||||||
|
DeviceConfigManager configManager = deviceRepository.getDeviceConfigManager(deviceId);
|
||||||
|
|
||||||
|
// 获取该设备的分数阈值(百分制,需要转换为0-1)
|
||||||
|
float scoreThreshold = DEFAULT_SCORE_THRESHOLD;
|
||||||
|
if (configManager != null) {
|
||||||
|
Float thresholdPercent = configManager.getFloat("ai_cam_face_score_threshold");
|
||||||
|
if (thresholdPercent != null) {
|
||||||
|
scoreThreshold = thresholdPercent / 100.0f;
|
||||||
|
log.debug("设备{}使用配置的分数阈值: {}% ({})", deviceId, thresholdPercent, scoreThreshold);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取该设备的照片数量限制
|
||||||
|
int photoLimit = DEFAULT_PHOTO_LIMIT;
|
||||||
|
if (configManager != null) {
|
||||||
|
Integer limit = configManager.getInteger("ai_cam_photo_limit");
|
||||||
|
if (limit != null && limit > 0) {
|
||||||
|
photoLimit = limit;
|
||||||
|
log.debug("设备{}使用配置的照片限制: {}", deviceId, photoLimit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取该设备的时间范围限制(分钟)
|
||||||
|
Integer timeRangeMin = null;
|
||||||
|
if (configManager != null) {
|
||||||
|
Integer range = configManager.getInteger("ai_cam_time_range_min");
|
||||||
|
if (range != null && range > 0) {
|
||||||
|
timeRangeMin = range;
|
||||||
|
log.debug("设备{}使用配置的时间范围: {}分钟", deviceId, timeRangeMin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 收集该设备符合阈值的faceSampleId,同时记录分数和时间信息用于后续过滤
|
||||||
|
class DetectResult {
|
||||||
|
Long faceSampleId;
|
||||||
|
Float score;
|
||||||
|
Date detectTime;
|
||||||
|
|
||||||
|
DetectResult(Long faceSampleId, Float score, Date detectTime) {
|
||||||
|
this.faceSampleId = faceSampleId;
|
||||||
|
this.score = score;
|
||||||
|
this.detectTime = detectTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<DetectResult> detectResults = new ArrayList<>();
|
||||||
|
for (FaceDetectLogAiCamEntity detectLog : deviceLogs) {
|
||||||
|
if (detectLog.getMatchRawResult() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
SearchFaceResp resp = JacksonUtil.parseObject(detectLog.getMatchRawResult(), SearchFaceResp.class);
|
||||||
|
if (resp != null && resp.getResult() != null) {
|
||||||
|
for (SearchFaceResultItem item : resp.getResult()) {
|
||||||
|
// 使用设备配置的分数阈值
|
||||||
|
if (item.getScore() != null && item.getScore() >= scoreThreshold && item.getExtData() != null) {
|
||||||
|
try {
|
||||||
|
Long faceSampleId = Long.parseLong(item.getExtData());
|
||||||
|
detectResults.add(new DetectResult(
|
||||||
|
faceSampleId,
|
||||||
|
item.getScore(),
|
||||||
|
detectLog.getCreateTime()
|
||||||
|
));
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
log.warn("解析faceSampleId失败: extData={}", item.getExtData());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("解析matchRawResult失败: logId={}", detectLog.getId(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用照片数量限制(保留前N个)
|
||||||
|
if (detectResults.size() > photoLimit) {
|
||||||
|
log.debug("设备{}的照片数量{}超过限制{},截取前{}张",
|
||||||
|
deviceId, detectResults.size(), photoLimit, photoLimit);
|
||||||
|
detectResults = detectResults.subList(0, photoLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用时间范围限制
|
||||||
|
List<Long> deviceFaceSampleIds;
|
||||||
|
if (timeRangeMin != null && !detectResults.isEmpty()) {
|
||||||
|
// 找到分数最高的照片
|
||||||
|
DetectResult highestScoreResult = detectResults.stream()
|
||||||
|
.max(Comparator.comparing(r -> r.score))
|
||||||
|
.orElse(null);
|
||||||
|
|
||||||
|
if (highestScoreResult != null && highestScoreResult.detectTime != null) {
|
||||||
|
Date baseTime = highestScoreResult.detectTime;
|
||||||
|
long halfRangeMillis = (long) timeRangeMin * 60 * 1000 / 2;
|
||||||
|
Date startTime = new Date(baseTime.getTime() - halfRangeMillis);
|
||||||
|
Date endTime = new Date(baseTime.getTime() + halfRangeMillis);
|
||||||
|
|
||||||
|
// 过滤出时间范围内的照片
|
||||||
|
List<DetectResult> filteredResults = detectResults.stream()
|
||||||
|
.filter(r -> r.detectTime != null
|
||||||
|
&& !r.detectTime.before(startTime)
|
||||||
|
&& !r.detectTime.after(endTime))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
log.debug("设备{}应用时间范围{}分钟过滤: {}张 -> {}张 (基准时间: {})",
|
||||||
|
deviceId, timeRangeMin, detectResults.size(), filteredResults.size(), baseTime);
|
||||||
|
|
||||||
|
deviceFaceSampleIds = filteredResults.stream()
|
||||||
|
.map(r -> r.faceSampleId)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
} else {
|
||||||
|
// 没有时间信息,不过滤
|
||||||
|
deviceFaceSampleIds = detectResults.stream()
|
||||||
|
.map(r -> r.faceSampleId)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 不限制时间范围
|
||||||
|
deviceFaceSampleIds = detectResults.stream()
|
||||||
|
.map(r -> r.faceSampleId)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!deviceFaceSampleIds.isEmpty()) {
|
||||||
|
deviceFaceSampleMap.put(deviceId, deviceFaceSampleIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 合并所有设备的faceSampleId(去重)
|
||||||
|
Set<Long> faceSampleIds = new HashSet<>();
|
||||||
|
for (List<Long> ids : deviceFaceSampleMap.values()) {
|
||||||
|
faceSampleIds.addAll(ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (faceSampleIds.isEmpty()) {
|
||||||
|
log.debug("没有符合条件的faceSampleId, faceId={}", faceId);
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("人脸{}在{}个设备上识别到{}个不重复的faceSampleId",
|
||||||
|
faceId, deviceFaceSampleMap.size(), faceSampleIds.size());
|
||||||
|
|
||||||
|
// 4. 根据faceSampleId列表查询type=3的source记录
|
||||||
|
List<SourceEntity> sources = sourceMapper.listByFaceSampleIdsAndType(
|
||||||
|
new ArrayList<>(faceSampleIds), AI_CAM_SOURCE_TYPE
|
||||||
|
);
|
||||||
|
|
||||||
|
if (sources == null || sources.isEmpty()) {
|
||||||
|
log.debug("未找到type=3的source记录, faceId={}", faceId);
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("查询到{}条AI相机图像记录, faceId={}", sources.size(), faceId);
|
||||||
|
|
||||||
|
// 5. 查询Face信息以获取scenicId
|
||||||
|
FaceEntity face = faceMapper.get(faceId);
|
||||||
|
if (face == null) {
|
||||||
|
log.warn("Face不存在: faceId={}", faceId);
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 转换为GoodsDetailVO
|
||||||
|
return sources.stream().map(source -> {
|
||||||
|
GoodsDetailVO vo = new GoodsDetailVO();
|
||||||
|
vo.setFaceId(faceId);
|
||||||
|
vo.setScenicId(face.getScenicId());
|
||||||
|
vo.setGoodsType(2); // 2表示原素材
|
||||||
|
vo.setGoodsId(source.getId());
|
||||||
|
vo.setUrl(source.getUrl());
|
||||||
|
vo.setVideoUrl(source.getVideoUrl());
|
||||||
|
vo.setCreateTime(source.getCreateTime());
|
||||||
|
vo.setIsBuy(source.getIsBuy() != null ? source.getIsBuy() : 0);
|
||||||
|
return vo;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int addMemberSourceRelations(Long faceId, List<Long> sourceIds) {
|
||||||
|
if (sourceIds == null || sourceIds.isEmpty()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询Face信息
|
||||||
|
FaceEntity face = faceMapper.get(faceId);
|
||||||
|
if (face == null) {
|
||||||
|
throw new IllegalArgumentException("Face不存在: faceId=" + faceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (face.getMemberId() == null) {
|
||||||
|
throw new IllegalArgumentException("Face未关联会员: faceId=" + faceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除该faceId对应的旧的type=3关系
|
||||||
|
int deleted = sourceMapper.deleteRelationsByFaceIdAndType(faceId, AI_CAM_SOURCE_TYPE);
|
||||||
|
log.info("删除faceId={}的旧AI相机关联记录: {}条", faceId, deleted);
|
||||||
|
|
||||||
|
// 构建MemberSourceEntity列表
|
||||||
|
List<MemberSourceEntity> relations = sourceIds.stream().map(sourceId -> {
|
||||||
|
MemberSourceEntity entity = new MemberSourceEntity();
|
||||||
|
entity.setMemberId(face.getMemberId());
|
||||||
|
entity.setScenicId(face.getScenicId());
|
||||||
|
entity.setFaceId(faceId);
|
||||||
|
entity.setSourceId(sourceId);
|
||||||
|
entity.setType(AI_CAM_SOURCE_TYPE);
|
||||||
|
entity.setIsBuy(0);
|
||||||
|
entity.setIsFree(0);
|
||||||
|
return entity;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
|
||||||
|
// 批量插入
|
||||||
|
int inserted = sourceMapper.addRelations(relations);
|
||||||
|
log.info("为faceId={}添加新AI相机关联记录: {}条", faceId, inserted);
|
||||||
|
|
||||||
|
return inserted;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FaceRecognizeResp useSample(Long userId, Long faceSampleId) {
|
||||||
|
// 1. 查询 faceSample 获取其 URL
|
||||||
|
FaceSampleEntity faceSample = faceSampleMapper.getEntity(faceSampleId);
|
||||||
|
if (faceSample == null) {
|
||||||
|
throw new BaseException("人脸样本不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
String faceUrl = faceSample.getFaceUrl();
|
||||||
|
if (StringUtils.isBlank(faceUrl)) {
|
||||||
|
throw new BaseException("人脸样本URL为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
Long scenicId = faceSample.getScenicId();
|
||||||
|
|
||||||
|
// 2. 检查face数据库中有没有同用户、同URL的face记录
|
||||||
|
FaceEntity existingFace = null;
|
||||||
|
Long faceId = null;
|
||||||
|
|
||||||
|
// 查询该用户在该景区的所有人脸记录
|
||||||
|
List<FaceRespVO> userFaces = faceMapper.listByScenicAndUserId(scenicId, userId);
|
||||||
|
|
||||||
|
// 查找是否存在相同URL的记录
|
||||||
|
for (FaceRespVO faceResp : userFaces) {
|
||||||
|
if (faceUrl.equals(faceResp.getFaceUrl())) {
|
||||||
|
existingFace = faceMapper.get(faceResp.getId());
|
||||||
|
faceId = existingFace.getId();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 如果不存在,则新建一个face记录
|
||||||
|
if (existingFace == null) {
|
||||||
|
faceId = SnowFlakeUtil.getLongId();
|
||||||
|
FaceEntity newFace = new FaceEntity();
|
||||||
|
newFace.setId(faceId);
|
||||||
|
newFace.setCreateAt(new Date());
|
||||||
|
newFace.setScenicId(scenicId);
|
||||||
|
newFace.setMemberId(userId);
|
||||||
|
newFace.setFaceUrl(faceUrl);
|
||||||
|
faceMapper.add(newFace);
|
||||||
|
|
||||||
|
log.info("创建新的face记录, userId: {}, faceSampleId: {}, faceId: {}, faceUrl: {}",
|
||||||
|
userId, faceSampleId, faceId, faceUrl);
|
||||||
|
} else {
|
||||||
|
log.info("使用已存在的face记录, userId: {}, faceSampleId: {}, faceId: {}, faceUrl: {}",
|
||||||
|
userId, faceSampleId, faceId, faceUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 查询对应的 type=3 的 source 记录并自动添加关联
|
||||||
|
SourceEntity sourceEntity = sourceMapper.getBySampleIdAndType(faceSampleId, AI_CAM_SOURCE_TYPE);
|
||||||
|
if (sourceEntity != null && existingFace == null) {
|
||||||
|
// 检查是否已存在该source的关联
|
||||||
|
List<GoodsDetailVO> existingGoods = getAiCamGoodsByFaceId(faceId);
|
||||||
|
boolean alreadyExists = existingGoods.stream()
|
||||||
|
.anyMatch(item -> Objects.equals(item.getGoodsId(), sourceEntity.getId()));
|
||||||
|
|
||||||
|
if (!alreadyExists) {
|
||||||
|
// 添加关联
|
||||||
|
MemberSourceEntity relation = new MemberSourceEntity();
|
||||||
|
relation.setMemberId(userId);
|
||||||
|
relation.setScenicId(scenicId);
|
||||||
|
relation.setFaceId(faceId);
|
||||||
|
relation.setSourceId(sourceEntity.getId());
|
||||||
|
relation.setType(AI_CAM_SOURCE_TYPE);
|
||||||
|
relation.setIsBuy(0);
|
||||||
|
relation.setIsFree(0);
|
||||||
|
sourceMapper.addRelations(Collections.singletonList(relation));
|
||||||
|
log.info("自动添加AI相机照片关联: userId={}, faceId={}, sourceId={}",
|
||||||
|
userId, faceId, sourceEntity.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 返回结果
|
||||||
|
FaceRecognizeResp resp = new FaceRecognizeResp();
|
||||||
|
resp.setUrl(faceUrl);
|
||||||
|
resp.setFaceId(faceId);
|
||||||
|
resp.setScenicId(scenicId);
|
||||||
|
IFaceBodyAdapter faceBodyAdapter = scenicService.getScenicFaceBodyAdapter(scenicId);
|
||||||
|
try {
|
||||||
|
faceService.matchFaceId(faceId);
|
||||||
|
faceDetectLogAiCamService.searchAndLog(scenicId, faceId, faceUrl, faceBodyAdapter);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 人脸匹配失败不可以阻止正常流程
|
||||||
|
log.error("人脸匹配失败", e);
|
||||||
|
}
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
package com.ycwl.basic.service.mobile.impl;
|
||||||
|
|
||||||
|
import com.ycwl.basic.exception.BaseException;
|
||||||
|
import com.ycwl.basic.integration.glm.GlmClient;
|
||||||
|
import com.ycwl.basic.mapper.FaceChatConversationMapper;
|
||||||
|
import com.ycwl.basic.mapper.FaceChatMessageMapper;
|
||||||
|
import com.ycwl.basic.model.mobile.chat.*;
|
||||||
|
import com.ycwl.basic.model.mobile.chat.entity.FaceChatConversationEntity;
|
||||||
|
import com.ycwl.basic.model.mobile.chat.entity.FaceChatMessageEntity;
|
||||||
|
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
||||||
|
import com.ycwl.basic.repository.FaceRepository;
|
||||||
|
import com.ycwl.basic.service.mobile.FaceChatService;
|
||||||
|
import com.ycwl.basic.utils.SnowFlakeUtil;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import ai.z.openapi.service.model.ChatMessage;
|
||||||
|
import ai.z.openapi.service.model.ChatMessageRole;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class FaceChatServiceImpl implements FaceChatService {
|
||||||
|
|
||||||
|
private static final String STATUS_ACTIVE = "active";
|
||||||
|
private static final String STATUS_CLOSED = "closed";
|
||||||
|
private static final String ROLE_USER = "user";
|
||||||
|
private static final String ROLE_ASSISTANT = "assistant";
|
||||||
|
private static final String DEFAULT_MODEL = "glm-4.5-airx";
|
||||||
|
private static final int HISTORY_LIMIT = 50;
|
||||||
|
|
||||||
|
private final FaceChatConversationMapper conversationMapper;
|
||||||
|
private final FaceChatMessageMapper messageMapper;
|
||||||
|
private final FaceRepository faceRepository;
|
||||||
|
private final GlmClient glmClient;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChatConversationVO getOrCreateConversation(Long faceId, Long memberId) {
|
||||||
|
FaceChatConversationEntity exist = conversationMapper.findByFaceId(faceId);
|
||||||
|
if (exist != null) {
|
||||||
|
assertOwner(exist, memberId);
|
||||||
|
return toConversationVO(exist);
|
||||||
|
}
|
||||||
|
// DEBUG阶段,暂时不检查
|
||||||
|
// FaceEntity face = faceRepository.getFace(faceId);
|
||||||
|
// if (face == null) {
|
||||||
|
// throw new BaseException("人脸不存在");
|
||||||
|
// }
|
||||||
|
// if (!Objects.equals(face.getMemberId(), memberId)) {
|
||||||
|
// throw new BaseException("无权访问该人脸");
|
||||||
|
// }
|
||||||
|
FaceChatConversationEntity entity = new FaceChatConversationEntity();
|
||||||
|
entity.setId(SnowFlakeUtil.getLongId());
|
||||||
|
entity.setFaceId(faceId);
|
||||||
|
entity.setMemberId(memberId);
|
||||||
|
entity.setStatus(STATUS_ACTIVE);
|
||||||
|
entity.setModel(DEFAULT_MODEL);
|
||||||
|
conversationMapper.insert(entity);
|
||||||
|
return toConversationVO(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public ChatSendMessageResp sendMessage(Long conversationId, Long memberId, String content, String traceId) {
|
||||||
|
ChatSendMessageStreamResp result = doSend(conversationId, memberId, content, traceId, null);
|
||||||
|
ChatSendMessageResp resp = new ChatSendMessageResp();
|
||||||
|
resp.setUserMessage(result.getUserMessage());
|
||||||
|
resp.setAssistantMessage(result.getAssistantMessage());
|
||||||
|
resp.setTraceId(result.getTraceId());
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public ChatSendMessageStreamResp sendMessageStream(Long conversationId, Long memberId, String content, String traceId,
|
||||||
|
java.util.function.Consumer<String> chunkConsumer) {
|
||||||
|
return doSend(conversationId, memberId, content, traceId, chunkConsumer);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChatMessagePageResp listMessages(Long conversationId, Integer cursor, Integer limit, Long memberId) {
|
||||||
|
FaceChatConversationEntity conv = conversationMapper.getById(conversationId);
|
||||||
|
if (conv == null) {
|
||||||
|
throw new BaseException("会话不存在");
|
||||||
|
}
|
||||||
|
assertOwner(conv, memberId);
|
||||||
|
int pageSize = limit == null ? 50 : Math.max(1, Math.min(limit, 100));
|
||||||
|
List<FaceChatMessageEntity> list = messageMapper.listByConversation(conversationId, cursor, pageSize);
|
||||||
|
List<ChatMessageVO> vos = list.stream().map(this::toMessageVO).collect(Collectors.toList());
|
||||||
|
ChatMessagePageResp resp = new ChatMessagePageResp();
|
||||||
|
resp.setMessages(vos);
|
||||||
|
if (!list.isEmpty()) {
|
||||||
|
resp.setNextCursor(list.getLast().getSeq());
|
||||||
|
} else {
|
||||||
|
resp.setNextCursor(cursor == null ? 0 : cursor);
|
||||||
|
}
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void closeConversation(Long conversationId, Long memberId) {
|
||||||
|
FaceChatConversationEntity conv = conversationMapper.getById(conversationId);
|
||||||
|
if (conv == null) {
|
||||||
|
throw new BaseException("会话不存在");
|
||||||
|
}
|
||||||
|
assertOwner(conv, memberId);
|
||||||
|
if (STATUS_CLOSED.equals(conv.getStatus())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
conversationMapper.updateStatus(conversationId, STATUS_CLOSED);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChatSendMessageStreamResp doSend(Long conversationId, Long memberId, String content, String traceId,
|
||||||
|
java.util.function.Consumer<String> liveConsumer) {
|
||||||
|
if (StringUtils.isBlank(content)) {
|
||||||
|
throw new BaseException("消息内容不能为空");
|
||||||
|
}
|
||||||
|
FaceChatConversationEntity conv = conversationMapper.getById(conversationId);
|
||||||
|
if (conv == null) {
|
||||||
|
throw new BaseException("会话不存在");
|
||||||
|
}
|
||||||
|
assertOwner(conv, memberId);
|
||||||
|
if (STATUS_CLOSED.equals(conv.getStatus())) {
|
||||||
|
throw new BaseException("会话已关闭,请重新创建");
|
||||||
|
}
|
||||||
|
String resolvedTraceId = StringUtils.isBlank(traceId) ? UUID.randomUUID().toString() : traceId;
|
||||||
|
|
||||||
|
Integer maxSeq = messageMapper.maxSeqForUpdate(conversationId);
|
||||||
|
int baseSeq = maxSeq == null ? 0 : maxSeq;
|
||||||
|
int userSeq = baseSeq + 1;
|
||||||
|
|
||||||
|
FaceChatMessageEntity userMsg = buildMessage(conv, userSeq, ROLE_USER, content, resolvedTraceId, null);
|
||||||
|
messageMapper.insert(userMsg);
|
||||||
|
|
||||||
|
long start = System.currentTimeMillis();
|
||||||
|
List<FaceChatMessageEntity> recentDesc = messageMapper.listRecentByConversation(conversationId, HISTORY_LIMIT);
|
||||||
|
Collections.reverse(recentDesc); // 按时间升序
|
||||||
|
List<ChatMessage> chatMessages = recentDesc.stream()
|
||||||
|
.map(this::toChatMessage)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
CopyOnWriteArrayList<String> chunks = new CopyOnWriteArrayList<>();
|
||||||
|
java.util.function.Consumer<String> chunkConsumer = piece -> {
|
||||||
|
if (StringUtils.isNotBlank(piece)) {
|
||||||
|
chunks.add(piece);
|
||||||
|
if (liveConsumer != null) {
|
||||||
|
liveConsumer.accept(piece);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
String assistantText = glmClient.streamReply(conv.getFaceId(), memberId, resolvedTraceId, chatMessages, chunkConsumer);
|
||||||
|
if (StringUtils.isBlank(assistantText)) {
|
||||||
|
assistantText = "GLM 暂未接入,稍后再试。";
|
||||||
|
chunkConsumer.accept(assistantText);
|
||||||
|
}
|
||||||
|
int latency = (int) (System.currentTimeMillis() - start);
|
||||||
|
|
||||||
|
FaceChatMessageEntity assistantMsg = buildMessage(conv, userSeq + 1, ROLE_ASSISTANT, assistantText, resolvedTraceId, latency);
|
||||||
|
messageMapper.insert(assistantMsg);
|
||||||
|
|
||||||
|
ChatSendMessageStreamResp resp = new ChatSendMessageStreamResp();
|
||||||
|
resp.setUserMessage(toMessageVO(userMsg));
|
||||||
|
resp.setAssistantMessage(toMessageVO(assistantMsg));
|
||||||
|
resp.setTraceId(resolvedTraceId);
|
||||||
|
resp.setChunks(chunks);
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
private FaceChatMessageEntity buildMessage(FaceChatConversationEntity conv, int seq, String role, String content, String traceId, Integer latencyMs) {
|
||||||
|
FaceChatMessageEntity msg = new FaceChatMessageEntity();
|
||||||
|
msg.setId(SnowFlakeUtil.getLongId());
|
||||||
|
msg.setConversationId(conv.getId());
|
||||||
|
msg.setFaceId(conv.getFaceId());
|
||||||
|
msg.setSeq(seq);
|
||||||
|
msg.setRole(role);
|
||||||
|
msg.setContent(content);
|
||||||
|
msg.setTraceId(traceId);
|
||||||
|
msg.setLatencyMs(latencyMs);
|
||||||
|
msg.setCreatedAt(new Date());
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertOwner(FaceChatConversationEntity conv, Long memberId) {
|
||||||
|
if (!Objects.equals(conv.getMemberId(), memberId)) {
|
||||||
|
throw new BaseException("无权访问该会话");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChatConversationVO toConversationVO(FaceChatConversationEntity entity) {
|
||||||
|
ChatConversationVO vo = new ChatConversationVO();
|
||||||
|
vo.setConversationId(entity.getId());
|
||||||
|
vo.setFaceId(entity.getFaceId());
|
||||||
|
vo.setStatus(entity.getStatus());
|
||||||
|
vo.setModel(entity.getModel());
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChatMessageVO toMessageVO(FaceChatMessageEntity entity) {
|
||||||
|
ChatMessageVO vo = new ChatMessageVO();
|
||||||
|
vo.setId(entity.getId());
|
||||||
|
vo.setSeq(entity.getSeq());
|
||||||
|
vo.setRole(entity.getRole());
|
||||||
|
vo.setContent(entity.getContent());
|
||||||
|
vo.setTraceId(entity.getTraceId());
|
||||||
|
vo.setCreatedAt(entity.getCreatedAt());
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChatMessage toChatMessage(FaceChatMessageEntity entity) {
|
||||||
|
String role = entity.getRole();
|
||||||
|
String mappedRole = ChatMessageRole.USER.value();
|
||||||
|
if (ROLE_ASSISTANT.equalsIgnoreCase(role)) {
|
||||||
|
mappedRole = ChatMessageRole.ASSISTANT.value();
|
||||||
|
} else if ("system".equalsIgnoreCase(role)) {
|
||||||
|
mappedRole = ChatMessageRole.SYSTEM.value();
|
||||||
|
}
|
||||||
|
return ChatMessage.builder()
|
||||||
|
.role(mappedRole)
|
||||||
|
.content(entity.getContent())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -137,10 +137,14 @@ public class GoodsServiceImpl implements GoodsService {
|
|||||||
goodsDetailVO.setFaceId(sourceRespVO.getFaceId());
|
goodsDetailVO.setFaceId(sourceRespVO.getFaceId());
|
||||||
goodsDetailVO.setGoodsId(sourceRespVO.getId());
|
goodsDetailVO.setGoodsId(sourceRespVO.getId());
|
||||||
String shootingTime = DateUtil.format(sourceRespVO.getCreateTime(), "yyyy.MM.dd HH:mm:ss");
|
String shootingTime = DateUtil.format(sourceRespVO.getCreateTime(), "yyyy.MM.dd HH:mm:ss");
|
||||||
if (i < 10) {
|
if (Integer.valueOf(3).equals(sourceType)) {
|
||||||
goodsDetailVO.setGoodsName(goodsNamePrefix + "0" + i + " " + shootingTime);
|
goodsDetailVO.setGoodsName("拍摄时间:" + shootingTime);
|
||||||
} else {
|
} else {
|
||||||
goodsDetailVO.setGoodsName(goodsNamePrefix + i + " " + shootingTime);
|
if (i < 10) {
|
||||||
|
goodsDetailVO.setGoodsName(goodsNamePrefix + "0" + i + " " + shootingTime);
|
||||||
|
} else {
|
||||||
|
goodsDetailVO.setGoodsName(goodsNamePrefix + i + " " + shootingTime);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
goodsDetailVO.setScenicId(sourceRespVO.getScenicId());
|
goodsDetailVO.setScenicId(sourceRespVO.getScenicId());
|
||||||
try {
|
try {
|
||||||
@@ -573,7 +577,7 @@ public class GoodsServiceImpl implements GoodsService {
|
|||||||
if (query.getGoodsId() != null) {
|
if (query.getGoodsId() != null) {
|
||||||
list = list.stream().filter(source -> source.getId().equals(query.getGoodsId())).toList();
|
list = list.stream().filter(source -> source.getId().equals(query.getGoodsId())).toList();
|
||||||
}
|
}
|
||||||
if (!Integer.valueOf(2).equals(query.getSourceType())) {
|
if (Integer.valueOf(1).equals(query.getSourceType())) {
|
||||||
return list.stream().map(source -> {
|
return list.stream().map(source -> {
|
||||||
GoodsUrlVO goodsUrlVO = new GoodsUrlVO();
|
GoodsUrlVO goodsUrlVO = new GoodsUrlVO();
|
||||||
goodsUrlVO.setGoodsType(source.getType());
|
goodsUrlVO.setGoodsType(source.getType());
|
||||||
@@ -592,7 +596,7 @@ public class GoodsServiceImpl implements GoodsService {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}).count();
|
}).count();
|
||||||
IsBuyRespVO isBuy = orderBiz.isBuy(face.getScenicId(), face.getMemberId(), face.getId(), query.getSourceType(), face.getId());
|
IsBuyRespVO isBuy = orderBiz.isBuy(face.getScenicId(), face.getMemberId(), face.getId(), sourceType == 3 ? 13 : sourceType, face.getId());
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
if (!isBuy.isBuy()) {
|
if (!isBuy.isBuy()) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
@@ -829,6 +833,9 @@ public class GoodsServiceImpl implements GoodsService {
|
|||||||
} else if (type == 2) {
|
} else if (type == 2) {
|
||||||
goodsPageVO.setGoodsName("照片集");
|
goodsPageVO.setGoodsName("照片集");
|
||||||
goodsPageVO.setTemplateCoverUrl(scenicConfig.getString("photo_cover_url"));
|
goodsPageVO.setTemplateCoverUrl(scenicConfig.getString("photo_cover_url"));
|
||||||
|
} else if (type == 3) {
|
||||||
|
goodsPageVO.setGoodsName("AI微单");
|
||||||
|
goodsPageVO.setTemplateCoverUrl(scenicConfig.getString("ai_camera_cover_url"));
|
||||||
} else {
|
} else {
|
||||||
goodsPageVO.setGoodsName("未知商品");
|
goodsPageVO.setGoodsName("未知商品");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,11 +11,10 @@ public interface FaceDetectLogAiCamService {
|
|||||||
/**
|
/**
|
||||||
* 搜索人脸库并保存日志
|
* 搜索人脸库并保存日志
|
||||||
* @param scenicId 景区ID
|
* @param scenicId 景区ID
|
||||||
* @param deviceId 设备ID
|
* @param faceId 人脸样本ID
|
||||||
* @param faceSampleId 人脸样本ID
|
|
||||||
* @param faceUrl 人脸URL
|
* @param faceUrl 人脸URL
|
||||||
* @param adapter 人脸适配器
|
* @param adapter 人脸适配器
|
||||||
* @return 搜索结果
|
* @return 搜索结果
|
||||||
*/
|
*/
|
||||||
SearchFaceResp searchAndLog(Long scenicId, Long deviceId, Long faceSampleId, String faceUrl, IFaceBodyAdapter adapter);
|
void searchAndLog(Long scenicId, Long faceId, String faceUrl, IFaceBodyAdapter adapter);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ package com.ycwl.basic.service.pc.impl;
|
|||||||
|
|
||||||
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
|
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
|
||||||
import com.ycwl.basic.facebody.entity.SearchFaceResp;
|
import com.ycwl.basic.facebody.entity.SearchFaceResp;
|
||||||
|
import com.ycwl.basic.integration.device.dto.device.DeviceV2DTO;
|
||||||
import com.ycwl.basic.mapper.FaceDetectLogAiCamMapper;
|
import com.ycwl.basic.mapper.FaceDetectLogAiCamMapper;
|
||||||
import com.ycwl.basic.model.pc.faceDetectLog.entity.FaceDetectLogAiCamEntity;
|
import com.ycwl.basic.model.pc.faceDetectLog.entity.FaceDetectLogAiCamEntity;
|
||||||
|
import com.ycwl.basic.repository.DeviceRepository;
|
||||||
import com.ycwl.basic.service.pc.FaceDetectLogAiCamService;
|
import com.ycwl.basic.service.pc.FaceDetectLogAiCamService;
|
||||||
import com.ycwl.basic.utils.JacksonUtil;
|
import com.ycwl.basic.utils.JacksonUtil;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -11,6 +13,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@@ -18,9 +21,20 @@ import java.util.Date;
|
|||||||
public class FaceDetectLogAiCamServiceImpl implements FaceDetectLogAiCamService {
|
public class FaceDetectLogAiCamServiceImpl implements FaceDetectLogAiCamService {
|
||||||
|
|
||||||
private final FaceDetectLogAiCamMapper faceDetectLogAiCamMapper;
|
private final FaceDetectLogAiCamMapper faceDetectLogAiCamMapper;
|
||||||
|
private final DeviceRepository deviceRepository;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SearchFaceResp searchAndLog(Long scenicId, Long deviceId, Long faceSampleId, String faceUrl, IFaceBodyAdapter adapter) {
|
public void searchAndLog(Long scenicId, Long faceId, String faceUrl, IFaceBodyAdapter adapter) {
|
||||||
|
List<DeviceV2DTO> devices = deviceRepository.getAllDeviceByScenicId(scenicId);
|
||||||
|
for (DeviceV2DTO device : devices) {
|
||||||
|
if (!device.getType().equals("AI_CAM")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
searchDeviceAndLog(scenicId, device.getId(), faceId, faceUrl, adapter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SearchFaceResp searchDeviceAndLog(Long scenicId, Long deviceId, Long faceId, String faceUrl, IFaceBodyAdapter adapter) {
|
||||||
String dbName = "AiCam" + deviceId;
|
String dbName = "AiCam" + deviceId;
|
||||||
|
|
||||||
SearchFaceResp resp = null;
|
SearchFaceResp resp = null;
|
||||||
@@ -28,7 +42,7 @@ public class FaceDetectLogAiCamServiceImpl implements FaceDetectLogAiCamService
|
|||||||
// 调用适配器搜索人脸
|
// 调用适配器搜索人脸
|
||||||
resp = adapter.searchFace(dbName, faceUrl);
|
resp = adapter.searchFace(dbName, faceUrl);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("AI相机人脸搜索异常: scenicId={}, deviceId={}, faceSampleId={}", scenicId, deviceId, faceSampleId, e);
|
log.error("AI相机人脸搜索异常: scenicId={}, deviceId={}, faceId={}", scenicId, deviceId, faceId, e);
|
||||||
// 发生异常时记录空结果或错误信息,视业务需求而定。这里暂不中断流程,继续记录日志
|
// 发生异常时记录空结果或错误信息,视业务需求而定。这里暂不中断流程,继续记录日志
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,7 +51,7 @@ public class FaceDetectLogAiCamServiceImpl implements FaceDetectLogAiCamService
|
|||||||
FaceDetectLogAiCamEntity logEntity = new FaceDetectLogAiCamEntity();
|
FaceDetectLogAiCamEntity logEntity = new FaceDetectLogAiCamEntity();
|
||||||
logEntity.setScenicId(scenicId);
|
logEntity.setScenicId(scenicId);
|
||||||
logEntity.setDeviceId(deviceId);
|
logEntity.setDeviceId(deviceId);
|
||||||
logEntity.setFaceId(faceSampleId);
|
logEntity.setFaceId(faceId);
|
||||||
logEntity.setDbName(dbName);
|
logEntity.setDbName(dbName);
|
||||||
logEntity.setFaceUrl(faceUrl);
|
logEntity.setFaceUrl(faceUrl);
|
||||||
logEntity.setCreateTime(new Date());
|
logEntity.setCreateTime(new Date());
|
||||||
@@ -51,13 +65,13 @@ public class FaceDetectLogAiCamServiceImpl implements FaceDetectLogAiCamService
|
|||||||
// 记录原始响应
|
// 记录原始响应
|
||||||
logEntity.setMatchRawResult(JacksonUtil.toJSONString(resp));
|
logEntity.setMatchRawResult(JacksonUtil.toJSONString(resp));
|
||||||
} else {
|
} else {
|
||||||
logEntity.setMatchRawResult("{\"error\": \"search failed or exception\"}");
|
logEntity.setMatchRawResult("{\"error\": \"search failed or exception\"}");
|
||||||
}
|
}
|
||||||
|
|
||||||
faceDetectLogAiCamMapper.add(logEntity);
|
faceDetectLogAiCamMapper.add(logEntity);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("保存AI相机人脸识别日志失败: faceSampleId={}", faceSampleId, e);
|
log.error("保存AI相机人脸识别日志失败: faceId={}", faceId, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return resp;
|
return resp;
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import com.ycwl.basic.constant.BaseContextHandler;
|
|||||||
import com.ycwl.basic.exception.BaseException;
|
import com.ycwl.basic.exception.BaseException;
|
||||||
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
|
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
|
||||||
import com.ycwl.basic.facebody.entity.SearchFaceResultItem;
|
import com.ycwl.basic.facebody.entity.SearchFaceResultItem;
|
||||||
import com.ycwl.basic.integration.common.manager.DeviceConfigManager;
|
|
||||||
import com.ycwl.basic.mapper.FaceSampleMapper;
|
import com.ycwl.basic.mapper.FaceSampleMapper;
|
||||||
import com.ycwl.basic.mapper.ProjectMapper;
|
import com.ycwl.basic.mapper.ProjectMapper;
|
||||||
import com.ycwl.basic.mapper.SourceMapper;
|
import com.ycwl.basic.mapper.SourceMapper;
|
||||||
@@ -62,6 +61,7 @@ import com.ycwl.basic.repository.TemplateRepository;
|
|||||||
import com.ycwl.basic.repository.VideoRepository;
|
import com.ycwl.basic.repository.VideoRepository;
|
||||||
import com.ycwl.basic.repository.VideoTaskRepository;
|
import com.ycwl.basic.repository.VideoTaskRepository;
|
||||||
import com.ycwl.basic.service.mobile.GoodsService;
|
import com.ycwl.basic.service.mobile.GoodsService;
|
||||||
|
import com.ycwl.basic.service.pc.FaceDetectLogAiCamService;
|
||||||
import com.ycwl.basic.service.pc.FaceService;
|
import com.ycwl.basic.service.pc.FaceService;
|
||||||
import com.ycwl.basic.service.pc.ScenicService;
|
import com.ycwl.basic.service.pc.ScenicService;
|
||||||
import com.ycwl.basic.constant.SourceType;
|
import com.ycwl.basic.constant.SourceType;
|
||||||
@@ -71,7 +71,6 @@ import com.ycwl.basic.service.pc.helper.SearchResultMerger;
|
|||||||
import com.ycwl.basic.service.pc.helper.ScenicConfigFacade;
|
import com.ycwl.basic.service.pc.helper.ScenicConfigFacade;
|
||||||
import com.ycwl.basic.service.pc.orchestrator.FaceMatchingOrchestrator;
|
import com.ycwl.basic.service.pc.orchestrator.FaceMatchingOrchestrator;
|
||||||
import com.ycwl.basic.service.pc.processor.BuyStatusProcessor;
|
import com.ycwl.basic.service.pc.processor.BuyStatusProcessor;
|
||||||
import com.ycwl.basic.service.pc.processor.FaceRecoveryStrategy;
|
|
||||||
import com.ycwl.basic.service.pc.processor.SourceRelationProcessor;
|
import com.ycwl.basic.service.pc.processor.SourceRelationProcessor;
|
||||||
import com.ycwl.basic.service.pc.processor.VideoRecreationHandler;
|
import com.ycwl.basic.service.pc.processor.VideoRecreationHandler;
|
||||||
import com.ycwl.basic.service.pc.strategy.RematchContext;
|
import com.ycwl.basic.service.pc.strategy.RematchContext;
|
||||||
@@ -86,7 +85,8 @@ import com.ycwl.basic.storage.enums.StorageAcl;
|
|||||||
import com.ycwl.basic.storage.utils.StorageUtil;
|
import com.ycwl.basic.storage.utils.StorageUtil;
|
||||||
import com.ycwl.basic.utils.*;
|
import com.ycwl.basic.utils.*;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.logging.log4j.util.Strings;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.commons.lang3.Strings;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -193,6 +193,8 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
private IPriceCalculationService iPriceCalculationService;
|
private IPriceCalculationService iPriceCalculationService;
|
||||||
@Autowired
|
@Autowired
|
||||||
private PuzzleTemplateMapper puzzleTemplateMapper;
|
private PuzzleTemplateMapper puzzleTemplateMapper;
|
||||||
|
@Autowired
|
||||||
|
private FaceDetectLogAiCamService faceDetectLogAiCamService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ApiResponse<PageInfo<FaceRespVO>> pageQuery(FaceReqQuery faceReqQuery) {
|
public ApiResponse<PageInfo<FaceRespVO>> pageQuery(FaceReqQuery faceReqQuery) {
|
||||||
@@ -312,13 +314,16 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
Long finalFaceId = newFaceId;
|
Long finalFaceId = newFaceId;
|
||||||
Thread thread = new Thread(() -> printerService.autoAddPhotosToPreferPrint(finalFaceId), "auto-add-print-" + newFaceId);
|
Thread thread = new Thread(() -> printerService.autoAddPhotosToPreferPrint(finalFaceId), "auto-add-print-" + newFaceId);
|
||||||
thread.start();
|
thread.start();
|
||||||
if (org.apache.commons.lang3.Strings.CI.equals("print", scene)) {
|
if (Strings.CI.equals("print", scene)) {
|
||||||
try {
|
try {
|
||||||
thread.join();
|
thread.join();
|
||||||
} catch (InterruptedException ignore) {
|
} catch (InterruptedException ignore) {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (Strings.CI.equals("aiCam", scene)) {
|
||||||
|
faceDetectLogAiCamService.searchAndLog(scenicId, newFaceId, faceUrl, faceBodyAdapter);
|
||||||
|
}
|
||||||
|
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
@@ -468,9 +473,9 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
contentPageVO.setLockType(1);
|
contentPageVO.setLockType(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
boolean buy = orderBiz.checkUserBuyItem(userId, contentPageVO.getGoodsType(), contentPageVO.getContentId());
|
boolean buy = orderBiz.checkUserBuyFaceItem(userId, faceId, contentPageVO.getGoodsType(), contentPageVO.getContentId());
|
||||||
if (!buy) {
|
if (!buy) {
|
||||||
buy = orderBiz.checkUserBuyItem(userId, -1, contentPageVO.getTemplateId());
|
buy = orderBiz.checkUserBuyFaceItem(userId, faceId, -1, contentPageVO.getTemplateId());
|
||||||
}
|
}
|
||||||
if (buy) {
|
if (buy) {
|
||||||
contentPageVO.setIsBuy(1);
|
contentPageVO.setIsBuy(1);
|
||||||
@@ -482,21 +487,25 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
List<PuzzleTemplateEntity> puzzleTemplateEntityList = puzzleTemplateMapper.list(face.getScenicId(), null, 1);
|
List<PuzzleTemplateEntity> puzzleTemplateEntityList = puzzleTemplateMapper.list(face.getScenicId(), null, 1);
|
||||||
if (!puzzleTemplateEntityList.isEmpty()) {
|
if (!puzzleTemplateEntityList.isEmpty()) {
|
||||||
List<PuzzleGenerationRecordEntity> records = puzzleGenerationRecordMapper.listByFaceId(faceId);
|
List<PuzzleGenerationRecordEntity> records = puzzleGenerationRecordMapper.listByFaceId(faceId);
|
||||||
puzzleTemplateEntityList.forEach(template -> {
|
PuzzleTemplateEntity template = puzzleTemplateEntityList.getFirst();
|
||||||
Optional<PuzzleGenerationRecordEntity> optionalRecord = records.stream().filter(r -> r.getTemplateId().equals(template.getId())).findFirst();
|
Optional<PuzzleGenerationRecordEntity> optionalRecord = records.stream().filter(r -> r.getTemplateId().equals(template.getId())).findFirst();
|
||||||
ContentPageVO sfpContent = new ContentPageVO();
|
ContentPageVO sfpContent = new ContentPageVO();
|
||||||
sfpContent.setName(template.getName());
|
sfpContent.setName(template.getName());
|
||||||
sfpContent.setGroup("plog");
|
sfpContent.setGroup("plog");
|
||||||
sfpContent.setScenicId(face.getScenicId());
|
sfpContent.setScenicId(face.getScenicId());
|
||||||
sfpContent.setContentType(3);
|
sfpContent.setContentType(3);
|
||||||
sfpContent.setSourceType(3);
|
sfpContent.setSourceType(3);
|
||||||
sfpContent.setLockType(-1);
|
sfpContent.setLockType(-1);
|
||||||
sfpContent.setContentId(optionalRecord.map(PuzzleGenerationRecordEntity::getId).orElse(null));
|
sfpContent.setContentId(optionalRecord.map(PuzzleGenerationRecordEntity::getId).orElse(null));
|
||||||
sfpContent.setTemplateId(template.getId());
|
sfpContent.setTemplateId(template.getId());
|
||||||
sfpContent.setTemplateCoverUrl(template.getCoverImage());
|
sfpContent.setTemplateCoverUrl(template.getCoverImage());
|
||||||
sfpContent.setGoodsType(3);
|
sfpContent.setGoodsType(3);
|
||||||
sfpContent.setSort(0);
|
sfpContent.setSort(0);
|
||||||
if (optionalRecord.isPresent()) {
|
if (optionalRecord.isPresent()) {
|
||||||
|
IsBuyRespVO isBuyScenic = orderBiz.isBuy(face.getScenicId(), face.getMemberId(), faceId, 5, face.getScenicId());
|
||||||
|
if (isBuyScenic.isBuy()) {
|
||||||
|
sfpContent.setIsBuy(1);
|
||||||
|
} else {
|
||||||
IsBuyRespVO isBuyRespVO = orderBiz.isBuy(face.getScenicId(), face.getMemberId(), faceId, 5, optionalRecord.get().getTemplateId());
|
IsBuyRespVO isBuyRespVO = orderBiz.isBuy(face.getScenicId(), face.getMemberId(), faceId, 5, optionalRecord.get().getTemplateId());
|
||||||
if (isBuyRespVO.isBuy()) {
|
if (isBuyRespVO.isBuy()) {
|
||||||
sfpContent.setIsBuy(1);
|
sfpContent.setIsBuy(1);
|
||||||
@@ -504,24 +513,24 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
sfpContent.setIsBuy(0);
|
sfpContent.setIsBuy(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PriceCalculationRequest calculationRequest = new PriceCalculationRequest();
|
}
|
||||||
ProductItem productItem = new ProductItem();
|
PriceCalculationRequest calculationRequest = new PriceCalculationRequest();
|
||||||
productItem.setProductType(ProductType.PHOTO_LOG);
|
ProductItem productItem = new ProductItem();
|
||||||
productItem.setProductId(template.getId().toString());
|
productItem.setProductType(ProductType.PHOTO_LOG);
|
||||||
productItem.setPurchaseCount(1);
|
productItem.setProductId(template.getId().toString());
|
||||||
productItem.setScenicId(face.getScenicId().toString());
|
productItem.setPurchaseCount(1);
|
||||||
calculationRequest.setProducts(Collections.singletonList(productItem));
|
productItem.setScenicId(face.getScenicId().toString());
|
||||||
calculationRequest.setUserId(face.getMemberId());
|
calculationRequest.setProducts(Collections.singletonList(productItem));
|
||||||
calculationRequest.setFaceId(face.getId());
|
calculationRequest.setUserId(face.getMemberId());
|
||||||
calculationRequest.setPreviewOnly(true); // 仅查询价格,不实际使用优惠
|
calculationRequest.setFaceId(face.getId());
|
||||||
PriceCalculationResult calculationResult = iPriceCalculationService.calculatePrice(calculationRequest);
|
calculationRequest.setPreviewOnly(true); // 仅查询价格,不实际使用优惠
|
||||||
if (calculationResult.getFinalAmount().compareTo(BigDecimal.ZERO) > 0) {
|
PriceCalculationResult calculationResult = iPriceCalculationService.calculatePrice(calculationRequest);
|
||||||
sfpContent.setFreeCount(0);
|
if (calculationResult.getFinalAmount().compareTo(BigDecimal.ZERO) > 0) {
|
||||||
} else {
|
sfpContent.setFreeCount(0);
|
||||||
sfpContent.setFreeCount(1);
|
} else {
|
||||||
}
|
sfpContent.setFreeCount(1);
|
||||||
contentList.add(1, sfpContent);
|
}
|
||||||
});
|
contentList.add(1, sfpContent);
|
||||||
}
|
}
|
||||||
SourceReqQuery sourceReqQuery = new SourceReqQuery();
|
SourceReqQuery sourceReqQuery = new SourceReqQuery();
|
||||||
sourceReqQuery.setScenicId(face.getScenicId());
|
sourceReqQuery.setScenicId(face.getScenicId());
|
||||||
@@ -531,20 +540,29 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
List<SourceRespVO> sourceList = sourceMapper.queryByRelation(sourceReqQuery);
|
List<SourceRespVO> sourceList = sourceMapper.queryByRelation(sourceReqQuery);
|
||||||
ContentPageVO sourceVideoContent = new ContentPageVO();
|
ContentPageVO sourceVideoContent = new ContentPageVO();
|
||||||
ContentPageVO sourceImageContent = new ContentPageVO();
|
ContentPageVO sourceImageContent = new ContentPageVO();
|
||||||
|
ContentPageVO sourceAiCamContent = new ContentPageVO();
|
||||||
sourceVideoContent.setName("录像集");
|
sourceVideoContent.setName("录像集");
|
||||||
sourceImageContent.setName("照片集");
|
sourceImageContent.setName("照片集");
|
||||||
|
sourceAiCamContent.setName("AI微单");
|
||||||
sourceVideoContent.setSort(9999);
|
sourceVideoContent.setSort(9999);
|
||||||
sourceImageContent.setSort(9999);
|
sourceImageContent.setSort(9999);
|
||||||
|
sourceAiCamContent.setSort(9999);
|
||||||
sourceVideoContent.setScenicId(face.getScenicId());
|
sourceVideoContent.setScenicId(face.getScenicId());
|
||||||
sourceImageContent.setScenicId(face.getScenicId());
|
sourceImageContent.setScenicId(face.getScenicId());
|
||||||
|
sourceAiCamContent.setScenicId(face.getScenicId());
|
||||||
sourceVideoContent.setGoodsType(1);
|
sourceVideoContent.setGoodsType(1);
|
||||||
sourceImageContent.setGoodsType(2);
|
sourceImageContent.setGoodsType(2);
|
||||||
|
sourceAiCamContent.setGoodsType(3);
|
||||||
sourceVideoContent.setContentType(2);
|
sourceVideoContent.setContentType(2);
|
||||||
sourceImageContent.setContentType(2);
|
sourceImageContent.setContentType(2);
|
||||||
|
sourceAiCamContent.setContentType(2);
|
||||||
sourceVideoContent.setLockType(-1);
|
sourceVideoContent.setLockType(-1);
|
||||||
sourceImageContent.setLockType(-1);
|
sourceImageContent.setLockType(-1);
|
||||||
|
sourceAiCamContent.setLockType(-1);
|
||||||
sourceVideoContent.setGroup("直出原片");
|
sourceVideoContent.setGroup("直出原片");
|
||||||
sourceImageContent.setGroup("直出原片");
|
sourceImageContent.setGroup("直出原片");
|
||||||
|
sourceAiCamContent.setGroup("直出原片");
|
||||||
|
ScenicConfigManager configManager = scenicRepository.getScenicConfigManager(face.getScenicId());
|
||||||
if (!scenicConfigFacade.isDisableSourceImage(face.getScenicId())) {
|
if (!scenicConfigFacade.isDisableSourceImage(face.getScenicId())) {
|
||||||
IsBuyRespVO isBuyRespVO = orderBiz.isBuy(face.getScenicId(), userId, faceId, SourceType.IMAGE.getCode(), faceId);
|
IsBuyRespVO isBuyRespVO = orderBiz.isBuy(face.getScenicId(), userId, faceId, SourceType.IMAGE.getCode(), faceId);
|
||||||
sourceImageContent.setSourceType(isBuyRespVO.getGoodsType());
|
sourceImageContent.setSourceType(isBuyRespVO.getGoodsType());
|
||||||
@@ -583,18 +601,40 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
sourceVideoContent.setFreeCount((int) freeCount);
|
sourceVideoContent.setFreeCount((int) freeCount);
|
||||||
contentList.add(sourceVideoContent);
|
contentList.add(sourceVideoContent);
|
||||||
}
|
}
|
||||||
|
// AI微单:只有存在type=3的数据时才添加
|
||||||
|
boolean hasAiCam = sourceList.stream().anyMatch(source -> source.getType() == 3);
|
||||||
|
if (hasAiCam) {
|
||||||
|
IsBuyRespVO isBuyRespVO = orderBiz.isBuy(face.getScenicId(), userId, faceId, 13, faceId);
|
||||||
|
sourceAiCamContent.setSourceType(isBuyRespVO.getGoodsType());
|
||||||
|
sourceAiCamContent.setContentId(isBuyRespVO.getGoodsId());
|
||||||
|
if (isBuyRespVO.isBuy()) {
|
||||||
|
sourceAiCamContent.setIsBuy(1);
|
||||||
|
} else {
|
||||||
|
sourceAiCamContent.setIsBuy(0);
|
||||||
|
}
|
||||||
|
// AI微单有数据才显示,所以lockType固定为-1
|
||||||
|
sourceAiCamContent.setLockType(-1);
|
||||||
|
List<MemberSourceEntity> relations = memberRelationRepository.listSourceByFaceRelation(faceId, 3);
|
||||||
|
long freeCount = relations.stream().filter(entity -> Integer.valueOf(1).equals(entity.getIsFree())).count();
|
||||||
|
sourceAiCamContent.setFreeCount((int) freeCount);
|
||||||
|
contentList.add(sourceAiCamContent);
|
||||||
|
}
|
||||||
sourceList.stream().collect(Collectors.groupingBy(SourceRespVO::getType)).forEach((type, list) -> {
|
sourceList.stream().collect(Collectors.groupingBy(SourceRespVO::getType)).forEach((type, list) -> {
|
||||||
if (type == 1) {
|
if (type == 1) {
|
||||||
sourceVideoContent.setSourceType(1);
|
sourceVideoContent.setSourceType(1);
|
||||||
sourceVideoContent.setLockType(-1);
|
sourceVideoContent.setLockType(-1);
|
||||||
sourceVideoContent.setTemplateCoverUrl(list.getFirst().getUrl());
|
sourceVideoContent.setTemplateCoverUrl(list.getFirst().getUrl());
|
||||||
} else {
|
} else if (type == 2) {
|
||||||
sourceImageContent.setSourceType(2);
|
sourceImageContent.setSourceType(2);
|
||||||
sourceImageContent.setLockType(-1);
|
sourceImageContent.setLockType(-1);
|
||||||
sourceImageContent.setTemplateCoverUrl(list.getFirst().getUrl());
|
sourceImageContent.setTemplateCoverUrl(list.getFirst().getUrl());
|
||||||
if (Strings.isBlank(sourceVideoContent.getTemplateCoverUrl())) {
|
if (StringUtils.isBlank(sourceVideoContent.getTemplateCoverUrl())) {
|
||||||
sourceVideoContent.setTemplateCoverUrl(list.getFirst().getUrl());
|
sourceVideoContent.setTemplateCoverUrl(list.getFirst().getUrl());
|
||||||
}
|
}
|
||||||
|
} else if (type == 3) {
|
||||||
|
sourceAiCamContent.setSourceType(13);
|
||||||
|
sourceAiCamContent.setLockType(-1);
|
||||||
|
sourceAiCamContent.setTemplateCoverUrl(configManager.getString("ai_camera_cover_url"));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return contentList;
|
return contentList;
|
||||||
@@ -803,7 +843,7 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
String matchResult = face.getMatchResult();
|
String matchResult = face.getMatchResult();
|
||||||
if (matchResult == null || Strings.isBlank(matchResult)) {
|
if (matchResult == null || StringUtils.isBlank(matchResult)) {
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(face.getScenicId());
|
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(face.getScenicId());
|
||||||
@@ -1009,7 +1049,7 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
handleCustomFaceMatching(faceId, finalSampleList);
|
handleCustomFaceMatching(faceId, finalSampleList);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Strings.isNotBlank(req.getRemark())) {
|
if (StringUtils.isNotBlank(req.getRemark())) {
|
||||||
log.info("人脸识别人工调整备注:faceId={}, remark={}", faceId, req.getRemark());
|
log.info("人脸识别人工调整备注:faceId={}, remark={}", faceId, req.getRemark());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1035,7 +1075,7 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
detail.setLastMatchedAt(face.getUpdateAt() != null ? face.getUpdateAt() : face.getCreateAt());
|
detail.setLastMatchedAt(face.getUpdateAt() != null ? face.getUpdateAt() : face.getCreateAt());
|
||||||
|
|
||||||
String matchResultJson = face.getMatchResult();
|
String matchResultJson = face.getMatchResult();
|
||||||
if (Strings.isBlank(matchResultJson)) {
|
if (StringUtils.isBlank(matchResultJson)) {
|
||||||
detail.setAcceptedSamples(Collections.emptyList());
|
detail.setAcceptedSamples(Collections.emptyList());
|
||||||
detail.setFilteredSamples(Collections.emptyList());
|
detail.setFilteredSamples(Collections.emptyList());
|
||||||
return detail;
|
return detail;
|
||||||
@@ -1155,7 +1195,7 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private List<Long> parseMatchSampleIds(String matchSampleIds) {
|
private List<Long> parseMatchSampleIds(String matchSampleIds) {
|
||||||
if (Strings.isBlank(matchSampleIds)) {
|
if (StringUtils.isBlank(matchSampleIds)) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
String[] segments = matchSampleIds.split(",");
|
String[] segments = matchSampleIds.split(",");
|
||||||
@@ -1170,7 +1210,7 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Long parseLongSilently(String value) {
|
private Long parseLongSilently(String value) {
|
||||||
if (Strings.isBlank(value)) {
|
if (StringUtils.isBlank(value)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -1222,10 +1262,10 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
if (sourceEntity == null) {
|
if (sourceEntity == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (!Strings.isBlank(sourceEntity.getThumbUrl())) {
|
if (!StringUtils.isBlank(sourceEntity.getThumbUrl())) {
|
||||||
return sourceEntity.getThumbUrl();
|
return sourceEntity.getThumbUrl();
|
||||||
}
|
}
|
||||||
if (!Strings.isBlank(sourceEntity.getUrl())) {
|
if (!StringUtils.isBlank(sourceEntity.getUrl())) {
|
||||||
return sourceEntity.getUrl();
|
return sourceEntity.getUrl();
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -177,6 +177,9 @@ public class OrderServiceImpl implements OrderService {
|
|||||||
} else if (Integer.valueOf(2).equals(orderItemList.getFirst().getGoodsType())) {
|
} else if (Integer.valueOf(2).equals(orderItemList.getFirst().getGoodsType())) {
|
||||||
item.setGoodsName("照片集");
|
item.setGoodsName("照片集");
|
||||||
item.setOrderType("照片集");
|
item.setOrderType("照片集");
|
||||||
|
} else if (Integer.valueOf(13).equals(orderItemList.getFirst().getGoodsType())) {
|
||||||
|
item.setGoodsName("AI微单");
|
||||||
|
item.setOrderType("AI微单");
|
||||||
} else if (Integer.valueOf(0).equals(orderItemList.getFirst().getGoodsType())) {
|
} else if (Integer.valueOf(0).equals(orderItemList.getFirst().getGoodsType())) {
|
||||||
item.setOrderType("旅行Vlog");
|
item.setOrderType("旅行Vlog");
|
||||||
item.setGoodsName(orderItemList.getFirst().getGoodsName());
|
item.setGoodsName(orderItemList.getFirst().getGoodsName());
|
||||||
@@ -237,6 +240,9 @@ public class OrderServiceImpl implements OrderService {
|
|||||||
} else if (Integer.valueOf(2).equals(orderItemList.getFirst().getGoodsType())) {
|
} else if (Integer.valueOf(2).equals(orderItemList.getFirst().getGoodsType())) {
|
||||||
item.setGoodsName("照片集");
|
item.setGoodsName("照片集");
|
||||||
item.setOrderType("照片集");
|
item.setOrderType("照片集");
|
||||||
|
} else if (Integer.valueOf(13).equals(orderItemList.getFirst().getGoodsType())) {
|
||||||
|
item.setGoodsName("AI微单");
|
||||||
|
item.setOrderType("AI微单");
|
||||||
} else if (Integer.valueOf(0).equals(orderItemList.getFirst().getGoodsType())) {
|
} else if (Integer.valueOf(0).equals(orderItemList.getFirst().getGoodsType())) {
|
||||||
item.setOrderType("旅行Vlog");
|
item.setOrderType("旅行Vlog");
|
||||||
item.setGoodsName(orderItemList.getFirst().getGoodsName());
|
item.setGoodsName(orderItemList.getFirst().getGoodsName());
|
||||||
@@ -344,6 +350,26 @@ public class OrderServiceImpl implements OrderService {
|
|||||||
item.setShootingTime(memberVideoEntityList.getFirst().getCreateTime());
|
item.setShootingTime(memberVideoEntityList.getFirst().getCreateTime());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (Integer.valueOf(13).equals(item.getGoodsType())) { // AI相机照片 goodsId就是人脸ID
|
||||||
|
List<SourceEntity> aiCamImageList = sourceMapper.listAiCamImageByFaceRelation(item.getGoodsId());
|
||||||
|
item.setCoverList(aiCamImageList.stream().map(SourceEntity::getUrl).collect(Collectors.toList()));
|
||||||
|
if (!_f.contains(13)) {
|
||||||
|
_f.add(13);
|
||||||
|
if (!aiCamImageList.isEmpty()) {
|
||||||
|
for (SourceEntity sourceEntity : aiCamImageList) {
|
||||||
|
GoodsDetailVO goods = new GoodsDetailVO();
|
||||||
|
goods.setGoodsId(sourceEntity.getId());
|
||||||
|
goods.setGoodsName("AI相机照片");
|
||||||
|
goods.setUrl(sourceEntity.getUrl());
|
||||||
|
goods.setGoodsType(sourceEntity.getType());
|
||||||
|
goods.setScenicId(sourceEntity.getScenicId());
|
||||||
|
goods.setTemplateCoverUrl(sourceEntity.getUrl());
|
||||||
|
goods.setCreateTime(sourceEntity.getCreateTime());
|
||||||
|
goodsList.add(goods);
|
||||||
|
}
|
||||||
|
item.setShootingTime(aiCamImageList.getFirst().getCreateTime());
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (Integer.valueOf(3).equals(item.getGoodsType())) { // 打印照片 goodsId就是memberPrintId
|
} else if (Integer.valueOf(3).equals(item.getGoodsType())) { // 打印照片 goodsId就是memberPrintId
|
||||||
List<MemberPrintResp> list = printerMapper.getUserPhotoByIds(orderItemList.stream().map(OrderItemVO::getGoodsId).collect(Collectors.toList()));
|
List<MemberPrintResp> list = printerMapper.getUserPhotoByIds(orderItemList.stream().map(OrderItemVO::getGoodsId).collect(Collectors.toList()));
|
||||||
item.setCoverList(orderItemList.stream().map(OrderItemVO::getCoverUrl).collect(Collectors.toList()));
|
item.setCoverList(orderItemList.stream().map(OrderItemVO::getCoverUrl).collect(Collectors.toList()));
|
||||||
@@ -546,6 +572,13 @@ public class OrderServiceImpl implements OrderService {
|
|||||||
item.setShootingTime(memberVideoEntityList.getFirst().getCreateTime());
|
item.setShootingTime(memberVideoEntityList.getFirst().getCreateTime());
|
||||||
item.setCount(1);
|
item.setCount(1);
|
||||||
}
|
}
|
||||||
|
} else if (Integer.valueOf(13).equals(item.getGoodsType())) {
|
||||||
|
List<SourceEntity> aiCamImageList = sourceMapper.listAiCamImageByFaceRelation(item.getFaceId());
|
||||||
|
item.setCoverList(aiCamImageList.stream().map(SourceEntity::getUrl).collect(Collectors.toList()));
|
||||||
|
if (!aiCamImageList.isEmpty()) {
|
||||||
|
item.setShootingTime(aiCamImageList.getFirst().getCreateTime());
|
||||||
|
item.setCount(1);
|
||||||
|
}
|
||||||
} else if (Integer.valueOf(0).equals(item.getGoodsType())) {
|
} else if (Integer.valueOf(0).equals(item.getGoodsType())) {
|
||||||
item.setCoverList(Collections.singletonList(item.getCoverUrl()));
|
item.setCoverList(Collections.singletonList(item.getCoverUrl()));
|
||||||
VideoEntity video = videoRepository.getVideo(item.getGoodsId());
|
VideoEntity video = videoRepository.getVideo(item.getGoodsId());
|
||||||
@@ -682,6 +715,9 @@ public class OrderServiceImpl implements OrderService {
|
|||||||
} else if (Integer.valueOf(2).equals(orderItemList.getFirst().getGoodsType())) {
|
} else if (Integer.valueOf(2).equals(orderItemList.getFirst().getGoodsType())) {
|
||||||
item.setGoodsName("照片集");
|
item.setGoodsName("照片集");
|
||||||
item.setOrderType("照片集");
|
item.setOrderType("照片集");
|
||||||
|
} else if (Integer.valueOf(13).equals(orderItemList.getFirst().getGoodsType())) {
|
||||||
|
item.setGoodsName("打卡点拍照");
|
||||||
|
item.setOrderType("打卡点拍照");
|
||||||
} else if (Integer.valueOf(0).equals(orderItemList.getFirst().getGoodsType())) {
|
} else if (Integer.valueOf(0).equals(orderItemList.getFirst().getGoodsType())) {
|
||||||
item.setOrderType("旅行Vlog");
|
item.setOrderType("旅行Vlog");
|
||||||
item.setGoodsName(orderItemList.getFirst().getGoodsName());
|
item.setGoodsName(orderItemList.getFirst().getGoodsName());
|
||||||
@@ -880,15 +916,13 @@ public class OrderServiceImpl implements OrderService {
|
|||||||
List<OrderItemEntity> orderItems = goodsList.stream().map(goods -> {
|
List<OrderItemEntity> orderItems = goodsList.stream().map(goods -> {
|
||||||
OrderItemEntity orderItem = new OrderItemEntity();
|
OrderItemEntity orderItem = new OrderItemEntity();
|
||||||
orderItem.setOrderId(orderId);
|
orderItem.setOrderId(orderId);
|
||||||
if (Long.valueOf(1L).equals(goods.getGoodsId())) {
|
orderItem.setGoodsId(goods.getGoodsId());
|
||||||
|
orderItem.setGoodsType(goods.getGoodsType());
|
||||||
|
if (Integer.valueOf(1).equals(goods.getGoodsType())) {
|
||||||
orderItem.setGoodsId(batchOrderReqVO.getFaceId());
|
orderItem.setGoodsId(batchOrderReqVO.getFaceId());
|
||||||
orderItem.setGoodsType(1);
|
} else if (Integer.valueOf(2).equals(goods.getGoodsType())) {
|
||||||
} else if (Long.valueOf(2L).equals(goods.getGoodsId())) {
|
|
||||||
orderItem.setGoodsId(batchOrderReqVO.getFaceId());
|
orderItem.setGoodsId(batchOrderReqVO.getFaceId());
|
||||||
orderItem.setGoodsType(2);
|
} else if (Integer.valueOf(0).equals(goods.getGoodsType())) {
|
||||||
} else {
|
|
||||||
// templateId
|
|
||||||
orderItem.setGoodsId(goods.getGoodsId());
|
|
||||||
orderItem.setGoodsType(-1);
|
orderItem.setGoodsType(-1);
|
||||||
}
|
}
|
||||||
return orderItem;
|
return orderItem;
|
||||||
@@ -940,11 +974,13 @@ public class OrderServiceImpl implements OrderService {
|
|||||||
case PHOTO_SET -> 2;
|
case PHOTO_SET -> 2;
|
||||||
case VLOG_VIDEO -> 0;
|
case VLOG_VIDEO -> 0;
|
||||||
case RECORDING_SET -> 1;
|
case RECORDING_SET -> 1;
|
||||||
|
case AI_CAM_PHOTO_SET -> 13;
|
||||||
default -> 0;
|
default -> 0;
|
||||||
};
|
};
|
||||||
Long goodsId = switch (productItem.getProductType()) {
|
Long goodsId = switch (productItem.getProductType()) {
|
||||||
case PHOTO_LOG -> Long.valueOf(productItem.getProductId());
|
case PHOTO_LOG -> Long.valueOf(productItem.getProductId());
|
||||||
case PHOTO_SET, RECORDING_SET -> face.getId();
|
case PHOTO_SET, RECORDING_SET -> face.getId();
|
||||||
|
case AI_CAM_PHOTO_SET -> face.getId();
|
||||||
case VLOG_VIDEO -> {
|
case VLOG_VIDEO -> {
|
||||||
List<MemberVideoEntity> videos = memberRelationRepository.listRelationByFaceAndTemplate(face.getId(), Long.valueOf(productItem.getProductId()));
|
List<MemberVideoEntity> videos = memberRelationRepository.listRelationByFaceAndTemplate(face.getId(), Long.valueOf(productItem.getProductId()));
|
||||||
yield videos.getFirst().getVideoId();
|
yield videos.getFirst().getVideoId();
|
||||||
@@ -1017,6 +1053,13 @@ public class OrderServiceImpl implements OrderService {
|
|||||||
orderItem.setGoodsType(type);
|
orderItem.setGoodsType(type);
|
||||||
orderItem.setOrderId(order.getId());
|
orderItem.setOrderId(order.getId());
|
||||||
orderItems.add(orderItem);
|
orderItems.add(orderItem);
|
||||||
|
// ======== 兼容旧逻辑 ==========
|
||||||
|
if (order.getType() == 3) {
|
||||||
|
redisTemplate.opsForValue().set("order_content_not_downloadable_" + order.getId(), "1");
|
||||||
|
}
|
||||||
|
if (type == 13) {
|
||||||
|
redisTemplate.opsForValue().set("order_content_not_downloadable_" + order.getId(), "1");
|
||||||
|
}
|
||||||
// 在事务中保存订单数据
|
// 在事务中保存订单数据
|
||||||
try {
|
try {
|
||||||
self.saveOrderInTransaction(order, orderItems, haveOldOrder);
|
self.saveOrderInTransaction(order, orderItems, haveOldOrder);
|
||||||
|
|||||||
@@ -390,42 +390,51 @@ public class FaceMatchingOrchestrator {
|
|||||||
baseDynamicData.put("scenicText", scenicBasic.getName());
|
baseDynamicData.put("scenicText", scenicBasic.getName());
|
||||||
baseDynamicData.put("dateStr", DateUtil.format(new Date(), "yyyy.MM.dd"));
|
baseDynamicData.put("dateStr", DateUtil.format(new Date(), "yyyy.MM.dd"));
|
||||||
|
|
||||||
// 遍历所有模板,逐个生成
|
// 使用虚拟线程池并行生成所有模板
|
||||||
int successCount = 0;
|
java.util.concurrent.atomic.AtomicInteger successCount = new java.util.concurrent.atomic.AtomicInteger(0);
|
||||||
int failCount = 0;
|
java.util.concurrent.atomic.AtomicInteger failCount = new java.util.concurrent.atomic.AtomicInteger(0);
|
||||||
for (PuzzleTemplateDTO template : templateList) {
|
|
||||||
try {
|
|
||||||
log.info("开始生成拼图: scenicId={}, templateCode={}, templateName={}",
|
|
||||||
scenicId, template.getCode(), template.getName());
|
|
||||||
|
|
||||||
// 构建生成请求
|
try (java.util.concurrent.ExecutorService executor = java.util.concurrent.Executors.newVirtualThreadPerTaskExecutor()) {
|
||||||
PuzzleGenerateRequest generateRequest = new PuzzleGenerateRequest();
|
// 为每个模板创建一个异步任务
|
||||||
generateRequest.setScenicId(scenicId);
|
List<java.util.concurrent.CompletableFuture<Void>> futures = templateList.stream()
|
||||||
generateRequest.setUserId(memberId);
|
.map(template -> java.util.concurrent.CompletableFuture.runAsync(() -> {
|
||||||
generateRequest.setFaceId(faceId);
|
try {
|
||||||
generateRequest.setBusinessType("face_matching");
|
log.info("开始生成拼图: scenicId={}, templateCode={}, templateName={}",
|
||||||
generateRequest.setTemplateCode(template.getCode());
|
scenicId, template.getCode(), template.getName());
|
||||||
generateRequest.setOutputFormat("PNG");
|
|
||||||
generateRequest.setQuality(90);
|
|
||||||
generateRequest.setDynamicData(new HashMap<>(baseDynamicData));
|
|
||||||
generateRequest.setRequireRuleMatch(true);
|
|
||||||
|
|
||||||
// 调用拼图生成服务
|
// 构建生成请求
|
||||||
PuzzleGenerateResponse response = puzzleGenerateService.generate(generateRequest);
|
PuzzleGenerateRequest generateRequest = new PuzzleGenerateRequest();
|
||||||
|
generateRequest.setScenicId(scenicId);
|
||||||
|
generateRequest.setUserId(memberId);
|
||||||
|
generateRequest.setFaceId(faceId);
|
||||||
|
generateRequest.setBusinessType("face_matching");
|
||||||
|
generateRequest.setTemplateCode(template.getCode());
|
||||||
|
generateRequest.setOutputFormat("PNG");
|
||||||
|
generateRequest.setQuality(90);
|
||||||
|
generateRequest.setDynamicData(new HashMap<>(baseDynamicData));
|
||||||
|
generateRequest.setRequireRuleMatch(true);
|
||||||
|
|
||||||
log.info("拼图生成成功: scenicId={}, templateCode={}, imageUrl={}",
|
// 调用拼图生成服务
|
||||||
scenicId, template.getCode(), response.getImageUrl());
|
PuzzleGenerateResponse response = puzzleGenerateService.generate(generateRequest);
|
||||||
successCount++;
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
log.info("拼图生成成功: scenicId={}, templateCode={}, imageUrl={}",
|
||||||
log.error("拼图生成失败: scenicId={}, templateCode={}, templateName={}",
|
scenicId, template.getCode(), response.getImageUrl());
|
||||||
scenicId, template.getCode(), template.getName(), e);
|
successCount.incrementAndGet();
|
||||||
failCount++;
|
|
||||||
}
|
} catch (Exception e) {
|
||||||
|
log.error("拼图生成失败: scenicId={}, templateCode={}, templateName={}",
|
||||||
|
scenicId, template.getCode(), template.getName(), e);
|
||||||
|
failCount.incrementAndGet();
|
||||||
|
}
|
||||||
|
}, executor))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
// 等待所有任务完成
|
||||||
|
java.util.concurrent.CompletableFuture.allOf(futures.toArray(new java.util.concurrent.CompletableFuture[0])).join();
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("景区拼图模板批量生成完成: scenicId={}, 总数={}, 成功={}, 失败={}",
|
log.info("景区拼图模板批量生成完成: scenicId={}, 总数={}, 成功={}, 失败={}",
|
||||||
scenicId, templateList.size(), successCount, failCount);
|
scenicId, templateList.size(), successCount.get(), failCount.get());
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// 异步任务失败不影响主流程,仅记录日志
|
// 异步任务失败不影响主流程,仅记录日志
|
||||||
|
|||||||
@@ -14,8 +14,11 @@ import com.ycwl.basic.image.pipeline.stages.ConditionalRotateStage;
|
|||||||
import com.ycwl.basic.image.pipeline.stages.DownloadStage;
|
import com.ycwl.basic.image.pipeline.stages.DownloadStage;
|
||||||
import com.ycwl.basic.image.pipeline.stages.ImageEnhanceStage;
|
import com.ycwl.basic.image.pipeline.stages.ImageEnhanceStage;
|
||||||
import com.ycwl.basic.image.pipeline.stages.ImageOrientationStage;
|
import com.ycwl.basic.image.pipeline.stages.ImageOrientationStage;
|
||||||
|
import com.ycwl.basic.image.pipeline.stages.ImageResizeStage;
|
||||||
|
import com.ycwl.basic.image.pipeline.stages.ImageSRStage;
|
||||||
import com.ycwl.basic.image.pipeline.stages.PuzzleBorderStage;
|
import com.ycwl.basic.image.pipeline.stages.PuzzleBorderStage;
|
||||||
import com.ycwl.basic.image.pipeline.stages.RestoreOrientationStage;
|
import com.ycwl.basic.image.pipeline.stages.RestoreOrientationStage;
|
||||||
|
import com.ycwl.basic.image.pipeline.stages.UpdateMemberPrintStage;
|
||||||
import com.ycwl.basic.image.pipeline.stages.UploadStage;
|
import com.ycwl.basic.image.pipeline.stages.UploadStage;
|
||||||
import com.ycwl.basic.image.pipeline.stages.WatermarkConfig;
|
import com.ycwl.basic.image.pipeline.stages.WatermarkConfig;
|
||||||
import com.ycwl.basic.image.pipeline.stages.WatermarkStage;
|
import com.ycwl.basic.image.pipeline.stages.WatermarkStage;
|
||||||
@@ -29,6 +32,7 @@ import com.ycwl.basic.mapper.OrderMapper;
|
|||||||
import com.ycwl.basic.mapper.PrintTaskMapper;
|
import com.ycwl.basic.mapper.PrintTaskMapper;
|
||||||
import com.ycwl.basic.mapper.PrinterMapper;
|
import com.ycwl.basic.mapper.PrinterMapper;
|
||||||
import com.ycwl.basic.mapper.SourceMapper;
|
import com.ycwl.basic.mapper.SourceMapper;
|
||||||
|
import com.ycwl.basic.model.Crop;
|
||||||
import com.ycwl.basic.model.PrinterOrderItem;
|
import com.ycwl.basic.model.PrinterOrderItem;
|
||||||
import com.ycwl.basic.model.mobile.face.FaceRecognizeResp;
|
import com.ycwl.basic.model.mobile.face.FaceRecognizeResp;
|
||||||
import com.ycwl.basic.model.mobile.order.PriceObj;
|
import com.ycwl.basic.model.mobile.order.PriceObj;
|
||||||
@@ -347,6 +351,8 @@ public class PrinterServiceImpl implements PrinterService {
|
|||||||
|
|
||||||
log.info("照片裁剪成功: memberId={}, scenicId={}, 原图={}, 裁剪后={}, 尺寸={}x{}",
|
log.info("照片裁剪成功: memberId={}, scenicId={}, 原图={}, 裁剪后={}, 尺寸={}x{}",
|
||||||
memberId, scenicId, url, cropUrl, printWidth, printHeight);
|
memberId, scenicId, url, cropUrl, printWidth, printHeight);
|
||||||
|
String crop = JacksonUtil.toJSONString(new Crop(270));
|
||||||
|
entity.setCrop(crop);
|
||||||
} finally {
|
} finally {
|
||||||
// 清理临时文件
|
// 清理临时文件
|
||||||
if (croppedFile != null && croppedFile.exists()) {
|
if (croppedFile != null && croppedFile.exists()) {
|
||||||
@@ -467,6 +473,34 @@ public class PrinterServiceImpl implements PrinterService {
|
|||||||
|
|
||||||
request.setProducts(productItems);
|
request.setProducts(productItems);
|
||||||
|
|
||||||
|
// 检查是否存在type=3的source记录,存在才自动发券
|
||||||
|
boolean hasType3Source = userPhotoList.stream()
|
||||||
|
.filter(item -> item.getSourceId() != null && item.getSourceId() > 0)
|
||||||
|
.anyMatch(item -> {
|
||||||
|
try {
|
||||||
|
SourceEntity source = sourceMapper.getEntity(item.getSourceId());
|
||||||
|
return source != null && Integer.valueOf(3).equals(source.getType());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("查询source失败: sourceId={}, error={}", item.getSourceId(), e.getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasType3Source) {
|
||||||
|
if (normalCount > 0) {
|
||||||
|
try {
|
||||||
|
autoCouponService.autoGrantCoupon(
|
||||||
|
memberId,
|
||||||
|
faceId,
|
||||||
|
scenicId,
|
||||||
|
ProductType.PHOTO_PRINT
|
||||||
|
);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("自动发券失败,不影响下单流程: memberId={}, faceId={}, scenicId={}, error={}",
|
||||||
|
memberId, faceId, scenicId, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (mobileCount > 0) {
|
if (mobileCount > 0) {
|
||||||
try {
|
try {
|
||||||
autoCouponService.autoGrantCoupon(
|
autoCouponService.autoGrantCoupon(
|
||||||
@@ -505,6 +539,10 @@ public class PrinterServiceImpl implements PrinterService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String url = byId.getUrl();
|
String url = byId.getUrl();
|
||||||
|
// 特殊兼容处理
|
||||||
|
if (Integer.valueOf(3).equals(byId.getType())) {
|
||||||
|
url = byId.getThumbUrl().replace("_t.", "_o.");
|
||||||
|
}
|
||||||
MemberPrintEntity entity = new MemberPrintEntity();
|
MemberPrintEntity entity = new MemberPrintEntity();
|
||||||
entity.setMemberId(memberId);
|
entity.setMemberId(memberId);
|
||||||
entity.setScenicId(scenicId);
|
entity.setScenicId(scenicId);
|
||||||
@@ -540,6 +578,7 @@ public class PrinterServiceImpl implements PrinterService {
|
|||||||
|
|
||||||
// 使用smartCropAndFill裁剪图片
|
// 使用smartCropAndFill裁剪图片
|
||||||
File croppedFile = ImageUtils.smartCropAndFill(url, printWidth, printHeight);
|
File croppedFile = ImageUtils.smartCropAndFill(url, printWidth, printHeight);
|
||||||
|
entity.setCrop(JacksonUtil.toJSONString(new Crop(270)));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 上传裁剪后的图片
|
// 上传裁剪后的图片
|
||||||
@@ -800,6 +839,18 @@ public class PrinterServiceImpl implements PrinterService {
|
|||||||
* @return 处理后的URL,失败返回原URL
|
* @return 处理后的URL,失败返回原URL
|
||||||
*/
|
*/
|
||||||
private String processPhotoWithPipeline(MemberPrintResp item, Long scenicId, File qrCodeFile) {
|
private String processPhotoWithPipeline(MemberPrintResp item, Long scenicId, File qrCodeFile) {
|
||||||
|
return processPhotoWithPipeline(item, scenicId, qrCodeFile, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用管线处理照片(支持增强选项)
|
||||||
|
* @param item 打印项
|
||||||
|
* @param scenicId 景区ID
|
||||||
|
* @param qrCodeFile 二维码文件
|
||||||
|
* @param needEnhance 是否需要图像增强(null 或 false 表示不增强)
|
||||||
|
* @return 处理后的URL,失败返回原URL
|
||||||
|
*/
|
||||||
|
private String processPhotoWithPipeline(MemberPrintResp item, Long scenicId, File qrCodeFile, Boolean needEnhance) {
|
||||||
PrinterOrderItem orderItem = PrinterOrderItem.fromMemberPrintResp(item);
|
PrinterOrderItem orderItem = PrinterOrderItem.fromMemberPrintResp(item);
|
||||||
|
|
||||||
PhotoProcessContext context = PhotoProcessContext.fromPrinterOrderItem(orderItem, scenicId);
|
PhotoProcessContext context = PhotoProcessContext.fromPrinterOrderItem(orderItem, scenicId);
|
||||||
@@ -812,11 +863,15 @@ public class PrinterServiceImpl implements PrinterService {
|
|||||||
// 设置管线场景为图片打印
|
// 设置管线场景为图片打印
|
||||||
context.setScene(PipelineScene.IMAGE_PRINT);
|
context.setScene(PipelineScene.IMAGE_PRINT);
|
||||||
|
|
||||||
// 根据sourceId判断图片来源
|
// 处理图像增强选项
|
||||||
// sourceId > 0: IPC设备拍摄
|
if (needEnhance != null && needEnhance) {
|
||||||
// sourceId == null: 手机上传
|
context.setStageState("image_enhance", true);
|
||||||
// sourceId == 0: 拼图(暂定为UNKNOWN)
|
}
|
||||||
|
|
||||||
|
// 根据sourceId判断图片来源和source类型
|
||||||
|
SourceEntity source = null;
|
||||||
if (item.getSourceId() != null && item.getSourceId() > 0) {
|
if (item.getSourceId() != null && item.getSourceId() > 0) {
|
||||||
|
source = sourceMapper.getEntity(item.getSourceId());
|
||||||
context.setSource(ImageSource.IPC);
|
context.setSource(ImageSource.IPC);
|
||||||
} else if (item.getSourceId() == null) {
|
} else if (item.getSourceId() == null) {
|
||||||
context.setSource(ImageSource.PHONE);
|
context.setSource(ImageSource.PHONE);
|
||||||
@@ -825,15 +880,49 @@ public class PrinterServiceImpl implements PrinterService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Pipeline<PhotoProcessContext> pipeline;
|
Pipeline<PhotoProcessContext> pipeline;
|
||||||
if (context.getImageType() == ImageType.NORMAL_PHOTO) {
|
|
||||||
|
// 特殊处理: sourceId > 0 且 source.type == 3
|
||||||
|
if (source != null && source.getType() != null && source.getType() == 3) {
|
||||||
|
// Type=3的特殊处理流程
|
||||||
|
log.info("检测到source.type=3, 使用特殊处理管线: sourceId={}", item.getSourceId());
|
||||||
|
|
||||||
|
// 准备百度云配置
|
||||||
|
BceEnhancerConfig bceConfig = new BceEnhancerConfig();
|
||||||
|
bceConfig.setQps(1);
|
||||||
|
bceConfig.setAppId("119554288");
|
||||||
|
bceConfig.setApiKey("OX6QoijgKio3eVtA0PiUVf7f");
|
||||||
|
bceConfig.setSecretKey("dYatXReVriPeiktTjUblhfubpcmYfuMk");
|
||||||
|
|
||||||
// 准备水印配置
|
// 准备水印配置
|
||||||
WatermarkConfig watermarkConfig = prepareWatermarkConfig(context, qrCodeFile);
|
WatermarkConfig watermarkConfig = prepareWatermarkConfig(context, qrCodeFile, 3.0);
|
||||||
// 准备存储适配器
|
// 准备存储适配器
|
||||||
prepareStorageAdapter(context);
|
prepareStorageAdapter(context);
|
||||||
// 创建普通照片管线
|
context.enableStage("image_sr");
|
||||||
|
context.enableStage("image_enhance");
|
||||||
|
|
||||||
|
// 构建特殊管线: 超分(2倍) -> 增强 -> 更新MemberPrint -> 缩小2倍 -> 水印 -> 上传
|
||||||
|
pipeline = new PipelineBuilder<PhotoProcessContext>("Type3Pipeline")
|
||||||
|
.addStage(new DownloadStage()) // 1. 下载图片
|
||||||
|
.addStage(new ImageOrientationStage()) // 2. 检测方向
|
||||||
|
.addStage(new ConditionalRotateStage()) // 3. 条件性旋转
|
||||||
|
.addStage(new ImageSRStage(bceConfig)) // 4. 超分辨率(2倍放大)
|
||||||
|
.addStage(new ImageEnhanceStage(bceConfig)) // 5. 图像增强
|
||||||
|
.addStage(new UploadStage()) // 6. 上传(用于更新MemberPrint)
|
||||||
|
.addStage(new UpdateMemberPrintStage(printerMapper, // 7. 更新MemberPrint的cropUrl
|
||||||
|
item.getId(), item.getMemberId(), scenicId))
|
||||||
|
.addStage(new WatermarkStage(watermarkConfig)) // 9. 添加水印
|
||||||
|
.addStage(new RestoreOrientationStage()) // 10. 恢复方向
|
||||||
|
.addStage(new UploadStage()) // 11. 最终上传
|
||||||
|
.addStage(new CleanupStage()) // 12. 清理
|
||||||
|
.build();
|
||||||
|
|
||||||
|
} else if (context.getImageType() == ImageType.NORMAL_PHOTO) {
|
||||||
|
// 普通照片处理流程
|
||||||
|
WatermarkConfig watermarkConfig = prepareWatermarkConfig(context, qrCodeFile, 1.5);
|
||||||
|
prepareStorageAdapter(context);
|
||||||
pipeline = createNormalPhotoPipeline(watermarkConfig);
|
pipeline = createNormalPhotoPipeline(watermarkConfig);
|
||||||
} else {
|
} else {
|
||||||
// 拼图
|
// 拼图处理流程
|
||||||
prepareStorageAdapter(context);
|
prepareStorageAdapter(context);
|
||||||
pipeline = createPuzzlePipeline();
|
pipeline = createPuzzlePipeline();
|
||||||
}
|
}
|
||||||
@@ -867,7 +956,7 @@ public class PrinterServiceImpl implements PrinterService {
|
|||||||
* @param qrCodeFile 二维码文件
|
* @param qrCodeFile 二维码文件
|
||||||
* @return WatermarkConfig
|
* @return WatermarkConfig
|
||||||
*/
|
*/
|
||||||
private WatermarkConfig prepareWatermarkConfig(PhotoProcessContext context, File qrCodeFile) {
|
private WatermarkConfig prepareWatermarkConfig(PhotoProcessContext context, File qrCodeFile, Double scale) {
|
||||||
ScenicConfigManager scenicConfig = context.getScenicConfigManager();
|
ScenicConfigManager scenicConfig = context.getScenicConfigManager();
|
||||||
if (scenicConfig == null) {
|
if (scenicConfig == null) {
|
||||||
log.warn("scenicConfigManager未设置,返回空水印配置");
|
log.warn("scenicConfigManager未设置,返回空水印配置");
|
||||||
@@ -888,6 +977,7 @@ public class PrinterServiceImpl implements PrinterService {
|
|||||||
.scenicText(scenicText)
|
.scenicText(scenicText)
|
||||||
.dateFormat(dateFormat)
|
.dateFormat(dateFormat)
|
||||||
.qrcodeFile(qrCodeFile)
|
.qrcodeFile(qrCodeFile)
|
||||||
|
.scale(scale)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -937,53 +1027,56 @@ public class PrinterServiceImpl implements PrinterService {
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
userPhotoListByOrderId.forEach(item -> {
|
Thread.ofVirtual().start(() -> {
|
||||||
PrinterEntity printer = printerMapper.getById(item.getPrinterId());
|
userPhotoListByOrderId.forEach(item -> {
|
||||||
|
PrinterEntity printer = printerMapper.getById(item.getPrinterId());
|
||||||
|
|
||||||
// 使用管线处理照片
|
// 使用管线处理照片
|
||||||
String printUrl = processPhotoWithPipeline(item, item.getScenicId(), qrCodeFile);
|
String printUrl = processPhotoWithPipeline(item, item.getScenicId(), qrCodeFile);
|
||||||
|
|
||||||
// 根据数量创建多个打印任务
|
// 根据数量创建多个打印任务
|
||||||
Integer quantity = item.getQuantity();
|
Integer quantity = item.getQuantity();
|
||||||
if (quantity == null || quantity <= 0) {
|
if (quantity == null || quantity <= 0) {
|
||||||
quantity = 1; // 默认至少打印1张
|
quantity = 1; // 默认至少打印1张
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < quantity; i++) {
|
for (int i = 0; i < quantity; i++) {
|
||||||
// 获取打印机名称(支持轮询)
|
// 获取打印机名称(支持轮询)
|
||||||
String selectedPrinter = getNextPrinter(printer);
|
String selectedPrinter = getNextPrinter(printer);
|
||||||
|
|
||||||
// 根据景区配置决定任务初始状态
|
// 根据景区配置决定任务初始状态
|
||||||
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(order.getScenicId());
|
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(order.getScenicId());
|
||||||
Boolean purchaseNeedReview = scenicConfig.getBoolean("printer_manual_approve");
|
Boolean purchaseNeedReview = scenicConfig.getBoolean("printer_manual_approve");
|
||||||
int initialStatus = (purchaseNeedReview != null && purchaseNeedReview)
|
int initialStatus = (purchaseNeedReview != null && purchaseNeedReview)
|
||||||
? TASK_STATUS_PENDING_REVIEW
|
? TASK_STATUS_PENDING_REVIEW
|
||||||
: TASK_STATUS_PENDING;
|
: TASK_STATUS_PENDING;
|
||||||
|
|
||||||
PrintTaskEntity task = new PrintTaskEntity();
|
PrintTaskEntity task = new PrintTaskEntity();
|
||||||
task.setPrinterId(printer.getId());
|
task.setPrinterId(printer.getId());
|
||||||
task.setPrinterName(selectedPrinter);
|
task.setPrinterName(selectedPrinter);
|
||||||
task.setMpId(item.getId());
|
task.setMpId(item.getId());
|
||||||
task.setPaper(printer.getPreferPaper());
|
task.setPaper(printer.getPreferPaper());
|
||||||
task.setStatus(initialStatus);
|
task.setStatus(initialStatus);
|
||||||
task.setUrl(printUrl);
|
task.setUrl(printUrl);
|
||||||
task.setHeight(printer.getPreferH());
|
task.setHeight(printer.getPreferH());
|
||||||
task.setWidth(printer.getPreferW());
|
task.setWidth(printer.getPreferW());
|
||||||
task.setCreateTime(new Date());
|
task.setCreateTime(new Date());
|
||||||
task.setUpdateTime(new Date());
|
task.setUpdateTime(new Date());
|
||||||
printTaskMapper.insertTask(task);
|
printTaskMapper.insertTask(task);
|
||||||
|
|
||||||
// ========== WebSocket 推送任务 ==========
|
// ========== WebSocket 推送任务 ==========
|
||||||
// 只推送立即可处理的任务(status=0),待审核任务(status=4)等审核通过后再推送
|
// 只推送立即可处理的任务(status=0),待审核任务(status=4)等审核通过后再推送
|
||||||
if (initialStatus == TASK_STATUS_PENDING) {
|
if (initialStatus == TASK_STATUS_PENDING) {
|
||||||
try {
|
try {
|
||||||
taskPushService.pushTaskToPrinter(printer.getId(), task.getId());
|
taskPushService.pushTaskToPrinter(printer.getId(), task.getId());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("推送任务失败: printerId={}, taskId={}", printer.getId(), task.getId(), e);
|
log.error("推送任务失败: printerId={}, taskId={}", printer.getId(), task.getId(), e);
|
||||||
// 推送失败不影响任务创建,任务会通过 HTTP 轮询获取
|
// 推送失败不影响任务创建,任务会通过 HTTP 轮询获取
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
redisTemplate.delete("order_content_not_downloadable_" + orderId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1418,58 +1511,15 @@ public class PrinterServiceImpl implements PrinterService {
|
|||||||
needEnhance = false; // 默认不增强
|
needEnhance = false; // 默认不增强
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3.1 创建图片处理上下文
|
// 3.1 使用管线处理照片(复用 processPhotoWithPipeline)
|
||||||
PrinterOrderItem orderItem = PrinterOrderItem.fromMemberPrintResp(memberPrint);
|
String newPrintUrl;
|
||||||
PhotoProcessContext context = PhotoProcessContext.fromPrinterOrderItem(orderItem, memberPrint.getScenicId());
|
|
||||||
context.setStageState("image_enhance", needEnhance); // 通过setStageState方法设置是否启用
|
|
||||||
|
|
||||||
// 3.2 设置景区配置和场景
|
|
||||||
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(memberPrint.getScenicId());
|
|
||||||
context.setScenicConfigManager(scenicConfig);
|
|
||||||
context.setScene(PipelineScene.IMAGE_PRINT);
|
|
||||||
|
|
||||||
// 3.3 判断图片来源
|
|
||||||
if (memberPrint.getSourceId() != null && memberPrint.getSourceId() > 0) {
|
|
||||||
context.setSource(ImageSource.IPC);
|
|
||||||
} else if (memberPrint.getSourceId() == null) {
|
|
||||||
context.setSource(ImageSource.PHONE);
|
|
||||||
} else {
|
|
||||||
context.setSource(ImageSource.UNKNOWN);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3.4 构建管线(关键:条件性添加 ImageEnhanceStage)
|
|
||||||
Pipeline<PhotoProcessContext> pipeline;
|
|
||||||
String newPrintUrl = null;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (context.getImageType() == ImageType.NORMAL_PHOTO) {
|
newPrintUrl = processPhotoWithPipeline(memberPrint, memberPrint.getScenicId(), qrCodeFile, needEnhance);
|
||||||
// 准备水印配置(重打印需要二维码)
|
log.info("handleReprint: 照片重新处理成功, taskId={}, mpId={}, enhance={}, newUrl={}",
|
||||||
WatermarkConfig watermarkConfig = prepareWatermarkConfig(context, qrCodeFile);
|
id, mpId, needEnhance, newPrintUrl);
|
||||||
prepareStorageAdapter(context);
|
|
||||||
|
|
||||||
// 创建管线,条件性添加增强 Stage
|
|
||||||
pipeline = createNormalPhotoPipeline(watermarkConfig);
|
|
||||||
} else {
|
|
||||||
// 拼图
|
|
||||||
prepareStorageAdapter(context);
|
|
||||||
pipeline = createPuzzlePipeline();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3.5 执行管线
|
|
||||||
boolean success = pipeline.execute(context);
|
|
||||||
if (success && context.getResultUrl() != null) {
|
|
||||||
newPrintUrl = context.getResultUrl();
|
|
||||||
log.info("handleReprint: 照片重新处理成功, taskId={}, mpId={}, enhance={}, newUrl={}",
|
|
||||||
id, mpId, needEnhance, newPrintUrl);
|
|
||||||
} else {
|
|
||||||
log.warn("handleReprint: 照片重新处理失败, taskId={}, 使用原图", id);
|
|
||||||
newPrintUrl = memberPrint.getCropUrl(); // 使用原裁剪图
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("handleReprint: 照片重新处理异常, taskId={}, 使用原图", id, e);
|
log.error("handleReprint: 照片重新处理异常, taskId={}, 使用原图", id, e);
|
||||||
newPrintUrl = memberPrint.getCropUrl();
|
newPrintUrl = memberPrint.getCropUrl();
|
||||||
} finally {
|
|
||||||
context.cleanup();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 更新打印任务
|
// 4. 更新打印任务
|
||||||
|
|||||||
@@ -14,9 +14,7 @@ public interface TaskService {
|
|||||||
|
|
||||||
TemplateRespVO workerGetTemplate(Long templateId, WorkerAuthReqVo req);
|
TemplateRespVO workerGetTemplate(Long templateId, WorkerAuthReqVo req);
|
||||||
|
|
||||||
void createTaskByFaceIdAndTemplateId(Long faceId, Long templateId);
|
void createTaskByFaceIdAndTemplateId(Long faceId, Long templateId, boolean automatic);
|
||||||
|
|
||||||
void createTaskByFaceIdAndTemplateId(Long faceId, Long templateId, int automatic);
|
|
||||||
|
|
||||||
void taskSuccess(Long taskId, TaskSuccessReqVo req);
|
void taskSuccess(Long taskId, TaskSuccessReqVo req);
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import com.ycwl.basic.mapper.TaskMapper;
|
|||||||
import com.ycwl.basic.mapper.VideoMapper;
|
import com.ycwl.basic.mapper.VideoMapper;
|
||||||
import com.ycwl.basic.model.mobile.order.IsBuyRespVO;
|
import com.ycwl.basic.model.mobile.order.IsBuyRespVO;
|
||||||
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
||||||
import com.ycwl.basic.model.pc.face.resp.FaceRespVO;
|
|
||||||
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
|
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
|
||||||
import com.ycwl.basic.model.pc.member.resp.MemberRespVO;
|
import com.ycwl.basic.model.pc.member.resp.MemberRespVO;
|
||||||
import com.ycwl.basic.model.pc.mp.MpConfigEntity;
|
import com.ycwl.basic.model.pc.mp.MpConfigEntity;
|
||||||
@@ -67,7 +66,6 @@ import org.springframework.data.redis.core.RedisTemplate;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -256,7 +254,7 @@ public class TaskTaskServiceImpl implements TaskService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void forceCreateTaskByFaceIdAndTempalteId(Long faceId, Long templateId) {
|
public void forceCreateTaskByFaceIdAndTempalteId(Long faceId, Long templateId) {
|
||||||
createTaskByFaceIdAndTemplateIdInternal(faceId, templateId, 0, true);
|
createTaskByFaceIdAndTemplateIdInternal(faceId, templateId, false, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -304,34 +302,30 @@ public class TaskTaskServiceImpl implements TaskService {
|
|||||||
}
|
}
|
||||||
if (Integer.valueOf(3).equals(scenicConfig.getInteger("book_routine")) || Integer.valueOf(4).equals(scenicConfig.getInteger("book_routine"))) {
|
if (Integer.valueOf(3).equals(scenicConfig.getInteger("book_routine")) || Integer.valueOf(4).equals(scenicConfig.getInteger("book_routine"))) {
|
||||||
// 生成全部视频的逻辑
|
// 生成全部视频的逻辑
|
||||||
templateList.forEach(template -> createTaskByFaceIdAndTemplateId(faceId, template.getId(), 1));
|
templateList.forEach(template -> createTaskByFaceIdAndTemplateId(faceId, template.getId(), true));
|
||||||
} else {
|
} else {
|
||||||
if (Boolean.TRUE.equals(scenicConfig.getBoolean("force_create_vlog"))) {
|
if (Boolean.TRUE.equals(scenicConfig.getBoolean("force_create_vlog"))) {
|
||||||
Long availableTemplateId = templateBiz.findFirstAvailableTemplate(templateList.stream().map(TemplateRespVO::getId).toList(), faceId, false);
|
Long availableTemplateId = templateBiz.findFirstAvailableTemplate(templateList.stream().map(TemplateRespVO::getId).toList(), faceId, false);
|
||||||
if (availableTemplateId != null) {
|
if (availableTemplateId != null) {
|
||||||
createTaskByFaceIdAndTemplateId(faceId, availableTemplateId, 1);
|
createTaskByFaceIdAndTemplateId(faceId, availableTemplateId, true);
|
||||||
} else {
|
} else {
|
||||||
log.info("faceId:{} available template is not exist", faceId);
|
log.info("faceId:{} available template is not exist", faceId);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 非强制创建,只创建第一个可用模板
|
// 非强制创建,只创建第一个可用模板
|
||||||
if (!templateList.isEmpty()) {
|
if (!templateList.isEmpty()) {
|
||||||
createTaskByFaceIdAndTemplateId(faceId, templateList.getFirst().getId(), 1);
|
createTaskByFaceIdAndTemplateId(faceId, templateList.getFirst().getId(), true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@Override
|
|
||||||
public void createTaskByFaceIdAndTemplateId(Long faceId, Long templateId) {
|
|
||||||
createTaskByFaceIdAndTemplateId(faceId, templateId, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void createTaskByFaceIdAndTemplateId(Long faceId, Long templateId, int automatic) {
|
public void createTaskByFaceIdAndTemplateId(Long faceId, Long templateId, boolean automatic) {
|
||||||
createTaskByFaceIdAndTemplateIdInternal(faceId, templateId, automatic, false);
|
createTaskByFaceIdAndTemplateIdInternal(faceId, templateId, automatic, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createTaskByFaceIdAndTemplateIdInternal(Long faceId, Long templateId, int automatic, boolean forceCreate) {
|
private void createTaskByFaceIdAndTemplateIdInternal(Long faceId, Long templateId, boolean automatic, boolean forceCreate) {
|
||||||
FaceEntity face = faceRepository.getFace(faceId);
|
FaceEntity face = faceRepository.getFace(faceId);
|
||||||
if (face == null) {
|
if (face == null) {
|
||||||
log.info("faceId:{} is not exist", faceId);
|
log.info("faceId:{} is not exist", faceId);
|
||||||
@@ -430,7 +424,7 @@ public class TaskTaskServiceImpl implements TaskService {
|
|||||||
taskEntity.setScenicId(face.getScenicId());
|
taskEntity.setScenicId(face.getScenicId());
|
||||||
taskEntity.setFaceId(faceId);
|
taskEntity.setFaceId(faceId);
|
||||||
taskEntity.setTemplateId(templateId);
|
taskEntity.setTemplateId(templateId);
|
||||||
taskEntity.setAutomatic(automatic);
|
taskEntity.setAutomatic(automatic ? 1 : 0);
|
||||||
}
|
}
|
||||||
taskEntity.setWorkerId(null);
|
taskEntity.setWorkerId(null);
|
||||||
taskEntity.setStatus(0);
|
taskEntity.setStatus(0);
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
server:
|
server:
|
||||||
port: 8030
|
port: 8030
|
||||||
|
shutdown: graceful
|
||||||
|
|
||||||
spring:
|
spring:
|
||||||
application:
|
application:
|
||||||
name: zt
|
name: zt
|
||||||
|
lifecycle:
|
||||||
|
timeout-per-shutdown-phase: 60s
|
||||||
|
|
||||||
# Feign配置(简化版,基于Nacos服务发现)
|
# Feign配置(简化版,基于Nacos服务发现)
|
||||||
feign:
|
feign:
|
||||||
@@ -39,3 +42,6 @@ kafka:
|
|||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
com.ycwl.basic.integration.scenic.client: DEBUG
|
com.ycwl.basic.integration.scenic.client: DEBUG
|
||||||
|
|
||||||
|
zhipu:
|
||||||
|
api-key: a331e0fcf3f74518818b8e5129b79058.RXuUxUUjKdcxbF4L
|
||||||
@@ -1,11 +1,17 @@
|
|||||||
server:
|
server:
|
||||||
port: 8031
|
port: 8031
|
||||||
|
shutdown: graceful
|
||||||
|
|
||||||
spring:
|
spring:
|
||||||
application:
|
application:
|
||||||
name: zt
|
name: zt
|
||||||
|
lifecycle:
|
||||||
|
timeout-per-shutdown-phase: 60s
|
||||||
|
|
||||||
# 生产环境日志级别
|
# 生产环境日志级别
|
||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
com.ycwl.basic.integration.scenic.client: WARN
|
com.ycwl.basic.integration.scenic.client: WARN
|
||||||
|
|
||||||
|
zhipu:
|
||||||
|
api-key: a331e0fcf3f74518818b8e5129b79058.RXuUxUUjKdcxbF4L
|
||||||
@@ -46,6 +46,82 @@
|
|||||||
</encoder>
|
</encoder>
|
||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
|
<!-- Task specific log -->
|
||||||
|
<appender name="TASK_LOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<File>logs/task.log</File>
|
||||||
|
<append>true</append>
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||||
|
<fileNamePattern>logs/task.%d.%i.log</fileNamePattern>
|
||||||
|
<maxHistory>30</maxHistory>
|
||||||
|
<totalSizeCap>5GB</totalSizeCap>
|
||||||
|
<maxFileSize>10MB</maxFileSize>
|
||||||
|
</rollingPolicy>
|
||||||
|
<encoder>
|
||||||
|
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level --- [%15.15(%thread)] %-40.40(%logger{40}) : %msg%n</pattern>
|
||||||
|
<charset>UTF-8</charset>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<!-- FaceProcessingKafkaService specific log -->
|
||||||
|
<appender name="FACE_PROCESSING_KAFKA_SERVICE_LOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<File>logs/face_processing_kafka_service.log</File>
|
||||||
|
<append>true</append>
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||||
|
<fileNamePattern>logs/face_processing_kafka_service.%d.%i.log</fileNamePattern>
|
||||||
|
<maxHistory>30</maxHistory>
|
||||||
|
<totalSizeCap>5GB</totalSizeCap>
|
||||||
|
<maxFileSize>10MB</maxFileSize>
|
||||||
|
</rollingPolicy>
|
||||||
|
<encoder>
|
||||||
|
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level --- [%15.15(%thread)] %-40.40(%logger{40}) : %msg%n</pattern>
|
||||||
|
<charset>UTF-8</charset>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<!-- DeviceStorageOperator specific log -->
|
||||||
|
<appender name="DEVICE_STORAGE_OPERATOR_LOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<File>logs/device_storage_operator.log</File>
|
||||||
|
<append>true</append>
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||||
|
<fileNamePattern>logs/device_storage_operator.%d.%i.log</fileNamePattern>
|
||||||
|
<maxHistory>30</maxHistory>
|
||||||
|
<totalSizeCap>5GB</totalSizeCap>
|
||||||
|
<maxFileSize>10MB</maxFileSize>
|
||||||
|
</rollingPolicy>
|
||||||
|
<encoder>
|
||||||
|
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level --- [%15.15(%thread)] %-40.40(%logger{40}) : %msg%n</pattern>
|
||||||
|
<charset>UTF-8</charset>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<logger name="com.ycwl.basic.device.operator" level="INFO" additivity="false">
|
||||||
|
<appender-ref ref="DEVICE_STORAGE_OPERATOR_LOG"/>
|
||||||
|
</logger>
|
||||||
|
|
||||||
|
<logger name="com.ycwl.basic.task.DeviceVideoContinuityCheckTask" level="INFO" additivity="false">
|
||||||
|
<appender-ref ref="TASK_LOG"/>
|
||||||
|
</logger>
|
||||||
|
|
||||||
|
<logger name="com.ycwl.basic.task.FaceCleaner" level="INFO" additivity="false">
|
||||||
|
<appender-ref ref="TASK_LOG"/>
|
||||||
|
</logger>
|
||||||
|
|
||||||
|
<logger name="com.ycwl.basic.task.VideoPieceCleaner" level="INFO" additivity="false">
|
||||||
|
<appender-ref ref="TASK_LOG"/>
|
||||||
|
</logger>
|
||||||
|
|
||||||
|
<logger name="com.ycwl.basic.task.DynamicTaskGenerator" level="INFO" additivity="false">
|
||||||
|
<appender-ref ref="TASK_LOG"/>
|
||||||
|
</logger>
|
||||||
|
|
||||||
|
<logger name="com.ycwl.basic.task.DownloadNotificationTasker" level="INFO" additivity="false">
|
||||||
|
<appender-ref ref="TASK_LOG"/>
|
||||||
|
</logger>
|
||||||
|
|
||||||
|
<logger name="com.ycwl.basic.integration.kafka.service.FaceProcessingKafkaService" level="INFO" additivity="false">
|
||||||
|
<appender-ref ref="FACE_PROCESSING_KAFKA_SERVICE_LOG"/>
|
||||||
|
</logger>
|
||||||
|
|
||||||
<root level="ERROR">
|
<root level="ERROR">
|
||||||
<appender-ref ref="error_log" />
|
<appender-ref ref="error_log" />
|
||||||
<appender-ref ref="STDOUT" />
|
<appender-ref ref="STDOUT" />
|
||||||
|
|||||||
43
src/main/resources/mapper/FaceChatConversationMapper.xml
Normal file
43
src/main/resources/mapper/FaceChatConversationMapper.xml
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<!DOCTYPE mapper
|
||||||
|
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.ycwl.basic.mapper.FaceChatConversationMapper">
|
||||||
|
|
||||||
|
<resultMap id="BaseResultMap" type="com.ycwl.basic.model.mobile.chat.entity.FaceChatConversationEntity">
|
||||||
|
<id column="id" property="id"/>
|
||||||
|
<result column="face_id" property="faceId"/>
|
||||||
|
<result column="member_id" property="memberId"/>
|
||||||
|
<result column="status" property="status"/>
|
||||||
|
<result column="model" property="model"/>
|
||||||
|
<result column="created_at" property="createdAt"/>
|
||||||
|
<result column="updated_at" property="updatedAt"/>
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<select id="findByFaceId" resultMap="BaseResultMap">
|
||||||
|
select id, face_id, member_id, status, model, created_at, updated_at
|
||||||
|
from face_chat_conversation
|
||||||
|
where face_id = #{faceId}
|
||||||
|
limit 1
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="getById" resultMap="BaseResultMap">
|
||||||
|
select id, face_id, member_id, status, model, created_at, updated_at
|
||||||
|
from face_chat_conversation
|
||||||
|
where id = #{id}
|
||||||
|
limit 1
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<insert id="insert">
|
||||||
|
insert into face_chat_conversation
|
||||||
|
(id, face_id, member_id, status, model, created_at, updated_at)
|
||||||
|
values
|
||||||
|
(#{id}, #{faceId}, #{memberId}, #{status}, #{model}, now(), now())
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<update id="updateStatus">
|
||||||
|
update face_chat_conversation
|
||||||
|
set status = #{status}, updated_at = now()
|
||||||
|
where id = #{id}
|
||||||
|
</update>
|
||||||
|
</mapper>
|
||||||
55
src/main/resources/mapper/FaceChatMessageMapper.xml
Normal file
55
src/main/resources/mapper/FaceChatMessageMapper.xml
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<!DOCTYPE mapper
|
||||||
|
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.ycwl.basic.mapper.FaceChatMessageMapper">
|
||||||
|
|
||||||
|
<resultMap id="BaseResultMap" type="com.ycwl.basic.model.mobile.chat.entity.FaceChatMessageEntity">
|
||||||
|
<id column="id" property="id"/>
|
||||||
|
<result column="conversation_id" property="conversationId"/>
|
||||||
|
<result column="face_id" property="faceId"/>
|
||||||
|
<result column="seq" property="seq"/>
|
||||||
|
<result column="role" property="role"/>
|
||||||
|
<result column="content" property="content"/>
|
||||||
|
<result column="trace_id" property="traceId"/>
|
||||||
|
<result column="latency_ms" property="latencyMs"/>
|
||||||
|
<result column="created_at" property="createdAt"/>
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<select id="maxSeqForUpdate" resultType="java.lang.Integer">
|
||||||
|
select ifnull(max(seq), 0)
|
||||||
|
from face_chat_message
|
||||||
|
where conversation_id = #{conversationId}
|
||||||
|
for update
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<insert id="insert">
|
||||||
|
insert into face_chat_message
|
||||||
|
(id, conversation_id, face_id, seq, role, content, trace_id, latency_ms, created_at)
|
||||||
|
values
|
||||||
|
(#{id}, #{conversationId}, #{faceId}, #{seq}, #{role}, #{content}, #{traceId}, #{latencyMs}, now())
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<select id="listByConversation" resultMap="BaseResultMap">
|
||||||
|
select id, conversation_id, face_id, seq, role, content, trace_id, latency_ms, created_at
|
||||||
|
from face_chat_message
|
||||||
|
where conversation_id = #{conversationId}
|
||||||
|
<if test="cursor != null">
|
||||||
|
and seq > #{cursor}
|
||||||
|
</if>
|
||||||
|
order by seq asc
|
||||||
|
<if test="limit != null">
|
||||||
|
limit #{limit}
|
||||||
|
</if>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="listRecentByConversation" resultMap="BaseResultMap">
|
||||||
|
select id, conversation_id, face_id, seq, role, content, trace_id, latency_ms, created_at
|
||||||
|
from face_chat_message
|
||||||
|
where conversation_id = #{conversationId}
|
||||||
|
order by seq desc
|
||||||
|
<if test="limit != null">
|
||||||
|
limit #{limit}
|
||||||
|
</if>
|
||||||
|
</select>
|
||||||
|
</mapper>
|
||||||
@@ -2,7 +2,16 @@
|
|||||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
<mapper namespace="com.ycwl.basic.mapper.FaceDetectLogAiCamMapper">
|
<mapper namespace="com.ycwl.basic.mapper.FaceDetectLogAiCamMapper">
|
||||||
<insert id="add" useGeneratedKeys="true" keyProperty="id">
|
<insert id="add" useGeneratedKeys="true" keyProperty="id">
|
||||||
insert into face_detect_log_ai_cam(scenic_id, device_id, face_sample_id, db_name, face_url, score, match_raw_result, create_time)
|
insert into face_detect_log_ai_cam(scenic_id, device_id, face_id, db_name, face_url, score, match_raw_result, create_time)
|
||||||
values (#{scenicId}, #{deviceId}, #{faceSampleId}, #{dbName}, #{faceUrl}, #{score}, #{matchRawResult}, #{createTime})
|
values (#{scenicId}, #{deviceId}, #{faceId}, #{dbName}, #{faceUrl}, #{score}, #{matchRawResult}, #{createTime})
|
||||||
</insert>
|
</insert>
|
||||||
|
|
||||||
|
<select id="listByFaceId" resultType="com.ycwl.basic.model.pc.faceDetectLog.entity.FaceDetectLogAiCamEntity">
|
||||||
|
select id, scenic_id as scenicId, device_id as deviceId, face_id as faceId,
|
||||||
|
db_name as dbName, face_url as faceUrl, score, match_raw_result as matchRawResult,
|
||||||
|
create_time as createTime
|
||||||
|
from face_detect_log_ai_cam
|
||||||
|
where face_id = #{faceId}
|
||||||
|
order by create_time desc
|
||||||
|
</select>
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
@@ -95,6 +95,13 @@
|
|||||||
LEFT JOIN face f ON ms.face_id = f.id
|
LEFT JOIN face f ON ms.face_id = f.id
|
||||||
LEFT JOIN source s ON ms.source_id = s.id
|
LEFT JOIN source s ON ms.source_id = s.id
|
||||||
WHERE s.id IS NOT NULL
|
WHERE s.id IS NOT NULL
|
||||||
|
),
|
||||||
|
member_source_aicam_data AS (
|
||||||
|
SELECT ms.member_id, ms.source_id, ms.face_id, f.face_url, s.url
|
||||||
|
FROM member_source ms
|
||||||
|
LEFT JOIN face f ON ms.face_id = f.id
|
||||||
|
LEFT JOIN source s ON ms.source_id = s.id
|
||||||
|
WHERE s.id IS NOT NULL AND ms.type = 3
|
||||||
),
|
),
|
||||||
member_photo_data AS (
|
member_photo_data AS (
|
||||||
SELECT mp.member_id, 3 as type, mp.id, mp.crop_url as url, mp.quantity, mp.status, mp.create_time
|
SELECT mp.member_id, 3 as type, mp.id, mp.crop_url as url, mp.quantity, mp.status, mp.create_time
|
||||||
@@ -105,8 +112,9 @@
|
|||||||
FROM source s
|
FROM source s
|
||||||
),
|
),
|
||||||
member_plog_data AS (
|
member_plog_data AS (
|
||||||
SELECT 5 as type, gr.template_id as id, gr.result_image_url as url, gr.face_id
|
SELECT 5 as type, gr.template_id as id, pt.scenic_id as scenic_id, gr.result_image_url as url, gr.face_id
|
||||||
FROM puzzle_generation_record gr
|
FROM puzzle_generation_record gr
|
||||||
|
left join puzzle_template pt on gr.template_id = pt.id
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
oi.id AS oiId,
|
oi.id AS oiId,
|
||||||
@@ -127,17 +135,20 @@
|
|||||||
WHEN '3' THEN '照片打印'
|
WHEN '3' THEN '照片打印'
|
||||||
WHEN '4' THEN '一体机照片打印'
|
WHEN '4' THEN '一体机照片打印'
|
||||||
WHEN '5' THEN 'pLog'
|
WHEN '5' THEN 'pLog'
|
||||||
|
WHEN '13' THEN '打卡点拍照'
|
||||||
ELSE '其他'
|
ELSE '其他'
|
||||||
END AS goods_name,
|
END AS goods_name,
|
||||||
CASE oi.goods_type
|
CASE oi.goods_type
|
||||||
WHEN '0' THEN mvd.face_id
|
WHEN '0' THEN mvd.face_id
|
||||||
WHEN '1' THEN oi.goods_id
|
WHEN '1' THEN oi.goods_id
|
||||||
WHEN '2' THEN oi.goods_id
|
WHEN '2' THEN oi.goods_id
|
||||||
|
WHEN '13' THEN oi.goods_id
|
||||||
END AS face_id,
|
END AS face_id,
|
||||||
CASE oi.goods_type
|
CASE oi.goods_type
|
||||||
WHEN '0' THEN mvd.face_url
|
WHEN '0' THEN mvd.face_url
|
||||||
WHEN '1' THEN msd.face_url
|
WHEN '1' THEN msd.face_url
|
||||||
WHEN '2' THEN msd.face_url
|
WHEN '2' THEN msd.face_url
|
||||||
|
WHEN '13' THEN msac.face_url
|
||||||
END AS face_url,
|
END AS face_url,
|
||||||
CASE oi.goods_type
|
CASE oi.goods_type
|
||||||
WHEN '0' THEN mvd.video_url
|
WHEN '0' THEN mvd.video_url
|
||||||
@@ -149,14 +160,16 @@
|
|||||||
WHEN '3' THEN mpd.url
|
WHEN '3' THEN mpd.url
|
||||||
WHEN '4' THEN mpa.url
|
WHEN '4' THEN mpa.url
|
||||||
WHEN '5' THEN mpl.url
|
WHEN '5' THEN mpl.url
|
||||||
|
WHEN '13' THEN msac.url
|
||||||
END AS imgUrl
|
END AS imgUrl
|
||||||
FROM order_item oi
|
FROM order_item oi
|
||||||
LEFT JOIN `order` o ON oi.order_id = o.id
|
LEFT JOIN `order` o ON oi.order_id = o.id
|
||||||
LEFT JOIN member_video_data mvd ON o.face_id = mvd.face_id AND oi.goods_id = mvd.video_id
|
LEFT JOIN member_video_data mvd ON o.face_id = mvd.face_id AND oi.goods_id = mvd.video_id
|
||||||
LEFT JOIN member_source_data msd ON o.face_id = msd.face_id AND oi.goods_id = msd.face_id AND msd.type = oi.goods_type
|
LEFT JOIN member_source_data msd ON o.face_id = msd.face_id AND oi.goods_id = msd.face_id AND msd.type = oi.goods_type
|
||||||
|
LEFT JOIN member_source_aicam_data msac ON o.face_id = msac.face_id AND oi.goods_id = msac.face_id AND oi.goods_type = 13
|
||||||
LEFT JOIN member_photo_data mpd ON oi.goods_id = mpd.id AND mpd.type = oi.goods_type
|
LEFT JOIN member_photo_data mpd ON oi.goods_id = mpd.id AND mpd.type = oi.goods_type
|
||||||
LEFT JOIN member_aio_photo_data mpa ON oi.goods_id = mpa.id AND mpa.type = oi.goods_type
|
LEFT JOIN member_aio_photo_data mpa ON oi.goods_id = mpa.id AND mpa.type = oi.goods_type
|
||||||
LEFT JOIN member_plog_data mpl ON oi.goods_id = mpl.id AND mpl.type = oi.goods_type AND o.face_id = mpl.face_id
|
LEFT JOIN member_plog_data mpl ON (oi.goods_id = mpl.id OR oi.goods_id = mpl.scenic_id) AND mpl.type = oi.goods_type AND o.face_id = mpl.face_id
|
||||||
WHERE oi.order_id = #{id};
|
WHERE oi.order_id = #{id};
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
|||||||
@@ -116,6 +116,7 @@
|
|||||||
source_id,
|
source_id,
|
||||||
orig_url,
|
orig_url,
|
||||||
crop_url,
|
crop_url,
|
||||||
|
crop,
|
||||||
quantity,
|
quantity,
|
||||||
status,
|
status,
|
||||||
create_time,
|
create_time,
|
||||||
@@ -127,6 +128,7 @@
|
|||||||
#{sourceId},
|
#{sourceId},
|
||||||
#{origUrl},
|
#{origUrl},
|
||||||
#{cropUrl},
|
#{cropUrl},
|
||||||
|
#{crop},
|
||||||
1,
|
1,
|
||||||
0,
|
0,
|
||||||
NOW(),
|
NOW(),
|
||||||
|
|||||||
@@ -198,7 +198,7 @@
|
|||||||
where so.id = #{id} and ms.member_id = #{userId} and so.id is not null
|
where so.id = #{id} and ms.member_id = #{userId} and so.id is not null
|
||||||
</select>
|
</select>
|
||||||
<select id="getById" resultType="com.ycwl.basic.model.pc.source.resp.SourceRespVO">
|
<select id="getById" resultType="com.ycwl.basic.model.pc.source.resp.SourceRespVO">
|
||||||
select so.id, scenic_id, device_id, thumb_url, url, video_url, so.create_time, so.update_time
|
select so.id, scenic_id, device_id, thumb_url, type, url, video_url, so.create_time, so.update_time
|
||||||
from source so
|
from source so
|
||||||
|
|
||||||
where so.id = #{id}
|
where so.id = #{id}
|
||||||
@@ -348,6 +348,12 @@
|
|||||||
left join source s on ms.source_id = s.id
|
left join source s on ms.source_id = s.id
|
||||||
where ms.face_id = #{faceId} and ms.type = 2
|
where ms.face_id = #{faceId} and ms.type = 2
|
||||||
</select>
|
</select>
|
||||||
|
<select id="listAiCamImageByFaceRelation" resultType="com.ycwl.basic.model.pc.source.entity.SourceEntity">
|
||||||
|
select s.*, ms.is_buy
|
||||||
|
from member_source ms
|
||||||
|
left join source s on ms.source_id = s.id
|
||||||
|
where ms.face_id = #{faceId} and ms.type = 3
|
||||||
|
</select>
|
||||||
<select id="getEntity" resultType="com.ycwl.basic.model.pc.source.entity.SourceEntity">
|
<select id="getEntity" resultType="com.ycwl.basic.model.pc.source.entity.SourceEntity">
|
||||||
select *
|
select *
|
||||||
from source
|
from source
|
||||||
@@ -486,4 +492,19 @@
|
|||||||
</choose>
|
</choose>
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="listByFaceSampleIdsAndType" resultType="com.ycwl.basic.model.pc.source.entity.SourceEntity">
|
||||||
|
SELECT * FROM source
|
||||||
|
WHERE face_sample_id IN
|
||||||
|
<foreach collection="faceSampleIds" item="item" open="(" separator="," close=")">
|
||||||
|
#{item}
|
||||||
|
</foreach>
|
||||||
|
AND `type` = #{type}
|
||||||
|
ORDER BY create_time DESC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<delete id="deleteRelationsByFaceIdAndType">
|
||||||
|
DELETE FROM member_source
|
||||||
|
WHERE face_id = #{faceId} AND `type` = #{type}
|
||||||
|
</delete>
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
Reference in New Issue
Block a user