add blog system
Some checks are pending
Docker Build and Push / build (push) Waiting to run

This commit is contained in:
tsuki 2025-08-12 21:25:17 +08:00
parent efe9a3c88e
commit 1d0e96ead4
79 changed files with 5504 additions and 27 deletions

View File

@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM settings_blocks WHERE id = $1 RETURNING id",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false
]
},
"hash": "006684071c99cb1c0801395a7723cfc5444953c0f31733ad4f1035b078d385cb"
}

View File

@ -36,7 +36,7 @@
{
"ordinal": 6,
"name": "created_at: DateTime<Utc>",
"type_info": "Timestamp"
"type_info": "Timestamptz"
}
],
"parameters": {

View File

@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM data_points WHERE chart_block_id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": []
},
"hash": "063b7d3782eb878facd48a0d24c6b3f8248aa8b2bd8878edc4f27c0d73778527"
}

View File

@ -0,0 +1,21 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO table_columns (table_block_id, name, label, data_type, is_sortable, is_filterable, width, \"order\")\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Varchar",
"Varchar",
"Varchar",
"Bool",
"Bool",
"Int4",
"Int4"
]
},
"nullable": []
},
"hash": "08d8ede4405c75d35650b1d96d6f0cbacaa6be26fb0c9ce2085b7f4961f13681"
}

View File

@ -0,0 +1,52 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id, chart_block_id, x, y, label, color\n FROM data_points WHERE chart_block_id = $1 ORDER BY x\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "chart_block_id",
"type_info": "Uuid"
},
{
"ordinal": 2,
"name": "x",
"type_info": "Float8"
},
{
"ordinal": 3,
"name": "y",
"type_info": "Float8"
},
{
"ordinal": 4,
"name": "label",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "color",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
false,
false,
false,
true,
true
]
},
"hash": "0dfd961e9f4524bde4bc151d7a7e9d4e1bac40ef1ebc022c3782794f2b60a263"
}

View File

@ -0,0 +1,68 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id, title, slug, description, is_active, \n created_at as \"created_at: DateTime<Utc>\", \n updated_at as \"updated_at: DateTime<Utc>\", \n created_by, updated_by\n FROM pages WHERE slug LIKE '%-settings' AND is_active = true\n ORDER BY title\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "title",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "slug",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "description",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "is_active",
"type_info": "Bool"
},
{
"ordinal": 5,
"name": "created_at: DateTime<Utc>",
"type_info": "Timestamptz"
},
{
"ordinal": 6,
"name": "updated_at: DateTime<Utc>",
"type_info": "Timestamptz"
},
{
"ordinal": 7,
"name": "created_by",
"type_info": "Uuid"
},
{
"ordinal": 8,
"name": "updated_by",
"type_info": "Uuid"
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
false,
false,
true,
false,
false,
false,
true,
true
]
},
"hash": "0e04293e919fb78f8b908a495f6f5e8ef311d65ff63311796f2bf498626bd2b7"
}

View File

@ -0,0 +1,94 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id, page_id, block_order, title, subtitle, background_image, background_color, text_color, cta_text, cta_link, is_active, created_at as \"created_at: DateTime<Utc>\", updated_at as \"updated_at: DateTime<Utc>\"\n FROM hero_blocks WHERE page_id = $1 AND is_active = true ORDER BY block_order\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "page_id",
"type_info": "Uuid"
},
{
"ordinal": 2,
"name": "block_order",
"type_info": "Int4"
},
{
"ordinal": 3,
"name": "title",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "subtitle",
"type_info": "Text"
},
{
"ordinal": 5,
"name": "background_image",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "background_color",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "text_color",
"type_info": "Varchar"
},
{
"ordinal": 8,
"name": "cta_text",
"type_info": "Varchar"
},
{
"ordinal": 9,
"name": "cta_link",
"type_info": "Varchar"
},
{
"ordinal": 10,
"name": "is_active",
"type_info": "Bool"
},
{
"ordinal": 11,
"name": "created_at: DateTime<Utc>",
"type_info": "Timestamptz"
},
{
"ordinal": 12,
"name": "updated_at: DateTime<Utc>",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
false,
false,
false,
true,
true,
true,
true,
true,
true,
false,
false,
false
]
},
"hash": "12ac5e85271d509743c9e4e199bf209459c163d9727f9393490a2782e7290ec9"
}

View File

@ -51,12 +51,12 @@
{
"ordinal": 9,
"name": "created_at: DateTime<Utc>",
"type_info": "Timestamp"
"type_info": "Timestamptz"
},
{
"ordinal": 10,
"name": "updated_at: DateTime<Utc>",
"type_info": "Timestamp"
"type_info": "Timestamptz"
},
{
"ordinal": 11,

View File

@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM hero_blocks WHERE id = $1 RETURNING id",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false
]
},
"hash": "1a070734e3029a0bafa3b3e175925f9eb8557db25003f3b49e846c1fd685c0c5"
}

View File

@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) as count FROM text_blocks WHERE page_id = $1 AND is_active = true",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
null
]
},
"hash": "1f4479ead07e8c7b3188251d166fdb0bdae0bca20ec36b6afac183e57c0f714c"
}

View File

@ -0,0 +1,68 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE text_blocks \n SET title = COALESCE($1, title),\n markdown = COALESCE($2, markdown),\n block_order = COALESCE($3, block_order),\n is_active = COALESCE($4, is_active),\n updated_at = NOW()\n WHERE id = $5\n RETURNING id, page_id, block_order, title, markdown, is_active, \n created_at as \"created_at: DateTime<Utc>\", \n updated_at as \"updated_at: DateTime<Utc>\"\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "page_id",
"type_info": "Uuid"
},
{
"ordinal": 2,
"name": "block_order",
"type_info": "Int4"
},
{
"ordinal": 3,
"name": "title",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "markdown",
"type_info": "Text"
},
{
"ordinal": 5,
"name": "is_active",
"type_info": "Bool"
},
{
"ordinal": 6,
"name": "created_at: DateTime<Utc>",
"type_info": "Timestamptz"
},
{
"ordinal": 7,
"name": "updated_at: DateTime<Utc>",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Varchar",
"Text",
"Int4",
"Bool",
"Uuid"
]
},
"nullable": [
false,
false,
false,
true,
false,
false,
false,
false
]
},
"hash": "20146adf2fda79bf8231af4f79dc2eea047c47beb5ee45693bdde230b64493b6"
}

View File

@ -0,0 +1,24 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE chart_blocks SET block_order = $1, updated_at = NOW() WHERE id = $2 AND page_id = $3 RETURNING id",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Int4",
"Uuid",
"Uuid"
]
},
"nullable": [
false
]
},
"hash": "250839c4ff5724972b78b1eaec6d39629adfaa3ba3767e369abb4da5fc5d82ef"
}

View File

@ -0,0 +1,70 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id, table_block_id, name, label, data_type, is_sortable, is_filterable, width, \"order\"\n FROM table_columns WHERE table_block_id = $1 ORDER BY \"order\"\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "table_block_id",
"type_info": "Uuid"
},
{
"ordinal": 2,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "label",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "data_type",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "is_sortable",
"type_info": "Bool"
},
{
"ordinal": 6,
"name": "is_filterable",
"type_info": "Bool"
},
{
"ordinal": 7,
"name": "width",
"type_info": "Int4"
},
{
"ordinal": 8,
"name": "order",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
true,
false
]
},
"hash": "285bbb0fac6996ba3c061dd52e3514c2d5957df529fd5b3bf9acf781781853d0"
}

View File

@ -0,0 +1,34 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT \n COUNT(*) as total_count,\n COUNT(CASE WHEN is_system = true THEN 1 END) as system_count,\n COUNT(CASE WHEN is_editable = true THEN 1 END) as editable_count\n FROM settings \n WHERE category = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "total_count",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "system_count",
"type_info": "Int8"
},
{
"ordinal": 2,
"name": "editable_count",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null,
null,
null
]
},
"hash": "2d32cb71ebda39278cfe19cf6f84d5643874dc0c61ebfa4b1b3bbd97623c86ab"
}

View File

@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM hero_blocks WHERE page_id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": []
},
"hash": "32079198d949123912be2fd9a5f258a29c443fdda04f5dfc06c2d7a0bcb5e31d"
}

View File

@ -51,12 +51,12 @@
{
"ordinal": 9,
"name": "created_at: DateTime<Utc>",
"type_info": "Timestamp"
"type_info": "Timestamptz"
},
{
"ordinal": 10,
"name": "updated_at: DateTime<Utc>",
"type_info": "Timestamp"
"type_info": "Timestamptz"
},
{
"ordinal": 11,

View File

@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) as count FROM hero_blocks WHERE page_id = $1 AND is_active = true",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
null
]
},
"hash": "34db85e1513819a46067e2412145c2bdd8555071031847f7cd9c2a43f9febb66"
}

View File

@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM settings_blocks WHERE page_id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": []
},
"hash": "36e750deedcdd11bad52c216695f3163b35fe21b9da0757f44141bdba7ac8efa"
}

View File

@ -0,0 +1,70 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id, table_block_id, name, label, data_type, is_sortable, is_filterable, width, \"order\"\n FROM table_columns WHERE table_block_id = $1 ORDER BY \"order\"\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "table_block_id",
"type_info": "Uuid"
},
{
"ordinal": 2,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "label",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "data_type",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "is_sortable",
"type_info": "Bool"
},
{
"ordinal": 6,
"name": "is_filterable",
"type_info": "Bool"
},
{
"ordinal": 7,
"name": "width",
"type_info": "Int4"
},
{
"ordinal": 8,
"name": "order",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
true,
false
]
},
"hash": "37f549d3f13dedc4d10e31cf4979412c28e2e06055f3f39bb311ca84e7f703a8"
}

View File

@ -0,0 +1,24 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE text_blocks SET block_order = $1, updated_at = NOW() WHERE id = $2 AND page_id = $3 RETURNING id",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Int4",
"Uuid",
"Uuid"
]
},
"nullable": [
false
]
},
"hash": "37f8e6d81450fd2a18a8b201cfa234fc0141f90bdd6e517005b04ccff7b0bc9b"
}

View File

@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM table_blocks WHERE id = $1 RETURNING id",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false
]
},
"hash": "3fdd78820ae558e250ffe01931f5dcc39d78641efa0610879678a4cc60d10d82"
}

View File

@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM table_blocks WHERE page_id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": []
},
"hash": "45d9c3d0fc785b8e0ca6056de391e62dc5552a3420849576b3521981b3d3537d"
}

View File

@ -0,0 +1,82 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO settings_blocks (page_id, block_order, title, category, editable, display_mode, is_active)\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n RETURNING id, page_id, block_order, title, category, editable, display_mode, is_active, \n created_at as \"created_at: DateTime<Utc>\", \n updated_at as \"updated_at: DateTime<Utc>\"\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "page_id",
"type_info": "Uuid"
},
{
"ordinal": 2,
"name": "block_order",
"type_info": "Int4"
},
{
"ordinal": 3,
"name": "title",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "category",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "editable",
"type_info": "Bool"
},
{
"ordinal": 6,
"name": "display_mode",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "is_active",
"type_info": "Bool"
},
{
"ordinal": 8,
"name": "created_at: DateTime<Utc>",
"type_info": "Timestamptz"
},
{
"ordinal": 9,
"name": "updated_at: DateTime<Utc>",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Uuid",
"Int4",
"Varchar",
"Varchar",
"Bool",
"Varchar",
"Bool"
]
},
"nullable": [
false,
false,
false,
true,
false,
false,
false,
false,
false,
false
]
},
"hash": "4721f4799049974626de030a22ffcb9d9475dc902120915755c059f79151fd7b"
}

View File

