從零玩轉系列之微信支付

2023-06-08 06:01:16

一、前言

halo各位大佬很久沒更新了最近在搞微信支付,因商戶號稽核了我半個月和小程式認證也找了資料並且將商戶號和小程式進行關聯,至此微信支付Native支付完成.此篇文章過長我將分幾個階段的文章釋出(專案原始碼都要,小程式和PC端)

 

一、微信支付介紹和接入指引

1、微信支付產品介紹

、付款碼支付

使用者展示微信錢包內的「付款碼」給商家,商家掃描後直接完成支付,適用於線下面對面收銀的場景。

    1. 、JSAPI支付

線下場所:商戶展示一個支付二維條碼,使用者使用微信掃描二維條碼後,輸入需要支付的金額,完成支 付。

公眾號場景:使用者在微信內進入商家公眾號,開啟某個頁面,選擇某個產品,完成支付。

PC網站場景:在網站中展示二維條碼,使用者使用微信掃描二維條碼,輸入需要支付的金額,完成支付。

特點:使用者在使用者端輸入支付金額

    1. 、小程式支付

在微信小程式平臺內實現支付的功能。

    1. 、Native支付

Native支付是指商戶展示支付二維條碼,使用者再用微信「掃一掃」完成支付的模式。這種方式適用於PC網站。

特點:商家預先指定支付金額

    1. 、APP支付

商戶通過在行動端獨立的APP應用程式中整合微信支付模組,完成支付。

    1. 、刷臉支付

使用者在刷臉裝置前通過攝像頭刷臉、識別身份後進行的一種支付方式。

2、接入指引

    1. 、獲取商戶號

微信商戶平臺:https://pay.weixin.qq.com/ 場景:Native支付

步驟:提交資料 => 簽署協定 => 獲取商戶號

    1. 、獲取APPID

微信公眾平臺:https://mp.weixin.qq.com/

步驟:註冊服務號 => 服務號認證 => 獲取APPID => 繫結商戶號

    1. 、獲取API祕鑰

APIv2版本的介面需要此祕鑰

步驟:登入商戶平臺 => 選擇 賬戶中心 => 安全中心 => API安全 => 設定API金鑰

    1. 、獲取APIv3祕鑰

APIv3版本的介面需要此祕鑰

步驟:登入商戶平臺 => 選擇 賬戶中心 => 安全中心 => API安全 => 設定APIv3金鑰隨機密碼生成工具:https://suijimimashengcheng.bmcx.com/

    1. 、申請商戶API證書

APIv3版本的所有介面都需要;APIv2版本的高階介面需要(如:退款、企業紅包、企業付款等) 步驟:登入商戶平臺 => 選擇 賬戶中心 => 安全中心 => API安全 => 申請API證書

    1. 、獲取微信平臺證書

可以預先下載,也可以通過程式設計的方式獲取。後面的課程中,我們會通過程式設計的方式來獲取。 注意:以上所有API祕鑰和證書需妥善保管防止洩露

二、支付安全(證書/祕鑰/簽名)

1、資訊保安的基礎 - 機密性

明文:加密前的訊息叫「明文」(plain text)

密文:加密後的文字叫「密文」(cipher text)

金鑰:只有掌握特殊「鑰匙」的人,才能對加密的文字進行解密,這裡的「鑰匙」就叫做「金鑰」(key)

「金鑰」就是一個字串,度量單位是「位」(bit),比如,金鑰長度是 128,就是 16 位元組的二進位制串

加密:實現機密性最常用的手段是「加密」(encrypt)

按照金鑰的使用方式,加密可以分為兩大類:對稱加密和非對稱加密。

解密:使用金鑰還原明文的過程叫「解密」(decrypt) 加密演演算法:加密解密的操作過程就是「加密演演算法」

所有的加密演演算法都是公開的,而演演算法使用的「金鑰」則必須保密

2、對稱加密和非對稱加密

對稱加密

特點:只使用一個金鑰,金鑰必須保密,常用的有 AES演演算法優點:運算速度快

缺點:祕鑰需要資訊交換的雙方共用,一旦被竊取,訊息會被破解,無法做到安全的金鑰交 換

非對稱加密

特點:使用兩個金鑰:公鑰和私鑰,公鑰可以任意分發而私鑰保密,常用的有 RSA

優點:駭客獲取公鑰無法破解密文,解決了金鑰交換的問題缺點:運算速度非常慢

混合加密

實際場景中把對稱加密和非對稱加密結合起來使用。

3、身份認證

公鑰加密,私鑰解密的作用是加密資訊私鑰加密,公鑰解密的作用是身份認證

4、摘要演演算法(Digest Algorithm)

摘要演演算法就是我們常說的雜湊函數、雜湊函數(Hash Function),它能夠把任意長度的資料「壓縮」成固定長度、而且獨一無二的「摘要」字串,就好像是給這段資料生成了一個數位「指紋」。

作用

保證資訊的完整性

特性

不可逆:只有演演算法,沒有祕鑰,只能加密,不能解密難題友好性:想要破解,只能暴力列舉

發散性:只要對原文進行一點點改動,摘要就會發生劇烈變化抗碰撞性:原文不同,計算後的摘要也要不同

常見摘要演演算法

MD5、SHA1、SHA2(SHA224、SHA256、SHA384)

5、數位簽章

數位簽章是使用私鑰對摘要加密生成簽名,需要由公鑰將簽名解密後進行驗證,實現身份認證和不可否 認

簽名和驗證簽名的流程

6、數位憑證

數位憑證解決「公鑰的信任」問題,可以防止駭客偽造公鑰。

不能直接分發公鑰,公鑰的分發必須使用數位憑證,數位憑證由CA頒發

https協定中的數位憑證:

7、微信APIv3證書

商戶證書

商戶API證書是指由商戶申請的,包含商戶的商戶號、公司名稱、公鑰資訊的證書。

商戶證書在商戶後臺申請:https://pay.weixin.qq.com/index.php/core/cert/api_cert#/

平臺證書(微信支付平臺):

微信支付平臺證書是指由微信支付 負責申請的,包含微信支付平臺標識、公鑰資訊的證書。商戶可以使用平臺證書中的公鑰進行驗籤。

平臺證書的獲取:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay3_0.shtml

8、API金鑰和APIv3金鑰

都是對稱加密需要使用的加密和解密金鑰,一定要保管好,不能洩露。

API金鑰對應V2版本的API APIv3金鑰對應V3版本的API

三、案例專案的建立

1、建立SpringBoot專案

    1. 、新建專案

注意:Java版本選擇8

    1. 、新增依賴

新增SpringBoot web依賴

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

</dependency>

    1. 、設定application.yml檔案

server:

port: 8090 #伺服器埠

spring: application:

name: payment-demo # 應用名稱

    1. 、建立controller

建立controller包,建立ProductController類

package com.atguigu.paymentdemo.controller;

import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;

@RestController @RequestMapping("/api/product") @CrossOrigin //跨域

public class ProductController {

@GetMapping("/test") public String test(){

return "hello";

}

}

    1. 、測試

存取:http://localhost:8090/api/product/test

2、引入Swagger

作用:自動生成介面檔案和測試頁面。

    1. 、引入依賴

<!--swagger-->

<dependency>

<groupId>io.springfox</groupId>

<artifactId>springfox-swagger2</artifactId>

<version>2.7.0</version>

</dependency>

<!--swagger ui-->

<dependency>

<groupId>io.springfox</groupId>

<artifactId>springfox-swagger-ui</artifactId>

<version>2.7.0</version>

</dependency>

    1. 、Swagger組態檔

建立config包,建立Swagger2Config類

package com.atguigu.paymentdemo.config;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration; import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket;

import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration @EnableSwagger2

