Java 集成银联电子商务(海南)微信公众号支付

对接完银联的公众号支付,趁现在记忆还比较深刻,赶紧记录一下。

Maven 导入
<!--okhttp3-->
<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>3.6.0</version>
</dependency>

1.准备参数(由银联电子商务提供)

  • 商户号: mid
  • 终端号: tid
  • 机构商户号: instMid
  • 来源系统: msgSrc
  • 来源编号: msgSrcId
  • 通讯秘钥: key
订单号生成规范:

商户需自行生成merOrderId。此时merOrderId需要符合银商规范,以银商分配的4位来源编号(msgSrcId)作为订单号的前4位,且在商户系统中此订单号保证唯一。总长度需大于6位,小于32位。

签名规则:

签名支持 MD5 方式与 SHA256 方式(不上送 signType 字段时,默认 md5 方式),
计算sign 的输入数据为待签名字符串加上 key(即:通讯密钥),key 由网付前置平台分配。
在请求参数列表中,除去 sign 参数外,其他需要使用到的参数均为要签名的参数

2.接口地址

  • 银联统一下单URL 【 apiUrl_makeOrder】: https://******/netpay-portal/webpay/pay.do
  • 银联API 接口URL 【APIurl】: https://******/netpay-route-server/api/
  • 异步回调URL(一般为商户系统后端接口,用于处理订单状态)【notifyUrl】 :https://******/wechat/publicpay/notify
  • 同步回调URL(一般为公众号界面,用于支付成功后界面跳转)【returnUrl 】:https://******/#/payResult

3.支付流程

Java 集成银联电子商务(海南)微信公众号支付_第1张图片

4.统一下单支付

  • 银联的公众号支付其实很简单,只要在用户提交订单之后拼接出统一支付URL,然后诱导用户触发URL,之后就坐等支付结果回调,做好后续工作就OK了。
/***
     * 获取统一下单支付URL
     * @param merOrderId 订单编号
     * @param totalAmount 支付金额
     * @param isSeparate 是否分账
     * @param subMchId   分账商户号
     * @param proportion 分账比例
     * @return
     */
