一、前言
欢迎来到本期的博客!
本篇将为您介绍微信支付在小程序 Uniapp 端的全新篇章。微信支付作为移动支付领域的先驱之一,不断演进与创新,为用户和开发者提供更便捷、安全的支付体验。在本文中,我们将深入探讨微信支付在小程序 Uniapp 端的应用与优势。
随着移动互联网的蓬勃发展,小程序成为了用户获取信息、进行交互和购买商品的重要平台之一。微信支付作为小程序中不可或缺的支付方式,不仅为用户提供了快捷方便的支付方式,还为商家创造了更多的销售机会。在这个背景下,微信支付不断完善其在小程序 Uniapp 端的集成,以满足不同场景下的支付需求。
在本文中,我们将探讨微信支付在小程序 Uniapp 端的接入步骤,介绍其提供的各种支付功能,我们还将深入研究微信支付在安全性方面的保障措施,确保用户的支付信息和资金得到充分的保护。
无论您是小程序开发者还是企业主,亦或是对移动支付技术感兴趣的读者,本文都将为您提供有关在小程序 Uniapp 端集成微信支付的实用知识和技巧。让我们一同探索微信支付的新篇章,为用户营造更加便捷、安全的支付环境。
? 本次为前端知识点如果不懂前段可以去仓库直接copy出来使用,如果有什么问题可以在评论区留言,我会第一时间回复大家的.关注我不迷路,如果本篇文章对你有所帮助,或者你有什么疑问,欢迎在评论区留言,我一般看到都会回复的。大家点赞支持一下哟~ ?
- 第一章从零玩转系列之微信支付开篇
- 第二章从零玩转系列之微信支付安全
- 第三章从零玩转系列之微信支付实战基础框架搭建
- 第四章从零玩转系列之微信支付实战PC端支付下单接口搭建
- 第五章从零玩转系列之微信支付实战PC端支付微信回调接口搭建
- 第六章从零玩转系列之微信支付实战PC端支付微信取消订单接口搭建
- 第七章从零玩转系列之微信支付实战PC端支付微信退款订单接口搭建
- 第八章从零玩转系列之微信支付实战PC端项目构建Vue3+Vite+页面基础搭建
- 第九章从零玩转系列之微信支付实战PC端装修下单页面
- 第十章从零玩转系列之微信支付实战PC端装修我的订单下单页面
- 第十一章从零玩转系列之微信支付实战PC端我的订单接入退款取消接口
- 第十二章从零玩转系列之微信支付实战Uni-App基础项目搭建
- 第十三章从零玩转系列之微信支付实战Uni-App微信授权登录和装修下单页面和搭建下单接口以及发起下单请求
本次项目使用技术栈
后端: SpringBoot3.1.x、Mysql8.0、MybatisPlus
前端: Vue3、Vite、ElementPlus
小程序: Uniapp、UviewPlus、Vue3
一、登录原型需求分析
可以看到一打开映入眼帘的是需要登录,可能同学们会有疑问?️? 为啥PC端的不需要就可以直接玩微信支付呢?
遇事不决直接打开文档看看为啥子
商户微信支付文档: https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_1.shtml
必须要传递 openId 才能玩支付呀,那么这玩意在哪里拿? 只能是登录咯
⚠️注意: 登录功能等搭建完毕下单页面在进行添加登录弹出功能
⚠️注意: 登录功能等搭建完毕下单页面在进行添加登录弹出功能
⚠️注意: 登录功能等搭建完毕下单页面在进行添加登录弹出功能
二、下单页面原型需求分析
可以看到这里的页面和我们在PC上面编写的不能说一模一样只能说一模一样.
分三个区域 上 中 下 和PC端的代码都是一样的样式我们直接CV大法复制PC端的css文件即可
但是对于没有看过PC的同学我就直接把样式文件贴出来了看下文 全局样式
三、全局样式
首先我们设置一个全局的样式 内边距 在 App.vue 当中
设置内边距 10 橡素
<style lang="scss">
.app-container {
padding: 10px !important;
box-sizing: border-box !important;
}
</style>
静态文件样式
在 static
文件夹当中新增 css 文件夹
新增全局自定义样式文件 global.scss
#index {
margin-left: auto;
margin-right: auto;
padding: 0 10px 80px 10px;
box-sizing: border-box;
}
.comm-title {
overflow: hidden;
clear: both;
margin: 40px 0 30px;
}
#footer {
background-color: #323232;
border-top-width: 5px;
border-top-style: solid;
color: #999;
width: 100%;
overflow: hidden;
padding-top: 30px;
}
.clear {
clear: both;
display: block;
overflow: hidden;
visibility: hidden;
width: 0;
height: 0;
}
#index .content {
padding: 10px;
box-sizing: border-box;
box-shadow: 0 4px 30px #a5a8abcc;
text-align: center;
display: flex;
flex-flow: wrap;
justify-content: space-between;
}
#index .item {
margin: 10px;
}
#index .orderBtn {
position: relative;
border: 1px solid #f3e2c6;
background-color: #ffffff;
color: #ff8686;
font-weight: bold;
border-radius: 5px;
width: 140px;
height: 50px;
line-height: 50px;
font-size: 15px;
display: inline-block;
text-align: center;
text-decoration: none;
}
.current {
border-color: #ff8686 !important;
}
.current:after {
content: "";
display: block;
position: absolute;
right: -1px;
bottom: -1px;
width: 28px;
height: 28px;
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADgAAAA4CAYAAACohjseAAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAOKADAAQAAAABAAAAOAAAAAANV2hTAAADnUlEQVRoBd3ZzUsUYRwH8O/zzK6llaZkm5mkWdEhN4KEgqKkDhpU9KJpNy/hKch/QK9BdRM7SAQeSnujDgUWXqKgyBBqISKDlswwETXzbZ15eubRGbZ1dp3dmdndmQd255nnZef34Tf7G2UBD7fIta3txKs+FcfA2jwJ1HBq8jwHjMZ5DhiL8xTQCOcZYDycJ4CJcK4HroZzNdAMzrVAszhXApPBuQ6YLM5VwFRwrgGminMF0Aou64FWcVkNtAOXtUC7cFkJtBOXdUC7cVkFdAKXNUCncFkBdBKXcaDTuIwC04HLGDBduIwA04lLOzDduLQCM4FLGzBTuLQAM4lTgZL65lRzGkerWyCduQX41oL9eGvIcOzXJcdxNe2Qqi8LFPv7G4sd+wyBPsNRi4OO4giFVHsdtKpRj1IZ7Nb7sR3bM+gojvohne4E3X1Sd8ivb0Dhr3jNVqCjOH8upLO3QcuPCgtjDEp/G5SBrng2MW4b0BRuTQEwP5kwIMNJvk+60A1aekBMM0WG/LwVLHTfcHn0oC1V1AxOqrsJ6VQnSH4p2NcX0TEk7q8rhu9iL2jJUhFhi/OQn7aAfX6SeN/yrOUiYwanXotsPwxCCEiwiZ9QkQGAJQ4yfxvH9YAUVoh1bGEG8uNmsO+vEu+LmrWUQbM49Xpsbgp0V624NAnsBdlQwjPZFxVKTLdoJ3xND0EKysQEm5uA3NsU93kXs1s/TRmYDE5cbTQEzIyDVh4XpyRQBbI+ADa08nYlgSB8jb18frNYy6ZHsXivHhj9qAdutpMSMGnccjTs1yAwNwm6o0aMkC1BIK8Y7NtLPV5SdhBS/V2Q3I1ijE2EOe48MD6kr0mmkzQwVZwWFBv5wCvpH9CKY2JIFI/cIo7sB6k8AencHZCcPDHHxr4s4aaGte1JH5MCWsVp0bGfA0BkRn+m0ZL9IPxFD10BkXLEMmVkEHJPA7+tx7RtKR1NA+3CaVGy4fcAL/m0/IgYUisl4dVVbUr4DeQHl3imp8S5lTdTQLtxWsBs+B0gR0D5I0RrCq+s8qNmjp/VhiwdVwU6hdOiFv/mRGZBNu2B8qkHyrOrPIURbdryMeGfak7jLEe/ygdw3PTSTW+w0O04lcQIwoZAL+BUIGG0b8Ut6hkc/yb7/P7gfxn0Ck5kj5BO0hoO6UCP4fql8sJWFSqAXsHx75tCCenwlRfWkYbQgsik23Hqo0CtlmpB8fmlLvW2VGFa+wcOncY5YWRXPQAAAABJRU5ErkJggg==) no-repeat;
background-size: 28px 28px;
}
.PaymentChannel_title {
position: relative;
display: flex;
padding-left: 18 rpx;
margin: 10px 0;
}
.PaymentChannel_title:before {
content: "";
display: block;
position: absolute;
left: 0;
top: calc(50% - 10px);
width: 4px;
height: 18px;
background: #fa8919;
border-radius: 0 4px 4px 0;
}
.payButtom {
margin: 30px 0;
display: flex;
justify-content: space-around;
}
新增默认HTML样式清空样式文件 reset.scss
@charset "utf-8";
html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td{margin:0;padding:0;border:0;outline:0;vertical-align:baseline;background:transparent}body{font-size:12px;line-height:160%;font-family:"Helvetica Neue",\5FAE\8F6F\96C5\9ED1,"SimHei",Tohoma;word-break:break-all;word-wrap:break-word;position:relative}ol,ul,li{list-style:none}blockquote,q{quotes:none}table{border-collapse:collapse;border-spacing:0;empty-cells:show}blockquote:before,blockquote:after,q:before,q:after{content:'';content:none}:focus{outline:0}ins,s{text-decoration:none}del{text-decoration:line-through}em,i{font-style:normal}a,img{border:0;text-decoration:none}a{text-decoration:none}a:hover{text-decoration:underline}a:focus{outline:0;-moz-outline:0}a:active{outline:0;blr:expression(this.onFocus=this.blur())}h1{font-size:36px;line-height:45px;font-weight:normal}h2{font-size:24px;line-height:30px;font-weight:normal}h3{font-size:18px;line-height:22px;font-weight:normal}h4{font-size:16px;line-height:20px;font-weight:normal}h5{font-size:14px;line-height:18px;font-weight:normal}h6{font-size:12px;line-height:16px;font-weight:normal}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}
这下子我们就有两个文件了,如果一两个引入到页面当中使用还好如果一旦多起来了是不是很麻烦不美观
我们创建一个 index.scss
文件用来管理 到时候引入一个文件就好啦
注意样式表名称
@import 'reset';
@import 'global';
引入全局使用
修改 main.js
放在公共css的下面即可
四、下单页面的搭建
ok,我们继续看看咋玩,可以看到顶部有一个?喇叭旁边有文字滚动
走去组件库看看
滚动通知?
<u-notice-bar color="red" text="本案例使用JSAPI模式拉取微信支付弹出用户进行微信支付操作"></u-notice-bar>
我滴妈 和 ElementPlus一样 so easy to happy! 啊 后面的我就不多说了嗷
使用自定义样式
<template>
<view class="app-container">
<view id="index" class="container">
我是index
</view>
</view>
</template>
跟着PC有肉?吃
剩下的就都和PC的一样了我们直接舔
⚠️ 下面的代码都是在 view id=“index” 当中
设置头部区域内容
<u-notice-bar color="red" text="本案例使用JSAPI模式拉取微信支付弹出用户进行微信支付操作"></u-notice-bar>
<view class="PaymentChannel_title">
<u-tooltip text="个人博客网站: https://yby6.com" copyText="https://yby6.com" overlay></u-tooltip>
</view>
<view class="PaymentChannel_title">
<u-tooltip text="PC端微信支付系统: https://lzys522.cn/wx" copyText="https://lzys522.cn/wx" overlay></u-tooltip>
</view>
<view class="PaymentChannel_title">
<u-tooltip text="博客项目案例: https://lzys522.cn" copyText="https://lzys522.cn" overlay></u-tooltip>
</view>
<view class="PaymentChannel_title"
style="color: red; margin-bottom: 10px;font-size: 14px;height: auto !important;">
<u-tooltip text="开源仓库: https://gitee.com/yangbuyi/wxDemo" copyText="https://gitee.com/yangbuyi/wxDemo"
overlay></u-tooltip>
</view>
设置中部区域内容
<!-- 内容区域 -->
<view class="content" v-if="productList.length > 0">
<view class="item" v-for="product in productList" :key="product.id">
<a :class="['orderBtn', {current:payOrder.productId === product.id}]"
@click="selectItem(product.id, (product.price / 100))" href="javascript:void(0);">
{{ product.title }}
¥{{ product.price / 100 }}
</a>
</view>
</view>
<u-alert description="注意:支付成功后可在订单列表进行退款,退款失败联系我:yangbuyiya" type="warning"></u-alert>
<view class="btn-arr">
<up-button @click="toPay()" color="#ff959b" :disabled='loading' text="确认支付V3"></up-button>
</view>
交互
编写 列表集合
在 setup 当中编写代码(和pc一样哦)
编写 函数方法
下单页面测试
刷新小程序开发工具
我滴妈太好看了~
小Bug
因编辑器格式化 导致 rpx 出现了空格导致样式丢失
解决将空格去掉即可
五、完善交互
我们将下单页面完整的展示出来了,那么我们接下里就是将列表的数据改为动态交互
剩下的API接口都是和PC端一样的可以直接copy pc端项目的api文件夹
创建api请求
商品请求
// axios 发送ajax请求
import request from '@/utils/request';
//查询商品列表
export function getProductList() {
return request({
url: '/api/product/productList',
method: 'get'
});
}
编写发送请求 获取商品列表
import { getProductList } from "../../api/product";
// 获取商品列表
const selectProductList = async () => {
const { data } = await getProductList()
productList.value = data
payOrder.value.productId = data[0].id
}
编写生命周期
我们知道Vue有自己的生命周期UniApp也有,详细文档参考: https://uniapp.dcloud.net.cn/collocation/App.html#applifecycle
import { onLoad, onShow } from '@dcloudio/uni-app'
// ==========================生命周期==========================
onLoad(() => {
// 只会加载一次
console.log("onLoad")
selectProductList()
})
onShow(() => {
// 每次都会加载
})
我们列表不需要重复去请求只需要一次就行了
测试查看
有那个味道了芜湖~
六、小程序下单前置准备授权
搭建微信授权登录
模态框
编写变量存储用户名称和用户头像显示
// 登录授权
let modal = ref({
show: false, // 是否显示
content: '请点击头像和昵称填充信息获取完整的微信支付服务!',
code: '', // 登录授权Code
avatarUrl: 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0',
oldAvatarUrl: 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0',
nickName: '',// 昵称
})
编写 模态框
⚠️注意:微信在多少版本就不支持授权返回用户名称和头像了只能用户自己上传和输入昵称
登录授权获取头像
// 登录授权 - 设置头像
const onChooseAvatar = (e) => {
console.log("onChooseAvatar", e);
const { avatarUrl } = e.detail
modal.value.avatarUrl = avatarUrl
uni.setStorageSync('avatarUrl', avatarUrl)
}
登录授权获取昵称
// 登录授权 - 设置昵称
const changeName = (e) => {
modal.value.nickName = e.detail.value
}
校验是否授权
使用生命周期 onShow
可以每次访问页面的时候触发
onShow(() => {
// 首先去获取是否授权登录了
const storageSync = uni.getStorageSync('token');
const nickName = uni.getStorageSync('nickName');
const avatarUrl = uni.getStorageSync('avatarUrl');
// 如果没有则弹出要求授权登录
if ([ null, undefined, '' ].includes(storageSync)) {
modal.value.show = true
} else {
modal.value.nickName = nickName
modal.value.avatarUrl = avatarUrl
}
})
测试头像获取和昵称
将头像和昵称填写完毕将会自动弹出现授权按钮
授权提交
可以看到我们将头像和昵称填写完毕后出现了提交按钮
登录原型需求分析的时候我们解析过下单需要 OpenId
我我滴妈样式变咯
获取OPENID
流程 uniapp —> 小程序登录授权 —> 获取到Code码 —> 根据Code码去后端请求获取OpenId
说明
- 调用 wx.login() 获取 临时登录凭证code ,并回传到开发者服务器。
- 调用 auth.code2Session 接口,换取 用户唯一标识 OpenID 、 用户在微信开放平台账号下的唯一标识UnionID(若当前小程序已绑定到微信开放平台账号) 和 会话密钥 session_key。
之后开发者服务器可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份。
注意事项
- 会话密钥
session_key
是对用户数据进行 加密签名 的密钥。为了应用自身的数据安全,开发者服务器不应该把会话密钥下发到小程序,也不应该对外提供这个密钥。 - 临时登录凭证 code 只能使用一次
授权提交获取Code
下面截图当中的代码都是需要同学们自己打
Uni-app小程序授权文档: https://uniapp.dcloud.net.cn/api/plugins/login.html#login
发送请求到后端获取OpenId
修改 wechatPay.js
新增请求接口 注意 url和你自己后端一致
传递的参数是 code
、nickName 昵称主要用来区分是小程序用户下单的
// 登录方法获取openId
export function loginOrRegister(data) {
return request({
url: '/api/wx-pay/js-api/loginOrRegister',
method: 'post',
data
})
}
后端微信授权接口
小程序登录详细文档: https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-login/code2Session.html
我们接收到 code
码之后直接访问微信服务器去拉授权信息
创建 WechatUniAppJsApiController
后端我就不细说了我相信都是大佬来的 ?
package com.yby6.controller;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.yby6.config.WxPayConfig;
import com.yby6.domain.wechat.LoginUser;
import com.yby6.reponse.R;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
* 微信小程序JS API 支付
*
* @author Yang Shuai
* Create By 2023/9/9
*/
@Slf4j
@RestController
@RequestMapping("/api/wx-pay/js-api")
@RequiredArgsConstructor
public class WechatUniAppJsApiController {
private final WxPayConfig wxPayConfig;
/**
* 微信小程序登录接口 (登录or注册)
*
* @param loginUser 必填 code
*/
@PostMapping("loginOrRegister")
public R loginOrRegister(@RequestBody LoginUser loginUser) {
return R.ok(getOpenId(loginUser.getCode()));
}
/**
* 获取微信唯一凭证
*
* @param code 代码
* @return {@link String}
*/
private String getOpenId(String code) {
String url = "https://api.weixin.qq.com/sns/jscode2session";
Map<String, Object> map = new HashMap<>();
map.put("appId", wxPayConfig.getAppid());
map.put("secret", wxPayConfig.getSecret());
map.put("js_code", code);
map.put("grant_type", "authorization_code");
String post = HttpUtil.post(url, map);
log.info("微信返回: {}", post);
JSONObject obj = JSONUtil.parseObj(post);
String openid = obj.getStr("openid");
if (StringUtils.isNoneBlank(openid)) {
return openid;
}
throw new RuntimeException("临时登录凭证错误");
}
}
测试小程序授权
启动后端程序,打开小程序清空全部缓存重新编译
将会弹出授权窗口,填写完毕后将会获取到code发送给后端获取openid
返回的OpenId 我们也存入了本地缓存当中
七、小程序下单接口
商户系统先调用该接口在微信支付服务后台生成预支付交易单,返回正确的预支付交易会话标识后再按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#申请流程-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的出现进行扫码可以进行支付了
支付成功
最后
本篇是我写的很累的一篇如果有帮帮助到您麻烦点个赞和评论
太难啦,本期结束咱们下次再见?
? 关注我不迷路,如果本篇文章对你有所帮助,或者你有什么疑问,欢迎在评论区留言,我一般看到都会回复的。大家点赞支持一下哟~ ?
评论区