JAVA微信扫码支付模式一功能实现

一、准备工作

4月份那会发了篇关于 JAVA微信扫码支付模式二功能实现的博客,无数人来追问模式一的开发,所以在这就贴出来,仅供参考。关于模式一和模式二的区别,我有解释过很多次,无非就是模式一的二维码是针对商品的,模式二的二维码是针对订单的,其他具体细节我就不费口舌了,各位可以自行去官方查看文档,然后是选模式一还是模式二就得看自己的业务了。

1.1、有关配置参数

还是之前那四样,APP_ID和APP_SECRET可以在公众平台找着,MCH_ID和API_KEY则在商户平台找到,特别是API_KEY要在商户平台设置好,这个东东关系到参数校验的正确与否,所以一定要设置正确。扫码支付模式一其实与扫码支付模式二类似,实际只会用到APP_ID、MCH_ID和API_KEY,其他的都不用。模式一的官方文档地址在这:

https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_4

1.2、有关概念

在这里我想先修正一个概念,在之前模式二开发过程中我曾提到了一个“支付回调地址”这样的概念,其作用实际就是客户在扫描完成支付后,微信服务器要访问我们提供的这个地址,给我们发送支付结果,以便我们核实订单进行发货,这是其他支付工具比较普遍的概念和叫法。不过后来我翻了一下微信官网的文档,发现在模式一开发中,他们把这个叫做“异步通知url”而不是什么“支付回调地址”,但本质这指的是一个意思。可是为什么我要在这提到这个东东呢?这是因为在模式一中,实际上还有另外一个所谓的“支付回调”称之为“扫码支付回调URL”,这东东与上面的“异步通知url”可就不一样了,简单理解可以认为是咱们的服务器上一个用来辅助完成下单的接口。模式一的开发同时需要“扫码支付回调URL”与“异步通知url”两个接口配合才能完成,所以这里大家要辨别好了。

“异步通知url”在调用统一下单接口时进行设置,可以动态设置,只要这个接口按照有关规则接收参数响应参数即可。而“扫码支付回调URL”则较为固定,它在微信公众平台设置,设置后需要10分钟左右才能生效,大家登录微信公众平台后,选择微信支付,在开发配置选项卡下面就可以找着:

这里咱们要设置一个自己服务器的地址(再说一遍公网地址,就是让微信服务器能找着你)。

1.3、开发环境

我这里以最基本的Servlet 3.0作为示例环境。关于引用第三方的jar包,相比较于模式二开发,除了用到了xml操作的jdom,以外就一个Google ZXing的二维码包和log4j包。如下图:

为了方便调试,建议各位先在这个环境下调通了再移植到真实项目当中去。

二、开发实战

在动手之前,我建议大家先去官方文档那好好看看那个时序图,理解了那个时序图,写代码也就不是什么难事了,当然如果看图你没办法理解,也可以结合我下面的代码来试着理解。

2.1、二维码生成

首先是二维码,二维码中的内容为链接,形式为:

weixin://wxpay/bizpayurl?sign=XXXXX&appid=XXXXX&mch_id=XXXXX&product_id=XXXXXX&time_stamp=XXXXXX&nonce_str=XXXXX

