一.小程序支付

参考小程序支付开发文档:https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=7_4&index=3

账号支持:小程序appid,小程序secret,商户号mchid,商户secret

服务端和微信支付系统主要交互:

1、小程序内调用登录接口,获取到用户的openid,api参见公共api【小程序登录API】

前端调用接口wx.login() 获取临时登录凭证(code)

通过code值获取openid地址:

https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code

请求参数:

参数 必填 说明
appid 小程序唯一标识
secret 小程序的 app secret
js_code 登录时获取的 code
grant_type 填写为 authorization_code

发送一个get请求即可参数也比较容易理解,这样我们就完成了第一步获取到了用户的openid

2、商户server调用支付统一下单,api参见公共api【统一下单API】

统一下单接口地址:

https://api.mch.weixin.qq.com/pay/unifiedorder

请求参数(有删减):

字段名

变量名

必填

类型

示例值

描述

小程序ID

appid

String(32)

wxd678efh567hg6787

微信分配的小程序ID

商户号

mch_id

String(32)

1230000109

微信支付分配的商户号

随机字符串

nonce_str

String(32)

5K8264ILTKCH16CQ2502SI8ZNMTM67VS

随机字符串,长度要求在32位以内。推荐随机数生成算法

签名

sign

String(32)

C380BEC2BFD727A4B6845133519F3AD6

通过签名算法计算得出的签名值,详见签名生成算法

商品描述

body

String(128)

腾讯充值中心-QQ会员充值

商品简单描述,该字段请按照规范传递,具体请见参数规定

商户订单号

out_trade_no

String(32)

20150806125346

商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-|*且在同一个商户号下唯一。详见商户订单号

标价金额

total_fee

Int

88

订单总金额,单位为分,详见支付金额

终端IP

spbill_create_ip

String(16)

123.12.12.123

APP和网页支付提交用户端ip。

通知地址

notify_url

String(256)

http://www.weixin.qq.com/wxpay/pay.php

异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。

交易类型

trade_type

String(16)

JSAPI

小程序取值如下:JSAPI,详细说明见参数规定

用户标识

openid

String(128)

oUpF8uMuAJO_M2pxb1Q9zNjWeS6o

trade_type=JSAPI,此参数必传,用户在商户appid下的唯一标识。openid如何获取,可参考【获取openid】。

提供代码片段:

/**
 * 商户发起生成预付单请求(统一下单接口)
 *
 * @param body      商品信息
 * @param orderId   商户自己的订单号
 * @param totalFee  支付价格(单位分)
 * @param openid    微信用户的openid(必须关注此公众号的openid)
 * @param notifyUrl 微信回调地址
 */
