Merge branch 'master' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into feature/user-social

 Conflicts:
	yudao-admin-server/pom.xml
	yudao-dependencies/pom.xml
	yudao-user-server/src/main/resources/application-dev.yaml
	yudao-user-server/src/main/resources/application-local.yaml
This commit is contained in:
YunaiV
2021-10-30 09:30:18 +08:00
159 changed files with 6708 additions and 75 deletions

View File

@ -32,6 +32,10 @@
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-biz-sms</artifactId>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-biz-pay</artifactId>
</dependency>
<!-- Web 相关 -->
<dependency>
@ -73,6 +77,12 @@
<artifactId>yudao-spring-boot-starter-mq</artifactId>
</dependency>
<!-- 服务保障相关 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-protection</artifactId>
</dependency>
<!-- Test 测试相关 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>

View File

@ -1,6 +1,6 @@
package cn.iocoder.yudao.coreservice.modules.infra.convert.logger;
import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiAccessLogCreateDTO;
import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiAccessLogCreateReqDTO;
import cn.iocoder.yudao.coreservice.modules.infra.dal.dataobject.logger.InfApiAccessLogDO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@ -10,6 +10,6 @@ public interface InfApiAccessLogCoreConvert {
InfApiAccessLogCoreConvert INSTANCE = Mappers.getMapper(InfApiAccessLogCoreConvert.class);
InfApiAccessLogDO convert(ApiAccessLogCreateDTO bean);
InfApiAccessLogDO convert(ApiAccessLogCreateReqDTO bean);
}

View File

@ -1,6 +1,6 @@
package cn.iocoder.yudao.coreservice.modules.infra.convert.logger;
import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiErrorLogCreateDTO;
import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiErrorLogCreateReqDTO;
import cn.iocoder.yudao.coreservice.modules.infra.dal.dataobject.logger.InfApiErrorLogDO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@ -10,6 +10,6 @@ public interface InfApiErrorLogCoreConvert {
InfApiErrorLogCoreConvert INSTANCE = Mappers.getMapper(InfApiErrorLogCoreConvert.class);
InfApiErrorLogDO convert(ApiErrorLogCreateDTO bean);
InfApiErrorLogDO convert(ApiErrorLogCreateReqDTO bean);
}

View File

@ -11,7 +11,7 @@ import lombok.ToString;
/**
* 参数配置表
*
* @author ruoyi
* @author 芋道源码
*/
@TableName("inf_config")
@Data

View File

