1.接口介绍
1.JSAPI下单
JSAPI下单 - JSAPI支付 | 微信支付商户文档中心 (qq.com)
商户系统先调用该接口在微信支付服务后台生成预支付交易单,返回正确的预支付交易会话标识后生成交易串调起支付。
支持商户:普通商户
请求URL:https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi
请求方式:POST
请求参数:
{
"appid" : "wxd678efh567hg6787", // 小程序id
"mchid" : "1230000109", // 商户号
"description" : "Image形象店-深圳腾大-QQ公仔", // 商品描述
"out_trade_no" : "1217752501201407033233368018", // 商户系统内容订单号,唯一
"time_expire" : "2018-06-08T10:34:56+08:00",
"attach" : "自定义数据说明",
"notify_url" : " https://www.weixin.qq.com/wxpay/pay.php", // 接收微信支付结果通知的回调地址
"goods_tag" : "WXG",
"support_fapiao" : true,
"amount" : { // 订单金额信息
"total" : 100,
"currency" : "CNY"
},
"payer" : { // 支付者信息
"openid" : "oUpF8uMuAJO_M2pxb1Q9zNjWeS6o\t"
}
}
返回参数:
{
// 预支付交易会话标识。用于后续接口调用中使用,该值有效期为2小时
"prepay_id" : "wx201410272009395522657a690389285100"
}
2.小程序调起支付
小程序调起支付 - 小程序支付 | 微信支付商户文档中心 (qq.com)
通过JSAPI下单接口获取到发起支付的必要参数 prepay_id,然后使用微信支付提供的小程序方法调起小程序支付。
支付商户:普通商户
接口名称:wx.requestPayment,详细说明支付 / wx.requestPayment (qq.com)
请求参数:
{
// 时间戳
"timeStamp": "1414561699",
// 随机字符串
"nonceStr": "5K8264ILTKCH16CQ2502SI8ZNMTM67VS",
// 下单接口返回的prepay_id参数值,格式为 prepay_id=***
"package": "prepay_id=wx201410272009395522657a690389285100",
// 签名类型,仅支持 RSA
"signType": "RSA",
// 签名,使用字段appid、timeStamp、nonceStr、package 计算得出的签名值
"paySign": "oR9d8PuhnIc+YZ8cBHFCwfgpaK9gd7vaRvkYD7rthRAZ\/X+QBhcCYL21N7cHCTUxbQ+EAt6Uy+lwSN22f5YZvI45MLko8Pfso0jm46v5hqcVwrk6uddkGuT+Cdvu4WBqDzaDjnNa5UK3GfE1Wfl2gHxIIY5lLdUgWFts17D4WuolLLkiFZV+JSHMvH7eaLdT9N5GBovBwu5yYKUR7skR8Fu+LozcSqQixnlEZUfyE55feLOQTUYzLmR9pNtPbPsu6WVhbNHMS3Ss2+AehHvz+n64GDmXxbX++IOBvm2olHu3PsOUGRwhudhVf7UcGcunXt8cqNjKNqZLhLw4jq\/xDg==",
"success":function(res){},
"fail":function(res){},
"complete":function(res){}
}
回调结果:
回调类型 | errMsg | 说明 |
---|---|---|
success | requestPayment:ok | 调用支付成功 |
fail | requestPayment:fail cancel | 用户取消支付 |
fail | requestPayment:fail (detail message) | 调用支付失败,其中 detail message 为后台返回的详细失败原因 |
2.准备工作
- 接入微信支付
- 提交资料:在线提交营业执照、身份证、银行账户等基本信息,并按指引完成账户验证
- 签署协议:微信支付团队会在1-2个工作日内完成审核,审核通过后请在线签约,即可体验各项产品能力
- 绑定场景:如需自行开发完成收款,需将商户号与APPID进行绑定,或开通微信收款商业版(免开发)完成收款
- 获取微信平台支付证书、商户私钥文件
- 获取临时域名:支付成功后微信服务通过该域名回调我们的程序
3.代码配置
-
在 application.yml 文件中进行微信支付相关配置:
sky: wechat: appid: 微信小程序的 appid secret: 微信小程序的密钥 mchid: 商户号 mchSerialNo: 商户API证书的证书序列号 privateKeyFilePath: 商户私钥文件 apiV3Key: 证书解密的密钥,在商户平台配置 weChatPayCertFilePath: 平台证书 notifyUrl: 支付成功的回调地址 refundNotifyUrl: 退款成功的回调地址
-
引入微信支付依赖
<dependency> <groupId>com.github.wechatpay-apiv3</groupId> <artifactId>wechatpay-apache-httpclient</artifactId> <version>0.4.8</version> </dependency>
-
创建微信支付配置属性类
@Component @ConfigurationProperties(prefix = "sky.wechat") @Data public class WeChatProperties { private String appid; //小程序的appid private String secret; //小程序的秘钥 private String mchid; //商户号 private String mchSerialNo; //商户API证书的证书序列号 private String privateKeyFilePath; //商户私钥文件 private String apiV3Key; //证书解密的密钥 private String weChatPayCertFilePath; //平台证书 private String notifyUrl; //支付成功的回调地址 private String refundNotifyUrl; //退款成功的回调地址 }
-
创建微信支付工具类实现微信支付的相关功能
@Component public class WeChatPayUtil { //微信支付下单接口地址 public static final String JSAPI = "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi"; //申请退款接口地址 public static final String REFUNDS = "https://api.mch.weixin.qq.com/v3/refund/domestic/refunds"; @Autowired private WeChatProperties weChatProperties; /** * 获取调用微信接口的客户端工具对象 * * @return */ private CloseableHttpClient getClient() { PrivateKey merchantPrivateKey = null; try { //merchantPrivateKey商户API私钥,如何加载商户API私钥请看常见问题 merchantPrivateKey = PemUtil.loadPrivateKey(new FileInputStream(new File(weChatProperties.getPrivateKeyFilePath()))); //加载平台证书文件 X509Certificate x509Certificate = PemUtil.loadCertificate(new FileInputStream(new File(weChatProperties.getWeChatPayCertFilePath()))); //wechatPayCertificates微信支付平台证书列表。你也可以使用后面章节提到的“定时更新平台证书功能”,而不需要关心平台证书的来龙去脉 List<X509Certificate> wechatPayCertificates = Arrays.asList(x509Certificate); WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create() .withMerchant(weChatProperties.getMchid(), weChatProperties.getMchSerialNo(), merchantPrivateKey) .withWechatPay(wechatPayCertificates); // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签 CloseableHttpClient httpClient = builder.build(); return httpClient; } catch (FileNotFoundException e) { e.printStackTrace(); return null; } } /** * 发送post方式请求 * * @param url * @param body * @return */ private String post(String url, String body) throws Exception { CloseableHttpClient httpClient = getClient(); HttpPost httpPost = new HttpPost(url); httpPost.addHeader(HttpHeaders.ACCEPT, ContentType.APPLICATION_JSON.toString()); httpPost.addHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.toString()); httpPost.addHeader("Wechatpay-Serial", weChatProperties.getMchSerialNo()); httpPost.setEntity(new StringEntity(body, "UTF-8")); CloseableHttpResponse response = httpClient.execute(httpPost); try { String bodyAsString = EntityUtils.toString(response.getEntity()); return bodyAsString; } finally { httpClient.close(); response.close(); } } /** * 发送get方式请求 * * @param url * @return */ private String get(String url) throws Exception { CloseableHttpClient httpClient = getClient(); HttpGet httpGet = new HttpGet(url); httpGet.addHeader(HttpHeaders.ACCEPT, ContentType.APPLICATION_JSON.toString()); httpGet.addHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.toString()); httpGet.addHeader("Wechatpay-Serial", weChatProperties.getMchSerialNo()); CloseableHttpResponse response = httpClient.execute(httpGet); try { String bodyAsString = EntityUtils.toString(response.getEntity()); return bodyAsString; } finally { httpClient.close(); response.close(); } } /** * jsapi下单 * * @param orderNum 商户订单号 * @param total 总金额 * @param description 商品描述 * @param openid 微信用户的openid * @return */ private String jsapi(String orderNum, BigDecimal total, String description, String openid) throws Exception { JSONObject jsonObject = new JSONObject(); jsonObject.put("appid", weChatProperties.getAppid()); jsonObject.put("mchid", weChatProperties.getMchid()); jsonObject.put("description", description); jsonObject.put("out_trade_no", orderNum); jsonObject.put("notify_url", weChatProperties.getNotifyUrl()); JSONObject amount = new JSONObject(); amount.put("total", total.multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP).intValue()); amount.put("currency", "CNY"); jsonObject.put("amount", amount); JSONObject payer = new JSONObject(); payer.put("openid", openid); jsonObject.put("payer", payer); String body = jsonObject.toJSONString(); return post(JSAPI, body); } /** * 小程序支付 * * @param orderNum 商户订单号 * @param total 金额,单位 元 * @param description 商品描述 * @param openid 微信用户的openid * @return */ public JSONObject pay(String orderNum, BigDecimal total, String description, String openid) throws Exception { //统一下单,生成预支付交易单 String bodyAsString = jsapi(orderNum, total, description, openid); //解析返回结果 JSONObject jsonObject = JSON.parseObject(bodyAsString); System.out.println(jsonObject); String prepayId = jsonObject.getString("prepay_id"); if (prepayId != null) { String timeStamp = String.valueOf(System.currentTimeMillis() / 1000); String nonceStr = RandomStringUtils.randomNumeric(32); ArrayList<Object> list = new ArrayList<>(); list.add(weChatProperties.getAppid()); list.add(timeStamp); list.add(nonceStr); list.add("prepay_id=" + prepayId); //二次签名,调起支付需要重新签名 StringBuilder stringBuilder = new StringBuilder(); for (Object o : list) { stringBuilder.append(o).append("\n"); } String signMessage = stringBuilder.toString(); byte[] message = signMessage.getBytes(); Signature signature = Signature.getInstance("SHA256withRSA"); signature.initSign(PemUtil.loadPrivateKey(new FileInputStream(new File(weChatProperties.getPrivateKeyFilePath())))); signature.update(message); String packageSign = Base64.getEncoder().encodeToString(signature.sign()); //构造数据给微信小程序,用于调起微信支付 JSONObject jo = new JSONObject(); jo.put("timeStamp", timeStamp); jo.put("nonceStr", nonceStr); jo.put("package", "prepay_id=" + prepayId); jo.put("signType", "RSA"); jo.put("paySign", packageSign); return jo; } return jsonObject; } /** * 申请退款 * * @param outTradeNo 商户订单号 * @param outRefundNo 商户退款单号 * @param refund 退款金额 * @param total 原订单金额 * @return */ public String refund(String outTradeNo, String outRefundNo, BigDecimal refund, BigDecimal total) throws Exception { JSONObject jsonObject = new JSONObject(); jsonObject.put("out_trade_no", outTradeNo); jsonObject.put("out_refund_no", outRefundNo); JSONObject amount = new JSONObject(); amount.put("refund", refund.multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP).intValue()); amount.put("total", total.multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP).intValue()); amount.put("currency", "CNY"); jsonObject.put("amount", amount); jsonObject.put("notify_url", weChatProperties.getRefundNotifyUrl()); String body = jsonObject.toJSONString(); //调用申请退款接口 return post(REFUNDS, body); } }
-
创建一个 PayNotifyController 类来实现支付回调相关接口
@RestController @RequestMapping("/notify") @Slf4j public class PayNotifyController { @Autowired private OrderService orderService; @Autowired private WeChatProperties weChatProperties; /** * 支付成功回调 * * @param request */ @RequestMapping("/paySuccess") public void paySuccessNotify(HttpServletRequest request, HttpServletResponse response) throws Exception { //读取数据 String body = readData(request); log.info("支付成功回调:{}", body); //数据解密 String plainText = decryptData(body); log.info("解密后的文本:{}", plainText); JSONObject jsonObject = JSON.parseObject(plainText); String outTradeNo = jsonObject.getString("out_trade_no");//商户平台订单号 String transactionId = jsonObject.getString("transaction_id");//微信支付交易号 log.info("商户平台订单号:{}", outTradeNo); log.info("微信支付交易号:{}", transactionId); //业务处理,修改订单状态、来单提醒 orderService.paySuccess(outTradeNo); //给微信响应 responseToWeixin(response); } /** * 读取数据 * * @param request * @return * @throws Exception */ private String readData(HttpServletRequest request) throws Exception { BufferedReader reader = request.getReader(); StringBuilder result = new StringBuilder(); String line = null; while ((line = reader.readLine()) != null) { if (result.length() > 0) { result.append("\n"); } result.append(line); } return result.toString(); } /** * 数据解密 * * @param body * @return * @throws Exception */ private String decryptData(String body) throws Exception { JSONObject resultObject = JSON.parseObject(body); JSONObject resource = resultObject.getJSONObject("resource"); String ciphertext = resource.getString("ciphertext"); String nonce = resource.getString("nonce"); String associatedData = resource.getString("associated_data"); AesUtil aesUtil = new AesUtil(weChatProperties.getApiV3Key().getBytes(StandardCharsets.UTF_8)); //密文解密 String plainText = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8), nonce.getBytes(StandardCharsets.UTF_8), ciphertext); return plainText; } /** * 给微信响应 * @param response */ private void responseToWeixin(HttpServletResponse response) throws Exception{ response.setStatus(200); HashMap<Object, Object> map = new HashMap<>(); map.put("code", "SUCCESS"); map.put("message", "SUCCESS"); response.setHeader("Content-type", ContentType.APPLICATION_JSON.toString()); response.getOutputStream().write(JSONUtils.toJSONString(map).getBytes(StandardCharsets.UTF_8)); response.flushBuffer(); } }
-
创建一个订单支付VO类,来保存预支付交易单
@Data @Builder @NoArgsConstructor @AllArgsConstructor public class OrderPaymentVO implements Serializable { private String nonceStr; //随机字符串 private String paySign; //签名 private String timeStamp; //时间戳 private String signType; //签名算法 private String packageStr; //统一下单接口返回的 prepay_id 参数值 }
-
创建 OrdersPaymentDTO 类来传递数据
@Data public class OrdersPaymentDTO implements Serializable { //订单号 private String orderNumber; //付款方式 private Integer payMethod; }
-
在 OrderController 中添加订单支付方法,此方法会返回预支付交易单
@PutMapping("/payment") @ApiOperation("订单支付") public Result<OrderPaymentVO> payment(@RequestBody OrdersPaymentDTO ordersPaymentDTO) throws Exception { log.info("订单支付:{}", ordersPaymentDTO); OrderPaymentVO orderPaymentVO = orderService.payment(ordersPaymentDTO); log.info("生成预支付交易单:{}", orderPaymentVO); return Result.success(orderPaymentVO); }
-
在 OrderServiceImpl 中实现订单支付方法,支付成功后订单状态修改方法
public OrderPaymentVO payment(OrdersPaymentDTO ordersPaymentDTO) throws Exception { // 当前登录用户id Long userId = BaseContext.getCurrentId(); User user = userMapper.getById(userId); //调用微信支付接口,生成预支付交易单 JSONObject jsonObject = weChatPayUtil.pay( ordersPaymentDTO.getOrderNumber(), //商户订单号,用户下单时生成 new BigDecimal(0.01), //支付金额,单位 元 "苍穹外卖订单", //商品描述 user.getOpenid() //微信用户的openid ); if (jsonObject.getString("code") != null && jsonObject.getString("code").equals("ORDERPAID")) { throw new OrderBusinessException("该订单已支付"); } OrderPaymentVO vo = jsonObject.toJavaObject(OrderPaymentVO.class); vo.setPackageStr(jsonObject.getString("package")); return vo; } public void paySuccess(String outTradeNo) { // 根据订单号查询订单 Orders ordersDB = orderMapper.getByNumber(outTradeNo); // 根据订单id更新订单的状态、支付方式、支付状态、结账时间 Orders orders = Orders.builder() .id(ordersDB.getId()) .status(Orders.TO_BE_CONFIRMED) .payStatus(Orders.PAID) .checkoutTime(LocalDateTime.now()) .build(); orderMapper.update(orders); }
4.参考
内容来自黑马程序员的苍穹外卖项目中的微信支付部分。