提交 d396743e authored 作者: hzh's avatar hzh

微信支付模块集成

上级 b27cce0b
...@@ -271,6 +271,13 @@ ...@@ -271,6 +271,13 @@
<version>${revision}</version> <version>${revision}</version>
</dependency> </dependency>
<!-- 支付 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-pay</artifactId>
<version>${revision}</version>
</dependency>
</dependencies> </dependencies>
</dependencyManagement> </dependencyManagement>
</project> </project>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ruoyi-common-pay</artifactId>
<description>
ruoyi-common-pay 支付接口模块
</description>
<dependencies>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-core</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.22</version>
</dependency>
<dependency>
<groupId>com.github.javen205</groupId>
<artifactId>IJPay-WxPay</artifactId>
<version>2.9.9</version>
</dependency>
<dependency>
<groupId>com.github.javen205</groupId>
<artifactId>IJPay-Core</artifactId>
<version>2.9.9</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
package org.dromara.common.pay.config;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* <p>IJPay 让支付触手可及,封装了微信支付、支付宝支付、银联支付常用的支付方式以及各种常用的接口。</p>
*
* <p>不依赖任何第三方 mvc 框架,仅仅作为工具使用简单快速完成支付模块的开发,可轻松嵌入到任何系统里。 </p>
*
* <p>IJPay 交流群: 723992875、864988890</p>
*
* <p>Node.js 版: <a href="https://gitee.com/javen205/TNWX">https://gitee.com/javen205/TNWX</a></p>
*
* <p>微信配置 Bean</p>
*
* @author Javen
*/
@Getter
@Setter
@ToString
@Component
@ConfigurationProperties(prefix = "pay.wechat.v3")
public class WechatPayConfiguration {
/**
* 应用编号
*/
private String appId;
/**
* 商户号
*/
private String mchId;
/**
* 商户平台「API安全」中的 APIv3 密钥
*/
private String apiKey3;
/**
* API 证书中的 key.pem
*/
private String keyPath;
/**
* API 证书中的 cert.pem
*/
private String certPath;
/**
* 微信平台证书
*/
private String platformCertPath;
/**
* 应用域名,回调中会使用此参数
*/
private String domain;
/**
* 回调函数的接口路径
*/
private String notify;
}
package org.dromara.common.pay.domain;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
* @author hzh
* @date 2024-12-10
**/
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
@Accessors(chain = true)
public class Amount implements Serializable {
/**
* 订单总金额,单位为分。
*/
private Integer total;
/**
* 用户支付金额,单位为分。
*/
private Integer payer_total;
/**
* CNY:人民币,境内商户号仅支持人民币。
*/
private String currency;
/**
* 用户支付币种
*/
private String payer_currency;
}
package org.dromara.common.pay.domain;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
* @author hzh
* @date 2024-12-10
**/
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
@Accessors(chain = true)
public class GoodsDetail implements Serializable {
/**
* 商品编码。
*/
private String goods_id;
/**
* 用户购买的数量。
*/
private Integer quantity;
/**
* 商品单价,单位为分。
*/
private Integer unit_price;
/**
* 商品优惠金额。
*/
private Integer discount_amount;
/**
* 商品备注信息。
*/
private String goods_remark;
}
package org.dromara.common.pay.domain;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
* @author hzh
* @date 2024-12-10
* @desc jsapi 回调参数
**/
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
@Accessors(chain = true)
public class JsapiNotifyModel implements Serializable {
/**
* 直连商户申请的公众号或移动应用AppID。
*/
private String appid;
/**
* 商户的商户号,由微信支付生成并下发。
*/
private String mchid;
/**
* 商户系统内部订单号,可以是数字、大小写字母_-*的任意组合且在同一个商户号下唯一。
*/
private String out_trade_no;
/**
* 微信支付系统生成的订单号。
*/
private String transaction_id;
/**
* 交易类型,枚举值:
* JSAPI:公众号支付
* NATIVE:扫码支付
* App:App支付
* MICROPAY:付款码支付
* MWEB:H5支付
* FACEPAY:刷脸支付
*/
private String trade_type;
/**
* 交易状态,枚举值:
* SUCCESS:支付成功
* REFUND:转入退款
* NOTPAY:未支付
* CLOSED:已关闭
* REVOKED:已撤销(付款码支付)
* USERPAYING:用户支付中(付款码支付)
* PAYERROR:支付失败(其他原因,如银行返回失败)
*/
private String trade_state;
/**
* 交易状态描述。
*/
private String trade_state_desc;
/**
* 银行类型,采用字符串类型的银行标识。银行标识请参考《银行类型对照表》。
*/
private String bank_type;
/**
* 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用,实际情况下只有支付完成状态才会返回该字段。
*/
private String attach;
/**
* 支付完成时间,遵循rfc3339标准格式,格式为yyyy-MM-DDTHH:mm:ss+TIMEZONE,yyyy-MM-DD表示年月日,T出现在字符串中,表示time元素的开头,HH:mm:ss表示时分秒,TIMEZONE表示时区(+08:00表示东八区时间,领先UTC 8小时,即北京时间)。例如:2015-05-20T13:29:35+08:00表示,北京时间2015年5月20日 13点29分35秒。
*/
private String success_time;
/**
* 银行类型,采用字符串类型的银行标识。银行标识请参考《银行类型对照表》。
*/
private Payer payer;
/**
* 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用,实际情况下只有支付完成状态才会返回该字段。
*/
private Amount amount;
/**
* 支付场景信息描述。
*/
private SceneInfo scene_info;
/**
* 优惠功能,享受优惠时返回该字段
*/
private PromotionDetail promotion_detail;
}
package org.dromara.common.pay.domain;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
* @author hzh
* @date 2024-12-10
**/
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
@Accessors(chain = true)
public class Payer implements Serializable {
/**
* 用户在直连商户AppID下的唯一标识。
*/
private String openid;
}
package org.dromara.common.pay.domain;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
* @author hzh
* @date 2024-12-10
**/
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
@Accessors(chain = true)
public class PromotionDetail implements Serializable {
/**
* 券ID。
*/
private String coupon_id;
/**
* 优惠名称。
*/
private String name;
/**
* 优惠范围,枚举值:
* GLOBAL:全场代金券
* SINGLE:单品优惠。
*/
private String scope;
/**
* 优惠类型,枚举值:
* CASH:充值型代金券
* NOCASH:免充值型代金券
*/
private String type;
/**
* 优惠券面额。
*/
private Integer amount;
/**
* 活动ID。
*/
private String stock_id;
/**
* 微信出资,单位为分。
*/
private Integer wechatpay_contribute;
/**
* 商户出资,单位为分。
*/
private Integer merchant_contribute;
/**
* 其他出资,单位为分。
*/
private Integer other_contribute;
/**
* CNY:人民币,境内商户号仅支持人民币
*/
private String currency;
/**
* 单品列表信息
*/
private GoodsDetail goods_detail;
}
package org.dromara.common.pay.domain;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
* @author hzh
* @date 2024-12-10
**/
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
@Accessors(chain = true)
public class SceneInfo implements Serializable {
/**
* 终端设备号(门店号或收银设备ID)。
*/
private String device_id;
}
package org.dromara.common.pay.service;
import com.ijpay.wxpay.model.v3.UnifiedOrderModel;
import org.dromara.common.pay.domain.JsapiNotifyModel;
/**
* @author wenhe
*/
public interface IWxPayService {
/**
* 微信支付小程序支付
*
* @param model model
* @return 支付结果
* @throws Exception
*/
String jsapi(UnifiedOrderModel model) throws Exception;
/**
* 微信支付查询
* @param outTradeNo 【商户订单号】 商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一。
* @return 支付结果
*/
JsapiNotifyModel query(String outTradeNo);
/**
* 自动获取证书
*
* @param serialNumber serialNumber
* @return 是否成功
* @throws Exception 异常
*/
boolean autoUpdateOrGetCertificate(String serialNumber) throws Exception;
/**
* 支付回调
*
* @param timestamp 时间搓
* @param nonce nonce
* @param serialNo 唯一序列号
* @param signature 签名
* @param result 支付通知密文
* @return 支付通知明文
*/
JsapiNotifyModel notify(String timestamp, String nonce, String serialNo, String signature, String result);
}
package org.dromara.common.pay.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.file.FileWriter;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.http.HttpStatus;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.ijpay.core.IJPayHttpResponse;
import com.ijpay.core.constant.IJPayConstants;
import com.ijpay.core.enums.RequestMethodEnum;
import com.ijpay.core.kit.AesUtil;
import com.ijpay.core.kit.PayKit;
import com.ijpay.core.kit.WxPayKit;
import com.ijpay.core.utils.DateTimeZoneUtil;
import com.ijpay.wxpay.WxPayApi;
import com.ijpay.wxpay.enums.WxDomainEnum;
import com.ijpay.wxpay.enums.v3.BasePayApiEnum;
import com.ijpay.wxpay.enums.v3.OtherApiEnum;
import com.ijpay.wxpay.model.v3.Certificate;
import com.ijpay.wxpay.model.v3.CertificateInfo;
import com.ijpay.wxpay.model.v3.EncryptCertificate;
import com.ijpay.wxpay.model.v3.UnifiedOrderModel;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.dromara.common.pay.config.WechatPayConfiguration;
import org.dromara.common.pay.domain.JsapiNotifyModel;
import org.dromara.common.pay.service.IWxPayService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.security.cert.X509Certificate;
import java.util.*;
/**
* @author hzh
* @date 2024-12-10
**/
@Service
@Slf4j
public class WxPayServiceImpl implements IWxPayService {
@Autowired
private WechatPayConfiguration config;
/**
* 商户 API 证书序列号
*/
private String serialNo;
/**
* 获取证书序列号
*
* @return 证书序列号
*/
private String getSerialNumber() {
if (StringUtils.isEmpty(serialNo)) {
// 获取证书序列号
X509Certificate certificate = PayKit.getCertificate(config.getCertPath());
if (null != certificate) {
serialNo = certificate.getSerialNumber().toString(16).toUpperCase();
// 提前两天检查证书是否有效
boolean isValid = PayKit.checkCertificateIsValid(certificate, config.getMchId(), -2);
log.info("证书是否可用 {} 证书有效期为 {}", isValid, DateUtil.format(certificate.getNotAfter(), DatePattern.NORM_DATETIME_PATTERN));
}
}
return serialNo;
}
@Override
public String jsapi(UnifiedOrderModel unifiedOrderModel) throws Exception {
log.info("统一下单参数 {}", JSONUtil.toJsonStr(unifiedOrderModel));
IJPayHttpResponse response = WxPayApi.v3(
RequestMethodEnum.POST,
WxDomainEnum.CHINA.toString(),
BasePayApiEnum.JS_API_PAY.toString(),
config.getMchId(),
getSerialNumber(),
null,
config.getKeyPath(),
JSONUtil.toJsonStr(unifiedOrderModel));
log.info("微信小程序统一下单响应 {}", response);
// 根据证书序列号查询对应的证书来验证签名结果
boolean verifySignature = WxPayKit.verifySignature(response, config.getPlatformCertPath());
log.info("verifySignature: {}", verifySignature);
if (response.getStatus() == HttpStatus.HTTP_OK && verifySignature) {
String body = response.getBody();
JSONObject jsonObject = JSONUtil.parseObj(body);
String prepayId = jsonObject.getStr("prepay_id");
Map<String, String> map = WxPayKit.jsApiCreateSign(config.getAppId(), prepayId, config.getKeyPath());
return JSONUtil.toJsonStr(map);
}
throw new RuntimeException("下单失败!");
}
@Override
public boolean autoUpdateOrGetCertificate(String serialNumber) throws Exception {
// 获取平台证书列表
IJPayHttpResponse response = WxPayApi.v3(
RequestMethodEnum.GET,
WxDomainEnum.CHINA.toString(),
OtherApiEnum.GET_CERTIFICATES.toString(),
config.getMchId(),
getSerialNumber(),
null,
config.getKeyPath(),
"");
// 接口响应使用的证书序列号,签名会使用到
String responseSerialNumber = response.getHeader("Wechatpay-Serial");
String body = response.getBody();
int status = response.getStatus();
if (status == IJPayConstants.CODE_200) {
Certificate model = JSONUtil.toBean(body, Certificate.class);
if (null == model) {
log.debug("解析返回数据失败");
return false;
}
List<CertificateInfo> data = model.getData();
if (CollUtil.isEmpty(data)) {
log.debug("未获取到任何有效平台证书");
return false;
}
log.debug("总共获取到 {} 个平台证书", data.size());
CertificateInfo certificateInfo = null;
// 获取指定序列号的平台证书
if (CharSequenceUtil.isNotEmpty(serialNumber)) {
Optional<CertificateInfo> optional = data
.stream()
.filter(item -> serialNumber.equals(item.getSerial_no()))
.findFirst();
if (optional.isPresent()) {
certificateInfo = optional.get();
}
}
// 指定序列号的平台证书不存在,遍历获取可用的平台证书
if (null == certificateInfo) {
log.debug("指定序列号 {} 的平台证书不存在,开始遍历获取可用的平台证书", serialNumber);
for (CertificateInfo info : data) {
if (null == info) {
continue;
}
String expireTime = info.getExpire_time();
String expireTimeStr = DateTimeZoneUtil.timeZoneDateToStr(expireTime);
Date expireDate = DateUtil.parse(expireTimeStr);
if (expireDate.before(new Date())) {
log.debug("序列号 {} 对应的平台证书已过期", info.getSerial_no());
continue;
}
log.debug("序列号 {} 对应的平台证书可用,忽略其他证书", info.getSerial_no());
certificateInfo = info;
break;
}
}
if (null == certificateInfo) {
log.debug("未获取到平台证书");
return false;
}
// 保存证书的序列号
String serialNo = certificateInfo.getSerial_no();
EncryptCertificate encryptCertificate = certificateInfo.getEncrypt_certificate();
String associatedData = encryptCertificate.getAssociated_data();
String cipherText = encryptCertificate.getCiphertext();
String nonce = encryptCertificate.getNonce();
String expireTime = certificateInfo.getExpire_time();
boolean isOk = savePlatformCert(associatedData, nonce, cipherText, config.getPlatformCertPath());
if (isOk) {
log.debug("平台证书保存成功,序列号为 {},失效时间为:{}, 保存路径为 {}", serialNo, expireTime, config.getPlatformCertPath());
if (CharSequenceUtil.equals(responseSerialNumber, serialNo)) {
// 根据证书序列号查询对应的证书来验证签名结果
boolean verifySignature = WxPayKit.verifySignature(response, config.getPlatformCertPath());
log.debug("使用序列号 {} 对应的平台证书签名验证结果 {}", serialNo, verifySignature);
return verifySignature;
}
return true;
}
}
return false;
}
/**
* 保存证书
*
* @param associatedData 关联数据
* @param nonce 随机数
* @param cipherText 加密报文
* @param certPath 证书路径
* @return 保存结果
* @throws Exception 异常信息
*/
protected boolean savePlatformCert(String associatedData, String nonce, String cipherText, String certPath) throws Exception {
AesUtil aesUtil = new AesUtil(config.getApiKey3().getBytes(StandardCharsets.UTF_8));
// 平台证书密文解密 encrypt_certificate 中的 associated_data nonce ciphertext
String publicKey = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8), nonce.getBytes(StandardCharsets.UTF_8), cipherText);
// 保存证书
FileWriter writer = new FileWriter(certPath);
File file = writer.write(publicKey);
return file.isFile() && file.exists();
}
@Override
public JsapiNotifyModel notify(String timestamp, String nonce, String serialNo, String signature, String result) {
log.info("timestamp:{} nonce:{} serialNo:{} signature:{}", timestamp, nonce, serialNo, signature);
log.info("支付通知密文 {}", result);
try {
// 需要通过证书序列号查找对应的证书,verifyNotify 中有验证证书的序列号
String plainText = WxPayKit.verifyNotify(
serialNo,
result,
signature,
nonce,
timestamp,
config.getApiKey3(),
config.getPlatformCertPath()
);
log.info("支付通知明文 {}", plainText);
return JSON.parseObject(plainText, JsapiNotifyModel.class);
} catch (Exception e) {
log.error("系统异常", e);
throw new RuntimeException("系统异常!");
}
}
@Override
public JsapiNotifyModel query(String outTradeNo) {
try {
Map<String, String> params = new HashMap<>(16);
params.put("mchid", config.getMchId());
log.info("统一查询订单参数 {},订单编号:{}", JSONUtil.toJsonStr(params), outTradeNo);
IJPayHttpResponse response = WxPayApi.v3(
RequestMethodEnum.GET,
WxDomainEnum.CHINA.toString(),
String.format(BasePayApiEnum.ORDER_QUERY_BY_OUT_TRADE_NO.toString(), outTradeNo),
config.getMchId(),
getSerialNumber(),
null,
config.getKeyPath(),
params);
log.info("查询订单:{} 查询响应 {}", outTradeNo, response);
if (response.getStatus() == IJPayConstants.CODE_200) {
// 根据证书序列号查询对应的证书来验证签名结果
boolean verifySignature = WxPayKit.verifySignature(response, config.getPlatformCertPath());
log.info("verifySignature: {}", verifySignature);
//验签成功
if (verifySignature) {
JSONObject result = JSONUtil.parseObj(response.getBody());
return JSON.parseObject(JSON.toJSONString(result), JsapiNotifyModel.class);
}
}
throw new RuntimeException("订单" + outTradeNo + "查询失败!");
} catch (Exception e) {
log.error("系统异常", e);
throw new RuntimeException("订单" + outTradeNo + "查询失败!");
}
}
}
org.dromara.common.pay.config.WechatPayConfiguration
org.dromara.common.pay.service.IWxPayService
...@@ -151,4 +151,5 @@ public class RemoteOrderServiceImpl implements RemoteOrderService { ...@@ -151,4 +151,5 @@ public class RemoteOrderServiceImpl implements RemoteOrderService {
return null; return null;
}).collect(Collectors.toList()); }).collect(Collectors.toList());
} }
} }
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论