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

 Conflicts:
	yudao-dependencies/pom.xml
	yudao-framework/pom.xml
This commit is contained in:
YunaiV
2021-10-30 10:24:58 +08:00
243 changed files with 8264 additions and 468 deletions

View File

@ -29,6 +29,9 @@
<module>yudao-spring-boot-starter-biz-dict</module>
<module>yudao-spring-boot-starter-biz-sms</module>
<module>yudao-spring-boot-starter-activiti</module>
<module>yudao-spring-boot-starter-biz-pay</module>
<module>yudao-spring-boot-starter-biz-weixin</module>
<module>yudao-spring-boot-starter-extension</module>
</modules>
<artifactId>yudao-framework</artifactId>

View File

@ -6,6 +6,8 @@ import java.util.Date;
/**
* 时间工具类
*
* @author 芋道源码
*/
public class DateUtils {
@ -14,6 +16,11 @@ public class DateUtils {
*/
public static final String TIME_ZONE_DEFAULT = "GMT+8";
/**
* 秒转换成毫秒
*/
public static final long SECOND_MILLIS = 1000;
public static final String FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND = "yyyy-MM-dd HH:mm:ss";
public static Date addTime(Duration duration) {
@ -74,4 +81,43 @@ public class DateUtils {
return a.compareTo(b) > 0 ? a : b;
}
public static boolean beforeNow(Date date) {
return date.getTime() < System.currentTimeMillis();
}
public static boolean afterNow(Date date) {
return date.getTime() >= System.currentTimeMillis();
}
/**
* 计算当期时间相差的日期
*
* @param field 日历字段.<br/>eg:Calendar.MONTH,Calendar.DAY_OF_MONTH,<br/>Calendar.HOUR_OF_DAY等.
* @param amount 相差的数值
* @return 计算后的日志
*/
public static Date addDate(int field, int amount) {
return addDate(null, field, amount);
}
/**
* 计算当期时间相差的日期
*
* @param date 设置时间
* @param field 日历字段 例如说,{@link Calendar#DAY_OF_MONTH} 等
* @param amount 相差的数值
* @return 计算后的日志
*/
public static Date addDate(Date date, int field, int amount) {
if (amount == 0) {
return date;
}
Calendar c = Calendar.getInstance();
if (date != null) {
c.setTime(date);
}
c.add(field, amount);
return c.getTime();
}
}

View File

@ -0,0 +1,34 @@
package cn.iocoder.yudao.framework.common.util.io;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import lombok.SneakyThrows;
import java.io.File;
/**
* 文件工具类
*
* @author 芋道源码
*/
public class FileUtils {
/**
* 创建临时文件
* 该文件会在 JVM 退出时,进行删除
*
* @param data 文件内容
* @return 文件
*/
@SneakyThrows
public static File createTempFile(String data) {
// 创建文件,通过 UUID 保证唯一
File file = File.createTempFile(IdUtil.simpleUUID(), null);
// 标记 JVM 退出时,自动删除
file.deleteOnExit();
// 写入内容
FileUtil.writeUtf8String(data, file);
return file;
}
}

View File

@ -29,4 +29,13 @@ public class ObjectUtils {
return obj1.compareTo(obj2) > 0 ? obj1 : obj2;
}
public static <T> T defaultIfNull(T... array) {
for (T item : array) {
if (item != null) {
return item;
}
}
return null;
}
}

View File

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>yudao-framework</artifactId>
<groupId>cn.iocoder.boot</groupId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>yudao-spring-boot-starter-biz-pay</artifactId>
<name>${artifactId}</name>
<description>支付拓展,接入国内多个支付渠道
1. 支付宝,基于官方 SDK 接入
2. 微信支付,基于 weixin-java-pay 接入
</description>
<dependencies>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-common</artifactId>
</dependency>
<!-- Spring 核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- 工具类相关 -->
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<!-- 三方云服务相关 -->
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.17.9.ALL</version>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-pay</artifactId>
<version>4.1.9.B</version>
</dependency>
<!-- TODO 芋艿:清理 -->
<!-- Test 测试相关 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,32 @@
package cn.iocoder.yudao.framework.pay.config;
import lombok.Data;
import org.hibernate.validator.constraints.URL;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.time.Duration;
@ConfigurationProperties(prefix = "yudao.pay")
@Validated
@Data
public class PayProperties {
/**
* 支付回调地址
* 注意,支付渠道统一回调到 payNotifyUrl 地址,由支付模块统一处理;然后,自己的支付模块,在回调 PayAppDO.payNotifyUrl 地址
*/
@NotEmpty(message = "支付回调地址不能为空")
@URL(message = "支付回调地址的格式必须是 URL")
private String payNotifyUrl;
/**
* 退款回调地址
* 注意点,同 {@link #payNotifyUrl} 属性
*/
@NotNull(message = "短信发送频率不能为空")
@URL(message = "退款回调地址的格式必须是 URL")
private String refundNotifyUrl;
}

View File

@ -0,0 +1,22 @@
package cn.iocoder.yudao.framework.pay.config;
import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
import cn.iocoder.yudao.framework.pay.core.client.impl.PayClientFactoryImpl;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 支付配置类
*
* @author 芋道源码
*/
@EnableConfigurationProperties(PayProperties.class)
public class YudaoPayAutoConfiguration {
@Bean
public PayClientFactory payClientFactory() {
return new PayClientFactoryImpl();
}
}

View File

@ -0,0 +1,33 @@
package cn.iocoder.yudao.framework.pay.core.client;
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
import cn.iocoder.yudao.framework.pay.core.enums.PayFrameworkErrorCodeConstants;
import lombok.extern.slf4j.Slf4j;
/**
* 将 API 的错误码,转换为通用的错误码
*
* @see PayCommonResult
* @see PayFrameworkErrorCodeConstants
*
* @author 芋道源码
*/
@Slf4j
public abstract class AbstractPayCodeMapping {
public final ErrorCode apply(String apiCode, String apiMsg) {
if (apiCode == null) {
log.error("[apply][API 错误码为空,请排查]");
return PayFrameworkErrorCodeConstants.EXCEPTION;
}
ErrorCode errorCode = this.apply0(apiCode, apiMsg);
if (errorCode == null) {
log.error("[apply][API 错误码({}) 错误提示({}) 无法匹配]", apiCode, apiMsg);
return PayFrameworkErrorCodeConstants.PAY_UNKNOWN;
}
return errorCode;
}
protected abstract ErrorCode apply0(String apiCode, String apiMsg);
}

View File

@ -0,0 +1,37 @@
package cn.iocoder.yudao.framework.pay.core.client;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderNotifyRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderUnifiedReqDTO;
/**
* 支付客户端,用于对接各支付渠道的 SDK实现发起支付、退款等功能
*
* @author 芋道源码
*/
public interface PayClient {
/**
* 获得渠道编号
*
* @return 渠道编号
*/
Long getId();
/**
* 调用支付渠道,统一下单
*
* @param reqDTO 下单信息
* @return 各支付渠道的返回结果
*/
PayCommonResult<?> unifiedOrder(PayOrderUnifiedReqDTO reqDTO);
/**
* 解析支付单的通知结果
*
* @param data 通知结果
* @return 解析结果
* @throws Exception 解析失败,抛出异常
*/
PayOrderNotifyRespDTO parseOrderNotify(String data) throws Exception;
}

View File

@ -0,0 +1,16 @@
package cn.iocoder.yudao.framework.pay.core.client;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
/**
* 支付客户端的配置,本质是支付渠道的配置
* 每个不同的渠道,需要不同的配置,通过子类来定义
*
* @author 芋道源码
*/
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
// @JsonTypeInfo 注解的作用Jackson 多态
// 1. 序列化到时数据库时,增加 @class 属性。
// 2. 反序列化到内存对象时,通过 @class 属性,可以创建出正确的类型
public interface PayClientConfig {
}

View File

@ -0,0 +1,28 @@
package cn.iocoder.yudao.framework.pay.core.client;
/**
* 支付客户端的工厂接口
*
* @author 芋道源码
*/
public interface PayClientFactory {
/**
* 获得支付客户端
*
* @param channelId 渠道编号
* @return 支付客户端
*/
PayClient getPayClient(Long channelId);
/**
* 创建支付客户端
*
* @param channelId 渠道编号
* @param channelCode 渠道编码
* @param config 支付配置
*/
<Config extends PayClientConfig> void createOrUpdatePayClient(Long channelId, String channelCode,
Config config);
}

View File

@ -0,0 +1,57 @@
package cn.iocoder.yudao.framework.pay.core.client;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.pay.core.enums.PayFrameworkErrorCodeConstants;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
/**
* 支付的 CommonResult 拓展类
*
* 考虑到不同的平台,返回的 code 和 msg 是不同的,所以统一额外返回 {@link #apiCode} 和 {@link #apiMsg} 字段
*
* @author 芋道源码
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayCommonResult<T> extends CommonResult<T> {
/**
* API 返回错误码
*
* 由于第三方的错误码可能是字符串,所以使用 String 类型
*/
private String apiCode;
/**
* API 返回提示
*/
private String apiMsg;
private PayCommonResult() {
}
public static <T> PayCommonResult<T> build(String apiCode, String apiMsg, T data, AbstractPayCodeMapping codeMapping) {
Assert.notNull(codeMapping, "参数 codeMapping 不能为空");
PayCommonResult<T> result = new PayCommonResult<T>().setApiCode(apiCode).setApiMsg(apiMsg);
result.setData(data);
// 翻译错误码
if (codeMapping != null) {
ErrorCode errorCode = codeMapping.apply(apiCode, apiMsg);
result.setCode(errorCode.getCode()).setMsg(errorCode.getMsg());
}
return result;
}
public static <T> PayCommonResult<T> error(Throwable ex) {
PayCommonResult<T> result = new PayCommonResult<>();
result.setCode(PayFrameworkErrorCodeConstants.EXCEPTION.getCode());
result.setMsg(ExceptionUtil.getRootCauseMessage(ex));
return result;
}
}

View File

@ -0,0 +1,45 @@
package cn.iocoder.yudao.framework.pay.core.client.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
/**
* 支付通知 Response DTO
*
* @author 芋道源码
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PayOrderNotifyRespDTO {
/**
* 支付订单号(支付模块的)
*/
private String orderExtensionNo;
/**
* 支付渠道编号
*/
private String channelOrderNo;
/**
* 支付渠道用户编号
*/
private String channelUserId;
/**
* 支付成功时间
*/
private Date successTime;
/**
* 通知的原始数据
*
* 主要用于持久化,方便后续修复数据,或者排错
*/
private String data;
}

View File

@ -0,0 +1,76 @@
package cn.iocoder.yudao.framework.pay.core.client.dto;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.URL;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.Date;
import java.util.Map;
/**
* 统一下单 Request DTO
*
* @author 芋道源码
*/
@Data
public class PayOrderUnifiedReqDTO {
/**
* 用户 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;
/**
* 支付结果的回调地址
*/
@NotEmpty(message = "支付结果的回调地址不能为空")
@URL(message = "支付结果的回调地址必须是 URL 格式")
private String notifyUrl;
// ========== 订单相关字段 ==========
/**
* 支付金额,单位:分
*/
@NotNull(message = "支付金额不能为空")
@DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
private Long amount;
/**
* 支付过期时间
*/
@NotNull(message = "支付过期时间不能为空")
private Date expireTime;
// ========== 拓展参数 ==========
/**
* 支付渠道的额外参数
*
* 例如说,微信公众号需要传递 openid 参数
*/
private Map<String, String> channelExtras;
}

