微信支付
小黄最在工作中对接需要对接微信支付,在此记录一下微信支付开发的相关流程,望能帮助到各位
前期准备
由于小黄是小程序端需要对接微信支付,需要注册小程序等,小黄会一一列举出来
小程序注册
所需文件
- 没有注册过微信公众平台、微信开放平台的邮箱
- 营业执照
- 对公账户信息(需要对公账户打款来校验)
- 纳税识别号
- 300大洋
注册
注册网址: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证书来解析你的api是否正确,所以这一步是必不可少的,另外推荐设置APIv3密钥,功能比v2版本多
绑定微信小程序
在产品中心里,可以绑定微信小程序
开发微信支付
微信支付有很多支付方式,具体可以参考产品文档,因为业务需求,小黄使用的是JSAPI支付方式,接下来就重点介绍一下JSAPI支付方式。
接入前准备
需要设置支付授权目录,这里小黄设置的就是公司测试域名
引入maven
<dependency><groupId>com.github.wechatpay-apiv3</groupId><artifactId>wechatpay-java</artifactId><version>0.2.11</version>
</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; //退款回调地址(目前用不到)@Beanpublic RSAAutoCertificateConfig rsaAutoCertificateConfig(){return new RSAAutoCertificateConfig.Builder().merchantId(mchId).merchantSerialNumber(merchantSerialNumber).apiV3Key(apiV3Key).privateKeyFromPath(filePath).build();}
}
通过api调用微信支付
之前小黄看到好多文档都是通过http请求来调用的,加了依赖后,相当于微信封装了一层,使用起来更方便了,只需要向PrepayRequest
中塞数据即可
这里有两个注意点:
- 金额的单位是分
- 必须要填异步回调地址(回调地址的作用是当用户支付后,会访问这个地址访问支付信息,该地址必须是https,测试的时候建议使用花生壳工具走内网穿透)
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()); //传入自己平台的订单idrequest.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");// 构造 RequestParamRequestParam requestParam = new RequestParam.Builder().serialNumber(serialNo).nonce(nonceStr).signature(signature).timestamp(timestamp).body(body).build();// 初始化 NotificationParserNotificationParser parser = new NotificationParser(config);Transaction transaction = null;try {// 验签、解密并转换成 Transactiontransaction = 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");// 构造 RequestParamRequestParam requestParam = new RequestParam.Builder().serialNumber(serialNo).nonce(nonceStr).signature(signature).timestamp(timestamp).body(body).build();// 初始化 NotificationParserNotificationParser parser = new NotificationParser(config);Transaction transaction = null;try {// 验签、解密并转换成 Transactiontransaction = 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来做,应该是轻而易举的