| @ -1,207 +0,0 @@ | |||||
| package org.jeecg.config.pay; | |||||
| import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder; | |||||
| import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner; | |||||
| import com.wechat.pay.contrib.apache.httpclient.auth.Verifier; | |||||
| import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials; | |||||
| import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator; | |||||
| import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager; | |||||
| import com.wechat.pay.contrib.apache.httpclient.exception.HttpCodeException; | |||||
| import com.wechat.pay.contrib.apache.httpclient.exception.NotFoundException; | |||||
| import com.wechat.pay.contrib.apache.httpclient.util.PemUtil; | |||||
| import lombok.Data; | |||||
| import lombok.extern.slf4j.Slf4j; | |||||
| import org.apache.http.impl.client.CloseableHttpClient; | |||||
| import org.springframework.boot.context.properties.ConfigurationProperties; | |||||
| import org.springframework.context.annotation.Bean; | |||||
| import org.springframework.stereotype.Component; | |||||
| import java.io.IOException; | |||||
| import java.io.InputStream; | |||||
| import java.nio.charset.StandardCharsets; | |||||
| import java.security.GeneralSecurityException; | |||||
| import java.security.PrivateKey; | |||||
| /** | |||||
| * @author java996.icu | |||||
| * @title: WeChatPayConfig2 | |||||
| * @projectName chemu | |||||
| * @description: TODO | |||||
| * @date 2022/12/7 16:36 | |||||
| * @Version V1.0 | |||||
| */ | |||||
| @Component | |||||
| @Data | |||||
| @Slf4j | |||||
| @ConfigurationProperties(prefix = "wxpay") | |||||
| public class WeChatPayConfig2 { | |||||
| /** | |||||
| * 应用编号 | |||||
| */ | |||||
| private String appId; | |||||
| /** | |||||
| * 商户号 | |||||
| */ | |||||
| private String mchId; | |||||
| /** | |||||
| * 服务商商户号 | |||||
| */ | |||||
| private String slMchId; | |||||
| /** | |||||
| * APIv2密钥 | |||||
| */ | |||||
| private String apiKey; | |||||
| /** | |||||
| * APIv3密钥 | |||||
| */ | |||||
| private String apiV3Key; | |||||
| /** | |||||
| * 支付通知回调地址 | |||||
| */ | |||||
| private String notifyUrl; | |||||
| /** | |||||
| * 退款回调地址 | |||||
| */ | |||||
| private String refundNotifyUrl; | |||||
| /** | |||||
| * API 证书中的 key.pem | |||||
| */ | |||||
| private String keyPemPath; | |||||
| /** | |||||
| * 商户序列号 | |||||
| */ | |||||
| private String serialNo; | |||||
| /** | |||||
| * 微信支付V3-url前缀 | |||||
| */ | |||||
| private String baseUrl; | |||||
| /** | |||||
| * 获取商户的私钥文件 | |||||
| * @param keyPemPath | |||||
| * @return | |||||
| */ | |||||
| public PrivateKey getPrivateKey(String keyPemPath){ | |||||
| InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(keyPemPath); | |||||
| if(inputStream==null){ | |||||
| throw new RuntimeException("私钥文件不存在"); | |||||
| } | |||||
| return PemUtil.loadPrivateKey(inputStream); | |||||
| } | |||||
| // | |||||
| // /** | |||||
| // * 获取证书管理器实例 | |||||
| // * @return | |||||
| // */ | |||||
| // @Bean | |||||
| // public Verifier getVerifier() throws GeneralSecurityException, IOException, HttpCodeException, NotFoundException { | |||||
| // | |||||
| // log.info("获取证书管理器实例"); | |||||
| // | |||||
| // //获取商户私钥 | |||||
| // PrivateKey privateKey = getPrivateKey(keyPemPath); | |||||
| // | |||||
| // //私钥签名对象 | |||||
| // PrivateKeySigner privateKeySigner = new PrivateKeySigner(serialNo, privateKey); | |||||
| // | |||||
| // //身份认证对象 | |||||
| // WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner); | |||||
| // | |||||
| // // 使用定时更新的签名验证器,不需要传入证书 | |||||
| // CertificatesManager certificatesManager = CertificatesManager.getInstance(); | |||||
| // certificatesManager.putMerchant(mchId,wechatPay2Credentials,apiV3Key.getBytes(StandardCharsets.UTF_8)); | |||||
| // | |||||
| // return certificatesManager.getVerifier(mchId); | |||||
| // } | |||||
| @Bean | |||||
| public Verifier getVerifier() throws GeneralSecurityException, IOException, HttpCodeException, NotFoundException { | |||||
| log.info("开始获取微信支付证书管理器实例"); | |||||
| // 验证关键参数 | |||||
| log.debug("商户号: {}", mchId); | |||||
| log.debug("证书序列号: {}", serialNo); | |||||
| log.debug("APIv3密钥长度: {}", apiV3Key.length()); // 不记录具体密钥内容 | |||||
| // 获取商户私钥 | |||||
| PrivateKey privateKey = getPrivateKey(keyPemPath); | |||||
| if (privateKey == null) { | |||||
| throw new IllegalArgumentException("无法从路径加载私钥: " + keyPemPath); | |||||
| } | |||||
| // 创建签名器 | |||||
| PrivateKeySigner privateKeySigner = new PrivateKeySigner(serialNo, privateKey); | |||||
| WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner); | |||||
| try { | |||||
| CertificatesManager certificatesManager = CertificatesManager.getInstance(); | |||||
| // 显式设置域名(如果库需要) | |||||
| // certificatesManager.setDomain("api.mch.weixin.qq.com"); | |||||
| // 添加商户信息 | |||||
| certificatesManager.putMerchant(mchId, wechatPay2Credentials, apiV3Key.getBytes(StandardCharsets.UTF_8)); | |||||
| // 获取验证器 | |||||
| Verifier verifier = certificatesManager.getVerifier(mchId); | |||||
| log.info("成功获取证书管理器实例"); | |||||
| return verifier; | |||||
| } catch (HttpCodeException e) { | |||||
| log.error("微信支付API返回错误: "+ e); | |||||
| throw e; | |||||
| } catch (Exception e) { | |||||
| log.error("获取证书管理器实例时发生错误", e); | |||||
| throw e; | |||||
| } | |||||
| } | |||||
| /** | |||||
| * 获取支付http请求对象 | |||||
| * @param verifier | |||||
| * @return | |||||
| */ | |||||
| @Bean(name = "wxPayClient") | |||||
| public CloseableHttpClient getWxPayClient(Verifier verifier) { | |||||
| //获取商户私钥 | |||||
| PrivateKey privateKey = getPrivateKey(keyPemPath); | |||||
| WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create() | |||||
| .withMerchant(mchId, serialNo, privateKey) | |||||
| .withValidator(new WechatPay2Validator(verifier)); | |||||
| // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新 | |||||
| return builder.build(); | |||||
| } | |||||
| /** | |||||
| * 获取HttpClient,无需进行应答签名验证,跳过验签的流程 | |||||
| */ | |||||
| @Bean(name = "wxPayNoSignClient") | |||||
| public CloseableHttpClient getWxPayNoSignClient(){ | |||||
| //获取商户私钥 | |||||
| PrivateKey privateKey = getPrivateKey(keyPemPath); | |||||
| //用于构造HttpClient | |||||
| WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create() | |||||
| //设置商户信息 | |||||
| .withMerchant(mchId, serialNo, privateKey) | |||||
| //无需进行签名验证、通过withValidator((response) -> true)实现 | |||||
| .withValidator((response) -> true); | |||||
| // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新 | |||||
| return builder.build(); | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,20 @@ | |||||
| package org.jeecg.modules.api.service; | |||||
| import io.swagger.annotations.ApiOperation; | |||||
| import org.jeecg.common.api.vo.Result; | |||||
| import org.springframework.web.bind.annotation.PostMapping; | |||||
| import org.springframework.web.bind.annotation.RequestBody; | |||||
| import org.springframework.web.bind.annotation.RequestMapping; | |||||
| import org.springframework.web.bind.annotation.RequestMethod; | |||||
| public interface CashoutService { | |||||
| //会员中心-开通会员 | |||||
| @ApiOperation(value="测试提现-提现", notes="测试提现-提现") | |||||
| @RequestMapping(value = "/cashout", method = {RequestMethod.POST}) | |||||
| public Result<?> cashout(); | |||||
| //开通会员支付回调 | |||||
| //支付回调 | |||||
| @PostMapping("/cashoutNotify") | |||||
| public Object cashoutNotify(@RequestBody String requestBody); | |||||
| } | |||||
| @ -0,0 +1,29 @@ | |||||
| package org.jeecg.modules.api.service.impl; | |||||
| import lombok.extern.slf4j.Slf4j; | |||||
| import org.jeecg.common.api.vo.Result; | |||||
| import org.jeecg.modules.api.service.CashoutService; | |||||
| import org.jeecg.modules.transferTest.TransferToUser; | |||||
| import org.springframework.stereotype.Service; | |||||
| @Slf4j | |||||
| @Service | |||||
| public class CashoutServiceImpl implements CashoutService { | |||||
| @Override | |||||
| public Result<?> cashout() { | |||||
| String mchid = "1673516176"; | |||||
| String certiticateSerialNo ="525CDBD76E640EFB008288572C97D2715F3F18B2"; | |||||
| String privateKeyFilePath = "jeecg-boot-module-system/src/main/resources/apiclient_key_yaodu.pem"; | |||||
| String wechatPayPublicKeyId = "PUB_KEY_ID_0116735161762025040100448900000949"; | |||||
| String wechatPayPublicKeyFilePaht = "jeecg-boot-module-system/src/main/resources/pub_key_yaodu.pem"; | |||||
| TransferToUser transferToUser = new TransferToUser(mchid, certiticateSerialNo, privateKeyFilePath, wechatPayPublicKeyId, wechatPayPublicKeyFilePaht ); | |||||
| transferToUser.run(); | |||||
| return Result.OK("测试提现结束"); | |||||
| } | |||||
| @Override | |||||
| public Object cashoutNotify(String requestBody) { | |||||
| System.out.println("测试提现回调"); | |||||
| return Result.OK("测试回调"); | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,35 @@ | |||||
| package org.jeecg.modules.api.yaoduapi; | |||||
| import io.swagger.annotations.Api; | |||||
| import io.swagger.annotations.ApiOperation; | |||||
| import lombok.extern.slf4j.Slf4j; | |||||
| import org.jeecg.common.api.vo.Result; | |||||
| import org.jeecg.modules.api.service.CashoutService; | |||||
| import org.springframework.web.bind.annotation.*; | |||||
| import javax.annotation.Resource; | |||||
| @Api(tags="测试提现接口-提现") | |||||
| @RestController | |||||
| @RequestMapping("/cashout") | |||||
| @Slf4j | |||||
| public class CashoutController { | |||||
| /******************************************************************************************************************/ | |||||
| //会员信息 | |||||
| @Resource | |||||
| private CashoutService cashoutService; | |||||
| /******************************************************************************************************************/ | |||||
| //会员中心-开通会员 | |||||
| @ApiOperation(value="测试提现-提现", notes="测试提现-提现") | |||||
| @RequestMapping(value = "/cashout", method = {RequestMethod.POST}) | |||||
| public Result<?> cashout(){ | |||||
| return cashoutService.cashout(); | |||||
| } | |||||
| //提现回调 | |||||
| @PostMapping("/cashoutNotify") | |||||
| public Object cashoutNotify(@RequestBody String requestBody){ | |||||
| return cashoutService.cashoutNotify(requestBody); | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,257 @@ | |||||
| package org.jeecg.modules.transferTest; | |||||
| import com.google.gson.annotations.SerializedName; | |||||
| import okhttp3.*; | |||||
| import java.io.IOException; | |||||
| import java.io.UncheckedIOException; | |||||
| import java.security.PrivateKey; | |||||
| import java.security.PublicKey; | |||||
| import java.util.ArrayList; | |||||
| import java.util.List; | |||||
| /** | |||||
| * 发起转账 | |||||
| */ | |||||
| public class TransferToUser { | |||||
| private static String HOST = "https://api.mch.weixin.qq.com"; | |||||
| private static String METHOD = "POST"; | |||||
| private static String PATH = "/v3/fund-app/mch-transfer/transfer-bills"; | |||||
| public static void main(String[] args) { | |||||
| // TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756 | |||||
| // String mchid = "1712378227"; | |||||
| // String certiticateSerialNo ="33E9FE8076531A7C7AD401DC34E053DBD7C28E22"; | |||||
| // String privateKeyFilePath = "jeecg-boot-module-system/src/main/resources/apiclient_key.pem"; | |||||
| // String wechatPayPublicKeyId = "PUB_KEY_ID_0117123782272025033100396400002931"; | |||||
| // String wechatPayPublicKeyFilePaht = "jeecg-boot-module-system/src/main/resources/pub_key.pem"; | |||||
| String mchid = "1673516176"; | |||||
| String certiticateSerialNo ="525CDBD76E640EFB008288572C97D2715F3F18B2"; | |||||
| String privateKeyFilePath = "jeecg-boot-module-system/src/main/resources/apiclient_key_yaodu.pem"; | |||||
| String wechatPayPublicKeyId = "PUB_KEY_ID_0116735161762025040100448900000949"; | |||||
| String wechatPayPublicKeyFilePaht = "jeecg-boot-module-system/src/main/resources/pub_key_yaodu.pem"; | |||||
| TransferToUser client = new TransferToUser( | |||||
| mchid, // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756 | |||||
| certiticateSerialNo, // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053 | |||||
| privateKeyFilePath, // 商户API证书私钥文件路径,本地文件路径 | |||||
| wechatPayPublicKeyId, // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816 | |||||
| wechatPayPublicKeyFilePaht // 微信支付公钥文件路径,本地文件路径 | |||||
| ); | |||||
| String appid = "wxa4d29e67e8a58d38"; | |||||
| String openid = "oFzrW4migndUepy7zYgYO2YoZ5to"; | |||||
| String notifyUrl = "https://admin.hhlm1688.com/api/hello/"; | |||||
| TransferToUserRequest request = new TransferToUserRequest(); | |||||
| request.appid = appid; | |||||
| request.outBillNo = "plfk2020042013"; | |||||
| request.transferSceneId = "1000"; | |||||
| request.openid = openid; | |||||
| request.userName = client.encrypt("唐斌"); | |||||
| request.transferAmount = 400L; | |||||
| request.transferRemark = "新会员开通有礼"; | |||||
| request.notifyUrl = notifyUrl; | |||||
| request.userRecvPerception = "现金奖励"; | |||||
| request.transferSceneReportInfos = new ArrayList<>(); | |||||
| { | |||||
| TransferSceneReportInfo item0 = new TransferSceneReportInfo(); | |||||
| item0.infoType = "活动名称"; | |||||
| item0.infoContent = "新会员有礼"; | |||||
| request.transferSceneReportInfos.add(item0); | |||||
| TransferSceneReportInfo item1 = new TransferSceneReportInfo(); | |||||
| item1.infoType = "奖励说明"; | |||||
| item1.infoContent = "注册会员抽奖一等奖"; | |||||
| request.transferSceneReportInfos.add(item1); | |||||
| }; | |||||
| try { | |||||
| TransferToUserResponse response = client.run(request); | |||||
| // TODO: 请求成功,继续业务逻辑 | |||||
| System.out.println(response); | |||||
| } catch (WXPayUtility.ApiException e) { | |||||
| // TODO: 请求失败,根据状态码执行不同的逻辑 | |||||
| e.printStackTrace(); | |||||
| } | |||||
| } | |||||
| public void run(){ | |||||
| String mchid = "1673516176"; | |||||
| String certiticateSerialNo ="525CDBD76E640EFB008288572C97D2715F3F18B2"; | |||||
| String privateKeyFilePath = "jeecg-boot-module-system/src/main/resources/apiclient_key_yaodu.pem"; | |||||
| String wechatPayPublicKeyId = "PUB_KEY_ID_0116735161762025040100448900000949"; | |||||
| String wechatPayPublicKeyFilePaht = "jeecg-boot-module-system/src/main/resources/pub_key_yaodu.pem"; | |||||
| TransferToUser client = new TransferToUser( | |||||
| mchid, // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756 | |||||
| certiticateSerialNo, // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053 | |||||
| privateKeyFilePath, // 商户API证书私钥文件路径,本地文件路径 | |||||
| wechatPayPublicKeyId, // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816 | |||||
| wechatPayPublicKeyFilePaht // 微信支付公钥文件路径,本地文件路径 | |||||
| ); | |||||
| String appid = "wxa4d29e67e8a58d38"; | |||||
| String openid = "oFzrW4migndUepy7zYgYO2YoZ5to"; | |||||
| String notifyUrl = "https://admin.hhlm1688.com/api/cashout/cashout/"; | |||||
| TransferToUserRequest request = new TransferToUserRequest(); | |||||
| request.appid = appid; | |||||
| request.outBillNo = "plfk2020042013"; | |||||
| request.transferSceneId = "1000"; | |||||
| request.openid = openid; | |||||
| request.userName = client.encrypt("唐斌"); | |||||
| request.transferAmount = 400L; | |||||
| request.transferRemark = "新会员开通有礼"; | |||||
| request.notifyUrl = notifyUrl; | |||||
| request.userRecvPerception = "现金奖励"; | |||||
| request.transferSceneReportInfos = new ArrayList<>(); | |||||
| { | |||||
| TransferSceneReportInfo item0 = new TransferSceneReportInfo(); | |||||
| item0.infoType = "活动名称"; | |||||
| item0.infoContent = "新会员有礼"; | |||||
| request.transferSceneReportInfos.add(item0); | |||||
| TransferSceneReportInfo item1 = new TransferSceneReportInfo(); | |||||
| item1.infoType = "奖励说明"; | |||||
| item1.infoContent = "注册会员抽奖一等奖"; | |||||
| request.transferSceneReportInfos.add(item1); | |||||
| }; | |||||
| try { | |||||
| TransferToUserResponse response = client.run(request); | |||||
| // TODO: 请求成功,继续业务逻辑 | |||||
| System.out.println(response); | |||||
| } catch (WXPayUtility.ApiException e) { | |||||
| // TODO: 请求失败,根据状态码执行不同的逻辑 | |||||
| e.printStackTrace(); | |||||
| } | |||||
| } | |||||
| public TransferToUserResponse run(TransferToUserRequest request) { | |||||
| String uri = PATH; | |||||
| String reqBody = WXPayUtility.toJson(request); | |||||
| Request.Builder reqBuilder = new Request.Builder().url(HOST + uri); | |||||
| reqBuilder.addHeader("Accept", "application/json"); | |||||
| reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId); | |||||
| reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo,privateKey, METHOD, uri, reqBody)); | |||||
| reqBuilder.addHeader("Content-Type", "application/json"); | |||||
| RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody); | |||||
| reqBuilder.method(METHOD, requestBody); | |||||
| Request httpRequest = reqBuilder.build(); | |||||
| // 发送HTTP请求 | |||||
| OkHttpClient client = new OkHttpClient.Builder().build(); | |||||
| try (Response httpResponse = client.newCall(httpRequest).execute()) { | |||||
| String respBody = WXPayUtility.extractBody(httpResponse); | |||||
| if (httpResponse.code() >= 200 && httpResponse.code() < 300) { | |||||
| // 2XX 成功,验证应答签名 | |||||
| WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey, | |||||
| httpResponse.headers(), respBody); | |||||
| // 从HTTP应答报文构建返回数据 | |||||
| return WXPayUtility.fromJson(respBody, TransferToUserResponse.class); | |||||
| } else { | |||||
| throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers()); | |||||
| } | |||||
| } catch (IOException e) { | |||||
| throw new UncheckedIOException("Sending request to " + uri + " failed.", e); | |||||
| } | |||||
| } | |||||
| private final String mchid; | |||||
| private final String certificateSerialNo; | |||||
| private final PrivateKey privateKey; | |||||
| private final String wechatPayPublicKeyId; | |||||
| private final PublicKey wechatPayPublicKey; | |||||
| public TransferToUser(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) { | |||||
| this.mchid = mchid; | |||||
| this.certificateSerialNo = certificateSerialNo; | |||||
| this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath); | |||||
| this.wechatPayPublicKeyId = wechatPayPublicKeyId; | |||||
| this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath); | |||||
| } | |||||
| public String encrypt(String plainText) { | |||||
| return WXPayUtility.encrypt(this.wechatPayPublicKey, plainText); | |||||
| } | |||||
| public static class TransferToUserResponse { | |||||
| @SerializedName("out_bill_no") | |||||
| public String outBillNo; | |||||
| @SerializedName("transfer_bill_no") | |||||
| public String transferBillNo; | |||||
| @SerializedName("create_time") | |||||
| public String createTime; | |||||
| @SerializedName("state") | |||||
| public TransferBillStatus state; | |||||
| @SerializedName("package_info") | |||||
| public String packageInfo; | |||||
| } | |||||
| public enum TransferBillStatus { | |||||
| @SerializedName("ACCEPTED") | |||||
| ACCEPTED, | |||||
| @SerializedName("PROCESSING") | |||||
| PROCESSING, | |||||
| @SerializedName("WAIT_USER_CONFIRM") | |||||
| WAIT_USER_CONFIRM, | |||||
| @SerializedName("TRANSFERING") | |||||
| TRANSFERING, | |||||
| @SerializedName("SUCCESS") | |||||
| SUCCESS, | |||||
| @SerializedName("FAIL") | |||||
| FAIL, | |||||
| @SerializedName("CANCELING") | |||||
| CANCELING, | |||||
| @SerializedName("CANCELLED") | |||||
| CANCELLED | |||||
| } | |||||
| public static class TransferSceneReportInfo { | |||||
| @SerializedName("info_type") | |||||
| public String infoType; | |||||
| @SerializedName("info_content") | |||||
| public String infoContent; | |||||
| } | |||||
| public static class TransferToUserRequest { | |||||
| @SerializedName("appid") | |||||
| public String appid; | |||||
| @SerializedName("out_bill_no") | |||||
| public String outBillNo; | |||||
| @SerializedName("transfer_scene_id") | |||||
| public String transferSceneId; | |||||
| @SerializedName("openid") | |||||
| public String openid; | |||||
| @SerializedName("user_name") | |||||
| public String userName; | |||||
| @SerializedName("transfer_amount") | |||||
| public Long transferAmount; | |||||
| @SerializedName("transfer_remark") | |||||
| public String transferRemark; | |||||
| @SerializedName("notify_url") | |||||
| public String notifyUrl; | |||||
| @SerializedName("user_recv_perception") | |||||
| public String userRecvPerception; | |||||
| @SerializedName("transfer_scene_report_infos") | |||||
| public List<TransferSceneReportInfo> transferSceneReportInfos; | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,381 @@ | |||||
| package org.jeecg.modules.transferTest; | |||||
| import com.google.gson.*; | |||||
| import com.google.gson.annotations.Expose; | |||||
| import okhttp3.Headers; | |||||
| import okhttp3.Response; | |||||
| import okio.BufferedSource; | |||||
| import javax.crypto.BadPaddingException; | |||||
| import javax.crypto.Cipher; | |||||
| import javax.crypto.IllegalBlockSizeException; | |||||
| import javax.crypto.NoSuchPaddingException; | |||||
| import java.io.IOException; | |||||
| import java.io.UncheckedIOException; | |||||
| import java.io.UnsupportedEncodingException; | |||||
| import java.net.URLEncoder; | |||||
| import java.nio.charset.StandardCharsets; | |||||
| import java.nio.file.Files; | |||||
| import java.nio.file.Paths; | |||||
| import java.security.*; | |||||
| import java.security.spec.InvalidKeySpecException; | |||||
| import java.security.spec.PKCS8EncodedKeySpec; | |||||
| import java.security.spec.X509EncodedKeySpec; | |||||
| import java.time.DateTimeException; | |||||
| import java.time.Duration; | |||||
| import java.time.Instant; | |||||
| import java.util.Base64; | |||||
| import java.util.Map; | |||||
| import java.util.Objects; | |||||
| public class WXPayUtility { | |||||
| private static final Gson gson = new GsonBuilder() | |||||
| .disableHtmlEscaping() | |||||
| .addSerializationExclusionStrategy(new ExclusionStrategy() { | |||||
| @Override | |||||
| public boolean shouldSkipField(FieldAttributes fieldAttributes) { | |||||
| final Expose expose = fieldAttributes.getAnnotation(Expose.class); | |||||
| return expose != null && !expose.serialize(); | |||||
| } | |||||
| @Override | |||||
| public boolean shouldSkipClass(Class<?> aClass) { | |||||
| return false; | |||||
| } | |||||
| }) | |||||
| .addDeserializationExclusionStrategy(new ExclusionStrategy() { | |||||
| @Override | |||||
| public boolean shouldSkipField(FieldAttributes fieldAttributes) { | |||||
| final Expose expose = fieldAttributes.getAnnotation(Expose.class); | |||||
| return expose != null && !expose.deserialize(); | |||||
| } | |||||
| @Override | |||||
| public boolean shouldSkipClass(Class<?> aClass) { | |||||
| return false; | |||||
| } | |||||
| }) | |||||
| .create(); | |||||
| private static final char[] SYMBOLS = | |||||
| "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray(); | |||||
| private static final SecureRandom random = new SecureRandom(); | |||||
| /** | |||||
| * 将 Object 转换为 JSON 字符串 | |||||
| */ | |||||
| public static String toJson(Object object) { | |||||
| return gson.toJson(object); | |||||
| } | |||||
| /** | |||||
| * 将 JSON 字符串解析为特定类型的实例 | |||||
| */ | |||||
| public static <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException { | |||||
| return gson.fromJson(json, classOfT); | |||||
| } | |||||
| /** | |||||
| * 从公私钥文件路径中读取文件内容 | |||||
| * | |||||
| * @param keyPath 文件路径 | |||||
| * @return 文件内容 | |||||
| */ | |||||
| private static String readKeyStringFromPath(String keyPath) { | |||||
| try { | |||||
| return new String(Files.readAllBytes(Paths.get(keyPath)), StandardCharsets.UTF_8); | |||||
| } catch (IOException e) { | |||||
| throw new UncheckedIOException(e); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * 读取 PKCS#8 格式的私钥字符串并加载为私钥对象 | |||||
| * | |||||
| * @param keyString 私钥文件内容,以 -----BEGIN PRIVATE KEY----- 开头 | |||||
| * @return PrivateKey 对象 | |||||
| */ | |||||
| public static PrivateKey loadPrivateKeyFromString(String keyString) { | |||||
| try { | |||||
| keyString = keyString.replace("-----BEGIN PRIVATE KEY-----", "") | |||||
| .replace("-----END PRIVATE KEY-----", "") | |||||
| .replaceAll("\\s+", ""); | |||||
| return KeyFactory.getInstance("RSA").generatePrivate( | |||||
| new PKCS8EncodedKeySpec(Base64.getDecoder().decode(keyString))); | |||||
| } catch (NoSuchAlgorithmException e) { | |||||
| throw new UnsupportedOperationException(e); | |||||
| } catch (InvalidKeySpecException e) { | |||||
| throw new IllegalArgumentException(e); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * 从 PKCS#8 格式的私钥文件中加载私钥 | |||||
| * | |||||
| * @param keyPath 私钥文件路径 | |||||
| * @return PrivateKey 对象 | |||||
| */ | |||||
| public static PrivateKey loadPrivateKeyFromPath(String keyPath) { | |||||
| return loadPrivateKeyFromString(readKeyStringFromPath(keyPath)); | |||||
| } | |||||
| /** | |||||
| * 读取 PKCS#8 格式的公钥字符串并加载为公钥对象 | |||||
| * | |||||
| * @param keyString 公钥文件内容,以 -----BEGIN PUBLIC KEY----- 开头 | |||||
| * @return PublicKey 对象 | |||||
| */ | |||||
| public static PublicKey loadPublicKeyFromString(String keyString) { | |||||
| try { | |||||
| keyString = keyString.replace("-----BEGIN PUBLIC KEY-----", "") | |||||
| .replace("-----END PUBLIC KEY-----", "") | |||||
| .replaceAll("\\s+", ""); | |||||
| return KeyFactory.getInstance("RSA").generatePublic( | |||||
| new X509EncodedKeySpec(Base64.getDecoder().decode(keyString))); | |||||
| } catch (NoSuchAlgorithmException e) { | |||||
| throw new UnsupportedOperationException(e); | |||||
| } catch (InvalidKeySpecException e) { | |||||
| throw new IllegalArgumentException(e); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * 从 PKCS#8 格式的公钥文件中加载公钥 | |||||
| * | |||||
| * @param keyPath 公钥文件路径 | |||||
| * @return PublicKey 对象 | |||||
| */ | |||||
| public static PublicKey loadPublicKeyFromPath(String keyPath) { | |||||
| return loadPublicKeyFromString(readKeyStringFromPath(keyPath)); | |||||
| } | |||||
| /** | |||||
| * 创建指定长度的随机字符串,字符集为[0-9a-zA-Z],可用于安全相关用途 | |||||
| */ | |||||
| public static String createNonce(int length) { | |||||
| char[] buf = new char[length]; | |||||
| for (int i = 0; i < length; ++i) { | |||||
| buf[i] = SYMBOLS[random.nextInt(SYMBOLS.length)]; | |||||
| } | |||||
| return new String(buf); | |||||
| } | |||||
| /** | |||||
| * 使用公钥按照 RSA_PKCS1_OAEP_PADDING 算法进行加密 | |||||
| * | |||||
| * @param publicKey 加密用公钥对象 | |||||
| * @param plaintext 待加密明文 | |||||
| * @return 加密后密文 | |||||
| */ | |||||
| public static String encrypt(PublicKey publicKey, String plaintext) { | |||||
| final String transformation = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"; | |||||
| try { | |||||
| Cipher cipher = Cipher.getInstance(transformation); | |||||
| cipher.init(Cipher.ENCRYPT_MODE, publicKey); | |||||
| return Base64.getEncoder().encodeToString(cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8))); | |||||
| } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { | |||||
| throw new IllegalArgumentException("The current Java environment does not support " + transformation, e); | |||||
| } catch (InvalidKeyException e) { | |||||
| throw new IllegalArgumentException("RSA encryption using an illegal publicKey", e); | |||||
| } catch (BadPaddingException | IllegalBlockSizeException e) { | |||||
| throw new IllegalArgumentException("Plaintext is too long", e); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * 使用私钥按照指定算法进行签名 | |||||
| * | |||||
| * @param message 待签名串 | |||||
| * @param algorithm 签名算法,如 SHA256withRSA | |||||
| * @param privateKey 签名用私钥对象 | |||||
| * @return 签名结果 | |||||
| */ | |||||
| public static String sign(String message, String algorithm, PrivateKey privateKey) { | |||||
| byte[] sign; | |||||
| try { | |||||
| Signature signature = Signature.getInstance(algorithm); | |||||
| signature.initSign(privateKey); | |||||
| signature.update(message.getBytes(StandardCharsets.UTF_8)); | |||||
| sign = signature.sign(); | |||||
| } catch (NoSuchAlgorithmException e) { | |||||
| throw new UnsupportedOperationException("The current Java environment does not support " + algorithm, e); | |||||
| } catch (InvalidKeyException e) { | |||||
| throw new IllegalArgumentException(algorithm + " signature uses an illegal privateKey.", e); | |||||
| } catch (SignatureException e) { | |||||
| throw new RuntimeException("An error occurred during the sign process.", e); | |||||
| } | |||||
| return Base64.getEncoder().encodeToString(sign); | |||||
| } | |||||
| /** | |||||
| * 使用公钥按照特定算法验证签名 | |||||
| * | |||||
| * @param message 待签名串 | |||||
| * @param signature 待验证的签名内容 | |||||
| * @param algorithm 签名算法,如:SHA256withRSA | |||||
| * @param publicKey 验签用公钥对象 | |||||
| * @return 签名验证是否通过 | |||||
| */ | |||||
| public static boolean verify(String message, String signature, String algorithm, | |||||
| PublicKey publicKey) { | |||||
| try { | |||||
| Signature sign = Signature.getInstance(algorithm); | |||||
| sign.initVerify(publicKey); | |||||
| sign.update(message.getBytes(StandardCharsets.UTF_8)); | |||||
| return sign.verify(Base64.getDecoder().decode(signature)); | |||||
| } catch (SignatureException e) { | |||||
| return false; | |||||
| } catch (InvalidKeyException e) { | |||||
| throw new IllegalArgumentException("verify uses an illegal publickey.", e); | |||||
| } catch (NoSuchAlgorithmException e) { | |||||
| throw new UnsupportedOperationException("The current Java environment does not support" + algorithm, e); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * 根据微信支付APIv3请求签名规则构造 Authorization 签名 | |||||
| * | |||||
| * @param mchid 商户号 | |||||
| * @param certificateSerialNo 商户API证书序列号 | |||||
| * @param privateKey 商户API证书私钥 | |||||
| * @param method 请求接口的HTTP方法,请使用全大写表述,如 GET、POST、PUT、DELETE | |||||
| * @param uri 请求接口的URL | |||||
| * @param body 请求接口的Body | |||||
| * @return 构造好的微信支付APIv3 Authorization 头 | |||||
| */ | |||||
| public static String buildAuthorization(String mchid, String certificateSerialNo, | |||||
| PrivateKey privateKey, | |||||
| String method, String uri, String body) { | |||||
| String nonce = createNonce(32); | |||||
| long timestamp = Instant.now().getEpochSecond(); | |||||
| String message = String.format("%s\n%s\n%d\n%s\n%s\n", method, uri, timestamp, nonce, | |||||
| body == null ? "" : body); | |||||
| String signature = sign(message, "SHA256withRSA", privateKey); | |||||
| return String.format( | |||||
| "WECHATPAY2-SHA256-RSA2048 mchid=\"%s\",nonce_str=\"%s\",signature=\"%s\"," + | |||||
| "timestamp=\"%d\",serial_no=\"%s\"", | |||||
| mchid, nonce, signature, timestamp, certificateSerialNo); | |||||
| } | |||||
| /** | |||||
| * 对参数进行 URL 编码 | |||||
| * | |||||
| * @param content 参数内容 | |||||
| * @return 编码后的内容 | |||||
| */ | |||||
| public static String urlEncode(String content) { | |||||
| try { | |||||
| return URLEncoder.encode(content, StandardCharsets.UTF_8.name()); | |||||
| } catch (UnsupportedEncodingException e) { | |||||
| throw new RuntimeException(e); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * 对参数Map进行 URL 编码,生成 QueryString | |||||
| * | |||||
| * @param params Query参数Map | |||||
| * @return QueryString | |||||
| */ | |||||
| public static String urlEncode(Map<String, Object> params) { | |||||
| if (params == null || params.isEmpty()) { | |||||
| return ""; | |||||
| } | |||||
| int index = 0; | |||||
| StringBuilder result = new StringBuilder(); | |||||
| for (Map.Entry<String, Object> entry : params.entrySet()) { | |||||
| result.append(entry.getKey()) | |||||
| .append("=") | |||||
| .append(urlEncode(entry.getValue().toString())); | |||||
| index++; | |||||
| if (index < params.size()) { | |||||
| result.append("&"); | |||||
| } | |||||
| } | |||||
| return result.toString(); | |||||
| } | |||||
| /** | |||||
| * 从应答中提取 Body | |||||
| * | |||||
| * @param response HTTP 请求应答对象 | |||||
| * @return 应答中的Body内容,Body为空时返回空字符串 | |||||
| */ | |||||
| public static String extractBody(Response response) { | |||||
| if (response.body() == null) { | |||||
| return ""; | |||||
| } | |||||
| try { | |||||
| BufferedSource source = response.body().source(); | |||||
| return source.readUtf8(); | |||||
| } catch (IOException e) { | |||||
| throw new RuntimeException(String.format("An error occurred during reading response body. Status: %d", response.code()), e); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * 根据微信支付APIv3应答验签规则对应答签名进行验证,验证不通过时抛出异常 | |||||
| * | |||||
| * @param wechatpayPublicKeyId 微信支付公钥ID | |||||
| * @param wechatpayPublicKey 微信支付公钥对象 | |||||
| * @param headers 微信支付应答 Header 列表 | |||||
| * @param body 微信支付应答 Body | |||||
| */ | |||||
| public static void validateResponse(String wechatpayPublicKeyId, PublicKey wechatpayPublicKey, | |||||
| Headers headers, | |||||
| String body) { | |||||
| String timestamp = headers.get("Wechatpay-Timestamp"); | |||||
| try { | |||||
| Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestamp)); | |||||
| // 拒绝过期请求 | |||||
| if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= 5) { | |||||
| throw new IllegalArgumentException( | |||||
| String.format("Validate http response,timestamp[%s] of httpResponse is expires, " | |||||
| + "request-id[%s]", | |||||
| timestamp, headers.get("Request-ID"))); | |||||
| } | |||||
| } catch (DateTimeException | NumberFormatException e) { | |||||
| throw new IllegalArgumentException( | |||||
| String.format("Validate http response,timestamp[%s] of httpResponse is invalid, " + | |||||
| "request-id[%s]", timestamp, | |||||
| headers.get("Request-ID"))); | |||||
| } | |||||
| String message = String.format("%s\n%s\n%s\n", timestamp, headers.get("Wechatpay-Nonce"), | |||||
| body == null ? "" : body); | |||||
| String serialNumber = headers.get("Wechatpay-Serial"); | |||||
| if (!Objects.equals(serialNumber, wechatpayPublicKeyId)) { | |||||
| throw new IllegalArgumentException( | |||||
| String.format("Invalid Wechatpay-Serial, Local: %s, Remote: %s", wechatpayPublicKeyId, | |||||
| serialNumber)); | |||||
| } | |||||
| String signature = headers.get("Wechatpay-Signature"); | |||||
| boolean success = verify(message, signature, "SHA256withRSA", wechatpayPublicKey); | |||||
| if (!success) { | |||||
| throw new IllegalArgumentException( | |||||
| String.format("Validate response failed,the WechatPay signature is incorrect.%n" | |||||
| + "Request-ID[%s]\tresponseHeader[%s]\tresponseBody[%.1024s]", | |||||
| headers.get("Request-ID"), headers, body)); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * 微信支付API错误异常,发送HTTP请求成功,但返回状态码不是 2XX 时抛出本异常 | |||||
| */ | |||||
| public static class ApiException extends RuntimeException { | |||||
| public final int statusCode; | |||||
| public final String body; | |||||
| public final Headers headers; | |||||
| public ApiException(int statusCode, String body, Headers headers) { | |||||
| super(String.format("微信支付API访问失败,StatusCode: %s, Body: %s", statusCode, body)); | |||||
| this.statusCode = statusCode; | |||||
| this.body = body; | |||||
| this.headers = headers; | |||||
| } | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,13 @@ | |||||
| package org.jeecg.modules.transferTest; | |||||
| public class test { | |||||
| public static void main(String[] args) { | |||||
| String mchid = "1673516176"; | |||||
| String certiticateSerialNo ="525CDBD76E640EFB008288572C97D2715F3F18B2"; | |||||
| String privateKeyFilePath = "jeecg-boot-module-system/src/main/resources/apiclient_key_yaodu.pem"; | |||||
| String wechatPayPublicKeyId = "PUB_KEY_ID_0116735161762025040100448900000949"; | |||||
| String wechatPayPublicKeyFilePaht = "jeecg-boot-module-system/src/main/resources/pub_key_yaodu.pem"; | |||||
| TransferToUser transferToUser = new TransferToUser(mchid, certiticateSerialNo, privateKeyFilePath, wechatPayPublicKeyId, wechatPayPublicKeyFilePaht ); | |||||
| transferToUser.run(); | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,28 @@ | |||||
| -----BEGIN PRIVATE KEY----- | |||||
| MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDFHyPaZuXY6GCd | |||||
| mC/CId2jMQ3Iun2cZycw2LxdWLNwtKGYKVk+GWa4upm60Zl+qrNpLam3qTD5Oyy3 | |||||
| T8Mz9EWHwBYr2xTSKINRbIjabdVyV0/H2RLitwRg3XyHQPxYFcvI9t9F2GHELsAA | |||||
| qGmFp0+PiCVMzT+nB3sU+yLaNyiMC8rcprjh4tBV857K3Y5rzljbJsxa9WVzS0EM | |||||
| GQRwPV2LaWPApcGZIKJLRAUa8yGMKMINyb9iJjdA1/4vD8tLWmvFhXtTYmrWmFhu | |||||
| 5QsyKDClOek2gqGgxDL9Hvvx5PYR59Mg57kyz0vSKhYa0MKgsFLuu7QJOFXk2bJo | |||||
| DJymcLypAgMBAAECggEBAL+1626riHsOdXiP3FLYEPB38sn35dZI1GrDP18ht1Kz | |||||
| uj18aVjl52tdv8lbtAbnCZoPWPJQUFr0XCbkIhrTRRQjkuyQI43I7P4xql+VVnPf | |||||
| yq24xo9MI6v5fPUmFMWuXQVUZA1PxrXAKef54raj49LaPDyXmYJe2iurm1fTMVIR | |||||
| Jba1rBi0hp5W1fMCiCPk1SMfANTE0UOTYdSRhNVSgf51aJHFI+FR8iSmf8rEv+B0 | |||||
| s8uDXvxTGgbEaZsO7wcjQfsbQixoqEmfTzEoBTHkQ1npigG32gsz8QO6P4dhJRX7 | |||||
| WJ3N8x0vgJqxCLeRLEb6c8ZJ62SuEi6PtETVWXujeCECgYEA/gP8fyp5lBKF/weC | |||||
| UzMCEAJzwTKjmC0IOWnoIwQTSKZTCKHmz5LMNYn3nTf2d8LpbB9GYmBhUdiO/88f | |||||
| AZ/vbuskkoH0rmOgelVx13tDaVuNOOVmtKmKvzYOxiGeL6ooX2cwfRnVOMIPTBFM | |||||
| G1lAzxeowTy5uLKvuuMGBVJj7MUCgYEAxqleqjcp3+4QoE9Z/2hEt6sYnI/I1T/h | |||||
| kbgTO8wQSGG8ambl/qJu+9ON/Ag1jK2amrNKP1vXbTQsjkUby6U5MgVwGDB4bNN1 | |||||
| +wgwxMXJ74QJtbnXp7XBqC96S6Pyw+cLEjvWk0xJ14Os2oCO1OvZibu5KHrsPECn | |||||
| 3OmddJkjFpUCgYEAoiwHW2TRxCBjXiP8J4QMUA5QusrKuVAezRD5fMmQSjSuFHfQ | |||||
| 9Tsilxfjd4OQHnvZLQd2lz4zQ96/xUAF6rKiWa1UZxkDDwdaIGBG0yzGKBCkQ+vp | |||||
| u3P2ugcYPZSe+o1nQymNQoFoqNj0jTsJ3PgJsW3Idr5/UBT8rpNcd69XToUCgYB8 | |||||
| AQTCIyTUTnm6V03KC3+5VedK8sVdtz5KAyieTsZrJ/bAQ/KUezfjoS4jf8xNP6Ad | |||||
| qIRUADP8SnD1bVXoS/3jp1lNABRreaNPStGGQh/GjhixgouGeAGlxd0EkhXbCsDy | |||||
| ZL+PujLtf5fJ3C1L4twrCS6OggwroAAn+Pr76Qrp8QKBgQD9uH47NRToKkaTPflY | |||||
| mVlbLKCWnAjoqlxHrANjM6lOeC4SfAYaZRIpCK9B7LFR7eeqIb0Y/RjfwtLdFKQF | |||||
| 1hVCnLYTsEJ5gG64qqInVb4e0FCF/2w1uoBGwB2pUWjd0DUXH3yHc3LVCBhxOkRa | |||||
| oULtLnG34V6fFclMh3Stk/pMVg== | |||||
| -----END PRIVATE KEY----- | |||||
| @ -0,0 +1,9 @@ | |||||
| -----BEGIN PUBLIC KEY----- | |||||
| MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxK2wqQooeGKkAQchZdwK | |||||
| U3qFiPSyf83l7USGGAdEhb78iq0Dvi7RfgdHuqSVBv0fnSQJicNO+s10ofi9waxl | |||||
| SCPnuO8t9MeOuS4IpZ54VWY/9pJ5A2Z0x49L0djoFWStFCpKzsg2fWBvc/7kYVFr | |||||
| nq/jFyRho8/GZtxL9RLZjWLyfnpe+erxSNFEnQLoW6LC4D5L0w9oiHboHmN9Igzc | |||||
| uB6pIuMwccImX1xPeu/jx2QMYrxAW/2bW3e6z4ojQWqhRtFq55INnXLV8VXE7rwI | |||||
| 1L0RB4R39JOsKroVE/g7SiHRbGEem2BbNaAFhjCMuZzpWSCFQkTlxLJMxxTt9j0S | |||||
| +wIDAQAB | |||||
| -----END PUBLIC KEY----- | |||||