前言:让我用Java写个微信扫码支付,身为小白,网上搜了好多文章,终于找到一个看得明白的,链接。表示人家讲的够详细了,现在自己要是实现一个,我觉得吧,可能入手比较乱。其实后来发现,代码都是按照那个流程图写的,写代码是用来实现功能的,当然要按照功能分析别人的代码,否则都不知道人家写来干什么的。别人的东西不一定都好,就要给他改改,改的好才是真好,,哈哈。下面看内容!!!
一、微信扫码支付内容
1.文档:
- 官方文档:微信扫码实际应用示例
- 微信支付模式二的时序流程图:所有功能实现围绕这个流程图来写
- 统一下单API:提交给微信生成订单:有xml格式参数的举例
- 支付结果API:微信返回支付结果参数说明,有xml格式参数的举例
2.选择:
- 代码工具与方式----用的eclipse的插件版:STS,用servlet简单实现。
- 入手----实现微信扫码支付的功能,先从模式二的时序流程图入手。
- 后台:微信商户平台----申请成为微信商户后能登陆的,后台查看订单情况。
- 模式:模式二----模式二的时序流程图,模式二相比模式一更简单,因为模式二不需要自己生成二维码信息,只是接收微信返回的信息。
- 交易类型:扫码支付----实现的是扫码支付,如果是公众号支付、app支付和刷卡支付,请绕行。
- 穿透工具:花生壳----因为公司已经买了,我用就行。由于微信的回调地址要求必须是是外网的80端口,怎么办?我用的是付费的花生壳内网穿透版,可以使用国内免费的类似ngrok的natapp。
- debug调试:查看返回xml内容
- 流程大概:从时序图分析,
- ①调用统一下单API:就是给统一下单的参数赋值(必需的参数就行),然后向地址(微信提供的统一下单URL)传参数,然后接受微信返回的参数。其中传递的参数格式都是xml格式。
- ②微信返回的信息里有二维码对应的地址url(code_url)。用第三方库将code_url生成二维码。
- ③调用支付结果API:异步接收微信通知的支付结果,接收地址(统一下单里自定义的)接收微信传过来的参数,再返给微信参数,表示收到通知。
3.流程
(1)参数赋值
- 根据统一下单API必须传的参数,进行参数赋值:
- 商户号 mch_id :微信分配,企业号corpid即为此appId
- 随机字符串 nonce_str :微信分配,微信支付分配的商户号
- 商品描述 body :商户自定义,商品名称
- 商户订单号 out_trade_no :商户自定义,唯一,详见商户订单号
- 总金额 total_fee :商户自定义,整数,单位为分,测试值设为1,代表一分钱
- 终端IP spbill_create_ip :获取支付端IP,测试值设为固定值,如127.0.0.1
- 通知地址 notify_url :商户自定义,微信返回含有二维码信息的地址
- 交易类型 trade_type :三选一,扫码支付是 NATIVE,详细说明见参数规定
- 签名 sign :详见签名生成算法
(2)拼接参数
- 将参数拼成xml格式字符串:
(3)参数提交
- 传xml字符串格式的参数到统一下单API的URL地址:post方式提交
(4)解析返回参数
- 微信返回xml字符串,详细参数见统一下单API,解析为map集合,其中解析xml会用到jdom库。
(5)接收支付结果
- 接收xml字符串,解析成map集合,判断签名是否正确,执行成功或失败的相应业务,
- 返回xml字符串,告诉微信已经接收到支付结果通知了。
- 因为对后台通知交互时,如果微信收到商户的应答不是成功或超时,微信认为通知失败。
(6)申请退款
- 请参考:JAVA微信扫码支付申请退款
4.资料
(1)固定参数:申请成为微信商户成功,微信会邮件通知
- APP_ID--微信分配的公众账号ID,如何获得
- MCH_ID--微信支付分配的商户号
- API_KEY--商户API密钥KEY,如何获得
- NOTIFY_URL--商户自定义的接收微信异步通知支付结果的网址
(2)用到的jar包:
- jdom-1.1.3.jar:解析xml
- zxing-3.2.1.jar:谷歌的生成二维码的库
- 需要servlet-api.jar,请到tomcat文件夹里复制。
(3)穿透工具:
(4)参考文章 :
- JAVA微信扫码支付模式二功能实现以及回调----详细借鉴,讲的通俗实用
- 微信支付之扫码支付相关代码(Java)----调理清晰,内容完整
- 网站添加微信支付功能(小白填坑)----提到natapp穿透工具
- 微信支付V3集成(Native)----用图来分析实现流程
- 微信NATIVE扫码支付JAVA实现----简单
- servlet使用:javaweb学习总结(五)——Servlet开发(一)----作者博客写的内容全
- post提交:java发送http的get、post请求~~HTTP深入浅出 http请求----简单实用
- xml生成与解析:Java XML解析工具 dom4j介绍及使用实例----简单实用
- json生成与解析:JSON介绍~~浅谈使用java解析和生成JSON~~Java构造和解析Json数据的两种方法详解一,gson.jar----简单实用
(5)易错总结--签名错误
- 产生原因:参数名多加空格???(我也不清楚)参数名拼写错误也有可能,待验证
- 解决方法:微信官方接口调试生成的字符串,和调试过程中的签名字符串对比。
- 解决步骤:
- ①微信公众平台支付接口调试工具,对应输入参数和值。
- ②点击生成签名,出现
- #1.生成字符串:
- #2.连接商户key:假如叫签名字符串A
- #3.md5编码并转成大写:
- #4.最终的提交xml:
- ③debug调试:在生成签名过程中打断点,输出拼成字符串,假如叫签名字符串B
- ④微信官方调试工具生成的签名字符串A,和实际debug过程中的签名字符串B,对比,不同之处一目了然。
(6)后续总结和问题
- SortedMap的用法:签名算法中的排序会用到
- 扫码支付常见问题:官方给的一些问题解答
- 网页授权回调域名:没有用到
- 获取外网IP,获取公网IP,获取公网IP和内网IP:获取IP方法,我一直用固定值测试
- JavaWeb之tomcat安装、配置与使用(一):挺详细
二、实现步骤代码
先来张图,文件结构要清晰
1.生成二维码
- 用servlet写的:参数赋值-->拼接参数-->参数提交-->参数返回并解析-->对应参数生成二维码
public class EnterServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
//1,根据统一下单API,进行参数设定
String appid = PayConfigUtil.APP_ID; // appid
String mch_id = PayConfigUtil.MCH_ID; // 商业号
String key = PayConfigUtil.API_KEY; // key
String currTime = PayCommonUtil.getCurrTime();
String strRandom = PayCommonUtil.buildRandom(4) + "";
String nonce_str = currTime + strRandom; // 随机字符串
String order_price = "1"; // 价格的单位是分(必须整数)
String body = "iphone7"; // 商品名称
String out_trade_no = nonce_str + "520" ; // 订单号
String spbill_create_ip = PayCommonUtil.CREAT_IP; // 获取发起电脑ip
String notify_url = PayConfigUtil.NOTIFY_URL; // 回调接口
String trade_type = "NATIVE"; // 交易类型是扫码支付
//2,将参数放进Map集合
SortedMap<String,String> packageParams = new TreeMap<String,String>();
packageParams.put("appid", appid);
packageParams.put("mch_id", mch_id);
packageParams.put("nonce_str", nonce_str);
packageParams.put("body", body);
packageParams.put("out_trade_no", out_trade_no);
packageParams.put("total_fee", order_price);
packageParams.put("spbill_create_ip", spbill_create_ip);
packageParams.put("notify_url", notify_url);
packageParams.put("trade_type", trade_type);
//3,签名算法
String sign = PayCommonUtil.createSign("UTF-8", packageParams,key);
packageParams.put("sign", sign);
//4,将Map集合转换为xml格式
String requestXML = PayCommonUtil.getRequestXml(packageParams);
//5,用POST方式提交
String resXml = HttpUtil.postData(PayConfigUtil.PAY_API, requestXML);
//6,解析xml为Map集合,得到code_url(二维码链接)
Map<String, String> map = XMLUtil.doXMLParse(resXml);
String urlCode = (String) map.get("code_url");
//7,将code_url生成二维码
Qrcode encoder = new Qrcode();
encoder.createQRCoder(urlCode, response);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
2.支付结果通知
- 返回xml数据解析-->返回xml表示已接收到通知
public class NotifyServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
//1,读取微信返回的xml格式信息流
StringBuffer sb = new StringBuffer();
InputStream inputStream = request.getInputStream();
String s ;
BufferedReader in = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
while ((s = in.readLine()) != null){
sb.append(s);
}
in.close();
inputStream.close();
//2,解析xml为Map集合
Map<String, String> m = XMLUtil.doXMLParse(sb.toString());
//3,过滤空,用TreeMap集合存储信息
SortedMap<String, String> packageParams = new TreeMap<String, String>();
Iterator<String> it = m.keySet().iterator();
while (it.hasNext()) {
String parameter = it.next();
String parameterValue = m.get(parameter);
String v = "";
if(null != parameterValue) {
v = parameterValue.trim();
}
packageParams.put(parameter, v);
}
// 4,判断签名是否正确
String key = PayConfigUtil.API_KEY; // key
if(PayCommonUtil.isTenpaySign("UTF-8", packageParams,key)) {
String resXml = "";
// String out_trade_no = packageParams.get("out_trade_no");
if("SUCCESS".equals((String)packageParams.get("result_code"))){
//里是支付成功 ,执行自己的业务逻辑
//通知微信.异步确认成功.必写.不然会一直通知后台.八次之后就认为交易失败了.
resXml = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>"
+ "<return_msg><![CDATA[OK]]></return_msg>" + "</xml> ";
} else {
//提示支付失败,执行相应业务逻辑
resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>"
+ "<return_msg><![CDATA[报文为空]]></return_msg>" + "</xml> ";
}
BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream());
out.write(resXml.getBytes());
out.flush();
out.close();
} else{
//通知签名验证失败
}
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
3.工具类
(1)固定参数
- 微信分配的账户号 + 自定义的接收支付结果通知用的回调地址
public class PayConfigUtil {
public static String APP_ID = "";//微信开放平台应用ID
public static String MCH_ID = "";//商业号
public static String API_KEY = "";//API key
public static String PayConfigUtil.CREATE_IP = "127.0.0.1";//测试用的固定值,怎么获取公网IP暂时不会
public static String NOTIFY_URL = "";//回调地址
}
(2)通用方法
- 通用方法:签名是否正确 + 签名算法 + 拼成xml方法 + 生成随机数方法 + 获取当前时间方法
public class PayCommonUtil {
/**
* 是否签名正确,规则是:按参数名称a-z排序,遇到空值的参数不参加签名。
*
* @return boolean
*/
public static boolean isTenpaySign(String characterEncoding, SortedMap<String, String> packageParams,
String API_KEY) {
StringBuffer sb = new StringBuffer();
Set<Entry<String,String>> es = packageParams.entrySet();
Iterator<Entry<String, String>> it = es.iterator();
while (it.hasNext()) {
Entry<String, String> entry = it.next();
String k = (String) entry.getKey();
String v = (String) entry.getValue();
if (!"sign".equals(k) && null != v && !"".equals(v)) {
sb.append(k + "=" + v + "&");
}
}
sb.append("key=" + API_KEY);
// 算出摘要
String mysign = MD5Util.MD5Encode(sb.toString(), characterEncoding).toLowerCase();
String tenpaySign = ((String) packageParams.get("sign")).toLowerCase();
// System.out.println(tenpaySign + " " + mysign);
return tenpaySign.equals(mysign);
}
/**
* @author
* @date
* @Description:sign签名
* @param characterEncoding
* 编码格式
* @param parameters
* 请求参数
* @return
*/
public static String createSign(String characterEncoding, SortedMap<String,String> packageParams, String API_KEY) {
StringBuffer sb = new StringBuffer();
Set<Entry<String,String>> es = packageParams.entrySet();
Iterator<Entry<String, String>> it = es.iterator();
while (it.hasNext()) {
Entry<String,String> entry = it.next();
String k = (String) entry.getKey();
String v = (String) entry.getValue();
if (null != v && !"".equals(v) && !"sign".equals(k) && !"key".equals(k)) {
sb.append(k + "=" + v + "&");
}
}
sb.append("key=" + API_KEY);
String sign = MD5Util.MD5Encode(sb.toString(), characterEncoding).toUpperCase();
return sign;
}
/**
* @Description:将请求参数转换为xml格式的string
*/
public static String getRequestXml(SortedMap<String,String> parameters) {
StringBuffer sb = new StringBuffer();
sb.append("<xml>");
Set<Entry<String,String>> es = parameters.entrySet();
Iterator<Entry<String, String>> it = es.iterator();
while (it.hasNext()) {
Entry<String,String> entry = it.next();
String k = (String) entry.getKey();
String v = (String) entry.getValue();
if ("attach".equalsIgnoreCase(k) || "body".equalsIgnoreCase(k) || "sign".equalsIgnoreCase(k)) {
sb.append("<" + k + ">" + "<![CDATA[" + v + "]]></" + k + ">");
} else {
sb.append("<" + k + ">" + v + "</" + k + ">");
}
}
sb.append("</xml>");
return sb.toString();
}
/**
* 取出一个指定长度大小的随机正整数.
*
* @param length
* int 设定所取出随机数的长度。length小于11
* @return int 返回生成的随机数。
*/
public static int buildRandom(int length) {
int num = 1;
double random = Math.random();
if (random < 0.1) {
random = random + 0.1;
}
for (int i = 0; i < length; i++) {
num = num * 10;
}
return (int) ((random * num));
}
/**
* 获取当前时间 yyyyMMddHHmmss
*
* @return String
*/
public static String getCurrTime() {
Date now = new Date();
SimpleDateFormat outFormat = new SimpleDateFormat("yyyyMMddHHmmss");
String s = outFormat.format(now);
return s;
}
}
(3)解析XML
- 将xml解析成map集合,根元素-->子元素-->遍历存到集合-->子元素是多层的要递归(按道理说)
public class XMLUtil {
/**
* 解析xml,返回第一级元素键值对。如果第一级元素有子节点,则此节点的值是子节点的xml数据。
* @param strxml
* @return
* @throws JDOMException
* @throws IOException
*/
@SuppressWarnings("unchecked")
public static Map<String, Object> doXMLParse(String strxml) throws JDOMException, IOException {
strxml = strxml.replaceFirst("encoding=\".*\"", "encoding=\"UTF-8\"");
if(null == strxml || "".equals(strxml)) {
return null;
}
Map<String, Object> m = new HashMap<String, Object>();
InputStream in = new ByteArrayInputStream(strxml.getBytes("UTF-8"));
SAXBuilder builder = new SAXBuilder();
Document doc = builder.build(in);
Element root = doc.getRootElement();
List<Element> list = root.getChildren();
Iterator<Element> it = list.iterator();
while(it.hasNext()) {
Element e = (Element) it.next();
String k = e.getName();
String v = "";
List<Element> children = e.getChildren();
if(children.isEmpty()) {
v = e.getTextNormalize();
} else {
v = XMLUtil.getChildrenText(children);
}
m.put(k, v);
}
//关闭流
in.close();
return m;
}
/**
* 获取子结点的xml
* @param children
* @return String
*/
@SuppressWarnings("unchecked")
public static String getChildrenText(List<Element> children) {
StringBuffer sb = new StringBuffer();
if(!children.isEmpty()) {
Iterator<Element> it = children.iterator();
while(it.hasNext()) {
Element e = (Element) it.next();
String name = e.getName();
String value = e.getTextNormalize();
List<Element> list = e.getChildren();
sb.append("<" + name + ">");
if(!list.isEmpty()) {
sb.append(XMLUtil.getChildrenText(list));
}
sb.append(value);
sb.append("</" + name + ">");
}
}
return sb.toString();
}
}
(4)POST提交
- post方式提交,说get方式不安全,不涉及证书
public class HttpUtil {
private final static int CONNECT_TIMEOUT = 5000; // in milliseconds
private final static String DEFAULT_ENCODING = "UTF-8";
public static String postData(String urlStr, String data) {
return postData(urlStr, data, null);
}
public static String postData(String urlStr, String data, String contentType) {
BufferedReader reader = null;
try {
URL url = new URL(urlStr);
URLConnection conn = url.openConnection();
conn.setDoOutput(true);
conn.setConnectTimeout(CONNECT_TIMEOUT);
conn.setReadTimeout(CONNECT_TIMEOUT);
if (contentType != null)
conn.setRequestProperty("content-type", contentType);
OutputStreamWriter writer = new OutputStreamWriter(conn.getOutputStream(), DEFAULT_ENCODING);
if (data == null)
data = "";
writer.write(data);
writer.flush();
writer.close();
reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), DEFAULT_ENCODING));
StringBuilder sb = new StringBuilder();
String line = null;
while ((line = reader.readLine()) != null) {
sb.append(line);
sb.append("\r\n");
}
return sb.toString();
} catch (IOException e) {
// logger.error("Error connecting to " + urlStr + ": " +
// e.getMessage());
} finally {
try {
if (reader != null)
reader.close();
} catch (IOException e) {
}
}
return null;
}
}
(5)MD5工具
- 为了省用一个jar包,commons-codec-1.9.jar,内容实现不用研究,完全复制来的
public class MD5Util {
private static String byteArrayToHexString(byte b[]) {
StringBuffer resultSb = new StringBuffer();
for (int i = 0; i < b.length; i++)
resultSb.append(byteToHexString(b[i]));
return resultSb.toString();
}
private static String byteToHexString(byte b) {
int n = b;
if (n < 0)
n += 256;
int d1 = n / 16;
int d2 = n % 16;
return hexDigits[d1] + hexDigits[d2];
}
public static String MD5Encode(String origin, String charsetname) {
String resultString = null;
try {
resultString = new String(origin);
MessageDigest md = MessageDigest.getInstance("MD5");
if (charsetname == null || "".equals(charsetname))
resultString = byteArrayToHexString(md.digest(resultString.getBytes()));
else
resultString = byteArrayToHexString(md.digest(resultString.getBytes(charsetname)));
} catch (Exception exception) {
}
return resultString;
}
private static final String hexDigits[] = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d",
"e", "f" };
}
MD5Util
(6)第三方库生成二维码
- 谷歌生成二维码的zxing包示例使用好简单,直接返回BufferImage类型,二进制流图片
public class Qrcode {
public void createQRCoder(String content, HttpServletResponse response) {
try {
int width = 400;//二维码宽度
int height = 400;//二维码高度
String qrcodeFormat = "png";//图片类型
HashMap<EncodeHintType, String> hints = new HashMap<EncodeHintType, String>();
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
//MultiFormatWriter 对象为生成二维码的核心类,后面的 MatrixToImageWriter 只是将二维码矩阵输出到图片上面。
BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints);
MatrixToImageWriter.writeToStream(bitMatrix, qrcodeFormat, response.getOutputStream());
} catch (Exception e) {
e.printStackTrace();
}
}
}
来源:oschina
链接:https://my.oschina.net/u/2647891/blog/782695