微信小程序登录和获取信息功能记录

背景需求

小程序用户进入小程序,判断用户未登录则跳转至登录状态进行登录操作,登录时后如果后台没有相关信息,则跳转至获取信息信息页面获取之后再进行登录。

img

小程序前台

检测用户是否登录,我将用户信息存到了 storage 中,以此来判断,在 onMounted 中调用:

1
2
3
4
5
6
7
const checkUserLogin = () => {
if (uni.getStorageSync("user") === "") {
uni.redirectTo({ url: 'login' })
} else {
console.log(uni.getStorageSync("user"));
}
}

如果用户没有登录,则跳转至登录页面。登录页:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
<view>
<up-button open-type="get" @click="userLogin()" type="primary" size="normal" shape="circle" text="登录"
:hairline="true"></up-button>
</view>

// script
const userLogin = () => {

// 使用 uniapp 的微信小程序登录接口
uni.login({
"provider": "weixin",
// 微信登录仅请求授权认证
"onlyAuthorize": true,
success: function (event) {
const { code } = event
console.log("weixin code: ", code);

// 根据项目需求,自定义的业务逻辑
if (uni.getStorageSync("dbFlag") && uni.getStorageSync("userData")) {
const userData = uni.getStorageSync("userData")
wxUserLogin({
code: code,
phone: userData.phone,
nickName: userData.nickName,
avatar: userData.avatar
}).then((res: any) => {
if (res.statusCode === 200) {
if (res.data.code === ResponseCode.Success) {
// 设置登录态
currentUser.value = res.data.data
let user: UserAuth = {}
user.token = currentUser.value.token
user.type = Number(currentUser.value.type)
user.userId = currentUser.value.id

uni.removeStorageSync("user")
uni.setStorageSync("user", JSON.stringify(user))
uni.removeStorageSync("userData")
uni.removeStorageSync("dbFlag")

uni.redirectTo({
url: "index"
})
} else {
uni.showModal({
title: '登录失败',
content: "登录失败,请检查网络重试",
success(res) {
if (res.confirm) {
} else if (res.cancel) {
}
}
})
}
} else {
uni.showModal({
title: '登录失败',
content: res.message,
success(res) {
if (res.confirm) {
// 再次尝试登录
userLogin()
} else if (res.cancel) {
}
}
})
}
})
} else {
wxUserLogin({
code: code
}).then((res: any) => {
if (res.statusCode === 200) {
if (res.data.code === ResponseCode.Success) {
if (res.data.data.dbFlag == 1) {
// 表明当前登录的用户在后台数据库中没有对应的用户昵称
// 等信息,需要获取保存到数据库中。
uni.setStorageSync("dbFlag", res.data.data.dbFlag)
setUserInfo()
console.log("后台数据库中没有用户信息,跳转至获取用户信息页!");
return
}

// 设置登录态
currentUser.value = res.data.data
let user: UserAuth = {}
user.token = currentUser.value.token
user.type = Number(currentUser.value.type)
user.userId = currentUser.value.id

uni.removeStorageSync("user")
uni.setStorageSync("user", JSON.stringify(user))

uni.redirectTo({
url: "index"
})
} else {
uni.showModal({
title: '登录失败',
content: "登录失败,请检查网络重试",
success(res) {
if (res.confirm) {
// 再次尝试登录
userLogin()
} else if (res.cancel) {
}
}
})
}
} else {
uni.showModal({
title: '登录失败',
content: res.message,
success(res) {
if (res.confirm) {
// 再次尝试登录
userLogin()
} else if (res.cancel) {
}
}
})
}
})
}
},
fail: function (err) {
console.log("微信登录失败:", err);
}
})
}

如果后台数据库中没有用户的身份信息,则返回的标志位为 1 ,小程序跳转到获取用户身份信息的页面,进行获取:

1
2
3
4
// 用户在登录之后,需要获取用户的昵称和头像信息用于展示
const setUserInfo = () => {
uni.redirectTo({ url: 'getUserInfo' })
}