public class Swagger2Config {

@Bean

public Docket docket(){

return new Docket(DocumentationType.SWAGGER_2)

.apiInfo(new ApiInfoBuilder().title("微信支付案例介面文

檔").build());

}

}

    1. 、Swagger註解

controller中可以新增常用註解

@Api(tags="商品管理") //用在類上

@ApiOperation("測試介面") //用在方法上

    1. 、測試

存取:http://localhost:8090/swagger-ui.html

3、定義統一結果

作用:定義統一響應結果,為前端返回標準格式的資料。

    1. 、引入lombok依賴

簡化實體類的開發

<!--實體物件工具類:低版本idea需要安裝lombok外掛-->

<dependency>

<groupId>org.projectlombok</groupId>

<artifactId>lombok</artifactId>

</dependency>

    1. 、建立R類

建立統一結果類

package com.atguigu.paymentdemo.vo;

import lombok.NoArgsConstructor; import lombok.Setter;

import java.util.HashMap; import java.util.Map;

@Data //生成set、get等方法 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;

}

}

    1. 、修改controller

修改test方法,返回統一結果

@ApiOperation("測試介面") @GetMapping("/test") public R test(){

return R

.ok()

.data("message", "hello")

.data("now", new Date());

}

    1. 、設定json時間格式

spring:

jackson: #json時間格式

date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8

    1. 、Swagger測試

4、建立資料庫

    1. 、建立資料庫

mysql -uroot -p

mysql> create database payment_demo;

    1. 、IDEA設定資料庫連線
  1. 開啟資料庫面板

  1. 新增資料庫
  2. 設定資料庫連線引數

    1. 、執行SQL指令碼

payment_demo.sql

5、整合MyBatis-Plus

    1. 、引入依賴

<!--mysql驅動-->

<dependency>

<groupId>mysql</groupId>

<artifactId>mysql-connector-java</artifactId>

</dependency>

<!--持久層-->

<dependency>

<groupId>com.baomidou</groupId>

<artifactId>mybatis-plus-boot-starter</artifactId>

<version>3.3.1</version>

</dependency>

    1. 、設定資料庫連線

spring:

datasource: #mysql資料庫連線

driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/payment_demo?

serverTimezone=GMT%2B8&characterEncoding=utf-8 username: root

password: 123456

    1. 、定義實體類

BaseEntity是父類別,其他類繼承BaseEntity

    1. 、定義持久層

定義Mapper介面繼承 BaseMapper<>, 定義xml組態檔

    1. 、定義MyBatis-Plus的組態檔

在config包中建立組態檔 MybatisPlusConfig

package com.atguigu.paymentdemo.config;

import org.mybatis.spring.annotation.MapperScan;

import org.springframework.context.annotation.Configuration;

import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration @MapperScan("com.atguigu.paymentdemo.mapper") //持久層掃描 @EnableTransactionManagement //啟用事務管理

public class MybatisPlusConfig {

}

    1. 、定義yml組態檔

新增持久層紀錄檔和xml檔案位置的設定

mybatis-plus: configuration: #sql紀錄檔

log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

mapper-locations: classpath:com/atguigu/paymentdemo/mapper/xml/*.xml

    1. 、定義業務層

定義業務層介面繼承 IService<>

定義業務層介面的實現類,並繼承 ServiceImpl<,>

    1. 、定義介面方法查詢所有商品

在 public class ProductController 中新增一個方法

@Resource

private ProductService productService;

@ApiOperation("商品列表") @GetMapping("/list") public R list(){

List<Product> list = productService.list(); return R.ok().data("productList", list);

}

    1. 、Swagger中測試
    2. 、pom中設定build節點

因為maven工程在預設情況下 src/main/java 目錄下的所有資原始檔是不釋出到 target 目錄下的,我們在 pom 檔案的 節點下設定一個資源釋出過濾器

<!-- 專案打包時會將java目錄中的*.xml檔案也進行打包 -->

<resources>

<resource>

<directory>src/main/java</directory>

<includes>

<include>**/*.xml</include>

</includes>

<filtering>false</filtering>

</resource>

</resources>

6、搭建前端環境

    1. 、安裝Node.js

Node.js是一個基於JavaScript引擎的伺服器端環境,前端專案在開發環境下要基於Node.js來執行安裝:node-v14.18.0-x64.msi

    1. 、執行前端專案

將專案放在磁碟的一個目錄中,例如 D:\demo\payment-demo-front

進入專案目錄,執行下面的命令啟動專案:

npm run serve

    1. 、安裝VSCode

如果你希望方便的檢視和修改前端程式碼,可以安裝一個VSCode 安裝:VSCodeUserSetup-x64-1.56.2

安裝外掛:

7、Vue.js入門

官網:https://cn.vuejs.org/

Vue.js是一個前端框架,幫助我們快速構建前端專案。

使用vue有兩種方式,一個是傳統的在 html 檔案中引入 js 指令碼檔案的方式,另一個是腳手架的方式。我們的專案,使用的是腳手架的方式。

    1. 、安裝腳手架

設定淘寶映象

#經過下面的設定,所有的 npm install 都會經過淘寶的映象地址下載

npm config set registry https://registry.npm.taobao.org

全域性安裝腳手架

npm install -g @vue/cli

    1. 、建立一個專案

先進入專案目錄(Ctrl + ~),然後建立一個專案

vue create vue-demo

    1. 、執行專案

npm run serve

指定執行埠

npm run serve -- --port 8888

    1. 、資料繫結

修改 src/App.vue

<!--定義頁面結構-->

<template>

<div>

<h1>Vue案例</h1>

<!-- 插值 -->

<p>{{course}}</p>

</div>

</template>

<!--定義頁面指令碼-->

<script>

export default {

// 定義資料

data () { return {

course: '微信支付'

}

}

}

</script>

    1. 、安裝Vue偵錯工具

在Chrome的擴充套件程式中安裝:Vue.jsDevtools.zip

  1. 擴充套件程式的安裝

  1. 擴充套件程式的使用
    1. 、雙向資料繫結

資料會繫結到元件,元件的改變也會影響資料定義

<p>

<!-- 指令 -->

<input type="text" v-model="course">

</p>

    1. 、事件處理
  1. 定義事件

// 定義方法

methods: {

toPay(){

console.log('去支付')

}

}

  1. 呼叫事件

<p>

<!-- 事件 -->

<button @click="toPay()">去支付</button>

</p>

四、基礎支付API V3

1、引入支付引數

    1. 、定義微信支付相關引數

將資料資料夾中的 wxpay.properties 複製到resources目錄中

這個檔案定義了之前我們準備的微信支付相關的引數,例如商戶號、APPID、API祕鑰等等

    1. 、讀取支付引數

將資料資料夾中的 config 目錄中的 WxPayConfig.java 複製到原始碼目錄中。

    1. 、測試支付引數的獲取

在 controller 包中建立 TestController

package com.atguigu.paymentdemo.controller;

import com.atguigu.paymentdemo.config.WxPayConfig; import com.atguigu.paymentdemo.vo.R;

import io.swagger.annotations.Api;

import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@Api(tags = "測試控制器") @RestController @RequestMapping("/api/test") public class TestController {

@Resource

private WxPayConfig wxPayConfig;

@GetMapping("/get-wx-pay-config") public R getWxPayConfig(){

String mchId = wxPayConfig.getMchId(); return R.ok().data("mchId", mchId);

}

}

    1. 、設定 Annotation Processor

可以幫助我們生成自定義設定的後設資料資訊,讓組態檔和Java程式碼之間的對應引數可以自動定位,方便開發。

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-configuration-processor</artifactId>

<optional>true</optional>

</dependency>

    1. 、在IDEA中設定 SpringBoot 組態檔

讓IDEA可以識別組態檔,將組態檔的圖示展示成SpringBoot的圖示,同時組態檔的內容可以高 亮顯示

File -> Project Structure -> Modules -> 選擇小葉子

點選(+) 圖示

選中組態檔:

2、載入商戶私鑰

    1. 、複製商戶私鑰

將下載的私鑰檔案複製到專案根目錄下:

    1. 、引入SDK

https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay6_0.shtml

我們可以使用官方提供的 SDK,幫助我們完成開發。實現了請求籤名的生成和應答簽名的驗證。

