java_小程序和Android app开通微信支付
近段时间我们app增加了几个第三方收费的接口,所以一直免费的app现在要开始收费了,app界面上的改造就是在收费的功能上增加了vip标示,只有充值了vip,这些功能才能查看。
这款app有Android版、ios版和小程序版,接下来我就切入正题,总结记录一下开发小程序和Android微信支付的过程。
首先呢就是结合自己业务场景,设计表结构,这就不细说了,业务场景都不同,简单带过。
其次需要调研微信开放平台,这里贴上官网地址 https://pay.weixin.qq.com/wiki/doc/apiv3/index.shtml 注意:目前文档支持新旧两个版本,我这里是用的旧版V2版
这个是小程序具体开发文档地址 https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=7_7&index=5
下边贴上我的核心代码,具体说一下踩过的坑
注意:有个前提就是开发前,需要先去微信申请 商户号,这个很关键,需要提前申请,这个很耗时
1.首先pom文件加入jar包依赖
<dependency> <groupId>com.github.wxpay</groupId> <artifactId>wxpay-sdk</artifactId> <version>0.0.3</version> </dependency>
2.获取预支付交易会话标识
/** * 获取预支付交易会话标识 * @param username * @param openid * @return */ @RequestMapping("/getPrepayId") @ResponseBody public ResponseJSON getPrepayId(String username, String openid, HttpServletRequest request) { if(StringUtils.isNotBlank(openid)) { return wxpay(username, openid, request); //小程序 需要openid }else { return apppay(username, request); //Android 不需要openid } } /** * 小程序支付 * @return */ private ResponseJSON wxpay(String username, String openid, HttpServletRequest request){ ResponseJSON responseJSON = ResponseJSON.instance(); try { responseJSON.setSuccess(false); Map<String, String> respData; WxpayParam wxpayParam = new WxpayParam(); OurWxPayConfig ourWxPayConfig = new OurWxPayConfig(); WXPay wxPay = new WXPay(ourWxPayConfig, SignType.MD5, false); //根据微信支付api来设置 Map<String, String> data = new HashMap<>(); String seckey = ""; data.put("nonce_str", RandomUtil.get32Num()); // 随机字符串小于32位 data.put("trade_type", "JSAPI"); //小程序 JSAPI data.put("mch_id", ourWxPayConfig.getMchID()); //商户号 data.put("appid", ourWxPayConfig.getAppID());//小程序id seckey = ourWxPayConfig.getKey(); data.put("openid", openid); data.put("notify_url", notifyUrl); //回调地址 这个是支付成功后回调我们系统的地址,这里有个坑,url必须是http,https调不通 data.put("spbill_create_ip", IPUtils.getIpAddr(request)); //终端ip data.put("total_fee", wxpayParam.getTotalFee()); //订单总金额 data.put("out_trade_no", RandomUtil.creatid(30)); //交易号 data.put("body", wxpayParam.getBody());//商品描述 String s = WXPayUtil.generateSignature(data, seckey); //签名 data必须是这几个字段,不然会提示加密失败 data.put("sign", s); respData = wxPay.unifiedOrder(data);if (respData.get("return_code").equals("SUCCESS")) { //返回给小程序端的参数,小程序端再调起支付接口 Map<String, String> repData = new HashMap<>(); repData.put("appId", ourWxPayConfig.getAppID()); repData.put("nonceStr", RandomUtil.get32Num()); repData.put("package", "prepay_id=" + respData.get("prepay_id")); repData.put("signType","MD5"); repData.put("timeStamp", String.valueOf(System.currentTimeMillis() / 1000)); String sign = WXPayUtil.generateSignature(repData, seckey); //签名 repData必须也是这几个字段,不然小程序拉起支付会失败 repData.put("paySign", sign); repData.put("prepay_id", respData.get("prepay_id")); repData.put("out_trade_no", data.get("out_trade_no")); repData.put("username", username);// 这个参数是我们平台的手机号 repData.put("amount", wxpayParam.getTotalFee()); responseJSON.setSuccess(true); responseJSON.setData(repData); } } catch (Exception e) { e.printStackTrace(); } return responseJSON; } /** * 安卓支付 * @param username * @param request * @return */ private ResponseJSON apppay(String username, HttpServletRequest request){ ResponseJSON responseJSON = ResponseJSON.instance(); /** wxPay.unifiedOrder 这个方法中调用微信统一下单接口 */ Map<String, String> respData; try { responseJSON.setSuccess(false); WxpayParam wxpayParam = new WxpayParam(); OurAppPayConfig ourAppPayConfig = new OurAppPayConfig(); WXPay wxPay = new WXPay(ourAppPayConfig, SignType.MD5, false); //根据微信支付api来设置 Map<String, String> data = new HashMap<>(); String seckey = ""; data.put("nonce_str", RandomUtil.get32Num()); // 随机字符串小于32位 data.put("trade_type", "APP"); //小程序 JSAPI data.put("mch_id", ourAppPayConfig.getMchID()); //商户号 data.put("appid", ourAppPayConfig.getAppID());//小程序id 注意:安卓支付 商户号和appID可以用小程序的,但是需要在微信平台绑定并开通 seckey = ourAppPayConfig.getKey(); data.put("notify_url", notifyUrl); //回调地址 注意的地方同上 data.put("spbill_create_ip", IPUtils.getIpAddr(request)); //终端ip data.put("total_fee", wxpayParam.getTotalFee()); //订单总金额 data.put("out_trade_no", RandomUtil.creatid(30)); //交易号 data.put("body", wxpayParam.getBody());//商品描述 String s = WXPayUtil.generateSignature(data, seckey); //签名 data.put("sign", s); respData = wxPay.unifiedOrder(data);if (respData.get("return_code").equals("SUCCESS")) { //返回给APP端的参数,APP端再调起支付接口 Map<String, String> repData = new HashMap<>(); repData.put("appid", ourAppPayConfig.getAppID()); repData.put("partnerid", ourAppPayConfig.getMchID()); repData.put("prepayid", respData.get("prepay_id")); repData.put("package", "Sign=WXPay"); repData.put("noncestr", RandomUtil.get32Num()); repData.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000)); for(String key :repData.keySet()) { String value1 = repData.get(key); } String sign = WXPayUtil.generateSignature(repData, seckey); //签名 repData.put("sign", sign); repData.put("prepay_id", respData.get("prepay_id")); repData.put("out_trade_no", data.get("out_trade_no")); repData.put("username", username); repData.put("amount", wxpayParam.getTotalFee()); responseJSON.setSuccess(true); responseJSON.setData(repData); }else { responseJSON.setMessage(respData.get("return_msg")); } } catch (Exception e) { e.printStackTrace(); } return responseJSON; }
下边添加一下代码中引用的几个工具类
IPUtils.java
import javax.servlet.http.HttpServletRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.StringUtils; public class IPUtils { private static Logger logger = LoggerFactory.getLogger(IPUtils.class); /** * 获取IP地址 * * 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址 * 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址 */ public static String getIpAddr(HttpServletRequest request) { String ip = null; try { ip = request.getHeader("x-forwarded-for"); if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("Proxy-Client-IP"); } if (StringUtils.isEmpty(ip) || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("WL-Proxy-Client-IP"); } if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_CLIENT_IP"); } if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_X_FORWARDED_FOR"); } if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); } } catch (Exception e) { logger.error("IPUtils ERROR ", e); } //使用代理,则获取第一个IP地址 if(StringUtils.isEmpty(ip) && ip.length() > 15) { if(ip.indexOf(",") > 0) { ip = ip.substring(0, ip.indexOf(",")); } } return ip; } }
OurWxPayConfig.java
import java.io.InputStream; import com.github.wxpay.sdk.WXPayConfig; public class OurWxPayConfig implements WXPayConfig{ @Override public String getAppID() { return "123"; //改成自己的 } @Override public String getMchID() { return "123"; //改成自己的 } @Override public String getKey() { return "123456"; //自己设定 然后同步到微信平台 } @Override public InputStream getCertStream() { return null; } @Override public int getHttpConnectTimeoutMs() { return 0; } @Override public int getHttpReadTimeoutMs() { return 0; } }
RandomUtil.java
import java.text.SimpleDateFormat; import java.util.Date; import java.util.Random; public class RandomUtil { /** * 生成32位随机数 * @return */ public static String get32Num() { Random rand = new Random(); StringBuffer sb=new StringBuffer(); for (int i=1;i<=31;i++){ int randNum = rand.nextInt(9)+1; String num=randNum+""; sb=sb.append(num); } String random=String.valueOf(sb); return random; } /** * 生成订单号 * @param endIndex * @return */ public static String creatid(int endIndex){ String id=""; SimpleDateFormat sf = new SimpleDateFormat("yyyyMMddHHmmss"); String tc = sf.format(new Date()); double du = Math.random(); String random = String.valueOf(du); random = random.length()<18?random+"000":random; String sj = random.substring(2, 18); id=(tc+sj).substring(0, endIndex); return id; } public static void main(String[] args) { RandomUtil randomUtil= new RandomUtil(); System.out.println(randomUtil.creatid(30)); } }
WxpayParam.java
import java.math.BigDecimal; public class WxpayParam { /** 微信支付的金额是String类型 并且是以分为单位 * 下面举个例子单位是元是怎么转为分的 * */ BigDecimal totalPrice = new BigDecimal(399); //此时的单位是元 private String body = "VIP会员"; //商品名称 private String totalFee = totalPrice.multiply(new BigDecimal(100)).toBigInteger().toString(); /** 随机数字字符串*/ private String outTradeNo = "4784984230432842944"; public String getBody() { return body; } public void setBody(String body) { this.body = body; } public String getTotalFee() { return totalFee; } public void setTotalFee(String totalFee) { this.totalFee = totalFee; } public String getOutTradeNo() { return outTradeNo; } public void setOutTradeNo(String outTradeNo) { this.outTradeNo = outTradeNo; } }
注意:Android 11系统策略更新,需要开发者适配 这里贴上链接 对应地方需要做改动,否则不能拉起支付 https://open.weixin.qq.com/cgi-bin/announce?action=getannouncement&key=11600155960jI9EY&version=&lang=zh_CN&token=
最后贴上支付成功后回调接口的代码
/** * 支付回调 * @param request * @param response * @throws Exception */ @RequestMapping("/payNotify") @ResponseBody public void payNotify(HttpServletRequest request,HttpServletResponse response) throws Exception { logger.info("已进入微信支付回调接口-----订单支付完成回调更新订单状态"); // 读取参数 InputStream inputStream; StringBuffer sb = new StringBuffer(); 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(); // 解析xml成map Map<String, String> map = new HashMap<String, String>(); map = WXPayUtil.xmlToMap(sb.toString()); // 过滤空 设置TreeMap SortedMap<Object, Object> packageParams = new TreeMap<Object, Object>(); Iterator it = map.keySet().iterator(); while (it.hasNext()) { String parameter = (String) it.next(); String parameterValue = map.get(parameter); String v = ""; if (null != parameterValue) { v = parameterValue.trim(); } packageParams.put(parameter, v); logger.info("微信支付回调"+"key:"+parameter +"&&&&&&&&&&&value:"+ v); } // 账号信息 String key = ourWxPayConfig.getKey(); // 判断签名是否正确 if (WXPayUtil.isSignatureValid(map, key)) { String resXml = ""; if ("SUCCESS".equals((String) packageParams.get("result_code"))) { logger.info("支付成功"); userVipinfoService.updateByOrderId(packageParams);//这里是更新订单表的状态,改为支付成功 // 通知微信.异步确认成功.必写.不然会一直通知后台.八次之后就认为交易失败了. resXml = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>" + "<return_msg><![CDATA[OK]]></return_msg>" + "</xml> "; } else { logger.info("支付失败,错误信息:" + packageParams.get("err_code")); 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 { logger.info("通知签名验证失败"); } }
这个是回调用的工具类 WXPayUtil.java
import java.io.ByteArrayInputStream; import java.io.InputStream; import java.io.StringWriter; import java.util.*; import java.security.MessageDigest; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import com.github.wxpay.sdk.WXPayConstants.SignType; public class WXPayUtil { /** * XML格式字符串转换为Map * * @param strXML XML字符串 * @return XML数据转换后的Map * @throws Exception */ public static Map<String, String> xmlToMap(String strXML) throws Exception { Map<String, String> data = new HashMap<String, String>(); DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); DocumentBuilder documentBuilder= documentBuilderFactory.newDocumentBuilder(); InputStream stream = new ByteArrayInputStream(strXML.getBytes("UTF-8")); org.w3c.dom.Document doc = documentBuilder.parse(stream); doc.getDocumentElement().normalize(); NodeList nodeList = doc.getDocumentElement().getChildNodes(); for (int idx=0; idx<nodeList.getLength(); ++idx) { Node node = nodeList.item(idx); if (node.getNodeType() == Node.ELEMENT_NODE) { org.w3c.dom.Element element = (org.w3c.dom.Element) node; data.put(element.getNodeName(), element.getTextContent()); } } try { stream.close(); } catch (Exception ex) { } return data; } /** * 将Map转换为XML格式的字符串 * * @param data Map类型数据 * @return XML格式的字符串 * @throws Exception */ public static String mapToXml(Map<String, String> data) throws Exception { DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); DocumentBuilder documentBuilder= documentBuilderFactory.newDocumentBuilder(); org.w3c.dom.Document document = documentBuilder.newDocument(); org.w3c.dom.Element root = document.createElement("xml"); document.appendChild(root); for (String key: data.keySet()) { String value = data.get(key); if (value == null) { value = ""; } value = value.trim(); org.w3c.dom.Element filed = document.createElement(key); filed.appendChild(document.createTextNode(value)); root.appendChild(filed); } TransformerFactory tf = TransformerFactory.newInstance(); Transformer transformer = tf.newTransformer(); DOMSource source = new DOMSource(document); transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); transformer.setOutputProperty(OutputKeys.INDENT, "yes"); StringWriter writer = new StringWriter(); StreamResult result = new StreamResult(writer); transformer.transform(source, result); String output = writer.getBuffer().toString(); //.replaceAll("\n|\r", ""); try { writer.close(); } catch (Exception ex) { } return output; } /** * 生成带有 sign 的 XML 格式字符串 * * @param data Map类型数据 * @param key API密钥 * @return 含有sign字段的XML */ public static String generateSignedXml(final Map<String, String> data, String key) throws Exception { return generateSignedXml(data, key, SignType.MD5); } /** * 生成带有 sign 的 XML 格式字符串 * * @param data Map类型数据 * @param key API密钥 * @param signType 签名类型 * @return 含有sign字段的XML */ public static String generateSignedXml(final Map<String, String> data, String key, SignType signType) throws Exception { String sign = generateSignature(data, key, signType); data.put(WXPayConstants.FIELD_SIGN, sign); return mapToXml(data); } /** * 判断签名是否正确 * * @param xmlStr XML格式数据 * @param key API密钥 * @return 签名是否正确 * @throws Exception */ public static boolean isSignatureValid(String xmlStr, String key) throws Exception { Map<String, String> data = xmlToMap(xmlStr); if (!data.containsKey(WXPayConstants.FIELD_SIGN) ) { return false; } String sign = data.get(WXPayConstants.FIELD_SIGN); return generateSignature(data, key).equals(sign); } /** * 判断签名是否正确,必须包含sign字段,否则返回false。使用MD5签名。 * * @param data Map类型数据 * @param key API密钥 * @return 签名是否正确 * @throws Exception */ public static boolean isSignatureValid(Map<String, String> data, String key) throws Exception { return isSignatureValid(data, key, SignType.MD5); } /** * 判断签名是否正确,必须包含sign字段,否则返回false。 * * @param data Map类型数据 * @param key API密钥 * @param signType 签名方式 * @return 签名是否正确 * @throws Exception */ public static boolean isSignatureValid(Map<String, String> data, String key, SignType signType) throws Exception { if (!data.containsKey(WXPayConstants.FIELD_SIGN) ) { return false; } String sign = data.get(WXPayConstants.FIELD_SIGN); return generateSignature(data, key, signType).equals(sign); } /** * 生成签名 * * @param data 待签名数据 * @param key API密钥 * @return 签名 */ public static String generateSignature(final Map<String, String> data, String key) throws Exception { return generateSignature(data, key, SignType.MD5); } /** * 生成签名. 注意,若含有sign_type字段,必须和signType参数保持一致。 * * @param data 待签名数据 * @param key API密钥 * @param signType 签名方式 * @return 签名 */ public static String generateSignature(final Map<String, String> data, String key, SignType signType) throws Exception { Set<String> keySet = data.keySet(); String[] keyArray = keySet.toArray(new String[keySet.size()]); Arrays.sort(keyArray); StringBuilder sb = new StringBuilder(); for (String k : keyArray) { if (k.equals(WXPayConstants.FIELD_SIGN)) { continue; } if (data.get(k).trim().length() > 0) // 参数值为空,则不参与签名 sb.append(k).append("=").append(data.get(k).trim()).append("&"); } sb.append("key=").append(key); if (SignType.MD5.equals(signType)) { return MD5(sb.toString()).toUpperCase(); } else if (SignType.HMACSHA256.equals(signType)) { return HMACSHA256(sb.toString(), key); } else { throw new Exception(String.format("Invalid sign_type: %s", signType)); } } /** * 获取随机字符串 Nonce Str * * @return String 随机字符串 */ public static String generateNonceStr() { return UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32); } /** * 生成 MD5 * * @param data 待处理数据 * @return MD5结果 */ public static String MD5(String data) throws Exception { java.security.MessageDigest md = MessageDigest.getInstance("MD5"); byte[] array = md.digest(data.getBytes("UTF-8")); StringBuilder sb = new StringBuilder(); for (byte item : array) { sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3)); } return sb.toString().toUpperCase(); } /** * 生成 HMACSHA256 * @param data 待处理数据 * @param key 密钥 * @return 加密结果 * @throws Exception */ public static String HMACSHA256(String data, String key) throws Exception { Mac sha256_HMAC = Mac.getInstance("HmacSHA256"); SecretKeySpec secret_key = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256"); sha256_HMAC.init(secret_key); byte[] array = sha256_HMAC.doFinal(data.getBytes("UTF-8")); StringBuilder sb = new StringBuilder(); for (byte item : array) { sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3)); } return sb.toString().toUpperCase(); } }
以上就是全部核心代码和开发过程中,踩过的坑,希望可以帮到大家,避免踩同样的坑。还提示一点,如果小程序和app没有控制重复点击问题,后端接口需要防止高并发,我这里推荐redis同步锁来解决这一问题。下一篇文章将介绍ios app开发的内容。
这个是支付成功后回调我们系统的地址,这里有个坑,url必须是http,https调不通