public Map<String, Object> unifiedOrder(String body, Long orderId, Long totalFee, String openid, String notifyUrl) {//微信账号校验
    checkConfig(weixinProperties);
    SortedMap<String, Object> params = new TreeMap<>();
    //小程序appid
    params.put("appid", "**************");
    //商户号
    params.put("mch_id", "**************");
    params.put("spbill_create_ip", getUserContext().getIp());
    params.put("trade_type", "JSAPI";
    params.put("nonce_str", StringUtil.getRandomStringByLength(24));
    params.put("notify_url", notifyUrl);
    params.put("body", body);
    params.put("out_trade_no", fictitiousOrderService.insertFictitiousOrder(orderId, body, totalFee).toString());
    params.put("total_fee", totalFee);
    params.put("openid", openid);
    //签名算法
    String sign = getSign(params);
    params.put("sign", sign);
    String xml = XmlHelper.mapToXml(params);
    try {//发送xmlPost请求
        String xmlStr = HttpUtil.xmlPost("https://api.mch.weixin.qq.com/pay/unifiedorder", xml);
        //加入微信支付日志
        payWechatLogService.insertPayWechatLog(Constants.PAY_UNIFIED_ORDER_RESULT_LOG, xmlStr);
        Map map = XmlHelper.xmlToMap(xmlStr);
        if (map != null && Constants.REQUEST_SUCCESS.equals(map.get("result_code")) && map.get("prepay_id") != null) {//返回二次签名前端调用微信信息
            Map<String, Object> result = secondarySign(map);
            return result;
        } else {//异常通知
            mqSendService.sendRobotMsg(String.format("微信消息-传入参数[%s];微信输出[%s]", JSON.toJSONString(params), JSON.toJSONString(map)),
                    "统一下单");
            throw serviceExceptionService.createServiceException(ExceptionConstants.UNIFIED_ORDER_INTERFACE_ERROR);
        }} catch (Exception e) {mqSendService.sendRobotMsg(String.format("微信消息-[%s]", e.getMessage()), "统一下单");
        //统一下单接口异常
        throw serviceExceptionService.createServiceException(ExceptionConstants.UNIFIED_ORDER_INTERFACE_ERROR);
    }
}

方法注意点:

参数out_trade_no商户订单号即自己数据库存储的订单号这里有个坑,统一下单接口不予许同一订单发起两次下单请求,但是我们业务常常有这个需要。解决的办法是下单时用商户订单号生成一个虚拟订单号,用虚拟支付单号去请求微信,微信回调时用返回的虚拟支付单号解析出商户的订单号,修改订单的支付状态。

微信下单和退款的接口请求方式都是将参数转为xml格式发送跑

/**
 * 获取签名 md5加密(微信支付必须用MD5加密) 获取支付签名
 */
public String getSign(SortedMap<String, Object> params) {StringBuffer sb = new StringBuffer();
    //所有参与传参的参数按照accsii排序(升序)
    Set es = params.entrySet();
    Iterator it = es.iterator();
    while (it.hasNext()) {Map.Entry entry = (Map.Entry) it.next();
        String k = (String) entry.getKey();
        Object v = entry.getValue();
        if (null != v && !"".equals(v) && !"sign".equals(k) && !"key".equals(k)) {sb.append(k + "=" + v + "&");
        }}//key后面接商户号的秘钥secret
    sb.append("key=" + "*************");
    String sign = EncryptUtil.MD5Encode(sb.toString(), "UTF-8").toUpperCase();
    return sign;
}

签名规则:

  • 所有的参数按照accsii排序(升序),
  • 所有参数按照key1=value1&key2=value2&...拼接成字符串
  • 最后在key后面拼上秘钥(注:该秘钥是商户秘钥不是小程序秘钥)
  • 通过MD5加密字符串后将所有的字符改为大写

微信在线签名校验工具地址:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=20_1

注:微信下单和退款的接口请求方式都是将参数转为xml格式发送post请求,提供xmlPost方法代码

/**
 * 微信提交xml参数
 */
public static String xmlPost(String url1, String xml) {try {// 创建url资源
        URL url = new URL(url1);
        // 建立http连接
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        // 设置允许输出
        conn.setDoOutput(true);

        conn.setDoInput(true);

        // 设置不用缓存
        conn.setUseCaches(false);
        // 设置传递方式
        conn.setRequestMethod("POST");
        // 设置维持长连接
        conn.setRequestProperty("Connection", "Keep-Alive");
        // 设置文件字符集:
        conn.setRequestProperty("Charset", "UTF-8");
        //转换为字节数组
        byte[] data = xml.getBytes();
        // 设置文件长度
        conn.setRequestProperty("Content-Length", String.valueOf(data.length));
        // 设置文件类型:
        conn.setRequestProperty("contentType", "text/xml");
        // 开始连接请求
        conn.connect();
        OutputStream out = conn.getOutputStream();
        // 写入请求的字符串
        out.write(data);
        out.flush();
        out.close();

        System.out.println(conn.getResponseCode());

        // 请求返回的状态
        if (conn.getResponseCode() == 200) {System.out.println("连接成功");
            // 请求返回的数据
            InputStream in = conn.getInputStream();
            String a = null;
            try {byte[] data1 = new byte[in.available()];
                in.read(data1);
                // 转成字符串
                a = new String(data1);
                System.out.println(a);
            } catch (Exception e1) {// TODO Auto-generated catch block
                e1.printStackTrace();
            }return a;
        } else {System.out.println("no++");
        }} catch (Exception e) {}return null;
}

3、商户server调用再次签名,api参见公共api【再次签名】

通过统一下单接口我们可以拿到prepay_id,将prepay_id组装成package进行签名。

签名参数:

字段名 变量名 必填 类型 示例值 描述
小程序ID appId String wxd678efh567hg6787 微信分配的小程序ID
时间戳 timeStamp String 1490840662 时间戳从1970年1月1日00:00:00至今的秒数,即当前的时间
随机串 nonceStr String 5K8264ILTKCH16CQ2502SI8ZNMTM67VS 随机字符串,不长于32位。推荐随机数生成算法
数据包 package String prepay_id=wx2017033010242291fcfe0db70013231072 统一下单接口返回的 prepay_id 参数值,提交格式如:prepay_id=wx2017033010242291fcfe0db70013231072
签名方式 signType String MD5 签名类型,默认为MD5,支持HMAC-SHA256和MD5。注意此处需与统一下单的签名类型一致

/**
 * 二次签名
 */
private Map<String, Object> secondarySign(Map map) {SortedMap<String, Object> secondarySignParam = new TreeMap<>();
    secondarySignParam.put("appId", weixinProperties.getMiniapp().getUser().getAppId());
    secondarySignParam.put("timeStamp", new Date().getTime() + "");
    secondarySignParam.put("nonceStr", StringUtil.getRandomStringByLength(24));
    secondarySignParam.put("package", "prepay_id=" + map.get("prepay_id").toString());
    secondarySignParam.put("signType", "MD5");
    String paySign = getSign(secondarySignParam);
    secondarySignParam.put("paySign", paySign);
    //签完名去掉appId防止暴露账号
    secondarySignParam.remove("appId");
    return secondarySignParam;
}

前端调起微信支付参数:

参数 类型 必填 说明
timeStamp String 时间戳从1970年1月1日00:00:00至今的秒数,即当前的时间
nonceStr String 随机字符串,长度为32个字符以下。
package String 统一下单接口返回的 prepay_id 参数值,提交格式如:prepay_id=*
signType String 签名类型,默认为MD5,支持HMAC-SHA256和MD5。注意此处需与统一下单的签名类型一致
paySign String 签名,具体签名方案参见微信公众号支付帮助文档;

组装前端所需要的参数通过统一下单接口直接返回给前端,前端调用wx.requestPayment(OBJECT)发起微信支付。

4、商户server接收支付通知,api参见公共api【支付结果通知API】

接口地址为【统一下单API】中提交的参数notify_url设置,如果链接无法访问,商户将无法接收到微信通知。

/**
 * 微信支付回调
 */
@RequestMapping(value = "/notify", produces = "application/xml; charset=UTF-8")
@ResponseBody
public void notify(HttpServletRequest request, HttpServletResponse response) throws Exception {BufferedReader reader = request.getReader();
    StringBuffer inputString = new StringBuffer();
    String line = "";
    while ((line = reader.readLine()) != null) {inputString.append(line);
    }payWechatLogService.insertPayWechatLog(Constants.PAY_SUCCESS_RESULT_LOG, inputString.toString());
    Map<String, String> map = XmlHelper.xmlToMap(inputString.toString());
    String return_code = map.get("return_code");
    //客户订单id(虚拟支付单号)
    String out_trade_no = map.get("out_trade_no");
    //微信支付订单号(流水号)
    String transaction_id = map.get("transaction_id");
    //todo 修改订单对应的状态
    //商户处理后同步返回给微信参数
    response.getWriter().print("<xml><return_code><![CDATA[" + return_code + "]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>");
}

回调成功给微信发送通知,不然微信会继续回调该接口

二.微信退款

参考小程序支付开发文档:https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=9_4

账号支持:小程序appid,商户号mchid,商户secret,退款证书,退款证书密码

系统中有了支付肯定会设及到退款

申请退款接口地址:https://api.mch.weixin.qq.com/secapi/pay/refund(请求需要双向证书。 详见证书使用)

请求参数:

字段名

变量名

必填

类型

示例值

描述

小程序ID

appid

String(32)

wx8888888888888888

微信分配的小程序ID

商户号

mch_id

String(32)

1900000109

微信支付分配的商户号

随机字符串

nonce_str

String(32)

5K8264ILTKCH16CQ2502SI8ZNMTM67VS

随机字符串,不长于32位。推荐随机数生成算法

签名

sign

String(32)

C380BEC2BFD727A4B6845133519F3AD6

签名,详见签名生成算法

微信订单号

transaction_id

二选一

String(32)

1217752501201407033233368018

微信生成的订单号,在支付通知中有返回

商户订单号

out_trade_no

String(32)

1217752501201407033233368018

商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-|*@ ,且在同一个商户号下唯一。

商户退款单号

out_refund_no

String(64)

1217752501201407033233368018

商户系统内部的退款单号,商户系统内部唯一,只能是数字、大小写字母_-|*@ ,同一退款单号多次请求只退一笔。

订单金额

total_fee

Int

100

订单总金额,单位为分,只能为整数,详见支付金额

退款金额

refund_fee

Int

100

退款总金额,订单总金额,单位为分,只能为整数,详见支付金额

退款资金来源

refund_account

String(30)

REFUND_SOURCE_RECHARGE_FUNDS

仅针对老资金流商户使用

REFUND_SOURCE_UNSETTLED_FUNDS---未结算资金退款(默认使用未结算资金退款)

REFUND_SOURCE_RECHARGE_FUNDS---可用余额退款

实现代码:

/**
 * 申请退款
 *
 * @param orderId       商户订单号
 * @param refundId      商户退款单号
 * @param totalFee      订单金额
 * @param refundFee     退款金额
 * @param refundAccount 退款资金来源(默认传 "REFUND_SOURCE_UNSETTLED_FUNDS")
 */
public Map<String, String> refund(Long orderId, String refundId, Long totalFee,
                                  Long refundFee, String refundAccount) {checkConfig(weixinProperties);
    SortedMap<String, Object> params = new TreeMap<>();
    params.put("appid", "************");
    params.put("mch_id", "************");
    params.put("nonce_str", StringUtil.getRandomStringByLength(24));
    //商户订单号和微信订单号二选一
    params.put("out_trade_no", fictitiousOrderService.findFictitiousIdByOrder(orderId));
    params.put("out_refund_no", refundId);
    params.put("total_fee", totalFee);
    params.put("refund_fee", refundFee);
    params.put("refund_account", refundAccount);
    //签名算法
    String sign = getSign(params);
    params.put("sign", sign);
    try {String xml = XmlHelper.mapToXml(params);
        String xmlStr = doRefund("https://api.mch.weixin.qq.com/secapi/pay/refund", xml);
        //加入微信支付日志
        payWechatLogService.insertPayWechatLog(Constants.PAY_REFUND_RESULT_LOG, xmlStr);
        Map map = XmlHelper.xmlToMap(xmlStr);
        if (map == null || !Constants.REQUEST_SUCCESS.equals(map.get("result_code"))) {//消息通知
            mqSendService.sendRobotMsg(String.format("微信消息-传入参数[%s];微信输出[%s]", JSON.toJSONString(params), JSON.toJSONString(map)),
                    "申请退款");
        }//未结算金额不足  使用余额退款
        if (map != null && Constants.REQUEST_FAIL.equals(map.get("result_code")) &&Constants.REQUEST_SUCCESS.equals(map.get("return_code")) && Constants.REFUND_NOT_ENOUGH_MONEY.equals(map.get("err_code")) &&Constants.REFUND_SOURCE_UNSETTLED_FUNDS.equals(refundAccount)) {refund(orderId, refundId, totalFee, refundFee, Constants.REFUND_SOURCE_RECHARGE_FUNDS);
        }return map;
    } catch (Exception e) {//微信退款接口异常
        mqSendService.sendRobotMsg(String.format("微信消息-传入参数[%s];异常信息-[%s]", JSON.toJSONString(params), e.getMessage()), "申请退款");
        throw serviceExceptionService.createServiceException(ExceptionConstants.PAY_REFUND_INTERFACE_RRROR);
    }
}

方法注意点:

其中getSign(params)获取签名方法和上面支付签名方法一致可共用;out_trade_no填写微信支付时对应的虚拟支付单号;这里我根据refund_account退款资金来源作了一个逻辑处理,退款资金优先退商户未结算资金,如果未结算资金不足退商户余额的资金。

退款请求及证书的使用:

/**
 * 申请退款
 */
public String doRefund(String url, String data) throws Exception {KeyStore keyStore = KeyStore.getInstance("PKCS12");
    FileInputStream is = new FileInputStream(new File("****证书文件存放的路劲*******"));
    try {keyStore.load(is, "********证书密码*******".toCharArray());
    } finally {is.close();
    }// Trust own CA and all self-signed certs
    SSLContext sslcontext = SSLContexts.custom().loadKeyMaterial(keyStore,
            "********证书密码*******".toCharArray()).build();
    // Allow TLSv1 protocol only
    SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslcontext,
            new String[]{"TLSv1"},
            null,
            SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER
    );
    CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(sslsf).build();
    try {HttpPost httpost = new HttpPost(url); // 设置响应头信息
        httpost.addHeader("Connection", "keep-alive");
        httpost.addHeader("Accept", "*/*");
        httpost.addHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
        httpost.addHeader("Host", "api.mch.weixin.qq.com");
        httpost.addHeader("X-Requested-With", "XMLHttpRequest");
        httpost.addHeader("Cache-Control", "max-age=0");
        httpost.addHeader("User-Agent", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0) ");
        httpost.setEntity(new StringEntity(data, "UTF-8"));
        CloseableHttpResponse response = httpclient.execute(httpost);
        try {HttpEntity entity = response.getEntity();

            String jsonStr = EntityUtils.toString(response.getEntity(), "UTF-8");
            EntityUtils.consume(entity);
            return jsonStr;
        } finally {response.close();
        }} finally {httpclient.close();
    }
}

