feat: change the avatar upload controller to separate

This commit is contained in:
wzp 2024-12-25 19:26:58 +08:00
parent b5c32288f7
commit 6c286868f6
8 changed files with 161 additions and 89 deletions

View File

@ -0,0 +1,33 @@
package org.blue.club.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.blue.club.annotation.Auth;
import org.blue.club.services.AvatarServices;
import org.mmga.spring.boot.starter.entities.Result;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@RestController
@RequestMapping("/api/avatar")
@Tag(name = "头像相关接口")
@RequiredArgsConstructor
public class AvatarController {
private final AvatarServices avatarServices;
@GetMapping("/{sha1}")
@Operation(description = "获取头像文件")
public void getAvatar(HttpServletResponse response, @Schema(description = "头像文件SHA1值") @PathVariable("sha1") String sha1) {
avatarServices.getAvatar(response, sha1);
}
@PostMapping("/upload")
@Operation(description = "上传头像")
@Auth
public Result<String> changeAvatar(MultipartFile file) {
return avatarServices.changeAvatar(file);
}
}

View File

@ -54,6 +54,7 @@ public class ClubController {
public Result<Boolean> clubRemoveUser(@Auth(permissions = {@PermissionDescription(3), @PermissionDescription(value = 2, type = DescriptionType.CLUB)}) User user, @Schema(description = "社团ID") @RequestParam long clubId, @Schema(description = "需要删除的用户ID") @RequestParam long userId) {
return clubServices.clubRemoveUser(user, clubId, userId);
}
@Operation(description = "修改社团用户权限")
@PutMapping("/user/auth")
public Result<Boolean> clubChangeUserAuth(@Auth(permissions = {@PermissionDescription(3), @PermissionDescription(value = 2, type = DescriptionType.CLUB)}) User user, @RequestBody ClubChangeUserAuthRequest request) {

View File

@ -0,0 +1,16 @@
package org.blue.club.controller;
import org.blue.club.exceptions.ServerException;
import org.mmga.spring.boot.starter.entities.Result;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
@ControllerAdvice
public class ServerExceptionHandler {
@ExceptionHandler({ServerException.class})
public ResponseEntity<Result<Void>> handleAuthorizationException(ServerException e) {
Result<Void> result = e.getResult();
return ResponseEntity.ok(result);
}
}

View File

@ -4,7 +4,6 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.blue.club.annotation.Auth;
@ -17,7 +16,6 @@ import org.mmga.spring.boot.starter.annotation.AuthMapping;
import org.mmga.spring.boot.starter.entities.PagerData;
import org.mmga.spring.boot.starter.entities.Result;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@Slf4j
@RestController
@ -74,16 +72,9 @@ public class UserController {
return userServices.changeAuth(userChangeAuthVo);
}
@PostMapping("/avatar")
@Operation(description = "上传头像")
@Auth
public Result<String> changeAvatar(MultipartFile file) {
return userServices.changeAvatar(file);
}
@PutMapping("/avatar")
@Operation(description = "修改用户头像")
public Result<Boolean> changeAvatar(@RequestParam("code") @Schema(description = "修改头像操作码,可以通过将图片文件上传至([POST] /avatar)接口获取") String avatarOperationCode, @Auth User user) {
public Result<Boolean> changeAvatar(@RequestParam("code") @Schema(description = "修改头像操作码,可以通过将图片文件上传至([POST] /api/avatar/upload)接口获取") String avatarOperationCode, @Auth User user) {
return userServices.changeAvatar(avatarOperationCode, user);
}
@ -93,12 +84,6 @@ public class UserController {
return userServices.getVerifyCode();
}
@GetMapping("/avatar/{sha1}")
@Operation(description = "获取头像文件")
public void getAvatar(HttpServletResponse response, @Schema(description = "头像文件SHA1值") @PathVariable("sha1") String sha1) {
userServices.getAvatar(response, sha1);
}
@GetMapping("/info/{id}")
@Operation(description = "获取简略用户信息")
public Result<User> getSimpleInfo(@PathVariable("id") int userId) {

View File

@ -13,4 +13,5 @@ public class ClubVo extends BaseDataVo {
private Long id;
private String name;
private String commit;
private String avatar;
}

View File

@ -0,0 +1,14 @@
package org.blue.club.exceptions;
import lombok.Getter;
import org.mmga.spring.boot.starter.entities.Result;
@Getter
public class ServerException extends RuntimeException {
private final Result<Void> result;
public ServerException(Result<Void> result) {
super(result.getMsg());
this.result = result;
}
}

View File

@ -0,0 +1,91 @@
package org.blue.club.services;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.tomcat.util.buf.HexUtils;
import org.blue.club.dao.redis.AvatarOperationDao;
import org.blue.club.entities.vo.data.AvatarTmpVo;
import org.blue.club.exceptions.ServerException;
import org.blue.club.utils.iop.FileUtils;
import org.mmga.spring.boot.starter.entities.Result;
import org.mmga.spring.boot.starter.utils.RandomUtils;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.util.StreamUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.util.Date;
import java.util.Optional;
@Service
@RequiredArgsConstructor
public class AvatarServices {
private final RandomUtils randomUtils;
private final File tmpFolder;
private final File avatarFolder;
private final AvatarOperationDao avatarOperationDao;
private final FileUtils fileUtils;
@SneakyThrows
public Result<String> changeAvatar(MultipartFile file) {
String s = randomUtils.generatorRandomFileName(32);
DigestInputStream digestInputStream = new DigestInputStream(file.getInputStream(), DigestUtils.getSha1Digest());
File tmpFile = new File(tmpFolder, s);
try (FileOutputStream fos = new FileOutputStream(tmpFile)) {
digestInputStream.transferTo(fos);
}
MessageDigest messageDigest = digestInputStream.getMessageDigest();
String sha1Hex = HexUtils.toHexString(messageDigest.digest());
avatarOperationDao.save(new AvatarTmpVo(s, sha1Hex, new Date()));
return Result.success("上传成功", s);
}
@SneakyThrows
public void getAvatar(HttpServletResponse response, String sha1) {
File targetFile = new File(avatarFolder, sha1);
if (!targetFile.getAbsolutePath().startsWith(avatarFolder.getAbsolutePath())) {
Result.failed(HttpStatus.NOT_FOUND, "文件不存在!").writeToResponse(response);
return;
}
if (!targetFile.exists()) {
Result.failed(HttpStatus.NOT_FOUND, "文件不存在!").writeToResponse(response);
return;
}
try (FileInputStream fileInputStream = new FileInputStream(targetFile)) {
response.setContentType("image/png");
response.setHeader("Content-Disposition", ContentDisposition.attachment().filename(sha1 + ".png").build().toString());
ServletOutputStream outputStream = response.getOutputStream();
StreamUtils.copy(fileInputStream, outputStream);
}
}
public Optional<String> permanentSaveAvatar(String avatarOperationCode) {
Optional<AvatarTmpVo> byId = avatarOperationDao.findById(avatarOperationCode);
if (byId.isEmpty()) return Optional.empty();
AvatarTmpVo avatarTmpVo = byId.get();
File tmpFile = new File(tmpFolder, avatarOperationCode);
String sha1 = avatarTmpVo.sha1();
File targetFile = new File(avatarFolder, sha1);
changeAvatarFile(tmpFile, targetFile);
avatarOperationDao.deleteById(avatarOperationCode);
return Optional.of(sha1);
}
public void changeAvatarFile(File tmpFile, File targetFile) {
if (targetFile.exists()) {
fileUtils.tryDeleteOrDeleteOnExit(tmpFile);
return;
}
if (!tmpFile.renameTo(targetFile))
throw new ServerException(Result.failed(HttpStatus.INTERNAL_SERVER_ERROR, "移动头像文件失败!请联系管理员处理"));
}
}

View File

@ -1,44 +1,26 @@
package org.blue.club.services;
import com.mybatisflex.core.paginate.Page;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.tomcat.util.buf.HexUtils;
import org.blue.club.dao.UserDao;
import org.blue.club.dao.redis.AvatarOperationDao;
import org.blue.club.dao.redis.VerifyDao;
import org.blue.club.entities.dto.user.User;
import org.blue.club.entities.dto.user.req.*;
import org.blue.club.entities.dto.user.resp.VerifyCodeResponse;
import org.blue.club.entities.vo.VerifyVo;
import org.blue.club.entities.vo.data.AvatarTmpVo;
import org.blue.club.entities.vo.data.UserVo;
import org.blue.club.utils.iop.FileUtils;
import org.blue.club.utils.iop.VerifyCodeUtils;
import org.mmga.spring.boot.starter.entities.PagerData;
import org.mmga.spring.boot.starter.entities.Result;
import org.mmga.spring.boot.starter.exception.AuthorizationException;
import org.mmga.spring.boot.starter.properties.Env;
import org.mmga.spring.boot.starter.properties.MainProperties;
import org.mmga.spring.boot.starter.utils.RandomUtils;
import org.mmga.spring.boot.starter.utils.VoUtils;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StreamUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.util.Date;
import java.util.List;
import java.util.Optional;
@ -52,13 +34,9 @@ public class UserServices {
private final UserDao userDao;
private final VerifyDao verifyDao;
private final VoUtils voUtils;
private final RandomUtils randomUtils;
private final File tmpFolder;
private final File avatarFolder;
private final AvatarOperationDao avatarOperationDao;
private final FileUtils fileUtils;
private final VerifyCodeUtils verifyCodeUtils;
private final MainProperties mainProperties;
private final AvatarServices avatarServices;
private boolean isWrongVerifyCode(String key, String code) {
if (mainProperties.getEnv().equals(Env.DEV)) return false;
@ -151,70 +129,23 @@ public class UserServices {
return Result.success(true);
}
@SneakyThrows
public Result<String> changeAvatar(MultipartFile file) {
String s = randomUtils.generatorRandomFileName(32);
DigestInputStream digestInputStream = new DigestInputStream(file.getInputStream(), DigestUtils.getSha1Digest());
File tmpFile = new File(tmpFolder, s);
try (FileOutputStream fos = new FileOutputStream(tmpFile)) {
digestInputStream.transferTo(fos);
}
MessageDigest messageDigest = digestInputStream.getMessageDigest();
String sha1Hex = HexUtils.toHexString(messageDigest.digest());
avatarOperationDao.save(new AvatarTmpVo(s, sha1Hex, new Date()));
return Result.success("上传成功", s);
}
public Result<Boolean> changeAvatar(String avatarOperationCode, User user) {
Optional<AvatarTmpVo> byId = avatarOperationDao.findById(avatarOperationCode);
if (byId.isEmpty()) return Result.failed(HttpStatus.NOT_FOUND, "操作码错误或过期");
AvatarTmpVo avatarTmpVo = byId.get();
File tmpFile = new File(tmpFolder, avatarOperationCode);
String sha1 = avatarTmpVo.sha1();
File targetFile = new File(avatarFolder, sha1);
Optional<Result<Boolean>> booleanResult = changeAvatarFile(tmpFile, targetFile);
if (booleanResult.isPresent()) return booleanResult.get();
avatarOperationDao.deleteById(avatarOperationCode);
Optional<String> result = avatarServices.permanentSaveAvatar(avatarOperationCode);
if (result.isEmpty()) return Result.failed(HttpStatus.NOT_FOUND, "操作码错误或过期");
UserVo userVo = new UserVo();
userVo.setId(user.getId());
userVo.setAvatar(sha1);
userVo.setAvatar(result.get());
userDao.update(userVo);
return Result.success(true);
}
public Optional<Result<Boolean>> changeAvatarFile(File tmpFile, File targetFile) {
if (targetFile.exists()) {
fileUtils.tryDeleteOrDeleteOnExit(tmpFile);
return Optional.empty();
}
if (!tmpFile.renameTo(targetFile))
return Optional.of(Result.failed(HttpStatus.INTERNAL_SERVER_ERROR, "移动头像文件失败!请联系管理员处理"));
return Optional.empty();
}
public Result<VerifyCodeResponse> getVerifyCode() {
VerifyCodeResponse verifyCodeResponse = verifyCodeUtils.generateVerifyCode();
return Result.success(verifyCodeResponse);
}
@SneakyThrows
public void getAvatar(HttpServletResponse response, String sha1) {
File targetFile = new File(avatarFolder, sha1);
if (!targetFile.getAbsolutePath().startsWith(avatarFolder.getAbsolutePath())) {
Result.failed(HttpStatus.NOT_FOUND, "文件不存在!").writeToResponse(response);
return;
}
if (!targetFile.exists()) {
Result.failed(HttpStatus.NOT_FOUND, "文件不存在!").writeToResponse(response);
return;
}
try (FileInputStream fileInputStream = new FileInputStream(targetFile)) {
response.setContentType("image/png");
response.setHeader("Content-Disposition", ContentDisposition.attachment().filename(sha1 + ".png").build().toString());
ServletOutputStream outputStream = response.getOutputStream();
StreamUtils.copy(fileInputStream, outputStream);
}
}
public Result<User> getUserInfo(int userId) {
UserVo userVo = userDao.selectOneWithRelationsById(userId);