Browse Source

saas 1.3:邮件发送业务编写

wangPH 1 year ago
parent
commit
4822f02b9e

+ 10 - 1
background-user/pom.xml

@@ -13,7 +13,9 @@
     <version>0.0.1</version>
     <name>background-user</name>
     <description>用户后台</description>
-
+    <properties>
+        <oss.version>3.10.2</oss.version>
+    </properties>
 
     <dependencies>
         <dependency>
@@ -52,6 +54,13 @@
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-mail</artifactId>
         </dependency>
+
+        <!--oss-->
+        <dependency>
+            <groupId>com.aliyun.oss</groupId>
+            <artifactId>aliyun-sdk-oss</artifactId>
+            <version>${oss.version}</version>
+        </dependency>
     </dependencies>
 
     <build>

+ 19 - 0
background-user/src/main/java/cn/kdan/compdf/config/OssConfig.java

@@ -0,0 +1,19 @@
+package cn.kdan.compdf.config;
+
+import cn.kdan.compdf.properties.OssProperties;
+import com.aliyun.oss.OSS;
+import com.aliyun.oss.OSSClientBuilder;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @author ZhouQiang 2022/7/13
+ */
+@Configuration
+public class OssConfig {
+
+    @Bean
+    public OSS ossClient(OssProperties ossProperties) {
+        return new OSSClientBuilder().build(ossProperties.getEndpoint(), ossProperties.getAccessKeyId(), ossProperties.getAccessKeySecret());
+    }
+}

+ 20 - 0
background-user/src/main/java/cn/kdan/compdf/dto/BillSendEmailDTO.java

@@ -0,0 +1,20 @@
+package cn.kdan.compdf.dto;
+
+import lombok.Data;
+import lombok.ToString;
+
+import javax.validation.constraints.NotEmpty;
+
+/**
+ * @author ComPDFKit-WPH 2023/5/18
+ */
+@Data
+@ToString
+public class BillSendEmailDTO {
+
+    @NotEmpty(message = "订单id不能为空")
+    private Integer orderId;
+    @NotEmpty(message = "email不能为空")
+    private String email;
+
+}

+ 293 - 0
background-user/src/main/java/cn/kdan/compdf/oss/OssFileClient.java

