用户端小程序 我的资料 头像上传 开发设计文档

  • 模块: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<SysFile>{ 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 <token>,与普通接口一致。

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 是共享入口、按 <if> 动态拼列,**严禁**把「只该由本模块显式改写的列」挂上去(参见 updateUserStatus / updateUserAuthStatus / updateUserProviderFlag 的注释)。因此头像更新**单开** updateUserAvatar,只写 avatar 一列,避免 /account/user(后台编辑)请求体带上 avatar 即改他人头像的口子。

2.6 字段长度约束

account_user.avatar 为 varchar(255),服务端对传入 URL 做长度校验(> 255 直接拒绝),避免截断或 SQL 报错。


三、接口设计

统一响应体:R<T>(code=200 成功),与 C 端 /miniapp/** 既有约定一致。

3.1 上传图片文件(复用既有,无需改动)

  • 外部路径:POST /file/upload(网关 StripPrefix=1 → 服务内 POST /upload)
  • 鉴权:需登录(携带 Authorization: Bearer <token>)
  • 请求:multipart/form-data,表单字段名 file
  • 出参:R<SysFile>
{
  "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<Void>(成功即 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 套路):

<!-- 头像更新专用:只动 avatar 一列,避免挂到共享的 updateAccountUser 上被后台编辑接口借道改头像 -->
<update id="updateUserAvatar" parameterType="AccountUser">
    update account_user
    <set>
        avatar = #{avatar},
        <if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
        update_time = sysdate()
    </set>
    where user_id = #{userId} and del_flag = '0'
</update>

六、枚举与状态

名称 取值 说明
允许图片类型 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)。