从零玩转系列之微信支付实战Uni-App微信授权登录和装修下单页面和搭建下单接口以及发起下单请求3

简介: 从零玩转系列之微信支付实战Uni-App微信授权登录和装修下单页面和搭建下单接口以及发起下单请求3

七、小程序下单接口


商户系统先调用该接口在微信支付服务后台生成预支付交易单,返回正确的预支付交易会话标识后再按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的出现进行扫码可以进行支付了


支付成功



最后


本期结束咱们下次再见👋~

🌊 关注我不迷路,如果本篇文章对你有所帮助,或者你有什么疑问,欢迎在评论区留言,我一般看到都会回复的。大家点赞支持一下哟~ 💗

相关文章
|
2月前
|
人工智能 JavaScript 定位技术
微信的接口都有哪些?
【10月更文挑战第17天】微信的接口都有哪些?
181 43
|
2月前
|
JSON 小程序 应用服务中间件
微信的接口wxLogin()的返回值都有什么?
【10月更文挑战第4天】微信的接口wxLogin()的返回值都有什么?
281 1
|
1月前
|
小程序 前端开发 算法
|
2月前
|
移动开发 前端开发 Android开发
开发指南059-App实现微信扫描登录
App是用uniapp开发的,打包为apk,上传到安卓平板中使用
|
2月前
|
JSON 前端开发 API
使用微信JS-SDK调用发票接口的完整开发指南
本文介绍了如何使用微信JS-SDK的`chooseInvoiceTitle`接口来调用微信的发票功能。通过微信发票接口,用户可以选择开具个人或单位发票,并获取相关发票信息,如抬头、税号、公司地址等。在文中,详细描述了JS-SDK的初始化、发票接口的调用方式,并提供了完整的代码示例。文章还介绍了如何处理返回的发票信息,帮助开发者快速集成微信发票功能。
93 2
|
2月前
|
JavaScript 小程序 开发者
uni-app开发实战:利用Vue混入(mixin)实现微信小程序全局分享功能,一键发送给朋友、分享到朋友圈、复制链接
uni-app开发实战:利用Vue混入(mixin)实现微信小程序全局分享功能,一键发送给朋友、分享到朋友圈、复制链接
456 0
|
3月前
|
小程序 开发者
微信小程序之网络数据请求 wx:request的简单使用
这篇文章介绍了微信小程序中如何使用wx.request进行网络数据请求,包括请求的配置、请求的格式以及如何在开发阶段关闭请求的合法检验。
微信小程序之网络数据请求 wx:request的简单使用
|
2月前
|
小程序 算法 前端开发
微信小程序---授权登录
微信小程序---授权登录
104 0
|
3月前
|
小程序 PHP
微信小程序给 thinkphp后端发送请求出现错误 Wrong number of segments 问题的解决 【踩坑记录】
本文记录了微信小程序向ThinkPHP后端发送请求时出现"Wrong number of segments"错误的解决方法。问题原因是小程序请求header中的token变量名写错,导致token未正确传递至后端。作者提供了详细的检查步骤和建议,包括验证URL路径、参数规范和路由配置的匹配,以确保请求能正确发送和处理。
|
4月前
|
小程序 JavaScript Java
微信小程序+SpringBoot接入后台服务,接口数据来自后端
这篇文章介绍了如何将微信小程序与SpringBoot后端服务进行数据交互,包括后端接口的编写、小程序获取接口数据的方法,以及数据在小程序中的展示。同时,还涉及到了使用Vue搭建后台管理系统,方便数据的查看和管理。
微信小程序+SpringBoot接入后台服务,接口数据来自后端
下一篇
DataWorks