用户头像本地上传与获取接口设计.md 12 KB

// Controller 在返回头像资源时会尝试使用 Files.probeContentType 识别 mime type 并写到响应头(若识别失败将使用默认且不影响返回)。 @Operation(summary = "获取用户头像", description = "根据用户ID获取头像图片") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "获取成功,返回图片流"), @ApiResponse(responseCode = "404", description = "未找到头像或文件不存在,返回404") }) @GetMapping(value = "/user/avatar/{userId}", produces = MediaType.IMAGE_JPEG_VALUE) public ResponseEntity getAvatar(@PathVariable String userId) { // ...查找头像文件并返回流...

用户头像本地上传与获取接口设计

本文档遵循《03-API设计规范》《06-日志和错误处理规范》《Swagger泛型返回类型字段信息不显示问题解决方案》《前端ID精度丢失问题解决方案》等项目规范。

概述

为提升用户体验,系统需支持用户上传、更新和获取头像图片,头像文件存储于本地磁盘,路径可配置,支持文件大小和类型限制,仅依赖MySQL+Spring Boot+本地存储。

背景和目标

  • 背景:当前仅支持通过字符串方式设置头像,缺乏本地文件上传与访问能力。
  • 目标:实现安全、灵活的头像上传与获取接口,支持本地存储、路径可变、文件大小限制,接口风格与现有API统一。

需求与约束

  1. 认证:接口需走Token认证(AuthInterceptor),依赖currentUserId,未认证返回401。
  2. 存储:头像文件存储于本地磁盘,根路径通过配置文件(如application.yml)指定。
  3. 文件限制:仅允许图片类型(jpg/png/jpeg/webp),最大文件大小(如2MB)可配置,超限或非法类型返回400。
  4. 数据库:UserInfo.avatar字段存储头像相对路径。
  5. 访问控制:仅允许用户本人上传/更新头像,获取接口可公开。
  6. API兼容:接口风格与现有API一致,所有响应均为R<T>格式,状态码、错误码、消息体与规范一致。
  7. 日志审计:所有上传/更新操作需用SLF4J记录info级日志,异常需error级,内容含userId、文件名、IP等。
  8. Swagger文档:所有接口需用@ApiResponse+@Content+@Schema(implementation=...)标注,确保泛型返回类型文档化。
  9. ID精度:所有响应中的userId等ID字段在VO层转为String,避免前端精度丢失。

设计要点(高层)

  1. Controller层新增接口:
    • 上传/更新头像:POST /user/avatar/upload,参数为MultipartFile,仅允许当前用户操作。
    • 获取头像:GET /user/avatar/{userId},userId为字符串,返回图片流。
  2. Service层实现头像存储、路径生成、文件校验、数据库更新、异常处理、日志记录等逻辑。

说明(基于当前实现):

  • 服务实现类:UserAvatarServiceImplwork.baiyun.chronicdiseaseapp.service.impl.UserAvatarServiceImpl)负责具体逻辑;控制器为 UserAvatarController
  • 配置:使用 AvatarProperties@ConfigurationProperties 前缀为 avatar,默认 maxSize 为 2MB,allowedTypes 默认为 "jpg,png,jpeg,webp";若未在配置文件中设置 root-path,则使用默认相对目录 avatar-storage
  • 文件命名:当前实现使用 timestamp.ext(如 1597891234567.jpg)并将文件放到 {root}/{userId}/{timestamp}.{ext} 下(relative = currentUserId + "/" + savedFileName)。
  • 路径与安全:保存前会使用 normalize() 并检查路径前缀以防止路径穿越;若检测失败,抛出系统异常并记录日志。
  • 错误码:对非法文件类型与超限的文件,服务会抛出 CustomException 并使用 ErrorCode.PARAMETER_ERROR;若用户不存在抛 ErrorCode.USER_NOT_EXIST;IO 错误返回 ErrorCode.SYSTEM_ERROR
  • 获取接口:/user/avatar/{userId} 返回 Resource 流;若找不到资源或字段为空则返回 404;若 userId 非法 (非数字),会记录错误并返回 404。
  • 配置文件增加头像存储根路径、最大文件大小、允许类型等配置项。
  • 文件存储结构说明(项目实现):{avatarRootPath}/{userId}/{timestamp}.{ext}
    • 说明:实现没有将 userId 作为文件名前缀,仅把 userId 作为目录名(relative = userId + "/" + timestamp + "." + ext)。
    • 建议(可选):如果希望文件名包含 userId(便于查找或排障),改为 {userId}_{timestamp}.{ext};当前实现已由目录结构与时间戳保证唯一性。
    • 文件命名策略:建议使用 {userId}_{timestamp}.{ext}(如 123_1597891234567.jpg)或基于日期格式的时间戳(如 123_20200820T153012.jpg)生成上传文件名。
    • 优点:保证唯一性、防止并发覆盖;便于前端缓存失效控制(每次上传返回不同 URL);也可以直接用于文件历史管理与清理。
    • 存储示例:
      • 完整路径:D:/avatar-storage/123/123_1597891234567.jpg
      • avatar 字段(相对路径)存储:user/123/123_1597891234567.jpg
    • 推荐策略:保存新头像后异步删除旧头像(注意合规和备份需求),避免磁盘堆积;若需要保留历史版本,则保留旧文件并在数据库中标记版本。
  • 上传接口需校验文件类型与大小,超限或非法类型返回400,未认证401,IO异常500。
  • 获取接口根据数据库记录返回本地文件流,若无头像或文件不存在返回 404(当前实现为返回 ResponseEntity.status(HttpStatus.NOT_FOUND)),路径防穿越。
  • 日志记录上传、更新、异常操作,包括userId、操作时间、文件名、IP等,按日志规范分级。
  • 所有接口响应均为R<T>格式,错误用R.fail,成功用R.success
  • Swagger注解需覆盖所有状态码,泛型返回类型需用@Schema(implementation=...)
  • 响应VO中userId等ID字段类型为String。

API设计示例

上传/更新头像接口

@Operation(summary = "上传/更新用户头像", description = "用户上传或更新自己的头像图片")
@ApiResponses(value = {
  @ApiResponse(responseCode = "200", description = "上传成功",
    content = @Content(mediaType = "application/json",
      schema = @Schema(implementation = R.class))),
  @ApiResponse(responseCode = "400", description = "参数错误/文件类型或大小不合法",
    content = @Content(mediaType = "application/json",
      schema = @Schema(implementation = R.class))),
  @ApiResponse(responseCode = "401", description = "未认证",
    content = @Content(mediaType = "application/json",
      schema = @Schema(implementation = R.class))),
  @ApiResponse(responseCode = "500", description = "服务器内部错误",
    content = @Content(mediaType = "application/json",
      schema = @Schema(implementation = R.class)))
})
@PostMapping(value = "/user/avatar/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public R<String> uploadAvatar(@RequestPart("file") MultipartFile file, HttpServletRequest request) {
  String userId = String.valueOf(request.getAttribute("currentUserId"));
  // 校验文件类型与大小...
  // 生成保存文件名(示例为 userId + 时间戳):
  // String originalFilename = file.getOriginalFilename();
  // String ext = StringUtils.getFilenameExtension(originalFilename); // 或使用 FilenameUtils
  // String savedFileName = System.currentTimeMillis() + "." + ext; // 项目实现使用 timestamp.ext
  // String relativePath = "user/" + userId + "/" + savedFileName;
  // 保存文件到 avatarRootPath + File.separator + relativePath;
  // 更新数据库:UserInfo.avatar = relativePath;
  // 可选:删除或异步清理旧文件
  // 日志示例:logger.info("[AvatarUpload] userId={}, savedFileName={}, ip={}", userId, savedFileName, request.getRemoteAddr());
  // 错误示例:logger.error("[AvatarUpload] userId={}, error=文件类型不支持", userId);
}

获取头像接口

