今天来分享一下之前做微信小程序微信支付遇到的一些坑,博主这里是微信小程序支付功能,因此选择的微信支付方式是JSAPI支付方式(温馨提示左下角有音乐哦)。
首先我们肯定是要在小程序后台绑定一个商户号的,接下来我们看一下整个开发流程如下图(微信官方图):
由此我们就可以得出下面这个支付的大致流程:
首先,选择商品和数量等,点击下单,然后后台将这些参数生成数字签名并以xml的方式传递,并调用微信统一下订单接口生成一张微信预支付订单表(此时也可以添加上自己业务逻辑),订单有效期都在半小时内,半小时后该条下单数据就失效了,因此应该在半小时内完成支付,签名成功后将微信返回的prepay_id等数据返回给前端,再由前端调起收银台完成支付。
由上面我们大概清楚了两点:
1.生成数字签名;
2.调用微信统一下订单;
3.小程序支付;
那么我们再来看看微信支付AIP接口文档:传送门
在返回数据中return_code和result_code都为SUCCESS的为签名成功,返回参数含义可以通过传送门了解,这里博主就不再说明了。
通过AIP接口文档我们发现要想调用微信统一下订单接口那么我们就需要传递以下必填参数:
字段名变量名必填类型示例值描述
小程序ID
appid
是
String(32)
wxd678efh567hg6787
微信分配的小程序ID
商户号
mch_id
是
String(32)
1230000109
微信支付分配的商户号
随机字符串
nonce_str
是
String(32)
5K8264ILTKCH16CQ2502SI8ZNMTM67VS
随机字符串,长度要求在32位以内。推荐随机数生成算法
签名
sign
是
String(64)
C380BEC2BFD727A4B6845133519F3AD6
通过签名算法计算得出的签名值,详见签名生成算法
商品描述
body
是
String(128)
腾讯充值中心-QQ会员充值
商品简单描述,该字段请按照规范传递,具体请见参数规定
商户订单号
out_trade_no
是
String(32)
0806125346
商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-|*且在同一个商户号下唯一。详见商户订单号
标价金额
total_fee
是
Int
88
订单总金额,单位为分,详见支付金额
终端IP
spbill_create_ip
是
String(64)
123.12.12.123
支持IPV4和IPV6两种格式的IP地址。调用微信支付API的机器IP
通知地址
notify_url
是
String(256)
http://www./wxpay/pay.php
异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。
交易类型
trade_type
是
String(16)
JSAPI
小程序取值如下:JSAPI,详细说明见参数规定
以上参数请仔细查看API接口文档说明,需注意随机字符转生成方法及位数,签名生成算法(博主采用MD5),商户订单号也就是自己平台生
成的订单号,金额是以分为单位;
下面我们重点来说一说数字签名的生成如图:
以上是java后台签名完调用微信统一下订单的步骤,下面我们来看小程序端完成支付前需要获取那些数据,官方文档:传送门
通过以上我们看到它是需要这些必填参数:
参数类型必填说明
timeStamp
String
是
时间戳从1970年1月1日00:00:00至今的秒数,即当前的时间
nonceStr
String
是
随机字符串,长度为32个字符以下。
package
String
是
统一下单接口返回的 prepay_id 参数值,提交格式如:prepay_id=*
signType
String
是
签名类型,默认为MD5,支持HMAC-SHA256和MD5。注意此处需与统一下单的签名类型一致
paySign
String
是
签名,具体签名方案参见微信公众号支付帮助文档;
其中我们需要注意的是paySign这个参数,根据文档我看可以看到,生成这个参数是需要以下这些必填参数:
字段名变量名必填类型示例值描述
小程序ID
appId
是
String
wxd678efh567hg6787
微信分配的小程序ID
时间戳
timeStamp
是
String
1490840662
时间戳从1970年1月1日00:00:00至今的秒数,即当前的时间
随机串
nonceStr
是
String
5K8264ILTKCH16CQ2502SI8ZNMTM67VS
随机字符串,不长于32位。推荐随机数生成算法
数据包
package
是
String
prepay_id=wx033010242291fcfe0db70013231072
统一下单接口返回的 prepay_id 参数值,提交格式如:prepay_id=wx033010242291fcfe0db70013231072
签名方式
signType
是
String
MD5
签名类型,默认为MD5,支持HMAC-SHA256和MD5。注意此处需与统一下单的签名类型一致
而刚好这些参数都是调用统一下订单接口成功后返回的参数,并将这些数据以数字签名的方式合成paySign字段,如有不清楚签名方式可以通过表格传送门了解,完成完这些将数据返回给前端,并由前端调用wx.requesetPayment()方法完成支付。
根据以上的了解咱们话不多说上代码:
Controller层
@RestControllerpublic classWeixinController {public final Logger log = Logger.getLogger(this.getClass());
@Autowired
OrdersService ordersService;//微信支付的商户密钥(换成自己的)
public static final String key = "1111111111111111";//微信统一下单接口地址
public static final String pay_url = "https://api.mch./pay/unifiedorder";
@RequestMapping(value="/pay/placeanorder" ,method =RequestMethod.POST)private Map goPay(@RequestParam Map maps, HttpServletRequest request) throwsException {
String openid=(String) maps.get("openid");
PaymentPo paymentPo=newPaymentPo();
paymentPo.setOpenid(openid);//生成的随机字符串
String nonce_str = StringUtils.getRandomStringByLength(32);
paymentPo.setNonce_str(nonce_str);//商品名称
String body = (String) maps.get("body");
paymentPo.setBody(body);//获取本机的ip地址
String spbill_create_ip =IpUtils.getIpAddr(request);
paymentPo.setSpbill_create_ip(spbill_create_ip);//小程序金额不是元,单位是分(如:100=1元)
paymentPo.setTotal_fee((String) maps.get("totalfee"));
String total_fee= String.valueOf(new BigDecimal(paymentPo.getTotal_fee()).multiply(new BigDecimal(100)).intValue());//商户订单号(自己写逻辑生成)
paymentPo.setOut_trade_no(ordersService.getOrderNoByAtomics());//组装参数,用户生成统一下单接口的签名
Map packageParams = new HashMap();
packageParams.put("appid", paymentPo.getAppid());//小程序ID
packageParams.put("body", paymentPo.getBody());//商品描述
packageParams.put("mch_id", paymentPo.getMch_id());//商户号
packageParams.put("nonce_str", paymentPo.getNonce_str());//随机字符串
packageParams.put("notify_url", paymentPo.getNotify_url());//支付成功后的回调地址
packageParams.put("openid", paymentPo.getOpenid());
packageParams.put("out_trade_no", paymentPo.getOut_trade_no());//商户订单号
packageParams.put("spbill_create_ip", paymentPo.getSpbill_create_ip());//终端IP
packageParams.put("total_fee", total_fee);//支付金额,这边需要转成字符串类型,否则后面的签名会失败
packageParams.put("trade_type", paymentPo.getTrade_type());//支付方式
String prestr= PayUtil.createLinkString(packageParams); //把数组所有元素,按照“参数=参数值”的模式用“&”字符拼接成字符串(第一步)
log.info("第一次签名:" +prestr);//MD5运算生成签名,这里是第一次签名,用于调用统一下单接口(第二步)
String mysign = PayUtil.sign(prestr, key, "utf-8").toUpperCase();//拼接统一下单接口使用的xml数据,要将上一步生成的签名一起拼接进去(第三步)
String xml = "" + paymentPo.getAppid() + ""
+ "
" + paymentPo.getBody() + ""
+ "" + paymentPo.getMch_id() + ""
+ "" + paymentPo.getNonce_str() + ""
+ "" + paymentPo.getNotify_url() + ""
+ "" + paymentPo.getOpenid() + ""
+ "" + paymentPo.getOut_trade_no() + ""
+ "" + paymentPo.getSpbill_create_ip() + ""
+ "" + Integer.valueOf(total_fee) + ""
+ "" + paymentPo.getTrade_type() + ""
+ "" + mysign + ""
+ "";
log.info("调试模式_统一下单接口 请求XML数据:" +xml);//调用统一下单接口,并接受返回的结果
String res = PayUtil.httpRequest(pay_url, "POST", xml);
log.info("调试模式_统一下单接口 返回XML数据:" +res);//将解析结果存储在HashMap中
Map map =PayUtil.doXMLParse(res);
String return_code= (String)map.get("return_code");//返回状态码
String result_code = (String)map.get("result_code");//结果状态码
Map result = new HashMap();//返回给小程序端需要的参数
String prepay_id = null;if ("SUCCESS".equals(return_code) &&return_code.equals(result_code)) {
prepay_id= (String) map.get("prepay_id");//返回的预付单信息
result.put("nonceStr", paymentPo.getNonce_str());
result.put("package", "prepay_id=" +prepay_id);
Long timeStamp= System.currentTimeMillis() / 1000;
result.put("timeStamp", timeStamp + "");//这边要将返回的时间戳转化成字符串,不然小程序端调用wx.requestPayment方法会报签名错误//拼接签名需要的参数
String stringSignTemp = "appId=" + paymentPo.getAppid() + "&nonceStr=" + paymentPo.getNonce_str() + "&package=prepay_id=" + prepay_id + "&signType=MD5&timeStamp=" +timeStamp;//再次签名,这个签名用于小程序端调用wx.requesetPayment方法完成支付(生成paySign字段)
String paySign = PayUtil.sign(stringSignTemp, key, "utf-8").toUpperCase();
result.put("paySign", paySign);
result.put("signType", "MD5");
result.put("moneyorderid", paymentPo.getOut_trade_no());
}
result.put("appid", paymentPo.getAppid());
log.info("结束1" +res);returnresult;
}
}
PaymentPo类
@Setter
@Getterpublic classPaymentPo {private static final long serialVersionUID = 1712467669291115101L;private String appid="1233444";//小程序ID(改为自己的)
private String mch_id="123333";//商户号(改为自己的)
private String device_info;//设备号
private String nonce_str;//随机字符串
private String sign;//签名
private String body;//商品描述
private String detail;//商品详情
private String attach;//附加数据
private String out_trade_no;//商户订单号
private String fee_type;//货币类型
private String spbill_create_ip;//终端IP
private String time_start;//交易起始时间
private String time_expire;//交易结束时间
private String goods_tag;//商品标记
private String total_fee;//总金额
private String notify_url="http://11111111";//通知地址(改为自己的)
private String trade_type="JSAPI";//交易类型
private String limit_pay;//指定支付方式
private String openid;//用户标识
private String code;//用户标识
private String placeId;//用户标识
private Integer carSum;//用户购买了多少次
private Integer type;//用户购卡类型
private String key = "11111111";//微信支付的商户密钥(改为自己的)
private String pay_url = "https://api.mch./pay/unifiedorder";//微信统一下单接口地址
}
StringUtils工具类
public class StringUtils mons.lang3.StringUtils{/*** StringUtils工具类方法
* 获取一定长度的随机字符串,范围0-9,a-z
*@paramlength:指定字符串长度
*@return一定长度的随机字符串*/
public static String getRandomStringByLength(intlength) {
String base= "abcdefghijklmnopqrstuvwxyz0123456789";
Random random= newRandom();
StringBuffer sb= newStringBuffer();for (int i = 0; i < length; i++) {int number =random.nextInt(base.length());
sb.append(base.charAt(number));
}returnsb.toString();
}
}
PayUtil工具类
public classPayUtil {/*** 签名字符串
*
*@paramtext需要签名的字符串
*@paramkey 密钥
*@paraminput_charset编码格式
*@return签名结果*/
public staticString sign(String text, String key, String input_charset) {
text= text + "&key=" +key;
System.out.println(text);returnDigestUtils.md5Hex(getContentBytes(text, input_charset));
}/*** 签名字符串
*
*@paramtext需要签名的字符串
*@paramsign 签名结果
*@paramkey密钥
*@paraminput_charset 编码格式
*@return签名结果*/
public static booleanverify(String text, String sign, String key, String input_charset) {
text= text +key;
String mysign=DigestUtils.md5Hex(getContentBytes(text, input_charset));if(mysign.equals(sign)) {return true;
}else{return false;
}
}/***@paramcontent
*@paramcharset
*@return*@throwsSignatureException
*@throwsUnsupportedEncodingException*/
public static byte[] getContentBytes(String content, String charset) {if (charset == null || "".equals(charset)) {returncontent.getBytes();
}try{returncontent.getBytes(charset);
}catch(UnsupportedEncodingException e) {throw new RuntimeException("MD5签名过程中出现错误,指定的编码集不对,您目前指定的编码集是:" +charset);
}
}private static boolean isValidChar(charch) {if ((ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z'))return true;if ((ch >= 0x4e00 && ch <= 0x7fff) || (ch >= 0x8000 && ch <= 0x952f))return true;//简体中文汉字编码
return false;
}/*** 除去数组中的空值和签名参数
*
*@paramsArray 签名参数组
*@return去掉空值与签名参数后的新签名参数组*/
public static Map paraFilter(MapsArray) {
Map result = new HashMap();if (sArray == null || sArray.size() <= 0) {returnresult;
}for(String key : sArray.keySet()) {
String value=sArray.get(key);if (value == null || value.equals("") || key.equalsIgnoreCase("sign")|| key.equalsIgnoreCase("sign_type")) {continue;
}
result.put(key, value);
}returnresult;
}/*** 把数组所有元素排序,并按照“参数=参数值”的模式用“&”字符拼接成字符串
*
*@paramparams 需要排序并参与字符拼接的参数组
*@return拼接后字符串*/
public static String createLinkString(Mapparams) {
List keys = new ArrayList(params.keySet());//对key键值按字典升序排序
Collections.sort(keys);
String prestr= "";for (int i = 0; i < keys.size(); i++) {
String key=keys.get(i);
String value=params.get(key);if (i == keys.size() - 1) {//拼接时,不包括最后一个&字符
prestr = prestr + key + "=" +value;
}else{
prestr= prestr + key + "=" + value + "&";
}
}returnprestr;
}public String getSignToken(HashMapmap) {
String result= "";
Collection keyset=map.keySet();
List list = new ArrayList(keyset);//对key键值按字典升序排序
Collections.sort(list);//构造签名键值对的格式
StringBuilder sb = newStringBuilder();for (int i = 0; i < list.size(); i++) {if (list.get(i) != null || list.get(i) != "") {
String key=list.get(i);
String val=map.get(list.get(i));if (!(val == "" || val == null)) {
sb.append(key+ "=" + val + "&");
}
}
}returnresult;
}/***@paramrequestUrl请求地址
*@paramrequestMethod请求方法
*@paramoutputStr参数*/
public staticString httpRequest(String requestUrl, String requestMethod, String outputStr) {//创建SSLContext
StringBuffer buffer = null;try{
URL url= newURL(requestUrl);
HttpURLConnection conn=(HttpURLConnection) url.openConnection();
conn.setRequestMethod(requestMethod);
conn.setDoOutput(true);
conn.setDoInput(true);
conn.connect();//往服务器端写内容
if (null !=outputStr) {
OutputStream os=conn.getOutputStream();
os.write(outputStr.getBytes("utf-8"));
os.close();
}//读取服务器端返回的内容
InputStream is =conn.getInputStream();
InputStreamReader isr= new InputStreamReader(is, "utf-8");
BufferedReader br= newBufferedReader(isr);
buffer= newStringBuffer();
String line= null;while ((line = br.readLine()) != null) {
buffer.append(line);
}
br.close();
}catch(Exception e) {
e.printStackTrace();
}returnbuffer.toString();
}public staticString urlEncodeUTF8(String source) {
String result=source;try{
result= .URLEncoder.encode(source, "UTF-8");
}catch(UnsupportedEncodingException e) {//TODO Auto-generated catch block
e.printStackTrace();
}returnresult;
}/*** 向指定 URL 发送POST方法的请求
*
*@paramurl 发送请求的 URL
*@paramparam 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。
*@return所代表远程资源的响应结果*/
public staticString sendPost(String param,String pay_url) {
PrintWriter out= null;
BufferedReader in= null;
String result= "";try{
URL realUrl= newURL(pay_url);//打开和URL之间的连接
URLConnection conn =realUrl.openConnection();//设置通用的请求属性
conn.setRequestProperty("accept", "*/*");
conn.setRequestProperty("connection", "Keep-Alive");
conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");//conn.setRequestProperty("Pragma:", "no-cache");//conn.setRequestProperty("Cache-Control", "no-cache");//conn.setRequestProperty("Content-Type", "text/xml;charset=utf-8");//发送POST请求必须设置如下两行
conn.setDoOutput(true);
conn.setDoInput(true);//获取URLConnection对象对应的输出流
out = newPrintWriter(conn.getOutputStream());//发送请求参数
out.print(param);//flush输出流的缓冲
out.flush();//定义BufferedReader输入流来读取URL的响应
in = newBufferedReader(newInputStreamReader(conn.getInputStream()));
String line;while ((line = in.readLine()) != null) {
result+=line;
}
}catch(Exception e) {
System.out.println("发送 POST 请求出现异常!" +e);
e.printStackTrace();
}//使用finally块来关闭输出流、输入流
finally{try{if (out != null) {
out.close();
}if (in != null) {
in.close();
}
}catch(IOException ex) {
ex.printStackTrace();
}
}returnresult;
}/*** 解析xml,返回第一级元素键值对。如果第一级元素有子节点,则此节点的值是子节点的xml数据。
*
*@paramstrxml
*@return*@throwsJDOMException
*@throwsIOException*/
public static Map doXMLParse(String strxml) throwsException {if (null == strxml || "".equals(strxml)) {return null;
}
Map m= newHashMap();
InputStream in=String2Inputstream(strxml);
SAXBuilder builder= newSAXBuilder();
Document doc=builder.build(in);
Element root=doc.getRootElement();
List list=root.getChildren();
Iterator it=list.iterator();while(it.hasNext()) {
Element e=(Element) it.next();
String k=e.getName();
String v= "";
List children=e.getChildren();if(children.isEmpty()) {
v=e.getTextNormalize();
}else{
v=getChildrenText(children);
}
m.put(k, v);
}//关闭流
in.close();returnm;
}/*** 获取子结点的xml
*
*@paramchildren
*@returnString*/
public staticString getChildrenText(List children) {
StringBuffer sb= newStringBuffer();if (!children.isEmpty()) {
Iterator it=children.iterator();while(it.hasNext()) {
Element e=(Element) it.next();
String name=e.getName();
String value=e.getTextNormalize();
List list=e.getChildren();
sb.append("");if (!list.isEmpty()) {
sb.append(getChildrenText(list));
}
sb.append(value);
sb.append("" + name + ">");
}
}returnsb.toString();
}public staticInputStream String2Inputstream(String str) {return newByteArrayInputStream(str.getBytes());
}
}
那么以上代码部分就是后端完成前支付的全部过程,支付成功后的业务逻辑我就不再分享,可以通过前端异步通知后端支付成功,并执行平台自己的业务;
当然这其中可能会出现签名返回错误,以及该订单已支付问题;
·订单已支付可能是因为商户订单号没有更新,导致第二次支付是传递的是同一个订单号所导致,可以沿着这个方向去排查;
·签名错误请仔细查看签名生成方法,可通过验证工具去验证,传送门
如出现下面问题签名验证正确,但是调用微信统一下订单返回签名错误;
那就请返回查看一下商户key,需注意商户key设置官方说明如图:
仔细看其实它的意思是要32个字符,其中包含数字,大写字母和小写字母的组合,一定要有这三个,切记,切记,切记。
下面就是我的测试成功图片
这里商户单号就是自己平台所产生的订单号。
好了今天的分享就到这里了
这里是小宋
如果问题请联系qq邮箱:
感谢大家观看,祝大家生活学习和工作愉快!
微信小程序支付返回签名错误_java 微信小程序微信支付统一下订单及数字签名错误问题(后端)...