@@ -0,0 +1,293 @@
+package cn.kdan.compdf.oss;
+
+import cn.kdan.compdf.exception.BusinessException;
+import cn.kdan.compdf.properties.OssProperties;
+import com.alibaba.fastjson.JSONObject;
+import com.aliyun.oss.ClientException;
+import com.aliyun.oss.OSS;
+import com.aliyun.oss.OSSClientBuilder;
+import com.aliyun.oss.OSSException;
+import com.aliyun.oss.common.utils.BinaryUtil;
+import com.aliyun.oss.internal.OSSHeaders;
+import com.aliyun.oss.model.*;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.*;
+import java.net.URL;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.time.LocalDate;
+import java.util.*;
+
+/**
+ * @author ZhouQiang 2022/7/13
+ */
+@Component
+@RequiredArgsConstructor
+@Slf4j
+public class OssFileClient {
+
+    private final OssProperties ossProperties;
+    private final OSS ossClient;
+
+    /**
+     * 普通上传
+     *
+     * @param bytes    文件字节数组
+     * @param fileName 文件名
+     * @return fileKey
+     * @throws IOException 异常
+     */
+    public String upload(byte[] bytes, String fileName) throws IOException {
+        String fileKey = generateFileKey(fileName);
+        PutObjectRequest request = new PutObjectRequest(ossProperties.getBucketName(), fileKey, new ByteArrayInputStream(bytes));
+        ossClient.putObject(request);
+        Arrays.fill(bytes, (byte) 0);
+        return fileKey;
+    }
+
+    /**
+     * 普通上传
+     *
+     * @param file 文件
+     * @return fileKey
+     * @throws IOException 异常
+     */
+    public String upload(MultipartFile file) throws IOException {
+        String fileKey = generateFileKey(file.getOriginalFilename());
+        PutObjectRequest request = new PutObjectRequest(ossProperties.getBucketName(), fileKey, file.getInputStream());
+        ossClient.putObject(request);
+        return fileKey;
+    }
+
+    /**
+     * 普通上传
+     *
+     * @param filePath 文件路径
+     * @param fileName 文件名
+     * @return fileKey 文件key
+     * @throws IOException 异常
+     */
+    public String upload(String filePath, String fileName) throws IOException {
+        String fileKey = generateFileKey(fileName);
+        PutObjectRequest request = new PutObjectRequest(ossProperties.getBucketName(), fileKey, new BufferedInputStream(new FileInputStream(filePath)));
+        ObjectMetadata metadata = new ObjectMetadata();
+        metadata.setHeader(OSSHeaders.CONTENT_DISPOSITION, "attachment;filename="+
+                URLEncoder.encode(fileName,"UTF-8")+";filename*=UTF-8''" +
+                URLEncoder.encode(fileName,"UTF-8"));
+        request.setMetadata(metadata);
+        ossClient.putObject(request);
+        return fileKey;
+    }
+
+    /**
+     * 分片上传
+     *
+     * @param file      文件
+     * @param sliceSize 分片
+     * @return fileKey
+     */
+    public String fragmentUpload(MultipartFile file, Long sliceSize) {
+
+        String fileKey = generateFileKey(file.getOriginalFilename());
+        InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(ossProperties.getBucketName(), fileKey);
+        try {
+            // 初始化分片。
+            InitiateMultipartUploadResult upResult = ossClient.initiateMultipartUpload(request);
+            // 返回uploadId,它是分片上传事件的唯一标识。您可以根据该uploadId发起相关的操作,例如取消分片上传、查询分片上传等。
+            String uploadId = upResult.getUploadId();
+
+            // partETags是PartETag的集合。PartETag由分片的ETag和分片号组成。
+            List<PartETag> partETags = new ArrayList<>();
+            // 每个分片的大小,用于计算文件有多少个分片。单位为字节。
+            final long partSize = sliceSize == null ? 1024 * 1024L : sliceSize;   //1 MB。
+
+            // 填写本地文件的完整路径。如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件。
+            long fileLength = file.getSize();
+            int partCount = (int) (fileLength / partSize);
+            if (fileLength % partSize != 0) {
+                partCount++;
+            }
+            // 遍历分片上传。
+            for (int i = 0; i < partCount; i++) {
+                long startPos = i * partSize;
+                long curPartSize = (i + 1 == partCount) ? (fileLength - startPos) : partSize;
+                // 跳过已经上传的分片。
+                InputStream is = file.getInputStream();
+                is.skip(startPos);
+                UploadPartRequest uploadPartRequest = new UploadPartRequest();
+                uploadPartRequest.setBucketName(ossProperties.getBucketName());
+                uploadPartRequest.setKey(fileKey);
+                uploadPartRequest.setUploadId(uploadId);
+                uploadPartRequest.setInputStream(is);
+                // 设置分片大小。除了最后一个分片没有大小限制,其他的分片最小为100 KB。
+                uploadPartRequest.setPartSize(curPartSize);
+                // 设置分片号。每一个上传的分片都有一个分片号,取值范围是1~10000,如果超出此范围,OSS将返回InvalidArgument错误码。
+                uploadPartRequest.setPartNumber(i + 1);
+                // 每个分片不需要按顺序上传,甚至可以在不同客户端上传,OSS会按照分片号排序组成完整的文件。
+                UploadPartResult uploadPartResult = ossClient.uploadPart(uploadPartRequest);
+                // 每次上传分片之后,OSS的返回结果包含PartETag。PartETag将被保存在partETags中。
+                partETags.add(uploadPartResult.getPartETag());
+            }
+
+            // 创建CompleteMultipartUploadRequest对象。
+            // 在执行完成分片上传操作时,需要提供所有有效的partETags。OSS收到提交的partETags后,会逐一验证每个分片的有效性。当所有的数据分片验证通过后,OSS将把这些分片组合成一个完整的文件。
+            CompleteMultipartUploadRequest completeMultipartUploadRequest =
+                    new CompleteMultipartUploadRequest(ossProperties.getBucketName(), fileKey, uploadId, partETags);
+
+            // 完成分片上传。
+            CompleteMultipartUploadResult completeMultipartUploadResult = ossClient.completeMultipartUpload(completeMultipartUploadRequest);
+            System.out.println(completeMultipartUploadResult.getETag());
+
+            return fileKey;
+        } catch (IOException e) {
+            log.error("分片上传异常", e);
+            throw new BusinessException("文件上传异常");
+        }
+    }
+
+    /**
+     * 文件下载
+     *
+     * @param fileObject 文件Key
+     * @param pathName   下载保存的文件全路径
+     */
+    public void download(String fileObject, String pathName) {
+        ossClient.getObject(new GetObjectRequest(ossProperties.getBucketName(), fileObject), new File(pathName));
+    }
+
+    /**
+     * 文件下载
+     *
+     * @param fileObject 文件key
+     * @return 文件流
+     */
+    public InputStream download(String fileObject) {
+        OSS oss = ossClient();
+        OSSObject ossObject = oss.getObject(ossProperties.getBucketName(), fileObject);
+        return ossObject.getObjectContent();
+    }
+
+    /**
+     * 生成有时效性的oss文件下载地址
+     *
+     * @param fileObject 文件key
+     * @param expiration 有效时间(毫秒)
+     * @return URL
+     */
+    public URL generateTermUrl(String fileObject, long expiration) {
+        return ossClient.generatePresignedUrl(ossProperties.getBucketName(), fileObject, new Date(new Date().getTime() + expiration));
+    }
+
+    /**
+     * 生成fileKey
+     *
+     * @param fileName 文件名
+     * @return fileKey
+     */
+    private String generateFileKey(String fileName) {
+        String uuid = UUID.randomUUID().toString().replaceAll("-", "");
+        return ossProperties.getFilePrefix() + "/" + LocalDate.now().toString().replaceAll("-", "/") + "/" + uuid + "@" + fileName;
+    }
+
+    /**
+     * police策略
+     *
+     * @return map
+     */
+    public Map<String, String> policy() {
+
+        // 设置上传到OSS文件的前缀,可置空此项。置空后,文件将上传至Bucket的根目录下。
+        String dir = LocalDate.now().toString().replaceAll("-", "/");
+        long expireTime = 30;
+        long expireEndTime = expireTime * 1000;
+        Date expiration = new Date(System.currentTimeMillis() + expireEndTime);
+        // TODO 修改自己服务的回调接口
+        String callbackUrl = "https://localhost/upload/callback";
+        PolicyConditions policyConds = new PolicyConditions();
+        policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 10L * 1024 * 1024 * 1024);
+        policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir);
+
+        String postPolicy = ossClient.generatePostPolicy(expiration, policyConds);
+        byte[] binaryData = postPolicy.getBytes(StandardCharsets.UTF_8);
+        String encodedPolicy = BinaryUtil.toBase64String(binaryData);
+        String postSignature = ossClient.calculatePostSignature(postPolicy);
+
+        Map<String, String> respMap = new LinkedHashMap<>();
+        respMap.put("accessid", ossProperties.getAccessKeyId());
+        respMap.put("policy", encodedPolicy);
+        respMap.put("signature", postSignature);
+        respMap.put("dir", dir);
+        respMap.put("host", ossProperties.getEndpoint().replaceFirst("https://", "https://" + ossProperties.getBucketName() + "."));
+        respMap.put("expire", String.valueOf(expireEndTime / 1000));
+
+        JSONObject jasonCallback = new JSONObject();
+        jasonCallback.put("callbackUrl", callbackUrl);
+        jasonCallback.put("callbackBody",
+                "filename=${object}&size=${size}&mimeType=${mimeType}&height=${imageInfo.height}&width=${imageInfo.width}");
+        jasonCallback.put("callbackBodyType", "application/x-www-form-urlencoded");
+        String base64CallbackBody = BinaryUtil.toBase64String(jasonCallback.toString().getBytes());
+        respMap.put("callback", base64CallbackBody);
+
+        return respMap;
+    }
+
+
+    public void deletePrefixFile(String fileDirectory) {
+        String bucketName = ossProperties.getBucketName();
+        String filePrefix = ossProperties.getFilePrefix();
+        StringBuilder prefix = new StringBuilder(filePrefix);
+        prefix.append("/").append(fileDirectory).append("/");
+        try {
+            // 删除目录及目录下的所有文件。
+            String nextMarker = null;
+            ObjectListing objectListing = null;
+            do {
+                ListObjectsRequest listObjectsRequest = new ListObjectsRequest(bucketName)
+                        .withPrefix(prefix.toString())
+                        .withMarker(nextMarker);
+
+                objectListing = ossClient.listObjects(listObjectsRequest);
+                if (objectListing.getObjectSummaries().size() > 0) {
+                    List<String> keys = new ArrayList<>();
+                    for (OSSObjectSummary s : objectListing.getObjectSummaries()) {
+                        keys.add(s.getKey());
+                    }
+                    DeleteObjectsRequest deleteObjectsRequest = new DeleteObjectsRequest(bucketName).withKeys(keys).withEncodingType("url");
+                    DeleteObjectsResult deleteObjectsResult = ossClient.deleteObjects(deleteObjectsRequest);
+                    List<String> deletedObjects = deleteObjectsResult.getDeletedObjects();
+                    try {
+                        for (String obj : deletedObjects) {
+                            String deleteObj = URLDecoder.decode(obj, "UTF-8");
+                            System.out.println(deleteObj);
+                        }
+                    } catch (UnsupportedEncodingException e) {
+                        e.printStackTrace();
+                    }
+                }
+
+                nextMarker = objectListing.getNextMarker();
+            } while (objectListing.isTruncated());
+        } catch (OSSException oe) {
+            System.out.println("Caught an OSSException, which means your request made it to OSS, "
+                    + "but was rejected with an error response for some reason.");
+            System.out.println("Error Message:" + oe.getErrorMessage());
+            System.out.println("Error Code:" + oe.getErrorCode());
+            System.out.println("Request ID:" + oe.getRequestId());
+            System.out.println("Host ID:" + oe.getHostId());
+        } catch (ClientException ce) {
+            System.out.println("Caught an ClientException, which means the client encountered "
+                    + "a serious internal problem while trying to communicate with OSS, "
+                    + "such as not being able to access the network.");
+            System.out.println("Error Message:" + ce.getMessage());
+        }
+    }
+
+    public OSS ossClient() {
+        return new OSSClientBuilder().build(ossProperties.getEndpoint(), ossProperties.getAccessKeyId(), ossProperties.getAccessKeySecret());
+    }
+}

