package com.ruoyi.api.util;


import com.google.gson.ExclusionStrategy;
import com.google.gson.FieldAttributes;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
import com.google.gson.internal.Primitives;
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.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Signature;
import java.security.SignatureException;
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.List;
import java.util.Objects;
import javax.annotation.Resource;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;

import com.ruoyi.payConfig.WechatPayConfig;
import okhttp3.Headers;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okio.BufferedSource;
import org.springframework.stereotype.Component;

/**
 * 发起转账
 */
@Component
public class TransferToUser {
    private static final String host = "https://api.mch.weixin.qq.com";
    private static final String path = "/v3/fund-app/mch-transfer/transfer-bills";
    private static final String method = "POST";




    @Resource
    private WechatPayConfig wechatPayConfig;



    /**
     * https://pay.weixin.qq.com/doc/v3/merchant/4012716434
     *
     * 商家转账用户确认模式下，用户申请收款时，商户可通过此接口申请创建转账单
     *
     * * 接口返回的HTTP状态码及错误码，仅代表本次请求的结果，不能代表订单状态。
     * * 接口返回的HTTP状态码为200，且赔付状态为ACCEPT时，可认为发起商家转账成功。
     * * **接口返回的HTTP状态码不为200时，请商户务必不要立即更换商户订单单号重试**。可根据错误码列表中的描述和接口返回的信息进行处理，并在查询原订单结果为失败或者联系客服确认情况后，再更换商户订单号进行重试。否则会有重复转账的资金风险。
     *
     * 注：单个商户的接口频率限制为100次/s
     *
     * @return TransferToUserResponse
     */
    public TransferToUserResponse run(TransferToUserRequest request) {
        // 请求参数验证


        // 构造HTTP请求
        String uri = path;
        String body = this.createRequestBody(request);

        // 发送HTTP请求
        try (Response httpResponse = this.sendHttpRequest(uri, body)) {
            // 应答结果检查
            String respBody = validateResponse(httpResponse);

            // 从HTTP应答报文构建返回数据
            return parseResponse(respBody);
        } catch (IOException e) {
            throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
        }
    }

    /**
     * 商家转账-发起转账-验证请求参数
     *
     * 参数列表：
     * - appid string(32): [必填] **【商户AppID】** 是微信开放平台和微信公众平台为开发者的应用程序(APP、小程序、公众号、企业号corpid即为此AppID)提供的一个唯一标识。此处，可以填写这四种类型中的任意一种APPID，但请确保该appid与商户号有绑定关系。详见：[普通商户模式开发必要参数说明](https://iwiki.woa.com/p/4013070756)。
     * - out_bill_no string(32): [必填] **【商户单号】** 商户系统内部的商家单号，要求此参数只能由数字、大小写字母组成，在商户系统内部唯一
     * - transfer_scene_id string(36): [必填] **【转账场景ID】** 该笔转账使用的转账场景，可前往“商户平台-产品中心-商家转账”中申请。如：1001-现金营销
     * - openid string(64): [必填] **【收款用户OpenID】** 用户在商户appid下的唯一标识。发起转账前需获取到用户的OpenID，获取方式详见 [参数说明](https://iwiki.woa.com/p/4012068676)。
     * - user_name string: [选填] **【收款用户姓名】** 收款方真实姓名。需要加密传入，支持标准RSA算法和国密算法，公钥由微信侧提供。<br/>转账金额 >= 2,000元时，该笔明细必须填写<br/>若商户传入收款用户姓名，微信支付会校验收款用户与输入姓名是否一致，并提供电子回单
     * - transfer_amount integer: [必填] **【转账金额】** 转账金额单位为“分”。
     * - transfer_remark string(32): [必填] **【转账备注】** 转账备注，用户收款时可见该备注信息，UTF8编码，最多允许32个字符
     * - notify_url string(256): [选填] **【通知地址】** 异步接收微信支付结果通知的回调地址，通知url必须为公网可访问的URL，必须为HTTPS，不能携带参数。
     * - user_recv_perception string: [选填] **【用户收款感知】** 用户收款时感知到的收款原因将根据转账场景自动展示默认内容。如有其他展示需求，可在本字段传入。各场景展示的默认内容和支持传入的内容，可查看产品文档了解。
     * - transfer_scene_report_infos array[TransferSceneReportInfo]: [选填] **【转账场景报备信息】** 各转账场景下需报备的内容，商户需要按照所属转账场景规则传参，详见[转账场景报备信息字段说明](https://iwiki.woa.com/p/4013774588)。
     * - transfer_scene_report_infos.info_type string(15): [必填] **【信息类型】** 不能超过15个字符，商户所属转账场景下的信息类型，此字段内容为固定值，需严格按照 [转账场景报备信息字段说明](https://iwiki.woa.com/p/4013774588) 传参。
     * - transfer_scene_report_infos.info_content string(32): [必填] **【信息内容】** 不能超过32个字符，商户所属转账场景下的信息内容，商户可按实际业务场景自定义传参，需严格按照 [转账场景报备信息字段说明](https://iwiki.woa.com/p/4013774588) 传参。
     *
     * 请根据上述字段列表提供的参数的必要性、最长长度限制，并根据描述中所述的关联，编写参数检查逻辑，每个字段以驼峰命名Public访问，无需Getter。
     * @param request 请求参数
     */
    private void validateRequestParameters(TransferToUserRequest request) {
        // 商家转账-发起转账-验证请求参数 生成示例，需选中该注释，通过 @demo 指令触发
        throw new UnsupportedOperationException("Method not implemented yet");
    }

