从零玩转系列之微信支付实战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的出现进行扫码可以进行支付了


支付成功



最后


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

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

相关文章
|
16天前
|
小程序 安全 数据安全/隐私保护
微信小程序全栈开发中的身份认证与授权机制
【4月更文挑战第12天】本文探讨了微信小程序全栈开发中的身份认证与授权机制。身份认证包括手机号验证、微信登录和第三方登录,而授权机制涉及角色权限控制、ACL和OAuth 2.0。实践中,开发者可利用微信登录获取用户信息,集成第三方登录,以及实施角色和ACL进行权限控制。注意点包括安全性、用户体验和合规性,以保障小程序的安全运行和良好体验。通过这些方法,开发者能有效掌握小程序全栈开发技术。
|
2月前
|
存储 移动开发 JavaScript
uni-app页面数据传参方式
uni-app页面数据传参方式
61 4
|
15天前
|
C++
深入理解 uni-app 页面生命周期(三):onHide vs onUnload
深入理解 uni-app 页面生命周期(三):onHide vs onUnload
|
1月前
|
JSON 小程序 C#
微信网页授权之使用完整服务解决方案
微信网页授权之使用完整服务解决方案
|
1月前
|
小程序 前端开发 程序员
【微信小程序】-- 网络数据请求(十九)
【微信小程序】-- 网络数据请求(十九)
|
1月前
uni-app 22发布朋友圈页面
uni-app 22发布朋友圈页面
17 0
uni-app 22发布朋友圈页面
|
1月前
|
前端开发
uni-app 4.14~4.23首页消息页面开发
uni-app 4.14~4.23首页消息页面开发
17 1
|
2月前
|
小程序 JavaScript
微信小程序授权登录?
微信小程序授权登录?
|
16天前
|
小程序 前端开发 API
微信小程序全栈开发中的异常处理与日志记录
【4月更文挑战第12天】本文探讨了微信小程序全栈开发中的异常处理和日志记录,强调其对确保应用稳定性和用户体验的重要性。异常处理涵盖前端(网络、页面跳转、用户输入、逻辑异常)和后端(数据库、API、业务逻辑)方面;日志记录则关注关键操作和异常情况的追踪。实践中,前端可利用try-catch处理异常,后端借助日志框架记录异常,同时采用集中式日志管理工具提升分析效率。开发者应注意安全性、性能和团队协作,以优化异常处理与日志记录流程。
|
16天前
|
JavaScript 前端开发 小程序
微信小程序全栈开发之性能优化策略
【4月更文挑战第12天】本文探讨了微信小程序全栈开发的性能优化策略,包括前端的资源和渲染优化,如图片压缩、虚拟DOM、代码分割;后端的数据库和API优化,如索引创建、缓存使用、RESTful API设计;以及服务器的负载均衡和CDN加速。通过这些方法,开发者可提升小程序性能,优化用户体验,增强商业价值。

热门文章

最新文章