+ 24 - 0
background-user/src/main/java/cn/kdan/compdf/properties/OssProperties.java

@@ -0,0 +1,24 @@
+package cn.kdan.compdf.properties;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @author ZhouQiang 2022/7/13
+ */
+@ConfigurationProperties(prefix = "compdf.oss")
+@Configuration
+@Data
+public class OssProperties {
+
+    private String bucketName;
+
+    private String endpoint;
+
+    private String accessKeyId;
+
+    private String accessKeySecret;
+
+    private String filePrefix;
+}

+ 2 - 0
background-user/src/main/java/cn/kdan/compdf/service/OrdersService.java

@@ -20,4 +20,6 @@ public interface OrdersService extends IService<Orders> {
     List<OrderBillVo> getSaaSOrder();
 
     void updateBillInfo(Integer orderId, String billUrl, String billNo, String toJSONString);
+
+    OrderBillVo getSaaSOrderByOrderId(Integer orderId);
 }

+ 11 - 0
background-user/src/main/java/cn/kdan/compdf/service/UserBillingInformationService.java

@@ -1,7 +1,9 @@
 package cn.kdan.compdf.service;
 
+import cn.kdan.compdf.dto.ApplyInvoiceDTO;
 import cn.kdan.compdf.dto.UserBillingUpInfoDTO;
 import cn.kdan.compdf.entity.UserBillingInformation;
