java-微信小程序支付代码分享
程序员文章站
2024-03-09 08:15:53
...
按照微信支付的流程来进行:
1.小程序调用后台接口生成预支付订单:
package com.stonedt.controller;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.alibaba.fastjson.JSON;
import com.stonedt.entity.HosOrder;
import com.stonedt.util.DateKit;
import com.stonedt.util.HttpClientsKit;
import com.stonedt.util.ResultUtil;
import com.stonedt.util.WxUtils;
public class Test {
// 小程序appid
public static final String appid = "";
// app **
public static final String secret = "";
// 微信支付的商户id
public static final String mch_id = "";
// 微信支付的商户**
public static final String key = "";
// 支付成功后的服务器回调url
public static final String notify_url = "";
// 签名方式,固定值
public static final String SIGNTYPE = "MD5";
// 交易类型,小程序支付的固定值为JSAPI
public static final String trade_type = "JSAPI";
// 微信统一下单接口地址
public static final String pay_url = "https://api.mch.weixin.qq.com/pay/unifiedorder";
// 微信统一下单接口地址
public static final String body = "";
// 交易类型 : 这里是微信小程序支付
public static final String JSAPI = "JSAPI";
/**
* 支付接口
*
* @param openid 用户唯一标识
* @return
*/
@RequestMapping(value = "pay", produces = "text/json;charset=UTF-8")
@ResponseBody
public String pay(HttpServletRequest request,String openid) {
// 商户订单号--->订单号唯一
//这里用时间戳模拟 商户订单号
String out_trade_no = System.currentTimeMillis()+"";
// 生成商户订单类 预支付信息组装 用于存入数据库
/**
* 这里省略 ....个人生成自己的 订单类存入数据
*/
// 生成随机串
String nonce_str = WxUtils.getRandomStringByLength(32);
// 获取用户后台IP地址(即使用微信小程序的手机IP)
String spbill_create_ip = WxUtils.getIpAddr(request);
// 组装参数,用户生成统一下单接口的签名
Map<String, String> preParams = new HashMap<String, String>();
//attach可以存储你想要在回调中获取的数据
String attach = out_trade_no;
preParams.put("appid", appid);
preParams.put("attach", attach);
preParams.put("mch_id", mch_id);
preParams.put("nonce_str", nonce_str);
preParams.put("body", body);
preParams.put("out_trade_no", out_trade_no);
preParams.put("total_fee", "1");// hospital.getHosPrice()
preParams.put("spbill_create_ip", spbill_create_ip);// 用户ip
preParams.put("notify_url", notify_url);
preParams.put("trade_type", trade_type);
preParams.put("openid", openid);
// 参数按照参数名ASCII码从小到大排序拼接 (即key1=value1&key2=value2…)
Map<String, String> cleanSignArray = WxUtils.cleanSignArray(preParams);
String preSign = WxUtils.createLinkString(cleanSignArray);
System.out.println(preSign);
// MD5运算生成签名,这里是第一次签名,用于调用统一下单接口 --签名时 需要全部转换编码
System.out.println(WxUtils.sign(preSign, key, "utf-8"));
String fistSign = WxUtils.sign(preSign, key, "utf-8").toUpperCase(); // 问题商户**
// 拼接统一下单接口使用的xml数据,和params存储的数据一样,并且排列顺序安装a.b.c.d...的从小到大顺序排列 要将上一步生成的签名fistSign一起拼接进去
String xml = "<xml>" + "<appid><![CDATA[" + appid + "]]></appid>" + "<attach><![CDATA[" + attach
+ "]]></attach>" + "<body><![CDATA[" + body + "]]></body>" + "<mch_id><![CDATA[" + mch_id
+ "]]></mch_id>" + "<nonce_str><![CDATA[" + nonce_str + "]]></nonce_str>" + "<notify_url><![CDATA["
+ notify_url + "]]></notify_url>" + "<openid><![CDATA[" + openid + "]]></openid>"
+ "<out_trade_no><![CDATA[" + out_trade_no + "]]></out_trade_no>" + "<spbill_create_ip><![CDATA["
+ spbill_create_ip + "]]></spbill_create_ip>" + "<total_fee><![CDATA[1]]></total_fee>"
+ "<trade_type><![CDATA[" + JSAPI + "]]></trade_type>" + "<sign><![CDATA[" + fistSign + "]]></sign>"
+ "</xml>";
System.out.println("调试模式_统一下单接口 请求XML数据:" + xml);
// 调用统一下单接口,并接受返回的结果 ---》发送请求时 参数需要转换统一编码
String result = HttpClientsKit.httpRequest(pay_url, "POST", xml);
System.out.println("调试模式_统一下单接口 返回XML数据:" + result);
// 这里解析的是xml数据
Map returnMap = WxUtils.doXMLParse(result);
// 返回状态码
String return_code = (String) returnMap.get("return_code");
System.out.println("返回状态码" + return_code);
// 返回给小程序端的结果
Map<String, Object> respone = new HashMap<String, Object>();
// 返回给小程序端需要的参数
Map<String, String> data = new HashMap<String, String>();
// 有返回结果
if (return_code.equals("SUCCESS")) {
// 业务结果
String result_code = (String) returnMap.get("result_code");
if (!"SUCCESS".equals(result_code)) {
//业务结果为fail
respone.put("code", 1);
String err_code_des = (String) returnMap.get("err_code_des");
respone.put("msg", err_code_des);
}
String prepay_id = (String) returnMap.get("prepay_id");
data.put("appId", appid);
data.put("timeStamp", System.currentTimeMillis() + "");
data.put("nonceStr", "m9fil9bt27e49ag1jz54vtxffwci7e08");
data.put("package", "prepay_id=" + prepay_id);
data.put("signType", "MD5");
// 拼接签名需要的参数
// 参数按照参数名ASCII码从小到大排序拼接 (即key1=value1&key2=value2…)
Map<String, String> signTemp = WxUtils.cleanSignArray(data);
// 生成签名字符串
String tempSign = WxUtils.createLinkString(signTemp);
// 再次签名,这个签名用于小程序端调用wx.requesetPayment方法
String resSign = WxUtils.sign(tempSign, key, "utf-8");
data.put("paySign", resSign);
respone.put("data", data);
respone.put("code", 0);
respone.put("msg", "success");
} else {
//返回结果失败
respone.put("code", 1);
String return_msg = (String) returnMap.get("return_msg");
respone.put("msg", return_msg);
}
//返回的数据用于小 程序 调起支付 或 展示 错误信息
return JSON.toJSONString(respone);
}
}
2.小程序根据后台接口生成预支付订单后返回的数据生成预支付订单:
wx.requestPayment({
timeStamp: e.data.data.timeStamp,
nonceStr: e.data.data.nonceStr,
package: e.data.data.package,
signType: 'MD5',
paySign: e.data.data.paySign,
success: function (event) {
// success
console.log(event);
wx.showToast({
title: '支付成功',
icon: 'success',
duration: 2000
});
//处理 业务逻辑
},
fail: function (error) {
// fail
console.log("支付失败")
wx.showToast({
title: '支付失败',
icon: 'none',
duration: 2000
});
}
})
3.后台的支付回调:
/**
* 微信通知后台的接口
*
* @param request
* @return
*/
@RequestMapping(value = "notifi")
@ResponseBody
public void notifi(HttpServletRequest request, HttpServletResponse response) {
/**
* 当收到通知进行处理时,应检查对应业务数据的状态,判断该通知是否已经处理过
* 涉及查询数据库 这里省略
* 直接默认未处理
*/
String resXml = "";
try {
// 通过ServletInputStream读取http请求传入的数据
BufferedReader br = new BufferedReader(
new InputStreamReader((ServletInputStream) request.getInputStream()));
String temp = null;
// 获取微信返回的xml
StringBuffer wxRes = new StringBuffer();
while ((temp = br.readLine()) != null) {
wxRes.append(temp);
}
br.close();
// 处理微信返回的xml数据
String notityXml = wxRes.toString();
System.out.println("接收到微信的返回值(notityXml):" + notityXml);
Map map = WxUtils.doXMLParse(notityXml);
// 判断返回结果
String returnCode = (String) map.get("return_code");
System.out.println("微信返回结果returnCode:" + returnCode);
if ("SUCCESS".equals(returnCode)) {
// 判断result_code--->交易是否成功需要查看result_code来判断
String result_code = (String) map.get("result_code");
System.out.println("微信业务返回结果result_code:" + result_code);
if ("SUCCESS".equals(result_code)) {
// 成功
// 获取签名
String notitySign = (String) map.get("sign");
// 去除原sign
map.remove("sign");
// 对微信传回的数据进行2次签名验证
if (WxUtils.verify(WxUtils.createLinkString(map), notitySign, key, "utf-8")) {
// 修改订单信息 --根据订单号order_id
/**
* 修改 根据out_trade_no(我们自己的订单号-->从微信返回的XML中获取 然后修改数据库的订单记录下面三项作为举例)
*
* 支付时间(pay_time)
* 支付状态(order_status=1,支付成功)
* 微信流水号(wexin_serial_num)
*
*/
// 获取参数
// 微信支付订单号
String wxOrderId = (String) map.get("transaction_id");
// 商户订单号(我们自己的订单号)
String out_trade_no = (String) map.get("out_trade_no");
// 支付时间
String payTime = (String) map.get("time_end");
// 生成order类
HosOrder order = new HosOrder();
order.setOrderId(out_trade_no);
order.setPayTime(DateKit.localDateTime2Date(payTime));
order.setOrderStatus(1);
order.setWexinSerialNum(wxOrderId);
// 修改订单信息
/**
* 根据serviceImpl 修改 对应订单数据
*/
// 通知微信服务器已经处理成功
resXml = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>"
+ "<return_msg><![CDATA[OK]]></return_msg>" + "</xml> ";
} else {
//签名2次验证失败
System.out.println("微信签名验证失败------");
resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>"
+ "<return_msg><![CDATA[签名验证失败!]]></return_msg>" + "</xml> ";
}
} else {
//微信业务结果失败
String err_code_des = (String) map.get("err_code_des");
System.out.println("微信业务结果失败,错误信息err_code_des:" + err_code_des);
resXml = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>" + "<return_msg><![CDATA["
+ err_code_des + "]]></return_msg>" + "</xml> ";
}
}
System.out.println("微信支付回调数据结束resXml:" + resXml);
// 返回结果
BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream());
out.write(resXml.getBytes());
out.flush();
out.close();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
4.微信小程序支付涉及工具类:
package com.stonedt.util;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.security.AlgorithmParameters;
import java.security.Key;
import java.security.Security;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.input.SAXBuilder;
/**
* 微信支付工具类
* @author spring
*
*/
public class WxUtils {
/**
* 把字符串最后拼接上 key ,再进行MD5运算,再将得到的字符串所有字符转换为大写,得到sign
* @param text 需要签名的字符串
* @param key **
* @param inpit_charset 编码格式
* @return
*/
public static String sign(String text,String key,String inpit_charset) {
String sign=text+"&key="+key;
return DigestUtils.md5Hex(getContentBytes(sign, inpit_charset));
}
/**
* 验证签名是否正确
* @param text 需要签名的字符串
* @param sign 微信的签名结果
* @param key **
* @param input_charset 编码格式
* @return 验证结果
*/
public static boolean verify(String text, String sign, String key, String input_charset){
if(null==text||null==sign||null==key) {
return false;
}else {
text = text +"&key="+ key;
String mysign = DigestUtils.md5Hex(getContentBytes(text, input_charset)).toUpperCase();
System.out.println("打印微信返回的数据组装的新的sign--mysign:"+mysign);
if (mysign.equals(sign)) {
return true;
} else {
return false;
}
}
}
/**
* 把字符串按照一点编码格式转成字节数组
* @param content 字符串
* @param charset 编码方式
* @return
*/
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);
}
}
/**
* 除去数组中的空值和签名参数
* @param sArray 签名参数组
* @return 去掉空值与签名参数后的新签名参数组
*/
public static Map<String, String> cleanSignArray(Map<String, String> signArray) {
Map<String, String> result = new HashMap<String, String>();
if (signArray == null || signArray.size() <= 0) {
return result;
}
for (String key : signArray.keySet()) {
String value = signArray.get(key);
if (value == null || value.equals("")) {
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是其中的静态方法,是用来对List类型进行排序,规则是从小到大
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;
}
/**
* 解析xml,返回第一级元素键值对。如果第一级元素有子节点,则此节点的值是子节点的xml数据。
* @param strxml
* @return
* @throws JDOMException
* @throws IOException
*/
public static Map doXMLParse(String strxml){
Map m = new HashMap();
try {
if(null == strxml || "".equals(strxml)) {
return null;
}
InputStream in = String2Inputstream(strxml);
SAXBuilder builder = new SAXBuilder();
Document doc;
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();
} catch (JDOMException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
return m;
}
/**
* 获取子结点的xml
* @param children
* @return String
*/
public static String getChildrenText(List children) {
StringBuffer sb = new StringBuffer();
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("<" + name + ">");
if(!list.isEmpty()) {
sb.append(getChildrenText(list));
}
sb.append(value);
sb.append("</" + name + ">");
}
}
return sb.toString();
}
public static InputStream String2Inputstream(String str) {
return new ByteArrayInputStream(str.getBytes());
}
/**
* 获取真实的ip地址
* @param request
* @return
*/
public static String getIpAddr(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if(StringUtils.isNotEmpty(ip) && !"unKnown".equalsIgnoreCase(ip)){
//多次反向代理后会有多个ip值,第一个ip才是真实ip
int index = ip.indexOf(",");
if(index != -1){
return ip.substring(0,index);
}else{
return ip;
}
}
ip = request.getHeader("X-Real-IP");
if(StringUtils.isNotEmpty(ip) && !"unKnown".equalsIgnoreCase(ip)){
return ip;
}
return request.getRemoteAddr();
}
/**
* 获取一定长度的随机字符串,范围0-9,a-z
* @param length:指定字符串长度
* @return 一定长度的随机字符串
*/
public static String getRandomStringByLength(int length) {
String base = "abcdefghijklmnopqrstuvwxyz0123456789";
Random random = new Random();
StringBuffer sb = new StringBuffer();
for (int i = 0; i < length; i++) {
int number = random.nextInt(base.length());
sb.append(base.charAt(number));
}
return sb.toString();
}
////////////////下面是解密手机号的方法
// 算法名
public static final String KEY_NAME = "AES";
// 加解密算法/模式/填充方式
// ECB模式只用**即可对数据进行加密解密,CBC模式需要添加一个iv
public static final String CIPHER_ALGORITHM = "AES/CBC/PKCS7Padding";
/**
* 微信 数据解密<br/>
* 对称解密使用的算法为 AES-128-CBC,数据采用PKCS#7填充
* 对称解密的目标密文:encrypted=Base64_Decode(encryptData)
* 对称解密秘钥:key = Base64_Decode(session_key),aeskey是16字节
* 对称解密算法初始向量:iv = Base64_Decode(iv),同样是16字节
*
* @param encrypted 目标密文
* @param session_key 会话ID
* @param iv 加密算法的初始向量
*/
public static String wxDecrypt(String encrypted, String session_key, String iv) {
String json = null;
byte[] encrypted64 = Base64.decodeBase64(encrypted);
byte[] key64 = Base64.decodeBase64(session_key);
byte[] iv64 = Base64.decodeBase64(iv);
byte[] data;
try {
init();
json = new String(decrypt(encrypted64, key64, generateIV(iv64)));
} catch (Exception e) {
e.printStackTrace();
}
return json;
}
/**
* 初始化**
*/
public static void init() throws Exception {
Security.addProvider(new BouncyCastleProvider());
KeyGenerator.getInstance(KEY_NAME).init(128);
}
/**
* 生成iv
*/
public static AlgorithmParameters generateIV(byte[] iv) throws Exception {
// iv 为一个 16 字节的数组,这里采用和 iOS 端一样的构造方法,数据全为0
// Arrays.fill(iv, (byte) 0x00);
AlgorithmParameters params = AlgorithmParameters.getInstance(KEY_NAME);
params.init(new IvParameterSpec(iv));
return params;
}
/**
* 生成解密
*/
public static byte[] decrypt(byte[] encryptedData, byte[] keyBytes, AlgorithmParameters iv)
throws Exception {
Key key = new SecretKeySpec(keyBytes, KEY_NAME);
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
// 设置为解密模式
cipher.init(Cipher.DECRYPT_MODE, key, iv);
return cipher.doFinal(encryptedData);
}
}
微信返回的时间的格式问题(yyyyMMddHHmmss类型)
/**
* yyyyMMddHHmmss 转换为Date
* @param localDateTime
*/
public static Date localDateTime2Date( String datetime){
//先把符合格式的字符串时间 转换成LocalDateTime
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
LocalDateTime ldt = LocalDateTime.parse(datetime,dtf);
//把LocalDateTime 转换成Date
ZoneId zoneId = ZoneId.systemDefault();
ZonedDateTime zdt = ldt.atZone(zoneId);//Combines this date-time with a time-zone to create a ZonedDateTime.
Date date = Date.from(zdt.toInstant());
return date;
}
发送和接收 统一下单接口内容
/**
*
* @param requestUrl 请求地址
* @param requestMethod 请求方法
* @param outputStr 参数
*/
public static String httpRequest(String requestUrl,String requestMethod,String outputStr){
StringBuffer buffer = null;
try{
URL url = new URL(requestUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod(requestMethod);
conn.setRequestProperty("Content-Type","text/xml;charset=UTF-8");
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();
}
最后感觉一位博主:https://blog.csdn.net/zhourenfei17/article/details/77765585
借鉴了不少,虽然有些地方出现了问题。。。。感谢!
下一篇: 计算机系统安全2018【2】第四章