SpringBoot整合微信小程序支付V3(支付退款)
一、微信支付-準(zhǔn)備工作
微信支付開發(fā)前,需要先獲取商家信息,包括商戶號、AppId、證書和密鑰。
獲取商戶號
微信商戶平臺 申請成為商戶 => 提交資料 => 簽署協(xié)議 => 獲取商戶號獲取AppID
微信公眾平臺 注冊服務(wù)號 => 服務(wù)號認證 => 獲取APPID => 綁定商戶號申請商戶證書
登錄商戶平臺 => 選擇 賬戶中心 => 安全中心 => API安全 => 申請API證書 包括商戶證書和商戶私鑰獲取微信的證書
獲取APIv3秘鑰(在微信支付回調(diào)通知和商戶獲取平臺證書使用APIv3密鑰)
登錄商戶平臺 => 選擇 賬戶中心 => 安全中心 => 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.配置商戶信息、證書、密鑰等。將客戶端對象構(gòu)建到Bean中,方便后續(xù)使用。
可以采用兩種方式
①.配置application.yml
weixin: appid: wx*************acx # appid mch-serial-no: 3FB18E2*******0127B3*****0053E2 # 證書序列號 private-key-path: D:\wx\pem\apiclient_key.pem # 證書路徑 mch-id: 16*****801 # 商戶號 key: F8CDeHBc***********2t5nvVeh1 # api秘鑰 domain: https://api.mch.weixin.qq.com # 微信服務(wù)器地址 notify-domain: https://xw666.mynatapp.cc # 回調(diào),自己的回調(diào)地址
然后創(chuàng)建對應(yīng)的實體
@Configuration
@PropertySource("classpath:application.yml") //讀取配置文件
@ConfigurationProperties(prefix = "weixin") //讀取wxpay節(jié)點
@Data //使用set方法將wxpay節(jié)點中的值填充到當(dāng)前類的屬性中
@ApiModel("微信支付靜態(tài)常量類")
public class WxPayConfig {
@ApiModelProperty("商戶號")
private String mchId;
@ApiModelProperty("商戶API證書序列號")
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ù)庫存儲,在項目啟動時加載到redis中,然后從redis中獲取
@Configuration
@Data
@ApiModel("微信支付靜態(tài)常量類")
public class WxPayConstants {
@Resource
private RedisCache redisCache;
@ApiModelProperty("APPID")
public String appid;
@ApiModelProperty("商戶API證書序列號")
public String mchSerialNo;
@ApiModelProperty("商戶私鑰文件")
public String privateKeyPath;
@ApiModelProperty("商戶號")
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");
}
}這兩個實體有幾個共同的方法,無論使用哪一個,放到下面即可
/**
* 獲取商戶的私鑰文件
*
* @param filename 證書地址
* @return 私鑰文件
*/
public PrivateKey getPrivateKey(String filename) {
try {
return PemUtil.loadPrivateKey(new FileInputStream(filename));
} catch (FileNotFoundException e) {
throw new ServiceException("私鑰文件不存在");
}
}
/**
* 獲取簽名驗證器
*/
@Bean
public Verifier getVerifier() {
// 獲取商戶私鑰
final PrivateKey privateKey = getPrivateKey(privateKeyPath);
// 私鑰簽名對象
PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKey);
// 身份認證對象
WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);
// 獲取證書管理器實例
CertificatesManager certificatesManager = CertificatesManager.getInstance();
try {
// 向證書管理器增加需要自動更新平臺證書的商戶信息
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("獲取簽名驗證器失敗");
}
}
/**
* 獲取微信支付的遠程請求對象
* @return Http請求對象
*/
@Bean
public CloseableHttpClient getWxPayClient() {
// 獲取簽名驗證器
Verifier verifier = getVerifier();
// 獲取商戶私鑰
final PrivateKey privateKey = getPrivateKey(privateKeyPath);
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create().withMerchant(mchId, mchSerialNo, privateKey)
.withValidator(new WechatPay2Validator(verifier));
return builder.build();
}3.請求地址枚舉類(WxApiConstants)
為了防止微信支付的請求地址前綴發(fā)生變化,因此請求前綴存儲在application.yml中,請求時進行拼接即可。
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Getter;
//為了防止微信支付的請求地址前綴發(fā)生變化,因此請求前綴存儲在mysql,redis中,請求時進行拼接即可。
@AllArgsConstructor
@Getter
@ApiModel("請求地址")
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("申請退款")
DOMESTIC_REFUNDS("/v3/refund/domestic/refunds"),
@ApiModelProperty("查詢單筆退款")
DOMESTIC_REFUNDS_QUERY("/v3/refund/domestic/refunds/%s"),
@ApiModelProperty("申請交易賬單")
TRADE_BILLS("/v3/bill/tradebill"),
@ApiModelProperty("申請資金賬單")
FUND_FLOW_BILLS("/v3/bill/fundflowbill");
@ApiModelProperty("類型")
private final String type;
}4.回調(diào)地址枚舉類(WxChatBasePayDto)
發(fā)生請求后微信官方會回調(diào)我們傳遞的地址,這里通過枚舉統(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ù)自己的項目來,不要直接照搬")
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ǔ)請求數(shù)據(jù)對象(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ǔ)請求數(shù)據(jù)對象")
public class WxChatBasePayDto {
@ApiModelProperty("商品描述")
private String title;
@ApiModelProperty("商家訂單號,對應(yīng) out_trade_no")
private String orderId;
@ApiModelProperty("訂單金額")
private BigDecimal price;
@ApiModelProperty("回調(diào)地址")
private WxNotifyConstants notify;
@ApiModelProperty("支付用戶的openid")
private String openId;
}6.將請求參數(shù)封裝成Map集合(WxPayCommon)
封裝完枚舉類后,首先就是請求參數(shù)的封裝,支付類請求參數(shù)都非常相近,我們將都需要的參數(shù)提取出來以map的方式進行返回。這里的參數(shù),指每個支付類請求都用到的參數(shù),個別支付需要額外添加數(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ǔ)通用請求數(shù)據(jù)
* @param wxPayConfig 微信的配置文件
* @param basePayData 微信支付基礎(chǔ)請求數(shù)據(jù)
* @return 封裝后的map對象
*/
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());
// 如果商品名稱過長則截取
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;
}
/**
* 獲取請求對象(Post請求)
* @param wxPayConfig 微信配置類
* @param apiType 接口請求地址
* @param paramsMap 請求參數(shù)
* @return Post請求對象
*/
public static HttpPost getHttpPost(WxPayConstants wxPayConfig, WxApiConstants apiType, Map<String, Object> paramsMap) {
// 1.設(shè)置請求地址
HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(apiType.getType()));
// 2.設(shè)置請求數(shù)據(jù)
Gson gson = new Gson();
String jsonParams = gson.toJson(paramsMap);
// 3.設(shè)置請求信息
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ā)送請求成功后,返回的數(shù)據(jù)
* @return 微信返回的參數(shù)
*/
public static HashMap<String, String> resolverResponse(CloseableHttpResponse response) {
try {
// 1.獲取請求碼
int statusCode = response.getStatusLine().getStatusCode();
// 2.獲取返回值 String 格式
final String bodyAsString = EntityUtils.toString(response.getEntity());
Gson gson = new Gson();
if (statusCode == 200) {
// 3.如果請求成功則解析成Map對象返回
HashMap<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
return resultMap;
} else {
if (StringUtils.isNoneBlank(bodyAsString)) {
log.error("微信支付請求失敗,提示信息:{}", bodyAsString);
// 4.請求碼顯示失敗,則嘗試獲取提示信息
HashMap<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
throw new ServiceException(resultMap.get("message"));
}
log.error("微信支付請求失敗,未查詢到原因,提示信息:{}", response);
// 其他異常,微信也沒有返回數(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ǒng)返回鏈接參數(shù)code_url,商戶后臺系統(tǒng)將code_url值生成二維碼圖片,用戶使用微信客戶端掃碼后發(fā)起支付,也就是說后端只需要返回code_url即可。
官方JSAPI支付開發(fā)指引繼續(xù)在上方WxPayCommon中加入
/**
* 創(chuàng)建微信支付訂單-Native方式
*
* @param wxPayConfig 微信配置信息
* @param basePayData 基礎(chǔ)請求信息,商品標(biāo)題、商家訂單id、訂單價格
* @param wxPayClient 微信請求客戶端()
* @return 微信支付二維碼地址
*/
public static String wxNativePay(WxPayConstants wxPayConfig, WxChatBasePayDto basePayData, CloseableHttpClient wxPayClient) {
// 1.獲取請求參數(shù)的Map格式
Map<String, Object> paramsMap = getBasePayParams(wxPayConfig, basePayData);
// 2.獲取請求對象
HttpPost httpPost = getHttpPost(wxPayConfig, WxApiConstants.NATIVE_PAY, paramsMap);
// 3.完成簽名并執(zhí)行請求
CloseableHttpResponse response = null;
try {
response = wxPayClient.execute(httpPost);
} catch (IOException e) {
e.printStackTrace();
throw new ServiceException("微信支付請求失敗");
}
// 4.解析response對象
HashMap<String, String> resultMap = resolverResponse(response);
if (resultMap != null) {
// native請求返回的是二維碼鏈接,前端將鏈接轉(zhuǎn)換成二維碼即可
return resultMap.get("code_url");
}
return null;
}
/**
* 創(chuàng)建微信支付訂單-jsapi方式
* @param wxPayConfig 微信配置信息
* @param basePayData 基礎(chǔ)請求信息,商品標(biāo)題、商家訂單id、訂單價格
* @param openId 通過微信小程序或者公眾號獲取到用戶的openId
* @param wxPayClient 微信請求客戶端()
* @return 微信支付二維碼地址
*/
public static String wxJsApiPay(WxPayConstants wxPayConfig, WxChatBasePayDto basePayData, String openId, CloseableHttpClient wxPayClient) {
// 1.獲取請求參數(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.獲取請求對象
HttpPost httpPost = getHttpPost(wxPayConfig, WxApiConstants.JSAPI_PAY, paramsMap);
// 3.完成簽名并執(zhí)行請求
CloseableHttpResponse response = null;
try {
response = wxPayClient.execute(httpPost);
} catch (IOException e) {
e.printStackTrace();
throw new ServiceException("微信支付請求失敗");
}
// 4.解析response對象
HashMap<String, String> resultMap = resolverResponse(response);
if (resultMap != null) {
// native請求返回的是二維碼鏈接,前端將鏈接轉(zhuǎn)換成二維碼即可
return resultMap.get("prepay_id");
}
return null;
}
/**
* 創(chuàng)建微信支付訂單-APP方式
*
* @param wxPayConfig 微信配置信息
* @param basePayData 基礎(chǔ)請求信息,商品標(biāo)題、商家訂單id、訂單價格
* @param wxPayClient 微信請求客戶端()
* @return 微信支付二維碼地址
*/
public static String wxAppPay(WxPayConstants wxPayConfig, WxChatBasePayDto basePayData, CloseableHttpClient wxPayClient) {
// 1.獲取請求參數(shù)的Map格式
Map<String, Object> paramsMap = getBasePayParams(wxPayConfig, basePayData);
// 2.獲取請求對象
HttpPost httpPost = getHttpPost(wxPayConfig, WxApiConstants.APP_PAY, paramsMap);
// 3.完成簽名并執(zhí)行請求
CloseableHttpResponse response = null;
try {
response = wxPayClient.execute(httpPost);
} catch (IOException e) {
e.printStackTrace();
throw new ServiceException("微信支付請求失敗");
}
// 4.解析response對象
HashMap<String, String> resultMap = resolverResponse(response);
if (resultMap != null) {
// native請求返回的是二維碼鏈接,前端將鏈接轉(zhuǎn)換成二維碼即可
return resultMap.get("prepay_id");
}
return null;
}8.創(chuàng)建實體存儲前端微信支付所需參數(shù)(WxChatPayDto)
因為前端拉起微信支付需要多個參數(shù),直接用一個實體返回更便捷
@Data
@ApiModel("前端微信支付所需參數(shù)")
public class WxChatPayDto {
@ApiModelProperty("需要支付的小程序id")
private String appid;
@ApiModelProperty("時間戳(當(dāng)前的時間)")
private String timeStamp;
@ApiModelProperty("隨機字符串,不長于32位。")
private String nonceStr;
@ApiModelProperty("小程序下單接口返回的prepay_id參數(shù)值,提交格式如:prepay_id=***")
private String prepayId;
@ApiModelProperty("簽名類型,默認為RSA,僅支持RSA。")
private String signType;
@ApiModelProperty("簽名,使用字段appId、timeStamp、nonceStr、package計算得出的簽名值")
private String paySign;三、微信支付-調(diào)起微信支付
設(shè)置一個公共方法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 隨機數(shù)
* @param appId 微信公眾號或者小程序等的appid
* @param prepay_id 預(yù)支付交易會話ID
* @param timestamp 時間戳 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("簽名異常,請檢查參數(shù)或商戶私鑰");
}
}
String sign(byte[] message) {
try {
//簽名方式
Signature sign = Signature.getInstance("SHA256withRSA");
//私鑰,通過MyPrivateKey來獲取,這是個靜態(tài)類可以接調(diào)用方法 ,需要的是_key.pem文件的絕對路徑配上文件名
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("簽名異常,請檢查參數(shù)或商戶私鑰");
}
}
/**
* 按照前端簽名文檔規(guī)范進行排序,\n是換行
*
* @param nonceStr 隨機數(shù)
* @param appId 微信公眾號或者小程序等的appid
* @param prepay_id 預(yù)支付交易會話ID
* @param timestamp 時間戳 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這個參數(shù),前端通過里面的參數(shù)拉起支付
四、微信支付-成功回調(diào)
同樣的通知可能會多次發(fā)送給商戶系統(tǒng)。商戶系統(tǒng)必須能夠正確處理重復(fù)的通知。 推薦的做法是,當(dāng)商戶系統(tǒng)收到通知進行處理時,先檢查對應(yīng)業(yè)務(wù)數(shù)據(jù)的狀態(tài),并判斷該通知是否已經(jīng)處理。如果未處理,則再進行處理;如果已處理,則直接返回結(jié)果成功。在對業(yè)務(wù)數(shù)據(jù)進行狀態(tài)檢查和處理之前,要采用數(shù)據(jù)鎖進行并發(fā)控制,以避免函數(shù)重入造成的數(shù)據(jù)混亂。
如果在所有通知頻率后沒有收到微信側(cè)回調(diào),商戶應(yīng)調(diào)用查詢訂單接口確認訂單狀態(tài)。
特別提醒:商戶系統(tǒng)對于開啟結(jié)果通知的內(nèi)容一定要做簽名驗證,并校驗通知的信息是否與商戶側(cè)的信息一致,防止數(shù)據(jù)泄露導(dǎo)致出現(xiàn)“假通知”,造成資金損失。
該鏈接是通過基礎(chǔ)下單接口中的請求參數(shù)“notify_url”來設(shè)置的,要求必須為https地址。請確?;卣{(diào)URL是外部可正常訪問的,且不能攜帶后綴參數(shù),否則可能導(dǎo)致商戶無法接收到微信的回調(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) {
//獲取報文
String body = getRequestBody(request);
//隨機串
String nonceStr = request.getHeader("Wechatpay-Nonce");
//微信傳遞過來的簽名
String signature = request.getHeader("Wechatpay-Signature");
//證書序列號(微信平臺)
String serialNo = request.getHeader("Wechatpay-Serial");
//時間戳
String timestamp = request.getHeader("Wechatpay-Timestamp");
//構(gòu)造簽名串 應(yīng)答時間戳\n,應(yīng)答隨機串\n,應(yīng)答報文主體\n
String signStr = Stream.of(timestamp, nonceStr, body).collect(Collectors.joining("\n", "", "\n"));
Map<String, String> map = new HashMap<>(2);
try {
//驗證簽名是否通過
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);
//商戶訂單號
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);
}
/**
* 驗證簽名
*
* @param serialNo 微信平臺-證書序列號
* @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);
}
/**
* 讀取請求數(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;
}五、微信支付-申請退款
- 申請退款請求對象
微信支付訂單號,微信支付訂單號和商家訂單號二選一,這個是必不可少的,原訂單金額也是必填的,微信會做二次驗證。
@Data
@ApiModel("微信退款對象")
public class WxChatRefundDto {
@ApiModelProperty("微信支付訂單號,微信支付訂單號和商家訂單號二選一")
private String transactionId;
@ApiModelProperty("商家訂單號,對應(yīng) out_trade_no")
private String orderId;
@ApiModelProperty("商戶退款單號,對應(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;- 將請求參數(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 {
/**
* 封裝微信支付申請退款請求參數(shù)
* @param dto 微信支付申請退款請求參數(shù)
* @return 封裝后的map微信支付申請退款請求參數(shù)對象
*/
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("微信支付訂單號和商戶訂單號必須填寫一個");
}
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ā)起微信退款申請
*
* @param wxPayConfig 微信配置信息
* @param param 微信支付申請退款請求參數(shù)
* @param wxPayClient 微信請求客戶端()
* @return 微信支付二維碼地址
*/
public static String refundPay(WxPayConstants wxPayConfig, WxChatRefundDto param, CloseableHttpClient wxPayClient) {
// 1.獲取請求參數(shù)的Map格式
Map<String, Object> paramsMap = getRefundParams(wxPayConfig, param);
// 2.獲取請求對象
HttpPost httpPost = WxPayCommon.getHttpPost(wxPayConfig, WxApiConstants.DOMESTIC_REFUNDS, paramsMap);
// 3.完成簽名并執(zhí)行請求
CloseableHttpResponse response = null;
try {
response = wxPayClient.execute(httpPost);
} catch (IOException e) {
e.printStackTrace();
throw new ServiceException("微信支付請求失敗");
}
// 4.解析response對象
HashMap<String, String> resultMap = WxPayCommon.resolverResponse(response);
log.info("發(fā)起退款參數(shù):{}",resultMap);
if (resultMap != null) {
// 返回微信支付退款單號
return resultMap.get("refund_id");
}
return null;
}
}- 申請退款使用
@Resource private WxPayConstants wxPayConstants; @Resource private CloseableHttpClient closeableHttpClient; WxChatRefundDto dto = new WxChatRefundDto(); dto.setOrderId(); //訂單號 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、交易時間超過一年的訂單無法提交退款
2、微信支付退款支持單筆交易分多次退款(不超50次),多次退款需要提交原支付訂單的商戶訂單號和設(shè)置不同的退款單號。申請退款總>金額不能超過訂單金額。一筆退款失敗后重新提交,請不要更換退款單號,請使用原商戶退款單號
3、錯誤或無效請求頻率限制:6qps,即每秒鐘異?;蝈e誤的退款申請請求不超過6次
4、每個支付訂單的部分退款次數(shù)不能超過50次
5、如果同一個用戶有多筆退款,建議分不同批次進行退款,避免并發(fā)退款導(dǎo)致退款失敗
6、申請退款接口的返回僅代表業(yè)務(wù)的受理情況,具體退款是否成功,需要通過退款查詢接口獲取結(jié)果
7、一個月之前的訂單申請退款頻率限制為:5000/min
8、同一筆訂單多次退款的請求需相隔1分鐘
六、微信支付-退款成功回調(diào)
- 退款返回數(shù)據(jù)對象
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("商戶訂單號")
private String orderId;
@ApiModelProperty("商戶退款單號,out_refund_no")
private String refundId;
@ApiModelProperty("微信支付系統(tǒng)生成的訂單號")
private String transactionId;
@ApiModelProperty("微信支付系統(tǒng)生成的退款訂單號")
private String transactionRefundId;
@ApiModelProperty("退款渠道 1.ORIGINAL:原路退款 2.BALANCE:退回到余額 " +
"3.OTHER_BALANCE:原賬戶異常退到其他余額賬戶 4.OTHER_BANKCARD:原銀行卡異常退到其他銀行卡")
private String channel;
@ApiModelProperty("退款成功時間 當(dāng)前退款成功時才有此返回值")
private Date successTime;
@ApiModelProperty("退款狀態(tài) 退款到銀行發(fā)現(xiàn)用戶的卡作廢或者凍結(jié)了,導(dǎo)致原路退款銀行卡失敗,可前往商戶平臺-交易中心,手動處理此筆退款。" +
"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工具包的方法,自動識別一些常用格式的日期字符串
this.successTime = DateUtil.parse(successTime);
}
}- 退款業(yè)務(wù)處理接口
import com.xxx.project.wx.dto.WxChatCallbackRefundDto;
/**
* 退款處理接口,為了防止項目開發(fā)人員,不手動判斷退款失敗的情況
* 退款失?。和丝畹姐y行發(fā)現(xiàn)用戶的卡作廢或者凍結(jié)了,導(dǎo)致原路退款銀行卡失敗,可前往商戶平臺-交易中心,手動處理此筆退款
*/
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)答超時時間,單位為分鐘
*/
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));
// 拒絕過期應(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 {
/**
* 微信支付申請退款回調(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.簽名驗證
WechatPayValidatorForRequest wechatForRequest = new WechatPayValidatorForRequest(verifier, body, (String) bodyMap.get("id"));
try {
if (!wechatForRequest.validate(request)) {
// 通知驗簽失敗
response.setStatus(500);
final HashMap<String, Object> map = new HashMap<>();
map.put("code", "ERROR");
map.put("message", "通知驗簽失敗");
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)致原路退款銀行卡失敗,可前往商戶平臺-交易中心,手動處理此筆退款
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;
}
/**
* 對稱解密
*/
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");
// 隨機串
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é)需要人工退款,此時可以郵件或短信提醒管理員,并攜帶退款單號等關(guān)鍵信息
System.out.println("退款失敗");
}
});
}到此這篇關(guān)于SpringBoot整合微信小程序支付V3(支付退款)的文章就介紹到這了,更多相關(guān)SpringBoot整合小程序支付V3內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
使用Idea或Datagrip導(dǎo)入excel數(shù)據(jù)的方法
這篇文章主要介紹了使用Idea或Datagrip導(dǎo)入excel數(shù)據(jù)的方法,本文通過圖文并茂的形式給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-11-11
Java連接mysql數(shù)據(jù)庫的詳細教程(推薦)
這篇文章主要介紹了Java連接mysql數(shù)據(jù)庫的詳細教程,本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-08-08
elasticsearch的靈魂唯一master選舉機制原理分析
這篇文章主要為大家介紹了elasticsearch的靈魂唯一master選舉機制原理分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-04-04
SpringBoot 如何添加容器啟動的初始化邏輯的操作方法
這篇文章主要介紹了SpringBoot 如何添加容器啟動的初始化邏輯,本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-09-09
Spring?IOC容器基于XML外部屬性文件的Bean管理
這篇文章主要為大家介紹了Spring?IOC容器Bean管理XML外部屬性文件,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-05-05