<dependency>

<groupId>com.github.wechatpay-apiv3</groupId>

<artifactId>wechatpay-apache-httpclient</artifactId>

<version>0.3.0</version>

</dependency>

    1. 、獲取商戶私鑰

https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient (如何載入商戶私鑰)

/**

  • 獲取商戶私鑰
  • @param filename
  • @return

*/

public PrivateKey getPrivateKey(String filename){

try {

return PemUtil.loadPrivateKey(new FileInputStream(filename));

} catch (FileNotFoundException e) {

throw new RuntimeException("私鑰檔案不存在", e);

}

}

    1. 、測試商戶私鑰的獲取

在 PaymentDemoApplicationTests 測試類中新增如下方法,測試私鑰物件是否能夠獲取出來。

(將前面的方法改成public的再進行測試)

package com.atguigu.paymentdemo;

import com.atguigu.paymentdemo.config.WxPayConfig; import org.junit.jupiter.api.Test;

import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource; import java.security.PrivateKey;

@SpringBootTest

class PaymentDemoApplicationTests {

@Resource

private WxPayConfig wxPayConfig;

/**

* 獲取商戶私鑰

*/ @Test

public void testGetPrivateKey(){

//獲取私鑰路徑

String privateKeyPath = wxPayConfig.getPrivateKeyPath();

//獲取商戶私鑰

PrivateKey privateKey = wxPayConfig.getPrivateKey(privateKeyPath);

System.out.println(privateKey);

}

}

3、獲取簽名驗證器和HttpClient

    1. 、證書金鑰使用說明

https://pay.weixin.qq.com/wiki/doc/apiv3_partner/wechatpay/wechatpay3_0.shtml

    1. 、獲取簽名驗證器

https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient (定時更新平臺證書功能) 平臺證書:平臺證書封裝了微信的公鑰,商戶可以使用平臺證書中的公鑰進行驗籤。

簽名驗證器:幫助我們進行驗籤工作,我們單獨將它定義出來,方便後面的開發。

/**

  • 獲取簽名驗證器
  • @return

*/ @Bean

public ScheduledUpdateCertificatesVerifier getVerifier(){

//獲取商戶私鑰

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;

}

3.4、獲取 HttpClient 物件

https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient (定時更新平臺證書功能)

HttpClient 物件:是建立遠端連線的基礎,我們通過SDK建立這個物件。

/**

  • 獲取HttpClient物件
  • @param verifier
  • @return

*/ @Bean

public CloseableHttpClient getWxPayClient(ScheduledUpdateCertificatesVerifier verifier){

//獲取商戶私鑰

PrivateKey privateKey = getPrivateKey(privateKeyPath);

//用於構造HttpClient

WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()

.withMerchant(mchId, mchSerialNo, privateKey)

.withValidator(new WechatPay2Validator(verifier));

// ... 接下來,你仍然可以通過builder設定各種引數,來設定你的HttpClient

// 通過WechatPayHttpClientBuilder構造的HttpClient,會自動的處理簽名和驗籤,並進行證書自動更新

CloseableHttpClient httpClient = builder.build();

return httpClient;

}

4、API字典和相關工具

    1. 、API列表

https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_7_3.shtml

我們的專案中要實現以下所有API的功能。

    1. 、介面規則

https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay2_0.shtml

微信支付 APIv3 使用 JSON 作為訊息體的資料交換格式。

<!--json處理-->

<dependency>

<groupId>com.google.code.gson</groupId>

<artifactId>gson</artifactId>

</dependency>

    1. 、定義列舉

將資料資料夾中的 enums 目錄複製到原始碼目錄中。

為了開發方便,我們預先在專案中定義一些列舉。列舉中定義的內容包括介面地址,支付狀態等資訊。

    1. 、新增工具類

將資料資料夾中的 util 目錄複製到原始碼目錄中,我們將會使用這些輔助工具簡化專案的開發

5、Native下單API

    1. 、Native支付流程

https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_4.shtml

    1. 、Native下單API

https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml

商戶端發起支付請求,微信端建立支付訂單並生成支付二維條碼連結,微信端將支付二維條碼返回給商戶 端,商戶端顯示支付二維條碼,使用者使用微信使用者端掃碼後發起支付。

  1. 建立 WxPayController

package com.atguigu.paymentdemo.controller;

import io.swagger.annotations.Api; import lombok.extern.slf4j.Slf4j;

import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

@CrossOrigin @RestController

@RequestMapping("/api/wx-pay") @Api(tags = "網站微信支付") @Slf4j

public class WxPayController {

}

  1. 建立 WxPayService

介面

package com.atguigu.paymentdemo.service; public interface WxPayService {

}

實現

package com.atguigu.paymentdemo.service.impl;

import com.atguigu.paymentdemo.service.WxPayService; import lombok.extern.slf4j.Slf4j;

import org.springframework.stereotype.Service;

@Service @Slf4j

public class WxPayServiceImpl implements WxPayService {

}

  1. 定義WxPayController方法

@Resource

private WxPayService wxPayService;

/**

  • Native下單
  • @param productId
  • @return
  • @throws Exception

*/

@ApiOperation("呼叫統一下單API,生成支付二維條碼") @PostMapping("/native/{productId}")

public R nativePay(@PathVariable Long productId) throws Exception {

log.info("發起支付請求");

//返回支付二維條碼連線和訂單號

Map<String, Object> map = wxPayService.nativePay(productId);

return R.ok().setData(map);

}

R物件中新增 @Accessors(chain = true),使其可以鏈式操作

@Data

@Accessors(chain = true) //鏈式操作 public class R {

  1. 定義WxPayService方法

參考:

API 字 典 -> 基 礎 支 付 -> Native 支 付 -> Native 下 單 : https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml

指引檔案 -> 基礎支付 -> Native支付 -> 開發指引 ->【伺服器端】Native下單: https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_7_2.shtml

介面

Map<String, Object> nativePay(Long productId) throws Exception;

實現

@Resource

private WxPayConfig wxPayConfig;

@Resource

private CloseableHttpClient wxPayClient;

/**

  • 建立訂單,呼叫Native支付介面
  • @param productId
  • @return code_url 和 訂單號
  • @throws Exception

*/ @Override

public Map<String, Object> nativePay(Long productId) throws Exception {

log.info("生成訂單");

//生成訂單

OrderInfo orderInfo = new OrderInfo();

orderInfo.setTitle("test"); orderInfo.setOrderNo(OrderNoUtils.getOrderNo()); //訂單號 orderInfo.setProductId(productId); orderInfo.setTotalFee(1); //分 orderInfo.setOrderStatus(OrderStatus.NOTPAY.getType());

//TODO:存入資料庫 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);

//二維條碼

String codeUrl = resultMap.get("code_url");

Map<String, Object> map = new HashMap<>(); map.put("codeUrl", codeUrl); map.put("orderNo", orderInfo.getOrderNo());

return map;

} finally {

response.close();

}

}

    1. 、簽名和驗籤原始碼解析

簽名原理

開啟debug紀錄檔

logging: level:

root: info

籤 名 生 成 流 程 : https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_0.shtml 簽名生成原始碼:

  1. 驗籤原理

籤 名 驗 證 流 程 : https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_1.shtml 簽名驗證原始碼:

    1. 、建立課程訂單
  1. 儲存訂單

OrderInfoService

介面:

OrderInfo createOrderByProductId(Long productId);

實現:

@Resource

private ProductMapper productMapper;

@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;

}

查詢未支付訂單:OrderInfoService中新增輔助方法

/**

  • 根據商品id查詢未支付訂單
  • 防止重複建立訂單物件
  • @param productId
  • @return

*/

private OrderInfo getNoPayOrderByProductId(Long productId) {

QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("product_id", productId); queryWrapper.eq("order_status", OrderStatus.NOTPAY.getType());

// queryWrapper.eq("user_id", userId);

OrderInfo orderInfo = baseMapper.selectOne(queryWrapper); return orderInfo;

}

  1. 快取二維條碼