+import cn.kdan.compdf.vo.ApplyInvoiceVO;
 import cn.kdan.compdf.vo.UserBillingInfoVO;
 import com.baomidou.mybatisplus.extension.service.IService;
 
@@ -34,4 +36,13 @@ public interface UserBillingInformationService extends IService<UserBillingInfor
      * @param email       email
      */
     void updateUserBillingEmail(Integer compdfKitId, String email);
+
+    /**
+     * 申请开发票
+     *
+     * @param compdfkitId     compdfkitId
+     * @param applyInvoiceDTO applyInvoiceDTO
+     * @return ApplyInvoiceVO
+     */
+    ApplyInvoiceVO applyInvoice(Integer compdfkitId, ApplyInvoiceDTO applyInvoiceDTO);
 }

+ 9 - 0
background-user/src/main/java/cn/kdan/compdf/service/impl/OrdersServiceImpl.java

@@ -7,6 +7,7 @@ import cn.kdan.compdf.vo.OrderBillVo;
 import com.alibaba.fastjson.JSONObject;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.beans.BeanUtils;
 import org.springframework.stereotype.Service;
 
 import java.util.List;
@@ -35,5 +36,13 @@ public class OrdersServiceImpl extends ServiceImpl<OrdersMapper, Orders> impleme
         this.baseMapper.update(orders,new LambdaQueryWrapper<Orders>().eq(Orders::getOrderNo,orderId));
     }
 