@ -3,6 +3,7 @@ package cn.iocoder.yudao.coreservice.modules.infra.dal.dataobject.logger;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.coreservice.modules.infra.enums.logger.InfApiErrorLogProcessStatusEnum;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
@ -25,6 +26,7 @@ public class InfApiErrorLogDO extends BaseDO {
/**
* 编号
*/
@TableId
private Long id;
/**
* 用户编号

View File

@ -1,18 +1,16 @@
package cn.iocoder.yudao.coreservice.modules.infra.service.logger.impl;
import cn.iocoder.yudao.coreservice.modules.infra.convert.logger.InfApiAccessLogCoreConvert;
import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiAccessLogCreateDTO;
import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiAccessLogCreateReqDTO;
import cn.iocoder.yudao.coreservice.modules.infra.dal.dataobject.logger.InfApiAccessLogDO;
import cn.iocoder.yudao.coreservice.modules.infra.dal.mysql.logger.InfApiAccessLogCoreMapper;
import cn.iocoder.yudao.coreservice.modules.infra.service.logger.InfApiAccessLogCoreService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import java.util.concurrent.Future;
/**
* API 访问日志 Service 实现类
@ -29,7 +27,7 @@ public class InfApiAccessLogCoreServiceImpl implements InfApiAccessLogCoreServic
@Override
@Async
public void createApiAccessLogAsync(ApiAccessLogCreateDTO createDTO) {
public void createApiAccessLogAsync(ApiAccessLogCreateReqDTO createDTO) {
InfApiAccessLogDO apiAccessLog = InfApiAccessLogCoreConvert.INSTANCE.convert(createDTO);
apiAccessLogMapper.insert(apiAccessLog);
}

View File

@ -5,15 +5,13 @@ import cn.iocoder.yudao.coreservice.modules.infra.dal.dataobject.logger.InfApiEr
import cn.iocoder.yudao.coreservice.modules.infra.dal.mysql.logger.InfApiErrorLogCoreMapper;
import cn.iocoder.yudao.coreservice.modules.infra.enums.logger.InfApiErrorLogProcessStatusEnum;
import cn.iocoder.yudao.coreservice.modules.infra.service.logger.InfApiErrorLogCoreService;
import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiErrorLogCreateDTO;
import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiErrorLogCreateReqDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import java.util.concurrent.Future;
/**
* API 错误日志 Service 实现类
@ -30,7 +28,7 @@ public class InfApiErrorLogCoreServiceImpl implements InfApiErrorLogCoreService
@Override
@Async
public void createApiErrorLogAsync(ApiErrorLogCreateDTO createDTO) {
public void createApiErrorLogAsync(ApiErrorLogCreateReqDTO createDTO) {
InfApiErrorLogDO apiErrorLog = InfApiErrorLogCoreConvert.INSTANCE.convert(createDTO);
apiErrorLog.setProcessStatus(InfApiErrorLogProcessStatusEnum.INIT.getStatus());
apiErrorLogMapper.insert(apiErrorLog);

View File

@ -0,0 +1,22 @@
package cn.iocoder.yudao.coreservice.modules.pay.convert.order;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderDO;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderExtensionDO;
import cn.iocoder.yudao.coreservice.modules.pay.service.order.dto.PayOrderCreateReqDTO;
import cn.iocoder.yudao.coreservice.modules.pay.service.order.dto.PayOrderSubmitReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderUnifiedReqDTO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper
public interface PayOrderCoreConvert {
PayOrderCoreConvert INSTANCE = Mappers.getMapper(PayOrderCoreConvert.class);
PayOrderDO convert(PayOrderCreateReqDTO bean);
PayOrderExtensionDO convert(PayOrderSubmitReqDTO bean);
PayOrderUnifiedReqDTO convert2(PayOrderSubmitReqDTO bean);
}

View File

@ -0,0 +1,6 @@
/**
* 提供 POJO 类的实体转换
*
* 目前使用 MapStruct 框架
*/
package cn.iocoder.yudao.coreservice.modules.pay.convert;

View File

@ -0,0 +1 @@
<http://www.iocoder.cn/Spring-Boot/MapStruct/?yudao>

View File

@ -0,0 +1,62 @@
package cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
/**
* 支付应用 DO
* 一个商户下,可能会有多个支付应用。例如说,京东有京东商城、京东到家等等
* 不过一般来说,一个商户,只有一个应用哈~
*
* 即 PayMerchantDO : PayAppDO = 1 : n
*
* @author 芋道源码
*/
@TableName("pay_app")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PayAppDO extends BaseDO {
/**
* 应用编号,数据库自增
*/
@TableId
private Long id;
/**
* 应用名
*/
private String name;
/**
* 状态
*
* 枚举 {@link CommonStatusEnum}
*/
private Integer status;
/**
* 备注
*/
private String remark;
/**
* 支付结果的回调地址
*/
private String payNotifyUrl;
/**
* 退款结果的回调地址
*/
private String refundNotifyUrl;
/**
* 商户编号
*
* 关联 {@link PayMerchantDO#getId()}
*/
private Long merchantId;
}

View File

@ -0,0 +1,68 @@
package cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import lombok.*;
/**
* 支付渠道 DO
* 一个应用下,会有多种支付渠道,例如说微信支付、支付宝支付等等
*
* 即 PayAppDO : PayChannelDO = 1 : n
*
* @author 芋道源码
*/
@Data
@TableName(value = "pay_channel", autoResultMap = true)
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PayChannelDO extends BaseDO {
/**
* 渠道编号,数据库自增
*/
private Long id;
/**
* 渠道编码
*
* 枚举 {@link PayChannelEnum}
*/
private String code;
/**
* 状态
*
* 枚举 {@link CommonStatusEnum}
*/
private Integer status;
/**
* 渠道费率,单位:百分比
*/
private Double feeRate;
/**
* 商户编号
*
* 关联 {@link PayMerchantDO#getId()}
*/
private Long merchantId;
/**
* 应用编号
*
* 关联 {@link PayAppDO#getId()}
*/
private Long appId;
/**
* 支付渠道配置
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private PayClientConfig config;
}

View File

@ -0,0 +1,53 @@
package cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
/**
* 支付商户信息 DO
* 目前暂时没有特别的用途,主要为未来多商户提供基础。
*
* @author 芋道源码
*/
@Data
@TableName("pay_merchant")
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PayMerchantDO extends BaseDO {
/**
* 商户编号,数据库自增
*/
@TableId
private Long id;
/**
* 商户号
* 例如说M233666999
*/
private String no;
/**
* 商户全称
*/
private String name;
/**
* 商户简称
*/
private String shortName;
/**
* 状态
*
* 枚举 {@link CommonStatusEnum}
*/
private Integer status;
/**
* 备注
*/
private String remark;
}

View File

@ -0,0 +1,49 @@
package cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.notify;
import cn.iocoder.yudao.coreservice.modules.pay.enums.notify.PayNotifyStatusEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
/**
* 商户支付、退款等的通知 Log
* 每次通知时,都会在该表中,记录一次 Log方便排查问题
*
* @author 芋道源码
*/
@TableName("pay_notify_log")
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PayNotifyLogDO extends BaseDO {
/**
* 日志编号,自增
*/
private Long id;
/**
* 通知任务编号
*
* 关联 {@link PayNotifyTaskDO#getId()}
*/
private Long taskId;
/**
* 第几次被通知
*
* 对应到 {@link PayNotifyTaskDO#getNotifyTimes()}
*/
private Integer notifyTimes;
/**
* HTTP 响应结果
*/
private String response;
/**
* 支付通知状态
*
* 外键 {@link PayNotifyStatusEnum}
*/
private Integer status;
}

View File

@ -0,0 +1,99 @@
package cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.notify;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayAppDO;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayMerchantDO;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderDO;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayRefundDO;
import cn.iocoder.yudao.coreservice.modules.pay.enums.notify.PayNotifyStatusEnum;
import cn.iocoder.yudao.coreservice.modules.pay.enums.notify.PayNotifyTypeEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.util.Date;
/**
* 商户支付、退款等的通知
* 在支付系统收到支付渠道的支付、退款的结果后,需要不断的通知到业务系统,直到成功。
*
* @author 芋道源码
*/
@TableName("pay_notify_task")
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
public class PayNotifyTaskDO extends BaseDO {
/**
* 通知频率,单位为秒。
*
* 算上首次的通知,实际是一共 1 + 8 = 9 次。
*/
public static final Integer[] NOTIFY_FREQUENCY = new Integer[]{
15, 15, 30, 180,
1800, 1800, 1800, 3600
};
/**
* 编号,自增
*/
private Long id;
/**
* 商户编号
*
* 关联 {@link PayMerchantDO#getId()}
*/
private Long merchantId;
/**
* 应用编号
*
* 关联 {@link PayAppDO#getId()}
*/
private Long appId;
/**
* 通知类型
*
* 外键 {@link PayNotifyTypeEnum}
*/
private Integer type;
/**
* 数据编号,根据不同 type 进行关联:
*
* 1. {@link PayNotifyTypeEnum#ORDER} 时,关联 {@link PayOrderDO#getId()}
* 2. {@link PayNotifyTypeEnum#REFUND} 时,关联 {@link PayRefundDO#getId()}
*/
private Long dataId;
/**
* 商户订单编号
*/
private String merchantOrderId;
/**
* 通知状态
*
* 外键 {@link PayNotifyStatusEnum}
*/
private Integer status;
/**
* 下一次通知时间
*/
private Date nextNotifyTime;
/**
* 最后一次执行时间
*/
private Date lastExecuteTime;
/**
* 当前通知次数
*/
private Integer notifyTimes;
/**
* 最大可通知次数
*/
private Integer maxNotifyTimes;
/**
* 通知地址
*/
private String notifyUrl;
}

View File

@ -0,0 +1,162 @@
package cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayAppDO;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayChannelDO;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayMerchantDO;
import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayOrderNotifyStatusEnum;
import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayOrderRefundStatusEnum;
import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayOrderStatusEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.util.Date;
/**
* 支付订单 DO
*
* @author 芋道源码
*/
@TableName("pay_order")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PayOrderDO extends BaseDO {
/**
* 订单编号,数据库自增
*/
private Long id;
/**
* 商户编号
*
* 关联 {@link PayMerchantDO#getId()}
*/
private Long merchantId;
/**
* 应用编号
*
* 关联 {@link PayAppDO#getId()}
*/
private Long appId;
/**
* 渠道编号
*
* 关联 {@link PayChannelDO#getId()}
*/
private Long channelId;
/**
* 渠道编码
*
* 枚举 {@link PayChannelEnum}
*/
private String channelCode;
// ========== 商户相关字段 ==========
/**
* 商户订单编号
* 例如说,内部系统 A 的订单号。需要保证每个 PayMerchantDO 唯一
*/
private String merchantOrderId;
/**
* 商品标题
*/
private String subject;
/**
* 商品描述信息
*/
private String body;
/**
* 异步通知地址
*/
private String notifyUrl;
/**
* 通知商户支付结果的回调状态
*
* 枚举 {@link PayOrderNotifyStatusEnum}
*/
private Integer notifyStatus;
// /**
// * 商户拓展参数
// */
// private Map<String, String> merchantExtras;
// ========== 订单相关字段 ==========
/**
* 支付金额,单位:分
*/
private Long amount;
/**
* 渠道手续费,单位:百分比
*
* 冗余 {@link PayChannelDO#getFeeRate()}
*/
private Double channelFeeRate;
/**
* 渠道手续金额,单位:分
*/
private Long channelFeeAmount;
/**
* 支付状态
*
* 枚举 {@link PayOrderStatusEnum}
*/
private Integer status;
/**
* 用户 IP
*/
private String userIp;
/**
* 订单失效时间
*/
private Date expireTime;
/**
* 订单支付成功时间
*/
private Date successTime;
/**
* 订单支付通知时间,即支付渠道的通知时间
*/
private Date notifyTime;
/**
* 支付成功的订单拓展单编号
*
* 关联 {@link PayOrderDO#getId()}
*/
private Long successExtensionId;
// ========== 退款相关字段 ==========
/**
* 退款状态
*
* 枚举 {@link PayOrderRefundStatusEnum}
*/
private Integer refundStatus;
/**
* 退款次数
*/
private Integer refundTimes;
/**
* 退款总金额,单位:分
*/
private Long refundAmount;
// ========== 渠道相关字段 ==========
/**
* 渠道用户编号
*
* 例如说,微信 openid、支付宝账号
*/
private String channelUserId;
/**
* 渠道订单号
*/
private String channelOrderNo;
}

View File

@ -0,0 +1,82 @@
package cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayChannelDO;
import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayOrderStatusEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import lombok.*;
import java.util.Map;
/**
* 支付订单拓展 DO
*
*
* @author 芋道源码
*/
@TableName("pay_order_extension")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PayOrderExtensionDO extends BaseDO {
/**
* 订单拓展编号,数据库自增
*/
private Long id;
/**
* 支付订单号,根据规则生成
* 调用支付渠道时,使用该字段作为对接的订单号。
* 1. 调用微信支付 https://api.mch.weixin.qq.com/pay/unifiedorder 时,使用该字段作为 out_trade_no
* 2. 调用支付宝 https://opendocs.alipay.com/apis 时,使用该字段作为 out_trade_no
*
* 例如说P202110132239124200055
*/
private String no;
/**
* 订单号
*
* 关联 {@link PayOrderDO#getId()}
*/
private Long orderId;
/**
* 渠道编号
*
* 关联 {@link PayChannelDO#getId()}
*/
private Long channelId;
/**
* 渠道编码
*/
private String channelCode;
/**
* 用户 IP
*/
private String userIp;
/**
* 支付状态
*
* 枚举 {@link PayOrderStatusEnum}
* 注意,只包含上述枚举的 WAITING 和 SUCCESS
*/
private Integer status;
/**
* 支付渠道的额外参数
*
* 参见 https://www.pingxx.com/api/支付渠道%20extra%20参数说明.html
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private Map<String, String> channelExtras;
/**
* 支付渠道异步通知的内容
*
* 在支持成功后,会记录回调的数据
*/
private String channelNotifyData;
}

View File

@ -0,0 +1,128 @@
package cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayAppDO;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayChannelDO;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayMerchantDO;
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import lombok.Data;
import java.util.Date;
/**
* 支付退款单 DO
* 一个支付订单,可以拥有多个支付退款单
*
* 即 PayOrderDO : PayRefundDO = 1 : n
*
* @author 芋道源码
*/
@Data
public class PayRefundDO extends BaseDO {
/**
* 退款单编号,数据库自增
*/
private Long id;
/**
* 退款单号,根据规则生成
*
* 例如说R202109181134287570000
*/
private String no;
/**
* 商户编号
*
* 关联 {@link PayMerchantDO#getId()}
*/
private Long merchantId;
/**
* 应用编号
*
* 关联 {@link PayAppDO#getId()}
*/
private Long appId;
/**
* 渠道编号
*
* 关联 {@link PayChannelDO#getId()}
*/
private Long channelId;
/**
* 商户编码
*
* 枚举 {@link PayChannelEnum}
*/
private String channelCode;
/**
* 订单编号
*
* 关联 {@link PayOrderDO#getId()}
*/
private Long orderId;
// ========== 商户相关字段 ==========
/**
* 商户退款订单号
* 例如说,内部系统 A 的退款订单号。需要保证每个 PayMerchantDO 唯一 TODO 芋艿:需要在测试下
*/
private String merchantRefundNo;
// /**
// * 商户拓展参数
// */
// private String merchantExtra;
/**
* 异步通知地址
*/
private String notifyUrl;
/**
* 通知商户退款结果的回调状态
* TODO 芋艿0 未发送 1 已发送
*/
private Integer notifyStatus;
// ========== 退款相关字段 ==========
/**
* 退款状态
*
* TODO 芋艿:状态枚举
*/
private Integer status;
/**
* 用户 IP
*/
private String userIp;
/**
* 退款金额,单位:分
*/
private Long amount;
/**
* 退款原因
*/
private String reason;
/**
* 订单退款成功时间
*/
private Date successTime;
/**
* 退款失效时间
*/
private Date expireTime;
/**
* 支付渠道的额外参数
*
* 参见 https://www.pingxx.com/api/Refunds%20退款概述.html
*/
private String channelExtra;
// ========== 渠道相关字段 ==========
/**
* 渠道订单号
*/
private String channelOrderNo;
/**
* 渠道退款号
*/
private String channelRefundNo;
}

View File

@ -0,0 +1,9 @@
package cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.merchant;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayAppDO;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface PayAppCoreMapper extends BaseMapperX<PayAppDO> {
}

View File

@ -0,0 +1,20 @@
package cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.merchant;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayChannelDO;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.Date;
@Mapper
public interface PayChannelCoreMapper extends BaseMapperX<PayChannelDO> {
default PayChannelDO selectByAppIdAndCode(Long appId, String code) {
return selectOne("app_id", appId, "code", code);
}
@Select("SELECT id FROM pay_channel WHERE update_time > #{maxUpdateTime} LIMIT 1")
Long selectExistsByUpdateTimeAfter(Date maxUpdateTime);
}

View File

@ -0,0 +1,9 @@
package cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.notify;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.notify.PayNotifyLogDO;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface PayNotifyLogCoreMapper extends BaseMapperX<PayNotifyLogDO> {
}

View File

@ -0,0 +1,30 @@
package cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.notify;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.notify.PayNotifyTaskDO;
import cn.iocoder.yudao.coreservice.modules.pay.enums.notify.PayNotifyStatusEnum;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.apache.ibatis.annotations.Mapper;
import java.util.Date;
import java.util.List;
@Mapper
public interface PayNotifyTaskCoreMapper extends BaseMapperX<PayNotifyTaskDO> {
/**
* 获得需要通知的 PayNotifyTaskDO 记录。需要满足如下条件:
*
* 1. status 非成功
* 2. nextNotifyTime 小于当前时间
*
* @return PayTransactionNotifyTaskDO 数组
*/
default List<PayNotifyTaskDO> selectListByNotify() {
return selectList(new QueryWrapper<PayNotifyTaskDO>()
.in("status", PayNotifyStatusEnum.WAITING.getStatus(), PayNotifyStatusEnum.REQUEST_SUCCESS.getStatus(),
PayNotifyStatusEnum.REQUEST_FAILURE.getStatus())
.le("next_notify_time", new Date()));
}
}

View File

@ -0,0 +1,22 @@
package cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.order;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderDO;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderExtensionDO;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface PayOrderCoreMapper extends BaseMapperX<PayOrderDO> {
default PayOrderDO selectByAppIdAndMerchantOrderId(Long appId, String merchantOrderId) {
return selectOne(new QueryWrapper<PayOrderDO>().eq("app_id", appId)
.eq("merchant_order_id", merchantOrderId));
}
default int updateByIdAndStatus(Long id, Integer status, PayOrderDO update) {
return update(update, new QueryWrapper<PayOrderDO>()
.eq("id", id).eq("status", status));
}
}

View File

@ -0,0 +1,20 @@
package cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.order;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderExtensionDO;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface PayOrderExtensionCoreMapper extends BaseMapperX<PayOrderExtensionDO> {
default PayOrderExtensionDO selectByNo(String no) {
return selectOne("no", no);
}
default int updateByIdAndStatus(Long id, Integer status, PayOrderExtensionDO update) {
return update(update, new QueryWrapper<PayOrderExtensionDO>()
.eq("id", id).eq("status", status));
}
}

View File

@ -0,0 +1,19 @@
package cn.iocoder.yudao.coreservice.modules.pay.dal.redis;
import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine;
import org.redisson.api.RLock;
import static cn.iocoder.yudao.framework.redis.core.RedisKeyDefine.KeyTypeEnum.HASH;
/**
* Lock4j Redis Key 枚举类
*
* @author 芋道源码
*/
public interface PayRedisKeyCoreConstants {
RedisKeyDefine PAY_NOTIFY_LOCK = new RedisKeyDefine("通知任务的分布式锁",
"pay_notify:lock:", // 参数来自 DefaultLockKeyBuilder 类
HASH, RLock.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC); // Redisson 的 Lock 锁,使用 Hash 数据结构
}

View File

@ -0,0 +1,39 @@
package cn.iocoder.yudao.coreservice.modules.pay.dal.redis.notify;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Repository;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
import static cn.iocoder.yudao.coreservice.modules.pay.dal.redis.PayRedisKeyCoreConstants.PAY_NOTIFY_LOCK;
/**
* 支付通知的锁 Redis DAO
*
* @author 芋道源码
*/
@Repository
public class PayNotifyLockCoreRedisDAO {
@Resource
private RedissonClient redissonClient;
public void lock(Long id, Long timeoutMillis, Runnable runnable) {
String lockKey = formatKey(id);
RLock lock = redissonClient.getLock(lockKey);
try {
lock.lock(timeoutMillis, TimeUnit.MILLISECONDS);
// 执行逻辑
runnable.run();
} finally {
lock.unlock();
}
}
private static String formatKey(Long id) {
return String.format(PAY_NOTIFY_LOCK.getKeyTemplate(), id);
}
}

View File

@ -0,0 +1,31 @@
package cn.iocoder.yudao.coreservice.modules.pay.enums;
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
/**
* Pay 错误码 Core 枚举类
*
* pay 系统,使用 1-007-000-000 段
*/
public interface PayErrorCodeCoreConstants {
// ========== APP 模块 1-007-000-000 ==========
ErrorCode PAY_APP_NOT_FOUND = new ErrorCode(1007000000, "App 不存在");
ErrorCode PAY_APP_IS_DISABLE = new ErrorCode(1007000002, "App 已经被禁用");
// ========== CHANNEL 模块 1-007-001-000 ==========
ErrorCode PAY_CHANNEL_NOT_FOUND = new ErrorCode(1007001000, "支付渠道的配置不存在");
ErrorCode PAY_CHANNEL_IS_DISABLE = new ErrorCode(1007001001, "支付渠道已经禁用");
ErrorCode PAY_CHANNEL_CLIENT_NOT_FOUND = new ErrorCode(1007001002, "支付渠道的客户端不存在");
// ========== ORDER 模块 1-007-002-000 ==========
ErrorCode PAY_ORDER_NOT_FOUND = new ErrorCode(1007002000, "支付订单不存在");
ErrorCode PAY_ORDER_STATUS_IS_NOT_WAITING = new ErrorCode(1007002001, "支付订单不处于待支付");
ErrorCode PAY_ORDER_STATUS_IS_NOT_SUCCESS = new ErrorCode(1007002002, "支付订单不处于已支付");
ErrorCode PAY_ORDER_ERROR_USER = new ErrorCode(1007002003, "支付订单用户不正确");
// ========== ORDER 模块(拓展单) 1-007-003-000 ==========
ErrorCode PAY_ORDER_EXTENSION_NOT_FOUND = new ErrorCode(1007003000, "支付交易拓展单不存在");
ErrorCode PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING = new ErrorCode(1007003001, "支付交易拓展单不处于待支付");
ErrorCode PAY_ORDER_EXTENSION_STATUS_IS_NOT_SUCCESS = new ErrorCode(1007003002, "支付订单不处于已支付");
}

View File

@ -0,0 +1,32 @@
package cn.iocoder.yudao.coreservice.modules.pay.enums.notify;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 支付通知状态枚举
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public enum PayNotifyStatusEnum {
WAITING(1, "等待通知"),
SUCCESS(2, "通知成功"),
FAILURE(3, "通知失败"), // 多次尝试,彻底失败
REQUEST_SUCCESS(4, "请求成功,但是结果失败"),
REQUEST_FAILURE(5, "请求失败"),
;
/**
* 状态
*/
private final Integer status;
/**
* 名字
*/
private final String name;
}

View File

@ -0,0 +1,28 @@
package cn.iocoder.yudao.coreservice.modules.pay.enums.notify;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 支付通知类型
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public enum PayNotifyTypeEnum {
ORDER(1, "支付单"),
REFUND(2, "退款单"),
;
/**
* 类型
*/
private final Integer type;
/**
* 名字
*/
private final String name;
}

View File

@ -0,0 +1,29 @@
package cn.iocoder.yudao.coreservice.modules.pay.enums.order;
import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 支付订单的通知状态枚举
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public enum PayOrderNotifyStatusEnum implements IntArrayValuable {
NO(0, "未通知"),
SUCCESS(10, "通知成功"),
FAILURE(20, "通知失败")
;
private final Integer status;
private final String name;
@Override
public int[] array() {
return new int[0];
}
}

View File

@ -0,0 +1,29 @@
package cn.iocoder.yudao.coreservice.modules.pay.enums.order;
import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 支付订单的退款状态枚举
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public enum PayOrderRefundStatusEnum implements IntArrayValuable {
NO(0, "未退款"),
SOME(10, "部分退款"),
ALL(20, "全部退款")
;
private final Integer status;
private final String name;
@Override
public int[] array() {
return new int[0];
}
}

View File

@ -0,0 +1,29 @@
package cn.iocoder.yudao.coreservice.modules.pay.enums.order;
import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 支付订单的状态枚举
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public enum PayOrderStatusEnum implements IntArrayValuable {
WAITING(0, "未支付"),
SUCCESS(10, "支付成功"),
CLOSED(20, "支付关闭"), // 未付款交易超时关闭,或支付完成后全额退款 TODO 芋艿:需要优化下
;
private final Integer status;
private final String name;
@Override
public int[] array() {
return new int[0];
}
}

View File

@ -0,0 +1,7 @@
/**
* pay 包下,我们放支付业务,提供业务的支付能力。
* 例如说:商户、应用、支付、退款等等
*
* 缩写pay
*/
package cn.iocoder.yudao.coreservice.modules.pay;

View File

@ -0,0 +1,23 @@
package cn.iocoder.yudao.coreservice.modules.pay.service.merchant;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayAppDO;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
/**
* 支付应用 Core Service 接口
*
* @author 芋道源码
*/
public interface PayAppCoreService {
/**
* 支付应用的合法性
*
* 如果不合法,抛出 {@link ServiceException} 业务异常
*
* @param id 应用编号
* @return 应用信息
*/
PayAppDO validPayApp(Long id);
}

View File

@ -0,0 +1,39 @@
package cn.iocoder.yudao.coreservice.modules.pay.service.merchant;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayChannelDO;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
/**
* 支付渠道 Core Service 接口
*
* @author 芋道源码
*/
public interface PayChannelCoreService {
/**
* 初始化支付客户端
*/
void initPayClients();
/**
* 支付渠道的合法性
*
* 如果不合法,抛出 {@link ServiceException} 业务异常
*
* @param id 渠道编号
* @return 渠道信息
*/
PayChannelDO validPayChannel(Long id);
/**
* 支付渠道的合法性
*
* 如果不合法,抛出 {@link ServiceException} 业务异常
*
* @param appId 应用编号
* @param code 支付渠道
* @return 渠道信息
*/
PayChannelDO validPayChannel(Long appId, String code);
}

View File

@ -0,0 +1,43 @@
package cn.iocoder.yudao.coreservice.modules.pay.service.merchant.impl;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayAppDO;
import cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.merchant.PayAppCoreMapper;
import cn.iocoder.yudao.coreservice.modules.pay.service.merchant.PayAppCoreService;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import javax.validation.Valid;
import static cn.iocoder.yudao.coreservice.modules.pay.enums.PayErrorCodeCoreConstants.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
/**
* 支付应用 Core Service 实现类
*
* @author 芋道源码
*/
@Service
@Valid
@Slf4j
public class PayAppCoreServiceImpl implements PayAppCoreService {
@Resource
private PayAppCoreMapper payAppCoreMapper;
@Override
public PayAppDO validPayApp(Long id) {
PayAppDO app = payAppCoreMapper.selectById(id);
// 校验是否存在
if (app == null) {
throw exception(PAY_APP_NOT_FOUND);
}
// 校验是否禁用
if (CommonStatusEnum.DISABLE.getStatus().equals(app.getStatus())) {
throw exception(PAY_APP_IS_DISABLE);
}
return app;
}
}

View File

@ -0,0 +1,121 @@
package cn.iocoder.yudao.coreservice.modules.pay.service.merchant.impl;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayChannelDO;
import cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.merchant.PayChannelCoreMapper;
import cn.iocoder.yudao.coreservice.modules.pay.enums.PayErrorCodeCoreConstants;
import cn.iocoder.yudao.coreservice.modules.pay.service.merchant.PayChannelCoreService;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import static cn.iocoder.yudao.coreservice.modules.pay.enums.PayErrorCodeCoreConstants.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
/**
* 支付渠道 Core Service 实现类
*
* @author 芋道源码
*/
@Service
@Valid
@Slf4j
public class PayChannelCoreServiceImpl implements PayChannelCoreService {
/**
* 定时执行 {@link #schedulePeriodicRefresh()} 的周期
* 因为已经通过 Redis Pub/Sub 机制,所以频率不需要高
*/
private static final long SCHEDULER_PERIOD = 5 * 60 * 1000L;
/**
* 缓存菜单的最大更新时间,用于后续的增量轮询,判断是否有更新
*/
private volatile Date maxUpdateTime;
@Resource
private PayChannelCoreMapper payChannelCoreMapper;
@Resource
private PayClientFactory payClientFactory;
@Override
@PostConstruct
public void initPayClients() {
// 获取支付渠道,如果有更新
List<PayChannelDO> payChannels = this.loadPayChannelIfUpdate(maxUpdateTime);
if (CollUtil.isEmpty(payChannels)) {
return;
}
// 创建或更新支付 Client
payChannels.forEach(payChannel -> payClientFactory.createOrUpdatePayClient(payChannel.getId(),
payChannel.getCode(), payChannel.getConfig()));
// 写入缓存
assert payChannels.size() > 0; // 断言,避免告警
maxUpdateTime = payChannels.stream().max(Comparator.comparing(BaseDO::getUpdateTime)).get().getUpdateTime();
log.info("[initPayClients][初始化 PayChannel 数量为 {}]", payChannels.size());
}
@Scheduled(fixedDelay = SCHEDULER_PERIOD, initialDelay = SCHEDULER_PERIOD)
public void schedulePeriodicRefresh() {
initPayClients();
}
/**
* 如果支付渠道发生变化,从数据库中获取最新的全量支付渠道。
* 如果未发生变化,则返回空
*
* @param maxUpdateTime 当前支付渠道的最大更新时间
* @return 支付渠道列表
*/
private List<PayChannelDO> loadPayChannelIfUpdate(Date maxUpdateTime) {
// 第一步,判断是否要更新。
if (maxUpdateTime == null) { // 如果更新时间为空,说明 DB 一定有新数据
log.info("[loadPayChannelIfUpdate][首次加载全量支付渠道]");
} else { // 判断数据库中是否有更新的支付渠道
if (payChannelCoreMapper.selectExistsByUpdateTimeAfter(maxUpdateTime) == null) {
return null;
}
log.info("[loadPayChannelIfUpdate][增量加载全量支付渠道]");
}
// 第二步,如果有更新,则从数据库加载所有支付渠道
return payChannelCoreMapper.selectList();
}
@Override
public PayChannelDO validPayChannel(Long id) {
PayChannelDO channel = payChannelCoreMapper.selectById(id);
this.validPayChannel(channel);
return channel;
}
@Override
public PayChannelDO validPayChannel(Long appId, String code) {
PayChannelDO channel = payChannelCoreMapper.selectByAppIdAndCode(appId, code);
this.validPayChannel(channel);
return channel;
}
private void validPayChannel(PayChannelDO channel) {
if (channel == null) {
throw exception(PAY_CHANNEL_NOT_FOUND);
}
if (CommonStatusEnum.DISABLE.getStatus().equals(channel.getStatus())) {
throw exception(PayErrorCodeCoreConstants.PAY_CHANNEL_IS_DISABLE);
}
}
}

View File

@ -0,0 +1,29 @@
package cn.iocoder.yudao.coreservice.modules.pay.service.notify;
import cn.iocoder.yudao.coreservice.modules.pay.service.notify.dto.PayNotifyTaskCreateReqDTO;
import javax.validation.Valid;
/**
* 支付通知 Core Service 接口
*
* @author 芋道源码
*/
public interface PayNotifyCoreService {
/**
* 创建支付通知任务
*
* @param reqDTO 任务信息
*/
void createPayNotifyTask(@Valid PayNotifyTaskCreateReqDTO reqDTO);
/**
* 执行支付通知
*
* 注意,该方法提供给定时任务调用。目前是 yudao-admin-server 进行调用
* @return 通知数量
*/
int executeNotify() throws InterruptedException;
}

View File

@ -0,0 +1,32 @@
package cn.iocoder.yudao.coreservice.modules.pay.service.notify.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotNull;
/**
* 支付通知创建 DTO
*
* @author 芋道源码
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PayNotifyTaskCreateReqDTO {
/**
* 类型
*/
@NotNull(message = "类型不能为空")
private Integer type;
/**
* 数据编号
*/
@NotNull(message = "数据编号不能为空")
private Long dataId;
}

View File

@ -0,0 +1,256 @@
package cn.iocoder.yudao.coreservice.modules.pay.service.notify.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.http.HttpUtil;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.notify.PayNotifyLogDO;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.notify.PayNotifyTaskDO;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderDO;
import cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.notify.PayNotifyLogCoreMapper;
import cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.notify.PayNotifyTaskCoreMapper;
import cn.iocoder.yudao.coreservice.modules.pay.dal.redis.notify.PayNotifyLockCoreRedisDAO;
import cn.iocoder.yudao.coreservice.modules.pay.enums.notify.PayNotifyStatusEnum;
import cn.iocoder.yudao.coreservice.modules.pay.enums.notify.PayNotifyTypeEnum;
import cn.iocoder.yudao.coreservice.modules.pay.service.notify.PayNotifyCoreService;
import cn.iocoder.yudao.coreservice.modules.pay.service.notify.dto.PayNotifyTaskCreateReqDTO;
import cn.iocoder.yudao.coreservice.modules.pay.service.notify.vo.PayNotifyOrderReqVO;
import cn.iocoder.yudao.coreservice.modules.pay.service.notify.vo.PayRefundOrderReqVO;
import cn.iocoder.yudao.coreservice.modules.pay.service.order.PayOrderCoreService;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.date.DateUtils;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import static cn.hutool.core.exceptions.ExceptionUtil.getRootCauseMessage;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.SECOND_MILLIS;
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
/**
* 支付通知 Core Service 实现类
*
* @author 芋道源码
*/
@Service
@Valid
@Slf4j
public class PayNotifyCoreServiceImpl implements PayNotifyCoreService {
/**
* 通知超时时间,单位:秒
*/
public static final int NOTIFY_TIMEOUT = 120;
/**
* {@link #NOTIFY_TIMEOUT} 的毫秒
*/
public static final long NOTIFY_TIMEOUT_MILLIS = 120 * SECOND_MILLIS;
@Resource
@Lazy // 循环依赖,避免报错
private PayOrderCoreService payOrderCoreService;
@Resource
private PayNotifyTaskCoreMapper payNotifyTaskCoreMapper;
@Resource
private PayNotifyLogCoreMapper payNotifyLogCoreMapper;
@Resource
private ThreadPoolTaskExecutor threadPoolTaskExecutor; // TODO 芋艿:未来提供独立的线程池
@Resource
private PayNotifyLockCoreRedisDAO payNotifyLockCoreRedisDAO;
@Resource
@Lazy // 循环依赖(自己依赖自己),避免报错
private PayNotifyCoreServiceImpl self;
@Override
public void createPayNotifyTask(PayNotifyTaskCreateReqDTO reqDTO) {
PayNotifyTaskDO task = new PayNotifyTaskDO();
task.setType(reqDTO.getType()).setDataId(reqDTO.getDataId());
task.setStatus(PayNotifyStatusEnum.WAITING.getStatus()).setNextNotifyTime(new Date())
.setNotifyTimes(0).setMaxNotifyTimes(PayNotifyTaskDO.NOTIFY_FREQUENCY.length + 1);
// 补充 merchantId + appId + notifyUrl 字段
if (Objects.equals(task.getType(), PayNotifyTypeEnum.ORDER.getType())) {
PayOrderDO order = payOrderCoreService.getPayOrder(task.getDataId()); // 不进行非空判断,有问题直接异常
task.setMerchantId(order.getMerchantId()).setAppId(order.getAppId()).
setMerchantOrderId(order.getMerchantOrderId()).setNotifyUrl(order.getNotifyUrl());
} else if (Objects.equals(task.getType(), PayNotifyTypeEnum.REFUND.getType())) {
// TODO 芋艿,需要实现下哈
throw new UnsupportedOperationException("需要实现");
}
// 执行插入
payNotifyTaskCoreMapper.insert(task);
// 异步直接发起任务。虽然会有定时任务扫描,但是会导致延迟
self.executeNotifyAsync(task);
}
@Override
public int executeNotify() throws InterruptedException {
// 获得需要通知的任务
List<PayNotifyTaskDO> tasks = payNotifyTaskCoreMapper.selectListByNotify();
if (CollUtil.isEmpty(tasks)) {
return 0;
}
// 遍历,逐个通知
CountDownLatch latch = new CountDownLatch(tasks.size());
tasks.forEach(task -> threadPoolTaskExecutor.execute(() -> {
try {
executeNotifySync(task);
} finally {
latch.countDown();
}
}));
// 等待完成
this.awaitExecuteNotify(latch);
// 返回执行完成的任务数(成功 + 失败)
return tasks.size();
}
/**
* 等待全部支付通知的完成
* 每 1 秒会打印一次剩余任务数量
*
* @param latch Latch
* @throws InterruptedException 如果被打断
*/
private void awaitExecuteNotify(CountDownLatch latch) throws InterruptedException {
long size = latch.getCount();
for (int i = 0; i < NOTIFY_TIMEOUT; i++) {
if (latch.await(1L, TimeUnit.SECONDS)) {
return;
}
log.info("[awaitExecuteNotify][任务处理中, 总任务数({}) 剩余任务数({})]", size, latch.getCount());
}
log.error("[awaitExecuteNotify][任务未处理完,总任务数({}) 剩余任务数({})]", size, latch.getCount());
}
/**
* 异步执行单个支付通知
*
* @param task 通知任务
*/
@Async
public void executeNotifyAsync(PayNotifyTaskDO task) {
self.executeNotifySync(task); // 使用 self避免事务不发起
}
/**
* 同步执行单个支付通知
*
* @param task 通知任务
*/
public void executeNotifySync(PayNotifyTaskDO task) {
// 分布式锁,避免并发问题
payNotifyLockCoreRedisDAO.lock(task.getId(), NOTIFY_TIMEOUT_MILLIS, () -> {
// 校验,当前任务是否已经被通知过
// 虽然已经通过分布式加锁,但是可能同时满足通知的条件,然后都去获得锁。此时,第一个执行完后,第二个还是能拿到锁,然后会再执行一次。
PayNotifyTaskDO dbTask = payNotifyTaskCoreMapper.selectById(task.getId());
if (DateUtils.afterNow(dbTask.getNextNotifyTime())) {
log.info("[executeNotify][dbTask({}) 任务被忽略,原因是未到达下次通知时间,可能是因为并发执行了]", toJsonString(dbTask));
return;
}
// 执行通知
executeNotify(dbTask);
});
}
@Transactional
public void executeNotify(PayNotifyTaskDO task) {
// 发起回调
CommonResult<?> invokeResult = null;
Throwable invokeException = null;
try {
invokeResult = executeNotifyInvoke(task);
} catch (Throwable e) {
invokeException = e;
}
// 处理
Integer newStatus = this.processNotifyResult(task, invokeResult, invokeException);
// 记录 PayNotifyLog 日志
String response = invokeException != null ? getRootCauseMessage(invokeException) : toJsonString(invokeResult);
payNotifyLogCoreMapper.insert(PayNotifyLogDO.builder().taskId(task.getId())
.notifyTimes(task.getNotifyTimes() + 1).status(newStatus).response(response).build());
}
/**
* 执行单个支付任务的 HTTP 调用
*
* @param task 通知任务
* @return HTTP 响应
*/
private CommonResult<?> executeNotifyInvoke(PayNotifyTaskDO task) {
// 拼接参数
Object request;
if (Objects.equals(task.getType(), PayNotifyTypeEnum.ORDER.getType())) {
request = PayNotifyOrderReqVO.builder().merchantOrderId(task.getMerchantOrderId())
.payOrderId(task.getDataId()).build();
} else if (Objects.equals(task.getType(), PayNotifyTypeEnum.REFUND.getType())) {
request = PayRefundOrderReqVO.builder().merchantOrderId(task.getMerchantOrderId())
.payRefundId(task.getDataId()).build();
} else {
throw new RuntimeException("未知的通知任务类型:" + toJsonString(task));
}
// 请求地址
String response = HttpUtil.post(task.getNotifyUrl(), toJsonString(request),
(int) NOTIFY_TIMEOUT_MILLIS);
// 解析结果
return JsonUtils.parseObject(response, CommonResult.class);
}
/**
* 处理并更新通知结果
*
* @param task 通知任务
* @param invokeResult 通知结果
* @param invokeException 通知异常
* @return 最终任务的状态
*/
private Integer processNotifyResult(PayNotifyTaskDO task, CommonResult<?> invokeResult, Throwable invokeException) {
// 设置通用的更新 PayNotifyTaskDO 的字段
PayNotifyTaskDO updateTask = new PayNotifyTaskDO()
.setId(task.getId())
.setLastExecuteTime(new Date())
.setNotifyTimes(task.getNotifyTimes() + 1);
// 情况一:调用成功
if (invokeResult != null && invokeResult.isSuccess()) {
updateTask.setStatus(PayNotifyStatusEnum.SUCCESS.getStatus());
return updateTask.getStatus();
}
// 情况二:调用失败、调用异常
// 2.1 超过最大回调次数
if (updateTask.getNotifyTimes() >= PayNotifyTaskDO.NOTIFY_FREQUENCY.length) {
updateTask.setStatus(PayNotifyStatusEnum.FAILURE.getStatus());
return updateTask.getStatus();
}
// 2.2 未超过最大回调次数
updateTask.setNextNotifyTime(DateUtils.addDate(Calendar.SECOND, PayNotifyTaskDO.NOTIFY_FREQUENCY[updateTask.getNotifyTimes()]));
updateTask.setStatus(invokeException != null ? PayNotifyStatusEnum.REQUEST_FAILURE.getStatus()
: PayNotifyStatusEnum.REQUEST_SUCCESS.getStatus());
return updateTask.getStatus();
}
private void processNotifySuccess(PayNotifyTaskDO task, PayNotifyTaskDO updateTask) {
payNotifyTaskCoreMapper.updateById(updateTask);
}
}

View File

@ -0,0 +1,28 @@
package cn.iocoder.yudao.coreservice.modules.pay.service.notify.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
@ApiModel(value = "支付单的通知 Request VO", description = "业务方接入支付回调时,使用该 VO 对象")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PayNotifyOrderReqVO {
@ApiModelProperty(value = "商户订单编号", required = true, example = "10")
@NotEmpty(message = "商户订单号不能为空")
private String merchantOrderId;
@ApiModelProperty(value = "支付订单编号", required = true, example = "20")
@NotNull(message = "支付订单编号不能为空")
private Long payOrderId;
}

View File

@ -0,0 +1,28 @@
package cn.iocoder.yudao.coreservice.modules.pay.service.notify.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
@ApiModel(value = "退款单的通知 Request VO", description = "业务方接入退款回调时,使用该 VO 对象")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PayRefundOrderReqVO {
@ApiModelProperty(value = "商户订单编号", required = true, example = "10")
@NotEmpty(message = "商户订单号不能为空")
private String merchantOrderId;
@ApiModelProperty(value = "支付退款编号", required = true, example = "20")
@NotNull(message = "支付退款编号不能为空")
private Long payRefundId;
}

View File

@ -0,0 +1,6 @@
/**
* 这里的 VO 包有点特殊,是提供给接入支付模块的业务,提供回调接口时,可以直接使用 VO
*
* 例如说,支付单的回调,使用
*/
package cn.iocoder.yudao.coreservice.modules.pay.service.notify.vo;

View File

@ -0,0 +1,51 @@
package cn.iocoder.yudao.coreservice.modules.pay.service.order;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderDO;
import cn.iocoder.yudao.coreservice.modules.pay.service.order.dto.PayOrderCreateReqDTO;
import cn.iocoder.yudao.coreservice.modules.pay.service.order.dto.PayOrderSubmitReqDTO;
import cn.iocoder.yudao.coreservice.modules.pay.service.order.dto.PayOrderSubmitRespDTO;
import javax.validation.Valid;
/**
* 支付订单 Core Service
*
* @author 芋道源码
*/
public interface PayOrderCoreService {
/**
* 获得支付单
*
* @param id 支付单编号
* @return 支付单
*/
PayOrderDO getPayOrder(Long id);
/**
* 创建支付单
*
* @param reqDTO 创建请求
* @return 支付单编号
*/
Long createPayOrder(@Valid PayOrderCreateReqDTO reqDTO);
/**
* 提交支付
* 此时,会发起支付渠道的调用
*
* @param reqDTO 提交请求
* @return 提交结果
*/
PayOrderSubmitRespDTO submitPayOrder(@Valid PayOrderSubmitReqDTO reqDTO);
/**
* 通知支付单成功
*
* @param channelId 渠道编号
* @param channelCode 渠道编码
* @param notifyData 通知数据
*/
void notifyPayOrder(Long channelId, String channelCode, String notifyData) throws Exception;
}

View File

@ -0,0 +1,64 @@
package cn.iocoder.yudao.coreservice.modules.pay.service.order.dto;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.util.Date;
/**
* 支付单创建 Request DTO
*/
@Data
public class PayOrderCreateReqDTO implements Serializable {
/**
* 应用编号
*/
@NotNull(message = "应用编号不能为空")
private Long appId;
/**
* 用户 IP
*/
@NotEmpty(message = "用户 IP 不能为空")
private String userIp;
// ========== 商户相关字段 ==========
/**
* 商户订单编号
*/
@NotEmpty(message = "商户订单编号不能为空")
private String merchantOrderId;
/**
* 商品标题
*/
@NotEmpty(message = "商品标题不能为空")
@Length(max = 32, message = "商品标题不能超过 32")
private String subject;
/**
* 商品描述
*/
@NotEmpty(message = "商品描述信息不能为空")
@Length(max = 128, message = "商品描述信息长度不能超过128")
private String body;
// ========== 订单相关字段 ==========
/**
* 支付金额,单位:分
*/
@NotNull(message = "支付金额不能为空")
@DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
private Integer amount;
/**
* 支付过期时间
*/
@NotNull(message = "支付过期时间不能为空")
private Date expireTime;
}

View File

@ -0,0 +1,47 @@
package cn.iocoder.yudao.coreservice.modules.pay.service.order.dto;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.util.Map;
/**
* 支付单提交 Request DTO
*/
@Data
@Accessors(chain = true)
public class PayOrderSubmitReqDTO implements Serializable {
/**
* 应用编号
*/
@NotNull(message = "应用编号不能为空")
private Long appId;
/**
* 支付单编号
*/
@NotNull(message = "支付单编号不能为空")
private Long id;
/**
* 支付渠道
*/
@NotEmpty(message = "支付渠道不能为空")
private String channelCode;
/**
* 用户 IP
*/
@NotEmpty(message = "用户 IP 不能为空")
private String userIp;
/**
* 支付渠道的额外参数
*/
private Map<String, String> channelExtras;
}

View File

@ -0,0 +1,23 @@
package cn.iocoder.yudao.coreservice.modules.pay.service.order.dto;
import lombok.Data;
import java.io.Serializable;
/**
* 支付单提交 Response DTO
*/
@Data
public class PayOrderSubmitRespDTO implements Serializable {
/**
* 支付拓展单的编号
*/
private Long extensionId;
/**
* 调用支付渠道的响应结果
*/
private Object invokeResponse;
}

View File

@ -0,0 +1,242 @@
package cn.iocoder.yudao.coreservice.modules.pay.service.order.impl;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.coreservice.modules.pay.convert.order.PayOrderCoreConvert;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayAppDO;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayChannelDO;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderDO;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderExtensionDO;
import cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.order.PayOrderCoreMapper;
import cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.order.PayOrderExtensionCoreMapper;
import cn.iocoder.yudao.coreservice.modules.pay.enums.notify.PayNotifyTypeEnum;
import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayOrderNotifyStatusEnum;
import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayOrderStatusEnum;
import cn.iocoder.yudao.coreservice.modules.pay.service.merchant.PayAppCoreService;
import cn.iocoder.yudao.coreservice.modules.pay.service.merchant.PayChannelCoreService;
import cn.iocoder.yudao.coreservice.modules.pay.service.notify.PayNotifyCoreService;
import cn.iocoder.yudao.coreservice.modules.pay.service.notify.dto.PayNotifyTaskCreateReqDTO;
import cn.iocoder.yudao.coreservice.modules.pay.service.order.PayOrderCoreService;
import cn.iocoder.yudao.coreservice.modules.pay.service.order.dto.PayOrderCreateReqDTO;
import cn.iocoder.yudao.coreservice.modules.pay.service.order.dto.PayOrderSubmitReqDTO;
import cn.iocoder.yudao.coreservice.modules.pay.service.order.dto.PayOrderSubmitRespDTO;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.pay.config.PayProperties;
import cn.iocoder.yudao.framework.pay.core.client.PayClient;
import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderNotifyRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderUnifiedReqDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import java.util.Date;
import java.util.Objects;
import static cn.iocoder.yudao.coreservice.modules.pay.enums.PayErrorCodeCoreConstants.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
/**
* 支付订单 Core Service 实现类
*
* @author 芋道源码
*/
@Service
@Validated
@Slf4j
public class PayOrderCoreServiceImpl implements PayOrderCoreService {
@Resource
private PayProperties payProperties;
@Resource
private PayAppCoreService payAppCoreService;
@Resource
private PayChannelCoreService payChannelCoreService;
@Resource
private PayNotifyCoreService payNotifyCoreService;
@Resource
private PayClientFactory payClientFactory;
@Resource
private PayOrderCoreMapper payOrderCoreMapper;
@Resource
private PayOrderExtensionCoreMapper payOrderExtensionCoreMapper;
@Override
public PayOrderDO getPayOrder(Long id) {
return payOrderCoreMapper.selectById(id);
}
@Override
public Long createPayOrder(PayOrderCreateReqDTO reqDTO) {
// 校验 App
PayAppDO app = payAppCoreService.validPayApp(reqDTO.getAppId());
// 查询对应的支付交易单是否已经存在。如果是,则直接返回
PayOrderDO order = payOrderCoreMapper.selectByAppIdAndMerchantOrderId(
reqDTO.getAppId(), reqDTO.getMerchantOrderId());
if (order != null) {
log.warn("[createPayOrder][appId({}) merchantOrderId({}) 已经存在对应的支付单({})]", order.getAppId(),
order.getMerchantOrderId(), JsonUtils.toJsonString(order)); // 理论来说,不会出现这个情况
return app.getId();
}
// 创建支付交易单
order = PayOrderCoreConvert.INSTANCE.convert(reqDTO)
.setMerchantId(app.getMerchantId()).setAppId(app.getId());
// 商户相关字段
order.setNotifyUrl(app.getPayNotifyUrl())
.setNotifyStatus(PayOrderNotifyStatusEnum.NO.getStatus());
// 订单相关字段
order.setStatus(PayOrderStatusEnum.WAITING.getStatus());
// 退款相关字段
order.setRefundStatus(PayOrderNotifyStatusEnum.NO.getStatus())
.setRefundTimes(0).setRefundAmount(0L);
payOrderCoreMapper.insert(order);
// 最终返回
return order.getId();
}
@Override
public PayOrderSubmitRespDTO submitPayOrder(PayOrderSubmitReqDTO reqDTO) {
// 校验 App
payAppCoreService.validPayApp(reqDTO.getAppId());
// 校验支付渠道是否有效
PayChannelDO channel = payChannelCoreService.validPayChannel(reqDTO.getAppId(), reqDTO.getChannelCode());
// 校验支付客户端是否正确初始化
PayClient client = payClientFactory.getPayClient(channel.getId());
if (client == null) {
log.error("[submitPayOrder][渠道编号({}) 找不到对应的支付客户端]", channel.getId());
throw exception(PAY_CHANNEL_CLIENT_NOT_FOUND);
}
// 获得 PayOrderDO ,并校验其是否存在
PayOrderDO order = payOrderCoreMapper.selectById(reqDTO.getId());
if (order == null || !Objects.equals(order.getAppId(), reqDTO.getAppId())) { // 是否存在
throw exception(PAY_ORDER_NOT_FOUND);
}
if (!PayOrderStatusEnum.WAITING.getStatus().equals(order.getStatus())) { // 校验状态,必须是待支付
throw exception(PAY_ORDER_STATUS_IS_NOT_WAITING);
}
// 插入 PayOrderExtensionDO
PayOrderExtensionDO orderExtension = PayOrderCoreConvert.INSTANCE.convert(reqDTO)
.setOrderId(order.getId()).setNo(generateOrderExtensionNo())
.setChannelId(channel.getId()).setChannelCode(channel.getCode())
.setStatus(PayOrderStatusEnum.WAITING.getStatus());
payOrderExtensionCoreMapper.insert(orderExtension);
// 调用三方接口
PayOrderUnifiedReqDTO unifiedOrderReqDTO = PayOrderCoreConvert.INSTANCE.convert2(reqDTO);
// 商户相关字段
unifiedOrderReqDTO.setMerchantOrderId(orderExtension.getNo()) // 注意,此处使用的是 PayOrderExtensionDO.no 属性!
.setSubject(order.getSubject()).setBody(order.getBody())
.setNotifyUrl(genChannelPayNotifyUrl(channel));
// 订单相关字段
unifiedOrderReqDTO.setAmount(order.getAmount()).setExpireTime(order.getExpireTime());
CommonResult<?> unifiedOrderResult = client.unifiedOrder(unifiedOrderReqDTO);
unifiedOrderResult.checkError();
// TODO 轮询三方接口,是否已经支付的任务
// 返回成功
return new PayOrderSubmitRespDTO().setExtensionId(orderExtension.getId())
.setInvokeResponse(unifiedOrderResult.getData());
}
/**
* 根据支付渠道的编码,生成支付渠道的回调地址
*
* @param channel 支付渠道
* @return 支付渠道的回调地址
*/
private String genChannelPayNotifyUrl(PayChannelDO channel) {
// _ 转化为 - 的原因,是因为 URL 我们统一采用中划线的原则
return payProperties.getPayNotifyUrl() + "/" + StrUtil.replace(channel.getCode(), "_", "-")
+ "/" + channel.getId();
}
private String generateOrderExtensionNo() {
// wx
// 2014
// 10
// 27
// 20
// 09
// 39
// 5522657
// a690389285100
// 目前的算法
// 时间序列,年月日时分秒 14 位
// 纯随机6 位 TODO 芋艿:此处估计是会有问题的,后续在调整
return DateUtil.format(new Date(), "yyyyMMddHHmmss") + // 时间序列
RandomUtil.randomInt(100000, 999999) // 随机。为什么是这个范围,因为偷懒
;
}
@Override
@Transactional
public void notifyPayOrder(Long channelId, String channelCode, String notifyData) throws Exception {
// TODO 芋艿,记录回调日志
log.info("[notifyPayOrder][channelId({}) 回调数据({})]", channelId, notifyData);
// 校验支付渠道是否有效
PayChannelDO channel = payChannelCoreService.validPayChannel(channelId);
// 校验支付客户端是否正确初始化
PayClient client = payClientFactory.getPayClient(channel.getId());
if (client == null) {
log.error("[notifyPayOrder][渠道编号({}) 找不到对应的支付客户端]", channel.getId());
throw exception(PAY_CHANNEL_CLIENT_NOT_FOUND);
}
// 解析支付结果
PayOrderNotifyRespDTO notifyRespDTO = client.parseOrderNotify(notifyData);
// TODO 芋艿,先最严格的校验。即使调用方重复调用,实际哪个订单已经被重复回调的支付,也返回 false 。也没问题,因为实际已经回调成功了。
// 1.1 查询 PayOrderExtensionDO
PayOrderExtensionDO orderExtension = payOrderExtensionCoreMapper.selectByNo(
notifyRespDTO.getOrderExtensionNo());
if (orderExtension == null) {
throw exception(PAY_ORDER_EXTENSION_NOT_FOUND);
}
if (!PayOrderStatusEnum.WAITING.getStatus().equals(orderExtension.getStatus())) { // 校验状态,必须是待支付
throw exception(PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING);
}
// 1.2 更新 PayOrderExtensionDO
int updateCounts = payOrderExtensionCoreMapper.updateByIdAndStatus(orderExtension.getId(),
PayOrderStatusEnum.WAITING.getStatus(), PayOrderExtensionDO.builder().id(orderExtension.getId())
.status(PayOrderStatusEnum.SUCCESS.getStatus()).channelNotifyData(notifyData).build());
if (updateCounts == 0) { // 校验状态,必须是待支付
throw exception(PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING);
}
log.info("[notifyPayOrder][支付拓展单({}) 更新为已支付]", orderExtension.getId());
// 2.1 判断 PayOrderDO 是否处于待支付
PayOrderDO order = payOrderCoreMapper.selectById(orderExtension.getOrderId());
if (order == null) {
throw exception(PAY_ORDER_NOT_FOUND);
}
if (!PayOrderStatusEnum.WAITING.getStatus().equals(order.getStatus())) { // 校验状态,必须是待支付
throw exception(PAY_ORDER_STATUS_IS_NOT_WAITING);
}
// 2.2 更新 PayOrderDO
updateCounts = payOrderCoreMapper.updateByIdAndStatus(order.getId(), PayOrderStatusEnum.WAITING.getStatus(),
PayOrderDO.builder().status(PayOrderStatusEnum.SUCCESS.getStatus()).channelId(channelId).channelCode(channelCode)
.successTime(notifyRespDTO.getSuccessTime()).successExtensionId(orderExtension.getId())
.channelOrderNo(notifyRespDTO.getChannelOrderNo()).channelUserId(notifyRespDTO.getChannelUserId())
.notifyTime(new Date()).build());
if (updateCounts == 0) { // 校验状态,必须是待支付
throw exception(PAY_ORDER_STATUS_IS_NOT_WAITING);
}
log.info("[notifyPayOrder][支付订单({}) 更新为已支付]", order.getId());
// 3. 插入支付通知记录
payNotifyCoreService.createPayNotifyTask(PayNotifyTaskCreateReqDTO.builder()
.type(PayNotifyTypeEnum.ORDER.getType()).dataId(order.getId()).build());
}
}

View File

@ -0,0 +1 @@
package cn.iocoder.yudao.coreservice.modules.pay.service;

View File

@ -19,9 +19,9 @@ import me.zhyd.oauth.request.AuthRequest;
import me.zhyd.oauth.utils.AuthStateUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.util.List;
import java.util.Objects;
@ -36,7 +36,7 @@ import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString
* @author 芋道源码
*/
@Service
@Valid
@Validated
@Slf4j
public class SysSocialServiceImpl implements SysSocialService {

View File

@ -0,0 +1,38 @@
package cn.iocoder.yudao.coreservice;
import cn.iocoder.yudao.framework.datasource.config.YudaoDataSourceAutoConfiguration;
import cn.iocoder.yudao.framework.mybatis.config.YudaoMybatisAutoConfiguration;
import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceAutoConfiguration;
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
import org.redisson.spring.starter.RedissonAutoConfiguration;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseDbAndRedisIntegrationTest.Application.class)
@ActiveProfiles("integration-test") // 设置使用 application-integration-test 配置文件
public class BaseDbAndRedisIntegrationTest {
@Import({
// DB 配置类
DynamicDataSourceAutoConfiguration.class, // Dynamic Datasource 配置类
YudaoDataSourceAutoConfiguration.class, // 自己的 DB 配置类
DataSourceAutoConfiguration.class, // Spring DB 自动配置类
DataSourceTransactionManagerAutoConfiguration.class, // Spring 事务自动配置类
// MyBatis 配置类
YudaoMybatisAutoConfiguration.class, // 自己的 MyBatis 配置类
MybatisPlusAutoConfiguration.class, // MyBatis 的自动配置类
// Redis 配置类
RedisAutoConfiguration.class, // Spring Redis 自动配置类
YudaoRedisAutoConfiguration.class, // 自己的 Redis 配置类
RedissonAutoConfiguration.class, // Redisson 自动高配置类
})
public static class Application {
}
}

View File

@ -0,0 +1,30 @@
package cn.iocoder.yudao.coreservice;
import cn.iocoder.yudao.framework.datasource.config.YudaoDataSourceAutoConfiguration;
import cn.iocoder.yudao.framework.mybatis.config.YudaoMybatisAutoConfiguration;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceAutoConfiguration;
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseDbIntegrationTest.Application.class)
@ActiveProfiles("integration-test") // 设置使用 application-integration-test 配置文件
public class BaseDbIntegrationTest {
@Import({
// DB 配置类
DynamicDataSourceAutoConfiguration.class, // Dynamic Datasource 配置类
YudaoDataSourceAutoConfiguration.class, // 自己的 DB 配置类
DataSourceAutoConfiguration.class, // Spring DB 自动配置类
DataSourceTransactionManagerAutoConfiguration.class, // Spring 事务自动配置类
// MyBatis 配置类
YudaoMybatisAutoConfiguration.class, // 自己的 MyBatis 配置类
MybatisPlusAutoConfiguration.class, // MyBatis 的自动配置类
})
public static class Application {
}
}