OrderInfoService

介面:

void saveCodeUrl(String orderNo, String codeUrl);

實現:

/**

  • 儲存訂單二維條碼
  • @param orderNo
  • @param codeUrl

*/ @Override

public void saveCodeUrl(String orderNo, String codeUrl) {

QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("order_no", orderNo);

OrderInfo orderInfo = new OrderInfo(); orderInfo.setCodeUrl(codeUrl);

baseMapper.update(orderInfo, queryWrapper);

}

  1. 修改WxPayServiceImpl 的 nativePay 方法

@Resource

private OrderInfoService orderInfoService;

/**

* 建立訂單,呼叫Native支付介面

    • @param productId
    • @return code_url 和 訂單號
    • @throws Exception

*/ @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");

//其他程式碼。。。。。。try {

//其他程式碼。。。。。。

//儲存二維條碼

String orderNo = orderInfo.getOrderNo(); orderInfoService.saveCodeUrl(orderNo, codeUrl);

//返回二維條碼

//其他程式碼。。。。。。

} finally {

response.close();

}

}

、顯示訂單列表

在我的訂單頁面按時間倒序顯示訂單列表

  1. 建立OrderInfoController

package com.atguigu.paymentdemo.controller;

import com.atguigu.paymentdemo.entity.OrderInfo;

import com.atguigu.paymentdemo.service.OrderInfoService; import com.atguigu.paymentdemo.vo.R;

import io.swagger.annotations.Api;

import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource; import java.util.List;

@CrossOrigin //開放前端的跨域存取 @Api(tags = "商品訂單管理") @RestController @RequestMapping("/api/order-info") public class OrderInfoController {

@Resource

private OrderInfoService orderInfoService;

@ApiOperation("訂單列表") @GetMapping("/list") public R list(){

List<OrderInfo> list = orderInfoService.listOrderByCreateTimeDesc(); return R.ok().data("list", list);

}

}

  1. 定義 OrderInfoService 方法

介面

List<OrderInfo> listOrderByCreateTimeDesc();

實現

/**

  • 查詢訂單列表,並倒序查詢
  • @return

*/ @Override

public List<OrderInfo> listOrderByCreateTimeDesc() {

QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<OrderInfo> ().orderByDesc("create_time");

return baseMapper.selectList(queryWrapper);

}

6、支付通知API

    1. 、內網穿透
  1. 存取ngrok官網

https://ngrok.com/

  1. 註冊賬號、登入
  2. 下載內網穿透工具

ngrok-stable-windows-amd64.zip

  1. 設定你的 authToken

為本地計算機做授權設定

ngrok authtoken 6aYc6Kp7kpxVr8pY88LkG_6x9o18yMY8BASrXiDFMeS

  1. 啟動服務

ngrok http 8090

  1. 測試外網存取

你獲得的外網地址/api/test

    1. 、接收通知和返回應答

支付通知API:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_5.shtml

  1. 啟動ngrok

ngrok http 8090

  1. 設定通知地址

wxpay.properties

注意:每次重新啟動ngrok,都需要根據實際情況修改這個設定

wxpay.notify-domain=https://7d92-115-171-63-135.ngrok.io

  1. 建立通知介面

通知規則:使用者支付完成後,微信會把相關支付結果和使用者資訊傳送給商戶,商戶需要接收處理 該訊息,並返回應答。對後臺通知互動時,如果微信收到商戶的應答不符合規範或超時,微信認 為通知失敗,微信會通過一定的策略定期重新發起通知,儘可能提高通知的成功率,但微信不保 證通知最終能成功。(通知頻率為15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 總計 24h4m)

/**

  • 支付通知
  • 微信支付通過支付通知介面將使用者支付成功訊息通知給商戶

*/

@ApiOperation("支付通知") @PostMapping("/native/notify")

public String nativeNotify(HttpServletRequest request, HttpServletResponse response){

Gson gson = new Gson();

Map<String, String> map = new HashMap<>();//應答物件

//處理通知引數

String body = HttpUtils.readData(request);

Map<String, Object> bodyMap = gson.fromJson(body, HashMap.class); log.info("支付通知的id ===> {}", bodyMap.get("id"));

log.info("支付通知的完整資料 ===> {}", body);

//TODO : 簽名的驗證

//TODO : 處理訂單

//成功應答:成功應答必須為200或204,否則就是失敗應答response.setStatus(200);

map.put("code", "SUCCESS");

map.put("message", "成功"); return gson.toJson(map);

}

  1. 測試失敗應答

用失敗應答替換成功應答

@PostMapping("/native/notify")

public String nativeNotify(HttpServletRequest request, HttpServletResponse response) throws Exception {

Gson gson = new Gson();

Map<String, String> map = new HashMap<>(); try {

} catch (Exception e) {

e.printStackTrace();

// 測試錯誤應答response.setStatus(500); map.put("code", "ERROR");

map.put("message", "系統錯誤"); return gson.toJson(map);

}

}

  1. 測試超時應答

回撥通知注意事項:https://pay.weixin.qq.com/wiki/doc/apiv3/Practices/chapter1_1_5.shtml

商戶系統收到支付結果通知,需要在 5秒內 返回應答報文,否則微信支付認為通知失敗,後續會重複傳送通知。

// 測試超時應答:新增睡眠時間使應答超時

TimeUnit.SECONDS.sleep(5);

    1. 、驗籤
  1. 工具類

參考SDK原始碼中的 WechatPay2Validator 建立通知驗籤工具類 WechatPay2ValidatorForRequest

  1. 驗籤

@Resource

private Verifier verifier;

//簽名的驗證WechatPay2ValidatorForRequest validator

= new WechatPay2ValidatorForRequest(verifier, body, requestId); if (!validator.validate(request)) {

log.error("通知驗籤失敗");

//失敗應答

response.setStatus(500); map.put("code", "ERROR");

map.put("message", "通知驗籤失敗");

return gson.toJson(map);

}

log.info("通知驗籤成功");

//TODO : 處理訂單

    1. 、解密

(1)WxPayController

nativeNotify 方法中新增處理訂單的程式碼

//處理訂單wxPayService.processOrder(bodyMap);

(1)WxPayService

介面:

void processOrder(Map<String, Object> bodyMap) throws GeneralSecurityException;

實現:

@Override

public void processOrder(Map<String, Object> bodyMap) throws GeneralSecurityException {

log.info("處理訂單");

String plainText = decryptFromResource(bodyMap);

//轉換明文

//更新訂單狀態

//記錄支付紀錄檔

}

輔助方法:

/**

  • 對稱解密
  • @param bodyMap
  • @return

*/

private String decryptFromResource(Map<String, Object> bodyMap) throws GeneralSecurityException {

log.info("密文解密");

//通知資料

Map<String, String> resourceMap = (Map) bodyMap.get("resource");

//資料密文

String ciphertext = resourceMap.get("ciphertext");

//隨機串

String nonce = resourceMap.get("nonce");

//附加資料

String associatedData = resourceMap.get("associated_data");

log.info("密文 ===> {}", ciphertext);

AesUtil aesUtil = new AesUtil(wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8));

String plainText = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),

nonce.getBytes(StandardCharsets.UTF_8),

ciphertext);

log.info("明文 ===> {}", plainText); return plainText;

}

    1. 、處理訂單
  1. 完善processOrder方法

@Resource

private PaymentInfoService paymentInfoService;

@Override

public void processOrder(Map<String, Object> bodyMap) throws GeneralSecurityException {

log.info("處理訂單");

String plainText = decryptFromResource(bodyMap);

//轉換明文

Gson gson = new Gson();

Map<String, Object> plainTextMap = gson.fromJson(plainText, HashMap.class); String orderNo = (String)plainTextMap.get("out_trade_no");

//更新訂單狀態

orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);

//記錄支付紀錄檔paymentInfoService.createPaymentInfo(plainText);

}

  1. 更新訂單狀態

OrderInfoService

介面:

void updateStatusByOrderNo(String orderNo, OrderStatus orderStatus);

