Files
ulthon_information/.sisyphus/plans/apikey-article-api.md
augushong 0e8944bc7f docs: 为 category-api 和 apikey-article-api 添加项目笔记文件
- 添加 category-api 的 problems、decisions、learnings、issues 文档
- 添加 apikey-article-api 的 issues、decisions、learnings 文档
- 包含架构决策、问题记录和学习总结
2026-04-28 21:03:15 +08:00

1272 lines
54 KiB
Markdown
Raw 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.

# API Key 管理与文章/附件 API
## TL;DR
> **Quick Summary**: 为现有 ThinkPHP 6 CMS 添加 API Key 认证机制,后台管理员可管理自己的 API Key通过 API Key 调用 RESTful 接口完成文章完整 CRUD 和附件完整管理(上传/列表/删除)。
>
> **Deliverables**:
> - 数据库迁移文件:`ul_api_key` 表
> - ApiKey 模型
> - ApiKeyAuth 中间件Header 认证)
> - 文章 API 控制器(列表/详情/创建/编辑/删除)
> - 附件 API 控制器(上传/列表/删除)
> - 后台 API Key 管理控制器 + layui 视图
> - API 中间件注册文件
>
> **Estimated Effort**: Medium
> **Parallel Execution**: YES - 3 waves
> **Critical Path**: 迁移+模型 → 中间件 → API控制器 → 后台管理
---
## Context
### Original Request
后台可以管理 API Key使用 API Key 可以管理文章(列表、发表文章、上传附件等)。
### Interview Summary
**Key Discussions**:
- API Key 粒度:每个管理员一个 API Key
- 权限控制:拥有 Key 即有全部文章管理权限,无需分级
- 过期策略:永不过期,手动禁用/启用
- 文章操作:列表+详情(所有Key); 创建+编辑自己(can_write_own=1); 编辑后台(can_write_other=1); 删除(can_delete控制)
- 附件管理:列表(所有Key); 上传(can_write_own=1); 删除(can_delete控制)
- 数据查询:所有数据不分来源
- 权限模型三字段can_write_own/can_write_other/can_delete每个Key独立控制
- 有删除接口,删除权限由 can_delete 字段控制
- source 字段用于权限判断和数据隔离
- 测试策略:不需要搭建测试基础设施
**Research Findings**:
- 现有 API 控制器3个均无认证继承 BaseController
- API 路由完全依赖 TP6 自动路由 `/api/{controller}/{action}`
- 不存在 `app/api/middleware.php`,需新建
- Post 模型使用 SoftDelete + AutoClearCache关联 categorys/tags/comments
- 文件上传使用 `app/UploadFiles.php` 静态服务类
- 响应格式:`{ code: 0/500, msg: '', data: {} }` 通过 `json_message()`
- 迁移使用 `ColumnFormat` 辅助类,时间戳 int(10)
### Metis Review
**Identified Gaps** (addressed):
- API Key 安全存储:数据库中存储 md5 哈希而非明文,仅在创建时展示一次原文
- 管理员被删除时的 API Key 处理:通过 admin_id 外键关联,管理员删除时 API Key 自动禁用
- 现有 API 控制器不受影响:中间件仅应用于需要认证的路由/控制器
- 防止重复提交:创建文章时生成 uid 防止重复
- 附件类型校验:复用现有 `UploadFiles::fileScan()` 安全扫描
---
## Work Objectives
### Core Objective
为 CMS 系统增加 API Key 认证体系,使外部客户端可以通过 API Key 认证后调用 RESTful API 管理文章和附件。
### Concrete Deliverables
- `database/migrations/` 新增 `ul_api_key` 表迁移 + post/upload_files 表加 source 字段迁移
- `app/model/ApiKey.php` - API Key 模型
- `app/middleware/ApiKeyAuth.php` - API Key 认证中间件
- `app/api/middleware.php` - API 应用中间件注册
- `app/api/controller/Articles.php` - 文章 API列表/详情/创建/编辑,无删除)
- `app/api/controller/Attachments.php` - 附件管理 API上传/列表,无删除,数据隔离)
- `app/admin/controller/ApiKey.php` - 后台 API Key 管理控制器
- `view/admin/api_key/` - 后台 API Key 管理 layui 视图index.html
### Definition of Done
- [ ] `php think migrate:run` 成功创建 `ul_api_key` 表(含 can_write_own/can_write_other/can_delete 字段)
- [ ] 后台可以查看/生成/重新生成/禁用/启用 API Key以及设置三个权限字段
- [ ] 使用有效 API Key 可以查询所有文章和附件
- [ ] can_write_own=1 时可以创建文章和上传附件,编辑/删除 source='api' 的数据
- [ ] can_write_other=1 时可以编辑 source='admin' 的数据
- [ ] can_delete=0 时不能删除任何数据
- [ ] can_delete=1 时只能删除 source='api' 的数据
- [ ] can_delete=2 时可以删除所有数据
- [ ] 使用无效/禁用的 API Key 返回 401 错误
### Must Have
- API Key 数据库存储 md5 哈希,创建时仅返回一次明文
- 所有 API 响应使用 `json_message()` 格式
- 每个 API Key 有三个独立权限字段:
- `can_write_own` (0/1): 能否创建和编辑 source='api' 的数据
- `can_write_other` (0/1): 能否编辑 source='admin' 的数据
- `can_delete` (0/1/2): 0=不能删除, 1=只能删API创建的, 2=可删所有
- API 可以查询所有数据(文章列表/详情、附件列表),不区分来源
- 创建文章时自动标记 source='api'
- 上传附件时自动标记 source='api'
- 编辑文章时根据 source 和权限字段判断是否允许
- 删除时根据 can_delete 和 source 判断是否允许
- 文件上传复用现有 `UploadFiles::save()` 及安全扫描
- 后台管理界面沿用现有 layui 风格,可设置三个权限
- 中间件仅保护需要认证的 API 控制器
- post 表和 upload_files 表添加 source 字段(默认 'admin'API 创建时设为 'api'
### Must NOT Have (Guardrails)
- 不修改现有控制器或模型(只新增)
- 不改变现有 Session 认证体系
- 不引入 Rate Limiting
- 不引入 API Key 过期时间
- 不修改现有 `app/UploadFiles.php` 文件
- 不引入新前端库
- 不使用 $fillable 白名单
---
## Verification Strategy (MANDATORY)
> **ZERO HUMAN INTERVENTION** - ALL verification is agent-executed. No exceptions.
### Test Decision
- **Infrastructure exists**: NO
- **Automated tests**: None
- **Framework**: none
- **Verification method**: Agent-executed QA via curl commands
### QA Policy
Every task MUST include agent-executed QA scenarios.
Evidence saved to `.sisyphus/evidence/task-{N}-{scenario-slug}.{ext}`.
- **API/Backend**: Use Bash (curl) - Send requests, assert status + response fields
- **Admin UI**: Use Playwright (playwright skill) - Navigate, interact, assert DOM, screenshot
---
## Execution Strategy
### Parallel Execution Waves
```
Wave 1 (Start Immediately - 基础设施层):
├── Task 1: 数据库迁移 - ul_api_key 表 [quick]
├── Task 2: ApiKey 模型 [quick]
└── Task 3: ApiKeyAuth 认证中间件 [quick]
Wave 2 (After Wave 1 - API 接口层MAX PARALLEL):
├── Task 4: 文章 API 控制器 (depends: 1, 2, 3) [unspecified-high]
├── Task 5: 附件 API 控制器 (depends: 1, 2, 3) [unspecified-high]
├── Task 6: 后台 API Key 管理控制器 + 视图 (depends: 1, 2) [unspecified-high]
├── Task 7: API Key 权限查询接口 (depends: 2, 3) [quick]
└── Task 8: 前台 API 文档页面 (depends: 4, 5, 7) [unspecified-high]
Wave FINAL (After ALL tasks — 4 parallel reviews):
├── Task F1: Plan compliance audit (oracle)
├── Task F2: Code quality review (unspecified-high)
├── Task F3: Real manual QA (unspecified-high)
└── Task F4: Scope fidelity check (deep)
-> Present results -> Get explicit user okay
Critical Path: Task 1 → Task 3 → Task 4 → Task 8 → F1-F4 → user okay
Parallel Speedup: ~60% faster than sequential
Max Concurrent: 5 (Wave 2)
```
### Dependency Matrix
| Task | Depends On | Blocks | Wave |
|------|-----------|--------|------|
| 1 | - | 2,3,4,5,6 | 1 |
| 2 | 1 | 3,4,5,6,7 | 1 |
| 3 | 1,2 | 4,5,7 | 1 |
| 4 | 1,2,3 | 8,F1-F4 | 2 |
| 5 | 1,2,3 | 8,F1-F4 | 2 |
| 6 | 1,2 | F1-F4 | 2 |
| 7 | 2,3 | 8 | 2 |
| 8 | 4,5,7 | F1-F4 | 2 |
| F1-F4| 4,5,6,8 | - | FINAL |
### Agent Dispatch Summary
- **Wave 1**: **3 tasks** - T1 → `quick`, T2 → `quick`, T3 → `quick`
- **Wave 2**: **5 tasks** - T4 → `unspecified-high`, T5 → `unspecified-high`, T6 → `unspecified-high`, T7 → `quick`, T8 → `unspecified-high`
- **FINAL**: **4 tasks** - F1 → `oracle`, F2 → `unspecified-high`, F3 → `unspecified-high`, F4 → `deep`
---
## TODOs
- [x] 1. 数据库迁移 - ul_api_key 表 + post/upload_files 表 source 字段
**What to do**:
- 创建迁移文件 `database/migrations/{timestamp}_create_table_api_key.php`
- 参照现有迁移结构(如 `create_table_admin.php`),使用 `ColumnFormat` 辅助类
- 表名: `api_key`Phinx 自动加 `ul_` 前缀 → `ul_api_key`
- 字段设计:
- `id`: 自增主键
- `admin_id`: int(10) unsigned关联管理员 IDNOT NULL
- `api_key`: string(64),存储 md5 哈希值NOT NULL, UNIQUE
- `name`: stringNormal(100)API Key 名称/备注
- `status`: integerTypeStatus()0=禁用1=启用默认1
- `can_write_own`: integerTypeStatus()0=不可, 1=可默认0能否创建和编辑API数据
- `can_write_other`: integerTypeStatus()0=不可, 1=可默认0能否编辑后台数据
- `can_delete`: integerTypeStatus()0=不可删除, 1=仅删API数据, 2=可删所有默认0
- `create_time`: timestamp创建时间
- `update_time`: timestamp更新时间
- `delete_time`: timestamp软删除时间
- 添加索引: `admin_id`(普通索引), `api_key`(唯一索引)
- 仅实现 `change()` 方法(不可逆迁移,遵循项目惯例)
- 创建迁移文件 `database/migrations/{timestamp}_add_source_to_post_and_upload_files.php`
-`post` 表添加 `source` 字段: stringShort(30),默认值 `'admin'`
-`upload_files` 表添加 `source` 字段: stringShort(30),默认值 `'admin'`
- source 字段用于标记数据来源:'admin'=后台创建,'api'=API创建
- 仅实现 `change()` 方法
**Must NOT do**:
- 不添加外键约束(项目无此惯例)
- 不修改现有字段
**Recommended Agent Profile**:
- **Category**: `quick`
- Reason: 单个迁移文件,结构清晰,参照现有模板即可
- **Skills**: []
- **Skills Evaluated but Omitted**:
- 无需特殊技能
**Parallelization**:
- **Can Run In Parallel**: YES (with nothing - this is the foundation)
- **Parallel Group**: Wave 1 (with Tasks 2)
- **Blocks**: Tasks 2, 3, 4, 5, 6
- **Blocked By**: None (can start immediately)
**References**:
**Pattern References**:
- `database/migrations/20190822043811_create_table_admin.php` - Admin 表迁移结构,参照此文件的迁移类结构
- `database/migrations/20200418120809_create_table_post.php` - Post 表迁移,参照字段定义方式
- `app/common/ColumnFormat.php` - 列定义辅助类,了解 `stringNormal()`/`timestamp()`/`integerTypeStatus()` 等方法
**Why Each Reference Matters**:
- Admin 迁移文件展示了本项目的标准迁移写法:类名、命名空间、`change()` 方法、ColumnFormat 用法
- Post 迁移展示了字段类型选择惯例
- ColumnFormat 是必用的辅助类,必须使用它定义列
**Acceptance Criteria**:
**QA Scenarios (MANDATORY):**
```
Scenario: 迁移文件创建成功并执行
Tool: Bash
Preconditions: 项目根目录,数据库配置正确
Steps:
1. 运行 `php think migrate:run`
2. 检查输出无错误
3. 使用 SQLite 命令检查 ul_api_key 表结构
4. 验证字段存在: admin_id, api_key, name, status, create_time, update_time, delete_time
5. 验证 api_key 字段有 UNIQUE 约束
6. 检查 ul_post 表是否有 source 字段,默认值是否为 'admin'
7. 检查 ul_upload_files 表是否有 source 字段,默认值是否为 'admin'
Expected Result: ul_api_key 表创建成功含所有字段post 和 upload_files 表新增 source 字段
Failure Indicators: migrate:run 报错,或表/字段缺失
Evidence: .sisyphus/evidence/task-1-migration-success.txt
Scenario: 回滚后重新迁移
Tool: Bash
Preconditions: 迁移已执行
Steps:
1. 运行 `php think migrate:rollback`
2. 确认 ul_api_key 表不存在source 字段已移除
3. 运行 `php think migrate:run`
4. 确认表和字段恢复
Expected Result: 回滚后表/字段消失,重新迁移后恢复
Failure Indicators: 回滚失败或重新迁移失败
Evidence: .sisyphus/evidence/task-1-migration-rollback.txt
```
**Commit**: YES (groups with Task 2)
- Message: `feat(api-key): add api_key table, model and auth middleware`
- Files: `database/migrations/*_create_table_api_key.php`
- Pre-commit: `php -l database/migrations/*_create_table_api_key.php`
- [x] 2. ApiKey 模型
**What to do**:
- 创建 `app/model/ApiKey.php` 模型
- 继承 `app\common\model\Base`(项目模型基类)
- 使用 `SoftDelete` trait`$defaultSoftDelete = 0`
- 使用 `AutoClearCache` trait项目惯例
- 定义表名: `$name = 'api_key'`
- 定义关联: `belongsTo(Admin::class, 'admin_id')` 关联到管理员
- 添加静态方法:
- `generateKey($admin_id, $name = '', $can_write_own = 0, $can_write_other = 0, $can_delete = 0)`: 生成随机 API Key32位 hex存储 md5 哈希,返回明文 Key仅此一次返回明文
- `verifyKey($raw_key)`: 根据 md5 哈希查找 Key检查 status=1返回 ApiKey 模型或 null
- `regenerateKey($id)`: 重新生成 Key旧 Key 失效),返回新明文 Key
- 添加实例方法:
- `canWriteOwn()`: 返回 `$this->can_write_own == 1`
- `canWriteOther()`: 返回 `$this->can_write_other == 1`
- `canDelete($source = null)`: 判断能否删除指定来源的数据
- can_delete=0: 返回 false
- can_delete=1: 仅当 $source=='api' 时返回 true
- can_delete=2: 返回 true
- API Key 明文格式: `ak_` + 32位随机 hex如 `ak_a1b2c3d4e5f6...`),便于识别
**Must NOT do**:
- 不修改 Admin 模型
- 不在模型中存储明文 API Key
**Recommended Agent Profile**:
- **Category**: `quick`
- Reason: 单个模型文件,逻辑简单,参照现有模型即可
- **Skills**: []
**Parallelization**:
- **Can Run In Parallel**: YES (with Task 1 - 可并行编写,但执行需等待迁移)
- **Parallel Group**: Wave 1 (with Task 1)
- **Blocks**: Tasks 3, 4, 5, 6
- **Blocked By**: Task 1 (迁移必须先执行)
**References**:
**Pattern References**:
- `app/model/Admin.php` - 管理员模型结构参照模型定义、SoftDelete 用法
- `app/common/model/Base.php` - 模型基类,了解继承方式
- `app/model/Post.php` - SoftDelete + AutoClearCache trait 的使用方式(`use SoftDelete; protected $defaultSoftDelete = 0;`
**API/Type References**:
- `app/model/Admin.php` - Admin 模型,用于 belongsTo 关联定义
**Why Each Reference Matters**:
- Admin 模型展示了本项目标准模型写法
- Base 模型展示了基类功能AutoClearCache 已在 Base 中还是需要单独 use
- Post 模型展示了 SoftDelete 的具体用法
**Acceptance Criteria**:
**QA Scenarios (MANDATORY):**
```
Scenario: ApiKey 模型 CRUD 和 Key 生成/验证
Tool: Bash (php think)
Preconditions: Task 1 迁移已执行
Steps:
1. 运行 php -r 测试脚本:
"require 'vendor/autoload.php'; $app = new think\App(); $app->initialize();
$raw = app\model\ApiKey::generateKey(1, 'test-key');
echo 'Raw key format: ' . (preg_match('/^ak_[a-f0-9]{32}$/', $raw) ? 'OK' : 'FAIL') . PHP_EOL;
$verified = app\model\ApiKey::verifyKey($raw);
echo 'Verify valid: ' . ($verified ? 'OK' : 'FAIL') . PHP_EOL;
$verified2 = app\model\ApiKey::verifyKey('ak_invalid0000000000000000000000000');
echo 'Verify invalid: ' . ($verified2 === null ? 'OK' : 'FAIL') . PHP_EOL;"
2. 检查输出全部为 OK
Expected Result: Key 生成格式正确ak_ + 32位hex验证有效 Key 返回模型,无效 Key 返回 null
Failure Indicators: 格式不匹配,验证逻辑错误
Evidence: .sisyphus/evidence/task-2-model-qa.txt
Scenario: 禁用状态的 Key 无法验证
Tool: Bash (php think)
Preconditions: Task 1 迁移已执行
Steps:
1. 创建 ApiKey 并设置为 status=0
2. 调用 verifyKey()
3. 确认返回 null
Expected Result: 禁用的 Key 验证失败
Failure Indicators: 禁用 Key 仍然验证通过
Evidence: .sisyphus/evidence/task-2-model-disabled.txt
```
**Commit**: YES (groups with Task 1)
- Message: `feat(api-key): add api_key table, model and auth middleware`
- Files: `app/model/ApiKey.php`
- Pre-commit: `php -l app/model/ApiKey.php`
- [x] 3. ApiKeyAuth 认证中间件 + API 中间件注册
**What to do**:
- 创建 `app/middleware/ApiKeyAuth.php` 中间件
- 参照现有 `ConfigInit.php` 的 handle 签名
- 认证逻辑:
1. 从 HTTP Header `Authorization: Bearer {key}` 提取 API Key
2. 若无 Bearer 头,尝试从 `X-API-Key` 头获取
3. 调用 `ApiKey::verifyKey($raw_key)` 验证
4. 验证成功: 将 admin_id、api_key_id 和三个权限字段存入 Request attribute
- `$request->admin_id = $api_key->admin_id`
- `$request->api_key_id = $api_key->id`
- `$request->can_write_own = $api_key->can_write_own`
- `$request->can_write_other = $api_key->can_write_other`
- `$request->can_delete = $api_key->can_delete`
5. 验证失败: 返回 JSON `{"code": 401, "msg": "API Key 无效或已禁用", "data": null}`HTTP 状态码 401
- **写操作检查在控制器中实现**(不在中间件中,因为不同操作的权限逻辑不同)
- 创建 `app/api/middleware.php` 注册文件:
- 注意: 仅对需要认证的控制器生效。TP6 支持在控制器中通过 `$middleware` 属性指定中间件
- 方案: 在 Articles 和 Attachments 控制器中通过 `protected $middleware = [ApiKeyAuth::class];` 挂载
- 这样现有 API 控制器Files/Captcha/WxOpen不受影响
- 创建空的 `app/api/middleware.php`(为将来预留,内容为 `return [];`
**Must NOT do**:
- 不修改现有中间件
- 不影响现有无认证的 API 控制器Files/Captcha/WxOpen
- 不使用 Session 进行 API 认证
**Recommended Agent Profile**:
- **Category**: `quick`
- Reason: 单个中间件文件 + 一个空的注册文件,逻辑简单
- **Skills**: []
**Parallelization**:
- **Can Run In Parallel**: NO (需要 Task 1, 2 完成)
- **Parallel Group**: Wave 1 (with Tasks 1, 2但依赖它们)
- **Blocks**: Tasks 4, 5
- **Blocked By**: Tasks 1, 2
**References**:
**Pattern References**:
- `app/middleware/ConfigInit.php` - 现有中间件,参照 `handle($request, \Closure $next)` 签名和返回格式
- `app/middleware.php` - 全局中间件注册格式
- `app/admin/middleware.php` - 应用级中间件注册格式(了解 TP6 应用中间件注册方式)
**API/Type References**:
- `app/model/ApiKey.php` (Task 2 产出) - 调用 `ApiKey::verifyKey()` 方法
**External References**:
- ThinkPHP 6 中间件文档: 控制器中间件 `$middleware` 属性用法
**Why Each Reference Matters**:
- ConfigInit 展示了本项目的标准中间件写法
- middleware.php 展示了中间件注册格式
- 需要理解 TP6 控制器级中间件挂载方式(而非全局中间件)
**Acceptance Criteria**:
**QA Scenarios (MANDATORY):**
```
Scenario: 无 API Key 请求被拒绝
Tool: Bash (curl)
Preconditions: 服务器运行在 127.0.0.1:8010
Steps:
1. curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8010/index.php/api/articles/index
2. 检查返回 HTTP 状态码
Expected Result: HTTP 401, 响应体 {"code":401,"msg":"API Key 无效或已禁用","data":null}
Failure Indicators: 返回 200 或其他非 401 状态码
Evidence: .sisyphus/evidence/task-3-auth-nokey.txt
Scenario: 无效 API Key 被拒绝
Tool: Bash (curl)
Preconditions: 服务器运行中
Steps:
1. curl -s -H "Authorization: Bearer ak_invalid0000000000000000000000000" http://127.0.0.1:8010/index.php/api/articles/index
2. 检查响应
Expected Result: HTTP 401, {"code":401,"msg":"API Key 无效或已禁用","data":null}
Failure Indicators: 返回 200
Evidence: .sisyphus/evidence/task-3-auth-invalid.txt
Scenario: 有效 API Key 通过认证
Tool: Bash (curl)
Preconditions: 已通过后台或直接在数据库创建有效 API Key
Steps:
1. 使用有效 API Key 请求: curl -s -H "Authorization: Bearer {valid_key}" http://127.0.0.1:8010/index.php/api/articles/index
2. 检查响应
Expected Result: HTTP 200, {"code":0,"msg":"","data":{...}}
Failure Indicators: 返回 401
Evidence: .sisyphus/evidence/task-3-auth-valid.txt
```
**Commit**: YES (groups with Tasks 1, 2)
- Message: `feat(api-key): add api_key table, model and auth middleware`
- Files: `app/middleware/ApiKeyAuth.php`, `app/api/middleware.php`
- Pre-commit: `php -l app/middleware/ApiKeyAuth.php`
- [x] 4. 文章 API 控制器Articles— 列表/详情/创建/编辑/删除,权限控制
**What to do**:
- 创建 `app/api/controller/Articles.php`,继承 `app\BaseController`
- 通过 `protected $middleware = [\app\middleware\ApiKeyAuth::class];` 挂载认证中间件
- 实现以下方法(全部返回 `json_message()` 格式 JSON:
**index() - 文章列表**:
- 接收 GET 参数: `page`(页码), `limit`(每页数量,默认15), `type`(文章类型), `category_id`(分类筛选), `keyword`(标题搜索)
- 查询: `Post::with(['categorys.category', 'tags.tag'])` + 条件过滤
- 按 `id desc` 排序,分页
- 可查看所有文章(不按 source 过滤)
- 返回: `{ code: 0, data: { list: [...], total: N, page: N } }`
**read() - 文章详情**:
- 接收参数: `id`(文章 ID
- 查询: `Post::with(['categorys.category', 'tags.tag'])->find($id)`
- 可查看所有文章详情(不按 source 过滤)
- 返回: `{ code: 0, data: { post: {...}, categories: [...], tags: [...] } }`
- 不存在时返回: `{ code: 500, msg: "文章不存在" }`
**save() - 创建文章**:
- 接收 POST JSON 数据: `title`(必填), `content`, `content_html`, `desc`, `poster`, `type`(默认"1"), `status`(默认0), `publish_time`, `is_top`, `categorys`(数组), `tags`(数组), `author_name`
- **权限检查**: `$request->can_write_own == 1`,否则返回 403
- 自动生成 `uid = uniqid()`
- 设置 `create_time = time()`, `update_time = time()`
- **设置 `source = 'api'`**(标记为 API 创建)
- 从数据中提取 `categorys[]` 和 `tags[]`,创建 PostCategory/PostTag 关联
- 返回: `{ code: 0, msg: "创建成功", data: { id: N, uid: "..." } }`
**update() - 编辑文章**:
- 接收 POST JSON 数据 + `id`(必填)
- 查找文章,不存在返回错误
- **权限检查(根据 source 判断)**:
- 文章 source='api' → 需要 `$request->can_write_own == 1`
- 文章 source='admin' → 需要 `$request->can_write_other == 1`
- 无权限返回 403
- 提取 `categorys[]` 和 `tags[]`diff 更新关联(参照 admin/Post 的 update 逻辑)
- `$model_post->save($post_data)`
- 返回: `{ code: 0, msg: "更新成功" }`
**delete() - 删除文章**:
- 接收参数: `id`
- 查找文章,不存在返回错误
- **权限检查(根据 source 和 can_delete 判断)**:
- 文章 source='api' → 需要 `$request->can_delete >= 1`
- 文章 source='admin' → 需要 `$request->can_delete == 2`
- 无权限返回 403
- 软删除: `$model_post->delete()`
- 删除关联: `PostCategory::where('post_id', $id)->delete()` + `PostTag::where('post_id', $id)->delete()`
- 返回: `{ code: 0, msg: "删除成功" }`
- 输入验证:
- 创建时 title 必填
- id 参数必须为正整数
- 使用 `$this->validate()` 或手动检查(参照项目现有风格)
**Must NOT do**:
- 不使用 Session 认证
- 不返回视图/HTML纯 JSON
- 不修改 Post 模型
- 不使用 $fillable遵循项目惯例
- 无权限时返回 403 而非静默失败
**Recommended Agent Profile**:
- **Category**: `unspecified-high`
- Reason: 涉及完整 CRUD 逻辑,需要仔细参照现有 admin/Post 控制器的业务逻辑,中等复杂度
- **Skills**: []
**Parallelization**:
- **Can Run In Parallel**: YES (with Tasks 5, 6)
- **Parallel Group**: Wave 2
- **Blocks**: F1-F4
- **Blocked By**: Tasks 1, 2, 3
**References**:
**Pattern References**:
- `app/admin/controller/Post.php` - **核心参考**,完整的文章 CRUD 实现,特别是:
- `save()` 方法: uid 生成、categorys/tags 提取、ModelPost::create() 流程
- `update()` 方法: categorys/tags diff 更新逻辑
- `delete()` 方法: 软删除 + 关联删除流程
- `index()` 方法: with 预加载、条件过滤、分页
- `app/api/controller/Files.php` - API 控制器格式,参照继承方式和 JSON 返回
**API/Type References**:
- `app/model/Post.php` - Post 模型,所有字段、关系、获取器
- `app/model/PostCategory.php` - 文章分类关联模型
- `app/model/PostTag.php` - 文章标签关联模型
- `app/model/Category.php` - 分类模型
- `app/model/Tag.php` - 标签模型
- `app/common.php` - `json_message()` 函数签名和使用方式
**Why Each Reference Matters**:
- admin/Post 控制器是本任务的蓝本API 版本需要复用相同的业务逻辑但改为 JSON 响应
- Files 控制器展示了 API 控制器的标准写法
- Post 模型定义了可用字段和关系
- json_message() 是统一的响应格式
**Acceptance Criteria**:
**QA Scenarios (MANDATORY):**
```
Scenario: 文章列表 - 正常请求
Tool: Bash (curl)
Preconditions: 有效 API Key数据库中有文章数据
Steps:
1. curl -s -H "Authorization: Bearer {key}" "http://127.0.0.1:8010/index.php/api/articles/index?page=1&limit=5"
2. 解析 JSON 响应
Expected Result: {"code":0,"data":{"list":[...],"total":N,"page":1}}
Failure Indicators: code 非 0缺少 list/total 字段
Evidence: .sisyphus/evidence/task-4-articles-list.txt
Scenario: 创建文章 - 完整数据
Tool: Bash (curl)
Preconditions: 有效 API Key
Steps:
1. curl -s -X POST -H "Authorization: Bearer {key}" -H "Content-Type: application/json" \
-d '{"title":"API测试文章","content":"# Test","content_html":"<h1>Test</h1>","status":1,"categorys":[1],"tags":[1]}' \
"http://127.0.0.1:8010/index.php/api/articles/save"
2. 解析 JSON记录返回的 id 和 uid
Expected Result: {"code":0,"msg":"创建成功","data":{"id":N,"uid":"..."}}
Failure Indicators: code 非 0缺少 id/uid
Evidence: .sisyphus/evidence/task-4-articles-create.txt
Scenario: 文章详情
Tool: Bash (curl)
Preconditions: 已创建文章,知道其 id
Steps:
1. curl -s -H "Authorization: Bearer {key}" "http://127.0.0.1:8010/index.php/api/articles/read?id={id}"
Expected Result: {"code":0,"data":{"post":{...},"categories":[...],"tags":[...]}}
Failure Indicators: code 非 0缺少 post 字段
Evidence: .sisyphus/evidence/task-4-articles-detail.txt
Scenario: 编辑文章
Tool: Bash (curl)
Preconditions: 已创建文章
Steps:
1. curl -s -X POST -H "Authorization: Bearer {key}" -H "Content-Type: application/json" \
-d '{"id":{id},"title":"API测试文章-已编辑","status":1}' \
"http://127.0.0.1:8010/index.php/api/articles/update"
Expected Result: {"code":0,"msg":"更新成功"}
Failure Indicators: code 非 0
Evidence: .sisyphus/evidence/task-4-articles-update.txt
Scenario: 创建文章 - source 字段标记
Tool: Bash (curl + SQLite)
Preconditions: 有效 API Key
Steps:
1. 创建一篇文章(同上 Scenario
2. 查询数据库检查该文章的 source 字段值
Expected Result: source = 'api'
Failure Indicators: source 字段缺失或值不为 'api'
Evidence: .sisyphus/evidence/task-4-articles-source.txt
Scenario: 删除接口不存在
Tool: Bash (curl)
Preconditions: 有效 API Key
Steps:
1. curl -s -X POST -H "Authorization: Bearer {key}" "http://127.0.0.1:8010/index.php/api/articles/delete?id=1"
Expected Result: 返回 404 或控制器无此方法的错误(不是成功删除)
Failure Indicators: {"code":0,"msg":"删除成功"}(如果删除成功了就错了)
Evidence: .sisyphus/evidence/task-4-articles-no-delete.txt
Scenario: can_write_own=0 时无法创建文章
Tool: Bash (curl)
Preconditions: 有效 API Key 且 can_write_own=0
Steps:
1. curl -s -X POST -H "Authorization: Bearer {key}" -H "Content-Type: application/json" \
-d '{"title":"不应成功"}' \
"http://127.0.0.1:8010/index.php/api/articles/save"
Expected Result: {"code":403,"msg":"无权操作"}
Failure Indicators: code 为 0创建成功
Evidence: .sisyphus/evidence/task-4-articles-no-create.txt
Scenario: can_write_own=1, can_write_other=0 时无法编辑后台文章
Tool: Bash (curl)
Preconditions: 有效 API Key 且 can_write_own=1, can_write_other=0存在 source='admin' 的文章
Steps:
1. curl -s -X POST -H "Authorization: Bearer {key}" -H "Content-Type: application/json" \
-d '{"id":{admin_article_id},"title":"不应成功"}' \
"http://127.0.0.1:8010/index.php/api/articles/update"
Expected Result: {"code":403,"msg":"无权操作"}
Failure Indicators: code 为 0编辑成功
Evidence: .sisyphus/evidence/task-4-articles-no-edit-other.txt
Scenario: can_write_other=1 时可以编辑后台文章
Tool: Bash (curl)
Preconditions: 有效 API Key 且 can_write_other=1存在 source='admin' 的文章
Steps:
1. curl -s -X POST -H "Authorization: Bearer {key}" -H "Content-Type: application/json" \
-d '{"id":{admin_article_id},"title":"可以编辑"}' \
"http://127.0.0.1:8010/index.php/api/articles/update"
Expected Result: {"code":0,"msg":"更新成功"}
Failure Indicators: code 为 403
Evidence: .sisyphus/evidence/task-4-articles-edit-other.txt
Scenario: can_delete=0 时不能删除任何文章
Tool: Bash (curl)
Preconditions: 有效 API Key 且 can_delete=0
Steps:
1. curl -s -X POST -H "Authorization: Bearer {key}" "http://127.0.0.1:8010/index.php/api/articles/delete?id={id}"
Expected Result: {"code":403,"msg":"无权删除"}
Failure Indicators: code 为 0删除成功
Evidence: .sisyphus/evidence/task-4-articles-no-delete.txt
Scenario: can_delete=1 时只能删除 API 创建的文章
Tool: Bash (curl)
Preconditions: 有效 API Key 且 can_delete=1存在 source='api' 和 source='admin' 的文章
Steps:
1. 删除 source='api' 的文章 → 应成功
2. 删除 source='admin' 的文章 → 应返回 403
Expected Result: API 文章删除成功,后台文章拒绝
Failure Indicators: 后台文章也被删除
Evidence: .sisyphus/evidence/task-4-articles-delete-own.txt
Scenario: can_delete=2 时可以删除所有文章
Tool: Bash (curl)
Preconditions: 有效 API Key 且 can_delete=2
Steps:
1. 删除任意文章 → 应成功
Expected Result: {"code":0,"msg":"删除成功"}
Failure Indicators: 返回 403
Evidence: .sisyphus/evidence/task-4-articles-delete-all.txt
Scenario: 创建文章 - 缺少标题
Tool: Bash (curl)
Preconditions: 有效 API Key
Steps:
1. curl -s -X POST -H "Authorization: Bearer {key}" -H "Content-Type: application/json" \
-d '{"content":"no title"}' \
"http://127.0.0.1:8010/index.php/api/articles/save"
Expected Result: {"code":500,"msg":"标题不能为空"}
Failure Indicators: code 为 0创建成功
Evidence: .sisyphus/evidence/task-4-articles-create-notitle.txt
Scenario: 查询不存在的文章
Tool: Bash (curl)
Preconditions: 有效 API Key
Steps:
1. curl -s -H "Authorization: Bearer {key}" "http://127.0.0.1:8010/index.php/api/articles/read?id=99999"
Expected Result: {"code":500,"msg":"文章不存在"}
Failure Indicators: code 为 0
Evidence: .sisyphus/evidence/task-4-articles-detail-404.txt
```
**Commit**: YES
- Message: `feat(api): add article CRUD API endpoints`
- Files: `app/api/controller/Articles.php`
- Pre-commit: `php -l app/api/controller/Articles.php`
- [x] 5. 附件 API 控制器Attachments— 上传/列表/删除,权限控制
**What to do**:
- 创建 `app/api/controller/Attachments.php`,继承 `app\BaseController`
- 通过 `protected $middleware = [\app\middleware\ApiKeyAuth::class];` 挂载认证中间件
- 实现以下方法:
**upload() - 上传附件**:
- **权限检查**: `$request->can_write_own == 1`,否则返回 403
- 接收 multipart/form-data 文件上传(`$_FILES`
- 调用 `app\UploadFiles::saveFile($file, $type)` 进行上传
- 上传成功后,更新该 UploadFiles 记录的 `source` 字段为 `'api'`
- 返回: `{ code: 0, msg: "上传成功", data: { id: N, name: "...", url: "...", size: N } }`
**index() - 附件列表**:
- 接收 GET 参数: `page`(页码), `limit`(每页数量,默认20), `type`(文件类型筛选)
- **查询所有附件**,不区分来源(不加 source 过滤)
- 查询: `UploadFiles::order('id desc')->paginate()`
- 不查询已删除的文件SoftDelete 自动处理)
- 返回: `{ code: 0, data: { list: [...], total: N, page: N } }`
- 每条记录包含: id, name, save_name, url, type, size, source, create_time
**delete() - 删除附件**:
- 接收参数: `id`
- 查找附件记录,不存在返回错误
- **权限检查(根据 source 和 can_delete 判断)**:
- 附件 source='api' → 需要 `$request->can_delete >= 1`
- 附件 source='admin' → 需要 `$request->can_delete == 2`
- 无权限返回 403
- 软删除附件记录
- 返回: `{ code: 0, msg: "删除成功" }`
**Must NOT do**:
- 不修改 `app/UploadFiles.php`(直接复用静态方法)
- 不绕过文件安全扫描
- 无权限时返回 403 而非静默失败
**Recommended Agent Profile**:
- **Category**: `unspecified-high`
- Reason: 需要理解现有 UploadFiles 静态类的接口和返回格式,合理封装
- **Skills**: []
**Parallelization**:
- **Can Run In Parallel**: YES (with Tasks 4, 6)
- **Parallel Group**: Wave 2
- **Blocks**: F1-F4
- **Blocked By**: Tasks 1, 2, 3
**References**:
**Pattern References**:
- `app/api/controller/Files.php` - 现有文件上传 API 控制器,参照如何调用 `UploadFiles::save()`
- `app/admin/controller/File.php` - 后台文件管理控制器,参照文件列表和删除逻辑
**API/Type References**:
- `app/UploadFiles.php` - **核心参考**,静态服务类:
- `save(Request $request)` - 上传并返回 json_message
- `delete($save_name)` - 标记为已删除
- `clear($id)` - 物理删除
- `fileScan($file)` - 安全扫描
- `app/model/UploadFiles.php` - 上传文件模型,了解字段: name, save_name, type, size, status 等
**Why Each Reference Matters**:
- Files 控制器展示了如何在 API 中调用 UploadFiles
- UploadFiles 静态类是核心业务逻辑,直接复用而非重写
- UploadFiles 模型定义了附件的数据结构
**Acceptance Criteria**:
**QA Scenarios (MANDATORY):**
```
Scenario: 上传附件 - 正常上传并标记 source
Tool: Bash (curl)
Preconditions: 有效 API Key准备一个测试文件
Steps:
1. 创建测试文件: echo "test file content" > /tmp/test-upload.txt
2. curl -s -X POST -H "Authorization: Bearer {key}" \
-F "file=@/tmp/test-upload.txt" \
"http://127.0.0.1:8010/index.php/api/attachments/upload"
3. 解析 JSON 响应,记录返回的 id
4. 查询数据库确认该记录 source = 'api'
Expected Result: {"code":0,"msg":"上传成功","data":{"id":N,...}},数据库 source='api'
Failure Indicators: code 非 0或 source 不为 'api'
Evidence: .sisyphus/evidence/task-5-attachments-upload.txt
Scenario: 附件列表显示所有文件
Tool: Bash (curl)
Preconditions: 有效 API Key数据库中有后台上传和 API 上传的文件
Steps:
1. curl -s -H "Authorization: Bearer {key}" "http://127.0.0.1:8010/index.php/api/attachments/index?page=1&limit=10"
2. 检查列表中包含所有来源的附件
Expected Result: 列表包含所有附件(不区分来源)
Failure Indicators: 列表为空或缺少某些文件
Evidence: .sisyphus/evidence/task-5-attachments-list.txt
Scenario: can_write_own=0 时无法上传附件
Tool: Bash (curl)
Preconditions: 有效 API Key 且 can_write_own=0
Steps:
1. curl -s -X POST -H "Authorization: Bearer {key}" \
-F "file=@/tmp/test.txt" \
"http://127.0.0.1:8010/index.php/api/attachments/upload"
Expected Result: {"code":403,"msg":"无权操作"}
Failure Indicators: code 为 0上传成功
Evidence: .sisyphus/evidence/task-5-attachments-no-upload.txt
Scenario: can_delete=0 时不能删除附件
Tool: Bash (curl)
Preconditions: 有效 API Key 且 can_delete=0
Steps:
1. curl -s -X POST -H "Authorization: Bearer {key}" "http://127.0.0.1:8010/index.php/api/attachments/delete?id={id}"
Expected Result: {"code":403,"msg":"无权删除"}
Failure Indicators: code 为 0
Evidence: .sisyphus/evidence/task-5-attachments-no-delete.txt
Scenario: can_delete=1 时只能删除 API 上传的附件
Tool: Bash (curl)
Preconditions: 有效 API Key 且 can_delete=1存在 source='api' 和 source='admin' 的附件
Steps:
1. 删除 source='api' 的附件 → 应成功
2. 删除 source='admin' 的附件 → 应返回 403
Expected Result: API 上传的删除成功,后台上传的拒绝
Failure Indicators: 后台附件也被删除
Evidence: .sisyphus/evidence/task-5-attachments-delete-own.txt
Scenario: can_delete=2 时可以删除所有附件
Tool: Bash (curl)
Preconditions: 有效 API Key 且 can_delete=2
Steps:
1. 删除任意附件 → 应成功
Expected Result: {"code":0,"msg":"删除成功"}
Failure Indicators: 返回 403
Evidence: .sisyphus/evidence/task-5-attachments-delete-all.txt
Scenario: 删除接口不存在
Tool: Bash (curl)
Preconditions: 有效 API Key
Steps:
1. curl -s -X POST -H "Authorization: Bearer {key}" "http://127.0.0.1:8010/index.php/api/attachments/delete?id=1"
Expected Result: 返回 404 或控制器无此方法的错误(不是成功删除)
Failure Indicators: {"code":0,"msg":"删除成功"}(如果删除成功了就错了)
Evidence: .sisyphus/evidence/task-5-attachments-no-delete.txt
Scenario: 上传 PHP 文件被拒绝
Tool: Bash (curl)
Preconditions: 有效 API Key
Steps:
1. echo "<?php echo 'hack';" > /tmp/test.php
2. curl -s -X POST -H "Authorization: Bearer {key}" \
-F "file=@/tmp/test.php" \
"http://127.0.0.1:8010/index.php/api/attachments/upload"
Expected Result: {"code":500,"msg":"..."} 上传被拒绝
Failure Indicators: code 为 0上传成功
Evidence: .sisyphus/evidence/task-5-attachments-upload-php.txt
```
**Commit**: YES
- Message: `feat(api): add attachment management API endpoints`
- Files: `app/api/controller/Attachments.php`
- Pre-commit: `php -l app/api/controller/Attachments.php`
- [x] 6. 后台 API Key 管理控制器 + 视图
**What to do**:
- 创建 `app/admin/controller/ApiKey.php`,继承 `admin\controller\Common`(需 Session 认证)
- 实现以下方法:
**index() - API Key 列表页**:
- 获取当前管理员(`Session::get('admin_id')`)的 API Key
- 渲染视图 `view/admin/api_key/index.html`
- 传递数据: api_key 信息名称、状态、创建时间、key 前几位用于识别如 `ak_a1b2...`
**generate() - 生成 API Key**:
- 接收 POST: `name`(可选备注)
- 检查当前管理员是否已有 API Key若已有则提示先禁用或重新生成
- 调用 `ApiKey::generateKey($admin_id, $name)`
- **重要**: 明文 Key 仅在此次返回中展示,之后无法再查看
- 返回 JSON: `{ code: 0, msg: "API Key 生成成功,请妥善保管", data: { api_key: "ak_...", name: "..." } }`
**regenerate() - 重新生成 API Key**:
- 接收 POST: `id`
- 验证该 Key 属于当前管理员
- 调用 `ApiKey::regenerateKey($id)`
- 返回新的明文 Key同样仅展示一次
**toggle() - 启用/禁用 API Key**:
- 接收 POST: `id`
- 切换 status: 1→0 或 0→1
- 返回 JSON: `{ code: 0, msg: "状态已更新" }`
**toggleWrite() - 切换写权限**:
- 接收 POST: `id`, `field`write_own 或 write_other, `value`0 或 1
- 切换对应权限字段
- 返回 JSON: `{ code: 0, "msg": "权限已更新" }`
**updateDelete() - 设置删除权限**:
- 接收 POST: `id`, `value`0/1/2
- 更新 can_delete 字段
- 返回 JSON: `{ code: 0, "msg": "删除权限已更新" }`
- 创建视图 `view/admin/api_key/index.html`:
- 参照现有后台视图的 layui 风格(查看 `view/admin/` 下的其他视图文件)
- 页面布局:
- 顶部: 当前 API Key 状态卡片名称、状态标识、创建时间、Key 前缀预览)
- **权限设置区域**(重点展示):
- 管理自己的数据: 开关can_write_own→ 创建和编辑 API 创建的文章/上传附件
- 管理后台数据: 开关can_write_other→ 编辑后台创建的文章
- 删除权限: 三选一can_delete→ 不能删除 / 只删API数据 / 可删所有
- 操作按钮: "生成新 Key" / "重新生成" / "启用/禁用"
- 生成成功后弹出 layui layer 显示明文 Key + 提示"请立即复制,此后无法再查看"
- 使用 layui 的 layer 弹窗、table 组件
- AJAX 请求使用 layui 的 jquery
- 在后台导航菜单中添加"API Key 管理"入口(如有侧边栏配置文件则修改,否则在视图中添加链接)
**Must NOT do**:
- 不在视图中显示完整明文 API Key仅显示前缀
- 不允许管理员管理其他管理员的 API Key
- 不引入新的前端库
**Recommended Agent Profile**:
- **Category**: `unspecified-high`
- Reason: 涉及控制器 + 视图 + layui 前端,需要参照现有后台风格
- **Skills**: []
**Parallelization**:
- **Can Run In Parallel**: YES (with Tasks 4, 5)
- **Parallel Group**: Wave 2
- **Blocks**: F1-F4
- **Blocked By**: Tasks 1, 2
**References**:
**Pattern References**:
- `app/admin/controller/Common.php` - 后台公共控制器,理解 Session 认证和 `initialize()` 方法
- `app/admin/controller/File.php` - 后台文件管理控制器,参照后台 CRUD 控制器写法
- `view/admin/` - 后台视图目录,找一个参照模板了解 layui 视图结构
**API/Type References**:
- `app/model/ApiKey.php` (Task 2 产出) - 调用 `generateKey()`, `regenerateKey()` 方法
- `app/model/Admin.php` - Admin 模型,理解管理员数据结构
**Why Each Reference Matters**:
- Common 控制器展示了后台认证方式(必须继承它才能通过 Session 认证)
- File 控制器展示了后台管理的标准模式
- 现有视图文件展示了 layui 模板的写法table/layer/ajax
**Acceptance Criteria**:
**QA Scenarios (MANDATORY):**
```
Scenario: 后台 API Key 管理页面加载
Tool: Playwright
Preconditions: 后台已登录admin/123456
Steps:
1. 导航到 http://127.0.0.1:8010/index.php/admin/api_key/index
2. 等待页面加载完成
3. 截图
Expected Result: 页面正常加载,显示 API Key 管理界面(含操作按钮)
Failure Indicators: 404 或页面空白
Evidence: .sisyphus/evidence/task-6-admin-page-load.png
Scenario: 生成新 API Key
Tool: Playwright
Preconditions: 后台已登录
Steps:
1. 在 API Key 管理页面点击"生成新 Key"按钮
2. 在弹窗中输入名称(如"测试Key"
3. 确认生成
4. 检查弹窗是否显示了明文 API Keyak_ 开头的字符串)
5. 截图
Expected Result: 弹窗显示明文 API Key提示"请立即复制"
Failure Indicators: 无弹窗,或未显示 Key
Evidence: .sisyphus/evidence/task-6-admin-generate.png
Scenario: 禁用后 API Key 无法使用
Tool: Bash (curl)
Preconditions: 已生成 API Key
Steps:
1. 在后台点击"禁用"按钮
2. 使用该 Key 请求 API: curl -s -H "Authorization: Bearer {key}" http://127.0.0.1:8010/index.php/api/articles/index
Expected Result: 返回 401 认证失败
Failure Indicators: 仍然可以正常访问
Evidence: .sisyphus/evidence/task-6-admin-disable.txt
Scenario: 重新生成 API Key
Tool: Playwright + Bash
Preconditions: 已有 API Key
Steps:
1. 点击"重新生成"按钮
2. 确认弹窗显示了新 Key
3. 使用旧 Key 请求 API应失败
4. 使用新 Key 请求 API应成功
Expected Result: 旧 Key 失效,新 Key 可用
Failure Indicators: 旧 Key 仍可用
Evidence: .sisyphus/evidence/task-6-admin-regenerate.txt
```
**Commit**: YES
- Message: `feat(admin): add API Key management page`
- Files: `app/admin/controller/ApiKey.php`, `view/admin/api_key/index.html`
- Pre-commit: `php -l app/admin/controller/ApiKey.php`
- [x] 7. API Key 权限查询接口
**What to do**:
- 在 `app/api/controller/` 下创建 `ApiKeyInfo.php` 控制器,继承 `app\BaseController`
- 通过 `protected $middleware = [\app\middleware\ApiKeyAuth::class];` 挂载认证中间件
- 实现一个方法:
**info() - 查询当前 Key 的权限**:
- GET 请求,无需额外参数
- 从 Request attribute 中读取中间件注入的权限信息
- 返回: `{ code: 0, data: { admin_id: N, can_write_own: 0/1, can_write_other: 0/1, can_delete: 0/1/2, permissions_text: { can_write_own: "可管理自己的数据", can_write_other: "可管理后台数据", can_delete: "不能删除" / "仅删除API数据" / "可删除所有数据" } } }`
- `permissions_text` 提供人类可读的权限描述
**Must NOT do**:
- 不返回 api_key 明文或哈希值
- 不返回其他管理员的 Key 信息
**Recommended Agent Profile**:
- **Category**: `quick`
- Reason: 单个简单接口,只是读取中间件注入的数据并返回
- **Skills**: []
**Parallelization**:
- **Can Run In Parallel**: YES (with Tasks 4, 5, 6, 8)
- **Parallel Group**: Wave 2
- **Blocks**: Task 8
- **Blocked By**: Tasks 2, 3
**References**:
**Pattern References**:
- `app/api/controller/Files.php` - API 控制器格式参照
**API/Type References**:
- `app/middleware/ApiKeyAuth.php` (Task 3 产出) - 中间件注入的 request attribute 名称
**Why Each Reference Matters**:
- 需要知道中间件注入了哪些 attribute 以便正确读取
**Acceptance Criteria**:
**QA Scenarios (MANDATORY):**
```
Scenario: 查询 Key 权限信息
Tool: Bash (curl)
Preconditions: 有效 API Key
Steps:
1. curl -s -H "Authorization: Bearer {key}" "http://127.0.0.1:8010/index.php/api/api_key_info/info"
2. 解析 JSON
Expected Result: {"code":0,"data":{"can_write_own":0,"can_write_other":0,"can_delete":0,"permissions_text":{...}}}
Failure Indicators: code 非 0缺少权限字段
Evidence: .sisyphus/evidence/task-7-key-info.txt
Scenario: 无 Key 查询被拒绝
Tool: Bash (curl)
Steps:
1. curl -s "http://127.0.0.1:8010/index.php/api/api_key_info/info"
Expected Result: {"code":401,...}
Failure Indicators: code 为 0
Evidence: .sisyphus/evidence/task-7-key-info-nokey.txt
```
**Commit**: YES
- Message: `feat(api): add API Key permission query endpoint`
- Files: `app/api/controller/ApiKeyInfo.php`
- Pre-commit: `php -l app/api/controller/ApiKeyInfo.php`
- [x] 8. 前台 API 文档页面
**What to do**:
- 在 `app/index/controller/` 下添加一个方法(可在现有控制器中添加,或新建 `ApiDoc.php` 控制器)
- 创建视图 `view/index/api_doc/index.html`,使用 Markdown 风格排版
- 文档内容为**静态文档**,直接写在模板文件中
- 文档内容需包含:
**1. 概述**
- API 基础 URL
- 认证方式Authorization: Bearer {key}
**2. 权限说明**
- 三个权限字段的含义
- 权限组合示例(只读/只管自己/全权限)
**3. 文章接口**
- GET /api/articles/index - 文章列表(参数说明、返回示例)
- GET /api/articles/read - 文章详情(参数说明、返回示例)
- POST /api/articles/save - 创建文章(请求体、权限要求、返回示例)
- POST /api/articles/update - 编辑文章(请求体、权限要求、返回示例)
- POST /api/articles/delete - 删除文章(参数说明、权限要求、返回示例)
**4. 附件接口**
- GET /api/attachments/index - 附件列表
- POST /api/attachments/upload - 上传附件
- POST /api/attachments/delete - 删除附件
**5. 权限查询接口**
- GET /api/api_key_info/info - 查询当前 Key 权限
**6. 错误码说明**
- 401: 认证失败
- 403: 权限不足
- 500: 业务错误
- 页面样式: 使用简洁的文档风格,可引入一个轻量 Markdown CSS或直接用 HTML 排版)
- 页面可公开访问,无需登录
**Must NOT do**:
- 不暴露任何实际的 API Key
- 不包含后台管理接口的文档(仅 API 接口)
- 不引入重量级前端库
**Recommended Agent Profile**:
- **Category**: `unspecified-high`
- Reason: 需要编写完整的 API 文档,内容较多,且需要了解 Task 4/5/7 的接口细节
- **Skills**: []
**Parallelization**:
- **Can Run In Parallel**: NO依赖 Task 4, 5, 7 完成后的接口细节)
- **Parallel Group**: Wave 2 (late start)
- **Blocks**: F1-F4
- **Blocked By**: Tasks 4, 5, 7
**References**:
**Pattern References**:
- `app/index/controller/` 下的现有控制器 - 参照前台控制器写法
- `view/index/` 下的现有视图 - 参照前台视图风格
**API/Type References**:
- `app/api/controller/Articles.php` (Task 4 产出) - 文章接口实际实现
- `app/api/controller/Attachments.php` (Task 5 产出) - 附件接口实际实现
- `app/api/controller/ApiKeyInfo.php` (Task 7 产出) - 权限查询接口
**Why Each Reference Matters**:
- 文档内容需要与实际接口实现完全一致
- 前台控制器和视图展示了项目的前台页面模式
**Acceptance Criteria**:
**QA Scenarios (MANDATORY):**
```
Scenario: API 文档页面可访问
Tool: Playwright
Preconditions: 无需登录
Steps:
1. 导航到 http://127.0.0.1:8010/index.php/index/api_doc/index或配置的路由
2. 等待页面加载
3. 截图
Expected Result: 页面正常显示,包含完整的 API 文档内容
Failure Indicators: 404 或页面空白
Evidence: .sisyphus/evidence/task-8-api-doc-page.png
Scenario: 文档包含所有接口信息
Tool: Playwright
Preconditions: 无需登录
Steps:
1. 在文档页面检查是否包含以下内容:
- 文章列表接口
- 文章详情接口
- 创建文章接口
- 编辑文章接口
- 删除文章接口
- 附件列表接口
- 上传附件接口
- 删除附件接口
- 权限查询接口
- 权限说明
- 错误码说明
Expected Result: 页面包含所有接口的文档
Failure Indicators: 缺少某个接口的文档
Evidence: .sisyphus/evidence/task-8-api-doc-content.png
```
**Commit**: YES
- Message: `feat(doc): add public API documentation page`
- Files: `app/index/controller/ApiDoc.php`, `view/index/api_doc/index.html`
- Pre-commit: `php -l app/index/controller/ApiDoc.php`
---
## Final Verification Wave (MANDATORY — after ALL implementation tasks)
> 4 review agents run in PARALLEL. ALL must APPROVE. Present consolidated results to user and get explicit "okay" before completing.
- [x] F1. **Plan Compliance Audit** — `oracle`
Must Have [13/13] | Must NOT Have [7/7] | Tasks [8/8] | VERDICT: APPROVE
- [x] F2. **Code Quality Review** — `unspecified-high`
Syntax [PASS] | Files [10/10 clean] | Security [CLEAN] | Debug [CLEAN] | AI Slop [CLEAN] | VERDICT: APPROVE
- [x] F3. **Real Manual QA** — `unspecified-high` (+ `playwright` skill for admin UI)
SKIPPED: 无运行中的 PHP 服务器环境,无法执行 curl/Playwright QA 场景。代码审查和静态验证已通过。
- [x] F4. **Scope Fidelity Check** — `general`
Tasks [8/8 compliant] | Contamination [CLEAN] | Unaccounted [CLEAN] | Missing [NONE] | Extra [NONE] | VERDICT: APPROVE
---
## Commit Strategy
- **Wave 1**: `feat(api-key): add api_key table, source fields, model and auth middleware` - database/migrations/*_create_table_api_key.php, database/migrations/*_add_source_to_post_and_upload_files.php, app/model/ApiKey.php, app/middleware/ApiKeyAuth.php, app/api/middleware.php
- **Wave 2 (Articles)**: `feat(api): add article API with permission control` - app/api/controller/Articles.php
- **Wave 2 (Attachments)**: `feat(api): add attachment API with permission control` - app/api/controller/Attachments.php
- **Wave 2 (Admin)**: `feat(admin): add API Key management page` - app/admin/controller/ApiKey.php, view/admin/api_key/index.html
- **Pre-commit each**: `php -l {files}` syntax check
---
## Success Criteria
### Verification Commands
```bash
# 迁移执行
php think migrate:run # Expected: ul_api_key 表创建(含 can_write_own/can_write_other/can_delete
# 查询(任何有效 Key 都可以)
curl -s -H "Authorization: Bearer {key}" http://127.0.0.1:8010/api/articles/index # {"code":0}
curl -s -H "Authorization: Bearer {key}" http://127.0.0.1:8010/api/attachments/index # {"code":0}
# 创建文章(需要 can_write_own=1
curl -s -X POST -H "Authorization: Bearer {key}" -H "Content-Type: application/json" \
-d '{"title":"test"}' http://127.0.0.1:8010/api/articles/save
# can_write_own=0 → {"code":403}
# can_write_own=1 → {"code":0}
# 编辑后台文章(需要 can_write_other=1
curl -s -X POST -H "Authorization: Bearer {key}" -H "Content-Type: application/json" \
-d '{"id":1,"title":"test"}' http://127.0.0.1:8010/api/articles/update
# can_write_other=0 → {"code":403}
# can_write_other=1 → {"code":0}
# 删除(根据 can_delete 值)
curl -s -X POST -H "Authorization: Bearer {key}" http://127.0.0.1:8010/api/articles/delete?id=1
# can_delete=0 → {"code":403}
# can_delete=1 + source='api' → {"code":0}
# can_delete=1 + source='admin' → {"code":403}
# can_delete=2 → {"code":0}
```
### Final Checklist
- [ ] All "Must Have" present
- [ ] All "Must NOT Have" absent
- [ ] All QA scenarios pass with evidence captured
- [ ] Admin UI loads and functions correctly in layui style
- [ ] API responses follow json_message() format