View File

@ -0,0 +1,23 @@
package cn.iocoder.yudao.coreservice;
import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
import org.redisson.spring.starter.RedissonAutoConfiguration;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseRedisIntegrationTest.Application.class)
@ActiveProfiles("integration-test") // 设置使用 application-integration-test 配置文件
public class BaseRedisIntegrationTest {
@Import({
// Redis 配置类
RedisAutoConfiguration.class, // Spring Redis 自动配置类
YudaoRedisAutoConfiguration.class, // 自己的 Redis 配置类
RedissonAutoConfiguration.class, // Redisson 自动高配置类
})
public static class Application {
}
}

View File

@ -0,0 +1,29 @@
package cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXPayClientConfig;
import org.junit.jupiter.api.Test;
public class PayChannelDOTest {
@Test
public void testSerialization() {
PayChannelDO payChannelDO = new PayChannelDO();
// 创建配置
WXPayClientConfig config = new WXPayClientConfig();
config.setAppId("wx041349c6f39b268b");
config.setMchId("1545083881");
config.setApiVersion(WXPayClientConfig.API_VERSION_V2);
config.setMchKey("0alL64UDQdlCwiKZ73ib7ypaIjMns06p");
payChannelDO.setConfig(config);
// 序列化
String text = JsonUtils.toJsonString(payChannelDO);
System.out.println(text);
// 反序列化
payChannelDO = JsonUtils.parseObject(text, PayChannelDO.class);
System.out.println(payChannelDO.getConfig().getClass());
}
}

