--- 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 两 method,access_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/修改记录" --- 本 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 criteria(GET 脱敏 / 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 中归档) @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.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 Task 1:端到端 curl + Django shell 验收(8 条 success criteria) qy_lty/.planning/phases/02-admin-rest/02-VERIFICATION.md - 必读 .planning/phases/02-admin-rest/02-01-SUMMARY.md(02-01 留给本 task 的 hook) - 必读 .planning/phases/02-admin-rest/02-CONTEXT.md `` 段(8 条验收点表格) - 必读 .planning/phases/02-admin-rest/02-RESEARCH.md `Validation Architecture` 段(test client 程序化验收脚本片段 + 采样频率) - 必读 qy_lty/userapp/utils.py:33-37(generate_token 签发 admin / user token 的方式) - 必读 qy_lty/aiapp/models.py 的 CredentialSlot.get_solo(验证 PUT 在空记录场景的 get_or_create) - 必读 .planning/phases/01-credential-data-layer/01-VERIFICATION.md(Phase 1 程序化验收风格,本 task 1:1 沿用) **目标**:在 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 2:Django 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() # === 验收点 1:GET 携 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") # === 验收点 2:PUT 携 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") # === 验收点 3:PUT 在空记录场景自动 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 + 需要管理员权限") # === 验收点 6:PUT 携 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 3:Swagger 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-04(ROADMAP 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 落地修改记录后**回写**为 ✓ PASS(02-VERIFICATION.md 在 Task 2 末尾再 Edit 一次) - 不要把 admin / user token 明文写进 02-VERIFICATION.md(Redis 30 天 TTL,落进 git 的 token 在 30 天内仍有效,是新的泄露面) 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 - 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 写入的最新值 8 条 success criteria 中 6 条 test client + 1 条 swagger 已验完并归档;DB 状态稳定;02-VERIFICATION.md 是 Phase 2 收尾时 ROADMAP 标记 ✓ Complete 的证据来源。 Task 2:两端修改记录互引条目(qy_lty + qy-lty-admin) qy_lty/docs/修改记录.md, qy-lty-admin/docs/修改记录.md - 必读 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.md(Phase 2 实际改动文件清单 + 接口契约) - 必读 02-CONTEXT.md `` 跨项目联动段(互引文案要点) **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 → 401(DRF 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 `(来源于 `/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 token,message 含"需要管理员权限")、400(参数无效) - **修改原因**: - 服务端首次为本管理后台暴露受控的凭据读写接口;本仓库即将启动 CRED-FE-01(API 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 一个文件) 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 - `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 `qy_lty` + `qy-lty-admin` 两端 `docs/修改记录.md` 顶部各有一条 Phase 2 条目,互相引用对方文件路径;CLAUDE.md "qy_lty 与 qy-lty-admin 是独立项目,跨项目联动两端各写一条互相引用对方"规则在本 phase 闭环;02-VERIFICATION.md 8 条全 PASS。 本 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" ## Trust Boundaries | Boundary | Description | |----------|-------------| | 端到端 verify 测试 token | 临时 admin / user token,30 天 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 同等;不引入新泄露面 | 本 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 中均找到对应的 ✓ 验收点映射 完成后创建 `.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 3(CRED-05 + CRED-06)