最近公司项目急着测试,需要开发微信小程序+微信支付+微信退款,本着这几天的一些研究,决定记录一下开发的过程。
本着知识分享的原则,希望对大家有所帮助。
本篇针对的是微信小程序的支付开发,如果有对微信公众号的支付开发需要的,可以去我的github上看看,有个sell的项目很好的完成了公众号方面的支付与退款,代码很全,用的是最优秀的sdk,肯定对你们学习和工作有帮助,下面贴一下github链接: https://github.com/wenbingshen/springboot
也可以关注我的微信公众号:《小沈干货》不迷路。
废话不多说,开始我们的小程序支付开发之旅:
首先呢,开发之前,需要交代的是,有关微信支付的开发需要有自己的商户号和密钥,这在微信支付开发文档上面讲的很清楚,有过支付开发经验的对这一点很清楚。
了解了上面的情况后咱们就开始着手开发吧!
先编写一个常量类Constant,将有关的配置常量配在里面:
public class Constant { public static final String DOMAIN = "http://sellbin.natapp1.cc";//配置自己的域名 public static final String APP_ID = "填写自己的"; public static final String APP_SECRET = "填写自己的"; public static final String APP_KEY = "填写自己的"; public static final String MCH_ID = "填写自己的"; //商户号 public static final String URL_UNIFIED_ORDER = "https://api.mch.weixin.qq.com/pay/unifiedorder"; public static final String URL_NOTIFY = Constant.DOMAIN + "/wxpay/views/payInfo.jsp"; public static final String TIME_FORMAT = "yyyyMMddHHmmss"; public static final int TIME_EXPIRE = 2; //单位是day }
支付的时候,我们需要利用发起支付的用户code去微信接口获取用户的openid,只有得到了openid才能去申请预付单获得prepayId,然后去唤起微信支付。
微信支付文档上面也写的很清楚:
接下来我们编写PayController类去调用微信支付的接口:
package luluteam.wxpay.controller; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import luluteam.wxpay.constant.Constant; import luluteam.wxpay.entity.PayInfo; import luluteam.wxpay.util.*; import org.apache.commons.lang3.StringUtils; import org.apache.log4j.Logger; import org.codehaus.jackson.map.ObjectMapper; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import javax.servlet.http.HttpServletRequest; import java.util.*; @Controller public class PayController { private static Logger log = Logger.getLogger(PayController.class); @ResponseBody @RequestMapping(value = "/prepay", produces = "text/html;charset=UTF-8") public String prePay(String code, ModelMap model, HttpServletRequest request) { System.out.println("code:"+code); String content = null; Map map = new HashMap(); ObjectMapper mapper = new ObjectMapper(); boolean result = true; String info = ""; log.error("\n======================================================"); log.error("code: " + code); String openId = getOpenId(code); System.out.println("获取openid啊"+openId); if(StringUtils.isBlank(openId)) { result = false; info = "获取到openId为空"; } else { openId = openId.replace("\"", "").trim(); String clientIP = CommonUtil.getClientIp(request); log.error("openId: " + openId + ", clientIP: " + clientIP); String randomNonceStr = RandomUtils.generateMixString(32); String prepayId = unifiedOrder(openId, clientIP, randomNonceStr); log.error("prepayId: " + prepayId); if(StringUtils.isBlank(prepayId)) { result = false; info = "出错了,未获取到prepayId"; } else { map.put("prepayId", prepayId); map.put("nonceStr", randomNonceStr); } } try { map.put("result", result); map.put("info", info); content = mapper.writeValueAsString(map); } catch (Exception e) { e.printStackTrace(); } return content; } private String getOpenId(String code) { String url = "https://api.weixin.qq.com/sns/jscode2session?appid=" + Constant.APP_ID + "&secret=" + Constant.APP_SECRET + "&js_code=" + code + "&grant_type=authorization_code"; HttpUtil httpUtil = new HttpUtil(); try { HttpResult httpResult = httpUtil.doGet(url, null, null); if(httpResult.getStatusCode() == 200) { JsonParser jsonParser = new JsonParser(); JsonObject obj = (JsonObject) jsonParser.parse(httpResult.getBody()); log.error("getOpenId: " + obj.toString()); if(obj.get("errcode") != null) { log.error("getOpenId returns errcode: " + obj.get("errcode")); return ""; } else { return obj.get("openid").toString(); } //return httpResult.getBody(); } } catch (Exception e) { e.printStackTrace(); } return ""; } /** * 调用统一下单接口 * @param openId */ private String unifiedOrder(String openId, String clientIP, String randomNonceStr) { try { String url = Constant.URL_UNIFIED_ORDER; PayInfo payInfo = createPayInfo(openId, clientIP, randomNonceStr); String md5 = getSign(payInfo); payInfo.setSign(md5); log.error("md5 value: " + md5); String xml = CommonUtil.payInfoToXML(payInfo); xml = xml.replace("__", "_").replace("<![CDATA[1]]>", "1"); //xml = xml.replace("__", "_").replace("<![CDATA[", "").replace("]]>", ""); log.error(xml); StringBuffer buffer = HttpUtil.httpsRequest(url, "POST", xml); log.error("unifiedOrder request return body: \n" + buffer.toString()); Map<String, String> result = CommonUtil.parseXml(buffer.toString()); String return_code = result.get("return_code"); if(StringUtils.isNotBlank(return_code) && return_code.equals("SUCCESS")) { String return_msg = result.get("return_msg"); if(StringUtils.isNotBlank(return_msg) && !return_msg.equals("OK")) { //log.error("统一下单错误!"); return ""; } String prepay_Id = result.get("prepay_id"); return prepay_Id; } else { return ""; } } catch (Exception e) { e.printStackTrace(); } return ""; } private PayInfo createPayInfo(String openId, String clientIP, String randomNonceStr) { Date date = new Date(); String timeStart = TimeUtils.getFormatTime(date, Constant.TIME_FORMAT); String timeExpire = TimeUtils.getFormatTime(TimeUtils.addDay(date, Constant.TIME_EXPIRE), Constant.TIME_FORMAT); String randomOrderId = CommonUtil.getRandomOrderId(); PayInfo payInfo = new PayInfo(); payInfo.setAppid(Constant.APP_ID); payInfo.setMch_id(Constant.MCH_ID); payInfo.setDevice_info("WEB"); payInfo.setNonce_str(randomNonceStr); payInfo.setSign_type("MD5"); //默认即为MD5 payInfo.setBody("JSAPI支付测试"); payInfo.setAttach("支付测试4luluteam"); payInfo.setOut_trade_no(randomOrderId); payInfo.setTotal_fee(1); payInfo.setSpbill_create_ip(clientIP); payInfo.setTime_start(timeStart); payInfo.setTime_expire(timeExpire); payInfo.setNotify_url(Constant.URL_NOTIFY); payInfo.setTrade_type("JSAPI"); payInfo.setLimit_pay("no_credit"); payInfo.setOpenid(openId); return payInfo; } private String getSign(PayInfo payInfo) throws Exception { StringBuffer sb = new StringBuffer(); sb.append("appid=" + payInfo.getAppid()) .append("&attach=" + payInfo.getAttach()) .append("&body=" + payInfo.getBody()) .append("&device_info=" + payInfo.getDevice_info()) .append("&limit_pay=" + payInfo.getLimit_pay()) .append("&mch_id=" + payInfo.getMch_id()) .append("&nonce_str=" + payInfo.getNonce_str()) .append("¬ify_url=" + payInfo.getNotify_url()) .append("&openid=" + payInfo.getOpenid()) .append("&out_trade_no=" + payInfo.getOut_trade_no()) .append("&sign_type=" + payInfo.getSign_type()) .append("&spbill_create_ip=" + payInfo.getSpbill_create_ip()) .append("&time_expire=" + payInfo.getTime_expire()) .append("&time_start=" + payInfo.getTime_start()) .append("&total_fee=" + payInfo.getTotal_fee()) .append("&trade_type=" + payInfo.getTrade_type()) .append("&key=" + Constant.APP_KEY); log.error("排序后的拼接参数:" + sb.toString()); return CommonUtil.getMD5(sb.toString().trim()).toUpperCase(); } }
小程序端通过wx.request发起网络请求,通过服务器发起预支付,获取prepayId以及其他支付需要签名的参数后,利用wx.requestPayment发起支付。
// 1. 完成页面结构、布局、样式 // 2. 设计数据结构 // 3. 完成数据绑定 // 4. 设计交互操作事件 // 5. 数据存储 var app = getApp() //实例化小程序,从而获取全局数据或者使用全局函数 // console.log(app.globalData) var MD5Util = require(\'../../utils/md5.js\'); Page({ // ===== 页面数据对象 ===== data: { input: \'\', todos: [], leftCount: 0, allCompleted: false, logs: [], price: 0.01, number: 18820000000, deviceNo: 10080925 }, // ===== 页面生命周期方法 ===== onLoad: function () { }, // ===== 事件处理函数 ===== wxPay: function (e) { var code = \'\' //传给服务器以获得openId var timestamp = String(Date.parse(new Date())) //时间戳 var nonceStr = \'\' //随机字符串,后台返回 var prepayId = \'\' //预支付id,后台返回 var paySign = \'\' //加密字符串 //获取用户登录状态 wx.login({ success: function (res) { if (res.code) { code = res.code //发起网络请求,发起的是HTTPS请求,向服务端请求预支付 wx.request({ url: \'http://sellbin.natapp1.cc/prepay\', data: { code: res.code }, success: function (res) { console.log(res.data); if (res.data.result == true) { nonceStr = res.data.nonceStr prepayId = res.data.prepayId // 按照字段首字母排序组成新字符串 var payDataA = "appId=" + app.globalData.appId + "&nonceStr=" + res.data.nonceStr + "&package=prepay_id=" + res.data.prepayId + "&signType=MD5&timeStamp=" + timestamp; var payDataB = payDataA + "&key=" + app.globalData.key; // 使用MD5加密算法计算加密字符串 paySign = MD5Util.MD5(payDataB).toUpperCase(); // 发起微信支付 wx.requestPayment({ \'timeStamp\': timestamp, \'nonceStr\': nonceStr, \'package\': \'prepay_id=\' + prepayId, \'signType\': \'MD5\', \'paySign\': paySign, \'success\': function (res) { // 保留当前页面,跳转到应用内某个页面,使用wx.nevigeteBack可以返回原页面 wx.navigateTo({ url: \'../pay/pay\' }) }, \'fail\': function (res) { console.log(res.errMsg) } }) } else { console.log(\'请求失败\' + res.data.info) } } }) } else { console.log(\'获取用户登录态失败!\' + res.errMsg) } } }); }, formSubmit: function (e) { console.log(\'form发生了submit事件,携带数据为:\', e.detail.value) }, formReset: function () { console.log(\'form发生了reset事件\') } })
接下来,我们贴一下微信退款相关的代码RefundController
package luluteam.wxpay.controller; import luluteam.wxpay.constant.Constant; import luluteam.wxpay.entity.WxRefundInfoEntity; import luluteam.wxpay.service.WxRefundInfoService; import luluteam.wxpay.util.common.PayUtils; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import java.text.DecimalFormat; import java.util.*; @Controller public class RefundController extends HttpServlet { private static Logger log = Logger.getLogger(PayController.class); @Autowired private WxRefundInfoService wxRefundInfoService; @RequestMapping(params = "refund", method = RequestMethod.POST) @Transactional public @ResponseBody Map<String, Object> refund(String openid, String orderId, HttpServletRequest request) { Map<String, Object> result = new HashMap<String, Object>(); String currTime = PayUtils.getCurrTime(); String strTime = currTime.substring(8, currTime.length()); String strRandom = PayUtils.buildRandom(4) + ""; String nonceStr = strTime + strRandom; String outRefundNo = "wx@re@" + PayUtils.getTimeStamp(); String outTradeNo = ""; String transactionId = ""; String unionId = openid; String appid = Constant.APP_ID; String mchid = Constant.MCH_ID; String key = Constant.APP_KEY;//mch_key // String key = ResourceUtil.getConfigByName("wx.application.mch_key"); if (StringUtils.isNotEmpty(orderId)) { int total_fee = 1; //商户侧传给微信的订单号32位 outTradeNo = "115151sdasdsadsadsadas"; DecimalFormat df = new DecimalFormat("0.00"); //String fee = String.valueOf(df.format((float)total_fee/100)); String fee = String.valueOf(total_fee); SortedMap<String, String> packageParams = new TreeMap<String, String>(); packageParams.put("appid", appid); packageParams.put("mch_id", mchid);//微信支付分配的商户号 packageParams.put("nonce_str", nonceStr);//随机字符串,不长于32位 packageParams.put("op_user_id", mchid);//操作员帐号, 默认为商户号 //out_refund_no只能含有数字、字母和字符_-|*@ packageParams.put("out_refund_no", outRefundNo);//商户系统内部的退款单号,商户系统内部唯一,同一退款单号多次请求只退一笔 packageParams.put("out_trade_no", outTradeNo);//商户侧传给微信的订单号32位 packageParams.put("refund_fee", fee); packageParams.put("total_fee", fee); packageParams.put("transaction_id", transactionId);//微信生成的订单号,在支付通知中有返回 String sign = PayUtils.createSign(packageParams, key); String refundUrl = "https://api.mch.weixin.qq.com/secapi/pay/refund"; String xmlParam = "<xml>" + "<appid>" + appid + "</appid>" + "<mch_id>" + mchid + "</mch_id>" + "<nonce_str>" + nonceStr + "</nonce_str>" + "<op_user_id>" + mchid + "</op_user_id>" + "<out_refund_no>" + outRefundNo + "</out_refund_no>" + "<out_trade_no>" + outTradeNo + "</out_trade_no>" + "<refund_fee>" + fee + "</refund_fee>" + "<total_fee>" + fee + "</total_fee>" + "<transaction_id>" + transactionId + "</transaction_id>" + "<sign>" + sign + "</sign>" + "</xml>"; log.info("---------xml返回:" + xmlParam); String resultStr = PayUtils.post(refundUrl, xmlParam); log.info("---------退款返回:" + resultStr); //解析结果 try { Map map = PayUtils.doXMLParse(resultStr); String returnCode = map.get("return_code").toString(); if (returnCode.equals("SUCCESS")) { String resultCode = map.get("result_code").toString(); if (resultCode.equals("SUCCESS")) { //保存退款记录,可在数据库建一个退款表记录 WxRefundInfoEntity refundInfoEntity = new WxRefundInfoEntity(); refundInfoEntity.setCreateDate(new Date()); refundInfoEntity.setAppid(appid); refundInfoEntity.setMchId(mchid); refundInfoEntity.setNonceStr(nonceStr); refundInfoEntity.setSign(sign); refundInfoEntity.setOutRefundNo(outRefundNo); refundInfoEntity.setOutTradeNo(outTradeNo); refundInfoEntity.setTotalFee(total_fee); refundInfoEntity.setRefundFee(total_fee); refundInfoEntity.setUnionid(unionId); wxRefundInfoService.save(refundInfoEntity); result.put("status", "success"); } else { result.put("status", "fail"); } } else { result.put("status", "fail"); } } catch (Exception e) { e.printStackTrace(); result.put("status", "fail"); } } return result; } }
支付与退款有关的工具类和实体类一并贴在下面,如果有不清楚的,可以去我的github上面下载源码:
https://github.com/wenbingshen/wechatpay
entity包:
package luluteam.wxpay.entity; import java.io.Serializable; public class PayInfo implements Serializable { private String appid; private String mch_id; private String device_info; //设备号,小程序传"WEB" private String nonce_str; private String sign; private String sign_type; //签名类型 private String body; //private String detail; private String attach; private String out_trade_no; private int total_fee; private String spbill_create_ip; private String time_start; private String time_expire; private String notify_url; private String trade_type; //交易类型,JSAPI private String limit_pay; //指定支付方式,no_credit private String openid; public String getAppid() { return appid; } public void setAppid(String appid) { this.appid = appid; } public String getMch_id() { return mch_id; } public void setMch_id(String mch_id) { this.mch_id = mch_id; } public String getDevice_info() { return device_info; } public void setDevice_info(String device_info) { this.device_info = device_info; } public String getNonce_str() { return nonce_str; } public void setNonce_str(String nonce_str) { this.nonce_str = nonce_str; } public String getSign() { return sign; } public void setSign(String sign) { this.sign = sign; } public String getSign_type() { return sign_type; } public void setSign_type(String sign_type) { this.sign_type = sign_type; } public String getBody() { return body; } public void setBody(String body) { this.body = body; } public String getAttach() { return attach; } public void setAttach(String attach) { this.attach = attach; } public String getOut_trade_no() { return out_trade_no; } public void setOut_trade_no(String out_trade_no) { this.out_trade_no = out_trade_no; } public int getTotal_fee() { return total_fee; } public void setTotal_fee(int total_fee) { this.total_fee = total_fee; } public String getSpbill_create_ip() { return spbill_create_ip; } public void setSpbill_create_ip(String spbill_create_ip) { this.spbill_create_ip = spbill_create_ip; } public String getTime_start() { return time_start; } public void setTime_start(String time_start) { this.time_start = time_start; } public String getTime_expire() { return time_expire; } public void setTime_expire(String time_expire) { this.time_expire = time_expire; } public String getNotify_url() { return notify_url; } public void setNotify_url(String notify_url) { this.notify_url = notify_url; } public String getTrade_type() { return trade_type; } public void setTrade_type(String trade_type) { this.trade_type = trade_type; } public String getLimit_pay() { return limit_pay; } public void setLimit_pay(String limit_pay) { this.limit_pay = limit_pay; } public String getOpenid() { return openid; } public void setOpenid(String openid) { this.openid = openid; } }
WxRefundInfoEntity类,用来退款后向数据添加退款的记录,方面查账:
package luluteam.wxpay.entity; import javax.persistence.Entity; import java.io.Serializable; import java.util.Date; @Entity public class WxRefundInfoEntity implements Serializable { private Date createDate; private String appid; private String mchId; private String nonceStr; private String sign; private String outRefundNo; private String outTradeNo; private int totalFee; private int refundFee; private String unionid; public Date getCreateDate() { return createDate; } public void setCreateDate(Date createDate) { this.createDate = createDate; } public String getAppid() { return appid; } public void setAppid(String appid) { this.appid = appid; } public String getMchId() { return mchId; } public void setMchId(String mchId) { this.mchId = mchId; } public String getNonceStr() { return nonceStr; } public void setNonceStr(String nonceStr) { this.nonceStr = nonceStr; } public String getSign() { return sign; } public void setSign(String sign) { this.sign = sign; } public String getOutRefundNo() { return outRefundNo; } public void setOutRefundNo(String outRefundNo) { this.outRefundNo = outRefundNo; } public String getOutTradeNo() { return outTradeNo; } public void setOutTradeNo(String outTradeNo) { this.outTradeNo = outTradeNo; } public int getTotalFee() { return totalFee; } public void setTotalFee(int totalFee) { this.totalFee = totalFee; } public int getRefundFee() { return refundFee; } public void setRefundFee(int refundFee) { this.refundFee = refundFee; } public String getUnionid() { return unionid; } public void setUnionid(String unionid) { this.unionid = unionid; } }
util包:
package luluteam.wxpay.util.common; import java.security.MessageDigest; public class MD5 { private final static String[] hexDigits = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"}; /** * 转换字节数组为16进制字串 * @param b 字节数组 * @return 16进制字串 */ public static String byteArrayToHexString(byte[] b) { StringBuilder resultSb = new StringBuilder(); for (byte aB : b) { resultSb.append(byteToHexString(aB)); } return resultSb.toString(); } /** * 转换byte到16进制 *
全部评论
请发表评论