View File

@ -0,0 +1,56 @@
package cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.merchant;
import cn.hutool.core.io.IoUtil;
import cn.iocoder.yudao.coreservice.BaseDbAndRedisIntegrationTest;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayChannelDO;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXPayClientConfig;
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
import org.junit.jupiter.api.Test;
import javax.annotation.Resource;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.util.List;
@Resource
public class PayChannelCoreMapperTest extends BaseDbAndRedisIntegrationTest {
@Resource
private PayChannelCoreMapper payChannelCoreMapper;
/**
* 插入初始配置
*/
@Test
public void testInsert() throws FileNotFoundException {
PayChannelDO payChannelDO = new PayChannelDO();
payChannelDO.setCode(PayChannelEnum.WX_PUB.getCode());
payChannelDO.setStatus(CommonStatusEnum.ENABLE.getStatus());
payChannelDO.setFeeRate(1D);
payChannelDO.setMerchantId(1L);
payChannelDO.setAppId(6L);
// 配置
WXPayClientConfig config = new WXPayClientConfig();
config.setAppId("wx041349c6f39b268b");
config.setMchId("1545083881");
config.setApiVersion(WXPayClientConfig.API_VERSION_V2);
config.setMchKey("0alL64UDQdlCwiKZ73ib7ypaIjMns06p");
config.setPrivateKeyContent(IoUtil.readUtf8(new FileInputStream("/Users/yunai/Downloads/wx_pay/apiclient_key.pem")));
config.setPrivateCertContent(IoUtil.readUtf8(new FileInputStream("/Users/yunai/Downloads/wx_pay/apiclient_cert.pem")));
config.setApiV3Key("joerVi8y5DJ3o4ttA0o1uH47Xz1u2Ase");
payChannelDO.setConfig(config);
// 执行插入
payChannelCoreMapper.insert(payChannelDO);
}
/**
* 查询所有支付配置,看看是否都是 ok 的
*/
@Test
public void testSelectList() {
List<PayChannelDO> payChannels = payChannelCoreMapper.selectList();
System.out.println(payChannels.size());
}
}