public String getPayUrl(int totalAmount, int isSeparate, String subMchId, Float proportion) {
        logger.info("获取支付url");
        // 组织请求报文,具体参数请参照接口文档,以下仅作模拟
        JSONObject json = new JSONObject();
        json.put("mid", mid);
        json.put("tid", tid);
        json.put("msgType", "WXPay.jsPay");
        json.put("msgSrc", msgSrc);
        json.put("instMid", instMid);
        String merOrderId = Util.genMerOrderId(msgSrcId); // 支付订单号需根据银联提供的规则生成
        json.put("merOrderId", merOrderId);  // 支付成功后订单编号会原样返回
        json.put("totalAmount", totalAmount);
        json.put("requestTimestamp", DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss"));
        
        // 当需要给子账号分账时,需拼接上分账信息
        json = separate(totalAmount, isSeparate, subMchId, proportion, json);
        
        json.put("notifyUrl", notifyUrl);
        json.put("returnUrl", returnUrl);
        // 验签,并拼接URL
        String url = Util.makeOrderRequest(json, md5Key, apiUrl_makeOrder);
        logger.info(url);
        return url;
    }

    

5.支付结果查询

/**
     * 支付结果查询方法
     * @param merOrderId  
     * @return
     */
    public JSONObject query(String merOrderId){
        JSONObject json = new JSONObject();
        json.put("mid", mid);
        json.put("tid", tid);
        json.put("msgType", "query");
        json.put("msgSrc", msgSrc);
        json.put("instMid", instMid);
        json.put("requestTimestamp", DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss"));
        json.put("merOrderId", merOrderId);
        Map<String,String> map = Util.jsonToMap(json);
        json.put("sign", Util.makeSign(md5Key,map));
        try {
            Response reResponse = OkHttpUtil.post(APIurl,json.toString());
            String StringTemp = reResponse.body().string();
            log.info("StringTemp:"+StringTemp);
            JSONObject jsonObject = JSONObject.fromObject(StringTemp);
            if("SUCCESS".equals(jsonObject.get("errCode"))){
                log.info("查询成功");
            } else {
                log.info("查询失敗");
                log.info(String.valueOf(jsonObject.get("errMsg")));
            }
            return jsonObject;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

6.支付成功退款

   /**
     * 支付成功退款方法
     * @param merOrderId
     * @param totalAmount
     * @param isSeparate
     * @param subMchId
     * @param proportion
     */
    private void refund( String merOrderId, Integer totalAmount,
    						Integer isSeparate, String subMchId, Float proportion){
        JSONObject json = new JSONObject();
        json.put("mid", mid);
        json.put("tid", tid);
        json.put("msgType", "refund");
        json.put("msgSrc", msgSrc);
        json.put("instMid", instMid);
        String refundOrderId = Util.genMerOrderId(msgSrcId);// 退款订单号需根据银联提供的规则生成
        json.put("requestTimestamp", DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss"));
        json.put("refundOrderId", refundOrderId);
        json.put("merOrderId", merOrderId);
        json.put("totalAmount", totalAmount);
        json.put("refundAmount", totalAmount);

        // 当需要给子账号分账时,需拼接上分账信息
        json = separate(totalAmount, isSeparate, subMchId, proportion, json);
        
        Map<String,String> map = Util.jsonToMap(json);
        json.put("sign", Util.makeSign(md5Key,map));
        log.info("退款:"+json);
        try {
            Response reResponse = OkHttpUtil.post(APIurl,json.toString());
            String  StringTemp = reResponse.body().string();
            log.info("StringTemp:"+StringTemp);
            JSONObject jsonObject = JSONObject.fromObject(StringTemp);
            if("SUCCESS".equals(jsonObject.get("errCode"))){
                log.info("退款成功");
            } else {
                log.info("退款失敗");
                log.info(String.valueOf(jsonObject.get("errMsg")));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

对接心得:

  • 对接第三方支付功能,要把第三方当成一个仅用于支付的工具,用Java的话来说就是一个支付对象。我们只需要知道怎么调用对象的方法,以及需要传入哪些参数和返回哪些值。
  • 关键点就在于要如何在异步回调的方法中对应已支付的订单。

附1:多商户号分账方法

/**
     * 多商户号分账(无需分账请忽略)
     * @param totalAmount
     * @param isSeparate
     * @param subMchId
     * @param proportion
     * @param json
     * @return
     */
private JSONObject separate(int totalAmount, int isSeparate, String subMchId, Float proportion, JSONObject json) {
        logger.info("是否启用分账功能(1-是,2-否):");
        logger.info(String.valueOf(isSeparate));
        logger.info("分账商户号:");
        logger.info(subMchId);
        logger.info("分账比例:");
        logger.info(String.valueOf(proportion));
        // 开启分账
        if(isSeparate == 1){
            json.put("divisionFlag", true); // 分账标记
            Float proportionFlaot = Float.valueOf(proportion);
            // 单位为分,不可以有小数点,最终的值要为INT
            Float subTotalAmountFloat = totalAmount * proportionFlaot / 100;
            Integer subTotalAmount = subTotalAmountFloat.intValue();
            Integer platformAmount = totalAmount - subTotalAmount;
            json.put("platformAmount", platformAmount); // 主商户号分账金额
            List<Map<String,Object>> subOrders = new ArrayList<>();
            Map<String,Object> map = new HashMap<>();
            map.put("mid",subMchId); // 子商户号
            map.put("totalAmount",subTotalAmount); //子商户号分账金额
            subOrders.add(map);
            json.put("subOrders", subOrders); // 子商户号
        }
        return json;
    }

附2:验签工具类


/**
 * Created by myinsert on 2019/8/25.
 * 验签方式有MD5和SHA256
 */
import net.sf.json.JSONObject;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateFormatUtils;

import javax.servlet.http.HttpServletRequest;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.*;

public class Util {

    public static String makeOrderRequest(JSONObject json, String md5Key, String apiUrl) {
        Map<String, String> params = jsonToMap(json);
        params.put("sign", makeSign(md5Key, params));
        return apiUrl + "?" + buildUrlParametersStr(params);
    }


    public static String makeSign(String md5Key, Map<String, String> params) {
        String preStr = buildSignString(params); // 把数组所有元素,按照“参数=参数值”的模式用“&”字符拼接成字符串
        String text = preStr + md5Key;
        if("SHA256".equalsIgnoreCase(params.get("signType"))){
            return DigestUtils.sha256Hex(getContentBytes(text));
        } else {
            return DigestUtils.md5Hex(getContentBytes(text)).toUpperCase();
        }
    }

    public static boolean checkSign(String md5Key, Map<String, String> params) {
        String sign = params.get("sign");

        if (StringUtils.isBlank(sign)) {
            return false;
        }

        String signV = makeSign(md5Key, params);

        return StringUtils.equalsIgnoreCase(sign, signV);
    }

    // 获取HttpServletRequest里面的参数,并decode
    public static Map<String, String> getRequestParams(HttpServletRequest request) {
        Map<String, String[]> params = request.getParameterMap();
        Map<String, String> params2 = new HashMap<>();
        for (String key : params.keySet()) {
            String[] values = params.get(key);
            if (values.length > 0) {
                params2.put(key, values[0]);
            }
        }
        return params2;
    }

    public static String genMerOrderId(String msgId) {
        String date = DateFormatUtils.format(new Date(), "yyyyMMddHHmmssSSS");
        String rand = RandomStringUtils.randomNumeric(7);
        return msgId + date + rand;
    }

    private static String buildUrlParametersStr(Map<String, String> paramMap) {
        Map.Entry entry;
        StringBuffer buffer = new StringBuffer();

        for (Iterator iterator = paramMap.entrySet().iterator(); iterator.hasNext(); ) {
            entry = (Map.Entry) iterator.next();

            buffer.append(entry.getKey().toString()).append("=");
            try {
                if (entry.getValue() != null && StringUtils.isNotBlank(entry.getValue().toString())) {
                    buffer.append(URLEncoder.encode(entry.getValue().toString(), "UTF-8"));
                }
            } catch (UnsupportedEncodingException e) {
            }

            buffer.append(iterator.hasNext() ? "&" : "");
        }

        return buffer.toString();
    }

    // 使json-lib来进行json到map的转换,fastjson有排序问题,不能用
    public static Map<String, String> jsonToMap(JSONObject json) {
        Map<String, String> map = new HashMap<>();
        for (Object key : json.keySet()) {
            String value = json.optString((String) key);
            map.put((String) key, value);
        }
        return map;
    }

    // 构建签名字符串
    private static String buildSignString(Map<String, String> params) {

        if (params == null || params.size() == 0) {
            return "";
        }

        List<String> keys = new ArrayList<>(params.size());

        for (String key : params.keySet()) {
            if ("sign".equals(key))
                continue;
            if (StringUtils.isEmpty(params.get(key)))
                continue;
            keys.add(key);
        }

        Collections.sort(keys);

        StringBuilder buf = new StringBuilder();

        for (int i = 0; i < keys.size(); i++) {
            String key = keys.get(i);
            String value = params.get(key);

            if (i == keys.size() - 1) {// 拼接时,不包括最后一个&字符
                buf.append(key + "=" + value);
            } else {
                buf.append(key + "=" + value + "&");
            }
        }

        return buf.toString();
    }

    // 根据编码类型获得签名内容byte[]
    private static byte[] getContentBytes(String content) {
        try {
            return content.getBytes("UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("签名过程中出现错误");
        }
    }

}

你可能感兴趣的:(Java 集成银联电子商务(海南)微信公众号支付)