@Operation(summary = "获取用户头像", description = "根据用户ID获取头像图片")
@ApiResponses(value = {
  @ApiResponse(responseCode = "200", description = "获取成功,返回图片流"),
  @ApiResponse(responseCode = "404", description = "未找到头像或文件不存在,返回404")
})
@GetMapping(value = "/user/avatar/{userId}", produces = MediaType.IMAGE_JPEG_VALUE)
public ResponseEntity<Resource> getAvatar(@PathVariable String userId) {
  // ...查找头像文件并返回流...
}

注意:Controller 在返回头像资源时会尝试使用 Files.probeContentType 自动识别 mime type 并设置到返回的 header(若无法识别则回退到默认处理)。

响应示例(上传成功)

{
  "code": 200,
  "message": "上传成功",
  "data": "123/1597891234567.jpg"
}

响应示例(参数错误)

{
  "code": 400,
  "message": "文件类型不支持",
  "data": null
}

配置示例(application.yml)

avatar:
  root-path: D:/avatar-storage/
  max-size: 2MB
  allowed-types: jpg,png,jpeg,webp

数据库字段设计与兼容性

avatar字段用途

  • docs/DataBase/t_user_info.txt 中的 avatar 字段用于存储头像图片的“相对路径”或“文件名”,如 user/123/avatar.jpg,不直接存储图片内容或外部URL。
  • 这样既兼容本地存储,也便于后续迁移和扩展。

统一访问与健壮性

  • 获取头像接口优先查找本地文件(根据avatar字段拼接绝对路径),找不到时返回404;如需兼容历史URL或默认图片,可在该接口实现中增加回退逻辑。
  • avatar字段建议varchar(255)或更长,确保能存储完整相对路径。
  • 开发和测试时可手动插入或模拟avatar字段的不同内容(如空、相对路径、异常值),验证接口健壮性。

UserInfo表已存在avatar字段,无需变更。如需兼容历史数据,可统一迁移旧头像路径。

权限与安全

  • 上传/更新接口仅允许当前登录用户操作自己的头像。
  • 获取接口可公开,但需防止路径遍历等安全风险。
  • 文件名、路径需规范化,防止覆盖他人文件。
  • 所有ID字段在响应VO中转为String,避免前端精度丢失。

日志与审计

  • 上传/更新/异常操作需用SLF4J记录日志,内容包括userId、文件名、操作时间、IP等,按info/error分级。
  • 日志格式建议:[AvatarUpload] userId=xxx, fileName=xxx, ip=xxx
    • 建议日志记录 savedFileName(含时间戳)和 relativePath,以便定位文件与审计:
    • logger.info("[AvatarUpload] userId={}, savedFileName={}, relativePath={}, ip={}", userId, savedFileName, relativePath, ip);
  • 可选:如需合规审计,可建立头像操作日志表。

测试建议

  1. 单元测试:覆盖文件类型、大小校验、路径生成(包括userId+timestamp的命名逻辑)、数据库更新等逻辑。
    • 验证命名规则:同一 userId 连续上传两次应生成不同 savedFileName;验证时间戳或日期格式正确。
    • 验证旧文件处理:上传新头像后老头像是否被删除或保留(依据配置),以及数据库 avatar 字段是否正确地指向新文件。
  2. 集成测试:模拟前端上传、获取流程,验证接口兼容性与安全性。
  3. 性能测试:批量上传、并发获取,评估磁盘IO与接口响应。

兼容性与前端要求

  • 前端需以multipart/form-data方式上传头像,文件字段名为file
  • 获取接口返回图片流,前端可直接用于img标签src。
  • 上传成功后返回头像访问URL,便于前端展示。

迁移/变更管理

  • 先实现后端接口与配置,测试通过后通知前端接入。
  • 头像存储路径、大小、类型等参数可通过配置灵活调整。

参考

  • Spring Boot官方文件上传文档
  • 项目现有认证、响应格式、日志规范
  • docs/DevRule/03-API设计规范.md
  • docs/DevRule/06-日志和错误处理规范.md
  • docs/Swagger泛型返回类型字段信息不显示问题解决方案.md
  • docs/前端ID精度丢失问题解决方案.md

责任人

  • 设计/实现:后端开发团队
  • 联调:后端+前端