    /**
     * 商家转账-发起转账-构造HTTP请求包体
     *
     * 包体参数列表：
     * - appid string(32): [必填] **【商户AppID】** 是微信开放平台和微信公众平台为开发者的应用程序(APP、小程序、公众号、企业号corpid即为此AppID)提供的一个唯一标识。此处，可以填写这四种类型中的任意一种APPID，但请确保该appid与商户号有绑定关系。详见：[普通商户模式开发必要参数说明](https://iwiki.woa.com/p/4013070756)。
     * - out_bill_no string(32): [必填] **【商户单号】** 商户系统内部的商家单号，要求此参数只能由数字、大小写字母组成，在商户系统内部唯一
     * - transfer_scene_id string(36): [必填] **【转账场景ID】** 该笔转账使用的转账场景，可前往“商户平台-产品中心-商家转账”中申请。如：1001-现金营销
     * - openid string(64): [必填] **【收款用户OpenID】** 用户在商户appid下的唯一标识。发起转账前需获取到用户的OpenID，获取方式详见 [参数说明](https://iwiki.woa.com/p/4012068676)。
     * - user_name string: [选填] **【收款用户姓名】** 收款方真实姓名。需要加密传入，支持标准RSA算法和国密算法，公钥由微信侧提供。<br/>转账金额 >= 2,000元时，该笔明细必须填写<br/>若商户传入收款用户姓名，微信支付会校验收款用户与输入姓名是否一致，并提供电子回单
     * - transfer_amount integer: [必填] **【转账金额】** 转账金额单位为“分”。
     * - transfer_remark string(32): [必填] **【转账备注】** 转账备注，用户收款时可见该备注信息，UTF8编码，最多允许32个字符
     * - notify_url string(256): [选填] **【通知地址】** 异步接收微信支付结果通知的回调地址，通知url必须为公网可访问的URL，必须为HTTPS，不能携带参数。
     * - user_recv_perception string: [选填] **【用户收款感知】** 用户收款时感知到的收款原因将根据转账场景自动展示默认内容。如有其他展示需求，可在本字段传入。各场景展示的默认内容和支持传入的内容，可查看产品文档了解。
     * - transfer_scene_report_infos array[TransferSceneReportInfo]: [选填] **【转账场景报备信息】** 各转账场景下需报备的内容，商户需要按照所属转账场景规则传参，详见[转账场景报备信息字段说明](https://iwiki.woa.com/p/4013774588)。
     * - transfer_scene_report_infos.info_type string(15): [必填] **【信息类型】** 不能超过15个字符，商户所属转账场景下的信息类型，此字段内容为固定值，需严格按照 [转账场景报备信息字段说明](https://iwiki.woa.com/p/4013774588) 传参。
     * - transfer_scene_report_infos.info_content string(32): [必填] **【信息内容】** 不能超过32个字符，商户所属转账场景下的信息内容，商户可按实际业务场景自定义传参，需严格按照 [转账场景报备信息字段说明](https://iwiki.woa.com/p/4013774588) 传参。
     *
     * 请参考上述字段列表从 request 中构造 TransferToUserRequest 类型，并根据描述对特定字段进行加密，加密函数使用 Utility.encrypt(this.wechatpayPublicKey, text)
     * @return HTTP 请求报文String
     */
    private String createRequestBody(TransferToUserRequest request) {
        TransferToUserRequest body = new TransferToUserRequest();
        body.appid = request.appid;
        body.outBillNo = request.outBillNo;
        body.transferSceneId = request.transferSceneId;
        body.openid = request.openid;
        if (request.userName != null) {
            body.userName = request.userName; // TODO: 需要加密
        }
        body.transferAmount = request.transferAmount;
        body.transferRemark = request.transferRemark;
        if (request.notifyUrl != null) {
            body.notifyUrl = request.notifyUrl;
        }
        if (request.userRecvPerception != null) {
            body.userRecvPerception = request.userRecvPerception;
        }
        if (request.transferSceneReportInfos != null) {
            body.transferSceneReportInfos = request.transferSceneReportInfos;
        }
        return Utility.toJson(body);
    }

