diff --git a/src/main/java/com/ai/da/common/constant/CommonConstant.java b/src/main/java/com/ai/da/common/constant/CommonConstant.java index 71f6c86b..35ff4504 100644 --- a/src/main/java/com/ai/da/common/constant/CommonConstant.java +++ b/src/main/java/com/ai/da/common/constant/CommonConstant.java @@ -75,5 +75,9 @@ public class CommonConstant { public static final String PORTFOLIO_DELETED_CN = "作品已删除"; + public static final String TIME_FORMAT_MMM_dd_yyyy_EEEE = "MMM. dd, yyyy, EEEE"; + public static final String TIME_FORMAT_MMM_dd_yyyy = "MMM. dd, yyyy"; + + public static final String AFFILIATE_LINK = "https://www.aida.com.hk?ref="; } diff --git a/src/main/java/com/ai/da/common/enums/OrderStatusEnum.java b/src/main/java/com/ai/da/common/enums/OrderStatusEnum.java index aac9719d..02767686 100644 --- a/src/main/java/com/ai/da/common/enums/OrderStatusEnum.java +++ b/src/main/java/com/ai/da/common/enums/OrderStatusEnum.java @@ -10,38 +10,34 @@ public enum OrderStatusEnum { * 未支付 */ NOT_PAY("未支付"), - - /** * 支付成功 */ SUCCESS("支付成功"), - + /** + * 支付失败 + */ + FAILURE("支付失败"), /** * 已关闭 */ TIMEOUT_CLOSED("超时已关闭"), - /** * 已取消 */ CANCEL("用户已取消"), - /** * 退款中 */ REFUND_PROCESSING("退款中"), - /** * 已退款 */ REFUND_SUCCESS("已退款"), - /** * 退款异常 */ REFUND_ABNORMAL("退款异常"), - /** * paypal订单状态为 APPROVED */ diff --git a/src/main/java/com/ai/da/common/enums/ProductEnum.java b/src/main/java/com/ai/da/common/enums/ProductEnum.java new file mode 100644 index 00000000..11237ffe --- /dev/null +++ b/src/main/java/com/ai/da/common/enums/ProductEnum.java @@ -0,0 +1,25 @@ +package com.ai.da.common.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum ProductEnum { + // 积分购买 + CreditsProduct("AiDA credits purchase", 6L), + // 年度订阅 + AnnualSubscription("AiDA Annual Subscription", 5000L), + // 月度订阅 + MonthlySubscription("AiDA Monthly Subscription", 500L), + // 测试 + DailySubscription("AiDA Daily Subscription", 5L), + ; + + /** + * 类型 + */ + private final String name; + + private final Long price; +} diff --git a/src/main/java/com/ai/da/common/task/AliPayTask.java b/src/main/java/com/ai/da/common/task/AliPayTask.java deleted file mode 100644 index 670a417c..00000000 --- a/src/main/java/com/ai/da/common/task/AliPayTask.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.ai.da.common.task; - -import com.ai.da.mapper.primary.entity.OrderInfo; -import com.ai.da.common.enums.PayTypeEnum; -import com.ai.da.service.AliPayService; -import com.ai.da.service.OrderInfoService; -import lombok.extern.slf4j.Slf4j; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Component; - -import javax.annotation.Resource; -import java.util.List; - -@Slf4j -@Component -public class AliPayTask { - - @Resource - private OrderInfoService orderInfoService; - - @Resource - private AliPayService aliPayService; - - /** - * 从第0秒开始每隔30秒执行1次,查询创建超过5分钟,并且未支付的订单 - */ -// @Scheduled(cron = "0/30 * * * * ?") - public void orderConfirm(){ - -// log.info("Alipay orderConfirm 被执行......"); - - List orderInfoList = orderInfoService.getNoPayOrderByDuration(5, PayTypeEnum.ALIPAY.getType()); - - for (OrderInfo orderInfo : orderInfoList) { - String orderNo = orderInfo.getOrderNo(); - log.warn("超时订单 ===> {}", orderNo); - - //核实订单状态:调用支付宝查单接口 - aliPayService.checkOrderStatus(orderNo); - } - } -} diff --git a/src/main/java/com/ai/da/common/task/GenerateTask.java b/src/main/java/com/ai/da/common/task/GenerateTask.java deleted file mode 100644 index 038976d0..00000000 --- a/src/main/java/com/ai/da/common/task/GenerateTask.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.ai.da.common.task; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Component; - -@Component -@Slf4j -public class GenerateTask { - -// @Scheduled(cron = "0 0 */1 * * ?") - public void generateScheduled(){ - log.info("测试定时器:generate"); - - try{ - - }catch(Exception e){ - - } - } -} diff --git a/src/main/java/com/ai/da/common/task/PaymentTask.java b/src/main/java/com/ai/da/common/task/PaymentTask.java new file mode 100644 index 00000000..40450055 --- /dev/null +++ b/src/main/java/com/ai/da/common/task/PaymentTask.java @@ -0,0 +1,121 @@ +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.*; +import com.paypal.http.exceptions.SerializeException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.time.LocalDate; +import java.util.List; + +@Slf4j +@Component +public class PaymentTask { + + @Resource + private OrderInfoService orderInfoService; + + @Resource + private StripeService stripeService; + + @Resource + private AffiliateService affiliateService; + + // 考虑删除该定时任务(原因:之后的订单列允许用户查看发票,发票未过期时仍可以支付,所以不需要手动使订单过期) +// @Scheduled(cron = "0/30 * * * * ?") + public void orderConfirmForStripe() throws SerializeException { + + // 查看超过30分钟以上仍未支付的订单 置为超时订单 + List orderInfoList = orderInfoService.getNoPayOrderByDuration(30, PayTypeEnum.STRIPE.getType()); + + for (OrderInfo orderInfo : orderInfoList) { + String orderNo = orderInfo.getOrderNo(); + log.warn("超时订单 ===> {}", orderNo); + + //核实订单状态:调用支付宝查单接口 + stripeService.checkOrderStatus(orderNo); + + } + } + + @Resource + private PayPalCheckoutService payPalCheckoutService; + + // @Scheduled(cron = "0/30 * * * * ?") + public void orderConfirmForPaypal() throws SerializeException { + +// log.info("PayPal orderConfirm 被执行......"); + + List orderInfoList = orderInfoService.getNoPayOrderByDuration(30, PayTypeEnum.PAYPAL.getType()); + + for (OrderInfo orderInfo : orderInfoList) { + String orderNo = orderInfo.getOrderNo(); + log.warn("超时订单 ===> {}", orderNo); + + //核实订单状态:调用支付宝查单接口 + payPalCheckoutService.checkOrderStatus(orderNo); + + } + } + + @Resource + private AliPayService aliPayService; + + /** + * 从第0秒开始每隔30秒执行1次,查询创建超过5分钟,并且未支付的订单 + */ +// @Scheduled(cron = "0/30 * * * * ?") + public void orderConfirmForAlipay(){ +/* + log.info("Alipay orderConfirm 被执行......"); + + List orderInfoList = orderInfoService.getNoPayOrderByDuration(5, PayTypeEnum.ALIPAY.getType()); + + for (OrderInfo orderInfo : orderInfoList) { + String orderNo = orderInfo.getOrderNo(); + log.warn("超时订单 ===> {}", orderNo); + + //核实订单状态:调用支付宝查单接口 + aliPayService.checkOrderStatus(orderNo); + }*/ + } + + // 提前7天向用户发送提醒邮件,每天早上8点执行 + @Scheduled(cron = "0 0 8 * * ?") + public void subscriptionReminder(){ + stripeService.subscriptionReminder(); + } + + // 每天凌晨检查subscription中有哪些已过期,更新状态 +// @Scheduled(cron = "0 0 0 * * ?") +// public void checkSubscriptionExpiration(){ +// stripeService.checkSubscriptionExpiration(); +// } + + // 如果有订阅已创建,但是没有发邮件通知的,需要主动获取回调信息并向用户发送邮件 + public void checkSubscriptionPayment(){ + // + + } + + @Scheduled(cron = "0 */5 * * * *") // Run every 5 minutes + public void updateAffiliateInfoWithPayment(){ + affiliateService.updateAffiliateInfoWithPayment(); + } + + @Scheduled(cron = "0 0 8 28-31 * ?") + public void commissionSummaryReminder(){ + // 每个月末的最后一天的早上八点执行 + LocalDate today = LocalDate.now(); + // 判断是否为月底 + if (today.plusDays(1).getDayOfMonth() == 1) { + log.info("今天是月底,执行佣金结算提醒任务!"); + affiliateService.commissionCalculation(null, null); + } + } + +} diff --git a/src/main/java/com/ai/da/common/task/PaypalTask.java b/src/main/java/com/ai/da/common/task/PaypalTask.java deleted file mode 100644 index fa069971..00000000 --- a/src/main/java/com/ai/da/common/task/PaypalTask.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.ai.da.common.task; - -import com.ai.da.common.enums.PayTypeEnum; -import com.ai.da.mapper.primary.entity.OrderInfo; -import com.ai.da.service.OrderInfoService; -import com.ai.da.service.PayPalCheckoutService; -import com.paypal.http.exceptions.SerializeException; -import lombok.extern.slf4j.Slf4j; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Component; -import org.springframework.stereotype.Service; - -import javax.annotation.Resource; -import java.util.List; - -@Slf4j -@Component -public class PaypalTask { - - @Resource - private OrderInfoService orderInfoService; - - @Resource - private PayPalCheckoutService payPalCheckoutService; - -// @Scheduled(cron = "0/30 * * * * ?") - public void orderConfirm() throws SerializeException { - -// log.info("PayPal orderConfirm 被执行......"); - - List orderInfoList = orderInfoService.getNoPayOrderByDuration(30, PayTypeEnum.PAYPAL.getType()); - - for (OrderInfo orderInfo : orderInfoList) { - String orderNo = orderInfo.getOrderNo(); - log.warn("超时订单 ===> {}", orderNo); - - //核实订单状态:调用支付宝查单接口 - payPalCheckoutService.checkOrderStatus(orderNo); - - } - } -} diff --git a/src/main/java/com/ai/da/common/task/StripeTask.java b/src/main/java/com/ai/da/common/task/StripeTask.java deleted file mode 100644 index d4a6fbaf..00000000 --- a/src/main/java/com/ai/da/common/task/StripeTask.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.ai.da.common.task; - -import com.ai.da.common.enums.PayTypeEnum; -import com.ai.da.mapper.primary.entity.OrderInfo; -import com.ai.da.service.OrderInfoService; -import com.ai.da.service.StripeService; -import com.paypal.http.exceptions.SerializeException; -import lombok.extern.slf4j.Slf4j; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Component; - -import javax.annotation.Resource; -import java.util.List; - -@Slf4j -@Component -public class StripeTask { - - @Resource - private OrderInfoService orderInfoService; - - @Resource - private StripeService stripeService; - -// @Scheduled(cron = "0/30 * * * * ?") - public void orderConfirm() throws SerializeException { - - // 查看超过30分钟以上仍未支付的订单 置为超时订单 - List orderInfoList = orderInfoService.getNoPayOrderByDuration(30, PayTypeEnum.STRIPE.getType()); - - for (OrderInfo orderInfo : orderInfoList) { - String orderNo = orderInfo.getOrderNo(); - log.warn("超时订单 ===> {}", orderNo); - - //核实订单状态:调用支付宝查单接口 - stripeService.checkOrderStatus(orderNo); - - } - } -} diff --git a/src/main/java/com/ai/da/common/utils/DateUtil.java b/src/main/java/com/ai/da/common/utils/DateUtil.java index 94269826..e1b1d755 100644 --- a/src/main/java/com/ai/da/common/utils/DateUtil.java +++ b/src/main/java/com/ai/da/common/utils/DateUtil.java @@ -9,8 +9,8 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; -import java.util.Calendar; import java.util.Date; +import java.util.Locale; import java.util.TimeZone; @Slf4j @@ -81,4 +81,21 @@ public class DateUtil { return String.valueOf(epochSecond).substring(0, 10); } + public static String changeTimeStampFormat(Long timeStamp, String type, String format){ + // 将秒级时间戳转换为毫秒级 + if (type.equals("seconds")){ + timeStamp = timeStamp * 1000; + } + // 输出格式 + SimpleDateFormat outputFormat = new SimpleDateFormat(format, Locale.ENGLISH); + // 创建Date对象 + Date date = new Date(timeStamp); + // 格式化输出 + return outputFormat.format(date); + } + + public static String changeTimeStampFormat(LocalDateTime localDate){ + return localDate.format(DateTimeFormatter.ofPattern("MMM. dd, yyyy, EEEE", Locale.US)); + } + } diff --git a/src/main/java/com/ai/da/common/utils/RedisUtil.java b/src/main/java/com/ai/da/common/utils/RedisUtil.java index 7d5295dc..7e0d9578 100644 --- a/src/main/java/com/ai/da/common/utils/RedisUtil.java +++ b/src/main/java/com/ai/da/common/utils/RedisUtil.java @@ -278,4 +278,18 @@ public class RedisUtil { // 设置过期时间为 5 分钟(300 秒) redisTemplate.expire(redisKey, 5, TimeUnit.MINUTES); } + + public final static String PAYMENT_INFO_LAST_SCAN_TIME = "PaymentInfoLastScanTime"; + + public final static String AFFILIATE_LINK_VIEW_KEY = "AffiliateLink:view:"; + + public void increaseAffiliateLinkViewCount(Long accountId) { + String key = AFFILIATE_LINK_VIEW_KEY + accountId; + redisTemplate.opsForValue().increment(key); + } + + public Long getAffiliateLinkViewCount(Long accountId) { + String key = AFFILIATE_LINK_VIEW_KEY + accountId; + return redisTemplate.opsForValue().increment(key, 0); + } } diff --git a/src/main/java/com/ai/da/common/utils/SendEmailUtil.java b/src/main/java/com/ai/da/common/utils/SendEmailUtil.java index 25e07339..9dd76934 100644 --- a/src/main/java/com/ai/da/common/utils/SendEmailUtil.java +++ b/src/main/java/com/ai/da/common/utils/SendEmailUtil.java @@ -2,8 +2,11 @@ package com.ai.da.common.utils; import com.ai.da.mapper.primary.entity.Account; import com.ai.da.mapper.primary.entity.TrialOrder; +import com.ai.da.model.dto.AffiliateEmailParamsDTO; +import com.ai.da.model.dto.SubscriptionEmailParamsDTO; import com.alibaba.fastjson.JSONObject; import com.ai.da.common.config.exception.BusinessException; +import com.alibaba.fastjson2.JSON; import com.tencentcloudapi.common.Credential; import com.tencentcloudapi.common.exception.TencentCloudSDKException; import com.tencentcloudapi.common.profile.ClientProfile; @@ -175,7 +178,7 @@ public class SendEmailUtil { subject = "Approval Confirmation for AiDA System Trial Access"; if (country.equals("China")) { template.setTemplateID(NOTIFICATION_CHINESE_TEMPLATE_ID); - }else { + } else { template.setTemplateID(NOTIFICATION_TEMPLATE_ID); } template.setTemplateData(buildNotificationData(trialOrder, link)); @@ -225,7 +228,7 @@ public class SendEmailUtil { attachment.setFileName(fileName); // 设置附件文件名 // 设置附件内容 attachment.setContent(Base64.getEncoder().encodeToString(fileContent)); - req.setAttachments(new Attachment[] {attachment}); + req.setAttachments(new Attachment[]{attachment}); // 发送邮件 SendEmailResponse resp = client.SendEmail(req); log.info("短信发送结果res###{}", SendEmailResponse.toJsonString(resp)); @@ -268,7 +271,9 @@ public class SendEmailUtil { throw new BusinessException("failed.to.send.mail"); } } + private final static Long WILLBEEXPIRED_TEMPLATE_ID = 118178L; + public static void sendWillBeExpiredEmail(Account account, String senderAddress) { try { // 实例化一个认证对象 @@ -360,7 +365,7 @@ public class SendEmailUtil { jsonObject.put("email", trialOrder.getEmail()); if (link) { jsonObject.put("days", 14); - }else { + } else { jsonObject.put("days", 5); } return jsonObject.toJSONString(); @@ -370,6 +375,7 @@ public class SendEmailUtil { private final static Long UPGRADE_SUCCESS_NOTIFICATION_ID = 118856L; private final static Long UPGRADE_NOTIFICATION_ID_CHINESE = 122898L; private final static Long UPGRADE_SUCCESS_NOTIFICATION_ID_CHINESE = 122899L; + public static void sendUpgradeNotification(Account account, String senderAddress, Integer type) { try { // 实例化一个认证对象 @@ -418,7 +424,8 @@ public class SendEmailUtil { } private final static Long GENERATE_EXCEPTION_WARNING_ID = 122589L; - public static void sendGenerateExceptionWarning(String message){ + + public static void sendGenerateExceptionWarning(String message) { try { // 实例化一个认证对象 Credential cred = new Credential(SECRET_ID, SECRET_KEy); @@ -457,7 +464,8 @@ public class SendEmailUtil { private final static Long QUESTIONNAIRE_FEEDBACK_EN_ID = 124151L; private final static Long QUESTIONNAIRE_FEEDBACK_CN_ID = 124156L; - public static void questionnaireRelatedNotify(String userName, String email, String language){ + + public static void questionnaireRelatedNotify(String userName, String email, String language) { try { // 实例化一个认证对象 Credential cred = new Credential(SECRET_ID, SECRET_KEy); @@ -502,7 +510,7 @@ public class SendEmailUtil { private final static Long RENEWAL_NOTIFICATION_FOR_OLD_USER_EN = 124892L; private final static Long RENEWAL_NOTIFICATION_FOR_OLD_USER_CN = 124891L; - public static void notificationForPaidUser(String receiverAddress, int emailType, String country, String userName, String date){ + public static void notificationForPaidUser(String receiverAddress, int emailType, String country, String userName, String date) { try { // 实例化一个认证对象 Credential cred = new Credential(SECRET_ID, SECRET_KEy); @@ -525,7 +533,7 @@ public class SendEmailUtil { subject = "Welcome to AiDA!"; if (country.equals("China")) { template.setTemplateID(NEW_USER_PAYMENT_NOTIFICATION_CN); - }else { + } else { template.setTemplateID(NEW_USER_PAYMENT_NOTIFICATION_EN); } parameter.put("userName", userName); @@ -536,7 +544,7 @@ public class SendEmailUtil { subject = "Account renewal notification"; if (country.equals("China")) { template.setTemplateID(RENEWAL_NOTIFICATION_FOR_OLD_USER_CN); - }else { + } else { template.setTemplateID(RENEWAL_NOTIFICATION_FOR_OLD_USER_EN); } break; @@ -597,8 +605,8 @@ public class SendEmailUtil { private final static Long NEW_USER_REGISTER_NOTIFICATION_EN = 126919L; - public static void notificationForRegisterUser(String receiverAddress){ - try{ + public static void notificationForRegisterUser(String receiverAddress) { + try { // 实例化一个认证对象,入参需要传入腾讯云账户 SecretId 和 SecretKey,此处还需注意密钥对的保密 // 代码泄露可能会导致 SecretId 和 SecretKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考,建议采用更安全的方式来使用密钥,请参见:https://cloud.tencent.com/document/product/1278/85305 // 密钥可前往官网控制台 https://console.cloud.tencent.com/cam/capi 进行获取 @@ -634,8 +642,8 @@ public class SendEmailUtil { private final static Long CHANGE_MAILBOX_CONFIRM_CN = 128278L; private final static Long CHANGE_MAILBOX_CONFIRM_EN = 128277L; - public static void changeMailboxConfirm(String receiverAddress, String language, String name, String link){ - try{ + public static void changeMailboxConfirm(String receiverAddress, String language, String name, String link) { + try { // 实例化一个认证对象,入参需要传入腾讯云账户 SecretId 和 SecretKey,此处还需注意密钥对的保密 // 代码泄露可能会导致 SecretId 和 SecretKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考,建议采用更安全的方式来使用密钥,请参见:https://cloud.tencent.com/document/product/1278/85305 // 密钥可前往官网控制台 https://console.cloud.tencent.com/cam/capi 进行获取 @@ -653,10 +661,10 @@ public class SendEmailUtil { req.setFromEmailAddress(SEND_ADDRESS); req.setDestination(new String[]{receiverAddress}); Template template = new Template(); - if (language.equals("ENGLISH")){ + if (language.equals("ENGLISH")) { req.setSubject("Change the email address bound to the AiDA account"); template.setTemplateID(CHANGE_MAILBOX_CONFIRM_EN); - }else { + } else { req.setSubject("更换AiDA账号绑定的邮箱地址"); template.setTemplateID(CHANGE_MAILBOX_CONFIRM_CN); } @@ -678,12 +686,12 @@ public class SendEmailUtil { private final static Long UPLOAD_TIMEOUT_REMINDER = 128324L; - public static void uploadTimeoutReminder(String userName, String time){ + public static void uploadTimeoutReminder(String userName, String time) { String xp = "xupei3360@163.com"; String shb = "shahaibodd99@gmail.com"; String wxd = "X1627315083@163.com"; String pkc = "kaicpang.pang@connect.polyu.hk"; - try{ + try { // 实例化一个认证对象,入参需要传入腾讯云账户 SecretId 和 SecretKey,此处还需注意密钥对的保密 // 代码泄露可能会导致 SecretId 和 SecretKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考,建议采用更安全的方式来使用密钥,请参见:https://cloud.tencent.com/document/product/1278/85305 // 密钥可前往官网控制台 https://console.cloud.tencent.com/cam/capi 进行获取 @@ -721,6 +729,7 @@ public class SendEmailUtil { private final static Long HALFPRICEPROMOTION_CN_ID = 128582L; private final static Long HALFPRICEPROMOTION_EN_ID = 128583L; + public static void halfPricePromotion(Account account, String senderAddress, int type) { try { // 实例化一个认证对象 @@ -750,7 +759,7 @@ public class SendEmailUtil { if (type == 1) { subject = "AiDA workshop - Win a trip to Hong Kong!"; template.setTemplateID(HALFPRICEPROMOTION_EN_ID); - }else { + } else { subject = "AiDA workshop - 赢取香港之旅"; template.setTemplateID(HALFPRICEPROMOTION_CN_ID); } @@ -808,4 +817,171 @@ 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; + private final static Long PAYMENT_FAILED_NEW_MERCHANT_EN = 131230L; + private final static Long PAYMENT_FAILED_RENEWAL_MERCHANT_EN = 131225L; + private final static Long PAYMENT_FAILED_RENEWAL_USER_EN = 131563L; + private final static Long PAYMENT_FAILED_RENEWAL_USER_CN = 131564L; + + public static void subscriptionEmailReminder(String type, SubscriptionEmailParamsDTO subscriptionEmailParamsDTO, String language, String receiverAddress) { + try { +// String merchantEmail = "kimwong@code-create.com.hk"; + String merchantEmail = "xupei3360@163.com"; + Credential cred = new Credential(SECRET_ID, SECRET_KEy); + // 实例化一个http选项,可选的,没有特殊需求可以跳过 + HttpProfile httpProfile = new HttpProfile(); + 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[]{merchantEmail}); + 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 "fail_new": + merchant.setSubject("[Code-Create] Payment Failed : New Order (" + subscriptionEmailParamsDTO.getOrderId() + ")"); + templateMerchant.setTemplateID(PAYMENT_FAILED_NEW_MERCHANT_EN); + break; + case "fail_renewal": + merchant.setSubject("[Code-Create] Payment Failed : Renewal Order (" + subscriptionEmailParamsDTO.getOrderId() + ")"); + templateMerchant.setTemplateID(PAYMENT_FAILED_RENEWAL_MERCHANT_EN); + // todo to user + if (language.equals("ENGLISH")) { + user.setSubject("[Code-Create] Payment Failed : Renewal Order (" + subscriptionEmailParamsDTO.getOrderId() + ")"); + templateUser.setTemplateID(PAYMENT_FAILED_RENEWAL_USER_EN); + } else { + user.setSubject("[Code-Create] 自动续费失败 (" + subscriptionEmailParamsDTO.getOrderId() + ")"); + templateUser.setTemplateID(PAYMENT_FAILED_RENEWAL_USER_CN); + } + break; + case "new": + merchant.setSubject("[Code-Create] New Order(" + subscriptionEmailParamsDTO.getOrderId() + ")"); + 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); + + if (!type.equals("cancel") && !type.equals("fail_new")) { + // 返回的resp是一个SendEmailResponse的实例,与请求对象对应 + SendEmailResponse respUser = client.SendEmail(user); + log.info("邮件主题:{},发送结果toUser###{}", user.getSubject(), SendEmailResponse.toJsonString(respUser)); + } + if (!type.equals("reminder")) { + SendEmailResponse respMerchant = client.SendEmail(merchant); + log.info("邮件主题:{},发送结果toMerchant###{}", merchant.getSubject(), SendEmailResponse.toJsonString(respMerchant)); + } + } catch (TencentCloudSDKException e) { + log.info("邮件发送失败###{}", e.toString()); + throw new BusinessException("failed.to.send.mail"); + } + } + + private final static Long NEW_REGISTRATION = 132123L; + private final static Long AFFILIATE_ACCEPTED = 132124L; + private final static Long AFFILIATE_REFUSED = 132125L; + private final static Long AFFILIATE_MONTHLY_SUMMARY = 132126L; + + public static void affiliateEmailReminder(String receiverAddress, AffiliateEmailParamsDTO paramsDTO, String type) { + try { + Credential cred = new Credential(SECRET_ID, SECRET_KEy); + // 实例化一个http选项,可选的,没有特殊需求可以跳过 + HttpProfile httpProfile = new HttpProfile(); + httpProfile.setEndpoint("ses.tencentcloudapi.com"); + // 实例化一个client选项,可选的,没有特殊需求可以跳过 + ClientProfile clientProfile = new ClientProfile(); + clientProfile.setHttpProfile(httpProfile); + // 实例化要请求产品的client对象,clientProfile是可选的 + SesClient client = new SesClient(cred, "ap-hongkong", clientProfile); + // 实例化一个请求对象,每个接口都会对应一个request对象 + SendEmailRequest req = new SendEmailRequest(); + req.setFromEmailAddress(SEND_ADDRESS); + req.setDestination(new String[]{receiverAddress}); + Template template = new Template(); + switch (type) { + case "new": + req.setSubject("New Affiliate Registration"); + template.setTemplateID(NEW_REGISTRATION); + break; + case "accepted": + req.setSubject("Affiliate Application Accepted"); + template.setTemplateID(AFFILIATE_ACCEPTED); + break; + case "refused": + req.setSubject("Affiliate Application Refused"); + template.setTemplateID(AFFILIATE_REFUSED); + break; + case "summary": + req.setSubject("Your Monthly AffiliateWP Summary for AiDA"); + template.setTemplateID(AFFILIATE_MONTHLY_SUMMARY); + break; + } + + template.setTemplateData(JSON.toJSONString(paramsDTO)); + req.setTemplate(template); + + // 返回的resp是一个SendEmailResponse的实例,与请求对象对应 + SendEmailResponse resp = client.SendEmail(req); + log.info("短信发送结果res###{}", SendEmailResponse.toJsonString(resp)); + } catch (TencentCloudSDKException e) { + log.info("邮件发送失败###{}", e.toString()); + throw new BusinessException("failed.to.send.mail"); + } + } + } diff --git a/src/main/java/com/ai/da/controller/AffiliateController.java b/src/main/java/com/ai/da/controller/AffiliateController.java new file mode 100644 index 00000000..1be751ac --- /dev/null +++ b/src/main/java/com/ai/da/controller/AffiliateController.java @@ -0,0 +1,86 @@ +package com.ai.da.controller; + + +import com.ai.da.common.response.Response; +import com.ai.da.mapper.primary.entity.Affiliate; +import com.ai.da.model.dto.AffiliateQueryDTO; +import com.ai.da.model.vo.AffiliateInvitationDetailsVO; +import com.ai.da.model.vo.AffiliateVO; +import com.ai.da.service.AffiliateService; +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +@Slf4j +@RestController +@RequestMapping("/api/affiliate") +@Api(tags = "Affiliate模块") +public class AffiliateController { + + @Resource + private AffiliateService affiliateService; + + @ApiOperation(value = "注册成为affiliate") + @GetMapping("/registration") + public Response completeGuidance(@RequestParam(value = "promotionMethod", required = false) String promotionMethod) { + return Response.success(affiliateService.registerAsAnAffiliate(promotionMethod)); + } + + @ApiOperation(value = "获取affiliate列表") + @PostMapping("/list") + public Response> getAffiliateList(@Valid @RequestBody AffiliateQueryDTO affiliateQueryDTO) { + return Response.success(affiliateService.getAffiliateList(affiliateQueryDTO)); + } + + @ApiOperation(value = "获取affiliate个人中心") + @GetMapping("/personalCenter") + public Response personalAffiliateCenter() { + return Response.success(affiliateService.personalAffiliateCenter()); + } + + @ApiOperation(value = "获取个人佣金图表数据") + @GetMapping("/getPersonalMonthlyIncome") + public Response getPersonalMonthlyIncome(@RequestParam("year")int year) { + return Response.success(affiliateService.getPersonalMonthlyIncome(year)); + } + + @ApiOperation(value = "审批affiliate申请") + @GetMapping("/approval") + public Response applicationApproval(@RequestParam("id") Long id, @RequestParam("isApproved")Boolean isApproved) { + return Response.success(affiliateService.applicationApproval(id, isApproved)); + } + + /*@ApiOperation(value = "定时计算佣金") + @GetMapping("/testTask") + public Response testTask() { + affiliateService.updateAffiliateInfoWithPayment(); + return Response.success("success "); + }*/ + + /*@ApiOperation(value = "每月发送结算邮件") + @GetMapping("/commissionCalculation") + public Response commissionCalculation() { + affiliateService.commissionCalculation(null, null); + return Response.success("success "); + }*/ + + @ApiOperation(value = "affiliate链接浏览量增加") + @GetMapping("/viewsIncrease") + public Response viewsGet(@RequestParam("id") Long id) { + return Response.success(affiliateService.affiliateLinkViewsIncrease(id)); + } + + @ApiOperation(value = "获取每个affiliate产生的收入") + @PostMapping("/getEachAffiliateGeneratedRevenue") + public Response> getEachAffiliateGeneratedRevenue(@RequestBody AffiliateQueryDTO affiliateQueryDTO) { + return Response.success(affiliateService.getEachAffiliateGeneratedRevenue(affiliateQueryDTO)); + } + + +} diff --git a/src/main/java/com/ai/da/controller/ElementController.java b/src/main/java/com/ai/da/controller/ElementController.java index 40dbcde1..8065c6dc 100644 --- a/src/main/java/com/ai/da/controller/ElementController.java +++ b/src/main/java/com/ai/da/controller/ElementController.java @@ -66,6 +66,8 @@ public class ElementController { return Response.success(); } + /** 该功能已删除 */ + @Deprecated @ApiOperation(value = "生成印花") @PostMapping("/generatePrint") public Response generatePrint(@Valid @RequestBody CollectionGeneratePrintDTO generatePrintDTO) { diff --git a/src/main/java/com/ai/da/controller/OrderInfoController.java b/src/main/java/com/ai/da/controller/OrderInfoController.java index 3a9ce1d5..56e60a48 100644 --- a/src/main/java/com/ai/da/controller/OrderInfoController.java +++ b/src/main/java/com/ai/da/controller/OrderInfoController.java @@ -3,9 +3,10 @@ package com.ai.da.controller; import com.ai.da.common.enums.OrderStatusEnum; import com.ai.da.common.response.PageBaseResponse; import com.ai.da.common.response.Response; -import com.ai.da.mapper.primary.entity.OrderInfo; import com.ai.da.model.dto.QueryPageByTimeDTO; +import com.ai.da.model.vo.OrderListVO; import com.ai.da.service.OrderInfoService; +import com.ai.da.service.PaymentInfoService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.web.bind.annotation.*; @@ -22,10 +23,13 @@ public class OrderInfoController { @Resource private OrderInfoService orderInfoService; + @Resource + private PaymentInfoService paymentInfoService; + @ApiOperation("订单列表") @PostMapping("/list") - public Response> list(@Valid @RequestBody QueryPageByTimeDTO queryPageByTimeDTO){ - PageBaseResponse orderByAccountId = orderInfoService.getOrderByPage(queryPageByTimeDTO); + public Response> list(@Valid @RequestBody QueryPageByTimeDTO queryPageByTimeDTO){ + PageBaseResponse orderByAccountId = paymentInfoService.getPaymentInfo(queryPageByTimeDTO); // List list = orderInfoService.listOrderByCreateTimeDesc(); return Response.success(orderByAccountId); } diff --git a/src/main/java/com/ai/da/controller/StripeController.java b/src/main/java/com/ai/da/controller/StripeController.java index 70430fab..82c5e1cc 100644 --- a/src/main/java/com/ai/da/controller/StripeController.java +++ b/src/main/java/com/ai/da/controller/StripeController.java @@ -1,47 +1,55 @@ 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.springframework.web.bind.annotation.*; +import springfox.documentation.annotations.ApiIgnore; import javax.annotation.Resource; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; import java.io.IOException; +import java.util.List; +import java.util.Map; @Api(tags = "Stripe模块") @Slf4j @RestController @RequestMapping("/api/stripe") +@ApiIgnore public class StripeController { @Resource private StripeService stripeService; @ApiOperation("创建支付链接") - @PostMapping("/createOrder/{amount}") - public Response pay(@PathVariable Integer amount, @RequestParam String returnUrl) { - return Response.success(stripeService.pay(amount, returnUrl)); + @PostMapping("/createOrder") + public Response pay(@Valid @RequestBody ProductPurchaseDTO productPurchaseDTO) { + return Response.success(stripeService.pay(productPurchaseDTO)); } @ApiOperation("支付通知") @PostMapping("/trade/notify") - public Response callback(HttpServletRequest request) throws ServletException, IOException { + public void callback(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Boolean result = stripeService.notify(request); if (result){ - return Response.success(200,"success"); + response.setStatus(HttpServletResponse.SC_OK); }else { - return Response.fail(400,"failure"); + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); } } @ApiOperation("申请退款") - @PostMapping("/trade/refund/{orderNo}/{reason}") + @GetMapping("/trade/refund/{orderNo}/{reason}") public Response> refund(@PathVariable String orderNo, @PathVariable String reason) throws IOException { String response = stripeService.refund(null,orderNo,reason); if (response.equals("退款成功")){ @@ -51,4 +59,57 @@ public class StripeController { } } + @ApiOperation("获取订阅") + @GetMapping("/getSubscription") + public Response> getSubscription(@RequestParam String name, @RequestParam String email) { + try { + return Response.success(stripeService.getSubscriptionIds(name, email)); + } catch (StripeException e) { + throw new RuntimeException(e); + } + } + + @ApiOperation("取消订阅") + @GetMapping("/cancelSubscription") + public Response cancelSubscription(@RequestParam String subscriptionId, @RequestParam(required = false) String reason) { + stripeService.cancelSubscription(subscriptionId, reason); + return Response.success("success"); + } + @ApiOperation("临时 取消订阅") + @GetMapping("/cancelSubscriptionTemp") + public Response cancelSubscriptionTemp(@RequestParam String subscriptionId) { + stripeService.cancelSubscriptionTemp(subscriptionId); + return Response.success("success"); + } + + @ApiOperation("创建订阅 临时") + @GetMapping("/createSubscriptionTemp") + public Response createSubscriptionTemp(@RequestParam String name, @RequestParam String email) { + return Response.success(stripeService.createSubscriptionTemp(name, email)); + } + + @ApiOperation("修改用户默认支付方式 临时") + @GetMapping("/changeCustomerPayment") + public Response changeCustomerPayment(@RequestParam String name, @RequestParam String email) { + return Response.success(stripeService.changeCustomerPayment(name, email)); + } + + @ApiOperation("临时 发送续订失败邮件") + @GetMapping("/sendRenewalFailEmail") + public Response sendRenewalFailEmail(@RequestParam String invoiceId, @RequestParam String subscriptionId, @RequestParam String orderNo) { + return Response.success(stripeService.sendRenewalFailEmail(invoiceId, subscriptionId,orderNo)); + } + + @ApiOperation("临时 查询指定用户绑定的付款方式") + @GetMapping("/getCustomerPaymentMethod") + public Response>> getCustomerPaymentMethod(@RequestParam String name, @RequestParam String email) { + return Response.success(stripeService.getCustomerPaymentMethod(name, email)); + } + + @ApiOperation("临时 解绑指定用户绑定的所有付款方式") + @GetMapping("/detachCustomerAllPaymentMethod") + public Response detachCustomerAllPaymentMethod(@RequestParam String name, @RequestParam String email) { + return Response.success(stripeService.detachCustomerAllPaymentMethod(name, email)); + } + } diff --git a/src/main/java/com/ai/da/mapper/primary/AffiliateIncomeMapper.java b/src/main/java/com/ai/da/mapper/primary/AffiliateIncomeMapper.java new file mode 100644 index 00000000..d0a7d6ce --- /dev/null +++ b/src/main/java/com/ai/da/mapper/primary/AffiliateIncomeMapper.java @@ -0,0 +1,14 @@ +package com.ai.da.mapper.primary; + +import com.ai.da.mapper.primary.entity.AffiliateIncome; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +import java.util.List; +import java.util.Map; + +public interface AffiliateIncomeMapper extends BaseMapper { + + List> getPersonalMonthlyIncome(Long affiliateAccountId, int year); + + List> getMonthlyAffiliateIncome(int year, int month); +} diff --git a/src/main/java/com/ai/da/mapper/primary/AffiliateMapper.java b/src/main/java/com/ai/da/mapper/primary/AffiliateMapper.java new file mode 100644 index 00000000..3974b904 --- /dev/null +++ b/src/main/java/com/ai/da/mapper/primary/AffiliateMapper.java @@ -0,0 +1,11 @@ +package com.ai.da.mapper.primary; + +import com.ai.da.mapper.primary.entity.Affiliate; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +import java.util.Map; + +public interface AffiliateMapper extends BaseMapper { + + Map getMonthlyApprovedAffiliate(int year, int month); +} diff --git a/src/main/java/com/ai/da/mapper/primary/PaymentInfoMapper.java b/src/main/java/com/ai/da/mapper/primary/PaymentInfoMapper.java index 27b147a4..ea23d39d 100644 --- a/src/main/java/com/ai/da/mapper/primary/PaymentInfoMapper.java +++ b/src/main/java/com/ai/da/mapper/primary/PaymentInfoMapper.java @@ -1,7 +1,14 @@ package com.ai.da.mapper.primary; import com.ai.da.mapper.primary.entity.PaymentInfo; +import com.ai.da.model.vo.OrderListVO; import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import java.util.List; + public interface PaymentInfoMapper extends BaseMapper { + + List selectPageOrderList(Long accountId, String startTime, String endTime, int offset, int pageSize); + + int queryOrderListTotalCount(Long accountId, String startTime, String endTime); } diff --git a/src/main/java/com/ai/da/mapper/primary/SubscriptionInfoMapper.java b/src/main/java/com/ai/da/mapper/primary/SubscriptionInfoMapper.java new file mode 100644 index 00000000..8a45bb7e --- /dev/null +++ b/src/main/java/com/ai/da/mapper/primary/SubscriptionInfoMapper.java @@ -0,0 +1,8 @@ +package com.ai.da.mapper.primary; + +import com.ai.da.mapper.primary.entity.SubscriptionInfo; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +public interface SubscriptionInfoMapper extends BaseMapper { + +} diff --git a/src/main/java/com/ai/da/mapper/primary/entity/Affiliate.java b/src/main/java/com/ai/da/mapper/primary/entity/Affiliate.java new file mode 100644 index 00000000..beb52662 --- /dev/null +++ b/src/main/java/com/ai/da/mapper/primary/entity/Affiliate.java @@ -0,0 +1,30 @@ +package com.ai.da.mapper.primary.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +@Data +@TableName("t_affiliate") +public class Affiliate extends BaseEntity{ + + private Long accountId; + + // Active(活跃) || Inactive(过期) || Pending(待审批) || Refused(拒绝) + private String status; + + private Float totalEarnings = 0.00F; + + private Float monthlyEarnings = 0.00F; + + private Float unpaidEarnings = 0.00F; + + private Integer visits = 0; + + private Boolean approved = false; + + private String link; + + private String promotionMethod; +} diff --git a/src/main/java/com/ai/da/mapper/primary/entity/AffiliateIncome.java b/src/main/java/com/ai/da/mapper/primary/entity/AffiliateIncome.java new file mode 100644 index 00000000..eb8ce12c --- /dev/null +++ b/src/main/java/com/ai/da/mapper/primary/entity/AffiliateIncome.java @@ -0,0 +1,26 @@ +package com.ai.da.mapper.primary.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; + +@EqualsAndHashCode(callSuper = true) +@Data +@TableName("t_affiliate_income") +public class AffiliateIncome extends BaseEntity { + + private Long affiliateId; + + private Long affiliateAccountId; + + private Long inviteeAccountId; + + private Float amount; + + private LocalDateTime paymentTime; + + private Float commission; + +} diff --git a/src/main/java/com/ai/da/mapper/primary/entity/OrderInfo.java b/src/main/java/com/ai/da/mapper/primary/entity/OrderInfo.java index 98a020b9..04a9d894 100644 --- a/src/main/java/com/ai/da/mapper/primary/entity/OrderInfo.java +++ b/src/main/java/com/ai/da/mapper/primary/entity/OrderInfo.java @@ -21,5 +21,12 @@ public class OrderInfo extends BaseEntity{ private String orderStatus;//订单状态 + private String note; + private String paymentType;//支付方式 + + // 可用于标记用户订单是否首次订阅 + private byte isFirstSubscription = 0; + + private byte isCommissionCalculated = 0; } diff --git a/src/main/java/com/ai/da/mapper/primary/entity/PaymentInfo.java b/src/main/java/com/ai/da/mapper/primary/entity/PaymentInfo.java index c3876106..d3533078 100644 --- a/src/main/java/com/ai/da/mapper/primary/entity/PaymentInfo.java +++ b/src/main/java/com/ai/da/mapper/primary/entity/PaymentInfo.java @@ -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,22 @@ 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; + + private String paymentMethod; + + private String last4; + + // 发票托管页面 + private String hostedInvoiceUrl; } diff --git a/src/main/java/com/ai/da/mapper/primary/entity/SubscriptionInfo.java b/src/main/java/com/ai/da/mapper/primary/entity/SubscriptionInfo.java new file mode 100644 index 00000000..7ee32ab0 --- /dev/null +++ b/src/main/java/com/ai/da/mapper/primary/entity/SubscriptionInfo.java @@ -0,0 +1,39 @@ +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 cancelNotified = (byte)0; + + // 续订的下一个付款日 + private String nextPayDate; + + // 当前订阅订单有效期开始时间 + private Long currentPeriodStart; + + // 当前订阅订单有效期结束时间 + private Long currentPeriodEnd; + + // 取消订阅原因 + private String cancelReason; + +} diff --git a/src/main/java/com/ai/da/model/dto/AffiliateEmailParamsDTO.java b/src/main/java/com/ai/da/model/dto/AffiliateEmailParamsDTO.java new file mode 100644 index 00000000..38c87b72 --- /dev/null +++ b/src/main/java/com/ai/da/model/dto/AffiliateEmailParamsDTO.java @@ -0,0 +1,31 @@ +package com.ai.da.model.dto; + +import lombok.Data; + +@Data +public class AffiliateEmailParamsDTO { + + private String username; + + private String promotionMethod; + + private String totalProgramRevenue; + + private String newApprovedAffiliates; + + private String unpaidEarnings; + + private String paidEarnings; + + public AffiliateEmailParamsDTO() { + } + + public AffiliateEmailParamsDTO(String username) { + this.username = username; + } + + public AffiliateEmailParamsDTO(String username, String promotionMethod) { + this.username = username; + this.promotionMethod = promotionMethod; + } +} diff --git a/src/main/java/com/ai/da/model/dto/AffiliateQueryDTO.java b/src/main/java/com/ai/da/model/dto/AffiliateQueryDTO.java new file mode 100644 index 00000000..79fac3e7 --- /dev/null +++ b/src/main/java/com/ai/da/model/dto/AffiliateQueryDTO.java @@ -0,0 +1,31 @@ +package com.ai.da.model.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +@Data +@ApiModel("查询affiliate列表") +public class AffiliateQueryDTO extends TimeQueryBaseDTO{ + @ApiModelProperty("Active(活跃) || Inactive(过期) || Pending(待审批) || Refused(拒绝)") + private String status; + + @ApiModelProperty("推广者id") + private Long affiliateId; + + @ApiModelProperty("按时间 DESC 降序 || ASC 升序") + private String order = "ASC"; + + @Override + public String toString() { + return "AffiliateQueryDTO{" + + "status='" + status + '\'' + ' ' + + "startTime='" + super.getStartTime() + '\'' + ' ' + + "endTime='" + super.getEndTime() + '\'' + ' ' + + "page='" + super.getPage() + '\'' + ' ' + + "size='" + super.getSize() + '\'' + ' ' + + '}'; + } +} diff --git a/src/main/java/com/ai/da/model/dto/GenerateThroughImageTextDTO.java b/src/main/java/com/ai/da/model/dto/GenerateThroughImageTextDTO.java index 8dbeca50..772cb8cf 100644 --- a/src/main/java/com/ai/da/model/dto/GenerateThroughImageTextDTO.java +++ b/src/main/java/com/ai/da/model/dto/GenerateThroughImageTextDTO.java @@ -12,49 +12,50 @@ import javax.validation.constraints.NotNull; public class GenerateThroughImageTextDTO { @NotNull(message = "userId cannot be empty") @ApiModelProperty("用户id") - Long userId; + private Long userId; @ApiModelProperty("caption | prompt") - String text; + private String text; @ApiModelProperty("图片在t_collection_element表中的id") - Long collectionElementId; + private Long collectionElementId; // todo 后续取消这个字段的传输,由后端自行判断相关参数是否有值 // @NotBlank(message = "you have to choose the generate type") @ApiModelProperty("text image text-image") - String generateType; + private String generateType; @ApiModelProperty("图片来源:update,从library中选择,从toProductImage结果中选择 collection || library || productImage") - String designType; + private String designType; @NotBlank(message = "level1Type cannot be empty!") @ApiModelProperty("Moodboard Printboard Sketchboard MarketingSketch") - String level1Type; + private String level1Type; @ApiModelProperty("Outwear Dress Blouse Skirt Trousers || Logo Slogan Pattern") - String level2Type; + private String level2Type; @ApiModelProperty("性别") - String gender; + private String gender; - @ApiModelProperty("选择的模型名") - String version; + + @ApiModelProperty("选择的模型名 high || fast") + private String version; @NotBlank(message = "timeZone cannot be empty!") @ApiModelProperty("本地时区,比如 'Asia/Tokyo' 东京时间 , 'Asia/Shanghai' 北京时间 由js本地获取") - String timeZone; + private String timeZone; @ApiModelProperty("唯一id,用于保持消息唯一性") - String uniqueId; + private String uniqueId; @NotNull(message = "Please check if the required fields are empty.(isTestUser)") @ApiModelProperty("是否是测试用户") - Boolean isTestUser; + private Boolean isTestUser; @ApiModelProperty("页面上用户设计的slogan所截的图片") - String sloganBase64; + private String sloganBase64; @ApiModelProperty("种子 取值范围 0~500") - String seed; + private String seed; } diff --git a/src/main/java/com/ai/da/model/dto/ProductPurchaseDTO.java b/src/main/java/com/ai/da/model/dto/ProductPurchaseDTO.java new file mode 100644 index 00000000..0e386e8a --- /dev/null +++ b/src/main/java/com/ai/da/model/dto/ProductPurchaseDTO.java @@ -0,0 +1,30 @@ +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; +} diff --git a/src/main/java/com/ai/da/model/dto/SubscriptionEmailParamsDTO.java b/src/main/java/com/ai/da/model/dto/SubscriptionEmailParamsDTO.java new file mode 100644 index 00000000..1f35b705 --- /dev/null +++ b/src/main/java/com/ai/da/model/dto/SubscriptionEmailParamsDTO.java @@ -0,0 +1,54 @@ +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; + + private String last4; + + // 订阅Id + private String subscriptionId; + + // 订阅方式 + private String subscriptionType; + + // 订阅开始时间 + private String startDate; + + // 下一个支付日期 + private String nextPayDate; + + // 下次付款时间(reminder) + private String renewalTime; + + // 付款失败原因 + private String failMessage; + + private String accountPageRef; + + +} diff --git a/src/main/java/com/ai/da/model/dto/TimeQueryBaseDTO.java b/src/main/java/com/ai/da/model/dto/TimeQueryBaseDTO.java new file mode 100644 index 00000000..3c10297b --- /dev/null +++ b/src/main/java/com/ai/da/model/dto/TimeQueryBaseDTO.java @@ -0,0 +1,19 @@ +package com.ai.da.model.dto; + +import com.ai.da.model.vo.PageQueryBaseVo; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +@Data +@ApiModel("按时间查询") +public class TimeQueryBaseDTO extends PageQueryBaseVo { + + @ApiModelProperty("按时间区间查询 区间起点") + private String startTime; + + @ApiModelProperty("按时间区间查询 区间终点") + private String endTime; +} diff --git a/src/main/java/com/ai/da/model/vo/AccountLoginVO.java b/src/main/java/com/ai/da/model/vo/AccountLoginVO.java index e3d02b58..5496c82f 100644 --- a/src/main/java/com/ai/da/model/vo/AccountLoginVO.java +++ b/src/main/java/com/ai/da/model/vo/AccountLoginVO.java @@ -46,4 +46,22 @@ public class AccountLoginVO { private String Language; + // 订阅id(stripe提供) + private String subscriptionId; + + // 订阅状态 + private String status; + + // 订阅过期时间 + private String expireTime; + + // 订阅类型 month || year + private String subscriptionType; + + // 是否自动续订 + private boolean isAutoRenewal; + + // 是否是affiliate + private boolean isAffiliate = false; + } diff --git a/src/main/java/com/ai/da/model/vo/AffiliateInvitationDetailsVO.java b/src/main/java/com/ai/da/model/vo/AffiliateInvitationDetailsVO.java new file mode 100644 index 00000000..4a9cdf54 --- /dev/null +++ b/src/main/java/com/ai/da/model/vo/AffiliateInvitationDetailsVO.java @@ -0,0 +1,25 @@ +package com.ai.da.model.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class AffiliateInvitationDetailsVO { + + private Long accountId; + + private String username; + + private Float firstSubscriptionPaymentAmount; + + private Float commission; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone="GMT+8") + private LocalDateTime time; +} diff --git a/src/main/java/com/ai/da/model/vo/AffiliateVO.java b/src/main/java/com/ai/da/model/vo/AffiliateVO.java new file mode 100644 index 00000000..96177e1a --- /dev/null +++ b/src/main/java/com/ai/da/model/vo/AffiliateVO.java @@ -0,0 +1,15 @@ +package com.ai.da.model.vo; + +import com.ai.da.mapper.primary.entity.Affiliate; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class AffiliateVO extends Affiliate { + + private Long linkViewCount; + +} diff --git a/src/main/java/com/ai/da/model/vo/OrderListVO.java b/src/main/java/com/ai/da/model/vo/OrderListVO.java new file mode 100644 index 00000000..44e42bff --- /dev/null +++ b/src/main/java/com/ai/da/model/vo/OrderListVO.java @@ -0,0 +1,29 @@ +package com.ai.da.model.vo; + + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 用于订单列表展示(展示的是所有支付信息) + */ +@Data +public class OrderListVO { + + private Long id; + + private Float amount; + + private String paymentMethod; + + private String state; + + private String orderType; + + private String invoiceLink; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone="GMT+8") + private LocalDateTime createTime; +} diff --git a/src/main/java/com/ai/da/service/AffiliateService.java b/src/main/java/com/ai/da/service/AffiliateService.java new file mode 100644 index 00000000..9ceb1cb1 --- /dev/null +++ b/src/main/java/com/ai/da/service/AffiliateService.java @@ -0,0 +1,31 @@ +package com.ai.da.service; + +import com.ai.da.mapper.primary.entity.Affiliate; +import com.ai.da.model.dto.AffiliateQueryDTO; +import com.ai.da.model.vo.AffiliateInvitationDetailsVO; +import com.ai.da.model.vo.AffiliateVO; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.service.IService; + +public interface AffiliateService extends IService { + + Boolean registerAsAnAffiliate(String promotionMethod); + + IPage getAffiliateList(AffiliateQueryDTO affiliateQueryDTO); + + AffiliateVO personalAffiliateCenter(); + + double[] getPersonalMonthlyIncome(int year); + + Boolean applicationApproval(Long id, Boolean isApproved); + + void updateAffiliateInfoWithPayment(); + + Boolean affiliateLinkViewsIncrease(Long id); + + IPage getEachAffiliateGeneratedRevenue(AffiliateQueryDTO affiliateQueryDTO); + + Affiliate getByAccountId(Long accountId); + + void commissionCalculation(Integer year, Integer month); +} diff --git a/src/main/java/com/ai/da/service/CreditsService.java b/src/main/java/com/ai/da/service/CreditsService.java index 1d076527..62d9e5e4 100644 --- a/src/main/java/com/ai/da/service/CreditsService.java +++ b/src/main/java/com/ai/da/service/CreditsService.java @@ -17,9 +17,9 @@ public interface CreditsService extends IService { String getCredits(Long accountId); - void creditsRefund(Long accountId, Integer quantity); + void creditsRefund(Long accountId, Integer quantity, String orderNo); - void insertToCreditsDetail(Long accountId, String changeEvent, String credits, String changeType); + void insertToCreditsDetail(Long accountId, String changeEvent, String credits, String changeType, String orderNo); PageBaseResponse queryCreditsDetailsPage(QueryIncomeOrExpenditureDTO queryPageByTimeDTO); diff --git a/src/main/java/com/ai/da/service/OrderInfoService.java b/src/main/java/com/ai/da/service/OrderInfoService.java index 35da4881..a420fe70 100644 --- a/src/main/java/com/ai/da/service/OrderInfoService.java +++ b/src/main/java/com/ai/da/service/OrderInfoService.java @@ -2,6 +2,7 @@ package com.ai.da.service; import com.ai.da.common.enums.OrderStatusEnum; +import com.ai.da.common.enums.ProductEnum; import com.ai.da.common.response.PageBaseResponse; import com.ai.da.mapper.primary.entity.OrderInfo; import com.ai.da.model.dto.QueryPageByTimeDTO; @@ -13,6 +14,8 @@ public interface OrderInfoService extends IService { OrderInfo createOrderByProductId(Integer productId, String paymentType); + OrderInfo createOrderByProductId(Integer amount, String paymentType, ProductEnum product); + void saveCodeUrl(String orderNo, String codeUrl); List listOrderByCreateTimeDesc(); @@ -28,4 +31,7 @@ public interface OrderInfoService extends IService { PageBaseResponse getOrderByPage(QueryPageByTimeDTO queryPageByTimeDTO); void updateOrderNoById(Long id, String orderNo); + + void updateTotalFeeByOrderNo(String orderNo); + } diff --git a/src/main/java/com/ai/da/service/PaymentInfoService.java b/src/main/java/com/ai/da/service/PaymentInfoService.java index 74492683..761c0d8b 100644 --- a/src/main/java/com/ai/da/service/PaymentInfoService.java +++ b/src/main/java/com/ai/da/service/PaymentInfoService.java @@ -1,13 +1,19 @@ package com.ai.da.service; +import com.ai.da.common.response.PageBaseResponse; import com.ai.da.mapper.primary.entity.PaymentInfo; import com.ai.da.model.dto.AlipayHKCallbackDTO; +import com.ai.da.model.dto.QueryPageByTimeDTO; +import com.ai.da.model.vo.OrderListVO; +import com.baomidou.mybatisplus.extension.service.IService; import com.paypal.orders.Order; -import com.stripe.model.checkout.Session; +import com.stripe.model.Charge; +import com.stripe.model.Invoice; +import java.util.List; import java.util.Map; -public interface PaymentInfoService { +public interface PaymentInfoService extends IService { void createPaymentInfo(String plainText); @@ -17,9 +23,13 @@ public interface PaymentInfoService { void createPaymentInfoForAliPayHK(AlipayHKCallbackDTO alipayHKCallbackDTO); - void createPaymentInfoForStripe(Session session); + PaymentInfo createOrUpdatePaymentInfoForStripe(Invoice invoice); - PaymentInfo getPaymentInfoByOrderId(String orderId); + PaymentInfo createOrUpdatePaymentInfoForStripe(Charge charge); + + List getPaymentInfoByOrderNo(String orderId, String order); void updatePaymentStatusById(Long id, String status, String content); + + PageBaseResponse getPaymentInfo(QueryPageByTimeDTO queryPageByTimeDTO); } diff --git a/src/main/java/com/ai/da/service/StripeService.java b/src/main/java/com/ai/da/service/StripeService.java index 98709597..978b7c41 100644 --- a/src/main/java/com/ai/da/service/StripeService.java +++ b/src/main/java/com/ai/da/service/StripeService.java @@ -1,15 +1,50 @@ package com.ai.da.service; +import com.ai.da.mapper.primary.entity.SubscriptionInfo; +import com.ai.da.model.dto.ProductPurchaseDTO; +import com.stripe.exception.StripeException; + 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); + SubscriptionInfo getLatestSubscriptionInfoByAccountId(Long accountId); + String refund(String amount, String orderId, String reason); void checkOrderStatus(String orderNo); + + List getSubscriptionIds(String name, String userEmail) throws StripeException; + + Map getPaymentMethodByInvoiceId(String invoiceId); + + void cancelSubscription(String orderNo, String cancelReason); + + void cancelSubscriptionTemp(String subscriptionId); + + Map getPaymentMethod(String paymentMethodId); + + /*void updateSubscription(String subscriptionId); + + void resume(String subscriptionId);*/ + + void subscriptionReminder(); + + void checkSubscriptionExpiration(); + + String createSubscriptionTemp(String name, String email); + + String changeCustomerPayment(String name, String email); + + boolean sendRenewalFailEmail(String invoiceId, String subscriptionId, String orderNo); + + List> getCustomerPaymentMethod(String name, String email); + + String detachCustomerAllPaymentMethod(String name, String email); } diff --git a/src/main/java/com/ai/da/service/impl/AccountServiceImpl.java b/src/main/java/com/ai/da/service/impl/AccountServiceImpl.java index fc3022a1..ca71c47d 100644 --- a/src/main/java/com/ai/da/service/impl/AccountServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/AccountServiceImpl.java @@ -104,6 +104,12 @@ public class AccountServiceImpl extends ServiceImpl impl @Resource private RedisUtil redisUtil; + @Resource + private StripeService stripeService; + + @Resource + private AffiliateService affiliateService; + @Override @Transactional(rollbackFor = Exception.class) public AccountPreLoginVO preLogin(AccountPreLoginDTO accountDTO) { @@ -2418,6 +2424,19 @@ public class AccountServiceImpl extends ServiceImpl impl response.setAccountExtendList(accountExtends); } response.setLanguage(Language.valueOf(account.getLanguage()).name()); + SubscriptionInfo subscriptionInfo = stripeService.getLatestSubscriptionInfoByAccountId(accountId); + if (!Objects.isNull(subscriptionInfo)) { + response.setSubscriptionId(subscriptionInfo.getSubscriptionId()); + response.setSubscriptionType(subscriptionInfo.getType()); + response.setStatus(subscriptionInfo.getStatus()); + response.setExpireTime(String.valueOf(subscriptionInfo.getCurrentPeriodEnd())); + response.setAutoRenewal(subscriptionInfo.getStatus().equals("active")); + } + + Affiliate affiliate = affiliateService.getByAccountId(accountId); + if (!Objects.isNull(affiliate) && affiliate.getStatus().equals("Active")) { + response.setAffiliate(true); + } return response; } diff --git a/src/main/java/com/ai/da/service/impl/AffiliateServiceImpl.java b/src/main/java/com/ai/da/service/impl/AffiliateServiceImpl.java new file mode 100644 index 00000000..220aefe6 --- /dev/null +++ b/src/main/java/com/ai/da/service/impl/AffiliateServiceImpl.java @@ -0,0 +1,295 @@ +package com.ai.da.service.impl; + +import com.ai.da.common.config.exception.BusinessException; +import com.ai.da.common.constant.CommonConstant; +import com.ai.da.common.context.UserContext; +import com.ai.da.common.response.ResultEnum; +import com.ai.da.common.utils.CopyUtil; +import com.ai.da.common.utils.RedisUtil; +import com.ai.da.common.utils.SendEmailUtil; +import com.ai.da.mapper.primary.AccountMapper; +import com.ai.da.mapper.primary.AffiliateIncomeMapper; +import com.ai.da.mapper.primary.AffiliateMapper; +import com.ai.da.mapper.primary.SubscriptionInfoMapper; +import com.ai.da.mapper.primary.entity.*; +import com.ai.da.model.dto.AffiliateEmailParamsDTO; +import com.ai.da.model.dto.AffiliateQueryDTO; +import com.ai.da.model.vo.AffiliateInvitationDetailsVO; +import com.ai.da.model.vo.AffiliateVO; +import com.ai.da.model.vo.AuthPrincipalVo; +import com.ai.da.service.AccountService; +import com.ai.da.service.AffiliateService; +import com.ai.da.service.OrderInfoService; +import com.ai.da.service.PaymentInfoService; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.mysql.cj.util.StringUtils; +import io.netty.util.internal.StringUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; + +@Service +@Slf4j +public class AffiliateServiceImpl extends ServiceImpl implements AffiliateService { + + @Resource + private OrderInfoService orderInfoService; + + @Resource + private AccountService accountService; + + @Resource + private PaymentInfoService paymentInfoService; + + @Resource + private SubscriptionInfoMapper subscriptionInfoMapper; + + @Resource + private AffiliateIncomeMapper affiliateIncomeMapper; + + @Resource + private RedisUtil redisUtil; + + // 推广者注册 + public Boolean registerAsAnAffiliate(String promotionMethod){ + AuthPrincipalVo userHolder = UserContext.getUserHolder(); + // 判断该用户是否已注册 + QueryWrapper qw = new QueryWrapper<>(); + qw.eq("account_id", userHolder.getId()); + Affiliate affiliate = baseMapper.selectOne(qw); + if (Objects.isNull(affiliate)){ + affiliate = new Affiliate(); + affiliate.setAccountId(userHolder.getId()); + affiliate.setStatus("Pending"); + affiliate.setCreateTime(LocalDateTime.now()); + affiliate.setPromotionMethod(promotionMethod); + baseMapper.insert(affiliate); + // 邮件通知审批者 +// String email = "kimwong@code-create.com.hk"; + String email = "xupei3360@163.com"; + SendEmailUtil.affiliateEmailReminder(email, new AffiliateEmailParamsDTO(userHolder.getUsername(), promotionMethod), "new"); + }else { + throw new BusinessException("You have registered an Affiliate", ResultEnum.PROMPT.getCode()); + } + return true; + } + + public IPage getAffiliateList(AffiliateQueryDTO affiliateQueryDTO){ + log.info("parameter => {}", affiliateQueryDTO.toString()); + QueryWrapper qw = new QueryWrapper<>(); + qw.eq(!StringUtils.isNullOrEmpty(affiliateQueryDTO.getStatus()), "status", affiliateQueryDTO.getStatus()) + .gt(!StringUtils.isNullOrEmpty(affiliateQueryDTO.getStartTime()), "create_time", affiliateQueryDTO.getStartTime()) + .lt(!StringUtils.isNullOrEmpty(affiliateQueryDTO.getEndTime()), "create_time", affiliateQueryDTO.getEndTime()) + .eq(!Objects.isNull(affiliateQueryDTO.getAffiliateId()), "id", affiliateQueryDTO.getAffiliateId()) + .orderByDesc(affiliateQueryDTO.getOrder().equals("DESC"), "create_time"); + return baseMapper.selectPage(new Page<>(affiliateQueryDTO.getPage(), affiliateQueryDTO.getSize()), qw); + } + + public AffiliateVO personalAffiliateCenter(){ + QueryWrapper qw = new QueryWrapper<>(); + Long accountId = UserContext.getUserHolder().getId(); + qw.eq("account_id", accountId); + Affiliate affiliate = baseMapper.selectOne(qw); + AffiliateVO affiliateVO = CopyUtil.copyObject(affiliate, AffiliateVO.class); + affiliateVO.setLinkViewCount(getAffiliateLinkViewCount(affiliate.getId())); + return affiliateVO; + } + + public double[] getPersonalMonthlyIncome(int year){ + Long accountId = UserContext.getUserHolder().getId(); + List> personalMonthlyIncome = affiliateIncomeMapper.getPersonalMonthlyIncome(accountId, year); + double[] commissions = new double[12]; + personalMonthlyIncome.forEach(income -> { + int month = Integer.parseInt(income.get("yearMonth").toString()); + commissions[month-1] = (double)income.get("totalCommission"); + }); + + return commissions; + } + + // 审批申请 + public Boolean applicationApproval(Long id, Boolean isApproved){ + Affiliate affiliate = baseMapper.selectById(id); + + // 1、更新db状态 + if (isApproved){ + // 更新状态 + affiliate.setStatus("Active"); + affiliate.setApproved(true); + affiliate.setLink(CommonConstant.AFFILIATE_LINK + affiliate.getId()); + } else { + affiliate.setStatus("Refused"); + affiliate.setApproved(false); + } + affiliate.setUpdateTime(LocalDateTime.now()); + baseMapper.updateById(affiliate); + + // 2、将批准结果邮件通知用户 + Account account = accountService.getById(affiliate.getAccountId()); + String userEmail = account.getUserEmail(); + String userName = account.getUserName(); + if (isApproved){ + SendEmailUtil.affiliateEmailReminder(userEmail, new AffiliateEmailParamsDTO(userName), "accepted"); + }else { + SendEmailUtil.affiliateEmailReminder(userEmail, new AffiliateEmailParamsDTO(userName), "refused"); + } + return true; + } + + // 定时计算佣金 + public void updateAffiliateInfoWithPayment(){ + // id存redis + String lastTime = redisUtil.getFromString(RedisUtil.PAYMENT_INFO_LAST_SCAN_TIME); + String currentTime = LocalDateTime.now().toString(); + // 1、查上次更新之后有无新订单 + QueryWrapper queryWrapper = new QueryWrapper<>(); + if (!StringUtil.isNullOrEmpty(lastTime)){ + queryWrapper.gt("create_time", lastTime); + } + queryWrapper.eq("type","new").eq("trade_state", "paid"); + + List paymentInfos = paymentInfoService.getBaseMapper().selectList(queryWrapper); + if (!paymentInfos.isEmpty()){ + paymentInfos.forEach(paymentInfo -> { + // 2、根据order_no查付款用户id + OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(paymentInfo.getOrderNo()); + if (Objects.isNull(orderInfo)){ + return; + } + Long accountId = orderInfo.getAccountId(); + // 3、查该用户之前是否有初次订阅的订单 + QueryWrapper qwOrderInfo = new QueryWrapper<>(); + qwOrderInfo.eq("account_id", accountId).eq("is_first_subscription", 1); + List orderInfos = orderInfoService.getBaseMapper().selectList(qwOrderInfo); + // 该用户首次订阅(非首次订阅,不分配佣金) + if (orderInfos.isEmpty()){ + // 查询是否绑定affiliateId + Account account = accountService.getById(accountId); + if (!Objects.isNull(account.getInvitationCode())){ + // 3、若有, 直接更新affiliate的所得 + Affiliate affiliate = baseMapper.selectById(account.getInvitationCode()); + Float payerTotal = paymentInfo.getPayerTotal(); + + if (payerTotal > 0){ + // 分配新用户首次订阅所付费用的25%作为佣金 + BigDecimal commission = BigDecimal.valueOf(payerTotal).multiply(new BigDecimal("0.25")); + BigDecimal monthlyEarning = BigDecimal.valueOf(affiliate.getMonthlyEarnings()).add(commission); + BigDecimal unpaidEarnings = BigDecimal.valueOf(affiliate.getUnpaidEarnings()).add(commission); + int visits = affiliate.getVisits() + 1; + affiliate.setMonthlyEarnings(monthlyEarning.floatValue()); + affiliate.setUnpaidEarnings(unpaidEarnings.floatValue()); + affiliate.setVisits(visits); + affiliate.setUpdateTime(LocalDateTime.now()); + baseMapper.updateById(affiliate); + + orderInfo.setIsCommissionCalculated((byte)1); + + // 4、添加到t_affiliate_income + AffiliateIncome affiliateIncome = new AffiliateIncome(); + affiliateIncome.setAffiliateId(affiliate.getId()); + affiliateIncome.setAffiliateAccountId(affiliate.getAccountId()); + affiliateIncome.setInviteeAccountId(accountId); + affiliateIncome.setAmount(payerTotal); + affiliateIncome.setPaymentTime(paymentInfo.getCreateTime()); + affiliateIncome.setCommission(commission.floatValue()); + affiliateIncome.setCreateTime(LocalDateTime.now()); + affiliateIncomeMapper.insert(affiliateIncome); + } + } + orderInfo.setIsFirstSubscription((byte)1); + orderInfo.setUpdateTime(LocalDateTime.now()); + orderInfoService.updateById(orderInfo); + } + }); + } + redisUtil.addToString(RedisUtil.PAYMENT_INFO_LAST_SCAN_TIME, currentTime); + } + + public Boolean affiliateLinkViewsIncrease(Long affiliateId) { + redisUtil.increaseAffiliateLinkViewCount(affiliateId); + return Boolean.TRUE; + } + + private Long getAffiliateLinkViewCount(Long affiliateId) { + return redisUtil.getAffiliateLinkViewCount(affiliateId); + } + + // 查看每个affiliate带来收入的详情 + @Override + public IPage getEachAffiliateGeneratedRevenue(AffiliateQueryDTO affiliateQueryDTO) { + if (Objects.isNull(affiliateQueryDTO.getAffiliateId())){ + throw new BusinessException("Please specify the affiliate ID.", ResultEnum.PROMPT.getCode()); + } + + QueryWrapper affiliateIncomeQueryWrapper = new QueryWrapper<>(); + affiliateIncomeQueryWrapper + .gt(!StringUtils.isNullOrEmpty(affiliateQueryDTO.getStartTime()), "create_time", affiliateQueryDTO.getStartTime()) + .lt(!StringUtils.isNullOrEmpty(affiliateQueryDTO.getEndTime()), "create_time", affiliateQueryDTO.getEndTime()) + .eq(!Objects.isNull(affiliateQueryDTO.getAffiliateId()), "affiliate_id", affiliateQueryDTO.getAffiliateId()) + .orderByDesc(affiliateQueryDTO.getOrder().equals("DESC"), "create_time"); + IPage affiliateIncomePage = affiliateIncomeMapper.selectPage(new Page<>(affiliateQueryDTO.getPage(), affiliateQueryDTO.getSize()), affiliateIncomeQueryWrapper); + return affiliateIncomePage.convert((Function) affiliateIncome -> { + AffiliateInvitationDetailsVO affiliateInvitationDetailsVO = CopyUtil.copyObject(affiliateIncome, AffiliateInvitationDetailsVO.class); + affiliateInvitationDetailsVO.setAccountId(affiliateIncome.getInviteeAccountId()); + affiliateInvitationDetailsVO.setUsername(accountService.getBaseMapper().selectById(affiliateIncome.getInviteeAccountId()).getUserName()); + affiliateInvitationDetailsVO.setFirstSubscriptionPaymentAmount(affiliateIncome.getAmount()); + affiliateInvitationDetailsVO.setCommission(affiliateIncome.getCommission()); + affiliateInvitationDetailsVO.setTime(affiliateIncome.getPaymentTime()); + return affiliateInvitationDetailsVO; + }); + } + + public void commissionCalculation(Integer year, Integer month) { + if (Objects.isNull(year)) { + year = LocalDateTime.now().getYear(); + } + if (Objects.isNull(month)) { + month = LocalDateTime.now().getMonthValue(); + } + + List> monthlyAffiliateIncome = affiliateIncomeMapper.getMonthlyAffiliateIncome(year, month); + // 1、总收入(近一个月通过affiliate产生的收入),未支付的金额 affiliate表中unpaid的总和 + Double totalAmount = 0.0; + Double totalCommission = 0.0; + if (!monthlyAffiliateIncome.isEmpty()){ + Map monthlyIncome = monthlyAffiliateIncome.get(0); + totalAmount = (Double) monthlyIncome.get("totalAmount"); + totalCommission = (Double) monthlyIncome.get("totalCommission"); + } + + // 2、本月新注册的Affiliate + Map monthlyApprovedAffiliate = baseMapper.getMonthlyApprovedAffiliate(year, month); + Long count = monthlyApprovedAffiliate.get("count"); + + AffiliateEmailParamsDTO affiliateEmailParamsDTO = new AffiliateEmailParamsDTO(); + affiliateEmailParamsDTO.setTotalProgramRevenue(totalAmount.toString()); + affiliateEmailParamsDTO.setNewApprovedAffiliates(count.toString()); + affiliateEmailParamsDTO.setUnpaidEarnings(totalCommission.toString()); + affiliateEmailParamsDTO.setPaidEarnings("0"); + + String receiverEmail = "xupei3360@163.com"; +// String receiverEmail = "kimwong@code-create.com.hk"; + // 邮件通知 + SendEmailUtil.affiliateEmailReminder(receiverEmail, affiliateEmailParamsDTO, "summary"); + } + + @Override + public Affiliate getByAccountId(Long accountId) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("account_id", accountId).orderByDesc("id").last("limit 1"); + + return baseMapper.selectOne(queryWrapper); + } + + +} diff --git a/src/main/java/com/ai/da/service/impl/AliPayServiceImpl.java b/src/main/java/com/ai/da/service/impl/AliPayServiceImpl.java index ccbd47d2..04fc8993 100644 --- a/src/main/java/com/ai/da/service/impl/AliPayServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/AliPayServiceImpl.java @@ -217,7 +217,8 @@ public class AliPayServiceImpl implements AliPayService { creditsService.insertToCreditsDetail(orderByOrderNo.getAccountId(), CreditsEventsEnum.BUY_CREDITS.getName() + "--Alipay", String.valueOf((Long.parseLong(CreditsEventsEnum.BUY_CREDITS.getValue()) * quantity)), - "positive"); + "positive", + orderByOrderNo.getOrderNo()); } finally { //要主动释放锁 lock.unlock(); @@ -320,7 +321,8 @@ public class AliPayServiceImpl implements AliPayService { creditsService.insertToCreditsDetail(orderByOrderNo.getAccountId(), CreditsEventsEnum.BUY_CREDITS.getName() + "--Alipay", String.valueOf((Long.parseLong(CreditsEventsEnum.BUY_CREDITS.getValue()) * quantity)), - "positive"); + "positive", + orderByOrderNo.getOrderNo()); } } @@ -393,7 +395,7 @@ public class AliPayServiceImpl implements AliPayService { // 更新积分状态 OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(orderNo); // creditsService.creditsRefund(orderByOrderNo.getAccountId(), orderByOrderNo.getTotalFee() / Integer.parseInt(CreditsEventsEnum.PRICE.getValue())); - creditsService.creditsRefund(orderByOrderNo.getAccountId(), (int)(orderByOrderNo.getTotalFee() / Float.parseFloat(CreditsEventsEnum.PRICE.getValue()))); + creditsService.creditsRefund(orderByOrderNo.getAccountId(), (int)(orderByOrderNo.getTotalFee() / Float.parseFloat(CreditsEventsEnum.PRICE.getValue())), orderNo); } else { log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg()); diff --git a/src/main/java/com/ai/da/service/impl/AlipayHKServiceImpl.java b/src/main/java/com/ai/da/service/impl/AlipayHKServiceImpl.java index f20d7350..417a03bd 100644 --- a/src/main/java/com/ai/da/service/impl/AlipayHKServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/AlipayHKServiceImpl.java @@ -248,7 +248,8 @@ public class AlipayHKServiceImpl implements AlipayHKService { creditsService.insertToCreditsDetail(orderByOrderNo.getAccountId(), CreditsEventsEnum.BUY_CREDITS.getName() + "--AlipayHK", String.valueOf((Long.parseLong(CreditsEventsEnum.BUY_CREDITS.getValue()) * quantity)), - "positive"); + "positive", + orderByOrderNo.getOrderNo()); log.info("用户:{} 积分信息更新成功",orderByOrderNo.getAccountId()); } finally { //要主动释放锁 diff --git a/src/main/java/com/ai/da/service/impl/CollectionElementServiceImpl.java b/src/main/java/com/ai/da/service/impl/CollectionElementServiceImpl.java index ca26b261..73f0ec99 100644 --- a/src/main/java/com/ai/da/service/impl/CollectionElementServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/CollectionElementServiceImpl.java @@ -216,6 +216,8 @@ public class CollectionElementServiceImpl extends ServiceImpl增 negative->减 */ @Override - public void insertToCreditsDetail(Long accountId, String changeEvent, String credits, String changeType) { + public void insertToCreditsDetail(Long accountId, String changeEvent, String credits, String changeType, String orderNo) { CreditsDetail creditsDetail = new CreditsDetail(); Account account = accountMapper.selectById(accountId); // BigDecimal finalCredits; @@ -137,6 +137,7 @@ public class CreditsServiceImpl extends ServiceImpl i if (b) creditsService.insertToCreditsDetail(accountId, CreditsEventsEnum.TO_PRODUCT_IMAGE.getName(), CreditsEventsEnum.TO_PRODUCT_IMAGE.getValue(), - "negative"); + "negative", null); } } @@ -764,7 +764,7 @@ public class GenerateServiceImpl extends ServiceImpl i if (b) creditsService.insertToCreditsDetail(accountId, CreditsEventsEnum.RELIGHT.getName(), CreditsEventsEnum.RELIGHT.getValue(), - "negative"); + "negative", null); } } diff --git a/src/main/java/com/ai/da/service/impl/OrderInfoServiceImpl.java b/src/main/java/com/ai/da/service/impl/OrderInfoServiceImpl.java index 416d84d2..ae12e612 100644 --- a/src/main/java/com/ai/da/service/impl/OrderInfoServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/OrderInfoServiceImpl.java @@ -4,15 +4,19 @@ package com.ai.da.service.impl; import com.ai.da.common.context.UserContext; import com.ai.da.common.enums.CreditsEventsEnum; import com.ai.da.common.enums.OrderStatusEnum; +import com.ai.da.common.enums.ProductEnum; import com.ai.da.common.response.PageBaseResponse; import com.ai.da.common.utils.OrderNoUtils; import com.ai.da.mapper.primary.OrderInfoMapper; +import com.ai.da.mapper.primary.PaymentInfoMapper; import com.ai.da.mapper.primary.ProductMapper; import com.ai.da.mapper.primary.entity.OrderInfo; +import com.ai.da.mapper.primary.entity.PaymentInfo; import com.ai.da.model.dto.QueryPageByTimeDTO; import com.ai.da.model.vo.AuthPrincipalVo; import com.ai.da.service.OrderInfoService; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import io.netty.util.internal.StringUtil; @@ -34,9 +38,13 @@ public class OrderInfoServiceImpl extends ServiceImpl queryWrapper = new QueryWrapper<>(); queryWrapper.eq("order_no", orderNo); OrderInfo orderInfo = baseMapper.selectOne(queryWrapper); - if(orderInfo == null){ + if (orderInfo == null) { return null; } return orderInfo.getOrderStatus(); @@ -129,6 +169,7 @@ public class OrderInfoServiceImpl extends ServiceImpl getOrderByPage(QueryPageByTimeDTO queryPageByTimeDTO){ + public PageBaseResponse getOrderByPage(QueryPageByTimeDTO queryPageByTimeDTO) { QueryWrapper qw = new QueryWrapper<>(); - qw.eq("account_id",UserContext.getUserHolder().getId()); + qw.eq("account_id", UserContext.getUserHolder().getId()); String startTime = queryPageByTimeDTO.getStartTime(); String endTime = queryPageByTimeDTO.getEndTime(); - if (StringUtil.isNullOrEmpty(startTime)){ + if (StringUtil.isNullOrEmpty(startTime)) { startTime = "2024-02-01 00:00:00"; } - if (StringUtil.isNullOrEmpty(endTime)){ + if (StringUtil.isNullOrEmpty(endTime)) { LocalDateTime now = LocalDateTime.now(); DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); endTime = now.format(dateTimeFormatter); @@ -206,11 +249,28 @@ public class OrderInfoServiceImpl extends ServiceImpl qw = new QueryWrapper<>(); + qw.eq("order_no", orderNo); + List paymentInfos = paymentInfoMapper.selectList(qw); + Float sum = paymentInfos.stream() + .map(PaymentInfo::getPayerTotal) + .reduce(0f, Float::sum); + + baseMapper.update( + new OrderInfo(), + new UpdateWrapper() + .eq("order_no", orderNo) + .set("total_fee", sum) + ); + } + } diff --git a/src/main/java/com/ai/da/service/impl/PayPalCheckoutServiceImpl.java b/src/main/java/com/ai/da/service/impl/PayPalCheckoutServiceImpl.java index 22c57621..fb5d5409 100644 --- a/src/main/java/com/ai/da/service/impl/PayPalCheckoutServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/PayPalCheckoutServiceImpl.java @@ -441,11 +441,11 @@ public class PayPalCheckoutServiceImpl implements PayPalCheckoutService { * 申请退款 */ @Transactional(rollbackFor = Exception.class) - public Boolean refundOrder(String orderId, String reason) throws IOException { + public Boolean refundOrder(String orderNo, String reason) throws IOException { - RefundInfo refundByOrderNo = refundsInfoService.createRefundByOrderNo(orderId, reason); + RefundInfo refundByOrderNo = refundsInfoService.createRefundByOrderNo(orderNo, reason); - OrdersGetRequest ordersGetRequest = new OrdersGetRequest(orderId); + OrdersGetRequest ordersGetRequest = new OrdersGetRequest(orderNo); PayPalClient payPalClient = new PayPalClient(); HttpResponse ordersGetResponse = null; ordersGetRequest.authorization("Bearer " + getOAuth()); @@ -461,7 +461,7 @@ public class PayPalCheckoutServiceImpl implements PayPalCheckoutService { request.authorization("Bearer " + getOAuth()); request.prefer("return=representation"); - OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderId); + OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo); request.requestBody(buildRefundRequestBody(String.valueOf(orderInfo.getTotalFee()), reason)); HttpResponse response = null; try { @@ -476,7 +476,7 @@ public class PayPalCheckoutServiceImpl implements PayPalCheckoutService { //进行数据库操作,修改状态为已退款(配合回调和退款查询确定退款成功) //更新订单状态 - orderInfoService.updateStatusByOrderNo(orderId, OrderStatusEnum.REFUND_SUCCESS); + orderInfoService.updateStatusByOrderNo(orderNo, OrderStatusEnum.REFUND_SUCCESS); refundsInfoService.updateRefundForPayPal( refundByOrderNo.getId(), @@ -485,14 +485,14 @@ public class PayPalCheckoutServiceImpl implements PayPalCheckoutService { AliPayTradeStateEnum.REFUND_SUCCESS.getType()); //退款成功 // 更新积分状态 - OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(orderId); + OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(orderNo); // creditsService.creditsRefund(orderByOrderNo.getAccountId(), orderByOrderNo.getTotalFee() / Integer.parseInt(CreditsEventsEnum.PRICE.getValue())); - creditsService.creditsRefund(orderByOrderNo.getAccountId(), (int)(orderByOrderNo.getTotalFee() / Float.parseFloat(CreditsEventsEnum.PRICE.getValue()))); + creditsService.creditsRefund(orderByOrderNo.getAccountId(), (int)(orderByOrderNo.getTotalFee() / Float.parseFloat(CreditsEventsEnum.PRICE.getValue())), orderNo); log.info("退款成功"); result = Boolean.TRUE; } else { //更新订单状态 - orderInfoService.updateStatusByOrderNo(orderId, OrderStatusEnum.REFUND_ABNORMAL); + orderInfoService.updateStatusByOrderNo(orderNo, OrderStatusEnum.REFUND_ABNORMAL); //更新退款单 refundsInfoService.updateRefundForPayPal( @@ -571,19 +571,19 @@ public class PayPalCheckoutServiceImpl implements PayPalCheckoutService { // 处理当前订单 @Transactional(rollbackFor = Exception.class) - public void processOrder(String orderId) { + public void processOrder(String orderNo) { // 1、确定当前订单是否已经被扣款 - OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderId); + OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo); if (orderInfo.getOrderStatus().equals(OrderStatusEnum.SUCCESS.getType())) { // 直接返回 return; } // 发起扣款请求 - Order capturedOrder = captureOrder(orderId); + Order capturedOrder = captureOrder(orderNo); // 业务处理 if (PayPalOrderStatusEnum.COMPLETED.getStatus().equals(capturedOrder.status())) { //更新订单状态 - orderInfoService.updateStatusByOrderNo(orderId, OrderStatusEnum.SUCCESS); + orderInfoService.updateStatusByOrderNo(orderNo, OrderStatusEnum.SUCCESS); //记录支付日志 paymentInfoService.createPaymentInfoForPayPal(capturedOrder); float quantity = orderInfo.getTotalFee() / Float.parseFloat(CreditsEventsEnum.PRICE.getValue()); @@ -593,7 +593,7 @@ public class PayPalCheckoutServiceImpl implements PayPalCheckoutService { creditsService.insertToCreditsDetail(orderInfo.getAccountId(), CreditsEventsEnum.BUY_CREDITS.getName() + "--PayPal", String.valueOf((Long.parseLong(CreditsEventsEnum.BUY_CREDITS.getValue()) * quantity)), - "positive"); + "positive", orderNo); } } @@ -637,7 +637,7 @@ public class PayPalCheckoutServiceImpl implements PayPalCheckoutService { creditsService.insertToCreditsDetail(orderByOrderNo.getAccountId(), CreditsEventsEnum.BUY_CREDITS.getName() + "--Paypal", String.valueOf((Long.parseLong(CreditsEventsEnum.BUY_CREDITS.getValue()) * quantity)), - "positive"); + "positive", orderNo); } } diff --git a/src/main/java/com/ai/da/service/impl/PaymentInfoServiceImpl.java b/src/main/java/com/ai/da/service/impl/PaymentInfoServiceImpl.java index 2a80a4f5..538886aa 100644 --- a/src/main/java/com/ai/da/service/impl/PaymentInfoServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/PaymentInfoServiceImpl.java @@ -1,27 +1,49 @@ package com.ai.da.service.impl; +import com.ai.da.common.context.UserContext; import com.ai.da.common.enums.PayTypeEnum; +import com.ai.da.common.response.PageBaseResponse; import com.ai.da.mapper.primary.PaymentInfoMapper; import com.ai.da.mapper.primary.entity.PaymentInfo; import com.ai.da.model.dto.AlipayHKCallbackDTO; -import com.ai.da.service.PaymentInfoService; +import com.ai.da.model.dto.QueryPageByTimeDTO; +import com.ai.da.model.vo.OrderListVO; +import com.ai.da.service.*; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.google.gson.Gson; import com.paypal.orders.Order; +import com.stripe.Stripe; +import com.stripe.exception.StripeException; +import com.stripe.model.Charge; +import com.stripe.model.Invoice; +import com.stripe.model.Subscription; import com.stripe.model.checkout.Session; +import io.netty.util.internal.StringUtil; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; +import javax.annotation.Resource; import java.math.BigDecimal; import java.math.RoundingMode; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Objects; @Service @Slf4j public class PaymentInfoServiceImpl extends ServiceImpl implements PaymentInfoService { + @Resource + private StripeService stripeService; + /** * 记录支付日志:微信支付 * @param plainText @@ -50,7 +72,6 @@ public class PaymentInfoServiceImpl extends ServiceImpl qw = new QueryWrapper<>(); - qw.eq("order_no", orderId); + @Value("${stripe.private-key}") + private String privateKey; + public PaymentInfo createOrUpdatePaymentInfoForStripe(Invoice invoice){ + Stripe.apiKey = privateKey; + // 获取transactionId,从sessionId更改为invoiceId + String invoiceId = invoice.getId(); - return baseMapper.selectOne(qw); + QueryWrapper qw = new QueryWrapper<>(); + qw.eq("transaction_id", invoiceId); + PaymentInfo paymentInfo = baseMapper.selectOne(qw); + String status = invoice.getStatus(); + // 判断当前支付是否已经被记录,确保同一个支付不会被重复记录 + if (Objects.isNull(paymentInfo)){ + String orderNo; + try { + if (invoice.getBillingReason().equals("manual")){ + // 手动创建的发票,针对one-time支付 + orderNo = invoice.getLines().getData().get(0).getPrice().getMetadata().get("orderId"); + }else { + String subscriptionId = invoice.getSubscription(); + // 从subscription中获取orderNo + orderNo = Subscription.retrieve(subscriptionId).getDescription().replace("AiDA - ", ""); + } + } catch (StripeException e) { + throw new RuntimeException(e); + } + Long amountTotal; + if (status.equals("paid")){ + amountTotal = invoice.getAmountPaid(); + }else { + amountTotal = invoice.getAmountDue(); + } + + // 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(); + + Map paymentMethod = stripeService.getPaymentMethodByInvoiceId(invoiceId); + + 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); + paymentInfo.setPaymentMethod(paymentMethod.get("paymentMethod")); + paymentInfo.setLast4(paymentMethod.get("last4")); + paymentInfo.setHostedInvoiceUrl(invoice.getHostedInvoiceUrl()); + paymentInfo.setCreateTime(LocalDateTime.now()); + + baseMapper.insert(paymentInfo); + }else { + paymentInfo.setTradeState(status); + paymentInfo.setUpdateTime(LocalDateTime.now()); + baseMapper.updateById(paymentInfo); + } + return paymentInfo; + } + + public PaymentInfo createOrUpdatePaymentInfoForStripe(Charge charge){ + Stripe.apiKey = privateKey; + QueryWrapper qw = new QueryWrapper<>(); + qw.eq("transaction_id", charge.getInvoice()); + PaymentInfo paymentInfo = baseMapper.selectOne(qw); + Charge.PaymentMethodDetails paymentMethodDetails = charge.getPaymentMethodDetails(); + String paymentMethod; + String last4 = "N/A"; + switch (paymentMethodDetails.getType()){ + case "alipay": + paymentMethod = "Alipay"; + break; + case "bancontact": + paymentMethod = "BanContact"; + break; + case "card": + Charge.PaymentMethodDetails.Card card = paymentMethodDetails.getCard(); + String brand = card.getBrand(); + brand = brand.substring(0, 1).toUpperCase() + brand.substring(1); + paymentMethod = brand + " " + card.getFunding() + "card"; + last4 = card.getLast4(); + break; + case "eps": + Charge.PaymentMethodDetails.Eps eps = paymentMethodDetails.getEps(); + paymentMethod = eps.getBank(); + break; + case "giropay": + paymentMethod = "GiroPay"; + break; + case "ideal": + Charge.PaymentMethodDetails.Ideal ideal = paymentMethodDetails.getIdeal(); + paymentMethod = ideal.getBank(); + break; + case "link": + paymentMethod = "Link"; + break; + default: + paymentMethod = "N/A"; + } + if (Objects.isNull(paymentInfo)){ + Stripe.apiKey = privateKey; + + Float divide = new BigDecimal(charge.getAmount()).divide(new BigDecimal(100), 2, RoundingMode.HALF_UP).floatValue(); + paymentInfo = new PaymentInfo(); + paymentInfo.setOrderNo(charge.getDescription().replace("AiDA - ", "")); + paymentInfo.setTransactionId(charge.getInvoice()); + paymentInfo.setPaymentType(PayTypeEnum.STRIPE.getType()); + paymentInfo.setTradeState(charge.getStatus()); + paymentInfo.setPayerTotal(divide); + paymentInfo.setNotified(0); + paymentInfo.setPaymentMethod(paymentMethod); + paymentInfo.setLast4(last4); + paymentInfo.setCreateTime(LocalDateTime.now()); + baseMapper.insert(paymentInfo); + }else { + paymentInfo.setTradeState(charge.getStatus()); + paymentInfo.setPaymentMethod(paymentMethod); + paymentInfo.setLast4(last4); + paymentInfo.setUpdateTime(LocalDateTime.now()); + baseMapper.updateById(paymentInfo); + } + + return paymentInfo; + } + + + @Override + public List getPaymentInfoByOrderNo(String orderId, String order){ + QueryWrapper qw = new QueryWrapper<>(); + qw.eq("order_no", orderId).orderByDesc(order.equals("DESC"),"id"); + + return baseMapper.selectList(qw); } @Override @@ -190,4 +339,36 @@ public class PaymentInfoServiceImpl extends ServiceImpl getPaymentInfo(QueryPageByTimeDTO queryPageByTimeDTO){ + Long accountId = UserContext.getUserHolder().getId(); + + String startTime = queryPageByTimeDTO.getStartTime(); + String endTime = queryPageByTimeDTO.getEndTime(); + if (StringUtil.isNullOrEmpty(startTime)) { + startTime = "2024-02-01 00:00:00"; + } + if (StringUtil.isNullOrEmpty(endTime)) { + LocalDateTime now = LocalDateTime.now(); + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + endTime = now.format(dateTimeFormatter); + } + + int offset = (queryPageByTimeDTO.getPage() - 1) * queryPageByTimeDTO.getSize(); + List orderListVOS = baseMapper.selectPageOrderList(accountId, startTime, endTime, offset, queryPageByTimeDTO.getSize()); + + if (CollectionUtils.isEmpty(orderListVOS)) { + return PageBaseResponse.success(new Page<>()); + }else { + int totalCount = baseMapper.queryOrderListTotalCount(accountId, startTime, endTime); + IPage orderListVOIPage = new Page<>(); + Integer size = queryPageByTimeDTO.getSize(); + orderListVOIPage.setSize(size); + orderListVOIPage.setRecords(orderListVOS); + orderListVOIPage.setCurrent(queryPageByTimeDTO.getPage()); + orderListVOIPage.setPages((long)Math.ceil((double) totalCount / size)); + orderListVOIPage.setTotal(totalCount); + return PageBaseResponse.success(orderListVOIPage); + } + } } diff --git a/src/main/java/com/ai/da/service/impl/StripeServiceImpl.java b/src/main/java/com/ai/da/service/impl/StripeServiceImpl.java index 7951adcd..f16bbb85 100644 --- a/src/main/java/com/ai/da/service/impl/StripeServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/StripeServiceImpl.java @@ -1,16 +1,23 @@ 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.DateUtil; +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 +28,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 +38,12 @@ 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.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,81 @@ 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 "CreditsPurchase": + productEnum = ProductEnum.CreditsProduct; + productPurchaseDTO.setAutoRenewal(false); + break; + case "Subscription": + switch (productPurchaseDTO.getSubscribeType()){ + case "Month": + productEnum = ProductEnum.MonthlySubscription; + break; + case "Year": + productEnum = ProductEnum.AnnualSubscription; + break; + case "Day": + productEnum = ProductEnum.DailySubscription; + break; + default: + throw new BusinessException("unknown subscription type"); + } + break; + default: + throw new BusinessException("unknown product type"); + } + log.info("生成订单"); + OrderInfo orderInfo = orderInfoService.createOrderByProductId(productPurchaseDTO.getQuantity(), PayTypeEnum.STRIPE.getType(), productEnum); + String payType; + if (productPurchaseDTO.getAutoRenewal()){ + payType = "recurring"; + }else { + payType = "one_time"; + } try { - //创建产品 - Map params = new HashMap<>(); - params.put("name", "AiDA credits purchase"); - Product product = Product.create(params); - String orderId = OrderNoUtils.getOrderNo(); - - BigDecimal actualAmount = new BigDecimal(Long.parseLong(CreditsEventsEnum.PRICE.getValue()) * 100); //stripe的默认单位是分,即传入的amount实际上小数点会被左移两位 - //给price绑定元数据并更新price用于检索 - Map metadata = new HashMap<>(); - metadata.put("orderId", orderId); - //创建价格 - Map priceParams = new HashMap<>(); - priceParams.put("metadata", metadata); //通过订单号关联用于检索price信息(可选) - priceParams.put("unit_amount", actualAmount.intValue()); - priceParams.put("currency", "HKD"); - priceParams.put("product", product.getId()); - Price price = Price.create(priceParams); + Long id = UserContext.getUserHolder().getId(); + com.ai.da.mapper.primary.entity.Account account = accountMapper.selectById(id); + // 获取或创建产品 + String productId = getProduct(productEnum.getName()); + // 获取或创建价格 + String priceId = getPrice(productEnum.getPrice(), productId, payType, productPurchaseDTO.getSubscribeType()); + // 获取或创建customer + String customerId = getCustomer(account.getUserName(), account.getUserEmail()); + // 获取自定义订单号 + String orderId = orderInfo.getOrderNo(); //创建支付信息得到url - SessionCreateParams params3 = SessionCreateParams.builder() - .setMode(SessionCreateParams.Mode.PAYMENT) - .setSuccessUrl(returnUrl)//可自定义成功页面 - .setPaymentIntentData(SessionCreateParams.PaymentIntentData.builder().setDescription("AiDA - " + orderId).build()) - .setLocale(account.getLanguage().equals("CHINESE_SIMPLIFIED") ? SessionCreateParams.Locale.ZH : SessionCreateParams.Locale.EN) - .addLineItem( - SessionCreateParams.LineItem.builder() - .setQuantity(Long.valueOf(quantity)) - .setPrice(price.getId()) - .build()) - .putMetadata("orderId", orderId) //通过订单号关联用于检索支付信息(可选) - .build(); - Session session = Session.create(params3); + // 一次性支付和周期扣款,需要区分mode: payment || subscription || setup + SessionCreateParams.Builder sessionBuilder = new SessionCreateParams.Builder(); + if (payType.equals("recurring")){ + sessionBuilder.setMode(SessionCreateParams.Mode.SUBSCRIPTION); + sessionBuilder.setSubscriptionData(SessionCreateParams.SubscriptionData.builder().setDescription("AiDA - " + orderId).build()); + }else { + sessionBuilder.setMode(SessionCreateParams.Mode.PAYMENT); + sessionBuilder.setPaymentIntentData(SessionCreateParams.PaymentIntentData.builder().setDescription("AiDA - " + orderId).build()); + // one-time 手动创建发票;订阅会自动创建invoice + sessionBuilder.setInvoiceCreation(SessionCreateParams.InvoiceCreation.builder().setEnabled(Boolean.TRUE).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 +156,69 @@ public class StripeServiceImpl implements StripeService { return ""; } + // 获取产品ID + private String getProduct(String productName) throws StripeException { + Stripe.apiKey = privateKey; + // 1、获取所有的产品 + ProductCollection productCollection = Product.list(ProductListParams.builder().setActive(Boolean.TRUE).build()); + // 2、取一个指定名称的产品 + for (Product product : productCollection.getData()) { + if (product.getName().equals(productName)) { + return product.getId(); + } + } + // 3、在现有产品中没有找到指定产品,新建产品 + Map params = new HashMap<>(); + params.put("name", productName); + Product product = Product.create(params); + return product.getId(); + } + + /** + * 获取价格 + * + * @param priceValue 价格值 + * @param payType recurring || one_time + * @param recurringType monthly || yearly + */ + private String getPrice(Long priceValue, String productId, String payType, String recurringType) throws StripeException { + Stripe.apiKey = privateKey; + PriceCollection priceCollection = Price.list(PriceListParams.builder() + .setActive(Boolean.TRUE) + .setProduct(productId).build()); + for (Price price : priceCollection.getData()) { + // stripe的金额单位为分,所以这里需要 ×100 + if (price.getUnitAmount().equals(priceValue * 100) && price.getType().equals(payType)) { + return price.getId(); + } + } + + Price price = createPrice(priceValue, productId, payType, recurringType); + return price.getId(); + } + + private Price createPrice(Long priceValue, String productId, String payType, String recurringType) throws StripeException { + BigDecimal actualAmount = new BigDecimal(priceValue * 100); //stripe的默认单位是分,即传入的amount实际上小数点会被左移两位 + + PriceCreateParams.Builder priceCreateParams = new PriceCreateParams.Builder(); + priceCreateParams.setBillingScheme(PriceCreateParams.BillingScheme.PER_UNIT); + priceCreateParams.setCurrency("HKD"); + priceCreateParams.setProduct(productId); + priceCreateParams.setUnitAmount(actualAmount.longValue()); + + if (payType.equals("recurring")) { + PriceCreateParams.Recurring.Builder recurring = new PriceCreateParams.Recurring.Builder(); + // One of day, week, month or year. + recurring.setInterval(PriceCreateParams.Recurring.Interval.valueOf(recurringType.toUpperCase())); + // The number of intervals (specified in the interval attribute) between subscription billings. + // For example, interval=month and interval_count=3 bills every 3 months. + recurring.setIntervalCount(1L); + priceCreateParams.setRecurring(recurring.build()); + } + + return Price.create(priceCreateParams.build()); + } + @Override @Transactional(rollbackFor = Exception.class) public Boolean notify(HttpServletRequest request) { @@ -127,19 +232,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 +253,119 @@ 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 (event.getType().equals("checkout.session.expired")){ + String orderNo = session.getMetadata().get("orderId"); + // 会话过期 未支付 且之后没有支付成功的订单 + processExpiredOrder(orderNo); + } + } 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")){ + // 更新订阅信息 + SubscriptionInfo subscriptionInfo = updateSubscription(subscription); + log.info("订阅更新"); + if (subscription.getStatus().equals("active")){ + response = sendEmail(subscription.getId(), null); + } + // 续订支付失败,邮件通知用户 + if (subscription.getStatus().equals("past_due")){ + // 发送续订失败邮件 + response = sendRenewalFailEmail(null, subscription.getId(), subscriptionInfo.getOrderNo()); - return Boolean.TRUE; + } + } else if (event.getType().equals("customer.subscription.deleted")){ + SubscriptionInfo subscriptionInfo = updateSubscription(subscription); + log.info("用户 {} 取消连续订阅 {}", subscriptionInfo.getAccountId(), subscription.getId()); + if (subscriptionInfo.getCancelNotified() == (byte)0){ + log.info("取消订阅 邮件通知商家"); + response = sendEmail(subscription.getId(), "cancel"); + subscriptionInfo.setCancelNotified((byte)1); + subscriptionInfoMapper.updateById(subscriptionInfo); + } + + }/* else if (event.getType().equals("customer.subscription.paused")){ + updateSubscription(subscription); + } else if (event.getType().equals("customer.subscription.resumed")){ + updateSubscription(subscription); + log.info("用户订阅恢复"); + }*/ + } else if (stripeObject instanceof Invoice) { + Invoice invoice = (Invoice) stripeObject; + if (event.getType().equals("invoice.paid")) { + // 新增支付成功的信息,返回orderNo,表示,该回调第一次被记录 + PaymentInfo paymentInfo = paymentInfoService.createOrUpdatePaymentInfoForStripe(invoice); + + // 当前支付没有被通知时才需要发送通知邮件 + if (paymentInfo.getNotified().equals(0)) { + // 更新t_order_info中的total_fee,记录该订单的累计付款金额 + orderInfoService.updateTotalFeeByOrderNo(paymentInfo.getOrderNo()); + // 邮件通知商家和用户 + if (invoice.getBillingReason().equals("subscription_create")){ + response = sendEmail(invoice.getSubscription(), "new"); + } else if (invoice.getBillingReason().equals("subscription_cycle")){ + response = sendEmail(invoice.getSubscription(), "renewal"); + } + } + } else if (event.getType().equals("invoice.payment_failed")) { + // 更新支付信息 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("transaction_id", invoice.getId()); + PaymentInfo paymentInfo = paymentInfoService.getBaseMapper().selectOne(queryWrapper); + if (!Objects.isNull(paymentInfo)){ + String type = invoice.getBillingReason().equals("subscription_create") ? "new" : + invoice.getBillingReason().equals("subscription_cycle") ? "renewal" : invoice.getBillingReason(); + Gson gson = new Gson(); + String json = gson.toJson(invoice); + paymentInfo.setContent(json); + paymentInfo.setType(type); + paymentInfo.setHostedInvoiceUrl(invoice.getHostedInvoiceUrl()); + paymentInfoService.updateById(paymentInfo); + + // 发送续订失败邮件 + response = sendRenewalFailEmail(invoice.getId(), null, paymentInfo.getOrderNo()); + } + + } + }else if (stripeObject instanceof Charge) { + Charge charge = (Charge) stripeObject; + String orderNo = charge.getDescription().replace("AiDA - ", ""); + OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo); + if (event.getType().equals("charge.failed")){ + // 添加支付信息 && 更新支付信息 + // 支付失败时,无法通过invoice_id获取支付方式,所以使用charge.failed回调添加支付信息 + paymentInfoService.createOrUpdatePaymentInfoForStripe(charge); + + orderInfo.setOrderStatus(OrderStatusEnum.FAILURE.getType()); + orderInfo.setNote(charge.getFailureMessage()); + orderInfoService.updateById(orderInfo); + }else if (event.getType().equals("charge.succeeded")){ + orderInfo.setOrderStatus(OrderStatusEnum.SUCCESS.getType()); + orderInfo.setNote(""); + orderInfoService.updateById(orderInfo); + } + } + return response; } public void processOrder(Session session) { @@ -186,37 +387,199 @@ 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", orderId); + log.info("用户:{} 积分信息更新成功", orderByOrderNo.getAccountId()); + } + } catch (Exception e) { log.info(e.getMessage()); } } + private void processExpiredOrder(String orderNo) { + // 支付失败 通知商家的条件 1、会话过期 2、支付失败 3、这个用户在这个支付失败后再无支付成功的订阅 + // 1、获取当前订单的支付状态 +// String orderNo = session.getMetadata().get("orderId"); + OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(orderNo); + // 2、确认订单状态为支付失败 + if (orderByOrderNo.getOrderStatus().equals(OrderStatusEnum.FAILURE.getType())) { + // 3、判断失败订单之后再无成功的订单 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("account_id", orderByOrderNo.getAccountId()); + queryWrapper.gt("create_time", orderByOrderNo.getCreateTime()); + queryWrapper.eq("order_status", OrderStatusEnum.SUCCESS.getType()); + queryWrapper.likeLeft("title", "Subscription"); + List orderInfos = orderInfoService.getBaseMapper().selectList(queryWrapper); + if (orderInfos.isEmpty()) { + // 4、判断当前订单有没有订阅信息 + QueryWrapper qw = new QueryWrapper<>(); + qw.eq("order_no", orderNo); + SubscriptionInfo subscriptionInfo = subscriptionInfoMapper.selectOne(qw); + // 发送邮件通知商家用户支付失败 + if (Objects.isNull(subscriptionInfo) + || subscriptionInfo.getStatus().equals("incomplete") + || subscriptionInfo.getStatus().equals("incomplete_expired")) { + sendEmail(orderNo); + }else { + // todo 续订失败 应该不会走这里 + sendEmail(subscriptionInfo.getSubscriptionId(), "fail_renewal"); + } + } + } + } - public String refund(String amount, String orderId, String reason) { - Refund refund ; - RefundInfo refundByOrderNo = refundInfoService.createRefundByOrderNo(orderId, reason); + @Transactional(rollbackFor = Exception.class) + public SubscriptionInfo createSubscription(Subscription subscription){ + // 确认当前subscription是否已经记录 + SubscriptionInfo subscriptionInfo = getSubscriptionInfoBySubId(subscription.getId()); +// SubscriptionInfo subscriptionInfo = subscriptionInfoMapper.selectOne(qw); + if (Objects.isNull(subscriptionInfo)) { + String description = subscription.getDescription(); + String orderNo = description.replace("AiDA - ", ""); + OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo); + + // 从回调信息中获取recurring type + SubscriptionItem subscriptionItem = subscription.getItems().getData().get(0); + String interval = subscriptionItem.getPrice().getRecurring().getInterval(); + + subscriptionInfo = new SubscriptionInfo(); + subscriptionInfo.setAccountId(orderInfo.getAccountId()); + subscriptionInfo.setOrderNo(orderNo); + subscriptionInfo.setSubscriptionId(subscription.getId()); + subscriptionInfo.setType(interval); + subscriptionInfo.setStatus(subscription.getStatus()); + subscriptionInfo.setNextPayDate(DateUtil.changeTimeStampFormat(subscription.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy_EEEE)); + subscriptionInfo.setCurrentPeriodStart(subscription.getCurrentPeriodStart()); + subscriptionInfo.setCurrentPeriodEnd(subscription.getCurrentPeriodEnd()); + subscriptionInfo.setCreateTime(LocalDateTime.now()); + subscriptionInfoMapper.insert(subscriptionInfo); + + // 更新账号到期时间 + updateAccountValidity(subscriptionInfo.getAccountId(), subscriptionInfo.getCurrentPeriodEnd()); + } + return subscriptionInfo; + } + + public SubscriptionInfo getSubscriptionInfoBySubId(String subId){ + QueryWrapper qw = new QueryWrapper<>(); + qw.eq("subscription_id", subId); + + List subscriptionInfos = subscriptionInfoMapper.selectList(qw); + if (subscriptionInfos.size() == 1){ + return subscriptionInfos.get(0); + }else if (subscriptionInfos.size() > 1) { + // 如果新建了多个订阅,则筛选出状态为active的订单 + Optional activeSubscriptionInfo = subscriptionInfos.stream() + .filter(sub -> sub.getStatus().equals("active")) + .findFirst(); + + return activeSubscriptionInfo.orElseGet(() -> subscriptionInfos.get(0)); + }else { + return null; + } + } + + public SubscriptionInfo getLatestSubscriptionInfoByAccountId(Long accountId){ + QueryWrapper qw = new QueryWrapper<>(); + qw.eq("account_id", accountId); + List subscriptionInfos = subscriptionInfoMapper.selectList(qw); + if (subscriptionInfos.isEmpty()){ + return null; + }else { + return subscriptionInfos.get(0); + } + } + + @Transactional(rollbackFor = Exception.class) + public SubscriptionInfo updateSubscription(Subscription subscription){ + // 获取当前是否有已经记录的subscriptionInfo + SubscriptionInfo subscriptionInfo = createSubscription(subscription); + // 用于标志数据有没有变动,避免在没有改动的情况下频繁的更新数据库 + boolean flag = false; + if (!subscriptionInfo.getStatus().equals(subscription.getStatus())){ + subscriptionInfo.setStatus(subscription.getStatus()); + flag = true; + } + if (!subscriptionInfo.getCurrentPeriodStart().equals(subscription.getCurrentPeriodStart())){ + subscriptionInfo.setCurrentPeriodStart(subscription.getCurrentPeriodStart()); + flag = true; + } + if (!subscriptionInfo.getCurrentPeriodEnd().equals(subscription.getCurrentPeriodEnd())){ + subscriptionInfo.setCurrentPeriodEnd(subscription.getCurrentPeriodEnd()); + subscriptionInfo.setNextPayDate(DateUtil.changeTimeStampFormat(subscription.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy_EEEE)); + // 更新账号到期时间 + updateAccountValidity(subscriptionInfo.getAccountId(), subscriptionInfo.getCurrentPeriodEnd()); + log.info("更新 {} 账号到期时间为:{}", subscriptionInfo.getAccountId(), DateUtil.changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy_EEEE)); + flag = true; + } + if (flag){ + subscriptionInfo.setUpdateTime(LocalDateTime.now()); + subscriptionInfoMapper.updateById(subscriptionInfo); + } + return subscriptionInfo; + } + + 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, String cancelReason) { + Stripe.apiKey = privateKey; + Long accountId = UserContext.getUserHolder().getId(); + log.info("用户 {} 申请取消连续订阅 {}", accountId, subscriptionId); + com.ai.da.mapper.primary.entity.Account account = accountMapper.selectById(accountId); + List subscriptions = getSubscription(account.getUserName(), account.getUserEmail()); + // 获取status = active的订阅 + subscriptions.forEach(subscription -> { + if (subscription.getId().equals(subscriptionId)) { + try { + Subscription cancel = subscription.cancel(); + cancel.getStatus(); + + // 更新数据库 + updateCancelReason(subscriptionId, cancelReason); + } catch (StripeException e) { + log.error("订阅 {} 取消失败, error message : {}", subscription.getId(), e.getMessage()); + } + } + }); + } + + public void cancelSubscriptionTemp(String subscriptionId) { + Stripe.apiKey = privateKey; + try { + log.info("申请取消连续订阅 {}", subscriptionId); + Subscription subscription = Subscription.retrieve(subscriptionId); + Subscription cancel = subscription.cancel(); + cancel.getStatus(); + } catch (StripeException e) { + log.error(e.getMessage()); +// throw new RuntimeException(e); + } + } + + public String refund(String amount, String orderNo, String reason) { + Refund refund; + RefundInfo refundByOrderNo = refundInfoService.createRefundByOrderNo(orderNo, reason); try { Stripe.apiKey = privateKey; + // todo transactionId不再是sessionId而是invoiceId,所以这里需要更新 // 根据orderId找到对应的sessionId - String sessionId = paymentInfoService.getPaymentInfoByOrderId(orderId).getTransactionId(); + String sessionId = paymentInfoService.getPaymentInfoByOrderNo(orderNo, "DESC").get(0).getTransactionId(); if (StringUtils.isNotEmpty(sessionId)) { //根据会话编号退款 Session session = Session.retrieve(sessionId); @@ -235,7 +598,7 @@ public class StripeServiceImpl implements StripeService { refund = Refund.create(params); log.info("根据会话编号退款成功"); - }else { + } else { log.error("当前订单不存在"); return "退款异常"; } @@ -250,7 +613,7 @@ public class StripeServiceImpl implements StripeService { if ("succeeded".equals(refund.getStatus())) { //进行数据库操作,修改状态为已退款(配合回调和退款查询确定退款成功) //更新订单状态 - orderInfoService.updateStatusByOrderNo(orderId, OrderStatusEnum.REFUND_SUCCESS); + orderInfoService.updateStatusByOrderNo(orderNo, OrderStatusEnum.REFUND_SUCCESS); refundInfoService.updateRefundForPayPal( refundByOrderNo.getId(), @@ -259,11 +622,11 @@ public class StripeServiceImpl implements StripeService { AliPayTradeStateEnum.REFUND_SUCCESS.getType()); //退款成功 // 更新积分状态 - OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(orderId); - creditsService.creditsRefund(orderByOrderNo.getAccountId(), (int)(orderByOrderNo.getTotalFee() / Float.parseFloat(CreditsEventsEnum.PRICE.getValue()))); + OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(orderNo); + creditsService.creditsRefund(orderByOrderNo.getAccountId(), (int) (orderByOrderNo.getTotalFee() / Float.parseFloat(CreditsEventsEnum.PRICE.getValue())), orderNo); } else { //更新订单状态 - orderInfoService.updateStatusByOrderNo(orderId, OrderStatusEnum.REFUND_ABNORMAL); + orderInfoService.updateStatusByOrderNo(orderNo, OrderStatusEnum.REFUND_ABNORMAL); //更新退款单 refundInfoService.updateRefundForPayPal( @@ -275,17 +638,18 @@ 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); + // todo transactionId不再是sessionId而是invoiceId,所以这里需要更新 + PaymentInfo paymentInfo = paymentInfoService.getPaymentInfoByOrderNo(orderNo, "DESC").get(0); 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 +668,480 @@ public class StripeServiceImpl implements StripeService { } - // 1、创建customer,获取customerId - // 2、创建客户支付方式 (从前端获取) - // 3、创建支付 paymentIntent - // 4、确认订单 - // 5、捕获金额(执行扣款操作) - public String createCustomer() throws StripeException { + public List getSubscription(String username, String userEmail) { + Stripe.apiKey = privateKey; + String customerId = null; + try { + customerId = getCustomer(username, userEmail); + SubscriptionCollection list = Subscription.list(SubscriptionListParams.builder() + .setCustomer(customerId).build()); + return list.getData(); + } catch (StripeException e) { + throw new RuntimeException(e); + } + } + + // 获取所有订阅 + public List getSubscriptionIds(String username, String userEmail) { + Stripe.apiKey = privateKey; + String customerId = null; + try { + customerId = getCustomer(username, userEmail); + SubscriptionCollection list = Subscription.list(SubscriptionListParams.builder() + .setCustomer(customerId).build()); + List data = list.getData(); + ArrayList subscriptionIds = new ArrayList<>(); + data.forEach(subscription -> { + subscriptionIds.add(subscription.getId()); + }); + return subscriptionIds; + } catch (StripeException e) { + throw new RuntimeException(e); + } + + } + + private String getCustomer(String username, String userEmail) throws StripeException { + CustomerCollection list = Customer.list(CustomerListParams.builder().setEmail(userEmail).build()); + List data = list.getData(); + if (!data.isEmpty()) { + return data.get(0).getId(); + } + return createCustomer(username, userEmail); + } + + private String createCustomer(String name, String userEmail) throws StripeException { Stripe.apiKey = privateKey; // Customer允许重复使用 CustomerCreateParams params = CustomerCreateParams.builder() - .setName("xp") - .setEmail("xupei3360@163.com") + .setName(name) + .setEmail(userEmail) .build(); - Customer customer = Customer.create(params); return customer.getId(); } - public String createPaymentMethod(String customerId) throws StripeException { - - Stripe.apiKey = privateKey; - - PaymentMethodCreateParams params = - PaymentMethodCreateParams.builder() - .setType(PaymentMethodCreateParams.Type.CARD) - .setCard( - // 测试中,不建议使用卡号,会不安全的异常,必须使用token(https://docs.stripe.com/testing?testing-method=tokens#visa) - PaymentMethodCreateParams.Token.builder().setToken("tok_visa").build() -// PaymentMethodCreateParams.CardDetails.builder() -// .setNumber("4242424242424242") -// .setExpMonth(8L) -// .setExpYear(2026L) -// .setCvc("314") -// .build() - ) - .build(); - - PaymentMethod paymentMethod = PaymentMethod.create(params); - return paymentMethod.getId(); + /** + * 使用连续订阅的订单,回调中没有paymentIntentId,所以通过invoiceId间接获取 + * @param invoiceId 发票Id + */ + public Map getPaymentMethodByInvoiceId(String invoiceId) { + try { + Stripe.apiKey = privateKey; + Invoice invoice = Invoice.retrieve(invoiceId); + if (!StringUtil.isNullOrEmpty(invoice.getPaymentIntent())){ + PaymentIntent paymentIntent = PaymentIntent.retrieve(invoice.getPaymentIntent()); + if (!StringUtil.isNullOrEmpty(paymentIntent.getPaymentMethod())){ + PaymentMethod paymentMethod = PaymentMethod.retrieve(paymentIntent.getPaymentMethod()); + return getPaymentMethod(paymentMethod.getId()); + } + } + HashMap resp = new HashMap<>(); + resp.put("paymentMethod", "N/A"); + resp.put("last4", "N/A"); + return resp; + } catch (StripeException e) { + throw new RuntimeException(e); + } } - public String createPaymentIntent(String paymentMethodId, String customerId) throws StripeException { + public Map getPaymentMethod(String paymentMethodId){ Stripe.apiKey = privateKey; + String paymentMethod = null; + String last4 = null; - Long amount = 600L; - PaymentIntentCreateParams params = - PaymentIntentCreateParams.builder() - .setAmount(amount) -// .setPaymentMethod(paymentMethodId) - .setCustomer(customerId) - .setCurrency("hkd") - .setAutomaticPaymentMethods( - PaymentIntentCreateParams.AutomaticPaymentMethods.builder() - .setEnabled(true) - .build() - ) - .build(); - - PaymentIntent paymentIntent = PaymentIntent.create(params); - return paymentIntent.getId(); + try { + PaymentMethod retrieve = PaymentMethod.retrieve(paymentMethodId); + switch (retrieve.getType()){ + case "alipay": + paymentMethod = "Alipay"; + last4 = "N/A"; + 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(); + last4 = "N/A"; + break; + case "giropay": + paymentMethod = "GiroPay"; + last4 = "N/A"; + break; + case "ideal": + PaymentMethod.Ideal ideal = retrieve.getIdeal(); + paymentMethod = ideal.getBank(); + last4 = "N/A"; + break; + case "link": + paymentMethod = "Link"; + last4 = "N/A"; + break; + default: + paymentMethod = "N/A"; + last4 = "N/A"; + } + HashMap resp = new HashMap<>(); + resp.put("paymentMethod", paymentMethod); + 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) { + SubscriptionEmailParamsDTO emailParamsDTO = new SubscriptionEmailParamsDTO(); + QueryWrapper qwSI = new QueryWrapper<>(); + qwSI.eq("subscription_id", subscriptionId); + SubscriptionInfo subscriptionInfo = subscriptionInfoMapper.selectOne(qwSI); + if (Objects.isNull(subscriptionInfo)) { + return false; + } + QueryWrapper qwPI = new QueryWrapper<>(); + qwPI.eq("order_no", subscriptionInfo.getOrderNo()).orderByDesc("id"); + List paymentInfos = paymentInfoMapper.selectList(qwPI); + if (paymentInfos.isEmpty()) { + return false; + } + PaymentInfo paymentInfo = paymentInfos.get(0); + if (StringUtil.isNullOrEmpty(type)){ + // 如果没有传入type,则使用paymentInfo中记录的类型 + // (其实这里也可以通过invoiceId查询stripe,但是记录在自己的db中可以不用每次都查,且方便查看) + type = StringUtil.isNullOrEmpty(paymentInfo.getType()) ? "new" : paymentInfo.getType(); + } + if (!type.equals("reminder") && !type.equals("cancel") && paymentInfo.getNotified() == 1){ + // 已经邮件通知过,直接返回 + return true; + } - PaymentIntent resource = PaymentIntent.retrieve(clientSecret); + com.ai.da.mapper.primary.entity.Account account = accountMapper.selectById(subscriptionInfo.getAccountId()); + String userName = account.getUserName(); + String language = account.getLanguage(); + OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(subscriptionInfo.getOrderNo()); - PaymentIntentConfirmParams params = - PaymentIntentConfirmParams.builder() - .setPaymentMethod("pm_card_visa") - .setReturnUrl("https://www.example.com") - .build(); + 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(DateUtil.changeTimeStampFormat(subscriptionInfo.getCurrentPeriodStart(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy)); + emailParamsDTO.setEndOfPrepaidTerm(DateUtil.changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy)); + setSubscriptionParams(paymentInfo, subscriptionInfo, orderByOrderNo, emailParamsDTO); - PaymentIntent paymentIntent = resource.confirm(params); - return paymentIntent.getId(); + SendEmailUtil.subscriptionEmailReminder(type, emailParamsDTO, language, account.getUserEmail()); + + // 邮件通知成功后,更新标志 + if (!type.equals("reminder") && !type.equals("cancel")){ + 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 boolean sendEmail(String orderNo){ + SubscriptionEmailParamsDTO emailParamsDTO = new SubscriptionEmailParamsDTO(); + OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo); + com.ai.da.mapper.primary.entity.Account account = accountMapper.selectById(orderInfo.getAccountId()); + String userName = account.getUserName(); + String language = account.getLanguage(); + QueryWrapper qwPI = new QueryWrapper<>(); + qwPI.eq("order_no", orderNo); + List paymentInfos = paymentInfoMapper.selectList(qwPI); + if (paymentInfos.isEmpty()) { + return false; + } + PaymentInfo paymentInfo = paymentInfos.get(0); + emailParamsDTO.setUsername(userName); + emailParamsDTO.setOrderId(paymentInfo.getId().toString()); + emailParamsDTO.setCreateDate(String.valueOf(paymentInfo.getCreateTime()).replace("T", " ")); + emailParamsDTO.setQuantity(String.valueOf(1)); + emailParamsDTO.setTotalFee(paymentInfo.getPayerTotal().toString()); + emailParamsDTO.setFailMessage(orderInfo.getNote()); + emailParamsDTO.setPaymentMethod(paymentInfo.getPaymentMethod()); + emailParamsDTO.setLast4(paymentInfo.getLast4()); + + SendEmailUtil.subscriptionEmailReminder("fail_new", emailParamsDTO, language, account.getUserEmail()); + + // 邮件通知成功后,更新标志 + PaymentInfo payment = new PaymentInfo(); + payment.setId(paymentInfo.getId()); + payment.setNotified(1); + payment.setUpdateTime(LocalDateTime.now()); + paymentInfoMapper.updateById(payment); + return true; + } + + public boolean sendRenewalFailEmail(String invoiceId, String subscriptionId, String orderNo){ + // 1、确认当前订单最后一笔支付为fail + // 更新支付信息 + PaymentInfo paymentInfo; + QueryWrapper queryWrapper = new QueryWrapper<>(); + if (StringUtil.isNullOrEmpty(invoiceId)){ + queryWrapper.eq("order_no", orderNo).orderByDesc("id"); + List paymentInfos = paymentInfoService.getBaseMapper().selectList(queryWrapper); + if (paymentInfos.isEmpty() || !paymentInfos.get(0).getTradeState().equals("failed")){ + return false; + }else { + paymentInfo = paymentInfos.get(0); + } + }else { + queryWrapper.eq("transaction_id", invoiceId); + paymentInfo = paymentInfoService.getBaseMapper().selectOne(queryWrapper); + if (Objects.isNull(paymentInfo) + || !paymentInfo.getTradeState().equals("failed") + || paymentInfo.getNotified().equals(1)){ + return false; + } + } + + // 2、确认当前订阅的状态为past_due + SubscriptionInfo subscriptionInfo; + QueryWrapper qwSI = new QueryWrapper<>(); + if (StringUtil.isNullOrEmpty(subscriptionId)){ + qwSI.eq("order_no", orderNo); + }else { + qwSI.eq("subscription_id", subscriptionId); + } + subscriptionInfo = subscriptionInfoMapper.selectOne(qwSI); + if (Objects.isNull(subscriptionInfo) || !subscriptionInfo.getStatus().equals("past_due")){ + return false; + } + + // 3、组参数 + com.ai.da.mapper.primary.entity.Account account = accountMapper.selectById(subscriptionInfo.getAccountId()); + String userName = account.getUserName(); + String language = account.getLanguage(); + OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(subscriptionInfo.getOrderNo()); + SubscriptionEmailParamsDTO emailParamsDTO = new SubscriptionEmailParamsDTO(); + emailParamsDTO.setUsername(userName); + emailParamsDTO.setOrderId(paymentInfo.getId().toString()); + emailParamsDTO.setCreateDate(String.valueOf(paymentInfo.getCreateTime()).replace("T", " ")); + emailParamsDTO.setQuantity(String.valueOf(1)); + emailParamsDTO.setTotalFee(paymentInfo.getPayerTotal().toString()); + setSubscriptionParams(paymentInfo, subscriptionInfo, orderByOrderNo, emailParamsDTO); + // todo + emailParamsDTO.setAccountPageRef("\"https://www.aida.com.hk/home/homePage\""); + + // 4、发邮件 + SendEmailUtil.subscriptionEmailReminder("fail_renewal", emailParamsDTO, language, account.getUserEmail()); + + PaymentInfo payment = new PaymentInfo(); + payment.setId(paymentInfo.getId()); + payment.setNotified(1); + payment.setUpdateTime(LocalDateTime.now()); + paymentInfoMapper.updateById(payment); + return true; + } + + private void setSubscriptionParams(PaymentInfo paymentInfo, SubscriptionInfo subscriptionInfo, OrderInfo orderByOrderNo, SubscriptionEmailParamsDTO emailParamsDTO) { + emailParamsDTO.setPaymentMethod(paymentInfo.getPaymentMethod()); + emailParamsDTO.setLast4(paymentInfo.getLast4()); + emailParamsDTO.setSubscriptionId(subscriptionInfo.getId().toString()); + emailParamsDTO.setFailMessage(orderByOrderNo.getNote()); + emailParamsDTO.setSubscriptionType(subscriptionInfo.getType()); + emailParamsDTO.setStartDate(DateUtil.changeTimeStampFormat(orderByOrderNo.getCreateTime())); + emailParamsDTO.setNextPayDate(DateUtil.changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy)); + emailParamsDTO.setRenewalTime(DateUtil.changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy)); + } + + public void subscriptionReminder(){ + // 提前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); + + // 转为时间戳 + long startTimestamp = startOfDay.toEpochSecond(ZoneOffset.UTC); + long endTimestamp = endOfDay.toEpochSecond(ZoneOffset.UTC); + + QueryWrapper qw = new QueryWrapper<>(); + qw.ge("current_period_end", startTimestamp); + qw.lt("current_period_end", endTimestamp); + qw.eq("status", "active"); + + List subscriptionInfos = subscriptionInfoMapper.selectList(qw); + for (SubscriptionInfo subscriptionInfo : subscriptionInfos) { + boolean b = sendEmail(subscriptionInfo.getSubscriptionId(), "reminder"); + if (b) log.info("提前7天向用户 {} 发送续订通知邮件", subscriptionInfo.getAccountId()); + } + } + + public void checkSubscriptionExpiration(){ + long epochSecond = Instant.now().getEpochSecond(); + QueryWrapper qw = new QueryWrapper<>(); + qw.lt("current_period_end", epochSecond); + qw.eq("status", "active"); + List subscriptionInfos = subscriptionInfoMapper.selectList(qw); + + for (SubscriptionInfo subscriptionInfo : subscriptionInfos) { + subscriptionInfo.setStatus("expired"); + subscriptionInfo.setUpdateTime(LocalDateTime.now()); + subscriptionInfoMapper.updateById(subscriptionInfo); + log.info("用户 {} 的订阅 {} 已过期", subscriptionInfo.getAccountId(), subscriptionInfo.getOrderNo()); + } + } + + // todo 新建一个订阅 使用不会成功的付款方式 + + public String createSubscriptionTemp(String name, String email){ Stripe.apiKey = privateKey; + try { + OrderInfo orderInfo = orderInfoService.createOrderByProductId(1, PayTypeEnum.STRIPE.getType(), ProductEnum.DailySubscription); - PaymentIntent resource = PaymentIntent.retrieve(clientSecret); +// String customerId = getCustomer(name, email); + String paymentMethodCode = "pm_card_mastercard"; + PaymentMethod paymentMethod = PaymentMethod.retrieve(paymentMethodCode); - PaymentIntentCaptureParams params = PaymentIntentCaptureParams.builder().build(); + String customerId = getCustomer(name, email); + log.info("customerId: {}", customerId); - PaymentIntent paymentIntent = resource.capture(params); + PaymentMethodAttachParams attachParams = PaymentMethodAttachParams.builder() + .setCustomer(customerId) + .build(); + paymentMethod.attach(attachParams); + + // 设置默认付款方式 + Customer updatedCustomer = Customer.retrieve(customerId); + CustomerUpdateParams params = CustomerUpdateParams.builder() + .setInvoiceSettings( + CustomerUpdateParams.InvoiceSettings.builder() + .setDefaultPaymentMethod(paymentMethod.getId()) + .build() + ) + .build(); + updatedCustomer.update(params); + + // 3. 创建订阅 + SubscriptionCreateParams subscriptionParams = SubscriptionCreateParams.builder() + .setCustomer(customerId) + .addItem( + SubscriptionCreateParams.Item.builder() + .setPrice("price_1QFXkf02n1TEydyNtA4TQ3Yz") // 替换为实际的价格 ID + .build() + ) + .setDescription("AiDA - " + orderInfo.getOrderNo()) + .build(); + Subscription subscription = Subscription.create(subscriptionParams); + + // 打印订阅 ID + System.out.println("Subscription created: " + subscription.getId()); + return subscription.getId(); + } catch (StripeException e) { + throw new RuntimeException(e); + } + } + + public String changeCustomerPayment(String name, String email){ + Stripe.apiKey = privateKey; + String paymentMethodCode = "pm_card_chargeCustomerFail"; + try { + PaymentMethod paymentMethod = PaymentMethod.retrieve(paymentMethodCode); + + String customerId = getCustomer(name, email); + log.info("customerId: {}", customerId); + + // 附加支付方式到客户 + PaymentMethodAttachParams attachParams = PaymentMethodAttachParams.builder() + .setCustomer(customerId) + .build(); + paymentMethod.attach(attachParams); + // 更新客户的默认支付方式 + CustomerUpdateParams params = CustomerUpdateParams.builder() + .setInvoiceSettings( + CustomerUpdateParams.InvoiceSettings.builder() + .setDefaultPaymentMethod(paymentMethod.getId()) + .build() + ) + .build(); + + // 更新客户信息 + Customer customer = Customer.retrieve(customerId); + customer.update(params); + + return paymentMethod.getId(); + + } catch (StripeException e) { + throw new RuntimeException(e); + } + } + + public List> getCustomerPaymentMethod(String name, String email){ + Stripe.apiKey = privateKey; + try { + String customerId = getCustomer(name, email); + Customer customer = Customer.retrieve(customerId); + PaymentMethodCollection paymentMethodCollection = customer.listPaymentMethods(); + List data = paymentMethodCollection.getData(); + ArrayList> resp = new ArrayList<>(); + data.forEach(paymentMethod -> { + Map map = new HashMap<>(); + if (paymentMethod.getType().equals("card")){ + map.put(paymentMethod.getId(),paymentMethod.getCard().getLast4()); + }else { + map.put(paymentMethod.getId(),null); + } + resp.add(map); + }); + + return resp; + // 方向: 向用户添加了多种付款方式,更改默认的付款方式后,默认付款方式付款失败后是否自动使用其他付款方式付款? + // 如果是的,则需要删除能成功的付款方式,保留唯一失败的付款方式进行续订付款失败测试 + } catch (StripeException e) { + throw new RuntimeException(e); + } + + } + + public String detachCustomerAllPaymentMethod(String name, String email){ + Stripe.apiKey = privateKey; + // 方向: 向用户添加了多种付款方式,更改默认的付款方式后,默认付款方式付款失败后是否自动使用其他付款方式付款? + // 如果是的,则需要删除能成功的付款方式,保留唯一失败的付款方式进行续订付款失败测试 + try { + String customerId = getCustomer(name, email); + Customer customer = Customer.retrieve(customerId); + PaymentMethodCollection paymentMethodCollection = customer.listPaymentMethods(); + List data = paymentMethodCollection.getData(); + data.forEach(paymentMethod -> { + try { + PaymentMethod retrieve = PaymentMethod.retrieve(paymentMethod.getId()); + PaymentMethodDetachParams params = PaymentMethodDetachParams.builder().build(); + retrieve.detach(params); + } catch (StripeException e) { + throw new RuntimeException(e); + } + }); + } catch (StripeException e) { + throw new RuntimeException(e); + } return null; } + public void updateCancelReason(String subscriptionId, String reason){ + QueryWrapper qw = new QueryWrapper<>(); + qw.eq("subscription_id", subscriptionId); + SubscriptionInfo subscriptionInfo = subscriptionInfoMapper.selectOne(qw); + + if (!Objects.isNull(subscriptionInfo)) { + subscriptionInfo.setCancelReason(reason); + subscriptionInfoMapper.updateById(subscriptionInfo); + } + } + } diff --git a/src/main/java/com/ai/da/service/impl/SuperResolutionServiceImpl.java b/src/main/java/com/ai/da/service/impl/SuperResolutionServiceImpl.java index 03ed3d69..eda65b46 100644 --- a/src/main/java/com/ai/da/service/impl/SuperResolutionServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/SuperResolutionServiceImpl.java @@ -149,7 +149,7 @@ public class SuperResolutionServiceImpl extends ServiceImpl + + + + + + + + diff --git a/src/main/resources/mapper/primary/AffiliateMapper.xml b/src/main/resources/mapper/primary/AffiliateMapper.xml new file mode 100644 index 00000000..8dbe92da --- /dev/null +++ b/src/main/resources/mapper/primary/AffiliateMapper.xml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/src/main/resources/mapper/primary/PaymentInfoMapper.xml b/src/main/resources/mapper/primary/PaymentInfoMapper.xml new file mode 100644 index 00000000..8bab6ca5 --- /dev/null +++ b/src/main/resources/mapper/primary/PaymentInfoMapper.xml @@ -0,0 +1,42 @@ + + + + + + + + + diff --git a/src/main/resources/messages_en.properties b/src/main/resources/messages_en.properties index 529b7bcf..04a4260a 100644 --- a/src/main/resources/messages_en.properties +++ b/src/main/resources/messages_en.properties @@ -148,6 +148,7 @@ remaining.modifications=Remaining modifications are 0 you.have.participated.in.the.event=You have participated in the event. only.original.works.can.participate.in.the.event=Sorry, only original works can participate in the event. remaining.credits.insufficient=Your remaining credits are insufficient for this generation. Please recharge. +you.haven't.subscribed.to.any.products.yet=You haven't subscribed to any products yet # 可能会报异常 # Informative: diff --git a/src/main/resources/messages_zh.properties b/src/main/resources/messages_zh.properties index c9ed6c67..d7c144ab 100644 --- a/src/main/resources/messages_zh.properties +++ b/src/main/resources/messages_zh.properties @@ -143,6 +143,7 @@ remaining.modifications=剩余修改次数为0 you.have.participated.in.the.event=您已经参与活动。 only.original.works.can.participate.in.the.event=抱歉,只有原创作品能参与活动。 remaining.credits.insufficient=您的剩余积分不够本次生成消耗,请充值 +you.haven't.subscribed.to.any.products.yet=您还未订阅任何产品 # 可能会报异常 # Informative: diff --git a/src/main/resources/payment.properties b/src/main/resources/payment.properties index 56e5a315..43c4e4b7 100644 --- a/src/main/resources/payment.properties +++ b/src/main/resources/payment.properties @@ -27,13 +27,13 @@ paypal.webhook_id=1D107312EX592781K ##### Stripe # developer -#stripe.private-key=sk_test_51P4ZZL02n1TEydyN8qQHjOA9imsFU7Oxs2HMHGy2urHnnQgSHnZuu5vVP6pKhEACwUpsKNyrbZpdcg5TJWJLRHcY008dEO1fn2 -#stripe.webhook-sign-secret=whsec_e0dBiJngx6qqgJj6yPyJ2A9ouh1Cjv5w +stripe.private-key=sk_test_51P4ZZL02n1TEydyN8qQHjOA9imsFU7Oxs2HMHGy2urHnnQgSHnZuu5vVP6pKhEACwUpsKNyrbZpdcg5TJWJLRHcY008dEO1fn2 +stripe.webhook-sign-secret=whsec_e0dBiJngx6qqgJj6yPyJ2A9ouh1Cjv5w # kim - test #stripe.private-key=sk_test_51LwPrxH7nPZ8bkrNj67TFD7sxucaTANs1lf0KGSu1QSJfxYXcnigq2wTaZyZzST7y0fMbhhvaJZ4LjjFhr95M83a00eXrmOTL0 #stripe.webhook-sign-secret=whsec_GoyVEAaBtuGD5Rt55z83JnPnLDAZTN3u # kim - live -stripe.private-key=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 -stripe.webhook-sign-secret=whsec_hhGDgdelQRHSg4LmChtQe41crj41eb11 \ No newline at end of file +#stripe.private-key=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 +#stripe.webhook-sign-secret=whsec_hhGDgdelQRHSg4LmChtQe41crj41eb11 \ No newline at end of file