465 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: 02
type: execute
wave: 2
depends_on:
- "02-01"
files_modified:
- qy_lty/docs/修改记录.md
- qy-lty-admin/docs/修改记录.md
autonomous: true
requirements:
- CRED-03
- CRED-04
must_haves:
truths:
- "qy_lty/docs/修改记录.md 顶部新增一条 Phase 2 条目,跨项目联动字段引用 qy-lty-admin/docs/修改记录.md 的同期条目"
- "qy-lty-admin/docs/修改记录.md 顶部新增一条 Phase 2 条目,服务端联动字段引用 qy_lty/docs/修改记录.md 的同期条目(互引闭环)"
- "端到端 curl 验收GET 携 admin token 返回 200 + 标准壳层 + access_token 脱敏(末 4 位明文 + 前缀 *"
- "端到端 curl 验收PUT 携 admin token + body 后 DB 全字段覆写 + updated_at 刷新 + 响应 access_token 脱敏"
- "端到端 curl 验收:无 token → 401 + 标准壳层user token → 403 + message=需要管理员权限 + 标准壳层"
- "Swagger schema 暴露:/swagger.json 含 /api/v1/admin/credential-slot/ 路径条目,含 GET/PUT 两 methodaccess_token 字段 description 标注脱敏掩码语义"
artifacts:
- path: "qy_lty/docs/修改记录.md"
provides: "Phase 2 修改记录条目(含跨项目联动字段)"
contains: "Phase 2"
- path: "qy-lty-admin/docs/修改记录.md"
provides: "Phase 2 互引条目(含服务端联动字段)"
contains: "Phase 2"
key_links:
- from: "qy_lty/docs/修改记录.md (Phase 2 条目的跨项目联动字段)"
to: "qy-lty-admin/docs/修改记录.md (Phase 2 同期条目)"
via: "条目内文字引用 ../qy-lty-admin/docs/修改记录.md 同期条目"
pattern: "qy-lty-admin/docs/修改记录"
- from: "qy-lty-admin/docs/修改记录.md (Phase 2 条目的服务端联动字段)"
to: "qy_lty/docs/修改记录.md (Phase 2 同期条目)"
via: "条目内文字引用 ../qy_lty/docs/修改记录.md 同期条目"
pattern: "qy_lty/docs/修改记录"
---
<objective>
本 plan 完成 Phase 2 收尾:
1. 端到端验收 02-01 落地的 GET/PUT 接口curl + Django shell test client 双层验证 8 条 success criteria
2. 在 qy_lty + qy-lty-admin 两端 `docs/修改记录.md` 顶部各写一条 Phase 2 条目,且**跨项目联动字段相互引用**CLAUDE.md 强制规则Phase 2 是首次跨项目接口契约落地)
Purpose
- 端到端验收:把 02-01 写出来的 view + serializer + URL 真正跑起来,证明三处 success criteriaGET 脱敏 / PUT 覆写 + 脱敏响应 / 鉴权拒绝矩阵)在生产路径成立
- 跨项目互引:给后续在 qy-lty-admin 起 CRED-FE-01 phase 时,前端能从修改记录直接定位到本 phase 的接口契约文档;给本仓库未来回查"这个接口什么时候上的、谁在消费"留一个反向锚点
Output
- `qy_lty/docs/修改记录.md` 顶部新增一条 Phase 2 条目5 字段 + 跨项目联动字段)
- `qy-lty-admin/docs/修改记录.md` 顶部新增一条 Phase 2 条目5 字段 + 服务端联动字段)
- VERIFICATION.md 补充端到端 curl + Django shell 验收记录(在 SUMMARY 中归档)
</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/phases/02-admin-rest/02-CONTEXT.md
@.planning/phases/02-admin-rest/02-RESEARCH.md
@.planning/phases/02-admin-rest/02-01-PLAN.md
# 02-01 落地后的 SUMMARY含端到端 verify hook
@.planning/phases/02-admin-rest/02-01-SUMMARY.md
# 修改记录格式 + 已落地条目模板
@qy_lty/docs/修改记录.md
@qy-lty-admin/docs/修改记录.md
# 项目宪法(修改记录强制 + 跨项目互引规则)
@qy_lty/CLAUDE.md
# 鉴权 / 工具入口(用于端到端验收脚本)
@qy_lty/userapp/utils.py
@qy_lty/userapp/authentication.py
@qy_lty/aiapp/models.py
@qy_lty/common/utils.py
</context>
<tasks>
<task type="auto" tdd="false">
<name>Task 1端到端 curl + Django shell 验收8 条 success criteria</name>
<files>qy_lty/.planning/phases/02-admin-rest/02-VERIFICATION.md</files>
<read_first>
- 必读 .planning/phases/02-admin-rest/02-01-SUMMARY.md02-01 留给本 task 的 hook
- 必读 .planning/phases/02-admin-rest/02-CONTEXT.md `<specifics>`8 条验收点表格)
- 必读 .planning/phases/02-admin-rest/02-RESEARCH.md `Validation Architecture`test client 程序化验收脚本片段 + 采样频率)
- 必读 qy_lty/userapp/utils.py:33-37generate_token 签发 admin / user token 的方式)
- 必读 qy_lty/aiapp/models.py 的 CredentialSlot.get_solo验证 PUT 在空记录场景的 get_or_create
- 必读 .planning/phases/01-credential-data-layer/01-VERIFICATION.mdPhase 1 程序化验收风格,本 task 1:1 沿用)
</read_first>
<action>
**目标**:在 Django shell + Daphne / runserver 两条路径都验完 8 条 success criteria留一份证据归档进 02-VERIFICATION.md。
**Step 1准备测试 token一次性 Django shell 脚本)**
```bash
cd qy_lty && python manage.py shell <<'EOF'
from userapp.models import ParadiseUser
from userapp.utils import generate_token
import json
# 取一个已有 staff 用户(必须 is_staff=True如全库无 staff先 createsuperuser
admin_user = ParadiseUser.objects.filter(is_staff=True).first()
assert admin_user is not None, "需要至少一个 is_staff=True 用户;运行 python manage.py createsuperuser"
admin_token = generate_token(admin_user.id, is_admin=True)
# 取一个普通用户is_staff=False如不存在可创建一个 phone-only 用户用于本验收)
user = ParadiseUser.objects.filter(is_staff=False).first()
if user is None:
user = ParadiseUser.objects.create(username='phase2_verify_user_temp', is_staff=False)
user_token = generate_token(user.id, is_admin=False)
print(json.dumps({
'admin_user_id': admin_user.id,
'admin_token': admin_token,
'user_id': user.id,
'user_token': user_token,
}, ensure_ascii=False))
EOF
```
记录输出的 `admin_token` / `user_token` 备用。**注意**token 写入 Redis 后 30 天内有效;本 task 验完无需清理(可让其自然过期)。
**Step 2Django test client 程序化验收(无需启动服务进程,全部在 shell 内跑)**
```bash
cd qy_lty && python manage.py shell <<'EOF'
from django.test import Client
from aiapp.models import CredentialSlot
from common.utils import mask_token
import json
ADMIN_TOKEN = "<填入 Step 1 拿到的 admin_token>"
USER_TOKEN = "<填入 Step 1 拿到的 user_token>"
URL = "/api/v1/admin/credential-slot/"
c = Client()
# === 验收点 1GET 携 admin token 返回脱敏 ===
r = c.get(URL, HTTP_AUTHORIZATION=f"Bearer {ADMIN_TOKEN}")
body = r.json()
assert r.status_code == 200, f"#1 status={r.status_code} body={body}"
assert body['success'] is True
assert body['code'] == 200
assert 'data' in body
assert {'app_id', 'access_token', 'updated_at'} <= set(body['data'].keys())
# access_token 必须脱敏要么是空DB 为空时),要么以 * 开头且末 4 位为原 token 末 4 位
slot = CredentialSlot.get_solo()
expected_masked = mask_token(slot.access_token)
assert body['data']['access_token'] == expected_masked, f"#1 mask mismatch: got={body['data']['access_token']!r} expected={expected_masked!r}"
print("#1 PASS GET admin -> 200 + masked access_token")
# === 验收点 2PUT 携 admin token 全字段覆写 + 响应脱敏 ===
new_token = "sk-phase2_verify_secret_ABCD1234"
r = c.put(URL, data=json.dumps({"app_id": "phase2_app", "access_token": new_token}),
content_type="application/json", HTTP_AUTHORIZATION=f"Bearer {ADMIN_TOKEN}")
body = r.json()
assert r.status_code == 200, f"#2 status={r.status_code} body={body}"
assert body['success'] is True
slot = CredentialSlot.get_solo()
assert slot.app_id == "phase2_app"
assert slot.access_token == new_token # DB 中明文存储
# 响应 access_token 必须脱敏:'sk-phase2_verify_secret_ABCD1234' -> '*****************************1234'
assert body['data']['access_token'] == mask_token(new_token), f"#2 PUT response not masked: {body['data']['access_token']!r}"
assert body['data']['access_token'].endswith('1234'), "末 4 位必须为 1234"
assert body['data']['access_token'].startswith('*'), "脱敏前缀必须以 * 开头"
print("#2 PASS PUT admin -> 200 + DB updated + response masked")
# === 验收点 3PUT 在空记录场景自动 get_or_create ===
CredentialSlot.objects.all().delete()
r = c.put(URL, data=json.dumps({"app_id": "after_delete", "access_token": "tok-XYZ9"}),
content_type="application/json", HTTP_AUTHORIZATION=f"Bearer {ADMIN_TOKEN}")
body = r.json()
assert r.status_code == 200, f"#3 status={r.status_code} body={body}"
slot = CredentialSlot.get_solo()
assert slot.app_id == "after_delete"
assert slot.access_token == "tok-XYZ9"
print("#3 PASS PUT 空记录场景 -> 200 + get_or_create 创建并写入")
# === 验收点 4无 Authorization 头 -> 401 + 标准壳层 ===
r = c.get(URL)
body = r.json()
assert r.status_code == 401, f"#4 status={r.status_code} body={body}"
assert body['success'] is False
assert body['code'] == 401
assert 'message' in body # 中间件 / DRF 至少要给一个 message 字段
print("#4 PASS no token -> 401 + 标准壳层")
# === 验收点 5携普通 user token -> 403 + 标准壳层 + message 含管理员关键字 ===
r = c.get(URL, HTTP_AUTHORIZATION=f"Bearer {USER_TOKEN}")
body = r.json()
assert r.status_code == 403, f"#5 status={r.status_code} body={body}"
assert body['success'] is False
assert body['code'] == 403
assert "管理员" in body['message'], f"#5 message 不含'管理员'关键字: {body['message']!r}"
print("#5 PASS user token -> 403 + 需要管理员权限")
# === 验收点 6PUT 携 user token -> 403同 #5 路径,验证 PUT 也走 _ensure_admin ===
r = c.put(URL, data=json.dumps({"app_id": "should_fail"}),
content_type="application/json", HTTP_AUTHORIZATION=f"Bearer {USER_TOKEN}")
body = r.json()
assert r.status_code == 403, f"#6 PUT user token status={r.status_code}"
print("#6 PASS PUT user token -> 403")
print("\n========== 全部 6 条 test client 验收通过 ==========")
EOF
```
**Step 3Swagger schema 验收(启动 daphne 后做一次 curl**
```bash
# 启动服务(如已运行可跳过)
# cd qy_lty && daphne -b 0.0.0.0 -p 8000 qy_lty.asgi:application &
curl -s http://localhost:8000/swagger.json | python -c "
import json, sys
schema = json.load(sys.stdin)
paths = schema.get('paths', {})
key = '/api/v1/admin/credential-slot/'
assert key in paths, f'#7 {key} 未出现在 swagger paths 中'
methods = paths[key]
assert 'get' in methods, '#7 GET 方法缺失'
assert 'put' in methods, '#7 PUT 方法缺失'
# 验证 access_token description 含脱敏关键字
get_resp = methods['get'].get('responses', {}).get('200', {})
schema_str = json.dumps(get_resp, ensure_ascii=False)
assert '脱敏' in schema_str or '末 4 位' in schema_str or 'mask' in schema_str.lower(), '#7 access_token description 不含脱敏掩码语义'
print('#7 PASS swagger paths 含 GET/PUT 两 method + access_token 脱敏描述')
"
```
**Step 4把 8 条验收结果写入 `.planning/phases/02-admin-rest/02-VERIFICATION.md`**
文件全文模板(直接 Write
```markdown
# Phase 2 Verification — 管理端读写接口端到端验收
**Verified**: 2026-05-07
**Phase**: 02-admin-rest
**Plan**: 02-02
**Coverage**: CRED-03 + CRED-04ROADMAP Phase 2 全部 4 条 success criteria
---
## 验收摘要
| # | 验收点 | 方法 | 结果 |
|---|--------|------|------|
| 1 | GET 携 admin token 返回脱敏壳层 | Django test client | ✓ PASS |
| 2 | PUT 携 admin token 全字段覆写 + 响应脱敏 | Django test client | ✓ PASS |
| 3 | PUT 在空记录场景自动 get_or_create | Django test client手动 delete + PUT | ✓ PASS |
| 4 | 无 Authorization 头 → 401 + 标准壳层 | Django test client | ✓ PASS |
| 5 | 携普通 user token → 403 + 需要管理员权限 | Django test client | ✓ PASS |
| 6 | PUT 携 user token → 403验证 PUT 也走 _ensure_admin | Django test client | ✓ PASS |
| 7 | /swagger.json 含路径条目 + GET/PUT 两 method + 脱敏 description | curl | ✓ PASS |
| 8 | 修改记录两端互引02-02 Task 2 落地后追加) | 文件 grep | ⏳ 待 02-02 Task 2 |
---
## 证据片段
[黏贴 Step 1 / Step 2 / Step 3 输出的关键行]
---
*由 02-02-PLAN.md Task 1 生成*
```
**关键约束**
- 6 条 test client 验收必须**全部 PASS** 才能进 02-02 Task 2任何一条 FAIL 则回到 02-01 修 view / serializer / URL
- Swagger 验收(#7)若 daphne 未启动可暂时记 "deferred to integration",但必须在 phase gate 前补做
- #8 在本 task 仅占位(标 ⏳Task 2 落地修改记录后**回写**为 ✓ PASS02-VERIFICATION.md 在 Task 2 末尾再 Edit 一次)
- 不要把 admin / user token 明文写进 02-VERIFICATION.mdRedis 30 天 TTL落进 git 的 token 在 30 天内仍有效,是新的泄露面)
</action>
<verify>
<automated>
cd qy_lty && python manage.py shell -c "from aiapp.models import CredentialSlot; from common.utils import mask_token; slot = CredentialSlot.get_solo(); print('DB state ok:', slot.app_id, mask_token(slot.access_token))" && test -f .planning/phases/02-admin-rest/02-VERIFICATION.md && grep -q "PASS" .planning/phases/02-admin-rest/02-VERIFICATION.md && echo OK
</automated>
</verify>
<acceptance_criteria>
- Step 2 脚本输出"========== 全部 6 条 test client 验收通过 =========="
- Step 3 输出 "#7 PASS"(如 daphne 未启动则记 deferred 但必须在 SUMMARY 中列出补做计划)
- 文件 `.planning/phases/02-admin-rest/02-VERIFICATION.md` 存在
- 文件含至少 6 处 "PASS"(验收点 1-6
- 文件不含 admin / user token 明文grep `Bearer ` 应只出现在脚本模板段落、不含具体 UUID 字串)
- DB 状态符合预期:`CredentialSlot.get_solo()` 返回的 app_id 与 access_token 是 Step 2 写入的最新值
</acceptance_criteria>
<done>
8 条 success criteria 中 6 条 test client + 1 条 swagger 已验完并归档DB 状态稳定02-VERIFICATION.md 是 Phase 2 收尾时 ROADMAP 标记 ✓ Complete 的证据来源。
</done>
</task>
<task type="auto" tdd="false">
<name>Task 2两端修改记录互引条目qy_lty + qy-lty-admin</name>
<files>qy_lty/docs/修改记录.md, qy-lty-admin/docs/修改记录.md</files>
<read_first>
- 必读 qy_lty/docs/修改记录.md:1-90确认头部「修改格式说明」+ 第 24 行 `<!-- 新的修改记录添加在此处下方,最新的在最前面 -->` 注释 + Phase 1 已有两条条目作模板)
- 必读 qy-lty-admin/docs/修改记录.md:1-58确认头部「修改格式说明」+ 第 26 行 `<!-- 新的修改记录添加在此处下方,最新的在最前面 -->` + 第 28-44 行 NEXT_PUBLIC_API_BASE_URL 修复条目作"含跨项目说明的"模板)
- 必读 qy_lty/CLAUDE.md `## 项目修改记录规则(重要 — 自动执行)`5 字段格式 + 跨项目联动两端各一条互引规则)
- 必读 .planning/phases/02-admin-rest/02-01-SUMMARY.mdPhase 2 实际改动文件清单 + 接口契约)
- 必读 02-CONTEXT.md `<decisions>` 跨项目联动段(互引文案要点)
</read_first>
<action>
**Step 1在 `qy_lty/docs/修改记录.md` 顶部(紧跟第 24 行注释 `<!-- 新的修改记录添加在此处下方,最新的在最前面 -->` 之下、Phase 1 第一条 `### [2026-05-07] Phase 1 — Django Admin 注册凭据槽位...` 之上)插入条目**
```markdown
### [2026-05-07] Phase 2 — 管理端通用凭据槽位 REST 接口GET 脱敏 / PUT 覆写)
配套 Phase[.planning/phases/02-admin-rest/](.planning/phases/02-admin-rest/)
覆盖需求CRED-03 + CRED-04
设计参考1:1 复刻 `aiapp.views.RTCChatHistoryAPIView``aiapp/views.py:434-555`)的单 URL 多方法 APIView 风格
- **文件路径**:
- `aiapp/serializers.py`(修改 — 顶部 import 追加 `CredentialSlot`,文件末尾追加 `CredentialSlotSerializer` ModelSerializer 类)
- `aiapp/views.py`(修改 — 顶部 import 追加 `CredentialSlot` / `CredentialSlotSerializer` / `mask_token` / `get_standardized_response_schema`;文件末尾追加 `CredentialSlotPutRequestSchema` swagger 请求体 + `_credential_slot_data_schema` 响应 data schema + `CredentialSlotAdminView` APIView 类)
- `userapp/admin_urls.py`(修改 — 追加 `from aiapp.views import CredentialSlotAdminView``path('credential-slot/', CredentialSlotAdminView.as_view(), name='admin_credential_slot')`
- **修改类型**: 新增
- **修改内容**:
- 暴露 `GET /api/v1/admin/credential-slot/`admin token 鉴权(`RedisTokenAuthentication` + 视图内 `is_staff` 二次校验,不发明 admin-only permission 类);返回 `{ success, code, message, data: { app_id, access_token: <末 4 位脱敏掩码>, updated_at } }`,脱敏由 view 层调 `common.utils.mask_token` 完成serializer 不参与脱敏,避免双重责任)
- 暴露 `PUT /api/v1/admin/credential-slot/`admin token 鉴权;接受 `{ app_id, access_token }` 全字段覆写;空记录场景自动走 `CredentialSlot.get_solo()``get_or_create(pk=1)`;写入后 `updated_at``auto_now=True` 自动刷新;响应同样脱敏 access_token避免运营在 admin UI 看到自己刚提交的明文回显)
- 鉴权拒绝矩阵:无 token → 401DRF NotAuthenticated → middleware 兜底标准壳层);持普通 user token非 staff→ 403 + `message="需要管理员权限"`
- Swagger / ReDoc 自动暴露method-level `@swagger_auto_schema` 装饰器;响应 schema 配 `common.swagger_utils.get_standardized_response_schema()`access_token 字段 description 显式标注「Access Token 末 4 位脱敏掩码(如 "*********1234")」,避免前端误解为明文
- 不引入新依赖(沿用 Django 4.2.13 + DRF + drf-yasg + Phase 1 落地的 `CredentialSlot.get_solo` / `mask_token`
- **修改原因**: Milestone v1.0「通用凭据槽位APP ID + Access Token」Phase 2 — 给管理后台前端qy-lty-admin暴露受控的凭据读写入口让运营无需进 Django Admin 也能管理凭据GET 与 PUT 响应均脱敏,避免明文经管理端 UI / 浏览器 devtools / 阿里云日志GET 响应体路径)泄露;为 Phase 3 客户端明文 GET 接口 + 阿里云日志 formatter 提供"接口已上线、凭据可写入"的稳定起点
- **跨项目联动**: 前端联动条目 [qy-lty-admin/docs/修改记录.md](../../qy-lty-admin/docs/修改记录.md) 同期 `[2026-05-07] Phase 2 — 锁定后端通用凭据槽位 REST 接口契约(消费方文档化)`。本 phase 是 Milestone v1.0 首次跨项目接口契约落地:本仓库(服务端)暴露 `/api/v1/admin/credential-slot/` GET/PUT前端 `qy-lty-admin` 后续 phase 将基于该契约写 API client含 React Hooks 调用 + 表单录入 UI。前后端各自维护独立修改记录本条与对方条目互相引用便于未来回查接口的双向上下游
```
**Step 2在 `qy-lty-admin/docs/修改记录.md` 顶部(紧跟第 26 行注释 `<!-- 新的修改记录添加在此处下方,最新的在最前面 -->` 之下、第 28 行 `### [2026-05-07] 修复 NEXT_PUBLIC_API_BASE_URL...` 之上)插入条目**
```markdown
### [2026-05-07] Phase 2 — 锁定后端通用凭据槽位 REST 接口契约(消费方文档化)
配套服务端 Phase[../qy_lty/.planning/phases/02-admin-rest/](../../qy_lty/.planning/phases/02-admin-rest/)
覆盖服务端需求CRED-03 + CRED-04本仓库消费方
- **文件路径**: `docs/修改记录.md`(仅文档新增条目;本仓库代码未改)
- **修改类型**: 新增
- **修改内容**:
- 文档化服务端在本日落地的 `/api/v1/admin/credential-slot/` REST 接口契约GET 脱敏读取 + PUT 全字段覆写 + admin token 鉴权),为后续 Web 管理后台前端(本仓库)的 CRED-FE-* phase 写 API client 与表单 UI 留下契约锚点
- 接口契约要点(消费方视角):
- URL`{NEXT_PUBLIC_API_BASE_URL}/v1/admin/credential-slot/`
- 鉴权:`Authorization: Bearer <admin_token>`(来源于 `/api/v1/admin/login/` 的现有 admin 登录返回值)
- GET 响应:`{ success: boolean, code: number, message: string, data: { app_id: string, access_token: string /* 末 4 位脱敏掩码,前缀 * */, updated_at: string /* ISO 8601 */ } }`
- PUT 请求体:`{ app_id?: string, access_token?: string }`(任一字段缺省时由后端兜底保留原值;写入是全字段覆写语义,建议前端 UI 始终提交两字段全集以避免歧义)
- PUT 响应同 GET 形态access_token 同样脱敏返回,前端**不应**用响应值回填明文输入框)
- 错误矩阵401无 token / token 失效、403持非 admin tokenmessage 含"需要管理员权限"、400参数无效
- **修改原因**:
- 服务端首次为本管理后台暴露受控的凭据读写接口;本仓库即将启动 CRED-FE-01API client + CRED-FE-02表单录入页面等 phase先把后端契约固化进本仓库修改记录便于反查
- 文档化"GET 与 PUT 响应均脱敏 access_token"避免前端工程师误以为可以从响应回填明文表单(实际明文仅存于 DB任何回填只能保留掩码或要求运营重新输入
- **服务端联动**: 后端联动条目 [../qy_lty/docs/修改记录.md](../../qy_lty/docs/修改记录.md) 同期 `[2026-05-07] Phase 2 — 管理端通用凭据槽位 REST 接口GET 脱敏 / PUT 覆写)`。本仓库代码未改,仅文档侧做契约固化;待本仓库 CRED-FE-01 phase 启动落地 API client + Hook 时再补一条独立条目并互引
```
**Step 3互引校验写完后立即 grep**
```bash
# 验证两端互引字段都在
grep -n "qy-lty-admin/docs/修改记录" qy_lty/docs/修改记录.md | head -3
grep -n "qy_lty/docs/修改记录" qy-lty-admin/docs/修改记录.md | head -3
# 验证两边都标了 Phase 2 + 2026-05-07
grep -n "Phase 2.*2026-05-07\|2026-05-07.*Phase 2" qy_lty/docs/修改记录.md qy-lty-admin/docs/修改记录.md
```
**Step 4回写 02-VERIFICATION.md 验收点 #8**
把 Task 1 写入的 02-VERIFICATION.md 中验收点 #8 的状态从 `⏳ 待 02-02 Task 2` 改为 `✓ PASS`,并追加证据:
```
| 8 | 修改记录两端互引 | grep `qy-lty-admin/docs/修改记录` qy_lty/docs/修改记录.md ✓grep `qy_lty/docs/修改记录` qy-lty-admin/docs/修改记录.md ✓ | ✓ PASS |
```
**关键约束**
- 两条条目必须用日期 `2026-05-07`(与 Phase 1 / 当前 currentDate 一致;从 system context 得知)
- 两条条目都必须含**跨项目联动 / 服务端联动**字段,且字段值必须**指向对方文件路径**(不能写"无"Phase 1 那样的"暂无前端"逻辑在 Phase 2 不成立 —— 本 phase 就是首次跨项目接口契约落地)
- 服务端条目中的相对路径用 `../../qy-lty-admin/docs/修改记录.md`(位于 `qy_lty/docs/`,跳到 `qy_lty/` 上级 `Lila-Server/` 再进 `qy-lty-admin/docs/`
- 前端条目中的相对路径用 `../../qy_lty/docs/修改记录.md`(同理对称)
- 两条条目都要插在各自文件的「修改历史」段顶部(最新在最前),不要追加到末尾
- **不要**修改 Phase 1 已有的两条条目Phase 1 当时纯服务端、无前端互引是合理的,不要回头改成"互引"破坏历史归档)
- **不要**在 `qy-lty-admin` 仓库改任何代码(本 task 仅在 qy-lty-admin 仓库内动 docs 一个文件)
</action>
<verify>
<automated>
grep -c "Phase 2 — 管理端通用凭据槽位 REST 接口" "C:\Users\admin\Desktop\Lila-Server\qy_lty\docs\修改记录.md" && grep -c "Phase 2 — 锁定后端通用凭据槽位 REST 接口契约" "C:\Users\admin\Desktop\Lila-Server\qy-lty-admin\docs\修改记录.md" && grep -q "qy-lty-admin/docs/修改记录" "C:\Users\admin\Desktop\Lila-Server\qy_lty\docs\修改记录.md" && grep -q "qy_lty/docs/修改记录" "C:\Users\admin\Desktop\Lila-Server\qy-lty-admin\docs\修改记录.md" && echo OK
</automated>
</verify>
<acceptance_criteria>
- `qy_lty/docs/修改记录.md` 顶部「修改历史」段第一条标题为 `### [2026-05-07] Phase 2 — 管理端通用凭据槽位 REST 接口GET 脱敏 / PUT 覆写)`
- 该条目含 5 个标准字段(文件路径 / 修改类型 / 修改内容 / 修改原因 / 跨项目联动)
- 该条目"跨项目联动"字段含字串 `qy-lty-admin/docs/修改记录.md`(指向前端互引)
- `qy-lty-admin/docs/修改记录.md` 顶部「修改历史」段第一条标题为 `### [2026-05-07] Phase 2 — 锁定后端通用凭据槽位 REST 接口契约(消费方文档化)`
- 该条目含 5 个标准字段(文件路径 / 修改类型 / 修改内容 / 修改原因 / 服务端联动)
- 该条目"服务端联动"字段含字串 `qy_lty/docs/修改记录.md`(指向后端互引)
- 两条条目互相引用对方grep 双向均命中 → 闭环)
- Phase 1 的两条条目位置不变(位于本次新增条目之下)
- 02-VERIFICATION.md 中验收点 #8 已改为 ✓ PASS
</acceptance_criteria>
<done>
`qy_lty` + `qy-lty-admin` 两端 `docs/修改记录.md` 顶部各有一条 Phase 2 条目互相引用对方文件路径CLAUDE.md "qy_lty 与 qy-lty-admin 是独立项目,跨项目联动两端各写一条互相引用对方"规则在本 phase 闭环02-VERIFICATION.md 8 条全 PASS。
</done>
</task>
</tasks>
<verification>
本 plan 完成后做一轮 phase-gate 自验:
1. **8 条 success criteria 全 PASS**02-VERIFICATION.md 表格全部 ✓
2. **互引闭环**
- `grep "qy-lty-admin" qy_lty/docs/修改记录.md` ≥ 1 hit在 Phase 2 条目内)
- `grep "qy_lty" qy-lty-admin/docs/修改记录.md` ≥ 1 hit在 Phase 2 条目内)
3. **ROADMAP Phase 2 4 条 success criteria 已覆盖**
- SC#1 GET 脱敏 ← 验收点 #1
- SC#2 PUT 全字段覆写 + get_or_create ← 验收点 #2 + #3
- SC#3 鉴权拒绝矩阵 ← 验收点 #4 + #5 + #6
- SC#4 Swagger / ReDoc schema 一致 ← 验收点 #7
4. **Phase 1 条目未被改动**`git diff qy_lty/docs/修改记录.md` 仅显示新增条目在文件顶部追加Phase 1 已有的两条 `[2026-05-07] Phase 1 — ...` 标题在 diff 内位置应该是"unchanged"
</verification>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| 端到端 verify 测试 token | 临时 admin / user token30 天 TTL不允许写入仓库 |
| 跨项目修改记录文件 | 文档边界;不修改对方仓库代码,仅在对方 docs/ 下追加一条文档 |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-02P2-01 | Information Disclosure | 验收脚本中的 admin / user token 误入 git | mitigate | 02-VERIFICATION.md 仅记录脚本**模板**与"PASS"判定;不黏贴具体 token UUID脚本输出在 SUMMARY 中也只摘要"6/6 PASS"不贴 token |
| T-02P2-02 | Information Disclosure | 验收用真实凭据写入 DB覆盖 Phase 1 探针) | accept | DB 中 access_token 本来就是明文存储Phase 1 落地决策PUT 写入的 `sk-phase2_verify_secret_ABCD1234` 不是真实第三方凭据只是测试串Task 1 Step 2 #3 还会再 delete + 重建覆盖一次 |
| T-02P2-03 | Tampering | 误修改 Phase 1 修改记录条目 | mitigate | Task 2 关键约束第 7 条明确"不要修改 Phase 1 已有的两条条目"verify 步骤 #4 用 git diff 校验 Phase 1 条目位置不变 |
| T-02P2-04 | Information Disclosure | 互引文档泄露内部路径 | accept | `.planning/` 已是仓库内文档,路径暴露与本仓库 README 同等;不引入新泄露面 |
</threat_model>
<success_criteria>
本 plan 落地完成的标志phase 收尾标志):
- [ ] `.planning/phases/02-admin-rest/02-VERIFICATION.md` 存在8 条验收点 6+1+1 = 8 全 ✓
- [ ] `qy_lty/docs/修改记录.md` 顶部含 `### [2026-05-07] Phase 2 — 管理端通用凭据槽位 REST 接口GET 脱敏 / PUT 覆写)`
- [ ] `qy-lty-admin/docs/修改记录.md` 顶部含 `### [2026-05-07] Phase 2 — 锁定后端通用凭据槽位 REST 接口契约(消费方文档化)`
- [ ] 两条互引字段双向命中:`grep "qy-lty-admin/docs/修改记录" qy_lty/docs/修改记录.md` ≥ 1`grep "qy_lty/docs/修改记录" qy-lty-admin/docs/修改记录.md` ≥ 1
- [ ] DB 中 CredentialSlot 单例存在且 access_token 是 Task 1 #3 写入的最终值(`tok-XYZ9` 或之后被 #3 覆盖的某个值)
- [ ] Phase 1 两条修改记录条目未被改动git diff 确认)
- [ ] ROADMAP Phase 2 4 条 success criteria 在 02-VERIFICATION.md 中均找到对应的 ✓ 验收点映射
</success_criteria>
<output>
完成后创建 `.planning/phases/02-admin-rest/02-02-SUMMARY.md`,记录:
- 端到端验收 8 条结果PASS / FAIL 计数)
- 两端修改记录条目的最终位置(行号 / 标题)
- 与 02-01-SUMMARY 合并形成 Phase 2 完整交付证据
- DB 状态最终值app_id / access_token 末 4 位 / updated_at
- 任何与本 PLAN 不一致的偏离(应为零)
- 标识 Phase 2 整体 Complete下一步进入 Phase 3CRED-05 + CRED-06
</output>