具体可以参考官方文档模式一生成二维码规则。接下来我们需要将该链接生成二维码,我这里使用了Google ZXing来生成二维码。

 
  1. package com.wqy;

  2.  
  3. import java.io.IOException;

  4. import java.io.OutputStream;

  5. import java.util.HashMap;

  6. import java.util.Iterator;

  7. import java.util.Map;

  8. import java.util.Set;

  9. import java.util.SortedMap;

  10. import java.util.TreeMap;

  11.  
  12. import javax.servlet.ServletException;

  13. import javax.servlet.annotation.WebServlet;

  14. import javax.servlet.http.HttpServlet;

  15. import javax.servlet.http.HttpServletRequest;

  16. import javax.servlet.http.HttpServletResponse;

  17.  
  18. import org.apache.log4j.Logger;

  19.  
  20. import com.google.zxing.BarcodeFormat;

  21. import com.google.zxing.EncodeHintType;

  22. import com.google.zxing.MultiFormatWriter;

  23. import com.google.zxing.WriterException;

  24. import com.google.zxing.client.j2se.MatrixToImageWriter;

  25. import com.google.zxing.common.BitMatrix;

  26. import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;

  27. import com.wqy.util.PayCommonUtil;

  28. import com.wqy.util.PayConfigUtil;

  29.  
  30. /**

  31. * Servlet implementation class Pay1

  32. */

  33. @WebServlet("/Pay1")

  34. public class Pay1 extends HttpServlet {

  35. private static final long serialVersionUID = 1L;

  36. private static Logger logger = Logger.getLogger(Pay1.class);

  37.  
  38. public static int defaultWidthAndHeight=200;

  39.  
  40. /**

  41. * @see HttpServlet#HttpServlet()

  42. */

  43. public Pay1() {

  44. super();

  45. // TODO Auto-generated constructor stub

  46. }

  47.  
  48. /**

  49. * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse

  50. * response)

  51. */

  52. protected void doGet(HttpServletRequest request, HttpServletResponse response)

  53. throws ServletException, IOException {

  54.  
  55. // TODO Auto-generated method stub

  56. String nonce_str = PayCommonUtil.getNonce_str();

  57. long time_stamp = System.currentTimeMillis() / 1000;

  58. String product_id = "hd_goodsssss_10";

  59. String key = PayConfigUtil.API_KEY; // key

  60.  
  61. SortedMap<Object, Object> packageParams = new TreeMap<Object, Object>();

  62. packageParams.put("appid", PayConfigUtil.APP_ID);

  63. packageParams.put("mch_id", PayConfigUtil.MCH_ID);

  64. packageParams.put("time_stamp", String.valueOf(time_stamp));

  65. packageParams.put("nonce_str", nonce_str);

  66. packageParams.put("product_id", product_id);

  67. String sign = PayCommonUtil.createSign("UTF-8", packageParams,key);//MD5哈希

  68. packageParams.put("sign", sign);

  69.  
  70. //生成参数

  71. String str = ToUrlParams(packageParams);

  72. String payurl = "weixin://wxpay/bizpayurl?" + str;

  73. logger.info("payurl:"+payurl);

  74.  
  75.  
  76. //生成二维码

  77. Map<EncodeHintType, Object> hints=new HashMap<EncodeHintType, Object>();

  78. // 指定纠错等级

  79. hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L);

  80. // 指定编码格式

  81. hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");

  82. hints.put(EncodeHintType.MARGIN, 1);

  83. try {

  84. BitMatrix bitMatrix = new MultiFormatWriter().encode(payurl,BarcodeFormat.QR_CODE, defaultWidthAndHeight, defaultWidthAndHeight, hints);

  85. OutputStream out = response.getOutputStream();

  86. MatrixToImageWriter.writeToStream(bitMatrix, "png", out);//输出二维码

  87. out.flush();

  88. out.close();

  89.  
  90. } catch (WriterException e) {

  91. // TODO Auto-generated catch block

  92. e.printStackTrace();

  93. }

  94. }

  95.  
  96. public String ToUrlParams(SortedMap<Object, Object> packageParams){

  97. //实际可以不排序

  98. StringBuffer sb = new StringBuffer();

  99. Set es = packageParams.entrySet();

  100. Iterator it = es.iterator();

  101. while (it.hasNext()) {

  102. Map.Entry entry = (Map.Entry) it.next();

  103. String k = (String) entry.getKey();

  104. String v = (String) entry.getValue();

  105. if (null != v && !"".equals(v)) {

  106. sb.append(k + "=" + v + "&");

  107. }

  108. }

  109.  
  110. sb.deleteCharAt(sb.length()-1);//删掉最后一个&

  111. return sb.toString();

  112. }

  113.  
  114. /**

  115. * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse

  116. * response)

  117. */

  118. protected void doPost(HttpServletRequest request, HttpServletResponse response)

  119. throws ServletException, IOException {

  120. // TODO Auto-generated method stub

  121. doGet(request, response);

  122. }

  123.  
  124. }


2.2、扫描支付回调url接口

当客户用微信扫了上面的二位码之后,微信服务器就会访问此接口,在这里我们要完成统一下单获取交易会话标识,处理的主要流程如下:

1)、接收微信服务器发送过来的参数,对参数进行签名校验;
2)、取出参数product_id,这是二维码上唯一能够透传过来的参数,其他参数可参照官方文档模式一3.1 输入参数;
3)、根据product_id处理自己的业务,比如计算支付金额,生成订单号等;
4)、调用统一下单接口获取交易会话标识prepay_id;
    4.1)、准备好相关参数(如appid、mch_id、支付金额、订单号、商品描述等),调用微信统一下单接口(与模式二调用统一下单接口类似),留意一下这里要加上上面提到的“异步通知url”,也就是后面会说道的异步通知url接口,具体参数参考官方文档统一下单请求参数;
    4.2)、接收统一下单接口返回的参数,对参数进行验签;
    4.3)、取出参数prepay_id,这是交易会话标识,极其重要,其他参数可参考官方文档统一下单返回结果;
