一、申请微信公众平台、微信商户号
1、首先需要去微信公众平台注册企业级小程序,个人账号是无法进行微信认证,没有认证的账号无法申请小程序支付功能(微信认证一次需要300元和一系列给腾讯账号打钱环节,太坑了,每次打钱几毛钱,手续费好几块),注册成功后会分配小程序的appID,后续对接支付时要用到。之后则可以申请微信支付,需要自己去申请商户号然后绑定小程序的appID。
2、申请商户号流程网上可以搜到,或者按照腾讯的步骤一步一步也能申请成功,这个不是很难。申请成功后,还需要填写一些基本设置都是程序中要用到的数据,比如商户平台的密钥key,之后可以绑定好小程序的ID,然后就可以设置支付了。在产品中心---》AppID账号管理里面关联小程序。
在我的产品中开通JSAPI支付权限
开发配置里面的JSAPI支付中的授权目录则是用来设置支付成功后的回调,比如微信支付成功后,微信会调用这个地址来反馈支付的结果,后端接收到后做一系列的处理。该地址必须是公网地址,域名可以是带端口号,亲测可以。http协议可以使用,建议是使用https。
以上设置完成即可。
二、uni-app+SSM框架对接微信支付
1、首先需要在支付页面的onLoad方法中调用uni.login来获取当前微信用户的code值,然后调用获取openID的方法。
onLoad() {
const that=this
uni.login({
success: res => {
//code值(5分钟失效)
console.info(res.code);
that.getOpenId(res.code);
}
});
}
2、通过用户的code值调用Java后端接口,来获得openID。
async getOpenId(data){
let jss = this.$store.state.user.JSESSIONID;
let param = {
mobileLogin: true,
JSESSIONID: jss,
code:data,
};
let type = RS.RequestType.getOpenid;
try {
//调用接口得到返回的openID
var res = await RS.RequestServer.fetch(type, param);
} catch (e) {
console.log(e)
uni.showToast({
title: e.msg || e.errMsg,
duration: 500,
})
}
this.openid=res.data.body.openid
}
3、Java后端获取到code值,调用微信提供的接口来获取openID
@ResponseBody
@RequestMapping(value = "getOpenid")
public AjaxJson getOpenid(String code) throws IOException {
AjaxJson j = new AjaxJson();
//wx接口路径
String url = "https://api.weixin.qq.com/sns/jscode2session?grant_type=authorization_code&" +"appid=" + APPID + "&secret=" + SECRET + "&js_code=" + code;
//使用HttpClient发送请求
CloseableHttpClient httpclient = HttpClients.createDefault();
//发送Get请求
HttpGet request = new HttpGet(url);
request.addHeader("Content-Type", "application/json");
//获得响应
CloseableHttpResponse response = httpclient.execute(request);
//拿到响应体
HttpEntity httpEntity = response.getEntity();
//使用工具转换
String result = EntityUtils.toString(httpEntity, "UTF-8");// 转成string
JSONObject jsonObject = JSONObject.parseObject(result);
System.out.println(jsonObject);//拿到的所有内容
String openid = jsonObject.get("openid").toString();
System.out.println(openid);//拿到的openid
j.setSuccess(true);
j.put("openid", openid);
return j;
}
4、当用户输入金额,点击提交按钮,则需要把金额、openID先发送到后端进行处理,后端接收后生成订单然后调用微信接口把这些信息发送过去,发送之后把生成的订单信息反馈到支付页面,然后把这些信息放到uni.requestPayment里面。
async commit() {
let jss = this.$store.state.user.JSESSIONID;
let username = this.$store.state.user.username;
var that=this
uni.showLoading()
let param = {
mobileLogin: true,
JSESSIONID: jss,
money:this.money,
openid:this.openid,
};
//调用接口把金额和openid发送到后端进行处理
let type = RS.RequestType.orderResult;
try {
var res = await RS.RequestServer.fetch(type, param);
} catch (e) {
console.log(e)
uni.showToast({
title: e.msg || e.errMsg,
duration: 500,
})
}
console.log(res)
//把得到的结果取出来,然后调用uni.requestPayment,则会弹出填写支付密码的页面,支付成功则调用success,支付失败则调用fail。
const configdata=res.data.body.orderResult
this.orderNo=res.data.body.orderNo
this.mchid=res.data.body.mchid
uni.requestPayment({
provider: 'wxpay',
appId:configdata.appId,
timeStamp: configdata.timeStamp,
nonceStr: configdata.nonceStr,
package: configdata.package,
signType: "MD5",
paySign: configdata.paySign,
success: function(res) {
console.log('success:' + JSON.stringify(res));
},
fail: function(err) {
console.log('fail:' + JSON.stringify(err));
}
});
uni.hideLoading()
}
5、Java后端的orderResult接口和回调方法,里面包含了一些处理数据的方法,所有处理微信支付的方法都在里面。
@Controller
@RequestMapping(value = "${adminPath}/miniprogram/miniprogram")
public class MiniprogramConfig extends BaseController {
//小程序appid ,需要改为真实的
private final static String APPID = "";
//小程序secret ,需要改为真实的
private final static String SECRET = "";
@ResponseBody
@RequestMapping(value = "getOpenid")
public AjaxJson getOpenid(String code) throws IOException {
AjaxJson j = new AjaxJson();
//wx接口路径
String url = "https://api.weixin.qq.com/sns/jscode2session?grant_type=authorization_code&" +
"appid=" + APPID + "&secret=" + SECRET + "&js_code=" + code;
//使用HttpClient发送请求
CloseableHttpClient httpclient = HttpClients.createDefault();
//发送Get请求
HttpGet request = new HttpGet(url);
request.addHeader("Content-Type", "application/json");
//获得响应
CloseableHttpResponse response = httpclient.execute(request);
//拿到响应体
HttpEntity httpEntity = response.getEntity();
//使用工具转换
String result = EntityUtils.toString(httpEntity, "UTF-8");// 转成string
JSONObject jsonObject = JSONObject.parseObject(result);
System.out.println(jsonObject);//拿到的所有内容
String openid = jsonObject.get("openid").toString();
System.out.println(openid);//拿到的openid
j.setSuccess(true);
j.put("openid", openid);
return j;
}
@ResponseBody
@RequestMapping(value = "orderResult")
public AjaxJson orderResult(Miniprogram miniprogram) throws IOException{
AjaxJson j = new AjaxJson();
//商户号
String mch_id = "";
//key为商户平台设置的密钥key
String WXKeHukey = "";
String appId = APPID;
String notify_url = "https://www.hnskxxjsyxgs.cn/zzys/a/miniprogram/miniprogram/xcxNotify";//微信回调地址
String trade_type = "JSAPI";
//签名类型
String SIGNTYPE = "MD5";
//第一次发起支付请求的接口地址
String pay_url = "https://api.mch.weixin.qq.com/pay/unifiedorder";
//生成的随机字符串
String nonce_str = getRandomString(32);
//本机的ip地址
String spbill_create_ip = "";
//商户订单号(先用时间代替),可以自己定义订单号格式
Date date = new Date();
String randomString = OrderUtils.getRandomString(5);
String orderNo = "R"+randomString+date.getTime();
Map<String, String> packageParams = new HashMap<String, String>();
//小程序ID,微信分配的小程序ID
packageParams.put("appid", appId);
//商户号,微信支付分配的商户号
packageParams.put("mch_id", mch_id);
//随机字符串,长度要求在32位以内。
packageParams.put("nonce_str", nonce_str);
//商品简单描述
packageParams.put("body", "用户充值");
//商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-|*且在同一个商户号下唯一
packageParams.put("out_trade_no", orderNo);//商户订单号
String money = miniprogram.getMoney().toString();
String changeBranch = changeBranch(money);
//订单总金额,单位为分
packageParams.put("total_fee", changeBranch);
//终端IP,支持IPV4和IPV6两种格式的IP地址。调用微信支付API的机器IP
packageParams.put("spbill_create_ip", spbill_create_ip);
//异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。
packageParams.put("notify_url", notify_url);
//交易类型(JSAPI--JSAPI支付(或小程序支付)、NATIVE--Native支付、APP--app支付,MWEB--H5支付,不同trade_type决定了调起支付的方式,请根据支付产品正确上传)
packageParams.put("trade_type", trade_type);
//用户标识,trade_type=JSAPI,此参数必传,用户在商户appid下的唯一标识。
packageParams.put("openid", miniprogram.getOpenid());
// 除去数组中的空值和签名参数
packageParams = paraFilter(packageParams);
String prestr = createLinkString(packageParams); // 把数组所有元素,按照“参数=参数值”的模式用“&”字符拼接成字符串
//MD5运算生成签名,这里是第一次签名,用于调用统一下单接口,key为商户平台设置的密钥key
String mysign = sign(prestr, WXKeHukey, "utf-8").toUpperCase();
System.out.println("=======================第一次签名:" + mysign + "=====================");
//拼接统一下单接口使用的xml数据,要将上一步生成的签名一起拼接进去
String xml = "<xml version='1.0' encoding='gbk'>" + "<appid>" + appId + "</appid>"
+ "<body><![CDATA[" + "用户充值" + "]]></body>"
+ "<mch_id>" + mch_id + "</mch_id>"
+ "<nonce_str>" + nonce_str + "</nonce_str>"
+ "<notify_url>" + notify_url + "</notify_url>"
+ "<openid>" + miniprogram.getOpenid() + "</openid>"
+ "<out_trade_no>" + orderNo + "</out_trade_no>"
+ "<spbill_create_ip>" + spbill_create_ip + "</spbill_create_ip>"
+ "<total_fee>" + changeBranch + "</total_fee>"
+ "<trade_type>" + trade_type + "</trade_type>"
+ "<sign>" + mysign + "</sign>"
+ "</xml>";
System.out.println("调试模式_统一下单接口 请求XML数据:" + xml);
//调用统一下单接口,并接受返回的结果
String result = httpRequest(pay_url, "POST", xml);
System.out.println("调试模式_统一下单接口 返回XML数据:" + result);
// 将解析结果存储在HashMap中
// Map map = doXMLParse(result);
// String return_code = (String) map.get("return_code");//返回状态码
//解析结果,将结果存在map中
Map map = doXMLToMap(result);
String return_code = (String) map.get("return_code");
//返回给移动端需要的参数
Map<String, Object> response = new HashMap<String, Object>();
if(return_code == "SUCCESS" || return_code.equals(return_code)){
// 业务结果
String prepay_id = (String) map.get("prepay_id");//返回的预付单信息
response.put("nonceStr", nonce_str);
response.put("package", "prepay_id=" + prepay_id);
Long timeStamp = System.currentTimeMillis() / 1000;
response.put("timeStamp", timeStamp + "");//这边要将返回的时间戳转化成字符串,不然小程序端调用wx.requestPayment方法会报签名错误
String stringSignTemp = "appId=" + appId + "&nonceStr=" + nonce_str + "&package=prepay_id=" + prepay_id+ "&signType=" + SIGNTYPE + "&timeStamp=" + timeStamp;
//再次签名,这个签名用于小程序端调用wx.requesetPayment方法
String paySign = sign(stringSignTemp, WXKeHukey, "utf-8").toUpperCase();
System.out.println("=======================第二次签名:" + paySign + "=====================");
response.put("paySign", paySign);
response.put("appid", appId);
}
j.setSuccess(true);
j.put("orderResult", response);
j.put("orderNo", orderNo);
j.put("mchid", mch_id);
return j;
}
/**
* 解析xml,取出其中的result_code,和prepay_id
*/
public HashMap<String ,String> doXMLToMap(String results) {
HashMap<String ,String> result = new HashMap<String ,String>();
String return_code = results.substring(results.indexOf("<return_code><![CDATA[")+22, results.indexOf("]]></return_code>"));
result.put("return_code", return_code);
if(return_code.equals("SUCCESS")) {
String prepay_id = results.substring(results.indexOf("<prepay_id><![CDATA[")+20, results.indexOf("]]></prepay_id>"));
result.put("prepay_id", prepay_id);
}
return result;
}
/**
*
* @param requestUrl请求地址
* @param requestMethod请求方法
* @param outputStr参数
*/
public static String httpRequest(String requestUrl, String requestMethod, String outputStr) {
// 创建SSLContext
StringBuffer buffer = null;
try {
URL url = new URL(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 = new BufferedReader(isr);
buffer = new StringBuffer();
String line = null;
while ((line = br.readLine()) != null) {
buffer.append(line);
}
br.close();
} catch (Exception e) {
e.printStackTrace();
}
return buffer.toString();
}
/**
* 除去数组中的空值和签名参数
*
* @param sArray 签名参数组
* @return 去掉空值与签名参数后的新签名参数组
*/
public static Map<String, String> paraFilter(Map<String, String> sArray) {
Map<String, String> result = new HashMap<String, String>();
if (sArray == null || sArray.size() <= 0) {
return result;
}
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);
}
return result;
}
/**
* 把数组所有元素排序,并按照“参数=参数值”的模式用“&”字符拼接成字符串
*
* @param params 需要排序并参与字符拼接的参数组
* @return 拼接后字符串
*/
public static String createLinkString(Map<String, String> params) {
List<String> keys = new ArrayList<String>(params.keySet());
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 + "&";
}
}
return prestr;
}
/**
* 签名字符串
* @param text需要签名的字符串
* @param key 密钥
* @param input_charset编码格式
* @return 签名结果
*/
public static String sign(String text, String key, String input_charset) {
text = text + "&key=" + key;
return DigestUtils.md5DigestAsHex(getContentBytes(text, input_charset));
}
/**
* @param content
* @param charset
* @return
* @throws SignatureException
* @throws UnsupportedEncodingException
*/
public static byte[] getContentBytes(String content, String charset) {
if (charset == null || "".equals(charset)) {
return content.getBytes();
}
try {
return content.getBytes(charset);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("MD5签名过程中出现错误,指定的编码集不对,您目前指定的编码集是:" + charset);
}
}
public static String getRandomString(int length){
String str="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
Random random=new Random();
StringBuffer sb=new StringBuffer();
for(int i=0;i<length;i++){
int number=random.nextInt(62);
sb.append(str.charAt(number));
}
return sb.toString();
}
/**
*
* 将元为单位的转换为分 (乘100)
*
* @param amount
* @return
*/
public static String changeBranch(Long amount) {
return BigDecimal.valueOf(amount).multiply(new BigDecimal(100)).toString();
}
/**
* 将元为单位的转换为分 替换小数点,支持以逗号区分的金额
*
* @param amount
* @return
*/
public static String changeBranch(String amount) {
String currency = amount.replaceAll("\\$|\\¥|\\,", ""); // 处理包含, ¥
// 或者$的金额
int index = currency.indexOf(".");
int length = currency.length();
Long amLong;
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();
}
@ResponseBody
@RequestMapping(value="xcxNotify")
public String xcxNotify(HttpServletRequest request,HttpServletResponse response) throws Exception {
InputStream inputStream = request.getInputStream();
//获取请求输入流
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
while ((len=inputStream.read(buffer))!=-1){
outputStream.write(buffer,0,len);
}
outputStream.close();
inputStream.close();
Map<String,Object> map = getMapFromXML(new String(outputStream.toByteArray(),"utf-8"));
logger.info("【小程序支付回调】 回调数据: \n"+map);
String resXml = "";
String returnCode = (String) map.get("return_code");
if ("SUCCESS".equalsIgnoreCase(returnCode)) {
String returnmsg = (String) map.get("result_code");
if("SUCCESS".equals(returnmsg)){
//支付成功则会收到微信回调反馈的信息,可以在下面做一些逻辑处理
String totalFee = (String) map.get("total_fee");//充值金额
String mchid = (String) map.get("mch_id");//商户号
String orderNumber = (String) map.get("out_trade_no");//订单号
//如果不返回resXml,则微信回调会一直调用,直到你返回支付成功的resXml才停止
//支付成功
resXml = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>"
+ "<return_msg><![CDATA[OK]]></return_msg>"+"</xml>";
}else{
resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>"
+ "<return_msg><![CDATA[报文为空]></return_msg>" + "</xml>";
logger.info("支付失败:"+resXml);
}
}else{
resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>"
+ "<return_msg><![CDATA[报文为空]></return_msg>" + "</xml>";
logger.info("【订单支付失败】");
}
logger.info("【小程序支付回调响应】 响应内容:\n"+resXml);
Thread.sleep(1000);
return resXml;
}
public static Map<String, Object> getMapFromXML(String responseString) {
Map<String, Object> map = new HashMap<String, Object>();
try {
SAXReader reader = new SAXReader();
Document doc;
doc = reader.read(new ByteArrayInputStream(responseString.getBytes("UTF-8")));
// doc = reader.read(responseString);
Element root = doc.getRootElement();
List<Element> list = root.elements();
for (Element element : list) {
map.put(element.getName(), element.getText());
}
} catch (DocumentException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return map;
}
/**
* 金额为分的格式
*/
public static final String CURRENCY_FEN_REGEX = "\\-?[0-9]+";
/**
* 将分为单位的转换为元 (除100)
*
* @param amount
* @return
* @throws Exception
*/
public static String fen2YuanStr(String amount) {
if (!amount.matches(CURRENCY_FEN_REGEX)) {
throw new RuntimeException("金额格式错误|"+amount);
}
return formatFen(BigDecimal.valueOf(Long.valueOf(amount)).divide(new BigDecimal(100)));
}
/**
* 格式化数字
* @param fen
* @return
*/
private static String formatFen(BigDecimal fen){
DecimalFormat df1 = new DecimalFormat("0.00");
return df1.format(fen);
}
/**
* 将元为单位的参数转换为分 , 只对小数点前2位支持
*
* @param yuan
* @return
* @throws Exception
*/
public static String yuan2FenInt(String yuan){
BigDecimal fenBd = new BigDecimal(yuan).multiply(new BigDecimal(100));
fenBd = fenBd.setScale(0, BigDecimal.ROUND_HALF_UP);
return String.valueOf(fenBd.intValue());
}