注册
- 先判断该邮箱和昵称是否存在
- 校验邮箱(之前写过)
- 将用户信息插入UserInfo表(包含总系统空间大小(5MB)和个人系统空间大小(0MB))
登陆
参数(email,password)
- 用邮箱获取个人信息
- 比对密码,判断是否被禁用
- 重置最近登陆时间
- 新建一个SessionWebUserDto对象(昵称,用户id,是否管理员)存入session
- 将该用户个人系统空间大小存入redis
@Override
public SessionWebUserDto login(String email, String password) {
UserInfo userInfo = this.userInfoMapper.selectByEmail(email);
if (null == userInfo || !userInfo.getPassword().equals(password)) {
throw new BusinessException("账号或者密码错误");
}
if (UserStatusEnum.DISABLE.getStatus().equals(userInfo.getStatus())) {
throw new BusinessException("账号已禁用");
}
UserInfo updateInfo = new UserInfo();
updateInfo.setLastLoginTime(new Date());
this.userInfoMapper.updateByUserId(updateInfo, userInfo.getUserId());
//新建session对象
SessionWebUserDto sessionWebUserDto = new SessionWebUserDto();
sessionWebUserDto.setNickName(userInfo.getNickName());
sessionWebUserDto.setUserId(userInfo.getUserId());
if (ArrayUtils.contains(appConfig.getAdminEmails().split(","), email)) {
sessionWebUserDto.setAdmin(true);
} else {
sessionWebUserDto.setAdmin(false);
}
//用户空间
UserSpaceDto userSpaceDto = new UserSpaceDto();
userSpaceDto.setUseSpace(fileInfoService.getUserUseSpace(userInfo.getUserId()));
userSpaceDto.setTotalSpace(userInfo.getTotalSpace());
redisComponent.saveUserSpaceUse(userInfo.getUserId(), userSpaceDto);
return sessionWebUserDto;
}
找回密码(重置密码)
参数(邮箱,新密码,邮箱验证码)
- 根据邮箱查找用户是否存在
- 校验邮箱验证码
- 更新密码
获取、更新头像
获取头像:根据id获取用户头像,如果没有就返回默认头像,如果没有默认头像就返回错误信息,最后用readFile方法读取头像文件
更新头像:判断头像文件夹是否存在,不存在就创建,然后新建头像文件。
获取用户使用内存大小
用session存的用户id从redis中取
- 这里补充一下,如果session不在参数里,怎么获取session?
Spring提供了RequestContextHolder类,可以从当前线程中获取上下文
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
HttpSession session = request.getSession();
登陆拦截
利用之前的AOP拦截每个方法(除了部分比如获取验证码之类的方法),Session中看有没有user,如果没有就拦截,否则放行。
文件条件分页查询
新建fileinfo文件表(用户id,文件id,文件类型(文件或文件夹),父级id,文件md5,文件大小,名称,路径,文件种类,状态(上传中,上传失败,已上传),删除标记(未被删除,在回收站中,彻底删除),回收时间)
将各种查询条件(文件类型、userId、排列顺序、删除标记)放入FileInfoQuery中,然后查询
public ResponseVO loadDataList(HttpSession session, FileInfoQuery query, String category) {
FileCategoryEnums categoryEnum = FileCategoryEnums.getByCode(category);
if (null != categoryEnum) {
query.setFileCategory(categoryEnum.getCategory());
}
query.setUserId(getUserInfoFromSession(session).getUserId());
query.setOrderBy("last_update_time desc");
query.setDelFlag(FileDelFlagEnums.USING.getFlag());
PaginationResultVO result = fileInfoService.findListByPage(query);
return getSuccessResponseVO(convert2PaginationVO(result, FileInfoVO.class));
}
文件上传
参数:session存储的用户信息,fileId(传第一个分片的时候没有,由代码生成,传其它分片的时候使用),file,filename,fileMd5(验证文件使用),chunkIndex(第几个分片),chunks(分片数)
- 秒传逻辑:用文件的md5值(前端生成的32位字符串)检查文件是否存在,如存在直接引用
- 分片存储:文件在前端分成n个分片,后端处理放入temp文件。同时要检查用户剩余空间是否充足。事务提交后处理异步合并。最后清理临时文件。
如果是第一个分片,查询数据库里有无MD5相同的文件,如果有直接秒传:先检查空间是否充足(redis查询),将查询到的文件更新数据(userId、文件信息)插入表里。
FileInfoQuery infoQuery = new FileInfoQuery();
infoQuery.setFileMd5(fileMd5); // 根据文件 MD5 查询是否已有相同文件
infoQuery.setSimplePage(new SimplePage(0, 1)); // 分页,取第一个结果
infoQuery.setStatus(FileStatusEnums.USING.getStatus());
List<FileInfo> dbFileList = this.fileInfoMapper.selectList(infoQuery);
// 秒传逻辑:如果已存在相同的文件,直接关联而不重新上传
if (!dbFileList.isEmpty()) {
FileInfo dbFile = dbFileList.get(0);
// 检查文件大小是否超出用户剩余空间
if (dbFile.getFileSize() + spaceDto.getUseSpace() > spaceDto.getTotalSpace()) {
throw new BusinessException(ResponseCodeEnum.CODE_904); // 空间不足
}
// 更新文件信息
dbFile.setFileId(fileId);
dbFile.setFilePid(filePid);
dbFile.setUserId(webUserDto.getUserId());
dbFile.setFileMd5(fileMd5);
dbFile.setCreateTime(curDate);
dbFile.setLastUpdateTime(curDate);
dbFile.setStatus(FileStatusEnums.USING.getStatus());
dbFile.setDelFlag(FileDelFlagEnums.USING.getFlag());
dbFile.setFileName(autoRename(filePid, webUserDto.getUserId(), fileName));
this.fileInfoMapper.insert(dbFile); // 保存到数据库
resultDto.setStatus(UploadStatusEnums.UPLOAD_SECONDS.getCode()); // 秒传成功状态
updateUserSpace(webUserDto, dbFile.getFileSize()); // 更新用户空间使用
return resultDto; // 返回结果
否则将文件分片到临时文件夹里,并更新redis里临时文件大小。如果不是最后一个分片,就返回数据(fileId和上传类型(上传中))。
如果是最后一个分片,设置文件属性(将状态设为转码中)并插入数据库。更新数据库中用户使用空间。异步合并。
合并逻辑:打开目标文件的写入流,并按顺序逐个读取分片文件内容。建一个缓冲区(大小为 10KB)将分片文件内容写入目标文件。
- 为什么需要异步合并?
- 如果采用同步合并,用户需要等待所有分片合并完成后才能收到上传成功的响应,影响体验。使用异步合并后,服务端在接收到最后一个分片时即可告知用户“上传完成”,随后在后台执行合并操作。
- 同步合并会占用当前线程,导致上传接口阻塞,无法及时处理其他用户的请求。异步操作将合并任务交由后台线程池处理,可以让主线程迅速返回,保持高并发能力。
- 如何在事务提交后异步调用
注册一个事务同步对象(可以监听事务的生命周期),在事务结束后触发afterCommit方法
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
transferFile(fileInfo.getFileId(), webUserDto);
}
});
加上@Async注释标记方法异步执行
但是这样依旧没法异步执行,要交给Spring管理,使用 fileInfoService 代理对象调用这个方法
public void afterCommit() {
fileInfoService.transferFile(fileInfo.getFileId(), webUserDto);
}
所以要注入自己,@lazy懒加载只有调用的时候加载,避免循环依赖的问题。
tip:用redis读取数据减少对数据库的压力