小黄最在工作中对接需要对接微信支付,在此记录一下微信支付开发的相关流程,望能帮助到各位
由于小黄是小程序端需要对接微信支付,需要注册小程序等,小黄会一一列举出来
注册网址:https://mp.weixin.qq.com/cgi-bin/wx
按要求填写所需文件信息即可,由于是企业注册的小程序,需要对公账户汇款,只需要几毛钱并且这个钱会退回来的
注册完成后,开始填写小程序信息,必须填写小程序信息,才能获得appid,也就是小程序id,开发中会用到
这里需要支付300元的费用
目前只支持企业注册微信支付
注册网址:https://pay.weixin.qq.com/index.php/apply/applyment_home/guide_normal
同样也是按要求填写信息即可,也是需要对公账户汇款
在开发中需要读取到api证书来解析你的api是否正确,所以这一步是必不可少的,另外推荐设置APIv3密钥,功能比v2版本多
在产品中心里,可以绑定微信小程序
微信支付有很多支付方式,具体可以参考产品文档,因为业务需求,小黄使用的是JSAPI支付方式,接下来就重点介绍一下JSAPI支付方式。
需要设置支付授权目录,这里小黄设置的就是公司测试域名
<dependency>
<groupId>com.github.wechatpay-apiv3groupId>
<artifactId>wechatpay-javaartifactId>
<version>0.2.11version>
dependency>
刚开始对接微信支付,咱们肯定先对接支付,看一下JSAPI支付的时序图,其实需要Java后端做的就只有2-6,15-21
参考文档
在创建订单之前的业务处理,小黄就不贴出来了,每个开发的业务都不一样,主要是JSAPI下单给微信接受他的返回值,也就是时序图中的2-6步骤
创建配置类
商户证书序列号,可以通过执行openssl x509 -noout -serial -in apiclient_cert.pem
指令解析出来,cmd到证书所在位置
@Configuration
@Data
@RefreshScope
public class WechatConfig {
@Value("${wechat.mchId}")
private String mchId; //微信支付id
@Value("${wechat.filePath}")
private String filePath; //微信支付证书所存放的位置
@Value("${wechat.merchantSerialNumber}")
private String merchantSerialNumber; //商户证书序列号
@Value("${wechat.apiV3Key}")
private String apiV3Key; //apiv3密钥
@Value("${wechat.appId}")
private String appId; //绑定的小程序id
@Value("${wechat.notify-url}")
private String notifyUrl; //支付回调地址(目前用不到)
@Value("${wechat.refund-notify-url}")
private String refundNotifyUrl; //退款回调地址(目前用不到)
@Bean
public RSAAutoCertificateConfig rsaAutoCertificateConfig(){
return new RSAAutoCertificateConfig.Builder()
.merchantId(mchId)
.merchantSerialNumber(merchantSerialNumber)
.apiV3Key(apiV3Key)
.privateKeyFromPath(filePath)
.build();
}
}
通过api调用微信支付
之前小黄看到好多文档都是通过http请求来调用的,加了依赖后,相当于微信封装了一层,使用起来更方便了,只需要向PrepayRequest
中塞数据即可
这里有两个注意点:
public PrepayWithRequestPaymentResponse prepay(JsPrepayDto jsPrepayDto, MemberUserEntity memberUser) {
//下单并生成调起支付的参数
JsapiServiceExtension service = new JsapiServiceExtension.Builder().config(config).build();
PrepayRequest request = new PrepayRequest();
request.setAppid(wechatConfig.getAppId());
request.setMchid(wechatConfig.getMchId());
request.setOutTradeNo(jsPrepayDto.getTradeNo()); //传入自己平台的订单id
request.setDescription(jsPrepayDto.getDescription());
request.setNotifyUrl(wechatConfig.getNotifyUrl());
Amount amount = new Amount();
//todo:测试只支付一分
amount.setTotal(1);
request.setAmount(amount);
Payer payer = new Payer();
payer.setOpenid(memberUser.getWxOpenId());
request.setPayer(payer);
PrepayWithRequestPaymentResponse response = service.prepayWithRequestPayment(request);
return response;
}
参考文档
创建订单完成后,微信会主动调用设置的回调地址,将加密的数据返回给我们
注意
对后台通知交互时,如果微信收到应答不是成功或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功
- 同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。 推荐的做法是,当商户系统收到通知进行处理时,先检查对应业务数据的状态,并判断该通知是否已经处理。如果未处理,则再进行处理;如果已处理,则直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。
- 如果在所有通知频率后没有收到微信侧回调。商户应调用查询订单接口确认订单状态。
特别提醒: 商户系统对于开启结果通知的内容一定要做签名验证,并校验通知的信息是否与商户侧的信息一致,防止数据泄露导致出现“假通知”,造成资金损失。
解析请求
private String getRequestBody(HttpServletRequest request) {
StringBuffer sb = new StringBuffer();
try (ServletInputStream inputStream = request.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
) {
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
} catch (IOException e) {
log.error("读取数据流异常:{}", e);
}
return sb.toString();
}
解密、处理业务
Transaction
类有很多,注意是com.wechat.pay.java.service.payments.model
包下的
@Transactional(rollbackFor = Exception.class)
public Map wechatNotify(HttpServletRequest request, HttpServletResponse response) {
HashMap<String, String> map = new HashMap<>();
//获取报文
String body = getRequestBody(request);
//随机串
String nonceStr = request.getHeader("Wechatpay-Nonce");
//微信传递过来的签名
String signature = request.getHeader("Wechatpay-Signature");
//证书序列号(微信平台)
String serialNo = request.getHeader("Wechatpay-Serial");
//时间戳
String timestamp = request.getHeader("Wechatpay-Timestamp");
// 构造 RequestParam
RequestParam requestParam = new RequestParam.Builder()
.serialNumber(serialNo)
.nonce(nonceStr)
.signature(signature)
.timestamp(timestamp)
.body(body)
.build();
// 初始化 NotificationParser
NotificationParser parser = new NotificationParser(config);
Transaction transaction = null;
try {
// 验签、解密并转换成 Transaction
transaction = parser.parse(requestParam, Transaction.class);
log.info("=============解密:Transaction ============= {}", transaction);
} catch (Exception e) {
log.error("签名验证失败");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
map.put("code", "FAIL");
map.put("message", "失败");
return map;
}
String lockName = "wechat_notice_" + transaction.getOutTradeNo();
RLock lock = redissonClient.getLock(lockName);
try {
//微信可能会重复发送数据,如果已经处理过,应该直接返回成功
//加锁,避免并发重复添加
lock.lock(5,TimeUnit.SECONDS);
log.info("=============接收到微信支付回调通知=============");
//-----处理业务逻辑-----
//通知微信回调成功
map.put("code", "SUCCESS");
return map;
} catch (Exception e) {
log.error("业务处理失败");
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
map.put("code", "FAIL");
map.put("message", "失败");
return map;
} finally {
lock.unlock();
}
}
参考文档
这个请求可以查询订单的支付状态
@Transactional(rollbackFor = Exception.class)
public void findPaymentOrder(String payOrderId) {
JsapiServiceExtension service = new JsapiServiceExtension.Builder().config(config).build();
QueryOrderByOutTradeNoRequest request = new QueryOrderByOutTradeNoRequest();
request.setMchid(wechatConfig.getMchId());
request.setOutTradeNo(payOrderId);
Transaction transaction = service.queryOrderByOutTradeNo(request);
log.info("============= Transaction ============= {}", transaction);
String lockName = "wechat_notice_" + transaction.getOutTradeNo();
RLock lock = redissonClient.getLock(lockName);
try {
//加锁,避免并发重复添加
lock.lock(5,TimeUnit.SECONDS);
//处理业务逻辑
} finally {
lock.unlock();
}
}
支付功能完成后,咱们开始着手做退款功能,其实跟支付差不多,先是去创建退款订单,再来接受微信发起的退款回调
参考文档
这里需要注意的是,创建退款订单会有一个返回对象,里面包含着请求信息,但是微信退款是有延迟的,测试的时候到账时间在1-10秒内,如果要在到账后在做操作,建议通过回调来实现
public Boolean refundOrder(WechatRefundReq data) {
//查询支付订单信息
PayOrderDetailsEntity order = payOrderDetailsDao.selectOne(new LambdaQueryWrapper<PayOrderDetailsEntity>().eq(PayOrderDetailsEntity::getPayOrderId, data.getPayOrderId()));
if (data.getRefundAmount().compareTo(order.getPayAmount()) > 0) {
log.error("退款金额超出订单金额:data : {} , order : {}", data, order);
return false;
}
//退款接口
RefundService service = new RefundService.Builder().config(config).build();
CreateRequest request = new CreateRequest();
request.setTransactionId(order.getTradeNo());
request.setOutRefundNo(data.getRefundOrderId());
//用户退款原因,会在用户端微信提示
// request.setReason(data.getReason());
request.setNotifyUrl(wechatConfig.getRefundNotifyUrl());
AmountReq amount = new AmountReq();
amount.setRefund(data.getRefundAmount().multiply(BigDecimal.valueOf(100)).longValue());
amount.setTotal(order.getPayAmount().multiply(BigDecimal.valueOf(100)).longValue());
amount.setCurrency("CNY");
request.setAmount(amount);
log.info("======开始处理微信退款======");
Refund refund = service.create(request);
log.info("refund : {}" ,refund);
return true;
}
参考文档
注意
对后台通知交互时,如果微信收到应答不是成功或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功
- 同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。 推荐的做法是,当商户系统收到通知进行处理时,先检查对应业务数据的状态,并判断该通知是否已经处理。如果未处理,则再进行处理;如果已处理,则直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。
- 如果在所有通知频率后没有收到微信侧回调。商户应调用查询订单接口确认订单状态。
特别提醒: 商户系统对于开启结果通知的内容一定要做签名验证,并校验通知的信息是否与商户侧的信息一致,防止数据泄露导致出现“假通知”,造成资金损失。
@Transactional(rollbackFor = Exception.class)
public Map wechatNotify(HttpServletRequest request, HttpServletResponse response) {
HashMap<String, String> map = new HashMap<>();
//获取报文
String body = getRequestBody(request);
//随机串
String nonceStr = request.getHeader("Wechatpay-Nonce");
//微信传递过来的签名
String signature = request.getHeader("Wechatpay-Signature");
//证书序列号(微信平台)
String serialNo = request.getHeader("Wechatpay-Serial");
//时间戳
String timestamp = request.getHeader("Wechatpay-Timestamp");
// 构造 RequestParam
RequestParam requestParam = new RequestParam.Builder()
.serialNumber(serialNo)
.nonce(nonceStr)
.signature(signature)
.timestamp(timestamp)
.body(body)
.build();
// 初始化 NotificationParser
NotificationParser parser = new NotificationParser(config);
Transaction transaction = null;
try {
// 验签、解密并转换成 Transaction
transaction = parser.parse(requestParam, Transaction.class);
log.info("=============解密:Transaction ============= {}", transaction);
} catch (Exception e) {
log.error("签名验证失败");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
map.put("code", "FAIL");
map.put("message", "失败");
return map;
}
String lockName = "wechat_notice_" + transaction.getOutTradeNo();
RLock lock = redissonClient.getLock(lockName);
try {
//微信可能会重复发送数据,如果已经处理过,应该直接返回成功
//加锁,避免并发重复添加
lock.lock(5,TimeUnit.SECONDS);
log.info("=============接收到微信支付回调通知=============");
//-----处理业务逻辑-----
//通知微信回调成功
map.put("code", "SUCCESS");
return map;
} catch (Exception e) {
log.error("业务处理失败");
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
map.put("code", "FAIL");
map.put("message", "失败");
return map;
} finally {
lock.unlock();
}
}
至此,微信支付的主要流程就已经走完了,其他的api可以参考官方文档,对照着SDK来做,应该是轻而易举的