账户维护、登出与多模态文件独立接口

📅 2026/7/15 4:57:53 👁️ 阅读次数 📝 编程学习
账户维护、登出与多模态文件独立接口

1)登出:Header 或 Query 传 Token

@PostMapping("/logout")

public Result<Void> logout(

@RequestHeader(value = "Authorization", required = false) String authorization,

@RequestParam(value = "token", required = false) String token

) {

return loginSevice.logout(authorization, token);

}

2)注册:username+ 用当前请求拼publicBaseUrl(头像 URL)

@PostMapping(value = "/register", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)

public Result<AuthResponse> register(

@RequestParam("username") String username,

@RequestParam("phone") String phone,

@RequestParam("password") String password,

@RequestParam(value = "avatar", required = false) MultipartFile avatar,

HttpServletRequest servletRequest

) {

RegisterRequest request = new RegisterRequest(username, phone, password);

String publicBaseUrl = ServletUriComponentsBuilder.fromRequestUri(servletRequest)

.replacePath(servletRequest.getContextPath())

.replaceQuery(null)

.build()

.toUriString();

return loginSevice.register(request, avatar, publicBaseUrl);

}

3)更新当前用户:昵称 / 头像 multipart,校验后save

String normalizedUsername = trimToNull(username);

boolean hasUsername = StringUtils.hasText(normalizedUsername);

boolean hasAvatar = avatar != null && !avatar.isEmpty();

if (!hasUsername && !hasAvatar) {

return failure("A0440", "username or avatar is required");

}

if (hasUsername && normalizedUsername.length() > 50) {

return failure("A0441", "username length must be <= 50");

}

UserInfoEntity user = authResult.getData();

if (hasUsername) {

user.setUsername(normalizedUsername);

}

if (hasAvatar) {

String extension = resolveAvatarExtension(avatar);

if (!StringUtils.hasText(extension)) {

return failure("A0413", "avatar format is invalid, only jpg/jpeg/png/gif/webp is supported");

}

String publicBaseUrl = ServletUriComponentsBuilder.fromRequestUri(servletRequest)

.replacePath(servletRequest.getContextPath())

.replaceQuery(null)

.build()

.toUriString();

try {

user.setAvatarUrl(saveAvatarFile(avatar, extension, publicBaseUrl));

} catch (IOException ex) {

return failure("A0414", "avatar upload failed");

}

}

user = userInfoRepository.save(user);

4)健康档案 GET:loadOrInitHealthProfile无则创建

@GetMapping("/health-profile")

public Result<HealthProfileResponse> getHealthProfile(

@RequestHeader(value = "Authorization", required = false) String authorization,

@RequestParam(value = "token", required = false) String tokenParam

) {

Result<UserInfoEntity> authResult = authenticate(authorization, tokenParam);

if (authResult.isFail()) {

return failure(authResult.getCode(), authResult.getMessage());

}

UserInfoEntity user = authResult.getData();

HealthProfileEntity profile = loadOrInitHealthProfile(user.getUserId());

return success(toHealthProfileResponse(profile), "get health profile success");

}

private HealthProfileEntity loadOrInitHealthProfile(Long userId) {

Optional<HealthProfileEntity> optionalProfile = healthProfileRepository.findByUserId(userId);

if (optionalProfile.isPresent()) {

return optionalProfile.get();

}

HealthProfileEntity profile = new HealthProfileEntity();

profile.setUserId(userId);

return healthProfileRepository.save(profile);

}

5)独立上传:fileType白名单 + 可选messageId+ 写FileIndex

@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)

