SpringBoot對接小程序微信支付的實現(xiàn)
前言
項目采用SpringBoot
微信支付有兩個版本:V3 和 V2,本文的接入版本為V2
API V2 和 API V3 的區(qū)別
1、接口請求參數(shù)不同
2、API V2 調(diào)用流程
在微信v2接口中,只有涉及資金流出或獲取重要信息才會使用證書,比如退款、企業(yè)付款和下載資金賬單等。
3、API V3 調(diào)用流程
(1)證書序列號
每個證書都有一個由CA頒發(fā)的唯一編號,即證書序列號。如果讀取到的序列號是10進(jìn)制整形需要轉(zhuǎn)換為大寫的16進(jìn)制。
(2)平臺證書
微信支付平臺證書是指由微信支付負(fù)責(zé)申請的,包含微信支付平臺標(biāo)識、公鑰信息的證書。
a、不同的商戶,對應(yīng)的微信支付平臺證書是不一樣的。
b、微信支付APIv3使用微信支付的平臺私鑰進(jìn)行應(yīng)答簽名,商戶使用平臺證書中的公鑰進(jìn)行驗簽。
c、微信平臺證書會周期性更換,商戶應(yīng)實現(xiàn)定期更新平臺證書的邏輯實現(xiàn)平臺證書平滑切換,即:定期調(diào)用該接口,間隔時間小于12 小時,參考獲取平臺證書接口。目前已知的微信更換平臺證書的場景:
- 證書到期后,必須更換。(目前是五年)
- 證書到期前,例行更換。(每年一次)
d、舊證書過期前10天生成新證書,舊證書過期前5天至過期當(dāng)天,新證書開始逐步放量用于應(yīng)答和回調(diào)的簽名。為了保證更換過程中不影響API的使用,請求和應(yīng)答的HTTP頭部中包括證書序列號,以聲明簽名或者加密所用的密鑰對和證書。
一、準(zhǔn)備工作
- 微信小程序賬號:要認(rèn)證、獲取appid、生成secret、開通支付、關(guān)聯(lián)商戶號
- 微信商戶平臺賬號:要認(rèn)證、獲取商戶號mch_id、獲取商戶API密鑰mch_key、APPID授權(quán)、配置支付接口
2.1、企業(yè)微信小程序開通
2.1.1、獲取開發(fā)者ID
- 獲取appid:小程序的身份證明
- 獲取secret:小程序的唯一憑證密鑰
2.1.2、開通支付功能
2.1.3、關(guān)聯(lián)商戶號
2.2、企業(yè)商戶號的開通
2.2.1、獲取商戶號mch_id
2.2.2、獲取商戶API密鑰mch_key
二、整體流程
商戶系統(tǒng)與微信支付系統(tǒng)主要交互:
1、JSAPI支付、APP支付、H5支付、Native支付、付款碼支付、小程序支付【微信支付API列表】
2、小程序內(nèi)調(diào)用登錄接口,獲取到用戶的openid【小程序登錄API】
3、商戶server調(diào)用支付統(tǒng)一下單【統(tǒng)一下單API】
4、小程序支付成功回調(diào)notity_url(業(yè)務(wù)邏輯處理)【支付結(jié)果通知】
5、商戶server查詢支付結(jié)果,如未收到支付通知的情況,商戶后臺系統(tǒng)可調(diào)用【查詢訂單API】
三、后端項目搭建
3.1、統(tǒng)一下單
1、導(dǎo)入相關(guān)依賴 pom.yml
<!-- 微信支付 SDK --> <dependency> <groupId>com.github.wxpay</groupId> <artifactId>wxpay-sdk</artifactId> <version>0.0.3</version> </dependency>
2、文件配置微信公眾號的基礎(chǔ)信息 application.yml
# 微信支付配置 notifyUrl:微信支付異步回調(diào)地址 pay: appId: #應(yīng)用id apiKey: #商戶私鑰key mchId: #商戶id appSecret: #小程序密鑰 notifyUrl: #支付回調(diào)地址
3、設(shè)置配置文件 WxPayConfig.java
import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Component; /** * 微信支付配置 * @author lf * @date 2023/8/30 */ @Data @Component @Configuration @ConfigurationProperties(prefix = "pay") public class WxPayConfig { /** * 微信公眾號appid */ private String appId; /** * 公眾號設(shè)置的API v2密鑰 */ private String apiKey; /** * 微信商戶平臺 商戶id */ private String mchId; /** *小程序密鑰 */ private String appSecret; /** * 小程序支付異步回調(diào)地址 */ private String notifyUrl; }
4、微信支付預(yù)下單實體類 WxChatPay.java
import lombok.Data; import lombok.experimental.Accessors; import java.math.BigDecimal; /** * 微信支付預(yù)下單實體類 */ @Data @Accessors(chain = true) public class WeChatPay { /** * 返回狀態(tài)碼 此字段是通信標(biāo)識,非交易標(biāo)識,交易是否成功需要查看result_code來判斷 */ public String return_code; /** * 返回信息 當(dāng)return_code為FAIL時返回信息為錯誤原因 ,例如 簽名失敗 參數(shù)格式校驗錯誤 */ private String return_msg; /** * 公眾賬號ID 調(diào)用接口提交的公眾賬號ID */ private String appid; /** * 商戶號 調(diào)用接口提交的商戶號 */ private String mch_id; /** * api密鑰 詳見:https://pay.weixin.qq.com/index.php/extend/employee */ private String api_key; /** * 設(shè)備號 自定義參數(shù),可以為請求支付的終端設(shè)備號等 */ private String device_info; /** * 隨機(jī)字符串 5K8264ILTKCH16CQ2502SI8ZNMTM67VS 微信返回的隨機(jī)字符串 */ private String nonce_str; /** * 簽名 微信返回的簽名值,詳見簽名算法:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=4_3 */ private String sign; /** * 簽名類型 */ private String sign_type; /** * 業(yè)務(wù)結(jié)果 SUCCESS SUCCESS/FAIL */ private String result_code; /** * 錯誤代碼 當(dāng)result_code為FAIL時返回錯誤代碼,詳細(xì)參見下文錯誤列表 */ private String err_code; /** * 錯誤代碼描述 當(dāng)result_code為FAIL時返回錯誤描述,詳細(xì)參見下文錯誤列表 */ private String err_code_des; /** * 交易類型 JSAPI JSAPI -JSAPI支付 NATIVE -Native支付 APP -APP支付 說明詳見;https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=4_2 */ private String trade_type; /** * 預(yù)支付交易會話標(biāo)識 微信生成的預(yù)支付會話標(biāo)識,用于后續(xù)接口調(diào)用中使用,該值有效期為2小時 */ private String prepay_id; /** * 二維碼鏈接 weixin://wxpay/bizpayurl/up?pr=NwY5Mz9&groupid=00 trade_type=NATIVE時有返回,此url用于生成支付二維碼,然后提供給用戶進(jìn)行掃碼支付。注意:code_url的值并非固定,使用時按照URL格式轉(zhuǎn)成二維碼即可 */ private String code_url; /** * 商品描述 商品簡單描述,該字段請按照規(guī)范傳遞,具體請見 https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=4_2 */ private String body; /** * 商家訂單號 商戶系統(tǒng)內(nèi)部訂單號,要求32個字符內(nèi),只能是數(shù)字、大小寫字母_-|* 且在同一個商戶號下唯一。詳見商戶訂單號 https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=4_2 */ private String out_trade_no; /** * 標(biāo)價金額 訂單總金額,單位為分,詳見支付金額 https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=4_2 */ private String total_fee; /** * 終端IP 支持IPV4和IPV6兩種格式的IP地址。用戶的客戶端IP */ private String spbill_create_ip; /** * 通知地址 異步接收微信支付結(jié)果通知的回調(diào)地址,通知url必須為外網(wǎng)可訪問的url,不能攜帶參數(shù)。公網(wǎng)域名必須為https,如果是走專線接入,使用專線NAT IP或者私有回調(diào)域名可使用http */ private String notify_url; /** * 子商戶號 sub_mch_id 非必填(商戶不需要傳入,服務(wù)商模式才需要傳入) 微信支付分配的子商戶號 */ private String sub_mch_id; /** * 附加數(shù)據(jù),在查詢API和支付通知中原樣返回,該字段主要用于商戶攜帶訂單的自定義數(shù)據(jù) */ private String attach; /** * 商戶系統(tǒng)內(nèi)部的退款單號,商戶系統(tǒng)內(nèi)部唯一,只能是數(shù)字、大小寫字母_-|*@ ,同一退款單號多次請求只退一筆。 */ private String out_refund_no; /** * 退款總金額,單位為分,只能為整數(shù),可部分退款。詳見支付金額 https://pay.weixin.qq.com/wiki/doc/api/native_sl.php?chapter=4_2 */ private String refund_fee; /** * 退款原因 若商戶傳入,會在下發(fā)給用戶的退款消息中體現(xiàn)退款原因 注意:若訂單退款金額≤1元,且屬于部分退款,則不會在退款消息中體現(xiàn)退款原因 */ private String refund_desc; /** * 交易結(jié)束時間 訂單失效時間,格式為yyyyMMddHHmmss,如2009年12月27日9點(diǎn)10分10秒表示為20091227091010。其他詳見時間規(guī)則 注意:最短失效時間間隔必須大于5分鐘 */ private String time_expire; /** * 用戶標(biāo)識 trade_type=JSAPI,此參數(shù)必傳,用戶在主商戶appid下的唯一標(biāo)識。openid和sub_openid可以選傳其中之一,如果選擇傳sub_openid,則必須傳sub_appid。下單前需要調(diào)用【網(wǎng)頁授權(quán)獲取用戶信息: https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html 】接口獲取到用戶的Openid。 */ private String openid; /** * 時間戳 */ private String time_stamp; /** * 會員類型 */ private String memberShipType; }
5、微信支付API地址 WeChatPayUrlContants.java
/** * 微信支付API地址 * @author lf * @date 2023/8/30 */ public class WeChatPayUrlConstants { /** * 統(tǒng)一下單預(yù)下單接口url */ public static final String Uifiedorder = "https://api.mch.weixin.qq.com/pay/unifiedorder"; /** * 訂單狀態(tài)查詢接口URL */ public static final String Orderquery = "https://api.mch.weixin.qq.com/pay/orderquery"; /** * 訂單申請退款 */ public static final String Refund = "https://api.mch.weixin.qq.com/secapi/pay/refund"; /** * 付款碼 支付 */ public static final String MicroPay = "https://api.mch.weixin.qq.com/pay/micropay"; /** * 微信網(wǎng)頁授權(quán) 獲取“code”請求地址 */ public static final String GainCodeUrl = "https://open.weixin.qq.com/connect/oauth2/authorize"; /** * 微信網(wǎng)頁授權(quán) 獲取“code” 回調(diào)地址 */ public static final String GainCodeRedirect_uri = "http://i5jmxe.natappfree.cc/boss/WeChatPayMobile/SkipPage.html"; }
6、預(yù)下單成功之后返回結(jié)果 OrderReturnInfo.java
import lombok.Data; @Data public class OrderReturnInfo { private String return_code; private String return_msg; private String result_code; private String appid; private String mch_id; private String nonce_str; private String sign; private String prepay_id; private String trade_type; }
7、查詢訂單返回的實體類 QueryReturnInfo.java
import lombok.Data; /** * 查詢訂單返回實體類 * @author lf * @date 2023/9/1 */ @Data public class QueryReturnInfo { private String return_code; private String return_msg; private String result_code; private String err_code; private String err_code_des; private String appid; private String mch_id; private String nonce_str; private String sign; private String prepay_id; private String trade_type; private String device_info; private String openid; private String is_subscribe; private String trade_state; private String bank_type; private int total_fee; private int settlement_total_fee; private String fee_type; private int cash_fee; private String cash_fee_type; private int coupon_fee; private int coupon_count; private String coupon_type_$n; private String coupon_id_$n; private String transaction_id; private String out_trade_no; private String time_end; private String trade_state_desc; }
8、簽名實體類 SignInfo.java
/** * 簽名實體類 * @author lf * @date 2023/9/1 */ @Data public class SignInfo { private String appId;//小程序ID private String timeStamp;//時間戳 private String nonceStr;//隨機(jī)串 @XStreamAlias("package") private String repay_id; private String signType;//簽名方式 public String getAppId() { return appId; } public void setAppId(String appId) { this.appId = appId; } public String getTimeStamp() { return timeStamp; } public void setTimeStamp(String timeStamp) { this.timeStamp = timeStamp; } public String getNonceStr() { return nonceStr; } public void setNonceStr(String nonceStr) { this.nonceStr = nonceStr; } public String getRepay_id() { return repay_id; } public void setRepay_id(String repay_id) { this.repay_id = repay_id; } public String getSignType() { return signType; } public void setSignType(String signType) { this.signType = signType; } }
9、Http工具類 HttpRequest.java
/** * Http工具類 * @author lf * @date 2023/9/1 */ public class HttpRequest { //連接超時時間,默認(rèn)10秒 private static final int socketTimeout = 10000; //傳輸超時時間,默認(rèn)30秒 private static final int connectTimeout = 30000; /** * post請求 * * @throws IOException * @throws ClientProtocolException * @throws NoSuchAlgorithmException * @throws KeyStoreException * @throws KeyManagementException * @throws UnrecoverableKeyException */ public static String sendPost(String url, Object xmlObj) throws ClientProtocolException, IOException, UnrecoverableKeyException, KeyManagementException, KeyStoreException, NoSuchAlgorithmException { HttpPost httpPost = new HttpPost(url); //解決XStream對出現(xiàn)雙下劃線的bug XStream xStreamForRequestPostData = new XStream(new DomDriver("UTF-8", new XmlFriendlyNameCoder("-_", "_"))); xStreamForRequestPostData.alias("xml", xmlObj.getClass()); //將要提交給API的數(shù)據(jù)對象轉(zhuǎn)換成XML格式數(shù)據(jù)Post給API String postDataXML = xStreamForRequestPostData.toXML(xmlObj); //得指明使用UTF-8編碼,否則到API服務(wù)器XML的中文不能被成功識別 StringEntity postEntity = new StringEntity(postDataXML, "UTF-8"); httpPost.addHeader("Content-Type", "text/xml"); httpPost.setEntity(postEntity); //設(shè)置請求器的配置 RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(socketTimeout).setConnectTimeout(connectTimeout).build(); httpPost.setConfig(requestConfig); HttpClient httpClient = HttpClients.createDefault(); HttpResponse response = httpClient.execute(httpPost); HttpEntity entity = response.getEntity(); String result = EntityUtils.toString(entity, "UTF-8"); return result; } /** * 自定義證書管理器,信任所有證書 * * @author pc */ public static class MyX509TrustManager implements X509TrustManager { @Override public void checkClientTrusted( java.security.cert.X509Certificate[] arg0, String arg1) throws CertificateException { } @Override public void checkServerTrusted( java.security.cert.X509Certificate[] arg0, String arg1) throws CertificateException { } @Override public java.security.cert.X509Certificate[] getAcceptedIssuers() { return null; } } }
10、微信簽名 SignUtils.java
/** * 微信簽名 * @author lf * @date 2023/9/1 */ public class SignUtils { /** * 簽名算法 * * @param o 要參與簽名的數(shù)據(jù)對象 * @return 簽名 * @throws IllegalAccessException */ public static String getSign(Object o) throws IllegalAccessException { ArrayList<String> list = new ArrayList<String>(); Class cls = o.getClass(); Field[] fields = cls.getDeclaredFields(); for (Field f : fields) { f.setAccessible(true); if (f.get(o) != null && f.get(o) != "") { String name = f.getName(); XStreamAlias anno = f.getAnnotation(XStreamAlias.class); if (anno != null) { name = anno.value(); } list.add(name + "=" + f.get(o) + "&"); } } int size = list.size(); String[] arrayToSort = list.toArray(new String[size]); Arrays.sort(arrayToSort, String.CASE_INSENSITIVE_ORDER); StringBuilder sb = new StringBuilder(); for (int i = 0; i < size; i++) { sb.append(arrayToSort[i]); } String result = sb.toString(); result += "key=" + Configure.getKey(); System.out.println("簽名數(shù)據(jù):" + result); result = MD5.MD5Encode(result).toUpperCase(); return result; } public static String getSign(Map<String, Object> map) { ArrayList<String> list = new ArrayList<String>(); for (Map.Entry<String, Object> entry : map.entrySet()) { if (entry.getValue() != "") { list.add(entry.getKey() + "=" + entry.getValue() + "&"); } } int size = list.size(); String[] arrayToSort = list.toArray(new String[size]); Arrays.sort(arrayToSort, String.CASE_INSENSITIVE_ORDER); StringBuilder sb = new StringBuilder(); for (int i = 0; i < size; i++) { sb.append(arrayToSort[i]); } String result = sb.toString(); result += "key=" + Configure.getKey(); //Util.log("Sign Before MD5:" + result); result = MD5.MD5Encode(result).toUpperCase(); //Util.log("Sign Result:" + result); return result; } }
11、MD5加密工具類 MD5.java
import java.security.MessageDigest; /** * MD5加密工具類 * @author lf * @date 2023/9/1 */ public class MD5 { private final static String[] hexDigits = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"}; /** * 轉(zhuǎn)換字節(jié)數(shù)組為16進(jìn)制字串 * * @param b 字節(jié)數(shù)組 * @return 16進(jìn)制字串 */ public static String byteArrayToHexString(byte[] b) { StringBuilder resultSb = new StringBuilder(); for (byte aB : b) { resultSb.append(byteToHexString(aB)); } return resultSb.toString(); } /** * 轉(zhuǎn)換byte到16進(jìn)制 * * @param b 要轉(zhuǎn)換的byte * @return 16進(jìn)制格式 */ private static String byteToHexString(byte b) { int n = b; if (n < 0) { n = 256 + n; } int d1 = n / 16; int d2 = n % 16; return hexDigits[d1] + hexDigits[d2]; } /** * MD5編碼 * * @param origin 原始字符串 * @return 經(jīng)過MD5加密之后的結(jié)果 */ public static String MD5Encode(String origin) { String resultString = null; try { resultString = origin; MessageDigest md = MessageDigest.getInstance("MD5"); resultString = byteArrayToHexString(md.digest(resultString.getBytes())); } catch (Exception e) { e.printStackTrace(); } return resultString; } }
12、微信支付配置類 Configure.java
/** * 微信支付配置類 * @author lf * @date 2023/9/1 */ public class Configure { /** * 商戶支付秘鑰 */ private static String key = ""; public static String getKey() { return key; } public static void setKey(String key) { Configure.key = key; } }
13、Controller層,PayparameterVO可以不需要,我這里是因為業(yè)務(wù)需要,處理業(yè)務(wù)邏輯
/** * 小程序支付下單接口 * @return 返回結(jié)果 */ @ApiOperation("小程序支付功能") @PostMapping("/pay") public AjaxResult wxPay(@RequestBody PayParameterVO payParameterVO){ Map payHistory = wxPayInfoService.insertPayRecord(payParameterVO); return success("success",payHistory); } /** * 查詢訂單 */ @ApiOperation("訂單查詢") @PostMapping("/wx/query") public AjaxResult orderQuery(@RequestParam("out_trade_no") String out_trade_no) { Map query = wxPayInfoService.orderQuery(out_trade_no); return success("success", query); }
14、業(yè)務(wù)接口層 WxPayInfoService.java
import com.ruoyi.ai.doamin.PayParameterVO; import java.util.Map; /** * 微信小程序支付-業(yè)務(wù)接口層 * @author lf * @date 2023/8/31 */ public interface WxPayInfoService { /** * 插入訂單記錄 */ Map insertPayRecord(PayParameterVO payParameterVO); /** * 查詢訂單 * @param out_trade_no 訂單號 * @return 返回結(jié)果 */ Map orderQuery(String out_trade_no); }
15、業(yè)務(wù)接口實現(xiàn)層 WxPayInfoServiceImpl.java
import cn.hutool.core.util.ObjectUtil; import com.github.wxpay.sdk.WXPayConstants; import com.github.wxpay.sdk.WXPayUtil; import com.ruoyi.ai.doamin.PayParameterVO; import com.ruoyi.ai.service.WxPayInfoService; import com.ruoyi.common.config.WxPayConfig; import com.ruoyi.common.constant.WeChatPayUrlConstants; import com.ruoyi.common.core.domain.pay.WeChatPay; import com.ruoyi.common.utils.pay.HttpRequest; import com.ruoyi.common.utils.pay.SignUtils; import com.ruoyi.common.utils.pay.entity.OrderReturnInfo; import com.ruoyi.common.utils.pay.entity.QueryReturnInfo; import com.ruoyi.common.utils.pay.entity.SignInfo; import com.thoughtworks.xstream.XStream; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.math.BigDecimal; import java.text.DecimalFormat; import java.util.*; /** * 微信小程序支付-業(yè)務(wù)接口實現(xiàn)層 * @author lf * @date 2023/8/31 */ @Service @Slf4j public class WxPayInfoServiceImpl implements WxPayInfoService { @Resource private WxPayConfig payProperties; private static final DecimalFormat df = new DecimalFormat("#"); /** * 插入訂單記錄 * @param payParameterVO 用戶ID 會員套餐ID * @return 返回結(jié)果 */ @Override @Transactional public Map insertPayRecord(PayParameterVO payParameterVO) { //接收返回的參數(shù) Map<String, Object> map = new HashMap<>(); String title = "koko測試點(diǎn)數(shù)"; //金額 * 100 以分為單位 BigDecimal fee = BigDecimal.valueOf(1); BigDecimal RMB = new BigDecimal(100); BigDecimal totalFee = fee.multiply(RMB); try { WeChatPay weChatPay = new WeChatPay(); weChatPay.setAppid(payProperties.getAppId()); weChatPay.setMch_id(payProperties.getMchId()); weChatPay.setNonce_str(getRandomStringByLength(32)); weChatPay.setBody(title); weChatPay.setOut_trade_no(getRandomStringByLength(32)); weChatPay.setTotal_fee( df.format(Double.parseDouble(String.valueOf(totalFee)))); weChatPay.setSpbill_create_ip("127.0.0.1"); weChatPay.setNotify_url(payProperties.getNotifyUrl()); weChatPay.setTrade_type("JSAPI"); //這里直接使用當(dāng)前用戶的openid weChatPay.setOpenid("oOKq*******xj8o"); weChatPay.setSign_type("MD5"); //生成簽名 String sign = SignUtils.getSign(weChatPay); weChatPay.setSign(sign); String result = HttpRequest.sendPost(WeChatPayUrlConstants.Uifiedorder, weChatPay); System.out.println(result); //將返回結(jié)果從xml格式轉(zhuǎn)換為map格式 Map<String, String> wxResultMap = WXPayUtil.xmlToMap(result); if (ObjectUtil.isNotEmpty(wxResultMap.get("return_code")) && wxResultMap.get("return_code").equals("SUCCESS")){ if (wxResultMap.get("result_code").equals("FAIL")){ map.put("msg", "統(tǒng)一下單失敗"); map.put("status",500); map.put("data", wxResultMap.get("err_code_des")); return map; } } XStream xStream = new XStream(); xStream.alias("xml", OrderReturnInfo.class); OrderReturnInfo returnInfo = (OrderReturnInfo) xStream.fromXML(result); // 二次簽名 if ("SUCCESS".equals(returnInfo.getReturn_code()) && returnInfo.getReturn_code().equals(returnInfo.getResult_code())) { SignInfo signInfo = new SignInfo(); signInfo.setAppId(payProperties.getAppId()); long time = System.currentTimeMillis() / 1000; signInfo.setTimeStamp(String.valueOf(time)); signInfo.setNonceStr(WXPayUtil.generateNonceStr()); signInfo.setRepay_id("prepay_id=" + returnInfo.getPrepay_id()); signInfo.setSignType("MD5"); //生成簽名 String sign1 = SignUtils.getSign(signInfo); Map<String, String> payInfo = new HashMap<>(); payInfo.put("timeStamp", signInfo.getTimeStamp()); payInfo.put("nonceStr", signInfo.getNonceStr()); payInfo.put("package", signInfo.getRepay_id()); payInfo.put("signType", signInfo.getSignType()); payInfo.put("paySign", sign1); map.put("status", 200); map.put("msg", "統(tǒng)一下單成功!"); map.put("data", payInfo); //預(yù)下單成功,處理業(yè)務(wù)邏輯 //****************************// // 業(yè)務(wù)邏輯結(jié)束 回傳給小程序端喚起支付 return map; } map.put("status", 500); map.put("msg", "統(tǒng)一下單失敗!"); map.put("data", null); return map; } catch (Exception e) { e.printStackTrace(); } return null; } /** * 查詢訂單 * @param out_trade_no 訂單號 * @return 返回結(jié)果 */ @Override public Map orderQuery(String out_trade_no){ Map<String, Object> map = new HashMap<>(); try { WeChatPay weChatPay = new WeChatPay(); weChatPay.setAppid(payProperties.getAppId()); weChatPay.setMch_id(payProperties.getMchId()); weChatPay.setNonce_str(WXPayUtil.generateNonceStr()); weChatPay.setOut_trade_no(out_trade_no); //order.setSign_type("MD5"); //生成簽名 String sign = SignUtils.getSign(weChatPay); weChatPay.setSign(sign); String result = HttpRequest.sendPost(WXPayConstants.ORDERQUERY_URL, weChatPay); System.out.println(result); XStream xStream = new XStream(); xStream.alias("xml", QueryReturnInfo.class); QueryReturnInfo returnInfo = (QueryReturnInfo) xStream.fromXML(result); map.put("status", 500); map.put("msg", "統(tǒng)一下單失敗!"); map.put("data", returnInfo); return map; } catch (Exception e) { e.printStackTrace(); } return null; } /** * 獲取一定長度的隨機(jī)字符串 * * @param length 指定字符串長度 * @return 一定長度的字符串 */ public static String getRandomStringByLength(int length) { String base = "abcdefghijklmnopqrstuvwxyz0123456789"; Random random = new Random(); StringBuffer sb = new StringBuffer(); for (int i = 0; i < length; i++) { int number = random.nextInt(base.length()); sb.append(base.charAt(number)); } return sb.toString(); } }
3.2、支付支付回調(diào)
/** * 微信小程序支付成功回調(diào) * @param request 請求 * @param response 響應(yīng) * @return 返回結(jié)果 * @throws Exception 異常處理 */ @RequestMapping("/weixin/callback") public String callBack(HttpServletRequest request, HttpServletResponse response) throws Exception { System.out.println("接口已被調(diào)用"); ServletInputStream inputStream = request.getInputStream(); String notifyXml = StreamUtils.inputStream2String(inputStream, "utf-8"); System.out.println(notifyXml); // 解析返回結(jié)果 Map<String, String> notifyMap = WXPayUtil.xmlToMap(notifyXml); // 判斷支付是否成功 if ("SUCCESS".equals(notifyMap.get("result_code"))) { //支付成功時候,處理業(yè)務(wù)邏輯 System.out.println("支付成功"); System.out.println("<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>" + "<return_msg><![CDATA[OK]]></return_msg>" + "</xml> "); /** * 注意 * 因為微信回調(diào)會有八次之多,所以當(dāng)?shù)谝淮位卣{(diào)成功了,那么我們就不再執(zhí)行邏輯了 * return返回的結(jié)果一定是這種格式,當(dāng)result_code返回的結(jié)果是SUCCESS時,則不進(jìn)行調(diào)用了 * 如果不返回下面的格式,業(yè)務(wù)邏輯會出現(xiàn)回調(diào)多次的情況,我就遇到過這種情況。 */ return "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>" + "<return_msg><![CDATA[OK]]></return_msg>" + "</xml> "; } // 創(chuàng)建響應(yīng)對象:微信接收到校驗失敗的結(jié)果后,會反復(fù)的調(diào)用當(dāng)前回調(diào)函數(shù) Map<String, String> returnMap = new HashMap<>(); returnMap.put("return_code", "FAIL"); returnMap.put("return_msg", ""); String returnXml = WXPayUtil.mapToXml(returnMap); response.setContentType("text/xml"); System.out.println("校驗失敗"); return returnXml; }
注:如果能正常走預(yù)支付的接口,而沒有處理回調(diào)接口的業(yè)務(wù)邏輯。
1、返回的xml格式是否正確,V2會比對xml返回的數(shù)據(jù)格式是否一致
2、回調(diào)的接口是否允許外網(wǎng)訪問,并且接口不能攜帶任何參數(shù),如果框架存在token驗證時,則需要關(guān)閉支付回調(diào)的token驗證。
3、如果以上都沒問題,卻還是沒走回調(diào),可以看看這篇文檔【接收不到回調(diào)排查指引】
3.3、問題排查
1、在與前端聯(lián)調(diào)時,如果出現(xiàn)如下問題,看看預(yù)下單的packeage參數(shù)是否正確,必須嚴(yán)格按照如下格式:
package: "prepay_id=wx*********0000";
2、在與前端聯(lián)調(diào)時,如果出現(xiàn)如下問題,看看簽名的工具類是否能夠驗證通過【簽名校驗】
3.4、統(tǒng)一下單和訂單查詢
1、統(tǒng)一下單
2、訂單查詢-支付成功
3、訂單查詢-支付失敗
4、預(yù)下單成功并且支付成功
到此這篇關(guān)于SpringBoot對接小程序微信支付的實現(xiàn)的文章就介紹到這了,更多相關(guān)SpringBoot 小程序微信支付內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot實現(xiàn)微信支付接口調(diào)用及回調(diào)函數(shù)(商戶參數(shù)獲取)
- java?Springboot對接開發(fā)微信支付詳細(xì)流程
- Springboot整合微信支付(訂單過期取消及商戶主動查單)
- UniApp?+?SpringBoot?實現(xiàn)微信支付和退款功能
- SpringBoot實現(xiàn)整合微信支付方法詳解
- springboot對接微信支付的完整流程(附前后端代碼)
- 一篇文章帶你入門Springboot整合微信登錄與微信支付(附源碼)
- springboot整合微信支付sdk過程解析
- SpringBoot+MyBatis集成微信支付實現(xiàn)示例
相關(guān)文章
Springboot?格式化LocalDateTime的方法
這篇文章主要介紹了Springboot格式化LocalDateTime的相關(guān)知識,本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-05-05SpringBoot中@RestControllerAdvice注解的使用
這篇文章主要介紹了SpringBoot中@RestControllerAdvice注解的使用,@RestControllerAdvice主要用精簡客戶端返回異常,它可以捕獲各種異常,需要的朋友可以參考下2024-01-01Java關(guān)于桶排序的知識點(diǎn)總結(jié)
這篇文章給大家總結(jié)了關(guān)于JAVA中J桶排序的相關(guān)知識點(diǎn)和用法分享,有興趣的讀者跟著學(xué)習(xí)下。2018-04-04如何用Java實現(xiàn).env文件讀取敏感數(shù)據(jù)
這篇文章主要介紹了如何用Java實現(xiàn).env文件讀取敏感數(shù)據(jù),并提供了一個自動配置類EnvAutoConfiguration,common-env-starter-demo模塊展示了如何配置和啟動一個簡單的Spring Boot應(yīng)用程序,需要的朋友可以參考下2025-02-02JAVA數(shù)據(jù)寫入生成excel文件和發(fā)送郵件
這篇文章主要介紹了JAVA數(shù)據(jù)寫入生成excel文件和發(fā)送郵件,流程:先導(dǎo)包 => 郵箱開啟配置 => java寫好配置類 => 測試發(fā)送 => 數(shù)據(jù)寫入excel => 郵件帶附件發(fā)送2024-06-06java基礎(chǔ)知識之FileInputStream流的使用
這篇文章主要介紹了java基礎(chǔ)知識之FileInputStream流的使用,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-12-12淺談Java數(shù)據(jù)結(jié)構(gòu)之稀疏數(shù)組知識總結(jié)
今天帶大家了解一下Java稀疏數(shù)組的相關(guān)知識,文中有非常詳細(xì)的介紹及代碼示例,對正在學(xué)習(xí)java的小伙伴們有很好地幫助,需要的朋友可以參考下2021-05-05