Commit d5b9db1d by yangjiarong

修复支付宝,微信回传v1.0.2-7_28

parent a14b4d5c
No preview for this file type
!**/src/main/**/target/
!**/src/test/**/target/
!**/api/icloud-server/target/
!/api/icloud-server/target/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
-- INSERT --
HELP.md
/target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
-- INSERT --
/target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
/
### STS ###
.apt_generated
.classpath
.factorypath
HELP.md
/target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
!**/**/target/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
No preview for this file type
#Generated by Maven
#Thu Jul 28 21:02:31 CST 2022
version=1.0.2-snapshot
groupId=com.icloud.boot
artifactId=icloud-common
/Users/yang/方卡/pay/ocloud-api/ocloud-api_v2-7_22/icloud-framework/icloud-common/src/main/java/com/icloud/framework/common/package-info.java
# 支付模块
\ No newline at end of file
......@@ -21,8 +21,11 @@
<artifactId>scala-library</artifactId>
<version>2.13.7</version>
</dependency>
<!--其他组件-->
<dependency>
<groupId>com.icloud.boot</groupId>
<artifactId>icloud-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
......@@ -37,8 +40,90 @@
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- 新jwt-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.0</version>
</dependency>
<!-- jwt-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
<!-- shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.5.3</version>
</dependency>
<!-- shiro-redis -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!--支付 -->
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-pay</artifactId>
</dependency>
<!--oss-->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
</dependency>
<dependency>
<groupId>com.voodoodyne.jackson.jsog</groupId>
<artifactId>jackson-jsog</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- <dependency>-->
<!--这个包的版本有问题,会报错-->
<!-- <groupId>org.redisson</groupId>-->
<!-- <artifactId>redisson-spring-boot-starter</artifactId>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>io.springfox</groupId>-->
<!-- <artifactId>springfox-swagger2</artifactId>-->
<!-- </dependency>-->
</dependencies>
......
package com.icloud.server.payutils;
import com.icloud.server.payutils.dto.PayOrderUnifiedReqDTO;
import com.icloud.server.payutils.dto.PayRefundUnifiedReqDTO;
import com.icloud.server.payutils.dto.PayRefundUnifiedRespDTO;
import lombok.extern.slf4j.Slf4j;
/**
* 支付客户端的抽象类,提供模板方法,减少子类的冗余代码
*
* @author
*/
@Slf4j
public abstract class AbstractPayClient<Config extends PayClientConfig> implements PayClient {
/**
* 渠道编号
*/
private final Long channelId;
/**
* 渠道编码
*/
private final String channelCode;
/**
* 错误码枚举类
*/
protected AbstractPayCodeMapping codeMapping;
/**
* 支付配置
*/
protected Config config;
public AbstractPayClient(Long channelId, String channelCode, Config config, AbstractPayCodeMapping codeMapping) {
this.channelId = channelId;
this.channelCode = channelCode;
this.codeMapping = codeMapping;
this.config = config;
}
/**
* 初始化
*/
public final void init() {
doInit();
log.info("[init][配置({}) 初始化完成]", config);
}
/**
* 自定义初始化
*/
protected abstract void doInit();
public final void refresh(Config config) {
// 判断是否更新
if (config.equals(this.config)) {
return;
}
log.info("[refresh][配置({})发生变化,重新初始化]", config);
this.config = config;
// 初始化
this.init();
}
protected Double calculateAmount(Long amount) {
return amount / 100.0;
}
@Override
public Long getId() {
return channelId;
}
@Override
public final PayCommonResult<?> unifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
// ValidationUtil.validate(reqDTO);
// 执行短信发送
PayCommonResult<?> result;
try {
result = doUnifiedOrder(reqDTO);
} catch (Throwable ex) {
// 打印异常日志
log.error("[unifiedOrder][request({}) 发起支付失败]", reqDTO, ex);
// 封装返回
return PayCommonResult.error(ex);
}
return result;
}
protected abstract PayCommonResult<?> doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO)
throws Throwable;
@Override
public PayCommonResult<PayRefundUnifiedRespDTO> unifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
PayCommonResult<PayRefundUnifiedRespDTO> resp;
try {
resp = doUnifiedRefund(reqDTO);
} catch (Throwable ex) {
// 记录异常日志
log.error("[unifiedRefund][request({}) 发起退款失败]", reqDTO, ex);
resp = PayCommonResult.error(ex);
}
return resp;
}
protected abstract PayCommonResult<PayRefundUnifiedRespDTO> doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable;
}
package com.icloud.server.payutils;
import com.icloud.server.web.err.ErrorCode;
import com.icloud.server.web.err.pay.PayFrameworkErrorCodeConstants;
import lombok.extern.slf4j.Slf4j;
/**
* 将 API 的错误码,转换为通用的错误码
*
* @see PayCommonResult
*
* @author
*/
@Slf4j
public abstract class AbstractPayCodeMapping {
public final ErrorCode apply(String apiCode, String apiMsg) {
if (apiCode == null) {
log.error("[apply][API 错误码为空,请排查]");
return PayFrameworkErrorCodeConstants.EXCEPTION;
}
ErrorCode errorCode = this.apply0(apiCode, apiMsg);
if (errorCode == null) {
log.error("[apply][API 错误码({}) 错误提示({}) 无法匹配]", apiCode, apiMsg);
return PayFrameworkErrorCodeConstants.PAY_UNKNOWN;
}
return errorCode;
}
protected abstract ErrorCode apply0(String apiCode, String apiMsg);
}
package com.icloud.server.payutils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayConfig;
import com.alipay.api.internal.util.AlipaySignature;
import com.github.binarywang.wxpay.bean.notify.WxPayNotifyResponse;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.icloud.server.payutils.dto.PayNotifyDataDTO;
import com.icloud.server.payutils.dto.PayOrderDO;
import com.icloud.server.scala.pay.Alipay1;
import com.icloud.server.scala.pay.Alipay1Repository;
import com.icloud.server.scala.paynotify.PayNotifyEntity;
import com.icloud.server.scala.paynotify.PayNotifyLogEntity;
import com.icloud.server.scala.paynotify.PayNotifyLogRepository;
import com.icloud.server.scala.paynotify.PayNotifyRepository;
import com.icloud.server.scala.payorder.PayOrderRepository;
import com.icloud.server.utils.JsonUtils;
import com.icloud.server.utils.log.Logs;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.util.*;
import com.alipay.api.internal.util.AlipaySignature;
/**
* 微信
*
* @Author : yang
* @Date : 2022-07-26 01:09
* @Version : 2.1.2
*/
@Api(tags = "支付宝回传")
//@ApiIgnore
@RestController
@RequestMapping("/pay/aliorder")
@Validated
@Slf4j
@AllArgsConstructor
public class AliPayNotify {
@Resource
private PayClientFactory payClientFactory;
@Resource
private PayNotifyRepository pnr;
@Resource
private Alipay1Repository ar;
@Resource
private PayOrderRepository por;
@Resource
private PayNotifyLogRepository pnlr;
static Logger logger = LoggerFactory.getLogger("alirepay");
public String alyPayNotify(HttpServletRequest request, HttpServletResponse response) {
log.info("支付宝支付成功回调");
//这里拿到支付宝通知数据
Map<String, Object> params = convertRequestParamsToMap(request); // 将异步通知中收到的待验证所有参数都存放到map中
String paramsJson = JSON.toJSONString(params);
log.info("支付宝回调,{}"+ paramsJson);
Map<String, String> map = JSON.parseObject(paramsJson, new TypeReference<Map<String, String>>(){});
return "";
}
// 将request中的参数转换成Map
private static Map<String, Object> convertRequestParamsToMap(HttpServletRequest request) {
Map<String,Object> returnMap = new HashMap<String,Object>();
Map<String,String[]> map = new HashMap<String,String[]>();
map = request.getParameterMap();
Iterator entries = map.entrySet().iterator();
Map.Entry entry;
String name ="";
String value=null;
while (entries.hasNext()){
entry=(Map.Entry)entries.next();
name = (String) entry.getKey();
Object objvalue = entry.getValue();
if(objvalue == null){
value = null;
}else if(objvalue instanceof String[]){
/**条件如果成立,objvalue就是一个数组,需要将它转换成为字符串,并拼接上逗号,并吧末尾的逗号去掉*/
String[] values = (String[]) objvalue;
for(int i=0;i<values.length;i++){
value = values[i]+",";//这里我拼接的是英文的逗号。
}
value = value.substring(0,value.length()-1);//截掉最后一个逗号。
}else{
value = objvalue.toString();
}
log.info("key:"+name);
log.info("value:"+value);
returnMap.put(name , value);
}
Iterator it = returnMap.keySet().iterator();
while (it.hasNext()){
Object key = it.next();
if(returnMap.get(key) == null || "".equals (((String)returnMap.get(key)).trim())){
returnMap.put((String) key, null);
}
}
return returnMap;
}
public Map<String, String> request_pay(HttpServletRequest request) throws UnsupportedEncodingException {
Map<String, String> params = new HashMap<String, String>();
Map requestParams = request.getParameterMap();
for (Iterator iter = requestParams.keySet().iterator(); iter.hasNext();) {
String name = (String) iter.next();
String[] values = (String[]) requestParams.get(name);
String valueStr = "";
for (int i = 0; i < values.length; i++) {
valueStr = (i == values.length - 1) ? valueStr + values[i] : valueStr + values[i] + ",";
}
// 乱码解决,这段代码在出现乱码时使用。如果mysign和sign不相等也可以使用这段代码转化
valueStr = new String(valueStr.getBytes("ISO-8859-1"), "utf-8");
params.put(name, valueStr);
}
return params;
}
private Map<String, String> parseAliPayReq(HttpServletRequest request){
// 构造传入参数
Map<String, String> params = new HashMap<>();
Map requestParams = request.getParameterMap();
for (Iterator iter = requestParams.keySet().iterator(); iter.hasNext(); ) {
String name = (String) iter.next();
String[] values = (String[]) requestParams.get(name);
String valueStr = "";
for (int i = 0; i < values.length; i++) {
valueStr = (i == values.length - 1) ? valueStr + values[i]
: valueStr + values[i] + ",";
}
// 乱码解决,这段代码在出现乱码时使用。如果mysign和sign不相等也可以使用这段代码转化
try {
valueStr = new String(valueStr.getBytes("ISO-8859-1"), "utf-8");
} catch (UnsupportedEncodingException e) {
}
params.put(name, valueStr);
}
return params;
}
@PostMapping(value = "/alinotify/{channelId}")
@ApiOperation("渠道统一的支付成功,或退款成功 通知url")
public String aa(@PathVariable("channelId") Long channelId,HttpServletRequest request, HttpServletResponse response){
System.out.println("=======支付域名:alinotify========");
System.out.println("channelId:" + channelId);
System.out.println("=======request:"+request);
Map<String,String> params = new HashMap<String,String>();
Map requestParams = request.getParameterMap();
for (Iterator iter = requestParams.keySet().iterator(); iter.hasNext();) {
String name = (String) iter.next();
String[] values = (String[]) requestParams.get(name);
String valueStr = "";
for (int i = 0; i < values.length; i++) {
valueStr = (i == values.length - 1) ? valueStr + values[i]
: valueStr + values[i] + ",";
}
//乱码解决,这段代码在出现乱码时使用。如果mysign和sign不相等也可以使用这段代码转化
//valueStr = new String(valueStr.getBytes("ISO-8859-1"), "gbk");
params.put(name, valueStr);
}
HashMap<String, String> map = new HashMap<>();
for(Object str:requestParams.keySet()){
String parameter = request.getParameter(str.toString());
map.put(str.toString(),parameter);
System.out.println("key======"+str+":"+parameter);
}
String req = JsonUtils.toJsonString(map);
//out_trade_no
String out_trade_no = request.getParameter("out_trade_no");
System.out.println("out_trade_no:"+out_trade_no);
PayOrderDO byMerchantorderid = por.findByMerchantorderid(out_trade_no);
Optional<Alipay1> alipay1 = ar.findById(byMerchantorderid.getAppid());
String alipaypublickey = alipay1.get().getAlipaypublickey();
try {
boolean verify_result = AlipaySignature.rsaCheckV1(params,alipaypublickey,"UTF-8","RSA2");
System.out.println("verify_result:"+verify_result);
if(verify_result){
System.out.println("订单支付成功");
List<PayNotifyEntity> pnes = pnr.findByMerchantorderidAndStatus(out_trade_no,0);
if(!pnes.isEmpty()){
PayNotifyLogEntity paynotifylogentity = new PayNotifyLogEntity().toEntity(out_trade_no, req, "ali", "ali", byMerchantorderid.getAppid().toString());
//mysql 记录
pnlr.save(paynotifylogentity);
//log 日志----输出
new Logs(logger).toLog( req);
PayNotifyEntity entity = pnes.get(0);
String merchantorderid = entity.getMerchantorderid();
entity.setStatus(1);
pnr.save(entity);
PayOrderDO por1 = por.findByMerchantorderid(merchantorderid);
por1.setStatus(1);
//mysql --修改订单状态
por.save(por1);
}
}
} catch (AlipayApiException e) {
throw new RuntimeException(e);
}
return "success";
}
}
package com.icloud.server.payutils;
import com.icloud.server.payutils.dto.*;
/**
* 支付客户端,用于对接各支付渠道的 SDK,实现发起支付、退款等功能
*
*/
public interface PayClient {
/**
* 获得渠道编号
*
* @return 渠道编号
*/
Long getId();
/**
* 调用支付渠道,统一下单
*
* @param reqDTO 下单信息
* @return 各支付渠道的返回结果
*/
PayCommonResult<?> unifiedOrder(PayOrderUnifiedReqDTO reqDTO);
/**
* 解析支付单的通知结果
*
* @param data 通知结果
* @return 解析结果
* @throws Exception 解析失败,抛出异常
*/
PayOrderNotifyRespDTO parseOrderNotify(PayNotifyDataDTO data) throws Exception;
/**
* 调用支付渠道,进行退款
* @param reqDTO 统一退款请求信息
* @return 各支付渠道的统一返回结果
*/
PayCommonResult<PayRefundUnifiedRespDTO> unifiedRefund(PayRefundUnifiedReqDTO reqDTO);
/**
* 解析支付退款通知数据
* @param notifyData 支付退款通知请求数据
* @return 支付退款通知的Notify DTO
*/
PayRefundNotifyDTO parseRefundNotify(PayNotifyDataDTO notifyData);
// TODO @:后续改成非 default,避免不知道去实现
/**
* 验证是否渠道通知
*
* @param notifyData 通知数据
* @return 默认是 true
*/
default boolean verifyNotifyData(PayNotifyDataDTO notifyData) {
return true;
}
// TODO @:后续改成非 default,避免不知道去实现
/**
* 判断是否为退款通知
*
* @param notifyData 通知数据
* @return 默认是 false
*/
default boolean isRefundNotify(PayNotifyDataDTO notifyData){
return false;
}
}
package com.icloud.server.payutils;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.Validator;
import java.util.Set;
/**
* 支付客户端的配置,本质是支付渠道的配置
* 每个不同的渠道,需要不同的配置,通过子类来定义
*
* @author
*/
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
// @JsonTypeInfo 注解的作用,Jackson 多态
// 1. 序列化到时数据库时,增加 @class 属性。
// 2. 反序列化到内存对象时,通过 @class 属性,可以创建出正确的类型
public interface PayClientConfig {
/**
* 配置验证参数是
*
* @param validator 校验对象
* @return 配置好的验证参数
*/
Set<ConstraintViolation<PayClientConfig>> verifyParam(Validator validator);
// TODO @aquan:貌似抽象一个 validation group 就好了!
/**
* 参数校验
*
* @param validator 校验对象
*/
default void validate(Validator validator) {
Set<ConstraintViolation<PayClientConfig>> violations = verifyParam(validator);
if (!violations.isEmpty()) {
throw new ConstraintViolationException(violations);
}
}
}
package com.icloud.server.payutils;
/**
* 支付客户端的工厂接口
*
* @author
*/
public interface PayClientFactory {
/**
* 获得支付客户端
*
* @param channelId 渠道编号
* @return 支付客户端
*/
PayClient getPayClient(Long channelId);
/**
* 创建支付客户端
*
* @param channelId 渠道编号
* @param channelCode 渠道编码
* @param config 支付配置
*/
<Config extends PayClientConfig> void createOrUpdatePayClient(Long channelId, String channelCode,
Config config);
}
package com.icloud.server.payutils;
import cn.hutool.core.lang.Assert;
import com.icloud.server.payutils.alipay.AlipayPayClientConfig;
import com.icloud.server.payutils.alipay.AlipayQrPayClient;
import com.icloud.server.payutils.alipay.AlipayWapPayClient;
import com.icloud.server.payutils.wx.*;
import com.icloud.server.web.err.pay.PayChannelEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* 支付客户端的工厂实现类
*
* @author
*/
@Slf4j
@Component
public class PayClientFactoryImpl implements PayClientFactory {
/**
* 支付客户端 Map
* key:渠道编号
*/
private final ConcurrentMap<Long, AbstractPayClient<?>> clients = new ConcurrentHashMap<>();
@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> void 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);
}
}
@SuppressWarnings("unchecked")
private <Config extends PayClientConfig> AbstractPayClient<Config> createPayClient(
Long channelId, String channelCode, Config config) {
PayChannelEnum channelEnum = PayChannelEnum.getByCode(channelCode);
Assert.notNull(channelEnum, String.format("支付渠道(%s) 为空", channelEnum));
// 创建客户端
// TODO WX_LITE WX_APP 如果不添加在 项目启动的时候去初始化会报错无法启动。所以我手动加了两个,具体需要你来配
switch (channelEnum) {
case WX_PUB: return (AbstractPayClient<Config>) new WXPubPayClient(channelId, (WXPayClientConfig) config);
case WX_H5: return (AbstractPayClient<Config>) new WXH5PayClient(channelId, (WXPayClientConfig) config);
case WX_LITE: return (AbstractPayClient<Config>) new WXLitePayClient(channelId, (WXPayClientConfig) config); //微信小程序请求支付
case WX_APP: return (AbstractPayClient<Config>) new WXPubPayClient(channelId, (WXPayClientConfig) config);
case WX_NATIVE: return (AbstractPayClient<Config>) new WXNativePayClient(channelId, (WXPayClientConfig) config);
case ALIPAY_WAP: return (AbstractPayClient<Config>) new AlipayWapPayClient(channelId, (AlipayPayClientConfig) config);
case ALIPAY_QR: return (AbstractPayClient<Config>) new AlipayQrPayClient(channelId, (AlipayPayClientConfig) config);
case ALIPAY_APP: return (AbstractPayClient<Config>) new AlipayQrPayClient(channelId, (AlipayPayClientConfig) config);
case ALIPAY_PC: return (AbstractPayClient<Config>) new AlipayQrPayClient(channelId, (AlipayPayClientConfig) config);
}
// 创建失败,错误日志 + 抛出异常
log.error("[createPayClient][配置({}) 找不到合适的客户端实现]", config);
throw new IllegalArgumentException(String.format("配置(%s) 找不到合适的客户端实现", config));
}
}
package com.icloud.server.payutils;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.lang.Assert;
import com.icloud.server.payutils.pojo.CommonResult;
import com.icloud.server.web.err.ErrorCode;
import com.icloud.server.web.err.pay.PayFrameworkErrorCodeConstants;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
/**
* 支付的 CommonResult 拓展类
*
* 考虑到不同的平台,返回的 code 和 msg 是不同的,所以统一额外返回 {@link #apiCode} 和 {@link #apiMsg} 字段
*
* @author yang
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayCommonResult<T> extends CommonResult<T> {
/**
* API 返回错误码
*
* 由于第三方的错误码可能是字符串,所以使用 String 类型
*/
private String apiCode;
/**
* API 返回提示
*/
private String apiMsg;
private PayCommonResult() {
}
public static <T> PayCommonResult<T> build(String apiCode, String apiMsg, T data, AbstractPayCodeMapping codeMapping) {
Assert.notNull(codeMapping, "参数 codeMapping 不能为空");
PayCommonResult<T> result = new PayCommonResult<T>().setApiCode(apiCode).setApiMsg(apiMsg);
result.setData(data);
// 翻译错误码
if (codeMapping != null) {
ErrorCode errorCode = codeMapping.apply(apiCode, apiMsg);
result.setCode(errorCode.getCode()).setMsg(errorCode.getMsg());
}
return result;
// return null;
}
public static <T> PayCommonResult<T> error(Throwable ex) {
PayCommonResult<T> result = new PayCommonResult<>();
result.setCode(PayFrameworkErrorCodeConstants.EXCEPTION.getCode());
result.setMsg(ExceptionUtil.getRootCauseMessage(ex));
return result;
}
}
package com.icloud.server.payutils;
import com.icloud.server.payutils.alipay.AlipayPayClientConfig;
import lombok.Builder;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.Entity;
import javax.validation.constraints.NotBlank;
import java.io.Serializable;
import java.util.Date;
@Data
public class PayEntiy implements Serializable {
private String appid;
/**
*微信
*/
private String openid;
private String mchId;
/**
*微信
*/
private String mchKey;
private String subject;
private String body;
/**
* 用户 IP
*/
private String userIp;
/**
* 支付结果的 notify
* 回调地址支付结果的 notify 回调地址必须是 URL 格式
*
*/
private String notifyUrl;
/**
* 商户私钥 ---阿里
*/
private String privateKey;
/**
* 支付宝公钥字符串---阿里
*/
private String alipayPublicKey;
/**
* 支付结果的 return 回调地址
* "支付结果的 return 回调地址必须是 URL 格式
*/
private String returnUrl;
private Long amount;
private Date expireTime;
/**
* 商户订单编号
*/
private String merchantOrderId;
/**
* 支付方式:1微信,2阿里
*/
private Integer payType;
/**
* 订单id
*/
private Long channelId;
private String OutTradeNo;
public String getOutTradeNo() {
return OutTradeNo;
}
public void setOutTradeNo(String outTradeNo) {
OutTradeNo = outTradeNo;
}
public String getAppid() {
return appid;
}
public void setAppid(String appid) {
this.appid = appid;
}
public String getOpenid() {
return openid;
}
public void setOpenid(String openid) {
this.openid = openid;
}
public String getMchId() {
return mchId;
}
public void setMchId(String mchId) {
this.mchId = mchId;
}
public String getMchKey() {
return mchKey;
}
public void setMchKey(String mchKey) {
this.mchKey = mchKey;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
public String getUserIp() {
return userIp;
}
public void setUserIp(String userIp) {
this.userIp = userIp;
}
public String getNotifyUrl() {
return notifyUrl;
}
public void setNotifyUrl(String notifyUrl) {
this.notifyUrl = notifyUrl;
}
public String getPrivateKey() {
return privateKey;
}
public void setPrivateKey(String privateKey) {
this.privateKey = privateKey;
}
public String getAlipayPublicKey() {
return alipayPublicKey;
}
public void setAlipayPublicKey(String alipayPublicKey) {
this.alipayPublicKey = alipayPublicKey;
}
public String getReturnUrl() {
return returnUrl;
}
public void setReturnUrl(String returnUrl) {
this.returnUrl = returnUrl;
}
public Long getAmount() {
return amount;
}
public void setAmount(Long amount) {
this.amount = amount;
}
public Date getExpireTime() {
return expireTime;
}
public void setExpireTime(Date expireTime) {
this.expireTime = expireTime;
}
public String getMerchantOrderId() {
return merchantOrderId;
}
public void setMerchantOrderId(String merchantOrderId) {
this.merchantOrderId = merchantOrderId;
}
public Long getChannelId() {
return channelId;
}
public void setChannelId(Long channelId) {
this.channelId = channelId;
}
public Integer getPayType() {
return payType;
}
public void setPayType(Integer payType) {
this.payType = payType;
}
}
package com.icloud.server.payutils;
import org.w3c.dom.Document;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
public class WXPayXmlUtil {
public static DocumentBuilder newDocumentBuilder() throws ParserConfigurationException {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
documentBuilderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
documentBuilderFactory.setFeature("http://xml.org/sax/features/external-general-entities", false);
documentBuilderFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
documentBuilderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
documentBuilderFactory.setXIncludeAware(false);
documentBuilderFactory.setExpandEntityReferences(false);
return documentBuilderFactory.newDocumentBuilder();
}
public static Document newDocument() throws ParserConfigurationException {
return newDocumentBuilder().newDocument();
}
}
package com.icloud.server.payutils;
import com.google.common.collect.Maps;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import java.io.IOException;
import java.util.List;
import java.util.Map;
/**
* @Description: XMLUtil
* @Author: lin.shi
* @CreateTime: 2017-08-10 16:47
*/
public class XMLUtil {
/**
* 解析xml,返回第一级元素键值对。如果第一级元素有子节点,则此节点的值是子节点的xml数据。
*
* @param strxml
* @return
* @throws IOException
*/
public static Map doXMLParse(String strxml) {
Document document = null;
try {
document = DocumentHelper.parseText(strxml);
} catch (DocumentException e) {
e.printStackTrace();
}
Element rootElement = document.getRootElement();
@SuppressWarnings("unchecked")
List<org.dom4j.Element> elements = rootElement.elements();
Map<String, String> map = Maps.newHashMap();
for (org.dom4j.Element element : elements) {
map.put(element.getName(), element.getText());
}
return map;
}
}
\ No newline at end of file
package com.icloud.server.payutils.alipay;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUtil;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayConfig;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.domain.AlipayTradeRefundModel;
import com.alipay.api.internal.util.AlipaySignature;
import com.alipay.api.request.AlipayTradeRefundRequest;
import com.alipay.api.response.AlipayTradeRefundResponse;
import com.icloud.server.payutils.AbstractPayClient;
import com.icloud.server.payutils.AbstractPayCodeMapping;
import com.icloud.server.payutils.PayCommonResult;
import com.icloud.server.payutils.dto.*;
import com.icloud.server.web.err.pay.PayNotifyRefundStatusEnum;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
/**
* 支付宝抽象类, 实现支付宝统一的接口。如退款
*
* @author jason
*/
@Slf4j
public abstract class AbstractAlipayClient extends AbstractPayClient<AlipayPayClientConfig> {
protected DefaultAlipayClient client;
public AbstractAlipayClient(Long channelId, String channelCode, AlipayPayClientConfig config, AbstractPayCodeMapping codeMapping, DefaultAlipayClient client) {
super(channelId, channelCode, config, codeMapping);
this.client = client;
}
public AbstractAlipayClient(Long channelId, String channelCode,
AlipayPayClientConfig config, AbstractPayCodeMapping codeMapping) {
super(channelId, channelCode, config, codeMapping);
}
@Override
@SneakyThrows
protected void doInit() {
com.alipay.api.AlipayConfig alipayConfig = new AlipayConfig();
BeanUtil.copyProperties(config, alipayConfig, false);
this.client = new DefaultAlipayClient(alipayConfig);
}
/**
* 从支付宝通知返回参数中解析 PayOrderNotifyRespDTO, 通知具体参数参考
* //https://opendocs.alipay.com/open/203/105286
* @param data 通知结果
* @return 解析结果 PayOrderNotifyRespDTO
* @throws Exception 解析失败,抛出异常
*/
@Override
public PayOrderNotifyRespDTO parseOrderNotify(PayNotifyDataDTO data) throws Exception {
Map<String, String> params = strToMap(data.getBody());
return PayOrderNotifyRespDTO.builder().orderExtensionNo(params.get("out_trade_no"))
.channelOrderNo(params.get("trade_no")).channelUserId(params.get("seller_id"))
.tradeStatus(params.get("trade_status"))
.successTime(DateUtil.parse(params.get("notify_time"), "yyyy-MM-dd HH:mm:ss"))
.data(data.getBody()).build();
}
@Override
public PayRefundNotifyDTO parseRefundNotify(PayNotifyDataDTO notifyData) {
Map<String, String> params = strToMap(notifyData.getBody());
PayRefundNotifyDTO notifyDTO = PayRefundNotifyDTO.builder().channelOrderNo(params.get("trade_no"))
.tradeNo(params.get("out_trade_no"))
.reqNo(params.get("out_biz_no"))
.status(PayNotifyRefundStatusEnum.SUCCESS)
.refundSuccessTime(DateUtil.parse(params.get("gmt_refund"), "yyyy-MM-dd HH:mm:ss"))
.build();
return notifyDTO;
}
@Override
public boolean isRefundNotify(PayNotifyDataDTO notifyData) {
if (notifyData.getParams().containsKey("refund_fee")) {
return true;
} else {
return false;
}
}
@Override
public boolean verifyNotifyData(PayNotifyDataDTO notifyData) {
boolean verifyResult = false;
try {
verifyResult = AlipaySignature.rsaCheckV1(notifyData.getParams(), config.getAlipayPublicKey(), StandardCharsets.UTF_8.name(), "RSA2");
} catch (AlipayApiException e) {
log.error("[AlipayClient verifyNotifyData][(notify param is :{}) 验证失败]", notifyData.getParams(), e);
}
return verifyResult;
}
/**
* 支付宝统一的退款接口 alipay.trade.refund
* @param reqDTO 退款请求 request DTO
* @return 退款请求 Response
*/
@Override
protected PayCommonResult<PayRefundUnifiedRespDTO> doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
AlipayTradeRefundModel model=new AlipayTradeRefundModel();
model.setTradeNo(reqDTO.getChannelOrderNo());
model.setOutTradeNo(reqDTO.getPayTradeNo());
model.setOutRequestNo(reqDTO.getMerchantRefundId());
model.setRefundAmount(calculateAmount(reqDTO.getAmount()).toString());
model.setRefundReason(reqDTO.getReason());
AlipayTradeRefundRequest refundRequest = new AlipayTradeRefundRequest();
refundRequest.setBizModel(model);
try {
AlipayTradeRefundResponse response = client.execute(refundRequest);
log.info("[doUnifiedRefund][response({}) 发起退款 渠道返回", response);
if (response.isSuccess()) {
//退款导致触发的异步通知是发送到支付接口中设置的notify_url
//支付宝不返回退款单号,设置为空
PayRefundUnifiedRespDTO respDTO = new PayRefundUnifiedRespDTO();
respDTO.setChannelRefundId("");
return PayCommonResult.build(response.getCode(), response.getMsg(), respDTO, codeMapping);
}
// 失败。需要抛出异常
return PayCommonResult.build(response.getCode(), response.getMsg(), null, codeMapping);
} catch (AlipayApiException e) {
// TODO 记录异常日志
log.error("[doUnifiedRefund][request({}) 发起退款失败,网络读超时,退款状态未知]", reqDTO, e);
return PayCommonResult.build(e.getErrCode(), e.getErrMsg(), null, codeMapping);
}
}
/**
* 支付宝统一回调参数 str 转 map
*
* @param s 支付宝支付通知回调参数
* @return map 支付宝集合
*/
public static Map<String, String> strToMap(String s) {
// TODO @zxy:这个可以使用 hutool 的 HttpUtil decodeParams 方法么?
Map<String, String> stringStringMap = new HashMap<>();
// 调整时间格式
String s3 = s.replaceAll("%3A", ":");
// 获取 map
String s4 = s3.replace("+", " ");
String[] split = s4.split("&");
for (String s1 : split) {
String[] split1 = s1.split("=");
stringStringMap.put(split1[0], split1[1]);
}
return stringStringMap;
}
}
package com.icloud.server.payutils.alipay;
public class AlipayConfig {
}
package com.icloud.server.payutils.alipay;
import com.icloud.server.payutils.PayClientConfig;
import lombok.Data;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.util.Set;
// TODO参数校验
/**
* 支付宝的 PayClientConfig 实现类
*
* @author
*/
@Data
public class AlipayPayClientConfig implements PayClientConfig {
/**
* 网关地址 - 线上
*/
public static final String SERVER_URL_PROD = "https://openapi.alipay.com/gateway.do";
/**
* 网关地址 - 沙箱
*/
public static final String SERVER_URL_SANDBOX = "https://openapi.alipaydev.com/gateway.do";
/**
* 公钥类型 - 公钥模式
*/
public static final Integer MODE_PUBLIC_KEY = 1;
/**
* 公钥类型 - 证书模式
*/
public static final Integer MODE_CERTIFICATE = 2;
/**
* 签名算法类型 - RSA
*/
public static final String SIGN_TYPE_DEFAULT = "RSA2";
/**
* 网关地址
* 1. {@link #SERVER_URL_PROD}
* 2. {@link #SERVER_URL_SANDBOX}
*/
@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;
public interface ModePublicKey {
}
public interface ModeCertificate {
}
@Override
public Set<ConstraintViolation<PayClientConfig>> verifyParam(Validator validator) {
return validator.validate(this,
MODE_PUBLIC_KEY.equals(this.getMode()) ? ModePublicKey.class : ModeCertificate.class);
}
}
package com.icloud.server.payutils.alipay;
import com.icloud.server.payutils.AbstractPayCodeMapping;
import com.icloud.server.web.err.ErrorCode;
import com.icloud.server.web.err.GlobalErrorCodeConstants;
import java.util.Objects;
/**
* 支付宝的 PayCodeMapping 实现类
*
* @author
*/
public class AlipayPayCodeMapping extends AbstractPayCodeMapping {
@Override
protected ErrorCode apply0(String apiCode, String apiMsg) {
if (Objects.equals(apiCode, "10000")) {
return GlobalErrorCodeConstants.SUCCESS;
}
// alipay wap api code 返回为null, 暂时定为-9999
if (Objects.equals(apiCode, "-9999")) {
return GlobalErrorCodeConstants.SUCCESS;
}
return null;
}
}
package com.icloud.server.payutils.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 com.icloud.server.payutils.AbstractPayCodeMapping;
import com.icloud.server.payutils.PayCommonResult;
import com.icloud.server.payutils.dto.PayOrderUnifiedReqDTO;
import com.icloud.server.web.err.pay.PayChannelEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* 支付宝【扫码支付】的 PayClient 实现类
* 文档:https://opendocs.alipay.com/apis/02890k
*
* @author
*/
@Slf4j
public class AlipayQrPayClient extends AbstractAlipayClient {
// public AlipayQrPayClient(Long channelId, String channelCode, AlipayPayClientConfig config, AbstractPayCodeMapping codeMapping) {
// super(channelId, channelCode, config, codeMapping);
// }
//
// @Override
// protected PayCommonResult<?> doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws Throwable {
// return null;
// }
//
public AlipayQrPayClient(Long channelId, AlipayPayClientConfig config) {
super(channelId, PayChannelEnum.ALIPAY_QR.getCode(), config, new AlipayPayCodeMapping());
}
@Override
public PayCommonResult<AlipayTradePrecreateResponse> doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
// 构建 AlipayTradePrecreateModel 请求
AlipayTradePrecreateModel model = new AlipayTradePrecreateModel();
model.setOutTradeNo(reqDTO.getMerchantOrderId());
model.setSubject(reqDTO.getSubject());
model.setBody(reqDTO.getBody());
model.setTotalAmount(calculateAmount(reqDTO.getAmount()).toString()); // 单位:元
// 构建 AlipayTradePrecreateRequest
AlipayTradePrecreateRequest request = new AlipayTradePrecreateRequest();
request.setBizModel(model);
request.setNotifyUrl(reqDTO.getNotifyUrl());
request.setReturnUrl(reqDTO.getReturnUrl());
// 执行请求
AlipayTradePrecreateResponse response;
try {
response = client.execute(request);
System.out.println(response);
} catch (AlipayApiException e) {
log.error("[unifiedOrder][request({}) 发起支付失败]", reqDTO, e);
return PayCommonResult.build(e.getErrCode(), e.getErrMsg(), null, codeMapping);
}
// TODO :sub Code 需要测试下各种失败的情况
return PayCommonResult.build(response.getCode(), response.getMsg(), response, codeMapping);
}
}
package com.icloud.server.payutils.alipay;
import cn.hutool.core.date.DateUtil;
import com.alibaba.fastjson.JSON;
import com.alipay.api.AlipayApiException;
import com.alipay.api.domain.AlipayTradeWapPayModel;
import com.alipay.api.request.AlipayTradeWapPayRequest;
import com.alipay.api.response.AlipayTradeWapPayResponse;
import com.icloud.server.payutils.AbstractPayCodeMapping;
import com.icloud.server.payutils.PayCommonResult;
import com.icloud.server.payutils.dto.PayOrderUnifiedReqDTO;
import com.icloud.server.utils.JsonUtils;
import com.icloud.server.web.err.pay.PayChannelEnum;
import lombok.extern.slf4j.Slf4j;
import java.util.HashMap;
import java.util.Objects;
/**
* 支付宝【手机网站】的 PayClient 实现类
* 文档:https://opendocs.alipay.com/apis/api_1/alipay.trade.wap.pay
*
* @author
*/
@Slf4j
public class AlipayWapPayClient extends AbstractAlipayClient {
// public AlipayWapPayClient(Long channelId, String channelCode, AlipayPayClientConfig config, AbstractPayCodeMapping codeMapping) {
// super(channelId, channelCode, config, codeMapping);
// }
//
// @Override
// protected PayCommonResult<?> doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws Throwable {
// return null;
// }
public AlipayWapPayClient(Long channelId, AlipayPayClientConfig config) {
super(channelId, PayChannelEnum.ALIPAY_WAP.getCode(), config, new AlipayPayCodeMapping());
}
@Override
public PayCommonResult<AlipayTradeWapPayResponse> doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
// 构建 AlipayTradeWapPayModel 请求
AlipayTradeWapPayModel model = new AlipayTradeWapPayModel();
model.setOutTradeNo(reqDTO.getMerchantOrderId());
model.setSubject(reqDTO.getSubject());
model.setBody(reqDTO.getBody());
HashMap<String, String> map = new HashMap<>();
map.put("attach",reqDTO.getChannelExtras().get("attach"));
String attach = JsonUtils.toJsonString(map);
model.setBusinessParams(attach);
model.setTotalAmount(calculateAmount(reqDTO.getAmount()).toString());
model.setProductCode("QUICK_WAP_PAY"); //
model.setQuitUrl(reqDTO.getReturnUrl());
// https://opendocs.alipay.com/apis/api_1/alipay.trade.wap.pay
//model.setSellerId("2088102147948060");
model.setTimeExpire(DateUtil.format(reqDTO.getExpireTime(),"yyyy-MM-dd HH:mm:ss"));
// TODO :userIp
// 构建 AlipayTradeWapPayRequest
AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest();
request.setBizModel(model);
request.setNotifyUrl(reqDTO.getNotifyUrl());
request.setReturnUrl(reqDTO.getReturnUrl());
// 执行请求
AlipayTradeWapPayResponse response;
try {
response = client.pageExecute(request);
} catch (AlipayApiException e) {
return PayCommonResult.build(e.getErrCode(), e.getErrMsg(), null, codeMapping);
}
// TODO :sub Code
if(response.isSuccess() && Objects.isNull(response.getCode()) && Objects.nonNull(response.getBody())){
//成功alipay wap 成功 code 为 null , body 为form 表单
return PayCommonResult.build("-9999", "Success", response, codeMapping);
}else {
return PayCommonResult.build(response.getCode(), response.getMsg(), response, codeMapping);
}
}
}
package com.icloud.server.payutils.dto;
import lombok.Data;
import javax.persistence.Column;
import java.io.Serializable;
import java.util.Date;
/**
* 基础实体对象
*
* @author
*/
@Data
public abstract class BaseDO implements Serializable {
/**
* 创建时间
*/
@Column(name = "createTime")
private Date createtime;
/**
* 最后更新时间
*/
@Column(name = "updateTime")
private Date updatetime;
/**
* 创建者,目前使用 SysUser 的 id 编号
*
* 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。
*/
@Column(name = "creator")
private String creator;
/**
* 更新者,目前使用 SysUser 的 id 编号
*
* 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。
*/
@Column(name = "updater")
private String updater;
/**
* 是否删除
*/
@Column(name = "deleted")
private Boolean deleted;
}
package com.icloud.server.payutils.dto;
import lombok.Builder;
import lombok.Data;
import lombok.ToString;
import java.util.Map;
/**
* 支付订单,退款订单回调,渠道的统一通知请求数据
*/
@Data
@ToString
@Builder
public class PayNotifyDataDTO {
/**
* HTTP 回调接口的 request body
*/
private String body;
/**
* HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数
*/
private Map<String,String> params;
}
package com.icloud.server.payutils.dto;
import lombok.Data;
import lombok.experimental.Accessors;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import javax.persistence.Column;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;
import java.util.Date;
/**
* 商户支付、退款等的通知
* 在支付系统收到支付渠道的支付、退款的结果后,需要不断的通知到业务系统,直到成功。
*
* @author
*/
//@Table("pay_notify_task") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Table(name = "pay_notify_task")
@Data
//@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class PayNotifyTaskDO implements Serializable {
/**
* 通知频率,单位为秒。
*
* 算上首次的通知,实际是一共 1 + 8 = 9 次。
*/
public static final Integer[] NOTIFY_FREQUENCY = new Integer[]{
15, 15, 30, 180,
1800, 1800, 1800, 3600
};
/**
* 编号,自增
*/
@Id
@Column(name = "id")
private Long id;
/**
* 商户编号
*/
@Column(name = "merchantId")
private Long merchantId;
/**
* 应用编号
*
*/
@Column(name = "appId")
private Long appId;
/**
* 通知类型
*/
@Column(name = "type")
private Integer type;
/**
* 数据编号,根据不同 type 进行关联:
*/
@Column(name = "type")
private Long dataId;
/**
* 商户订单编号
*/
@Column(name = "type")
private String merchantOrderId;
/**
* 通知状态
*
*/
@Column(name = "type")
private Integer status;
/**
* 下一次通知时间
*/
@Column(name = "type")
private Date nextNotifyTime;
/**
* 最后一次执行时间
*/
@Column(name = "type")
private Date lastExecuteTime;
/**
* 当前通知次数
*/
@Column(name = "type")
private Integer notifyTimes;
/**
* 最大可通知次数
*/
@Column(name = "type")
private Integer maxNotifyTimes;
/**
* 通知地址
*/
@Column(name = "type")
private String notifyUrl;
@CreationTimestamp
@Column(name = "create_time")
private Date createtime;
/**
* 最后更新时间
*/
@UpdateTimestamp
@Column(name = "update_time")
private Date updatetime;
/**
* 创建者,目前使用 SysUser 的 id 编号
*
* 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。
*/
@Column(name = "creator")
private String creator;
/**
* 更新者,目前使用 SysUser 的 id 编号
*
* 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。
*/
@Column(name = "updater")
private String updater;
/**
* 是否删除
*/
@Column(name = "deleted")
private Boolean deleted;
}
package com.icloud.server.payutils.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
/**
* 支付通知 Response DTO
*
* @author
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PayOrderNotifyRespDTO {
/**
* 支付订单号(支付模块的)
*/
private String orderExtensionNo;
/**
* 支付渠道编号
*/
private String channelOrderNo;
/**
* 支付渠道用户编号
*/
private String channelUserId;
/**
* 支付成功时间
*/
private Date successTime;
/**
* 通知的原始数据
*
* 主要用于持久化,方便后续修复数据,或者排错
*/
private String data;
/**
* TODO @jason 结合其他的渠道定义成枚举,
* alipay
* TRADE_CLOSED,未付款交易超时关闭,或支付完成后全额退款。
* TRADE_SUCCESS, 交易支付成功
* TRADE_FINISHED 交易结束,不可退款。
*/
private String tradeStatus;
}
package com.icloud.server.payutils.dto;
import lombok.Data;
import java.util.Date;
import java.util.Map;
/**
* 统一下单 Request DTO
*
* @author
*/
@Data
public class PayOrderUnifiedReqDTO {
/**
* 用户 IP
*/
private String userIp;
// ========== 商户相关字段 ==========
/**
* 商户订单编号
*/
private String merchantOrderId;
/**
* 商品标题
* 商品标题不能超过 32
*/
private String subject;
/**
* 商品描述信息
* 商品描述信息长度不能超过128
*/
private String body;
/**
* 支付结果的 notify
* 回调地址支付结果的 notify 回调地址必须是 URL 格式
*
*/
private String notifyUrl;
/**
* 支付结果的 return 回调地址
* "支付结果的 return 回调地址必须是 URL 格式
*/
private String returnUrl;
// ========== 订单相关字段 ==========
/**
* 支付金额,单位:分
* 支付金额不能为空
* 支付金额必须大于零
*/
private Long amount;
/**
* 支付过期时间
* 支付过期时间不能为空
*/
private Date expireTime;
// ========== 拓展参数 ==========
/**
* 支付渠道的额外参数
*
* 例如说,微信公众号需要传递 openid 参数
*/
private Map<String, String> channelExtras;
}
package com.icloud.server.payutils.dto;
import com.icloud.server.web.err.pay.PayNotifyRefundStatusEnum;
import lombok.Builder;
import lombok.Data;
import lombok.ToString;
import java.util.Date;
/**
* 从渠道返回数据中解析得到的支付退款通知的Notify DTO
*
* @author jason
*/
@Data
@ToString
@Builder
public class PayRefundNotifyDTO {
/**
* 支付渠道编号
*/
private String channelOrderNo;
/**
* 交易订单号,根据规则生成
* 调用支付渠道时,使用该字段作为对接的订单号。
* 1. 调用微信支付 https://api.mch.weixin.qq.com/pay/unifiedorder 时,使用该字段作为 out_trade_no
* 2. 调用支付宝 https://opendocs.alipay.com/apis 时,使用该字段作为 out_trade_no
* 这里对应 pay_extension 里面的 no
* 例如说,P202110132239124200055
*/
private String tradeNo;
/**
* https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 out_refund_no
* https://opendocs.alipay.com/apis alipay.trade.refund 中的 out_request_no
* 退款请求号。
* 标识一次退款请求,需要保证在交易号下唯一,如需部分退款,则此参数必传。
* 注:针对同一次退款请求,如果调用接口失败或异常了,重试时需要保证退款请求号不能变更,
* 防止该笔交易重复退款。支付宝会保证同样的退款请求号多次请求只会退一次。
* 退款单请求号,根据规则生成
*
* 例如说,RR202109181134287570000
*/
private String reqNo;
/**
* 退款是否成功
*/
private PayNotifyRefundStatusEnum status;
/**
* 退款成功时间
*/
private Date refundSuccessTime;
}
package com.icloud.server.payutils.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
/**
* 统一 退款 Request DTO
*
* @author jason
*/
@Accessors(chain = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class PayRefundUnifiedReqDTO {
/**
* 用户 IP
*/
private String userIp;
// TODO @jason:这个是否为非必传字段呀,只需要传递 payTradeNo 字段即可。尽可能精简
/**
* https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 transaction_id
* https://opendocs.alipay.com/apis alipay.trade.refund 中的 trade_no
* 渠道订单号
*/
private String channelOrderNo;
/**
* https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 out_trade_no
* https://opendocs.alipay.com/apis alipay.trade.refund 中的 out_trade_no
* 支付交易号 {PayOrderExtensionDO no字段} 和 渠道订单号 不能同时为空
*/
private String payTradeNo;
/**
* https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 out_refund_no
* https://opendocs.alipay.com/apis alipay.trade.refund 中的 out_trade_no
* 退款请求单号 同一退款请求单号多次请求只退一笔。
* 退款请求单号
* 使用 商户的退款单号。{PayRefundDO 字段 merchantRefundNo}
*/
private String merchantRefundId;
/**
* 退款原因
* 退款原因不能为空
*/
private String reason;
/**
* 退款金额,单位:分
* 退款金额不能为空
* 支付金额必须大于零
*/
private Long amount;
/**
* 退款结果 notify 回调地址, 支付宝退款不需要回调地址, 微信需要
* 支付结果的 notify 回调地址必须是 URL 格式
*/
private String notifyUrl;
}
package com.icloud.server.payutils.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
/**
* 统一退款 Response DTO
*
* @author jason
*/
@Accessors(chain = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class PayRefundUnifiedRespDTO {
/**
* 渠道退款单编号
*/
private String channelRefundId;
}
package com.icloud.server.payutils.enums;
/**
* 可生成 Int 数组的接口
*
* @author
*/
public interface IntArrayValuable {
/**
* @return int 数组
*/
int[] array();
}
package com.icloud.server.payutils.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 支付订单的状态枚举
*
* @author
*/
@Getter
@AllArgsConstructor
public enum PayTypeEnum {
WXPAY(1, "微信"),
WXJSPAY(3, "微信"),
ALIPAY(2, "支付宝");
private final Integer status;
private final String name;
// @Override
// public int[] array() {
// return new int[0];
// }
public Integer getStatus() {
return status;
}
public String getName() {
return name;
}
}
package com.icloud.server.payutils.goods;
import com.icloud.server.scala.engine.service.Goodspaye;
import org.joda.time.DateTime;
import java.util.List;
/**
* @Author : yang
* @Date : 2022-07-14 00:24
* @Version : 2.1.2
*/
public interface GoodsPayDao {
public List<Goodspaye> getGoodsPaysByHour(DateTime hour);
}
package com.icloud.server.payutils.goods;
import com.icloud.server.scala.engine.service.Goodspaye;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.ArrayList;
import java.util.List;
public class GoodsPayDaoImpl implements GoodsPayDao{
@PersistenceContext
private EntityManager em;
Logger log = LoggerFactory.getLogger(GoodsPayDaoImpl.class);
@Override
public List<Goodspaye> getGoodsPaysByHour(DateTime now) {
DateTime hour= now.withSecondOfMinute(0).withMillisOfSecond(0).withMinuteOfHour(0);
DateTime nextHour = hour.plusHours(1);
String curDay = hour.withMillisOfDay(0).toString("yyyy-MM-dd HH:mm:ss");
String nextDay = hour.withMillisOfDay(0).plusDays(1).toString("yyyy-MM-dd HH:mm:ss");
StringBuffer query = new StringBuffer();
query.append(" select distinct p from Goodspaye p");
query.append(" where starttime <= '").append(now.toString("yyyy-MM-dd HH:mm:ss")).append("'");
query.append(" and expire_endtime > '").append(now.toString("yyyy-MM-dd HH:mm:ss")).append("'");
log.info(query.toString());
List<Goodspaye> Goodspaye = this.em.createQuery(query.toString(), Goodspaye.class).getResultList();
List<Goodspaye> res = new ArrayList<Goodspaye>();
for (Goodspaye gps : Goodspaye) {
res.add(gps);
}
return res;
}
}
package com.icloud.server.payutils.goods;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.repository.CrudRepository;
public interface GoodsRepositorye extends CrudRepository<Goodspayentity, Integer>, GoodsPayDao, JpaSpecificationExecutor<Goodspayentity> {
}
package com.icloud.server.payutils.goods;
import com.fasterxml.jackson.annotation.JsonIdentityInfo;
import org.hibernate.annotations.BatchSize;
import org.hibernate.annotations.Fetch;
import org.hibernate.annotations.FetchMode;
import org.hibernate.annotations.Proxy;
import org.joda.time.DateTime;
import com.voodoodyne.jackson.jsog.JSOGGenerator;
import javax.persistence.*;
import java.util.*;
@Proxy(lazy = false)
@Entity
@Table(name = "goods_pay")
@JsonIdentityInfo(generator = JSOGGenerator.class)
public class Goodspayentity implements Cloneable {
@Id
@Column
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(name = "starttime")
private Date starttime;
@Column(name = "expire_endtime")
private Date expire_endtime;
@Column(name = "payment")
private String payment;
}
package com.icloud.server.payutils.goods;
import org.joda.time.DateTime;
public class testt {
public static void main(String[] args) {
DateTime now = DateTime.now();
DateTime hour= now.withHourOfDay(0).withMinuteOfHour(0).withSecondOfMinute(0).withMillisOfSecond(0);
System.out.println(hour);
ss(hour);
}
public static void ss(DateTime now){
DateTime hour= now.withSecondOfMinute(0).withMillisOfSecond(0).withMinuteOfHour(0);
DateTime nextHour = hour.plusHours(1);
String curDay = hour.withMillisOfDay(0).toString("yyyy-MM-dd HH:mm:ss");
String nextDay = hour.withMillisOfDay(0).plusDays(1).toString("yyyy-MM-dd HH:mm:ss");
System.out.println(nextHour);
System.out.println(curDay);
System.out.println(nextDay);
}
}
package com.icloud.server.payutils.job;
/**
* 任务处理器
*
* @author
*/
public interface JobHandler {
/**
* 执行任务
*
* @param param 参数
* @return 结果
* @throws Exception 异常
*/
String execute(String param) throws Exception;
}
package com.icloud.server.payutils.notify;
import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
import com.github.binarywang.wxpay.service.WxPayService;
import com.icloud.server.payutils.PayClient;
import com.icloud.server.payutils.PayClientFactory;
import com.icloud.server.payutils.dto.PayNotifyDataDTO;
import com.icloud.server.payutils.enums.PayTypeEnum;
import com.icloud.server.payutils.order.PayOrderService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.Map;
import static com.icloud.server.web.err.ServiceExceptionUtil.exception;
import static com.icloud.server.web.err.enums.ErrorCodeConstants.PAY_CHANNEL_CLIENT_NOT_FOUND;
@Api(tags = "用户 APP - 支付订单")
@RestController
@RequestMapping("/pay/order")
@Validated
@Slf4j
public class AppPayOrderController {
// @Resource
// private PayRefundService refundService;
@Resource
private PayOrderService orderService;
@Resource
private PayClientFactory payClientFactory;
@PostMapping(value = "/wxnotify/{channelId}")
@ApiOperation("渠道统一的支付成功,或退款成功 通知url")
public String notifyChannelPay(@PathVariable("channelId") Long channelId,
@RequestParam Map<String, String> params,
@RequestBody String originData) throws Exception {
System.out.println("channelId:"+channelId);
for(String value: params.values()) {
// 输出返回的值
System.out.println("====微信回传参数:"+value + ";======");
}
// 校验支付渠道是否存在
PayClient payClient = payClientFactory.getPayClient(channelId);
if (payClient == null) {
log.error("[notifyPayOrder][渠道编号({}) 找不到对应的支付客户端]", channelId);
throw exception(PAY_CHANNEL_CLIENT_NOT_FOUND);
}
// 校验通知数据是否合法
PayNotifyDataDTO notifyData = PayNotifyDataDTO.builder().params(params).body(originData).build();
payClient.verifyNotifyData(notifyData);
// 如果是退款,则发起退款通知
// if (payClient.isRefundNotify(notifyData)) {
// refundService.notifyPayRefund(channelId, PayNotifyDataDTO.builder().params(params).body(originData).build());
// return "success";
// }
// 如果非退款,则发起支付通知
orderService.notifyPayOrder(PayTypeEnum.WXPAY,channelId, PayNotifyDataDTO.builder().params(params).body(originData).build());
return "success";
}
@PostMapping(value = "/alinotify/{channelId}")
@ApiOperation("渠道统一的支付成功,或退款成功 通知url")
public String notifyChannelaliPay(@PathVariable("channelId") Long channelId,
@RequestParam Map<String, String> params,
@RequestBody String originData) throws Exception {
System.out.println("channelId:"+channelId);
for(String value: params.values()) {
// 输出返回的值
System.out.print(value + ", ");
}
// 校验支付渠道是否存在
PayClient payClient = payClientFactory.getPayClient(channelId);
if (payClient == null) {
log.error("[notifyPayOrder][渠道编号({}) 找不到对应的支付客户端]", channelId);
throw exception(PAY_CHANNEL_CLIENT_NOT_FOUND);
}
// 校验通知数据是否合法
PayNotifyDataDTO notifyData = PayNotifyDataDTO.builder().params(params).body(originData).build();
payClient.verifyNotifyData(notifyData);
// 如果是退款,则发起退款通知
// if (payClient.isRefundNotify(notifyData)) {
// refundService.notifyPayRefund(channelId, PayNotifyDataDTO.builder().params(params).body(originData).build());
// return "success";
// }
// 如果非退款,则发起支付通知
orderService.notifyPayOrder(PayTypeEnum.ALIPAY,channelId, PayNotifyDataDTO.builder().params(params).body(originData).build());
return "success";
}
}
package com.icloud.server.payutils.notify;
import com.icloud.server.payutils.job.JobHandler;
import javax.annotation.Resource;
/**
* 支付通知 Job
* 通过不断扫描待通知的 PayNotifyTaskDO 记录,回调业务线的回调接口
*
* @author
*/
//@Component
//@Slf4j
public class PayNotifyJob implements JobHandler {
@Resource
private PayNotifyService payNotifyCoreService;
@Override
public String execute(String param) throws Exception {
// int notifyCount = payNotifyCoreService.executeNotify();
return String.format("执行支付通知 %s 个", 1);
}
}
package com.icloud.server.payutils.notify;
//import org.redisson.api.RLock;
//import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Repository;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
//import static com.icloud.server.payutils.notify.RedisKeyConstants.PAY_NOTIFY_LOCK;
/**
* 支付通知的锁 Redis DAO
*
* @author
*/
@Repository
public class PayNotifyLockRedisDAO {
// @Resource
// private RedissonClient redissonClient;
//
// public void lock(Long id, Long timeoutMillis, Runnable runnable) {
// String lockKey = formatKey(id);
// RLock lock = redissonClient.getLock(lockKey);
// try {
// lock.lock(timeoutMillis, TimeUnit.MILLISECONDS);
// // 执行逻辑
// runnable.run();
// } finally {
// lock.unlock();
// }
// }
// private static String formatKey(Long id) {
// return String.format(PAY_NOTIFY_LOCK.getKeyTemplate(), id);
// }
}
package com.icloud.server.payutils.notify;
import javax.validation.Valid;
/**
* 支付通知 Service 接口
*
* @author
*/
public interface PayNotifyService {
/**
* 创建支付通知任务
*
* @param reqDTO 任务信息
*/
void createPayNotifyTask(@Valid PayNotifyTaskCreateReqDTO reqDTO);
/**
* 执行支付通知
*
* 注意,该方法提供给定时任务调用。目前是 yudao-server 进行调用
* @return 通知数量
*/
// int executeNotify() throws InterruptedException;
}
package com.icloud.server.payutils.notify;
import com.icloud.server.payutils.dto.PayNotifyTaskDO;
import com.icloud.server.payutils.order.PayOrderService;
import com.icloud.server.utils.time.DateUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.util.Date;
import java.util.Objects;
/**
* 支付通知 Core Service 实现类
*
* @author
*/
@Service
@Valid
@Slf4j
public class PayNotifyServiceImpl implements PayNotifyService {
/**
* 通知超时时间,单位:秒
*/
public static final int NOTIFY_TIMEOUT = 120;
/**
* {@link #NOTIFY_TIMEOUT} 的毫秒
*/
public static final long NOTIFY_TIMEOUT_MILLIS = 120 * DateUtils.SECOND_MILLIS;
@Resource
@Lazy // 循环依赖,避免报错
private PayOrderService orderService;
// @Resource
// @Lazy // 循环依赖,避免报错
// private PayRefundService refundService;
@Resource
private ThreadPoolTaskExecutor threadPoolTaskExecutor; // TODO :未来提供独立的线程池
@Resource
private PayNotifyLockRedisDAO payNotifyLockCoreRedisDAO;
@Override
public void createPayNotifyTask(PayNotifyTaskCreateReqDTO reqDTO) {
PayNotifyTaskDO task = new PayNotifyTaskDO();
task.setType(reqDTO.getType()).setDataId(reqDTO.getDataId());
task.setStatus(PayNotifyStatusEnum.WAITING.getStatus()).setNextNotifyTime(new Date())
.setNotifyTimes(0).setMaxNotifyTimes(PayNotifyTaskDO.NOTIFY_FREQUENCY.length + 1);
// 补充 merchantId + appId + notifyUrl 字段
if (Objects.equals(task.getType(), PayNotifyTypeEnum.ORDER.getType())) {
//保存支付单通知
} else if (Objects.equals(task.getType(), PayNotifyTypeEnum.REFUND.getType())) {
//保存退款单通知
}
// 保存订单
}
}
package com.icloud.server.payutils.notify;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 支付通知状态枚举
*
* @author
*/
@Getter
@AllArgsConstructor
public enum PayNotifyStatusEnum {
WAITING(1, "等待通知"),
SUCCESS(2, "通知成功"),
FAILURE(3, "通知失败"), // 多次尝试,彻底失败
REQUEST_SUCCESS(4, "请求成功,但是结果失败"),
REQUEST_FAILURE(5, "请求失败"),
;
/**
* 状态
*/
private final Integer status;
/**
* 名字
*/
private final String name;
}
package com.icloud.server.payutils.notify;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotNull;
/**
* 支付通知创建 DTO
*
* @author
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PayNotifyTaskCreateReqDTO {
/**
* 类型
*/
@NotNull(message = "类型不能为空")
private Integer type;
/**
* 数据编号
*/
@NotNull(message = "数据编号不能为空")
private Long dataId;
}
package com.icloud.server.payutils.notify;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 支付通知类型
*
* @author
*/
@Getter
@AllArgsConstructor
public enum PayNotifyTypeEnum {
ORDER(1, "支付单"),
REFUND(2, "退款单"),
;
/**
* 类型
*/
private final Integer type;
/**
* 名字
*/
private final String name;
}
package com.icloud.server.payutils.notify;
//import org.redisson.api.RLock;
/**
* 支付 Redis Key 枚举类
*
* @author
*/
public interface RedisKeyConstants {
// RedisKeyDefine PAY_NOTIFY_LOCK = new RedisKeyDefine("通知任务的分布式锁",
// "pay_notify:lock:", // 参数来自 DefaultLockKeyBuilder 类
// RedisKeyDefine.KeyTypeEnum.HASH, RLock.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC); // Redisson 的 Lock 锁,使用 Hash 数据结构
//
}
package com.icloud.server.payutils.notify;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import java.time.Duration;
/**
* Redis Key 定义类
*
* @author
*/
@Data
public class RedisKeyDefine {
@Getter
@AllArgsConstructor
public enum KeyTypeEnum {
STRING("String"),
LIST("List"),
HASH("Hash"),
SET("Set"),
ZSET("Sorted Set"),
STREAM("Stream"),
PUBSUB("Pub/Sub");
/**
* 类型
*/
@JsonValue
private final String type;
}
@Getter
@AllArgsConstructor
public enum TimeoutTypeEnum {
FOREVER(1), // 永不超时
DYNAMIC(2), // 动态超时
FIXED(3); // 固定超时
/**
* 类型
*/
@JsonValue
private final Integer type;
}
/**
* Key 模板
*/
private final String keyTemplate;
/**
* Key 类型的枚举
*/
private final KeyTypeEnum keyType;
/**
* Value 类型
*
* 如果是使用分布式锁,设置为 {@link java.util.concurrent.locks.Lock} 类型
*/
private final Class<?> valueType;
/**
* 超时类型
*/
private final TimeoutTypeEnum timeoutType;
/**
* 过期时间
*/
private final Duration timeout;
/**
* 备注
*/
private final String memo;
private RedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class<?> valueType,
TimeoutTypeEnum timeoutType, Duration timeout) {
this.memo = memo;
this.keyTemplate = keyTemplate;
this.keyType = keyType;
this.valueType = valueType;
this.timeout = timeout;
this.timeoutType = timeoutType;
// 添加注册表
RedisKeyRegistry.add(this);
}
public RedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class<?> valueType, Duration timeout) {
this(memo, keyTemplate, keyType, valueType, TimeoutTypeEnum.FIXED, timeout);
}
public RedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class<?> valueType, TimeoutTypeEnum timeoutType) {
this(memo, keyTemplate, keyType, valueType, timeoutType, Duration.ZERO);
}
/**
* 格式化 Key
*
* 注意,内部采用 {@link String#format(String, Object...)} 实现
*
* @param args 格式化的参数
* @return Key
*/
public String formatKey(Object... args) {
return String.format(keyTemplate, args);
}
}
package com.icloud.server.payutils.notify;
import java.util.ArrayList;
import java.util.List;
/**
* {@link RedisKeyDefine} 注册表
*/
public class RedisKeyRegistry {
/**
* Redis RedisKeyDefine 数组
*/
private static final List<RedisKeyDefine> defines = new ArrayList<>();
public static void add(RedisKeyDefine define) {
defines.add(define);
}
public static List<RedisKeyDefine> list() {
return defines;
}
public static int size() {
return defines.size();
}
}
//package com.icloud.server.payutils.order;
//
//import cn.hutool.core.collection.CollectionUtil;
//import cn.hutool.core.util.ObjectUtil;
//
//import io.swagger.annotations.Api;
//import io.swagger.annotations.ApiImplicitParam;
//import io.swagger.annotations.ApiOperation;
//
//import org.springframework.validation.annotation.Validated;
//import org.springframework.web.bind.annotation.GetMapping;
//import org.springframework.web.bind.annotation.RequestMapping;
//import org.springframework.web.bind.annotation.RequestParam;
//import org.springframework.web.bind.annotation.RestController;
//
//import javax.annotation.Resource;
//import javax.servlet.http.HttpServletResponse;
//import javax.validation.Valid;
//import java.io.IOException;
//import java.util.ArrayList;
//
//
//@Api(tags = "管理后台 - 支付订单")
//@RestController
//@RequestMapping("/pay/order")
//@Validated
//public class PayOrderController {
//
// @Resource
// private PayOrderService orderService;
// @Resource
// private PayOrderExtensionService orderExtensionService;
// @Resource
// private PayMerchantService merchantService;
// @Resource
// private PayAppService appService;
//
// @GetMapping("/get")
// @ApiOperation("获得支付订单")
// @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class)
// public CommonResult<PayOrderDetailsRespVO> getOrder(@RequestParam("id") Long id) {
// PayOrderDO order = orderService.getOrder(id);
// if (ObjectUtil.isNull(order)) {
// return success(new PayOrderDetailsRespVO());
// }
//
// PayMerchantDO merchantDO = merchantService.getMerchant(order.getMerchantId());
// PayAppDO appDO = appService.getApp(order.getAppId());
// PayChannelEnum channelEnum = PayChannelEnum.getByCode(order.getChannelCode());
//
// // TODO @aquan:文案,都是前端 format;
// PayOrderDetailsRespVO respVO = PayOrderConvert.INSTANCE.orderDetailConvert(order);
// respVO.setMerchantName(ObjectUtil.isNotNull(merchantDO) ? merchantDO.getName() : "未知商户");
// respVO.setAppName(ObjectUtil.isNotNull(appDO) ? appDO.getName() : "未知应用");
// respVO.setChannelCodeName(ObjectUtil.isNotNull(channelEnum) ? channelEnum.getName() : "未知渠道");
//
// PayOrderExtensionDO extensionDO = orderExtensionService.getOrderExtension(order.getSuccessExtensionId());
// if (ObjectUtil.isNotNull(extensionDO)) {
// respVO.setPayOrderExtension(PayOrderConvert.INSTANCE.orderDetailExtensionConvert(extensionDO));
// }
//
// return success(respVO);
// }
//
// @GetMapping("/page")
// @ApiOperation("获得支付订单分页")
// @PreAuthorize("@ss.hasPermission('pay:order:query')")
// public CommonResult<PageResult<PayOrderPageItemRespVO>> getOrderPage(@Valid PayOrderPageReqVO pageVO) {
// PageResult<PayOrderDO> pageResult = orderService.getOrderPage(pageVO);
// if (CollectionUtil.isEmpty(pageResult.getList())) {
// return success(new PageResult<>(pageResult.getTotal()));
// }
//
// // 处理商户ID数据
// Map<Long, PayMerchantDO> merchantMap = merchantService.getMerchantMap(
// CollectionUtils.convertList(pageResult.getList(), PayOrderDO::getMerchantId));
// // 处理应用ID数据
// Map<Long, PayAppDO> appMap = appService.getAppMap(
// CollectionUtils.convertList(pageResult.getList(), PayOrderDO::getAppId));
//
// List<PayOrderPageItemRespVO> pageList = new ArrayList<>(pageResult.getList().size());
// pageResult.getList().forEach(c -> {
// PayMerchantDO merchantDO = merchantMap.get(c.getMerchantId());
// PayAppDO appDO = appMap.get(c.getAppId());
// PayChannelEnum channelEnum = PayChannelEnum.getByCode(c.getChannelCode());
//
// PayOrderPageItemRespVO orderItem = PayOrderConvert.INSTANCE.pageConvertItemPage(c);
// orderItem.setMerchantName(ObjectUtil.isNotNull(merchantDO) ? merchantDO.getName() : "未知商户");
// orderItem.setAppName(ObjectUtil.isNotNull(appDO) ? appDO.getName() : "未知应用");
// orderItem.setChannelCodeName(ObjectUtil.isNotNull(channelEnum) ? channelEnum.getName() : "未知渠道");
// pageList.add(orderItem);
// });
// return success(new PageResult<>(pageList, pageResult.getTotal()));
// }
//
// @GetMapping("/export-excel")
// @ApiOperation("导出支付订单Excel")
// @PreAuthorize("@ss.hasPermission('pay:order:export')")
// @OperateLog(type = EXPORT)
// public void exportOrderExcel(@Valid PayOrderExportReqVO exportReqVO,
// HttpServletResponse response) throws IOException {
//
// List<PayOrderDO> list = orderService.getOrderList(exportReqVO);
// if (CollectionUtil.isEmpty(list)) {
// ExcelUtils.write(response, "支付订单.xls", "数据",
// PayOrderExcelVO.class, new ArrayList<>());
// }
//
// // 处理商户ID数据
// Map<Long, PayMerchantDO> merchantMap = merchantService.getMerchantMap(
// CollectionUtils.convertList(list, PayOrderDO::getMerchantId));
// // 处理应用ID数据
// Map<Long, PayAppDO> appMap = appService.getAppMap(
// CollectionUtils.convertList(list, PayOrderDO::getAppId));
// // 处理扩展订单数据
// Map<Long, PayOrderExtensionDO> orderExtensionMap = orderExtensionService
// .getOrderExtensionMap(CollectionUtils.convertList(list, PayOrderDO::getSuccessExtensionId));
//
// List<PayOrderExcelVO> excelDatum = new ArrayList<>(list.size());
// list.forEach(c -> {
// PayMerchantDO merchantDO = merchantMap.get(c.getMerchantId());
// PayAppDO appDO = appMap.get(c.getAppId());
// PayChannelEnum channelEnum = PayChannelEnum.getByCode(c.getChannelCode());
// PayOrderExtensionDO orderExtensionDO = orderExtensionMap.get(c.getSuccessExtensionId());
//
// PayOrderExcelVO excelItem = PayOrderConvert.INSTANCE.excelConvert(c);
// excelItem.setMerchantName(ObjectUtil.isNotNull(merchantDO) ? merchantDO.getName() : "未知商户");
// excelItem.setAppName(ObjectUtil.isNotNull(appDO) ? appDO.getName() : "未知应用");
// excelItem.setChannelCodeName(ObjectUtil.isNotNull(channelEnum) ? channelEnum.getName() : "未知渠道");
// excelItem.setNo(ObjectUtil.isNotNull(orderExtensionDO) ? orderExtensionDO.getNo() : "");
// excelDatum.add(excelItem);
// });
//
// // 导出 Excel
// ExcelUtils.write(response, "支付订单.xls", "数据", PayOrderExcelVO.class, excelDatum);
// }
//
//}
//package com.icloud.server.payutils.order;
//
//import lombok.Data;
//
//import java.util.Date;
//
//@Data
//public class PayOrderExcelVO {
//
// @ExcelProperty("支付订单编号")
// private Long id;
//
// @ExcelProperty(value = "商户名称")
// private String merchantName;
//
// @ExcelProperty(value = "应用名称")
// private String appName;
//
// @ExcelProperty("商品标题")
// private String subject;
//
// @ExcelProperty("商户订单编号")
// private String merchantOrderId;
//
// @ExcelProperty("渠道订单号")
// private String channelOrderNo;
//
// @ExcelProperty(value = "支付订单号")
// private String no;
//
// @ExcelProperty("支付金额,单位:元")
// private String amount;
//
// @ExcelProperty("渠道手续金额,单位:元")
// private String channelFeeAmount;
//
// @ExcelProperty("渠道手续费,单位:百分比")
// private String channelFeeRate;
//
// @DictFormat(DictTypeConstants.ORDER_STATUS)
// @ExcelProperty(value = "支付状态", converter = DictConvert.class)
// private Integer status;
//
// @DictFormat(DictTypeConstants.ORDER_NOTIFY_STATUS)
// @ExcelProperty(value = "通知商户支付结果的回调状态", converter = DictConvert.class)
// private Integer notifyStatus;
//
// @ExcelProperty("异步通知地址")
// private String notifyUrl;
//
// @ExcelProperty("创建时间")
// private Date createTime;
//
// @ExcelProperty("订单支付成功时间")
// private Date successTime;
//
// @ExcelProperty("订单失效时间")
// private Date expireTime;
//
// @ExcelProperty("订单支付通知时间")
// private Date notifyTime;
//
// @ExcelProperty(value = "渠道编号名称")
// private String channelCodeName;
//
// @ExcelProperty("用户 IP")
// private String userIp;
//
// @DictFormat(DictTypeConstants.ORDER_REFUND_STATUS)
// @ExcelProperty(value = "退款状态", converter = DictConvert.class)
// private Integer refundStatus;
//
// @ExcelProperty("退款次数")
// private Integer refundTimes;
//
// @ExcelProperty("退款总金额,单位:元")
// private String refundAmount;
//
// @ExcelProperty("商品描述")
// private String body;
//}
package com.icloud.server.payutils.order;
import com.icloud.server.payutils.dto.PayNotifyDataDTO;
import com.icloud.server.payutils.enums.PayTypeEnum;
/**
* 支付订单 Service 接口
*
* @author
*/
public interface PayOrderService {
//
// /**
// * 获得支付订单
// *
// * @param id 编号
// * @return 支付订单
// */
// PayOrderDO getOrder(Long id);
//
// /**
// * 获得支付订单
// * 分页
// *
// * @param pageReqVO 分页查询
// * @return 支付订单
// * 分页
// */
// PageResult<PayOrderDO> getOrderPage(PayOrderPageReqVO pageReqVO);
//
// /**
// * 获得支付订单
// * 列表, 用于 Excel 导出
// *
// * @param exportReqVO 查询条件
// * @return 支付订单
// * 列表
// */
// List<PayOrderDO> getOrderList(PayOrderExportReqVO exportReqVO);
//
// /**
// * 根据 ID 集合获取只包含商品名称的订单集合
// *
// * @param idList 订单 ID 集合
// * @return 只包含商品名称的订单集合
// */
// List<PayOrderDO> getOrderSubjectList(Collection<Long> idList);
//
// /**
// * 根据订单 ID 集合获取订单商品名称Map集合
// *
// * @param idList 订单 ID 集合
// * @return 订单商品 map 集合
// */
// default Map<Long, PayOrderDO> getOrderSubjectMap(Collection<Long> idList) {
// List<PayOrderDO> list = getOrderSubjectList(idList);
// return CollectionUtils.convertMap(list, PayOrderDO::getId);
// }
//
// /**
// * 创建支付单
// *
// * @param reqDTO 创建请求
// * @return 支付单编号
// */
// Long createPayOrder(@Valid PayOrderCreateReqDTO reqDTO);
//
// /**
// * 提交支付
// * 此时,会发起支付渠道的调用
// *
// * @param reqDTO 提交请求
// * @return 提交结果
// */
// PayOrderSubmitRespDTO submitPayOrder(@Valid PayOrderSubmitReqDTO reqDTO);
/**
* 通知支付单成功
*
* @param channelId 渠道编号
* @param notifyData 通知数据
*/
void notifyPayOrder(PayTypeEnum paytype, Long channelId, PayNotifyDataDTO notifyData) throws Exception;
}
package com.icloud.server.payutils.order;
import com.icloud.server.payutils.PayClient;
import com.icloud.server.payutils.PayClientFactory;
import com.icloud.server.payutils.dto.PayNotifyDataDTO;
import com.icloud.server.payutils.dto.PayOrderDO;
import com.icloud.server.payutils.dto.PayOrderNotifyRespDTO;
import com.icloud.server.payutils.enums.PayTypeEnum;
import com.icloud.server.scala.payorder.PayOrderRepository;
import com.icloud.server.web.err.ServiceExceptionUtil;
import com.icloud.server.web.err.enums.ErrorCodeConstants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
@Service
@Validated
@Slf4j
public class PayOrderServiceImpl implements PayOrderService {
@Resource
@Lazy // 循环依赖,避免报错
private PayOrderRepository payorderrepository;
@Resource
private PayClientFactory payClientFactory;
@Override
public void notifyPayOrder(PayTypeEnum paytype,Long channelId, PayNotifyDataDTO notifyData) throws Exception {
log.info("[notifyPayOrder][channelId({}) 回调数据({})]", channelId, notifyData.getBody());
System.out.println("--------------");
System.out.println(channelId);
System.out.println("--------");
// 校验支付渠道是否有效
PayOrderDO channel = payorderrepository.findByChannelid(channelId);
// PayOrderDO channel = new PayOrderDO();
// if (paytype == PayTypeEnum.WXPAY){
//
// }
// 校验支付客户端是否正确初始化
PayClient client = payClientFactory.getPayClient(channel.getId());
if(client==null){
throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_ORDER_EXTENSION_NOT_FOUND);
}
if (client == null) {
log.error("[notifyPayOrder][渠道编号({}) 找不到对应的支付客户端]", channel.getId());
throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_CHANNEL_CLIENT_NOT_FOUND);
}
// 解析支付结果
PayOrderNotifyRespDTO notifyRespDTO = client.parseOrderNotify(notifyData);
//保存订单 ---已经完成支付
if (channel.getStatus()==1){
log.info("[notifyPayOrder][支付拓展单({}) 更新为已支付]", channel.getId());
}else {
log.info("[notifyPayOrder][支付订单({}) 更新为已支付]", channel.getId());
System.out.println("getChannelUserId"+notifyRespDTO.getChannelUserId());
System.out.println("getChannelOrderNo"+notifyRespDTO.getChannelOrderNo());
System.out.println("getSuccessTime"+notifyRespDTO.getSuccessTime());
channel.setStatus(1);
payorderrepository.save(channel);
}
}
}
package com.icloud.server.payutils.pojo;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.icloud.server.web.err.ErrorCode;
import com.icloud.server.web.err.GlobalErrorCodeConstants;
import com.icloud.server.web.err.ServerException;
import com.icloud.server.web.err.ServiceException;
import lombok.Data;
import org.springframework.util.Assert;
import java.io.Serializable;
import java.util.Objects;
/**
* 通用返回
*
* @param <T> 数据泛型
*/
@Data
public class CommonResult<T> implements Serializable {
/**
* 错误码
*
* @see ErrorCode#getCode()
*/
private Integer code;
/**
* 返回数据
*/
private T data;
/**
* 错误提示,用户可阅读
*
* @see ErrorCode#getMsg() ()
*/
private String msg;
/**
* 将传入的 result 对象,转换成另外一个泛型结果的对象
*
* 因为 A 方法返回的 CommonResult 对象,不满足调用其的 B 方法的返回,所以需要进行转换。
*
* @param result 传入的 result 对象
* @param <T> 返回的泛型
* @return 新的 CommonResult 对象
*/
public static <T> CommonResult<T> error(CommonResult<?> result) {
return error(result.getCode(), result.getMsg());
}
public static <T> CommonResult<T> error(Integer code, String message) {
Assert.isTrue(!GlobalErrorCodeConstants.SUCCESS.getCode().equals(code), "code 必须是错误的!");
CommonResult<T> result = new CommonResult<>();
result.code = code;
result.msg = message;
return result;
}
public static <T> CommonResult<T> error(ErrorCode errorCode) {
return error(errorCode.getCode(), errorCode.getMsg());
}
public static <T> CommonResult<T> success(T data) {
CommonResult<T> result = new CommonResult<>();
result.code = GlobalErrorCodeConstants.SUCCESS.getCode();
result.data = data;
result.msg = "";
return result;
}
public static boolean isSuccess(Integer code) {
return Objects.equals(code, GlobalErrorCodeConstants.SUCCESS.getCode());
}
@JsonIgnore // 避免 jackson 序列化
public boolean isSuccess() {
return isSuccess(code);
}
@JsonIgnore // 避免 jackson 序列化
public boolean isError() {
return !isSuccess();
}
// ========= 和 Exception 异常体系集成 =========
/**
* 判断是否有异常。如果有,则抛出 {@link ServiceException} 异常
*/
public void checkError() throws ServiceException {
if (isSuccess()) {
return;
}
// 服务端异常
if (GlobalErrorCodeConstants.isServerErrorCode(code)) {
throw new ServerException(code, msg);
}
// 业务异常
throw new ServiceException(code, msg);
}
/**
* 判断是否有异常。如果有,则抛出 {@link ServiceException} 异常
* 如果没有,则返回 {@link #data} 数据
*/
@JsonIgnore // 避免 jackson 序列化
public T getCheckedData() {
checkError();
return data;
}
public static <T> CommonResult<T> error(ServiceException serviceException) {
return error(serviceException.getCode(), serviceException.getMessage());
}
}
package com.icloud.server.payutils.pojo;
import lombok.Data;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
@Data
public class PageParam implements Serializable {
private static final Integer PAGE_NO = 1;
private static final Integer PAGE_SIZE = 10;
@NotNull(message = "页码不能为空")
@Min(value = 1, message = "页码最小值为 1")
private Integer pageNo = PAGE_NO;
@NotNull(message = "每页条数不能为空")
@Min(value = 1, message = "页码最小值为 1")
@Max(value = 100, message = "页码最大值为 100")
private Integer pageSize = PAGE_SIZE;
}
package com.icloud.server.payutils.wx;
import cn.hutool.core.util.StrUtil;
import com.icloud.server.payutils.AbstractPayCodeMapping;
import com.icloud.server.web.err.ErrorCode;
import com.icloud.server.web.err.GlobalErrorCodeConstants;
import java.util.Objects;
import static com.icloud.server.web.err.pay.PayFrameworkErrorCodeConstants.*;
/**
* 微信支付 PayCodeMapping 实现类
*
* @author
*/
public class WXCodeMapping extends AbstractPayCodeMapping {
/**
* 错误码 - 成功
* 由于 weixin-java-pay 封装的 Result 未返回 code,所以自己定义下
*/
public static final String CODE_SUCCESS = "SUCCESS";
/**
* 错误提示 - 成功
*/
public static final String MESSAGE_SUCCESS = "成功";
@Override
protected ErrorCode apply0(String apiCode, String apiMsg) {
if (Objects.equals(apiCode, CODE_SUCCESS)) {
return GlobalErrorCodeConstants.SUCCESS;
}
if (Objects.equals(apiCode, "FAIL")) {
if (Objects.equals(apiMsg, "AppID不存在,请检查后再试")) {
return PAY_CONFIG_APP_ID_ERROR;
}
if (Objects.equals(apiMsg, "签名错误,请检查后再试")
|| Objects.equals(apiMsg, "签名错误")) {
return PAY_CONFIG_SIGN_ERROR;
}
}
if (Objects.equals(apiCode, "PARAM_ERROR")) {
if (Objects.equals(apiMsg, "无效的openid")) {
return PAY_OPENID_ERROR;
}
}
if (Objects.equals(apiCode, "CustomErrorCode")) {
if (StrUtil.contains(apiMsg, "必填字段")) {
return PAY_PARAM_MISSING;
}
}
return null;
}
}
package com.icloud.server.payutils.wx;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyV3Result;
import com.github.binarywang.wxpay.bean.order.WxPayMpOrderResult;
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request;
import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderV3Result;
import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
import com.github.binarywang.wxpay.config.WxPayConfig;
import com.github.binarywang.wxpay.constant.WxPayConstants;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.service.WxPayService;
import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
import com.icloud.server.payutils.AbstractPayClient;
import com.icloud.server.payutils.PayCommonResult;
import com.icloud.server.payutils.dto.*;
import com.icloud.server.utils.io.FileUtils;
import com.icloud.server.utils.object.ObjectUtils;
import com.icloud.server.web.err.pay.PayChannelEnum;
import lombok.extern.slf4j.Slf4j;
import java.util.Objects;
import static com.icloud.server.payutils.wx.WXCodeMapping.CODE_SUCCESS;
import static com.icloud.server.payutils.wx.WXCodeMapping.MESSAGE_SUCCESS;
import static com.icloud.server.utils.JsonUtils.toJsonString;
/**
* 微信小程序下支付
*
* @author zwy
*/
@Slf4j
public class WXLitePayClient extends AbstractPayClient<WXPayClientConfig> {
private WxPayService client;
public WXLitePayClient(Long channelId, WXPayClientConfig config) {
super(channelId, PayChannelEnum.WX_LITE.getCode(), config, new WXCodeMapping());
}
@Override
protected void doInit() {
WxPayConfig payConfig = new WxPayConfig();
BeanUtil.copyProperties(config, payConfig, "keyContent");
payConfig.setTradeType(WxPayConstants.TradeType.JSAPI); // 设置使用 JS API 支付方式
// if (StrUtil.isNotEmpty(config.getKeyContent())) {
// payConfig.setKeyContent(config.getKeyContent().getBytes(StandardCharsets.UTF_8));
// }
if (StrUtil.isNotEmpty(config.getPrivateKeyContent())) {
// weixin-pay-java 存在 BUG,无法直接设置内容,所以创建临时文件来解决
payConfig.setPrivateKeyPath(FileUtils.createTempFile(config.getPrivateKeyContent()).getPath());
}
if (StrUtil.isNotEmpty(config.getPrivateCertContent())) {
// weixin-pay-java 存在 BUG,无法直接设置内容,所以创建临时文件来解决
payConfig.setPrivateCertPath(FileUtils.createTempFile(config.getPrivateCertContent()).getPath());
}
// 真实客户端
this.client = new WxPayServiceImpl();
client.setConfig(payConfig);
}
@Override
public PayCommonResult<WxPayMpOrderResult> doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
WxPayMpOrderResult response;
try {
switch (config.getApiVersion()) {
case WXPayClientConfig.API_VERSION_V2:
response = this.unifiedOrderV2(reqDTO);
break;
case WXPayClientConfig.API_VERSION_V3:
WxPayUnifiedOrderV3Result.JsapiResult responseV3 = this.unifiedOrderV3(reqDTO);
// 将 V3 的结果,统一转换成 V2。返回的字段是一致的
response = new WxPayMpOrderResult();
BeanUtil.copyProperties(responseV3, response, true);
break;
default:
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
}
} catch (WxPayException e) {
log.error("[unifiedOrder][request({}) 发起支付失败,原因({})]", toJsonString(reqDTO), e);
return PayCommonResult.build(ObjectUtils.defaultIfNull(e.getErrCode(), e.getReturnCode(), "CustomErrorCode"),
ObjectUtils.defaultIfNull(e.getErrCodeDes(), e.getCustomErrorMsg()), null, codeMapping);
}
return PayCommonResult.build(CODE_SUCCESS, MESSAGE_SUCCESS, response, codeMapping);
}
private WxPayMpOrderResult unifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
// 构建 WxPayUnifiedOrderRequest 对象
WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest.newBuilder()
.outTradeNo(reqDTO.getMerchantOrderId())
.attach(reqDTO.getChannelExtras().get("attach"))
.body(reqDTO.getBody())
.totalFee(reqDTO.getAmount().intValue()) // 单位分
.timeExpire(DateUtil.format(reqDTO.getExpireTime(), "yyyyMMddHHmmss")) // v2的时间格式
.spbillCreateIp(reqDTO.getUserIp())
.openid(getOpenid(reqDTO))
.notifyUrl(reqDTO.getNotifyUrl())
.build();
// 执行请求
return client.createOrder(request);
}
private WxPayUnifiedOrderV3Result.JsapiResult unifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
// 构建 WxPayUnifiedOrderRequest 对象
WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request();
request.setOutTradeNo(reqDTO.getMerchantOrderId());
request.setDescription(reqDTO.getBody());
request.setAmount(new WxPayUnifiedOrderV3Request
.Amount()
.setTotal(reqDTO
.getAmount()
.intValue())); // 单位分
request.setTimeExpire(DateUtil.format(reqDTO.getExpireTime(), "yyyy-MM-dd'T'HH:mm:ssXXX")); // v3的时间格式
request.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(getOpenid(reqDTO)));
request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp()));
request.setNotifyUrl(reqDTO.getNotifyUrl());
// 执行请求
return client.createOrderV3(TradeTypeEnum.JSAPI, request);
}
private static String getOpenid(PayOrderUnifiedReqDTO reqDTO) {
String openid = MapUtil.getStr(reqDTO.getChannelExtras(), "openid");
if (StrUtil.isEmpty(openid)) {
throw new IllegalArgumentException("支付请求的 openid 不能为空!");
}
return openid;
}
/**
*
* 微信支付回调 分 v2 和v3 的处理方式
*
* @param data 通知结果
* @return 支付回调对象
* @throws WxPayException 微信异常类
*/
@Override
public PayOrderNotifyRespDTO parseOrderNotify(PayNotifyDataDTO data) throws WxPayException {
log.info("[parseOrderNotify][微信支付回调data数据:{}]", data.getBody());
// 微信支付 v2 回调结果处理
switch (config.getApiVersion()) {
case WXPayClientConfig.API_VERSION_V2:
return parseOrderNotifyV2(data);
case WXPayClientConfig.API_VERSION_V3:
return parseOrderNotifyV3(data);
default:
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
}
}
private PayOrderNotifyRespDTO parseOrderNotifyV3(PayNotifyDataDTO data) throws WxPayException {
WxPayOrderNotifyV3Result wxPayOrderNotifyV3Result = client.parseOrderNotifyV3Result(data.getBody(), null);
WxPayOrderNotifyV3Result.DecryptNotifyResult result = wxPayOrderNotifyV3Result.getResult();
// 转换结果
Assert.isTrue(Objects.equals(wxPayOrderNotifyV3Result.getResult().getTradeState(), "SUCCESS"),
"支付结果非 SUCCESS");
return PayOrderNotifyRespDTO
.builder()
.orderExtensionNo(result.getOutTradeNo())
.channelOrderNo(result.getTradeState())
.successTime(DateUtil.parse(result.getSuccessTime(), "yyyy-MM-dd'T'HH:mm:ssXXX"))
.data(data.getBody())
.build();
}
private PayOrderNotifyRespDTO parseOrderNotifyV2(PayNotifyDataDTO data) throws WxPayException {
WxPayOrderNotifyResult notifyResult = client.parseOrderNotifyResult(data.getBody());
Assert.isTrue(Objects.equals(notifyResult.getResultCode(), "SUCCESS"), "支付结果非 SUCCESS");
// 转换结果
return PayOrderNotifyRespDTO
.builder()
.orderExtensionNo(notifyResult.getOutTradeNo())
.channelOrderNo(notifyResult.getTransactionId())
.channelUserId(notifyResult.getOpenid())
.successTime(DateUtil.parse(notifyResult.getTimeEnd(), "yyyyMMddHHmmss"))
.data(data.getBody())
.build();
}
@Override
public PayRefundNotifyDTO parseRefundNotify(PayNotifyDataDTO notifyData) {
//TODO 需要实现
throw new UnsupportedOperationException("需要实现");
}
@Override
protected PayCommonResult<PayRefundUnifiedRespDTO> doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
//TODO 需要实现
throw new UnsupportedOperationException();
}
}
package com.icloud.server.payutils.wx;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyV3Result;
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.config.WxPayConfig;
import com.github.binarywang.wxpay.constant.WxPayConstants;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.service.WxPayService;
import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
import com.icloud.server.payutils.AbstractPayClient;
import com.icloud.server.payutils.PayCommonResult;
import com.icloud.server.payutils.dto.*;
import com.icloud.server.utils.io.FileUtils;
import com.icloud.server.utils.object.ObjectUtils;
import com.icloud.server.web.err.pay.PayChannelEnum;
import lombok.extern.slf4j.Slf4j;
import java.util.Objects;
import static com.icloud.server.payutils.wx.WXCodeMapping.CODE_SUCCESS;
import static com.icloud.server.payutils.wx.WXCodeMapping.MESSAGE_SUCCESS;
import static com.icloud.server.utils.JsonUtils.toJsonString;
/**
* 微信 App 支付
*
* @author zwy
*/
@Slf4j
public class WXNativePayClient extends AbstractPayClient<WXPayClientConfig> {
private WxPayService client;
public WXNativePayClient(Long channelId, WXPayClientConfig config) {
super(channelId, PayChannelEnum.WX_NATIVE.getCode(), config, new WXCodeMapping());
}
@Override
protected void doInit() {
WxPayConfig payConfig = new WxPayConfig();
BeanUtil.copyProperties(config, payConfig, "keyContent");
payConfig.setTradeType(WxPayConstants.TradeType.NATIVE); // 设置使用 native 支付方式
// if (StrUtil.isNotEmpty(config.getKeyContent())) {
// payConfig.setKeyContent(config.getKeyContent().getBytes(StandardCharsets.UTF_8));
// }
if (StrUtil.isNotEmpty(config.getPrivateKeyContent())) {
// weixin-pay-java 存在 BUG,无法直接设置内容,所以创建临时文件来解决
payConfig.setPrivateKeyPath(FileUtils.createTempFile(config.getPrivateKeyContent()).getPath());
}
if (StrUtil.isNotEmpty(config.getPrivateCertContent())) {
// weixin-pay-java 存在 BUG,无法直接设置内容,所以创建临时文件来解决
payConfig.setPrivateCertPath(FileUtils.createTempFile(config.getPrivateCertContent()).getPath());
}
// 真实客户端
this.client = new WxPayServiceImpl();
client.setConfig(payConfig);
}
@Override
public PayCommonResult<String> doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
// 这里原生的返回的是支付的 url 所以直接使用string接收
// "invokeResponse": "weixin://wxpay/bizpayurl?pr=EGYAem7zz"
String responseV3;
try {
switch (config.getApiVersion()) {
case WXPayClientConfig.API_VERSION_V2:
responseV3 = unifiedOrderV2(reqDTO).getCodeUrl();
break;
case WXPayClientConfig.API_VERSION_V3:
responseV3 = this.unifiedOrderV3(reqDTO);
break;
default:
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
}
} catch (WxPayException e) {
log.error("[unifiedOrder][request({}) 发起支付失败,原因({})]", toJsonString(reqDTO), e);
return PayCommonResult.build(ObjectUtils.defaultIfNull(e.getErrCode(), e.getReturnCode(), "CustomErrorCode"),
ObjectUtils.defaultIfNull(e.getErrCodeDes(), e.getCustomErrorMsg()), null, codeMapping);
}
return PayCommonResult.build(CODE_SUCCESS, MESSAGE_SUCCESS, responseV3, codeMapping);
}
private WxPayNativeOrderResult unifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
//前端
String tradeType = reqDTO.getChannelExtras().get("trade_type");
// 构建 WxPayUnifiedOrderRequest 对象
WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest
.newBuilder()
.outTradeNo(reqDTO.getMerchantOrderId())
.body(reqDTO.getBody())
.totalFee(reqDTO.getAmount().intValue()) // 单位分
.timeExpire(DateUtil.format(reqDTO.getExpireTime(), "yyyy-MM-dd'T'HH:mm:ssXXX"))
.spbillCreateIp(reqDTO.getUserIp())
.notifyUrl(reqDTO.getNotifyUrl())
.productId(tradeType)
.build();
// 执行请求
return client.createOrder(request);
}
private String unifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
// 构建 WxPayUnifiedOrderRequest 对象
WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request();
request.setOutTradeNo(reqDTO.getMerchantOrderId());
request.setDescription(reqDTO.getBody());
request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getAmount().intValue())); // 单位分
request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp()));
request.setNotifyUrl(reqDTO.getNotifyUrl());
// 执行请求
return client.createOrderV3(TradeTypeEnum.NATIVE, request);
}
/**
*
* 微信支付回调 分v2 和v3 的处理方式
*
* @param data 通知结果
* @return 支付回调对象
* @throws WxPayException 微信异常类
*/
@Override
public PayOrderNotifyRespDTO parseOrderNotify(PayNotifyDataDTO data) throws WxPayException {
log.info("微信支付回调data数据:{}", data.getBody());
// 微信支付 v2 回调结果处理
switch (config.getApiVersion()) {
case WXPayClientConfig.API_VERSION_V2:
return parseOrderNotifyV2(data);
case WXPayClientConfig.API_VERSION_V3:
return parseOrderNotifyV3(data);
default:
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
}
}
private PayOrderNotifyRespDTO parseOrderNotifyV3(PayNotifyDataDTO data) throws WxPayException {
WxPayOrderNotifyV3Result wxPayOrderNotifyV3Result = client.parseOrderNotifyV3Result(data.getBody(), null);
WxPayOrderNotifyV3Result.DecryptNotifyResult result = wxPayOrderNotifyV3Result.getResult();
// 转换结果
Assert.isTrue(Objects.equals(wxPayOrderNotifyV3Result.getResult().getTradeState(), "SUCCESS"),
"支付结果非 SUCCESS");
return PayOrderNotifyRespDTO
.builder()
.orderExtensionNo(result.getOutTradeNo())
.channelOrderNo(result.getTradeState())
.successTime(DateUtil.parse(result.getSuccessTime(), "yyyy-MM-dd'T'HH:mm:ssXXX"))
.data(data.getBody())
.build();
}
private PayOrderNotifyRespDTO parseOrderNotifyV2(PayNotifyDataDTO data) throws WxPayException {
WxPayOrderNotifyResult notifyResult = client.parseOrderNotifyResult(data.getBody());
Assert.isTrue(Objects.equals(notifyResult.getResultCode(), "SUCCESS"), "支付结果非 SUCCESS");
// 转换结果
return PayOrderNotifyRespDTO
.builder()
.orderExtensionNo(notifyResult.getOutTradeNo())
.channelOrderNo(notifyResult.getTransactionId())
.channelUserId(notifyResult.getOpenid())
.successTime(DateUtil.parse(notifyResult.getTimeEnd(), "yyyyMMddHHmmss"))
.data(data.getBody())
.build();
}
@Override
public PayRefundNotifyDTO parseRefundNotify(PayNotifyDataDTO notifyData) {
// TODO 需要实现
throw new UnsupportedOperationException("需要实现");
}
@Override
protected PayCommonResult<PayRefundUnifiedRespDTO> doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
// TODO 需要实现
throw new UnsupportedOperationException();
}
}
package com.icloud.server.payutils.wx;
import cn.hutool.core.io.IoUtil;
import com.icloud.server.payutils.PayClientConfig;
import lombok.Data;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import javax.validation.constraints.NotBlank;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.util.Set;
/**
* 微信支付的 PayClientConfig 实现类
* 属性主要来自 {@link com.github.binarywang.wxpay.config.WxPayConfig} 的必要属性
*
* @author
*/
@Data
public class WXPayClientConfig implements PayClientConfig {
/**
* API 版本 - V2
* https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_1
*/
public static final String API_VERSION_V2 = "v2";
/**
* API 版本 - V3
* https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay-1.shtml
*/
public static final String API_VERSION_V3 = "v3";
/**
* 公众号或者小程序的 appid
*/
@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 证书文件的绝对路径或者以 classpath: 开头的类路径.
* 对应的字符串
*
* 注意,可通过 {@link #main(String[])} 读取
*/
/// private String keyContent;
// ========== V3 版本的参数 ==========
/**
* apiclient_key.pem 证书文件的绝对路径或者以 classpath: 开头的类路径.
* 对应的字符串
* 注意,可通过 {@link #main(String[])} 读取
*/
@NotBlank(message = "apiclient_key 不能为空", groups = V3.class)
private String privateKeyContent;
/**
* apiclient_cert.pem 证书文件的绝对路径或者以 classpath: 开头的类路径.
* 对应的字符串
* <p>
* 注意,可通过 {@link #main(String[])} 读取
*/
@NotBlank(message = "apiclient_cert 不能为空", groups = V3.class)
private String privateCertContent;
/**
* apiV3 密钥值
*/
@NotBlank(message = "apiV3 密钥值 不能为空", groups = V3.class)
private String apiV3Key;
/**
* 分组校验 v2版本
*/
public interface V2 {
}
/**
* 分组校验 v3版本
*/
public interface V3 {
}
@Override
public Set<ConstraintViolation<PayClientConfig>> verifyParam(Validator validator) {
return validator.validate(this, this.getApiVersion().equals(API_VERSION_V2) ? V2.class : V3.class);
}
public static void main(String[] args) throws FileNotFoundException {
String path = "/Users/yunai/Downloads/wx_pay/apiclient_cert.p12";
/// String path = "/Users/yunai/Downloads/wx_pay/apiclient_key.pem";
/// String path = "/Users/yunai/Downloads/wx_pay/apiclient_cert.pem";
System.out.println(IoUtil.readUtf8(new FileInputStream(path)));
}
}
This diff is collapsed. Click to expand it.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment