· 首先记录几个开发过程中的坑:
- 提示签名错误,但是在微信官方提供的校验显示正确(官方校验只会校验输入字符串和生成的sign)。如果在没有其他可以运行的业务证明key没有问题的话,这可能是微信支付Key的初始化问题,这个key很可能在第一次初始化时出现错误,需要重置key的值,可以重置成一样的。
- body字段传入时,不能为空、中文需要指定编码
- 总金额必须要求整数,最终金额是/1000得到的
- trade_type=JSAPI时用户的openid一定要传,但是不是子商户的情况下sub_openid不是必传的
- 如果还会出现签名错误,请将所有的参数都和微信自己的实例demo调整到一致后尝试
- 微信小程序的支付完成回调会在用户点击支付成功页面的"完成"按钮后才会回调
制作小程序时需要用户支付定金功能,微信的支付功能接口依旧要求传输XML作为请求体,同时要求进行所有字段的签名操作,首先编写两个bean作为统一下单传输的XML和请求订单状态的XML。
import lombok.Data;
import java.io.Serializable;
/**
*统一下单的请求bean
**/
@Data
public class PaymentDto implements Serializable {
private String appid;//小程序ID
private String mch_id;//商户号
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;//通知地址
private String trade_type;//交易类型
private String limit_pay;//指定支付方式
private String openid;//用户标识
}
import lombok.Data;
import java.io.Serializable;
@Data
public class GetWxOrderDto implements Serializable {
private String appid;
private String mch_id;//商户号
private String nonce_str;//随机字符串
private String sign;//签名
private String out_trade_no;//商户订单号
}
接下来进行一些常量的配置,如下代码所示。
private final String mch_id = "";//商户号
private final String spbill_create_ip = "";//终端IP
private final String notify_url = "/wx-make-true";//回调通知地址,因为手动拉取订单状态,所以可以随便传
private final String trade_type = "JSAPI";//交易类型
private final String url = "https://api.mch.weixin.qq.com/pay/unifiedorder";//统一下单API接口链接
private final String key = "&key="; // 商户支付密钥,在生成sign时需要使用,格式为&key=XXXXX
private final String appid = "";//小程序appid
具体的应用方法和生成对象的类,写的比较乱直接写在了一个方法里,没有封装一下,源代码来自网络哪位大佬的文字。
/**
* @param openId
* @param total_fee 订单总金额,单位为分。
* @param body 商品简单描述,该字段请按照规范传递。 例:腾讯充值中心-心悦会员充值
* @param attach 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用。 例:广州分店
* @return
* @throws UnsupportedEncodingException
* @throws DocumentException
*/
private JSONObject payment(String openId, String total_fee, String body, String out_trade_no, String attach) throws UnsupportedEncodingException, DocumentException, DocumentException, UnsupportedEncodingException {
body = new String(body.getBytes("UTF-8"), "ISO-8859-1");
String nonce_str = UUIDHexGenerator.generate();//随机字符串
PaymentDto paymentPo = new PaymentDto();
paymentPo.setAppid(appid);
paymentPo.setMch_id(mch_id);
paymentPo.setNonce_str(nonce_str);
String newbody = new String(body.getBytes("ISO-8859-1"), "UTF-8");//以utf-8编码放入paymentPo,微信支付要求字符编码统一采用UTF-8字符编码
paymentPo.setBody(newbody);
paymentPo.setOut_trade_no(out_trade_no);
paymentPo.setTotal_fee(total_fee);
paymentPo.setSpbill_create_ip(spbill_create_ip);
paymentPo.setNotify_url(notify_url);
paymentPo.setTrade_type(trade_type);
paymentPo.setOpenid(openId);
// 把请求参数打包成数组
Map<String, Object> sParaTemp = new HashMap<>();
sParaTemp.put("appid", paymentPo.getAppid());
sParaTemp.put("mch_id", paymentPo.getMch_id());
sParaTemp.put("nonce_str", paymentPo.getNonce_str());
sParaTemp.put("body", paymentPo.getBody());
sParaTemp.put("out_trade_no", paymentPo.getOut_trade_no());
sParaTemp.put("total_fee", paymentPo.getTotal_fee());
sParaTemp.put("spbill_create_ip", paymentPo.getSpbill_create_ip());
sParaTemp.put("notify_url", paymentPo.getNotify_url());
sParaTemp.put("trade_type", paymentPo.getTrade_type());
sParaTemp.put("openid", paymentPo.getOpenid());
// 除去数组中的空值和签名参数
Map sPara = PayUtils.paraFilter(sParaTemp);
String prestr = PayUtils.createLinkString(sPara); // 把数组所有元素,按照“参数=参数值”的模式用“&”字符拼接成字符串
//MD5运算生成签名
String mysign = PayUtils.sign(prestr, key, "utf-8").toUpperCase();
paymentPo.setSign(mysign);
//打包要发送的xml
String respXml = XmlUtil.messageToXML(paymentPo);
System.out.println(respXml);
// 打印respXml发现,得到的xml中有“__”不对,应该替换成“_”
respXml = respXml.replace("__", "_");
String param = respXml;
//String result = SendRequestForUrl.sendRequest(url, param);//发起请求
String result = PayUtils.httpRequest(url, "POST", param);
System.out.println("请求微信预支付接口,发送:" + param);
System.out.println("请求微信预支付接口,返回 result:" + result);
// 将解析结果存储在Map中
Map map = new HashMap();
InputStream in = new ByteArrayInputStream(result.getBytes());
// 读取输入流
SAXReader reader = new SAXReader();
Document document = reader.read(in);
// 得到xml根元素
Element root = document.getRootElement();
// 得到根元素的所有子节点
List<Element> elementList = root.elements();
for (Element element : elementList) {
map.put(element.getName(), element.getText());
}
// 返回信息
String return_code = map.get("return_code").toString();//返回状态码
String return_msg = map.get("return_msg").toString();//返回信息
String result_code = map.get("result_code").toString();//返回状态码
System.out.println("请求微信预支付接口,返回 code:" + return_code);
System.out.println("请求微信预支付接口,返回 msg:" + return_msg);
JSONObject wxReturnJson = new JSONObject();
if ("SUCCESS".equals(return_code) && "SUCCESS".equals(result_code)) {
// 业务结果
String prepay_id = map.get("prepay_id").toString();//返回的预付单信息
String nonceStr = UUIDHexGenerator.generate();
wxReturnJson.put("nonceStr", nonceStr);
wxReturnJson.put("package", "prepay_id=" + prepay_id);
Long timeStamp = System.currentTimeMillis() / 1000;
wxReturnJson.put("timeStamp", timeStamp + "");
String stringSignTemp = "appId=" + appid + "&nonceStr=" + nonceStr + "&package=prepay_id=" + prepay_id + "&signType=MD5&timeStamp=" + timeStamp;
//再次签名
String paySign = PayUtils.sign(stringSignTemp, key, "utf-8").toUpperCase();
wxReturnJson.put("paySign", paySign);
}
return wxReturnJson;
}
调用方法
JSONObject jsonObjectPayment = payment(wxUser.getOpenId(), "1", "qjh-member", out_trade_no, "支付定金");//支付订单,out_trade_no是生成的唯一订单
将这个Json返回给小程序端,小程序可以直接通过传递这些参数就可以掉起微信支付,当然在小程序中生成sign和随机串也是可以的。这样解决了前端的一些麻烦。在上述代码中使用到了封装的XML Util类和Pay Util类,在XML类中用来转化对象和XML文档,PayUtil中用来完成排序和签名,如下代码所示。
import org.apache.commons.codec.digest.DigestUtils;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.*;
public class PayUtils {
/**
* 签名字符串
*
* @param text 需要签名的字符串
* @param key 密钥
* @param input_charset 编码格式
* @return 签名结果
*/
public static String sign(String text, String key, String input_charset) {
text = text + key;
return DigestUtils.md5Hex(getContentBytes(text, input_charset));
}
/**
* 签名字符串
*
* @param text 需要签名的字符串
* @param sign 签名结果
* @param key 密钥
* @param input_charset 编码格式
* @return 签名结果
*/
public static boolean verify(String text, String sign, String key, String input_charset) {
text = text + key;
String mysign = DigestUtils.md5Hex(getContentBytes(text, input_charset));
return mysign.equals(sign);
}
/**
* @param content
* @param charset
* @return
* @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);
}
}
/**
* 生成6位或10位随机数 param codeLength(多少位)
*
* @return
*/
public static String createCode(int codeLength) {
String code = "";
for (int i = 0; i < codeLength; i++) {
code += (int) (Math.random() * 9);
}
return code;
}
private static boolean isValidChar(char ch) {
if ((ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z'))
return true;
return (ch >= 0x4e00 && ch <= 0x7fff) || (ch >= 0x8000 && ch <= 0x952f);
}
/**
* 除去数组中的空值和签名参数
*
* @param sArray 签名参数组
* @return 去掉空值与签名参数后的新签名参数组
*/
public static Map paraFilter(Map<String, Object> sArray) {
Map result = new HashMap();
if (sArray == null || sArray.size() <= 0) {
return result;
}
for (String key : sArray.keySet()) {
String value = (String) 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 params) {
List keys = new ArrayList(params.keySet());
Collections.sort(keys);
String prestr = "";
for (int i = 0; i < keys.size(); i++) {
String key = (String) keys.get(i);
String value = (String) params.get(key);
if (i == keys.size() - 1) {// 拼接时,不包括最后一个&字符
prestr = prestr + key + "=" + value;
} else {
prestr = prestr + key + "=" + value + "&";
}
}
return prestr;
}
/**
* @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);
}
} catch (Exception e) {
e.printStackTrace();
}
return buffer.toString();
}
public static String urlEncodeUTF8(String source) {
String result = source;
try {
result = java.net.URLEncoder.encode(source, "UTF-8");
} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return result;
}
}
import com.x.x.model.GetWxOrderDto;
import com.x.x.model.PaymentDto;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.core.util.QuickWriter;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import com.thoughtworks.xstream.io.xml.PrettyPrintWriter;
import com.thoughtworks.xstream.io.xml.XppDriver;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.Writer;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class XmlUtil {
public static Map<String, Object> parseXML(HttpServletRequest request) throws IOException, DocumentException {
Map<String, Object> map = new HashMap<String, Object>();
/* 通过IO获得Document */
SAXReader reader = new SAXReader();
Document doc = reader.read(request.getInputStream());
//得到xml的根节点
Element root = doc.getRootElement();
recursiveParseXML(root, map);
return map;
}
private static void recursiveParseXML(Element root, Map<String, Object> map) {
//得到根节点的子节点列表
List<Element> elementList = root.elements();
//判断有没有子元素列表
if (elementList.size() == 0) {
map.put(root.getName(), root.getTextTrim());
} else {
//遍历
for (Element e : elementList) {
recursiveParseXML(e, map);
}
}
}
private static XStream xstream = new XStream(new XppDriver() {
public HierarchicalStreamWriter createWriter(Writer out) {
return new PrettyPrintWriter(out) {
// 对所有xml节点都增加CDATA标记
boolean cdata = true;
public void startNode(String name, Class clazz) {
super.startNode(name, clazz);
}
protected void writeText(QuickWriter writer, String text) {
if (cdata) {
writer.write(text);
} else {
writer.write(text);
}
}
};
}
});
public static String messageToXML(PaymentDto paymentPo) {
xstream.alias("xml", PaymentDto.class);
// PaymentDto root=new PaymentDto();
// root.appid
return xstream.toXML(paymentPo);
}
public static String getPayStatusToXML(GetWxOrderDto getWxOrderDto) {
xstream.alias("xml", GetWxOrderDto.class);
return xstream.toXML(getWxOrderDto);
}
}
除了上述两个工具类,还使用了一个随机数的生成类,当然这个并没有必要使用这个类,毕竟不会存在这么多的订单和巧合,这只是一种随机数的生成方式,自己找到掉因子做做梅森旋转啥的也行。
import java.net.InetAddress;
public class UUIDHexGenerator {
private static String sep = "";
private static final int IP;
private static short counter = (short) 0;
private static final int JVM = (int) (System.currentTimeMillis() >>> 8);
private static UUIDHexGenerator uuidgen = new UUIDHexGenerator();
static {
int ipadd;
try {
ipadd = toInt(InetAddress.getLocalHost().getAddress());
} catch (Exception e) {
ipadd = 0;
}
IP = ipadd;
}
public static UUIDHexGenerator getInstance() {
return uuidgen;
}
public static int toInt(byte[] bytes) {
int result = 0;
for (int i = 0; i < 4; i++) {
result = (result << 8) - Byte.MIN_VALUE + bytes[i];
// result = (result << - Byte.MIN_VALUE + (int) bytes);
}
return result;
}
protected static String format(int intval) {
String formatted = Integer.toHexString(intval);
StringBuffer buf = new StringBuffer("00000000");
buf.replace(8 - formatted.length(), 8, formatted);
return buf.toString();
}
protected static String format(short shortval) {
String formatted = Integer.toHexString(shortval);
StringBuffer buf = new StringBuffer("0000");
buf.replace(4 - formatted.length(), 4, formatted);
return buf.toString();
}
protected static int getJVM() {
return JVM;
}
protected synchronized static short getCount() {
if (counter < 0) {
counter = 0;
}
return counter++;
}
protected static int getIP() {
return IP;
}
protected static short getHiTime() {
return (short) (System.currentTimeMillis() >>> 32);
}
protected static int getLoTime() {
return (int) System.currentTimeMillis();
}
public static String generate() {
return new StringBuffer(36).append(format(getIP())).append(sep).append(format(getJVM())).append(sep)
.append(format(getHiTime())).append(sep).append(format(getLoTime())).append(sep)
.append(format(getCount())).toString();
}
/**
* @param args
*/
public static void main(String[] args) {
String id = "";
UUIDHexGenerator uuid = UUIDHexGenerator.getInstance();
/*
for (int i = 0; i < 100; i++) {
id = uuid.generate();
}*/
id = generate();
System.out.println(id);
}
}
在用户点击完成后,可以请求一个新的接口来实现主动在微信服务器中拉取订单状态,这样就不用等微信的回调,内网的机器也不用配置穿透就可以实现查看微信订单状态,如下代码所示。
/**
* 微信支付不一定会推送,直接自取
* 触发器是用户主动的请求,会保留用户的订单状态
*/
@PostMapping("/get-wx-pay-result-self")
public ResultInfo getWxPayResultSelf(@RequestBody JSONObject jsonObject, HttpServletRequest request) throws DocumentException {
WxUser wxUser = (WxUser) request.getAttribute("wxUserInfo");
String nonceStr = UUIDHexGenerator.generate();
String out_trade_no = jsonObject.getString("out_trade_no");
GetWxOrderDto getWxOrderDto = new GetWxOrderDto();
getWxOrderDto.setAppid(appid);
getWxOrderDto.setMch_id(mch_id);
getWxOrderDto.setNonce_str(nonceStr);
getWxOrderDto.setOut_trade_no(out_trade_no);
Long timeStamp = System.currentTimeMillis() / 1000;
// 签名
// 把请求参数打包成数组
Map<String, Object> sParaTemp = new HashMap<>();
sParaTemp.put("appid", getWxOrderDto.getAppid());
sParaTemp.put("mch_id", getWxOrderDto.getMch_id());
sParaTemp.put("nonce_str", getWxOrderDto.getNonce_str());
sParaTemp.put("out_trade_no", getWxOrderDto.getOut_trade_no());
// 除去数组中的空值和签名参数
Map sPara = PayUtils.paraFilter(sParaTemp);
String prestr = PayUtils.createLinkString(sPara); // 把数组所有元素,按照“参数=参数值”的模式用“&”字符拼接成字符串
System.out.println(prestr);
//MD5运算生成签名
String mysign = PayUtils.sign(prestr, key, "utf-8").toUpperCase();
getWxOrderDto.setSign(mysign);
//打包要发送的xml
String respXml = XmlUtil.getPayStatusToXML(getWxOrderDto);
System.out.println(respXml);
respXml = respXml.replace("__", "_");
String param = respXml;
String result = PayUtils.httpRequest("https://api.mch.weixin.qq.com/pay/orderquery", "POST", param);
System.out.println("请求微信支付结果接口,发送:" + param);
System.out.println("请求微信支付结果接口,返回 result:" + result);
// 将解析结果存储在Map中
Map map = new HashMap();
InputStream in = new ByteArrayInputStream(result.getBytes());
// 读取输入流
SAXReader reader = new SAXReader();
Document document = reader.read(in);
// 得到xml根元素
Element root = document.getRootElement();
// 得到根元素的所有子节点
List<Element> elementList = root.elements();
for (Element element : elementList) {
map.put(element.getName(), element.getText());
}
// 返回信息
String return_code = map.get("return_code").toString();//返回状态码
String result_code = map.get("result_code").toString();//业务结果
String trade_state = map.get("trade_state").toString();//交易状态
if (trade_state.equals("SUCCESS")) {
// 支付成功
// 更改用户状态
return new ResultInfo(Status.SUCCESS);
} else {
return new ResultInfo(Status.REQUEST_PARAMETER_ERROR);
}
}