背景需求
小程序用户进入小程序,判断用户未登录则跳转至登录状态进行登录操作,登录时后如果后台没有相关信息,则跳转至获取信息信息页面获取之后再进行登录。
小程序前台
检测用户是否登录,我将用户信息存到了 storage 中,以此来判断,在 onMounted 中调用:
const checkUserLogin = () => {
if (uni.getStorageSync("user") === "") {
uni.redirectTo({ url: 'login' })
} else {
console.log(uni.getStorageSync("user"));
}
}
如果用户没有登录,则跳转至登录页面。登录页:
<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 ,小程序跳转到获取用户身份信息的页面,进行获取:
// 用户在登录之后,需要获取用户的昵称和头像信息用于展示
const setUserInfo = () => {
uni.redirectTo({ url: 'getUserInfo' })
}
获取用户信息的 vue 页面:
<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 代码:
@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 代码:
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 返回的数据信息。
public class WxCode2SessionResp {
// openId 用户唯一标识
private String openid;
// 会话密钥
private String session_key;
// 用户在开放平台的唯一标识符,若当前小程序已绑定到微信开放平台帐号下会返回,详见 UnionID 机制说明。
private String unionid;
// number 错误码
private String errcode;
// 错误信息
private String errmsg;
}
获取用户手机号接口
微信小程序在获取用户手机号时,需要在后台调用 API 接口完成
// 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;
}
}