新增功能 -- 产品订阅 年度/月度

This commit is contained in:
2024-11-18 16:20:25 +08:00
parent 4a23e33080
commit 8d27b5b51e
21 changed files with 1029 additions and 318 deletions

View File

@@ -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";
}

View 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;
}

View File

@@ -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);
}
}
}

View File

@@ -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){
}
}
}

View 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();
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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");
}
}
}

View File

@@ -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");
}
}

View File

@@ -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> {
}

View File

@@ -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;
}

View File

@@ -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;
}

View 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;
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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();
}

View File

@@ -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)
);
}
}

View File

@@ -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<>();

View File

@@ -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());
}
}
}

View File

@@ -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
#stripe.private-key=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
#stripe.webhook-sign-secret=whsec_hhGDgdelQRHSg4LmChtQe41crj41eb11