微信服务商模式(电商收付通)合单支付APIV3完整Demo,可直接使用,适用于(H5、JSAPI、H5、App、小程序)

这篇具有很好参考价值的文章主要介绍了微信服务商模式(电商收付通)合单支付APIV3完整Demo,可直接使用,适用于(H5、JSAPI、H5、App、小程序)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

😊 @ 作者: 一恍过去
💖 @ 主页: https://blog.csdn.net/zhuocailing3390
🎊 @ 社区: Java技术栈交流
🎉 @ 主题: 微信服务商模式(电商收付通)合单支付APIV3完整Demo,可直接使用,适用于(H5、JSAPI、H5、App、小程序)
⏱️ @ 创作时间: 2022年08月29日

微信服务商模式(电商收付通)合单支付APIV3完整Demo,可直接使用,适用于(H5、JSAPI、H5、App、小程序)

1、前置准备

使用微信合单支付需要先开通电商收付通功能。
文档API: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/index.shtml

开通步骤: 进入「微信支付服务商平台->产品中心->合作工具箱」,开通「电商收付通」

微信服务商模式(电商收付通)合单支付APIV3完整Demo,可直接使用,适用于(H5、JSAPI、H5、App、小程序)

2、引入POM

        <!--微信支付SDK-->
        <dependency>
            <groupId>com.github.wechatpay-apiv3</groupId>
            <artifactId>wechatpay-apache-httpclient</artifactId>
            <version>0.4.7</version>
        </dependency>

3、配置Yaml

wxpay:
  #应用编号
  appId: xxxx
  #商户号
  mchId: xxx
  # APIv2密钥
  apiKey: xxxx
  # APIv3密钥
  apiV3Key: xxx
  # 微信支付V3-url前缀
  baseUrl: https://api.mch.weixin.qq.com/v3
  # 合单支付通知回调, pjm6m9.natappfree.cc 为内网穿透地址
  combineNotifyUrl: http://pjm6m9.natappfree.cc/pay/combinePayNotify
  # 退款通知回调, pjm6m9.natappfree.cc 为内网穿透地址
  refundNotifyUrl: http://pjm6m9.natappfree.cc/pay/refundNotify
  # 密钥路径,resources根目录下
  keyPemPath: apiclient_key.pem
  #商户证书序列号
  serialNo: xxxxx

4、配置密钥文件

在商户/服务商平台的”账户中心" => “API安全” 进行API证书、密钥的设置,API证书主要用于获取“商户证书序列号”以及“p12”、“key.pem”、”cert.pem“证书文件,j将获取的apiclient_key.pem文件放在项目的resources目录下。
微信服务商模式(电商收付通)合单支付APIV3完整Demo,可直接使用,适用于(H5、JSAPI、H5、App、小程序)

5、配置PayConfig


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: LiHuaZhi
 * @Date: 2022/7/3 22:08
 * @Description:
 **/
@Component
@Data
@Slf4j
@ConfigurationProperties(prefix = "wxpay")
public class WechatPayConfig {
    /**
     * 应用编号
     */
    private String appId;
    /**
     * 商户号
     */
    private String mchId;
    /**
     * 服务商商户号
     */
    private String slMchId;
    /**
     * APIv2密钥
     */
    private String apiKey;
    /**
     * APIv3密钥
     */
    private String apiV3Key;
    /**
     * 合单支付通知回调地址
     */
    private String combineNotifyUrl;
    /**
     * 退款回调地址
     */
    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);
    }


    /**
     * 获取支付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();
    }
}

6、定义统一枚举

CombinePayUrlEnum :


import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * @author 
 */

@AllArgsConstructor
@Getter
public enum CombinePayUrlEnum {


	/**
	 * native
	 */
	NATIVE("native"),
	/**
	 * app
	 */
	APP("app"),
	/**
	 * h5
	 */
	H5("h5"),
	/**
	 *  jsapi
	 */
	JSAPI("jsapi"),

	/**
	 *  小程序jsapi
	 */
	SUB_JSAPI("sub_jsapi"),

	/**
	 * 查询订单
	 */
	ORDER_QUERY_BY_NO("/combine-transactions/out-trade-no/"),

	/**
	 * 关闭订单
	 */
	CLOSE_ORDER_BY_NO("/combine-transactions/out-trade-no/%s/close"),

	/**
	 * 申请退款
	 */
	DOMESTIC_REFUNDS("/refund/domestic/refunds"),

	/**
	 * 查询单笔退款
	 */
	DOMESTIC_REFUNDS_QUERY("/refund/domestic/refunds/"),

	/**
	 * 申请交易账单
	 */
	TRADE_BILLS("/bill/tradebill"),

	/**
	 * 申请资金账单
	 */
	FUND_FLOW_BILLS("/bill/fundflowbill"),

	/**
	 * 申请单个子商户资金账单
	 */
	SUB_MERCHANT_FUND_FLOW_BILLS("/bill/sub-merchant-fundflowbill"),

	/**
	 * 合单支付APIV3
	 */
	COMBINE_TRANSACTIONS("/combine-transactions/");


	/**
	 * 类型
	 */
	private final String type;
}

7、封装统一请求处理

WechatPayRequest:


import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpEntity;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.io.IOException;

/**
 * @Description:
 **/
@Component
@Slf4j
public class WechatPayRequest {
    @Resource
    private CloseableHttpClient wxPayClient;
    public  String wechatHttpGet(String url) {
        try {
            // 拼接请求参数
            HttpGet httpGet = new HttpGet(url);
            httpGet.setHeader("Accept", "application/json");

            //完成签名并执行请求
            CloseableHttpResponse response = wxPayClient.execute(httpGet);

            return getResponseBody(response);
        }catch (Exception e){
            throw new RuntimeException(e.getMessage());
        }
    }

    public  String wechatHttpPost(String url,String paramsStr) {
        try {
            HttpPost httpPost = new HttpPost(url);
            StringEntity entity = new StringEntity(paramsStr, "utf-8");
            entity.setContentType("application/json");
            httpPost.setEntity(entity);
            httpPost.setHeader("Accept", "application/json");

            CloseableHttpResponse response = wxPayClient.execute(httpPost);
            return getResponseBody(response);
        }catch (Exception e){
            throw new RuntimeException(e.getMessage());
        }
    }

