# Phase 2：管理端读写接口 - Context

**Gathered**: 2026-05-07
**Status**: Ready for planning
**Source**: 用户在 `/gsd-plan-phase 2` 调用时提供的内联约束（PRD 快速通道，等同 Phase 1 同模式）

<domain>
## Phase 边界

本 phase 仅负责 **DRF 序列化器 + view + URL 路由 + 鉴权 + 修改记录**：
- 在 `/api/v1/admin/credential-slot/` 暴露 GET（脱敏返回）+ PUT（全字段覆写）两个方法
- 复用现有 `RedisTokenAuthentication`（admin token 体系，key `admin_token:{token}`）
- 响应必须经过 `StandardResponseMiddleware` 壳层
- Access Token 在 GET 响应中通过 `mask_token` 脱敏（仅末 4 位）
- PUT 响应也走脱敏（写入成功后用脱敏值返回，避免运营在 admin 工具里看到自己刚提交的明文回显）
- 接口自动暴露到 `/swagger/` + `/redoc/`（drf-yasg），schema 与实际行为一致
- 修改记录在 qy_lty + qy-lty-admin **两端互引条目**（Phase 2 是首次跨项目接口契约落地，需要前端记一条互引；qy-lty-admin 那侧 Phase 1 plan 已定义 CRED-FE-01 API client 会消费这个接口契约）

**不负责**（留给后续 phase）：
- Phase 3：客户端读取接口（GET `/api/credential-slot/`，user token 鉴权，明文返回）+ 阿里云日志脱敏
- 任何前端代码（在 qy-lty-admin 仓库的 CRED-FE-01 phase 里）
- DB 字段加密、审计日志、token 轮换等增强项

</domain>

<decisions>
## 实现决策（锁定）

### URL 与路由

- **路径**：`/api/v1/admin/credential-slot/`（trailing slash 沿用 Django 默认风格）
- **HTTP 方法**：GET 和 PUT 在同一 URL（不分两个 endpoint，符合 RESTful 单例资源约定）
- **路由注册位置**：决策由 planner 基于 read_first 后选择
  - 候选 A：`aiapp/urls.py`（凭据槽位语义偏向 AI）
  - 候选 B：仓库现有的 admin namespace `/api/v1/admin/` 在哪个 urls 文件汇总？planner 必须 read_first 全仓 grep `/api/v1/admin/` 找到现有汇总点（推测在 `userapp/urls.py` 或 `qy_lty/urls.py`），照抄注册风格
- **不**新建 `credential` app（凭据槽位不值得一个独立 app；它就是 aiapp 的一个子能力）

### View 实现

- **不用** `ModelViewSet`（带不必要的 list / create / delete，单例资源用不上）
- **用** `RetrieveUpdateAPIView`（DRF 提供，开箱支持 GET + PUT/PATCH）或自定义 `APIView` + 手写 `get` / `put` 方法
- **推荐** 自定义 `APIView` —— 单例语义不走 `lookup_field`/`pk`，调 `get_or_create(pk=1)` 取那条唯一记录；用 `RetrieveUpdateAPIView` 反而需要重写 `get_object()`，绕一圈不值得
- View 类命名：`CredentialSlotAdminView`，放 `aiapp/views.py`（沿用现有 view 文件）

### Serializer

- **DRF ModelSerializer**：`CredentialSlotSerializer`，放 `aiapp/serializers.py`（如不存在则新建；planner 检查 `aiapp/` 是否已有 serializers.py）
- 字段：`app_id`、`access_token`、`updated_at`
- `updated_at` 在 GET 响应里 read_only（auto_now 自动维护）
- **Access Token 脱敏在 view 层处理，不在 serializer 层**：serializer 直接把明文交给 view，view 在返回响应前用 `mask_token` 替换；理由：PUT 写入时需要明文走 serializer.is_valid + save()，脱敏放 view 层避免序列化器既要明文又要脱敏的双重责任
- 写入校验：`app_id` 和 `access_token` 都允许空字符串（与模型 `blank=True` 一致），但**不允许 None**（serializer 字段配 `allow_null=False`）

### 鉴权

