---
id: 015
title: 照片上传（头像 + 相册，含审核）
status: closed
labels: [feature, p1, storage]
assignee: team-lead
closed_at: 2026-04-23
created_at: 2026-04-23
---

# #015 照片上传

## 背景

相亲场景**照片是必需**——用户看不到对方长相就不会发信。当前 xiangqin profile 只有文本字段（gender / age / tags / bio），没有照片。**这是 v0.4.x 用户拒绝使用的第一原因**（老板 2026-04-23 凌晨反馈）。

## 方案

### 存储

- **OSS 桶**：新建 `agentaily-xiangqin-photos-prod`（和 backup 桶分开，权限隔离）
- **路径约定**：`users/<user_id>/<purpose>/<ulid>.jpg`
  - purpose：`avatar` 头像 / `album` 相册 / `idcard` 身份证正反面（issue #009）
- **签名 URL**：`avatar` 可公开 URL（profile 展示需要）；`album` + `idcard` 签名 URL（只认证用户可看）

### 审核

- **上传时同步审核**：阿里云绿网 / 内容安全 API
- **检测维度**：鉴黄、鉴政、广告、暴力、恶心
- **不通过**：直接拒绝 + 告诉用户"图片不合规"
- **通过**：存 db + 允许使用

### db schema

```sql
CREATE TABLE user_photos (
  id TEXT PRIMARY KEY,          -- ULID
  user_id TEXT NOT NULL,
  purpose TEXT NOT NULL,        -- 'avatar' | 'album'
  oss_key TEXT NOT NULL,
  sha256 TEXT NOT NULL,
  content_type TEXT NOT NULL,
  size_bytes INTEGER NOT NULL,
  uploaded_at INTEGER NOT NULL,
  moderation_status TEXT,       -- 'passed' | 'rejected' | 'pending'
  moderation_reason TEXT,
  FOREIGN KEY (user_id) REFERENCES users(id)
);
```

profile 表加 `avatar_photo_id` 字段指向当前头像。

### CLI

```bash
xq profile avatar upload <file>   # 上传头像（覆盖）
xq profile album add <file>       # 加一张相册照
xq profile album list             # 列我的相册
xq profile album remove <photo_id>

# 查看对方时 query 结果返回头像签名 URL
xq query 'gender=f AND city=hangzhou' --with-photos
```

### 查询展示

query 结果里加 `avatar_url` 字段（签名 URL，1 小时有效）：

```json
{"results":[{"user_id":"01JX...","avatar_url":"https://...","gender":"f",...}]}
```

## 工作量

- OSS 桶创建 + 权限：0.5 天
- 上传 API + 审核集成：2-3 天
- db schema + migration：0.5 天
- 签名 URL 生成 + 返回 query：1 天
- CLI 命令：1 天
- 测试 + 上线：1 天

**单人全职 6-8 天**。

## 不做

- 不做滤镜 / 美颜（本地工具已经够）
- 不做 视频头像（复杂 + 审核成本翻倍）
- 不做 Exif 位置暴露（上传时 strip GPS metadata 保护隐私）

## 相关凭证

- 新 vault resource `aliyun/oss/agentaily-xiangqin-photos-prod`
- 阿里云绿网 API 权限（aliyun-main AK 应够）

## 相关

- P1-3 of blueprint
- 依赖：审核不通过的举报 → #016
