0%

【工作技能】微信支付-native支付

微信支付对接


1 参数准备

  • 最终需要的参数
    • appid:应用id
    • mchid:商户id
    • mchSerialNo:证书序列号
    • apiV3key:apiV3密钥
    • apiclient_key.pem:商户私钥文件

2 环境准备

  • (1)yaml文件
    1
    2
    3
    4
    5
    6
    7
    wechat:
    appId: 应用id
    mchId: 商户id
    mchSerialNo: 证书序列号
    apiV3Key: apiV3密钥
    mchPrivateKeyPath: 商户私钥文件地址路径
    notifyUrl: 通知接受接口地址(按实际情况填写)
  • (2)依赖引入
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    <!-- web启动 -->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- yaml参数注入 -->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
    </dependency>

    <!-- 注解生成get、set方法 -->
    <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    </dependency>

    <!-- 微信支付SDK -->
    <dependency>
    <groupId>com.github.wechatpay-apiv3</groupId>
    <artifactId>wechatpay-apache-httpclient</artifactId>
    <version>0.3.0</version>
    </dependency>

    <!-- json处理 -->
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.78</version>
    </dependency>
  • (3)微信支付配置(工具)类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    @Data
    @Component
    @ConfigurationProperties(prefix = "wechat")
    public class WechatConfig {

    // 应用id
    private String appId;

    // 商户id
    private String mchId;

    // 证书序列号
    private String mchSerialNo;

    // apiV3密钥
    private String apiV3Key;

    // 商户私钥文件路径
    private String mchPrivateKeyPath;

    // 通知接收地址
    private String notifyUrl;

    /**
    * 获取商户私钥文件
    * @return 私钥
    */
    public PrivateKey getPrivateKey() {
    try {
    return PemUtil.loadPrivateKey(new FileInputStream(this.mchPrivateKeyPath));
    } catch (FileNotFoundException e) {
    throw new RuntimeException("私钥文件不存在", e);
    }
    }

    /**
    * 获取签名验证器
    * @param privateKey 商户私钥
    * @return
    */
    public ScheduledUpdateCertificatesVerifier getVerifier(PrivateKey privateKey) {
    // 私钥签名
    PrivateKeySigner privateKeySigner = new PrivateKeySigner(this.mchSerialNo, privateKey);
    // 身份认证
    WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(this.mchId, privateKeySigner);
    // 验证器
    return new ScheduledUpdateCertificatesVerifier(wechatPay2Credentials, this.apiV3Key.getBytes(StandardCharsets.UTF_8));
    }

    /**
    * 获取http请求客户端
    * @return
    */
    public CloseableHttpClient getHttpClient() {
    // 商户私钥
    PrivateKey privateKey = this.getPrivateKey();
    // 验证器
    ScheduledUpdateCertificatesVerifier verifier = this.getVerifier(privateKey);
    WechatPay2Validator validator = new WechatPay2Validator(verifier);
    // httpClient
    return WechatPayHttpClientBuilder.create()
    .withMerchant(this.mchId, this.mchSerialNo, privateKey)
    .withValidator(validator)
    .build();
    }
    }

3 下单(获取qrCode)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Autowired
private WechatConfig wechatConfig;

@GetMapping("/createOrder")
public String createOrder() throws Exception{
String orderNo = "NO" + System.currentTimeMillis();
// 封装必填参数(具体看文档:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml)
Map<String, Object> paramsMap = new HashMap<>();
paramsMap.put("appid", wechatConfig.getAppId());
paramsMap.put("mchid", wechatConfig.getMchId());
paramsMap.put("description", "下单测试");
paramsMap.put("out_trade_no", orderNo);
paramsMap.put("notify_url", wechatConfig.getNotifyUrl());
Map<String, Object> amountMap = new HashMap<>();
amountMap.put("total", 1);
paramsMap.put("amount", amountMap);
// 参数处理
String paramsJson = JSONObject.toJSONString(paramsMap);
StringEntity entity = new StringEntity(paramsJson, StandardCharsets.UTF_8);
entity.setContentType("application/json");
// 请求封装
String uri = "https://api.mch.weixin.qq.com/v3/pay/transactions/native";
HttpPost httpPost = new HttpPost(uri);
httpPost.setEntity(entity);
httpPost.setHeader("Accept", "application/json");
// 发送请求
CloseableHttpClient httpClient = wechatConfig.getHttpClient();
CloseableHttpResponse response = httpClient.execute(httpPost);
// 处理响应
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200) {
String qrCode = JSONObject.parseObject(EntityUtils.toString(response.getEntity())).getString("code_url");
return orderNo + ":" + qrCode;
}
return "发送失败 or 无响应数据";
}

4 订单查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Autowired
private WechatConfig wechatConfig;

/**
* 订单查询
* @param orderNo
* @return
* @throws Exception
*/
@GetMapping("/queryOrder")
public String queryOrder(String orderNo) throws Exception {
// 封装请求(参数具体看文档:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_2.shtml)
HttpGet httpGet = new HttpGet("https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/" + orderNo + "?mchid=" + wechatConfig.getMchId());
httpGet.setHeader("Accept", "application/json");
// 发送请求
CloseableHttpClient httpClient = wechatConfig.getHttpClient();
CloseableHttpResponse response = httpClient.execute(httpGet);
// 数据响应
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200) {
return EntityUtils.toString(response.getEntity());
}
return "无响应数据 or 查询失败";
}

5 支付成功通知接收

支付通知接收需要一个外网可以访问到的接口,给微信进行请求发送,如电脑没有可以访问的外网,则可以进行内网穿透来实现

5.1 内网穿透

  • ngrok下载:https://ngrok.com
  • 注意 :需要注册获取authtoken,注册建议使用github来注册