View File

@ -0,0 +1,97 @@
package cn.iocoder.yudao.framework.pay.core.client.impl;
import cn.hutool.extra.validation.ValidationUtil;
import cn.iocoder.yudao.framework.pay.core.client.AbstractPayCodeMapping;
import cn.iocoder.yudao.framework.pay.core.client.PayClient;
import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
import cn.iocoder.yudao.framework.pay.core.client.PayCommonResult;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderUnifiedReqDTO;
import lombok.extern.slf4j.Slf4j;
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
/**
* 支付客户端的抽象类,提供模板方法,减少子类的冗余代码
*
* @author 芋道源码
*/
@Slf4j
public abstract class AbstractPayClient<Config extends PayClientConfig> implements PayClient {
/**
* 渠道编号
*/
private final Long channelId;
/**
* 渠道编码
*/
private final String channelCode;
/**
* 错误码枚举类
*/
protected AbstractPayCodeMapping codeMapping;
/**
* 支付配置
*/
protected Config config;
protected Double calculateAmount(Long amount) {
return amount / 100.0;
}
public AbstractPayClient(Long channelId, String channelCode, Config config, AbstractPayCodeMapping codeMapping) {
this.channelId = channelId;
this.channelCode = channelCode;
this.codeMapping = codeMapping;
this.config = config;
}
/**
* 初始化
*/
public final void init() {
doInit();
log.info("[init][配置({}) 初始化完成]", config);
}
/**
* 自定义初始化
*/
protected abstract void doInit();
public final void refresh(Config config) {
// 判断是否更新
if (config.equals(this.config)) {
return;
}
log.info("[refresh][配置({})发生变化,重新初始化]", config);
this.config = config;
// 初始化
this.init();
}
@Override
public Long getId() {
return channelId;
}
@Override
public final PayCommonResult<?> unifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
ValidationUtil.validate(reqDTO);
// 执行短信发送
PayCommonResult<?> result;
try {
result = doUnifiedOrder(reqDTO);
} catch (Throwable ex) {
// 打印异常日志
log.error("[unifiedOrder][request({}) 发起支付失败]", toJsonString(reqDTO), ex);
// 封装返回
return PayCommonResult.error(ex);
}
return result;
}
protected abstract PayCommonResult<?> doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO)
throws Throwable;
}

View File

@ -0,0 +1,71 @@
package cn.iocoder.yudao.framework.pay.core.client.impl;
import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.framework.pay.core.client.PayClient;
import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayPayClientConfig;
import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayQrPayClient;
import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayWapPayClient;
import cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXPayClientConfig;
import cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXPubPayClient;
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* 支付客户端的工厂实现类
*
* @author 芋道源码
*/
@Slf4j
public class PayClientFactoryImpl implements PayClientFactory {
/**
* 支付客户端 Map
* key渠道编号
*/
private final ConcurrentMap<Long, AbstractPayClient<?>> channelIdClients = new ConcurrentHashMap<>();
@Override
public PayClient getPayClient(Long channelId) {
AbstractPayClient<?> client = channelIdClients.get(channelId);
if (client == null) {
log.error("[getPayClient][渠道编号({}) 找不到客户端]", channelId);
}
return client;
}
@Override
@SuppressWarnings("unchecked")
public <Config extends PayClientConfig> void createOrUpdatePayClient(Long channelId, String channelCode,
Config config) {
AbstractPayClient<Config> client = (AbstractPayClient<Config>) channelIdClients.get(channelId);
if (client == null) {
client = this.createPayClient(channelId, channelCode, config);
client.init();
channelIdClients.put(client.getId(), client);
} else {
client.refresh(config);
}
}
@SuppressWarnings("unchecked")
private <Config extends PayClientConfig> AbstractPayClient<Config> createPayClient(
Long channelId, String channelCode, Config config) {
PayChannelEnum channelEnum = PayChannelEnum.getByCode(channelCode);
Assert.notNull(channelEnum, String.format("支付渠道(%s) 为空", channelEnum));
// 创建客户端
switch (channelEnum) {
case WX_PUB: return (AbstractPayClient<Config>) new WXPubPayClient(channelId, (WXPayClientConfig) config);
case ALIPAY_WAP: return (AbstractPayClient<Config>) new AlipayWapPayClient(channelId, (AlipayPayClientConfig) config);
case ALIPAY_QR: return (AbstractPayClient<Config>) new AlipayQrPayClient(channelId, (AlipayPayClientConfig) config);
}
// 创建失败,错误日志 + 抛出异常
log.error("[createSmsClient][配置({}) 找不到合适的客户端实现]", config);
throw new IllegalArgumentException(String.format("配置(%s) 找不到合适的客户端实现", config));
}
}

View File

@ -0,0 +1,89 @@
package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
import lombok.Data;
// TODO 芋艿:参数校验
/**
* 支付宝的 PayClientConfig 实现类
* 属性主要来自 {@link com.alipay.api.AlipayConfig} 的必要属性
*
* @author 芋道源码
*/
@Data
public class AlipayPayClientConfig implements PayClientConfig {
/**
* 网关地址 - 线上
*/
public static final String SERVER_URL_PROD = "https://openapi.alipay.com/gateway.do";
/**
* 网关地址 - 沙箱
*/
public static final String SERVER_URL_SANDBOX = "https://openapi.alipaydev.com/gateway.do";
/**
* 公钥类型 - 公钥模式
*/
private static final Integer MODE_PUBLIC_KEY = 1;
/**
* 公钥类型 - 证书模式
*/
private static final Integer MODE_CERTIFICATE = 2;
/**
* 签名算法类型 - RSA
*/
public static final String SIGN_TYPE_DEFAULT = "RSA2";
/**
* 网关地址
* 1. {@link #SERVER_URL_PROD}
* 2. {@link #SERVER_URL_SANDBOX}
*/
private String serverUrl;
/**
* 开放平台上创建的应用的 ID
*/
private String appId;
/**
* 签名算法类型推荐RSA2
*
* {@link #SIGN_TYPE_DEFAULT}
*/
private String signType;
/**
* 公钥类型
* 1. {@link #MODE_PUBLIC_KEY} 情况privateKey + alipayPublicKey
* 2. {@link #MODE_CERTIFICATE} 情况appCertContent + alipayPublicCertContent + rootCertContent
*/
private Integer mode;
// ========== 公钥模式 ==========
/**
* 商户私钥
*/
private String privateKey;
/**
* 支付宝公钥字符串
*/
private String alipayPublicKey;
// ========== 证书模式 ==========
/**
* 指定商户公钥应用证书内容字符串
*/
private String appCertContent;
/**
* 指定支付宝公钥证书内容字符串
*/
private String alipayPublicCertContent;
/**
* 指定根证书内容字符串
*/
private String rootCertContent;
}

View File

@ -0,0 +1,24 @@
package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
import cn.iocoder.yudao.framework.pay.core.client.AbstractPayCodeMapping;
import java.util.Objects;
/**
* 支付宝的 PayCodeMapping 实现类
*
* @author 芋道源码
*/
public class AlipayPayCodeMapping extends AbstractPayCodeMapping {
@Override
protected ErrorCode apply0(String apiCode, String apiMsg) {
if (Objects.equals(apiCode, "10000")) {
return GlobalErrorCodeConstants.SUCCESS;
}
return null;
}
}

View File

@ -0,0 +1,74 @@
package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
import cn.hutool.core.bean.BeanUtil;
import cn.iocoder.yudao.framework.pay.core.client.PayCommonResult;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderNotifyRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayConfig;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.domain.AlipayTradePrecreateModel;
import com.alipay.api.request.AlipayTradePrecreateRequest;
import com.alipay.api.response.AlipayTradePrecreateResponse;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
/**
* 支付宝【扫码支付】的 PayClient 实现类
* 文档https://opendocs.alipay.com/apis/02890k
*
* @author 芋道源码
*/
@Slf4j
public class AlipayQrPayClient extends AbstractPayClient<AlipayPayClientConfig> {
private DefaultAlipayClient client;
public AlipayQrPayClient(Long channelId, AlipayPayClientConfig config) {
super(channelId, PayChannelEnum.ALIPAY_QR.getCode(), config, new AlipayPayCodeMapping());
}
@Override
@SneakyThrows
protected void doInit() {
AlipayConfig alipayConfig = new AlipayConfig();
BeanUtil.copyProperties(config, alipayConfig, false);
// 真实客户端
this.client = new DefaultAlipayClient(alipayConfig);
}
@Override
public PayCommonResult<AlipayTradePrecreateResponse> doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
// 构建 AlipayTradePrecreateModel 请求
AlipayTradePrecreateModel model = new AlipayTradePrecreateModel();
model.setOutTradeNo(reqDTO.getMerchantOrderId());
model.setSubject(reqDTO.getSubject());
model.setBody(reqDTO.getBody());
model.setTotalAmount(calculateAmount(reqDTO.getAmount()).toString()); // 单位:元
// TODO 芋艿userIp + expireTime
// 构建 AlipayTradePrecreateRequest
AlipayTradePrecreateRequest request = new AlipayTradePrecreateRequest();
request.setBizModel(model);
// 执行请求
AlipayTradePrecreateResponse response;
try {
response = client.execute(request);
} catch (AlipayApiException e) {
log.error("[unifiedOrder][request({}) 发起支付失败]", toJsonString(reqDTO), e);
return PayCommonResult.build(e.getErrCode(), e.getErrMsg(), null, codeMapping);
}
// TODO 芋艿sub Code 需要测试下各种失败的情况
return PayCommonResult.build(response.getCode(), response.getMsg(), response, codeMapping);
}
@Override
public PayOrderNotifyRespDTO parseOrderNotify(String data) throws Exception {
// TODO 芋艿:待完成
return null;
}
}

View File