5)、准备好相关参数(如appid、mch_id、return_code、prepay_id等),响应最开始的支付回调(如果上面步骤如果错误,如验签失败则可以返回错误参数给微信服务器),具体参数可参照官方文档模式一3.2 输出参数。

 
  1. package com.wqy;

  2.  
  3. import java.io.BufferedOutputStream;

  4. import java.io.BufferedReader;

  5. import java.io.IOException;

  6. import java.io.InputStream;

  7. import java.io.InputStreamReader;

  8. import java.util.SortedMap;

  9. import java.util.TreeMap;

  10.  
  11. import javax.servlet.ServletException;

  12. import javax.servlet.annotation.WebServlet;

  13. import javax.servlet.http.HttpServlet;

  14. import javax.servlet.http.HttpServletRequest;

  15. import javax.servlet.http.HttpServletResponse;

  16.  
  17. import org.apache.log4j.Logger;

  18.  
  19. import com.wqy.util.HttpUtil;

  20. import com.wqy.util.PayCommonUtil;

  21. import com.wqy.util.PayConfigUtil;

  22.  
  23. /**

  24. * Servlet implementation class Notify1

  25. */

  26. @WebServlet("/Notify1")

  27. public class Notify1 extends HttpServlet {

  28. private static final long serialVersionUID = 1L;

  29. private static Logger logger = Logger.getLogger(Notify1.class);

  30.  
  31. /**

  32. * @see HttpServlet#HttpServlet()

  33. */

  34. public Notify1() {

  35. super();

  36. // TODO Auto-generated constructor stub

  37. }

  38.  
  39. /**

  40. * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse

  41. * response)

  42. */

  43. protected void doGet(HttpServletRequest request, HttpServletResponse response)

  44. throws ServletException, IOException {

  45. // TODO Auto-generated method stub

  46.  
  47. // 读取xml

  48. InputStream inputStream;

  49. StringBuffer sb = new StringBuffer();

  50. inputStream = request.getInputStream();

  51. String s;

  52. BufferedReader in = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));

  53. while ((s = in.readLine()) != null) {

  54. sb.append(s);

  55. }

  56. in.close();

  57. inputStream.close();

  58.  
  59. SortedMap<Object, Object> packageParams = PayCommonUtil.xmlConvertToMap(sb.toString());

  60. logger.info(packageParams);

  61.  
  62. // 账号信息

  63. String key = PayConfigUtil.API_KEY; // key

  64.  
  65. String resXml="";//反馈给微信服务器

  66. // 验签

  67. if (PayCommonUtil.isTenpaySign("UTF-8", packageParams, key)) {

  68. //appid openid mch_id is_subscribe nonce_str product_id sign

  69.  
  70. //统一下单

  71. String openid = (String)packageParams.get("openid");

  72. String product_id = (String)packageParams.get("product_id");

  73. //解析product_id,计算价格等

  74.  
  75. String out_trade_no = String.valueOf(System.currentTimeMillis()); // 订单号

  76. String order_price = "1"; // 价格 注意:价格的单位是分

  77. String body = product_id; // 商品名称 这里设置为product_id

  78. String attach = "XXX店"; //附加数据

  79.  
  80. String nonce_str0 = PayCommonUtil.getNonce_str();

  81.  
  82. // 获取发起电脑 ip

  83. String spbill_create_ip = PayConfigUtil.CREATE_IP;

  84. String trade_type = "NATIVE";

  85.  
  86.  
  87. SortedMap<Object,Object> unifiedParams = new TreeMap<Object,Object>();

  88. unifiedParams.put("appid", PayConfigUtil.APP_ID); // 必须

  89. unifiedParams.put("mch_id", PayConfigUtil.MCH_ID); // 必须

  90. unifiedParams.put("out_trade_no", out_trade_no); // 必须

  91. unifiedParams.put("product_id", product_id);

  92. unifiedParams.put("body", body); // 必须

  93. unifiedParams.put("attach", attach);

  94. unifiedParams.put("total_fee", order_price); // 必须

  95. unifiedParams.put("nonce_str", nonce_str0); // 必须

  96. unifiedParams.put("spbill_create_ip", spbill_create_ip); // 必须

  97. unifiedParams.put("trade_type", trade_type); // 必须

  98. unifiedParams.put("openid", openid);

  99. unifiedParams.put("notify_url", PayConfigUtil.NOTIFY_URL);//异步通知url

  100.  
  101. String sign0 = PayCommonUtil.createSign("UTF-8", unifiedParams,key);

  102. unifiedParams.put("sign", sign0); //签名

  103.  
  104. String requestXML = PayCommonUtil.getRequestXml(unifiedParams);

  105. logger.info(requestXML);

  106. //统一下单接口

  107. String rXml = HttpUtil.postData(PayConfigUtil.UFDODER_URL, requestXML);

  108.  
  109. //统一下单响应

  110. SortedMap<Object, Object> reParams = PayCommonUtil.xmlConvertToMap(rXml);

  111. logger.info(reParams);

  112.  
  113. //验签

  114. if (PayCommonUtil.isTenpaySign("UTF-8", reParams, key)) {

  115. // 统一下单返回的参数

  116. String prepay_id = (String)reParams.get("prepay_id");//交易会话标识 2小时内有效

  117.  
  118. String nonce_str1 = PayCommonUtil.getNonce_str();

  119.  
  120. SortedMap<Object,Object> resParams = new TreeMap<Object,Object>();

  121. resParams.put("return_code", "SUCCESS"); // 必须

  122. resParams.put("return_msg", "OK");

  123. resParams.put("appid", PayConfigUtil.APP_ID); // 必须

  124. resParams.put("mch_id", PayConfigUtil.MCH_ID);

  125. resParams.put("nonce_str", nonce_str1); // 必须

  126. resParams.put("prepay_id", prepay_id); // 必须

  127. resParams.put("result_code", "SUCCESS"); // 必须

  128. resParams.put("err_code_des", "OK");

  129.  
  130. String sign1 = PayCommonUtil.createSign("UTF-8", resParams,key);

  131. resParams.put("sign", sign1); //签名

  132.  
  133. resXml = PayCommonUtil.getRequestXml(resParams);

  134. logger.info(resXml);

  135.  
  136. }else{

  137. logger.info("签名验证错误");

  138. resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>"

  139. + "<return_msg><![CDATA[签名验证错误]]></return_msg>" + "</xml> ";

  140. }

  141.  
  142. }else{

  143. logger.info("签名验证错误");

  144. resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>"

  145. + "<return_msg><![CDATA[签名验证错误]]></return_msg>" + "</xml> ";

  146. }

  147.  
  148. //------------------------------

  149. //处理业务完毕

  150. //------------------------------

  151. BufferedOutputStream out = new BufferedOutputStream(

  152. response.getOutputStream());

  153. out.write(resXml.getBytes());

  154. out.flush();

  155. out.close();

  156.  
  157. }

  158.  
  159. /**

  160. * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse

  161. * response)

  162. */

  163. protected void doPost(HttpServletRequest request, HttpServletResponse response)

  164. throws ServletException, IOException {

  165. // TODO Auto-generated method stub

  166. doGet(request, response);

  167. }

  168.  
  169. }


