# Phase 1：凭据槽位数据层 - Context

**Gathered**: 2026-05-07
**Status**: Ready for planning
**Source**: 用户在 `/gsd-plan-phase 1` 调用时提供的内联约束（等同 PRD 快速通道）

<domain>
## Phase 边界

本 phase 仅负责**数据库层 + Django Admin 入口**：
- 落地 `CredentialSlot` 单例 Django 模型 + 数据迁移
- 注册 Django Admin（SimpleUI 主题 + 中英 i18n）
- 单例语义：DB / 模型 / Admin 三层都要禁止出现第二条记录
- Access Token 字段在 Admin 列表/查看态脱敏（仅显示末 4 位），编辑态明文供运营录入

**不负责**（留给后续 phase）：
- Phase 2：管理端 REST 接口（GET/PUT `/api/v1/admin/credential-slot/`）
- Phase 3：客户端 REST 接口 + 阿里云日志脱敏
- 任何前端工作（在独立项目 `../qy-lty-admin/` 自己的 milestone 里）

</domain>

<decisions>
## 实现决策（锁定）

### 模型层（CRED-01）

- 单例语义实现方式：**`pk=1` 固定主键 + `get_or_create(pk=1)` 模式**
  - 在模型 `save()` 钩子中强制 `self.pk = 1`，配合 `get_or_create(pk=1)` 让首次访问拿到一条空记录
  - 不使用"单字段唯一约束"备选方案，因为该方案要求始终有非空字段，灵活性不如 pk=1
- 字段（最小集，沿用 ParadiseUser / 其它现有模型的命名习惯）：
  - `app_id`：CharField，max_length 合理（128 足够覆盖常见服务商 ID 长度），允许空字符串（运营初次访问时尚未填写）
  - `access_token`：CharField，max_length 长（512 留余量，覆盖 JWT 之类的长 token），允许空字符串
  - `updated_at`：DateTimeField，`auto_now=True`，每次保存自动刷新
- **app 归属：`aiapp`**（researcher 已确认：`common/` 不是 Django app 无 `apps.py`、未在 `INSTALLED_APPS`，无法承载 Model；`userapp/models.py` 已 471 行承重过大；`aiapp/models.py` 当前仅 51 行追加合适；语义"凭据"与 AI 服务商接入强相关）。模型追加到 `aiapp/models.py` 末尾，不新建子文件
- **单例模式直接复刻 `userapp.models.AffinitySetting`（247-314 行）**：仓库已有 pk=1 单例范本含 `save()` 钩子重定向 + `get_solo()` 类方法。Planner 必须把这两个文件读进 read_first，照抄结构，不要重新发明
- **脱敏工具新建 `common/utils.py:mask_token(token, visible_tail=4)`**：grep `mask|脱敏|redact` 在代码侧 0 命中，须新建。放 `common/utils.py` 让 Phase 3 阿里云日志 formatter 可直接复用
- 模型 `__str__` 返回类似 `f"凭据槽位 (updated {self.updated_at:%Y-%m-%d %H:%M})"` 的可读串

### 数据迁移

- 用 `python manage.py makemigrations <app>` 生成迁移文件（不要手写）
- 迁移生成后须能在 dev 环境跑 `python manage.py migrate` 通过
- 迁移文件命名沿用 Django 默认（`0001_initial.py` 等），不强求中文注释

### Django Admin 注册（CRED-02）

- 注册位置：与模型同 app 的 `admin.py`，沿用 SimpleUI + django-rosetta 中英双语注册风格（参考 `userapp/admin.py` / `aiapp/admin.py`）
- 列表页 `list_display`：包含脱敏后的 `access_token_masked`、`app_id`、`updated_at`
- 列表/查看态 access_token 脱敏实现：在 `ModelAdmin` 上加自定义方法 `access_token_masked(self, obj)` 返回末 4 位掩码（如 `****abcd`）；通过 `list_display` / `readonly_fields` 暴露
- 编辑表单字段：`app_id`、`access_token` 都明文（让运营录入 / 修改）
- **隐藏"新增"按钮**：重写 `has_add_permission(self, request)` 返回 `False if exists else True`（即记录已存在时禁止新增）
- **禁止删除**（避免运营误删后单例语义丢失）：重写 `has_delete_permission(self, request)` 返回 `False`
- 中英 i18n：**沿用仓库实操约定 = 中文字面量**（researcher 实测：本仓库 4 个 admin.py 全是中文字面量，`_()` 仅 7 处零散使用，且 `LANGUAGES` / `LOCALE_PATHS` 在 `settings.py` 已被注释掉）。Phase 1 不为 `verbose_name` / `short_description` 引入 `gettext_lazy`，保持与 `userapp/admin.py` / `aiapp/admin.py` 一致。i18n 体系化清洗留给独立 milestone。

### 兼容性 / 不引入新依赖

