新增功能 -- 产品订阅 年度/月度
This commit is contained in:
@@ -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";
|
||||
|
||||
|
||||
}
|
||||
|
||||
25
src/main/java/com/ai/da/common/enums/ProductEnum.java
Normal file
25
src/main/java/com/ai/da/common/enums/ProductEnum.java
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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<OrderInfo> orderInfoList = orderInfoService.getNoPayOrderByDuration(5, PayTypeEnum.ALIPAY.getType());
|
||||
|
||||
for (OrderInfo orderInfo : orderInfoList) {
|
||||
String orderNo = orderInfo.getOrderNo();
|
||||
log.warn("超时订单 ===> {}", orderNo);
|
||||
|
||||
//核实订单状态:调用支付宝查单接口
|
||||
aliPayService.checkOrderStatus(orderNo);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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){
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
96
src/main/java/com/ai/da/common/task/PaymentTask.java
Normal file
96
src/main/java/com/ai/da/common/task/PaymentTask.java
Normal file
@@ -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<OrderInfo> 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<OrderInfo> 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<OrderInfo> 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();
|
||||
}
|
||||
}
|
||||
@@ -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<OrderInfo> orderInfoList = orderInfoService.getNoPayOrderByDuration(30, PayTypeEnum.PAYPAL.getType());
|
||||
|
||||
for (OrderInfo orderInfo : orderInfoList) {
|
||||
String orderNo = orderInfo.getOrderNo();
|
||||
log.warn("超时订单 ===> {}", orderNo);
|
||||
|
||||
//核实订单状态:调用支付宝查单接口
|
||||
payPalCheckoutService.checkOrderStatus(orderNo);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<OrderInfo> orderInfoList = orderInfoService.getNoPayOrderByDuration(30, PayTypeEnum.STRIPE.getType());
|
||||
|
||||
for (OrderInfo orderInfo : orderInfoList) {
|
||||
String orderNo = orderInfo.getOrderNo();
|
||||
log.warn("超时订单 ===> {}", orderNo);
|
||||
|
||||
//核实订单状态:调用支付宝查单接口
|
||||
stripeService.checkOrderStatus(orderNo);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String> pay(@PathVariable Integer amount, @RequestParam String returnUrl) {
|
||||
return Response.success(stripeService.pay(amount, returnUrl));
|
||||
@PostMapping("/createOrder")
|
||||
public Response<String> pay(@Validate @RequestBody ProductPurchaseDTO productPurchaseDTO) {
|
||||
return Response.success(stripeService.pay(productPurchaseDTO));
|
||||
}
|
||||
|
||||
@ApiOperation("支付通知")
|
||||
@PostMapping("/trade/notify")
|
||||
public Response<String> 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<String> cancelSubscription(@RequestParam String subscriptionId) {
|
||||
stripeService.cancelSubscription(subscriptionId);
|
||||
return Response.success("success");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<SubscriptionInfo> {
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
32
src/main/java/com/ai/da/model/dto/ProductPurchaseDTO.java
Normal file
32
src/main/java/com/ai/da/model/dto/ProductPurchaseDTO.java
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
OrderInfo createOrderByProductId(Integer productId, String paymentType);
|
||||
|
||||
OrderInfo createOrderByProductId(Integer amount, String paymentType, ProductEnum product);
|
||||
|
||||
void saveCodeUrl(String orderNo, String codeUrl);
|
||||
|
||||
List<OrderInfo> listOrderByCreateTimeDesc();
|
||||
@@ -28,4 +31,7 @@ public interface OrderInfoService extends IService<OrderInfo> {
|
||||
PageBaseResponse<OrderInfo> getOrderByPage(QueryPageByTimeDTO queryPageByTimeDTO);
|
||||
|
||||
void updateOrderNoById(Long id, String orderNo);
|
||||
|
||||
void updateTotalFeeByOrderNo(String orderNo);
|
||||
|
||||
}
|
||||
|
||||
@@ -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<PaymentInfo> {
|
||||
|
||||
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);
|
||||
|
||||
|
||||
@@ -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<Subscription> getSubscription(String name, String userEmail) throws StripeException;
|
||||
|
||||
void cancelSubscription(String orderNo);
|
||||
|
||||
Map<String, String> getPaymentMethod(String paymentMethodId);
|
||||
|
||||
/*void updateSubscription(String subscriptionId);
|
||||
|
||||
void resume(String subscriptionId);*/
|
||||
|
||||
void subscriptionReminder();
|
||||
|
||||
void checkSubscriptionExpiration();
|
||||
|
||||
}
|
||||
|
||||
@@ -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<OrderInfoMapper, OrderInfo
|
||||
@Resource
|
||||
private ProductMapper productMapper;
|
||||
|
||||
@Resource
|
||||
private PaymentInfoMapper paymentInfoMapper;
|
||||
|
||||
@Override
|
||||
public OrderInfo createOrderByProductId(Integer amount, String paymentType) {
|
||||
|
||||
|
||||
//查找已存在但未支付的订单
|
||||
/*OrderInfo orderInfo = this.getNoPayOrderByProductId(amount, paymentType);
|
||||
if( orderInfo != null){
|
||||
@@ -51,7 +59,7 @@ public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo
|
||||
//生成订单
|
||||
OrderInfo orderInfo = new OrderInfo();
|
||||
orderInfo.setAccountId(accountId);
|
||||
orderInfo.setTitle("积分购买 X" + amount );
|
||||
orderInfo.setTitle("积分购买 X" + amount);
|
||||
orderInfo.setOrderNo(OrderNoUtils.getOrderNo()); //订单号 ??
|
||||
// orderInfo.setProductId(amount);
|
||||
// orderInfo.setTotalFee(Integer.parseInt(CreditsEventsEnum.PRICE.getValue()) * amount); // 元 HKD
|
||||
@@ -63,8 +71,37 @@ public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo
|
||||
return orderInfo;
|
||||
}
|
||||
|
||||
public OrderInfo createOrderByProductId(Integer amount, String paymentType, ProductEnum product) {
|
||||
|
||||
//获取商品信息
|
||||
// Product product = productMapper.selectById(amount);
|
||||
String title;
|
||||
if (product.equals(ProductEnum.CreditsProduct)) {
|
||||
title = "积分购买 X" + amount;
|
||||
} else {
|
||||
title = product.getName();
|
||||
}
|
||||
AuthPrincipalVo userHolder = UserContext.getUserHolder();
|
||||
Long accountId = userHolder.getId();
|
||||
|
||||
//生成订单
|
||||
OrderInfo orderInfo = new OrderInfo();
|
||||
orderInfo.setAccountId(accountId);
|
||||
orderInfo.setTitle(title);
|
||||
orderInfo.setOrderNo(OrderNoUtils.getOrderNo()); // 自定义订单号
|
||||
// orderInfo.setProductId(amount);
|
||||
// orderInfo.setTotalFee(Integer.parseInt(CreditsEventsEnum.PRICE.getValue()) * amount); // 元 HKD
|
||||
orderInfo.setTotalFee((float) (product.getPrice() * amount)); // 元 HKD
|
||||
orderInfo.setOrderStatus(OrderStatusEnum.NOT_PAY.getType()); //未支付
|
||||
orderInfo.setPaymentType(paymentType);
|
||||
baseMapper.insert(orderInfo);
|
||||
|
||||
return orderInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 存储订单二维码
|
||||
*
|
||||
* @param orderNo
|
||||
* @param codeUrl
|
||||
*/
|
||||
@@ -82,6 +119,7 @@ public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo
|
||||
|
||||
/**
|
||||
* 查询订单列表,并倒序查询
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
@@ -93,6 +131,7 @@ public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo
|
||||
|
||||
/**
|
||||
* 根据订单号更新订单状态
|
||||
*
|
||||
* @param orderNo
|
||||
* @param orderStatus
|
||||
*/
|
||||
@@ -112,6 +151,7 @@ public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo
|
||||
|
||||
/**
|
||||
* 根据订单号获取订单状态
|
||||
*
|
||||
* @param orderNo
|
||||
* @return
|
||||
*/
|
||||
@@ -121,7 +161,7 @@ public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo
|
||||
QueryWrapper<OrderInfo> 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<OrderInfoMapper, OrderInfo
|
||||
|
||||
/**
|
||||
* 查询创建超过minutes分钟并且未支付的订单
|
||||
*
|
||||
* @param minutes
|
||||
* @return
|
||||
*/
|
||||
@@ -149,6 +190,7 @@ public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo
|
||||
|
||||
/**
|
||||
* 根据订单号获取订单
|
||||
*
|
||||
* @param orderNo
|
||||
* @return
|
||||
*/
|
||||
@@ -166,6 +208,7 @@ public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo
|
||||
/**
|
||||
* 根据商品id查询未支付订单
|
||||
* 防止重复创建订单对象
|
||||
*
|
||||
* @param productId
|
||||
* @return
|
||||
*/
|
||||
@@ -181,16 +224,16 @@ public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageBaseResponse<OrderInfo> getOrderByPage(QueryPageByTimeDTO queryPageByTimeDTO){
|
||||
public PageBaseResponse<OrderInfo> getOrderByPage(QueryPageByTimeDTO queryPageByTimeDTO) {
|
||||
QueryWrapper<OrderInfo> 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<OrderInfoMapper, OrderInfo
|
||||
return PageBaseResponse.success(orderInfo);
|
||||
}
|
||||
|
||||
public void updateOrderNoById(Long id, String orderNo){
|
||||
public void updateOrderNoById(Long id, String orderNo) {
|
||||
OrderInfo orderInfo = new OrderInfo();
|
||||
orderInfo.setId(id);
|
||||
orderInfo.setOrderNo(orderNo);
|
||||
|
||||
baseMapper.updateById(orderInfo);
|
||||
}
|
||||
|
||||
public void updateTotalFeeByOrderNo(String orderNo) {
|
||||
QueryWrapper<PaymentInfo> qw = new QueryWrapper<>();
|
||||
qw.eq("order_no", orderNo);
|
||||
List<PaymentInfo> paymentInfos = paymentInfoMapper.selectList(qw);
|
||||
Float sum = paymentInfos.stream()
|
||||
.map(PaymentInfo::getPayerTotal)
|
||||
.reduce(0f, Float::sum);
|
||||
|
||||
baseMapper.update(
|
||||
new OrderInfo(),
|
||||
new UpdateWrapper<OrderInfo>()
|
||||
.eq("order_no", orderNo)
|
||||
.set("total_fee", sum)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<PaymentInfoMapper, Payme
|
||||
paymentInfo.setOrderNo(orderNo);
|
||||
paymentInfo.setPaymentType(PayTypeEnum.WXPAY.getType());
|
||||
paymentInfo.setTransactionId(transactionId);
|
||||
paymentInfo.setTradeType(tradeType);
|
||||
paymentInfo.setTradeState(tradeState);
|
||||
// 原来的单位是:分 Int 现改为:元 Float
|
||||
paymentInfo.setPayerTotal(payerTotal / 100.0F);
|
||||
@@ -83,7 +88,6 @@ public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, Payme
|
||||
paymentInfo.setOrderNo(orderNo);
|
||||
paymentInfo.setPaymentType(PayTypeEnum.ALIPAY.getType());
|
||||
paymentInfo.setTransactionId(transactionId);
|
||||
paymentInfo.setTradeType("电脑网站支付");
|
||||
paymentInfo.setTradeState(tradeStatus);
|
||||
// 原来的单位是分 Int 现改为元 Long
|
||||
paymentInfo.setPayerTotal(totalAmountInt / 100.0F);
|
||||
@@ -107,7 +111,6 @@ public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, Payme
|
||||
paymentInfo.setOrderNo(order.id());
|
||||
paymentInfo.setPaymentType(PayTypeEnum.PAYPAL.getType());
|
||||
paymentInfo.setTransactionId(order.id());
|
||||
paymentInfo.setTradeType("电脑网站支付");
|
||||
paymentInfo.setTradeState(order.status());
|
||||
// todo 确认这里的数据单位是不是元
|
||||
paymentInfo.setPayerTotal(totalAmountFloat);
|
||||
@@ -139,7 +142,6 @@ public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, Payme
|
||||
paymentInfo.setOrderNo(orderNo);
|
||||
paymentInfo.setPaymentType(PayTypeEnum.ALIPAY_HK.getType());
|
||||
paymentInfo.setTransactionId(transactionId);
|
||||
paymentInfo.setTradeType("电脑网站支付");
|
||||
paymentInfo.setTradeState(tradeStatus);
|
||||
paymentInfo.setPayerTotal(totalAmountFloat);
|
||||
|
||||
@@ -150,10 +152,10 @@ public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, Payme
|
||||
baseMapper.insert(paymentInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createPaymentInfoForStripe(Session session){
|
||||
String orderId = session.getMetadata().get("orderId");
|
||||
String status = session.getStatus();
|
||||
// 获取transactionId,从sessionId更改为invoiceId
|
||||
String sessionId = session.getId();
|
||||
Long amountTotal = session.getAmountTotal();
|
||||
// stripe 的支付金额单位是分
|
||||
@@ -163,7 +165,6 @@ public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, Payme
|
||||
paymentInfo.setOrderNo(orderId);
|
||||
paymentInfo.setPaymentType(PayTypeEnum.STRIPE.getType());
|
||||
paymentInfo.setTransactionId(sessionId);
|
||||
paymentInfo.setTradeType("电脑网站支付");
|
||||
paymentInfo.setTradeState(status);
|
||||
paymentInfo.setPayerTotal(divide);
|
||||
Gson gson = new Gson();
|
||||
@@ -173,6 +174,54 @@ public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, Payme
|
||||
baseMapper.insert(paymentInfo);
|
||||
}
|
||||
|
||||
@Value("${stripe.private-key}")
|
||||
private String privateKey;
|
||||
public String createPaymentInfoForStripe(Invoice invoice){
|
||||
Stripe.apiKey = privateKey;
|
||||
// 获取transactionId,从sessionId更改为invoiceId
|
||||
String invoiceId = invoice.getId();
|
||||
|
||||
QueryWrapper<PaymentInfo> 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<PaymentInfo> qw = new QueryWrapper<>();
|
||||
|
||||
@@ -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<String, Object> 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<String, Object> metadata = new HashMap<>();
|
||||
metadata.put("orderId", orderId);
|
||||
//创建价格
|
||||
Map<String, Object> 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<String, Object> 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<SubscriptionInfo> 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<String, String> 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<Subscription> 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<Subscription> 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<Customer> 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<String, String> 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<String, String> 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<String, String> 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<SubscriptionInfo> qwSI = new QueryWrapper<>();
|
||||
qwSI.eq("subscription_id", subscriptionId);
|
||||
SubscriptionInfo subscriptionInfo = subscriptionInfoMapper.selectOne(qwSI);
|
||||
if (Objects.isNull(subscriptionInfo)) {
|
||||
return false;
|
||||
}
|
||||
QueryWrapper<PaymentInfo> qwPI = new QueryWrapper<>();
|
||||
qwPI.eq("order_no", subscriptionInfo.getOrderNo()).orderByDesc("id");
|
||||
List<PaymentInfo> 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<SubscriptionInfo> 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<SubscriptionInfo> 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<SubscriptionInfo> qw = new QueryWrapper<>();
|
||||
qw.lt("current_period_end", epochSecond);
|
||||
qw.eq("status", "active");
|
||||
List<SubscriptionInfo> subscriptionInfos = subscriptionInfoMapper.selectList(qw);
|
||||
|
||||
for (SubscriptionInfo subscriptionInfo : subscriptionInfos) {
|
||||
subscriptionInfo.setStatus("expired");
|
||||
subscriptionInfo.setUpdateTime(LocalDateTime.now());
|
||||
subscriptionInfoMapper.updateById(subscriptionInfo);
|
||||
log.info("用户 {} 的订阅 {} 已过期", subscriptionInfo.getAccountId(), subscriptionInfo.getOrderNo());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user