1
2
3
4
5
// 关联ngrok(token在注册登录后,可以在后台看到)
ngrok authtoken <token>

// 端口映射
ngrok http <端口号>

5.2 通知接收代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@Autowired
private WechatConfig wechatConfig;

@PostMapping("/native/notify")
public String nativeNotify(@RequestBody String body, HttpServletRequest request, HttpServletResponse response) {
Map<String, String> result = new HashMap<>();
try {
// 获取请求体数据
JSONObject bodyObject = JSONObject.parseObject(body);
// 参数验签
WechatPay2ValidatorForReq validator = new WechatPay2ValidatorForReq(
wechatConfig.getVerifier(wechatConfig.getPrivateKeyByStr()),
bodyObject.getString("id"),
body);
if (!validator.validate(request)) {
throw new Exception("验签失败");
}
// 数据解密(解密后的数据具体看文档:https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter4_4_5.shtml)
JSONObject resourceObject = bodyObject.getJSONObject("resource");
AesUtil aesUtil = new AesUtil(wechatConfig.getApiV3Key().getBytes());
String data = aesUtil.decryptToString(
resourceObject.getString("associated_data").getBytes(),
resourceObject.getString("nonce").getBytes(),
resourceObject.getString("ciphertext"));
// 订单逻辑处理...
Map<String, Object> dataMap = JSONObject.parseObject(data, Map.class);
System.out.println(dataMap);
// 处理成功
response.setStatus(200);
result.put("code", "SUCCESS");
result.put("message", "成功");
return JSONObject.toJSONString(result);
} catch (Exception e) {
e.printStackTrace();
// 处理失败
response.setStatus(500);
result.put("code", "FAILURE");
result.put("message", "失败");
return JSONObject.toJSONString(result);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
// 改写WechatPay2Validator类,用作对request请求进行参数校验
public class WechatPay2ValidatorForReq {
protected static final Logger log = LoggerFactory.getLogger(WechatPay2Validator.class);
protected static final long RESPONSE_EXPIRED_MINUTES = 5L;
protected final Verifier verifier;
protected final String body;
protected final String requestId;

public WechatPay2ValidatorForReq(Verifier verifier, String requestId, String body) {
this.verifier = verifier;
this.requestId = requestId;
this.body = body;
}

/**
* 参数异常
* @param message
* @param args
* @return
*/
protected static IllegalArgumentException parameterError(String message, Object... args) {
message = String.format(message, args);
return new IllegalArgumentException("parameter error: " + message);
}

/**
* 签名失败异常
* @param message
* @param args
* @return
*/
protected static IllegalArgumentException verifyFail(String message, Object... args) {
message = String.format(message, args);
return new IllegalArgumentException("signature verify fail: " + message);
}

/**
* 签名验证
* @param request
* @return
* @throws IOException
*/
public final boolean validate(HttpServletRequest request) throws IOException {
try {
// 参数校验
this.validateParameters(request);
// 构建验签字符串
String message = this.buildMessage(request);
// 验签
String serial = request.getHeader("Wechatpay-Serial");
String signature = request.getHeader("Wechatpay-Signature");
if (!this.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"));
} else {
return true;
}
} catch (IllegalArgumentException var5) {
log.warn(var5.getMessage());
return false;
}
}

/**
* 验证参数
* @param request
*/
protected final void validateParameters(HttpServletRequest request) {
String[] headers = new String[]{"Wechatpay-Serial", "Wechatpay-Signature", "Wechatpay-Nonce", "Wechatpay-Timestamp"};
String value = null;
for (String headerName : headers) {
value = request.getHeader(headerName);
if (value == null) {
throw parameterError("empty [%s], request-id=[%s]", headerName, requestId);
}
}
String timestampStr = value;
try {
Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestampStr));
if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= 5L) {
throw parameterError("timestamp=[%s] expires, request-id=[%s]", timestampStr, requestId);
}
} catch (NumberFormatException | DateTimeException var10) {
throw parameterError("invalid timestamp=[%s], request-id=[%s]", timestampStr, requestId);
}
}

/**
* 构建签名信息
* @param request
* @return
* @throws IOException
*/
protected final String buildMessage(HttpServletRequest request) throws IOException {
String timestamp = request.getHeader("Wechatpay-Timestamp");
String nonce = request.getHeader("Wechatpay-Nonce");
String body = this.body;
return timestamp + "\n" + nonce + "\n" + body + "\n";
}
}

6 订单关闭

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Autowired
private WechatConfig wechatConfig;

/**
* 订单关闭
* @param orderNo
* @return
* @throws Exception
*/
@GetMapping("/closeOrder/{orderNo}")
public String closeOrder(@PathVariable String orderNo) throws Exception{
// 封装必填参数(具体看文档:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_3.shtml)
Map<String, Object> paramsMap = new HashMap<>();
paramsMap.put("mchid", wechatConfig.getMchId());
// 参数处理
String paramsJson = JSONObject.toJSONString(paramsMap);
StringEntity entity = new StringEntity(paramsJson, StandardCharsets.UTF_8);
entity.setContentType("application/json");
// 封装请求
String uri = "https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/" + orderNo + "/close";
HttpPost httpPost = new HttpPost(uri);
httpPost.setEntity(entity);
httpPost.setHeader("Accept", "application/json");
// 发送请求
CloseableHttpClient httpClient = wechatConfig.getHttpClient();
CloseableHttpResponse response = httpClient.execute(httpPost);
// 响应处理
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200 || statusCode == 204) {
return "订单:" + orderNo + ",关闭成功!";
}
return "订单:" + orderNo + ",关闭失败!";
}