From 73eb1198f69c2f85cbfea6fb7d07c35d8ed2d6b9 Mon Sep 17 00:00:00 2001 From: LingandRX Date: Tue, 25 Mar 2025 22:04:19 +0800 Subject: [PATCH] =?UTF-8?q?feat(user):=20=E6=B7=BB=E5=8A=A0=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E7=9B=B8=E5=85=B3=E5=8A=9F=E8=83=BD-=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E7=94=A8=E6=88=B7=E4=BF=A1=E6=81=AF=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E5=8A=9F=E8=83=BD-=20=E6=96=B0=E5=A2=9E=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E5=A4=B4=E5=83=8F=E4=B8=8A=E4=BC=A0=E5=8A=9F=E8=83=BD=20-=20?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E7=94=A8=E6=88=B7=E5=88=97=E8=A1=A8=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E5=8A=9F=E8=83=BD=20-=20=E5=AE=9E=E7=8E=B0=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E4=BF=A1=E6=81=AF=E8=8E=B7=E5=8F=96=E5=92=8C=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E5=9C=B0=E5=9B=BE=E8=8E=B7=E5=8F=96=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=20-=20=E6=B7=BB=E5=8A=A0=E6=96=87=E4=BB=B6=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../copykamanotes/annotation/NeedLogin.java | 9 ++ .../controller/UserController.java | 29 +++++ .../copykamanotes/mapper/UserMapper.java | 11 ++ .../model/base/PaginationApiResponse.java | 1 - .../copykamanotes/service/FileService.java | 21 ++++ .../copykamanotes/service/UserService.java | 8 ++ .../service/impl/LocalFileServiceImpl.java | 102 ++++++++++++++++++ .../service/impl/UserServiceImpl.java | 73 ++++++++++++- .../copykamanotes/utils/PaginationUtil.java | 2 +- src/main/resources/application.properties | 4 + src/main/resources/mapper/UserMapper.xml | 53 +++++++++ 11 files changed, 307 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/example/copykamanotes/annotation/NeedLogin.java create mode 100644 src/main/java/com/example/copykamanotes/service/FileService.java create mode 100644 src/main/java/com/example/copykamanotes/service/impl/LocalFileServiceImpl.java diff --git a/src/main/java/com/example/copykamanotes/annotation/NeedLogin.java b/src/main/java/com/example/copykamanotes/annotation/NeedLogin.java new file mode 100644 index 0000000..e1b8737 --- /dev/null +++ b/src/main/java/com/example/copykamanotes/annotation/NeedLogin.java @@ -0,0 +1,9 @@ +package com.example.copykamanotes.annotation; + +import java.lang.annotation.*; + +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface NeedLogin { +} diff --git a/src/main/java/com/example/copykamanotes/controller/UserController.java b/src/main/java/com/example/copykamanotes/controller/UserController.java index 60fe4db..5a5f3e2 100644 --- a/src/main/java/com/example/copykamanotes/controller/UserController.java +++ b/src/main/java/com/example/copykamanotes/controller/UserController.java @@ -3,14 +3,20 @@ package com.example.copykamanotes.controller; import com.example.copykamanotes.model.base.ApiResponse; import com.example.copykamanotes.model.dto.user.LoginRequest; import com.example.copykamanotes.model.dto.user.RegisterRequest; +import com.example.copykamanotes.model.dto.user.UpdateUserRequest; +import com.example.copykamanotes.model.dto.user.UserQueryParam; +import com.example.copykamanotes.model.entity.User; +import com.example.copykamanotes.model.vo.user.AvatarVO; import com.example.copykamanotes.model.vo.user.LoginUserVO; import com.example.copykamanotes.model.vo.user.RegisterVO; import com.example.copykamanotes.service.UserService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; import javax.validation.Valid; +import java.util.List; @Slf4j @RestController @@ -47,4 +53,27 @@ public class UserController { public ApiResponse whoami() { return userService.whoami(); } + + @PostMapping("/users/me") + public ApiResponse updateUserInfo( + @Valid + @RequestBody + UpdateUserRequest updateUserRequest + ) { + return userService.updateUserInfo(updateUserRequest); + } + + @PostMapping("/users/avatar") + public ApiResponse uploadAvatar( + @RequestParam("file") MultipartFile file + ) { + return userService.uploadAvatar(file); + } + + @GetMapping("/admin/users") + public ApiResponse> adminGetUser( + @Valid UserQueryParam userQueryParam + ) { + return userService.getUserList(userQueryParam); + } } diff --git a/src/main/java/com/example/copykamanotes/mapper/UserMapper.java b/src/main/java/com/example/copykamanotes/mapper/UserMapper.java index cf6de15..abbf04a 100644 --- a/src/main/java/com/example/copykamanotes/mapper/UserMapper.java +++ b/src/main/java/com/example/copykamanotes/mapper/UserMapper.java @@ -1,9 +1,12 @@ package com.example.copykamanotes.mapper; +import com.example.copykamanotes.model.dto.user.UserQueryParam; import com.example.copykamanotes.model.entity.User; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; +import java.util.List; + @Mapper public interface UserMapper { /** @@ -20,4 +23,12 @@ public interface UserMapper { int updateLastLoginAt(@Param("userId") Long userId); User findById(@Param("userId") Long userId); + + int update(User user); + + List findByIds(@Param("userIds") List userIds); + + int countByQueryParam(@Param("queryParams") UserQueryParam userQueryParam); + + List findByQueryParam(@Param("queryParams") UserQueryParam userQueryParam, @Param("limit") int pageSize, @Param("offset") int offset); } diff --git a/src/main/java/com/example/copykamanotes/model/base/PaginationApiResponse.java b/src/main/java/com/example/copykamanotes/model/base/PaginationApiResponse.java index d5dd499..db188e8 100644 --- a/src/main/java/com/example/copykamanotes/model/base/PaginationApiResponse.java +++ b/src/main/java/com/example/copykamanotes/model/base/PaginationApiResponse.java @@ -4,7 +4,6 @@ package com.example.copykamanotes.model.base; * PaginationApiResponse类用于处理分页的API响应。 * @param 泛型类型,表示分页数据类型。 */ - public class PaginationApiResponse extends ApiResponse { // 分页对象 private final Pagination pagination; diff --git a/src/main/java/com/example/copykamanotes/service/FileService.java b/src/main/java/com/example/copykamanotes/service/FileService.java new file mode 100644 index 0000000..bbd3a53 --- /dev/null +++ b/src/main/java/com/example/copykamanotes/service/FileService.java @@ -0,0 +1,21 @@ +package com.example.copykamanotes.service; + + +import org.springframework.web.multipart.MultipartFile; + +public interface FileService { + /** + * 上传文件,返回文件路径 + * @param file 文件 + * @return 文件路径 + */ + String uploadFile(MultipartFile file); + + /** + * 上传图片,返回图片路径 + * @param file 图片 + * @return 图片路径 + */ + String uploadImage(MultipartFile file); + +} diff --git a/src/main/java/com/example/copykamanotes/service/UserService.java b/src/main/java/com/example/copykamanotes/service/UserService.java index 8b3f1d7..e59fcd5 100644 --- a/src/main/java/com/example/copykamanotes/service/UserService.java +++ b/src/main/java/com/example/copykamanotes/service/UserService.java @@ -4,6 +4,7 @@ import com.example.copykamanotes.model.base.ApiResponse; import com.example.copykamanotes.model.dto.user.LoginRequest; import com.example.copykamanotes.model.dto.user.RegisterRequest; import com.example.copykamanotes.model.dto.user.UpdateUserRequest; +import com.example.copykamanotes.model.dto.user.UserQueryParam; import com.example.copykamanotes.model.entity.User; import com.example.copykamanotes.model.vo.user.AvatarVO; import com.example.copykamanotes.model.vo.user.LoginUserVO; @@ -65,6 +66,13 @@ public interface UserService { */ Map getUserMapByIds(List authorIds); + /** + * 获取用户列表 + * @param userQueryParam 查询参数 + * @return 用户列表 + */ + ApiResponse> getUserList(UserQueryParam userQueryParam); + /** * 上传用户头像 * diff --git a/src/main/java/com/example/copykamanotes/service/impl/LocalFileServiceImpl.java b/src/main/java/com/example/copykamanotes/service/impl/LocalFileServiceImpl.java new file mode 100644 index 0000000..025afc7 --- /dev/null +++ b/src/main/java/com/example/copykamanotes/service/impl/LocalFileServiceImpl.java @@ -0,0 +1,102 @@ +package com.example.copykamanotes.service.impl; + +import com.example.copykamanotes.service.FileService; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +@Log4j2 +@Service +public class LocalFileServiceImpl implements FileService { + + + /** + * 基础上传路径(本地存储的绝对或相对路径) + */ + @Value("${upload.path}") + private String uploadBasePath; + + /** + * 返回给前端的地址前缀 (可配合CDN/Nginx等) + */ + @Value("${upload.url-prefix}") + private String urlPrefix; + + private static final List ALLOWED_IMAGE_EXTENSIONS = Arrays.asList("jpg", "jpeg", "png", "gif"); + + + private static final long MAX_IMAGE_SIZE = 5 * 1024 * 1024; + + @Override + public String uploadFile(MultipartFile file) { + if (file == null || file.isEmpty()) { + throw new IllegalArgumentException("file is empty"); + } + + if (file.getSize() > MAX_IMAGE_SIZE) { + throw new IllegalArgumentException("file size is too large"); + } + + String originFilename = file.getOriginalFilename(); + if (originFilename == null || !originFilename.contains(".")) { + throw new IllegalArgumentException("file name is invalid"); + } + + String lowerCaseExtension = originFilename.substring(originFilename.lastIndexOf(".") + 1).toLowerCase(); + + if (!ALLOWED_IMAGE_EXTENSIONS.contains(lowerCaseExtension)) { + throw new IllegalArgumentException("file extension is not allowed"); + } + + return doUpload(file); + } + + @Override + public String uploadImage(MultipartFile file) { + if (file == null || file.isEmpty()) { + throw new IllegalArgumentException("file is empty"); + } + return doUpload(file); + } + + /** + * 上传文件 + * @param file MultipartFile + * @return url + */ + private String doUpload(MultipartFile file) { + String filename = file.getOriginalFilename(); + + if (filename == null || !filename.contains(".")) { + throw new IllegalArgumentException("file name is invalid"); + } + + String fileExtension = filename.substring(filename.lastIndexOf(".") + 1); + + String newFileName = UUID.randomUUID() + fileExtension; + + File uploadDir = new File(uploadBasePath); + + if (!uploadDir.exists() && !uploadDir.mkdirs()) { + throw new RuntimeException("create upload dir failed"); + } + + File destFile = new File(uploadBasePath, newFileName); + + try { + file.transferTo(destFile); + } catch (IOException e) { + log.error("upload file failed", e); + throw new RuntimeException("upload file failed"); + } + + return urlPrefix + newFileName; + } +} diff --git a/src/main/java/com/example/copykamanotes/service/impl/UserServiceImpl.java b/src/main/java/com/example/copykamanotes/service/impl/UserServiceImpl.java index bfac14d..98312f8 100644 --- a/src/main/java/com/example/copykamanotes/service/impl/UserServiceImpl.java +++ b/src/main/java/com/example/copykamanotes/service/impl/UserServiceImpl.java @@ -1,10 +1,13 @@ package com.example.copykamanotes.service.impl; +import com.example.copykamanotes.annotation.NeedLogin; import com.example.copykamanotes.mapper.UserMapper; import com.example.copykamanotes.model.base.ApiResponse; +import com.example.copykamanotes.model.base.Pagination; import com.example.copykamanotes.model.dto.user.LoginRequest; import com.example.copykamanotes.model.dto.user.RegisterRequest; import com.example.copykamanotes.model.dto.user.UpdateUserRequest; +import com.example.copykamanotes.model.dto.user.UserQueryParam; import com.example.copykamanotes.model.entity.User; import com.example.copykamanotes.model.vo.user.AvatarVO; import com.example.copykamanotes.model.vo.user.LoginUserVO; @@ -12,9 +15,11 @@ import com.example.copykamanotes.model.vo.user.RegisterVO; import com.example.copykamanotes.model.vo.user.UserVO; import com.example.copykamanotes.scope.RequestScopeData; import com.example.copykamanotes.service.EmailService; +import com.example.copykamanotes.service.FileService; import com.example.copykamanotes.service.UserService; import com.example.copykamanotes.utils.ApiResponseUtil; import com.example.copykamanotes.utils.JwtUtil; +import com.example.copykamanotes.utils.PaginationUtil; import com.google.protobuf.Api; import lombok.extern.log4j.Log4j2; import org.springframework.beans.BeanUtils; @@ -24,8 +29,10 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; +import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; @Log4j2 @Service @@ -43,6 +50,9 @@ public class UserServiceImpl implements UserService { @Autowired private RequestScopeData requestScopeData; + @Autowired + private FileService fileService; + @Autowired private EmailService emailService; @@ -151,21 +161,76 @@ public class UserServiceImpl implements UserService { @Override public ApiResponse getUserInfo(Long userId) { - return null; + User user = userMapper.findById(userId); + + if (user == null) { + return ApiResponseUtil.error("用户不存在"); + } + + UserVO userVO = new UserVO(); + BeanUtils.copyProperties(user, userVO); + return ApiResponseUtil.success("获取用户信息成功", userVO); } @Override + @Transactional + @NeedLogin public ApiResponse updateUserInfo(UpdateUserRequest updateUserRequest) { - return null; + Long userId = requestScopeData.getUserId(); + + if (userId == null) { + return ApiResponseUtil.error("未登录"); + } + + User user = new User(); + BeanUtils.copyProperties(updateUserRequest, user); + user.setUserId(userId); + + try { + userMapper.update(user); + return ApiResponseUtil.success("更新用户信息成功"); + } catch (Exception e) { + log.error("更新用户信息失败", e); + return ApiResponseUtil.error("更新用户信息失败"); + } } @Override public Map getUserMapByIds(List authorIds) { - return Map.of(); + if (authorIds.isEmpty()) return Collections.emptyMap(); + + List users = userMapper.findByIds(authorIds); + + return users.stream().collect(Collectors.toMap(User::getUserId, user -> user)); + } + + @Override + public ApiResponse> getUserList(UserQueryParam userQueryParam) { + + // 分页数据 + int total = userMapper.countByQueryParam(userQueryParam); + int offset = PaginationUtil.calculateOffset(userQueryParam.getPage(), userQueryParam.getPageSize()); + Pagination pagination = new Pagination(userQueryParam.getPage(), userQueryParam.getPageSize(), total); + + try { + List users = userMapper.findByQueryParam(userQueryParam, userQueryParam.getPageSize(), offset); + + return ApiResponseUtil.success("获取用户列表成功", users, pagination); + } catch (Exception e) { + return ApiResponseUtil.error(e.getMessage()); + } } @Override public ApiResponse uploadAvatar(MultipartFile file) { - return null; + try { + String url = fileService.uploadImage(file); + AvatarVO avatarVO = new AvatarVO(); + avatarVO.setAvatarUrl(url); + return ApiResponseUtil.success("上传成功", avatarVO); + } catch (Exception e) { + log.error("上传失败", e); + return ApiResponseUtil.error("上传失败"); + } } } diff --git a/src/main/java/com/example/copykamanotes/utils/PaginationUtil.java b/src/main/java/com/example/copykamanotes/utils/PaginationUtil.java index 4900ac2..4256432 100644 --- a/src/main/java/com/example/copykamanotes/utils/PaginationUtil.java +++ b/src/main/java/com/example/copykamanotes/utils/PaginationUtil.java @@ -9,7 +9,7 @@ public class PaginationUtil { * @return 计算出的起始位置 * @throws IllegalArgumentException 如果页码或每页数量小于1,则抛出异常 */ - public static int calculatePagination(Integer page, Integer pageSize) { + public static int calculateOffset(Integer page, Integer pageSize) { if (page < 1) { throw new IllegalArgumentException("page must be greater than 0"); } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 6f57928..ed0204a 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -41,3 +41,7 @@ spring.mail.default-encoding=utf-8 mail.verify-code.expire-minutes=15 mail.verify-code.resend-interval=60 mail.verify-code.template-path="templates/mail/verify-code.html" + + +upload.path=C:/uploads +upload.url-prefix=C:/uploads diff --git a/src/main/resources/mapper/UserMapper.xml b/src/main/resources/mapper/UserMapper.xml index 46ffe1d..c3d6c9b 100644 --- a/src/main/resources/mapper/UserMapper.xml +++ b/src/main/resources/mapper/UserMapper.xml @@ -49,5 +49,58 @@ WHERE user_id = #{userId} + + update user + + username = #{username}, + gender = #{gender}, + birthday = #{birthday}, + avatar_url = #{avatarUrl}, + email = #{email}, + school = #{school}, + signature = #{signature}, + + where user_id = #{userId} + + + + + + + + AND user_id = #{queryParams.userId} + + + AND `account` LIKE CONCAT('%', #{queryParams.account}, '%') + + + AND username LIKE CONCAT('%', #{queryParams.username}, '%') + + + AND is_admin = #{queryParams.isAdmin} + + + AND is_banned = #{queryParams.isBanned} + + + + + + + \ No newline at end of file