一、事情起因
【油耗笔记OilNote】小程序好久没有升级了,最近打算对代码进行一些优化,但是新版本突然发现无法获取到用户微信头像及微信昵称了。
查阅官方文档才知道,官方有对getUserProfile
接口进行调整了。
自 2022 年 10 月 25 日 24 时后(以下统称 “生效期” ),用户头像昵称获取规则将进行如下调整:
- 自生效期起,小程序 wx.getUserProfile 接口将被收回:生效期后发布的小程序新版本,通过 wx.getUserProfile 接口获取用户头像将统一返回默认灰色头像,昵称将统一返回 “微信用户”。生效期前发布的小程序版本不受影响,但如果要进行版本更新则需要进行适配。
- 自生效期起,插件通过 wx.getUserInfo 接口获取用户昵称头像将被收回:生效期后发布的插件新版本,通过 wx.getUserInfo 接口获取用户头像将统一返回默认灰色头像,昵称将统一返回 “微信用户”。生效期前发布的插件版本不受影响,但如果要进行版本更新则需要进行适配。通过 wx.login 与 wx.getUserInfo 接口获取 openId、unionId 能力不受影响。
- 「头像昵称填写能力」支持获取用户头像昵称:如业务需获取用户头像昵称,可以使用「头像昵称填写能力」(基础库 2.21.2 版本开始支持,覆盖iOS与安卓微信 8.0.16 以上版本),具体实践可见下方《最佳实践》。
- 小程序 wx.getUserProfile 与插件 wx.getUserInfo 接口兼容基础库 2.27.1 以下版本的头像昵称获取需求:对于来自低版本的基础库与微信客户端的访问,小程序通过 wx.getUserProfile 接口将正常返回用户头像昵称,插件通过 wx.getUserInfo 接口将正常返回用户头像昵称,开发者可继续使用以上能力做向下兼容。
现在只要是发布的新版本,默认都需要调整,不然就显示下面灰色头像,已经发布的版本不受影响。
既然官方调整了,那么我们也只有被动接受的份。
二、油耗笔记的开发框架
油耗笔记OilNote不是直接使用微信开发者工具开发的,而是使用UniApp开发的,后端是SpringBoot。
三、改进思路
3.1、获取用户头像
由于官方指导意见是,使用button
组件 open-type
的值设置为 chooseAvatar
,当用户选择需要使用的头像之后,可以通过 bindchooseavatar
事件回调获取到头像信息的临时路径。
从官方的指导我们可以看到,微信并没有给我们返回一个具体的路径,只是返回了一个临时的路径,因此我们就必须自己获取到这个临时的文件,然后存储起来。
3.2、使用七牛云
由于使用的腾讯云的低配服务器,带宽、存储都比较捉襟见肘,所以我打算把头像都存储到七牛云上,既能减轻带宽压力也能节省服务器空间。
四、具体改进
4.1、UniApp页面改进
当用户通过微信登录时,此时获取用户头像信息,如果头像存在,登录之后跳转到首页,否则跳转到个人信息界面,让用户维护头像及昵称,此方法适用于新用户,同时也适用于老用户重新登录。
//微信授权登录
getUserInfo(e) {
let that = this;
var p = this.getSetting();
p.then(function(isAuth) {
console.log('是否已经授权', isAuth);
if (isAuth) {
console.log('用户信息,加密数据', e);
//eData 包括//微信头像//微信名称 还有加密的数据.
// let eData = JSON.parse(e.detail.rawData);
uni.getUserProfile({
desc: 'Wexin', // 这个参数是必须的
success: function(infoRes) {
//接下来就是访问接口.
that.$request(
'wechat/authCode2Session?code=' + that.weChatCode, 'POST'
).then(function(res) {
if (res.code == 200) {
//将接口返回的数据保存在全局变量中.
let userInfo = {}
// 用户id
userInfo.id = res.data.id
userInfo.username = res.data.username
userInfo.tel = res.data.tel
userInfo.email = res.data.email
userInfo.wechatOpenId = res.data.wechatOpenId
userInfo.nickName = res.data.nickName
userInfo.avatarUrl = res.data.avatarUrl ? res.data
.avatarUrl : infoRes.userInfo.avatarUrl
userInfo.gender = infoRes.userInfo.gender
userInfo.password = ''
if (!userInfo.province) {
userInfo.province = uni.getStorageSync('province')
}
if (!userInfo.city) {
userInfo.city = uni.getStorageSync('city')
}
uni.setStorageSync('userInfo', userInfo);
if (!res.data.avatarUrl) { //没有头像时,跳转到用户信息维护界面
uni.redirectTo({
url: '/pages/profile/profile'
})
} else {
uni.redirectTo({
url: '/pages/index/index'
})
}
}
},
function(err) {
uni.showToast({
title: '授权登录失败!',
mask: true,
icon: 'none'
})
}
)
}
});
} else {
uni.showToast({
title: '授权失败,请确认授权已开启',
mask: true,
icon: 'none'
})
}
});
},
4.1.1、登录界面改造
4.1.2、个人信息界面改造
油耗笔记之前有一个【个人信息】页面,因此我打算把修改头像的功能发放到里面。
在合适的位置放入选择头像的按钮
<button class="avatar-wrapper" open-type="chooseAvatar" @chooseavatar="onChooseAvatar">
<view class="cu-avatar xl round margin-center" :style="{backgroundImage:'url('+userInfo.avatarUrl+')'}"></view>
</button>
增加回调方法
用户选择微信头像之后,会回调chooseavatar
方法,因此我们增加一个chooseavatar
用于用户选择头像之后上传到服务器(进一步上传到七牛)
//选择头像回调
onChooseAvatar(e) {
const that = this;
this.$set(this.userInfo, "avatarUrl", e.detail.avatarUrl);
uni.uploadFile({
url: operate.api + 'user/uploadAvatar/', //上传接口
header: {
token: that.userInfo.id ? that.userInfo.id : '',
},
formData: {
'userInfo': JSON.stringify(that.userInfo)
},
filePath: e.detail.avatarUrl,
name: 'file',
success: (uploadFileRes) => {
uni.hideLoading();
const back = JSON.parse(uploadFileRes.data);
if (back.code == 200) {
that.$set(that.userInfo, 'avatarUrl', back.data.avatarUrl)
} else {
uni.showToast(back.msg)
}
},
fail: (error) => {
uni.hideLoading();
uni.showToast("图片上传失败,请联系开发!")
},
complete: function() {
uni.hideLoading();
}
});
}
4.2、后端改造
我们首先在后端增加一个方法,用于接受前端传递的附件及其他参数(我这里主要传递的是用户信息,用户更新用户表,记录头像地址)。
4.2.1、Controller
增加接受用户上传头像的Api,具体的实现我们稍后在说。
/**
* 上传头像
*
* @param multipartFile 文件信息
* @return 用户信息
*/
@PostMapping("uploadAvatar")
public AjaxResult uploadAvatar(@RequestParam("file") MultipartFile multipartFile,@RequestParam("userInfo") String oilUser) {
return AjaxResult.success(oilUserService.uploadAvatar(multipartFile));
}
4.2.2、增加七牛云依赖
经过上面改造,我们已经可以将用户头像上传到我们的后端了,接下来的任务就是将头像上传到我们的七牛云了。
现在pom.xml
中增加七牛云的依赖
<!-- 七牛云-->
<dependency>
<groupId>com.qiniu</groupId>
<artifactId>qiniu-java-sdk</artifactId>
<version>7.2.28</version>
</dependency>
4.2.3、增加七牛云配置
为了方便使用,我们将七牛云的一些配置信息放入yaml
文件中,方便维护。
# ========================== ↓↓↓↓↓↓ 七牛云配置 ↓↓↓↓↓↓ ==========================
qiniu:
accessKey: XXX # Key
secretKey: XXX # 密钥
bucket: XXX # 空间名称
domain: XXX # 访问域名
dir: XXX/ # 目录
参数说明:
- accessKey:AK,在七牛云,个人中心,密钥管理中可以看到
- secretKey:SK,在七牛云,个人中心,密钥管理中可以看到
- bucket:空间名称,根据自己创建的空间填写
- domain:访问域名,根据控件绑定的域名实际填写
- dir:存储路径,因为七牛云默认是直接存储到根目录,为了方便管理,我们可以创建子目录,比如
avatar
,可以填写avatar/
4.2.4、增加配置类
为了方便使用,我们将yaml
的值,映射到配置类上。
/**
* 七牛云实体
*/
@Component
@ConfigurationProperties(prefix = "qiniu")
public class QiNiuConfig {
/**
* Key
*/
private static String accessKey;
/**
* 密钥
*/
private static String secretKey;
/**
* 空间名称
*/
private static String bucket;
/**
* 访问域名
*/
private static String domain;
/**
* 目录
*/
private static String dir;
public static String getAccessKey() {
return accessKey;
}
public void setAccessKey(String accessKey) {
QiNiuConfig.accessKey = accessKey;
}
public static String getSecretKey() {
return secretKey;
}
public void setSecretKey(String secretKey) {
QiNiuConfig.secretKey = secretKey;
}
public static String getBucket() {
return bucket;
}
public void setBucket(String bucket) {
QiNiuConfig.bucket = bucket;
}
public static String getDomain() {
return domain;
}
public void setDomain(String domain) {
QiNiuConfig.domain = domain;
}
public static String getDir() {
return dir;
}
public void setDir(String dir) {
QiNiuConfig.dir = dir;
}
}
4.2.5、封装公共方法
为了方便调用,我们将上传、删除等方法封装到单独的服务中。
接口
/**
* 七牛接口
*/
public interface IQiNiuService {
/**
* 以文件的形式上传
*
* @param file
* @param fileName:
* @return: java.lang.String
*/
String uploadFile(File file, String fileName) throws QiniuException;
/**
* 以流的形式上传
*
* @param inputStream
* @param fileName:
* @return: java.lang.String
*/
String uploadFile(InputStream inputStream, String fileName) throws QiniuException;
/**
* 删除文件
*
* @param key:
* @return: java.lang.String
*/
String delete(String key) throws QiniuException;
}
实现
@Service
public class QiNiuServiceImpl implements IQiNiuService, InitializingBean {
// 七牛文件上传管理器
private final Configuration cfg;
private final Auth auth;
public QiNiuServiceImpl() {
// //构造一个带指定 Region 对象的配置类
cfg = new Configuration(Region.huadong());
auth = Auth.create(QiNiuConfig.getAccessKey(), QiNiuConfig.getSecretKey());
}
/**
* 定义七牛云上传的相关策略
*/
private StringMap putPolicy;
@Override
public String uploadFile(File file, String fileName) throws QiniuException {
if (!StringUtils.isEmpty(QiNiuConfig.getDir())) {
fileName = QiNiuConfig.getDir() + fileName;
}
UploadManager uploadManager = new UploadManager(cfg);
Response response = uploadManager.put(file, fileName, getUploadToken());
int retry = 0;
while (response.needRetry() && retry < 3) {
response = uploadManager.put(file, fileName, getUploadToken());
retry++;
}
if (response.statusCode == 200) {
return "http://" + QiNiuConfig.getDomain() + "/" + fileName;
}
return "上传失败!";
}
@Override
public String uploadFile(InputStream inputStream, String fileName) throws QiniuException {
if (!StringUtils.isEmpty(QiNiuConfig.getDir())) {
fileName = QiNiuConfig.getDir() + fileName;
}
UploadManager uploadManager = new UploadManager(cfg);
Response response = uploadManager.put(inputStream, fileName, getUploadToken(), null, null);
int retry = 0;
while (response.needRetry() && retry < 3) {
response = uploadManager.put(inputStream, fileName, getUploadToken(), null, null);
retry++;
}
if (response.statusCode == 200) {
return "http://" + QiNiuConfig.getDomain() + "/" + fileName;
}
return "上传失败!";
}
@Override
public String delete(String key) throws QiniuException {
BucketManager bucketManager = new BucketManager(auth, cfg);
Response response = bucketManager.delete(QiNiuConfig.getBucket(), key);
int retry = 0;
while (response.needRetry() && retry++ < 3) {
response = bucketManager.delete(QiNiuConfig.getBucket(), key);
}
return response.statusCode == 200 ? "删除成功!" : "删除失败!";
}
@Override
public void afterPropertiesSet() throws Exception {
this.putPolicy = new StringMap();
putPolicy.put("insertOnly", 0);
}
/**
* 获取上传凭证
*/
private String getUploadToken() {
return this.auth.uploadToken(QiNiuConfig.getBucket(), null, 3600, putPolicy);
}
}
有几个需要注意的点:
- 在构造函数中,构造
Configuration
时,需要指定区域,因为我是华东区域
的,因此使用的是Region.huadong()
,如果使用的其他区域的,需要根据自己实际区域指定。 - 在指定策略时,因为我一个用户只允许一个头像,因此上传时,如果存在我们直接覆盖的,所以在
afterPropertiesSet
方法中,设置上传策略时,直接指定的putPolicy.put("insertOnly", 0);
,即如果存在就覆盖,如果不想覆盖,可以设置putPolicy.put("insertOnly", 1);
,但是此时需要注意,如果上传重名文件,会返回异常。
4.2.6、完善后用户上传头像方法
用户上传头像后,更新用户实体(但是此时不更新数据库),将更新后的实体返回到前端,点击保存时,再更新数据库。
@Override
public OilUser uploadAvatar(MultipartFile multipartFile, OilUser oilUser) throws IOException {
String originalFilename = multipartFile.getOriginalFilename();
if (originalFilename == null || !originalFilename.contains(".")) {
throw new CustomException("文件名不正确");
}
String fileName = "avatar" + oilUser.getId() + originalFilename.substring(originalFilename.lastIndexOf("."));
String avatarUrl = qiNiuService.uploadFile(multipartFile.getInputStream(), fileName);
oilUser.setAvatarUrl(avatarUrl);
// LambdaUpdateWrapper<OilUser> userUpdateWrapper = new LambdaUpdateWrapper<>();
// userUpdateWrapper.set(OilUser::getAvatarUrl, oilUser.getAvatarUrl());
// userUpdateWrapper.eq(OilUser::getId, oilUser.getId());
// oilUserMapper.update(null, userUpdateWrapper);
return oilUser;
}
4.2.7、用户保存方法分改造
用户保存方法主要增加userUpdateWrapper.set(OilUser::getAvatarUrl, user.getAvatarUrl());
,当用户有头像时,同步更新用户的头像信息。
/**
* 新增或保存用户
*
* @param user 用户
* @return 结果
*/
public OilUser saveUser(OilUser user) {
if (user == null) {
throw new CustomException("用户信息不能为空");
}
if (user.getId() == null) {
user.setId("");
}
if (checkUserNameExist(user)) {
throw new CustomException("用户名已存在");
}
if (StringUtils.isEmpty(user.getId())) {
user.setId(UUID.randomUUID().toString());
if (!StringUtils.isEmpty(user.getPassword())) {
user.setPassword(passwordEncoder.encode(user.getPassword()));
}
oilUserMapper.insert(user);
} else {
LambdaUpdateWrapper<OilUser> userUpdateWrapper = new LambdaUpdateWrapper<>();
userUpdateWrapper.set(OilUser::getUsername, user.getUsername());
userUpdateWrapper.set(OilUser::getNickName, user.getNickName());
userUpdateWrapper.set(OilUser::getTel, user.getTel());
userUpdateWrapper.set(OilUser::getEmail, user.getEmail());
if (!StringUtils.isEmpty(user.getPassword())) {
userUpdateWrapper.set(OilUser::getPassword, passwordEncoder.encode(user.getPassword()));
}
if (!StringUtils.isEmpty(user.getProvince())) {
userUpdateWrapper.set(OilUser::getProvince, user.getProvince());
}
if (!StringUtils.isEmpty(user.getCity())) {
userUpdateWrapper.set(OilUser::getCity, user.getCity());
}
if (!StringUtils.isEmpty(user.getAvatarUrl())) {
userUpdateWrapper.set(OilUser::getAvatarUrl, user.getAvatarUrl());
}
userUpdateWrapper.eq(OilUser::getId, user.getId());
oilUserMapper.update(null, userUpdateWrapper);
user.setPassword("");
}
return user;
}
五、效果
微信用户登录后,如果没有上传过头像,会自动跳转到【个人信息】页面
在个人信息上传头像后,自动跳转到首页。
六、其他注意事项
- 七牛云域名需要配置HTTPS
- 小程序域名白名单
uploadFile合法域名
需要配置后台上传附件的域名。
评论 (0)