SpringBoot整合微信小程序支付V3(支付退款)
一、微信支付-準(zhǔn)備工作
微信支付開發(fā)前,需要先獲取商家信息,包括商戶號(hào)、AppId、證書和密鑰。
獲取商戶號(hào)
微信商戶平臺(tái) 申請(qǐng)成為商戶 => 提交資料 => 簽署協(xié)議 => 獲取商戶號(hào)獲取AppID
微信公眾平臺(tái) 注冊(cè)服務(wù)號(hào) => 服務(wù)號(hào)認(rèn)證 => 獲取APPID => 綁定商戶號(hào)申請(qǐng)商戶證書
登錄商戶平臺(tái) => 選擇 賬戶中心 => 安全中心 => API安全 => 申請(qǐng)API證書 包括商戶證書和商戶私鑰獲取微信的證書
獲取APIv3秘鑰(在微信支付回調(diào)通知和商戶獲取平臺(tái)證書使用APIv3密鑰)
登錄商戶平臺(tái) => 選擇 賬戶中心 => 安全中心 => API安全 => 設(shè)置APIv3密鑰
二、微信支付-基本配置
1.引入pom.xml
<!-- 微信支付 --> <dependency> <groupId>com.github.wechatpay-apiv3</groupId> <artifactId>wechatpay-apache-httpclient</artifactId> <version>0.4.2</version> </dependency>
2.配置商戶信息、證書、密鑰等。將客戶端對(duì)象構(gòu)建到Bean中,方便后續(xù)使用。
可以采用兩種方式
①.配置application.yml
weixin: appid: wx*************acx # appid mch-serial-no: 3FB18E2*******0127B3*****0053E2 # 證書序列號(hào) private-key-path: D:\wx\pem\apiclient_key.pem # 證書路徑 mch-id: 16*****801 # 商戶號(hào) key: F8CDeHBc***********2t5nvVeh1 # api秘鑰 domain: https://api.mch.weixin.qq.com # 微信服務(wù)器地址 notify-domain: https://xw666.mynatapp.cc # 回調(diào),自己的回調(diào)地址
然后創(chuàng)建對(duì)應(yīng)的實(shí)體
@Configuration @PropertySource("classpath:application.yml") //讀取配置文件 @ConfigurationProperties(prefix = "weixin") //讀取wxpay節(jié)點(diǎn) @Data //使用set方法將wxpay節(jié)點(diǎn)中的值填充到當(dāng)前類的屬性中 @ApiModel("微信支付靜態(tài)常量類") public class WxPayConfig { @ApiModelProperty("商戶號(hào)") private String mchId; @ApiModelProperty("商戶API證書序列號(hào)") private String mchSerialNo; @ApiModelProperty("商戶私鑰文件") private String privateKeyPath; @ApiModelProperty("APIv3密鑰") private String key; @ApiModelProperty("APPID") private String appid; @ApiModelProperty("微信服務(wù)器地址") private String domain; @ApiModelProperty("接收結(jié)果通知地址") private String notifyDomain; }
②使用數(shù)據(jù)庫(kù)存儲(chǔ),在項(xiàng)目啟動(dòng)時(shí)加載到redis中,然后從redis中獲取
@Configuration @Data @ApiModel("微信支付靜態(tài)常量類") public class WxPayConstants { @Resource private RedisCache redisCache; @ApiModelProperty("APPID") public String appid; @ApiModelProperty("商戶API證書序列號(hào)") public String mchSerialNo; @ApiModelProperty("商戶私鑰文件") public String privateKeyPath; @ApiModelProperty("商戶號(hào)") public String mchId; @ApiModelProperty("APIv3密鑰") public String key; @ApiModelProperty("微信服務(wù)器地址") public String domain; @ApiModelProperty("接收結(jié)果通知地址") public String notifyDomain; @Resource public void getParam(RedisCache redisCache){ appid = redisCache.getCacheObject("WX_PAY_SAVE_WX_APPID"); mchSerialNo = redisCache.getCacheObject("WX_PAY_SAVE_MCH_SERIAL_NO"); privateKeyPath = redisCache.getCacheObject("WX_PAY_SAVE_PRIVATE_KEY_PATH"); mchId = redisCache.getCacheObject("WX_PAY_SAVE_MCH_ID"); key = redisCache.getCacheObject("WX_PAY_SAVE_KEY"); domain = redisCache.getCacheObject("WX_PAY_SAVE_DOMAIN"); notifyDomain = redisCache.getCacheObject("WX_PAY_SAVE_NOTIFY_DOMAIN"); } }
這兩個(gè)實(shí)體有幾個(gè)共同的方法,無(wú)論使用哪一個(gè),放到下面即可
/** * 獲取商戶的私鑰文件 * * @param filename 證書地址 * @return 私鑰文件 */ public PrivateKey getPrivateKey(String filename) { try { return PemUtil.loadPrivateKey(new FileInputStream(filename)); } catch (FileNotFoundException e) { throw new ServiceException("私鑰文件不存在"); } } /** * 獲取簽名驗(yàn)證器 */ @Bean public Verifier getVerifier() { // 獲取商戶私鑰 final PrivateKey privateKey = getPrivateKey(privateKeyPath); // 私鑰簽名對(duì)象 PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKey); // 身份認(rèn)證對(duì)象 WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner); // 獲取證書管理器實(shí)例 CertificatesManager certificatesManager = CertificatesManager.getInstance(); try { // 向證書管理器增加需要自動(dòng)更新平臺(tái)證書的商戶信息 certificatesManager.putMerchant(mchId, wechatPay2Credentials, key.getBytes(StandardCharsets.UTF_8)); } catch (IOException | GeneralSecurityException | HttpCodeException e) { e.printStackTrace(); } try { return certificatesManager.getVerifier(mchId); } catch (NotFoundException e) { e.printStackTrace(); throw new ServiceException("獲取簽名驗(yàn)證器失敗"); } } /** * 獲取微信支付的遠(yuǎn)程請(qǐng)求對(duì)象 * @return Http請(qǐng)求對(duì)象 */ @Bean public CloseableHttpClient getWxPayClient() { // 獲取簽名驗(yàn)證器 Verifier verifier = getVerifier(); // 獲取商戶私鑰 final PrivateKey privateKey = getPrivateKey(privateKeyPath); WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create().withMerchant(mchId, mchSerialNo, privateKey) .withValidator(new WechatPay2Validator(verifier)); return builder.build(); }
3.請(qǐng)求地址枚舉類(WxApiConstants)
為了防止微信支付的請(qǐng)求地址前綴發(fā)生變化,因此請(qǐng)求前綴存儲(chǔ)在application.yml中,請(qǐng)求時(shí)進(jìn)行拼接即可。
import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.AllArgsConstructor; import lombok.Getter; //為了防止微信支付的請(qǐng)求地址前綴發(fā)生變化,因此請(qǐng)求前綴存儲(chǔ)在mysql,redis中,請(qǐng)求時(shí)進(jìn)行拼接即可。 @AllArgsConstructor @Getter @ApiModel("請(qǐng)求地址") public enum WxApiConstants { @ApiModelProperty("Native下單") NATIVE_PAY("/v3/pay/transactions/native"), @ApiModelProperty("jsapi下單") JSAPI_PAY("/v3/pay/transactions/jsapi"), @ApiModelProperty("jsapi下單") H5_PAY("/v3/pay/transactions/h5"), @ApiModelProperty("APP下單") APP_PAY("/v3/pay/transactions/app"), @ApiModelProperty("查詢訂單") ORDER_QUERY_BY_NO("/v3/pay/transactions/out-trade-no/%s"), @ApiModelProperty("關(guān)閉訂單") CLOSE_ORDER_BY_NO("/v3/pay/transactions/out-trade-no/%s/close"), @ApiModelProperty("申請(qǐng)退款") DOMESTIC_REFUNDS("/v3/refund/domestic/refunds"), @ApiModelProperty("查詢單筆退款") DOMESTIC_REFUNDS_QUERY("/v3/refund/domestic/refunds/%s"), @ApiModelProperty("申請(qǐng)交易賬單") TRADE_BILLS("/v3/bill/tradebill"), @ApiModelProperty("申請(qǐng)資金賬單") FUND_FLOW_BILLS("/v3/bill/fundflowbill"); @ApiModelProperty("類型") private final String type; }
4.回調(diào)地址枚舉類(WxChatBasePayDto)
發(fā)生請(qǐng)求后微信官方會(huì)回調(diào)我們傳遞的地址,這里通過(guò)枚舉統(tǒng)一管理我們的回調(diào)地址,回調(diào)地址由application.yml中的
weixin.notify-domain拼接組成。
import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.AllArgsConstructor; import lombok.Getter; @Getter @AllArgsConstructor @ApiModel("微信回調(diào)地址,根據(jù)自己的項(xiàng)目來(lái),不要直接照搬") public enum WxNotifyConstants { @ApiModelProperty("訂單支付通知") RUN_ERRANDS_NOTIFY("/wx/order/wxOrderCallBack"), @ApiModelProperty("卡支付成功通知") CAMPUS_CARD_NOTIFY("/wx/campusCardOrder/wxCampusCardOrderCallBack"), @ApiModelProperty("卡退款成功通知") CAMPUS_CARD_REFUND_NOTIFY("/wx/campusCardOrder/refundWechatCallback"); @ApiModelProperty("類型") private final String type; }
5.微信支付基礎(chǔ)請(qǐng)求數(shù)據(jù)對(duì)象(WxChatBasePayDto)
import com.xxx.project.wx.constants.WxNotifyConstants; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.math.BigDecimal; @Data @ApiModel("微信支付基礎(chǔ)請(qǐng)求數(shù)據(jù)對(duì)象") public class WxChatBasePayDto { @ApiModelProperty("商品描述") private String title; @ApiModelProperty("商家訂單號(hào),對(duì)應(yīng) out_trade_no") private String orderId; @ApiModelProperty("訂單金額") private BigDecimal price; @ApiModelProperty("回調(diào)地址") private WxNotifyConstants notify; @ApiModelProperty("支付用戶的openid") private String openId; }
6.將請(qǐng)求參數(shù)封裝成Map集合(WxPayCommon)
封裝完枚舉類后,首先就是請(qǐng)求參數(shù)的封裝,支付類請(qǐng)求參數(shù)都非常相近,我們將都需要的參數(shù)提取出來(lái)以map的方式進(jìn)行返回。這里的參數(shù),指每個(gè)支付類請(qǐng)求都用到的參數(shù),個(gè)別支付需要額外添加數(shù)據(jù)
import com.google.gson.Gson; import com.xxx.common.exception.ServiceException; import com.xxx.project.wx.constants.WxPayConstants; import com.xxx.project.wx.constants.WxApiConstants; import com.xxx.project.wx.dto.WxChatBasePayDto; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.util.EntityUtils; import java.io.IOException; import java.math.BigDecimal; import java.util.HashMap; import java.util.Map; @Slf4j public class WxPayCommon { /** * 封裝基礎(chǔ)通用請(qǐng)求數(shù)據(jù) * @param wxPayConfig 微信的配置文件 * @param basePayData 微信支付基礎(chǔ)請(qǐng)求數(shù)據(jù) * @return 封裝后的map對(duì)象 */ public static Map<String, Object> getBasePayParams(WxPayConstants wxPayConfig, WxChatBasePayDto basePayData) { Map<String, Object> paramsMap = new HashMap<>(); paramsMap.put("appid", wxPayConfig.getAppid()); paramsMap.put("mchid", wxPayConfig.getMchId()); // 如果商品名稱過(guò)長(zhǎng)則截取 String title = basePayData.getTitle().length() > 62 ? basePayData.getTitle().substring(0, 62) : basePayData.getTitle(); paramsMap.put("description",title); paramsMap.put("out_trade_no", basePayData.getOrderId()); paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(basePayData.getNotify().getType())); Map<String, Integer> amountMap = new HashMap<>(); amountMap.put("total", basePayData.getPrice().multiply(new BigDecimal("100")).intValue()); paramsMap.put("amount", amountMap); return paramsMap; } /** * 獲取請(qǐng)求對(duì)象(Post請(qǐng)求) * @param wxPayConfig 微信配置類 * @param apiType 接口請(qǐng)求地址 * @param paramsMap 請(qǐng)求參數(shù) * @return Post請(qǐng)求對(duì)象 */ public static HttpPost getHttpPost(WxPayConstants wxPayConfig, WxApiConstants apiType, Map<String, Object> paramsMap) { // 1.設(shè)置請(qǐng)求地址 HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(apiType.getType())); // 2.設(shè)置請(qǐng)求數(shù)據(jù) Gson gson = new Gson(); String jsonParams = gson.toJson(paramsMap); // 3.設(shè)置請(qǐng)求信息 StringEntity entity = new StringEntity(jsonParams, "utf-8"); entity.setContentType("application/json"); httpPost.setEntity(entity); httpPost.setHeader("Accept", "application/json"); return httpPost; } /** * 解析響應(yīng)數(shù)據(jù) * @param response 發(fā)送請(qǐng)求成功后,返回的數(shù)據(jù) * @return 微信返回的參數(shù) */ public static HashMap<String, String> resolverResponse(CloseableHttpResponse response) { try { // 1.獲取請(qǐng)求碼 int statusCode = response.getStatusLine().getStatusCode(); // 2.獲取返回值 String 格式 final String bodyAsString = EntityUtils.toString(response.getEntity()); Gson gson = new Gson(); if (statusCode == 200) { // 3.如果請(qǐng)求成功則解析成Map對(duì)象返回 HashMap<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class); return resultMap; } else { if (StringUtils.isNoneBlank(bodyAsString)) { log.error("微信支付請(qǐng)求失敗,提示信息:{}", bodyAsString); // 4.請(qǐng)求碼顯示失敗,則嘗試獲取提示信息 HashMap<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class); throw new ServiceException(resultMap.get("message")); } log.error("微信支付請(qǐng)求失敗,未查詢到原因,提示信息:{}", response); // 其他異常,微信也沒有返回?cái)?shù)據(jù),這就需要具體排查了 throw new IOException("request failed"); } } catch (Exception e) { e.printStackTrace(); throw new ServiceException(e.getMessage()); } finally { try { response.close(); } catch (IOException e) { e.printStackTrace(); } } } }
7.創(chuàng)建微信支付訂單的三種方式(Native,Jsapi,App)
商戶Native支付下單接口,微信后臺(tái)系統(tǒng)返回鏈接參數(shù)code_url,商戶后臺(tái)系統(tǒng)將code_url值生成二維碼圖片,用戶使用微信客戶端掃碼后發(fā)起支付,也就是說(shuō)后端只需要返回code_url即可。
官方JSAPI支付開發(fā)指引繼續(xù)在上方WxPayCommon中加入
/** * 創(chuàng)建微信支付訂單-Native方式 * * @param wxPayConfig 微信配置信息 * @param basePayData 基礎(chǔ)請(qǐng)求信息,商品標(biāo)題、商家訂單id、訂單價(jià)格 * @param wxPayClient 微信請(qǐng)求客戶端() * @return 微信支付二維碼地址 */ public static String wxNativePay(WxPayConstants wxPayConfig, WxChatBasePayDto basePayData, CloseableHttpClient wxPayClient) { // 1.獲取請(qǐng)求參數(shù)的Map格式 Map<String, Object> paramsMap = getBasePayParams(wxPayConfig, basePayData); // 2.獲取請(qǐng)求對(duì)象 HttpPost httpPost = getHttpPost(wxPayConfig, WxApiConstants.NATIVE_PAY, paramsMap); // 3.完成簽名并執(zhí)行請(qǐng)求 CloseableHttpResponse response = null; try { response = wxPayClient.execute(httpPost); } catch (IOException e) { e.printStackTrace(); throw new ServiceException("微信支付請(qǐng)求失敗"); } // 4.解析response對(duì)象 HashMap<String, String> resultMap = resolverResponse(response); if (resultMap != null) { // native請(qǐng)求返回的是二維碼鏈接,前端將鏈接轉(zhuǎn)換成二維碼即可 return resultMap.get("code_url"); } return null; } /** * 創(chuàng)建微信支付訂單-jsapi方式 * @param wxPayConfig 微信配置信息 * @param basePayData 基礎(chǔ)請(qǐng)求信息,商品標(biāo)題、商家訂單id、訂單價(jià)格 * @param openId 通過(guò)微信小程序或者公眾號(hào)獲取到用戶的openId * @param wxPayClient 微信請(qǐng)求客戶端() * @return 微信支付二維碼地址 */ public static String wxJsApiPay(WxPayConstants wxPayConfig, WxChatBasePayDto basePayData, String openId, CloseableHttpClient wxPayClient) { // 1.獲取請(qǐng)求參數(shù)的Map格式 Map<String, Object> paramsMap = getBasePayParams(wxPayConfig, basePayData); // 1.1 添加支付者信息 Map<String,String> payerMap = new HashMap(); payerMap.put("openid",openId); paramsMap.put("payer",payerMap); // 2.獲取請(qǐng)求對(duì)象 HttpPost httpPost = getHttpPost(wxPayConfig, WxApiConstants.JSAPI_PAY, paramsMap); // 3.完成簽名并執(zhí)行請(qǐng)求 CloseableHttpResponse response = null; try { response = wxPayClient.execute(httpPost); } catch (IOException e) { e.printStackTrace(); throw new ServiceException("微信支付請(qǐng)求失敗"); } // 4.解析response對(duì)象 HashMap<String, String> resultMap = resolverResponse(response); if (resultMap != null) { // native請(qǐng)求返回的是二維碼鏈接,前端將鏈接轉(zhuǎn)換成二維碼即可 return resultMap.get("prepay_id"); } return null; } /** * 創(chuàng)建微信支付訂單-APP方式 * * @param wxPayConfig 微信配置信息 * @param basePayData 基礎(chǔ)請(qǐng)求信息,商品標(biāo)題、商家訂單id、訂單價(jià)格 * @param wxPayClient 微信請(qǐng)求客戶端() * @return 微信支付二維碼地址 */ public static String wxAppPay(WxPayConstants wxPayConfig, WxChatBasePayDto basePayData, CloseableHttpClient wxPayClient) { // 1.獲取請(qǐng)求參數(shù)的Map格式 Map<String, Object> paramsMap = getBasePayParams(wxPayConfig, basePayData); // 2.獲取請(qǐng)求對(duì)象 HttpPost httpPost = getHttpPost(wxPayConfig, WxApiConstants.APP_PAY, paramsMap); // 3.完成簽名并執(zhí)行請(qǐng)求 CloseableHttpResponse response = null; try { response = wxPayClient.execute(httpPost); } catch (IOException e) { e.printStackTrace(); throw new ServiceException("微信支付請(qǐng)求失敗"); } // 4.解析response對(duì)象 HashMap<String, String> resultMap = resolverResponse(response); if (resultMap != null) { // native請(qǐng)求返回的是二維碼鏈接,前端將鏈接轉(zhuǎn)換成二維碼即可 return resultMap.get("prepay_id"); } return null; }
8.創(chuàng)建實(shí)體存儲(chǔ)前端微信支付所需參數(shù)(WxChatPayDto)
因?yàn)榍岸死鹞⑿胖Ц缎枰鄠€(gè)參數(shù),直接用一個(gè)實(shí)體返回更便捷
@Data @ApiModel("前端微信支付所需參數(shù)") public class WxChatPayDto { @ApiModelProperty("需要支付的小程序id") private String appid; @ApiModelProperty("時(shí)間戳(當(dāng)前的時(shí)間)") private String timeStamp; @ApiModelProperty("隨機(jī)字符串,不長(zhǎng)于32位。") private String nonceStr; @ApiModelProperty("小程序下單接口返回的prepay_id參數(shù)值,提交格式如:prepay_id=***") private String prepayId; @ApiModelProperty("簽名類型,默認(rèn)為RSA,僅支持RSA。") private String signType; @ApiModelProperty("簽名,使用字段appId、timeStamp、nonceStr、package計(jì)算得出的簽名值") private String paySign;
三、微信支付-調(diào)起微信支付
設(shè)置一個(gè)公共方法pay,每次支付可以直接調(diào)用
@Resource private WxPayConstants wxPayConfig; @Resource private CloseableHttpClient wxPayClient; /** * 微信用戶調(diào)用微信支付 */ @Override public WxChatPayDto pay(WxChatBasePayDto payData) { String prepayId = WxPayCommon.wxJsApiPay(wxPayConfig, payData, payData.getOpenId(), wxPayClient); WxChatPayDto wxChatPayDto = new WxChatPayDto(); wxChatPayDto.setAppid(redisCache.getCacheObject("WX_PAY_SAVE_WX_APPID")); wxChatPayDto.setTimeStamp(String.valueOf(System.currentTimeMillis() / 1000)); wxChatPayDto.setNonceStr(UUID.randomUUID().toString().replaceAll("-", "")); wxChatPayDto.setPrepayId("prepay_id=" + prepayId); wxChatPayDto.setSignType("RSA"); wxChatPayDto.setPaySign(getSign(wxChatPayDto.getNonceStr(),wxChatPayDto.getAppid(),wxChatPayDto.getPrepayId(),Long.parseLong(wxChatPayDto.getTimeStamp()))); return wxChatPayDto; } /** * 獲取簽名 * @param nonceStr 隨機(jī)數(shù) * @param appId 微信公眾號(hào)或者小程序等的appid * @param prepay_id 預(yù)支付交易會(huì)話ID * @param timestamp 時(shí)間戳 10位 * @return String 新簽名 */ String getSign(String nonceStr, String appId, String prepay_id, long timestamp) { //從下往上依次生成 String message = buildMessage(appId, timestamp, nonceStr, prepay_id); //簽名 try { return sign(message.getBytes("utf-8")); } catch (IOException e) { throw new RuntimeException("簽名異常,請(qǐng)檢查參數(shù)或商戶私鑰"); } } String sign(byte[] message) { try { //簽名方式 Signature sign = Signature.getInstance("SHA256withRSA"); //私鑰,通過(guò)MyPrivateKey來(lái)獲取,這是個(gè)靜態(tài)類可以接調(diào)用方法 ,需要的是_key.pem文件的絕對(duì)路徑配上文件名 sign.initSign(PemUtil.loadPrivateKey(new FileInputStream(redisCache.getCacheObject("WX_PAY_SAVE_PRIVATE_KEY_PATH").toString()))); sign.update(message); return Base64.getEncoder().encodeToString(sign.sign()); } catch (Exception e) { throw new RuntimeException("簽名異常,請(qǐng)檢查參數(shù)或商戶私鑰"); } } /** * 按照前端簽名文檔規(guī)范進(jìn)行排序,\n是換行 * * @param nonceStr 隨機(jī)數(shù) * @param appId 微信公眾號(hào)或者小程序等的appid * @param prepay_id 預(yù)支付交易會(huì)話ID * @param timestamp 時(shí)間戳 10位 * @return String 新簽名 */ String buildMessage(String appId, long timestamp, String nonceStr, String prepay_id) { return appId + "\n" + timestamp + "\n" + nonceStr + "\n" + prepay_id + "\n"; }
調(diào)用pay,獲取前端拉起支付所需要的參數(shù)
//支付 WxChatBasePayDto payData = new WxChatBasePayDto(); payData.setTitle("訂單支付"); payData.setOrderId(runOrder.getOrderNumber()); payData.setPrice(new BigDecimal(0.01)); payData.setNotify(WxNotifyConstants.RUN_ERRANDS_NOTIFY); payData.setOpenId(runOrder.getOpenId()); WxChatPayDto pay = pay(payData);
給前端直接返回pay這個(gè)參數(shù),前端通過(guò)里面的參數(shù)拉起支付
四、微信支付-成功回調(diào)
同樣的通知可能會(huì)多次發(fā)送給商戶系統(tǒng)。商戶系統(tǒng)必須能夠正確處理重復(fù)的通知。 推薦的做法是,當(dāng)商戶系統(tǒng)收到通知進(jìn)行處理時(shí),先檢查對(duì)應(yīng)業(yè)務(wù)數(shù)據(jù)的狀態(tài),并判斷該通知是否已經(jīng)處理。如果未處理,則再進(jìn)行處理;如果已處理,則直接返回結(jié)果成功。在對(duì)業(yè)務(wù)數(shù)據(jù)進(jìn)行狀態(tài)檢查和處理之前,要采用數(shù)據(jù)鎖進(jìn)行并發(fā)控制,以避免函數(shù)重入造成的數(shù)據(jù)混亂。
如果在所有通知頻率后沒有收到微信側(cè)回調(diào),商戶應(yīng)調(diào)用查詢訂單接口確認(rèn)訂單狀態(tài)。
特別提醒:商戶系統(tǒng)對(duì)于開啟結(jié)果通知的內(nèi)容一定要做簽名驗(yàn)證,并校驗(yàn)通知的信息是否與商戶側(cè)的信息一致,防止數(shù)據(jù)泄露導(dǎo)致出現(xiàn)“假通知”,造成資金損失。
該鏈接是通過(guò)基礎(chǔ)下單接口中的請(qǐng)求參數(shù)“notify_url”來(lái)設(shè)置的,要求必須為https地址。請(qǐng)確保回調(diào)URL是外部可正常訪問(wèn)的,且不能攜帶后綴參數(shù),否則可能導(dǎo)致商戶無(wú)法接收到微信的回調(diào)通知信息。
import com.alibaba.fastjson.JSONObject; import com.xxx.framework.redis.RedisCache; import com.wechat.pay.contrib.apache.httpclient.auth.Verifier; import com.wechat.pay.contrib.apache.httpclient.util.AesUtil; import org.springframework.stereotype.Component; import javax.annotation.Resource; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import java.security.GeneralSecurityException; import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; /** * 微信支付回調(diào)工具類 */ @Component public class WxPayCallbackUtil { @Resource private RedisCache redisCache; @Resource private Verifier verifier; /** * 獲取回調(diào)數(shù)據(jù) * @param request * @param response * @return */ public Map<String, String> wxChatPayCallback(HttpServletRequest request, HttpServletResponse response) { //獲取報(bào)文 String body = getRequestBody(request); //隨機(jī)串 String nonceStr = request.getHeader("Wechatpay-Nonce"); //微信傳遞過(guò)來(lái)的簽名 String signature = request.getHeader("Wechatpay-Signature"); //證書序列號(hào)(微信平臺(tái)) String serialNo = request.getHeader("Wechatpay-Serial"); //時(shí)間戳 String timestamp = request.getHeader("Wechatpay-Timestamp"); //構(gòu)造簽名串 應(yīng)答時(shí)間戳\n,應(yīng)答隨機(jī)串\n,應(yīng)答報(bào)文主體\n String signStr = Stream.of(timestamp, nonceStr, body).collect(Collectors.joining("\n", "", "\n")); Map<String, String> map = new HashMap<>(2); try { //驗(yàn)證簽名是否通過(guò) boolean result = verifiedSign(serialNo, signStr, signature); if(result){ //解密數(shù)據(jù) String plainBody = decryptBody(body); return convertWechatPayMsgToMap(plainBody); } } catch (Exception e) { e.printStackTrace(); } return map; } /** * 轉(zhuǎn)換body為map * @param plainBody * @return */ public Map<String,String> convertWechatPayMsgToMap(String plainBody){ Map<String,String> paramsMap = new HashMap<>(2); JSONObject jsonObject = JSONObject.parseObject(plainBody); //商戶訂單號(hào) paramsMap.put("out_trade_no",jsonObject.getString("out_trade_no")); //交易狀態(tài) paramsMap.put("trade_state",jsonObject.getString("trade_state")); //附加數(shù)據(jù) paramsMap.put("attach",jsonObject.getString("attach")); if (jsonObject.getJSONObject("attach") != null && !jsonObject.getJSONObject("attach").equals("")){ paramsMap.put("account_no",jsonObject.getJSONObject("attach").getString("accountNo")); } return paramsMap; } /** * 解密body的密文 * * "resource": { * "original_type": "transaction", * "algorithm": "AEAD_AES_256_GCM", * "ciphertext": "", * "associated_data": "", * "nonce": "" * } * * @param body * @return */ public String decryptBody(String body) throws UnsupportedEncodingException, GeneralSecurityException { AesUtil aesUtil = new AesUtil(redisCache.getCacheObject("WX_PAY_SAVE_KEY").toString().getBytes("utf-8")); JSONObject object = JSONObject.parseObject(body); JSONObject resource = object.getJSONObject("resource"); String ciphertext = resource.getString("ciphertext"); String associatedData = resource.getString("associated_data"); String nonce = resource.getString("nonce"); return aesUtil.decryptToString(associatedData.getBytes("utf-8"),nonce.getBytes("utf-8"),ciphertext); } /** * 驗(yàn)證簽名 * * @param serialNo 微信平臺(tái)-證書序列號(hào) * @param signStr 自己組裝的簽名串 * @param signature 微信返回的簽名 * @return * @throws UnsupportedEncodingException */ public boolean verifiedSign(String serialNo, String signStr, String signature) throws UnsupportedEncodingException { return verifier.verify(serialNo, signStr.getBytes("utf-8"), signature); } /** * 讀取請(qǐng)求數(shù)據(jù)流 * * @param request * @return */ public String getRequestBody(HttpServletRequest request) { StringBuffer sb = new StringBuffer(); try (ServletInputStream inputStream = request.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); ) { String line; while ((line = reader.readLine()) != null) { sb.append(line); } } catch (IOException e) { e.printStackTrace(); } return sb.toString(); } }
支付成功后的回調(diào)方法
/** * 訂單支付后回調(diào) */ @Override public Map<String, String> wxOrderCallBack(HttpServletRequest request, HttpServletResponse response) { Map<String, String> map = new HashMap<>(2); try { Map<String, String> stringMap = wxPayCallbackUtil.wxChatPayCallback(request, response); //支付成功 if (stringMap.get("trade_state").equals("SUCCESS")){ //編寫支付成功后邏輯 } //響應(yīng)微信 map.put("code", "SUCCESS"); map.put("message", "成功"); } catch (Exception e) { e.printStackTrace(); } return map; }
五、微信支付-申請(qǐng)退款
- 申請(qǐng)退款請(qǐng)求對(duì)象
微信支付訂單號(hào),微信支付訂單號(hào)和商家訂單號(hào)二選一,這個(gè)是必不可少的,原訂單金額也是必填的,微信會(huì)做二次驗(yàn)證。
@Data @ApiModel("微信退款對(duì)象") public class WxChatRefundDto { @ApiModelProperty("微信支付訂單號(hào),微信支付訂單號(hào)和商家訂單號(hào)二選一") private String transactionId; @ApiModelProperty("商家訂單號(hào),對(duì)應(yīng) out_trade_no") private String orderId; @ApiModelProperty("商戶退款單號(hào),對(duì)應(yīng)out_refund_no") private String refundOrderId; @ApiModelProperty("退款原因,選填") private String reason; @ApiModelProperty("回調(diào)地址") private WxNotifyConstants notify; @ApiModelProperty("退款金額") private BigDecimal refundMoney; @ApiModelProperty("原訂單金額,必填") private BigDecimal totalMoney;
- 將請(qǐng)求參數(shù)封裝成Map集合
import com.xxx.common.exception.ServiceException; import com.xxx.project.wx.constants.WxApiConstants; import com.xxx.project.wx.constants.WxPayConstants; import com.xxx.project.wx.dto.WxChatRefundDto; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.impl.client.CloseableHttpClient; import java.io.IOException; import java.util.HashMap; import java.util.Map; @Slf4j public class WxPayRefundUtil { /** * 封裝微信支付申請(qǐng)退款請(qǐng)求參數(shù) * @param dto 微信支付申請(qǐng)退款請(qǐng)求參數(shù) * @return 封裝后的map微信支付申請(qǐng)退款請(qǐng)求參數(shù)對(duì)象 */ private static Map<String, Object> getRefundParams(WxPayConstants wxPayConstants, WxChatRefundDto dto) { Map<String, Object> paramsMap = new HashMap<>(); if (StringUtils.isNoneBlank(dto.getTransactionId())) { paramsMap.put("transaction_id", dto.getTransactionId()); } else if (StringUtils.isNoneBlank(dto.getOrderId())) { paramsMap.put("out_trade_no", dto.getOrderId()); } else { throw new ServiceException("微信支付訂單號(hào)和商戶訂單號(hào)必須填寫一個(gè)"); } paramsMap.put("out_refund_no", dto.getRefundOrderId()); if (StringUtils.isNoneBlank(dto.getReason())) { paramsMap.put("reason", dto.getReason()); } paramsMap.put("notify_url", wxPayConstants.getNotifyDomain().concat(dto.getNotify().getType())); Map<String, Object> amountMap = new HashMap<>(); amountMap.put("refund", dto.getRefundMoney()); amountMap.put("total", dto.getTotalMoney()); amountMap.put("currency", "CNY"); paramsMap.put("amount", amountMap); return paramsMap; } /** * 發(fā)起微信退款申請(qǐng) * * @param wxPayConfig 微信配置信息 * @param param 微信支付申請(qǐng)退款請(qǐng)求參數(shù) * @param wxPayClient 微信請(qǐng)求客戶端() * @return 微信支付二維碼地址 */ public static String refundPay(WxPayConstants wxPayConfig, WxChatRefundDto param, CloseableHttpClient wxPayClient) { // 1.獲取請(qǐng)求參數(shù)的Map格式 Map<String, Object> paramsMap = getRefundParams(wxPayConfig, param); // 2.獲取請(qǐng)求對(duì)象 HttpPost httpPost = WxPayCommon.getHttpPost(wxPayConfig, WxApiConstants.DOMESTIC_REFUNDS, paramsMap); // 3.完成簽名并執(zhí)行請(qǐng)求 CloseableHttpResponse response = null; try { response = wxPayClient.execute(httpPost); } catch (IOException e) { e.printStackTrace(); throw new ServiceException("微信支付請(qǐng)求失敗"); } // 4.解析response對(duì)象 HashMap<String, String> resultMap = WxPayCommon.resolverResponse(response); log.info("發(fā)起退款參數(shù):{}",resultMap); if (resultMap != null) { // 返回微信支付退款單號(hào) return resultMap.get("refund_id"); } return null; } }
- 申請(qǐng)退款使用
@Resource private WxPayConstants wxPayConstants; @Resource private CloseableHttpClient closeableHttpClient; WxChatRefundDto dto = new WxChatRefundDto(); dto.setOrderId(); //訂單號(hào) String refundOrderId = IdWorker.getIdStr(); dto.setRefundOrderId(refundOrderId); dto.setNotify(WxNotifyConstants.CAMPUS_CARD_REFUND_NOTIFY); //回調(diào)地址 dto.setRefundMoney(new BigDecimal(1));//單位為分 dto.setTotalMoney(new BigDecimal(1));//單位為分 String s = WxPayRefundUtil.refundPay(wxPayConstants, dto, closeableHttpClient);
注意:
1、交易時(shí)間超過(guò)一年的訂單無(wú)法提交退款
2、微信支付退款支持單筆交易分多次退款(不超50次),多次退款需要提交原支付訂單的商戶訂單號(hào)和設(shè)置不同的退款單號(hào)。申請(qǐng)退款總>金額不能超過(guò)訂單金額。一筆退款失敗后重新提交,請(qǐng)不要更換退款單號(hào),請(qǐng)使用原商戶退款單號(hào)
3、錯(cuò)誤或無(wú)效請(qǐng)求頻率限制:6qps,即每秒鐘異?;蝈e(cuò)誤的退款申請(qǐng)請(qǐng)求不超過(guò)6次
4、每個(gè)支付訂單的部分退款次數(shù)不能超過(guò)50次
5、如果同一個(gè)用戶有多筆退款,建議分不同批次進(jìn)行退款,避免并發(fā)退款導(dǎo)致退款失敗
6、申請(qǐng)退款接口的返回僅代表業(yè)務(wù)的受理情況,具體退款是否成功,需要通過(guò)退款查詢接口獲取結(jié)果
7、一個(gè)月之前的訂單申請(qǐng)退款頻率限制為:5000/min
8、同一筆訂單多次退款的請(qǐng)求需相隔1分鐘
六、微信支付-退款成功回調(diào)
- 退款返回?cái)?shù)據(jù)對(duì)象
import cn.hutool.core.date.DateUtil; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.math.BigDecimal; import java.util.Date; @Data @ApiModel("微信退款回調(diào)參數(shù)") public class WxChatCallbackRefundDto { @ApiModelProperty("商戶訂單號(hào)") private String orderId; @ApiModelProperty("商戶退款單號(hào),out_refund_no") private String refundId; @ApiModelProperty("微信支付系統(tǒng)生成的訂單號(hào)") private String transactionId; @ApiModelProperty("微信支付系統(tǒng)生成的退款訂單號(hào)") private String transactionRefundId; @ApiModelProperty("退款渠道 1.ORIGINAL:原路退款 2.BALANCE:退回到余額 " + "3.OTHER_BALANCE:原賬戶異常退到其他余額賬戶 4.OTHER_BANKCARD:原銀行卡異常退到其他銀行卡") private String channel; @ApiModelProperty("退款成功時(shí)間 當(dāng)前退款成功時(shí)才有此返回值") private Date successTime; @ApiModelProperty("退款狀態(tài) 退款到銀行發(fā)現(xiàn)用戶的卡作廢或者凍結(jié)了,導(dǎo)致原路退款銀行卡失敗,可前往商戶平臺(tái)-交易中心,手動(dòng)處理此筆退款。" + "1.SUCCESS:退款成功 2.CLOSED:退款關(guān)閉 3.PROCESSING:退款處理中 4.ABNORMAL:退款異常") private String status; @ApiModelProperty("退款金額") private BigDecimal refundMoney; public Date getSuccessTime() { return successTime; } public void setSuccessTime(String successTime) { // Hutool工具包的方法,自動(dòng)識(shí)別一些常用格式的日期字符串 this.successTime = DateUtil.parse(successTime); } }
- 退款業(yè)務(wù)處理接口
import com.xxx.project.wx.dto.WxChatCallbackRefundDto; /** * 退款處理接口,為了防止項(xiàng)目開發(fā)人員,不手動(dòng)判斷退款失敗的情況 * 退款失敗:退款到銀行發(fā)現(xiàn)用戶的卡作廢或者凍結(jié)了,導(dǎo)致原路退款銀行卡失敗,可前往商戶平臺(tái)-交易中心,手動(dòng)處理此筆退款 */ public interface WechatRefundCallback { /** * 退款成功處理情況 */ void success(WxChatCallbackRefundDto refundData); /** * 退款失敗處理情況 */ void fail(WxChatCallbackRefundDto refundData); }
- 微信退款回調(diào)方法
public class HttpUtils{ /** * 將通知參數(shù)轉(zhuǎn)化為字符串 * @param request * @return */ public static String readData(HttpServletRequest request) { BufferedReader br = null; try { StringBuilder result = new StringBuilder(); br = request.getReader(); for (String line; (line = br.readLine()) != null; ) { if (result.length() > 0) { result.append("\n"); } result.append(line); } return result.toString(); } catch (IOException e) { throw new RuntimeException(e); } finally { if (br != null) { try { br.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
package com.runerrands.project.wx.util; import com.wechat.pay.contrib.apache.httpclient.auth.Verifier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.time.DateTimeException; import java.time.Duration; import java.time.Instant; import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.*; public class WechatPayValidatorForRequest { protected static final Logger log = LoggerFactory.getLogger(WechatPayValidatorForRequest.class); /** * 應(yīng)答超時(shí)時(shí)間,單位為分鐘 */ protected static final long RESPONSE_EXPIRED_MINUTES = 5; protected final Verifier verifier; protected final String body; protected final String requestId; public WechatPayValidatorForRequest(Verifier verifier, String body, String requestId) { this.verifier = verifier; this.body = body; this.requestId = requestId; } protected static IllegalArgumentException parameterError(String message, Object... args) { message = String.format(message, args); return new IllegalArgumentException("parameter error: " + message); } protected static IllegalArgumentException verifyFail(String message, Object... args) { message = String.format(message, args); return new IllegalArgumentException("signature verify fail: " + message); } public final boolean validate(HttpServletRequest request) throws IOException { try { validateParameters(request); String message = buildMessage(request); String serial = request.getHeader(WECHAT_PAY_SERIAL); String signature = request.getHeader(WECHAT_PAY_SIGNATURE); if (!verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature)) { throw verifyFail("serial=[%s] message=[%s] sign=[%s], request-id=[%s]", serial, message, signature, request.getHeader(REQUEST_ID)); } } catch (IllegalArgumentException e) { log.warn(e.getMessage()); return false; } return true; } protected final void validateParameters(HttpServletRequest request) { // NOTE: ensure HEADER_WECHAT_PAY_TIMESTAMP at last String[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP}; String header = null; for (String headerName : headers) { header = request.getHeader(headerName); if (header == null) { throw parameterError("empty [%s], request-id=[%s]", headerName, requestId); } } String timestampStr = header; try { Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestampStr)); // 拒絕過(guò)期應(yīng)答 if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= RESPONSE_EXPIRED_MINUTES) { throw parameterError("timestamp=[%s] expires, request-id=[%s]", timestampStr, requestId); } } catch (DateTimeException | NumberFormatException e) { throw parameterError("invalid timestamp=[%s], request-id=[%s]", timestampStr, requestId); } } protected final String buildMessage(HttpServletRequest request) throws IOException { String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP); String nonce = request.getHeader(WECHAT_PAY_NONCE); return timestamp + "\n" + nonce + "\n" + body + "\n"; } }
import com.google.gson.Gson; import com.xxx.common.exception.ServiceException; import com.xxx.common.utils.http.HttpUtils; import com.xxx.project.wx.constants.WxPayConstants; import com.xxx.project.wx.dto.WxChatCallbackRefundDto; import com.xxx.project.wx.service.WechatRefundCallback; import com.wechat.pay.contrib.apache.httpclient.auth.Verifier; import com.wechat.pay.contrib.apache.httpclient.util.AesUtil; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.math.BigDecimal; import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.util.HashMap; import java.util.Map; @Slf4j public class WxPayRefundCallbackUtil { /** * 微信支付申請(qǐng)退款回調(diào)方法 * * @param verifier 證書 * @param wxPayConfig 微信配置 * @param refundCallback 回調(diào)方法,用于處理業(yè)務(wù)邏輯,包含退款成功處理于退款失敗處理 * @return json格式的string數(shù)據(jù),直接返回給微信 */ public static String wxPayRefundCallback(HttpServletRequest request, HttpServletResponse response, Verifier verifier, WxPayConstants wxPayConfig, WechatRefundCallback refundCallback) { Gson gson = new Gson(); // 1.處理通知參數(shù) final String body = HttpUtils.readData(request); HashMap<String, Object> bodyMap = gson.fromJson(body, HashMap.class); // 2.簽名驗(yàn)證 WechatPayValidatorForRequest wechatForRequest = new WechatPayValidatorForRequest(verifier, body, (String) bodyMap.get("id")); try { if (!wechatForRequest.validate(request)) { // 通知驗(yàn)簽失敗 response.setStatus(500); final HashMap<String, Object> map = new HashMap<>(); map.put("code", "ERROR"); map.put("message", "通知驗(yàn)簽失敗"); return gson.toJson(map); } } catch (Exception e) { e.printStackTrace(); } // 3.獲取明文數(shù)據(jù) String plainText = decryptFromResource(bodyMap, wxPayConfig); HashMap<String, Object> plainTextMap = gson.fromJson(plainText, HashMap.class); // log.info("退款plainTextMap:{}", plainTextMap); // 4.封裝微信返回的數(shù)據(jù) WxChatCallbackRefundDto refundData = getRefundCallbackData(plainTextMap); if ("SUCCESS".equals(refundData.getStatus())) { // 執(zhí)行業(yè)務(wù)邏輯 refundCallback.success(refundData); } else { // 特殊情況退款失敗業(yè)務(wù)處理,退款到銀行發(fā)現(xiàn)用戶的卡作廢或者凍結(jié)了,導(dǎo)致原路退款銀行卡失敗,可前往商戶平臺(tái)-交易中心,手動(dòng)處理此筆退款 refundCallback.fail(refundData); } // 5.成功應(yīng)答 response.setStatus(200); final HashMap<String, Object> resultMap = new HashMap<>(); resultMap.put("code", "SUCCESS"); resultMap.put("message", "成功"); return gson.toJson(resultMap); } private static WxChatCallbackRefundDto getRefundCallbackData(HashMap<String, Object> plainTextMap) { Gson gson = new Gson(); WxChatCallbackRefundDto refundData = new WxChatCallbackRefundDto(); String successTime = String.valueOf(plainTextMap.get("success_time")); if (StringUtils.isNoneBlank(successTime)) { refundData.setSuccessTime(successTime); } refundData.setOrderId(String.valueOf(plainTextMap.get("out_trade_no"))); refundData.setRefundId(String.valueOf(plainTextMap.get("out_refund_no"))); refundData.setTransactionId(String.valueOf(plainTextMap.get("transaction_id"))); refundData.setTransactionRefundId(String.valueOf(plainTextMap.get("refund_id"))); refundData.setChannel(String.valueOf(plainTextMap.get("channel"))); final String status = String.valueOf(plainTextMap.get("refund_status")); refundData.setStatus(status); String amount = String.valueOf(plainTextMap.get("amount")); HashMap<String, Object> amountMap = gson.fromJson(amount, HashMap.class); String refundMoney = String.valueOf(amountMap.get("refund")); refundData.setRefundMoney(new BigDecimal(refundMoney).movePointLeft(2)); // log.info("refundData:{}", refundData); return refundData; } /** * 對(duì)稱解密 */ private static String decryptFromResource(HashMap<String, Object> bodyMap, WxPayConstants wxPayConfig) { // 通知數(shù)據(jù) Map<String, String> resourceMap = (Map) bodyMap.get("resource"); // 數(shù)據(jù)密文 String ciphertext = resourceMap.get("ciphertext"); // 隨機(jī)串 String nonce = resourceMap.get("nonce"); // 附加數(shù)據(jù) String associateData = resourceMap.get("associated_data"); AesUtil aesUtil = new AesUtil(wxPayConfig.getKey().getBytes(StandardCharsets.UTF_8)); try { return aesUtil.decryptToString(associateData.getBytes(StandardCharsets.UTF_8), nonce.getBytes(StandardCharsets.UTF_8), ciphertext); } catch (GeneralSecurityException e) { e.printStackTrace(); throw new ServiceException("解密失敗"); } } }
- 回調(diào)方法
@Resource private WxPayConstants wxPayConfig; @Resource private Verifier verifier; @ApiOperation("微信退款回調(diào)接口") @PostMapping("/refundWechatCallback") public String refundWechatCallback(HttpServletRequest request, HttpServletResponse response) { return WxPayRefundCallbackUtil.wxPayRefundCallback(request, response, verifier, wxPayConfig, new WechatRefundCallback() { @Override public void success(WxChatCallbackRefundDto refundData) { // TODO 退款成功的業(yè)務(wù)邏輯,例如更改訂單狀態(tài)為退款成功等 System.out.println("退款成功"); } @Override public void fail(WxChatCallbackRefundDto refundData) { // TODO 特殊情況下退款失敗業(yè)務(wù)處理,例如銀行卡凍結(jié)需要人工退款,此時(shí)可以郵件或短信提醒管理員,并攜帶退款單號(hào)等關(guān)鍵信息 System.out.println("退款失敗"); } }); }
到此這篇關(guān)于SpringBoot整合微信小程序支付V3(支付退款)的文章就介紹到這了,更多相關(guān)SpringBoot整合小程序支付V3內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
使用Idea或Datagrip導(dǎo)入excel數(shù)據(jù)的方法
這篇文章主要介紹了使用Idea或Datagrip導(dǎo)入excel數(shù)據(jù)的方法,本文通過(guò)圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-11-11Java連接mysql數(shù)據(jù)庫(kù)的詳細(xì)教程(推薦)
這篇文章主要介紹了Java連接mysql數(shù)據(jù)庫(kù)的詳細(xì)教程,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-08-08elasticsearch的靈魂唯一master選舉機(jī)制原理分析
這篇文章主要為大家介紹了elasticsearch的靈魂唯一master選舉機(jī)制原理分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-04-04Java利用Picocli開發(fā)一個(gè)簡(jiǎn)化命令行工具
Picocli 是一個(gè)強(qiáng)大、易用且功能豐富的 Java 庫(kù),用于開發(fā)命令行工具,本文我們就來(lái)為大家介紹一下Java如何利用Picocli進(jìn)行命令行簡(jiǎn)化功能的吧2025-03-03Spring事件監(jiān)聽機(jī)制使用和原理示例講解
Spring事件監(jiān)聽機(jī)制是一個(gè)很不錯(cuò)的功能,我們?cè)谶M(jìn)行業(yè)務(wù)開發(fā)的時(shí)候可以引入,在相關(guān)的開源框架中也是用它的身影,比如高性能網(wǎng)關(guān)ShenYu中就使用了Spring事件監(jiān)聽機(jī)制來(lái)發(fā)布網(wǎng)關(guān)的更新數(shù)據(jù),它可以降低系統(tǒng)的耦合性,使系統(tǒng)的擴(kuò)展性更好2023-06-06SpringBoot 如何添加容器啟動(dòng)的初始化邏輯的操作方法
這篇文章主要介紹了SpringBoot 如何添加容器啟動(dòng)的初始化邏輯,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-09-09Spring?IOC容器基于XML外部屬性文件的Bean管理
這篇文章主要為大家介紹了Spring?IOC容器Bean管理XML外部屬性文件,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-05-05