From 6b62cf7299207109a1462779d0faf2ef8abdcd73 Mon Sep 17 00:00:00 2001 From: xupei Date: Thu, 20 Mar 2025 17:42:16 +0800 Subject: [PATCH 1/5] =?UTF-8?q?PoseTransformation-=E5=88=9D=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../da/common/RabbitMQ/GenerateConsumer.java | 59 +++++++- .../common/RabbitMQ/RabbitMQProperties.java | 1 + .../ai/da/common/constant/CommonConstant.java | 2 + .../ai/da/common/enums/CreditsEventsEnum.java | 1 + .../ai/da/controller/GenerateController.java | 27 +++- .../primary/PoseTransformationMapper.java | 7 + .../primary/SketchReconstructionMapper.java | 7 + .../primary/entity/PoseTransformation.java | 33 +++++ .../primary/entity/SketchReconstruction.java | 22 +++ .../com/ai/da/model/dto/ImageToSketchDTO.java | 9 ++ .../da/model/dto/SketchReconstructionDTO.java | 32 ++++ .../ai/da/model/vo/PoseTransformationVO.java | 25 ++++ .../java/com/ai/da/python/PythonService.java | 58 ++++++++ .../com/ai/da/service/GenerateService.java | 6 + .../da/service/impl/GenerateServiceImpl.java | 138 ++++++++++++++++++ src/main/resources/application-dev.properties | 1 + .../resources/application-prod.properties | 1 + 17 files changed, 425 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/ai/da/mapper/primary/PoseTransformationMapper.java create mode 100644 src/main/java/com/ai/da/mapper/primary/SketchReconstructionMapper.java create mode 100644 src/main/java/com/ai/da/mapper/primary/entity/PoseTransformation.java create mode 100644 src/main/java/com/ai/da/mapper/primary/entity/SketchReconstruction.java create mode 100644 src/main/java/com/ai/da/model/dto/SketchReconstructionDTO.java create mode 100644 src/main/java/com/ai/da/model/vo/PoseTransformationVO.java diff --git a/src/main/java/com/ai/da/common/RabbitMQ/GenerateConsumer.java b/src/main/java/com/ai/da/common/RabbitMQ/GenerateConsumer.java index b3aee7cd..23fb9496 100644 --- a/src/main/java/com/ai/da/common/RabbitMQ/GenerateConsumer.java +++ b/src/main/java/com/ai/da/common/RabbitMQ/GenerateConsumer.java @@ -1,24 +1,22 @@ package com.ai.da.common.RabbitMQ; -import com.ai.da.common.config.exception.BusinessException; import com.ai.da.common.constant.CommonConstant; import com.ai.da.common.utils.RedisUtil; import com.ai.da.model.dto.GenerateThroughImageTextDTO; import com.ai.da.model.vo.GenerateResultVO; +import com.ai.da.model.vo.PoseTransformationVO; import com.ai.da.service.GenerateService; import com.ai.da.service.UserLikeGroupService; import com.alibaba.fastjson.JSONObject; import com.google.gson.Gson; import com.rabbitmq.client.Channel; import lombok.extern.slf4j.Slf4j; -import org.apache.tomcat.jni.Time; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.annotation.RabbitHandler; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -import org.springframework.util.StringUtils; import javax.annotation.Resource; import java.io.IOException; @@ -258,6 +256,55 @@ public class GenerateConsumer { log.info("============ProcessRelightResult End listening=========="); } + public void processPoseTransformResult(Message msg, Channel channel) { + log.info("============ProcessPoseTransformResult listening=========="); + long start = System.currentTimeMillis(); + + Map generateResult = JSONObject.parseObject(msg.getBody(), Map.class); + log.info("PoseTransformation response : {}", generateResult); + + try { + log.info("tasks_id : {} start ", generateResult.get("tasks_id")); + if (generateResult.get("status").equals("SUCCESS")) { + String gifUrl = generateResult.get("gif_url"); + String taskId = generateResult.get("tasks_id"); + String videoUrl = generateResult.get("video_url"); + String imageUrl = generateResult.get("image_url"); + generateService.processPoseTransformResult(taskId, gifUrl, videoUrl, imageUrl); + } else { + // 修改redis中的数据状态为exception + String key = generateResultKey + ":" + generateResult.get("tasks_id"); + redisUtil.addToString(key, new Gson().toJson(new PoseTransformationVO(null, generateResult.get("tasks_id"),null, null, null, (byte)0, "Fail")), CommonConstant.GENERATE_RESULT_EXPIRE_TIME); + // 将异常信息存到exception中 + HashMap exceptionInfo = new HashMap<>(); + exceptionInfo.put(generateResult.get("tasks_id"), generateResult.get("message")); + // 存redis + redisUtil.addToMap(exceptionMapKey, exceptionInfo); + } + } catch (Exception e) { + log.error(e.getMessage()); + try { + channel.basicAck(msg.getMessageProperties().getDeliveryTag(), false); + // 将消息从redis排队队列中删除,需保证被消费的消息存储到db之后再从redis删除 + redisUtil.removeFromZSet(consumptionOrderKey, generateResult.get("tasks_id")); + } catch (IOException exception) { + log.error("手动确认,取消返回队列,不再重新消费"); + } + // 将入参和错误信息存入数据库 + String exceptionMessage = JSONObject.toJSONString(generateResult) + + " Exception message : " + e.getMessage(); + HashMap exceptionInfo = new HashMap<>(); + exceptionInfo.put(String.valueOf(generateResult.get("tasks_id")), exceptionMessage); + // 存redis + redisUtil.addToMap(exceptionMapKey, exceptionInfo); + } + + long end = System.currentTimeMillis(); + log.info("tasks_id : {}, end , message : {}, 执行时长: {} 毫秒", generateResult.get("tasks_id"), generateResult.get("message"), (end - start)); + log.info("============ProcessPoseTransformResult End listening=========="); + + } + @RabbitListener(queues = "#{rabbitMQProperties.queues.generate}") @RabbitHandler public void generateConsumer1(Message msg, Channel channel) { @@ -329,4 +376,10 @@ public class GenerateConsumer { public void getRelightResult(Message msg, Channel channel) { processRelightResult(msg, channel); } + + @RabbitListener(queues = "#{rabbitMQProperties.queues.poseTransform}") + @RabbitHandler + public void getPoseTransformationResult(Message msg, Channel channel) { + processPoseTransformResult(msg, channel); + } } diff --git a/src/main/java/com/ai/da/common/RabbitMQ/RabbitMQProperties.java b/src/main/java/com/ai/da/common/RabbitMQ/RabbitMQProperties.java index 29a2e1de..55990193 100644 --- a/src/main/java/com/ai/da/common/RabbitMQ/RabbitMQProperties.java +++ b/src/main/java/com/ai/da/common/RabbitMQ/RabbitMQProperties.java @@ -20,6 +20,7 @@ public class RabbitMQProperties { private String generateResult; private String toProductImageResult; private String relightResult; + private String poseTransform; } @Data diff --git a/src/main/java/com/ai/da/common/constant/CommonConstant.java b/src/main/java/com/ai/da/common/constant/CommonConstant.java index b5dabb24..8b790c52 100644 --- a/src/main/java/com/ai/da/common/constant/CommonConstant.java +++ b/src/main/java/com/ai/da/common/constant/CommonConstant.java @@ -30,6 +30,8 @@ public class CommonConstant { public static final String GENERATE_LOGO_SINGLE_CANCEL = "/api/generate_single_logo_cancel/"; + public static final String POSE_TRANSFORMATION_CANCEL = "/api/pose_transform_cancel/"; + public static final String PYTHON_PORT_9996 = "9996"; public static final String PYTHON_PORT_9997 = "9997"; diff --git a/src/main/java/com/ai/da/common/enums/CreditsEventsEnum.java b/src/main/java/com/ai/da/common/enums/CreditsEventsEnum.java index e8565984..2711d04b 100644 --- a/src/main/java/com/ai/da/common/enums/CreditsEventsEnum.java +++ b/src/main/java/com/ai/da/common/enums/CreditsEventsEnum.java @@ -35,6 +35,7 @@ public enum CreditsEventsEnum { RELIGHT("Relight","5"), QUESTIONNAIRE("Questionnaire","100"), IMAGE_TO_SKETCH("ImageToSketch","5"), + POSE_TRANSFORMATION("PoseTransformation","10"), OTHER("Other","5"); diff --git a/src/main/java/com/ai/da/controller/GenerateController.java b/src/main/java/com/ai/da/controller/GenerateController.java index 94a0da8a..f1e633ad 100644 --- a/src/main/java/com/ai/da/controller/GenerateController.java +++ b/src/main/java/com/ai/da/controller/GenerateController.java @@ -16,7 +16,6 @@ import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.validation.Valid; import java.util.List; - /** * @author XP */ @@ -98,4 +97,30 @@ public class GenerateController { return Response.success(generateService.modifySketch(generateModifyDTO)); } + @ApiOperation(value = "请求poseTransform,异步获取结果") + @PostMapping("/poseTransform") + public Response poseTransform(@ApiParam("projectId") @RequestParam Long projectId, + @ApiParam("productImage") @RequestParam String productImage, + @ApiParam("poseId") @RequestParam int poseId) { + return Response.success(generateService.poseTransform(projectId, productImage, poseId)); + } + + @ApiOperation(value = "获取pose transformation生成结果") + @PostMapping("/poseTransformResult") + public Response getPoseTransformationResults(@ApiParam("taskId") @RequestParam String taskId) { + PoseTransformationVO generateResult = generateService.getPoseTransformationResult(taskId); + return Response.success(generateResult); + } + + public Response modifyModelProportion(){ + return null; + } + + public Response sketchReconstruction(){ + return null; + } + + + + } diff --git a/src/main/java/com/ai/da/mapper/primary/PoseTransformationMapper.java b/src/main/java/com/ai/da/mapper/primary/PoseTransformationMapper.java new file mode 100644 index 00000000..4c6da51b --- /dev/null +++ b/src/main/java/com/ai/da/mapper/primary/PoseTransformationMapper.java @@ -0,0 +1,7 @@ +package com.ai.da.mapper.primary; + +import com.ai.da.common.config.mybatis.plus.CommonMapper; +import com.ai.da.mapper.primary.entity.PoseTransformation; + +public interface PoseTransformationMapper extends CommonMapper { +} diff --git a/src/main/java/com/ai/da/mapper/primary/SketchReconstructionMapper.java b/src/main/java/com/ai/da/mapper/primary/SketchReconstructionMapper.java new file mode 100644 index 00000000..712191b0 --- /dev/null +++ b/src/main/java/com/ai/da/mapper/primary/SketchReconstructionMapper.java @@ -0,0 +1,7 @@ +package com.ai.da.mapper.primary; + +import com.ai.da.mapper.primary.entity.SketchReconstruction; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +public interface SketchReconstructionMapper extends BaseMapper { +} diff --git a/src/main/java/com/ai/da/mapper/primary/entity/PoseTransformation.java b/src/main/java/com/ai/da/mapper/primary/entity/PoseTransformation.java new file mode 100644 index 00000000..55c95939 --- /dev/null +++ b/src/main/java/com/ai/da/mapper/primary/entity/PoseTransformation.java @@ -0,0 +1,33 @@ +package com.ai.da.mapper.primary.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +@TableName("t_pose_transformation") +@Data +public class PoseTransformation extends BaseEntity { + + private Long projectId; + + private Long accountId; + + private String uniqueId; + + private String productImage; + + private int poseId; + + private String gifUrl; + + private String videoUrl; + // GIF第一帧截图 + private String imageUrl; + + private byte isLiked; + + private byte isDeleted; + + +} diff --git a/src/main/java/com/ai/da/mapper/primary/entity/SketchReconstruction.java b/src/main/java/com/ai/da/mapper/primary/entity/SketchReconstruction.java new file mode 100644 index 00000000..24a46ea4 --- /dev/null +++ b/src/main/java/com/ai/da/mapper/primary/entity/SketchReconstruction.java @@ -0,0 +1,22 @@ +package com.ai.da.mapper.primary.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +@Data +@TableName("t_sketch_reconstruction") +public class SketchReconstruction extends BaseEntity{ + + private Long projectId; + + private Long elementId; + + // upload、library、generate + private String elementSource; + + private String path; + + +} diff --git a/src/main/java/com/ai/da/model/dto/ImageToSketchDTO.java b/src/main/java/com/ai/da/model/dto/ImageToSketchDTO.java index 983172c9..e356a7d1 100644 --- a/src/main/java/com/ai/da/model/dto/ImageToSketchDTO.java +++ b/src/main/java/com/ai/da/model/dto/ImageToSketchDTO.java @@ -19,4 +19,13 @@ public class ImageToSketchDTO { @ApiModelProperty("性别") private String gender; + + public ImageToSketchDTO() { + } + + public ImageToSketchDTO(Long elementId, String style, String gender) { + this.elementId = elementId; + this.style = style; + this.gender = gender; + } } diff --git a/src/main/java/com/ai/da/model/dto/SketchReconstructionDTO.java b/src/main/java/com/ai/da/model/dto/SketchReconstructionDTO.java new file mode 100644 index 00000000..ba665441 --- /dev/null +++ b/src/main/java/com/ai/da/model/dto/SketchReconstructionDTO.java @@ -0,0 +1,32 @@ +package com.ai.da.model.dto; + +import lombok.Data; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +@Data +public class SketchReconstructionDTO { + private Long projectId; + + private String collagePicture; + + private List elements; + + private MultipartFile file; + + // like到library时分类用 + private String gender; + + private boolean Save; + + + @Data + public static class Element{ + private Long elementId; + + private String elementSource; + + private String path; + } +} diff --git a/src/main/java/com/ai/da/model/vo/PoseTransformationVO.java b/src/main/java/com/ai/da/model/vo/PoseTransformationVO.java new file mode 100644 index 00000000..fb7e57b5 --- /dev/null +++ b/src/main/java/com/ai/da/model/vo/PoseTransformationVO.java @@ -0,0 +1,25 @@ +package com.ai.da.model.vo; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class PoseTransformationVO { + + private Long id; + + private String taskId; + + private String gifUrl; + + private String videoUrl; + // GIF第一帧截图 + private String imageUrl; + + private byte isLiked; + + private String status; +} diff --git a/src/main/java/com/ai/da/python/PythonService.java b/src/main/java/com/ai/da/python/PythonService.java index f67e7916..2ed86455 100644 --- a/src/main/java/com/ai/da/python/PythonService.java +++ b/src/main/java/com/ai/da/python/PythonService.java @@ -3799,4 +3799,62 @@ public class PythonService { throw new BusinessException("design.interface.exception"); } + public Boolean poseTransformation(String productImage, int poseId, String taskId) { + OkHttpClient client = new OkHttpClient().newBuilder() + .connectTimeout(30, TimeUnit.SECONDS) + .pingInterval(5, TimeUnit.SECONDS)//websocket轮训间隔(单位:秒) + .readTimeout(60, TimeUnit.SECONDS)//读取超时(单位:秒) + .writeTimeout(60, TimeUnit.SECONDS)//写入超时(单位:秒) + .build(); + MediaType mediaType = MediaType.parse("application/json"); + Map content = Maps.newHashMap(); + content.put("image_url", productImage); + content.put("tasks_id", taskId); + content.put("pose_id", String.valueOf(poseId)); + RequestBody body = RequestBody.create(mediaType, JSON.toJSONString(content)); + + log.info("poseTransformation 请求地址: {}", accessPythonIp + ":" + accessPythonPort + "/api/pose_transform"); + Request request = new Request.Builder() + .url(accessPythonIp + ":" + accessPythonPort + "/api/pose_transform") + .method("POST", body) + .addHeader("Content-Type", "application/json") + .build(); + Response response = null; + String bodyString; + try { + log.info("poseTransformation请求入参content###{}", JSON.toJSONString(content)); + response = client.newCall(request).execute(); + } catch (IOException ioException) { + log.error("PythonService##poseTransformation异常###{}", ExceptionUtil.getThrowableList(ioException)); + throw new BusinessException(ioException.getMessage()); + } + + // 判断是否生成失败 + if (Objects.isNull(response.body())) { + log.error("PythonService##poseTransformation异常###{}", "response or body is empty!"); + throw new BusinessException("PythonService##poseTransformation异常###: response or body is empty!"); + } else if (response.code() != HttpURLConnection.HTTP_OK) { + log.error("PythonService##poseTransformation异常###{}", "Response error!Response code ## " + response.code() + " ##"); + throw new BusinessException("PythonService##poseTransformation异常### Response error!Response code ## " + response.code() + " ##"); + } else { + try { + bodyString = response.body().string(); + } catch (IOException e) { + log.error(e.getMessage()); + throw new BusinessException(e.getMessage()); + } + } + JSONObject jsonObject = JSON.parseObject(bodyString); + Boolean result = JSON.parseObject(JSON.toJSONString(response)).getBoolean("successful"); + + if (result && jsonObject.get("code").equals(200)) { + log.info("poseTransformation##responseObject###{}", jsonObject); + return Boolean.TRUE; + } else { + log.info("poseTransformation失败###{}", jsonObject); + log.info("poseTransformation Exception! Code : {}", jsonObject.get("code")); + return Boolean.FALSE; + } + } + } diff --git a/src/main/java/com/ai/da/service/GenerateService.java b/src/main/java/com/ai/da/service/GenerateService.java index dda193e6..1872314c 100644 --- a/src/main/java/com/ai/da/service/GenerateService.java +++ b/src/main/java/com/ai/da/service/GenerateService.java @@ -47,4 +47,10 @@ public interface GenerateService extends IService { GenerateResultVO imageToSketch(ImageToSketchDTO imageToSketchDTO); GenerateResultVO modifySketch(GenerateModifyDTO generateModifyDTO); + + String poseTransform(Long projectId, String productImage, int poseId); + + void processPoseTransformResult(String taskId, String gifUrl, String videoUrl, String imageUrl); + + PoseTransformationVO getPoseTransformationResult(String taskId); } diff --git a/src/main/java/com/ai/da/service/impl/GenerateServiceImpl.java b/src/main/java/com/ai/da/service/impl/GenerateServiceImpl.java index cbe440a2..e15ceebf 100644 --- a/src/main/java/com/ai/da/service/impl/GenerateServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/GenerateServiceImpl.java @@ -59,6 +59,8 @@ public class GenerateServiceImpl extends ServiceImpl i private RedisUtil redisUtil; @Resource private GenerateCancelMapper generateCancelMapper; + @Resource + private SketchReconstructionMapper sketchReconstructionMapper; @Value("${redis.key.orderForGenerate}") private String consumptionOrderKey; @@ -715,6 +717,8 @@ public class GenerateServiceImpl extends ServiceImpl i String path; if (type.equals("Logo")) { path = CommonConstant.GENERATE_LOGO_SINGLE_CANCEL; + } else if(type.equals("PoseTransformation")){ + path =CommonConstant.POSE_TRANSFORMATION_CANCEL; } else { path = CommonConstant.GENERATE_CANCEL; } @@ -920,4 +924,138 @@ public class GenerateServiceImpl extends ServiceImpl i return new GenerateResultVO(generateDetailId, minioUtil.getPreSignedUrl(minioPath, CommonConstant.MINIO_IMAGE_EXPIRE_TIME, true), "Success", category); } + + public String poseTransform(Long projectId, String productImage, int poseId){ + Long accountId = UserContext.getUserHolder().getId(); + + // 1、判断用户当前积分是否够本次生成消耗 + CreditsEventsEnum creditsEventsEnum = CreditsEventsEnum.POSE_TRANSFORMATION; + Boolean preDeduction = creditsService.creditsPreDeduction(creditsEventsEnum, 1); + if (!preDeduction) { + throw new BusinessException("remaining.credits.insufficient", ResultEnum.WARNING.getCode()); + } + + // 3、生成唯一id 使用uuid,由于uuid重复的几率很小,故取消对uuid重复性的校验 + String uuid = UUID.randomUUID().toString(); + String taskId = uuid + "-" + accountId; + + PoseTransformation poseTransformation = new PoseTransformation(); + poseTransformation.setProjectId(projectId); + poseTransformation.setAccountId(accountId); + poseTransformation.setUniqueId(taskId); + poseTransformation.setProductImage(productImage); + poseTransformation.setPoseId(poseId); + poseTransformation.setCreateTime(LocalDateTime.now()); + poseTransformationMapper.insert(poseTransformation); + + Boolean b = pythonService.poseTransformation(productImage, poseId, taskId); + if (b){ + // 6、添加预扣除积分到redis + creditsService.addRecordToCreditsDeduction(accountId, uuid, creditsEventsEnum); + // 6.1 添加积分扣除记录到db + creditsService.preInsert(accountId, creditsEventsEnum.getName(), uuid, Boolean.TRUE, null); + return taskId; + } + throw new BusinessException("pose transformation error", ResultEnum.ERROR.getCode()); + } + + @Resource + private PoseTransformationMapper poseTransformationMapper; + + public void processPoseTransformResult(String taskId, String gifUrl, String videoUrl, String imageUrl){ + // 1、存储模型返回的数据 + PoseTransformation poseTransformation; + QueryWrapper qw = new QueryWrapper<>(); + qw.eq("unique_id", taskId); + List poseTransformations = poseTransformationMapper.selectList(qw); + if (poseTransformations != null && poseTransformations.size() > 1){ + log.warn("通过taskId {} 查询到的PoseTransformation的结果不止一条", taskId); + }else if (poseTransformations == null || poseTransformations.isEmpty()){ + return ; + } + poseTransformation = poseTransformations.get(0); + poseTransformation.setGifUrl(gifUrl); + poseTransformation.setVideoUrl(videoUrl); + poseTransformation.setImageUrl(imageUrl); + poseTransformation.setUpdateTime(LocalDateTime.now()); + poseTransformationMapper.updateById(poseTransformation); + + String key = generateResultKey + ":" + taskId; + PoseTransformationVO poseTransformationVO = new PoseTransformationVO( + poseTransformation.getId(), taskId, gifUrl, videoUrl, imageUrl, (byte) 0, "Success"); + + // 2、更新redis + redisUtil.addToString(key, new Gson().toJson(poseTransformationVO), CommonConstant.GENERATE_RESULT_EXPIRE_TIME); + + // 3、执行积分扣除 + String accountId = taskId.substring(taskId.lastIndexOf("-") + 1); + String uuid = taskId.substring(0, taskId.lastIndexOf("-")); + Boolean flag = creditsService.taskCreditsDeduction(Long.parseLong(accountId), uuid); + if (flag) creditsService.updateChangedCredits(accountId, uuid); + } + + public PoseTransformationVO getPoseTransformationResult(String taskId){ + String key = generateResultKey + ":" + taskId; + String resultJson = redisUtil.getFromString(key); + + if (!StringUtil.isNullOrEmpty(resultJson)){ + PoseTransformationVO poseTransformationVO = new Gson().fromJson(redisUtil.getFromString(key), PoseTransformationVO.class); + if (poseTransformationVO.getStatus().equals("Success")){ + poseTransformationVO.setGifUrl(minioUtil.getPreSignedUrl(poseTransformationVO.getGifUrl(), CommonConstant.MINIO_IMAGE_EXPIRE_TIME)); + poseTransformationVO.setVideoUrl(minioUtil.getPreSignedUrl(poseTransformationVO.getVideoUrl(), CommonConstant.MINIO_IMAGE_EXPIRE_TIME)); + poseTransformationVO.setImageUrl(minioUtil.getPreSignedUrl(poseTransformationVO.getImageUrl(), CommonConstant.MINIO_IMAGE_EXPIRE_TIME)); + } + return poseTransformationVO; + }else { + return new PoseTransformationVO(); + } + } + + /** + * String collagePicture(Base64) + * List elements + * File file + * @return + */ + public String sketchReconstruction(SketchReconstructionDTO sketchReconstructionDTO){ + Long accountId = UserContext.getUserHolder().getId(); + // 1、线稿生成 + String collagePictureBase64 = sketchReconstructionDTO.getCollagePicture(); + String path = accountId + "/CollagePicture/" + UUID.randomUUID(); + String minioPath = minioUtil.base64UploadToPath(collagePictureBase64, userBucket, path); + CollectionElement collectionElement = new CollectionElement(); + collectionElement.setAccountId(accountId); + collectionElement.setLevel1Type(SKETCH_BOARD.getRealName()); + collectionElement.setUrl(minioPath); + collectionElement.setMd5(MD5Utils.encryptFile(minioPath, false)); + collectionElement.setCreateDate(new Date()); + collectionElementService.save(collectionElement); + GenerateResultVO generateResultVO = imageToSketch(new ImageToSketchDTO(collectionElement.getId(), "2", sketchReconstructionDTO.getGender())); + // 2、以文件形式保存元素,同时还要将使用的元素单独存储 + + if (sketchReconstructionDTO.isSave() && !sketchReconstructionDTO.getElements().isEmpty()){ + // 将使用的元素全部都保存到新建表 + // 先判断该project下有没有数据,无 --> 直接保存;有 --> 先删除,再保存 + QueryWrapper qw = new QueryWrapper<>(); + qw.eq("project_id", sketchReconstructionDTO.getProjectId()); + List sketchReconstructions = sketchReconstructionMapper.selectList(qw); + if (!sketchReconstructions.isEmpty()){ + sketchReconstructionMapper.delete(qw); + } + sketchReconstructionDTO.getElements().forEach(element -> { + SketchReconstruction sketchReconstruction = new SketchReconstruction(); + sketchReconstruction.setProjectId(sketchReconstructionDTO.getProjectId()); + sketchReconstruction.setElementId(element.getElementId()); + sketchReconstruction.setElementSource(element.getElementSource()); + sketchReconstruction.setPath(element.getPath()); + sketchReconstructionMapper.insert(sketchReconstruction); + }); + + // 将画布文件上传到minio,地址保存到project表中 + String canvasPath = minioUtil.upload("aida-users", accountId + "/CollagePicture/CanvasFile", sketchReconstructionDTO.getFile()); + } + + // 需要返回哪些信息呢? + return null; + } } diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index 9fcc04d6..a45b62e2 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -105,6 +105,7 @@ rabbitmq.queues.srResult=SuperResolution-dev rabbitmq.queues.generateResult=GenerateImage-dev rabbitmq.queues.toProductImageResult=ToProductImage-dev rabbitmq.queues.relightResult=Relight-dev +rabbitmq.queues.poseTransform=PoseTransform-dev rabbitmq.exchange.generate=generate-exchange orderList.link=https://develop.aida.com.hk/home/homePage?order= diff --git a/src/main/resources/application-prod.properties b/src/main/resources/application-prod.properties index eacfd054..9c4a1e76 100644 --- a/src/main/resources/application-prod.properties +++ b/src/main/resources/application-prod.properties @@ -107,6 +107,7 @@ rabbitmq.queues.srResult=SuperResolution-prod rabbitmq.queues.generateResult=GenerateImage-prod rabbitmq.queues.toProductImageResult=ToProductImage-prod rabbitmq.queues.relightResult=Relight-prod +rabbitmq.queues.poseTransform=PoseTransform-prod rabbitmq.exchange.generate=generate-exchange orderList.link=https://aida.com.hk/home/homePage?order= From 078a0c0dfba9e50e47119f2165fa7e4185243ed2 Mon Sep 17 00:00:00 2001 From: xupei Date: Tue, 25 Mar 2025 11:19:55 +0800 Subject: [PATCH 2/5] =?UTF-8?q?=E6=A8=A1=E7=89=B9=E6=AF=94=E4=BE=8B?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=E3=80=81sketch=E6=8B=BC=E8=B4=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/ai/da/common/utils/FileUtil.java | 2 +- .../ai/da/controller/GenerateController.java | 44 ++-- .../ai/da/mapper/primary/entity/Generate.java | 9 + .../primary/entity/SketchReconstruction.java | 10 +- .../model/dto/ModifyModelProportionDTO.java | 68 ++++++ .../da/model/dto/SketchReconstructionDTO.java | 29 +-- .../da/model/vo/SketchReconstructionVO.java | 15 ++ .../java/com/ai/da/python/PythonService.java | 62 ++++++ .../com/ai/da/service/GenerateService.java | 16 +- .../da/service/impl/GenerateServiceImpl.java | 195 +++++++++++++++--- 10 files changed, 371 insertions(+), 79 deletions(-) create mode 100644 src/main/java/com/ai/da/model/dto/ModifyModelProportionDTO.java create mode 100644 src/main/java/com/ai/da/model/vo/SketchReconstructionVO.java diff --git a/src/main/java/com/ai/da/common/utils/FileUtil.java b/src/main/java/com/ai/da/common/utils/FileUtil.java index 43801af6..30f4018b 100644 --- a/src/main/java/com/ai/da/common/utils/FileUtil.java +++ b/src/main/java/com/ai/da/common/utils/FileUtil.java @@ -174,7 +174,7 @@ public class FileUtil extends cn.hutool.core.io.FileUtil { URL url = new URL(path); return url.openStream(); } catch (IOException ioException) { - log.error("获取文件尺寸异常###{}###path##{}", ExceptionUtil.stacktraceToString(ioException), path); + log.error("获取源文件异常###{}###path##{}", ExceptionUtil.stacktraceToString(ioException), path); throw new BusinessException("get.file.failed"); } } diff --git a/src/main/java/com/ai/da/controller/GenerateController.java b/src/main/java/com/ai/da/controller/GenerateController.java index f1e633ad..8dbd4478 100644 --- a/src/main/java/com/ai/da/controller/GenerateController.java +++ b/src/main/java/com/ai/da/controller/GenerateController.java @@ -1,10 +1,7 @@ package com.ai.da.controller; import com.ai.da.common.response.Response; -import com.ai.da.model.dto.GenerateLikeDTO; -import com.ai.da.model.dto.GenerateModifyDTO; -import com.ai.da.model.dto.GenerateThroughImageTextDTO; -import com.ai.da.model.dto.ImageToSketchDTO; +import com.ai.da.model.dto.*; import com.ai.da.model.vo.*; import com.ai.da.service.GenerateService; import io.swagger.annotations.Api; @@ -12,6 +9,7 @@ import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; import javax.validation.Valid; @@ -87,7 +85,7 @@ public class GenerateController { @ApiOperation(value = "imageToSketch") @PostMapping("/imageToSketch") public Response imageToSketch(@Valid @RequestBody ImageToSketchDTO imageToSketchDTO) { - return Response.success(generateService.imageToSketch(imageToSketchDTO)); + return Response.success(generateService.imageToSketch(imageToSketchDTO, null, null)); } // modifySketch @@ -97,27 +95,47 @@ public class GenerateController { return Response.success(generateService.modifySketch(generateModifyDTO)); } - @ApiOperation(value = "请求poseTransform,异步获取结果") - @PostMapping("/poseTransform") + @ApiOperation(value = "请求进行姿势变换") + @GetMapping("/poseTransform") public Response poseTransform(@ApiParam("projectId") @RequestParam Long projectId, @ApiParam("productImage") @RequestParam String productImage, @ApiParam("poseId") @RequestParam int poseId) { return Response.success(generateService.poseTransform(projectId, productImage, poseId)); } - @ApiOperation(value = "获取pose transformation生成结果") - @PostMapping("/poseTransformResult") + @ApiOperation(value = "获取姿势变换生成结果") + @GetMapping("/poseTransformResult") public Response getPoseTransformationResults(@ApiParam("taskId") @RequestParam String taskId) { PoseTransformationVO generateResult = generateService.getPoseTransformationResult(taskId); return Response.success(generateResult); } - public Response modifyModelProportion(){ - return null; + @ApiOperation(value = "修改模特比例") + @PostMapping("/modifyProportion") + public Response modifyModelProportion(@Valid @RequestBody ModifyModelProportionDTO proportionDTO){ + String path = generateService.modifyModelProportion(proportionDTO); + return Response.success(path); } - public Response sketchReconstruction(){ - return null; + @ApiOperation(value = "拼贴图生成线稿") + @PostMapping("/genSketchRecon") + public Response sketchReconstructionGenerate(@Valid @RequestBody SketchReconstructionDTO sketchReconstructionDTO){ + GenerateResultVO generateResultVO = generateService.sketchReconstructionGenerate(sketchReconstructionDTO); + return Response.success(generateResultVO); + } + + @ApiOperation(value = "拼贴图画布保存") + @GetMapping("/saveReconCanvas") + public Response sketchReconstructionSave(@RequestParam("file") MultipartFile file, @RequestParam("projectId") Long projectId){ + generateService.sketchReconstructionSave(file, projectId); + return Response.success("success"); + } + + @ApiOperation(value = "获取拼贴图画布") + @GetMapping("/getReconCanvas") + public Response getSketchReconstruction(@RequestParam("projectId") Long projectId){ + SketchReconstructionVO sketchReconstruction = generateService.getSketchReconstruction(projectId); + return Response.success(sketchReconstruction); } diff --git a/src/main/java/com/ai/da/mapper/primary/entity/Generate.java b/src/main/java/com/ai/da/mapper/primary/entity/Generate.java index 7a3157ab..ef5f6809 100644 --- a/src/main/java/com/ai/da/mapper/primary/entity/Generate.java +++ b/src/main/java/com/ai/da/mapper/primary/entity/Generate.java @@ -85,6 +85,15 @@ public class Generate { */ private Long styleImageElementId; + /** + * 由拼贴图生成线稿的项目id + */ + private Long projectId; + /** + * 输入模型的拼贴图 + */ + private String inputImageUrl; + /** * 创建时间 */ diff --git a/src/main/java/com/ai/da/mapper/primary/entity/SketchReconstruction.java b/src/main/java/com/ai/da/mapper/primary/entity/SketchReconstruction.java index 24a46ea4..3ba957a3 100644 --- a/src/main/java/com/ai/da/mapper/primary/entity/SketchReconstruction.java +++ b/src/main/java/com/ai/da/mapper/primary/entity/SketchReconstruction.java @@ -10,13 +10,11 @@ import lombok.EqualsAndHashCode; public class SketchReconstruction extends BaseEntity{ private Long projectId; + // 最后一次拼贴图生成的sketch + private String collageImgSketchUrl; - private Long elementId; - - // upload、library、generate - private String elementSource; - - private String path; + private Long generateDetailId; + private String canvasUrl; } diff --git a/src/main/java/com/ai/da/model/dto/ModifyModelProportionDTO.java b/src/main/java/com/ai/da/model/dto/ModifyModelProportionDTO.java new file mode 100644 index 00000000..499b9b51 --- /dev/null +++ b/src/main/java/com/ai/da/model/dto/ModifyModelProportionDTO.java @@ -0,0 +1,68 @@ +package com.ai.da.model.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +@Data +@ApiModel("ModifyModelProportionDTO") +public class ModifyModelProportionDTO { + @ApiModelProperty("模特id") + @NotNull(message = "model id cannot be empty") + private Long id; + + @ApiModelProperty("Library || System") + @NotBlank(message = "model type cannot be empty") + private String type; + + @ApiModelProperty("top") + @NotNull(message = "top cannot be empty") + private Integer top; + + @ApiModelProperty("bottom") + @NotNull(message = "bottom cannot be empty") + private Integer bottom; + + @ApiModelProperty("stretch") + @NotNull(message = "stretch cannot be empty") + private Float stretch; + + @ApiModelProperty("模特minio地址") + @NotBlank(message = "modelPath type cannot be empty") + private String modelPath; + + @NotNull(message = "handLeft cannot be null") + @NotEmpty(message = "handLeft cannot be empty") + @ApiModelProperty("handLeft") + private Float[] handLeft; + + @NotNull(message = "handRight cannot be null") + @NotEmpty(message = "handRight cannot be empty") + @ApiModelProperty("handRight") + private Float[] handRight; + + @NotNull(message = "shoulderLeft cannot be null") + @NotEmpty(message = "shoulderLeft cannot be empty") + @ApiModelProperty("shoulderLeft") + private Float[] shoulderLeft; + + @NotNull(message = "shoulderRight cannot be null") + @NotEmpty(message = "shoulderRight cannot be empty") + @ApiModelProperty("shoulderRight") + private Float[] shoulderRight; + + @NotNull(message = "waistbandLeft cannot be null") + @NotEmpty(message = "waistbandLeft cannot be empty") + @ApiModelProperty("waistbandLeft") + private Float[] waistbandLeft; + + @NotNull(message = "waistbandRight cannot be null") + @NotEmpty(message = "waistbandRight cannot be empty") + @ApiModelProperty("waistbandRight") + private Float[] waistbandRight; + +} diff --git a/src/main/java/com/ai/da/model/dto/SketchReconstructionDTO.java b/src/main/java/com/ai/da/model/dto/SketchReconstructionDTO.java index ba665441..e44fd76c 100644 --- a/src/main/java/com/ai/da/model/dto/SketchReconstructionDTO.java +++ b/src/main/java/com/ai/da/model/dto/SketchReconstructionDTO.java @@ -1,32 +1,19 @@ package com.ai.da.model.dto; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; -import org.springframework.web.multipart.MultipartFile; - -import java.util.List; @Data +@ApiModel(value = "sketch拼贴") public class SketchReconstructionDTO { + + @ApiModelProperty("项目id") private Long projectId; + @ApiModelProperty("拼贴图的base64数据") private String collagePicture; - - private List elements; - - private MultipartFile file; - - // like到library时分类用 + // 识别衣服类型用 + @ApiModelProperty("性别") private String gender; - - private boolean Save; - - - @Data - public static class Element{ - private Long elementId; - - private String elementSource; - - private String path; - } } diff --git a/src/main/java/com/ai/da/model/vo/SketchReconstructionVO.java b/src/main/java/com/ai/da/model/vo/SketchReconstructionVO.java new file mode 100644 index 00000000..047d2d43 --- /dev/null +++ b/src/main/java/com/ai/da/model/vo/SketchReconstructionVO.java @@ -0,0 +1,15 @@ +package com.ai.da.model.vo; + +import com.alibaba.fastjson.JSONObject; +import lombok.Data; + +@Data +public class SketchReconstructionVO { + + private JSONObject canvasFile; + + private String collageSketchUrl; + + private boolean isLiked; + +} diff --git a/src/main/java/com/ai/da/python/PythonService.java b/src/main/java/com/ai/da/python/PythonService.java index 2ed86455..84aca510 100644 --- a/src/main/java/com/ai/da/python/PythonService.java +++ b/src/main/java/com/ai/da/python/PythonService.java @@ -3857,4 +3857,66 @@ public class PythonService { } } + public String modifyModelProportion(String mannequinPath, Float scale, String name, int top, int bottom) { + OkHttpClient client = new OkHttpClient().newBuilder() + .connectTimeout(30, TimeUnit.SECONDS) + .pingInterval(5, TimeUnit.SECONDS)//websocket轮训间隔(单位:秒) + .readTimeout(60, TimeUnit.SECONDS)//读取超时(单位:秒) + .writeTimeout(60, TimeUnit.SECONDS)//写入超时(单位:秒) + .build(); + + MediaType mediaType = MediaType.parse("application/json"); + Map content = Maps.newHashMap(); + // 模特的minio地址 + content.put("mannequins", mannequinPath); + // 缩放比 + content.put("scale", scale.toString()); + // 结果存放桶名 + content.put("bucket_name", "aida-users"); + // 模特名uuid + content.put("mannequin_name", name); + content.put("top", String.valueOf(top)); + content.put("bottom", String.valueOf(bottom)); + RequestBody body = RequestBody.create(mediaType, JSON.toJSONString(content)); + + log.info("modifyModelProportion 请求地址: {},\n 参数:{}", accessPythonIp + ":" + accessPythonPort + "/api/mannequins_edit", JSON.toJSONString(content)); + Request request = new Request.Builder() + .url(accessPythonIp + ":" + accessPythonPort + "/api/mannequins_edit") + .method("POST", body) + .addHeader("Content-Type", "application/json") + .build(); + Response response = null; + try { + response = client.newCall(request).execute(); + } catch (IOException ioException) { + log.error("PythonService##modifyModelProportion异常###{}", ExceptionUtil.getThrowableList(ioException)); + throw new BusinessException("generate.interface.error"); + } + int responseCode = response.code(); + String bodyString; + try { + bodyString = response.body().string(); + if (responseCode != HttpURLConnection.HTTP_OK) { + // 基本不会有除200以外的code + log.info("modifyModelProportion 失败。 Response code {}", responseCode); + throw new BusinessException("modifyModelProportion 失败。 Response code " + responseCode); + } + JSONObject jsonObject = JSON.parseObject(bodyString); + if (response.isSuccessful() && jsonObject.get("msg").equals("OK!")) { + String modifiedModel = jsonObject.get("data").toString(); + log.info("modifyModelProportion 结果 : {}", modifiedModel); + return modifiedModel; + }else { + log.info("modifyModelProportion 失败。 Response code {}", responseCode); + throw new BusinessException("modifyModelProportion 失败。 Response code " + responseCode); + } + } catch (IOException e) { + log.error("modifyModelProportion 失败; error message => {}", e.getMessage()); + response.close(); + throw new BusinessException("generate.interface.error"); + } finally { + response.close(); + } + } + } diff --git a/src/main/java/com/ai/da/service/GenerateService.java b/src/main/java/com/ai/da/service/GenerateService.java index 1872314c..f86f70a0 100644 --- a/src/main/java/com/ai/da/service/GenerateService.java +++ b/src/main/java/com/ai/da/service/GenerateService.java @@ -2,12 +2,10 @@ package com.ai.da.service; import com.ai.da.mapper.primary.entity.Generate; import com.ai.da.mapper.primary.entity.GenerateDetail; -import com.ai.da.model.dto.GenerateLikeDTO; -import com.ai.da.model.dto.GenerateModifyDTO; -import com.ai.da.model.dto.GenerateThroughImageTextDTO; -import com.ai.da.model.dto.ImageToSketchDTO; +import com.ai.da.model.dto.*; import com.ai.da.model.vo.*; import com.baomidou.mybatisplus.extension.service.IService; +import org.springframework.web.multipart.MultipartFile; import java.util.List; import java.util.Map; @@ -44,7 +42,7 @@ public interface GenerateService extends IService { List> getCountByUserAndTime(String startTime, String endTime, List accountIdList); - GenerateResultVO imageToSketch(ImageToSketchDTO imageToSketchDTO); + GenerateResultVO imageToSketch(ImageToSketchDTO imageToSketchDTO, String collagePictureUrl, Long projectId); GenerateResultVO modifySketch(GenerateModifyDTO generateModifyDTO); @@ -53,4 +51,12 @@ public interface GenerateService extends IService { void processPoseTransformResult(String taskId, String gifUrl, String videoUrl, String imageUrl); PoseTransformationVO getPoseTransformationResult(String taskId); + + String modifyModelProportion(ModifyModelProportionDTO proportionDTO); + + GenerateResultVO sketchReconstructionGenerate(SketchReconstructionDTO sketchReconstructionDTO); + + String sketchReconstructionSave(MultipartFile multipartFile, Long projectId); + + SketchReconstructionVO getSketchReconstruction(Long projectId); } diff --git a/src/main/java/com/ai/da/service/impl/GenerateServiceImpl.java b/src/main/java/com/ai/da/service/impl/GenerateServiceImpl.java index e15ceebf..4750cd98 100644 --- a/src/main/java/com/ai/da/service/impl/GenerateServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/GenerateServiceImpl.java @@ -14,6 +14,8 @@ import com.ai.da.model.vo.*; import com.ai.da.python.PythonService; import com.ai.da.service.*; import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.serializer.SerializerFeature; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException; @@ -26,14 +28,17 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; import java.io.IOException; +import java.io.InputStream; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.*; import static com.ai.da.common.enums.CollectionLevel1TypeEnum.*; +import static com.ai.da.service.impl.UserLikeGroupServiceImpl.convert; @Slf4j @Service @@ -814,7 +819,7 @@ public class GenerateServiceImpl extends ServiceImpl i @Override @Transactional(rollbackFor = Exception.class) - public GenerateResultVO imageToSketch(ImageToSketchDTO imageToSketchDTO) { + public GenerateResultVO imageToSketch(ImageToSketchDTO imageToSketchDTO, String collagePictureUrl, Long projectId) { String bucket = userBucket; Long accountId = UserContext.getUserHolder().getId(); @@ -827,8 +832,13 @@ public class GenerateServiceImpl extends ServiceImpl i throw new BusinessException("remaining.credits.insufficient", ResultEnum.PROMPT.getCode()); } - CollectionElement collectionElement = collectionElementService.getById(imageToSketchDTO.getElementId()); - String imagePath = collectionElement.getUrl(); + String imagePath; + if (StringUtil.isNullOrEmpty(collagePictureUrl)){ + CollectionElement collectionElement = collectionElementService.getById(imageToSketchDTO.getElementId()); + imagePath = collectionElement.getUrl(); + }else { + imagePath = collagePictureUrl; + } log.info(minioUtil.getPreSignedUrl(imagePath, CommonConstant.MINIO_IMAGE_EXPIRE_TIME)); String imageName = UUID.randomUUID().toString(); @@ -856,7 +866,9 @@ public class GenerateServiceImpl extends ServiceImpl i generate.setElementId(imageToSketchDTO.getElementId()); generate.setGenerateType("image"); generate.setSketchStyle(styleCode); - generate.setStyleImageElementId(imageToSketchDTO.getElementId()); + generate.setStyleImageElementId(imageToSketchDTO.getStyleImageId()); + generate.setProjectId(projectId); + generate.setInputImageUrl(collagePictureUrl); generate.setCreateDate(new Date()); baseMapper.insert(generate); @@ -1011,51 +1023,168 @@ public class GenerateServiceImpl extends ServiceImpl i } } + @Resource + private SysFileService sysFileService; + + @Resource + private LibraryModelPointService libraryModelPointService; + public String modifyModelProportion(ModifyModelProportionDTO proportionDTO){ + log.info("modifyModelProportion params: {}", proportionDTO); + String name; + String gender; + Library model = null; + Long accountId = UserContext.getUserHolder().getId(); + String uuid = UUID.randomUUID().toString(); + // 所有修改的图片都另存为,不覆盖原图 + if (proportionDTO.getType().equals("Library")){ + model = libraryService.getById(proportionDTO.getId()); + String url = model.getUrl(); + name = url.substring(url.indexOf("/") + 1, url.lastIndexOf("/")) + "/" + uuid; + gender = model.getLevel2Type(); + }else { + SysFileVO sysModel = sysFileService.getById(proportionDTO.getId()); + gender = sysModel.getLevel2Type(); + name = accountId + "/models/" + gender.toLowerCase() + "/" + uuid; + } + // 只需要将结果存入library + String modifiedModel = pythonService.modifyModelProportion(proportionDTO.getModelPath(), proportionDTO.getStretch(), name, proportionDTO.getTop(), proportionDTO.getBottom()); + List imagesWidthAndHeight = minioUtil.getImagesWidthAndHeight(modifiedModel); + + // 存储修改后的模特到个人library + model = new Library(); + model.setAccountId(accountId); + model.setLevel1Type(LibraryLevel1TypeEnum.MODELS.getRealName()); + model.setLevel2Type(gender); + model.setName(uuid); + model.setUrl(modifiedModel); + model.setMd5(MD5Utils.encryptFile(minioUtil.getPreSignedUrl(modifiedModel, 24 * 60),false)); + model.setWidth(imagesWidthAndHeight.get(0)); + model.setHigh(imagesWidthAndHeight.get(1)); + model.setCreateDate(new Date()); + libraryService.save(model); + + // 新建模特点位信息 + LibraryModelPoint libraryModelPoint = new LibraryModelPoint(); + libraryModelPoint.setModelType("Library"); + libraryModelPoint.setRelationId(model.getId()); + libraryModelPoint.setShoulderLeft(Arrays.toString(proportionDTO.getShoulderLeft())); + libraryModelPoint.setShoulderRight(Arrays.toString(proportionDTO.getShoulderRight())); + libraryModelPoint.setWaistbandLeft(Arrays.toString(proportionDTO.getWaistbandLeft())); + libraryModelPoint.setWaistbandRight(Arrays.toString(proportionDTO.getWaistbandRight())); + libraryModelPoint.setHandLeft(Arrays.toString(proportionDTO.getHandLeft())); + libraryModelPoint.setHandRight(Arrays.toString(proportionDTO.getHandRight())); + libraryModelPoint.setCreateDate(new Date()); + libraryModelPointService.save(libraryModelPoint); + return minioUtil.getPreSignedUrl(modifiedModel, CommonConstant.MINIO_IMAGE_EXPIRE_TIME); + } + /** * String collagePicture(Base64) * List elements * File file * @return */ - public String sketchReconstruction(SketchReconstructionDTO sketchReconstructionDTO){ + @Transactional(rollbackFor = Exception.class) + public GenerateResultVO sketchReconstructionGenerate(SketchReconstructionDTO sketchReconstructionDTO){ Long accountId = UserContext.getUserHolder().getId(); // 1、线稿生成 String collagePictureBase64 = sketchReconstructionDTO.getCollagePicture(); String path = accountId + "/CollagePicture/" + UUID.randomUUID(); String minioPath = minioUtil.base64UploadToPath(collagePictureBase64, userBucket, path); - CollectionElement collectionElement = new CollectionElement(); - collectionElement.setAccountId(accountId); - collectionElement.setLevel1Type(SKETCH_BOARD.getRealName()); - collectionElement.setUrl(minioPath); - collectionElement.setMd5(MD5Utils.encryptFile(minioPath, false)); - collectionElement.setCreateDate(new Date()); - collectionElementService.save(collectionElement); - GenerateResultVO generateResultVO = imageToSketch(new ImageToSketchDTO(collectionElement.getId(), "2", sketchReconstructionDTO.getGender())); - // 2、以文件形式保存元素,同时还要将使用的元素单独存储 - if (sketchReconstructionDTO.isSave() && !sketchReconstructionDTO.getElements().isEmpty()){ - // 将使用的元素全部都保存到新建表 - // 先判断该project下有没有数据,无 --> 直接保存;有 --> 先删除,再保存 - QueryWrapper qw = new QueryWrapper<>(); - qw.eq("project_id", sketchReconstructionDTO.getProjectId()); - List sketchReconstructions = sketchReconstructionMapper.selectList(qw); - if (!sketchReconstructions.isEmpty()){ - sketchReconstructionMapper.delete(qw); - } - sketchReconstructionDTO.getElements().forEach(element -> { - SketchReconstruction sketchReconstruction = new SketchReconstruction(); - sketchReconstruction.setProjectId(sketchReconstructionDTO.getProjectId()); - sketchReconstruction.setElementId(element.getElementId()); - sketchReconstruction.setElementSource(element.getElementSource()); - sketchReconstruction.setPath(element.getPath()); - sketchReconstructionMapper.insert(sketchReconstruction); - }); + Long projectId = sketchReconstructionDTO.getProjectId(); - // 将画布文件上传到minio,地址保存到project表中 - String canvasPath = minioUtil.upload("aida-users", accountId + "/CollagePicture/CanvasFile", sketchReconstructionDTO.getFile()); + GenerateResultVO generateResultVO = imageToSketch(new ImageToSketchDTO(null, "2", sketchReconstructionDTO.getGender()), minioPath, projectId); + QueryWrapper qw = new QueryWrapper<>(); + qw.eq("project_id", projectId); + SketchReconstruction sketchReconstruction = sketchReconstructionMapper.selectOne(qw); + + String url = generateResultVO.getUrl(); + // 找到路径的起始位置(从"://"之后查找第一个"/") + int pathStartIndex = url.indexOf("/", url.indexOf("://") + 3); + // 找到查询参数的起始位置("?" 的位置) + int queryStartIndex = url.indexOf("?"); + // 截取目标部分 + String targetPath = url.substring(pathStartIndex + 1, queryStartIndex); + + if (Objects.isNull(sketchReconstruction)){ + sketchReconstruction = new SketchReconstruction(); + sketchReconstruction.setProjectId(projectId); + sketchReconstruction.setCollageImgSketchUrl(targetPath); + sketchReconstruction.setGenerateDetailId(generateResultVO.getId()); + sketchReconstruction.setCreateTime(LocalDateTime.now()); + sketchReconstructionMapper.insert(sketchReconstruction); + }else { + sketchReconstruction.setCollageImgSketchUrl(targetPath); + sketchReconstruction.setGenerateDetailId(generateResultVO.getId()); + sketchReconstructionMapper.updateById(sketchReconstruction); + } + + return generateResultVO; + } + + public String sketchReconstructionSave(MultipartFile multipartFile, Long projectId){ + Long accountId = UserContext.getUserHolder().getId(); + // 元素都在画布上,不用额外保存 + String object = accountId + "/CollageSketchFile/" + projectId; + String canvasFilePath = minioUtil.upload("aida-users", object, multipartFile,null); + + // 将画布文件上传到minio,地址保存到project表中 + QueryWrapper qw = new QueryWrapper<>(); + qw.eq("project_id", projectId).isNotNull("canvas_url").orderByDesc("id"); + SketchReconstruction sketchReconstruction = sketchReconstructionMapper.selectOne(qw); + if (Objects.isNull(sketchReconstruction)){ + sketchReconstruction = new SketchReconstruction(); + sketchReconstruction.setProjectId(projectId); + sketchReconstruction.setCanvasUrl(canvasFilePath); + sketchReconstruction.setCreateTime(LocalDateTime.now()); + sketchReconstructionMapper.insert(sketchReconstruction); + }else if (StringUtil.isNullOrEmpty(sketchReconstruction.getCanvasUrl())){ + sketchReconstruction.setCanvasUrl(canvasFilePath); + sketchReconstructionMapper.updateById(sketchReconstruction); } // 需要返回哪些信息呢? return null; } + + public SketchReconstructionVO getSketchReconstruction(Long projectId){ + QueryWrapper qw = new QueryWrapper<>(); + qw.eq("project_id", projectId); + SketchReconstruction sketchReconstruction = sketchReconstructionMapper.selectOne(qw); + if (Objects.isNull(sketchReconstruction) || StringUtil.isNullOrEmpty(sketchReconstruction.getCanvasUrl())){ + return null; + } + + try { + InputStream download = minioUtil.download(sketchReconstruction.getCanvasUrl()); + String convert = convert(download); + JSONObject jsonObject = JSONObject.parseObject(convert); + JSONArray objects = jsonObject.getJSONArray("objects"); + for (int i = 0; i < objects.size(); i++) { + JSONObject jsonObject1 = objects.getJSONObject(i); + String type = jsonObject1.getString("type"); + if (type.equals("image")) { + String minioUrl = jsonObject1.getString("minioUrl"); + jsonObject1.put("src", minioUtil.getPreSignedUrl(minioUrl, 24 * 60)); + } + objects.set(i, jsonObject1); + } + jsonObject.put("objects", objects); + log.info(String.valueOf(jsonObject)); + + // 除返回jsonObject之外,还要返回最后一次生成的线稿图以及like状态 + SketchReconstructionVO vo = new SketchReconstructionVO(); + vo.setCanvasFile(jsonObject); + if (Objects.nonNull(sketchReconstruction.getGenerateDetailId())){ + GenerateDetail generateDetail = generateDetailMapper.selectById(sketchReconstruction.getGenerateDetailId()); + vo.setCollageSketchUrl(minioUtil.getPreSignedUrl(generateDetail.getUrl(), CommonConstant.MINIO_IMAGE_EXPIRE_TIME)); + vo.setLiked(generateDetail.getIsLike().equals((byte)1)); + } + return vo; + }catch (Exception e){ + return null; + } + } + } From b18a5a00506c1b285cc18041b5bafac900b40f68 Mon Sep 17 00:00:00 2001 From: xupei Date: Tue, 25 Mar 2025 11:22:17 +0800 Subject: [PATCH 3/5] =?UTF-8?q?=E6=A8=A1=E7=89=B9=E6=AF=94=E4=BE=8B?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=E3=80=81sketch=E6=8B=BC=E8=B4=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/ai/da/common/utils/MailUtil.java | 168 +++++ .../com/ai/da/controller/EmailController.java | 43 ++ .../ai/da/mapper/primary/EmailLogMapper.java | 7 + .../mapper/primary/EmailTemplateMapper.java | 7 + .../ai/da/mapper/primary/entity/EmailLog.java | 35 + .../mapper/primary/entity/EmailTemplate.java | 24 + .../ai/da/model/dto/BasicEmailParamDTO.java | 29 + .../java/com/ai/da/service/EmailService.java | 138 ++++ .../ai/da/service/impl/EmailServiceImpl.java | 664 ++++++++++++++++++ 9 files changed, 1115 insertions(+) create mode 100644 src/main/java/com/ai/da/common/utils/MailUtil.java create mode 100644 src/main/java/com/ai/da/controller/EmailController.java create mode 100644 src/main/java/com/ai/da/mapper/primary/EmailLogMapper.java create mode 100644 src/main/java/com/ai/da/mapper/primary/EmailTemplateMapper.java create mode 100644 src/main/java/com/ai/da/mapper/primary/entity/EmailLog.java create mode 100644 src/main/java/com/ai/da/mapper/primary/entity/EmailTemplate.java create mode 100644 src/main/java/com/ai/da/model/dto/BasicEmailParamDTO.java create mode 100644 src/main/java/com/ai/da/service/EmailService.java create mode 100644 src/main/java/com/ai/da/service/impl/EmailServiceImpl.java diff --git a/src/main/java/com/ai/da/common/utils/MailUtil.java b/src/main/java/com/ai/da/common/utils/MailUtil.java new file mode 100644 index 00000000..cd5882ec --- /dev/null +++ b/src/main/java/com/ai/da/common/utils/MailUtil.java @@ -0,0 +1,168 @@ +package com.ai.da.common.utils; + +import com.ai.da.model.dto.BasicEmailParamDTO; +import com.alibaba.fastjson.JSONObject; +import com.sun.mail.smtp.SMTPTransport; +import io.netty.util.internal.StringUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.io.InputStreamSource; +import org.springframework.mail.MailException; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.JavaMailSenderImpl; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Component; +import org.thymeleaf.TemplateEngine; +import org.thymeleaf.context.Context; + +import javax.annotation.Resource; +import javax.mail.MessagingException; +import javax.mail.internet.*; +import java.util.List; +import java.util.Objects; + +@Slf4j +@Component +public class MailUtil { + + @Resource + private JavaMailSender javaMailSender; + + @Resource + private TemplateEngine templateEngine; + + /** + * 发送邮件 - 默认发件人 + * + * @param basicEmailParamDTO 发送邮件所需参数 + * @param inputStreamSource 附件(如果有) + */ + public int sendMail(BasicEmailParamDTO basicEmailParamDTO, String fileName, InputStreamSource inputStreamSource) throws MessagingException { + MimeMessage mimeMessage = createSimpleMail(basicEmailParamDTO, fileName, inputStreamSource); + // 提取配置 + String host; + String username; + String password; + if (StringUtil.isNullOrEmpty(basicEmailParamDTO.getServiceAddress())) { + host = ((JavaMailSenderImpl) javaMailSender).getHost(); + } else { + host = basicEmailParamDTO.getServiceAddress(); + } + if (StringUtil.isNullOrEmpty(basicEmailParamDTO.getSenderUser())) { + username = ((JavaMailSenderImpl) javaMailSender).getUsername(); + } else { + username = basicEmailParamDTO.getSenderUser(); + } + if (StringUtil.isNullOrEmpty(basicEmailParamDTO.getServiceAddress())) { + password = ((JavaMailSenderImpl) javaMailSender).getPassword(); + } else { + password = basicEmailParamDTO.getPassword(); + } + return sendMail(mimeMessage, host, username, password); + } + + private int sendMail(MimeMessage mimeMessage, String host, String username, String password) throws MessagingException { + SMTPTransport transport = null; + try { + // 获取 SMTPTransport + transport = (SMTPTransport) mimeMessage.getSession().getTransport("smtp"); + // 连接到 SMTP 服务器 + transport.connect(host, username, password); + // 发送邮件 + transport.sendMessage(mimeMessage, mimeMessage.getAllRecipients()); + // 获取 SMTP 服务器的响应 + String lastServerResponse = transport.getLastServerResponse(); + int lastReturnCode = transport.getLastReturnCode(); + + log.info("SMTP 状态码: {}, SMTP 服务器响应: {}", lastReturnCode, lastServerResponse); + return lastReturnCode; + } catch (MailException | MessagingException e) { + // 记录日志或执行其他补偿逻辑 + log.info("邮件发送失败:{}", e.getMessage()); + } finally { + // 关闭连接 + assert transport != null; + transport.close(); + } + return 0; + } + + /** + * 创建一封邮件 + * + * @param basicEmailParamDTO 创建邮件需要的参数 + * @param inputStreamSource 附件(如果有) + * @return 一封邮件 + */ + private MimeMessage createSimpleMail(BasicEmailParamDTO basicEmailParamDTO, String fileName, InputStreamSource inputStreamSource) throws MessagingException { + // 创建邮件对象 + MimeMessage message = javaMailSender.createMimeMessage(); + // 使用 MimeMessageHelper 简化邮件内容和附件的设置 + MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(message, true); + // 设置发件人 + mimeMessageHelper.setFrom(new InternetAddress(basicEmailParamDTO.getSenderUserMail())); + // 设置收件人 + mimeMessageHelper.setTo(basicEmailParamDTO.getMailTo()); + // 设置抄送人 + if (basicEmailParamDTO.getCc() != null && basicEmailParamDTO.getCc().length > 0) { + mimeMessageHelper.setCc(basicEmailParamDTO.getCc()); + } + // 设置暗送人 + if (basicEmailParamDTO.getBcc() != null && basicEmailParamDTO.getBcc().length > 0) { + mimeMessageHelper.setBcc(basicEmailParamDTO.getBcc()); + } + // 设置邮件主题 + mimeMessageHelper.setSubject(basicEmailParamDTO.getSubject()); + // 设置邮件内容(HTML 格式) + mimeMessageHelper.setText(basicEmailParamDTO.getContent(), true); + // 设置附件 + if (inputStreamSource != null) { + mimeMessageHelper.addAttachment(fileName, inputStreamSource); + } + return message; + } + + + /** + * 设置实体参数 + * + * @param mailTo 接收邮件的邮箱地址 + * @param jsonObject 模板中变量的值 + * @return 返回一个MailEntity + * @throws AddressException 邮箱地址值异常 + */ + public BasicEmailParamDTO setBasicEmailParams(List mailTo, JSONObject jsonObject, String templatePath, String title) throws AddressException { + BasicEmailParamDTO basicEmailParamDTO = new BasicEmailParamDTO(); + basicEmailParamDTO.setSenderUserMail("info@aida.com.hk"); + basicEmailParamDTO.setMailTo(getInternetAddressList(mailTo)); + basicEmailParamDTO.setSubject(title); + // todo 邮件模板不存在的报错与重试机制 + basicEmailParamDTO.setContent(setContent(jsonObject, templatePath)); + return basicEmailParamDTO; + } + + /** + * 将地址转换为InternetAddress类型 + * + * @param addressList 普通的地址字符串列表 + * @return InternetAddress类型的地址列表 + * @throws AddressException 地址异常 + */ + public InternetAddress[] getInternetAddressList(List addressList) throws AddressException { + InternetAddress[] toAddress = new InternetAddress[addressList.size()]; + for (String address : addressList) { + toAddress[addressList.indexOf(address)] = new InternetAddress(address); + } + return toAddress; + } + + public String setContent(JSONObject jsonObject, String templatePath) { + Context context = new Context(); + if (Objects.nonNull(jsonObject)) { + for (String key : jsonObject.keySet()) { + context.setVariable(key, jsonObject.get(key)); + } + } + return templateEngine.process(templatePath, context); + } + +} diff --git a/src/main/java/com/ai/da/controller/EmailController.java b/src/main/java/com/ai/da/controller/EmailController.java new file mode 100644 index 00000000..f7424937 --- /dev/null +++ b/src/main/java/com/ai/da/controller/EmailController.java @@ -0,0 +1,43 @@ +package com.ai.da.controller; + +import com.ai.da.model.dto.BasicEmailParamDTO; +import com.ai.da.service.EmailService; +import com.alibaba.fastjson.JSONObject; +import io.swagger.annotations.Api; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.thymeleaf.context.Context; + +import javax.annotation.Resource; +import java.util.Collections; + +@Api(tags = "邮件模块") +@Slf4j +@RestController +@RequestMapping("/api/email") +public class EmailController { + + @Resource + private EmailService emailService; + + @GetMapping("/loadSingleTemplate") + public void loadSingleEmailTemplate(){ + emailService.loadSingleEmailTemplate("templates\\upgrade\\122899_AiDA发版完成通知中文版.html"); + } + + @GetMapping("/loadTemplate") + public void loadTemplatesFromResources(){ + emailService.loadTemplatesFromResources("templates"); + } + + @GetMapping("/sendEmailTest") + public void sendEmailTest(){ + Context context = new Context(); + context.setVariable("username", "小白"); + JSONObject jsonObject = new JSONObject(); + jsonObject.put("username", "小白"); + emailService.sendEmail(Collections.singletonList("xupei3360@163.com"), jsonObject, "132124_affiliate_accepted_en.html", "测试邮件", null, null ); + } +} diff --git a/src/main/java/com/ai/da/mapper/primary/EmailLogMapper.java b/src/main/java/com/ai/da/mapper/primary/EmailLogMapper.java new file mode 100644 index 00000000..8bdeabc9 --- /dev/null +++ b/src/main/java/com/ai/da/mapper/primary/EmailLogMapper.java @@ -0,0 +1,7 @@ +package com.ai.da.mapper.primary; + +import com.ai.da.common.config.mybatis.plus.CommonMapper; +import com.ai.da.mapper.primary.entity.EmailLog; + +public interface EmailLogMapper extends CommonMapper { +} diff --git a/src/main/java/com/ai/da/mapper/primary/EmailTemplateMapper.java b/src/main/java/com/ai/da/mapper/primary/EmailTemplateMapper.java new file mode 100644 index 00000000..570cc302 --- /dev/null +++ b/src/main/java/com/ai/da/mapper/primary/EmailTemplateMapper.java @@ -0,0 +1,7 @@ +package com.ai.da.mapper.primary; + +import com.ai.da.common.config.mybatis.plus.CommonMapper; +import com.ai.da.mapper.primary.entity.EmailTemplate; + +public interface EmailTemplateMapper extends CommonMapper { +} diff --git a/src/main/java/com/ai/da/mapper/primary/entity/EmailLog.java b/src/main/java/com/ai/da/mapper/primary/entity/EmailLog.java new file mode 100644 index 00000000..4fb2e205 --- /dev/null +++ b/src/main/java/com/ai/da/mapper/primary/entity/EmailLog.java @@ -0,0 +1,35 @@ +package com.ai.da.mapper.primary.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +@Data +@TableName("t_email_log") +public class EmailLog extends BaseEntity { + + private Long templateId; + + private String parameter; + + // from是SQL关键字,直接使用会报错 + private String sender; + + private String recipients; + + private String cc = null; + + private String bcc = null; + + private String subject; + + /** + * failed 邮件发送失败(如网络问题、邮件服务器问题等)。 + * retrying 邮件发送失败后,正在重试发送。 + * delivered 邮件已成功投递到收件人的邮箱服务器。 + */ + private String status; + + private int retryCount = 0; +} diff --git a/src/main/java/com/ai/da/mapper/primary/entity/EmailTemplate.java b/src/main/java/com/ai/da/mapper/primary/entity/EmailTemplate.java new file mode 100644 index 00000000..e43ebf09 --- /dev/null +++ b/src/main/java/com/ai/da/mapper/primary/entity/EmailTemplate.java @@ -0,0 +1,24 @@ +package com.ai.da.mapper.primary.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +@Data +@TableName("t_email_template") +public class EmailTemplate extends BaseEntity { + + // 考虑添加唯一索引 + private String templateName; + + private String templatePath; + + private String content; + + private int version; + + private String language; + + private byte isDeleted = 0; +} diff --git a/src/main/java/com/ai/da/model/dto/BasicEmailParamDTO.java b/src/main/java/com/ai/da/model/dto/BasicEmailParamDTO.java new file mode 100644 index 00000000..e973f914 --- /dev/null +++ b/src/main/java/com/ai/da/model/dto/BasicEmailParamDTO.java @@ -0,0 +1,29 @@ +package com.ai.da.model.dto; + +import lombok.Data; + +import javax.mail.internet.InternetAddress; + +@Data +public class BasicEmailParamDTO { + /** 邮箱服务器 */ + private String serviceAddress; + /** 邮箱服务器端口 */ + private String servicePort; + /** 发件人邮箱地址 */ + private String senderUserMail; + /** 发件人账号 */ + private String senderUser; + /** 发件人密码 */ + private String password; + /** 邮件标题 */ + private String subject; + /** 邮件内容 */ + private String content; + /** 收件人邮箱地址 */ + private InternetAddress[] mailTo; + /** 抄送人 */ + private InternetAddress[] cc; + /** 暗抄送人 */ + private InternetAddress[] bcc; +} diff --git a/src/main/java/com/ai/da/service/EmailService.java b/src/main/java/com/ai/da/service/EmailService.java new file mode 100644 index 00000000..00373e8a --- /dev/null +++ b/src/main/java/com/ai/da/service/EmailService.java @@ -0,0 +1,138 @@ +package com.ai.da.service; + +import com.ai.da.mapper.primary.entity.Account; +import com.ai.da.mapper.primary.entity.TrialOrder; +import com.ai.da.model.dto.AffiliateEmailParamsDTO; +import com.ai.da.model.dto.BasicEmailParamDTO; +import com.ai.da.model.dto.SubscriptionEmailParamsDTO; +import com.alibaba.fastjson.JSONObject; +import org.springframework.core.io.InputStreamSource; + +import java.util.List; + +public interface EmailService { + + + void loadSingleEmailTemplate(String templatePath); + + void loadTemplatesFromResources(String resourcesPath); + + /** + * 发邮件 + * + * @param mailTo 收件人邮箱 + * @param jsonObject 动态邮件模板参数 + * @param templateName 邮件模板名(只有文件名且需要带文件后缀) + * @param title 邮件标题 + * @param fileName 附件文件名。没有附件置为null + * @param inputStreamSource 附件文件数据。没有附件置为null + */ + + void sendEmail(List mailTo, JSONObject jsonObject, String templateName, String title, String fileName, InputStreamSource inputStreamSource); + + /** + * 适用于 : 需要自定义发件人信息 + * + * @param jsonObject 模板参数 + * @param basicEmailParamDTO 包含发件人信息、邮件标题、收件人信息 + * @param templateName 使用的模板文件名 + * @param fileName 附件文件名 + * @param inputStreamSource 附件文件信息 + */ + + void sendEmail(JSONObject jsonObject, BasicEmailParamDTO basicEmailParamDTO, String templateName, String fileName, InputStreamSource inputStreamSource); + + // 登入模板id + String LOGIN_TEMPLATE_ID = "58020_login_aida_en.html"; + // 修改密码模板id + String UPDATE_PWD_TEMPLATE_ID = "58022_update_password.html"; + // 异常ip模板id + String EXCEPTION_ID_TEMPLATE_ID = "58021_exception_ip.html"; + // 绑定邮箱模板id + String BIND_MAILBOX_TEMPLATE_ID = "132754_绑定邮箱.html"; + // 更换绑定邮箱 + String CHANGE_MAILBOX_TEMPLATE_ID = "128210_change_mailbox_en.html"; + + /** + * 发送登录相关的邮件 + * + * @param receiverAddress 收件地址 + * @param ip 请求ip + * @param templateId 模板ID名 + * @param verifyCode 验证码 + */ + Boolean send(String receiverAddress, String ip, String templateId, String verifyCode); + + /** + * 发送试用订单相关的邮件 + * + * @param receiverAddress 收件人邮箱地址 + * @param trialOrder 试用订单相关参数 + * @param emailType 邮件类型:1 - 提交试用请求,2 - 审批通过,3 - 试用请求通过通知 + * @param country 通过城市判断邮件模板的语言 + * @param link ? + */ + void sendCustomEmail(String receiverAddress, TrialOrder trialOrder, int emailType, String country, Boolean link); + + /** + * 发送昨日的试用订单用户数据 + * + * @param receiverAddress 收件人地址 + * @param fileName 附件文件名 + * @param inputStreamSource 附件文件数据 + */ + void sendExcelEmail(List receiverAddress, String fileName, InputStreamSource inputStreamSource); + + /** + * 发送昨日的试用订单用户数据--无试用订单情况 + * + * @param receiverAddress 收件人地址 + */ + void sendNoExcelEmail(List receiverAddress); + + /** + * 向账号快要到期的用户发送提醒邮件 + * + * @param account 账号信息 + */ + void sendWillBeExpiredEmail(Account account); + + /** + * 发送系统升级通知邮件 + * + * @param account 用户信息 + * @param senderAddress 发件人邮件 + * @param type 邮件类型 + */ + void sendUpgradeNotification(Account account, String senderAddress, Integer type); + + /** + * 通知在Code_Create上付费的用户,AiDA账号的更新 + * + * @param receiverAddress 收件人地址 + * @param emailType 邮件类型 + * @param country 国家(确定发送邮件的语言) + * @param userName 用户名 + * @param date 账号到期时间 + */ + void notificationForPaidUser(String receiverAddress, int emailType, String country, String userName, String date); + + /** + * 广场用户注册通知邮件 + * + * @param userEmail + * @param randomVerifyCode + * @return + */ + Boolean designWorksRegister(String userEmail, String randomVerifyCode); + + void uploadTimeoutReminder(String userName, String time); + + boolean subscriptionEmailReminder(String type, SubscriptionEmailParamsDTO subscriptionEmailParamsDTO, String language, String receiverAddress); + + void affiliateEmailReminder(List receiverAddress, AffiliateEmailParamsDTO paramsDTO, String type); + + void creditsPurchaseReminder(String username, String quantity, String amount); + + void commonExceptionReminder(String functionName, List destination); +} diff --git a/src/main/java/com/ai/da/service/impl/EmailServiceImpl.java b/src/main/java/com/ai/da/service/impl/EmailServiceImpl.java new file mode 100644 index 00000000..fe0b90f8 --- /dev/null +++ b/src/main/java/com/ai/da/service/impl/EmailServiceImpl.java @@ -0,0 +1,664 @@ +package com.ai.da.service.impl; + +import com.ai.da.common.config.exception.BusinessException; +import com.ai.da.common.utils.DateUtil; +import com.ai.da.common.utils.MailUtil; +import com.ai.da.mapper.primary.EmailLogMapper; +import com.ai.da.mapper.primary.EmailTemplateMapper; +import com.ai.da.mapper.primary.entity.*; +import com.ai.da.model.dto.AffiliateEmailParamsDTO; +import com.ai.da.model.dto.BasicEmailParamDTO; +import com.ai.da.model.dto.SubscriptionEmailParamsDTO; +import com.ai.da.service.EmailService; +import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson2.JSON; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.tencentcloudapi.common.Credential; +import com.tencentcloudapi.common.exception.TencentCloudSDKException; +import com.tencentcloudapi.common.profile.ClientProfile; +import com.tencentcloudapi.common.profile.HttpProfile; +import com.tencentcloudapi.ses.v20201002.SesClient; +import com.tencentcloudapi.ses.v20201002.models.SendEmailRequest; +import com.tencentcloudapi.ses.v20201002.models.SendEmailResponse; +import com.tencentcloudapi.ses.v20201002.models.Template; +import jdk.nashorn.internal.ir.ObjectNode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.io.InputStreamSource; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import javax.mail.MessagingException; +import javax.mail.internet.AddressException; +import java.io.*; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Stream; + +@Slf4j +@Service +public class EmailServiceImpl implements EmailService { + + @Resource + private MailUtil mailUtil; + @Resource + private EmailTemplateMapper emailTemplateMapper; + @Resource + private EmailLogMapper emailLogMapper; + + public void loadSingleEmailTemplate(String templatePath){ + // 获取 ClassLoader + ClassLoader classLoader = this.getClass().getClassLoader(); + // 获取文件的 URL + URL resourceUrl = classLoader.getResource(templatePath); + + if (resourceUrl == null) { + System.out.println("File not found: " + templatePath); + return; + } + // 获取文件名 + String fileName = templatePath.substring(templatePath.lastIndexOf("\\") + 1); + // 获取文件内容 + try (InputStream inputStream = resourceUrl.openStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { + StringBuilder content = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + content.append(line).append("\n"); + } + // 调用方法将数据存入数据库 + saveTemplateToDatabase(fileName, removePrefixAndFileExtension(templatePath), String.valueOf(content)); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void loadTemplatesFromResources(String resourcesPath) { + try { + // 获取 ClassLoader + ClassLoader classLoader = this.getClass().getClassLoader(); + // 获取 resources 文件夹的路径 + Path path = Paths.get(classLoader.getResource(resourcesPath).toURI()); + + // 遍历文件夹 + try (Stream paths = Files.walk(path)) { + paths.filter(Files::isRegularFile) + .forEach(file -> { + try { + // 读取文件内容 + String content = new String(Files.readAllBytes(file), StandardCharsets.UTF_8); + // 获取文件名和路径 + String fileName = file.getFileName().toString(); + // 去除文件后缀 + String filePath = removePrefixAndFileExtension(file.toString()); + // 调用方法将数据存入数据库 + saveTemplateToDatabase(fileName, filePath, content); + } catch (IOException e) { + e.printStackTrace(); + } + }); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + + /** + * 去掉文件路径的前缀和后缀 + * + * @param filePath 文件路径(可以包含路径) + * @return 去掉后缀的文件名 + */ + private static String removePrefixAndFileExtension(String filePath) { + String keyword = "templates\\"; + int index = filePath.indexOf(keyword); + if (index == -1) { + return null; // 如果路径中不包含 templates/,返回空 + } + int lastDotIndex = filePath.lastIndexOf('.'); + if (lastDotIndex == -1) { + return filePath.substring(index + keyword.length()); // 没有后缀,直接返回 + } + return filePath.substring(index+ keyword.length(), lastDotIndex); + } + + public void saveTemplateToDatabase(String fileName, String filePath, String content) { + // 这里实现将数据存入数据库的逻辑 + // 使用 JDBC 或 JPA 将 fileName、filePath 和 content 插入到 email_template 表 + log.info("Saving to database: {}", fileName); + + EmailTemplate emailTemplate = new EmailTemplate(); + emailTemplate.setTemplateName(fileName); + emailTemplate.setTemplatePath(filePath); + emailTemplate.setContent(content); + emailTemplate.setVersion(1); + emailTemplate.setCreateTime(LocalDateTime.now()); + if (fileName.endsWith("en.html")){ + emailTemplate.setLanguage("EN"); + }else { + emailTemplate.setLanguage("CN"); + } + + emailTemplateMapper.insert(emailTemplate); + } + + /** + * 发邮件 + * @param mailTo 收件人邮箱 + * @param jsonObject 动态邮件模板【参数】 + * @param templateName 邮件模板名(只有文件名且需要带文件后缀) + * @param title 邮件标题 + * @param fileName 附件文件名 + * @param inputStreamSource 附件 + */ + public void sendEmail(List mailTo, JSONObject jsonObject, String templateName, String title, String fileName, InputStreamSource inputStreamSource) { + EmailTemplate emailTemplate = getEmailTemplateByName(templateName); + if (Objects.isNull(emailTemplate)){ + log.error("Email template: {}, dose not exist!", templateName); + return; + } + try { + BasicEmailParamDTO basicEmailParamDTO = mailUtil.setBasicEmailParams(mailTo, jsonObject, emailTemplate.getTemplatePath(), title); + int lastReturnCode = mailUtil.sendMail(basicEmailParamDTO, fileName, inputStreamSource); + + if (lastReturnCode == 250) { + log.info("邮件发送成功!Subject : {}", basicEmailParamDTO.getSubject()); + } else if (lastReturnCode == 450) { + log.info("目标邮箱 {} 暂时不可用,请稍后重试", mailTo); + } else if (lastReturnCode == 550) { + log.info("目标邮箱 {} 不可用,邮件发送失败", mailTo); + } else { + log.info("邮件发送失败,Subject : {}, 状态码: {}", basicEmailParamDTO.getSubject(), lastReturnCode); + } + + EmailLog emailLog = new EmailLog(); + emailLog.setTemplateId(emailTemplate.getId()); + if (Objects.nonNull(jsonObject)) emailLog.setParameter(jsonObject.toString()); + emailLog.setSender("info@aida.com.hk"); + emailLog.setRecipients(mailTo.toString()); + emailLog.setSubject(title); + emailLog.setCreateTime(LocalDateTime.now()); + switch (lastReturnCode) { + case 0: + break; + case 250: + emailLog.setStatus("delivered"); + break; + case 450: + emailLog.setStatus("retrying"); + break; + case 550: + emailLog.setStatus("failed"); + break; + } + emailLogMapper.insert(emailLog); + + } catch (MessagingException e) { + throw new RuntimeException(e); + } + } + + /** + * 适用于 : 需要自定义发件人信息 + * @param jsonObject 模板参数 + * @param basicEmailParamDTO 包含发件人信息、邮件标题、收件人信息 + * @param templateName 使用的模板文件名 + * @param fileName 附件文件名 + * @param inputStreamSource 附件文件信息 + */ + public void sendEmail(JSONObject jsonObject, BasicEmailParamDTO basicEmailParamDTO, String templateName, String fileName, InputStreamSource inputStreamSource) { + EmailTemplate emailTemplate = getEmailTemplateByName(templateName); + if (Objects.isNull(emailTemplate)){ + log.error("Email template: {}, dose not exist!", templateName); + return; + } + try { + basicEmailParamDTO.setContent(mailUtil.setContent(jsonObject, emailTemplate.getTemplatePath())); + int lastReturnCode = mailUtil.sendMail(basicEmailParamDTO, fileName, inputStreamSource); + EmailLog emailLog = new EmailLog(); + emailLog.setTemplateId(emailTemplate.getId()); + if (Objects.nonNull(jsonObject)) emailLog.setParameter(jsonObject.toString()); + emailLog.setSender(basicEmailParamDTO.getSenderUser()); + emailLog.setRecipients(basicEmailParamDTO.getMailTo().toString()); + emailLog.setSubject(basicEmailParamDTO.getSubject()); + emailLog.setCreateTime(LocalDateTime.now()); + switch (lastReturnCode) { + case 0: + break; + case 250: + emailLog.setStatus("delivered"); + break; + case 450: + emailLog.setStatus("retrying"); + break; + case 550: + emailLog.setStatus("failed"); + break; + } + emailLogMapper.insert(emailLog); + + } catch (MessagingException e) { + throw new RuntimeException(e); + } + } + + public EmailTemplate getEmailTemplateByName(String templateName) { + QueryWrapper qw = new QueryWrapper<>(); + qw.eq("template_name", templateName).orderByDesc("id"); + List emailTemplates = emailTemplateMapper.selectList(qw); + if (emailTemplates.isEmpty()) { + return null; + } + return emailTemplates.get(0); + } + + // 登入主题 + public final String LOGIN_SUBJECT = "Log on"; + // 忘记密码主题 + public final String FORGET_PWD_SUBJECT = "Reset password"; + // 异常ip + public final String EXCEPTION_ID_SUBJECT = "Exception ip"; + // 绑定邮箱 + public final String BIND_MAILBOX_SUBJECT = "绑定邮箱"; + // 更换邮箱 + public final String CHANGE_MAILBOX_SUBJECT = "Change Mailbox"; + + // 登入模板id +// public final String LOGIN_TEMPLATE_ID = "58020_login_aida_en.html"; + // 修改密码模板id +// public final String UPDATE_PWD_TEMPLATE_ID = "58022_update_password.html"; + // 异常ip模板id +// public final String EXCEPTION_ID_TEMPLATE_ID = "58021_exception_ip.html"; + // 绑定邮箱模板id +// public final String BIND_MAILBOX_TEMPLATE_ID = "132754_绑定邮箱.html"; + // 更换绑定邮箱 +// public final String CHANGE_MAILBOX_TEMPLATE_ID = "128210_change_mailbox_en.html"; + + public Boolean send(String receiverAddress, String ip, String templateId, String verifyCode) { + String subject = Objects.equals(templateId, LOGIN_TEMPLATE_ID) ? LOGIN_SUBJECT : + Objects.equals(templateId, UPDATE_PWD_TEMPLATE_ID) ? FORGET_PWD_SUBJECT : + Objects.equals(templateId, EXCEPTION_ID_TEMPLATE_ID) ? EXCEPTION_ID_SUBJECT : + Objects.equals(templateId, CHANGE_MAILBOX_TEMPLATE_ID) ? CHANGE_MAILBOX_SUBJECT : BIND_MAILBOX_SUBJECT; + + JSONObject jsonObject = contractTemplate(templateId, verifyCode, ip); + sendEmail(Collections.singletonList(receiverAddress), jsonObject, templateId, subject, null, null); + return Boolean.TRUE; + } + + private static JSONObject contractTemplate(String templateId, String verifyCode, String ip) { + JSONObject jsonObject = new JSONObject(); + if (templateId == EXCEPTION_ID_TEMPLATE_ID) { + jsonObject.put("exceptionIp", ip); + jsonObject.put("loginTime", DateUtil.dateToStr(new Date(), DateUtil.YYYY_MM_DD_HH_MM_SS)); + } else { + jsonObject.put("code", verifyCode); + } + return jsonObject; + } + + + private final static String YOUR_TRIAL_TEMPLATE_ID = "117214_trailOrderTemplate.html"; + private final static String APPROVAL_TEMPLATE_ID = "117215_trailOrderApprovalTemplate.html"; + private final static String NOTIFICATION_TEMPLATE_ID = "117216_notificationTemplate.html"; + private final static String NOTIFICATION_CHINESE_TEMPLATE_ID = "122229_notificationChineseTemplate.html"; + + /** + * 发送不同类型的邮件 + * + * @param receiverAddress 收件人邮箱地址 + * @param emailType 邮件类型:1 - 提交试用请求,2 - 审批通过,3 - 试用请求通过通知 + * @return 发送结果 + */ + // 由于原本的参数【String senderAddress】在实际使用过程中都是空,即使用默认发件地址,故这里删除该参数 + // todo 确认入参能否被删除 + public void sendCustomEmail(String receiverAddress, TrialOrder trialOrder, int emailType, String country, Boolean link) { + + // 根据邮件类型设置不同的主题和模板 + String subject = ""; + String templateId = ""; + JSONObject jsonObject = new JSONObject(); + switch (emailType) { + case 1: + subject = "试用订单请求"; + templateId = YOUR_TRIAL_TEMPLATE_ID; + jsonObject = buildTrialOrderData(trialOrder, null); + break; + case 2: + subject = "试用订单审批通过"; + templateId = APPROVAL_TEMPLATE_ID; + jsonObject = buildTrialOrderData(trialOrder, null); + break; + case 3: + subject = "Approval Confirmation for AiDA System Trial Access"; + if (country.equals("China")) { + templateId = NOTIFICATION_CHINESE_TEMPLATE_ID; + } else { + templateId = NOTIFICATION_TEMPLATE_ID; + } + jsonObject = buildTrialOrderData(trialOrder, link); + break; + default: + break; + } + // 发送邮件 + sendEmail(Collections.singletonList(receiverAddress), jsonObject, templateId, subject, null, null); + } + + // 构建试用订单数据 + private JSONObject buildTrialOrderData(TrialOrder trialOrder, Boolean link) { + JSONObject jsonObject = new JSONObject(); + // 设置试用订单通过通知相关数据 + jsonObject.put("title", trialOrder.getTitle()); + jsonObject.put("surname", trialOrder.getSurname()); + jsonObject.put("givenName", trialOrder.getGivenName()); + jsonObject.put("userName", trialOrder.getUserName()); + jsonObject.put("email", trialOrder.getEmail()); + if (Objects.nonNull(link)) { + if (link) { + jsonObject.put("days", 14); + } else { + jsonObject.put("days", 5); + } + } + return jsonObject; + } + + private final static String TRIAL_ORDER_LIST_ID = "122273_trailOrderData.html"; + public void sendExcelEmail(List receiverAddress, String fileName, InputStreamSource inputStreamSource) { + // 根据邮件类型设置不同的主题和模板 + String subject = "昨日试用订单数据"; + // 发送邮件 + sendEmail(receiverAddress, null, TRIAL_ORDER_LIST_ID, subject, fileName, inputStreamSource); + } + + private final static String NO_TRIAL_ORDER_LIST_ID = "122591_noTrailOrderTemplate.html"; + public void sendNoExcelEmail(List receiverAddress) { + // 根据邮件类型设置不同的主题和模板 + String subject = "昨日试用订单数据"; + // 发送邮件 + sendEmail(receiverAddress, null, NO_TRIAL_ORDER_LIST_ID, subject, null, null); + } + + private final static String WILLBEEXPIRED_TEMPLATE_ID = "118178_willBeExpiredNotification.html"; + public void sendWillBeExpiredEmail(Account account) { + // 根据邮件类型设置不同的主题和模板 + String subject = "Renewal notice"; + // 发送邮件 + sendEmail(Collections.singletonList(account.getUserEmail()), buildAccountData(account), WILLBEEXPIRED_TEMPLATE_ID, subject, null, null); + } + + private static JSONObject buildAccountData(Account account) { + JSONObject jsonObject = new JSONObject(); + // 设置试用订单相关数据 + jsonObject.put("userName", account.getUserName()); + + // 用户到期时间戳 + Long timestamp = account.getValidEndTime(); // 替换为你的时间戳 + if (null != timestamp) { + // 获取当前时间戳 + Long currentTimestamp = System.currentTimeMillis(); + // 计算时间差(毫秒) + long timeDifference = currentTimestamp - timestamp; + // 向上取整计算天数 + long days = (timeDifference + 24 * 60 * 60 * 1000 - 1) / (24 * 60 * 60 * 1000); + jsonObject.put("days", days); + } + return jsonObject; + } + + private final static String UPGRADE_SUCCESS_NOTIFICATION_ID = "118856_AiDA发版完成通知英文版.html"; + private final static String UPGRADE_SUCCESS_NOTIFICATION_ID_CHINESE = "122899_AiDA发版完成通知中文版.html"; + + // todo 将这里的发件人信息包装为枚举类,枚举类中包含发件邮箱的服务地址,密码,发件人邮箱 + public void sendUpgradeNotification(Account account, String senderAddress, Integer type) { + try { + // 根据邮件类型设置不同的主题和模板 + String subject = ""; + String templateId = ""; + + if (type == 1) { + subject = "Successful System Upgrade and New Features in AiDA 3.0"; + templateId = UPGRADE_SUCCESS_NOTIFICATION_ID; + }else { + subject = "系统升级成功和AiDA 3.0新功能"; + templateId = UPGRADE_SUCCESS_NOTIFICATION_ID_CHINESE; + } + + BasicEmailParamDTO basicEmailParamDTO = new BasicEmailParamDTO(); + basicEmailParamDTO.setServiceAddress("mail.code-create.com.hk."); + basicEmailParamDTO.setSenderUserMail("info@code-create.com.hk"); + basicEmailParamDTO.setSenderUser("info@code-create.com.hk"); + basicEmailParamDTO.setPassword("???"); + basicEmailParamDTO.setSubject(subject); + basicEmailParamDTO.setMailTo(mailUtil.getInternetAddressList(Collections.singletonList(account.getUserEmail()))); + + // 发送邮件 + sendEmail(buildAccountData(account), basicEmailParamDTO, templateId, null, null); + + } catch (AddressException e) { + throw new RuntimeException(e); + } + } + + private final static String NEW_USER_PAYMENT_NOTIFICATION_EN = "124889_new_user_payment_notification_en.html"; + private final static String NEW_USER_PAYMENT_NOTIFICATION_CN = "124888_new_user_payment_notification_cn.html"; + private final static String RENEWAL_NOTIFICATION_FOR_OLD_USER_EN = "124892_renewal_notification_for_old_user_en.html"; + private final static String RENEWAL_NOTIFICATION_FOR_OLD_USER_CN = "124891_renewal_notification_for_old_user_cn.html"; + + public void notificationForPaidUser(String receiverAddress, int emailType, String country, String userName, String date) { + // 根据邮件类型设置不同的主题和模板 + String subject = ""; + String templateId = ""; + JSONObject parameter = new JSONObject(); + switch (emailType) { + // 新用户 + case 1: + subject = "Welcome to AiDA!"; + if (country.equals("China")) { + templateId = NEW_USER_PAYMENT_NOTIFICATION_CN; + } else { + templateId = NEW_USER_PAYMENT_NOTIFICATION_EN; + } + parameter.put("userName", userName); + parameter.put("email", receiverAddress); + break; + // 续费用户 + case 2: + subject = "Account renewal notification"; + if (country.equals("China")) { + templateId = RENEWAL_NOTIFICATION_FOR_OLD_USER_CN; + } else { + templateId = RENEWAL_NOTIFICATION_FOR_OLD_USER_EN; + } + break; + default: + break; + } + parameter.put("userName", userName); + parameter.put("date", date); + // 发送邮件 + sendEmail(Collections.singletonList(receiverAddress), parameter, templateId, subject, null, null); + } + + private final static String PORTFOLIO_REGISTER_ID = "124847_portfolio-account-register.html"; + public Boolean designWorksRegister(String userEmail, String randomVerifyCode) { + String subject = "Tourist registration"; + sendEmail(Collections.singletonList(userEmail), contractTemplate(PORTFOLIO_REGISTER_ID, randomVerifyCode, null), PORTFOLIO_REGISTER_ID, subject, null, null); + return Boolean.TRUE; + } + + private final static String UPLOAD_TIMEOUT_REMINDER = "128324_upload_timeout_reminder.html"; + public void uploadTimeoutReminder(String userName, String time) { + String xp = "xupei3360@163.com"; + String shb = "shahaibodd99@gmail.com"; + String wxd = "X1627315083@163.com"; + String pkc = "kaicpang.pang@connect.polyu.hk"; + JSONObject param = new JSONObject(); + param.put("username", userName); + param.put("time", time); + + // 返回的resp是一个SendEmailResponse的实例,与请求对象对应 + sendEmail(Arrays.asList(shb, xp, wxd, pkc), param, UPLOAD_TIMEOUT_REMINDER, "上传图片超时提醒", null, null); + } + + private final static String CANCEL_MERCHANT_EN = "130720_cancel-merchant-en.html"; + private final static String NEW_MERCHANT_EN = "135190_new-merchant-en-updated01.html"; + private final static String NEW_USER_EN = "135189_new-user-en-updated01.html"; + private final static String NEW_USER_CN = "135186_new-user-cn-updated01.html"; + private final static String RENEWAL_MERCHANT_EN = "130724_renewal-merchant-en.html"; + private final static String RENEWAL_USER_EN = "130725_renewal-user-en.html"; + private final static String RENEWAL_USER_CN = "130726_renewal-user-cn.html"; + private final static String RENEWAL_REMINDER_USER_EN = "130727_renewal-reminder-user-en.html"; + private final static String RENEWAL_REMINDER_USER_CN = "130728_renewal-reminder-user-cn.html"; + private final static String PAYMENT_FAILED_NEW_MERCHANT_EN = "131230_payment_failed_new_merchant_en.html"; + private final static String PAYMENT_FAILED_RENEWAL_MERCHANT_EN = "131225_payment_failed_renewal_merchant_en.html"; + private final static String PAYMENT_FAILED_RENEWAL_USER_EN = "131563_payment_failed_renewal_user_en.html"; + private final static String PAYMENT_FAILED_RENEWAL_USER_CN = "131564_payment_failed_renewal_user_cn.html"; + + public boolean subscriptionEmailReminder(String type, SubscriptionEmailParamsDTO subscriptionEmailParamsDTO, String language, String receiverAddress) { + try { + String merchantEmail = "kimwong@code-create.com.hk"; + String developer = "xupei3360@163.com"; + List merchantReceiver = Arrays.asList(/*merchantEmail, */developer); + + String merchantSubject = null; + String merchantTemplate = null; + String userSubject = null; + String userTemplate = null; + switch (type) { + case "cancel": + merchantSubject = "[Code-Create] Subscription Cancelled"; + merchantTemplate = CANCEL_MERCHANT_EN; + break; + case "fail_new": + merchantSubject = "[Code-Create] Payment Failed : New Order (" + subscriptionEmailParamsDTO.getOrderId() + ")"; + merchantTemplate = PAYMENT_FAILED_NEW_MERCHANT_EN; + break; + case "fail_renewal": + merchantSubject = "[Code-Create] Payment Failed : Renewal Order (" + subscriptionEmailParamsDTO.getOrderId() + ")"; + merchantTemplate = PAYMENT_FAILED_RENEWAL_MERCHANT_EN; + if (language.equals("ENGLISH")) { + userSubject = "[Code-Create] Payment Failed : Renewal Order (" + subscriptionEmailParamsDTO.getOrderId() + ")"; + userTemplate = PAYMENT_FAILED_RENEWAL_USER_EN; + } else { + userSubject = "[Code-Create] 自动续费失败 (" + subscriptionEmailParamsDTO.getOrderId() + ")"; + userTemplate = PAYMENT_FAILED_RENEWAL_USER_CN; + } + break; + case "new": + merchantSubject = "[Code-Create] New Order(" + subscriptionEmailParamsDTO.getOrderId() + ")"; + merchantTemplate = NEW_MERCHANT_EN; + if (language.equals("ENGLISH")) { + userSubject = "[Code-Create] You have successfully subscribed to AiDA"; + userTemplate = NEW_USER_EN; + } else { + userSubject = "[Code-Create] 您已成功订阅AiDA"; + userTemplate = NEW_USER_CN; + } + break; + case "renewal": + merchantSubject = "[Code-Create] New subscription renewal order (" + subscriptionEmailParamsDTO.getOrderId() + ")"; + merchantTemplate = RENEWAL_MERCHANT_EN; + if (language.equals("ENGLISH")) { + userSubject = "[Code-Create] AiDA Renewal Successful"; + userTemplate = RENEWAL_USER_EN; + } else { + userSubject = "[Code-Create] AiDA续订成功"; + userTemplate = RENEWAL_USER_CN; + } + break; + case "reminder": + if (language.equals("ENGLISH")) { + userSubject = "[Code-Create] AiDA Subscription Renewal Reminder"; + userTemplate = RENEWAL_REMINDER_USER_EN; + } else { + userSubject = "[Code-Create] AiDA续订提醒"; + userTemplate = RENEWAL_REMINDER_USER_CN; + } + break; + default: + log.error("unknown subscription email type"); + return false; + } + JSONObject jsonObject = JSONObject.parseObject(JSONObject.toJSONString(subscriptionEmailParamsDTO)); + + // 排除不向用户发送邮件的情况 + if (!type.equals("cancel") && !type.equals("fail_new")) { + sendEmail(Collections.singletonList(receiverAddress), jsonObject, userTemplate, userSubject, null, null); + } + // 排除不向商家发送邮件的情况 + if (!type.equals("reminder")) { + sendEmail(merchantReceiver, jsonObject, merchantTemplate, merchantSubject, null, null); + } + return true; + } catch (Exception e) { + log.error("邮件发送失败,{}", e.getMessage()); + return false; + } + } + + private final static String NEW_REGISTRATION = "132123_affiliate_registration_en.html"; + private final static String AFFILIATE_ACCEPTED = "132124_affiliate_accepted_en.html"; + private final static String AFFILIATE_REFUSED = "132125_affiliate_refused_en.html"; + private final static String AFFILIATE_MONTHLY_SUMMARY = "132126_affiliate_monthly_summary_en.html"; + + public void affiliateEmailReminder(List receiverAddress, AffiliateEmailParamsDTO paramsDTO, String type) { + String subject = ""; + String templateId = ""; + switch (type) { + case "new": + subject = "New Affiliate Registration"; + templateId = NEW_REGISTRATION; + break; + case "accepted": + subject = "Affiliate Application Accepted"; + templateId = AFFILIATE_ACCEPTED; + break; + case "refused": + subject = "Affiliate Application Refused"; + templateId = AFFILIATE_REFUSED; + break; + case "summary": + subject = "Your Monthly AffiliateWP Summary for AiDA"; + templateId = AFFILIATE_MONTHLY_SUMMARY; + break; + } + + // 将 DTO 转换为 JSONObject + JSONObject jsonObject = (JSONObject) JSONObject.toJSON(paramsDTO); + sendEmail(receiverAddress, jsonObject, templateId, subject, null, null); + } + + private final static String CREDITS_PURCHASE_MERCHANT = "133275_AiDA 积分购买通知-merchant.html"; + public void creditsPurchaseReminder(String username, String quantity, String amount) { + String merchantEmail = "kimwong@code-create.com.hk"; + String developerEmail = "xupei@code-create.com.hk"; + JSONObject jsonObject = new JSONObject(); + // 设置试用订单相关数据 + jsonObject.put("username", username); + jsonObject.put("quantity", quantity); + jsonObject.put("totalFee", amount); + + sendEmail(Arrays.asList(/*merchantEmail,*/developerEmail), jsonObject, CREDITS_PURCHASE_MERCHANT, "New Credit Purchase Order", null, null); + } + + private final static String COMMON_EXCEPTION_REMINDER = "135279_common-exception-reminder.html"; + public void commonExceptionReminder(String functionName, List destination) { + // 邮件内容 {{function}}处理异常,请及时查看 + JSONObject param = new JSONObject(); + param.put("function", functionName); + + sendEmail(destination, param, COMMON_EXCEPTION_REMINDER, "AiDA发生异常,请及时处理", null, null); + } +} + + From 6b288a3b3c39c69910022aa6f4ecf2f3b6786394 Mon Sep 17 00:00:00 2001 From: xupei Date: Tue, 25 Mar 2025 11:37:53 +0800 Subject: [PATCH 4/5] =?UTF-8?q?=E9=82=AE=E4=BB=B6=E5=8F=91=E9=80=81?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F-=E5=88=9D=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/ai/da/controller/EmailController.java | 1 - .../com/ai/da/service/impl/EmailServiceImpl.java | 15 --------------- 2 files changed, 16 deletions(-) diff --git a/src/main/java/com/ai/da/controller/EmailController.java b/src/main/java/com/ai/da/controller/EmailController.java index f7424937..eebe05e0 100644 --- a/src/main/java/com/ai/da/controller/EmailController.java +++ b/src/main/java/com/ai/da/controller/EmailController.java @@ -1,6 +1,5 @@ package com.ai.da.controller; -import com.ai.da.model.dto.BasicEmailParamDTO; import com.ai.da.service.EmailService; import com.alibaba.fastjson.JSONObject; import io.swagger.annotations.Api; diff --git a/src/main/java/com/ai/da/service/impl/EmailServiceImpl.java b/src/main/java/com/ai/da/service/impl/EmailServiceImpl.java index fe0b90f8..d146f137 100644 --- a/src/main/java/com/ai/da/service/impl/EmailServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/EmailServiceImpl.java @@ -1,6 +1,5 @@ package com.ai.da.service.impl; -import com.ai.da.common.config.exception.BusinessException; import com.ai.da.common.utils.DateUtil; import com.ai.da.common.utils.MailUtil; import com.ai.da.mapper.primary.EmailLogMapper; @@ -11,21 +10,7 @@ import com.ai.da.model.dto.BasicEmailParamDTO; import com.ai.da.model.dto.SubscriptionEmailParamsDTO; import com.ai.da.service.EmailService; import com.alibaba.fastjson.JSONObject; -import com.alibaba.fastjson2.JSON; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.gson.Gson; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import com.tencentcloudapi.common.Credential; -import com.tencentcloudapi.common.exception.TencentCloudSDKException; -import com.tencentcloudapi.common.profile.ClientProfile; -import com.tencentcloudapi.common.profile.HttpProfile; -import com.tencentcloudapi.ses.v20201002.SesClient; -import com.tencentcloudapi.ses.v20201002.models.SendEmailRequest; -import com.tencentcloudapi.ses.v20201002.models.SendEmailResponse; -import com.tencentcloudapi.ses.v20201002.models.Template; -import jdk.nashorn.internal.ir.ObjectNode; import lombok.extern.slf4j.Slf4j; import org.springframework.core.io.InputStreamSource; import org.springframework.stereotype.Service; From a38c15005e048915eda6f0d5390d210e917eda8f Mon Sep 17 00:00:00 2001 From: xupei Date: Tue, 25 Mar 2025 11:45:10 +0800 Subject: [PATCH 5/5] =?UTF-8?q?=E5=BC=80=E5=8F=91=E7=8E=AF=E5=A2=83=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E9=82=AE=E4=BB=B6=E7=9B=B8=E5=85=B3=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 11 ++++++++++ src/main/resources/application-dev.properties | 22 ++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index db7bdb0b..f781cd68 100644 --- a/pom.xml +++ b/pom.xml @@ -316,6 +316,17 @@ 1.41.5 + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + org.springframework.boot + spring-boot-starter-mail + + diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index a45b62e2..d129aa4d 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -113,4 +113,24 @@ orderList.link=https://develop.aida.com.hk/home/homePage?order= # 0 不发送邮件通知 1 发送邮件通知 stripe.webhook.fail.reminder=0 # kim test -stripe.paymentMethodConfiguration=pmc_1LywTWH7nPZ8bkrN6FvdCUWG \ No newline at end of file +stripe.paymentMethodConfiguration=pmc_1LywTWH7nPZ8bkrN6FvdCUWG + +#thymelea模板配置 +#控制 Thymeleaf 是否启用模板缓存 生产环境用true,以提高性能 +spring.thymeleaf.cache=false + +#指定邮件服务器的地址。 +spring.mail.host=mail.aida.com.hk +#指定邮件服务器的端口号。 +spring.mail.port=465 +#指定登录邮件服务器的用户名 +spring.mail.username=info@aida.com.hk +#指定登录邮件服务器的密码 / 授权码 +spring.mail.password=AIdlab@2025 +spring.mail.default-encoding=UTF-8 + +# SSL 配置 +#启用 SSL 加密 +spring.mail.properties.mail.smtp.ssl.enable=true +#指定 SSL 连接的端口号。通常与 spring.mail.port 一致 +spring.mail.properties.mail.smtp.socketFactory.port=465 \ No newline at end of file