diff --git a/src/main/java/com/ai/da/common/RabbitMQ/EmailRetryConsumer.java b/src/main/java/com/ai/da/common/RabbitMQ/EmailRetryConsumer.java new file mode 100644 index 00000000..e2bec880 --- /dev/null +++ b/src/main/java/com/ai/da/common/RabbitMQ/EmailRetryConsumer.java @@ -0,0 +1,96 @@ +package com.ai.da.common.RabbitMQ; + + +import com.ai.da.common.utils.MailUtil; +import com.ai.da.model.dto.BasicEmailParamDTO; +import com.ai.da.service.EmailService; +import com.alibaba.fastjson.JSONObject; +import com.rabbitmq.client.Channel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.core.io.InputStreamSource; +import org.springframework.stereotype.Component; +import software.amazon.awssdk.core.exception.RetryableException; +import org.springframework.amqp.core.Message; + +import javax.annotation.Resource; + +import javax.mail.MessagingException; +import java.io.IOException; +import java.util.Map; +import java.util.Objects; + +@Slf4j +@Component +public class EmailRetryConsumer { + + @Resource + private MailUtil mailUtil; + @Resource + private MQPublisher mqPublisher; + @Resource + private EmailService emailService; + +// @RabbitListener(queues = "#{rabbitMQProperties.deadLetter.queue}") + public void handleRetry(Map mailParams, Message message, Channel channel) throws IOException { + long tag = message.getMessageProperties().getDeliveryTag(); + try { + log.info("死信队列收到消息:{}", message); + // 处理邮件发送参数 + BasicEmailParamDTO basicEmailParamDTO = JSONObject.parseObject(mailParams.get("dto"), BasicEmailParamDTO.class); + String fileName = mailParams.get("filename"); + InputStreamSource inputStreamSource = Objects.isNull(mailParams.get("source")) ? + null : JSONObject.parseObject(mailParams.get("source"), InputStreamSource.class); + JSONObject templateParams = JSONObject.parseObject(mailParams.get("templateParams"), JSONObject.class); + String templateName = mailParams.get("templatePath"); + long logId = Long.parseLong(mailParams.get("logId")); + basicEmailParamDTO.setContent(mailUtil.setContent(templateParams, templateName)); + // 发邮件 + int lastReturnCode = mailUtil.sendMail(basicEmailParamDTO, fileName, inputStreamSource); + if (lastReturnCode == 250) { + log.info("邮件发送成功!Subject : {}", basicEmailParamDTO.getSubject()); + emailService.updateStatus(logId, EmailService.DELIVERED); + } else if (lastReturnCode == 450) { + log.info("目标邮箱 {} 暂时不可用,请稍后重试", (Object) basicEmailParamDTO.getMailTo()); + // 重试 + retry(mailParams, message, channel, tag, logId); + } else if (lastReturnCode == 550) { + log.info("目标邮箱 {} 不可用,邮件发送失败", (Object) basicEmailParamDTO.getMailTo()); + emailService.updateStatus(logId, EmailService.FAILED); + } else { + log.info("邮件发送失败,Subject : {}, 状态码: {}", basicEmailParamDTO.getSubject(), lastReturnCode); + retry(mailParams, message, channel, tag, logId); + emailService.updateStatus(logId, EmailService.FAILED); + } + channel.basicAck(tag, false); + } catch (RetryableException e) { + log.info("邮件重试发生异常:RetryableException -> {}", e.getMessage()); + channel.basicAck(tag, false); // 确认原消息 + } catch (Exception e) { + log.info("邮件重试发生异常:Exception -> {}", e.getMessage()); + channel.basicAck(tag, false); // 确认原消息 + } + } + + private int getRetryAttempt(Message message) { + Integer attempt = message.getMessageProperties() + .getHeader("x-retry-attempt"); + return attempt != null ? attempt : 1; + } + + private void retry(Map mailParams, Message message, Channel channel, long tag, long logId) throws IOException{ + int attempt = getRetryAttempt(message); + if (attempt >= 3) { // 最大重试次数 + channel.basicReject(tag, false); + emailService.updateStatus(logId, EmailService.FAILED); + log.error("重试结束,邮件最终发送失败: {}", mailParams); + } else { + log.info("重新将邮件信息发送到重试队列"); + mqPublisher.sendEmailMsg(mailParams, attempt); + channel.basicAck(tag, false); // 确认原消息 + // 更新数据库 + emailService.updateRetryCount(logId, attempt + 1); + } + } + +} diff --git a/src/main/java/com/ai/da/common/RabbitMQ/MQConfig.java b/src/main/java/com/ai/da/common/RabbitMQ/MQConfig.java index 493c5a61..de4c1bce 100644 --- a/src/main/java/com/ai/da/common/RabbitMQ/MQConfig.java +++ b/src/main/java/com/ai/da/common/RabbitMQ/MQConfig.java @@ -1,43 +1,82 @@ -package com.ai.da.common.RabbitMQ; - -import org.springframework.amqp.core.Queue; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class MQConfig { - - @Autowired - private RabbitMQProperties rabbitMQProperties; - - @Bean - public Queue generateQueue() { - return new Queue(rabbitMQProperties.getQueues().getGenerate()); - } - - @Bean - public Queue SRQueue() { - return new Queue(rabbitMQProperties.getQueues().getSr()); - } - - @Bean - public Queue SRResultQueue() { - return new Queue(rabbitMQProperties.getQueues().getSrResult()); - } - - @Bean - public Queue generateResultQueue() { - return new Queue(rabbitMQProperties.getQueues().getGenerateResult()); - } - - @Bean - public Queue toProductImageResultQueue() { - return new Queue(rabbitMQProperties.getQueues().getToProductImageResult()); - } - - @Bean - public Queue relightResultQueue() { - return new Queue(rabbitMQProperties.getQueues().getRelightResult()); - } -} +package com.ai.da.common.RabbitMQ; + +import org.springframework.amqp.core.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class MQConfig { + + @Autowired + private RabbitMQProperties rabbitMQProperties; + + @Bean + public Queue generateQueue() { + return new Queue(rabbitMQProperties.getQueues().getGenerate()); + } + + @Bean + public Queue SRQueue() { + return new Queue(rabbitMQProperties.getQueues().getSr()); + } + + @Bean + public Queue SRResultQueue() { + return new Queue(rabbitMQProperties.getQueues().getSrResult()); + } + + @Bean + public Queue generateResultQueue() { + return new Queue(rabbitMQProperties.getQueues().getGenerateResult()); + } + + @Bean + public Queue toProductImageResultQueue() { + return new Queue(rabbitMQProperties.getQueues().getToProductImageResult()); + } + + @Bean + public Queue relightResultQueue() { + return new Queue(rabbitMQProperties.getQueues().getRelightResult()); + } + + @Bean + public Queue poseTransformQueue() { + return new Queue(rabbitMQProperties.getQueues().getPoseTransform()); + } + + @Bean + public Queue mailRetryQueue() { + // 普通队列,不绑定DLX(首次失败后才进入MQ) + // durable 持久化队列 + return QueueBuilder.durable(rabbitMQProperties.getQueues().getEmailRetry()) + // 关键参数:绑定死信交换机 + .withArgument("x-dead-letter-exchange", rabbitMQProperties.getDeadLetter().getExchange()) + // 可选:指定死信路由键(默认使用原消息的路由键) + .withArgument("x-dead-letter-routing-key", rabbitMQProperties.getDeadLetter().getRoutingKey()) + .build(); + } + + // 新增死信交换机 + @Bean + public DirectExchange deadLetterExchange() { + return new DirectExchange(rabbitMQProperties.getDeadLetter().getExchange()); + } + + // 新增死信队列 + @Bean + public Queue deadLetterQueue() { + return QueueBuilder.durable(rabbitMQProperties.getDeadLetter().getQueue()).build(); + } + + // 绑定死信队列 + @Bean + public Binding deadLetterBinding() { + return BindingBuilder.bind(deadLetterQueue()) + .to(deadLetterExchange()) + .with(rabbitMQProperties.getDeadLetter().getRoutingKey()); + } + + +} diff --git a/src/main/java/com/ai/da/common/RabbitMQ/MQPublisher.java b/src/main/java/com/ai/da/common/RabbitMQ/MQPublisher.java index 56b4b724..02c3f99f 100644 --- a/src/main/java/com/ai/da/common/RabbitMQ/MQPublisher.java +++ b/src/main/java/com/ai/da/common/RabbitMQ/MQPublisher.java @@ -1,29 +1,56 @@ -package com.ai.da.common.RabbitMQ; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.amqp.core.AmqpTemplate; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -import javax.annotation.Resource; - -@Slf4j -@Component -public class MQPublisher { - - @Autowired - private RabbitMQProperties rabbitMQProperties; - - @Autowired - private AmqpTemplate amqpTemplate; - - public void sendGenerateMessage(String mm) { - log.info("send message: " + mm); - amqpTemplate.convertAndSend(rabbitMQProperties.getQueues().getGenerate(), mm); - } - - public void sendSRMessage(String mm) { - log.info("send message: " + mm); - amqpTemplate.convertAndSend(rabbitMQProperties.getQueues().getSr(), mm); - } -} +package com.ai.da.common.RabbitMQ; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.core.AmqpTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.Map; + +@Slf4j +@Component +public class MQPublisher { + + @Autowired + private RabbitMQProperties rabbitMQProperties; + + @Autowired + private AmqpTemplate amqpTemplate; + + public void sendGenerateMessage(String mm) { + log.info("send generate message: {}", mm); + amqpTemplate.convertAndSend(rabbitMQProperties.getQueues().getGenerate(), mm); + } + + public void sendSRMessage(String mm) { + log.info("send message: {}", mm); + amqpTemplate.convertAndSend(rabbitMQProperties.getQueues().getSr(), mm); + } + + /** + * + * @param mailParams 含有的字段 + * {"dto": basicEmailParamDTO, "filename": fileName, "source": inputStreamSource, + * "templateParams": jsonObject, "templatePath": path} + * 邮件发送参数,附件文件名,附件数据 + * @param retryTimes 重试次数(初始为0) + */ + public void sendEmailMsg(Map mailParams, int retryTimes){ + log.info("send email MQ message: {} ", mailParams); + // // 重新入队(指数退避) 时间单位:毫秒 + long newDelay = (long) (5000 * Math.pow(2, retryTimes + 1)); + log.info("send email MQ delay: {} ms, retry attempt: {}", newDelay, retryTimes + 1); + amqpTemplate.convertAndSend( + rabbitMQProperties.getQueues().getEmailRetry(), + mailParams, + m -> { + m.getMessageProperties().setExpiration(String.valueOf(newDelay)); + m.getMessageProperties().setHeader("x-retry-attempt", retryTimes + 1); + return m; + } + ); + } + + +} 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 5ce7e692..86a92ae0 100644 --- a/src/main/java/com/ai/da/common/RabbitMQ/RabbitMQProperties.java +++ b/src/main/java/com/ai/da/common/RabbitMQ/RabbitMQProperties.java @@ -1,35 +1,45 @@ -package com.ai.da.common.RabbitMQ; - -import lombok.Data; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -@Component -@ConfigurationProperties(prefix = "rabbitmq") -@Data -public class RabbitMQProperties { - - private Queues queues; - private Exchange exchange; - - @Data - public static class Queues { - private String generate; - private String sr; - private String srResult; - private String generateResult; - private String toProductImageResult; - private String relightResult; - private String poseTransform; - private String designBatch; - private String relightBatch; - private String toProductImageBatch; - private String poseTransformBatch; - } - - @Data - public static class Exchange { - private String generate; - } -} - +package com.ai.da.common.RabbitMQ; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Component +@ConfigurationProperties(prefix = "rabbitmq") +@Data +public class RabbitMQProperties { + + private Queues queues; + private Exchange exchange; + private DeadLetter deadLetter; // 新增死信配置 + + @Data + public static class Queues { + private String generate; + private String sr; + private String srResult; + private String generateResult; + private String toProductImageResult; + private String relightResult; + private String poseTransform; + private String emailRetry; + private String designBatch; + private String relightBatch; + private String toProductImageBatch; + private String poseTransformBatch; + } + + @Data + public static class Exchange { + private String generate; + } + + // 新增死信配置内部类 + @Data + public static class DeadLetter { + private String exchange; + private String queue; + private String routingKey; + } +} + diff --git a/src/main/java/com/ai/da/common/utils/MailUtil.java b/src/main/java/com/ai/da/common/utils/MailUtil.java index cd5882ec..cd060af1 100644 --- a/src/main/java/com/ai/da/common/utils/MailUtil.java +++ b/src/main/java/com/ai/da/common/utils/MailUtil.java @@ -1,168 +1,176 @@ -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); - } - -} +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; + } + + public BasicEmailParamDTO setBasicEmailParams(List mailTo, String title) throws AddressException { + BasicEmailParamDTO basicEmailParamDTO = new BasicEmailParamDTO(); + basicEmailParamDTO.setSenderUserMail("info@aida.com.hk"); + basicEmailParamDTO.setMailTo(getInternetAddressList(mailTo)); + basicEmailParamDTO.setSubject(title); + 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/common/utils/RedisUtil.java b/src/main/java/com/ai/da/common/utils/RedisUtil.java index a77d14cd..6b8294ea 100644 --- a/src/main/java/com/ai/da/common/utils/RedisUtil.java +++ b/src/main/java/com/ai/da/common/utils/RedisUtil.java @@ -1,530 +1,597 @@ -package com.ai.da.common.utils; - -import com.ai.da.model.dto.ProgressDTO; -import com.ai.da.python.vo.DesignPythonObject; -import com.alibaba.fastjson.JSON; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.springframework.data.redis.core.HashOperations; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.core.ZSetOperations; -import org.springframework.stereotype.Component; -import org.springframework.util.CollectionUtils; - -import javax.annotation.Resource; -import java.math.BigDecimal; -import java.math.RoundingMode; -import java.time.Duration; -import java.util.*; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; - -@Slf4j -@Component -public class RedisUtil { - - @Resource - private RedisTemplate redisTemplate; - - public final static String FLUX_POLLING_URL = "Flux:"; - - public Boolean hasKey(String key){ - return redisTemplate.hasKey(key); - } - - //- - - - - - - - - - - - - - - - - - - - - ZSet类型 - - - - - - - - - - - - - - - - - - - - - - /** - * 向ZSet中添加元素 - */ - public void addToZSet(String key, String value, Double score) { - redisTemplate.opsForZSet().add(key, value, score); - } - - /** - * 从ZSet中删除元素 - */ - public void removeFromZSet(String key, String value) { - redisTemplate.opsForZSet().remove(key, value); - } - - /** - * 获取指定元素的当前排列顺序 - */ - public Long getRank(String key, String value) { - return redisTemplate.opsForZSet().rank(key, value); - } - - /** - * 获取当前ZSet中的最大score - */ - public Double getMaxScore(String key) { - Set> set = redisTemplate.opsForZSet().reverseRangeWithScores(key, 0, 0); - - if (!CollectionUtils.isEmpty(set)) { - Double score = set.iterator().next().getScore(); - return score + 1.0; - } else { - return 1.0; - } - } - - /** - * 判断元素是否存在 - */ - public Boolean isElementExistsInZSet(String key, String value) { - return redisTemplate.opsForZSet().score(key, value) != null; - } - - /** - * 获取当前ZSet中数据量的总和 - */ - public Long getZSetTotalCount(String key) { - return redisTemplate.opsForZSet().zCard(key); - } - - - public Set getZSetTotalData(String key){ - return redisTemplate.opsForZSet().range(key, 0, -1); - } - - //- - - - - - - - - - - - - - - - - - - - - set类型 - - - - - - - - - - - - - - - - - - - - - - /** - * 将数据放入set缓存 - */ - public void addToSet(String key, String value) { - redisTemplate.opsForSet().add(key, value); - } - - /** - * 弹出变量中的元素 - */ - public void removeFromSet(String key, String value) { - redisTemplate.opsForSet().remove(key, value); - } - - /** - * 检查给定的元素是否在变量中。 - */ - public Boolean isElementExistsInSet(String key, String obj) { - return redisTemplate.opsForSet().isMember(key, obj); - } - - - //- - - - - - - - - - - - - - - - - - - - - hash类型 - - - - - - - - - - - - - - - - - - - - - - /** - * 加入缓存 - */ - public void addToMap(String key, Map map) { - redisTemplate.opsForHash().putAll(key, map); - } - - /** - * 验证指定 key 下 有没有指定的 hashkey - */ - public Boolean isElementExistsInMap(String key, String hashKey) { - return redisTemplate.opsForHash().hasKey(key, hashKey); - } - - /** - * 获取指定key的值string - */ - public String getMapValue(String key1, String key2) { - return String.valueOf(redisTemplate.opsForHash().get(key1, key2)); - } - - /** - * 删除指定 hash 的 HashKey - * - * @return 删除成功的 数量 - */ - public Long removeFromMap(String key, String hashKeys) { - return redisTemplate.opsForHash().delete(key, hashKeys); - } - - //- - - - - - - - - - - - - - - - - - - - - String类型 - - - - - - - - - - - - - - - - - - - - - public void addToString(String key, String value){ - redisTemplate.opsForValue().set(key,value); - } - - public void addToString(String key, String value, Long expiresIn){ - redisTemplate.opsForValue().set(key,value,expiresIn, TimeUnit.SECONDS); - } - - public String getFromString(String key){ - return redisTemplate.opsForValue().get(key); - } - - public Set getKeysFromString(String key){ - return redisTemplate.keys(key); - } - - public Long getSize(String key){return redisTemplate.opsForSet().size(key);} - - public List getMultiValue(Set keys){ - return redisTemplate.opsForValue().multiGet(keys); - } - - public Long getExpire(String key){ - return redisTemplate.getExpire(key); - } - - public void removeFromString(String key){ - redisTemplate.delete(key); - } - - public final static String PORTFOLIO_LIKE_KEY = "portfolio:like:"; - - public void likePost(Long portfolioId, Long userId) { - redisTemplate.opsForSet().add(PORTFOLIO_LIKE_KEY + portfolioId, String.valueOf(userId)); - } - - public Long getLikeCount(Long portfolioId) { - String key = PORTFOLIO_LIKE_KEY + portfolioId; - return redisTemplate.opsForSet().size(key); - } - - public List getLikedPortfolios(Long userId) { - // 获取所有包含PORTFOLIO_LIKE_KEY的键 - Set likedPortfolios = redisTemplate.keys(PORTFOLIO_LIKE_KEY + "*"); - - // 如果没有喜欢的,返回空列表 - if (likedPortfolios == null || likedPortfolios.isEmpty()) { - return new ArrayList<>(); - } - - // 过滤出包含指定用户ID的键,并提取投资组合ID - return likedPortfolios.stream() - .filter(key -> redisTemplate.opsForSet().isMember(key, String.valueOf(userId))) - .map(key -> Long.valueOf(key.replace(PORTFOLIO_LIKE_KEY, ""))) - .collect(Collectors.toList()); - } - - public void unLikePost(Long portfolioId, Long userId) { - redisTemplate.opsForSet().remove(PORTFOLIO_LIKE_KEY + portfolioId, userId.toString()); - } - - // 检查用户是否喜欢某个作品 - public boolean isPostLikedByUser(Long portfolioId, Long userId) { - String key = PORTFOLIO_LIKE_KEY + portfolioId; - Boolean isMember = redisTemplate.opsForSet().isMember(key, userId.toString()); - return isMember != null && isMember; - } - - public final static String PORTFOLIO_VIEW_KEY = "portfolio:view:"; - - public void increaseViewCount(Long portfolioId) { - String key = PORTFOLIO_VIEW_KEY + portfolioId; - redisTemplate.opsForValue().increment(key); - } - - public Long getViewCount(Long portfolioId) { - String key = PORTFOLIO_VIEW_KEY + portfolioId; - return redisTemplate.opsForValue().increment(key, 0); - } - - public Long getViewCount(String key) { - Object value = redisTemplate.opsForValue().get(key); - if (value instanceof Integer) { - return Long.valueOf((Integer) value); - } else if (value instanceof Long) { - return (Long) value; - } else if (value instanceof String) { - return Long.valueOf((String) value); - } else { - throw new IllegalArgumentException("Unexpected value type"); - } - } - - public final static String PERSONAL_HOMEPAGE_VIEW_KEY = "PersonalHomepage:view:"; - - public void increasePersonalHomepageViewCount(Long accountId) { - String key = PERSONAL_HOMEPAGE_VIEW_KEY + accountId; - redisTemplate.opsForValue().increment(key); - } - - public Long getPersonalHomepageViewCount(Long accountId) { - String key = PERSONAL_HOMEPAGE_VIEW_KEY + accountId; - return redisTemplate.opsForValue().increment(key, 0); - } - - public final static String MOODBOARD_POSITION_KEY = "moodboard:position:"; - - public void saveMoodboardPosition(Long id, String moodboardPosition) { - addToString(MOODBOARD_POSITION_KEY + id, moodboardPosition); - } - - public String getMoodboardPosition(Long id) { - return getFromString(MOODBOARD_POSITION_KEY + id); - } - public final static String NICKNAME_MODIFY_TIMES = "NicknameModifyTimes:"; - public void increaseCount(String key) { - redisTemplate.opsForValue().increment(key); - } - - public Long getIncrementCount(String key) { - return redisTemplate.opsForValue().increment(key, 0); - } - - public void setKeyExpire(String key, Long expire) { - redisTemplate.expire(key, expire, TimeUnit.DAYS); - } - - public final static String CHANGE_MAILBOX = "ChangeMailbox:"; - - // 每天允许通知3次 - public final static String UPLOAD_TIMEOUT_REMINDER_COUNTER = "UploadTimeoutReminderCounter"; - - public void addProcessId(String processId, int progress) { - // Redis 中的键,可以通过 processId 来唯一标识 - String redisKey = "process:progress:" + processId; - - // 将当前进度存储到 Redis - redisTemplate.opsForValue().set(redisKey, String.valueOf(progress)); - - // 设置过期时间为 5 分钟(300 秒) - redisTemplate.expire(redisKey, 5, TimeUnit.MINUTES); - } - - public void addPathToCache(Long collectionId, Long userId, String path) { - // Redis 中的键,唯一标识由 collectionId 和 userId 组成 - String redisKey = "path:cache:" + collectionId + ":" + userId; - - // 增加路径的计数 - redisTemplate.opsForHash().increment(redisKey, path, 1); - - // 设置过期时间为 2 小时(7200 秒) - redisTemplate.expire(redisKey, 8, TimeUnit.HOURS); - } - - public int getPathUsageCount(Long collectionId, Long userId, String path) { - String redisKey = "path:cache:" + collectionId + ":" + userId; - - // 获取路径的使用次数 - Object count = redisTemplate.opsForHash().get(redisKey, path); - return count != null ? Integer.parseInt(count.toString()) : 0; - } - - public void addAssembledObjects(Long collectionId, Set assembledObjects) { - // Redis 中的键,使用 collectionId 来唯一标识 - String redisKey = "collection:assembledObjects:" + collectionId; - - // 将 assembledObjects 转换为 JSON 格式存储,避免直接存储对象 - String assembledObjectsJson = convertToJson(assembledObjects); - - // 使用 Redis 的 set 操作更新集合 - redisTemplate.opsForValue().set(redisKey, assembledObjectsJson); - - // 设置过期时间为 5 分钟(300 秒) - redisTemplate.expire(redisKey, 30, TimeUnit.MINUTES); - } - - // 将 Set 转换为 JSON 格式 - private String convertToJson(Set assembledObjects) { - try { - ObjectMapper objectMapper = new ObjectMapper(); - return objectMapper.writeValueAsString(assembledObjects); - } catch (JsonProcessingException e) { - e.printStackTrace(); - return null; - } - } - - public Set getAssembledObjects(Long collectionId) { - // Redis 中的键,使用 collectionId 来唯一标识 - String redisKey = "collection:assembledObjects:" + collectionId; - - // 从 Redis 获取存储的 JSON 字符串 - String assembledObjectsJson = (String) redisTemplate.opsForValue().get(redisKey); - - if (assembledObjectsJson == null) { - return new HashSet<>(); // 如果没有找到数据,返回一个空的 Set - } - - // 将 JSON 字符串转换为 Set - return convertFromJson(assembledObjectsJson); - } - - // 将 JSON 字符串转换为 Set - private Set convertFromJson(String json) { - try { - ObjectMapper objectMapper = new ObjectMapper(); - // 使用 TypeReference 来指定目标类型是 Set - return objectMapper.readValue(json, new TypeReference>() {}); - } catch (JsonProcessingException e) { - e.printStackTrace(); - return new HashSet<>(); // 如果转换失败,返回空的 Set - } - } - - public final static String PAYMENT_INFO_LAST_SCAN_TIME = "PaymentInfoLastScanTime"; - - public final static String AFFILIATE_LINK_VIEW_KEY = "AffiliateLink:view:"; - - public void increaseAffiliateLinkViewCount(Long accountId) { - String key = AFFILIATE_LINK_VIEW_KEY + accountId; - redisTemplate.opsForValue().increment(key); - } - - public Long getAffiliateLinkViewCount(Long accountId) { - String key = AFFILIATE_LINK_VIEW_KEY + accountId; - return redisTemplate.opsForValue().increment(key, 0); - } - - /** - * 记录任务的耗时到Redis - * @param taskKey 任务标识,如 "taskA" - * @param elapsedTime 本次耗时,单位为毫秒 - */ - public void recordTaskElapsedTime(String taskKey, long elapsedTime) { - String hashKey = "task:stats"; - - // 累加总耗时 - redisTemplate.opsForHash().increment(hashKey, taskKey + ":totalTime", elapsedTime); - - // 增加计数器 - redisTemplate.opsForHash().increment(hashKey, taskKey + ":count", 1); - } - - /** - * 获取任务的平均耗时 - * @param taskKey 任务标识,如 "taskA" - * @return 平均耗时(毫秒) - */ - public double getTaskAverageTime(String taskKey) { - String hashKey = "task:stats"; - - // 获取总耗时和计数 - Object totalTime = redisTemplate.opsForHash().get(hashKey, taskKey + ":totalTime"); - Object count = redisTemplate.opsForHash().get(hashKey, taskKey + ":count"); - - // 计算平均值 - if (totalTime == null || count == null) { - return 0; - } - return Double.parseDouble(totalTime.toString()) / Long.parseLong(count.toString()); - } - - /** - * 清除指定任务的统计数据 - * @param taskKey 任务标识,如 "taskA" - */ - public void clearTaskStats(String taskKey) { - String hashKey = "task:stats"; - - // 删除总耗时和计数器 - redisTemplate.opsForHash().delete(hashKey, taskKey + ":totalTime", taskKey + ":count"); - } - - public void recordTaskElapsedTime(String taskKey, double elapsedTimeInSeconds) { - // 将耗时转换为 BigDecimal,并四舍五入保留四位小数 - BigDecimal elapsedTime = new BigDecimal(elapsedTimeInSeconds).setScale(4, RoundingMode.HALF_UP); - - // 累加总耗时(以毫秒为单位) - redisTemplate.opsForHash().increment("task:stats", taskKey + ":totalTime", elapsedTime.doubleValue()); - - // 增加计数器 - redisTemplate.opsForHash().increment("task:stats", taskKey + ":count", 1); - } - - // 获取第一部分(Sketch)耗时 - public double getFirstSketchTime() { - // 获取 "firstSketchTime:totalTime" 对应的值,并返回(单位为秒) - Object time = redisTemplate.opsForHash().get("task:stats", "firstSketchTime:totalTime"); - return time != null ? (double) time : 0.0; - } - - // 获取第二部分(获取特征值)耗时 - public double getGetAttributeRecognitionTime() { - // 获取 "getAttributeRecognitionTime:totalTime" 对应的值,并返回(单位为秒) - Object time = redisTemplate.opsForHash().get("task:stats", "getAttributeRecognitionTime:totalTime"); - return time != null ? (double) time : 0.0; - } - - // 获取第三部分(搭配 Sketch)耗时 - public double getOtherSketchTime() { - // 获取 "otherSketchTime:totalTime" 对应的值,并返回(单位为秒) - Object time = redisTemplate.opsForHash().get("task:stats", "otherSketchTime:totalTime"); - return time != null ? (double) time : 0.0; - } - - // 清理三部分的缓存 - public void clearTaskElapsedTimeCache() { - // 删除第一部分的缓存 - redisTemplate.opsForHash().delete("task:stats", "firstSketchTime:totalTime"); - redisTemplate.opsForHash().delete("task:stats", "firstSketchTime:count"); - - // 删除第二部分的缓存 - redisTemplate.opsForHash().delete("task:stats", "getAttributeRecognitionTime:totalTime"); - redisTemplate.opsForHash().delete("task:stats", "getAttributeRecognitionTime:count"); - - // 删除第三部分的缓存 - redisTemplate.opsForHash().delete("task:stats", "otherSketchTime:totalTime"); - redisTemplate.opsForHash().delete("task:stats", "otherSketchTime:count"); - } - - public boolean incrementLikeCount(Long userId, String sketchPath) { - String redisKey = "user_like_count:" + userId; - try { - redisTemplate.opsForHash().increment(redisKey, sketchPath, 1); - return true; - } catch (Exception e) { - log.error("Error incrementing like count for userId {} and sketchPath {}: {}", userId, sketchPath, e.getMessage()); - return false; - } - } - - public int getLikeCount(Long userId, String sketchPath) { - String redisKey = "user_like_count:" + userId; - Object count = redisTemplate.opsForHash().get(redisKey, sketchPath); - return count != null ? Integer.parseInt(count.toString()) : 0; - } - - public void storeMaxLikeCount(Long userId, int maxLikeCount) { - String redisKey = "user_max_like_count:" + userId; - redisTemplate.opsForValue().set(redisKey, String.valueOf(maxLikeCount)); - } - - public int getMaxLikeCount(Long userId) { - String redisKey = "user_max_like_count:" + userId; - String maxLikeCount = redisTemplate.opsForValue().get(redisKey); - return maxLikeCount != null ? Integer.parseInt(maxLikeCount) : 0; - } - - public final static String IMAGE_SEGMENTATION = "ImageSegmentation:"; - - public final static String STRIPE_EXCEPTION_LOG = "StripeException:"; - - public void batchDeleteKeysWithSamePrefix(String prefix){ - Set keys = redisTemplate.keys(prefix + "*"); - assert keys != null; - if (!keys.isEmpty()){ - redisTemplate.delete(keys); - } - } - - public void setTaskProgressDTO(String taskId, ProgressDTO dto) { - String key = "task:progress:" + taskId; - redisTemplate.opsForValue().set(key, JSON.toJSONString(dto), Duration.ofDays(1)); - } - - public ProgressDTO getTaskProgressDTO(String taskId) { - String key = "task:progress:" + taskId; - String json = redisTemplate.opsForValue().get(key); - if (StringUtils.isBlank(json)) { -// return new ProgressDTO(0, 0, false); - return null; - } - try { - return JSON.parseObject(json, ProgressDTO.class); - } catch (Exception e) { - log.warn("任务进度解析失败 key={}, json={}", key, json); - return new ProgressDTO(0, 0, false); - } - } -} +package com.ai.da.common.utils; + +import com.ai.da.model.dto.ProgressDTO; +import com.ai.da.python.vo.DesignPythonObject; +import com.alibaba.fastjson.JSON; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ZSetOperations; +import org.springframework.data.redis.core.script.DefaultRedisScript; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +@Slf4j +@Component +public class RedisUtil { + + @Resource + private RedisTemplate redisTemplate; + + public final static String FLUX_POLLING_URL = "Flux:"; + + public Boolean hasKey(String key){ + return redisTemplate.hasKey(key); + } + + //- - - - - - - - - - - - - - - - - - - - - ZSet类型 - - - - - - - - - - - - - - - - - - - - + + /** + * 向ZSet中添加元素 + */ + public void addToZSet(String key, String value, Double score) { + redisTemplate.opsForZSet().add(key, value, score); + } + + /** + * 从ZSet中删除元素 + */ + public void removeFromZSet(String key, String value) { + redisTemplate.opsForZSet().remove(key, value); + } + + /** + * 获取指定元素的当前排列顺序 + */ + public Long getRank(String key, String value) { + return redisTemplate.opsForZSet().rank(key, value); + } + + /** + * 获取当前ZSet中的最大score + */ + public Double getMaxScore(String key) { + Set> set = redisTemplate.opsForZSet().reverseRangeWithScores(key, 0, 0); + + if (!CollectionUtils.isEmpty(set)) { + Double score = set.iterator().next().getScore(); + return score + 1.0; + } else { + return 1.0; + } + } + + /** + * 判断元素是否存在 + */ + public Boolean isElementExistsInZSet(String key, String value) { + return redisTemplate.opsForZSet().score(key, value) != null; + } + + /** + * 获取当前ZSet中数据量的总和 + */ + public Long getZSetTotalCount(String key) { + return redisTemplate.opsForZSet().zCard(key); + } + + + public Set getZSetTotalData(String key){ + return redisTemplate.opsForZSet().range(key, 0, -1); + } + + //- - - - - - - - - - - - - - - - - - - - - set类型 - - - - - - - - - - - - - - - - - - - - + + /** + * 将数据放入set缓存 + */ + public void addToSet(String key, String value) { + redisTemplate.opsForSet().add(key, value); + } + + /** + * 弹出变量中的元素 + */ + public void removeFromSet(String key, String value) { + redisTemplate.opsForSet().remove(key, value); + } + + /** + * 检查给定的元素是否在变量中。 + */ + public Boolean isElementExistsInSet(String key, String obj) { + return redisTemplate.opsForSet().isMember(key, obj); + } + + + //- - - - - - - - - - - - - - - - - - - - - hash类型 - - - - - - - - - - - - - - - - - - - - + + /** + * 加入缓存 + */ + public void addToMap(String key, Map map) { + redisTemplate.opsForHash().putAll(key, map); + } + + /** + * 验证指定 key 下 有没有指定的 hashkey + */ + public Boolean isElementExistsInMap(String key, String hashKey) { + return redisTemplate.opsForHash().hasKey(key, hashKey); + } + + /** + * 获取指定key的值string + */ + public String getMapValue(String key1, String key2) { + return String.valueOf(redisTemplate.opsForHash().get(key1, key2)); + } + + /** + * 删除指定 hash 的 HashKey + * + * @return 删除成功的 数量 + */ + public Long removeFromMap(String key, String hashKeys) { + return redisTemplate.opsForHash().delete(key, hashKeys); + } + + //- - - - - - - - - - - - - - - - - - - - - String类型 - - - - - - - - - - - - - - - - - - - - + public void addToString(String key, String value){ + redisTemplate.opsForValue().set(key,value); + } + + public void addToString(String key, String value, Long expiresIn){ + redisTemplate.opsForValue().set(key,value,expiresIn, TimeUnit.SECONDS); + } + + public String getFromString(String key){ + return redisTemplate.opsForValue().get(key); + } + + public Set getKeysFromString(String key){ + return redisTemplate.keys(key); + } + + public Long getSize(String key){return redisTemplate.opsForSet().size(key);} + + public List getMultiValue(Set keys){ + return redisTemplate.opsForValue().multiGet(keys); + } + + public Long getExpire(String key){ + return redisTemplate.getExpire(key); + } + + public void removeFromString(String key){ + redisTemplate.delete(key); + } + + public final static String PORTFOLIO_LIKE_KEY = "portfolio:like:"; + + public void likePost(Long portfolioId, Long userId) { + redisTemplate.opsForSet().add(PORTFOLIO_LIKE_KEY + portfolioId, String.valueOf(userId)); + } + + public Long getLikeCount(Long portfolioId) { + String key = PORTFOLIO_LIKE_KEY + portfolioId; + return redisTemplate.opsForSet().size(key); + } + + public List getLikedPortfolios(Long userId) { + // 获取所有包含PORTFOLIO_LIKE_KEY的键 + Set likedPortfolios = redisTemplate.keys(PORTFOLIO_LIKE_KEY + "*"); + + // 如果没有喜欢的,返回空列表 + if (likedPortfolios == null || likedPortfolios.isEmpty()) { + return new ArrayList<>(); + } + + // 过滤出包含指定用户ID的键,并提取投资组合ID + return likedPortfolios.stream() + .filter(key -> redisTemplate.opsForSet().isMember(key, String.valueOf(userId))) + .map(key -> Long.valueOf(key.replace(PORTFOLIO_LIKE_KEY, ""))) + .collect(Collectors.toList()); + } + + public void unLikePost(Long portfolioId, Long userId) { + redisTemplate.opsForSet().remove(PORTFOLIO_LIKE_KEY + portfolioId, userId.toString()); + } + + // 检查用户是否喜欢某个作品 + public boolean isPostLikedByUser(Long portfolioId, Long userId) { + String key = PORTFOLIO_LIKE_KEY + portfolioId; + Boolean isMember = redisTemplate.opsForSet().isMember(key, userId.toString()); + return isMember != null && isMember; + } + + public final static String PORTFOLIO_VIEW_KEY = "portfolio:view:"; + + public void increaseViewCount(Long portfolioId) { + String key = PORTFOLIO_VIEW_KEY + portfolioId; + redisTemplate.opsForValue().increment(key); + } + + public Long getViewCount(Long portfolioId) { + String key = PORTFOLIO_VIEW_KEY + portfolioId; + return redisTemplate.opsForValue().increment(key, 0); + } + + public Long getViewCount(String key) { + Object value = redisTemplate.opsForValue().get(key); + if (value instanceof Integer) { + return Long.valueOf((Integer) value); + } else if (value instanceof Long) { + return (Long) value; + } else if (value instanceof String) { + return Long.valueOf((String) value); + } else { + throw new IllegalArgumentException("Unexpected value type"); + } + } + + public final static String PERSONAL_HOMEPAGE_VIEW_KEY = "PersonalHomepage:view:"; + + public void increasePersonalHomepageViewCount(Long accountId) { + String key = PERSONAL_HOMEPAGE_VIEW_KEY + accountId; + redisTemplate.opsForValue().increment(key); + } + + public Long getPersonalHomepageViewCount(Long accountId) { + String key = PERSONAL_HOMEPAGE_VIEW_KEY + accountId; + return redisTemplate.opsForValue().increment(key, 0); + } + + public final static String MOODBOARD_POSITION_KEY = "moodboard:position:"; + + public void saveMoodboardPosition(Long id, String moodboardPosition) { + addToString(MOODBOARD_POSITION_KEY + id, moodboardPosition); + } + + public String getMoodboardPosition(Long id) { + return getFromString(MOODBOARD_POSITION_KEY + id); + } + public final static String NICKNAME_MODIFY_TIMES = "NicknameModifyTimes:"; + public void increaseCount(String key) { + redisTemplate.opsForValue().increment(key); + } + + public Long getIncrementCount(String key) { + return redisTemplate.opsForValue().increment(key, 0); + } + + public void setKeyExpire(String key, Long expire) { + redisTemplate.expire(key, expire, TimeUnit.DAYS); + } + + public final static String CHANGE_MAILBOX = "ChangeMailbox:"; + + // 每天允许通知3次 + public final static String UPLOAD_TIMEOUT_REMINDER_COUNTER = "UploadTimeoutReminderCounter"; + + public void addProcessId(String processId, int progress) { + // Redis 中的键,可以通过 processId 来唯一标识 + String redisKey = "process:progress:" + processId; + + // 将当前进度存储到 Redis + redisTemplate.opsForValue().set(redisKey, String.valueOf(progress)); + + // 设置过期时间为 5 分钟(300 秒) + redisTemplate.expire(redisKey, 5, TimeUnit.MINUTES); + } + + public void addPathToCache(Long collectionId, Long userId, String path) { + // Redis 中的键,唯一标识由 collectionId 和 userId 组成 + String redisKey = "path:cache:" + collectionId + ":" + userId; + + // 增加路径的计数 + redisTemplate.opsForHash().increment(redisKey, path, 1); + + // 设置过期时间为 2 小时(7200 秒) + redisTemplate.expire(redisKey, 8, TimeUnit.HOURS); + } + + public int getPathUsageCount(Long collectionId, Long userId, String path) { + String redisKey = "path:cache:" + collectionId + ":" + userId; + + // 获取路径的使用次数 + Object count = redisTemplate.opsForHash().get(redisKey, path); + return count != null ? Integer.parseInt(count.toString()) : 0; + } + + public void addAssembledObjects(Long collectionId, Set assembledObjects) { + // Redis 中的键,使用 collectionId 来唯一标识 + String redisKey = "collection:assembledObjects:" + collectionId; + + // 将 assembledObjects 转换为 JSON 格式存储,避免直接存储对象 + String assembledObjectsJson = convertToJson(assembledObjects); + + // 使用 Redis 的 set 操作更新集合 + redisTemplate.opsForValue().set(redisKey, assembledObjectsJson); + + // 设置过期时间为 5 分钟(300 秒) + redisTemplate.expire(redisKey, 30, TimeUnit.MINUTES); + } + + // 将 Set 转换为 JSON 格式 + private String convertToJson(Set assembledObjects) { + try { + ObjectMapper objectMapper = new ObjectMapper(); + return objectMapper.writeValueAsString(assembledObjects); + } catch (JsonProcessingException e) { + e.printStackTrace(); + return null; + } + } + + public Set getAssembledObjects(Long collectionId) { + // Redis 中的键,使用 collectionId 来唯一标识 + String redisKey = "collection:assembledObjects:" + collectionId; + + // 从 Redis 获取存储的 JSON 字符串 + String assembledObjectsJson = (String) redisTemplate.opsForValue().get(redisKey); + + if (assembledObjectsJson == null) { + return new HashSet<>(); // 如果没有找到数据,返回一个空的 Set + } + + // 将 JSON 字符串转换为 Set + return convertFromJson(assembledObjectsJson); + } + + // 将 JSON 字符串转换为 Set + private Set convertFromJson(String json) { + try { + ObjectMapper objectMapper = new ObjectMapper(); + // 使用 TypeReference 来指定目标类型是 Set + return objectMapper.readValue(json, new TypeReference>() {}); + } catch (JsonProcessingException e) { + e.printStackTrace(); + return new HashSet<>(); // 如果转换失败,返回空的 Set + } + } + + public final static String PAYMENT_INFO_LAST_SCAN_TIME = "PaymentInfoLastScanTime"; + + public final static String AFFILIATE_LINK_VIEW_KEY = "AffiliateLink:view:"; + + public void increaseAffiliateLinkViewCount(Long accountId) { + String key = AFFILIATE_LINK_VIEW_KEY + accountId; + redisTemplate.opsForValue().increment(key); + } + + public Long getAffiliateLinkViewCount(Long accountId) { + String key = AFFILIATE_LINK_VIEW_KEY + accountId; + return redisTemplate.opsForValue().increment(key, 0); + } + + /** + * 记录任务的耗时到Redis + * @param taskKey 任务标识,如 "taskA" + * @param elapsedTime 本次耗时,单位为毫秒 + */ + public void recordTaskElapsedTime(String taskKey, long elapsedTime) { + String hashKey = "task:stats"; + + // 累加总耗时 + redisTemplate.opsForHash().increment(hashKey, taskKey + ":totalTime", elapsedTime); + + // 增加计数器 + redisTemplate.opsForHash().increment(hashKey, taskKey + ":count", 1); + } + + /** + * 获取任务的平均耗时 + * @param taskKey 任务标识,如 "taskA" + * @return 平均耗时(毫秒) + */ + public double getTaskAverageTime(String taskKey) { + String hashKey = "task:stats"; + + // 获取总耗时和计数 + Object totalTime = redisTemplate.opsForHash().get(hashKey, taskKey + ":totalTime"); + Object count = redisTemplate.opsForHash().get(hashKey, taskKey + ":count"); + + // 计算平均值 + if (totalTime == null || count == null) { + return 0; + } + return Double.parseDouble(totalTime.toString()) / Long.parseLong(count.toString()); + } + + /** + * 清除指定任务的统计数据 + * @param taskKey 任务标识,如 "taskA" + */ + public void clearTaskStats(String taskKey) { + String hashKey = "task:stats"; + + // 删除总耗时和计数器 + redisTemplate.opsForHash().delete(hashKey, taskKey + ":totalTime", taskKey + ":count"); + } + + public void recordTaskElapsedTime(String taskKey, double elapsedTimeInSeconds) { + // 将耗时转换为 BigDecimal,并四舍五入保留四位小数 + BigDecimal elapsedTime = new BigDecimal(elapsedTimeInSeconds).setScale(4, RoundingMode.HALF_UP); + + // 累加总耗时(以毫秒为单位) + redisTemplate.opsForHash().increment("task:stats", taskKey + ":totalTime", elapsedTime.doubleValue()); + + // 增加计数器 + redisTemplate.opsForHash().increment("task:stats", taskKey + ":count", 1); + } + + // 获取第一部分(Sketch)耗时 + public double getFirstSketchTime() { + // 获取 "firstSketchTime:totalTime" 对应的值,并返回(单位为秒) + Object time = redisTemplate.opsForHash().get("task:stats", "firstSketchTime:totalTime"); + return time != null ? (double) time : 0.0; + } + + // 获取第二部分(获取特征值)耗时 + public double getGetAttributeRecognitionTime() { + // 获取 "getAttributeRecognitionTime:totalTime" 对应的值,并返回(单位为秒) + Object time = redisTemplate.opsForHash().get("task:stats", "getAttributeRecognitionTime:totalTime"); + return time != null ? (double) time : 0.0; + } + + // 获取第三部分(搭配 Sketch)耗时 + public double getOtherSketchTime() { + // 获取 "otherSketchTime:totalTime" 对应的值,并返回(单位为秒) + Object time = redisTemplate.opsForHash().get("task:stats", "otherSketchTime:totalTime"); + return time != null ? (double) time : 0.0; + } + + // 清理三部分的缓存 + public void clearTaskElapsedTimeCache() { + // 删除第一部分的缓存 + redisTemplate.opsForHash().delete("task:stats", "firstSketchTime:totalTime"); + redisTemplate.opsForHash().delete("task:stats", "firstSketchTime:count"); + + // 删除第二部分的缓存 + redisTemplate.opsForHash().delete("task:stats", "getAttributeRecognitionTime:totalTime"); + redisTemplate.opsForHash().delete("task:stats", "getAttributeRecognitionTime:count"); + + // 删除第三部分的缓存 + redisTemplate.opsForHash().delete("task:stats", "otherSketchTime:totalTime"); + redisTemplate.opsForHash().delete("task:stats", "otherSketchTime:count"); + } + + public boolean incrementLikeCount(Long userId, String sketchPath) { + String redisKey = "user_like_count:" + userId; + try { + redisTemplate.opsForHash().increment(redisKey, sketchPath, 1); + return true; + } catch (Exception e) { + log.error("Error incrementing like count for userId {} and sketchPath {}: {}", userId, sketchPath, e.getMessage()); + return false; + } + } + + public int getLikeCount(Long userId, String sketchPath) { + String redisKey = "user_like_count:" + userId; + Object count = redisTemplate.opsForHash().get(redisKey, sketchPath); + return count != null ? Integer.parseInt(count.toString()) : 0; + } + + public void storeMaxLikeCount(Long userId, int maxLikeCount) { + String redisKey = "user_max_like_count:" + userId; + redisTemplate.opsForValue().set(redisKey, String.valueOf(maxLikeCount)); + } + + public int getMaxLikeCount(Long userId) { + String redisKey = "user_max_like_count:" + userId; + String maxLikeCount = redisTemplate.opsForValue().get(redisKey); + return maxLikeCount != null ? Integer.parseInt(maxLikeCount) : 0; + } + + public final static String IMAGE_SEGMENTATION = "ImageSegmentation:"; + + public final static String STRIPE_EXCEPTION_LOG = "StripeException:"; + + public void batchDeleteKeysWithSamePrefix(String prefix){ + Set keys = redisTemplate.keys(prefix + "*"); + assert keys != null; + if (!keys.isEmpty()){ + redisTemplate.delete(keys); + } + } + + public void setTaskProgressDTO(String taskId, ProgressDTO dto) { + String key = "task:progress:" + taskId; + redisTemplate.opsForValue().set(key, JSON.toJSONString(dto), Duration.ofDays(1)); + } + + public ProgressDTO getTaskProgressDTO(String taskId) { + String key = "task:progress:" + taskId; + String json = redisTemplate.opsForValue().get(key); + if (StringUtils.isBlank(json)) { +// return new ProgressDTO(0, 0, false); + return null; + } + try { + return JSON.parseObject(json, ProgressDTO.class); + } catch (Exception e) { + log.warn("任务进度解析失败 key={}, json={}", key, json); + return new ProgressDTO(0, 0, false); + } + } + + // Lua脚本(原子化操作) + /*private static final String RATE_LIMIT_SCRIPT = + "local current = redis.call('INCR', KEYS[1])\n" + + "if tonumber(current) == 1 then\n" + + " redis.call('EXPIRE', KEYS[1], ARGV[1])\n" + + "end\n" + + "return tonumber(current) <= tonumber(ARGV[2])";*/ + private static final String RATE_LIMIT_SCRIPT = + "local current = redis.call('INCR', KEYS[1])\n" + + "local ttl = redis.call('TTL', KEYS[1])\n" + + "if tonumber(current) == 1 or tonumber(ttl) == -1 then\n" + + " redis.call('EXPIRE', KEYS[1], ARGV[1])\n" + + "end\n" + + "return tonumber(current) <= tonumber(ARGV[2])"; + + /** + * 检查是否允许发送 + * @param userId 用户ID + * @return true-允许发送,false-已超限 + */ + public boolean allowSend(Long userId) { + String hourKey = getCurrentHourKey(userId); + + // 执行Lua脚本 + List keys = Collections.singletonList(hourKey); + List args = Arrays.asList( + 3600L, // 1小时过期 + 10L // 限制数量 一小时只能向普通用户发10封 + ); + + Boolean result = redisTemplate.execute( + new DefaultRedisScript<>(RATE_LIMIT_SCRIPT, Boolean.class), + keys, + args.toArray() + ); + + return Boolean.TRUE.equals(result); + } + + /** + * 获取当前小时的Key + * 格式:email_limit:{userId}:{yyyyMMddHH} + */ + private String getCurrentHourKey(Long userId) { + String hour = LocalDateTime.now() + .format(DateTimeFormatter.ofPattern("yyyyMMddHH")); + return String.format("email_limit:%s:%s", userId, hour); + } + + /** + * 获取当前已发送数量 + */ + public int getCurrentCount(Long userId) { + String key = getCurrentHourKey(userId); + String val = redisTemplate.opsForValue().get(key); + int count; + if (StringUtils.isBlank(val)){ + count = 0; + }else { + count = Integer.parseInt(val); + } + return count; + } + +} diff --git a/src/main/java/com/ai/da/common/utils/SendEmailUtil.java b/src/main/java/com/ai/da/common/utils/SendEmailUtil.java index 877f728c..f6474c01 100644 --- a/src/main/java/com/ai/da/common/utils/SendEmailUtil.java +++ b/src/main/java/com/ai/da/common/utils/SendEmailUtil.java @@ -426,45 +426,7 @@ public class SendEmailUtil { } } - private final static Long GENERATE_EXCEPTION_WARNING_ID = 122589L; - - public static void sendGenerateExceptionWarning(String message) { - try { - // 实例化一个认证对象 - Credential cred = new Credential(SECRET_ID, SECRET_KEy); - HttpProfile httpProfile = new HttpProfile(); - httpProfile.setEndpoint("ses.tencentcloudapi.com"); - ClientProfile clientProfile = new ClientProfile(); - clientProfile.setHttpProfile(httpProfile); - SesClient client = new SesClient(cred, "ap-hongkong", clientProfile); - SendEmailRequest req = new SendEmailRequest(); - req.setFromEmailAddress(CODE_CREATE_SEND_ADDRESS); - req.setDestination(new String[]{"xupei3360@163.com"}); - - // 根据邮件类型设置不同的主题和模板 - String subject = ""; - Template template = new Template(); - subject = "Warning: AiDA 3.0 Generate Exception Warning"; - template.setTemplateID(GENERATE_EXCEPTION_WARNING_ID); - - JSONObject parameter = new JSONObject(); - parameter.put("errorMessage", message); - parameter.put("time", DateUtil.dateToStr(new Date(), DateUtil.YYYY_MM_DD_HH_MM_SS)); - - template.setTemplateData(parameter.toJSONString()); - - req.setSubject(subject); - req.setTemplate(template); - - // 发送邮件 - SendEmailResponse resp = client.SendEmail(req); - log.info("短信发送结果res###{}", SendEmailResponse.toJsonString(resp)); - } catch (TencentCloudSDKException e) { - log.info("邮件发送失败###{}", e.toString()); - throw new BusinessException("failed.to.send.mail"); - } - } - + // todo ?需要保留吗 private final static Long QUESTIONNAIRE_FEEDBACK_EN_ID = 124151L; private final static Long QUESTIONNAIRE_FEEDBACK_CN_ID = 124156L; @@ -606,6 +568,7 @@ public class SendEmailUtil { } } + // todo 目前该定时器已取消,是否需要保留该模板? private final static Long NEW_USER_REGISTER_NOTIFICATION_EN = 126919L; public static void notificationForRegisterUser(String receiverAddress) { @@ -644,51 +607,6 @@ public class SendEmailUtil { } } - private final static Long CHANGE_MAILBOX_CONFIRM_CN = 128278L; - private final static Long CHANGE_MAILBOX_CONFIRM_EN = 128277L; - - public static void changeMailboxConfirm(String receiverAddress, String language, String name, String link) { - try { - // 实例化一个认证对象,入参需要传入腾讯云账户 SecretId 和 SecretKey,此处还需注意密钥对的保密 - // 代码泄露可能会导致 SecretId 和 SecretKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考,建议采用更安全的方式来使用密钥,请参见:https://cloud.tencent.com/document/product/1278/85305 - // 密钥可前往官网控制台 https://console.cloud.tencent.com/cam/capi 进行获取 - Credential cred = new Credential(SECRET_ID, SECRET_KEy); - // 实例化一个http选项,可选的,没有特殊需求可以跳过 - HttpProfile httpProfile = new HttpProfile(); - httpProfile.setEndpoint("ses.tencentcloudapi.com"); - // 实例化一个client选项,可选的,没有特殊需求可以跳过 - ClientProfile clientProfile = new ClientProfile(); - clientProfile.setHttpProfile(httpProfile); - // 实例化要请求产品的client对象,clientProfile是可选的 - SesClient client = new SesClient(cred, "ap-hongkong", clientProfile); - // 实例化一个请求对象,每个接口都会对应一个request对象 - SendEmailRequest req = new SendEmailRequest(); - req.setFromEmailAddress(SEND_ADDRESS); - req.setDestination(new String[]{receiverAddress}); - Template template = new Template(); - if (language.equals("ENGLISH")) { - req.setSubject("Change the email address bound to the AiDA account"); - template.setTemplateID(CHANGE_MAILBOX_CONFIRM_EN); - } else { - req.setSubject("更换AiDA账号绑定的邮箱地址"); - template.setTemplateID(CHANGE_MAILBOX_CONFIRM_CN); - } - JSONObject param = new JSONObject(); - param.put("userName", name); - param.put("link", link); - - template.setTemplateData(param.toJSONString()); - req.setTemplate(template); - - // 返回的resp是一个SendEmailResponse的实例,与请求对象对应 - SendEmailResponse resp = client.SendEmail(req); - log.info("短信发送结果res###{}", SendEmailResponse.toJsonString(resp)); - } catch (TencentCloudSDKException e) { - log.info("邮件发送失败###{}", e.toString()); - throw new BusinessException("failed.to.send.mail"); - } - } - private final static Long UPLOAD_TIMEOUT_REMINDER = 128324L; public static void uploadTimeoutReminder(String userName, String time) { @@ -732,7 +650,7 @@ public class SendEmailUtil { } } - private final static Long HALFPRICEPROMOTION_CN_ID = 128582L; + /*private final static Long HALFPRICEPROMOTION_CN_ID = 128582L; private final static Long HALFPRICEPROMOTION_EN_ID = 128583L; public static void halfPricePromotion(Account account, String senderAddress, int type) { @@ -821,7 +739,7 @@ public class SendEmailUtil { log.info("邮件发送失败###{}", e.toString()); throw new BusinessException("failed.to.send.mail"); } - } + }*/ private final static Long CANCEL_MERCHANT_EN = 130720L; // private final static Long NEW_MERCHANT_EN = 130721L; @@ -1074,4 +992,52 @@ public class SendEmailUtil { } } + private final static Long CN_2025_618 = 141425L; + private final static Long EN_2025_618 = 141424L; + public static void send618PromotionEmailTemp(String receiver, String language){ + try { + // 实例化一个认证对象 + Credential cred = new Credential(SECRET_ID, SECRET_KEy); + HttpProfile httpProfile = new HttpProfile(); + httpProfile.setEndpoint("ses.tencentcloudapi.com"); + ClientProfile clientProfile = new ClientProfile(); + clientProfile.setHttpProfile(httpProfile); + SesClient client = new SesClient(cred, "ap-hongkong", clientProfile); + SendEmailRequest req = new SendEmailRequest(); + req.setFromEmailAddress(CODE_CREATE_SEND_ADDRESS); + req.setDestination(new String[]{receiver}); + + // 根据邮件类型设置不同的主题和模板 + String subject = ""; + Template template = new Template(); +// if (type == 1) { +// subject = "Upcoming System Upgrade for AiDA 3.0"; +// template.setTemplateID(UPGRADE_NOTIFICATION_ID); +// }else { +// subject = "即将到来的AiDA 3.0系统升级"; +// template.setTemplateID(UPGRADE_NOTIFICATION_ID_CHINESE); +// } + if (language.equals("ENGLISH")) { + subject = "Welcome back !Subscribe AiDA with the discount code to enjoy 50% OFF!"; + template.setTemplateID(EN_2025_618); + }else { + subject = "设计时速狂飙!AiDA 618半价让灵感永不限流!"; + template.setTemplateID(CN_2025_618); + } + + req.setSubject(subject); + req.setTemplate(template); + + // 发送邮件 + SendEmailResponse resp = client.SendEmail(req); + log.info("邮件发送成功,收件人地址:{}", receiver); + log.info("短信发送结果res###{}", SendEmailResponse.toJsonString(resp)); + } catch (TencentCloudSDKException e) { + log.info(receiver); + log.error("邮件发送失败###{},收件人地址:{}", e.toString(), receiver); + + } + + } + } diff --git a/src/main/java/com/ai/da/service/EmailService.java b/src/main/java/com/ai/da/service/EmailService.java index 00373e8a..1e72dde2 100644 --- a/src/main/java/com/ai/da/service/EmailService.java +++ b/src/main/java/com/ai/da/service/EmailService.java @@ -1,138 +1,145 @@ -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); -} +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 { + + String FAILED = "failed"; + String DELIVERED = "delivered"; + String RETRYING = "retrying"; + + 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); + + void updateRetryCount(Long logId, int retryCount); + + void updateStatus(Long logId, String status); + + /** + * 适用于 : 需要自定义发件人信息 + * + * @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/AffiliateServiceImpl.java b/src/main/java/com/ai/da/service/impl/AffiliateServiceImpl.java index ca3b87cc..6ff66865 100644 --- a/src/main/java/com/ai/da/service/impl/AffiliateServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/AffiliateServiceImpl.java @@ -1,388 +1,394 @@ -package com.ai.da.service.impl; - -import com.ai.da.common.config.exception.BusinessException; -import com.ai.da.common.constant.CommonConstant; -import com.ai.da.common.context.UserContext; -import com.ai.da.common.response.PageBaseResponse; -import com.ai.da.common.response.ResultEnum; -import com.ai.da.common.utils.CopyUtil; -import com.ai.da.common.utils.RedisUtil; -import com.ai.da.common.utils.SendEmailUtil; -import com.ai.da.mapper.primary.AffiliateIncomeMapper; -import com.ai.da.mapper.primary.AffiliateMapper; -import com.ai.da.mapper.primary.ProductCouponsMapper; -import com.ai.da.mapper.primary.SubscriptionInfoMapper; -import com.ai.da.mapper.primary.entity.*; -import com.ai.da.model.dto.AffiliateEmailParamsDTO; -import com.ai.da.model.dto.AffiliateQueryDTO; -import com.ai.da.model.vo.AffiliateInvitationDetailsVO; -import com.ai.da.model.vo.AffiliateVO; -import com.ai.da.model.vo.AuthPrincipalVo; -import com.ai.da.service.*; -import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; -import com.baomidou.mybatisplus.core.metadata.IPage; -import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.mysql.cj.util.StringUtils; -import io.netty.util.internal.StringUtil; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.util.CollectionUtils; - -import javax.annotation.Resource; -import java.math.BigDecimal; -import java.time.LocalDateTime; -import java.util.*; -import java.util.function.Function; - -@Service -@Slf4j -public class AffiliateServiceImpl extends ServiceImpl implements AffiliateService { - - @Resource - private OrderInfoService orderInfoService; - @Resource - private AccountService accountService; - @Resource - private PaymentInfoService paymentInfoService; - @Resource - private SubscriptionInfoMapper subscriptionInfoMapper; - @Resource - private AffiliateIncomeMapper affiliateIncomeMapper; - @Resource - private StripeService stripeService; - @Resource - private ProductCouponsMapper productCouponsMapper; - @Resource - private RedisUtil redisUtil; - - // 推广者注册 - public Boolean registerAsAnAffiliate(String promotionMethod){ - AuthPrincipalVo userHolder = UserContext.getUserHolder(); - // 判断该用户是否已注册 - QueryWrapper qw = new QueryWrapper<>(); - qw.eq("account_id", userHolder.getId()); - Affiliate affiliate = baseMapper.selectOne(qw); - if (Objects.isNull(affiliate)){ - affiliate = new Affiliate(); - affiliate.setAccountId(userHolder.getId()); - affiliate.setStatus("Pending"); - affiliate.setCreateTime(LocalDateTime.now()); - affiliate.setPromotionMethod(promotionMethod); - baseMapper.insert(affiliate); - // 邮件通知审批者 - String merchantEmail = "kimwong@code-create.com.hk"; - String developer = "xupei3360@163.com"; - String[] receiverEmail = {merchantEmail, developer}; - SendEmailUtil.affiliateEmailReminder(receiverEmail, new AffiliateEmailParamsDTO(userHolder.getUsername(), promotionMethod), "new"); - }else { - throw new BusinessException("You have registered an Affiliate", ResultEnum.PROMPT.getCode()); - } - return true; - } - - public PageBaseResponse getAffiliateList(AffiliateQueryDTO affiliateQueryDTO){ - log.info("parameter => {}", affiliateQueryDTO.toString()); - - int offset = (affiliateQueryDTO.getPage() - 1) * affiliateQueryDTO.getSize(); - List affiliateList = baseMapper.getAffiliateList(affiliateQueryDTO.getStatus(), - affiliateQueryDTO.getStartTime(), - affiliateQueryDTO.getEndTime(), - affiliateQueryDTO.getOrder(), - affiliateQueryDTO.getAffiliateId(), - affiliateQueryDTO.getSize(), - offset - ); - if (CollectionUtils.isEmpty(affiliateList)) { - return PageBaseResponse.success(new Page<>()); - }else { - int totalCount = baseMapper.queryAffiliateTotalCount(affiliateQueryDTO.getStatus(), - affiliateQueryDTO.getStartTime(), - affiliateQueryDTO.getEndTime(), - affiliateQueryDTO.getAffiliateId() - ); - IPage orderListVOIPage = new Page<>(); - Integer size = affiliateQueryDTO.getSize(); - orderListVOIPage.setSize(size); - orderListVOIPage.setRecords(affiliateList); - orderListVOIPage.setCurrent(affiliateQueryDTO.getPage()); - orderListVOIPage.setPages((long)Math.ceil((double) totalCount / size)); - orderListVOIPage.setTotal(totalCount); - return PageBaseResponse.success(orderListVOIPage); - } - /*QueryWrapper qw = new QueryWrapper<>(); - qw.eq(!StringUtils.isNullOrEmpty(affiliateQueryDTO.getStatus()), "status", affiliateQueryDTO.getStatus()) - .gt(!StringUtils.isNullOrEmpty(affiliateQueryDTO.getStartTime()), "create_time", affiliateQueryDTO.getStartTime()) - .lt(!StringUtils.isNullOrEmpty(affiliateQueryDTO.getEndTime()), "create_time", affiliateQueryDTO.getEndTime()) - .eq(!Objects.isNull(affiliateQueryDTO.getAffiliateId()), "id", affiliateQueryDTO.getAffiliateId()) - .orderByDesc(affiliateQueryDTO.getOrder().equals("DESC"), "create_time"); - Page affiliatePage = baseMapper.selectPage(new Page<>(affiliateQueryDTO.getPage(), affiliateQueryDTO.getSize()), qw); - affiliatePage.convert((Function) affiliate-> { - AffiliateVO affiliateVO = CopyUtil.copyObject(affiliate, AffiliateVO.class); - affiliateVO.setUsername(); - }); - return affiliatePage;*/ - } - - public AffiliateVO personalAffiliateCenter(){ - QueryWrapper qw = new QueryWrapper<>(); - Long accountId = UserContext.getUserHolder().getId(); - qw.eq("account_id", accountId); - Affiliate affiliate = baseMapper.selectOne(qw); - AffiliateVO affiliateVO = CopyUtil.copyObject(affiliate, AffiliateVO.class); - affiliateVO.setLinkViewCount(getAffiliateLinkViewCount(affiliate.getId())); - return affiliateVO; - } - - public double[] getPersonalMonthlyIncome(int year){ - Long accountId = UserContext.getUserHolder().getId(); - List> personalMonthlyIncome = affiliateIncomeMapper.getPersonalMonthlyIncome(accountId, year); - double[] commissions = new double[12]; - personalMonthlyIncome.forEach(income -> { - int month = Integer.parseInt(income.get("yearMonth").toString()); - commissions[month-1] = (double)income.get("totalCommission"); - }); - - return commissions; - } - - // 审批申请 - public Boolean applicationApproval(Long id, Boolean isApproved, Float commission){ - Affiliate affiliate = baseMapper.selectById(id); - - // 1、更新db状态 - if (isApproved){ - // 更新状态 - affiliate.setStatus("Active"); - affiliate.setApproved(true); - affiliate.setLink(CommonConstant.AFFILIATE_LINK + affiliate.getId()); - if (Objects.isNull(commission)) { - // 未设置佣金比例的情况下,默认25% - affiliate.setCommissionPercent(25f); - } else { - affiliate.setCommissionPercent(commission); - } - } else { - affiliate.setStatus("Refused"); - affiliate.setApproved(false); - } - affiliate.setUpdateTime(LocalDateTime.now()); - baseMapper.updateById(affiliate); - - // 2、将批准结果邮件通知用户 - Account account = accountService.getById(affiliate.getAccountId()); - String[] userEmail = {account.getUserEmail()}; - String userName = account.getUserName(); - if (isApproved){ - SendEmailUtil.affiliateEmailReminder(userEmail, new AffiliateEmailParamsDTO(userName), "accepted"); - }else { - SendEmailUtil.affiliateEmailReminder(userEmail, new AffiliateEmailParamsDTO(userName), "refused"); - } - return true; - } - - public void updateCommissionPercentage(Long id, Float commission){ - Affiliate affiliate = baseMapper.selectById(id); - if (Objects.isNull(affiliate)){ - log.info("未知affiliate id :{}", id); - throw new BusinessException("unknown affiliate"); - } - if (!Objects.equals(affiliate.getCommissionPercent(), commission)){ - affiliate.setCommissionPercent(commission); - affiliate.setUpdateTime(LocalDateTime.now()); - baseMapper.updateById(affiliate); - } - } - - // 定时计算佣金 - public void updateAffiliateInfoWithPayment(){ - // id存redis - String lastTime = redisUtil.getFromString(RedisUtil.PAYMENT_INFO_LAST_SCAN_TIME); - String currentTime = LocalDateTime.now().toString(); - // 1、查上次更新之后有无新订单 - QueryWrapper queryWrapper = new QueryWrapper<>(); - if (!StringUtil.isNullOrEmpty(lastTime)){ - queryWrapper.gt("create_time", lastTime); - } - queryWrapper.eq("type","new").eq("trade_state", "paid"); - - List paymentInfos = paymentInfoService.getBaseMapper().selectList(queryWrapper); - if (!paymentInfos.isEmpty()){ - paymentInfos.forEach(paymentInfo -> { - // 2、根据order_no查付款用户id - OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(paymentInfo.getOrderNo()); - if (Objects.isNull(orderInfo)){ - return; - } - Long accountId = orderInfo.getAccountId(); - // 3、查该用户之前是否有初次订阅的订单 - QueryWrapper qwOrderInfo = new QueryWrapper<>(); - qwOrderInfo.eq("account_id", accountId).eq("is_first_subscription", 1); - List orderInfos = orderInfoService.getBaseMapper().selectList(qwOrderInfo); - // 该用户首次订阅(非首次订阅,不分配佣金) - if (orderInfos.isEmpty()){ - // 查询是否绑定affiliateId - Account account = accountService.getById(accountId); - if (!Objects.isNull(account.getInvitationCode())){ - log.info("结算订单id为{}的佣金", orderInfo.getId()); - // 3、若有, 直接更新affiliate的所得 - Affiliate affiliate = baseMapper.selectById(account.getInvitationCode()); - Float payerTotal = paymentInfo.getPayerTotal(); - - if (payerTotal > 0){ - // 分配新用户首次订阅所付费用 预设的佣金比例 作为佣金 - BigDecimal commission = BigDecimal.valueOf(payerTotal).multiply(BigDecimal.valueOf(affiliate.getCommissionPercent() / 100)); - BigDecimal monthlyEarning = BigDecimal.valueOf(affiliate.getMonthlyEarnings()).add(commission); - BigDecimal unpaidEarnings = BigDecimal.valueOf(affiliate.getUnpaidEarnings()).add(commission); - int visits = affiliate.getVisits() + 1; - affiliate.setMonthlyEarnings(monthlyEarning.floatValue()); - affiliate.setUnpaidEarnings(unpaidEarnings.floatValue()); - affiliate.setVisits(visits); - affiliate.setUpdateTime(LocalDateTime.now()); - baseMapper.updateById(affiliate); - - orderInfo.setIsCommissionCalculated((byte)1); - - // 4、添加到t_affiliate_income - AffiliateIncome affiliateIncome = new AffiliateIncome(); - affiliateIncome.setAffiliateId(affiliate.getId()); - affiliateIncome.setAffiliateAccountId(affiliate.getAccountId()); - affiliateIncome.setInviteeAccountId(accountId); - affiliateIncome.setAmount(payerTotal); - affiliateIncome.setPaymentInfoId(paymentInfo.getId()); - affiliateIncome.setPaymentTime(paymentInfo.getCreateTime()); - affiliateIncome.setCommission(commission.floatValue()); - affiliateIncome.setCreateTime(LocalDateTime.now()); - affiliateIncomeMapper.insert(affiliateIncome); - } - } - orderInfo.setIsFirstSubscription((byte)1); - orderInfo.setUpdateTime(LocalDateTime.now()); - orderInfoService.updateById(orderInfo); - } - }); - } - redisUtil.addToString(RedisUtil.PAYMENT_INFO_LAST_SCAN_TIME, currentTime); - } - - public Boolean affiliateLinkViewsIncrease(Long affiliateId) { - redisUtil.increaseAffiliateLinkViewCount(affiliateId); - return Boolean.TRUE; - } - - private Long getAffiliateLinkViewCount(Long affiliateId) { - return redisUtil.getAffiliateLinkViewCount(affiliateId); - } - - // 查看每个affiliate带来收入的详情 - @Override - public IPage getEachAffiliateGeneratedRevenue(AffiliateQueryDTO affiliateQueryDTO) { - if (Objects.isNull(affiliateQueryDTO.getAffiliateId())){ - throw new BusinessException("Please specify the affiliate ID.", ResultEnum.PROMPT.getCode()); - } - - QueryWrapper affiliateIncomeQueryWrapper = new QueryWrapper<>(); - affiliateIncomeQueryWrapper - .gt(!StringUtils.isNullOrEmpty(affiliateQueryDTO.getStartTime()), "create_time", affiliateQueryDTO.getStartTime()) - .lt(!StringUtils.isNullOrEmpty(affiliateQueryDTO.getEndTime()), "create_time", affiliateQueryDTO.getEndTime()) - .eq(!Objects.isNull(affiliateQueryDTO.getAffiliateId()), "affiliate_id", affiliateQueryDTO.getAffiliateId()) - .orderByDesc(affiliateQueryDTO.getOrder().equals("DESC"), "create_time"); - IPage affiliateIncomePage = affiliateIncomeMapper.selectPage(new Page<>(affiliateQueryDTO.getPage(), affiliateQueryDTO.getSize()), affiliateIncomeQueryWrapper); - return affiliateIncomePage.convert((Function) affiliateIncome -> { - AffiliateInvitationDetailsVO affiliateInvitationDetailsVO = CopyUtil.copyObject(affiliateIncome, AffiliateInvitationDetailsVO.class); - affiliateInvitationDetailsVO.setAccountId(affiliateIncome.getInviteeAccountId()); - affiliateInvitationDetailsVO.setUsername(accountService.getBaseMapper().selectById(affiliateIncome.getInviteeAccountId()).getUserName()); - affiliateInvitationDetailsVO.setFirstSubscriptionPaymentAmount(affiliateIncome.getAmount()); - affiliateInvitationDetailsVO.setCommission(affiliateIncome.getCommission()); - affiliateInvitationDetailsVO.setTime(affiliateIncome.getPaymentTime()); - return affiliateInvitationDetailsVO; - }); - } - - public void commissionCalculation(Integer year, Integer month) { - if (Objects.isNull(year)) { - year = LocalDateTime.now().getYear(); - } - if (Objects.isNull(month)) { - month = LocalDateTime.now().getMonthValue(); - } - - List> monthlyAffiliateIncome = affiliateIncomeMapper.getMonthlyAffiliateIncome(year, month); - // 1、总收入(近一个月通过affiliate产生的收入),未支付的金额 affiliate表中unpaid的总和 - Double totalAmount = 0.0; - Double totalCommission = 0.0; - if (!monthlyAffiliateIncome.isEmpty()){ - Map monthlyIncome = monthlyAffiliateIncome.get(0); - totalAmount = (Double) monthlyIncome.get("totalAmount"); - totalCommission = (Double) monthlyIncome.get("totalCommission"); - } - - // 2、本月新注册的Affiliate - Map monthlyApprovedAffiliate = baseMapper.getMonthlyApprovedAffiliate(year, month); - Long count = monthlyApprovedAffiliate.get("count"); - - AffiliateEmailParamsDTO affiliateEmailParamsDTO = new AffiliateEmailParamsDTO(); - affiliateEmailParamsDTO.setTotalProgramRevenue(totalAmount.toString()); - affiliateEmailParamsDTO.setNewApprovedAffiliates(count.toString()); - affiliateEmailParamsDTO.setUnpaidEarnings(totalCommission.toString()); - affiliateEmailParamsDTO.setPaidEarnings("0"); - - String merchantEmail = "kimwong@code-create.com.hk"; - String developer = "xupei3360@163.com"; - String[] receiverEmail = {merchantEmail, developer}; - // 邮件通知 - SendEmailUtil.affiliateEmailReminder(receiverEmail, affiliateEmailParamsDTO, "summary"); - } - - @Override - public Affiliate getByAccountId(Long accountId) { - QueryWrapper queryWrapper = new QueryWrapper<>(); - queryWrapper.eq("account_id", accountId).orderByDesc("id").last("limit 1"); - - return baseMapper.selectOne(queryWrapper); - } - - public void calcCouponsCommission(){ - // id存redis - String lastTime = redisUtil.getFromString(RedisUtil.PAYMENT_INFO_LAST_SCAN_TIME); - String currentTime = LocalDateTime.now().toString(); - // 1、查上次更新之后有无使用了优惠券的新订单 - QueryWrapper queryWrapper = new QueryWrapper<>(); - if (!StringUtil.isNullOrEmpty(lastTime)){ - queryWrapper.gt("create_time", lastTime) - .lt("create_time", currentTime) - .isNotNull("promotion_code"); - } - List paymentInfos = paymentInfoService.getBaseMapper().selectList(queryWrapper); - - // key:推广码, value:用户支付的金额 - HashMap codeAmount = new HashMap<>(); - if (!paymentInfos.isEmpty()){ - for (PaymentInfo paymentInfo : paymentInfos){ - String promotionCode = paymentInfo.getPromotionCode(); - Float sum = codeAmount.get(promotionCode); - if (sum == null || sum == 0.0f){ - codeAmount.put(promotionCode, paymentInfo.getPayerTotal()); - }else { - codeAmount.put(promotionCode, sum + paymentInfo.getPayerTotal()); - } - } - for (Map.Entry entry : codeAmount.entrySet()){ - String promotionCode = entry.getKey(); - ProductCoupons productCoupons = stripeService.getProductCoupon(promotionCode, null); - if (!Objects.isNull(productCoupons)){ - // 2、计算支付金额的总和,更新totalEarnings,commission,unpaidCommission - float sum = productCoupons.getTotalEarnings() + entry.getValue(); - productCoupons.setTotalEarnings(sum); - float commission = sum * productCoupons.getCommissionRate() / 100; - productCoupons.setCommission(commission); - productCoupons.setUnpaidCommission(commission - productCoupons.getPaidCommission()); - productCouponsMapper.updateById(productCoupons); - } - } - } - redisUtil.addToString(RedisUtil.PAYMENT_INFO_LAST_SCAN_TIME, currentTime); - } - - -} +package com.ai.da.service.impl; + +import com.ai.da.common.config.exception.BusinessException; +import com.ai.da.common.constant.CommonConstant; +import com.ai.da.common.context.UserContext; +import com.ai.da.common.response.PageBaseResponse; +import com.ai.da.common.response.ResultEnum; +import com.ai.da.common.utils.CopyUtil; +import com.ai.da.common.utils.RedisUtil; +import com.ai.da.common.utils.SendEmailUtil; +import com.ai.da.mapper.primary.AffiliateIncomeMapper; +import com.ai.da.mapper.primary.AffiliateMapper; +import com.ai.da.mapper.primary.ProductCouponsMapper; +import com.ai.da.mapper.primary.SubscriptionInfoMapper; +import com.ai.da.mapper.primary.entity.*; +import com.ai.da.model.dto.AffiliateEmailParamsDTO; +import com.ai.da.model.dto.AffiliateQueryDTO; +import com.ai.da.model.vo.AffiliateInvitationDetailsVO; +import com.ai.da.model.vo.AffiliateVO; +import com.ai.da.model.vo.AuthPrincipalVo; +import com.ai.da.service.*; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.mysql.cj.util.StringUtils; +import io.netty.util.internal.StringUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.*; +import java.util.function.Function; + +@Service +@Slf4j +public class AffiliateServiceImpl extends ServiceImpl implements AffiliateService { + + @Resource + private OrderInfoService orderInfoService; + @Resource + private AccountService accountService; + @Resource + private PaymentInfoService paymentInfoService; + @Resource + private SubscriptionInfoMapper subscriptionInfoMapper; + @Resource + private AffiliateIncomeMapper affiliateIncomeMapper; + @Resource + private StripeService stripeService; + @Resource + private ProductCouponsMapper productCouponsMapper; + @Resource + private RedisUtil redisUtil; + @Resource + private EmailService emailService; + + // 推广者注册 + public Boolean registerAsAnAffiliate(String promotionMethod){ + AuthPrincipalVo userHolder = UserContext.getUserHolder(); + // 判断该用户是否已注册 + QueryWrapper qw = new QueryWrapper<>(); + qw.eq("account_id", userHolder.getId()); + Affiliate affiliate = baseMapper.selectOne(qw); + if (Objects.isNull(affiliate)){ + affiliate = new Affiliate(); + affiliate.setAccountId(userHolder.getId()); + affiliate.setStatus("Pending"); + affiliate.setCreateTime(LocalDateTime.now()); + affiliate.setPromotionMethod(promotionMethod); + baseMapper.insert(affiliate); + // 邮件通知审批者 + String merchantEmail = "kimwong@code-create.com.hk"; + String developer = "xupei3360@163.com"; + String[] receiverEmail = {merchantEmail, developer}; + SendEmailUtil.affiliateEmailReminder(receiverEmail, new AffiliateEmailParamsDTO(userHolder.getUsername(), promotionMethod), "new"); +// emailService.affiliateEmailReminder(Arrays.asList(/*merchantEmail,*/ developer), new AffiliateEmailParamsDTO(userHolder.getUsername(), promotionMethod), "new"); + }else { + throw new BusinessException("You have registered an Affiliate", ResultEnum.PROMPT.getCode()); + } + return true; + } + + public PageBaseResponse getAffiliateList(AffiliateQueryDTO affiliateQueryDTO){ + log.info("parameter => {}", affiliateQueryDTO.toString()); + + int offset = (affiliateQueryDTO.getPage() - 1) * affiliateQueryDTO.getSize(); + List affiliateList = baseMapper.getAffiliateList(affiliateQueryDTO.getStatus(), + affiliateQueryDTO.getStartTime(), + affiliateQueryDTO.getEndTime(), + affiliateQueryDTO.getOrder(), + affiliateQueryDTO.getAffiliateId(), + affiliateQueryDTO.getSize(), + offset + ); + if (CollectionUtils.isEmpty(affiliateList)) { + return PageBaseResponse.success(new Page<>()); + }else { + int totalCount = baseMapper.queryAffiliateTotalCount(affiliateQueryDTO.getStatus(), + affiliateQueryDTO.getStartTime(), + affiliateQueryDTO.getEndTime(), + affiliateQueryDTO.getAffiliateId() + ); + IPage orderListVOIPage = new Page<>(); + Integer size = affiliateQueryDTO.getSize(); + orderListVOIPage.setSize(size); + orderListVOIPage.setRecords(affiliateList); + orderListVOIPage.setCurrent(affiliateQueryDTO.getPage()); + orderListVOIPage.setPages((long)Math.ceil((double) totalCount / size)); + orderListVOIPage.setTotal(totalCount); + return PageBaseResponse.success(orderListVOIPage); + } + /*QueryWrapper qw = new QueryWrapper<>(); + qw.eq(!StringUtils.isNullOrEmpty(affiliateQueryDTO.getStatus()), "status", affiliateQueryDTO.getStatus()) + .gt(!StringUtils.isNullOrEmpty(affiliateQueryDTO.getStartTime()), "create_time", affiliateQueryDTO.getStartTime()) + .lt(!StringUtils.isNullOrEmpty(affiliateQueryDTO.getEndTime()), "create_time", affiliateQueryDTO.getEndTime()) + .eq(!Objects.isNull(affiliateQueryDTO.getAffiliateId()), "id", affiliateQueryDTO.getAffiliateId()) + .orderByDesc(affiliateQueryDTO.getOrder().equals("DESC"), "create_time"); + Page affiliatePage = baseMapper.selectPage(new Page<>(affiliateQueryDTO.getPage(), affiliateQueryDTO.getSize()), qw); + affiliatePage.convert((Function) affiliate-> { + AffiliateVO affiliateVO = CopyUtil.copyObject(affiliate, AffiliateVO.class); + affiliateVO.setUsername(); + }); + return affiliatePage;*/ + } + + public AffiliateVO personalAffiliateCenter(){ + QueryWrapper qw = new QueryWrapper<>(); + Long accountId = UserContext.getUserHolder().getId(); + qw.eq("account_id", accountId); + Affiliate affiliate = baseMapper.selectOne(qw); + AffiliateVO affiliateVO = CopyUtil.copyObject(affiliate, AffiliateVO.class); + affiliateVO.setLinkViewCount(getAffiliateLinkViewCount(affiliate.getId())); + return affiliateVO; + } + + public double[] getPersonalMonthlyIncome(int year){ + Long accountId = UserContext.getUserHolder().getId(); + List> personalMonthlyIncome = affiliateIncomeMapper.getPersonalMonthlyIncome(accountId, year); + double[] commissions = new double[12]; + personalMonthlyIncome.forEach(income -> { + int month = Integer.parseInt(income.get("yearMonth").toString()); + commissions[month-1] = (double)income.get("totalCommission"); + }); + + return commissions; + } + + // 审批申请 + public Boolean applicationApproval(Long id, Boolean isApproved, Float commission){ + Affiliate affiliate = baseMapper.selectById(id); + + // 1、更新db状态 + if (isApproved){ + // 更新状态 + affiliate.setStatus("Active"); + affiliate.setApproved(true); + affiliate.setLink(CommonConstant.AFFILIATE_LINK + affiliate.getId()); + if (Objects.isNull(commission)) { + // 未设置佣金比例的情况下,默认25% + affiliate.setCommissionPercent(25f); + } else { + affiliate.setCommissionPercent(commission); + } + } else { + affiliate.setStatus("Refused"); + affiliate.setApproved(false); + } + affiliate.setUpdateTime(LocalDateTime.now()); + baseMapper.updateById(affiliate); + + // 2、将批准结果邮件通知用户 + Account account = accountService.getById(affiliate.getAccountId()); + String[] userEmail = {account.getUserEmail()}; + String userName = account.getUserName(); + if (isApproved){ + SendEmailUtil.affiliateEmailReminder(userEmail, new AffiliateEmailParamsDTO(userName), "accepted"); +// emailService.affiliateEmailReminder(Collections.singletonList(account.getUserEmail()), new AffiliateEmailParamsDTO(userName), "accepted"); + }else { + SendEmailUtil.affiliateEmailReminder(userEmail, new AffiliateEmailParamsDTO(userName), "refused"); +// emailService.affiliateEmailReminder(Collections.singletonList(account.getUserEmail()), new AffiliateEmailParamsDTO(userName), "refused"); + } + return true; + } + + public void updateCommissionPercentage(Long id, Float commission){ + Affiliate affiliate = baseMapper.selectById(id); + if (Objects.isNull(affiliate)){ + log.info("未知affiliate id :{}", id); + throw new BusinessException("unknown affiliate"); + } + if (!Objects.equals(affiliate.getCommissionPercent(), commission)){ + affiliate.setCommissionPercent(commission); + affiliate.setUpdateTime(LocalDateTime.now()); + baseMapper.updateById(affiliate); + } + } + + // 定时计算佣金 + public void updateAffiliateInfoWithPayment(){ + // id存redis + String lastTime = redisUtil.getFromString(RedisUtil.PAYMENT_INFO_LAST_SCAN_TIME); + String currentTime = LocalDateTime.now().toString(); + // 1、查上次更新之后有无新订单 + QueryWrapper queryWrapper = new QueryWrapper<>(); + if (!StringUtil.isNullOrEmpty(lastTime)){ + queryWrapper.gt("create_time", lastTime); + } + queryWrapper.eq("type","new").eq("trade_state", "paid"); + + List paymentInfos = paymentInfoService.getBaseMapper().selectList(queryWrapper); + if (!paymentInfos.isEmpty()){ + paymentInfos.forEach(paymentInfo -> { + // 2、根据order_no查付款用户id + OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(paymentInfo.getOrderNo()); + if (Objects.isNull(orderInfo)){ + return; + } + Long accountId = orderInfo.getAccountId(); + // 3、查该用户之前是否有初次订阅的订单 + QueryWrapper qwOrderInfo = new QueryWrapper<>(); + qwOrderInfo.eq("account_id", accountId).eq("is_first_subscription", 1); + List orderInfos = orderInfoService.getBaseMapper().selectList(qwOrderInfo); + // 该用户首次订阅(非首次订阅,不分配佣金) + if (orderInfos.isEmpty()){ + // 查询是否绑定affiliateId + Account account = accountService.getById(accountId); + if (!Objects.isNull(account.getInvitationCode())){ + log.info("结算订单id为{}的佣金", orderInfo.getId()); + // 3、若有, 直接更新affiliate的所得 + Affiliate affiliate = baseMapper.selectById(account.getInvitationCode()); + Float payerTotal = paymentInfo.getPayerTotal(); + + if (payerTotal > 0){ + // 分配新用户首次订阅所付费用 预设的佣金比例 作为佣金 + BigDecimal commission = BigDecimal.valueOf(payerTotal).multiply(BigDecimal.valueOf(affiliate.getCommissionPercent() / 100)); + BigDecimal monthlyEarning = BigDecimal.valueOf(affiliate.getMonthlyEarnings()).add(commission); + BigDecimal unpaidEarnings = BigDecimal.valueOf(affiliate.getUnpaidEarnings()).add(commission); + int visits = affiliate.getVisits() + 1; + affiliate.setMonthlyEarnings(monthlyEarning.floatValue()); + affiliate.setUnpaidEarnings(unpaidEarnings.floatValue()); + affiliate.setVisits(visits); + affiliate.setUpdateTime(LocalDateTime.now()); + baseMapper.updateById(affiliate); + + orderInfo.setIsCommissionCalculated((byte)1); + + // 4、添加到t_affiliate_income + AffiliateIncome affiliateIncome = new AffiliateIncome(); + affiliateIncome.setAffiliateId(affiliate.getId()); + affiliateIncome.setAffiliateAccountId(affiliate.getAccountId()); + affiliateIncome.setInviteeAccountId(accountId); + affiliateIncome.setAmount(payerTotal); + affiliateIncome.setPaymentInfoId(paymentInfo.getId()); + affiliateIncome.setPaymentTime(paymentInfo.getCreateTime()); + affiliateIncome.setCommission(commission.floatValue()); + affiliateIncome.setCreateTime(LocalDateTime.now()); + affiliateIncomeMapper.insert(affiliateIncome); + } + } + orderInfo.setIsFirstSubscription((byte)1); + orderInfo.setUpdateTime(LocalDateTime.now()); + orderInfoService.updateById(orderInfo); + } + }); + } + redisUtil.addToString(RedisUtil.PAYMENT_INFO_LAST_SCAN_TIME, currentTime); + } + + public Boolean affiliateLinkViewsIncrease(Long affiliateId) { + redisUtil.increaseAffiliateLinkViewCount(affiliateId); + return Boolean.TRUE; + } + + private Long getAffiliateLinkViewCount(Long affiliateId) { + return redisUtil.getAffiliateLinkViewCount(affiliateId); + } + + // 查看每个affiliate带来收入的详情 + @Override + public IPage getEachAffiliateGeneratedRevenue(AffiliateQueryDTO affiliateQueryDTO) { + if (Objects.isNull(affiliateQueryDTO.getAffiliateId())){ + throw new BusinessException("Please specify the affiliate ID.", ResultEnum.PROMPT.getCode()); + } + + QueryWrapper affiliateIncomeQueryWrapper = new QueryWrapper<>(); + affiliateIncomeQueryWrapper + .gt(!StringUtils.isNullOrEmpty(affiliateQueryDTO.getStartTime()), "create_time", affiliateQueryDTO.getStartTime()) + .lt(!StringUtils.isNullOrEmpty(affiliateQueryDTO.getEndTime()), "create_time", affiliateQueryDTO.getEndTime()) + .eq(!Objects.isNull(affiliateQueryDTO.getAffiliateId()), "affiliate_id", affiliateQueryDTO.getAffiliateId()) + .orderByDesc(affiliateQueryDTO.getOrder().equals("DESC"), "create_time"); + IPage affiliateIncomePage = affiliateIncomeMapper.selectPage(new Page<>(affiliateQueryDTO.getPage(), affiliateQueryDTO.getSize()), affiliateIncomeQueryWrapper); + return affiliateIncomePage.convert((Function) affiliateIncome -> { + AffiliateInvitationDetailsVO affiliateInvitationDetailsVO = CopyUtil.copyObject(affiliateIncome, AffiliateInvitationDetailsVO.class); + affiliateInvitationDetailsVO.setAccountId(affiliateIncome.getInviteeAccountId()); + affiliateInvitationDetailsVO.setUsername(accountService.getBaseMapper().selectById(affiliateIncome.getInviteeAccountId()).getUserName()); + affiliateInvitationDetailsVO.setFirstSubscriptionPaymentAmount(affiliateIncome.getAmount()); + affiliateInvitationDetailsVO.setCommission(affiliateIncome.getCommission()); + affiliateInvitationDetailsVO.setTime(affiliateIncome.getPaymentTime()); + return affiliateInvitationDetailsVO; + }); + } + + public void commissionCalculation(Integer year, Integer month) { + if (Objects.isNull(year)) { + year = LocalDateTime.now().getYear(); + } + if (Objects.isNull(month)) { + month = LocalDateTime.now().getMonthValue(); + } + + List> monthlyAffiliateIncome = affiliateIncomeMapper.getMonthlyAffiliateIncome(year, month); + // 1、总收入(近一个月通过affiliate产生的收入),未支付的金额 affiliate表中unpaid的总和 + Double totalAmount = 0.0; + Double totalCommission = 0.0; + if (!monthlyAffiliateIncome.isEmpty()){ + Map monthlyIncome = monthlyAffiliateIncome.get(0); + totalAmount = (Double) monthlyIncome.get("totalAmount"); + totalCommission = (Double) monthlyIncome.get("totalCommission"); + } + + // 2、本月新注册的Affiliate + Map monthlyApprovedAffiliate = baseMapper.getMonthlyApprovedAffiliate(year, month); + Long count = monthlyApprovedAffiliate.get("count"); + + AffiliateEmailParamsDTO affiliateEmailParamsDTO = new AffiliateEmailParamsDTO(); + affiliateEmailParamsDTO.setTotalProgramRevenue(totalAmount.toString()); + affiliateEmailParamsDTO.setNewApprovedAffiliates(count.toString()); + affiliateEmailParamsDTO.setUnpaidEarnings(totalCommission.toString()); + affiliateEmailParamsDTO.setPaidEarnings("0"); + + String merchantEmail = "kimwong@code-create.com.hk"; + String developer = "xupei3360@163.com"; + String[] receiverEmail = {merchantEmail, developer}; + // 邮件通知 + SendEmailUtil.affiliateEmailReminder(receiverEmail, affiliateEmailParamsDTO, "summary"); +// emailService.affiliateEmailReminder(Arrays.asList(/*merchantEmail,*/ developer), affiliateEmailParamsDTO, "summary"); + } + + @Override + public Affiliate getByAccountId(Long accountId) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("account_id", accountId).orderByDesc("id").last("limit 1"); + + return baseMapper.selectOne(queryWrapper); + } + + public void calcCouponsCommission(){ + // id存redis + String lastTime = redisUtil.getFromString(RedisUtil.PAYMENT_INFO_LAST_SCAN_TIME); + String currentTime = LocalDateTime.now().toString(); + // 1、查上次更新之后有无使用了优惠券的新订单 + QueryWrapper queryWrapper = new QueryWrapper<>(); + if (!StringUtil.isNullOrEmpty(lastTime)){ + queryWrapper.gt("create_time", lastTime) + .lt("create_time", currentTime) + .isNotNull("promotion_code"); + } + List paymentInfos = paymentInfoService.getBaseMapper().selectList(queryWrapper); + + // key:推广码, value:用户支付的金额 + HashMap codeAmount = new HashMap<>(); + if (!paymentInfos.isEmpty()){ + for (PaymentInfo paymentInfo : paymentInfos){ + String promotionCode = paymentInfo.getPromotionCode(); + Float sum = codeAmount.get(promotionCode); + if (sum == null || sum == 0.0f){ + codeAmount.put(promotionCode, paymentInfo.getPayerTotal()); + }else { + codeAmount.put(promotionCode, sum + paymentInfo.getPayerTotal()); + } + } + for (Map.Entry entry : codeAmount.entrySet()){ + String promotionCode = entry.getKey(); + ProductCoupons productCoupons = stripeService.getProductCoupon(promotionCode, null); + if (!Objects.isNull(productCoupons)){ + // 2、计算支付金额的总和,更新totalEarnings,commission,unpaidCommission + float sum = productCoupons.getTotalEarnings() + entry.getValue(); + productCoupons.setTotalEarnings(sum); + float commission = sum * productCoupons.getCommissionRate() / 100; + productCoupons.setCommission(commission); + productCoupons.setUnpaidCommission(commission - productCoupons.getPaidCommission()); + productCouponsMapper.updateById(productCoupons); + } + } + } + redisUtil.addToString(RedisUtil.PAYMENT_INFO_LAST_SCAN_TIME, currentTime); + } + + +} diff --git a/src/main/java/com/ai/da/service/impl/CollectionElementServiceImpl.java b/src/main/java/com/ai/da/service/impl/CollectionElementServiceImpl.java index 23e6235e..be628868 100644 --- a/src/main/java/com/ai/da/service/impl/CollectionElementServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/CollectionElementServiceImpl.java @@ -1,1125 +1,1128 @@ -package com.ai.da.service.impl; - -import cn.hutool.core.collection.CollectionUtil; -import com.ai.da.common.config.FileProperties; -import com.ai.da.common.config.exception.BusinessException; -import com.ai.da.common.constant.CommonConstant; -import com.ai.da.common.context.UserContext; -import com.ai.da.common.enums.*; -import com.ai.da.common.response.ResultEnum; -import com.ai.da.common.utils.*; -import com.ai.da.mapper.primary.CollectionElementMapper; -import com.ai.da.mapper.primary.GenerateDetailMapper; -import com.ai.da.mapper.primary.GenerateMapper; -import com.ai.da.mapper.primary.entity.*; -import com.ai.da.model.dto.*; -import com.ai.da.model.enums.ModelType; -import com.ai.da.model.enums.Module; -import com.ai.da.model.enums.Sex; -import com.ai.da.model.vo.*; -import com.ai.da.python.PythonService; -import com.ai.da.python.vo.DesignPythonItem; -import com.ai.da.python.vo.ImageSegmentation; -import com.ai.da.service.*; -import com.alibaba.fastjson.JSON; -import com.alibaba.fastjson.JSONObject; -import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; -import com.baomidou.mybatisplus.core.metadata.IPage; -import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.google.common.collect.Lists; -import com.google.gson.Gson; -import io.netty.util.internal.StringUtil; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.BeanUtils; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.CollectionUtils; -import org.springframework.util.StringUtils; -import org.springframework.web.multipart.MultipartFile; - -import javax.annotation.Resource; -import java.io.File; -import java.math.BigDecimal; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.util.*; -import java.util.stream.Collectors; - -/** - * 服务实现类 - * - * @author yanglei - * @since 2022-09-30 - */ -@Slf4j -@Service -public class CollectionElementServiceImpl extends ServiceImpl implements CollectionElementService { - @Resource - private CollectionElementMapper collectionElementMapper; - @Resource - private FileProperties fileProperties; - @Resource - private PanToneService panToneService; - @Resource - private PythonService pythonService; - @Resource - private LibraryService libraryService; - @Resource - private SysFileService sysFileService; - @Resource - private GenerateMapper generateMapper; - @Resource - private GenerateDetailMapper generateDetailMapper; - @Resource - private LibraryModelPointService libraryModelPointService; - @Resource - private TCollectionElementRelationService tCollectionElementRelationService; - @Resource - private MinioUtil minioUtil; - - @Value("${minio.bucketName.collectionElement}") - private String collectionElement; - @Value("${minio.bucketName.gradient}") - private String gradientBucketName; - @Value("${minio.bucketName.users}") - private String userBucketName; - @Resource - private RedisUtil redisUtil; - -// @Resource -// private RedisUtil redisUtil; - - @Transactional(rollbackFor = Exception.class) - @Override - public CollectionElementVO upload(CollectionElementUploadDTO uploadDTO) { - long start = System.currentTimeMillis(); - //用户信息 - AuthPrincipalVo userInfo = UserContext.getUserHolder(); - CollectionLevel1TypeEnum level1TypeEnum = CollectionLevel1TypeEnum.uploadOf(uploadDTO.getLevel1Type()); - String name; - if (Objects.isNull(level1TypeEnum) && !uploadDTO.getLevel1Type().equals(Module.deReconstruction.getValue())) { - throw new BusinessException("unknown.parameter.level1Type"); - } else if (uploadDTO.getLevel1Type().equals(Module.deReconstruction.getValue())){ - name = "Construction"; - } else { - name = level1TypeEnum.getRealName(); - } - String objectName = userInfo.getId() + "/" + name; - String path = minioUtil.upload(collectionElement, objectName, uploadDTO.getFile()); - - String level2Type = null; - if (uploadDTO.getLevel1Type().equals(CollectionLevel1TypeEnum.SKETCH_BOARD.getRealName())){ - if (StringUtil.isNullOrEmpty(uploadDTO.getGender())) { - throw new BusinessException("gender.cannot.be.empty"); - } - level2Type = pythonService.getClothCategory(path,uploadDTO.getGender()); - }else if (!StringUtil.isNullOrEmpty(uploadDTO.getLevel2Type())){ - level2Type = uploadDTO.getLevel2Type(); - } - - //保存element元素 - CollectionElement collectionElement = resolveData(uploadDTO, userInfo, path, level2Type); - saveOne(collectionElement); -// if (!StringUtils.isEmpty(uploadDTO.getMoodboardPosition())) { -// redisUtil.saveMoodboardPosition(collectionElement.getId(), uploadDTO.getMoodboardPosition()); -// } - CollectionElementVO collectionElementVO = CopyUtil.copyObject(collectionElement, CollectionElementVO.class); - collectionElementVO.setMinIOPath(collectionElementVO.getUrl()); - collectionElementVO.setUrl(minioUtil.getPreSignedUrl(collectionElementVO.getUrl(), 24 * 60)); - collectionElementVO.setDesignType(DesignTypeEnum.COLLECTION.getRealName()); - - long end = System.currentTimeMillis(); - double floor = Math.floor((double) (end - start) / 1000); - log.info("本次图片上传耗时:{} 毫秒", end - start); - Long incrementCount = redisUtil.getIncrementCount(RedisUtil.UPLOAD_TIMEOUT_REMINDER_COUNTER); - if (floor > 5 && incrementCount < 3) { - // 邮件通知 一天最多通知3次 - log.info("上传超过5秒,发送邮件通知"); - SendEmailUtil.uploadTimeoutReminder(userInfo.getUsername(), String.valueOf(((end - start) / 1000))); - redisUtil.increaseCount(RedisUtil.UPLOAD_TIMEOUT_REMINDER_COUNTER); - } - return collectionElementVO; - } - - private String calculateFileUrl(CollectionLevel1TypeEnum level1TypeEnum, Long userId) { - String rootPath = fileProperties.getSys().getPath(); - String day = DateUtil.dateToStr(new Date(), DateUtil.YYYYMM); - return rootPath + day + "/" + "userFile" + "/collection" - + "/" + level1TypeEnum.getRealName() + "/" + userId + "/" + UUID.randomUUID().toString(); - } - - private CollectionElement resolveData(CollectionElementUploadDTO uploadDTO, AuthPrincipalVo userInfo, File file) { - CollectionElement element = CopyUtil.copyObject(uploadDTO, CollectionElement.class); - element.setAccountId(userInfo.getId()); - element.setCollectionId(0L); - element.setHasPin((byte) 0); - String pictureCollectonName = file.getName(); - //获取图片后缀 - String suffix = pictureCollectonName.substring(pictureCollectonName.lastIndexOf(".")); - //获取图片前缀 - String prefix = pictureCollectonName.substring(0, pictureCollectonName.lastIndexOf(".")); - element.setName(DateUtil.dateToStr(new Date(), DateUtil.YYYY_MM_DD)); - element.setUrl(file.getAbsolutePath()); - //按时区计算 - element.setCreateDate(DateUtil.getByTimeZone(uploadDTO.getTimeZone())); - String linuxDomain = fileProperties.getLinuxDomain(); - if (!StringUtils.isEmpty(linuxDomain)) { - //linux 系统 - String oldPath = fileProperties.getSys().getPath(); - element.setUrl(file.getAbsolutePath().replace(oldPath, linuxDomain)); - } - return element; - } - - private CollectionElement resolveData(CollectionElementUploadDTO uploadDTO, AuthPrincipalVo userInfo, String path, String level2Type) { - CollectionElement element = CopyUtil.copyObject(uploadDTO, CollectionElement.class); - element.setAccountId(userInfo.getId()); - element.setCollectionId(0L); - element.setHasPin((byte) 0); -// String pictureCollectonName = fileName; -// //获取图片后缀 -// String suffix = pictureCollectonName.substring(pictureCollectonName.lastIndexOf(".")); -// //获取图片前缀 -// String prefix = pictureCollectonName.substring(0,pictureCollectonName.lastIndexOf(".")); - String originalFilename = uploadDTO.getFile().getOriginalFilename(); - if (originalFilename != null && originalFilename.contains(".")) { - // 如果文件名包含点号,则去除最后一个点及其后面的内容 - int lastDotIndex = originalFilename.lastIndexOf("."); - if (lastDotIndex != -1) { - originalFilename = originalFilename.substring(0, lastDotIndex); - } - } - element.setName(originalFilename); - element.setUrl(path); - //按时区计算 - element.setCreateDate(DateUtil.getByTimeZone(uploadDTO.getTimeZone())); - element.setLevel2Type(level2Type); - if (Objects.nonNull(uploadDTO.getProjectId())){ - element.setProjectId(uploadDTO.getProjectId()); - } -// String linuxDomain = fileProperties.getLinuxDomain(); -// if (!StringUtils.isEmpty(linuxDomain)) { -// //linux 系统 -// String oldPath = fileProperties.getSys().getPath(); -// element.setUrl(file.getAbsolutePath().replace(oldPath, linuxDomain)); -// } - return element; - } - - @Override - public void delete(Long id) { - CollectionElement collectionElement = selectById(id); - if (Objects.isNull(collectionElement)) { - throw new BusinessException("collectionElement.not.found"); - } - minioUtil.deleteObject(collectionElement.getUrl()); - collectionElementMapper.deleteById(id); -// if (!FileUtil.delete(collectionElement.getUrl())) { -// throw new BusinessException("file deletion failed! "); -// } - } - - @Override - public void batchDelete(List ids) { - if (CollectionUtils.isEmpty(ids)) { - return; - } -// QueryWrapper queryWrapper = new QueryWrapper<>(); -// queryWrapper.in("id", ids); -// CollectionElement collectionElement = new CollectionElement(); -// collectionElement.setCollectionId(0L); - collectionElementMapper.deleteBatchIds(ids); - } - - /** 该方法已不再使用 */ - @Deprecated - @Override - public GenerateCollectionItemVO generatePrint(CollectionGeneratePrintDTO generatePrintDTO) { - Long userId = UserContext.getUserHolder().getId(); - String url1 = null; - String url2 = null; - CollectionElement element1 = selectById(generatePrintDTO.getSelect1Id()); - if (Objects.isNull(element1)) { - Library library1 = libraryService.getById(generatePrintDTO.getSelect1Id()); - if (Objects.isNull(library1)) { - throw new BusinessException("select1.file.does.not.exist"); - } - url1 = library1.getUrl(); - } else { - url1 = element1.getUrl(); - } - CollectionElement element2 = selectById(generatePrintDTO.getSelect2Id()); - if (Objects.isNull(element2)) { - Library library2 = libraryService.getById(generatePrintDTO.getSelect2Id()); - if (Objects.isNull(library2)) { - throw new BusinessException("select2.file.does.not.exist"); - } - url2 = library2.getUrl(); - } else { - url2 = element2.getUrl(); - } - List printPath = Arrays.asList(url1, url2); - //调取python 接口 - String generateUrl = pythonService.generatePrint(printPath,userId); - if (StringUtils.isEmpty(generateUrl)) { - throw new BusinessException("generate.interface.exception"); - } - - // 保存合成信息到generate表 - Generate generate = setGenerate(userId, generatePrintDTO.getTimeZone()); - generateMapper.insert(generate); - - // 保存合成后的信息到generateDetail - GenerateDetail generateDetail = setGenerateDetail(generate.getId(), generateUrl, generatePrintDTO.getTimeZone()); - generateDetailMapper.insert(generateDetail); - -// CollectionElement element = resolveData(generateUrl, generatePrintDTO.getTimeZone(), userId); -// if (!this.save(element)) { -// throw new BusinessException("save.collectionElement.failed"); -// } -// CollectionGeneratePrintVO collectionGeneratePrint = CopyUtil.copyObject(element, CollectionGeneratePrintVO.class); -// collectionGeneratePrint.setUrl(minioUtil.getPresignedUrl(generateUrl, 24 * 60)); -// collectionGeneratePrint.setDesignType(DesignTypeEnum.COLLECTION.getRealName()); -// return collectionGeneratePrint; - return new GenerateCollectionItemVO(generateDetail.getId(), - minioUtil.getPreSignedUrl(generateUrl, 24 * 60), - generateDetail.getIsLike().equals((byte) 0) ? Boolean.FALSE : Boolean.TRUE); - } - - @Override - public Boolean savePrint(CollectionSavePrintDTO savePrintDTO) { - //用户信息 - List elements = listByIds(savePrintDTO.getPrintId()); - if (CollectionUtils.isEmpty(elements)) { - throw new BusinessException("collectionElements.not.found"); - } - return saveLibraryByCollectionElement(elements, savePrintDTO.getTimeZone()); - } - - @Override - public Boolean saveLibraryByCollectionElement(List elements, String timeZone) { - if (CollectionUtils.isEmpty(elements)) { - return Boolean.TRUE; - } - //获取已存在相同的library - List md5List = elements.stream().map(CollectionElement::getMd5).collect(Collectors.toList()); - List existsLibrarys = libraryService.getByMD5List(md5List); - if (!CollectionUtils.isEmpty(existsLibrarys)) { - //去重 - List existsMd5Lists = existsLibrarys.stream().map(Library::getMd5).collect(Collectors.toList()); - elements = elements.stream().filter(element -> !existsMd5Lists.contains(element.getMd5())).collect(Collectors.toList()); - } - if (CollectionUtils.isEmpty(elements)) { - //都是重复的 - return Boolean.TRUE; - } - String name = DateUtil.dateToStr(new Date(), DateUtil.YYYY_MM_DD); - List libraryList = CopyUtil.copyList(elements, Library.class, (o, d) -> { - d.setCreateDate(DateUtil.getByTimeZone(timeZone)); - d.setName(name); - d.setId(null); - }); - if (!libraryService.saveBatch(libraryList)) { - throw new BusinessException("batch.save.libraryList.failed"); - } - return Boolean.TRUE; - } - - @Override - public Boolean saveLibraryByCollectionElement(List elements, String timeZone, String modelSex) { - if (CollectionUtils.isEmpty(elements)) { - return Boolean.TRUE; - } - //获取已存在相同的library - List md5List = elements.stream().map(CollectionElement::getMd5).collect(Collectors.toList()); - List existsLibrarys = libraryService.getByMD5List(md5List); - if (!CollectionUtils.isEmpty(existsLibrarys)) { - //去重 - List existsMd5Lists = existsLibrarys.stream().map(Library::getMd5).collect(Collectors.toList()); - elements = elements.stream().filter(element -> !existsMd5Lists.contains(element.getMd5())).collect(Collectors.toList()); - } - if (CollectionUtils.isEmpty(elements)) { - //都是重复的 - return Boolean.TRUE; - } - String name = DateUtil.dateToStr(new Date(), DateUtil.YYYY_MM_DD); - List libraryList = CopyUtil.copyList(elements, Library.class, (o, d) -> { - if (d.getLevel1Type().equals(LibraryLevel1TypeEnum.SKETCH_BOARD.getRealName())) { - d.setLevel3Type(modelSex); -// try { -// libraryService.processSketchBoards(d.getUrl(), d.getLevel2Type()); -// }catch (Exception e) { -// // TODO:暂不处理 -// } - } - d.setCreateDate(DateUtil.getByTimeZone(timeZone)); - d.setName(name); - d.setId(null); - }); - if (!libraryService.saveBatch(libraryList)) { - throw new BusinessException("batch.save.libraryList.failed"); - } - return Boolean.TRUE; - } - - private CollectionElement resolveData(String path, String timeZone, Long userId){ -// File file = new File(path); - - String name = path.substring(path.lastIndexOf("/") + 1, path.lastIndexOf(".")); - CollectionElement element = new CollectionElement(); - element.setAccountId(userId); - element.setCollectionId(0L); - element.setName(name); - element.setLevel1Type(CollectionLevel1TypeEnum.PRINT_BOARD.getRealName()); - element.setUrl(path); - element.setHasPin((byte) 0); - try { - element.setMd5(MD5Utils.encryptFile(minioUtil.download(path))); - }catch (Exception e){ - throw new RuntimeException(e); - } - /* catch (MinioException | IOException e) { - throw new RuntimeException(e); - }*/ - - //按时区计算 - element.setCreateDate(DateUtil.getByTimeZone(timeZone)); - return element; - } - - @Override - public ValidateElementVO validateElement(DesignCollectionDTO designDTO) { - ValidateElementVO elementVO = CopyUtil.copyObject(designDTO, ValidateElementVO.class); - List colorBoards = elementVO.getColorBoards(); - for (CollectionColorDTO colorBoard : colorBoards) { - if (Objects.nonNull(colorBoard.getGradient())) { - String colorImg = colorBoard.getGradient().getColorImg(); - String[] parts = colorImg.split(","); - String imageType = parts[0].split("/")[1].split(";")[0]; - String base64Data = parts[1]; - String gradientMinioUrl = minioUtil.uploadImageFromBase64(gradientBucketName, base64Data, imageType); - colorBoard.setGradientMinioUrl(gradientMinioUrl); - colorBoard.getGradient().setColorImg(null); - colorBoard.setGradientString(JSON.toJSONString(colorBoard.getGradient())); - } - } - elementVO.setColorBoards(colorBoards); - List usedElementIds = elementVO.getUsedElementIds(); - List libraryCollectionElements = elementVO.getLibraryCollectionElements(); - List generateCollectionElements = elementVO.getGenerateCollectionElements(); - //校验moodboard - if (CollectionUtil.isNotEmpty(designDTO.getMoodBoards())) { - //校验designType - validateDesignType(designDTO.getMoodBoards(), "moodBoards"); - List moodBoardIds = designDTO.getMoodBoards().stream() - .filter(f -> f.getDesignType().equals(DesignTypeEnum.COLLECTION.getRealName())) - .map(DesignCollectionElementDTO::getId) - .collect(Collectors.toList()); - if (!CollectionUtils.isEmpty(moodBoardIds)) { - List MoodBoardElements = collectionElementMapper.selectBatchIds(moodBoardIds); - if (CollectionUtil.isEmpty(MoodBoardElements) || MoodBoardElements.size() != moodBoardIds.size()) { - throw new BusinessException("get.moodBoards.data.is.mismatch"); - } - elementVO.setMoodBoardElements(MoodBoardElements); - usedElementIds.addAll(moodBoardIds); - } - //library - List libraryIds = designDTO.getMoodBoards().stream() - .filter(f -> f.getDesignType().equals(DesignTypeEnum.LIBRARY.getRealName())) - .map(DesignCollectionElementDTO::getId) - .collect(Collectors.toList()); - if (!CollectionUtils.isEmpty(libraryIds)) { - List librarys = libraryService.getByIds(libraryIds); - //不校验了防止用户在library删除 对应不上 - if (CollectionUtil.isNotEmpty(librarys)) { - libraryCollectionElements.addAll(covertLibrarysToCollections(librarys, null)); - } - } - // generate - List generateIds = designDTO.getMoodBoards().stream() - .filter(o -> o.getDesignType().equals((DesignTypeEnum.GENERATE.getRealName()))) - .map(DesignCollectionElementDTO::getId) - .collect((Collectors.toList())); - if (CollectionUtil.isNotEmpty(generateIds)) { - List generateDetailList = generateDetailMapper.selectBatchIds(generateIds); - if (CollectionUtil.isNotEmpty(generateDetailList)) { - generateCollectionElements.addAll(covertGeneratesToCollections(generateDetailList, null)); - } - } - } - if (CollectionUtil.isNotEmpty(designDTO.getPrintBoards())) { - //校验designType - validateDesignType(CopyUtil.copyList(designDTO.getPrintBoards(), DesignCollectionElementDTO.class), "printBoards"); - List printBoardIds = designDTO.getPrintBoards().stream() - .filter(f -> f.getDesignType().equals(DesignTypeEnum.COLLECTION.getRealName())) - .map(DesignCollectionPrintElementDTO::getId) - .collect(Collectors.toList()); - if (!CollectionUtils.isEmpty(printBoardIds)) { - //校验printboard - List printBoardElements = collectionElementMapper.selectBatchIds(printBoardIds); - if (CollectionUtil.isEmpty(printBoardElements) || printBoardElements.size() != printBoardIds.size()) { - throw new BusinessException("get.printBoards.data.is.mismatch"); - } - elementVO.setPrintBoardElements(printBoardElements); - usedElementIds.addAll(printBoardIds); - } - //library - List libraryIds = designDTO.getPrintBoards().stream() - .filter(f -> f.getDesignType().equals(DesignTypeEnum.LIBRARY.getRealName())) - .map(DesignCollectionPrintElementDTO::getId) - .collect(Collectors.toList()); - if (!CollectionUtils.isEmpty(libraryIds)) { - List librarys = libraryService.getByIds(libraryIds); - //不校验了防止用户在library删除 对应不上 - if (CollectionUtil.isNotEmpty(librarys)) { - Map idToMap = designDTO.getPrintBoards() - .stream() - .collect(Collectors.toMap(DesignCollectionPrintElementDTO::getId, v -> v)); - libraryCollectionElements.addAll(covertLibrarysToPrintCollections(librarys, idToMap)); - } - } - // generate - List generateIds = designDTO.getPrintBoards().stream() - .filter(o -> o.getDesignType().equals((DesignTypeEnum.GENERATE.getRealName()))) - .map(DesignCollectionPrintElementDTO::getId) - .collect((Collectors.toList())); - if (CollectionUtil.isNotEmpty(generateIds)) { - List generateDetailList = generateDetailMapper.selectBatchIds(generateIds); - if (CollectionUtil.isNotEmpty(generateDetailList)) { - Map idToMap = designDTO.getPrintBoards() - .stream() - .collect(Collectors.toMap(DesignCollectionPrintElementDTO::getId, v -> v)); - generateCollectionElements.addAll(covertGeneratesToPrintCollections(generateDetailList, idToMap)); - } - } - } - if (CollectionUtil.isNotEmpty(designDTO.getSketchBoards())) { - //校验PIN是否满足 上衣或者下衣必须不超过8 - long topNum = 0; - long bottomNum = 0; - long outerwearNum = 0; - if (designDTO.getModelSex().equals(Sex.FEMALE.getValue())) { - topNum= designDTO.getSketchBoards().stream() - .filter(skecth -> skecth.getIsPin() == 1 - && DesignPythonItem.DRESS_BLOUSE.contains(skecth.getLevel2Type())).count(); - bottomNum = designDTO.getSketchBoards().stream() - .filter(skecth -> skecth.getIsPin() == 1 - && DesignPythonItem.SKIRT_TROUSERS.contains(skecth.getLevel2Type())).count(); - }else if (designDTO.getModelSex().equals(Sex.MALE.getValue())) { - topNum= designDTO.getSketchBoards().stream() - .filter(skecth -> skecth.getIsPin() == 1 - && DesignPythonItem.TOPS.contains(skecth.getLevel2Type())).count(); - bottomNum = designDTO.getSketchBoards().stream() - .filter(skecth -> skecth.getIsPin() == 1 - && DesignPythonItem.BOTTOMS.contains(skecth.getLevel2Type())).count(); - } - outerwearNum = designDTO.getSketchBoards().stream() - .filter(skecth -> skecth.getIsPin() == 1 - && DesignPythonItem.OUTWEAR.contains(skecth.getLevel2Type())).count(); - if (topNum > 8 || bottomNum > 8 || outerwearNum > 8) { - throw new BusinessException("the.number.of.PIN.top.or.bottom.or.outerwear.sketchBoard.cannot.be.more.than.8", ResultEnum.PROMPT.getCode()); - } - //校验designType - Boolean result = designDTO.getSketchBoards().stream() - .filter(mood -> StringUtils.isEmpty(mood.getDesignType())) - .findFirst().isPresent(); - if (result) { - throw new BusinessException("sketchBoards.designType.cannot.be.empty"); - } - - List sketchBoardIds = designDTO.getSketchBoards().stream() - .filter(f -> f.getDesignType().equals(DesignTypeEnum.COLLECTION.getRealName())) - .map(CollectionSketchDTO::getSketchBoardId) - .collect(Collectors.toList()); - if (!CollectionUtils.isEmpty(sketchBoardIds)) { - //校验sketchBoard - List sketchBoardElements = collectionElementMapper.selectBatchIds(sketchBoardIds); - if (CollectionUtil.isEmpty(sketchBoardElements) || sketchBoardElements.size() != sketchBoardIds.size()) { - throw new BusinessException("get.sketchBoards.data.is.mismatch"); - } - elementVO.setSketchBoardElements(sketchBoardElements); - usedElementIds.addAll(sketchBoardIds); - } - //library - List libraryIds = designDTO.getSketchBoards().stream() - .filter(f -> f.getDesignType().equals(DesignTypeEnum.LIBRARY.getRealName())) - .map(CollectionSketchDTO::getSketchBoardId) - .collect(Collectors.toList()); - if (!CollectionUtils.isEmpty(libraryIds)) { - List librarys = libraryService.getByIds(libraryIds); - //不校验了防止用户在library删除 对应不上 - if (CollectionUtil.isNotEmpty(librarys)) { - Map idToMap = designDTO.getSketchBoards() - .stream() - .collect(Collectors.toMap(CollectionSketchDTO::getSketchBoardId, v -> v)); - libraryCollectionElements.addAll(covertLibrarysToCollections(librarys, idToMap)); - } - } - // generate - List generateIds = designDTO.getSketchBoards().stream() - .filter(o -> o.getDesignType().equals((DesignTypeEnum.GENERATE.getRealName()))) - .map(CollectionSketchDTO::getSketchBoardId) - .collect((Collectors.toList())); - if (CollectionUtil.isNotEmpty(generateIds)) { - List generateDetailList = generateDetailMapper.selectBatchIds(generateIds); - if (CollectionUtil.isNotEmpty(generateDetailList)) { - Map idToMap = designDTO.getSketchBoards() - .stream() - .collect(Collectors.toMap(CollectionSketchDTO::getSketchBoardId, v -> v)); - generateCollectionElements.addAll(covertGeneratesToCollections(generateDetailList, idToMap)); - } - } - } - //校验marketingSketch - // 2023.12版本去掉了这个入参 -/* if (CollectionUtil.isNotEmpty(designDTO.getMarketingSketchs())) { - //校验designType - validateDesignType(designDTO.getMarketingSketchs(),"marketingSketchs"); - List printBoardIds = designDTO.getMarketingSketchs().stream() - .filter(f ->f.getDesignType().equals(DesignTypeEnum.COLLECTION.getRealName())) - .map(DesignCollectionElementDTO::getId) - .collect(Collectors.toList()); - if(!CollectionUtils.isEmpty(printBoardIds)){ - List marketingSketchElements = collectionElementMapper.selectBatchIds(printBoardIds); - Assert.isTrue(CollectionUtil.isNotEmpty(marketingSketchElements) - && marketingSketchElements.size() == printBoardIds.size(), "get marketingSketch data is mismatch"); - elementVO.setMarketingSketchElements(marketingSketchElements); - usedElementIds.addAll(printBoardIds); - } - //library - List libraryIds = designDTO.getMarketingSketchs().stream() - .filter(f ->f.getDesignType().equals(DesignTypeEnum.LIBRARY.getRealName())) - .map(DesignCollectionElementDTO::getId) - .collect(Collectors.toList()); - if(!CollectionUtils.isEmpty(libraryIds)){ - List librarys = libraryService.getByIds(libraryIds); - //不校验了防止用户在library删除 对应不上 - if(CollectionUtil.isNotEmpty(librarys)){ - libraryCollectionElements.addAll(covertLibrarysToCollections(librarys,null)); - } - } - }*/ - //校验控制生成类型 - SingleOverallEnum singleOverall = SingleOverallEnum.of(designDTO.getSingleOverall()); - if (Objects.isNull(singleOverall)) { - log.error("未知singleOverall param:{}", designDTO.getSingleOverall()); - log.info("入参 designDTO => {}", designDTO); - throw new BusinessException("unknown.parameter.singleOverall"); - } - if (SingleOverallEnum.SINGLE.equals(singleOverall)) { - SwitchCategoryEnum switchCategory = SwitchCategoryEnum.of(designDTO.getSwitchCategory()); - if (Objects.isNull(switchCategory)) { - log.error("未知switchCategory:{}", designDTO.getSwitchCategory()); - log.info("入参 designDTO => {}", designDTO); - throw new BusinessException("unknown.parameter.switchCategory"); - } - } - // 校验模特 - if (!CollectionUtils.isEmpty(designDTO.getMannequins())) { - List designLibraryModelPointVOList = new ArrayList<>(); - for (MannequinDTO mannequin : designDTO.getMannequins()) { - if (mannequin.getType().equals("System")) { - SysFileVO byId = sysFileService.getById(mannequin.getId()); -// if (!StringUtils.isEmpty(byId.getLevel3Type()) && byId.getLevel2Type().equals("Female")) { -// elementVO.setStyle(byId.getLevel3Type()); -// } - LibraryModelPoint modelPoint = libraryModelPointService.getByRelationId(byId.getId(), mannequin.getType()); - designLibraryModelPointVOList.add(calculateTemplatePointTemplate(modelPoint, 700, 320, byId.getUrl())); - }else { - Library byId = libraryService.getById(mannequin.getId()); - LibraryModelPoint modelPoint = libraryModelPointService.getByRelationId(byId.getId(), mannequin.getType()); - designLibraryModelPointVOList.add(calculateTemplatePointTemplate(modelPoint, byId.getHigh(), byId.getWidth(), byId.getUrl())); - } - } - elementVO.setMannequins(designLibraryModelPointVOList); - }else { - if (!StringUtils.isEmpty(designDTO.getModelType())) { - if (designDTO.getModelType().equals(ModelType.LIBRARY.getValue())) { - Library byId = libraryService.getById(designDTO.getTemplateId()); - LibraryModelPoint modelPoint = libraryModelPointService.getByRelationId(byId.getId(), designDTO.getModelType()); - elementVO.setDesignLibraryModelPoint(calculateTemplatePointTemplate(modelPoint, byId.getHigh(), byId.getWidth(), byId.getUrl())); - } else if (designDTO.getModelType().equals(ModelType.SYSTEM.getValue())) { - SysFileVO byId = sysFileService.getById(designDTO.getTemplateId()); - if (!StringUtils.isEmpty(byId.getLevel3Type()) && byId.getLevel2Type().equals("Female")) { - elementVO.setStyle(byId.getLevel3Type()); - } - LibraryModelPoint modelPoint = libraryModelPointService.getByRelationId(byId.getId(), designDTO.getModelType()); - elementVO.setDesignLibraryModelPoint(calculateTemplatePointTemplate(modelPoint, 700, 320, byId.getUrl())); - } - } - } - elementVO.setModelSex(designDTO.getModelSex()); - elementVO.setRequestIdList(designDTO.getRequestIdList()); - if (null != designDTO.getDesignNum()) { - elementVO.setDesignNum(designDTO.getDesignNum()); - }else { - elementVO.setDesignNum(8); - } - if (null != designDTO.getBrandId()) { - elementVO.setBrandId(designDTO.getBrandId()); - elementVO.setBrandScale(designDTO.getBrandScale()); - } - return elementVO; - } - - @Override - public DesignLibraryModelPointVO calculateTemplatePoint(LibraryModelPoint modelPoint, Integer high, Integer width, String templateUrl) { - DesignLibraryModelPointVO libraryModelPoint = new DesignLibraryModelPointVO(); - libraryModelPoint.setHandLeft(calculateTemplatePointOne(modelPoint.getHandLeft(), high, width)); - libraryModelPoint.setHandRight(calculateTemplatePointOne(modelPoint.getHandRight(), high, width)); - libraryModelPoint.setShoulderLeft(calculateTemplatePointOne(modelPoint.getShoulderLeft(), high, width)); - libraryModelPoint.setShoulderRight(calculateTemplatePointOne(modelPoint.getShoulderRight(), high, width)); - libraryModelPoint.setWaistbandLeft(calculateTemplatePointOne(modelPoint.getWaistbandLeft(), high, width)); - libraryModelPoint.setWaistbandRight(calculateTemplatePointOne(modelPoint.getWaistbandRight(), high, width)); - libraryModelPoint.setTemplateUrl(templateUrl); - return libraryModelPoint; - } - - @Override - public DesignLibraryModelPointVO calculateTemplatePointTemplate(LibraryModelPoint modelPoint, Integer high, Integer width, String templateUrl) { - DesignLibraryModelPointVO libraryModelPoint = new DesignLibraryModelPointVO(); -// LibraryModelPoint template = libraryModelPointService.getById(96L); -// libraryModelPoint.setHandLeft(calculateTemplatePointOne(template.getHandLeft(),752,564)); -// libraryModelPoint.setHandRight(calculateTemplatePointOne(template.getHandRight(),752,564)); -// libraryModelPoint.setShoulderLeft(calculateTemplatePointOne(template.getShoulderLeft(),752,564)); -// libraryModelPoint.setShoulderRight(calculateTemplatePointOne(template.getShoulderRight(),752,564)); -// libraryModelPoint.setWaistbandLeft(calculateTemplatePointOne(template.getWaistbandLeft(),752,564)); -// libraryModelPoint.setWaistbandRight(calculateTemplatePointOne(template.getWaistbandRight(),752,564)); -// libraryModelPoint.setTemplateUrl("aida-mannequins/model_1693218345.2714432.png"); - libraryModelPoint.setHandLeft(calculateTemplatePointOne(modelPoint.getHandLeft(), high, width)); - libraryModelPoint.setHandRight(calculateTemplatePointOne(modelPoint.getHandRight(), high, width)); - libraryModelPoint.setShoulderLeft(calculateTemplatePointOne(modelPoint.getShoulderLeft(), high, width)); - libraryModelPoint.setShoulderRight(calculateTemplatePointOne(modelPoint.getShoulderRight(), high, width)); - libraryModelPoint.setWaistbandLeft(calculateTemplatePointOne(modelPoint.getWaistbandLeft(), high, width)); - libraryModelPoint.setWaistbandRight(calculateTemplatePointOne(modelPoint.getWaistbandRight(), high, width)); - libraryModelPoint.setTemplateUrl(templateUrl); - libraryModelPoint.setRelationId(modelPoint.getRelationId()); - libraryModelPoint.setRelationType(modelPoint.getModelType()); - return libraryModelPoint; - } - - private List calculateTemplatePointOne(String template, Integer high, Integer width) { - List originRatioList = JSON.parseObject(template, List.class); - originRatioList.set(0, originRatioList.get(0).multiply(BigDecimal.valueOf(width))); - originRatioList.set(1, originRatioList.get(1).multiply(BigDecimal.valueOf(high))); - return originRatioList; - } - - private List covertLibrarysToCollections(List libraries, Map idToMap) { - return CopyUtil.copyList(libraries, CollectionElement.class, (o, d) -> { - if (null != idToMap) { - CollectionSketchDTO sketchDTO = idToMap.get(o.getId()); - d.setLevel2Type(sketchDTO.getLevel2Type()); - d.setHasPin(sketchDTO.getIsPin()); - } - }); - } - - private List covertGeneratesToCollections(List generateDetailList, Map idToMap) { - return CopyUtil.copyList(generateDetailList, CollectionElement.class, (o, d) -> { - Generate byId = generateMapper.selectById(o.getGenerateId()); - d.setAccountId(byId.getAccountId()); - d.setLevel1Type(byId.getLevel1Type()); - d.setCreateDate(Date.from(o.getCreateDate().atZone(ZoneId.systemDefault()).toInstant())); - if (null != idToMap) { - CollectionSketchDTO sketchDTO = idToMap.get(o.getId()); - d.setLevel2Type(sketchDTO.getLevel2Type()); - d.setHasPin(sketchDTO.getIsPin()); - } - }); - } - - private List covertLibrarysToPrintCollections(List libraries, Map idToMap) { - return CopyUtil.copyList(libraries, CollectionElement.class, (o, d) -> { - if (null != idToMap) { - DesignCollectionPrintElementDTO printDTO = idToMap.get(o.getId()); - d.setHasPin(printDTO.getIsPin()); - } - }); - } - - private List covertGeneratesToPrintCollections(List generateDetailList, Map idToMap) { - return CopyUtil.copyList(generateDetailList, CollectionElement.class, (o, d) -> { - Generate byId = generateMapper.selectById(o.getGenerateId()); - d.setAccountId(byId.getAccountId()); - d.setLevel1Type(byId.getLevel1Type()); - if (!StringUtils.isEmpty(byId.getLevel2Type())) { - d.setLevel2Type(byId.getLevel2Type()); - } - d.setCreateDate(Date.from(o.getCreateDate().atZone(ZoneId.systemDefault()).toInstant())); - if (null != idToMap) { - DesignCollectionPrintElementDTO printDTO = idToMap.get(o.getId()); - d.setHasPin(printDTO.getIsPin()); - } - }); - } - - private void validateDesignType(List collectionElements, String msg) { - Boolean result = collectionElements.stream().filter(mood -> StringUtils.isEmpty(mood.getDesignType())).findFirst().isPresent(); - if (result) { - throw new BusinessException(msg + ".designType.cannot.be.empty"); - } - } - - @Override - public void editSketchBoardsElement(ValidateElementVO elementVO, List sketchBoards) { - if (CollectionUtil.isNotEmpty(sketchBoards)) { - List collect = sketchBoards.stream().filter(o -> o.getDesignType().equals(DesignTypeEnum.COLLECTION.getRealName())).collect(Collectors.toList()); - collect.forEach(sketchBoard -> { - CollectionElement collectionElement = CopyUtil.copyObject(sketchBoard, CollectionElement.class); - collectionElement.setHasPin(sketchBoard.getIsPin()); - collectionElement.setId(sketchBoard.getSketchBoardId()); - collectionElementMapper.updateById(collectionElement); - }); - List sketchBoardIds = collect.stream().map(CollectionSketchDTO::getSketchBoardId).collect(Collectors.toList()); - if (!CollectionUtils.isEmpty(sketchBoardIds)) { - List sketchBoardElements = collectionElementMapper.selectBatchIds(sketchBoardIds); - elementVO.setSketchBoardElements(sketchBoardElements); - }else { - elementVO.setSketchBoardElements(new ArrayList<>()); - } - } - } - - @Override - public void editPrintBoardsElement(ValidateElementVO elementVO, List printBoards) { - if (CollectionUtil.isNotEmpty(printBoards)) { - List collect = printBoards.stream().filter(o -> o.getDesignType().equals(DesignTypeEnum.COLLECTION.getRealName())).collect(Collectors.toList()); - collect.forEach(printBoard -> { - CollectionElement collectionElement = CopyUtil.copyObject(printBoard, CollectionElement.class); - collectionElement.setHasPin(Objects.isNull(printBoard.getIsPin()) ? 0 : printBoard.getIsPin()); - collectionElement.setId(printBoard.getId()); - collectionElementMapper.updateById(collectionElement); - }); - List printBoardIds = collect.stream().map(DesignCollectionPrintElementDTO::getId).collect(Collectors.toList()); - if (!CollectionUtils.isEmpty(printBoardIds)) { - List printBoardElements = collectionElementMapper.selectBatchIds(printBoardIds); - elementVO.setPrintBoardElements(printBoardElements); - }else { - elementVO.setPrintBoardElements(new ArrayList<>()); - } - } - } - - @Transactional(rollbackFor = Exception.class) - @Override - public void relationCollection(List elementIds, Long collectionId) { - if (CollectionUtils.isEmpty(elementIds) || null == collectionId) { - return; - } - QueryWrapper queryWrapper = new QueryWrapper<>(); - queryWrapper.lambda().in(CollectionElement::getId, elementIds); - CollectionElement element = new CollectionElement(); - element.setCollectionId(collectionId); - //批量关联 - collectionElementMapper.update(element, queryWrapper); - } - - @Transactional(rollbackFor = Exception.class) - @Override - public List saveColorBoard(List colorBoards, Long collectionId, String timeZone) { - //用户信息 - AuthPrincipalVo userInfo = UserContext.getUserHolder(); - List colorElements = resolveColorData(colorBoards, userInfo, collectionId, timeZone); - if (!this.saveBatch(colorElements)) { - throw new BusinessException("batch.save.colorElements.failed"); - } - return CopyUtil.copyList(colorElements, CollectionElementVO.class); - } - - @Override - public void refreshHistoryData() { - //幂等 - if (!CollectionUtils.isEmpty(tCollectionElementRelationService.getByCollectionId(1083L))) { - return; - } - // 分页数据 - QueryWrapper queryWrapper = new QueryWrapper<>(); - queryWrapper.orderByAsc("id"); - PageQueryBaseVo pageQuery = new PageQueryBaseVo(); - pageQuery.setPage(1); - pageQuery.setSize(200); - while (true) { - IPage page = getBaseMapper().selectPage( - new Page<>(pageQuery.getPage(), pageQuery.getSize()), queryWrapper); - List list = page.getRecords(); - if (CollectionUtils.isEmpty(list)) { - break; - } - - //保存 - List relations = list.stream().map(element -> - TCollectionElementRelation.builder().elementId(element.getId()) - .collectionId(element.getCollectionId()).createDate(new Date()).build()) - .collect(Collectors.toList()); - tCollectionElementRelationService.saveBatch(relations); - pageQuery.setPage(pageQuery.getPage() + 1); - log.info("refreshHistoryData###process###page###" + pageQuery.getPage()); - } - } - - private List resolveColorData(List colorBoards, AuthPrincipalVo userInfo, Long collectionId, String timeZone) { - List elements = Lists.newArrayList(); - colorBoards.forEach(color -> { - CollectionElement element = new CollectionElement(); - element.setAccountId(userInfo.getId()); - element.setCollectionId(collectionId); - if (StringUtils.isEmpty(color.getName())) { - element.setName(null); - } else { - element.setName(color.getId() + "_" + color.getName() + "_" + color.getTcx()); - } - element.setLevel1Type(CollectionLevel1TypeEnum.COLOR_BOARD.getRealName()); - element.setHasPin((byte) 0); - element.setMd5("0"); - element.setColorRgb(color.getRgbValue()); - //按时区计算 - element.setCreateDate(DateUtil.getByTimeZone(timeZone)); - if (Objects.nonNull(color.getGradient())) { - color.getGradient().setColorImg(null); - } - element.setGradientString(JSON.toJSONString(color.getGradient())); - elements.add(element); - }); - return elements; - } - - @Override - public List getByCollectionId(Long collectionId) { -// List elementIds = tCollectionElementRelationService.getByCollectionId(collectionId); -// if (CollectionUtils.isEmpty(elementIds)) { -// return null; -// } - QueryWrapper queryWrapper = new QueryWrapper<>(); -// queryWrapper.in("id", elementIds); - queryWrapper.lambda().eq(CollectionElement::getCollectionId, collectionId); - return collectionElementMapper.selectList(queryWrapper); - } - - @Override - public List getByOnlyCollectionId(Long collectionId) { - QueryWrapper queryWrapper = new QueryWrapper<>(); - queryWrapper.eq("collection_id", collectionId); - return collectionElementMapper.selectList(queryWrapper); - } - - private CollectionElement selectById(Long id) { - QueryWrapper queryWrapper = new QueryWrapper<>(); - queryWrapper.eq("id", id); - return collectionElementMapper.selectOne(queryWrapper); - } - - private Boolean exists(Long id) { - QueryWrapper queryWrapper = new QueryWrapper<>(); - queryWrapper.eq("id", id); - return collectionElementMapper.exists(queryWrapper); - } - - private boolean saveOne(CollectionElement collectionElement) { - if (collectionElementMapper.insert(collectionElement) <= 0) { - throw new BusinessException("save.collectionElement.failed"); - } - return Boolean.TRUE; - } - - @Override - public CollectionElement editLevel2Type(Long elementId, String level2Type, String designType) { - CollectionElement collectionElement = new CollectionElement(); - - if (!Objects.isNull(elementId)) { - if (!StringUtil.isNullOrEmpty(designType)){ - switch (designType){ - case "collection": - collectionElement = collectionElementMapper.selectById(elementId); - if (StringUtil.isNullOrEmpty(collectionElement.getLevel2Type()) || !(collectionElement.getLevel2Type()).equals(level2Type)) { - collectionElement.setLevel2Type(level2Type); - updateById(collectionElement); - } - break; - case "library": - Library libraryElement = libraryService.getById(elementId); - if (!Objects.isNull(libraryElement)) { - if (StringUtil.isNullOrEmpty(libraryElement.getLevel2Type()) || !(libraryElement.getLevel2Type()).equals(level2Type)){ - libraryElement.setLevel2Type(level2Type); - libraryService.updateById(libraryElement); - } - BeanUtils.copyProperties(libraryElement,collectionElement); - } - break; - } - } else { - log.error("designType cannot be empty"); - throw new BusinessException("element source type cannot be empty!"); - } - }else { - return null; - } - return collectionElement; - } - - @Override - public List getByProjectId(Long projectId) { - QueryWrapper qw = new QueryWrapper<>(); - qw.lambda().eq(CollectionElement::getProjectId, projectId); - List collectionElementList = collectionElementMapper.selectList(qw); - if (CollectionUtils.isEmpty(collectionElementList)) { - return new ArrayList<>(); - } - return collectionElementList; - } - - private Generate setGenerate(Long userId,String timeZone){ - Generate generate = new Generate(); - generate.setAccountId(userId); - generate.setLevel1Type(CollectionLevel1TypeEnum.PRINT_BOARD.getRealName()); - generate.setGenerateType("synthesis"); -// generate.setModelName("Image Synthesis Model"); - generate.setCreateDate(DateUtil.getByTimeZone(timeZone)); - return generate; - } - - private GenerateDetail setGenerateDetail(Long generateId, String url, String timeZone){ - GenerateDetail generateDetail = new GenerateDetail(); - generateDetail.setGenerateId(generateId); - generateDetail.setUrl(url); - String md5; - try { - md5 = MD5Utils.encryptFile(minioUtil.download(url)); - } catch (Exception e){ - throw new RuntimeException(e); - }/*catch (MinioException | IOException e) { - throw new RuntimeException(e); - }*/ - // 通过MD5来确认当前图片是否有被like过,避免重复like - List> libraryIds = generateDetailMapper.getLibraryIdThroughMD5(md5, CollectionLevel1TypeEnum.PRINT_BOARD.getRealName()); - if (libraryIds.isEmpty()){ - generateDetail.setIsLike((byte) 0); - }else { - generateDetail.setIsLike((byte) 1); - generateDetail.setLibraryId(libraryIds.get(0).get("library_id")); - } - generateDetail.setMd5(md5); - generateDetail.setCreateDate(LocalDateTime.now()); - - return generateDetail; - } - - // 对于上传图片或者从library选择的图片进行图片分割 - public List selectedImageSeg(List files, Long id, String type, String sourceType) { - Long accountId = UserContext.getUserHolder().getId(); - List resp = new ArrayList<>(); - List imageDates = new ArrayList<>(); - - boolean isUploadMode = !files.isEmpty(); - Library library = null; - CollectionElement collectionElement = null; - - // 判断是否是上传的图片 - if (isUploadMode) { - String objectName = accountId + "/ImageSegment/input"; - for (MultipartFile file : files) { - String md5 = MD5Utils.encryptFile(file); - String segmentedResult = redisUtil.getFromString(RedisUtil.IMAGE_SEGMENTATION + type + ":" + md5); - // 判断上传的图片是否有已分割的数据 - if (StringUtil.isNullOrEmpty(segmentedResult)) { - String path = minioUtil.upload(userBucketName, objectName, file); - ImageSegmentation.ImageDate imageDate = new ImageSegmentation().new ImageDate(); - imageDate.setImage_url(path); - imageDate.setImage_type(type); - imageDates.add(imageDate); - } else { - ImageSegmentation.ImageDate imageData = JSONObject.parseObject(segmentedResult, ImageSegmentation.ImageDate.class); - resp.add(createCollectionElementVO(accountId, null, null, imageData.getImage_url(), imageData.getClothing_url())); - } - } - } else if (Objects.nonNull(id) && sourceType.equals("Library")) { - library = libraryService.getById(id); - // 判断从library中选择的图片是否有分割数据 - if (Objects.isNull(library)) { - throw new BusinessException("library.not.found"); - } - if (StringUtil.isNullOrEmpty(library.getSegmentedData())) { - ImageSegmentation.ImageDate imageDate = new ImageSegmentation().new ImageDate(); - imageDate.setImage_url(library.getUrl()); - imageDate.setImage_type(type); - imageDates.add(imageDate); - } else { - List restoredList = Arrays.asList(library.getSegmentedData().split(",")); - resp.add(createCollectionElementVO(accountId, id, library.getLevel1Type(), library.getUrl(), restoredList)); - } - } else if (Objects.nonNull(id) && sourceType.equals("Upload")) { - collectionElement = collectionElementMapper.selectById(id); - // 判断id对应的数据是否存在 - if (Objects.isNull(collectionElement)) { - throw new BusinessException("get.file.failed"); - } - // 上传的图片分割数据没存(原因:上传的数据一般不会被再次使用,存储意义不大;上传存储表中数据复杂,添加字段,浪费多) - ImageSegmentation.ImageDate imageDate = new ImageSegmentation().new ImageDate(); - imageDate.setImage_url(collectionElement.getUrl()); - imageDate.setImage_type(type); - imageDates.add(imageDate); - } - - // 处理需要分割的图片 - if (!imageDates.isEmpty()) { - // 准备图片分割的参数 - ImageSegmentation imageSegmentation = new ImageSegmentation(); - imageSegmentation.setUser_id(accountId); - imageSegmentation.setImage_data(imageDates); - // 图片分割 - List segmented = pythonService.imageSegmentation(imageSegmentation); - // 处理图片分割结果 - for (ImageSegmentation.ImageDate imageData : segmented) { - if (isUploadMode) { - // 上传的图片需要添加到redis中存一天 - String key = RedisUtil.IMAGE_SEGMENTATION + type + ":" + - MD5Utils.encryptFile - (minioUtil.getPreSignedUrl - (imageData.getImage_url(), CommonConstant.MINIO_IMAGE_EXPIRE_TIME), false); - redisUtil.addToString(key, new Gson().toJson(imageData), CommonConstant.GENERATE_RESULT_EXPIRE_TIME); - resp.add(createCollectionElementVO(accountId, null, null, imageData.getImage_url(), imageData.getClothing_url())); - } else if (sourceType.equals("Library")){ - // 从library中选择的图片需要更新数据库中对应图片的分割数据 - String segmentedData = String.join(",", imageData.getClothing_url()); - library.setSegmentedData(segmentedData); - library.setUpdateDate(new Date()); - libraryService.updateById(library); - resp.add(createCollectionElementVO(accountId, id, library.getLevel1Type(), library.getUrl(), imageData.getClothing_url())); - }else { - resp.add(createCollectionElementVO(accountId, id, collectionElement.getLevel1Type(), collectionElement.getUrl(), imageData.getClothing_url())); - } - } - } - return resp; - } - - // 准备返回数据 - private CollectionElementVO createCollectionElementVO(Long accountId, Long id, String level1Type, - String imageUrl, List clothingUrls) { - CollectionElementVO vo = new CollectionElementVO(); - vo.setAccountId(accountId); - vo.setId(id); - vo.setLevel1Type(level1Type); - vo.setUrl(minioUtil.getPreSignedUrl(imageUrl, CommonConstant.MINIO_IMAGE_EXPIRE_TIME)); - vo.setMinIOPath(imageUrl); - - List segUrls = new ArrayList<>(); - for (String seg : clothingUrls) { - segUrls.add(minioUtil.getPreSignedUrl(seg, CommonConstant.MINIO_IMAGE_EXPIRE_TIME)); - } - vo.setSegmentedImages(segUrls); - return vo; - } - - public void updateElementLevel2Type(Long elementId, String level2Type){ - CollectionElement collectionElement = baseMapper.selectById(elementId); - if (Objects.nonNull(collectionElement)){ - collectionElement.setLevel2Type(level2Type); - collectionElement.setUpdateDate(new Date()); - baseMapper.updateById(collectionElement); - } - } - -} +package com.ai.da.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import com.ai.da.common.config.FileProperties; +import com.ai.da.common.config.exception.BusinessException; +import com.ai.da.common.constant.CommonConstant; +import com.ai.da.common.context.UserContext; +import com.ai.da.common.enums.*; +import com.ai.da.common.response.ResultEnum; +import com.ai.da.common.utils.*; +import com.ai.da.mapper.primary.CollectionElementMapper; +import com.ai.da.mapper.primary.GenerateDetailMapper; +import com.ai.da.mapper.primary.GenerateMapper; +import com.ai.da.mapper.primary.entity.*; +import com.ai.da.model.dto.*; +import com.ai.da.model.enums.ModelType; +import com.ai.da.model.enums.Module; +import com.ai.da.model.enums.Sex; +import com.ai.da.model.vo.*; +import com.ai.da.python.PythonService; +import com.ai.da.python.vo.DesignPythonItem; +import com.ai.da.python.vo.ImageSegmentation; +import com.ai.da.service.*; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.google.common.collect.Lists; +import com.google.gson.Gson; +import io.netty.util.internal.StringUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import java.io.File; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 服务实现类 + * + * @author yanglei + * @since 2022-09-30 + */ +@Slf4j +@Service +public class CollectionElementServiceImpl extends ServiceImpl implements CollectionElementService { + @Resource + private CollectionElementMapper collectionElementMapper; + @Resource + private FileProperties fileProperties; + @Resource + private PanToneService panToneService; + @Resource + private PythonService pythonService; + @Resource + private LibraryService libraryService; + @Resource + private SysFileService sysFileService; + @Resource + private GenerateMapper generateMapper; + @Resource + private GenerateDetailMapper generateDetailMapper; + @Resource + private LibraryModelPointService libraryModelPointService; + @Resource + private TCollectionElementRelationService tCollectionElementRelationService; + @Resource + private MinioUtil minioUtil; + + @Value("${minio.bucketName.collectionElement}") + private String collectionElement; + @Value("${minio.bucketName.gradient}") + private String gradientBucketName; + @Value("${minio.bucketName.users}") + private String userBucketName; + @Resource + private RedisUtil redisUtil; + +// @Resource +// private RedisUtil redisUtil; + @Resource + private EmailService emailService; + + @Transactional(rollbackFor = Exception.class) + @Override + public CollectionElementVO upload(CollectionElementUploadDTO uploadDTO) { + long start = System.currentTimeMillis(); + //用户信息 + AuthPrincipalVo userInfo = UserContext.getUserHolder(); + CollectionLevel1TypeEnum level1TypeEnum = CollectionLevel1TypeEnum.uploadOf(uploadDTO.getLevel1Type()); + String name; + if (Objects.isNull(level1TypeEnum) && !uploadDTO.getLevel1Type().equals(Module.deReconstruction.getValue())) { + throw new BusinessException("unknown.parameter.level1Type"); + } else if (uploadDTO.getLevel1Type().equals(Module.deReconstruction.getValue())){ + name = "Construction"; + } else { + name = level1TypeEnum.getRealName(); + } + String objectName = userInfo.getId() + "/" + name; + String path = minioUtil.upload(collectionElement, objectName, uploadDTO.getFile()); + + String level2Type = null; + if (uploadDTO.getLevel1Type().equals(CollectionLevel1TypeEnum.SKETCH_BOARD.getRealName())){ + if (StringUtil.isNullOrEmpty(uploadDTO.getGender())) { + throw new BusinessException("gender.cannot.be.empty"); + } + level2Type = pythonService.getClothCategory(path,uploadDTO.getGender()); + }else if (!StringUtil.isNullOrEmpty(uploadDTO.getLevel2Type())){ + level2Type = uploadDTO.getLevel2Type(); + } + + //保存element元素 + CollectionElement collectionElement = resolveData(uploadDTO, userInfo, path, level2Type); + saveOne(collectionElement); +// if (!StringUtils.isEmpty(uploadDTO.getMoodboardPosition())) { +// redisUtil.saveMoodboardPosition(collectionElement.getId(), uploadDTO.getMoodboardPosition()); +// } + CollectionElementVO collectionElementVO = CopyUtil.copyObject(collectionElement, CollectionElementVO.class); + collectionElementVO.setMinIOPath(collectionElementVO.getUrl()); + collectionElementVO.setUrl(minioUtil.getPreSignedUrl(collectionElementVO.getUrl(), 24 * 60)); + collectionElementVO.setDesignType(DesignTypeEnum.COLLECTION.getRealName()); + + long end = System.currentTimeMillis(); + double floor = Math.floor((double) (end - start) / 1000); + log.info("本次图片上传耗时:{} 毫秒", end - start); + Long incrementCount = redisUtil.getIncrementCount(RedisUtil.UPLOAD_TIMEOUT_REMINDER_COUNTER); + if (floor > 5 && incrementCount < 3) { + // 邮件通知 一天最多通知3次 + log.info("上传超过5秒,发送邮件通知"); + SendEmailUtil.uploadTimeoutReminder(userInfo.getUsername(), String.valueOf(((end - start) / 1000))); +// emailService.uploadTimeoutReminder(userInfo.getUsername(), String.valueOf(((end - start) / 1000))); + redisUtil.increaseCount(RedisUtil.UPLOAD_TIMEOUT_REMINDER_COUNTER); + } + return collectionElementVO; + } + + private String calculateFileUrl(CollectionLevel1TypeEnum level1TypeEnum, Long userId) { + String rootPath = fileProperties.getSys().getPath(); + String day = DateUtil.dateToStr(new Date(), DateUtil.YYYYMM); + return rootPath + day + "/" + "userFile" + "/collection" + + "/" + level1TypeEnum.getRealName() + "/" + userId + "/" + UUID.randomUUID().toString(); + } + + private CollectionElement resolveData(CollectionElementUploadDTO uploadDTO, AuthPrincipalVo userInfo, File file) { + CollectionElement element = CopyUtil.copyObject(uploadDTO, CollectionElement.class); + element.setAccountId(userInfo.getId()); + element.setCollectionId(0L); + element.setHasPin((byte) 0); + String pictureCollectonName = file.getName(); + //获取图片后缀 + String suffix = pictureCollectonName.substring(pictureCollectonName.lastIndexOf(".")); + //获取图片前缀 + String prefix = pictureCollectonName.substring(0, pictureCollectonName.lastIndexOf(".")); + element.setName(DateUtil.dateToStr(new Date(), DateUtil.YYYY_MM_DD)); + element.setUrl(file.getAbsolutePath()); + //按时区计算 + element.setCreateDate(DateUtil.getByTimeZone(uploadDTO.getTimeZone())); + String linuxDomain = fileProperties.getLinuxDomain(); + if (!StringUtils.isEmpty(linuxDomain)) { + //linux 系统 + String oldPath = fileProperties.getSys().getPath(); + element.setUrl(file.getAbsolutePath().replace(oldPath, linuxDomain)); + } + return element; + } + + private CollectionElement resolveData(CollectionElementUploadDTO uploadDTO, AuthPrincipalVo userInfo, String path, String level2Type) { + CollectionElement element = CopyUtil.copyObject(uploadDTO, CollectionElement.class); + element.setAccountId(userInfo.getId()); + element.setCollectionId(0L); + element.setHasPin((byte) 0); +// String pictureCollectonName = fileName; +// //获取图片后缀 +// String suffix = pictureCollectonName.substring(pictureCollectonName.lastIndexOf(".")); +// //获取图片前缀 +// String prefix = pictureCollectonName.substring(0,pictureCollectonName.lastIndexOf(".")); + String originalFilename = uploadDTO.getFile().getOriginalFilename(); + if (originalFilename != null && originalFilename.contains(".")) { + // 如果文件名包含点号,则去除最后一个点及其后面的内容 + int lastDotIndex = originalFilename.lastIndexOf("."); + if (lastDotIndex != -1) { + originalFilename = originalFilename.substring(0, lastDotIndex); + } + } + element.setName(originalFilename); + element.setUrl(path); + //按时区计算 + element.setCreateDate(DateUtil.getByTimeZone(uploadDTO.getTimeZone())); + element.setLevel2Type(level2Type); + if (Objects.nonNull(uploadDTO.getProjectId())){ + element.setProjectId(uploadDTO.getProjectId()); + } +// String linuxDomain = fileProperties.getLinuxDomain(); +// if (!StringUtils.isEmpty(linuxDomain)) { +// //linux 系统 +// String oldPath = fileProperties.getSys().getPath(); +// element.setUrl(file.getAbsolutePath().replace(oldPath, linuxDomain)); +// } + return element; + } + + @Override + public void delete(Long id) { + CollectionElement collectionElement = selectById(id); + if (Objects.isNull(collectionElement)) { + throw new BusinessException("collectionElement.not.found"); + } + minioUtil.deleteObject(collectionElement.getUrl()); + collectionElementMapper.deleteById(id); +// if (!FileUtil.delete(collectionElement.getUrl())) { +// throw new BusinessException("file deletion failed! "); +// } + } + + @Override + public void batchDelete(List ids) { + if (CollectionUtils.isEmpty(ids)) { + return; + } +// QueryWrapper queryWrapper = new QueryWrapper<>(); +// queryWrapper.in("id", ids); +// CollectionElement collectionElement = new CollectionElement(); +// collectionElement.setCollectionId(0L); + collectionElementMapper.deleteBatchIds(ids); + } + + /** 该方法已不再使用 */ + @Deprecated + @Override + public GenerateCollectionItemVO generatePrint(CollectionGeneratePrintDTO generatePrintDTO) { + Long userId = UserContext.getUserHolder().getId(); + String url1 = null; + String url2 = null; + CollectionElement element1 = selectById(generatePrintDTO.getSelect1Id()); + if (Objects.isNull(element1)) { + Library library1 = libraryService.getById(generatePrintDTO.getSelect1Id()); + if (Objects.isNull(library1)) { + throw new BusinessException("select1.file.does.not.exist"); + } + url1 = library1.getUrl(); + } else { + url1 = element1.getUrl(); + } + CollectionElement element2 = selectById(generatePrintDTO.getSelect2Id()); + if (Objects.isNull(element2)) { + Library library2 = libraryService.getById(generatePrintDTO.getSelect2Id()); + if (Objects.isNull(library2)) { + throw new BusinessException("select2.file.does.not.exist"); + } + url2 = library2.getUrl(); + } else { + url2 = element2.getUrl(); + } + List printPath = Arrays.asList(url1, url2); + //调取python 接口 + String generateUrl = pythonService.generatePrint(printPath,userId); + if (StringUtils.isEmpty(generateUrl)) { + throw new BusinessException("generate.interface.exception"); + } + + // 保存合成信息到generate表 + Generate generate = setGenerate(userId, generatePrintDTO.getTimeZone()); + generateMapper.insert(generate); + + // 保存合成后的信息到generateDetail + GenerateDetail generateDetail = setGenerateDetail(generate.getId(), generateUrl, generatePrintDTO.getTimeZone()); + generateDetailMapper.insert(generateDetail); + +// CollectionElement element = resolveData(generateUrl, generatePrintDTO.getTimeZone(), userId); +// if (!this.save(element)) { +// throw new BusinessException("save.collectionElement.failed"); +// } +// CollectionGeneratePrintVO collectionGeneratePrint = CopyUtil.copyObject(element, CollectionGeneratePrintVO.class); +// collectionGeneratePrint.setUrl(minioUtil.getPresignedUrl(generateUrl, 24 * 60)); +// collectionGeneratePrint.setDesignType(DesignTypeEnum.COLLECTION.getRealName()); +// return collectionGeneratePrint; + return new GenerateCollectionItemVO(generateDetail.getId(), + minioUtil.getPreSignedUrl(generateUrl, 24 * 60), + generateDetail.getIsLike().equals((byte) 0) ? Boolean.FALSE : Boolean.TRUE); + } + + @Override + public Boolean savePrint(CollectionSavePrintDTO savePrintDTO) { + //用户信息 + List elements = listByIds(savePrintDTO.getPrintId()); + if (CollectionUtils.isEmpty(elements)) { + throw new BusinessException("collectionElements.not.found"); + } + return saveLibraryByCollectionElement(elements, savePrintDTO.getTimeZone()); + } + + @Override + public Boolean saveLibraryByCollectionElement(List elements, String timeZone) { + if (CollectionUtils.isEmpty(elements)) { + return Boolean.TRUE; + } + //获取已存在相同的library + List md5List = elements.stream().map(CollectionElement::getMd5).collect(Collectors.toList()); + List existsLibrarys = libraryService.getByMD5List(md5List); + if (!CollectionUtils.isEmpty(existsLibrarys)) { + //去重 + List existsMd5Lists = existsLibrarys.stream().map(Library::getMd5).collect(Collectors.toList()); + elements = elements.stream().filter(element -> !existsMd5Lists.contains(element.getMd5())).collect(Collectors.toList()); + } + if (CollectionUtils.isEmpty(elements)) { + //都是重复的 + return Boolean.TRUE; + } + String name = DateUtil.dateToStr(new Date(), DateUtil.YYYY_MM_DD); + List libraryList = CopyUtil.copyList(elements, Library.class, (o, d) -> { + d.setCreateDate(DateUtil.getByTimeZone(timeZone)); + d.setName(name); + d.setId(null); + }); + if (!libraryService.saveBatch(libraryList)) { + throw new BusinessException("batch.save.libraryList.failed"); + } + return Boolean.TRUE; + } + + @Override + public Boolean saveLibraryByCollectionElement(List elements, String timeZone, String modelSex) { + if (CollectionUtils.isEmpty(elements)) { + return Boolean.TRUE; + } + //获取已存在相同的library + List md5List = elements.stream().map(CollectionElement::getMd5).collect(Collectors.toList()); + List existsLibrarys = libraryService.getByMD5List(md5List); + if (!CollectionUtils.isEmpty(existsLibrarys)) { + //去重 + List existsMd5Lists = existsLibrarys.stream().map(Library::getMd5).collect(Collectors.toList()); + elements = elements.stream().filter(element -> !existsMd5Lists.contains(element.getMd5())).collect(Collectors.toList()); + } + if (CollectionUtils.isEmpty(elements)) { + //都是重复的 + return Boolean.TRUE; + } + String name = DateUtil.dateToStr(new Date(), DateUtil.YYYY_MM_DD); + List libraryList = CopyUtil.copyList(elements, Library.class, (o, d) -> { + if (d.getLevel1Type().equals(LibraryLevel1TypeEnum.SKETCH_BOARD.getRealName())) { + d.setLevel3Type(modelSex); +// try { +// libraryService.processSketchBoards(d.getUrl(), d.getLevel2Type()); +// }catch (Exception e) { +// // TODO:暂不处理 +// } + } + d.setCreateDate(DateUtil.getByTimeZone(timeZone)); + d.setName(name); + d.setId(null); + }); + if (!libraryService.saveBatch(libraryList)) { + throw new BusinessException("batch.save.libraryList.failed"); + } + return Boolean.TRUE; + } + + private CollectionElement resolveData(String path, String timeZone, Long userId){ +// File file = new File(path); + + String name = path.substring(path.lastIndexOf("/") + 1, path.lastIndexOf(".")); + CollectionElement element = new CollectionElement(); + element.setAccountId(userId); + element.setCollectionId(0L); + element.setName(name); + element.setLevel1Type(CollectionLevel1TypeEnum.PRINT_BOARD.getRealName()); + element.setUrl(path); + element.setHasPin((byte) 0); + try { + element.setMd5(MD5Utils.encryptFile(minioUtil.download(path))); + }catch (Exception e){ + throw new RuntimeException(e); + } + /* catch (MinioException | IOException e) { + throw new RuntimeException(e); + }*/ + + //按时区计算 + element.setCreateDate(DateUtil.getByTimeZone(timeZone)); + return element; + } + + @Override + public ValidateElementVO validateElement(DesignCollectionDTO designDTO) { + ValidateElementVO elementVO = CopyUtil.copyObject(designDTO, ValidateElementVO.class); + List colorBoards = elementVO.getColorBoards(); + for (CollectionColorDTO colorBoard : colorBoards) { + if (Objects.nonNull(colorBoard.getGradient())) { + String colorImg = colorBoard.getGradient().getColorImg(); + String[] parts = colorImg.split(","); + String imageType = parts[0].split("/")[1].split(";")[0]; + String base64Data = parts[1]; + String gradientMinioUrl = minioUtil.uploadImageFromBase64(gradientBucketName, base64Data, imageType); + colorBoard.setGradientMinioUrl(gradientMinioUrl); + colorBoard.getGradient().setColorImg(null); + colorBoard.setGradientString(JSON.toJSONString(colorBoard.getGradient())); + } + } + elementVO.setColorBoards(colorBoards); + List usedElementIds = elementVO.getUsedElementIds(); + List libraryCollectionElements = elementVO.getLibraryCollectionElements(); + List generateCollectionElements = elementVO.getGenerateCollectionElements(); + //校验moodboard + if (CollectionUtil.isNotEmpty(designDTO.getMoodBoards())) { + //校验designType + validateDesignType(designDTO.getMoodBoards(), "moodBoards"); + List moodBoardIds = designDTO.getMoodBoards().stream() + .filter(f -> f.getDesignType().equals(DesignTypeEnum.COLLECTION.getRealName())) + .map(DesignCollectionElementDTO::getId) + .collect(Collectors.toList()); + if (!CollectionUtils.isEmpty(moodBoardIds)) { + List MoodBoardElements = collectionElementMapper.selectBatchIds(moodBoardIds); + if (CollectionUtil.isEmpty(MoodBoardElements) || MoodBoardElements.size() != moodBoardIds.size()) { + throw new BusinessException("get.moodBoards.data.is.mismatch"); + } + elementVO.setMoodBoardElements(MoodBoardElements); + usedElementIds.addAll(moodBoardIds); + } + //library + List libraryIds = designDTO.getMoodBoards().stream() + .filter(f -> f.getDesignType().equals(DesignTypeEnum.LIBRARY.getRealName())) + .map(DesignCollectionElementDTO::getId) + .collect(Collectors.toList()); + if (!CollectionUtils.isEmpty(libraryIds)) { + List librarys = libraryService.getByIds(libraryIds); + //不校验了防止用户在library删除 对应不上 + if (CollectionUtil.isNotEmpty(librarys)) { + libraryCollectionElements.addAll(covertLibrarysToCollections(librarys, null)); + } + } + // generate + List generateIds = designDTO.getMoodBoards().stream() + .filter(o -> o.getDesignType().equals((DesignTypeEnum.GENERATE.getRealName()))) + .map(DesignCollectionElementDTO::getId) + .collect((Collectors.toList())); + if (CollectionUtil.isNotEmpty(generateIds)) { + List generateDetailList = generateDetailMapper.selectBatchIds(generateIds); + if (CollectionUtil.isNotEmpty(generateDetailList)) { + generateCollectionElements.addAll(covertGeneratesToCollections(generateDetailList, null)); + } + } + } + if (CollectionUtil.isNotEmpty(designDTO.getPrintBoards())) { + //校验designType + validateDesignType(CopyUtil.copyList(designDTO.getPrintBoards(), DesignCollectionElementDTO.class), "printBoards"); + List printBoardIds = designDTO.getPrintBoards().stream() + .filter(f -> f.getDesignType().equals(DesignTypeEnum.COLLECTION.getRealName())) + .map(DesignCollectionPrintElementDTO::getId) + .collect(Collectors.toList()); + if (!CollectionUtils.isEmpty(printBoardIds)) { + //校验printboard + List printBoardElements = collectionElementMapper.selectBatchIds(printBoardIds); + if (CollectionUtil.isEmpty(printBoardElements) || printBoardElements.size() != printBoardIds.size()) { + throw new BusinessException("get.printBoards.data.is.mismatch"); + } + elementVO.setPrintBoardElements(printBoardElements); + usedElementIds.addAll(printBoardIds); + } + //library + List libraryIds = designDTO.getPrintBoards().stream() + .filter(f -> f.getDesignType().equals(DesignTypeEnum.LIBRARY.getRealName())) + .map(DesignCollectionPrintElementDTO::getId) + .collect(Collectors.toList()); + if (!CollectionUtils.isEmpty(libraryIds)) { + List librarys = libraryService.getByIds(libraryIds); + //不校验了防止用户在library删除 对应不上 + if (CollectionUtil.isNotEmpty(librarys)) { + Map idToMap = designDTO.getPrintBoards() + .stream() + .collect(Collectors.toMap(DesignCollectionPrintElementDTO::getId, v -> v)); + libraryCollectionElements.addAll(covertLibrarysToPrintCollections(librarys, idToMap)); + } + } + // generate + List generateIds = designDTO.getPrintBoards().stream() + .filter(o -> o.getDesignType().equals((DesignTypeEnum.GENERATE.getRealName()))) + .map(DesignCollectionPrintElementDTO::getId) + .collect((Collectors.toList())); + if (CollectionUtil.isNotEmpty(generateIds)) { + List generateDetailList = generateDetailMapper.selectBatchIds(generateIds); + if (CollectionUtil.isNotEmpty(generateDetailList)) { + Map idToMap = designDTO.getPrintBoards() + .stream() + .collect(Collectors.toMap(DesignCollectionPrintElementDTO::getId, v -> v)); + generateCollectionElements.addAll(covertGeneratesToPrintCollections(generateDetailList, idToMap)); + } + } + } + if (CollectionUtil.isNotEmpty(designDTO.getSketchBoards())) { + //校验PIN是否满足 上衣或者下衣必须不超过8 + long topNum = 0; + long bottomNum = 0; + long outerwearNum = 0; + if (designDTO.getModelSex().equals(Sex.FEMALE.getValue())) { + topNum= designDTO.getSketchBoards().stream() + .filter(skecth -> skecth.getIsPin() == 1 + && DesignPythonItem.DRESS_BLOUSE.contains(skecth.getLevel2Type())).count(); + bottomNum = designDTO.getSketchBoards().stream() + .filter(skecth -> skecth.getIsPin() == 1 + && DesignPythonItem.SKIRT_TROUSERS.contains(skecth.getLevel2Type())).count(); + }else if (designDTO.getModelSex().equals(Sex.MALE.getValue())) { + topNum= designDTO.getSketchBoards().stream() + .filter(skecth -> skecth.getIsPin() == 1 + && DesignPythonItem.TOPS.contains(skecth.getLevel2Type())).count(); + bottomNum = designDTO.getSketchBoards().stream() + .filter(skecth -> skecth.getIsPin() == 1 + && DesignPythonItem.BOTTOMS.contains(skecth.getLevel2Type())).count(); + } + outerwearNum = designDTO.getSketchBoards().stream() + .filter(skecth -> skecth.getIsPin() == 1 + && DesignPythonItem.OUTWEAR.contains(skecth.getLevel2Type())).count(); + if (topNum > 8 || bottomNum > 8 || outerwearNum > 8) { + throw new BusinessException("the.number.of.PIN.top.or.bottom.or.outerwear.sketchBoard.cannot.be.more.than.8", ResultEnum.PROMPT.getCode()); + } + //校验designType + Boolean result = designDTO.getSketchBoards().stream() + .filter(mood -> StringUtils.isEmpty(mood.getDesignType())) + .findFirst().isPresent(); + if (result) { + throw new BusinessException("sketchBoards.designType.cannot.be.empty"); + } + + List sketchBoardIds = designDTO.getSketchBoards().stream() + .filter(f -> f.getDesignType().equals(DesignTypeEnum.COLLECTION.getRealName())) + .map(CollectionSketchDTO::getSketchBoardId) + .collect(Collectors.toList()); + if (!CollectionUtils.isEmpty(sketchBoardIds)) { + //校验sketchBoard + List sketchBoardElements = collectionElementMapper.selectBatchIds(sketchBoardIds); + if (CollectionUtil.isEmpty(sketchBoardElements) || sketchBoardElements.size() != sketchBoardIds.size()) { + throw new BusinessException("get.sketchBoards.data.is.mismatch"); + } + elementVO.setSketchBoardElements(sketchBoardElements); + usedElementIds.addAll(sketchBoardIds); + } + //library + List libraryIds = designDTO.getSketchBoards().stream() + .filter(f -> f.getDesignType().equals(DesignTypeEnum.LIBRARY.getRealName())) + .map(CollectionSketchDTO::getSketchBoardId) + .collect(Collectors.toList()); + if (!CollectionUtils.isEmpty(libraryIds)) { + List librarys = libraryService.getByIds(libraryIds); + //不校验了防止用户在library删除 对应不上 + if (CollectionUtil.isNotEmpty(librarys)) { + Map idToMap = designDTO.getSketchBoards() + .stream() + .collect(Collectors.toMap(CollectionSketchDTO::getSketchBoardId, v -> v)); + libraryCollectionElements.addAll(covertLibrarysToCollections(librarys, idToMap)); + } + } + // generate + List generateIds = designDTO.getSketchBoards().stream() + .filter(o -> o.getDesignType().equals((DesignTypeEnum.GENERATE.getRealName()))) + .map(CollectionSketchDTO::getSketchBoardId) + .collect((Collectors.toList())); + if (CollectionUtil.isNotEmpty(generateIds)) { + List generateDetailList = generateDetailMapper.selectBatchIds(generateIds); + if (CollectionUtil.isNotEmpty(generateDetailList)) { + Map idToMap = designDTO.getSketchBoards() + .stream() + .collect(Collectors.toMap(CollectionSketchDTO::getSketchBoardId, v -> v)); + generateCollectionElements.addAll(covertGeneratesToCollections(generateDetailList, idToMap)); + } + } + } + //校验marketingSketch + // 2023.12版本去掉了这个入参 +/* if (CollectionUtil.isNotEmpty(designDTO.getMarketingSketchs())) { + //校验designType + validateDesignType(designDTO.getMarketingSketchs(),"marketingSketchs"); + List printBoardIds = designDTO.getMarketingSketchs().stream() + .filter(f ->f.getDesignType().equals(DesignTypeEnum.COLLECTION.getRealName())) + .map(DesignCollectionElementDTO::getId) + .collect(Collectors.toList()); + if(!CollectionUtils.isEmpty(printBoardIds)){ + List marketingSketchElements = collectionElementMapper.selectBatchIds(printBoardIds); + Assert.isTrue(CollectionUtil.isNotEmpty(marketingSketchElements) + && marketingSketchElements.size() == printBoardIds.size(), "get marketingSketch data is mismatch"); + elementVO.setMarketingSketchElements(marketingSketchElements); + usedElementIds.addAll(printBoardIds); + } + //library + List libraryIds = designDTO.getMarketingSketchs().stream() + .filter(f ->f.getDesignType().equals(DesignTypeEnum.LIBRARY.getRealName())) + .map(DesignCollectionElementDTO::getId) + .collect(Collectors.toList()); + if(!CollectionUtils.isEmpty(libraryIds)){ + List librarys = libraryService.getByIds(libraryIds); + //不校验了防止用户在library删除 对应不上 + if(CollectionUtil.isNotEmpty(librarys)){ + libraryCollectionElements.addAll(covertLibrarysToCollections(librarys,null)); + } + } + }*/ + //校验控制生成类型 + SingleOverallEnum singleOverall = SingleOverallEnum.of(designDTO.getSingleOverall()); + if (Objects.isNull(singleOverall)) { + log.error("未知singleOverall param:{}", designDTO.getSingleOverall()); + log.info("入参 designDTO => {}", designDTO); + throw new BusinessException("unknown.parameter.singleOverall"); + } + if (SingleOverallEnum.SINGLE.equals(singleOverall)) { + SwitchCategoryEnum switchCategory = SwitchCategoryEnum.of(designDTO.getSwitchCategory()); + if (Objects.isNull(switchCategory)) { + log.error("未知switchCategory:{}", designDTO.getSwitchCategory()); + log.info("入参 designDTO => {}", designDTO); + throw new BusinessException("unknown.parameter.switchCategory"); + } + } + // 校验模特 + if (!CollectionUtils.isEmpty(designDTO.getMannequins())) { + List designLibraryModelPointVOList = new ArrayList<>(); + for (MannequinDTO mannequin : designDTO.getMannequins()) { + if (mannequin.getType().equals("System")) { + SysFileVO byId = sysFileService.getById(mannequin.getId()); +// if (!StringUtils.isEmpty(byId.getLevel3Type()) && byId.getLevel2Type().equals("Female")) { +// elementVO.setStyle(byId.getLevel3Type()); +// } + LibraryModelPoint modelPoint = libraryModelPointService.getByRelationId(byId.getId(), mannequin.getType()); + designLibraryModelPointVOList.add(calculateTemplatePointTemplate(modelPoint, 700, 320, byId.getUrl())); + }else { + Library byId = libraryService.getById(mannequin.getId()); + LibraryModelPoint modelPoint = libraryModelPointService.getByRelationId(byId.getId(), mannequin.getType()); + designLibraryModelPointVOList.add(calculateTemplatePointTemplate(modelPoint, byId.getHigh(), byId.getWidth(), byId.getUrl())); + } + } + elementVO.setMannequins(designLibraryModelPointVOList); + }else { + if (!StringUtils.isEmpty(designDTO.getModelType())) { + if (designDTO.getModelType().equals(ModelType.LIBRARY.getValue())) { + Library byId = libraryService.getById(designDTO.getTemplateId()); + LibraryModelPoint modelPoint = libraryModelPointService.getByRelationId(byId.getId(), designDTO.getModelType()); + elementVO.setDesignLibraryModelPoint(calculateTemplatePointTemplate(modelPoint, byId.getHigh(), byId.getWidth(), byId.getUrl())); + } else if (designDTO.getModelType().equals(ModelType.SYSTEM.getValue())) { + SysFileVO byId = sysFileService.getById(designDTO.getTemplateId()); + if (!StringUtils.isEmpty(byId.getLevel3Type()) && byId.getLevel2Type().equals("Female")) { + elementVO.setStyle(byId.getLevel3Type()); + } + LibraryModelPoint modelPoint = libraryModelPointService.getByRelationId(byId.getId(), designDTO.getModelType()); + elementVO.setDesignLibraryModelPoint(calculateTemplatePointTemplate(modelPoint, 700, 320, byId.getUrl())); + } + } + } + elementVO.setModelSex(designDTO.getModelSex()); + elementVO.setRequestIdList(designDTO.getRequestIdList()); + if (null != designDTO.getDesignNum()) { + elementVO.setDesignNum(designDTO.getDesignNum()); + }else { + elementVO.setDesignNum(8); + } + if (null != designDTO.getBrandId()) { + elementVO.setBrandId(designDTO.getBrandId()); + elementVO.setBrandScale(designDTO.getBrandScale()); + } + return elementVO; + } + + @Override + public DesignLibraryModelPointVO calculateTemplatePoint(LibraryModelPoint modelPoint, Integer high, Integer width, String templateUrl) { + DesignLibraryModelPointVO libraryModelPoint = new DesignLibraryModelPointVO(); + libraryModelPoint.setHandLeft(calculateTemplatePointOne(modelPoint.getHandLeft(), high, width)); + libraryModelPoint.setHandRight(calculateTemplatePointOne(modelPoint.getHandRight(), high, width)); + libraryModelPoint.setShoulderLeft(calculateTemplatePointOne(modelPoint.getShoulderLeft(), high, width)); + libraryModelPoint.setShoulderRight(calculateTemplatePointOne(modelPoint.getShoulderRight(), high, width)); + libraryModelPoint.setWaistbandLeft(calculateTemplatePointOne(modelPoint.getWaistbandLeft(), high, width)); + libraryModelPoint.setWaistbandRight(calculateTemplatePointOne(modelPoint.getWaistbandRight(), high, width)); + libraryModelPoint.setTemplateUrl(templateUrl); + return libraryModelPoint; + } + + @Override + public DesignLibraryModelPointVO calculateTemplatePointTemplate(LibraryModelPoint modelPoint, Integer high, Integer width, String templateUrl) { + DesignLibraryModelPointVO libraryModelPoint = new DesignLibraryModelPointVO(); +// LibraryModelPoint template = libraryModelPointService.getById(96L); +// libraryModelPoint.setHandLeft(calculateTemplatePointOne(template.getHandLeft(),752,564)); +// libraryModelPoint.setHandRight(calculateTemplatePointOne(template.getHandRight(),752,564)); +// libraryModelPoint.setShoulderLeft(calculateTemplatePointOne(template.getShoulderLeft(),752,564)); +// libraryModelPoint.setShoulderRight(calculateTemplatePointOne(template.getShoulderRight(),752,564)); +// libraryModelPoint.setWaistbandLeft(calculateTemplatePointOne(template.getWaistbandLeft(),752,564)); +// libraryModelPoint.setWaistbandRight(calculateTemplatePointOne(template.getWaistbandRight(),752,564)); +// libraryModelPoint.setTemplateUrl("aida-mannequins/model_1693218345.2714432.png"); + libraryModelPoint.setHandLeft(calculateTemplatePointOne(modelPoint.getHandLeft(), high, width)); + libraryModelPoint.setHandRight(calculateTemplatePointOne(modelPoint.getHandRight(), high, width)); + libraryModelPoint.setShoulderLeft(calculateTemplatePointOne(modelPoint.getShoulderLeft(), high, width)); + libraryModelPoint.setShoulderRight(calculateTemplatePointOne(modelPoint.getShoulderRight(), high, width)); + libraryModelPoint.setWaistbandLeft(calculateTemplatePointOne(modelPoint.getWaistbandLeft(), high, width)); + libraryModelPoint.setWaistbandRight(calculateTemplatePointOne(modelPoint.getWaistbandRight(), high, width)); + libraryModelPoint.setTemplateUrl(templateUrl); + libraryModelPoint.setRelationId(modelPoint.getRelationId()); + libraryModelPoint.setRelationType(modelPoint.getModelType()); + return libraryModelPoint; + } + + private List calculateTemplatePointOne(String template, Integer high, Integer width) { + List originRatioList = JSON.parseObject(template, List.class); + originRatioList.set(0, originRatioList.get(0).multiply(BigDecimal.valueOf(width))); + originRatioList.set(1, originRatioList.get(1).multiply(BigDecimal.valueOf(high))); + return originRatioList; + } + + private List covertLibrarysToCollections(List libraries, Map idToMap) { + return CopyUtil.copyList(libraries, CollectionElement.class, (o, d) -> { + if (null != idToMap) { + CollectionSketchDTO sketchDTO = idToMap.get(o.getId()); + d.setLevel2Type(sketchDTO.getLevel2Type()); + d.setHasPin(sketchDTO.getIsPin()); + } + }); + } + + private List covertGeneratesToCollections(List generateDetailList, Map idToMap) { + return CopyUtil.copyList(generateDetailList, CollectionElement.class, (o, d) -> { + Generate byId = generateMapper.selectById(o.getGenerateId()); + d.setAccountId(byId.getAccountId()); + d.setLevel1Type(byId.getLevel1Type()); + d.setCreateDate(Date.from(o.getCreateDate().atZone(ZoneId.systemDefault()).toInstant())); + if (null != idToMap) { + CollectionSketchDTO sketchDTO = idToMap.get(o.getId()); + d.setLevel2Type(sketchDTO.getLevel2Type()); + d.setHasPin(sketchDTO.getIsPin()); + } + }); + } + + private List covertLibrarysToPrintCollections(List libraries, Map idToMap) { + return CopyUtil.copyList(libraries, CollectionElement.class, (o, d) -> { + if (null != idToMap) { + DesignCollectionPrintElementDTO printDTO = idToMap.get(o.getId()); + d.setHasPin(printDTO.getIsPin()); + } + }); + } + + private List covertGeneratesToPrintCollections(List generateDetailList, Map idToMap) { + return CopyUtil.copyList(generateDetailList, CollectionElement.class, (o, d) -> { + Generate byId = generateMapper.selectById(o.getGenerateId()); + d.setAccountId(byId.getAccountId()); + d.setLevel1Type(byId.getLevel1Type()); + if (!StringUtils.isEmpty(byId.getLevel2Type())) { + d.setLevel2Type(byId.getLevel2Type()); + } + d.setCreateDate(Date.from(o.getCreateDate().atZone(ZoneId.systemDefault()).toInstant())); + if (null != idToMap) { + DesignCollectionPrintElementDTO printDTO = idToMap.get(o.getId()); + d.setHasPin(printDTO.getIsPin()); + } + }); + } + + private void validateDesignType(List collectionElements, String msg) { + Boolean result = collectionElements.stream().filter(mood -> StringUtils.isEmpty(mood.getDesignType())).findFirst().isPresent(); + if (result) { + throw new BusinessException(msg + ".designType.cannot.be.empty"); + } + } + + @Override + public void editSketchBoardsElement(ValidateElementVO elementVO, List sketchBoards) { + if (CollectionUtil.isNotEmpty(sketchBoards)) { + List collect = sketchBoards.stream().filter(o -> o.getDesignType().equals(DesignTypeEnum.COLLECTION.getRealName())).collect(Collectors.toList()); + collect.forEach(sketchBoard -> { + CollectionElement collectionElement = CopyUtil.copyObject(sketchBoard, CollectionElement.class); + collectionElement.setHasPin(sketchBoard.getIsPin()); + collectionElement.setId(sketchBoard.getSketchBoardId()); + collectionElementMapper.updateById(collectionElement); + }); + List sketchBoardIds = collect.stream().map(CollectionSketchDTO::getSketchBoardId).collect(Collectors.toList()); + if (!CollectionUtils.isEmpty(sketchBoardIds)) { + List sketchBoardElements = collectionElementMapper.selectBatchIds(sketchBoardIds); + elementVO.setSketchBoardElements(sketchBoardElements); + }else { + elementVO.setSketchBoardElements(new ArrayList<>()); + } + } + } + + @Override + public void editPrintBoardsElement(ValidateElementVO elementVO, List printBoards) { + if (CollectionUtil.isNotEmpty(printBoards)) { + List collect = printBoards.stream().filter(o -> o.getDesignType().equals(DesignTypeEnum.COLLECTION.getRealName())).collect(Collectors.toList()); + collect.forEach(printBoard -> { + CollectionElement collectionElement = CopyUtil.copyObject(printBoard, CollectionElement.class); + collectionElement.setHasPin(Objects.isNull(printBoard.getIsPin()) ? 0 : printBoard.getIsPin()); + collectionElement.setId(printBoard.getId()); + collectionElementMapper.updateById(collectionElement); + }); + List printBoardIds = collect.stream().map(DesignCollectionPrintElementDTO::getId).collect(Collectors.toList()); + if (!CollectionUtils.isEmpty(printBoardIds)) { + List printBoardElements = collectionElementMapper.selectBatchIds(printBoardIds); + elementVO.setPrintBoardElements(printBoardElements); + }else { + elementVO.setPrintBoardElements(new ArrayList<>()); + } + } + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void relationCollection(List elementIds, Long collectionId) { + if (CollectionUtils.isEmpty(elementIds) || null == collectionId) { + return; + } + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda().in(CollectionElement::getId, elementIds); + CollectionElement element = new CollectionElement(); + element.setCollectionId(collectionId); + //批量关联 + collectionElementMapper.update(element, queryWrapper); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public List saveColorBoard(List colorBoards, Long collectionId, String timeZone) { + //用户信息 + AuthPrincipalVo userInfo = UserContext.getUserHolder(); + List colorElements = resolveColorData(colorBoards, userInfo, collectionId, timeZone); + if (!this.saveBatch(colorElements)) { + throw new BusinessException("batch.save.colorElements.failed"); + } + return CopyUtil.copyList(colorElements, CollectionElementVO.class); + } + + @Override + public void refreshHistoryData() { + //幂等 + if (!CollectionUtils.isEmpty(tCollectionElementRelationService.getByCollectionId(1083L))) { + return; + } + // 分页数据 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.orderByAsc("id"); + PageQueryBaseVo pageQuery = new PageQueryBaseVo(); + pageQuery.setPage(1); + pageQuery.setSize(200); + while (true) { + IPage page = getBaseMapper().selectPage( + new Page<>(pageQuery.getPage(), pageQuery.getSize()), queryWrapper); + List list = page.getRecords(); + if (CollectionUtils.isEmpty(list)) { + break; + } + + //保存 + List relations = list.stream().map(element -> + TCollectionElementRelation.builder().elementId(element.getId()) + .collectionId(element.getCollectionId()).createDate(new Date()).build()) + .collect(Collectors.toList()); + tCollectionElementRelationService.saveBatch(relations); + pageQuery.setPage(pageQuery.getPage() + 1); + log.info("refreshHistoryData###process###page###" + pageQuery.getPage()); + } + } + + private List resolveColorData(List colorBoards, AuthPrincipalVo userInfo, Long collectionId, String timeZone) { + List elements = Lists.newArrayList(); + colorBoards.forEach(color -> { + CollectionElement element = new CollectionElement(); + element.setAccountId(userInfo.getId()); + element.setCollectionId(collectionId); + if (StringUtils.isEmpty(color.getName())) { + element.setName(null); + } else { + element.setName(color.getId() + "_" + color.getName() + "_" + color.getTcx()); + } + element.setLevel1Type(CollectionLevel1TypeEnum.COLOR_BOARD.getRealName()); + element.setHasPin((byte) 0); + element.setMd5("0"); + element.setColorRgb(color.getRgbValue()); + //按时区计算 + element.setCreateDate(DateUtil.getByTimeZone(timeZone)); + if (Objects.nonNull(color.getGradient())) { + color.getGradient().setColorImg(null); + } + element.setGradientString(JSON.toJSONString(color.getGradient())); + elements.add(element); + }); + return elements; + } + + @Override + public List getByCollectionId(Long collectionId) { +// List elementIds = tCollectionElementRelationService.getByCollectionId(collectionId); +// if (CollectionUtils.isEmpty(elementIds)) { +// return null; +// } + QueryWrapper queryWrapper = new QueryWrapper<>(); +// queryWrapper.in("id", elementIds); + queryWrapper.lambda().eq(CollectionElement::getCollectionId, collectionId); + return collectionElementMapper.selectList(queryWrapper); + } + + @Override + public List getByOnlyCollectionId(Long collectionId) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("collection_id", collectionId); + return collectionElementMapper.selectList(queryWrapper); + } + + private CollectionElement selectById(Long id) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("id", id); + return collectionElementMapper.selectOne(queryWrapper); + } + + private Boolean exists(Long id) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("id", id); + return collectionElementMapper.exists(queryWrapper); + } + + private boolean saveOne(CollectionElement collectionElement) { + if (collectionElementMapper.insert(collectionElement) <= 0) { + throw new BusinessException("save.collectionElement.failed"); + } + return Boolean.TRUE; + } + + @Override + public CollectionElement editLevel2Type(Long elementId, String level2Type, String designType) { + CollectionElement collectionElement = new CollectionElement(); + + if (!Objects.isNull(elementId)) { + if (!StringUtil.isNullOrEmpty(designType)){ + switch (designType){ + case "collection": + collectionElement = collectionElementMapper.selectById(elementId); + if (StringUtil.isNullOrEmpty(collectionElement.getLevel2Type()) || !(collectionElement.getLevel2Type()).equals(level2Type)) { + collectionElement.setLevel2Type(level2Type); + updateById(collectionElement); + } + break; + case "library": + Library libraryElement = libraryService.getById(elementId); + if (!Objects.isNull(libraryElement)) { + if (StringUtil.isNullOrEmpty(libraryElement.getLevel2Type()) || !(libraryElement.getLevel2Type()).equals(level2Type)){ + libraryElement.setLevel2Type(level2Type); + libraryService.updateById(libraryElement); + } + BeanUtils.copyProperties(libraryElement,collectionElement); + } + break; + } + } else { + log.error("designType cannot be empty"); + throw new BusinessException("element source type cannot be empty!"); + } + }else { + return null; + } + return collectionElement; + } + + @Override + public List getByProjectId(Long projectId) { + QueryWrapper qw = new QueryWrapper<>(); + qw.lambda().eq(CollectionElement::getProjectId, projectId); + List collectionElementList = collectionElementMapper.selectList(qw); + if (CollectionUtils.isEmpty(collectionElementList)) { + return new ArrayList<>(); + } + return collectionElementList; + } + + private Generate setGenerate(Long userId,String timeZone){ + Generate generate = new Generate(); + generate.setAccountId(userId); + generate.setLevel1Type(CollectionLevel1TypeEnum.PRINT_BOARD.getRealName()); + generate.setGenerateType("synthesis"); +// generate.setModelName("Image Synthesis Model"); + generate.setCreateDate(DateUtil.getByTimeZone(timeZone)); + return generate; + } + + private GenerateDetail setGenerateDetail(Long generateId, String url, String timeZone){ + GenerateDetail generateDetail = new GenerateDetail(); + generateDetail.setGenerateId(generateId); + generateDetail.setUrl(url); + String md5; + try { + md5 = MD5Utils.encryptFile(minioUtil.download(url)); + } catch (Exception e){ + throw new RuntimeException(e); + }/*catch (MinioException | IOException e) { + throw new RuntimeException(e); + }*/ + // 通过MD5来确认当前图片是否有被like过,避免重复like + List> libraryIds = generateDetailMapper.getLibraryIdThroughMD5(md5, CollectionLevel1TypeEnum.PRINT_BOARD.getRealName()); + if (libraryIds.isEmpty()){ + generateDetail.setIsLike((byte) 0); + }else { + generateDetail.setIsLike((byte) 1); + generateDetail.setLibraryId(libraryIds.get(0).get("library_id")); + } + generateDetail.setMd5(md5); + generateDetail.setCreateDate(LocalDateTime.now()); + + return generateDetail; + } + + // 对于上传图片或者从library选择的图片进行图片分割 + public List selectedImageSeg(List files, Long id, String type, String sourceType) { + Long accountId = UserContext.getUserHolder().getId(); + List resp = new ArrayList<>(); + List imageDates = new ArrayList<>(); + + boolean isUploadMode = !files.isEmpty(); + Library library = null; + CollectionElement collectionElement = null; + + // 判断是否是上传的图片 + if (isUploadMode) { + String objectName = accountId + "/ImageSegment/input"; + for (MultipartFile file : files) { + String md5 = MD5Utils.encryptFile(file); + String segmentedResult = redisUtil.getFromString(RedisUtil.IMAGE_SEGMENTATION + type + ":" + md5); + // 判断上传的图片是否有已分割的数据 + if (StringUtil.isNullOrEmpty(segmentedResult)) { + String path = minioUtil.upload(userBucketName, objectName, file); + ImageSegmentation.ImageDate imageDate = new ImageSegmentation().new ImageDate(); + imageDate.setImage_url(path); + imageDate.setImage_type(type); + imageDates.add(imageDate); + } else { + ImageSegmentation.ImageDate imageData = JSONObject.parseObject(segmentedResult, ImageSegmentation.ImageDate.class); + resp.add(createCollectionElementVO(accountId, null, null, imageData.getImage_url(), imageData.getClothing_url())); + } + } + } else if (Objects.nonNull(id) && sourceType.equals("Library")) { + library = libraryService.getById(id); + // 判断从library中选择的图片是否有分割数据 + if (Objects.isNull(library)) { + throw new BusinessException("library.not.found"); + } + if (StringUtil.isNullOrEmpty(library.getSegmentedData())) { + ImageSegmentation.ImageDate imageDate = new ImageSegmentation().new ImageDate(); + imageDate.setImage_url(library.getUrl()); + imageDate.setImage_type(type); + imageDates.add(imageDate); + } else { + List restoredList = Arrays.asList(library.getSegmentedData().split(",")); + resp.add(createCollectionElementVO(accountId, id, library.getLevel1Type(), library.getUrl(), restoredList)); + } + } else if (Objects.nonNull(id) && sourceType.equals("Upload")) { + collectionElement = collectionElementMapper.selectById(id); + // 判断id对应的数据是否存在 + if (Objects.isNull(collectionElement)) { + throw new BusinessException("get.file.failed"); + } + // 上传的图片分割数据没存(原因:上传的数据一般不会被再次使用,存储意义不大;上传存储表中数据复杂,添加字段,浪费多) + ImageSegmentation.ImageDate imageDate = new ImageSegmentation().new ImageDate(); + imageDate.setImage_url(collectionElement.getUrl()); + imageDate.setImage_type(type); + imageDates.add(imageDate); + } + + // 处理需要分割的图片 + if (!imageDates.isEmpty()) { + // 准备图片分割的参数 + ImageSegmentation imageSegmentation = new ImageSegmentation(); + imageSegmentation.setUser_id(accountId); + imageSegmentation.setImage_data(imageDates); + // 图片分割 + List segmented = pythonService.imageSegmentation(imageSegmentation); + // 处理图片分割结果 + for (ImageSegmentation.ImageDate imageData : segmented) { + if (isUploadMode) { + // 上传的图片需要添加到redis中存一天 + String key = RedisUtil.IMAGE_SEGMENTATION + type + ":" + + MD5Utils.encryptFile + (minioUtil.getPreSignedUrl + (imageData.getImage_url(), CommonConstant.MINIO_IMAGE_EXPIRE_TIME), false); + redisUtil.addToString(key, new Gson().toJson(imageData), CommonConstant.GENERATE_RESULT_EXPIRE_TIME); + resp.add(createCollectionElementVO(accountId, null, null, imageData.getImage_url(), imageData.getClothing_url())); + } else if (sourceType.equals("Library")){ + // 从library中选择的图片需要更新数据库中对应图片的分割数据 + String segmentedData = String.join(",", imageData.getClothing_url()); + library.setSegmentedData(segmentedData); + library.setUpdateDate(new Date()); + libraryService.updateById(library); + resp.add(createCollectionElementVO(accountId, id, library.getLevel1Type(), library.getUrl(), imageData.getClothing_url())); + }else { + resp.add(createCollectionElementVO(accountId, id, collectionElement.getLevel1Type(), collectionElement.getUrl(), imageData.getClothing_url())); + } + } + } + return resp; + } + + // 准备返回数据 + private CollectionElementVO createCollectionElementVO(Long accountId, Long id, String level1Type, + String imageUrl, List clothingUrls) { + CollectionElementVO vo = new CollectionElementVO(); + vo.setAccountId(accountId); + vo.setId(id); + vo.setLevel1Type(level1Type); + vo.setUrl(minioUtil.getPreSignedUrl(imageUrl, CommonConstant.MINIO_IMAGE_EXPIRE_TIME)); + vo.setMinIOPath(imageUrl); + + List segUrls = new ArrayList<>(); + for (String seg : clothingUrls) { + segUrls.add(minioUtil.getPreSignedUrl(seg, CommonConstant.MINIO_IMAGE_EXPIRE_TIME)); + } + vo.setSegmentedImages(segUrls); + return vo; + } + + public void updateElementLevel2Type(Long elementId, String level2Type){ + CollectionElement collectionElement = baseMapper.selectById(elementId); + if (Objects.nonNull(collectionElement)){ + collectionElement.setLevel2Type(level2Type); + collectionElement.setUpdateDate(new Date()); + baseMapper.updateById(collectionElement); + } + } + +} 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 d146f137..35c7488b 100644 --- a/src/main/java/com/ai/da/service/impl/EmailServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/EmailServiceImpl.java @@ -1,649 +1,720 @@ -package com.ai.da.service.impl; - -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.baomidou.mybatisplus.core.conditions.query.QueryWrapper; -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); - } -} - - +package com.ai.da.service.impl; + +import com.ai.da.common.RabbitMQ.MQPublisher; +import com.ai.da.common.config.exception.BusinessException; +import com.ai.da.common.response.ResultEnum; +import com.ai.da.common.utils.DateUtil; +import com.ai.da.common.utils.MailUtil; +import com.ai.da.common.utils.RedisUtil; +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.AccountService; +import com.ai.da.service.EmailService; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +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; + @Resource + private AccountService accountService; + @Resource + private RedisUtil redisUtil; + @Resource + private MQPublisher mqPublisher; + + 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) { + if (mailTo.size() == 1){ + String receiver = mailTo.get(0); + Account account = accountService.getBaseMapper().selectOne(new QueryWrapper().eq("user_email", receiver)); + if (Objects.nonNull(account)){ + boolean b = redisUtil.allowSend(account.getId()); + if (!b){throw new BusinessException("email.count.limit", ResultEnum.PROMPT.getCode());} + } + } + 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); + + 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: + case 550: + emailLog.setStatus(FAILED); + break; + case 250: + emailLog.setStatus(DELIVERED); + break; + case 450: + emailLog.setStatus(RETRYING); + break; + } + emailLogMapper.insert(emailLog); + + if (lastReturnCode == 250) { + log.info("邮件发送成功!Subject : {}", basicEmailParamDTO.getSubject()); + } else if (lastReturnCode == 450) { + log.info("目标邮箱 {} 暂时不可用,请稍后重试", mailTo); + Map params = setRetryParams(mailTo, jsonObject, emailTemplate.getTemplatePath(), title, fileName, inputStreamSource, emailLog.getId()); + // 异步重试 + mqPublisher.sendEmailMsg(params, 0); + // 更新数据库重试次数 + emailLog.setRetryCount(1); + emailLog.setUpdateTime(LocalDateTime.now()); + emailLogMapper.updateById(emailLog); + } else if (lastReturnCode == 550) { + log.info("目标邮箱 {} 不可用,邮件发送失败", mailTo); + } else { + log.info("邮件发送失败,Subject : {}, 状态码: {}", basicEmailParamDTO.getSubject(), lastReturnCode); + Map params = setRetryParams(mailTo, jsonObject, emailTemplate.getTemplatePath(), title, fileName, inputStreamSource, emailLog.getId()); + // 异步重试 + mqPublisher.sendEmailMsg(params, 0); + // 更新数据库重试次数 + emailLog.setRetryCount(1); + emailLog.setUpdateTime(LocalDateTime.now()); + emailLogMapper.updateById(emailLog); + } + } catch (MessagingException e) { + // 计数回滚 + throw new RuntimeException(e); + } + } + + public void updateRetryCount(Long logId, int retryCount){ + UpdateWrapper uw = new UpdateWrapper<>(); + uw.eq("id", logId); + uw.set("retry_count", retryCount); + uw.set("update_time", LocalDateTime.now()); + + emailLogMapper.update(null, uw); + } + + public void updateStatus(Long logId, String status){ + UpdateWrapper uw = new UpdateWrapper<>(); + uw.eq("id", logId); + uw.set("status", status); + uw.set("update_time", LocalDateTime.now()); + + emailLogMapper.update(null, uw); + } + + private Map setRetryParams(List mailTo, JSONObject jsonObject, String templatePath, + String title, String fileName, InputStreamSource inputStreamSource, Long logId) throws AddressException { + BasicEmailParamDTO dto = mailUtil.setBasicEmailParams(mailTo, title); + Map params = new HashMap<>(); + params.put("dto", JSONObject.toJSONString(dto)); + params.put("filename", fileName); + String source = Objects.nonNull(inputStreamSource) ? inputStreamSource.toString() : null; + params.put("source", source); + params.put("templateParams", JSONObject.toJSONString(jsonObject)); + params.put("templatePath", templatePath); + params.put("logId", logId.toString()); + return params; + } + + /** + * 适用于 : 需要自定义发件人信息 + * @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 void asyncRetry(BasicEmailParamDTO basicEmailParamDTO, String fileName, InputStreamSource inputStreamSource){ + // todo + } + + + + // 登入主题 + 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); + } +} + + diff --git a/src/main/java/com/ai/da/service/impl/StripeServiceImpl.java b/src/main/java/com/ai/da/service/impl/StripeServiceImpl.java index d8219bdb..9aceed4b 100644 --- a/src/main/java/com/ai/da/service/impl/StripeServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/StripeServiceImpl.java @@ -1,1635 +1,1640 @@ -package com.ai.da.service.impl; - -import com.ai.da.common.config.exception.BusinessException; -import com.ai.da.common.constant.CommonConstant; -import com.ai.da.common.context.UserContext; -import com.ai.da.common.enums.*; -import com.ai.da.common.utils.DateUtil; -import com.ai.da.common.utils.SendEmailUtil; -import com.ai.da.mapper.primary.AccountMapper; -import com.ai.da.mapper.primary.PaymentInfoMapper; -import com.ai.da.mapper.primary.ProductCouponsMapper; -import com.ai.da.mapper.primary.SubscriptionInfoMapper; -import com.ai.da.mapper.primary.entity.*; -import com.ai.da.model.dto.CreateCouponDTO; -import com.ai.da.model.dto.ProductPurchaseDTO; -import com.ai.da.model.dto.QueryCouponsPageDTO; -import com.ai.da.model.dto.SubscriptionEmailParamsDTO; -import com.ai.da.model.vo.CheckCouponsVO; -import com.ai.da.service.*; -import com.alibaba.fastjson.JSON; -import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; -import com.baomidou.mybatisplus.core.metadata.IPage; -import com.baomidou.mybatisplus.core.toolkit.StringUtils; -import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.google.gson.Gson; -import com.stripe.Stripe; -import com.stripe.exception.InvalidRequestException; -import com.stripe.exception.SignatureVerificationException; -import com.stripe.exception.StripeException; -import com.stripe.model.*; -import com.stripe.model.Product; -import com.stripe.model.checkout.Session; -import com.stripe.net.Webhook; -import com.stripe.param.*; -import com.stripe.param.checkout.SessionCreateParams; -import io.netty.util.internal.StringUtil; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import javax.annotation.Resource; -import javax.servlet.http.HttpServletRequest; -import java.math.BigDecimal; -import java.math.RoundingMode; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneOffset; -import java.time.ZonedDateTime; -import java.util.*; -import java.util.stream.Collectors; - -@SuppressWarnings("LoggingSimilarMessage") -@Service -@Slf4j -public class StripeServiceImpl implements StripeService { - - @Resource - private OrderInfoService orderInfoService; - @Resource - private PayPalCheckoutService payPalCheckoutService; - @Resource - private PaymentInfoService paymentInfoService; - @Resource - private CreditsService creditsService; - @Resource - private RefundInfoService refundInfoService; - @Resource - private AccountService accountService; - - @Resource - private AccountMapper accountMapper; - @Resource - private SubscriptionInfoMapper subscriptionInfoMapper; - @Resource - private PaymentInfoMapper paymentInfoMapper; - @Resource - private ProductCouponsMapper productCouponsMapper; - - @Value("${stripe.private-key}") - private String privateKey; - - @Value("${stripe.webhook-sign-secret}") - private String signSecret; - - @Value("${orderList.link}") - private String orderListLink; - - @Value("${stripe.paymentMethodConfiguration}") - private String paymentMethodConfigurationId; - - @Override - @Transactional(rollbackFor = Exception.class) - public String pay(ProductPurchaseDTO productPurchaseDTO, HttpServletRequest request) { - Stripe.apiKey = privateKey; - - //创建支付信息得到url - // 一次性支付和周期扣款,需要区分mode: payment || subscription || setup - SessionCreateParams.Builder sessionBuilder = new SessionCreateParams.Builder(); - ProductEnum productEnum; - switch (productPurchaseDTO.getProductName()){ - case "CreditsPurchase": - productEnum = ProductEnum.CreditsProduct; - productPurchaseDTO.setAutoRenewal(false); - break; - case "Subscription": - switch (productPurchaseDTO.getSubscribeType()){ - case "Month": - productEnum = ProductEnum.MonthlySubscription; - break; - case "Year": - productEnum = ProductEnum.AnnualSubscription; - break; - case "Day": - productEnum = ProductEnum.DailySubscription; - break; - default: - throw new BusinessException("unknown subscription type"); - } - - // 添加优惠券(只允许在订阅时使用优惠券) - String promotionCode = productPurchaseDTO.getPromotionCode(); - if (!StringUtil.isNullOrEmpty(promotionCode)){ - ProductCoupons productCoupon = checkProductCoupon(promotionCode);; - if (productCoupon != null){ - sessionBuilder.addDiscount(SessionCreateParams.Discount.builder() - .setPromotionCode(productCoupon.getPromotionCodeId()).build()); - } - } - // 只有订阅时才允许使用推广码优惠 -// sessionBuilder.setAllowPromotionCodes(true); - break; - default: - throw new BusinessException("unknown product type"); - } - log.info("生成订单"); - String payType; - byte autoRenewal; - if (productPurchaseDTO.getAutoRenewal()){ - payType = "recurring"; - autoRenewal = 1; - }else { - payType = "one_time"; - autoRenewal = 0; - } - OrderInfo orderInfo = orderInfoService.createOrderByProductId(productPurchaseDTO.getQuantity(), - PayTypeEnum.STRIPE.getType(), productEnum, request, autoRenewal); - - try { - Long id = UserContext.getUserHolder().getId(); - com.ai.da.mapper.primary.entity.Account account = accountMapper.selectById(id); - // 获取或创建产品 - String productId = getProduct(productEnum.getName()); - // 获取或创建价格 - String priceId = getPrice(productEnum.getPrice(), productId, payType, productPurchaseDTO.getSubscribeType()); - // 获取或创建customer - String customerId = getCustomer(account.getUserName(), account.getUserEmail()); - log.info("customerId:{}", customerId); - // 获取自定义订单号 - String orderId = orderInfo.getOrderNo(); - - - if (payType.equals("recurring")){ - sessionBuilder.setMode(SessionCreateParams.Mode.SUBSCRIPTION); - sessionBuilder.setSubscriptionData(SessionCreateParams.SubscriptionData.builder().setDescription("AiDA - " + orderId).build()); - }else { - sessionBuilder.setMode(SessionCreateParams.Mode.PAYMENT); - sessionBuilder.setPaymentIntentData(SessionCreateParams.PaymentIntentData.builder().setDescription("AiDA - " + orderId).build()); - // one-time 手动创建发票;订阅会自动创建invoice - sessionBuilder.setInvoiceCreation(SessionCreateParams.InvoiceCreation.builder().setEnabled(Boolean.TRUE).build()); - } - - sessionBuilder.setPaymentMethodConfiguration(paymentMethodConfigurationId); - sessionBuilder.setCustomer(customerId); - sessionBuilder.setSuccessUrl(productPurchaseDTO.getReturnUrl());//可自定义成功页面 - sessionBuilder.setLocale(account.getLanguage().equals("CHINESE_SIMPLIFIED") ? SessionCreateParams.Locale.ZH : SessionCreateParams.Locale.EN); - sessionBuilder.addLineItem( - SessionCreateParams.LineItem.builder() - .setQuantity((long) productPurchaseDTO.getQuantity()) - .setPrice(priceId) - .build()); - sessionBuilder.putMetadata("orderId", orderId); //通过订单号关联用于检索支付信息(可选) - - Session session = Session.create(sessionBuilder.build()); - List paymentMethodTypes = session.getPaymentMethodTypes(); - log.info("paymentMethodTypes: {}", paymentMethodTypes); - - Session.PaymentMethodConfigurationDetails paymentMethodConfigurationDetails = session.getPaymentMethodConfigurationDetails(); - log.info("paymentMethodConfigurationDetails ID: {}", paymentMethodConfigurationDetails.getId()); - log.info("sessionId:{}", session.getId()); //退款方式1:拿到sessionId入库,退款的时候根据这个id找到PaymentIntent的id然后发起退款 - - // 更新order信息 - orderInfoService.updateOrderNoById(orderInfo.getId(), orderId); - return session.getUrl(); - } catch (BusinessException e) { - throw e; - } catch (InvalidRequestException e) { - log.info("创建会话出现异常:", e); - throw new BusinessException(e.getMessage().substring(0, e.getMessage().indexOf(";"))); - } catch (Exception e) { - log.error("创建支付会话出现异常:", e); - } - return ""; - } - - // 获取产品ID - private String getProduct(String productName) throws StripeException { - Stripe.apiKey = privateKey; - // 1、获取所有的产品 - ProductCollection productCollection = Product.list(ProductListParams.builder().setActive(Boolean.TRUE).build()); - // 2、取一个指定名称的产品 - for (Product product : productCollection.getData()) { - if (product.getName().equals(productName)) { - return product.getId(); - } - } - // 3、在现有产品中没有找到指定产品,新建产品 - Map params = new HashMap<>(); - params.put("name", productName); - Product product = Product.create(params); - return product.getId(); - } - - /** - * 获取价格 - * - * @param priceValue 价格值 - * @param payType recurring || one_time - * @param recurringType monthly || yearly - */ - private String getPrice(Long priceValue, String productId, String payType, String recurringType) throws StripeException { - Stripe.apiKey = privateKey; - PriceCollection priceCollection = Price.list(PriceListParams.builder() - .setActive(Boolean.TRUE) - .setProduct(productId).build()); - for (Price price : priceCollection.getData()) { - // stripe的金额单位为分,所以这里需要 ×100 - if (price.getUnitAmount().equals(priceValue * 100) && price.getType().equals(payType)) { - return price.getId(); - } - } - - Price price = createPrice(priceValue, productId, payType, recurringType); - return price.getId(); - } - - private Price createPrice(Long priceValue, String productId, String payType, String recurringType) throws StripeException { - BigDecimal actualAmount = new BigDecimal(priceValue * 100); //stripe的默认单位是分,即传入的amount实际上小数点会被左移两位 - - PriceCreateParams.Builder priceCreateParams = new PriceCreateParams.Builder(); - priceCreateParams.setBillingScheme(PriceCreateParams.BillingScheme.PER_UNIT); - priceCreateParams.setCurrency("HKD"); - priceCreateParams.setProduct(productId); - priceCreateParams.setUnitAmount(actualAmount.longValue()); - - if (payType.equals("recurring")) { - PriceCreateParams.Recurring.Builder recurring = new PriceCreateParams.Recurring.Builder(); - // One of day, week, month or year. - recurring.setInterval(PriceCreateParams.Recurring.Interval.valueOf(recurringType.toUpperCase())); - // The number of intervals (specified in the interval attribute) between subscription billings. - // For example, interval=month and interval_count=3 bills every 3 months. - recurring.setIntervalCount(1L); - priceCreateParams.setRecurring(recurring.build()); - } - - return Price.create(priceCreateParams.build()); - } - - @Override - @Transactional(rollbackFor = Exception.class) - public Boolean notify(HttpServletRequest request) { - log.info("stripe异步通知进行中"); - String payload = null; - String sigHeader = null; - String endpointSecret = signSecret; - try { - sigHeader = request.getHeader("Stripe-Signature"); - payload = payPalCheckoutService.getBody(request); - } catch (Exception e) { - log.info("stripe 支付回调参数解析异常:errorMsg {}", e.getMessage()); - log.info("request sigHeader = {}", sigHeader); - log.info("request body = {}", JSON.toJSONString(payload)); - e.printStackTrace(); - return Boolean.FALSE; - } - - Event event; - try { - assert sigHeader != null; - event = Webhook.constructEvent(payload, sigHeader, endpointSecret); - } catch (SignatureVerificationException e) { - log.info("stripe 验签,获取事件异常, errorMsg={}", e.getMessage()); - log.info("request sigHeader = {}", sigHeader); - log.info("request body = {}", JSON.toJSONString(payload)); - e.printStackTrace(); - return Boolean.FALSE; - } - - //获取自定义参数 - // Deserialize the nested object inside the event - assert event != null; - EventDataObjectDeserializer dataObjectDeserializer = event.getDataObjectDeserializer(); - StripeObject stripeObject ; - if (dataObjectDeserializer.getObject().isPresent()) { - stripeObject = dataObjectDeserializer.getObject().get(); - } else { - log.info("stripe 验签失败!"); - log.info("request sigHeader = {}", sigHeader); - log.info("request body = {}", JSON.toJSONString(payload)); - return Boolean.FALSE; - } - log.info("stripe验签成功"); - boolean response = Boolean.TRUE; - - log.info("回调事件 {}", event.getType()); - if (stripeObject instanceof Session){ - Session session = (Session) stripeObject; - if (event.getType().equals("checkout.session.completed")) { - response = processOrder(session); - }else if (event.getType().equals("checkout.session.expired")){ - String orderNo = session.getMetadata().get("orderId"); - // 会话过期 未支付 且之后没有支付成功的订单 - response = processExpiredOrder(orderNo); - } - } else if (stripeObject instanceof Subscription){ - Subscription subscription = (Subscription) stripeObject; - if (event.getType().equals("customer.subscription.created")){ - // 添加数据到t_subscription_info表 需记录订阅id。需要判断订阅的状态是否active吗 ?? - createSubscription(subscription); - log.info("创建连续订阅"); - } else if (event.getType().equals("customer.subscription.updated")){ - // 更新订阅信息 - SubscriptionInfo subscriptionInfo = updateSubscription(subscription); - log.info("订阅更新"); - if (subscription.getStatus().equals("active")){ - response = sendEmail(subscription.getId(), null, null); - } - // 续订支付失败,邮件通知用户 - if (subscription.getStatus().equals("past_due")){ - // 发送续订失败邮件 - response = sendRenewalFailEmail(null, subscription.getId(), subscriptionInfo.getOrderNo()); - - } - } else if (event.getType().equals("customer.subscription.deleted")){ - SubscriptionInfo subscriptionInfo = updateSubscription(subscription); - log.info("用户 {} 取消连续订阅 {}", subscriptionInfo.getAccountId(), subscription.getId()); - if (subscriptionInfo.getCancelNotified() == (byte)0){ - log.info("取消订阅 邮件通知商家"); - response = sendEmail(subscription.getId(), "cancel", null); - if (response){ - subscriptionInfo.setCancelNotified((byte)1); - subscriptionInfoMapper.updateById(subscriptionInfo); - // 更新订单信息 - OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(subscriptionInfo.getOrderNo()); - orderInfo.setAutoRenewal((byte)0); - } - } - - }/* else if (event.getType().equals("customer.subscription.paused")){ - updateSubscription(subscription); - } else if (event.getType().equals("customer.subscription.resumed")){ - updateSubscription(subscription); - log.info("用户订阅恢复"); - }*/ - } else if (stripeObject instanceof Invoice) { - Invoice invoice = (Invoice) stripeObject; - if (event.getType().equals("invoice.paid")) { - // 新增支付成功的信息,返回orderNo,表示,该回调第一次被记录 - PaymentInfo paymentInfo = paymentInfoService.createOrUpdatePaymentInfoForStripe(invoice); - - // 当前支付没有被通知时才需要发送通知邮件 - if (paymentInfo.getNotified().equals(0)) { - // 更新t_order_info中的total_fee,记录该订单的累计付款金额 - orderInfoService.updateTotalFeeByOrderNo(paymentInfo.getOrderNo()); - // 邮件通知商家和用户 - String billingReason = invoice.getBillingReason(); - switch (billingReason) { - case "subscription_create": - response = sendEmail(invoice.getSubscription(), "new", null); - break; - case "subscription_cycle": - response = sendEmail(invoice.getSubscription(), "renewal", null); - break; - case "manual": - boolean b = invoice.getLines().getData().get(0).getDescription().endsWith("Subscription"); - if (b) { - // 非自动续订式订阅,Stripe不会创建Subscription,所以invoice中不会有subscriptionId - response = sendEmail(null, "new", paymentInfo.getOrderNo()); - } - break; - } - } - } else if (event.getType().equals("invoice.payment_failed")) { - // 更新支付信息 - QueryWrapper queryWrapper = new QueryWrapper<>(); - queryWrapper.eq("transaction_id", invoice.getId()); - PaymentInfo paymentInfo = paymentInfoService.getBaseMapper().selectOne(queryWrapper); - if (!Objects.isNull(paymentInfo)){ - String type = invoice.getBillingReason().equals("subscription_create") ? "new" : - invoice.getBillingReason().equals("subscription_cycle") ? "renewal" : invoice.getBillingReason(); - Gson gson = new Gson(); - String json = gson.toJson(invoice); - paymentInfo.setContent(json); - paymentInfo.setType(type); - paymentInfo.setHostedInvoiceUrl(invoice.getHostedInvoiceUrl()); - paymentInfoService.updateById(paymentInfo); - - // 发送续订失败邮件 - response = sendRenewalFailEmail(invoice.getId(), null, paymentInfo.getOrderNo()); - }else { - // 新增支付信息 - PaymentInfo paymentInfoFail = paymentInfoService.createOrUpdatePaymentInfoForStripe(invoice); - // 发送新订阅失败邮件 - response = sendEmail(paymentInfoFail.getOrderNo()); - } - } - }else if (stripeObject instanceof Charge) { - Charge charge = (Charge) stripeObject; - String orderNo = charge.getDescription().replace("AiDA - ", ""); - OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo); - if (Objects.isNull(orderInfo)){ - // 说明该回调不是从AiDA订阅获得 - return true; - } - if (event.getType().equals("charge.failed")){ - // 添加支付信息 && 更新支付信息 - // 支付失败时,无法通过invoice_id获取支付方式,所以使用charge.failed回调添加支付信息 - paymentInfoService.createOrUpdatePaymentInfoForStripe(charge); - - orderInfo.setOrderStatus(OrderStatusEnum.FAILURE.getType()); - orderInfo.setNote(charge.getFailureMessage()); - orderInfoService.updateById(orderInfo); - }else if (event.getType().equals("charge.succeeded")){ - orderInfo.setOrderStatus(OrderStatusEnum.SUCCESS.getType()); - orderInfo.setNote(""); - orderInfoService.updateById(orderInfo); - }else if (event.getType().equals("charge.refunded")){ - // 更新退款信息 - RefundInfo refundInfo = refundInfoService.updateRefundForStripe(charge); - // 更新 t_payment_info的支付状态 - if (Objects.nonNull(refundInfo)){ - paymentInfoService.updatePaymentRefundStatus(charge); - } - } - }else if (stripeObject instanceof Refund){ - Refund refund = (Refund) stripeObject; - if (event.getType().equals("refund.created")){ - // 新增退款信息 - refundInfoService.createRefundForStripe(refund); - }else if (event.getType().equals("refund.updated")){ - // 根据***id更新退款记录信息 - RefundInfo refundInfo = refundInfoService.updateRefundStatusForStripe(refund); - if (Objects.isNull(refundInfo)){ - // 等事件先创建,再更新。回调事件的顺序随机 - response = false; - } - } - } - log.info("回调事件 {} 处理完成", event.getType()); - return response; - } - - public boolean processOrder(Session session) { - Stripe.apiKey = privateKey; - String orderNo = session.getMetadata().get("orderId"); - float totalAmount = new BigDecimal(session.getAmountTotal()).divide(new BigDecimal(100), 2, RoundingMode.HALF_UP).floatValue(); - - boolean resp = true; - try { - //处理重复通知 - //接口调用的幂等性:无论接口被调用多少次,以下业务执行一次 -// String orderStatus = orderInfoService.getOrderStatus(orderNo); - OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(orderNo); - String orderStatus = orderByOrderNo.getOrderStatus(); - // 当订单状态处于未支付或超时已关闭时,更新订单状态,其他状态均不更新订单状态 - if (!OrderStatusEnum.NOT_PAY.getType().equals(orderStatus) && !OrderStatusEnum.TIMEOUT_CLOSED.getType().equals(orderStatus)) { - log.info("订单状态 : {}", orderStatus); - }else { - //更新订单状态 - orderInfoService.updateStatusByOrderNo(orderNo, OrderStatusEnum.SUCCESS); - log.info("Stripe 订单:{} 状态更新成功", orderNo); - } - - if (orderByOrderNo.getTitle().startsWith("积分购买")){ - // 查询当前订单的积分是否已添加 - CreditsDetail creditsDetail = creditsService.queryDetailByTaskId(orderNo); - if (Objects.isNull(creditsDetail)){ - float quantity = totalAmount / ProductEnum.CreditsProduct.getPrice(); - // 更新积分 - creditsService.buyCredits(orderByOrderNo.getAccountId(), quantity); - // 添加积分变更记录 - creditsService.insertToCreditsDetail(orderByOrderNo.getAccountId(), - CreditsEventsEnum.BUY_CREDITS.getName() + "--Stripe", - String.valueOf((Long.parseLong(CreditsEventsEnum.BUY_CREDITS.getValue()) * quantity)), - "positive", orderNo); - log.info("用户:{} 积分信息更新成功", orderByOrderNo.getAccountId()); - } - }else if (orderByOrderNo.getTitle().endsWith("Subscription") && orderByOrderNo.getAutoRenewal() == (byte)0){ - String invoiceId = session.getInvoice(); - Invoice invoice = Invoice.retrieve(invoiceId); - InvoiceLineItem invoiceLineItem = invoice.getLines().getData().get(0); - String description = invoiceLineItem.getDescription(); - Long amount = invoiceLineItem.getAmount(); - log.info("单次订阅 description : {}, amount: {} 分", description, amount); - boolean b = createSubscriptionAndUpdateAccount(orderNo, orderByOrderNo.getAccountId(), description, amount); - // 邮件通知用户 - if (b){ - resp = sendEmail(null, "new", orderNo); - } - log.info("单次订阅订单:{} 处理完成", orderNo); - } - } catch (Exception e) { - log.info(e.getMessage()); - resp = false; - } - return resp; - } - - private boolean processExpiredOrder(String orderNo) { - // 支付失败 通知商家的条件 1、会话过期 2、支付失败 3、这个用户在这个支付失败后再无支付成功的订阅 - // 1、获取当前订单的支付状态 -// String orderNo = session.getMetadata().get("orderId"); - OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(orderNo); - // 2、确认订单状态为支付失败 - boolean resp = true; - if (!Objects.isNull(orderByOrderNo) && orderByOrderNo.getOrderStatus().equals(OrderStatusEnum.FAILURE.getType())) { - // 3、判断失败订单之后再无成功的订单 - QueryWrapper queryWrapper = new QueryWrapper<>(); - queryWrapper.eq("account_id", orderByOrderNo.getAccountId()); - queryWrapper.gt("create_time", orderByOrderNo.getCreateTime()); - queryWrapper.eq("order_status", OrderStatusEnum.SUCCESS.getType()); - queryWrapper.likeLeft("title", "Subscription"); - List orderInfos = orderInfoService.getBaseMapper().selectList(queryWrapper); - if (orderInfos.isEmpty()) { - // 4、判断当前订单有没有订阅信息 - QueryWrapper qw = new QueryWrapper<>(); - qw.eq("order_no", orderNo); - SubscriptionInfo subscriptionInfo = subscriptionInfoMapper.selectOne(qw); - // 发送邮件通知商家用户支付失败 - if (Objects.isNull(subscriptionInfo) - || subscriptionInfo.getStatus().equals("incomplete") - || subscriptionInfo.getStatus().equals("incomplete_expired")) { - resp = sendEmail(orderNo); - }else { - // todo 续订失败 应该不会走这里 - resp = sendEmail(subscriptionInfo.getSubscriptionId(), "fail_renewal", null); - } - } - } - return resp; - - } - - @Transactional(rollbackFor = Exception.class) - public SubscriptionInfo createSubscription(Subscription subscription){ - // 确认当前subscription是否已经记录 - SubscriptionInfo subscriptionInfo = getSubscriptionInfoBySubId(subscription.getId()); -// SubscriptionInfo subscriptionInfo = subscriptionInfoMapper.selectOne(qw); - if (Objects.isNull(subscriptionInfo)) { - String description = subscription.getDescription(); - String orderNo = description.replace("AiDA - ", ""); - OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo); - - // 从回调信息中获取recurring type - SubscriptionItem subscriptionItem = subscription.getItems().getData().get(0); - String interval = subscriptionItem.getPrice().getRecurring().getInterval(); - - subscriptionInfo = new SubscriptionInfo(); - subscriptionInfo.setAccountId(orderInfo.getAccountId()); - subscriptionInfo.setOrderNo(orderNo); - subscriptionInfo.setSubscriptionId(subscription.getId()); - subscriptionInfo.setType(interval); - subscriptionInfo.setStatus(subscription.getStatus()); - subscriptionInfo.setNextPayDate(DateUtil.changeTimeStampFormat(subscription.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy_EEEE)); - subscriptionInfo.setCurrentPeriodStart(subscription.getCurrentPeriodStart()); - subscriptionInfo.setCurrentPeriodEnd(subscription.getCurrentPeriodEnd()); - subscriptionInfo.setCreateTime(LocalDateTime.now()); - - int rows = subscriptionInfoMapper.insertIgnore(subscriptionInfo); - log.info("Subscription info insert affect rows : {}", rows); - - if (subscriptionInfo.getStatus().equals("active")){ - log.info("创建订阅更新账号信息"); - // 更新账号到期时间 - boolean b = accountService.updateAccountValidity(subscriptionInfo.getAccountId(), subscriptionInfo.getCurrentPeriodEnd()); - // 更新账号身份和积分 - if (b) accountService.updateUserRoleAndCredits(subscriptionInfo.getAccountId(), interval); - } - } - return subscriptionInfo; - } - - /** - * 非自动续订订阅 - * Stripe不会自动创建Subscription,所以没有subscription相关的回调,无法触发订阅相关的处理代码 - */ - public boolean createSubscriptionAndUpdateAccount(String orderNo, Long accountId, String description, Long amount){ - QueryWrapper qw = new QueryWrapper<>(); - qw.eq("order_no", orderNo); - SubscriptionInfo subscriptionInfo = subscriptionInfoMapper.selectOne(qw); - if (Objects.isNull(subscriptionInfo)) { - String interval; - // 获取当前时间戳(秒级) - long currentPeriodStart = Instant.now().getEpochSecond();; - long currentPeriodEnd; -// InvoiceLineItem invoiceLineItem = invoice.getLines().getData().get(0); - if (description.equals(ProductEnum.DailySubscription.getName()) - && amount.equals(ProductEnum.DailySubscription.getPrice() * 100)){ - interval = "day"; - // 获取一天后的时间戳(秒级) - ZonedDateTime now = ZonedDateTime.now(); - currentPeriodEnd = now.plusDays(1).toEpochSecond(); - }else if (description.equals(ProductEnum.MonthlySubscription.getName()) - && amount.equals(ProductEnum.MonthlySubscription.getPrice() * 100)){ - interval = "month"; - // 获取一天后的时间戳(秒级) - ZonedDateTime now = ZonedDateTime.now(); - currentPeriodEnd = now.plusMonths(1).toEpochSecond(); - }else if (description.equals(ProductEnum.AnnualSubscription.getName()) - && amount.equals(ProductEnum.AnnualSubscription.getPrice() * 100)){ - interval = "year"; - // 获取一天后的时间戳(秒级) - ZonedDateTime now = ZonedDateTime.now(); - currentPeriodEnd = now.plusYears(1).toEpochSecond(); - }else { - log.error("未知订阅类型"); - return false; - } - subscriptionInfo = new SubscriptionInfo(); - subscriptionInfo.setAccountId(accountId); - subscriptionInfo.setOrderNo(orderNo); - subscriptionInfo.setType(interval); - subscriptionInfo.setStatus("canceled"); - subscriptionInfo.setCurrentPeriodStart(currentPeriodStart); - subscriptionInfo.setCurrentPeriodEnd(currentPeriodEnd); - subscriptionInfo.setCreateTime(LocalDateTime.now()); - - subscriptionInfoMapper.insertIgnore(subscriptionInfo); - - log.info("创建订阅, 更新账号信息"); - // 更新账号到期时间 - boolean b = accountService.updateAccountValidity(subscriptionInfo.getAccountId(), subscriptionInfo.getCurrentPeriodEnd()); - // 更新账号身份和积分 - if (b) accountService.updateUserRoleAndCredits(subscriptionInfo.getAccountId(), interval); - return true; - } - return true; - } - - public SubscriptionInfo getSubscriptionInfoBySubId(String subId){ - QueryWrapper qw = new QueryWrapper<>(); - qw.eq("subscription_id", subId); - - List subscriptionInfos = subscriptionInfoMapper.selectList(qw); - if (subscriptionInfos.size() == 1){ - return subscriptionInfos.get(0); - }else if (subscriptionInfos.size() > 1) { - // 如果新建了多个订阅,则筛选出状态为active的订单 - Optional activeSubscriptionInfo = subscriptionInfos.stream() - .filter(sub -> sub.getStatus().equals("active")) - .findFirst(); - - return activeSubscriptionInfo.orElseGet(() -> subscriptionInfos.get(0)); - }else { - return null; - } - } - - public SubscriptionInfo getLatestSubscriptionInfoByAccountId(Long accountId){ - QueryWrapper qw = new QueryWrapper<>(); - qw.eq("account_id", accountId).orderByDesc("id"); - List subscriptionInfos = subscriptionInfoMapper.selectList(qw); - if (subscriptionInfos.isEmpty()){ - return null; - }else { - return subscriptionInfos.get(0); - } - } - - @Transactional(rollbackFor = Exception.class) - public SubscriptionInfo updateSubscription(Subscription subscription){ - // 获取当前是否有已经记录的subscriptionInfo - SubscriptionInfo subscriptionInfo = createSubscription(subscription); - // 用于标志数据有没有变动,避免在没有改动的情况下频繁的更新数据库 - boolean flag = false; - if (!subscriptionInfo.getStatus().equals(subscription.getStatus())){ - subscriptionInfo.setStatus(subscription.getStatus()); - flag = true; - } - if (!subscriptionInfo.getCurrentPeriodStart().equals(subscription.getCurrentPeriodStart())){ - subscriptionInfo.setCurrentPeriodStart(subscription.getCurrentPeriodStart()); - flag = true; - } - if (!subscriptionInfo.getCurrentPeriodEnd().equals(subscription.getCurrentPeriodEnd())){ - subscriptionInfo.setCurrentPeriodEnd(subscription.getCurrentPeriodEnd()); - subscriptionInfo.setNextPayDate(DateUtil.changeTimeStampFormat(subscription.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy_EEEE)); - log.info("更新订阅更新账号信息"); - // 更新账号到期时间 - accountService.updateAccountValidity(subscriptionInfo.getAccountId(), subscriptionInfo.getCurrentPeriodEnd()); - // 更新账号身份和积分 - accountService.updateUserRoleAndCredits(subscriptionInfo.getAccountId(), subscriptionInfo.getType()); - log.info("更新 {} 账号到期时间为:{}", subscriptionInfo.getAccountId(), DateUtil.changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy_EEEE)); - flag = true; - } - if (subscriptionInfo.getStatus().equals("active")){ - // 更新账号到期时间 - boolean b = accountService.updateAccountValidity(subscriptionInfo.getAccountId(), subscriptionInfo.getCurrentPeriodEnd()); - // 更新账号身份和积分 - if (b) accountService.updateUserRoleAndCredits(subscriptionInfo.getAccountId(), subscriptionInfo.getType()); - } - if (flag){ - subscriptionInfo.setUpdateTime(LocalDateTime.now()); - subscriptionInfoMapper.updateById(subscriptionInfo); - } - return subscriptionInfo; - } - - // 取消连续订阅 将订阅从pause状态转为cancel状态(使用定时器,定期检索DB中,过期且不续订的订阅) - public void cancelSubscription(String subscriptionId, String cancelReason) { - Stripe.apiKey = privateKey; - log.info("cancel subscription"); - Long accountId = UserContext.getUserHolder().getId(); - com.ai.da.mapper.primary.entity.Account account = accountMapper.selectById(accountId); - List subscriptions = getSubscription(account.getUserName(), account.getUserEmail()); - // 获取status = active的订阅 - subscriptions.forEach(subscription -> { - if (subscription.getId().equals(subscriptionId)) { - try { - Subscription cancel = subscription.cancel(); - cancel.getStatus(); - log.info("用户 {} 申请取消连续订阅 {}", accountId, subscriptionId); - // 更新数据库 - updateCancelReason(subscriptionId, cancelReason); - } catch (StripeException e) { - log.error("订阅 {} 取消失败, error message : {}", subscription.getId(), e.getMessage()); - } - } - }); - } - - public void cancelSubscriptionTemp(String subscriptionId) { - Stripe.apiKey = privateKey; - try { - log.info("申请取消连续订阅 {}", subscriptionId); - Subscription subscription = Subscription.retrieve(subscriptionId); - Subscription cancel = subscription.cancel(); - cancel.getStatus(); - } catch (StripeException e) { - log.error(e.getMessage()); -// throw new RuntimeException(e); - } - } - - public String refund(String amount, String orderNo, String reason) { - Refund refund; - RefundInfo refundByOrderNo = refundInfoService.createRefundByOrderNo(orderNo, reason); - - try { - Stripe.apiKey = privateKey; - // todo transactionId不再是sessionId而是invoiceId,所以这里需要更新 - // 根据orderId找到对应的sessionId - String sessionId = paymentInfoService.getPaymentInfoByOrderNo(orderNo, "DESC").get(0).getTransactionId(); - - if (StringUtils.isNotEmpty(sessionId)) { //根据会话编号退款 - Session session = Session.retrieve(sessionId); - RefundCreateParams params; - if (amount != null && !amount.equals("0")) { //指定退款金额 - BigDecimal actualAmount = new BigDecimal(amount).multiply(BigDecimal.valueOf(100)); //api默认单位分 - params = RefundCreateParams.builder() - .setPaymentIntent(session.getPaymentIntent()) - .setAmount(actualAmount.longValue()) - .build(); - } else { //全额退款 - params = RefundCreateParams.builder() - .setPaymentIntent(session.getPaymentIntent()) - .build(); - } - refund = Refund.create(params); - log.info("根据会话编号退款成功"); - - } else { - log.error("当前订单不存在"); - return "退款异常"; - } - } catch (Exception e) { - //e.getMessage.contain("charge_already_refunded") 已退款 - //e.getMessage.contain("resource_missing") 退款编号错误 - //e.getMessage.contain("amount on charge ($n)") 金额应小于n - log.error("退款异常:", e); - return "退款异常"; - } - - if ("succeeded".equals(refund.getStatus())) { - //进行数据库操作,修改状态为已退款(配合回调和退款查询确定退款成功) - //更新订单状态 - orderInfoService.updateStatusByOrderNo(orderNo, OrderStatusEnum.REFUND_SUCCESS); - - refundInfoService.updateRefundForPayPal( - refundByOrderNo.getId(), - refund.getId(), - new Gson().toJson(refund), - AliPayTradeStateEnum.REFUND_SUCCESS.getType()); //退款成功 - - // 更新积分状态 - OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(orderNo); - creditsService.creditsRefund(orderByOrderNo.getAccountId(), (int) (orderByOrderNo.getTotalFee() / Float.parseFloat(CreditsEventsEnum.PRICE.getValue())), orderNo); - } else { - //更新订单状态 - orderInfoService.updateStatusByOrderNo(orderNo, OrderStatusEnum.REFUND_ABNORMAL); - - //更新退款单 - refundInfoService.updateRefundForPayPal( - refundByOrderNo.getId(), - refund.getId(), - new Gson().toJson(refund), - AliPayTradeStateEnum.REFUND_ERROR.getType()); //退款失败 - } - log.info("记录退款订单"); - return "退款成功"; - } - - public void checkOrderStatus(String orderNo) { - Stripe.apiKey = privateKey; - // 1、通过orderNo 查询sessionId - // todo transactionId不再是sessionId而是invoiceId,所以这里需要更新 - PaymentInfo paymentInfo = paymentInfoService.getPaymentInfoByOrderNo(orderNo, "DESC").get(0); - try { - Session session = Session.retrieve(paymentInfo.getTransactionId()); - if (Objects.isNull(session)) { - log.warn("核实订单未创建 ===> {}", orderNo); - return; - } else if (session.getStatus().equals("open") || session.getStatus().equals("expired")) { - // 订单未支付 || 订单过期 ---> 均设置为超时未支付 - log.info("订单超时未支付 ===> {}", orderNo); - //更新本地订单状态 - orderInfoService.updateStatusByOrderNo(orderNo, OrderStatusEnum.TIMEOUT_CLOSED); - paymentInfoService.updatePaymentStatusById(paymentInfo.getId(), - session.getStatus(), - new Gson().toJson(session)); - } else if (session.getStatus().equals("complete")) { - // 订单已完成 - processOrder(session); - } - } catch (StripeException e) { - log.error("根据sessionId获取Stripe Session失败"); - throw new RuntimeException(e); - } - - } - - public List getSubscription(String username, String userEmail) { - Stripe.apiKey = privateKey; - String customerId = null; - try { - customerId = getCustomer(username, userEmail); - SubscriptionCollection list = Subscription.list(SubscriptionListParams.builder() - .setCustomer(customerId).setLimit(20L) - .build()); - return list.getData(); - } catch (StripeException e) { - throw new RuntimeException(e); - } - } - - // 获取所有订阅 - public List getSubscriptionIds(String username, String userEmail) { - Stripe.apiKey = privateKey; - String customerId = null; - try { - customerId = getCustomer(username, userEmail); - SubscriptionCollection list = Subscription.list(SubscriptionListParams.builder() - .setCustomer(customerId).build()); - List data = list.getData(); - ArrayList subscriptionIds = new ArrayList<>(); - data.forEach(subscription -> { - subscriptionIds.add(subscription.getId()); - }); - return subscriptionIds; - } catch (StripeException e) { - throw new RuntimeException(e); - } - - } - - private String getCustomer(String username, String userEmail) throws StripeException { - CustomerCollection list = Customer.list(CustomerListParams.builder().setEmail(userEmail).build()); - List data = list.getData(); - if (!data.isEmpty()) { - return data.get(0).getId(); - } - return createCustomer(username, userEmail); - } - - private String createCustomer(String name, String userEmail) throws StripeException { - Stripe.apiKey = privateKey; - - // Customer允许重复使用 - CustomerCreateParams params = - CustomerCreateParams.builder() - .setName(name) - .setEmail(userEmail) - .build(); - Customer customer = Customer.create(params); - - return customer.getId(); - } - - /** - * 使用连续订阅的订单,回调中没有paymentIntentId,所以通过invoiceId间接获取 - * @param invoiceId 发票Id - */ - public Map getPaymentMethodByInvoiceId(String invoiceId) { - try { - Stripe.apiKey = privateKey; - Invoice invoice = Invoice.retrieve(invoiceId); - if (!StringUtil.isNullOrEmpty(invoice.getPaymentIntent())){ - PaymentIntent paymentIntent = PaymentIntent.retrieve(invoice.getPaymentIntent()); - if (!StringUtil.isNullOrEmpty(paymentIntent.getPaymentMethod())){ - PaymentMethod paymentMethod = PaymentMethod.retrieve(paymentIntent.getPaymentMethod()); - return getPaymentMethod(paymentMethod.getId()); - } - } - HashMap resp = new HashMap<>(); - resp.put("paymentMethod", "N/A"); - resp.put("last4", "N/A"); - return resp; - } catch (StripeException e) { - throw new RuntimeException(e); - } - } - - public Map getPaymentMethod(String paymentMethodId){ - Stripe.apiKey = privateKey; - String paymentMethod = null; - String last4 = null; - - try { - PaymentMethod retrieve = PaymentMethod.retrieve(paymentMethodId); - switch (retrieve.getType()){ - case "alipay": - paymentMethod = "Alipay"; - last4 = "N/A"; - break; - case "bancontact": - paymentMethod = "BanContact"; - break; - case "card": - PaymentMethod.Card card = retrieve.getCard(); - String brand = card.getBrand(); - brand = brand.substring(0, 1).toUpperCase() + brand.substring(1); - paymentMethod = brand + " " + card.getFunding() + "card"; - last4 = card.getLast4(); - break; - case "eps": - PaymentMethod.Eps eps = retrieve.getEps(); - paymentMethod = eps.getBank(); - last4 = "N/A"; - break; - case "giropay": - paymentMethod = "GiroPay"; - last4 = "N/A"; - break; - case "ideal": - PaymentMethod.Ideal ideal = retrieve.getIdeal(); - paymentMethod = ideal.getBank(); - last4 = "N/A"; - break; - case "link": - paymentMethod = "Link"; - last4 = "N/A"; - break; - default: - paymentMethod = "N/A"; - last4 = "N/A"; - } - HashMap resp = new HashMap<>(); - resp.put("paymentMethod", paymentMethod); - resp.put("last4", last4); - return resp; - } catch (StripeException e) { - throw new RuntimeException(e); - } -// return null; - } - - public boolean sendEmail(String subscriptionId, String type, String orderNo) { - SubscriptionInfo subscriptionInfo; - QueryWrapper qwSI = new QueryWrapper<>(); - if (!StringUtil.isNullOrEmpty(subscriptionId)) { - qwSI.eq("subscription_id", subscriptionId); - List subscriptionInfoList = subscriptionInfoMapper.selectList(qwSI); - - if (subscriptionInfoList.isEmpty()){ - log.info("不发送邮件,原因:【subscriptionInfoList 为空】"); - return false; - }else { - List activeSubscriptions = subscriptionInfoList.stream() - .filter(subscription -> "active".equals(subscription.getStatus())) - .collect(Collectors.toList()); - if (!StringUtil.isNullOrEmpty(type) && type.equals("cancel")){ - subscriptionInfo = subscriptionInfoList.get(0); - }else if (activeSubscriptions.isEmpty()){ - log.info("不发送邮件,原因:【当前邮件类型:{}, 但是状态为active的subscriptionInfo为空】", type); - return false; - }else { - subscriptionInfo = activeSubscriptions.get(0); - } - } - }else if (!StringUtil.isNullOrEmpty(orderNo)) { - qwSI.eq("order_no", orderNo); - subscriptionInfo = subscriptionInfoMapper.selectOne(qwSI); - if (Objects.isNull(subscriptionInfo)){ - log.info("不发送邮件,原因:【根据order_no:{},查询到的subscriptionInfo为空】", orderNo); - return false; - } - }else { - log.info("不发送邮件,原因:【入参中的subscriptionId,orderNo均为空】"); - return false; - } - - QueryWrapper qwPI = new QueryWrapper<>(); - qwPI.eq("order_no", subscriptionInfo.getOrderNo()).orderByDesc("id"); - List paymentInfos = paymentInfoMapper.selectList(qwPI); - if (paymentInfos.isEmpty()) { - log.info("不发送邮件,原因:【根据order_no:{},查询到的paymentInfos为空】", orderNo); - return false; - } - PaymentInfo paymentInfo = paymentInfos.get(0); - if (StringUtil.isNullOrEmpty(type)){ - // 如果没有传入type,则使用paymentInfo中记录的类型 - // (其实这里也可以通过invoiceId查询stripe,但是记录在自己的db中可以不用每次都查,且方便查看) - type = StringUtil.isNullOrEmpty(paymentInfo.getType()) ? "new" : paymentInfo.getType(); - } - if (!type.equals("reminder") && !type.equals("cancel") && paymentInfo.getNotified() == 1){ - // 已经邮件通知过,直接返回 - log.info("不发送邮件,原因:【type为:{},order_no为:{},已经进行邮件通知】", type, orderNo); - return true; - } - - com.ai.da.mapper.primary.entity.Account account = accountMapper.selectById(subscriptionInfo.getAccountId()); - String userName = account.getUserName(); - String language = account.getLanguage(); - OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(subscriptionInfo.getOrderNo()); - - SubscriptionEmailParamsDTO emailParamsDTO = new SubscriptionEmailParamsDTO(); - emailParamsDTO.setUsername(userName); - emailParamsDTO.setOrderId(paymentInfo.getId().toString()); - emailParamsDTO.setOrderRef("\"" + orderListLink + paymentInfo.getId().toString() + "\""); - emailParamsDTO.setCreateDate(String.valueOf(paymentInfo.getCreateTime()).replace("T", " ")); - emailParamsDTO.setQuantity(String.valueOf(1)); - emailParamsDTO.setTotalFee(paymentInfo.getPayerTotal().toString()); - emailParamsDTO.setLastOrderDate(DateUtil.changeTimeStampFormat(subscriptionInfo.getCurrentPeriodStart(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy)); - emailParamsDTO.setEndOfPrepaidTerm(DateUtil.changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy)); - setSubscriptionParams(paymentInfo, subscriptionInfo, orderByOrderNo, emailParamsDTO, language); - - boolean b = SendEmailUtil.subscriptionEmailReminder(type, emailParamsDTO, language, account.getUserEmail()); - if (!b) return false; - - // 邮件通知成功后,更新标志 - if (!type.equals("reminder") && !type.equals("cancel")){ - PaymentInfo payment = new PaymentInfo(); - payment.setId(paymentInfo.getId()); - payment.setNotified(1); - payment.setUpdateTime(LocalDateTime.now()); - paymentInfoMapper.updateById(payment); - } - return true; - } - - public boolean sendEmail(String orderNo){ - SubscriptionEmailParamsDTO emailParamsDTO = new SubscriptionEmailParamsDTO(); - OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo); - - com.ai.da.mapper.primary.entity.Account account = accountMapper.selectById(orderInfo.getAccountId()); - String userName = account.getUserName(); - String language = account.getLanguage(); - QueryWrapper qwPI = new QueryWrapper<>(); - qwPI.eq("order_no", orderNo); - List paymentInfos = paymentInfoMapper.selectList(qwPI); - if (paymentInfos.isEmpty()) { - log.info("不发送邮件,原因:【根据order_no:{},查询到的paymentInfos为空】", orderNo); - return false; - } - PaymentInfo paymentInfo = paymentInfos.get(0); - emailParamsDTO.setUsername(userName); - emailParamsDTO.setOrderId(paymentInfo.getId().toString()); - emailParamsDTO.setCreateDate(String.valueOf(paymentInfo.getCreateTime()).replace("T", " ")); - emailParamsDTO.setQuantity(String.valueOf(1)); - emailParamsDTO.setTotalFee(paymentInfo.getPayerTotal().toString()); - emailParamsDTO.setFailMessage(orderInfo.getNote()); - emailParamsDTO.setPaymentMethod(paymentInfo.getPaymentMethod()); - emailParamsDTO.setLast4(paymentInfo.getLast4()); - - boolean b = SendEmailUtil.subscriptionEmailReminder("fail_new", emailParamsDTO, language, account.getUserEmail()); - if (!b) return false; - - // 邮件通知成功后,更新标志 - PaymentInfo payment = new PaymentInfo(); - payment.setId(paymentInfo.getId()); - payment.setNotified(1); - payment.setUpdateTime(LocalDateTime.now()); - paymentInfoMapper.updateById(payment); - return true; - } - - public boolean sendRenewalFailEmail(String invoiceId, String subscriptionId, String orderNo){ - // 1、确认当前订单最后一笔支付为fail - // 更新支付信息 - PaymentInfo paymentInfo; - QueryWrapper queryWrapper = new QueryWrapper<>(); - if (StringUtil.isNullOrEmpty(invoiceId)){ - queryWrapper.eq("order_no", orderNo).orderByDesc("id"); - List paymentInfos = paymentInfoService.getBaseMapper().selectList(queryWrapper); - if (paymentInfos.isEmpty() || !paymentInfos.get(0).getTradeState().equals("failed")){ - return false; - }else { - paymentInfo = paymentInfos.get(0); - } - }else { - queryWrapper.eq("transaction_id", invoiceId); - paymentInfo = paymentInfoService.getBaseMapper().selectOne(queryWrapper); - if (Objects.isNull(paymentInfo) - || !paymentInfo.getTradeState().equals("failed") - || paymentInfo.getNotified().equals(1)){ - return false; - } - } - - // 2、确认当前订阅的状态为past_due - SubscriptionInfo subscriptionInfo; - QueryWrapper qwSI = new QueryWrapper<>(); - if (StringUtil.isNullOrEmpty(subscriptionId)){ - qwSI.eq("order_no", orderNo); - }else { - qwSI.eq("subscription_id", subscriptionId); - } - subscriptionInfo = subscriptionInfoMapper.selectOne(qwSI); - if (Objects.isNull(subscriptionInfo) || !subscriptionInfo.getStatus().equals("past_due")){ - return false; - } - - // 3、组参数 - com.ai.da.mapper.primary.entity.Account account = accountMapper.selectById(subscriptionInfo.getAccountId()); - String userName = account.getUserName(); - String language = account.getLanguage(); - OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(subscriptionInfo.getOrderNo()); - SubscriptionEmailParamsDTO emailParamsDTO = new SubscriptionEmailParamsDTO(); - emailParamsDTO.setUsername(userName); - emailParamsDTO.setOrderId(paymentInfo.getId().toString()); - emailParamsDTO.setCreateDate(String.valueOf(paymentInfo.getCreateTime()).replace("T", " ")); - emailParamsDTO.setQuantity(String.valueOf(1)); - emailParamsDTO.setTotalFee(paymentInfo.getPayerTotal().toString()); - setSubscriptionParams(paymentInfo, subscriptionInfo, orderByOrderNo, emailParamsDTO, language); - - // 4、发邮件 - boolean b = SendEmailUtil.subscriptionEmailReminder("fail_renewal", emailParamsDTO, language, account.getUserEmail()); - if (!b) return false; - - PaymentInfo payment = new PaymentInfo(); - payment.setId(paymentInfo.getId()); - payment.setNotified(1); - payment.setUpdateTime(LocalDateTime.now()); - paymentInfoMapper.updateById(payment); - return true; - } - - private void setSubscriptionParams(PaymentInfo paymentInfo, SubscriptionInfo subscriptionInfo, OrderInfo orderByOrderNo, - SubscriptionEmailParamsDTO emailParamsDTO, String language) { - emailParamsDTO.setPaymentMethod(paymentInfo.getPaymentMethod()); - emailParamsDTO.setLast4(paymentInfo.getLast4()); - emailParamsDTO.setSubscriptionId(subscriptionInfo.getId().toString()); - emailParamsDTO.setFailMessage(orderByOrderNo.getNote()); - emailParamsDTO.setSubscriptionType(subscriptionInfo.getType()); - emailParamsDTO.setStartDate(DateUtil.changeTimeStampFormat(orderByOrderNo.getCreateTime())); - if (subscriptionInfo.getType().equals("month")){ - emailParamsDTO.setRenewalFee(String.valueOf(500)); - } else if (subscriptionInfo.getType().equals("year")){ - emailParamsDTO.setRenewalFee(String.valueOf(5000)); - } else { - emailParamsDTO.setRenewalFee(emailParamsDTO.getTotalFee()); - } - if (subscriptionInfo.getStatus().equals("active")){ - if (language.equals("ENGLISH")){ - emailParamsDTO.setEndDate("When cancelled"); - }else { - emailParamsDTO.setEndDate("手动取消订阅时"); - } - }else { - emailParamsDTO.setEndDate(DateUtil.changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy_EEEE)); - } - if (StringUtil.isNullOrEmpty(subscriptionInfo.getNextPayDate())){ - emailParamsDTO.setNextPayDate("N/A"); - } else { - emailParamsDTO.setNextPayDate(DateUtil.changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy)); - } - emailParamsDTO.setRenewalTime(DateUtil.changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy)); - } - - public void subscriptionReminder(){ - // 提前7天的 00:00:00 和 23:59:59 - LocalDateTime startOfDay = LocalDateTime.now().plusDays(7).toLocalDate().atStartOfDay(); - LocalDateTime endOfDay = LocalDateTime.now().plusDays(7).toLocalDate().atTime(23, 59, 59); - - // 转为时间戳 - long startTimestamp = startOfDay.toEpochSecond(ZoneOffset.UTC); - long endTimestamp = endOfDay.toEpochSecond(ZoneOffset.UTC); - - QueryWrapper qw = new QueryWrapper<>(); - qw.ge("current_period_end", startTimestamp); - qw.lt("current_period_end", endTimestamp); - qw.eq("status", "active"); - - List subscriptionInfos = subscriptionInfoMapper.selectList(qw); - for (SubscriptionInfo subscriptionInfo : subscriptionInfos) { - boolean b = sendEmail(subscriptionInfo.getSubscriptionId(), "reminder", null); - if (b) log.info("提前7天向用户 {} 发送续订通知邮件", subscriptionInfo.getAccountId()); - } - } - - public void checkSubscriptionExpiration(){ - long epochSecond = Instant.now().getEpochSecond(); - QueryWrapper qw = new QueryWrapper<>(); - qw.lt("current_period_end", epochSecond); - qw.eq("status", "active"); - List subscriptionInfos = subscriptionInfoMapper.selectList(qw); - - for (SubscriptionInfo subscriptionInfo : subscriptionInfos) { - subscriptionInfo.setStatus("expired"); - subscriptionInfo.setUpdateTime(LocalDateTime.now()); - subscriptionInfoMapper.updateById(subscriptionInfo); - log.info("用户 {} 的订阅 {} 已过期", subscriptionInfo.getAccountId(), subscriptionInfo.getOrderNo()); - } - } - - // 新建一个订阅 使用不会成功的付款方式(仅供测试使用) - public String createSubscriptionTemp(String name, String email){ - Stripe.apiKey = privateKey; - try { - OrderInfo orderInfo = orderInfoService.createOrderByProductId(1, PayTypeEnum.STRIPE.getType(), ProductEnum.DailySubscription, null, (byte)0); - -// String customerId = getCustomer(name, email); - String paymentMethodCode = "pm_card_mastercard"; - PaymentMethod paymentMethod = PaymentMethod.retrieve(paymentMethodCode); - - String customerId = getCustomer(name, email); - log.info("customerId: {}", customerId); - - PaymentMethodAttachParams attachParams = PaymentMethodAttachParams.builder() - .setCustomer(customerId) - .build(); - paymentMethod.attach(attachParams); - - // 设置默认付款方式 - Customer updatedCustomer = Customer.retrieve(customerId); - CustomerUpdateParams params = CustomerUpdateParams.builder() - .setInvoiceSettings( - CustomerUpdateParams.InvoiceSettings.builder() - .setDefaultPaymentMethod(paymentMethod.getId()) - .build()) - .build(); - updatedCustomer.update(params); - - // 3. 创建订阅 - SubscriptionCreateParams subscriptionParams = SubscriptionCreateParams.builder() - .setCustomer(customerId) - .addItem( - SubscriptionCreateParams.Item.builder() - .setPrice("price_1QFXkf02n1TEydyNtA4TQ3Yz") // 替换为实际的价格 ID - .build()) - .setDescription("AiDA - " + orderInfo.getOrderNo()) - .build(); - Subscription subscription = Subscription.create(subscriptionParams); - - // 打印订阅 ID - System.out.println("Subscription created: " + subscription.getId()); - return subscription.getId(); - } catch (StripeException e) { - throw new RuntimeException(e); - } - } - - public String changeCustomerPayment(String name, String email){ - Stripe.apiKey = privateKey; - String paymentMethodCode = "pm_card_chargeCustomerFail"; - try { - PaymentMethod paymentMethod = PaymentMethod.retrieve(paymentMethodCode); - - String customerId = getCustomer(name, email); - log.info("customerId: {}", customerId); - - // 附加支付方式到客户 - PaymentMethodAttachParams attachParams = PaymentMethodAttachParams.builder() - .setCustomer(customerId) - .build(); - paymentMethod.attach(attachParams); - // 更新客户的默认支付方式 - CustomerUpdateParams params = CustomerUpdateParams.builder() - .setInvoiceSettings( - CustomerUpdateParams.InvoiceSettings.builder() - .setDefaultPaymentMethod(paymentMethod.getId()) - .build() - ) - .build(); - - // 更新客户信息 - Customer customer = Customer.retrieve(customerId); - customer.update(params); - - return paymentMethod.getId(); - - } catch (StripeException e) { - throw new RuntimeException(e); - } - } - - public List> getCustomerPaymentMethod(String name, String email){ - Stripe.apiKey = privateKey; - try { - String customerId = getCustomer(name, email); - Customer customer = Customer.retrieve(customerId); - PaymentMethodCollection paymentMethodCollection = customer.listPaymentMethods(); - List data = paymentMethodCollection.getData(); - ArrayList> resp = new ArrayList<>(); - data.forEach(paymentMethod -> { - Map map = new HashMap<>(); - if (paymentMethod.getType().equals("card")){ - map.put(paymentMethod.getId(),paymentMethod.getCard().getLast4()); - }else { - map.put(paymentMethod.getId(),null); - } - resp.add(map); - }); - - return resp; - // 方向: 向用户添加了多种付款方式,更改默认的付款方式后,默认付款方式付款失败后是否自动使用其他付款方式付款? - // 如果是的,则需要删除能成功的付款方式,保留唯一失败的付款方式进行续订付款失败测试 - } catch (StripeException e) { - throw new RuntimeException(e); - } - - } - - public String detachCustomerAllPaymentMethod(String name, String email){ - Stripe.apiKey = privateKey; - // 方向: 向用户添加了多种付款方式,更改默认的付款方式后,默认付款方式付款失败后是否自动使用其他付款方式付款? - // 如果是的,则需要删除能成功的付款方式,保留唯一失败的付款方式进行续订付款失败测试 - try { - String customerId = getCustomer(name, email); - Customer customer = Customer.retrieve(customerId); - PaymentMethodCollection paymentMethodCollection = customer.listPaymentMethods(); - List data = paymentMethodCollection.getData(); - data.forEach(paymentMethod -> { - try { - PaymentMethod retrieve = PaymentMethod.retrieve(paymentMethod.getId()); - PaymentMethodDetachParams params = PaymentMethodDetachParams.builder().build(); - retrieve.detach(params); - } catch (StripeException e) { - throw new RuntimeException(e); - } - }); - } catch (StripeException e) { - throw new RuntimeException(e); - } - return null; - } - - public void updateCancelReason(String subscriptionId, String reason){ - QueryWrapper qw = new QueryWrapper<>(); - qw.eq("subscription_id", subscriptionId); - SubscriptionInfo subscriptionInfo = subscriptionInfoMapper.selectOne(qw); - - if (!Objects.isNull(subscriptionInfo)) { - subscriptionInfo.setCancelReason(reason); - subscriptionInfoMapper.updateById(subscriptionInfo); - } - } - -// public String getIp(HttpServletRequest request) { -// String ipAddress = RequestInfoUtil.getIpAddress(request); -// if (!StringUtil.isNullOrEmpty(ipAddress)) { -// return getIPLocation(ipAddress); -// } -// -// return request.getRemoteAddr(); -// } - - - public String getStackTrace(Exception e, int maxLines) { - StringBuilder sb = new StringBuilder(); - StackTraceElement[] stackTraceElements = e.getStackTrace(); - int lines = Math.min(maxLines, stackTraceElements.length); - - for (int i = 0; i < lines; i++) { - sb.append(stackTraceElements[i].toString()).append("\n"); - } - // 如果堆栈信息超过 maxLines 行,添加提示 - if (stackTraceElements.length > maxLines) { - sb.append("... (More stack trace lines truncated)\n"); - } - return sb.toString(); - } - - - /* - * 优惠券实施: - * 1、由管理员创建优惠券,手动设置该优惠券对应的折扣 - * 2、用户在订阅时,手动输入优惠码 - * 3、使用stripe的字段 redeem_by 管理优惠券的有效期 - * 4、promotion code与 coupons需要绑定 - * 5、用户只能使用一次优惠券 - * - * 问题:对于一次付款和订阅,优惠码是否可以混用 - */ - public String createCoupon(CreateCouponDTO createCouponDTO){ - Stripe.apiKey = privateKey; - CouponCreateParams.Builder couponParams = CouponCreateParams.builder() - // 任何客户只能用一次这个优惠券 - .setDuration(CouponCreateParams.Duration.ONCE) - // percent_off 与 amount_off 不能同时设置 - .setPercentOff(BigDecimal.valueOf(createCouponDTO.getPercentOff())); - if (Objects.nonNull(createCouponDTO.getTimestamp())){ - couponParams.setRedeemBy(createCouponDTO.getTimestamp()); - } - try { - // 1、创建优惠券 - Coupon coupon = Coupon.create(couponParams.build()); - // 2、创建一个推广码 - PromotionCode promotionCode = createPromotionCode(coupon.getId(), createCouponDTO.getMaxRedemptions()); - // 3、落库 - ProductCoupons productCoupons = new ProductCoupons(coupon.getId(), createCouponDTO.getTimestamp(), promotionCode.getId(), - promotionCode.getCode(), createCouponDTO.getMaxRedemptions(), createCouponDTO.getPercentOff(), - createCouponDTO.getCommissionRate(), createCouponDTO.getCooperator(), createCouponDTO.getRemark()); - productCoupons.setCreateTime(LocalDateTime.now()); - productCouponsMapper.insert(productCoupons); - - return promotionCode.getCode(); - } catch (StripeException e) { - throw new RuntimeException(e); - } - } - - public PromotionCode createPromotionCode(String couponId, Long maxRedemption){ - Stripe.apiKey = privateKey; - PromotionCodeCreateParams.Builder promotionCodeParams = PromotionCodeCreateParams.builder() - .setCoupon(couponId) - .setRestrictions(PromotionCodeCreateParams.Restrictions.builder().build()); - if (Objects.nonNull(maxRedemption)){ - promotionCodeParams.setMaxRedemptions(maxRedemption); - } - - try { - return PromotionCode.create(promotionCodeParams.build()); - } catch (StripeException e) { - throw new RuntimeException(e); - } - } - - public ProductCoupons updateCouponsInfo(Long id, Long paidCommission, String cooperator, String remark){ - ProductCoupons productCoupons = productCouponsMapper.selectById(id); - if (Objects.isNull(productCoupons)){ - throw new BusinessException("Unknown Promotion Code"); - } - boolean flag = false; - if (!StringUtil.isNullOrEmpty(cooperator)){ - productCoupons.setCooperator(cooperator); - flag = true; - } - if (!StringUtil.isNullOrEmpty(remark)){ - productCoupons.setRemark(remark); - flag = true; - } - if (Objects.nonNull(paidCommission)){ - productCoupons.setPaidCommission(paidCommission); - flag = true; - } - if (flag){ - productCoupons.setUpdateTime(LocalDateTime.now()); - productCouponsMapper.updateById(productCoupons); - } - return productCoupons; - } - - public ProductCoupons checkProductCoupon(String promotionCode){ - Stripe.apiKey = privateKey; - Long accountId = UserContext.getUserHolder().getId(); - // 1、从数据库查找promotionCode对应的promotionCodeId - ProductCoupons productCoupons = productCouponsMapper.selectOne(new QueryWrapper().eq("promotion_code", promotionCode)); - if (Objects.nonNull(productCoupons)){ - // 2、查绑定的Coupons是否存在以及是否过期 - long epochSecondNow = Instant.now().getEpochSecond(); - Long redeemBy = productCoupons.getRedeemBy(); - if (redeemBy < epochSecondNow){ - throw new BusinessException("this.promotion.code.has.expired"); - } else { - // 判断该用户是否有成功使用过这个推广码 - List paymentInfoByPromCode = paymentInfoService.getPaymentInfoByPromCode(accountId, promotionCode); - if (!paymentInfoByPromCode.isEmpty()) { - // 已使用过推广码,状态无效 - if (paymentInfoByPromCode.size() > 1) { - log.error("用户[{}]多次成功使用优惠码[{}]", accountId, promotionCode); - } - log.info("用户[{}]已成功使用过优惠码[{}]", accountId, promotionCode); - throw new BusinessException("one.time.limit.per.customer"); - } - } - }else { - throw new BusinessException("this.promotion.code.is.invalid"); - } - return productCoupons; - } - - public CheckCouponsVO checkProductCoupon(String promotionCode, Long price){ - Stripe.apiKey = privateKey; - Long accountId = UserContext.getUserHolder().getId(); - CheckCouponsVO checkCouponsVO = new CheckCouponsVO(); - // 1、从数据库查找promotionCode对应的promotionCodeId - ProductCoupons productCoupons = productCouponsMapper.selectOne(new QueryWrapper().eq("promotion_code", promotionCode)); - if (Objects.nonNull(productCoupons)){ - // 2、查绑定的Coupons是否存在以及是否过期 - long epochSecondNow = Instant.now().getEpochSecond(); - Long redeemBy = productCoupons.getRedeemBy(); - if (redeemBy < epochSecondNow){ - String msg = BusinessException.getMessageFromResource("this.promotion.code.has.expired"); - checkCouponsVO.setMessage(msg); - checkCouponsVO.setStatus("expired"); - }else { - // 判断该用户是否有成功使用过这个推广码 - List paymentInfoByPromCode = paymentInfoService.getPaymentInfoByPromCode(accountId, promotionCode); - if (paymentInfoByPromCode.isEmpty()) { - // 未使用过推广码,状态有效 - checkCouponsVO.setStatus("valid"); - checkCouponsVO.setDiscountedPrice(price * (1 - productCoupons.getPercentOff() / 100)); - } else { - // 已使用过推广码,状态无效 - if (paymentInfoByPromCode.size() > 1) { - log.error("用户[{}]多次成功使用优惠码[{}]", accountId, promotionCode); - } - log.info("用户[{}]已成功使用过优惠码[{}]", accountId, promotionCode); - String msg = BusinessException.getMessageFromResource("one.time.limit.per.customer"); - checkCouponsVO.setMessage(msg); - checkCouponsVO.setStatus("invalid"); - } - } - }else { - String msg = BusinessException.getMessageFromResource("this.promotion.code.is.invalid"); - checkCouponsVO.setMessage(msg); - checkCouponsVO.setStatus("invalid"); - } - return checkCouponsVO; - } - - public ProductCoupons getProductCoupon(String promotionCode, String promotionCodeId){ - Stripe.apiKey = privateKey; - QueryWrapper qw = new QueryWrapper<>(); - // 1、从数据库查找promotionCode对应的promotionCodeId - if (!StringUtil.isNullOrEmpty(promotionCode)){ - qw.eq("promotion_code", promotionCode); - } - if (!StringUtil.isNullOrEmpty(promotionCodeId)){ - qw.eq("promotion_code_id", promotionCodeId); - } - return productCouponsMapper.selectOne(qw); - } - - public String retrieveCoupon(String couponId){ - Stripe.apiKey = privateKey; - try { - Coupon coupon = Coupon.retrieve(couponId); - log.info("retrieve的coupon: {}", coupon); - return JSON.toJSONString(coupon); - } catch (StripeException e) { - throw new RuntimeException(e); - } - } - - public String retrievePromotionCode(String promotionCode){ - Stripe.apiKey = privateKey; - try { - ProductCoupons productCoupon = getProductCoupon(promotionCode, null); - PromotionCode retrieve = PromotionCode.retrieve(productCoupon.getPromotionCodeId()); - log.info("retrieve的promotionCode: {}", retrieve); - return JSON.toJSONString(retrieve); - } catch (StripeException e) { - throw new RuntimeException(e); - } - } - - public IPage getAllCoupons(QueryCouponsPageDTO queryCouponsPageDTO){ - // 分页 + 按条件查询 - QueryWrapper queryWrapper = new QueryWrapper<>(); - if (!StringUtil.isNullOrEmpty(queryCouponsPageDTO.getOrderById()) && queryCouponsPageDTO.getOrderById().equals("DESC")){ - queryWrapper.orderByDesc("id"); - } - if (!StringUtil.isNullOrEmpty(queryCouponsPageDTO.getPromotionCode())){ - queryWrapper.like("promotion_code", queryCouponsPageDTO.getPromotionCode()); - } - if (Objects.nonNull(queryCouponsPageDTO.getIsExpired())){ - long epochSecond = Instant.now().getEpochSecond(); - if (queryCouponsPageDTO.getIsExpired()){ - queryWrapper.lt("redeem_by", epochSecond); - }else { - queryWrapper.gt("redeem_by", epochSecond); - } - } - if (!StringUtil.isNullOrEmpty(queryCouponsPageDTO.getCooperator())){ - queryWrapper.like("cooperator", queryCouponsPageDTO.getCooperator()); - } - if (!StringUtil.isNullOrEmpty(queryCouponsPageDTO.getStartTime())){ - queryWrapper.gt("create_time", queryCouponsPageDTO.getStartTime()); - } - if (!StringUtil.isNullOrEmpty(queryCouponsPageDTO.getEndTime())){ - queryWrapper.lt("create_time", queryCouponsPageDTO.getEndTime()); - } - - return productCouponsMapper.selectPage(new Page<>(queryCouponsPageDTO.getPage(), queryCouponsPageDTO.getSize()), queryWrapper); - } - - @Transactional - public void deleteCoupon(Long id){ - Stripe.apiKey = privateKey; - ProductCoupons productCoupons = productCouponsMapper.selectById(id); - if (Objects.isNull(productCoupons)){ - throw new BusinessException("unknown promotion code"); - } - try { - Coupon coupon = Coupon.retrieve(productCoupons.getCouponId()); - coupon.delete(); - log.info("coupon {} 删除成功", productCoupons.getCouponId()); - productCouponsMapper.deleteById(id); - } catch (StripeException e) { - log.error("未知coupons,无法通过couponId: {} 获得Coupons", productCoupons.getCouponId()); - } - } - -} +package com.ai.da.service.impl; + +import com.ai.da.common.config.exception.BusinessException; +import com.ai.da.common.constant.CommonConstant; +import com.ai.da.common.context.UserContext; +import com.ai.da.common.enums.*; +import com.ai.da.common.utils.DateUtil; +import com.ai.da.common.utils.SendEmailUtil; +import com.ai.da.mapper.primary.AccountMapper; +import com.ai.da.mapper.primary.PaymentInfoMapper; +import com.ai.da.mapper.primary.ProductCouponsMapper; +import com.ai.da.mapper.primary.SubscriptionInfoMapper; +import com.ai.da.mapper.primary.entity.*; +import com.ai.da.model.dto.CreateCouponDTO; +import com.ai.da.model.dto.ProductPurchaseDTO; +import com.ai.da.model.dto.QueryCouponsPageDTO; +import com.ai.da.model.dto.SubscriptionEmailParamsDTO; +import com.ai.da.model.vo.CheckCouponsVO; +import com.ai.da.service.*; +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.google.gson.Gson; +import com.stripe.Stripe; +import com.stripe.exception.InvalidRequestException; +import com.stripe.exception.SignatureVerificationException; +import com.stripe.exception.StripeException; +import com.stripe.model.*; +import com.stripe.model.Product; +import com.stripe.model.checkout.Session; +import com.stripe.net.Webhook; +import com.stripe.param.*; +import com.stripe.param.checkout.SessionCreateParams; +import io.netty.util.internal.StringUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.*; +import java.util.stream.Collectors; + +@SuppressWarnings("LoggingSimilarMessage") +@Service +@Slf4j +public class StripeServiceImpl implements StripeService { + + @Resource + private OrderInfoService orderInfoService; + @Resource + private PayPalCheckoutService payPalCheckoutService; + @Resource + private PaymentInfoService paymentInfoService; + @Resource + private CreditsService creditsService; + @Resource + private RefundInfoService refundInfoService; + @Resource + private AccountService accountService; + + @Resource + private AccountMapper accountMapper; + @Resource + private SubscriptionInfoMapper subscriptionInfoMapper; + @Resource + private PaymentInfoMapper paymentInfoMapper; + @Resource + private ProductCouponsMapper productCouponsMapper; + + @Value("${stripe.private-key}") + private String privateKey; + + @Value("${stripe.webhook-sign-secret}") + private String signSecret; + + @Value("${orderList.link}") + private String orderListLink; + + @Value("${stripe.paymentMethodConfiguration}") + private String paymentMethodConfigurationId; + + @Override + @Transactional(rollbackFor = Exception.class) + public String pay(ProductPurchaseDTO productPurchaseDTO, HttpServletRequest request) { + Stripe.apiKey = privateKey; + + //创建支付信息得到url + // 一次性支付和周期扣款,需要区分mode: payment || subscription || setup + SessionCreateParams.Builder sessionBuilder = new SessionCreateParams.Builder(); + ProductEnum productEnum; + switch (productPurchaseDTO.getProductName()){ + case "CreditsPurchase": + productEnum = ProductEnum.CreditsProduct; + productPurchaseDTO.setAutoRenewal(false); + break; + case "Subscription": + switch (productPurchaseDTO.getSubscribeType()){ + case "Month": + productEnum = ProductEnum.MonthlySubscription; + break; + case "Year": + productEnum = ProductEnum.AnnualSubscription; + break; + case "Day": + productEnum = ProductEnum.DailySubscription; + break; + default: + throw new BusinessException("unknown subscription type"); + } + + // 添加优惠券(只允许在订阅时使用优惠券) + String promotionCode = productPurchaseDTO.getPromotionCode(); + if (!StringUtil.isNullOrEmpty(promotionCode)){ + ProductCoupons productCoupon = checkProductCoupon(promotionCode);; + if (productCoupon != null){ + sessionBuilder.addDiscount(SessionCreateParams.Discount.builder() + .setPromotionCode(productCoupon.getPromotionCodeId()).build()); + } + } + // 只有订阅时才允许使用推广码优惠 +// sessionBuilder.setAllowPromotionCodes(true); + break; + default: + throw new BusinessException("unknown product type"); + } + log.info("生成订单"); + String payType; + byte autoRenewal; + if (productPurchaseDTO.getAutoRenewal()){ + payType = "recurring"; + autoRenewal = 1; + }else { + payType = "one_time"; + autoRenewal = 0; + } + OrderInfo orderInfo = orderInfoService.createOrderByProductId(productPurchaseDTO.getQuantity(), + PayTypeEnum.STRIPE.getType(), productEnum, request, autoRenewal); + + try { + Long id = UserContext.getUserHolder().getId(); + com.ai.da.mapper.primary.entity.Account account = accountMapper.selectById(id); + // 获取或创建产品 + String productId = getProduct(productEnum.getName()); + // 获取或创建价格 + String priceId = getPrice(productEnum.getPrice(), productId, payType, productPurchaseDTO.getSubscribeType()); + // 获取或创建customer + String customerId = getCustomer(account.getUserName(), account.getUserEmail()); + log.info("customerId:{}", customerId); + // 获取自定义订单号 + String orderId = orderInfo.getOrderNo(); + + + if (payType.equals("recurring")){ + sessionBuilder.setMode(SessionCreateParams.Mode.SUBSCRIPTION); + sessionBuilder.setSubscriptionData(SessionCreateParams.SubscriptionData.builder().setDescription("AiDA - " + orderId).build()); + }else { + sessionBuilder.setMode(SessionCreateParams.Mode.PAYMENT); + sessionBuilder.setPaymentIntentData(SessionCreateParams.PaymentIntentData.builder().setDescription("AiDA - " + orderId).build()); + // one-time 手动创建发票;订阅会自动创建invoice + sessionBuilder.setInvoiceCreation(SessionCreateParams.InvoiceCreation.builder().setEnabled(Boolean.TRUE).build()); + } + + sessionBuilder.setPaymentMethodConfiguration(paymentMethodConfigurationId); + sessionBuilder.setCustomer(customerId); + sessionBuilder.setSuccessUrl(productPurchaseDTO.getReturnUrl());//可自定义成功页面 + sessionBuilder.setLocale(account.getLanguage().equals("CHINESE_SIMPLIFIED") ? SessionCreateParams.Locale.ZH : SessionCreateParams.Locale.EN); + sessionBuilder.addLineItem( + SessionCreateParams.LineItem.builder() + .setQuantity((long) productPurchaseDTO.getQuantity()) + .setPrice(priceId) + .build()); + sessionBuilder.putMetadata("orderId", orderId); //通过订单号关联用于检索支付信息(可选) + + Session session = Session.create(sessionBuilder.build()); + List paymentMethodTypes = session.getPaymentMethodTypes(); + log.info("paymentMethodTypes: {}", paymentMethodTypes); + + Session.PaymentMethodConfigurationDetails paymentMethodConfigurationDetails = session.getPaymentMethodConfigurationDetails(); + log.info("paymentMethodConfigurationDetails ID: {}", paymentMethodConfigurationDetails.getId()); + log.info("sessionId:{}", session.getId()); //退款方式1:拿到sessionId入库,退款的时候根据这个id找到PaymentIntent的id然后发起退款 + + // 更新order信息 + orderInfoService.updateOrderNoById(orderInfo.getId(), orderId); + return session.getUrl(); + } catch (BusinessException e) { + throw e; + } catch (InvalidRequestException e) { + log.info("创建会话出现异常:", e); + throw new BusinessException(e.getMessage().substring(0, e.getMessage().indexOf(";"))); + } catch (Exception e) { + log.error("创建支付会话出现异常:", e); + } + return ""; + } + + // 获取产品ID + private String getProduct(String productName) throws StripeException { + Stripe.apiKey = privateKey; + // 1、获取所有的产品 + ProductCollection productCollection = Product.list(ProductListParams.builder().setActive(Boolean.TRUE).build()); + // 2、取一个指定名称的产品 + for (Product product : productCollection.getData()) { + if (product.getName().equals(productName)) { + return product.getId(); + } + } + // 3、在现有产品中没有找到指定产品,新建产品 + Map params = new HashMap<>(); + params.put("name", productName); + Product product = Product.create(params); + return product.getId(); + } + + /** + * 获取价格 + * + * @param priceValue 价格值 + * @param payType recurring || one_time + * @param recurringType monthly || yearly + */ + private String getPrice(Long priceValue, String productId, String payType, String recurringType) throws StripeException { + Stripe.apiKey = privateKey; + PriceCollection priceCollection = Price.list(PriceListParams.builder() + .setActive(Boolean.TRUE) + .setProduct(productId).build()); + for (Price price : priceCollection.getData()) { + // stripe的金额单位为分,所以这里需要 ×100 + if (price.getUnitAmount().equals(priceValue * 100) && price.getType().equals(payType)) { + return price.getId(); + } + } + + Price price = createPrice(priceValue, productId, payType, recurringType); + return price.getId(); + } + + private Price createPrice(Long priceValue, String productId, String payType, String recurringType) throws StripeException { + BigDecimal actualAmount = new BigDecimal(priceValue * 100); //stripe的默认单位是分,即传入的amount实际上小数点会被左移两位 + + PriceCreateParams.Builder priceCreateParams = new PriceCreateParams.Builder(); + priceCreateParams.setBillingScheme(PriceCreateParams.BillingScheme.PER_UNIT); + priceCreateParams.setCurrency("HKD"); + priceCreateParams.setProduct(productId); + priceCreateParams.setUnitAmount(actualAmount.longValue()); + + if (payType.equals("recurring")) { + PriceCreateParams.Recurring.Builder recurring = new PriceCreateParams.Recurring.Builder(); + // One of day, week, month or year. + recurring.setInterval(PriceCreateParams.Recurring.Interval.valueOf(recurringType.toUpperCase())); + // The number of intervals (specified in the interval attribute) between subscription billings. + // For example, interval=month and interval_count=3 bills every 3 months. + recurring.setIntervalCount(1L); + priceCreateParams.setRecurring(recurring.build()); + } + + return Price.create(priceCreateParams.build()); + } + + @Resource + private EmailService emailService; + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean notify(HttpServletRequest request) { + log.info("stripe异步通知进行中"); + String payload = null; + String sigHeader = null; + String endpointSecret = signSecret; + try { + sigHeader = request.getHeader("Stripe-Signature"); + payload = payPalCheckoutService.getBody(request); + } catch (Exception e) { + log.info("stripe 支付回调参数解析异常:errorMsg {}", e.getMessage()); + log.info("request sigHeader = {}", sigHeader); + log.info("request body = {}", JSON.toJSONString(payload)); + e.printStackTrace(); + return Boolean.FALSE; + } + + Event event; + try { + assert sigHeader != null; + event = Webhook.constructEvent(payload, sigHeader, endpointSecret); + } catch (SignatureVerificationException e) { + log.info("stripe 验签,获取事件异常, errorMsg={}", e.getMessage()); + log.info("request sigHeader = {}", sigHeader); + log.info("request body = {}", JSON.toJSONString(payload)); + e.printStackTrace(); + return Boolean.FALSE; + } + + //获取自定义参数 + // Deserialize the nested object inside the event + assert event != null; + EventDataObjectDeserializer dataObjectDeserializer = event.getDataObjectDeserializer(); + StripeObject stripeObject ; + if (dataObjectDeserializer.getObject().isPresent()) { + stripeObject = dataObjectDeserializer.getObject().get(); + } else { + log.info("stripe 验签失败!"); + log.info("request sigHeader = {}", sigHeader); + log.info("request body = {}", JSON.toJSONString(payload)); + return Boolean.FALSE; + } + log.info("stripe验签成功"); + boolean response = Boolean.TRUE; + + log.info("回调事件 {}", event.getType()); + if (stripeObject instanceof Session){ + Session session = (Session) stripeObject; + if (event.getType().equals("checkout.session.completed")) { + response = processOrder(session); + }else if (event.getType().equals("checkout.session.expired")){ + String orderNo = session.getMetadata().get("orderId"); + // 会话过期 未支付 且之后没有支付成功的订单 + response = processExpiredOrder(orderNo); + } + } else if (stripeObject instanceof Subscription){ + Subscription subscription = (Subscription) stripeObject; + if (event.getType().equals("customer.subscription.created")){ + // 添加数据到t_subscription_info表 需记录订阅id。需要判断订阅的状态是否active吗 ?? + createSubscription(subscription); + log.info("创建连续订阅"); + } else if (event.getType().equals("customer.subscription.updated")){ + // 更新订阅信息 + SubscriptionInfo subscriptionInfo = updateSubscription(subscription); + log.info("订阅更新"); + if (subscription.getStatus().equals("active")){ + response = sendEmail(subscription.getId(), null, null); + } + // 续订支付失败,邮件通知用户 + if (subscription.getStatus().equals("past_due")){ + // 发送续订失败邮件 + response = sendRenewalFailEmail(null, subscription.getId(), subscriptionInfo.getOrderNo()); + + } + } else if (event.getType().equals("customer.subscription.deleted")){ + SubscriptionInfo subscriptionInfo = updateSubscription(subscription); + log.info("用户 {} 取消连续订阅 {}", subscriptionInfo.getAccountId(), subscription.getId()); + if (subscriptionInfo.getCancelNotified() == (byte)0){ + log.info("取消订阅 邮件通知商家"); + response = sendEmail(subscription.getId(), "cancel", null); + if (response){ + subscriptionInfo.setCancelNotified((byte)1); + subscriptionInfoMapper.updateById(subscriptionInfo); + // 更新订单信息 + OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(subscriptionInfo.getOrderNo()); + orderInfo.setAutoRenewal((byte)0); + } + } + + }/* else if (event.getType().equals("customer.subscription.paused")){ + updateSubscription(subscription); + } else if (event.getType().equals("customer.subscription.resumed")){ + updateSubscription(subscription); + log.info("用户订阅恢复"); + }*/ + } else if (stripeObject instanceof Invoice) { + Invoice invoice = (Invoice) stripeObject; + if (event.getType().equals("invoice.paid")) { + // 新增支付成功的信息,返回orderNo,表示,该回调第一次被记录 + PaymentInfo paymentInfo = paymentInfoService.createOrUpdatePaymentInfoForStripe(invoice); + + // 当前支付没有被通知时才需要发送通知邮件 + if (paymentInfo.getNotified().equals(0)) { + // 更新t_order_info中的total_fee,记录该订单的累计付款金额 + orderInfoService.updateTotalFeeByOrderNo(paymentInfo.getOrderNo()); + // 邮件通知商家和用户 + String billingReason = invoice.getBillingReason(); + switch (billingReason) { + case "subscription_create": + response = sendEmail(invoice.getSubscription(), "new", null); + break; + case "subscription_cycle": + response = sendEmail(invoice.getSubscription(), "renewal", null); + break; + case "manual": + boolean b = invoice.getLines().getData().get(0).getDescription().endsWith("Subscription"); + if (b) { + // 非自动续订式订阅,Stripe不会创建Subscription,所以invoice中不会有subscriptionId + response = sendEmail(null, "new", paymentInfo.getOrderNo()); + } + break; + } + } + } else if (event.getType().equals("invoice.payment_failed")) { + // 更新支付信息 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("transaction_id", invoice.getId()); + PaymentInfo paymentInfo = paymentInfoService.getBaseMapper().selectOne(queryWrapper); + if (!Objects.isNull(paymentInfo)){ + String type = invoice.getBillingReason().equals("subscription_create") ? "new" : + invoice.getBillingReason().equals("subscription_cycle") ? "renewal" : invoice.getBillingReason(); + Gson gson = new Gson(); + String json = gson.toJson(invoice); + paymentInfo.setContent(json); + paymentInfo.setType(type); + paymentInfo.setHostedInvoiceUrl(invoice.getHostedInvoiceUrl()); + paymentInfoService.updateById(paymentInfo); + + // 发送续订失败邮件 + response = sendRenewalFailEmail(invoice.getId(), null, paymentInfo.getOrderNo()); + }else { + // 新增支付信息 + PaymentInfo paymentInfoFail = paymentInfoService.createOrUpdatePaymentInfoForStripe(invoice); + // 发送新订阅失败邮件 + response = sendEmail(paymentInfoFail.getOrderNo()); + } + } + }else if (stripeObject instanceof Charge) { + Charge charge = (Charge) stripeObject; + String orderNo = charge.getDescription().replace("AiDA - ", ""); + OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo); + if (Objects.isNull(orderInfo)){ + // 说明该回调不是从AiDA订阅获得 + return true; + } + if (event.getType().equals("charge.failed")){ + // 添加支付信息 && 更新支付信息 + // 支付失败时,无法通过invoice_id获取支付方式,所以使用charge.failed回调添加支付信息 + paymentInfoService.createOrUpdatePaymentInfoForStripe(charge); + + orderInfo.setOrderStatus(OrderStatusEnum.FAILURE.getType()); + orderInfo.setNote(charge.getFailureMessage()); + orderInfoService.updateById(orderInfo); + }else if (event.getType().equals("charge.succeeded")){ + orderInfo.setOrderStatus(OrderStatusEnum.SUCCESS.getType()); + orderInfo.setNote(""); + orderInfoService.updateById(orderInfo); + }else if (event.getType().equals("charge.refunded")){ + // 更新退款信息 + RefundInfo refundInfo = refundInfoService.updateRefundForStripe(charge); + // 更新 t_payment_info的支付状态 + if (Objects.nonNull(refundInfo)){ + paymentInfoService.updatePaymentRefundStatus(charge); + } + } + }else if (stripeObject instanceof Refund){ + Refund refund = (Refund) stripeObject; + if (event.getType().equals("refund.created")){ + // 新增退款信息 + refundInfoService.createRefundForStripe(refund); + }else if (event.getType().equals("refund.updated")){ + // 根据***id更新退款记录信息 + RefundInfo refundInfo = refundInfoService.updateRefundStatusForStripe(refund); + if (Objects.isNull(refundInfo)){ + // 等事件先创建,再更新。回调事件的顺序随机 + response = false; + } + } + } + log.info("回调事件 {} 处理完成", event.getType()); + return response; + } + + public boolean processOrder(Session session) { + Stripe.apiKey = privateKey; + String orderNo = session.getMetadata().get("orderId"); + float totalAmount = new BigDecimal(session.getAmountTotal()).divide(new BigDecimal(100), 2, RoundingMode.HALF_UP).floatValue(); + + boolean resp = true; + try { + //处理重复通知 + //接口调用的幂等性:无论接口被调用多少次,以下业务执行一次 +// String orderStatus = orderInfoService.getOrderStatus(orderNo); + OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(orderNo); + String orderStatus = orderByOrderNo.getOrderStatus(); + // 当订单状态处于未支付或超时已关闭时,更新订单状态,其他状态均不更新订单状态 + if (!OrderStatusEnum.NOT_PAY.getType().equals(orderStatus) && !OrderStatusEnum.TIMEOUT_CLOSED.getType().equals(orderStatus)) { + log.info("订单状态 : {}", orderStatus); + }else { + //更新订单状态 + orderInfoService.updateStatusByOrderNo(orderNo, OrderStatusEnum.SUCCESS); + log.info("Stripe 订单:{} 状态更新成功", orderNo); + } + + if (orderByOrderNo.getTitle().startsWith("积分购买")){ + // 查询当前订单的积分是否已添加 + CreditsDetail creditsDetail = creditsService.queryDetailByTaskId(orderNo); + if (Objects.isNull(creditsDetail)){ + float quantity = totalAmount / ProductEnum.CreditsProduct.getPrice(); + // 更新积分 + creditsService.buyCredits(orderByOrderNo.getAccountId(), quantity); + // 添加积分变更记录 + creditsService.insertToCreditsDetail(orderByOrderNo.getAccountId(), + CreditsEventsEnum.BUY_CREDITS.getName() + "--Stripe", + String.valueOf((Long.parseLong(CreditsEventsEnum.BUY_CREDITS.getValue()) * quantity)), + "positive", orderNo); + log.info("用户:{} 积分信息更新成功", orderByOrderNo.getAccountId()); + } + }else if (orderByOrderNo.getTitle().endsWith("Subscription") && orderByOrderNo.getAutoRenewal() == (byte)0){ + String invoiceId = session.getInvoice(); + Invoice invoice = Invoice.retrieve(invoiceId); + InvoiceLineItem invoiceLineItem = invoice.getLines().getData().get(0); + String description = invoiceLineItem.getDescription(); + Long amount = invoiceLineItem.getAmount(); + log.info("单次订阅 description : {}, amount: {} 分", description, amount); + boolean b = createSubscriptionAndUpdateAccount(orderNo, orderByOrderNo.getAccountId(), description, amount); + // 邮件通知用户 + if (b){ + resp = sendEmail(null, "new", orderNo); + } + log.info("单次订阅订单:{} 处理完成", orderNo); + } + } catch (Exception e) { + log.info(e.getMessage()); + resp = false; + } + return resp; + } + + private boolean processExpiredOrder(String orderNo) { + // 支付失败 通知商家的条件 1、会话过期 2、支付失败 3、这个用户在这个支付失败后再无支付成功的订阅 + // 1、获取当前订单的支付状态 +// String orderNo = session.getMetadata().get("orderId"); + OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(orderNo); + // 2、确认订单状态为支付失败 + boolean resp = true; + if (!Objects.isNull(orderByOrderNo) && orderByOrderNo.getOrderStatus().equals(OrderStatusEnum.FAILURE.getType())) { + // 3、判断失败订单之后再无成功的订单 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("account_id", orderByOrderNo.getAccountId()); + queryWrapper.gt("create_time", orderByOrderNo.getCreateTime()); + queryWrapper.eq("order_status", OrderStatusEnum.SUCCESS.getType()); + queryWrapper.likeLeft("title", "Subscription"); + List orderInfos = orderInfoService.getBaseMapper().selectList(queryWrapper); + if (orderInfos.isEmpty()) { + // 4、判断当前订单有没有订阅信息 + QueryWrapper qw = new QueryWrapper<>(); + qw.eq("order_no", orderNo); + SubscriptionInfo subscriptionInfo = subscriptionInfoMapper.selectOne(qw); + // 发送邮件通知商家用户支付失败 + if (Objects.isNull(subscriptionInfo) + || subscriptionInfo.getStatus().equals("incomplete") + || subscriptionInfo.getStatus().equals("incomplete_expired")) { + resp = sendEmail(orderNo); + }else { + // todo 续订失败 应该不会走这里 + resp = sendEmail(subscriptionInfo.getSubscriptionId(), "fail_renewal", null); + } + } + } + return resp; + + } + + @Transactional(rollbackFor = Exception.class) + public SubscriptionInfo createSubscription(Subscription subscription){ + // 确认当前subscription是否已经记录 + SubscriptionInfo subscriptionInfo = getSubscriptionInfoBySubId(subscription.getId()); +// SubscriptionInfo subscriptionInfo = subscriptionInfoMapper.selectOne(qw); + if (Objects.isNull(subscriptionInfo)) { + String description = subscription.getDescription(); + String orderNo = description.replace("AiDA - ", ""); + OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo); + + // 从回调信息中获取recurring type + SubscriptionItem subscriptionItem = subscription.getItems().getData().get(0); + String interval = subscriptionItem.getPrice().getRecurring().getInterval(); + + subscriptionInfo = new SubscriptionInfo(); + subscriptionInfo.setAccountId(orderInfo.getAccountId()); + subscriptionInfo.setOrderNo(orderNo); + subscriptionInfo.setSubscriptionId(subscription.getId()); + subscriptionInfo.setType(interval); + subscriptionInfo.setStatus(subscription.getStatus()); + subscriptionInfo.setNextPayDate(DateUtil.changeTimeStampFormat(subscription.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy_EEEE)); + subscriptionInfo.setCurrentPeriodStart(subscription.getCurrentPeriodStart()); + subscriptionInfo.setCurrentPeriodEnd(subscription.getCurrentPeriodEnd()); + subscriptionInfo.setCreateTime(LocalDateTime.now()); + + int rows = subscriptionInfoMapper.insertIgnore(subscriptionInfo); + log.info("Subscription info insert affect rows : {}", rows); + + if (subscriptionInfo.getStatus().equals("active")){ + log.info("创建订阅更新账号信息"); + // 更新账号到期时间 + boolean b = accountService.updateAccountValidity(subscriptionInfo.getAccountId(), subscriptionInfo.getCurrentPeriodEnd()); + // 更新账号身份和积分 + if (b) accountService.updateUserRoleAndCredits(subscriptionInfo.getAccountId(), interval); + } + } + return subscriptionInfo; + } + + /** + * 非自动续订订阅 + * Stripe不会自动创建Subscription,所以没有subscription相关的回调,无法触发订阅相关的处理代码 + */ + public boolean createSubscriptionAndUpdateAccount(String orderNo, Long accountId, String description, Long amount){ + QueryWrapper qw = new QueryWrapper<>(); + qw.eq("order_no", orderNo); + SubscriptionInfo subscriptionInfo = subscriptionInfoMapper.selectOne(qw); + if (Objects.isNull(subscriptionInfo)) { + String interval; + // 获取当前时间戳(秒级) + long currentPeriodStart = Instant.now().getEpochSecond();; + long currentPeriodEnd; +// InvoiceLineItem invoiceLineItem = invoice.getLines().getData().get(0); + if (description.equals(ProductEnum.DailySubscription.getName()) + && amount.equals(ProductEnum.DailySubscription.getPrice() * 100)){ + interval = "day"; + // 获取一天后的时间戳(秒级) + ZonedDateTime now = ZonedDateTime.now(); + currentPeriodEnd = now.plusDays(1).toEpochSecond(); + }else if (description.equals(ProductEnum.MonthlySubscription.getName()) + && amount.equals(ProductEnum.MonthlySubscription.getPrice() * 100)){ + interval = "month"; + // 获取一天后的时间戳(秒级) + ZonedDateTime now = ZonedDateTime.now(); + currentPeriodEnd = now.plusMonths(1).toEpochSecond(); + }else if (description.equals(ProductEnum.AnnualSubscription.getName()) + && amount.equals(ProductEnum.AnnualSubscription.getPrice() * 100)){ + interval = "year"; + // 获取一天后的时间戳(秒级) + ZonedDateTime now = ZonedDateTime.now(); + currentPeriodEnd = now.plusYears(1).toEpochSecond(); + }else { + log.error("未知订阅类型"); + return false; + } + subscriptionInfo = new SubscriptionInfo(); + subscriptionInfo.setAccountId(accountId); + subscriptionInfo.setOrderNo(orderNo); + subscriptionInfo.setType(interval); + subscriptionInfo.setStatus("canceled"); + subscriptionInfo.setCurrentPeriodStart(currentPeriodStart); + subscriptionInfo.setCurrentPeriodEnd(currentPeriodEnd); + subscriptionInfo.setCreateTime(LocalDateTime.now()); + + subscriptionInfoMapper.insertIgnore(subscriptionInfo); + + log.info("创建订阅, 更新账号信息"); + // 更新账号到期时间 + boolean b = accountService.updateAccountValidity(subscriptionInfo.getAccountId(), subscriptionInfo.getCurrentPeriodEnd()); + // 更新账号身份和积分 + if (b) accountService.updateUserRoleAndCredits(subscriptionInfo.getAccountId(), interval); + return true; + } + return true; + } + + public SubscriptionInfo getSubscriptionInfoBySubId(String subId){ + QueryWrapper qw = new QueryWrapper<>(); + qw.eq("subscription_id", subId); + + List subscriptionInfos = subscriptionInfoMapper.selectList(qw); + if (subscriptionInfos.size() == 1){ + return subscriptionInfos.get(0); + }else if (subscriptionInfos.size() > 1) { + // 如果新建了多个订阅,则筛选出状态为active的订单 + Optional activeSubscriptionInfo = subscriptionInfos.stream() + .filter(sub -> sub.getStatus().equals("active")) + .findFirst(); + + return activeSubscriptionInfo.orElseGet(() -> subscriptionInfos.get(0)); + }else { + return null; + } + } + + public SubscriptionInfo getLatestSubscriptionInfoByAccountId(Long accountId){ + QueryWrapper qw = new QueryWrapper<>(); + qw.eq("account_id", accountId).orderByDesc("id"); + List subscriptionInfos = subscriptionInfoMapper.selectList(qw); + if (subscriptionInfos.isEmpty()){ + return null; + }else { + return subscriptionInfos.get(0); + } + } + + @Transactional(rollbackFor = Exception.class) + public SubscriptionInfo updateSubscription(Subscription subscription){ + // 获取当前是否有已经记录的subscriptionInfo + SubscriptionInfo subscriptionInfo = createSubscription(subscription); + // 用于标志数据有没有变动,避免在没有改动的情况下频繁的更新数据库 + boolean flag = false; + if (!subscriptionInfo.getStatus().equals(subscription.getStatus())){ + subscriptionInfo.setStatus(subscription.getStatus()); + flag = true; + } + if (!subscriptionInfo.getCurrentPeriodStart().equals(subscription.getCurrentPeriodStart())){ + subscriptionInfo.setCurrentPeriodStart(subscription.getCurrentPeriodStart()); + flag = true; + } + if (!subscriptionInfo.getCurrentPeriodEnd().equals(subscription.getCurrentPeriodEnd())){ + subscriptionInfo.setCurrentPeriodEnd(subscription.getCurrentPeriodEnd()); + subscriptionInfo.setNextPayDate(DateUtil.changeTimeStampFormat(subscription.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy_EEEE)); + log.info("更新订阅更新账号信息"); + // 更新账号到期时间 + accountService.updateAccountValidity(subscriptionInfo.getAccountId(), subscriptionInfo.getCurrentPeriodEnd()); + // 更新账号身份和积分 + accountService.updateUserRoleAndCredits(subscriptionInfo.getAccountId(), subscriptionInfo.getType()); + log.info("更新 {} 账号到期时间为:{}", subscriptionInfo.getAccountId(), DateUtil.changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy_EEEE)); + flag = true; + } + if (subscriptionInfo.getStatus().equals("active")){ + // 更新账号到期时间 + boolean b = accountService.updateAccountValidity(subscriptionInfo.getAccountId(), subscriptionInfo.getCurrentPeriodEnd()); + // 更新账号身份和积分 + if (b) accountService.updateUserRoleAndCredits(subscriptionInfo.getAccountId(), subscriptionInfo.getType()); + } + if (flag){ + subscriptionInfo.setUpdateTime(LocalDateTime.now()); + subscriptionInfoMapper.updateById(subscriptionInfo); + } + return subscriptionInfo; + } + + // 取消连续订阅 将订阅从pause状态转为cancel状态(使用定时器,定期检索DB中,过期且不续订的订阅) + public void cancelSubscription(String subscriptionId, String cancelReason) { + Stripe.apiKey = privateKey; + log.info("cancel subscription"); + Long accountId = UserContext.getUserHolder().getId(); + com.ai.da.mapper.primary.entity.Account account = accountMapper.selectById(accountId); + List subscriptions = getSubscription(account.getUserName(), account.getUserEmail()); + // 获取status = active的订阅 + subscriptions.forEach(subscription -> { + if (subscription.getId().equals(subscriptionId)) { + try { + Subscription cancel = subscription.cancel(); + cancel.getStatus(); + log.info("用户 {} 申请取消连续订阅 {}", accountId, subscriptionId); + // 更新数据库 + updateCancelReason(subscriptionId, cancelReason); + } catch (StripeException e) { + log.error("订阅 {} 取消失败, error message : {}", subscription.getId(), e.getMessage()); + } + } + }); + } + + public void cancelSubscriptionTemp(String subscriptionId) { + Stripe.apiKey = privateKey; + try { + log.info("申请取消连续订阅 {}", subscriptionId); + Subscription subscription = Subscription.retrieve(subscriptionId); + Subscription cancel = subscription.cancel(); + cancel.getStatus(); + } catch (StripeException e) { + log.error(e.getMessage()); +// throw new RuntimeException(e); + } + } + + public String refund(String amount, String orderNo, String reason) { + Refund refund; + RefundInfo refundByOrderNo = refundInfoService.createRefundByOrderNo(orderNo, reason); + + try { + Stripe.apiKey = privateKey; + // todo transactionId不再是sessionId而是invoiceId,所以这里需要更新 + // 根据orderId找到对应的sessionId + String sessionId = paymentInfoService.getPaymentInfoByOrderNo(orderNo, "DESC").get(0).getTransactionId(); + + if (StringUtils.isNotEmpty(sessionId)) { //根据会话编号退款 + Session session = Session.retrieve(sessionId); + RefundCreateParams params; + if (amount != null && !amount.equals("0")) { //指定退款金额 + BigDecimal actualAmount = new BigDecimal(amount).multiply(BigDecimal.valueOf(100)); //api默认单位分 + params = RefundCreateParams.builder() + .setPaymentIntent(session.getPaymentIntent()) + .setAmount(actualAmount.longValue()) + .build(); + } else { //全额退款 + params = RefundCreateParams.builder() + .setPaymentIntent(session.getPaymentIntent()) + .build(); + } + refund = Refund.create(params); + log.info("根据会话编号退款成功"); + + } else { + log.error("当前订单不存在"); + return "退款异常"; + } + } catch (Exception e) { + //e.getMessage.contain("charge_already_refunded") 已退款 + //e.getMessage.contain("resource_missing") 退款编号错误 + //e.getMessage.contain("amount on charge ($n)") 金额应小于n + log.error("退款异常:", e); + return "退款异常"; + } + + if ("succeeded".equals(refund.getStatus())) { + //进行数据库操作,修改状态为已退款(配合回调和退款查询确定退款成功) + //更新订单状态 + orderInfoService.updateStatusByOrderNo(orderNo, OrderStatusEnum.REFUND_SUCCESS); + + refundInfoService.updateRefundForPayPal( + refundByOrderNo.getId(), + refund.getId(), + new Gson().toJson(refund), + AliPayTradeStateEnum.REFUND_SUCCESS.getType()); //退款成功 + + // 更新积分状态 + OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(orderNo); + creditsService.creditsRefund(orderByOrderNo.getAccountId(), (int) (orderByOrderNo.getTotalFee() / Float.parseFloat(CreditsEventsEnum.PRICE.getValue())), orderNo); + } else { + //更新订单状态 + orderInfoService.updateStatusByOrderNo(orderNo, OrderStatusEnum.REFUND_ABNORMAL); + + //更新退款单 + refundInfoService.updateRefundForPayPal( + refundByOrderNo.getId(), + refund.getId(), + new Gson().toJson(refund), + AliPayTradeStateEnum.REFUND_ERROR.getType()); //退款失败 + } + log.info("记录退款订单"); + return "退款成功"; + } + + public void checkOrderStatus(String orderNo) { + Stripe.apiKey = privateKey; + // 1、通过orderNo 查询sessionId + // todo transactionId不再是sessionId而是invoiceId,所以这里需要更新 + PaymentInfo paymentInfo = paymentInfoService.getPaymentInfoByOrderNo(orderNo, "DESC").get(0); + try { + Session session = Session.retrieve(paymentInfo.getTransactionId()); + if (Objects.isNull(session)) { + log.warn("核实订单未创建 ===> {}", orderNo); + return; + } else if (session.getStatus().equals("open") || session.getStatus().equals("expired")) { + // 订单未支付 || 订单过期 ---> 均设置为超时未支付 + log.info("订单超时未支付 ===> {}", orderNo); + //更新本地订单状态 + orderInfoService.updateStatusByOrderNo(orderNo, OrderStatusEnum.TIMEOUT_CLOSED); + paymentInfoService.updatePaymentStatusById(paymentInfo.getId(), + session.getStatus(), + new Gson().toJson(session)); + } else if (session.getStatus().equals("complete")) { + // 订单已完成 + processOrder(session); + } + } catch (StripeException e) { + log.error("根据sessionId获取Stripe Session失败"); + throw new RuntimeException(e); + } + + } + + public List getSubscription(String username, String userEmail) { + Stripe.apiKey = privateKey; + String customerId = null; + try { + customerId = getCustomer(username, userEmail); + SubscriptionCollection list = Subscription.list(SubscriptionListParams.builder() + .setCustomer(customerId).setLimit(20L) + .build()); + return list.getData(); + } catch (StripeException e) { + throw new RuntimeException(e); + } + } + + // 获取所有订阅 + public List getSubscriptionIds(String username, String userEmail) { + Stripe.apiKey = privateKey; + String customerId = null; + try { + customerId = getCustomer(username, userEmail); + SubscriptionCollection list = Subscription.list(SubscriptionListParams.builder() + .setCustomer(customerId).build()); + List data = list.getData(); + ArrayList subscriptionIds = new ArrayList<>(); + data.forEach(subscription -> { + subscriptionIds.add(subscription.getId()); + }); + return subscriptionIds; + } catch (StripeException e) { + throw new RuntimeException(e); + } + + } + + private String getCustomer(String username, String userEmail) throws StripeException { + CustomerCollection list = Customer.list(CustomerListParams.builder().setEmail(userEmail).build()); + List data = list.getData(); + if (!data.isEmpty()) { + return data.get(0).getId(); + } + return createCustomer(username, userEmail); + } + + private String createCustomer(String name, String userEmail) throws StripeException { + Stripe.apiKey = privateKey; + + // Customer允许重复使用 + CustomerCreateParams params = + CustomerCreateParams.builder() + .setName(name) + .setEmail(userEmail) + .build(); + Customer customer = Customer.create(params); + + return customer.getId(); + } + + /** + * 使用连续订阅的订单,回调中没有paymentIntentId,所以通过invoiceId间接获取 + * @param invoiceId 发票Id + */ + public Map getPaymentMethodByInvoiceId(String invoiceId) { + try { + Stripe.apiKey = privateKey; + Invoice invoice = Invoice.retrieve(invoiceId); + if (!StringUtil.isNullOrEmpty(invoice.getPaymentIntent())){ + PaymentIntent paymentIntent = PaymentIntent.retrieve(invoice.getPaymentIntent()); + if (!StringUtil.isNullOrEmpty(paymentIntent.getPaymentMethod())){ + PaymentMethod paymentMethod = PaymentMethod.retrieve(paymentIntent.getPaymentMethod()); + return getPaymentMethod(paymentMethod.getId()); + } + } + HashMap resp = new HashMap<>(); + resp.put("paymentMethod", "N/A"); + resp.put("last4", "N/A"); + return resp; + } catch (StripeException e) { + throw new RuntimeException(e); + } + } + + public Map getPaymentMethod(String paymentMethodId){ + Stripe.apiKey = privateKey; + String paymentMethod = null; + String last4 = null; + + try { + PaymentMethod retrieve = PaymentMethod.retrieve(paymentMethodId); + switch (retrieve.getType()){ + case "alipay": + paymentMethod = "Alipay"; + last4 = "N/A"; + break; + case "bancontact": + paymentMethod = "BanContact"; + break; + case "card": + PaymentMethod.Card card = retrieve.getCard(); + String brand = card.getBrand(); + brand = brand.substring(0, 1).toUpperCase() + brand.substring(1); + paymentMethod = brand + " " + card.getFunding() + "card"; + last4 = card.getLast4(); + break; + case "eps": + PaymentMethod.Eps eps = retrieve.getEps(); + paymentMethod = eps.getBank(); + last4 = "N/A"; + break; + case "giropay": + paymentMethod = "GiroPay"; + last4 = "N/A"; + break; + case "ideal": + PaymentMethod.Ideal ideal = retrieve.getIdeal(); + paymentMethod = ideal.getBank(); + last4 = "N/A"; + break; + case "link": + paymentMethod = "Link"; + last4 = "N/A"; + break; + default: + paymentMethod = "N/A"; + last4 = "N/A"; + } + HashMap resp = new HashMap<>(); + resp.put("paymentMethod", paymentMethod); + resp.put("last4", last4); + return resp; + } catch (StripeException e) { + throw new RuntimeException(e); + } +// return null; + } + + public boolean sendEmail(String subscriptionId, String type, String orderNo) { + SubscriptionInfo subscriptionInfo; + QueryWrapper qwSI = new QueryWrapper<>(); + if (!StringUtil.isNullOrEmpty(subscriptionId)) { + qwSI.eq("subscription_id", subscriptionId); + List subscriptionInfoList = subscriptionInfoMapper.selectList(qwSI); + + if (subscriptionInfoList.isEmpty()){ + log.info("不发送邮件,原因:【subscriptionInfoList 为空】"); + return false; + }else { + List activeSubscriptions = subscriptionInfoList.stream() + .filter(subscription -> "active".equals(subscription.getStatus())) + .collect(Collectors.toList()); + if (!StringUtil.isNullOrEmpty(type) && type.equals("cancel")){ + subscriptionInfo = subscriptionInfoList.get(0); + }else if (activeSubscriptions.isEmpty()){ + log.info("不发送邮件,原因:【当前邮件类型:{}, 但是状态为active的subscriptionInfo为空】", type); + return false; + }else { + subscriptionInfo = activeSubscriptions.get(0); + } + } + }else if (!StringUtil.isNullOrEmpty(orderNo)) { + qwSI.eq("order_no", orderNo); + subscriptionInfo = subscriptionInfoMapper.selectOne(qwSI); + if (Objects.isNull(subscriptionInfo)){ + log.info("不发送邮件,原因:【根据order_no:{},查询到的subscriptionInfo为空】", orderNo); + return false; + } + }else { + log.info("不发送邮件,原因:【入参中的subscriptionId,orderNo均为空】"); + return false; + } + + QueryWrapper qwPI = new QueryWrapper<>(); + qwPI.eq("order_no", subscriptionInfo.getOrderNo()).orderByDesc("id"); + List paymentInfos = paymentInfoMapper.selectList(qwPI); + if (paymentInfos.isEmpty()) { + log.info("不发送邮件,原因:【根据order_no:{},查询到的paymentInfos为空】", orderNo); + return false; + } + PaymentInfo paymentInfo = paymentInfos.get(0); + if (StringUtil.isNullOrEmpty(type)){ + // 如果没有传入type,则使用paymentInfo中记录的类型 + // (其实这里也可以通过invoiceId查询stripe,但是记录在自己的db中可以不用每次都查,且方便查看) + type = StringUtil.isNullOrEmpty(paymentInfo.getType()) ? "new" : paymentInfo.getType(); + } + if (!type.equals("reminder") && !type.equals("cancel") && paymentInfo.getNotified() == 1){ + // 已经邮件通知过,直接返回 + log.info("不发送邮件,原因:【type为:{},order_no为:{},已经进行邮件通知】", type, orderNo); + return true; + } + + com.ai.da.mapper.primary.entity.Account account = accountMapper.selectById(subscriptionInfo.getAccountId()); + String userName = account.getUserName(); + String language = account.getLanguage(); + OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(subscriptionInfo.getOrderNo()); + + SubscriptionEmailParamsDTO emailParamsDTO = new SubscriptionEmailParamsDTO(); + emailParamsDTO.setUsername(userName); + emailParamsDTO.setOrderId(paymentInfo.getId().toString()); + emailParamsDTO.setOrderRef("\"" + orderListLink + paymentInfo.getId().toString() + "\""); + emailParamsDTO.setCreateDate(String.valueOf(paymentInfo.getCreateTime()).replace("T", " ")); + emailParamsDTO.setQuantity(String.valueOf(1)); + emailParamsDTO.setTotalFee(paymentInfo.getPayerTotal().toString()); + emailParamsDTO.setLastOrderDate(DateUtil.changeTimeStampFormat(subscriptionInfo.getCurrentPeriodStart(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy)); + emailParamsDTO.setEndOfPrepaidTerm(DateUtil.changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy)); + setSubscriptionParams(paymentInfo, subscriptionInfo, orderByOrderNo, emailParamsDTO, language); + + boolean b = SendEmailUtil.subscriptionEmailReminder(type, emailParamsDTO, language, account.getUserEmail()); +// boolean b = emailService.subscriptionEmailReminder(type, emailParamsDTO, language, account.getUserEmail()); + if (!b) return false; + + // 邮件通知成功后,更新标志 + if (!type.equals("reminder") && !type.equals("cancel")){ + PaymentInfo payment = new PaymentInfo(); + payment.setId(paymentInfo.getId()); + payment.setNotified(1); + payment.setUpdateTime(LocalDateTime.now()); + paymentInfoMapper.updateById(payment); + } + return true; + } + + public boolean sendEmail(String orderNo){ + SubscriptionEmailParamsDTO emailParamsDTO = new SubscriptionEmailParamsDTO(); + OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo); + + com.ai.da.mapper.primary.entity.Account account = accountMapper.selectById(orderInfo.getAccountId()); + String userName = account.getUserName(); + String language = account.getLanguage(); + QueryWrapper qwPI = new QueryWrapper<>(); + qwPI.eq("order_no", orderNo); + List paymentInfos = paymentInfoMapper.selectList(qwPI); + if (paymentInfos.isEmpty()) { + log.info("不发送邮件,原因:【根据order_no:{},查询到的paymentInfos为空】", orderNo); + return false; + } + PaymentInfo paymentInfo = paymentInfos.get(0); + emailParamsDTO.setUsername(userName); + emailParamsDTO.setOrderId(paymentInfo.getId().toString()); + emailParamsDTO.setCreateDate(String.valueOf(paymentInfo.getCreateTime()).replace("T", " ")); + emailParamsDTO.setQuantity(String.valueOf(1)); + emailParamsDTO.setTotalFee(paymentInfo.getPayerTotal().toString()); + emailParamsDTO.setFailMessage(orderInfo.getNote()); + emailParamsDTO.setPaymentMethod(paymentInfo.getPaymentMethod()); + emailParamsDTO.setLast4(paymentInfo.getLast4()); + + boolean b = SendEmailUtil.subscriptionEmailReminder("fail_new", emailParamsDTO, language, account.getUserEmail()); +// boolean b = emailService.subscriptionEmailReminder("fail_new", emailParamsDTO, language, account.getUserEmail()); + if (!b) return false; + + // 邮件通知成功后,更新标志 + PaymentInfo payment = new PaymentInfo(); + payment.setId(paymentInfo.getId()); + payment.setNotified(1); + payment.setUpdateTime(LocalDateTime.now()); + paymentInfoMapper.updateById(payment); + return true; + } + + public boolean sendRenewalFailEmail(String invoiceId, String subscriptionId, String orderNo){ + // 1、确认当前订单最后一笔支付为fail + // 更新支付信息 + PaymentInfo paymentInfo; + QueryWrapper queryWrapper = new QueryWrapper<>(); + if (StringUtil.isNullOrEmpty(invoiceId)){ + queryWrapper.eq("order_no", orderNo).orderByDesc("id"); + List paymentInfos = paymentInfoService.getBaseMapper().selectList(queryWrapper); + if (paymentInfos.isEmpty() || !paymentInfos.get(0).getTradeState().equals("failed")){ + return false; + }else { + paymentInfo = paymentInfos.get(0); + } + }else { + queryWrapper.eq("transaction_id", invoiceId); + paymentInfo = paymentInfoService.getBaseMapper().selectOne(queryWrapper); + if (Objects.isNull(paymentInfo) + || !paymentInfo.getTradeState().equals("failed") + || paymentInfo.getNotified().equals(1)){ + return false; + } + } + + // 2、确认当前订阅的状态为past_due + SubscriptionInfo subscriptionInfo; + QueryWrapper qwSI = new QueryWrapper<>(); + if (StringUtil.isNullOrEmpty(subscriptionId)){ + qwSI.eq("order_no", orderNo); + }else { + qwSI.eq("subscription_id", subscriptionId); + } + subscriptionInfo = subscriptionInfoMapper.selectOne(qwSI); + if (Objects.isNull(subscriptionInfo) || !subscriptionInfo.getStatus().equals("past_due")){ + return false; + } + + // 3、组参数 + com.ai.da.mapper.primary.entity.Account account = accountMapper.selectById(subscriptionInfo.getAccountId()); + String userName = account.getUserName(); + String language = account.getLanguage(); + OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(subscriptionInfo.getOrderNo()); + SubscriptionEmailParamsDTO emailParamsDTO = new SubscriptionEmailParamsDTO(); + emailParamsDTO.setUsername(userName); + emailParamsDTO.setOrderId(paymentInfo.getId().toString()); + emailParamsDTO.setCreateDate(String.valueOf(paymentInfo.getCreateTime()).replace("T", " ")); + emailParamsDTO.setQuantity(String.valueOf(1)); + emailParamsDTO.setTotalFee(paymentInfo.getPayerTotal().toString()); + setSubscriptionParams(paymentInfo, subscriptionInfo, orderByOrderNo, emailParamsDTO, language); + + // 4、发邮件 + boolean b = SendEmailUtil.subscriptionEmailReminder("fail_renewal", emailParamsDTO, language, account.getUserEmail()); +// boolean b = emailService.subscriptionEmailReminder("fail_renewal", emailParamsDTO, language, account.getUserEmail()); + if (!b) return false; + + PaymentInfo payment = new PaymentInfo(); + payment.setId(paymentInfo.getId()); + payment.setNotified(1); + payment.setUpdateTime(LocalDateTime.now()); + paymentInfoMapper.updateById(payment); + return true; + } + + private void setSubscriptionParams(PaymentInfo paymentInfo, SubscriptionInfo subscriptionInfo, OrderInfo orderByOrderNo, + SubscriptionEmailParamsDTO emailParamsDTO, String language) { + emailParamsDTO.setPaymentMethod(paymentInfo.getPaymentMethod()); + emailParamsDTO.setLast4(paymentInfo.getLast4()); + emailParamsDTO.setSubscriptionId(subscriptionInfo.getId().toString()); + emailParamsDTO.setFailMessage(orderByOrderNo.getNote()); + emailParamsDTO.setSubscriptionType(subscriptionInfo.getType()); + emailParamsDTO.setStartDate(DateUtil.changeTimeStampFormat(orderByOrderNo.getCreateTime())); + if (subscriptionInfo.getType().equals("month")){ + emailParamsDTO.setRenewalFee(String.valueOf(500)); + } else if (subscriptionInfo.getType().equals("year")){ + emailParamsDTO.setRenewalFee(String.valueOf(5000)); + } else { + emailParamsDTO.setRenewalFee(emailParamsDTO.getTotalFee()); + } + if (subscriptionInfo.getStatus().equals("active")){ + if (language.equals("ENGLISH")){ + emailParamsDTO.setEndDate("When cancelled"); + }else { + emailParamsDTO.setEndDate("手动取消订阅时"); + } + }else { + emailParamsDTO.setEndDate(DateUtil.changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy_EEEE)); + } + if (StringUtil.isNullOrEmpty(subscriptionInfo.getNextPayDate())){ + emailParamsDTO.setNextPayDate("N/A"); + } else { + emailParamsDTO.setNextPayDate(DateUtil.changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy)); + } + emailParamsDTO.setRenewalTime(DateUtil.changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy)); + } + + public void subscriptionReminder(){ + // 提前7天的 00:00:00 和 23:59:59 + LocalDateTime startOfDay = LocalDateTime.now().plusDays(7).toLocalDate().atStartOfDay(); + LocalDateTime endOfDay = LocalDateTime.now().plusDays(7).toLocalDate().atTime(23, 59, 59); + + // 转为时间戳 + long startTimestamp = startOfDay.toEpochSecond(ZoneOffset.UTC); + long endTimestamp = endOfDay.toEpochSecond(ZoneOffset.UTC); + + QueryWrapper qw = new QueryWrapper<>(); + qw.ge("current_period_end", startTimestamp); + qw.lt("current_period_end", endTimestamp); + qw.eq("status", "active"); + + List subscriptionInfos = subscriptionInfoMapper.selectList(qw); + for (SubscriptionInfo subscriptionInfo : subscriptionInfos) { + boolean b = sendEmail(subscriptionInfo.getSubscriptionId(), "reminder", null); + if (b) log.info("提前7天向用户 {} 发送续订通知邮件", subscriptionInfo.getAccountId()); + } + } + + public void checkSubscriptionExpiration(){ + long epochSecond = Instant.now().getEpochSecond(); + QueryWrapper qw = new QueryWrapper<>(); + qw.lt("current_period_end", epochSecond); + qw.eq("status", "active"); + List subscriptionInfos = subscriptionInfoMapper.selectList(qw); + + for (SubscriptionInfo subscriptionInfo : subscriptionInfos) { + subscriptionInfo.setStatus("expired"); + subscriptionInfo.setUpdateTime(LocalDateTime.now()); + subscriptionInfoMapper.updateById(subscriptionInfo); + log.info("用户 {} 的订阅 {} 已过期", subscriptionInfo.getAccountId(), subscriptionInfo.getOrderNo()); + } + } + + // 新建一个订阅 使用不会成功的付款方式(仅供测试使用) + public String createSubscriptionTemp(String name, String email){ + Stripe.apiKey = privateKey; + try { + OrderInfo orderInfo = orderInfoService.createOrderByProductId(1, PayTypeEnum.STRIPE.getType(), ProductEnum.DailySubscription, null, (byte)0); + +// String customerId = getCustomer(name, email); + String paymentMethodCode = "pm_card_mastercard"; + PaymentMethod paymentMethod = PaymentMethod.retrieve(paymentMethodCode); + + String customerId = getCustomer(name, email); + log.info("customerId: {}", customerId); + + PaymentMethodAttachParams attachParams = PaymentMethodAttachParams.builder() + .setCustomer(customerId) + .build(); + paymentMethod.attach(attachParams); + + // 设置默认付款方式 + Customer updatedCustomer = Customer.retrieve(customerId); + CustomerUpdateParams params = CustomerUpdateParams.builder() + .setInvoiceSettings( + CustomerUpdateParams.InvoiceSettings.builder() + .setDefaultPaymentMethod(paymentMethod.getId()) + .build()) + .build(); + updatedCustomer.update(params); + + // 3. 创建订阅 + SubscriptionCreateParams subscriptionParams = SubscriptionCreateParams.builder() + .setCustomer(customerId) + .addItem( + SubscriptionCreateParams.Item.builder() + .setPrice("price_1QFXkf02n1TEydyNtA4TQ3Yz") // 替换为实际的价格 ID + .build()) + .setDescription("AiDA - " + orderInfo.getOrderNo()) + .build(); + Subscription subscription = Subscription.create(subscriptionParams); + + // 打印订阅 ID + System.out.println("Subscription created: " + subscription.getId()); + return subscription.getId(); + } catch (StripeException e) { + throw new RuntimeException(e); + } + } + + public String changeCustomerPayment(String name, String email){ + Stripe.apiKey = privateKey; + String paymentMethodCode = "pm_card_chargeCustomerFail"; + try { + PaymentMethod paymentMethod = PaymentMethod.retrieve(paymentMethodCode); + + String customerId = getCustomer(name, email); + log.info("customerId: {}", customerId); + + // 附加支付方式到客户 + PaymentMethodAttachParams attachParams = PaymentMethodAttachParams.builder() + .setCustomer(customerId) + .build(); + paymentMethod.attach(attachParams); + // 更新客户的默认支付方式 + CustomerUpdateParams params = CustomerUpdateParams.builder() + .setInvoiceSettings( + CustomerUpdateParams.InvoiceSettings.builder() + .setDefaultPaymentMethod(paymentMethod.getId()) + .build() + ) + .build(); + + // 更新客户信息 + Customer customer = Customer.retrieve(customerId); + customer.update(params); + + return paymentMethod.getId(); + + } catch (StripeException e) { + throw new RuntimeException(e); + } + } + + public List> getCustomerPaymentMethod(String name, String email){ + Stripe.apiKey = privateKey; + try { + String customerId = getCustomer(name, email); + Customer customer = Customer.retrieve(customerId); + PaymentMethodCollection paymentMethodCollection = customer.listPaymentMethods(); + List data = paymentMethodCollection.getData(); + ArrayList> resp = new ArrayList<>(); + data.forEach(paymentMethod -> { + Map map = new HashMap<>(); + if (paymentMethod.getType().equals("card")){ + map.put(paymentMethod.getId(),paymentMethod.getCard().getLast4()); + }else { + map.put(paymentMethod.getId(),null); + } + resp.add(map); + }); + + return resp; + // 方向: 向用户添加了多种付款方式,更改默认的付款方式后,默认付款方式付款失败后是否自动使用其他付款方式付款? + // 如果是的,则需要删除能成功的付款方式,保留唯一失败的付款方式进行续订付款失败测试 + } catch (StripeException e) { + throw new RuntimeException(e); + } + + } + + public String detachCustomerAllPaymentMethod(String name, String email){ + Stripe.apiKey = privateKey; + // 方向: 向用户添加了多种付款方式,更改默认的付款方式后,默认付款方式付款失败后是否自动使用其他付款方式付款? + // 如果是的,则需要删除能成功的付款方式,保留唯一失败的付款方式进行续订付款失败测试 + try { + String customerId = getCustomer(name, email); + Customer customer = Customer.retrieve(customerId); + PaymentMethodCollection paymentMethodCollection = customer.listPaymentMethods(); + List data = paymentMethodCollection.getData(); + data.forEach(paymentMethod -> { + try { + PaymentMethod retrieve = PaymentMethod.retrieve(paymentMethod.getId()); + PaymentMethodDetachParams params = PaymentMethodDetachParams.builder().build(); + retrieve.detach(params); + } catch (StripeException e) { + throw new RuntimeException(e); + } + }); + } catch (StripeException e) { + throw new RuntimeException(e); + } + return null; + } + + public void updateCancelReason(String subscriptionId, String reason){ + QueryWrapper qw = new QueryWrapper<>(); + qw.eq("subscription_id", subscriptionId); + SubscriptionInfo subscriptionInfo = subscriptionInfoMapper.selectOne(qw); + + if (!Objects.isNull(subscriptionInfo)) { + subscriptionInfo.setCancelReason(reason); + subscriptionInfoMapper.updateById(subscriptionInfo); + } + } + +// public String getIp(HttpServletRequest request) { +// String ipAddress = RequestInfoUtil.getIpAddress(request); +// if (!StringUtil.isNullOrEmpty(ipAddress)) { +// return getIPLocation(ipAddress); +// } +// +// return request.getRemoteAddr(); +// } + + + public String getStackTrace(Exception e, int maxLines) { + StringBuilder sb = new StringBuilder(); + StackTraceElement[] stackTraceElements = e.getStackTrace(); + int lines = Math.min(maxLines, stackTraceElements.length); + + for (int i = 0; i < lines; i++) { + sb.append(stackTraceElements[i].toString()).append("\n"); + } + // 如果堆栈信息超过 maxLines 行,添加提示 + if (stackTraceElements.length > maxLines) { + sb.append("... (More stack trace lines truncated)\n"); + } + return sb.toString(); + } + + + /* + * 优惠券实施: + * 1、由管理员创建优惠券,手动设置该优惠券对应的折扣 + * 2、用户在订阅时,手动输入优惠码 + * 3、使用stripe的字段 redeem_by 管理优惠券的有效期 + * 4、promotion code与 coupons需要绑定 + * 5、用户只能使用一次优惠券 + * + * 问题:对于一次付款和订阅,优惠码是否可以混用 + */ + public String createCoupon(CreateCouponDTO createCouponDTO){ + Stripe.apiKey = privateKey; + CouponCreateParams.Builder couponParams = CouponCreateParams.builder() + // 任何客户只能用一次这个优惠券 + .setDuration(CouponCreateParams.Duration.ONCE) + // percent_off 与 amount_off 不能同时设置 + .setPercentOff(BigDecimal.valueOf(createCouponDTO.getPercentOff())); + if (Objects.nonNull(createCouponDTO.getTimestamp())){ + couponParams.setRedeemBy(createCouponDTO.getTimestamp()); + } + try { + // 1、创建优惠券 + Coupon coupon = Coupon.create(couponParams.build()); + // 2、创建一个推广码 + PromotionCode promotionCode = createPromotionCode(coupon.getId(), createCouponDTO.getMaxRedemptions()); + // 3、落库 + ProductCoupons productCoupons = new ProductCoupons(coupon.getId(), createCouponDTO.getTimestamp(), promotionCode.getId(), + promotionCode.getCode(), createCouponDTO.getMaxRedemptions(), createCouponDTO.getPercentOff(), + createCouponDTO.getCommissionRate(), createCouponDTO.getCooperator(), createCouponDTO.getRemark()); + productCoupons.setCreateTime(LocalDateTime.now()); + productCouponsMapper.insert(productCoupons); + + return promotionCode.getCode(); + } catch (StripeException e) { + throw new RuntimeException(e); + } + } + + public PromotionCode createPromotionCode(String couponId, Long maxRedemption){ + Stripe.apiKey = privateKey; + PromotionCodeCreateParams.Builder promotionCodeParams = PromotionCodeCreateParams.builder() + .setCoupon(couponId) + .setRestrictions(PromotionCodeCreateParams.Restrictions.builder().build()); + if (Objects.nonNull(maxRedemption)){ + promotionCodeParams.setMaxRedemptions(maxRedemption); + } + + try { + return PromotionCode.create(promotionCodeParams.build()); + } catch (StripeException e) { + throw new RuntimeException(e); + } + } + + public ProductCoupons updateCouponsInfo(Long id, Long paidCommission, String cooperator, String remark){ + ProductCoupons productCoupons = productCouponsMapper.selectById(id); + if (Objects.isNull(productCoupons)){ + throw new BusinessException("Unknown Promotion Code"); + } + boolean flag = false; + if (!StringUtil.isNullOrEmpty(cooperator)){ + productCoupons.setCooperator(cooperator); + flag = true; + } + if (!StringUtil.isNullOrEmpty(remark)){ + productCoupons.setRemark(remark); + flag = true; + } + if (Objects.nonNull(paidCommission)){ + productCoupons.setPaidCommission(paidCommission); + flag = true; + } + if (flag){ + productCoupons.setUpdateTime(LocalDateTime.now()); + productCouponsMapper.updateById(productCoupons); + } + return productCoupons; + } + + public ProductCoupons checkProductCoupon(String promotionCode){ + Stripe.apiKey = privateKey; + Long accountId = UserContext.getUserHolder().getId(); + // 1、从数据库查找promotionCode对应的promotionCodeId + ProductCoupons productCoupons = productCouponsMapper.selectOne(new QueryWrapper().eq("promotion_code", promotionCode)); + if (Objects.nonNull(productCoupons)){ + // 2、查绑定的Coupons是否存在以及是否过期 + long epochSecondNow = Instant.now().getEpochSecond(); + Long redeemBy = productCoupons.getRedeemBy(); + if (redeemBy < epochSecondNow){ + throw new BusinessException("this.promotion.code.has.expired"); + } else { + // 判断该用户是否有成功使用过这个推广码 + List paymentInfoByPromCode = paymentInfoService.getPaymentInfoByPromCode(accountId, promotionCode); + if (!paymentInfoByPromCode.isEmpty()) { + // 已使用过推广码,状态无效 + if (paymentInfoByPromCode.size() > 1) { + log.error("用户[{}]多次成功使用优惠码[{}]", accountId, promotionCode); + } + log.info("用户[{}]已成功使用过优惠码[{}]", accountId, promotionCode); + throw new BusinessException("one.time.limit.per.customer"); + } + } + }else { + throw new BusinessException("this.promotion.code.is.invalid"); + } + return productCoupons; + } + + public CheckCouponsVO checkProductCoupon(String promotionCode, Long price){ + Stripe.apiKey = privateKey; + Long accountId = UserContext.getUserHolder().getId(); + CheckCouponsVO checkCouponsVO = new CheckCouponsVO(); + // 1、从数据库查找promotionCode对应的promotionCodeId + ProductCoupons productCoupons = productCouponsMapper.selectOne(new QueryWrapper().eq("promotion_code", promotionCode)); + if (Objects.nonNull(productCoupons)){ + // 2、查绑定的Coupons是否存在以及是否过期 + long epochSecondNow = Instant.now().getEpochSecond(); + Long redeemBy = productCoupons.getRedeemBy(); + if (redeemBy < epochSecondNow){ + String msg = BusinessException.getMessageFromResource("this.promotion.code.has.expired"); + checkCouponsVO.setMessage(msg); + checkCouponsVO.setStatus("expired"); + }else { + // 判断该用户是否有成功使用过这个推广码 + List paymentInfoByPromCode = paymentInfoService.getPaymentInfoByPromCode(accountId, promotionCode); + if (paymentInfoByPromCode.isEmpty()) { + // 未使用过推广码,状态有效 + checkCouponsVO.setStatus("valid"); + checkCouponsVO.setDiscountedPrice(price * (1 - productCoupons.getPercentOff() / 100)); + } else { + // 已使用过推广码,状态无效 + if (paymentInfoByPromCode.size() > 1) { + log.error("用户[{}]多次成功使用优惠码[{}]", accountId, promotionCode); + } + log.info("用户[{}]已成功使用过优惠码[{}]", accountId, promotionCode); + String msg = BusinessException.getMessageFromResource("one.time.limit.per.customer"); + checkCouponsVO.setMessage(msg); + checkCouponsVO.setStatus("invalid"); + } + } + }else { + String msg = BusinessException.getMessageFromResource("this.promotion.code.is.invalid"); + checkCouponsVO.setMessage(msg); + checkCouponsVO.setStatus("invalid"); + } + return checkCouponsVO; + } + + public ProductCoupons getProductCoupon(String promotionCode, String promotionCodeId){ + Stripe.apiKey = privateKey; + QueryWrapper qw = new QueryWrapper<>(); + // 1、从数据库查找promotionCode对应的promotionCodeId + if (!StringUtil.isNullOrEmpty(promotionCode)){ + qw.eq("promotion_code", promotionCode); + } + if (!StringUtil.isNullOrEmpty(promotionCodeId)){ + qw.eq("promotion_code_id", promotionCodeId); + } + return productCouponsMapper.selectOne(qw); + } + + public String retrieveCoupon(String couponId){ + Stripe.apiKey = privateKey; + try { + Coupon coupon = Coupon.retrieve(couponId); + log.info("retrieve的coupon: {}", coupon); + return JSON.toJSONString(coupon); + } catch (StripeException e) { + throw new RuntimeException(e); + } + } + + public String retrievePromotionCode(String promotionCode){ + Stripe.apiKey = privateKey; + try { + ProductCoupons productCoupon = getProductCoupon(promotionCode, null); + PromotionCode retrieve = PromotionCode.retrieve(productCoupon.getPromotionCodeId()); + log.info("retrieve的promotionCode: {}", retrieve); + return JSON.toJSONString(retrieve); + } catch (StripeException e) { + throw new RuntimeException(e); + } + } + + public IPage getAllCoupons(QueryCouponsPageDTO queryCouponsPageDTO){ + // 分页 + 按条件查询 + QueryWrapper queryWrapper = new QueryWrapper<>(); + if (!StringUtil.isNullOrEmpty(queryCouponsPageDTO.getOrderById()) && queryCouponsPageDTO.getOrderById().equals("DESC")){ + queryWrapper.orderByDesc("id"); + } + if (!StringUtil.isNullOrEmpty(queryCouponsPageDTO.getPromotionCode())){ + queryWrapper.like("promotion_code", queryCouponsPageDTO.getPromotionCode()); + } + if (Objects.nonNull(queryCouponsPageDTO.getIsExpired())){ + long epochSecond = Instant.now().getEpochSecond(); + if (queryCouponsPageDTO.getIsExpired()){ + queryWrapper.lt("redeem_by", epochSecond); + }else { + queryWrapper.gt("redeem_by", epochSecond); + } + } + if (!StringUtil.isNullOrEmpty(queryCouponsPageDTO.getCooperator())){ + queryWrapper.like("cooperator", queryCouponsPageDTO.getCooperator()); + } + if (!StringUtil.isNullOrEmpty(queryCouponsPageDTO.getStartTime())){ + queryWrapper.gt("create_time", queryCouponsPageDTO.getStartTime()); + } + if (!StringUtil.isNullOrEmpty(queryCouponsPageDTO.getEndTime())){ + queryWrapper.lt("create_time", queryCouponsPageDTO.getEndTime()); + } + + return productCouponsMapper.selectPage(new Page<>(queryCouponsPageDTO.getPage(), queryCouponsPageDTO.getSize()), queryWrapper); + } + + @Transactional + public void deleteCoupon(Long id){ + Stripe.apiKey = privateKey; + ProductCoupons productCoupons = productCouponsMapper.selectById(id); + if (Objects.isNull(productCoupons)){ + throw new BusinessException("unknown promotion code"); + } + try { + Coupon coupon = Coupon.retrieve(productCoupons.getCouponId()); + coupon.delete(); + log.info("coupon {} 删除成功", productCoupons.getCouponId()); + productCouponsMapper.deleteById(id); + } catch (StripeException e) { + log.error("未知coupons,无法通过couponId: {} 获得Coupons", productCoupons.getCouponId()); + } + } + +}