@ -0,0 +1,70 @@
package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
import cn.hutool.core.bean.BeanUtil;
import cn.iocoder.yudao.framework.pay.core.client.PayCommonResult;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderNotifyRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayConfig;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.domain.AlipayTradeWapPayModel;
import com.alipay.api.request.AlipayTradeWapPayRequest;
import com.alipay.api.response.AlipayTradeWapPayResponse;
import lombok.SneakyThrows;
/**
* 支付宝【手机网站】的 PayClient 实现类
* 文档https://opendocs.alipay.com/apis/api_1/alipay.trade.wap.pay
*
* @author 芋道源码
*/
public class AlipayWapPayClient extends AbstractPayClient<AlipayPayClientConfig> {
private DefaultAlipayClient client;
public AlipayWapPayClient(Long channelId, AlipayPayClientConfig config) {
super(channelId, PayChannelEnum.ALIPAY_WAP.getCode(), config, new AlipayPayCodeMapping());
}
@Override
@SneakyThrows
protected void doInit() {
AlipayConfig alipayConfig = new AlipayConfig();
BeanUtil.copyProperties(config, alipayConfig, false);
this.client = new DefaultAlipayClient(alipayConfig);
}
@Override
public PayCommonResult<AlipayTradeWapPayResponse> doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
// 构建 AlipayTradeWapPayModel 请求
AlipayTradeWapPayModel model = new AlipayTradeWapPayModel();
model.setOutTradeNo(reqDTO.getMerchantOrderId());
model.setSubject(reqDTO.getSubject());
model.setBody(reqDTO.getBody());
model.setTotalAmount(calculateAmount(reqDTO.getAmount()).toString());
model.setProductCode("QUICK_WAP_PAY"); // TODO 芋艿:这里咋整
model.setSellerId("2088102147948060"); // TODO 芋艿:这里咋整
// TODO 芋艿userIp + expireTime
// 构建 AlipayTradeWapPayRequest
AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest();
request.setBizModel(model);
// 执行请求
AlipayTradeWapPayResponse response;
try {
response = client.pageExecute(request);
} catch (AlipayApiException e) {
return PayCommonResult.build(e.getErrCode(), e.getErrMsg(), null, codeMapping);
}
// TODO 芋艿sub Code
return PayCommonResult.build(response.getCode(), response.getMsg(), response, codeMapping);
}
@Override
public PayOrderNotifyRespDTO parseOrderNotify(String data) throws Exception {
// TODO 芋艿:待完成
return null;
}
}

View File

@ -0,0 +1,56 @@
package cn.iocoder.yudao.framework.pay.core.client.impl.wx;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
import cn.iocoder.yudao.framework.pay.core.client.AbstractPayCodeMapping;
import java.util.Objects;
import static cn.iocoder.yudao.framework.pay.core.enums.PayFrameworkErrorCodeConstants.*;
/**
* 微信支付 PayCodeMapping 实现类
*
* @author 芋道源码
*/
public class WXCodeMapping extends AbstractPayCodeMapping {
/**
* 错误码 - 成功
* 由于 weixin-java-pay 封装的 Result 未返回 code所以自己定义下
*/
public static final String CODE_SUCCESS = "SUCCESS";
/**
* 错误提示 - 成功
*/
public static final String MESSAGE_SUCCESS = "成功";
@Override
protected ErrorCode apply0(String apiCode, String apiMsg) {
if (Objects.equals(apiCode, CODE_SUCCESS)) {
return GlobalErrorCodeConstants.SUCCESS;
}
if (Objects.equals(apiCode, "FAIL")) {
if (Objects.equals(apiMsg, "AppID不存在请检查后再试")) {
return PAY_CONFIG_APP_ID_ERROR;
}
if (Objects.equals(apiMsg, "签名错误,请检查后再试")
|| Objects.equals(apiMsg, "签名错误")) {
return PAY_CONFIG_SIGN_ERROR;
}
}
if (Objects.equals(apiCode, "PARAM_ERROR")) {
if (Objects.equals(apiMsg, "无效的openid")) {
return PAY_OPENID_ERROR;
}
}
if (Objects.equals(apiCode, "CustomErrorCode")) {
if (StrUtil.contains(apiMsg, "必填字段")) {
return PAY_PARAM_MISSING;
}
}
return null;
}
}

View File

@ -0,0 +1,88 @@
package cn.iocoder.yudao.framework.pay.core.client.impl.wx;
import cn.hutool.core.io.IoUtil;
import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
import lombok.Data;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
// TODO 芋艿:参数校验
/**
* 微信支付的 PayClientConfig 实现类
* 属性主要来自 {@link com.github.binarywang.wxpay.config.WxPayConfig} 的必要属性
*
* @author 芋道源码
*/
@Data
public class WXPayClientConfig implements PayClientConfig {
// TODO 芋艿V2 or V3 客户端
/**
* API 版本 - V2
*
* https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_1
*/
public static final String API_VERSION_V2 = "v2";
/**
* API 版本 - V3
*
* https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay-1.shtml
*/
public static final String API_VERSION_V3 = "v3";
/**
* 公众号或者小程序的 appid
*/
private String appId;
/**
* 商户号
*/
private String mchId;
/**
* API 版本
*/
private String apiVersion;
// ========== V2 版本的参数 ==========
/**
* 商户密钥
*/
private String mchKey;
// /**
// * apiclient_cert.p12 证书文件的绝对路径或者以 classpath: 开头的类路径.
// * 对应的字符串
// *
// * 注意,可通过 {@link #main(String[])} 读取
// */
// private String keyContent;
// ========== V3 版本的参数 ==========
/**
* apiclient_key.pem 证书文件的绝对路径或者以 classpath: 开头的类路径.
* 对应的字符串
*
* 注意,可通过 {@link #main(String[])} 读取
*/
private String privateKeyContent;
/**
* apiclient_cert.pem 证书文件的绝对路径或者以 classpath: 开头的类路径.
* 对应的字符串
*
* 注意,可通过 {@link #main(String[])} 读取
*/
private String privateCertContent;
/**
* apiV3 秘钥值
*/
private String apiV3Key;
public static void main(String[] args) throws FileNotFoundException {
String path = "/Users/yunai/Downloads/wx_pay/apiclient_cert.p12";
// String path = "/Users/yunai/Downloads/wx_pay/apiclient_key.pem";
// String path = "/Users/yunai/Downloads/wx_pay/apiclient_cert.pem";
System.out.println(IoUtil.readUtf8(new FileInputStream(path)));
}
}

View File

@ -0,0 +1,144 @@
package cn.iocoder.yudao.framework.pay.core.client.impl.wx;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.io.FileUtils;
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
import cn.iocoder.yudao.framework.pay.core.client.PayCommonResult;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderNotifyRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
import com.github.binarywang.wxpay.bean.order.WxPayMpOrderResult;
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request;
import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderV3Result;
import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
import com.github.binarywang.wxpay.config.WxPayConfig;
import com.github.binarywang.wxpay.constant.WxPayConstants;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.service.WxPayService;
import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
import lombok.extern.slf4j.Slf4j;
import java.util.Objects;
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
import static cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXCodeMapping.CODE_SUCCESS;
import static cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXCodeMapping.MESSAGE_SUCCESS;
/**
* 微信支付(公众号)的 PayClient 实现类
*
* @author 芋道源码
*/
@Slf4j
public class WXPubPayClient extends AbstractPayClient<WXPayClientConfig> {
private WxPayService client;
public WXPubPayClient(Long channelId, WXPayClientConfig config) {
super(channelId, PayChannelEnum.WX_PUB.getCode(), config, new WXCodeMapping());
}
@Override
protected void doInit() {
WxPayConfig payConfig = new WxPayConfig();
BeanUtil.copyProperties(config, payConfig, "keyContent");
payConfig.setTradeType(WxPayConstants.TradeType.JSAPI); // 设置使用 JS API 支付方式
// if (StrUtil.isNotEmpty(config.getKeyContent())) {
// payConfig.setKeyContent(config.getKeyContent().getBytes(StandardCharsets.UTF_8));
// }
if (StrUtil.isNotEmpty(config.getPrivateKeyContent())) {
// weixin-pay-java 存在 BUG无法直接设置内容所以创建临时文件来解决
payConfig.setPrivateKeyPath(FileUtils.createTempFile(config.getPrivateKeyContent()).getPath());
}
if (StrUtil.isNotEmpty(config.getPrivateCertContent())) {
// weixin-pay-java 存在 BUG无法直接设置内容所以创建临时文件来解决
payConfig.setPrivateCertPath(FileUtils.createTempFile(config.getPrivateCertContent()).getPath());
}
// 真实客户端
this.client = new WxPayServiceImpl();
client.setConfig(payConfig);
}
@Override
public PayCommonResult<WxPayMpOrderResult> doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
WxPayMpOrderResult response;
try {
switch (config.getApiVersion()) {
case WXPayClientConfig.API_VERSION_V2:
response = this.unifiedOrderV2(reqDTO);
break;
case WXPayClientConfig.API_VERSION_V3:
WxPayUnifiedOrderV3Result.JsapiResult responseV3 = this.unifiedOrderV3(reqDTO);
// 将 V3 的结果,统一转换成 V2。返回的字段是一致的
response = new WxPayMpOrderResult();
BeanUtil.copyProperties(responseV3, response, true);
break;
default:
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
}
} catch (WxPayException e) {
log.error("[unifiedOrder][request({}) 发起支付失败,原因({})]", toJsonString(reqDTO), e);
return PayCommonResult.build(ObjectUtils.defaultIfNull(e.getErrCode(), e.getReturnCode(), "CustomErrorCode"),
ObjectUtils.defaultIfNull(e.getErrCodeDes(), e.getCustomErrorMsg()),null, codeMapping);
}
return PayCommonResult.build(CODE_SUCCESS, MESSAGE_SUCCESS, response, codeMapping);
}
private WxPayMpOrderResult unifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
// 构建 WxPayUnifiedOrderRequest 对象
WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest.newBuilder()
.outTradeNo(reqDTO.getMerchantOrderId())
// TODO 芋艿:貌似没 title
.body(reqDTO.getBody())
.totalFee(reqDTO.getAmount().intValue()) // 单位分
.timeExpire(DateUtil.format(reqDTO.getExpireTime(), "yyyyMMddHHmmss"))
.spbillCreateIp(reqDTO.getUserIp())
.openid(getOpenid(reqDTO))
.notifyUrl(reqDTO.getNotifyUrl())
.build();
// 执行请求
return client.createOrder(request);
}
private WxPayUnifiedOrderV3Result.JsapiResult unifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
// 构建 WxPayUnifiedOrderRequest 对象
WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request();
request.setOutTradeNo(reqDTO.getMerchantOrderId());
// TODO 芋艿:貌似没 title
request.setDescription(reqDTO.getBody());
request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getAmount().intValue())); // 单位分
request.setTimeExpire(DateUtil.format(reqDTO.getExpireTime(), "yyyyMMddHHmmss"));
request.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(getOpenid(reqDTO)));
request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp()));
request.setNotifyUrl(reqDTO.getNotifyUrl());
// 执行请求
return client.createOrderV3(TradeTypeEnum.JSAPI, request);
}
private static String getOpenid(PayOrderUnifiedReqDTO reqDTO) {
String openid = MapUtil.getStr(reqDTO.getChannelExtras(), "openid");
if (StrUtil.isEmpty(openid)) {
throw new IllegalArgumentException("支付请求的 openid 不能为空!");
}
return openid;
}
@Override
public PayOrderNotifyRespDTO parseOrderNotify(String data) throws WxPayException {
WxPayOrderNotifyResult notifyResult = client.parseOrderNotifyResult(data);
Assert.isTrue(Objects.equals(notifyResult.getResultCode(), "SUCCESS"), "支付结果非 SUCCESS");
// 转换结果
return PayOrderNotifyRespDTO.builder().orderExtensionNo(notifyResult.getOutTradeNo())
.channelOrderNo(notifyResult.getTransactionId()).channelUserId(notifyResult.getOpenid())
.successTime(DateUtil.parse(notifyResult.getTimeEnd(), "yyyyMMddHHmmss"))
.data(data).build();
}
}

View File

