From 8d27b5b51ebb1172cefd29293fe115675eb1b09a Mon Sep 17 00:00:00 2001 From: xupei Date: Mon, 18 Nov 2024 16:20:25 +0800 Subject: [PATCH 1/8] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=8A=9F=E8=83=BD=20--?= =?UTF-8?q?=20=E4=BA=A7=E5=93=81=E8=AE=A2=E9=98=85=20=E5=B9=B4=E5=BA=A6/?= =?UTF-8?q?=E6=9C=88=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ai/da/common/constant/CommonConstant.java | 4 + .../com/ai/da/common/enums/ProductEnum.java | 25 + .../com/ai/da/common/task/AliPayTask.java | 42 -- .../com/ai/da/common/task/GenerateTask.java | 21 - .../com/ai/da/common/task/PaymentTask.java | 96 +++ .../com/ai/da/common/task/PaypalTask.java | 42 -- .../com/ai/da/common/task/StripeTask.java | 40 -- .../com/ai/da/common/utils/SendEmailUtil.java | 92 +++ .../ai/da/controller/StripeController.java | 32 +- .../primary/SubscriptionInfoMapper.java | 8 + .../da/mapper/primary/entity/PaymentInfo.java | 10 +- .../primary/entity/SubscriptionInfo.java | 43 ++ .../ai/da/model/dto/ProductPurchaseDTO.java | 32 + .../model/dto/SubscriptionEmailParamsDTO.java | 47 ++ .../com/ai/da/service/OrderInfoService.java | 6 + .../com/ai/da/service/PaymentInfoService.java | 7 +- .../java/com/ai/da/service/StripeService.java | 24 +- .../da/service/impl/OrderInfoServiceImpl.java | 74 +- .../service/impl/PaymentInfoServiceImpl.java | 61 +- .../ai/da/service/impl/StripeServiceImpl.java | 633 ++++++++++++++---- src/main/resources/payment.properties | 8 +- 21 files changed, 1029 insertions(+), 318 deletions(-) create mode 100644 src/main/java/com/ai/da/common/enums/ProductEnum.java delete mode 100644 src/main/java/com/ai/da/common/task/AliPayTask.java delete mode 100644 src/main/java/com/ai/da/common/task/GenerateTask.java create mode 100644 src/main/java/com/ai/da/common/task/PaymentTask.java delete mode 100644 src/main/java/com/ai/da/common/task/PaypalTask.java delete mode 100644 src/main/java/com/ai/da/common/task/StripeTask.java create mode 100644 src/main/java/com/ai/da/mapper/primary/SubscriptionInfoMapper.java create mode 100644 src/main/java/com/ai/da/mapper/primary/entity/SubscriptionInfo.java create mode 100644 src/main/java/com/ai/da/model/dto/ProductPurchaseDTO.java create mode 100644 src/main/java/com/ai/da/model/dto/SubscriptionEmailParamsDTO.java diff --git a/src/main/java/com/ai/da/common/constant/CommonConstant.java b/src/main/java/com/ai/da/common/constant/CommonConstant.java index 71f6c86b..e495ee26 100644 --- a/src/main/java/com/ai/da/common/constant/CommonConstant.java +++ b/src/main/java/com/ai/da/common/constant/CommonConstant.java @@ -75,5 +75,9 @@ public class CommonConstant { public static final String PORTFOLIO_DELETED_CN = "作品已删除"; + public static final String TIME_FORMAT_MMM_dd_yyyy_EEEE = "MMM. dd, yyyy, EEEE"; + + public static final String TIME_FORMAT_MMM_dd_yyyy = "MMM. dd, yyyy"; + } diff --git a/src/main/java/com/ai/da/common/enums/ProductEnum.java b/src/main/java/com/ai/da/common/enums/ProductEnum.java new file mode 100644 index 00000000..11237ffe --- /dev/null +++ b/src/main/java/com/ai/da/common/enums/ProductEnum.java @@ -0,0 +1,25 @@ +package com.ai.da.common.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum ProductEnum { + // 积分购买 + CreditsProduct("AiDA credits purchase", 6L), + // 年度订阅 + AnnualSubscription("AiDA Annual Subscription", 5000L), + // 月度订阅 + MonthlySubscription("AiDA Monthly Subscription", 500L), + // 测试 + DailySubscription("AiDA Daily Subscription", 5L), + ; + + /** + * 类型 + */ + private final String name; + + private final Long price; +} diff --git a/src/main/java/com/ai/da/common/task/AliPayTask.java b/src/main/java/com/ai/da/common/task/AliPayTask.java deleted file mode 100644 index 670a417c..00000000 --- a/src/main/java/com/ai/da/common/task/AliPayTask.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.ai.da.common.task; - -import com.ai.da.mapper.primary.entity.OrderInfo; -import com.ai.da.common.enums.PayTypeEnum; -import com.ai.da.service.AliPayService; -import com.ai.da.service.OrderInfoService; -import lombok.extern.slf4j.Slf4j; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Component; - -import javax.annotation.Resource; -import java.util.List; - -@Slf4j -@Component -public class AliPayTask { - - @Resource - private OrderInfoService orderInfoService; - - @Resource - private AliPayService aliPayService; - - /** - * 从第0秒开始每隔30秒执行1次,查询创建超过5分钟,并且未支付的订单 - */ -// @Scheduled(cron = "0/30 * * * * ?") - public void orderConfirm(){ - -// log.info("Alipay orderConfirm 被执行......"); - - List orderInfoList = orderInfoService.getNoPayOrderByDuration(5, PayTypeEnum.ALIPAY.getType()); - - for (OrderInfo orderInfo : orderInfoList) { - String orderNo = orderInfo.getOrderNo(); - log.warn("超时订单 ===> {}", orderNo); - - //核实订单状态:调用支付宝查单接口 - aliPayService.checkOrderStatus(orderNo); - } - } -} diff --git a/src/main/java/com/ai/da/common/task/GenerateTask.java b/src/main/java/com/ai/da/common/task/GenerateTask.java deleted file mode 100644 index 038976d0..00000000 --- a/src/main/java/com/ai/da/common/task/GenerateTask.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.ai.da.common.task; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Component; - -@Component -@Slf4j -public class GenerateTask { - -// @Scheduled(cron = "0 0 */1 * * ?") - public void generateScheduled(){ - log.info("测试定时器:generate"); - - try{ - - }catch(Exception e){ - - } - } -} diff --git a/src/main/java/com/ai/da/common/task/PaymentTask.java b/src/main/java/com/ai/da/common/task/PaymentTask.java new file mode 100644 index 00000000..656e3a23 --- /dev/null +++ b/src/main/java/com/ai/da/common/task/PaymentTask.java @@ -0,0 +1,96 @@ +package com.ai.da.common.task; + +import com.ai.da.common.enums.PayTypeEnum; +import com.ai.da.mapper.primary.entity.OrderInfo; +import com.ai.da.service.AliPayService; +import com.ai.da.service.OrderInfoService; +import com.ai.da.service.PayPalCheckoutService; +import com.ai.da.service.StripeService; +import com.paypal.http.exceptions.SerializeException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.List; + +@Slf4j +@Component +public class PaymentTask { + + @Resource + private OrderInfoService orderInfoService; + + @Resource + private StripeService stripeService; + +// @Scheduled(cron = "0/30 * * * * ?") + public void orderConfirmForStripe() throws SerializeException { + + // 查看超过30分钟以上仍未支付的订单 置为超时订单 + List orderInfoList = orderInfoService.getNoPayOrderByDuration(30, PayTypeEnum.STRIPE.getType()); + + for (OrderInfo orderInfo : orderInfoList) { + String orderNo = orderInfo.getOrderNo(); + log.warn("超时订单 ===> {}", orderNo); + + //核实订单状态:调用支付宝查单接口 + stripeService.checkOrderStatus(orderNo); + + } + } + + @Resource + private PayPalCheckoutService payPalCheckoutService; + + // @Scheduled(cron = "0/30 * * * * ?") + public void orderConfirmForPaypal() throws SerializeException { + +// log.info("PayPal orderConfirm 被执行......"); + + List orderInfoList = orderInfoService.getNoPayOrderByDuration(30, PayTypeEnum.PAYPAL.getType()); + + for (OrderInfo orderInfo : orderInfoList) { + String orderNo = orderInfo.getOrderNo(); + log.warn("超时订单 ===> {}", orderNo); + + //核实订单状态:调用支付宝查单接口 + payPalCheckoutService.checkOrderStatus(orderNo); + + } + } + + @Resource + private AliPayService aliPayService; + + /** + * 从第0秒开始每隔30秒执行1次,查询创建超过5分钟,并且未支付的订单 + */ +// @Scheduled(cron = "0/30 * * * * ?") + public void orderConfirmForAlipay(){ +/* + log.info("Alipay orderConfirm 被执行......"); + + List orderInfoList = orderInfoService.getNoPayOrderByDuration(5, PayTypeEnum.ALIPAY.getType()); + + for (OrderInfo orderInfo : orderInfoList) { + String orderNo = orderInfo.getOrderNo(); + log.warn("超时订单 ===> {}", orderNo); + + //核实订单状态:调用支付宝查单接口 + aliPayService.checkOrderStatus(orderNo); + }*/ + } + + // 提前7天向用户发送提醒邮件,每天早上8点执行 + @Scheduled(cron = "0 0 8 * * ?") + public void subscriptionReminder(){ + stripeService.subscriptionReminder(); + } + + // 每天凌晨检查subscription中有哪些已过期,更新状态 + @Scheduled(cron = "0 0 0 * * ?") + public void checkSubscriptionExpiration(){ + stripeService.checkSubscriptionExpiration(); + } +} diff --git a/src/main/java/com/ai/da/common/task/PaypalTask.java b/src/main/java/com/ai/da/common/task/PaypalTask.java deleted file mode 100644 index fa069971..00000000 --- a/src/main/java/com/ai/da/common/task/PaypalTask.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.ai.da.common.task; - -import com.ai.da.common.enums.PayTypeEnum; -import com.ai.da.mapper.primary.entity.OrderInfo; -import com.ai.da.service.OrderInfoService; -import com.ai.da.service.PayPalCheckoutService; -import com.paypal.http.exceptions.SerializeException; -import lombok.extern.slf4j.Slf4j; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Component; -import org.springframework.stereotype.Service; - -import javax.annotation.Resource; -import java.util.List; - -@Slf4j -@Component -public class PaypalTask { - - @Resource - private OrderInfoService orderInfoService; - - @Resource - private PayPalCheckoutService payPalCheckoutService; - -// @Scheduled(cron = "0/30 * * * * ?") - public void orderConfirm() throws SerializeException { - -// log.info("PayPal orderConfirm 被执行......"); - - List orderInfoList = orderInfoService.getNoPayOrderByDuration(30, PayTypeEnum.PAYPAL.getType()); - - for (OrderInfo orderInfo : orderInfoList) { - String orderNo = orderInfo.getOrderNo(); - log.warn("超时订单 ===> {}", orderNo); - - //核实订单状态:调用支付宝查单接口 - payPalCheckoutService.checkOrderStatus(orderNo); - - } - } -} diff --git a/src/main/java/com/ai/da/common/task/StripeTask.java b/src/main/java/com/ai/da/common/task/StripeTask.java deleted file mode 100644 index d4a6fbaf..00000000 --- a/src/main/java/com/ai/da/common/task/StripeTask.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.ai.da.common.task; - -import com.ai.da.common.enums.PayTypeEnum; -import com.ai.da.mapper.primary.entity.OrderInfo; -import com.ai.da.service.OrderInfoService; -import com.ai.da.service.StripeService; -import com.paypal.http.exceptions.SerializeException; -import lombok.extern.slf4j.Slf4j; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Component; - -import javax.annotation.Resource; -import java.util.List; - -@Slf4j -@Component -public class StripeTask { - - @Resource - private OrderInfoService orderInfoService; - - @Resource - private StripeService stripeService; - -// @Scheduled(cron = "0/30 * * * * ?") - public void orderConfirm() throws SerializeException { - - // 查看超过30分钟以上仍未支付的订单 置为超时订单 - List orderInfoList = orderInfoService.getNoPayOrderByDuration(30, PayTypeEnum.STRIPE.getType()); - - for (OrderInfo orderInfo : orderInfoList) { - String orderNo = orderInfo.getOrderNo(); - log.warn("超时订单 ===> {}", orderNo); - - //核实订单状态:调用支付宝查单接口 - stripeService.checkOrderStatus(orderNo); - - } - } -} 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 a89115f8..d1a30ae1 100644 --- a/src/main/java/com/ai/da/common/utils/SendEmailUtil.java +++ b/src/main/java/com/ai/da/common/utils/SendEmailUtil.java @@ -2,8 +2,10 @@ package com.ai.da.common.utils; import com.ai.da.mapper.primary.entity.Account; import com.ai.da.mapper.primary.entity.TrialOrder; +import com.ai.da.model.dto.SubscriptionEmailParamsDTO; import com.alibaba.fastjson.JSONObject; import com.ai.da.common.config.exception.BusinessException; +import com.alibaba.fastjson2.JSON; import com.tencentcloudapi.common.Credential; import com.tencentcloudapi.common.exception.TencentCloudSDKException; import com.tencentcloudapi.common.profile.ClientProfile; @@ -767,4 +769,94 @@ public class SendEmailUtil { throw new BusinessException("failed.to.send.mail"); } } + + private final static Long CANCEL_MERCHANT_EN = 130720L; + private final static Long NEW_MERCHANT_EN = 130721L; + private final static Long NEW_USER_EN = 130722L; + private final static Long NEW_USER_CN = 130723L; + private final static Long RENEWAL_MERCHANT_EN = 130724L; + private final static Long RENEWAL_USER_EN = 130725L; + private final static Long RENEWAL_USER_CN = 130726L; + private final static Long RENEWAL_REMINDER_USER_EN = 130727L; + private final static Long RENEWAL_REMINDER_USER_CN = 130728L; + + public static void subscriptionEmailReminder(String type, SubscriptionEmailParamsDTO subscriptionEmailParamsDTO, String language, String receiverAddress){ + try{ + String kimEmail = "kimwong@code-create.com.hk"; + 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 user = new SendEmailRequest(); + user.setFromEmailAddress(SEND_ADDRESS); + user.setDestination(new String[]{receiverAddress}); + SendEmailRequest merchant = new SendEmailRequest(); + merchant.setFromEmailAddress(SEND_ADDRESS); + merchant.setDestination(new String[]{kimEmail}); + Template templateUser = new Template(); + Template templateMerchant = new Template(); + switch (type) { + case "cancel": + merchant.setSubject("[Code-Create] Subscription Cancelled"); + templateMerchant.setTemplateID(CANCEL_MERCHANT_EN); + break; + case "new": + merchant.setSubject("[Code-Create] New Order(" + subscriptionEmailParamsDTO.getOrderId() + ")"); + templateMerchant.setTemplateID(NEW_MERCHANT_EN); + if (language.equals("ENGLISH")){ + user.setSubject("[Code-Create] You have successfully subscribed to AiDA"); + templateUser.setTemplateID(NEW_USER_EN); + }else { + user.setSubject("[Code-Create] 您已成功订阅AiDA"); + templateUser.setTemplateID(NEW_USER_CN); + } + break; + case "renewal": + merchant.setSubject("[Code-Create] New subscription renewal order (" + subscriptionEmailParamsDTO.getOrderId() + ")"); + templateMerchant.setTemplateID(RENEWAL_MERCHANT_EN); + if (language.equals("ENGLISH")){ + user.setSubject("[Code-Create] AiDA Renewal Successful"); + templateUser.setTemplateID(RENEWAL_USER_EN); + }else { + user.setSubject("[Code-Create] AiDA续订成功"); + templateUser.setTemplateID(RENEWAL_USER_CN); + } + break; + case "reminder": + if (language.equals("ENGLISH")){ + user.setSubject("[Code-Create] AiDA Subscription Renewal Reminder"); + templateUser.setTemplateID(RENEWAL_REMINDER_USER_EN); + }else { + user.setSubject("[Code-Create] AiDA续订提醒"); + templateUser.setTemplateID(RENEWAL_REMINDER_USER_CN); + } + break; + default: + log.error("unknown subscription email type"); +// throw new BusinessException("unknown subscription email type"); + } + + templateUser.setTemplateData(JSON.toJSONString(subscriptionEmailParamsDTO)); + user.setTemplate(templateUser); + + templateMerchant.setTemplateData(JSON.toJSONString(subscriptionEmailParamsDTO)); + merchant.setTemplate(templateMerchant); + + // 返回的resp是一个SendEmailResponse的实例,与请求对象对应 + SendEmailResponse respUser = client.SendEmail(user); + // todo 暂时先不发商家邮件 +// SendEmailResponse respMerchant = client.SendEmail(merchant); + log.info("邮件发送结果toUser###{}", SendEmailResponse.toJsonString(respUser)); +// log.info("邮件发送结果toMerchant###{}", SendEmailResponse.toJsonString(respMerchant)); + } catch (TencentCloudSDKException e) { + log.info("邮件发送失败###{}", e.toString()); + throw new BusinessException("failed.to.send.mail"); + } + } } diff --git a/src/main/java/com/ai/da/controller/StripeController.java b/src/main/java/com/ai/da/controller/StripeController.java index 70430fab..eac49851 100644 --- a/src/main/java/com/ai/da/controller/StripeController.java +++ b/src/main/java/com/ai/da/controller/StripeController.java @@ -1,17 +1,21 @@ package com.ai.da.controller; import com.ai.da.common.response.Response; +import com.ai.da.model.dto.ProductPurchaseDTO; import com.ai.da.service.StripeService; import com.paypal.http.HttpResponse; import com.paypal.payments.Refund; +import com.stripe.exception.StripeException; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; +import org.simpleframework.xml.core.Validate; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Api(tags = "Stripe模块") @@ -24,19 +28,19 @@ public class StripeController { private StripeService stripeService; @ApiOperation("创建支付链接") - @PostMapping("/createOrder/{amount}") - public Response pay(@PathVariable Integer amount, @RequestParam String returnUrl) { - return Response.success(stripeService.pay(amount, returnUrl)); + @PostMapping("/createOrder") + public Response pay(@Validate @RequestBody ProductPurchaseDTO productPurchaseDTO) { + return Response.success(stripeService.pay(productPurchaseDTO)); } @ApiOperation("支付通知") @PostMapping("/trade/notify") - public Response callback(HttpServletRequest request) throws ServletException, IOException { + public void callback(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Boolean result = stripeService.notify(request); if (result){ - return Response.success(200,"success"); + response.setStatus(HttpServletResponse.SC_OK); }else { - return Response.fail(400,"failure"); + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); } } @@ -51,4 +55,20 @@ public class StripeController { } } + @ApiOperation("获取订阅") + @PostMapping("/getSubscription") + public void getSubscription() { + try { + stripeService.getSubscription("xp", "xupei3360@163.com"); + } catch (StripeException e) { + throw new RuntimeException(e); + } + } + + @ApiOperation("取消订阅") + @PostMapping("/cancelSubscription") + public Response cancelSubscription(@RequestParam String subscriptionId) { + stripeService.cancelSubscription(subscriptionId); + return Response.success("success"); + } } diff --git a/src/main/java/com/ai/da/mapper/primary/SubscriptionInfoMapper.java b/src/main/java/com/ai/da/mapper/primary/SubscriptionInfoMapper.java new file mode 100644 index 00000000..8a45bb7e --- /dev/null +++ b/src/main/java/com/ai/da/mapper/primary/SubscriptionInfoMapper.java @@ -0,0 +1,8 @@ +package com.ai.da.mapper.primary; + +import com.ai.da.mapper.primary.entity.SubscriptionInfo; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +public interface SubscriptionInfoMapper extends BaseMapper { + +} diff --git a/src/main/java/com/ai/da/mapper/primary/entity/PaymentInfo.java b/src/main/java/com/ai/da/mapper/primary/entity/PaymentInfo.java index c3876106..3806d888 100644 --- a/src/main/java/com/ai/da/mapper/primary/entity/PaymentInfo.java +++ b/src/main/java/com/ai/da/mapper/primary/entity/PaymentInfo.java @@ -2,7 +2,9 @@ package com.ai.da.mapper.primary.entity; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; +import lombok.EqualsAndHashCode; +@EqualsAndHashCode(callSuper = true) @Data @TableName("t_payment_info") public class PaymentInfo extends BaseEntity{ @@ -13,11 +15,15 @@ public class PaymentInfo extends BaseEntity{ private String paymentType;//支付类型 - private String tradeType;//交易类型 - private String tradeState;//交易状态 private Float payerTotal;//支付金额(元) private String content;//通知参数 + + // 支付类型 new || renewal + private String type; + + // 当前支付是否已邮件通知 0 || 1 + private Integer notified; } diff --git a/src/main/java/com/ai/da/mapper/primary/entity/SubscriptionInfo.java b/src/main/java/com/ai/da/mapper/primary/entity/SubscriptionInfo.java new file mode 100644 index 00000000..2b61cf48 --- /dev/null +++ b/src/main/java/com/ai/da/mapper/primary/entity/SubscriptionInfo.java @@ -0,0 +1,43 @@ +package com.ai.da.mapper.primary.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +@Data +@TableName("t_subscription_info") +public class SubscriptionInfo extends BaseEntity{ + + private Long accountId; + + private String orderNo; + + // stripe || paypal 平台生成的id + private String subscriptionId; + + // month || year + private String type; + + // active || expired + private String status = "active"; + + // 是否自动续订 + private byte autoRenewal = (byte)1; + + // 支付方式 + private String paymentMethod; + + // 如果是用卡支付,可以看到银行卡最后四位 + private String last4; + + // 续订的下一个付款日 + private String nextPayDate; + + // 当前订阅订单有效期开始时间 + private Long currentPeriodStart; + + // 当前订阅订单有效期结束时间 + private Long currentPeriodEnd; + +} diff --git a/src/main/java/com/ai/da/model/dto/ProductPurchaseDTO.java b/src/main/java/com/ai/da/model/dto/ProductPurchaseDTO.java new file mode 100644 index 00000000..ce1fb595 --- /dev/null +++ b/src/main/java/com/ai/da/model/dto/ProductPurchaseDTO.java @@ -0,0 +1,32 @@ +package com.ai.da.model.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotBlank; + +@Data +@ApiModel("购买产品DTO") +public class ProductPurchaseDTO { + + @ApiModelProperty("购买数量") + private int quantity; + + // http://example.com + @NotBlank(message = "return url cannot be empty") + @ApiModelProperty("购买完成后返回页面地址") + private String returnUrl; + + @NotBlank(message = "product name cannot be empty") + @ApiModelProperty("产品名 CreditsPurchase || Subscription") + private String productName; + + @ApiModelProperty("Month || Year") + private String subscribeType; + + @ApiModelProperty("是否自动续订 one_time || recurring") + private Boolean autoRenewal; + + private String refId; +} diff --git a/src/main/java/com/ai/da/model/dto/SubscriptionEmailParamsDTO.java b/src/main/java/com/ai/da/model/dto/SubscriptionEmailParamsDTO.java new file mode 100644 index 00000000..0020582f --- /dev/null +++ b/src/main/java/com/ai/da/model/dto/SubscriptionEmailParamsDTO.java @@ -0,0 +1,47 @@ +package com.ai.da.model.dto; + +import lombok.Data; + +@Data +public class SubscriptionEmailParamsDTO { + // 用户名 + private String username; + + // t_payment_info id(每次支付对于用户来说是一笔新订单) + private String orderId; + + // 订单支付创建日期 + private String createDate; + + // 购买数量 + private String quantity; + + // 费用 + private String totalFee; + + // 当前订阅开始时间 + private String lastOrderDate; + + // 当前订阅结束时间 + private String endOfPrepaidTerm; + + // 付款方式 + private String paymentMethod; + + // 订阅Id + private String subscriptionId; + + // 订阅方式 + private String subscriptionType; + + // 订阅开始时间 + private String startDate; + + // 下一个支付日期 + private String nextPayDate; + + // 下次付款时间(reminder) + private String renewalTime; + + +} diff --git a/src/main/java/com/ai/da/service/OrderInfoService.java b/src/main/java/com/ai/da/service/OrderInfoService.java index 35da4881..a420fe70 100644 --- a/src/main/java/com/ai/da/service/OrderInfoService.java +++ b/src/main/java/com/ai/da/service/OrderInfoService.java @@ -2,6 +2,7 @@ package com.ai.da.service; import com.ai.da.common.enums.OrderStatusEnum; +import com.ai.da.common.enums.ProductEnum; import com.ai.da.common.response.PageBaseResponse; import com.ai.da.mapper.primary.entity.OrderInfo; import com.ai.da.model.dto.QueryPageByTimeDTO; @@ -13,6 +14,8 @@ public interface OrderInfoService extends IService { OrderInfo createOrderByProductId(Integer productId, String paymentType); + OrderInfo createOrderByProductId(Integer amount, String paymentType, ProductEnum product); + void saveCodeUrl(String orderNo, String codeUrl); List listOrderByCreateTimeDesc(); @@ -28,4 +31,7 @@ public interface OrderInfoService extends IService { PageBaseResponse getOrderByPage(QueryPageByTimeDTO queryPageByTimeDTO); void updateOrderNoById(Long id, String orderNo); + + void updateTotalFeeByOrderNo(String orderNo); + } diff --git a/src/main/java/com/ai/da/service/PaymentInfoService.java b/src/main/java/com/ai/da/service/PaymentInfoService.java index 74492683..6d3d2d5e 100644 --- a/src/main/java/com/ai/da/service/PaymentInfoService.java +++ b/src/main/java/com/ai/da/service/PaymentInfoService.java @@ -2,12 +2,13 @@ package com.ai.da.service; import com.ai.da.mapper.primary.entity.PaymentInfo; import com.ai.da.model.dto.AlipayHKCallbackDTO; +import com.baomidou.mybatisplus.extension.service.IService; import com.paypal.orders.Order; -import com.stripe.model.checkout.Session; +import com.stripe.model.Invoice; import java.util.Map; -public interface PaymentInfoService { +public interface PaymentInfoService extends IService { void createPaymentInfo(String plainText); @@ -17,7 +18,7 @@ public interface PaymentInfoService { void createPaymentInfoForAliPayHK(AlipayHKCallbackDTO alipayHKCallbackDTO); - void createPaymentInfoForStripe(Session session); + String createPaymentInfoForStripe(Invoice invoice); PaymentInfo getPaymentInfoByOrderId(String orderId); diff --git a/src/main/java/com/ai/da/service/StripeService.java b/src/main/java/com/ai/da/service/StripeService.java index 98709597..b517b325 100644 --- a/src/main/java/com/ai/da/service/StripeService.java +++ b/src/main/java/com/ai/da/service/StripeService.java @@ -1,15 +1,35 @@ package com.ai.da.service; +import com.ai.da.model.dto.ProductPurchaseDTO; +import com.stripe.exception.StripeException; +import com.stripe.model.Subscription; + import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import java.util.List; +import java.util.Map; public interface StripeService { - String pay(Integer quantity, String returnUrl); + String pay(ProductPurchaseDTO productPurchaseDTO); Boolean notify(HttpServletRequest request); String refund(String amount, String orderId, String reason); void checkOrderStatus(String orderNo); + + List getSubscription(String name, String userEmail) throws StripeException; + + void cancelSubscription(String orderNo); + + Map getPaymentMethod(String paymentMethodId); + + /*void updateSubscription(String subscriptionId); + + void resume(String subscriptionId);*/ + + void subscriptionReminder(); + + void checkSubscriptionExpiration(); + } diff --git a/src/main/java/com/ai/da/service/impl/OrderInfoServiceImpl.java b/src/main/java/com/ai/da/service/impl/OrderInfoServiceImpl.java index 416d84d2..ae12e612 100644 --- a/src/main/java/com/ai/da/service/impl/OrderInfoServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/OrderInfoServiceImpl.java @@ -4,15 +4,19 @@ package com.ai.da.service.impl; import com.ai.da.common.context.UserContext; import com.ai.da.common.enums.CreditsEventsEnum; import com.ai.da.common.enums.OrderStatusEnum; +import com.ai.da.common.enums.ProductEnum; import com.ai.da.common.response.PageBaseResponse; import com.ai.da.common.utils.OrderNoUtils; import com.ai.da.mapper.primary.OrderInfoMapper; +import com.ai.da.mapper.primary.PaymentInfoMapper; import com.ai.da.mapper.primary.ProductMapper; import com.ai.da.mapper.primary.entity.OrderInfo; +import com.ai.da.mapper.primary.entity.PaymentInfo; import com.ai.da.model.dto.QueryPageByTimeDTO; import com.ai.da.model.vo.AuthPrincipalVo; import com.ai.da.service.OrderInfoService; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import io.netty.util.internal.StringUtil; @@ -34,9 +38,13 @@ public class OrderInfoServiceImpl extends ServiceImpl queryWrapper = new QueryWrapper<>(); queryWrapper.eq("order_no", orderNo); OrderInfo orderInfo = baseMapper.selectOne(queryWrapper); - if(orderInfo == null){ + if (orderInfo == null) { return null; } return orderInfo.getOrderStatus(); @@ -129,6 +169,7 @@ public class OrderInfoServiceImpl extends ServiceImpl getOrderByPage(QueryPageByTimeDTO queryPageByTimeDTO){ + public PageBaseResponse getOrderByPage(QueryPageByTimeDTO queryPageByTimeDTO) { QueryWrapper qw = new QueryWrapper<>(); - qw.eq("account_id",UserContext.getUserHolder().getId()); + qw.eq("account_id", UserContext.getUserHolder().getId()); String startTime = queryPageByTimeDTO.getStartTime(); String endTime = queryPageByTimeDTO.getEndTime(); - if (StringUtil.isNullOrEmpty(startTime)){ + if (StringUtil.isNullOrEmpty(startTime)) { startTime = "2024-02-01 00:00:00"; } - if (StringUtil.isNullOrEmpty(endTime)){ + if (StringUtil.isNullOrEmpty(endTime)) { LocalDateTime now = LocalDateTime.now(); DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); endTime = now.format(dateTimeFormatter); @@ -206,11 +249,28 @@ public class OrderInfoServiceImpl extends ServiceImpl qw = new QueryWrapper<>(); + qw.eq("order_no", orderNo); + List paymentInfos = paymentInfoMapper.selectList(qw); + Float sum = paymentInfos.stream() + .map(PaymentInfo::getPayerTotal) + .reduce(0f, Float::sum); + + baseMapper.update( + new OrderInfo(), + new UpdateWrapper() + .eq("order_no", orderNo) + .set("total_fee", sum) + ); + } + } diff --git a/src/main/java/com/ai/da/service/impl/PaymentInfoServiceImpl.java b/src/main/java/com/ai/da/service/impl/PaymentInfoServiceImpl.java index 2a80a4f5..035e1135 100644 --- a/src/main/java/com/ai/da/service/impl/PaymentInfoServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/PaymentInfoServiceImpl.java @@ -9,14 +9,20 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.google.gson.Gson; import com.paypal.orders.Order; +import com.stripe.Stripe; +import com.stripe.exception.StripeException; +import com.stripe.model.Invoice; +import com.stripe.model.Subscription; import com.stripe.model.checkout.Session; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.HashMap; import java.util.Map; +import java.util.Objects; @Service @Slf4j @@ -50,7 +56,6 @@ public class PaymentInfoServiceImpl extends ServiceImpl qw = new QueryWrapper<>(); + qw.eq("transaction_id", invoiceId); + PaymentInfo paymentInfo = baseMapper.selectOne(qw); + // 判断当前支付是否已经被记录,确保同一个支付不会被重复记录 + if (Objects.isNull(paymentInfo)){ + String orderNo; + String subscriptionId = invoice.getSubscription(); + try { + // 从subscription中获取orderNo + orderNo = Subscription.retrieve(subscriptionId).getDescription().replace("AiDA - ", ""); + } catch (StripeException e) { + throw new RuntimeException(e); + } + String status = invoice.getStatus(); + Long amountTotal = invoice.getAmountPaid(); + // stripe 的支付金额单位是分,在我们数据库中金额单位为 元 + Float divide = new BigDecimal(amountTotal).divide(new BigDecimal(100), 2, RoundingMode.HALF_UP).floatValue(); + String type = invoice.getBillingReason().equals("subscription_create") ? "new" : + invoice.getBillingReason().equals("subscription_cycle") ? "renewal" : invoice.getBillingReason(); + + + paymentInfo = new PaymentInfo(); + paymentInfo.setOrderNo(orderNo); + paymentInfo.setPaymentType(PayTypeEnum.STRIPE.getType()); + paymentInfo.setTransactionId(invoiceId); + // 使用invoice的状态 + paymentInfo.setTradeState(status); + paymentInfo.setPayerTotal(divide); + Gson gson = new Gson(); + String json = gson.toJson(invoice); + paymentInfo.setContent(json); + paymentInfo.setType(type); + paymentInfo.setNotified(0); + + baseMapper.insert(paymentInfo); + return orderNo; + } + return null; + } + + @Override public PaymentInfo getPaymentInfoByOrderId(String orderId){ QueryWrapper qw = new QueryWrapper<>(); 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 7951adcd..b5e3a68e 100644 --- a/src/main/java/com/ai/da/service/impl/StripeServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/StripeServiceImpl.java @@ -1,16 +1,22 @@ 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.AliPayTradeStateEnum; -import com.ai.da.common.enums.CreditsEventsEnum; -import com.ai.da.common.enums.OrderStatusEnum; -import com.ai.da.common.enums.PayTypeEnum; -import com.ai.da.common.utils.OrderNoUtils; +import com.ai.da.common.enums.*; +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.SubscriptionInfoMapper; import com.ai.da.mapper.primary.entity.OrderInfo; import com.ai.da.mapper.primary.entity.PaymentInfo; import com.ai.da.mapper.primary.entity.RefundInfo; +import com.ai.da.mapper.primary.entity.SubscriptionInfo; +import com.ai.da.model.dto.ProductPurchaseDTO; +import com.ai.da.model.dto.SubscriptionEmailParamsDTO; import com.ai.da.service.*; +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.toolkit.StringUtils; import com.google.gson.Gson; import com.stripe.Stripe; @@ -21,6 +27,7 @@ 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; @@ -30,10 +37,13 @@ import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import java.math.BigDecimal; import java.math.RoundingMode; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; +import java.text.SimpleDateFormat; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.*; +@SuppressWarnings("LoggingSimilarMessage") @Service @Slf4j public class StripeServiceImpl implements StripeService { @@ -51,6 +61,10 @@ public class StripeServiceImpl implements StripeService { @Resource private AccountMapper accountMapper; + @Resource + private SubscriptionInfoMapper subscriptionInfoMapper; + @Resource + private PaymentInfoMapper paymentInfoMapper; @Value("${stripe.private-key}") private String privateKey; @@ -60,53 +74,78 @@ public class StripeServiceImpl implements StripeService { @Override @Transactional(rollbackFor = Exception.class) - public String pay(Integer quantity, String returnUrl) { + public String pay(ProductPurchaseDTO productPurchaseDTO) { Stripe.apiKey = privateKey; - log.info("生成订单"); - OrderInfo orderInfo = orderInfoService.createOrderByProductId(quantity, PayTypeEnum.STRIPE.getType()); - Long id = UserContext.getUserHolder().getId(); - com.ai.da.mapper.primary.entity.Account account = accountMapper.selectById(id); + ProductEnum productEnum; + switch (productPurchaseDTO.getProductName()){ + case "Credits Purchase": + productEnum = ProductEnum.CreditsProduct; + 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"); + } + break; + default: + throw new BusinessException("unknown product type"); + } + log.info("生成订单"); + OrderInfo orderInfo = orderInfoService.createOrderByProductId(productPurchaseDTO.getQuantity(), PayTypeEnum.STRIPE.getType(), productEnum); + String payType; + if (productPurchaseDTO.getAutoRenewal()){ + payType = "recurring"; + }else { + payType = "one_time"; + } try { - //创建产品 - Map params = new HashMap<>(); - params.put("name", "AiDA credits purchase"); - Product product = Product.create(params); - String orderId = OrderNoUtils.getOrderNo(); - - BigDecimal actualAmount = new BigDecimal(Long.parseLong(CreditsEventsEnum.PRICE.getValue()) * 100); //stripe的默认单位是分,即传入的amount实际上小数点会被左移两位 - //给price绑定元数据并更新price用于检索 - Map metadata = new HashMap<>(); - metadata.put("orderId", orderId); - //创建价格 - Map priceParams = new HashMap<>(); - priceParams.put("metadata", metadata); //通过订单号关联用于检索price信息(可选) - priceParams.put("unit_amount", actualAmount.intValue()); - priceParams.put("currency", "HKD"); - priceParams.put("product", product.getId()); - Price price = Price.create(priceParams); + 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()); + // 获取自定义订单号 + String orderId = orderInfo.getOrderNo(); //创建支付信息得到url - SessionCreateParams params3 = SessionCreateParams.builder() - .setMode(SessionCreateParams.Mode.PAYMENT) - .setSuccessUrl(returnUrl)//可自定义成功页面 - .setPaymentIntentData(SessionCreateParams.PaymentIntentData.builder().setDescription("AiDA - " + orderId).build()) - .setLocale(account.getLanguage().equals("CHINESE_SIMPLIFIED") ? SessionCreateParams.Locale.ZH : SessionCreateParams.Locale.EN) - .addLineItem( - SessionCreateParams.LineItem.builder() - .setQuantity(Long.valueOf(quantity)) - .setPrice(price.getId()) - .build()) - .putMetadata("orderId", orderId) //通过订单号关联用于检索支付信息(可选) - .build(); - Session session = Session.create(params3); + // 一次性支付和周期扣款,需要区分mode: payment || subscription || setup + SessionCreateParams.Builder sessionBuilder = new SessionCreateParams.Builder(); + 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()); + } + 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()); log.info("sessionId:" + session.getId()); //退款方式1:拿到sessionId入库,退款的时候根据这个id找到PaymentIntent的id然后发起退款 // 更新order信息 orderInfoService.updateOrderNoById(orderInfo.getId(), orderId); - //记录支付日志 - paymentInfoService.createPaymentInfoForStripe(session); return session.getUrl(); } catch (Exception e) { log.error("创建支付会话出现异常:", e); @@ -114,6 +153,69 @@ public class StripeServiceImpl implements StripeService { 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) { @@ -127,19 +229,19 @@ public class StripeServiceImpl implements StripeService { } catch (Exception e) { log.info("stripe 支付回调参数解析异常:errorMsg {}", e.getMessage()); log.info("request sigHeader = {}", sigHeader); - log.info("request body = {}", payload); + log.info("request body = {}", JSON.toJSONString(payload)); e.printStackTrace(); return Boolean.FALSE; } - Event event = null; + Event event; try { assert sigHeader != null; event = Webhook.constructEvent(payload, sigHeader, endpointSecret); } catch (SignatureVerificationException e) { - log.info("stripe 验签,获取事件异常, errorMsg=" + e.getMessage()); + log.info("stripe 验签,获取事件异常, errorMsg={}", e.getMessage()); log.info("request sigHeader = {}", sigHeader); - log.info("request body = {}", payload); + log.info("request body = {}", JSON.toJSONString(payload)); e.printStackTrace(); return Boolean.FALSE; } @@ -148,23 +250,65 @@ public class StripeServiceImpl implements StripeService { // Deserialize the nested object inside the event assert event != null; EventDataObjectDeserializer dataObjectDeserializer = event.getDataObjectDeserializer(); - StripeObject stripeObject = null; + StripeObject stripeObject ; if (dataObjectDeserializer.getObject().isPresent()) { stripeObject = dataObjectDeserializer.getObject().get(); } else { log.info("stripe 验签失败!"); log.info("request sigHeader = {}", sigHeader); - log.info("request body = {}", payload); + log.info("request body = {}", JSON.toJSONString(payload)); return Boolean.FALSE; } log.info("stripe验签成功"); + Boolean response = Boolean.TRUE; - if (event.getType().equals("checkout.session.completed")) { + log.info("回调事件 {}", event.getType()); + if (stripeObject instanceof Session){ Session session = (Session) stripeObject; - processOrder(session); - } + if (event.getType().equals("checkout.session.completed")) { + processOrder(session); + } + } 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")){ + // 更新订阅信息 + response = updateSubscription(subscription); + log.info("订阅更新"); + if (subscription.getStatus().equals("active")){ + response = sendEmail(subscription.getId(), null); + } + } else if (event.getType().equals("customer.subscription.deleted")){ + response = updateSubscription(subscription); + log.info("用户取消连续订阅"); + } 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,表示,该回调第一次被记录 + String orderNo = paymentInfoService.createPaymentInfoForStripe(invoice); - return Boolean.TRUE; + if (!StringUtil.isNullOrEmpty(orderNo)) { + // 更新t_order_info中的total_fee,记录该订单的累计付款金额 + orderInfoService.updateTotalFeeByOrderNo(orderNo); + // 邮件通知商家和用户 + if (invoice.getBillingReason().equals("subscription_create")){ + response = sendEmail(invoice.getSubscription(), "new"); + } else if (invoice.getBillingReason().equals("subscription_cycle")){ + response = sendEmail(invoice.getSubscription(), "renewal"); + } + } + } + } + return response; } public void processOrder(Session session) { @@ -186,31 +330,135 @@ public class StripeServiceImpl implements StripeService { orderInfoService.updateStatusByOrderNo(orderId, OrderStatusEnum.SUCCESS); log.info("Stripe 订单:{} 状态更新成功", orderId); - // 更新支付日志 - paymentInfoService.updatePaymentStatusById( - paymentInfoService.getPaymentInfoByOrderId(orderId).getId(), - session.getStatus(), - new Gson().toJson(session)); - - log.info("Stripe 订单:{} 支付信息状态更新成功", orderId); - float quantity = totalAmount / Float.parseFloat(CreditsEventsEnum.PRICE.getValue()); - // 更新积分 - creditsService.buyCredits(orderByOrderNo.getAccountId(), quantity); - // 添加积分变更记录 - creditsService.insertToCreditsDetail(orderByOrderNo.getAccountId(), - CreditsEventsEnum.BUY_CREDITS.getName() + "--Stripe", - String.valueOf((Long.parseLong(CreditsEventsEnum.BUY_CREDITS.getValue()) * quantity)), - "positive"); - log.info("用户:{} 积分信息更新成功", orderByOrderNo.getAccountId()); - }catch (Exception e){ + if (orderByOrderNo.getTitle().startsWith("积分购买")){ + 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"); + log.info("用户:{} 积分信息更新成功", orderByOrderNo.getAccountId()); + } + } catch (Exception e) { log.info(e.getMessage()); } } + @Transactional(rollbackFor = Exception.class) + public SubscriptionInfo createSubscription(Subscription subscription){ + // 确认当前subscription是否已经记录 + QueryWrapper qw = new QueryWrapper<>(); + qw.eq("subscription_id", 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(); + Map paymentMethod = getPaymentMethodByInvoiceId(subscription.getLatestInvoice()); + + subscriptionInfo = new SubscriptionInfo(); + subscriptionInfo.setAccountId(orderInfo.getAccountId()); + subscriptionInfo.setOrderNo(orderNo); + subscriptionInfo.setSubscriptionId(subscription.getId()); + subscriptionInfo.setType(interval); + subscriptionInfo.setStatus(subscription.getStatus()); + subscriptionInfo.setPaymentMethod(paymentMethod.get("paymentMethod")); + subscriptionInfo.setLast4(paymentMethod.get("last4")); + subscriptionInfo.setNextPayDate(changeTimeStampFormat(subscription.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy_EEEE)); + subscriptionInfo.setCurrentPeriodStart(subscription.getCurrentPeriodStart()); + subscriptionInfo.setCurrentPeriodEnd(subscription.getCurrentPeriodEnd()); + subscriptionInfo.setCreateTime(LocalDateTime.now()); + subscriptionInfoMapper.insert(subscriptionInfo); + + // 更新账号到期时间 + updateAccountValidity(subscriptionInfo.getAccountId(), subscriptionInfo.getCurrentPeriodEnd()); + } + return subscriptionInfo; + } + + public String changeTimeStampFormat(Long timeStamp, String type, String format){ + // 将秒级时间戳转换为毫秒级 + if (type.equals("seconds")){ + timeStamp = timeStamp * 1000; + } + // 输出格式 + SimpleDateFormat outputFormat = new SimpleDateFormat(format, Locale.ENGLISH); + // 创建Date对象 + Date date = new Date(timeStamp); + // 格式化输出 + return outputFormat.format(date); + } + + @Transactional(rollbackFor = Exception.class) + public Boolean updateSubscription(Subscription subscription){ + // 获取当前是否有已经记录的subscriptionInfo + SubscriptionInfo subscriptionInfo = createSubscription(subscription); + // 用于标志数据有没有变动,避免在没有改动的情况下频繁的更新数据库 + boolean flag = false; + if (!subscriptionInfo.getStatus().equals(subscription.getStatus())){ + subscriptionInfo.setStatus(subscription.getStatus()); + flag = true; + } + if (subscription.getStatus().equals("canceled")){ + subscriptionInfo.setAutoRenewal((byte) 0); + } + if (!subscriptionInfo.getCurrentPeriodStart().equals(subscription.getCurrentPeriodStart())){ + subscriptionInfo.setCurrentPeriodStart(subscription.getCurrentPeriodStart()); + flag = true; + } + if (!subscriptionInfo.getCurrentPeriodEnd().equals(subscription.getCurrentPeriodEnd())){ + subscriptionInfo.setCurrentPeriodEnd(subscription.getCurrentPeriodEnd()); + subscriptionInfo.setNextPayDate(changeTimeStampFormat(subscription.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy_EEEE)); + // 更新账号到期时间 + updateAccountValidity(subscriptionInfo.getAccountId(), subscriptionInfo.getCurrentPeriodEnd()); + log.info("更新 {} 账号到期时间为:{}", subscriptionInfo.getAccountId(), changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy_EEEE)); + flag = true; + } + if (flag){ + subscriptionInfo.setUpdateTime(LocalDateTime.now()); + // todo 这里需要再检查支付方式吗? + subscriptionInfoMapper.updateById(subscriptionInfo); + } + return true; + } + + private void updateAccountValidity(Long accountId, Long currentPeriodEnd){ + // 不管当前用户的账号是否到期,都根据付款信息重置账号到期时间 + com.ai.da.mapper.primary.entity.Account account = accountMapper.selectById(accountId); + account.setValidEndTime(currentPeriodEnd * 1000); + + accountMapper.updateById(account); + } + + // 取消连续订阅 将订阅从pause状态转为cancel状态(使用定时器,定期检索DB中,过期且不续订的订阅) + public void cancelSubscription(String subscriptionId) { + Stripe.apiKey = privateKey; + Long accountId = UserContext.getUserHolder().getId(); + log.info("用户 {} 申请取消连续订阅 {}", accountId, subscriptionId); + 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(); + } catch (StripeException e) { + log.error("订阅 {} 取消失败, error message : {}", subscription.getId(), e.getMessage()); + } + } + }); + } public String refund(String amount, String orderId, String reason) { - Refund refund ; + Refund refund; RefundInfo refundByOrderNo = refundInfoService.createRefundByOrderNo(orderId, reason); try { @@ -235,7 +483,7 @@ public class StripeServiceImpl implements StripeService { refund = Refund.create(params); log.info("根据会话编号退款成功"); - }else { + } else { log.error("当前订单不存在"); return "退款异常"; } @@ -260,7 +508,7 @@ public class StripeServiceImpl implements StripeService { // 更新积分状态 OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(orderId); - creditsService.creditsRefund(orderByOrderNo.getAccountId(), (int)(orderByOrderNo.getTotalFee() / Float.parseFloat(CreditsEventsEnum.PRICE.getValue()))); + creditsService.creditsRefund(orderByOrderNo.getAccountId(), (int) (orderByOrderNo.getTotalFee() / Float.parseFloat(CreditsEventsEnum.PRICE.getValue()))); } else { //更新订单状态 orderInfoService.updateStatusByOrderNo(orderId, OrderStatusEnum.REFUND_ABNORMAL); @@ -275,17 +523,17 @@ public class StripeServiceImpl implements StripeService { log.info("记录退款订单"); return "退款成功"; } - - public void checkOrderStatus(String orderNo){ + + public void checkOrderStatus(String orderNo) { Stripe.apiKey = privateKey; // 1、通过orderNo 查询sessionId PaymentInfo paymentInfo = paymentInfoService.getPaymentInfoByOrderId(orderNo); try { Session session = Session.retrieve(paymentInfo.getTransactionId()); - if (Objects.isNull(session)){ + if (Objects.isNull(session)) { log.warn("核实订单未创建 ===> {}", orderNo); return; - } else if (session.getStatus().equals("open") || session.getStatus().equals("expired")){ + } else if (session.getStatus().equals("open") || session.getStatus().equals("expired")) { // 订单未支付 || 订单过期 ---> 均设置为超时未支付 log.info("订单超时未支付 ===> {}", orderNo); //更新本地订单状态 @@ -304,96 +552,195 @@ public class StripeServiceImpl implements StripeService { } - // 1、创建customer,获取customerId - // 2、创建客户支付方式 (从前端获取) - // 3、创建支付 paymentIntent - // 4、确认订单 - // 5、捕获金额(执行扣款操作) - public String createCustomer() throws StripeException { + // 获取所有订阅 + 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).build()); + return list.getData(); + } 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("xp") - .setEmail("xupei3360@163.com") + .setName(name) + .setEmail(userEmail) .build(); - Customer customer = Customer.create(params); return customer.getId(); } - public String createPaymentMethod(String customerId) throws StripeException { - - Stripe.apiKey = privateKey; - - PaymentMethodCreateParams params = - PaymentMethodCreateParams.builder() - .setType(PaymentMethodCreateParams.Type.CARD) - .setCard( - // 测试中,不建议使用卡号,会不安全的异常,必须使用token(https://docs.stripe.com/testing?testing-method=tokens#visa) - PaymentMethodCreateParams.Token.builder().setToken("tok_visa").build() -// PaymentMethodCreateParams.CardDetails.builder() -// .setNumber("4242424242424242") -// .setExpMonth(8L) -// .setExpYear(2026L) -// .setCvc("314") -// .build() - ) - .build(); - - PaymentMethod paymentMethod = PaymentMethod.create(params); - return paymentMethod.getId(); + /** + * 使用连续订阅的订单,回调中没有paymentIntentId,所以通过invoiceId间接获取 + * @param invoiceId 发票Id + */ + public Map getPaymentMethodByInvoiceId(String invoiceId) { + try { + Stripe.apiKey = privateKey; + Invoice invoice = Invoice.retrieve(invoiceId); + PaymentIntent paymentIntent = PaymentIntent.retrieve(invoice.getPaymentIntent()); + PaymentMethod paymentMethod = PaymentMethod.retrieve(paymentIntent.getPaymentMethod()); + return getPaymentMethod(paymentMethod.getId()); + } catch (StripeException e) { + throw new RuntimeException(e); + } } - public String createPaymentIntent(String paymentMethodId, String customerId) throws StripeException { + public Map getPaymentMethod(String paymentMethodId){ Stripe.apiKey = privateKey; + String paymentMethod = null; + String last4 = null; - Long amount = 600L; - PaymentIntentCreateParams params = - PaymentIntentCreateParams.builder() - .setAmount(amount) -// .setPaymentMethod(paymentMethodId) - .setCustomer(customerId) - .setCurrency("hkd") - .setAutomaticPaymentMethods( - PaymentIntentCreateParams.AutomaticPaymentMethods.builder() - .setEnabled(true) - .build() - ) - .build(); - - PaymentIntent paymentIntent = PaymentIntent.create(params); - return paymentIntent.getId(); + try { + PaymentMethod retrieve = PaymentMethod.retrieve(paymentMethodId); + switch (retrieve.getType()){ + case "alipay": + paymentMethod = "Alipay"; + 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(); + break; + case "giropay": + paymentMethod = "GiroPay"; + break; + case "ideal": + PaymentMethod.Ideal ideal = retrieve.getIdeal(); + paymentMethod = ideal.getBank(); + break; + case "link": + paymentMethod = "Link"; + break; + } + HashMap resp = new HashMap<>(); + resp.put("paymentMethod", paymentMethod); + resp.put("last4", last4); + return resp; + } catch (StripeException e) { + throw new RuntimeException(e); + } +// return null; } - public String confirmPaymentIntent(String clientSecret) throws StripeException { - Stripe.apiKey = privateKey; + public boolean sendEmail(String subscriptionId, String type){ - PaymentIntent resource = PaymentIntent.retrieve(clientSecret); + SubscriptionEmailParamsDTO emailParamsDTO = new SubscriptionEmailParamsDTO(); + QueryWrapper qwSI = new QueryWrapper<>(); + qwSI.eq("subscription_id", subscriptionId); + SubscriptionInfo subscriptionInfo = subscriptionInfoMapper.selectOne(qwSI); + if (Objects.isNull(subscriptionInfo)) { + return false; + } + QueryWrapper qwPI = new QueryWrapper<>(); + qwPI.eq("order_no", subscriptionInfo.getOrderNo()).orderByDesc("id"); + List paymentInfos = paymentInfoMapper.selectList(qwPI); + if (paymentInfos.isEmpty()) { + return false; + } + PaymentInfo paymentInfo = paymentInfos.get(0); + if (paymentInfo.getNotified() == 1){ + // 已经邮件通知过,直接返回 + return true; + } + if (StringUtil.isNullOrEmpty(type)){ + // 如果没有传入type,则使用paymentInfo中记录的类型 + // (其实这里也可以通过invoiceId查询stripe,但是记录在自己的db中可以不用每次都查,且方便查看) + type = paymentInfo.getType(); + } - PaymentIntentConfirmParams params = - PaymentIntentConfirmParams.builder() - .setPaymentMethod("pm_card_visa") - .setReturnUrl("https://www.example.com") - .build(); + com.ai.da.mapper.primary.entity.Account account = accountMapper.selectById(subscriptionInfo.getAccountId()); + String userName = account.getUserName(); + String language = account.getLanguage(); - PaymentIntent paymentIntent = resource.confirm(params); - return paymentIntent.getId(); + 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.setLastOrderDate(changeTimeStampFormat(subscriptionInfo.getCurrentPeriodStart(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy)); + emailParamsDTO.setEndOfPrepaidTerm(changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy)); + emailParamsDTO.setPaymentMethod(subscriptionInfo.getPaymentMethod()); + emailParamsDTO.setSubscriptionId(subscriptionInfo.getId().toString()); + emailParamsDTO.setSubscriptionType(subscriptionInfo.getType()); + emailParamsDTO.setStartDate(changeTimeStampFormat(subscriptionInfo.getCurrentPeriodStart(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy)); + emailParamsDTO.setNextPayDate(changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy)); + emailParamsDTO.setRenewalTime(changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy)); + + SendEmailUtil.subscriptionEmailReminder(type, emailParamsDTO, language, account.getUserEmail()); + + // 邮件通知成功后,更新标志 + PaymentInfo payment = new PaymentInfo(); + payment.setId(paymentInfo.getId()); + payment.setNotified(1); + payment.setUpdateTime(LocalDateTime.now()); + paymentInfoMapper.updateById(payment); + return true; } - public String capturePaymentIntent(String clientSecret) throws StripeException { + 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); - Stripe.apiKey = privateKey; + // 转为时间戳 + long startTimestamp = startOfDay.toEpochSecond(ZoneOffset.UTC); + long endTimestamp = endOfDay.toEpochSecond(ZoneOffset.UTC); - PaymentIntent resource = PaymentIntent.retrieve(clientSecret); + QueryWrapper qw = new QueryWrapper<>(); + qw.ge("current_period_end", startTimestamp); + qw.lt("current_period_end", endTimestamp); + qw.eq("status", "active"); - PaymentIntentCaptureParams params = PaymentIntentCaptureParams.builder().build(); - - PaymentIntent paymentIntent = resource.capture(params); - return null; + List subscriptionInfos = subscriptionInfoMapper.selectList(qw); + for (SubscriptionInfo subscriptionInfo : subscriptionInfos) { + boolean b = sendEmail(subscriptionInfo.getSubscriptionId(), 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()); + } + } } diff --git a/src/main/resources/payment.properties b/src/main/resources/payment.properties index 56e5a315..43c4e4b7 100644 --- a/src/main/resources/payment.properties +++ b/src/main/resources/payment.properties @@ -27,13 +27,13 @@ paypal.webhook_id=1D107312EX592781K ##### Stripe # developer -#stripe.private-key=sk_test_51P4ZZL02n1TEydyN8qQHjOA9imsFU7Oxs2HMHGy2urHnnQgSHnZuu5vVP6pKhEACwUpsKNyrbZpdcg5TJWJLRHcY008dEO1fn2 -#stripe.webhook-sign-secret=whsec_e0dBiJngx6qqgJj6yPyJ2A9ouh1Cjv5w +stripe.private-key=sk_test_51P4ZZL02n1TEydyN8qQHjOA9imsFU7Oxs2HMHGy2urHnnQgSHnZuu5vVP6pKhEACwUpsKNyrbZpdcg5TJWJLRHcY008dEO1fn2 +stripe.webhook-sign-secret=whsec_e0dBiJngx6qqgJj6yPyJ2A9ouh1Cjv5w # kim - test #stripe.private-key=sk_test_51LwPrxH7nPZ8bkrNj67TFD7sxucaTANs1lf0KGSu1QSJfxYXcnigq2wTaZyZzST7y0fMbhhvaJZ4LjjFhr95M83a00eXrmOTL0 #stripe.webhook-sign-secret=whsec_GoyVEAaBtuGD5Rt55z83JnPnLDAZTN3u # kim - live -stripe.private-key=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 -stripe.webhook-sign-secret=whsec_hhGDgdelQRHSg4LmChtQe41crj41eb11 \ No newline at end of file +#stripe.private-key=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 +#stripe.webhook-sign-secret=whsec_hhGDgdelQRHSg4LmChtQe41crj41eb11 \ No newline at end of file From f6f759110fddc981ad402b433c577912a85b0f24 Mon Sep 17 00:00:00 2001 From: xupei Date: Tue, 19 Nov 2024 16:00:30 +0800 Subject: [PATCH 2/8] =?UTF-8?q?=E6=94=AF=E4=BB=98=E4=BC=98=E5=8C=96--?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=E7=A7=AF=E5=88=86=E8=B4=AD=E4=B9=B0=E7=9B=B8?= =?UTF-8?q?=E5=BA=94=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/ai/da/service/CreditsService.java | 4 +- .../ai/da/service/impl/AliPayServiceImpl.java | 8 ++- .../da/service/impl/AlipayHKServiceImpl.java | 3 +- .../da/service/impl/CreditsServiceImpl.java | 7 +- .../da/service/impl/GenerateServiceImpl.java | 4 +- .../impl/PayPalCheckoutServiceImpl.java | 28 ++++---- .../service/impl/PaymentInfoServiceImpl.java | 11 +++- .../ai/da/service/impl/StripeServiceImpl.java | 64 ++++++++++++------- .../impl/SuperResolutionServiceImpl.java | 2 +- src/main/resources/messages_en.properties | 1 + src/main/resources/messages_zh.properties | 1 + 11 files changed, 81 insertions(+), 52 deletions(-) diff --git a/src/main/java/com/ai/da/service/CreditsService.java b/src/main/java/com/ai/da/service/CreditsService.java index 1d076527..62d9e5e4 100644 --- a/src/main/java/com/ai/da/service/CreditsService.java +++ b/src/main/java/com/ai/da/service/CreditsService.java @@ -17,9 +17,9 @@ public interface CreditsService extends IService { String getCredits(Long accountId); - void creditsRefund(Long accountId, Integer quantity); + void creditsRefund(Long accountId, Integer quantity, String orderNo); - void insertToCreditsDetail(Long accountId, String changeEvent, String credits, String changeType); + void insertToCreditsDetail(Long accountId, String changeEvent, String credits, String changeType, String orderNo); PageBaseResponse queryCreditsDetailsPage(QueryIncomeOrExpenditureDTO queryPageByTimeDTO); diff --git a/src/main/java/com/ai/da/service/impl/AliPayServiceImpl.java b/src/main/java/com/ai/da/service/impl/AliPayServiceImpl.java index ccbd47d2..04fc8993 100644 --- a/src/main/java/com/ai/da/service/impl/AliPayServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/AliPayServiceImpl.java @@ -217,7 +217,8 @@ public class AliPayServiceImpl implements AliPayService { creditsService.insertToCreditsDetail(orderByOrderNo.getAccountId(), CreditsEventsEnum.BUY_CREDITS.getName() + "--Alipay", String.valueOf((Long.parseLong(CreditsEventsEnum.BUY_CREDITS.getValue()) * quantity)), - "positive"); + "positive", + orderByOrderNo.getOrderNo()); } finally { //要主动释放锁 lock.unlock(); @@ -320,7 +321,8 @@ public class AliPayServiceImpl implements AliPayService { creditsService.insertToCreditsDetail(orderByOrderNo.getAccountId(), CreditsEventsEnum.BUY_CREDITS.getName() + "--Alipay", String.valueOf((Long.parseLong(CreditsEventsEnum.BUY_CREDITS.getValue()) * quantity)), - "positive"); + "positive", + orderByOrderNo.getOrderNo()); } } @@ -393,7 +395,7 @@ public class AliPayServiceImpl implements AliPayService { // 更新积分状态 OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(orderNo); // creditsService.creditsRefund(orderByOrderNo.getAccountId(), orderByOrderNo.getTotalFee() / Integer.parseInt(CreditsEventsEnum.PRICE.getValue())); - creditsService.creditsRefund(orderByOrderNo.getAccountId(), (int)(orderByOrderNo.getTotalFee() / Float.parseFloat(CreditsEventsEnum.PRICE.getValue()))); + creditsService.creditsRefund(orderByOrderNo.getAccountId(), (int)(orderByOrderNo.getTotalFee() / Float.parseFloat(CreditsEventsEnum.PRICE.getValue())), orderNo); } else { log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg()); diff --git a/src/main/java/com/ai/da/service/impl/AlipayHKServiceImpl.java b/src/main/java/com/ai/da/service/impl/AlipayHKServiceImpl.java index f20d7350..417a03bd 100644 --- a/src/main/java/com/ai/da/service/impl/AlipayHKServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/AlipayHKServiceImpl.java @@ -248,7 +248,8 @@ public class AlipayHKServiceImpl implements AlipayHKService { creditsService.insertToCreditsDetail(orderByOrderNo.getAccountId(), CreditsEventsEnum.BUY_CREDITS.getName() + "--AlipayHK", String.valueOf((Long.parseLong(CreditsEventsEnum.BUY_CREDITS.getValue()) * quantity)), - "positive"); + "positive", + orderByOrderNo.getOrderNo()); log.info("用户:{} 积分信息更新成功",orderByOrderNo.getAccountId()); } finally { //要主动释放锁 diff --git a/src/main/java/com/ai/da/service/impl/CreditsServiceImpl.java b/src/main/java/com/ai/da/service/impl/CreditsServiceImpl.java index 9cc17ce8..5c24211c 100644 --- a/src/main/java/com/ai/da/service/impl/CreditsServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/CreditsServiceImpl.java @@ -100,7 +100,7 @@ public class CreditsServiceImpl extends ServiceImpl增 negative->减 */ @Override - public void insertToCreditsDetail(Long accountId, String changeEvent, String credits, String changeType) { + public void insertToCreditsDetail(Long accountId, String changeEvent, String credits, String changeType, String orderNo) { CreditsDetail creditsDetail = new CreditsDetail(); Account account = accountMapper.selectById(accountId); // BigDecimal finalCredits; @@ -137,6 +137,7 @@ public class CreditsServiceImpl extends ServiceImpl i if (b) creditsService.insertToCreditsDetail(accountId, CreditsEventsEnum.TO_PRODUCT_IMAGE.getName(), CreditsEventsEnum.TO_PRODUCT_IMAGE.getValue(), - "negative"); + "negative", null); } } @@ -736,7 +736,7 @@ public class GenerateServiceImpl extends ServiceImpl i if (b) creditsService.insertToCreditsDetail(accountId, CreditsEventsEnum.RELIGHT.getName(), CreditsEventsEnum.RELIGHT.getValue(), - "negative"); + "negative", null); } } diff --git a/src/main/java/com/ai/da/service/impl/PayPalCheckoutServiceImpl.java b/src/main/java/com/ai/da/service/impl/PayPalCheckoutServiceImpl.java index 22c57621..fb5d5409 100644 --- a/src/main/java/com/ai/da/service/impl/PayPalCheckoutServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/PayPalCheckoutServiceImpl.java @@ -441,11 +441,11 @@ public class PayPalCheckoutServiceImpl implements PayPalCheckoutService { * 申请退款 */ @Transactional(rollbackFor = Exception.class) - public Boolean refundOrder(String orderId, String reason) throws IOException { + public Boolean refundOrder(String orderNo, String reason) throws IOException { - RefundInfo refundByOrderNo = refundsInfoService.createRefundByOrderNo(orderId, reason); + RefundInfo refundByOrderNo = refundsInfoService.createRefundByOrderNo(orderNo, reason); - OrdersGetRequest ordersGetRequest = new OrdersGetRequest(orderId); + OrdersGetRequest ordersGetRequest = new OrdersGetRequest(orderNo); PayPalClient payPalClient = new PayPalClient(); HttpResponse ordersGetResponse = null; ordersGetRequest.authorization("Bearer " + getOAuth()); @@ -461,7 +461,7 @@ public class PayPalCheckoutServiceImpl implements PayPalCheckoutService { request.authorization("Bearer " + getOAuth()); request.prefer("return=representation"); - OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderId); + OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo); request.requestBody(buildRefundRequestBody(String.valueOf(orderInfo.getTotalFee()), reason)); HttpResponse response = null; try { @@ -476,7 +476,7 @@ public class PayPalCheckoutServiceImpl implements PayPalCheckoutService { //进行数据库操作,修改状态为已退款(配合回调和退款查询确定退款成功) //更新订单状态 - orderInfoService.updateStatusByOrderNo(orderId, OrderStatusEnum.REFUND_SUCCESS); + orderInfoService.updateStatusByOrderNo(orderNo, OrderStatusEnum.REFUND_SUCCESS); refundsInfoService.updateRefundForPayPal( refundByOrderNo.getId(), @@ -485,14 +485,14 @@ public class PayPalCheckoutServiceImpl implements PayPalCheckoutService { AliPayTradeStateEnum.REFUND_SUCCESS.getType()); //退款成功 // 更新积分状态 - OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(orderId); + OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(orderNo); // creditsService.creditsRefund(orderByOrderNo.getAccountId(), orderByOrderNo.getTotalFee() / Integer.parseInt(CreditsEventsEnum.PRICE.getValue())); - creditsService.creditsRefund(orderByOrderNo.getAccountId(), (int)(orderByOrderNo.getTotalFee() / Float.parseFloat(CreditsEventsEnum.PRICE.getValue()))); + creditsService.creditsRefund(orderByOrderNo.getAccountId(), (int)(orderByOrderNo.getTotalFee() / Float.parseFloat(CreditsEventsEnum.PRICE.getValue())), orderNo); log.info("退款成功"); result = Boolean.TRUE; } else { //更新订单状态 - orderInfoService.updateStatusByOrderNo(orderId, OrderStatusEnum.REFUND_ABNORMAL); + orderInfoService.updateStatusByOrderNo(orderNo, OrderStatusEnum.REFUND_ABNORMAL); //更新退款单 refundsInfoService.updateRefundForPayPal( @@ -571,19 +571,19 @@ public class PayPalCheckoutServiceImpl implements PayPalCheckoutService { // 处理当前订单 @Transactional(rollbackFor = Exception.class) - public void processOrder(String orderId) { + public void processOrder(String orderNo) { // 1、确定当前订单是否已经被扣款 - OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderId); + OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo); if (orderInfo.getOrderStatus().equals(OrderStatusEnum.SUCCESS.getType())) { // 直接返回 return; } // 发起扣款请求 - Order capturedOrder = captureOrder(orderId); + Order capturedOrder = captureOrder(orderNo); // 业务处理 if (PayPalOrderStatusEnum.COMPLETED.getStatus().equals(capturedOrder.status())) { //更新订单状态 - orderInfoService.updateStatusByOrderNo(orderId, OrderStatusEnum.SUCCESS); + orderInfoService.updateStatusByOrderNo(orderNo, OrderStatusEnum.SUCCESS); //记录支付日志 paymentInfoService.createPaymentInfoForPayPal(capturedOrder); float quantity = orderInfo.getTotalFee() / Float.parseFloat(CreditsEventsEnum.PRICE.getValue()); @@ -593,7 +593,7 @@ public class PayPalCheckoutServiceImpl implements PayPalCheckoutService { creditsService.insertToCreditsDetail(orderInfo.getAccountId(), CreditsEventsEnum.BUY_CREDITS.getName() + "--PayPal", String.valueOf((Long.parseLong(CreditsEventsEnum.BUY_CREDITS.getValue()) * quantity)), - "positive"); + "positive", orderNo); } } @@ -637,7 +637,7 @@ public class PayPalCheckoutServiceImpl implements PayPalCheckoutService { creditsService.insertToCreditsDetail(orderByOrderNo.getAccountId(), CreditsEventsEnum.BUY_CREDITS.getName() + "--Paypal", String.valueOf((Long.parseLong(CreditsEventsEnum.BUY_CREDITS.getValue()) * quantity)), - "positive"); + "positive", orderNo); } } diff --git a/src/main/java/com/ai/da/service/impl/PaymentInfoServiceImpl.java b/src/main/java/com/ai/da/service/impl/PaymentInfoServiceImpl.java index 035e1135..36acae75 100644 --- a/src/main/java/com/ai/da/service/impl/PaymentInfoServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/PaymentInfoServiceImpl.java @@ -187,10 +187,15 @@ public class PaymentInfoServiceImpl extends ServiceImpl resp = new HashMap<>(); + resp.put("paymentMethod", null); + resp.put("last4", null); + return resp; } catch (StripeException e) { throw new RuntimeException(e); } @@ -668,15 +684,15 @@ public class StripeServiceImpl implements StripeService { return false; } PaymentInfo paymentInfo = paymentInfos.get(0); - if (paymentInfo.getNotified() == 1){ - // 已经邮件通知过,直接返回 - return true; - } if (StringUtil.isNullOrEmpty(type)){ // 如果没有传入type,则使用paymentInfo中记录的类型 // (其实这里也可以通过invoiceId查询stripe,但是记录在自己的db中可以不用每次都查,且方便查看) type = paymentInfo.getType(); } + if (!type.equals("reminder") && paymentInfo.getNotified() == 1){ + // 已经邮件通知过,直接返回 + return true; + } com.ai.da.mapper.primary.entity.Account account = accountMapper.selectById(subscriptionInfo.getAccountId()); String userName = account.getUserName(); @@ -692,18 +708,20 @@ public class StripeServiceImpl implements StripeService { emailParamsDTO.setPaymentMethod(subscriptionInfo.getPaymentMethod()); emailParamsDTO.setSubscriptionId(subscriptionInfo.getId().toString()); emailParamsDTO.setSubscriptionType(subscriptionInfo.getType()); - emailParamsDTO.setStartDate(changeTimeStampFormat(subscriptionInfo.getCurrentPeriodStart(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy)); + emailParamsDTO.setStartDate(changeTimeStampFormat(subscriptionInfo.getCreateTime())); emailParamsDTO.setNextPayDate(changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy)); emailParamsDTO.setRenewalTime(changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy)); SendEmailUtil.subscriptionEmailReminder(type, emailParamsDTO, language, account.getUserEmail()); // 邮件通知成功后,更新标志 - PaymentInfo payment = new PaymentInfo(); - payment.setId(paymentInfo.getId()); - payment.setNotified(1); - payment.setUpdateTime(LocalDateTime.now()); - paymentInfoMapper.updateById(payment); + if (!type.equals("reminder")){ + PaymentInfo payment = new PaymentInfo(); + payment.setId(paymentInfo.getId()); + payment.setNotified(1); + payment.setUpdateTime(LocalDateTime.now()); + paymentInfoMapper.updateById(payment); + } return true; } @@ -723,7 +741,7 @@ public class StripeServiceImpl implements StripeService { List subscriptionInfos = subscriptionInfoMapper.selectList(qw); for (SubscriptionInfo subscriptionInfo : subscriptionInfos) { - boolean b = sendEmail(subscriptionInfo.getSubscriptionId(), null); + boolean b = sendEmail(subscriptionInfo.getSubscriptionId(), "reminder"); if (b) log.info("提前7天向用户 {} 发送续订通知邮件", subscriptionInfo.getAccountId()); } } diff --git a/src/main/java/com/ai/da/service/impl/SuperResolutionServiceImpl.java b/src/main/java/com/ai/da/service/impl/SuperResolutionServiceImpl.java index 03ed3d69..eda65b46 100644 --- a/src/main/java/com/ai/da/service/impl/SuperResolutionServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/SuperResolutionServiceImpl.java @@ -149,7 +149,7 @@ public class SuperResolutionServiceImpl extends ServiceImpl Date: Tue, 19 Nov 2024 17:08:16 +0800 Subject: [PATCH 3/8] =?UTF-8?q?=E6=94=AF=E4=BB=98=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/ai/da/common/utils/SendEmailUtil.java | 20 +++++++++++-------- .../com/ai/da/service/PaymentInfoService.java | 2 +- .../service/impl/PaymentInfoServiceImpl.java | 5 ++--- .../ai/da/service/impl/StripeServiceImpl.java | 14 +++++++------ 4 files changed, 23 insertions(+), 18 deletions(-) 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 d1a30ae1..c52d3d8c 100644 --- a/src/main/java/com/ai/da/common/utils/SendEmailUtil.java +++ b/src/main/java/com/ai/da/common/utils/SendEmailUtil.java @@ -782,7 +782,8 @@ public class SendEmailUtil { public static void subscriptionEmailReminder(String type, SubscriptionEmailParamsDTO subscriptionEmailParamsDTO, String language, String receiverAddress){ try{ - String kimEmail = "kimwong@code-create.com.hk"; +// String merchantEmail = "kimwong@code-create.com.hk"; + String merchantEmail = "xupei3360@163.com"; Credential cred = new Credential(SECRET_ID, SECRET_KEy); // 实例化一个http选项,可选的,没有特殊需求可以跳过 HttpProfile httpProfile = new HttpProfile(); @@ -798,7 +799,7 @@ public class SendEmailUtil { user.setDestination(new String[]{receiverAddress}); SendEmailRequest merchant = new SendEmailRequest(); merchant.setFromEmailAddress(SEND_ADDRESS); - merchant.setDestination(new String[]{kimEmail}); + merchant.setDestination(new String[]{merchantEmail}); Template templateUser = new Template(); Template templateMerchant = new Template(); switch (type) { @@ -848,12 +849,15 @@ public class SendEmailUtil { templateMerchant.setTemplateData(JSON.toJSONString(subscriptionEmailParamsDTO)); merchant.setTemplate(templateMerchant); - // 返回的resp是一个SendEmailResponse的实例,与请求对象对应 - SendEmailResponse respUser = client.SendEmail(user); - // todo 暂时先不发商家邮件 -// SendEmailResponse respMerchant = client.SendEmail(merchant); - log.info("邮件发送结果toUser###{}", SendEmailResponse.toJsonString(respUser)); -// log.info("邮件发送结果toMerchant###{}", SendEmailResponse.toJsonString(respMerchant)); + if (!type.equals("cancel")){ + // 返回的resp是一个SendEmailResponse的实例,与请求对象对应 + SendEmailResponse respUser = client.SendEmail(user); + log.info("邮件发送结果toUser###{}", SendEmailResponse.toJsonString(respUser)); + } + if (!type.equals("reminder")){ + SendEmailResponse respMerchant = client.SendEmail(merchant); + log.info("邮件发送结果toMerchant###{}", SendEmailResponse.toJsonString(respMerchant)); + } } catch (TencentCloudSDKException e) { log.info("邮件发送失败###{}", e.toString()); throw new BusinessException("failed.to.send.mail"); diff --git a/src/main/java/com/ai/da/service/PaymentInfoService.java b/src/main/java/com/ai/da/service/PaymentInfoService.java index 6d3d2d5e..fcc69d00 100644 --- a/src/main/java/com/ai/da/service/PaymentInfoService.java +++ b/src/main/java/com/ai/da/service/PaymentInfoService.java @@ -18,7 +18,7 @@ public interface PaymentInfoService extends IService { void createPaymentInfoForAliPayHK(AlipayHKCallbackDTO alipayHKCallbackDTO); - String createPaymentInfoForStripe(Invoice invoice); + PaymentInfo createPaymentInfoForStripe(Invoice invoice); PaymentInfo getPaymentInfoByOrderId(String orderId); diff --git a/src/main/java/com/ai/da/service/impl/PaymentInfoServiceImpl.java b/src/main/java/com/ai/da/service/impl/PaymentInfoServiceImpl.java index 36acae75..e78fd856 100644 --- a/src/main/java/com/ai/da/service/impl/PaymentInfoServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/PaymentInfoServiceImpl.java @@ -176,7 +176,7 @@ public class PaymentInfoServiceImpl extends ServiceImpl resp = new HashMap<>(); - resp.put("paymentMethod", null); - resp.put("last4", null); + resp.put("paymentMethod", "N/A"); + resp.put("last4", "N/A"); return resp; } catch (StripeException e) { throw new RuntimeException(e); @@ -697,6 +698,7 @@ public class StripeServiceImpl implements StripeService { 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()); emailParamsDTO.setUsername(userName); emailParamsDTO.setOrderId(paymentInfo.getId().toString()); @@ -708,7 +710,7 @@ public class StripeServiceImpl implements StripeService { emailParamsDTO.setPaymentMethod(subscriptionInfo.getPaymentMethod()); emailParamsDTO.setSubscriptionId(subscriptionInfo.getId().toString()); emailParamsDTO.setSubscriptionType(subscriptionInfo.getType()); - emailParamsDTO.setStartDate(changeTimeStampFormat(subscriptionInfo.getCreateTime())); + emailParamsDTO.setStartDate(changeTimeStampFormat(orderByOrderNo.getCreateTime())); emailParamsDTO.setNextPayDate(changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy)); emailParamsDTO.setRenewalTime(changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy)); From 5019fbd3fc6156227c97f683e627f89a607be7d7 Mon Sep 17 00:00:00 2001 From: xupei Date: Mon, 25 Nov 2024 10:53:09 +0800 Subject: [PATCH 4/8] =?UTF-8?q?=E6=94=AF=E4=BB=98=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ai/da/common/enums/OrderStatusEnum.java | 5 + .../com/ai/da/common/utils/SendEmailUtil.java | 16 +- .../da/mapper/primary/entity/OrderInfo.java | 2 + .../da/mapper/primary/entity/PaymentInfo.java | 4 + .../primary/entity/SubscriptionInfo.java | 9 +- .../model/dto/SubscriptionEmailParamsDTO.java | 5 + .../com/ai/da/service/PaymentInfoService.java | 5 +- .../service/impl/PaymentInfoServiceImpl.java | 95 ++++++++++- .../ai/da/service/impl/StripeServiceImpl.java | 151 +++++++++++++++--- 9 files changed, 257 insertions(+), 35 deletions(-) diff --git a/src/main/java/com/ai/da/common/enums/OrderStatusEnum.java b/src/main/java/com/ai/da/common/enums/OrderStatusEnum.java index aac9719d..8f8699a6 100644 --- a/src/main/java/com/ai/da/common/enums/OrderStatusEnum.java +++ b/src/main/java/com/ai/da/common/enums/OrderStatusEnum.java @@ -17,6 +17,11 @@ public enum OrderStatusEnum { */ SUCCESS("支付成功"), + /** + * 支付失败 + */ + FAILURE("支付失败"), + /** * 已关闭 */ 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 c52d3d8c..c6d61e68 100644 --- a/src/main/java/com/ai/da/common/utils/SendEmailUtil.java +++ b/src/main/java/com/ai/da/common/utils/SendEmailUtil.java @@ -779,6 +779,8 @@ public class SendEmailUtil { private final static Long RENEWAL_USER_CN = 130726L; private final static Long RENEWAL_REMINDER_USER_EN = 130727L; private final static Long RENEWAL_REMINDER_USER_CN = 130728L; + private final static Long PAYMENT_FAILED_NEW_MERCHANT_EN = 131230L; + private final static Long PAYMENT_FAILED_RENEWAL_MERCHANT_EN = 131225L; public static void subscriptionEmailReminder(String type, SubscriptionEmailParamsDTO subscriptionEmailParamsDTO, String language, String receiverAddress){ try{ @@ -807,6 +809,14 @@ public class SendEmailUtil { merchant.setSubject("[Code-Create] Subscription Cancelled"); templateMerchant.setTemplateID(CANCEL_MERCHANT_EN); break; + case "fail_new": + merchant.setSubject("[Code-Create] Payment Failed : New Order (" + subscriptionEmailParamsDTO.getOrderId() + ")"); + templateMerchant.setTemplateID(PAYMENT_FAILED_NEW_MERCHANT_EN); + break; + case "fail_renewal": + merchant.setSubject("[Code-Create] Payment Failed : Renewal Order (" + subscriptionEmailParamsDTO.getOrderId() + ")"); + templateMerchant.setTemplateID(PAYMENT_FAILED_RENEWAL_MERCHANT_EN); + break; case "new": merchant.setSubject("[Code-Create] New Order(" + subscriptionEmailParamsDTO.getOrderId() + ")"); templateMerchant.setTemplateID(NEW_MERCHANT_EN); @@ -849,14 +859,14 @@ public class SendEmailUtil { templateMerchant.setTemplateData(JSON.toJSONString(subscriptionEmailParamsDTO)); merchant.setTemplate(templateMerchant); - if (!type.equals("cancel")){ + if (!type.equals("cancel") && !type.equals("fail_new") && !type.equals("fail_renewal") ){ // 返回的resp是一个SendEmailResponse的实例,与请求对象对应 SendEmailResponse respUser = client.SendEmail(user); - log.info("邮件发送结果toUser###{}", SendEmailResponse.toJsonString(respUser)); + log.info("邮件主题:{},发送结果toUser###{}", user.getSubject(), SendEmailResponse.toJsonString(respUser)); } if (!type.equals("reminder")){ SendEmailResponse respMerchant = client.SendEmail(merchant); - log.info("邮件发送结果toMerchant###{}", SendEmailResponse.toJsonString(respMerchant)); + log.info("邮件主题:{},发送结果toMerchant###{}", merchant.getSubject(), SendEmailResponse.toJsonString(respMerchant)); } } catch (TencentCloudSDKException e) { log.info("邮件发送失败###{}", e.toString()); 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 98a020b9..e5bc0a98 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 @@ -21,5 +21,7 @@ public class OrderInfo extends BaseEntity{ private String orderStatus;//订单状态 + private String note; + private String paymentType;//支付方式 } diff --git a/src/main/java/com/ai/da/mapper/primary/entity/PaymentInfo.java b/src/main/java/com/ai/da/mapper/primary/entity/PaymentInfo.java index 3806d888..8172f4bd 100644 --- a/src/main/java/com/ai/da/mapper/primary/entity/PaymentInfo.java +++ b/src/main/java/com/ai/da/mapper/primary/entity/PaymentInfo.java @@ -26,4 +26,8 @@ public class PaymentInfo extends BaseEntity{ // 当前支付是否已邮件通知 0 || 1 private Integer notified; + + private String paymentMethod; + + private String last4; } diff --git a/src/main/java/com/ai/da/mapper/primary/entity/SubscriptionInfo.java b/src/main/java/com/ai/da/mapper/primary/entity/SubscriptionInfo.java index 2b61cf48..b7895c08 100644 --- a/src/main/java/com/ai/da/mapper/primary/entity/SubscriptionInfo.java +++ b/src/main/java/com/ai/da/mapper/primary/entity/SubscriptionInfo.java @@ -22,14 +22,7 @@ public class SubscriptionInfo extends BaseEntity{ // active || expired private String status = "active"; - // 是否自动续订 - private byte autoRenewal = (byte)1; - - // 支付方式 - private String paymentMethod; - - // 如果是用卡支付,可以看到银行卡最后四位 - private String last4; + private byte cancelNotified = (byte)0; // 续订的下一个付款日 private String nextPayDate; 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 0020582f..9ecf0241 100644 --- a/src/main/java/com/ai/da/model/dto/SubscriptionEmailParamsDTO.java +++ b/src/main/java/com/ai/da/model/dto/SubscriptionEmailParamsDTO.java @@ -28,6 +28,8 @@ public class SubscriptionEmailParamsDTO { // 付款方式 private String paymentMethod; + private String last4; + // 订阅Id private String subscriptionId; @@ -43,5 +45,8 @@ public class SubscriptionEmailParamsDTO { // 下次付款时间(reminder) private String renewalTime; + // 付款失败原因 + private String failMessage; + } diff --git a/src/main/java/com/ai/da/service/PaymentInfoService.java b/src/main/java/com/ai/da/service/PaymentInfoService.java index fcc69d00..785fb286 100644 --- a/src/main/java/com/ai/da/service/PaymentInfoService.java +++ b/src/main/java/com/ai/da/service/PaymentInfoService.java @@ -4,6 +4,7 @@ import com.ai.da.mapper.primary.entity.PaymentInfo; import com.ai.da.model.dto.AlipayHKCallbackDTO; import com.baomidou.mybatisplus.extension.service.IService; import com.paypal.orders.Order; +import com.stripe.model.Charge; import com.stripe.model.Invoice; import java.util.Map; @@ -18,7 +19,9 @@ public interface PaymentInfoService extends IService { void createPaymentInfoForAliPayHK(AlipayHKCallbackDTO alipayHKCallbackDTO); - PaymentInfo createPaymentInfoForStripe(Invoice invoice); + PaymentInfo createOrUpdatePaymentInfoForStripe(Invoice invoice); + + PaymentInfo createOrUpdatePaymentInfoForStripe(Charge charge); PaymentInfo getPaymentInfoByOrderId(String orderId); diff --git a/src/main/java/com/ai/da/service/impl/PaymentInfoServiceImpl.java b/src/main/java/com/ai/da/service/impl/PaymentInfoServiceImpl.java index e78fd856..204a9ce4 100644 --- a/src/main/java/com/ai/da/service/impl/PaymentInfoServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/PaymentInfoServiceImpl.java @@ -11,6 +11,7 @@ import com.google.gson.Gson; import com.paypal.orders.Order; import com.stripe.Stripe; import com.stripe.exception.StripeException; +import com.stripe.model.Charge; import com.stripe.model.Invoice; import com.stripe.model.Subscription; import com.stripe.model.checkout.Session; @@ -20,6 +21,7 @@ import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.math.RoundingMode; +import java.time.LocalDateTime; import java.util.HashMap; import java.util.Map; import java.util.Objects; @@ -28,6 +30,12 @@ import java.util.Objects; @Slf4j public class PaymentInfoServiceImpl extends ServiceImpl implements PaymentInfoService { + private final StripeServiceImpl stripeServiceImpl; + + public PaymentInfoServiceImpl(StripeServiceImpl stripeServiceImpl) { + this.stripeServiceImpl = stripeServiceImpl; + } + /** * 记录支付日志:微信支付 * @param plainText @@ -152,7 +160,7 @@ public class PaymentInfoServiceImpl extends ServiceImpl qw = new QueryWrapper<>(); qw.eq("transaction_id", invoiceId); PaymentInfo paymentInfo = baseMapper.selectOne(qw); + String status = invoice.getStatus(); // 判断当前支付是否已经被记录,确保同一个支付不会被重复记录 if (Objects.isNull(paymentInfo)){ String orderNo; @@ -199,13 +208,19 @@ public class PaymentInfoServiceImpl extends ServiceImpl paymentMethod = stripeServiceImpl.getPaymentMethodByInvoiceId(invoiceId); paymentInfo = new PaymentInfo(); paymentInfo.setOrderNo(orderNo); @@ -219,12 +234,84 @@ public class PaymentInfoServiceImpl extends ServiceImpl qw = new QueryWrapper<>(); + qw.eq("transaction_id", charge.getInvoice()); + PaymentInfo paymentInfo = baseMapper.selectOne(qw); + Charge.PaymentMethodDetails paymentMethodDetails = charge.getPaymentMethodDetails(); + String paymentMethod; + String last4 = "N/A"; + switch (paymentMethodDetails.getType()){ + case "alipay": + paymentMethod = "Alipay"; + break; + case "bancontact": + paymentMethod = "BanContact"; + break; + case "card": + Charge.PaymentMethodDetails.Card card = paymentMethodDetails.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": + Charge.PaymentMethodDetails.Eps eps = paymentMethodDetails.getEps(); + paymentMethod = eps.getBank(); + break; + case "giropay": + paymentMethod = "GiroPay"; + break; + case "ideal": + Charge.PaymentMethodDetails.Ideal ideal = paymentMethodDetails.getIdeal(); + paymentMethod = ideal.getBank(); + break; + case "link": + paymentMethod = "Link"; + break; + default: + paymentMethod = "N/A"; + } + if (Objects.isNull(paymentInfo)){ + Stripe.apiKey = privateKey; + + Float divide = new BigDecimal(charge.getAmount()).divide(new BigDecimal(100), 2, RoundingMode.HALF_UP).floatValue(); + paymentInfo = new PaymentInfo(); + paymentInfo.setOrderNo(charge.getDescription().replace("AiDA - ", "")); + paymentInfo.setTransactionId(charge.getInvoice()); + paymentInfo.setPaymentType(PayTypeEnum.STRIPE.getType()); + paymentInfo.setTradeState(charge.getStatus()); + paymentInfo.setPayerTotal(divide); + paymentInfo.setNotified(0); + paymentInfo.setPaymentMethod(paymentMethod); + paymentInfo.setLast4(last4); + paymentInfo.setCreateTime(LocalDateTime.now()); + baseMapper.insert(paymentInfo); + }else { + paymentInfo.setTradeState(charge.getStatus()); + paymentInfo.setPaymentMethod(paymentMethod); + paymentInfo.setLast4(last4); + paymentInfo.setUpdateTime(LocalDateTime.now()); + baseMapper.updateById(paymentInfo); + } + + return paymentInfo; + } + @Override public PaymentInfo getPaymentInfoByOrderId(String orderId){ 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 fe903590..dba20370 100644 --- a/src/main/java/com/ai/da/service/impl/StripeServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/StripeServiceImpl.java @@ -271,6 +271,10 @@ public class StripeServiceImpl implements StripeService { Session session = (Session) stripeObject; if (event.getType().equals("checkout.session.completed")) { processOrder(session); + }else if (event.getType().equals("checkout.session.expired")){ + String orderNo = session.getMetadata().get("orderId"); + // 会话过期 未支付 且之后没有支付成功的订单 + processExpiredOrder(orderNo); } } else if (stripeObject instanceof Subscription){ Subscription subscription = (Subscription) stripeObject; @@ -280,25 +284,30 @@ public class StripeServiceImpl implements StripeService { log.info("创建连续订阅"); } else if (event.getType().equals("customer.subscription.updated")){ // 更新订阅信息 - response = updateSubscription(subscription); + updateSubscription(subscription); log.info("订阅更新"); if (subscription.getStatus().equals("active")){ response = sendEmail(subscription.getId(), null); } } else if (event.getType().equals("customer.subscription.deleted")){ - response = updateSubscription(subscription); + SubscriptionInfo subscriptionInfo = updateSubscription(subscription); log.info("用户取消连续订阅"); - } else if (event.getType().equals("customer.subscription.paused")){ + if (subscriptionInfo.getCancelNotified() == (byte)0){ + response = sendEmail(subscription.getId(), "cancel"); + } + subscriptionInfo.setCancelNotified((byte)1); + subscriptionInfoMapper.updateById(subscriptionInfo); + }/* 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.createPaymentInfoForStripe(invoice); + PaymentInfo paymentInfo = paymentInfoService.createOrUpdatePaymentInfoForStripe(invoice); // 当前支付没有被通知时才需要发送通知邮件 if (paymentInfo.getNotified().equals(0)) { @@ -311,6 +320,43 @@ public class StripeServiceImpl implements StripeService { response = sendEmail(invoice.getSubscription(), "renewal"); } } + } 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); + paymentInfoService.updateById(paymentInfo); + } + /*// 支付失败 通知商家的条件 1、会话过期 2、支付失败 3、这个用户在这个支付失败后再无支付成功的订阅 + if (invoice.getBillingReason().equals("subscription_create")){ + response = sendEmail(paymentInfo.getOrderNo()); + } else if (invoice.getBillingReason().equals("subscription_cycle")){ + response = sendEmail(invoice.getSubscription(), "fail_renewal"); + }*/ + } + }else if (stripeObject instanceof Charge) { + Charge charge = (Charge) stripeObject; + String orderNo = charge.getDescription().replace("AiDA - ", ""); + OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo); + 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); } } return response; @@ -351,6 +397,36 @@ public class StripeServiceImpl implements StripeService { } } + private void processExpiredOrder(String orderNo) { + // 支付失败 通知商家的条件 1、会话过期 2、支付失败 3、这个用户在这个支付失败后再无支付成功的订阅 + // 1、获取当前订单的支付状态 +// String orderNo = session.getMetadata().get("orderId"); + OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(orderNo); + // 2、确认订单状态为支付失败 + if (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")) { + sendEmail(orderNo); + }else { + sendEmail(subscriptionInfo.getSubscriptionId(), "fail_renewal"); + } + } + } + + } + @Transactional(rollbackFor = Exception.class) public SubscriptionInfo createSubscription(Subscription subscription){ // 确认当前subscription是否已经记录 @@ -366,7 +442,6 @@ public class StripeServiceImpl implements StripeService { // 从回调信息中获取recurring type SubscriptionItem subscriptionItem = subscription.getItems().getData().get(0); String interval = subscriptionItem.getPrice().getRecurring().getInterval(); - Map paymentMethod = getPaymentMethodByInvoiceId(subscription.getLatestInvoice()); subscriptionInfo = new SubscriptionInfo(); subscriptionInfo.setAccountId(orderInfo.getAccountId()); @@ -374,8 +449,6 @@ public class StripeServiceImpl implements StripeService { subscriptionInfo.setSubscriptionId(subscription.getId()); subscriptionInfo.setType(interval); subscriptionInfo.setStatus(subscription.getStatus()); - subscriptionInfo.setPaymentMethod(paymentMethod.get("paymentMethod")); - subscriptionInfo.setLast4(paymentMethod.get("last4")); subscriptionInfo.setNextPayDate(changeTimeStampFormat(subscription.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy_EEEE)); subscriptionInfo.setCurrentPeriodStart(subscription.getCurrentPeriodStart()); subscriptionInfo.setCurrentPeriodEnd(subscription.getCurrentPeriodEnd()); @@ -406,7 +479,7 @@ public class StripeServiceImpl implements StripeService { } @Transactional(rollbackFor = Exception.class) - public Boolean updateSubscription(Subscription subscription){ + public SubscriptionInfo updateSubscription(Subscription subscription){ // 获取当前是否有已经记录的subscriptionInfo SubscriptionInfo subscriptionInfo = createSubscription(subscription); // 用于标志数据有没有变动,避免在没有改动的情况下频繁的更新数据库 @@ -415,9 +488,6 @@ public class StripeServiceImpl implements StripeService { subscriptionInfo.setStatus(subscription.getStatus()); flag = true; } - if (subscription.getStatus().equals("canceled")){ - subscriptionInfo.setAutoRenewal((byte) 0); - } if (!subscriptionInfo.getCurrentPeriodStart().equals(subscription.getCurrentPeriodStart())){ subscriptionInfo.setCurrentPeriodStart(subscription.getCurrentPeriodStart()); flag = true; @@ -432,10 +502,9 @@ public class StripeServiceImpl implements StripeService { } if (flag){ subscriptionInfo.setUpdateTime(LocalDateTime.now()); - // todo 这里需要再检查支付方式吗? subscriptionInfoMapper.updateById(subscriptionInfo); } - return true; + return subscriptionInfo; } private void updateAccountValidity(Long accountId, Long currentPeriodEnd){ @@ -633,6 +702,7 @@ public class StripeServiceImpl implements StripeService { switch (retrieve.getType()){ case "alipay": paymentMethod = "Alipay"; + last4 = "N/A"; break; case "bancontact": paymentMethod = "BanContact"; @@ -647,17 +717,24 @@ public class StripeServiceImpl implements StripeService { 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); @@ -669,7 +746,7 @@ public class StripeServiceImpl implements StripeService { // return null; } - public boolean sendEmail(String subscriptionId, String type){ + public boolean sendEmail(String subscriptionId, String type) { SubscriptionEmailParamsDTO emailParamsDTO = new SubscriptionEmailParamsDTO(); QueryWrapper qwSI = new QueryWrapper<>(); @@ -688,9 +765,9 @@ public class StripeServiceImpl implements StripeService { if (StringUtil.isNullOrEmpty(type)){ // 如果没有传入type,则使用paymentInfo中记录的类型 // (其实这里也可以通过invoiceId查询stripe,但是记录在自己的db中可以不用每次都查,且方便查看) - type = paymentInfo.getType(); + type = StringUtil.isNullOrEmpty(paymentInfo.getType()) ? "new" : paymentInfo.getType(); } - if (!type.equals("reminder") && paymentInfo.getNotified() == 1){ + if (!type.equals("reminder") && !type.equals("cancel") && paymentInfo.getNotified() == 1){ // 已经邮件通知过,直接返回 return true; } @@ -707,8 +784,10 @@ public class StripeServiceImpl implements StripeService { emailParamsDTO.setTotalFee(paymentInfo.getPayerTotal().toString()); emailParamsDTO.setLastOrderDate(changeTimeStampFormat(subscriptionInfo.getCurrentPeriodStart(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy)); emailParamsDTO.setEndOfPrepaidTerm(changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy)); - emailParamsDTO.setPaymentMethod(subscriptionInfo.getPaymentMethod()); + emailParamsDTO.setPaymentMethod(paymentInfo.getPaymentMethod()); + emailParamsDTO.setLast4(paymentInfo.getLast4()); emailParamsDTO.setSubscriptionId(subscriptionInfo.getId().toString()); + emailParamsDTO.setFailMessage(orderByOrderNo.getNote()); emailParamsDTO.setSubscriptionType(subscriptionInfo.getType()); emailParamsDTO.setStartDate(changeTimeStampFormat(orderByOrderNo.getCreateTime())); emailParamsDTO.setNextPayDate(changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy)); @@ -717,7 +796,7 @@ public class StripeServiceImpl implements StripeService { SendEmailUtil.subscriptionEmailReminder(type, emailParamsDTO, language, account.getUserEmail()); // 邮件通知成功后,更新标志 - if (!type.equals("reminder")){ + if (!type.equals("reminder") && !type.equals("cancel")){ PaymentInfo payment = new PaymentInfo(); payment.setId(paymentInfo.getId()); payment.setNotified(1); @@ -727,6 +806,40 @@ public class StripeServiceImpl implements StripeService { 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()) { + 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()); + + SendEmailUtil.subscriptionEmailReminder("fail_new", emailParamsDTO, language, account.getUserEmail()); + + // 邮件通知成功后,更新标志 + PaymentInfo payment = new PaymentInfo(); + payment.setId(paymentInfo.getId()); + payment.setNotified(1); + payment.setUpdateTime(LocalDateTime.now()); + paymentInfoMapper.updateById(payment); + return true; + } + public void subscriptionReminder(){ // 提前7天的 00:00:00 和 23:59:59 LocalDateTime startOfDay = LocalDateTime.now().plusDays(7).toLocalDate().atStartOfDay(); From 1b15aed6a28801b2a6f8f433f01678d7d19ca35a Mon Sep 17 00:00:00 2001 From: xupei Date: Thu, 28 Nov 2024 10:43:06 +0800 Subject: [PATCH 5/8] =?UTF-8?q?=E6=94=AF=E4=BB=98=E4=BC=98=E5=8C=96-?= =?UTF-8?q?=E7=BB=AD=E8=AE=A2=E5=A4=B1=E8=B4=A5=E9=82=AE=E4=BB=B6=E9=80=9A?= =?UTF-8?q?=E7=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ai/da/common/enums/OrderStatusEnum.java | 9 - .../com/ai/da/common/utils/SendEmailUtil.java | 12 +- .../ai/da/controller/StripeController.java | 42 +++- .../da/mapper/primary/entity/PaymentInfo.java | 3 + .../ai/da/model/dto/ProductPurchaseDTO.java | 2 - .../model/dto/SubscriptionEmailParamsDTO.java | 2 + .../java/com/ai/da/service/StripeService.java | 10 +- .../service/impl/PaymentInfoServiceImpl.java | 1 + .../ai/da/service/impl/StripeServiceImpl.java | 230 ++++++++++++++++-- 9 files changed, 269 insertions(+), 42 deletions(-) diff --git a/src/main/java/com/ai/da/common/enums/OrderStatusEnum.java b/src/main/java/com/ai/da/common/enums/OrderStatusEnum.java index 8f8699a6..02767686 100644 --- a/src/main/java/com/ai/da/common/enums/OrderStatusEnum.java +++ b/src/main/java/com/ai/da/common/enums/OrderStatusEnum.java @@ -10,43 +10,34 @@ public enum OrderStatusEnum { * 未支付 */ NOT_PAY("未支付"), - - /** * 支付成功 */ SUCCESS("支付成功"), - /** * 支付失败 */ FAILURE("支付失败"), - /** * 已关闭 */ TIMEOUT_CLOSED("超时已关闭"), - /** * 已取消 */ CANCEL("用户已取消"), - /** * 退款中 */ REFUND_PROCESSING("退款中"), - /** * 已退款 */ REFUND_SUCCESS("已退款"), - /** * 退款异常 */ REFUND_ABNORMAL("退款异常"), - /** * paypal订单状态为 APPROVED */ 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 c6d61e68..b14cd28a 100644 --- a/src/main/java/com/ai/da/common/utils/SendEmailUtil.java +++ b/src/main/java/com/ai/da/common/utils/SendEmailUtil.java @@ -781,6 +781,8 @@ public class SendEmailUtil { private final static Long RENEWAL_REMINDER_USER_CN = 130728L; private final static Long PAYMENT_FAILED_NEW_MERCHANT_EN = 131230L; private final static Long PAYMENT_FAILED_RENEWAL_MERCHANT_EN = 131225L; + private final static Long PAYMENT_FAILED_RENEWAL_USER_EN = 131563L; + private final static Long PAYMENT_FAILED_RENEWAL_USER_CN = 131564L; public static void subscriptionEmailReminder(String type, SubscriptionEmailParamsDTO subscriptionEmailParamsDTO, String language, String receiverAddress){ try{ @@ -816,6 +818,14 @@ public class SendEmailUtil { case "fail_renewal": merchant.setSubject("[Code-Create] Payment Failed : Renewal Order (" + subscriptionEmailParamsDTO.getOrderId() + ")"); templateMerchant.setTemplateID(PAYMENT_FAILED_RENEWAL_MERCHANT_EN); + // todo to user + if (language.equals("ENGLISH")){ + user.setSubject("[Code-Create] Payment Failed : Renewal Order (" + subscriptionEmailParamsDTO.getOrderId() + ")"); + templateUser.setTemplateID(PAYMENT_FAILED_RENEWAL_USER_EN); + }else { + user.setSubject("[Code-Create] 自动续费失败 (" + subscriptionEmailParamsDTO.getOrderId() + ")"); + templateUser.setTemplateID(PAYMENT_FAILED_RENEWAL_USER_CN); + } break; case "new": merchant.setSubject("[Code-Create] New Order(" + subscriptionEmailParamsDTO.getOrderId() + ")"); @@ -859,7 +869,7 @@ public class SendEmailUtil { templateMerchant.setTemplateData(JSON.toJSONString(subscriptionEmailParamsDTO)); merchant.setTemplate(templateMerchant); - if (!type.equals("cancel") && !type.equals("fail_new") && !type.equals("fail_renewal") ){ + if (!type.equals("cancel") && !type.equals("fail_new") ){ // 返回的resp是一个SendEmailResponse的实例,与请求对象对应 SendEmailResponse respUser = client.SendEmail(user); log.info("邮件主题:{},发送结果toUser###{}", user.getSubject(), SendEmailResponse.toJsonString(respUser)); diff --git a/src/main/java/com/ai/da/controller/StripeController.java b/src/main/java/com/ai/da/controller/StripeController.java index eac49851..8becf26b 100644 --- a/src/main/java/com/ai/da/controller/StripeController.java +++ b/src/main/java/com/ai/da/controller/StripeController.java @@ -9,19 +9,22 @@ import com.stripe.exception.StripeException; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; -import org.simpleframework.xml.core.Validate; import org.springframework.web.bind.annotation.*; +import springfox.documentation.annotations.ApiIgnore; import javax.annotation.Resource; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; import java.io.IOException; +import java.util.List; @Api(tags = "Stripe模块") @Slf4j @RestController @RequestMapping("/api/stripe") +@ApiIgnore public class StripeController { @Resource @@ -29,7 +32,7 @@ public class StripeController { @ApiOperation("创建支付链接") @PostMapping("/createOrder") - public Response pay(@Validate @RequestBody ProductPurchaseDTO productPurchaseDTO) { + public Response pay(@Valid @RequestBody ProductPurchaseDTO productPurchaseDTO) { return Response.success(stripeService.pay(productPurchaseDTO)); } @@ -45,7 +48,7 @@ public class StripeController { } @ApiOperation("申请退款") - @PostMapping("/trade/refund/{orderNo}/{reason}") + @GetMapping("/trade/refund/{orderNo}/{reason}") public Response> refund(@PathVariable String orderNo, @PathVariable String reason) throws IOException { String response = stripeService.refund(null,orderNo,reason); if (response.equals("退款成功")){ @@ -56,19 +59,44 @@ public class StripeController { } @ApiOperation("获取订阅") - @PostMapping("/getSubscription") - public void getSubscription() { + @GetMapping("/getSubscription") + public Response> getSubscription(@RequestParam String name, @RequestParam String email) { try { - stripeService.getSubscription("xp", "xupei3360@163.com"); + return Response.success(stripeService.getSubscriptionIds(name, email)); } catch (StripeException e) { throw new RuntimeException(e); } } @ApiOperation("取消订阅") - @PostMapping("/cancelSubscription") + @GetMapping("/cancelSubscription") public Response cancelSubscription(@RequestParam String subscriptionId) { stripeService.cancelSubscription(subscriptionId); return Response.success("success"); } + @ApiOperation("临时 取消订阅") + @GetMapping("/cancelSubscriptionTemp") + public Response cancelSubscriptionTemp(@RequestParam String subscriptionId) { + stripeService.cancelSubscriptionTemp(subscriptionId); + return Response.success("success"); + } + + @ApiOperation("创建订阅 临时") + @GetMapping("/createSubscriptionTemp") + public Response createSubscriptionTemp(@RequestParam String name, @RequestParam String email) { + return Response.success(stripeService.createSubscriptionTemp(name, email)); + } + + @ApiOperation("修改用户默认支付方式 临时") + @GetMapping("/changeCustomerPayment") + public Response changeCustomerPayment(@RequestParam String name, @RequestParam String email) { + return Response.success(stripeService.changeCustomerPayment(name, email)); + } + + @ApiOperation("临时 发送续订失败邮件") + @GetMapping("/sendRenewalFailEmail") + public Response sendRenewalFailEmail(@RequestParam String invoiceId, @RequestParam String subscriptionId, @RequestParam String orderNo) { + return Response.success(stripeService.sendRenewalFailEmail(invoiceId, subscriptionId,orderNo)); + } + } diff --git a/src/main/java/com/ai/da/mapper/primary/entity/PaymentInfo.java b/src/main/java/com/ai/da/mapper/primary/entity/PaymentInfo.java index 8172f4bd..d3533078 100644 --- a/src/main/java/com/ai/da/mapper/primary/entity/PaymentInfo.java +++ b/src/main/java/com/ai/da/mapper/primary/entity/PaymentInfo.java @@ -30,4 +30,7 @@ public class PaymentInfo extends BaseEntity{ private String paymentMethod; private String last4; + + // 发票托管页面 + private String hostedInvoiceUrl; } diff --git a/src/main/java/com/ai/da/model/dto/ProductPurchaseDTO.java b/src/main/java/com/ai/da/model/dto/ProductPurchaseDTO.java index ce1fb595..0e386e8a 100644 --- a/src/main/java/com/ai/da/model/dto/ProductPurchaseDTO.java +++ b/src/main/java/com/ai/da/model/dto/ProductPurchaseDTO.java @@ -27,6 +27,4 @@ public class ProductPurchaseDTO { @ApiModelProperty("是否自动续订 one_time || recurring") private Boolean autoRenewal; - - private String refId; } 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 9ecf0241..1f35b705 100644 --- a/src/main/java/com/ai/da/model/dto/SubscriptionEmailParamsDTO.java +++ b/src/main/java/com/ai/da/model/dto/SubscriptionEmailParamsDTO.java @@ -48,5 +48,7 @@ public class SubscriptionEmailParamsDTO { // 付款失败原因 private String failMessage; + private String accountPageRef; + } diff --git a/src/main/java/com/ai/da/service/StripeService.java b/src/main/java/com/ai/da/service/StripeService.java index b517b325..268f08d2 100644 --- a/src/main/java/com/ai/da/service/StripeService.java +++ b/src/main/java/com/ai/da/service/StripeService.java @@ -2,7 +2,6 @@ package com.ai.da.service; import com.ai.da.model.dto.ProductPurchaseDTO; import com.stripe.exception.StripeException; -import com.stripe.model.Subscription; import javax.servlet.http.HttpServletRequest; import java.util.List; @@ -18,10 +17,12 @@ public interface StripeService { void checkOrderStatus(String orderNo); - List getSubscription(String name, String userEmail) throws StripeException; + List getSubscriptionIds(String name, String userEmail) throws StripeException; void cancelSubscription(String orderNo); + void cancelSubscriptionTemp(String subscriptionId); + Map getPaymentMethod(String paymentMethodId); /*void updateSubscription(String subscriptionId); @@ -32,4 +33,9 @@ public interface StripeService { void checkSubscriptionExpiration(); + String createSubscriptionTemp(String name, String email); + + String changeCustomerPayment(String name, String email); + + boolean sendRenewalFailEmail(String invoiceId, String subscriptionId, String orderNo); } diff --git a/src/main/java/com/ai/da/service/impl/PaymentInfoServiceImpl.java b/src/main/java/com/ai/da/service/impl/PaymentInfoServiceImpl.java index 204a9ce4..3459bf93 100644 --- a/src/main/java/com/ai/da/service/impl/PaymentInfoServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/PaymentInfoServiceImpl.java @@ -236,6 +236,7 @@ public class PaymentInfoServiceImpl extends ServiceImpl getSubscription(String username, String userEmail) { Stripe.apiKey = privateKey; String customerId = null; @@ -642,6 +663,25 @@ public class StripeServiceImpl implements StripeService { } 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); + } } @@ -747,7 +787,6 @@ public class StripeServiceImpl implements StripeService { } public boolean sendEmail(String subscriptionId, String type) { - SubscriptionEmailParamsDTO emailParamsDTO = new SubscriptionEmailParamsDTO(); QueryWrapper qwSI = new QueryWrapper<>(); qwSI.eq("subscription_id", subscriptionId); @@ -784,14 +823,7 @@ public class StripeServiceImpl implements StripeService { emailParamsDTO.setTotalFee(paymentInfo.getPayerTotal().toString()); emailParamsDTO.setLastOrderDate(changeTimeStampFormat(subscriptionInfo.getCurrentPeriodStart(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy)); emailParamsDTO.setEndOfPrepaidTerm(changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy)); - emailParamsDTO.setPaymentMethod(paymentInfo.getPaymentMethod()); - emailParamsDTO.setLast4(paymentInfo.getLast4()); - emailParamsDTO.setSubscriptionId(subscriptionInfo.getId().toString()); - emailParamsDTO.setFailMessage(orderByOrderNo.getNote()); - emailParamsDTO.setSubscriptionType(subscriptionInfo.getType()); - emailParamsDTO.setStartDate(changeTimeStampFormat(orderByOrderNo.getCreateTime())); - emailParamsDTO.setNextPayDate(changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy)); - emailParamsDTO.setRenewalTime(changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy)); + setSubscriptionParams(paymentInfo, subscriptionInfo, orderByOrderNo, emailParamsDTO); SendEmailUtil.subscriptionEmailReminder(type, emailParamsDTO, language, account.getUserEmail()); @@ -840,6 +872,79 @@ public class StripeServiceImpl implements StripeService { 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); + // todo + emailParamsDTO.setAccountPageRef("\"https://www.aida.com.hk/home/homePage\""); + + // 4、发邮件 + SendEmailUtil.subscriptionEmailReminder("fail_renewal", emailParamsDTO, language, account.getUserEmail()); + + 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) { + emailParamsDTO.setPaymentMethod(paymentInfo.getPaymentMethod()); + emailParamsDTO.setLast4(paymentInfo.getLast4()); + emailParamsDTO.setSubscriptionId(subscriptionInfo.getId().toString()); + emailParamsDTO.setFailMessage(orderByOrderNo.getNote()); + emailParamsDTO.setSubscriptionType(subscriptionInfo.getType()); + emailParamsDTO.setStartDate(changeTimeStampFormat(orderByOrderNo.getCreateTime())); + emailParamsDTO.setNextPayDate(changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy)); + emailParamsDTO.setRenewalTime(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(); @@ -876,4 +981,87 @@ public class StripeServiceImpl implements StripeService { } } + // todo 新建一个订阅 使用不会成功的付款方式 + + public String createSubscriptionTemp(String name, String email){ + Stripe.apiKey = privateKey; + try { + OrderInfo orderInfo = orderInfoService.createOrderByProductId(1, PayTypeEnum.STRIPE.getType(), ProductEnum.DailySubscription); + + 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); + } + } + } From 47ca7bde419ed0df415e1b2769ccbf4720ae8ced Mon Sep 17 00:00:00 2001 From: xupei Date: Mon, 9 Dec 2024 16:53:29 +0800 Subject: [PATCH 6/8] =?UTF-8?q?Affiliate-=E6=96=B0=E5=A2=9E=E3=80=81?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2=E3=80=81=E4=BD=A3=E9=87=91=E8=AE=A1=E7=AE=97?= =?UTF-8?q?=E7=AD=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ai/da/common/constant/CommonConstant.java | 2 + .../com/ai/da/common/utils/RedisUtil.java | 14 ++ .../com/ai/da/common/utils/SendEmailUtil.java | 120 ++++++++--- .../ai/da/controller/AffiliateController.java | 63 ++++++ .../ai/da/controller/ElementController.java | 2 + .../ai/da/controller/StripeController.java | 6 + .../ai/da/mapper/primary/AffiliateMapper.java | 7 + .../ai/da/mapper/primary/entity/Account.java | 3 + .../da/mapper/primary/entity/Affiliate.java | 28 +++ .../da/mapper/primary/entity/OrderInfo.java | 5 + .../da/model/dto/AffiliateEmailParamsDTO.java | 31 +++ .../ai/da/model/dto/AffiliateQueryDTO.java | 13 ++ .../dto/GenerateThroughImageTextDTO.java | 31 +-- .../com/ai/da/model/dto/TimeQueryBaseDTO.java | 19 ++ .../java/com/ai/da/model/vo/AffiliateVO.java | 15 ++ .../com/ai/da/service/AffiliateService.java | 22 ++ .../java/com/ai/da/service/StripeService.java | 4 + .../da/service/impl/AffiliateServiceImpl.java | 189 ++++++++++++++++++ .../impl/CollectionElementServiceImpl.java | 4 +- .../service/impl/PaymentInfoServiceImpl.java | 12 +- .../ai/da/service/impl/StripeServiceImpl.java | 28 ++- 21 files changed, 561 insertions(+), 57 deletions(-) create mode 100644 src/main/java/com/ai/da/controller/AffiliateController.java create mode 100644 src/main/java/com/ai/da/mapper/primary/AffiliateMapper.java create mode 100644 src/main/java/com/ai/da/mapper/primary/entity/Affiliate.java create mode 100644 src/main/java/com/ai/da/model/dto/AffiliateEmailParamsDTO.java create mode 100644 src/main/java/com/ai/da/model/dto/AffiliateQueryDTO.java create mode 100644 src/main/java/com/ai/da/model/dto/TimeQueryBaseDTO.java create mode 100644 src/main/java/com/ai/da/model/vo/AffiliateVO.java create mode 100644 src/main/java/com/ai/da/service/AffiliateService.java create mode 100644 src/main/java/com/ai/da/service/impl/AffiliateServiceImpl.java diff --git a/src/main/java/com/ai/da/common/constant/CommonConstant.java b/src/main/java/com/ai/da/common/constant/CommonConstant.java index e495ee26..cd019602 100644 --- a/src/main/java/com/ai/da/common/constant/CommonConstant.java +++ b/src/main/java/com/ai/da/common/constant/CommonConstant.java @@ -79,5 +79,7 @@ public class CommonConstant { public static final String TIME_FORMAT_MMM_dd_yyyy = "MMM. dd, yyyy"; + public static final String AFFILIATE_LINK = ""; + } 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 7d5295dc..d3ba2a93 100644 --- a/src/main/java/com/ai/da/common/utils/RedisUtil.java +++ b/src/main/java/com/ai/da/common/utils/RedisUtil.java @@ -278,4 +278,18 @@ public class RedisUtil { // 设置过期时间为 5 分钟(300 秒) redisTemplate.expire(redisKey, 5, TimeUnit.MINUTES); } + + 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); + } } 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 b14cd28a..490b598f 100644 --- a/src/main/java/com/ai/da/common/utils/SendEmailUtil.java +++ b/src/main/java/com/ai/da/common/utils/SendEmailUtil.java @@ -2,6 +2,7 @@ package com.ai.da.common.utils; 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.SubscriptionEmailParamsDTO; import com.alibaba.fastjson.JSONObject; import com.ai.da.common.config.exception.BusinessException; @@ -177,7 +178,7 @@ public class SendEmailUtil { subject = "Approval Confirmation for AiDA System Trial Access"; if (country.equals("China")) { template.setTemplateID(NOTIFICATION_CHINESE_TEMPLATE_ID); - }else { + } else { template.setTemplateID(NOTIFICATION_TEMPLATE_ID); } template.setTemplateData(buildNotificationData(trialOrder, link)); @@ -227,7 +228,7 @@ public class SendEmailUtil { attachment.setFileName(fileName); // 设置附件文件名 // 设置附件内容 attachment.setContent(Base64.getEncoder().encodeToString(fileContent)); - req.setAttachments(new Attachment[] {attachment}); + req.setAttachments(new Attachment[]{attachment}); // 发送邮件 SendEmailResponse resp = client.SendEmail(req); log.info("短信发送结果res###{}", SendEmailResponse.toJsonString(resp)); @@ -270,7 +271,9 @@ public class SendEmailUtil { throw new BusinessException("failed.to.send.mail"); } } + private final static Long WILLBEEXPIRED_TEMPLATE_ID = 118178L; + public static void sendWillBeExpiredEmail(Account account, String senderAddress) { try { // 实例化一个认证对象 @@ -362,7 +365,7 @@ public class SendEmailUtil { jsonObject.put("email", trialOrder.getEmail()); if (link) { jsonObject.put("days", 14); - }else { + } else { jsonObject.put("days", 5); } return jsonObject.toJSONString(); @@ -372,6 +375,7 @@ public class SendEmailUtil { private final static Long UPGRADE_SUCCESS_NOTIFICATION_ID = 118856L; private final static Long UPGRADE_NOTIFICATION_ID_CHINESE = 122898L; private final static Long UPGRADE_SUCCESS_NOTIFICATION_ID_CHINESE = 122899L; + public static void sendUpgradeNotification(Account account, String senderAddress, Integer type) { try { // 实例化一个认证对象 @@ -394,7 +398,7 @@ public class SendEmailUtil { if (type == 1) { subject = "Upcoming System Upgrade for AiDA 3.0"; template.setTemplateID(UPGRADE_NOTIFICATION_ID); - }else { + } else { subject = "即将到来的AiDA 3.0系统升级"; template.setTemplateID(UPGRADE_NOTIFICATION_ID_CHINESE); } @@ -420,7 +424,8 @@ public class SendEmailUtil { } private final static Long GENERATE_EXCEPTION_WARNING_ID = 122589L; - public static void sendGenerateExceptionWarning(String message){ + + public static void sendGenerateExceptionWarning(String message) { try { // 实例化一个认证对象 Credential cred = new Credential(SECRET_ID, SECRET_KEy); @@ -459,7 +464,8 @@ public class SendEmailUtil { private final static Long QUESTIONNAIRE_FEEDBACK_EN_ID = 124151L; private final static Long QUESTIONNAIRE_FEEDBACK_CN_ID = 124156L; - public static void questionnaireRelatedNotify(String userName, String email, String language){ + + public static void questionnaireRelatedNotify(String userName, String email, String language) { try { // 实例化一个认证对象 Credential cred = new Credential(SECRET_ID, SECRET_KEy); @@ -504,7 +510,7 @@ public class SendEmailUtil { private final static Long RENEWAL_NOTIFICATION_FOR_OLD_USER_EN = 124892L; private final static Long RENEWAL_NOTIFICATION_FOR_OLD_USER_CN = 124891L; - public static void notificationForPaidUser(String receiverAddress, int emailType, String country, String userName, String date){ + public static void notificationForPaidUser(String receiverAddress, int emailType, String country, String userName, String date) { try { // 实例化一个认证对象 Credential cred = new Credential(SECRET_ID, SECRET_KEy); @@ -527,7 +533,7 @@ public class SendEmailUtil { subject = "Welcome to AiDA!"; if (country.equals("China")) { template.setTemplateID(NEW_USER_PAYMENT_NOTIFICATION_CN); - }else { + } else { template.setTemplateID(NEW_USER_PAYMENT_NOTIFICATION_EN); } parameter.put("userName", userName); @@ -538,7 +544,7 @@ public class SendEmailUtil { subject = "Account renewal notification"; if (country.equals("China")) { template.setTemplateID(RENEWAL_NOTIFICATION_FOR_OLD_USER_CN); - }else { + } else { template.setTemplateID(RENEWAL_NOTIFICATION_FOR_OLD_USER_EN); } break; @@ -599,8 +605,8 @@ public class SendEmailUtil { private final static Long NEW_USER_REGISTER_NOTIFICATION_EN = 126919L; - public static void notificationForRegisterUser(String receiverAddress){ - try{ + public static void notificationForRegisterUser(String receiverAddress) { + try { // 实例化一个认证对象,入参需要传入腾讯云账户 SecretId 和 SecretKey,此处还需注意密钥对的保密 // 代码泄露可能会导致 SecretId 和 SecretKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考,建议采用更安全的方式来使用密钥,请参见:https://cloud.tencent.com/document/product/1278/85305 // 密钥可前往官网控制台 https://console.cloud.tencent.com/cam/capi 进行获取 @@ -636,8 +642,8 @@ 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{ + 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 进行获取 @@ -655,10 +661,10 @@ public class SendEmailUtil { req.setFromEmailAddress(SEND_ADDRESS); req.setDestination(new String[]{receiverAddress}); Template template = new Template(); - if (language.equals("ENGLISH")){ + if (language.equals("ENGLISH")) { req.setSubject("Change the email address bound to the AiDA account"); template.setTemplateID(CHANGE_MAILBOX_CONFIRM_EN); - }else { + } else { req.setSubject("更换AiDA账号绑定的邮箱地址"); template.setTemplateID(CHANGE_MAILBOX_CONFIRM_CN); } @@ -680,12 +686,12 @@ public class SendEmailUtil { private final static Long UPLOAD_TIMEOUT_REMINDER = 128324L; - public static void uploadTimeoutReminder(String userName, String time){ + public static 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"; - try{ + try { // 实例化一个认证对象,入参需要传入腾讯云账户 SecretId 和 SecretKey,此处还需注意密钥对的保密 // 代码泄露可能会导致 SecretId 和 SecretKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考,建议采用更安全的方式来使用密钥,请参见:https://cloud.tencent.com/document/product/1278/85305 // 密钥可前往官网控制台 https://console.cloud.tencent.com/cam/capi 进行获取 @@ -723,6 +729,7 @@ public class SendEmailUtil { 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) { try { // 实例化一个认证对象 @@ -752,7 +759,7 @@ public class SendEmailUtil { if (type == 1) { subject = "AiDA workshop - Win a trip to Hong Kong!"; template.setTemplateID(HALFPRICEPROMOTION_EN_ID); - }else { + } else { subject = "AiDA workshop - 赢取香港之旅"; template.setTemplateID(HALFPRICEPROMOTION_CN_ID); } @@ -784,8 +791,8 @@ public class SendEmailUtil { private final static Long PAYMENT_FAILED_RENEWAL_USER_EN = 131563L; private final static Long PAYMENT_FAILED_RENEWAL_USER_CN = 131564L; - public static void subscriptionEmailReminder(String type, SubscriptionEmailParamsDTO subscriptionEmailParamsDTO, String language, String receiverAddress){ - try{ + public static void subscriptionEmailReminder(String type, SubscriptionEmailParamsDTO subscriptionEmailParamsDTO, String language, String receiverAddress) { + try { // String merchantEmail = "kimwong@code-create.com.hk"; String merchantEmail = "xupei3360@163.com"; Credential cred = new Credential(SECRET_ID, SECRET_KEy); @@ -819,10 +826,10 @@ public class SendEmailUtil { merchant.setSubject("[Code-Create] Payment Failed : Renewal Order (" + subscriptionEmailParamsDTO.getOrderId() + ")"); templateMerchant.setTemplateID(PAYMENT_FAILED_RENEWAL_MERCHANT_EN); // todo to user - if (language.equals("ENGLISH")){ + if (language.equals("ENGLISH")) { user.setSubject("[Code-Create] Payment Failed : Renewal Order (" + subscriptionEmailParamsDTO.getOrderId() + ")"); templateUser.setTemplateID(PAYMENT_FAILED_RENEWAL_USER_EN); - }else { + } else { user.setSubject("[Code-Create] 自动续费失败 (" + subscriptionEmailParamsDTO.getOrderId() + ")"); templateUser.setTemplateID(PAYMENT_FAILED_RENEWAL_USER_CN); } @@ -830,10 +837,10 @@ public class SendEmailUtil { case "new": merchant.setSubject("[Code-Create] New Order(" + subscriptionEmailParamsDTO.getOrderId() + ")"); templateMerchant.setTemplateID(NEW_MERCHANT_EN); - if (language.equals("ENGLISH")){ + if (language.equals("ENGLISH")) { user.setSubject("[Code-Create] You have successfully subscribed to AiDA"); templateUser.setTemplateID(NEW_USER_EN); - }else { + } else { user.setSubject("[Code-Create] 您已成功订阅AiDA"); templateUser.setTemplateID(NEW_USER_CN); } @@ -841,19 +848,19 @@ public class SendEmailUtil { case "renewal": merchant.setSubject("[Code-Create] New subscription renewal order (" + subscriptionEmailParamsDTO.getOrderId() + ")"); templateMerchant.setTemplateID(RENEWAL_MERCHANT_EN); - if (language.equals("ENGLISH")){ + if (language.equals("ENGLISH")) { user.setSubject("[Code-Create] AiDA Renewal Successful"); templateUser.setTemplateID(RENEWAL_USER_EN); - }else { + } else { user.setSubject("[Code-Create] AiDA续订成功"); templateUser.setTemplateID(RENEWAL_USER_CN); } break; case "reminder": - if (language.equals("ENGLISH")){ + if (language.equals("ENGLISH")) { user.setSubject("[Code-Create] AiDA Subscription Renewal Reminder"); templateUser.setTemplateID(RENEWAL_REMINDER_USER_EN); - }else { + } else { user.setSubject("[Code-Create] AiDA续订提醒"); templateUser.setTemplateID(RENEWAL_REMINDER_USER_CN); } @@ -869,12 +876,12 @@ public class SendEmailUtil { templateMerchant.setTemplateData(JSON.toJSONString(subscriptionEmailParamsDTO)); merchant.setTemplate(templateMerchant); - if (!type.equals("cancel") && !type.equals("fail_new") ){ + if (!type.equals("cancel") && !type.equals("fail_new")) { // 返回的resp是一个SendEmailResponse的实例,与请求对象对应 SendEmailResponse respUser = client.SendEmail(user); log.info("邮件主题:{},发送结果toUser###{}", user.getSubject(), SendEmailResponse.toJsonString(respUser)); } - if (!type.equals("reminder")){ + if (!type.equals("reminder")) { SendEmailResponse respMerchant = client.SendEmail(merchant); log.info("邮件主题:{},发送结果toMerchant###{}", merchant.getSubject(), SendEmailResponse.toJsonString(respMerchant)); } @@ -883,4 +890,57 @@ public class SendEmailUtil { throw new BusinessException("failed.to.send.mail"); } } + + private final static Long NEW_REGISTRATION = 132123L; + private final static Long AFFILIATE_ACCEPTED = 132124L; + private final static Long AFFILIATE_REFUSED = 132125L; + private final static Long AFFILIATE_MONTHLY_SUMMARY = 132126L; + + public static void affiliateEmailReminder(String receiverAddress, AffiliateEmailParamsDTO paramsDTO, String type) { + try { + 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(); + switch (type) { + case "new": + req.setSubject("New Affiliate Registration"); + template.setTemplateID(NEW_REGISTRATION); + break; + case "accepted": + req.setSubject("Affiliate Application Accepted"); + template.setTemplateID(AFFILIATE_ACCEPTED); + break; + case "refused": + req.setSubject("Affiliate Application Refused"); + template.setTemplateID(AFFILIATE_REFUSED); + break; + case "summary": + req.setSubject("Your Monthly AffiliateWP Summary for AiDA"); + template.setTemplateID(AFFILIATE_MONTHLY_SUMMARY); + break; + } + + template.setTemplateData(JSON.toJSONString(paramsDTO)); + 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"); + } + } + } diff --git a/src/main/java/com/ai/da/controller/AffiliateController.java b/src/main/java/com/ai/da/controller/AffiliateController.java new file mode 100644 index 00000000..4e55ce71 --- /dev/null +++ b/src/main/java/com/ai/da/controller/AffiliateController.java @@ -0,0 +1,63 @@ +package com.ai.da.controller; + + +import com.ai.da.common.response.Response; +import com.ai.da.mapper.primary.entity.Affiliate; +import com.ai.da.model.dto.AffiliateQueryDTO; +import com.ai.da.model.vo.AffiliateVO; +import com.ai.da.service.AffiliateService; +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +@Slf4j +@RestController +@RequestMapping("/api/affiliate") +public class AffiliateController { + + @Resource + private AffiliateService affiliateService; + + @ApiOperation(value = "注册成为affiliate") + @GetMapping("/registration") + public Response completeGuidance(@RequestParam("promotionMethod") String promotionMethod) { + return Response.success(affiliateService.registerAsAnAffiliate(promotionMethod)); + } + + @ApiOperation(value = "获取affiliate列表") + @PostMapping("/list") + public Response> getAffiliateList(@Valid @RequestBody AffiliateQueryDTO affiliateQueryDTO) { + return Response.success(affiliateService.getAffiliateList(affiliateQueryDTO)); + } + + @ApiOperation(value = "获取affiliate个人中心") + @GetMapping("/personalCenter") + public Response personalAffiliateCenter() { + return Response.success(affiliateService.personalAffiliateCenter()); + } + + @ApiOperation(value = "审批affiliate申请") + @GetMapping("/approval") + public Response applicationApproval(@RequestParam("id") Long id, @RequestParam("isApproved")Boolean isApproved) { + return Response.success(affiliateService.applicationApproval(id, isApproved)); + } + + @ApiOperation(value = "审批affiliate申请") + @GetMapping("/testTask") + public Response testTask() { + affiliateService.updateAffiliateInfoWithPayment(); + return Response.success("success "); + } + + @ApiOperation(value = "affiliate链接浏览量增加") + @GetMapping("/viewsIncrease") + public Response viewsGet(@RequestParam("id") Long id) { + return Response.success(affiliateService.affiliateLinkViewsIncrease(id)); + } + + +} diff --git a/src/main/java/com/ai/da/controller/ElementController.java b/src/main/java/com/ai/da/controller/ElementController.java index 40dbcde1..8065c6dc 100644 --- a/src/main/java/com/ai/da/controller/ElementController.java +++ b/src/main/java/com/ai/da/controller/ElementController.java @@ -66,6 +66,8 @@ public class ElementController { return Response.success(); } + /** 该功能已删除 */ + @Deprecated @ApiOperation(value = "生成印花") @PostMapping("/generatePrint") public Response generatePrint(@Valid @RequestBody CollectionGeneratePrintDTO generatePrintDTO) { diff --git a/src/main/java/com/ai/da/controller/StripeController.java b/src/main/java/com/ai/da/controller/StripeController.java index 8becf26b..21c91a86 100644 --- a/src/main/java/com/ai/da/controller/StripeController.java +++ b/src/main/java/com/ai/da/controller/StripeController.java @@ -99,4 +99,10 @@ public class StripeController { return Response.success(stripeService.sendRenewalFailEmail(invoiceId, subscriptionId,orderNo)); } + @ApiOperation("临时 查询指定用户绑定的付款方式") + @GetMapping("/getCustomerPaymentMethod") + public Response getCustomerPaymentMethod(@RequestParam String name, @RequestParam String email) { + return Response.success(stripeService.getCustomerPaymentMethod(name, email)); + } + } diff --git a/src/main/java/com/ai/da/mapper/primary/AffiliateMapper.java b/src/main/java/com/ai/da/mapper/primary/AffiliateMapper.java new file mode 100644 index 00000000..314a0952 --- /dev/null +++ b/src/main/java/com/ai/da/mapper/primary/AffiliateMapper.java @@ -0,0 +1,7 @@ +package com.ai.da.mapper.primary; + +import com.ai.da.mapper.primary.entity.Affiliate; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +public interface AffiliateMapper extends BaseMapper { +} diff --git a/src/main/java/com/ai/da/mapper/primary/entity/Account.java b/src/main/java/com/ai/da/mapper/primary/entity/Account.java index 285acd90..60fcc9f0 100644 --- a/src/main/java/com/ai/da/mapper/primary/entity/Account.java +++ b/src/main/java/com/ai/da/mapper/primary/entity/Account.java @@ -104,4 +104,7 @@ public class Account implements Serializable { * 头像 */ private String avatar; + + private Long invitationCode; + } diff --git a/src/main/java/com/ai/da/mapper/primary/entity/Affiliate.java b/src/main/java/com/ai/da/mapper/primary/entity/Affiliate.java new file mode 100644 index 00000000..56204d75 --- /dev/null +++ b/src/main/java/com/ai/da/mapper/primary/entity/Affiliate.java @@ -0,0 +1,28 @@ +package com.ai.da.mapper.primary.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +@Data +@TableName("t_affiliate") +public class Affiliate extends BaseEntity{ + + private Long accountId; + + // Active(活跃) || Inactive(过期) || Pending(待审批) || Refused(拒绝) + private String status; + + private Float totalEarnings = 0.00F; + + private Float monthlyEarnings = 0.00F; + + private Float unpaidEarnings = 0.00F; + + private Integer visits = 0; + + private Boolean approved = false; + + private String link; +} 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 e5bc0a98..04a9d894 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 @@ -24,4 +24,9 @@ public class OrderInfo extends BaseEntity{ private String note; private String paymentType;//支付方式 + + // 可用于标记用户订单是否首次订阅 + private byte isFirstSubscription = 0; + + private byte isCommissionCalculated = 0; } diff --git a/src/main/java/com/ai/da/model/dto/AffiliateEmailParamsDTO.java b/src/main/java/com/ai/da/model/dto/AffiliateEmailParamsDTO.java new file mode 100644 index 00000000..38c87b72 --- /dev/null +++ b/src/main/java/com/ai/da/model/dto/AffiliateEmailParamsDTO.java @@ -0,0 +1,31 @@ +package com.ai.da.model.dto; + +import lombok.Data; + +@Data +public class AffiliateEmailParamsDTO { + + private String username; + + private String promotionMethod; + + private String totalProgramRevenue; + + private String newApprovedAffiliates; + + private String unpaidEarnings; + + private String paidEarnings; + + public AffiliateEmailParamsDTO() { + } + + public AffiliateEmailParamsDTO(String username) { + this.username = username; + } + + public AffiliateEmailParamsDTO(String username, String promotionMethod) { + this.username = username; + this.promotionMethod = promotionMethod; + } +} diff --git a/src/main/java/com/ai/da/model/dto/AffiliateQueryDTO.java b/src/main/java/com/ai/da/model/dto/AffiliateQueryDTO.java new file mode 100644 index 00000000..5498964b --- /dev/null +++ b/src/main/java/com/ai/da/model/dto/AffiliateQueryDTO.java @@ -0,0 +1,13 @@ +package com.ai.da.model.dto; + +import io.swagger.annotations.ApiModel; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +@Data +@ApiModel("查询affiliate列表") +public class AffiliateQueryDTO extends TimeQueryBaseDTO{ + + private String status; +} diff --git a/src/main/java/com/ai/da/model/dto/GenerateThroughImageTextDTO.java b/src/main/java/com/ai/da/model/dto/GenerateThroughImageTextDTO.java index 8dbeca50..772cb8cf 100644 --- a/src/main/java/com/ai/da/model/dto/GenerateThroughImageTextDTO.java +++ b/src/main/java/com/ai/da/model/dto/GenerateThroughImageTextDTO.java @@ -12,49 +12,50 @@ import javax.validation.constraints.NotNull; public class GenerateThroughImageTextDTO { @NotNull(message = "userId cannot be empty") @ApiModelProperty("用户id") - Long userId; + private Long userId; @ApiModelProperty("caption | prompt") - String text; + private String text; @ApiModelProperty("图片在t_collection_element表中的id") - Long collectionElementId; + private Long collectionElementId; // todo 后续取消这个字段的传输,由后端自行判断相关参数是否有值 // @NotBlank(message = "you have to choose the generate type") @ApiModelProperty("text image text-image") - String generateType; + private String generateType; @ApiModelProperty("图片来源:update,从library中选择,从toProductImage结果中选择 collection || library || productImage") - String designType; + private String designType; @NotBlank(message = "level1Type cannot be empty!") @ApiModelProperty("Moodboard Printboard Sketchboard MarketingSketch") - String level1Type; + private String level1Type; @ApiModelProperty("Outwear Dress Blouse Skirt Trousers || Logo Slogan Pattern") - String level2Type; + private String level2Type; @ApiModelProperty("性别") - String gender; + private String gender; - @ApiModelProperty("选择的模型名") - String version; + + @ApiModelProperty("选择的模型名 high || fast") + private String version; @NotBlank(message = "timeZone cannot be empty!") @ApiModelProperty("本地时区,比如 'Asia/Tokyo' 东京时间 , 'Asia/Shanghai' 北京时间 由js本地获取") - String timeZone; + private String timeZone; @ApiModelProperty("唯一id,用于保持消息唯一性") - String uniqueId; + private String uniqueId; @NotNull(message = "Please check if the required fields are empty.(isTestUser)") @ApiModelProperty("是否是测试用户") - Boolean isTestUser; + private Boolean isTestUser; @ApiModelProperty("页面上用户设计的slogan所截的图片") - String sloganBase64; + private String sloganBase64; @ApiModelProperty("种子 取值范围 0~500") - String seed; + private String seed; } diff --git a/src/main/java/com/ai/da/model/dto/TimeQueryBaseDTO.java b/src/main/java/com/ai/da/model/dto/TimeQueryBaseDTO.java new file mode 100644 index 00000000..3c10297b --- /dev/null +++ b/src/main/java/com/ai/da/model/dto/TimeQueryBaseDTO.java @@ -0,0 +1,19 @@ +package com.ai.da.model.dto; + +import com.ai.da.model.vo.PageQueryBaseVo; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +@Data +@ApiModel("按时间查询") +public class TimeQueryBaseDTO extends PageQueryBaseVo { + + @ApiModelProperty("按时间区间查询 区间起点") + private String startTime; + + @ApiModelProperty("按时间区间查询 区间终点") + private String endTime; +} diff --git a/src/main/java/com/ai/da/model/vo/AffiliateVO.java b/src/main/java/com/ai/da/model/vo/AffiliateVO.java new file mode 100644 index 00000000..96177e1a --- /dev/null +++ b/src/main/java/com/ai/da/model/vo/AffiliateVO.java @@ -0,0 +1,15 @@ +package com.ai.da.model.vo; + +import com.ai.da.mapper.primary.entity.Affiliate; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class AffiliateVO extends Affiliate { + + private Long linkViewCount; + +} diff --git a/src/main/java/com/ai/da/service/AffiliateService.java b/src/main/java/com/ai/da/service/AffiliateService.java new file mode 100644 index 00000000..ed340ae4 --- /dev/null +++ b/src/main/java/com/ai/da/service/AffiliateService.java @@ -0,0 +1,22 @@ +package com.ai.da.service; + +import com.ai.da.mapper.primary.entity.Affiliate; +import com.ai.da.model.dto.AffiliateQueryDTO; +import com.ai.da.model.vo.AffiliateVO; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.service.IService; + +public interface AffiliateService extends IService { + + Boolean registerAsAnAffiliate(String promotionMethod); + + IPage getAffiliateList(AffiliateQueryDTO affiliateQueryDTO); + + AffiliateVO personalAffiliateCenter(); + + Boolean applicationApproval(Long id, Boolean isApproved); + + void updateAffiliateInfoWithPayment(); + + Boolean affiliateLinkViewsIncrease(Long id); +} diff --git a/src/main/java/com/ai/da/service/StripeService.java b/src/main/java/com/ai/da/service/StripeService.java index 268f08d2..f6f60dc3 100644 --- a/src/main/java/com/ai/da/service/StripeService.java +++ b/src/main/java/com/ai/da/service/StripeService.java @@ -19,6 +19,8 @@ public interface StripeService { List getSubscriptionIds(String name, String userEmail) throws StripeException; + Map getPaymentMethodByInvoiceId(String invoiceId); + void cancelSubscription(String orderNo); void cancelSubscriptionTemp(String subscriptionId); @@ -38,4 +40,6 @@ public interface StripeService { String changeCustomerPayment(String name, String email); boolean sendRenewalFailEmail(String invoiceId, String subscriptionId, String orderNo); + + String getCustomerPaymentMethod(String name, String email); } diff --git a/src/main/java/com/ai/da/service/impl/AffiliateServiceImpl.java b/src/main/java/com/ai/da/service/impl/AffiliateServiceImpl.java new file mode 100644 index 00000000..47f2a0b6 --- /dev/null +++ b/src/main/java/com/ai/da/service/impl/AffiliateServiceImpl.java @@ -0,0 +1,189 @@ +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.ResultEnum; +import com.ai.da.common.utils.CopyUtil; +import com.ai.da.common.utils.ObjectUtils; +import com.ai.da.common.utils.RedisUtil; +import com.ai.da.common.utils.SendEmailUtil; +import com.ai.da.mapper.primary.AffiliateMapper; +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.AffiliateVO; +import com.ai.da.model.vo.AuthPrincipalVo; +import com.ai.da.service.AccountService; +import com.ai.da.service.AffiliateService; +import com.ai.da.service.OrderInfoService; +import com.ai.da.service.PaymentInfoService; +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 io.netty.util.internal.StringUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Objects; + +@Service +@Slf4j +public class AffiliateServiceImpl extends ServiceImpl implements AffiliateService { + + @Resource + private OrderInfoService orderInfoService; + + @Resource + private AccountService accountService; + + @Resource + private PaymentInfoService paymentInfoService; + + @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()); + baseMapper.insert(affiliate); + // 邮件通知审批者 +// String email = "kimwong@code-create.com.hk"; + String email = "xupei3360@163.com"; + SendEmailUtil.affiliateEmailReminder(email, new AffiliateEmailParamsDTO(userHolder.getUsername(), promotionMethod), "new"); + }else { + throw new BusinessException("You have registered an Affiliate", ResultEnum.PROMPT.getCode()); + } + return true; + } + + public IPage getAffiliateList(AffiliateQueryDTO affiliateQueryDTO){ + QueryWrapper qw = new QueryWrapper<>(); + qw.eq(affiliateQueryDTO.getStatus() != null, "status", affiliateQueryDTO.getStatus()); + qw.gt(affiliateQueryDTO.getStartTime() != null, "create_time", affiliateQueryDTO.getStartTime()); + qw.lt(affiliateQueryDTO.getEndTime() != null, "create_time", affiliateQueryDTO.getEndTime()); + return baseMapper.selectPage(new Page<>(affiliateQueryDTO.getPage(), affiliateQueryDTO.getSize()), qw); + } + + 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(accountId)); + return affiliateVO; + } + + // 审批申请 + public Boolean applicationApproval(Long id, Boolean isApproved){ + Affiliate affiliate = baseMapper.selectById(id); + + // 1、更新db状态 + if (isApproved){ + // 更新状态 + affiliate.setStatus("Active"); + affiliate.setApproved(true); + affiliate.setLink(CommonConstant.AFFILIATE_LINK + affiliate.getId()); + } else { + affiliate.setStatus("Inactive"); + affiliate.setApproved(false); + } + affiliate.setUpdateTime(LocalDateTime.now()); + + // 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 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()); + 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())){ + // 3、若有, 直接更新affiliate的所得 + Affiliate affiliate = baseMapper.selectById(account.getInvitationCode()); + Float payerTotal = paymentInfo.getPayerTotal(); + if (payerTotal > 0){ + // 分配新用户首次订阅所付费用的25%作为佣金 + BigDecimal commission = BigDecimal.valueOf(payerTotal).multiply(new BigDecimal("0.25")); + 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); + } + } + orderInfo.setIsFirstSubscription((byte)1); + orderInfo.setUpdateTime(LocalDateTime.now()); + orderInfoService.updateById(orderInfo); + } + }); + } + redisUtil.addToString(RedisUtil.PAYMENT_INFO_LAST_SCAN_TIME, currentTime); + } + + public Boolean affiliateLinkViewsIncrease(Long id) { + redisUtil.increaseAffiliateLinkViewCount(id); + return Boolean.TRUE; + } + + private Long getAffiliateLinkViewCount(Long accountId) { + return redisUtil.getAffiliateLinkViewCount(accountId); + } + + // todo 每个月给kim发一封邮件统计本月的affiliate等的收入 + public void commissionCalculation(){ + + } + + +} 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 a6bcedc6..e0f86569 100644 --- a/src/main/java/com/ai/da/service/impl/CollectionElementServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/CollectionElementServiceImpl.java @@ -216,6 +216,8 @@ public class CollectionElementServiceImpl extends ServiceImpl implements PaymentInfoService { - private final StripeServiceImpl stripeServiceImpl; - - public PaymentInfoServiceImpl(StripeServiceImpl stripeServiceImpl) { - this.stripeServiceImpl = stripeServiceImpl; - } + @Resource + private StripeService stripeService; /** * 记录支付日志:微信支付 @@ -220,7 +218,7 @@ public class PaymentInfoServiceImpl extends ServiceImpl paymentMethod = stripeServiceImpl.getPaymentMethodByInvoiceId(invoiceId); + Map paymentMethod = stripeService.getPaymentMethodByInvoiceId(invoiceId); paymentInfo = new PaymentInfo(); paymentInfo.setOrderNo(orderNo); 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 3e8238bc..35b44944 100644 --- a/src/main/java/com/ai/da/service/impl/StripeServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/StripeServiceImpl.java @@ -553,7 +553,8 @@ public class StripeServiceImpl implements StripeService { Subscription cancel = subscription.cancel(); cancel.getStatus(); } catch (StripeException e) { - throw new RuntimeException(e); + log.error(e.getMessage()); +// throw new RuntimeException(e); } } @@ -988,7 +989,8 @@ public class StripeServiceImpl implements StripeService { try { OrderInfo orderInfo = orderInfoService.createOrderByProductId(1, PayTypeEnum.STRIPE.getType(), ProductEnum.DailySubscription); - String paymentMethodCode = "pm_card_mastercard"; + String customerId = getCustomer(name, email); +/* String paymentMethodCode = "pm_card_mastercard"; PaymentMethod paymentMethod = PaymentMethod.retrieve(paymentMethodCode); String customerId = getCustomer(name, email); @@ -997,14 +999,14 @@ public class StripeServiceImpl implements StripeService { PaymentMethodAttachParams attachParams = PaymentMethodAttachParams.builder() .setCustomer(customerId) .build(); - paymentMethod.attach(attachParams); + paymentMethod.attach(attachParams);*/ // 设置默认付款方式 Customer updatedCustomer = Customer.retrieve(customerId); CustomerUpdateParams params = CustomerUpdateParams.builder() .setInvoiceSettings( CustomerUpdateParams.InvoiceSettings.builder() - .setDefaultPaymentMethod(paymentMethod.getId()) +// .setDefaultPaymentMethod(paymentMethod.getId()) .build() ) .build(); @@ -1064,4 +1066,22 @@ public class StripeServiceImpl implements StripeService { } } + public String 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(); + + // todo 方向: 向用户添加了多种付款方式,更改默认的付款方式后,默认付款方式付款失败后是否自动使用其他付款方式付款? + // 如果是的,则需要删除能成功的付款方式,保留唯一失败的付款方式进行续订付款失败测试 + } catch (StripeException e) { + throw new RuntimeException(e); + } + return null; + } + + + } From bf8af41f3f95b8cd29c7c170dc664cfe79e925f7 Mon Sep 17 00:00:00 2001 From: xupei Date: Mon, 16 Dec 2024 10:26:02 +0800 Subject: [PATCH 7/8] =?UTF-8?q?=E6=B7=BB=E5=8A=A0Affiliate=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ai/da/common/constant/CommonConstant.java | 4 +- .../java/com/ai/da/common/utils/DateUtil.java | 19 ++++- .../ai/da/controller/AffiliateController.java | 12 ++- .../ai/da/controller/OrderInfoController.java | 10 ++- .../ai/da/controller/StripeController.java | 4 +- .../da/mapper/primary/PaymentInfoMapper.java | 7 ++ .../da/mapper/primary/entity/Affiliate.java | 2 + .../primary/entity/SubscriptionInfo.java | 3 + .../com/ai/da/model/vo/AccountLoginVO.java | 15 ++++ .../vo/AffiliateInvitationDetailsVO.java | 25 ++++++ .../java/com/ai/da/model/vo/OrderListVO.java | 29 +++++++ .../com/ai/da/service/AffiliateService.java | 5 ++ .../com/ai/da/service/PaymentInfoService.java | 8 +- .../java/com/ai/da/service/StripeService.java | 5 +- .../da/service/impl/AccountServiceImpl.java | 11 +++ .../da/service/impl/AffiliateServiceImpl.java | 70 ++++++++++++++-- .../service/impl/PaymentInfoServiceImpl.java | 48 ++++++++++- .../ai/da/service/impl/StripeServiceImpl.java | 83 ++++++++++++------- src/main/resources/application-dev.properties | 2 +- .../mapper/primary/PaymentInfoMapper.xml | 42 ++++++++++ 20 files changed, 349 insertions(+), 55 deletions(-) create mode 100644 src/main/java/com/ai/da/model/vo/AffiliateInvitationDetailsVO.java create mode 100644 src/main/java/com/ai/da/model/vo/OrderListVO.java create mode 100644 src/main/resources/mapper/primary/PaymentInfoMapper.xml diff --git a/src/main/java/com/ai/da/common/constant/CommonConstant.java b/src/main/java/com/ai/da/common/constant/CommonConstant.java index cd019602..35ff4504 100644 --- a/src/main/java/com/ai/da/common/constant/CommonConstant.java +++ b/src/main/java/com/ai/da/common/constant/CommonConstant.java @@ -79,7 +79,5 @@ public class CommonConstant { public static final String TIME_FORMAT_MMM_dd_yyyy = "MMM. dd, yyyy"; - public static final String AFFILIATE_LINK = ""; - - + public static final String AFFILIATE_LINK = "https://www.aida.com.hk?ref="; } 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 94269826..e1b1d755 100644 --- a/src/main/java/com/ai/da/common/utils/DateUtil.java +++ b/src/main/java/com/ai/da/common/utils/DateUtil.java @@ -9,8 +9,8 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; -import java.util.Calendar; import java.util.Date; +import java.util.Locale; import java.util.TimeZone; @Slf4j @@ -81,4 +81,21 @@ public class DateUtil { return String.valueOf(epochSecond).substring(0, 10); } + public static String changeTimeStampFormat(Long timeStamp, String type, String format){ + // 将秒级时间戳转换为毫秒级 + if (type.equals("seconds")){ + timeStamp = timeStamp * 1000; + } + // 输出格式 + SimpleDateFormat outputFormat = new SimpleDateFormat(format, Locale.ENGLISH); + // 创建Date对象 + Date date = new Date(timeStamp); + // 格式化输出 + return outputFormat.format(date); + } + + public static String changeTimeStampFormat(LocalDateTime localDate){ + return localDate.format(DateTimeFormatter.ofPattern("MMM. dd, yyyy, EEEE", Locale.US)); + } + } diff --git a/src/main/java/com/ai/da/controller/AffiliateController.java b/src/main/java/com/ai/da/controller/AffiliateController.java index 4e55ce71..f980dd10 100644 --- a/src/main/java/com/ai/da/controller/AffiliateController.java +++ b/src/main/java/com/ai/da/controller/AffiliateController.java @@ -4,19 +4,23 @@ package com.ai.da.controller; import com.ai.da.common.response.Response; import com.ai.da.mapper.primary.entity.Affiliate; 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.service.AffiliateService; import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.validation.Valid; +import java.util.List; @Slf4j @RestController @RequestMapping("/api/affiliate") +@Api(tags = "Affiliate模块") public class AffiliateController { @Resource @@ -46,7 +50,7 @@ public class AffiliateController { return Response.success(affiliateService.applicationApproval(id, isApproved)); } - @ApiOperation(value = "审批affiliate申请") + @ApiOperation(value = "定时计算佣金") @GetMapping("/testTask") public Response testTask() { affiliateService.updateAffiliateInfoWithPayment(); @@ -59,5 +63,11 @@ public class AffiliateController { return Response.success(affiliateService.affiliateLinkViewsIncrease(id)); } + @ApiOperation(value = "获取每个affiliate产生的收入") + @GetMapping("/getEachAffiliateGeneratedRevenue") + public Response> getEachAffiliateGeneratedRevenue(@RequestParam("id") Long id, @RequestParam(required = false) String startTime, @RequestParam(required = false) String endTime) { + return Response.success(affiliateService.getEachAffiliateGeneratedRevenue(id, startTime, endTime)); + } + } diff --git a/src/main/java/com/ai/da/controller/OrderInfoController.java b/src/main/java/com/ai/da/controller/OrderInfoController.java index 3a9ce1d5..56e60a48 100644 --- a/src/main/java/com/ai/da/controller/OrderInfoController.java +++ b/src/main/java/com/ai/da/controller/OrderInfoController.java @@ -3,9 +3,10 @@ package com.ai.da.controller; import com.ai.da.common.enums.OrderStatusEnum; import com.ai.da.common.response.PageBaseResponse; import com.ai.da.common.response.Response; -import com.ai.da.mapper.primary.entity.OrderInfo; import com.ai.da.model.dto.QueryPageByTimeDTO; +import com.ai.da.model.vo.OrderListVO; import com.ai.da.service.OrderInfoService; +import com.ai.da.service.PaymentInfoService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.web.bind.annotation.*; @@ -22,10 +23,13 @@ public class OrderInfoController { @Resource private OrderInfoService orderInfoService; + @Resource + private PaymentInfoService paymentInfoService; + @ApiOperation("订单列表") @PostMapping("/list") - public Response> list(@Valid @RequestBody QueryPageByTimeDTO queryPageByTimeDTO){ - PageBaseResponse orderByAccountId = orderInfoService.getOrderByPage(queryPageByTimeDTO); + public Response> list(@Valid @RequestBody QueryPageByTimeDTO queryPageByTimeDTO){ + PageBaseResponse orderByAccountId = paymentInfoService.getPaymentInfo(queryPageByTimeDTO); // List list = orderInfoService.listOrderByCreateTimeDesc(); return Response.success(orderByAccountId); } diff --git a/src/main/java/com/ai/da/controller/StripeController.java b/src/main/java/com/ai/da/controller/StripeController.java index 21c91a86..ede043d6 100644 --- a/src/main/java/com/ai/da/controller/StripeController.java +++ b/src/main/java/com/ai/da/controller/StripeController.java @@ -70,8 +70,8 @@ public class StripeController { @ApiOperation("取消订阅") @GetMapping("/cancelSubscription") - public Response cancelSubscription(@RequestParam String subscriptionId) { - stripeService.cancelSubscription(subscriptionId); + public Response cancelSubscription(@RequestParam String subscriptionId, @RequestParam String reason) { + stripeService.cancelSubscription(subscriptionId, reason); return Response.success("success"); } @ApiOperation("临时 取消订阅") diff --git a/src/main/java/com/ai/da/mapper/primary/PaymentInfoMapper.java b/src/main/java/com/ai/da/mapper/primary/PaymentInfoMapper.java index 27b147a4..ea23d39d 100644 --- a/src/main/java/com/ai/da/mapper/primary/PaymentInfoMapper.java +++ b/src/main/java/com/ai/da/mapper/primary/PaymentInfoMapper.java @@ -1,7 +1,14 @@ package com.ai.da.mapper.primary; import com.ai.da.mapper.primary.entity.PaymentInfo; +import com.ai.da.model.vo.OrderListVO; import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import java.util.List; + public interface PaymentInfoMapper extends BaseMapper { + + List selectPageOrderList(Long accountId, String startTime, String endTime, int offset, int pageSize); + + int queryOrderListTotalCount(Long accountId, String startTime, String endTime); } diff --git a/src/main/java/com/ai/da/mapper/primary/entity/Affiliate.java b/src/main/java/com/ai/da/mapper/primary/entity/Affiliate.java index 56204d75..beb52662 100644 --- a/src/main/java/com/ai/da/mapper/primary/entity/Affiliate.java +++ b/src/main/java/com/ai/da/mapper/primary/entity/Affiliate.java @@ -25,4 +25,6 @@ public class Affiliate extends BaseEntity{ private Boolean approved = false; private String link; + + private String promotionMethod; } diff --git a/src/main/java/com/ai/da/mapper/primary/entity/SubscriptionInfo.java b/src/main/java/com/ai/da/mapper/primary/entity/SubscriptionInfo.java index b7895c08..7ee32ab0 100644 --- a/src/main/java/com/ai/da/mapper/primary/entity/SubscriptionInfo.java +++ b/src/main/java/com/ai/da/mapper/primary/entity/SubscriptionInfo.java @@ -33,4 +33,7 @@ public class SubscriptionInfo extends BaseEntity{ // 当前订阅订单有效期结束时间 private Long currentPeriodEnd; + // 取消订阅原因 + private String cancelReason; + } diff --git a/src/main/java/com/ai/da/model/vo/AccountLoginVO.java b/src/main/java/com/ai/da/model/vo/AccountLoginVO.java index 9a1f9b85..8033057b 100644 --- a/src/main/java/com/ai/da/model/vo/AccountLoginVO.java +++ b/src/main/java/com/ai/da/model/vo/AccountLoginVO.java @@ -44,4 +44,19 @@ public class AccountLoginVO { private List accountExtendList; + // 订阅id(stripe提供) + private String subscriptionId; + + // 订阅状态 + private String status; + + // 订阅过期时间 + private String expireTime; + + // 订阅类型 month || year + private String subscriptionType; + + // 是否自动续订 + private boolean isAutoRenewal; + } diff --git a/src/main/java/com/ai/da/model/vo/AffiliateInvitationDetailsVO.java b/src/main/java/com/ai/da/model/vo/AffiliateInvitationDetailsVO.java new file mode 100644 index 00000000..4a9cdf54 --- /dev/null +++ b/src/main/java/com/ai/da/model/vo/AffiliateInvitationDetailsVO.java @@ -0,0 +1,25 @@ +package com.ai.da.model.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class AffiliateInvitationDetailsVO { + + private Long accountId; + + private String username; + + private Float firstSubscriptionPaymentAmount; + + private Float commission; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone="GMT+8") + private LocalDateTime time; +} diff --git a/src/main/java/com/ai/da/model/vo/OrderListVO.java b/src/main/java/com/ai/da/model/vo/OrderListVO.java new file mode 100644 index 00000000..44e42bff --- /dev/null +++ b/src/main/java/com/ai/da/model/vo/OrderListVO.java @@ -0,0 +1,29 @@ +package com.ai.da.model.vo; + + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 用于订单列表展示(展示的是所有支付信息) + */ +@Data +public class OrderListVO { + + private Long id; + + private Float amount; + + private String paymentMethod; + + private String state; + + private String orderType; + + private String invoiceLink; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone="GMT+8") + private LocalDateTime createTime; +} diff --git a/src/main/java/com/ai/da/service/AffiliateService.java b/src/main/java/com/ai/da/service/AffiliateService.java index ed340ae4..768bb214 100644 --- a/src/main/java/com/ai/da/service/AffiliateService.java +++ b/src/main/java/com/ai/da/service/AffiliateService.java @@ -2,10 +2,13 @@ package com.ai.da.service; import com.ai.da.mapper.primary.entity.Affiliate; import com.ai.da.model.dto.AffiliateQueryDTO; +import com.ai.da.model.vo.AffiliateInvitationDetailsVO; import com.ai.da.model.vo.AffiliateVO; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.service.IService; +import java.util.List; + public interface AffiliateService extends IService { Boolean registerAsAnAffiliate(String promotionMethod); @@ -19,4 +22,6 @@ public interface AffiliateService extends IService { void updateAffiliateInfoWithPayment(); Boolean affiliateLinkViewsIncrease(Long id); + + List getEachAffiliateGeneratedRevenue(Long affiliateId, String startTime, String endTime); } diff --git a/src/main/java/com/ai/da/service/PaymentInfoService.java b/src/main/java/com/ai/da/service/PaymentInfoService.java index 785fb286..761c0d8b 100644 --- a/src/main/java/com/ai/da/service/PaymentInfoService.java +++ b/src/main/java/com/ai/da/service/PaymentInfoService.java @@ -1,12 +1,16 @@ package com.ai.da.service; +import com.ai.da.common.response.PageBaseResponse; import com.ai.da.mapper.primary.entity.PaymentInfo; import com.ai.da.model.dto.AlipayHKCallbackDTO; +import com.ai.da.model.dto.QueryPageByTimeDTO; +import com.ai.da.model.vo.OrderListVO; import com.baomidou.mybatisplus.extension.service.IService; import com.paypal.orders.Order; import com.stripe.model.Charge; import com.stripe.model.Invoice; +import java.util.List; import java.util.Map; public interface PaymentInfoService extends IService { @@ -23,7 +27,9 @@ public interface PaymentInfoService extends IService { PaymentInfo createOrUpdatePaymentInfoForStripe(Charge charge); - PaymentInfo getPaymentInfoByOrderId(String orderId); + List getPaymentInfoByOrderNo(String orderId, String order); void updatePaymentStatusById(Long id, String status, String content); + + PageBaseResponse getPaymentInfo(QueryPageByTimeDTO queryPageByTimeDTO); } diff --git a/src/main/java/com/ai/da/service/StripeService.java b/src/main/java/com/ai/da/service/StripeService.java index f6f60dc3..f0a95a7c 100644 --- a/src/main/java/com/ai/da/service/StripeService.java +++ b/src/main/java/com/ai/da/service/StripeService.java @@ -1,5 +1,6 @@ package com.ai.da.service; +import com.ai.da.mapper.primary.entity.SubscriptionInfo; import com.ai.da.model.dto.ProductPurchaseDTO; import com.stripe.exception.StripeException; @@ -13,6 +14,8 @@ public interface StripeService { Boolean notify(HttpServletRequest request); + SubscriptionInfo getLatestSubscriptionInfoByAccountId(Long accountId); + String refund(String amount, String orderId, String reason); void checkOrderStatus(String orderNo); @@ -21,7 +24,7 @@ public interface StripeService { Map getPaymentMethodByInvoiceId(String invoiceId); - void cancelSubscription(String orderNo); + void cancelSubscription(String orderNo, String cancelReason); void cancelSubscriptionTemp(String subscriptionId); diff --git a/src/main/java/com/ai/da/service/impl/AccountServiceImpl.java b/src/main/java/com/ai/da/service/impl/AccountServiceImpl.java index 7fff9732..74ccd3f2 100644 --- a/src/main/java/com/ai/da/service/impl/AccountServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/AccountServiceImpl.java @@ -104,6 +104,9 @@ public class AccountServiceImpl extends ServiceImpl impl @Resource private RedisUtil redisUtil; + @Resource + private StripeService stripeService; + @Override @Transactional(rollbackFor = Exception.class) public AccountPreLoginVO preLogin(AccountPreLoginDTO accountDTO) { @@ -2416,6 +2419,14 @@ public class AccountServiceImpl extends ServiceImpl impl if (CollectionUtil.isNotEmpty(accountExtends)) { response.setAccountExtendList(accountExtends); } + SubscriptionInfo subscriptionInfo = stripeService.getLatestSubscriptionInfoByAccountId(accountId); + if (!Objects.isNull(subscriptionInfo)) { + response.setSubscriptionId(subscriptionInfo.getSubscriptionId()); + response.setSubscriptionType(subscriptionInfo.getType()); + response.setStatus(subscriptionInfo.getStatus()); + response.setExpireTime(String.valueOf(subscriptionInfo.getCurrentPeriodEnd())); + response.setAutoRenewal(subscriptionInfo.getStatus().equals("active")); + } return response; } 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 47f2a0b6..a249b08d 100644 --- a/src/main/java/com/ai/da/service/impl/AffiliateServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/AffiliateServiceImpl.java @@ -5,13 +5,14 @@ import com.ai.da.common.constant.CommonConstant; import com.ai.da.common.context.UserContext; import com.ai.da.common.response.ResultEnum; import com.ai.da.common.utils.CopyUtil; -import com.ai.da.common.utils.ObjectUtils; import com.ai.da.common.utils.RedisUtil; import com.ai.da.common.utils.SendEmailUtil; import com.ai.da.mapper.primary.AffiliateMapper; +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.AccountService; @@ -22,6 +23,7 @@ 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; @@ -29,6 +31,7 @@ import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.math.BigDecimal; import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -45,6 +48,9 @@ public class AffiliateServiceImpl extends ServiceImpl getAffiliateList(AffiliateQueryDTO affiliateQueryDTO){ QueryWrapper qw = new QueryWrapper<>(); - qw.eq(affiliateQueryDTO.getStatus() != null, "status", affiliateQueryDTO.getStatus()); - qw.gt(affiliateQueryDTO.getStartTime() != null, "create_time", affiliateQueryDTO.getStartTime()); - qw.lt(affiliateQueryDTO.getEndTime() != null, "create_time", affiliateQueryDTO.getEndTime()); + qw.eq(!StringUtils.isNullOrEmpty(affiliateQueryDTO.getStatus()), "status", affiliateQueryDTO.getStatus()); + qw.gt(!StringUtils.isNullOrEmpty(affiliateQueryDTO.getStartTime()), "create_time", affiliateQueryDTO.getStartTime()); + qw.lt(!StringUtils.isNullOrEmpty(affiliateQueryDTO.getEndTime()), "create_time", affiliateQueryDTO.getEndTime()); return baseMapper.selectPage(new Page<>(affiliateQueryDTO.getPage(), affiliateQueryDTO.getSize()), qw); } @@ -85,7 +92,7 @@ public class AffiliateServiceImpl extends ServiceImpl getEachAffiliateGeneratedRevenue(Long affiliateId, String startTime, String endTime) { + List resp = new ArrayList<>() ; + // 1、从account表中找到所有关联了指定affiliateId的accountId + QueryWrapper qw = new QueryWrapper<>(); + qw.eq("invitation_code", affiliateId); + + List accountList = accountService.getBaseMapper().selectList(qw); + if (accountList.isEmpty()){ + return null; + } else { + accountList.forEach(account -> { + // 2、分别找到各个accountId产生的第一笔订阅 + Long accountId = account.getId(); + QueryWrapper subscriptionInfoQueryWrapper = new QueryWrapper<>(); + subscriptionInfoQueryWrapper.eq("account_id", accountId) + .and(s -> s.eq("status", "active").or().eq("status", "canceled")) + .gt(!StringUtils.isNullOrEmpty(startTime) ,"create_time", startTime) + .lt(!StringUtils.isNullOrEmpty(endTime) ,"create_time", endTime).last("limit 1"); + + SubscriptionInfo subscriptionInfo = subscriptionInfoMapper.selectOne(subscriptionInfoQueryWrapper); + // 2、分别第一笔订阅的付款信息 + if (!Objects.isNull(subscriptionInfo)){ + PaymentInfo paymentInfo = paymentInfoService.getPaymentInfoByOrderNo(subscriptionInfo.getOrderNo(), "ASC").get(0); + AffiliateInvitationDetailsVO affiliateInvitationDetailsVO = new AffiliateInvitationDetailsVO(); + affiliateInvitationDetailsVO.setAccountId(accountId); + affiliateInvitationDetailsVO.setUsername(account.getUserName()); + affiliateInvitationDetailsVO.setFirstSubscriptionPaymentAmount(paymentInfo.getPayerTotal()); + affiliateInvitationDetailsVO.setCommission(BigDecimal.valueOf(paymentInfo.getPayerTotal()).multiply(new BigDecimal("0.25")).floatValue()); + affiliateInvitationDetailsVO.setTime(subscriptionInfo.getCreateTime()); + resp.add(affiliateInvitationDetailsVO); + } + }); + } + return resp; } // todo 每个月给kim发一封邮件统计本月的affiliate等的收入 public void commissionCalculation(){ + // 1、总收入(近一个月通过affiliate产生的收入) + + // 2、未支付的金额 affiliate表中unpaid的总和 + + // 3、邀请的新人 查询account表中,本月新增并有invitation_id的数量 + } diff --git a/src/main/java/com/ai/da/service/impl/PaymentInfoServiceImpl.java b/src/main/java/com/ai/da/service/impl/PaymentInfoServiceImpl.java index e5b35ed0..538886aa 100644 --- a/src/main/java/com/ai/da/service/impl/PaymentInfoServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/PaymentInfoServiceImpl.java @@ -1,11 +1,17 @@ package com.ai.da.service.impl; +import com.ai.da.common.context.UserContext; import com.ai.da.common.enums.PayTypeEnum; +import com.ai.da.common.response.PageBaseResponse; import com.ai.da.mapper.primary.PaymentInfoMapper; import com.ai.da.mapper.primary.entity.PaymentInfo; import com.ai.da.model.dto.AlipayHKCallbackDTO; +import com.ai.da.model.dto.QueryPageByTimeDTO; +import com.ai.da.model.vo.OrderListVO; 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.google.gson.Gson; import com.paypal.orders.Order; @@ -15,15 +21,19 @@ import com.stripe.model.Charge; import com.stripe.model.Invoice; import com.stripe.model.Subscription; import com.stripe.model.checkout.Session; +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.util.CollectionUtils; import javax.annotation.Resource; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Objects; @@ -313,11 +323,11 @@ public class PaymentInfoServiceImpl extends ServiceImpl getPaymentInfoByOrderNo(String orderId, String order){ QueryWrapper qw = new QueryWrapper<>(); - qw.eq("order_no", orderId); + qw.eq("order_no", orderId).orderByDesc(order.equals("DESC"),"id"); - return baseMapper.selectOne(qw); + return baseMapper.selectList(qw); } @Override @@ -329,4 +339,36 @@ public class PaymentInfoServiceImpl extends ServiceImpl getPaymentInfo(QueryPageByTimeDTO queryPageByTimeDTO){ + Long accountId = UserContext.getUserHolder().getId(); + + String startTime = queryPageByTimeDTO.getStartTime(); + String endTime = queryPageByTimeDTO.getEndTime(); + if (StringUtil.isNullOrEmpty(startTime)) { + startTime = "2024-02-01 00:00:00"; + } + if (StringUtil.isNullOrEmpty(endTime)) { + LocalDateTime now = LocalDateTime.now(); + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + endTime = now.format(dateTimeFormatter); + } + + int offset = (queryPageByTimeDTO.getPage() - 1) * queryPageByTimeDTO.getSize(); + List orderListVOS = baseMapper.selectPageOrderList(accountId, startTime, endTime, offset, queryPageByTimeDTO.getSize()); + + if (CollectionUtils.isEmpty(orderListVOS)) { + return PageBaseResponse.success(new Page<>()); + }else { + int totalCount = baseMapper.queryOrderListTotalCount(accountId, startTime, endTime); + IPage orderListVOIPage = new Page<>(); + Integer size = queryPageByTimeDTO.getSize(); + orderListVOIPage.setSize(size); + orderListVOIPage.setRecords(orderListVOS); + orderListVOIPage.setCurrent(queryPageByTimeDTO.getPage()); + orderListVOIPage.setPages((long)Math.ceil((double) totalCount / size)); + orderListVOIPage.setTotal(totalCount); + return PageBaseResponse.success(orderListVOIPage); + } + } } 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 35b44944..3d776186 100644 --- a/src/main/java/com/ai/da/service/impl/StripeServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/StripeServiceImpl.java @@ -4,6 +4,7 @@ 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; @@ -37,11 +38,9 @@ import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import java.math.BigDecimal; import java.math.RoundingMode; -import java.text.SimpleDateFormat; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneOffset; -import java.time.format.DateTimeFormatter; import java.util.*; @SuppressWarnings("LoggingSimilarMessage") @@ -440,11 +439,9 @@ public class StripeServiceImpl implements StripeService { @Transactional(rollbackFor = Exception.class) public SubscriptionInfo createSubscription(Subscription subscription){ // 确认当前subscription是否已经记录 - QueryWrapper qw = new QueryWrapper<>(); - qw.eq("subscription_id", subscription.getId()); - - SubscriptionInfo subscriptionInfo = subscriptionInfoMapper.selectOne(qw); - if (Objects.isNull(subscriptionInfo)){ + 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); @@ -459,7 +456,7 @@ public class StripeServiceImpl implements StripeService { subscriptionInfo.setSubscriptionId(subscription.getId()); subscriptionInfo.setType(interval); subscriptionInfo.setStatus(subscription.getStatus()); - subscriptionInfo.setNextPayDate(changeTimeStampFormat(subscription.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy_EEEE)); + 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()); @@ -471,21 +468,34 @@ public class StripeServiceImpl implements StripeService { return subscriptionInfo; } - public String changeTimeStampFormat(Long timeStamp, String type, String format){ - // 将秒级时间戳转换为毫秒级 - if (type.equals("seconds")){ - timeStamp = timeStamp * 1000; + 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; } - // 输出格式 - SimpleDateFormat outputFormat = new SimpleDateFormat(format, Locale.ENGLISH); - // 创建Date对象 - Date date = new Date(timeStamp); - // 格式化输出 - return outputFormat.format(date); } - public String changeTimeStampFormat(LocalDateTime localDate){ - return localDate.format(DateTimeFormatter.ofPattern("MMM. dd, yyyy, EEEE", Locale.US)); + public SubscriptionInfo getLatestSubscriptionInfoByAccountId(Long accountId){ + QueryWrapper qw = new QueryWrapper<>(); + qw.eq("account_id", accountId); + List subscriptionInfos = subscriptionInfoMapper.selectList(qw); + if (subscriptionInfos.isEmpty()){ + return null; + }else { + return subscriptionInfos.get(0); + } } @Transactional(rollbackFor = Exception.class) @@ -504,10 +514,10 @@ public class StripeServiceImpl implements StripeService { } if (!subscriptionInfo.getCurrentPeriodEnd().equals(subscription.getCurrentPeriodEnd())){ subscriptionInfo.setCurrentPeriodEnd(subscription.getCurrentPeriodEnd()); - subscriptionInfo.setNextPayDate(changeTimeStampFormat(subscription.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy_EEEE)); + subscriptionInfo.setNextPayDate(DateUtil.changeTimeStampFormat(subscription.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy_EEEE)); // 更新账号到期时间 updateAccountValidity(subscriptionInfo.getAccountId(), subscriptionInfo.getCurrentPeriodEnd()); - log.info("更新 {} 账号到期时间为:{}", subscriptionInfo.getAccountId(), changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy_EEEE)); + log.info("更新 {} 账号到期时间为:{}", subscriptionInfo.getAccountId(), DateUtil.changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy_EEEE)); flag = true; } if (flag){ @@ -526,7 +536,7 @@ public class StripeServiceImpl implements StripeService { } // 取消连续订阅 将订阅从pause状态转为cancel状态(使用定时器,定期检索DB中,过期且不续订的订阅) - public void cancelSubscription(String subscriptionId) { + public void cancelSubscription(String subscriptionId, String cancelReason) { Stripe.apiKey = privateKey; Long accountId = UserContext.getUserHolder().getId(); log.info("用户 {} 申请取消连续订阅 {}", accountId, subscriptionId); @@ -538,6 +548,9 @@ public class StripeServiceImpl implements StripeService { try { Subscription cancel = subscription.cancel(); cancel.getStatus(); + + // 更新数据库 + updateCancelReason(subscriptionId, cancelReason); } catch (StripeException e) { log.error("订阅 {} 取消失败, error message : {}", subscription.getId(), e.getMessage()); } @@ -564,8 +577,9 @@ public class StripeServiceImpl implements StripeService { try { Stripe.apiKey = privateKey; + // todo transactionId不再是sessionId而是invoiceId,所以这里需要更新 // 根据orderId找到对应的sessionId - String sessionId = paymentInfoService.getPaymentInfoByOrderId(orderNo).getTransactionId(); + String sessionId = paymentInfoService.getPaymentInfoByOrderNo(orderNo, "DESC").get(0).getTransactionId(); if (StringUtils.isNotEmpty(sessionId)) { //根据会话编号退款 Session session = Session.retrieve(sessionId); @@ -628,7 +642,8 @@ public class StripeServiceImpl implements StripeService { public void checkOrderStatus(String orderNo) { Stripe.apiKey = privateKey; // 1、通过orderNo 查询sessionId - PaymentInfo paymentInfo = paymentInfoService.getPaymentInfoByOrderId(orderNo); + // todo transactionId不再是sessionId而是invoiceId,所以这里需要更新 + PaymentInfo paymentInfo = paymentInfoService.getPaymentInfoByOrderNo(orderNo, "DESC").get(0); try { Session session = Session.retrieve(paymentInfo.getTransactionId()); if (Objects.isNull(session)) { @@ -822,8 +837,8 @@ public class StripeServiceImpl implements StripeService { emailParamsDTO.setCreateDate(String.valueOf(paymentInfo.getCreateTime()).replace("T", " ")); emailParamsDTO.setQuantity(String.valueOf(1)); emailParamsDTO.setTotalFee(paymentInfo.getPayerTotal().toString()); - emailParamsDTO.setLastOrderDate(changeTimeStampFormat(subscriptionInfo.getCurrentPeriodStart(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy)); - emailParamsDTO.setEndOfPrepaidTerm(changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy)); + 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); SendEmailUtil.subscriptionEmailReminder(type, emailParamsDTO, language, account.getUserEmail()); @@ -941,9 +956,9 @@ public class StripeServiceImpl implements StripeService { emailParamsDTO.setSubscriptionId(subscriptionInfo.getId().toString()); emailParamsDTO.setFailMessage(orderByOrderNo.getNote()); emailParamsDTO.setSubscriptionType(subscriptionInfo.getType()); - emailParamsDTO.setStartDate(changeTimeStampFormat(orderByOrderNo.getCreateTime())); - emailParamsDTO.setNextPayDate(changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy)); - emailParamsDTO.setRenewalTime(changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy)); + emailParamsDTO.setStartDate(DateUtil.changeTimeStampFormat(orderByOrderNo.getCreateTime())); + 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(){ @@ -1082,6 +1097,14 @@ public class StripeServiceImpl implements StripeService { return null; } + public void updateCancelReason(String subscriptionId, String reason){ + QueryWrapper qw = new QueryWrapper<>(); + qw.eq("subscription_id", subscriptionId); + SubscriptionInfo subscriptionInfo = subscriptionInfoMapper.selectOne(qw); + + subscriptionInfo.setCancelReason(reason); + subscriptionInfoMapper.updateById(subscriptionInfo); + } } diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index 992396c3..2afd01c3 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -20,7 +20,7 @@ spring.security.jwtExpiration=8640000000 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/**,\ /api/product/**,/api/ali-pay/**,/api/order-info/**,/api/paypal/**,/api/credits/**,/api/inquiry/**,/api/tasks/**,/api/python/prepareForSR,/api/alipay-hk/**,/api/portfolio/**,\ - /api/stripe/**,/api/message/**,/notification/** + /api/stripe/**,/api/message/**,/notification/**,/api/affiliate/** spring.security.authApi=/auth/login diff --git a/src/main/resources/mapper/primary/PaymentInfoMapper.xml b/src/main/resources/mapper/primary/PaymentInfoMapper.xml new file mode 100644 index 00000000..8bab6ca5 --- /dev/null +++ b/src/main/resources/mapper/primary/PaymentInfoMapper.xml @@ -0,0 +1,42 @@ + + + + + + + + + From 7d8f047087f2ecceb38bce959e955242c7c4d5c9 Mon Sep 17 00:00:00 2001 From: xupei Date: Wed, 18 Dec 2024 11:53:41 +0800 Subject: [PATCH 8/8] =?UTF-8?q?Affiliate=E5=8A=9F=E8=83=BD-=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93=E8=A1=A8=E8=AE=BE=E8=AE=A1=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/ai/da/common/task/PaymentTask.java | 39 ++++- .../com/ai/da/common/utils/RedisUtil.java | 2 +- .../ai/da/controller/AffiliateController.java | 25 +++- .../ai/da/controller/StripeController.java | 11 +- .../mapper/primary/AffiliateIncomeMapper.java | 14 ++ .../ai/da/mapper/primary/AffiliateMapper.java | 4 + .../primary/entity/AffiliateIncome.java | 26 ++++ .../ai/da/model/dto/AffiliateQueryDTO.java | 20 ++- .../com/ai/da/model/vo/AccountLoginVO.java | 3 + .../com/ai/da/service/AffiliateService.java | 10 +- .../java/com/ai/da/service/StripeService.java | 4 +- .../da/service/impl/AccountServiceImpl.java | 7 + .../da/service/impl/AffiliateServiceImpl.java | 140 ++++++++++++------ .../ai/da/service/impl/StripeServiceImpl.java | 53 ++++++- .../mapper/primary/AffiliateIncomeMapper.xml | 36 +++++ .../mapper/primary/AffiliateMapper.xml | 16 ++ 16 files changed, 338 insertions(+), 72 deletions(-) create mode 100644 src/main/java/com/ai/da/mapper/primary/AffiliateIncomeMapper.java create mode 100644 src/main/java/com/ai/da/mapper/primary/entity/AffiliateIncome.java create mode 100644 src/main/resources/mapper/primary/AffiliateIncomeMapper.xml create mode 100644 src/main/resources/mapper/primary/AffiliateMapper.xml diff --git a/src/main/java/com/ai/da/common/task/PaymentTask.java b/src/main/java/com/ai/da/common/task/PaymentTask.java index 656e3a23..40450055 100644 --- a/src/main/java/com/ai/da/common/task/PaymentTask.java +++ b/src/main/java/com/ai/da/common/task/PaymentTask.java @@ -2,16 +2,14 @@ package com.ai.da.common.task; import com.ai.da.common.enums.PayTypeEnum; import com.ai.da.mapper.primary.entity.OrderInfo; -import com.ai.da.service.AliPayService; -import com.ai.da.service.OrderInfoService; -import com.ai.da.service.PayPalCheckoutService; -import com.ai.da.service.StripeService; +import com.ai.da.service.*; import com.paypal.http.exceptions.SerializeException; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import javax.annotation.Resource; +import java.time.LocalDate; import java.util.List; @Slf4j @@ -24,6 +22,10 @@ public class PaymentTask { @Resource private StripeService stripeService; + @Resource + private AffiliateService affiliateService; + + // 考虑删除该定时任务(原因:之后的订单列允许用户查看发票,发票未过期时仍可以支付,所以不需要手动使订单过期) // @Scheduled(cron = "0/30 * * * * ?") public void orderConfirmForStripe() throws SerializeException { @@ -89,8 +91,31 @@ public class PaymentTask { } // 每天凌晨检查subscription中有哪些已过期,更新状态 - @Scheduled(cron = "0 0 0 * * ?") - public void checkSubscriptionExpiration(){ - stripeService.checkSubscriptionExpiration(); +// @Scheduled(cron = "0 0 0 * * ?") +// public void checkSubscriptionExpiration(){ +// stripeService.checkSubscriptionExpiration(); +// } + + // 如果有订阅已创建,但是没有发邮件通知的,需要主动获取回调信息并向用户发送邮件 + public void checkSubscriptionPayment(){ + // + } + + @Scheduled(cron = "0 */5 * * * *") // Run every 5 minutes + public void updateAffiliateInfoWithPayment(){ + affiliateService.updateAffiliateInfoWithPayment(); + } + + @Scheduled(cron = "0 0 8 28-31 * ?") + public void commissionSummaryReminder(){ + // 每个月末的最后一天的早上八点执行 + LocalDate today = LocalDate.now(); + // 判断是否为月底 + if (today.plusDays(1).getDayOfMonth() == 1) { + log.info("今天是月底,执行佣金结算提醒任务!"); + affiliateService.commissionCalculation(null, null); + } + } + } 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 d3ba2a93..7e0d9578 100644 --- a/src/main/java/com/ai/da/common/utils/RedisUtil.java +++ b/src/main/java/com/ai/da/common/utils/RedisUtil.java @@ -279,7 +279,7 @@ public class RedisUtil { redisTemplate.expire(redisKey, 5, TimeUnit.MINUTES); } - public final static String PAYMENT_INFO_LAST_SCAN_TIME = "PaymentInfoLastScanTime:"; + public final static String PAYMENT_INFO_LAST_SCAN_TIME = "PaymentInfoLastScanTime"; public final static String AFFILIATE_LINK_VIEW_KEY = "AffiliateLink:view:"; diff --git a/src/main/java/com/ai/da/controller/AffiliateController.java b/src/main/java/com/ai/da/controller/AffiliateController.java index f980dd10..1be751ac 100644 --- a/src/main/java/com/ai/da/controller/AffiliateController.java +++ b/src/main/java/com/ai/da/controller/AffiliateController.java @@ -28,7 +28,7 @@ public class AffiliateController { @ApiOperation(value = "注册成为affiliate") @GetMapping("/registration") - public Response completeGuidance(@RequestParam("promotionMethod") String promotionMethod) { + public Response completeGuidance(@RequestParam(value = "promotionMethod", required = false) String promotionMethod) { return Response.success(affiliateService.registerAsAnAffiliate(promotionMethod)); } @@ -44,18 +44,31 @@ public class AffiliateController { return Response.success(affiliateService.personalAffiliateCenter()); } + @ApiOperation(value = "获取个人佣金图表数据") + @GetMapping("/getPersonalMonthlyIncome") + public Response getPersonalMonthlyIncome(@RequestParam("year")int year) { + return Response.success(affiliateService.getPersonalMonthlyIncome(year)); + } + @ApiOperation(value = "审批affiliate申请") @GetMapping("/approval") public Response applicationApproval(@RequestParam("id") Long id, @RequestParam("isApproved")Boolean isApproved) { return Response.success(affiliateService.applicationApproval(id, isApproved)); } - @ApiOperation(value = "定时计算佣金") + /*@ApiOperation(value = "定时计算佣金") @GetMapping("/testTask") public Response testTask() { affiliateService.updateAffiliateInfoWithPayment(); return Response.success("success "); - } + }*/ + + /*@ApiOperation(value = "每月发送结算邮件") + @GetMapping("/commissionCalculation") + public Response commissionCalculation() { + affiliateService.commissionCalculation(null, null); + return Response.success("success "); + }*/ @ApiOperation(value = "affiliate链接浏览量增加") @GetMapping("/viewsIncrease") @@ -64,9 +77,9 @@ public class AffiliateController { } @ApiOperation(value = "获取每个affiliate产生的收入") - @GetMapping("/getEachAffiliateGeneratedRevenue") - public Response> getEachAffiliateGeneratedRevenue(@RequestParam("id") Long id, @RequestParam(required = false) String startTime, @RequestParam(required = false) String endTime) { - return Response.success(affiliateService.getEachAffiliateGeneratedRevenue(id, startTime, endTime)); + @PostMapping("/getEachAffiliateGeneratedRevenue") + public Response> getEachAffiliateGeneratedRevenue(@RequestBody AffiliateQueryDTO affiliateQueryDTO) { + return Response.success(affiliateService.getEachAffiliateGeneratedRevenue(affiliateQueryDTO)); } diff --git a/src/main/java/com/ai/da/controller/StripeController.java b/src/main/java/com/ai/da/controller/StripeController.java index ede043d6..82c5e1cc 100644 --- a/src/main/java/com/ai/da/controller/StripeController.java +++ b/src/main/java/com/ai/da/controller/StripeController.java @@ -19,6 +19,7 @@ import javax.servlet.http.HttpServletResponse; import javax.validation.Valid; import java.io.IOException; import java.util.List; +import java.util.Map; @Api(tags = "Stripe模块") @Slf4j @@ -70,7 +71,7 @@ public class StripeController { @ApiOperation("取消订阅") @GetMapping("/cancelSubscription") - public Response cancelSubscription(@RequestParam String subscriptionId, @RequestParam String reason) { + public Response cancelSubscription(@RequestParam String subscriptionId, @RequestParam(required = false) String reason) { stripeService.cancelSubscription(subscriptionId, reason); return Response.success("success"); } @@ -101,8 +102,14 @@ public class StripeController { @ApiOperation("临时 查询指定用户绑定的付款方式") @GetMapping("/getCustomerPaymentMethod") - public Response getCustomerPaymentMethod(@RequestParam String name, @RequestParam String email) { + public Response>> getCustomerPaymentMethod(@RequestParam String name, @RequestParam String email) { return Response.success(stripeService.getCustomerPaymentMethod(name, email)); } + @ApiOperation("临时 解绑指定用户绑定的所有付款方式") + @GetMapping("/detachCustomerAllPaymentMethod") + public Response detachCustomerAllPaymentMethod(@RequestParam String name, @RequestParam String email) { + return Response.success(stripeService.detachCustomerAllPaymentMethod(name, email)); + } + } diff --git a/src/main/java/com/ai/da/mapper/primary/AffiliateIncomeMapper.java b/src/main/java/com/ai/da/mapper/primary/AffiliateIncomeMapper.java new file mode 100644 index 00000000..d0a7d6ce --- /dev/null +++ b/src/main/java/com/ai/da/mapper/primary/AffiliateIncomeMapper.java @@ -0,0 +1,14 @@ +package com.ai.da.mapper.primary; + +import com.ai.da.mapper.primary.entity.AffiliateIncome; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +import java.util.List; +import java.util.Map; + +public interface AffiliateIncomeMapper extends BaseMapper { + + List> getPersonalMonthlyIncome(Long affiliateAccountId, int year); + + List> getMonthlyAffiliateIncome(int year, int month); +} diff --git a/src/main/java/com/ai/da/mapper/primary/AffiliateMapper.java b/src/main/java/com/ai/da/mapper/primary/AffiliateMapper.java index 314a0952..3974b904 100644 --- a/src/main/java/com/ai/da/mapper/primary/AffiliateMapper.java +++ b/src/main/java/com/ai/da/mapper/primary/AffiliateMapper.java @@ -3,5 +3,9 @@ package com.ai.da.mapper.primary; import com.ai.da.mapper.primary.entity.Affiliate; import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import java.util.Map; + public interface AffiliateMapper extends BaseMapper { + + Map getMonthlyApprovedAffiliate(int year, int month); } diff --git a/src/main/java/com/ai/da/mapper/primary/entity/AffiliateIncome.java b/src/main/java/com/ai/da/mapper/primary/entity/AffiliateIncome.java new file mode 100644 index 00000000..eb8ce12c --- /dev/null +++ b/src/main/java/com/ai/da/mapper/primary/entity/AffiliateIncome.java @@ -0,0 +1,26 @@ +package com.ai.da.mapper.primary.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; + +@EqualsAndHashCode(callSuper = true) +@Data +@TableName("t_affiliate_income") +public class AffiliateIncome extends BaseEntity { + + private Long affiliateId; + + private Long affiliateAccountId; + + private Long inviteeAccountId; + + private Float amount; + + private LocalDateTime paymentTime; + + private Float commission; + +} diff --git a/src/main/java/com/ai/da/model/dto/AffiliateQueryDTO.java b/src/main/java/com/ai/da/model/dto/AffiliateQueryDTO.java index 5498964b..79fac3e7 100644 --- a/src/main/java/com/ai/da/model/dto/AffiliateQueryDTO.java +++ b/src/main/java/com/ai/da/model/dto/AffiliateQueryDTO.java @@ -1,6 +1,7 @@ package com.ai.da.model.dto; import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import lombok.EqualsAndHashCode; @@ -8,6 +9,23 @@ import lombok.EqualsAndHashCode; @Data @ApiModel("查询affiliate列表") public class AffiliateQueryDTO extends TimeQueryBaseDTO{ - + @ApiModelProperty("Active(活跃) || Inactive(过期) || Pending(待审批) || Refused(拒绝)") private String status; + + @ApiModelProperty("推广者id") + private Long affiliateId; + + @ApiModelProperty("按时间 DESC 降序 || ASC 升序") + private String order = "ASC"; + + @Override + public String toString() { + return "AffiliateQueryDTO{" + + "status='" + status + '\'' + ' ' + + "startTime='" + super.getStartTime() + '\'' + ' ' + + "endTime='" + super.getEndTime() + '\'' + ' ' + + "page='" + super.getPage() + '\'' + ' ' + + "size='" + super.getSize() + '\'' + ' ' + + '}'; + } } diff --git a/src/main/java/com/ai/da/model/vo/AccountLoginVO.java b/src/main/java/com/ai/da/model/vo/AccountLoginVO.java index 8033057b..d514dc8d 100644 --- a/src/main/java/com/ai/da/model/vo/AccountLoginVO.java +++ b/src/main/java/com/ai/da/model/vo/AccountLoginVO.java @@ -59,4 +59,7 @@ public class AccountLoginVO { // 是否自动续订 private boolean isAutoRenewal; + // 是否是affiliate + private boolean isAffiliate = false; + } diff --git a/src/main/java/com/ai/da/service/AffiliateService.java b/src/main/java/com/ai/da/service/AffiliateService.java index 768bb214..9ceb1cb1 100644 --- a/src/main/java/com/ai/da/service/AffiliateService.java +++ b/src/main/java/com/ai/da/service/AffiliateService.java @@ -7,8 +7,6 @@ import com.ai.da.model.vo.AffiliateVO; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.service.IService; -import java.util.List; - public interface AffiliateService extends IService { Boolean registerAsAnAffiliate(String promotionMethod); @@ -17,11 +15,17 @@ public interface AffiliateService extends IService { AffiliateVO personalAffiliateCenter(); + double[] getPersonalMonthlyIncome(int year); + Boolean applicationApproval(Long id, Boolean isApproved); void updateAffiliateInfoWithPayment(); Boolean affiliateLinkViewsIncrease(Long id); - List getEachAffiliateGeneratedRevenue(Long affiliateId, String startTime, String endTime); + IPage getEachAffiliateGeneratedRevenue(AffiliateQueryDTO affiliateQueryDTO); + + Affiliate getByAccountId(Long accountId); + + void commissionCalculation(Integer year, Integer month); } diff --git a/src/main/java/com/ai/da/service/StripeService.java b/src/main/java/com/ai/da/service/StripeService.java index f0a95a7c..978b7c41 100644 --- a/src/main/java/com/ai/da/service/StripeService.java +++ b/src/main/java/com/ai/da/service/StripeService.java @@ -44,5 +44,7 @@ public interface StripeService { boolean sendRenewalFailEmail(String invoiceId, String subscriptionId, String orderNo); - String getCustomerPaymentMethod(String name, String email); + List> getCustomerPaymentMethod(String name, String email); + + String detachCustomerAllPaymentMethod(String name, String email); } diff --git a/src/main/java/com/ai/da/service/impl/AccountServiceImpl.java b/src/main/java/com/ai/da/service/impl/AccountServiceImpl.java index 74ccd3f2..5840852e 100644 --- a/src/main/java/com/ai/da/service/impl/AccountServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/AccountServiceImpl.java @@ -107,6 +107,9 @@ public class AccountServiceImpl extends ServiceImpl impl @Resource private StripeService stripeService; + @Resource + private AffiliateService affiliateService; + @Override @Transactional(rollbackFor = Exception.class) public AccountPreLoginVO preLogin(AccountPreLoginDTO accountDTO) { @@ -2428,6 +2431,10 @@ public class AccountServiceImpl extends ServiceImpl impl response.setAutoRenewal(subscriptionInfo.getStatus().equals("active")); } + Affiliate affiliate = affiliateService.getByAccountId(accountId); + if (!Objects.isNull(affiliate) && affiliate.getStatus().equals("Active")) { + response.setAffiliate(true); + } return response; } 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 a249b08d..220aefe6 100644 --- a/src/main/java/com/ai/da/service/impl/AffiliateServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/AffiliateServiceImpl.java @@ -7,6 +7,8 @@ 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.AccountMapper; +import com.ai.da.mapper.primary.AffiliateIncomeMapper; import com.ai.da.mapper.primary.AffiliateMapper; import com.ai.da.mapper.primary.SubscriptionInfoMapper; import com.ai.da.mapper.primary.entity.*; @@ -31,9 +33,10 @@ import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.math.BigDecimal; import java.time.LocalDateTime; -import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.function.Function; @Service @Slf4j @@ -51,6 +54,9 @@ public class AffiliateServiceImpl extends ServiceImpl getAffiliateList(AffiliateQueryDTO affiliateQueryDTO){ + log.info("parameter => {}", affiliateQueryDTO.toString()); QueryWrapper qw = new QueryWrapper<>(); - qw.eq(!StringUtils.isNullOrEmpty(affiliateQueryDTO.getStatus()), "status", affiliateQueryDTO.getStatus()); - qw.gt(!StringUtils.isNullOrEmpty(affiliateQueryDTO.getStartTime()), "create_time", affiliateQueryDTO.getStartTime()); - qw.lt(!StringUtils.isNullOrEmpty(affiliateQueryDTO.getEndTime()), "create_time", affiliateQueryDTO.getEndTime()); + 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"); return baseMapper.selectPage(new Page<>(affiliateQueryDTO.getPage(), affiliateQueryDTO.getSize()), qw); } @@ -96,6 +105,18 @@ public class AffiliateServiceImpl extends ServiceImpl> 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){ Affiliate affiliate = baseMapper.selectById(id); @@ -107,7 +128,7 @@ public class AffiliateServiceImpl extends ServiceImpl { // 2、根据order_no查付款用户id OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(paymentInfo.getOrderNo()); + if (Objects.isNull(orderInfo)){ + return; + } Long accountId = orderInfo.getAccountId(); // 3、查该用户之前是否有初次订阅的订单 QueryWrapper qwOrderInfo = new QueryWrapper<>(); @@ -155,6 +179,7 @@ public class AffiliateServiceImpl extends ServiceImpl 0){ // 分配新用户首次订阅所付费用的25%作为佣金 BigDecimal commission = BigDecimal.valueOf(payerTotal).multiply(new BigDecimal("0.25")); @@ -168,6 +193,17 @@ public class AffiliateServiceImpl extends ServiceImpl getEachAffiliateGeneratedRevenue(Long affiliateId, String startTime, String endTime) { - List resp = new ArrayList<>() ; - // 1、从account表中找到所有关联了指定affiliateId的accountId - QueryWrapper qw = new QueryWrapper<>(); - qw.eq("invitation_code", affiliateId); - - List accountList = accountService.getBaseMapper().selectList(qw); - if (accountList.isEmpty()){ - return null; - } else { - accountList.forEach(account -> { - // 2、分别找到各个accountId产生的第一笔订阅 - Long accountId = account.getId(); - QueryWrapper subscriptionInfoQueryWrapper = new QueryWrapper<>(); - subscriptionInfoQueryWrapper.eq("account_id", accountId) - .and(s -> s.eq("status", "active").or().eq("status", "canceled")) - .gt(!StringUtils.isNullOrEmpty(startTime) ,"create_time", startTime) - .lt(!StringUtils.isNullOrEmpty(endTime) ,"create_time", endTime).last("limit 1"); - - SubscriptionInfo subscriptionInfo = subscriptionInfoMapper.selectOne(subscriptionInfoQueryWrapper); - // 2、分别第一笔订阅的付款信息 - if (!Objects.isNull(subscriptionInfo)){ - PaymentInfo paymentInfo = paymentInfoService.getPaymentInfoByOrderNo(subscriptionInfo.getOrderNo(), "ASC").get(0); - AffiliateInvitationDetailsVO affiliateInvitationDetailsVO = new AffiliateInvitationDetailsVO(); - affiliateInvitationDetailsVO.setAccountId(accountId); - affiliateInvitationDetailsVO.setUsername(account.getUserName()); - affiliateInvitationDetailsVO.setFirstSubscriptionPaymentAmount(paymentInfo.getPayerTotal()); - affiliateInvitationDetailsVO.setCommission(BigDecimal.valueOf(paymentInfo.getPayerTotal()).multiply(new BigDecimal("0.25")).floatValue()); - affiliateInvitationDetailsVO.setTime(subscriptionInfo.getCreateTime()); - resp.add(affiliateInvitationDetailsVO); - } - }); + public IPage getEachAffiliateGeneratedRevenue(AffiliateQueryDTO affiliateQueryDTO) { + if (Objects.isNull(affiliateQueryDTO.getAffiliateId())){ + throw new BusinessException("Please specify the affiliate ID.", ResultEnum.PROMPT.getCode()); } - return resp; + + 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; + }); } - // todo 每个月给kim发一封邮件统计本月的affiliate等的收入 - public void commissionCalculation(){ - // 1、总收入(近一个月通过affiliate产生的收入) + public void commissionCalculation(Integer year, Integer month) { + if (Objects.isNull(year)) { + year = LocalDateTime.now().getYear(); + } + if (Objects.isNull(month)) { + month = LocalDateTime.now().getMonthValue(); + } - // 2、未支付的金额 affiliate表中unpaid的总和 + 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"); + } - // 3、邀请的新人 查询account表中,本月新增并有invitation_id的数量 + // 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 receiverEmail = "xupei3360@163.com"; +// String receiverEmail = "kimwong@code-create.com.hk"; + // 邮件通知 + 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); } 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 3d776186..f16bbb85 100644 --- a/src/main/java/com/ai/da/service/impl/StripeServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/StripeServiceImpl.java @@ -1004,8 +1004,8 @@ public class StripeServiceImpl implements StripeService { try { OrderInfo orderInfo = orderInfoService.createOrderByProductId(1, PayTypeEnum.STRIPE.getType(), ProductEnum.DailySubscription); - String customerId = getCustomer(name, email); -/* String paymentMethodCode = "pm_card_mastercard"; +// String customerId = getCustomer(name, email); + String paymentMethodCode = "pm_card_mastercard"; PaymentMethod paymentMethod = PaymentMethod.retrieve(paymentMethodCode); String customerId = getCustomer(name, email); @@ -1014,14 +1014,14 @@ public class StripeServiceImpl implements StripeService { PaymentMethodAttachParams attachParams = PaymentMethodAttachParams.builder() .setCustomer(customerId) .build(); - paymentMethod.attach(attachParams);*/ + paymentMethod.attach(attachParams); // 设置默认付款方式 Customer updatedCustomer = Customer.retrieve(customerId); CustomerUpdateParams params = CustomerUpdateParams.builder() .setInvoiceSettings( CustomerUpdateParams.InvoiceSettings.builder() -// .setDefaultPaymentMethod(paymentMethod.getId()) + .setDefaultPaymentMethod(paymentMethod.getId()) .build() ) .build(); @@ -1081,19 +1081,54 @@ public class StripeServiceImpl implements StripeService { } } - public String getCustomerPaymentMethod(String name, String email){ + 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); + }); - // todo 方向: 向用户添加了多种付款方式,更改默认的付款方式后,默认付款方式付款失败后是否自动使用其他付款方式付款? + 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; } @@ -1102,8 +1137,10 @@ public class StripeServiceImpl implements StripeService { qw.eq("subscription_id", subscriptionId); SubscriptionInfo subscriptionInfo = subscriptionInfoMapper.selectOne(qw); - subscriptionInfo.setCancelReason(reason); - subscriptionInfoMapper.updateById(subscriptionInfo); + if (!Objects.isNull(subscriptionInfo)) { + subscriptionInfo.setCancelReason(reason); + subscriptionInfoMapper.updateById(subscriptionInfo); + } } diff --git a/src/main/resources/mapper/primary/AffiliateIncomeMapper.xml b/src/main/resources/mapper/primary/AffiliateIncomeMapper.xml new file mode 100644 index 00000000..bac4a547 --- /dev/null +++ b/src/main/resources/mapper/primary/AffiliateIncomeMapper.xml @@ -0,0 +1,36 @@ + + + + + + + + + diff --git a/src/main/resources/mapper/primary/AffiliateMapper.xml b/src/main/resources/mapper/primary/AffiliateMapper.xml new file mode 100644 index 00000000..8dbe92da --- /dev/null +++ b/src/main/resources/mapper/primary/AffiliateMapper.xml @@ -0,0 +1,16 @@ + + + + + + +