From 37ff60fcfb508f1ac479c6996a06e07fb3e512bb Mon Sep 17 00:00:00 2001 From: xupei Date: Mon, 17 Feb 2025 17:14:30 +0800 Subject: [PATCH] =?UTF-8?q?Stripe=20=E6=B7=BB=E5=8A=A0=E4=B8=80=E6=AC=A1?= =?UTF-8?q?=E8=AE=A2=E9=98=85=E6=9C=8D=E5=8A=A1=EF=BC=88=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E5=85=B3=E9=97=AD=E8=87=AA=E5=8A=A8=E7=BB=AD=E8=AE=A2=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/ai/da/common/utils/DateUtil.java | 3 +- .../com/ai/da/common/utils/SendEmailUtil.java | 4 +- .../da/mapper/primary/entity/OrderInfo.java | 2 + .../model/dto/SubscriptionEmailParamsDTO.java | 2 + .../com/ai/da/service/OrderInfoService.java | 3 +- .../da/service/impl/AffiliateServiceImpl.java | 4 +- .../da/service/impl/OrderInfoServiceImpl.java | 3 +- .../service/impl/PaymentInfoServiceImpl.java | 18 +- .../ai/da/service/impl/StripeServiceImpl.java | 216 ++++++++++++++---- .../resources/application-prod.properties | 3 +- .../mapper/primary/PaymentInfoMapper.xml | 4 +- src/main/resources/payment.properties | 13 +- 12 files changed, 207 insertions(+), 68 deletions(-) diff --git a/src/main/java/com/ai/da/common/utils/DateUtil.java b/src/main/java/com/ai/da/common/utils/DateUtil.java index e386ad54..bacf0983 100644 --- a/src/main/java/com/ai/da/common/utils/DateUtil.java +++ b/src/main/java/com/ai/da/common/utils/DateUtil.java @@ -1,5 +1,6 @@ package com.ai.da.common.utils; +import com.ai.da.common.constant.CommonConstant; import lombok.extern.slf4j.Slf4j; import java.text.ParseException; @@ -96,7 +97,7 @@ public class DateUtil { } public static String changeTimeStampFormat(LocalDateTime localDate){ - return localDate.format(DateTimeFormatter.ofPattern("MMM. dd, yyyy, EEEE", Locale.US)); + return localDate.format(DateTimeFormatter.ofPattern(CommonConstant.TIME_FORMAT_MMM_dd_yyyy_EEEE, Locale.US)); } } 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 8c0db85e..45850236 100644 --- a/src/main/java/com/ai/da/common/utils/SendEmailUtil.java +++ b/src/main/java/com/ai/da/common/utils/SendEmailUtil.java @@ -841,7 +841,7 @@ public class SendEmailUtil { try { String merchantEmail = "kimwong@code-create.com.hk"; String developer = "xupei3360@163.com"; - String[] receiverEmail = {merchantEmail, developer}; + String[] receiverEmail = {/*merchantEmail, */developer}; Credential cred = new Credential(SECRET_ID, SECRET_KEy); // 实例化一个http选项,可选的,没有特殊需求可以跳过 HttpProfile httpProfile = new HttpProfile(); @@ -1008,7 +1008,7 @@ public class SendEmailUtil { req.setFromEmailAddress(SEND_ADDRESS); String merchantEmail = "kimwong@code-create.com.hk"; String developerEmail = "xupei@code-create.com.hk"; - req.setDestination(new String[]{merchantEmail, developerEmail}); + req.setDestination(new String[]{/*merchantEmail,*/ developerEmail}); Template template = new Template(); req.setSubject("New Credit Purchase Order"); template.setTemplateID(CREDITS_PURCHASE_MERCHANT); diff --git a/src/main/java/com/ai/da/mapper/primary/entity/OrderInfo.java b/src/main/java/com/ai/da/mapper/primary/entity/OrderInfo.java index 15ae0cee..dd462038 100644 --- a/src/main/java/com/ai/da/mapper/primary/entity/OrderInfo.java +++ b/src/main/java/com/ai/da/mapper/primary/entity/OrderInfo.java @@ -23,6 +23,8 @@ public class OrderInfo extends BaseEntity{ private String note; + private byte autoRenewal; + private String paymentType;//支付方式 // 可用于标记用户订单是否首次订阅 diff --git a/src/main/java/com/ai/da/model/dto/SubscriptionEmailParamsDTO.java b/src/main/java/com/ai/da/model/dto/SubscriptionEmailParamsDTO.java index 1d6f6fd7..43a2d3af 100644 --- a/src/main/java/com/ai/da/model/dto/SubscriptionEmailParamsDTO.java +++ b/src/main/java/com/ai/da/model/dto/SubscriptionEmailParamsDTO.java @@ -42,6 +42,8 @@ public class SubscriptionEmailParamsDTO { // 订阅开始时间 private String startDate; + private String endDate; + // 下一个支付日期 private String nextPayDate; diff --git a/src/main/java/com/ai/da/service/OrderInfoService.java b/src/main/java/com/ai/da/service/OrderInfoService.java index 19264239..62cbf35a 100644 --- a/src/main/java/com/ai/da/service/OrderInfoService.java +++ b/src/main/java/com/ai/da/service/OrderInfoService.java @@ -15,7 +15,8 @@ public interface OrderInfoService extends IService { OrderInfo createOrderByProductId(Integer productId, String paymentType, HttpServletRequest request); - OrderInfo createOrderByProductId(Integer amount, String paymentType, ProductEnum product, HttpServletRequest request); + OrderInfo createOrderByProductId(Integer amount, String paymentType, ProductEnum product, + HttpServletRequest request, byte autoRenewal); void saveCodeUrl(String orderNo, String codeUrl); 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 509325a0..d62d91f2 100644 --- a/src/main/java/com/ai/da/service/impl/AffiliateServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/AffiliateServiceImpl.java @@ -78,7 +78,7 @@ public class AffiliateServiceImpl extends ServiceImpl queryWrapper = new QueryWrapper<>(); @@ -448,13 +487,14 @@ public class StripeServiceImpl implements StripeService { if (Objects.isNull(subscriptionInfo) || subscriptionInfo.getStatus().equals("incomplete") || subscriptionInfo.getStatus().equals("incomplete_expired")) { - sendEmail(orderNo); + resp = sendEmail(orderNo); }else { // todo 续订失败 应该不会走这里 - sendEmail(subscriptionInfo.getSubscriptionId(), "fail_renewal"); + resp = sendEmail(subscriptionInfo.getSubscriptionId(), "fail_renewal", null); } } } + return resp; } @@ -497,6 +537,60 @@ public class StripeServiceImpl implements StripeService { 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"; + // 获取一天后的时间戳(秒级) + currentPeriodEnd = Instant.now().plus(1, ChronoUnit.DAYS).getEpochSecond(); + }else if (description.equals(ProductEnum.MonthlySubscription.getName()) + && amount.equals(ProductEnum.MonthlySubscription.getPrice() * 100)){ + interval = "month"; + // 获取一天后的时间戳(秒级) + currentPeriodEnd = Instant.now().plus(1, ChronoUnit.MONTHS).getEpochSecond(); + }else if (description.equals(ProductEnum.AnnualSubscription.getName()) + && amount.equals(ProductEnum.AnnualSubscription.getPrice() * 100)){ + interval = "year"; + // 获取一天后的时间戳(秒级) + currentPeriodEnd = Instant.now().plus(1, ChronoUnit.YEARS).getEpochSecond(); + }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); @@ -704,7 +798,8 @@ public class StripeServiceImpl implements StripeService { try { customerId = getCustomer(username, userEmail); SubscriptionCollection list = Subscription.list(SubscriptionListParams.builder() - .setCustomer(customerId).build()); + .setCustomer(customerId).setLimit(20L) + .build()); return list.getData(); } catch (StripeException e) { throw new RuntimeException(e); @@ -832,28 +927,42 @@ public class StripeServiceImpl implements StripeService { // return null; } - public boolean sendEmail(String subscriptionId, String type) { - SubscriptionEmailParamsDTO emailParamsDTO = new SubscriptionEmailParamsDTO(); - QueryWrapper qwSI = new QueryWrapper<>(); - qwSI.eq("subscription_id", subscriptionId); - List subscriptionInfoList = subscriptionInfoMapper.selectList(qwSI); + public boolean sendEmail(String subscriptionId, String type, String orderNo) { SubscriptionInfo subscriptionInfo; - if (subscriptionInfoList.isEmpty()){ - return false; - }else { - List activeSubscriptions = subscriptionInfoList.stream() - .filter(subscription -> "active".equals(subscription.getStatus())) - .collect(Collectors.toList()); - if (activeSubscriptions.isEmpty()){ + 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); + } } - subscriptionInfo = activeSubscriptions.get(0); + }else if (!StringUtil.isNullOrEmpty(orderNo)) { + qwSI.eq("order_no", orderNo); + subscriptionInfo = subscriptionInfoMapper.selectOne(qwSI); + }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); @@ -864,6 +973,7 @@ public class StripeServiceImpl implements StripeService { } if (!type.equals("reminder") && !type.equals("cancel") && paymentInfo.getNotified() == 1){ // 已经邮件通知过,直接返回 + log.info("不发送邮件,原因:【type为:{},order_no为:{},且已经已经进行邮件通知】", type, orderNo); return true; } @@ -872,6 +982,7 @@ public class StripeServiceImpl implements StripeService { 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() + "\""); @@ -880,7 +991,7 @@ public class StripeServiceImpl implements StripeService { 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); + setSubscriptionParams(paymentInfo, subscriptionInfo, orderByOrderNo, emailParamsDTO, language); SendEmailUtil.subscriptionEmailReminder(type, emailParamsDTO, language, account.getUserEmail()); @@ -906,6 +1017,7 @@ public class StripeServiceImpl implements StripeService { 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); @@ -976,7 +1088,7 @@ public class StripeServiceImpl implements StripeService { emailParamsDTO.setCreateDate(String.valueOf(paymentInfo.getCreateTime()).replace("T", " ")); emailParamsDTO.setQuantity(String.valueOf(1)); emailParamsDTO.setTotalFee(paymentInfo.getPayerTotal().toString()); - setSubscriptionParams(paymentInfo, subscriptionInfo, orderByOrderNo, emailParamsDTO); + setSubscriptionParams(paymentInfo, subscriptionInfo, orderByOrderNo, emailParamsDTO, language); // 4、发邮件 SendEmailUtil.subscriptionEmailReminder("fail_renewal", emailParamsDTO, language, account.getUserEmail()); @@ -989,14 +1101,28 @@ public class StripeServiceImpl implements StripeService { return true; } - private void setSubscriptionParams(PaymentInfo paymentInfo, SubscriptionInfo subscriptionInfo, OrderInfo orderByOrderNo, SubscriptionEmailParamsDTO emailParamsDTO) { + 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())); - emailParamsDTO.setNextPayDate(DateUtil.changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy)); + 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)); } @@ -1016,7 +1142,7 @@ public class StripeServiceImpl implements StripeService { List subscriptionInfos = subscriptionInfoMapper.selectList(qw); for (SubscriptionInfo subscriptionInfo : subscriptionInfos) { - boolean b = sendEmail(subscriptionInfo.getSubscriptionId(), "reminder"); + boolean b = sendEmail(subscriptionInfo.getSubscriptionId(), "reminder", null); if (b) log.info("提前7天向用户 {} 发送续订通知邮件", subscriptionInfo.getAccountId()); } } @@ -1040,7 +1166,7 @@ public class StripeServiceImpl implements StripeService { public String createSubscriptionTemp(String name, String email){ Stripe.apiKey = privateKey; try { - OrderInfo orderInfo = orderInfoService.createOrderByProductId(1, PayTypeEnum.STRIPE.getType(), ProductEnum.DailySubscription, null); + OrderInfo orderInfo = orderInfoService.createOrderByProductId(1, PayTypeEnum.STRIPE.getType(), ProductEnum.DailySubscription, null, (byte)0); // String customerId = getCustomer(name, email); String paymentMethodCode = "pm_card_mastercard"; diff --git a/src/main/resources/application-prod.properties b/src/main/resources/application-prod.properties index aee86f9b..27c8d42a 100644 --- a/src/main/resources/application-prod.properties +++ b/src/main/resources/application-prod.properties @@ -16,7 +16,8 @@ spring.security.jwtSecret=JWTSECRET spring.security.jwtTokenHeader=Authorization spring.security.jwtTokenPrefix=Bearer- ## 24Сʱ -spring.security.jwtExpiration=8640000000 +#spring.security.jwtExpiration=8640000000 +spring.security.jwtExpiration=604800000 #spring security权限设置 认证了token还要认证权限 不然报错Full authentication is required to access this resource spring.security.ignorePaths=/,/favicon.ico,/doc.html,/webjars/**,/swagger-resources,/v2/api-docs,\ /api/account/**,/api/element/**,/api/python/**,/api/design/**,/api/history/**,/api/library/**,/api/third/party/**,/api/generate/**,/api/workspace/**,/api/classification/**,\ diff --git a/src/main/resources/mapper/primary/PaymentInfoMapper.xml b/src/main/resources/mapper/primary/PaymentInfoMapper.xml index 4abc6c8d..ce03b6c4 100644 --- a/src/main/resources/mapper/primary/PaymentInfoMapper.xml +++ b/src/main/resources/mapper/primary/PaymentInfoMapper.xml @@ -169,9 +169,9 @@ type, content, notified, payment_method, last4, hosted_invoice_url, country, city, ip_address, create_time) VALUES (#{paymentInfo.orderNo}, #{paymentInfo.transactionId}, #{paymentInfo.paymentType}, - #{paymentInfo.tradeState}, #{paymentInfo.payerTotal}, #{paymentInfo.content},#{paymentInfo.type}, + #{paymentInfo.tradeState}, #{paymentInfo.payerTotal},#{paymentInfo.type}, #{paymentInfo.content}, #{paymentInfo.notified},#{paymentInfo.paymentMethod}, #{paymentInfo.last4},#{paymentInfo.hostedInvoiceUrl}, - #{paymentInfo.ipAddress},#{paymentInfo.country},#{paymentInfo.city},#{paymentInfo.createTime}); + #{paymentInfo.country},#{paymentInfo.city},#{paymentInfo.ipAddress},#{paymentInfo.createTime}); diff --git a/src/main/resources/payment.properties b/src/main/resources/payment.properties index 6bdc0f6b..2a4e8978 100644 --- a/src/main/resources/payment.properties +++ b/src/main/resources/payment.properties @@ -28,16 +28,19 @@ paypal.webhook_id=1D107312EX592781K # developer #stripe.private-key=sk_test_51P4ZZL02n1TEydyN8qQHjOA9imsFU7Oxs2HMHGy2urHnnQgSHnZuu5vVP6pKhEACwUpsKNyrbZpdcg5TJWJLRHcY008dEO1fn2 -#stripe.webhook-sign-secret=whsec_e0dBiJngx6qqgJj6yPyJ2A9ouh1Cjv5w +##stripe.webhook-sign-secret=whsec_e0dBiJngx6qqgJj6yPyJ2A9ouh1Cjv5w #stripe.webhook-sign-secret=whsec_TJcMSnAkh4uktrNY1M6Iy8XaVze4Rzqm # kim - test -#stripe.private-key=sk_test_51LwPrxH7nPZ8bkrNj67TFD7sxucaTANs1lf0KGSu1QSJfxYXcnigq2wTaZyZzST7y0fMbhhvaJZ4LjjFhr95M83a00eXrmOTL0 +stripe.private-key=sk_test_51LwPrxH7nPZ8bkrNj67TFD7sxucaTANs1lf0KGSu1QSJfxYXcnigq2wTaZyZzST7y0fMbhhvaJZ4LjjFhr95M83a00eXrmOTL0 +# prod 端点 #stripe.webhook-sign-secret=whsec_GoyVEAaBtuGD5Rt55z83JnPnLDAZTN3u +# local 端点 +stripe.webhook-sign-secret=whsec_NvwM3hDQiN5GXclYOYekE9IKHLjmROF8 # kim - live -stripe.private-key=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 -# 正式支付环境下生产分支端点 -stripe.webhook-sign-secret=whsec_hhGDgdelQRHSg4LmChtQe41crj41eb11 +#stripe.private-key=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 +## 正式支付环境下生产分支端点 +#stripe.webhook-sign-secret=whsec_hhGDgdelQRHSg4LmChtQe41crj41eb11 # 正式支付环境下测试分支端点 #stripe.webhook-sign-secret=whsec_cFUtjUOo8wnrIKZmt4GNvt7ZY1bOfrYr