@ -0,0 +1,24 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE hero_blocks SET block_order = $1, updated_at = NOW() WHERE id = $2 AND page_id = $3 RETURNING id",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Int4",
"Uuid",
"Uuid"
]
},
"nullable": [
false
]
},
"hash": "4d880f3063c6a80fb091867b94e40ebc6aef1e1053b8992b23c92b5e4f63c8a3"
}

View File

@ -0,0 +1,75 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO chart_blocks (page_id, block_order, title, chart_type, config, is_active)\n VALUES ($1, $2, $3, $4, $5, $6)\n RETURNING id, page_id, block_order, title, chart_type, config, is_active, \n created_at as \"created_at: DateTime<Utc>\", \n updated_at as \"updated_at: DateTime<Utc>\"\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "page_id",
"type_info": "Uuid"
},
{
"ordinal": 2,
"name": "block_order",
"type_info": "Int4"
},
{
"ordinal": 3,
"name": "title",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "chart_type",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "config",
"type_info": "Jsonb"
},
{
"ordinal": 6,
"name": "is_active",
"type_info": "Bool"
},
{
"ordinal": 7,
"name": "created_at: DateTime<Utc>",
"type_info": "Timestamptz"
},
{
"ordinal": 8,
"name": "updated_at: DateTime<Utc>",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Uuid",
"Int4",
"Varchar",
"Varchar",
"Jsonb",
"Bool"
]
},
"nullable": [
false,
false,
false,
false,
false,
true,
false,
false,
false
]
},
"hash": "4dec15acfaf647eaee056655cb3743b97e53c11bc1f68d16ac7c4cc79388f067"
}

View File

@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) as count FROM settings_blocks WHERE page_id = $1 AND is_active = true",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
null
]
},
"hash": "5c667496c3cb12aa0fb4e498de6c1a5bc6424e078b0d19c1bc1af4b70e0d5abe"
}

View File

@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COALESCE(SUM(view_count), 0) FROM blogs WHERE is_active = true",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "coalesce",
"type_info": "Int8"
}
],
"parameters": {
"Left": []
},
"nullable": [
null
]
},
"hash": "5f747eb2f4b37872b7dd9cc83fc0c969afbbd32595c6b94f9fed470f449a633f"
}

View File

@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) FROM blogs WHERE status = 'draft' AND is_active = true",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": []
},
"nullable": [
null
]
},
"hash": "6224838e35f5aaa67c3f9f94de194c4ff0121aedca4aea52777bc3d0085805a4"
}

View File

@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) FROM blog_categories WHERE is_active = true",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": []
},
"nullable": [
null
]
},
"hash": "67f4ecc3acdf4abfa9fc45c0d889fec43110633c7c9ae745ad9918a4fead1245"
}

View File

@ -51,12 +51,12 @@
{
"ordinal": 9,
"name": "created_at: DateTime<Utc>",
"type_info": "Timestamp"
"type_info": "Timestamptz"
},
{
"ordinal": 10,
"name": "updated_at: DateTime<Utc>",
"type_info": "Timestamp"
"type_info": "Timestamptz"
},
{
"ordinal": 11,

View File

@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) FROM blog_tags WHERE is_active = true",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": []
},
"nullable": [
null
]
},
"hash": "6c7cb940fca35c491af8b4b07c111f2dbf6b58deea19db4eb3eafa7e4e5c316a"
}

View File

@ -0,0 +1,18 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO data_points (chart_block_id, x, y, label, color)\n VALUES ($1, $2, $3, $4, $5)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Float8",
"Float8",
"Varchar",
"Varchar"
]
},
"nullable": []
},
"hash": "6cbfa10af054707a593004f379031731a9c89f52471dd8ef353fe462fc5eb251"
}

View File

@ -0,0 +1,70 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id, title, slug, description, is_active, \n created_at as \"created_at: DateTime<Utc>\", \n updated_at as \"updated_at: DateTime<Utc>\", \n created_by, updated_by\n FROM pages WHERE id = $1 AND is_active = true\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "title",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "slug",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "description",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "is_active",
"type_info": "Bool"
},
{
"ordinal": 5,
"name": "created_at: DateTime<Utc>",
"type_info": "Timestamptz"
},
{
"ordinal": 6,
"name": "updated_at: DateTime<Utc>",
"type_info": "Timestamptz"
},
{
"ordinal": 7,
"name": "created_by",
"type_info": "Uuid"
},
{
"ordinal": 8,
"name": "updated_by",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
false,
false,
true,
false,
false,
false,
true,
true
]
},
"hash": "7049523511ae5c39c433eedecd8ea2f3d61a045f5504d9646bd051038d0f9cfb"
}

View File

@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM pages WHERE id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": []
},
"hash": "74b30e4a74f27fa5d0e7f5a758185db3453c0eb51d154f90a89e66e8e92c8421"
}

View File

@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM text_blocks WHERE id = $1 RETURNING id",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false
]
},
"hash": "74bd3010cb42f1a124df3ecfbb93c3feafb25eaf1730343ba0ba4e7c9fd7cf1b"
}

View File

@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) FROM blogs WHERE is_active = true",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": []
},
"nullable": [
null
]
},
"hash": "753787e580c2fc8c48bafc58b8d62b0f4328f988efda30c10d3dc41d7089c35b"
}

View File

@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM chart_blocks WHERE page_id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": []
},
"hash": "76d3196348484270fd83699229c0db618eaee8640219493b83765eacf769b922"
}

View File

@ -0,0 +1,103 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE hero_blocks \n SET title = COALESCE($1, title),\n subtitle = COALESCE($2, subtitle),\n background_image = COALESCE($3, background_image),\n background_color = COALESCE($4, background_color),\n text_color = COALESCE($5, text_color),\n cta_text = COALESCE($6, cta_text),\n cta_link = COALESCE($7, cta_link),\n block_order = COALESCE($8, block_order),\n is_active = COALESCE($9, is_active),\n updated_at = NOW()\n WHERE id = $10\n RETURNING id, page_id, block_order, title, subtitle, background_image, background_color, text_color, cta_text, cta_link, is_active, \n created_at as \"created_at: DateTime<Utc>\", \n updated_at as \"updated_at: DateTime<Utc>\"\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "page_id",
"type_info": "Uuid"
},
{
"ordinal": 2,
"name": "block_order",
"type_info": "Int4"
},
{
"ordinal": 3,
"name": "title",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "subtitle",
"type_info": "Text"
},
{
"ordinal": 5,
"name": "background_image",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "background_color",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "text_color",
"type_info": "Varchar"
},
{
"ordinal": 8,
"name": "cta_text",
"type_info": "Varchar"
},
{
"ordinal": 9,
"name": "cta_link",
"type_info": "Varchar"
},
{
"ordinal": 10,
"name": "is_active",
"type_info": "Bool"
},
{
"ordinal": 11,
"name": "created_at: DateTime<Utc>",
"type_info": "Timestamptz"
},
{
"ordinal": 12,
"name": "updated_at: DateTime<Utc>",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Varchar",
"Text",
"Varchar",
"Varchar",
"Varchar",
"Varchar",
"Varchar",
"Int4",
"Bool",
"Uuid"
]
},
"nullable": [
false,
false,
false,
false,
true,
true,
true,
true,
true,
true,
false,
false,
false
]
},
"hash": "76ebd5c5e6d6234192b323dcbc1ad30a115851dd6664a6538fe5b85586edb0fc"
}

View File

@ -0,0 +1,75 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE table_blocks \n SET title = COALESCE($1, title),\n data_source = COALESCE($2, data_source),\n data_config = COALESCE($3, data_config),\n block_order = COALESCE($4, block_order),\n is_active = COALESCE($5, is_active),\n updated_at = NOW()\n WHERE id = $6\n RETURNING id, page_id, block_order, title, data_source, data_config, is_active, \n created_at as \"created_at: DateTime<Utc>\", \n updated_at as \"updated_at: DateTime<Utc>\"\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "page_id",
"type_info": "Uuid"
},
{
"ordinal": 2,
"name": "block_order",
"type_info": "Int4"
},
{
"ordinal": 3,
"name": "title",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "data_source",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "data_config",
"type_info": "Jsonb"
},
{
"ordinal": 6,
"name": "is_active",
"type_info": "Bool"
},
{
"ordinal": 7,
"name": "created_at: DateTime<Utc>",
"type_info": "Timestamptz"
},
{
"ordinal": 8,
"name": "updated_at: DateTime<Utc>",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Jsonb",
"Int4",
"Bool",
"Uuid"
]
},
"nullable": [
false,
false,
false,
true,
false,
true,
false,
false,
false
]
},
"hash": "79c26fa0a70d890f43063e151653b6727018788d7cc361ee64a0ce9c7f6f8786"
}

View File

@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM chart_blocks WHERE id = $1 RETURNING id",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false
]
},
"hash": "7ee383b9cd74d3a94c543878b8933b77684f4482f82fbcb81fa1d98b2968de03"
}

View File

@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) as count FROM chart_blocks WHERE page_id = $1 AND is_active = true",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
null
]
},
"hash": "7f6785c40d5a768b4b67685d6f90d31a7995035ff242c95b251174d56a8d0dea"
}

View File

@ -51,12 +51,12 @@
{
"ordinal": 9,
"name": "created_at: DateTime<Utc>",
"type_info": "Timestamp"
"type_info": "Timestamptz"
},
{
"ordinal": 10,
"name": "updated_at: DateTime<Utc>",
"type_info": "Timestamp"
"type_info": "Timestamptz"
},
{
"ordinal": 11,

View File

@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) FROM blogs WHERE status = 'published' AND is_active = true",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": []
},
"nullable": [
null
]
},
"hash": "8a3cb562905ba7ebac3000a29f9d718b1059e04b1020ca158abd7f075552d1e2"
}

View File

@ -0,0 +1,75 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE pages \n SET title = COALESCE($1, title),\n slug = COALESCE($2, slug),\n description = COALESCE($3, description),\n is_active = COALESCE($4, is_active),\n updated_at = NOW(),\n updated_by = $5\n WHERE id = $6\n RETURNING id, title, slug, description, is_active, \n created_at as \"created_at: DateTime<Utc>\", \n updated_at as \"updated_at: DateTime<Utc>\", \n created_by, updated_by\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "title",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "slug",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "description",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "is_active",
"type_info": "Bool"
},
{
"ordinal": 5,
"name": "created_at: DateTime<Utc>",
"type_info": "Timestamptz"
},
{
"ordinal": 6,
"name": "updated_at: DateTime<Utc>",
"type_info": "Timestamptz"
},
{
"ordinal": 7,
"name": "created_by",
"type_info": "Uuid"
},
{
"ordinal": 8,
"name": "updated_by",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Text",
"Bool",
"Uuid",
"Uuid"
]
},
"nullable": [
false,
false,
false,
true,
false,
false,
false,
true,
true
]
},
"hash": "8ad20bc169ba756b347954c4c3849a2ac1ecbd40f8695e3c9021a4f84eaf54ad"
}

View File

@ -0,0 +1,74 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO pages (title, slug, description, is_active, created_by)\n VALUES ($1, $2, $3, $4, $5)\n RETURNING id, title, slug, description, is_active, \n created_at as \"created_at: DateTime<Utc>\", \n updated_at as \"updated_at: DateTime<Utc>\", \n created_by, updated_by\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "title",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "slug",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "description",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "is_active",
"type_info": "Bool"
},
{
"ordinal": 5,
"name": "created_at: DateTime<Utc>",
"type_info": "Timestamptz"
},
{
"ordinal": 6,
"name": "updated_at: DateTime<Utc>",
"type_info": "Timestamptz"
},
{
"ordinal": 7,
"name": "created_by",
"type_info": "Uuid"
},
{
"ordinal": 8,
"name": "updated_by",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Text",
"Bool",
"Uuid"
]
},
"nullable": [
false,
false,
false,
true,
false,
false,
false,
true,
true
]
},
"hash": "8dbecde6ae9d3e259a0239a2848211367950021aa3d2a1a26539b1809521418f"
}

View File

@ -0,0 +1,71 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id, title, slug, description, is_active, \n created_at as \"created_at: DateTime<Utc>\", \n updated_at as \"updated_at: DateTime<Utc>\", \n created_by, updated_by\n FROM pages \n WHERE is_active = true \n AND (title ILIKE $1 OR description ILIKE $1 OR slug ILIKE $1)\n ORDER BY \n CASE \n WHEN title ILIKE $1 THEN 1\n WHEN slug ILIKE $1 THEN 2\n ELSE 3\n END,\n updated_at DESC\n LIMIT $2\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "title",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "slug",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "description",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "is_active",
"type_info": "Bool"
},
{
"ordinal": 5,
"name": "created_at: DateTime<Utc>",
"type_info": "Timestamptz"
},
{
"ordinal": 6,
"name": "updated_at: DateTime<Utc>",
"type_info": "Timestamptz"
},
{
"ordinal": 7,
"name": "created_by",
"type_info": "Uuid"
},
{
"ordinal": 8,
"name": "updated_by",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Text",
"Int8"
]
},
"nullable": [
false,
false,
false,
true,
false,
false,
false,
true,
true
]
},
"hash": "8fcc976536dcff8c9120ef11a020430d5fcc1a733a2de10453a38ae98741b89e"
}

View File

@ -51,12 +51,12 @@
{
"ordinal": 9,
"name": "created_at: DateTime<Utc>",
"type_info": "Timestamp"
"type_info": "Timestamptz"
},
{
"ordinal": 10,
"name": "updated_at: DateTime<Utc>",
"type_info": "Timestamp"
"type_info": "Timestamptz"
},
{
"ordinal": 11,

View File

@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM data_points WHERE chart_block_id IN (SELECT id FROM chart_blocks WHERE page_id = $1)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": []
},
"hash": "911ba2c819ab358e455aa87627fafd11601b9d8266970565430627fc27e0bdf7"
}

View File

@ -51,12 +51,12 @@
{
"ordinal": 9,
"name": "created_at: DateTime<Utc>",
"type_info": "Timestamp"
"type_info": "Timestamptz"
},
{
"ordinal": 10,
"name": "updated_at: DateTime<Utc>",
"type_info": "Timestamp"
"type_info": "Timestamptz"
},
{
"ordinal": 11,

View File

@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) as count FROM table_blocks WHERE page_id = $1 AND is_active = true",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
null
]
},
"hash": "94a29b4c81cbad35681b776c22fff58ee972f132e8aeb66abec252338050d3fb"
}

View File

@ -0,0 +1,24 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE table_blocks SET block_order = $1, updated_at = NOW() WHERE id = $2 AND page_id = $3 RETURNING id",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Int4",
"Uuid",
"Uuid"
]
},
"nullable": [
false
]
},
"hash": "94d1a5e4289defa7afd41e6640be8f3bf2cf50184733423c9754e8ba221285da"
}

View File

@ -0,0 +1,18 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO data_points (chart_block_id, x, y, label, color)\n VALUES ($1, $2, $3, $4, $5)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Float8",
"Float8",
"Varchar",
"Varchar"
]
},
"nullable": []
},
"hash": "97a1320fd98948fcba950bb04d7a9f37e72130b57cca23c8d371acc6be8721fa"
}

View File

@ -0,0 +1,52 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id, chart_block_id, x, y, label, color\n FROM data_points WHERE chart_block_id = $1 ORDER BY x\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "chart_block_id",
"type_info": "Uuid"
},
{
"ordinal": 2,
"name": "x",
"type_info": "Float8"
},
{
"ordinal": 3,
"name": "y",
"type_info": "Float8"
},
{
"ordinal": 4,
"name": "label",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "color",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
false,
false,
false,
true,
true
]
},
"hash": "a8005db05313b629d7e3ed5d98a02cb5eed62bc5fddf852fab1e70b76614491d"
}

View File

@ -0,0 +1,24 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE settings_blocks SET block_order = $1, updated_at = NOW() WHERE id = $2 AND page_id = $3 RETURNING id",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Int4",
"Uuid",
"Uuid"
]
},
"nullable": [
false
]
},
"hash": "a91efb27039b215feafafe0ea35aeebf6ab8952b093780cf0d3fde20651481bf"
}

View File

@ -0,0 +1,70 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id, page_id, block_order, title, chart_type, config, is_active, created_at as \"created_at: DateTime<Utc>\", updated_at as \"updated_at: DateTime<Utc>\"\n FROM chart_blocks WHERE page_id = $1 AND is_active = true ORDER BY block_order\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "page_id",
"type_info": "Uuid"
},
{
"ordinal": 2,
"name": "block_order",
"type_info": "Int4"
},
{
"ordinal": 3,
"name": "title",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "chart_type",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "config",
"type_info": "Jsonb"
},
{
"ordinal": 6,
"name": "is_active",
"type_info": "Bool"
},
{
"ordinal": 7,
"name": "created_at: DateTime<Utc>",
"type_info": "Timestamptz"
},
{
"ordinal": 8,
"name": "updated_at: DateTime<Utc>",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
false,
false,
false,
false,
true,
false,
false,
false
]
},
"hash": "ab667fa7f6f6a8bef7837b7c0ac6d446d172f8ec457cbedbcf90cd4a0be7b41f"
}

View File

@ -0,0 +1,70 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id, page_id, block_order, title, data_source, data_config, is_active, created_at as \"created_at: DateTime<Utc>\", updated_at as \"updated_at: DateTime<Utc>\"\n FROM table_blocks WHERE page_id = $1 AND is_active = true ORDER BY block_order\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "page_id",
"type_info": "Uuid"
},
{
"ordinal": 2,
"name": "block_order",
"type_info": "Int4"
},
{
"ordinal": 3,
"name": "title",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "data_source",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "data_config",
"type_info": "Jsonb"
},
{
"ordinal": 6,
"name": "is_active",
"type_info": "Bool"
},
{
"ordinal": 7,
"name": "created_at: DateTime<Utc>",
"type_info": "Timestamptz"
},
{
"ordinal": 8,
"name": "updated_at: DateTime<Utc>",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
false,
false,
true,
false,
true,
false,
false,
false
]
},
"hash": "bf0d70ab0f89920b684f0f3e7eb26c7f14572099a2c26832a12cba3666218e28"
}

View File

@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) FROM blogs WHERE status = 'archived' AND is_active = true",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": []
},
"nullable": [
null
]
},
"hash": "c2370c206aeebe9102fb103dece903e3588535c2e17b23a280337795ec53a996"
}

View File

@ -0,0 +1,70 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id, title, slug, description, is_active, \n created_at as \"created_at: DateTime<Utc>\", \n updated_at as \"updated_at: DateTime<Utc>\", \n created_by, updated_by\n FROM pages WHERE slug = $1 AND is_active = true\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "title",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "slug",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "description",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "is_active",
"type_info": "Bool"
},
{
"ordinal": 5,
"name": "created_at: DateTime<Utc>",
"type_info": "Timestamptz"
},
{
"ordinal": 6,
"name": "updated_at: DateTime<Utc>",
"type_info": "Timestamptz"
},
{
"ordinal": 7,
"name": "created_by",
"type_info": "Uuid"
},
{
"ordinal": 8,
"name": "updated_by",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false,
true,
false,
false,
false,
true,
true
]
},
"hash": "c60bc0b511ce32dfe64b4e572a89b2a774e2510161a69ce2c0894e3968648fd7"
}

View File

@ -0,0 +1,82 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE settings_blocks \n SET title = COALESCE($1, title),\n category = COALESCE($2, category),\n editable = COALESCE($3, editable),\n display_mode = COALESCE($4, display_mode),\n block_order = COALESCE($5, block_order),\n is_active = COALESCE($6, is_active),\n updated_at = NOW()\n WHERE id = $7\n RETURNING id, page_id, block_order, title, category, editable, display_mode, is_active, \n created_at as \"created_at: DateTime<Utc>\", \n updated_at as \"updated_at: DateTime<Utc>\"\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "page_id",
"type_info": "Uuid"
},
{
"ordinal": 2,
"name": "block_order",
"type_info": "Int4"
},
{
"ordinal": 3,
"name": "title",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "category",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "editable",
"type_info": "Bool"
},
{
"ordinal": 6,
"name": "display_mode",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "is_active",
"type_info": "Bool"
},
{
"ordinal": 8,
"name": "created_at: DateTime<Utc>",
"type_info": "Timestamptz"
},
{
"ordinal": 9,
"name": "updated_at: DateTime<Utc>",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Bool",
"Varchar",
"Int4",
"Bool",
"Uuid"
]
},
"nullable": [
false,
false,
false,
true,
false,
false,
false,
false,
false,
false
]
},
"hash": "c65d91fb0d8f721d2915b399bcbfbb3b2e491c2e1d2ef29f4a27e0372e1e2640"
}

View File

@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM table_columns WHERE table_block_id IN (SELECT id FROM table_blocks WHERE page_id = $1)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": []
},
"hash": "cecf12cc6887ce7ec52c50b1b0041e76796d61e298020c0c0d333c8797ea315d"
}

View File

@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM text_blocks WHERE page_id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": []
},
"hash": "d5f900c12fb2c8def741b8cef4053d982364e5e6daa4bdc13b6db119d0f189de"
}

View File

@ -0,0 +1,76 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id, page_id, block_order, title, category, editable, display_mode, is_active, created_at as \"created_at: DateTime<Utc>\", updated_at as \"updated_at: DateTime<Utc>\"\n FROM settings_blocks WHERE page_id = $1 AND is_active = true ORDER BY block_order\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "page_id",
"type_info": "Uuid"
},
{
"ordinal": 2,
"name": "block_order",
"type_info": "Int4"
},
{
"ordinal": 3,
"name": "title",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "category",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "editable",
"type_info": "Bool"
},
{
"ordinal": 6,
"name": "display_mode",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "is_active",
"type_info": "Bool"
},
{
"ordinal": 8,
"name": "created_at: DateTime<Utc>",
"type_info": "Timestamptz"
},
{
"ordinal": 9,
"name": "updated_at: DateTime<Utc>",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
false,
false,
true,
false,
false,
false,
false,
false,
false
]
},
"hash": "ddca5b33d5e464ce263c6fc38b6e3102baa587bd1c70af11e2dd7d455ab76994"
}

View File

@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM table_columns WHERE table_block_id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": []
},
"hash": "e73a002aba1d824ed2f0ee5314cfa83cf84e83e48490cc41c3fb64e9400ce2b4"
}

View File

@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) as count FROM pages WHERE slug = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "ed0c216842515dd047b07f4cddf99588744b3f05c4762af95a22cd519b82be13"
}

View File

@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) as count FROM pages WHERE slug = $1 AND id != $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text",
"Uuid"
]
},
"nullable": [
null
]
},
"hash": "fd4b2f69a310a48efe720f4cf833eea8bd11a2efe7aa9b578297f429f27d8cad"
}

View File