获取用户信息的 vue 页面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
<template>
<up-navbar leftIconColor="#f5f5f5" :placeholder="true" :safeAreaInsetTop="true" title="获取用户头像昵称" :autoBack="true">
</up-navbar>
<up-toast ref="uToastRef"></up-toast>
<view class="containar">
<view class="index">
<view class="avatarUrl">
<button open-type="chooseAvatar" @chooseavatar="onChooseavatar">
<image :src="avatarUrl" class="refreshIcon"></image>
</button>
</view>
<view class="userName">
<text>昵称:</text>
<input :clearable="false" type="nickname" class="weui-input" :value="userName" @blur="bindblur"
placeholder="请选择或输入昵称" @input="bindinput" />
</view>
<view class="userPhone">
<text>手机号:</text>
<up-button class="phone" type="success" open-type="getPhoneNumber"
@getphonenumber="getPhoneNumber">获取手机号</up-button>
</view>
</view>

<view style="width: 100%;height: 1px; background: #EEE;">

</view>
<view class="tips_1" style="width: 700rpx; height: 20px; font-size: 13px; margin: auto; margin-top: 40rpx;">
· 申请获取以下权限
</view>
<view class="tips">
· 获得你的信息(昵称、头像、手机号等)
</view>

<view>
<br>
<up-button @click="onSubmit" type="primary">保存</up-button>
</view>
</view>
</template>

<script lang="ts" setup>
import { ResponseCode } from "@/types";
import { getUserPhone } from "../alova/service"
import { BASE_URL } from '@/config';

const avatarUrl = ref<any>()
const userName = ref<string>('')
const avatarPath = ref<string>('')
const userPhone = ref<string>('')
const uToastRef = ref(null)
const list = ref([
{
type: 'success',
position: 'top',
icon: false,
duration: 3000,
message: "小程序检测到没有您的用户信息,请依次点击下方头像和昵称以获取!"
}
])

onReady(() => {
showToast(list.value[0])
})

// 方法
const showToast = (params: any) => {
uToastRef.value.show({
...params,
complete() {
params.url && uni.navigateTo({
url: params.url
});
}
});
}

const getPhoneNumber = (e: any) => {

console.log("微信获取电话号码的 code: ", e.detail.code);

uni.login({
provider: 'weixin',
success: (_: any) => {
// 调用后端接口获取用户手机号码
getUserPhone({ code: e.detail.code }).then((res: any) => {
if (res.data.code === ResponseCode.Success) {
const data = JSON.parse(res.data.data)
// console.log(data);
userPhone.value = data.phone_info.phoneNumber;
}
})
},
fail: ((err: any) => {
uni.showModal({
title: '错误',
content: '获取用户手机号失败, err: ' + err,
success(res) {
if (res.confirm) {
} else if (res.cancel) {
}
}
})
})
});

// 手动赋值,dev 跳过获取手机号
// userPhone.value='18198086793'

}

const bindblur = (e: any) => {
userName.value = e.detail.value;
}

const bindinput = (e: any) => {
userName.value = e.detail.value;
}

const onChooseavatar = (e: any) => {

avatarUrl.value = e.detail.avatarUrl
}

/**
* 上传头像,返回头像的路径信息
*/
const onSubmit = () => {

if (userName.value == '' || userPhone.value == '' || avatarUrl.value == '') {
uni.showModal({
title: '个人信息未设置',
content: '请点击相应选项以设置',
})
return
}

// 在微信小程序开发工具和真机调试中,不会出现问题。如果在体验版中,需要在小程序后台设置上传文件域名,不然报错!
// 在提交中,将用户的昵称和头像信息提交到数据库存储
// 先上传图片获得 图片路径
uni.uploadFile({
url: BASE_URL + 'upload/avatar',
filePath: avatarUrl.value,
name: 'file',
success: function (res) {
let resp = JSON.parse(res.data)
avatarPath.value = resp.data

// 保存用户信息
save()
},
fail: function (err) {
uni.showModal({
title: '上传头像文件失败',
content: err.errMsg,
})
console.log(err)
}
})
}

const save = () => {

const data = {
nickName: userName.value,
avatar: avatarPath.value,
phone: userPhone.value,
}

// 暂时存储到 storage 中,在 login 中使用
uni.removeStorageSync("userData")
uni.setStorageSync("userData", data)

// 跳转至登录页
uni.redirectTo({
url: "login"
})
}
</script>