實現:

/**

  • 根據訂單編號更新訂單狀態
  • @param orderNo
  • @param orderStatus

*/ @Override

public void updateStatusByOrderNo(String orderNo, OrderStatus orderStatus) {

log.info("更新訂單狀態 ===> {}", orderStatus.getType());

QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("order_no", orderNo);

OrderInfo orderInfo = new OrderInfo(); orderInfo.setOrderStatus(orderStatus.getType());

baseMapper.update(orderInfo, queryWrapper);

}

  1. 處理支付紀錄檔

PaymentInfoService

介面:

void createPaymentInfo(String plainText);

實現:

/**

  • 記錄支付紀錄檔
  • @param plainText

*/ @Override

public void createPaymentInfo(String plainText) {

log.info("記錄支付紀錄檔"); Gson gson = new Gson();

Map<String, Object> 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);

}

    1. 、處理重複通知

  1. 測試重複的通知

//應答超時

//設定響應超時,可以接收到微信支付的重複的支付結果通知。

//通知重複,資料庫會記錄多餘的支付紀錄檔

TimeUnit.SECONDS.sleep(5);

  1. 處理重複通知

在 processOrder 方法中,更新訂單狀態之前,新增如下程式碼

//處理重複通知

//保證介面呼叫的冪等性:無論介面被呼叫多少次,產生的結果是一致的

String orderStatus = orderInfoService.getOrderStatus(orderNo); if (!OrderStatus.NOTPAY.getType().equals(orderStatus)) {

return;

}

OrderInfoService

介面:

String getOrderStatus(String orderNo);

實現:

/**

  • 根據訂單號獲取訂單狀態
  • @param orderNo
  • @return

*/ @Override

public String getOrderStatus(String orderNo) {

QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("order_no", orderNo);

OrderInfo orderInfo = baseMapper.selectOne(queryWrapper);

//防止被刪除的訂單的回撥通知的呼叫

if(orderInfo == null){ return null;

}

return orderInfo.getOrderStatus();

}

    1. 、資料鎖

  1. 測試通知並行

//處理重複的通知

//模擬通知並行try {

TimeUnit.SECONDS.sleep(5);

} catch (InterruptedException e) { e.printStackTrace();

}

//更新訂單狀態

//記錄支付紀錄檔

  1. 定義ReentrantLock

定義 ReentrantLock 進行並行控制。注意,必須手動釋放鎖。

private final ReentrantLock lock = new ReentrantLock();

@Override

public void processOrder(Map<String, Object> bodyMap) throws GeneralSecurityException {

log.info("處理訂單");

//解密報文

String plainText = decryptFromResource(bodyMap);

//將明文轉換成map

Gson gson = new Gson();

HashMap plainTextMap = gson.fromJson(plainText, HashMap.class);

String orderNo = (String)plainTextMap.get("out_trade_no");

/*在對業務資料進行狀態檢查和處理之前,

要採用資料鎖進行並行控制,

以避免函數重入造成的資料混亂*/

//嘗試獲取鎖:

// 成功獲取則立即返回true,獲取失敗則立即返回false。不必一直等待鎖的釋放

if(lock.tryLock()){ try {

//處理重複的通知

//介面呼叫的冪等性:無論介面被呼叫多少次,產生的結果是一致的。

String orderStatus = orderInfoService.getOrderStatus(orderNo); if(!OrderStatus.NOTPAY.getType().equals(orderStatus)){

return;

}

//模擬通知並行try {

TimeUnit.SECONDS.sleep(5);

} catch (InterruptedException e) { e.printStackTrace();

}

//更新訂單狀態orderInfoService.updateStatusByOrderNo(orderNo,

OrderStatus.SUCCESS);

//記錄支付紀錄檔paymentInfoService.createPaymentInfo(plainText);

} finally {

//要主動釋放鎖lock.unlock();

}

}

}

7、商戶定時查詢本地訂單

    1. 、後端定義商戶查單介面

支付成功後,商戶側查詢本地資料庫,訂單是否支付成功

/**

* 查詢本地訂單狀態

*/

@ApiOperation("查詢本地訂單狀態") @GetMapping("/query-order-status/{orderNo}")

public R queryOrderStatus(@PathVariable String orderNo) {

String orderStatus = orderInfoService.getOrderStatus(orderNo); if (OrderStatus.SUCCESS.getType().equals(orderStatus)) {//支付成功

return R.ok();

}

return R.ok().setCode(101).setMessage("支付中...");

}

    1. 、前端定時輪詢查單

在二維條碼展示頁面,前端定時輪詢查詢訂單是否已支付,如果支付成功則跳轉到訂單頁面

  1. 定義定時器

//啟動定時器

this.timer = setInterval(() => {

//查詢訂單是否支付成功this.queryOrderStatus()

}, 3000)

  1. 查詢訂單

// 查詢訂單狀態

queryOrderStatus() {

orderInfoApi.queryOrderStatus(this.orderNo).then(response => { console.log('查詢訂單狀態:' + response.code)

// 支付成功後的頁面跳轉

if (response.code === 0) { console.log('清除定時器') clearInterval(this.timer)

// 三秒後跳轉到訂單列表

setTimeout(() => {

this.$router.push({ path: '/success' })

}, 3000)

}

})

}

8、使用者取消訂單API

實現使用者主動取消訂單的功能

    1. 、定義取消訂單介面

WxPayController中新增介面方法

/**

  • 使用者取消訂單
  • @param orderNo
  • @return
  • @throws Exception

*/

@ApiOperation("使用者取消訂單") @PostMapping("/cancel/{orderNo}")

public R cancel(@PathVariable String orderNo) throws Exception {

log.info("取消訂單");

wxPayService.cancelOrder(orderNo); return R.ok().setMessage("訂單已取消");

}

    1. 、WxPayService

介面

void cancelOrder(String orderNo) throws Exception;

實現

/**

  • 使用者取消訂單
  • @param orderNo

*/ @Override

public void cancelOrder(String orderNo) throws Exception {

//呼叫微信支付的關單介面this.closeOrder(orderNo);

//更新商戶端的訂單狀態

orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CANCEL);

}

關單方法

/**

  • 關單介面的呼叫
  • @param orderNo

*/

private void closeOrder(String orderNo) throws Exception {

log.info("關單介面的呼叫,訂單號 ===> {}", orderNo);

//建立遠端請求物件

String url = String.format(WxApiType.CLOSE_ORDER_BY_NO.getType(), orderNo); url = wxPayConfig.getDomain().concat(url);

HttpPost httpPost = new HttpPost(url);

//組裝json請求體

Gson gson = new Gson();

Map<String, String> paramsMap = new HashMap<>();

paramsMap.put("mchid", wxPayConfig.getMchId()); 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 {

int statusCode = response.getStatusLine().getStatusCode();//響應狀態碼 if (statusCode == 200) { //處理成功

log.info("成功200");

} else if (statusCode == 204) { //處理成功,無返回Body log.info("成功204");

} else {

log.info("Native下單失敗,響應碼 = " + statusCode); throw new IOException("request failed");

}

} finally {

response.close();

}

}

9、微信支付查單API

    1. 、查單介面的呼叫

商戶後臺未收到非同步支付結果通知時,商戶應該主動呼叫《微信支付查單介面》,同步訂單狀態。

  1. WxPayController

/**

  • 查詢訂單
  • @param orderNo
  • @return
  • @throws URISyntaxException
  • @throws IOException

*/

@ApiOperation("查詢訂單:測試訂單狀態用") @GetMapping("query/{orderNo}")

public R queryOrder(@PathVariable String orderNo) throws Exception {

log.info("查詢訂單");

String bodyAsString = wxPayService.queryOrder(orderNo);

return R.ok().setMessage("查詢成功").data("bodyAsString", bodyAsString);

}

  1. WxPayService

介面

String queryOrder(String orderNo) throws Exception;

實現

/**

* 查單介面呼叫

*/ @Override

