561 lines
29 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
type: execute
wave: 1
depends_on: []
files_modified:
- qy_lty/aiapp/serializers.py
- qy_lty/aiapp/views.py
- qy_lty/userapp/admin_urls.py
autonomous: true
requirements:
- CRED-03
- CRED-04
must_haves:
truths:
- "携 admin token 调用 GET /api/v1/admin/credential-slot/ 返回 200 + 标准壳层 + access_token 末 4 位脱敏"
- "携 admin token 调用 PUT /api/v1/admin/credential-slot/ 写入后 DB 被全字段覆写、updated_at 自动刷新、响应中 access_token 同样脱敏"
- "DB 不存在 pk=1 记录时人为删除场景PUT 仍能 get_or_create 成功创建并写入"
- "无 Authorization 头返回 401DRF NotAuthenticated → middleware 兜底 success:false 标准壳层)"
- "携普通 user token非 staff返回 403 + 标准壳层 + message='需要管理员权限'"
- "/swagger.json 包含 /api/v1/admin/credential-slot/ 路径条目,含 GET 与 PUT 两个 method 及 access_token 脱敏掩码描述"
artifacts:
- path: "qy_lty/aiapp/serializers.py"
provides: "CredentialSlotSerializer ModelSerializer 类"
contains: "class CredentialSlotSerializer"
- path: "qy_lty/aiapp/views.py"
provides: "CredentialSlotAdminView APIView 类(含 GET/PUT 两个方法 + swagger 装饰器)"
contains: "class CredentialSlotAdminView"
- path: "qy_lty/userapp/admin_urls.py"
provides: "/api/v1/admin/credential-slot/ URL 注册"
contains: "admin_credential_slot"
key_links:
- from: "qy_lty/userapp/admin_urls.py"
to: "qy_lty/aiapp/views.py:CredentialSlotAdminView"
via: "from aiapp.views import CredentialSlotAdminView"
pattern: "credential-slot"
- from: "qy_lty/aiapp/views.py:CredentialSlotAdminView"
to: "qy_lty/aiapp/models.py:CredentialSlot.get_solo()"
via: "instance = CredentialSlot.get_solo()"
pattern: "CredentialSlot\\.get_solo"
- from: "qy_lty/aiapp/views.py:CredentialSlotAdminView"
to: "qy_lty/common/utils.py:mask_token"
via: "data['access_token'] = mask_token(instance.access_token)"
pattern: "mask_token\\(instance\\.access_token"
- from: "qy_lty/aiapp/views.py:CredentialSlotAdminView (is_staff 校验)"
to: "PUT/GET 早返回 403"
via: "if not request.user.is_staff: return error_response(... code=403 ...)"
pattern: "is_staff"
---
<objective>
本 plan 在 `/api/v1/admin/credential-slot/` 暴露 GET脱敏+ PUT全字段覆写两个端点覆盖 CRED-03 + CRED-04。
Purpose
- CRED-03管理后台读取脱敏后的凭据槽位仅末 4 位明文,避免运营在 admin UI 看到完整明文)
- CRED-04管理后台以全字段覆写方式更新凭据槽位PUT 响应同样走脱敏避免明文回显
Output
- 新增 `CredentialSlotSerializer`ModelSerializer3 字段 + read_only_fields + extra_kwargs allow_blank
- 新增 `CredentialSlotAdminView`(自定义 APIView + 手写 GET/PUT1:1 复刻 RTCChatHistoryAPIView 风格)
-`userapp/admin_urls.py` 追加 `path('credential-slot/', ...)` 注册
- View 方法挂 `@swagger_auto_schema` 装饰器,响应 schema 显式标注 access_token 末 4 位脱敏掩码语义
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/REQUIREMENTS.md
@.planning/phases/02-admin-rest/02-CONTEXT.md
@.planning/phases/02-admin-rest/02-RESEARCH.md
# Phase 1 落地决策(为何模型走 pk=1 + get_solo + mask_token
@.planning/phases/01-credential-data-layer/01-01-SUMMARY.md
@.planning/phases/01-credential-data-layer/01-02-SUMMARY.md
# 1:1 复刻样板(必读)
@qy_lty/aiapp/views.py
@qy_lty/aiapp/serializers.py
@qy_lty/aiapp/models.py
@qy_lty/aiapp/urls.py
@qy_lty/userapp/views.py
@qy_lty/userapp/admin_urls.py
@qy_lty/userapp/authentication.py
@qy_lty/userapp/utils.py
@qy_lty/common/responses.py
@qy_lty/common/middleware.py
@qy_lty/common/swagger_utils.py
@qy_lty/common/utils.py
@qy_lty/qy_lty/urls.py
<interfaces>
<!-- 关键契约。Executor 直接照用,不要再去 grep。 -->
【已存在 — 复用】Model: qy_lty/aiapp/models.py
```python
class CredentialSlot(models.Model):
app_id = models.CharField(max_length=128, blank=True, default='')
access_token = models.CharField(max_length=512, blank=True, default='')
updated_at = models.DateTimeField(auto_now=True)
def save(self, *args, **kwargs):
# pk=1 单例钩子:已有记录时把 pk 重定向DB 永远只有一行
...
@classmethod
def get_solo(cls):
instance, _created = cls.objects.get_or_create(pk=1)
return instance
```
【已存在 — 复用】Auth: qy_lty/userapp/authentication.py
```python
class RedisTokenAuthentication(BaseAuthentication):
# 读取 Authorization: Bearer <token>,调 get_user_id_from_token
# 优先查 admin_token:{token},否则 token:{token};都查不到 → AuthenticationFailed (401)
# 命中 → 返回 (ParadiseUser, None)
def authenticate(self, request): ...
```
注意:本类**不区分** admin / user token。区分点是 `request.user.is_staff`(只有走 AdminEmailLoginView 的用户才会是 staff
【已存在 — 复用】Helpers: qy_lty/common/responses.py
```python
def success_response(data=None, message="操作成功", code=200, **kwargs) -> Response # 200
def error_response(message="操作失败", code=400, status_code=400, **kwargs) -> Response
```
两者已构造壳层四字段success / code / message / data与 StandardResponseMiddleware 协同middleware.py:53-55 检查 success+code 二者皆在则不二次包装)。
【已存在 — 复用】Tool: qy_lty/common/utils.py
```python
def mask_token(token: str, visible_tail: int = 4, mask_char: str = '*') -> str:
# 'sk-abcdef1234' -> '*********1234'
# '' -> ''
# 'abc' -> '***' # 短于 visible_tail 时全脱敏
```
【已存在 — 复用】Swagger: qy_lty/common/swagger_utils.py
```python
def get_standardized_response_schema(data_schema=None) -> openapi.Schema
# 返回 OpenAPI Schematype=OBJECTproperties=success/code/message + 可选 data
```
【新增 — 本 plan 落地】Serializer: qy_lty/aiapp/serializers.py
```python
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},
}
```
【新增 — 本 plan 落地】View: qy_lty/aiapp/views.py 末尾
```python
class CredentialSlotAdminView(APIView):
authentication_classes = [RedisTokenAuthentication]
permission_classes = [IsAuthenticated]
def get(self, request): ... # 返回脱敏 data
def put(self, request): ... # 全字段覆写 + 脱敏响应
```
</interfaces>
</context>
<tasks>
<task type="auto" tdd="false">
<name>Task 1新增 CredentialSlotSerializeraiapp/serializers.py</name>
<files>qy_lty/aiapp/serializers.py</files>
<read_first>
- 必读 qy_lty/aiapp/serializers.py 全文(仅 9 行;现有 ChatMessageSerializer 是同款 ModelSerializer 模板)
- 必读 qy_lty/aiapp/models.py 中 CredentialSlot 模型定义(验证字段名 app_id / access_token / updated_at
- 必读 02-RESEARCH.md `Pattern 2DRF ModelSerializer 写法` 段落(含完整骨架)
</read_first>
<action>
`qy_lty/aiapp/serializers.py` 顶部 import 行追加 `CredentialSlot`,文件末尾追加 `CredentialSlotSerializer` 类。
**完整修改后文件内容(直接覆写)**
```python
from rest_framework import serializers
from .models import ChatMessage, CredentialSlot
class ChatMessageSerializer(serializers.ModelSerializer):
class Meta:
model = ChatMessage
fields = ['id', 'user', 'bot', 'message', 'timestamp', 'sender', 'message_type', 'message_audio_url', 'message_video_url']
read_only_fields = ['id', 'timestamp', 'sender']
class CredentialSlotSerializer(serializers.ModelSerializer):
"""通用凭据槽位序列化器(明文存储,脱敏由 view 层完成)。
设计动机per CONTEXT.md D-Serializer
- 脱敏放 view 层不放 serializerPUT 路径需要明文走 is_valid + saveserializer
不应承担"既要明文又要脱敏"的双重责任。
- app_id / access_token 在模型层 blank=True, default='',对应 serializer 配
allow_blank=True, allow_null=False, required=False既允许空字符串覆写、又
拒绝 None缺字段时由 ModelSerializer 默认行为(用现有值兜底)。
- updated_at 由模型层 auto_now=True 自动维护read_only 双重保险。
"""
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},
}
```
**注意**
- 不要发明 `CredentialSlotReadSerializer` / `CredentialSlotWriteSerializer` 拆分两个类CONTEXT.md / RESEARCH.md "Alternatives Considered" 已锁定单一 serializer + view 层脱敏)
- 不要在 serializer 内写 `to_representation` 覆写做脱敏(同上锁定)
- 不要给 `app_id` / `access_token``allow_null=True`(与模型层 blank=True/default='' 不一致)
</action>
<verify>
<automated>
cd qy_lty && python -c "from aiapp.serializers import CredentialSlotSerializer; from aiapp.models import CredentialSlot; s = CredentialSlotSerializer(CredentialSlot.get_solo()); assert set(s.data.keys()) == {'app_id', 'access_token', 'updated_at'}, s.data.keys(); print('OK fields=', list(s.data.keys()))"
</automated>
</verify>
<acceptance_criteria>
- `python -c "from aiapp.serializers import CredentialSlotSerializer"` 无 ImportError
- serializer 实例化已有的 pk=1 记录后 `.data` 三键齐全:`app_id` / `access_token` / `updated_at`
- serializer.fields['updated_at'].read_only == True
- serializer.fields['app_id'].allow_blank == True 且 allow_null == False
- serializer.fields['access_token'].allow_blank == True 且 allow_null == False
- 文件不再含其它 serializer 类(仅追加 CredentialSlotSerializer
</acceptance_criteria>
<done>
`aiapp/serializers.py` 含 CredentialSlotSerializer 类,三字段对齐模型,`updated_at` read-onlyGET 用它返回 .data 含明文view 层稍后脱敏PUT 用它做 is_valid + save。
</done>
</task>
<task type="auto" tdd="false">
<name>Task 2新增 CredentialSlotAdminViewaiapp/views.py 末尾追加)</name>
<files>qy_lty/aiapp/views.py</files>
<read_first>
- 必读 qy_lty/aiapp/views.py:1-18顶部 import 区,确认现有 import 结构)
- 必读 qy_lty/aiapp/views.py:434-555RTCChatHistoryAPIView 完整段;本 view 1:1 复刻其单 URL 多方法骨架)
- 必读 qy_lty/userapp/views.py:705-823AdminEmailLoginView + AdminLogoutViewadmin-only 二次校验 `is_staff` 模板line 748-754
- 必读 qy_lty/userapp/views.py:722-730method-level @swagger_auto_schema 简洁样板)
- 必读 02-RESEARCH.md `Pattern 1` + `Pattern 4` + `Pitfall 1/2/3/5`
- 必读 qy_lty/common/swagger_utils.py 全文(确认 `get_standardized_response_schema` 签名)
- 必读 qy_lty/common/responses.py 全文(确认 `success_response` / `error_response` 签名)
</read_first>
<action>
**Step 1在 `qy_lty/aiapp/views.py` 顶部 import 区追加 / 修改三处**
修改第 5 行(追加 `CredentialSlot`
```python
# 修改前:
from .models import ChatMessage, Bot
# 修改后:
from .models import ChatMessage, Bot, CredentialSlot
```
修改第 7 行(追加 `CredentialSlotSerializer`
```python
# 修改前:
from .serializers import ChatMessageSerializer
# 修改后:
from .serializers import ChatMessageSerializer, CredentialSlotSerializer
```
在第 13 行(`from drf_yasg.utils import swagger_auto_schema` 已存在)之后追加两行(如果文件中已存在则跳过该行;只追加缺失的):
```python
from common.utils import mask_token
from common.swagger_utils import get_standardized_response_schema
```
**Step 2在 `qy_lty/aiapp/views.py` 文件末尾(紧跟 `RTCChatHistoryAPIView.delete` 之后)追加完整代码块**
```python
# ======================================================================
# Phase 2 — 通用凭据槽位管理端读写接口CRED-03 + CRED-04
# 1:1 复刻 RTCChatHistoryAPIView 的单 URL 多方法 APIView 风格
# ======================================================================
class CredentialSlotPutRequestSchema(serializers.Serializer):
"""drf-yasg 专用 — PUT 请求体 schema不参与实际写入校验仅给 swagger 看)。
实际写入校验由 CredentialSlotSerializer.is_valid 执行。
"""
app_id = serializers.CharField(
required=False, allow_blank=True,
help_text="第三方服务商分配的 APP ID明文写入缺省时保留原值"
)
access_token = serializers.CharField(
required=False, allow_blank=True,
help_text="第三方服务商访问令牌(明文写入;响应阶段会脱敏返回末 4 位)"
)
# 响应 data 子 schemaaccess_token 字段 description 显式标注脱敏掩码语义
_credential_slot_data_schema = openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'app_id': openapi.Schema(
type=openapi.TYPE_STRING,
description='第三方服务商分配的 APP ID明文',
),
'access_token': openapi.Schema(
type=openapi.TYPE_STRING,
description='Access Token 末 4 位脱敏掩码(如 "*********1234",前缀字符数 = 原长 - 4',
),
'updated_at': openapi.Schema(
type=openapi.TYPE_STRING,
format='date-time',
description='最近一次更新时间ISO 8601',
),
},
)
class CredentialSlotAdminView(APIView):
"""通用凭据槽位管理端读写接口admin token 鉴权)。
GET: 返回 access_token 末 4 位脱敏后的凭据槽位
PUT: 全字段覆写凭据槽位(空记录场景自动 get_or_create响应同样脱敏
"""
authentication_classes = [RedisTokenAuthentication]
permission_classes = [IsAuthenticated]
tags = ['通用凭据槽位(管理端)']
def _ensure_admin(self, request):
"""admin-only 二次校验:拒绝非 staff 用户(含普通 user token 持有者)。
per RESEARCH.md仓库零处 IsAdminTokenAuthenticated permission 类;
现有 AdminEmailLoginView (userapp/views.py:748-754) / AdminLogoutView 一律走
视图内 is_staff 检查。统一沿用此模式,不发明新 permission 类。
"""
if not request.user.is_staff:
logger.warning(
f"Non-admin user attempted CredentialSlot admin endpoint: user_id={request.user.id}"
)
return error_response(
message="需要管理员权限",
code=403,
status_code=status.HTTP_403_FORBIDDEN,
)
return None
def _build_response_data(self, instance):
"""构造脱敏后的响应 data 字典。
per CONTEXT.mdGET 与 PUT 响应都必须脱敏 access_token避免运营在
admin UI 看到自己刚提交的明文回显CONTEXT.md 决策"PUT 响应也走脱敏")。
"""
serializer = CredentialSlotSerializer(instance)
data = dict(serializer.data)
# 关键脱敏点:用 instance.access_token明文走 mask_token覆盖 serializer.data 里的明文
data['access_token'] = mask_token(instance.access_token)
return data
@swagger_auto_schema(
operation_description="读取通用凭据槽位access_token 末 4 位脱敏返回admin token 鉴权)",
responses={
200: openapi.Response('读取成功', get_standardized_response_schema(_credential_slot_data_schema)),
401: openapi.Response('未提供有效 token', get_standardized_response_schema()),
403: openapi.Response('需要管理员权限', get_standardized_response_schema()),
},
security=[{'Bearer': []}],
tags=['通用凭据槽位(管理端)'],
)
def get(self, request):
forbidden = self._ensure_admin(request)
if forbidden:
return forbidden
instance = CredentialSlot.get_solo()
data = self._build_response_data(instance)
return success_response(data=data, message="读取成功")
@swagger_auto_schema(
request_body=CredentialSlotPutRequestSchema,
operation_description="全字段覆写通用凭据槽位admin token 鉴权;写入后响应脱敏返回)",
responses={
200: openapi.Response('更新成功', get_standardized_response_schema(_credential_slot_data_schema)),
400: openapi.Response('参数无效', get_standardized_response_schema()),
401: openapi.Response('未提供有效 token', get_standardized_response_schema()),
403: openapi.Response('需要管理员权限', get_standardized_response_schema()),
},
security=[{'Bearer': []}],
tags=['通用凭据槽位(管理端)'],
)
def put(self, request):
forbidden = self._ensure_admin(request)
if forbidden:
return forbidden
instance = CredentialSlot.get_solo() # 空记录场景自动 get_or_create
serializer = CredentialSlotSerializer(instance, data=request.data)
if not serializer.is_valid():
return error_response(
message="参数无效",
code=400,
status_code=status.HTTP_400_BAD_REQUEST,
data=serializer.errors,
)
serializer.save() # auto_now 自动刷 updated_at
# 重新读取 instance.access_tokenserializer.save() 后 instance 已被同步刷新;
# _build_response_data 内部会再次 dict(serializer.data) 拿最新 OrderedDict。
data = self._build_response_data(instance)
return success_response(data=data, message="凭据已更新")
```
**关键约束(不要违反)**
- 不要把 view 写成 `RetrieveUpdateAPIView` 子类仓库零先例per RESEARCH.md "Alternatives Considered"
- 不要直接 `return Response({...}, status=200)`;一律走 `success_response` / `error_response`(避免 middleware 二次包装的不确定行为per Pitfall 2
- 不要在 PUT 路径忘记脱敏:`return success_response(data=serializer.data)` 直接返回是 BUG —— `serializer.data` 含明文 access_tokenper Pitfall 3
- 不要新增 `IsAdminTokenAuthenticated` permission 类仓库零先例与现有约定相悖per RESEARCH.md "Anti-Patterns"
- 不要把 `_ensure_admin` 改成 `permission_classes = [IsAdminUser]`DRF 自带 IsAdminUser 也是 is_staff但与本仓库"视图内手写 is_staff"统一约定不一致)
</action>
<verify>
<automated>
cd qy_lty && python -c "from aiapp.views import CredentialSlotAdminView; v = CredentialSlotAdminView(); assert hasattr(v, 'get') and hasattr(v, 'put'); from userapp.authentication import RedisTokenAuthentication; assert RedisTokenAuthentication in CredentialSlotAdminView.authentication_classes; print('OK view loaded with auth')"
</automated>
</verify>
<acceptance_criteria>
- `python -c "from aiapp.views import CredentialSlotAdminView"` 无 ImportError
- View 类拥有 `get` / `put` / `_ensure_admin` / `_build_response_data` 方法
- `CredentialSlotAdminView.authentication_classes``RedisTokenAuthentication`
- `CredentialSlotAdminView.permission_classes``IsAuthenticated`(不是 IsAdminUser、不是自定义 admin permission
- grep `qy_lty/aiapp/views.py``mask_token(instance.access_token)` 至少一次(脱敏调用点)
- grep `qy_lty/aiapp/views.py``if not request.user.is_staff` 至少一次admin 二次校验)
- grep `qy_lty/aiapp/views.py` 不含 `RetrieveUpdateAPIView`(不准走 DRF 通用 view
- grep `qy_lty/aiapp/views.py` 不含 `IsAdminTokenAuthenticated`(不准发明新 permission 类)
- swagger 装饰器在 GET 与 PUT 各挂一份grep `@swagger_auto_schema` 出现次数比改动前多 2
</acceptance_criteria>
<done>
`aiapp/views.py` 末尾追加 `CredentialSlotAdminView` 类(含 GET/PUT 两方法 + admin 二次校验 + 脱敏 helper顶部 import 已对齐view 可被 import 不报错。
</done>
</task>
<task type="auto" tdd="false">
<name>Task 3注册 URLuserapp/admin_urls.py 追加 path</name>
<files>qy_lty/userapp/admin_urls.py</files>
<read_first>
- 必读 qy_lty/userapp/admin_urls.py 全文11 行;现有 login/logout 两条 path
- 必读 qy_lty/qy_lty/urls.py:59`path('v1/admin/', include('userapp.admin_urls'))` — 确认 prefix 拼接:`/api/v1/admin/credential-slot/`
- 必读 02-RESEARCH.md `Pitfall 4`admin_urls.py 漏注册导致 404
</read_first>
<action>
**完整修改后 `qy_lty/userapp/admin_urls.py` 内容(直接覆写)**
```python
from django.urls import path
from .views import AdminEmailLoginView, AdminLogoutView
# Phase 2 — 通用凭据槽位管理端读写接口CRED-03 + CRED-04
from aiapp.views import CredentialSlotAdminView
# 管理员专用API路径
urlpatterns = [
# 管理员登录
path('login/', AdminEmailLoginView.as_view(), name='admin_login'),
# 管理员登出
path('logout/', AdminLogoutView.as_view(), name='admin_logout'),
# 通用凭据槽位GET 脱敏读取 / PUT 全字段覆写admin token 鉴权)
path('credential-slot/', CredentialSlotAdminView.as_view(), name='admin_credential_slot'),
# 后续可以添加更多管理员专用接口
]
```
**关键约束**
- import 必须从 `aiapp.views` 引入(凭据槽位 view 落地于 aiapp不是 userapp
- 路径必须是 `'credential-slot/'`trailing slash + 中划线,对齐 CONTEXT.md "trailing slash 沿用 Django 默认风格"
- name 必须是 `'admin_credential_slot'`reverse 时使用)
- 不要在 `aiapp/urls.py` 重复注册CONTEXT.md 锁定路由汇总点是 userapp/admin_urls.py
</action>
<verify>
<automated>
cd qy_lty && python -c "from django.urls import reverse; from django.conf import settings; import django; django.setup() if not settings.configured else None; from django.urls import get_resolver; r = get_resolver(); url = reverse('admin_credential_slot'); assert url == '/api/v1/admin/credential-slot/', f'got {url}'; print('OK url=', url)" 2>&1 || (cd qy_lty && DJANGO_SETTINGS_MODULE=qy_lty.settings python -c "import django; django.setup(); from django.urls import reverse; print('url=', reverse('admin_credential_slot'))")
</automated>
</verify>
<acceptance_criteria>
- `python manage.py check` 无 URL 注册类报错
- `reverse('admin_credential_slot')` 返回 `/api/v1/admin/credential-slot/`
- `qy_lty/userapp/admin_urls.py``from aiapp.views import CredentialSlotAdminView`
- `qy_lty/userapp/admin_urls.py` 不含将凭据槽位重复注册到 `aiapp/urls.py` 的逻辑
- `qy_lty/aiapp/urls.py` 内**未**新增 credential-slot 注册(汇总点单一)
</acceptance_criteria>
<done>
`/api/v1/admin/credential-slot/` URL 可被 reverse 解析到 `CredentialSlotAdminView`Django check 通过;与 login/logout 在同一 admin namespace 注册块内。
</done>
</task>
</tasks>
<verification>
本 plan 三个 task 完成后,做一轮 plan 内自验(不替代 02-02-PLAN 的端到端 verify
1. **import 链路完整**`python -c "from aiapp.views import CredentialSlotAdminView; from aiapp.serializers import CredentialSlotSerializer"` 同行无 ImportError
2. **URL 路由生效**`python manage.py shell -c "from django.urls import reverse; print(reverse('admin_credential_slot'))"` 输出 `/api/v1/admin/credential-slot/`
3. **Django check 通过**`python manage.py check` 无 ERRORS / WARNINGSW 级 noise 可忽略)
4. **Swagger schema 暴露(运行时验证留 02-02**:本 plan 不启动 daphne但通过 import 检查保证 `@swagger_auto_schema` 装饰器无语法错误
**reachability self-check**goal-backward
- truth #1GET 脱敏 200→ artifact: views.py CredentialSlotAdminView.get + serializers.py CredentialSlotSerializer + admin_urls.py path → reachable ✓
- truth #2PUT 全字段覆写 + updated_at 刷新 + 响应脱敏)→ artifact: views.py CredentialSlotAdminView.putserializer.save + _build_response_data→ reachable ✓
- truth #3PUT 在空记录场景 get_or_create→ artifact: CredentialSlot.get_solo()Phase 1 落地,本 plan 调用)→ reachable ✓
- truth #4(无 token → 401→ artifact: RedisTokenAuthenticationPhase 0 已在)+ DRF NotAuthenticated → reachable ✓
- truth #5user token → 403→ artifact: views.py `_ensure_admin` is_staff 校验 → reachable ✓
- truth #6swagger 路径条目 + access_token 脱敏 description→ artifact: views.py @swagger_auto_schema + _credential_slot_data_schema description → reachable ✓
</verification>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| 公网 → /api/v1/admin/credential-slot/ | 来自 qy-lty-admin Web UI 的 HTTP 请求untrusted Authorization headeruntrusted PUT body |
| view → DBCredentialSlot 表) | 内部信任边界;写入前必须经过 serializer.is_valid + admin 校验 |
| view → 响应回 admin 端 | 出站脱敏边界access_token 必须经 mask_token 才能离开后端 |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-02-01 | Spoofing | Authorization header | mitigate | RedisTokenAuthentication 走 Redis admin_token:{token} key 验证TTL 30 天token 验证失败抛 AuthenticationFailed → 401 |
| T-02-02 | Elevation of Privilege | user token 持有者调 admin 端点 | mitigate | view 内 `_ensure_admin` 早返回 403CONTEXT.md success #5 的核心防线 |
| T-02-03 | Information Disclosure | GET 响应明文 access_token | mitigate | `_build_response_data` 强制走 `mask_token(instance.access_token)`serializer 不在 to_representation 内做脱敏避免责任分散 |
| T-02-04 | Information Disclosure | PUT 响应回显明文 access_token | mitigate | PUT 路径 save() 后同样调 `_build_response_data`;不直接 return success_response(data=serializer.data) — 该写法会回显明文per Pitfall 3 |
| T-02-05 | Tampering | mass assignment未声明字段 | mitigate | DRF ModelSerializer 默认拒绝未在 Meta.fields 声明的字段fields=['app_id', 'access_token', 'updated_at'] 三字段封死updated_at read_only 双重保险 |
| T-02-06 | Tampering | PUT 重放 / 幂等性 | accept | 全字段覆写本身幂等(重放结果一致);不需要 ETag / If-Match |
| T-02-07 | Denial of Service | 暴力 PUT 消耗 DB | accept | 现有架构无限流PROJECT.md candidate priorities #2 跟踪);本 phase 不引入新依赖deferred ideas 已明确 |
| T-02-08 | Information Disclosure | access_token 写入 access log请求体 / 响应体) | accept本 phase+ transferPhase 3 | 本 phase 仅做响应脱敏;阿里云 access log 链路过滤由 Phase 3 落地CRED-06Phase 3 完成后此项关闭 |
</threat_model>
<success_criteria>
本 plan 落地完成的标志plan 内自验):
- [ ] `qy_lty/aiapp/serializers.py` 顶部 `from .models import ChatMessage, CredentialSlot`,文件内含 `class CredentialSlotSerializer(serializers.ModelSerializer):`
- [ ] `qy_lty/aiapp/views.py` 顶部 import 区含 `CredentialSlot` / `CredentialSlotSerializer` / `mask_token` / `get_standardized_response_schema`
- [ ] `qy_lty/aiapp/views.py` 文件末尾含 `class CredentialSlotAdminView(APIView):`,含 GET/PUT 两个方法及各自 `@swagger_auto_schema` 装饰器
- [ ] `qy_lty/userapp/admin_urls.py``path('credential-slot/', CredentialSlotAdminView.as_view(), name='admin_credential_slot')`
- [ ] `python manage.py check` 通过
- [ ] `reverse('admin_credential_slot')` 解析到 `/api/v1/admin/credential-slot/`
- [ ] grep `aiapp/views.py``mask_token(instance.access_token)` ≥ 1 次(脱敏调用点)
- [ ] grep `aiapp/views.py``if not request.user.is_staff` ≥ 1 次admin 二次校验)
- [ ] grep `aiapp/views.py` 不含 `RetrieveUpdateAPIView`(确认未走通用 view
</success_criteria>
<output>
完成后创建 `.planning/phases/02-admin-rest/02-01-SUMMARY.md`,记录:
- 改动文件清单aiapp/serializers.py / aiapp/views.py / userapp/admin_urls.py
- 实际落地的 view 类全名 / 路由 path / 调用 mask_token 的位置
- 任何与本 PLAN 不一致的偏离(应为零;如有偏离说明原因)
- 留给 02-02 的端到端 verify hookcurl 命令模板 + Django shell 程序化验收脚本片段)
</output>