<style lang="scss" scoped>
.containar {
width: 100vw;
height: 100vh;
background: #fff;
box-sizing: border-box;
padding: 0 30rpx;

.index {
display: flex;
align-items: center;
flex-direction: column;
}

.avatarUrl {
padding: 80rpx 0 40rpx;
background: #fff;

button {
background: #fff;
line-height: 80rpx;
height: auto;
border: none !important;
width: auto;
// padding: 20rpx 30rpx;
margin: 0;
display: flex;
border: none;
justify-content: center;
align-items: center;

&::after {
border: none;
}

.refreshIcon {
width: 160rpx;
height: 160rpx;
border-radius: 50%;
background-color: #ccc;
}

.jt {
width: 14rpx;
height: 28rpx;
}
}
}

.userName {
background: #fff;
padding: 20rpx 30rpx 80rpx;
display: flex;
align-items: center;
justify-content: center;

// set font color
color: black;

.weui-input {
width: 110px;
padding-left: 60rpx;
}
}

.userPhone {
display: flex;
width: 233px;
align-items: center;
margin-bottom: 10px;

// 解决如果用户手机设置了黑暗模式,字不显示的问题
color: black;
}
}

.tips {
width: 700rpx;
height: 20px;
font-size: 13px;
margin: 5px 0 15px 19px;
color: #cbcbcb;
}

.tips_1 {
color: black;
}

::v-deep .u-button {
border-radius: 10px !important;
width: 160px !important;
}
</style>

用户点击确认之后,跳转至 login 页面,完成登录操作。

Spring Boot 后台逻辑

微信小程序用户登录接口

在前面的流程图中,微信小程序的登录操作是先请求微信服务器,获取 code,在将 code 传递至应用后台服务器,请求接口获取 sessionId 和 openId 等信息。

controller 代码:

1
2
3
4
5
6
7
8
@PostMapping("/user/weixin/login")
public Result<Map<String, String>> login(@RequestBody User user) {

log.info("微信小程序用户登录....");
Map<String, String> userInfo = garageUserService.weChatLogin(user);

return Result.success(userInfo);
}

Service 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
public Map<String, String> weChatLogin(User user) {

// 微信小程序通用登录逻辑
String url = GarageConstants.WeChatConstants.baseUrl +
"?appid=" +
configProperties.getAppId() +
"&secret=" +
configProperties.getAppSecret() +
"&js_code=" +
user.getCode() +
"&grant_type=authorization_code";

String resp = restTemplate.getForObject(url, String.class);
WxCode2SessionResp wxCode2SessionResp = JsonUtils.fromJson(resp, WxCode2SessionResp.class);

Map<String, String> res = new HashMap<>();
if (Objects.isNull(wxCode2SessionResp) || Objects.isNull(wxCode2SessionResp.getSession_key())) {
log.error("微信小程序登录失败,查看日志!!!");
throw new ServiceException("微信登录失败,请重试!");
}

// 业务逻辑
// 根据 openId 查询
int dbFlag;
// 如果 openId 为空,则证明用户从未登陆过,则需要设置用户相关信息,手机号,头像,昵称等。
User openIdUser = this.userMapper.selectOneByQuery(new QueryWrapper().eq(User::getOpenId, wxCode2SessionResp.getOpenid()));
if (openIdUser == null) {
// 证明用户未登陆过
if (user.getPhone() == null) {
// 用户未登录过,同时也没有携带用户信息,则直接返回。
dbFlag = 1;
res.put("dbFlag", dbFlag + "");

} else {
User phoneUser = this.userMapper.selectOneByQuery(new QueryWrapper().eq(User::getPhone, user.getPhone()));
if (phoneUser == null) {
// 为用户,插入数据
var wxUser = new User();
wxUser.setId(SnowFlakeIdGenerator.generateId());
wxUser.setType(2);
wxUser.setAvatar(user.getAvatar());
wxUser.setNickName(user.getNickName());
wxUser.setOpenId(wxCode2SessionResp.getOpenid());

this.userMapper.insert(wxUser);

// 登录
StpUtil.login(wxUser.getId());
res.put("token", StpUtil.getTokenValue());
res.put("id", wxUser.getId() + "");
res.put("type", wxUser.getType() + "");

} else {
// 维护人员,更新数据
phoneUser.setAvatar(user.getAvatar());
phoneUser.setNickName(user.getNickName());
phoneUser.setOpenId(wxCode2SessionResp.getOpenid());

this.userMapper.update(phoneUser);

// 登录
StpUtil.login(phoneUser.getOpenId());
res.put("token", StpUtil.getTokenValue());
res.put("id", phoneUser.getId() + "");
res.put("type", phoneUser.getType() + "");

}
}
} else {
// 用户信息已经存在,直接登录
StpUtil.login(openIdUser.getId());
res.put("token", StpUtil.getTokenValue());
res.put("id", openIdUser.getId() + "");
res.put("type", openIdUser.getType() + "");

}

return res;
}

