From 172ab321c1466f288bf72b7d881fd2320b949ec1 Mon Sep 17 00:00:00 2001 From: pmc <740076875@qq.com> Date: Thu, 7 May 2026 18:15:54 +0800 Subject: [PATCH] =?UTF-8?q?docs(02):=20=E4=BB=8E=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E5=86=85=E8=81=94=E7=BA=A6=E6=9D=9F=E7=94=9F=E6=88=90=20Phase?= =?UTF-8?q?=202=20CONTEXT.md=EF=BC=88=E7=AE=A1=E7=90=86=E7=AB=AF=20REST=20?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=20PRD=20=E5=BF=AB=E9=80=9F=E9=80=9A=E9=81=93?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../phases/02-admin-rest/02-CONTEXT.md | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 qy_lty/.planning/phases/02-admin-rest/02-CONTEXT.md diff --git a/qy_lty/.planning/phases/02-admin-rest/02-CONTEXT.md b/qy_lty/.planning/phases/02-admin-rest/02-CONTEXT.md new file mode 100644 index 0000000..abed411 --- /dev/null +++ b/qy_lty/.planning/phases/02-admin-rest/02-CONTEXT.md @@ -0,0 +1,150 @@ +# Phase 2:管理端读写接口 - Context + +**Gathered**: 2026-05-07 +**Status**: Ready for planning +**Source**: 用户在 `/gsd-plan-phase 2` 调用时提供的内联约束(PRD 快速通道,等同 Phase 1 同模式) + + +## 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 轮换等增强项 + + + + +## 实现决策(锁定) + +### 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 钩子` 兜底,可以不加锁) + + + + +## 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 中提示用同款骨架格式新建) + + + + +## 具体要点(Success Criteria 显式化) + +| # | 验证点 | 检查方式 | +|---|--------|----------| +| 1 | GET 携 admin token 返回脱敏壳层 | `curl -H "Authorization: Bearer " /api/v1/admin/credential-slot/` 返回 200 + JSON 含 `success: true / data.access_token: <末 4 位掩码>` | +| 2 | PUT 携 admin token 全字段覆写 | `curl -X PUT -H "Authorization: Bearer " -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 条目,跨项目联动字段相互引用 | + + + + +## 推迟事项(明确不在 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 范畴 + + + +--- + +*Phase: 02-admin-rest* +*Context gathered: 2026-05-07 via inline PRD(用户在 /gsd-plan-phase 2 调用时提供完整约束)*