微信支付/退费(服务商)模式
服务商支付都搞定了,普通商户的支付自然就OK了,下面就记录下在服务商下支付的坑!!!吐槽下 官方文档@#¥%& 果然只有自己写的人看的懂
工具类都贴上去了 什么签名boby中文乱码,各种签名失败基本都踩完了
开发前的准备
注册服务号!划重点 一定是服务号
认证完找到微信支付----》服务商申请
开始配置服务商
1、API密钥 这个很重要,支付的签名验证需要它 这个是在账户中心–》API安全–》API密钥 APIV2 APIV3都可以用
2、下载安全证书,支付功能可忽略。退费需用到安全证书
3、申请服务商的特约商户 即申请需要支付到对应对公账户的商户号
4、给特约商户开通支付权限 退费权限。授权支付域名
特约商户APPID配置 即绑定需要支付的微信小程序
微信服务商支付
首先打开这不够严谨!!!的微信服务商统一下单文档
以下比较坑爹的几个参数参数
1、appid 注意⚠️ 这里是用服务号的appid 不是小程序的
2、mch_id 这里是用服务商的id 在我的账号一栏可以找到
3、sub_appid 这里才是特约商户的小程序的appid
4、sub_openid 其实就是wx.login中获取的的openId
5、sign 此参数为签名参数 需要将需要传递的参数进行排序并且进行md5签名,需要注意的是需添加参数key 即之前设置的服务商API密钥
🆗!!下面调用统一下单接口 获取所谓的prepay_id预支付标识
SortedMap<Object,Object> parameters= new TreeMap<Object,Object>();
String body=foodNames.toString()+foodCount+"件餐品";
parameters.put("appid", PayConst.SERVICE_APPID);
parameters.put("body", body);
parameters.put("mch_id",PayConst.ORDER_ID);
parameters.put("nonce_str", WXPayUtil.generateNonceStr());//随机字符串
parameters.put("notify_url","外网Ip");//通知地址 异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。
parameters.put("out_trade_no",out_trade_no);//商户订单号保证唯一
parameters.put("spbill_create_ip", ip); //终端IP APP和网页支付提交用户端ip,Native支付填调用微信支付API的机器IP。
parameters.put("total_fee",CommonUtil.getMoney(totalFee.toString()));//标价金额
parameters.put("trade_type","JSAPI");
parameters.put("sub_openid",sub_openid);
parameters.put("sub_mch_id",PayConst.CONTRIBUTING_MCH_ID);
parameters.put("sub_appid",PayConst.CONTRIBUTING_APPID);
String sign=PayCommonUtil.createSign("UTF-8",parameters);//签名
parameters.put("sign",sign);
String parametersXml=PayCommonUtil.getRequestXml(parameters);
logger.info("签名字符串"+parametersXml);
String result = CommonUtil.sendPost(PayConst.UNIFIED_ORDER_URL, parametersXml);
logger.info("微信签名结果"+result);
Map<String,String> resultMap = WXPayUtil.xmlToMap(result);
// 预付商品id
String prepay_id = resultMap.get("prepay_id").toString();
import com.matcheng.common.utils.MD5Util;
import com.matcheng.order.wx.PayConst;
import org.apache.commons.lang.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.text.SimpleDateFormat;
import java.util.*;
public class PayCommonUtil {
/**
* 获取支付随机码
* @return
*/
public static String create_nonce_str() {
return UUID.randomUUID().toString();
}
/**
* 获取微信支付时间戳
* @return
*/
public static String create_timestamp() {
return Long.toString(System.currentTimeMillis() / 1000);
}
/**
* 获取预支付ID时 获取随机码
* @param length
* @return
*/
public static String CreateNoncestr(int length) {
String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
String res = "";
for (int i = 0; i < length; i++) {
Random rd = new Random();
res += chars.indexOf(rd.nextInt(chars.length() - 1));
}
return res;
}
/**
* @author Mark
* @Description:sign签名
* @param characterEncoding 编码格式
* @param parameters 请求参数
* @return
*/
public static String createSign(String characterEncoding, SortedMap<Object,Object> parameters){
StringBuffer sb = new StringBuffer();
Set es = parameters.entrySet();//所有参与传参的参数按照accsii排序(升序)
Iterator it = es.iterator();
while(it.hasNext()) {
Map.Entry entry = (Map.Entry)it.next();
String k = (String)entry.getKey();
Object v = entry.getValue();
if(null != v && !"".equals(v)
&& !"sign".equals(k) && !"key".equals(k)) {
sb.append(k + "=" + v + "&");
}
}
sb.append("key=" + PayConst.ORDER_KEY2);
String sign = MD5Util.MD5Encode(sb.toString(), characterEncoding).toUpperCase();
return sign;
}
/**
* @author Mark
* @Description:将请求参数转换为xml格式的string
* @param parameters 请求参数
* @return
*/
public static String getRequestXml(SortedMap<Object,Object> parameters){
StringBuffer sb = new StringBuffer();
sb.append("<xml>");
Set es = parameters.entrySet();
Iterator it = es.iterator();
while(it.hasNext()) {
Map.Entry entry = (Map.Entry)it.next();
String k = (String)entry.getKey();
String v = (String)entry.getValue();
if ("attach".equalsIgnoreCase(k)||"body".equalsIgnoreCase(k)||"sign".equalsIgnoreCase(k)) {
sb.append("<"+k+">"+"<![CDATA["+v+"]]></"+k+">");
}else {
sb.append("<"+k+">"+v+"</"+k+">");
}
}
sb.append("</xml>");
return sb.toString();
}
}
import net.sf.json.JSONObject;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.util.EntityUtils;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import java.io.*;
import java.net.*;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.MessageDigest;
import java.util.*;
public class CommonUtil {
/**
* 发起https请求并获取结果
*
* @param requestUrl 请求地址
* @param requestMethod 请求方式(GET、POST)
* @param outputStr 提交的数据
* @return JSONObject (通过JSONObject.get(key)的方式获取json对象的属性值)
*/
public static JSONObject httpRequest(String requestUrl, String requestMethod, String outputStr) {
JSONObject jsonObject = null;
try {
//创建SSLContext对象,并使用我们指定的信任管理器初始化(证书过滤)
TrustManager[] tm = { new MyX509TrustManager() };
//取得SSL的SSLContext实例
SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");
//初始化SSLContext
sslContext.init(null, tm, new java.security.SecureRandom());
//从上述SSLContext对象中得到SSLSocketFactory对象
SSLSocketFactory ssf = sslContext.getSocketFactory();
URL url = new URL(requestUrl);
HttpsURLConnection httpUrlConn=(HttpsURLConnection) url.openConnection();
httpUrlConn.setSSLSocketFactory(ssf);
httpUrlConn.setDoOutput(true);
httpUrlConn.setDoInput(true);
httpUrlConn.setUseCaches(false);
//设置请求方式(GET/POST)
httpUrlConn.setRequestMethod(requestMethod);
/*if ("GET".equalsIgnoreCase(requestMethod))
httpUrlConn.connect(); */
//当有数据需要提交时(当outputStr不为null时,向输出流写数据)
if (null != outputStr) {
OutputStream outputStream = httpUrlConn.getOutputStream();
// 注意编码格式,防止中文乱码
outputStream.write(outputStr.getBytes("UTF-8"));
outputStream.close();
}
// 将返回的输入流转换成字符串
InputStream inputStream = httpUrlConn.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8");
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String str = null;
StringBuffer buffer=new StringBuffer();
while ((str = bufferedReader.readLine()) != null) {
buffer.append(str);
}
//释放资源
bufferedReader.close();
inputStreamReader.close();
inputStream.close();
inputStream = null;
httpUrlConn.disconnect();
jsonObject = JSONObject.fromObject(buffer.toString());
} catch (ConnectException ce) {
} catch (Exception e) {
}
return jsonObject;
}
/**
* 向指定 URL 发送POST方法的请求
*
* @param url 发送请求的 URL
* @param param
* @param isJson 是否json包文 发送
* @return 所代表远程资源的响应结果
*/
public static String sendPostRefund(String url, String param, boolean isJson) {
//System.out.println(url+":"+param);
File certFile = new File("C:\\key\\apiclient_cert.p12");
String result = "";
try {
KeyStore keyStore = KeyStore.getInstance("PKCS12");
FileInputStream instream = new FileInputStream(certFile);
try {
keyStore.load(instream, PayConst.ORDER_ID.toCharArray());
} finally {
instream.close();
}
// Trust own CA and all self-signed certs
SSLContext sslcontext = SSLContexts.custom().loadKeyMaterial(keyStore, PayConst.ORDER_ID.toCharArray()).build();
// Allow TLSv1 protocol only
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslcontext,
new String[]{"TLSv1"}, null, SSLConnectionSocketFactory.getDefaultHostnameVerifier());
CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(sslsf).build();
HttpPost httppost = new HttpPost(url);
StringEntity se = new StringEntity(param, "UTF-8");
se.setContentEncoding("UTF-8");
// if (isJson)
se.setContentType("application/json");//发送json数据需要设置contentType
httppost.addHeader("Accept-Charset", "UTF-8");
httppost.addHeader("Content-Type", "application/json; charset=utf-8");
httppost.setEntity(se);
// System.out.println("executing request" + httppost.getRequestLine());
CloseableHttpResponse responseEntry = httpclient.execute(httppost);
HttpEntity entity = responseEntry.getEntity();
// System.out.println("executing response:" + responseEntry.getStatusLine());
result = EntityUtils.toString(entity, "UTF-8");
} catch (Exception e) {
//System.out.println("发送 POST 请求出现异常!" + e);
e.printStackTrace();
}
return result;
}
/**
* 发送https请求
* @param requestUrl 请求地址
* @param requestMethod 请求方式(GET、POST)
* @param outputStr 提交的数据
* @return 返回微信服务器响应的信息
*/
public static String httpsRequest(String requestUrl, String requestMethod, String outputStr) {
try {
// 创建SSLContext对象,并使用我们指定的信任管理器初始化
TrustManager[] tm = { new MyX509TrustManager() };
SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");
sslContext.init(null, tm, new java.security.SecureRandom());
// 从上述SSLContext对象中得到SSLSocketFactory对象
SSLSocketFactory ssf = sslContext.getSocketFactory();
URL url = new URL(requestUrl);
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
conn.setSSLSocketFactory(ssf);
conn.setDoOutput(true);
conn.setDoInput(true);
conn.setUseCaches(false);
// 设置请求方式(GET/POST)
conn.setRequestMethod(requestMethod);
conn.setRequestProperty("content-type", "application/x-www-form-urlencoded");
// 当outputStr不为null时向输出流写数据
if (null != outputStr) {
OutputStream outputStream = conn.getOutputStream();
// 注意编码格式
outputStream.write(outputStr.getBytes("UTF-8"));
outputStream.close();
}
// 从输入流读取返回内容
InputStream inputStream = conn.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8");
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String str = null;
StringBuffer buffer = new StringBuffer();
while ((str = bufferedReader.readLine()) != null) {
buffer.append(str);
}
// 释放资源
bufferedReader.close();
inputStreamReader.close();
inputStream.close();
inputStream = null;
conn.disconnect();
return buffer.toString();
} catch (ConnectException ce) {
} catch (Exception e) {
}
return null;
}
/**
* 获取预支付ID时 获取随机码
* @param
* @return
*/
public static String CreateNoncestr() {
String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
String res = "";
for (int i = 0; i < 16; i++) {
Random rd = new Random();
res += chars.charAt(rd.nextInt(chars.length() - 1));
}
return res;
}
/**
* @author Mark
* @Description:sign签名
* @param characterEncoding 编码格式
* @param parameters 请求参数
* @return
*/
public static String createSign(String characterEncoding, HashMap<String,String> parameters){
StringBuffer sb = new StringBuffer();
Set es = parameters.entrySet();//所有参与传参的参数按照accsii排序(升序)
Iterator it = es.iterator();
while(it.hasNext()) {
Map.Entry entry = (Map.Entry)it.next();
String k = (String)entry.getKey();
Object v = entry.getValue();
if(null != v && !"".equals(v)
&& !"sign".equals(k) && !"key".equals(k)) {
sb.append(k + "=" + v + "&");
}
}
sb.append("key=" + PayConst.ORDER_KEY2);
String sign = MD5Encode(sb.toString(), characterEncoding).toUpperCase();
return sign;
}
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 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];
}
private static final String hexDigits[] = { "0", "1", "2", "3", "4", "5",
"6", "7", "8", "9", "a", "b", "c", "d", "e", "f" };
/**
* 获取域名对应的IP地址
*
* @param domainName
* @return
*/
public static String getIpFromName(String domainName) {
try {
return InetAddress.getByName(domainName).getHostAddress();
} catch (UnknownHostException e) {
// TODO Auto-generated catch block
e.printStackTrace();
return "127.0.0.1";
}
}
/**
* 向指定URL发送GET方法的请求
*
* @param url
* 发送请求的URL
* @param param
* 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。
* @return URL 所代表远程资源的响应结果
*/
public static String sendGet(String url, String param) {
String result = "";
BufferedReader in = null;
try {
String urlNameString = url + "?" + param;
System.out.println(urlNameString);
URL realUrl = new URL(urlNameString);
// 打开和URL之间的连接
URLConnection connection = realUrl.openConnection();
// 设置通用的请求属性
connection.setRequestProperty("accept", "*/*");
connection.setRequestProperty("connection", "Keep-Alive");
connection.setRequestProperty("user-agent",
"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
// 建立实际的连接
connection.connect();
// 获取所有响应头字段
Map<String, List<String>> map = connection.getHeaderFields();
// 遍历所有的响应头字段
for (String key : map.keySet()) {
System.out.println(key + "--->" + map.get(key));
}
// 定义 BufferedReader输入流来读取URL的响应
in = new BufferedReader(new InputStreamReader(
connection.getInputStream()));
String line;
while ((line = in.readLine()) != null) {
result += line;
}
} catch (Exception e) {
System.out.println("发送GET请求出现异常!" + e);
e.printStackTrace();
}
// 使用finally块来关闭输入流
finally {
try {
if (in != null) {
in.close();
}
} catch (Exception e2) {
e2.printStackTrace();
}
}
return result;
}
/**
* 向指定 URL 发送POST方法的请求
*
* @param url
* 发送请求的 URL
* @param param
* 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。
* @return 所代表远程资源的响应结果
*/
public static String sendPost(String url, String param) {
//System.out.println(url+":"+param);
OutputStream out = null;
BufferedReader in = null;
String result = "";
try {
URL realUrl = new URL(url);
// 打开和URL之间的连接
URLConnection conn = realUrl.openConnection();
// 设置通用的请求属性
conn.setRequestProperty("Content-Type", "application/json;charset=UTF-8");
conn.setRequestProperty("Accept-Charset", "UTF-8");
conn.setRequestProperty("Accept", "application/json;charset=UTF-8");
conn.setRequestProperty("connection", "Keep-Alive");
conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
// 发送POST请求必须设置如下两行
conn.setDoOutput(true);
conn.setDoInput(true);
// 获取URLConnection对象对应的输出流
conn.connect();
out = conn.getOutputStream();
// 发送请求参数
out.write(param.getBytes("UTF-8"));
// flush输出流的缓冲
out.flush();
out.close();
// 定义BufferedReader输入流来读取URL的响应
in = new BufferedReader(
new InputStreamReader(conn.getInputStream(), "UTF-8"));
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();
}
}
return result;
}
/**
* 元转换成分
* @param amount
* @return
*/
public static String getMoney(String amount) {
if(amount==null){
return "";
}
// 金额转化为分为单位
// 处理包含, ¥ 或者$的金额
String currency = amount.replaceAll("\\$|\\¥|\\,", "");
int index = currency.indexOf(".");
int length = currency.length();
Long amLong = 0l;
if(index == -1){
amLong = Long.valueOf(currency+"00");
}else if(length - index >= 3){
amLong = Long.valueOf((currency.substring(0, index+3)).replace(".", ""));
}else if(length - index == 2){
amLong = Long.valueOf((currency.substring(0, index+2)).replace(".", "")+0);
}else{
amLong = Long.valueOf((currency.substring(0, index+1)).replace(".", "")+"00");
}
return amLong.toString();
}
}
不出意外prepay_id 值已经成功获取到了。 当然在调试的过程中遇到了各种问题。
商户ID与APPID不匹配
sub_openid and sub_appid not match
签名失败
等错误.
首先~ 你得保证,你的sign签名没有问题. 顺序正确,没有传入空值,不存在大小写错误,在签名验证工具中可以通过.如果还是报错签名失败!!!!!返到服务商配置,APIV2 或者APIV3密钥反复修改吧。2~3次就可以了
--------------------------------------------分割线------------------------------------------------------
OKK!!!!!小程序开始调起微信支付 激动人心的时候
还是先看一下文档 [微信小程序支付文档]
(https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_4.shtml)
以下参数注意!!!
1)appId为特约商户的小程序ID千万不要弄混了
2)key 就是服务商APIV2 或者APIV3的密钥
3) 其他参数没什么好说的,paySign 此参数巨坑!! 请注意
首先进行此参数拼接 先看文档–https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=7_7&index=3
// 预付商品id
String prepay_id = resultMap.get("prepay_id").toString();
SortedMap<Object,Object> payMap = new TreeMap<>();
payMap.put("appId", PayConst.CONTRIBUTING_APPID); //特约商户下的小程序appId
payMap.put("nonceStr",out_trade_no);
payMap.put("package", "prepay_id="+prepay_id);
payMap.put("signType", "MD5");
payMap.put("timeStamp", PayCommonUtil.create_timestamp());
//二次签名
String paySign= PayCommonUtil.createSign("UTF-8",payMap);
ok 到现在 支付所需要的值都已经全部拿到了,小程序那边直接发起支付就OK了
扣钱 美滋滋
//提交微信支付
pay:function(){
let _this=this
wx.requestPayment({
appId:"小程序APPId",
timeStamp: _this.data.payObject.timeStamp,
nonceStr: _this.data.payObject.nonceStr,
package:_this.data.payObject.package,
signType:_this.data.payObject.signType,
paySign:_this.data.payObject.sign,
success:function(event){
console.log(event)
wx.showToast({
title: '支付成功',
icon:'success',
duration:2000,
complete: function () {
//支付成功后的逻辑
}
})
},
fail:function(error){
console.log("支付失败")
console.log(error)
},
})
}
至此 微信支付结束
微信退费
支付都已经OK了 退费肯定就是小意思啦~~,参数直接调用,带上我们的安全证书即可
直接上代码
//元转分
String sum= query.getString("sum");
BigDecimal bigDecimal = new BigDecimal(sum).setScale(2);
Integer amount=bigDecimal.multiply(new BigDecimal(100)).intValue();
refunds.put("appid", PayConst.SERVICE_APPID); //服务号ID
refunds.put("mch_id", PayConst.ORDER_ID); //商户号
refunds.put("sub_mch_id", PayConst.CONTRIBUTING_MCH_ID); //子商户号
refunds.put("nonce_str", WXPayUtil.generateNonceStr()+""); //随机字符串
refunds.put("out_trade_no",query.getString("serial"));//微信支付订单号
refunds.put("out_refund_no", query.getString("serial"));//退款单号
refunds.put("total_fee",amount+"");
refunds.put("refund_fee",amount+"");
refunds.put("refund_desc",query.getString("cancelInit")); //退款原因
String sign=PayCommonUtil.createSign("UTF-8",refunds);//签名
refunds.put("sign",sign);
String parametersXml=PayCommonUtil.getRequestXml(refunds);
System.out.println(parametersXml);
String results = CommonUtil.sendPostRefund(PayConst.RETURN_ORDER_URL, parametersXml,true);//统一下单
logger.info("微信退款返回"+results);