java微信公众号支付
来源:互联网 发布:丑陋的中国人 知乎 编辑:程序博客网 时间:2024/06/05 04:52
最近公司突然叫我开发微信公众号相关的东西鸟,作为一个新人(其实也不怎么新了但是很菜就对了),开发过程还是有点坎坷的~~ 现在开发完啦,决定写下来供大家参考参考~ 因为比较粗心,估计还是有不少可以优化的地方,也可能有一些写得不好的地方,欢迎指正~
一.准备工作
所用环境 jdk1.7 spring+springmvc+mybatis
1.开通商户
设置秘钥key(设置路径:微信商户平台(pay.weixin.qq.com)–>账户设置–>API安全–>密钥设置)
2.拥有微信公众号,并开通微信支付
3.可外网访问的链接
4.实体类和工具类
注:实体类省略get set方法
统一下单必填请求信息实体UnifiedOrderRequest
public class UnifiedOrderRequest { private String appid; //公众账号ID private String mch_id; //商户号 private String nonce_str; //随机字符串 private String sign; //签名 private String body; //商品描述 private String out_trade_no; //商户订单号 private Integer total_fee; //总金额 private String spbill_create_ip; //终端IP private String notify_url; //通知地址 private String trade_type; //交易类型 private String openid; //用户标识 }
统一下单请求扩展信息实体UnifiedOrderRequestExt
public class UnifiedOrderRequestExt extends UnifiedOrderRequest{ private String device_info; //设备号 private String detail; //商品详情 private String attach; //附加数据 private String fee_type; //货币类型 private String time_start; //交易起始时间 private String time_expire; //交易结束时间 private String goods_tag; //商品标记 private String product_id; //商品ID private String limit_pay; //指定支付方式 }
统一下单返回信息实体UnifiedOrderRespose
timeStamp、paySign、packages为jsapi支付接口中必要参数。接收统一下单返回消息后,对消息进行处理,生成这三个字段。
public class UnifiedOrderRespose { private String return_code; //返回状态码 private String return_msg; //返回信息 private String appid; //公众账号ID private String mch_id; //商户号 private String device_info; //设备号 private String nonce_str; //随机字符串 private String sign; //签名 private String result_code; //业务结果 private String err_code; //错误代码 private String err_code_des; //错误代码描述 private String trade_type; //交易类型 private String prepay_id; //预支付交易会话标识 private String code_url; //二维码链接 private String timeStamp; //时间戳 [jsapi用] private String paySign; //签名 [jsapi] private String packages; //数据包(预订单id)[jsapi]}
Util-Redis
用于存放统一下单的信以及支付结果等,以防重复多次请求微信端。存放时间为2小时,保持和微信端一致。在此我使用jedis。由于项目差异性,具体实现不再赘述。
依赖jar
<dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-redis</artifactId> <version>${spring-redis.version}</version></dependency><dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>${redis.version}</version></dependency>
Util-签名验证
无论是统一下单还是jsapi中的支付接口,都有一个名为签名的参数。此参数根据不同接口不同参数动态生成,用于通讯时双方验证安全性。具体的生成算法如下。
参考文档:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_3
/** * 生成签名 * @param packageParams 接口参数map * @param key 设置路径:微信商户平台(pay.weixin.qq.com)-->账户设置-->API安全-->密钥设置 * @return */public static String createSign(SortedMap<String, Object> packageParams, String key) { StringBuffer sb = new StringBuffer(); // 第一步按字典序排列参数 Set es = packageParams.entrySet(); Iterator it = es.iterator(); while (it.hasNext()) { Map.Entry entry = (Map.Entry) it.next(); String k = String.valueOf(entry.getKey()); if(!StringUtils.isEmpty(entry.getValue())){ String v = String.valueOf(entry.getValue()); // 为空不参与签名、参数名区分大小写 if (null != v && !"".equals(v) && !"sign".equals(k) && !"key".equals(k)) { sb.append(k + "=" + v + "&"); } } } // 第二步拼接key sb.append("key=" + key); // 第三步MD5加密 String sign = MD5Util.turnToMd5(sb.toString()).toUpperCase(); return sign;}
Util-跨域请求
1.用于请求微信端统一下单接口
/** * 跨域请求 * @param xmlStr 请求xml * @param url 请求地址 * @return */public static Object getDataFromURL(String xmlStr, String url) { Object resObj=null; try { URLConnection conn = new URL(url).openConnection(); // 加入数据 conn.setRequestProperty("Content-Type", "application/json"); conn.setDoOutput(true); BufferedOutputStream buffOutStr = new BufferedOutputStream(conn.getOutputStream()); buffOutStr.write(xmlStr.getBytes()); buffOutStr.flush(); buffOutStr.close(); // 获取输入流 BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream())); String line = null; StringBuffer sb = new StringBuffer(); while ((line = reader.readLine()) != null) { sb.append(line); } XStream xStream = new XStream(new XppDriver(new XmlFriendlyNameCoder("_-", "_")));// 解决转换时'_' 变成 '__'的问题 // 将请求返回的内容通过xStream转换为UnifiedOrderRespose对象 xStream.alias("xml", UnifiedOrderRespose.class); resObj = xStream.fromXML(sb.toString()); } catch (Exception e) { e.printStackTrace(); } return resObj;}
- 用于一般跨域请求
public JSONObject getDataFromURL(String strURL, Map<String, Object> param, String requestType) throws Exception { URL url = new URL(strURL); URLConnection conn = url.openConnection(); conn.setDoOutput(true); conn.setDoInput(true); OutputStreamWriter writer = null; if (param != null) { if ("POST".equals(requestType)) { conn.setRequestProperty("Content-Type", "application/json"); writer = new OutputStreamWriter(conn.getOutputStream()); if (!StringUtils.isEmpty(param.get("JSON"))) { writer.write(param.get("JSON").toString()); } else { writer.write(new StringBuilder(100 << 4).toString()); } } else { writer = new OutputStreamWriter(conn.getOutputStream()); JSONObject jsonRes = JSONObject.fromObject(param); StringBuilder sb = new StringBuilder(jsonRes.size() << 4);// 4次方 final Set<String> keys = jsonRes.keySet(); for (final String key : keys) { final Object value = jsonRes.get(key); sb.append(key); // 不能包含特殊字符 sb.append('='); sb.append(value); sb.append('&'); } // 将最后的 '&' 去掉 sb.deleteCharAt(sb.length() - 1); writer.write(sb.toString()); } } else { writer = new OutputStreamWriter(conn.getOutputStream()); writer.write(new StringBuilder(100 << 4).toString()); } writer.flush(); writer.close(); InputStreamReader reder = new InputStreamReader(conn.getInputStream(), "utf-8"); BufferedReader breader = new BufferedReader(reder); String content = null; String result = ""; while ((content = breader.readLine()) != null) { result += content; } JSONObject jsonRes = JSONObject.fromObject(result); return jsonRes;}
Util-读取配置文件内容
/** * 读取配置文件 * * @param fileName * @param key * @return * @throws Exception */public static String getPropertyVal(String fileName, String key) throws Exception { Common c = new Common(); String classPath = c.getPath(); Properties pps = new Properties(); pps.load(new FileInputStream(classPath + fileName)); String strValue = pps.getProperty(key); return strValue;}/*** 获取路径*/public String getPath() { return this.getClass().getResource("/").getPath();}
配置文件 weChatConfig.properties
AppID=公众号唯一标识AppSecret=xxxxxx#商户号mch_id=xxxxxxx#支付keysignKey=xxxxxxx#支付回调urlpayNoticeUrl=http://xxx.xxxx.xxx/wechat/payResponseNoticegrant_type_client=client_credentialgrant_type_auth=authorization_codegetTokenUrl=https://api.weixin.qq.com/cgi-bin/tokengetTokenUrlForGetUser=https://api.weixin.qq.com/sns/oauth2/access_tokengetUserListUrl=https://api.weixin.qq.com/cgi-bin/user/getgetUserInfo=https://api.weixin.qq.com/cgi-bin/user/infounifiedorder=https://api.mch.weixin.qq.com/pay/unifiedordersignToken=xxxxxx
二.流程
- 第一步,获取用户openid。
- 第二步,点击支付按钮后,调用统一下单接口,获取订单id,并生成 timeStamp、paySign、packages三个参数加上appid、nonceStr一并返回给页面。
- 第三步,页面调用jsapi支付接口,完成支付。
- 第四步,支付回调处理。(微信端异步处理)。
1.获取用户openid
流程简述:严格按照微信授权步骤,准备好授权链接,将目标页面作为参数放入授权链接中。微信端返回code至目标页面,页面调用controller方法,在方法中用code等参数请求微信获取用户openid的接口,将openid存入session备用。
1.1 链接
https://open.weixin.qq.com/connect/oauth2/authorize?appid=公众号标识&redirect_uri=encode之后的目标页面链接&response_type=code&scope=snsapi_base&state=STATE#wechat_redirect
说明:snsapi_base为静默授权,用于仅需要取得openid时。其他需要请参考接口文档。
1.2 目标页面js接收code
var code=GetQueryString("code");if(code != "" && code != null){ $.ajax({ url:"/wechat/dealWechatCode", type:"post", dataType:"json", async:true, data:{ code:code }, success:function(data){}, error:function(data){} })}
小工具
//获取地址栏参数方法function GetQueryString(name){ var reg = new RegExp("(^|&)"+ name +"=([^&]*)(&|$)"); var r = window.location.search.substr(1).match(reg); if(r!=null){ return decodeURI(r[2])} return null;}
1.3 处理code生成openid并存入session
controller层:
/** * 处理code * @param code * */@RequestMapping(value = "/dealWechatCode", method = RequestMethod.POST)public void dealWechatCode(String code, HttpServletRequest request, HttpServletResponse response) { LOGGER.info("POST请求处理code"); HttpSession session=request.getSession(); try { Object openIdobj = GetAPIResult.getToken(code).get("openid"); if(openIdobj!=null){ //存入session session.setAttribute("openId", openIdobj.toString()); System.out.println(openIdobj.toString()); } } catch (Exception e) { e.printStackTrace(); }}
其中getToken方法用于用code换取openid:
/*** 获取token(获取用户信息)* @param code* @return* @throws Exception*/public static Map<String, String> getToken(String code) throws Exception{ Map<String,String> resMap=new HashMap<String, String>(); Map<String,Object> paraMap=new HashMap<String, Object>(); paraMap.put("grant_type", Common.getPropertyVal("weChatConfig.properties", "grant_type_auth")); paraMap.put("appid", Common.getPropertyVal("weChatConfig.properties", "AppID")); paraMap.put("secret", Common.getPropertyVal("weChatConfig.properties", "AppSecret")); paraMap.put("code", code); JSONObject obj=util.getDataFromURL(Common.getPropertyVal("weChatConfig.properties", "getTokenUrlForGetUser"), paraMap,"GET"); if(obj.get("access_token")!=null&&obj.get("openid")!=null){ resMap.put("access_token", obj.get("access_token").toString()); resMap.put("openid", obj.get("openid").toString()); } return resMap;}
2.页面点击“支付”调用方法submitOrder()
function submitOrder(){ var orderInfo={ "body":"xxxx-xxxx", //商品信息 "out_trade_no":"10f0f6f24c304b5eb955a3d3b816fa7b", //系统生成订单号 "total_fee":1 //支付金额 }; $.ajax({ type : "post", async: false, data:{orderInfo:JSON.stringify(orderInfo)}, url : "/wechat/test/pay", dataType : "json", success : function(data) { if (data.code == 200) { var appid = data.obj.appid; var nonceStr = data.obj.nonce_str; var packages = data.obj.packages; var timeStamp=data.obj.timeStamp; var paySign=data.obj.paySign; //调取JSSDK支付接口 pay(appid,nonceStr,packages,timeStamp,paySign); } } }); }
3.统一下单
controller层
@RequestMapping(value = "/test/pay", method = RequestMethod.POST)@ResponseBodypublic PageInfo pay(HttpServletRequest request, HttpServletResponse response,String orderInfo) { PageInfo pageInfo=new PageInfo(); //接受支付信息 JSONObject jsonObj=JSONObject.fromObject(HtmlUtils.htmlUnescape(orderInfo));//消除json中数据encode后可能产生的影响 UnifiedOrderRequest wechatPay=(UnifiedOrderRequest)JSONObject.toBean(jsonObj, UnifiedOrderRequest.class); //验证必要参数 if(StringUtils.isBlank(wechatPay.getBody())){ pageInfo.setCode(1001); pageInfo.setMsg("参数:商品描述[body]不可为空"); return pageInfo; } if(StringUtils.isBlank(wechatPay.getOut_trade_no())){ pageInfo.setCode(1001); pageInfo.setMsg("参数:订单号[out_trade_no]不可为空"); return pageInfo; } if(wechatPay.getTotal_fee()==null&&wechatPay.getTotal_fee()==0){ pageInfo.setCode(1001); pageInfo.setMsg("参数:订单金额[total_fee]不可为0或null"); return pageInfo; } LOGGER.info("订单"+wechatPay.getOut_trade_no()+"请求支付"); //获取用户orderId HttpSession session = request.getSession(); Object openId=session.getAttribute("openId"); if(openId!=null){ wechatPay.setOpenid(openId.toString()); }else{ pageInfo.setCode(1001); pageInfo.setMsg("openId未取到,请检查访问渠道"); return pageInfo; } wechatPay.setSpbill_create_ip(Common.getIpAddr(request)); pageInfo=weChatService.pay(wechatPay); return pageInfo;}
service层:
@Overridepublic PageInfo pay(UnifiedOrderRequest unifiedOrder) { PageInfo pageInfo = new PageInfo(); try { if (unifiedOrder.getOut_trade_no() != null) { Object redisPrePayStr = redisGet.getRedisStringResult("weChatPay_" + unifiedOrder.getOut_trade_no()); if (!StringUtils.isEmpty(redisPrePayStr)) { UnifiedOrderRespose redisPrePay = (UnifiedOrderRespose) JSONObject .toBean(JSONObject.fromObject(redisPrePayStr), UnifiedOrderRespose.class); pageInfo.setObj(redisPrePay); pageInfo.setCode(200); } else { // 已失效或不存在 // 准备预订单数据 unifiedOrder.setAppid(Common.getStaticProperty("weChatConfig.properties", "AppID")); unifiedOrder.setMch_id(Common.getStaticProperty("weChatConfig.properties", "mch_id")); unifiedOrder.setNonce_str(Common.generateRandomCharArray(32)); unifiedOrder.setNotify_url(Common.getStaticProperty("weChatConfig.properties", "payNoticeUrl"));// 通知结果回调地址 unifiedOrder.setTrade_type("JSAPI");// jsapi方式用于微信内置浏览器内 // 支付key String key = Common.getStaticProperty("weChatConfig.properties", "signKey"); // 获取签名 SortedMap<String, Object> packageParams = new TreeMap<String, Object>(); packageParams.put("appid", unifiedOrder.getAppid()); packageParams.put("body", unifiedOrder.getBody()); packageParams.put("mch_id", unifiedOrder.getMch_id()); packageParams.put("nonce_str", unifiedOrder.getNonce_str()); packageParams.put("notify_url", unifiedOrder.getNotify_url()); packageParams.put("openid", unifiedOrder.getOpenid()); packageParams.put("out_trade_no", unifiedOrder.getOut_trade_no()); packageParams.put("spbill_create_ip", unifiedOrder.getSpbill_create_ip()); packageParams.put("total_fee", unifiedOrder.getTotal_fee()); packageParams.put("trade_type", unifiedOrder.getTrade_type()); unifiedOrder.setSign(SignUtil.createSign(packageParams, key)); // 转换为xml XStream xStream = new XStream(new XppDriver(new XmlFriendlyNameCoder("_-", "_"))); xStream.alias("xml", UnifiedOrderRequest.class); String xmlStr = xStream.toXML(unifiedOrder); // 请求微信统一下单接口 UnifiedOrderRespose respose = (UnifiedOrderRespose) UrlUtil.getDataFromURL(xmlStr, Common.getStaticProperty("weChatConfig.properties", "unifiedorder")); // 根据微信文档return_code // result_code都为SUCCESS的时候才会返回prepay_id和code_url if (null != respose && "SUCCESS".equals(respose.getReturn_code()) && "SUCCESS".equals(respose.getResult_code())) { //jsapi所需参数 String timeStamp = String.valueOf(System.currentTimeMillis()); String packages = "prepay_id=" + respose.getPrepay_id(); respose.setTimeStamp(timeStamp); respose.setPackages(packages); // 获取jsapi所需签名 SortedMap<String, Object> paramsMap = new TreeMap<String, Object>(); paramsMap.put("appId", respose.getAppid()); paramsMap.put("timeStamp", timeStamp); paramsMap.put("nonceStr", respose.getNonce_str()); paramsMap.put("package", packages); paramsMap.put("signType", "MD5"); respose.setPaySign(SignUtil.createSign(paramsMap, key)); // 将处理好的数据放到obj中 返回给页面 pageInfo.setObj(respose); pageInfo.setCode(200); // 存入redis 有效期为2小时(与微信官方一致) 当同一订单重复请求时 不再请求生成新的预支付订单码 String resposeStr = JSONObject.fromObject(respose).toString(); redisSet.setRedisStringResult("weChatPay_" + unifiedOrder.getOut_trade_no(), resposeStr, 2, TimeUnit.HOURS); } else { pageInfo.setCode(1020); pageInfo.setMsg("微信支付平台错误代码:" + respose.getReturn_code() + ":" + respose.getReturn_msg()); } } } else { pageInfo.setCode(1020); pageInfo.setMsg("订单号为空"); } } catch (Exception e) { pageInfo.setCode(500); pageInfo.setMsg(e.getMessage()); e.printStackTrace(); } return pageInfo;}
4.页面调用jsapi支付接口
//微信支付function pay(appid,nonceStr,packages,timeStamp,paySign){ //判断是否为微信浏览器 //如果是 if(is_weixin()){ if (typeof WeixinJSBridge == "undefined") { if (document.addEventListener) { document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false); } else if (document.attachEvent) { document.attachEvent('WeixinJSBridgeReady', onBridgeReady); document.attachEvent('onWeixinJSBridgeReady', onBridgeReady); } } else { onBridgeReady(appid,nonceStr,packages,timeStamp,paySign); } }else{ alert("请在微信中打开此链接"); }}function onBridgeReady(appid,nonceStr,packages,timeStamp,paySign) { WeixinJSBridge.invoke('getBrandWCPayRequest', { "appId" : appid, //公众号名称,由商户传入 "timeStamp" : timeStamp, //时间戳,自1970年以来的秒数 "nonceStr" : nonceStr, //随机串 "package" : packages, //预订单id "signType" : "MD5", //微信签名方式 "paySign" : paySign //签名 }, function(res) { alert(res.err_msg); if (res.err_msg == "get_brand_wcpay_request:ok") { } // 使用以上方式判断前端返回,微信团队郑重提示:res.err_msg将在用户支付成功后返回ok,但并不保证它绝对可靠。 });}
5.支付回调
注意要验证微信端返回的参数和签名是否匹配,以防居心不良的人模仿回调此接口假装他支付过了!喵的
controller层:
/** * 支付回调[此接口配置在统一下单的参数值] * @param request * @param response * @return */@RequestMapping(value = "/payResponseNotice", method = RequestMethod.POST)public String payResponseNotice(HttpServletRequest request, HttpServletResponse response) { LOGGER.info("支付结果回调"); String resXml=null; try { InputStream stream=request.getInputStream(); //接收支付结果 XStream xStream = new XStream(new XppDriver(new XmlFriendlyNameCoder("_-", "_"))); xStream.alias("xml", OrderRequest.class); OrderRequest xmlStr = (OrderRequest)xStream.fromXML(stream); //处理 resXml=weChatService.payResponseNotice(xmlStr);//service } catch (Exception e) { e.printStackTrace(); } return resXml;}
service层:
@Overridepublic String payResponseNotice(OrderRequest orderRequest) { Object redisPayNoticeStr = redisGet.getRedisStringResult("weChatPayNotice_" + orderRequest.getOut_trade_no()); String res = "<xml>" + "<return_code><![CDATA[{0}]]></return_code>" + "<return_msg><![CDATA[{1}]]></return_msg>" + "</xml>"; if (!StringUtils.isEmpty(redisPayNoticeStr)) { // 已经处理过 return MessageFormat.format(res, "SUCCESS", "OK"); } else { // 判断支付状态 if ("SUCCESS".equals(orderRequest.getReturn_code())) { // 通讯成功 // 为防止数据泄露造成的假通知,验证签名 // 支付key String key = Common.getStaticProperty("weChatConfig.properties", "signKey"); Map<String, Object> map = Common.beanToMap(orderRequest); SortedMap<String, Object> packageParams = new TreeMap<String, Object>(map); packageParams.remove("sign"); String reqSign = orderRequest.getSign(); String mySign = SignUtil.createSign(packageParams, key); if (reqSign.equals(mySign)) { // 验证成功 OrdersInfo orderInfo = new OrdersInfo(); orderInfo.setId(orderRequest.getOut_trade_no()); if ("SUCCESS".equals(orderRequest.getResult_code())) { System.out.println( "订单[" + orderRequest.getOut_trade_no() + "]于" + orderRequest.getTime_end() + "已支付成功!"); } else { // 支付失败 System.out.println("订单[" + orderRequest.getOut_trade_no() + "]于" + orderRequest.getTime_end() + "支付失败!错误" + orderRequest.getErr_code() + ":" + orderRequest.getErr_code_des()); } redisSet.setRedisStringResult("weChatPayNotice_" + orderRequest.getOut_trade_no(), 1, 2, TimeUnit.HOURS); // 返回 return MessageFormat.format(res, "SUCCESS", "OK"); } else { return MessageFormat.format(res, "FAIL", "ops,我可能接到了假回应!"); } } else { return MessageFormat.format(res, "FAIL", "接收通知失败"); } }}
三、关于一些坑
1.关于jsapi报 “签名错误” “total_fee”等错误
其实开始写还是挺顺利的,直到调用jsapi这一步。
这个接口参数有五个,一开始我是在js里处理参数的。因为有个参数是 package:”prepay_id=XXXXXXXX” ,怎么请求都不对,开始提示签名错误,后来将参数encode后,签名搞对了。可是又报类似 total_fee参数为空这种错。方了一下午。。。 因为total_fee是统一下单接口就传过去的参数,这一步已经执行完毕了,完全搞不懂为什么支付的时候又报出来。于是又怀疑到了package这个诡异参数。。。最后,把这些参数在java里处理完毕直接返回给页面,终于ok了。我坐在工位上忍不住给自己鼓了一会儿掌。哈哈哈。
当然首先还是要检查自己的参数到底是否正确,签名的部分可以先用微信提供的签名验证工具验证下自己生成的签名是否正确。
2.关于非微信内置浏览器的支付办法
其实需求是要求在任何浏览器都可以支付的。找了很久,找到了此文档 https://pay.weixin.qq.com/wiki/doc/api/wap.php?chapter=15_3
也确实有一些网站是可以的。但是打了客服,表示并没此方式。 我估计这个方式是单独提供给一些公司的。。。
尝试过weixin://www.XXXXX.com 的方式唤醒微信浏览器,BUT,是我太天真。。确实可以在safari浏览器中唤醒微信,但是无法直接跳入微信浏览器。 所以不行啊~~
3.关于无法调起输入密码界面/过载动画一闪而过的问题
前两天支付碰见了一个问题,点击支付时,出现“微信支付”和省略号的过载动画,动画一闪而过,并没有调起后续输入密码界面。这个问题表现在部分安卓手机上。
尝试的解决方案:
1、更换支付秘钥key:搜索了下有些文章里写了如果更换支付授权目录必须要更换key。正巧这个问题是我将链接更换到线上后出现的,于是真的跑去更换了key,然鹅并没什么卵用。。。
2、检查统一下单返回的预订单id:打了客服,客服表示如果出现一闪而过的问题,一般是用jsapi请求支付时,预订单id参数有问题。于是我兢兢业业的检查了这个,发现并没问题。。
3、信号问题:在办公室测试基本每次都一闪而过,在楼外三次里大概有一次能成功。。我也说不清,可能是有一些影响吧。。
4、缓存:清了也没用,嗯。
比较尴尬的是到现在我也不知道到底因为什么,因为第二天这个问题就莫名好了呢。可能真的是信号问题吧。写出来下次遇到少走些弯路吧~~
- Java微信公众号支付
- 微信公众号支付Java DEMO
- 微信公众号支付开发 --Java
- java开发微信公众号支付
- java微信公众号支付案例
- java微信公众号支付
- java微信公众号支付接口
- java开发微信公众号支付
- java微信公众号支付授权
- java微信公众号支付
- java微信公众号支付案例
- 微信支付-公众号支付-JSAPI调用(Java)
- 微信支付-公众号支付(java实现)
- 微信支付-公众号支付(java实现)
- Java微信支付全教程demo【公众号支付】
- 微信支付-公众号支付(java实现)
- 微信公众号支付
- 微信公众号支付
- 线段树
- WebSocket/WebWorker/WebStorage(HTML5的一些新特性)
- 二叉树前序、中序、后序非递归遍历
- 配置集群ssh
- java客户端操作hdfs权限问题初探
- java微信公众号支付
- 最小生成树(prime)
- C# + Socket断线重连
- 解决微信及360浏览器无法读取本地图片问题
- IntelliJ IDEA 与 Gradle + Spring项目的初次尝试
- linux常用命令——3.文件打包压缩
- 软考项目管理师备考的几点建议和思考
- Linux pthread 入门
- 移动端全局css