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