Table of Contents
对外提供请求查询订单入参:OrderQueryRequest
对外提供接收查询订单入参:OrderQueryRequest
一、引言
上篇文章中,讲解并且实现微信支付,既然微信下单成功了,那我们怎么知道用户有没有付款呢?
说到这里,小编先要明确一下,我们主要的是针对开发一个支付的SDK。SDK通俗来讲,把我们所编写的一个项目可以打包成一个jar包,其他项目进行依赖,就可以使用该jar包中所对应的方法。
那么这里微信首先会:异步回调通知商户系统 —— 商户系统调用SDK中异步回调处理的方法 —— 商户系统拿到SDK处理结果做相对应的处理。
二、内网穿透
在开始讲解之前,小编先给大家介绍一款开发调试神器 : Sunny-Ngrok 点击访问
内网穿透可能还有小伙伴们没有听说过,简单来说,通过内网穿透可以通过互联网来访问对应本机的某个项目。
这个也是小编实际在公司中也会所使用的,在一个新项目开发完毕之后,肯定还会遗留bug、或者一些细节部分处理不当等操作。 按照正常来说双方都是公司的小伙伴则可以使用内网IP来访问对应本机项目,那么双方如果没在同一个局域网呢?
小编公司需要和B公司进行接口联调,在完全一个新开发的项目,小编首先会通过内网穿透进行接口联调,这样能够快速定位问题以及解决,解决后也无需发布新的版本,大大提高效率。
因为微信支付异步回调通知,要求商户系统回调地址必须为直接可访问的url,不能携带参数。那么这个可访问的url代表的就是可通过互联网进行访问。
这个Sunny-Ngrok之所以推荐是因为,可免费提供固定域名、免费穿透(当然也有收费的)、跨平台。
教程地址:http://www.ngrok.cc/_book/general/open.html,只需要开通对应的隧道,下载客户端,按照教程启动就行了,小编就不多讲了。
三、支付异步回调处理
支付结果通知官方文档:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_7
小编来说说需要注意的几个点:
1、首先url可直接访问的,可参考上文内网穿透,或者直接使用公司或者个人域名也都行。
2、用户支付后,微信可能会多次进行通知商户系统,如果微信不能收到商户系统的正确返回信息,则判定本次通知失败,知道成功为止(在通知一直不成功的情况下,微信总共会发起多次通知,通知频率为(15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 总计 24h4m),但微信不保证通知最终一定能成功
3、如果下单成功,收到微信支付结果通知或者在订单状态不明的情况下,则需要商户系统调用订单查询API。
4、商户系统收到支付结果通知,一定要做签名验证,并校验返回的订单金额是否与商户侧的订单金额一致,以及检查数据对应业务状态。
5、需要严格按照示例返回参数给微信支付,在文档最下方微信有给出实例。
以上说的这五点,指的是商户系统所需要注意的事情。而我们本次所开发的SDK异步回调处理,只是仅仅完成签名验证,以及把相对应的参数返回出去,至于一些金额校验需要商户系统查询数据库订单信息来与SDK返回参数信息进行对比等等。
本文代码环节是接着前几篇文章接着来的,如有疑惑的小伙伴,可参考一下前几篇文章。
对接微信Native支付:点击查看
项目工具类:实战开发支付SDK —— 项目结构设计讲解(微信、支付宝)
本文中的所使用的工具类,可在上文友情链接中可以找到 👆👆👆👆👆👆👆
异步回调通知返回出参:PayResponse
/**
* @Auther: IT贱男
* @Date: 2019/12/19 11:44
* @Description: 返回支付结果信息(针对商户系统)
*/
@Data
public class PayResponse {
// 以下字段在用来接收微信统一下单接口
/**
* 扫码付模式二用来生成二维码
*/
private String codeUrl;
/**
* 支付平台
*/
private BestPayPlatformEnum payPlatformEnum;
// 以下字段在微信异步通知下返回
/**
* 订单金额
*/
private Double orderAmount;
/**
* 订单号 商户平台所生成
*/
private String orderId;
/**
* 第三方支付的流水号 微信支付订单号
*/
private String outTradeNo;
}
SDK API定义:BestPayService
/**
* @Auther: IT贱男
* @Date: 2019/12/19 10:55
* @Description: SDK API定义
*/
public interface BestPayService {
/**
* 异步回调通知
*
* @param asyncData
* @return
*/
PayResponse asyncNotify(String asyncData);
}
SDK API实现:BestPayServiceImpl
/**
* @Auther: IT贱男
* @Date: 2019/12/19 10:58
* @Description: 基本实现,本SDK需要接入 WeChat And Alipay
*/
public class BestPayServiceImpl implements BestPayService {
/**
* 微信支付配置
*/
private WxPayConfig wxPayConfig;
public void setWxPayConfig(WxPayConfig wxPayConfig) {
this.wxPayConfig = wxPayConfig;
}
@Override
public PayResponse asyncNotify(String asyncData) {
// xml 开头的是微信通知
if (asyncData.startsWith("<xml>")) {
WxPayServiceImpl wxPayService = new WxPayServiceImpl(wxPayConfig);
return wxPayService.asyncNotify(asyncData);
}
throw new RuntimeException("支付异步回调错误");
}
}
微信相关API实现:WxPayServiceImpl
/**
* @Auther: IT贱男
* @Date: 2019/12/19 10:58
* @Description: 微信相关API实现
*/
@Slf4j
public class WxPayServiceImpl extends BestPayServiceImpl {
private WxPayConfig wxPayConfig;
public WxPayServiceImpl(WxPayConfig wxPayConfig) {
this.wxPayConfig = wxPayConfig;
}
@Override
public PayResponse asyncNotify(String asyncData) {
// 签名认证
if (!WxPaySignature.verify(XmlUtil.toMap(asyncData), wxPayConfig.getMchKey())) {
log.error("[微信异步支付通知] 签名认证失败, response = {}", asyncData);
throw new RuntimeException("【微信异步支付通知】签名认证失败");
}
// 解析xml对象
WxPayAsyncResponse wxPayAsyncResponse = (WxPayAsyncResponse) XmlUtil.toObject(asyncData, WxPayAsyncResponse.class);
if (!wxPayAsyncResponse.getReturnCode().equals(WxPayConstants.SUCCESS)) {
log.error("[微信异步支付通知] , returnCode != SUCCESS ", wxPayAsyncResponse.getReturnMsg());
throw new RuntimeException("【微信异步支付通知】返回状态码错误");
}
if (!wxPayAsyncResponse.getResultCode().equals(WxPayConstants.SUCCESS)) {
log.error("[微信异步支付通知] , resultCode != SUCCESS ", wxPayAsyncResponse.getErrCodeDes());
throw new RuntimeException("【微信异步支付通知】返回状态码错误");
}
// TODO 少量直接写在方法里面,多则单独提取出来
PayResponse response = new PayResponse();
response.setPayPlatformEnum(BestPayPlatformEnum.WX);
response.setOrderAmount(MoneyUtil.FenToYuan(wxPayAsyncResponse.getTotalFee()));
response.setOrderId(wxPayAsyncResponse.getOutTradeNo());
response.setOutTradeNo(wxPayAsyncResponse.getTransactionId());
return response;
}
}
支付异步回调通知测试:
@Test
public void asyncNotify() {
// 微信支付配置
WxPayConfig config = new WxPayConfig();
config.setAppId("898fcb******");
config.setMchId("834******");
config.setMchKey("F6BCD4621D37******");
config.setNotifyUrl("http://www.baidu.com");
BestPayServiceImpl bestPayService = new BestPayServiceImpl();
bestPayService.setWxPayConfig(config);
// 微信支付异步通知,商户系统接收的是字符串类型的xml格式。
String str = "<xml>......</xml>";
PayResponse payResponse = bestPayService.asyncNotify(str);
System.out.println(payResponse.toString());
}
这里SDK只需做签名认证,把相关需要数据封装好,返回给商户系统,剩下的一些业务处理都交给商户系统自行决定。
四、订单状态查询
上文也有讲述到,在订单状态不明的情况下我们需要主动向微信发起订单状态查询接口,来获得订单状态。
订单状态查询官方文档:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_2
查询的方式支持商户订单号、或者微信订单号,返回参数一般我们只关心订单状态以及订单金额。
首先我们来定义订单的几种状态的表示:
订单状态枚举:OrderStatusEnum
/**
* @Auther: IT贱男
* @Date: 2019/12/19 11:47
* @Description: 订单状态
*/
@Getter
public enum OrderStatusEnum {
SUCCESS("支付成功"),
REFUND("转入退款"),
NOTPAY("未支付"),
CLOSED("已关闭"),
REVOKED("已撤销(付款码支付)"),
USERPAYING("用户支付中"),
PAYERROR("支付失败"),
UNKNOW("未知状态"),;
private String desc;
OrderStatusEnum(String desc) {
this.desc = desc;
}
/**
* 根据名称查找对应状态
*
* @param name
* @return
*/
public static OrderStatusEnum findByName(String name) {
for (OrderStatusEnum orderStatusEnum : OrderStatusEnum.values()) {
if (name.toLowerCase().equals(orderStatusEnum.name().toLowerCase())) {
return orderStatusEnum;
}
}
throw new RuntimeException("错误的微信支付状态");
}
}
订单查询请求入参:WxOrderQueryRequest
import lombok.Data;
import org.simpleframework.xml.Element;
/**
* @Auther: IT贱男
* @Date: 2019/12/20 16:04
* @Description: 支付订单查询
*/
@Data
public class WxOrderQueryRequest {
/**
* 公众账号ID
*/
@Element(name = "appid")
private String appid;
/**
* 商户号
*/
@Element(name = "mch_id")
private String mchId;
/**
* 微信订单号 二选一
*/
@Element(name = "transaction_id", required = false)
private String transactionId;
/**
* 商户订单号 二选一
*/
@Element(name = "out_trade_no", required = false)
private String outTradeNo;
/**
* 随机字符串
*/
@Element(name = "nonce_str")
private String nonceStr;
/**
* 签名
*/
@Element(name = "sign")
private String sign;
/**
* 签名类型 目前支持HMAC-SHA256和MD5,默认为MD5
*/
@Element(name = "sign_type", required = false)
private String signType;
}
订单查询返回入参:WxOrderQueryResponse
/**
* @Auther: IT贱男
* @Date: 2019/12/20 16:14
* @Description: 订单查询结果
*/
@Data
public class WxOrderQueryResponse {
/**
* 返回状态码 SUCCESS/FAIL
*/
@Element(name = "return_code")
private String returnCode;
/**
* 返回信息
*/
@Element(name = "return_msg", required = false)
private String returnMsg;
/**
* 以下字段在return_code为SUCCESS的时候有返回.
*/
@Element(name = "appid", required = false)
private String appid;
/**
* 商户号
*/
@Element(name = "mch_id", required = false)
private String mchId;
/**
* 随机字符串
*/
@Element(name = "nonce_str", required = false)
private String nonceStr;
/**
* 签名
*/
@Element(name = "sign", required = false)
private String sign;
/**
* 业务结果
*/
@Element(name = "result_code", required = false)
private String resultCode;
/**
* 错误代码
*/
@Element(name = "err_code", required = false)
private String errCode;
/**
* 错误代码描述
*/
@Element(name = "err_code_des", required = false)
private String errCodeDes;
/**
* 设备号
*/
@Element(name = "device_info", required = false)
private String deviceInfo;
/**
* 用户标识
*/
@Element(name = "openid", required = false)
private String openid;
/**
* 是否关注公众账号
*/
@Element(name = "is_subscribe", required = false)
private String isSubscribe;
/**
* 交易类型 JSAPI,NATIVE,APP,MICROPAY
*/
@Element(name = "trade_type", required = false)
private String tradeType;
/**
* 交易状态 SUCCESS —支付成功、REFUND—转入退款、NOTPAY—未支付、CLOSED—已关闭、REVOKED—已撤销(付款码支付)、USERPAYING--用户支付中(付款码支付)、PAYERROR--支付失败(其他原因,如银行返回失败)
*/
@Element(name = "trade_state", required = false)
private String tradeState;
/**
* 付款银行
*/
@Element(name = "bank_type", required = false)
private String bankType;
/**
* 标价金额
*/
@Element(name = "total_fee", required = false)
private Integer totalFee;
/**
* 应结订单金额
*/
@Element(name = "settlement_total_fee", required = false)
private String settlementTotalFee;
/**
* 标价币种
*/
@Element(name = "fee_type", required = false)
private String feeType;
/**
* 现金支付金额
*/
@Element(name = "cash_fee", required = false)
private String cashFee;
/**
* 现金支付币种
*/
@Element(name = "cash_fee_type", required = false)
private String cashFeeType;
/**
* 代金券金额
*/
@Element(name = "coupon_fee", required = false)
private String couponFee;
/**
* 代金券使用数量
*/
@Element(name = "coupon_count", required = false)
private String couponCount;
/**
* 微信支付订单号
*/
@Element(name = "transaction_id", required = false)
private String transactionId;
/**
* 商户订单号
*/
@Element(name = "out_trade_no", required = false)
private String outTradeNo;
/**
* 附加数据
*/
@Element(name = "attach", required = false)
private String attach;
/**
* 支付完成时间
*/
@Element(name = "time_end", required = false)
private String timeEnd;
/**
* 交易状态描述
*/
@Element(name = "trade_state_desc", required = false)
private String tradeStateDesc;
}
请求微信API定义:WxPayApi
/**
* @Auther: IT贱男
* @Date: 2019/12/19 15:12
* @Description: 微信API
*/
public interface WxPayApi {
/**
* 订单查询
*
* @param body
* @return
*/
@POST("/pay/orderquery")
Call<WxOrderQueryResponse> orderquery(@Body RequestBody body);
}
对外提供请求查询订单入参:OrderQueryRequest
/**
* @Auther: IT贱男
* @Date: 2019/12/23 09:32
* @Description: 查询订单参数(提供给商户系统使用)
*/
@Data
public class OrderQueryRequest {
/**
* 支付平台.
*/
private BestPayPlatformEnum platformEnum;
/**
* 订单号(orderId 和 outOrderId 二选一,两个都传以outOrderId为准)
*/
private String orderId;
/**
* 外部订单号(例如微信生成的)
*/
private String outOrderId;
}
对外提供接收查询订单入参:OrderQueryRequest
/**
* @Auther: IT贱男
* @Date: 2019/12/23 09:31
* @Description: 订单查询结果
*/
@Data
public class OrderQueryResponse {
/**
* 订单状态
*/
private OrderStatusEnum orderStatusEnum;
/**
* 第三方支付的流水号
*/
private String outTradeNo;
/**
* 附加内容,发起支付时传入
*/
private String attach;
/**
* 错误原因
*/
private String resultMsg;
/**
* 订单号
*/
private String orderId;
/**
* 订单金额
*/
private Double totalFee;
/**
* 交易类型
*/
private String tradeType;
/**
* 支付完成时间
*/
private String finishTime;
/**
* 交易状态描述
*/
private String tradeStateDesc;
}
SDK API定义:BestPayService
/**
* @Auther: IT贱男
* @Date: 2019/12/19 10:55
* @Description: 支付API定义
*/
public interface BestPayService {
/**
* 订单查询
*
* @param orderQueryRequest
* @return
*/
OrderQueryResponse query(OrderQueryRequest orderQueryRequest);
}
SDK API实现:BestPayServiceImpl
/**
* @Auther: IT贱男
* @Date: 2019/12/19 10:58
* @Description: 基本实现,本SDK需要接入 WeChat And Alipay
*/
public class BestPayServiceImpl implements BestPayService {
/**
* 微信支付配置
*/
private WxPayConfig wxPayConfig;
public void setWxPayConfig(WxPayConfig wxPayConfig) {
this.wxPayConfig = wxPayConfig;
}
@Override
public OrderQueryResponse query(OrderQueryRequest orderQueryRequest) {
if (orderQueryRequest.getPlatformEnum() == BestPayPlatformEnum.WX) {
WxPayServiceImpl wxPayService = new WxPayServiceImpl(wxPayConfig);
return wxPayService.query(orderQueryRequest);
}
throw new RuntimeException("查询订单支付平台错误");
}
}
微信相关API实现:WxPayServiceImpl
import lombok.extern.slf4j.Slf4j;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.RequestBody;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Call;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.simplexml.SimpleXmlConverterFactory;
import java.io.IOException;
/**
* @Auther: IT贱男
* @Date: 2019/12/19 10:58
* @Description: 微信相关API实现
*/
@Slf4j
public class WxPayServiceImpl extends BestPayServiceImpl {
private WxPayConfig wxPayConfig;
/**
* 初始化对象
*/
private Retrofit retrofit = new Retrofit.Builder()
.baseUrl(WxPayConstants.WXPAY_GATEWAY)
.addConverterFactory(SimpleXmlConverterFactory.create())
.client(new OkHttpClient.Builder()
.addInterceptor((new HttpLoggingInterceptor()
.setLevel(HttpLoggingInterceptor.Level.BODY)))
.build()
)
.build();
public WxPayServiceImpl(WxPayConfig wxPayConfig) {
this.wxPayConfig = wxPayConfig;
}
@Override
public OrderQueryResponse query(OrderQueryRequest orderQueryRequest) {
// 封装请求参数
WxOrderQueryRequest queryRequest = new WxOrderQueryRequest();
queryRequest.setAppid(wxPayConfig.getAppId());
queryRequest.setMchId(wxPayConfig.getMchId());
queryRequest.setTransactionId(orderQueryRequest.getOutOrderId());
queryRequest.setOutTradeNo(orderQueryRequest.getOrderId());
queryRequest.setNonceStr(RandomUtil.getRandomStr());
queryRequest.setSign(WxPaySignature.sing(MapUtil.buildMap(queryRequest), wxPayConfig.getMchKey()));
// 发起请求
RequestBody requestBody = RequestBody.create(MediaType.parse("application/xml; charset=utf-8"), XmlUtil.toString(queryRequest));
Call<WxOrderQueryResponse> orderquery = retrofit.create(WxPayApi.class).orderquery(requestBody);
Response<WxOrderQueryResponse> execute = null;
try {
execute = orderquery.execute();
} catch (IOException e) {
e.printStackTrace();
}
assert execute != null;
if (!execute.isSuccessful()) {
throw new RuntimeException("[微信订单查询] 发起支付, 网络异常");
}
// 获取结果
WxOrderQueryResponse queryResponse = execute.body();
if (!queryResponse.getReturnCode().equals(WxPayConstants.SUCCESS)) {
throw new RuntimeException("[微信订单查询] 查询失败,ReturnCode != SUCCESS 错误详情:" + queryResponse.getReturnMsg());
}
if (!queryResponse.getResultCode().equals(WxPayConstants.SUCCESS)) {
throw new RuntimeException("[微信订单查询] 查询失败,ResultCode != SUCCESS错误详情:" + queryResponse.getErrCodeDes());
}
// 封装返回结果
OrderQueryResponse response = new OrderQueryResponse();
response.setOrderStatusEnum(OrderStatusEnum.findByName(queryResponse.getTradeState()));
response.setOutTradeNo(queryResponse.getTransactionId());
response.setOrderId(queryResponse.getOutTradeNo());
response.setAttach(queryResponse.getAttach());
response.setResultMsg(queryResponse.getReturnMsg());
response.setTotalFee(MoneyUtil.FenToYuan(queryResponse.getTotalFee()));
response.setTradeType(queryResponse.getTradeType());
response.setTradeStateDesc(queryResponse.getTradeStateDesc());
response.setFinishTime(queryResponse.getTimeEnd());
return response;
}
}
订单查询测试:
@Test
public void query() {
// 微信支付配置
WxPayConfig config = new WxPayConfig();
config.setAppId("898fcb******");
config.setMchId("834******");
config.setMchKey("F6BCD4621D37******");
config.setNotifyUrl("http://www.baidu.com");
BestPayServiceImpl bestPayService = new BestPayServiceImpl();
bestPayService.setWxPayConfig(config);
OrderQueryRequest queryRequest = new OrderQueryRequest();
queryRequest.setPlatformEnum(BestPayPlatformEnum.WX);
queryRequest.setOrderId("11222333444555");
OrderQueryResponse query = bestPayService.query(queryRequest);
System.out.println(query.toString());
}