+    @Override
+    public OrderBillVo getSaaSOrderByOrderId(Integer orderId) {
+        Orders orders = this.baseMapper.selectOne(new LambdaQueryWrapper<Orders>().eq(Orders::getId, orderId));
+        OrderBillVo orderBillVo = new OrderBillVo();
+        BeanUtils.copyProperties(orders,orderBillVo);
+        return orderBillVo;
+    }
+
 
 }

+ 52 - 2
background-user/src/main/java/cn/kdan/compdf/service/impl/UserBillingInformationServiceImpl.java

@@ -3,6 +3,7 @@ package cn.kdan.compdf.service.impl;
 import cn.hutool.core.bean.BeanUtil;
 import cn.hutool.core.date.DateUtil;
 import cn.kdan.compdf.dto.ApplyInvoiceDTO;
+import cn.kdan.compdf.dto.BillSendEmailDTO;
 import cn.kdan.compdf.dto.UserBillingUpInfoDTO;
 import cn.kdan.compdf.entity.UserBillingInformation;
 import cn.kdan.compdf.enums.ResponseEnum;
@@ -11,6 +12,7 @@ import cn.kdan.compdf.mapper.UserBillingInformationMapper;
 import cn.kdan.compdf.service.OrdersService;
 import cn.kdan.compdf.service.UserBillingInformationService;
 import cn.kdan.compdf.utils.TemplatesUtil;
+import cn.kdan.compdf.vo.ApplyInvoiceVO;
 import cn.kdan.compdf.vo.OrderBillVo;
 import cn.kdan.compdf.vo.UserBillingInfoVO;
 import com.alibaba.fastjson.JSONObject;
@@ -19,20 +21,28 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.io.FileUtils;
 import org.apache.commons.lang3.ObjectUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.http.HttpEntity;
 import org.springframework.http.HttpHeaders;
 import org.springframework.http.MediaType;
+import org.springframework.mail.javamail.JavaMailSender;
+import org.springframework.mail.javamail.MimeMessageHelper;
 import org.springframework.stereotype.Service;
 import org.springframework.util.LinkedMultiValueMap;
 import org.springframework.util.MultiValueMap;
 import org.springframework.util.StreamUtils;
 import org.springframework.web.client.RestTemplate;
 
+import javax.mail.MessagingException;
+import javax.mail.internet.MimeMessage;
+import java.io.File;
 import java.io.IOException;
 import java.lang.reflect.Field;
+import java.net.MalformedURLException;
+import java.net.URL;
 import java.nio.charset.StandardCharsets;
 import java.text.SimpleDateFormat;
 import java.time.Instant;
@@ -54,7 +64,9 @@ public class UserBillingInformationServiceImpl extends ServiceImpl<UserBillingIn
     private String invoiceUrl;
 
     private static final RestTemplate restTemplate = new RestTemplate();
-
+    private final JavaMailSender javaMailSender;
+    @Value("${spring.mail.username}")
+    private String fromEmail;
     private final OrdersService ordersService;
 
     static {
@@ -123,7 +135,16 @@ public class UserBillingInformationServiceImpl extends ServiceImpl<UserBillingIn
         }
     }
 
-    public void applyInvoice(Integer compdfkitId, ApplyInvoiceDTO applyInvoiceDTO) {
+
+    /**
+     * 申请开发票
+     *
+     * @param compdfkitId     compdfkitId
+     * @param applyInvoiceDTO applyInvoiceDTO
+     * @return ApplyInvoiceVO
+     */
+    @Override
+    public ApplyInvoiceVO applyInvoice(Integer compdfkitId, ApplyInvoiceDTO applyInvoiceDTO) {
 
         UserBillingInfoVO userBillingInfo = this.getUserBillingInfo(compdfkitId);
         // 发送邮箱是否填写
@@ -159,8 +180,37 @@ public class UserBillingInformationServiceImpl extends ServiceImpl<UserBillingIn
         // 持久化发票数据
         ordersService.updateBillInfo(applyInvoiceDTO.getOrderId(), billUrl, billNo, JSONObject.toJSONString(userBillingInfo));
         // 返回结果
+        ApplyInvoiceVO applyInvoiceVO = new ApplyInvoiceVO();
+        applyInvoiceVO.setBillNo(billNo);
+        applyInvoiceVO.setBillUrl(billUrl);
+        return applyInvoiceVO;
     }
 
+
+    public void billSendEmail(BillSendEmailDTO billSendEmailDTO) {
+        OrderBillVo orderBillVo = ordersService.getSaaSOrderByOrderId(billSendEmailDTO.getOrderId());
+        try {
+            // 发送邮件并发送附件
+            MimeMessage message = javaMailSender.createMimeMessage();
+            //第2个参数:是否允许添加多部件
+            MimeMessageHelper helper = new MimeMessageHelper(message, true);
+            helper.setFrom(fromEmail);
+            helper.setTo(billSendEmailDTO.getEmail());
+            helper.setSubject("发票");
+            //第2个参数:是否解析html
+            helper.setText("这是一个发票", true);
+            //添加附件
+            File file = new File("invoice.pdf");
+            FileUtils.copyURLToFile(new URL(orderBillVo.getBillUrl()), file);
+            helper.addAttachment("invoice.pdf", file);
+            javaMailSender.send(message);
+        } catch (MessagingException | IOException e) {
+            log.error(e.getMessage(),e);
+            throw new BusinessException("发票发送失败");
+        }
+    }
+
+
     // 工具方法:判断属性是否为null或空字符
     private void checkFieldNotNullOrEmpty(Object obj, Field field) {
         try {

+ 17 - 0
background-user/src/main/java/cn/kdan/compdf/vo/ApplyInvoiceVO.java

@@ -0,0 +1,17 @@
+package cn.kdan.compdf.vo;
+
+import lombok.Data;
+import lombok.ToString;
+
+/**
+ * @author ComPDFKit-WPH 2023/5/18
+ */
+@Data
+@ToString
+public class ApplyInvoiceVO {
+
+    private String billNo;
+
+    private String billUrl;
+
+}