Merge remote-tracking branch 'origin/dev/dev' into dev/dev

This commit is contained in:
shahaibo
2025-04-29 15:09:09 +08:00
16 changed files with 228 additions and 22 deletions

View File

@@ -51,8 +51,17 @@ public class AffiliateController {
@ApiOperation(value = "审批affiliate申请")
@GetMapping("/approval")
public Response<Boolean> applicationApproval(@RequestParam("id") Long id, @RequestParam("isApproved")Boolean isApproved) {
return Response.success(affiliateService.applicationApproval(id, isApproved));
public Response<Boolean> applicationApproval(@RequestParam("id") Long id,
@RequestParam("isApproved")Boolean isApproved,
@RequestParam("commission") Float commission) {
return Response.success(affiliateService.applicationApproval(id, isApproved, commission));
}
@ApiOperation(value = "更新佣金比例")
@GetMapping("/updateCommission")
public Response<String> updateCommissionPercentage(@RequestParam("id") Long id, @RequestParam("commission") Float commission) {
affiliateService.updateCommissionPercentage(id, commission);
return Response.success("success");
}
/*@ApiOperation(value = "定时计算佣金")

View File

@@ -141,6 +141,14 @@ public class StripeController {
@RequestParam(required = false) String cooperator, @RequestParam(required = false) String remark){
return Response.success(stripeService.updateCouponsInfo(id, paidCommission, cooperator, remark));
}
@ApiOperation("删除推广码")
@GetMapping("/deletePromCode")
public Response<String> deleteCoupon(@RequestParam Long id){
stripeService.deleteCoupon(id);
return Response.success("success");
}
/*@ApiOperation("临时 取消订阅")
@GetMapping("/cancelSubscriptionTemp")
public Response<String> cancelSubscriptionTemp(@RequestParam String subscriptionId) {

View File

@@ -14,6 +14,8 @@ public class Affiliate extends BaseEntity{
// Active活跃 || Inactive过期 || Pending待审批 || Refused(拒绝)
private String status;
private Float commissionPercent;
private Float totalEarnings = 0.00F;
private Float monthlyEarnings = 0.00F;

View File

@@ -22,6 +22,8 @@ public class PaymentInfo extends BaseEntity{
* paid and liquidated means the refund request has been executed.
* expired means the request has been rejected.
* wait means the request is still under processing.
* -------------------------------------------------
* 退款Partial refund 部分退款; Refunded 全部退款
*/
private String tradeState;//交易状态

View File

@@ -1,5 +1,7 @@
package com.ai.da.mapper.primary.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -37,6 +39,9 @@ public class ProductCoupons extends BaseEntity{
// 备注
private String remark;
@TableLogic
private Integer isDeleted;
public ProductCoupons(String couponId, Long redeemBy, String promotionCodeId, String promotionCode, Long maxRedemptions, float percentOff, float commissionRate, String cooperator, String remark) {
this.couponId = couponId;
this.redeemBy = redeemBy;

View File

@@ -13,11 +13,14 @@ public class RefundInfo extends BaseEntity{
private String refundId;//支付系统退款单号(微信)
// 退款必定对应一次具体的收费款项
private String chargeId;
// private Integer totalFee;//原订单金额(分)
private Float totalFee;//原订单金额(分)
// private Integer refund;//退款金额(分)
private Float refund;//退款金额()
private Float refund;//退款金额()
private String reason;//退款原因

View File

@@ -22,6 +22,8 @@ public class SubscriptionEmailParamsDTO {
// 费用
private String totalFee;
private String renewalFee;
// 当前订阅开始时间
private String lastOrderDate;

View File

@@ -18,7 +18,9 @@ public interface AffiliateService extends IService<Affiliate> {
double[] getPersonalMonthlyIncome(int year);
Boolean applicationApproval(Long id, Boolean isApproved);
Boolean applicationApproval(Long id, Boolean isApproved, Float commission);
void updateCommissionPercentage(Long id, Float commission);
void updateAffiliateInfoWithPayment();

View File

@@ -34,4 +34,6 @@ public interface PaymentInfoService extends IService<PaymentInfo> {
PageBaseResponse<OrderListVO> getPaymentInfo(QueryPageByTimeDTO queryPageByTimeDTO);
List<PaymentInfo> getPaymentInfoByPromCode(Long accountId, String promCode);
PaymentInfo updatePaymentRefundStatus(Charge charge);
}

View File

@@ -3,6 +3,8 @@ package com.ai.da.service;
import com.ai.da.mapper.primary.entity.RefundInfo;
import com.baomidou.mybatisplus.extension.service.IService;
import com.stripe.model.Charge;
import com.stripe.model.Refund;
import java.util.List;
@@ -19,4 +21,13 @@ public interface RefundInfoService extends IService<RefundInfo> {
void updateRefundForAliPay(String refundNo, String content, String refundStatus);
void updateRefundForPayPal(Long id, String refundId, String content, String refundStatus);
List<RefundInfo> getByChargeId(String chargeId);
RefundInfo createRefundForStripe(Refund refund);
RefundInfo updateRefundStatusForStripe(Refund refund);
RefundInfo updateRefundForStripe(Charge charge);
}

View File

@@ -70,4 +70,6 @@ public interface StripeService {
String retrievePromotionCode(String promotionCode);
IPage<ProductCoupons> getAllCoupons(QueryCouponsPageDTO queryCouponsPageDTO);
void deleteCoupon(Long id);
}

View File

@@ -147,7 +147,7 @@ public class AffiliateServiceImpl extends ServiceImpl<AffiliateMapper, Affiliate
}
// 审批申请
public Boolean applicationApproval(Long id, Boolean isApproved){
public Boolean applicationApproval(Long id, Boolean isApproved, Float commission){
Affiliate affiliate = baseMapper.selectById(id);
// 1、更新db状态
@@ -156,6 +156,12 @@ public class AffiliateServiceImpl extends ServiceImpl<AffiliateMapper, Affiliate
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);
@@ -175,6 +181,19 @@ public class AffiliateServiceImpl extends ServiceImpl<AffiliateMapper, Affiliate
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
@@ -211,8 +230,8 @@ public class AffiliateServiceImpl extends ServiceImpl<AffiliateMapper, Affiliate
Float payerTotal = paymentInfo.getPayerTotal();
if (payerTotal > 0){
// 分配新用户首次订阅所付费用的25%作为佣金
BigDecimal commission = BigDecimal.valueOf(payerTotal).multiply(new BigDecimal("0.25"));
// 分配新用户首次订阅所付费用 预设的佣金比例 作为佣金
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;

View File

@@ -106,7 +106,11 @@ public class MessageCenterServiceImpl extends ServiceImpl<NotificationMapper, No
IPage<NotificationVO> convert = notificationPage.convert(o -> {
NotificationVO notificationVO = CopyUtil.copyObject(o, NotificationVO.class);
Account senderAccount = accountService.getById(notificationVO.getSenderId());
if (Objects.nonNull(senderAccount)){
notificationVO.setUserName(senderAccount.getUserName());
}else {
notificationVO.setUserName("--");
}
// notificationVO.setSenderUserAvatar(StringUtils.isNullOrEmpty(senderAccount.getAvatar()) ? null : minioUtil.getPreSignedUrl(senderAccount.getAvatar(), CommonConstant.MINIO_IMAGE_EXPIRE_TIME));
if (Objects.isNull(notificationVO.getPortfolioId())){
notificationVO.setPortfolioId(null);
@@ -131,7 +135,7 @@ public class MessageCenterServiceImpl extends ServiceImpl<NotificationMapper, No
notificationVO.setIsRead(1);
}
}else {
String avatar = StringUtil.isNullOrEmpty(senderAccount.getAvatar()) ? CommonConstant.DEFAULT_AVATAR : senderAccount.getAvatar();
String avatar = Objects.isNull(senderAccount) || StringUtil.isNullOrEmpty(senderAccount.getAvatar()) ? CommonConstant.DEFAULT_AVATAR : senderAccount.getAvatar();
notificationVO.setAvatar(minioUtil.getPreSignedUrl(avatar, CommonConstant.MINIO_IMAGE_EXPIRE_TIME));
}
return notificationVO;

View File

@@ -435,4 +435,28 @@ public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, Payme
public List<PaymentInfo> getPaymentInfoByPromCode(Long accountId, String promCode){
return baseMapper.selectPaidPaymentsByAccountAndPromotion(accountId, promCode);
}
public PaymentInfo updatePaymentRefundStatus(Charge charge){
// 判断当前退款是部分退款还是全部退款
QueryWrapper<PaymentInfo> qw = new QueryWrapper<>();
qw.eq("transaction_id", charge.getInvoice());
PaymentInfo paymentInfo = baseMapper.selectOne(qw);
if (Objects.nonNull(paymentInfo)){
String status ;
if (Objects.equals(charge.getAmount(), charge.getAmountRefunded())){
status = "Refunded";
}else if (charge.getAmount() > charge.getAmountRefunded()){
status = "Partial refund";
}else {
status = "Refund Exception";
log.warn("{}, 退款金额高于付款金额, ChargeId为{}", status, charge.getId());
}
if (!paymentInfo.getTradeState().equals(status)){
paymentInfo.setTradeState(status);
paymentInfo.setUpdateTime(LocalDateTime.now());
baseMapper.updateById(paymentInfo);
}
}
return null;
}
}

View File

@@ -10,14 +10,16 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.gson.Gson;
import com.stripe.model.Charge;
import com.stripe.model.Refund;
import io.netty.util.internal.StringUtil;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.time.LocalDateTime;
import java.util.*;
@Service
public class RefundInfoServiceImpl extends ServiceImpl<RefundInfoMapper, RefundInfo> implements RefundInfoService {
@@ -170,4 +172,63 @@ public class RefundInfoServiceImpl extends ServiceImpl<RefundInfoMapper, RefundI
// baseMapper.update(refundInfo, queryWrapper);
baseMapper.updateById(refundInfo);
}
public List<RefundInfo> getByChargeId(String chargeId){
List<RefundInfo> refundInfoList = baseMapper.selectList(new QueryWrapper<RefundInfo>().eq("charge_id", chargeId));
// 判断当前已退款的金额是否全部退完(针对多次部分退款的情况)
if (refundInfoList.isEmpty()){
return new ArrayList<>();
}else {
return refundInfoList;
}
}
public RefundInfo getByRefundId(String refundId){
return baseMapper.selectOne(new QueryWrapper<RefundInfo>().eq("refund_id", refundId));
}
public RefundInfo createRefundForStripe(Refund refund){
// 先确认这个回调是否已经被记录
RefundInfo refundInfo = getByRefundId(refund.getId());
if (Objects.isNull(refundInfo)){
refundInfo = new RefundInfo();
refundInfo.setRefundId(refund.getId());
refundInfo.setChargeId(refund.getCharge());
refundInfo.setRefund(refund.getAmount() / 100f);
refundInfo.setReason(refund.getReason());
refundInfo.setRefundStatus(refund.getStatus());
refundInfo.setCreateTime(LocalDateTime.now());
baseMapper.insert(refundInfo);
}
return refundInfo;
}
public RefundInfo updateRefundStatusForStripe(Refund refund){
RefundInfo refundInfo = getByRefundId(refund.getId());
if (Objects.isNull(refundInfo)){
return null;
}
if (!refundInfo.getRefundStatus().equals(refund.getStatus())){
refundInfo.setRefundStatus(refund.getStatus());
refundInfo.setUpdateTime(LocalDateTime.now());
baseMapper.updateById(refundInfo);
}
return refundInfo;
}
public RefundInfo updateRefundForStripe(Charge charge){
List<RefundInfo> refundInfoList = getByChargeId(charge.getId());
if (!refundInfoList.isEmpty()){
RefundInfo refundInfo = refundInfoList.get(refundInfoList.size() - 1);
if (StringUtil.isNullOrEmpty(refundInfo.getOrderNo())){
String orderNo = charge.getDescription().replace("AiDA - ", "");
refundInfo.setOrderNo(orderNo);
refundInfo.setTotalFee(charge.getAmount() / 100f);
refundInfo.setUpdateTime(LocalDateTime.now());
baseMapper.updateById(refundInfo);
return refundInfo;
}
}
return null;
}
}

View File

@@ -24,6 +24,7 @@ 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.*;
@@ -116,6 +117,16 @@ public class StripeServiceImpl implements StripeService {
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;
@@ -170,16 +181,6 @@ public class StripeServiceImpl implements StripeService {
.build());
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());
List<String> paymentMethodTypes = session.getPaymentMethodTypes();
log.info("paymentMethodTypes: {}", paymentMethodTypes);
@@ -193,6 +194,9 @@ public class StripeServiceImpl implements StripeService {
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);
}
@@ -430,6 +434,26 @@ public class StripeServiceImpl implements StripeService {
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());
@@ -1148,6 +1172,13 @@ public class StripeServiceImpl implements StripeService {
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");
@@ -1385,6 +1416,7 @@ public class StripeServiceImpl implements StripeService {
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());
@@ -1582,4 +1614,22 @@ public class StripeServiceImpl implements StripeService {
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());
}
}
}