一文实现微信小程序支付 史上最全版

前言

微信小程序在开发中大部分都是电商类的业务,电商必不可少的是支付环节,实现微信小程序支付看微信官方文档太晦涩(多数人都觉得官方人员写的烂),博主也是在看官方文档的路上迷失去了自己。这篇文章由博主实战采坑后总结的经验构成,尽量带着大家读懂官方文档,绕开前方大坑,哈哈哈哈哈

微信小程序支付必须有商户号,绑定商户平台才有微信支付功能,个人小程序是没有支付功能的。商户平台可使用商户营业执照申请

一、支付流程导读

1.支付系统交互流程

在开发前若时间充足,建议大家通篇阅读一下微信支付官方文档,对业务名称加以熟悉,看清楚接口参数名,是否必传,方便后面的开发。[微信支付文档,点我跳转] (https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=7_10&index=1)

微信支付业务流程图:
一文实现微信小程序支付 史上最全版_第1张图片

  1. 调用微信登录API接口获取用户openid。
  2. 将支付金额,回调地址,小程序appid等传入调用统一下单接口API。
  3. 微信官方生成预支付订单数据,包括订单id,支付金额等。
  4. 将返回数据进行签名,防止篡改。
  5. 将5个参数数据放回给前端调用Payment(输密码的面板)。
  6. 用户确认金额,输入密码。
  7. 微信进行密码鉴权。
  8. 微信回调你的业务系统,将支付是否成功通知给你,进行订单支付状态更新。此时你的接口必须放开权限,且公网可访问

二、开发前的准备

1.准备资料

  1. appid :小程序的id
  2. apikey : 可在小程序开发配置找到
  3. mchid : 商户号,商户平台可找到

2.引入SDK

微信支付官方给我们提供了很多工具类。SDK方法中有方法可以也没有业务订单数据的情况下实现跑通微信支付,给大家提供的一个测试类。很友好,里面提供了支付时需要用的工具类方法。
SDK下载地址 点我下载SDK


三、获取用户openid

1. 配置文件

weixin:
  # 小程序ID 	
  appid: wx08huxxxxxxxxxxx
  # 小程序秘钥
  appSecret: 95e2932xxxxxxxxxxxx
  # 商户秘钥
  apiKey: ymruurG3xxxxxxxxxx
   # 商户ID
  mchId: 1244xxxxxxxx
#支付回调通知地址 必须为公网可访问的域名地址 且权限放开
routine:
  notify: https:xxxxxx/wx/pay/notify

2. 获取用户openid

若不懂openid的请自行百度

首先前端同学需要请求微信服务获取一个登陆code 。然后带着登陆code请求后端接口

 	@GetMapping(value = "/login/{code}")
    @OperationLog(name = "获取用户openid", type = OperationLogType.ADD)
    @ApiOperation(value = "获取用户openid" , response = ApiResult.class)
    @ResponseBody
    public ApiResult login(@PathVariable String code) throws Exception {
        Map<String, String> result= wxAuthApiService.wxLogin(code);
        if (result.isEmpty()){
          return ApiResult.fail("获取用户小程序openid失败");
        }
        return ApiResult.ok(result);
    }

Service

public interface WxAuthApiService {
	// 获取获取用户小程序openid
    Map<String,String> wxLogin(String code) throws Exception;
}

实现类

/**
 * @Author 公众号:真香号
 * @Version 1.0
 * @Description
 */
@Slf4j
@Service
public class WxAuthApiServiceImpl implements WxAuthApiService {
  	// 小程序ID
  	@Value("${weixin.appid}")
    private String APPID;
    // 小程序秘钥
    @Value("${weixin.appSecret}")
    private String SECRET;
    
 	@Transactional(rollbackFor = Exception.class)
    @Override
    public Map<String, String> wxLogin(String code) throws Exception {
        Map<String, String> param = new ConcurrentHashMap<>();
        if (StringUtils.isEmpty(code)) {
            throw new BusinessException("获取用户openid失败,code 不能为空");
        }
        String url = String.format("https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code",
                APPID, SECRET, code);
        String result = HttpUtils.sendGet(url, null);
        log.info(result);
        JSONObject object = JSON.parseObject(result);
        if (object.getInteger("errcode") != null) {
            log.info("获取用户openid失败code:{},msg:{}", object.getInteger("errcode"), object.getString("errmsg"));
            throw new BusinessException("获取用户openid失败,errcode:" + object.getInteger("errcode") + "errmsg:" + object.getString("errmsg"));
        }
        String openId = object.getString("openid");
        // 会话密钥
        String sessionKey = object.getString("session_key");
		// 根据业务将openid与sessionKey 持久化存入数据库
 		param.put("openid", openId);
        param.put("sessionKey", sessionKey);
        return param;
  }
 

顺利拿到openid 以后就可以请求支付,因为微信支付必须确定订单的支付人openid。


四、支付流程

1. 支付接口

/**
     * 微信小程序 统一下单支付
     * @param request
     * @param response
     * @return
     */
    @PostMapping("/unifiedOrder")
    @ResponseBody
    @ApiOperation(value = "微信统一下单", response = ApiResult.class)
    public ApiResult unifiedOrder(HttpServletRequest request, HttpServletResponse response) throws Exception {
        log.info("---------统一下单--------");
        String openId = request.getParameter("openId");
        if (StringUtils.isEmpty(openId)) {
            throw  new BusinessException("openid 不能为空");
        }
        log.info("---------openId--------" + openId);
        Map<String, String> map = wxPayService.unifiedOrder(openId, IpUtil.getIpAddress(request), request);
        return ApiResult.ok(map);
    }

service

