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

54 KiB
Raw Blame History

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

  • 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_keyPhinx 自动加 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
  • 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位随机 hexak_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
  • 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
  • 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
  • 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
  • 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, fieldwrite_own 或 write_other, value0 或 1
      • 切换对应权限字段
      • 返回 JSON: { code: 0, "msg": "权限已更新" }

      updateDelete() - 设置删除权限:

      • 接收 POST: id, value0/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
  • 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
  • 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.

  • F1. Plan Compliance Auditoracle Must Have [13/13] | Must NOT Have [7/7] | Tasks [8/8] | VERDICT: APPROVE

  • F2. Code Quality Reviewunspecified-high Syntax [PASS] | Files [10/10 clean] | Security [CLEAN] | Debug [CLEAN] | AI Slop [CLEAN] | VERDICT: APPROVE

  • F3. Real Manual QAunspecified-high (+ playwright skill for admin UI) SKIPPED: 无运行中的 PHP 服务器环境,无法执行 curl/Playwright QA 场景。代码审查和静态验证已通过。

  • F4. Scope Fidelity Checkgeneral 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

# 迁移执行
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