证书密码如果没设置默认为商户号mchid

服务端微信小程序支付/退款详解相关推荐

  1. 微信小程序支付流程详解

    原创 Dr Hydra 码农参上 2020-11-22 11:00 收录于合集#微信开发技术3个 最近在工作中接入了一下微信小程序支付的功能,虽然说官方文档已经比较详细了,但在使用过程中还是踩了不少的 ...

  2. 微信小程序支付退款功能

    微信小程序支付退款功能 2022年02月做了一个微信小程序前端+ASP.NET后台的微信退款功能.功能的基本原理是使用JSAPI方式,对接之前的微信支付功能差不多.也是要签名,校验等.只不过它的退款传 ...

  3. php小程序地图处理,微信小程序 地图map详解及简单实例

    微信小程序 地图map 微信小程序map 地图属性名类型默认值说明longitudeNumber中心经度 latitudeNumber中心纬度 scaleNumber1缩放级别 markersArra ...

  4. 小程序向Java传值,微信小程序 页面传值详解

    微信小程序 页面传值详解 一. 跨页面传值. 1 . 用 navigator标签传值或 wx.navigator, 比如 这里将good_id=16 参数传入detail页面, 然后detail页面的 ...

  5. 微信小程序详解 php,微信小程序canvas基础详解

    canvas 元素用于在网页上绘制图形.HTML5 的 canvas 元素使用 JavaScript 在网页上绘制2D图像.本文主要和大家分享微信小程序canvas基础详解,希望能帮助到大家. 一.了 ...

  6. 微信小程序底部菜单详解

    微信小程序底部菜单详解 只需要在app.json里面修改配置,即可 {"pages":["pages/index/index","pages/logs ...

  7. 微信小程序详解 php,微信小程序列表开发详解

    本文主要和大家分享微信小程序列表开发详解,主要以代码的形式和大家分享,希望能帮助到大家. 一.知识点 (一).列表渲染 wx:for tip:wx:for="array"可以等于参 ...

  8. php 微信小程序 循环 多选,微信小程序 for 循环详解

    1,wx:for 在组件上使用wx:for控制属性绑定一个数组,即可使用数组中各项的数据重复渲染该组件.默认数组的当前项的下标变量名默认为index,数组当前项的变量名默认为item 事例如下: wx ...

  9. 微信小程序后台开发详解

    微信小程序后台开发 前言 开发环境 开发流程 项目整体结构 接口开发 项目部署 ip映射 Nginx反向代理 gunicorn+super多进程开启服务+进程监控 ssl证书 小程序常用功能 微信支付 ...

最新文章

  1. 计算机在人力资源管理中的应用论文,计算机人事管理论文
  2. mAP@.5 含义:
  3. python与办公自动化-用 Python 自动化办公,我与大神之间的差距一下就
  4. oracle连接本地数据库
  5. 如何通过css控制内容显示顺序 第二行的内容优先显示
  6. 点击率预测算法:FTRL
  7. SM_INTEGRATION_SRV
  8. Tarjan求lca
  9. spring-security-学习笔记-03-spring-security快速上手
  10. php ajax session,Ajax处理用户session失效
  11. 收藏十二:ExtJs
  12. java day20【字节流、字符流】
  13. oracle中分析函数range值范围,Oracle实战4(分析函数)
  14. CentOS7.0离线安装RHadoop
  15. 将APP发布到各大官方网站的方法,如华为、360手机助手、小米等
  16. SSH使用教程( Bitvise Tunnelier+Chrome+Proxy Switchy)
  17. request:fail 发生了 SSL 错误无法建立与该服务器的安全连接——openssl报漏洞该升级了
  18. 国药集团获得美国默沙东公司新冠口服药“莫诺拉韦”经销权和独家进口权 | 美通社头条...
  19. 最受玩家喜爱的十大游戏IP类型,你最喜欢哪个?
  20. uni-app:使用uni.downloadFile下载文件并保存到手机

热门文章

  1. “十四五”数字泉城建设应用场景
  2. LCD显示屏与OLED屏幕对比分析
  3. 《沟通的技术——让交流、会议与演讲更有效》一1.1 一切尽在计划之中
  4. 阿里实现Redis亿级存储的方案
  5. POI设置Excel下拉列表(数据有效性验证)
  6. 懂车帝上配置高的国产车为什么那么便宜?
  7. 云开发(微信-小程序)笔记(十四)---- 收藏,点赞(上)
  8. (79)FPGA减法器设计(半减法器)
  9. ASP.NET和ASP的区别?
  10. 网络安全——Webshell管理工具