@ -0,0 +1,68 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO text_blocks (page_id, block_order, title, markdown, is_active)\n VALUES ($1, $2, $3, $4, $5)\n RETURNING id, page_id, block_order, title, markdown, is_active, \n created_at as \"created_at: DateTime<Utc>\", \n updated_at as \"updated_at: DateTime<Utc>\"\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "page_id",
"type_info": "Uuid"
},
{
"ordinal": 2,
"name": "block_order",
"type_info": "Int4"
},
{
"ordinal": 3,
"name": "title",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "markdown",
"type_info": "Text"
},
{
"ordinal": 5,
"name": "is_active",
"type_info": "Bool"
},
{
"ordinal": 6,
"name": "created_at: DateTime<Utc>",
"type_info": "Timestamptz"
},
{
"ordinal": 7,
"name": "updated_at: DateTime<Utc>",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Uuid",
"Int4",
"Varchar",
"Text",
"Bool"
]
},
"nullable": [
false,
false,
false,
true,
false,
false,
false,
false
]
},
"hash": "fe4c7e3637409a4b8d7c3385ccb579beea8ade005e976a4afaa553e643aa4de1"
}

View File

@ -0,0 +1,75 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE chart_blocks \n SET title = COALESCE($1, title),\n chart_type = COALESCE($2, chart_type),\n config = COALESCE($3, config),\n block_order = COALESCE($4, block_order),\n is_active = COALESCE($5, is_active),\n updated_at = NOW()\n WHERE id = $6\n RETURNING id, page_id, block_order, title, chart_type, config, is_active, \n created_at as \"created_at: DateTime<Utc>\", \n updated_at as \"updated_at: DateTime<Utc>\"\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "page_id",
"type_info": "Uuid"
},
{
"ordinal": 2,
"name": "block_order",
"type_info": "Int4"
},
{
"ordinal": 3,
"name": "title",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "chart_type",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "config",
"type_info": "Jsonb"
},
{
"ordinal": 6,
"name": "is_active",
"type_info": "Bool"
},
{
"ordinal": 7,
"name": "created_at: DateTime<Utc>",
"type_info": "Timestamptz"
},
{
"ordinal": 8,
"name": "updated_at: DateTime<Utc>",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Jsonb",
"Int4",
"Bool",
"Uuid"
]
},
"nullable": [
false,
false,
false,
false,
false,
true,
false,
false,
false
]
},
"hash": "fe7162f56bbaec5f10c2095343abd9b2443bbee952788112d934c12361e486d5"
}

View File

@ -0,0 +1,64 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id, page_id, block_order, title, markdown, is_active, created_at as \"created_at: DateTime<Utc>\", updated_at as \"updated_at: DateTime<Utc>\"\n FROM text_blocks WHERE page_id = $1 AND is_active = true ORDER BY block_order\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "page_id",
"type_info": "Uuid"
},
{
"ordinal": 2,
"name": "block_order",
"type_info": "Int4"
},
{
"ordinal": 3,
"name": "title",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "markdown",
"type_info": "Text"
},
{
"ordinal": 5,
"name": "is_active",
"type_info": "Bool"
},
{
"ordinal": 6,
"name": "created_at: DateTime<Utc>",
"type_info": "Timestamptz"
},
{
"ordinal": 7,
"name": "updated_at: DateTime<Utc>",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
false,
false,
true,
false,
false,
false,
false
]
},
"hash": "fec82028aca77848dd54023e9a6607c3443a51a514f05ae43a4c34842b5abc7a"
}

View File

@ -0,0 +1,150 @@
-- Blog 系统数据库迁移
-- 创建博客分类表
CREATE TABLE IF NOT EXISTS blog_categories (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL UNIQUE,
slug VARCHAR(255) NOT NULL UNIQUE,
description TEXT,
color VARCHAR(7), -- 十六进制颜色代码,如 #FF5733
icon VARCHAR(100), -- 图标名称或路径
is_active BOOLEAN DEFAULT true,
sort_order INTEGER DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
created_by UUID REFERENCES users(id),
updated_by UUID REFERENCES users(id)
);
-- 创建博客标签表
CREATE TABLE IF NOT EXISTS blog_tags (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL UNIQUE,
slug VARCHAR(255) NOT NULL UNIQUE,
description TEXT,
color VARCHAR(7), -- 十六进制颜色代码
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
created_by UUID REFERENCES users(id),
updated_by UUID REFERENCES users(id)
);
-- 创建博客文章表
CREATE TABLE IF NOT EXISTS blogs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title VARCHAR(500) NOT NULL,
slug VARCHAR(500) NOT NULL UNIQUE,
excerpt TEXT, -- 摘要
content JSONB NOT NULL, -- JSON格式的博客内容
category_id UUID REFERENCES blog_categories(id),
status VARCHAR(20) DEFAULT 'draft', -- draft, published, archived
featured_image VARCHAR(500), -- 特色图片URL
meta_title VARCHAR(255), -- SEO标题
meta_description TEXT, -- SEO描述
published_at TIMESTAMPTZ, -- 发布时间
view_count INTEGER DEFAULT 0, -- 浏览次数
is_featured BOOLEAN DEFAULT false, -- 是否推荐
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
created_by UUID REFERENCES users(id),
updated_by UUID REFERENCES users(id)
);
-- 创建博客标签关联表(多对多关系)
CREATE TABLE IF NOT EXISTS blog_tag_relations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
blog_id UUID NOT NULL REFERENCES blogs(id) ON DELETE CASCADE,
tag_id UUID NOT NULL REFERENCES blog_tags(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(blog_id, tag_id)
);
-- 创建索引以提高查询性能
CREATE INDEX IF NOT EXISTS idx_blog_categories_slug ON blog_categories(slug);
CREATE INDEX IF NOT EXISTS idx_blog_categories_active ON blog_categories(is_active);
CREATE INDEX IF NOT EXISTS idx_blog_categories_sort ON blog_categories(sort_order);
CREATE INDEX IF NOT EXISTS idx_blog_tags_slug ON blog_tags(slug);
CREATE INDEX IF NOT EXISTS idx_blog_tags_active ON blog_tags(is_active);
CREATE INDEX IF NOT EXISTS idx_blogs_slug ON blogs(slug);
CREATE INDEX IF NOT EXISTS idx_blogs_status ON blogs(status);
CREATE INDEX IF NOT EXISTS idx_blogs_category ON blogs(category_id);
CREATE INDEX IF NOT EXISTS idx_blogs_published_at ON blogs(published_at);
CREATE INDEX IF NOT EXISTS idx_blogs_featured ON blogs(is_featured);
CREATE INDEX IF NOT EXISTS idx_blogs_active ON blogs(is_active);
CREATE INDEX IF NOT EXISTS idx_blogs_created_at ON blogs(created_at);
CREATE INDEX IF NOT EXISTS idx_blogs_view_count ON blogs(view_count);
CREATE INDEX IF NOT EXISTS idx_blog_tag_relations_blog ON blog_tag_relations(blog_id);
CREATE INDEX IF NOT EXISTS idx_blog_tag_relations_tag ON blog_tag_relations(tag_id);
-- 创建全文搜索索引
CREATE INDEX IF NOT EXISTS idx_blogs_content_gin ON blogs USING GIN (content);
-- CREATE INDEX IF NOT EXISTS idx_blogs_title_gin ON blogs USING GIN (to_tsvector('chinese', title));
-- CREATE INDEX IF NOT EXISTS idx_blogs_excerpt_gin ON blogs USING GIN (to_tsvector('chinese', excerpt));
-- 创建触发器更新 updated_at
CREATE TRIGGER update_blog_categories_updated_at BEFORE UPDATE
ON blog_categories FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_blog_tags_updated_at BEFORE UPDATE
ON blog_tags FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_blogs_updated_at BEFORE UPDATE
ON blogs FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- 插入默认分类
INSERT INTO blog_categories (name, slug, description, color, icon, sort_order, created_at, updated_at) VALUES
('Announcement', 'announcement', 'Announcement', '#3B82F6', 'code', 1, NOW(), NOW()),
('Product Update', 'product-update', 'Product Update', '#10B981', 'bell', 2, NOW(), NOW()),
('Usage Guide', 'usage-guide', 'Usage Guide', '#F59E0B', 'book-open', 3, NOW(), NOW()),
('Industry Trend', 'industry-trend', 'Industry Trend', '#8B5CF6', 'trending-up', 4, NOW(), NOW()),
('Team Story', 'team-story', 'Team Story', '#EF4444', 'users', 5, NOW(), NOW());
-- 插入默认标签
INSERT INTO blog_tags (name, slug, description, color, created_at, updated_at) VALUES
('Rust', 'rust', 'Rust编程语言相关', '#CE422B', NOW(), NOW());
-- 为Casbin添加Blog相关权限
INSERT INTO casbin_rule (ptype, v0, v1, v2, v3, v4, v5) VALUES
-- 管理员Blog权限
('p', 'admin', 'blogs', 'read', '', '', ''),
('p', 'admin', 'blogs', 'write', '', '', ''),
('p', 'admin', 'blogs', 'delete', '', '', ''),
('p', 'admin', 'blog_categories', 'read', '', '', ''),
('p', 'admin', 'blog_categories', 'write', '', '', ''),
('p', 'admin', 'blog_categories', 'delete', '', '', ''),
('p', 'admin', 'blog_tags', 'read', '', '', ''),
('p', 'admin', 'blog_tags', 'write', '', '', ''),
('p', 'admin', 'blog_tags', 'delete', '', '', ''),
-- 用户Blog权限只读
('p', 'user', 'blogs', 'read', '', '', ''),
('p', 'user', 'blog_categories', 'read', '', '', ''),
('p', 'user', 'blog_tags', 'read', '', '', '');
-- 添加Blog相关权限到permissions表
INSERT INTO permissions (name, description, resource, action, is_active, created_at, updated_at) VALUES
-- Blog管理权限
('blogs_read', '读取博客文章', 'blogs', 'read', true, NOW(), NOW()),
('blogs_write', '创建/修改博客文章', 'blogs', 'write', true, NOW(), NOW()),
('blogs_delete', '删除博客文章', 'blogs', 'delete', true, NOW(), NOW()),
-- 博客分类管理权限
('blog_categories_read', '读取博客分类', 'blog_categories', 'read', true, NOW(), NOW()),
('blog_categories_write', '创建/修改博客分类', 'blog_categories', 'write', true, NOW(), NOW()),
('blog_categories_delete', '删除博客分类', 'blog_categories', 'delete', true, NOW(), NOW()),
-- 博客标签管理权限
('blog_tags_read', '读取博客标签', 'blog_tags', 'read', true, NOW(), NOW()),
('blog_tags_write', '创建/修改博客标签', 'blog_tags', 'write', true, NOW(), NOW()),
('blog_tags_delete', '删除博客标签', 'blog_tags', 'delete', true, NOW(), NOW());
-- 为角色分配Blog权限
INSERT INTO role_permissions (role_name, permission_id, granted_by, granted_at)
SELECT 'admin', id, NULL, NOW() FROM permissions WHERE resource IN ('blogs', 'blog_categories', 'blog_tags');
INSERT INTO role_permissions (role_name, permission_id, granted_by, granted_at)
SELECT 'user', id, NULL, NOW() FROM permissions WHERE resource IN ('blogs', 'blog_categories', 'blog_tags') AND action = 'read';

View File