public String queryOrder(String orderNo) throws Exception {

log.info("查單介面呼叫 ===> {}", orderNo);

String url = String.format(WxApiType.ORDER_QUERY_BY_NO.getType(), orderNo); url = wxPayConfig.getDomain().concat(url).concat("?

mchid=").concat(wxPayConfig.getMchId());

HttpGet httpGet = new HttpGet(url); httpGet.setHeader("Accept", "application/json");

//完成簽名並執行請求

CloseableHttpResponse response = wxPayClient.execute(httpGet);

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");

}

return bodyAsString;

} finally {

response.close();

}

}

    1. 、整合Spring Task

Spring 3.0後提供Spring Task實現任務排程

  1. 啟動類新增註解

statistics啟動類新增註解

@EnableScheduling

  1. 測試定時任務

建立 task 包,建立 WxPayTask.java

package com.atguigu.paymentdemo.task;

import lombok.extern.slf4j.Slf4j;

import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component;

@Slf4j @Component

public class WxPayTask {

/**

  • 測試
  • (cron=" 秒 分 時 日 月 周 ")
  • *:每隔一秒執行
  • 0/3:從第0秒開始,每隔3秒執行一次
  • 1-3: 從第1秒開始執行,到第3秒結束執行

* 1,2,3:第1、2、3秒執行

  • ?:不指定,若指定日期,則不指定周,反之同理

*/

@Scheduled(cron="0/3 * * * * ?") public void task1() {

log.info("task1 執行");

}

}

    1. 、定時查詢超時訂單
  1. WxPayTask

@Resource

private OrderInfoService orderInfoService;

@Resource

private WxPayService wxPayService;

/**

* 從第0秒開始每隔30秒執行1次,查詢建立超過5分鐘,並且未支付的訂單

*/

@Scheduled(cron = "0/30 * * * * ?")

public void orderConfirm() throws Exception { log.info("orderConfirm 被執行 ");

List<OrderInfo> orderInfoList = orderInfoService.getNoPayOrderByDuration(5);

for (OrderInfo orderInfo : orderInfoList) { String orderNo = orderInfo.getOrderNo(); log.warn("超時訂單 ===> {}", orderNo);

//核實訂單狀態:呼叫微信支付查單介面

wxPayService.checkOrderStatus(orderNo);

}

}

  1. OrderInfoService

介面

List<OrderInfo> getNoPayOrderByDuration(int minutes);

實現

/**

  • 找出建立超過minutes分鐘並且未支付的訂單
  • @param minutes
  • @return

*/ @Override

public List<OrderInfo> getNoPayOrderByDuration(int minutes) {

//minutes分鐘之前的時間

Instant instant = Instant.now().minus(Duration.ofMinutes(minutes));

QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("order_status", OrderStatus.NOTPAY.getType()); queryWrapper.le("create_time", instant);

List<OrderInfo> orderInfoList = baseMapper.selectList(queryWrapper); return orderInfoList;

}

    1. 、處理超時訂單

WxPayService

核實訂單狀態介面:

void checkOrderStatus(String orderNo) throws Exception;

實現:

/**

  • 根據訂單號查詢微信支付查單介面,核實訂單狀態
  • 如果訂單已支付,則更新商戶端訂單狀態,並記錄支付紀錄檔
  • 如果訂單未支付,則呼叫關單介面關閉訂單,並更新商戶端訂單狀態
  • @param orderNo

*/ @Override

public void checkOrderStatus(String orderNo) throws Exception {

log.warn("根據訂單號核實訂單狀態 ===> {}", orderNo);

//呼叫微信支付查單介面

String result = this.queryOrder(orderNo);

Gson gson = new Gson();

Map resultMap = gson.fromJson(result, HashMap.class);

//獲取微信支付端的訂單狀態

Object tradeState = resultMap.get("trade_state");

//判斷訂單狀態if(WxTradeState.SUCCESS.getType().equals(tradeState)){

log.warn("核實訂單已支付 ===> {}", orderNo);

//如果確認訂單已支付則更新本地訂單狀態orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);

//記錄支付紀錄檔paymentInfoService.createPaymentInfo(result);

}

if(WxTradeState.NOTPAY.getType().equals(tradeState)){ log.warn("核實訂單未支付 ===> {}", orderNo);

//如果訂單未支付,則呼叫關單介面this.closeOrder(orderNo);

//更新本地訂單狀態

orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CLOSED);

}

}

6.8

介面:

OrderInfo getOrderByOrderNo(String orderNo);

實現:

/**

  • 根據訂單號獲取訂單
  • @param orderNo
  • @return

*/ @Override

public OrderInfo getOrderByOrderNo(String orderNo) { QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("order_no", orderNo);

OrderInfo orderInfo = baseMapper.selectOne(queryWrapper);

return orderInfo;

}

11、申請退款API

檔案:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_9.shtml

    1. 、建立退款單
  1. 根據訂單號查詢訂單

OrderInfoService

介面:

OrderInfo getOrderByOrderNo(String orderNo);

實現:

/**

  • 根據訂單號獲取訂單
  • @param orderNo
  • @return

*/ @Override

public OrderInfo getOrderByOrderNo(String orderNo) {

QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("order_no", orderNo);

OrderInfo orderInfo = baseMapper.selectOne(queryWrapper);

return orderInfo;

}

  1. 建立退款單記錄

RefundsInfoService

介面:

RefundInfo createRefundByOrderNo(String orderNo, String reason);

實現:

@Resource

private OrderInfoService orderInfoService;

/**

  • 根據訂單號建立退款訂單
  • @param orderNo
  • @return

*/ @Override

public RefundInfo createRefundByOrderNo(String orderNo, String reason) {

//根據訂單號獲取訂單資訊

OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo);

//根據訂單號生成退款訂單

RefundInfo refundInfo = new RefundInfo(); refundInfo.setOrderNo(orderNo);//訂單編號 refundInfo.setRefundNo(OrderNoUtils.getRefundNo());//退款單編號

refundInfo.setTotalFee(orderInfo.getTotalFee());//原訂單金額(分) refundInfo.setRefund(orderInfo.getTotalFee());//退款金額(分) refundInfo.setReason(reason);//退款原因

//儲存退款訂單baseMapper.insert(refundInfo);

return refundInfo;

}

    1. 、更新退款單

RefundInfoService

介面:

void updateRefund(String content);

實現:

/**

  • 記錄退款記錄
  • @param content

*/ @Override

public void updateRefund(String content) {

//將json字串轉換成Map Gson gson = new Gson();

Map<String, String> resultMap = gson.fromJson(content, HashMap.class);

//根據退款單編號修改退款單

QueryWrapper<RefundInfo> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("refund_no", resultMap.get("out_refund_no"));

//設定要修改的欄位

RefundInfo refundInfo = new RefundInfo();

refundInfo.setRefundId(resultMap.get("refund_id"));//微信支付退款單號

//查詢退款和申請退款中的返回引數if(resultMap.get("status") != null){

refundInfo.setRefundStatus(resultMap.get("status"));//退款狀態 refundInfo.setContentReturn(content);//將全部響應結果存入資料庫的content欄位

}

//退款回撥中的回撥引數if(resultMap.get("refund_status") != null){

refundInfo.setRefundStatus(resultMap.get("refund_status"));//退款狀態 refundInfo.setContentNotify(content);//將全部響應結果存入資料庫的content欄位

}

//更新退款單

baseMapper.update(refundInfo, queryWrapper);

}

    1. 、申請退款
  1. WxPayController

@ApiOperation("申請退款") @PostMapping("/refunds/{orderNo}/{reason}")

public R refunds(@PathVariable String orderNo, @PathVariable String reason) throws Exception {

log.info("申請退款"); wxPayService.refund(orderNo, reason); return R.ok();

}

  1. WxPayService

介面:

void refund(String orderNo, String reason) throws Exception;

實現:

@Resource

private RefundInfoService refundsInfoService;

/**

  • 退款
  • @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();

}

}

12、查詢退款API

檔案:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_10.shtml

    1. 、查單介面的呼叫
  1. WxPayController

/**

  • 查詢退款
  • @param refundNo
  • @return
  • @throws Exception

*/

