Merge branch 'dev/dev_xp' into dev/dev

This commit is contained in:
2025-04-15 10:23:30 +08:00
17 changed files with 416 additions and 31 deletions

View File

@@ -113,4 +113,10 @@ public class PaymentTask {
} }
} }
// @Scheduled(cron = "0 */5 * * * *") // Run every 5 minutes
public void calcCouponsCommission(){
log.info("优惠券佣金计算定时器");
affiliateService.calcCouponsCommission();
}
} }

View File

@@ -4,8 +4,13 @@ import com.ai.da.common.response.Response;
import com.ai.da.common.utils.DateUtil; import com.ai.da.common.utils.DateUtil;
import com.ai.da.common.utils.RedisUtil; import com.ai.da.common.utils.RedisUtil;
import com.ai.da.common.utils.SendEmailUtil; import com.ai.da.common.utils.SendEmailUtil;
import com.ai.da.mapper.primary.entity.ProductCoupons;
import com.ai.da.model.dto.CreateCouponDTO;
import com.ai.da.model.dto.ProductPurchaseDTO; import com.ai.da.model.dto.ProductPurchaseDTO;
import com.ai.da.model.dto.QueryCouponsPageDTO;
import com.ai.da.model.vo.CheckCouponsVO;
import com.ai.da.service.StripeService; import com.ai.da.service.StripeService;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.paypal.http.HttpResponse; import com.paypal.http.HttpResponse;
import com.paypal.payments.Refund; import com.paypal.payments.Refund;
import com.stripe.exception.StripeException; import com.stripe.exception.StripeException;
@@ -99,6 +104,36 @@ public class StripeController {
stripeService.cancelSubscription(subscriptionId, reason); stripeService.cancelSubscription(subscriptionId, reason);
return Response.success("success"); return Response.success("success");
} }
@ApiOperation("创建推广码")
@PostMapping("/createCoupon")
public Response<String> createCoupon(@Valid @RequestBody CreateCouponDTO createCouponDTO){
return Response.success(stripeService.createCoupon(createCouponDTO));
}
@ApiOperation("检查推广码")
@GetMapping("/checkCoupon")
public Response<CheckCouponsVO> checkCoupon(@RequestParam String promotionCode, @RequestParam Long price){
return Response.success(stripeService.checkProductCoupon(promotionCode, price));
}
@ApiOperation("获取所有推广码")
@PostMapping("/getAllCoupons")
public Response<IPage<ProductCoupons>> getAllCoupons(@RequestBody QueryCouponsPageDTO queryCouponsPageDTO){
return Response.success(stripeService.getAllCoupons(queryCouponsPageDTO));
}
@ApiOperation("检索优惠券")
@GetMapping("/retrieveCoupon")
public Response<String> retrieveCoupon(@RequestParam String couponId){
return Response.success(stripeService.retrieveCoupon(couponId));
}
@ApiOperation("检索推广码")
@GetMapping("/retrievePromotionCode")
public Response<String> retrievePromotionCode(@RequestParam String retrievePromotionCode){
return Response.success(stripeService.retrievePromotionCode(retrievePromotionCode));
}
/*@ApiOperation("临时 取消订阅") /*@ApiOperation("临时 取消订阅")
@GetMapping("/cancelSubscriptionTemp") @GetMapping("/cancelSubscriptionTemp")
public Response<String> cancelSubscriptionTemp(@RequestParam String subscriptionId) { public Response<String> cancelSubscriptionTemp(@RequestParam String subscriptionId) {

View File

@@ -29,4 +29,6 @@ public interface PaymentInfoMapper extends BaseMapper<PaymentInfo> {
List<Map<String, String>> getCountries(); List<Map<String, String>> getCountries();
int insertIgnore(@Param("paymentInfo")PaymentInfo paymentInfo); int insertIgnore(@Param("paymentInfo")PaymentInfo paymentInfo);
List<PaymentInfo> selectPaidPaymentsByAccountAndPromotion(Long accountId, String promotionCode);
} }

View File

@@ -47,4 +47,6 @@ public class PaymentInfo extends BaseEntity{
private String country; private String country;
private String city; private String city;
private String promotionCode;
} }

View File

@@ -17,7 +17,7 @@ public class ProductCoupons extends BaseEntity{
// 对应的推广码 // 对应的推广码
private String promotionCode; private String promotionCode;
// 最大兑换次数 // 最大兑换次数
private Integer maxRedemptions; private Long maxRedemptions;
// 优惠券的折扣 // 优惠券的折扣
private float percentOff; private float percentOff;
// 佣金比例 // 佣金比例
@@ -38,11 +38,12 @@ public class ProductCoupons extends BaseEntity{
public ProductCoupons() { public ProductCoupons() {
} }
public ProductCoupons(String couponId, Long redeemBy, String promotionCodeId, String promotionCode, float percentOff, float commissionRate) { public ProductCoupons(String couponId, Long redeemBy, String promotionCodeId, String promotionCode, Long maxRedemptions, float percentOff, float commissionRate) {
this.couponId = couponId; this.couponId = couponId;
this.redeemBy = redeemBy; this.redeemBy = redeemBy;
this.promotionCodeId = promotionCodeId; this.promotionCodeId = promotionCodeId;
this.promotionCode = promotionCode; this.promotionCode = promotionCode;
this.maxRedemptions = maxRedemptions;
this.percentOff = percentOff; this.percentOff = percentOff;
this.commissionRate = commissionRate; this.commissionRate = commissionRate;
} }

View File

@@ -0,0 +1,20 @@
package com.ai.da.model.dto;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotNull;
@Data
public class CreateCouponDTO {
@ApiModelProperty("折扣率")
@NotNull(message = "Please set the percentOff")
private Float percentOff;
@ApiModelProperty("佣金比例")
@NotNull(message = "Please set the commissionRate.")
private Float commissionRate;
@ApiModelProperty("推广码到期时间 秒级时间戳")
private Long timestamp;
@ApiModelProperty("推广码最大使用次数")
private Long maxRedemptions;
}

View File

@@ -30,4 +30,7 @@ public class ProductPurchaseDTO {
@ApiModelProperty("使用Alipay-HK时需要选择 ALIPAYHK || ALIPAYCN") @ApiModelProperty("使用Alipay-HK时需要选择 ALIPAYHK || ALIPAYCN")
private String wallet; private String wallet;
@ApiModelProperty("优惠码")
private String promotionCode;
} }

View File

@@ -0,0 +1,18 @@
package com.ai.da.model.vo;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
public class CheckCouponsVO {
@ApiModelProperty("expired || invalid || valid")
private String status;
private String message;
private Float discountedPrice;
}

View File

@@ -29,4 +29,6 @@ public interface AffiliateService extends IService<Affiliate> {
Affiliate getByAccountId(Long accountId); Affiliate getByAccountId(Long accountId);
void commissionCalculation(Integer year, Integer month); void commissionCalculation(Integer year, Integer month);
void calcCouponsCommission();
} }

View File

@@ -32,4 +32,6 @@ public interface PaymentInfoService extends IService<PaymentInfo> {
void updatePaymentStatusById(Long id, String status, String content); void updatePaymentStatusById(Long id, String status, String content);
PageBaseResponse<OrderListVO> getPaymentInfo(QueryPageByTimeDTO queryPageByTimeDTO); PageBaseResponse<OrderListVO> getPaymentInfo(QueryPageByTimeDTO queryPageByTimeDTO);
List<PaymentInfo> getPaymentInfoByPromCode(Long accountId, String promCode);
} }

View File

@@ -1,7 +1,12 @@
package com.ai.da.service; package com.ai.da.service;
import com.ai.da.mapper.primary.entity.ProductCoupons;
import com.ai.da.mapper.primary.entity.SubscriptionInfo; import com.ai.da.mapper.primary.entity.SubscriptionInfo;
import com.ai.da.model.dto.CreateCouponDTO;
import com.ai.da.model.dto.ProductPurchaseDTO; import com.ai.da.model.dto.ProductPurchaseDTO;
import com.ai.da.model.dto.QueryCouponsPageDTO;
import com.ai.da.model.vo.CheckCouponsVO;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.stripe.exception.StripeException; import com.stripe.exception.StripeException;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
@@ -51,4 +56,16 @@ public interface StripeService {
// Map getIp(HttpServletRequest request); // Map getIp(HttpServletRequest request);
String getStackTrace(Exception e, int maxLines); String getStackTrace(Exception e, int maxLines);
String createCoupon(CreateCouponDTO createCouponDTO);
CheckCouponsVO checkProductCoupon(String promotionCode, Long price);
ProductCoupons getProductCoupon(String promotionCode, String promotionCodeId);
String retrieveCoupon(String couponId);
String retrievePromotionCode(String promotionCode);
IPage<ProductCoupons> getAllCoupons(QueryCouponsPageDTO queryCouponsPageDTO);
} }

View File

@@ -10,6 +10,7 @@ import com.ai.da.common.utils.RedisUtil;
import com.ai.da.common.utils.SendEmailUtil; import com.ai.da.common.utils.SendEmailUtil;
import com.ai.da.mapper.primary.AffiliateIncomeMapper; import com.ai.da.mapper.primary.AffiliateIncomeMapper;
import com.ai.da.mapper.primary.AffiliateMapper; import com.ai.da.mapper.primary.AffiliateMapper;
import com.ai.da.mapper.primary.ProductCouponsMapper;
import com.ai.da.mapper.primary.SubscriptionInfoMapper; import com.ai.da.mapper.primary.SubscriptionInfoMapper;
import com.ai.da.mapper.primary.entity.*; import com.ai.da.mapper.primary.entity.*;
import com.ai.da.model.dto.AffiliateEmailParamsDTO; import com.ai.da.model.dto.AffiliateEmailParamsDTO;
@@ -17,10 +18,7 @@ import com.ai.da.model.dto.AffiliateQueryDTO;
import com.ai.da.model.vo.AffiliateInvitationDetailsVO; import com.ai.da.model.vo.AffiliateInvitationDetailsVO;
import com.ai.da.model.vo.AffiliateVO; import com.ai.da.model.vo.AffiliateVO;
import com.ai.da.model.vo.AuthPrincipalVo; import com.ai.da.model.vo.AuthPrincipalVo;
import com.ai.da.service.AccountService; import com.ai.da.service.*;
import com.ai.da.service.AffiliateService;
import com.ai.da.service.OrderInfoService;
import com.ai.da.service.PaymentInfoService;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@@ -34,9 +32,7 @@ import org.springframework.util.CollectionUtils;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.*;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function; import java.util.function.Function;
@Service @Service
@@ -45,19 +41,18 @@ public class AffiliateServiceImpl extends ServiceImpl<AffiliateMapper, Affiliate
@Resource @Resource
private OrderInfoService orderInfoService; private OrderInfoService orderInfoService;
@Resource @Resource
private AccountService accountService; private AccountService accountService;
@Resource @Resource
private PaymentInfoService paymentInfoService; private PaymentInfoService paymentInfoService;
@Resource @Resource
private SubscriptionInfoMapper subscriptionInfoMapper; private SubscriptionInfoMapper subscriptionInfoMapper;
@Resource @Resource
private AffiliateIncomeMapper affiliateIncomeMapper; private AffiliateIncomeMapper affiliateIncomeMapper;
@Resource
private StripeService stripeService;
@Resource
private ProductCouponsMapper productCouponsMapper;
@Resource @Resource
private RedisUtil redisUtil; private RedisUtil redisUtil;
@@ -328,5 +323,47 @@ public class AffiliateServiceImpl extends ServiceImpl<AffiliateMapper, Affiliate
return baseMapper.selectOne(queryWrapper); 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<PaymentInfo> queryWrapper = new QueryWrapper<>();
if (!StringUtil.isNullOrEmpty(lastTime)){
queryWrapper.gt("create_time", lastTime)
.lt("create_time", currentTime)
.isNotNull("promotion_code");
}
List<PaymentInfo> paymentInfos = paymentInfoService.getBaseMapper().selectList(queryWrapper);
// key:推广码, value:用户支付的金额
HashMap<String, Float> 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<String, Float> entry : codeAmount.entrySet()){
String promotionCode = entry.getKey();
ProductCoupons productCoupons = stripeService.getProductCoupon(promotionCode, null);
if (!Objects.isNull(productCoupons)){
// 2、计算支付金额的总和更新totalEarningscommissionunpaidCommission
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);
}
} }

View File

@@ -6,6 +6,7 @@ import com.ai.da.common.response.PageBaseResponse;
import com.ai.da.mapper.primary.PaymentInfoMapper; import com.ai.da.mapper.primary.PaymentInfoMapper;
import com.ai.da.mapper.primary.entity.OrderInfo; import com.ai.da.mapper.primary.entity.OrderInfo;
import com.ai.da.mapper.primary.entity.PaymentInfo; import com.ai.da.mapper.primary.entity.PaymentInfo;
import com.ai.da.mapper.primary.entity.ProductCoupons;
import com.ai.da.model.dto.AlipayHKCallbackDTO; import com.ai.da.model.dto.AlipayHKCallbackDTO;
import com.ai.da.model.dto.QueryPageByTimeDTO; import com.ai.da.model.dto.QueryPageByTimeDTO;
import com.ai.da.model.vo.OrderListVO; import com.ai.da.model.vo.OrderListVO;
@@ -232,20 +233,27 @@ public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, Payme
qw.eq("transaction_id", invoiceId); qw.eq("transaction_id", invoiceId);
PaymentInfo paymentInfo = baseMapper.selectOne(qw); PaymentInfo paymentInfo = baseMapper.selectOne(qw);
String status = invoice.getStatus(); String status = invoice.getStatus();
// 判断是否有优惠码
String promotionCode = null;
if (Objects.nonNull(invoice.getDiscount()) && !StringUtil.isNullOrEmpty(invoice.getDiscount().getPromotionCode())){
ProductCoupons productCoupon = stripeService.getProductCoupon(null, invoice.getDiscount().getPromotionCode());
promotionCode = productCoupon.getPromotionCode();
}
// 判断当前支付是否已经被记录,确保同一个支付不会被重复记录 // 判断当前支付是否已经被记录,确保同一个支付不会被重复记录
if (Objects.isNull(paymentInfo)){ if (Objects.isNull(paymentInfo)){
String orderNo; String orderNo;
try { try {
String chargeId = invoice.getCharge(); if (invoice.getBillingReason().equals("manual")){
orderNo = Charge.retrieve(chargeId).getDescription().replace("AiDA - ", ""); // 手动创建的发票针对one-time支付
// if (invoice.getBillingReason().equals("manual")){ // orderNo = invoice.getLines().getData().get(0).getPrice().getMetadata().get("orderId");
// // 手动创建的发票针对one-time支付 // 当支付失败时chargeId为空
//// orderNo = invoice.getLines().getData().get(0).getPrice().getMetadata().get("orderId"); String chargeId = invoice.getCharge();
// }else { orderNo = Charge.retrieve(chargeId).getDescription().replace("AiDA - ", "");
// String subscriptionId = invoice.getSubscription(); }else {
// // 从subscription中获取orderNo String subscriptionId = invoice.getSubscription();
// orderNo = Subscription.retrieve(subscriptionId).getDescription().replace("AiDA - ", ""); // 从subscription中获取orderNo
// } orderNo = Subscription.retrieve(subscriptionId).getDescription().replace("AiDA - ", "");
}
} catch (StripeException e) { } catch (StripeException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
@@ -281,6 +289,7 @@ public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, Payme
paymentInfo.setPaymentMethod(paymentMethod.get("paymentMethod")); paymentInfo.setPaymentMethod(paymentMethod.get("paymentMethod"));
paymentInfo.setLast4(paymentMethod.get("last4")); paymentInfo.setLast4(paymentMethod.get("last4"));
paymentInfo.setHostedInvoiceUrl(invoice.getHostedInvoiceUrl()); paymentInfo.setHostedInvoiceUrl(invoice.getHostedInvoiceUrl());
paymentInfo.setPromotionCode(promotionCode);
paymentInfo.setCreateTime(LocalDateTime.now()); paymentInfo.setCreateTime(LocalDateTime.now());
if (!Objects.isNull(orderByOrderNo)){ if (!Objects.isNull(orderByOrderNo)){
paymentInfo.setCountry(orderByOrderNo.getCountry()); paymentInfo.setCountry(orderByOrderNo.getCountry());
@@ -291,6 +300,7 @@ public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, Payme
log.info("Payment Info insert affect rows:{}", row); log.info("Payment Info insert affect rows:{}", row);
}else { }else {
paymentInfo.setTradeState(status); paymentInfo.setTradeState(status);
paymentInfo.setPromotionCode(promotionCode);
paymentInfo.setUpdateTime(LocalDateTime.now()); paymentInfo.setUpdateTime(LocalDateTime.now());
baseMapper.updateById(paymentInfo); baseMapper.updateById(paymentInfo);
} }
@@ -421,4 +431,8 @@ public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, Payme
return PageBaseResponse.success(orderListVOIPage); return PageBaseResponse.success(orderListVOIPage);
} }
} }
public List<PaymentInfo> getPaymentInfoByPromCode(Long accountId, String promCode){
return baseMapper.selectPaidPaymentsByAccountAndPromotion(accountId, promCode);
}
} }

View File

@@ -8,14 +8,20 @@ import com.ai.da.common.utils.DateUtil;
import com.ai.da.common.utils.SendEmailUtil; import com.ai.da.common.utils.SendEmailUtil;
import com.ai.da.mapper.primary.AccountMapper; import com.ai.da.mapper.primary.AccountMapper;
import com.ai.da.mapper.primary.PaymentInfoMapper; 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.SubscriptionInfoMapper;
import com.ai.da.mapper.primary.entity.*; 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.ProductPurchaseDTO;
import com.ai.da.model.dto.QueryCouponsPageDTO;
import com.ai.da.model.dto.SubscriptionEmailParamsDTO; import com.ai.da.model.dto.SubscriptionEmailParamsDTO;
import com.ai.da.model.vo.CheckCouponsVO;
import com.ai.da.service.*; import com.ai.da.service.*;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; 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.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.stripe.Stripe; import com.stripe.Stripe;
import com.stripe.exception.SignatureVerificationException; import com.stripe.exception.SignatureVerificationException;
@@ -67,6 +73,8 @@ public class StripeServiceImpl implements StripeService {
private SubscriptionInfoMapper subscriptionInfoMapper; private SubscriptionInfoMapper subscriptionInfoMapper;
@Resource @Resource
private PaymentInfoMapper paymentInfoMapper; private PaymentInfoMapper paymentInfoMapper;
@Resource
private ProductCouponsMapper productCouponsMapper;
@Value("${stripe.private-key}") @Value("${stripe.private-key}")
private String privateKey; private String privateKey;
@@ -85,6 +93,9 @@ public class StripeServiceImpl implements StripeService {
public String pay(ProductPurchaseDTO productPurchaseDTO, HttpServletRequest request) { public String pay(ProductPurchaseDTO productPurchaseDTO, HttpServletRequest request) {
Stripe.apiKey = privateKey; Stripe.apiKey = privateKey;
//创建支付信息得到url
// 一次性支付和周期扣款需要区分mode: payment || subscription || setup
SessionCreateParams.Builder sessionBuilder = new SessionCreateParams.Builder();
ProductEnum productEnum; ProductEnum productEnum;
switch (productPurchaseDTO.getProductName()){ switch (productPurchaseDTO.getProductName()){
case "CreditsPurchase": case "CreditsPurchase":
@@ -105,6 +116,8 @@ public class StripeServiceImpl implements StripeService {
default: default:
throw new BusinessException("unknown subscription type"); throw new BusinessException("unknown subscription type");
} }
// 只有订阅时才允许使用推广码优惠
// sessionBuilder.setAllowPromotionCodes(true);
break; break;
default: default:
throw new BusinessException("unknown product type"); throw new BusinessException("unknown product type");
@@ -131,12 +144,11 @@ public class StripeServiceImpl implements StripeService {
String priceId = getPrice(productEnum.getPrice(), productId, payType, productPurchaseDTO.getSubscribeType()); String priceId = getPrice(productEnum.getPrice(), productId, payType, productPurchaseDTO.getSubscribeType());
// 获取或创建customer // 获取或创建customer
String customerId = getCustomer(account.getUserName(), account.getUserEmail()); String customerId = getCustomer(account.getUserName(), account.getUserEmail());
log.info("customerId:{}", customerId);
// 获取自定义订单号 // 获取自定义订单号
String orderId = orderInfo.getOrderNo(); String orderId = orderInfo.getOrderNo();
//创建支付信息得到url
// 一次性支付和周期扣款需要区分mode: payment || subscription || setup
SessionCreateParams.Builder sessionBuilder = new SessionCreateParams.Builder();
if (payType.equals("recurring")){ if (payType.equals("recurring")){
sessionBuilder.setMode(SessionCreateParams.Mode.SUBSCRIPTION); sessionBuilder.setMode(SessionCreateParams.Mode.SUBSCRIPTION);
sessionBuilder.setSubscriptionData(SessionCreateParams.SubscriptionData.builder().setDescription("AiDA - " + orderId).build()); sessionBuilder.setSubscriptionData(SessionCreateParams.SubscriptionData.builder().setDescription("AiDA - " + orderId).build());
@@ -148,8 +160,6 @@ public class StripeServiceImpl implements StripeService {
} }
sessionBuilder.setPaymentMethodConfiguration(paymentMethodConfigurationId); sessionBuilder.setPaymentMethodConfiguration(paymentMethodConfigurationId);
// sessionBuilder.addPaymentMethodType(SessionCreateParams.PaymentMethodType.ALIPAY);
// sessionBuilder.addPaymentMethodType(SessionCreateParams.PaymentMethodType.CARD);
sessionBuilder.setCustomer(customerId); sessionBuilder.setCustomer(customerId);
sessionBuilder.setSuccessUrl(productPurchaseDTO.getReturnUrl());//可自定义成功页面 sessionBuilder.setSuccessUrl(productPurchaseDTO.getReturnUrl());//可自定义成功页面
sessionBuilder.setLocale(account.getLanguage().equals("CHINESE_SIMPLIFIED") ? SessionCreateParams.Locale.ZH : SessionCreateParams.Locale.EN); sessionBuilder.setLocale(account.getLanguage().equals("CHINESE_SIMPLIFIED") ? SessionCreateParams.Locale.ZH : SessionCreateParams.Locale.EN);
@@ -160,6 +170,16 @@ public class StripeServiceImpl implements StripeService {
.build()); .build());
sessionBuilder.putMetadata("orderId", orderId); //通过订单号关联用于检索支付信息(可选) sessionBuilder.putMetadata("orderId", orderId); //通过订单号关联用于检索支付信息(可选)
// 添加优惠券
String promotionCode = productPurchaseDTO.getPromotionCode();
if (!StringUtil.isNullOrEmpty(promotionCode)){
ProductCoupons productCoupon = checkProductCoupon(promotionCode);;
if (productCoupon != null){
sessionBuilder.addDiscount(SessionCreateParams.Discount.builder()
.setPromotionCode(productCoupon.getPromotionCodeId()).build());
}
}
Session session = Session.create(sessionBuilder.build()); Session session = Session.create(sessionBuilder.build());
List<String> paymentMethodTypes = session.getPaymentMethodTypes(); List<String> paymentMethodTypes = session.getPaymentMethodTypes();
log.info("paymentMethodTypes: {}", paymentMethodTypes); log.info("paymentMethodTypes: {}", paymentMethodTypes);
@@ -171,6 +191,8 @@ public class StripeServiceImpl implements StripeService {
// 更新order信息 // 更新order信息
orderInfoService.updateOrderNoById(orderInfo.getId(), orderId); orderInfoService.updateOrderNoById(orderInfo.getId(), orderId);
return session.getUrl(); return session.getUrl();
} catch (BusinessException e) {
throw e;
} catch (Exception e) { } catch (Exception e) {
log.error("创建支付会话出现异常:", e); log.error("创建支付会话出现异常:", e);
} }
@@ -1346,4 +1368,192 @@ public class StripeServiceImpl implements StripeService {
} }
return sb.toString(); 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)
.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());
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 checkProductCoupon(String promotionCode){
Stripe.apiKey = privateKey;
Long accountId = UserContext.getUserHolder().getId();
// 1、从数据库查找promotionCode对应的promotionCodeId
ProductCoupons productCoupons = productCouponsMapper.selectOne(new QueryWrapper<ProductCoupons>().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<PaymentInfo> 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<ProductCoupons>().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<PaymentInfo> 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<ProductCoupons> 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<ProductCoupons> getAllCoupons(QueryCouponsPageDTO queryCouponsPageDTO){
// 分页 + 按条件查询
QueryWrapper<ProductCoupons> 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);
}
} }

View File

@@ -167,11 +167,21 @@
INSERT IGNORE INTO INSERT IGNORE INTO
t_payment_info (order_no, transaction_id, payment_type, trade_state, payer_total, t_payment_info (order_no, transaction_id, payment_type, trade_state, payer_total,
type, content, notified, payment_method, last4, hosted_invoice_url, type, content, notified, payment_method, last4, hosted_invoice_url,
country, city, ip_address, create_time) country, city, ip_address, promotion_code, create_time)
VALUES (#{paymentInfo.orderNo}, #{paymentInfo.transactionId}, #{paymentInfo.paymentType}, VALUES (#{paymentInfo.orderNo}, #{paymentInfo.transactionId}, #{paymentInfo.paymentType},
#{paymentInfo.tradeState}, #{paymentInfo.payerTotal},#{paymentInfo.type}, #{paymentInfo.content}, #{paymentInfo.tradeState}, #{paymentInfo.payerTotal},#{paymentInfo.type}, #{paymentInfo.content},
#{paymentInfo.notified},#{paymentInfo.paymentMethod}, #{paymentInfo.last4},#{paymentInfo.hostedInvoiceUrl}, #{paymentInfo.notified},#{paymentInfo.paymentMethod}, #{paymentInfo.last4},#{paymentInfo.hostedInvoiceUrl},
#{paymentInfo.country},#{paymentInfo.city},#{paymentInfo.ipAddress},#{paymentInfo.createTime}); #{paymentInfo.country},#{paymentInfo.city},#{paymentInfo.ipAddress},#{paymentInfo.promotionCode},#{paymentInfo.createTime});
</insert> </insert>
<select id="selectPaidPaymentsByAccountAndPromotion" resultType="com.ai.da.mapper.primary.entity.PaymentInfo">
SELECT pi.*
FROM t_order_info oi
LEFT JOIN t_payment_info pi ON oi.order_no = pi.order_no
WHERE oi.account_id = #{accountId}
AND pi.promotion_code = #{promotionCode}
AND pi.trade_state = 'paid'
</select>
</mapper> </mapper>

View File

@@ -153,6 +153,9 @@ generate.result.below.standard=The quality of the generated images currently fal
partial.design.failed=Partial design failed, Please try again later. partial.design.failed=Partial design failed, Please try again later.
email.count.limit=Rate limit reached. Retry in 1 hour. email.count.limit=Rate limit reached. Retry in 1 hour.
model.path.cannot.be.empty=Model path cannot be empty. model.path.cannot.be.empty=Model path cannot be empty.
this.promotion.code.has.expired=This promotion code has expired.
this.promotion.code.is.invalid=This promotion code is invalid.
one.time.limit.per.customer=This code has already been redeemed. Promo codes are limited to one-time use per customer.
# 可能会报异常 # 可能会报异常
# Informative: # Informative:

View File

@@ -149,6 +149,9 @@ generate.result.below.standard=当前生成的图像质量低于标准。请考
partial.design.failed=局部设计失败。请稍后重试。 partial.design.failed=局部设计失败。请稍后重试。
email.count.limit=您的账号触发邮件发送频率限制,请一小时后重试。 email.count.limit=您的账号触发邮件发送频率限制,请一小时后重试。
model.path.cannot.be.empty=模特路径不能为空 model.path.cannot.be.empty=模特路径不能为空
this.promotion.code.has.expired=该促销码已过期。
this.promotion.code.is.invalid=该促销码无效。
one.time.limit.per.customer=该码已兑换。每个促销码每位用户仅限使用一次。
# 可能会报异常 # 可能会报异常
# Informative: # Informative: