1登出Header 或 Query 传 TokenPostMapping(/logout)public ResultVoid logout(RequestHeader(value Authorization, required false) String authorization,RequestParam(value token, required false) String token) {return loginSevice.logout(authorization, token);}2注册username 用当前请求拼publicBaseUrl头像 URLPostMapping(value /register, consumes MediaType.MULTIPART_FORM_DATA_VALUE)public ResultAuthResponse 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校验后saveString 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健康档案 GETloadOrInitHealthProfile无则创建GetMapping(/health-profile)public ResultHealthProfileResponse getHealthProfile(RequestHeader(value Authorization, required false) String authorization,RequestParam(value token, required false) String tokenParam) {ResultUserInfoEntity 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) {OptionalHealthProfileEntity optionalProfile healthProfileRepository.findByUserId(userId);if (optionalProfile.isPresent()) {return optionalProfile.get();}HealthProfileEntity profile new HealthProfileEntity();profile.setUserId(userId);return healthProfileRepository.save(profile);}5独立上传fileType白名单 可选messageId 写FileIndexPostMapping(value /upload, consumes MediaType.MULTIPART_FORM_DATA_VALUE)public ResultFileUploadResponse 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) {ResultUserInfoEntity 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下载按fileIduserId查归属路径规范化防穿越OptionalFileIndexEntity 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;}