pmc 2dec1fd813 docs(02-01): 收尾 Plan 02-01(CredentialSlotAdminView 已落地)
- 新增 .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
2026-05-07 22:58:40 +08:00

299 lines
15 KiB
Markdown
Raw Permalink 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: 02-admin-rest
plan: 01
subsystem: aiapp + userappadmin namespace 路由)
tags: [credential-slot, admin-api, drf, mask, swagger, rest]
requirements_completed:
- CRED-03
- CRED-04
dependency_graph:
requires:
- aiapp.models.CredentialSlotPhase 1 / Plan 01-01 落地)
- aiapp.models.CredentialSlot.get_solo()Phase 1 / Plan 01-01 落地)
- common.utils.mask_tokenPhase 1 / Plan 01-01 落地)
- common.responses.success_response / error_response已存在
- common.swagger_utils.get_standardized_response_schema已存在
- userapp.authentication.RedisTokenAuthentication已存在
provides:
- aiapp.serializers.CredentialSlotSerializerDRF ModelSerializer3 字段)
- aiapp.views.CredentialSlotAdminViewAPIViewGET/PUT 两端点)
- URL: /api/v1/admin/credential-slot/name='admin_credential_slot'
affects:
- 下一 plan02-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 层不放 serializerPUT 路径需明文走 is_valid + saveserializer 只做字段校验,避免双重责任"
- "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): 新增 CredentialSlotAdminViewGET 脱敏 / PUT 全字段覆写)
- 9d02021 feat(02-01): 注册 /api/v1/admin/credential-slot/ 路由
completed_date: 2026-05-07
---
# Phase 2 Plan 02-01管理端 REST 接口serializer + view + URL + SwaggerSummary
`/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`ModelSerializer3 字段) |
| `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 子 schemaaccess_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 条 W004STATICFILES_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 + xxxxlen=17 |
### Goal-backward reachability self-check
- truth #1GET 脱敏 200`views.py CredentialSlotAdminView.get` + `serializers.py CredentialSlotSerializer` + `admin_urls.py path` → reachable ✓
- truth #2PUT 全字段覆写 + updated_at 刷新 + 响应脱敏)→ `views.py CredentialSlotAdminView.put``serializer.save` + `_build_response_data`)→ reachable ✓
- truth #3PUT 在空记录场景 get_or_create`CredentialSlot.get_solo()`Phase 1 已在)→ reachable ✓
- truth #4(无 token → 401`RedisTokenAuthentication` + DRF `NotAuthenticated` → reachable ✓
- truth #5user token → 403`_ensure_admin` `is_staff` 校验 → reachable ✓
- truth #6swagger 路径条目 + 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
### 偏差 1Plan 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`
### 偏差 2Task 1 自动化校验命令补 Django setupRule 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/修改记录.mdper execution_context 强制约束)
- **Reason:** 用户在 `<sequential_execution>` 显式声明 "本 plan 不写 docs/修改记录.mdPlan 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
### SetupWave 0 — 签发测试 token
```python
# Django shellpython 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 的 `<threat_model>` 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): 新增 CredentialSlotAdminViewGET 脱敏 / 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 隔离)*