第 10 章

用户中心

个人中心资料查询与编辑、个性化配置读写、朋友圈动态列表与发布(图片经 MinIO 预签名访问)。

10.1 接口总览

GET/api/user/profile个人中心信息(资料 + 统计 + 最近翻译)
PUT/api/user/profile更新个人资料(昵称 / 手机号 / 简介)
GET/api/user/settings读取个性化配置(键值对)
PUT/api/user/settings更新个性化配置
GET/api/moments/list朋友圈动态分页列表
POST/api/moments/create发布动态(multipart,可选图片)

🔐 本章 6 个接口均不在认证白名单内,必须携带 Authorization(Bearer Token 或 auth_token Cookie,见第 02 章)。 所有「当前用户」语义均由服务端从 UserContext 取得,客户端无法冒充他人查询或修改。

10.2 个人中心(/api/user/profile)

GET

/api/user/profile

获取当前登录用户的个人中心聚合信息:基础资料、翻译统计与最近 5 条成功翻译记录,一次请求全部返回。

响应 data(UserProfileResponse)
字段类型说明
usernameString用户名(只读展示,不可修改)
emailString邮箱(只读展示,不可修改)
nicknameString原始昵称,可为空(供编辑表单预填)
displayNameString展示名(昵称为空时回退用户名)
phoneString手机号(服务端脱敏后返回)
bioString个人简介
avatarUrlString头像 URL
createdAtDateTime注册时间
lastLoginTimeDateTime最后登录时间
totalTranslationsLong成功翻译总数
totalCharactersLong翻译字符总数
recentTranslationsList最近 5 条成功翻译记录,元素结构见下
recentTranslations 元素(RecentTranslationItem)
{
  "id": 1001,
  "sourceText": "你好,世界",
  "translatedText": "Hello, world",
  "sourceLanguage": "zh",
  "targetLanguage": "en",
  "engine": "QWEN",
  "createdAt": "2026-09-06T10:00:00"
}
PUT

/api/user/profile

更新当前用户的个人资料,返回更新后的完整个人中心数据(结构与 GET 相同,便于前端直接刷新页面)。

请求体(UpdateUserProfileRequest)
字段类型必填校验规则
nicknameString可选长度 ≤ 100,否则 400「昵称长度不能超过100字符」
phoneString可选正则 ^(\+?[0-9]{6,20})?$,否则 400「手机号格式不正确」
bioString可选长度 ≤ 500,否则 400「个人简介不能超过500字符」
字段更新语义
null      → 不修改该字段(保持原值)
"" 空串   → 清空该字段
其他值    → 更新为该值(需通过对应校验)

用户名与邮箱为只读字段,本接口无法修改;校验失败由全局异常处理器统一转为 400 VALIDATION_ERROR(错误明细见第 11 章)。

10.3 个性化配置(/api/user/settings)

GET

/api/user/settings

读取当前用户的个性化配置,返回配置键值对 Map(键为 setting_key,值为字符串形式的配置值)。

响应 data(UserSettingsResponse)
{
  "settings": {
    "defaultSourceLanguage": "zh",
    "defaultTargetLanguage": "en",
    "defaultEngine": "QWEN",
    "autoTranslate": "true",
    "useTerminology": "true",
    "qualityAssessment": "false",
    "theme": "light",
    "fontSize": "14",
    "notificationEmail": "true"
  }
}
PUT

/api/user/settings

更新个性化配置(部分字段更新),返回更新后的配置键值对。所有字段均以字符串传值并通过正则校验。

请求体(UpdateUserSettingsRequest)— null 表示不修改
字段类型合法值校验失败提示
defaultSourceLanguageStringzh|en|ja|ko|fr|de|es|ru|ar|pt 十选一默认源语言不支持
defaultTargetLanguageString同上十种语言代码默认目标语言不支持
defaultEngineStringQWEN / ALIBABA_CLOUD默认翻译引擎不支持
autoTranslateString"true" / "false"(字符串布尔)自动翻译开关值不正确
useTerminologyString"true" / "false"术语库匹配开关值不正确
qualityAssessmentString"true" / "false"质量评估开关值不正确
themeStringlight / dark主题值不支持
fontSizeString12 – 24 的整数字号需为 12-24 的整数
notificationEmailString"true" / "false"邮件通知开关值不正确
更新语义与语言代码
null → 不修改;非 null → 整体校验后更新该字段
语言代码集合(与全系统支持语言一致,10 种):
  zh(中文) en(英语) ja(日语) ko(韩语) fr(法语)
  de(德语) es(西班牙语) ru(俄语) ar(阿拉伯语) pt(葡萄牙语)