   private String getResponseBody(CloseableHttpResponse response) throws IOException {

            //响应体
           HttpEntity entity = response.getEntity();
           String body = entity==null?"":EntityUtils.toString(entity);
            //响应状态码
            int statusCode = response.getStatusLine().getStatusCode();

            //处理成功,204是,关闭订单时微信返回的正常状态码
            if (statusCode == HttpStatus.SC_OK || statusCode == HttpStatus.SC_NO_CONTENT) {
                log.info("成功, 返回结果 = " + body);
            } else {
                String msg = "微信支付请求失败,响应码 = " + statusCode + ",返回结果 = " + body;
                log.error(msg);
                throw new RuntimeException(msg);
            }
            return body;
    }
}

8、回调校验器

WechatPayValidator:


import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.*;

/**
 * @Description:
 **/
@Slf4j
public class WechatPayValidator {
    /**
     * 应答超时时间,单位为分钟
     */
    private static final long RESPONSE_EXPIRED_MINUTES = 5;
    private final Verifier verifier;
    private final String requestId;
    private final String body;


    public WechatPayValidator(Verifier verifier, String requestId, String body) {
        this.verifier = verifier;
        this.requestId = requestId;
        this.body = body;
    }

    protected static IllegalArgumentException parameterError(String message, Object... args) {
        message = String.format(message, args);
        return new IllegalArgumentException("parameter error: " + message);
    }

    protected static IllegalArgumentException verifyFail(String message, Object... args) {
        message = String.format(message, args);
        return new IllegalArgumentException("signature verify fail: " + message);
    }

    public final boolean validate(HttpServletRequest request)  {
        try {
            //处理请求参数
            validateParameters(request);

            //构造验签名串
            String message = buildMessage(request);

            String serial = request.getHeader(WECHAT_PAY_SERIAL);
            String signature = request.getHeader(WECHAT_PAY_SIGNATURE);

            //验签
            if (!verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature)) {
                throw verifyFail("serial=[%s] message=[%s] sign=[%s], request-id=[%s]",
                        serial, message, signature, requestId);
            }
        } catch (IllegalArgumentException e) {
            log.warn(e.getMessage());
            return false;
        }

        return true;
    }

    private  void validateParameters(HttpServletRequest request) {

        // NOTE: ensure HEADER_WECHAT_PAY_TIMESTAMP at last
        String[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP};

        String header = null;
        for (String headerName : headers) {
            header = request.getHeader(headerName);
            if (header == null) {
                throw parameterError("empty [%s], request-id=[%s]", headerName, requestId);
            }
        }

        //判断请求是否过期
        String timestampStr = header;
        try {
            Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestampStr));
            // 拒绝过期请求
            if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= RESPONSE_EXPIRED_MINUTES) {
                throw parameterError("timestamp=[%s] expires, request-id=[%s]", timestampStr, requestId);
            }
        } catch (DateTimeException | NumberFormatException e) {
            throw parameterError("invalid timestamp=[%s], request-id=[%s]", timestampStr, requestId);
        }
    }

    private  String buildMessage(HttpServletRequest request)  {
        String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);
        String nonce = request.getHeader(WECHAT_PAY_NONCE);
        return timestamp + "\n"
                + nonce + "\n"
                + body + "\n";
    }

    private  String getResponseBody(CloseableHttpResponse response) throws IOException {
        HttpEntity entity = response.getEntity();
        return (entity != null && entity.isRepeatable()) ? EntityUtils.toString(entity) : "";
    }

    /**
     * 对称解密,异步通知的加密数据
     * @param resource 加密数据
     * @param apiV3Key apiV3密钥
     * @param type 1-支付,2-退款
     * @return
     */
    public static  Map<String, Object> decryptFromResource(String resource,String apiV3Key,Integer type) {

        String msg = type==1?"支付成功":"退款成功";
        log.info(msg+",回调通知,密文解密");
        try {
        //通知数据
        Map<String, String> resourceMap = JSONObject.parseObject(resource, new TypeReference<Map<String, Object>>() {
        });
        //数据密文
        String ciphertext = resourceMap.get("ciphertext");
        //随机串
        String nonce = resourceMap.get("nonce");
        //附加数据
        String associatedData = resourceMap.get("associated_data");

        log.info("密文: {}", ciphertext);
        AesUtil aesUtil = new AesUtil(apiV3Key.getBytes(StandardCharsets.UTF_8));
        String resourceStr = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),
                nonce.getBytes(StandardCharsets.UTF_8),
                ciphertext);

        log.info(msg+",回调通知,解密结果 : {}", resourceStr);
        return JSONObject.parseObject(resourceStr, new TypeReference<Map<String, Object>>(){});
    }catch (Exception e){
        throw new RuntimeException("回调参数,解密失败!");
        }
    }
}

9、回调Body内容处理工具

HttpUtils:


import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;

/**
 * @Description:
 **/
public class HttpUtils {

