# 用户端小程序 我的资料 头像上传 开发设计文档 - 模块:ruoyi-user(会员业务)+ ruoyi-file(文件存储)+ ruoyi-gateway(路由/鉴权头注入) - 日期:2026-10-13 - 需求来源:“实现头像上传功能” - 关联前端:`hugray-user-miniapp`(`pages/profile/edit.vue`) - 关联文档:`doc/design/用户端_个人中心_20261013.md`(个人中心 / 头像字段来源)、`doc/design/用户端_我的资料_20261013.html`(本页 UI 原型) - 数据权威源:`192.168.110.111:3306/hugray-account`(实际表结构优先于本文档) --- ## 一、功能概述 「我的资料」页(`pages/profile/edit.vue`)头像行当前点击仅弹占位 toast,本次改为**真实上传**:点击头像 → 选图 → 上传文件 → 写回 `account_user.avatar` → 页面即时回显;个人中心页再次进入时同步展示新头像。 采用**前端直传文件服务 + 会员服务只更新头像地址**两段式链路: 1. 小程序 `uni.uploadFile` 直传网关 `POST /file/upload`(复用 `ruoyi-file` 现成上传能力),拿到图片可访问 URL; 2. 小程序携 URL 调新增的 C 端接口 `POST /miniapp/account/user/avatar`,`ruoyi-user` **只更新 `avatar` 一列**。 本次只新增**一个 C 端写接口**;文件上传复用既有 `/file/upload`,无需改动。 --- ## 二、现状与关键约束 ### 2.1 文件服务已就绪,直接复用 `ruoyi-file`(端口 9300)已提供上传入口 `SysFileController#upload`: - 服务内路径 `POST /upload`;网关路由 `/file/**` → `lb://ruoyi-file`(`StripPrefix=1`),**对外路径 `POST /file/upload`**。 - 返回 `R{ name, url }`,`url` 即图片可访问地址。 - 默认存储实现为 `LocalSysFileServiceImpl`(`@Primary`),落盘到 `file.path`,访问前缀由 `file.domain` + `file.prefix` 拼出(配置由 Nacos `ruoyi-file-dev.yml` 下发)。 ### 2.2 上传接口需登录态 `/file/upload` **不在**网关白名单(`ignore.whites`)内,网关 `AuthFilter` 会校验令牌并注入 `user_id` 头。故小程序上传时必须携带 `Authorization: Bearer `,与普通接口一致。 ### 2.3 前端请求封装仅支持 JSON `hugray-user-miniapp/utils/request.js` 固定 `Content-Type: application/json`,基于 `uni.request`,**不能直接传文件**。需新增一个基于 `uni.uploadFile` 的封装(单独注入令牌与错误解包),不污染现有 JSON 请求链路。 ### 2.4 `ruoyi-user` 不依赖 `ruoyi-api-system` `ruoyi-user/pom.xml` 未引入 `ruoyi-api-system`,**无法**直接使用 `RemoteFileService`(Feign)做服务端转存。本方案采用「前端直传」,故**无需新增依赖**。 ### 2.5 写头像必须走「单列更新语句」 `AccountUserMapper` 既有约定:`insertAccountUser` / `updateAccountUser` 是共享入口、按 `` 动态拼列,**严禁**把「只该由本模块显式改写的列」挂上去(参见 `updateUserStatus` / `updateUserAuthStatus` / `updateUserProviderFlag` 的注释)。因此头像更新**单开** `updateUserAvatar`,只写 `avatar` 一列,避免 `/account/user`(后台编辑)请求体带上 `avatar` 即改他人头像的口子。 ### 2.6 字段长度约束 `account_user.avatar` 为 `varchar(255)`,服务端对传入 URL 做长度校验(> 255 直接拒绝),避免截断或 SQL 报错。 --- ## 三、接口设计 统一响应体:`R`(`code=200` 成功),与 C 端 `/miniapp/**` 既有约定一致。 ### 3.1 上传图片文件(复用既有,无需改动) - **外部路径**:`POST /file/upload`(网关 `StripPrefix=1` → 服务内 `POST /upload`) - **鉴权**:需登录(携带 `Authorization: Bearer `) - **请求**:`multipart/form-data`,表单字段名 `file` - **出参**:`R` ```json { "code": 200, "msg": "操作成功", "data": { "name": "2026/10/13/xxx.png", "url": "https://xxx/file/upload/2026/10/13/xxx.png" } } ``` ### 3.2 更新当前会员头像(本次新增) - **外部路径**:`POST /miniapp/account/user/avatar`(网关 `StripPrefix=2` → 服务内 `POST /user/avatar`) - **鉴权**:需登录。路径不在网关白名单,未登录由 `AuthFilter` 直接返回 401,前端 `request.js` 自动跳登录页;**后端不做未登录分支**。 - **入参**:`{ "avatar": "<图片URL>" }`(会员身份取自网关注入的 `user_id` 头,**不接受前端传 userId**) - **出参**:`R`(成功即 `code=200`) | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | avatar | String | 是 | 头像地址(来自 `/file/upload` 返回的 `url`);非空、长度 ≤ 255 | - **错误提示**:`头像地址不能为空` / `头像地址过长` / `更新头像失败`(会员不存在或更新异常) --- ## 四、数据库设计 库:`hugray-account`(权威库 `192.168.110.111:3306`)。 **无 DDL 变更**:`account_user.avatar varchar(255)` 已存在,本次仅写入该列。 --- ## 五、流程设计 ### 5.1 前端上传链路(`pages/profile/edit.vue` → `handleAvatarChange`) ``` STEP 1: uni.chooseImage({ count:1, sizeType:['compressed'], sourceType:['album','camera'] }) 选图 STEP 2: 上传前本地校验:临时文件大小 ≤ 5MB,否则提示「图片不能超过 5MB」并中止 STEP 3: uploadFile('/file/upload', tempFilePath) 直传文件服务(携带 Bearer 令牌) STEP 4: 取响应 data.url;为空则提示「上传失败」 STEP 5: updateAvatar(url) 调 POST /miniapp/account/user/avatar 写回 STEP 6: 成功则更新本地 avatar 展示;失败提示且不改动已展示头像 ``` ### 5.2 后端更新头像(`AccountUserServiceImpl.updateAvatar`) ``` STEP 1: 校验 userId 非空(未登录由网关拦截,此处兜底),为空抛「更新头像失败」 STEP 2: 校验 avatarUrl 非空,为空抛「头像地址不能为空」 STEP 3: 校验 avatarUrl 长度 ≤ 255,超长抛「头像地址过长」 STEP 4: 组装 AccountUser(userId + avatar + updateBy)调用 mapper.updateUserAvatar(只更 avatar 列) STEP 5: affected == 0(会员不存在/已删除)抛「更新头像失败」;否则返回 ``` Mapper 单列更新语句(沿用 `updateUserStatus` 套路): ```xml update account_user avatar = #{avatar}, update_by = #{updateBy}, update_time = sysdate() where user_id = #{userId} and del_flag = '0' ``` --- ## 六、枚举与状态 | 名称 | 取值 | 说明 | | --- | --- | --- | | 允许图片类型 | `jpg` / `jpeg` / `png` / `webp` | 由文件服务既有校验 + 前端 `chooseImage` 共同约束 | | 图片大小上限 | 5 MB | 前端上传前本地校验(文件服务侧上限以其自身配置为准) | 本次不涉及状态流转。 --- ## 七、影响范围与文件清单 ### 新增(ruoyi-user) | 文件 | 说明 | | --- | --- | | `domain/dto/AccountUserAvatarDto.java` | 头像更新入参(`avatar`,含校验注解与字段注释) | ### 修改(ruoyi-user) | 文件 | 改动 | | --- | --- | | `controller/AccountProfileController.java` | 新增 `POST /user/avatar` | | `service/IAccountUserService.java` | 新增 `updateAvatar(Long userId, String avatarUrl)` | | `service/impl/AccountUserServiceImpl.java` | 实现 `updateAvatar`(校验 + 单列更新) | | `mapper/AccountUserMapper.java` | 新增 `updateUserAvatar(AccountUser)` | | `resources/mapper/user/AccountUserMapper.xml` | 新增 `updateUserAvatar` 语句 | ### 修改(前端 hugray-user-miniapp) | 文件 | 改动 | | --- | --- | | `utils/request.js` | 新增 `uploadFile(url, filePath, options)`:基于 `uni.uploadFile`,注入令牌、统一解包与错误提示,复用登录失效处理 | | `api/index.js` | 新增 `uploadAvatarFile(filePath)`(→ `/file/upload`)与 `updateAvatar(avatar)`(→ `/miniapp/account/user/avatar`) | | `pages/profile/edit.vue` | 头像行 `handlePlaceholder('头像更换')` 改为 `handleAvatarChange`(选图 → 上传 → 写回 → 回显) | ### 修改(文档) | 文件 | 改动 | | --- | --- | | `doc/design/用户端_个人中心_20261013.md` | 头像字段「现状」由占位更新为「已支持上传」 | ### 单测(ruoyi-user) | 文件 | 用例 | | --- | --- | | `AccountUserServiceImplTest.java` | `updateAvatar`:正常更新 / 空地址 / 超长地址 / 会员不存在(affected=0) | > 网关无需改动:`/miniapp/account/**`、`/file/**` 路由均已存在。 --- ## 八、边界与异常清单 | # | 场景 | 处理 | | --- | --- | --- | | 1 | 未登录访问 | 网关 `AuthFilter` 返回 401,前端跳登录页;后端不做分支 | | 2 | `user_id` 头缺失/为空 | 服务内兜底判空,抛「更新头像失败」(不 NPE) | | 3 | 上传的 `avatar` 为空 | 抛「头像地址不能为空」 | | 4 | `avatar` 长度 > 255 | 抛「头像地址过长」(列 `varchar(255)`) | | 5 | 用户取消选图 | `chooseImage` 成功回调不触发,静默中止(不提示、不请求) | | 6 | 图片超过 5MB | 前端本地提示「图片不能超过 5MB」,不发起上传 | | 7 | 上传失败(网络/文件服务异常) | 提示「上传失败」,不改动已展示头像 | | 8 | 写回失败(接口异常) | 提示后端错误信息,头像保持原值 | | 9 | 会员不存在 / 已逻辑删除 | `updateUserAvatar` 命中 `del_flag='0'` 过滤,affected=0 → 抛「更新头像失败」 | | 10 | 越权 | 会员身份取自令牌注入头,不接受前端传 `userId`,天然防水平越权 | | 11 | 旧头像文件 | **本期不清理**(已确认);后续可加定时清理 | | 12 | 并发重复上传 | 写回为整列覆盖,后写生效;无幂等诉求,不做去重 | --- ## 九、规范符合性说明 - 分层:Controller 只做取身份 + 转发,业务在 Service,SQL 在 Mapper;Controller 不直接调 Mapper。 - 命名:`updateAvatar` / `updateUserAvatar` / `uploadAvatarFile` 见名知意;DTO 与 Entity 分离。 - 注释:DTO / Mapper 方法 / Service 方法具备 Javadoc;核心流程按 `// STEP x:` 分步注释。 - 魔法值:图片大小上限等阈值抽为常量;无散落魔法值。 - 异常:统一 `ServiceException` + 全局处理器,不散落 try-catch 拼返回值。 - 日志:`@Slf4j` 占位符 + `[业务标记]` 前缀;头像 URL 非敏感,日志不打完整用户隐私。 - 数据库:单列更新语句;无 DDL;无 `SELECT *`;无 `${}` 拼接;无循环查库。 - 单测:`updateAvatar` 覆盖正常 / 空值 / 超长 / 会员不存在,Mockito Mock Mapper。 --- ## 十、已确认决策 1. **上传链路**:**已定** —— 前端 `uni.uploadFile` 直传 `/file/upload`,`ruoyi-user` 只更新 `avatar`(免加 `ruoyi-api-system` 依赖)。 2. **存储介质**:**已定** —— 沿用 `ruoyi-file` 本地存储(`LocalSysFileServiceImpl`,`@Primary`)。 3. **旧头像文件**:**已定** —— 本期不清理,仅覆盖 `avatar` 字段。 4. **数据库**:**已定** —— 无 DDL 变更,复用既有 `account_user.avatar varchar(255)`。