⚠️ 布尔型配置传的是字符串 "true"/"false"(非 JSON 布尔), 传 true(无引号)会因类型不匹配被反序列化拒绝;正则校验失败统一 400 VALIDATION_ERROR。

10.4 朋友圈动态(/api/moments)

GET

/api/moments/list

分页查询朋友圈动态(全站动态流,按发布时间倒序),图片映射为可直接访问的 MinIO 预签名 URL。

Query 参数
参数必填默认值说明
page可选00 起始(服务端自动 +1 适配 MyBatis-Plus,负值按 0 处理)
size可选20每页条数
响应 data(自定义 Map,非 PagedResponse)
{
  "total": 57,
  "page": 0,                          // 回显客户端传入的 0 起始页码
  "size": 20,
  "items": [
    {
      "id": 12,
      "userId": 1,
      "nickname": "小明",              // 昵称为空时回退用户名
      "content": "今天用 AI 翻了一整份合同,效率翻倍!",
      "imageUrl": "https://minio.../moments/xxx?X-Amz-...",  // 预签名,可为 null
      "createdAt": "2026-09-06T09:30:00"
    }
  ]
}

⚠️ 注意本接口的分页结构与第 11 章的通用 PagedResponse 不同: 这里是 {total, page, size, items},且 items 直接是动态数组。 图片预签名链接获取失败不影响列表返回(对应条目 imageUrl 为 null,仅记 warn 日志)。

POST

/api/moments/create

发布一条动态,支持可选图片上传。multipart/form-data 编码,图片经 MinIO 对象存储持久化。

请求参数(form-data)
参数类型必填说明
contentString必填动态内容;缺失或为空抛 IllegalArgumentException → 400 INVALID_ARGUMENT
imageFile可选动态配图(MultipartFile),上传后返回预签名访问 URL
响应 data(MomentResponse)
{
  "id": 13,
  "userId": 1,
  "nickname": "小明",
  "content": "第一条动态!",
  "imageUrl": "https://minio.../moments/xxx?X-Amz-...",
  "createdAt": "2026-09-06T10:00:00"
}
错误响应
场景HTTPerror 错误码
未登录(Token 缺失但未过拦截器 / 上下文丢失)401UNAUTHORIZED
content 缺失 / 非法参数400INVALID_ARGUMENT(message 为具体原因)
列表查询内部异常500MOMENTS_LIST_FAILED
发布内部异常(含图片存储失败)500MOMENTS_CREATE_FAILED

💰 发布与浏览动态不消耗点数、不校验会员配额;动态功能与翻译计费链路完全隔离。

10.5 调用示例

# 1. 查询个人中心
curl http://localhost:8090/api/user/profile \
  -H "Authorization: Bearer $TOKEN"

# 2. 更新昵称与简介(手机号传空串 = 清空)
curl -X PUT http://localhost:8090/api/user/profile \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"nickname":"新昵称","phone":"","bio":"翻译爱好者"}'

# 3. 更新个性化配置(注意布尔值是字符串)
curl -X PUT http://localhost:8090/api/user/settings \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"theme":"dark","fontSize":"16","autoTranslate":"true"}'

# 4. 发布带图动态
curl -X POST http://localhost:8090/api/moments/create \
  -H "Authorization: Bearer $TOKEN" \
  -F "content=第一条动态!" \
  -F "image=@./screenshot.png"

# 5. 翻页浏览动态
curl "http://localhost:8090/api/moments/list?page=0&size=20" \
  -H "Authorization: Bearer $TOKEN"
// 更新资料并直接用返回值刷新个人中心
const resp = await fetch('/api/user/profile', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${token}`
  },
  body: JSON.stringify({ nickname: '新昵称', bio: '翻译爱好者' })
}).then(r => r.json());

if (!resp.success) throw new Error(resp.message);

const { displayName, totalTranslations, recentTranslations } = resp.data;
console.log(`欢迎 ${displayName},累计翻译 ${totalTranslations} 次`);
recentTranslations.forEach(t =>
  console.log(`· [${t.engine}] ${t.sourceText} → ${t.translatedText}`));

// 发布动态(FormData 自动处理 multipart 边界,勿手动设置 Content-Type)
const fd = new FormData();
fd.append('content', '第一条动态!');
fd.append('image', fileInput.files[0]);          // 可选
await fetch('/api/moments/create', {
  method: 'POST',
  headers: { 'Authorization': `Bearer ${token}` },
  body: fd
});