@ -24,10 +24,10 @@ use crate::{
config::Config,
graphql::{subscription::StatusUpdate, MutationRoot, QueryRoot, SubscriptionRoot},
services::{
casbin_service::CasbinService, invite_code_service::InviteCodeService,
mosaic_service::MosaicService, page_block_service::PageBlockService,
settings_service::SettingsService, system_config_service::SystemConfigService,
user_service::UserService,
blog_service::BlogService, casbin_service::CasbinService,
invite_code_service::InviteCodeService, mosaic_service::MosaicService,
page_block_service::PageBlockService, settings_service::SettingsService,
system_config_service::SystemConfigService, user_service::UserService,
},
};
@ -54,6 +54,7 @@ pub async fn create_router(
let mosaic_service = MosaicService::new(pool.clone());
let settings_service = SettingsService::new(pool.clone());
let page_block_service = PageBlockService::new(pool.clone());
let blog_service = BlogService::new(pool.clone());
let casbin_service = CasbinService::new(config.database_url.clone())
.await
@ -67,6 +68,7 @@ pub async fn create_router(
.data(mosaic_service)
.data(settings_service)
.data(page_block_service)
.data(blog_service)
.data(casbin_service)
.data(config.clone())
.data(status_sender.clone())

View File

@ -18,6 +18,8 @@ pub enum Commands {
Migrate(MigrateArgs),
/// 权限管理
Permissions(PermissionsArgs),
/// 博客管理
Blog(BlogArgs),
/// 显示版本信息
Version,
/// 显示配置信息
@ -180,6 +182,223 @@ pub struct CheckPermissionArgs {
pub action: String,
}
/// 博客管理相关命令
#[derive(Subcommand)]
pub enum BlogCommands {
/// 创建博客文章
Create(CreateBlogArgs),
/// 列出博客文章
List(ListBlogArgs),
/// 查看博客文章详情
Show(ShowBlogArgs),
/// 更新博客文章
Update(UpdateBlogArgs),
/// 删除博客文章
Delete(DeleteBlogArgs),
/// 创建博客分类
CreateCategory(CreateCategoryArgs),
/// 列出博客分类
ListCategories,
/// 创建博客标签
CreateTag(CreateTagArgs),
/// 列出博客标签
ListTags,
/// 博客统计信息
Stats,
}
/// 博客管理参数
#[derive(Args)]
pub struct BlogArgs {
#[command(subcommand)]
pub command: BlogCommands,
}
/// 创建博客文章参数
#[derive(Args)]
pub struct CreateBlogArgs {
/// 文章标题
#[arg(short, long)]
pub title: String,
/// 文章slugURL友好标识符
#[arg(short, long)]
pub slug: String,
/// 文章摘要
#[arg(short = 'e', long)]
pub excerpt: Option<String>,
/// 文章内容JSON格式
#[arg(short, long)]
pub content: String,
/// 分类ID
#[arg(short = 'C', long)]
pub category_id: Option<String>,
/// 文章状态draft, published, archived
#[arg(short = 'S', long, default_value = "draft")]
pub status: String,
/// 是否为推荐文章
#[arg(short, long)]
pub featured: bool,
/// 标签ID列表逗号分隔
#[arg(short = 'T', long)]
pub tags: Option<String>,
/// 作者用户ID
#[arg(short = 'u', long)]
pub user_id: String,
}
/// 列出博客文章参数
#[derive(Args)]
pub struct ListBlogArgs {
/// 页码
#[arg(short, long, default_value = "1")]
pub page: i32,
/// 每页数量
#[arg(short = 'l', long, default_value = "10")]
pub limit: i32,
/// 按状态过滤
#[arg(short, long)]
pub status: Option<String>,
/// 按分类过滤
#[arg(short, long)]
pub category: Option<String>,
/// 搜索关键词
#[arg(short = 'q', long)]
pub search: Option<String>,
/// 只显示推荐文章
#[arg(short, long)]
pub featured: bool,
}
/// 查看博客文章参数
#[derive(Args)]
pub struct ShowBlogArgs {
/// 博客文章ID或slug
pub id_or_slug: String,
/// 显示详细信息(包括分类和标签)
#[arg(short, long)]
pub detail: bool,
}
/// 更新博客文章参数
#[derive(Args)]
pub struct UpdateBlogArgs {
/// 博客文章ID
pub id: String,
/// 文章标题
#[arg(short, long)]
pub title: Option<String>,
/// 文章slug
#[arg(short, long)]
pub slug: Option<String>,
/// 文章摘要
#[arg(short = 'e', long)]
pub excerpt: Option<String>,
/// 文章内容JSON格式
#[arg(short, long)]
pub content: Option<String>,
/// 分类ID
#[arg(short = 'C', long)]
pub category_id: Option<String>,
/// 文章状态
#[arg(short = 'S', long)]
pub status: Option<String>,
/// 是否为推荐文章
#[arg(short, long)]
pub featured: Option<bool>,
/// 标签ID列表逗号分隔
#[arg(short = 'T', long)]
pub tags: Option<String>,
/// 更新者用户ID
#[arg(short = 'u', long)]
pub user_id: String,
}
/// 删除博客文章参数
#[derive(Args)]
pub struct DeleteBlogArgs {
/// 博客文章ID
pub id: String,
}
/// 创建博客分类参数
#[derive(Args)]
pub struct CreateCategoryArgs {
/// 分类名称
#[arg(short, long)]
pub name: String,
/// 分类slug
#[arg(short, long)]
pub slug: String,
/// 分类描述
#[arg(short, long)]
pub description: Option<String>,
/// 分类颜色
#[arg(short, long)]
pub color: Option<String>,
/// 分类图标
#[arg(short, long)]
pub icon: Option<String>,
/// 排序顺序
#[arg(short = 'o', long, default_value = "0")]
pub sort_order: i32,
/// 创建者用户ID
#[arg(short = 'u', long)]
pub user_id: String,
}
/// 创建博客标签参数
#[derive(Args)]
pub struct CreateTagArgs {
/// 标签名称
#[arg(short, long)]
pub name: String,
/// 标签slug
#[arg(short, long)]
pub slug: String,
/// 标签描述
#[arg(short, long)]
pub description: Option<String>,
/// 标签颜色
#[arg(short, long)]
pub color: Option<String>,
/// 创建者用户ID
#[arg(short = 'u', long)]
pub user_id: String,
}
impl Default for Commands {
fn default() -> Self {
Commands::Serve(ServeArgs {

View File

@ -1,6 +1,5 @@
use crate::auth::get_auth_user;
use crate::graphql::guards::RequireRole;
use crate::graphql::guards::RequireWritePermission;
use crate::graphql::guards::{RequireReadPermission, RequireRole, RequireWritePermission};
use crate::graphql::types::ConfigUpdateResultType;
use crate::graphql::types::UpdateDocsSupportInput;
use crate::graphql::types::UpdateModalAnnouncementInput;
@ -8,15 +7,24 @@ use crate::graphql::types::UpdateNoticeConfigInput;
use crate::graphql::types::UpdateOpsConfigInput;
use crate::graphql::types::UpdateSiteConfigInput;
use crate::graphql::types::*;
use crate::models::blog::{
CreateBlogCategoryInput, CreateBlogInput, CreateBlogTagInput, UpdateBlogCategoryInput,
UpdateBlogInput, UpdateBlogTagInput,
};
use crate::models::page_block::*;
use crate::models::settings::{CreateSetting, UpdateSetting};
use crate::models::user::Role;
use crate::models::user::User;
use crate::models::Blog;
use crate::models::BlogCategory;
use crate::models::BlogTag;
use crate::services::blog_service::BlogService;
use crate::services::casbin_service::CasbinService;
use crate::services::invite_code_service::InviteCodeService;
use crate::services::page_block_service::PageBlockService;
use crate::services::settings_service::SettingsService;
use crate::services::user_service::UserService;
use async_graphql::Error as GraphQLError;
use async_graphql::{Context, Object, Result};
use uuid::Uuid;
@ -1170,4 +1178,136 @@ impl MutationRoot {
.collect(),
})
}
// ==================== Blog 相关变更 ====================
/// 创建博客文章
#[graphql(guard = "RequireWritePermission::new(\"blogs\")")]
async fn create_blog(&self, ctx: &Context<'_>, input: CreateBlogInput) -> Result<Blog> {
let auth_user = get_auth_user(ctx).await?;
let blog_service = ctx.data::<BlogService>()?;
blog_service
.create_blog(input, auth_user.id)
.await
.map_err(|e| GraphQLError::new(e.to_string()))
}
/// 更新博客文章
#[graphql(guard = "RequireWritePermission::new(\"blogs\")")]
async fn update_blog(
&self,
ctx: &Context<'_>,
id: Uuid,
input: UpdateBlogInput,
) -> Result<Blog> {
let auth_user = get_auth_user(ctx).await?;
let blog_service = ctx.data::<BlogService>()?;
blog_service
.update_blog(id, input, auth_user.id)
.await
.map_err(|e| GraphQLError::new(e.to_string()))
}
/// 删除博客文章
#[graphql(guard = "RequireWritePermission::new(\"blogs\")")]
async fn delete_blog(&self, ctx: &Context<'_>, id: Uuid) -> Result<bool> {
let blog_service = ctx.data::<BlogService>()?;
blog_service
.delete_blog(id)
.await
.map_err(|e| GraphQLError::new(e.to_string()))
}
/// 增加博客浏览次数
#[graphql(guard = "RequireReadPermission::new(\"blogs\")")]
async fn increment_blog_view_count(&self, ctx: &Context<'_>, id: Uuid) -> Result<bool> {
let blog_service = ctx.data::<BlogService>()?;
blog_service
.increment_blog_view_count(id)
.await
.map_err(|e| GraphQLError::new(e.to_string()))?;
Ok(true)
}
/// 创建博客分类
#[graphql(guard = "RequireWritePermission::new(\"blog_categories\")")]
async fn create_blog_category(
&self,
ctx: &Context<'_>,
input: CreateBlogCategoryInput,
) -> Result<BlogCategory> {
let auth_user = get_auth_user(ctx).await?;
let blog_service = ctx.data::<BlogService>()?;
blog_service
.create_category(input, auth_user.id)
.await
.map_err(|e| GraphQLError::new(e.to_string()))
}
/// 更新博客分类
#[graphql(guard = "RequireWritePermission::new(\"blog_categories\")")]
async fn update_blog_category(
&self,
ctx: &Context<'_>,
id: Uuid,
input: UpdateBlogCategoryInput,
) -> Result<BlogCategory> {
let auth_user = get_auth_user(ctx).await?;
let blog_service = ctx.data::<BlogService>()?;
blog_service
.update_category(id, input, auth_user.id)
.await
.map_err(|e| GraphQLError::new(e.to_string()))
}
/// 删除博客分类
#[graphql(guard = "RequireWritePermission::new(\"blog_categories\")")]
async fn delete_blog_category(&self, ctx: &Context<'_>, id: Uuid) -> Result<bool> {
let blog_service = ctx.data::<BlogService>()?;
blog_service
.delete_category(id)
.await
.map_err(|e| GraphQLError::new(e.to_string()))
}
/// 创建博客标签
#[graphql(guard = "RequireWritePermission::new(\"blog_tags\")")]
async fn create_blog_tag(
&self,
ctx: &Context<'_>,
input: CreateBlogTagInput,
) -> Result<BlogTag> {
let auth_user = get_auth_user(ctx).await?;
let blog_service = ctx.data::<BlogService>()?;
blog_service
.create_tag(input, auth_user.id)
.await
.map_err(|e| GraphQLError::new(e.to_string()))
}
/// 更新博客标签
#[graphql(guard = "RequireWritePermission::new(\"blog_tags\")")]
async fn update_blog_tag(
&self,
ctx: &Context<'_>,
id: Uuid,
input: UpdateBlogTagInput,
) -> Result<BlogTag> {
let auth_user = get_auth_user(ctx).await?;
let blog_service = ctx.data::<BlogService>()?;
blog_service
.update_tag(id, input, auth_user.id)
.await
.map_err(|e| GraphQLError::new(e.to_string()))
}
/// 删除博客标签
#[graphql(guard = "RequireWritePermission::new(\"blog_tags\")")]
async fn delete_blog_tag(&self, ctx: &Context<'_>, id: Uuid) -> Result<bool> {
let blog_service = ctx.data::<BlogService>()?;
blog_service
.delete_tag(id)
.await
.map_err(|e| GraphQLError::new(e.to_string()))
}
}

View File

@ -1,18 +1,23 @@
use crate::auth::get_auth_user;
use crate::graphql::guards::{
RequireDeletePermission, RequireMultiplePermissions, RequirePermission, RequireReadPermission,
RequireRole, RequireWritePermission,
RequireMultiplePermissions, RequirePermission, RequireReadPermission,
};
use crate::graphql::types::*;
use crate::graphql::types::*;
use crate::models::blog::{
Blog, BlogCategory, BlogCategoryFilterInput, BlogDetail, BlogFilterInput, BlogSortInput,
BlogStats, BlogTag, BlogTagFilterInput,
};
use crate::models::invite_code::InviteCode;
use crate::models::settings::{SettingFilter, SettingHistory};
use crate::models::user::{Role, User, UserInfoRow};
use crate::models::page_block::{PaginatedResult, PaginationInput};
use crate::models::settings::SettingFilter;
use crate::models::user::{User, UserInfoRow};
use crate::services::blog_service::BlogService;
use crate::services::casbin_service::CasbinService;
use crate::services::invite_code_service::InviteCodeService;
use crate::services::page_block_service::PageBlockService;
use crate::services::settings_service::SettingsService;
use crate::services::user_service::UserService;
use async_graphql::Error as GraphQLError;
use async_graphql::{Context, Object, Result};
use tracing::info;
use uuid::Uuid;
@ -1238,4 +1243,110 @@ impl QueryRoot {
warnings,
})
}
// ==================== Blog 相关查询 ====================
/// 获取博客文章列表
#[graphql(guard = "RequireReadPermission::new(\"blogs\")")]
async fn blogs(
&self,
ctx: &Context<'_>,
filter: Option<BlogFilterInput>,
sort: Option<BlogSortInput>,
pagination: Option<PaginationInput>,
) -> Result<PaginatedResult<Blog>> {
let blog_service = ctx.data::<BlogService>()?;
blog_service
.get_blogs(filter, sort, pagination)
.await
.map_err(|e| GraphQLError::new(e.to_string()))
}
/// 根据ID获取博客文章
#[graphql(guard = "RequireReadPermission::new(\"blogs\")")]
async fn blog(&self, ctx: &Context<'_>, id: Uuid) -> Result<Blog> {
let blog_service = ctx.data::<BlogService>()?;
blog_service
.get_blog_by_id(id)
.await
.map_err(|e| GraphQLError::new(e.to_string()))
}
/// 根据slug获取博客文章
#[graphql(guard = "RequireReadPermission::new(\"blogs\")")]
async fn blog_by_slug(&self, ctx: &Context<'_>, slug: String) -> Result<Blog> {
let blog_service = ctx.data::<BlogService>()?;
blog_service
.get_blog_by_slug(&slug)
.await
.map_err(|e| GraphQLError::new(e.to_string()))
}
/// 获取博客文章详情(包含分类和标签)
#[graphql(guard = "RequireReadPermission::new(\"blogs\")")]
async fn blog_detail(&self, ctx: &Context<'_>, id: Uuid) -> Result<BlogDetail> {
let blog_service = ctx.data::<BlogService>()?;
blog_service
.get_blog_detail(id)
.await
.map_err(|e| GraphQLError::new(e.to_string()))
}
/// 获取博客统计信息
#[graphql(guard = "RequireReadPermission::new(\"blogs\")")]
async fn blog_stats(&self, ctx: &Context<'_>) -> Result<BlogStats> {
let blog_service = ctx.data::<BlogService>()?;
blog_service
.get_blog_stats()
.await
.map_err(|e| GraphQLError::new(e.to_string()))
}
/// 获取博客分类列表
#[graphql(guard = "RequireReadPermission::new(\"blog_categories\")")]
async fn blog_categories(
&self,
ctx: &Context<'_>,
filter: Option<BlogCategoryFilterInput>,
) -> Result<Vec<BlogCategory>> {
let blog_service = ctx.data::<BlogService>()?;
blog_service
.get_categories(filter)
.await
.map_err(|e| GraphQLError::new(e.to_string()))
}
/// 根据ID获取博客分类
#[graphql(guard = "RequireReadPermission::new(\"blog_categories\")")]
async fn blog_category(&self, ctx: &Context<'_>, id: Uuid) -> Result<BlogCategory> {
let blog_service = ctx.data::<BlogService>()?;
blog_service
.get_category_by_id(id)
.await
.map_err(|e| GraphQLError::new(e.to_string()))
}
/// 获取博客标签列表
#[graphql(guard = "RequireReadPermission::new(\"blog_tags\")")]
async fn blog_tags(
&self,
ctx: &Context<'_>,
filter: Option<BlogTagFilterInput>,
) -> Result<Vec<BlogTag>> {
let blog_service = ctx.data::<BlogService>()?;
blog_service
.get_tags(filter)
.await
.map_err(|e| GraphQLError::new(e.to_string()))
}
/// 根据ID获取博客标签
#[graphql(guard = "RequireReadPermission::new(\"blog_tags\")")]
async fn blog_tag(&self, ctx: &Context<'_>, id: Uuid) -> Result<BlogTag> {
let blog_service = ctx.data::<BlogService>()?;
blog_service
.get_tag_by_id(id)
.await
.map_err(|e| GraphQLError::new(e.to_string()))
}
}

View File

@ -11,9 +11,10 @@ mod services;
use app::create_router;
use clap::Parser;
use cli::{
AddPolicyArgs, AssignRoleArgs, CheckPermissionArgs, Cli, Commands, ListRolePermissionsArgs,
ListUserRolesArgs, MigrateArgs, PermissionsArgs, PermissionsCommands, RemovePolicyArgs,
RemoveRoleArgs, ServeArgs,
AddPolicyArgs, AssignRoleArgs, BlogArgs, BlogCommands, CheckPermissionArgs, Cli, Commands,
CreateBlogArgs, CreateCategoryArgs, CreateTagArgs, DeleteBlogArgs, ListBlogArgs,
ListRolePermissionsArgs, ListUserRolesArgs, MigrateArgs, PermissionsArgs, PermissionsCommands,
RemovePolicyArgs, RemoveRoleArgs, ServeArgs, ShowBlogArgs, UpdateBlogArgs,
};
use config::Config;
use db::{create_pool, run_migrations};
@ -30,6 +31,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
Commands::Serve(args) => serve_command(args).await,
Commands::Migrate(args) => migrate_command(args).await,
Commands::Permissions(args) => permissions_command(args).await,
Commands::Blog(args) => blog_command(args).await,
Commands::Version => version_command(),
Commands::Config => config_command(),
}
@ -508,3 +510,419 @@ fn print_config_info(config: &Config, args: &ServeArgs) {
);
println!(" 🗺️ 瓦片服务: {}", config.tile_server_url);
}
async fn blog_command(args: BlogArgs) -> Result<(), Box<dyn std::error::Error>> {
use models::blog::*;
use models::page_block::PaginationInput;
use serde_json;
use services::blog_service::BlogService;
use uuid::Uuid;
// 初始化配置和数据库连接
let config = Config::from_env()?;
let pool = create_pool(&config.database_url).await?;
let blog_service = BlogService::new(pool);
match args.command {
BlogCommands::Create(create_args) => {
println!("📝 创建博客文章...");
// 解析用户ID
let user_id = Uuid::parse_str(&create_args.user_id).map_err(|_| "无效的用户ID格式")?;
// 解析分类ID
let category_id = if let Some(category_str) = create_args.category_id {
Some(Uuid::parse_str(&category_str).map_err(|_| "无效的分类ID格式")?)
} else {
None
};
// 解析标签ID列表
let tag_ids = if let Some(tags_str) = create_args.tags {
let tags: Result<Vec<Uuid>, _> = tags_str
.split(',')
.map(|s| Uuid::parse_str(s.trim()))
.collect();
Some(tags.map_err(|_| "无效的标签ID格式")?)
} else {
None
};
// 解析内容JSON
let content: serde_json::Value =
serde_json::from_str(&create_args.content).map_err(|_| "无效的JSON内容格式")?;
let input = CreateBlogInput {
title: create_args.title.clone(),
slug: create_args.slug.clone(),
excerpt: create_args.excerpt.clone(),
content,
category_id,
status: Some(create_args.status.clone()),
featured_image: None,
meta_title: None,
meta_description: None,
is_featured: Some(create_args.featured),
is_active: Some(true),
tag_ids,
};
match blog_service.create_blog(input, user_id).await {
Ok(blog) => {
println!("✅ 博客文章创建成功!");
println!(" 📄 ID: {}", blog.id);
println!(" 📝 标题: {}", blog.title);
println!(" 🔗 Slug: {}", blog.slug);
println!(" 📊 状态: {}", blog.status);
if create_args.featured {
println!(" ⭐ 推荐文章");
}
}
Err(e) => {
eprintln!("❌ 创建失败: {}", e);
process::exit(1);
}
}
}
BlogCommands::List(list_args) => {
println!("📋 博客文章列表");
// 构建过滤器
let filter =
if list_args.status.is_some() || list_args.search.is_some() || list_args.featured {
Some(BlogFilterInput {
title: None,
slug: None,
category_id: None,
status: list_args.status,
is_featured: if list_args.featured { Some(true) } else { None },
is_active: Some(true),
tag_ids: None,
search: list_args.search,
date_from: None,
date_to: None,
})
} else {
None
};
// 分页参数
let pagination = Some(PaginationInput {
page: Some(list_args.page),
per_page: Some(list_args.limit),
});
match blog_service.get_blogs(filter, None, pagination).await {
Ok(result) => {
println!(
" 📊 总计: {} 篇文章 (第 {}/{} 页)",
result.total, result.page, result.total_pages
);
println!();
for blog in result.items {
println!("📄 {} | {}", blog.title, blog.id);
println!(" 🔗 Slug: {}", blog.slug);
println!(" 📊 状态: {}", blog.status);
if blog.is_featured {
println!(" ⭐ 推荐");
}
if let Some(excerpt) = &blog.excerpt {
let short_excerpt = if excerpt.len() > 100 {
format!("{}...", &excerpt[..100])
} else {
excerpt.clone()
};
println!(" 📝 摘要: {}", short_excerpt);
}
println!(" 📅 创建: {}", blog.created_at.format("%Y-%m-%d %H:%M"));
println!();
}
}
Err(e) => {
eprintln!("❌ 获取列表失败: {}", e);
process::exit(1);
}
}
}
BlogCommands::Show(show_args) => {
println!("📖 博客文章详情");
// 尝试解析为UUID如果失败则当作slug处理
let blog = if let Ok(id) = Uuid::parse_str(&show_args.id_or_slug) {
blog_service.get_blog_by_id(id).await
} else {
blog_service.get_blog_by_slug(&show_args.id_or_slug).await
};
match blog {
Ok(blog) => {
println!(" 📄 ID: {}", blog.id);
println!(" 📝 标题: {}", blog.title);
println!(" 🔗 Slug: {}", blog.slug);
println!(" 📊 状态: {}", blog.status);
if let Some(excerpt) = &blog.excerpt {
println!(" 📝 摘要: {}", excerpt);
}
if blog.is_featured {
println!(" ⭐ 推荐文章");
}
println!(" 👀 浏览次数: {}", blog.view_count);
println!(
" 📅 创建时间: {}",
blog.created_at.format("%Y-%m-%d %H:%M:%S")
);
println!(
" 📅 更新时间: {}",
blog.updated_at.format("%Y-%m-%d %H:%M:%S")
);
if show_args.detail {
// 显示详细信息,包括分类和标签
match blog_service.get_blog_detail(blog.id).await {
Ok(detail) => {
if let Some(category) = detail.category {
println!(" 📁 分类: {} ({})", category.name, category.slug);
}
if !detail.tags.is_empty() {
let tag_names: Vec<String> =
detail.tags.iter().map(|tag| tag.name.clone()).collect();
println!(" 🏷️ 标签: {}", tag_names.join(", "));
}
}
Err(e) => {
eprintln!("⚠️ 获取详细信息失败: {}", e);
}
}
}
println!();
println!("📄 内容:");
println!("{}", serde_json::to_string_pretty(&blog.content)?);
}
Err(e) => {
eprintln!("❌ 未找到博客文章: {}", e);
process::exit(1);
}
}
}
BlogCommands::Update(update_args) => {
println!("✏️ 更新博客文章...");
let blog_id = Uuid::parse_str(&update_args.id).map_err(|_| "无效的博客ID格式")?;
let user_id = Uuid::parse_str(&update_args.user_id).map_err(|_| "无效的用户ID格式")?;
// 解析分类ID
let category_id = if let Some(category_str) = update_args.category_id {
Some(Uuid::parse_str(&category_str).map_err(|_| "无效的分类ID格式")?)
} else {
None
};
// 解析标签ID列表
let tag_ids = if let Some(tags_str) = update_args.tags {
let tags: Result<Vec<Uuid>, _> = tags_str
.split(',')
.map(|s| Uuid::parse_str(s.trim()))
.collect();
Some(tags.map_err(|_| "无效的标签ID格式")?)
} else {
None
};
// 解析内容JSON
let content = if let Some(content_str) = update_args.content {
Some(serde_json::from_str(&content_str).map_err(|_| "无效的JSON内容格式")?)
} else {
None
};
let input = UpdateBlogInput {
title: update_args.title,
slug: update_args.slug,
excerpt: update_args.excerpt,
content,
category_id,
status: update_args.status,
featured_image: None,
meta_title: None,
meta_description: None,
is_featured: update_args.featured,
is_active: None,
tag_ids,
};
match blog_service.update_blog(blog_id, input, user_id).await {
Ok(blog) => {
println!("✅ 博客文章更新成功!");
println!(" 📄 ID: {}", blog.id);
println!(" 📝 标题: {}", blog.title);
println!(
" 📅 更新时间: {}",
blog.updated_at.format("%Y-%m-%d %H:%M:%S")
);
}
Err(e) => {
eprintln!("❌ 更新失败: {}", e);
process::exit(1);
}
}
}
BlogCommands::Delete(delete_args) => {
println!("🗑️ 删除博客文章...");
let blog_id = Uuid::parse_str(&delete_args.id).map_err(|_| "无效的博客ID格式")?;
match blog_service.delete_blog(blog_id).await {
Ok(true) => {
println!("✅ 博客文章删除成功!");
}
Ok(false) => {
eprintln!("❌ 博客文章不存在");
process::exit(1);
}
Err(e) => {
eprintln!("❌ 删除失败: {}", e);
process::exit(1);
}
}
}
BlogCommands::CreateCategory(create_args) => {
println!("📁 创建博客分类...");
let user_id = Uuid::parse_str(&create_args.user_id).map_err(|_| "无效的用户ID格式")?;
let input = CreateBlogCategoryInput {
name: create_args.name.clone(),
slug: create_args.slug.clone(),
description: create_args.description.clone(),
color: create_args.color.clone(),
icon: create_args.icon.clone(),
is_active: Some(true),
sort_order: Some(create_args.sort_order),
};
match blog_service.create_category(input, user_id).await {
Ok(category) => {
println!("✅ 博客分类创建成功!");
println!(" 📁 ID: {}", category.id);
println!(" 📝 名称: {}", category.name);
println!(" 🔗 Slug: {}", category.slug);
if let Some(desc) = &category.description {
println!(" 📄 描述: {}", desc);
}
}
Err(e) => {
eprintln!("❌ 创建分类失败: {}", e);
process::exit(1);
}
}
}
BlogCommands::ListCategories => {
println!("📁 博客分类列表");
match blog_service.get_categories(None).await {
Ok(categories) => {
for category in categories {
println!("📁 {} | {}", category.name, category.id);
println!(" 🔗 Slug: {}", category.slug);
if let Some(desc) = &category.description {
println!(" 📄 描述: {}", desc);
}
if let Some(color) = &category.color {
println!(" 🎨 颜色: {}", color);
}
println!(" 📊 排序: {}", category.sort_order);
println!();
}
}
Err(e) => {
eprintln!("❌ 获取分类列表失败: {}", e);
process::exit(1);
}
}
}
BlogCommands::CreateTag(create_args) => {
println!("🏷️ 创建博客标签...");
let user_id = Uuid::parse_str(&create_args.user_id).map_err(|_| "无效的用户ID格式")?;
let input = CreateBlogTagInput {
name: create_args.name.clone(),
slug: create_args.slug.clone(),
description: create_args.description.clone(),
color: create_args.color.clone(),
is_active: Some(true),
};
match blog_service.create_tag(input, user_id).await {
Ok(tag) => {
println!("✅ 博客标签创建成功!");
println!(" 🏷️ ID: {}", tag.id);
println!(" 📝 名称: {}", tag.name);
println!(" 🔗 Slug: {}", tag.slug);
if let Some(desc) = &tag.description {
println!(" 📄 描述: {}", desc);
}
}
Err(e) => {
eprintln!("❌ 创建标签失败: {}", e);
process::exit(1);
}
}
}
BlogCommands::ListTags => {
println!("🏷️ 博客标签列表");
match blog_service.get_tags(None).await {
Ok(tags) => {
for tag in tags {
println!("🏷️ {} | {}", tag.name, tag.id);
println!(" 🔗 Slug: {}", tag.slug);
if let Some(desc) = &tag.description {
println!(" 📄 描述: {}", desc);
}
if let Some(color) = &tag.color {
println!(" 🎨 颜色: {}", color);
}
println!();
}
}
Err(e) => {
eprintln!("❌ 获取标签列表失败: {}", e);
process::exit(1);
}
}
}
BlogCommands::Stats => {
println!("📊 博客统计信息");
match blog_service.get_blog_stats().await {
Ok(stats) => {
println!(" 📄 总文章数: {}", stats.total_blogs);
println!(" ✅ 已发布: {}", stats.published_blogs);
println!(" 📝 草稿: {}", stats.draft_blogs);
println!(" 📦 归档: {}", stats.archived_blogs);
println!(" 📁 分类数: {}", stats.total_categories);
println!(" 🏷️ 标签数: {}", stats.total_tags);
println!(" 👀 总浏览量: {}", stats.total_views);
}
Err(e) => {
eprintln!("❌ 获取统计信息失败: {}", e);
process::exit(1);
}
}
}
}
Ok(())
}