entity :用来接受微信 API 返回的数据信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class WxCode2SessionResp {

// openId 用户唯一标识
private String openid;

// 会话密钥
private String session_key;

// 用户在开放平台的唯一标识符,若当前小程序已绑定到微信开放平台帐号下会返回,详见 UnionID 机制说明。
private String unionid;

// number 错误码
private String errcode;

// 错误信息
private String errmsg;
}

获取用户手机号接口

微信小程序在获取用户手机号时,需要在后台调用 API 接口完成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// controller
@PostMapping(GarageConstants.WeChatConstants.apiPrefix + "/getPhoneNumber")
public Result<String> getPhoneNumber(@RequestBody User user) {

return Result.success(garageUserService.getPhoneNumber(user));
}

// service
@Override
public String getPhoneNumber(User user) {

String accessToken = WeChatMsgHelper.refreshAccessToken(
configProperties.getAppId(),
configProperties.getAppSecret(),
restTemplate);

// String PhoneBaseUrl = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=";
String url = GarageConstants.WeChatConstants.PhoneBaseUrl + accessToken;
Map<String, String> paramMap = new HashMap<>();
paramMap.put("code", user.getCode());
String body = JsonUtils.toJson(paramMap);

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
// spring boot 3.x 以上必须设置,不然会出现 412 错误状态码
headers.setContentLength(body.getBytes(StandardCharsets.UTF_8).length);

HttpEntity<String> httpEntity = new HttpEntity<>(body, headers);
ResponseEntity<String> response = restTemplate.postForEntity(url, httpEntity, String.class);

return response.getBody();
}

// helper
public class WeChatMsgHelper {

private static final Logger logger = LoggerFactory.getLogger(WeChatMsgHelper.class);

// 推送消息的时候需要 accessToken,这里是获取
// token的方法,在实际环境中,一般是定时刷新,
// 两个小时内有效,获取accesstoken时需要appid和secret信息,一般是后台参数或常量进行配置
// 获取 accessToken 的请求接口
// String TokenBaseUrl = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential";
private final static String GetAccessToken = GarageConstants.WeChatConstants.TokenBaseUrl + "&appid={appid}&secret={secret}";

public static String refreshAccessToken(String appid, String secret, RestTemplate restTemplate){

if(!StringUtils.hasText(appid) || !StringUtils.hasText(secret)){
logger.error("刷新微信 AccessToken 时,缺少 appid 或 secret 参数!");
throw new ServiceException("刷新微信 AccessToken 时,缺少 appid 或 secret 参数!");
}

Map<String, Object> param = new HashMap<>();
param.put("appid",appid);
param.put("secret",secret);

ResponseEntity<JSONObject> resp = restTemplate.getForEntity(
GetAccessToken,
JSONObject.class,
param
);

JSONObject jsonObj = resp.getBody();
String accessToken = null;
if(jsonObj != null){
accessToken = jsonObj.getStr("access_token");
}

return accessToken;
}
}

微信小程序登录和获取信息功能记录
http://yuluo-yx.github.io/2024/10/07/微信小程序登录和获取信息功能记录/
作者
yuluo
发布于
2024年10月7日
许可协议