@ApiOperation("查詢退款:測試用") @GetMapping("/query-refund/{refundNo}")

public R queryRefund(@PathVariable String refundNo) throws Exception {

log.info("查詢退款");

String result = wxPayService.queryRefund(refundNo);

return R.ok().setMessage("查詢成功").data("result", result);

}

  1. WxPayService

介面:

String queryRefund(String orderNo) throws Exception;

實現:

/**

  • 查詢退款介面呼叫
  • @param refundNo
  • @return

*/ @Override

public String queryRefund(String refundNo) throws Exception {

log.info("查詢退款介面呼叫 ===> {}", refundNo);

String url = String.format(WxApiType.DOMESTIC_REFUNDS_QUERY.getType(), refundNo);

url = wxPayConfig.getDomain().concat(url);

//建立遠端Get 請求物件

HttpGet httpGet = new HttpGet(url); httpGet.setHeader("Accept", "application/json");

//完成簽名並執行請求

CloseableHttpResponse response = wxPayClient.execute(httpGet);

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);

}

return bodyAsString;

} finally {

response.close();

}

}

    1. 、定時查詢退款中的訂單
  1. WxPayTask

/**

* 從第0秒開始每隔30秒執行1次,查詢建立超過5分鐘,並且未成功的退款單

*/

@Scheduled(cron = "0/30 * * * * ?")

public void refundConfirm() throws Exception { log.info("refundConfirm 被執行 ");

//找出申請退款超過5分鐘並且未成功的退款單List<RefundInfo> refundInfoList =

refundInfoService.getNoRefundOrderByDuration(5);

for (RefundInfo refundInfo : refundInfoList) { String refundNo = refundInfo.getRefundNo(); log.warn("超時未退款的退款單號 ===> {}", refundNo);

//核實訂單狀態:呼叫微信支付查詢退款介面wxPayService.checkRefundStatus(refundNo);

}

}

  1. RefundInfoService

介面

List<RefundInfo> getNoRefundOrderByDuration(int minutes);

實現

/**

  • 找出申請退款超過minutes分鐘並且未成功的退款單
  • @param minutes
  • @return

*/ @Override

public List<RefundInfo> getNoRefundOrderByDuration(int minutes) {

//minutes分鐘之前的時間

Instant instant = Instant.now().minus(Duration.ofMinutes(minutes));

QueryWrapper<RefundInfo> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("refund_status", WxRefundStatus.PROCESSING.getType()); queryWrapper.le("create_time", instant);

List<RefundInfo> refundInfoList = baseMapper.selectList(queryWrapper); return refundInfoList;

}

    1. 、處理超時未退款訂單

WxPayService

核實訂單狀態介面:

void checkRefundStatus(String refundNo);

實現:

/**

  • 根據退款單號核實退款單狀態
  • @param refundNo
  • @return

*/

@Transactional(rollbackFor = Exception.class) @Override

public void checkRefundStatus(String refundNo) throws Exception {

log.warn("根據退款單號核實退款單狀態 ===> {}", refundNo);

//呼叫查詢退款單介面

String result = this.queryRefund(refundNo);

//組裝json請求體字串 Gson gson = new Gson();

Map<String, String> resultMap = gson.fromJson(result, HashMap.class);

//獲取微信支付端退款狀態

String status = resultMap.get("status"); String orderNo = resultMap.get("out_trade_no");

if (WxRefundStatus.SUCCESS.getType().equals(status)) {

log.warn("核實訂單已退款成功 ===> {}", refundNo);

//如果確認退款成功,則更新訂單狀態orderInfoService.updateStatusByOrderNo(orderNo,

OrderStatus.REFUND_SUCCESS);

//更新退款單refundsInfoService.updateRefund(result);

}

if (WxRefundStatus.ABNORMAL.getType().equals(status)) { log.warn("核實訂單退款異常 ===> {}", refundNo);

//如果確認退款成功,則更新訂單狀態orderInfoService.updateStatusByOrderNo(orderNo,

OrderStatus.REFUND_ABNORMAL);

//更新退款單refundsInfoService.updateRefund(result);

}

}

13、退款結果通知API

檔案:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_11.shtml

    1. 、接收退款通知

WxPayController

/**

  • 退款結果通知
  • 退款狀態改變後,微信會把相關退款結果傳送給商戶。

*/ @PostMapping("/refunds/notify")

public String refundsNotify(HttpServletRequest request, HttpServletResponse response){

log.info("退款通知執行"); Gson gson = new Gson();

Map<String, String> map = new HashMap<>();//應答物件

try {

//處理通知引數

String body = HttpUtils.readData(request);

Map<String, Object> bodyMap = gson.fromJson(body, HashMap.class);

String requestId = (String)bodyMap.get("id");

log.info("支付通知的id ===> {}", requestId);

//簽名的驗證

WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest

= new WechatPay2ValidatorForRequest(verifier, requestId, body); if(!wechatPay2ValidatorForRequest.validate(request)){

log.error("通知驗籤失敗");

//失敗應答

response.setStatus(500); map.put("code", "ERROR");

map.put("message", "通知驗籤失敗");

return gson.toJson(map);

}

log.info("通知驗籤成功");

//處理退款單wxPayService.processRefund(bodyMap);

//成功應答response.setStatus(200); map.put("code", "SUCCESS");

map.put("message", "成功"); return gson.toJson(map);

} catch (Exception e) { e.printStackTrace();

//失敗應答

response.setStatus(500); map.put("code", "ERROR");

map.put("message", "失敗");

return gson.toJson(map);

}

}

    1. 、處理訂單和退款單

WxPayService

介面:

void processRefund(Map<String, Object> bodyMap) throws Exception;

實現:

/**

* 處理退款單

*/

@Transactional(rollbackFor = Exception.class) @Override

public void processRefund(Map<String, Object> bodyMap) throws Exception {

log.info("退款單");

//解密報文

String plainText = decryptFromResource(bodyMap);

//將明文轉換成map

Gson gson = new Gson();

HashMap plainTextMap = gson.fromJson(plainText, HashMap.class); String orderNo = (String)plainTextMap.get("out_trade_no");

if(lock.tryLock()){ try {

String orderStatus = orderInfoService.getOrderStatus(orderNo);

if (!OrderStatus.REFUND_PROCESSING.getType().equals(orderStatus)) { return;

}

//更新訂單狀態orderInfoService.updateStatusByOrderNo(orderNo,

OrderStatus.REFUND_SUCCESS);

//更新退款單refundsInfoService.updateRefund(plainText);

} finally {

//要主動釋放鎖lock.unlock();

}

}

}

14、賬單

    1. 、申請交易賬單和資金賬單
  1. WxPayController

@ApiOperation("獲取賬單url:測試用") @GetMapping("/querybill/{billDate}/{type}") public R queryTradeBill(

@PathVariable String billDate,

@PathVariable String type) throws Exception {

log.info("獲取賬單url");

String downloadUrl = wxPayService.queryBill(billDate, type);

return R.ok().setMessage("獲取賬單url成功").data("downloadUrl", downloadUrl);

}

  1. WxPayService

介面:

String queryBill(String billDate, String type) throws Exception;

實現

/**

  • 申請賬單
  • @param billDate
  • @param type
  • @return
  • @throws Exception

*/

@Override

public String queryBill(String billDate, String type) throws Exception { log.warn("申請賬單介面呼叫 {}", billDate);

String url = ""; if("tradebill".equals(type)){

url = WxApiType.TRADE_BILLS.getType();

}else if("fundflowbill".equals(type)){

url = WxApiType.FUND_FLOW_BILLS.getType();

}else{

throw new RuntimeException("不支援的賬單型別");

}

url = wxPayConfig.getDomain().concat(url).concat("? bill_date=").concat(billDate);

//建立遠端Get 請求物件

HttpGet httpGet = new HttpGet(url); httpGet.addHeader("Accept", "application/json");

//使用wxPayClient傳送請求得到響應

CloseableHttpResponse response = wxPayClient.execute(httpGet); 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);

}

