背景:近期公司的app要做一个支付的功能。因此研究了一下微信的支付文档。做完了这个功能。光看文档还是踩了狠多坑。总结一下自己完成这个功能的步骤:
主要是开发的步骤
一、微信支付文档介绍
1、微信支付开发者文档首页
https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/pages/index.shtml
简单介绍一下申请账户的流程:
首先要注册微信平台开发者账号,然后登录商户平台,注册商户商户账号
接入微信,开通商户平台里面的支付功能。需要企业银行账户认证。
关键在于拿到这几个参数:(经理们已经申请好了)
2、接口文档
https://pay.weixin.qq.com/wiki/doc/apiv3/index.shtml
有以下几种基础的支付功能,我们使用的是app支付
用户使用支付的整个流程:
步骤一 用户进入我们的APP,选择商品下单、确认购买,进入支付环节,下单,在我们的系统生成订单,然后还需要调用微信的接口(携带的参数包括一个回调的接口),在微信的平台生成一个订单,这个时候就需要后端提供一个在微信平台预下单的接口,app用户调用这个接口,接口去请求微信平台生成预下单信息并且返回预下单的参数。app拿到这些参数调起微信支付。
步骤二 app拿到预下单后返回的参数后,app调出微信支付,用户点击后发起支付操作,进入到微信界面,调起微信支付,出现确认支付界面
步骤三 用户确认收款方和金额,点击立即支付后出现输入密码界面,可选择零钱或银行卡支付
步骤四 输入正确密码后,支付完成,用户端微信出现支付详情页面。
这里支付完成,在步骤一中预下单请求微信平台的参数中会携带一个回调的地址,这里支付完会立刻调用这个接口,因此我们的平台还要提供一个回调的接口,这个接口不能有参数,直接通过流获取到微信回调带回来的支付结果的通知数据。根据这些数据更新我们平台的订单状态。
步骤五 回跳到商户APP中,商户APP根据支付结果个性化展示订单处理结果。
整体的流程就是这样的。
增加知识:
1、目前我们使用的是v2版本的老接口。请求微信接口携带的参数和回调获取的参数都是xml包裹的,需要工具类来进行解析。
新版的V3 的接口是将V2 的接口请求的流程封装起来了。
2、连接V3的接口点击这里:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay-1.shtml
二、上完整代码
1、添加依赖
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.7</version>
</dependency>
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.1</version>
</dependency>
<!-- 微信支付 -->
<dependency>
<groupId>com.github.wxpay</groupId>
<artifactId>wxpay-sdk</artifactId>
<version>0.0.3</version>
</dependency>
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>3.7.4.ALL</version>
</dependency>
<dependency>
<groupId>com.ehe</groupId>
<artifactId>ehe-pay-pojo</artifactId>
<version>2.2.2-V3-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.squareup.okhttp</groupId>
<artifactId>okhttp</artifactId>
<version>2.7.5</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.58</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
</dependency>
2、直接装微信支付的功能封装成在一个工具类中
预下单的接口:
2-1:contoller
/**
* 新增-在线下单
* @param onlineContributionAdd 新增DTO
* @return VO
*/
@ApiOperation(value = "app支付-在线下单", notes = "在线下单")
@PostMapping("/")
ResponseInfo<Map<String,String>> add(@ApiParam(value = "在线下单 新增DTO", required = true) @RequestBody @Validated OnlineContributionAdd onlineContributionAdd);
2-1:service
@Override
public synchronized ResponseInfo<Map<String,String>> add(OnlineContributionAdd onlineContributionAdd) {
//一大堆业务逻辑
//创建订单支付失效redis
// String key = RedisKeyDefinition.ORDER_PAY_STATUS_PERF + //onlineContribution.getOrderCode();
// redis 保存有效期 30天
//stringRedisTemplate.opsForValue().set(key, PayStatus.PAYING.getValue(), 7, TimeUnit.DAYS);
//这里才是下单的入口
Map<String,String> map= VxPayUtils.onlinePayOrder(onlineContribution.getOrderCode(),onlineContribution.getAmount(),onlineContribution.getSource());
//try {
// saveOnlineContributionFail(map,onlineContribution);
// }catch (Exception e){
// log.error("保存支付参数失败:{}",e.getMessage());
// }
return ResponseInfo.ofCreate(map);
}
2-2: 支付工具类
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.ehe.elder.config.WxPayConfig;
import com.ehe.elder.enumeration.ElderConstant;
import com.squareup.okhttp.*;
import lombok.extern.slf4j.Slf4j;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.math.BigDecimal;
import java.net.InetAddress;
import java.net.URLEncoder;
import java.net.UnknownHostException;
import java.util.*;
import java.util.concurrent.TimeUnit;
@Slf4j
public class VxPayUtils {
private static final MediaType CONTENT_TYPE = MediaType.parse("application/json");
/**
* 读超时时间(秒)
*/
private static final int READ_TIMEOUT = 30;
/**
* 写超时时间(秒)
*/
private static final int WRITE_TIMEOUT = 30;
/**
* 连接超时时间(秒)
*/
private static final int CONN_TIMEOUT = 30;
/**
* 重试次数
*/
private static final int MAX_RETRY = 3;
/** 预支付订单
* <xml>
* <appid>wx2421b1c4370ec43b</appid>
* <attach>支付测试</attach>
* <body>APP支付测试</body>
* <mch_id>10000100</mch_id>
* <nonce_str>1add1a30ac87aa2db72f57a2375d8fec</nonce_str>
* <notify_url>https://wxpay.wxutil.com/pub_v2/pay/notify.v2.php</notify_url>
* <out_trade_no>1415659990</out_trade_no>
* <spbill_create_ip>14.23.150.211</spbill_create_ip>
* <total_fee>1</total_fee>
* <trade_type>APP</trade_type>
* <sign>0CB01533B8C1EF103065174F50BCA001</sign>
* </xml>
*
* @param orderCode
* @param totalFee
* @return
*/
public static Map<String, String> onlinePayOrder(String orderCode, BigDecimal totalFee, String description) {
Map<String, String> reqMap = new HashMap<String, String>();
reqMap.put("appid", WxPayConfig.getAppID());
reqMap.put("mch_id", WxPayConfig.getMchID());
reqMap.put("nonce_str", getRandomString(32));
reqMap.put("body", description);
//商户系统内部的订单号,
reqMap.put("out_trade_no", orderCode);
//订单总金额,单位为分
reqMap.put("total_fee", changeToFen(totalFee.doubleValue()));
//用户端实际ip
reqMap.put("spbill_create_ip", getHostIp());
//通知地址
reqMap.put("notify_url", WxPayConfig.getNotifyUrl());
//交易类型
reqMap.put("trade_type", WxPayConfig.TRADE_TYPE.APP);
reqMap.put("sign", getSign(reqMap));
String reqStr = creatXml(reqMap);
System.out.println("请求参数:"+reqStr);
String retStr = postHtpps(WxPayConfig.getUnifiedorderURL(), reqStr);
Map<String,String> resultMap = getInfoByXml(retStr);
//将返回的xml转为map
String prepayId = resultMap.getOrDefault("prepay_id", "");
String return_code = resultMap.getOrDefault("return_code", "");
if(return_code != null && ElderConstant.SUCCESS.equals(return_code) && prepayId != null && !"".equals(prepayId)){
log.info(" prepayId [ {} ]",prepayId);
//APP端调起支付的参数列表-要返回给app端的支付参数
Map<String, String> paraMapApp = new HashMap<>();
//微信开放平台审核通过的应用APPID
paraMapApp.put("appid",WxPayConfig.getAppID());
// 微信支付分配的商户号
paraMapApp.put("partnerid",WxPayConfig.getMchID());
paraMapApp.put("prepayid",prepayId);
paraMapApp.put("package",WxPayConfig.getPackage());
paraMapApp.put("noncestr",getRandomString(32));
String timeStamp = String.valueOf(getSecondTimestamp(new Date()));
paraMapApp.put("timestamp",timeStamp);
String stringSignTempApp = formatUrlMap(paraMapApp,false,false);
stringSignTempApp = stringSignTempApp + "&key=" + WxPayConfig.getKey();
log.info("stringSignTempApp [ {} ]",stringSignTempApp);
//得到app支付签名
String signApp = MD5Utils.MD5Encoding(stringSignTempApp).toUpperCase();
paraMapApp.put("sign",signApp);
log.info("返回给app的参数 [ {} ]",paraMapApp);
return paraMapApp;
}else {
log.info("获取prepay_id失败 [ {} ]",resultMap.getOrDefault("return_msg",""));
}
return null;
}
/**
* 方法用途: 对所有传入参数按照字段名的Unicode码从小到大排序(字典序),并且生成url参数串<br>
* 实现步骤: <br>
* @param paraMap 要排序的Map对象
* @param urlEncode 是否需要URLENCODE
* @param keyToLower 是否需要将Key转换为全小写
* true:key转化成小写,false:不转化
* @return
*/
public static String formatUrlMap(Map<String, String> paraMap, boolean urlEncode, boolean keyToLower) {
String buff = "";
Map<String, String> tmpMap = paraMap;
try {
List<Map.Entry<String, String>> infoIds = new ArrayList<Map.Entry<String, String>>(tmpMap.entrySet());
// 对所有传入参数按照字段名的 ASCII 码从小到大排序(字典序)
Collections.sort(infoIds, new Comparator<Map.Entry<String, String>>() {
@Override
public int compare(Map.Entry<String, String> o1, Map.Entry<String, String> o2) {
return (o1.getKey()).toString().compareTo(o2.getKey());
}
});
// 构造URL 键值对的格式
StringBuilder buf = new StringBuilder();
for (Map.Entry<String, String> item : infoIds) {
if (StringUtils.isNotBlank(item.getKey())) {
String key = item.getKey();
String val = item.getValue();
if (urlEncode) {
val = URLEncoder.encode(val, "utf-8");
}
if (keyToLower) {
buf.append(key.toLowerCase() + "=" + val);
} else {
buf.append(key + "=" + val);
}
buf.append("&");
}
}
buff = buf.toString();
if (buff.isEmpty() == false) {
buff = buff.substring(0, buff.length() - 1);
}
} catch (Exception e) {
return null;
}
return buff;
}
/**
* 获取精确到秒的时间戳 10 位数
*
* @return
*/
public static int getSecondTimestamp(Date date) {
if (null == date) {
return 0;
}
String timestamp = String.valueOf(date.getTime());
int length = timestamp.length();
if (length > 3) {
return Integer.valueOf(timestamp.substring(0, length - 3));
} else {
return 0;
}
}
/**
* 关闭订单
* @param orderId 商户自己的订单号
* @return
*/
public static Map<String, String> closeOrder(String orderId){
Map<String, String> reqMap = new HashMap<String, String>();
reqMap.put("appid", WxPayConfig.getAppID());
reqMap.put("mch_id", WxPayConfig.getMchID());
reqMap.put("nonce_str", getRandomString(32));
reqMap.put("out_trade_no", orderId);
//商户系统内部的订单号,
reqMap.put("sign", getSign(reqMap));
String reqStr = creatXml(reqMap);
String retStr = postHtpps(WxPayConfig.getCloseOrderURL(), reqStr);
return getInfoByXml(retStr);
}
/**
* 查询订单
* @param orderId 商户自己的订单号
* @return
*/
public static Map<String,String> getOrder(String orderId){
Map<String, String> reqMap = new HashMap<String, String>();
reqMap.put("appid", WxPayConfig.getAppID());
reqMap.put("mch_id", WxPayConfig.getMchID());
reqMap.put("nonce_str", getRandomString(32));
//商户系统内部的订单号
reqMap.put("out_trade_no", orderId);
reqMap.put("sign", getSign(reqMap));
String reqStr = creatXml(reqMap);
String retStr = postHtpps(WxPayConfig.getOrderQueryURL(), reqStr);
return getInfoByXml(retStr);
}
/**
* 退款:需要商行平台的证书
* @param orderId 商户订单号
* @param refundId 退款单号
* @param totralFee 总金额(分)
* @param refundFee 退款金额(分)
* @param opUserId 操作员ID
* @return
*/
public static Map<String, String> refundWei(String orderId,String refundId,String totralFee,String refundFee,String opUserId){
Map<String, String> reqMap = new HashMap<String, String>();
reqMap.put("appid", WxPayConfig.getAppID());
reqMap.put("mch_id", WxPayConfig.getMchID());
reqMap.put("nonce_str", getRandomString(32));
//商户系统内部的订单号
reqMap.put("out_trade_no", orderId);
//商户退款单号
reqMap.put("out_refund_no", refundId);
//总金额
reqMap.put("total_fee", totralFee);
//退款金额
reqMap.put("refund_fee", refundFee);
//操作员
reqMap.put("op_user_id", opUserId);
reqMap.put("sign", getSign(reqMap));
String requestParam = creatXml(reqMap);
String retStr = "";
try{
//retStr = postHttplientNeedSSL(WxPayConfig.getOrderRefundURL(), requestParam, WxPayConfig.refund_file_path, orderRefundURL.MchId);
}catch(Exception e){
e.printStackTrace();
return null;
}
return getInfoByXml(retStr);
}
/**
* 退款查询
* @param refundId 退款单号
* @return
*/
public static Map<String, String> getRefundWeiInfo(String refundId){
Map<String, String> reqMap = new HashMap<String, String>();
reqMap.put("appid", WxPayConfig.getAppID());
reqMap.put("mch_id", WxPayConfig.getMchID());
reqMap.put("nonce_str", getRandomString(32));
reqMap.put("out_refund_no", refundId); //商户退款单号
reqMap.put("sign", getSign(reqMap));
String reqStr = creatXml(reqMap);
String retStr = postHtpps(WxPayConfig.getOrderRefundQueryURL(), reqStr);
return getInfoByXml(retStr);
}
/**
* 传入map 生成头为XML的xml字符串,例:<xml><key>123</key></xml>
* @param requestParamMap
* @return
*/
public static String creatXml(Map<String, String> requestParamMap){
Set<String> set = requestParamMap.keySet();
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
stringBuffer.append("<xml>");
for(String key : set){
stringBuffer.append("<"+key+">").append(requestParamMap.get(key)).append("</"+key+">");
}
stringBuffer.append("</xml>");
return stringBuffer.toString();
}
/**
* 得到加密值
* @param map
* @return
*/
public static String getSign(Map<String, String> map){
String[] keys = map.keySet().toArray(new String[0]);
Arrays.sort(keys);
StringBuffer reqStr = new StringBuffer();
for(String key : keys){
String v = map.get(key);
if(v != null && ! ElderConstant.NULL.equals(v)){
reqStr.append(key).append("=").append(v).append("&");
}
}
reqStr.append("key").append("=").append(WxPayConfig.getKey());
return WeiMd5.encode(reqStr.toString()).toUpperCase();
}
/**
* 得到10 位的时间戳
* 如果在JAVA上转换为时间要在后面补上三个0
* @return
*/
public static String getTenTimes(){
String t = System.currentTimeMillis()+"";
t = t.substring(0, t.length()-3);
return t;
}
/**
* 得到随机字符串
* @return
*/
public static String getRandomString(int length){
length = 32;
String str = "jklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
Random random = new Random();
StringBuffer sb = new StringBuffer();
for(int i = 0; i < length; ++i){
int number = random.nextInt(62);//[0,62)
sb.append(str.charAt(number));
}
return sb.toString();
}
/**
* 得到本地机器的IP
* @return
*/
private static String getHostIp(){
String ip = "";
try{
ip = InetAddress.getLocalHost().getHostAddress();
}catch(UnknownHostException e){
log.error("pay:{}",e.getMessage());
e.printStackTrace();
}
return ip;
}
public static Map<String, String> getInfoByXml(String xmlStr){
try{
Map<String, String> m = new HashMap<String, String>();
Document d = DocumentHelper.parseText(xmlStr);
Element root = d.getRootElement();
for ( Iterator<?> i = root.elementIterator(); i.hasNext(); ) {
Element element = (Element) i.next();
String name = element.getName();
if(!element.isTextOnly()){
//不是字符串 跳过。确定了微信放回的xml只有根目录
continue;
}else{
m.put(name, element.getTextTrim());
}
}
//对返回结果做校验.去除sign 字段再去加密
String retSign = m.get("sign");
m.remove("sign");
String rightSing = getSign(m);
if(rightSing.equals(retSign)){
return m;
}
}catch(DocumentException e){
e.printStackTrace();
}
return null;
}
/**
* 将金额转换成分
* @param fee 元格式的
* @return 分
*/
public static String changeToFen(Double fee){
String priceStr = "";
if(fee != null){
//价格变为分
int p = (int)(fee * 100);
priceStr = Integer.toString(p);
}
return priceStr;
}
public static String postHtpps(String url,String param){
Request request = new Request.Builder()
.url(url)
.post(RequestBody.create(CONTENT_TYPE, param))
.build();
Response response = null;
try {
response = getHttpClient().newCall(request).execute();
} catch (IOException e) {
e.printStackTrace();
}
String result = null;
try {
result = response.body().string();
log.info("http请求公共方法接收返回结果:"+result);
} catch (IOException e) {
e.printStackTrace();
}
Integer code = response.code();
if (code != 200) {
//接入帐号异常
throw new RuntimeException(response.message());
}
return result;
}
/**
* 获取 http client
*
* @return
*/
private static OkHttpClient getHttpClient() {
OkHttpClient client = new OkHttpClient();
client.setConnectTimeout(CONN_TIMEOUT, TimeUnit.SECONDS);
client.setReadTimeout(READ_TIMEOUT, TimeUnit.SECONDS);
client.setWriteTimeout(WRITE_TIMEOUT, TimeUnit.SECONDS);
return client;
}
}
2-3:配置类
import lombok.Data;
import org.springframework.context.annotation.Configuration;
import java.io.InputStream;
@Data
@Configuration
public class WxPayConfig{
/**
* 公众账号ID
*/
private static String appid = "-------------------------------";
/**
* 商户号
*/
private static String mchid = "-------------------------------";
/**
* 合作key
*/
private static String key = "----------------------------------";
/**
* 支付回调地址-本地环境地址
*/
private static String notifyURL_local = "http://111.111.111:8080/api/dlele/online/notify";
/**
* 支付回调地址-测试环境地址
*/
private static String notifyURL_dev = "http://111.111.111:8080/api/notify";
/**
* 支付回调地址-正式环境地址
*/
private static String notifyURL_prod = "http://111.111.111:8080/der/onion/notify";
private static String notifySuccess = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>" + "<return_msg><![CDATA[OK]]></return_msg>" + "</xml> ";
private static String notifyFailed = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>" + "<return_msg><![CDATA[报文为空]]></return_msg>" + "</xml> ";
/**
* 证书绝对路径
*/
private static String certPath;
/**
* 证书密码
*/
private static String certPassword;
/**
* 统一下单接口链接
*/
private static String unifiedorderURL = "https://api.mch.weixin.qq.com/pay/unifiedorder";
/**
* 企业付款接口链接
*/
private static String enterprisePayURL = "https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers";
/**
* 单笔订单查询接口
*/
private static String orderQueryURL = "https://api.mch.weixin.qq.com/pay/orderquery";
/**
* 申请退款接口
*/
private static String orderRefundURL = "https://api.mch.weixin.qq.com/secapi/pay/refund";
/**
* 退款查询接口
*/
private static String orderRefundQueryURL = "https://api.mch.weixin.qq.com/pay/refundquery";
/**
* 关闭订单接口
*/
private static String closeOrderURL = "https://api.mch.weixin.qq.com/pay/closeorder";
/**
* 转账接口
*/
private static String transfersURL = "https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers";
/**
* 转账查询接口
*/
private static String transfersQueryURL = "https://api.mch.weixin.qq.com/mmpaymkttransfers/gettransferinfo";
/**
* HTTP(S) 连接超时时间,单位毫秒
*/
private int httpConnectTimeoutMs = 8000;
/**
* HTTP(S) 读数据超时时间,单位毫秒
*/
private int httpReadTimeoutMs = 10000;
private static String PACKAGE = "Sign=WXPay";
public static String getPackage(){
return PACKAGE;
}
public static String getOrderRefundQueryURL(){
return orderRefundQueryURL;
}
public static String getNotifyUrl(){
return notifyURL_local;
}
public static String getOrderQueryURL(){
return orderQueryURL;
}
public static String getUnifiedorderURL() {
return unifiedorderURL;
}
public static String getCloseOrderURL() {
return closeOrderURL;
}
public static String getOrderRefundURL(){
return orderRefundURL;
}
public static String getAppID() {
return appid = appid;
}
public static String getMchID() {
return mchid = mchid;
}
public static String getKey() {
return key = key;
}
public InputStream getCertStream() {
InputStream certStream =getClass().getClassLoader().getResourceAsStream(certPath);
return certStream;
}
public int getHttpConnectTimeoutMs() {
return httpConnectTimeoutMs;
}
public int getHttpReadTimeoutMs() {
return httpReadTimeoutMs;
}
public interface TRADE_TYPE {
String APP = "APP";
}
}
2-4:md5加密工具
import java.security.MessageDigest;
public class MD5Utils {
private static final char HEX_DIGITS[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
/*
* 先转为utf-8
* */
public static String MD5Encoding(String s) {
byte[] btInput = null;
try {
btInput = s.getBytes("UTF-8");
}catch (Exception e){
}
return MD5(btInput, 32);
}
public static String MD5(String s) {
byte[] btInput = s.getBytes();
return MD5(btInput, 32);
}
public static String MD5_16(String str) {
byte[] btInput = str.getBytes();
return MD5(btInput, 16);
}
private static String MD5(byte[] btInput, int length) {
try {
// 获得MD5摘要算法的 MessageDigest 对象
MessageDigest mdInst = MessageDigest.getInstance("MD5");
// 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 (byte byte0 : md) {
str[k++] = HEX_DIGITS[byte0 >>> 4 & 0xf];
str[k++] = HEX_DIGITS[byte0 & 0xf];
}
String result = new String(str);
return length == 16 ? result.substring(8, 24) : result;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class WeiMd5 {
// 全局数组
private final static String[] STR_DIGITS = { "0", "1", "2", "3", "4", "5",
"6", "7", "8", "9", "a", "b", "c", "d", "e", "f" };
// 返回形式为数字跟字符串
private static String byteToArrayString(byte bByte) {
int iRet = bByte;
// System.out.println("iRet="+iRet);
if (iRet < 0) {
iRet += 256;
}
int iD1 = iRet / 16;
int iD2 = iRet % 16;
return STR_DIGITS[iD1] + STR_DIGITS[iD2];
}
// 转换字节数组为16进制字串
private static String byteToString(byte[] bByte) {
StringBuffer sBuffer = new StringBuffer();
for (int i = 0; i < bByte.length; i++) {
sBuffer.append(byteToArrayString(bByte[i]));
}
return sBuffer.toString();
}
public static String encode(String strObj) {
String resultString = null;
try {
resultString = new String(strObj);
MessageDigest md = MessageDigest.getInstance("MD5");
// md.digest() 该函数返回值为存放哈希值结果的byte数组
try{
resultString = byteToString(md.digest(strObj.getBytes("UTF-8")));
}catch(UnsupportedEncodingException e){
// TODO Auto-generated catch block
e.printStackTrace();
}
} catch (NoSuchAlgorithmException ex) {
ex.printStackTrace();
}
return resultString;
}
3、查询订单的接口(不需要证书)
3-1:controller
/**
* orderCode 查询
* @param orderCode
* @return VO
*/
@ApiOperation(value = "根据订单号查询微信平台订单详情", notes = "orderCode 查询")
@GetMapping("/findOrderStatus/{orderCode}")
Map<String,String> findOrderStatus(@ApiParam(value = "orderCode", required = true) @PathVariable String orderCode);
3-2:service:
/**
* 根据订单号获取微信端订单状态
* @param orderCode
* @return
*/
@Override
public Map<String,String> findOrderStatus(String orderCode) {
return VxPayUtils.getOrder(orderCode);
}
3-3:返回的参数
返回支付成功的数据
{
"transaction_id": "4200001136202108121973910876",
"nonce_str": "vezB3i2l2WKStdbS",
"trade_state": "SUCCESS",
"bank_type": "OTHERS",
"openid": "oj7EI66VJi7gXabPQ3Rby91S5SdQ",
"return_msg": "OK",
"fee_type": "CNY",
"mch_id": "1609852809",
"cash_fee": "1",
"out_trade_no": "2021081200000000000000000006",
"cash_fee_type": "CNY",
"appid": "wx2e4196f713eac31a",
"total_fee": "1",
"trade_state_desc": "支付成功",
"trade_type": "APP",
"result_code": "SUCCESS",
"attach": "",
"time_end": "20210812154656",
"is_subscribe": "N",
"return_code": "SUCCESS"
}
未支付的订单查询的数据
{
"nonce_str": "2WvDtB7ckmXjQ6a6",
"device_info": "",
"trade_state": "NOTPAY",
"out_trade_no": "2021081200000000000000000011",
"appid": "wx2e4196f713eac31a",
"total_fee": "1",
"trade_state_desc": "订单未支付",
"return_msg": "OK",
"result_code": "SUCCESS",
"mch_id": "1609852809",
"return_code": "SUCCESS"
}
订单不存在返回的数据
{
"nonce_str": "yfyjuq7eyAECIfSL",
"appid": "wx2e4196f713eac31a",
"err_code": "ORDERNOTEXIST",
"return_msg": "OK",
"result_code": "FAIL",
"err_code_des": "订单不存在",
"mch_id": "1609852809",
"return_code": "SUCCESS"
}
(我们用的是jdk11)
4、支付回调的接口
4-1:controller
/**
* 微信支付回调地址
* @return 在线捐款 VO
*/
@ApiOperation(value = "微信支付回调地址", notes = "微信支付回调地址")
@PostMapping(value = "/notify" ,produces = "application/json;charset=UTF-8")
String add(HttpServletRequest request) throws Exception;
4-2:service
/**
* 交易成功判断条件:return_code和result_code都为SUCCESS且trade_type为MICROPAY(二维码支付)
* @param request
* @return
*/
@Override
public synchronized String add(HttpServletRequest request) throws Exception {
log.info("====微信回调===========日期:{}", TimeUtils.ldtToStandardString(LocalDateTime.now()));
try {
//读取参数
BufferedReader reader = request.getReader();
String line = "";
StringBuffer inputString = new StringBuffer();
while ((line = reader.readLine()) != null) {
inputString.append(line);
}
reader.close();
//解析xml成map
Map<String,String> map= WXPayUtil.xmlToMap(inputString.toString());
//判断签名是否正确
Boolean isSignTrue = WXPayUtil.isSignatureValid(map, WxPayConfig.getKey());
if (!isSignTrue){
log.error("回调验证失败,签名结果:{},时间:{}",(isSignTrue == true ? "验签成功" : "验签失败"),TimeUtils.ldtToStandardString(LocalDateTime.now()));
return "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>" + "<return_msg><![CDATA[支付回调验签失败]]></return_msg>" + "</xml>";
}
//校验回调结果code
if(!ElderConstant.SUCCESS.equals(map.get(ElderConstant.WX_NOTICEFY.RETURN_CODE)) || !ElderConstant.SUCCESS.equals(map.get(ElderConstant.WX_NOTICEFY.RESULT_CODE))){
log.error("回调验证失败,结果:return_code:{},result_code:{}",map.get(ElderConstant.WX_NOTICEFY.RETURN_CODE),map.get(ElderConstant.WX_NOTICEFY.RESULT_CODE));
return "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>" + "<return_msg><![CDATA[支付回调结果失败,回调参数fail]]></return_msg>" + "</xml>";
}
String outTradeNo=map.get(ElderConstant.WX_NOTICEFY.OUT_TRADE_NO);
if (StringUtils.isEmpty(outTradeNo)){
log.error("回调验证失败,结果:{}","订单号为空");
return "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>" + "<return_msg><![CDATA[订单号为空]]></return_msg>" + "</xml>";
}
OnlineContribution onlineContribution = onlineContributionService.selectOne(this.onlineContributionService.createQueryWrapper().lambda().eq(OnlineContribution::getOrderCode,outTradeNo));
if (onlineContribution == null || Objects.equals((onlineContribution.getAmount().multiply(new BigDecimal(100))).toString(),map.get("total_fee"))){
log.error("回调验证失败(注意检查订单数据是否安全),结果:系统支付金额:{},回调返回金额:{}",onlineContribution.getAmount().toString(),map.get(ElderConstant.WX_NOTICEFY.RESULT_CODE));
return "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>" + "<return_msg><![CDATA[支付回调结果失败,下单金额与回调金额不一致]]></return_msg>" + "</xml>";
}
//处理业务开始-账单的金额要和支付回调的金额保持一致
//保存微信支付回调记录数据
saveOnlineContributionNotify(map,isSignTrue);
//返回之前异步请求一次微信订单,查询订单状态并更新支付结果
//Map<String,String> mapAsyncCheckStatus = new HashMap<>(5);
//mapAsyncCheckStatus.put("orderCode",onlineContribution.getOrderCode());
//stringRedisTemplate.convertAndSend(RedisKeyConstant.appendElderPrefix(RedisKeyConstant.REDIS_LISTENER_TOPIC_PAY),new Gson().toJson(map));
}catch (Exception e){
log.error("微信支付回调失败:{}"+e.getMessage());
return "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>" + "<return_msg><![CDATA[回调异常]]></return_msg>" + "</xml> ";
}
log.info("微信支付回调成功:时间{}",TimeUtils.ldtToStandardString(LocalDateTime.now()));
return "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>" + "<return_msg><![CDATA[OK]]></return_msg>" + "</xml>";
}
三、定时任务刷新订单状态,防止订单支付后回调失败导致状态没有及时更新
定时任务看用的啥定时任务了。我们直接用的spring提供的 的schedule。
四、V3版本接口
https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient