---
id: FEAT-008
title: avatar
domain: profile
status: done
since_version: 0.5.0
---

# FEAT-008 — 头像上传 / 查看 / 替换 / 删除

## User Story

As a **相亲平台用户**, I want to **上传个人头像**, so that **别人查到我时能看到长相，决定要不要联系我**。

**核心痛点**：相亲没照片 = 对方根本不会发信。0.4.x 期只能靠 bio 文字自我介绍，首批试用者反馈"没照片不玩"。

## Acceptance Criteria

- [x] `xq profile avatar upload <file>` 上传一张照片做头像（jpeg / png / webp，≤ 5MB）
- [x] `xq profile avatar show` 拿到自己头像签名 URL（1h 有效）
- [x] 上传头像自动替换旧头像（profiles.avatar_photo_id 指向最新）
- [x] `xq profile album remove <photo_id>` 能删头像；删完 profile.avatar_photo_id 自动清
- [x] query 结果每条 hit 带 `avatar_url` 字段（没头像的用户返 null）
- [x] 审核入口：上传同步调 `_moderate()` stub（当前永远 passed，绿网 SDK 集成 TODO）
- [x] OSS 凭证未配时返 503（`oss_not_configured`）而不是隐蔽挂
- [x] 不能删别人的头像（403 `forbidden`）
- [x] 上传空文件 / 超大文件 / 不支持类型 → 400

## Tests

- `test/test_photo.py::test_upload_avatar_happy`
- `test/test_photo.py::test_upload_replaces_old_avatar`
- `test/test_photo.py::test_avatar_url_no_avatar_returns_none`
- `test/test_photo.py::test_avatar_url_after_upload`
- `test/test_photo.py::test_delete_own_avatar_clears_profile_ref`
- `test/test_photo.py::test_upload_rejects_unsupported_content_type`
- `test/test_photo.py::test_upload_rejects_empty_file`
- `test/test_photo.py::test_upload_rejects_too_large`
- `test/test_photo.py::test_oss_not_configured_raises_503`
- `test/test_db.py::test_user_photos_schema_fields` / `test_profiles_has_avatar_photo_id_column`

## CLI / API

- CLI:
  - `xq profile avatar upload <file>` 实现：`src/xiangqin/cli.py::avatar_upload`
  - `xq profile avatar show` 实现：`src/xiangqin/cli.py::avatar_show`
- API:
  - `POST /photo/avatar`（multipart, file=<image>）→ `src/xiangqin/app.py::photo_avatar_upload`
  - `GET /photo/avatar/{target_user_id}` → `src/xiangqin/app.py::photo_avatar_of`
- 核心：`src/xiangqin/photo.py::upload(..., purpose='avatar')` + `photo.avatar_url(user_id)`

## 依赖 / 被依赖

- **依赖**：
  - `FEAT-001/002` 登录
  - `FEAT-005 profile.set`（avatar_photo_id 列在 profiles 表）
  - 阿里云 OSS 桶 `agentaily-xiangqin-photos-prod`（vault.json 声明）
  - `ALIYUN_ACCESS_KEY_ID` + `ALIYUN_ACCESS_KEY_SECRET` 环境变量注入（epsilon systemd unit EnvironmentFile）
- **被依赖**：
  - `FEAT-010 query` 结果返回 `avatar_url`（头像决定对方是否发信的关键）
  - `FEAT-014 dm.send`（发信时看对方头像辅助决定）

## 风险 / 边界

- **内容审核目前是 stub**（永远 passed），v0.5 后接入阿里云绿网（鉴黄鉴政广告暴力）—— 上线前必须真审核否则违规内容传播平台有责
- **OSS 成本**：5MB * 1000 用户头像 + 每人 3 张相册 = 20GB，阿里云 OSS 标准存储 ~¥2.5/月，可控
- **签名 URL 1h**：用户 agent 长时间持有失效就重拉。不支持长链接（防盗链）
- **删除 OSS 幂等**：db 删 + OSS 删 try/except，OSS 失败不阻塞 db（过期清理任务兜底）
- **不做**：暂不做缩略图 / 裁剪 / 滤镜（客户端 agent 可自行处理）；不 strip EXIF（用户要隐私可自己压缩）

## 相关

- issue `#015` 照片上传
- blueprint `production-readiness.md` P1-3
- objective `O3` KR3.4（照片是用户留存的基础）
- 下阶段：绿网真审核（`_moderate` 实现） / EXIF 剥离 / 缩略图 / 相册上限 20 张
