微信H5支付(Java)
一、场景介绍
微信H5支付是在手机移动浏览器端调起微信支付的方式。本文中仅介绍后台开发端的API对接,具体怎么开通H5支付,微信商户平台相关的内容请参考微信开发文档。
开通微信H5支付后,获取到APPID,商户号mch_id,商户支付**key等备用。
二、开发准备
1、域名
要求商户已有H5商城网站,并且域名已经过ICP备案。
(所以,对于个人开发demo测试并不适合)
2、项目
本文中一律使用SpringBoot项目下的配置。
3、配置文件
创建配置文件:api.properties,其中关键支付参数数据:
# APPID
wap.appid=wx1803e3r3f31614a6
# 商户号
wap.mchid=1494336672
# 支付**
wap.key=gOxGoTq6aYlg2ZFWvB2uhAR4Xh81l9U9
# 微信H5支付API地址
wap.unifiedorder=https://api.mch.weixin.qq.com/pay/unifiedorder
自定义配置文件的类,读取配置文件
@ConfigurationProperties(prefix = "wap", locations = "classpath:api.properties")
@Component
public class ApiConfig {
private String appid;
private String mchid;
private String key;
private String unifiedorder;
public String getUnifiedorder() {
return unifiedorder;
}
public void setUnifiedorder(String unifiedorder) {
this.unifiedorder = unifiedorder;
}
public String getAppid() {
return appid;
}
public void setAppid(String appid) {
this.appid = appid;
}
public String getMchid() {
return mchid;
}
public void setMchid(String mchid) {
this.mchid = mchid;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
}
三、准备开发
1、订单创建
支付的必要条件是必须创建完订单,获取到订单的一系列数据,包括商户订单号,商品名称,商品介绍等。(每个商城都有自己的订单创建方式,此处不做详细介绍)
2、获取用户真实IP
由于安全性考虑,H5支付要求商户在统一下单接口中上传用户真实ip地址“spbill_create_ip”,保证微信端获取的用户ip地址与商户端获取的一致。
此处参考:微信支付开发文档
或者我的博文:穿透代理获取用户真实IP地址
3、API对接
首先是一个Controller方法:
@ResponseBody
@RequestMapping("/wechatPay")
public String wechatPay(HttpServletRequest request, JSONObject order) {
JSONObject ret = new JSONObject();
ret.put("success", false);
ret.put("msg", "请求失败[CCO01]");
try {
// 获取用户真实IP
String ip = getClientIpAddress(request);
// 微信API调用相关
JSONObject wxPayJson = wxInfoService.getWXPayJSON(order,ip);
logger.info("微信支付返回参数 "+wxPayJson);
if("success".equalsIgnoreCase(wxPayJson.getString("return_code"))){
if("success".equalsIgnoreCase(wxPayJson.getString("result_code"))){
// 保存支付信息什么的
insertOrderPadPay(order, wxPayJson.getString("prepay_id"));
ret.put("success", true);
ret.put("msg", "ok");
ret.put("data", wxPayJson);
ret.put("orderNO", order.getString("orderNo"));
}
if("fail".equalsIgnoreCase(wxPayJson.getString("result_code"))){
ret.put("success", false);
ret.put("msg", wxPayJson.getString("err_code_des"));
ret.put("data", wxPayJson);
ret.put("orderNO", order.getString("orderNo"));
}
}else {
ret.put("success", false);
ret.put("msg", wxPayJson.getString("err_code_des"));
ret.put("data", wxPayJson);
ret.put("orderNO", order.getString("orderNo"));
}
} catch (Exception e) {
e.printStackTrace();
ret.put("msg", e.getMessage());
}
return ret.toString();
}
其中的获取用户IP的方法是:
private static final String[] HEADERS_TO_TRY = { "X-Forwarded-For", "Proxy-Client-IP", "WL-Proxy-Client-IP",
"HTTP_X_FORWARDED_FOR", "HTTP_X_FORWARDED", "HTTP_X_CLUSTER_CLIENT_IP", "HTTP_CLIENT_IP",
"HTTP_FORWARDED_FOR", "HTTP_FORWARDED", "HTTP_VIA", "REMOTE_ADDR", "PROXY_FORWARDED_FOR", "X-Real-IP"};
/**
* getClientIpAddress:(获取用户ip,可穿透代理).
* @author SongYapeng
* @Date 2018年3月2日下午4:41:47
* @param request
* @since JDK 1.8
*/
public static String getClientIpAddress(HttpServletRequest request) {
for (String header : HEADERS_TO_TRY) {
String ip = request.getHeader(header);
if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
if (ip != null && ip.indexOf(",") != -1) {
String[] ips = ip.split(",");
for (int i = 0; i < ips.length; i++) {
String ipMulti = (String) ips[i];
if (!("unknown".equalsIgnoreCase(ipMulti))) {
ip = ipMulti;
break;
}
}
}
return ip;
}
}
return request.getRemoteAddr();
}
接着getWXPayJSON方法实现如下:
public JSONObject getWXPayJSON(JSONObject order, String ip) throws Exception {
Map<String, String> map = new HashMap<String, String>();
/**
* order 商户订单信息
* appid 微信分配的公众账号ID(企业号corpid即为此appId)
* wap_url 网站域名
* nonce_str 随机字符串
* mch_id 微信支付分配的商户号
* notify_url 回调地址,支付结束后,根据相应的结果执行相应的步骤(如修改oms订单状态为已支付)
* out_trade_no 订单号
* total_fee 订单总金额,单位为分
* trade_type 支付方式微信h5支付
*/
String appid = apiConfig.getAppid();
String wap_url = apiConfig.getWap_url();
// 生成指定长度的随机字符串方法
String nonce_str = CommonUtil.createNonceStr(10);
// 商品描述
String body = Constants.WAP_NAME + "订单号:" + order.getString("orderNo");
String mch_id = apiConfig.getMchid();
String notify_url = wap_url + "/notify.html";
// 商户订单号
String out_trade_no = order.getString("orderNo");
String spbill_create_ip = ip;
// 支付金额,单位 分
Double saleMoney = order.getDouble("needSaleMoneySum") * 100;
BigDecimal total_fee = new BigDecimal(saleMoney);
total_fee = total_fee.setScale(0, BigDecimal.ROUND_HALF_UP);
// 交易类型 微信H5支付
String trade_type = "MWEB";
JSONObject json = new JSONObject();
// 场景信息
JSONObject scene_info = new JSONObject();
json.put("type", "WAP");
json.put("wap_url", wap_url);
// 网站名称,自定义
json.put("wap_name", Constants.WAP_NAME);
scene_info.put("h5_info", json);
map.put("appid", appid);
map.put("nonce_str", nonce_str);
map.put("body", body);
map.put("mch_id", mch_id);
map.put("notify_url", notify_url);
map.put("out_trade_no", out_trade_no);
map.put("spbill_create_ip", spbill_create_ip);
map.put("total_fee", total_fee + "");
map.put("trade_type", trade_type);
map.put("scene_info", scene_info.toString());
// 签名,很重要
map.put("sign", createSign(map, true));
return getUnifiedorder(map);
}
getWXPayJSON方法中的签名方法createSign(map, true)具体实现如下:
public String createSign(Map<String, String> map, boolean isLowerCase) throws Exception {
// map取出空值
Map<String, String> preMap = CommonUtil.delNull(map, isLowerCase);
// 排序并把数组所有元素按照参数=参数名 的模式用&字符拼接成字符串
String temp = CommonUtil.createSortParams(preMap, false, isLowerCase);
// 拼上key=key(商户支付秘钥)进行md5运算,再将得到的字符串所有字符转换为大写
String signStr = temp + "&key=" + apiConfig.getKey();
logger.info("待签名字符串:"+signStr);
String sign = CommonUtil.Sign(temp, apiConfig.getKey());
return sign;
}
getWXPayJSON方法中的getUnifiedorder方法如下:
public JSONObject getUnifiedorder(Map<String, String> map) throws Exception {
String unifiedorder = apiConfig.getUnifiedorder();
String xml = CommonUtil.map2xml(map, false);
String result = HttpClientUtil.httpPostXml(unifiedorder, null, xml);
JSONObject json = CommonUtil.xml2JSON(result);
logger.info("支付请求参数:"+map+";支付返回参数" + json);
return json;
}
上面方法中多次用到CommonUtil工具类中的方法,现呈上CommonUtil工具类:
package net.shopin.wap.common.util;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.math.BigDecimal;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.UUID;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.XMLWriter;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
public class CommonUtil {
/**
* 指定长度uuid
* @param length 长度
* @return String
*/
public static String createUUID(int length) {
if (length > 36) {
throw new RuntimeException("请控制长度在36位以内!");
} else {
String[] chars = new String[]{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o",
"p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5", "6", "7", "8",
"9", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S",
"T", "U", "V", "W", "X", "Y", "Z"};
StringBuffer shortBuffer = new StringBuffer();
String uuid = UUID.randomUUID().toString().replace("-", "");
for (int i = 0; i < length; i++) {
String str = uuid.substring(i * 4, i * 4 + 4);
int x = Integer.parseInt(str, 16);
shortBuffer.append(chars[x % 0x3E]);
}
return shortBuffer.toString().toLowerCase();
}
}
/**
* 生成指定长度的随机数字
* @param length 长度
* @return String
*/
public static String createNonceNum(int length) {
String chars = "0123456789";
String res = "";
for (int i = 0; i < length; i++) {
Random rd = new Random();
res += chars.charAt(rd.nextInt(chars.length() - 1));
}
return res;
}
/**
* 生成指定长度的随机字符串
* @param length 长度
* @return String
*/
public static String createNonceStr(int length) {
String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
String res = "";
for (int i = 0; i < length; i++) {
Random rd = new Random();
res += chars.charAt(rd.nextInt(chars.length() - 1));
}
return res;
}
/**
* xml2JSON:(xml转为fastJson).
* @param xml
* @return
* @throws DocumentException
* @author SongYapeng
* @Date 2017年12月19日下午2:13:16
* @since JDK 1.7
*/
public static JSONObject xml2JSON(String xml) throws DocumentException {
return elementToJSONObject(strToDocument(xml).getRootElement());
}
public static Document strToDocument(String xml) throws DocumentException {
return DocumentHelper.parseText(xml);
}
public static JSONObject elementToJSONObject(Element node) {
JSONObject result = new JSONObject();
/**
* 当前节点的名称、文本内容和属性
* 当前节点的所有属性的list
*/
@SuppressWarnings("unchecked")
List<Attribute> listAttr = node.attributes();
for (Attribute attr : listAttr) {
result.put(attr.getName(), attr.getValue());
}
/**
* 递归遍历当前节点所有的子节点
* 所有一级子节点的list
*/
@SuppressWarnings("unchecked")
List<Element> listElement = node.elements();
if (!listElement.isEmpty()) {
/**
* 遍历所有一级子节点
*/
for (Element e : listElement) {
/**
* 判断一级节点是否有属性和子节点
* 沒有则将当前节点作为上级节点的属性对待
*/
if (e.attributes().isEmpty() && e.elements().isEmpty())
result.put(e.getName(), e.getTextTrim());
else {
/**
* 判断父节点是否存在该一级节点名称的属性
* 没有则创建
* 将该一级节点放入该节点名称的属性对应的值中
*/
if (!result.containsKey(e.getName()))
result.put(e.getName(), new JSONArray());
((JSONArray) result.get(e.getName())).add(elementToJSONObject(e));
}
}
}
return result;
}
/**
* Map 转 XML
* @param map
* @param isLowerCase
* @return
*/
public static String map2xml(Map<String, String> map, boolean isLowerCase) {
map = CommonUtil.delNull(map, isLowerCase);
/**
* 开始对map进行解析
*/
if (map == null)
throw new NullPointerException("map 数据为空,不能解析!");
Document document = DocumentHelper.createDocument();
Element nodeElement = document.addElement("xml");
for (Object obj : map.keySet()) {
Element keyElement = nodeElement.addElement(String.valueOf(obj));
keyElement.setText(String.valueOf(map.get(obj)));
}
return doc2String(document);
}
/**
* Document 转 String
* @param document
* @return String
*/
public static String doc2String(Document document) {
String s = "";
try {
/**
* 使用输出流来进行转化
*/
ByteArrayOutputStream out = new ByteArrayOutputStream();
OutputFormat format = new OutputFormat(" ", true, "UTF-8");
XMLWriter writer = new XMLWriter(out, format);
writer.write(document);
s = out.toString("UTF-8");
} catch (Exception ex) {
ex.printStackTrace();
}
return s;
}
/**
* 去除Map中的空值
* @param map
* @return 去掉空值后的map
*/
public static Map<String, String> delNull(Map<String, String> map, boolean isLowerCase) {
Map<String, String> result = new HashMap<String, String>();
if (map == null || map.size() <= 0) {
return result;
}
for (String key : map.keySet()) {
String value = map.get(key);
if (value == null || value.equals("") || value.equals("null")) {
continue;
}
if (isLowerCase) {
result.put(key.toLowerCase(), value);
} else {
result.put(key, value);
}
}
return result;
}
/**
* 把Map所有元素排序,并按照“参数=参数值”的模式用“&”字符拼接成字符串
* @param params 需要排序并参与字符拼接的Map
* @param isEncode 是否对value进行urlencode
* @param isLowerCase 是否转换小写
* @return 拼接后字符串
*/
public static String createSortParams(Map<String, String> params, boolean isEncode, boolean isLowerCase) {
String result = "";
try {
List<String> keys = new ArrayList<String>(params.keySet());
Collections.sort(keys);
if (isEncode) {
for (int i = 0; i < keys.size(); i++) {
String key = keys.get(i);
if (isLowerCase) {
key = key.toLowerCase();
}
String value = URLEncoder.encode(params.get(key), "UTF-8");
result = result + key + "=" + value + "&";
}
} else {
for (int i = 0; i < keys.size(); i++) {
String key = keys.get(i);
if (isLowerCase) {
key = key.toLowerCase();
}
String value = params.get(key);
result = result + key + "=" + value + "&";
}
}
} catch (Exception e) {
e.printStackTrace();
}
return result.substring(0, result.length() - 1);
}
/**
* MD5签名,微信专用
* @param content 内容
* @param key key值
* @return
*/
public static String Sign(String content, String key) throws Exception {
String signStr = "";
if ("" == key) {
throw new Exception("财付通签名key不能为空!");
}
if ("" == content) {
throw new Exception("财付通签名内容不能为空");
}
signStr = content + "&key=" + key;
return MD5(signStr).toUpperCase();
}
/**
* MD5 加密
* @param data
* @return
*/
public final static String MD5(String data) {
char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
try {
byte[] btInput = data.getBytes();
/**
* 获得MD5摘要算法的 MessageDigest 对象
*/
MessageDigest mdInst = MessageDigest.getInstance("MD5");
/**
* 使用指定的字节更新摘要
*/
mdInst.update(btInput);
/**
* 获得密文
*/
byte[] md = mdInst.digest();
/**
* 把密文转换成十六进制的字符串形式
*/
int j = md.length;
char str[] = new char[j * 2];
int k = 0;
for (int i = 0; i < j; i++) {
byte byte0 = md[i];
str[k++] = hexDigits[byte0 >>> 4 & 0xf];
str[k++] = hexDigits[byte0 & 0xf];
}
return new String(str);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* sh1 加密
* @param s
* @return
*/
public final static String Sha1(String s) {
char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
try {
byte[] btInput = s.getBytes();
/**
* 获得MD5摘要算法的 MessageDigest 对象
*/
MessageDigest mdInst = MessageDigest.getInstance("sha-1");
/**
* 使用指定的字节更新摘要
*/
mdInst.update(btInput);
/**
* 获得密文
*/
byte[] md = mdInst.digest();
/**
* 把密文转换成十六进制的字符串形式
*/
int j = md.length;
char str[] = new char[j * 2];
int k = 0;
for (int i = 0; i < j; i++) {
byte byte0 = md[i];
str[k++] = hexDigits[byte0 >>> 4 & 0xf];
str[k++] = hexDigits[byte0 & 0xf];
}
return new String(str);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将cookie封装到Map里面
* @param request
* @return
*/
public static Map<String, Cookie> getCookieMap(HttpServletRequest request) {
Map<String, Cookie> cookieMap = new HashMap<String, Cookie>();
Cookie[] cookies = request.getCookies();
if (null != cookies) {
for (Cookie cookie : cookies) {
cookieMap.put(cookie.getName(), cookie);
}
}
return cookieMap;
}
/**
* 保留两位小数的double
* @param number
* @return
*/
public static String formatDouble(Double number) {
return new DecimalFormat("######0.00").format(number);
}
public static String formatDouble1(Double number) {
return new DecimalFormat("######0.0").format(number);
}
/**
* 从request 中获取字符串
*/
public static String getStringFrom(HttpServletRequest request) throws Exception {
InputStream in = request.getInputStream();
StringBuffer out = new StringBuffer();
byte[] b = new byte[1024];
for (int n; (n = in.read(b)) != -1; ) {
out.append(new String(b, 0, n));
}
return out.toString();
}
/**
* 随机生成 指定范围的小数 min :最小值范围 max:最大值范围
*/
public static BigDecimal getDecimalNum(int min, int max) {
Random random = new Random();
int s = random.nextInt(max) % (max - min + 1) + min;
String temp = "0." + s;
BigDecimal number = new BigDecimal(temp);
return number;
}
}
通过以上代码请求,最终可获取到支付跳转链接:mweb_url,mweb_url为拉起微信支付收银台的中间页面,可通过访问该url来拉起微信客户端,完成支付,mweb_url的有效期为5分钟。
具体更多API参数请参考微信支付文档:微信H5支付文档
上一篇: 将博客搬至CSDN
下一篇: PHP微信H5支付开发