最近再做微信支付--扫码支付的后台。总结一下吧,免得大家入坑,贴出的代码是可以直接运行的,并且没有问题,加密验证那一块都是一次性通过的,很幸运。
参考文档 http://blog.csdn.net/wangqiuyun/article/details/51241064。
在此感谢一下这个文档,代码没有问题,本人修改的也就是循环那一块。
首先说下,微信支付的一些常见知识点吧。
https://pay.weixin.qq.com/index.php/core/home/login?return_url=%2F 。 这个链接是商户平台,注意需要区分下,公众平台和开放平台 以及商户平台。
公众平台:公众平台支持的是公众号支付 模式,扫码支付 也在其中。
开放平台:开放平台支持的是移动端支付,也就是App支付,开发平台账号是需要给 APP人员使用的,当然,服务端人员需要用开放平台账号去生成订单的。
商户平台:当你申请了公众平台或者开放平台之后,微信会有一封邮件给你,里面有对应的商户平台信息。
本人公司申请了三个账户,一个公众平台,一个手机App开放平台账号,一个pad开放平台账号,这就对应了三个商户平台,(注意:每个账号都需要使用各自的商户平台账号去生成订单,也必须使用对应商户平台才能查询到各自的订单)
下面就说下扫码支付吧,流程不废话了,微信文档内就有UML图,这个自己去读,不难,这里使用模式二去开发,模式一太烦,不方便,一般公司都是使用模式二去开发,获取订单URL简单方便。
以下为一些常用配置项,根据自己去配置,appSecret暂时在支付时没有用到,应该是在分享时会用到,这一ApiKey这个配置项,这个配置项很重要,签名时需要额外添加的,详情见微信开发文档的签名算法,需要细读,不然会有很多问题。笔者很幸运没有碰到问题,也多谢了上面的参考文章。
WeiXinConfigUtils
/*微信分配的公众号ID*/
public static String appId = "11122223355";
/*ID Secret*/
public static String appSecret = "454878asdasda4545848sdr";
/*微信支付分配的商户号ID*/
public static String mchId = "4845754";
/*api加密时使用的key*/
public static String apiKey = "HNTYDJHFK4156787DSS";
/*扫码支付交易类型*/
public static String tradeNative = "NATIVE";
/*回调地址*/
public static String notifyUrl = "/weixinpay/wei-xin-notify.htm";
/*统一下单Url*/
public static String orderUrl = "https://api.mch.weixin.qq.com/pay/unifiedorder";
/*查询订单Url*/
public static String queryUrl = "https://api.mch.weixin.qq.com/pay/orderquery";
/*机器ip*/
public static String ip;
/*随机数位数*/
public static int randomNumLength = 5;
以下即为获取订单url的方法,各项参数需要自己去组织
public String getWeiXinPayUrl(String outTradeNo) throws Exception {
//公众账号ID
String appid = WeiXinConfigUtils.appId;
//商户号
String mch_id = WeiXinConfigUtils.mchId;
//加密key
String apiKey = WeiXinConfigUtils.apiKey;
//随机数,32位以内即可
String nonce_str = PayCommonUtil.getNonceStr();
//商品简单描述,可定义为商品名称
String body = "测试商品名";
//商户订单号,每次生成交易订单时需保证不一致
String out_trade_no = outTradeNo;
//商品总金额,单位为分
int total_fee = 1;
//终端IP
String spbill_create_ip = WeiXinConfigUtils.ip;
//回调地址
String notify_url = WeiXinConfigUtils.notifyUrl;
//交易类型
String trade_type = WeiXinConfigUtils.tradeNative;
//商品Id,交易类型为Native时必传
String product_id = "2222";
final Date date = new Date();
//订单生成时间
String time_start = HbdDateUtils.dateFormatCurrent2.format(date);
//订单失效时间 间隔最短要求5分钟
String time_expire = HbdDateUtils.dateFormatCurrent2.format(HbdDateUtils.getRelativeMinute(date,6));
//请求参数,需排序
SortedMap<Object,Object> packageParams = new TreeMap<Object,Object>();
packageParams.put("appid", appid);
packageParams.put("mch_id", 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);
packageParams.put("notify_url", notify_url);
packageParams.put("trade_type", trade_type);
packageParams.put("product_id", product_id);
packageParams.put("time_start", time_start);
packageParams.put("time_expire", time_expire);
//签名
String sign = PayCommonUtil.createSign(SysConst.DEFAULT_ENCODING,packageParams,apiKey);
packageParams.put("sign", sign);
//请求参数转换成XML
String requestXML = PayCommonUtil.getRequestXml(packageParams);
//返回信息
String resXml = HttpUtils.postData(WeiXinConfigUtils.orderUrl, requestXML);
//返回信息XML格式化为map
Map map = PayCommonUtil.doXMLParse(resXml);
这是工具类。要注意APIKey是需要增加到尾部去参与签名,详情参考微信签名算法。
public class PayCommonUtil {
private static String sign = "sign";
private static String key = "key";
private static String eg = "=";
private static String and = "&";
private static final String flagStart = "<![CDATA[";
private static final String flagEnd = "]]>";
private static final String xmlStart = "<xml>";
private static final String xmlEnd = "</xml>";
private static final String leftStartTag = "<";
private static final String leftEndTag = "</";
private static final String rightTag = ">";
private static final String returnCodeTagStart = "<return_code>";
private static final String returnCodeTagEnd = "</return_code>";
private static final String returnMsgStart = "<return_msg>";
private static final String returnMsgEnd = "</return_msg>";
private static final String successCode = "<![CDATA[SUCCESS]]>";
private static final String failCode = "<![CDATA[FAIL]]>";
/**
* 构建一个指定长度大小的随机数
* @param length
* @return
*/
public 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));
}
/**
* 获取随机字符串
* @return
* @throws Exception
*/
public static String getNonceStr() throws Exception{
String currentTime = HbdDateUtils.dateFormatCurrent.format(HbdDateUtils.now());
String strTime = currentTime.substring(8,currentTime.length());
//微信推荐随机数生成算法
String strRandom = buildRandom(WeiXinConfigUtils.randomNumLength) + "";
return strTime + strRandom;
}
/**
* 微信sign签名
* @param characterEncoding
* @param packageParams
* @param apiKey
* @return
*/
public static String createSign(String characterEncoding, SortedMap<Object, Object> packageParams, String apiKey) {
StringBuffer sb = new StringBuffer();
Set<Map.Entry<Object,Object>> es = packageParams.entrySet();
for(Map.Entry<Object,Object> entry : es){
String k = String.valueOf(entry.getKey());
String v = String.valueOf(entry.getValue());
//sign和key不参与签名计算
if (StringUtils.isNotBlank(v) && !sign.equals(k) && !key.equals(k)) {
//k + "=" + v + "&"
sb.append(k + eg + v + and);
}
}
sb.append(key + eg + apiKey);
String sign = MD5Util.MD5Encode(sb.toString(), characterEncoding).toUpperCase();
return sign;
}
/**
* 微信请求参数转换为xml格式
* @param parameters
* @return
*/
public static String getRequestXml(SortedMap<Object, Object> parameters) {
StringBuffer sb = new StringBuffer();
sb.append(xmlStart);
Set<Map.Entry<Object,Object>> es = parameters.entrySet();
for(Map.Entry<Object,Object> entry : es){
String k = String.valueOf(entry.getKey());
String v = String.valueOf(entry.getValue());
if ("attach".equalsIgnoreCase(k) || "body".equalsIgnoreCase(k) || "sign".equalsIgnoreCase(k)) {
// ("<" + k + ">" + "<![CDATA[" + v + "]]></" + k + ">");
sb.append(leftStartTag).append(k).append(rightTag)
.append(flagStart).append(v).append(flagEnd)
.append(leftEndTag).append(k).append(rightTag);
} else {
// ("<" + k + ">" + v + "</" + k + ">");
sb.append(leftStartTag).append(k).append(rightTag)
.append(v)
.append(leftEndTag).append(k).append(rightTag);
}
}
sb.append(xmlEnd);
return sb.toString();
}
/**
* 微信 签名正确,规则是:按参数名称a-z排序,遇到空值的参数不参加签名。
* @return boolean
*/
public static boolean isTenPaySign(String characterEncoding, SortedMap<Object, Object> packageParams, String apiKey) {
StringBuffer sb = new StringBuffer();
Set<Map.Entry<Object,Object>> es = packageParams.entrySet();
for(Map.Entry<Object,Object> entry : es){
String k = String.valueOf(entry.getKey());
String v = String.valueOf(entry.getValue());
//sign和key不参与签名计算
if(!sign.equals(k) && StringUtils.isNotBlank(v)) {
sb.append(k + eg + v + and);
}
}
sb.append(key + eg + apiKey);
//算出摘要
String mySign = MD5Util.MD5Encode(sb.toString(), characterEncoding).toLowerCase();
String tenPaySign = (String.valueOf(packageParams.get(sign))).toLowerCase();
return tenPaySign.equals(mySign);
}
/**
* 微信返回值 xml格式化为map
* @param strxml
* @return
* @throws JDOMException
* @throws IOException
*/
public static Map doXMLParse(String strxml) throws JDOMException, IOException {
strxml = strxml.replaceFirst("encoding=\".*\"", "encoding=\"UTF-8\"");
Map returnMap = Maps.newHashMap();
if(StringUtils.isBlank(strxml)){
return returnMap;
}
InputStream in = new ByteArrayInputStream(strxml.getBytes("UTF-8"));
SAXBuilder builder = new SAXBuilder();
Document doc = builder.build(in);
Element root = doc.getRootElement();
List list = root.getChildren();
for(Object s: list){
Element e = (Element)s;
String k = e.getName();
String v = "";
List children = e.getChildren();
if(children.isEmpty()){
v = e.getTextNormalize();
}else {
v = getChildrenText(children);
}
returnMap.put(k, v);
}
//关闭流
in.close();
return returnMap;
}
/**
* 获取子结点的xml
* @param children
* @return String
*/
private static String getChildrenText(List children) {
StringBuffer sb = new StringBuffer();
if(!children.isEmpty()) {
for (Object s : children){
Element e = (Element)s;
String k = e.getName();
String v = e.getTextNormalize();
List list = e.getChildren();
// ("<" + k + ">");
sb.append(leftStartTag).append(k).append(rightTag);
if(!list.isEmpty()){
sb.append(getChildrenText(list));
}
sb.append(v);
// ("</" + k + ">");
sb.append(leftEndTag).append(k).append(rightTag);
}
}
return sb.toString();
}
/**
* 微信判断订单金额和支付返回金额是否相同
* 避免假通知
* @param orderFee
* @param returnFee
* @return
*/
public static Boolean isMatchFee(Double orderFee, int returnFee){
//换算成分
Double pointsFee = orderFee * 100;
int pointsIntFee = pointsFee.intValue();
if(pointsIntFee == returnFee){
return true;
}else{
return false;
}
}
/**
* 微信回传值封装
*
* <xml>
* <return_code><![CDATA[SUCCESS]]></return_code>
* <return_msg><![CDATA[OK]]></return_msg>
* </xml>
* @param isSuccess 是否成功
* @param value 回传值
* @return
*/
public static String freezeReturnXml(Boolean isSuccess,String value){
StringBuilder msg = new StringBuilder();
if(isSuccess){
msg.append(xmlStart)
.append(returnCodeTagStart)
.append(successCode)
.append(returnCodeTagEnd)
.append(returnMsgStart)
.append(flagStart).append(value).append(flagEnd)
.append(returnMsgEnd)
.append(xmlEnd);
}else{
msg.append(xmlStart)
.append(returnCodeTagStart)
.append(failCode)
.append(returnCodeTagEnd)
.append(returnMsgStart)
.append(flagStart).append(value).append(flagEnd)
.append(returnMsgEnd)
.append(xmlEnd);
}
return msg.toString();
}
public static String getTimeStamp() {
return String.valueOf(System.currentTimeMillis() / 1000);
}
以下为回调接口。回调接口具体的内容自己去封装,大致的框架能直接拿来用。
首先判断 通信是否成功,然后才是真正的交易判断 resultCode为SUCCESS时才是交易成功,其他均为失败。需要修改本地订单一些信息。
@RequestMapping(value = "/wei-xin-notify", method = RequestMethod.GET)
public void weiXinNotify(HttpServletRequest request,HttpServletResponse response) throws Exception{
String returnValue = payBiz.getWeiXinReturnValue(request);
//解析xml成map
Map<String, String> returnValueMap = Maps.newHashMap();
returnValueMap = PayCommonUtil.doXMLParse(returnValue.toString());
//过滤空 设置 TreeMap
SortedMap<Object,Object> packageParams = new TreeMap<Object,Object>();
Set<Map.Entry<String,String>> value = returnValueMap.entrySet();
for(Map.Entry<String,String> entry : value){
if(null != entry.getValue()){
packageParams.put(entry.getKey(),entry.getValue().trim());
}
}
payBiz.handleOrderInfo(packageParams,WeiXinConfigUtils.apiKey,response);
}
具体逻辑封装在handOrderInfo中。这个锁有没有必要加暂时不清楚,不过微信回调文档是提示需要加上锁来避免脏读,但是本例中的锁加的不是很完美,会有一些意外情况,后期再修复,正常情况下不加锁其实也没有多大问题。
/**
* 处理微信支付回调后的业务逻辑
* @param packageParams
* @param apiKey
*/
public void handleOrderInfo(SortedMap<Object,Object> packageParams,String apiKey,HttpServletResponse response){
//返回给微信的通知
String resXml = "";
//判断此次通信是否成功
if(PayCommonUtil.isTenPaySign(SysConst.DEFAULT_ENCODING, packageParams, apiKey)){
PayInfo queryPayInfo = new PayInfo();
queryPayInfo.setPayOrderNumber(String.valueOf(packageParams.get(tradeNum)));
PayInfo payInfo = null;
OrderInfo orderInfo = null;
String redisKey = SysConst.WEI_XIN_PAY_LOCK_KEY + String.valueOf(packageParams.get(tradeNum));
//针对每个交易订单号进行加锁
if(redisBillLockSerivce.tryLock(redisKey,20, TimeUnit.SECONDS)){
try{
payInfo = this.getPayInfo(queryPayInfo);
orderInfo = this.getOrderInfo(payInfo);
}catch (Exception e){
//意外情况下全部直接return,不做任何处理
redisBillLockSerivce.unLock(redisKey);
return;
}
// result_code 为SUCCESS时才是支付成功
if(SUCCESS.equals(String.valueOf(packageParams.get(resultCode)))){
//判断是否能够修改当前系统订单信息
try{
resXml = this.handleExceptionTrade(payInfo, orderInfo, packageParams);
}catch (Exception e){
redisBillLockSerivce.unLock(redisKey);
return;
}
if(StringUtils.isBlank(resXml)){
try{
payInfoService.modifyPayAndOrderInfo(true,payInfo,orderInfo);
}catch (Exception e){
logger.error("交易成功--修改本地表失败" + e.getMessage(),e);
redisBillLockSerivce.unLock(redisKey);
return;
}
logger.info("更新--成功交易, 交易Id: "+ payInfo.getPayId());
resXml = PayCommonUtil.freezeReturnXml(true,ok);
}
}else {
logger.error("支付失败,错误信息:" + packageParams.get(errCode));
try{
payInfoService.modifyPayAndOrderInfo(false,payInfo,orderInfo);
}catch (Exception e){
logger.error("交易失败--修改本地表失败"+e.getMessage(),e);
redisBillLockSerivce.unLock(redisKey);
return;
}
logger.info("更新--失败交易, 交易Id: "+ payInfo.getPayId());
resXml = PayCommonUtil.freezeReturnXml(false, failed);
}
}else{
logger.error("获取锁失败");
return;
}
redisBillLockSerivce.unLock(redisKey);
sendResponse(response,resXml);
}else {
//当前回调请求视为无效,只记录log,等待后续回调过来
logger.error("通知签名验证失败: " + packageParams.get(returnMsg));
}
}
微信提示需要处理已处理的订单,还有金额的判断,可以参照官方文档,这边也是去判断了一下,避免一些非常情况。
/**
* 处理回传交易异常情况
* @param payInfo
* @param orderInfo
* @param packageParams
* @return
* @throws Exception
*/
private String handleExceptionTrade(PayInfo payInfo,OrderInfo orderInfo, SortedMap<Object,Object> packageParams) throws Exception{
String returnMsg = "";
try{
if(HbdEnum.PayInfoStatus.Normal.value() != payInfo.getPayStatus()){
returnMsg = PayCommonUtil.freezeReturnXml(false,handled);
logger.error("交易: " +payInfo.getPayOrderNumber()+"已经处理过");
return returnMsg;
}
if(!PayCommonUtil.isMatchFee(orderInfo.getOrderPrice(), Integer.parseInt(packageParams.get(tradeFee).toString()))){
returnMsg = PayCommonUtil.freezeReturnXml(false,unMatched);
logger.error("订单: " + orderInfo.getOrderId() + "金额不匹配 "+ "订单价格: "+ orderInfo.getOrderPrice()+ " 交易价格: "+(int) packageParams.get(tradeFee));
return returnMsg;
}
}catch (Exception e){
//此处避免程序处理错误导致returnMsg依然为空,进行Order 和PayInfo的修改动作,
//直接return等待下次微信请求。
logger.error("程序处理错误: "+ e.getMessage(),e);
throw new Exception();
}
return returnMsg;
}
Md5工具类
public class MD5Util {
private static String byteArrayToHexString(byte b[]) {
StringBuffer resultSb = new StringBuffer();
for (int i = 0; i < b.length; i++)
resultSb.append(byteToHexString(b[i]));
return resultSb.toString();
}
private static String byteToHexString(byte b) {
int n = b;
if (n < 0){
n += 256;
}
int d1 = n / 16;
int d2 = n % 16;
return hexDigits[d1] + hexDigits[d2];
}
public static String MD5Encode(String origin, String charsetName) {
String resultString = null;
try {
resultString = origin;
MessageDigest md = MessageDigest.getInstance("MD5");
if (charsetName == null || "".equals(charsetName))
resultString = byteArrayToHexString(md.digest(resultString
.getBytes()));
else
resultString = byteArrayToHexString(md.digest(resultString
.getBytes(charsetName)));
} catch (Exception exception) {
}
return resultString;
}
private static final String hexDigits[] = { "0", "1", "2", "3", "4", "5",
"6", "7", "8", "9", "a", "b", "c", "d", "e", "f" };
}
最后需要指明的是,一个交易订单号(out_trade_no)在微信那边只能生成一次,下一次再去生成时如果交易订单号不变的话,微信会报错,所以本地表的设计就有讲究了。
本人项目中有一个订单关联表,里面会生成一个随机交易订单号,每次都拿这个交易订单号去微信生成交易URL,这样即使重复的生成二维码Url,但是这些Url都指向同一个订单,但是每次的交易订单号都不一样,满足了微信的要求,这边需要特别注意下。