Files
webgame-backend/src/main/java/com/webgame/webgamebackend/service/impl/UserServiceImpl.java
2026-06-24 12:17:09 +08:00

186 lines
7.0 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.webgame.webgamebackend.service.impl;
import com.webgame.webgamebackend.common.auth.AuthenticationProvider;
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 com.webgame.webgamebackend.common.exception.BusinessException;
import com.webgame.webgamebackend.common.exception.ErrorCode;
import com.webgame.webgamebackend.entities.UserEntity;
import com.webgame.webgamebackend.repository.UserRepository;
import com.webgame.webgamebackend.service.UserService;
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;
/**
* 用户服务实现
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
/** 头像存储目录(相对于项目工作目录) */
private static final String AVATAR_DIR = "uploads/avatars";
/**
* 获取头像存储的绝对路径目录
*
* 使用 user.dir 构建绝对路径,避免 Tomcat 临时工作目录导致 FileNotFoundException。
*/
private Path getAvatarDir() {
return Paths.get(System.getProperty("user.dir"), AVATAR_DIR).toAbsolutePath();
}
/** 允许的头像文件类型 */
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;
private final AuthenticationProvider authProvider;
@Override
public UserInfoResponse getCurrentUserInfo() {
String accountId = authProvider.getCurrentUserId();
log.info("[用户服务] 获取当前登录用户信息, accountId: {}", accountId);
UserEntity user = userRepository.findByAccountId(accountId)
.orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));
return UserInfoResponse.from(user);
}
@Override
@Transactional
public UserInfoResponse updateUserInfo(String accountId, UpdateUserInfoRequest request) {
UserEntity user = userRepository.findByAccountId(accountId)
.orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));
// 仅更新非 null 字段
if (request.nickName() != null) {
user.setNickName(request.nickName());
}
if (request.avatar() != null) {
user.setAvatar(request.avatar());
}
if (request.signature() != null) {
user.setSignature(request.signature());
}
if (request.age() != null) {
user.setAge(request.age());
}
if (request.sex() != null) {
user.setSex(request.sex());
}
if (request.birthday() != null) {
user.setBirthday(request.birthday());
}
userRepository.save(user);
log.info("[用户服务] 更新用户信息, accountId={}", accountId);
return UserInfoResponse.from(user);
}
@Override
@Transactional
public void updateStatus(String accountId, UpdateStatusRequest request) {
UserEntity user = userRepository.findByAccountId(accountId)
.orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));
user.setStatus(request.status());
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 {
// 确保目录存在(使用绝对路径,避免 Tomcat 临时目录问题)
Path avatarDir = getAvatarDir();
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 {
// 从 URL 路径中提取文件名,使用绝对路径删除
String filename = avatarPath.substring(avatarPath.lastIndexOf('/') + 1);
Path oldFile = getAvatarDir().resolve(filename);
Files.deleteIfExists(oldFile);
log.debug("[用户服务] 旧头像已删除: {}", oldFile);
} catch (IOException e) {
log.warn("[用户服务] 旧头像删除失败: {}", avatarPath, e);
}
}
}