一: 项目背景
最近和朋友一起做了一款APP ,涉及到微信支付, 简单记录一下微信支付服务端流程
微信支付的难点在于没有测试账号,无客服,这点远不如支付宝友好,网上搜索的微信支付千篇一律,对初学者很不友好,因此踩了很多坑。本文从账号的注册,代码的编写详细介绍。
二: 账号注册
微信支付是需要审核资质,拥有营业执照的个体户即可申请。值得注意的是,申请账号关联相关身份必须与营业执照一致。
注册方式开发总结如下,按照流程注册即可
若app需要接入微信支付,需要先注册并认证开放平台(https://open.weixin.qq.com/)账号,才能生成appid,然后再注册商户平台(https://pay.weixin.qq.com)账号,获取商户号、密钥等,申请成功后,请通过微信支付商户平台发起appid授权绑定(https://kf.qq.com/faq/1801116VJfua1801113QVNVz.html),微信支付开发文档地址为:https://pay.weixin.qq.com/wiki/doc/api/index.html
三:微信支付接口调试
注意代码注释结合,微信支付开发文档查看
常量类
/**
* created by wangch on 2019/8/9
*/
public class ConstantUtil {
/**
* 微信开发平台应用ID
*/
public static final String APP_ID="";
/**
* 应用对应的凭证
*/
public static final String APP_SECRET="";
/**
* 应用对应的密钥
*/
public static final String APP_KEY="";
/**
* 微信支付商户号
*/
public static final String MCH_ID="1557353761";
/**
* 商品描述
*/
public static final String BODY="QQ游戏-账户充值";
/**
* 商户号对应的密钥
*/
public static final String PARTNER_key="123466";
/**
* 商户id
*/
public static final String PARTNER_ID="";
/**
* 常量固定值
*/
public static final String GRANT_TYPE="client_credential";
/**
* 获取预支付id的接口url
*/
public static String GATEURL = "https://api.mch.weixin.qq.com/pay/unifiedorder";
/**
* 微信服务器回调通知url
*/
public static String NOTIFY_URL="ip:8080/order/pay/notify";
}
接口类
@RestController
@RequestMapping("/order")
public class PayController {
private static final Logger LOG = Logger.getLogger(PayController.class);
private static final String ORDER_PAY = "https://api.mch.weixin.qq.com/pay/unifiedorder"; // 统一下单
private static final String ORDER_PAY_QUERY = "https://api.mch.weixin.qq.com/pay/orderquery"; // 支付订单查询
private static final String ORDER_REFUND = "https://api.mch.weixin.qq.com/secapi/pay/refund"; // 申请退款
private static final String ORDER_REFUND_QUERY = "https://api.mch.weixin.qq.com/pay/refundquery"; // 申请退款
private static final String APP_ID = ConstantUtil.APP_ID;
private static final String MCH_ID = ConstantUtil.MCH_ID;
private static final String API_SECRET = ConstantUtil.APP_KEY;
@Autowired
private IOrderService iOrderService;
/**
* 支付下订单
*
* @param request
* @param response
* @RequestParam(required = false, defaultValue = "0") Double cashnum, String mercid, String callback
*/
@RequestMapping(value = "/pay", method = RequestMethod.POST)
public R orderPay(@RequestBody Order order, HttpServletRequest request, HttpServletResponse response) {
LOG.info("[/order/pay]");
// if (!"001".equals(mercid)) {
// WebUtil.response(response, WebUtil.packJsonp(callback, JSON
// .toJSONString(new JsonResult(-1, "商品不存在", new ResponseData()), SerializerFeatureUtil.FEATURES)));
// }
Map<String, String> restmap = null;
boolean flag = true; // 是否订单创建成功
try {
String total_fee = BigDecimal.valueOf(order.getPrice()).multiply(BigDecimal.valueOf(100))
.setScale(0, BigDecimal.ROUND_HALF_UP).toString();
Map<String, String> parm = new HashMap<String, String>();
parm.put("appid", APP_ID);
parm.put("mch_id", MCH_ID);
parm.put("device_info", "WEB");
parm.put("nonce_str", PayUtil.getNonceStr());
parm.put("body", "微信支付");
parm.put("attach", "wangch");
// PayUtil.getTradeNo
parm.put("out_trade_no", order.getCode());
// String.valueOf(order.getPrice())
parm.put("total_fee", total_fee);
parm.put("spbill_create_ip", PayUtil.getRemoteAddrIp(request));
// "https://www.andy.org/wxpay/order/pay/notify.shtml"
parm.put("notify_url",ConstantUtil.NOTIFY_URL );
parm.put("trade_type", "APP");
parm.put("sign", PayUtil.getSign(parm, API_SECRET));
String restxml = HttpUtils.post(ORDER_PAY, XmlUtil.xmlFormat(parm, false));
restmap = XmlUtil.xmlParse(restxml);
} catch (Exception e) {
LOG.error(e.getMessage(), e);
}
Map<String, String> payMap = new HashMap<String, String>();
if (CollectionUtil.isNotEmpty(restmap) && "SUCCESS".equals(restmap.get("result_code"))) {
payMap.put("appid", APP_ID);
payMap.put("partnerid", MCH_ID);
payMap.put("prepayid", restmap.get("prepay_id"));
payMap.put("package", "Sign=WXPay");
payMap.put("noncestr", PayUtil.getNonceStr());
payMap.put("timestamp", PayUtil.payTimestamp());
try {
payMap.put("sign", PayUtil.getSign(payMap, API_SECRET));
} catch (Exception e) {
flag = false;
}
}
// if (flag) {
// //return new R().put("获取成功",new ResponseData(null, payMap));
// WebUtil.response(response,
// WebUtil.packJsonp("data",
// JSON.toJSONString(new JsonResult(1, "订单获取成功", new ResponseData(null, payMap)),
// SerializerFeatureUtil.FEATURES)));
// } else {
// if (CollectionUtil.isNotEmpty(restmap)) {
// //return new R().put("订单创建失败",new ResponseData(null, payMap));
// LOG.info("订单创建失败:" + restmap.get("err_code") + ":" + restmap.get("err_code_des"));
// }
// WebUtil.response(response, WebUtil.packJsonp(order.getCode(), JSON
// .toJSONString(new JsonResult(-1, "订单获取失败", new ResponseData()), SerializerFeatureUtil.FEATURES)));
// //return new R().put("订单获取失败",new ResponseData());
// }
R r = new R();
if (flag){
return r.put("data",payMap);
} else {
return R.error("获取失败");
}
}
/**
* 查询支付结果
*
* @param request
* @param response
* @param tradeid 微信交易订单号
* @param tradeno 商品订单号
* @param callback
*/
@RequestMapping(value = "/pay/query", method = RequestMethod.POST)
public R orderPayQuery(HttpServletRequest request, HttpServletResponse response,Integer orderId) {
LOG.info("[/order/pay/query]");
// if (StringUtil.isEmpty(tradeno) && StringUtil.isEmpty(tradeid)) {
// WebUtil.response(response, WebUtil.packJsonp(callback, JSON
// .toJSONString(new JsonResult(-1, "订单号不能为空", new ResponseData()), SerializerFeatureUtil.FEATURES)));
// }
// 通过id 获取商户订单号与微信订单号
Map<String,String> map = iOrderService.getOrderInfoByOrderId(orderId);
Map<String, String> restmap = null;
try {
Map<String, String> parm = new HashMap<String, String>();
parm.put("appid", APP_ID);
parm.put("mch_id", MCH_ID);
parm.put("transaction_id", map.get("tradeNo"));
parm.put("out_trade_no", map.get("code"));
parm.put("nonce_str", PayUtil.getNonceStr());
parm.put("sign", PayUtil.getSign(parm, API_SECRET));
String restxml = HttpUtils.post(ORDER_PAY_QUERY, XmlUtil.xmlFormat(parm, false));
restmap = XmlUtil.xmlParse(restxml);
} catch (Exception e) {
LOG.error(e.getMessage(), e);
}
// if (CollectionUtil.isNotEmpty(restmap) && "SUCCESS".equals(restmap.get("result_code"))) {
// // 订单查询成功 处理业务逻辑
// LOG.info("订单查询:订单" + restmap.get("out_trade_no") + "支付成功");
// // 将微信订单号存入order 表中
//
// WebUtil.response(response, WebUtil.packJsonp(callback, JSON
// .toJSONString(new JsonResult(1, "订单支付成功", new ResponseData()), SerializerFeatureUtil.FEATURES)));
// } else {
// if (CollectionUtil.isNotEmpty(restmap)) {
// LOG.info("订单支付失败:" + restmap.get("err_code") + ":" + restmap.get("err_code_des"));
// }
// WebUtil.response(response, WebUtil.packJsonp(callback, JSON
// .toJSONString(new JsonResult(-1, "订单支付失败", new ResponseData()), SerializerFeatureUtil.FEATURES)));
// }
R r = new R();
if (CollectionUtil.isNotEmpty(restmap) && "SUCCESS".equals(restmap.get("result_code"))) {
// 订单查询成功 处理业务逻辑
iOrderService.updateOrderPayByOrderCode(map.get("code"),1);
return r.ok();
}else {
return R.error("支付失败");
}
}
/**
* 订单支付微信服务器异步通知
*
* @param request
* @param response
*/
@RequestMapping("/pay/notify")
public void orderPayNotify(HttpServletRequest request, HttpServletResponse response) {
LOG.info("[/order/pay/notify]");
response.setCharacterEncoding("UTF-8");
response.setContentType("text/xml");
try {
ServletInputStream in = request.getInputStream();
String resxml = FileUtil.readInputStream2String(in);
Map<String, String> restmap = XmlUtil.xmlParse(resxml);
LOG.info("支付结果通知:" + restmap);
// 将微信订单号存入order 表中
iOrderService.insertTradeNo(restmap.get("transaction_id"),restmap.get("out_trade_no"));
if ("SUCCESS".equals(restmap.get("result_code"))) {
// 订单支付成功 业务处理
String out_trade_no = restmap.get("out_trade_no"); // 商户订单号
// 通过商户订单判断是否该订单已经处理 如果处理跳过 如果未处理先校验sign签名 再进行订单业务相关的处理
String sing = restmap.get("sign"); // 返回的签名
restmap.remove("sign");
String signnow = PayUtil.getSign(restmap, API_SECRET);
if (signnow.equals(sing)) {
// 进行业务处理
LOG.info("订单支付通知: 支付成功,订单号" + out_trade_no);
// 支付成功
int count = iOrderService.updateOrderPayByOrderCode(out_trade_no,1);
// 处理成功后相应给响应xml
Map<String, String> respMap = new HashMap<>();
respMap = new HashMap<String, String>();
respMap.put("return_code", "SUCCESS"); //相应给微信服务器
respMap.put("return_msg", "OK");
String resXml = XmlUtil.xmlFormat(restmap, true);
response.getWriter().write(resXml);
} else {
LOG.info("订单支付通知:签名错误");
}
} else {
LOG.info("订单支付通知:支付失败," + restmap.get("err_code") + ":" + restmap.get("err_code_des"));
}
} catch (Exception e) {
LOG.error(e.getMessage(), e);
}
}
工具类
PayUtil
**
* created by wangch on 2019/8/9
*/
public class PayUtil {
/**
* 生成订单号
*
* @return
*/
public static String getTradeNo() {
// 自增8位数 00000001
return "TNO" + DatetimeUtil.formatDate(new Date(), DatetimeUtil.TIME_STAMP_PATTERN) + "00000001";
}
/**
* 退款单号
*
* @return
*/
public static String getRefundNo() {
// 自增8位数 00000001
return "RNO" + DatetimeUtil.formatDate(new Date(), DatetimeUtil.TIME_STAMP_PATTERN) + "00000001";
}
/**
* 退款单号
*
* @return
*/
public static String getTransferNo() {
// 自增8位数 00000001
return "TNO" + DatetimeUtil.formatDate(new Date(), DatetimeUtil.TIME_STAMP_PATTERN) + "00000001";
}
/**
* 返回客户端ip
*
* @param request
* @return
*/
public static String getRemoteAddrIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (StringUtil.isNotEmpty(ip) && !"unKnown".equalsIgnoreCase(ip)) {
// 多次反向代理后会有多个ip值,第一个ip才是真实ip
int index = ip.indexOf(",");
if (index != -1) {
return ip.substring(0, index);
} else {
return ip;
}
}
ip = request.getHeader("X-Real-IP");
if (StringUtil.isNotEmpty(ip) && !"unKnown".equalsIgnoreCase(ip)) {
return ip;
}
return request.getRemoteAddr();
}
/**
* 获取服务器的ip地址
*
* @param request
* @return
*/
public static String getLocalIp(HttpServletRequest request) {
return request.getLocalAddr();
}
public static String getSign(Map<String, String> params, String paternerKey) throws UnsupportedEncodingException {
return MD5Utils.getMD5(createSign(params, false) + "&key=" + paternerKey).toUpperCase();
}
/**
* 构造签名
*
* @param params
* @param encode
* @return
* @throws UnsupportedEncodingException
*/
public static String createSign(Map<String, String> params, boolean encode) throws UnsupportedEncodingException {
Set<String> keysSet = params.keySet();
Object[] keys = keysSet.toArray();
Arrays.sort(keys);
StringBuffer temp = new StringBuffer();
boolean first = true;
for (Object key : keys) {
if (key == null || StringUtil.isEmpty(params.get(key))) // 参数为空不参与签名
continue;
if (first) {
first = false;
} else {
temp.append("&");
}
temp.append(key).append("=");
Object value = params.get(key);
String valueStr = "";
if (null != value) {
valueStr = value.toString();
}
if (encode) {
temp.append(URLEncoder.encode(valueStr, "UTF-8"));
} else {
temp.append(valueStr);
}
}
return temp.toString();
}
/**
* 创建支付随机字符串
* @return
*/
public static String getNonceStr(){
String s = RandomUtil.randomString(RandomUtil.LETTER_NUMBER_CHAR, 32);
return RandomUtil.randomString(RandomUtil.LETTER_NUMBER_CHAR, 32);
}
/**
* 支付时间戳
* @return
*/
public static String payTimestamp() {
String s = Long.toString(System.currentTimeMillis() / 1000);
return Long.toString(System.currentTimeMillis() / 1000);
}
}
HttpUtils
**
* created by wangch on 2019/8/9
*/
public class HttpUtils {
private static final String DEFAULT_CHARSET = "UTF-8";
private static final int CONNECT_TIME_OUT = 5000; //链接超时时间3秒
private static final RequestConfig REQUEST_CONFIG = RequestConfig.custom().setConnectTimeout(CONNECT_TIME_OUT).build();
private static SSLContext wx_ssl_context = null; //微信支付ssl证书
static{
Resource resource = new ClassPathResource("wx_apiclient_cert.p12");
try {
KeyStore keystore = KeyStore.getInstance("PKCS12");
char[] keyPassword = ConfigUtil.getProperty("wx.mchid").toCharArray(); //证书密码
keystore.load(resource.getInputStream(), keyPassword);
wx_ssl_context = SSLContexts.custom().loadKeyMaterial(keystore, keyPassword).build();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* @description 功能描述: get 请求
* @param url 请求地址
* @param params 参数
* @param headers headers参数
* @return 请求失败返回null
*/
public static String get(String url, Map<String, String> params, Map<String, String> headers) {
CloseableHttpClient httpClient = null;
if (params != null && !params.isEmpty()) {
StringBuffer param = new StringBuffer();
boolean flag = true; // 是否开始
for (Entry<String, String> entry : params.entrySet()) {
if (flag) {
param.append("?");
flag = false;
} else {
param.append("&");
}
param.append(entry.getKey()).append("=");
try {
param.append(URLEncoder.encode(entry.getValue(), DEFAULT_CHARSET));
} catch (UnsupportedEncodingException e) {
//编码失败
}
}
url += param.toString();
}
String body = null;
CloseableHttpResponse response = null;
try {
httpClient = HttpClients.custom()
.setDefaultRequestConfig(REQUEST_CONFIG)
.build();
HttpGet httpGet = new HttpGet(url);
response = httpClient.execute(httpGet);
body = EntityUtils.toString(response.getEntity(), DEFAULT_CHARSET);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (response != null) {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (httpClient != null) {
try {
httpClient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return body;
}
/**
* @description 功能描述: get 请求
* @param url 请求地址
* @return 请求失败返回null
*/
public static String get(String url) {
return get(url, null);
}
/**
* @description 功能描述: get 请求
* @param url 请求地址
* @param params 参数
* @return 请求失败返回null
*/
public static String get(String url, Map<String, String> params) {
return get(url, params, null);
}
/**
* @description 功能描述: post 请求
* @param url 请求地址
* @param params 参数
* @return 请求失败返回null
*/
public static String post(String url, Map<String, String> params) {
CloseableHttpClient httpClient = null;
HttpPost httpPost = new HttpPost(url);
List<NameValuePair> nameValuePairs = new ArrayList<>();
if (params != null && !params.isEmpty()) {
for (Entry<String, String> entry : params.entrySet()) {
nameValuePairs.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
}
}
String body = null;
CloseableHttpResponse response = null;
try {
httpClient = HttpClients.custom()
.setDefaultRequestConfig(REQUEST_CONFIG)
.build();
httpPost.setEntity(new UrlEncodedFormEntity(nameValuePairs, DEFAULT_CHARSET));
response = httpClient.execute(httpPost);
body = EntityUtils.toString(response.getEntity(), DEFAULT_CHARSET);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (response != null) {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (httpClient != null) {
try {
httpClient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return body;
}
/**
* @description 功能描述: post 请求
* @param url 请求地址
* @param s 参数xml
* @return 请求失败返回null
*/
public static String post(String url, String s) {
CloseableHttpClient httpClient = null;
HttpPost httpPost = new HttpPost(url);
String body = null;
CloseableHttpResponse response = null;
try {
httpClient = HttpClients.custom()
.setDefaultRequestConfig(REQUEST_CONFIG)
.build();
httpPost.setEntity(new StringEntity(s, DEFAULT_CHARSET));
response = httpClient.execute(httpPost);
body = EntityUtils.toString(response.getEntity(), DEFAULT_CHARSET);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (response != null) {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (httpClient != null) {
try {
httpClient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return body;
}
/**
* @description 功能描述: post https请求,服务器双向证书验证
* @param url 请求地址
* @param params 参数
* @return 请求失败返回null
*/
public static String posts(String url, Map<String, String> params) {
CloseableHttpClient httpClient = null;
HttpPost httpPost = new HttpPost(url);
List<NameValuePair> nameValuePairs = new ArrayList<>();
if (params != null && !params.isEmpty()) {
for (Entry<String, String> entry : params.entrySet()) {
nameValuePairs.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
}
}
String body = null;
CloseableHttpResponse response = null;
try {
httpClient = HttpClients.custom()
.setDefaultRequestConfig(REQUEST_CONFIG)
.setSSLSocketFactory(getSSLConnectionSocket())
.build();
httpPost.setEntity(new UrlEncodedFormEntity(nameValuePairs, DEFAULT_CHARSET));
response = httpClient.execute(httpPost);
body = EntityUtils.toString(response.getEntity(), DEFAULT_CHARSET);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (response != null) {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (httpClient != null) {
try {
httpClient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return body;
}
/**
* @description 功能描述: post https请求,服务器双向证书验证
* @param url 请求地址
* @param s 参数xml
* @return 请求失败返回null
*/
public static String posts(String url, String s) {
CloseableHttpClient httpClient = null;
HttpPost httpPost = new HttpPost(url);
String body = null;
CloseableHttpResponse response = null;
try {
httpClient = HttpClients.custom()
.setDefaultRequestConfig(REQUEST_CONFIG)
.setSSLSocketFactory(getSSLConnectionSocket())
.build();
httpPost.setEntity(new StringEntity(s, DEFAULT_CHARSET));
response = httpClient.execute(httpPost);
body = EntityUtils.toString(response.getEntity(), DEFAULT_CHARSET);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (response != null) {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (httpClient != null) {
try {
httpClient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return body;
}
//获取ssl connection链接
private static SSLConnectionSocketFactory getSSLConnectionSocket() {
return new SSLConnectionSocketFactory(wx_ssl_context, new String[] {"TLSv1", "TLSv1.1", "TLSv1.2"}, null,
SSLConnectionSocketFactory.getDefaultHostnameVerifier());
}
}
XmlUtil
**
* created by wangch on 2019/8/9
*/
public class XmlUtil {
private static final String PREFIX_XML = "<xml>";
private static final String SUFFIX_XML = "</xml>";
private static final String PREFIX_CDATA = "<![CDATA[";
private static final String SUFFIX_CDATA = "]]>";
/**
* 转化成xml, 单层无嵌套
*
* @param map
* @param isAddCDATA
* @return
*/
public static String xmlFormat(Map<String, String> parm, boolean isAddCDATA) {
StringBuffer strbuff = new StringBuffer(PREFIX_XML);
if (CollectionUtil.isNotEmpty(parm)) {
for (Entry<String, String> entry : parm.entrySet()) {
strbuff.append("<").append(entry.getKey()).append(">");
if (isAddCDATA) {
strbuff.append(PREFIX_CDATA);
if (StringUtil.isNotEmpty(entry.getValue())) {
strbuff.append(entry.getValue());
}
strbuff.append(SUFFIX_CDATA);
} else {
if (StringUtil.isNotEmpty(entry.getValue())) {
strbuff.append(entry.getValue());
}
}
strbuff.append("</").append(entry.getKey()).append(">");
}
}
return strbuff.append(SUFFIX_XML).toString();
}
/**
* 解析xml
*
* @param xml
* @return
* @throws XmlPullParserException
* @throws IOException
*/
public static Map<String, String> xmlParse(String xml) throws XmlPullParserException, IOException {
Map<String, String> map = null;
if (StringUtil.isNotEmpty(xml)) {
InputStream inputStream = new ByteArrayInputStream(xml.getBytes());
XmlPullParser pullParser = XmlPullParserFactory.newInstance().newPullParser();
pullParser.setInput(inputStream, "UTF-8"); // 为xml设置要解析的xml数据
int eventType = pullParser.getEventType();
while (eventType != XmlPullParser.END_DOCUMENT) {
switch (eventType) {
case XmlPullParser.START_DOCUMENT:
map = new HashMap<String, String>();
break;
case XmlPullParser.START_TAG:
String key = pullParser.getName();
if (key.equals("xml"))
break;
String value = pullParser.nextText().trim();
map.put(key, value);
break;
case XmlPullParser.END_TAG:
break;
}
eventType = pullParser.next();
}
}
return map;
}
}
MD5Util
**
* created by wangch on 2019/8/9
*/
public class MD5Util {
/**
* MD5加密
* @param b
* @return
*/
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" };
}
四:总结
上述代码结合微信支付文档很好理解,耐心品味
参考了andy大神的写法,侵删
网友评论