387
src/models/blog.rs Normal file
View File

@ -0,0 +1,387 @@
use async_graphql::{InputObject, SimpleObject};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use uuid::Uuid;
/// 博客分类模型
#[derive(Debug, Clone, Serialize, Deserialize, FromRow, SimpleObject)]
pub struct BlogCategory {
pub id: Uuid,
pub name: String,
pub slug: String,
pub description: Option<String>,
pub color: Option<String>,
pub icon: Option<String>,
pub is_active: bool,
pub sort_order: i32,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub created_by: Option<Uuid>,
pub updated_by: Option<Uuid>,
}
/// 博客标签模型
#[derive(Debug, Clone, Serialize, Deserialize, FromRow, SimpleObject)]
pub struct BlogTag {
pub id: Uuid,
pub name: String,
pub slug: String,
pub description: Option<String>,
pub color: Option<String>,
pub is_active: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub created_by: Option<Uuid>,
pub updated_by: Option<Uuid>,
}
/// 博客文章模型
#[derive(Debug, Clone, Serialize, Deserialize, FromRow, SimpleObject)]
pub struct Blog {
pub id: Uuid,
pub title: String,
pub slug: String,
pub excerpt: Option<String>,
pub content: serde_json::Value,
pub category_id: Option<Uuid>,
pub status: String,
pub featured_image: Option<String>,
pub meta_title: Option<String>,
pub meta_description: Option<String>,
pub published_at: Option<DateTime<Utc>>,
pub view_count: i32,
pub is_featured: bool,
pub is_active: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub created_by: Option<Uuid>,
pub updated_by: Option<Uuid>,
}
/// 博客文章详情(包含分类和标签信息)
#[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)]
pub struct BlogDetail {
#[graphql(flatten)]
pub blog: Blog,
pub category: Option<BlogCategory>,
pub tags: Vec<BlogTag>,
}
/// 博客标签关联模型
#[derive(Debug, Clone, Serialize, Deserialize, FromRow, SimpleObject)]
pub struct BlogTagRelation {
pub id: Uuid,
pub blog_id: Uuid,
pub tag_id: Uuid,
pub created_at: DateTime<Utc>,
}
// 创建博客分类输入
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
pub struct CreateBlogCategoryInput {
pub name: String,
pub slug: String,
pub description: Option<String>,
pub color: Option<String>,
pub icon: Option<String>,
pub is_active: Option<bool>,
pub sort_order: Option<i32>,
}
// 更新博客分类输入
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
pub struct UpdateBlogCategoryInput {
pub name: Option<String>,
pub slug: Option<String>,
pub description: Option<String>,
pub color: Option<String>,
pub icon: Option<String>,
pub is_active: Option<bool>,
pub sort_order: Option<i32>,
}
// 创建博客标签输入
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
pub struct CreateBlogTagInput {
pub name: String,
pub slug: String,
pub description: Option<String>,
pub color: Option<String>,
pub is_active: Option<bool>,
}
// 更新博客标签输入
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
pub struct UpdateBlogTagInput {
pub name: Option<String>,
pub slug: Option<String>,
pub description: Option<String>,
pub color: Option<String>,
pub is_active: Option<bool>,
}
// 创建博客文章输入
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
pub struct CreateBlogInput {
pub title: String,
pub slug: String,
pub excerpt: Option<String>,
pub content: serde_json::Value,
pub category_id: Option<Uuid>,
pub status: Option<String>,
pub featured_image: Option<String>,
pub meta_title: Option<String>,
pub meta_description: Option<String>,
pub is_featured: Option<bool>,
pub is_active: Option<bool>,
pub tag_ids: Option<Vec<Uuid>>,
}
// 更新博客文章输入
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
pub struct UpdateBlogInput {
pub title: Option<String>,
pub slug: Option<String>,
pub excerpt: Option<String>,
pub content: Option<serde_json::Value>,
pub category_id: Option<Uuid>,
pub status: Option<String>,
pub featured_image: Option<String>,
pub meta_title: Option<String>,
pub meta_description: Option<String>,
pub is_featured: Option<bool>,
pub is_active: Option<bool>,
pub tag_ids: Option<Vec<Uuid>>,
}
// 博客过滤器
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
pub struct BlogFilterInput {
pub title: Option<String>,
pub slug: Option<String>,
pub category_id: Option<Uuid>,
pub status: Option<String>,
pub is_featured: Option<bool>,
pub is_active: Option<bool>,
pub tag_ids: Option<Vec<Uuid>>,
pub search: Option<String>,
pub date_from: Option<DateTime<Utc>>,
pub date_to: Option<DateTime<Utc>>,
}
// 博客分类过滤器
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
pub struct BlogCategoryFilterInput {
pub name: Option<String>,
pub slug: Option<String>,
pub is_active: Option<bool>,
pub search: Option<String>,
}
// 博客标签过滤器
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
pub struct BlogTagFilterInput {
pub name: Option<String>,
pub slug: Option<String>,
pub is_active: Option<bool>,
pub search: Option<String>,
}
// 博客排序
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
pub struct BlogSortInput {
pub field: String,
pub direction: String, // "asc" or "desc"
}
// 博客统计信息
#[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)]
pub struct BlogStats {
pub total_blogs: i64,
pub published_blogs: i64,
pub draft_blogs: i64,
pub archived_blogs: i64,
pub total_categories: i64,
pub total_tags: i64,
pub total_views: i64,
}
impl BlogCategory {
/// 创建新博客分类
pub fn new(input: CreateBlogCategoryInput, user_id: Uuid) -> Self {
Self {
id: Uuid::new_v4(),
name: input.name,
slug: input.slug,
description: input.description,
color: input.color,
icon: input.icon,
is_active: input.is_active.unwrap_or(true),
sort_order: input.sort_order.unwrap_or(0),
created_at: Utc::now(),
updated_at: Utc::now(),
created_by: Some(user_id),
updated_by: Some(user_id),
}
}
/// 更新博客分类
pub fn update(&mut self, input: UpdateBlogCategoryInput, user_id: Uuid) {
if let Some(name) = input.name {
self.name = name;
}
if let Some(slug) = input.slug {
self.slug = slug;
}
if let Some(description) = input.description {
self.description = Some(description);
}
if let Some(color) = input.color {
self.color = Some(color);
}
if let Some(icon) = input.icon {
self.icon = Some(icon);
}
if let Some(is_active) = input.is_active {
self.is_active = is_active;
}
if let Some(sort_order) = input.sort_order {
self.sort_order = sort_order;
}
self.updated_at = Utc::now();
self.updated_by = Some(user_id);
}
}
impl BlogTag {
/// 创建新博客标签
pub fn new(input: CreateBlogTagInput, user_id: Uuid) -> Self {
Self {
id: Uuid::new_v4(),
name: input.name,
slug: input.slug,
description: input.description,
color: input.color,
is_active: input.is_active.unwrap_or(true),
created_at: Utc::now(),
updated_at: Utc::now(),
created_by: Some(user_id),
updated_by: Some(user_id),
}
}
/// 更新博客标签
pub fn update(&mut self, input: UpdateBlogTagInput, user_id: Uuid) {
if let Some(name) = input.name {
self.name = name;
}
if let Some(slug) = input.slug {
self.slug = slug;
}
if let Some(description) = input.description {
self.description = Some(description);
}
if let Some(color) = input.color {
self.color = Some(color);
}
if let Some(is_active) = input.is_active {
self.is_active = is_active;
}
self.updated_at = Utc::now();
self.updated_by = Some(user_id);
}
}
impl Blog {
/// 创建新博客文章
pub fn new(input: CreateBlogInput, user_id: Uuid) -> Self {
let now = Utc::now();
let published_at = if input.status.as_deref() == Some("published") {
Some(now)
} else {
None
};
Self {
id: Uuid::new_v4(),
title: input.title,
slug: input.slug,
excerpt: input.excerpt,
content: input.content,
category_id: input.category_id,
status: input.status.unwrap_or_else(|| "draft".to_string()),
featured_image: input.featured_image,
meta_title: input.meta_title,
meta_description: input.meta_description,
published_at,
view_count: 0,
is_featured: input.is_featured.unwrap_or(false),
is_active: input.is_active.unwrap_or(true),
created_at: now,
updated_at: now,
created_by: Some(user_id),
updated_by: Some(user_id),
}
}
/// 更新博客文章
pub fn update(&mut self, input: UpdateBlogInput, user_id: Uuid) {
if let Some(title) = input.title {
self.title = title;
}
if let Some(slug) = input.slug {
self.slug = slug;
}
if let Some(excerpt) = input.excerpt {
self.excerpt = Some(excerpt);
}
if let Some(content) = input.content {
self.content = content;
}
if let Some(category_id) = input.category_id {
self.category_id = Some(category_id);
}
if let Some(status) = input.status {
self.status = status.clone();
// 如果状态变为published且之前未发布设置发布时间
if status == "published" && self.published_at.is_none() {
self.published_at = Some(Utc::now());
}
}
if let Some(featured_image) = input.featured_image {
self.featured_image = Some(featured_image);
}
if let Some(meta_title) = input.meta_title {
self.meta_title = Some(meta_title);
}
if let Some(meta_description) = input.meta_description {
self.meta_description = Some(meta_description);
}
if let Some(is_featured) = input.is_featured {
self.is_featured = is_featured;
}
if let Some(is_active) = input.is_active {
self.is_active = is_active;
}
self.updated_at = Utc::now();
self.updated_by = Some(user_id);
}
/// 增加浏览次数
pub fn increment_view_count(&mut self) {
self.view_count += 1;
self.updated_at = Utc::now();
}
}
impl BlogDetail {
/// 从博客文章创建详情对象
pub fn new(blog: Blog, category: Option<BlogCategory>, tags: Vec<BlogTag>) -> Self {
Self {
blog,
category,
tags,
}
}
}