View File

@ -0,0 +1,52 @@
package cn.iocoder.yudao.coreservice.modules.pay.service.order;
import cn.iocoder.yudao.coreservice.BaseDbIntegrationTest;
import cn.iocoder.yudao.coreservice.modules.pay.service.merchant.impl.PayAppCoreServiceImpl;
import cn.iocoder.yudao.coreservice.modules.pay.service.merchant.impl.PayChannelCoreServiceImpl;
import cn.iocoder.yudao.coreservice.modules.pay.service.order.dto.PayOrderCreateReqDTO;
import cn.iocoder.yudao.coreservice.modules.pay.service.order.dto.PayOrderSubmitReqDTO;
import cn.iocoder.yudao.coreservice.modules.pay.service.order.impl.PayOrderCoreServiceImpl;
import cn.iocoder.yudao.framework.common.util.date.DateUtils;
import cn.iocoder.yudao.framework.pay.config.YudaoPayAutoConfiguration;
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Import;
import javax.annotation.Resource;
import java.time.Duration;
@Import({PayOrderCoreServiceImpl.class, PayAppCoreServiceImpl.class,
PayChannelCoreServiceImpl.class, YudaoPayAutoConfiguration.class})
public class PayOrderCoreServiceTest extends BaseDbIntegrationTest {
@Resource
private PayOrderCoreService payOrderCoreService;
@Test
public void testCreatePayOrder() {
// 构造请求
PayOrderCreateReqDTO reqDTO = new PayOrderCreateReqDTO();
reqDTO.setAppId(6L);
reqDTO.setUserIp("127.0.0.1");
reqDTO.setMerchantOrderId(String.valueOf(System.currentTimeMillis()));
reqDTO.setSubject("标题");
reqDTO.setBody("内容");
reqDTO.setAmount(100);
reqDTO.setExpireTime(DateUtils.addTime(Duration.ofDays(1)));
// 发起请求
payOrderCoreService.createPayOrder(reqDTO);
}
@Test
public void testSubmitPayOrder() {
// 构造请求
PayOrderSubmitReqDTO reqDTO = new PayOrderSubmitReqDTO();
reqDTO.setId(10L);
reqDTO.setAppId(6L);
reqDTO.setChannelCode(PayChannelEnum.WX_PUB.getCode());
reqDTO.setUserIp("127.0.0.1");
// 发起请求
payOrderCoreService.submitPayOrder(reqDTO);
}
}

