From dc116a1c77dcf3294e6da6c2488a954e2aa53aad Mon Sep 17 00:00:00 2001 From: augushong Date: Mon, 27 Apr 2026 00:38:57 +0800 Subject: [PATCH] feat(api): add article/attachment API endpoints, admin management, and API docs - Articles API: list/detail/create/update/delete with source-based permission control - Attachments API: upload/list/delete with source-based permission control - ApiKeyInfo API: query current key permissions - Admin ApiKey management: generate/regenerate/toggle/permission settings with layui UI - Frontend API documentation page with complete interface reference --- app/admin/controller/ApiKey.php | 130 +++++++ app/api/controller/ApiKeyInfo.php | 33 ++ app/api/controller/Articles.php | 241 +++++++++++++ app/api/controller/Attachments.php | 124 +++++++ app/index/controller/ApiDoc.php | 16 + view/admin/api_key/index.html | 283 +++++++++++++++ view/index/api_doc/index.html | 538 +++++++++++++++++++++++++++++ 7 files changed, 1365 insertions(+) create mode 100644 app/admin/controller/ApiKey.php create mode 100644 app/api/controller/ApiKeyInfo.php create mode 100644 app/api/controller/Articles.php create mode 100644 app/api/controller/Attachments.php create mode 100644 app/index/controller/ApiDoc.php create mode 100644 view/admin/api_key/index.html create mode 100644 view/index/api_doc/index.html diff --git a/app/admin/controller/ApiKey.php b/app/admin/controller/ApiKey.php new file mode 100644 index 0000000..882dde3 --- /dev/null +++ b/app/admin/controller/ApiKey.php @@ -0,0 +1,130 @@ +find(); + + if (!empty($api_key)) { + $api_key->api_key_preview = substr($api_key->getData('api_key'), 0, 8) . '...'; + $api_key->status_text = $api_key->status == 1 ? '启用' : '禁用'; + $api_key->create_time_text = date('Y-m-d H:i:s', $api_key->create_time); + } + + View::assign('api_key', $api_key); + View::assign('admin_info', $this->adminInfo); + + return View::fetch(); + } + + /** + * 生成 API Key + */ + public function generate() + { + $admin_id = Session::get('admin_id'); + $name = $this->request->param('name', ''); + + $exists = ApiKeyModel::where('admin_id', $admin_id)->find(); + if (!empty($exists)) { + return json_message('您已有 API Key,如需更换请使用重新生成'); + } + + $raw_key = ApiKeyModel::generateKey($admin_id, $name); + + return json_message(['api_key' => $raw_key, 'name' => $name], 0, 'API Key 生成成功,请妥善保管'); + } + + /** + * 重新生成 API Key + */ + public function regenerate() + { + $id = $this->request->param('id', 0); + $admin_id = Session::get('admin_id'); + + $api_key = ApiKeyModel::find($id); + if (empty($api_key) || $api_key->admin_id != $admin_id) { + return json_message('API Key 不存在或无权操作'); + } + + $raw_key = ApiKeyModel::regenerateKey($id); + + return json_message(['api_key' => $raw_key], 0, 'API Key 已重新生成'); + } + + /** + * 启用/禁用 API Key + */ + public function toggle() + { + $id = $this->request->param('id', 0); + $admin_id = Session::get('admin_id'); + + $api_key = ApiKeyModel::find($id); + if (empty($api_key) || $api_key->admin_id != $admin_id) { + return json_message('API Key 不存在或无权操作'); + } + + $api_key->status = $api_key->status == 1 ? 0 : 1; + $api_key->save(); + + return json_message('', 0, '状态已更新'); + } + + /** + * 切换写权限 + */ + public function toggleWrite() + { + $id = $this->request->param('id', 0); + $field = $this->request->param('field', ''); + $value = $this->request->param('value', 0); + $admin_id = Session::get('admin_id'); + + if (!in_array($field, ['can_write_own', 'can_write_other'])) { + return json_message('无效的权限字段'); + } + + $api_key = ApiKeyModel::find($id); + if (empty($api_key) || $api_key->admin_id != $admin_id) { + return json_message('API Key 不存在或无权操作'); + } + + $api_key->$field = $value ? 1 : 0; + $api_key->save(); + + return json_message('', 0, '权限已更新'); + } + + /** + * 设置删除权限 + */ + public function updateDelete() + { + $id = $this->request->param('id', 0); + $value = $this->request->param('value', 0); + $admin_id = Session::get('admin_id'); + + $api_key = ApiKeyModel::find($id); + if (empty($api_key) || $api_key->admin_id != $admin_id) { + return json_message('API Key 不存在或无权操作'); + } + + $api_key->can_delete = intval($value); + $api_key->save(); + + return json_message('', 0, '删除权限已更新'); + } +} diff --git a/app/api/controller/ApiKeyInfo.php b/app/api/controller/ApiKeyInfo.php new file mode 100644 index 0000000..7b5a6dc --- /dev/null +++ b/app/api/controller/ApiKeyInfo.php @@ -0,0 +1,33 @@ +request; + + $can_delete_text = '不能删除'; + if ($request->can_delete == 1) { + $can_delete_text = '仅删除API数据'; + } elseif ($request->can_delete == 2) { + $can_delete_text = '可删除所有数据'; + } + + return json_message([ + 'admin_id' => $request->admin_id, + 'can_write_own' => $request->can_write_own, + 'can_write_other' => $request->can_write_other, + 'can_delete' => $request->can_delete, + 'permissions_text' => [ + 'can_write_own' => $request->can_write_own ? '可管理自己的数据' : '不可管理自己的数据', + 'can_write_other' => $request->can_write_other ? '可管理后台数据' : '不可管理后台数据', + 'can_delete' => $can_delete_text, + ], + ]); + } +} diff --git a/app/api/controller/Articles.php b/app/api/controller/Articles.php new file mode 100644 index 0000000..ccf7677 --- /dev/null +++ b/app/api/controller/Articles.php @@ -0,0 +1,241 @@ +param('page', 1, 'intval'); + $limit = $request->param('limit', 15, 'intval'); + $type = $request->param('type', ''); + $category_id = $request->param('category_id', 0, 'intval'); + $keyword = $request->param('keyword', ''); + + $model_list = Post::with(['categorys.category', 'tags.tag']) + ->order('id desc'); + + if (!empty($type)) { + $model_list = $model_list->where('type', $type); + } + if (!empty($category_id)) { + $model_list = $model_list->where('category_id', $category_id); + } + if (!empty($keyword)) { + $model_list = $model_list->where('title', 'like', "%{$keyword}%"); + } + + $list = $model_list->paginate([ + 'page' => $page, + 'list_rows' => $limit, + ]); + + return json_message([ + 'list' => $list->items(), + 'total' => $list->total(), + 'page' => $page, + ]); + } + + /** + * 文章详情 + */ + public function read(Request $request) + { + $id = $request->param('id', 0, 'intval'); + if ($id <= 0) { + return json_message('参数错误'); + } + + $post = Post::with(['categorys.category', 'tags.tag'])->find($id); + if (empty($post)) { + return json_message('文章不存在'); + } + + return json_message(['post' => $post]); + } + + /** + * 创建文章 + */ + public function save(Request $request) + { + if ($request->can_write_own != 1) { + return json_message('无权操作', 403); + } + + $post_data = $request->post(); + + if (empty($post_data['title'])) { + return json_message('标题必填'); + } + + $categorys = []; + $tags = []; + if (isset($post_data['categorys'])) { + $categorys = $post_data['categorys']; + unset($post_data['categorys']); + } + if (isset($post_data['tags'])) { + $tags = $post_data['tags']; + unset($post_data['tags']); + } + + $post_data['uid'] = uniqid(); + $post_data['source'] = 'api'; + $post_data['create_time'] = time(); + $post_data['update_time'] = time(); + + if (!isset($post_data['type'])) { + $post_data['type'] = '1'; + } + if (!isset($post_data['status'])) { + $post_data['status'] = 0; + } + + $model_post = Post::create($post_data); + + foreach ($categorys as $category) { + PostCategory::create([ + 'post_id' => $model_post->id, + 'category_id' => $category, + ]); + } + foreach ($tags as $tag) { + PostTag::create([ + 'post_id' => $model_post->id, + 'tag_id' => $tag, + ]); + } + + return json_message(['id' => $model_post->id, 'uid' => $model_post->uid], 0, '创建成功'); + } + + /** + * 编辑文章 + */ + public function update(Request $request) + { + $id = $request->param('id', 0, 'intval'); + if ($id <= 0) { + return json_message('参数错误'); + } + + $model_post = Post::find($id); + if (empty($model_post)) { + return json_message('文章不存在'); + } + + $source = $model_post->getData('source'); + if ($source === 'api') { + if ($request->can_write_own != 1) { + return json_message('无权操作', 403); + } + } else { + if ($request->can_write_other != 1) { + return json_message('无权操作', 403); + } + } + + $post_data = $request->post(); + unset($post_data['id']); + + // categorys diff update + if (isset($post_data['categorys'])) { + $categorys = $post_data['categorys']; + unset($post_data['categorys']); + + $old_category_list = PostCategory::where('post_id', $id)->select(); + $old_category_ids = array_column($old_category_list->toArray(), 'category_id'); + + // old has, new doesn't -> delete + foreach ($old_category_list as $model_category) { + if (!in_array($model_category->category_id, $categorys)) { + $model_category->delete(); + } + } + // new has, old doesn't -> create + foreach ($categorys as $category) { + if (!in_array($category, $old_category_ids)) { + PostCategory::create([ + 'post_id' => $id, + 'category_id' => $category, + ]); + } + } + } + + // tags diff update + if (isset($post_data['tags'])) { + $tags = $post_data['tags']; + unset($post_data['tags']); + + $old_tag_list = PostTag::where('post_id', $id)->select(); + $old_tag_ids = array_column($old_tag_list->toArray(), 'tag_id'); + + foreach ($old_tag_list as $model_tag) { + if (!in_array($model_tag->tag_id, $tags)) { + $model_tag->delete(); + } + } + foreach ($tags as $tag) { + if (!in_array($tag, $old_tag_ids)) { + PostTag::create([ + 'post_id' => $id, + 'tag_id' => $tag, + ]); + } + } + } + + $model_post->save($post_data); + + return json_message([], 0, '更新成功'); + } + + /** + * 删除文章 + */ + public function delete(Request $request) + { + $id = $request->param('id', 0, 'intval'); + if ($id <= 0) { + return json_message('参数错误'); + } + + $model_post = Post::find($id); + if (empty($model_post)) { + return json_message('文章不存在'); + } + + $source = $model_post->getData('source'); + if ($source === 'api') { + if ($request->can_delete < 1) { + return json_message('无权删除', 403); + } + } else { + if ($request->can_delete != 2) { + return json_message('无权删除', 403); + } + } + + $model_post->delete(); + PostCategory::where('post_id', $id)->delete(); + PostTag::where('post_id', $id)->delete(); + + return json_message([], 0, '删除成功'); + } +} diff --git a/app/api/controller/Attachments.php b/app/api/controller/Attachments.php new file mode 100644 index 0000000..0ab8f1f --- /dev/null +++ b/app/api/controller/Attachments.php @@ -0,0 +1,124 @@ +request->param('page', 1, 'intval'); + $limit = $this->request->param('limit', 20, 'intval'); + $type = $this->request->param('type', ''); + + $query = UploadFilesModel::order('id desc'); + + if (!empty($type)) { + $query->where('type', $type); + } + + $list = $query->paginate([ + 'list_rows' => $limit, + 'page' => $page, + ]); + + $items = []; + foreach ($list as $item) { + $items[] = [ + 'id' => $item->id, + 'name' => $item->getData('file_name'), + 'save_name' => $item->getData('save_name'), + 'url' => $item->src, + 'type' => $item->getData('type'), + 'size' => $item->getData('file_size'), + 'source' => $item->getData('source'), + 'create_time' => $item->create_time, + ]; + } + + return json_message([ + 'list' => $items, + 'total' => $list->total(), + 'page' => $page, + ]); + } + + /** + * 上传附件 + */ + public function upload() + { + if (empty($this->request->can_write_own)) { + return json_message('无权操作', 403); + } + + $file = $this->request->file('file'); + if (empty($file)) { + return json_message('请选择上传文件'); + } + + try { + AppUploadFiles::fileScan($file); + $model_file = AppUploadFiles::saveFile($file, 'api_upload'); + + $upload_model = UploadFilesModel::where('save_name', $model_file->getData('save_name'))->find(); + if ($upload_model) { + $upload_model->source = 'api'; + $upload_model->save(); + } + + return json_message([ + 'id' => $model_file->id, + 'name' => $model_file->getData('file_name'), + 'url' => $model_file->src, + 'size' => $model_file->getData('file_size'), + ], 0, '上传成功'); + } catch (\Throwable $th) { + return json_message($th->getMessage()); + } + } + + /** + * 删除附件 + */ + public function delete() + { + $id = $this->request->param('id', 0, 'intval'); + if (empty($id)) { + return json_message('缺少参数'); + } + + $file = UploadFilesModel::find($id); + if (empty($file)) { + return json_message('附件不存在'); + } + + $source = $file->getData('source'); + $can_delete = $this->request->can_delete; + + if ($source === 'api' && $can_delete < 1) { + return json_message('无权操作', 403); + } + + if ($source === 'admin' && $can_delete != 2) { + return json_message('无权操作', 403); + } + + if (!in_array($source, ['api', 'admin']) && $can_delete < 2) { + return json_message('无权操作', 403); + } + + $file->delete(); + + return json_message('', 0, '删除成功'); + } +} diff --git a/app/index/controller/ApiDoc.php b/app/index/controller/ApiDoc.php new file mode 100644 index 0000000..6fbefea --- /dev/null +++ b/app/index/controller/ApiDoc.php @@ -0,0 +1,16 @@ + + + + + + + + API Key 管理 + {include file="common/_require"} + + + + + + +
+ {include file="common/_header"} + + {include file="common/left_admin"} + +
+ +
+ +
+
+
API Key 管理
+
+ + {empty name="api_key"} + +
+

您还没有 API Key,点击下方按钮生成一个。

+
+
+ +
+ +
+
+
+
+ +
+
+
+
+ {else /} + +
+ + + + + + + + + + + + + + + + + + + + + +
名称{$api_key.name|default='-'}
Key{$api_key.api_key_preview}
状态 + {if condition="$api_key.status == 1"} + 启用 + {else /} + 禁用 + {/if} +
创建时间{$api_key.create_time_text}
+
+ + +
+ 权限设置 +
+
+
+ +
+ 创建/编辑 API 数据 + +
+
+
+ +
+ 编辑后台创建的数据 + +
+
+
+ +
+ + + +
+
+
+ + +
+ + +
+ {/empty} + +
+
+
+
+
+ + {include file="common/_footer"} +
+ + + + + diff --git a/view/index/api_doc/index.html b/view/index/api_doc/index.html new file mode 100644 index 0000000..47ace2e --- /dev/null +++ b/view/index/api_doc/index.html @@ -0,0 +1,538 @@ + + + + + + + {$page_title}-{:get_system_config('site_name')} + {include file='common/_require'/} + + + + +
+
+ {include file='common/_left'/} +
+
+
+ +

1. 概述

+ +

API 基础 URL:{域名}/index.php/api/

+ +

认证方式

+

所有 API 请求需在 Header 中携带 API Key,支持以下两种方式:

+
Authorization: Bearer {api_key} +X-API-Key: {api_key}
+ +

响应格式

+
{ + "code": 0, + "msg": "success", + "data": {} +}
+ + + + + + + + + +
字段类型说明
codeint状态码:0=成功, 401=认证失败, 403=权限不足, 500=业务错误
msgstring提示信息
dataobject/array返回数据
+ +

2. 权限说明

+

每个 API Key 对应一组权限,由管理员在后台分配:

+ + + + + + + + + +
权限字段说明
can_write_own0 或 1能否创建和编辑通过 API 创建的数据
can_write_other0 或 1能否编辑通过后台管理创建的数据
can_delete0 / 1 / 20=不能删除, 1=仅删除 API 创建的数据, 2=可删除所有数据
+ +

权限组合示例

+ + + + + + + + + +
组合can_write_owncan_write_othercan_delete
只读000
只管自己101
全权限112
+ +

3. 文章接口

+ +

GET /api/articles/index - 文章列表 所有有效 Key

+
+

请求参数

+ + + + + + + + + + + +
参数类型必填说明
pageint页码,默认 1
limitint每页数量,默认 15
typestring文章类型
category_idint分类 ID
keywordstring搜索关键词
+ +

请求示例

+
curl -X GET \ + "https://example.com/index.php/api/articles/index?page=1&limit=10" \ + -H "Authorization: Bearer your_api_key"
+ +

返回示例

+
{ + "code": 0, + "msg": "success", + "data": { + "list": [ + { + "id": 1, + "title": "文章标题", + "content": "文章内容", + "type": "1", + "status": 1, + "source": "api", + "create_time": 1700000000, + "update_time": 1700000000, + "categorys": [...], + "tags": [...] + } + ], + "total": 100, + "page": 1 + } +}
+
+ +

GET /api/articles/read - 文章详情 所有有效 Key

+
+

请求参数

+ + + + + + + +
参数类型必填说明
idint文章 ID
+ +

请求示例

+
curl -X GET \ + "https://example.com/index.php/api/articles/read?id=1" \ + -H "Authorization: Bearer your_api_key"
+ +

返回示例

+
{ + "code": 0, + "msg": "success", + "data": { + "post": { + "id": 1, + "title": "文章标题", + "content": "文章内容", + "content_html": "<p>HTML内容</p>", + "desc": "摘要", + "poster": "/uploads/poster.jpg", + "type": "1", + "status": 1, + "source": "api" + }, + "categories": [...], + "tags": [...] + } +}
+
+ +

POST /api/articles/save - 创建文章 can_write_own=1

+
+

Content-Type: application/json

+

通过此接口创建的文章会自动标记 source='api'

+ +

请求参数

+ + + + + + + + + + + + + + + +
参数类型必填说明
titlestring文章标题
contentstring文章内容(Markdown)
content_htmlstring文章内容(HTML)
descstring文章摘要
posterstring封面图 URL
typestring文章类型,默认 "1"
statusint状态,默认 0(草稿)
categorysarray分类 ID 数组,如 [1, 2]
tagsarray标签 ID 数组,如 [1, 2]
+ +

请求示例

+
curl -X POST \ + "https://example.com/index.php/api/articles/save" \ + -H "Authorization: Bearer your_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "title": "新文章标题", + "content": "文章内容", + "status": 1, + "categorys": [1, 2], + "tags": [3] + }'
+ +

返回示例

+
{ + "code": 0, + "msg": "创建成功", + "data": { + "id": 42, + "uid": "6501a2b3c4d5e" + } +}
+
+ +

POST /api/articles/update - 编辑文章 需对应写入权限

+
+

Content-Type: application/json

+

权限规则:

+
    +
  • source='api' 的文章:需要 can_write_own=1
  • +
  • source='admin' 的文章:需要 can_write_other=1
  • +
+ +

请求参数

+ + + + + + + + + + + + + + + +
参数类型必填说明
idint文章 ID
titlestring文章标题
contentstring文章内容(Markdown)
content_htmlstring文章内容(HTML)
descstring文章摘要
posterstring封面图 URL
statusint状态
categorysarray分类 ID 数组(全量覆盖)
tagsarray标签 ID 数组(全量覆盖)
+ +

请求示例

+
curl -X POST \ + "https://example.com/index.php/api/articles/update" \ + -H "Authorization: Bearer your_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "id": 42, + "title": "修改后的标题", + "status": 1 + }'
+ +

返回示例

+
{ + "code": 0, + "msg": "更新成功", + "data": [] +}
+
+ +

POST /api/articles/delete - 删除文章 需对应删除权限

+
+

权限规则:

+
    +
  • source='api' 的文章:需要 can_delete >= 1
  • +
  • source='admin' 的文章:需要 can_delete = 2
  • +
+ +

请求参数

+ + + + + + + +
参数类型必填说明
idint文章 ID
+ +

请求示例

+
curl -X POST \ + "https://example.com/index.php/api/articles/delete" \ + -H "Authorization: Bearer your_api_key" \ + -d "id=42"
+ +

返回示例

+
{ + "code": 0, + "msg": "删除成功", + "data": [] +}
+
+ +

4. 附件接口

+ +

GET /api/attachments/index - 附件列表 所有有效 Key

+
+

请求参数

+ + + + + + + + + +
参数类型必填说明
pageint页码,默认 1
limitint每页数量,默认 20
typestring附件类型
+ +

请求示例

+
curl -X GET \ + "https://example.com/index.php/api/attachments/index?page=1&limit=10" \ + -H "Authorization: Bearer your_api_key"
+ +

返回示例

+
{ + "code": 0, + "msg": "success", + "data": { + "list": [ + { + "id": 1, + "name": "image.png", + "save_name": "20260401_abc.png", + "url": "/uploads/20260401_abc.png", + "type": "image", + "size": 102400, + "source": "api", + "create_time": 1700000000 + } + ], + "total": 50, + "page": 1 + } +}
+
+ +

POST /api/attachments/upload - 上传附件 can_write_own=1

+
+

Content-Type: multipart/form-data

+

上传的附件会自动标记 source='api'

+ +

请求参数

+ + + + + + + +
参数类型必填说明
filefile上传的文件
+ +

请求示例

+
curl -X POST \ + "https://example.com/index.php/api/attachments/upload" \ + -H "Authorization: Bearer your_api_key" \ + -F "file=@/path/to/image.png"
+ +

返回示例

+
{ + "code": 0, + "msg": "上传成功", + "data": { + "id": 10, + "name": "image.png", + "url": "/uploads/20260401_abc.png", + "size": 102400 + } +}
+
+ +

POST /api/attachments/delete - 删除附件 需对应删除权限

+
+

权限规则:

+
    +
  • source='api' 的附件:需要 can_delete >= 1
  • +
  • source='admin' 的附件:需要 can_delete = 2
  • +
+ +

请求参数

+ + + + + + + +
参数类型必填说明
idint附件 ID
+ +

请求示例

+
curl -X POST \ + "https://example.com/index.php/api/attachments/delete" \ + -H "Authorization: Bearer your_api_key" \ + -d "id=10"
+ +

返回示例

+
{ + "code": 0, + "msg": "删除成功", + "data": [] +}
+
+ +

5. 权限查询接口

+ +

GET /api/api_key_info/info - 查询当前 Key 权限 所有有效 Key

+
+

查询当前 API Key 的权限信息,无需额外参数。

+ +

请求示例

+
curl -X GET \ + "https://example.com/index.php/api/api_key_info/info" \ + -H "Authorization: Bearer your_api_key"
+ +

返回示例

+
{ + "code": 0, + "msg": "success", + "data": { + "admin_id": 1, + "can_write_own": 1, + "can_write_other": 0, + "can_delete": 1, + "permissions_text": { + "can_write_own": "可管理自己的数据", + "can_write_other": "不可管理后台数据", + "can_delete": "仅删除API数据" + } + } +}
+
+ +

6. 错误码

+ + + + + + + + + + +
错误码说明
0成功
401认证失败(无效或已禁用的 API Key)
403权限不足
500业务错误(参数缺失、资源不存在等)
+ +
+
+
+ {include file='common/_right'/} +
+
+ + +