View File

@ -1,9 +1,11 @@
pub mod blog;
pub mod invite_code;
pub mod kafka_message;
pub mod page_block;
pub mod settings;
pub mod user;
pub use blog::*;
pub use invite_code::*;
pub use kafka_message::*;
pub use page_block::*;

1140
src/services/blog_service.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,4 @@
pub mod blog_service;
pub mod casbin_service;
pub mod invite_code_service;
pub mod mosaic_service;

477
test_blog Normal file
View File

@ -0,0 +1,477 @@
{
"type": "doc",
"content": [
{
"type": "heading",
"attrs": {
"textAlign": null,
"level": 1
},
"content": [
{
"type": "text",
"text": "Getting started"
}
]
},
{
"type": "paragraph",
"attrs": {
"textAlign": null
},
"content": [
{
"type": "text",
"text": "Welcome to the "
},
{
"type": "text",
"marks": [
{
"type": "italic"
},
{
"type": "highlight",
"attrs": {
"color": "var(--tt-color-highlight-yellow)"
}
}
],
"text": "Simple Editor"
},
{
"type": "text",
"text": " template! This template integrates "
},
{
"type": "text",
"marks": [
{
"type": "bold"
}
],
"text": "open source"
},
{
"type": "text",
"text": " UI components and Tiptap extensions licensed under "
},
{
"type": "text",
"marks": [
{
"type": "bold"
}
],
"text": "MIT"
},
{
"type": "text",
"text": "."
}
]
},
{
"type": "paragraph",
"attrs": {
"textAlign": null
},
"content": [
{
"type": "text",
"text": "Integrate it by following the "
},
{
"type": "text",
"marks": [
{
"type": "link",
"attrs": {
"href": "https://tiptap.dev/docs/ui-components/templates/simple-editor",
"target": "_blank",
"rel": "noopener noreferrer nofollow",
"class": null
}
}
],
"text": "Tiptap UI Components docs"
},
{
"type": "text",
"text": " or using our CLI tool."
}
]
},
{
"type": "codeBlock",
"attrs": {
"language": null
},
"content": [
{
"type": "text",
"text": "npx @tiptap/cli init"
}
]
},
{
"type": "heading",
"attrs": {
"textAlign": null,
"level": 2
},
"content": [
{
"type": "text",
"text": "Features"
}
]
},
{
"type": "blockquote",
"content": [
{
"type": "paragraph",
"attrs": {
"textAlign": null
},
"content": [
{
"type": "text",
"marks": [
{
"type": "italic"
}
],
"text": "A fully responsive rich text editor with built-in support for common formatting and layout tools. Type markdown "
},
{
"type": "text",
"marks": [
{
"type": "code"
}
],
"text": "**"
},
{
"type": "text",
"marks": [
{
"type": "italic"
}
],
"text": " or use keyboard shortcuts "
},
{
"type": "text",
"marks": [
{
"type": "code"
}
],
"text": "⌘+B"
},
{
"type": "text",
"text": " for "
},
{
"type": "text",
"marks": [
{
"type": "strike"
}
],
"text": "most"
},
{
"type": "text",
"text": " all common markdown marks. 🪄"
}
]
}
]
},
{
"type": "paragraph",
"attrs": {
"textAlign": "left"
},
"content": [
{
"type": "text",
"text": "Add images, customize alignment, and apply "
},
{
"type": "text",
"marks": [
{
"type": "highlight",
"attrs": {
"color": "var(--tt-color-highlight-blue)"
}
}
],
"text": "advanced formatting"
},
{
"type": "text",
"text": " to make your writing more engaging and professional."
}
]
},
{
"type": "image",
"attrs": {
"src": "/images/tiptap-ui-placeholder-image.jpg",
"alt": "placeholder-image",
"title": "placeholder-image"
}
},
{
"type": "bulletList",
"content": [
{
"type": "listItem",
"content": [
{
"type": "paragraph",
"attrs": {
"textAlign": "left"
},
"content": [
{
"type": "text",
"marks": [
{
"type": "bold"
}
],
"text": "Superscript"
},
{
"type": "text",
"text": " (x"
},
{
"type": "text",
"marks": [
{
"type": "superscript"
}
],
"text": "2"
},
{
"type": "text",
"text": ") and "
},
{
"type": "text",
"marks": [
{
"type": "bold"
}
],
"text": "Subscript"
},
{
"type": "text",
"text": " (H"
},
{
"type": "text",
"marks": [
{
"type": "subscript"
}
],
"text": "2"
},
{
"type": "text",
"text": "O) for precision."
}
]
}
]
},
{
"type": "listItem",
"content": [
{
"type": "paragraph",
"attrs": {
"textAlign": "left"
},
"content": [
{
"type": "text",
"marks": [
{
"type": "bold"
}
],
"text": "Typographic conversion"
},
{
"type": "text",
"text": ": automatically convert to "
},
{
"type": "text",
"marks": [
{
"type": "code"
}
],
"text": "->"
},
{
"type": "text",
"text": " an arrow "
},
{
"type": "text",
"marks": [
{
"type": "bold"
}
],
"text": "→"
},
{
"type": "text",
"text": "."
}
]
}
]
}
]
},
{
"type": "paragraph",
"attrs": {
"textAlign": "left"
},
"content": [
{
"type": "text",
"marks": [
{
"type": "italic"
}
],
"text": "→ "
},
{
"type": "text",
"marks": [
{
"type": "link",
"attrs": {
"href": "https://tiptap.dev/docs/ui-components/templates/simple-editor#features",
"target": "_blank",
"rel": "noopener noreferrer nofollow",
"class": null
}
}
],
"text": "Learn more"
}
]
},
{
"type": "horizontalRule"
},
{
"type": "heading",
"attrs": {
"textAlign": "left",
"level": 2
},
"content": [
{
"type": "text",
"text": "Make it your own"
}
]
},
{
"type": "paragraph",
"attrs": {
"textAlign": "left"
},
"content": [
{
"type": "text",
"text": "Switch between light and dark modes, and tailor the editor's appearance with customizable CSS to match your style."
}
]
},
{
"type": "taskList",
"content": [
{
"type": "taskItem",
"attrs": {
"checked": true
},
"content": [
{
"type": "paragraph",
"attrs": {
"textAlign": "left"
},
"content": [
{
"type": "text",
"text": "Test template"
}
]
}
]
},
{
"type": "taskItem",
"attrs": {
"checked": false
},
"content": [
{
"type": "paragraph",
"attrs": {
"textAlign": "left"
},
"content": [
{
"type": "text",
"marks": [
{
"type": "link",
"attrs": {
"href": "https://tiptap.dev/docs/ui-components/templates/simple-editor",
"target": "_blank",
"rel": "noopener noreferrer nofollow",
"class": null
}
}
],
"text": "Integrate the free template"
}
]
}
]
}
]
},
{
"type": "paragraph",
"attrs": {
"textAlign": "left"
}
}
]
}