You've already forked FrameTour-BE
Compare commits
24 Commits
fe3bda28b4
...
3b11ddef6a
| Author | SHA1 | Date | |
|---|---|---|---|
| 3b11ddef6a | |||
| 6e7b4729a8 | |||
| 917cb37ccf | |||
| 7c0a3a63bb | |||
| 478467e124 | |||
| d5befd75e1 | |||
| b2c55c9feb | |||
| fef616c837 | |||
| a5fe00052d | |||
| 349b702fc3 | |||
| 9f5a61247b | |||
| 9321422e56 | |||
| 1834fe3ddd | |||
| fa8f92d38b | |||
| df33e7929f | |||
| 554f55a7c1 | |||
| f71149fd06 | |||
| e8eb8d816b | |||
| 576d87d113 | |||
| a2378053a8 | |||
| c92ea20575 | |||
| bb71cf9458 | |||
| 7749faf807 | |||
| c42b055d5f |
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;
|
||||
}
|
||||
@@ -220,7 +235,7 @@ public class OrderBiz {
|
||||
break;
|
||||
case 1: // 视频原素材
|
||||
case 2: // 照片原素材
|
||||
case 13: // AI微单
|
||||
case 13: // AI微单
|
||||
sourceRepository.setUserIsBuyItem(order.getMemberId(), item.getGoodsType(), item.getGoodsId(), order.getId());
|
||||
break;
|
||||
case 3:
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package com.ycwl.basic.controller.mobile;
|
||||
|
||||
import com.ycwl.basic.model.jwt.JwtInfo;
|
||||
import com.ycwl.basic.model.mobile.face.FaceRecognizeResp;
|
||||
import com.ycwl.basic.model.mobile.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.*;
|
||||
@@ -58,4 +61,21 @@ public class AppAiCamController {
|
||||
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;
|
||||
|
||||
/**
|
||||
* 移动端价格计算
|
||||
@@ -349,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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,15 +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);
|
||||
@@ -52,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,5 +1,6 @@
|
||||
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;
|
||||
@@ -23,4 +24,12 @@ public interface AppAiCamService {
|
||||
* @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);
|
||||
}
|
||||
@@ -1,21 +1,36 @@
|
||||
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.*;
|
||||
@@ -33,10 +48,14 @@ public class AppAiCamServiceImpl implements AppAiCamService {
|
||||
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) {
|
||||
@@ -272,4 +291,93 @@ public class AppAiCamServiceImpl implements AppAiCamService {
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -127,8 +127,6 @@ public class GoodsServiceImpl implements GoodsService {
|
||||
goodsNamePrefix = "录像";
|
||||
} else if (sourceType == 2) {
|
||||
goodsNamePrefix = "图片";
|
||||
} else if (sourceType == 3) {
|
||||
goodsNamePrefix = "AI微单";
|
||||
} else {
|
||||
goodsNamePrefix = "其他类型";
|
||||
}
|
||||
@@ -139,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 {
|
||||
|
||||
@@ -177,7 +177,7 @@ public class OrderServiceImpl implements OrderService {
|
||||
} else if (Integer.valueOf(2).equals(orderItemList.getFirst().getGoodsType())) {
|
||||
item.setGoodsName("照片集");
|
||||
item.setOrderType("照片集");
|
||||
} else if (Integer.valueOf(3).equals(orderItemList.getFirst().getGoodsType())) {
|
||||
} else if (Integer.valueOf(13).equals(orderItemList.getFirst().getGoodsType())) {
|
||||
item.setGoodsName("AI微单");
|
||||
item.setOrderType("AI微单");
|
||||
} else if (Integer.valueOf(0).equals(orderItemList.getFirst().getGoodsType())) {
|
||||
@@ -240,7 +240,7 @@ public class OrderServiceImpl implements OrderService {
|
||||
} else if (Integer.valueOf(2).equals(orderItemList.getFirst().getGoodsType())) {
|
||||
item.setGoodsName("照片集");
|
||||
item.setOrderType("照片集");
|
||||
} else if (Integer.valueOf(3).equals(orderItemList.getFirst().getGoodsType())) {
|
||||
} else if (Integer.valueOf(13).equals(orderItemList.getFirst().getGoodsType())) {
|
||||
item.setGoodsName("AI微单");
|
||||
item.setOrderType("AI微单");
|
||||
} else if (Integer.valueOf(0).equals(orderItemList.getFirst().getGoodsType())) {
|
||||
@@ -350,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()));
|
||||
@@ -552,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());
|
||||
@@ -688,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());
|
||||
@@ -1025,6 +1055,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);
|
||||
|
||||
@@ -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. 更新打印任务
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -127,17 +134,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,11 +159,13 @@
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user