@ -0,0 +1,41 @@
package cn.iocoder.yudao.framework.pay.core.enums;
import cn.hutool.core.util.ArrayUtil;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 支付渠道的编码的枚举
* 枚举值
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public enum PayChannelEnum {
WX_PUB("wx_pub", "微信 JSAPI 支付"), // 公众号的网页
WX_LITE("wx_lit","微信小程序支付"),
WX_APP("wx_app", "微信 App 支付"),
ALIPAY_PC("alipay_pc", "支付宝 PC 网站支付"),
ALIPAY_WAP("alipay_wap", "支付宝 Wap 网站支付"),
ALIPAY_APP("alipay_app", "支付宝App 支付"),
ALIPAY_QR("alipay_qr", "支付宝扫码支付");
/**
* 编码
*
* 参考 https://www.pingxx.com/api/支付渠道属性值.html
*/
private String code;
/**
* 名字
*/
private String name;
public static PayChannelEnum getByCode(String code) {
return ArrayUtil.firstMatch(o -> o.getCode().equals(code), values());
}
}

View File

@ -0,0 +1,27 @@
package cn.iocoder.yudao.framework.pay.core.enums;
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
/**
* 支付框架的错误码枚举
*
* 短信框架,使用 2-002-000-000 段
*
* @author 芋道源码
*/
public interface PayFrameworkErrorCodeConstants {
ErrorCode PAY_UNKNOWN = new ErrorCode(2002000000, "未知错误,需要解析");
// ========== 配置相关相关 2002000100 ==========
ErrorCode PAY_CONFIG_APP_ID_ERROR = new ErrorCode(2002000100, "支付渠道 AppId 不正确");
ErrorCode PAY_CONFIG_SIGN_ERROR = new ErrorCode(2002000100, "签名错误"); // 例如说,微信支付,配置错了 mchId 或者 mchKey
// ========== 其它相关 2002000900 开头 ==========
ErrorCode PAY_OPENID_ERROR = new ErrorCode(2002000900, "无效的 openid"); // 例如说,微信 openid 未授权过
ErrorCode PAY_PARAM_MISSING = new ErrorCode(2002000901, "请求参数缺失"); // 例如说,支付少传了金额
ErrorCode EXCEPTION = new ErrorCode(2002000999, "调用异常");
}

View File

@ -0,0 +1,2 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.iocoder.yudao.framework.pay.config.YudaoPayAutoConfiguration

View File

@ -0,0 +1,129 @@
package cn.iocoder.yudao.framework.core.client.impl;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.RandomUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.pay.core.client.PayClient;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.impl.PayClientFactoryImpl;
import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayPayClientConfig;
import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayQrPayClient;
import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayWapPayClient;
import cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXPayClientConfig;
import cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXPubPayClient;
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
import org.junit.jupiter.api.Test;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
/**
* {@link PayClientFactoryImpl} 的集成测试
*
* @author 芋道源码
*/
public class PayClientFactoryImplTest {
private final PayClientFactoryImpl payClientFactory = new PayClientFactoryImpl();
/**
* {@link WXPubPayClient} 的 V2 版本
*/
@Test
public void testCreatePayClient_WX_PUB_V2() {
// 创建配置
WXPayClientConfig config = new WXPayClientConfig();
config.setAppId("wx041349c6f39b268b");
config.setMchId("1545083881");
config.setApiVersion(WXPayClientConfig.API_VERSION_V2);
config.setMchKey("0alL64UDQdlCwiKZ73ib7ypaIjMns06p");
// 创建客户端
Long channelId = RandomUtil.randomLong();
payClientFactory.createOrUpdatePayClient(channelId, PayChannelEnum.WX_PUB.getCode(), config);
PayClient client = payClientFactory.getPayClient(channelId);
// 发起支付
PayOrderUnifiedReqDTO reqDTO = buildPayOrderUnifiedReqDTO();
CommonResult<?> result = client.unifiedOrder(reqDTO);
System.out.println(result);
}
/**
* {@link WXPubPayClient} 的 V3 版本
*/
@Test
public void testCreatePayClient_WX_PUB_V3() throws FileNotFoundException {
// 创建配置
WXPayClientConfig config = new WXPayClientConfig();
config.setAppId("wx041349c6f39b268b");
config.setMchId("1545083881");
config.setApiVersion(WXPayClientConfig.API_VERSION_V3);
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");
// 创建客户端
Long channelId = RandomUtil.randomLong();
payClientFactory.createOrUpdatePayClient(channelId, PayChannelEnum.WX_PUB.getCode(), config);
PayClient client = payClientFactory.getPayClient(channelId);
// 发起支付
PayOrderUnifiedReqDTO reqDTO = buildPayOrderUnifiedReqDTO();
CommonResult<?> result = client.unifiedOrder(reqDTO);
System.out.println(result);
}
/**
* {@link AlipayQrPayClient}
*/
@Test
public void testCreatePayClient_ALIPAY_QR() {
// 创建配置
AlipayPayClientConfig config = new AlipayPayClientConfig();
config.setAppId("2021000118634035");
config.setServerUrl(AlipayPayClientConfig.SERVER_URL_SANDBOX);
config.setSignType(AlipayPayClientConfig.SIGN_TYPE_DEFAULT);
config.setPrivateKey("MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCHsEV1cDupwJv890x84qbppUtRIfhaKSwSVN0thCcsDCaAsGR5MZslDkO8NCT9V4r2SVXjyY7eJUZlZd1M0C8T01Tg4UOx5LUbic0O3A1uJMy6V1n9IyYwbAW3AEZhBd5bSbPgrqvmv3NeWSTQT6Anxnllf+2iDH6zyA2fPl7cYyQtbZoDJQFGqr4F+cGh2R6akzRKNoBkAeMYwoY6es2lX8sJxCVPWUmxNUoL3tScwlSpd7Bxw0q9c/X01jMwuQ0+Va358zgFiGERTE6yD01eu40OBDXOYO3z++y+TAYHlQQ2toMO63trepo88X3xV3R44/1DH+k2pAm2IF5ixiLrAgMBAAECggEAPx3SoXcseaD7rmcGcE0p4SMfbsUDdkUSmBBbtfF0GzwnqNLkWa+mgE0rWt9SmXngTQH97vByAYmLPl1s3G82ht1V7Sk7yQMe74lhFllr8eEyTjeVx3dTK1EEM4TwN+936DTXdFsr4TELJEcJJdD0KaxcCcfBLRDs2wnitEFZ9N+GoZybVmY8w0e0MI7PLObUZ2l0X4RurQnfG9ZxjXjC7PkeMVv7cGGylpNFi3BbvkRhdhLPDC2E6wqnr9e7zk+hiENivAezXrtxtwKovzCtnWJ1r0IO14Rh47H509Ic0wFnj+o5YyUL4LdmpL7yaaH6fM7zcSLFjNZPHvZCKPwYcQKBgQDQFho98QvnL8ex4v6cry4VitGpjSXm1qP3vmMQk4rTsn8iPWtcxPjqGEqOQJjdi4Mi0VZKQOLFwlH0kl95wNrD/isJ4O1yeYfX7YAXApzHqYNINzM79HemO3Yx1qLMW3okRFJ9pPRzbQ9qkTpsaegsmyX316zOBhzGRYjKbutTYwKBgQCm7phr9XdFW5Vh+XR90mVs483nrLmMiDKg7YKxSLJ8amiDjzPejCn7i95Hah08P+2MIZLIPbh2VLacczR6ltRRzN5bg5etFuqSgfkuHyxpoDmpjbe08+Q2h8JBYqcC5Nhv1AKU4iOUhVLHo/FBAQliMcGc/J3eiYTFC7EsNx382QKBgClb20doe7cttgFTXswBvaUmfFm45kmla924B7SpvrQpDD/f+VDtDZRp05fGmxuduSjYdtA3aVtpLiTwWu22OUUvZZqHDGruYOO4Hvdz23mL5b4ayqImCwoNU4bAZIc9v18p/UNf3/55NNE3oGcf/bev9rH2OjCQ4nM+Ktwhg8CFAoGACSgvbkShzUkv0ZcIf9ppu+ZnJh1AdGgINvGwaJ8vQ0nm/8h8NOoFZ4oNoGc+wU5Ubops7dUM6FjPR5e+OjdJ4E7Xp7d5O4J1TaIZlCEbo5OpdhaTDDcQvrkFu+Z4eN0qzj+YAKjDAOOrXc4tbr5q0FsgXscwtcNfaBuzFVTUrUkCgYEAwzPnMNhWG3zOWLUs2QFA2GP4Y+J8cpUYfj6pbKKzeLwyG9qBwF1NJpN8m+q9q7V9P2LY+9Lp9e1mGsGeqt5HMEA3P6vIpcqLJLqE/4PBLLRzfccTcmqb1m71+erxTRhHBRkGS+I7dZEb3olQfnS1Y1tpMBxiwYwR3LW4oXuJwj8=");
config.setAlipayPublicKey("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnq90KnF4dTnlzzmxpujbI05OYqi5WxAS6cL0gnZFv2gK51HExF8v/BaP7P979PhFMgWTqmOOI+Dtno5s+yD09XTY1WkshbLk6i4g2Xlr8fyW9ODnkU88RI2w9UdPhQU4cPPwBNlrsYhKkVK2OxwM3kFqjoBBY0CZoZCsSQ3LDH5WeZqPArlsS6xa2zqJBuuoKjMrdpELl3eXSjP8K54eDJCbeetCZNKWLL3DPahTPB7LZikfYmslb0QUvCgGapD0xkS7eVq70NaL1G57MWABs4tbfWgxike4Daj3EfUrzIVspQxj7w8HEj9WozJPgL88kSJSits0pqD3n5r8HSuseQIDAQAB");
// 创建客户端
Long channelId = RandomUtil.randomLong();
payClientFactory.createOrUpdatePayClient(channelId, PayChannelEnum.ALIPAY_QR.getCode(), config);
PayClient client = payClientFactory.getPayClient(channelId);
// 发起支付
PayOrderUnifiedReqDTO reqDTO = buildPayOrderUnifiedReqDTO();
CommonResult<?> result = client.unifiedOrder(reqDTO);
System.out.println(JsonUtils.toJsonString(result));
}
/**
* {@link AlipayWapPayClient}
*/
@Test
public void testCreatePayClient_ALIPAY_WAP() {
// 创建配置
AlipayPayClientConfig config = new AlipayPayClientConfig();
config.setAppId("2021000118634035");
config.setServerUrl(AlipayPayClientConfig.SERVER_URL_SANDBOX);
config.setSignType(AlipayPayClientConfig.SIGN_TYPE_DEFAULT);
config.setPrivateKey("MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCHsEV1cDupwJv890x84qbppUtRIfhaKSwSVN0thCcsDCaAsGR5MZslDkO8NCT9V4r2SVXjyY7eJUZlZd1M0C8T01Tg4UOx5LUbic0O3A1uJMy6V1n9IyYwbAW3AEZhBd5bSbPgrqvmv3NeWSTQT6Anxnllf+2iDH6zyA2fPl7cYyQtbZoDJQFGqr4F+cGh2R6akzRKNoBkAeMYwoY6es2lX8sJxCVPWUmxNUoL3tScwlSpd7Bxw0q9c/X01jMwuQ0+Va358zgFiGERTE6yD01eu40OBDXOYO3z++y+TAYHlQQ2toMO63trepo88X3xV3R44/1DH+k2pAm2IF5ixiLrAgMBAAECggEAPx3SoXcseaD7rmcGcE0p4SMfbsUDdkUSmBBbtfF0GzwnqNLkWa+mgE0rWt9SmXngTQH97vByAYmLPl1s3G82ht1V7Sk7yQMe74lhFllr8eEyTjeVx3dTK1EEM4TwN+936DTXdFsr4TELJEcJJdD0KaxcCcfBLRDs2wnitEFZ9N+GoZybVmY8w0e0MI7PLObUZ2l0X4RurQnfG9ZxjXjC7PkeMVv7cGGylpNFi3BbvkRhdhLPDC2E6wqnr9e7zk+hiENivAezXrtxtwKovzCtnWJ1r0IO14Rh47H509Ic0wFnj+o5YyUL4LdmpL7yaaH6fM7zcSLFjNZPHvZCKPwYcQKBgQDQFho98QvnL8ex4v6cry4VitGpjSXm1qP3vmMQk4rTsn8iPWtcxPjqGEqOQJjdi4Mi0VZKQOLFwlH0kl95wNrD/isJ4O1yeYfX7YAXApzHqYNINzM79HemO3Yx1qLMW3okRFJ9pPRzbQ9qkTpsaegsmyX316zOBhzGRYjKbutTYwKBgQCm7phr9XdFW5Vh+XR90mVs483nrLmMiDKg7YKxSLJ8amiDjzPejCn7i95Hah08P+2MIZLIPbh2VLacczR6ltRRzN5bg5etFuqSgfkuHyxpoDmpjbe08+Q2h8JBYqcC5Nhv1AKU4iOUhVLHo/FBAQliMcGc/J3eiYTFC7EsNx382QKBgClb20doe7cttgFTXswBvaUmfFm45kmla924B7SpvrQpDD/f+VDtDZRp05fGmxuduSjYdtA3aVtpLiTwWu22OUUvZZqHDGruYOO4Hvdz23mL5b4ayqImCwoNU4bAZIc9v18p/UNf3/55NNE3oGcf/bev9rH2OjCQ4nM+Ktwhg8CFAoGACSgvbkShzUkv0ZcIf9ppu+ZnJh1AdGgINvGwaJ8vQ0nm/8h8NOoFZ4oNoGc+wU5Ubops7dUM6FjPR5e+OjdJ4E7Xp7d5O4J1TaIZlCEbo5OpdhaTDDcQvrkFu+Z4eN0qzj+YAKjDAOOrXc4tbr5q0FsgXscwtcNfaBuzFVTUrUkCgYEAwzPnMNhWG3zOWLUs2QFA2GP4Y+J8cpUYfj6pbKKzeLwyG9qBwF1NJpN8m+q9q7V9P2LY+9Lp9e1mGsGeqt5HMEA3P6vIpcqLJLqE/4PBLLRzfccTcmqb1m71+erxTRhHBRkGS+I7dZEb3olQfnS1Y1tpMBxiwYwR3LW4oXuJwj8=");
config.setAlipayPublicKey("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnq90KnF4dTnlzzmxpujbI05OYqi5WxAS6cL0gnZFv2gK51HExF8v/BaP7P979PhFMgWTqmOOI+Dtno5s+yD09XTY1WkshbLk6i4g2Xlr8fyW9ODnkU88RI2w9UdPhQU4cPPwBNlrsYhKkVK2OxwM3kFqjoBBY0CZoZCsSQ3LDH5WeZqPArlsS6xa2zqJBuuoKjMrdpELl3eXSjP8K54eDJCbeetCZNKWLL3DPahTPB7LZikfYmslb0QUvCgGapD0xkS7eVq70NaL1G57MWABs4tbfWgxike4Daj3EfUrzIVspQxj7w8HEj9WozJPgL88kSJSits0pqD3n5r8HSuseQIDAQAB");
// 创建客户端
Long channelId = RandomUtil.randomLong();
payClientFactory.createOrUpdatePayClient(channelId, PayChannelEnum.ALIPAY_WAP.getCode(), config);
PayClient client = payClientFactory.getPayClient(channelId);
// 发起支付
PayOrderUnifiedReqDTO reqDTO = buildPayOrderUnifiedReqDTO();
CommonResult<?> result = client.unifiedOrder(reqDTO);
System.out.println(JsonUtils.toJsonString(result));
}
private static PayOrderUnifiedReqDTO buildPayOrderUnifiedReqDTO() {
PayOrderUnifiedReqDTO reqDTO = new PayOrderUnifiedReqDTO();
reqDTO.setAmount(123L);
reqDTO.setSubject("IPhone 13");
reqDTO.setBody("biubiubiu");
reqDTO.setMerchantOrderId(String.valueOf(System.currentTimeMillis()));
reqDTO.setUserIp("127.0.0.1");
reqDTO.setNotifyUrl("http://127.0.0.1:8080");
return reqDTO;
}
}