    private String buildAuthorization(String method, String uri, String body) {
        return Utility.buildAuthorization(wechatPayConfig.getMchId(), wechatPayConfig.getSerialNo(),  Utility.loadPrivateKeyFromPath(wechatPayConfig.getPrivateKeyPath()), method, uri, body);
    }

    private Response sendHttpRequest(String uri, String body) throws IOException {
        Request.Builder builder = new Request.Builder().url(host + uri);
        builder.addHeader("Accept", "application/json");
        builder.addHeader("Wechatpay-Serial", wechatPayConfig.getPublicKeyId());
        builder.addHeader("Authorization", buildAuthorization(method, uri, body));
        builder.addHeader("Content-Type", "application/json");
        RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), body);
        builder.method(method, requestBody);

        Request request = builder.build();
        OkHttpClient client = new OkHttpClient.Builder().build();
        return client.newCall(request).execute();
    }

    private String validateResponse(Response response) {
        String body = "";
        if (response.body() != null) {
            try {
                BufferedSource source = response.body().source();
                body = source.readUtf8();
            } catch (IOException e) {
                throw new RuntimeException(String.format("An error occurred during reading response body. Status: %d", response.code()), e);
            }
        }

        if (response.code() >= 200 && response.code() < 300) {
            // 2XX 成功，继续验证应答签名
            Headers headers = response.headers();
            Utility.validateResponse(wechatPayConfig.getPublicKeyId(),Utility.loadPublicKeyFromPath(wechatPayConfig.getPublicKeyPath()) , headers, body);
            return body;
        }

        // TODO: 根据错误码执行不同的处理
        throw new UnsupportedOperationException(String.format("接口请求错误，StatusCode: %d, Body: %s",
                response.code(), body));
    }
    /**
     * 商家转账-发起转账-解析回包结果
     * - out_bill_no string(32): [必填] **【商户单号】** 商户系统内部的商家单号，要求此参数只能由数字、大小写字母组成，在商户系统内部唯一
     * - transfer_bill_no string(64): [必填] **【微信转账单号】** 微信转账单号，微信商家转账系统返回的唯一标识
     * - create_time string: [必填] **【单据创建时间】** 单据受理成功时返回，按照使用rfc3339所定义的格式，格式为yyyy-MM-DDThh:mm:ss+TIMEZONE
     * - state string: [必填] **【单据状态】** 商家转账订单状态
     * - fail_reason string: [选填] **【失败原因】** 订单已失败或者已退资金时，会返回 [订单失败原因](https://iwiki.woa.com/p/4013774966)
     * - package_info string: [选填] **【跳转领取页面的package信息】** 跳转微信支付收款页的package信息，[APP调起用户确认收款](https://iwiki.woa.com/p/4012719576) 或者 [JSAPI调起用户确认收款](https://iwiki.woa.com/p/4012716430) 时需要使用的参数。<br/>单据创建后，**用户24小时内不领取将过期关闭**，建议拉起用户确认收款页面前，先查单据状态：如单据状态为待收款用户确认，可用之前的package信息拉起；单据到终态时需更换单号重新发起转账。
     *
     * @return TransferToUserResponse
     */
    private TransferToUserResponse parseResponse(String body) {
        // 商家转账-发起转账-解析回包结果 生成示例，需选中该注释，通过 @demo 指令触发
        TransferToUserResponse response = Utility.fromJson(body, TransferToUserResponse.class);
        return response;
    }

    static class Utility {
        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();

        public static String toJson(Object object) { return gson.toJson(object); }

        public static <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException {
            return gson.fromJson(json, classOfT);
        }

        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);
            }
        }

        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);
            }
        }

        public static PrivateKey loadPrivateKeyFromPath(String keyPath) {
            return loadPrivateKeyFromString(readKeyStringFromPath(keyPath));
        }

        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);
            }
        }

        public static PublicKey loadPublicKeyFromPath(String keyPath) {
            return loadPublicKeyFromString(readKeyStringFromPath(keyPath));
        }

        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);
        }

        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);
            }
        }

        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);
        }

        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);
            }
        }

        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);
        }

        public static String urlEncode(String content) {
            try {
                return URLEncoder.encode(content, StandardCharsets.UTF_8.name());
            } catch (UnsupportedEncodingException e) {
                throw new RuntimeException(e);
            }
        }

        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));
            }
        }
    }

    /**
     * TransferToUserResponse
     */
    public static class TransferToUserResponse {

        /**
         * 商户单号
         * 说明：商户系统内部的商家单号，要求此参数只能由数字、大小写字母组成，在商户系统内部唯一
         */
        @SerializedName("out_bill_no")
        public String outBillNo;

        /**
         * 微信转账单号
         * 说明：微信转账单号，微信商家转账系统返回的唯一标识
         */
        @SerializedName("transfer_bill_no")
        public String transferBillNo;

        /**
         * 单据创建时间
         * 说明：单据受理成功时返回，按照使用rfc3339所定义的格式，格式为yyyy-MM-DDThh:mm:ss+TIMEZONE
         */
        @SerializedName("create_time")
        public String createTime;

        /**
         * 单据状态
         * 说明：商家转账订单状态
         */
        @SerializedName("state")
        public TransferBillStatus state;

        /**
         * 失败原因
         * 说明：订单已失败或者已退资金时，会返回 [订单失败原因](https://iwiki.woa.com/p/4013774966)
         */
        @SerializedName("fail_reason")
        public String failReason;

        /**
         * 跳转领取页面的package信息
         * 说明：跳转微信支付收款页的package信息，[APP调起用户确认收款](https://iwiki.woa.com/p/4012719576) 或者 [JSAPI调起用户确认收款](https://iwiki.woa.com/p/4012716430) 时需要使用的参数。  单据创建后，**用户24小时内不领取将过期关闭**，建议拉起用户确认收款页面前，先查单据状态：如单据状态为待收款用户确认，可用之前的package信息拉起；单据到终态时需更换单号重新发起转账。
         */
        @SerializedName("package_info")
        public String packageInfo;
    }
    /**
     * TransferBillStatus
     */
    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
    }

    /**
     * TransferSceneReportInfo
     */
    public static class TransferSceneReportInfo {

        /**
         * 信息类型
         * 说明：不能超过15个字符，商户所属转账场景下的信息类型，此字段内容为固定值，需严格按照 [转账场景报备信息字段说明](https://iwiki.woa.com/p/4013774588) 传参。
         */
        @SerializedName("info_type")
        public String infoType;

        /**
         * 信息内容
         * 说明：不能超过32个字符，商户所属转账场景下的信息内容，商户可按实际业务场景自定义传参，需严格按照 [转账场景报备信息字段说明](https://iwiki.woa.com/p/4013774588) 传参。
         */
        @SerializedName("info_content")
        public String infoContent;
    }

    /**
     * TransferToUserRequest
     */
    public static class TransferToUserRequest {

        /**
         * 商户AppID
         * 说明：是微信开放平台和微信公众平台为开发者的应用程序(APP、小程序、公众号、企业号corpid即为此AppID)提供的一个唯一标识。此处，可以填写这四种类型中的任意一种APPID，但请确保该appid与商户号有绑定关系。详见：[普通商户模式开发必要参数说明](https://iwiki.woa.com/p/4013070756)。
         */
        @SerializedName("appid")
        public String appid;

        /**
         * 商户单号
         * 说明：商户系统内部的商家单号，要求此参数只能由数字、大小写字母组成，在商户系统内部唯一
         */
        @SerializedName("out_bill_no")
        public String outBillNo;

        /**
         * 转账场景ID
         * 说明：该笔转账使用的转账场景，可前往“商户平台-产品中心-商家转账”中申请。如：1001-现金营销
         */
        @SerializedName("transfer_scene_id")
        public String transferSceneId;

        /**
         * 收款用户OpenID
         * 说明：用户在商户appid下的唯一标识。发起转账前需获取到用户的OpenID，获取方式详见 [参数说明](https://iwiki.woa.com/p/4012068676)。
         */
        @SerializedName("openid")
        public String openid;

        /**
         * 收款用户姓名
         * 说明：收款方真实姓名。需要加密传入，支持标准RSA算法和国密算法，公钥由微信侧提供。 转账金额 >= 2,000元时，该笔明细必须填写 若商户传入收款用户姓名，微信支付会校验收款用户与输入姓名是否一致，并提供电子回单
         */
        @SerializedName("user_name")
        public String userName;

        /**
         * 转账金额
         * 说明：转账金额单位为“分”。
         */
        @SerializedName("transfer_amount")
        public Long transferAmount;

        /**
         * 转账备注
         * 说明：转账备注，用户收款时可见该备注信息，UTF8编码，最多允许32个字符
         */
        @SerializedName("transfer_remark")
        public String transferRemark;

        /**
         * 通知地址
         * 说明：异步接收微信支付结果通知的回调地址，通知url必须为公网可访问的URL，必须为HTTPS，不能携带参数。
         */
        @SerializedName("notify_url")
        public String notifyUrl;

        /**
         * 用户收款感知
         * 说明：用户收款时感知到的收款原因将根据转账场景自动展示默认内容。如有其他展示需求，可在本字段传入。各场景展示的默认内容和支持传入的内容，可查看产品文档了解。
         */
        @SerializedName("user_recv_perception")
        public String userRecvPerception;

        /**
         * 转账场景报备信息
         * 说明：各转账场景下需报备的内容，商户需要按照所属转账场景规则传参，详见[转账场景报备信息字段说明](https://iwiki.woa.com/p/4013774588)。
         */
        @SerializedName("transfer_scene_report_infos")
        public List<TransferSceneReportInfo> transferSceneReportInfos;
    }
}