   /**
     *  统一下单接口
     * @param openid 用户标识
     * @param ip    请求ip
     * @param request 订单数据
     * @return
     */
    Map<String ,String> unifiedOrder(String openid, String ip, HttpServletRequest request) throws Exception;

实现类

/**
 * @Author 公众号:真香号
 * @Version 1.0
 * @Description
 */
@Service
@Slf4j
public class WxPayServiceImpl implements WxPayService {

    @Value("${weixin.appid}")
    private String APPID;
    @Value("${weixin.appSecret}")
    private String SECRET;
    @Value("${weixin.apiKey}")
    private String APIKEY;
    @Value("${weixin.mchId}")
    private String MCHID;

	 //交易类型
    public static final String TRADE_TYPE_JS = "JSAPI";
    public static final String FAIL = "FAIL";
    public static final String SUCCESS = "SUCCESS";
  	// 微信统一下单url
    public static final String UNIFIED_ORDER_URL = "https://api.mch.weixin.qq.com/pay/unifiedorder";
    // 支付回调地址
    @Value("${routine.notify}")
    public String NOTIFY_URL;
    
     /** 统一下单
     * @param openid 用户唯一标识
     * @param ip 请求方IP地址
     * @param request 请求数据
     * @return 已生成订单信息
     */
    @Override
    public Map<String, String> unifiedOrder(String openid, String ip, HttpServletRequest request) throws Exception {
        // 1. 拼接统⼀下单地址参数
        log.info("进入下单流程");
        Map<String, String> paramMap = new ConcurrentHashMap<String, String>(10);
        paramMap.put("appid", APPID);
        paramMap.put("mch_id", MCHID);
        // 商品信息
        paramMap.put("body", "test");
        paramMap.put("openid", openid);
        // 订单号
        paramMap.put("out_trade_no", request.getParameter("orderId"));
        // 随机字符串,长度要求在32位以内。
        paramMap.put("nonce_str", StringHelpUtils.generateNonceStr());
        // 以分为单位,1代表0.01元
        paramMap.put("total_fee", "1");
        paramMap.put("spbill_create_ip", ip);
        paramMap.put("notify_url", NOTIFY_URL);
        // 支付类型
        paramMap.put("trade_type", TRADE_TYPE_JS);
        // 微信官方SDK中生成签名方法
        String paramXml = WxPayUtil.generateSignedXml(paramMap, APIKEY, WxPayUtil.SignType.MD5);
        log.info("签名参数:{}", paramXml);
        Map<String, String> resultMap = new ConcurrentHashMap<>(10);
        // 请求微信服务发起支付 生成预支付订单ID
        String returnXml = HttpClientUtil.httpsRequest(UNIFIED_ORDER_URL, "POST", paramXml);
        log.info("返回参数:{}", returnXml);
        resultMap = WxPayUtil.xmlToMap(returnXml);
        Map<String, String> returnMap = new ConcurrentHashMap<>();
        // 微信SDK方法验证签名 再次签名
        if (WxPayUtil.isSignatureValid(resultMap, APIKEY, WxPayUtil.SignType.MD5)) {
            // 签名成功后返给前端
            returnMap.put("appId", APPID);
            // 时间戳
            returnMap.put("timeStamp", WXPayUtil.getCurrentTimestamp() + "");
            // 随机字符串
            returnMap.put("nonceStr", WXPayUtil.generateNonceStr());
            // 签名类型
            returnMap.put("signType", WXPayConstants.MD5);
            // 微信预订单ID ,package参数名在微信文档中已指定必填
            returnMap.put("package", "prepay_id=" + resultMap.get("prepay_id"));
            // 5. 通过appId, timeStamp, nonceStr, signType, package及商户密钥进⾏key=value形式拼接并加密
            String paySign = WXPayUtil.generateSignature(returnMap, APIKEY, WXPayConstants.SignType.MD5);
            returnMap.put("paySign", paySign);
            returnMap.put("mchId", MCHID);
            returnMap.put("resultCode", SUCCESS);
            log.info("returnMap:{}", returnMap);
            return returnMap;
        } else {
            returnMap.put("resultCode", FAIL);
            log.info("签名验证错误");
        }
        return returnMap;
    }
}

https 请求方法

