E企盈营销工具技术服务商 热线:4006-838-530

服务端微信小程序支付/退款详解

E企盈直播平台营销卖货系统

一.小程序支付参考小程序支付开发文档: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 secretjs_code是登录时获取的 codegrant_type是填写为 authorization_code发送一个get请求即可参数也比较容易理解,这样我们就完成了第一步获取到了用户的openid2、商户server调用支付统一下单,api参见公共api【统一下单API】统一下单接口地址:https://api.mch.weixin.qq.com/pay/unifiedorder提供代码片段:/** * 商户发起生成预付单请求(统一下单接口) * * @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进行签名。签名参数:字段名变量名必填类型示例值描述小程序IDappId是Stringwxd678efh567hg6787微信分配的小程序ID时间戳timeStamp是String1490840662时间戳从1970年1月1日00:00:00至今的秒数,即当前的时间随机串nonceStr是String5K8264ILTKCH16CQ2502SI8ZNMTM67VS随机字符串,不长于32位。推荐随机数生成算法数据包package是Stringprepay_id=wx2017033010242291fcfe0db70013231072统一下单接口返回的 prepay_id 参数值,提交格式如:prepay_id=wx2017033010242291fcfe0db70013231072签名方式signType是StringMD5签名类型,默认为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;}前端调起微信支付参数:参数类型必填说明timeStampString是时间戳从1970年1月1日00:00:00至今的秒数,即当前的时间nonceStrString是随机字符串,长度为32个字符以下。packageString是统一下单接口返回的 prepay_id 参数值,提交格式如:prepay_id=*signTypeString是签名类型,默认为MD5,支持HMAC-SHA256和MD5。注意此处需与统一下单的签名类型一致paySignString是签名,具体签名方案参见微信公众号支付帮助文档;组装前端所需要的参数通过统一下单接口直接返回给前端,前端调用wx.requestPayment(OBJECT)发起微信支付。4、商户server接收支付通知,api参见公共api【支付结果通知API】接口地址为【统一下单API】中提交的参数notify_url设置,如果链接无法访问,商户将无法接收到微信通知。/** * 微信支付回调 */@RequestMapping(value = “/notify”, produces = “application/xml; charset=UTF-8”)@ResponseBodypublic 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(请求需要双向证书。 详见证书使用)实现代码:/** * 申请退款 * * @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

赞(0) 打赏
未经允许不得转载:E企盈小程序开发-热线:4006-838-530 » 服务端微信小程序支付/退款详解
分享到: 更多 (0)
E企盈小程序直播营销卖货系统
E企盈直播平台营销卖货系统

评论 抢沙发

E企盈小程序开发

联系我们联系我们

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

支付宝扫一扫打赏

微信扫一扫打赏