- **直接复用 `RedisTokenAuthentication`** —— Phase 2 researcher 必须 read_first 找到该类的具体位置（推测 `userapp/authentication.py`），并 read 现有 `/api/v1/admin/` namespace 下任一接口的鉴权配置（如 Bot 管理接口）作为照抄对象
- View 类上配 `authentication_classes = [RedisTokenAuthentication]` + `permission_classes = [IsAdminTokenAuthenticated]`（如果项目已有这种 admin-only permission；否则用 `IsAuthenticated` + 在 view 里加 `request.user.is_staff` 检查）
- **关键**：admin token 的语义是"该 token 来自 `admin_token:{token}` Redis key"，普通 user token 来自 `token:{token}`；planner 应在 read_first 阶段确认现有 admin 接口怎么区分这两类 token（很可能 `RedisTokenAuthentication` 自身根据 key 前缀做了区分，且配套有一个 admin-only permission class）
- 拒绝时返回 401（无 token）或 403（user token 但非 admin），错误响应必须经过 `StandardResponseMiddleware` 壳层

### Swagger / ReDoc

- 接口必须出现在 `/swagger/` + `/redoc/`，drf-yasg 会自动扫描 DRF 视图
- View 类上加 `@swagger_auto_schema` 装饰器（method-level，给 GET / PUT 各写一份），声明 request body schema 和 response schema
- response schema 显式标注 `access_token` 字段语义为「末 4 位脱敏掩码」，避免前端误解为明文

### 跨项目联动（修改记录互引）

- **本 phase 同期写两端 `docs/修改记录.md`**：
  - `qy_lty/docs/修改记录.md` 顶部写一条："新增 Phase 2 管理端 REST 接口"，跨项目联动字段引用 `qy-lty-admin/docs/修改记录.md` 的对应条目
  - `qy-lty-admin/docs/修改记录.md` 顶部写一条："锁定 Phase 2 后端 API 契约（消费方文档化）"，跨项目联动字段引用 qy_lty 的对应条目
- 这是 Phase 2 与 Phase 1 的关键差异：Phase 1 是纯服务端模型层、不涉及 API 契约，所以前端不需要互引；Phase 2 暴露 REST 接口给前端消费，**必须**互引

### 兼容性 / 不引入新依赖

- 沿用 Django 4.2.13、DRF 3.x（现版）、Python 3.8
- drf-yasg 已在依赖里，复用即可
- 不引入新依赖

### Claude's Discretion

- 序列化器是否拆 `CredentialSlotReadSerializer`（脱敏返回） + `CredentialSlotWriteSerializer`（明文写入）两个类，还是用同一个 + view 层脱敏 —— planner 决定
- view 是放 `aiapp/views.py` 末尾，还是新建 `aiapp/views/credential_slot.py` 子文件 —— 取决于 `aiapp/views.py` 现有规模
- 错误响应的具体 message 文案（中文），如 `"凭据槽位需要管理员权限"` / `"未提供有效的管理员 token"`
- View 里如何确保 `get_or_create(pk=1)` 在并发请求下不出竞态（最简：用 `select_for_update` 或单例语义本身已经被 `pk=1 + save 钩子` 兜底，可以不加锁）

</decisions>

<canonical_refs>
## Canonical References

**下游 agent 必读**：

### 项目宪法
- `qy_lty/CLAUDE.md` — 沟通语言（中文）+ 修改记录强制规则 + **跨项目互引强制**
- `qy_lty/.planning/PROJECT.md` — Milestone v1.0「本期 Milestone」段、关键约束（响应壳层 / token 命名）
- `qy_lty/.planning/REQUIREMENTS.md` — Active 段 CRED-03 + CRED-04 完整描述
- `qy_lty/.planning/ROADMAP.md` — Phase 2 详情段（Goal、Success Criteria 4 条）
- `qy_lty/.planning/phases/01-credential-data-layer/01-CONTEXT.md` — 上一 phase 决策（pk=1 单例 + mask_token 工具的来源）
- `qy_lty/.planning/phases/01-credential-data-layer/01-VERIFICATION.md` — 上一 phase 验证证据（mask_token 行为、CredentialSlot.get_solo() 用法）