//獲取賬單下載地址

Gson gson = new Gson();

Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);

return resultMap.get("download_url");

} finally {

response.close();

}

}

    1. 、下載賬單

WxPayController

@ApiOperation("下載賬單") @GetMapping("/downloadbill/{billDate}/{type}") public R downloadBill(

@PathVariable String billDate,

@PathVariable String type) throws Exception {

log.info("下載賬單");

String result = wxPayService.downloadBill(billDate, type);

return R.ok().data("result", result);

}

  1. WxPayService

介面:

String downloadBill(String billDate, String type) throws Exception;

實現:

/**

  • 下載賬單
  • @param billDate
  • @param type
  • @return
  • @throws Exception

*/ @Override

public String downloadBill(String billDate, String type) throws Exception { log.warn("下載賬單介面呼叫 {}, {}", billDate, type);

//獲取賬單url地址

String downloadUrl = this.queryBill(billDate, type);

//建立遠端Get 請求物件

HttpGet httpGet = new HttpGet(downloadUrl); httpGet.addHeader("Accept", "application/json");

//使用wxPayClient傳送請求得到響應

CloseableHttpResponse response = wxPayNoSignClient.execute(httpGet); 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);

}

return bodyAsString;

} finally {

response.close();

}

}

五、基礎支付API V2

1、V2和V3的比較

2、引入依賴和工具

2.1、引入依賴

<!--微信支付-->

<dependency>

<groupId>com.github.wxpay</groupId>

<artifactId>wxpay-sdk</artifactId>

<version>0.0.3</version>

</dependency>

2.1、複製工具類

    1. 、新增商戶APIv2 key

yml檔案

# APIv2金鑰

wxpay.partnerKey: T6m9iK73b0kn9g5v426MKfHQH7X8rKwb

WxPayConfig.java

private String partnerKey;

    1. 、新增列舉

enum WxApiType

/**

* Native下單V2

*/ NATIVE_PAY_V2("/pay/unifiedorder"),

enum WxNotifyType

/**

* 支付通知V2

*/

NATIVE_NOTIFY_V2("/api/wx-pay-v2/native/notify"),

3、統一下單

    1. 、建立WxPayV2Controller

package com.atguigu.paymentdemo.controller;

import com.atguigu.paymentdemo.service.WxPayService; import com.atguigu.paymentdemo.vo.R;

import io.swagger.annotations.Api;

import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j;

import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;

import javax.servlet.http.HttpServletRequest;

import java.util.Map;

@CrossOrigin //跨域 @RestController @RequestMapping("/api/wx-pay-v2") @Api(tags = "網站微信支付APIv2") @Slf4j

public class WxPayV2Controller {

@Resource

private WxPayService wxPayService;

/**

  • Native下單
  • @param productId
  • @return
  • @throws Exception

*/

@ApiOperation("呼叫統一下單API,生成支付二維條碼") @PostMapping("/native/{productId}")

public R createNative(@PathVariable Long productId, HttpServletRequest request) throws Exception {

log.info("發起支付請求 v2");

String remoteAddr = request.getRemoteAddr();

Map<String, Object> map = wxPayService.nativePayV2(productId, remoteAddr);

return R.ok().setData(map);

}

}

    1. 、WxPayService

介面:

Map<String, Object> nativePayV2(Long productId, String remoteAddr) throws Exception;

實現:

@Override

public Map<String, Object> nativePayV2(Long productId, String remoteAddr) 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");

HttpClientUtils client = new HttpClientUtils("https://api.mch.weixin.qq.com/pay/unifiedorder");

//組裝介面引數

Map<String, String> params = new HashMap<>(); params.put("appid", wxPayConfig.getAppid());//關聯的公眾號的appid params.put("mch_id", wxPayConfig.getMchId());//商戶號

params.put("nonce_str", WXPayUtil.generateNonceStr());//生成隨機字串

params.put("body", orderInfo.getTitle()); params.put("out_trade_no", orderInfo.getOrderNo());

//注意,這裡必須使用字串型別的引數(總金額:分) String totalFee = orderInfo.getTotalFee() + ""; params.put("total_fee", totalFee);

params.put("spbill_create_ip", remoteAddr); params.put("notify_url",

wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType())); params.put("trade_type", "NATIVE");

//將引數轉換成xml字串格式:生成帶有簽名的xml格式字串 String xmlParams = WXPayUtil.generateSignedXml(params,

wxPayConfig.getPartnerKey());

log.info("\n xmlParams:\n" + xmlParams);

client.setXmlParam(xmlParams);//將引數放入請求物件的方法體 client.setHttps(true);//使用https形式傳送 client.post();//傳送請求

String resultXml = client.getContent();//得到響應結果 log.info("\n resultXml:\n" + resultXml);

//將xml響應結果轉成map物件

Map<String, String> resultMap = WXPayUtil.xmlToMap(resultXml);

//錯誤處理if("FAIL".equals(resultMap.get("return_code")) ||

"FAIL".equals(resultMap.get("result_code"))){

log.error("微信支付統一下單錯誤 ===> {} ", resultXml); throw new RuntimeException("微信支付統一下單錯誤");

}

//二維條碼

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;

}

4、支付回撥

@Resource

private WxPayService wxPayService;

@Resource

private WxPayConfig wxPayConfig;

@Resource

private OrderInfoService orderInfoService;

@Resource

private PaymentInfoService paymentInfoService;

private final ReentrantLock lock = new ReentrantLock();

/**

  • 支付通知
  • 微信支付通過支付通知介面將使用者支付成功訊息通知給商戶

*/

@PostMapping("/native/notify")

public String wxNotify(HttpServletRequest request) throws Exception {

System.out.println("微信傳送的回撥");

Map<String, String> returnMap = new HashMap<>();//應答物件

//處理通知引數

String body = HttpUtils.readData(request);

//驗籤

if(!WXPayUtil.isSignatureValid(body, wxPayConfig.getPartnerKey())) { log.error("通知驗籤失敗");

//失敗應答

returnMap.put("return_code", "FAIL"); returnMap.put("return_msg", "驗籤失敗");

String returnXml = WXPayUtil.mapToXml(returnMap); return returnXml;

}

//解析xml資料

Map<String, String> notifyMap = WXPayUtil.xmlToMap(body);

//判斷通訊和業務是否成功if(!"SUCCESS".equals(notifyMap.get("return_code")) ||

!"SUCCESS".equals(notifyMap.get("result_code"))) { log.error("失敗");

//失敗應答

returnMap.put("return_code", "FAIL"); returnMap.put("return_msg", "失敗");

String returnXml = WXPayUtil.mapToXml(returnMap); return returnXml;

}

//獲取商戶訂單號

String orderNo = notifyMap.get("out_trade_no");

OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo);

//並校驗返回的訂單金額是否與商戶側的訂單金額一致

if (orderInfo != null && orderInfo.getTotalFee() != Long.parseLong(notifyMap.get("total_fee"))) {

log.error("金額校驗失敗");

//失敗應答

returnMap.put("return_code", "FAIL"); returnMap.put("return_msg", "金額校驗失敗");

String returnXml = WXPayUtil.mapToXml(returnMap); return returnXml;

}

//處理訂單if(lock.tryLock()){

try {

//處理重複的通知

//介面呼叫的冪等性:無論介面被呼叫多少次,產生的結果是一致的。

String orderStatus = orderInfoService.getOrderStatus(orderNo); if(OrderStatus.NOTPAY.getType().equals(orderStatus)){

//更新訂單狀態

orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);

//記錄支付紀錄檔paymentInfoService.createPaymentInfo(body);

}

} finally {

//要主動釋放鎖lock.unlock();

}

}

returnMap.put("return_code", "SUCCESS"); returnMap.put("return_msg", "OK");

String returnXml = WXPayUtil.mapToXml(returnMap); log.info("支付成功,已應答");

return returnXml;

}