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>
|
||||
</dependency>
|
||||
|
||||
<!-- 智谱AI SDK -->
|
||||
<dependency>
|
||||
<groupId>ai.z.openapi</groupId>
|
||||
<artifactId>zai-sdk</artifactId>
|
||||
<version>0.1.3</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Kafka -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.kafka</groupId>
|
||||
|
||||
@@ -147,6 +147,21 @@ public class OrderBiz {
|
||||
priceObj.setSlashPrice(priceCalculationResult.getOriginalAmount());
|
||||
priceObj.setFaceId(goodsId);
|
||||
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;
|
||||
}
|
||||
@@ -215,11 +230,15 @@ public class OrderBiz {
|
||||
orderRepository.updateOrder(orderId, orderUpdate);
|
||||
orderItems.forEach(item -> {
|
||||
switch (item.getGoodsType()) {
|
||||
case -1: // vlog视频模板
|
||||
videoRepository.setUserIsBuyTemplate(order.getMemberId(), item.getGoodsId(), order.getId(), order.getFaceId());
|
||||
break;
|
||||
case 0: // vlog视频
|
||||
videoRepository.setUserIsBuyItem(order.getMemberId(), item.getGoodsId(), order.getId());
|
||||
break;
|
||||
case 1: // 视频原素材
|
||||
case 2: // 照片原素材
|
||||
case 13: // AI微单
|
||||
sourceRepository.setUserIsBuyItem(order.getMemberId(), item.getGoodsType(), item.getGoodsId(), order.getId());
|
||||
break;
|
||||
case 3:
|
||||
@@ -295,10 +314,14 @@ public class OrderBiz {
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否购买了指定商品
|
||||
* 提供给PriceBiz使用,避免循环调用
|
||||
* 检查用户是否购买了指定商品,并额外校验订单的faceId是否匹配
|
||||
* @param userId 用户ID
|
||||
* @param faceId 人脸ID
|
||||
* @param goodsType 商品类型
|
||||
* @param goodsId 商品ID
|
||||
* @return 是否已购买且faceId匹配
|
||||
*/
|
||||
public boolean checkUserBuyItem(Long userId, int goodsType, Long goodsId) {
|
||||
return orderRepository.checkUserBuyItem(userId, goodsType, goodsId);
|
||||
public boolean checkUserBuyFaceItem(Long userId, Long faceId, int goodsType, Long goodsId) {
|
||||
return orderRepository.checkUserBuyFaceItem(userId, faceId, goodsType, goodsId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,14 @@ public class PriceBiz {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -125,6 +133,7 @@ public class PriceBiz {
|
||||
|
||||
case "PHOTO_LOG":
|
||||
// 从 template 表查询pLog模板
|
||||
goodsList.add(new SimpleGoodsRespVO(scenicId, "pLog图<景区打包>", productType));
|
||||
List<PuzzleTemplateEntity> puzzleList = puzzleTemplateMapper.list(scenicId, null, null);
|
||||
puzzleList.stream()
|
||||
.map(template -> new SimpleGoodsRespVO(template.getId(), template.getName(), productType))
|
||||
@@ -272,7 +281,7 @@ public class PriceBiz {
|
||||
allContentsPurchased = false;
|
||||
break;
|
||||
}
|
||||
boolean hasPurchasedTemplate = orderBiz.checkUserBuyItem(userId, -1, videoEntities.getFirst().getVideoId());
|
||||
boolean hasPurchasedTemplate = orderBiz.checkUserBuyFaceItem(userId, faceId, -1, videoEntities.getFirst().getVideoId());
|
||||
if (!hasPurchasedTemplate) {
|
||||
allContentsPurchased = false;
|
||||
break;
|
||||
@@ -284,7 +293,7 @@ public class PriceBiz {
|
||||
if (scenicConfig != null) {
|
||||
// 检查录像集
|
||||
if (!Boolean.TRUE.equals(scenicConfig.getDisableSourceVideo())) {
|
||||
boolean hasPurchasedRecording = orderBiz.checkUserBuyItem(userId, 1, faceId);
|
||||
boolean hasPurchasedRecording = orderBiz.checkUserBuyFaceItem(userId, faceId, 1, faceId);
|
||||
if (!hasPurchasedRecording) {
|
||||
allContentsPurchased = false;
|
||||
}
|
||||
@@ -292,7 +301,7 @@ public class PriceBiz {
|
||||
|
||||
// 检查照片集
|
||||
if (allContentsPurchased && !Boolean.TRUE.equals(scenicConfig.getDisableSourceImage())) {
|
||||
boolean hasPurchasedPhoto = orderBiz.checkUserBuyItem(userId, 2, faceId);
|
||||
boolean hasPurchasedPhoto = orderBiz.checkUserBuyFaceItem(userId, faceId, 2, faceId);
|
||||
if (!hasPurchasedPhoto) {
|
||||
allContentsPurchased = false;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,12 @@ public enum SourceType {
|
||||
/**
|
||||
* 图片类型
|
||||
*/
|
||||
IMAGE(2, "图片");
|
||||
IMAGE(2, "图片"),
|
||||
|
||||
/**
|
||||
* AI微单类型
|
||||
*/
|
||||
AI_CAM(3, "AI微单");
|
||||
|
||||
private final int code;
|
||||
private final String description;
|
||||
@@ -68,4 +73,14 @@ public enum SourceType {
|
||||
public static boolean isImage(Integer 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 lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
@@ -55,6 +56,7 @@ public class AppOrderV2Controller {
|
||||
private final VideoTaskRepository videoTaskRepository;
|
||||
private final TemplateRepository templateRepository;
|
||||
private final VideoRepository videoRepository;
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
/**
|
||||
* 移动端价格计算
|
||||
@@ -86,7 +88,7 @@ public class AppOrderV2Controller {
|
||||
TaskEntity task = videoTaskRepository.getTaskById(video.getTaskId());
|
||||
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);
|
||||
product.setQuantity(count);
|
||||
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:
|
||||
log.warn("未知的商品类型,跳过重复购买检查: productType={}", product.getProductType());
|
||||
break;
|
||||
@@ -341,4 +351,9 @@ public class AppOrderV2Controller {
|
||||
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
|
||||
vo.setTemplateId(record.getTemplateId());
|
||||
IsBuyRespVO isBuyRespVO = orderBiz.isBuy(face.getScenicId(), face.getMemberId(), face.getId(), 5, record.getTemplateId());
|
||||
if (isBuyRespVO.isBuy()) {
|
||||
IsBuyRespVO isBuyScenic = orderBiz.isBuy(face.getScenicId(), face.getMemberId(), face.getId(), 5, face.getScenicId());
|
||||
if (isBuyScenic.isBuy()) {
|
||||
vo.setIsBuy(1);
|
||||
} else {
|
||||
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);
|
||||
IsBuyRespVO isBuyRespVO = orderBiz.isBuy(face.getScenicId(), face.getMemberId(), face.getId(), 5, record.getTemplateId());
|
||||
if (isBuyRespVO.isBuy()) {
|
||||
vo.setIsBuy(1);
|
||||
} 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;
|
||||
|
||||
@@ -55,7 +55,7 @@ public class AppTaskController {
|
||||
|
||||
@PostMapping("/submit")
|
||||
public ApiResponse<String> submitVideoTask(@RequestBody VideoTaskReq videoTaskReq) {
|
||||
taskService.createTaskByFaceIdAndTemplateId(videoTaskReq.getFaceId(),videoTaskReq.getTemplateId(),0);
|
||||
taskService.createTaskByFaceIdAndTemplateId(videoTaskReq.getFaceId(),videoTaskReq.getTemplateId(),false);
|
||||
return ApiResponse.success("成功");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ package com.ycwl.basic.controller.printer;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
|
||||
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.source.entity.MemberSourceEntity;
|
||||
import com.ycwl.basic.model.task.resp.SearchFaceRespVo;
|
||||
@@ -99,14 +100,20 @@ public class PrinterTvController {
|
||||
@GetMapping("/{sampleId}/qrcode")
|
||||
public void getQrcode(@PathVariable("sampleId") Long sampleId, HttpServletResponse response) throws Exception {
|
||||
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 {
|
||||
FaceSampleEntity faceSample = faceRepository.getFaceSample(sampleId);
|
||||
if (faceSample == null) {
|
||||
response.setStatus(404);
|
||||
return;
|
||||
}
|
||||
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");
|
||||
|
||||
@@ -74,22 +74,31 @@ public class PuzzleGenerationOrchestrator {
|
||||
// 3. 准备公共动态数据
|
||||
Map<String, String> baseDynamicData = buildBaseDynamicData(faceId, faceUrl, scenicBasic);
|
||||
|
||||
// 4. 遍历所有模板,逐个生成
|
||||
int successCount = 0;
|
||||
int failCount = 0;
|
||||
for (PuzzleTemplateDTO template : templateList) {
|
||||
try {
|
||||
generateSingleTemplate(scenicId, faceId, memberId, template, baseDynamicData);
|
||||
successCount++;
|
||||
} catch (Exception e) {
|
||||
log.error("拼图生成失败: scenicId={}, templateCode={}, templateName={}",
|
||||
scenicId, template.getCode(), template.getName(), e);
|
||||
failCount++;
|
||||
}
|
||||
// 4. 使用虚拟线程池并行生成所有模板
|
||||
java.util.concurrent.atomic.AtomicInteger successCount = new java.util.concurrent.atomic.AtomicInteger(0);
|
||||
java.util.concurrent.atomic.AtomicInteger failCount = new java.util.concurrent.atomic.AtomicInteger(0);
|
||||
|
||||
try (java.util.concurrent.ExecutorService executor = java.util.concurrent.Executors.newVirtualThreadPerTaskExecutor()) {
|
||||
// 为每个模板创建一个异步任务
|
||||
List<java.util.concurrent.CompletableFuture<Void>> futures = templateList.stream()
|
||||
.map(template -> java.util.concurrent.CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
generateSingleTemplate(scenicId, faceId, memberId, template, baseDynamicData);
|
||||
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={}, 总数={}, 成功={}, 失败={}",
|
||||
scenicId, templateList.size(), successCount, failCount);
|
||||
scenicId, templateList.size(), successCount.get(), failCount.get());
|
||||
|
||||
} catch (Exception e) {
|
||||
// 异步任务失败不影响主流程,仅记录日志
|
||||
|
||||
@@ -22,7 +22,13 @@ public enum PipelineScene {
|
||||
* 源图片超分辨率增强场景
|
||||
* 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 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;
|
||||
|
||||
/**
|
||||
* 缩放倍数,用于将所有定位和大小乘以该倍数
|
||||
* 默认值为 1.0(不缩放)
|
||||
*/
|
||||
@Builder.Default
|
||||
private final Double scale = 1.0;
|
||||
}
|
||||
|
||||
@@ -170,6 +170,12 @@ public class WatermarkStage extends AbstractPipelineStage<PhotoProcessContext> {
|
||||
info.setQrcodeFile(qrcodeFile);
|
||||
}
|
||||
|
||||
// 从 config 读取缩放倍数
|
||||
Double scale = config.getScale();
|
||||
if (scale != null) {
|
||||
info.setScale(scale);
|
||||
}
|
||||
|
||||
// 根据旋转状态自己处理 offsetLeft
|
||||
if (context.isRotationApplied()) {
|
||||
if (context.getImageRotation() == 90) {
|
||||
|
||||
@@ -33,6 +33,13 @@ public class WatermarkInfo {
|
||||
private Integer offsetLeft;
|
||||
private Integer offsetRight;
|
||||
|
||||
/**
|
||||
* 缩放倍数,用于将所有定位和大小乘以该倍数
|
||||
* 例如: scale=2.0 表示所有尺寸和位置都放大2倍
|
||||
* null 表示使用默认值1.0(不缩放)
|
||||
*/
|
||||
private Double scale;
|
||||
|
||||
public String getDatetimeLine() {
|
||||
if (datetimeLine == null) {
|
||||
datetimeLine = DateUtil.format(datetime, dtFormat);
|
||||
|
||||
@@ -64,11 +64,14 @@ public class PrinterDefaultWatermarkOperator implements IOperator {
|
||||
|
||||
@Override
|
||||
public File process(WatermarkInfo info) throws ImageWatermarkException {
|
||||
// 获取四边偏移值,优先使用传入的值,否则使用默认值
|
||||
int offsetTop = info.getOffsetTop() != null ? info.getOffsetTop() : DEFAULT_OFFSET_TOP;
|
||||
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;
|
||||
// 获取缩放倍数,默认为1.0(不缩放)
|
||||
double scale = info.getScale() != null ? info.getScale() : 1.0;
|
||||
|
||||
// 获取四边偏移值,优先使用传入的值,否则使用默认值,并应用缩放
|
||||
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 qrcodeImage;
|
||||
@@ -86,17 +89,26 @@ public class PrinterDefaultWatermarkOperator implements IOperator {
|
||||
} catch (IOException e) {
|
||||
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();
|
||||
g2d.setColor(BG_COLOR);
|
||||
g2d.fillRect(0, 0, newImage.getWidth(), newImage.getHeight());
|
||||
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
g2d.drawImage(baseImage, EXTRA_BORDER_PX, EXTRA_BORDER_PX, null);
|
||||
int newQrcodeHeight = QRCODE_SIZE;
|
||||
g2d.drawImage(baseImage, scaledExtraBorder, scaledExtraBorder, null);
|
||||
int newQrcodeHeight = scaledQrcodeSize;
|
||||
int newQrcodeWidth = (int) (newQrcodeHeight * 1.0 / qrcodeImage.getHeight() * qrcodeImage.getWidth());
|
||||
Font scenicFont = new Font(defaultFontName, Font.BOLD, SCENIC_FONT_SIZE);
|
||||
Font datetimeFont = new Font(defaultFontName, Font.BOLD, DATETIME_FONT_SIZE);
|
||||
Font scenicFont = new Font(defaultFontName, Font.BOLD, scaledScenicFontSize);
|
||||
Font datetimeFont = new Font(defaultFontName, Font.BOLD, scaledDatetimeFontSize);
|
||||
FontMetrics scenicFontMetrics = g2d.getFontMetrics(scenicFont);
|
||||
FontMetrics datetimeFontMetrics = g2d.getFontMetrics(datetimeFont);
|
||||
int scenicLineHeight = scenicFontMetrics.getHeight();
|
||||
@@ -106,13 +118,14 @@ public class PrinterDefaultWatermarkOperator implements IOperator {
|
||||
|
||||
// 二维码放置在左下角,距离左边缘图片宽度的5%,再加上左侧偏移
|
||||
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();
|
||||
|
||||
// 创建比二维码大10像素的白色圆形背景
|
||||
int whiteCircleSize = Math.max(newQrcodeWidth, newQrcodeHeight) + 10;
|
||||
// 创建比二维码大10像素的白色圆形背景(10像素也要缩放)
|
||||
int whiteCirclePadding = (int) (10 * scale);
|
||||
int whiteCircleSize = Math.max(newQrcodeWidth, newQrcodeHeight) + whiteCirclePadding;
|
||||
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);
|
||||
@@ -122,7 +135,7 @@ public class PrinterDefaultWatermarkOperator implements IOperator {
|
||||
// 用白色圆形尺寸裁切二维码(保持二维码原始尺寸,但用大圆裁切)
|
||||
Ellipse2D qrcodeCircle = new Ellipse2D.Double(whiteCircleX, whiteCircleY, whiteCircleSize, whiteCircleSize);
|
||||
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);
|
||||
|
||||
// 在圆形二维码中央绘制圆形头像
|
||||
@@ -130,7 +143,7 @@ public class PrinterDefaultWatermarkOperator implements IOperator {
|
||||
// 计算圆形头像的尺寸和位置
|
||||
int avatarDiameter = (int) (newQrcodeHeight * 0.45);
|
||||
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();
|
||||
@@ -149,10 +162,10 @@ public class PrinterDefaultWatermarkOperator implements IOperator {
|
||||
double faceHeight = faceImage.getHeight();
|
||||
double scaleX = avatarDiameter / faceWidth;
|
||||
double scaleY = avatarDiameter / faceHeight;
|
||||
double scale = Math.max(scaleX, scaleY); // 使用较大的缩放比例以填满圆形
|
||||
double faceScale = Math.max(scaleX, scaleY); // 使用较大的缩放比例以填满圆形
|
||||
|
||||
int scaledWidth = (int) (faceWidth * scale);
|
||||
int scaledHeight = (int) (faceHeight * scale);
|
||||
int scaledWidth = (int) (faceWidth * faceScale);
|
||||
int scaledHeight = (int) (faceHeight * faceScale);
|
||||
|
||||
// 计算居中位置
|
||||
int faceDrawX = avatarX + (avatarDiameter - scaledWidth) / 2;
|
||||
@@ -167,7 +180,7 @@ public class PrinterDefaultWatermarkOperator implements IOperator {
|
||||
}
|
||||
|
||||
// 计算文字与二维码垂直居中对齐的Y坐标
|
||||
int qrcodeTop = qrcodeOffsetY + QRCODE_OFFSET_Y;
|
||||
int qrcodeTop = qrcodeOffsetY + scaledQrcodeOffsetY;
|
||||
int qrcodeBottom = qrcodeTop + newQrcodeHeight;
|
||||
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 org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* AI相机人脸识别日志Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface FaceDetectLogAiCamMapper {
|
||||
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> listImageByFaceRelation(Long faceId);
|
||||
List<SourceEntity> listAiCamImageByFaceRelation(Long faceId);
|
||||
List<MemberSourceEntity> listByFaceRelation(Long faceId, Integer type);
|
||||
|
||||
SourceEntity getEntity(Long id);
|
||||
@@ -148,4 +149,20 @@ public interface SourceMapper {
|
||||
* @return source实体
|
||||
*/
|
||||
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;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 裁剪信息
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class Crop {
|
||||
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 Date createTime;
|
||||
private Date updateTime;
|
||||
private String crop;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ public enum ProductType {
|
||||
// 照片类
|
||||
PHOTO("PHOTO", "照片", 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),
|
||||
|
||||
// 视频类(素材视频)
|
||||
|
||||
@@ -89,22 +89,22 @@ public interface PriceCouponClaimRecordMapper extends BaseMapper<PriceCouponClai
|
||||
"LEFT JOIN price_coupon_config c ON r.coupon_id = c.id " +
|
||||
"<where>" +
|
||||
"<if test='userId != null'>" +
|
||||
"AND r.user_id = #{userId}" +
|
||||
"AND r.user_id = #{userId} " +
|
||||
"</if>" +
|
||||
"<if test='couponId != null'>" +
|
||||
"AND r.coupon_id = #{couponId}" +
|
||||
"AND r.coupon_id = #{couponId} " +
|
||||
"</if>" +
|
||||
"<if test='status != null'>" +
|
||||
"AND r.status = #{status}" +
|
||||
"AND r.status = #{status} " +
|
||||
"</if>" +
|
||||
"<if test='startTime != null and startTime != \"\"'>" +
|
||||
"AND r.claim_time >= #{startTime}" +
|
||||
"AND r.claim_time >= #{startTime} " +
|
||||
"</if>" +
|
||||
"<if test='endTime != null and endTime != \"\"'>" +
|
||||
"AND r.claim_time <= #{endTime}" +
|
||||
"AND r.claim_time <= #{endTime} " +
|
||||
"</if>" +
|
||||
"<if test='scenicId != null and scenicId != \"\"'>" +
|
||||
"AND r.scenic_id = #{scenicId}" +
|
||||
"AND r.scenic_id = #{scenicId} " +
|
||||
"</if>" +
|
||||
"</where>" +
|
||||
"ORDER BY r.create_time DESC" +
|
||||
|
||||
@@ -36,39 +36,62 @@ public class AutoCouponServiceImpl implements IAutoCouponService {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 查找该景区、该商品类型的首次打印优惠券配置
|
||||
Long couponId = findFirstCouponId(scenicId, productType);
|
||||
if (couponId == null) {
|
||||
// 2. 查找该景区、该商品类型的所有首次打印优惠券配置
|
||||
List<Long> couponIds = findFirstCouponIds(scenicId, productType);
|
||||
if (couponIds == null || couponIds.isEmpty()) {
|
||||
log.debug("景区未配置首次打印优惠券: scenicId={}, productType={}", scenicId, productType);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. 检查用户是否已领取过该券(领券即消耗首次资格)
|
||||
PriceCouponClaimRecord existingRecord = couponClaimRecordMapper.selectUserCouponRecord(
|
||||
memberId,
|
||||
couponId
|
||||
);
|
||||
log.info("找到{}张首次优惠券待发放: scenicId={}, productType={}, couponIds={}",
|
||||
couponIds.size(), scenicId, productType, couponIds);
|
||||
|
||||
if (existingRecord != null) {
|
||||
log.debug("用户已领取过首次优惠券,不重复发券: memberId={}, couponId={}, claimTime={}",
|
||||
memberId, couponId, existingRecord.getClaimTime());
|
||||
return false;
|
||||
// 3. 遍历所有优惠券,逐一检查并发放
|
||||
int successCount = 0;
|
||||
int skipCount = 0;
|
||||
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. 自动发券
|
||||
CouponClaimRequest request = new CouponClaimRequest(
|
||||
memberId,
|
||||
couponId,
|
||||
scenicId.toString(),
|
||||
"AUTO_GRANT" // 标记为自动发券来源
|
||||
);
|
||||
log.info("自动发券完成: memberId={}, 成功{}张, 跳过{}张, 失败{}张",
|
||||
memberId, successCount, skipCount, failCount);
|
||||
|
||||
couponService.claimCoupon(request);
|
||||
|
||||
log.info("成功自动发放首次打印优惠券: memberId={}, faceId={}, scenicId={}, productType={}, couponId={}",
|
||||
memberId, faceId, scenicId, productType, couponId);
|
||||
|
||||
return true;
|
||||
// 只要有一张成功就返回true
|
||||
return successCount > 0;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("自动发券失败: memberId={}, faceId={}, scenicId={}, productType={}",
|
||||
@@ -78,14 +101,15 @@ public class AutoCouponServiceImpl implements IAutoCouponService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找指定景区、指定商品类型的首次打印优惠券ID
|
||||
* 查找指定景区、指定商品类型的所有首次打印优惠券ID
|
||||
* 规则:优惠券名称包含 "首次" 且 适用商品类型包含目标类型
|
||||
*
|
||||
* @param scenicId 景区ID
|
||||
* @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 {
|
||||
// 查询该景区的有效优惠券
|
||||
List<PriceCouponConfig> coupons = couponConfigMapper.selectValidCouponsByScenicId(
|
||||
@@ -100,17 +124,22 @@ public class AutoCouponServiceImpl implements IAutoCouponService {
|
||||
String applicableProducts = coupon.getApplicableProducts();
|
||||
if (applicableProducts != null &&
|
||||
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);
|
||||
return null;
|
||||
if (couponIds.isEmpty()) {
|
||||
log.debug("未找到匹配的首次打印优惠券: scenicId={}, productType={}", scenicId, productType);
|
||||
}
|
||||
|
||||
return couponIds;
|
||||
|
||||
} catch (Exception 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个设备的图片)
|
||||
*/
|
||||
DEVICE_IMAGE("DEVICE_IMAGE", "设备图片"),
|
||||
DEVICE_THUMB_IMAGE("DEVICE_THUMB_IMAGE", "设备缩略图片"),
|
||||
|
||||
/**
|
||||
* 静态值(直接使用fallbackValue)
|
||||
|
||||
@@ -97,6 +97,17 @@ public class DeviceRepository {
|
||||
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) {
|
||||
log.debug("根据设备编号获取设备信息, deviceNo: {}", 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_ITEMS_CACHE_KEY = "order:%s:items";
|
||||
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) {
|
||||
if (redisTemplate.hasKey(String.format(ORDER_CACHE_KEY, orderId))) {
|
||||
@@ -62,40 +62,12 @@ public class OrderRepository {
|
||||
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) {
|
||||
OrderEntity order = getOrder(orderId);
|
||||
redisTemplate.delete(String.format(ORDER_CACHE_KEY, orderId));
|
||||
getOrderItems(orderId).forEach(orderItem -> {
|
||||
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));
|
||||
}
|
||||
@@ -105,4 +77,37 @@ public class OrderRepository {
|
||||
orderMapper.updateOrder(updateEntity);
|
||||
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;
|
||||
|
||||
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.model.pc.face.entity.FaceEntity;
|
||||
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
|
||||
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
|
||||
import com.ycwl.basic.model.pc.device.entity.DeviceEntity;
|
||||
import com.ycwl.basic.pipeline.core.Pipeline;
|
||||
import com.ycwl.basic.pipeline.core.PipelineBuilder;
|
||||
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.storage.StorageFactory;
|
||||
import com.ycwl.basic.storage.adapters.IStorageAdapter;
|
||||
import com.ycwl.basic.storage.exceptions.StorageUnsupportedException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
@@ -16,18 +28,29 @@ import org.springframework.stereotype.Component;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
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
|
||||
private SourceMapper sourceMapper;
|
||||
@Autowired
|
||||
private RedisTemplate<String, String> redisTemplate;
|
||||
@Autowired
|
||||
private IVoucherService iVoucherService;
|
||||
@Autowired
|
||||
private FaceRepository faceRepository;
|
||||
@Autowired
|
||||
private TemplateRepository templateRepository;
|
||||
@@ -35,12 +58,21 @@ public class SourceRepository {
|
||||
private DeviceRepository deviceRepository;
|
||||
@Autowired
|
||||
private MemberRelationRepository memberRelationRepository;
|
||||
@Autowired
|
||||
private ScenicRepository scenicRepository;
|
||||
|
||||
public void addSource(SourceEntity source) {
|
||||
sourceMapper.add(source);
|
||||
}
|
||||
|
||||
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();
|
||||
memberSource.setMemberId(memberId);
|
||||
memberSource.setFaceId(faceId);
|
||||
@@ -49,6 +81,143 @@ public class SourceRepository {
|
||||
memberSource.setIsBuy(1);
|
||||
sourceMapper.updateRelation(memberSource);
|
||||
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) {
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package com.ycwl.basic.repository;
|
||||
|
||||
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.utils.JacksonUtil;
|
||||
import com.ycwl.basic.mapper.VideoMapper;
|
||||
@@ -116,4 +113,25 @@ public class VideoRepository {
|
||||
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.setUrl(message.getSourceUrl()); // 使用sourceUrl,不使用缩略图
|
||||
entity.setThumbUrl(message.getThumbnailUrl()); // 设置缩略图URL
|
||||
entity.setType(2); // 照片类型
|
||||
entity.setType(message.getSourceType()); // 照片类型
|
||||
|
||||
// 人脸样本ID处理
|
||||
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.setGoodsId(sourceRespVO.getId());
|
||||
String shootingTime = DateUtil.format(sourceRespVO.getCreateTime(), "yyyy.MM.dd HH:mm:ss");
|
||||
if (i < 10) {
|
||||
goodsDetailVO.setGoodsName(goodsNamePrefix + "0" + i + " " + shootingTime);
|
||||
if (Integer.valueOf(3).equals(sourceType)) {
|
||||
goodsDetailVO.setGoodsName("拍摄时间:" + shootingTime);
|
||||
} 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());
|
||||
try {
|
||||
@@ -573,7 +577,7 @@ public class GoodsServiceImpl implements GoodsService {
|
||||
if (query.getGoodsId() != null) {
|
||||
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 -> {
|
||||
GoodsUrlVO goodsUrlVO = new GoodsUrlVO();
|
||||
goodsUrlVO.setGoodsType(source.getType());
|
||||
@@ -592,7 +596,7 @@ public class GoodsServiceImpl implements GoodsService {
|
||||
}
|
||||
return true;
|
||||
}).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 (!isBuy.isBuy()) {
|
||||
return Collections.emptyList();
|
||||
@@ -829,6 +833,9 @@ public class GoodsServiceImpl implements GoodsService {
|
||||
} else if (type == 2) {
|
||||
goodsPageVO.setGoodsName("照片集");
|
||||
goodsPageVO.setTemplateCoverUrl(scenicConfig.getString("photo_cover_url"));
|
||||
} else if (type == 3) {
|
||||
goodsPageVO.setGoodsName("AI微单");
|
||||
goodsPageVO.setTemplateCoverUrl(scenicConfig.getString("ai_camera_cover_url"));
|
||||
} else {
|
||||
goodsPageVO.setGoodsName("未知商品");
|
||||
}
|
||||
|
||||
@@ -11,11 +11,10 @@ public interface FaceDetectLogAiCamService {
|
||||
/**
|
||||
* 搜索人脸库并保存日志
|
||||
* @param scenicId 景区ID
|
||||
* @param deviceId 设备ID
|
||||
* @param faceSampleId 人脸样本ID
|
||||
* @param faceId 人脸样本ID
|
||||
* @param faceUrl 人脸URL
|
||||
* @param adapter 人脸适配器
|
||||
* @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.entity.SearchFaceResp;
|
||||
import com.ycwl.basic.integration.device.dto.device.DeviceV2DTO;
|
||||
import com.ycwl.basic.mapper.FaceDetectLogAiCamMapper;
|
||||
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.utils.JacksonUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -11,6 +13,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@@ -18,17 +21,28 @@ import java.util.Date;
|
||||
public class FaceDetectLogAiCamServiceImpl implements FaceDetectLogAiCamService {
|
||||
|
||||
private final FaceDetectLogAiCamMapper faceDetectLogAiCamMapper;
|
||||
private final DeviceRepository deviceRepository;
|
||||
|
||||
@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;
|
||||
|
||||
|
||||
SearchFaceResp resp = null;
|
||||
try {
|
||||
// 调用适配器搜索人脸
|
||||
resp = adapter.searchFace(dbName, faceUrl);
|
||||
} 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();
|
||||
logEntity.setScenicId(scenicId);
|
||||
logEntity.setDeviceId(deviceId);
|
||||
logEntity.setFaceId(faceSampleId);
|
||||
logEntity.setFaceId(faceId);
|
||||
logEntity.setDbName(dbName);
|
||||
logEntity.setFaceUrl(faceUrl);
|
||||
logEntity.setCreateTime(new Date());
|
||||
@@ -47,17 +61,17 @@ public class FaceDetectLogAiCamServiceImpl implements FaceDetectLogAiCamService
|
||||
// SearchFaceResp 的 getOriginalFaceScore 通常是图片质量分,getFirstMatchRate 是最佳匹配分
|
||||
// 需根据 SearchFaceResp 定义确认。假设 getFirstMatchRate() 是匹配分
|
||||
// 实际上 searchFace 返回的是匹配列表。
|
||||
|
||||
|
||||
// 记录原始响应
|
||||
logEntity.setMatchRawResult(JacksonUtil.toJSONString(resp));
|
||||
} else {
|
||||
logEntity.setMatchRawResult("{\"error\": \"search failed or exception\"}");
|
||||
logEntity.setMatchRawResult("{\"error\": \"search failed or exception\"}");
|
||||
}
|
||||
|
||||
faceDetectLogAiCamMapper.add(logEntity);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("保存AI相机人脸识别日志失败: faceSampleId={}", faceSampleId, e);
|
||||
log.error("保存AI相机人脸识别日志失败: faceId={}", faceId, e);
|
||||
}
|
||||
|
||||
return resp;
|
||||
|
||||
@@ -9,7 +9,6 @@ import com.ycwl.basic.constant.BaseContextHandler;
|
||||
import com.ycwl.basic.exception.BaseException;
|
||||
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
|
||||
import com.ycwl.basic.facebody.entity.SearchFaceResultItem;
|
||||
import com.ycwl.basic.integration.common.manager.DeviceConfigManager;
|
||||
import com.ycwl.basic.mapper.FaceSampleMapper;
|
||||
import com.ycwl.basic.mapper.ProjectMapper;
|
||||
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.VideoTaskRepository;
|
||||
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.ScenicService;
|
||||
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.orchestrator.FaceMatchingOrchestrator;
|
||||
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.VideoRecreationHandler;
|
||||
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.utils.*;
|
||||
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.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -193,6 +193,8 @@ public class FaceServiceImpl implements FaceService {
|
||||
private IPriceCalculationService iPriceCalculationService;
|
||||
@Autowired
|
||||
private PuzzleTemplateMapper puzzleTemplateMapper;
|
||||
@Autowired
|
||||
private FaceDetectLogAiCamService faceDetectLogAiCamService;
|
||||
|
||||
@Override
|
||||
public ApiResponse<PageInfo<FaceRespVO>> pageQuery(FaceReqQuery faceReqQuery) {
|
||||
@@ -312,13 +314,16 @@ public class FaceServiceImpl implements FaceService {
|
||||
Long finalFaceId = newFaceId;
|
||||
Thread thread = new Thread(() -> printerService.autoAddPhotosToPreferPrint(finalFaceId), "auto-add-print-" + newFaceId);
|
||||
thread.start();
|
||||
if (org.apache.commons.lang3.Strings.CI.equals("print", scene)) {
|
||||
if (Strings.CI.equals("print", scene)) {
|
||||
try {
|
||||
thread.join();
|
||||
} catch (InterruptedException ignore) {
|
||||
|
||||
}
|
||||
}
|
||||
if (Strings.CI.equals("aiCam", scene)) {
|
||||
faceDetectLogAiCamService.searchAndLog(scenicId, newFaceId, faceUrl, faceBodyAdapter);
|
||||
}
|
||||
|
||||
return resp;
|
||||
}
|
||||
@@ -468,9 +473,9 @@ public class FaceServiceImpl implements FaceService {
|
||||
contentPageVO.setLockType(1);
|
||||
}
|
||||
}
|
||||
boolean buy = orderBiz.checkUserBuyItem(userId, contentPageVO.getGoodsType(), contentPageVO.getContentId());
|
||||
boolean buy = orderBiz.checkUserBuyFaceItem(userId, faceId, contentPageVO.getGoodsType(), contentPageVO.getContentId());
|
||||
if (!buy) {
|
||||
buy = orderBiz.checkUserBuyItem(userId, -1, contentPageVO.getTemplateId());
|
||||
buy = orderBiz.checkUserBuyFaceItem(userId, faceId, -1, contentPageVO.getTemplateId());
|
||||
}
|
||||
if (buy) {
|
||||
contentPageVO.setIsBuy(1);
|
||||
@@ -482,21 +487,25 @@ public class FaceServiceImpl implements FaceService {
|
||||
List<PuzzleTemplateEntity> puzzleTemplateEntityList = puzzleTemplateMapper.list(face.getScenicId(), null, 1);
|
||||
if (!puzzleTemplateEntityList.isEmpty()) {
|
||||
List<PuzzleGenerationRecordEntity> records = puzzleGenerationRecordMapper.listByFaceId(faceId);
|
||||
puzzleTemplateEntityList.forEach(template -> {
|
||||
Optional<PuzzleGenerationRecordEntity> optionalRecord = records.stream().filter(r -> r.getTemplateId().equals(template.getId())).findFirst();
|
||||
ContentPageVO sfpContent = new ContentPageVO();
|
||||
sfpContent.setName(template.getName());
|
||||
sfpContent.setGroup("plog");
|
||||
sfpContent.setScenicId(face.getScenicId());
|
||||
sfpContent.setContentType(3);
|
||||
sfpContent.setSourceType(3);
|
||||
sfpContent.setLockType(-1);
|
||||
sfpContent.setContentId(optionalRecord.map(PuzzleGenerationRecordEntity::getId).orElse(null));
|
||||
sfpContent.setTemplateId(template.getId());
|
||||
sfpContent.setTemplateCoverUrl(template.getCoverImage());
|
||||
sfpContent.setGoodsType(3);
|
||||
sfpContent.setSort(0);
|
||||
if (optionalRecord.isPresent()) {
|
||||
PuzzleTemplateEntity template = puzzleTemplateEntityList.getFirst();
|
||||
Optional<PuzzleGenerationRecordEntity> optionalRecord = records.stream().filter(r -> r.getTemplateId().equals(template.getId())).findFirst();
|
||||
ContentPageVO sfpContent = new ContentPageVO();
|
||||
sfpContent.setName(template.getName());
|
||||
sfpContent.setGroup("plog");
|
||||
sfpContent.setScenicId(face.getScenicId());
|
||||
sfpContent.setContentType(3);
|
||||
sfpContent.setSourceType(3);
|
||||
sfpContent.setLockType(-1);
|
||||
sfpContent.setContentId(optionalRecord.map(PuzzleGenerationRecordEntity::getId).orElse(null));
|
||||
sfpContent.setTemplateId(template.getId());
|
||||
sfpContent.setTemplateCoverUrl(template.getCoverImage());
|
||||
sfpContent.setGoodsType(3);
|
||||
sfpContent.setSort(0);
|
||||
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());
|
||||
if (isBuyRespVO.isBuy()) {
|
||||
sfpContent.setIsBuy(1);
|
||||
@@ -504,24 +513,24 @@ public class FaceServiceImpl implements FaceService {
|
||||
sfpContent.setIsBuy(0);
|
||||
}
|
||||
}
|
||||
PriceCalculationRequest calculationRequest = new PriceCalculationRequest();
|
||||
ProductItem productItem = new ProductItem();
|
||||
productItem.setProductType(ProductType.PHOTO_LOG);
|
||||
productItem.setProductId(template.getId().toString());
|
||||
productItem.setPurchaseCount(1);
|
||||
productItem.setScenicId(face.getScenicId().toString());
|
||||
calculationRequest.setProducts(Collections.singletonList(productItem));
|
||||
calculationRequest.setUserId(face.getMemberId());
|
||||
calculationRequest.setFaceId(face.getId());
|
||||
calculationRequest.setPreviewOnly(true); // 仅查询价格,不实际使用优惠
|
||||
PriceCalculationResult calculationResult = iPriceCalculationService.calculatePrice(calculationRequest);
|
||||
if (calculationResult.getFinalAmount().compareTo(BigDecimal.ZERO) > 0) {
|
||||
sfpContent.setFreeCount(0);
|
||||
} else {
|
||||
sfpContent.setFreeCount(1);
|
||||
}
|
||||
contentList.add(1, sfpContent);
|
||||
});
|
||||
}
|
||||
PriceCalculationRequest calculationRequest = new PriceCalculationRequest();
|
||||
ProductItem productItem = new ProductItem();
|
||||
productItem.setProductType(ProductType.PHOTO_LOG);
|
||||
productItem.setProductId(template.getId().toString());
|
||||
productItem.setPurchaseCount(1);
|
||||
productItem.setScenicId(face.getScenicId().toString());
|
||||
calculationRequest.setProducts(Collections.singletonList(productItem));
|
||||
calculationRequest.setUserId(face.getMemberId());
|
||||
calculationRequest.setFaceId(face.getId());
|
||||
calculationRequest.setPreviewOnly(true); // 仅查询价格,不实际使用优惠
|
||||
PriceCalculationResult calculationResult = iPriceCalculationService.calculatePrice(calculationRequest);
|
||||
if (calculationResult.getFinalAmount().compareTo(BigDecimal.ZERO) > 0) {
|
||||
sfpContent.setFreeCount(0);
|
||||
} else {
|
||||
sfpContent.setFreeCount(1);
|
||||
}
|
||||
contentList.add(1, sfpContent);
|
||||
}
|
||||
SourceReqQuery sourceReqQuery = new SourceReqQuery();
|
||||
sourceReqQuery.setScenicId(face.getScenicId());
|
||||
@@ -531,20 +540,29 @@ public class FaceServiceImpl implements FaceService {
|
||||
List<SourceRespVO> sourceList = sourceMapper.queryByRelation(sourceReqQuery);
|
||||
ContentPageVO sourceVideoContent = new ContentPageVO();
|
||||
ContentPageVO sourceImageContent = new ContentPageVO();
|
||||
ContentPageVO sourceAiCamContent = new ContentPageVO();
|
||||
sourceVideoContent.setName("录像集");
|
||||
sourceImageContent.setName("照片集");
|
||||
sourceAiCamContent.setName("AI微单");
|
||||
sourceVideoContent.setSort(9999);
|
||||
sourceImageContent.setSort(9999);
|
||||
sourceAiCamContent.setSort(9999);
|
||||
sourceVideoContent.setScenicId(face.getScenicId());
|
||||
sourceImageContent.setScenicId(face.getScenicId());
|
||||
sourceAiCamContent.setScenicId(face.getScenicId());
|
||||
sourceVideoContent.setGoodsType(1);
|
||||
sourceImageContent.setGoodsType(2);
|
||||
sourceAiCamContent.setGoodsType(3);
|
||||
sourceVideoContent.setContentType(2);
|
||||
sourceImageContent.setContentType(2);
|
||||
sourceAiCamContent.setContentType(2);
|
||||
sourceVideoContent.setLockType(-1);
|
||||
sourceImageContent.setLockType(-1);
|
||||
sourceAiCamContent.setLockType(-1);
|
||||
sourceVideoContent.setGroup("直出原片");
|
||||
sourceImageContent.setGroup("直出原片");
|
||||
sourceAiCamContent.setGroup("直出原片");
|
||||
ScenicConfigManager configManager = scenicRepository.getScenicConfigManager(face.getScenicId());
|
||||
if (!scenicConfigFacade.isDisableSourceImage(face.getScenicId())) {
|
||||
IsBuyRespVO isBuyRespVO = orderBiz.isBuy(face.getScenicId(), userId, faceId, SourceType.IMAGE.getCode(), faceId);
|
||||
sourceImageContent.setSourceType(isBuyRespVO.getGoodsType());
|
||||
@@ -583,18 +601,40 @@ public class FaceServiceImpl implements FaceService {
|
||||
sourceVideoContent.setFreeCount((int) freeCount);
|
||||
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) -> {
|
||||
if (type == 1) {
|
||||
sourceVideoContent.setSourceType(1);
|
||||
sourceVideoContent.setLockType(-1);
|
||||
sourceVideoContent.setTemplateCoverUrl(list.getFirst().getUrl());
|
||||
} else {
|
||||
} else if (type == 2) {
|
||||
sourceImageContent.setSourceType(2);
|
||||
sourceImageContent.setLockType(-1);
|
||||
sourceImageContent.setTemplateCoverUrl(list.getFirst().getUrl());
|
||||
if (Strings.isBlank(sourceVideoContent.getTemplateCoverUrl())) {
|
||||
if (StringUtils.isBlank(sourceVideoContent.getTemplateCoverUrl())) {
|
||||
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;
|
||||
@@ -803,7 +843,7 @@ public class FaceServiceImpl implements FaceService {
|
||||
return List.of();
|
||||
}
|
||||
String matchResult = face.getMatchResult();
|
||||
if (matchResult == null || Strings.isBlank(matchResult)) {
|
||||
if (matchResult == null || StringUtils.isBlank(matchResult)) {
|
||||
return List.of();
|
||||
}
|
||||
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(face.getScenicId());
|
||||
@@ -1009,7 +1049,7 @@ public class FaceServiceImpl implements FaceService {
|
||||
handleCustomFaceMatching(faceId, finalSampleList);
|
||||
}
|
||||
|
||||
if (Strings.isNotBlank(req.getRemark())) {
|
||||
if (StringUtils.isNotBlank(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());
|
||||
|
||||
String matchResultJson = face.getMatchResult();
|
||||
if (Strings.isBlank(matchResultJson)) {
|
||||
if (StringUtils.isBlank(matchResultJson)) {
|
||||
detail.setAcceptedSamples(Collections.emptyList());
|
||||
detail.setFilteredSamples(Collections.emptyList());
|
||||
return detail;
|
||||
@@ -1155,7 +1195,7 @@ public class FaceServiceImpl implements FaceService {
|
||||
}
|
||||
|
||||
private List<Long> parseMatchSampleIds(String matchSampleIds) {
|
||||
if (Strings.isBlank(matchSampleIds)) {
|
||||
if (StringUtils.isBlank(matchSampleIds)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
String[] segments = matchSampleIds.split(",");
|
||||
@@ -1170,7 +1210,7 @@ public class FaceServiceImpl implements FaceService {
|
||||
}
|
||||
|
||||
private Long parseLongSilently(String value) {
|
||||
if (Strings.isBlank(value)) {
|
||||
if (StringUtils.isBlank(value)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
@@ -1222,10 +1262,10 @@ public class FaceServiceImpl implements FaceService {
|
||||
if (sourceEntity == null) {
|
||||
return null;
|
||||
}
|
||||
if (!Strings.isBlank(sourceEntity.getThumbUrl())) {
|
||||
if (!StringUtils.isBlank(sourceEntity.getThumbUrl())) {
|
||||
return sourceEntity.getThumbUrl();
|
||||
}
|
||||
if (!Strings.isBlank(sourceEntity.getUrl())) {
|
||||
if (!StringUtils.isBlank(sourceEntity.getUrl())) {
|
||||
return sourceEntity.getUrl();
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -177,6 +177,9 @@ public class OrderServiceImpl implements OrderService {
|
||||
} else if (Integer.valueOf(2).equals(orderItemList.getFirst().getGoodsType())) {
|
||||
item.setGoodsName("照片集");
|
||||
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())) {
|
||||
item.setOrderType("旅行Vlog");
|
||||
item.setGoodsName(orderItemList.getFirst().getGoodsName());
|
||||
@@ -237,6 +240,9 @@ public class OrderServiceImpl implements OrderService {
|
||||
} else if (Integer.valueOf(2).equals(orderItemList.getFirst().getGoodsType())) {
|
||||
item.setGoodsName("照片集");
|
||||
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())) {
|
||||
item.setOrderType("旅行Vlog");
|
||||
item.setGoodsName(orderItemList.getFirst().getGoodsName());
|
||||
@@ -344,6 +350,26 @@ public class OrderServiceImpl implements OrderService {
|
||||
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
|
||||
List<MemberPrintResp> list = printerMapper.getUserPhotoByIds(orderItemList.stream().map(OrderItemVO::getGoodsId).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.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())) {
|
||||
item.setCoverList(Collections.singletonList(item.getCoverUrl()));
|
||||
VideoEntity video = videoRepository.getVideo(item.getGoodsId());
|
||||
@@ -682,6 +715,9 @@ public class OrderServiceImpl implements OrderService {
|
||||
} else if (Integer.valueOf(2).equals(orderItemList.getFirst().getGoodsType())) {
|
||||
item.setGoodsName("照片集");
|
||||
item.setOrderType("照片集");
|
||||
} else if (Integer.valueOf(13).equals(orderItemList.getFirst().getGoodsType())) {
|
||||
item.setGoodsName("打卡点拍照");
|
||||
item.setOrderType("打卡点拍照");
|
||||
} else if (Integer.valueOf(0).equals(orderItemList.getFirst().getGoodsType())) {
|
||||
item.setOrderType("旅行Vlog");
|
||||
item.setGoodsName(orderItemList.getFirst().getGoodsName());
|
||||
@@ -880,15 +916,13 @@ public class OrderServiceImpl implements OrderService {
|
||||
List<OrderItemEntity> orderItems = goodsList.stream().map(goods -> {
|
||||
OrderItemEntity orderItem = new OrderItemEntity();
|
||||
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.setGoodsType(1);
|
||||
} else if (Long.valueOf(2L).equals(goods.getGoodsId())) {
|
||||
} else if (Integer.valueOf(2).equals(goods.getGoodsType())) {
|
||||
orderItem.setGoodsId(batchOrderReqVO.getFaceId());
|
||||
orderItem.setGoodsType(2);
|
||||
} else {
|
||||
// templateId
|
||||
orderItem.setGoodsId(goods.getGoodsId());
|
||||
} else if (Integer.valueOf(0).equals(goods.getGoodsType())) {
|
||||
orderItem.setGoodsType(-1);
|
||||
}
|
||||
return orderItem;
|
||||
@@ -940,11 +974,13 @@ public class OrderServiceImpl implements OrderService {
|
||||
case PHOTO_SET -> 2;
|
||||
case VLOG_VIDEO -> 0;
|
||||
case RECORDING_SET -> 1;
|
||||
case AI_CAM_PHOTO_SET -> 13;
|
||||
default -> 0;
|
||||
};
|
||||
Long goodsId = switch (productItem.getProductType()) {
|
||||
case PHOTO_LOG -> Long.valueOf(productItem.getProductId());
|
||||
case PHOTO_SET, RECORDING_SET -> face.getId();
|
||||
case AI_CAM_PHOTO_SET -> face.getId();
|
||||
case VLOG_VIDEO -> {
|
||||
List<MemberVideoEntity> videos = memberRelationRepository.listRelationByFaceAndTemplate(face.getId(), Long.valueOf(productItem.getProductId()));
|
||||
yield videos.getFirst().getVideoId();
|
||||
@@ -1017,6 +1053,13 @@ public class OrderServiceImpl implements OrderService {
|
||||
orderItem.setGoodsType(type);
|
||||
orderItem.setOrderId(order.getId());
|
||||
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 {
|
||||
self.saveOrderInTransaction(order, orderItems, haveOldOrder);
|
||||
|
||||
@@ -390,42 +390,51 @@ public class FaceMatchingOrchestrator {
|
||||
baseDynamicData.put("scenicText", scenicBasic.getName());
|
||||
baseDynamicData.put("dateStr", DateUtil.format(new Date(), "yyyy.MM.dd"));
|
||||
|
||||
// 遍历所有模板,逐个生成
|
||||
int successCount = 0;
|
||||
int failCount = 0;
|
||||
for (PuzzleTemplateDTO template : templateList) {
|
||||
try {
|
||||
log.info("开始生成拼图: scenicId={}, templateCode={}, templateName={}",
|
||||
scenicId, template.getCode(), template.getName());
|
||||
// 使用虚拟线程池并行生成所有模板
|
||||
java.util.concurrent.atomic.AtomicInteger successCount = new java.util.concurrent.atomic.AtomicInteger(0);
|
||||
java.util.concurrent.atomic.AtomicInteger failCount = new java.util.concurrent.atomic.AtomicInteger(0);
|
||||
|
||||
// 构建生成请求
|
||||
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);
|
||||
try (java.util.concurrent.ExecutorService executor = java.util.concurrent.Executors.newVirtualThreadPerTaskExecutor()) {
|
||||
// 为每个模板创建一个异步任务
|
||||
List<java.util.concurrent.CompletableFuture<Void>> futures = templateList.stream()
|
||||
.map(template -> java.util.concurrent.CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
log.info("开始生成拼图: scenicId={}, templateCode={}, templateName={}",
|
||||
scenicId, template.getCode(), template.getName());
|
||||
|
||||
// 调用拼图生成服务
|
||||
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());
|
||||
successCount++;
|
||||
// 调用拼图生成服务
|
||||
PuzzleGenerateResponse response = puzzleGenerateService.generate(generateRequest);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("拼图生成失败: scenicId={}, templateCode={}, templateName={}",
|
||||
scenicId, template.getCode(), template.getName(), e);
|
||||
failCount++;
|
||||
}
|
||||
log.info("拼图生成成功: scenicId={}, templateCode={}, imageUrl={}",
|
||||
scenicId, template.getCode(), response.getImageUrl());
|
||||
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={}, 总数={}, 成功={}, 失败={}",
|
||||
scenicId, templateList.size(), successCount, failCount);
|
||||
log.info("景区拼图模板批量生成完成: scenicId={}, 总数={}, 成功={}, 失败={}",
|
||||
scenicId, templateList.size(), successCount.get(), failCount.get());
|
||||
|
||||
} 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.ImageEnhanceStage;
|
||||
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.RestoreOrientationStage;
|
||||
import com.ycwl.basic.image.pipeline.stages.UpdateMemberPrintStage;
|
||||
import com.ycwl.basic.image.pipeline.stages.UploadStage;
|
||||
import com.ycwl.basic.image.pipeline.stages.WatermarkConfig;
|
||||
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.PrinterMapper;
|
||||
import com.ycwl.basic.mapper.SourceMapper;
|
||||
import com.ycwl.basic.model.Crop;
|
||||
import com.ycwl.basic.model.PrinterOrderItem;
|
||||
import com.ycwl.basic.model.mobile.face.FaceRecognizeResp;
|
||||
import com.ycwl.basic.model.mobile.order.PriceObj;
|
||||
@@ -347,6 +351,8 @@ public class PrinterServiceImpl implements PrinterService {
|
||||
|
||||
log.info("照片裁剪成功: memberId={}, scenicId={}, 原图={}, 裁剪后={}, 尺寸={}x{}",
|
||||
memberId, scenicId, url, cropUrl, printWidth, printHeight);
|
||||
String crop = JacksonUtil.toJSONString(new Crop(270));
|
||||
entity.setCrop(crop);
|
||||
} finally {
|
||||
// 清理临时文件
|
||||
if (croppedFile != null && croppedFile.exists()) {
|
||||
@@ -467,6 +473,34 @@ public class PrinterServiceImpl implements PrinterService {
|
||||
|
||||
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) {
|
||||
try {
|
||||
autoCouponService.autoGrantCoupon(
|
||||
@@ -505,6 +539,10 @@ public class PrinterServiceImpl implements PrinterService {
|
||||
}
|
||||
|
||||
String url = byId.getUrl();
|
||||
// 特殊兼容处理
|
||||
if (Integer.valueOf(3).equals(byId.getType())) {
|
||||
url = byId.getThumbUrl().replace("_t.", "_o.");
|
||||
}
|
||||
MemberPrintEntity entity = new MemberPrintEntity();
|
||||
entity.setMemberId(memberId);
|
||||
entity.setScenicId(scenicId);
|
||||
@@ -540,6 +578,7 @@ public class PrinterServiceImpl implements PrinterService {
|
||||
|
||||
// 使用smartCropAndFill裁剪图片
|
||||
File croppedFile = ImageUtils.smartCropAndFill(url, printWidth, printHeight);
|
||||
entity.setCrop(JacksonUtil.toJSONString(new Crop(270)));
|
||||
|
||||
try {
|
||||
// 上传裁剪后的图片
|
||||
@@ -800,6 +839,18 @@ public class PrinterServiceImpl implements PrinterService {
|
||||
* @return 处理后的URL,失败返回原URL
|
||||
*/
|
||||
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);
|
||||
|
||||
PhotoProcessContext context = PhotoProcessContext.fromPrinterOrderItem(orderItem, scenicId);
|
||||
@@ -812,11 +863,15 @@ public class PrinterServiceImpl implements PrinterService {
|
||||
// 设置管线场景为图片打印
|
||||
context.setScene(PipelineScene.IMAGE_PRINT);
|
||||
|
||||
// 根据sourceId判断图片来源
|
||||
// sourceId > 0: IPC设备拍摄
|
||||
// sourceId == null: 手机上传
|
||||
// sourceId == 0: 拼图(暂定为UNKNOWN)
|
||||
// 处理图像增强选项
|
||||
if (needEnhance != null && needEnhance) {
|
||||
context.setStageState("image_enhance", true);
|
||||
}
|
||||
|
||||
// 根据sourceId判断图片来源和source类型
|
||||
SourceEntity source = null;
|
||||
if (item.getSourceId() != null && item.getSourceId() > 0) {
|
||||
source = sourceMapper.getEntity(item.getSourceId());
|
||||
context.setSource(ImageSource.IPC);
|
||||
} else if (item.getSourceId() == null) {
|
||||
context.setSource(ImageSource.PHONE);
|
||||
@@ -825,15 +880,49 @@ public class PrinterServiceImpl implements PrinterService {
|
||||
}
|
||||
|
||||
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);
|
||||
// 创建普通照片管线
|
||||
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);
|
||||
} else {
|
||||
// 拼图
|
||||
// 拼图处理流程
|
||||
prepareStorageAdapter(context);
|
||||
pipeline = createPuzzlePipeline();
|
||||
}
|
||||
@@ -867,7 +956,7 @@ public class PrinterServiceImpl implements PrinterService {
|
||||
* @param qrCodeFile 二维码文件
|
||||
* @return WatermarkConfig
|
||||
*/
|
||||
private WatermarkConfig prepareWatermarkConfig(PhotoProcessContext context, File qrCodeFile) {
|
||||
private WatermarkConfig prepareWatermarkConfig(PhotoProcessContext context, File qrCodeFile, Double scale) {
|
||||
ScenicConfigManager scenicConfig = context.getScenicConfigManager();
|
||||
if (scenicConfig == null) {
|
||||
log.warn("scenicConfigManager未设置,返回空水印配置");
|
||||
@@ -888,6 +977,7 @@ public class PrinterServiceImpl implements PrinterService {
|
||||
.scenicText(scenicText)
|
||||
.dateFormat(dateFormat)
|
||||
.qrcodeFile(qrCodeFile)
|
||||
.scale(scale)
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -937,53 +1027,56 @@ public class PrinterServiceImpl implements PrinterService {
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
userPhotoListByOrderId.forEach(item -> {
|
||||
PrinterEntity printer = printerMapper.getById(item.getPrinterId());
|
||||
Thread.ofVirtual().start(() -> {
|
||||
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();
|
||||
if (quantity == null || quantity <= 0) {
|
||||
quantity = 1; // 默认至少打印1张
|
||||
}
|
||||
// 根据数量创建多个打印任务
|
||||
Integer quantity = item.getQuantity();
|
||||
if (quantity == null || quantity <= 0) {
|
||||
quantity = 1; // 默认至少打印1张
|
||||
}
|
||||
|
||||
for (int i = 0; i < quantity; i++) {
|
||||
// 获取打印机名称(支持轮询)
|
||||
String selectedPrinter = getNextPrinter(printer);
|
||||
for (int i = 0; i < quantity; i++) {
|
||||
// 获取打印机名称(支持轮询)
|
||||
String selectedPrinter = getNextPrinter(printer);
|
||||
|
||||
// 根据景区配置决定任务初始状态
|
||||
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(order.getScenicId());
|
||||
Boolean purchaseNeedReview = scenicConfig.getBoolean("printer_manual_approve");
|
||||
int initialStatus = (purchaseNeedReview != null && purchaseNeedReview)
|
||||
? TASK_STATUS_PENDING_REVIEW
|
||||
: TASK_STATUS_PENDING;
|
||||
// 根据景区配置决定任务初始状态
|
||||
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(order.getScenicId());
|
||||
Boolean purchaseNeedReview = scenicConfig.getBoolean("printer_manual_approve");
|
||||
int initialStatus = (purchaseNeedReview != null && purchaseNeedReview)
|
||||
? TASK_STATUS_PENDING_REVIEW
|
||||
: TASK_STATUS_PENDING;
|
||||
|
||||
PrintTaskEntity task = new PrintTaskEntity();
|
||||
task.setPrinterId(printer.getId());
|
||||
task.setPrinterName(selectedPrinter);
|
||||
task.setMpId(item.getId());
|
||||
task.setPaper(printer.getPreferPaper());
|
||||
task.setStatus(initialStatus);
|
||||
task.setUrl(printUrl);
|
||||
task.setHeight(printer.getPreferH());
|
||||
task.setWidth(printer.getPreferW());
|
||||
task.setCreateTime(new Date());
|
||||
task.setUpdateTime(new Date());
|
||||
printTaskMapper.insertTask(task);
|
||||
PrintTaskEntity task = new PrintTaskEntity();
|
||||
task.setPrinterId(printer.getId());
|
||||
task.setPrinterName(selectedPrinter);
|
||||
task.setMpId(item.getId());
|
||||
task.setPaper(printer.getPreferPaper());
|
||||
task.setStatus(initialStatus);
|
||||
task.setUrl(printUrl);
|
||||
task.setHeight(printer.getPreferH());
|
||||
task.setWidth(printer.getPreferW());
|
||||
task.setCreateTime(new Date());
|
||||
task.setUpdateTime(new Date());
|
||||
printTaskMapper.insertTask(task);
|
||||
|
||||
// ========== WebSocket 推送任务 ==========
|
||||
// 只推送立即可处理的任务(status=0),待审核任务(status=4)等审核通过后再推送
|
||||
if (initialStatus == TASK_STATUS_PENDING) {
|
||||
try {
|
||||
taskPushService.pushTaskToPrinter(printer.getId(), task.getId());
|
||||
} catch (Exception e) {
|
||||
log.error("推送任务失败: printerId={}, taskId={}", printer.getId(), task.getId(), e);
|
||||
// 推送失败不影响任务创建,任务会通过 HTTP 轮询获取
|
||||
// ========== WebSocket 推送任务 ==========
|
||||
// 只推送立即可处理的任务(status=0),待审核任务(status=4)等审核通过后再推送
|
||||
if (initialStatus == TASK_STATUS_PENDING) {
|
||||
try {
|
||||
taskPushService.pushTaskToPrinter(printer.getId(), task.getId());
|
||||
} catch (Exception e) {
|
||||
log.error("推送任务失败: printerId={}, taskId={}", printer.getId(), task.getId(), e);
|
||||
// 推送失败不影响任务创建,任务会通过 HTTP 轮询获取
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
redisTemplate.delete("order_content_not_downloadable_" + orderId);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1418,58 +1511,15 @@ public class PrinterServiceImpl implements PrinterService {
|
||||
needEnhance = false; // 默认不增强
|
||||
}
|
||||
|
||||
// 3.1 创建图片处理上下文
|
||||
PrinterOrderItem orderItem = PrinterOrderItem.fromMemberPrintResp(memberPrint);
|
||||
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;
|
||||
|
||||
// 3.1 使用管线处理照片(复用 processPhotoWithPipeline)
|
||||
String newPrintUrl;
|
||||
try {
|
||||
if (context.getImageType() == ImageType.NORMAL_PHOTO) {
|
||||
// 准备水印配置(重打印需要二维码)
|
||||
WatermarkConfig watermarkConfig = prepareWatermarkConfig(context, qrCodeFile);
|
||||
prepareStorageAdapter(context);
|
||||
|
||||
// 创建管线,条件性添加增强 Stage
|
||||
pipeline = createNormalPhotoPipeline(watermarkConfig);
|
||||
} else {
|
||||
// 拼图
|
||||
prepareStorageAdapter(context);
|
||||
pipeline = createPuzzlePipeline();
|
||||
}
|
||||
|
||||
// 3.5 执行管线
|
||||
boolean success = pipeline.execute(context);
|
||||
if (success && context.getResultUrl() != null) {
|
||||
newPrintUrl = context.getResultUrl();
|
||||
log.info("handleReprint: 照片重新处理成功, taskId={}, mpId={}, enhance={}, newUrl={}",
|
||||
id, mpId, needEnhance, newPrintUrl);
|
||||
} else {
|
||||
log.warn("handleReprint: 照片重新处理失败, taskId={}, 使用原图", id);
|
||||
newPrintUrl = memberPrint.getCropUrl(); // 使用原裁剪图
|
||||
}
|
||||
newPrintUrl = processPhotoWithPipeline(memberPrint, memberPrint.getScenicId(), qrCodeFile, needEnhance);
|
||||
log.info("handleReprint: 照片重新处理成功, taskId={}, mpId={}, enhance={}, newUrl={}",
|
||||
id, mpId, needEnhance, newPrintUrl);
|
||||
} catch (Exception e) {
|
||||
log.error("handleReprint: 照片重新处理异常, taskId={}, 使用原图", id, e);
|
||||
newPrintUrl = memberPrint.getCropUrl();
|
||||
} finally {
|
||||
context.cleanup();
|
||||
}
|
||||
|
||||
// 4. 更新打印任务
|
||||
|
||||
@@ -14,9 +14,7 @@ public interface TaskService {
|
||||
|
||||
TemplateRespVO workerGetTemplate(Long templateId, WorkerAuthReqVo req);
|
||||
|
||||
void createTaskByFaceIdAndTemplateId(Long faceId, Long templateId);
|
||||
|
||||
void createTaskByFaceIdAndTemplateId(Long faceId, Long templateId, int automatic);
|
||||
void createTaskByFaceIdAndTemplateId(Long faceId, Long templateId, boolean automatic);
|
||||
|
||||
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.model.mobile.order.IsBuyRespVO;
|
||||
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.member.resp.MemberRespVO;
|
||||
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 java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
@@ -256,7 +254,7 @@ public class TaskTaskServiceImpl implements TaskService {
|
||||
|
||||
@Override
|
||||
public void forceCreateTaskByFaceIdAndTempalteId(Long faceId, Long templateId) {
|
||||
createTaskByFaceIdAndTemplateIdInternal(faceId, templateId, 0, true);
|
||||
createTaskByFaceIdAndTemplateIdInternal(faceId, templateId, false, true);
|
||||
}
|
||||
|
||||
@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"))) {
|
||||
// 生成全部视频的逻辑
|
||||
templateList.forEach(template -> createTaskByFaceIdAndTemplateId(faceId, template.getId(), 1));
|
||||
templateList.forEach(template -> createTaskByFaceIdAndTemplateId(faceId, template.getId(), true));
|
||||
} else {
|
||||
if (Boolean.TRUE.equals(scenicConfig.getBoolean("force_create_vlog"))) {
|
||||
Long availableTemplateId = templateBiz.findFirstAvailableTemplate(templateList.stream().map(TemplateRespVO::getId).toList(), faceId, false);
|
||||
if (availableTemplateId != null) {
|
||||
createTaskByFaceIdAndTemplateId(faceId, availableTemplateId, 1);
|
||||
createTaskByFaceIdAndTemplateId(faceId, availableTemplateId, true);
|
||||
} else {
|
||||
log.info("faceId:{} available template is not exist", faceId);
|
||||
}
|
||||
} else {
|
||||
// 非强制创建,只创建第一个可用模板
|
||||
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
|
||||
public void createTaskByFaceIdAndTemplateId(Long faceId, Long templateId, int automatic) {
|
||||
public void createTaskByFaceIdAndTemplateId(Long faceId, Long templateId, boolean automatic) {
|
||||
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);
|
||||
if (face == null) {
|
||||
log.info("faceId:{} is not exist", faceId);
|
||||
@@ -430,7 +424,7 @@ public class TaskTaskServiceImpl implements TaskService {
|
||||
taskEntity.setScenicId(face.getScenicId());
|
||||
taskEntity.setFaceId(faceId);
|
||||
taskEntity.setTemplateId(templateId);
|
||||
taskEntity.setAutomatic(automatic);
|
||||
taskEntity.setAutomatic(automatic ? 1 : 0);
|
||||
}
|
||||
taskEntity.setWorkerId(null);
|
||||
taskEntity.setStatus(0);
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
server:
|
||||
port: 8030
|
||||
shutdown: graceful
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: zt
|
||||
lifecycle:
|
||||
timeout-per-shutdown-phase: 60s
|
||||
|
||||
# Feign配置(简化版,基于Nacos服务发现)
|
||||
feign:
|
||||
@@ -38,4 +41,7 @@ kafka:
|
||||
# 开发环境日志配置
|
||||
logging:
|
||||
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:
|
||||
port: 8031
|
||||
shutdown: graceful
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: zt
|
||||
lifecycle:
|
||||
timeout-per-shutdown-phase: 60s
|
||||
|
||||
# 生产环境日志级别
|
||||
logging:
|
||||
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>
|
||||
</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">
|
||||
<appender-ref ref="error_log" />
|
||||
<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">
|
||||
<mapper namespace="com.ycwl.basic.mapper.FaceDetectLogAiCamMapper">
|
||||
<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)
|
||||
values (#{scenicId}, #{deviceId}, #{faceSampleId}, #{dbName}, #{faceUrl}, #{score}, #{matchRawResult}, #{createTime})
|
||||
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}, #{faceId}, #{dbName}, #{faceUrl}, #{score}, #{matchRawResult}, #{createTime})
|
||||
</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>
|
||||
|
||||
@@ -95,6 +95,13 @@
|
||||
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
|
||||
),
|
||||
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 (
|
||||
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
|
||||
),
|
||||
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
|
||||
left join puzzle_template pt on gr.template_id = pt.id
|
||||
)
|
||||
SELECT
|
||||
oi.id AS oiId,
|
||||
@@ -127,17 +135,20 @@
|
||||
WHEN '3' THEN '照片打印'
|
||||
WHEN '4' THEN '一体机照片打印'
|
||||
WHEN '5' THEN 'pLog'
|
||||
WHEN '13' THEN '打卡点拍照'
|
||||
ELSE '其他'
|
||||
END AS goods_name,
|
||||
CASE oi.goods_type
|
||||
WHEN '0' THEN mvd.face_id
|
||||
WHEN '1' THEN oi.goods_id
|
||||
WHEN '2' THEN oi.goods_id
|
||||
WHEN '13' THEN oi.goods_id
|
||||
END AS face_id,
|
||||
CASE oi.goods_type
|
||||
WHEN '0' THEN mvd.face_url
|
||||
WHEN '1' THEN msd.face_url
|
||||
WHEN '2' THEN msd.face_url
|
||||
WHEN '13' THEN msac.face_url
|
||||
END AS face_url,
|
||||
CASE oi.goods_type
|
||||
WHEN '0' THEN mvd.video_url
|
||||
@@ -149,14 +160,16 @@
|
||||
WHEN '3' THEN mpd.url
|
||||
WHEN '4' THEN mpa.url
|
||||
WHEN '5' THEN mpl.url
|
||||
WHEN '13' THEN msac.url
|
||||
END AS imgUrl
|
||||
FROM order_item oi
|
||||
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_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_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};
|
||||
</select>
|
||||
|
||||
|
||||
@@ -116,6 +116,7 @@
|
||||
source_id,
|
||||
orig_url,
|
||||
crop_url,
|
||||
crop,
|
||||
quantity,
|
||||
status,
|
||||
create_time,
|
||||
@@ -127,6 +128,7 @@
|
||||
#{sourceId},
|
||||
#{origUrl},
|
||||
#{cropUrl},
|
||||
#{crop},
|
||||
1,
|
||||
0,
|
||||
NOW(),
|
||||
|
||||
@@ -198,7 +198,7 @@
|
||||
where so.id = #{id} and ms.member_id = #{userId} and so.id is not null
|
||||
</select>
|
||||
<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
|
||||
|
||||
where so.id = #{id}
|
||||
@@ -348,6 +348,12 @@
|
||||
left join source s on ms.source_id = s.id
|
||||
where ms.face_id = #{faceId} and ms.type = 2
|
||||
</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 *
|
||||
from source
|
||||
@@ -486,4 +492,19 @@
|
||||
</choose>
|
||||
LIMIT 1
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user