View File

@ -79,4 +79,5 @@
</dependency>
<!-- SMS SDK end -->
</dependencies>
</project>

View File

@ -8,7 +8,7 @@ import cn.iocoder.yudao.framework.sms.core.client.dto.SmsTemplateRespDTO;
import java.util.List;
/**
* 短信客户端接口
* 短信客户端,用于对接各短信平台的 SDK实现短信发送等功能
*
* @author zzf
* @date 2021/1/25 14:14

View File

@ -3,7 +3,7 @@ package cn.iocoder.yudao.framework.sms.core.client;
import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties;
/**
* 短信客户端工厂接口
* 短信客户端工厂接口
*
* @author zzf
* @date 2021/1/28 14:01

View File

@ -13,7 +13,7 @@ import lombok.extern.slf4j.Slf4j;
import java.util.List;
/**
* 短信客户端抽象类
* 短信客户端抽象类,提供模板方法,减少子类的冗余代码
*
* @author zzf
* @date 2021/2/1 9:28
@ -30,11 +30,6 @@ public abstract class AbstractSmsClient implements SmsClient {
*/
protected final SmsCodeMapping codeMapping;
/**
* 短信客户端有参构造函数
*
* @param properties 短信配置
*/
public AbstractSmsClient(SmsChannelProperties properties, SmsCodeMapping codeMapping) {
this.properties = properties;
this.codeMapping = codeMapping;
@ -48,6 +43,11 @@ public abstract class AbstractSmsClient implements SmsClient {
log.info("[init][配置({}) 初始化完成]", properties);
}
/**
* 自定义初始化
*/
protected abstract void doInit();
public final void refresh(SmsChannelProperties properties) {
// 判断是否更新
if (properties.equals(this.properties)) {
@ -59,11 +59,6 @@ public abstract class AbstractSmsClient implements SmsClient {
this.init();
}
/**
* 自定义初始化
*/
protected abstract void doInit();
@Override
public Long getId() {
return properties.getId();

View File

@ -83,7 +83,8 @@ public class YunpianSmsClient extends AbstractSmsClient {
}
// 参考 https://www.yunpian.com/official/document/sms/zh_cn/introduction_demos_encode_sample 格式化
StringJoiner joiner = new StringJoiner("&");
templateParams.forEach(param -> joiner.add(String.format("#%s#=%s", param.getKey(), URLUtil.encode(String.valueOf(param.getValue())))));
templateParams.forEach(param -> joiner.add(String.format("#%s#=%s", param.getKey(),
URLUtil.encode(String.valueOf(param.getValue())))));
return joiner.toString();
}

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-framework</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>yudao-spring-boot-starter-biz-weixin</artifactId>
<packaging>jar</packaging>
<name>${artifactId}</name>
<description>微信拓展
1. 基于 weixin-java-mp 库,对接微信公众号平台。目前主要解决微信公众号的支付场景。
</description>
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<dependencies>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-common</artifactId>
</dependency>
<!-- Test 测试相关 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 三方云服务相关 -->
<dependency>
<groupId>com.github.binarywang</groupId>
<!-- <artifactId>weixin-java-mp</artifactId>-->
<artifactId>wx-java-mp-spring-boot-starter</artifactId>
<version>4.1.9.B</version>
</dependency>
<!-- TODO 芋艿:清理 -->
</dependencies>
</project>

View File

@ -0,0 +1,34 @@
package cn.iocoder.yudao.framework.weixin;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.mp.api.WxMpService;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
@SpringBootTest(classes = WxMpServiceTest.Application.class)
public class WxMpServiceTest {
@Resource
private WxMpService wxMpService;
@Test
public void testGetAccessToken() throws WxErrorException {
String accessToken = wxMpService.getAccessToken();
System.out.println(accessToken);
}
@Test
public void testGet() throws WxErrorException {
String jsapiTicket = wxMpService.getJsapiTicket();
System.out.println(jsapiTicket);
}
@SpringBootApplication
public static class Application {
}
}

View File

@ -0,0 +1,11 @@
--- #################### 微信公众号相关配置 ####################
wx: # 参见 https://github.com/Wechat-Group/WxJava/blob/develop/spring-boot-starters/wx-java-mp-spring-boot-starter/README.md 文档
mp:
# 公众号配置(必填)
app-id: wx041349c6f39b268b
secret: 5abee519483bc9f8cb37ce280e814bd0
# 存储配置,解决 AccessToken 的跨节点的共享
# config-storage:
# type: RedisTemplate # 采用 RedisTemplate 操作 Redis会自动从 Spring 中获取
# key-prefix: wx # Redis Key 的前缀 TODO 芋艿:解决下 Redis key 管理的配置
# http-client-type: HttpClient # 采用 HttpClient 请求微信公众号平台

View File

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>yudao-framework</artifactId>
<groupId>cn.iocoder.boot</groupId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>yudao-spring-boot-starter-extension</artifactId>
<packaging>jar</packaging>
<name>${artifactId}</name>
<description>扩展点组件</description>
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<properties>
</properties>
<dependencies>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-common</artifactId>
</dependency>
<!-- Spring 核心 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<scope>provided</scope>
</dependency>
<!-- Spring 核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- 测试包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 测试包 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<!-- 工具类相关 -->
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,62 @@
package cn.iocoder.yudao.framework.extension.config;
import cn.iocoder.yudao.framework.extension.core.ExtensionBootstrap;
import cn.iocoder.yudao.framework.extension.core.context.ExtensionContext;
import cn.iocoder.yudao.framework.extension.core.context.ExtensionContextHolder;
import cn.iocoder.yudao.framework.extension.core.context.ExtensionExecutor;
import cn.iocoder.yudao.framework.extension.core.factory.ExtensionFactory;
import cn.iocoder.yudao.framework.extension.core.factory.ExtensionRegisterFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @description 扩展点组件自动装配
* @author Qingchen
* @version 1.0.0
* @date 2021-08-28 21:50
* @class cn.iocoder.yudao.framework.extension.config.YudaoExtensionAutoConfiguration.java
*/
@Configuration
public class YudaoExtensionAutoConfiguration {
/**
* 组件初始化
* @return
*/
@Bean(initMethod = "init")
@ConditionalOnMissingBean(ExtensionBootstrap.class)
public ExtensionBootstrap bootstrap() {
return new ExtensionBootstrap();
}
/**
* 扩展点工厂
* @return
*/
@Bean
@ConditionalOnMissingBean({ExtensionRegisterFactory.class, ExtensionFactory.class})
public ExtensionRegisterFactory registerFactory() {
return new ExtensionRegisterFactory();
}
/**
* 扩展组件上下文对象
* @return
*/
@Bean
@ConditionalOnMissingBean({ExtensionContextHolder.class, ExtensionContext.class})
public ExtensionContextHolder context() {
return new ExtensionContextHolder();
}
/**
* 扩展组件执行器
* @return
*/
@Bean
@ConditionalOnMissingBean(ExtensionExecutor.class)
public ExtensionExecutor executor() {
return new ExtensionExecutor();
}
}

View File

@ -0,0 +1,142 @@
package cn.iocoder.yudao.framework.extension.core;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.util.StringJoiner;
/**
* @description 业务场景 = businessId + useCase + scenario, 用来标识系统中唯一的一个场景<br/>
* @author Qingchen
* @version 1.0.0
* @date 2021-08-28 22:19
* @class cn.iocoder.yudao.framework.extension.core.BusinessScenario.java
*/
public class BusinessScenario implements Serializable {
/**
* 默认业务id
*/
public final static String DEFAULT_BUSINESS_ID = "#defaultBusinessId#";
/**
* 默认用例
*/
public final static String DEFAULT_USECASE = "#defaultUseCase#";
/**
* 默认场景
*/
public final static String DEFAULT_SCENARIO = "#defaultScenario#";
/**
* 分隔符
*/
private final static String DOT_SEPARATOR = ".";
/**
* 业务Id
*/
private String businessId;
/**
* 用例
*/
private String useCase;
/**
* 场景
*/
private String scenario;
public BusinessScenario() {
this.businessId = DEFAULT_BUSINESS_ID;
this.useCase = DEFAULT_USECASE;
this.scenario = DEFAULT_SCENARIO;
}
public BusinessScenario(@NotNull String businessId, @NotNull String useCase, @NotNull String scenario) {
this.businessId = businessId;
this.useCase = useCase;
this.scenario = scenario;
}
public BusinessScenario(@NotNull String scenario) {
this();
this.scenario = scenario;
}
public BusinessScenario(@NotNull String useCase, @NotNull String scenario) {
this(DEFAULT_BUSINESS_ID, useCase, scenario);
}
public String getBusinessId() {
return businessId;
}
public void setBusinessId(String businessId) {
this.businessId = businessId;
}
public String getUseCase() {
return useCase;
}
public void setUseCase(String useCase) {
this.useCase = useCase;
}
public String getScenario() {
return scenario;
}
public void setScenario(String scenario) {
this.scenario = scenario;
}
/**
* 构建业务场景
* @param businessId
* @param useCase
* @param scenario
* @return
*/
public static BusinessScenario valueOf(@NotNull String businessId, @NotNull String useCase, @NotNull String scenario) {
return new BusinessScenario(businessId, useCase, scenario);
}
/**
* 构建业务场景
* @param useCase
* @param scenario
* @return
*/
public static BusinessScenario valueOf(@NotNull String useCase, @NotNull String scenario) {
return new BusinessScenario(useCase, scenario);
}
/**
* 构建业务场景
* @param scenario
* @return
*/
public static BusinessScenario valueOf(@NotNull String scenario) {
return new BusinessScenario(scenario);
}
/**
* 业务场景唯一标识
* @return
*/
public String getUniqueIdentity(){
return new StringJoiner(DOT_SEPARATOR).add(businessId).add(useCase).add(scenario).toString();
}
@Override
public String toString() {
return "BusinessScenario{" +
"businessId='" + businessId + '\'' +
", useCase='" + useCase + '\'' +
", scenario='" + scenario + '\'' +
'}';
}
}

View File

@ -0,0 +1,41 @@
package cn.iocoder.yudao.framework.extension.core;
import cn.iocoder.yudao.framework.extension.core.factory.ExtensionRegisterFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import javax.annotation.PostConstruct;
/**
* @description
* @author Qingchen
* @version 1.0.0
* @date 2021-08-29 00:18
* @class cn.iocoder.yudao.framework.extension.core.ExtensionBootstrap.java
*/
public class ExtensionBootstrap implements ApplicationContextAware {
/**
* spring 容器
*/
private ApplicationContext applicationContext;
@Autowired
private ExtensionRegisterFactory registerFactory;
/**
* 初始化
*/
@PostConstruct
public void init() {
registerFactory.setApplicationContext(applicationContext);
registerFactory.register(null);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}

View File

@ -0,0 +1,131 @@
package cn.iocoder.yudao.framework.extension.core.context;
import cn.iocoder.yudao.framework.extension.core.BusinessScenario;
import cn.iocoder.yudao.framework.extension.core.point.ExtensionPoint;
import java.util.function.Consumer;
import java.util.function.Function;
/**
* @description 执行器通用方法
* @author Qingchen
* @version 1.0.0
* @date 2021-08-29 00:38
* @class cn.iocoder.yudao.framework.extension.core.context.AbstractComponentExecutor.java
*/
public abstract class AbstractComponentExecutor {
/**
* "业务" + "用例" + "场景")执行扩展组件,并返回执行结果
* @param targetClazz
* @param businessId
* @param useCase
* @param scenario
* @param function
* @param <R>
* @param <T>
* @return
*/
public <R, T extends ExtensionPoint> R execute(Class<T> targetClazz, String businessId, String useCase, String scenario, Function<T, R> function) {
return execute(targetClazz, BusinessScenario.valueOf(businessId, useCase, scenario), function);
}
/**
* "用例" + "场景")执行扩展组件,并返回执行结果
* @param targetClazz
* @param useCase
* @param scenario
* @param function
* @param <R>
* @param <T>
* @return
*/
public <R, T extends ExtensionPoint> R execute(Class<T> targetClazz, String useCase, String scenario, Function<T, R> function) {
return execute(targetClazz, BusinessScenario.valueOf(useCase, scenario), function);
}
/**
* "场景")执行扩展组件,并返回执行结果
* @param targetClazz
* @param scenario
* @param function
* @param <R>
* @param <T>
* @return
*/
public <R, T extends ExtensionPoint> R execute(Class<T> targetClazz, String scenario, Function<T, R> function) {
return execute(targetClazz, BusinessScenario.valueOf(scenario), function);
}
/**
* 执行扩展组件,并返回执行结果
* @param targetClazz
* @param businessScenario
* @param function
* @param <R> Response Type
* @param <T> Parameter Type
* @return
*/
public <R, T extends ExtensionPoint> R execute(Class<T> targetClazz, BusinessScenario businessScenario, Function<T, R> function) {
T component = locateComponent(targetClazz, businessScenario);
return function.apply(component);
}
/**
* "业务" + "用例" + "场景")执行扩展组件,适用于无返回值的业务
* @param targetClazz
* @param businessId
* @param useCase
* @param scenario
* @param consumer
* @param <T>
*/
public <T extends ExtensionPoint> void accept(Class<T> targetClazz, String businessId, String useCase, String scenario, Consumer<T> consumer) {
accept(targetClazz, BusinessScenario.valueOf(businessId, useCase, scenario), consumer);
}
/**
* "场景")执行扩展组件,适用于无返回值的业务
* @param targetClazz
* @param useCase
* @param scenario
* @param consumer
* @param <T>
*/
public <T extends ExtensionPoint> void accept(Class<T> targetClazz, String useCase, String scenario, Consumer<T> consumer) {
accept(targetClazz, BusinessScenario.valueOf(useCase, scenario), consumer);
}
/**
* "场景")执行扩展组件,适用于无返回值的业务
* @param targetClazz
* @param scenario
* @param consumer
* @param <T>
*/
public <T extends ExtensionPoint> void accept(Class<T> targetClazz, String scenario, Consumer<T> consumer) {
accept(targetClazz, BusinessScenario.valueOf(scenario), consumer);
}
/**
* 执行扩展组件,适用于无返回值的业务
* @param targetClazz
* @param businessScenario
* @param consumer
* @param <T> Parameter Type
*/
public <T extends ExtensionPoint> void accept(Class<T> targetClazz, BusinessScenario businessScenario, Consumer<T> consumer) {
T component = locateComponent(targetClazz, businessScenario);
consumer.accept(component);
}
/**
* 获取/定位扩展点组件
* @param targetClazz
* @param businessScenario
* @param <C>
* @return
*/
protected abstract <C extends ExtensionPoint> C locateComponent(Class<C> targetClazz, BusinessScenario businessScenario);
}

View File

@ -0,0 +1,56 @@
package cn.iocoder.yudao.framework.extension.core.context;
import cn.iocoder.yudao.framework.extension.core.BusinessScenario;
import cn.iocoder.yudao.framework.extension.core.point.ExtensionPoint;
/**
* @description 上下文,包含各个扩展点的相关操作
* @author Qingchen
* @version 1.0.0
* @date 2021-08-28 22:15
* @class cn.iocoder.yudao.framework.extension.core.context.ExtensionContext.java
*/
public interface ExtensionContext {
/**
* 根据业务场景唯一标识获取扩展点组件实现类
* @param businessId
* @param useCase
* @param scenario
* @param clazz
* @param <T>
* @return
*/
<T extends ExtensionPoint> T getPoint(String businessId, String useCase, String scenario, Class<T> clazz);
/**
* 根据("实例" + "场景"获取扩展点组件实现类其中业务idbusinessId= {@linkplain cn.iocoder.yudao.framework.extension.core.BusinessScenario.DEFAULT_BUSINESS_ID}
* @param useCase
* @param scenario
* @param clazz
* @param <T>
* @return
*/
<T extends ExtensionPoint> T getPoint(String useCase, String scenario, Class<T> clazz);
/**
* 根据("场景")获取扩展点组件实现类 <br/>
* 其中:
* 业务idbusinessId= {@linkplain cn.iocoder.yudao.framework.extension.core.BusinessScenario.DEFAULT_BUSINESS_ID}
* 实例useCase= {@linkplain cn.iocoder.yudao.framework.extension.core.BusinessScenario.DEFAULT_USECASE}
* @param scenario
* @param clazz
* @param <T>
* @return
*/
<T extends ExtensionPoint> T getPoint(String scenario, Class<T> clazz);
/**
* 根据业务场景唯一标识获取扩展点组件实现类
* @param businessScenario
* @param clazz
* @param <T>
* @return
*/
<T extends ExtensionPoint> T getPoint(BusinessScenario businessScenario, Class<T> clazz);
}

View File

@ -0,0 +1,45 @@
package cn.iocoder.yudao.framework.extension.core.context;
import cn.iocoder.yudao.framework.extension.core.BusinessScenario;
import cn.iocoder.yudao.framework.extension.core.factory.ExtensionFactory;
import cn.iocoder.yudao.framework.extension.core.point.ExtensionPoint;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.validation.constraints.NotNull;
/**
* @description 上下文及扩展点组件工厂的持有类
* @author Qingchen
* @version 1.0.0
* @date 2021-08-29 00:29
* @class cn.iocoder.yudao.framework.extension.core.context.ExtensionContextHolder.java
*/
@Component
@Slf4j
public class ExtensionContextHolder implements ExtensionContext{
@Autowired
private ExtensionFactory factory;
@Override
public <T extends ExtensionPoint> T getPoint(@NotNull String businessId, @NotNull String useCase, @NotNull String scenario, Class<T> clazz) {
return getPoint(BusinessScenario.valueOf(businessId, useCase, scenario), clazz);
}
@Override
public <T extends ExtensionPoint> T getPoint(@NotNull String useCase, String scenario, Class<T> clazz) {
return getPoint(BusinessScenario.valueOf(useCase, scenario), clazz);
}
@Override
public <T extends ExtensionPoint> T getPoint(@NotNull String scenario, Class<T> clazz) {
return getPoint(BusinessScenario.valueOf(scenario), clazz);
}
@Override
public <T extends ExtensionPoint> T getPoint(@NotNull BusinessScenario businessScenario, Class<T> clazz) {
return factory.get(businessScenario, clazz);
}
}

View File

@ -0,0 +1,28 @@
package cn.iocoder.yudao.framework.extension.core.context;
import cn.iocoder.yudao.framework.extension.core.BusinessScenario;
import cn.iocoder.yudao.framework.extension.core.point.ExtensionPoint;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @description 扩展组件执行器
* @author Qingchen
* @version 1.0.0
* @date 2021-08-29 00:32
* @class cn.iocoder.yudao.framework.extension.core.context.ExtensionExecutor.java
*/
@Component
@Slf4j
public class ExtensionExecutor extends AbstractComponentExecutor{
@Autowired
private ExtensionContextHolder contextHolder;
@Override
protected <C extends ExtensionPoint> C locateComponent(Class<C> targetClazz, BusinessScenario businessScenario) {
return contextHolder.getPoint(businessScenario, targetClazz);
}
}

View File

@ -0,0 +1,96 @@
package cn.iocoder.yudao.framework.extension.core.factory;
import cn.iocoder.yudao.framework.extension.core.BusinessScenario;
import cn.iocoder.yudao.framework.extension.core.point.ExtensionPoint;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.util.Objects;
/**
* @description 扩展定义(扩展坐标),标识唯一一个业务场景实现
* @author Qingchen
* @version 1.0.0
* @date 2021-08-28 23:14
* @class cn.iocoder.yudao.framework.extension.core.factory.ExtensionDefinition.java
*/
@Setter
@Getter
public class ExtensionDefinition implements Serializable {
/**
* 业务场景唯一标识(id)
*/
private String uniqueIdentify;
/**
* 扩展点实现类名称
*/
private String extensionPointName;
/**
* 业务场景
*/
private BusinessScenario businessScenario;
/**
* 扩展点实现类
*/
private ExtensionPoint extensionPoint;
/**
* class
*/
private Class extensionPointClass;
public ExtensionDefinition() {
}
public ExtensionDefinition(@NotNull BusinessScenario businessScenario, @NotNull ExtensionPoint extensionPoint) {
this.businessScenario = businessScenario;
this.extensionPoint = extensionPoint;
this.uniqueIdentify = this.businessScenario.getUniqueIdentity();
this.extensionPointClass = this.extensionPoint.getClass();
}
/**
* 构建definition
* @param businessScenario
* @param point
* @return
*/
public static ExtensionDefinition valueOf(@NotNull BusinessScenario businessScenario, @NotNull ExtensionPoint point) {
return new ExtensionDefinition(businessScenario, point);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ExtensionDefinition that = (ExtensionDefinition) o;
return Objects.equals(uniqueIdentify, that.uniqueIdentify) && Objects.equals(extensionPointName, that.extensionPointName);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((uniqueIdentify == null) ? 0 : uniqueIdentify.hashCode());
result = prime * result + ((extensionPointName == null) ? 0 : extensionPointName.hashCode());
return result;
}
@Override
public String toString() {
return "ExtensionDefinition{" +
"uniqueIdentify='" + uniqueIdentify + '\'' +
", extensionPointName='" + extensionPointName + '\'' +
'}';
}
}

View File

@ -0,0 +1,29 @@
package cn.iocoder.yudao.framework.extension.core.factory;
import cn.iocoder.yudao.framework.extension.core.BusinessScenario;
import cn.iocoder.yudao.framework.extension.core.point.ExtensionPoint;
/**
* @description 扩展点工厂
* @author Qingchen
* @version 1.0.0
* @date 2021-08-28 23:04
* @class cn.iocoder.yudao.framework.extension.core.factory.ExtensionFactory.java
*/
public interface ExtensionFactory {
/**
* 注册所有扩展点实现类
* @param basePackage
*/
void register(String basePackage);
/**
* 根据业务场景获取指定类型的扩展点
* @param businessScenario
* @param clazz
* @param <T>
* @return
*/
<T extends ExtensionPoint> T get(BusinessScenario businessScenario, Class<T> clazz);
}

View File

@ -0,0 +1,86 @@
package cn.iocoder.yudao.framework.extension.core.factory;
import cn.iocoder.yudao.framework.extension.core.BusinessScenario;
import cn.iocoder.yudao.framework.extension.core.point.ExtensionPoint;
import cn.iocoder.yudao.framework.extension.core.stereotype.Extension;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.support.AopUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.ClassUtils;
import javax.validation.constraints.NotNull;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @description 注册工厂
* @author Qingchen
* @version 1.0.0
* @date 2021-08-28 23:07
* @class cn.iocoder.yudao.framework.extension.core.factory.ExtensionRegisterFactory.java
*/
@Component
@Slf4j
public class ExtensionRegisterFactory implements ExtensionFactory {
/**
* spring ApplicationContext
*/
private ApplicationContext applicationContext;
/**
* 扩展点实现类集合
*/
private Map<String, ExtensionDefinition> registerExtensionBeans = new ConcurrentHashMap<>();
@Override
public void register(String basePackage) {
final Map<String, Object> beans = applicationContext.getBeansWithAnnotation(Extension.class);
if(beans == null || beans.isEmpty()) {
return;
}
beans.values().forEach(point -> doRegister((ExtensionPoint) point));
log.info("业务场景相关扩展点注册完成,注册数量: {}", registerExtensionBeans.size());
}
@Override
public <T extends ExtensionPoint> T get(BusinessScenario businessScenario, Class<T> clazz) {
final ExtensionDefinition definition = registerExtensionBeans.get(businessScenario.getUniqueIdentity());
if(definition == null) {
log.error("获取业务场景扩展点实现失败,失败原因:尚未定义该业务场景相关扩展点。{}", businessScenario);
throw new RuntimeException("尚未定义该业务场景相关扩展点 [" + businessScenario + "]");
}
return (T) definition.getExtensionPoint();
}
/**
* 注册扩展点
* @param point
*/
private void doRegister(@NotNull ExtensionPoint point) {
Class<?> extensionClazz = point.getClass();
if (AopUtils.isAopProxy(point)) {
extensionClazz = ClassUtils.getUserClass(point);
}
Extension extension = AnnotationUtils.findAnnotation(extensionClazz, Extension.class);
final BusinessScenario businessScenario = BusinessScenario.valueOf(extension.businessId(), extension.useCase(), extension.scenario());
final ExtensionDefinition definition = ExtensionDefinition.valueOf(businessScenario, point);
final ExtensionDefinition exist = registerExtensionBeans.get(businessScenario.getUniqueIdentity());
if(exist != null && !exist.equals(definition)) {
throw new RuntimeException("相同的业务场景重复注册了不同类型的扩展点实现 :【" + definition + "】【" + exist + "");
}
registerExtensionBeans.put(businessScenario.getUniqueIdentity(), definition);
}
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
}

View File

@ -0,0 +1,8 @@
/**
* @description core 核心
* @author Qingchen
* @version 1.0.0
* @date 2021-08-28 21:54
* @class cn.iocoder.yudao.framework.extension.core.package-info.java
*/
package cn.iocoder.yudao.framework.extension.core;

View File

@ -0,0 +1,11 @@
package cn.iocoder.yudao.framework.extension.core.point;
/**
* @description 扩展点 <br/>
* 表示一块逻辑在不同的业务有不同的实现,使用扩展点做接口申明,然后用{@linkplain cn.iocoder.yudao.framework.extension.core.stereotype.Extension}(扩展)去实现扩展点。
* @author Qingchen
* @version 1.0.0
* @date 2021-08-28 22:06
* @class cn.iocoder.yudao.framework.extension.core.point.ExtensionPoint.java
*/
public interface ExtensionPoint {
}

View File

@ -0,0 +1,41 @@
package cn.iocoder.yudao.framework.extension.core.stereotype;
import cn.iocoder.yudao.framework.extension.core.BusinessScenario;
import org.springframework.stereotype.Component;
import java.lang.annotation.*;
/**
* @description 表示带注释的类是“扩展组件”
* @author Qingchen
* @version 1.0.0
* @date 2021-08-28 21:59
* @class cn.iocoder.yudao.framework.extension.core.stereotype.Extension.java
*/
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Component
public @interface Extension {
/**
* 业务 <br/>
* 一个自负盈亏的财务主体比如tmall、淘宝和零售通就是三个不同的业务
* @return
*/
String businessId() default BusinessScenario.DEFAULT_BUSINESS_ID;
/**
* 用例 <br/>
* 描述了用户和系统之间的互动,每个用例提供了一个或多个场景。比如,支付订单就是一个典型的用例。
* @return
*/
String useCase() default BusinessScenario.DEFAULT_USECASE;
/**
* 场景 <br/>
* 场景也被称为用例的实例Instance包括用例所有的可能情况正常的和异常的。比如对于"订单支付"这个用例,就有“支付宝支付”、“银行卡支付”、"微信支付"等多个场景
* @return
*/
String scenario() default BusinessScenario.DEFAULT_SCENARIO;
}

View File

@ -0,0 +1,8 @@
/**
* @description 扩展点组件
* @author Qingchen
* @version 1.0.0
* @date 2021-08-28 14:35
* @class cn.iocoder.yudao.framework.extension.package-info.java
*/
package cn.iocoder.yudao.framework.extension;

View File

@ -0,0 +1,2 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.iocoder.yudao.framework.extension.config.YudaoExtensionAutoConfiguration

View File

@ -0,0 +1,19 @@
package cn.iocoder.yudao.framework.extension;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @description Application
* @author Qingchen
* @version 1.0.0
* @date 2021-08-30 10:32
* @class cn.iocoder.yudao.framework.extension.Application.java
*/
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

View File

@ -0,0 +1,43 @@
package cn.iocoder.yudao.framework.extension;
import cn.hutool.core.util.IdUtil;
import cn.hutool.json.JSONUtil;
import cn.iocoder.yudao.framework.extension.core.BusinessScenario;
import cn.iocoder.yudao.framework.extension.core.context.ExtensionExecutor;
import cn.iocoder.yudao.framework.extension.pay.PayExtensionPoint;
import cn.iocoder.yudao.framework.extension.pay.command.TransactionsCommand;
import cn.iocoder.yudao.framework.extension.pay.domain.TransactionsResult;
import lombok.extern.slf4j.Slf4j;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.math.BigDecimal;
/**
* @description
* @author Qingchen
* @version 1.0.0
* @date 2021-08-30 10:30
* @class cn.iocoder.yudao.framework.extension.ExtensionTest.java
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = Application.class)
@Slf4j
public class ExtensionTest {
@Autowired
private ExtensionExecutor extensionExecutor;
@Test
public void unifiedOrder() {
final BusinessScenario scenario = BusinessScenario.valueOf("pay", "jsapi", "wechat");
final TransactionsCommand command = new TransactionsCommand(IdUtil.objectId(), new BigDecimal(105), "Image形象店-深圳腾大-QQ公仔", "https://www.weixin.qq.com/wxpay/pay.php");
final TransactionsResult result = extensionExecutor.execute(PayExtensionPoint.class, scenario, extension -> extension.unifiedOrder(command));
log.info("result is: {}", JSONUtil.toJsonStr(result));
Assert.assertSame("wechat", result.getChannel());
}
}

View File

@ -0,0 +1,8 @@
/**
* @description
* @author Qingchen
* @version 1.0.0
* @date 2021-08-30 10:25
* @class cn.iocoder.yudao.framework.extension.package-info.java
*/
package cn.iocoder.yudao.framework.extension;

View File

@ -0,0 +1,22 @@
package cn.iocoder.yudao.framework.extension.pay;
import cn.iocoder.yudao.framework.extension.core.point.ExtensionPoint;
import cn.iocoder.yudao.framework.extension.pay.command.TransactionsCommand;
import cn.iocoder.yudao.framework.extension.pay.domain.TransactionsResult;
/**
* @description 支付操作接口
* @author Qingchen
* @version 1.0.0
* @date 2021-08-30 10:35
* @class cn.iocoder.yudao.framework.extension.pay.PayExtensionPoint.java
*/
public interface PayExtensionPoint extends ExtensionPoint {
/**
* 统一下单:获取"预支付交易会话标识"
* @param command
* @return
*/
TransactionsResult unifiedOrder(TransactionsCommand command);
}

View File

@ -0,0 +1,40 @@
package cn.iocoder.yudao.framework.extension.pay.command;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* @description 下单请求
* @author Qingchen
* @version 1.0.0
* @date 2021-08-30 10:48
* @class cn.iocoder.yudao.framework.extension.pay.command.TransactionsCommand.java
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TransactionsCommand implements Serializable {
/**
* 订单编号
*/
private String orderNo;
/**
* 支付金额
*/
private BigDecimal amount;
/**
* 商品描述
*/
private String productDescription;
/**
* 通知地址
*/
private String notifyUrl;
}

View File

@ -0,0 +1,39 @@
package cn.iocoder.yudao.framework.extension.pay.domain;
import lombok.*;
import java.io.Serializable;
/**
* @description 下单: 预支付交易单返回结果
* @author Qingchen
* @version 1.0.0
* @date 2021-08-30 10:43
* @class cn.iocoder.yudao.framework.extension.pay.domain.TransactionsResult.java
*/
@Data
@AllArgsConstructor
public class TransactionsResult implements Serializable {
/**
* 预支付交易会话标识
*/
private String prepayId;
/**
* 订单编号
*/
private String orderNo;
/**
* 系统内部支付单号
*/
private String paymentNo;
/**
* 支付渠道:微信 or 支付宝
*/
private String channel;
}

View File

@ -0,0 +1,26 @@
package cn.iocoder.yudao.framework.extension.pay.impl;
import cn.hutool.core.util.IdUtil;
import cn.hutool.json.JSONUtil;
import cn.iocoder.yudao.framework.extension.core.stereotype.Extension;
import cn.iocoder.yudao.framework.extension.pay.PayExtensionPoint;
import cn.iocoder.yudao.framework.extension.pay.command.TransactionsCommand;
import cn.iocoder.yudao.framework.extension.pay.domain.TransactionsResult;
import lombok.extern.slf4j.Slf4j;
/**
* @description 微信 JSAPI 支付
* @author Qingchen
* @version 1.0.0
* @date 2021-08-30 10:38
* @class cn.iocoder.yudao.framework.extension.pay.impl.AlipayService.java
*/
@Extension(businessId = "pay", useCase = "jsapi", scenario = "alipay")
@Slf4j
public class AlipayService implements PayExtensionPoint {
@Override
public TransactionsResult unifiedOrder(TransactionsCommand command) {
log.info("微信 JSAPI 支付:{}", JSONUtil.toJsonStr(command));
return new TransactionsResult("alipay26112221580621e9b071c00d9e093b0000", command.getOrderNo(), IdUtil.objectId(), "alipay");
}
}

View File

@ -0,0 +1,26 @@
package cn.iocoder.yudao.framework.extension.pay.impl;
import cn.hutool.core.util.IdUtil;
import cn.hutool.json.JSONUtil;
import cn.iocoder.yudao.framework.extension.core.stereotype.Extension;
import cn.iocoder.yudao.framework.extension.pay.PayExtensionPoint;
import cn.iocoder.yudao.framework.extension.pay.command.TransactionsCommand;
import cn.iocoder.yudao.framework.extension.pay.domain.TransactionsResult;
import lombok.extern.slf4j.Slf4j;
/**
* @description 微信 JSAPI 支付
* @author Qingchen
* @version 1.0.0
* @date 2021-08-30 10:37
* @class cn.iocoder.yudao.framework.extension.pay.impl.WechatPayService.java
*/
@Extension(businessId = "pay", useCase = "jsapi", scenario = "wechat")
@Slf4j
public class WechatPayService implements PayExtensionPoint {
@Override
public TransactionsResult unifiedOrder(TransactionsCommand command) {
log.info("微信 JSAPI 支付:{}", JSONUtil.toJsonStr(command));
return new TransactionsResult("wx26112221580621e9b071c00d9e093b0000", command.getOrderNo(), IdUtil.objectId(), "wechat");
}
}

View File

@ -0,0 +1,19 @@
### 作用
为了解决同一个流程不同业务有不同处理逻辑而产生,减少代码中 if else 逻辑,降低代码的耦合性,通过统一的扩展形式来支撑业务的变化。
### 原理
https://blog.csdn.net/significantfrank/article/details/100074716
### 使用介绍
参考测试代码 `cn.iocoder.yudao.framework.extension.ExtensionTest`

View File

@ -28,6 +28,10 @@ public interface BaseMapperX<T> extends BaseMapper<T> {
return selectOne(new QueryWrapper<T>().eq(field, value));
}
default T selectOne(String field1, Object value1, String field2, Object value2) {
return selectOne(new QueryWrapper<T>().eq(field1, value1).eq(field2, value2));
}
default Integer selectCount(String field, Object value) {
return selectCount(new QueryWrapper<T>().eq(field, value));
}

View File

@ -6,7 +6,7 @@ import cn.hutool.extra.servlet.ServletUtil;
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.apilog.core.service.ApiAccessLogFrameworkService;
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.util.monitor.TracerUtils;
import cn.iocoder.yudao.framework.web.config.WebProperties;
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
@ -68,7 +68,7 @@ public class ApiAccessLogFilter extends OncePerRequestFilter {
private void createApiAccessLog(HttpServletRequest request, Date beginTime,
Map<String, String> queryString, String requestBody, Exception ex) {
ApiAccessLogCreateDTO accessLog = new ApiAccessLogCreateDTO();
ApiAccessLogCreateReqDTO accessLog = new ApiAccessLogCreateReqDTO();
try {
this.buildApiAccessLogDTO(accessLog, request, beginTime, queryString, requestBody, ex);
apiAccessLogFrameworkService.createApiAccessLogAsync(accessLog);
@ -77,7 +77,7 @@ public class ApiAccessLogFilter extends OncePerRequestFilter {
}
}
private void buildApiAccessLogDTO(ApiAccessLogCreateDTO accessLog, HttpServletRequest request, Date beginTime,
private void buildApiAccessLogDTO(ApiAccessLogCreateReqDTO accessLog, HttpServletRequest request, Date beginTime,
Map<String, String> queryString, String requestBody, Exception ex) {
// 处理用户信息
accessLog.setUserId(WebFrameworkUtils.getLoginUserId(request));

View File

@ -1,9 +1,8 @@
package cn.iocoder.yudao.framework.apilog.core.service;
import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiAccessLogCreateDTO;
import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiAccessLogCreateReqDTO;
import javax.validation.Valid;
import java.util.concurrent.Future;
/**
* API 访问日志 Framework Service 接口
@ -17,6 +16,6 @@ public interface ApiAccessLogFrameworkService {
*
* @param createDTO 创建信息
*/
void createApiAccessLogAsync(@Valid ApiAccessLogCreateDTO createDTO);
void createApiAccessLogAsync(@Valid ApiAccessLogCreateReqDTO createDTO);
}

View File

@ -1,9 +1,8 @@
package cn.iocoder.yudao.framework.apilog.core.service;
import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiErrorLogCreateDTO;
import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiErrorLogCreateReqDTO;
import javax.validation.Valid;
import java.util.concurrent.Future;
/**
* API 错误日志 Framework Service 接口
@ -17,6 +16,6 @@ public interface ApiErrorLogFrameworkService {
*
* @param createDTO 创建信息
*/
void createApiErrorLogAsync(@Valid ApiErrorLogCreateDTO createDTO);
void createApiErrorLogAsync(@Valid ApiErrorLogCreateReqDTO createDTO);
}

View File

@ -11,7 +11,7 @@ import java.util.Date;
* @author 芋道源码
*/
@Data
public class ApiAccessLogCreateDTO {
public class ApiAccessLogCreateReqDTO {
/**
* 链路追踪编号

View File

@ -4,7 +4,6 @@ import lombok.Data;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.util.Date;
/**
@ -14,7 +13,7 @@ import java.util.Date;
*/
@Data
@Accessors(chain = true)
public class ApiErrorLogCreateDTO implements Serializable {
public class ApiErrorLogCreateReqDTO {
/**
* 链路编号

View File

@ -6,7 +6,7 @@ import cn.hutool.extra.servlet.ServletUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.apilog.core.service.ApiErrorLogFrameworkService;
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.util.monitor.TracerUtils;
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
@ -15,7 +15,6 @@ import io.github.resilience4j.ratelimiter.RequestNotPermitted;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.util.Assert;
import org.springframework.validation.BindException;
@ -28,7 +27,6 @@ import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.NoHandlerFoundException;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
@ -231,7 +229,7 @@ public class GlobalExceptionHandler {
private void createExceptionLog(HttpServletRequest req, Throwable e) {
// 插入错误日志
ApiErrorLogCreateDTO errorLog = new ApiErrorLogCreateDTO();
ApiErrorLogCreateReqDTO errorLog = new ApiErrorLogCreateReqDTO();
try {
// 初始化 errorLog
initExceptionLog(errorLog, req, e);
@ -242,7 +240,7 @@ public class GlobalExceptionHandler {
}
}
private void initExceptionLog(ApiErrorLogCreateDTO errorLog, HttpServletRequest request, Throwable e) {
private void initExceptionLog(ApiErrorLogCreateReqDTO errorLog, HttpServletRequest request, Throwable e) {
// 处理用户信息
errorLog.setUserId(WebFrameworkUtils.getLoginUserId(request));
errorLog.setUserType(WebFrameworkUtils.getLoginUserType(request));