七、小程序下单接口
商户系统先调用该接口在微信支付服务后台生成预支付交易单,返回正确的预支付交易会话标识后再按Native、JSAPI、APP等不同场景生成交易串调起支付。
接口说明
请求方式:
【POST】/v3/pay/transactions/jsapi
⚠️注意: 参数除了要传递 openId 之外其他的都和PC端的一模一样哇不相信可以去对比
返回的预交易ID用于小程序拉起支付窗口的时候使用
编写小程序下单接口
编写下单请求地址
在 enums 文件夹下面创建 weChatPayJSAPI 文件夹在创建 WxJSApiType
package com.yby6.enums.weChatPayJSAPI; import lombok.AllArgsConstructor; import lombok.Getter; /** * JSAPI 接口枚举 * * @author Yang Shuai * Create By 2023/09/10 */ @AllArgsConstructor @Getter public enum WxJSApiType { /** * jsapi 下单 * POST */ JSAPI_PAY("/v3/pay/transactions/jsapi"); /** * 类型 */ private final String type; }
在编写 支付回调地址 创建 WxJSNotifyType
package com.yby6.enums.weChatPayJSAPI; import lombok.Getter; /** * JS回调枚举 * 商户服务接收的回调 API 接口 */ @Getter public enum WxJSNotifyType { /** * 支付通知 v3 * /v1/play/callback * /api/wx-pay/native/notify */ NATIVE_NOTIFY("/api/wx-pay/js-api/notify"), /** * 退款结果通知 */ REFUND_NOTIFY("/api/wx-pay/js-api/refunds/notify"); /** * 类型 */ final String type; WxJSNotifyType(String s) { this.type = s; } }
⚠️注意: 回调地址是你自定义嗷,我这里后续将回调接口放在 WechatUniAppJsApiController 里面所以回调接口地址是 "/api/wx-pay/js-api/notify"
编写小程序统一下单接口
真滴是一模一样,我也是去pc端接口复制来的
// 引入装饰器 private final CloseableHttpClient wxPayClient; // 引入订单服务 private final OrderInfoService orderInfoService; // 引入微信签名验证 private final Verifier verifier; /** * 小程序JSApi 调用统一下单API,生成支付二维码 */ @SneakyThrows @PostMapping("{productId}") public R<Map<String, Object>> jsPayPay(@PathVariable Long productId, @RequestParam("openId") String openId) { // 将昵称拆出来用于后续我的订单展示 String[] arr = openId.split("\\|"); String openIdTep = arr[0]; String nickName = arr.length > 1 ? arr[1] : "小程序用户"; // 生成订单 OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId, nickName); String prepayId = orderInfo.getCodeUrl(); // prepayId if (StrUtil.isNotEmpty(prepayId) && "未支付".equals(orderInfo.getOrderStatus())) { log.info("订单已存在,JSAPI已保存"); Map<String, Object> map = WxSignUtil.jsApiCreateSign(prepayId); log.info("唤起小程序支付参数:{}", map); return R.ok(map); } HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxJSApiType.JSAPI_PAY.getType())); Map<String, Object> paramsMap = new HashMap<>(14); paramsMap.put("appid", wxPayConfig.getAppid()); paramsMap.put("mchid", wxPayConfig.getMchId()); paramsMap.put("description", orderInfo.getTitle() + "-" + nickName); paramsMap.put("out_trade_no", orderInfo.getOrderNo()); paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxJSNotifyType.NATIVE_NOTIFY.getType())); Map<String, Object> amountMap = new HashMap<>(); amountMap.put("total", orderInfo.getTotalFee()); amountMap.put("currency", "CNY"); // 设置金额 paramsMap.put("amount", amountMap); paramsMap.put("payer", new HashMap<String, Object>() {{ put("openid", openIdTep); }}); //将参数转换成json字符串 JSONObject jsonObject = JSONUtil.parseObj(paramsMap); log.info("请求参数 ===> {0}" + jsonObject); StringEntity entity = new StringEntity(jsonObject.toString(), "utf-8"); entity.setContentType("application/json"); httpPost.setEntity(entity); httpPost.setHeader("Accept", "application/json"); CloseableHttpResponse response = wxPayClient.execute(httpPost); String bodyAsString = EntityUtils.toString(response.getEntity()); JSONObject object = JSONUtil.parseObj(bodyAsString); response.close(); prepayId = object.getStr("prepay_id"); return R.ok(WxSignUtil.jsApiCreateSign(prepayId)); }
代码可优化同学们手动将请求参数优化一下也可以
组装小程序调用支付参数
详细文档: https://pay.weixin.qq.com/docs/merchant/apis/mini-program-payment/mini-transfer-payment.html
// yangbuyi Copyright (c) https://yby6.com 2023. package com.yby6.wechat; import cn.hutool.extra.spring.SpringUtil; import cn.hutool.json.JSONUtil; import com.wechat.pay.contrib.apache.httpclient.util.AesUtil; import com.yby6.config.WxPayConfig; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; import java.nio.charset.StandardCharsets; import java.security.*; import java.util.Base64; import java.util.HashMap; import java.util.Map; /** * 微信小程序验签和解密报文 * * @author Yang Shuai * Create By 2023/09/10 * <p> */ @Slf4j @Component public class WxSignUtil<T> { protected static final SecureRandom RANDOM = new SecureRandom(); /** * 生成签名组装微信调起支付参数 * 返回参数如有不理解 请访问微信官方文档 * * @param prepayId 微信下单返回的prepay_id * @return 当前调起支付所需的参数 */ public static Map<String, Object> jsApiCreateSign(String prepayId) { if (StringUtils.isNotBlank(prepayId)) { final WxPayConfig wxPayConfig = SpringUtil.getBean(WxPayConfig.class); final String appid = wxPayConfig.getAppid(); // 加载签名 String timeStamp = String.valueOf(System.currentTimeMillis() / 1000); String nonceStr = String.valueOf(System.currentTimeMillis()); String packageStr = "prepay_id=" + prepayId; String packageSign = sign(buildMessage(appid, timeStamp, nonceStr, packageStr).getBytes(StandardCharsets.UTF_8), wxPayConfig.getPrivateKey(wxPayConfig.getPrivateKeyPath())); Map<String, Object> packageParams = new HashMap<>(6); packageParams.put("appId", appid); packageParams.put("timeStamp", timeStamp); packageParams.put("nonceStr", nonceStr); packageParams.put("package", packageStr); packageParams.put("signType", "RSA"); packageParams.put("paySign", packageSign); return packageParams; } return null; } /** * 生成签名 * <p> * 小程序appId * 时间戳 * 随机字符串 * 订单详情扩展字符串 */ public static String sign(byte[] message, PrivateKey privateKey) { try { Signature sign = Signature.getInstance("SHA256withRSA"); sign.initSign(privateKey); // 加载商户私钥 sign.update(message); // UTF-8 return Base64.getEncoder().encodeToString(sign.sign()); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("当前Java环境不支持SHA256withRSA", e); } catch (SignatureException e) { throw new RuntimeException("签名计算失败", e); } catch (InvalidKeyException e) { throw new RuntimeException("无效的私钥", e); } } /** * 生成随机字符串 微信底层的方法 */ protected static String generateNonceStr() { char[] nonceChars = new char[32]; for (int index = 0; index < nonceChars.length; ++index) { nonceChars[index] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".charAt(RANDOM.nextInt("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".length())); } return new String(nonceChars); } /** * 按照前端签名文档规范进行排序,\n是换行 * * @param appId appId * @param timestamp 时间 * @param nonceStr 随机字符串 * @param prepayIds prepay_id */ public static String buildMessage(String appId, String timestamp, String nonceStr, String prepayIds) { return appId + "\n" + timestamp + "\n" + nonceStr + "\n" + prepayIds + "\n"; } /** * 解密 对称解密 * 参考: <a href="https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient/blob/master/src/main/java/com/wechat/pay/contrib/apache/httpclient/util/AesUtil.java">...</a> * * @param plainText 秘文 * @return {@link String} */ public static <T> T decryptFromResource(String plainText, Class<T> clazz) { Map<String, Object> bodyMap = JSONUtil.toBean(plainText, Map.class); log.info("密文解密"); final WxPayConfig wxPayConfig = SpringUtil.getBean(WxPayConfig.class); //通知数据拿到 resource 节点 Map<String, String> resourceMap = (Map) bodyMap.get("resource"); //数据密文 String ciphertext = resourceMap.get("ciphertext"); //随机串 String nonce = resourceMap.get("nonce"); //附加数据 String associatedData = resourceMap.get("associated_data"); log.info("密文 ===> {}", ciphertext); AesUtil aesUtil = new AesUtil(wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8)); // 使用key、nonce和associated_data,对数据密文resource.ciphertext进行解密,得到JSON形式的资源对象 String resource; try { resource = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8), nonce.getBytes(StandardCharsets.UTF_8), ciphertext); } catch (GeneralSecurityException e) { throw new RuntimeException(e); } log.info("明文 ===> {}", resource); return JSONUtil.toBean(resource, clazz); } }
对比构建请求参数
你看看我没骗你吧我们直接copy这段新增 payer
openid 字段即可
小程序下单支付回调
不用说也是和PC一样的
// 引入处理支付成功日志 private final WxJSAPIPayService wxJSAPIPayService; /** * 支付通知->微信支付通过支付通知接口将用户支付成功消息通知给商户 * * @return {@link R} */ @PostMapping("/notify") public Map<String, String> transactionCallBack(HttpServletRequest request, HttpServletResponse response) { Map<String, String> map = new HashMap<>(12); try { String timestamp = request.getHeader("Wechatpay-Timestamp"); String nonce = request.getHeader("Wechatpay-Nonce"); String serialNo = request.getHeader("Wechatpay-Serial"); String signature = request.getHeader("Wechatpay-Signature"); log.info("timestamp:" + timestamp + " nonce:" + nonce + " serialNo:" + serialNo + " signature:" + signature); //处理通知参数 String body = HttpUtils.readData(request); log.info("支付通知密文: {} ", body); JSONObject jsonObject = JSONUtil.parseObj(body); final String requestId = jsonObject.getStr("id"); //签名的验证 WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest = new WechatPay2ValidatorForRequest(verifier, requestId, body); if (!wechatPay2ValidatorForRequest.validate(request)) { log.error("通知验签失败"); //失败应答 response.setStatus(500); return WechatRep.fail(); } // 处理订单 final CallBackResource decrypt = WxSignUtil.decryptFromResource(body, CallBackResource.class); wxJSAPIPayService.processOrder(JSONUtil.toJsonStr(decrypt)); log.info("回调业务处理完毕"); response.setHeader("Content-type", ContentType.JSON.toString()); response.getOutputStream().write(JSONUtil.toJsonStr(WechatRep.ok()).getBytes(StandardCharsets.UTF_8)); response.flushBuffer(); } catch (Exception e) { log.error("处理微信回调失败:", e); } // 成功应答 response.setStatus(200); return WechatRep.ok(); }
编写小程序下单日志记录
在 service
当中创建 WxJSAPIPayService ,实不相瞒哈哈哈我也是直接PC拿过来的
package com.yby6.service; import cn.hutool.json.JSONUtil; import com.yby6.config.WxPayConfig; import com.yby6.domain.OrderInfo; import com.yby6.domain.wechat.CallBackResource; import com.yby6.enums.OrderStatus; import com.yby6.enums.weChatPayNative.WxNotifyType; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.springframework.stereotype.Service; import java.util.HashMap; import java.util.Map; import java.util.concurrent.locks.ReentrantLock; /** * @author Yang Shuai * Create By 2023/9/9 */ @Service @Slf4j @RequiredArgsConstructor public class WxJSAPIPayService { private final ReentrantLock lock = new ReentrantLock(); private final OrderInfoService orderInfoService; private final PaymentInfoService paymentInfoService; /** * jsapi回调 */ public void processOrder(String plainText) { final CallBackResource data = JSONUtil.toBean(plainText, CallBackResource.class); log.info("处理订单"); // 微信特别提醒: // 在对业务数据进行状态检查和处理之前, // 要采用数据锁进行并发控制,以避免函数重入造成的数据混乱. // 尝试获取锁: // 成功获取则立即返回true,获取失败则立即返回false。不必一直等待锁的释放. if (lock.tryLock()) { try { // 处理重复的通知 // 接口调用的幂等性:无论接口被调用多少次,产生的结果是一致的。 OrderInfo orderInfo = orderInfoService.lambdaQuery().eq(OrderInfo::getOrderNo, (data.getOutTradeNo())).one(); if (null != orderInfo && !OrderStatus.NOTPAY.getType().equals(orderInfo.getOrderStatus())) { log.info("重复的通知,已经支付成功啦"); return; } // 模拟通知并发 //TimeUnit.SECONDS.sleep(5); // 更新订单状态 orderInfoService.lambdaUpdate().eq(OrderInfo::getOrderNo, data.getOutTradeNo()).set(OrderInfo::getOrderStatus, OrderStatus.SUCCESS.getType()).update(); log.info("更新订单状态,订单号:{},订单状态:{}", data.getOutTradeNo(), OrderStatus.SUCCESS); // 记录支付日志 paymentInfoService.createPaymentInfo(plainText); } finally { // 要主动释放锁 lock.unlock(); } } } /*==========================================================================*/ }
支付回调的实体类映射
// yangbuyi Copyright (c) https://yby6.com 2023. package com.yby6.domain.wechat; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import lombok.NoArgsConstructor; /** * js API 支付回调 * * @author Yang Shuai * Create By 2023/9/9 */ @NoArgsConstructor @Data public class CallBackResource { /** * mchid */ @JsonProperty("mchid") private String mchid; /** * appid */ @JsonProperty("appid") private String appid; /** * 贸易没有 */ @JsonProperty("out_trade_no") private String outTradeNo; /** * 交易订单id */ @JsonProperty("transaction_id") private String transactionId; /** * 贸易类型 */ @JsonProperty("trade_type") private String tradeType; /** * 贸易国家 */ @JsonProperty("trade_state") private String tradeState; /** * 贸易国家desc */ @JsonProperty("trade_state_desc") private String tradeStateDesc; /** * 银行类型 */ @JsonProperty("bank_type") private String bankType; /** * 附加 */ @JsonProperty("attach") private String attach; /** * 成功时间 */ @JsonProperty("success_time") private String successTime; /** * 付款人 */ @JsonProperty("payer") private PayerDTO payer; /** * 量 */ @JsonProperty("amount") private AmountDTO amount; /** * ../ * * @author Yang Shuai * Create By 2023/05/24 */ @NoArgsConstructor @Data public static class PayerDTO { /** * openid */ @JsonProperty("openid") private String openid; } /** * ../ * * @author Yang Shuai * Create By 2023/05/24 */ @NoArgsConstructor @Data public static class AmountDTO { /** * 总 */ @JsonProperty("total") private Integer total; /** * 付款人总 */ @JsonProperty("payer_total") private Integer payerTotal; /** * 货币 */ @JsonProperty("currency") private String currency; /** * 支付货币 */ @JsonProperty("payer_currency") private String payerCurrency; } }
本次新增的代码
测试小程序统一下单接口
重新启动小程序、重新启动后端服务、开启内网穿透、小程序进行授权登录拿到 openId 复制一份
openId: o6Yr-xxxxxxxxxx
切换后端idea 使用接口调试工具发送下单请求
完美没有任何问题
八、小程序调起支付窗口
uni.requestPayment(OBJECT)
支付
uni.requestPayment是一个统一各平台的客户端支付API,不管是在某家小程序还是在App中,客户端均使用本API调用支付。
本API运行在各端时,会自动转换为各端的原生支付调用API。
注意支付不仅仅需要客户端的开发,还需要服务端开发。虽然客户端API统一了,但各平台的支付申请开通、配置回填仍然需要看各个平台本身的支付文档。
比如微信有App支付、小程序支付、H5支付等不同的申请入口和使用流程,对应到uni-app,在App端要申请微信的App支付,而小程序端则申请微信的小程序支付。
详细文档地址: https://uniapp.dcloud.net.cn/api/plugins/payment.html#%E7%94%B3%E8%AF%B7%E6%B5%81%E7%A8%8B-2
这参数和我们组装的是一致的后面只需要将参数设置进去发起调用支付即可
编写小程序统一下单请求
修改 wechatPay.js
// 统一JSAPI下单 export function JSAPI(productId, openId) { return request({ 'url': `/api/wx-pay/js-api/${productId}`, 'method': 'post', 'params': { "openId" : openId } }) }
完善 toPay 函数
// 发起支付 const toPay = async () => { // 获取微信支付凭证创建支付订单 const storageSync = uni.getStorageSync('token'); const nickName = uni.getStorageSync('nickName'); // 发送小程序统一下单 const {code, data} = await JSAPI(payOrder.value.productId, storageSync + "|" + nickName) if (code !== 200) { toast("创建订单失败请稍后重试!") return } toast("创建订单成功正在拉起支付请稍等....") setTimeout(() => { const wx = data // 调用微信支付弹窗 uni.requestPayment({ provide: 'wxpay', timeStamp: wx.timeStamp, // 当前时间 nonceStr: wx.nonceStr, // 随机字符串 package: wx.package, // prepayId signType: wx.signType, // 签名算法 paySign: wx.paySign, // 支付签名 success: (res) => { loading.value = false toast("支付成功请在订单列表查看订单状态,并且退款功能也在订单列表中哦", 5) }, fail: (res) => { console.log(res); toast("取消支付可继续点击支付重新发起") loading.value = false } }) }, 500) }
测试完整小程序统一下单流程
启动小程序、启动后端服务、启动花生壳内网穿透、清空小程序缓存
完美没有任何Bug的出现进行扫码可以进行支付了
支付成功
最后
本期结束咱们下次再见👋~
🌊 关注我不迷路,如果本篇文章对你有所帮助,或者你有什么疑问,欢迎在评论区留言,我一般看到都会回复的。大家点赞支持一下哟~ 💗