151 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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-yasgschema 与实际行为一致
- 修改记录在 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_onlyauto_now 自动维护)
- **Access Token 脱敏在 view 层处理,不在 serializer 层**serializer 直接把明文交给 viewview 在返回响应前用 `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或 403user 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 调用时提供完整约束)*