diff --git a/src/main/java/com/ai/da/common/config/MyTaskScheduler.java b/src/main/java/com/ai/da/common/config/MyTaskScheduler.java index 695472f6..21dd2066 100644 --- a/src/main/java/com/ai/da/common/config/MyTaskScheduler.java +++ b/src/main/java/com/ai/da/common/config/MyTaskScheduler.java @@ -130,7 +130,7 @@ public class MyTaskScheduler { private CollectionSortMapper collectionSortMapper; @Resource private DesignService designService; - + @Resource private ToProductElementMapper toProductELementMapper; @@ -245,30 +245,26 @@ public class MyTaskScheduler { try (FileOutputStream fileOut = new FileOutputStream(fileName)) { workbook.write(fileOut); // SendEmailUtil.sendExcelEmail("1023316923@qq.com", null, Files.readAllBytes(Paths.get(fileName)), fileName); -// SendEmailUtil.sendExcelEmail("calvinwong@aidlab.hk", null, Files.readAllBytes(Paths.get(fileName)), fileName); -// SendEmailUtil.sendExcelEmail("kaicpang.pang@connect.polyu.hk", null, Files.readAllBytes(Paths.get(fileName)), fileName); -// SendEmailUtil.sendExcelEmail("kimwong@code-create.com.hk", null, Files.readAllBytes(Paths.get(fileName)), fileName); - SendEmailUtil.sendExcelEmail("ningning@code-create.com.hk", null, Files.readAllBytes(Paths.get(fileName)), fileName); - SendEmailUtil.sendExcelEmail("johnnyho@code-create.com.hk", null, Files.readAllBytes(Paths.get(fileName)), fileName); - SendEmailUtil.sendExcelEmail("ringolau@code-create.com.hk", null, Files.readAllBytes(Paths.get(fileName)), fileName); - SendEmailUtil.sendExcelEmail("ningning@code-create.com.hk", null, Files.readAllBytes(Paths.get(fileName)), fileName); + SendEmailUtil.sendExcelEmail("calvinwong@aidlab.hk", null, Files.readAllBytes(Paths.get(fileName)), fileName); + SendEmailUtil.sendExcelEmail("kaicpang.pang@connect.polyu.hk", null, Files.readAllBytes(Paths.get(fileName)), fileName); + SendEmailUtil.sendExcelEmail("kimwong@code-create.com.hk", null, Files.readAllBytes(Paths.get(fileName)), fileName); +// SendEmailUtil.sendExcelEmail("ningning@code-create.com.hk", null, Files.readAllBytes(Paths.get(fileName)), fileName); SendEmailUtil.sendExcelEmail("johnnyho@code-create.com.hk", null, Files.readAllBytes(Paths.get(fileName)), fileName); SendEmailUtil.sendExcelEmail("ringolau@code-create.com.hk", null, Files.readAllBytes(Paths.get(fileName)), fileName); + SendEmailUtil.sendExcelEmail("chelseayu@code-create.com.hk", null, Files.readAllBytes(Paths.get(fileName)), fileName); } } catch (IOException e) { e.printStackTrace(); } }else { // SendEmailUtil.sendNoExcelEmail("1023316923@qq.com", null); -// SendEmailUtil.sendNoExcelEmail("calvinwong@aidlab.hk", null); -// SendEmailUtil.sendNoExcelEmail("kaicpang.pang@connect.polyu.hk", null); -// SendEmailUtil.sendNoExcelEmail("kimwong@code-create.com.hk", null); - SendEmailUtil.sendNoExcelEmail("ningning@code-create.com.hk", null); - SendEmailUtil.sendNoExcelEmail("johnnyho@code-create.com.hk", null); - SendEmailUtil.sendNoExcelEmail("ringolau@code-create.com.hk", null); - SendEmailUtil.sendNoExcelEmail("ningning@code-create.com.hk", null); + SendEmailUtil.sendNoExcelEmail("calvinwong@aidlab.hk", null); + SendEmailUtil.sendNoExcelEmail("kaicpang.pang@connect.polyu.hk", null); + SendEmailUtil.sendNoExcelEmail("kimwong@code-create.com.hk", null); +// SendEmailUtil.sendNoExcelEmail("ningning@code-create.com.hk", null); SendEmailUtil.sendNoExcelEmail("johnnyho@code-create.com.hk", null); SendEmailUtil.sendNoExcelEmail("ringolau@code-create.com.hk", null); + SendEmailUtil.sendNoExcelEmail("chelseayu@code-create.com.hk", null); } } @@ -290,7 +286,7 @@ public class MyTaskScheduler { @Resource private GenerateDetailMapper generateDetailMapper; - + @Resource private ToProductImageResultMapper toProductImageResultMapper; 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 8b790c52..643d526a 100644 --- a/src/main/java/com/ai/da/common/constant/CommonConstant.java +++ b/src/main/java/com/ai/da/common/constant/CommonConstant.java @@ -13,6 +13,8 @@ public class CommonConstant { public static final Integer MINIO_IMAGE_EXPIRE_TIME = 24 * 60; // 单位 秒 一天过期 in redis public static final Long GENERATE_RESULT_EXPIRE_TIME = 24 * 60 * 60L; + // 单位 秒 7天过期 + public static final Long REDIS_SET_EXPIRE_TIME = 24 * 60 * 60 * 7L; public static class Numbers{ public static final Integer NUMBER_10 = 10; @@ -81,6 +83,8 @@ public class CommonConstant { public static final String TIME_FORMAT_MMM_dd_yyyy = "MMM. dd, yyyy"; + public static final String TIME_FORMAT_yyyy_MM_dd_HH_mm_ss = "yyyy-MM-dd HH:mm:ss"; + public static final String AFFILIATE_LINK = "https://www.aida.com.hk?ref="; public static final String PARTIAL_DESIGN_FILENAME = "PartialDesign"; diff --git a/src/main/java/com/ai/da/common/enums/AuthenticationOperationTypeEnum.java b/src/main/java/com/ai/da/common/enums/AuthenticationOperationTypeEnum.java index 784edb63..32623e55 100644 --- a/src/main/java/com/ai/da/common/enums/AuthenticationOperationTypeEnum.java +++ b/src/main/java/com/ai/da/common/enums/AuthenticationOperationTypeEnum.java @@ -31,7 +31,9 @@ public enum AuthenticationOperationTypeEnum { /** * 填写用户国家和职业 */ - UPDATE_USERINFO; + UPDATE_USERINFO, + + REGISTER; public static AuthenticationOperationTypeEnum of(String name) { return Stream.of(AuthenticationOperationTypeEnum.values()).filter(v -> v.name().equals(name)).findFirst().orElse(null); diff --git a/src/main/java/com/ai/da/common/enums/ProductEnum.java b/src/main/java/com/ai/da/common/enums/ProductEnum.java index 6ecf43df..c68a7385 100644 --- a/src/main/java/com/ai/da/common/enums/ProductEnum.java +++ b/src/main/java/com/ai/da/common/enums/ProductEnum.java @@ -7,7 +7,7 @@ import lombok.Getter; @AllArgsConstructor public enum ProductEnum { // 积分购买 - CreditsProduct("AiDA credits purchase", 10L, 60L), + CreditsProduct("AiDA credits purchase", 10L, 50L), // 年度订阅 AnnualSubscription("AiDA Annual Subscription", 5000L, 50000L), // 月度订阅(订阅费500,每月3500 积分) diff --git a/src/main/java/com/ai/da/common/response/TransactionPageResponse.java b/src/main/java/com/ai/da/common/response/TransactionPageResponse.java new file mode 100644 index 00000000..25200d49 --- /dev/null +++ b/src/main/java/com/ai/da/common/response/TransactionPageResponse.java @@ -0,0 +1,15 @@ +package com.ai.da.common.response; + +import io.swagger.annotations.ApiModel; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Data +@NoArgsConstructor +@ApiModel("交易记录分页响应结果") +public class TransactionPageResponse extends PageBaseResponse { + + private BigDecimal totalAmount; +} diff --git a/src/main/java/com/ai/da/common/task/PaymentTask.java b/src/main/java/com/ai/da/common/task/PaymentTask.java index 1b77e452..ba92300b 100644 --- a/src/main/java/com/ai/da/common/task/PaymentTask.java +++ b/src/main/java/com/ai/da/common/task/PaymentTask.java @@ -84,6 +84,7 @@ public class PaymentTask { }*/ } + // !!关闭此定时器,改为提前三天站内信提醒!! // 提前7天向用户发送提醒邮件,每天早上8点执行 // @Scheduled(cron = "0 0 8 * * ?") public void subscriptionReminder(){ diff --git a/src/main/java/com/ai/da/common/utils/ComprehensivePunctuationConverter.java b/src/main/java/com/ai/da/common/utils/ComprehensivePunctuationConverter.java new file mode 100644 index 00000000..c4c4ce63 --- /dev/null +++ b/src/main/java/com/ai/da/common/utils/ComprehensivePunctuationConverter.java @@ -0,0 +1,88 @@ +package com.ai.da.common.utils; + +import java.util.HashMap; +import java.util.Map; + +public class ComprehensivePunctuationConverter { + private static final Map FULL_TO_HALF_MAP = new HashMap<>(); + + static { + // 中文标点到英文标点的映射(扩展版) + FULL_TO_HALF_MAP.put(',', ','); + FULL_TO_HALF_MAP.put('。', '.'); + FULL_TO_HALF_MAP.put(';', ';'); + FULL_TO_HALF_MAP.put(':', ':'); + FULL_TO_HALF_MAP.put('?', '?'); + FULL_TO_HALF_MAP.put('!', '!'); + FULL_TO_HALF_MAP.put('(', '('); + FULL_TO_HALF_MAP.put(')', ')'); + FULL_TO_HALF_MAP.put('【', '['); + FULL_TO_HALF_MAP.put('】', ']'); + FULL_TO_HALF_MAP.put('「', '\''); + FULL_TO_HALF_MAP.put('」', '\''); + FULL_TO_HALF_MAP.put('『', '"'); + FULL_TO_HALF_MAP.put('』', '"'); + FULL_TO_HALF_MAP.put('、', '\\'); + FULL_TO_HALF_MAP.put('~', '~'); + FULL_TO_HALF_MAP.put('—', '-'); + FULL_TO_HALF_MAP.put('.', '.'); + FULL_TO_HALF_MAP.put('〈', '<'); + FULL_TO_HALF_MAP.put('〉', '>'); + FULL_TO_HALF_MAP.put('《', '«'); + FULL_TO_HALF_MAP.put('》', '»'); + FULL_TO_HALF_MAP.put('〝', '"'); + FULL_TO_HALF_MAP.put('〞', '"'); + FULL_TO_HALF_MAP.put('﹁', '"'); + FULL_TO_HALF_MAP.put('﹂', '"'); + FULL_TO_HALF_MAP.put('…', '.'); + FULL_TO_HALF_MAP.put('﹏', '_'); + + // 全角字母和数字 + for (char c = 'A'; c <= 'Z'; c++) { + FULL_TO_HALF_MAP.put(c, (char)(c - 'A' + 'A')); + } + for (char c = 'a'; c <= 'z'; c++) { + FULL_TO_HALF_MAP.put(c, (char)(c - 'a' + 'a')); + } + for (char c = '0'; c <= '9'; c++) { + FULL_TO_HALF_MAP.put(c, (char)(c - '0' + '0')); + } + } + + /** + * 将字符串中的全角字符(包括标点、字母、数字)转换为半角字符 + */ + public static String convertToHalfWidth(String input) { + if (input == null || input.isEmpty()) { + return input; + } + + StringBuilder result = new StringBuilder(); + for (int i = 0; i < input.length(); i++) { + char c = input.charAt(i); + + // 检查映射表 + if (FULL_TO_HALF_MAP.containsKey(c)) { + result.append(FULL_TO_HALF_MAP.get(c)); + } + // 处理全角空格(Unicode 12288) + else if (c == ' ') { + result.append(' '); + } + // 其他字符保持不变 + else { + result.append(c); + } + } + + return result.toString(); + } + + public static void main(String[] args) { +// String text = "这是一个全角示例,包含:中文标点、全角字母(ABC)、全角数字(123) 还有全角空格!"; + String text = "birds,yellow"; + String converted = convertToHalfWidth(text); + System.out.println("原始文本: " + text); + System.out.println("转换后: " + converted); + } +} 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 6d2a597a..82a7a3ef 100644 --- a/src/main/java/com/ai/da/common/utils/RedisUtil.java +++ b/src/main/java/com/ai/da/common/utils/RedisUtil.java @@ -100,8 +100,10 @@ public class RedisUtil { /** * 将数据放入set缓存 */ - public void addToSet(String key, String value) { + public void addToSet(String key, String value, Long expiresIn) { redisTemplate.opsForSet().add(key, value); + // 设置过期时间 + redisTemplate.expire(key, expiresIn, TimeUnit.SECONDS); } /** @@ -503,6 +505,7 @@ public class RedisUtil { public final static String IMAGE_SEGMENTATION = "ImageSegmentation:"; public final static String STRIPE_EXCEPTION_LOG = "StripeException:"; + public final static String SUBSCRIPTION_SENT_EMAIL_TYPE = "SubscriptionEmailSentType:"; public void batchDeleteKeysWithSamePrefix(String prefix){ Set keys = redisTemplate.keys(prefix + "*"); 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 ac23b9d7..018e5bc4 100644 --- a/src/main/java/com/ai/da/common/utils/SendEmailUtil.java +++ b/src/main/java/com/ai/da/common/utils/SendEmailUtil.java @@ -611,7 +611,6 @@ public class SendEmailUtil { 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 { @@ -630,7 +629,7 @@ public class SendEmailUtil { // 实例化一个请求对象,每个接口都会对应一个request对象 SendEmailRequest req = new SendEmailRequest(); req.setFromEmailAddress(SEND_ADDRESS); - req.setDestination(new String[]{shb, xp, wxd, pkc}); + req.setDestination(new String[]{xp, wxd, pkc}); Template template = new Template(); req.setSubject("上传图片超时提醒"); template.setTemplateID(UPLOAD_TIMEOUT_REMINDER); @@ -745,9 +744,12 @@ public class SendEmailUtil { // 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 NEW_MERCHANT_EN = 135190L; - private final static Long NEW_USER_EN = 135189L; - private final static Long NEW_USER_CN = 135186L; + private final static Long NEW_MERCHANT_EN = 140335L; +// private final static Long NEW_MERCHANT_EN = 135190L; +// private final static Long NEW_USER_EN = 135189L; +// private final static Long NEW_USER_CN = 135186L; + private final static Long NEW_USER_EN = 140316L; + private final static Long NEW_USER_CN = 140317L; private final static Long RENEWAL_MERCHANT_EN = 130724L; private final static Long RENEWAL_USER_EN = 130725L; private final static Long RENEWAL_USER_CN = 130726L; diff --git a/src/main/java/com/ai/da/controller/ConvenientInquiryController.java b/src/main/java/com/ai/da/controller/ConvenientInquiryController.java index f19db4c0..007b75fb 100644 --- a/src/main/java/com/ai/da/controller/ConvenientInquiryController.java +++ b/src/main/java/com/ai/da/controller/ConvenientInquiryController.java @@ -49,7 +49,9 @@ public class ConvenientInquiryController { String userEmail = accountService.getById(accountId).getUserEmail(); if (accountId.equals(31L) || accountId.equals(87L) || accountId.equals(83L) || accountId.equals(6L) || accountId.equals(4L) || accountId.equals(73L) - || userEmail.equals("joho8228@hotmail.com") || userEmail.equals("wanninghua160@gmail.com") + || userEmail.equals("joho8228@hotmail.com") + || userEmail.equals("chelseayu@code-create.com.hk") + || userEmail.equals("cheungzt007@gmail.com") ) { return Response.success(convenientInquiryService.getTrial(queryUserConditionsVO)); } else { @@ -61,7 +63,28 @@ public class ConvenientInquiryController { @GetMapping("/getDesignStatistic") public Response> getDesignStatistic(@RequestParam(required = false) String startTime, @RequestParam(required = false) String endTime, @RequestParam(required = false) List ids, @RequestParam(required = false) String email) { - return Response.success(convenientInquiryService.getDesignStatistic(startTime, endTime, ids, email)); + Long accountId = UserContext.getUserHolder().getId(); + String userEmail = accountService.getById(accountId).getUserEmail(); + if (accountId.equals(31L) || accountId.equals(87L) || accountId.equals(83L) + || accountId.equals(6L) || accountId.equals(4L) || accountId.equals(73L) + || userEmail.equals("joho8228@hotmail.com") + || userEmail.equals("chelseayu@code-create.com.hk") + || userEmail.equals("cheungzt007@gmail.com") + ) { + if (StringUtil.isNullOrEmpty(startTime)) startTime = "2024-02-01 00:00:00"; + if (StringUtil.isNullOrEmpty(endTime)) { + SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); + Date date = new Date(); + endTime = simpleDateFormat.format(date); + } + if (!StringUtil.isNullOrEmpty(email)){ + email = email.trim(); + } + List designStatistic = designMapper.getDesignStatistic(startTime, endTime, ids, email); + return Response.success(designStatistic); + } else { + return Response.fail("Sorry, you don't have permission"); + } } @@ -163,7 +186,9 @@ public class ConvenientInquiryController { String userEmail = accountService.getById(accountId).getUserEmail(); if (accountId.equals(31L) || accountId.equals(87L) || accountId.equals(83L) || accountId.equals(6L) || accountId.equals(4L) || accountId.equals(73L) - || userEmail.equals("joho8228@hotmail.com") || userEmail.equals("wanninghua160@gmail.com") + || userEmail.equals("joho8228@hotmail.com") + || userEmail.equals("chelseayu@code-create.com.hk") + || userEmail.equals("cheungzt007@gmail.com") ) { return Response.success(convenientInquiryService.getUserInfo(queryUserConditionsVO)); } else { diff --git a/src/main/java/com/ai/da/controller/StripeController.java b/src/main/java/com/ai/da/controller/StripeController.java index e02a1765..07ab7a5d 100644 --- a/src/main/java/com/ai/da/controller/StripeController.java +++ b/src/main/java/com/ai/da/controller/StripeController.java @@ -137,9 +137,11 @@ public class StripeController { @ApiOperation("更新推广码信息") @GetMapping("/updatePromCodeInfo") - public Response updateCouponsInfo(@RequestParam Long id, @RequestParam(required = false) Long paidCommission, - @RequestParam(required = false) String cooperator, @RequestParam(required = false) String remark){ - return Response.success(stripeService.updateCouponsInfo(id, paidCommission, cooperator, remark)); + public Response updateCouponsInfo(@RequestParam Long id, @RequestParam(required = false) String paidCommission, + @RequestParam(required = false) String cooperator, + @RequestParam(required = false) String remark, + @RequestParam(required = false) Long startTime){ + return Response.success(stripeService.updateCouponsInfo(id, paidCommission, cooperator, remark, startTime)); } @ApiOperation("删除推广码") diff --git a/src/main/java/com/ai/da/mapper/primary/PaymentInfoMapper.java b/src/main/java/com/ai/da/mapper/primary/PaymentInfoMapper.java index 9384ad6b..0dc85cce 100644 --- a/src/main/java/com/ai/da/mapper/primary/PaymentInfoMapper.java +++ b/src/main/java/com/ai/da/mapper/primary/PaymentInfoMapper.java @@ -6,6 +6,7 @@ import com.ai.da.model.vo.PaymentInfoVO; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Param; +import java.math.BigDecimal; import java.util.List; import java.util.Map; @@ -24,6 +25,10 @@ public interface PaymentInfoMapper extends BaseMapper { String country, String city, String startTime, String endTime, String payer ); + BigDecimal queryTotalPaymentAmount(String paymentType,String payerTotal, String type, String status, + String country, String city, String startTime, String endTime, String payer + ); + List> getCities(); List> getCountries(); diff --git a/src/main/java/com/ai/da/mapper/primary/entity/ProductCoupons.java b/src/main/java/com/ai/da/mapper/primary/entity/ProductCoupons.java index 3b6a225d..c742ae14 100644 --- a/src/main/java/com/ai/da/mapper/primary/entity/ProductCoupons.java +++ b/src/main/java/com/ai/da/mapper/primary/entity/ProductCoupons.java @@ -7,6 +7,8 @@ import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import java.math.BigDecimal; + @EqualsAndHashCode(callSuper = true) @Data @TableName("t_product_coupons") @@ -20,6 +22,8 @@ public class ProductCoupons extends BaseEntity{ private String promotionCodeId; // 对应的推广码 private String promotionCode; + // 优惠券有效期开始时间 + private Long startTime; // 最大兑换次数 private Long maxRedemptions; // 优惠券的折扣 @@ -29,13 +33,13 @@ public class ProductCoupons extends BaseEntity{ // 合作者 private String cooperator; // 使用了该优惠券支付的总金额 - private float totalEarnings; + private BigDecimal totalEarnings = BigDecimal.ZERO; // 佣金 - private float commission; + private BigDecimal commission = BigDecimal.ZERO; // 已付佣金 - private float paidCommission; + private BigDecimal paidCommission = BigDecimal.ZERO; // 未付佣金 - private float unpaidCommission; + private BigDecimal unpaidCommission = BigDecimal.ZERO; // 备注 private String remark; diff --git a/src/main/java/com/ai/da/model/dto/CreateCouponDTO.java b/src/main/java/com/ai/da/model/dto/CreateCouponDTO.java index 311f7c67..3e1c60b5 100644 --- a/src/main/java/com/ai/da/model/dto/CreateCouponDTO.java +++ b/src/main/java/com/ai/da/model/dto/CreateCouponDTO.java @@ -14,7 +14,9 @@ public class CreateCouponDTO { @NotNull(message = "Please set the commissionRate.") private Float commissionRate; @ApiModelProperty("推广码到期时间 秒级时间戳") - private Long timestamp; + private Long endTime; + @ApiModelProperty("推广码开始时间 秒级时间戳") + private Long startTime; @ApiModelProperty("推广码最大使用次数") private Long maxRedemptions; @ApiModelProperty("合作者/机构名") diff --git a/src/main/java/com/ai/da/model/dto/EmailSendDTO.java b/src/main/java/com/ai/da/model/dto/EmailSendDTO.java index da5bc476..8e130d36 100644 --- a/src/main/java/com/ai/da/model/dto/EmailSendDTO.java +++ b/src/main/java/com/ai/da/model/dto/EmailSendDTO.java @@ -15,8 +15,8 @@ public class EmailSendDTO { private String email; @NotBlank(message = "operationType.cannot.be.empty") - @ApiModelProperty("操作类型 LOGIN 注册 FORGET_PWD 忘记密码 BIND_MAILBOX 绑定邮箱 " + - "CHANGE_MAILBOX 更改邮箱 UPDATE_USERINFO 仅填写国家、职业(不发送邮件)") + @ApiModelProperty("操作类型 LOGIN 登录 FORGET_PWD 忘记密码 BIND_MAILBOX 绑定邮箱 " + + "CHANGE_MAILBOX 更改邮箱 UPDATE_USERINFO 仅填写国家、职业(不发送邮件) REGISTER 注册") private String operationType; @ApiModelProperty("异常ip") diff --git a/src/main/java/com/ai/da/model/dto/GenerateModifyDTO.java b/src/main/java/com/ai/da/model/dto/GenerateModifyDTO.java index 38ddf43a..63dae8d4 100644 --- a/src/main/java/com/ai/da/model/dto/GenerateModifyDTO.java +++ b/src/main/java/com/ai/da/model/dto/GenerateModifyDTO.java @@ -23,6 +23,9 @@ public class GenerateModifyDTO { @ApiModelProperty(value = "sketch所属分类", required = true) private String category; + @ApiModelProperty(value = "originalId的来源 Library || Generate(默认为空)", required = true) + private String originalIdSource; + @NotNull(message = "id cannot be empty") @ApiModelProperty(value = "原图id", required = true) private Long originalId; diff --git a/src/main/java/com/ai/da/model/vo/CheckCouponsVO.java b/src/main/java/com/ai/da/model/vo/CheckCouponsVO.java index 97b72d9c..dbbf2aeb 100644 --- a/src/main/java/com/ai/da/model/vo/CheckCouponsVO.java +++ b/src/main/java/com/ai/da/model/vo/CheckCouponsVO.java @@ -9,7 +9,7 @@ import lombok.NoArgsConstructor; @NoArgsConstructor public class CheckCouponsVO { - @ApiModelProperty("expired || invalid || valid") + @ApiModelProperty("expired 过期 || invalid 无效 || valid 有效 || pending 尚未生效") private String status; private String message; diff --git a/src/main/java/com/ai/da/model/vo/PaymentInfoVO.java b/src/main/java/com/ai/da/model/vo/PaymentInfoVO.java index caa1955c..98ac7f14 100644 --- a/src/main/java/com/ai/da/model/vo/PaymentInfoVO.java +++ b/src/main/java/com/ai/da/model/vo/PaymentInfoVO.java @@ -13,6 +13,8 @@ public class PaymentInfoVO { private Long id; @ApiModelProperty("付款用户名") private String payer; + @ApiModelProperty("付款者邮箱") + private String email; @ApiModelProperty("选择的支付平台 PayPal || Stripe || Alipay-HK") private String platform; @ApiModelProperty("支付的金额 单位:HKD") diff --git a/src/main/java/com/ai/da/service/DesignItemService.java b/src/main/java/com/ai/da/service/DesignItemService.java index fcc1e78b..6702ad52 100644 --- a/src/main/java/com/ai/da/service/DesignItemService.java +++ b/src/main/java/com/ai/da/service/DesignItemService.java @@ -53,6 +53,8 @@ public interface DesignItemService extends IService { DesignSingleVO designSingleIncludeLayers(DesignSingleIncludeLayersDTO designSingleIncludeLayersDTO); + Map setPriorityAndUndividedLayer(JSONArray layers); + Map setTypeAndUndividedLayer(JSONArray layers); ComposeLayersVO editLayersPositionAndScale(EditLayersPositionAndScaleVO positionAndScaleVO) throws IOException; diff --git a/src/main/java/com/ai/da/service/StripeService.java b/src/main/java/com/ai/da/service/StripeService.java index 7087ca1a..4ab7b2b9 100644 --- a/src/main/java/com/ai/da/service/StripeService.java +++ b/src/main/java/com/ai/da/service/StripeService.java @@ -61,7 +61,7 @@ public interface StripeService { CheckCouponsVO checkProductCoupon(String promotionCode, Long price); - ProductCoupons updateCouponsInfo(Long id, Long paidCommission, String cooperator, String remark); + ProductCoupons updateCouponsInfo(Long id, String paidCommission, String cooperator, String remark, Long startTime); ProductCoupons getProductCoupon(String promotionCode, String promotionCodeId); 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 9adee153..6c9e9746 100644 --- a/src/main/java/com/ai/da/service/impl/AccountServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/AccountServiceImpl.java @@ -549,11 +549,15 @@ public class AccountServiceImpl extends ServiceImpl impl result = true; } break; + case REGISTER: + result = SendEmailUtil.designWorksRegister(emailSendDTO.getEmail(), randomVerifyCode); + break; default: } if (!result) { throw new BusinessException("failed.to.send.mail"); } + log.info("向邮箱 {} 发送验证码为:{}, 邮件类型:{}", emailSendDTO.getEmail(), randomVerifyCode, authenticationOperationTypeEnum); return Boolean.TRUE; } @@ -1182,13 +1186,13 @@ public class AccountServiceImpl extends ServiceImpl impl } String randomVerifyCode = RandomsUtil.generateVerifyCode(100000L, 999999L); - LocalCacheUtils.setVerifyCodeCache("DesignWorksRegister" + "_" + accountDesignWorksRegisterDTO.getUserEmail(), randomVerifyCode); + LocalCacheUtils.setVerifyCodeCache("REGISTER" + "_" + userEmail, randomVerifyCode); - Boolean b = SendEmailUtil.designWorksRegister(accountDesignWorksRegisterDTO.getUserEmail(), randomVerifyCode); + Boolean b = SendEmailUtil.designWorksRegister(userEmail, randomVerifyCode); if (!b) { throw new BusinessException("failed.to.send.mail"); } - log.info("注册账号。成功向 {} 账号发送验证码:{}", accountDesignWorksRegisterDTO.getUserEmail(), randomVerifyCode); + log.info("向邮箱 {} 发送验证码:{}, 邮件类型:REGISTER", userEmail, randomVerifyCode); return Boolean.TRUE; } @@ -1196,7 +1200,7 @@ public class AccountServiceImpl extends ServiceImpl impl public AccountLoginVO designWorksRegisterCode(AccountDesignWorksRegisterDTO accountDesignWorksRegisterDTO, HttpServletRequest request) { - String verifyCode = LocalCacheUtils.getVerifyCodeCache("DesignWorksRegister" + "_" + accountDesignWorksRegisterDTO.getUserEmail()); + String verifyCode = LocalCacheUtils.getVerifyCodeCache("REGISTER" + "_" + accountDesignWorksRegisterDTO.getUserEmail()); if (StringUtils.isBlank(verifyCode)) { throw new BusinessException("the.verification.code.has.expired", ResultEnum.PROMPT.getCode()); } @@ -1260,9 +1264,6 @@ public class AccountServiceImpl extends ServiceImpl impl config.setJdbcUrl("jdbc:mysql://code-create.com.hk:3306/db1nfvsgmjp3b8"); config.setUsername("uafqtz4gsvfrw"); config.setPassword("aida123456."); -// config.setJdbcUrl("jdbc:mysql://18.167.251.121:33008/aida"); -// config.setUsername("aida_con"); -// config.setPassword("123456"); // config.setJdbcUrl("jdbc:mysql://localhost:3306/code-create-local?serverTimezone=UTC"); // config.setUsername("root"); // config.setPassword("root"); diff --git a/src/main/java/com/ai/da/service/impl/AffiliateServiceImpl.java b/src/main/java/com/ai/da/service/impl/AffiliateServiceImpl.java index c4066afd..1b837b4f 100644 --- a/src/main/java/com/ai/da/service/impl/AffiliateServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/AffiliateServiceImpl.java @@ -1,448 +1,555 @@ -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.PageBaseResponse; -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.RedisUtilEnhance; -import com.ai.da.common.utils.SendEmailUtil; -import com.ai.da.mapper.primary.*; -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.*; -import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; -import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; -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 org.springframework.util.CollectionUtils; - -import javax.annotation.Resource; -import java.math.BigDecimal; -import java.time.LocalDateTime; -import java.util.*; -import java.util.function.Function; - -import static com.ai.da.common.utils.RedisUtil.AFFILIATE_LINK_VIEW_KEY; - -@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 ReferralService referralService; - @Resource - private ReferralMapper referralMapper; - @Resource - private StripeService stripeService; - @Resource - private ProductCouponsMapper productCouponsMapper; - @Resource - private RedisUtil redisUtil; - @Resource - private EmailService emailService; - - // 推广者注册 - 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 merchantEmail = "kimwong@code-create.com.hk"; - String developer = "xupei3360@163.com"; - String[] receiverEmail = {/*merchantEmail, */developer}; - SendEmailUtil.affiliateEmailReminder(receiverEmail, new AffiliateEmailParamsDTO(userHolder.getUsername(), promotionMethod), "new"); -// emailService.affiliateEmailReminder(Arrays.asList(/*merchantEmail,*/ developer), new AffiliateEmailParamsDTO(userHolder.getUsername(), promotionMethod), "new"); - }else { - throw new BusinessException("You have registered an Affiliate", ResultEnum.PROMPT.getCode()); - } - return true; - } - - public PageBaseResponse getAffiliateList(AffiliateQueryDTO affiliateQueryDTO){ - log.info("parameter => {}", affiliateQueryDTO.toString()); - - int offset = (affiliateQueryDTO.getPage() - 1) * affiliateQueryDTO.getSize(); - String orderBy = affiliateQueryDTO.getOrderBy() == null ? "id" : - affiliateQueryDTO.getOrderBy().equals("id") ? "id" : - affiliateQueryDTO.getOrderBy().equals("totalIncome") ? "total_earnings" : - "create_time"; - List affiliateList = baseMapper.getAffiliateList(affiliateQueryDTO.getStatus(), - affiliateQueryDTO.getStartTime(), - affiliateQueryDTO.getEndTime(), - affiliateQueryDTO.getOrder(), - affiliateQueryDTO.getAffiliateId(), - orderBy, - affiliateQueryDTO.getSize(), - offset - ); - if (CollectionUtils.isEmpty(affiliateList)) { - return PageBaseResponse.success(new Page<>()); - }else { - int totalCount = baseMapper.queryAffiliateTotalCount(affiliateQueryDTO.getStatus(), - affiliateQueryDTO.getStartTime(), - affiliateQueryDTO.getEndTime(), - affiliateQueryDTO.getAffiliateId() - ); - IPage orderListVOIPage = new Page<>(); - Integer size = affiliateQueryDTO.getSize(); - orderListVOIPage.setSize(size); - orderListVOIPage.setRecords(affiliateList); - orderListVOIPage.setCurrent(affiliateQueryDTO.getPage()); - orderListVOIPage.setPages((long)Math.ceil((double) totalCount / size)); - orderListVOIPage.setTotal(totalCount); - return PageBaseResponse.success(orderListVOIPage); - } - /*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"); - Page affiliatePage = baseMapper.selectPage(new Page<>(affiliateQueryDTO.getPage(), affiliateQueryDTO.getSize()), qw); - affiliatePage.convert((Function) affiliate-> { - AffiliateVO affiliateVO = CopyUtil.copyObject(affiliate, AffiliateVO.class); - affiliateVO.setUsername(); - }); - return affiliatePage;*/ - } - - 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); - return affiliateVO; - } - - public BigDecimal[] getPersonalMonthlyIncome(int year){ - Long accountId = UserContext.getUserHolder().getId(); - List> personalMonthlyIncome = referralMapper.getPersonalMonthlyIncome(accountId, year); - BigDecimal[] commissions = new BigDecimal[12]; - Arrays.fill(commissions, BigDecimal.ZERO); - personalMonthlyIncome.forEach(income -> { - int month = Integer.parseInt(income.get("yearMonth").toString()); - commissions[month-1] = (BigDecimal) income.get("totalCommission"); - }); - - return commissions; - } - - // 审批申请 - public Boolean applicationApproval(Long id, Boolean isApproved, Float commission){ - Affiliate affiliate = baseMapper.selectById(id); - - // 1、更新db状态 - if (isApproved){ - // 更新状态 - affiliate.setStatus("Active"); - affiliate.setApproved(true); - affiliate.setLink(CommonConstant.AFFILIATE_LINK + affiliate.getId()); - if (Objects.isNull(commission)) { - // 未设置佣金比例的情况下,默认25% - affiliate.setCommissionPercent(25f); - } else { - affiliate.setCommissionPercent(commission); - } - } 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"); -// emailService.affiliateEmailReminder(Collections.singletonList(account.getUserEmail()), new AffiliateEmailParamsDTO(userName), "accepted"); - }else { - SendEmailUtil.affiliateEmailReminder(userEmail, new AffiliateEmailParamsDTO(userName), "refused"); -// emailService.affiliateEmailReminder(Collections.singletonList(account.getUserEmail()), new AffiliateEmailParamsDTO(userName), "refused"); - } - return true; - } - - public void updateCommissionPercentage(Long id, Float commission){ - Affiliate affiliate = baseMapper.selectById(id); - if (Objects.isNull(affiliate)){ - log.info("未知affiliate id :{}", id); - throw new BusinessException("unknown affiliate"); - } - if (!Objects.equals(affiliate.getCommissionPercent(), commission)){ - affiliate.setCommissionPercent(commission); - affiliate.setUpdateTime(LocalDateTime.now()); - baseMapper.updateById(affiliate); - } - } - - // 定时计算佣金 - 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<>(); - queryWrapper.lambda().eq(PaymentInfo::getTradeState, "paid") - .and(wrapper -> wrapper - .eq(PaymentInfo::getType,"new") - .or() - .eq(PaymentInfo::getType, "manual")); - if (!StringUtil.isNullOrEmpty(lastTime)){ - queryWrapper.lambda().gt(PaymentInfo::getCreateTime, lastTime); - } - - 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())){ - log.info("结算订单id为{}的佣金", orderInfo.getId()); - // 3、若有, 直接更新affiliate的所得 - Affiliate affiliate = baseMapper.selectById(account.getInvitationCode()); - if (Objects.isNull(affiliate)){ - log.error("未知affiliate Id:{}, 关联订单:{}", account.getInvitationCode(), orderInfo.getOrderNo()); - return; - } - Float payerTotal = paymentInfo.getPayerTotal(); - - if (payerTotal > 0){ - // 分配新用户首次订阅所付费用 预设的佣金比例 作为佣金 - // 逻辑修改。只有当该笔referral被审批通过后才被计算为推广者的佣金 - BigDecimal commission = BigDecimal.valueOf(payerTotal).multiply(BigDecimal.valueOf(affiliate.getCommissionPercent() / 100)); - /*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、添加到referral - Referral referral = new Referral(); - referral.setAffiliateId(affiliate.getId()); - referral.setAffiliateAccountId(affiliate.getAccountId()); - referral.setInviteeAccountId(accountId); - referral.setAmount(BigDecimal.valueOf(payerTotal)); - referral.setPaymentInfoId(paymentInfo.getId()); - referral.setPaymentTime(paymentInfo.getCreateTime()); - referral.setCommission(BigDecimal.valueOf(commission.floatValue())); - referral.setCommissionPercent(affiliate.getCommissionPercent()); - referral.setStatus("Pending"); - referral.setCreateTime(LocalDateTime.now()); - int insert = referralService.getBaseMapper().insert(referral); - log.info("向Referral中插入 {} 条数据", insert); - } - } - 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); - } - - @Resource - private RedisUtilEnhance redisUtilEnhance; - - public void syncLinkViewCountToDB() { - // 1、获取当前所有激活状态的affiliate - List affiliateList = baseMapper.selectList(new QueryWrapper().lambda().eq(Affiliate::getStatus, "Active")); - - // 2、获取redis中各自的viewCount - for (Affiliate affiliate : affiliateList){ - String redisKey = AFFILIATE_LINK_VIEW_KEY + affiliate.getId(); - - // 原子性获取并重置为0 - Long redisCount = redisUtilEnhance.getAndSetKey(redisKey, 0L); - - if (redisCount != null && redisCount > 0) { - // 累加到数据库 - baseMapper.update( - null, - new UpdateWrapper() - .setSql("visits = visits + " + redisCount) - .set("update_time", LocalDateTime.now()) - .eq("id", affiliate.getId()) - ); - } - } - } - - // 查看每个affiliate带来收入的详情 - @Override - public IPage getEachAffiliateGeneratedRevenue(AffiliateQueryDTO affiliateQueryDTO) { - if (Objects.isNull(affiliateQueryDTO.getAffiliateId())){ - throw new BusinessException("Please specify the affiliate ID.", ResultEnum.PROMPT.getCode()); - } - - QueryWrapper referralQueryWrapper = new QueryWrapper<>(); - referralQueryWrapper - .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 = referralMapper.selectPage(new Page<>(affiliateQueryDTO.getPage(), affiliateQueryDTO.getSize()), referralQueryWrapper); - 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 = referralMapper.getMonthlyAffiliateIncome(year, month); - // 1、总收入(近一个月通过affiliate产生的收入),未支付的金额 affiliate表中unpaid的总和 - BigDecimal totalAmount = BigDecimal.ZERO; - BigDecimal paidCommission = BigDecimal.ZERO; - BigDecimal unpaidCommission = BigDecimal.ZERO; - if (!monthlyAffiliateIncome.isEmpty()){ - Map monthlyIncome = monthlyAffiliateIncome.get(0); - totalAmount = (BigDecimal) monthlyIncome.get("totalAmount"); - paidCommission = (BigDecimal) monthlyIncome.get("paidCommission"); - unpaidCommission = (BigDecimal) monthlyIncome.get("unpaidCommission"); - } - - // 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(String.valueOf(unpaidCommission)); - affiliateEmailParamsDTO.setPaidEarnings(String.valueOf(paidCommission)); - - String merchantEmail = "kimwong@code-create.com.hk"; - String developer = "xupei3360@163.com"; - String[] receiverEmail = {/*merchantEmail,*/ developer}; - // 邮件通知 - SendEmailUtil.affiliateEmailReminder(receiverEmail, affiliateEmailParamsDTO, "summary"); -// emailService.affiliateEmailReminder(Arrays.asList(/*merchantEmail,*/ developer), 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); - } - - public void calcCouponsCommission(){ - // 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) - .lt("create_time", currentTime) - .isNotNull("promotion_code"); - } - List paymentInfos = paymentInfoService.getBaseMapper().selectList(queryWrapper); - - // key:推广码, value:用户支付的金额 - HashMap codeAmount = new HashMap<>(); - if (!paymentInfos.isEmpty()){ - for (PaymentInfo paymentInfo : paymentInfos){ - String promotionCode = paymentInfo.getPromotionCode(); - Float sum = codeAmount.get(promotionCode); - if (sum == null || sum == 0.0f){ - codeAmount.put(promotionCode, paymentInfo.getPayerTotal()); - }else { - codeAmount.put(promotionCode, sum + paymentInfo.getPayerTotal()); - } - } - for (Map.Entry entry : codeAmount.entrySet()){ - String promotionCode = entry.getKey(); - ProductCoupons productCoupons = stripeService.getProductCoupon(promotionCode, null); - if (!Objects.isNull(productCoupons)){ - // 2、计算支付金额的总和,更新totalEarnings,commission,unpaidCommission - float sum = productCoupons.getTotalEarnings() + entry.getValue(); - productCoupons.setTotalEarnings(sum); - float commission = sum * productCoupons.getCommissionRate() / 100; - productCoupons.setCommission(commission); - productCoupons.setUnpaidCommission(commission - productCoupons.getPaidCommission()); - productCouponsMapper.updateById(productCoupons); - } - } - } - redisUtil.addToString(RedisUtil.PAYMENT_INFO_LAST_SCAN_TIME, currentTime); - } - - @Override - public List> getAllAffiliateUsername(){ - return baseMapper.selectAllAffiliateUsername(); - } - - -} +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.PageBaseResponse; +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.RedisUtilEnhance; +import com.ai.da.common.utils.SendEmailUtil; +import com.ai.da.mapper.primary.*; +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.*; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +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 org.springframework.util.CollectionUtils; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDateTime; +import java.util.*; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static com.ai.da.common.utils.RedisUtil.AFFILIATE_LINK_VIEW_KEY; + +@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 ReferralService referralService; + @Resource + private ReferralMapper referralMapper; + @Resource + private StripeService stripeService; + @Resource + private ProductCouponsMapper productCouponsMapper; + @Resource + private RedisUtil redisUtil; + @Resource + private EmailService emailService; + + // 推广者注册 + 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 merchantEmail = "kimwong@code-create.com.hk"; + String developer = "xupei3360@163.com"; + String[] receiverEmail = {/*merchantEmail, */developer}; + SendEmailUtil.affiliateEmailReminder(receiverEmail, new AffiliateEmailParamsDTO(userHolder.getUsername(), promotionMethod), "new"); +// emailService.affiliateEmailReminder(Arrays.asList(/*merchantEmail,*/ developer), new AffiliateEmailParamsDTO(userHolder.getUsername(), promotionMethod), "new"); + }else { + throw new BusinessException("You have registered an Affiliate", ResultEnum.PROMPT.getCode()); + } + return true; + } + + public PageBaseResponse getAffiliateList(AffiliateQueryDTO affiliateQueryDTO){ + log.info("parameter => {}", affiliateQueryDTO.toString()); + + int offset = (affiliateQueryDTO.getPage() - 1) * affiliateQueryDTO.getSize(); + String orderBy = affiliateQueryDTO.getOrderBy() == null ? "id" : + affiliateQueryDTO.getOrderBy().equals("id") ? "id" : + affiliateQueryDTO.getOrderBy().equals("totalIncome") ? "total_earnings" : + "create_time"; + List affiliateList = baseMapper.getAffiliateList(affiliateQueryDTO.getStatus(), + affiliateQueryDTO.getStartTime(), + affiliateQueryDTO.getEndTime(), + affiliateQueryDTO.getOrder(), + affiliateQueryDTO.getAffiliateId(), + orderBy, + affiliateQueryDTO.getSize(), + offset + ); + if (CollectionUtils.isEmpty(affiliateList)) { + return PageBaseResponse.success(new Page<>()); + }else { + int totalCount = baseMapper.queryAffiliateTotalCount(affiliateQueryDTO.getStatus(), + affiliateQueryDTO.getStartTime(), + affiliateQueryDTO.getEndTime(), + affiliateQueryDTO.getAffiliateId() + ); + IPage orderListVOIPage = new Page<>(); + Integer size = affiliateQueryDTO.getSize(); + orderListVOIPage.setSize(size); + orderListVOIPage.setRecords(affiliateList); + orderListVOIPage.setCurrent(affiliateQueryDTO.getPage()); + orderListVOIPage.setPages((long)Math.ceil((double) totalCount / size)); + orderListVOIPage.setTotal(totalCount); + return PageBaseResponse.success(orderListVOIPage); + } + /*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"); + Page affiliatePage = baseMapper.selectPage(new Page<>(affiliateQueryDTO.getPage(), affiliateQueryDTO.getSize()), qw); + affiliatePage.convert((Function) affiliate-> { + AffiliateVO affiliateVO = CopyUtil.copyObject(affiliate, AffiliateVO.class); + affiliateVO.setUsername(); + }); + return affiliatePage;*/ + } + + 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); + return affiliateVO; + } + + public BigDecimal[] getPersonalMonthlyIncome(int year){ + Long accountId = UserContext.getUserHolder().getId(); + List> personalMonthlyIncome = referralMapper.getPersonalMonthlyIncome(accountId, year); + BigDecimal[] commissions = new BigDecimal[12]; + Arrays.fill(commissions, BigDecimal.ZERO); + personalMonthlyIncome.forEach(income -> { + int month = Integer.parseInt(income.get("yearMonth").toString()); + commissions[month-1] = (BigDecimal) income.get("totalCommission"); + }); + + return commissions; + } + + // 审批申请 + public Boolean applicationApproval(Long id, Boolean isApproved, Float commission){ + Affiliate affiliate = baseMapper.selectById(id); + + // 1、更新db状态 + if (isApproved){ + // 更新状态 + affiliate.setStatus("Active"); + affiliate.setApproved(true); + affiliate.setLink(CommonConstant.AFFILIATE_LINK + affiliate.getId()); + if (Objects.isNull(commission)) { + // 未设置佣金比例的情况下,默认25% + affiliate.setCommissionPercent(25f); + } else { + affiliate.setCommissionPercent(commission); + } + } 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"); +// emailService.affiliateEmailReminder(Collections.singletonList(account.getUserEmail()), new AffiliateEmailParamsDTO(userName), "accepted"); + }else { + SendEmailUtil.affiliateEmailReminder(userEmail, new AffiliateEmailParamsDTO(userName), "refused"); +// emailService.affiliateEmailReminder(Collections.singletonList(account.getUserEmail()), new AffiliateEmailParamsDTO(userName), "refused"); + } + return true; + } + + public void updateCommissionPercentage(Long id, Float commission){ + Affiliate affiliate = baseMapper.selectById(id); + if (Objects.isNull(affiliate)){ + log.info("未知affiliate id :{}", id); + throw new BusinessException("unknown affiliate"); + } + if (!Objects.equals(affiliate.getCommissionPercent(), commission)){ + affiliate.setCommissionPercent(commission); + affiliate.setUpdateTime(LocalDateTime.now()); + baseMapper.updateById(affiliate); + } + } + + // 定时计算佣金 + 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<>(); + queryWrapper.lambda().eq(PaymentInfo::getTradeState, "paid") + .and(wrapper -> wrapper + .eq(PaymentInfo::getType,"new") + .or() + .eq(PaymentInfo::getType, "manual")); + if (!StringUtil.isNullOrEmpty(lastTime)){ + queryWrapper.lambda().gt(PaymentInfo::getCreateTime, lastTime); + } + + 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())){ + log.info("结算订单id为{}的佣金", orderInfo.getId()); + // 3、若有, 直接更新affiliate的所得 + Affiliate affiliate = baseMapper.selectById(account.getInvitationCode()); + if (Objects.isNull(affiliate)){ + log.error("未知affiliate Id:{}, 关联订单:{}", account.getInvitationCode(), orderInfo.getOrderNo()); + return; + } + Float payerTotal = paymentInfo.getPayerTotal(); + + if (payerTotal > 0){ + // 分配新用户首次订阅所付费用 预设的佣金比例 作为佣金 + // 逻辑修改。只有当该笔referral被审批通过后才被计算为推广者的佣金 + BigDecimal commission = BigDecimal.valueOf(payerTotal).multiply(BigDecimal.valueOf(affiliate.getCommissionPercent() / 100)); + /*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、添加到referral + Referral referral = new Referral(); + referral.setAffiliateId(affiliate.getId()); + referral.setAffiliateAccountId(affiliate.getAccountId()); + referral.setInviteeAccountId(accountId); + referral.setAmount(BigDecimal.valueOf(payerTotal)); + referral.setPaymentInfoId(paymentInfo.getId()); + referral.setPaymentTime(paymentInfo.getCreateTime()); + referral.setCommission(BigDecimal.valueOf(commission.floatValue())); + referral.setCommissionPercent(affiliate.getCommissionPercent()); + referral.setStatus("Pending"); + referral.setCreateTime(LocalDateTime.now()); + int insert = referralService.getBaseMapper().insert(referral); + log.info("向Referral中插入 {} 条数据", insert); + } + } + 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); + } + + @Resource + private RedisUtilEnhance redisUtilEnhance; + + public void syncLinkViewCountToDB() { + // 1、获取当前所有激活状态的affiliate + List affiliateList = baseMapper.selectList(new QueryWrapper().lambda().eq(Affiliate::getStatus, "Active")); + + // 2、获取redis中各自的viewCount + for (Affiliate affiliate : affiliateList){ + String redisKey = AFFILIATE_LINK_VIEW_KEY + affiliate.getId(); + + // 原子性获取并重置为0 + Long redisCount = redisUtilEnhance.getAndSetKey(redisKey, 0L); + + if (redisCount != null && redisCount > 0) { + // 累加到数据库 + baseMapper.update( + null, + new UpdateWrapper() + .setSql("visits = visits + " + redisCount) + .set("update_time", LocalDateTime.now()) + .eq("id", affiliate.getId()) + ); + } + } + } + + // 查看每个affiliate带来收入的详情 + @Override + public IPage getEachAffiliateGeneratedRevenue(AffiliateQueryDTO affiliateQueryDTO) { + if (Objects.isNull(affiliateQueryDTO.getAffiliateId())){ + throw new BusinessException("Please specify the affiliate ID.", ResultEnum.PROMPT.getCode()); + } + + QueryWrapper referralQueryWrapper = new QueryWrapper<>(); + referralQueryWrapper + .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 = referralMapper.selectPage(new Page<>(affiliateQueryDTO.getPage(), affiliateQueryDTO.getSize()), referralQueryWrapper); + 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 = referralMapper.getMonthlyAffiliateIncome(year, month); + // 1、总收入(近一个月通过affiliate产生的收入),未支付的金额 affiliate表中unpaid的总和 + BigDecimal totalAmount = BigDecimal.ZERO; + BigDecimal paidCommission = BigDecimal.ZERO; + BigDecimal unpaidCommission = BigDecimal.ZERO; + if (!monthlyAffiliateIncome.isEmpty()){ + Map monthlyIncome = monthlyAffiliateIncome.get(0); + totalAmount = (BigDecimal) monthlyIncome.get("totalAmount"); + paidCommission = (BigDecimal) monthlyIncome.get("paidCommission"); + unpaidCommission = (BigDecimal) monthlyIncome.get("unpaidCommission"); + } + + // 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(String.valueOf(unpaidCommission)); + affiliateEmailParamsDTO.setPaidEarnings(String.valueOf(paidCommission)); + + String merchantEmail = "kimwong@code-create.com.hk"; + String developer = "xupei3360@163.com"; + String[] receiverEmail = {/*merchantEmail,*/ developer}; + // 邮件通知 + SendEmailUtil.affiliateEmailReminder(receiverEmail, affiliateEmailParamsDTO, "summary"); +// emailService.affiliateEmailReminder(Arrays.asList(/*merchantEmail,*/ developer), 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); + } + + public void calcCouponsCommission(){ + // 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) + .lt("create_time", currentTime) + .isNotNull("promotion_code"); + } + List paymentInfos = paymentInfoService.getBaseMapper().selectList(queryWrapper); + + // key:推广码, value:用户支付的金额 + HashMap codeAmount = new HashMap<>(); + if (!paymentInfos.isEmpty()){ + for (PaymentInfo paymentInfo : paymentInfos){ + String promotionCode = paymentInfo.getPromotionCode(); + Float sum = codeAmount.get(promotionCode); + if (sum == null || sum == 0.0f){ + codeAmount.put(promotionCode, paymentInfo.getPayerTotal()); + }else { + codeAmount.put(promotionCode, sum + paymentInfo.getPayerTotal()); + } + } + for (Map.Entry entry : codeAmount.entrySet()){ + String promotionCode = entry.getKey(); + ProductCoupons productCoupons = stripeService.getProductCoupon(promotionCode, null); + if (!Objects.isNull(productCoupons)){ + // 2、计算支付金额的总和,更新totalEarnings,commission,unpaidCommission + float sum = productCoupons.getTotalEarnings() + entry.getValue(); + productCoupons.setTotalEarnings(sum); + float commission = sum * productCoupons.getCommissionRate() / 100; + productCoupons.setCommission(commission); + productCoupons.setUnpaidCommission(commission - productCoupons.getPaidCommission()); + productCouponsMapper.updateById(productCoupons); + } + } + } + redisUtil.addToString(RedisUtil.PAYMENT_INFO_LAST_SCAN_TIME, currentTime); + } + + @Override + public List> getAllAffiliateUsername(){ + return baseMapper.selectAllAffiliateUsername(); + } + + /*public void calcCouponsCommission() { + String lastTime = redisUtil.getFromString(RedisUtil.PAYMENT_INFO_LAST_SCAN_TIME); + log.info("优惠券佣金计算,上次执行时间:{}", lastTime); + String currentTime = LocalDateTime.now().toString(); + + // 1. 查询新增的优惠券订单 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("trade_state", "paid") + .lt("create_time", currentTime) + .isNotNull("promotion_code"); + Optional.ofNullable(lastTime) + .filter(time -> !time.isEmpty()) + .ifPresent(time -> queryWrapper.gt("create_time", time)); + + List paymentInfos = paymentInfoService.list(queryWrapper); + log.info("目前,新增使用优惠券的订单数:{}", paymentInfos.size()); + + // 2. 按推广码汇总支付金额 + Map codeAmount = paymentInfos.stream() + .collect(Collectors.toMap( + PaymentInfo::getPromotionCode, + payment -> new BigDecimal(payment.getPayerTotal()), + BigDecimal::add + )); + + // 3. 更新佣金数据 + codeAmount.forEach((promotionCode, amount) -> { + ProductCoupons coupon = stripeService.getProductCoupon(promotionCode, null); + if (coupon != null) { + updateCouponCommission(coupon, amount); + productCouponsMapper.updateById(coupon); + } + }); + + // 4. 更新最后执行时间 + redisUtil.addToString(RedisUtil.PAYMENT_INFO_LAST_SCAN_TIME, currentTime); + }*/ + + public void calcCouponsCommission() { + // 1. 获取并格式化时间 + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + String lastTime = redisUtil.getFromString(RedisUtil.PAYMENT_INFO_LAST_SCAN_TIME); + log.info("优惠券佣金计算,上次执行时间:{}", lastTime); + + // 2. 查询新增订单(使用>=和<) + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("trade_state", "paid") + .isNotNull("promotion_code"); + + if (!StringUtils.isNullOrEmpty(lastTime)) { + queryWrapper.ge("create_time", lastTime); // 关键修复:>=代替> + } + queryWrapper.lt("create_time", LocalDateTime.now().format(formatter)); + + List paymentInfos = paymentInfoService.list(queryWrapper); + log.info("扫描时间范围: {} - {}, 新增订单数: {}", + lastTime, LocalDateTime.now().format(formatter), paymentInfos.size()); + + // 3. 按推广码汇总支付金额 + Map codeAmount = paymentInfos.stream() + .collect(Collectors.toMap( + PaymentInfo::getPromotionCode, + payment -> new BigDecimal(payment.getPayerTotal()), + BigDecimal::add + )); + + // 4. 更新佣金数据 + codeAmount.forEach((promotionCode, amount) -> { + ProductCoupons coupon = stripeService.getProductCoupon(promotionCode, null); + if (coupon != null) { + updateCouponCommission(coupon, amount); + productCouponsMapper.updateById(coupon); + } + }); + + // 5. 结束时更新Redis时间 + String newLastTime = LocalDateTime.now().format(formatter); + redisUtil.addToString(RedisUtil.PAYMENT_INFO_LAST_SCAN_TIME, newLastTime); + } + + /** + * 更新优惠券的佣金数据 + */ + private void updateCouponCommission(ProductCoupons coupon, BigDecimal newAmount) { + // 总收益 = 原收益(默认0) + 新增金额 + BigDecimal totalEarnings = Optional.ofNullable(coupon.getTotalEarnings()) + .orElse(BigDecimal.ZERO) + .add(newAmount); + coupon.setTotalEarnings(totalEarnings); + + // 佣金 = 总收益 × 佣金比例(需除以100) + BigDecimal commission = totalEarnings.multiply( + BigDecimal.valueOf(coupon.getCommissionRate()) + .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP)); + coupon.setCommission(commission); + + // 未支付佣金 = 总佣金 - 已支付佣金(默认0) + BigDecimal unpaidCommission = commission.subtract( + Optional.ofNullable(coupon.getPaidCommission()) + .orElse(BigDecimal.ZERO)); + coupon.setUnpaidCommission(unpaidCommission); + } + + +} diff --git a/src/main/java/com/ai/da/service/impl/ConvenientInquiryServiceImpl.java b/src/main/java/com/ai/da/service/impl/ConvenientInquiryServiceImpl.java index 842fb1bd..8dec1178 100644 --- a/src/main/java/com/ai/da/service/impl/ConvenientInquiryServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/ConvenientInquiryServiceImpl.java @@ -5,6 +5,7 @@ import com.ai.da.common.constant.CommonConstant; import com.ai.da.common.context.UserContext; import com.ai.da.common.enums.CreditsEventsEnum; import com.ai.da.common.response.PageBaseResponse; +import com.ai.da.common.response.TransactionPageResponse; import com.ai.da.common.response.ResultEnum; import com.ai.da.common.utils.CopyUtil; import com.ai.da.common.utils.DateUtil; @@ -695,16 +696,21 @@ public class ConvenientInquiryServiceImpl extends ServiceImpl response = new PageBaseResponse<>(); + TransactionPageResponse response = new TransactionPageResponse<>(); response.setContent(paymentInfoVOS); response.setPage(queryPaymentInfoDTO.getPage()); response.setSize(size); response.setTotal(total); response.setPages((long) totalPage); + response.setTotalAmount(Objects.isNull(payerTotal) ? BigDecimal.ZERO : payerTotal); return response; } diff --git a/src/main/java/com/ai/da/service/impl/DesignItemServiceImpl.java b/src/main/java/com/ai/da/service/impl/DesignItemServiceImpl.java index 626b88b5..d1eae84d 100644 --- a/src/main/java/com/ai/da/service/impl/DesignItemServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/DesignItemServiceImpl.java @@ -336,7 +336,7 @@ public class DesignItemServiceImpl extends ServiceImpl saveDesignSingleItemDetailAndLayers(DesignPythonObjects pythonObjects , Long designId, Long designItemId, Long userId , JSONObject outfit, String timeZone, List designSingleItemDTOList - , Map categoryAndUndividedLayer + , Map priorityAndUndividedLayer , boolean changeModelFlag , Long modelId, String modelType, boolean isSingleCollectionFlag) { @@ -374,7 +374,14 @@ public class DesignItemServiceImpl extends ServiceImpl categoryAndUndividedLayer = setTypeAndUndividedLayer(layers); + Map priorityAndUndividedLayer = setPriorityAndUndividedLayer(layers); if (!designSingleIncludeLayersDTO.getIsPreview()) { // 更新及保存图层信息 tDesignPythonOutfitDetails = saveDesignSingleItemDetailAndLayers(objects, design.getId(), designSingleIncludeLayersDTO.getDesignItemId() , userId, outfit, designSingleIncludeLayersDTO.getTimeZone() , designSingleIncludeLayersDTO.getDesignSingleItemDTOList() - , categoryAndUndividedLayer, changeModelFlag, modelId, modelType, isSingleCollectionFlag); + , priorityAndUndividedLayer, changeModelFlag, modelId, modelType, isSingleCollectionFlag); saveCollectionElement(designSingleIncludeLayersDTO); } else { @@ -627,13 +634,14 @@ public class DesignItemServiceImpl extends ServiceImpl setPriorityAndUndividedLayer(JSONArray layers){ + HashMap priorityAndLayer = new HashMap<>(); + for (int i = 0; i < layers.size(); i++) { + JSONObject jsonObject = layers.getJSONObject(i); + String priority = jsonObject.getString("priority"); + String category = jsonObject.getString("image_category").split("_")[0]; + if (!category.equals("body") && !priorityAndLayer.containsKey(priority)) priorityAndLayer.put(priority, jsonObject.getString("pattern_image_url")); + } + return priorityAndLayer; + } + + // 由于在design过程中没有priority 优先级的概念,并且在design时,不会出现上下两件使用相同服装类型的情况,所以这里依然保留这个方法。 @Override public Map setTypeAndUndividedLayer(JSONArray layers){ - HashMap categoryAndLayer = new HashMap<>(); + HashMap typeAndLayer = new HashMap<>(); for (int i = 0; i < layers.size(); i++) { JSONObject jsonObject = layers.getJSONObject(i); String category = jsonObject.getString("image_category").split("_")[0]; - if (!category.equals("body") && !categoryAndLayer.containsKey(category)) categoryAndLayer.put(category, jsonObject.getString("pattern_image_url")); + if (!category.equals("body") && !typeAndLayer.containsKey(category)) typeAndLayer.put(category, jsonObject.getString("pattern_image_url")); } - return categoryAndLayer; + return typeAndLayer; } @Override @@ -827,7 +848,7 @@ public class DesignItemServiceImpl extends ServiceImpl designSingleItemDTOList, List layersObject, String singleOrOverall, - Map categoryAndUndividedLayer) { + Map priorityAndUndividedLayer) { DesignSingleVO designSingleVO = new DesignSingleVO(); ArrayList clothes = new ArrayList<>(); @@ -865,7 +886,8 @@ public class DesignItemServiceImpl extends ServiceImpl layers.getImageCategory().equals("body")).collect(Collectors.toList())); clothes.add(designItemClothesDetailVO); diff --git a/src/main/java/com/ai/da/service/impl/DesignServiceImpl.java b/src/main/java/com/ai/da/service/impl/DesignServiceImpl.java index 1fd4e3e6..1d4f3a00 100644 --- a/src/main/java/com/ai/da/service/impl/DesignServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/DesignServiceImpl.java @@ -838,6 +838,7 @@ public class DesignServiceImpl extends ServiceImpl impleme d -> Math.abs(d.getPriority()), (existing, replacement) -> replacement)); Map typeAndUndividedLayer = designItemService.setTypeAndUndividedLayer(layers); + log.info("all typeLayers Map:{}", typeAndUndividedLayer); for (DesignPythonItem detail : item.getItems()) { if (null == detail) { continue; @@ -848,7 +849,11 @@ public class DesignServiceImpl extends ServiceImpl impleme designItemDetail.setDesignItemId(designItemId); designItemDetail.setCollectionElementId(detail.getElementId()); designItemDetail.setCreateDate(DateUtil.getByTimeZone(timeZone)); - designItemDetail.setUndividedLayer(typeAndUndividedLayer.get(designItemDetail.getType().toLowerCase())); + log.info("detail.getType():{}", detail.getType()); + if (!detail.getType().equals("Body")){ + log.info("layer : {}", typeAndUndividedLayer.get(designItemDetail.getType())); + designItemDetail.setUndividedLayer(typeAndUndividedLayer.get(designItemDetail.getType())); + } if (SysFileLevel2TypeEnum.BODY.getRealName().equals(detail.getType())) { designItemDetail.setPath(detail.getBody_path()); //BODY不关联businessId @@ -979,7 +984,10 @@ public class DesignServiceImpl extends ServiceImpl impleme designItemDetail.setDesignItemId(designItemId); designItemDetail.setCollectionElementId(detail.getElementId()); designItemDetail.setCreateDate(DateUtil.getByTimeZone(timeZone)); - designItemDetail.setUndividedLayer(typeAndUndividedLayer.get(designItemDetail.getType().toLowerCase())); + if (!detail.getType().equals("Body")){ + designItemDetail.setUndividedLayer(typeAndUndividedLayer.get(designItemDetail.getType())); + } + if (SysFileLevel2TypeEnum.BODY.getRealName().equals(detail.getType())) { designItemDetail.setPath(detail.getBody_path()); //BODY不关联businessId @@ -2633,7 +2641,9 @@ public class DesignServiceImpl extends ServiceImpl impleme designItemDetail.setDesignItemId(designItemId); designItemDetail.setCollectionElementId(detail.getElementId()); designItemDetail.setCreateDate(DateUtil.getByTimeZone(timeZone)); - designItemDetail.setUndividedLayer(typeAndUndividedLayer.get(designItemDetail.getType().toLowerCase())); + if (!detail.getType().equals("Body")){ + designItemDetail.setUndividedLayer(typeAndUndividedLayer.get(designItemDetail.getType())); + } if (SysFileLevel2TypeEnum.BODY.getRealName().equals(detail.getType())) { designItemDetail.setPath(detail.getBody_path()); //BODY不关联businessId diff --git a/src/main/java/com/ai/da/service/impl/GenerateServiceImpl.java b/src/main/java/com/ai/da/service/impl/GenerateServiceImpl.java index caa7dfcd..19ff10bb 100644 --- a/src/main/java/com/ai/da/service/impl/GenerateServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/GenerateServiceImpl.java @@ -184,8 +184,9 @@ public class GenerateServiceImpl extends ServiceImpl i public void generateThroughImageText(GenerateThroughImageTextDTO generateThroughImageTextDTO) { // 1、获取用户信息 Long accountId = generateThroughImageTextDTO.getUserId(); - String generateType = generateThroughImageTextDTO.getGenerateType(); + GenerateModeEnum modeEnum = getMode(generateThroughImageTextDTO); + String generateType = modeEnum.getValue(); // 2、判断必须入参是否为非空(在prepare阶段已校验) Generate generate = new Generate(); generate.setAccountId(accountId); @@ -214,9 +215,7 @@ public class GenerateServiceImpl extends ServiceImpl i CollectionElement collectionElement = collectionElementService.editLevel2Type(elementId, generateThroughImageTextDTO.getLevel2Type(), generateThroughImageTextDTO.getDesignType()); // 3、向模型发起请求 - String mode = GenerateModeEnum.TEXT.getValue().equals(generateType) ? - GenerateModeEnum.TEXT.getType() : - GenerateModeEnum.TEXT_IMAGE.getType(); + String mode = modeEnum.getType(); String category = generateThroughImageTextDTO.getLevel1Type().equals(SKETCH_BOARD.getRealName()) ? "sketch" : generateThroughImageTextDTO.getLevel1Type().equals(PRINT_BOARD.getRealName()) ? "print" : "moodboard"; String path = CommonConstant.GENERATE_PATH; @@ -261,7 +260,6 @@ public class GenerateServiceImpl extends ServiceImpl i jsonString = JSON.toJSONString(generateToPythonDTO, SerializerFeature.WriteMapNullValue); } - Boolean requestResult = pythonService.generateSketchOrPrint(jsonString, port, path); // 4、将请求信息落库,将本次generate的请求信息添加到t_generate表中 @@ -280,6 +278,21 @@ public class GenerateServiceImpl extends ServiceImpl i } + public GenerateModeEnum getMode(GenerateThroughImageTextDTO generateThroughImageTextDTO){ + if (!StringUtil.isNullOrEmpty(generateThroughImageTextDTO.getText())){ + if (Objects.nonNull(generateThroughImageTextDTO.getCollectionElementId())){ + return GenerateModeEnum.TEXT_IMAGE; + }else { + return GenerateModeEnum.TEXT; + } + }else { + if (Objects.nonNull(generateThroughImageTextDTO.getCollectionElementId())){ + return GenerateModeEnum.IMAGE; + } + } + return GenerateModeEnum.TEXT; + } + @Override @Transactional(rollbackFor = Exception.class) public void processGenerateResult(String taskId, String url, String category) { @@ -410,6 +423,10 @@ public class GenerateServiceImpl extends ServiceImpl i prefix = userInput.substring(0, userInput.indexOf(",")) + ", "; userInput = userInput.substring(userInput.indexOf(",") + 1); } + // 替换用户输入中的中文字符 + log.info("用户输入,处理前:{}", userInput); + userInput = ComprehensivePunctuationConverter.convertToHalfWidth(userInput); + log.info("用户输入,处理后:{}", userInput); String translated = prefix + pythonService.promptTranslate(userInput); switch (level1Type) { case "Moodboard": @@ -1026,7 +1043,7 @@ public class GenerateServiceImpl extends ServiceImpl i // todo 取消待优化 uniqueIdList.forEach(uniqueId -> { // 1、将需要取消的唯一id加入redis,以便及时取消生成 - redisUtil.addToSet(cancelSetKey, uniqueId); + redisUtil.addToSet(cancelSetKey, uniqueId, CommonConstant.REDIS_SET_EXPIRE_TIME); /*// 1、确认当前消息是否还在排队中 Boolean exists = redisUtil.isElementExistsInZSet(consumptionOrderKey, uniqueId); @@ -1354,12 +1371,12 @@ public class GenerateServiceImpl extends ServiceImpl i @Override @Transactional(rollbackFor = Exception.class) public GenerateResultVO modifySketch(GenerateModifyDTO generateModifyDTO) { - log.info("修改生成或library中的sketch或print"); + log.info("修改生成或library中的sketch或print,并加入到library"); // 提取常用参数 Long accountId = UserContext.getUserHolder().getId(); String base64 = generateModifyDTO.getBase64(); - String gender = generateModifyDTO.getGender(); + String gender = generateModifyDTO.getGender().toLowerCase(); String category = generateModifyDTO.getCategory(); Long originalId = generateModifyDTO.getOriginalId(); String originalIdSource = generateModifyDTO.getOriginalIdSource(); @@ -3000,6 +3017,4 @@ public class GenerateServiceImpl extends ServiceImpl i return null; } } - - } 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 355e29ac..9442d076 100644 --- a/src/main/java/com/ai/da/service/impl/PaymentInfoServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/PaymentInfoServiceImpl.java @@ -310,6 +310,7 @@ public class PaymentInfoServiceImpl extends ServiceImpl qw = new QueryWrapper<>(); + // todo 首次支付失败,没有invoiceId,所以如果这个order之后成功支付后,会有多条paymentInfo 是否需要优化?? qw.eq("transaction_id", charge.getInvoice()); PaymentInfo paymentInfo = baseMapper.selectOne(qw); Charge.PaymentMethodDetails paymentMethodDetails = charge.getPaymentMethodDetails(); 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 fc725f61..629ddb3e 100644 --- a/src/main/java/com/ai/da/service/impl/StripeServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/StripeServiceImpl.java @@ -1,1650 +1,1694 @@ -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.*; -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.ProductCouponsMapper; -import com.ai.da.mapper.primary.SubscriptionInfoMapper; -import com.ai.da.mapper.primary.entity.*; -import com.ai.da.model.dto.CreateCouponDTO; -import com.ai.da.model.dto.ProductPurchaseDTO; -import com.ai.da.model.dto.QueryCouponsPageDTO; -import com.ai.da.model.dto.SubscriptionEmailParamsDTO; -import com.ai.da.model.vo.CheckCouponsVO; -import com.ai.da.service.*; -import com.alibaba.fastjson.JSON; -import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; -import com.baomidou.mybatisplus.core.metadata.IPage; -import com.baomidou.mybatisplus.core.toolkit.StringUtils; -import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.google.gson.Gson; -import com.stripe.Stripe; -import com.stripe.exception.InvalidRequestException; -import com.stripe.exception.SignatureVerificationException; -import com.stripe.exception.StripeException; -import com.stripe.model.*; -import com.stripe.model.Product; -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; -import org.springframework.transaction.annotation.Transactional; - -import javax.annotation.Resource; -import javax.servlet.http.HttpServletRequest; -import java.math.BigDecimal; -import java.math.RoundingMode; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneOffset; -import java.time.ZonedDateTime; -import java.util.*; -import java.util.stream.Collectors; - -@SuppressWarnings("LoggingSimilarMessage") -@Service -@Slf4j -public class StripeServiceImpl implements StripeService { - - @Resource - private OrderInfoService orderInfoService; - @Resource - private PayPalCheckoutService payPalCheckoutService; - @Resource - private PaymentInfoService paymentInfoService; - @Resource - private CreditsService creditsService; - @Resource - private RefundInfoService refundInfoService; - @Resource - private AccountService accountService; - - @Resource - private AccountMapper accountMapper; - @Resource - private SubscriptionInfoMapper subscriptionInfoMapper; - @Resource - private PaymentInfoMapper paymentInfoMapper; - @Resource - private ProductCouponsMapper productCouponsMapper; - - @Value("${stripe.private-key}") - private String privateKey; - - @Value("${stripe.webhook-sign-secret}") - private String signSecret; - - @Value("${orderList.link}") - private String orderListLink; - - @Value("${stripe.paymentMethodConfiguration}") - private String paymentMethodConfigurationId; - - @Override - @Transactional(rollbackFor = Exception.class) - public String pay(ProductPurchaseDTO productPurchaseDTO, HttpServletRequest request) { - Stripe.apiKey = privateKey; - - //创建支付信息得到url - // 一次性支付和周期扣款,需要区分mode: payment || subscription || setup - SessionCreateParams.Builder sessionBuilder = new SessionCreateParams.Builder(); - 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 "EcoMonth": - productEnum = ProductEnum.Eco_MonthlySubscription; - productPurchaseDTO.setSubscribeType("Month"); - break; - case "Year": - productEnum = ProductEnum.AnnualSubscription; - break; - case "Day": - productEnum = ProductEnum.DailySubscription; - break; - default: - throw new BusinessException("unknown subscription type"); - } - - // 添加优惠券(只允许在订阅时使用优惠券) - String promotionCode = productPurchaseDTO.getPromotionCode(); - if (!StringUtil.isNullOrEmpty(promotionCode)){ - ProductCoupons productCoupon = checkProductCoupon(promotionCode);; - if (productCoupon != null){ - sessionBuilder.addDiscount(SessionCreateParams.Discount.builder() - .setPromotionCode(productCoupon.getPromotionCodeId()).build()); - } - } - // 只有订阅时才允许使用推广码优惠 -// sessionBuilder.setAllowPromotionCodes(true); - break; - default: - throw new BusinessException("unknown product type"); - } - log.info("生成订单"); - String payType; - byte autoRenewal; - if (productPurchaseDTO.getAutoRenewal()){ - payType = "recurring"; - autoRenewal = 1; - }else { - payType = "one_time"; - autoRenewal = 0; - } - OrderInfo orderInfo = orderInfoService.createOrderByProductId(productPurchaseDTO.getQuantity(), - PayTypeEnum.STRIPE.getType(), productEnum, request, autoRenewal); - - try { - 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()); - log.info("customerId:{}", customerId); - // 获取自定义订单号 - String orderId = orderInfo.getOrderNo(); - - - 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.setPaymentMethodConfiguration(paymentMethodConfigurationId); - 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()); - List paymentMethodTypes = session.getPaymentMethodTypes(); - log.info("paymentMethodTypes: {}", paymentMethodTypes); - - Session.PaymentMethodConfigurationDetails paymentMethodConfigurationDetails = session.getPaymentMethodConfigurationDetails(); - log.info("paymentMethodConfigurationDetails ID: {}", paymentMethodConfigurationDetails.getId()); - log.info("sessionId:{}", session.getId()); //退款方式1:拿到sessionId入库,退款的时候根据这个id找到PaymentIntent的id然后发起退款 - - // 更新order信息 - orderInfoService.updateOrderNoById(orderInfo.getId(), orderId); - return session.getUrl(); - } catch (BusinessException e) { - throw e; - } catch (InvalidRequestException e) { - log.info("创建会话出现异常:", e); - throw new BusinessException(e.getMessage().substring(0, e.getMessage().indexOf(";"))); - } catch (Exception e) { - log.error("创建支付会话出现异常:", e); - } - 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()); - } - - @Resource - private EmailService emailService; - @Override - @Transactional(rollbackFor = Exception.class) - public Boolean notify(HttpServletRequest request) { - log.info("stripe异步通知进行中"); - String payload = null; - String sigHeader = null; - String endpointSecret = signSecret; - try { - sigHeader = request.getHeader("Stripe-Signature"); - payload = payPalCheckoutService.getBody(request); - } catch (Exception e) { - log.info("stripe 支付回调参数解析异常:errorMsg {}", e.getMessage()); - log.info("request sigHeader = {}", sigHeader); - log.info("request body = {}", JSON.toJSONString(payload)); - e.printStackTrace(); - return Boolean.FALSE; - } - - Event event; - try { - assert sigHeader != null; - event = Webhook.constructEvent(payload, sigHeader, endpointSecret); - } catch (SignatureVerificationException e) { - log.info("stripe 验签,获取事件异常, errorMsg={}", e.getMessage()); - log.info("request sigHeader = {}", sigHeader); - log.info("request body = {}", JSON.toJSONString(payload)); - e.printStackTrace(); - return Boolean.FALSE; - } - - //获取自定义参数 - // Deserialize the nested object inside the event - assert event != null; - EventDataObjectDeserializer dataObjectDeserializer = event.getDataObjectDeserializer(); - StripeObject stripeObject ; - if (dataObjectDeserializer.getObject().isPresent()) { - stripeObject = dataObjectDeserializer.getObject().get(); - } else { - log.info("stripe 验签失败!"); - log.info("request sigHeader = {}", sigHeader); - log.info("request body = {}", JSON.toJSONString(payload)); - return Boolean.FALSE; - } - log.info("stripe验签成功"); - boolean response = Boolean.TRUE; - - log.info("回调事件 {}", event.getType()); - if (stripeObject instanceof Session){ - Session session = (Session) stripeObject; - if (event.getType().equals("checkout.session.completed")) { - response = processOrder(session); - }else if (event.getType().equals("checkout.session.expired")){ - String orderNo = session.getMetadata().get("orderId"); - // 会话过期 未支付 且之后没有支付成功的订单 - response = 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, null); - } - // 续订支付失败,邮件通知用户 - if (subscription.getStatus().equals("past_due")){ - // 发送续订失败邮件 - response = sendRenewalFailEmail(null, subscription.getId(), subscriptionInfo.getOrderNo()); - - } - } 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", null); - if (response){ - subscriptionInfo.setCancelNotified((byte)1); - subscriptionInfoMapper.updateById(subscriptionInfo); - // 更新订单信息 - OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(subscriptionInfo.getOrderNo()); - orderInfo.setAutoRenewal((byte)0); - } - } - - }/* 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()); - // 邮件通知商家和用户 - String billingReason = invoice.getBillingReason(); - switch (billingReason) { - case "subscription_create": - response = sendEmail(invoice.getSubscription(), "new", null); - break; - case "subscription_cycle": - response = sendEmail(invoice.getSubscription(), "renewal", null); - break; - case "manual": - boolean b = invoice.getLines().getData().get(0).getDescription().endsWith("Subscription"); - if (b) { - // 非自动续订式订阅,Stripe不会创建Subscription,所以invoice中不会有subscriptionId - response = sendEmail(null, "new", paymentInfo.getOrderNo()); - } - break; - } - } - } 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 { - // 新增支付信息 - PaymentInfo paymentInfoFail = paymentInfoService.createOrUpdatePaymentInfoForStripe(invoice); - // 发送新订阅失败邮件 - response = sendEmail(paymentInfoFail.getOrderNo()); - } - } - }else if (stripeObject instanceof Charge) { - Charge charge = (Charge) stripeObject; - String orderNo = charge.getDescription().replace("AiDA - ", ""); - OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo); - if (Objects.isNull(orderInfo)){ - // 说明该回调不是从AiDA订阅获得 - return true; - } - 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); - }else if (event.getType().equals("charge.refunded")){ - // 更新退款信息 - RefundInfo refundInfo = refundInfoService.updateRefundForStripe(charge); - // 更新 t_payment_info的支付状态 - if (Objects.nonNull(refundInfo)){ - paymentInfoService.updatePaymentRefundStatus(charge); - } - } - }else if (stripeObject instanceof Refund){ - Refund refund = (Refund) stripeObject; - if (event.getType().equals("refund.created")){ - // 新增退款信息 - refundInfoService.createRefundForStripe(refund); - }else if (event.getType().equals("refund.updated")){ - // 根据***id更新退款记录信息 - RefundInfo refundInfo = refundInfoService.updateRefundStatusForStripe(refund); - if (Objects.isNull(refundInfo)){ - // 等事件先创建,再更新。回调事件的顺序随机 - response = false; - } - } - } - log.info("回调事件 {} 处理完成", event.getType()); - return response; - } - - public boolean processOrder(Session session) { - Stripe.apiKey = privateKey; - String orderNo = session.getMetadata().get("orderId"); - float totalAmount = new BigDecimal(session.getAmountTotal()).divide(new BigDecimal(100), 2, RoundingMode.HALF_UP).floatValue(); - - boolean resp = true; - try { - //处理重复通知 - //接口调用的幂等性:无论接口被调用多少次,以下业务执行一次 -// String orderStatus = orderInfoService.getOrderStatus(orderNo); - OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(orderNo); - String orderStatus = orderByOrderNo.getOrderStatus(); - // 当订单状态处于未支付或超时已关闭时,更新订单状态,其他状态均不更新订单状态 - if (!OrderStatusEnum.NOT_PAY.getType().equals(orderStatus) && !OrderStatusEnum.TIMEOUT_CLOSED.getType().equals(orderStatus)) { - log.info("订单状态 : {}", orderStatus); - }else { - //更新订单状态 - orderInfoService.updateStatusByOrderNo(orderNo, OrderStatusEnum.SUCCESS); - log.info("Stripe 订单:{} 状态更新成功", orderNo); - } - - if (orderByOrderNo.getTitle().startsWith("积分购买")){ - // 查询当前订单的积分是否已添加 - CreditsDetail creditsDetail = creditsService.queryDetailByTaskId(orderNo); - if (Objects.isNull(creditsDetail)){ - 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", orderNo); - log.info("用户:{} 积分信息更新成功", orderByOrderNo.getAccountId()); - } - }else if (orderByOrderNo.getTitle().endsWith("Subscription") && orderByOrderNo.getAutoRenewal() == (byte)0){ - String invoiceId = session.getInvoice(); - Invoice invoice = Invoice.retrieve(invoiceId); - InvoiceLineItem invoiceLineItem = invoice.getLines().getData().get(0); - String description = invoiceLineItem.getDescription(); - Long amount = invoiceLineItem.getAmount(); - log.info("单次订阅 description : {}, amount: {} 分", description, amount); - boolean b = createSubscriptionAndUpdateAccount(orderNo, orderByOrderNo.getAccountId(), description, amount); - // 邮件通知用户 - if (b){ - resp = sendEmail(null, "new", orderNo); - } - log.info("单次订阅订单:{} 处理完成", orderNo); - } - } catch (Exception e) { - log.info(e.getMessage()); - resp = false; - } - return resp; - } - - private boolean processExpiredOrder(String orderNo) { - // 支付失败 通知商家的条件 1、会话过期 2、支付失败 3、这个用户在这个支付失败后再无支付成功的订阅 - // 1、获取当前订单的支付状态 -// String orderNo = session.getMetadata().get("orderId"); - OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(orderNo); - // 2、确认订单状态为支付失败 - boolean resp = true; - if (!Objects.isNull(orderByOrderNo) && 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")) { - resp = sendEmail(orderNo); - }else { - // todo 续订失败 应该不会走这里 - resp = sendEmail(subscriptionInfo.getSubscriptionId(), "fail_renewal", null); - } - } - } - return resp; - - } - - @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()); - - int rows = subscriptionInfoMapper.insertIgnore(subscriptionInfo); - log.info("Subscription info insert affect rows : {}", rows); - - if (subscriptionInfo.getStatus().equals("active")){ - log.info("创建订阅更新账号信息"); - // 更新账号到期时间 - boolean b = accountService.updateAccountValidity(subscriptionInfo.getAccountId(), subscriptionInfo.getCurrentPeriodEnd()); - // 更新账号身份和积分 - if (b) accountService.updateUserRoleAndCredits(subscriptionInfo.getAccountId(), orderNo); - } - } - return subscriptionInfo; - } - - /** - * 非自动续订订阅 - * Stripe不会自动创建Subscription,所以没有subscription相关的回调,无法触发订阅相关的处理代码 - */ - public boolean createSubscriptionAndUpdateAccount(String orderNo, Long accountId, String description, Long amount){ - QueryWrapper qw = new QueryWrapper<>(); - qw.eq("order_no", orderNo); - SubscriptionInfo subscriptionInfo = subscriptionInfoMapper.selectOne(qw); - if (Objects.isNull(subscriptionInfo)) { - String interval; - // 获取当前时间戳(秒级) - long currentPeriodStart = Instant.now().getEpochSecond();; - long currentPeriodEnd; -// InvoiceLineItem invoiceLineItem = invoice.getLines().getData().get(0); - if (description.equals(ProductEnum.DailySubscription.getName()) - && amount.equals(ProductEnum.DailySubscription.getPrice() * 100)){ - interval = "day"; - // 获取一天后的时间戳(秒级) - ZonedDateTime now = ZonedDateTime.now(); - currentPeriodEnd = now.plusDays(1).toEpochSecond(); - }else if (description.equals(ProductEnum.MonthlySubscription.getName()) - && amount.equals(ProductEnum.MonthlySubscription.getPrice() * 100)){ - interval = "month"; - // 获取一天后的时间戳(秒级) - ZonedDateTime now = ZonedDateTime.now(); - currentPeriodEnd = now.plusMonths(1).toEpochSecond(); - } else if (description.equals(ProductEnum.Eco_MonthlySubscription.getName()) - && amount.equals(ProductEnum.Eco_MonthlySubscription.getPrice() * 100)){ - interval = "month"; - // 获取一天后的时间戳(秒级) - ZonedDateTime now = ZonedDateTime.now(); - currentPeriodEnd = now.plusMonths(1).toEpochSecond(); - } else if (description.equals(ProductEnum.AnnualSubscription.getName()) - && amount.equals(ProductEnum.AnnualSubscription.getPrice() * 100)){ - interval = "year"; - // 获取一天后的时间戳(秒级) - ZonedDateTime now = ZonedDateTime.now(); - currentPeriodEnd = now.plusYears(1).toEpochSecond(); - }else { - log.error("未知订阅类型"); - return false; - } - subscriptionInfo = new SubscriptionInfo(); - subscriptionInfo.setAccountId(accountId); - subscriptionInfo.setOrderNo(orderNo); - subscriptionInfo.setType(interval); - subscriptionInfo.setStatus("canceled"); - subscriptionInfo.setCurrentPeriodStart(currentPeriodStart); - subscriptionInfo.setCurrentPeriodEnd(currentPeriodEnd); - subscriptionInfo.setCreateTime(LocalDateTime.now()); - - subscriptionInfoMapper.insertIgnore(subscriptionInfo); - - log.info("创建订阅, 更新账号信息"); - // 更新账号到期时间 - boolean b = accountService.updateAccountValidity(subscriptionInfo.getAccountId(), subscriptionInfo.getCurrentPeriodEnd()); - // 更新账号身份和积分 - if (b) accountService.updateUserRoleAndCredits(subscriptionInfo.getAccountId(), orderNo); - return true; - } - return true; - } - - 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).orderByDesc("id"); - 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)); - log.info("更新订阅更新账号信息"); - // 更新账号到期时间 - accountService.updateAccountValidity(subscriptionInfo.getAccountId(), subscriptionInfo.getCurrentPeriodEnd()); - // 更新账号身份和积分 - accountService.updateUserRoleAndCredits(subscriptionInfo.getAccountId(), subscriptionInfo.getOrderNo()); - log.info("更新 {} 账号到期时间为:{}", subscriptionInfo.getAccountId(), DateUtil.changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy_EEEE)); - flag = true; - } - if (subscriptionInfo.getStatus().equals("active")){ - // 更新账号到期时间 - boolean b = accountService.updateAccountValidity(subscriptionInfo.getAccountId(), subscriptionInfo.getCurrentPeriodEnd()); - // 更新账号身份和积分 - if (b) accountService.updateUserRoleAndCredits(subscriptionInfo.getAccountId(), subscriptionInfo.getOrderNo()); - } - if (flag){ - subscriptionInfo.setUpdateTime(LocalDateTime.now()); - subscriptionInfoMapper.updateById(subscriptionInfo); - } - return subscriptionInfo; - } - - // 取消连续订阅 将订阅从pause状态转为cancel状态(使用定时器,定期检索DB中,过期且不续订的订阅) - public void cancelSubscription(String subscriptionId, String cancelReason) { - Stripe.apiKey = privateKey; - log.info("cancel subscription"); - Long accountId = UserContext.getUserHolder().getId(); - 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(); - log.info("用户 {} 申请取消连续订阅 {}", accountId, subscriptionId); - // 更新数据库 - 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.getPaymentInfoByOrderNo(orderNo, "DESC").get(0).getTransactionId(); - - if (StringUtils.isNotEmpty(sessionId)) { //根据会话编号退款 - Session session = Session.retrieve(sessionId); - RefundCreateParams params; - if (amount != null && !amount.equals("0")) { //指定退款金额 - BigDecimal actualAmount = new BigDecimal(amount).multiply(BigDecimal.valueOf(100)); //api默认单位分 - params = RefundCreateParams.builder() - .setPaymentIntent(session.getPaymentIntent()) - .setAmount(actualAmount.longValue()) - .build(); - } else { //全额退款 - params = RefundCreateParams.builder() - .setPaymentIntent(session.getPaymentIntent()) - .build(); - } - refund = Refund.create(params); - log.info("根据会话编号退款成功"); - - } else { - log.error("当前订单不存在"); - return "退款异常"; - } - } catch (Exception e) { - //e.getMessage.contain("charge_already_refunded") 已退款 - //e.getMessage.contain("resource_missing") 退款编号错误 - //e.getMessage.contain("amount on charge ($n)") 金额应小于n - log.error("退款异常:", e); - return "退款异常"; - } - - if ("succeeded".equals(refund.getStatus())) { - //进行数据库操作,修改状态为已退款(配合回调和退款查询确定退款成功) - //更新订单状态 - orderInfoService.updateStatusByOrderNo(orderNo, OrderStatusEnum.REFUND_SUCCESS); - - refundInfoService.updateRefundForPayPal( - refundByOrderNo.getId(), - refund.getId(), - new Gson().toJson(refund), - AliPayTradeStateEnum.REFUND_SUCCESS.getType()); //退款成功 - - // 更新积分状态 - OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(orderNo); - creditsService.creditsRefund(orderByOrderNo.getAccountId(), (int) (orderByOrderNo.getTotalFee() / Float.parseFloat(CreditsEventsEnum.PRICE.getValue())), orderNo); - } else { - //更新订单状态 - orderInfoService.updateStatusByOrderNo(orderNo, OrderStatusEnum.REFUND_ABNORMAL); - - //更新退款单 - refundInfoService.updateRefundForPayPal( - refundByOrderNo.getId(), - refund.getId(), - new Gson().toJson(refund), - AliPayTradeStateEnum.REFUND_ERROR.getType()); //退款失败 - } - log.info("记录退款订单"); - return "退款成功"; - } - - public void checkOrderStatus(String orderNo) { - Stripe.apiKey = privateKey; - // 1、通过orderNo 查询sessionId - // todo transactionId不再是sessionId而是invoiceId,所以这里需要更新 - PaymentInfo paymentInfo = paymentInfoService.getPaymentInfoByOrderNo(orderNo, "DESC").get(0); - try { - Session session = Session.retrieve(paymentInfo.getTransactionId()); - if (Objects.isNull(session)) { - log.warn("核实订单未创建 ===> {}", orderNo); - return; - } else if (session.getStatus().equals("open") || session.getStatus().equals("expired")) { - // 订单未支付 || 订单过期 ---> 均设置为超时未支付 - log.info("订单超时未支付 ===> {}", orderNo); - //更新本地订单状态 - orderInfoService.updateStatusByOrderNo(orderNo, OrderStatusEnum.TIMEOUT_CLOSED); - paymentInfoService.updatePaymentStatusById(paymentInfo.getId(), - session.getStatus(), - new Gson().toJson(session)); - } else if (session.getStatus().equals("complete")) { - // 订单已完成 - processOrder(session); - } - } catch (StripeException e) { - log.error("根据sessionId获取Stripe Session失败"); - throw new RuntimeException(e); - } - - } - - 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).setLimit(20L) - .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(name) - .setEmail(userEmail) - .build(); - Customer customer = Customer.create(params); - - return customer.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 Map getPaymentMethod(String paymentMethodId){ - Stripe.apiKey = privateKey; - String paymentMethod = null; - String last4 = null; - - 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 boolean sendEmail(String subscriptionId, String type, String orderNo) { - SubscriptionInfo subscriptionInfo; - QueryWrapper qwSI = new QueryWrapper<>(); - if (!StringUtil.isNullOrEmpty(subscriptionId)) { - qwSI.eq("subscription_id", subscriptionId); - List subscriptionInfoList = subscriptionInfoMapper.selectList(qwSI); - - if (subscriptionInfoList.isEmpty()){ - log.info("不发送邮件,原因:【subscriptionInfoList 为空】"); - return false; - }else { - List activeSubscriptions = subscriptionInfoList.stream() - .filter(subscription -> "active".equals(subscription.getStatus())) - .collect(Collectors.toList()); - if (!StringUtil.isNullOrEmpty(type) && type.equals("cancel")){ - subscriptionInfo = subscriptionInfoList.get(0); - }else if (activeSubscriptions.isEmpty()){ - log.info("不发送邮件,原因:【当前邮件类型:{}, 但是状态为active的subscriptionInfo为空】", type); - return false; - }else { - subscriptionInfo = activeSubscriptions.get(0); - } - } - }else if (!StringUtil.isNullOrEmpty(orderNo)) { - qwSI.eq("order_no", orderNo); - subscriptionInfo = subscriptionInfoMapper.selectOne(qwSI); - if (Objects.isNull(subscriptionInfo)){ - log.info("不发送邮件,原因:【根据order_no:{},查询到的subscriptionInfo为空】", orderNo); - return false; - } - }else { - log.info("不发送邮件,原因:【入参中的subscriptionId,orderNo均为空】"); - return false; - } - - QueryWrapper qwPI = new QueryWrapper<>(); - qwPI.eq("order_no", subscriptionInfo.getOrderNo()).orderByDesc("id"); - List paymentInfos = paymentInfoMapper.selectList(qwPI); - if (paymentInfos.isEmpty()) { - log.info("不发送邮件,原因:【根据order_no:{},查询到的paymentInfos为空】", orderNo); - return false; - } - PaymentInfo paymentInfo = paymentInfos.get(0); - 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){ - // 已经邮件通知过,直接返回 - log.info("不发送邮件,原因:【type为:{},order_no为:{},已经进行邮件通知】", type, orderNo); - return true; - } - - 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.setOrderRef("\"" + orderListLink + 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, language); - - boolean b = SendEmailUtil.subscriptionEmailReminder(type, emailParamsDTO, language, account.getUserEmail()); -// boolean b = emailService.subscriptionEmailReminder(type, emailParamsDTO, language, account.getUserEmail()); - if (!b) return false; - - // 邮件通知成功后,更新标志 - 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 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()) { - log.info("不发送邮件,原因:【根据order_no:{},查询到的paymentInfos为空】", orderNo); - 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()); - - boolean b = SendEmailUtil.subscriptionEmailReminder("fail_new", emailParamsDTO, language, account.getUserEmail()); -// boolean b = emailService.subscriptionEmailReminder("fail_new", emailParamsDTO, language, account.getUserEmail()); - if (!b) return false; - - // 邮件通知成功后,更新标志 - 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, language); - - // 4、发邮件 - boolean b = SendEmailUtil.subscriptionEmailReminder("fail_renewal", emailParamsDTO, language, account.getUserEmail()); -// boolean b = emailService.subscriptionEmailReminder("fail_renewal", emailParamsDTO, language, account.getUserEmail()); - if (!b) return false; - - 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, String language) { - emailParamsDTO.setPaymentMethod(paymentInfo.getPaymentMethod()); - emailParamsDTO.setLast4(paymentInfo.getLast4()); - emailParamsDTO.setSubscriptionId(subscriptionInfo.getId().toString()); - emailParamsDTO.setFailMessage(orderByOrderNo.getNote()); - emailParamsDTO.setSubscriptionType(subscriptionInfo.getType()); - emailParamsDTO.setStartDate(DateUtil.changeTimeStampFormat(orderByOrderNo.getCreateTime())); - if (subscriptionInfo.getType().equals("month")){ - emailParamsDTO.setRenewalFee(String.valueOf(500)); - } else if (subscriptionInfo.getType().equals("year")){ - emailParamsDTO.setRenewalFee(String.valueOf(5000)); - } else { - emailParamsDTO.setRenewalFee(emailParamsDTO.getTotalFee()); - } - if (subscriptionInfo.getStatus().equals("active")){ - if (language.equals("ENGLISH")){ - emailParamsDTO.setEndDate("When cancelled"); - }else { - emailParamsDTO.setEndDate("手动取消订阅时"); - } - }else { - emailParamsDTO.setEndDate(DateUtil.changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy_EEEE)); - } - if (StringUtil.isNullOrEmpty(subscriptionInfo.getNextPayDate())){ - emailParamsDTO.setNextPayDate("N/A"); - } else { - emailParamsDTO.setNextPayDate(DateUtil.changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy)); - } - emailParamsDTO.setRenewalTime(DateUtil.changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy)); - } - - 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", null); - if (b) log.info("提前7天向用户 {} 发送续订通知邮件", subscriptionInfo.getAccountId()); - } - } - - public void checkSubscriptionExpiration(){ - long epochSecond = Instant.now().getEpochSecond(); - QueryWrapper qw = new QueryWrapper<>(); - qw.lt("current_period_end", epochSecond); - qw.eq("status", "active"); - List subscriptionInfos = subscriptionInfoMapper.selectList(qw); - - for (SubscriptionInfo subscriptionInfo : subscriptionInfos) { - subscriptionInfo.setStatus("expired"); - subscriptionInfo.setUpdateTime(LocalDateTime.now()); - subscriptionInfoMapper.updateById(subscriptionInfo); - log.info("用户 {} 的订阅 {} 已过期", subscriptionInfo.getAccountId(), subscriptionInfo.getOrderNo()); - } - } - - // 新建一个订阅 使用不会成功的付款方式(仅供测试使用) - public String createSubscriptionTemp(String name, String email){ - Stripe.apiKey = privateKey; - try { - OrderInfo orderInfo = orderInfoService.createOrderByProductId(1, PayTypeEnum.STRIPE.getType(), ProductEnum.DailySubscription, null, (byte)0); - -// String customerId = getCustomer(name, email); - String paymentMethodCode = "pm_card_mastercard"; - PaymentMethod paymentMethod = PaymentMethod.retrieve(paymentMethodCode); - - String customerId = getCustomer(name, email); - log.info("customerId: {}", customerId); - - PaymentMethodAttachParams attachParams = PaymentMethodAttachParams.builder() - .setCustomer(customerId) - .build(); - paymentMethod.attach(attachParams); - - // 设置默认付款方式 - Customer updatedCustomer = Customer.retrieve(customerId); - CustomerUpdateParams params = CustomerUpdateParams.builder() - .setInvoiceSettings( - CustomerUpdateParams.InvoiceSettings.builder() - .setDefaultPaymentMethod(paymentMethod.getId()) - .build()) - .build(); - updatedCustomer.update(params); - - // 3. 创建订阅 - SubscriptionCreateParams subscriptionParams = SubscriptionCreateParams.builder() - .setCustomer(customerId) - .addItem( - SubscriptionCreateParams.Item.builder() - .setPrice("price_1QFXkf02n1TEydyNtA4TQ3Yz") // 替换为实际的价格 ID - .build()) - .setDescription("AiDA - " + orderInfo.getOrderNo()) - .build(); - Subscription subscription = Subscription.create(subscriptionParams); - - // 打印订阅 ID - System.out.println("Subscription created: " + subscription.getId()); - return subscription.getId(); - } catch (StripeException e) { - throw new RuntimeException(e); - } - } - - public String changeCustomerPayment(String name, String email){ - Stripe.apiKey = privateKey; - String paymentMethodCode = "pm_card_chargeCustomerFail"; - try { - PaymentMethod paymentMethod = PaymentMethod.retrieve(paymentMethodCode); - - String customerId = getCustomer(name, email); - log.info("customerId: {}", customerId); - - // 附加支付方式到客户 - PaymentMethodAttachParams attachParams = PaymentMethodAttachParams.builder() - .setCustomer(customerId) - .build(); - paymentMethod.attach(attachParams); - // 更新客户的默认支付方式 - CustomerUpdateParams params = CustomerUpdateParams.builder() - .setInvoiceSettings( - CustomerUpdateParams.InvoiceSettings.builder() - .setDefaultPaymentMethod(paymentMethod.getId()) - .build() - ) - .build(); - - // 更新客户信息 - Customer customer = Customer.retrieve(customerId); - customer.update(params); - - return paymentMethod.getId(); - - } catch (StripeException e) { - throw new RuntimeException(e); - } - } - - 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); - } - } - -// public String getIp(HttpServletRequest request) { -// String ipAddress = RequestInfoUtil.getIpAddress(request); -// if (!StringUtil.isNullOrEmpty(ipAddress)) { -// return getIPLocation(ipAddress); -// } -// -// return request.getRemoteAddr(); -// } - - - public String getStackTrace(Exception e, int maxLines) { - StringBuilder sb = new StringBuilder(); - StackTraceElement[] stackTraceElements = e.getStackTrace(); - int lines = Math.min(maxLines, stackTraceElements.length); - - for (int i = 0; i < lines; i++) { - sb.append(stackTraceElements[i].toString()).append("\n"); - } - // 如果堆栈信息超过 maxLines 行,添加提示 - if (stackTraceElements.length > maxLines) { - sb.append("... (More stack trace lines truncated)\n"); - } - return sb.toString(); - } - - - /* - * 优惠券实施: - * 1、由管理员创建优惠券,手动设置该优惠券对应的折扣 - * 2、用户在订阅时,手动输入优惠码 - * 3、使用stripe的字段 redeem_by 管理优惠券的有效期 - * 4、promotion code与 coupons需要绑定 - * 5、用户只能使用一次优惠券 - * - * 问题:对于一次付款和订阅,优惠码是否可以混用 - */ - public String createCoupon(CreateCouponDTO createCouponDTO){ - Stripe.apiKey = privateKey; - CouponCreateParams.Builder couponParams = CouponCreateParams.builder() - // 任何客户只能用一次这个优惠券 - .setDuration(CouponCreateParams.Duration.ONCE) - // percent_off 与 amount_off 不能同时设置 - .setPercentOff(BigDecimal.valueOf(createCouponDTO.getPercentOff())); - if (Objects.nonNull(createCouponDTO.getTimestamp())){ - couponParams.setRedeemBy(createCouponDTO.getTimestamp()); - } - try { - // 1、创建优惠券 - Coupon coupon = Coupon.create(couponParams.build()); - // 2、创建一个推广码 - PromotionCode promotionCode = createPromotionCode(coupon.getId(), createCouponDTO.getMaxRedemptions()); - // 3、落库 - ProductCoupons productCoupons = new ProductCoupons(coupon.getId(), createCouponDTO.getTimestamp(), promotionCode.getId(), - promotionCode.getCode(), createCouponDTO.getMaxRedemptions(), createCouponDTO.getPercentOff(), - createCouponDTO.getCommissionRate(), createCouponDTO.getCooperator(), createCouponDTO.getRemark()); - productCoupons.setCreateTime(LocalDateTime.now()); - productCouponsMapper.insert(productCoupons); - - return promotionCode.getCode(); - } catch (StripeException e) { - throw new RuntimeException(e); - } - } - - public PromotionCode createPromotionCode(String couponId, Long maxRedemption){ - Stripe.apiKey = privateKey; - PromotionCodeCreateParams.Builder promotionCodeParams = PromotionCodeCreateParams.builder() - .setCoupon(couponId) - .setRestrictions(PromotionCodeCreateParams.Restrictions.builder().build()); - if (Objects.nonNull(maxRedemption)){ - promotionCodeParams.setMaxRedemptions(maxRedemption); - } - - try { - return PromotionCode.create(promotionCodeParams.build()); - } catch (StripeException e) { - throw new RuntimeException(e); - } - } - - public ProductCoupons updateCouponsInfo(Long id, Long paidCommission, String cooperator, String remark){ - ProductCoupons productCoupons = productCouponsMapper.selectById(id); - if (Objects.isNull(productCoupons)){ - throw new BusinessException("Unknown Promotion Code"); - } - boolean flag = false; - if (!StringUtil.isNullOrEmpty(cooperator)){ - productCoupons.setCooperator(cooperator); - flag = true; - } - if (!StringUtil.isNullOrEmpty(remark)){ - productCoupons.setRemark(remark); - flag = true; - } - if (Objects.nonNull(paidCommission)){ - productCoupons.setPaidCommission(paidCommission); - flag = true; - } - if (flag){ - productCoupons.setUpdateTime(LocalDateTime.now()); - productCouponsMapper.updateById(productCoupons); - } - return productCoupons; - } - - public ProductCoupons checkProductCoupon(String promotionCode){ - Stripe.apiKey = privateKey; - Long accountId = UserContext.getUserHolder().getId(); - // 1、从数据库查找promotionCode对应的promotionCodeId - ProductCoupons productCoupons = productCouponsMapper.selectOne(new QueryWrapper().eq("promotion_code", promotionCode)); - if (Objects.nonNull(productCoupons)){ - // 2、查绑定的Coupons是否存在以及是否过期 - long epochSecondNow = Instant.now().getEpochSecond(); - Long redeemBy = productCoupons.getRedeemBy(); - if (redeemBy < epochSecondNow){ - throw new BusinessException("this.promotion.code.has.expired"); - } else { - // 判断该用户是否有成功使用过这个推广码 - List paymentInfoByPromCode = paymentInfoService.getPaymentInfoByPromCode(accountId, promotionCode); - if (!paymentInfoByPromCode.isEmpty()) { - // 已使用过推广码,状态无效 - if (paymentInfoByPromCode.size() > 1) { - log.error("用户[{}]多次成功使用优惠码[{}]", accountId, promotionCode); - } - log.info("用户[{}]已成功使用过优惠码[{}]", accountId, promotionCode); - throw new BusinessException("one.time.limit.per.customer"); - } - } - }else { - throw new BusinessException("this.promotion.code.is.invalid"); - } - return productCoupons; - } - - public CheckCouponsVO checkProductCoupon(String promotionCode, Long price){ - Stripe.apiKey = privateKey; - Long accountId = UserContext.getUserHolder().getId(); - CheckCouponsVO checkCouponsVO = new CheckCouponsVO(); - // 1、从数据库查找promotionCode对应的promotionCodeId - ProductCoupons productCoupons = productCouponsMapper.selectOne(new QueryWrapper().eq("promotion_code", promotionCode)); - if (Objects.nonNull(productCoupons)){ - // 2、查绑定的Coupons是否存在以及是否过期 - long epochSecondNow = Instant.now().getEpochSecond(); - Long redeemBy = productCoupons.getRedeemBy(); - if (redeemBy < epochSecondNow){ - String msg = BusinessException.getMessageFromResource("this.promotion.code.has.expired"); - checkCouponsVO.setMessage(msg); - checkCouponsVO.setStatus("expired"); - }else { - // 判断该用户是否有成功使用过这个推广码 - List paymentInfoByPromCode = paymentInfoService.getPaymentInfoByPromCode(accountId, promotionCode); - if (paymentInfoByPromCode.isEmpty()) { - // 未使用过推广码,状态有效 - checkCouponsVO.setStatus("valid"); - checkCouponsVO.setDiscountedPrice(price * (1 - productCoupons.getPercentOff() / 100)); - } else { - // 已使用过推广码,状态无效 - if (paymentInfoByPromCode.size() > 1) { - log.error("用户[{}]多次成功使用优惠码[{}]", accountId, promotionCode); - } - log.info("用户[{}]已成功使用过优惠码[{}]", accountId, promotionCode); - String msg = BusinessException.getMessageFromResource("one.time.limit.per.customer"); - checkCouponsVO.setMessage(msg); - checkCouponsVO.setStatus("invalid"); - } - } - }else { - String msg = BusinessException.getMessageFromResource("this.promotion.code.is.invalid"); - checkCouponsVO.setMessage(msg); - checkCouponsVO.setStatus("invalid"); - } - return checkCouponsVO; - } - - public ProductCoupons getProductCoupon(String promotionCode, String promotionCodeId){ - Stripe.apiKey = privateKey; - QueryWrapper qw = new QueryWrapper<>(); - // 1、从数据库查找promotionCode对应的promotionCodeId - if (!StringUtil.isNullOrEmpty(promotionCode)){ - qw.eq("promotion_code", promotionCode); - } - if (!StringUtil.isNullOrEmpty(promotionCodeId)){ - qw.eq("promotion_code_id", promotionCodeId); - } - return productCouponsMapper.selectOne(qw); - } - - public String retrieveCoupon(String couponId){ - Stripe.apiKey = privateKey; - try { - Coupon coupon = Coupon.retrieve(couponId); - log.info("retrieve的coupon: {}", coupon); - return JSON.toJSONString(coupon); - } catch (StripeException e) { - throw new RuntimeException(e); - } - } - - public String retrievePromotionCode(String promotionCode){ - Stripe.apiKey = privateKey; - try { - ProductCoupons productCoupon = getProductCoupon(promotionCode, null); - PromotionCode retrieve = PromotionCode.retrieve(productCoupon.getPromotionCodeId()); - log.info("retrieve的promotionCode: {}", retrieve); - return JSON.toJSONString(retrieve); - } catch (StripeException e) { - throw new RuntimeException(e); - } - } - - public IPage getAllCoupons(QueryCouponsPageDTO queryCouponsPageDTO){ - // 分页 + 按条件查询 - QueryWrapper queryWrapper = new QueryWrapper<>(); - if (!StringUtil.isNullOrEmpty(queryCouponsPageDTO.getOrderById()) && queryCouponsPageDTO.getOrderById().equals("DESC")){ - queryWrapper.orderByDesc("id"); - } - if (!StringUtil.isNullOrEmpty(queryCouponsPageDTO.getPromotionCode())){ - queryWrapper.like("promotion_code", queryCouponsPageDTO.getPromotionCode()); - } - if (Objects.nonNull(queryCouponsPageDTO.getIsExpired())){ - long epochSecond = Instant.now().getEpochSecond(); - if (queryCouponsPageDTO.getIsExpired()){ - queryWrapper.lt("redeem_by", epochSecond); - }else { - queryWrapper.gt("redeem_by", epochSecond); - } - } - if (!StringUtil.isNullOrEmpty(queryCouponsPageDTO.getCooperator())){ - queryWrapper.like("cooperator", queryCouponsPageDTO.getCooperator()); - } - if (!StringUtil.isNullOrEmpty(queryCouponsPageDTO.getStartTime())){ - queryWrapper.gt("create_time", queryCouponsPageDTO.getStartTime()); - } - if (!StringUtil.isNullOrEmpty(queryCouponsPageDTO.getEndTime())){ - queryWrapper.lt("create_time", queryCouponsPageDTO.getEndTime()); - } - - return productCouponsMapper.selectPage(new Page<>(queryCouponsPageDTO.getPage(), queryCouponsPageDTO.getSize()), queryWrapper); - } - - @Transactional - public void deleteCoupon(Long id){ - Stripe.apiKey = privateKey; - ProductCoupons productCoupons = productCouponsMapper.selectById(id); - if (Objects.isNull(productCoupons)){ - throw new BusinessException("unknown promotion code"); - } - try { - Coupon coupon = Coupon.retrieve(productCoupons.getCouponId()); - coupon.delete(); - log.info("coupon {} 删除成功", productCoupons.getCouponId()); - productCouponsMapper.deleteById(id); - } catch (StripeException e) { - log.error("未知coupons,无法通过couponId: {} 获得Coupons", productCoupons.getCouponId()); - } - } - -} +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.*; +import com.ai.da.common.utils.DateUtil; +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.PaymentInfoMapper; +import com.ai.da.mapper.primary.ProductCouponsMapper; +import com.ai.da.mapper.primary.SubscriptionInfoMapper; +import com.ai.da.mapper.primary.entity.*; +import com.ai.da.model.dto.CreateCouponDTO; +import com.ai.da.model.dto.ProductPurchaseDTO; +import com.ai.da.model.dto.QueryCouponsPageDTO; +import com.ai.da.model.dto.SubscriptionEmailParamsDTO; +import com.ai.da.model.vo.CheckCouponsVO; +import com.ai.da.service.*; +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.google.gson.Gson; +import com.stripe.Stripe; +import com.stripe.exception.InvalidRequestException; +import com.stripe.exception.SignatureVerificationException; +import com.stripe.exception.StripeException; +import com.stripe.model.*; +import com.stripe.model.Product; +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; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.*; +import java.util.stream.Collectors; + +@SuppressWarnings("LoggingSimilarMessage") +@Service +@Slf4j +public class StripeServiceImpl implements StripeService { + + @Resource + private OrderInfoService orderInfoService; + @Resource + private PayPalCheckoutService payPalCheckoutService; + @Resource + private PaymentInfoService paymentInfoService; + @Resource + private CreditsService creditsService; + @Resource + private RefundInfoService refundInfoService; + @Resource + private AccountService accountService; + @Resource + private AccountMapper accountMapper; + @Resource + private SubscriptionInfoMapper subscriptionInfoMapper; + @Resource + private PaymentInfoMapper paymentInfoMapper; + @Resource + private ProductCouponsMapper productCouponsMapper; + @Resource + private RedisUtil redisUtil; + + @Value("${stripe.private-key}") + private String privateKey; + + @Value("${stripe.webhook-sign-secret}") + private String signSecret; + + @Value("${orderList.link}") + private String orderListLink; + + @Value("${stripe.paymentMethodConfiguration}") + private String paymentMethodConfigurationId; + + @Override + @Transactional(rollbackFor = Exception.class) + public String pay(ProductPurchaseDTO productPurchaseDTO, HttpServletRequest request) { + Stripe.apiKey = privateKey; + + //创建支付信息得到url + // 一次性支付和周期扣款,需要区分mode: payment || subscription || setup + SessionCreateParams.Builder sessionBuilder = new SessionCreateParams.Builder(); + 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 "EcoMonth": + productEnum = ProductEnum.Eco_MonthlySubscription; + productPurchaseDTO.setSubscribeType("Month"); + break; + case "Year": + productEnum = ProductEnum.AnnualSubscription; + break; + case "Day": + productEnum = ProductEnum.DailySubscription; + break; + default: + throw new BusinessException("unknown subscription type"); + } + + // 添加优惠券(只允许在订阅时使用优惠券) + String promotionCode = productPurchaseDTO.getPromotionCode(); + if (!StringUtil.isNullOrEmpty(promotionCode)){ + ProductCoupons productCoupon = checkProductCoupon(promotionCode);; + if (productCoupon != null){ + sessionBuilder.addDiscount(SessionCreateParams.Discount.builder() + .setPromotionCode(productCoupon.getPromotionCodeId()).build()); + } + } + // 只有订阅时才允许使用推广码优惠 +// sessionBuilder.setAllowPromotionCodes(true); + break; + default: + throw new BusinessException("unknown product type"); + } + log.info("生成订单"); + String payType; + byte autoRenewal; + if (productPurchaseDTO.getAutoRenewal()){ + payType = "recurring"; + autoRenewal = 1; + }else { + payType = "one_time"; + autoRenewal = 0; + } + OrderInfo orderInfo = orderInfoService.createOrderByProductId(productPurchaseDTO.getQuantity(), + PayTypeEnum.STRIPE.getType(), productEnum, request, autoRenewal); + + try { + 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()); + log.info("customerId:{}", customerId); + // 获取自定义订单号 + String orderId = orderInfo.getOrderNo(); + + + 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.setPaymentMethodConfiguration(paymentMethodConfigurationId); + 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()); + List paymentMethodTypes = session.getPaymentMethodTypes(); + log.info("paymentMethodTypes: {}", paymentMethodTypes); + + Session.PaymentMethodConfigurationDetails paymentMethodConfigurationDetails = session.getPaymentMethodConfigurationDetails(); + log.info("paymentMethodConfigurationDetails ID: {}", paymentMethodConfigurationDetails.getId()); + log.info("sessionId:{}", session.getId()); //退款方式1:拿到sessionId入库,退款的时候根据这个id找到PaymentIntent的id然后发起退款 + + // 更新order信息 + orderInfoService.updateOrderNoById(orderInfo.getId(), orderId); + return session.getUrl(); + } catch (BusinessException e) { + throw e; + } catch (InvalidRequestException e) { + log.info("创建会话出现异常:", e); + throw new BusinessException(e.getMessage().substring(0, e.getMessage().indexOf(";"))); + } catch (Exception e) { + log.error("创建支付会话出现异常:", e); + } + 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()); + } + + @Resource + private EmailService emailService; + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean notify(HttpServletRequest request) { + log.info("stripe异步通知进行中"); + String payload = null; + String sigHeader = null; + String endpointSecret = signSecret; + try { + sigHeader = request.getHeader("Stripe-Signature"); + payload = payPalCheckoutService.getBody(request); + } catch (Exception e) { + log.info("stripe 支付回调参数解析异常:errorMsg {}", e.getMessage()); + log.info("request sigHeader = {}", sigHeader); + log.info("request body = {}", JSON.toJSONString(payload)); + e.printStackTrace(); + return Boolean.FALSE; + } + + Event event; + try { + assert sigHeader != null; + event = Webhook.constructEvent(payload, sigHeader, endpointSecret); + } catch (SignatureVerificationException e) { + log.info("stripe 验签,获取事件异常, errorMsg={}", e.getMessage()); + log.info("request sigHeader = {}", sigHeader); + log.info("request body = {}", JSON.toJSONString(payload)); + e.printStackTrace(); + return Boolean.FALSE; + } + + //获取自定义参数 + // Deserialize the nested object inside the event + assert event != null; + EventDataObjectDeserializer dataObjectDeserializer = event.getDataObjectDeserializer(); + StripeObject stripeObject ; + if (dataObjectDeserializer.getObject().isPresent()) { + stripeObject = dataObjectDeserializer.getObject().get(); + } else { + log.info("stripe 验签失败!"); + log.info("request sigHeader = {}", sigHeader); + log.info("request body = {}", JSON.toJSONString(payload)); + return Boolean.FALSE; + } + log.info("stripe验签成功"); + boolean response = Boolean.TRUE; + + log.info("回调事件 {}", event.getType()); + if (stripeObject instanceof Session){ + Session session = (Session) stripeObject; + if (event.getType().equals("checkout.session.completed")) { + response = processOrder(session); + }else if (event.getType().equals("checkout.session.expired")){ + String orderNo = session.getMetadata().get("orderId"); + // 会话过期 未支付 且之后没有支付成功的订单 + response = 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, null); + } + // 续订支付失败,邮件通知用户 + if (subscription.getStatus().equals("past_due")){ + // 发送续订失败邮件 + response = sendRenewalFailEmail(null, subscription.getId(), subscriptionInfo.getOrderNo()); + + } + } 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", null); + if (response){ + subscriptionInfo.setCancelNotified((byte)1); + subscriptionInfoMapper.updateById(subscriptionInfo); + // 更新订单信息 + OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(subscriptionInfo.getOrderNo()); + orderInfo.setAutoRenewal((byte)0); + } + } + + }/* 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); + + /* 在sendEmail方法中有做判断,这里的判断取消 + // 当前支付没有被通知时才需要发送通知邮件 + if (paymentInfo.getNotified().equals(0)) { + + }*/ + // 更新t_order_info中的total_fee,记录该订单的累计付款金额 + orderInfoService.updateTotalFeeByOrderNo(paymentInfo.getOrderNo()); + // 邮件通知商家和用户 + String billingReason = invoice.getBillingReason(); + switch (billingReason) { + case "subscription_create": + response = sendEmail(invoice.getSubscription(), "new", null); + break; + case "subscription_cycle": + response = sendEmail(invoice.getSubscription(), "renewal", null); + break; + case "manual": + boolean b = invoice.getLines().getData().get(0).getDescription().endsWith("Subscription"); + if (b) { + // 非自动续订式订阅,Stripe不会创建Subscription,所以invoice中不会有subscriptionId + response = sendEmail(null, "new", paymentInfo.getOrderNo()); + } + break; + } + + } 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 { + // 新增支付信息 + PaymentInfo paymentInfoFail = paymentInfoService.createOrUpdatePaymentInfoForStripe(invoice); + // 发送新订阅失败邮件 + response = sendEmail(paymentInfoFail.getOrderNo()); + } + } + }else if (stripeObject instanceof Charge) { + Charge charge = (Charge) stripeObject; + String orderNo = charge.getDescription().replace("AiDA - ", ""); + OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo); + if (Objects.isNull(orderInfo)){ + // 说明该回调不是从AiDA订阅获得 + return true; + } + 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); + }else if (event.getType().equals("charge.refunded")){ + // 更新退款信息 + RefundInfo refundInfo = refundInfoService.updateRefundForStripe(charge); + // 更新 t_payment_info的支付状态 + if (Objects.nonNull(refundInfo)){ + paymentInfoService.updatePaymentRefundStatus(charge); + } + } + }else if (stripeObject instanceof Refund){ + Refund refund = (Refund) stripeObject; + if (event.getType().equals("refund.created")){ + // 新增退款信息 + refundInfoService.createRefundForStripe(refund); + }else if (event.getType().equals("refund.updated")){ + // 根据***id更新退款记录信息 + RefundInfo refundInfo = refundInfoService.updateRefundStatusForStripe(refund); + if (Objects.isNull(refundInfo)){ + // 等事件先创建,再更新。回调事件的顺序随机 + response = false; + } + } + } + log.info("回调事件 {} 处理完成", event.getType()); + return response; + } + + public boolean processOrder(Session session) { + Stripe.apiKey = privateKey; + String orderNo = session.getMetadata().get("orderId"); + float totalAmount = new BigDecimal(session.getAmountTotal()).divide(new BigDecimal(100), 2, RoundingMode.HALF_UP).floatValue(); + + boolean resp = true; + try { + //处理重复通知 + //接口调用的幂等性:无论接口被调用多少次,以下业务执行一次 +// String orderStatus = orderInfoService.getOrderStatus(orderNo); + OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(orderNo); + String orderStatus = orderByOrderNo.getOrderStatus(); + // 当订单状态处于未支付或超时已关闭时,更新订单状态,其他状态均不更新订单状态 + if (!OrderStatusEnum.NOT_PAY.getType().equals(orderStatus) && !OrderStatusEnum.TIMEOUT_CLOSED.getType().equals(orderStatus)) { + log.info("订单状态 : {}", orderStatus); + }else { + //更新订单状态 + orderInfoService.updateStatusByOrderNo(orderNo, OrderStatusEnum.SUCCESS); + log.info("Stripe 订单:{} 状态更新成功", orderNo); + } + + if (orderByOrderNo.getTitle().startsWith("积分购买")){ + // 查询当前订单的积分是否已添加 + CreditsDetail creditsDetail = creditsService.queryDetailByTaskId(orderNo); + if (Objects.isNull(creditsDetail)){ + 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", orderNo); + log.info("用户:{} 积分信息更新成功", orderByOrderNo.getAccountId()); + } + }else if (orderByOrderNo.getTitle().endsWith("Subscription") && orderByOrderNo.getAutoRenewal() == (byte)0){ + String invoiceId = session.getInvoice(); + Invoice invoice = Invoice.retrieve(invoiceId); + InvoiceLineItem invoiceLineItem = invoice.getLines().getData().get(0); + String description = invoiceLineItem.getDescription(); + Long amount = invoiceLineItem.getAmount(); + log.info("单次订阅 description : {}, amount: {} 分", description, amount); + boolean b = createSubscriptionAndUpdateAccount(orderNo, orderByOrderNo.getAccountId(), description, amount); + // 邮件通知用户 + if (b){ + resp = sendEmail(null, "new", orderNo); + } + log.info("单次订阅订单:{} 处理完成", orderNo); + } + } catch (Exception e) { + log.info(e.getMessage()); + resp = false; + } + return resp; + } + + private boolean processExpiredOrder(String orderNo) { + // 支付失败 通知商家的条件 1、会话过期 2、支付失败 3、这个用户在这个支付失败后再无支付成功的订阅 + // 1、获取当前订单的支付状态 +// String orderNo = session.getMetadata().get("orderId"); + OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(orderNo); + // 2、确认订单状态为支付失败 + boolean resp = true; + if (!Objects.isNull(orderByOrderNo) && 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")) { + resp = sendEmail(orderNo); + }else { + // todo 续订失败 应该不会走这里 + resp = sendEmail(subscriptionInfo.getSubscriptionId(), "fail_renewal", null); + } + } + } + return resp; + + } + + @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()); + + int rows = subscriptionInfoMapper.insertIgnore(subscriptionInfo); + log.info("Subscription info insert affect rows : {}", rows); + + if (subscriptionInfo.getStatus().equals("active")){ + log.info("创建订阅更新账号信息"); + // 更新账号到期时间 + boolean b = accountService.updateAccountValidity(subscriptionInfo.getAccountId(), subscriptionInfo.getCurrentPeriodEnd()); + // 更新账号身份和积分 + if (b) accountService.updateUserRoleAndCredits(subscriptionInfo.getAccountId(), orderNo); + } + } + return subscriptionInfo; + } + + /** + * 非自动续订订阅 + * Stripe不会自动创建Subscription,所以没有subscription相关的回调,无法触发订阅相关的处理代码 + */ + public boolean createSubscriptionAndUpdateAccount(String orderNo, Long accountId, String description, Long amount){ + QueryWrapper qw = new QueryWrapper<>(); + qw.eq("order_no", orderNo); + SubscriptionInfo subscriptionInfo = subscriptionInfoMapper.selectOne(qw); + if (Objects.isNull(subscriptionInfo)) { + String interval; + // 获取当前时间戳(秒级) + long currentPeriodStart = Instant.now().getEpochSecond();; + long currentPeriodEnd; +// InvoiceLineItem invoiceLineItem = invoice.getLines().getData().get(0); + if (description.equals(ProductEnum.DailySubscription.getName()) + && amount.equals(ProductEnum.DailySubscription.getPrice() * 100)){ + interval = "day"; + // 获取一天后的时间戳(秒级) + ZonedDateTime now = ZonedDateTime.now(); + currentPeriodEnd = now.plusDays(1).toEpochSecond(); + }else if (description.equals(ProductEnum.MonthlySubscription.getName()) + && amount.equals(ProductEnum.MonthlySubscription.getPrice() * 100)){ + interval = "month"; + // 获取一天后的时间戳(秒级) + ZonedDateTime now = ZonedDateTime.now(); + currentPeriodEnd = now.plusMonths(1).toEpochSecond(); + } else if (description.equals(ProductEnum.Eco_MonthlySubscription.getName()) + && amount.equals(ProductEnum.Eco_MonthlySubscription.getPrice() * 100)){ + interval = "month"; + // 获取一天后的时间戳(秒级) + ZonedDateTime now = ZonedDateTime.now(); + currentPeriodEnd = now.plusMonths(1).toEpochSecond(); + } else if (description.equals(ProductEnum.AnnualSubscription.getName()) + && amount.equals(ProductEnum.AnnualSubscription.getPrice() * 100)){ + interval = "year"; + // 获取一天后的时间戳(秒级) + ZonedDateTime now = ZonedDateTime.now(); + currentPeriodEnd = now.plusYears(1).toEpochSecond(); + }else { + log.error("未知订阅类型"); + return false; + } + subscriptionInfo = new SubscriptionInfo(); + subscriptionInfo.setAccountId(accountId); + subscriptionInfo.setOrderNo(orderNo); + subscriptionInfo.setType(interval); + subscriptionInfo.setStatus("canceled"); + subscriptionInfo.setCurrentPeriodStart(currentPeriodStart); + subscriptionInfo.setCurrentPeriodEnd(currentPeriodEnd); + subscriptionInfo.setCreateTime(LocalDateTime.now()); + + subscriptionInfoMapper.insertIgnore(subscriptionInfo); + + log.info("创建订阅, 更新账号信息"); + // 更新账号到期时间 + boolean b = accountService.updateAccountValidity(subscriptionInfo.getAccountId(), subscriptionInfo.getCurrentPeriodEnd()); + // 更新账号身份和积分 + if (b) accountService.updateUserRoleAndCredits(subscriptionInfo.getAccountId(), orderNo); + return true; + } + return true; + } + + 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).orderByDesc("id"); + 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)); + log.info("更新订阅更新账号信息"); + // 更新账号到期时间 + accountService.updateAccountValidity(subscriptionInfo.getAccountId(), subscriptionInfo.getCurrentPeriodEnd()); + // 更新账号身份和积分 + accountService.updateUserRoleAndCredits(subscriptionInfo.getAccountId(), subscriptionInfo.getOrderNo()); + log.info("更新 {} 账号到期时间为:{}", subscriptionInfo.getAccountId(), DateUtil.changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy_EEEE)); + flag = true; + } + if (subscriptionInfo.getStatus().equals("active")){ + // 更新账号到期时间 + boolean b = accountService.updateAccountValidity(subscriptionInfo.getAccountId(), subscriptionInfo.getCurrentPeriodEnd()); + // 更新账号身份和积分 + if (b) accountService.updateUserRoleAndCredits(subscriptionInfo.getAccountId(), subscriptionInfo.getOrderNo()); + } + if (flag){ + subscriptionInfo.setUpdateTime(LocalDateTime.now()); + subscriptionInfoMapper.updateById(subscriptionInfo); + } + return subscriptionInfo; + } + + // 取消连续订阅 将订阅从pause状态转为cancel状态(使用定时器,定期检索DB中,过期且不续订的订阅) + public void cancelSubscription(String subscriptionId, String cancelReason) { + Stripe.apiKey = privateKey; + log.info("cancel subscription"); + Long accountId = UserContext.getUserHolder().getId(); + 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(); + log.info("用户 {} 申请取消连续订阅 {}", accountId, subscriptionId); + // 更新数据库 + 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.getPaymentInfoByOrderNo(orderNo, "DESC").get(0).getTransactionId(); + + if (StringUtils.isNotEmpty(sessionId)) { //根据会话编号退款 + Session session = Session.retrieve(sessionId); + RefundCreateParams params; + if (amount != null && !amount.equals("0")) { //指定退款金额 + BigDecimal actualAmount = new BigDecimal(amount).multiply(BigDecimal.valueOf(100)); //api默认单位分 + params = RefundCreateParams.builder() + .setPaymentIntent(session.getPaymentIntent()) + .setAmount(actualAmount.longValue()) + .build(); + } else { //全额退款 + params = RefundCreateParams.builder() + .setPaymentIntent(session.getPaymentIntent()) + .build(); + } + refund = Refund.create(params); + log.info("根据会话编号退款成功"); + + } else { + log.error("当前订单不存在"); + return "退款异常"; + } + } catch (Exception e) { + //e.getMessage.contain("charge_already_refunded") 已退款 + //e.getMessage.contain("resource_missing") 退款编号错误 + //e.getMessage.contain("amount on charge ($n)") 金额应小于n + log.error("退款异常:", e); + return "退款异常"; + } + + if ("succeeded".equals(refund.getStatus())) { + //进行数据库操作,修改状态为已退款(配合回调和退款查询确定退款成功) + //更新订单状态 + orderInfoService.updateStatusByOrderNo(orderNo, OrderStatusEnum.REFUND_SUCCESS); + + refundInfoService.updateRefundForPayPal( + refundByOrderNo.getId(), + refund.getId(), + new Gson().toJson(refund), + AliPayTradeStateEnum.REFUND_SUCCESS.getType()); //退款成功 + + // 更新积分状态 + OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(orderNo); + creditsService.creditsRefund(orderByOrderNo.getAccountId(), (int) (orderByOrderNo.getTotalFee() / Float.parseFloat(CreditsEventsEnum.PRICE.getValue())), orderNo); + } else { + //更新订单状态 + orderInfoService.updateStatusByOrderNo(orderNo, OrderStatusEnum.REFUND_ABNORMAL); + + //更新退款单 + refundInfoService.updateRefundForPayPal( + refundByOrderNo.getId(), + refund.getId(), + new Gson().toJson(refund), + AliPayTradeStateEnum.REFUND_ERROR.getType()); //退款失败 + } + log.info("记录退款订单"); + return "退款成功"; + } + + public void checkOrderStatus(String orderNo) { + Stripe.apiKey = privateKey; + // 1、通过orderNo 查询sessionId + // todo transactionId不再是sessionId而是invoiceId,所以这里需要更新 + PaymentInfo paymentInfo = paymentInfoService.getPaymentInfoByOrderNo(orderNo, "DESC").get(0); + try { + Session session = Session.retrieve(paymentInfo.getTransactionId()); + if (Objects.isNull(session)) { + log.warn("核实订单未创建 ===> {}", orderNo); + return; + } else if (session.getStatus().equals("open") || session.getStatus().equals("expired")) { + // 订单未支付 || 订单过期 ---> 均设置为超时未支付 + log.info("订单超时未支付 ===> {}", orderNo); + //更新本地订单状态 + orderInfoService.updateStatusByOrderNo(orderNo, OrderStatusEnum.TIMEOUT_CLOSED); + paymentInfoService.updatePaymentStatusById(paymentInfo.getId(), + session.getStatus(), + new Gson().toJson(session)); + } else if (session.getStatus().equals("complete")) { + // 订单已完成 + processOrder(session); + } + } catch (StripeException e) { + log.error("根据sessionId获取Stripe Session失败"); + throw new RuntimeException(e); + } + + } + + 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).setLimit(20L) + .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(name) + .setEmail(userEmail) + .build(); + Customer customer = Customer.create(params); + + return customer.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 Map getPaymentMethod(String paymentMethodId){ + Stripe.apiKey = privateKey; + String paymentMethod = null; + String last4 = null; + + 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 boolean sendEmail(String subscriptionId, String type, String orderNo) { + SubscriptionInfo subscriptionInfo; + QueryWrapper qwSI = new QueryWrapper<>(); + if (!StringUtil.isNullOrEmpty(subscriptionId)) { + qwSI.eq("subscription_id", subscriptionId); + List subscriptionInfoList = subscriptionInfoMapper.selectList(qwSI); + + if (subscriptionInfoList.isEmpty()){ + log.info("不发送邮件,原因:【subscriptionInfoList 为空】"); + return false; + }else { + List activeSubscriptions = subscriptionInfoList.stream() + .filter(subscription -> "active".equals(subscription.getStatus())) + .collect(Collectors.toList()); + if (!StringUtil.isNullOrEmpty(type) && type.equals("cancel")){ + subscriptionInfo = subscriptionInfoList.get(0); + }else if (activeSubscriptions.isEmpty()){ + log.info("不发送邮件,原因:【当前邮件类型:{}, 但是状态为active的subscriptionInfo为空】", type); + return false; + }else { + subscriptionInfo = activeSubscriptions.get(0); + } + } + }else if (!StringUtil.isNullOrEmpty(orderNo)) { + qwSI.eq("order_no", orderNo); + subscriptionInfo = subscriptionInfoMapper.selectOne(qwSI); + if (Objects.isNull(subscriptionInfo)){ + log.info("不发送邮件,原因:【根据order_no:{},查询到的subscriptionInfo为空】", orderNo); + return false; + } + }else { + log.info("不发送邮件,原因:【入参中的subscriptionId,orderNo均为空】"); + return false; + } + + QueryWrapper qwPI = new QueryWrapper<>(); + qwPI.eq("order_no", subscriptionInfo.getOrderNo()).orderByDesc("id"); + List paymentInfos = paymentInfoMapper.selectList(qwPI); + if (paymentInfos.isEmpty()) { + log.info("不发送邮件,原因:【根据order_no:{},查询到的paymentInfos为空】", orderNo); + return false; + } + PaymentInfo paymentInfo = paymentInfos.get(0); + if (StringUtil.isNullOrEmpty(type)){ + // 如果没有传入type,则使用paymentInfo中记录的类型 + // (其实这里也可以通过invoiceId查询stripe,但是记录在自己的db中可以不用每次都查,且方便查看) + type = StringUtil.isNullOrEmpty(paymentInfo.getType()) ? "new" : paymentInfo.getType(); + } + + // todo 之后这种改成通过email-log来判断 + String key = RedisUtil.SUBSCRIPTION_SENT_EMAIL_TYPE + subscriptionInfo.getId(); + // 先判断当前订单 这个类型的邮件是否已发送过 + Boolean elementExistsInSet = redisUtil.isElementExistsInSet(key, type); + if (!type.equals("reminder") && !type.equals("cancel") && paymentInfo.getNotified() == 1 && elementExistsInSet){ + // 已经邮件通知过,直接返回 + log.info("不发送邮件,原因:【type为:{},order_no为:{},已经进行邮件通知】", type, orderNo); + return true; + } + + 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.setOrderRef("\"" + orderListLink + 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, language); + + boolean b = SendEmailUtil.subscriptionEmailReminder(type, emailParamsDTO, language, account.getUserEmail()); +// boolean b = emailService.subscriptionEmailReminder(type, emailParamsDTO, language, account.getUserEmail()); + if (!b) return false; + + // 邮件通知成功后,更新标志 + 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); + } + + // 将发成功的邮件类型存入redis 避免同一个订阅重复发送相同类型的邮件 subId:type + redisUtil.addToSet(key, type, CommonConstant.REDIS_SET_EXPIRE_TIME); + return true; + } + + public boolean sendEmail(String orderNo){ + SubscriptionEmailParamsDTO emailParamsDTO = new SubscriptionEmailParamsDTO(); + OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo); + + com.ai.da.mapper.primary.entity.Account account = accountMapper.selectById(orderInfo.getAccountId()); + String userName = account.getUserName(); + String language = account.getLanguage(); + QueryWrapper qwPI = new QueryWrapper<>(); + qwPI.eq("order_no", orderNo); + List paymentInfos = paymentInfoMapper.selectList(qwPI); + if (paymentInfos.isEmpty()) { + log.info("不发送邮件,原因:【根据order_no:{},查询到的paymentInfos为空】", orderNo); + 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()); + + boolean b = SendEmailUtil.subscriptionEmailReminder("fail_new", emailParamsDTO, language, account.getUserEmail()); +// boolean b = emailService.subscriptionEmailReminder("fail_new", emailParamsDTO, language, account.getUserEmail()); + if (!b) return false; + + // 邮件通知成功后,更新标志 + 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, language); + + // 4、发邮件 + boolean b = SendEmailUtil.subscriptionEmailReminder("fail_renewal", emailParamsDTO, language, account.getUserEmail()); +// boolean b = emailService.subscriptionEmailReminder("fail_renewal", emailParamsDTO, language, account.getUserEmail()); + if (!b) return false; + + 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, String language) { + emailParamsDTO.setPaymentMethod(paymentInfo.getPaymentMethod()); + emailParamsDTO.setLast4(paymentInfo.getLast4()); + emailParamsDTO.setSubscriptionId(subscriptionInfo.getId().toString()); + emailParamsDTO.setFailMessage(orderByOrderNo.getNote()); + emailParamsDTO.setSubscriptionType(subscriptionInfo.getType()); + emailParamsDTO.setStartDate(DateUtil.changeTimeStampFormat(orderByOrderNo.getCreateTime())); + if (subscriptionInfo.getType().equals("month")){ + emailParamsDTO.setRenewalFee(String.valueOf(ProductEnum.MonthlySubscription.getPrice())); + } else if (subscriptionInfo.getType().equals("year")){ + emailParamsDTO.setRenewalFee(String.valueOf(ProductEnum.AnnualSubscription.getPrice())); + } else if (subscriptionInfo.getType().equals("day")){ + emailParamsDTO.setRenewalFee(String.valueOf(ProductEnum.DailySubscription.getPrice())); + } else { + emailParamsDTO.setRenewalFee("?"); + } + if (subscriptionInfo.getStatus().equals("active")){ + if (language.equals("ENGLISH")){ + emailParamsDTO.setEndDate("When cancelled"); + }else { + emailParamsDTO.setEndDate("手动取消订阅时"); + } + }else { + emailParamsDTO.setEndDate(DateUtil.changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy_EEEE)); + } + if (StringUtil.isNullOrEmpty(subscriptionInfo.getNextPayDate())){ + emailParamsDTO.setNextPayDate("N/A"); + } else { + emailParamsDTO.setNextPayDate(DateUtil.changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy)); + } + emailParamsDTO.setRenewalTime(DateUtil.changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy)); + } + + 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", null); + if (b) log.info("提前7天向用户 {} 发送续订通知邮件", subscriptionInfo.getAccountId()); + } + } + + public void checkSubscriptionExpiration(){ + long epochSecond = Instant.now().getEpochSecond(); + QueryWrapper qw = new QueryWrapper<>(); + qw.lt("current_period_end", epochSecond); + qw.eq("status", "active"); + List subscriptionInfos = subscriptionInfoMapper.selectList(qw); + + for (SubscriptionInfo subscriptionInfo : subscriptionInfos) { + subscriptionInfo.setStatus("expired"); + subscriptionInfo.setUpdateTime(LocalDateTime.now()); + subscriptionInfoMapper.updateById(subscriptionInfo); + log.info("用户 {} 的订阅 {} 已过期", subscriptionInfo.getAccountId(), subscriptionInfo.getOrderNo()); + } + } + + // 新建一个订阅 使用不会成功的付款方式(仅供测试使用) + public String createSubscriptionTemp(String name, String email){ + Stripe.apiKey = privateKey; + try { + OrderInfo orderInfo = orderInfoService.createOrderByProductId(1, PayTypeEnum.STRIPE.getType(), ProductEnum.DailySubscription, null, (byte)0); + +// String customerId = getCustomer(name, email); + String paymentMethodCode = "pm_card_mastercard"; + PaymentMethod paymentMethod = PaymentMethod.retrieve(paymentMethodCode); + + String customerId = getCustomer(name, email); + log.info("customerId: {}", customerId); + + PaymentMethodAttachParams attachParams = PaymentMethodAttachParams.builder() + .setCustomer(customerId) + .build(); + paymentMethod.attach(attachParams); + + // 设置默认付款方式 + Customer updatedCustomer = Customer.retrieve(customerId); + CustomerUpdateParams params = CustomerUpdateParams.builder() + .setInvoiceSettings( + CustomerUpdateParams.InvoiceSettings.builder() + .setDefaultPaymentMethod(paymentMethod.getId()) + .build()) + .build(); + updatedCustomer.update(params); + + // 3. 创建订阅 + SubscriptionCreateParams subscriptionParams = SubscriptionCreateParams.builder() + .setCustomer(customerId) + .addItem( + SubscriptionCreateParams.Item.builder() + .setPrice("price_1QFXkf02n1TEydyNtA4TQ3Yz") // 替换为实际的价格 ID + .build()) + .setDescription("AiDA - " + orderInfo.getOrderNo()) + .build(); + Subscription subscription = Subscription.create(subscriptionParams); + + // 打印订阅 ID + System.out.println("Subscription created: " + subscription.getId()); + return subscription.getId(); + } catch (StripeException e) { + throw new RuntimeException(e); + } + } + + public String changeCustomerPayment(String name, String email){ + Stripe.apiKey = privateKey; + String paymentMethodCode = "pm_card_chargeCustomerFail"; + try { + PaymentMethod paymentMethod = PaymentMethod.retrieve(paymentMethodCode); + + String customerId = getCustomer(name, email); + log.info("customerId: {}", customerId); + + // 附加支付方式到客户 + PaymentMethodAttachParams attachParams = PaymentMethodAttachParams.builder() + .setCustomer(customerId) + .build(); + paymentMethod.attach(attachParams); + // 更新客户的默认支付方式 + CustomerUpdateParams params = CustomerUpdateParams.builder() + .setInvoiceSettings( + CustomerUpdateParams.InvoiceSettings.builder() + .setDefaultPaymentMethod(paymentMethod.getId()) + .build() + ) + .build(); + + // 更新客户信息 + Customer customer = Customer.retrieve(customerId); + customer.update(params); + + return paymentMethod.getId(); + + } catch (StripeException e) { + throw new RuntimeException(e); + } + } + + 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); + } + } + +// public String getIp(HttpServletRequest request) { +// String ipAddress = RequestInfoUtil.getIpAddress(request); +// if (!StringUtil.isNullOrEmpty(ipAddress)) { +// return getIPLocation(ipAddress); +// } +// +// return request.getRemoteAddr(); +// } + + + public String getStackTrace(Exception e, int maxLines) { + StringBuilder sb = new StringBuilder(); + StackTraceElement[] stackTraceElements = e.getStackTrace(); + int lines = Math.min(maxLines, stackTraceElements.length); + + for (int i = 0; i < lines; i++) { + sb.append(stackTraceElements[i].toString()).append("\n"); + } + // 如果堆栈信息超过 maxLines 行,添加提示 + if (stackTraceElements.length > maxLines) { + sb.append("... (More stack trace lines truncated)\n"); + } + return sb.toString(); + } + + + /* + * 优惠券实施: + * 1、由管理员创建优惠券,手动设置该优惠券对应的折扣 + * 2、用户在订阅时,手动输入优惠码 + * 3、使用stripe的字段 redeem_by 管理优惠券的有效期 + * 4、promotion code与 coupons需要绑定 + * 5、用户只能使用一次优惠券 + * + * 问题:对于一次付款和订阅,优惠码是否可以混用 + */ + public String createCoupon(CreateCouponDTO createCouponDTO){ + Stripe.apiKey = privateKey; + CouponCreateParams.Builder couponParams = CouponCreateParams.builder() + // 任何客户只能用一次这个优惠券 + .setDuration(CouponCreateParams.Duration.ONCE) + // percent_off 与 amount_off 不能同时设置 + .setPercentOff(BigDecimal.valueOf(createCouponDTO.getPercentOff())); + if (Objects.nonNull(createCouponDTO.getEndTime())){ + couponParams.setRedeemBy(createCouponDTO.getEndTime()); + } + try { + // 1、创建优惠券 + Coupon coupon = Coupon.create(couponParams.build()); + // 2、创建一个推广码 + PromotionCode promotionCode = createPromotionCode(coupon.getId(), createCouponDTO.getMaxRedemptions()); + // 3、落库 + ProductCoupons productCoupons = new ProductCoupons(coupon.getId(), createCouponDTO.getEndTime(), promotionCode.getId(), + promotionCode.getCode(), createCouponDTO.getMaxRedemptions(), createCouponDTO.getPercentOff(), + createCouponDTO.getCommissionRate(), createCouponDTO.getCooperator(), createCouponDTO.getRemark()); + productCoupons.setStartTime(createCouponDTO.getStartTime()); + productCoupons.setCreateTime(LocalDateTime.now()); + productCouponsMapper.insert(productCoupons); + + return promotionCode.getCode(); + } catch (StripeException e) { + throw new RuntimeException(e); + } + } + + public PromotionCode createPromotionCode(String couponId, Long maxRedemption){ + Stripe.apiKey = privateKey; + PromotionCodeCreateParams.Builder promotionCodeParams = PromotionCodeCreateParams.builder() + .setCoupon(couponId) + .setRestrictions(PromotionCodeCreateParams.Restrictions.builder().build()); + if (Objects.nonNull(maxRedemption)){ + promotionCodeParams.setMaxRedemptions(maxRedemption); + } + + try { + return PromotionCode.create(promotionCodeParams.build()); + } catch (StripeException e) { + throw new RuntimeException(e); + } + } + + public ProductCoupons updateCouponsInfo(Long id, String paidCommission, String cooperator, String remark, Long startTime){ + ProductCoupons productCoupons = productCouponsMapper.selectById(id); + if (Objects.isNull(productCoupons)){ + throw new BusinessException("Unknown Promotion Code"); + } + boolean flag = false; + if (!StringUtil.isNullOrEmpty(cooperator)){ + productCoupons.setCooperator(cooperator); + flag = true; + } + if (!StringUtil.isNullOrEmpty(remark)){ + productCoupons.setRemark(remark); + flag = true; + } + if (Objects.nonNull(paidCommission)){ + // 将 paidCommission 从 String 转换为 BigDecimal + if (!paidCommission.matches("-?\\d+(\\.\\d+)?")) { + throw new BusinessException("Invalid paidCommission value: " + paidCommission); + } + BigDecimal paidCommissionBigDecimal = new BigDecimal(paidCommission); + // 设置已支付佣金 + productCoupons.setPaidCommission(paidCommissionBigDecimal); + BigDecimal commission = Objects.isNull(productCoupons.getCommission()) ? new BigDecimal(0) : productCoupons.getCommission(); + // 计算未支付佣金 + BigDecimal unpaidCommission = commission.subtract(paidCommissionBigDecimal); + // 设置未支付佣金,确保其不为负数 + productCoupons.setUnpaidCommission(unpaidCommission.compareTo(BigDecimal.ZERO) > 0 ? unpaidCommission : BigDecimal.ZERO); + flag = true; + } + if (Objects.nonNull(startTime)){ + productCoupons.setStartTime(startTime); + flag = true; + } + if (flag){ + productCoupons.setUpdateTime(LocalDateTime.now()); + productCouponsMapper.updateById(productCoupons); + } + return productCoupons; + } + + public ProductCoupons checkProductCoupon(String promotionCode){ + Stripe.apiKey = privateKey; + Long accountId = UserContext.getUserHolder().getId(); + String language = UserContext.getUserHolder().getLanguage(); + // 1、从数据库查找promotionCode对应的promotionCodeId + ProductCoupons productCoupons = productCouponsMapper.selectOne(new QueryWrapper().eq("promotion_code", promotionCode)); + if (Objects.nonNull(productCoupons)){ + // 2、查绑定的Coupons是否存在以及是否过期 + long epochSecondNow = Instant.now().getEpochSecond(); + Long redeemBy = productCoupons.getRedeemBy(); + if (redeemBy < epochSecondNow){ + throw new BusinessException("this.promotion.code.has.expired"); + } else if (Objects.nonNull(productCoupons.getStartTime()) && productCoupons.getStartTime() > epochSecondNow) { + String startTime = DateUtil.changeTimeStampFormat(productCoupons.getStartTime(), "seconds", CommonConstant.TIME_FORMAT_yyyy_MM_dd_HH_mm_ss); + String en = "This coupon will become active on " + startTime + ". Please try again then!"; + String cn = "该优惠券尚未到生效时间,请在 " + startTime + " 后使用。"; + throw new BusinessException(language.equals("ENGLISH") ? en : cn); + } else { + // 判断该用户是否有成功使用过这个推广码 + List paymentInfoByPromCode = paymentInfoService.getPaymentInfoByPromCode(accountId, promotionCode); + if (!paymentInfoByPromCode.isEmpty()) { + // 已使用过推广码,状态无效 + if (paymentInfoByPromCode.size() > 1) { + log.error("用户[{}]多次成功使用优惠码[{}]", accountId, promotionCode); + } + log.info("用户[{}]已成功使用过优惠码[{}]", accountId, promotionCode); + throw new BusinessException("one.time.limit.per.customer"); + } + } + }else { + throw new BusinessException("this.promotion.code.is.invalid"); + } + return productCoupons; + } + + public CheckCouponsVO checkProductCoupon(String promotionCode, Long price){ + Stripe.apiKey = privateKey; + Long accountId = UserContext.getUserHolder().getId(); + String language = UserContext.getUserHolder().getLanguage(); + CheckCouponsVO checkCouponsVO = new CheckCouponsVO(); + // 1、从数据库查找promotionCode对应的promotionCodeId + ProductCoupons productCoupons = productCouponsMapper.selectOne(new QueryWrapper().eq("promotion_code", promotionCode)); + if (Objects.nonNull(productCoupons)){ + // 2、查绑定的Coupons是否存在以及是否过期 + long epochSecondNow = Instant.now().getEpochSecond(); + Long redeemBy = productCoupons.getRedeemBy(); + if (redeemBy < epochSecondNow){ + String msg = BusinessException.getMessageFromResource("this.promotion.code.has.expired"); + checkCouponsVO.setMessage(msg); + checkCouponsVO.setStatus("expired"); + }else if (Objects.nonNull(productCoupons.getStartTime()) && productCoupons.getStartTime() > epochSecondNow) { + String startTime = DateUtil.changeTimeStampFormat(productCoupons.getStartTime(), "seconds", CommonConstant.TIME_FORMAT_yyyy_MM_dd_HH_mm_ss); + String en = "This coupon will become active on " + startTime + ". Please try again then!"; + String cn = "该优惠券尚未到生效时间,请在 " + startTime + " 后使用。"; + checkCouponsVO.setMessage(language.equals("ENGLISH") ? en : cn); + checkCouponsVO.setStatus("pending"); + } else { + // 判断该用户是否有成功使用过这个推广码 + List paymentInfoByPromCode = paymentInfoService.getPaymentInfoByPromCode(accountId, promotionCode); + if (paymentInfoByPromCode.isEmpty()) { + // 未使用过推广码,状态有效 + checkCouponsVO.setStatus("valid"); + checkCouponsVO.setDiscountedPrice(price * (1 - productCoupons.getPercentOff() / 100)); + } else { + // 已使用过推广码,状态无效 + if (paymentInfoByPromCode.size() > 1) { + log.error("用户[{}]多次成功使用优惠码[{}]", accountId, promotionCode); + } + log.info("用户[{}]已成功使用过优惠码[{}]", accountId, promotionCode); + String msg = BusinessException.getMessageFromResource("one.time.limit.per.customer"); + checkCouponsVO.setMessage(msg); + checkCouponsVO.setStatus("invalid"); + } + } + }else { + String msg = BusinessException.getMessageFromResource("this.promotion.code.is.invalid"); + checkCouponsVO.setMessage(msg); + checkCouponsVO.setStatus("invalid"); + } + return checkCouponsVO; + } + + public ProductCoupons getProductCoupon(String promotionCode, String promotionCodeId){ + Stripe.apiKey = privateKey; + QueryWrapper qw = new QueryWrapper<>(); + // 1、从数据库查找promotionCode对应的promotionCodeId + if (!StringUtil.isNullOrEmpty(promotionCode)){ + qw.eq("promotion_code", promotionCode); + } + if (!StringUtil.isNullOrEmpty(promotionCodeId)){ + qw.eq("promotion_code_id", promotionCodeId); + } + return productCouponsMapper.selectOne(qw); + } + + public String retrieveCoupon(String couponId){ + Stripe.apiKey = privateKey; + try { + Coupon coupon = Coupon.retrieve(couponId); + log.info("retrieve的coupon: {}", coupon); + return JSON.toJSONString(coupon); + } catch (StripeException e) { + throw new RuntimeException(e); + } + } + + public String retrievePromotionCode(String promotionCode){ + Stripe.apiKey = privateKey; + try { + ProductCoupons productCoupon = getProductCoupon(promotionCode, null); + PromotionCode retrieve = PromotionCode.retrieve(productCoupon.getPromotionCodeId()); + log.info("retrieve的promotionCode: {}", retrieve); + return JSON.toJSONString(retrieve); + } catch (StripeException e) { + throw new RuntimeException(e); + } + } + + public IPage getAllCoupons(QueryCouponsPageDTO queryCouponsPageDTO){ + // 分页 + 按条件查询 + QueryWrapper queryWrapper = new QueryWrapper<>(); + if (!StringUtil.isNullOrEmpty(queryCouponsPageDTO.getOrderById()) && queryCouponsPageDTO.getOrderById().equals("DESC")){ + queryWrapper.orderByDesc("id"); + } + if (!StringUtil.isNullOrEmpty(queryCouponsPageDTO.getPromotionCode())){ + queryWrapper.like("promotion_code", queryCouponsPageDTO.getPromotionCode()); + } + if (Objects.nonNull(queryCouponsPageDTO.getIsExpired())){ + long epochSecond = Instant.now().getEpochSecond(); + if (queryCouponsPageDTO.getIsExpired()){ + queryWrapper.lt("redeem_by", epochSecond); + }else { + queryWrapper.gt("redeem_by", epochSecond); + } + } + if (!StringUtil.isNullOrEmpty(queryCouponsPageDTO.getCooperator())){ + queryWrapper.like("cooperator", queryCouponsPageDTO.getCooperator()); + } + if (!StringUtil.isNullOrEmpty(queryCouponsPageDTO.getStartTime())){ + queryWrapper.gt("create_time", queryCouponsPageDTO.getStartTime()); + } + if (!StringUtil.isNullOrEmpty(queryCouponsPageDTO.getEndTime())){ + queryWrapper.lt("create_time", queryCouponsPageDTO.getEndTime()); + } + + return productCouponsMapper.selectPage(new Page<>(queryCouponsPageDTO.getPage(), queryCouponsPageDTO.getSize()), queryWrapper); + } + + @Transactional + public void deleteCoupon(Long id){ + Stripe.apiKey = privateKey; + ProductCoupons productCoupons = productCouponsMapper.selectById(id); + if (Objects.isNull(productCoupons)){ + throw new BusinessException("unknown promotion code"); + } + try { + Coupon coupon = Coupon.retrieve(productCoupons.getCouponId()); + coupon.delete(); + log.info("coupon {} 删除成功", productCoupons.getCouponId()); + productCouponsMapper.deleteById(id); + } catch (StripeException e) { + log.error("未知coupons,无法通过couponId: {} 获得Coupons", productCoupons.getCouponId()); + } + } + +} diff --git a/src/main/resources/application-prod.properties b/src/main/resources/application-prod.properties index 9c4a1e76..37d58f16 100644 --- a/src/main/resources/application-prod.properties +++ b/src/main/resources/application-prod.properties @@ -50,7 +50,7 @@ access.python.port=9990 access.python.generate_sr_port=9990 access.python.address=http://18.167.251.121:9990 -minio.endpoint=https://www.minio.aida.com.hk:12024 +minio.endpoint=https://www.minio-api.aida.com.hk minio.accessKey=admin minio.secretKey=Aidlab123123! minio.bucketName.clothing=aida-clothing diff --git a/src/main/resources/mapper/primary/PaymentInfoMapper.xml b/src/main/resources/mapper/primary/PaymentInfoMapper.xml index c358a722..5a295797 100644 --- a/src/main/resources/mapper/primary/PaymentInfoMapper.xml +++ b/src/main/resources/mapper/primary/PaymentInfoMapper.xml @@ -48,6 +48,7 @@ SELECT p.id, a.user_name payer, + a.user_email email, p.payment_type platform, p.payer_total, p.type, @@ -149,6 +150,44 @@ AND p.transaction_id NOT LIKE 'cs_test%' + +