---
phase: 03-client-and-log-mask
plan: 01
subsystem: api
tags: [drf, apiview, redis-token-auth, swagger, credential-slot, plaintext-client]

# Dependency graph
requires:
  - phase: 01-credential-slot-foundation
    provides: aiapp.models.CredentialSlot 单例 + get_solo() / common.utils.mask_token / aiapp.serializers.CredentialSlotSerializer
  - phase: 02-admin-rest
    provides: aiapp.views.CredentialSlotAdminView（1:1 复刻起点）+ DB 探针态 pk=1/probe_app/probe_secret_xxxx
provides:
  - aiapp.views.CredentialSlotClientView：客户端只读 GET，明文返回 access_token，user/admin token 双兼容
  - _credential_slot_client_data_schema：drf-yasg 客户端响应 schema（access_token description 标注「明文」）
  - 路由 /api/credential-slot/（顶层 api_urlpatterns，name=client_credential_slot）
  - 端到端验收脚本 _phase3_01_verify.py（仓库根，未入 git，留给 Plan 03-02 Task 4 删除）
affects:
  - Phase 3 Plan 02（CRED-06 阿里云日志脱敏）：以本 plan 落地的客户端 view 为日志脱敏的兜底防御目标之一
  - Unity 客户端 LTY_App_Project_URP / LTY_Project：未来通过 /api/credential-slot/ 拉取明文凭据调用第三方服务

# Tech tracking
tech-stack:
  added: []
  patterns:
    - "客户端 view 与 admin view 行为反向（admin 脱敏 / client 明文），同一份 serializer，差异隔离在 view 层"
    - "客户端命名空间路由直接挂顶层 api_urlpatterns（不走任何 sub-include），保证最终 URL 是 /api/credential-slot/ 而非 /api/<app>/credential-slot/"
    - "drf-yasg 客户端响应 schema 单独命名（_credential_slot_client_data_schema），与 admin 端 _credential_slot_data_schema 形成对称对照，避免混用脱敏掩码语义"
    - "端到端验收沿用 Phase 2 模式：Django test client in-process + Redis 临时 token + DB 探针态主动还原"

key-files:
  created:
    - .planning/phases/03-client-and-log-mask/03-01-SUMMARY.md
    - _phase3_01_verify.py（仓库根，未入 git，Plan 03-02 Task 4 删除）
  modified:
    - aiapp/views.py（末尾追加 64 行：1 块 schema + 1 个 APIView 类）
    - qy_lty/urls.py（imports 段 +1 行 / api_urlpatterns +2 行含注释）

key-decisions:
  - "[Plan 03-01] CredentialSlotClientView 完全独立 APIView 类，不继承 / mixin admin view，避免 Phase 2 admin view 演进时意外波及客户端语义"
  - "[Plan 03-01] 不调 _ensure_admin / _build_response_data / mask_token，仅调用 success_response(data=serializer.data)：明文返回是 CONTEXT 锁定决策，admin user 是手机用户超集，对客户端 view 不做 is_staff 二次校验"
  - "[Plan 03-01] 客户端响应 schema 独立命名 _credential_slot_client_data_schema，access_token description 显式写「明文 Access Token，供手机/设备端实际调用第三方服务（管理端同接口会脱敏返回末 4 位）」，与 admin 端 description 形成对照"
  - "[Plan 03-01] 路由放顶层 qy_lty/urls.py:api_urlpatterns（紧邻 common/upload/ 后），与 Phase 2 admin 路由（v1/admin/） 视觉分组隔离，最终 URL = /api/credential-slot/"
  - "[Plan 03-01] 验收脚本 _phase3_01_verify.py 不入 git：与 Phase 2 _phase2_verify.py 同模式，是一次性证据生成器，证据落地本 SUMMARY 后由 Plan 03-02 Task 4 统一删除"

patterns-established:
  - "客户端 / 管理端同 model 的双 view 反向行为模式（admin 脱敏 + 写 / client 明文 + 只读）"
  - "顶层 api_urlpatterns 直挂客户端散点路径模式（参考 common/upload/，避免 sub-include 命名污染）"

requirements-completed:
  - CRED-05

# Metrics
duration: 4min30s
completed: 2026-05-08
---

# Phase 3 Plan 03-01：客户端通用凭据槽位 GET 接口 Summary

**CredentialSlotClientView 客户端只读 APIView 落地，/api/credential-slot/ 顶层路由注册，user / admin token 双兼容明文返回 access_token，端到端 6 条 truth 全 PASS（15 项独立断言）**

## Performance

- **Duration:** ~4.5 min（auto 模式纯顺序执行）
- **Started:** 2026-05-08T02:11:32Z
- **Completed:** 2026-05-08T02:16:02Z
- **Tasks:** 3（2 个文件落地 + 1 个端到端验收）
- **Files modified:** 2（aiapp/views.py + qy_lty/urls.py）
- **Files created:** 1（_phase3_01_verify.py，仓库根，未入 git）

