tangxiangan 4 місяців тому
батько
коміт
cf25d82af5
17 змінених файлів з 339 додано та 59 видалено
  1. 5 4
      pdf-office-account/src/main/java/cn/kdan/cloud/pdf/office/account/enums/SourceProduct.java
  2. 2 0
      pdf-office-api/pdf-office-api-payment/src/main/java/cn/kdan/cloud/pdf/office/api/payment/dto/CreateOrderManualDTO.java
  3. 21 21
      pdf-office-email/src/main/java/cn/kdan/cloud/pdf/office/email/rabbit/listener/SendEmailListener.java
  4. 34 2
      pdf-office-payment/src/main/java/cn/kdan/cloud/pdf/office/payment/client/AppStoreClient.java
  5. 1 0
      pdf-office-payment/src/main/java/cn/kdan/cloud/pdf/office/payment/entity/Order.java
  6. 2 0
      pdf-office-payment/src/main/java/cn/kdan/cloud/pdf/office/payment/properties/AppStoreProperties.java
  7. 96 10
      pdf-office-payment/src/main/java/cn/kdan/cloud/pdf/office/payment/service/impl/AppStoreServiceImpl.java
  8. 81 8
      pdf-office-payment/src/main/java/cn/kdan/cloud/pdf/office/payment/service/impl/GooglePayServiceImpl.java
  9. 8 0
      pdf-office-payment/src/main/java/cn/kdan/cloud/pdf/office/payment/service/impl/OrderServiceImpl.java
  10. 41 3
      pdf-office-payment/src/main/java/cn/kdan/cloud/pdf/office/payment/webhook/AppStoreWebhookMonitor.java
  11. 31 0
      pdf-office-payment/src/main/java/cn/kdan/cloud/pdf/office/payment/webhook/GoogleWebhookMonitor.java
  12. 3 0
      pdf-office-payment/src/main/java/cn/kdan/cloud/pdf/office/payment/webhook/appstore/notification/JWSRenewalInfoDecodedPayload.java
  13. 1 0
      pdf-office-payment/src/main/java/cn/kdan/cloud/pdf/office/payment/webhook/appstore/notification/JWSTransactionDecodedPayload.java
  14. 9 0
      pdf-office-pdf-website/src/main/java/cn/kdan/cloud/pdf/office/website/service/impl/OrderGiftServiceImpl.java
  15. 1 1
      pdf-office-sso/src/main/java/cn/kdan/cloud/pdf/office/sso/controller/UserCenterController.java
  16. 1 7
      pdf-office-sso/src/main/java/cn/kdan/cloud/pdf/office/sso/controller/UserController.java
  17. 2 3
      pdf-office-sso/src/main/java/cn/kdan/cloud/pdf/office/sso/service/impl/AuthServiceImpl.java

+ 5 - 4
pdf-office-account/src/main/java/cn/kdan/cloud/pdf/office/account/enums/SourceProduct.java

