Java实现微信native支付
1、介绍
Native支付是指商户系统按微信支付协议生成支付二维码,用户再用微信“扫一扫”完成支付的模式。
详细介绍以及实现流程可参考官方文档:微信native支付官方指引文档,本文主要为编码者讲解具体实现细节。
2、业务流程
根据业务流程图可以得知,编码主要有两部分:
1. 调用统一下单API,将返回的预支付交易链接(code_url)生成二维码图片,供用户扫描;
2. 调用查询订单API,获取订单的支付状态;
3、实现逻辑
先说第一部分:调用统一下单API,将返回的预支付交易链接(code_url)生成二维码图片,供用户扫描
1、导入依赖
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-apache-httpclient</artifactId>
<version>0.4.9</version>
</dependency>
2、调用统一下单API
public class NativePayCreateOrder {
// 一些参数,公司提供的,通常定义在项目的配置文件中,以下皆是模拟数据
private appId = "wx8790234sau98232ehb2"; // 应用号
private String mchId = "1571814339"; // 商户号
private String privateKey = ""; // 私钥字符串,此处省略
private String mchSerialNo = "25FS78SGSGFGG7879SGS987GS675AT6"; // 商户证书序列号
private String apiV3Key = "CZBK12Y675AHIGA97987987957AD"; // V3密钥
// 定义httpClient对象
private CloseableHttpClient httpClient;
@Before
public void setup() throws IOException {
// 加载商户私钥(privateKey:私钥字符串)
PrivateKey merchantPrivateKey = PemUtil
.loadPrivateKey(new ByteArrayInputStream(privateKey.getBytes("utf-8")));
// 加载平台证书(mchId:商户号,mchSerialNo:商户证书序列号,apiV3Key:V3密钥)
AutoUpdateCertificatesVerifier verifier = new AutoUpdateCertificatesVerifier(
new WechatPay2Credentials(mchId, new PrivateKeySigner(mchSerialNo, merchantPrivateKey)),apiV3Key.getBytes("utf-8"));
// 初始化httpClient
httpClient = WechatPayHttpClientBuilder.create()
.withMerchant(mchId, mchSerialNo, merchantPrivateKey)
.withValidator(new WechatPay2Validator(verifier)).build();
}
@After
public void after() throws IOException {
httpClient.close();
}
上述代码是初始化httpClient对象(就是将商户ID、商户序列号、商户私钥、
平台证书设置到httpClient对象中),
httpClient对象是用来发送请求的,向微信支付系统发送下单请求。
public void CreateOrder() throws Exception{
// 定义Post类型的http请求(参数是请求的地址,固定不变的,因为请求的是微信支付系统的地址)
HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/native");
// 定义请求body参数(后面要修改,其包含应用ID、商户号、订单金额等信息)
String reqdata = "{"
+ "\"time_expire\":\"2018-06-08T10:34:56+08:00\","
+ "\"amount\": {"
+ "\"total\":100,"
+ "\"currency\":\"CNY\""
+ "},"
+ "\"mchid\":\"1230000109\","
+ "\"description\":\"Image形象店-深圳腾大-QQ公仔\","
+ "\"notify_url\":\"https://www.weixin.qq.com/wxpay/pay.php\","
+ "\"out_trade_no\":\"1217752501201407033233368018\","
+ "\"goods_tag\":\"WXG\","
+ "\"appid\":\"wxd678efh567hg6787\","
+ "\"attach\":\"自定义数据说明\","
+ "\"detail\": {"
+ "\"invoice_id\":\"wx123\","
+ "\"goods_detail\": ["
+ "{"
+ "\"goods_name\":\"iPhoneX 256G\","
+ "\"wechatpay_goods_id\":\"1001\","
+ "\"quantity\":1,"
+ "\"merchant_goods_id\":\"商品编码\","
+ "\"unit_price\":828800"
+ "},"
+ "{"
+ "\"goods_name\":\"iPhoneX 256G\","
+ "\"wechatpay_goods_id\":\"1001\","
+ "\"quantity\":1,"
+ "\"merchant_goods_id\":\"商品编码\","
+ "\"unit_price\":828800"
+ "}"
+ "],"
+ "\"cost_price\":608800"
+ "},"
+ "\"scene_info\": {"
+ "\"store_info\": {"
+ "\"address\":\"广东省深圳市南山区科技中一道10000号\","
+ "\"area_code\":\"440305\","
+ "\"name\":\"腾讯大厦分店\","
+ "\"id\":\"0001\""
+ "},"
+ "\"device_id\":\"013467007045764\","
+ "\"payer_client_ip\":\"14.23.150.211\""
+ "}"
+ "}";
StringEntity entity = new StringEntity(reqdata,"utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
httpPost.setHeader("Accept", "application/json");
//完成签名并执行请求(调用下单API)
CloseableHttpResponse response = httpClient.execute(httpPost);
try {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200) { //处理成功
System.out.println("success,return body = " + EntityUtils.toString(response.getEntity()));
// 将上面的返回结果code_url返回(return)给前端,前端根据这个code_url生成二维码图片
} else if (statusCode == 204) { //处理成功,无返回Body
System.out.println("success");
} else {
System.out.println("failed,resp code = " + statusCode+ ",return body = " + EntityUtils.toString(response.getEntity()));
throw new IOException("request failed");
}
} finally {
response.close();
}
}
}
3.3、定义订单参数类
import lombok.Builder;
import lombok.Data;
@Builder // 此注解的作用是可以通过builder()方法链式创建对象
@Data
public class NativePayParams {
private String appid; // 应用id
private String mchid; // 商户id
private String description; // 商品描述
private String out_trade_no; // 订单号
private String notify_url; // 支付成功回调通知地址
private Amount amount; // 订单金额信息
}
定义订单金额类
import lombok.Builder;
import lombok.Data;
@Builder
@Data
public class Amount {
private Integer total; // 金额,单位为分
private String currency; // 货币类型,目前仅支持人民币
}
3.4、修改第二步中httpPost请求的body参数
// 定义请求body参数
Amount amount = Amount.builder()
.currency("CNY")
.total(1)
.build();
NativePayParams payParams = NativePayParams.builder()
.appid(appId)
.mchid(mchId)
.description("商品描述,如VIP特权")
.out_trade_no("ALJFKL7987987U98KJK")
.notify_url("https://项目的域名/native/notify") // 支付成功后通知的回调地址
.amount(amount)
.build();
String reqdata = JSON.toJSONString(payParams);
接下来,就是获取订单的支付状态,官网给了两种方案:一种是用户支付成功后,微信支付系统会将支付成功的结果以回调通知的形式同步给商户,商户的回调地址需要在调用Native下单API时传入notify_url参数;另一种获取订单支付状态的方式是当因网络抖动或本身notify_url存在问题等原因,导致无法接收到回调通知时,商户也可主动调用微信支付系统的查询订单API来获取订单状态。
先说第一种方式的实现逻辑:用户支付成功后,微信支付系统会将支付成功的结果以回调通知的形式同步给商户,商户的回调地址需要在调用Native下单API时传入notify_url参数
1、编写接口,接收微信的支付成功通知
编写通知数据实体类
@Data
public class ResourceDto {
private String algorithm;
private String ciphertext;
private String associated_data;
private String original_type;
private String nonce;
}
编写支付通知参数实体类
@Data
public class NotifyDto {
private String id;
private String create_time;
private String event_type;
private String resource_type;
private ResourceDto resource;
private String summary;
}
编写controller层接口
@RestController
@RequestMapping("/native")
public class NativePayController {
@Autowired
private NativePayService nativePayService;
@PostMapping("/notify")
public Map<String, String> payNotify(@RequestBody NotifyDto dto){
// 解密支付通知数据(调用业务逻辑层)
return nativePayService.payNotify(dto);
}
}
2、解密支付通知的内容
业务层接口
public interface NativePayService {
Map<String, String> payNotify(NotifyDto dto);
}
业务层实现类
@Service
public class NativePayServiceImpl implements NativePayService {
private String apiV3Key = "CZBK12Y675AHIGA97987987957AD"; // V3密钥
@Override
public Map<String, String> payNotify(NotifyDto dto) {
Map<String,String> res = null;
try {
// 解密微信支付系统传过来的参数
String json = new AesUtil(apiV3Key.getBytes()).decryptToString(dto.getResource().getAssociated_data().getBytes(),
dto.getResource().getNonce().getBytes(),
dto.getResource().getCiphertext());
String outTradeNo = JSON.parseObject(json, Map.class).get("out_trade_no").toString();
System.out.println("支付成功的订单号:" + outTradeNo);
} catch (GeneralSecurityException e) {
// 支付不成功
e.printStackTrace();
res.put("code","FAIL");
res.put("message","失败");
}
return res; // 返回正确的信息微信支付系统才不会继续发送通知
}
}
3、内网穿透(本地测试的IP不是公网IP,外部无法访问,需要域名穿透,才能让微信支付系统回调到本地项目的接口)
可以选择用花生壳这个软件工具来做内容穿透,其实就是一个域名映射,映射到本地IP与项目端口;在实际开发中不需要做内网穿透,因为公司有服务器域名。
4、微信主动通知的地址是通过下单接口中的请求参数notify_url来设置的,要求必须是https地址
接下来说主动查询订单支付状态这种方式,因为微信支付系统不能保证通知成功,所以在企业开发中,都还会用这种方式来获取订单支付状态。本质上就是写接口向微信支付系统发请求。
以查询商户订单号为例:(还可以查询支付订单号,实现逻辑同理,可模仿调用下单API的接口编写)
public void queryOrder() throws Exception{
// 定义Get类型的http请求
HttpGet httpGet = new HttpGet("https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/ALJFKL7987987U98KJK?mchid=1571814339");
httpGet.setHeader("Accept", "application/json");
//完成签名并执行请求(调用查询订单API)
CloseableHttpResponse response = httpClient.execute(httpGet);
try {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200) { //处理成功
System.out.println("success,return body = " + EntityUtils.toString(response.getEntity()));
// 上面的返回结果就是json格式的订单信息,包括支付状态
} else if (statusCode == 204) { //处理成功,无返回Body
System.out.println("success");
} else {
System.out.println("failed,resp code = " + statusCode+ ",return body = " + EntityUtils.toString(response.getEntity()));
throw new IOException("request failed");
}
} finally {
response.close();
}
}
最后还有个问题,就是什么时候去调用刚编写的查询订单状态的这个接口;创建订单1分钟之后调用,用户可能还没支付,创建订单10分钟后调用,用户可能早就支付了,所以要用轮询的方式来调用查询订单状态这个接口,具体实现就是用定时任务,不会用定时任务的可以去看我之前的文章。
到这里其实就已经说完了这个微信支付的实现。下面就是扩展知识了,说一下SpringBoot的starter封装。比如说我们这里做好了微信扫码支付的功能,其他项目也要用这个功能,就不需要再实现这个功能了,直接在项目里引入这个starter就行了。这个SpringBoot的starter封装的原理以及具体使用方式留在下篇文章讲解。