最近项目中支付部分涉及到微信支付,使用的是h5支付,官方文档中是没有demo的,所以摸着石头过河,将踩过的坑记录如下。
一 应用场景H5支付是指商户在微信客户端外的移动端网页展示商品或服务,用户在前述页面确认使用微信支付时,商户发起本服务呼起微信客户端进行支付。 主要用于触屏版的手机浏览器请求微信支付的场景。可以方便的从外部浏览器唤起微信支付。有关于h5支付接口详细相关内容可以参考官方文档微信H5支付,本文主要记录实现代码及遇到的问题。
二 服务商模式介绍服务商模式,适用于帮助其他商户接入微信支付的开发公司,该公司在微信开通的商户称为服务商。服务商在商户平台的服务商配置里,可以帮助商户提交公司或者个体户的资料,申请通过后可以获得特约商户号和密码,这个特约商户也可以称为是子商户,服务商可以对其下的子商户的功能进行配置。这样在一个服务商下面,可以实现对不同的客户提供微信支付服务,资金直接进入子商户的微信账户中(或银行对公户中)。
三 实现代码 1.调用微信统一下单接口
public class WxPay {
private static final Logger logger = Logger.getLogger(WxPay.class);
public Map<String,String> wxPay(HttpServletRequest request){
// 账号信息
String appid = "";
// 公众账号ID
String mch_id = "";
// 商户号
String sub_mch_id = "";
// 子商户号
String key = ""; // 微信密钥key
String nonce_str = creRandStr(); // 随机字符串
String body = "1";
// 商品描述
String out_trade_no = "wxTest2"; // 商户订单号
String total_fee = "1";
// 总金额,单位为分
String spbill_create_ip = getRealIp(request); // 终端IP
String notify_url = "";
// 回调通知地址
String trade_type = "MWEB"; // 交易类型,H5支付的交易类型为MWEB
String scene_info = "{\"h5_info\": {\"type\":\"Wap\",\"wap_url\": \"https://pay.qq.com\",\"wap_name\": \"腾讯充值\"}}"; // 场景信息,可参考官方文档
Map<String,String> packageParams = new HashMap<>();
packageParams.put("appid",appid);
// 公众账号ID
packageParams.put("mch_id",mch_id);
// 商户号
packageParams.put("sub_mch_id",sub_mch_id);
// 子商户号
packageParams.put("nonce_str",nonce_str);
// 随机字符串
packageParams.put("body",body);
// 商品描述
packageParams.put("out_trade_no",out_trade_no);
// 商户订单号
packageParams.put("total_fee",total_fee);
// 总金额,单位为分
packageParams.put("spbill_create_ip",spbill_create_ip); // 终端IP
packageParams.put("notify_url",notify_url);
// 通知地址
packageParams.put("trade_type",trade_type);
// 交易类型,H5支付的交易类型为MWEB
packageParams.put("scene_info",scene_info);
// 场景信息
String sign = createSign(packageParams,key);
// 签名
packageParams.put("sign",sign);
String xml = getRequestXml(packageParams);
logger.info("xml-->" + xml);
//请求微信统一下单接口,成功后返回预支付交易会话标识prepay_id
String createOrderURL = "https://api.mch.weixin.qq.com/pay/unifiedorder";
String result = getPayNo(createOrderURL,xml); //返回结果,String
Map<String,String> result = doXMLParse(result); //将返回结果解析封装到Map中
return resultMap;
}
public static String getPayNo(String url,String xmlParam){
String prepay_id = "";
String jsonStr = null;
try {
Map<String, String> querys = new HashMap<String, String>();
Map<String, String> headers = new HashMap<String, String>();
HttpResponse response = HttpUtils.doPost(url,"","POST",headers,querys,xmlParam);
jsonStr = EntityUtils.toString(response.getEntity(), "UTF-8");
Map<String, Object> dataMap = new HashMap<String, Object>();
System.out.println("json是:"+jsonStr);
Map map = doXMLParse(jsonStr);
String return_code = (String) map.get("return_code");
prepay_id = (String) map.get("prepay_id");
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return jsonStr;
}
/**
* 解析xml,返回第一级元素键值对。如果第一级元素有子节点,则此节点的值是子节点的xml数据。
* @param strxml
* @return
* @throws JDOMException
* @throws IOException
*/
private static Map doXMLParse(String strxml) throws JDOMException,IOException {
if(null == strxml || "".equals(strxml)) {
return null;
}
Map m = new HashMap();
InputStream in = new ByteArrayInputStream(strxml.getBytes());
SAXBuilder builder = new SAXBuilder();
Document 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();
return m;
}
/**
* 获取子结点的xml
* @param children
* @return String
*/
private 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();
}
/**
* 将请求参数转换为xml格式的string
* @param parameters 请求参数
* @return
*/
private static String getRequestXml(Map<String, String> 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 (null != v && !"".equals(v)) {
sb.append("<" + k + ">" + v + "</" + k + ">");
}
}
sb.append("</xml>");
return sb.toString();
}
/**
* 创建md5摘要,规则是:按参数名称a-z排序,遇到空值的参数不参加签名。
*/
private String createSign(Map<String, String> packageParams ,String partnerkey) {
StringBuffer sb = new StringBuffer();
List<String> keys = new ArrayList<String>(packageParams.keySet());
Collections.sort(keys);
for (int i = 0; i < keys.size(); i++) {
String key = keys.get(i);
String value = packageParams.get(key);
if (null != value && !"".equals(value) && !"sign".equals(key)
&& !"key".equals(key)) {
sb.append(key + "=" + value + "&");
}
}
/*Set es = packageParams.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 (null != v && !"".equals(v) && !"sign".equals(k)
&& !"key".equals(k)) {
sb.append(k + "=" + v + "&");
}
}*/
sb.append("key=" + partnerkey);
System.out.println("md5 sb:" + sb+",key=" + partnerkey);
String sign = MD5Util.MD5Encode(sb.toString(), "utf-8").toUpperCase();
System.out.println("packge签名:" + sign);
return sign;
}
/**
* 产生随机字符串
* @return
*/
private String creRandStr(){
String currTime = getCurrTime();//获取当前时间
String strTime = currTime.substring(8, currTime.length());//8位日期
String strRandom = buildRandom(4) + "";//四位随机数
String strReq = strTime + strRandom;//10位序列号,可以自行调整
return strReq;
}
/**
* 获取当前时间 yyyyMMddHHmmss
* @return String
*/
public static String getCurrTime() {
Date now = new Date();
SimpleDateFormat outFormat = new SimpleDateFormat("yyyyMMddHHmmss");
String s = outFormat.format(now);
return s;
}
/**
* 取出一个指定长度大小的随机正整数.
* @param length int 设定所取出随机数的长度。length小于11
* @return int 返回生成的随机数。
*/
private static int buildRandom(int length) {
int num = 1;
double random = Math.random();
if (random < 0.1) {
random = random + 0.1;
}
for (int i = 0; i < length; i++) {
num = num * 10;
}
return (int) ((random * num));
}
/**
* 获取客户端的真实ip
* @param request
* @return
*/
private String getRealIp(HttpServletRequest request){
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
/*ip = request.getHeader("HTTP_CLIENT_IP");
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
ip = request.getHeader("X-Real-IP");*/
logger.info(ip);
return ip.equals("0:0:0:0:0:0:0:1")?"127.0.0.1":ip;
//
return "192.168.1.1";
}
}