提交 44629dc1 authored 作者: hzh's avatar hzh

商城模块集成

上级 10b339a4
......@@ -15,6 +15,7 @@
<module>ruoyi-api-workflow</module>
<module>ruoyi-api-server</module>
<module>ruoyi-api-order</module>
<module>ruoyi-api-mall</module>
</modules>
<artifactId>ruoyi-api</artifactId>
......
......@@ -55,6 +55,13 @@
<version>${revision}</version>
</dependency>
<!-- 商城接口 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-api-mall</artifactId>
<version>${revision}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-api</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ruoyi-api-mall</artifactId>
<description>
ruoyi-api-resource 商城接口模块
</description>
<dependencies>
<!-- RuoYi Common Core-->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-core</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-mall</artifactId>
</dependency>
</dependencies>
</project>
package org.dromara.mall.api.domain.member;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* @author hzh
* @date 2025-02-08
**/
@Data
public class RemoteMember implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 用户id
*/
private Long id;
/**
* 第三方用户ID
*/
private Long thirdUserId;
/**
* 手机号
*/
private String phonenumber;
}
package org.dromara.mall.api.domain.member;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serial;
import java.io.Serializable;
/**
* @author hzh
* @date 2025-02-08
**/
@Data
@Accessors(chain = true)
public class RemoteMemberSave implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 第三方用户ID
*/
private Long thirdUserId;
/**
* 手机号
*/
private String phonenumber;
/**
* 注册IP
*/
private String registerIp;
}
package org.dromara.mall.api.member;
import org.dromara.mall.api.domain.member.RemoteMember;
import org.dromara.mall.api.domain.member.RemoteMemberSave;
/**
* 会员中心
*
* @author wenhe
*/
public interface RemoteMemberService {
/**
* 根据第三方用户ID查询会员信息
*
* @param thirdUserId 第三方用户ID
* @return 会员信息
*/
RemoteMember selectByThirdUserId(Long thirdUserId);
/**
* 保存会员信息
*
* @param member 会员信息
* @return 会员信息
*/
RemoteMember saveMember(RemoteMemberSave member);
}
package org.dromara.mall.api.pay;
/**
* @author wenhe
*/
public interface RemotePayNotifyService {
/**
* 执行回调通知
*
* 注意,该方法提供给定时任务调用。目前是 yudao-server 进行调用
* @return 通知数量
*/
int executeNotify() throws InterruptedException;
}
package org.dromara.mall.api.pay;
import java.time.LocalDateTime;
/**
* @author wenhe
*/
public interface RemotePayOrderService {
/**
* 将已过期的订单,状态修改为已关闭
*
* @return 过期的订单数量
*/
int expireOrder();
/**
* 同步订单的支付状态
*
* @param minCreateTime 最小创建时间
* @return 同步到已支付的订单数量
*/
int syncOrder(LocalDateTime minCreateTime);
}
package org.dromara.mall.api.pay;
/**
* @author wenhe
*/
public interface RemotePayRefundService {
/**
* 同步渠道退款的退款状态
*
* @return 同步到状态的退款数量,包括退款成功、退款失败
*/
int syncRefund();
}
package org.dromara.mall.api.pay;
/**
* @author wenhe
*/
public interface RemotePayTransferService {
/**
* 同步渠道转账单状态
*
* @return 同步到状态的转账数量,包括转账成功、转账失败、转账中的
*/
int syncTransfer();
}
package org.dromara.mall.api.promotion;
import org.dromara.common.mall.core.KeyValue;
/**
* @author hzh
* @date 2025-02-11
* @desc 拼团过期
**/
public interface RemoteCombinationRecordService {
/**
* 处理过期拼团
*
* @return key 过期拼团数量, value 虚拟成团数量
*/
KeyValue<Integer, Integer> expireCombinationRecord();
}
package org.dromara.mall.api.promotion;
/**
* 优惠券过期
*
* @author wenhe
*/
public interface RemoteCouponService {
/**
* 过期优惠券
*
* @return 过期数量
*/
int expireCoupon();
}
package org.dromara.mall.api.statistics;
/**
* 商品统计
*
* @author wenhe
*/
public interface RemoteProductStatisticsService {
/**
* 统计指定天数的商品数据
*
* @return 统计结果
*/
String statisticsProduct(Integer days);
}
package org.dromara.mall.api.statistics;
/**
* 交易统计
*
* @author wenhe
*/
public interface RemoteTradeStatisticsService {
/**
* 统计指定天数的交易数据
*
* @param days 天数
* @return 统计结果
*/
String statisticsTrade(Integer days);
}
package org.dromara.mall.api.trade;
/**
* @author wenhe
*/
public interface RemoteBrokerageRecordService {
/**
* 解冻佣金:将待结算的佣金记录,状态修改为已结算
*
* @return 解冻佣金的数量
*/
int unfreezeRecord();
}
package org.dromara.mall.api.trade;
/**
* @author wenhe
*/
public interface RemoteTradeOrderUpdateService {
/**
* 【系统】自动取消订单
*
* @return 取消数量
*/
int cancelOrderBySystem();
/**
* 【系统】创建订单项的评论
*
* @return 被评论的订单数
*/
int createOrderItemCommentBySystem();
/**
* 【系统】自动收货交易订单
*
* @return 收货数量
*/
int receiveOrderBySystem();
}
......@@ -91,6 +91,11 @@
<artifactId>ruoyi-api-server</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-api-mall</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-weixin</artifactId>
......
package org.dromara.auth.service.impl;
import lombok.RequiredArgsConstructor;
import org.apache.dubbo.config.annotation.DubboReference;
import org.dromara.auth.service.IAuthStrategy;
import org.dromara.common.core.domain.model.LoginBody;
import org.dromara.common.core.enums.UserType;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.mall.api.domain.member.RemoteMember;
import org.dromara.mall.api.domain.member.RemoteMemberSave;
import org.dromara.mall.api.member.RemoteMemberService;
import org.dromara.system.api.RemoteUserService;
import org.dromara.system.api.domain.bo.RemoteUserBo;
import org.dromara.system.api.model.XcxLoginUser;
import org.jetbrains.annotations.Nullable;
import org.springframework.stereotype.Service;
import static org.dromara.common.core.constant.TenantConstants.DEFAULT_TENANT_ID;
import static org.dromara.common.core.utils.ServletUtils.getClientIP;
/**
* @author hzh
* @date 2025-02-08
* @desc TODO
**/
@Service
@RequiredArgsConstructor
public abstract class AbstractMallStrategy implements IAuthStrategy {
@DubboReference
private RemoteUserService remoteUserService;
@DubboReference
private RemoteMemberService remoteMemberService;
@Nullable
Long getMemberId(LoginBody loginBody, XcxLoginUser loginUser) {
// 增加商城逻辑
Long thirdUserId;
if (StringUtils.equals(loginBody.getTenantId(), DEFAULT_TENANT_ID) && StringUtils.equals(loginUser.getUserType(), UserType.APP_USER.getUserType())) {
thirdUserId = loginUser.getUserId();
} else {
// 创建用户
RemoteUserBo remoteUserBo = new RemoteUserBo();
remoteUserBo.setTenantId(DEFAULT_TENANT_ID);
remoteUserBo.setUserName(StringUtils.isNotEmpty(loginUser.getPhone()) ? loginUser.getPhone() : loginUser.getUsername());
remoteUserBo.setNickName(remoteUserBo.getUserName());
remoteUserBo.setUserType(UserType.APP_USER.getUserType());
remoteUserBo.setPhonenumber(loginUser.getPhone());
thirdUserId = remoteUserService.registerUserInfo(remoteUserBo);
}
Long memberId = null;
if (thirdUserId != null) {
RemoteMember rm = remoteMemberService.selectByThirdUserId(thirdUserId);
if (rm == null) {
rm = remoteMemberService.saveMember(
new RemoteMemberSave()
.setThirdUserId(thirdUserId)
.setPhonenumber(loginUser.getPhone())
.setRegisterIp(getClientIP())
);
}
memberId = rm.getId();
}
return memberId;
}
}
......@@ -32,7 +32,7 @@ import java.util.Optional;
@Slf4j
@Service("xcx" + IAuthStrategy.BASE_NAME)
@RequiredArgsConstructor
public class XcxAuthStrategy implements IAuthStrategy {
public class XcxAuthStrategy extends AbstractMallStrategy implements IAuthStrategy {
private final SysLoginService loginService;
......@@ -70,6 +70,9 @@ public class XcxAuthStrategy implements IAuthStrategy {
loginUser.setClientKey(client.getClientKey());
loginUser.setDeviceType(client.getDeviceType());
//获取会员id
Long memberId = getMemberId(loginBody, loginUser);
SaLoginModel model = new SaLoginModel();
model.setDevice(client.getDeviceType());
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
......@@ -81,6 +84,7 @@ public class XcxAuthStrategy implements IAuthStrategy {
model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
model.setExtra(LoginHelper.YS_USER_ID, Optional.ofNullable(ru).map(RemoteUser::getUserNo).orElse(null));
model.setExtra(LoginHelper.OPEN_ID, openid);
model.setExtra(LoginHelper.MEMBER_ID, memberId);
// 生成token
LoginHelper.login(loginUser, model);
......
......@@ -32,7 +32,7 @@ import java.util.Optional;
@Slf4j
@Service("xcxPhone" + IAuthStrategy.BASE_NAME)
@RequiredArgsConstructor
public class XcxPhoneAuthStrategy implements IAuthStrategy {
public class XcxPhoneAuthStrategy extends AbstractMallStrategy implements IAuthStrategy {
private final SysLoginService loginService;
......@@ -65,6 +65,9 @@ public class XcxPhoneAuthStrategy implements IAuthStrategy {
loginUser.setClientKey(client.getClientKey());
loginUser.setDeviceType(client.getDeviceType());
//获取会员id
Long memberId = getMemberId(loginBody, loginUser);
SaLoginModel model = new SaLoginModel();
model.setDevice(client.getDeviceType());
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
......@@ -75,6 +78,7 @@ public class XcxPhoneAuthStrategy implements IAuthStrategy {
model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
model.setExtra(LoginHelper.YS_USER_ID, Optional.ofNullable(ru).map(RemoteUser::getUserNo).orElse(null));
model.setExtra(LoginHelper.OPEN_ID, loginBody.getOpenId());
model.setExtra(LoginHelper.MEMBER_ID, memberId);
// 生成token
LoginHelper.login(loginUser, model);
......
......@@ -48,6 +48,9 @@
<module>ruoyi-common-weixin</module>
<module>ruoyi-common-ys</module>
<module>ruoyi-common-pay</module>
<module>ruoyi-common-allPay</module>
<module>ruoyi-common-mall</module>
<module>ruoyi-common-yudao-mybatis</module>
</modules>
<artifactId>ruoyi-common</artifactId>
......
<?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>org.dromara</groupId>
<artifactId>ruoyi-common</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ruoyi-common-allPay</artifactId>
<description>
ruoyi-common-allPay 支付模块
支付拓展,接入国内多个支付渠道
1. 支付宝,基于官方 SDK 接入
2. 微信支付,基于 weixin-java-pay 接入
</description>
<dependencies>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-core</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-mall</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.35</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</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.35.79.ALL</version>
<exclusions>
<exclusion>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-pay</artifactId>
<version>4.6.0</version>
</dependency>
</dependencies>
</project>
package com.github.binarywang.wxpay.bean.transfer;
import com.github.binarywang.wxpay.v3.SpecEncrypt;
import com.google.gson.annotations.SerializedName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.List;
/**
* 发起商家转账API参数
*
* @author zhongjun
* created on 2022/6/17
**/
@Data
@Accessors(chain = true)
@Builder(builderMethodName = "newBuilder")
@NoArgsConstructor
@AllArgsConstructor
public class TransferBatchesRequest implements Serializable {
private static final long serialVersionUID = -2175582517588397426L;
/**
* 直连商户的appid
*/
@SerializedName("appid")
private String appid;
/**
* 商家批次单号
*/
@SerializedName("out_batch_no")
private String outBatchNo;
/**
* 批次名称
*/
@SerializedName("batch_name")
private String batchName;
/**
* 批次备注
*/
@SerializedName("batch_remark")
private String batchRemark;
/**
* 转账总金额
*/
@SerializedName("total_amount")
private Integer totalAmount;
/**
* 转账总笔数
*/
@SerializedName("total_num")
private Integer totalNum;
/**
* 转账明细列表
*/
@SpecEncrypt
@SerializedName("transfer_detail_list")
private List<TransferDetail> transferDetailList;
/**
* 转账场景ID
*/
@SerializedName("transfer_scene_id")
private String transferSceneId;
/**
* 通知地址 说明:异步接收微信支付结果通知的回调地址,通知url必须为公网可访问的url,必须为https,不能携带参数。
*/
@SerializedName("notify_url")
private String notifyUrl;
@Data
@Builder(builderMethodName = "newBuilder")
@AllArgsConstructor
@NoArgsConstructor
public static class TransferDetail {
/**
* 商家明细单号
*/
@SerializedName("out_detail_no")
private String outDetailNo;
/**
* 转账金额
*/
@SerializedName("transfer_amount")
private Integer transferAmount;
/**
* 转账备注
*/
@SerializedName("transfer_remark")
private String transferRemark;
/**
* 用户在直连商户应用下的用户标示
*/
@SerializedName("openid")
private String openid;
/**
* 收款用户姓名
*/
@SpecEncrypt
@SerializedName("user_name")
private String userName;
}
}
package org.dromara.common.pay.config;
import org.dromara.common.pay.core.client.PayClientFactory;
import org.dromara.common.pay.core.client.impl.PayClientFactoryImpl;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
/**
* 支付配置类
*
* @author 芋道源码
*/
@AutoConfiguration
public class YudaoPayAutoConfiguration {
@Bean
public PayClientFactory payClientFactory() {
return new PayClientFactoryImpl();
}
}
package org.dromara.common.pay.core.client;
import org.dromara.common.pay.core.client.dto.order.PayOrderRespDTO;
import org.dromara.common.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import org.dromara.common.pay.core.client.dto.refund.PayRefundRespDTO;
import org.dromara.common.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
import org.dromara.common.pay.core.client.dto.transfer.PayTransferRespDTO;
import org.dromara.common.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
import org.dromara.common.pay.core.enums.transfer.PayTransferTypeEnum;
import java.util.Map;
/**
* 支付客户端,用于对接各支付渠道的 SDK,实现发起支付、退款等功能
*
* @author 芋道源码
*/
public interface PayClient {
/**
* 获得渠道编号
*
* @return 渠道编号
*/
Long getId();
// ============ 支付相关 ==========
/**
* 调用支付渠道,统一下单
*
* @param reqDTO 下单信息
* @return 支付订单信息
*/
PayOrderRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO);
/**
* 解析 order 回调数据
*
* @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数
* @param body HTTP 回调接口的 request body
* @return 支付订单信息
*/
PayOrderRespDTO parseOrderNotify(Map<String, String> params, String body);
/**
* 获得支付订单信息
*
* @param outTradeNo 外部订单号
* @return 支付订单信息
*/
PayOrderRespDTO getOrder(String outTradeNo);
// ============ 退款相关 ==========
/**
* 调用支付渠道,进行退款
*
* @param reqDTO 统一退款请求信息
* @return 退款信息
*/
PayRefundRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO);
/**
* 解析 refund 回调数据
*
* @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数
* @param body HTTP 回调接口的 request body
* @return 支付订单信息
*/
PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body);
/**
* 获得退款订单信息
*
* @param outTradeNo 外部订单号
* @param outRefundNo 外部退款号
* @return 退款订单信息
*/
PayRefundRespDTO getRefund(String outTradeNo, String outRefundNo);
// ============ 转账相关 ==========
/**
* 调用渠道,进行转账
*
* @param reqDTO 统一转账请求信息
* @return 转账信息
*/
PayTransferRespDTO unifiedTransfer(PayTransferUnifiedReqDTO reqDTO);
/**
* 获得转账订单信息
*
* @param outTradeNo 外部订单号
* @param type 转账类型
* @return 转账信息
*/
PayTransferRespDTO getTransfer(String outTradeNo, PayTransferTypeEnum type);
/**
* 解析 transfer 回调数据
*
* @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数
* @param body HTTP 回调接口的 request body
* @return 转账信息
*/
PayTransferRespDTO parseTransferNotify(Map<String, String> params, String body);
}
package org.dromara.common.pay.core.client;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import jakarta.validation.Validator;
/**
* 支付客户端的配置,本质是支付渠道的配置
* 每个不同的渠道,需要不同的配置,通过子类来定义
*
* @author 芋道源码
*/
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
// @JsonTypeInfo 注解的作用,Jackson 多态
// 1. 序列化到时数据库时,增加 @class 属性。
// 2. 反序列化到内存对象时,通过 @class 属性,可以创建出正确的类型
public interface PayClientConfig {
/**
* 参数校验
*
* @param validator 校验对象
*/
void validate(Validator validator);
}
package org.dromara.common.pay.core.client;
import org.dromara.common.pay.core.enums.channel.PayChannelEnum;
/**
* 支付客户端的工厂接口
*
* @author 芋道源码
*/
public interface PayClientFactory {
/**
* 获得支付客户端
*
* @param channelId 渠道编号
* @return 支付客户端
*/
PayClient getPayClient(Long channelId);
/**
* 创建支付客户端
*
* @param channelId 渠道编号
* @param channelCode 渠道编码
* @param config 支付配置
* @return 支付客户端
*/
<Config extends PayClientConfig> PayClient createOrUpdatePayClient(Long channelId, String channelCode,
Config config);
/**
* 注册支付客户端 Class,用于模块中实现的 PayClient
*
* @param channel 支付渠道的编码的枚举
* @param payClientClass 支付客户端 class
*/
void registerPayClientClass(PayChannelEnum channel, Class<?> payClientClass);
}
package org.dromara.common.pay.core.client.dto.order;
import lombok.Data;
import lombok.experimental.Accessors;
import org.dromara.common.pay.core.enums.order.PayOrderStatusRespEnum;
import java.time.LocalDateTime;
/**
* 渠道支付订单 Response DTO
*
* @author 芋道源码
*/
@Data
@Accessors(chain = true)
public class PayOrderRespDTO {
/**
* 支付状态
*
* 枚举:{@link org.dromara.common.pay.core.enums.order.PayOrderStatusRespEnum}
*/
private Integer status;
/**
* 外部订单号
*
* 对应 PayOrderExtensionDO 的 no 字段
*/
private String outTradeNo;
/**
* 支付渠道编号
*/
private String channelOrderNo;
/**
* 支付渠道用户编号
*/
private String channelUserId;
/**
* 支付成功时间
*/
private LocalDateTime successTime;
/**
* 原始的同步/异步通知结果
*/
private Object rawData;
// ========== 主动发起支付时,会返回的字段 ==========
/**
* 展示模式
*
* 枚举 {@link org.dromara.common.pay.core.enums.order.PayOrderDisplayModeEnum} 类
*/
private String displayMode;
/**
* 展示内容
*/
private String displayContent;
/**
* 调用渠道的错误码
*
* 注意:这里返回的是业务异常,而是不系统异常。
* 如果是系统异常,则会抛出 {@link org.dromara.common.pay.core.client.exception.PayException}
*/
private String channelErrorCode;
/**
* 调用渠道报错时,错误信息
*/
private String channelErrorMsg;
public PayOrderRespDTO() {
}
/**
* 创建【WAITING】状态的订单返回
*/
public static PayOrderRespDTO waitingOf(String displayMode, String displayContent,
String outTradeNo, Object rawData) {
PayOrderRespDTO respDTO = new PayOrderRespDTO();
respDTO.status = PayOrderStatusRespEnum.WAITING.getStatus();
respDTO.displayMode = displayMode;
respDTO.displayContent = displayContent;
// 相对通用的字段
respDTO.outTradeNo = outTradeNo;
respDTO.rawData = rawData;
return respDTO;
}
/**
* 创建【SUCCESS】状态的订单返回
*/
public static PayOrderRespDTO successOf(String channelOrderNo, String channelUserId, LocalDateTime successTime,
String outTradeNo, Object rawData) {
PayOrderRespDTO respDTO = new PayOrderRespDTO();
respDTO.status = PayOrderStatusRespEnum.SUCCESS.getStatus();
respDTO.channelOrderNo = channelOrderNo;
respDTO.channelUserId = channelUserId;
respDTO.successTime = successTime;
// 相对通用的字段
respDTO.outTradeNo = outTradeNo;
respDTO.rawData = rawData;
return respDTO;
}
/**
* 创建指定状态的订单返回,适合支付渠道回调时
*/
public static PayOrderRespDTO of(Integer status, String channelOrderNo, String channelUserId, LocalDateTime successTime,
String outTradeNo, Object rawData) {
PayOrderRespDTO respDTO = new PayOrderRespDTO();
respDTO.status = status;
respDTO.channelOrderNo = channelOrderNo;
respDTO.channelUserId = channelUserId;
respDTO.successTime = successTime;
// 相对通用的字段
respDTO.outTradeNo = outTradeNo;
respDTO.rawData = rawData;
return respDTO;
}
/**
* 创建【CLOSED】状态的订单返回,适合调用支付渠道失败时
*/
public static PayOrderRespDTO closedOf(String channelErrorCode, String channelErrorMsg,
String outTradeNo, Object rawData) {
PayOrderRespDTO respDTO = new PayOrderRespDTO();
respDTO.status = PayOrderStatusRespEnum.CLOSED.getStatus();
respDTO.channelErrorCode = channelErrorCode;
respDTO.channelErrorMsg = channelErrorMsg;
// 相对通用的字段
respDTO.outTradeNo = outTradeNo;
respDTO.rawData = rawData;
return respDTO;
}
}
package org.dromara.common.pay.core.client.dto.order;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.experimental.Accessors;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.URL;
import java.time.LocalDateTime;
import java.util.Map;
/**
* 统一下单 Request DTO
*
* @author 芋道源码
*/
@Data
@Accessors(chain = true)
public class PayOrderUnifiedReqDTO {
/**
* 用户 IP
*/
@NotEmpty(message = "用户 IP 不能为空")
private String userIp;
// ========== 商户相关字段 ==========
/**
* 外部订单号
*
* 对应 PayOrderExtensionDO 的 no 字段
*/
@NotEmpty(message = "外部订单编号不能为空")
private String outTradeNo;
/**
* 商品标题
*/
@NotEmpty(message = "商品标题不能为空")
@Length(max = 32, message = "商品标题不能超过 32")
private String subject;
/**
* 商品描述信息
*/
@Length(max = 128, message = "商品描述信息长度不能超过128")
private String body;
/**
* 支付结果的 notify 回调地址
*/
@NotEmpty(message = "支付结果的回调地址不能为空")
@URL(message = "支付结果的 notify 回调地址必须是 URL 格式")
private String notifyUrl;
/**
* 支付结果的 return 回调地址
*/
@URL(message = "支付结果的 return 回调地址必须是 URL 格式")
private String returnUrl;
// ========== 订单相关字段 ==========
/**
* 支付金额,单位:分
*/
@NotNull(message = "支付金额不能为空")
@DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
private Integer price;
/**
* 支付过期时间
*/
@NotNull(message = "支付过期时间不能为空")
private LocalDateTime expireTime;
// ========== 拓展参数 ==========
/**
* 支付渠道的额外参数
*
* 例如说,微信公众号需要传递 openid 参数
*/
private Map<String, String> channelExtras;
/**
* 展示模式
*
* 如果不传递,则每个支付渠道使用默认的方式
*
* 枚举 {@link org.dromara.common.pay.core.enums.order.PayOrderDisplayModeEnum}
*/
private String displayMode;
}
package org.dromara.common.pay.core.client.dto.refund;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.dromara.common.pay.core.enums.refund.PayRefundStatusRespEnum;
import java.time.LocalDateTime;
/**
* 渠道退款订单 Response DTO
*
* @author jason
*/
@Data
@AllArgsConstructor
public class PayRefundRespDTO {
/**
* 退款状态
*
* 枚举 {@link PayRefundStatusRespEnum}
*/
private Integer status;
/**
* 外部退款号
*
* 对应 PayRefundDO 的 no 字段
*/
private String outRefundNo;
/**
* 渠道退款单号
*
* 对应 PayRefundDO.channelRefundNo 字段
*/
private String channelRefundNo;
/**
* 退款成功时间
*/
private LocalDateTime successTime;
/**
* 原始的异步通知结果
*/
private Object rawData;
/**
* 调用渠道的错误码
*
* 注意:这里返回的是业务异常,而是不系统异常。
* 如果是系统异常,则会抛出 {@link org.dromara.common.pay.core.client.exception.PayException}
*/
private String channelErrorCode;
/**
* 调用渠道报错时,错误信息
*/
private String channelErrorMsg;
private PayRefundRespDTO() {
}
/**
* 创建【WAITING】状态的退款返回
*/
public static PayRefundRespDTO waitingOf(String channelRefundNo,
String outRefundNo, Object rawData) {
PayRefundRespDTO respDTO = new PayRefundRespDTO();
respDTO.status = PayRefundStatusRespEnum.WAITING.getStatus();
respDTO.channelRefundNo = channelRefundNo;
// 相对通用的字段
respDTO.outRefundNo = outRefundNo;
respDTO.rawData = rawData;
return respDTO;
}
/**
* 创建【SUCCESS】状态的退款返回
*/
public static PayRefundRespDTO successOf(String channelRefundNo, LocalDateTime successTime,
String outRefundNo, Object rawData) {
PayRefundRespDTO respDTO = new PayRefundRespDTO();
respDTO.status = PayRefundStatusRespEnum.SUCCESS.getStatus();
respDTO.channelRefundNo = channelRefundNo;
respDTO.successTime = successTime;
// 相对通用的字段
respDTO.outRefundNo = outRefundNo;
respDTO.rawData = rawData;
return respDTO;
}
/**
* 创建【FAILURE】状态的退款返回
*/
public static PayRefundRespDTO failureOf(String outRefundNo, Object rawData) {
return failureOf(null, null,
outRefundNo, rawData);
}
/**
* 创建【FAILURE】状态的退款返回
*/
public static PayRefundRespDTO failureOf(String channelErrorCode, String channelErrorMsg,
String outRefundNo, Object rawData) {
PayRefundRespDTO respDTO = new PayRefundRespDTO();
respDTO.status = PayRefundStatusRespEnum.FAILURE.getStatus();
respDTO.channelErrorCode = channelErrorCode;
respDTO.channelErrorMsg = channelErrorMsg;
// 相对通用的字段
respDTO.outRefundNo = outRefundNo;
respDTO.rawData = rawData;
return respDTO;
}
}
package org.dromara.common.pay.core.client.dto.refund;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.hibernate.validator.constraints.URL;
/**
* 统一 退款 Request DTO
*
* @author jason
*/
@Accessors(chain = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class PayRefundUnifiedReqDTO {
/**
* 外部订单号
*
* 对应 PayOrderExtensionDO 的 no 字段
*/
@NotEmpty(message = "外部订单编号不能为空")
private String outTradeNo;
/**
* 外部退款号
*
* 对应 PayRefundDO 的 no 字段
*/
@NotEmpty(message = "退款请求单号不能为空")
private String outRefundNo;
/**
* 退款原因
*/
@NotEmpty(message = "退款原因不能为空")
private String reason;
/**
* 支付金额,单位:分
*
* 目前微信支付在退款的时候,必须传递该字段
*/
@NotNull(message = "支付金额不能为空")
@DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
private Integer payPrice;
/**
* 退款金额,单位:分
*/
@NotNull(message = "退款金额不能为空")
@DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
private Integer refundPrice;
/**
* 退款结果的 notify 回调地址
*/
@NotEmpty(message = "支付结果的回调地址不能为空")
@URL(message = "支付结果的 notify 回调地址必须是 URL 格式")
private String notifyUrl;
}
package org.dromara.common.pay.core.client.dto.transfer;
import lombok.Data;
import org.dromara.common.pay.core.enums.transfer.PayTransferStatusRespEnum;
import java.time.LocalDateTime;
/**
* 统一转账 Response DTO
*
* @author jason
*/
@Data
public class PayTransferRespDTO {
/**
* 转账状态
*
* 关联 {@link PayTransferStatusRespEnum#getStatus()}
*/
private Integer status;
/**
* 外部转账单号
*
*/
private String outTransferNo;
/**
* 支付渠道编号
*/
private String channelTransferNo;
/**
* 支付成功时间
*/
private LocalDateTime successTime;
/**
* 原始的返回结果
*/
private Object rawData;
/**
* 调用渠道的错误码
*/
private String channelErrorCode;
/**
* 调用渠道报错时,错误信息
*/
private String channelErrorMsg;
/**
* 创建【WAITING】状态的转账返回
*/
public static PayTransferRespDTO waitingOf(String channelTransferNo,
String outTransferNo, Object rawData) {
PayTransferRespDTO respDTO = new PayTransferRespDTO();
respDTO.status = PayTransferStatusRespEnum.WAITING.getStatus();
respDTO.channelTransferNo = channelTransferNo;
respDTO.outTransferNo = outTransferNo;
respDTO.rawData = rawData;
return respDTO;
}
/**
* 创建【IN_PROGRESS】状态的转账返回
*/
public static PayTransferRespDTO dealingOf(String channelTransferNo,
String outTransferNo, Object rawData) {
PayTransferRespDTO respDTO = new PayTransferRespDTO();
respDTO.status = PayTransferStatusRespEnum.IN_PROGRESS.getStatus();
respDTO.channelTransferNo = channelTransferNo;
respDTO.outTransferNo = outTransferNo;
respDTO.rawData = rawData;
return respDTO;
}
/**
* 创建【CLOSED】状态的转账返回
*/
public static PayTransferRespDTO closedOf(String channelErrorCode, String channelErrorMsg,
String outTransferNo, Object rawData) {
PayTransferRespDTO respDTO = new PayTransferRespDTO();
respDTO.status = PayTransferStatusRespEnum.CLOSED.getStatus();
respDTO.channelErrorCode = channelErrorCode;
respDTO.channelErrorMsg = channelErrorMsg;
// 相对通用的字段
respDTO.outTransferNo = outTransferNo;
respDTO.rawData = rawData;
return respDTO;
}
/**
* 创建【SUCCESS】状态的转账返回
*/
public static PayTransferRespDTO successOf(String channelTransferNo, LocalDateTime successTime,
String outTransferNo, Object rawData) {
PayTransferRespDTO respDTO = new PayTransferRespDTO();
respDTO.status = PayTransferStatusRespEnum.SUCCESS.getStatus();
respDTO.channelTransferNo = channelTransferNo;
respDTO.successTime = successTime;
// 相对通用的字段
respDTO.outTransferNo = outTransferNo;
respDTO.rawData = rawData;
return respDTO;
}
}
package org.dromara.common.pay.core.client.dto.transfer;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.experimental.Accessors;
import org.dromara.common.mall.validation.InEnum;
import org.dromara.common.pay.core.enums.transfer.PayTransferTypeEnum;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.URL;
import java.util.Map;
/**
* 统一转账 Request DTO
*
* @author jason
*/
@Data
@Accessors(chain = true)
public class PayTransferUnifiedReqDTO {
/**
* 转账类型
*
* 关联 {@link PayTransferTypeEnum#getType()}
*/
@NotNull(message = "转账类型不能为空")
@InEnum(PayTransferTypeEnum.class)
private Integer type;
/**
* 用户 IP
*/
@NotEmpty(message = "用户 IP 不能为空")
private String userIp;
@NotEmpty(message = "外部转账单编号不能为空")
private String outTransferNo;
/**
* 转账金额,单位:分
*/
@NotNull(message = "转账金额不能为空")
@Min(value = 1, message = "转账金额必须大于零")
private Integer price;
/**
* 转账标题
*/
@NotEmpty(message = "转账标题不能为空")
@Length(max = 128, message = "转账标题不能超过 128")
private String subject;
/**
* 收款人姓名
*/
@NotBlank(message = "收款人姓名不能为空", groups = {PayTransferTypeEnum.Alipay.class})
private String userName;
/**
* 支付宝登录号
*/
@NotBlank(message = "支付宝登录号不能为空", groups = {PayTransferTypeEnum.Alipay.class})
private String alipayLogonId;
/**
* 微信 openId
*/
@NotBlank(message = "微信 openId 不能为空", groups = {PayTransferTypeEnum.WxPay.class})
private String openid;
/**
* 支付渠道的额外参数
*/
private Map<String, String> channelExtras;
/**
* 转账结果的 notify 回调地址
*/
@NotEmpty(message = "转账结果的回调地址不能为空")
@URL(message = "转账结果的 notify 回调地址必须是 URL 格式")
private String notifyUrl;
}
package org.dromara.common.pay.core.client.dto.transfer;
import com.github.binarywang.wxpay.bean.notify.OriginNotifyResponse;
import com.github.binarywang.wxpay.bean.notify.WxPayBaseNotifyV3Result;
import com.google.gson.annotations.SerializedName;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
// TODO @luchi:这个可以复用 wxjava 里的类么?
@NoArgsConstructor
public class WxPayTransferPartnerNotifyV3Result implements Serializable, WxPayBaseNotifyV3Result<WxPayTransferPartnerNotifyV3Result.TransferNotifyResult> {
private static final long serialVersionUID = -1L;
/**
* 源数据
*/
private OriginNotifyResponse rawData;
/**
* 解密后的数据
*/
private TransferNotifyResult result;
@Override
public void setRawData(OriginNotifyResponse rawData) {
this.rawData = rawData;
}
@Override
public void setResult(TransferNotifyResult data) {
this.result = data;
}
public TransferNotifyResult getResult() {
return result;
}
public OriginNotifyResponse getRawData() {
return rawData;
}
@Data
@NoArgsConstructor
public static class TransferNotifyResult implements Serializable {
private static final long serialVersionUID = 1L;
/*********************** 公共字段 ********************
/**
* 商家批次单号
*/
@SerializedName(value = "out_batch_no")
protected String outBatchNo;
/**
* 微信批次单号
*/
@SerializedName(value = "batch_id")
protected String batchId;
/**
* 批次状态
*/
@SerializedName(value = "batch_status")
protected String batchStatus;
/**
* 批次总笔数
*/
@SerializedName(value = "total_num")
protected Integer totalNum;
/**
* 批次总金额
*/
@SerializedName(value = "total_amount")
protected Integer totalAmount;
/**
* 批次更新时间
*/
@SerializedName(value = "update_time")
private String updateTime;
/*********************** FINISHED ********************
/**
* 转账成功金额
*/
@SerializedName(value = "success_amount")
protected Integer successAmount;
/**
* 转账成功笔数
*/
@SerializedName(value = "success_num")
protected Integer successNum;
/**
* 转账失败金额
*/
@SerializedName(value = "fail_amount")
protected Integer failAmount;
/**
* 转账失败笔数
*/
@SerializedName(value = "fail_num")
protected Integer failNum;
/*********************** CLOSED ********************
/**
* 商户号
*/
@SerializedName(value = "mchid")
protected String mchId;
/**
* 批次关闭原因
*/
@SerializedName(value = "close_reason")
protected String closeReason;
}
}
package org.dromara.common.pay.core.client.exception;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 支付系统异常 Exception
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class PayException extends RuntimeException {
public PayException(Throwable cause) {
super(cause);
}
}
package org.dromara.common.pay.core.client.impl;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.mall.util.validation.ValidationUtils;
import org.dromara.common.pay.core.client.PayClient;
import org.dromara.common.pay.core.client.PayClientConfig;
import org.dromara.common.pay.core.client.dto.order.PayOrderRespDTO;
import org.dromara.common.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import org.dromara.common.pay.core.client.dto.refund.PayRefundRespDTO;
import org.dromara.common.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
import org.dromara.common.pay.core.client.dto.transfer.PayTransferRespDTO;
import org.dromara.common.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
import org.dromara.common.pay.core.client.exception.PayException;
import org.dromara.common.pay.core.enums.transfer.PayTransferTypeEnum;
import java.util.Map;
import static org.dromara.common.core.utils.json.JsonUtils.toJsonString;
import static org.dromara.common.mall.exception.enums.GlobalErrorCodeConstants.NOT_IMPLEMENTED;
import static org.dromara.common.mall.exception.util.ServiceExceptionUtil.exception;
/**
* 支付客户端的抽象类,提供模板方法,减少子类的冗余代码
*
* @author 芋道源码
*/
@Slf4j
public abstract class AbstractPayClient<Config extends PayClientConfig> implements PayClient {
/**
* 渠道编号
*/
private final Long channelId;
/**
* 渠道编码
*/
@SuppressWarnings("FieldCanBeLocal")
private final String channelCode;
/**
* 支付配置
*/
protected Config config;
public AbstractPayClient(Long channelId, String channelCode, Config config) {
this.channelId = channelId;
this.channelCode = channelCode;
this.config = config;
}
/**
* 初始化
*/
public final void init() {
doInit();
log.debug("[init][客户端({}) 初始化完成]", getId());
}
/**
* 自定义初始化
*/
protected abstract void doInit();
public final void refresh(Config config) {
// 判断是否更新
if (config.equals(this.config)) {
return;
}
log.info("[refresh][客户端({})发生变化,重新初始化]", getId());
this.config = config;
// 初始化
this.init();
}
@Override
public Long getId() {
return channelId;
}
// ============ 支付相关 ==========
@Override
public final PayOrderRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
ValidationUtils.validate(reqDTO);
// 执行统一下单
PayOrderRespDTO resp;
try {
resp = doUnifiedOrder(reqDTO);
} catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
throw ex;
} catch (Throwable ex) {
// 系统异常,则包装成 PayException 异常抛出
log.error("[unifiedOrder][客户端({}) request({}) 发起支付异常]",
getId(), toJsonString(reqDTO), ex);
throw buildPayException(ex);
}
return resp;
}
protected abstract PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO)
throws Throwable;
@Override
public final PayOrderRespDTO parseOrderNotify(Map<String, String> params, String body) {
try {
return doParseOrderNotify(params, body);
} catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
throw ex;
} catch (Throwable ex) {
log.error("[parseOrderNotify][客户端({}) params({}) body({}) 解析失败]",
getId(), params, body, ex);
throw buildPayException(ex);
}
}
protected abstract PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body)
throws Throwable;
@Override
public final PayOrderRespDTO getOrder(String outTradeNo) {
try {
return doGetOrder(outTradeNo);
} catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
throw ex;
} catch (Throwable ex) {
log.error("[getOrder][客户端({}) outTradeNo({}) 查询支付单异常]",
getId(), outTradeNo, ex);
throw buildPayException(ex);
}
}
protected abstract PayOrderRespDTO doGetOrder(String outTradeNo)
throws Throwable;
// ============ 退款相关 ==========
@Override
public final PayRefundRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
ValidationUtils.validate(reqDTO);
// 执行统一退款
PayRefundRespDTO resp;
try {
resp = doUnifiedRefund(reqDTO);
} catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
throw ex;
} catch (Throwable ex) {
// 系统异常,则包装成 PayException 异常抛出
log.error("[unifiedRefund][客户端({}) request({}) 发起退款异常]",
getId(), toJsonString(reqDTO), ex);
throw buildPayException(ex);
}
return resp;
}
protected abstract PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable;
@Override
public final PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body) {
try {
return doParseRefundNotify(params, body);
} catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
throw ex;
} catch (Throwable ex) {
log.error("[parseRefundNotify][客户端({}) params({}) body({}) 解析失败]",
getId(), params, body, ex);
throw buildPayException(ex);
}
}
protected abstract PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body)
throws Throwable;
@Override
public final PayRefundRespDTO getRefund(String outTradeNo, String outRefundNo) {
try {
return doGetRefund(outTradeNo, outRefundNo);
} catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
throw ex;
} catch (Throwable ex) {
log.error("[getRefund][客户端({}) outTradeNo({}) outRefundNo({}) 查询退款单异常]",
getId(), outTradeNo, outRefundNo, ex);
throw buildPayException(ex);
}
}
protected abstract PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo)
throws Throwable;
@Override
public final PayTransferRespDTO unifiedTransfer(PayTransferUnifiedReqDTO reqDTO) {
validatePayTransferReqDTO(reqDTO);
PayTransferRespDTO resp;
try {
resp = doUnifiedTransfer(reqDTO);
} catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
throw ex;
} catch (Throwable ex) {
// 系统异常,则包装成 PayException 异常抛出
log.error("[unifiedTransfer][客户端({}) request({}) 发起转账异常]",
getId(), toJsonString(reqDTO), ex);
throw buildPayException(ex);
}
return resp;
}
private void validatePayTransferReqDTO(PayTransferUnifiedReqDTO reqDTO) {
PayTransferTypeEnum transferType = PayTransferTypeEnum.typeOf(reqDTO.getType());
switch (transferType) {
case ALIPAY_BALANCE: {
ValidationUtils.validate(reqDTO, PayTransferTypeEnum.Alipay.class);
break;
}
case WX_BALANCE: {
ValidationUtils.validate(reqDTO, PayTransferTypeEnum.WxPay.class);
break;
}
default: {
throw exception(NOT_IMPLEMENTED);
}
}
}
@Override
public final PayTransferRespDTO parseTransferNotify(Map<String, String> params, String body) {
try {
return doParseTransferNotify(params, body);
} catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
throw ex;
} catch (Throwable ex) {
log.error("[doParseTransferNotify][客户端({}) params({}) body({}) 解析失败]",
getId(), params, body, ex);
throw buildPayException(ex);
}
}
protected abstract PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body)
throws Throwable;
@Override
public final PayTransferRespDTO getTransfer(String outTradeNo, PayTransferTypeEnum type) {
try {
return doGetTransfer(outTradeNo, type);
} catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
throw ex;
} catch (Throwable ex) {
log.error("[getTransfer][客户端({}) outTradeNo({}) type({}) 查询转账单异常]",
getId(), outTradeNo, type, ex);
throw buildPayException(ex);
}
}
protected abstract PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO)
throws Throwable;
protected abstract PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type)
throws Throwable;
// ========== 各种工具方法 ==========
private PayException buildPayException(Throwable ex) {
if (ex instanceof PayException) {
return (PayException) ex;
}
throw new PayException(ex);
}
}
package org.dromara.common.pay.core.client.impl;
import jakarta.validation.Validator;
import lombok.Data;
import org.dromara.common.pay.core.client.PayClientConfig;
/**
* 无需任何配置 PayClientConfig 实现类
*
* @author jason
*/
@Data
public class NonePayClientConfig implements PayClientConfig {
/**
* 配置名称
* <p>
* 如果不加任何属性,JsonUtils.parseObject2 解析会报错,所以暂时加个名称
*/
private String name;
public NonePayClientConfig(){
this.name = "none-config";
}
@Override
public void validate(Validator validator) {
// 无任何配置不需要校验
}
}
package org.dromara.common.pay.core.client.impl;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ReflectUtil;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.pay.core.client.PayClient;
import org.dromara.common.pay.core.client.PayClientConfig;
import org.dromara.common.pay.core.client.PayClientFactory;
import org.dromara.common.pay.core.client.impl.alipay.*;
import org.dromara.common.pay.core.client.impl.mock.MockPayClient;
import org.dromara.common.pay.core.client.impl.weixin.*;
import org.dromara.common.pay.core.enums.channel.PayChannelEnum;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import static org.dromara.common.pay.core.enums.channel.PayChannelEnum.*;
/**
* 支付客户端的工厂实现类
*
* @author 芋道源码
*/
@Slf4j
public class PayClientFactoryImpl implements PayClientFactory {
/**
* 支付客户端 Map
*
* key:渠道编号
*/
private final ConcurrentMap<Long, AbstractPayClient<?>> clients = new ConcurrentHashMap<>();
/**
* 支付客户端 Class Map
*/
private final Map<PayChannelEnum, Class<?>> clientClass = new ConcurrentHashMap<>();
public PayClientFactoryImpl() {
// 微信支付客户端
clientClass.put(WX_PUB, WxPubPayClient.class);
clientClass.put(WX_LITE, WxLitePayClient.class);
clientClass.put(WX_APP, WxAppPayClient.class);
clientClass.put(WX_BAR, WxBarPayClient.class);
clientClass.put(WX_NATIVE, WxNativePayClient.class);
clientClass.put(WX_WAP, WxWapPayClient.class);
// 支付包支付客户端
clientClass.put(ALIPAY_WAP, AlipayWapPayClient.class);
clientClass.put(ALIPAY_QR, AlipayQrPayClient.class);
clientClass.put(ALIPAY_APP, AlipayAppPayClient.class);
clientClass.put(ALIPAY_PC, AlipayPcPayClient.class);
clientClass.put(ALIPAY_BAR, AlipayBarPayClient.class);
// Mock 支付客户端
clientClass.put(MOCK, MockPayClient.class);
}
@Override
public void registerPayClientClass(PayChannelEnum channel, Class<?> payClientClass) {
clientClass.put(channel, payClientClass);
}
@Override
public PayClient getPayClient(Long channelId) {
AbstractPayClient<?> client = clients.get(channelId);
if (client == null) {
log.error("[getPayClient][渠道编号({}) 找不到客户端]", channelId);
}
return client;
}
@Override
@SuppressWarnings("unchecked")
public <Config extends PayClientConfig> PayClient createOrUpdatePayClient(Long channelId, String channelCode,
Config config) {
AbstractPayClient<Config> client = (AbstractPayClient<Config>) clients.get(channelId);
if (client == null) {
client = this.createPayClient(channelId, channelCode, config);
client.init();
clients.put(client.getId(), client);
} else {
client.refresh(config);
}
return client;
}
@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) 为空", channelCode));
Class<?> payClientClass = clientClass.get(channelEnum);
Assert.notNull(payClientClass, String.format("支付渠道(%s) Class 为空", channelCode));
return (AbstractPayClient<Config>) ReflectUtil.newInstance(payClientClass, channelId, config);
}
}
package org.dromara.common.pay.core.client.impl.alipay;
import com.alipay.api.AlipayApiException;
import com.alipay.api.domain.AlipayTradeAppPayModel;
import com.alipay.api.request.AlipayTradeAppPayRequest;
import com.alipay.api.response.AlipayTradeAppPayResponse;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.pay.core.client.dto.order.PayOrderRespDTO;
import org.dromara.common.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import org.dromara.common.pay.core.enums.channel.PayChannelEnum;
import org.dromara.common.pay.core.enums.order.PayOrderDisplayModeEnum;
/**
* 支付宝【App 支付】的 PayClient 实现类
*
* 文档:<a href="https://opendocs.alipay.com/open/02e7gq">App 支付</a>
*
* // TODO 芋艿:未详细测试,因为手头没 App
*
* @author 芋道源码
*/
@Slf4j
public class AlipayAppPayClient extends AbstractAlipayPayClient {
public AlipayAppPayClient(Long channelId, AlipayPayClientConfig config) {
super(channelId, PayChannelEnum.ALIPAY_APP.getCode(), config);
}
@Override
public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
// 1.1 构建 AlipayTradeAppPayModel 请求
AlipayTradeAppPayModel model = new AlipayTradeAppPayModel();
// ① 通用的参数
model.setOutTradeNo(reqDTO.getOutTradeNo());
model.setSubject(reqDTO.getSubject());
model.setBody(reqDTO.getBody() + "test");
model.setTotalAmount(formatAmount(reqDTO.getPrice()));
model.setTimeExpire(formatTime(reqDTO.getExpireTime()));
model.setProductCode("QUICK_MSECURITY_PAY"); // 销售产品码:无线快捷支付产品
// ② 个性化的参数【无】
// ③ 支付宝扫码支付只有一种展示
String displayMode = PayOrderDisplayModeEnum.APP.getMode();
// 1.2 构建 AlipayTradePrecreateRequest 请求
AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest();
request.setBizModel(model);
request.setNotifyUrl(reqDTO.getNotifyUrl());
request.setReturnUrl(reqDTO.getReturnUrl());
// 2.1 执行请求
AlipayTradeAppPayResponse response = client.sdkExecute(request);
// 2.2 处理结果
if (!response.isSuccess()) {
return buildClosedPayOrderRespDTO(reqDTO, response);
}
return PayOrderRespDTO.waitingOf(displayMode, response.getBody(),
reqDTO.getOutTradeNo(), response);
}
}
package org.dromara.common.pay.core.client.impl.alipay;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import com.alipay.api.AlipayApiException;
import com.alipay.api.domain.AlipayTradePayModel;
import com.alipay.api.request.AlipayTradePayRequest;
import com.alipay.api.response.AlipayTradePayResponse;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.pay.core.client.dto.order.PayOrderRespDTO;
import org.dromara.common.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import org.dromara.common.pay.core.enums.channel.PayChannelEnum;
import org.dromara.common.pay.core.enums.order.PayOrderDisplayModeEnum;
import java.time.LocalDateTime;
import java.util.Objects;
import static org.dromara.common.mall.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST;
import static org.dromara.common.mall.exception.util.ServiceExceptionUtil.exception0;
import static org.dromara.common.pay.core.client.impl.alipay.AlipayPayClientConfig.MODE_CERTIFICATE;
/**
* 支付宝【条码支付】的 PayClient 实现类
*
* 文档:<a href="https://opendocs.alipay.com/open/194/105072">当面付</a>
*
* @author 芋道源码
*/
@Slf4j
public class AlipayBarPayClient extends AbstractAlipayPayClient {
public AlipayBarPayClient(Long channelId, AlipayPayClientConfig config) {
super(channelId, PayChannelEnum.ALIPAY_BAR.getCode(), config);
}
@Override
public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
String authCode = MapUtil.getStr(reqDTO.getChannelExtras(), "auth_code");
if (StrUtil.isEmpty(authCode)) {
throw exception0(BAD_REQUEST.getCode(), "条形码不能为空");
}
// 1.1 构建 AlipayTradePayModel 请求
AlipayTradePayModel model = new AlipayTradePayModel();
// ① 通用的参数
model.setOutTradeNo(reqDTO.getOutTradeNo());
model.setSubject(reqDTO.getSubject());
model.setBody(reqDTO.getBody());
model.setTotalAmount(formatAmount(reqDTO.getPrice()));
model.setScene("bar_code"); // 当面付条码支付场景
// ② 个性化的参数
model.setAuthCode(authCode);
// ③ 支付宝条码支付只有一种展示
String displayMode = PayOrderDisplayModeEnum.BAR_CODE.getMode();
// 1.2 构建 AlipayTradePayRequest 请求
AlipayTradePayRequest request = new AlipayTradePayRequest();
request.setBizModel(model);
request.setNotifyUrl(reqDTO.getNotifyUrl());
request.setReturnUrl(reqDTO.getReturnUrl());
// 2.1 执行请求
AlipayTradePayResponse response;
if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) {
// 证书模式
response = client.certificateExecute(request);
} else {
response = client.execute(request);
}
// 2.2 处理结果
if (!response.isSuccess()) {
return buildClosedPayOrderRespDTO(reqDTO, response);
}
if ("10000".equals(response.getCode())) { // 免密支付
LocalDateTime successTime = LocalDateTimeUtil.of(response.getGmtPayment());
return PayOrderRespDTO.successOf(response.getTradeNo(), response.getBuyerUserId(), successTime,
response.getOutTradeNo(), response)
.setDisplayMode(displayMode).setDisplayContent("");
}
// 大额支付,需要用户输入密码,所以返回 waiting。此时,前端一般会进行轮询
return PayOrderRespDTO.waitingOf(displayMode, "",
reqDTO.getOutTradeNo(), response);
}
}
package org.dromara.common.pay.core.client.impl.alipay;
import jakarta.validation.Validator;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.dromara.common.mall.util.validation.ValidationUtils;
import org.dromara.common.pay.core.client.PayClientConfig;
/**
* 支付宝的 PayClientConfig 实现类
* 属性主要来自 {@link com.alipay.api.AlipayConfig} 的必要属性
*
* @author 芋道源码
*/
@Data
public class AlipayPayClientConfig implements PayClientConfig {
/**
* 公钥类型 - 公钥模式
*/
public static final Integer MODE_PUBLIC_KEY = 1;
/**
* 公钥类型 - 证书模式
*/
public static final Integer MODE_CERTIFICATE = 2;
/**
* 接口内容加密方式 - AES 加密
*/
public static final String ENC_TYPE_AES = "AES";
/**
* 签名算法类型 - RSA
*/
public static final String SIGN_TYPE_DEFAULT = "RSA2";
/**
* 网关地址
*
* 1. <a href="https://openapi.alipay.com/gateway.do">生产环境</a>
* 2. <a href="https://openapi-sandbox.dl.alipaydev.com/gateway.do">沙箱环境</a>
*/
@NotBlank(message = "网关地址不能为空", groups = {ModePublicKey.class, ModeCertificate.class})
private String serverUrl;
/**
* 开放平台上创建的应用的 ID
*/
@NotBlank(message = "开放平台上创建的应用的 ID不能为空", groups = {ModePublicKey.class, ModeCertificate.class})
private String appId;
/**
* 签名算法类型,推荐:RSA2
* <p>
* {@link #SIGN_TYPE_DEFAULT}
*/
@NotBlank(message = "签名算法类型不能为空", groups = {ModePublicKey.class, ModeCertificate.class})
private String signType;
/**
* 公钥类型
* 1. {@link #MODE_PUBLIC_KEY} 情况,privateKey + alipayPublicKey
* 2. {@link #MODE_CERTIFICATE} 情况,appCertContent + alipayPublicCertContent + rootCertContent
*/
@NotNull(message = "公钥类型不能为空", groups = {ModePublicKey.class, ModeCertificate.class})
private Integer mode;
// ========== 公钥模式 ==========
/**
* 商户私钥
*/
@NotBlank(message = "商户私钥不能为空", groups = {ModePublicKey.class})
private String privateKey;
/**
* 支付宝公钥字符串
*/
@NotBlank(message = "支付宝公钥字符串不能为空", groups = {ModePublicKey.class})
private String alipayPublicKey;
// ========== 证书模式 ==========
/**
* 指定商户公钥应用证书内容字符串
*/
@NotBlank(message = "指定商户公钥应用证书内容不能为空", groups = {ModeCertificate.class})
private String appCertContent;
/**
* 指定支付宝公钥证书内容字符串
*/
@NotBlank(message = "指定支付宝公钥证书内容不能为空", groups = {ModeCertificate.class})
private String alipayPublicCertContent;
/**
* 指定根证书内容字符串
*/
@NotBlank(message = "指定根证书内容字符串不能为空", groups = {ModeCertificate.class})
private String rootCertContent;
/**
* 接口内容加密方式
*
* 1. 如果为空,将使用无加密方式
* 2. 如果要加密,目前支付宝只有 AES 一种加密方式
*
* @see <a href="https://opendocs.alipay.com/common/02mse3">支付宝开放平台</a>
* @see AlipayPayClientConfig#ENC_TYPE_AES
*/
private String encryptType;
/**
* 接口内容加密的私钥
*/
private String encryptKey;
public interface ModePublicKey {
}
public interface ModeCertificate {
}
@Override
public void validate(Validator validator) {
ValidationUtils.validate(validator, this,
MODE_PUBLIC_KEY.equals(this.getMode()) ? ModePublicKey.class : ModeCertificate.class);
}
}
package org.dromara.common.pay.core.client.impl.alipay;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.http.Method;
import com.alipay.api.AlipayApiException;
import com.alipay.api.domain.AlipayTradePagePayModel;
import com.alipay.api.request.AlipayTradePagePayRequest;
import com.alipay.api.response.AlipayTradePagePayResponse;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.pay.core.client.dto.order.PayOrderRespDTO;
import org.dromara.common.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import org.dromara.common.pay.core.enums.channel.PayChannelEnum;
import org.dromara.common.pay.core.enums.order.PayOrderDisplayModeEnum;
import java.util.Objects;
/**
* 支付宝【PC 网站】的 PayClient 实现类
*
* 文档:<a href="https://opendocs.alipay.com/open/270/105898">电脑网站支付</a>
*
* @author XGD
*/
@Slf4j
public class AlipayPcPayClient extends AbstractAlipayPayClient {
public AlipayPcPayClient(Long channelId, AlipayPayClientConfig config) {
super(channelId, PayChannelEnum.ALIPAY_PC.getCode(), config);
}
@Override
public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
// 1.1 构建 AlipayTradePagePayModel 请求
AlipayTradePagePayModel model = new AlipayTradePagePayModel();
// ① 通用的参数
model.setOutTradeNo(reqDTO.getOutTradeNo());
model.setSubject(reqDTO.getSubject());
model.setBody(reqDTO.getBody());
model.setTotalAmount(formatAmount(reqDTO.getPrice()));
model.setTimeExpire(formatTime(reqDTO.getExpireTime()));
model.setProductCode("FAST_INSTANT_TRADE_PAY"); // 销售产品码. 目前 PC 支付场景下仅支持 FAST_INSTANT_TRADE_PAY
// ② 个性化的参数
// 如果想弄更多个性化的参数,可参考 https://www.pingxx.com/api/支付渠道 extra 参数说明.html 的 alipay_pc_direct 部分进行拓展
model.setQrPayMode("2"); // 跳转模式 - 订单码,效果参见:https://help.pingxx.com/article/1137360/
// ③ 支付宝 PC 支付有两种展示模式:FORM、URL
String displayMode = ObjectUtil.defaultIfNull(reqDTO.getDisplayMode(),
PayOrderDisplayModeEnum.URL.getMode());
// 1.2 构建 AlipayTradePagePayRequest 请求
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
request.setBizModel(model);
request.setNotifyUrl(reqDTO.getNotifyUrl());
request.setReturnUrl(reqDTO.getReturnUrl());
// 2.1 执行请求
AlipayTradePagePayResponse response;
if (Objects.equals(displayMode, PayOrderDisplayModeEnum.FORM.getMode())) {
response = client.pageExecute(request, Method.POST.name()); // 需要特殊使用 POST 请求
} else {
response = client.pageExecute(request, Method.GET.name());
}
// 2.2 处理结果
if (!response.isSuccess()) {
return buildClosedPayOrderRespDTO(reqDTO, response);
}
return PayOrderRespDTO.waitingOf(displayMode, response.getBody(),
reqDTO.getOutTradeNo(), response);
}
}
package org.dromara.common.pay.core.client.impl.alipay;
import com.alipay.api.AlipayApiException;
import com.alipay.api.domain.AlipayTradePrecreateModel;
import com.alipay.api.request.AlipayTradePrecreateRequest;
import com.alipay.api.response.AlipayTradePrecreateResponse;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.pay.core.client.dto.order.PayOrderRespDTO;
import org.dromara.common.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import org.dromara.common.pay.core.enums.channel.PayChannelEnum;
import org.dromara.common.pay.core.enums.order.PayOrderDisplayModeEnum;
import java.util.Objects;
import static org.dromara.common.pay.core.client.impl.alipay.AlipayPayClientConfig.MODE_CERTIFICATE;
/**
* 支付宝【扫码支付】的 PayClient 实现类
*
* 文档:<a href="https://opendocs.alipay.com/apis/02890k">扫码支付</a>
*
* @author 芋道源码
*/
@Slf4j
public class AlipayQrPayClient extends AbstractAlipayPayClient {
public AlipayQrPayClient(Long channelId, AlipayPayClientConfig config) {
super(channelId, PayChannelEnum.ALIPAY_QR.getCode(), config);
}
@Override
public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
// 1.1 构建 AlipayTradePrecreateModel 请求
AlipayTradePrecreateModel model = new AlipayTradePrecreateModel();
// ① 通用的参数
model.setOutTradeNo(reqDTO.getOutTradeNo());
model.setSubject(reqDTO.getSubject());
model.setBody(reqDTO.getBody());
model.setTotalAmount(formatAmount(reqDTO.getPrice()));
model.setProductCode("FACE_TO_FACE_PAYMENT"); // 销售产品码. 目前扫码支付场景下仅支持 FACE_TO_FACE_PAYMENT
// ② 个性化的参数【无】
// ③ 支付宝扫码支付只有一种展示,考虑到前端可能希望二维码扫描后,手机打开
String displayMode = PayOrderDisplayModeEnum.QR_CODE.getMode();
// 1.2 构建 AlipayTradePrecreateRequest 请求
AlipayTradePrecreateRequest request = new AlipayTradePrecreateRequest();
request.setBizModel(model);
request.setNotifyUrl(reqDTO.getNotifyUrl());
request.setReturnUrl(reqDTO.getReturnUrl());
// 2.1 执行请求
AlipayTradePrecreateResponse response;
if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) {
// 证书模式
response = client.certificateExecute(request);
} else {
response = client.execute(request);
}
// 2.2 处理结果
if (!response.isSuccess()) {
return buildClosedPayOrderRespDTO(reqDTO, response);
}
return PayOrderRespDTO.waitingOf(displayMode, response.getQrCode(),
reqDTO.getOutTradeNo(), response);
}
}
package org.dromara.common.pay.core.client.impl.alipay;
import cn.hutool.http.Method;
import com.alipay.api.AlipayApiException;
import com.alipay.api.domain.AlipayTradeWapPayModel;
import com.alipay.api.request.AlipayTradeWapPayRequest;
import com.alipay.api.response.AlipayTradeWapPayResponse;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.pay.core.client.dto.order.PayOrderRespDTO;
import org.dromara.common.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import org.dromara.common.pay.core.enums.channel.PayChannelEnum;
import org.dromara.common.pay.core.enums.order.PayOrderDisplayModeEnum;
/**
* 支付宝【Wap 网站】的 PayClient 实现类
* <p>
* 文档:<a href="https://opendocs.alipay.com/apis/api_1/alipay.trade.wap.pay">手机网站支付接口</a>
*
* @author 芋道源码
*/
@Slf4j
public class AlipayWapPayClient extends AbstractAlipayPayClient {
public AlipayWapPayClient(Long channelId, AlipayPayClientConfig config) {
super(channelId, PayChannelEnum.ALIPAY_WAP.getCode(), config);
}
@Override
public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
// 1.1 构建 AlipayTradeWapPayModel 请求
AlipayTradeWapPayModel model = new AlipayTradeWapPayModel();
// ① 通用的参数
model.setOutTradeNo(reqDTO.getOutTradeNo());
model.setSubject(reqDTO.getSubject());
model.setBody(reqDTO.getBody());
model.setTotalAmount(formatAmount(reqDTO.getPrice()));
model.setProductCode("QUICK_WAP_PAY"); // 销售产品码. 目前 Wap 支付场景下仅支持 QUICK_WAP_PAY
// ② 个性化的参数【无】
// ③ 支付宝 Wap 支付只有一种展示:URL
String displayMode = PayOrderDisplayModeEnum.URL.getMode();
// 1.2 构建 AlipayTradeWapPayRequest 请求
AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest();
request.setBizModel(model);
request.setNotifyUrl(reqDTO.getNotifyUrl());
request.setReturnUrl(reqDTO.getReturnUrl());
model.setQuitUrl(reqDTO.getReturnUrl());
model.setTimeExpire(formatTime(reqDTO.getExpireTime()));
// 2.1 执行请求
AlipayTradeWapPayResponse response = client.pageExecute(request, Method.GET.name());
// 2.2 处理结果
if (!response.isSuccess()) {
return buildClosedPayOrderRespDTO(reqDTO, response);
}
return PayOrderRespDTO.waitingOf(displayMode, response.getBody(),
reqDTO.getOutTradeNo(), response);
}
}
package org.dromara.common.pay.core.client.impl.mock;
import org.dromara.common.pay.core.client.dto.order.PayOrderRespDTO;
import org.dromara.common.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import org.dromara.common.pay.core.client.dto.refund.PayRefundRespDTO;
import org.dromara.common.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
import org.dromara.common.pay.core.client.dto.transfer.PayTransferRespDTO;
import org.dromara.common.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
import org.dromara.common.pay.core.client.impl.AbstractPayClient;
import org.dromara.common.pay.core.client.impl.NonePayClientConfig;
import org.dromara.common.pay.core.enums.channel.PayChannelEnum;
import org.dromara.common.pay.core.enums.transfer.PayTransferTypeEnum;
import java.time.LocalDateTime;
import java.util.Map;
/**
* 模拟支付的 PayClient 实现类
*
* 模拟支付返回结果都是成功,方便大家日常流畅
*
* @author jason
*/
public class MockPayClient extends AbstractPayClient<NonePayClientConfig> {
private static final String MOCK_RESP_SUCCESS_DATA = "MOCK_SUCCESS";
public MockPayClient(Long channelId, NonePayClientConfig config) {
super(channelId, PayChannelEnum.MOCK.getCode(), config);
}
@Override
protected void doInit() {
}
@Override
protected PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
return PayOrderRespDTO.successOf("MOCK-P-" + reqDTO.getOutTradeNo(), "", LocalDateTime.now(),
reqDTO.getOutTradeNo(), MOCK_RESP_SUCCESS_DATA);
}
@Override
protected PayOrderRespDTO doGetOrder(String outTradeNo) {
return PayOrderRespDTO.successOf("MOCK-P-" + outTradeNo, "", LocalDateTime.now(),
outTradeNo, MOCK_RESP_SUCCESS_DATA);
}
@Override
protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
return PayRefundRespDTO.successOf("MOCK-R-" + reqDTO.getOutRefundNo(), LocalDateTime.now(),
reqDTO.getOutRefundNo(), MOCK_RESP_SUCCESS_DATA);
}
@Override
protected PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo) {
return PayRefundRespDTO.successOf("MOCK-R-" + outRefundNo, LocalDateTime.now(),
outRefundNo, MOCK_RESP_SUCCESS_DATA);
}
@Override
protected PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body) throws Throwable {
throw new UnsupportedOperationException("未实现");
}
@Override
protected PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body) {
throw new UnsupportedOperationException("模拟支付无退款回调");
}
@Override
protected PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body) {
throw new UnsupportedOperationException("模拟支付无支付回调");
}
@Override
protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) {
throw new UnsupportedOperationException("待实现");
}
@Override
protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) {
throw new UnsupportedOperationException("待实现");
}
}
package org.dromara.common.pay.core.client.impl.weixin;
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.constant.WxPayConstants;
import com.github.binarywang.wxpay.exception.WxPayException;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.pay.core.client.dto.order.PayOrderRespDTO;
import org.dromara.common.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import org.dromara.common.pay.core.enums.channel.PayChannelEnum;
import org.dromara.common.pay.core.enums.order.PayOrderDisplayModeEnum;
import static org.dromara.common.core.utils.json.JsonUtils.toJsonString;
/**
* 微信支付【App 支付】的 PayClient 实现类
*
* 文档:<a href="https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_5_3.shtml">App 支付</a>
*
* // TODO 芋艿:未详细测试,因为手头没 App
*
* @author 芋道源码
*/
@Slf4j
public class WxAppPayClient extends AbstractWxPayClient {
public WxAppPayClient(Long channelId, WxPayClientConfig config) {
super(channelId, PayChannelEnum.WX_APP.getCode(), config);
}
@Override
protected void doInit() {
super.doInit(WxPayConstants.TradeType.APP);
}
@Override
protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
// 构建 WxPayUnifiedOrderRequest 对象
WxPayUnifiedOrderRequest request = buildPayUnifiedOrderRequestV2(reqDTO);
// 执行请求
WxPayMpOrderResult response = client.createOrder(request);
// 转换结果
return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response),
reqDTO.getOutTradeNo(), response);
}
@Override
protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
// 构建 WxPayUnifiedOrderV3Request 对象
WxPayUnifiedOrderV3Request request = buildPayUnifiedOrderRequestV3(reqDTO);
// 执行请求
WxPayUnifiedOrderV3Result.AppResult response = client.createOrderV3(TradeTypeEnum.APP, request);
// 转换结果
return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response),
reqDTO.getOutTradeNo(), response);
}
}
package org.dromara.common.pay.core.client.impl.weixin;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.StrUtil;
import com.github.binarywang.wxpay.bean.request.WxPayMicropayRequest;
import com.github.binarywang.wxpay.bean.result.WxPayMicropayResult;
import com.github.binarywang.wxpay.constant.WxPayConstants;
import com.github.binarywang.wxpay.exception.WxPayException;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.mall.util.date.LocalDateTimeUtils;
import org.dromara.common.pay.core.client.dto.order.PayOrderRespDTO;
import org.dromara.common.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import org.dromara.common.pay.core.enums.channel.PayChannelEnum;
import org.dromara.common.pay.core.enums.order.PayOrderDisplayModeEnum;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;
import static org.dromara.common.core.utils.json.JsonUtils.toJsonString;
import static org.dromara.common.mall.exception.util.ServiceExceptionUtil.invalidParamException;
/**
* 微信支付【付款码支付】的 PayClient 实现类
*
* 文档:<a href="https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=9_10&index=1">付款码支付</a>
*
* @author 芋道源码
*/
@Slf4j
public class WxBarPayClient extends AbstractWxPayClient {
/**
* 微信付款码的过期时间
*/
private static final Duration AUTH_CODE_EXPIRE = Duration.ofMinutes(3);
public WxBarPayClient(Long channelId, WxPayClientConfig config) {
super(channelId, PayChannelEnum.WX_BAR.getCode(), config);
}
@Override
protected void doInit() {
super.doInit(WxPayConstants.TradeType.MICROPAY);
}
@Override
protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
// 由于付款码需要不断轮询,所以需要在较短的时间完成支付
LocalDateTime expireTime = LocalDateTimeUtils.addTime(AUTH_CODE_EXPIRE);
if (expireTime.isAfter(reqDTO.getExpireTime())) {
expireTime = reqDTO.getExpireTime();
}
// 构建 WxPayMicropayRequest 对象
WxPayMicropayRequest request = WxPayMicropayRequest.newBuilder()
.outTradeNo(reqDTO.getOutTradeNo())
.body(reqDTO.getSubject())
.detail(reqDTO.getBody())
.totalFee(reqDTO.getPrice()) // 单位分
.timeExpire(formatDateV2(expireTime))
.spbillCreateIp(reqDTO.getUserIp())
.authCode(getAuthCode(reqDTO))
.build();
// 执行请求,重试直到失败(过期),或者成功
WxPayException lastWxPayException = null;
for (int i = 1; i < Byte.MAX_VALUE; i++) {
try {
WxPayMicropayResult response = client.micropay(request);
// 支付成功,例如说:1)用户输入了密码;2)用户免密支付
return PayOrderRespDTO.successOf(response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()),
response.getOutTradeNo(), response)
.setDisplayMode(PayOrderDisplayModeEnum.BAR_CODE.getMode());
} catch (WxPayException ex) {
lastWxPayException = ex;
// 如果不满足这 3 种任一的,则直接抛出 WxPayException 异常,不仅需处理
// 1. SYSTEMERROR:接口返回错误:请立即调用被扫订单结果查询API,查询当前订单状态,并根据订单的状态决定下一步的操作。
// 2. USERPAYING:用户支付中,需要输入密码:等待 5 秒,然后调用被扫订单结果查询 API,查询当前订单的不同状态,决定下一步的操作。
// 3. BANKERROR:银行系统异常:请立即调用被扫订单结果查询 API,查询当前订单的不同状态,决定下一步的操作。
if (!StrUtil.equalsAny(ex.getErrCode(), "SYSTEMERROR", "USERPAYING", "BANKERROR")) {
throw ex;
}
// 等待 5 秒,继续下一轮重新发起支付
log.info("[doUnifiedOrderV2][发起微信 Bar 支付第({})失败,等待下一轮重试,请求({}),响应({})]", i,
toJsonString(request), ex.getMessage());
ThreadUtil.sleep(5, TimeUnit.SECONDS);
}
}
throw lastWxPayException;
}
@Override
protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
return doUnifiedOrderV2(reqDTO);
}
// ========== 各种工具方法 ==========
static String getAuthCode(PayOrderUnifiedReqDTO reqDTO) {
String authCode = MapUtil.getStr(reqDTO.getChannelExtras(), "authCode");
if (StrUtil.isEmpty(authCode)) {
throw invalidParamException("支付请求的 authCode 不能为空!");
}
return authCode;
}
}
package org.dromara.common.pay.core.client.impl.weixin;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.pay.core.enums.channel.PayChannelEnum;
/**
* 微信支付【小程序】的 PayClient 实现类
*
* 由于公众号和小程序的微信支付逻辑一致,所以直接进行继承
*
* 文档:<a href="https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_1.shtml">JSAPI 下单</>
*
* @author zwy
*/
@Slf4j
public class WxLitePayClient extends WxPubPayClient {
public WxLitePayClient(Long channelId, WxPayClientConfig config) {
super(channelId, PayChannelEnum.WX_LITE.getCode(), config);
}
}
package org.dromara.common.pay.core.client.impl.weixin;
import com.github.binarywang.wxpay.bean.order.WxPayNativeOrderResult;
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request;
import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
import com.github.binarywang.wxpay.constant.WxPayConstants;
import com.github.binarywang.wxpay.exception.WxPayException;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.pay.core.client.dto.order.PayOrderRespDTO;
import org.dromara.common.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import org.dromara.common.pay.core.enums.channel.PayChannelEnum;
import org.dromara.common.pay.core.enums.order.PayOrderDisplayModeEnum;
/**
* 微信支付【Native 二维码】的 PayClient 实现类
*
* 文档:<a href="https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml">Native 下单</a>
*
* @author zwy
*/
@Slf4j
public class WxNativePayClient extends AbstractWxPayClient {
public WxNativePayClient(Long channelId, WxPayClientConfig config) {
super(channelId, PayChannelEnum.WX_NATIVE.getCode(), config);
}
@Override
protected void doInit() {
super.doInit(WxPayConstants.TradeType.NATIVE);
}
@Override
protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
// 构建 WxPayUnifiedOrderRequest 对象
WxPayUnifiedOrderRequest request = buildPayUnifiedOrderRequestV2(reqDTO)
.setProductId(reqDTO.getOutTradeNo()); // V2 必须传递 productId,无需在微信配置。该参数在 V3 简化,无需传递!
// 执行请求
WxPayNativeOrderResult response = client.createOrder(request);
// 转换结果
return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.QR_CODE.getMode(), response.getCodeUrl(),
reqDTO.getOutTradeNo(), response);
}
@Override
protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
// 构建 WxPayUnifiedOrderV3Request 对象
WxPayUnifiedOrderV3Request request = buildPayUnifiedOrderRequestV3(reqDTO);
// 执行请求
String response = client.createOrderV3(TradeTypeEnum.NATIVE, request);
// 转换结果
return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.QR_CODE.getMode(), response,
reqDTO.getOutTradeNo(), response);
}
}
package org.dromara.common.pay.core.client.impl.weixin;
import jakarta.validation.Validator;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import org.dromara.common.mall.util.validation.ValidationUtils;
import org.dromara.common.pay.core.client.PayClientConfig;
/**
* 微信支付的 PayClientConfig 实现类
* 属性主要来自 {@link com.github.binarywang.wxpay.config.WxPayConfig} 的必要属性
*
* @author 芋道源码
*/
@Data
public class WxPayClientConfig implements PayClientConfig {
/**
* API 版本 - V2
*
* <a href="https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_1">V2 协议说明</a>
*/
public static final String API_VERSION_V2 = "v2";
/**
* API 版本 - V3
*
* <a href="https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay-1.shtml">V3 协议说明</a>
*/
public static final String API_VERSION_V3 = "v3";
/**
* 公众号或者小程序的 appid
*
* 只有公众号或小程序需要该字段
*/
@NotBlank(message = "APPID 不能为空", groups = {V2.class, V3.class})
private String appId;
/**
* 商户号
*/
@NotBlank(message = "商户号不能为空", groups = {V2.class, V3.class})
private String mchId;
/**
* API 版本
*/
@NotBlank(message = "API 版本不能为空", groups = {V2.class, V3.class})
private String apiVersion;
// ========== V2 版本的参数 ==========
/**
* 商户密钥
*/
@NotBlank(message = "商户密钥不能为空", groups = V2.class)
private String mchKey;
/**
* apiclient_cert.p12 证书文件的对应字符串【base64 格式】
*
* 为什么采用 base64 格式?因为 p12 读取后是二进制,需要转换成 base64 格式才好传输和存储
*/
@NotBlank(message = "apiclient_cert.p12 不能为空", groups = V2.class)
private String keyContent;
// ========== V3 版本的参数 ==========
/**
* apiclient_key.pem 证书文件的对应字符串
*/
@NotBlank(message = "apiclient_key 不能为空", groups = V3.class)
private String privateKeyContent;
/**
* apiV3 密钥值
*/
@NotBlank(message = "apiV3 密钥值不能为空", groups = V3.class)
private String apiV3Key;
/**
* 证书序列号
*/
@NotBlank(message = "证书序列号不能为空", groups = V3.class)
private String certSerialNo;
@Deprecated // TODO 芋艿:V2.3.0 进行移除
private String privateCertContent;
/**
* 分组校验 v2版本
*/
public interface V2 {
}
/**
* 分组校验 v3版本
*/
public interface V3 {
}
@Override
public void validate(Validator validator) {
ValidationUtils.validate(validator, this,
API_VERSION_V2.equals(this.getApiVersion()) ? V2.class : V3.class);
}
}
package org.dromara.common.pay.core.client.impl.weixin;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
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.constant.WxPayConstants;
import com.github.binarywang.wxpay.exception.WxPayException;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.pay.core.client.dto.order.PayOrderRespDTO;
import org.dromara.common.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import org.dromara.common.pay.core.enums.channel.PayChannelEnum;
import org.dromara.common.pay.core.enums.order.PayOrderDisplayModeEnum;
import static org.dromara.common.core.utils.json.JsonUtils.toJsonString;
import static org.dromara.common.mall.exception.util.ServiceExceptionUtil.invalidParamException;
/**
* 微信支付(公众号)的 PayClient 实现类
*
* 文档:<a href="https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_1.shtml">JSAPI 下单</>
*
* @author 芋道源码
*/
@Slf4j
public class WxPubPayClient extends AbstractWxPayClient {
public WxPubPayClient(Long channelId, WxPayClientConfig config) {
super(channelId, PayChannelEnum.WX_PUB.getCode(), config);
}
protected WxPubPayClient(Long channelId, String channelCode, WxPayClientConfig config) {
super(channelId, channelCode, config);
}
@Override
protected void doInit() {
super.doInit(WxPayConstants.TradeType.JSAPI);
}
@Override
protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
// 构建 WxPayUnifiedOrderRequest 对象
WxPayUnifiedOrderRequest request = buildPayUnifiedOrderRequestV2(reqDTO)
.setOpenid(getOpenid(reqDTO));
// 执行请求
WxPayMpOrderResult response = client.createOrder(request);
// 转换结果
return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response),
reqDTO.getOutTradeNo(), response);
}
@Override
protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
// 构建 WxPayUnifiedOrderRequest 对象
WxPayUnifiedOrderV3Request request = buildPayUnifiedOrderRequestV3(reqDTO)
.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(getOpenid(reqDTO)));
// 执行请求
WxPayUnifiedOrderV3Result.JsapiResult response = client.createOrderV3(TradeTypeEnum.JSAPI, request);
// 转换结果
return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response),
reqDTO.getOutTradeNo(), response);
}
// ========== 各种工具方法 ==========
static String getOpenid(PayOrderUnifiedReqDTO reqDTO) {
String openid = MapUtil.getStr(reqDTO.getChannelExtras(), "openid");
if (StrUtil.isEmpty(openid)) {
throw invalidParamException("支付请求的 openid 不能为空!");
}
return openid;
}
}
package org.dromara.common.pay.core.client.impl.weixin;
import com.github.binarywang.wxpay.bean.order.WxPayMwebOrderResult;
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request;
import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
import com.github.binarywang.wxpay.constant.WxPayConstants;
import com.github.binarywang.wxpay.exception.WxPayException;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.pay.core.client.dto.order.PayOrderRespDTO;
import org.dromara.common.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import org.dromara.common.pay.core.enums.channel.PayChannelEnum;
import org.dromara.common.pay.core.enums.order.PayOrderDisplayModeEnum;
/**
* 微信支付(H5 网页)的 PayClient 实现类
*
* 文档:<a href="https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_3_1.shtml">H5下单API</>
*
* @author YYQ
*/
@Slf4j
public class WxWapPayClient extends AbstractWxPayClient {
public WxWapPayClient(Long channelId, WxPayClientConfig config) {
super(channelId, PayChannelEnum.WX_WAP.getCode(), config);
}
protected WxWapPayClient(Long channelId, String channelCode, WxPayClientConfig config) {
super(channelId, channelCode, config);
}
@Override
protected void doInit() {
super.doInit(WxPayConstants.TradeType.MWEB);
}
@Override
protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
// 构建 WxPayUnifiedOrderRequest 对象
WxPayUnifiedOrderRequest request = buildPayUnifiedOrderRequestV2(reqDTO);
// 执行请求
WxPayMwebOrderResult response = client.createOrder(request);
// 转换结果
return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.URL.getMode(), response.getMwebUrl(),
reqDTO.getOutTradeNo(), response);
}
@Override
protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
// 构建 WxPayUnifiedOrderRequest 对象
WxPayUnifiedOrderV3Request request = buildPayUnifiedOrderRequestV3(reqDTO);
// 执行请求
String response = client.createOrderV3(TradeTypeEnum.H5, request);
// 转换结果
return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.URL.getMode(), response,
reqDTO.getOutTradeNo(), response);
}
}
package org.dromara.common.pay.core.enums.channel;
import cn.hutool.core.util.ArrayUtil;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.dromara.common.pay.core.client.PayClientConfig;
import org.dromara.common.pay.core.client.impl.NonePayClientConfig;
import org.dromara.common.pay.core.client.impl.alipay.AlipayPayClientConfig;
import org.dromara.common.pay.core.client.impl.weixin.WxPayClientConfig;
/**
* 支付渠道的编码的枚举
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public enum PayChannelEnum {
WX_PUB("wx_pub", "微信 JSAPI 支付", WxPayClientConfig.class), // 公众号网页
WX_LITE("wx_lite", "微信小程序支付", WxPayClientConfig.class),
WX_APP("wx_app", "微信 App 支付", WxPayClientConfig.class),
WX_NATIVE("wx_native", "微信 Native 支付", WxPayClientConfig.class),
WX_WAP("wx_wap", "微信 Wap 网站支付", WxPayClientConfig.class), // H5 网页
WX_BAR("wx_bar", "微信付款码支付", WxPayClientConfig.class),
ALIPAY_PC("alipay_pc", "支付宝 PC 网站支付", AlipayPayClientConfig.class),
ALIPAY_WAP("alipay_wap", "支付宝 Wap 网站支付", AlipayPayClientConfig.class),
ALIPAY_APP("alipay_app", "支付宝App 支付", AlipayPayClientConfig.class),
ALIPAY_QR("alipay_qr", "支付宝扫码支付", AlipayPayClientConfig.class),
ALIPAY_BAR("alipay_bar", "支付宝条码支付", AlipayPayClientConfig.class),
MOCK("mock", "模拟支付", NonePayClientConfig.class),
WALLET("wallet", "钱包支付", NonePayClientConfig.class);
/**
* 编码
*
* 参考 <a href="https://www.pingxx.com/api/支付渠道属性值.html">支付渠道属性值</a>
*/
private final String code;
/**
* 名字
*/
private final String name;
/**
* 配置类
*/
private final Class<? extends PayClientConfig> configClass;
/**
* 微信支付
*/
public static final String WECHAT = "WECHAT";
/**
* 支付宝支付
*/
public static final String ALIPAY = "ALIPAY";
public static PayChannelEnum getByCode(String code) {
return ArrayUtil.firstMatch(o -> o.getCode().equals(code), values());
}
public static boolean isAlipay(String channelCode) {
return channelCode != null && channelCode.startsWith("alipay");
}
}
package org.dromara.common.pay.core.enums.order;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 支付 UI 展示模式
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public enum PayOrderDisplayModeEnum {
URL("url"), // Redirect 跳转链接的方式
IFRAME("iframe"), // IFrame 内嵌链接的方式【目前暂时用不到】
FORM("form"), // HTML 表单提交
QR_CODE("qr_code"), // 二维码的文字内容
QR_CODE_URL("qr_code_url"), // 二维码的图片链接
BAR_CODE("bar_code"), // 条形码
APP("app"), // 应用:Android、iOS、微信小程序、微信公众号等,需要做自定义处理的
;
/**
* 展示模式
*/
private final String mode;
}
package org.dromara.common.pay.core.enums.order;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Objects;
/**
* 渠道的支付状态枚举
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public enum PayOrderStatusRespEnum {
WAITING(0, "未支付"),
SUCCESS(10, "支付成功"),
REFUND(20, "已退款"),
CLOSED(30, "支付关闭"),
;
private final Integer status;
private final String name;
/**
* 判断是否支付成功
*
* @param status 状态
* @return 是否支付成功
*/
public static boolean isSuccess(Integer status) {
return Objects.equals(status, SUCCESS.getStatus());
}
/**
* 判断是否已退款
*
* @param status 状态
* @return 是否支付成功
*/
public static boolean isRefund(Integer status) {
return Objects.equals(status, REFUND.getStatus());
}
/**
* 判断是否支付关闭
*
* @param status 状态
* @return 是否支付关闭
*/
public static boolean isClosed(Integer status) {
return Objects.equals(status, CLOSED.getStatus());
}
}
package org.dromara.common.pay.core.enums.refund;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Objects;
/**
* 渠道的退款状态枚举
*
* @author jason
*/
@Getter
@AllArgsConstructor
public enum PayRefundStatusRespEnum {
WAITING(0, "等待退款"),
SUCCESS(10, "退款成功"),
FAILURE(20, "退款失败");
private final Integer status;
private final String name;
public static boolean isSuccess(Integer status) {
return Objects.equals(status, SUCCESS.getStatus());
}
public static boolean isFailure(Integer status) {
return Objects.equals(status, FAILURE.getStatus());
}
}
package org.dromara.common.pay.core.enums.transfer;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Objects;
/**
* 渠道的转账状态枚举
*
* @author jason
*/
@Getter
@AllArgsConstructor
public enum PayTransferStatusRespEnum {
WAITING(0, "等待转账"),
/**
* TODO 转账到银行卡. 会有T+0 T+1 到账的请情况。 还未实现
* TODO @jason:可以看看其它开源项目,针对这个场景,处理策略是怎么样的?例如说,每天主动轮询?这个状态的单子?
*/
IN_PROGRESS(10, "转账进行中"),
SUCCESS(20, "转账成功"),
/**
* 转账关闭 (失败,或者其它情况)
*/
CLOSED(30, "转账关闭");
private final Integer status;
private final String name;
public static boolean isSuccess(Integer status) {
return Objects.equals(status, SUCCESS.getStatus());
}
public static boolean isClosed(Integer status) {
return Objects.equals(status, CLOSED.getStatus());
}
public static boolean isInProgress(Integer status) {
return Objects.equals(status, IN_PROGRESS.getStatus());
}
}
package org.dromara.common.pay.core.enums.transfer;
import cn.hutool.core.util.ArrayUtil;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.dromara.common.mall.core.IntArrayValuable;
import java.util.Arrays;
/**
* 转账类型枚举
*
* @author jason
*/
@AllArgsConstructor
@Getter
public enum PayTransferTypeEnum implements IntArrayValuable {
ALIPAY_BALANCE(1, "支付宝余额"),
WX_BALANCE(2, "微信余额"),
BANK_CARD(3, "银行卡"),
WALLET_BALANCE(4, "钱包余额");
public interface WxPay {
}
public interface Alipay {
}
private final Integer type;
private final String name;
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(PayTransferTypeEnum::getType).toArray();
@Override
public int[] array() {
return ARRAYS;
}
public static PayTransferTypeEnum typeOf(Integer type) {
return ArrayUtil.firstMatch(item -> item.getType().equals(type), values());
}
}
......@@ -278,6 +278,27 @@
<version>${revision}</version>
</dependency>
<!-- 聚合支付 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-allPay</artifactId>
<version>${revision}</version>
</dependency>
<!-- 商城 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-mall</artifactId>
<version>${revision}</version>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-yudao-mybatis</artifactId>
<version>${revision}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
......@@ -99,6 +99,21 @@
<artifactId>ip2region</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-json</artifactId>
</dependency>
</dependencies>
</project>
package org.dromara.common.core.utils.file;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.SneakyThrows;
import java.io.File;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
......@@ -40,4 +43,49 @@ public class FileUtils extends FileUtil {
String encode = URLEncoder.encode(s, StandardCharsets.UTF_8);
return encode.replaceAll("\\+", "%20");
}
/**
* 创建临时文件
* 该文件会在 JVM 退出时,进行删除
*
* @param data 文件内容
* @return 文件
*/
@SneakyThrows
public static File createTempFile(byte[] data) {
File file = createTempFile();
// 写入内容
FileUtil.writeBytes(data, file);
return file;
}
/**
* 创建临时文件,无内容
* 该文件会在 JVM 退出时,进行删除
*
* @return 文件
*/
@SneakyThrows
public static File createTempFile() {
// 创建文件,通过 UUID 保证唯一
File file = File.createTempFile(IdUtil.simpleUUID(), null);
// 标记 JVM 退出时,自动删除
file.deleteOnExit();
return file;
}
/**
* 创建临时文件
* 该文件会在 JVM 退出时,进行删除
*
* @param data 文件内容
* @return 文件
*/
@SneakyThrows
public static File createTempFile(String data) {
File file = createTempFile();
// 写入内容
FileUtil.writeUtf8String(data, file);
return file;
}
}
package org.dromara.common.core.utils.json;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
/**
* JSON 工具类
*
* @author 芋道源码
*/
@Slf4j
public class JsonUtils {
private static ObjectMapper objectMapper = new ObjectMapper();
static {
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); // 忽略 null 值
objectMapper.registerModules(new JavaTimeModule()); // 解决 LocalDateTime 的序列化
}
/**
* 初始化 objectMapper 属性
* <p>
* 通过这样的方式,使用 Spring 创建的 ObjectMapper Bean
*
* @param objectMapper ObjectMapper 对象
*/
public static void init(ObjectMapper objectMapper) {
JsonUtils.objectMapper = objectMapper;
}
@SneakyThrows
public static String toJsonString(Object object) {
return objectMapper.writeValueAsString(object);
}
@SneakyThrows
public static byte[] toJsonByte(Object object) {
return objectMapper.writeValueAsBytes(object);
}
@SneakyThrows
public static String toJsonPrettyString(Object object) {
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(object);
}
public static <T> T parseObject(String text, Class<T> clazz) {
if (StrUtil.isEmpty(text)) {
return null;
}
try {
return objectMapper.readValue(text, clazz);
} catch (IOException e) {
log.error("json parse err,json:{}", text, e);
throw new RuntimeException(e);
}
}
public static <T> T parseObject(String text, String path, Class<T> clazz) {
if (StrUtil.isEmpty(text)) {
return null;
}
try {
JsonNode treeNode = objectMapper.readTree(text);
JsonNode pathNode = treeNode.path(path);
return objectMapper.readValue(pathNode.toString(), clazz);
} catch (IOException e) {
log.error("json parse err,json:{}", text, e);
throw new RuntimeException(e);
}
}
public static <T> T parseObject(String text, Type type) {
if (StrUtil.isEmpty(text)) {
return null;
}
try {
return objectMapper.readValue(text, objectMapper.getTypeFactory().constructType(type));
} catch (IOException e) {
log.error("json parse err,json:{}", text, e);
throw new RuntimeException(e);
}
}
/**
* 将字符串解析成指定类型的对象
* 使用 {@link #parseObject(String, Class)} 时,在@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) 的场景下,
* 如果 text 没有 class 属性,则会报错。此时,使用这个方法,可以解决。
*
* @param text 字符串
* @param clazz 类型
* @return 对象
*/
public static <T> T parseObject2(String text, Class<T> clazz) {
if (StrUtil.isEmpty(text)) {
return null;
}
return JSONUtil.toBean(text, clazz);
}
public static <T> T parseObject(byte[] bytes, Class<T> clazz) {
if (ArrayUtil.isEmpty(bytes)) {
return null;
}
try {
return objectMapper.readValue(bytes, clazz);
} catch (IOException e) {
log.error("json parse err,json:{}", bytes, e);
throw new RuntimeException(e);
}
}
public static <T> T parseObject(String text, TypeReference<T> typeReference) {
try {
return objectMapper.readValue(text, typeReference);
} catch (IOException e) {
log.error("json parse err,json:{}", text, e);
throw new RuntimeException(e);
}
}
/**
* 解析 JSON 字符串成指定类型的对象,如果解析失败,则返回 null
*
* @param text 字符串
* @param typeReference 类型引用
* @return 指定类型的对象
*/
public static <T> T parseObjectQuietly(String text, TypeReference<T> typeReference) {
try {
return objectMapper.readValue(text, typeReference);
} catch (IOException e) {
return null;
}
}
public static <T> List<T> parseArray(String text, Class<T> clazz) {
if (StrUtil.isEmpty(text)) {
return new ArrayList<>();
}
try {
return objectMapper.readValue(text, objectMapper.getTypeFactory().constructCollectionType(List.class, clazz));
} catch (IOException e) {
log.error("json parse err,json:{}", text, e);
throw new RuntimeException(e);
}
}
public static <T> List<T> parseArray(String text, String path, Class<T> clazz) {
if (StrUtil.isEmpty(text)) {
return null;
}
try {
JsonNode treeNode = objectMapper.readTree(text);
JsonNode pathNode = treeNode.path(path);
return objectMapper.readValue(pathNode.toString(), objectMapper.getTypeFactory().constructCollectionType(List.class, clazz));
} catch (IOException e) {
log.error("json parse err,json:{}", text, e);
throw new RuntimeException(e);
}
}
public static JsonNode parseTree(String text) {
try {
return objectMapper.readTree(text);
} catch (IOException e) {
log.error("json parse err,json:{}", text, e);
throw new RuntimeException(e);
}
}
public static JsonNode parseTree(byte[] text) {
try {
return objectMapper.readTree(text);
} catch (IOException e) {
log.error("json parse err,json:{}", text, e);
throw new RuntimeException(e);
}
}
public static boolean isJson(String text) {
return JSONUtil.isTypeJSON(text);
}
}
package org.dromara.common.core.utils.json.databind;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JacksonStdImpl;
import java.io.IOException;
/**
* Long 序列化规则
*
* 会将超长 long 值转换为 string,解决前端 JavaScript 最大安全整数是 2^53-1 的问题
*
* @author 星语
*/
@JacksonStdImpl
public class NumberSerializer extends com.fasterxml.jackson.databind.ser.std.NumberSerializer {
private static final long MAX_SAFE_INTEGER = 9007199254740991L;
private static final long MIN_SAFE_INTEGER = -9007199254740991L;
public static final NumberSerializer INSTANCE = new NumberSerializer(Number.class);
public NumberSerializer(Class<? extends Number> rawType) {
super(rawType);
}
@Override
public void serialize(Number value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
// 超出范围 序列化位字符串
if (value.longValue() > MIN_SAFE_INTEGER && value.longValue() < MAX_SAFE_INTEGER) {
super.serialize(value, gen, serializers);
} else {
gen.writeString(value.toString());
}
}
}
package org.dromara.common.core.utils.json.databind;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import java.io.IOException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
/**
* 基于时间戳的 LocalDateTime 反序列化器
*
* @author 老五
*/
public class TimestampLocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {
public static final TimestampLocalDateTimeDeserializer INSTANCE = new TimestampLocalDateTimeDeserializer();
@Override
public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
// 将 Long 时间戳,转换为 LocalDateTime 对象
return LocalDateTime.ofInstant(Instant.ofEpochMilli(p.getValueAsLong()), ZoneId.systemDefault());
}
}
package org.dromara.common.core.utils.json.databind;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.ZoneId;
/**
* 基于时间戳的 LocalDateTime 序列化器
*
* @author 老五
*/
public class TimestampLocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {
public static final TimestampLocalDateTimeSerializer INSTANCE = new TimestampLocalDateTimeSerializer();
@Override
public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
// 将 LocalDateTime 对象,转换为 Long 时间戳
gen.writeNumber(value.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
}
}
package org.dromara.common.core.utils.string;
import cn.hutool.core.text.StrPool;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 字符串工具类
*
* @author 芋道源码
*/
public class StrUtils {
public static String maxLength(CharSequence str, int maxLength) {
return StrUtil.maxLength(str, maxLength - 3); // -3 的原因,是该方法会补充 ... 恰好
}
/**
* 给定字符串是否以任何一个字符串开始
* 给定字符串和数组为空都返回 false
*
* @param str 给定字符串
* @param prefixes 需要检测的开始字符串
* @since 3.0.6
*/
public static boolean startWithAny(String str, Collection<String> prefixes) {
if (StrUtil.isEmpty(str) || ArrayUtil.isEmpty(prefixes)) {
return false;
}
for (CharSequence suffix : prefixes) {
if (StrUtil.startWith(str, suffix, false)) {
return true;
}
}
return false;
}
public static List<Long> splitToLong(String value, CharSequence separator) {
long[] longs = StrUtil.splitToLong(value, separator);
return Arrays.stream(longs).boxed().collect(Collectors.toList());
}
public static Set<Long> splitToLongSet(String value) {
return splitToLongSet(value, StrPool.COMMA);
}
public static Set<Long> splitToLongSet(String value, CharSequence separator) {
long[] longs = StrUtil.splitToLong(value, separator);
return Arrays.stream(longs).boxed().collect(Collectors.toSet());
}
public static List<Integer> splitToInteger(String value, CharSequence separator) {
int[] integers = StrUtil.splitToInt(value, separator);
return Arrays.stream(integers).boxed().collect(Collectors.toList());
}
/**
* 移除字符串中,包含指定字符串的行
*
* @param content 字符串
* @param sequence 包含的字符串
* @return 移除后的字符串
*/
public static String removeLineContains(String content, String sequence) {
if (StrUtil.isEmpty(content) || StrUtil.isEmpty(sequence)) {
return content;
}
return Arrays.stream(content.split("\n"))
.filter(line -> !line.contains(sequence))
.collect(Collectors.joining("\n"));
}
}
package org.dromara.common.dict.utils;
import io.github.linpeilie.utils.CollectionUtils;
import org.dromara.common.core.constant.CacheNames;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.redis.utils.CacheUtils;
import org.dromara.system.api.domain.vo.RemoteDictDataVo;
......@@ -48,4 +50,32 @@ public class DictUtils {
CacheUtils.clear(CacheNames.SYS_DICT);
}
/**
* 获取字典标签
*
* @param dictType 字典类型
* @param value value值
* @return 数据
*/
public static String getDictDataLabel(String dictType, String value) {
List<RemoteDictDataVo> list = CacheUtils.get(CacheNames.SYS_DICT, dictType);
return CollectionUtils.isEmpty(list) ? StringUtils.EMPTY :
list.stream().filter(item -> StringUtils.equals(item.getDictValue(), value)).findFirst().map(RemoteDictDataVo::getDictLabel).orElse("");
}
/**
* 获取字典标签
*
* @param dictType 字典类型
* @param value value值
* @return 数据
*/
public static String getDictDataLabel(String dictType, Integer value) {
List<RemoteDictDataVo> list = CacheUtils.get(CacheNames.SYS_DICT, dictType);
return CollectionUtils.isEmpty(list) ? StringUtils.EMPTY :
list.stream().filter(item -> StringUtils.equals(item.getDictValue(), value + "")).findFirst().map(RemoteDictDataVo::getDictLabel).orElse("");
}
}
差异被折叠。
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论