From 2dec1fd813e77965f957d5b5f40b15566ae36059 Mon Sep 17 00:00:00 2001 From: pmc <740076875@qq.com> Date: Thu, 7 May 2026 22:58:40 +0800 Subject: [PATCH] =?UTF-8?q?docs(02-01):=20=E6=94=B6=E5=B0=BE=20Plan=2002-0?= =?UTF-8?q?1=EF=BC=88CredentialSlotAdminView=20=E5=B7=B2=E8=90=BD=E5=9C=B0?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 .planning/phases/02-admin-rest/02-01-SUMMARY.md(含 frontmatter / decisions / metrics / 偏差 / Plan 02-02 端到端 verify hook) - STATE.md:当前位置 1→2、Plan 02 of 02→01 of 02、progress 75%、性能指标加 Plan 02-01 行、 累积决策追加 5 条 [Plan 02-01] 标签项、下一步切到 /gsd-execute-plan 02-02 - ROADMAP.md:Phase 2 plan 进度 0/2 → 1/2 - REQUIREMENTS.md:CRED-03 / CRED-04 标记 complete + traceability 表更新 - config.json:gsd-tools init 写入 workflow._auto_chain_active flag(不影响本期执行) Plan 02-01 三个 task commit: 6820fe7 / 192d0a1 / 9d02021 --- qy_lty/.planning/REQUIREMENTS.md | 4 +- qy_lty/.planning/ROADMAP.md | 2 +- qy_lty/.planning/STATE.md | 65 ++-- qy_lty/.planning/config.json | 3 +- .../phases/02-admin-rest/02-01-SUMMARY.md | 298 ++++++++++++++++++ 5 files changed, 339 insertions(+), 33 deletions(-) create mode 100644 qy_lty/.planning/phases/02-admin-rest/02-01-SUMMARY.md diff --git a/qy_lty/.planning/REQUIREMENTS.md b/qy_lty/.planning/REQUIREMENTS.md index e107d5d..8e9a45e 100644 --- a/qy_lty/.planning/REQUIREMENTS.md +++ b/qy_lty/.planning/REQUIREMENTS.md @@ -97,8 +97,8 @@ - [x] **CRED-01** 单例 `CredentialSlot` Django 模型 + 迁移;DB 层强制最多一条记录(`pk=1` 固定主键或单字段唯一约束);含 `app_id`、`access_token`、`updated_at` 字段 ✓ Plan 01-01 完成(2026-05-07,commits a9c25eb / 30c7caf / a475fe4) - [x] **CRED-02** Django Admin 注册:列表态/查看态对 `access_token` 字段脱敏;新增/编辑态可见明文(运营录入需要);隐藏"新增"按钮(强制单例语义) ✓ Plan 01-02 完成(2026-05-07,commit 653f057;Task 2 由 orchestrator Django test client 程序化验收 10/10 PASS) -- [ ] **CRED-03** 管理端 GET `/api/v1/admin/credential-slot/`:admin token 鉴权(`admin_token:{token}` Redis key 体系);返回 `{ app_id, access_token: , updated_at }`,Access Token 仅返回末 4 位脱敏掩码 -- [ ] **CRED-04** 管理端 PUT `/api/v1/admin/credential-slot/`:admin token 鉴权;接受 `{ app_id, access_token }` 全字段覆写更新;空记录场景自动 `get_or_create`;变更写入 `updated_at` +- [x] **CRED-03** 管理端 GET `/api/v1/admin/credential-slot/`:admin token 鉴权(`admin_token:{token}` Redis key 体系);返回 `{ app_id, access_token: , updated_at }`,Access Token 仅返回末 4 位脱敏掩码 +- [x] **CRED-04** 管理端 PUT `/api/v1/admin/credential-slot/`:admin token 鉴权;接受 `{ app_id, access_token }` 全字段覆写更新;空记录场景自动 `get_or_create`;变更写入 `updated_at` - [ ] **CRED-05** 客户端 GET `/api/credential-slot/`:user token 鉴权(`token:{token}` Redis key 体系,复用 `RedisTokenAuthentication`);**明文**返回 `{ app_id, access_token, updated_at }`(手机端 / 设备端实际调用第三方服务需要) - [ ] **CRED-06** Access Token 日志过滤:阿里云日志格式化器 / 自定义日志过滤器中识别 `access_token` 字段并脱敏,覆盖 PUT 请求体、admin GET 响应体两条最易泄露路径 diff --git a/qy_lty/.planning/ROADMAP.md b/qy_lty/.planning/ROADMAP.md index 5835c28..13e9672 100644 --- a/qy_lty/.planning/ROADMAP.md +++ b/qy_lty/.planning/ROADMAP.md @@ -45,7 +45,7 @@ 3. 不携带 admin token、或仅携带普通 user token 调用上述两个端点均被拒绝(401 / 403),错误响应同样符合 `StandardResponseMiddleware` 壳层 4. 接口出现在 `/swagger/` 与 `/redoc/` 中,请求/响应 schema 与实际行为一致(drf-yasg 自动生成) **Plans:** 2 plans - - [ ] 02-01-PLAN.md — CredentialSlot serializer + view(GET 脱敏 / PUT 覆写 + admin 二次校验)+ admin_urls 路由注册(CRED-03 + CRED-04) + - [x] 02-01-PLAN.md — CredentialSlot serializer + view(GET 脱敏 / PUT 覆写 + admin 二次校验)+ admin_urls 路由注册(CRED-03 + CRED-04) - [ ] 02-02-PLAN.md — 端到端 curl + Django shell 验收 8 条 success criteria + qy_lty / qy-lty-admin 两端修改记录互引(CRED-03 + CRED-04) ### Phase 3: 客户端读取与日志脱敏 diff --git a/qy_lty/.planning/STATE.md b/qy_lty/.planning/STATE.md index c8b2a9a..d2dcfeb 100644 --- a/qy_lty/.planning/STATE.md +++ b/qy_lty/.planning/STATE.md @@ -2,21 +2,21 @@ gsd_state_version: 1.0 milestone: v1.0 milestone_name: 通用凭据槽位 -status: Phase 1 全部完成,等待启动 Phase 2 规划 -stopped_at: Phase 1 全部完成(Plan 01-01 数据层 + Plan 01-02 Admin/修改记录),ROADMAP Phase 1 4 条 success criteria 全部满足;下一步启动 `/gsd-plan-phase 2`(管理端 REST 接口) -last_updated: "2026-05-07T10:34:22.576Z" +status: Phase 2 Plan 02-01 完成,等待执行 Plan 02-02(端到端 verify + 修改记录两端互引) +stopped_at: Plan 02-01 完成(CredentialSlotSerializer + CredentialSlotAdminView + URL 注册);下一步启动 Plan 02-02(端到端 verify + 修改记录两端互引) +last_updated: "2026-05-07T14:57:06.337Z" last_activity: 2026-05-07 progress: total_phases: 3 completed_phases: 1 total_plans: 4 - completed_plans: 2 - percent: 50 + completed_plans: 3 + percent: 75 --- # Project State — QY LTY Backend -**最后更新**: 2026-05-07(Phase 1 Plan 01-02 完成,Admin 注册 + 修改记录归档落地;Phase 1 整体 Complete) +**最后更新**: 2026-05-07(Phase 2 Plan 02-01 完成:CredentialSlotSerializer + CredentialSlotAdminView + URL 注册) ## 项目引用 @@ -24,37 +24,38 @@ progress: **核心价值**:设备端与手机端通过同一个 user_id 实时互通——`device_{user_id}` 分组语义必须始终成立。 -**当前重点**:Milestone v1.0 通用凭据槽位(APP ID + Access Token)— Phase 1 已完成,等待启动 Phase 2(管理端 REST 接口,覆盖 CRED-03 + CRED-04) +**当前重点**:Milestone v1.0 通用凭据槽位(APP ID + Access Token)— Phase 1 已完成;Phase 2 Plan 02-01(管理端 REST 接口,CRED-03 + CRED-04)已落地,等待 Plan 02-02 端到端 verify + 修改记录两端互引。 ## 当前位置 ``` -Phase: 1 of 3(凭据槽位数据层)— Complete -Plan: 02 of 02(Admin 注册 + 修改记录)— Complete -Status: Phase 1 全部完成,等待启动 Phase 2 规划 +Phase: 2 of 3(管理端 REST 接口)— In Progress +Plan: 01 of 02(serializer + view + URL + Swagger)— Complete +Status: Plan 02-01 完成,等待执行 Plan 02-02 Last activity: 2026-05-07 ``` -Progress: [██████████] 100%(Phase 1 内 plan 进度:2/2,已收尾) +Progress: [████████░░] 75%(累计完成 plan:3/4 — Phase 1 全部 + Phase 2 Plan 02-01) ## 性能指标 **速度:** -- 已完成 plan 数:2 -- 平均耗时:~6.5 min(顺序执行模式) -- 总执行时间:784 s(Plan 01-01: 184s + Plan 01-02: ~600s 含 checkpoint 验收) +- 已完成 plan 数:3 +- 平均耗时:~333 s(顺序执行模式) +- 总执行时间:1000 s(Plan 01-01: 184 s + Plan 01-02: ~600 s + Plan 02-01: 216 s) **按 Phase:** -| Phase | Plans | Total | Avg/Plan | -|-------|-------|-------|----------| -| 1 | 2/2 | 784 s | 392 s | +| Phase | Plans | Total | Avg/Plan | +|-------|-------|--------|----------| +| 1 | 2/2 | 784 s | 392 s | +| 2 | 1/2 | 216 s | 216 s | **最近趋势:** -- 最近 5 个 plan:01-01(184 s,3 task / 3 commit / 3 文件) / 01-02(~600 s,4 task / 2 commit / 2 文件 + 1 checkpoint 验收) -- 趋势:第二个 plan 含 checkpoint:human-verify 验收阶段,耗时显著高于纯 auto plan,符合预期 +- 最近 5 个 plan:01-01(184 s,3 task)/ 01-02(~600 s,4 task + checkpoint 验收)/ 02-01(216 s,3 task / 3 commit / 3 文件) +- 趋势:纯 auto plan(无 checkpoint)落地速度稳定在 200-220 s 区间;checkpoint 验收 plan 显著放大耗时 *每完成一个 plan 后更新* @@ -77,6 +78,11 @@ Progress: [██████████] 100%(Phase 1 内 plan 进度:2/2 - **[Plan 01-02]** BotAdmin / ChatMessage 注册块的历史 class 名误用问题不修(不在 phase scope) - **[Plan 01-02]** 修改记录两条条目都在 Task 3 一次性写入「跨项目联动: 无」字段(INFO #2 调整),不留 Task 4 二次写入 - **[Plan 01-02]** qy-lty-admin/docs/修改记录.md 不写互引条目;Phase 1 是纯服务端改动,CLAUDE.md 跨项目规则下纯单端不需要互引;Phase 2 暴露 REST 接口时再做前后端互引 +- **[Plan 02-01]** CredentialSlotAdminView 1:1 复刻 RTCChatHistoryAPIView 自定义 APIView 风格(不走 RetrieveUpdateAPIView,仓库零先例) +- **[Plan 02-01]** permission_classes=[IsAuthenticated] + view 内 _ensure_admin 二次校验 is_staff(不发明 IsAdminTokenAuthenticated permission 类,沿用 AdminEmailLoginView/AdminLogoutView 模式) +- **[Plan 02-01]** 脱敏放 view 层 _build_response_data helper:GET 与 PUT 响应都强制走脱敏,避免 PUT 直接 return success_response(data=serializer.data) 明文回显(Pitfall 3) +- **[Plan 02-01]** drf-yasg request_body 用独立 CredentialSlotPutRequestSchema 类(与 AdminEmailLoginRequestSchema 模式一致),与实际写入校验的 CredentialSlotSerializer 解耦 +- **[Plan 02-01]** 不写 docs/修改记录.md(用户在 prompt 显式声明);由 Plan 02-02 Task 2 一次性补两端互引条目 ### Pending Todos @@ -104,17 +110,18 @@ Progress: [██████████] 100%(Phase 1 内 plan 进度:2/2 ## 下一步 ``` -/gsd-plan-phase 2 +/gsd-execute-plan 02-02 ``` -Phase 1 已完成(Plan 01-01 + Plan 01-02 全部交付): +Phase 2 Plan 02-01 已完成(commits 6820fe7 / 192d0a1 / 9d02021): -- Plan 01-01:CredentialSlot 单例模型 + 0004 迁移 + mask_token 工具 + DB 探针(commits a9c25eb / 30c7caf / a475fe4 / 20036ee) -- Plan 01-02:CredentialSlotAdmin 注册(脱敏 + 单例 + 禁删,commit 653f057)+ 修改记录两条 Phase 1 条目(commit ddbcb7d) +- Task 1:`aiapp/serializers.py` 追加 `CredentialSlotSerializer`(commit 6820fe7) +- Task 2:`aiapp/views.py` 追加 `CredentialSlotAdminView`(GET 脱敏 + PUT 全字段覆写 + _ensure_admin + _build_response_data)(commit 192d0a1) +- Task 3:`userapp/admin_urls.py` 注册 `path('credential-slot/', ..., name='admin_credential_slot')`(commit 9d02021) -ROADMAP Phase 1 全部 4 条 success criteria + 2 条工程硬要求均满足,DB 探针 `pk=1, access_token='probe_secret_xxxx'` 已还原至稳定起点。 +URL `/api/v1/admin/credential-slot/` 已可被 reverse / Django check 通过;探针脱敏 `probe_secret_xxxx` → `*************xxxx` 验证 OK。CRED-03 / CRED-04 已在 REQUIREMENTS.md 标记 complete。 -下一步启动 `/gsd-plan-phase 2` 规划管理端 REST 接口(CRED-03 GET 脱敏 + CRED-04 PUT 全字段覆写,admin token 鉴权)。 +下一步执行 Plan 02-02 端到端 verify(Django test client + curl)+ qy_lty 与 qy-lty-admin 两端 docs/修改记录.md 互引条目。 ## 工作流配置 @@ -144,10 +151,10 @@ CLAUDE.md 两条强制规则(任何 phase 都必须遵守): ## Session Continuity -Last session: 2026-05-07 -Stopped at: Phase 1 全部完成(Plan 01-01 数据层 + Plan 01-02 Admin/修改记录),ROADMAP Phase 1 4 条 success criteria 全部满足;下一步启动 `/gsd-plan-phase 2`(管理端 REST 接口) -Resume file: 无 — Phase 1 收尾,等待 Phase 2 规划启动 +Last session: 2026-05-07T14:57:06.334Z +Stopped at: Plan 02-01 完成(CredentialSlotSerializer + CredentialSlotAdminView + URL 注册);下一步启动 Plan 02-02(端到端 verify + 修改记录两端互引) +Resume file: None --- -*由 /gsd-execute-phase 顺序执行器于 2026-05-07 更新(Plan 01-02 完成时点 = Phase 1 收尾)* +*由 /gsd-execute-phase 顺序执行器于 2026-05-07 更新(Plan 02-01 完成时点)* diff --git a/qy_lty/.planning/config.json b/qy_lty/.planning/config.json index b679586..6d49183 100644 --- a/qy_lty/.planning/config.json +++ b/qy_lty/.planning/config.json @@ -35,7 +35,8 @@ "plan_bounce": false, "plan_bounce_script": null, "plan_bounce_passes": 2, - "auto_prune_state": false + "auto_prune_state": false, + "_auto_chain_active": false }, "hooks": { "context_warnings": true diff --git a/qy_lty/.planning/phases/02-admin-rest/02-01-SUMMARY.md b/qy_lty/.planning/phases/02-admin-rest/02-01-SUMMARY.md new file mode 100644 index 0000000..a1f3c1f --- /dev/null +++ b/qy_lty/.planning/phases/02-admin-rest/02-01-SUMMARY.md @@ -0,0 +1,298 @@ +--- +phase: 02-admin-rest +plan: 01 +subsystem: aiapp + userapp(admin namespace 路由) +tags: [credential-slot, admin-api, drf, mask, swagger, rest] +requirements_completed: + - CRED-03 + - CRED-04 +dependency_graph: + requires: + - aiapp.models.CredentialSlot(Phase 1 / Plan 01-01 落地) + - aiapp.models.CredentialSlot.get_solo()(Phase 1 / Plan 01-01 落地) + - common.utils.mask_token(Phase 1 / Plan 01-01 落地) + - common.responses.success_response / error_response(已存在) + - common.swagger_utils.get_standardized_response_schema(已存在) + - userapp.authentication.RedisTokenAuthentication(已存在) + provides: + - aiapp.serializers.CredentialSlotSerializer(DRF ModelSerializer,3 字段) + - aiapp.views.CredentialSlotAdminView(APIView,GET/PUT 两端点) + - URL: /api/v1/admin/credential-slot/(name='admin_credential_slot') + affects: + - 下一 plan:02-02-PLAN(端到端 verify + 修改记录两端互引) +tech_stack: + added: [] + patterns: + - DRF 自定义 APIView 单 URL 多方法(1:1 复刻 RTCChatHistoryAPIView) + - permission_classes=[IsAuthenticated] + view 内 _ensure_admin 二次校验 is_staff + (沿用 AdminEmailLoginView / AdminLogoutView 模式,不发明 IsAdminTokenAuthenticated) + - 脱敏在 view 层 _build_response_data helper 完成,不在 serializer 层 + - GET 与 PUT 响应都走 _build_response_data,避免 PUT 明文回显 + - method-level @swagger_auto_schema 装饰器 + access_token 字段 description + 显式标注脱敏掩码语义 +key_files: + created: [] + modified: + - qy_lty/aiapp/serializers.py + - qy_lty/aiapp/views.py + - qy_lty/userapp/admin_urls.py +decisions: + - "View 1:1 复刻 RTCChatHistoryAPIView 风格:不走 RetrieveUpdateAPIView(仓库零先例)" + - "permission_classes=[IsAuthenticated] + 视图内 _ensure_admin 二次校验:与 AdminEmailLoginView/AdminLogoutView 一致;不发明 IsAdminTokenAuthenticated permission 类" + - "脱敏放 view 层不放 serializer:PUT 路径需明文走 is_valid + save,serializer 只做字段校验,避免双重责任" + - "GET 与 PUT 响应都强制走 _build_response_data:避免 PUT 直接 return success_response(data=serializer.data) 导致刚提交的明文回显(Pitfall 3)" + - "drf-yasg request body 用独立 CredentialSlotPutRequestSchema serializer 类(与 userapp/views.py:705-708 AdminEmailLoginRequestSchema 模式一致):与实际写入校验的 CredentialSlotSerializer 解耦" + - "method-level @swagger_auto_schema:在 GET 与 PUT 各挂一份;access_token 响应字段 description 明示末 4 位脱敏掩码" +metrics: + duration_seconds: 216 + tasks_completed: 3 + files_modified: 3 + commits: + - 6820fe7 feat(02-01): 新增 CredentialSlotSerializer + - 192d0a1 feat(02-01): 新增 CredentialSlotAdminView(GET 脱敏 / PUT 全字段覆写) + - 9d02021 feat(02-01): 注册 /api/v1/admin/credential-slot/ 路由 + completed_date: 2026-05-07 +--- + +# Phase 2 Plan 02-01:管理端 REST 接口(serializer + view + URL + Swagger)Summary + +在 `/api/v1/admin/credential-slot/` 暴露 GET(脱敏读取)+ PUT(全字段覆写)两个 admin token 鉴权端点,全部沿用仓库现有 RTCChatHistoryAPIView / AdminEmailLoginView 模式,零新依赖。 + +## 一句话概述 + +新增 `CredentialSlotSerializer` + `CredentialSlotAdminView`(GET/PUT),在 `userapp/admin_urls.py` 注册到 `/api/v1/admin/credential-slot/`,view 层 `mask_token` 脱敏 access_token,覆盖 CRED-03 / CRED-04。 + +## 改动文件清单 + +| 文件 | 类型 | 描述 | +|------|------|------| +| `qy_lty/aiapp/serializers.py` | 修改 | import 区追加 `CredentialSlot`;文件末尾追加 `CredentialSlotSerializer`(ModelSerializer,3 字段) | +| `qy_lty/aiapp/views.py` | 修改 | import 区追加 `CredentialSlot` / `CredentialSlotSerializer` / `mask_token` / `get_standardized_response_schema`;文件末尾追加 `CredentialSlotPutRequestSchema`(drf-yasg 请求体 schema)+ `_credential_slot_data_schema`(响应 data 子 schema)+ `CredentialSlotAdminView`(含 `_ensure_admin` / `_build_response_data` / GET / PUT 4 个方法) | +| `qy_lty/userapp/admin_urls.py` | 修改 | 顶部 import `CredentialSlotAdminView`;urlpatterns 追加 `path('credential-slot/', ..., name='admin_credential_slot')` | + +## 实际落地的关键产物 + +### Serializer + +```python +# qy_lty/aiapp/serializers.py +class CredentialSlotSerializer(serializers.ModelSerializer): + class Meta: + model = CredentialSlot + fields = ['app_id', 'access_token', 'updated_at'] + read_only_fields = ['updated_at'] + extra_kwargs = { + 'app_id': {'allow_blank': True, 'allow_null': False, 'required': False}, + 'access_token': {'allow_blank': True, 'allow_null': False, 'required': False}, + } +``` + +### View + +`qy_lty/aiapp/views.py` 文件末尾: + +- `CredentialSlotPutRequestSchema(serializers.Serializer)` — drf-yasg 请求体 schema +- `_credential_slot_data_schema = openapi.Schema(...)` — 响应 data 子 schema(access_token description 明示末 4 位脱敏掩码) +- `CredentialSlotAdminView(APIView)`: + - `authentication_classes = [RedisTokenAuthentication]` + - `permission_classes = [IsAuthenticated]` + - `_ensure_admin(request)` — `if not request.user.is_staff: return error_response(code=403, status_code=403)` + - `_build_response_data(instance)` — 调 `mask_token(instance.access_token)` 覆盖明文 + - `get(request)` / `put(request)` — 各挂 `@swagger_auto_schema` + +### URL + +```python +# qy_lty/userapp/admin_urls.py +from aiapp.views import CredentialSlotAdminView +urlpatterns = [ + ..., + path('credential-slot/', CredentialSlotAdminView.as_view(), name='admin_credential_slot'), +] +``` + +完整路径:`/api/v1/admin/credential-slot/`(拼接自 `qy_lty/urls.py:59` 的 `path('v1/admin/', include('userapp.admin_urls'))` + `path('credential-slot/', ...)`)。 + +### 调用 mask_token 的位置(脱敏调用点) + +`qy_lty/aiapp/views.py:637`: + +```python +data['access_token'] = mask_token(instance.access_token) +``` + +`_build_response_data` 是 GET 与 PUT 唯一的响应数据构造点,强制脱敏。 + +## Plan 内自验证据 + +| 验证点 | 命令 | 结果 | +|--------|------|------| +| import 链路 | `from aiapp.views import CredentialSlotAdminView; from aiapp.serializers import CredentialSlotSerializer` | OK 无 ImportError | +| URL 解析 | `reverse('admin_credential_slot')` | `/api/v1/admin/credential-slot/` | +| Django check | `python manage.py check` | 仅 1 条 W004(STATICFILES_DIRS)— 预先存在,与本 plan 无关 | +| Serializer 字段 | 实例化 `pk=1` 后 `.data.keys()` | `['app_id', 'access_token', 'updated_at']` | +| Serializer read_only_fields | `.fields['updated_at'].read_only` | `True` | +| Serializer allow_null/allow_blank | `.fields['app_id'/'access_token']` | `allow_blank=True, allow_null=False` | +| View 鉴权 / 权限链 | `CredentialSlotAdminView.authentication_classes / permission_classes` | `[RedisTokenAuthentication]` / `[IsAuthenticated]` | +| View 4 个 method 完整性 | `hasattr(v, 'get'/'put'/'_ensure_admin'/'_build_response_data')` | 全部 True | +| Swagger 装饰器 | `hasattr(get_method/put_method, '_swagger_auto_schema')` | 全部 True | +| 探针脱敏 | `mask_token('probe_secret_xxxx')` | `*************xxxx`(13 stars + xxxx;len=17) | + +### Goal-backward reachability self-check + +- truth #1(GET 脱敏 200)→ `views.py CredentialSlotAdminView.get` + `serializers.py CredentialSlotSerializer` + `admin_urls.py path` → reachable ✓ +- truth #2(PUT 全字段覆写 + updated_at 刷新 + 响应脱敏)→ `views.py CredentialSlotAdminView.put`(`serializer.save` + `_build_response_data`)→ reachable ✓ +- truth #3(PUT 在空记录场景 get_or_create)→ `CredentialSlot.get_solo()`(Phase 1 已在)→ reachable ✓ +- truth #4(无 token → 401)→ `RedisTokenAuthentication` + DRF `NotAuthenticated` → reachable ✓ +- truth #5(user token → 403)→ `_ensure_admin` `is_staff` 校验 → reachable ✓ +- truth #6(swagger 路径条目 + access_token 脱敏 description)→ `@swagger_auto_schema` + `_credential_slot_data_schema` description → reachable ✓ + +端到端 curl 验收(含 admin token 签发 / user token 拒绝 / swagger.json 校验)由 Plan 02-02 完成。 + +## Deviations from Plan + +### 偏差 1:Plan import 行号偏移(Rule 1 等价 — 现实修正) + +- **Found during:** Task 2 +- **Plan 假设:** `from drf_yasg.utils import swagger_auto_schema` 在第 13 行 +- **实际仓库状态:** 该 import 实际位于第 14 行(第 11 行已是 `from common.swagger_utils import swagger_schema`) +- **Adjustment:** 不按行号定位,按字符串精确 Edit 替换 8 行 import 块(包含 `.models` / `.serializers` / `RedisTokenAuthentication` / `serializers` / `swagger_utils` / `responses`),追加 `mask_token` 1 行;新增 `get_standardized_response_schema` 通过扩展现有 `from common.swagger_utils import swagger_schema` 一行完成 +- **Reason:** Plan 行号是参考值,不是契约;本仓库 import 区结构与 plan 描述完全等价(仅个别行偏移),按精确 token 串替换更安全 +- **Files modified:** `qy_lty/aiapp/views.py` +- **Commit:** `192d0a1` + +### 偏差 2:Task 1 自动化校验命令补 Django setup(Rule 3 — 阻塞修复) + +- **Found during:** Task 1 verify +- **Issue:** Plan 提供的 `python -c "from aiapp.serializers import CredentialSlotSerializer..."` 命令在 windows 命令行下直接 import 会触发 `ImproperlyConfigured: Requested setting INSTALLED_APPS, but settings are not configured` +- **Fix:** 在 verify 命令内显式 `os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'qy_lty.settings'); django.setup()`;功能等价但兼容裸 python -c 调用 +- **Reason:** Plan 假设的 verify 命令行假设了某个隐式环境(如 `python manage.py shell -c`),裸 python 解释器需显式 setup;不影响功能正确性 +- **Files modified:** 无(仅 verify 命令) +- **Commit:** 无(不涉及代码改动) + +### 偏差 3:未写 docs/修改记录.md(per execution_context 强制约束) + +- **Reason:** 用户在 `` 显式声明 "本 plan 不写 docs/修改记录.md(Plan 02-02 Task 2 一并写两端互引)" +- **Status:** 故意不写,由 Plan 02-02 一次性补完两端互引条目(CLAUDE.md 跨项目联动规则) +- **Risk:** 在 Plan 02-02 落地前,本仓库 `docs/修改记录.md` 不含 Phase 2 条目;可接受,因为 Plan 02-01 + 02-02 是同 session 同 phase 的连续动作 + +### 其余偏差 + +无。Plan 三个 task 的代码内容、acceptance criteria、anti-pattern 约束、reachability 验证全部按 Plan 1:1 落地。 + +## 留给 Plan 02-02 的端到端 verify hook + +### Setup(Wave 0 — 签发测试 token) + +```python +# Django shell:python manage.py shell +from userapp.models import ParadiseUser +from userapp.utils import generate_token + +# 找一个 staff 用户(或现成的 admin) +admin_user = ParadiseUser.objects.filter(is_staff=True).first() +admin_token = generate_token(admin_user.id, is_admin=True) +print('ADMIN_TOKEN=', admin_token) + +# 找一个普通用户 +normal_user = ParadiseUser.objects.filter(is_staff=False).first() +user_token = generate_token(normal_user.id, is_admin=False) +print('USER_TOKEN=', user_token) +``` + +### Curl 矩阵 + +```bash +# 1. 无 token → 401 +curl -i http://localhost:8000/api/v1/admin/credential-slot/ + +# 2. user token → 403 + "需要管理员权限" +curl -i -H "Authorization: Bearer ${USER_TOKEN}" http://localhost:8000/api/v1/admin/credential-slot/ + +# 3. admin token GET → 200 + access_token 脱敏(probe_secret_xxxx → *************xxxx) +curl -i -H "Authorization: Bearer ${ADMIN_TOKEN}" http://localhost:8000/api/v1/admin/credential-slot/ + +# 4. admin token PUT → 200 + DB 写入 + 响应同样脱敏 +curl -i -X PUT -H "Authorization: Bearer ${ADMIN_TOKEN}" -H "Content-Type: application/json" \ + -d '{"app_id":"new_app","access_token":"new_secret_token_5678"}' \ + http://localhost:8000/api/v1/admin/credential-slot/ +# 期望响应 data.access_token = "****************5678",updated_at 刷新 + +# 5. swagger.json 含 credential-slot 路径条目 + access_token 脱敏 description +curl -s http://localhost:8000/swagger.json | python -c "import json,sys; d=json.load(sys.stdin); p='/api/v1/admin/credential-slot/'; assert p in d['paths'], p; assert 'GET' in [m.upper() for m in d['paths'][p].keys()]; assert 'PUT' in [m.upper() for m in d['paths'][p].keys()]; print('OK swagger')" +``` + +### Django shell 程序化验收(test client) + +```python +# python manage.py shell +from rest_framework.test import APIClient +from userapp.models import ParadiseUser +from userapp.utils import generate_token + +c = APIClient() + +# admin token GET +admin_user = ParadiseUser.objects.filter(is_staff=True).first() +admin_token = generate_token(admin_user.id, is_admin=True) +c.credentials(HTTP_AUTHORIZATION=f'Bearer {admin_token}') +resp = c.get('/api/v1/admin/credential-slot/') +print(resp.status_code, resp.json()) +assert resp.status_code == 200 +assert resp.json()['success'] == True +assert '*' in resp.json()['data']['access_token'] # 脱敏特征 + +# admin token PUT +resp2 = c.put('/api/v1/admin/credential-slot/', + data={'app_id': 'put_app', 'access_token': 'put_secret_5678'}, + format='json') +print(resp2.status_code, resp2.json()) +assert resp2.status_code == 200 +assert resp2.json()['data']['access_token'].endswith('5678') # 末 4 位 +assert '*' in resp2.json()['data']['access_token'] # 脱敏 + +# user token → 403 +normal_user = ParadiseUser.objects.filter(is_staff=False).first() +user_token = generate_token(normal_user.id, is_admin=False) +c.credentials(HTTP_AUTHORIZATION=f'Bearer {user_token}') +resp3 = c.get('/api/v1/admin/credential-slot/') +print(resp3.status_code, resp3.json()) +assert resp3.status_code == 403 +assert resp3.json()['success'] == False + +# 无 token → 401 +c.credentials() +resp4 = c.get('/api/v1/admin/credential-slot/') +print(resp4.status_code, resp4.json()) +assert resp4.status_code == 401 +``` + +## Threat Flags + +无。本 plan 改动严格落在 02-01-PLAN 的 `` 8 条已声明威胁内(T-02-01 ~ T-02-08),未引入新 trust boundary 或新攻击面。 + +## Self-Check: PASSED + +### Files + +- FOUND: `qy_lty/aiapp/serializers.py`(已修改,含 CredentialSlotSerializer) +- FOUND: `qy_lty/aiapp/views.py`(已修改,含 CredentialSlotAdminView) +- FOUND: `qy_lty/userapp/admin_urls.py`(已修改,含 admin_credential_slot URL) +- FOUND: `.planning/phases/02-admin-rest/02-01-SUMMARY.md`(本文件) + +### Commits + +- FOUND: `6820fe7` feat(02-01): 新增 CredentialSlotSerializer +- FOUND: `192d0a1` feat(02-01): 新增 CredentialSlotAdminView(GET 脱敏 / PUT 全字段覆写) +- FOUND: `9d02021` feat(02-01): 注册 /api/v1/admin/credential-slot/ 路由 + +### Imports + +- VERIFIED: `from aiapp.views import CredentialSlotAdminView; from aiapp.serializers import CredentialSlotSerializer` 同行无 ImportError +- VERIFIED: `reverse('admin_credential_slot')` = `/api/v1/admin/credential-slot/` +- VERIFIED: `mask_token('probe_secret_xxxx')` = `*************xxxx` + +--- + +*Phase: 02-admin-rest / Plan: 01* +*Executed: 2026-05-07 by gsd-executor(顺序执行模式,无 worktree 隔离)*