@@ -1,10 +1,11 @@
 package cn.kdan.cloud.pdf.office.account.enums;
 
 public enum SourceProduct {
-    PDF_READER_PRO_MAC("1", "PDF Reader Pro Mac"),
-    PDF_READER_PRO_WINDOWS("2", "PDF Reader Pro Windows"),
-    PDF_READER_PRO_ANDROID("3", "PDF Reader Pro Android"),
-    PDF_READER_PRO_IOS("4", "PDF Reader Pro iOS");
+    PDF_READER_PRO_MAC("1", "mac"),
+    PDF_READER_PRO_WINDOWS("2", "windows"),
+    PDF_READER_PRO_ANDROID("3", "android"),
+    PDF_READER_PRO_IOS("4", "ios"),
+    PDF_READER_PRO_WEB("5", "官网");
 
     private final String id;
     private final String name;

+ 2 - 0
pdf-office-api/pdf-office-api-payment/src/main/java/cn/kdan/cloud/pdf/office/api/payment/dto/CreateOrderManualDTO.java

@@ -61,4 +61,6 @@ public class CreateOrderManualDTO {
 
     private String result;
 
+    private String currency;
+    private BigDecimal reducedPrice;
 }

+ 21 - 21
pdf-office-email/src/main/java/cn/kdan/cloud/pdf/office/email/rabbit/listener/SendEmailListener.java

@@ -32,30 +32,30 @@ import java.util.Objects;
 /**
  * @author ComPDFKit-WPH 2023/1/11
  */
-@Component
-@Slf4j
-@RequiredArgsConstructor
-public class SendEmailListener {
+        @Component
+        @Slf4j
+        @RequiredArgsConstructor
+        public class SendEmailListener {
 
-    private final SendEmailHandlerService sendEmailService;
+            private final SendEmailHandlerService sendEmailService;
 
-    private final MailSenderConfig mailSenderConfig;
-    private final EmailLogService emailLogService;
-    private final EmailTemplateService emailTemplateService;
+            private final MailSenderConfig mailSenderConfig;
+            private final EmailLogService emailLogService;
+            private final EmailTemplateService emailTemplateService;
 
-    /**
-     * 发送邮件 监听
-     *
-     * @param message    message
-     * @param channel    channel
-     * @param emailSendBO sendObject
-     */
-    @RabbitListener(queues = RabbitMqConstant.EMAIL_SEND_QUEUE)
-    public void backgroundApiAddQueue(Message message, Channel channel, EmailSendBO emailSendBO) {
-        log.info("邮件发送监听内容:{}", emailSendBO);
-        EmailLog emailLog = new EmailLog();
-        String emailTemplateId = null;
-        File file = null;
+            /**
+             * 发送邮件 监听
+             *
+             * @param message    message
+             * @param channel    channel
+             * @param emailSendBO sendObject
+             */
+            @RabbitListener(queues = RabbitMqConstant.EMAIL_SEND_QUEUE)
+            public void backgroundApiAddQueue(Message message, Channel channel, EmailSendBO emailSendBO) {
+                log.info("邮件发送监听内容:{}", emailSendBO);
+                EmailLog emailLog = new EmailLog();
+                String emailTemplateId = null;
+                File file = null;
         try {
             if(StringUtils.isEmpty(emailSendBO.getToEmail())){
                 log.info("邮箱为空:{}", emailSendBO);

+ 34 - 2
pdf-office-payment/src/main/java/cn/kdan/cloud/pdf/office/payment/client/AppStoreClient.java

@@ -179,6 +179,39 @@ public class AppStoreClient {
         return null; // No transactions found
     }
 
+    public AppTransaction fetchSandboxFirstAppTransaction(String transactionId, String appBundleId) {
+        String bearerToken = generateBearerToken(appBundleId); // 直接调用生成Bearer token的方法
+
+        // Create headers and add the Bearer token
+        HttpHeaders httpHeaders = new HttpHeaders();
+        httpHeaders.setContentType(MediaType.APPLICATION_JSON);
+        httpHeaders.setBearerAuth(bearerToken);
+        String isSandbox = properties.getIsSandbox();
+        // Define the sorting order
+        String sortingOrder = "?sort=DESCENDING"; // 设置为 DESCENDING 以按最新的顺序排序
+        String url = "https://api.storekit-sandbox.itunes.apple.com/inApps/v2/history/{transactionId}";
+        // Make the GET request
+        HistoryResponse response = restTemplate.exchange(
+                url + sortingOrder,
+                HttpMethod.GET,
+                new HttpEntity<>(null, httpHeaders),
+                HistoryResponse.class,
+                transactionId
+        ).getBody();
+
+        if (response != null && response.getSignedTransactions() != null && !response.getSignedTransactions().isEmpty()) {
+            List<AppTransaction> signedTransactions = new ArrayList<>();
+            response.getSignedTransactions().forEach(item -> {
+                String payloadCode = StrUtil.split(item, '.').get(1);
+                String decodedString = new String(Base64.getDecoder().decode(payloadCode));
+                AppTransaction appTransaction = JsonUtils.jsonStringToBean(decodedString, AppTransaction.class);
+                signedTransactions.add(appTransaction);
+            });
+            // Return the first AppTransaction
+            return signedTransactions.isEmpty() ? null : signedTransactions.get(0);
+        }
+        return null; // No transactions found
+    }
 
 
     public static void main(String[] args) {
@@ -201,7 +234,6 @@ public class AppStoreClient {
 
 
         RestTemplate restTemplate = new RestTemplate();
-        restTemplate.setInterceptors(Collections.singletonList(new AppStoreClientHttpRequestInterceptor()));
         restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {
             @Override
             public void handleError(@NotNull ClientHttpResponse clientHttpResponse) throws IOException {
@@ -213,7 +245,7 @@ public class AppStoreClient {
 
         // Make the GET request with sorting order
         HistoryResponse a = restTemplate.exchange(
-                "https://api.storekit.itunes.apple.com/inApps/v2/history/{transactionId}?sort=" + sortingOrder,
+                "https://api.storekit-sandbox.itunes.apple.com/inApps/v2/history/{transactionId}?sort=" + sortingOrder,
                 HttpMethod.GET, // Change to GET
                 new HttpEntity<>(null, httpHeaders), // No body for GET, just headers
                 HistoryResponse.class,

+ 1 - 0
pdf-office-payment/src/main/java/cn/kdan/cloud/pdf/office/payment/entity/Order.java

@@ -114,4 +114,5 @@ public class Order extends BaseEntity{
      * 付费模式(1自动续订 2单次付费)
      */
     private Integer paymentModel;
+    private String currency;
 }

+ 2 - 0
pdf-office-payment/src/main/java/cn/kdan/cloud/pdf/office/payment/properties/AppStoreProperties.java

@@ -37,4 +37,6 @@ public class AppStoreProperties {
     private String keyId;
     private String isSandbox;
 
+    private String currency;
+    private  String testCallBack;
 }

+ 96 - 10
pdf-office-payment/src/main/java/cn/kdan/cloud/pdf/office/payment/service/impl/AppStoreServiceImpl.java

@@ -16,6 +16,7 @@ import cn.kdan.cloud.pdf.office.common.enums.ExceptionEnum;
 import cn.kdan.cloud.pdf.office.common.enums.payment.SubscriptionTypeEnum;
 import cn.kdan.cloud.pdf.office.common.exception.BackendRuntimeException;
 import cn.kdan.cloud.pdf.office.common.utils.CommonUtils;
+import cn.kdan.cloud.pdf.office.common.utils.MyDateUtils;
 import cn.kdan.cloud.pdf.office.common.vo.UserInfoVO;
 import cn.kdan.cloud.pdf.office.common.vo.UserVO;
 import cn.kdan.cloud.pdf.office.payment.client.AppStoreClient;
@@ -24,14 +25,25 @@ import cn.kdan.cloud.pdf.office.payment.service.AppStoreService;
 import cn.kdan.cloud.pdf.office.payment.service.OrderService;
 import cn.kdan.cloud.pdf.office.payment.service.RestorePurchaseLogsService;
 import cn.kdan.cloud.pdf.office.payment.service.SubscriptionsService;
+import cn.kdan.cloud.pdf.office.payment.utils.TemplatesUtil;
 import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
 import com.baomidou.mybatisplus.core.toolkit.StringUtils;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+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.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.util.StreamUtils;
+import org.springframework.web.client.RestTemplate;
 
+import java.io.IOException;
 import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
 import java.text.SimpleDateFormat;
 import java.util.*;
 
@@ -58,6 +70,12 @@ public class AppStoreServiceImpl implements AppStoreService {
 
 
     private final EmailApi emailApi;
+    private AppStoreProperties properties;
+    private static final RestTemplate restTemplate = new RestTemplate();
+
+    @Value("${htmlToPdfUrl:http://139.196.160.101:3060/api/get-invoice}")
+    private String htmlToPdfUrl;
+
 
 
     @Transactional
@@ -75,6 +93,7 @@ public class AppStoreServiceImpl implements AppStoreService {
         String transactionId = appTransaction.getTransactionId();
         String originalTransactionId = appTransaction.getOriginalTransactionId();
         BigDecimal price = BigDecimal.valueOf(appTransaction.getPrice()/1000);
+        String currency = appTransaction.getCurrency();
         OrdersVO orderByThirdOrderNo = orderService.getOrderByThirdOrderNo(transactionId);
         if (null != orderByThirdOrderNo) {
             throw new BackendRuntimeException(ExceptionEnum.EXCEPTION_TICKET_HAS_BEEN_CREATED);
@@ -103,11 +122,13 @@ public class AppStoreServiceImpl implements AppStoreService {
             createSubscription.setPrice(price);
             createSubscription.setPayTime(0);
             subscriptionsService.createSubscription(createSubscription);
+            String readeNo = MyDateUtils.getTimeStamp() + "-" + product.getId() + "-" + (int) ((Math.random() * 9 + 1) * 1000);
             // 更新订单
             CreateOrderManualDTO orderManualDTO = CreateOrderManualDTO.builder()
                     .thirdTradeNo(originalTransactionId)
                     .thirdOrderNo(transactionId)
-                    .price(price)
+                    .price(product.getPrice())
+                    .reducedPrice(price)
                     .subscriptionId(subscriptionId)
                     .userId(equityVerificationDTO.getUserId())
                     .email(userVO.getEmail())
@@ -118,16 +139,19 @@ public class AppStoreServiceImpl implements AppStoreService {
                     .paymentModel(2)
                     .subscriptionType(1)
                     .payNumber(1)
+                    .tradeNo(readeNo)
+                    .invoiceNo(MyDateUtils.getTimeStamp() + (int) ((Math.random() * 9 + 1) * 1000))
                     .result(appTransaction.toString())
+                    .currency(currency)
                     .build();
         orderService.createOrderManual(orderManualDTO);
-        sendBuyEmail(product, userVO);
         UserInfoVO result = userApi.getInfoById(equityVerificationDTO.getUserId()).getResult();
         result.setDigestPassword(null);
+        sendBuyEmail(product, userVO,orderManualDTO);
         return result;
     }
 
-    private void sendBuyEmail(ProductVO product, UserVO userVO) {
+    private void sendBuyEmail(ProductVO product, UserVO userVO,CreateOrderManualDTO orderManualDTO) {
         if(product.getCode().contains("upgrade")){
             EmailSendBO bo = new EmailSendBO();
             bo.setToEmail(userVO.getEmail());
@@ -144,13 +168,22 @@ public class AppStoreServiceImpl implements AppStoreService {
             // 格式化当前日期
             String formattedDate = sdf.format(date);
             contentMap.put("@date@",formattedDate);
-            String price = ObjectUtils.isNotEmpty(product.getDisplayPrice())? product.getDisplayPrice().toString() : product.getPrice().toString();
-
+            String price;
+            BigDecimal renewPrice;
+            if(orderManualDTO.getCurrency().equals("USD")){
+                price = ObjectUtils.isNotEmpty(product.getDisplayPrice())? product.getDisplayPrice().toString() : product.getPrice().toString();
+                renewPrice = product.getPrice();
+            }else{
+                price = ObjectUtils.isNotEmpty(product.getCnyDisplayPrice())? product.getCnyDisplayPrice().toString() : product.getCnyPrice().toString();
+                renewPrice = product.getCnyPrice();
+            }
             contentMap.put("@payPrice@", price);
-            contentMap.put("@renewPrice@", product.getPrice().toString());
+            contentMap.put("@renewPrice@", renewPrice.toString());
             bo.setSendTitleContent(titleMap);
             //设置内容
             bo.setSendContent(contentMap);
+            String file = sendInvoice(userVO.getEmail(),formattedDate,orderManualDTO.getTradeNo(),orderManualDTO.getInvoiceNo(),renewPrice,orderManualDTO.getPrice());
+            bo.setFileUrl(file);
             emailApi.sendEmail(bo);
         }else{
             EmailSendBO bo = new EmailSendBO();
@@ -168,15 +201,62 @@ public class AppStoreServiceImpl implements AppStoreService {
             // 格式化当前日期
             String formattedDate = sdf.format(date);
             contentMap.put("@date@",formattedDate);
-            String price = ObjectUtils.isNotEmpty(product.getDisplayPrice())? product.getDisplayPrice().toString() : product.getPrice().toString();
+            String price;
+            BigDecimal renewPrice;
+            if(orderManualDTO.getCurrency().equals("USD")){
+                price = ObjectUtils.isNotEmpty(product.getDisplayPrice())? product.getDisplayPrice().toString() : product.getPrice().toString();
+                renewPrice = product.getPrice();
+            }else{
+                price = ObjectUtils.isNotEmpty(product.getCnyDisplayPrice())? product.getCnyDisplayPrice().toString() : product.getCnyPrice().toString();
+                renewPrice = product.getCnyPrice();
+            }
             contentMap.put("@payPrice@", price);
             bo.setSendTitleContent(titleMap);
             //设置内容
             bo.setSendContent(contentMap);
+            String file = sendInvoice(userVO.getEmail(),formattedDate,orderManualDTO.getTradeNo(),orderManualDTO.getInvoiceNo(),renewPrice,orderManualDTO.getPrice());
+            bo.setFileUrl(file);
             emailApi.sendEmail(bo);
         }
     }
 
+    //生成发票
+    private String sendInvoice(String email, String purchasedDate,String tradeNo,String invoiceNumber,BigDecimal unitPrice,BigDecimal amount) {
+        String invoiceHtml = null;
+        try {
+            invoiceHtml = new String(StreamUtils.copyToByteArray(this.getClass().getClassLoader().getResourceAsStream("templates/invoice_member.html")), StandardCharsets.UTF_8);
+        } catch (IOException e) {
+            log.error(e.getMessage());
+        }
+        Map<String, Object> map = new HashMap<>();
+        map.put("Name", email);
+        map.put("email", email);
+        map.put("purchasedDate", purchasedDate);
+        map.put("orderNumber",tradeNo);
+        map.put("invoiceDate", purchasedDate);
+        map.put("invoiceNumber", invoiceNumber);
+        map.put("quantity", 1);
+        map.put("unitPrice", unitPrice);
+        BigDecimal discountAmount = unitPrice.subtract(amount);
+        map.put("discount", discountAmount);
+        map.put("amount", amount);
+        map.put("total", amount);
+        log.info("发票html内容填充map:{}", map);
+        // 生成替换内容后的发票
+        String htmlStr = TemplatesUtil.replaceStringUsingFreeMarker(invoiceHtml, map);
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
+        // 封装表单数据
+        MultiValueMap<String, String> paramMap = new LinkedMultiValueMap<>();
+        paramMap.add("html", htmlStr);
+        HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(paramMap, headers);
+        // 发送POST请求,html转pdf
+        Map postForObjectMap = restTemplate.postForObject(htmlToPdfUrl, request, Map.class);
+        String invoiceFileUrl = postForObjectMap.get("url").toString();
+        log.info("发票文件地址:{}", invoiceFileUrl);
+        return invoiceFileUrl;
+    }
+
     /**
      * 票据验证
      *
@@ -185,11 +265,17 @@ public class AppStoreServiceImpl implements AppStoreService {
     private AppTransaction appStoreReceiptVerify(String transactionId, String applePayProductId,String appBundleId) {
         // 验证票据信息
         AppTransaction transaction = appStoreClient.fetchFirstAppTransaction(transactionId,appBundleId);
-        if(Objects.isNull(transaction)){
-            log.error("验证苹果交易失败,transactionId:{}",transactionId);
+        // 如果主环境交易记录为null,尝试获取沙箱环境交易记录
+        if (Objects.isNull(transaction) && "false".equals(properties.getIsSandbox())) {
+            transaction = appStoreClient.fetchSandboxFirstAppTransaction(transactionId, appBundleId);
+        }
+
+        // 如果最终transaction仍为null,抛出验证失败异常
+        if (Objects.isNull(transaction)) {
+            log.error("验证苹果交易失败,transactionId:{}", transactionId);
             throw new BackendRuntimeException(ExceptionEnum.EXCEPTION_MSG_APP_STORE_TRANSACTION_ID_VALIDATION_FAILED);
         }
-        log.info("票据信息:{}", transaction.toString());
+        log.info("苹果票据信息:{}", transaction.toString());
         // 检查支付状态等
         if (transaction.getProductId().equals(applePayProductId)) {
             Date expiresDateMs = transaction.getExpiresDate();

+ 81 - 8
pdf-office-payment/src/main/java/cn/kdan/cloud/pdf/office/payment/service/impl/GooglePayServiceImpl.java

@@ -24,6 +24,7 @@ import cn.kdan.cloud.pdf.office.payment.service.GooglePayService;
 import cn.kdan.cloud.pdf.office.payment.service.OrderService;
 import cn.kdan.cloud.pdf.office.payment.service.RestorePurchaseLogsService;
 import cn.kdan.cloud.pdf.office.payment.service.SubscriptionsService;
+import cn.kdan.cloud.pdf.office.payment.utils.TemplatesUtil;
 import cn.kdan.cloud.pdf.office.payment.webhook.google.SubscriptionPurchaseLineItemMS;
 import cn.kdan.cloud.pdf.office.payment.webhook.google.SubscriptionPurchaseV3;
 import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
@@ -33,10 +34,20 @@ import com.google.api.services.androidpublisher.model.SubscriptionPurchaseLineIt
 import com.google.api.services.androidpublisher.model.SubscriptionPurchaseV2;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+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.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.util.StreamUtils;
+import org.springframework.web.client.RestTemplate;
 
+import java.io.IOException;
 import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
 import java.text.SimpleDateFormat;
 import java.time.ZoneOffset;
 import java.time.ZonedDateTime;
@@ -69,7 +80,9 @@ public class GooglePayServiceImpl implements GooglePayService {
     private final UserApi userApi;
 
     private final EmailApi emailApi;
-
+    private static final RestTemplate restTemplate = new RestTemplate();
+    @Value("${htmlToPdfUrl:http://139.196.160.101:3060/api/get-invoice}")
+    private String htmlToPdfUrl;
 
 
     @Transactional
@@ -132,7 +145,8 @@ public class GooglePayServiceImpl implements GooglePayService {
         CreateOrderManualDTO orderManualDTO = CreateOrderManualDTO.builder()
                 .thirdTradeNo(subscriptionPurchaseV2.getExternalAccountIdentifiers().getObfuscatedExternalAccountId())
                 .thirdOrderNo(subscriptionPurchaseV2.getLatestOrderId())
-                .price(price)
+                .price(product.getPrice())
+                .reducedPrice(price)
                 .subscriptionId(subscriptionId)
                 .userId(googlePayDTO.getUserId())
                 .id(orderId)
@@ -144,17 +158,20 @@ public class GooglePayServiceImpl implements GooglePayService {
                 .paymentModel(2)
                 .subscriptionType(1)
                 .payNumber(1)
+                .tradeNo(readeNo)
+                .invoiceNo(MyDateUtils.getTimeStamp() + (int) ((Math.random() * 9 + 1) * 1000))
                 .result(googlePayDTO.getPurchaseToken())
                 .tradeNo(readeNo)
+                .currency("USD")
                 .build();
         orderService.createOrderManual(orderManualDTO);
-        sendBuyEmail(product, userVO);
+        sendBuyEmail(product, userVO,orderManualDTO);
         UserInfoVO result = userApi.getInfoById(googlePayDTO.getUserId()).getResult();
         result.setDigestPassword(null);
         return result;
     }
 
-    private void sendBuyEmail(ProductVO product, UserVO userVO) {
+    private void sendBuyEmail(ProductVO product, UserVO userVO,CreateOrderManualDTO orderManualDTO) {
         if(product.getCode().contains("upgrade")){
             EmailSendBO bo = new EmailSendBO();
             bo.setToEmail(userVO.getEmail());
@@ -171,13 +188,22 @@ public class GooglePayServiceImpl implements GooglePayService {
             // 格式化当前日期
             String formattedDate = sdf.format(date);
             contentMap.put("@date@",formattedDate);
-            String price = ObjectUtils.isNotEmpty(product.getDisplayPrice())? product.getDisplayPrice().toString() : product.getPrice().toString();
-
+            String price;
+            BigDecimal renewPrice;
+            if(orderManualDTO.getCurrency().equals("USD")){
+                price = ObjectUtils.isNotEmpty(product.getDisplayPrice())? product.getDisplayPrice().toString() : product.getPrice().toString();
+                renewPrice = product.getPrice();
+            }else{
+                price = ObjectUtils.isNotEmpty(product.getCnyDisplayPrice())? product.getCnyDisplayPrice().toString() : product.getCnyPrice().toString();
+                renewPrice = product.getCnyPrice();
+            }
             contentMap.put("@payPrice@", price);
-            contentMap.put("@renewPrice@", product.getPrice().toString());
+            contentMap.put("@renewPrice@", renewPrice.toString());
             bo.setSendTitleContent(titleMap);
             //设置内容
             bo.setSendContent(contentMap);
+            String file = sendInvoice(userVO.getEmail(),formattedDate,orderManualDTO.getTradeNo(),orderManualDTO.getInvoiceNo(),renewPrice,orderManualDTO.getPrice());
+            bo.setFileUrl(file);
             emailApi.sendEmail(bo);
         }else{
             EmailSendBO bo = new EmailSendBO();
@@ -195,15 +221,62 @@ public class GooglePayServiceImpl implements GooglePayService {
             // 格式化当前日期
             String formattedDate = sdf.format(date);
             contentMap.put("@date@",formattedDate);
-            String price = ObjectUtils.isNotEmpty(product.getDisplayPrice())? product.getDisplayPrice().toString() : product.getPrice().toString();
+            String price;
+            BigDecimal renewPrice;
+            if(orderManualDTO.getCurrency().equals("USD")){
+                price = ObjectUtils.isNotEmpty(product.getDisplayPrice())? product.getDisplayPrice().toString() : product.getPrice().toString();
+                renewPrice = product.getPrice();
+            }else{
+                price = ObjectUtils.isNotEmpty(product.getCnyDisplayPrice())? product.getCnyDisplayPrice().toString() : product.getCnyPrice().toString();
+                renewPrice = product.getCnyPrice();
+            }
             contentMap.put("@payPrice@", price);
             bo.setSendTitleContent(titleMap);
             //设置内容
             bo.setSendContent(contentMap);
+            String file = sendInvoice(userVO.getEmail(),formattedDate,orderManualDTO.getTradeNo(),orderManualDTO.getInvoiceNo(),renewPrice,orderManualDTO.getPrice());
+            bo.setFileUrl(file);
             emailApi.sendEmail(bo);
         }
     }
 
+    //生成发票
+    private String sendInvoice(String email, String purchasedDate,String tradeNo,String invoiceNumber,BigDecimal unitPrice,BigDecimal amount) {
+        String invoiceHtml = null;
+        try {
+            invoiceHtml = new String(StreamUtils.copyToByteArray(this.getClass().getClassLoader().getResourceAsStream("templates/invoice_member.html")), StandardCharsets.UTF_8);
+        } catch (IOException e) {
+            log.error(e.getMessage());
+        }
+        Map<String, Object> map = new HashMap<>();
+        map.put("Name", email);
+        map.put("email", email);
+        map.put("purchasedDate", purchasedDate);
+        map.put("orderNumber",tradeNo);
+        map.put("invoiceDate", purchasedDate);
+        map.put("invoiceNumber", invoiceNumber);
+        map.put("quantity", 1);
+        map.put("unitPrice", unitPrice);
+        BigDecimal discountAmount = unitPrice.subtract(amount);
+        map.put("discount", discountAmount);
+        map.put("amount", amount);
+        map.put("total", amount);
+        log.info("发票html内容填充map:{}", map);
+        // 生成替换内容后的发票
+        String htmlStr = TemplatesUtil.replaceStringUsingFreeMarker(invoiceHtml, map);
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
+        // 封装表单数据
+        MultiValueMap<String, String> paramMap = new LinkedMultiValueMap<>();
+        paramMap.add("html", htmlStr);
+        HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(paramMap, headers);
+        // 发送POST请求,html转pdf
+        Map postForObjectMap = restTemplate.postForObject(htmlToPdfUrl, request, Map.class);
+        String invoiceFileUrl = postForObjectMap.get("url").toString();
+        log.info("发票文件地址:{}", invoiceFileUrl);
+        return invoiceFileUrl;
+    }
+
     @Override
     public SubscriptionPurchaseV3 verify(String purchaseToken, String packageName) {
         SubscriptionPurchaseV3 subscriptionGoogleOrderV2 = googlePayClient.getSubscription(packageName, purchaseToken);

+ 8 - 0
pdf-office-payment/src/main/java/cn/kdan/cloud/pdf/office/payment/service/impl/OrderServiceImpl.java

@@ -742,6 +742,9 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
         orders.setPayment(createOrderManual.getPaymentMethod().getValue());
         orders.setStatus(OrderConstant.COMPLETED);
         orders.setPayDate(new Date());
+        if(ObjectUtils.isNotEmpty(createOrderManual.getReducedPrice())){
+            orders.setReducedPrice(createOrderManual.getReducedPrice());
+        }
         //subscriptionType payNumber paymentModel 判断有没有,有的话才设置
         if (createOrderManual.getSubscriptionType() != null) {
             orders.setSubscriptionType(createOrderManual.getSubscriptionType());
@@ -752,6 +755,11 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
         if (createOrderManual.getPaymentModel() != null) {
             orders.setPaymentModel(createOrderManual.getPaymentModel());
         }
+        if (StringUtils.isNotEmpty(createOrderManual.getCurrency())) {
+            orders.setCurrency(createOrderManual.getCurrency());
+        }else {
+            orders.setCurrency("USD");
+        }
         if (createOrderManual.getResult() != null) {
             orders.setResult(createOrderManual.getResult());
         }

+ 41 - 3
pdf-office-payment/src/main/java/cn/kdan/cloud/pdf/office/payment/webhook/AppStoreWebhookMonitor.java

@@ -1,8 +1,16 @@
 package cn.kdan.cloud.pdf.office.payment.webhook;
 
+import cn.kdan.cloud.pdf.office.api.payment.appstore.AppStoreReceiptInfo;
+import cn.kdan.cloud.pdf.office.api.payment.constant.AppStoreAPIConstant;
 import cn.kdan.cloud.pdf.office.api.payment.constant.PaddleAPIConstant;
+import cn.kdan.cloud.pdf.office.api.payment.dto.AppStoreOrderSucceedDTO;
+import cn.kdan.cloud.pdf.office.api.payment.dto.EquityVerificationDTO;
+import cn.kdan.cloud.pdf.office.common.enums.ExceptionEnum;
 import cn.kdan.cloud.pdf.office.common.exception.BackendRuntimeException;
+import cn.kdan.cloud.pdf.office.common.utils.JsonUtils;
 import cn.kdan.cloud.pdf.office.payment.error.ErrorMessage;
+import cn.kdan.cloud.pdf.office.payment.properties.AppStoreProperties;
+import cn.kdan.cloud.pdf.office.payment.service.AppStoreService;
 import cn.kdan.cloud.pdf.office.payment.service.AppStoreWebhookService;
 import cn.kdan.cloud.pdf.office.payment.service.PaddleWebhookService;
 import cn.kdan.cloud.pdf.office.payment.utils.JwsUtil;
@@ -21,6 +29,8 @@ import org.springframework.web.bind.annotation.RequestMethod;
 import org.springframework.web.client.RestTemplate;
 
 import java.security.cert.CertificateException;
+import java.util.HashMap;
+import java.util.Map;
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -32,13 +42,15 @@ import java.util.concurrent.TimeUnit;
 @Slf4j
 public class AppStoreWebhookMonitor {
 
-    private final JwsUtil jwsUtil;
-
     private final NotificationDecoder notificationDecoder = new NotificationDecoderImpl();
 
     private final RedissonClient redissonClient;
 
     private final AppStoreWebhookService appStoreWebhookService;
+    private final RestTemplate restTemplate = new RestTemplate();
+
+    private AppStoreProperties properties;
+    private AppStoreService appStoreService;
 
     @RequestMapping(value = "/webhook/subscription", method = RequestMethod.POST)
     public ResponseEntity<String> receiveSubscriptionCallback(@RequestBody(required = false) ResponseBodyV2 responseBodyV2) {
@@ -54,6 +66,14 @@ public class AppStoreWebhookMonitor {
 
             log.info("appstore 接收回调信息jwsRenewalInfoDecodedPayload值:{}", jwsRenewalInfoDecodedPayload);
             log.info("appstore 接收回调信息jwsTransactionDecodedPayload值:{}", jwsTransactionDecodedPayload);
+            //
+            if(jwsRenewalInfoDecodedPayload.getEnvironment().equals("Sandbox")){
+                //如果当前服务不是沙盒环境(正式环境)就把请求转到测试环境
+                if(!properties.getIsSandbox().equals("true")){
+                    sandboxCallBack(responseBodyV2);
+                    return ResponseEntity.status(HttpStatus.OK).build(); // 返回 200 OK
+                }
+            }
             String lockKey = decodedPayload.getNotificationType().toString().concat(decodedPayload.getNotificationUUID()) + "-RedissonLock";
             // 获取 RLock 对象
             RLock rLock = redissonClient.getLock(lockKey);
@@ -103,7 +123,25 @@ public class AppStoreWebhookMonitor {
         return ResponseEntity.ok().build();
     }
 
-
+    public void sandboxCallBack(ResponseBodyV2 responseBodyV2) {
+        // 将 responseBodyV2 转换为 JSON 字符串
+        String jsonBody = JsonUtils.getJsonString(responseBodyV2); // 假设 JsonUtils.getJsonString 可以完成序列化
+        // 创建 HttpEntity,包含 JSON 字符串和请求头
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_JSON);
+        HttpEntity<String> entity = new HttpEntity<>(jsonBody, headers); // getJsonHttpHeaders() 负责设置请求头
+        // 使用 restTemplate 发送 POST 请求并检查响应状态码
+        ResponseEntity<Void> response = restTemplate.exchange(
+                properties.getTestCallBack(),  // URL
+                HttpMethod.POST,            // 请求方法
+                entity,                     // 请求体和头信息
+                Void.class                  // 响应体类型,这里不需要实际的响应内容
+        );
+        if (response.getStatusCode()!= HttpStatus.OK) {
+            log.error("appstore 回调测试环境失败:响应状态码不为 200");
+            throw new BackendRuntimeException(ExceptionEnum.EXCEPTION_MSG_APP_STORE_TRANSACTION_ID_VALIDATION_FAILED);
+        }
+    }
     public static void main(String[] args) {
 //        String url = "https://api.storekit-sandbox.itunes.apple.com/inApps/v1/notifications/test";
         String url = "https://api.storekit-sandbox.itunes.apple.com/inApps/v1/notifications/test";

+ 31 - 0
pdf-office-payment/src/main/java/cn/kdan/cloud/pdf/office/payment/webhook/GoogleWebhookMonitor.java

@@ -2,8 +2,10 @@ package cn.kdan.cloud.pdf.office.payment.webhook;
 
 import cn.kdan.cloud.pdf.office.common.enums.ExceptionEnum;
 import cn.kdan.cloud.pdf.office.common.exception.BackendRuntimeException;
+import cn.kdan.cloud.pdf.office.common.utils.JsonUtils;
 import cn.kdan.cloud.pdf.office.payment.client.GooglePayClient;
 import cn.kdan.cloud.pdf.office.payment.service.GooglePayService;
+import cn.kdan.cloud.pdf.office.payment.webhook.appstore.notification.ResponseBodyV2;
 import cn.kdan.cloud.pdf.office.payment.webhook.google.DeveloperNotification;
 import cn.kdan.cloud.pdf.office.payment.webhook.google.SubscriptionNotification;
 import cn.kdan.cloud.pdf.office.payment.webhook.google.SubscriptionNotifyTypeEnum;
@@ -40,6 +42,7 @@ public class GoogleWebhookMonitor {
 
     private final GooglePayService googlePayService;
     private final GooglePayClient googlePayClient;
+    private final RestTemplate restTemplate = new RestTemplate();
 
     @RequestMapping(value = "/webhook/subscription", method = RequestMethod.POST)
     public ResponseEntity<Void> receiveSubscriptionCallback(@RequestBody(required = false) byte[] body) {
@@ -69,6 +72,10 @@ public class GoogleWebhookMonitor {
             log.info("谷歌 token:{}", purchaseToken);
             // 获取订阅订单信息
             SubscriptionPurchaseV3 subscriptionGoogleOrderV2 = googlePayClient.getSubscription(developerNotification.getPackageName(), purchaseToken);
+            if(subscriptionGoogleOrderV2.getTestPurchase() == null){
+                sandboxCallBack(body);
+                return ResponseEntity.status(HttpStatus.OK).build(); // 返回 200 OK
+            }
             //判断订单状态
             if (!googlePayClient.checkSubscriptionSuccess(subscriptionGoogleOrderV2)) {
                 log.error("谷歌订单验证状态异常:{}", subscriptionGoogleOrderV2);
@@ -112,4 +119,28 @@ public class GoogleWebhookMonitor {
             return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); // 返回 500 错误
         }
     }
+    public void sandboxCallBack(byte[] body) {
+        byte[] requestBody = body;  // 使用传入的字节数组作为请求体
+
+        // 创建 HttpHeaders,设置 Content-Type 为 application/json
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_JSON);
+
+        // 创建 HttpEntity,包含字节数组请求体和请求头
+        HttpEntity<byte[]> entity = new HttpEntity<>(requestBody, headers);
+
+        // 使用 restTemplate 发送 POST 请求并检查响应状态码
+        ResponseEntity<Void> response = restTemplate.exchange(
+                "https://test-pdf-pro.kdan.cn/member-system-payment/google/webhook/subscription",  // URL
+                HttpMethod.POST,               // 请求方法
+                entity,                        // 请求体和头信息
+                Void.class                     // 响应体类型,这里不需要实际的响应内容
+        );
+
+        // 检查响应状态码是否为 200 OK
+        if (response.getStatusCode() != HttpStatus.OK) {
+            log.error("谷歌 回调测试环境失败:响应状态码不为 200");
+            throw new BackendRuntimeException(ExceptionEnum.EXCEPTION_MSG_APP_STORE_TRANSACTION_ID_VALIDATION_FAILED);
+        }
+    }
 }

+ 3 - 0
pdf-office-payment/src/main/java/cn/kdan/cloud/pdf/office/payment/webhook/appstore/notification/JWSRenewalInfoDecodedPayload.java

@@ -23,6 +23,7 @@ public class JWSRenewalInfoDecodedPayload {
   private Long gracePeriodExpiresDate; // timestamp
   private Boolean isInBillingRetryPeriod;
   private String offerIdentifier;
+
   // 1 An introductory offer.
   // 2 A promotional offer.
   // 3 An offer with a subscription offer code.
@@ -34,4 +35,6 @@ public class JWSRenewalInfoDecodedPayload {
   private Integer priceIncreaseStatus;
   private String productId;
   private Long signedDate;// timestamp
+
+  private String environment;
 }

+ 1 - 0
pdf-office-payment/src/main/java/cn/kdan/cloud/pdf/office/payment/webhook/appstore/notification/JWSTransactionDecodedPayload.java

@@ -36,4 +36,5 @@ public class JWSTransactionDecodedPayload {
   private String type;
 
   private String webOrderLineItemId;
+  private String environment;
 }

+ 9 - 0
pdf-office-pdf-website/src/main/java/cn/kdan/cloud/pdf/office/website/service/impl/OrderGiftServiceImpl.java

@@ -189,6 +189,15 @@ public class OrderGiftServiceImpl implements OrderGiftService {
                 userInfoVO = userApi.getByEmail(email).getResult();
             //插永久版的订阅info
             }
+        }else {
+            UserInfoVO user = userApi.getMemberInfoById(userInfoVO.getId()).getResult();
+            user.getSubscriptionInfoList().forEach(userSubscriptionInfoVO -> {
+                if (userSubscriptionInfoVO.getPaymentModel().equals("2")
+                        && userSubscriptionInfoVO.getStatus().equals(PDFOfficeUserSubscriptionStatusEnum.SUBSCRIPTION_IN_PROGRESS.value())) {
+                    //2.如果是永久会员,提示:该用户已经是PDF Reader Pro永久版会员,无需再赠送哦~
+                    throw new BackendRuntimeException(ExceptionEnum.USER_ALREADY_SUBSCRIBED_PACKAGE);
+                }
+            });
         }
         ProductVO product = productApi.getProductByCode("advanced-permanent").getResult();
         UserSubscriptionInfo newInfo = new UserSubscriptionInfo().withId(CommonUtils.generateId())

+ 1 - 1
pdf-office-sso/src/main/java/cn/kdan/cloud/pdf/office/sso/controller/UserCenterController.java

@@ -89,7 +89,7 @@ public class UserCenterController {
         AtomicBoolean isUpgrade = new AtomicBoolean(false);
         user.getSubscriptionInfoList().stream().forEach(item -> {
             item.setPlatforms(convertToLowerCase(item.getPlatforms()));
-            if(item.getCode().contains("upgrade")){
+            if(item.getCode().contains("upgrade")&&item.getCode().contains("subscription")){
                 isUpgrade.set(true);
             }
             // 没有购买显示当前时间

+ 1 - 7
pdf-office-sso/src/main/java/cn/kdan/cloud/pdf/office/sso/controller/UserController.java

@@ -203,15 +203,9 @@ public class UserController {
                 UserInfoVO user = userApi.getMemberInfoById(userId).getResult();
                 AtomicBoolean isUpgrade = new AtomicBoolean(false);
                 List<SimpleUserSubscriptionInfoVO> resultList = user.getSubscriptionInfoList().stream().map(item -> {
-                    if(item.getCode().contains("upgrade")){
+                    if(item.getCode().contains("upgrade")&&item.getCode().contains("subscription")){
                         isUpgrade.set(true);
                     }
-                    //自动续订,显示续订日期
-                    if (!ObjectUtils.isEmpty(item.getEndDate()) && PayTypeEnum.AUTO.value().equals(item.getPayType())) {
-                        LocalDateTime payTime = subscriptionApi.getNextPayTime(item.getUserId(), item.getProductId()).getResult();
-                        Date date = MyDateUtils.localDateTimeToDate(payTime);
-                        item.setEndDate(date);
-                    }
                     item.setPlatforms(convertToLowerCase(item.getPlatforms()));
                     // 没有购买显示当前时间
                     if (ObjectUtils.isEmpty(item.getEndDate())) {

+ 2 - 3
pdf-office-sso/src/main/java/cn/kdan/cloud/pdf/office/sso/service/impl/AuthServiceImpl.java

@@ -483,7 +483,8 @@ public class AuthServiceImpl implements AuthService {
         UserRegisterDTO userRegisterDTO = new UserRegisterDTO();
         userRegisterDTO.setUsername(loginParam.getEmail());
         userRegisterDTO.setAppId(loginParam.getAppId());
-        userRegisterDTO.setModel(loginParam.getModel());
+        String model = StringUtils.isEmpty(loginParam.getModel())?"官网":loginParam.getModel();
+        userRegisterDTO.setModel(model);
         userRegisterDTO.setPlatformType("0");
         if(StringUtils.isNotEmpty(loginParam.getInviteUserId())){
             userRegisterDTO.setInviteUserId(loginParam.getInviteUserId());
@@ -532,8 +533,6 @@ public class AuthServiceImpl implements AuthService {
 
     public void processInvite(String inviteUserId) {
         Integer count = userApi.getInviteNum(inviteUserId).getResult();
-        count += 1;
-
         // 仅在达到特定阈值时发放奖励
         if (count == 15) {
             givePrize(inviteUserId,10,70);