mirror of
https://gitee.com/ulthon/ulthon_information.git
synced 2026-07-09 06:22:48 +08:00
- 添加 category-api 的 problems、decisions、learnings、issues 文档 - 添加 apikey-article-api 的 issues、decisions、learnings 文档 - 包含架构决策、问题记录和学习总结
1272 lines
54 KiB
Markdown
1272 lines
54 KiB
Markdown
# 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":"<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 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
|