- 沿用 Django 4.2.13、Python 3.8（已在 PROJECT.md 「约束」段标注 Python 3.8 EOL，但不在本 milestone 内升级）
- 不引入新第三方包（不使用 `django-encrypted-model-fields` 等加密库；如未来需要 at-rest 加密，开新 phase 评估）
- 沿用 `StandardResponseMiddleware` —— 本 phase 不直接产生 REST 响应，所以无关

### Claude's Discretion

下面是 planner / 执行者可以自行决定的细节，不算锁定：

- 模型放在 `aiapp/models.py` 还是新建 `aiapp/models/credential_slot.py` 子文件 —— 取决于 `aiapp` 现有 models 文件大小
- 是否把 `access_token_masked` 工具函数抽到 `common/utils.py` 复用（Phase 3 阿里云日志脱敏可能也用得上） —— 推荐抽，但不是 Phase 1 强约束
- Admin 列表页的字段顺序、过滤器等 UX 细节
- `verbose_name` 中文字面量（如"凭据槽位"还是"通用凭据"）

</decisions>

<canonical_refs>
## Canonical References

**下游 agent 必读**（researcher / planner / executor 在生成或落地代码前都要读）：

### 项目宪法
- `qy_lty/CLAUDE.md` — 沟通语言（中文）+ 修改记录强制规则 + 跨项目联动
- `qy_lty/.planning/PROJECT.md` — Milestone v1.0「本期 Milestone」段、关键约束、关键决策
- `qy_lty/.planning/REQUIREMENTS.md` — Active 段 CRED-01 + CRED-02 的完整描述
- `qy_lty/.planning/ROADMAP.md` — Phase 1 详情段（Goal、Success Criteria 4 条）

### 模型 / Admin 现成模式（必读，pattern mapper 应该会自动列出来）
- `qy_lty/userapp/models.py` — `ParadiseUser` 看自定义模型 + 字段命名 + Meta
- `qy_lty/userapp/admin.py` — SimpleUI 主题下的 Admin 注册模式 + `list_display` / `readonly_fields` 写法
- `qy_lty/aiapp/models.py` — 同 app 内现有模型（决定 CredentialSlot 是否塞进 aiapp）
- `qy_lty/aiapp/admin.py` — 同 app 内现有 Admin（决定共存方式）

### 修改记录
- `qy_lty/docs/修改记录.md` — 文件头部「修改格式说明」即本 phase 落地后必须遵循的写入格式

### 跨项目互引
- `qy-lty-admin/.planning/REQUIREMENTS.md` — CRED-FE-01~05；本 phase 提交时需在前后端两份 `docs/修改记录.md` 互相引用条目

</canonical_refs>

<specifics>
## 具体要点（Success Criteria 显式化）

| # | 验证点 | 检查方式 |
|---|--------|----------|
| 1 | DB / 模型层强制最多一条 | Django shell：尝试 `CredentialSlot.objects.create()` 第二次必须抛异常 / 被 save() 钩子改写到 pk=1 覆盖现有 |
| 2 | 迁移落地 + schema 字段齐全 | `python manage.py migrate` 退出码 0；`python manage.py dbshell` 看 `\d <表名>` 含 app_id / access_token / updated_at 三列 |
| 3 | `get_or_create(pk=1)` 首访拿到空记录 | shell 中 `obj, created = CredentialSlot.objects.get_or_create(pk=1)`，`created==True` 且 `obj.app_id == ''` 且 `obj.access_token == ''` |
| 4 | Admin 列表/查看态脱敏，编辑态明文 | 浏览器登录 admin → 看列表页 access_token 字段显示 `****<末4位>` 形态；点进编辑表单看 access_token 字段为完整明文 input |
| 5 | Admin 列表页**不显示**「新增」按钮 | 浏览器登录 admin → 列表页右上角无 "Add 凭据槽位" / "新增凭据槽位" 按钮 |
| 6 | Admin **禁止删除**（额外保险） | 浏览器登录 admin → 编辑页底部无 "Delete" 按钮；批量动作不含 "Delete selected" |

</specifics>

<deferred>
## 推迟事项（明确不在 Phase 1 范围）

- **at-rest 加密 access_token 字段**：当前明文存 DB，依赖 PostgreSQL 访问控制；如未来需要应用层加密，开新 phase 评估 `django-encrypted-model-fields` 等方案
- **审计日志**（谁在什么时候改了 access_token）：Django Admin 自带 `LogEntry` 已经记录基本信息，本 phase 不做专门审计表
- **管理端 / 客户端 REST 接口**：分别在 Phase 2 / Phase 3
- **阿里云日志脱敏过滤器**：Phase 3 处理；Phase 1 仅保证 DB / Admin 不暴露明文；如果 `aiapp` 现有日志中已经有"打印 model 实例"的代码路径，最多在本 phase 给 `__str__` / `__repr__` 加上 access_token 脱敏，但不展开做全链路日志改造

</deferred>

---

*Phase: 01-credential-data-layer*
*Context gathered: 2026-05-07 via inline PRD（用户在 /gsd-plan-phase 1 调用时提供完整约束）*