    public static String httpsRequest(String requestUrl, String requestMethod, String outputStr) {
        try {

            URL url = new URL(requestUrl);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setDoOutput(true);
            conn.setDoInput(true);
            conn.setUseCaches(false);
            // 设置请求方式(GET/POST)
            conn.setRequestMethod(requestMethod);
            conn.setRequestProperty("content-type", "application/x-www-form-urlencoded");
            // 当outputStr不为null时向输出流写数据
            if (null != outputStr) {
                OutputStream outputStream = conn.getOutputStream();
                // 注意编码格式
                outputStream.write(outputStr.getBytes("UTF-8"));
                outputStream.close();
            }
            // 从输入流读取返回内容
            InputStream inputStream = conn.getInputStream();
            InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8");
            BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
            String str = null;
            StringBuffer buffer = new StringBuffer();
            while ((str = bufferedReader.readLine()) != null) {
                buffer.append(str);
            }
            // 释放资源
            bufferedReader.close();
            inputStreamReader.close();
            inputStream.close();
            inputStream = null;
            conn.disconnect();
            return buffer.toString();
        } catch (ConnectException ce) {
            System.out.println("连接超时:{}" + ce);
        } catch (Exception e) {
            System.out.println("https请求异常:{}" + e);
        }
        return null;
    }

前端同学拿到以上参数就能唤起Payment 支付密码输入面板啦。

2. 注意事项

  • 微信支付金额以“分”为单位,需将"元"转换为"分"。
  • 微信支付金额不能为"0"元。0元将导致签名失败,无法唤起Payment。
    以上给我记牢了,我曾经一次又一次在这翻车,一步步看日志的那种,唉不说了,一把辛酸泪。

五 、支付回调

何所谓支付回调:订单支付钱到了微信,究竟是否支付成功,业务系统无从知道。微信官方提供一个消息推送接口,下单后,将会请求你订单支付时设置的回调地址接口地址。微信会传送订单号、支付是否成功、支付金额、时间等信息。根据信息我们对相应的订单进行业务处理。

上代码。此处请自行脑补抖音“三支花”大妈 “上才艺”视频场景。哈哈哈哈哈

接口:

	/**
     * 通知回调 notify
     * @param request 订单信息
     * @param response
     * @return  SUCCESS/FAIL
     * @throws Exception
     */
    @RequestMapping(value = "/notify")
    @ApiOperation(value = "微信支付回调", response = ApiResult.class)
    public void notify(HttpServletRequest request, HttpServletResponse response) throws Exception {
        log.info("微信开始回调");
        String  resXml = wxPayService.notify(request,response);
        log.info("微信回调成功,返回值:{}",resXml);
    }

实现类:

	/** 支付回调
     * @param request
     * @param response
     * @return
     */
    @Override
    public String notify(HttpServletRequest request, HttpServletResponse response) throws IOException {
        log.info("---------------微信支付回调----------------------");
        // 调用返回 xml
        String resXml = "";
        try {
            // 解析request中xml 转换成map
            Map<String, String> map = WxPayUtil.parseXml(request);
            log.info("---------------微信支付回调订单号为:{}--------", map.get("out_trade_no"));
            // 检验签名是否成功
            if (WxPayUtil.isSignatureValid(map, APIKEY, WxPayUtil.SignType.MD5)) {
                log.info("微信支付return_code:{}", map.get("return_code"));
                //log.info("判断结果:{}", "SUCCESS".equals(map.get("return_code")));
                if ("SUCCESS".equals(map.get("return_code"))) {
                    Boolean result = orderService.orderCallback(map).getData();
                    log.info("支付回调得到order更新数据库结果:{}", result);
                    if (result) {
                        // 返回success给微信服务器 剔除推送队列中的本次交易
                        resXml = " " +
                                "";
                    } else {
                        log.info("----------------微信回调成功 更新数据库状态失败---------------");
                    }
                } else {
                    log.info("微信回调失败:{}", map.get("return_code"));
                }
            } else {
                 resXml = "" + "" + "" + " ";
            }
        } catch (Exception e) {
            log.error("微信回调失败,抛出异常:{}", e);
        }
        PrintWriter printWriter = new PrintWriter(response.getOutputStream());
        log.info("回调返回给微信服务器:{}", resXml);
        printWriter.write(resXml);
        printWriter.flush();
        printWriter.close();
        return resXml;
    }

一定要放开支付回调的权限啊,不然微信想回调你,也进不了你家大门啊

以上就是微信小程序支付实现的大致过程,下期讲 微信退款

一文实现微信小程序支付 史上最全版_第2张图片

关注个人微信公众号,与作者交流,第一时间接收更新通知:
一文实现微信小程序支付 史上最全版_第3张图片

你可能感兴趣的:(微信开发,java,小程序)