您現在的位置是:首頁 > 垂釣
接入微信支付詳細流程,前後端具體程式碼,SpringBoot+Vue實戰(二)
- 由 美女電影 發表于 垂釣
- 2022-05-11
serial no是出廠編號嗎
本專案引入了Swagger,Lombok,集成了mp,以及日誌和引入微信支付的sdk,有興趣的小夥伴可以私下去了解一下配置資訊,本文不在多講。
後端專案總體結構:
一。建立統一返回結果R類,在開發中很多專案都會建立統一結果類用來返回資料
@Data
@Accessors
(chain =
true
)
public
class
R {
private
Integer code;
//響應碼
private
String
message;
//響應訊息
private
Map<
String
,
Object
> data =
new
HashMap<>();
public
static
R ok(){
R r =
new
R();
r。setCode(
0
);
r。setMessage(
“成功”
);
return
r;
}
public
static
R error(){
R r =
new
R();
r。setCode(
-1
);
r。setMessage(
“失敗”
);
return
r;
}
public
R data(
String
key,
Object
value){
this
。data。put(key, value);
return
this
;
}
}
二。建立並定義實體類(BaseEntity是父類,其他繼承BaseEntity。注:Mapper介面繼承BaseMapper<>,業務層介面繼承IService<>,業務層實現類繼承ServiceImpl<,>)
這裡只展示OrderInfo訂單實體類,如下
三。為了開發方便,這裡定義微信支付中的列舉常量,並新增工具類,這裡只展示訂單狀態列舉
訂單狀態列舉
@AllArgsConstructor
@Getter
public
enum
OrderStatus {
/**
* 未支付
*/
NOTPAY(
“未支付”
),
/**
* 支付成功
*/
SUCCESS(
“支付成功”
),
/**
* 已關閉
*/
CLOSED(
“超時已關閉”
),
/**
* 已取消
*/
CANCEL(
“使用者已取消”
),
/**
* 退款中
*/
REFUND_PROCESSING(
“退款中”
),
/**
* 已退款
*/
REFUND_SUCCESS(
“已退款”
),
/**
* 退款異常
*/
REFUND_ABNORMAL(
“退款異常”
);
/**
* 型別
*/
private
final
String
type
;
}
工具類:
四。引入微信支付引數,將wxpay。properties 複製到resources目錄中,這個檔案定義了之前我們準備的微信支付相關的引數,例如商戶號、APPID、API秘鑰等等,並將下載的私鑰檔案複製到專案根目錄,新建config包建
WxPayConfig
類用來讀取配置檔案獲取引數。
WxPayConfig
類
@Configuration
@PropertySource
(
“classpath:wxpay。properties”
)
//讀取配置檔案
@ConfigurationProperties
(prefix=
“wxpay”
)
//讀取wxpay節點
@Data
//使用set方法將wxpay節點中的值填充到當前類的屬性中
@Slf4j
public
class
WxPayConfig {
// 商戶號
private
String
mchId;
// 商戶API證書序列號
private
String
mchSerialNo;
// 商戶私鑰檔案
private
String
privateKeyPath;
// APIv3金鑰
private
String
apiV3Key;
// APPID
private
String
appid;
// 微信伺服器地址
private
String
domain;
// 接收結果通知地址
private
String
notifyDomain;
// APIv2金鑰
private
String
partnerKey;
/**
* 獲取商戶的私鑰檔案
* @param filename
* @return
*/
private
PrivateKey getPrivateKey(
String
filename){
try
{
return
PemUtil。loadPrivateKey(
new
FileInputStream(filename));
}
catch
(FileNotFoundException e) {
throw
new
RuntimeException(
“私鑰檔案不存在”
, e);
}
}
/**
* 獲取簽名驗證器
* @return
*/
@Bean
public
ScheduledUpdateCertificatesVerifier getVerifier(){
log。info(
“獲取簽名驗證器”
);
//獲取商戶私鑰
PrivateKey privateKey = getPrivateKey(privateKeyPath);
//私鑰簽名物件
PrivateKeySigner privateKeySigner =
new
PrivateKeySigner(mchSerialNo, privateKey);
//身份認證物件
WechatPay2Credentials wechatPay2Credentials =
new
WechatPay2Credentials(mchId, privateKeySigner);
// 使用定時更新的簽名驗證器,不需要傳入證書
ScheduledUpdateCertificatesVerifier verifier =
new
ScheduledUpdateCertificatesVerifier(
wechatPay2Credentials,
apiV3Key。getBytes(StandardCharsets。UTF_8));
return
verifier;
}
/**
* 獲取http請求物件
* @param verifier
* @return
*/
@Bean
(name =
“wxPayClient”
)
public
CloseableHttpClient getWxPayClient(ScheduledUpdateCertificatesVerifier verifier){
log。info(
“獲取httpClient”
);
//獲取商戶私鑰
PrivateKey privateKey = getPrivateKey(privateKeyPath);
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder。create()
。withMerchant(mchId, mchSerialNo, privateKey)
。withValidator(
new
WechatPay2Validator(verifier));
// 。。。 接下來,你仍然可以透過builder設定各種引數,來配置你的HttpClient
// 透過WechatPayHttpClientBuilder構造的HttpClient,會自動的處理簽名和驗籤,並進行證書自動更新
CloseableHttpClient httpClient = builder。build();
return
httpClient;
}
/**
* 獲取HttpClient,無需進行應答簽名驗證,跳過驗籤的流程
*/
@Bean
(name =
“wxPayNoSignClient”
)
public
CloseableHttpClient getWxPayNoSignClient(){
//獲取商戶私鑰
PrivateKey privateKey = getPrivateKey(privateKeyPath);
//用於構造HttpClient
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder。create()
//設定商戶資訊
。withMerchant(mchId, mchSerialNo, privateKey)
//無需進行簽名驗證、透過withValidator((response) -> true)實現
。withValidator((response) ->
true
);
// 透過WechatPayHttpClientBuilder構造的HttpClient,會自動的處理簽名和驗籤,並進行證書自動更新
CloseableHttpClient httpClient = builder。build();
log。info(
“== getWxPayNoSignClient END ==”
);
return
httpClient;
}
}
五。Native支付流程
簡述:
1.使用者點選確認支付,後臺系統生成訂單,在後端資料庫表中生成一條記錄
2.商戶後臺系統呼叫統一下單API進行下單(Native下單介面)
3.微信支付系統收成請求後,生成預支付交易連結,並返回給商戶後臺系統
4.商戶後臺系統將連結生成二維碼圖片code_url,並展示給使用者
5.使用者開啟微信客戶端掃描二維碼,並提交掃碼連結給微信支付系統
6.驗證連結有效性,並要求使用者支付授權
7.授權即支付密碼,使用者確認支付,支付密碼,提交授權
8.微信支付系統驗證授權,完成支付交易
9.微信支付系統返回微信客戶端支付結果(訊息提示)
10.同時非同步告知商戶後臺系統(主動發起),用於修改系統狀態,並告知微信支付系統
11.為防止10網路異常掛掉或者沒有執行,導致使用者收到通知但商戶後臺系統沒有更新,利用查詢訂單API查詢訂單狀態,返回支付結果
12.發貨操作
六。主要
業務程式碼:
1。生成訂單,需要判斷是非已經存在
@Override
public
OrderInfo createOrderByProductId(
Long
productId) {
//查詢已存在但未支付的訂單
OrderInfo orderInfo =
this
。getNoPayOrderByProductId(productId);
if
( orderInfo !=
null
){
return
orderInfo;
}
//獲取商品資訊
Product product = productMapper。selectById(productId);
//生成訂單
orderInfo = new OrderInfo();
orderInfo。setTitle(product。getTitle());
orderInfo。setOrderNo(OrderNoUtils。getOrderNo());
//訂單號
orderInfo。setProductId(productId);
orderInfo。setTotalFee(product。getPrice());
//分
orderInfo。setOrderStatus(OrderStatus。NOTPAY。getType());
baseMapper。insert(orderInfo);
return
orderInfo;
}
2。記錄支付日誌
/**
* 記錄支付日誌
* @param plainText
*/
@Override
public
void
createPaymentInfo(
String
plainText) {
log。info(
“記錄支付日誌”
);
Gson gson =
new
Gson();
HashMap plainTextMap = gson。fromJson(plainText, HashMap。class);
//訂單號
String
orderNo = (
String
)plainTextMap。get(
“out_trade_no”
);
//業務編號
String
transactionId = (
String
)plainTextMap。get(
“transaction_id”
);
//支付型別
String
tradeType = (
String
)plainTextMap。get(
“trade_type”
);
//交易狀態
String
tradeState = (
String
)plainTextMap。get(
“trade_state”
);
//使用者實際支付金額
Map
<
String
,
Object
> amount = (
Map
)plainTextMap。get(
“amount”
);
Integer payerTotal = ((Double) amount。get(
“payer_total”
))。intValue();
PaymentInfo paymentInfo =
new
PaymentInfo();
paymentInfo。setOrderNo(orderNo);
paymentInfo。setPaymentType(PayType。WXPAY。getType());
paymentInfo。setTransactionId(transactionId);
paymentInfo。setTradeType(tradeType);
paymentInfo。setTradeState(tradeState);
paymentInfo。setPayerTotal(payerTotal);
paymentInfo。setContent(plainText);
baseMapper。insert(paymentInfo);
}
3。建立訂單,呼叫Native支付介面介面,生成二維碼並儲存
/**
* 建立訂單,呼叫Native支付介面
* @param productId
* @return code_url 和 訂單號
* @throws Exception
*/
@Transactional(rollbackFor = Exception。class)
@Override
public
Map
<
String
,
Object
> nativePay(Long productId) throws Exception {
log。info(
“生成訂單”
);
//生成訂單
OrderInfo orderInfo = orderInfoService。createOrderByProductId(productId);
String
codeUrl = orderInfo。getCodeUrl();
if
(orderInfo !=
null
&& !StringUtils。isEmpty(codeUrl)){
log。info(
“訂單已存在,二維碼已儲存”
);
//返回二維碼
Map
<
String
,
Object
> map =
new
HashMap<>();
map。put(
“codeUrl”
, codeUrl);
map。put(
“orderNo”
, orderInfo。getOrderNo());
return
map;
}
log。info(
“呼叫統一下單API”
);
//呼叫統一下單API
HttpPost httpPost =
new
HttpPost(wxPayConfig。getDomain()。concat(WxApiType。NATIVE_PAY。getType()));
// 請求body引數
Gson gson =
new
Gson();
Map
paramsMap =
new
HashMap();
paramsMap。put(
“appid”
, wxPayConfig。getAppid());
paramsMap。put(
“mchid”
, wxPayConfig。getMchId());
paramsMap。put(
“description”
, orderInfo。getTitle());
paramsMap。put(
“out_trade_no”
, orderInfo。getOrderNo());
paramsMap。put(
“notify_url”
, wxPayConfig。getNotifyDomain()。concat(WxNotifyType。NATIVE_NOTIFY。getType()));
Map
amountMap =
new
HashMap();
amountMap。put(
“total”
, orderInfo。getTotalFee());
amountMap。put(
“currency”
,
“CNY”
);
paramsMap。put(
“amount”
, amountMap);
//將引數轉換成json字串
String
jsonParams = gson。toJson(paramsMap);
log。info(
“請求引數 ===> {}”
+ jsonParams);
StringEntity entity =
new
StringEntity(jsonParams,
“utf-8”
);
entity。setContentType(
“application/json”
);
httpPost。setEntity(entity);
httpPost。setHeader(
“Accept”
,
“application/json”
);
//完成簽名並執行請求
CloseableHttpResponse response = wxPayClient。execute(httpPost);
try
{
String
bodyAsString = EntityUtils。toString(response。getEntity());
//響應體
int statusCode = response。getStatusLine()。getStatusCode();
//響應狀態碼
if
(statusCode ==
200
) {
//處理成功
log。info(
“成功, 返回結果 = ”
+ bodyAsString);
}
else
if
(statusCode ==
204
) {
//處理成功,無返回Body
log。info(
“成功”
);
}
else
{
log。info(
“Native下單失敗,響應碼 = ”
+ statusCode+
“,返回結果 = ”
+ bodyAsString);
throw
new
IOException(
“request failed”
);
}
//響應結果
Map
<
String
,
String
> resultMap = gson。fromJson(bodyAsString, HashMap。class);
//二維碼
codeUrl = resultMap。get(
“code_url”
);
//儲存二維碼
String
orderNo = orderInfo。getOrderNo();
orderInfoService。saveCodeUrl(orderNo, codeUrl);
//返回二維碼
Map
<
String
,
Object
> map =
new
HashMap<>();
map。put(
“codeUrl”
, codeUrl);
map。put(
“orderNo”
, orderInfo。getOrderNo());
return
map;
}
finally
{
response。close();
}
}
儲存二維碼
/**
* 儲存訂單二維碼
* @param orderNo
* @param codeUrl
*/
@Override
public
void
saveCodeUrl(
String
orderNo,
String
codeUrl) {
QueryWrapper
new
QueryWrapper<>();
queryWrapper。eq(
“order_no”
, orderNo);
OrderInfo orderInfo =
new
OrderInfo();
orderInfo。setCodeUrl(codeUrl);
baseMapper。update(orderInfo, queryWrapper);
}
4。退款
/**
* 退款
* @param orderNo
* @param reason
* @throws IOException
*/
@Transactional
(rollbackFor = Exception。class)
@Override
public
void
refund(
String
orderNo,
String
reason) throws Exception {
log。info(
“建立退款單記錄”
);
//根據訂單編號建立退款單
RefundInfo refundsInfo = refundsInfoService。createRefundByOrderNo(orderNo, reason);
log。info(
“呼叫退款API”
);
//呼叫統一下單API
String
url = wxPayConfig。getDomain()。concat(WxApiType。DOMESTIC_REFUNDS。getType());
HttpPost httpPost =
new
HttpPost(url);
// 請求body引數
Gson gson =
new
Gson();
Map paramsMap =
new
HashMap();
paramsMap。put(
“out_trade_no”
, orderNo);
//訂單編號
paramsMap。put(
“out_refund_no”
, refundsInfo。getRefundNo());
//退款單編號
paramsMap。put(
“reason”
,reason);
//退款原因
paramsMap。put(
“notify_url”
, wxPayConfig。getNotifyDomain()。concat(WxNotifyType。REFUND_NOTIFY。getType()));
//退款通知地址
Map amountMap =
new
HashMap();
amountMap。put(
“refund”
, refundsInfo。getRefund());
//退款金額
amountMap。put(
“total”
, refundsInfo。getTotalFee());
//原訂單金額
amountMap。put(
“currency”
,
“CNY”
);
//退款幣種
paramsMap。put(
“amount”
, amountMap);
//將引數轉換成json字串
String
jsonParams = gson。toJson(paramsMap);
log。info(
“請求引數 ===> {}”
+ jsonParams);
StringEntity entity =
new
StringEntity(jsonParams,
“utf-8”
);
entity。setContentType(
“application/json”
);
//設定請求報文格式
httpPost。setEntity(entity);
//將請求報文放入請求物件
httpPost。setHeader(
“Accept”
,
“application/json”
);
//設定響應報文格式
//完成簽名並執行請求,並完成驗籤
CloseableHttpResponse response = wxPayClient。execute(httpPost);
try
{
//解析響應結果
String
bodyAsString = EntityUtils。toString(response。getEntity());
int statusCode = response。getStatusLine()。getStatusCode();
if
(statusCode ==
200
) {
log。info(
“成功, 退款返回結果 = ”
+ bodyAsString);
}
else
if
(statusCode ==
204
) {
log。info(
“成功”
);
}
else
{
throw
new
RuntimeException(
“退款異常, 響應碼 = ”
+ statusCode+
“, 退款返回結果 = ”
+ bodyAsString);
}
//更新訂單狀態
orderInfoService。updateStatusByOrderNo(orderNo, OrderStatus。REFUND_PROCESSING);
//更新退款單
refundsInfoService。updateRefund(bodyAsString);
}
finally
{
response。close();
}
}
七。本專案用到了定時任務
*
根據訂單號查詢微信支付查單介面,核實訂單狀態
*
如果訂單已支付,則更新商戶端訂單狀態,並記錄支付日誌
*
如果訂單未支付,則呼叫關單介面關閉訂單,並更新商戶端訂單狀態
從第0秒開始每隔30秒執行1次,查詢建立超過5分鐘,並且未支付的訂單
*
獲取微信支付端退款狀態
*
如果確認退款成功,則更新訂單狀態
從第0秒開始每隔30秒執行1次,查詢建立超過5分鐘,並且未成功的退款單
@Slf
4j
@Component
public
class
WxPayTask
{
@Resource
private
OrderInfoService orderInfoService;
@Resource
private
WxPayService wxPayService;
@Resource
private
RefundInfoService refundInfoService;
/**
* 秒 分 時 日 月 周
* 以秒為例
* *:每秒都執行
* 1-3:從第1秒開始執行,到第3秒結束執行
* 0/3:從第0秒開始,每隔3秒執行1次
* 1,2,3:在指定的第1、2、3秒執行
* ?:不指定
* 日和周不能同時制定,指定其中之一,則另一個設定為?
*/
//@Scheduled(cron = “0/3 * * * * ?”)
public
void
task1
()
{
log。info(
“task1 被執行……”
);
}
/**
* 從第0秒開始每隔30秒執行1次,查詢建立超過5分鐘,並且未支付的訂單
*/
@Scheduled
(cron =
“0/30 * * * * ?”
)
public
void
orderConfirm
()
throws
Exception
{
log。info(
“orderConfirm 被執行……”
);
List
1
);
for
(OrderInfo orderInfo : orderInfoList) {
String orderNo = orderInfo。getOrderNo();
log。warn(
“超時訂單 ===> {}”
, orderNo);
//核實訂單狀態:呼叫微信支付查單介面
wxPayService。checkOrderStatus(orderNo);
}
}
/**
* 從第0秒開始每隔30秒執行1次,查詢建立超過5分鐘,並且未成功的退款單
*/
@Scheduled
(cron =
“0/30 * * * * ?”
)
public
void
refundConfirm
()
throws
Exception
{
log。info(
“refundConfirm 被執行……”
);
//找出申請退款超過5分鐘並且未成功的退款單
List
1
);
for
(RefundInfo refundInfo : refundInfoList) {
String refundNo = refundInfo。getRefundNo();
log。warn(
“超時未退款的退款單號 ===> {}”
, refundNo);
//核實訂單狀態:呼叫微信支付查詢退款介面
wxPayService。checkRefundStatus(refundNo);
}
}
前端程式碼為vue,下載依賴npm install建議國內映象,執行npm run serve
前端專案:
api:訪問後端介面的地址,引數,請求方法
具體前端頁面不再介紹
原始碼地址:git@gitee。com:liu_88/wxzf。git