# Private Zone 全链路改名联调说明

- 日期：2026-07-22
- 目标环境：本地、`pre`、生产
- 当前状态：已验证

## 1. 目标

原功能名称存在拼写错误。本次将前端、unified 后端、Manager、数据库、埋点和文档中的正式命名统一为 `Private Zone`，并同步切换页面路由、API 路径和公开枚举值。

## 2. 环境地址

| 环境 | 前端 | API Base URL |
| --- | --- | --- |
| `pre` | `https://frontend-test.banlv-ai.com` | `https://proapi.banlv-ai.com` |
| 生产 | `https://cozsweet.com` | `https://api.banlv-ai.com` |

## 3. 前端影响

前端已完成以下修改：

- 页面路由统一为 `/private-zone` 和 `/characters/{characterSlug}/private-zone`。
- 外部入口参数统一为 `target=private-zone`。
- 支付回跳参数统一为 `returnTo=private-zone`。
- 类型、Repository、API client、XState actor、Provider 和资源目录统一使用 `privateZone`、`PrivateZone`、`private-zone` 或 `private_zone`。
- 埋点键统一为 `navigation.private_zone`、`chat.open_private_zone_from_avatar` 等新名称。
- 角色能力字段统一为 `capabilities.privateZone`。

旧页面和旧查询参数不再作为正式入口保留。发布时必须让前后端属于同一个发布批次。

## 4. API 变更清单

所有接口都使用 `Authorization: Bearer <TOKEN>`，响应继续使用现有统一 envelope。请求与响应中未列出的业务字段保持原类型和含义。

| 方法 | 新路径 | 用途 |
| --- | --- | --- |
| `GET` | `/api/private-zone/albums` | 查询付费图片包 |
| `POST` | `/api/private-zone/albums/{albumId}/unlock` | 解锁图片包 |
| `GET` | `/api/private-zone/moments` | 查询朋友圈式内容 |
| `POST` | `/api/private-zone/moments/{momentId}/unlock` | 解锁单条内容 |
| `GET` | `/api/private-zone/config` | 查询当前角色和用户解锁配置 |
| `GET` | `/api/private-zone/diaries` | 查询关系日记 |
| `POST` | `/api/private-zone/diaries/{diaryId}/seen` | 标记关系日记已读 |
| `POST` | `/api/private-zone/diaries/{diaryId}/unlock` | 解锁关系日记 |

### 4.1 图片包列表

```http
GET /api/private-zone/albums?characterId=elio&limit=20
Authorization: Bearer <TOKEN>
```

| 字段 | 位置 | 类型 | 必填 | 可为 `null` | 说明 |
| --- | --- | --- | --- | --- | --- |
| `characterId` | query | string | 是 | 否 | 角色 ID |
| `limit` | query | integer | 否 | 否 | `1-50`，默认 `20` |
| `cursor` | query | string | 否 | 是 | 分页游标 |

调用示例：

```bash
curl 'https://proapi.banlv-ai.com/api/private-zone/albums?characterId=elio&limit=20' \
  -H 'Authorization: Bearer <TOKEN>'
```

### 4.2 解锁图片包

```http
POST /api/private-zone/albums/{albumId}/unlock
Authorization: Bearer <TOKEN>
Content-Type: application/json
```

| 字段 | 位置 | 类型 | 必填 | 可为 `null` | 说明 |
| --- | --- | --- | --- | --- | --- |
| `albumId` | path | string | 是 | 否 | 图片包 ID |
| `expectedCost` | body | integer | 否 | 是 | 前端确认价格；与后端价格不一致时拒绝解锁 |

```bash
curl -X POST 'https://proapi.banlv-ai.com/api/private-zone/albums/<ALBUM_ID>/unlock' \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"expectedCost":320}'
```

### 4.3 Moments 内容与解锁

```http
GET /api/private-zone/moments?characterId=elio&limit=20
POST /api/private-zone/moments/{momentId}/unlock
```

解锁请求体与图片包一致，`expectedCost` 为可选整数。响应中的正式分类值同步改为：

```json
{
  "lockDetail": {
    "type": "private_zone_moment",
    "reason": "private_zone_moment"
  }
}
```

## 5. 其他公开值

| 使用位置 | 新值 | 类型 |
| --- | --- | --- |
| `/api/chat/unlock-private` 的 `lockType` 别名 | `private_zone` | string enum |
| 日程导入 `contentType` 别名 | `private_zone` | string enum，归一化为 `paid_content` |
| 外部入口 `target` | `private-zone` | string enum |
| 支付回跳 `returnTo` | `private-zone` | string enum |
| 锁定详情分类 | `private_zone_moment` | string enum |

## 6. 数据库迁移

因为 `pre` 与生产当前共用 `https://dbapi.banlv-ai.com`，预发部署前先执行 unified 仓库中的临时桥接脚本：

```text
database/private-zone-bridge.sql
```

桥接脚本会创建新表并在切换窗口内保持新旧解锁写入双向同步。生产前后端全部切换完成后，再执行最终迁移：

```text
database/private-zone-migration.sql
```

迁移会在同一事务中完成：

- 将历史解锁表无损重命名；新旧表同时存在时先合并再删除旧表。
- 重命名关联约束、索引、策略和触发器。
- 合并 `users.preferences` 中历史解锁键，防止已解锁内容重新锁定。
- 把 `credit_ledger` 历史分类迁移到 `private_zone` 和 `private_zone_moment`。
- 定向迁移历史埋点键、页面 URL 和系统媒体路径，不改写用户聊天正文。
- 删除切换窗口使用的双写触发器和函数。
- 支持重复执行。

## 7. 兼容性与发布顺序

这是破坏性命名修正，不保留旧 API、旧页面路由或旧公开枚举作为正式兼容层。必须按以下顺序发布：

1. 备份数据库并记录当前 unified、前端和 Manager 版本。
2. 执行 `database/private-zone-bridge.sql`，确认新旧表双向写入一致。
3. 部署 unified `pre`，验证所有 `/api/private-zone/*` 接口。
4. 部署前端 `pre`，验证页面、外部入口、解锁和支付回跳。
5. Manager 更新后验证积分用途显示为新分类。
6. `pre` 验证通过后，先启动新生产后端并临时配置旧 API 到新 API 的 Nginx 转发。
7. 切换生产后端，再部署同批次生产前端；确认新前端只调用 `/api/private-zone/*`。
8. 删除临时 Nginx 转发，执行 `database/private-zone-migration.sql` 清理旧数据库对象。

## 8. 错误与界面状态

- `401`：Token 缺失或无效，前端进入现有登录恢复流程。
- `403`：游客访问仅注册用户可用的关系日记操作，展示现有注册引导。
- `404`：角色、图片包、内容或日记不存在，展示现有空状态或失效提示。
- `409`：`expectedCost` 与后端价格不同，刷新列表后重新确认。
- `402` 或业务余额不足错误：进入现有充值流程，并使用 `returnTo=private-zone` 返回原角色页面。
- 网络失败：不修改本地解锁状态，保留重试入口。

## 9. 验收用例

1. `/characters/elio/private-zone`、Maya 和 Nayeli 对应页面均可打开。
2. `GET /api/private-zone/albums` 返回当前角色内容，角色之间不混用。
3. 已解锁历史记录在迁移后保持解锁状态，不重复扣积分。
4. 新解锁成功后刷新页面仍为已解锁状态。
5. 积分不足进入充值页后，回跳到原角色 `/private-zone` 页面。
6. `target=private-zone&character=nayeli` 进入 Nayeli 对应页面。
7. Manager 的积分用途不再出现旧分类。
8. 三个仓库执行旧命名残留扫描，结果为 `0`。

## 10. 当前测试证据

- unified：`316 passed`。
- 前端：TypeScript 类型检查通过；Vitest `639 passed`；ESLint 通过；契约测试 `4 passed`；生产构建通过并生成三个角色的 `/private-zone` 页面。
- Manager：`75 passed`。
- PostgreSQL 16 临时库：升级、重复升级、回滚和再次升级全部通过；历史解锁、偏好键、积分账本、埋点、媒体路径和数据库对象名均通过断言，临时容器已删除。
- `pre`：unified `71da49b` 和前端 `35939e7` 已验证；新 API 已注册 `8` 条 `/api/private-zone/*` 路径，旧 API 路径数量为 `0`；新页面返回 `200`，旧页面返回 `404`。
- 生产：unified 镜像 `ai-boyfriend-unified:prod-20260722-71da49b`（镜像 ID `sha256:755ef5741856d83d7d808e7df8ee259b6fa5fb94fe333b523ee280ed1d5f674b`）和前端镜像 `prod-35939e7` 已健康运行；Manager 当前版本为 `6eea005`。
- 生产接口：测试账号调用 `GET https://api.banlv-ai.com/api/private-zone/config?characterId=elio` 返回 `200`，`success=true`；旧版 API 路径在所有公开 API 入口均返回 `404`。
- 生产数据库：最终迁移已执行，旧表、桥接函数、桥接触发器、旧偏好键、旧积分分类和旧埋点值的残留数量均为 `0`。

## 11. 回滚影响

最终迁移执行前可直接恢复旧应用，桥接表会保留双向一致的数据。最终迁移执行后，回滚必须同时执行 unified 仓库中的 `database/private-zone-rollback.sql`。回滚前应停止写入，先保存切换后的新增解锁记录，再执行逆向迁移并恢复旧应用版本。不得只恢复前端或只恢复 unified。

## 12. 待确认事项

- 无。
