feat: 个人信息修改
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
package com.webgame.webgamebackend.common.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/**
|
||||
* 静态资源配置
|
||||
*
|
||||
* 将本地 uploads/ 目录映射为 /uploads/** URL,用于头像等用户上传文件的访问。
|
||||
*/
|
||||
@Configuration
|
||||
public class StaticResourceConfig implements WebMvcConfigurer {
|
||||
|
||||
/**
|
||||
* 注册静态资源处理器
|
||||
*
|
||||
* /uploads/avatars/xxx.jpg → file:./uploads/avatars/xxx.jpg
|
||||
*/
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
registry.addResourceHandler("/uploads/**")
|
||||
.addResourceLocations("file:./uploads/");
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,9 @@ public enum ErrorCode {
|
||||
|
||||
// ========== 用户模块 ==========
|
||||
USER_NOT_FOUND(2001, "用户不存在"),
|
||||
AVATAR_UPLOAD_FAILED(2002, "头像上传失败"),
|
||||
AVATAR_INVALID_TYPE(2003, "头像格式不支持,仅支持 JPG、PNG、WebP、GIF"),
|
||||
AVATAR_TOO_LARGE(2004, "头像文件大小不能超过 2MB"),
|
||||
|
||||
// ========== 游戏模块 ==========
|
||||
GAME_NOT_FOUND(3001, "游戏不存在或已下架"),
|
||||
|
||||
@@ -10,6 +10,7 @@ import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
/**
|
||||
* 用户控制器
|
||||
@@ -54,4 +55,29 @@ public class UserController {
|
||||
userService.updateStatus(accountId, request);
|
||||
return ApiResponse.ok(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传用户头像
|
||||
*
|
||||
* 接收裁剪后的图片文件(JPEG/PNG/WebP/GIF,≤2MB),保存后返回更新后的用户信息。
|
||||
* 旧头像文件会被自动删除。
|
||||
*/
|
||||
@PostMapping("/avatar")
|
||||
public ApiResponse<UserInfoResponse> uploadAvatar(@RequestParam("file") MultipartFile file) {
|
||||
String accountId = StpUtil.getLoginIdAsString();
|
||||
UserInfoResponse result = userService.uploadAvatar(accountId, file);
|
||||
return ApiResponse.ok(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户头像
|
||||
*
|
||||
* 删除磁盘文件并将 DB 中 avatar 置空,前端会回退显示默认头像。
|
||||
*/
|
||||
@DeleteMapping("/avatar")
|
||||
public ApiResponse<UserInfoResponse> deleteAvatar() {
|
||||
String accountId = StpUtil.getLoginIdAsString();
|
||||
UserInfoResponse result = userService.deleteAvatar(accountId);
|
||||
return ApiResponse.ok(result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.webgame.webgamebackend.service;
|
||||
import com.webgame.webgamebackend.common.dto.user.UpdateStatusRequest;
|
||||
import com.webgame.webgamebackend.common.dto.user.UpdateUserInfoRequest;
|
||||
import com.webgame.webgamebackend.common.dto.user.UserInfoResponse;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
/**
|
||||
* 用户服务接口
|
||||
@@ -32,4 +33,25 @@ public interface UserService {
|
||||
* @param request 状态更新请求
|
||||
*/
|
||||
void updateStatus(String accountId, UpdateStatusRequest request);
|
||||
|
||||
/**
|
||||
* 上传并更新当前登录用户的头像
|
||||
*
|
||||
* 文件会被保存到 uploads/avatars/{accountId}.jpg,并更新 DB 中的 avatar 字段。
|
||||
*
|
||||
* @param accountId 账号 ID
|
||||
* @param file 头像文件(前端已裁剪为 200×200 JPEG)
|
||||
* @return 更新后的用户信息
|
||||
*/
|
||||
UserInfoResponse uploadAvatar(String accountId, MultipartFile file);
|
||||
|
||||
/**
|
||||
* 删除当前登录用户的头像
|
||||
*
|
||||
* 删除磁盘文件,并将 DB 中 avatar 置为 null(前端会回退显示默认头像)。
|
||||
*
|
||||
* @param accountId 账号 ID
|
||||
* @return 更新后的用户信息
|
||||
*/
|
||||
UserInfoResponse deleteAvatar(String accountId);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,13 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 用户服务实现
|
||||
@@ -22,6 +29,15 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
@RequiredArgsConstructor
|
||||
public class UserServiceImpl implements UserService {
|
||||
|
||||
/** 头像存储目录(相对于项目工作目录) */
|
||||
private static final String AVATAR_DIR = "uploads/avatars";
|
||||
/** 允许的头像文件类型 */
|
||||
private static final Set<String> ALLOWED_CONTENT_TYPES = Set.of(
|
||||
"image/jpeg", "image/png", "image/webp", "image/gif"
|
||||
);
|
||||
/** 头像文件大小上限(2MB) */
|
||||
private static final long MAX_AVATAR_SIZE = 2 * 1024 * 1024;
|
||||
|
||||
private final UserRepository userRepository;
|
||||
|
||||
@Override
|
||||
@@ -74,4 +90,84 @@ public class UserServiceImpl implements UserService {
|
||||
userRepository.save(user);
|
||||
log.info("[用户服务] 更新在线状态, accountId={}, status={}", accountId, request.status());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public UserInfoResponse uploadAvatar(String accountId, MultipartFile file) {
|
||||
// 校验文件类型
|
||||
String contentType = file.getContentType();
|
||||
if (contentType == null || !ALLOWED_CONTENT_TYPES.contains(contentType)) {
|
||||
throw new BusinessException(ErrorCode.AVATAR_INVALID_TYPE);
|
||||
}
|
||||
|
||||
// 校验文件大小
|
||||
if (file.getSize() > MAX_AVATAR_SIZE) {
|
||||
throw new BusinessException(ErrorCode.AVATAR_TOO_LARGE);
|
||||
}
|
||||
|
||||
UserEntity user = userRepository.findByAccountId(accountId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));
|
||||
|
||||
try {
|
||||
// 确保目录存在
|
||||
Path avatarDir = Paths.get(AVATAR_DIR);
|
||||
Files.createDirectories(avatarDir);
|
||||
|
||||
// 删除旧头像文件(如果存在且不是默认 SVG)
|
||||
deleteOldAvatarFile(user.getAvatar());
|
||||
|
||||
// 保存新头像:uploads/avatars/{accountId}.jpg
|
||||
String filename = accountId + ".jpg";
|
||||
Path targetPath = avatarDir.resolve(filename);
|
||||
file.transferTo(targetPath.toFile());
|
||||
|
||||
// 更新数据库
|
||||
String avatarUrl = "/" + AVATAR_DIR + "/" + filename;
|
||||
user.setAvatar(avatarUrl);
|
||||
userRepository.save(user);
|
||||
|
||||
log.info("[用户服务] 头像上传成功, accountId={}, path={}", accountId, avatarUrl);
|
||||
return UserInfoResponse.from(user);
|
||||
} catch (IOException e) {
|
||||
log.error("[用户服务] 头像上传失败, accountId={}", accountId, e);
|
||||
throw new BusinessException(ErrorCode.AVATAR_UPLOAD_FAILED, "头像保存失败,请重试");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public UserInfoResponse deleteAvatar(String accountId) {
|
||||
UserEntity user = userRepository.findByAccountId(accountId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));
|
||||
|
||||
// 删除旧头像文件
|
||||
deleteOldAvatarFile(user.getAvatar());
|
||||
|
||||
// 数据库置空(前端会回退显示默认头像)
|
||||
user.setAvatar(null);
|
||||
userRepository.save(user);
|
||||
|
||||
log.info("[用户服务] 头像已删除, accountId={}", accountId);
|
||||
return UserInfoResponse.from(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除旧的用户头像文件
|
||||
*
|
||||
* 仅删除 uploads/avatars/ 下的文件,跳过默认 SVG 和非本地路径。
|
||||
*
|
||||
* @param avatarPath 头像路径,可能为 null
|
||||
*/
|
||||
private void deleteOldAvatarFile(String avatarPath) {
|
||||
if (avatarPath == null || !avatarPath.startsWith("/" + AVATAR_DIR + "/")) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Path oldFile = Paths.get(avatarPath.substring(1)); // 去掉开头的 /
|
||||
Files.deleteIfExists(oldFile);
|
||||
log.debug("[用户服务] 旧头像已删除: {}", oldFile);
|
||||
} catch (IOException e) {
|
||||
log.warn("[用户服务] 旧头像删除失败: {}", avatarPath, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user