### DRF / 鉴权 / 路由现成模式（必读）
- `qy_lty/aiapp/views.py` — 同 app 内现有 view 写法
- `qy_lty/aiapp/models.py` — `CredentialSlot` + `get_solo()` + `mask_token` 复用入口
- `qy_lty/userapp/authentication.py` — `RedisTokenAuthentication` 实现（推测路径，researcher 确认）
- `qy_lty/userapp/views.py` —— 现有 admin 接口写法（推测含 `/api/v1/admin/` 命名空间下的 view 模板）
- `qy_lty/qy_lty/urls.py` 或 `qy_lty/userapp/urls.py` — `/api/v1/admin/` 路由汇总点（researcher 找出来）
- `qy_lty/common/middleware.py` — `StandardResponseMiddleware`（确认它如何处理 DRF Response 对象）
- `qy_lty/common/utils.py` — `mask_token` 工具（Phase 1 落地）

### Swagger
- `qy_lty/qy_lty/urls.py` 或独立的 swagger 注册文件 — 看 drf-yasg 如何配 schema_view，照搬 `@swagger_auto_schema` 风格

### 修改记录
- `qy_lty/docs/修改记录.md` — 头部「修改格式说明」+ Phase 1 已落地的两条条目可作模板
- `qy-lty-admin/docs/修改记录.md` — 头部「修改格式说明」（如有；如不存在 planner 应在 task 中提示用同款骨架格式新建）

</canonical_refs>

<specifics>
## 具体要点（Success Criteria 显式化）

| # | 验证点 | 检查方式 |
|---|--------|----------|
| 1 | GET 携 admin token 返回脱敏壳层 | `curl -H "Authorization: Bearer <admin_token>" /api/v1/admin/credential-slot/` 返回 200 + JSON 含 `success: true / data.access_token: <末 4 位掩码>` |
| 2 | PUT 携 admin token 全字段覆写 | `curl -X PUT -H "Authorization: Bearer <admin_token>" -d '{"app_id": "x", "access_token": "y"}' .../credential-slot/` 返回 200 + DB 更新 + `updated_at` 刷新 |
| 3 | PUT 在空记录场景自动 get_or_create | DB 删除 pk=1 后调 PUT，依旧成功创建 |
| 4 | 无 token 返回 401 + 标准壳层 | `curl /api/v1/admin/credential-slot/` 返回 401 + JSON 含 `success: false / code != 0 / message != ""` |
| 5 | 仅 user token（非 admin）返回 403 + 标准壳层 | 携 `token:{token}`（非 admin_token）调用返回 403 + 标准壳层 |
| 6 | Swagger 暴露 | `/swagger/` HTML 页面含 `/api/v1/admin/credential-slot/` 路径条目，含 GET + PUT 两个方法及对应 schema |
| 7 | drf-yasg request/response schema 与实际一致 | `/swagger.json` 含 `app_id` / `access_token` / `updated_at` 字段定义 + `access_token` schema description 标注「末 4 位脱敏掩码」 |
| 8 | 修改记录两端互引 | `qy_lty/docs/修改记录.md` 与 `qy-lty-admin/docs/修改记录.md` 顶部各有一条 Phase 2 条目，跨项目联动字段相互引用 |

</specifics>

<deferred>
## 推迟事项（明确不在 Phase 2 范围）

- **客户端 GET `/api/credential-slot/`** — Phase 3
- **阿里云日志脱敏过滤器** — Phase 3
- **PUT 写入时旧 access_token 的审计日志** — 不在 v1.0 milestone 范畴
- **token 鉴权失败时尝试用其它 token 类型重试 / fallback** — 当前架构禁止此类降级，无 token 就 401
- **API 限流（防暴力 PUT）** — 现有架构未上限流，不在本 phase 范畴
- **DB at-rest 加密** — 未来评估，不在 v1.0 范畴

</deferred>

---

*Phase: 02-admin-rest*
*Context gathered: 2026-05-07 via inline PRD（用户在 /gsd-plan-phase 2 调用时提供完整约束）*