public Result<FileUploadResponse> upload(

@RequestHeader(value = "Authorization", required = false) String authorization,

@RequestParam(value = "token", required = false) String tokenParam,

@RequestParam("fileType") String fileType,

@RequestParam("file") MultipartFile file,

@RequestParam(value = "messageId", required = false) Long messageId,

HttpServletRequest servletRequest

) {

Result<UserInfoEntity> authResult = authenticate(authorization, tokenParam);

if (authResult.isFail()) {

return failure(authResult.getCode(), authResult.getMessage());

}

String normalizedFileType = trimToNull(fileType);

if (!StringUtils.hasText(normalizedFileType)) {

return failure("A0430", "fileType is required");

}

normalizedFileType = normalizedFileType.toUpperCase(Locale.ROOT);

if (!ALLOWED_FILE_TYPES.contains(normalizedFileType)) {

return failure("A0430", "fileType must be SYMPTOM_PIC, VOICE or PRESCRIPTION");

}

if (file == null || file.isEmpty()) {

return failure("A0431", "file is required");

}

String fileId = UUID.randomUUID().toString().replace("-", "");

String extension = resolveExtension(file.getOriginalFilename());

String storedName = StringUtils.hasText(extension) ? fileId + "." + extension : fileId;

Path rootPath = Paths.get(uploadRootDir).toAbsolutePath().normalize();

Path fileDirPath = rootPath.resolve(FILE_SUB_DIR).normalize();

Path targetPath = fileDirPath.resolve(storedName).normalize();

if (!targetPath.startsWith(fileDirPath)) {

return failure("A0434", "invalid file path");

}

try {

Files.createDirectories(fileDirPath);

file.transferTo(targetPath.toFile());

} catch (IOException ex) {

return failure("A0435", "save file failed");

}

String baseUrl = ServletUriComponentsBuilder.fromRequestUri(servletRequest)

.replacePath(servletRequest.getContextPath())

.replaceQuery(null)

.build()

.toUriString();

String objectUrl = baseUrl + PREFIX_UPLOADS + FILE_SUB_DIR + "/" + storedName;

FileIndexEntity entity = new FileIndexEntity();

entity.setFileId(fileId);

entity.setUserId(authResult.getData().getUserId());

entity.setMessageId(messageId);

entity.setFileType(normalizedFileType);

entity.setObjectUrl(objectUrl);

entity.setFileSize((int) Math.max(1, Math.ceil(file.getSize() / 1024.0)));

entity = fileIndexRepository.save(entity);

6)下载:按fileId+userId查归属,路径规范化防穿越

Optional<FileIndexEntity> optionalFile = fileIndexRepository.findByFileIdAndUserId(normalizedFileId, authResult.getData().getUserId());

if (optionalFile.isEmpty()) {

return ResponseEntity.status(HttpStatus.NOT_FOUND).body(failure("A0432", "file not found"));

}

FileIndexEntity entity = optionalFile.get();

Path filePath = resolveStoredFilePath(entity.getObjectUrl());

if (filePath == null || !Files.exists(filePath)) {

return ResponseEntity.status(HttpStatus.NOT_FOUND).body(failure("A0433", "file resource not found"));

}

try {

Resource resource = new UrlResource(filePath.toUri());

String contentType = Files.probeContentType(filePath);

if (!StringUtils.hasText(contentType)) {

contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE;

}

return ResponseEntity.ok()

.contentType(MediaType.parseMediaType(contentType))

.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filePath.getFileName() + "\"")

.body(resource);

} catch (MalformedURLException ex) {

return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(failure("A0436", "file resource open failed"));

} catch (IOException ex) {

return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(failure("A0437", "file content type detect failed"));

}

}

private Path resolveStoredFilePath(String objectUrl) {

String normalizedPath = trimToNull(objectUrl);

if (!StringUtils.hasText(normalizedPath)) {

return null;

}

if (normalizedPath.startsWith("http://") || normalizedPath.startsWith("https://")) {

normalizedPath = URI.create(normalizedPath).getPath();

}

if (!normalizedPath.startsWith(PREFIX_UPLOADS)) {

return null;

}

String relativePath = normalizedPath.substring(PREFIX_UPLOADS.length());

Path rootPath = Paths.get(uploadRootDir).toAbsolutePath().normalize();

Path filePath = rootPath.resolve(relativePath).normalize();

if (!filePath.startsWith(rootPath)) {

return null;

}

return filePath;

}