摘要:最近的一个项目中涉及到了支付业务,其中用到了微信支付和支付宝支付,在做的过程中也遇到些问题,所以现在总结梳理一下,分享给有需要的人,也为自己以后回顾留个思路。
一、微信支付接入准备工作:
首先,微信支付,只支持企业用户,个人用户是不能接入微信支付的,所以要想接入微信支付,首先需要有微信公众号,这个的企业才能申请。有了微信公众号,就能申请微信支付的相关内容,所以在准备开始写代码之前需要先把下面的这些参数申请好:公众账号ID、微信支付商户号、API密钥、AppSecret是APPID对应的接口密码、回调地址(回调必须保证外网能访问到此地址)、发起请求的电脑IP
二、微信支付流程说明:
有了上面提到的这些参数,那我们就可以接入微信支付了,下面我来看下微信支付的官方文档(https://pay.weixin.qq.com/wiki/doc/api/index.html)、访问该地址可以看到有多种支付方式可以选择,我们这里选择扫码支付的方式(https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_1)
这里我们选择模式二,下面看下模式二的时序图,如下图:
模式二与模式一相比,流程更为简单,不依赖设置的回调支付URL。商户后台系统先调用微信支付的统一下单接口,微信后台系统返回链接参数code_url,商户后台系统将code_url值生成二维码图片,用户使用微信客户端扫码后发起支付。注意:code_url有效期为2小时,过期后扫码不能再发起支付。
三、微信支付所需Maven依赖
<!--微信支付SDK-->
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-apache-httpclient</artifactId>
<version>0.3.0</version>
</dependency>
<!-- json处理器:引入gson依赖 -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.6</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.12</version>
</dependency>
<!-- 二维码 -->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.3.3</version>
</dependency>
<!-- 生成二维码 -->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.3.3</version>
</dependency>
四、配置文件添加微信支付所需参数
# 微信支付相关参数
wxpay:
# 商户号
mch-id: xxxxxxx
# 商户API证书序列号
mch-serial-no: xxxxxxxxxx
# 商户私钥文件
# 注意:该文件放在项目根目录下
private-key-path: ./apiclient_key.pem
# APIv3密钥
api-v3-key: xxxxxxxx
# APPID
appid: xxxxxxc27e0e7cxxx
# 微信服务器地址
domain: https://api.mch.weixin.qq.com
# 接收结果通知地址
# 注意:每次重新启动ngrok,都需要根据实际情况修改这个配置
notify-domain: https://c7c1-240e-3b5-3015-be0-1bc-9bed-fca4-d09b.ngrok.io
五、微信支付下单代码实现
1.Controller层
/**
* native下单
*/
@ApiOperation(value = "native 微信支付下单 返回Image")
@GetMapping("/native")
public BaseRes<String> nativePay(@RequestParam("packageId") Integer packageId) {
return wxPayService.nativePay(packageId);
}
/**
* JSAPI下单
*/
@ApiOperation(value = "JSAPI微信支付下单")
@GetMapping("/jsapi")
public BaseRes<String> jsapiPay(@RequestParam("packageId") Integer packageId,@RequestParam("openId") String openId) {
return wxPayService.jsapiPay(packageId,openId);
}
注意:packageId是套餐Id,可根据情况修改
2.Service层
BaseRes<String> nativePay(Integer packageId);
BaseRes<String> jsapiPay(Integer packageId, String openId);
3.实现层
/**
* Mavicat下单
* @return
* @throws Exception
*/
@Transactional(rollbackFor = Exception.class)
@Override
@SneakyThrows
public BaseRes<String> nativePay(Integer packageId){
log.info("发起Navicat支付请求");
HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType()));
CloseableHttpResponse response = wxPayExecute(packageId, null, 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");
}
Gson gson = new Gson();
//响应结果
Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
//二维码
String codeUrl = resultMap.get("code_url");
return new BaseRes<>(codeUrl,ServiceCode.SUCCESS);
//生成二维码
// WxPayUtil.makeQRCode(codeUrl);
} finally {
response.close();
}
}
/**
* JSAPI下单
* @return
*/
@Override
@SneakyThrows
public BaseRes<String> jsapiPay(Integer packageId, String openId) {
log.info("发起Navicat支付请求");
HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.JSAPI_PAY.getType()));
CloseableHttpResponse response = wxPayExecute(packageId, openId, 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("JSAPI下单失败,响应码 = " + statusCode + ",返回结果 = " + bodyAsString);
throw new IOException("request failed");
}
Gson gson = new Gson();
//响应结果
Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
String prepayId = resultMap.get("prepay_id");
return new BaseRes<>(prepayId,ServiceCode.SUCCESS);
} finally {
response.close();
}
}
// 封装统一下单方法
private CloseableHttpResponse wxPayExecute(Integer packageId,String openId,HttpPost httpPost) throws IOException {
// 获取套餐金额 还有相关信息
ChatPackage chatPackage = chatPackageMapper.selectById(packageId);
if (null == chatPackage) {
throw new NingException(ServiceCode.FAILED);
}
BigDecimal amount = chatPackage.getAmount();
if (null == amount || amount.equals(BigDecimal.ZERO)) {
throw new NingException(ServiceCode.SUCCESS);
}
// 从登录信息中获取用户信息
TokenUser loginUserInfo = CommUtils.getLoginUserInfo();
Integer userId = loginUserInfo.getUserId();
// 请求body参数
Gson gson = new Gson();
Map<String,Object> paramsMap = new HashMap<>();
paramsMap.put("appid", wxPayConfig.getAppid());
paramsMap.put("mchid", wxPayConfig.getMchId());
paramsMap.put("description", chatPackage.getName());
paramsMap.put("out_trade_no", WxPayUtil.generateOrderNumber(userId,packageId)); //订单号
paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxApiType.NATIVE_NOTIFY.getType()));
Map<String,Object> amountMap = new HashMap<>();
//由单位:元 转换为单位:分,并由Bigdecimal转换为整型
BigDecimal total = amount.multiply(new BigDecimal(100));
amountMap.put("total", total.intValue());
amountMap.put("currency", "CNY");
paramsMap.put("amount", amountMap);
// 判断是Navicat下单还是JSAPI下单 JSAPI需要传OPENID
if (StringUtils.isNotBlank(openId)) {
Map<String,Object> payerMap = new HashMap<>();
payerMap.put("openid",openId);
paramsMap.put("payer",payerMap);
}
JSONObject attachJson = new JSONObject();
attachJson.put("packageId",packageId);
attachJson.put("userId",userId);
attachJson.put("total",total);
paramsMap.put("attach",attachJson.toJSONString());
//将参数转换成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");
//完成签名并执行请求
return wxPayClient.execute(httpPost);
}
六、微信支付回调接口
1.Controller层
/**
* 支付通知
* 微信支付通过支付通知接口将用户支付成功消息通知给商户
*/
@ApiOperation(value = "支付通知", notes = "支付通知")
@PostMapping("/pay/notify")
@ClientAuthControl
public WxRes nativeNotify() {
return wxPayService.nativeNotify();
}
2.Service层
WxRes nativeNotify();
3.实现层
@Resource
private Verifier verifier;
private final ReentrantLock lock = new ReentrantLock();
@Override
@SneakyThrows
@Transactional
public WxRes nativeNotify() {
HttpServletRequest request = CommUtils.getRequest();
HttpServletResponse response = CommUtils.getResponse();
Gson gson = new Gson();
try {
//处理通知参数
String body = WxPayUtil.readData(request);
Map<String, Object> bodyMap = gson.fromJson(body, HashMap.class);
String requestId = (String) bodyMap.get("id");
//签名的验证
WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest
= new WechatPay2ValidatorForRequest(verifier, requestId, body);
if (wechatPay2ValidatorForRequest.validate(request)) {
throw new RuntimeException();
}
log.info("通知验签成功");
//处理订单
processOrder(bodyMap);
return new WxRes("SUCCESS","成功");
} catch (Exception e) {
e.printStackTrace();
response.setStatus(500);
return new WxRes("FAIL","成功");
}
}
/**
* 处理订单
*
* @param bodyMap
*/
@Transactional
@SneakyThrows
public void processOrder(Map<String, Object> bodyMap){
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");
String attach = (String) plainTextMap.get("attach");
JSONObject attachJson = JSONObject.parseObject(attach);
Integer packageId = attachJson.getInteger("packageId");
Integer userId = attachJson.getInteger("userId");
Integer total = attachJson.getInteger("total");
/*在对业务数据进行状态检查和处理之前,
要采用数据锁进行并发控制,
以避免函数重入造成的数据混乱*/
//尝试获取锁:
// 成功获取则立即返回true,获取失败则立即返回false。不必一直等待锁的释放
if (lock.tryLock()) {
try {
log.info("plainText={}",plainText);
//处理重复的通知
//接口调用的幂等性:无论接口被调用多少次,产生的结果是一致的。
String orderStatus = orderService.getOrderStatus(orderNo);
if (!OrderStatus.NOTPAY.getType().equals(orderStatus)) {
return;
}
// TODO 修改订单状态、添加支付记录等
// 通知前端用户 已完成支付
messageSocketHandle.sendMessageByUserID(userId,new TextMessage("PaySuccess"));
} finally {
//要主动释放锁
lock.unlock();
}
}
}
/**
* 对称解密
*
* @param bodyMap
* @return
*/
@SneakyThrows
private String decryptFromResource(Map<String, Object> bodyMap) {
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");
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);
return plainText;
}
七、工具类和相关配置类
1.WxPayUtil工具类
@Slf4j
public class WxPayUtil {
private static final Random random = new Random();
// 生成订单号
public static String generateOrderNumber(int userId, int packageId) {
// 获取当前时间戳
long timestamp = System.currentTimeMillis();
// 生成6位随机数
int randomNum = random.nextInt(900000) + 100000;
// 组装订单号
return String.format("%d%d%d%d", timestamp, randomNum, userId, packageId);
}
/**
* 生成二维码
* @param url
*/
public static void makeQRCode(String url){
HttpServletResponse response = CommUtils.getResponse();
//通过支付链接生成二维码
HashMap<EncodeHintType, Object> hints = new HashMap<>();
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
hints.put(EncodeHintType.MARGIN, 2);
try {
BitMatrix bitMatrix = new MultiFormatWriter().encode(url, BarcodeFormat.QR_CODE, 200, 200, hints);
MatrixToImageWriter.writeToStream(bitMatrix, "PNG", response.getOutputStream());
System.out.println("创建二维码完成");
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 将通知参数转化为字符串
*
* @param request
* @return
*/
public static String readData(HttpServletRequest request) {
BufferedReader br = null;
try {
StringBuilder result = new StringBuilder();
br = request.getReader();
for (String line; (line = br.readLine()) != null; ) {
if (result.length() > 0) {
result.append("\n");
}
result.append(line);
}
return result.toString();
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
2.微信支付配置类
@Configuration
@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;
/**
* 获取商户的私钥文件
*
* @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;
}
}
3.微信支付枚举类
@AllArgsConstructor
@Getter
public enum WxApiType {
/**
* Native下单
*/
NATIVE_PAY("/v3/pay/transactions/native"),
/**
* JSAPI下单
*/
JSAPI_PAY("/v3/pay/transactions/jsapi"),
/**
* 查询订单
*/
ORDER_QUERY_BY_NO("/v3/pay/transactions/out-trade-no/%s"),
/**
* 关闭订单
*/
CLOSE_ORDER_BY_NO("/v3/pay/transactions/out-trade-no/%s/close"),
/**
* 支付通知
*/
NATIVE_NOTIFY("/client/order/pay/notify");
/**
* 类型
*/
private final String type;
}
4.签名验证类
@Slf4j
public class WechatPay2ValidatorForRequest {
/**
* 应答超时时间,单位为分钟
*/
protected static final long RESPONSE_EXPIRED_MINUTES = 5;
protected final Verifier verifier;
protected final String requestId;
protected final String body;
public WechatPay2ValidatorForRequest(Verifier verifier, String requestId, String body) {
this.verifier = verifier;
this.requestId = requestId;
this.body = body;
}
protected static IllegalArgumentException parameterError(String message, Object... args) {
message = String.format(message, args);
return new IllegalArgumentException("parameter error: " + message);
}
protected static IllegalArgumentException verifyFail(String message, Object... args) {
message = String.format(message, args);
return new IllegalArgumentException("signature verify fail: " + message);
}
public final boolean validate(HttpServletRequest request) throws IOException {
try {
//处理请求参数
validateParameters(request);
//构造验签名串
String message = buildMessage(request);
String serial = request.getHeader(WECHAT_PAY_SERIAL);
String signature = request.getHeader(WECHAT_PAY_SIGNATURE);
//验签
if (!verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature)) {
throw verifyFail("serial=[%s] message=[%s] sign=[%s], request-id=[%s]",
serial, message, signature, requestId);
}
} catch (IllegalArgumentException e) {
log.error(e.getMessage());
return false;
}
return true;
}
protected final void validateParameters(HttpServletRequest request) {
// NOTE: ensure HEADER_WECHAT_PAY_TIMESTAMP at last
String[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP};
String header = null;
for (String headerName : headers) {
header = request.getHeader(headerName);
if (header == null) {
throw parameterError("empty [%s], request-id=[%s]", headerName, requestId);
}
}
//判断请求是否过期
String timestampStr = header;
try {
Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestampStr));
// 拒绝过期请求
if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= RESPONSE_EXPIRED_MINUTES) {
throw parameterError("timestamp=[%s] expires, request-id=[%s]", timestampStr, requestId);
}
} catch (DateTimeException | NumberFormatException e) {
throw parameterError("invalid timestamp=[%s], request-id=[%s]", timestampStr, requestId);
}
}
protected final String buildMessage(HttpServletRequest request) throws IOException {
String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);
String nonce = request.getHeader(WECHAT_PAY_NONCE);
return timestamp + "\n"
+ nonce + "\n"
+ body + "\n";
}
protected final String getResponseBody(CloseableHttpResponse response) throws IOException {
HttpEntity entity = response.getEntity();
return (entity != null && entity.isRepeatable()) ? EntityUtils.toString(entity) : "";
}
}
OK,齐活~