    /**
     * 将通知参数转化为字符串
     * @param request
     * @return
     */
    public static String readData(HttpServletRequest request) {
        BufferedReader br = null;
        try {
            StringBuilder result = new StringBuilder();
            br = request.getReader();
            for (String line; (line = br.readLine()) != null; ) {
                if (result.length() > 0) {
                    result.append("\n");
                }
                result.append(line);
            }
            return result.toString();
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

微信服务商模式(电商收付通)合单支付APIV3完整Demo,可直接使用,适用于(H5、JSAPI、H5、App、小程序)

10、封装统一代码

10.1、统一合单下单处理接口

CombinePayController:

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.TypeReference;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import com.lhz.demo.model.enums.CombinePayUrlEnum;
import com.lhz.demo.pay.WechatPayConfig;
import com.lhz.demo.pay.WechatPayRequest;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;

/**
 * @Description:
 * 
 **/
 @Api(tags = "合单支付接口(API3)")
@RestController
@RequestMapping("/combine")
@Slf4j
public class PayController {

    @Resource
    private WechatPayConfig wechatPayConfig;

    @Resource
    private WechatPayRequest wechatPayRequest;

	/**
     * 无需应答签名
     */
    @Resource
    private CloseableHttpClient wxPayNoSignClient;

	// 公众号APPID
    private static final String WX_JSAPI_SUB_APPID = "xxxxxxxx";
	// 小程序APPID
    private static final String WX_JSAPI_MINI_SUB_APPID = "xxxxxxxx";
    // 模拟特约商户子商户号,实际情况:应该写在表中,与商家表进行映射,根据订单动态获取
    private static  final String subMchId ="xxx";
    
    /**
     * type:h5、jsapi、app、native、sub_jsapi
     * H5支付时,需要在商户平台--"产品中心"--"开发配置"自行配置支付提交的域名
     *
     * @param type
     * @return
     */
    @ApiOperation(value = "统一下单-统一接口", notes = "统一下单-统一接口")
    @ApiOperationSupport(order = 10)
    @GetMapping("/transactions")
    public Map<String, Object> transactions(String type) {
        int combineTradeNo = new Random().nextInt(999999999);
        log.info("统一下单API,支付方式:{},合单订单号:{}", type,combineTradeNo);

        // 统一参数封装
        Map<String, Object> params = new HashMap<>(8);
        params.put("combine_appid", wechatPayConfig.getAppId());
        params.put("combine_mchid", wechatPayConfig.getMchId());
        // 合单商户订单号
        params.put("combine_out_trade_no", combineTradeNo+"");
        params.put("notify_url", wechatPayConfig.getCombineNotifyUrl());

        // 场景信息
        Map<String, Object> sceneInfoMap = new HashMap<>(4);
        // 客户端IP
        sceneInfoMap.put("payer_client_ip", "127.0.0.1");
        // 商户端设备号(门店号或收银设备ID)
        sceneInfoMap.put("device_id", "127.0.0.1");

        // 除H5与JSAPI有特殊参数外,其他的支付方式都一样
        if (type.equals(CombinePayUrlEnum.H5.getType())) {

            Map<String, Object> h5InfoMap = new HashMap<>(4);
            // 场景类型:iOS, Android, Wap
            h5InfoMap.put("type", "IOS");
            sceneInfoMap.put("h5_info", h5InfoMap);
        } else if (type.equals(CombinePayUrlEnum.JSAPI.getType()) || type.equals(CombinePayUrlEnum.SUB_JSAPI.getType())) {
            Map<String, Object> payerMap = new HashMap<>(4);
            // 用户在服务商appid下的唯一标识,测试使用默认值 o6wzFwzBMdZDr-2VgR_ZsB1d0mYk(公众号应用的)
            // 小程序下的openId暂无,所以无法测试
            // 实际情况,应该是由前端获取openId值
            payerMap.put("sub_openid", "o6wzFwzBMdZDr-2VgR_ZsB1d0mYk");
            params.put("combine_payer_info", payerMap);
        }

        params.put("scene_info", sceneInfoMap);

        // TODO 模拟两个订单
        List<Map<String, Object>> subOrders = new ArrayList<>();
        for (int a=1;a<=2;a++) {
            // 子单信息
            Map<String, Object> subOrderMap = new HashMap<>(4);
            if (type.equals(CombinePayUrlEnum.JSAPI.getType()) || type.equals(CombinePayUrlEnum.SUB_JSAPI.getType())) {
                // TODO 小程序或者公众号支付时,需要传入子应用的appId,并且在服务商平台的特约商户管理中进行授权
                // 判断是否小程序还是服务号支付
                String subAppId = WX_JSAPI_SUB_APPID;
                if (type.equals(CombinePayUrlEnum.SUB_JSAPI.getType())) {
                    // 发起JSAPI的小程序
                    subAppId = WX_JSAPI_MINI_SUB_APPID;
                }

                // 子商户申请的应用ID,若combine_payer_info参数中sub_openid有传的情况下,sub_appid必填
                subOrderMap.put("sub_appid", subAppId);
            }

            int outTradeNo = new Random().nextInt(999999999);
            // 子单商户订单号
            subOrderMap.put("out_trade_no", outTradeNo + "");
            // 发起商户号
            subOrderMap.put("mchid",wechatPayConfig.getMchId());
            // 	二级商户商户号(特约商户),由微信支付生成并下发。
            subOrderMap.put("sub_mchid",subMchId);
            // 附加信息
            subOrderMap.put("attach", "附加信息");
            // 商品描述
            subOrderMap.put("description", "测试商品");
            // 金额信息
            Map<String, Object> amountMap = new HashMap<>(4);
            // 金额单位为分
            amountMap.put("total_amount", 1);
            amountMap.put("currency", "CNY");
            subOrderMap.put("amount", amountMap);

            // 结算信息
            Map<String, Object> settleInfoMap = new HashMap<>(4);
            settleInfoMap.put("profit_sharing",false);
            // 单笔订单最高补差金额
            settleInfoMap.put("subsidy_amount",5000);
            subOrderMap.put("settle_info", settleInfoMap);

            // 子订单列表
            subOrders.add(subOrderMap);
        }
        params.put("sub_orders",subOrders);

        String paramsStr = JSON.toJSONString(params);
        log.info("请求参数 ===> {}" + paramsStr);

        // 重写type值,因为小程序会多一个下划线(sub_type)
        String[] split = type.split("_");
        String newType = split[split.length - 1];
        String resStr = wechatPayRequest.wechatHttpPost(wechatPayConfig.getBaseUrl().concat(CombinePayUrlEnum.COMBINE_TRANSACTIONS.getType().concat(newType)), paramsStr);
        Map<String, Object> resMap = JSONObject.parseObject(resStr, new TypeReference<Map<String, Object>>() {
        });
        Map<String, Object> signMap = paySignMsg(resMap, type);
        resMap.put("type", type);
        resMap.put("signMap", signMap);
        return resMap;
    }

    private Map<String, Object> paySignMsg(Map<String, Object> map, String type) {
        // 设置签名信息,Native与H5不需要
        if (type.equals(CombinePayUrlEnum.H5.getType()) || type.equals(CombinePayUrlEnum.NATIVE.getType())) {
            return null;
        }

        long timeMillis = System.currentTimeMillis();
        String appId = wechatPayConfig.getAppId();
        String timeStamp = timeMillis / 1000 + "";
        String nonceStr = timeMillis + "";
        String prepayId = map.get("prepay_id").toString();
        String packageStr = "prepay_id=" + prepayId;

        // 公共参数
        Map<String, Object> resMap = new HashMap<>();
        resMap.put("nonceStr", nonceStr);
        resMap.put("timeStamp", timeStamp);

        // JSAPI、SUB_JSAPI(小程序)
        if (type.equals(CombinePayUrlEnum.JSAPI.getType()) || type.equals(CombinePayUrlEnum.SUB_JSAPI.getType())) {
            resMap.put("appId", appId);
            resMap.put("package", packageStr);
            // 使用字段appId、timeStamp、nonceStr、package进行签名
            String paySign = createSign(resMap);
            resMap.put("paySign", paySign);
            resMap.put("signType", "HMAC-SHA256");
        }
        // APP
        if (type.equals(CombinePayUrlEnum.APP.getType())) {
            resMap.put("appid", appId);
            resMap.put("prepayid", prepayId);
            // 使用字段appId、timeStamp、nonceStr、prepayId进行签名
            String sign = createSign(resMap);
            resMap.put("package", "Sign=WXPay");
            resMap.put("partnerid", wechatPayConfig.getMchId());
            resMap.put("sign", sign);
            resMap.put("signType", "HMAC-SHA256");
        }
        return resMap;
    }
    /**
     * 获取加密数据
     */
    private String createSign(Map<String, Object> params) {
        try {
            Map<String, Object> treeMap = new TreeMap<>(params);
            List<String> signList = new ArrayList<>(5);
            for (Map.Entry<String, Object> entry : treeMap.entrySet()) {
                signList.add(entry.getKey() + "=" + entry.getValue());
            }
            String signStr = String.join("&", signList);

            signStr = signStr + "&key=" + wechatPayConfig.getApiV3Key();
            System.out.println(signStr);

            Mac sha = Mac.getInstance("HmacSHA256");
            SecretKeySpec secretKey = new SecretKeySpec(wechatPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8), "HmacSHA256");
            sha.init(secretKey);
            byte[] array = sha.doFinal(signStr.getBytes(StandardCharsets.UTF_8));
            StringBuilder sb = new StringBuilder();
            for (byte item : array) {
                sb.append(Integer.toHexString((item & 0xFF) | 0x100), 1, 3);
            }
            signStr = sb.toString().toUpperCase();
            System.out.println(signStr);

            return signStr;
        } catch (Exception e) {
            throw new RuntimeException("加密失败!");
        }
    }
}

10.2 、其他处理(退款、查询、取消订单等)接口

包含接口:根据订单号查询订单关闭(取消)订单申请退款查询单笔退款信息申请交易账单申请资金账单申请单个子商户资金账单下载账单

CombinePayController:

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.TypeReference;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import com.lhz.demo.model.enums.CombinePayUrlEnum;
import com.lhz.demo.pay.WechatPayConfig;
import com.lhz.demo.pay.WechatPayRequest;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;

/**
 * @Description:
 **/
@Api(tags = "合单支付接口(API3)")
@RestController
@RequestMapping("/combine")
@Slf4j
public class PayController {

    @Resource
    private WechatPayConfig wechatPayConfig;

    @Resource
    private WechatPayRequest wechatPayRequest;

	/**
     * 无需应答签名
     */
    @Resource
    private CloseableHttpClient wxPayNoSignClient;
    
    // 模拟特约商户子商户号,实际情况:应该写在表中,与商家表进行映射,根据订单动态获取
    private static  final String subMchId ="xxx";

    /**
     * "根据合单商户订单号,查询订单接口",如果要查询单个订单可以调用,普通的订单查询接口(transactions)
     * @param combineOutTradeNo
     * @return
     */
    @ApiOperation(value = "根据合单订单号-统一接口", notes = "根据合单订单号-统一接口")
    @ApiOperationSupport(order = 15)
    @GetMapping("/combineTransactions/{combineOutTradeNo}")
    public  Map<String, Object> transactionsByOrderNo(@PathVariable("combineOutTradeNo") String combineOutTradeNo) {

        // TODO 如果是扫码支付时,该接口就很有必要,应该前端通过轮询的方式请求该接口查询订单是否支付成功

        log.info("根据合单订单号查询订单,合单号: {}", combineOutTradeNo);

        String url = wechatPayConfig.getBaseUrl().concat(CombinePayUrlEnum.ORDER_QUERY_BY_NO.getType().concat(combineOutTradeNo));

        String res = wechatPayRequest.wechatHttpGet(url);
        log.info("查询订单结果:{}", res);

        Map<String, Object> resMap = JSONObject.parseObject(res, new TypeReference<Map<String, Object>>() {
        });

        String outTradeNo = resMap.get("combine_out_trade_no").toString();
        String appId = resMap.get("combine_appid").toString();
        String mchId = resMap.get("combine_mchid").toString();
        // 支付后才返回参数
        Object payer = resMap.get("combine_payer_info");

        log.info("outTradeNo:" + outTradeNo);
        log.info("appId:" + appId);
        log.info("mchId:" + mchId);
        log.info("payer:" + payer);

        // 子订单数据
        String subOrders = resMap.get("sub_orders").toString();
        log.info("subOrders:" + subOrders);

        if(subOrders!=null){
            List<Map<String, Object>> subOrderList = JSONObject.parseObject(subOrders, new TypeReference<List<Map<String, Object>>>() {
            });
            log.info("============== 子订单信息: ==============");
            for(Map<String, Object> subOrderMap:subOrderList) {
                String subOrderNo = subOrderMap.get("out_trade_no").toString();
                String subOrderTransactionId = subOrderMap.get("transaction_id").toString();
                String subOrderMchId = subOrderMap.get("mchid").toString();
                String subOrderSubMchId = subOrderMap.get("sub_mchid").toString();
                // 支付后才返回参数
                Object subOrderAttach = subOrderMap.get("attach");
                /**
                 *         交易状态,枚举值:
                 *         SUCCESS:支付成功
                 *         REFUND:转入退款
                 *         NOTPAY:未支付
                 *         CLOSED:已关闭
                 *         REVOKED:已撤销(仅付款码支付会返回)
                 *         USERPAYING:用户支付中(仅付款码支付会返回)
                 *         PAYERROR:支付失败(仅付款码支付会返回)
                 */
                String subOrderTradeState = subOrderMap.get("trade_state").toString();

                log.info("subOrderNo:" + subOrderNo);
                log.info("subOrderTransactionId:" + subOrderTransactionId);
                log.info("subOrderMchId:" + subOrderMchId);
                log.info("subOrderTradeState:" + subOrderTradeState);
                log.info("subOrderAttach:" + subOrderAttach);
                log.info("subMchId:" + subOrderSubMchId);
            }
        }

        return resMap;
    }

    /**
     * 关闭(取消)订单
     * @param combineOutTradeNo
     * @return
     */
    @ApiOperation(value = "合单关闭(取消)订单-统一接口", notes = "合单关闭(取消)订单-统一接口")
    @ApiOperationSupport(order = 20)
    @PostMapping("/combineCloseOrder/{combineOutTradeNo}")
    public void closeOrder(@PathVariable("combineOutTradeNo") String combineOutTradeNo) {

        // TODO 用于在客户下单后,不进行支付,取消订单的场景
        log.info("根据合单订单号查询订单,合单号: {}", combineOutTradeNo);

        String url = String.format(CombinePayUrlEnum.CLOSE_ORDER_BY_NO.getType(), combineOutTradeNo);
        url = wechatPayConfig.getBaseUrl().concat(url);

        // 设置参数
        Map<String, Object> params = new HashMap<>(2);
        params.put("combine_appid", wechatPayConfig.getAppId());

        // TODO 模拟两个订单
        List<Map<String, Object>> subOrders = new ArrayList<>();
        for (int a=1;a<=2;a++) {
            // 子单信息
            Map<String, Object> subOrderMap = new HashMap<>(4);

            // 此处进行模拟生成,实际情况下,应该由前端进行传递,而`combineOutTradeNo`则是根据`outTradeNo`从数据库中获取
            int outTradeNo = new Random().nextInt(999999999);
            // 子单商户订单号
            subOrderMap.put("out_trade_no", outTradeNo + "");
            // 发起商户号
            subOrderMap.put("mchid",wechatPayConfig.getMchId());
            // 	二级商户商户号(特约商户),由微信支付生成并下发。
            subOrderMap.put("sub_mchid",subMchId);

            subOrders.add(subOrderMap);
        }
        params.put("sub_orders",subOrders);


        String paramsStr = JSON.toJSONString(params);
        log.info("请求参数 ===> {}" + paramsStr);

        String res = wechatPayRequest.wechatHttpPost(url,paramsStr);
    }


    /**
     * 申请退款
     * @param orderNo
     */
    @ApiOperation(value = "申请退款-统一接口", notes = "申请退款-统一接口")
    @ApiOperationSupport(order = 25)
    @PostMapping("/refundOrder/{orderNo}")
    public void refundOrder(@PathVariable("orderNo") String orderNo) {

        log.info("根据订单号申请退款,订单号: {}", orderNo);

        String url = wechatPayConfig.getBaseUrl().concat(CombinePayUrlEnum.DOMESTIC_REFUNDS.getType());

        // 设置参数
        Map<String, Object> params = new HashMap<>(2);
        // 订单编号
        params.put("out_trade_no", orderNo);
        params.put("sub_mchid", subMchId);
        // 退款单编号 - 自定义
        int outRefundNo = new Random().nextInt(999999999);
        log.info("退款申请号:{}",outRefundNo);
        params.put("out_refund_no",outRefundNo+"");
        // 退款原因
        params.put("reason","申请退款");
        // 退款通知回调地址
        params.put("notify_url", wechatPayConfig.getRefundNotifyUrl());

        Map<String, Object>  amountMap =new HashMap<>();
        //退款金额,单位:分
        amountMap.put("refund", 1);
        //原订单金额,单位:分
        amountMap.put("total", 1);
        //退款币种
        amountMap.put("currency", "CNY");
        params.put("amount", amountMap);

        String paramsStr = JSON.toJSONString(params);
        log.info("请求参数 ===> {}" + paramsStr);


        String res = wechatPayRequest.wechatHttpPost(url,paramsStr);

        log.info("退款结果:{}",res);
    }

    /**
     * 查询单笔退款信息
     * @param refundNo
     * @return
     */
    @ApiOperation(value = "查询单笔退款信息-统一接口", notes = "查询单笔退款信息-统一接口")
    @ApiOperationSupport(order = 30)
    @GetMapping("/queryRefundOrder/{refundNo}")
    public  Map<String, Object> queryRefundOrder(@PathVariable("refundNo") String refundNo) {

        log.info("根据订单号查询退款订单,订单号: {}", refundNo);

        String url = wechatPayConfig.getBaseUrl().concat(CombinePayUrlEnum.DOMESTIC_REFUNDS_QUERY.getType().concat(refundNo))
                .concat("?sub_mchid=").concat(subMchId);

        String res = wechatPayRequest.wechatHttpGet(url);
        log.info("查询退款订单结果:{}",res);

        Map<String, Object> resMap = JSONObject.parseObject(res, new TypeReference<Map<String, Object>>(){});

        String successTime = resMap.get("success_time").toString();
        String refundId = resMap.get("refund_id").toString();
        /**
         * 款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,可前往商户平台-交易中心,手动处理此笔退款。
         * 枚举值:
         * SUCCESS:退款成功
         * CLOSED:退款关闭
         * PROCESSING:退款处理中
         * ABNORMAL:退款异常
         */
        String status = resMap.get("status").toString();

        /**
         * 枚举值:
         * ORIGINAL:原路退款
         * BALANCE:退回到余额
         * OTHER_BALANCE:原账户异常退到其他余额账户
         * OTHER_BANKCARD:原银行卡异常退到其他银行卡
         */
        String channel = resMap.get("channel").toString();
        String userReceivedAccount = resMap.get("user_received_account").toString();

        log.info("successTime:"+successTime);
        log.info("channel:"+channel);
        log.info("refundId:"+refundId);
        log.info("status:"+status);
        log.info("userReceivedAccount:"+userReceivedAccount);

        // TODO 在查询单笔退款信息时,可以再去查询一次订单的状态,保证该订单已经退款完毕了

        return resMap;
    }

    /**
     * 申请交易账单
     * @param billDate 格式yyyy-MM-dd 仅支持三个月内的账单下载申请 ,如果传入日期未为当天则会出错
     * @param billType 分为:ALL、SUCCESS、REFUND
     * ALL:返回当日所有订单信息(不含充值退款订单)
     * SUCCESS:返回当日成功支付的订单(不含充值退款订单)
     * REFUND:返回当日退款订单(不含充值退款订单)
     * @return
     */
    @ApiOperation(value = "申请交易账单-统一接口", notes = "申请交易账单-统一接口")
    @ApiOperationSupport(order = 35)
    @GetMapping("/tradeBill")
    public  String tradeBill(@RequestParam("billDate") String billDate, @RequestParam("billType")  String billType) {

        log.info("申请交易账单,billDate:{},billType:{}", billDate,billType);

        String url = wechatPayConfig.getBaseUrl().concat(CombinePayUrlEnum.TRADE_BILLS.getType())
                .concat("?bill_date=").concat(billDate).concat("&bill_type=").concat(billType);
        // 填则默认返回服务商下的交易或退款数据,下载某个子商户下的交易或退款数据,则该字段必填
        url = url.concat("&sub_mchid=").concat(subMchId);

        String res = wechatPayRequest.wechatHttpGet(url);
        log.info("查询退款订单结果:{}",res);

        Map<String, Object> resMap = JSONObject.parseObject(res, new TypeReference<Map<String, Object>>(){});

        String downloadUrl = resMap.get("download_url").toString();

        return downloadUrl;
    }

    /**
     *
     * @param billDate 格式yyyy-MM-dd 仅支持三个月内的账单下载申请,如果传入日期未为当天则会出错
     * @param accountType 分为:BASIC、OPERATION、FEES
     * BASIC:基本账户
     * OPERATION:运营账户
     * FEES:手续费账户
     * @return
     */
    @ApiOperation(value = "申请资金账单-统一接口", notes = "申请资金账单-统一接口")
    @ApiOperationSupport(order = 40)
    @GetMapping("/fundFlowBill")
    public String fundFlowBill(@RequestParam("billDate") String billDate, @RequestParam("accountType")  String accountType) {

        log.info("申请交易账单,billDate:{},accountType:{}", billDate,accountType);

        String url = wechatPayConfig.getBaseUrl().concat(CombinePayUrlEnum.FUND_FLOW_BILLS.getType())
                .concat("?bill_date=").concat(billDate).concat("&account_type=").concat(accountType);

        String res = wechatPayRequest.wechatHttpGet(url);
        log.info("查询退款订单结果:{}",res);

        Map<String, Object> resMap = JSONObject.parseObject(res, new TypeReference<Map<String, Object>>(){});

        String downloadUrl = resMap.get("download_url").toString();

        return downloadUrl;
    }

    /**
     *
     * @param billDate 格式yyyy-MM-dd 仅支持三个月内的账单下载申请,如果传入日期未为当天则会出错
     * @param accountType 分为:BASIC、OPERATION、FEES
     * BASIC:基本账户
     * OPERATION:运营账户
     * FEES:手续费账户
     * @return
     */
    @ApiOperation(value = "申请单个子商户资金账单-统一接口", notes = "申请单个子商户资金账单-统一接口")
    @ApiOperationSupport(order = 40)
    @GetMapping("/subFundFlowBill")
    public String subMerchantFundFlowBill(@RequestParam("billDate") String billDate, @RequestParam("accountType")  String accountType) {

        log.info("申请单个子商户资金账单,billDate:{},accountType:{}", billDate,accountType);

        String url = wechatPayConfig.getBaseUrl().concat(CombinePayUrlEnum.FUND_FLOW_BILLS.getType())
                .concat("?bill_date=").concat(billDate).concat("&account_type=").concat(accountType)
                .concat("&sub_mchid=").concat(billDate).concat("&algorithm=").concat("AEAD_AES_256_GCM");

        String res = wechatPayRequest.wechatHttpGet(url);
        log.info("查询退款订单结果:{}",res);

        Map<String, Object> resMap = JSONObject.parseObject(res, new TypeReference<Map<String, Object>>(){});

        String downloadBillCount = resMap.get("download_bill_count").toString();
        String downloadBillList = resMap.get("download_bill_list").toString();
        List<Map<String, Object>> billListMap = JSONObject.parseObject(downloadBillList, new TypeReference<List<Map<String, Object>>>(){});
        String downloadUrl = billListMap.get(0).get("download_url").toString();

        log.info("downloadBillCount="+downloadBillCount);
        log.info("downloadUrl="+downloadUrl);

        return downloadUrl;
    }


    @ApiOperation(value = "下载账单-统一接口", notes = "下载账单-统一接口")
    @ApiOperationSupport(order = 45)
    @GetMapping("/downloadBill")
    public void downloadBill(String downloadUrl) {

        log.info("下载账单,下载地址:{}",downloadUrl);

        HttpGet httpGet = new HttpGet(downloadUrl);
        httpGet.addHeader("Accept", "application/json");

        CloseableHttpResponse response =null;
        try {
            //使用wxPayClient发送请求得到响应
            response =  wxPayNoSignClient.execute(httpGet);

            String body = EntityUtils.toString(response.getEntity());

            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode == 200 || statusCode == 204) {
                log.info("下载账单,返回结果 = " + body);
            } else {
                throw new RuntimeException("下载账单异常, 响应码 = " + statusCode+ ", 下载账单返回结果 = " + body);
            }
            // TODO 将body内容转为excel存入本地或者输出到浏览器,演示存入本地
            writeStringToFile(body);

        }catch (Exception e){
            throw new RuntimeException(e.getMessage());
        }
        finally {
            if(response!=null) {
                try {
                    response.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private void writeStringToFile(String body) {
        FileWriter fw = null;
        try {
            String filePath = "C:\\Users\\lhz12\\Desktop\\wxPay.txt";
            fw = new FileWriter(filePath, true);
            BufferedWriter bw = new BufferedWriter(fw);
            bw.write(body);
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            try {
                if(fw!=null) {
                    fw.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

11、支付/退款回调通知

NotifyController:


import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.TypeReference;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import com.lhz.demo.pay.WechatPayConfig;
import com.lhz.demo.pay.WechatPayValidator;
import com.lhz.demo.utils.HttpUtils;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;


/**
 * @Author: 
 * @Description:
 **/
@Api(tags = "回调接口(API3)")
@RestController
@Slf4j
public class NotifyController {

    @Resource
    private WechatPayConfig wechatPayConfig;

    @Resource
    private Verifier verifier;

    private final ReentrantLock lock = new ReentrantLock();

    @ApiOperation(value = "合单支付回调", notes = "合单支付回调")
    @ApiOperationSupport(order = 5)
    @PostMapping("/combinePayNotify")
    public Map<String, String> combinePayNotify(HttpServletRequest request, HttpServletResponse response) {
        log.info("合单支付回调");
        if(lock.tryLock()) {
            // 处理通知参数
            Map<String,Object> bodyMap = getNotifyBody(request);
            if(bodyMap==null){
                return falseMsg(response);
            }

            log.warn("=========== 在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱 ===========");
            try {
                // 解密resource中的通知数据
                String resource = bodyMap.get("resource").toString();
                Map<String, Object> resourceMap = WechatPayValidator.decryptFromResource(resource, wechatPayConfig.getApiV3Key(),1);
                log.info("通知参数:{}", JSON.toJSONString(resourceMap));
                // 合单号s
                String combineOutTradeNo = resourceMap.get("combine_out_trade_no").toString();
                // 合单商户appid
                String combineAppid = resourceMap.get("combine_appid").toString();
                // 合单商户号
                String combineMchid = resourceMap.get("combine_mchid").toString();

                // 子订单信息
                // 子订单数据
                String subOrders = resourceMap.get("sub_orders").toString();
                log.info("subOrders:" + subOrders);
                if(subOrders!=null){
                    List<Map<String, Object>> subOrderList = JSONObject.parseObject(subOrders, new TypeReference<List<Map<String, Object>>>() {
                    });
                    log.info("============== 子订单信息: ==============");
                    for(Map<String, Object> subOrderMap:subOrderList) {
                        String subOrderNo = subOrderMap.get("out_trade_no").toString();
                        String subOrderTransactionId = subOrderMap.get("transaction_id").toString();
                        String subOrderMchId = subOrderMap.get("mchid").toString();
                        String subOrderSubMchId = subOrderMap.get("sub_mchid").toString();
                        String subOrderSuccessTime = subOrderMap.get("success_time").toString();
                        // 支付后才返回参数
                        Object subOrderAttach = subOrderMap.get("attach");
                        /**
                         *         交易状态,枚举值:
                         *         SUCCESS:支付成功
                         *         REFUND:转入退款
                         *         NOTPAY:未支付
                         *         CLOSED:已关闭
                         *         REVOKED:已撤销(仅付款码支付会返回)
                         *         USERPAYING:用户支付中(仅付款码支付会返回)
                         *         PAYERROR:支付失败(仅付款码支付会返回)
                         */
                        String subOrderTradeState = subOrderMap.get("trade_state").toString();

                        log.info("subOrderNo:" + subOrderNo);
                        log.info("subOrderTransactionId:" + subOrderTransactionId);
                        log.info("subOrderMchId:" + subOrderMchId);
                        log.info("subOrderTradeState:" + subOrderTradeState);
                        log.info("subOrderAttach:" + subOrderAttach);
                        log.info("subOrderSubMchId:" + subOrderSubMchId);
                        log.info("subOrderSuccessTime:" + subOrderSuccessTime);
                    }
                }

                // TODO 根据订单号,做幂等处理,并且在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱
                log.warn("=========== 根据订单号,做幂等处理 ===========");
            } finally {
                //要主动释放锁
                lock.unlock();
            }
        }

        //成功应答
        return trueMsg(response);
    }

    @ApiOperation(value = "退款回调", notes = "退款回调")
    @ApiOperationSupport(order = 5)
    @PostMapping("/refundNotify")
    public Map<String, String> refundNotify(HttpServletRequest request, HttpServletResponse response) {
        log.info("退款回调");
        if(lock.tryLock()) {
            // 处理通知参数
            Map<String,Object> bodyMap = getNotifyBody(request);
            if(bodyMap==null){
                return falseMsg(response);
            }

            log.warn("=========== 在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱 ===========");

            try {
                // 解密resource中的通知数据
                String resource = bodyMap.get("resource").toString();
                Map<String, Object> resourceMap = WechatPayValidator.decryptFromResource(resource, wechatPayConfig.getApiV3Key(),2);
                log.info("通知参数:{}", JSON.toJSONString(resourceMap));
                String orderNo = resourceMap.get("out_trade_no").toString();
                String transactionId = resourceMap.get("transaction_id").toString();

                // TODO 根据订单号,做幂等处理,并且在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱

                log.warn("=========== 根据订单号,做幂等处理 ===========");
            } finally {
                //要主动释放锁
                lock.unlock();
            }
        }

        //成功应答
        return trueMsg(response);
    }

    private Map<String,Object> getNotifyBody(HttpServletRequest request){
        //处理通知参数
        String body = HttpUtils.readData(request);
        log.info("退款回调参数:{}",body);

        // 转换为Map
        Map<String, Object> bodyMap = JSONObject.parseObject(body, new TypeReference<Map<String, Object>>(){});
        // 微信的通知ID(通知的唯一ID)
        String notifyId = bodyMap.get("id").toString();

        // 验证签名信息
        WechatPayValidator wechatPayValidator
                = new WechatPayValidator(verifier, notifyId, body);
        if(!wechatPayValidator.validate(request)){

            log.error("通知验签失败");
            return null;
        }
        log.info("通知验签成功");
        return bodyMap;
    }

    private Map<String, String> falseMsg(HttpServletResponse response){
        Map<String, String> resMap = new HashMap<>(8);
        //失败应答
        response.setStatus(500);
        resMap.put("code", "ERROR");
        resMap.put("message", "通知验签失败");
        return resMap;
    }

    private Map<String, String> trueMsg(HttpServletResponse response){
        Map<String, String> resMap = new HashMap<>(8);
        //成功应答
        response.setStatus(200);
        resMap.put("code", "SUCCESS");
        resMap.put("message", "成功");
        return resMap;
    }
}

微信服务商模式(电商收付通)合单支付APIV3完整Demo,可直接使用,适用于(H5、JSAPI、H5、App、小程序)文章来源地址https://www.toymoban.com/news/detail-490371.html

到了这里,关于微信服务商模式(电商收付通)合单支付APIV3完整Demo,可直接使用,适用于(H5、JSAPI、H5、App、小程序)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 微信支付apiV3异常:The corresponding provider for the merchant already exists

    异常信息 原因 这个错误是微信SDK抛出的,这是因为微信支付apiV3的RSAConfig重复build导致,即RSAConfig要保证是 单例 才不会导致报错。 参数说明 mchId:商户号 privateKey:商户号密钥 mchSerialNo:商户证书号 apiV3Key:apiV3密钥 建议 可以把商户配置参数使用数据库保存,服务启动的时

    2024年02月11日
    浏览(60)
  • 微信服务商分账思路剖析、设计流程及源码实现

    🔭 嗨,您好 👋 我是 vnjohn,在互联网企业担任 Java 开发,CSDN 优质创作者 📖 推荐专栏:Spring、MySQL、Nacos、Java,后续其他专栏会持续优化更新迭代 🌲文章所在专栏:微信体系 🤔 我当前正在学习微服务领域、云原生领域、消息中间件等架构、原理知识 💬 向我询问任何您

    2023年04月15日
    浏览(33)
  • 店群批量返款工具,最新支付宝批量转账的解决方案-收付无忧系统

    淘系、拼多多、抖店等卖家涉及需要 批量返款的场景较多,如“BD返款、好评返现、免单活动批量转账” 等业务场景,电商人一天够忙的了,批量返款一直是困扰卖家的大问题。 商家批量退款、退费、补差、退邮费等就会爆增,或者淘宝阿里、拼多、抖店等商家需要批量返

    2024年02月04日
    浏览(136)
  • java微信小程序支付-回调(Jsapi-APIv3)

            准备:  接入前准备-小程序支付 | 微信支付商户平台文档中心 准备好了就可以获得( 第二点里需要的参数 ):         参数1 商户号 merchantId:xxxxxx(全是数字)         参数2 商户APIV3密钥 apiV3key:xxxxxxx(32位字母数字大小写串,开发自己准备的)         参

    2024年02月08日
    浏览(53)
  • JAVA 小程序支付+服务商分账

    产品介绍: 服务商分账,主要用于服务商帮助特约商户完成订单收单成功后的资金分配。 使用场景举例: 1、服务商抽成 在各个行业中,服务商为特约商户提供增值服务,服务商与特约商户协商,可以从特约商户的交易流水中抽取一定的手续费。 引用自微信服务商分账 流程

    2024年02月09日
    浏览(43)
  • 支付宝服务商第三方代发布小程序

      在 开放平台 创建第三方应用   获取第三方的支付宝公钥和私钥  创建模板小程序 通过你得到的模板小程序appid  下载   支付宝开发工具 上传版本 发布到标准版 然后发布审核到服务市场   在下面平台去管理授权小程序  服务商代商家小程序发布管理平台 https://consol

    2024年02月09日
    浏览(50)
  • 高防CDN如何保护电商平台的在线支付系统安全

    高防CDN如何保护电商平台的在线支付系统安全?随着移动互联网的快速发展,越来越多的用户选择在手机上进行购物和支付。这种形式的便利性和灵活性推动了电商平台的发展,但同时也给电商平台的安全带来了新的挑战。尤其是在线支付系统,由于其涉及用户的金融信息,

    2024年02月07日
    浏览(58)
  • 跨境防诈指南 | 了解美国电商持续遭遇的“超额支付”欺诈

    美国商业委员会的统计报告显示,2023年年1至6月,联邦贸易委员会(Federal Trade Commission,简称FTC)接到的商业诈骗投诉高达110万人次,损失金额高达44亿元,商业诈骗正以前所未有的猖獗横扫全美各地。     联邦贸易委员会市场部主任Greisman,在2023年9月22日一次网上简报会议

    2024年01月25日
    浏览(40)
  • IT服务商服务运营方案--PIGOSS BSM +TOC 服务加工具的新型运维模式

    该解决方案适用于各种数据中心端专业运维服务商,包括驻场服务商,MA服务商,ITO服务商,IDC服务商,云运维服务商等 PIGOSS 是专业服务商的共同选择 专业的服务团队离不开专业的技术平台和技术工具,PIGOSS TOC+BSM 产品成为一流专业服务商不约而同的专业选择。 服务运营模

    2024年02月09日
    浏览(56)
  • 【业务功能篇104】 补充【业务功能篇99】微服务-springcloud-springboot-电商订单模块--整合支付

    在前面我们业务功能篇98-99中,我们介绍了电商项目中的订单模块服务,那么最后就是需要进行支付动作,那么我们这里就通过订阅第三方平台支付宝的支付调用接口功能,来进一步完成订单提交后的支付动作,支付宝的接口使用可以登录官网开发指南详情去了解 在我们对应

    2024年02月09日
    浏览(50)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包