// 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`格式,状态码、错误码、消息体与规范一致。 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层实现头像存储、路径生成、文件校验、数据库更新、异常处理、日志记录等逻辑。 说明(基于当前实现): - 服务实现类:`UserAvatarServiceImpl`(`work.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。 3. 配置文件增加头像存储根路径、最大文件大小、允许类型等配置项。 4. 文件存储结构说明(项目实现):`{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` - 推荐策略:保存新头像后异步删除旧头像(注意合规和备份需求),避免磁盘堆积;若需要保留历史版本,则保留旧文件并在数据库中标记版本。 5. 上传接口需校验文件类型与大小,超限或非法类型返回400,未认证401,IO异常500。 6. 获取接口根据数据库记录返回本地文件流,若无头像或文件不存在返回 404(当前实现为返回 `ResponseEntity.status(HttpStatus.NOT_FOUND)`),路径防穿越。 7. 日志记录上传、更新、异常操作,包括userId、操作时间、文件名、IP等,按日志规范分级。 8. 所有接口响应均为`R`格式,错误用`R.fail`,成功用`R.success`。 9. Swagger注解需覆盖所有状态码,泛型返回类型需用`@Schema(implementation=...)`。 10. 响应VO中userId等ID字段类型为String。 ## API设计示例 ### 上传/更新头像接口 ```java @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 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); } ``` ### 获取头像接口 ```java @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) { // ...查找头像文件并返回流... } ``` 注意:Controller 在返回头像资源时会尝试使用 `Files.probeContentType` 自动识别 mime type 并设置到返回的 header(若无法识别则回退到默认处理)。 #### 响应示例(上传成功) ```json { "code": 200, "message": "上传成功", "data": "123/1597891234567.jpg" } ``` #### 响应示例(参数错误) ```json { "code": 400, "message": "文件类型不支持", "data": null } ``` ## 配置示例(application.yml) ```yaml 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 ## 责任人 - 设计/实现:后端开发团队 - 联调:后端+前端