## Accomplishments

- **CRED-05 落地完成**：`/api/credential-slot/` GET 接口已注册到 Django URLConf，明文返回 `{app_id, access_token, updated_at}` 标准壳层
- **6 条 truth 全 PASS**（15 项独立断言）：
  - T1 user token GET → 200 + 明文 + success=true（5 项断言）
  - T2 admin token GET → 200 + 明文（2 项断言）
  - T3 无 token → 401 + success=false（2 项断言）
  - T4 伪造 token → 401（1 项断言）
  - T5 /swagger.json/ 含 /credential-slot/ + GET 方法（3 项断言）
  - T6 DB 探针态保持 probe_app / probe_secret_xxxx（2 项断言）
- **DB 探针态保持稳定**：脚本前后 `pk=1, app_id='probe_app', access_token='probe_secret_xxxx'`，给 Plan 03-02 留下与 Phase 2 一致的起点
- **Phase 2 既有代码 0 改动**：`CredentialSlotAdminView` / `CredentialSlotPutRequestSchema` / `_credential_slot_data_schema` 完全未动；imports 段未变

## Task Commits

每个 task 原子提交（中文 commit message）：

1. **Task 1：在 aiapp/views.py 末尾追加 CredentialSlotClientView 类** — `5269a08` (feat) — 新增 64 行（schema + view 类）
2. **Task 2：在 qy_lty/urls.py 注册 /api/credential-slot/ 路由** — `50dcf1c` (feat) — imports 段 +1 行 + api_urlpatterns +2 行
3. **Task 3：端到端验收（CRED-05 6 条 truth）** — 无 commit（验收脚本 `_phase3_01_verify.py` 留在仓库根未入 git，由 Plan 03-02 Task 4 统一删除）

**Plan metadata:** 待 final commit（本 SUMMARY + STATE.md / ROADMAP.md / REQUIREMENTS.md）

## Files Created/Modified

- **`aiapp/views.py`**（行 688-748，新增 60 行）：末尾追加 `_credential_slot_client_data_schema` 客户端响应 schema + `CredentialSlotClientView(APIView)` 类
- **`qy_lty/urls.py`**（行 27 + 行 58-59，新增 3 行）：imports 段 `from aiapp.views import CredentialSlotClientView`；`api_urlpatterns` 列表新增 `path('credential-slot/', CredentialSlotClientView.as_view(), name='client_credential_slot')`
- **`_phase3_01_verify.py`**（仓库根，未入 git）：端到端验收脚本，6 条 truth × 15 项断言；Plan 03-02 Task 4 删除

## 与 Phase 2 admin view 的对照差异

| 项 | CredentialSlotAdminView (Phase 2) | CredentialSlotClientView (Phase 3 Plan 01) |
|---|---|---|
| 类体内 `_ensure_admin` | 含（is_staff 二次校验，非 admin 返 403） | 不含（admin / user 都允许） |
| 类体内 `_build_response_data` | 含（调 mask_token 脱敏 access_token） | 不含（直接 serializer.data 明文） |
| 类体内 `mask_token` 调用 | 1 次（_build_response_data 内） | 0 次 |
| HTTP 方法 | GET + PUT（双方法） | 仅 GET（只读） |
| 响应行为 | access_token 末 4 位脱敏 `*************xxxx` | 明文 `probe_secret_xxxx` |
| swagger response data schema | `_credential_slot_data_schema`（脱敏掩码语义） | `_credential_slot_client_data_schema`（明文语义） |
| 路由命名空间 | `/api/v1/admin/credential-slot/`（admin sub-include） | `/api/credential-slot/`（顶层 api_urlpatterns） |
| URL name | `admin_credential_slot` | `client_credential_slot` |

## Decisions Made

见 frontmatter `key-decisions` 字段，已 5 条决策全部归档。

## Deviations from Plan

### Auto-fixed Issues

**1. [Rule 1 - Bug] 客户端 view docstring 字面量触发自动化 verify 误报**
- **Found during:** Task 1（首次跑 verify regex check 时）
- **Issue:** PLAN.md 自动化校验脚本用 `re.search(r'^class CredentialSlotClientView.*?(?=^class |\Z)', src, re.S | re.M)` 切类体后 grep `_ensure_admin / _build_response_data / def put / mask_token` 是否出现；但首版 docstring 内含「删 _ensure_admin / 删 _build_response_data / 删 PUT 方法」字面引用，被 regex 误判为客户端类体含禁词，验收 FAIL
- **Fix:** 把 docstring 中的禁词替换为语义等价但不触发字面量匹配的描述（「不做 is_staff 二次校验」/「不脱敏：直接返回 serializer.data」/「仅 GET：客户端只读」），保留对照管理端 view 的差异说明
- **Files modified:** aiapp/views.py
- **Verification:** verify regex check 复跑全 PASS；class body 字面量不再含 `_ensure_admin` / `_build_response_data` / `def put` / `mask_token`
- **Committed in:** 5269a08（与 Task 1 主体一并提交）

