# 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,关联管理员 ID(NOT 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 Key(32位 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":"
Test
","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 " /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 Key(ak_ 开头的字符串)
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