feat(content-type): add content_type field, markdown auto-conversion, and API doc updates

- Add content_type column to ul_post via migration
- Install league/commonmark for markdown->HTML conversion
- Add Post model accessors/setters for content_type and content
- Update API Articles controller save/update with content_type support
- Update API docs with content_type parameter and markdown example

Closes content-type-support plan
This commit is contained in:
augushong
2026-04-29 20:46:44 +08:00
parent 0e8944bc7f
commit 6f332467df
5 changed files with 104 additions and 2 deletions

View File

@@ -8,6 +8,7 @@ use app\BaseController;
use app\model\Post;
use app\model\PostCategory;
use app\model\PostTag;
use League\CommonMark\CommonMarkConverter;
use think\Request;
class Articles extends BaseController
@@ -105,6 +106,21 @@ class Articles extends BaseController
if (!isset($post_data['status'])) {
$post_data['status'] = 0;
}
if (!isset($post_data['content_type'])) {
$post_data['content_type'] = 'html';
}
// content_type 白名单校验
$allowed_content_types = ['html', 'markdown'];
if (!in_array($post_data['content_type'], $allowed_content_types)) {
return json_message('无效的content_type', 500);
}
// Markdown→HTML 自动转换
if ($post_data['content_type'] === 'markdown' && !empty($post_data['content'])) {
$converter = new CommonMarkConverter(['html_input' => 'strip', 'allow_unsafe_links' => false]);
$post_data['content_html'] = $converter->convert($post_data['content']);
}
$model_post = Post::create($post_data);
@@ -201,6 +217,12 @@ class Articles extends BaseController
}
}
// Markdown→HTML 自动转换
if (isset($post_data['content_type']) && $post_data['content_type'] === 'markdown' && !empty($post_data['content'])) {
$converter = new CommonMarkConverter(['html_input' => 'strip', 'allow_unsafe_links' => false]);
$post_data['content_html'] = $converter->convert($post_data['content']);
}
$model_post->save($post_data);
return json_message([], 0, '更新成功');

View File

@@ -156,6 +156,30 @@ class Post extends Base
return trim($value);
}
public function getContentTypeAttr($value)
{
return $value ?: 'html';
}
public function setContentTypeAttr($value)
{
if (!in_array($value, ['html', 'markdown'], true)) {
throw new \InvalidArgumentException('Invalid content_type: ' . $value);
}
return $value;
}
public function setContentAttr($value)
{
return trim($value);
}
public function getContentAttr($value)
{
return $value;
}
public function getContentMarkdownAttr()
{
$content_html = $this->getAttr('content_html');

View File

@@ -33,7 +33,8 @@
"topthink/think-filesystem": "^2.0",
"mibe/feedwriter": "^1.1",
"matomo/device-detector": "^6.1",
"intervention/image": "^2.7"
"intervention/image": "^2.7",
"league/commonmark": "^2.8"
},
"require-dev": {
"symfony/var-dumper": "^4.2"

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
use think\migration\Migrator;
use think\migration\db\Column;
class AddContentTypeToPost extends Migrator
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* http://docs.phinx.org/en/latest/migrations.html#the-abstractmigration-class
*
* The following commands can be used in this method and Phinx will
* automatically reverse them when rolling back:
*
* createTable
* renameTable
* addColumn
* renameColumn
* addIndex
* addForeignKey
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change()
{
$this->table('post')->addColumn(
Column::make('content_type', 'string', ['limit' => 20, 'default' => 'html', 'comment' => '内容类型: html/markdown'])
)->update();
}
}

View File

@@ -196,6 +196,7 @@ X-API-Key: {api_key}</div>
"id": 1,
"title": "文章标题",
"content": "文章内容",
"content_type": "html",
"type": "1",
"status": 1,
"source": "api",
@@ -238,6 +239,7 @@ X-API-Key: {api_key}</div>
"title": "文章标题",
"content": "文章内容",
"content_html": "&lt;p&gt;HTML内容&lt;/p&gt;",
"content_type": "html",
"desc": "摘要",
"poster": "/uploads/poster.jpg",
"type": "1",
@@ -268,12 +270,13 @@ X-API-Key: {api_key}</div>
<tr><td>poster</td><td>string</td><td></td><td>封面图 URL</td></tr>
<tr><td>type</td><td>string</td><td></td><td>文章类型,默认 "1"</td></tr>
<tr><td>status</td><td>int</td><td></td><td>状态,默认 0草稿</td></tr>
<tr><td>content_type</td><td>string</td><td></td><td>内容类型: "html"(默认) 或 "markdown"</td></tr>
<tr><td>categorys</td><td>array</td><td></td><td>分类 ID 数组,如 [1, 2]</td></tr>
<tr><td>tags</td><td>array</td><td></td><td>标签 ID 数组,如 [1, 2]</td></tr>
</tbody>
</table>
<h4>请求示例</h4>
<h4>请求示例HTML</h4>
<div class="api-code">curl -X POST \
"https://example.com/index.php/api/articles/save" \
-H "Authorization: Bearer your_api_key" \
@@ -286,6 +289,21 @@ X-API-Key: {api_key}</div>
"tags": [3]
}'</div>
<h4>请求示例Markdown</h4>
<div class="api-code">curl -X POST \
"https://example.com/index.php/api/articles/save" \
-H "Authorization: Bearer your_api_key" \
-H "Content-Type: application/json" \
-d '{
"title": "Markdown 文章",
"content": "# 标题\n\n段落内容",
"content_type": "markdown",
"type": "1",
"status": 1,
"categorys": [1, 2],
"tags": [3]
}'</div>
<h4>返回示例</h4>
<div class="api-code">{
"code": 0,
@@ -319,6 +337,7 @@ X-API-Key: {api_key}</div>
<tr><td>desc</td><td>string</td><td></td><td>文章摘要</td></tr>
<tr><td>poster</td><td>string</td><td></td><td>封面图 URL</td></tr>
<tr><td>status</td><td>int</td><td></td><td>状态</td></tr>
<tr><td>content_type</td><td>string</td><td></td><td>内容类型: "html"(默认) 或 "markdown"</td></tr>
<tr><td>categorys</td><td>array</td><td></td><td>分类 ID 数组(全量覆盖)</td></tr>
<tr><td>tags</td><td>array</td><td></td><td>标签 ID 数组(全量覆盖)</td></tr>
</tbody>