**2. [Rule 3 - Blocking] python manage.py shell < 在 Windows 下行级 REPL 执行**
- **Found during:** Task 3（首次跑 `python manage.py shell < _phase3_01_verify.py` 时）
- **Issue:** Windows PowerShell + Django shell 把脚本内容当 REPL 输入逐行送入，所有缩进块（if/for/函数体等）触发 IndentationError；脚本完全无法执行
- **Fix:** 改用 `python manage.py shell -c "exec(open('_phase3_01_verify.py', encoding='utf-8').read())"` —— Django shell 的 `-c` 参数把字符串作为 Python 源码完整 exec，保留缩进语义；与 Linux/macOS 下 `<` 重定向行为等价
- **Files modified:** 无（仅调整执行命令）
- **Verification:** 复跑成功，15 PASSes / 0 FAILs
- **Committed in:** 不需要 commit（执行方式调整，不触动代码）

**3. [Rule 3 - Blocking] Windows GBK 控制台无法编码 Unicode 字符 ✓**
- **Found during:** Task 3（exec 执行成功收尾时）
- **Issue:** 脚本 print 循环用 `✓` 标记 PASS 项；Windows 默认 stdout 编码 cp936/gbk，UnicodeEncodeError 中断输出（且 PASS 标记中的中文「客户端响应未脱敏」也触发）；脚本运行成功但 print 阶段 crash
- **Fix:** 把 `✓` 替换为 ASCII 字符串 `[PASS]`；把 PASS list 中的中文标签替换为英文短标签（保持验收语义）；其余日志保留中文（不进 print 循环）
- **Files modified:** _phase3_01_verify.py（未入 git）
- **Verification:** 复跑全部 15 项 PASS 完整 print 输出，无编码异常
- **Committed in:** 不需要 commit（脚本本身不入 git）

---

**Total deviations:** 3 auto-fixed（1 Bug + 2 Blocking）
**Impact on plan:** 三处偏差均为执行环境兼容性问题（Windows shell + verify regex 假阳性），不涉及业务逻辑 / 安全语义 / 架构变更；plan acceptance criteria 全部达成，CRED-05 6 条 truth 端到端 PASS

## Issues Encountered

无业务 / 架构问题。仅遇到上述 3 处 Windows 环境兼容性问题，已自动修复。

## Stub Status

无。本 plan 落地的 view + 路由是直连 DB 的真实数据通路（CredentialSlot.get_solo() → Postgres pk=1 单例），不含任何 placeholder / 硬编码 / mock 数据。

## User Setup Required

无 —— 不引入新依赖、不要求新环境变量、不需要外部服务配置。

## Next Phase Readiness

- **Phase 3 Plan 02（CRED-06 阿里云日志脱敏）准备就绪**：本 plan 已确认 `CredentialSlotClientView` 直接 `success_response(data=serializer.data)` 不调 logger.info / logger.debug，**未引入新泄露源**；Plan 02 的 `AccessTokenMaskFilter` 是兜底防御
- **DB 探针态保持**：`pk=1, app_id='probe_app', access_token='probe_secret_xxxx'`，与 Phase 2 完全一致，Plan 02 验收时直接复用
- **临时验收脚本**：`_phase3_01_verify.py` 留在仓库根（未入 git），Plan 03-02 Task 4 末尾统一删除（本 plan 不写 docs/修改记录.md，由 Plan 03-02 Task 4 一并写）
- **Hand-off to Plan 03-02**：本 plan 不修改 `qy_lty/settings.py:LOGGING`，Plan 02 即可直接 patch；本 plan 不创建 `common/logging/` 目录，Plan 02 创建

## Self-Check: PASSED

- [x] aiapp/views.py 含 `class CredentialSlotClientView(APIView):` (FOUND)
- [x] aiapp/views.py 含 `_credential_slot_client_data_schema` (FOUND, 2 occurrences)
- [x] qy_lty/urls.py 含 `from aiapp.views import CredentialSlotClientView` (FOUND)
- [x] qy_lty/urls.py 含 `path('credential-slot/', CredentialSlotClientView.as_view(), name='client_credential_slot')` (FOUND)
- [x] commit 5269a08 in git log (FOUND)
- [x] commit 50dcf1c in git log (FOUND)
- [x] _phase3_01_verify.py 输出 ALL PASS (FOUND, 15 PASSes / 0 FAILs)
- [x] DB 探针态 pk=1/probe_app/probe_secret_xxxx 保持 (FOUND)
- [x] python manage.py check 通过（仅遗留 staticfiles.W004 与本 plan 无关）

---
*Phase: 03-client-and-log-mask*
*Plan: 01*
*Completed: 2026-05-08*
