From bbd9c436f23f5fdbe712c4a22d90b457066bdf38 Mon Sep 17 00:00:00 2001
From: k94314517 <8417338+k94314517@user.noreply.gitee.com>
Date: 星期四, 17 七月 2025 19:25:47 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/master'

---
 server/services/src/main/java/com/doumee/config/wx/TransferToUser.java |  312 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 312 insertions(+), 0 deletions(-)

diff --git a/server/services/src/main/java/com/doumee/config/wx/TransferToUser.java b/server/services/src/main/java/com/doumee/config/wx/TransferToUser.java
new file mode 100644
index 0000000..af0b96e
--- /dev/null
+++ b/server/services/src/main/java/com/doumee/config/wx/TransferToUser.java
@@ -0,0 +1,312 @@
+package com.doumee.config.wx;
+
+import com.alibaba.fastjson.JSONObject;
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+import okhttp3.*;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by IntelliJ IDEA.
+ *
+ * @Author : Rk
+ * @create 2025/7/17 9:19
+ */
+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";
+    private static String CANCEL_PATH = "/v3/fund-app/mch-transfer/transfer-bills/out-bill-no/{out_bill_no}/cancel";
+
+
+    public static void main(String[] args) {
+        // TODO: 璇峰噯澶囧晢鎴峰紑鍙戝繀瑕佸弬鏁帮紝鍙傝�冿細https://pay.weixin.qq.com/doc/v3/merchant/4013070756
+        TransferToUser client = new TransferToUser(
+                "1229817002",                    // 鍟嗘埛鍙凤紝鏄敱寰俊鏀粯绯荤粺鐢熸垚骞跺垎閰嶇粰姣忎釜鍟嗘埛鐨勫敮涓�鏍囪瘑绗︼紝鍟嗘埛鍙疯幏鍙栨柟寮忓弬鑰� https://pay.weixin.qq.com/doc/v3/merchant/4013070756
+                "3FE90C2F3D40A56E1C51926F31B8A8D22426CCE0",         // 鍟嗘埛API璇佷功搴忓垪鍙凤紝濡備綍鑾峰彇璇峰弬鑰� https://pay.weixin.qq.com/doc/v3/merchant/4013053053
+                "d://wechatApiclient_key.pem",    // 鍟嗘埛API璇佷功绉侀挜鏂囦欢璺緞锛屾湰鍦版枃浠惰矾寰�
+                "PUB_KEY_ID_0112298170022025071700291836000600",      // 寰俊鏀粯鍏挜ID锛屽浣曡幏鍙栬鍙傝�� https://pay.weixin.qq.com/doc/v3/merchant/4013038816
+                "d://pub_key.pem"          // 寰俊鏀粯鍏挜鏂囦欢璺緞锛屾湰鍦版枃浠惰矾寰�
+        );
+
+        TransferToUserRequest request = new TransferToUserRequest();
+        request.appid = "wxcd2b89fd2ff065f8"; //灏忕▼搴廼d
+        request.outBillNo = "plfk2020042016";
+        request.transferSceneId = "1005";
+        request.openid = "oKKHU5IFKpss_DIbFX1lqghFJOEg";
+        request.userName = client.encrypt("鏂芥棴杈�");
+        request.transferAmount = 91L;
+        request.transferRemark = "~~~";
+        request.notifyUrl = "https://www.weixin.qq.com/wxpay/pay.php";
+        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.testRun(request);
+
+            // TODO: 璇锋眰鎴愬姛锛岀户缁笟鍔¢�昏緫
+            System.out.println(JSONObject.toJSONString(response));
+        } catch (WXPayUtility.ApiException e) {
+            // TODO: 璇锋眰澶辫触锛屾牴鎹姸鎬佺爜鎵ц涓嶅悓鐨勯�昏緫
+            e.printStackTrace();
+        }
+    }
+
+    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 TransferToUserResponse testRun(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();
+
+        // 鍙戦�丠TTP璇锋眰
+        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);
+
+                // 浠嶩TTP搴旂瓟鎶ユ枃鏋勫缓杩斿洖鏁版嵁
+                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);
+        }
+    }
+
+    public TransferToUserResponse run(TransferToUserRequest request,String name) {
+        String uri = PATH;
+        request.appid = WxMiniConfig.wxProperties.getSubAppId();
+        request.transferSceneId = "1005";
+        request.userRecvPerception = "鍔冲姟鎶ラ叕";
+        if(request.transferAmount >= 30){
+            request.userName = this.encrypt(name);
+        }
+
+        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);
+        };
+        String reqBody = WXPayUtility.toJson(request);
+
+        Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
+        reqBuilder.addHeader("Accept", "application/json");
+        reqBuilder.addHeader("Wechatpay-Serial", WxMiniConfig.wxProperties.getWechatPayPublicKeyId());
+        reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(WxMiniConfig.wxProperties.getMchId(),
+                WxMiniConfig.wxProperties.getWechatSerialNumer(),
+                WXPayUtility.loadPrivateKeyFromPath(WxMiniConfig.wxProperties.getWechatPrivateKeyPath()),
+                METHOD, uri, reqBody));
+
+//        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();
+
+        // 鍙戦�丠TTP璇锋眰
+        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(WxMiniConfig.wxProperties.getWechatPayPublicKeyId(), WXPayUtility.loadPublicKeyFromPath(WxMiniConfig.wxProperties.getWechatPubKeyPath()),
+                        httpResponse.headers(), respBody);
+
+                // 浠嶩TTP搴旂瓟鎶ユ枃鏋勫缓杩斿洖鏁版嵁
+                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);
+        }
+    }
+
+
+    public TransferToUser.CancelTransferResponse cancelRun(TransferToUser.CancelTransferRequest request) {
+        String uri = CANCEL_PATH;
+        uri = uri.replace("{out_bill_no}", WXPayUtility.urlEncode(request.outBillNo));
+
+        Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
+        reqBuilder.addHeader("Accept", "application/json");
+        reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
+        reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(WxMiniConfig.wxProperties.getMchId(),
+                WxMiniConfig.wxProperties.getWechatSerialNumer(),
+                WXPayUtility.loadPrivateKeyFromPath(WxMiniConfig.wxProperties.getWechatPrivateKeyPath()),
+                METHOD, uri, null));
+        reqBuilder.addHeader("Content-Type", "application/json");
+        RequestBody emptyBody = RequestBody.create(null, "");
+        reqBuilder.method(METHOD, emptyBody);
+        Request httpRequest = reqBuilder.build();
+
+        // 鍙戦�丠TTP璇锋眰
+        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(WxMiniConfig.wxProperties.getWechatPayPublicKeyId(), WXPayUtility.loadPublicKeyFromPath(WxMiniConfig.wxProperties.getWechatPubKeyPath()),
+                        httpResponse.headers(), respBody);
+
+                // 浠嶩TTP搴旂瓟鎶ユ枃鏋勫缓杩斿洖鏁版嵁
+                return WXPayUtility.fromJson(respBody, TransferToUser.CancelTransferResponse.class);
+            } else {
+                throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
+            }
+        } catch (IOException e) {
+            throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
+        }
+    }
+
+
+
+    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;
+    }
+
+    public static class CancelTransferResponse {
+        @SerializedName("out_bill_no")
+        public String outBillNo;
+
+        @SerializedName("transfer_bill_no")
+        public String transferBillNo;
+
+        @SerializedName("state")
+        public String state;
+
+        @SerializedName("update_time")
+        public String updateTime;
+    }
+
+    public static class CancelTransferRequest {
+        @SerializedName("out_bill_no")
+        @Expose(serialize = false)
+        public String outBillNo;
+    }
+
+}

--
Gitblit v1.9.3