feat(user): 添加用户相关功能- 新增用户信息更新功能- 新增用户头像上传功能

- 新增用户列表查询功能
- 实现用户信息获取和用户地图获取功能
- 添加文件上传服务
This commit is contained in:
LingandRX 2025-03-25 22:04:19 +08:00
parent f7a4995c58
commit 73eb1198f6
11 changed files with 307 additions and 6 deletions

View File

@ -0,0 +1,9 @@
package com.example.copykamanotes.annotation;
import java.lang.annotation.*;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NeedLogin {
}

View File

@ -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<LoginUserVO> whoami() {
return userService.whoami();
}
@PostMapping("/users/me")
public ApiResponse<LoginUserVO> updateUserInfo(
@Valid
@RequestBody
UpdateUserRequest updateUserRequest
) {
return userService.updateUserInfo(updateUserRequest);
}
@PostMapping("/users/avatar")
public ApiResponse<AvatarVO> uploadAvatar(
@RequestParam("file") MultipartFile file
) {
return userService.uploadAvatar(file);
}
@GetMapping("/admin/users")
public ApiResponse<List<User>> adminGetUser(
@Valid UserQueryParam userQueryParam
) {
return userService.getUserList(userQueryParam);
}
}

View File

@ -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<User> findByIds(@Param("userIds") List<Long> userIds);
int countByQueryParam(@Param("queryParams") UserQueryParam userQueryParam);
List<User> findByQueryParam(@Param("queryParams") UserQueryParam userQueryParam, @Param("limit") int pageSize, @Param("offset") int offset);
}

View File

@ -4,7 +4,6 @@ package com.example.copykamanotes.model.base;
* PaginationApiResponse类用于处理分页的API响应
* @param <T> 泛型类型表示分页数据类型
*/
public class PaginationApiResponse<T> extends ApiResponse<T> {
// 分页对象
private final Pagination pagination;

View File

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

View File

@ -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<Long, User> getUserMapByIds(List<Long> authorIds);
/**
* 获取用户列表
* @param userQueryParam 查询参数
* @return 用户列表
*/
ApiResponse<List<User>> getUserList(UserQueryParam userQueryParam);
/**
* 上传用户头像
*

View File

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

View File

@ -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<UserVO> 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<LoginUserVO> 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<Long, User> getUserMapByIds(List<Long> authorIds) {
return Map.of();
if (authorIds.isEmpty()) return Collections.emptyMap();
List<User> users = userMapper.findByIds(authorIds);
return users.stream().collect(Collectors.toMap(User::getUserId, user -> user));
}
@Override
public ApiResponse<List<User>> 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<User> users = userMapper.findByQueryParam(userQueryParam, userQueryParam.getPageSize(), offset);
return ApiResponseUtil.success("获取用户列表成功", users, pagination);
} catch (Exception e) {
return ApiResponseUtil.error(e.getMessage());
}
}
@Override
public ApiResponse<AvatarVO> 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("上传失败");
}
}
}

View File

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

View File

@ -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

View File

@ -49,5 +49,58 @@
WHERE user_id = #{userId}
</update>
<update id="update" parameterType="com.example.copykamanotes.model.entity.User">
update user
<set>
<if test="username != null">username = #{username},</if>
<if test="gender != null">gender = #{gender},</if>
<if test="birthday != null">birthday = #{birthday},</if>
<if test="avatarUrl != null">avatar_url = #{avatarUrl},</if>
<if test="email != null">email = #{email},</if>
<if test="school != null">school = #{school},</if>
<if test="signature != null">signature = #{signature},</if>
</set>
where user_id = #{userId}
</update>
<select id="findByIds" resultType="com.example.copykamanotes.model.entity.User">
SELECT *
FROM user
WHERE user_id IN
<foreach collection="userIds" item="userId" open="(" separator="," close=")">
#{userId}
</foreach>
</select>
<sql id="whereClause">
<where>
<if test="queryParams.userId != null">
AND user_id = #{queryParams.userId}
</if>
<if test="queryParams.account != null">
AND `account` LIKE CONCAT('%', #{queryParams.account}, '%')
</if>
<if test="queryParams.username != null">
AND username LIKE CONCAT('%', #{queryParams.username}, '%')
</if>
<if test="queryParams.isAdmin != null">
AND is_admin = #{queryParams.isAdmin}
</if>
<if test="queryParams.isBanned != null">
AND is_banned = #{queryParams.isBanned}
</if>
</where>
</sql>
<select id="countByQueryParam" resultType="integer">
SELECT COUNT(*) FROM user
<include refid="whereClause"/>
</select>
<select id="findByQueryParam" resultType="com.example.copykamanotes.model.entity.User">
SELECT * FROM user
<include refid="whereClause"/>
LIMIT #{limit} OFFSET #{offset}
</select>
</mapper>