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 → 页面即时回显;个人中心页再次进入时同步展示新头像。
采用**前端直传文件服务 + 会员服务只更新头像地址**两段式链路:
uni.uploadFile 直传网关 POST /file/upload(复用 ruoyi-file 现成上传能力),拿到图片可访问 URL;POST /miniapp/account/user/avatar,ruoyi-user 只更新 avatar 一列。本次只新增**一个 C 端写接口**;文件上传复用既有 /file/upload,无需改动。
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 下发)。/file/upload 不在网关白名单(ignore.whites)内,网关 AuthFilter 会校验令牌并注入 user_id 头。故小程序上传时必须携带 Authorization: Bearer <token>,与普通接口一致。
hugray-user-miniapp/utils/request.js 固定 Content-Type: application/json,基于 uni.request,**不能直接传文件**。需新增一个基于 uni.uploadFile 的封装(单独注入令牌与错误解包),不污染现有 JSON 请求链路。
ruoyi-user 不依赖 ruoyi-api-systemruoyi-user/pom.xml 未引入 ruoyi-api-system,**无法**直接使用 RemoteFileService(Feign)做服务端转存。本方案采用「前端直传」,故**无需新增依赖**。
AccountUserMapper 既有约定:insertAccountUser / updateAccountUser 是共享入口、按 <if> 动态拼列,**严禁**把「只该由本模块显式改写的列」挂上去(参见 updateUserStatus / updateUserAuthStatus / updateUserProviderFlag 的注释)。因此头像更新**单开** updateUserAvatar,只写 avatar 一列,避免 /account/user(后台编辑)请求体带上 avatar 即改他人头像的口子。
account_user.avatar 为 varchar(255),服务端对传入 URL 做长度校验(> 255 直接拒绝),避免截断或 SQL 报错。
统一响应体:R<T>(code=200 成功),与 C 端 /miniapp/** 既有约定一致。
POST /file/upload(网关 StripPrefix=1 → 服务内 POST /upload)Authorization: Bearer <token>)multipart/form-data,表单字段名 fileR<SysFile>{
"code": 200,
"msg": "操作成功",
"data": { "name": "2026/10/13/xxx.png", "url": "https://xxx/file/upload/2026/10/13/xxx.png" }
}
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) 已存在,本次仅写入该列。
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 展示;失败提示且不改动已展示头像
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 | 前端上传前本地校验(文件服务侧上限以其自身配置为准) |
本次不涉及状态流转。
| 文件 | 说明 |
|---|---|
domain/dto/AccountUserAvatarDto.java |
头像更新入参(avatar,含校验注解与字段注释) |
| 文件 | 改动 |
|---|---|
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 语句 |
| 文件 | 改动 |
|---|---|
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 |
头像字段「现状」由占位更新为「已支持上传」 |
| 文件 | 用例 |
|---|---|
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 | 并发重复上传 | 写回为整列覆盖,后写生效;无幂等诉求,不做去重 |
updateAvatar / updateUserAvatar / uploadAvatarFile 见名知意;DTO 与 Entity 分离。// STEP x: 分步注释。ServiceException + 全局处理器,不散落 try-catch 拼返回值。@Slf4j 占位符 + [业务标记] 前缀;头像 URL 非敏感,日志不打完整用户隐私。SELECT *;无 ${} 拼接;无循环查库。updateAvatar 覆盖正常 / 空值 / 超长 / 会员不存在,Mockito Mock Mapper。uni.uploadFile 直传 /file/upload,ruoyi-user 只更新 avatar(免加 ruoyi-api-system 依赖)。ruoyi-file 本地存储(LocalSysFileServiceImpl,@Primary)。avatar 字段。account_user.avatar varchar(255)。