View File

@ -0,0 +1 @@
package cn.iocoder.yudao.coreservice.modules.pay.service;

View File

@ -0,0 +1,92 @@
spring:
main:
lazy-initialization: true # 开启懒加载,加快速度
banner-mode: off # 单元测试,禁用 Banner
--- #################### 数据库相关配置 ####################
spring:
# 数据源配置项
autoconfigure:
exclude:
- com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure # 排除 Druid 的自动配置,使用 dynamic-datasource-spring-boot-starter 配置多数据源
datasource:
druid: # Druid 【监控】相关的全局配置
web-stat-filter:
enabled: true
dynamic: # 多数据源配置
druid: # Druid 【连接池】相关的全局配置
initial-size: 5 # 初始连接数
min-idle: 10 # 最小连接池数量
max-active: 20 # 最大连接池数量
max-wait: 600000 # 配置获取连接等待超时的时间,单位:毫秒
time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒
min-evictable-idle-time-millis: 300000 # 配置一个连接在池中最小生存的时间,单位:毫秒
max-evictable-idle-time-millis: 900000 # 配置一个连接在池中最大生存的时间,单位:毫秒
validation-query: SELECT 1 FROM DUAL # 配置检测连接是否有效
test-while-idle: true
test-on-borrow: false
test-on-return: false
primary: master
datasource:
master:
name: ruoyi-vue-pro
url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.master.name}?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT
driver-class-name: com.mysql.jdbc.Driver
username: root
password: 123456
slave: # 模拟从库,可根据自己需要修改
name: ruoyi-vue-pro
url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.slave.name}?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT
driver-class-name: com.mysql.jdbc.Driver
username: root
password: 123456
# Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
redis:
host: 127.0.0.1 # 地址
port: 6379 # 端口
database: 0 # 数据库索引
mybatis:
lazy-initialization: true # 单元测试,设置 MyBatis Mapper 延迟加载,加速每个单元测试
mybatis-plus:
configuration:
map-underscore-to-camel-case: true # 虽然默认为 true ,但是还是显示去指定下。
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印日志
global-config:
db-config:
id-type: AUTO # 自增 ID
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
mapper-locations: classpath*:mapper/*.xml
type-aliases-package: ${yudao.core-service.base-package}.modules.*.dal.dataobject
--- #################### 定时任务相关配置 ####################
--- #################### 配置中心相关配置 ####################
--- #################### 服务保障相关配置 ####################
# Lock4j 配置项(单元测试,禁用 Lock4j
# Resilience4j 配置项
resilience4j:
ratelimiter:
instances:
backendA:
limit-for-period: 1 # 每个周期内,允许的请求数。默认为 50
limit-refresh-period: 60s # 每个周期的时长,单位:微秒。默认为 500
timeout-duration: 1s # 被限流时,阻塞等待的时长,单位:微秒。默认为 5s
register-health-indicator: true # 是否注册到健康监测
--- #################### 监控相关配置 ####################
--- #################### 芋道相关配置 ####################
yudao:
info:
version: 1.0.0
base-package: cn.iocoder.yudao.adminserver
core-service:
base-package: cn.iocoder.yudao.coreservice

View File

@ -5,14 +5,13 @@ import cn.iocoder.yudao.coreservice.BaseDbUnitTest;
import cn.iocoder.yudao.coreservice.modules.infra.dal.dataobject.logger.InfApiAccessLogDO;
import cn.iocoder.yudao.coreservice.modules.infra.dal.mysql.logger.InfApiAccessLogCoreMapper;
import cn.iocoder.yudao.coreservice.modules.infra.service.logger.impl.InfApiAccessLogCoreServiceImpl;
import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiAccessLogCreateDTO;
import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiAccessLogCreateReqDTO;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.test.core.util.RandomUtils;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Import;
import javax.annotation.Resource;
import java.util.concurrent.Future;
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@ -32,7 +31,7 @@ public class InfApiAccessLogCoreServiceTest extends BaseDbUnitTest {
@Test
public void testCreateApiAccessLogAsync() {
// 准备参数
ApiAccessLogCreateDTO createDTO = RandomUtils.randomPojo(ApiAccessLogCreateDTO.class,
ApiAccessLogCreateReqDTO createDTO = RandomUtils.randomPojo(ApiAccessLogCreateReqDTO.class,
dto -> dto.setUserType(RandomUtil.randomEle(UserTypeEnum.values()).getValue()));
// 调用

View File

@ -5,7 +5,7 @@ import cn.iocoder.yudao.coreservice.BaseDbUnitTest;
import cn.iocoder.yudao.coreservice.modules.infra.dal.dataobject.logger.InfApiErrorLogDO;
import cn.iocoder.yudao.coreservice.modules.infra.dal.mysql.logger.InfApiErrorLogCoreMapper;
import cn.iocoder.yudao.coreservice.modules.infra.service.logger.impl.InfApiErrorLogCoreServiceImpl;
import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiErrorLogCreateDTO;
import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiErrorLogCreateReqDTO;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.test.core.util.RandomUtils;
import org.junit.jupiter.api.Test;
@ -31,7 +31,7 @@ public class InfApiErrorLogCoreServiceTest extends BaseDbUnitTest {
@Test
public void testCreateApiErrorLogAsync() {
// 准备参数
ApiErrorLogCreateDTO createDTO = RandomUtils.randomPojo(ApiErrorLogCreateDTO.class,
ApiErrorLogCreateReqDTO createDTO = RandomUtils.randomPojo(ApiErrorLogCreateReqDTO.class,
dto -> dto.setUserType(RandomUtil.randomEle(UserTypeEnum.values()).getValue()));
// 调用