至此,用户的微信单就会显示出要支付的金额及商品描述等,接下来就是等待客户完成支付。

2.3、异步通知url接口

当用户在微信上完成支付操作后,微信服务器就会异步通知这个接口,给我们发送最终的支付结果,以便我们核实订单进行发货等操作,注意这个接口和模式二的开发是一模一样的。大概流程如下:

1)、接收微信服务器发送过来的参数,对参数进行签名校验;
2)、取出参数result_code、订单号out_trade_no、订单金额total_fee及其他业务相关的参数,具体参数可参照官方文档支付结果通用通知的通知参数;
3)、处理业务,如校验订单号及订单金额、修改订单状态等;
4)、准备好相关参数(return_code和return_msg),应答微信服务器。

注意,如果微信收到商户的应答不是成功或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功。 (通知频率为15/15/30/180/1800/1800/1800/1800/3600,单位:秒)

 
  1. package com.wqy;

  2.  
  3. import java.io.BufferedOutputStream;

  4. import java.io.BufferedReader;

  5. import java.io.IOException;

  6. import java.io.InputStream;

  7. import java.io.InputStreamReader;

  8. import java.util.SortedMap;

  9.  
  10. import javax.servlet.ServletException;

  11. import javax.servlet.annotation.WebServlet;

  12. import javax.servlet.http.HttpServlet;

  13. import javax.servlet.http.HttpServletRequest;

  14. import javax.servlet.http.HttpServletResponse;

  15.  
  16. import org.apache.log4j.Logger;

  17.  
  18. import com.wqy.util.PayCommonUtil;

  19. import com.wqy.util.PayConfigUtil;

  20.  
  21. /**

  22. * Servlet implementation class Re_notify

  23. */

  24. @WebServlet("/Re_notify")

  25. public class Re_notify extends HttpServlet {

  26. private static final long serialVersionUID = 1L;

  27. private static Logger logger = Logger.getLogger(Re_notify.class);

  28.  
  29. /**

  30. * @see HttpServlet#HttpServlet()

  31. */

  32. public Re_notify() {

  33. super();

  34. // TODO Auto-generated constructor stub

  35. }

  36.  
  37. /**

  38. * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse

  39. * response)

  40. */

  41. protected void doGet(HttpServletRequest request, HttpServletResponse response)

  42. throws ServletException, IOException {

  43. // TODO Auto-generated method stub

  44. // 读取参数

  45. InputStream inputStream;

  46. StringBuffer sb = new StringBuffer();

  47. inputStream = request.getInputStream();

  48. String s;

  49. BufferedReader in = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));

  50. while ((s = in.readLine()) != null) {

  51. sb.append(s);

  52. }

  53. in.close();

  54. inputStream.close();

  55.  
  56. SortedMap<Object, Object> packageParams = PayCommonUtil.xmlConvertToMap(sb.toString());

  57. logger.info(packageParams);

  58.  
  59. // 账号信息

  60. String key = PayConfigUtil.API_KEY; // key

  61.  
  62. String resXml = ""; // 反馈给微信服务器

  63. // 判断签名是否正确

  64. if (PayCommonUtil.isTenpaySign("UTF-8", packageParams, key)) {

  65. // ------------------------------

  66. // 处理业务开始

  67. // ------------------------------

  68. if ("SUCCESS".equals((String) packageParams.get("result_code"))) {

  69. // 这里是支付成功

  70. ////////// 执行自己的业务逻辑////////////////

  71. String mch_id = (String) packageParams.get("mch_id");

  72. String openid = (String) packageParams.get("openid");

  73. String is_subscribe = (String) packageParams.get("is_subscribe");

  74. String out_trade_no = (String) packageParams.get("out_trade_no");

  75.  
  76. String total_fee = (String) packageParams.get("total_fee");

  77.  
  78. logger.info("mch_id:" + mch_id);

  79. logger.info("openid:" + openid);

  80. logger.info("is_subscribe:" + is_subscribe);

  81. logger.info("out_trade_no:" + out_trade_no);

  82. logger.info("total_fee:" + total_fee);

  83.  
  84. ////////// 执行自己的业务逻辑////////////////

  85.  
  86. logger.info("支付成功");

  87. // 通知微信.异步确认成功.必写.不然会一直通知后台.八次之后就认为交易失败了.

  88. resXml = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>"

  89. + "<return_msg><![CDATA[OK]]></return_msg>" + "</xml> ";

  90.  
  91. } else {

  92. logger.info("支付失败,错误信息:" + packageParams.get("err_code"));

  93. resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>"

  94. + "<return_msg><![CDATA[报文为空]]></return_msg>" + "</xml> ";

  95. }

  96.  
  97. } else {

  98. logger.info("签名验证错误");

  99. resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>"

  100. + "<return_msg><![CDATA[签名验证错误]]></return_msg>" + "</xml> ";

  101. }

  102.  
  103. // ------------------------------

  104. // 处理业务完毕

  105. // ------------------------------

  106. BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream());

  107. out.write(resXml.getBytes());

  108. out.flush();

  109. out.close();

  110. }

  111.  
  112. /**

  113. * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse

  114. * response)

  115. */

  116. protected void doPost(HttpServletRequest request, HttpServletResponse response)

  117. throws ServletException, IOException {

  118. // TODO Auto-generated method stub

  119. doGet(request, response);

  120. }

  121.  
  122. }


三、测试结果

3.1、生成的支付二维码链接

3.2、支付回调url接口接收到的参数

3.3、发起统一下单请求参数

3.4、统一下单返回参数

3.5、支付回调url接口最终的响应参数

猜你喜欢

转载自blog.csdn.net/laiyuan999/article/details/81782607