feat: 个人信息修改

This commit is contained in:
2026-06-22 12:31:30 +08:00
parent 6d90c21b44
commit b7e8b02606
5 changed files with 172 additions and 0 deletions

View File

@@ -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/");
}
}

View File

@@ -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, "游戏不存在或已下架"),

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}
}