-- 微信支付 --
前提条件
- 微信支付商户号:必须有一个有效的微信支付商户号。
- 公众号或小程序:需要有一个经过认证的微信公众号或小程序,用于生成支付授权。
- 微信支付API证书:用于在服务器端调用微信支付API时进行身份验证。
引入依赖
<!--微信支付-->
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-java</artifactId>
<version>0.2.12</version>
</dependency>
编写配置文件
- 这里默认已经获得微信支付需要的相关参数
- 下面直接进行代码Coding阶段
wx:
# 微信小程序appid
app-id: wxcb25xxxxxxxxxxxx
# 商户号
mch-id: 16xxxxxxxx
# 证书序列号
mch-serial-no: 59BAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# api密钥
api-key: tonxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# 回调接口地址(订单不做回调可以不用配)
notify-url: https://15x.xx.xxx.xx/pay/payCallBack
# 证书地址
key-path: /apiclient_key.pem
cert-path: /apiclient_cert.pem
cert-p12-path: /apiclient_cert.p12
msgDataFormat: JSON
编写配置WxProperties
/**
* @Author:Ccoo
* @Date:2024/3/21 20:40
*/
@Component
@ConfigurationProperties(prefix = "wx")
@Data
@ToString
public class WxProperties {
/**
* 设置微信小程序的appid
*/
private String appId;
/**
* 微信支付分配的商户号
*/
private String mchId;
/**
* 密钥文件的路径
*/
private String keyPath;
/**
* 商户证书的路径
*/
private String certPath;
/**
* 商户密钥,用于签名和验证
*/
private String apiKey;
/**
* 接收微信支付异步通知的URL
*/
private String notifyUrl;
/**
* 商户API证书序列号
*/
private String mchSerialNo;
/**
* 消息格式,XML或者JSON
*/
private String msgDataFormat;
}
编写配置WechatAutoConfiguration
import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import com.wechat.pay.java.core.notification.NotificationConfig;
import com.wechat.pay.java.core.notification.NotificationParser;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.wechat.pay.java.service.payments.jsapi.JsapiServiceExtension;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import com.wechat.pay.java.core.Config;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.security.cert.*;
@Slf4j
@Configuration
@RequiredArgsConstructor
public class WechatAutoConfiguration {
private final WxProperties wxProperties;
private final ResourceLoader resourceLoader;
private static final String CLASS_PATH = "classpath:";
/**
* 自动更新证书
*
* @return RSAAutoCertificateConfig
*/
@Bean
public Config config() throws IOException {
String merchantSerialNumber = getCertificateSerialNumber(wxProperties.getCertPath());
String privateKey = readResourceAsString(wxProperties.getKeyPath());
return new RSAAutoCertificateConfig.Builder()
.merchantId(wxProperties.getMchId())
.privateKey(privateKey)
.merchantSerialNumber(merchantSerialNumber)
.apiV3Key(wxProperties.getApiKey())
.build();
}
/**
* 微信支付对象
*
* @param config Config
* @return JsapiServiceExtension
*/
@Bean
public JsapiServiceExtension jsapiServiceExtension(Config config) {
return new JsapiServiceExtension.Builder().config(config).build();
}
/**
* 微信回调对象
*
* @param config Config
* @return NotificationParser
*/
@Bean
public NotificationParser notificationParser(Config config) {
return new NotificationParser((NotificationConfig) config);
}
/**
* 读取私钥文件,将文件流读取成 string
*
* @param path 文件路径
* @return 文件内容
* @throws IOException
*/
private String readResourceAsString(String path) throws IOException {
Resource resource = resourceLoader.getResource(CLASS_PATH + path);
try (InputStream inputStream = resource.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
StringBuilder stringBuilder = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
stringBuilder.append(line);
}
return stringBuilder.toString();
}
}
/**
* 获取证书序列号
*
* @param certPath 证书路径
* @return 证书序列号
* @throws IOException
*/
private String getCertificateSerialNumber(String certPath) throws IOException {
Resource resource = resourceLoader.getResource(CLASS_PATH + certPath);
try (InputStream inputStream = resource.getInputStream()) {
X509Certificate certificate = getCertificate(inputStream);
return certificate.getSerialNumber().toString(16).toUpperCase();
}
}
/**
* 获取证书,将文件流转成证书文件
*
* @param inputStream 证书文件流
* @return {@link X509Certificate} 证书对象
*/
public static X509Certificate getCertificate(InputStream inputStream) {
try {
CertificateFactory cf = CertificateFactory.getInstance("X509");
X509Certificate cert = (X509Certificate) cf.generateCertificate(inputStream);
cert.checkValidity();
return cert;
} catch (CertificateExpiredException e) {
throw new RuntimeException("证书已过期", e);
} catch (CertificateNotYetValidException e) {
throw new RuntimeException("证书尚未生效", e);
} catch (CertificateException e) {
throw new RuntimeException("无效的证书", e);
}
}
}
- 微信JSAPI支付官方文档
JSAPI下单 - JSAPI支付 | 微信支付商户文档中心
微信支付相关代码
Controller控制器代码
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* @author Ccoo
* @since 2024-8-23
*/
@RestController
@RequestMapping("/wechat")
public class WeChatController {
@Autowired
private WeChatService wechatService;
@ApiOperation("微信预支付")
@PostMapping("/prePay")
public R<?> createPreOrder(@RequestBody PrePayReq prePayReq) {
return R.ok(wechatService.prepayWithRequest(prePayReq));
}
@ApiOperation("查询订单")
@GetMapping("/queryOrder")
public R<?> queryOrder(@RequestParam String outTradeNo) {
return R.ok(wechatService.queryStatus(outTradeNo));
}
@ApiOperation("取消订单")
@PostMapping("/closeOrder")
public R<?> closeOrder(@RequestParam String outTradeNo) {
//方法没有返回值,意味着成功时API返回204 No Content
return R.ok(wechatService.closeOrder(outTradeNo));
}
}
Service接口层代码
import com.itheima.mp.domain.R;
import com.itheima.mp.domain.wechat.PrePayReq;
/**
* @author Ccoo
* 2024/8/23
*/
public interface WeChatService {
/**
* 微信预支付
* @param prePayReq 预支付请求
* @return 结果
*/
R<?> prepayWithRequest(PrePayReq prePayReq);
/**
* 查询订单状态
* @param outTradeNo 订单号
* @return 结果
*/
R<?> queryStatus(String outTradeNo);
/**
* 关闭订单
* @param outTradeNo 订单号
*/
R<?> closeOrder(String outTradeNo);
}
ServiceImpl实现层代码
import com.wechat.pay.java.core.exception.ServiceException;
import com.wechat.pay.java.service.payments.jsapi.JsapiServiceExtension;
import com.wechat.pay.java.service.payments.jsapi.model.*;
import com.wechat.pay.java.service.payments.model.Transaction;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.security.SecureRandom;
/**
* @author Ccoo
* 2024/8/23
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class WeChatServiceImpl implements WeChatService {
private final WxProperties wxProperties;
private final JsapiServiceExtension jsapiService;
/**
* 微信预支付
* @param prePayReq 预支付请求
* @return 结果
* 商户系统先调用该接口在微信支付服务后台生成预支付交易单,
* 返回正确的预支付交易会话标识后再按Native、JSAPI、APP等不同场景生成交易串调起支付。
*/
@Override
public R<?> prepayWithRequest(PrePayReq prePayReq) {
// 判断参数是否为空
if (prePayReq == null || prePayReq.getAmount() == null || prePayReq.getDescription() == null || prePayReq.getOpenid() == null) {
return R.fail(HttpStatus.BAD_REQUEST, "参数错误");
}
PrepayRequest request = new PrepayRequest();
String outTradeNo = System.currentTimeMillis() + String.format("%06d", new SecureRandom().nextInt(999999));
// 1. 设置AppId
request.setAppid(wxProperties.getAppId());
// 2. 设置商户号
request.setMchid(wxProperties.getMchId());
// 3. 设置商品描述
request.setDescription(prePayReq.getDescription());
// 4. 设置商户订单号
request.setOutTradeNo(outTradeNo);
// 5. 设置支付回调地址
request.setNotifyUrl(wxProperties.getNotifyUrl());
// 6. 设置金额
Amount amount = new Amount();
amount.setTotal(prePayReq.getAmount());
amount.setCurrency("CNY");
request.setAmount(amount);
// 7. 设置支付者信息
Payer payer = new Payer();
payer.setOpenid(prePayReq.getOpenid());
request.setPayer(payer);
try {
// 请求微信服务器JSAPI下单
PrepayWithRequestPaymentResponse response = jsapiService.prepayWithRequestPayment(request);
return R.ok(response);
} catch (ServiceException e) {
return R.fail(e.getHttpStatusCode(), e.getErrorMessage());
}
}
/**
* 查询订单
* @param outTradeNo 订单号
* @return 结果
* 商户可以通过查询订单接口主动查询订单状态,完成下一步的业务逻辑。
* 查询订单可通过微信支付订单号 (opens new window)和商户订单号 (opens new window)两种方式查询。
* 需要调用查询接口的情况:
* 1.当商户后台、网络、服务器等出现异常,商户系统最终未接收到支付通知。
* 2.调用支付接口后,返回系统错误或未知交易状态情况。
* 3.调用付款码支付API,返回USERPAYING的状态。
* 4.调用关单或撤销接口API之前,需确认支付状态。
*/
@Override
public R<?> queryStatus(String outTradeNo) {
// 判断参数是否为空
if (outTradeNo == null || outTradeNo.isEmpty()) {
return R.fail(HttpStatus.BAD_REQUEST, "参数错误");
}
QueryOrderByOutTradeNoRequest request = new QueryOrderByOutTradeNoRequest();
// 1. 设置商户订单号
request.setOutTradeNo(outTradeNo);
// 2. 设置商户号
request.setMchid(wxProperties.getMchId());
try {
// 请求微信服务器查询订单
Transaction response = jsapiService.queryOrderByOutTradeNo(request);
return R.ok(response);
} catch (ServiceException e) {
return R.fail(e.getHttpStatusCode(), e.getErrorMessage());
}
}
/**
* 取消订单
* @param outTradeNo 订单号
* 关闭订单,以下情况需要调用关单接口:
* 商户订单支付失败需要生成新单号重新发起支付,要对原订单号调用关单,避免重复支付;
* 系统下单后,用户支付超时,系统退出不再受理,避免用户继续,请调用关单接口。
*/
@Override
public R<?> closeOrder(String outTradeNo) {
// 判断参数是否为空
if (outTradeNo == null || outTradeNo.isEmpty()) {
return R.fail(HttpStatus.BAD_REQUEST, "参数错误");
}
CloseOrderRequest request = new CloseOrderRequest();
// 1. 设置商户订单号
request.setOutTradeNo(outTradeNo);
// 2. 设置商户号
request.setMchid(wxProperties.getMchId());
try {
// 请求微信服务器关闭订单
jsapiService.closeOrder(request);
return R.ok();
} catch (ServiceException e) {
return R.fail(e.getHttpStatusCode(), e.getErrorMessage());
}
}
}
部分实体类
统一响应实体类
package com.itheima.mp.domain;
import java.io.Serializable;
/**
* 响应信息主体
*
* @author Ccoo
*/
public class R<T> implements Serializable
{
private static final long serialVersionUID = 1L;
/** 成功 */
public static final int SUCCESS = 200;
/** 失败 */
public static final int FAIL = 500;
private int code;
private String msg;
private T data;
public static <T> R<T> ok()
{
return restResult(null, SUCCESS, "操作成功");
}
public static <T> R<T> ok(String msg)
{
return restResult(null, SUCCESS, msg);
}
public static <T> R<T> ok(T data)
{
return restResult(data, SUCCESS, "操作成功");
}
public static <T> R<T> ok(T data, String msg)
{
return restResult(data, SUCCESS, msg);
}
public static <T> R<T> fail()
{
return restResult(null, FAIL, "操作失败");
}
public static <T> R<T> fail(String msg)
{
return restResult(null, FAIL, msg);
}
public static <T> R<T> fail(T data)
{
return restResult(data, FAIL, "操作失败");
}
public static <T> R<T> fail(T data, String msg)
{
return restResult(data, FAIL, msg);
}
public static <T> R<T> fail(int code, String msg)
{
return restResult(null, code, msg);
}
private static <T> R<T> restResult(T data, int code, String msg)
{
R<T> apiResult = new R<>();
apiResult.setCode(code);
apiResult.setData(data);
apiResult.setMsg(msg);
return apiResult;
}
public int getCode()
{
return code;
}
public void setCode(int code)
{
this.code = code;
}
public String getMsg()
{
return msg;
}
public void setMsg(String msg)
{
this.msg = msg;
}
public T getData()
{
return data;
}
public void setData(T data)
{
this.data = data;
}
}
预支付订单请求实体类
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* @author Ccoo
* 2024/8/24
*/
@Data
@ApiModel(description = "订单预支付实体类")
public class PrePayReq {
@ApiModelProperty("商品描述")
private String description;
@ApiModelProperty("订单金额")
private Integer amount;
@ApiModelProperty("用户标识")
private String openid;
}
响应状态码枚举类
/**
* 返回状态码
*
* @author ruoyi
*/
public class HttpStatus
{
/**
* 操作成功
*/
public static final int SUCCESS = 200;
/**
* 对象创建成功
*/
public static final int CREATED = 201;
/**
* 请求已经被接受
*/
public static final int ACCEPTED = 202;
/**
* 操作已经执行成功,但是没有返回数据
*/
public static final int NO_CONTENT = 204;
/**
* 资源已被移除
*/
public static final int MOVED_PERM = 301;
/**
* 重定向
*/
public static final int SEE_OTHER = 303;
/**
* 资源没有被修改
*/
public static final int NOT_MODIFIED = 304;
/**
* 参数列表错误(缺少,格式不匹配)
*/
public static final int BAD_REQUEST = 400;
/**
* 未授权
*/
public static final int UNAUTHORIZED = 401;
/**
* 访问受限,授权过期
*/
public static final int FORBIDDEN = 403;
/**
* 资源,服务未找到
*/
public static final int NOT_FOUND = 404;
/**
* 不允许的http方法
*/
public static final int BAD_METHOD = 405;
/**
* 资源冲突,或者资源被锁
*/
public static final int CONFLICT = 409;
/**
* 不支持的数据,媒体类型
*/
public static final int UNSUPPORTED_TYPE = 415;
/**
* 系统内部错误
*/
public static final int ERROR = 500;
/**
* 接口未实现
*/
public static final int NOT_IMPLEMENTED = 501;
/**
* 系统警告消息
*/
public static final int WARN = 601;
}