# 用户头像本地上传与获取接口设计 > 本文档遵循《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层实现头像存储、路径生成、文件校验、数据库更新、异常处理、日志记录等逻辑。 3. 配置文件增加头像存储根路径、最大文件大小、允许类型等配置项。 4. 文件存储结构建议:`{avatarRootPath}/{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. 获取接口根据数据库记录返回本地文件流,若无头像返回默认图片,路径防穿越。 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 = userId + "_" + System.currentTimeMillis() + "." + 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 = "未找到头像,返回默认图片") }) @GetMapping(value = "/user/avatar/{userId}", produces = MediaType.IMAGE_JPEG_VALUE) public ResponseEntity getAvatar(@PathVariable String userId) { // ...查找头像文件并返回流... } ``` #### 响应示例(上传成功) ```json { "code": 200, "message": "上传成功", "data": "/user/avatar/user/123/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字段拼接绝对路径),找不到时可回退到默认头像或兼容历史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 ## 责任人 - 设计/实现:后端开发团队 - 联调:后端+前端