第三方通過微信APP支付流程
本章文獻基本都來源於微信支付平臺,詳情請看微信官方文件:APP支付
系統互動圖
文件位置:https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=8_3APP支付-業務流程
根據文件內容,服務端只要做好獲取 prepay_id 和 sign 傳送給客戶端,並做好回撥接收處理就行
服務端demo
APP支付文件裡面的demo,主要是供客戶端使用的,對後臺來說,基本沒什麼用。
不過,依舊有我們後端可以直接拿來使用的demo:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=11_1
這個demo主要用於付款碼/掃碼/H5支付使用,但裡面提供了很多方便的工具類。
這裡我們引用demo裡面的工具類並繼承WXPayConfig和實現IWXPayDomain的抽象介面。
-
public class WxPayConfigImpl extends WXPayConfig{
-
// 設定應用Id
-
public static String appid = "2019102168481752";
-
// 設定商戶號
-
public static String mch_id = "1230000109";
-
// 設定裝置號(終端裝置號(門店號或收銀裝置ID),預設請傳"WEB",非必需)
-
public static String device_info = "WEB";
-
// 設定字符集
-
public static String key = "192006250b4c09247ec02edce69f6a2d";
-
private static WxPayConfigImpl INSTANCE;
-
public static WxPayConfigImpl getInstance() throws Exception {
-
if (INSTANCE == null) {
-
synchronized (WxPayConfigImpl.class) {
-
if (INSTANCE == null) {
-
INSTANCE = new WxPayConfigImpl();
-
}
-
}
-
}
-
return INSTANCE;
-
}
-
@Override
-
public String getAppID() {
-
return appid;
-
}
-
@Override
-
public String getMchID() {
-
return mch_id;
-
}
-
@Override
-
public String getKey() {
-
return key;
-
}
-
@Override
-
public InputStream getCertStream() {
-
//這個可以看我另一篇“支付寶:APP支付介面2.0(alipay.trade.app.pay)”
-
//裡的“使用demo”步驟,有講解,如果是個springboot構成的jar,如何設定證書路徑
-
//文章連結:https://blog.csdn.net/u014799292/article/details/102680149
-
String fileUrl = "證書路徑";
-
InputStream certStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(fileUrl);
-
byte[] certData = null;
-
try {
-
certData = IOUtils.toByteArray(certStream);
-
} catch (IOException e) {
-
// TODO Auto-generated catch block
-
e.printStackTrace();
-
}finally{
-
if(certStream != null){
-
try {
-
certStream.close();
-
} catch (IOException e) {
-
// TODO Auto-generated catch block
-
e.printStackTrace();
-
}
-
}
-
}
-
return new ByteArrayInputStream(certData);
-
}
-
@Override
-
public IWXPayDomain getWXPayDomain() {
-
return WxPayDomainImpl.instance();
-
}
-
}
-
public class WxPayDomainImpl implements IWXPayDomain {
-
private final int MIN_SWITCH_PRIMARY_MSEC = 3 * 60 * 1000; //3 minutes
-
private long switchToAlternateDomainTime = 0;
-
private Map<String, DomainStatics> domainData = new HashMap<String, DomainStatics>();
-
private WxPayDomainImpl(){
-
}
-
private static class WxpayDomainHolder{
-
private static IWXPayDomain holder = new WxPayDomainImpl();
-
}
-
public static IWXPayDomain instance(){
-
return WxpayDomainHolder.holder;
-
}
-
@Override
-
public void report(String domain, long elapsedTimeMillis, Exception ex) {
-
DomainStatics info = domainData.get(domain);
-
if(info == null){
-
info = new DomainStatics(domain);
-
domainData.put(domain, info);
-
}
-
if(ex == null){ //success
-
if(info.succCount >= 2){ //continue succ, clear error count
-
info.connectTimeoutCount = info.dnsErrorCount = info.otherErrorCount = 0;
-
}else{
-
++info.succCount;
-
}
-
}else if(ex instanceof ConnectTimeoutException){
-
info.succCount = info.dnsErrorCount = 0;
-
++info.connectTimeoutCount;
-
}else if(ex instanceof UnknownHostException){
-
info.succCount = 0;
-
++info.dnsErrorCount;
-
}else{
-
info.succCount = 0;
-
++info.otherErrorCount;
-
}
-
}
-
@Override
-
public DomainInfo getDomain(WXPayConfig config) {
-
DomainStatics primaryDomain = domainData.get(WXPayConstants.DOMAIN_API);
-
if(primaryDomain == null ||
-
primaryDomain.isGood()) {
-
return new DomainInfo(WXPayConstants.DOMAIN_API, true);
-
}
-
long now = System.currentTimeMillis();
-
if(switchToAlternateDomainTime == 0){ //first switch
-
switchToAlternateDomainTime = now;
-
return new DomainInfo(WXPayConstants.DOMAIN_API2, false);
-
}else if(now - switchToAlternateDomainTime < MIN_SWITCH_PRIMARY_MSEC){
-
DomainStatics alternateDomain = domainData.get(WXPayConstants.DOMAIN_API2);
-
if(alternateDomain == null ||
-
alternateDomain.isGood() ||
-
alternateDomain.badCount() < primaryDomain.badCount()){
-
return new DomainInfo(WXPayConstants.DOMAIN_API2, false);
-
}else{
-
return new DomainInfo(WXPayConstants.DOMAIN_API, true);
-
}
-
}else{ //force switch back
-
switchToAlternateDomainTime = 0;
-
primaryDomain.resetCount();
-
DomainStatics alternateDomain = domainData.get(WXPayConstants.DOMAIN_API2);
-
if(alternateDomain != null)
-
alternateDomain.resetCount();
-
return new DomainInfo(WXPayConstants.DOMAIN_API, true);
-
}
-
}
-
static class DomainStatics {
-
final String domain;
-
int succCount = 0;
-
int connectTimeoutCount = 0;
-
int dnsErrorCount =0;
-
int otherErrorCount = 0;
-
DomainStatics(String domain) {
-
this.domain = domain;
-
}
-
void resetCount(){
-
succCount = connectTimeoutCount = dnsErrorCount = otherErrorCount = 0;
-
}
-
boolean isGood(){ return connectTimeoutCount <= 2 && dnsErrorCount <= 2; }
-
int badCount(){
-
return connectTimeoutCount + dnsErrorCount * 5 + otherErrorCount / 4;
-
}
-
}
-
}
給WXPayUtil再新增幾個時間工具方法(看自己需求定製),專案會在最後附連結。
appid & 商戶號位置
登入商戶平臺-產品中心-賬號關聯(AppID繫結),進入授權申請頁面;
金鑰key位置
登入商戶平臺-->>賬戶中心-->>API安全-->>設定API金鑰(32位,下面給出生成方式,然後複製貼上到祕鑰位置,記得保留,之後無法檢視,只能再次生成,測試環境隨便改,正式服記得最好固定一次,修改可能會引起服務端資料錯誤)
-
private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
-
private static final Random RANDOM = new SecureRandom();
-
/**
-
* 獲取隨機字串 Nonce Str
-
*
-
* @return String 隨機字串
-
*/
-
public static String generateNonceStr() {
-
char[] nonceChars = new char[32];
-
for (int index = 0; index < nonceChars.length; ++index) {
-
nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length()));
-
}
-
return new String(nonceChars);
-
}
再來寫一個請求類,處理微信訂單(訂單--統一下單/調起支付介面/支付結果通知/查詢訂單/關閉訂單)
-
/**
-
* 訂單--統一下單/調起支付介面/退款/結果通知/查詢訂單/關閉訂單
-
* 借鑑:https://blog.csdn.net/asd54090/article/details/81028323
-
*/
-
public class WxAppPayRequest {
-
private static final Logger logger = LoggerFactory.getLogger(WxAppPayRequest.class);
-
private WxPayConfigImpl config;
-
private WXPay wxpay;
-
/**
-
* 微信支付請求
-
*/
-
public WxAppPayRequest() {
-
try {
-
config = WxPayConfigImpl.getInstance();
-
wxpay = new WXPay(config);
-
} catch (Exception e) {
-
e.printStackTrace();
-
logger.error("微信配置初始化錯誤", e);
-
}
-
}
-
/**
-
* APP支付訂單請求
-
*
-
* @param body
-
* 格式:APP名字-實際商品名稱,如:天天愛消除-遊戲充值
-
* @param attach
-
* 附加資料,在查詢API和支付通知中原樣返回
-
* @param outTradeNo
-
* 商戶訂單號
-
* @param totalFee
-
* 總金額
-
* @param startTime
-
* 訂單開始時間String格式: yyyy-MM-dd HH:mm:ss
-
* @param expireMinute
-
* 有效時間(分鐘)
-
* @param notifyUrl
-
* 微信支付非同步通知回撥地址
-
* @return
-
*/
-
private WxAppPayResponseCode getOrderSign(String body, String attach, String outTradeNo, BigDecimal totalFee,
-
String startTime, int expireMinute, String notifyUrl) {
-
// 準備好請求引數
-
Map<String, String> map = new HashMap<String, String>();
-
map.put("device_info", WxPayConfigImpl.device_info);
-
map.put("body", body);
-
if (attach != null && !attach.isEmpty()) {
-
map.put("attach", attach);
-
}
-
map.put("out_trade_no", outTradeNo);
-
map.put("total_fee", totalFee.toString());
-
map.put("spbill_create_ip", WXPayUtil.getLocalAddress());
-
map.put("time_start", WXPayUtil.getFormatTime(startTime));
-
String endTime = WXPayUtil.getNSecondTime(startTime, expireMinute);
-
map.put("time_expire", WXPayUtil.getFormatTime(endTime));
-
map.put("notify_url", notifyUrl);
-
map.put("trade_type", "APP");
-
// 生成帶sign的xml字串
-
Map<String, String> unifiedOrderMap = null;
-
try {
-
unifiedOrderMap = wxpay.unifiedOrder(map);
-
if (unifiedOrderMap == null || (unifiedOrderMap != null
-
&& WxAppPayResponseCode.FAIL.code().equals(unifiedOrderMap.get("return_code")))) {
-
String errorMsg = "呼叫微信“統一下單”獲取prepayid 失敗...";
-
logger.info("getOrderSign --unifiedOrder: 呼叫微信“統一下單”獲取prepayid 失敗.");
-
logger.info("getOrderSign --unifiedOrder: 請求引數:" + map.toString());
-
logger.info("getOrderSign --unifiedOrder: 返回Map:" + unifiedOrderMap);
-
if (unifiedOrderMap != null) {
-
errorMsg += " 異常資訊為:" + unifiedOrderMap.get("return_msg");
-
}
-
WxAppPayResponseCode error = WxAppPayResponseCode.ERROR;
-
error.setAlias(errorMsg);
-
return error;
-
}
-
} catch (Exception e) {
-
// TODO Auto-generated catch block
-
e.printStackTrace();
-
logger.error("getOrderSign : 呼叫微信“統一下單”失敗 。", e);
-
WxAppPayResponseCode error = WxAppPayResponseCode.ERROR;
-
error.setAlias("呼叫微信“統一下單”失敗 。" + e.toString());
-
return error;
-
}
-
// 呼叫微信請求成功,但響應失敗
-
String resultCode = unifiedOrderMap.get("result_code");
-
if (WxAppPayResponseCode.FAIL.code().equals(resultCode)) {
-
WxAppPayResponseCode error = WxAppPayResponseCode.findCode(unifiedOrderMap.get("err_code"));
-
return error;
-
}
-
return parseWXOrderResponse(unifiedOrderMap);
-
}
-
/**
-
* 將map轉成客戶端訂單用的封裝體
-
*
-
* @param map
-
* map
-
* @return 使用者端用的封裝體
-
*/
-
private WxAppPayResponseCode parseWXOrderResponse(Map<String, String> map) {
-
WxOrderResponse response = new WxOrderResponse();
-
response.setAppid(map.get("appid"));
-
response.setPartnerid(map.get("partnerid"));
-
response.setPrepayid(map.get("prepay_id"));
-
response.setPack("Sign=WXPay");
-
response.setNoncestr(map.get("noncestr"));
-
String timestamp = WXPayUtil.getCurrentTimestamp() + "";
-
response.setTimestamp(timestamp);
-
// 前人踩坑,咱們乘涼
-
// sgin(簽名),不是拿微信“統一下單”返回的sgin,而是自己再籤一次,返回給客戶端
-
// 簽名的引數拿的不是“統一下單”,而是拿“調起支付介面”裡面的引數,這步API文件寫的是客戶端生成,不過咱們服務端就幫他做了
-
// 注意:map的key不能是大寫
-
Map<String, String> params = new HashMap<>();
-
params.put("appid", map.get("appid"));
-
params.put("partnerid", map.get("partnerid"));
-
params.put("prepayid", map.get("prepay_id"));
-
params.put("package", "Sign=WXPay");
-
params.put("noncestr", map.get("nonce_str"));
-
params.put("timestamp", timestamp);
-
try {
-
// 這個sign是移動端要請求微信服務端的,也是我們要儲存後面校驗的
-
String sgin = WXPayUtil.generateSignature(params, config.getKey());
-
response.setSign(sgin);
-
} catch (Exception e) {
-
e.printStackTrace();
-
logger.error("parseWXOrderResponse : 訂單第二次供客戶端簽名信息失敗 。");
-
logger.error("parseWXOrderResponse : 請求引數:" + params.toString());
-
logger.error("parseWXOrderResponse : 返回錯誤資訊:", e);
-
WxAppPayResponseCode errorData = WxAppPayResponseCode.ERROR;
-
errorData.setAlias("呼叫支付介面生成簽名sign失敗!");
-
return errorData;
-
}
-
WxAppPayResponseCode successData = WxAppPayResponseCode.SUCCESS;
-
successData.setAlias(JSONObject.toJSONString(response));
-
return successData;
-
}
-
/**
-
* 查微信訂單
-
*
-
* @param outTradeNo
-
* 訂單號
-
*/
-
public WxAppPayResponseCode queryOrderByOutTradeNo(String outTradeNo) {
-
logger.info("查詢微信支付訂單資訊,訂單號為:" + outTradeNo);
-
HashMap<String, String> data = new HashMap<String, String>();
-
data.put("out_trade_no", outTradeNo);
-
try {
-
Map<String, String> orderQueryMap = wxpay.orderQuery(data);
-
return disposeReturnInfo(orderQueryMap);
-
} catch (Exception e) {
-
e.printStackTrace();
-
logger.error("queryOrderByOutTradeNo : 查詢微信訂單支付資訊失敗 。訂單號:" + outTradeNo);
-
logger.error("queryOrderByOutTradeNo : 返回錯誤資訊:", e);
-
WxAppPayResponseCode errorData = WxAppPayResponseCode.ERROR;
-
errorData.setAlias("呼叫查微信訂單支付資訊介面失敗!");
-
return errorData;
-
}
-
}
-
/**
-
* 關閉訂單(剛剛生成的訂單不能立馬關閉,要間隔5分鐘,請自行做好判斷)
-
*
-
* @param outTradeNo
-
* 訂單號
-
* @return
-
*/
-
public WxAppPayResponseCode closeOrder(String outTradeNo) {
-
logger.info("關閉微信支付訂單資訊,訂單號為:" + outTradeNo);
-
HashMap<String, String> data = new HashMap<>();
-
data.put("out_trade_no", outTradeNo);
-
try {
-
Map<String, String> closeOrderMap = wxpay.closeOrder(data);
-
return disposeReturnInfo(closeOrderMap);
-
} catch (Exception e) {
-
e.printStackTrace();
-
logger.error("closeOrder : 微信關閉訂單失敗 。訂單號:" + outTradeNo);
-
logger.error("closeOrder : 返回錯誤資訊:", e);
-
WxAppPayResponseCode errorData = WxAppPayResponseCode.ERROR;
-
errorData.setAlias("呼叫查微信訂單支付資訊介面失敗!");
-
return errorData;
-
}
-
}
-
/**
-
* 微信退款申請
-
*
-
* @param outTradeNo
-
* 商戶訂單號
-
* @param amount
-
* 金額
-
* @param refund_desc
-
* 退款原因(可空)
-
* @param notifyUrl
-
* 退款非同步通知連結
-
*
-
* @return 返回map(已做過簽名驗證),具體資料參見微信退款API
-
*/
-
public WxAppPayResponseCode refundOrder(String outTradeNo, BigDecimal amount, String refundDesc, String notifyUrl)
-
throws Exception {
-
Map<String, String> data = new HashMap<String, String>();
-
data.put("out_trade_no", outTradeNo);
-
data.put("out_refund_no", outTradeNo);
-
data.put("total_fee", amount + "");
-
data.put("refund_fee", amount + "");
-
data.put("refund_fee_type", "CNY");
-
data.put("refund_desc", refundDesc);
-
data.put("notifyUrl", notifyUrl);
-
try {
-
Map<String, String> refundOrderMap = wxpay.refund(data);
-
return disposeReturnInfo(refundOrderMap);
-
} catch (Exception e) {
-
e.printStackTrace();
-
logger.error("closeOrder : 微信退款申請失敗 。訂單號:" + outTradeNo);
-
logger.error("closeOrder : 返回錯誤資訊:", e);
-
WxAppPayResponseCode errorData = WxAppPayResponseCode.ERROR;
-
errorData.setAlias("呼叫微信退款申請資訊介面失敗!");
-
return errorData;
-
}
-
}
-
/**
-
* 查微信退款訂單 注:如果單個支付訂單部分退款次數超過20次請使用退款單號查詢
-
*
-
* @param outTradeNo
-
* 訂單號
-
*/
-
public WxAppPayResponseCode queryRefundOrderByOutTradeNo(String outTradeNo) {
-
logger.info("查詢微信支付訂單資訊,訂單號為:" + outTradeNo);
-
HashMap<String, String> data = new HashMap<String, String>();
-
data.put("out_trade_no", outTradeNo);
-
try {
-
Map<String, String> refundQueryMap = wxpay.refundQuery(data);
-
return disposeReturnInfo(refundQueryMap);
-
} catch (Exception e) {
-
e.printStackTrace();
-
logger.error("queryRefundOrderByOutTradeNo : 查詢微信退款訂單資訊失敗 。訂單號:" + outTradeNo);
-
logger.error("queryRefundOrderByOutTradeNo : 返回錯誤資訊:", e);
-
WxAppPayResponseCode errorData = WxAppPayResponseCode.ERROR;
-
errorData.setAlias("呼叫查微信退款訂單介面失敗!");
-
return errorData;
-
}
-
}
-
/**
-
* 對介面接收成功後的返回進行處理
-
*
-
* @param resultMap
-
* @return
-
*/
-
private WxAppPayResponseCode disposeReturnInfo(Map<String, String> resultMap) {
-
if (resultMap == null
-
|| (resultMap != null && WxAppPayResponseCode.FAIL.code().equals(resultMap.get("return_code")))) {
-
WxAppPayResponseCode errorData = WxAppPayResponseCode.ERROR;
-
errorData.setAlias("呼叫微信介面失敗!返回資料為 : " + resultMap);
-
return errorData;
-
}
-
if (WxAppPayResponseCode.FAIL.code().equals(resultMap.get("result_code"))) {
-
WxAppPayResponseCode errorData = WxAppPayResponseCode.findCode(resultMap.get("err_code"));
-
return errorData;
-
}
-
WxAppPayResponseCode successData = WxAppPayResponseCode.SUCCESS;
-
successData.setAlias(JSONObject.toJSONString(resultMap));
-
return successData;
-
}
-
/**
-
* 是否成功接收微信支付回撥 用於回覆微信,否則微信回預設為商戶後端沒有收到回撥
-
*
-
* @return
-
*/
-
public String returnWXPayVerifyMsg() {
-
return "<xml>\n" + "\n" + " <return_code><![CDATA[SUCCESS]]></return_code>\n"
-
+ " <return_msg><![CDATA[OK]]></return_msg>\n" + "</xml>";
-
}
-
public static void main(String[] args) {
-
WxAppPayRequest pay = new WxAppPayRequest();
-
WxAppPayResponseCode orderSign = pay.getOrderSign("APP-商品訂單支付", "data1;data2", "212458542512542542",
-
new BigDecimal("100.12"), "2019-01-02 23:55:14", 10, "http://XXX");
-
System.out.println(orderSign);
-
}
-
}
OK,基本搞定,構建成一個處理微信訂單功能的JAR。
專案已推送github:https://github.com/leopardF/wxpay
最後,再次附上本次借鑑的文章:
如果問題,請提醒修正。