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

This commit is contained in:
tsuki 2025-08-14 21:34:27 +08:00
parent fb3706ff38
commit 6a3ce7e9d3
90 changed files with 2290 additions and 8036 deletions

View File

@ -1,22 +0,0 @@
{
"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

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

View File

@ -1,21 +0,0 @@
{
"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

@ -1,52 +0,0 @@
{
"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

@ -1,68 +0,0 @@
{
"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

@ -1,94 +0,0 @@
{
"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

@ -1,22 +0,0 @@
{
"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

@ -1,22 +0,0 @@
{
"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

@ -1,68 +0,0 @@
{
"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

@ -1,24 +0,0 @@
{
"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

@ -1,70 +0,0 @@
{
"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

@ -1,34 +0,0 @@
{
"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

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

View File

@ -1,22 +0,0 @@
{
"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

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

View File

@ -1,70 +0,0 @@
{
"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

@ -1,24 +0,0 @@
{
"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

@ -1,22 +0,0 @@
{
"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

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

View File

@ -1,82 +0,0 @@
{
"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

@ -1,24 +0,0 @@
{
"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

@ -1,75 +0,0 @@
{
"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

@ -1,22 +0,0 @@
{
"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

@ -1,18 +0,0 @@
{
"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

@ -1,70 +0,0 @@
{
"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

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

View File

@ -1,22 +0,0 @@
{
"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

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

View File

@ -1,103 +0,0 @@
{
"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

@ -1,75 +0,0 @@
{
"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

@ -1,22 +0,0 @@
{
"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

@ -1,22 +0,0 @@
{
"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

@ -1,75 +0,0 @@
{
"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

@ -1,74 +0,0 @@
{
"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

@ -1,71 +0,0 @@
{
"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

@ -1,14 +0,0 @@
{
"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

@ -1,22 +0,0 @@
{
"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

@ -1,24 +0,0 @@
{
"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

@ -1,18 +0,0 @@
{
"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

@ -1,52 +0,0 @@
{
"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

@ -1,24 +0,0 @@
{
"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

@ -1,70 +0,0 @@
{
"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

@ -1,70 +0,0 @@
{
"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

@ -1,70 +0,0 @@
{
"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

@ -1,82 +0,0 @@
{
"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

@ -1,14 +0,0 @@
{
"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

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

View File

@ -1,76 +0,0 @@
{
"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

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

View File

@ -1,22 +0,0 @@
{
"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

@ -1,23 +0,0 @@
{
"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

@ -1,68 +0,0 @@
{
"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

@ -1,75 +0,0 @@
{
"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

@ -1,64 +0,0 @@
{
"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,132 @@
-- Rollback: Delete default configuration items
-- Delete all inserted configuration items
DELETE FROM settings WHERE key IN (
-- Site configuration
'site.name',
'site.description',
'site.keywords',
'site.url',
'site.logo',
'site.copyright',
'site.icp',
'site.icp_url',
'site.color_style',
-- User configuration
'user.default_avatar',
'user.default_role',
'user.register_invite_code',
'user.register_email_verification',
'user.open_login',
'user.open_reset_password',
-- Email configuration
'email.smtp_host',
'email.smtp_port',
'email.smtp_user',
'email.smtp_password',
'email.smtp_from',
'email.smtp_from_name',
'email.smtp_from_email',
'email.system_template',
-- Blog configuration
'blog.default_author',
'blog.default_category',
'blog.default_tag',
'blog.open_comment',
-- Logging configuration
'logging.level',
'logging.max_files',
'logging.max_file_size',
-- Cache configuration
'cache.ttl',
'cache.max_size',
-- Feature switches configuration
'switch.open_register',
'switch.open_login',
'switch.open_reset_password',
'switch.open_comment',
'switch.open_like',
'switch.open_share',
'switch.open_view'
);
-- Drop unique index (if exists)
DROP INDEX IF EXISTS idx_settings_key_unique;
-- Insert default configuration items
-- Site configuration
INSERT INTO settings (key, value, value_type, description, category, is_system, is_editable) VALUES
('site.name', 'Mapp', 'string', 'Site name', 'site', true, true),
('site.description', 'A modern application platform', 'string', 'Site description', 'site', true, true),
('site.keywords', 'mapp,application,platform', 'string', 'Site keywords', 'site', true, true),
('site.url', 'http://localhost:3000', 'string', 'Site URL', 'site', true, true),
('site.logo', '/static/logo.png', 'string', 'Site logo path', 'site', true, true),
('site.copyright', '© 2024 Mapp. All rights reserved.', 'string', 'Copyright information', 'site', true, true),
('site.icp', '', 'string', 'ICP registration number', 'site', true, true),
('site.icp_url', '', 'string', 'ICP registration URL', 'site', true, true),
('site.color_style', 'default', 'string', 'Site color scheme', 'site', true, true);
-- User configuration
INSERT INTO settings (key, value, value_type, description, category, is_system, is_editable) VALUES
('user.default_avatar', '/static/default-avatar.png', 'string', 'Default user avatar', 'user', true, true),
('user.default_role', 'User', 'string', 'Default user role', 'user', true, true),
('user.register_invite_code', 'true', 'boolean', 'Require invite code for registration', 'user', true, true),
('user.register_email_verification', 'false', 'boolean', 'Require email verification for registration', 'user', true, true),
('user.open_login', 'true', 'boolean', 'Enable login', 'user', true, true),
('user.open_reset_password', 'true', 'boolean', 'Enable password reset', 'user', true, true);
-- Email configuration
INSERT INTO settings (key, value, value_type, description, category, is_system, is_editable) VALUES
('email.smtp_host', '', 'string', 'SMTP server address', 'email', true, true),
('email.smtp_port', '587', 'number', 'SMTP server port', 'email', true, true),
('email.smtp_user', '', 'string', 'SMTP username', 'email', true, true),
('email.smtp_password', '', 'string', 'SMTP password', 'email', true, true),
('email.smtp_from', '', 'string', 'Sender email', 'email', true, true),
('email.smtp_from_name', 'Mapp System', 'string', 'Sender name', 'email', true, true),
('email.smtp_from_email', '', 'string', 'Sender email address', 'email', true, true),
('email.system_template', 'default', 'string', 'System email template', 'email', true, true);
-- Blog configuration
INSERT INTO settings (key, value, value_type, description, category, is_system, is_editable) VALUES
('blog.default_author', 'System', 'string', 'Default blog author', 'blog', true, true),
('blog.default_category', 'Uncategorized', 'string', 'Default blog category', 'blog', true, true),
('blog.default_tag', 'Default', 'string', 'Default blog tag', 'blog', true, true),
('blog.open_comment', 'true', 'boolean', 'Enable comments', 'blog', true, true);
-- Logging configuration
INSERT INTO settings (key, value, value_type, description, category, is_system, is_editable) VALUES
('logging.level', 'info', 'string', 'Log level', 'logging', true, true),
('logging.max_files', '10', 'number', 'Maximum log files', 'logging', true, true),
('logging.max_file_size', '10485760', 'number', 'Maximum log file size (bytes)', 'logging', true, true);
-- Cache configuration
INSERT INTO settings (key, value, value_type, description, category, is_system, is_editable) VALUES
('cache.ttl', '3600', 'number', 'Cache time to live (seconds)', 'cache', true, true),
('cache.max_size', '1000', 'number', 'Maximum cache entries', 'cache', true, true);
-- Feature switches configuration
INSERT INTO settings (key, value, value_type, description, category, is_system, is_editable) VALUES
('switch.open_register', 'true', 'boolean', 'Enable registration', 'switch', true, true),
('switch.open_login', 'true', 'boolean', 'Enable login', 'switch', true, true),
('switch.open_reset_password', 'true', 'boolean', 'Enable password reset', 'switch', true, true),
('switch.open_comment', 'true', 'boolean', 'Enable comments', 'switch', true, true),
('switch.open_like', 'true', 'boolean', 'Enable likes', 'switch', true, true),
('switch.open_share', 'true', 'boolean', 'Enable sharing', 'switch', true, true),
('switch.open_view', 'true', 'boolean', 'Enable view statistics', 'switch', true, true);
-- 创建唯一索引约束(如果不存在)
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_indexes
WHERE indexname = 'idx_settings_key_unique'
AND tablename = 'settings'
) THEN
CREATE UNIQUE INDEX idx_settings_key_unique ON settings(key);
END IF;
END $$;

View File

@ -10,7 +10,6 @@ use async_graphql::{
use async_graphql_axum::{GraphQLRequest, GraphQLResponse, GraphQLSubscription};
use axum::{
extract::{FromRef, State},
http::Method,
response::{Html, IntoResponse},
routing::get,
Router,
@ -24,10 +23,10 @@ use crate::{
config::Config,
graphql::{subscription::StatusUpdate, MutationRoot, QueryRoot, SubscriptionRoot},
services::{
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,
blog_service::BlogService, casbin_service::CasbinService, config_manager::ConfigsManager,
config_service::ConfigsService, invite_code_service::InviteCodeService,
mosaic_service::MosaicService, system_config_service::SystemConfigService,
user_service::UserService,
},
};
@ -48,31 +47,36 @@ pub async fn create_router(
config: Config,
status_sender: Option<broadcast::Sender<StatusUpdate>>,
) -> Router {
let user_service = UserService::new(pool.clone(), config.jwt_secret.clone());
let invite_code_service = InviteCodeService::new(pool.clone());
let system_config_service = SystemConfigService::new(pool.clone());
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
.expect("Failed to initialize CasbinService");
let schema = Schema::build(QueryRoot, MutationRoot, SubscriptionRoot)
.data(pool)
.data(user_service)
.data(invite_code_service)
.data(system_config_service)
.data(mosaic_service)
.data(settings_service)
.data(page_block_service)
.data(blog_service)
.data(casbin_service)
.data(config.clone())
.data(status_sender.clone())
.finish();
let user_service = UserService::new(pool.clone(), config.jwt_secret.clone())
.with_casbin(casbin_service.clone());
let invite_code_service = InviteCodeService::new(pool.clone());
let system_config_service = SystemConfigService::new(pool.clone());
let mosaic_service = MosaicService::new(pool.clone());
let configs_service = ConfigsService::new(pool.clone());
let config_manager = ConfigsManager::new(configs_service).await;
let blog_service = BlogService::new(pool.clone());
let schema = Schema::build(
QueryRoot::default(),
MutationRoot::default(),
SubscriptionRoot,
)
.data(pool)
.data(user_service)
.data(invite_code_service)
.data(system_config_service)
.data(mosaic_service)
.data(config_manager)
.data(blog_service)
.data(casbin_service)
.data(config.clone())
.data(status_sender.clone())
.finish();
let keys = vec![DecodingKey::from_secret(config.jwt_secret.as_bytes())];
let validation = Validation::default();
@ -94,19 +98,19 @@ pub async fn create_router(
Router::new()
.route("/", get(graphql_playground))
.route("/graphql", get(graphql_playground).post(graphql_handler))
.route_layer(
RateLimitLayer::<RealIp>::builder()
.with_route(
(Method::GET, "/graphql"),
Quota::new(Duration::from_millis(100), NonZero::new(10).unwrap()),
)
.with_route(
(Method::POST, "/graphql"),
Quota::new(Duration::from_millis(100), NonZero::new(10).unwrap()),
)
.with_gc_interval(1000)
.default_handle_error(),
)
// .route_layer(
// RateLimitLayer::<RealIp>::builder()
// .with_route(
// (Method::GET, "/graphql"),
// Quota::new(Duration::from_millis(100), NonZero::new(10).unwrap()),
// )
// .with_route(
// (Method::POST, "/graphql"),
// Quota::new(Duration::from_millis(100), NonZero::new(10).unwrap()),
// )
// .with_gc_interval(1000)
// .default_handle_error(),
// )
.route_service("/ws", GraphQLSubscription::new(schema))
.layer(CorsLayer::permissive())
.merge(router)

View File

@ -3,6 +3,15 @@ use crate::{auth::get_auth_user, services::casbin_service::CasbinService};
use async_graphql::{Context, Error, Guard, Result};
use tracing::warn;
pub struct RequireLogin;
impl Guard for RequireLogin {
async fn check(&self, ctx: &Context<'_>) -> Result<()> {
get_auth_user(ctx).await?;
Ok(())
}
}
pub struct RequireRole(pub Role);
impl Guard for RequireRole {

View File

@ -1,9 +1,9 @@
pub mod guards;
pub mod mutation;
pub mod query;
pub mod mutations;
pub mod queries;
pub mod subscription;
pub mod types;
pub use mutation::MutationRoot;
pub use query::QueryRoot;
pub use subscription::SubscriptionRoot;
pub use mutations::MutationRoot;
pub use queries::QueryRoot;
pub use subscription::SubscriptionRoot;

File diff suppressed because it is too large Load Diff

13
src/graphql/mutations.rs Normal file
View File

@ -0,0 +1,13 @@
mod blog;
mod config;
mod permissions;
mod users;
use async_graphql::MergedObject;
#[derive(MergedObject, Default)]
pub struct MutationRoot(
blog::BlogMutation,
config::ConfigMutation,
permissions::PermissionMutation,
users::UserMutation,
);

View File

@ -0,0 +1,148 @@
use crate::auth::get_auth_user;
use crate::graphql::guards::*;
use crate::graphql::types::*;
use crate::services::blog_service::BlogService;
use async_graphql::{Context, Error as GraphQLError, Object, Result};
use uuid::Uuid;
#[derive(Default)]
pub struct BlogMutation;
#[Object]
impl BlogMutation {
/// 创建博客文章
#[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(|blog| blog.into())
.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(|blog| blog.into())
.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(|category| category.into())
.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(|category| category.into())
.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(|tag| tag.into())
.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(|tag| tag.into())
.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

@ -0,0 +1,33 @@
use crate::graphql::guards::*;
use crate::graphql::types::*;
use crate::services::config_manager::ConfigsManager;
use async_graphql::{Context, Object, Result};
use std::collections::HashMap;
#[derive(Default)]
pub struct ConfigMutation;
#[Object]
impl ConfigMutation {
#[graphql(guard = "RequireWritePermission::new(\"config\")")]
async fn update_config_batch(
&self,
ctx: &Context<'_>,
input: Vec<UpdateConfig>,
) -> Result<String> {
let config_manager = ctx.data::<ConfigsManager>()?;
let configs = input
.into_iter()
.map(|input| {
(
input.key,
serde_json::to_value(input.value.unwrap()).unwrap(),
)
})
.collect::<HashMap<String, serde_json::Value>>();
config_manager.set_values(configs).await?;
Ok("successed".to_string())
}
}

View File

@ -0,0 +1,86 @@
use crate::graphql::guards::*;
use crate::services::casbin_service::CasbinService;
use async_graphql::{Context, Object, Result};
use uuid::Uuid;
#[derive(Default)]
pub struct PermissionMutation;
#[Object]
impl PermissionMutation {
// 权限管理相关的 Mutation
#[graphql(guard = "RequireWritePermission::new(\"permissions\")")]
async fn assign_role_to_user(
&self,
ctx: &Context<'_>,
user_id: Uuid,
role_name: String,
) -> Result<bool> {
let casbin_service = ctx.data::<CasbinService>()?;
casbin_service
.assign_role(&user_id.to_string(), &role_name)
.await?;
Ok(true)
}
#[graphql(guard = "RequireWritePermission::new(\"permissions\")")]
async fn remove_role_from_user(
&self,
ctx: &Context<'_>,
user_id: Uuid,
role_name: String,
) -> Result<bool> {
let casbin_service = ctx.data::<CasbinService>()?;
casbin_service
.remove_role(&user_id.to_string(), &role_name)
.await?;
Ok(true)
}
#[graphql(guard = "RequireWritePermission::new(\"permissions\")")]
async fn add_policy(
&self,
ctx: &Context<'_>,
role_name: String,
resource: String,
action: String,
) -> Result<bool> {
let casbin_service = ctx.data::<CasbinService>()?;
casbin_service
.add_policy(&role_name, &resource, &action)
.await?;
Ok(true)
}
#[graphql(guard = "RequireWritePermission::new(\"permissions\")")]
async fn remove_policy(
&self,
ctx: &Context<'_>,
role_name: String,
resource: String,
action: String,
) -> Result<bool> {
let casbin_service = ctx.data::<CasbinService>()?;
casbin_service
.remove_policy(&role_name, &resource, &action)
.await?;
Ok(true)
}
#[graphql(guard = "RequireWritePermission::new(\"permissions\")")]
async fn reload_policies(&self, ctx: &Context<'_>) -> Result<bool> {
let casbin_service = ctx.data::<CasbinService>()?;
casbin_service.reload_policy().await?;
Ok(true)
}
}

View File

@ -0,0 +1,56 @@
use crate::graphql::guards::*;
use crate::graphql::types::users::*;
use crate::services::user_service::UserService;
use async_graphql::{Context, Object, Result};
#[derive(Default)]
pub struct UserMutation;
#[Object]
impl UserMutation {
async fn register(&self, ctx: &Context<'_>, input: RegisterInput) -> Result<User> {
let user_service = ctx.data::<UserService>()?;
user_service.register(input).await.map(|user| user.into())
}
#[graphql(guard = "RequireWritePermission::new(\"users\")")]
async fn create_user(&self, ctx: &Context<'_>, input: CreateUserInput) -> Result<User> {
let user_service = ctx.data::<UserService>()?;
user_service
.create_user(input)
.await
.map(|user| user.into())
}
async fn login(&self, ctx: &Context<'_>, input: LoginInput) -> Result<LoginResponse> {
let user_service = ctx.data::<UserService>()?;
user_service.login(input).await.map(|user| user.into())
}
// async fn create_invite_code(
// &self,
// ctx: &Context<'_>,
// input: CreateInviteCodeInput,
// ) -> Result<InviteCodeResponse> {
// let auth_user = get_auth_user(ctx).await?;
// let invite_code_service = ctx.data::<InviteCodeService>()?;
// let code = invite_code_service
// .create_invite_code(auth_user.id, input)
// .await?;
// // Get the invite code details to return expires_at
// let invite_codes = invite_code_service
// .get_invite_codes_by_creator(auth_user.id)
// .await?;
// let invite_code = invite_codes
// .into_iter()
// .find(|ic| ic.code == code)
// .ok_or_else(|| async_graphql::Error::new("Failed to retrieve created invite code"))?;
// Ok(InviteCodeResponse {
// code,
// expires_at: invite_code.expires_at,
// })
// }
}

13
src/graphql/queries.rs Normal file
View File

@ -0,0 +1,13 @@
mod blog;
mod config;
mod permissions;
mod user;
use async_graphql::MergedObject;
#[derive(MergedObject, Default)]
pub struct QueryRoot(
blog::BlogQuery,
config::ConfigQuery,
permissions::PermissionQuery,
user::UserQuery,
);

120
src/graphql/queries/blog.rs Normal file
View File

@ -0,0 +1,120 @@
use crate::graphql::types::{blog::*, PaginatedResult, PaginationInput};
use crate::services::blog_service::BlogService;
use async_graphql::{Context, Error as GraphQLError, Object, Result};
use uuid::Uuid;
#[derive(Default)]
pub struct BlogQuery;
#[Object]
impl BlogQuery {
async fn blogs(
&self,
ctx: &Context<'_>,
filter: Option<BlogFilterInput>,
sort: Option<BlogSortInput>,
pagination: Option<PaginationInput>,
) -> Result<PaginatedResult<Blog>> {
let blog_service = ctx.data::<BlogService>()?;
let result = blog_service
.get_blogs(filter, sort, pagination)
.await
.map_err(|e| GraphQLError::new(e.to_string()))?;
// 转换 model 类型到 GraphQL 类型
Ok(PaginatedResult::new(
result.items.into_iter().map(|item| item.into()).collect(),
result.total,
result.page,
result.per_page,
))
}
/// 根据ID获取博客文章
async fn blog(&self, ctx: &Context<'_>, id: Uuid) -> Result<Blog> {
let blog_service = ctx.data::<BlogService>()?;
let blog = blog_service
.get_blog_by_id(id)
.await
.map_err(|e| GraphQLError::new(e.to_string()))?;
Ok(blog.into())
}
/// 根据slug获取博客文章
async fn blog_by_slug(&self, ctx: &Context<'_>, slug: String) -> Result<Blog> {
let blog_service = ctx.data::<BlogService>()?;
let blog = blog_service
.get_blog_by_slug(&slug)
.await
.map_err(|e| GraphQLError::new(e.to_string()))?;
Ok(blog.into())
}
async fn blog_detail(&self, ctx: &Context<'_>, id: Uuid) -> Result<BlogDetail> {
let blog_service = ctx.data::<BlogService>()?;
let detail = blog_service
.get_blog_detail(id)
.await
.map_err(|e| GraphQLError::new(e.to_string()))?;
// 手动转换 BlogDetail因为它包含嵌套结构
Ok(BlogDetail {
blog: detail.blog.into(),
category: detail.category.map(|c| c.into()),
tags: detail.tags.into_iter().map(|t| t.into()).collect(),
})
}
async fn blog_stats(&self, ctx: &Context<'_>) -> Result<BlogStats> {
let blog_service = ctx.data::<BlogService>()?;
let stats = blog_service
.get_blog_stats()
.await
.map_err(|e| GraphQLError::new(e.to_string()))?;
Ok(stats.into())
}
async fn blog_categories(
&self,
ctx: &Context<'_>,
filter: Option<BlogCategoryFilterInput>,
) -> Result<Vec<BlogCategory>> {
let blog_service = ctx.data::<BlogService>()?;
let categories = blog_service
.get_categories(filter)
.await
.map_err(|e| GraphQLError::new(e.to_string()))?;
Ok(categories.into_iter().map(|c| c.into()).collect())
}
async fn blog_category(&self, ctx: &Context<'_>, id: Uuid) -> Result<BlogCategory> {
let blog_service = ctx.data::<BlogService>()?;
let category = blog_service
.get_category_by_id(id)
.await
.map_err(|e| GraphQLError::new(e.to_string()))?;
Ok(category.into())
}
async fn blog_tags(
&self,
ctx: &Context<'_>,
filter: Option<BlogTagFilterInput>,
) -> Result<Vec<BlogTag>> {
let blog_service = ctx.data::<BlogService>()?;
let tags = blog_service
.get_tags(filter)
.await
.map_err(|e| GraphQLError::new(e.to_string()))?;
Ok(tags.into_iter().map(|t| t.into()).collect())
}
async fn blog_tag(&self, ctx: &Context<'_>, id: Uuid) -> Result<BlogTag> {
let blog_service = ctx.data::<BlogService>()?;
let tag = blog_service
.get_tag_by_id(id)
.await
.map_err(|e| GraphQLError::new(e.to_string()))?;
Ok(tag.into())
}
}

View File

@ -0,0 +1,23 @@
use crate::graphql::guards::*;
use crate::graphql::types::{config::*, permission::*};
use crate::services::config_manager::ConfigsManager;
use async_graphql::{Context, Object, Result};
#[derive(Default)]
pub struct ConfigQuery;
#[Object]
impl ConfigQuery {
#[graphql(guard = "RequireWritePermission::new(\"config\")")]
async fn configs(&self, ctx: &Context<'_>) -> Result<Vec<Config>> {
let configs_service = ctx.data::<ConfigsManager>()?;
let configs = configs_service.get_all_settings().await?;
Ok(configs)
}
async fn site_configs(&self, ctx: &Context<'_>) -> Result<Vec<Config>> {
let configs_service = ctx.data::<ConfigsManager>()?;
let configs = configs_service.get_settings_by_category("site").await?;
Ok(configs)
}
}

View File

@ -0,0 +1,132 @@
use crate::auth::get_auth_user;
use crate::graphql::guards::*;
use crate::graphql::types::*;
use crate::services::casbin_service::CasbinService;
use async_graphql::{Context, Object, Result};
#[derive(Default)]
pub struct PermissionQuery;
#[Object]
impl PermissionQuery {
// 权限管理查询
#[graphql(guard = "RequireReadPermission::new(\"permissions\")")]
async fn check_permission(
&self,
ctx: &Context<'_>,
resource: String,
action: String,
) -> Result<PermissionCheckResult> {
let user = get_auth_user(ctx).await?;
let casbin_service = ctx.data::<CasbinService>()?;
let has_permission = casbin_service
.check_permission(&user.id.to_string(), &resource, &action)
.await?;
let roles = casbin_service.get_user_roles(&user.id.to_string()).await?;
Ok(PermissionCheckResult {
user_id: user.id.to_string(),
resource,
action,
has_permission,
roles,
})
}
#[graphql(guard = "RequireReadPermission::new(\"permissions\")")]
async fn get_user_roles(&self, ctx: &Context<'_>) -> Result<Vec<String>> {
let user = get_auth_user(ctx).await?;
let casbin_service = ctx.data::<CasbinService>()?;
let roles = casbin_service.get_user_roles(&user.id.to_string()).await?;
Ok(roles)
}
#[graphql(guard = "RequireLogin")]
async fn get_user_permissions(&self, ctx: &Context<'_>) -> Result<Vec<PermissionPair>> {
let user = get_auth_user(ctx).await?;
let casbin_service = ctx.data::<CasbinService>()?;
let permissions = casbin_service
.get_user_permissions(&user.id.to_string())
.await?;
Ok(permissions
.into_iter()
.map(|p| PermissionPair {
resource: p.0,
action: p.1,
})
.collect())
}
#[graphql(guard = "RequireReadPermission::new(\"permissions\")")]
async fn get_all_policies(&self, ctx: &Context<'_>) -> Result<Vec<PolicyType>> {
let casbin_service = ctx.data::<CasbinService>()?;
let policies = casbin_service.get_all_policies().await?;
Ok(policies
.into_iter()
.filter(|p| p.len() >= 3)
.map(|p| PolicyType {
role: p[0].clone(),
resource: p[1].clone(),
action: p[2].clone(),
})
.collect())
}
#[graphql(guard = "RequireReadPermission::new(\"permissions\")")]
async fn get_role_permissions(
&self,
ctx: &Context<'_>,
role_name: String,
) -> Result<Vec<PermissionPair>> {
let casbin_service = ctx.data::<CasbinService>()?;
let permissions = casbin_service.get_role_permissions(&role_name).await?;
Ok(permissions
.into_iter()
.map(|p| PermissionPair {
resource: p.0,
action: p.1,
})
.collect())
}
#[graphql(guard = "RequireReadPermission::new(\"permissions\")")]
async fn can_read(&self, ctx: &Context<'_>, resource: String) -> Result<bool> {
let user = get_auth_user(ctx).await?;
let casbin_service = ctx.data::<CasbinService>()?;
let can_read = casbin_service
.can_read(&user.id.to_string(), &resource)
.await?;
Ok(can_read)
}
#[graphql(guard = "RequireReadPermission::new(\"permissions\")")]
async fn can_write(&self, ctx: &Context<'_>, resource: String) -> Result<bool> {
let user = get_auth_user(ctx).await?;
let casbin_service = ctx.data::<CasbinService>()?;
let can_write = casbin_service
.can_write(&user.id.to_string(), &resource)
.await?;
Ok(can_write)
}
#[graphql(guard = "RequireReadPermission::new(\"permissions\")")]
async fn can_delete(&self, ctx: &Context<'_>, resource: String) -> Result<bool> {
let user = get_auth_user(ctx).await?;
let casbin_service = ctx.data::<CasbinService>()?;
let can_delete = casbin_service
.can_delete(&user.id.to_string(), &resource)
.await?;
Ok(can_delete)
}
}

166
src/graphql/queries/user.rs Normal file
View File

@ -0,0 +1,166 @@
use crate::auth::get_auth_user;
use crate::graphql::guards::*;
use crate::graphql::types::users::*;
use crate::services::casbin_service::CasbinService;
use crate::services::user_service::UserService;
use async_graphql::{Context, Object, Result};
use tracing::info;
use tracing_subscriber::filter;
#[derive(Default)]
pub struct UserQuery;
#[Object]
impl UserQuery {
#[graphql(guard = "RequireReadPermission::new(\"users\")")]
async fn current_user(&self, ctx: &Context<'_>) -> Result<User> {
let auth_user = get_auth_user(ctx).await?;
let user_service = ctx.data::<UserService>()?;
user_service
.get_user_by_id(auth_user.id)
.await?
.map(|user| user.into())
.ok_or_else(|| async_graphql::Error::new("User not found"))
}
/// 获取当前用户的组信息
#[graphql(guard = "RequireReadPermission::new(\"users\")")]
async fn current_user_groups(&self, ctx: &Context<'_>) -> Result<Vec<String>> {
let auth_user = get_auth_user(ctx).await?;
let casbin_service = ctx.data::<CasbinService>()?;
let groups = casbin_service
.get_user_roles(&auth_user.id.to_string())
.await
.map_err(|e| async_graphql::Error::new(format!("Failed to get user groups: {}", e)))?;
Ok(groups)
}
/// 获取当前用户信息(包含组)
#[graphql(guard = "RequireReadPermission::new(\"users\")")]
async fn current_user_with_groups(&self, ctx: &Context<'_>) -> Result<UserWithGroups> {
let auth_user = get_auth_user(ctx).await?;
let user_service = ctx.data::<UserService>()?;
let user_with_groups = user_service
.get_user_with_groups(auth_user.id)
.await?
.ok_or_else(|| async_graphql::Error::new("User not found"))?;
Ok(user_with_groups.into())
}
/// 获取指定用户的组信息
#[graphql(guard = "RequireReadPermission::new(\"users\")")]
async fn user_groups(&self, ctx: &Context<'_>, user_id: uuid::Uuid) -> Result<Vec<String>> {
let casbin_service = ctx.data::<CasbinService>()?;
let groups = casbin_service
.get_user_roles(&user_id.to_string())
.await
.map_err(|e| async_graphql::Error::new(format!("Failed to get user groups: {}", e)))?;
Ok(groups)
}
/// 获取指定用户信息(包含组)
#[graphql(guard = "RequireReadPermission::new(\"users\")")]
async fn user_with_groups(
&self,
ctx: &Context<'_>,
user_id: uuid::Uuid,
) -> Result<UserWithGroups> {
let user_service = ctx.data::<UserService>()?;
let user_with_groups = user_service
.get_user_with_groups(user_id)
.await?
.ok_or_else(|| async_graphql::Error::new("User not found"))?;
Ok(user_with_groups.into())
}
#[graphql(guard = "RequireReadPermission::new(\"users\")")]
async fn users(
&self,
ctx: &Context<'_>,
offset: Option<u64>,
limit: Option<u64>,
sort_by: Option<String>,
sort_order: Option<String>,
filter: Option<String>,
) -> Result<Vec<User>> {
let user_service = ctx.data::<UserService>()?;
info!("users im here");
let offset = offset.unwrap_or(0);
let limit = limit.unwrap_or(20);
let sort_by = sort_by.unwrap_or("created_at".to_string());
let sort_order = sort_order.unwrap_or("desc".to_string());
user_service
.get_all_users(offset, limit, sort_by, sort_order, filter)
.await
.map(|user| user.into_iter().map(|u| u.into()).collect())
}
#[graphql(guard = "RequireReadPermission::new(\"users\")")]
async fn users_with_groups(
&self,
ctx: &Context<'_>,
offset: Option<u64>,
limit: Option<u64>,
sort_by: Option<String>,
sort_order: Option<String>,
filter: Option<String>,
) -> Result<Vec<UserWithGroups>> {
let user_service = ctx.data::<UserService>()?;
let offset = offset.unwrap_or(0);
let limit = limit.unwrap_or(20);
let sort_by = sort_by.unwrap_or("created_at".to_string());
let sort_order = sort_order.unwrap_or("desc".to_string());
user_service
.get_all_users_with_groups(offset, limit, sort_by, sort_order, filter)
.await
.map(|user| user.into_iter().map(|u| u.into()).collect())
}
#[graphql(guard = "RequireReadPermission::new(\"users\")")]
async fn users_info(
&self,
ctx: &Context<'_>,
offset: Option<u64>,
limit: Option<u64>,
sort_by: Option<String>,
sort_order: Option<String>,
filter: Option<String>,
) -> Result<UserInfoRespnose> {
let user_service = ctx.data::<UserService>()?;
let offset = offset.unwrap_or(0);
let limit = limit.unwrap_or(20);
let sort_by = sort_by.unwrap_or("created_at".to_string());
let sort_order = sort_order.unwrap_or("desc".to_string());
user_service
.users_info(offset, limit, sort_by, sort_order, filter)
.await
.map(|r| r.into())
}
// #[graphql(guard = "RequireReadPermission::new(\"invite_codes\")")]
// async fn my_invite_codes(&self, ctx: &Context<'_>) -> Result<Vec<InviteCode>> {
// let auth_user = get_auth_user(ctx).await?;
// let invite_code_service = ctx.data::<InviteCodeService>()?;
// invite_code_service
// .get_invite_codes_by_creator(auth_user.id)
// .await
// }
// #[graphql(guard = "RequirePermission::new(\"invite_codes\", \"write\")")]
// async fn validate_invite_code(&self, ctx: &Context<'_>, code: String) -> Result<bool> {
// let invite_code_service = ctx.data::<InviteCodeService>()?;
// invite_code_service
// .validate_invite_code(crate::models::invite_code::ValidateInviteCodeInput { code })
// .await
// }
}

File diff suppressed because it is too large Load Diff

View File

@ -1,701 +0,0 @@
use crate::models::{user::Role, Setting};
use async_graphql::{InputObject, SimpleObject, Union};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use uuid::Uuid;
#[derive(InputObject)]
pub struct RegisterInput {
pub username: String,
pub email: String,
pub password: String,
pub invite_code: String,
pub role: Option<Role>,
}
#[derive(InputObject)]
pub struct CreateUserInput {
pub username: String,
pub email: String,
pub password: String,
pub role: Option<Role>,
}
#[derive(InputObject)]
pub struct LoginInput {
pub username: String,
pub password: String,
}
#[derive(SimpleObject)]
pub struct LoginResponse {
pub token: String,
pub user_id: String,
}
#[derive(InputObject)]
pub struct CreateInviteCodeInput {
pub expires_in_days: Option<i32>,
}
#[derive(InputObject)]
pub struct ValidateInviteCodeInput {
pub code: String,
}
#[derive(SimpleObject)]
pub struct InviteCodeResponse {
pub code: String,
pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
}
#[derive(InputObject)]
pub struct InitializeAdminInput {
pub username: String,
pub email: String,
pub password: String,
}
#[derive(SimpleObject)]
pub struct InitializeAdminResponse {
pub success: bool,
pub message: String,
pub user: Option<crate::models::user::User>,
}
#[derive(SimpleObject)]
pub struct UserInfoRespnose {
pub total_users: i64,
pub total_active_users: i64,
pub total_inactive_users: i64,
pub total_admin_users: i64,
pub total_user_users: i64,
pub users: Vec<crate::models::user::UserInfoRow>,
}
// Settings GraphQL types
#[derive(SimpleObject, Debug, Clone)]
pub struct SettingType {
pub id: Uuid,
pub key: String,
pub value: Option<String>,
pub value_type: String,
pub description: Option<String>,
pub category: Option<String>,
pub is_encrypted: Option<bool>,
pub is_system: Option<bool>,
pub is_editable: Option<bool>,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
pub created_by: Option<Uuid>,
pub updated_by: Option<Uuid>,
}
impl From<Setting> for SettingType {
fn from(setting: Setting) -> Self {
SettingType {
id: setting.id,
key: setting.key,
value: setting.value,
value_type: setting.value_type,
description: setting.description,
category: setting.category,
is_encrypted: setting.is_encrypted,
is_system: setting.is_system,
is_editable: setting.is_editable,
created_at: setting.created_at,
updated_at: setting.updated_at,
created_by: setting.created_by,
updated_by: setting.updated_by,
}
}
}
#[derive(InputObject)]
pub struct CreateSettingInput {
pub key: String,
pub value: Option<String>,
pub value_type: String,
pub description: Option<String>,
pub category: String,
pub is_encrypted: Option<bool>,
pub is_system: Option<bool>,
pub is_editable: Option<bool>,
}
#[derive(InputObject)]
pub struct UpdateSettingInput {
pub value: Option<String>,
pub description: Option<String>,
pub category: Option<String>,
pub is_editable: Option<bool>,
}
#[derive(InputObject)]
pub struct SettingFilterInput {
pub category: Option<String>,
pub is_system: Option<bool>,
pub is_editable: Option<bool>,
pub search: Option<String>,
}
#[derive(SimpleObject)]
pub struct SettingHistoryType {
pub id: Uuid,
pub setting_id: Uuid,
pub old_value: Option<String>,
pub new_value: Option<String>,
pub changed_by: Option<Uuid>,
pub change_reason: Option<String>,
pub created_at: chrono::DateTime<chrono::Utc>,
}
#[derive(SimpleObject)]
pub struct SettingsStatsType {
pub categories: Vec<String>,
pub stats: std::collections::HashMap<String, i64>,
}
// Page Block GraphQL types
#[derive(SimpleObject)]
pub struct PageType {
pub id: Uuid,
pub title: String,
pub slug: String,
pub description: Option<String>,
pub is_active: bool,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
pub created_by: Option<Uuid>,
pub updated_by: Option<Uuid>,
}
#[derive(Union)]
pub enum BlockType {
TextBlock(TextBlockType),
ChartBlock(ChartBlockType),
SettingsBlock(SettingsBlockType),
TableBlock(TableBlockType),
HeroBlock(HeroBlockType),
}
#[derive(SimpleObject)]
pub struct TextBlockType {
pub id: Uuid,
pub page_id: Uuid,
pub block_order: i32,
pub title: Option<String>,
pub markdown: String,
pub is_active: bool,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
}
#[derive(SimpleObject)]
pub struct ChartBlockType {
pub id: Uuid,
pub page_id: Uuid,
pub block_order: i32,
pub title: String,
pub chart_type: String,
pub series: Vec<DataPointType>,
pub config: Option<Value>,
pub is_active: bool,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
}
#[derive(SimpleObject)]
pub struct DataPointType {
pub id: Uuid,
pub chart_block_id: Uuid,
pub x: f64,
pub y: f64,
pub label: Option<String>,
pub color: Option<String>,
}
#[derive(SimpleObject)]
pub struct SettingsBlockType {
pub id: Uuid,
pub page_id: Uuid,
pub block_order: i32,
pub title: Option<String>,
pub category: String,
pub editable: bool,
pub display_mode: String,
pub is_active: bool,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
}
#[derive(SimpleObject)]
pub struct TableBlockType {
pub id: Uuid,
pub page_id: Uuid,
pub block_order: i32,
pub title: Option<String>,
pub columns: Vec<TableColumnType>,
pub data_source: String,
pub data_config: Option<Value>,
pub is_active: bool,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
}
#[derive(SimpleObject)]
pub struct TableColumnType {
pub id: Uuid,
pub table_block_id: Uuid,
pub name: String,
pub label: String,
pub data_type: String,
pub is_sortable: bool,
pub is_filterable: bool,
pub width: Option<i32>,
pub order: i32,
}
#[derive(SimpleObject)]
pub struct HeroBlockType {
pub id: Uuid,
pub page_id: Uuid,
pub block_order: i32,
pub title: String,
pub subtitle: Option<String>,
pub background_image: Option<String>,
pub background_color: Option<String>,
pub text_color: Option<String>,
pub cta_text: Option<String>,
pub cta_link: Option<String>,
pub is_active: bool,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
}
// Page Block Input types
#[derive(InputObject)]
pub struct CreatePageInputType {
pub title: String,
pub slug: String,
pub description: Option<String>,
pub is_active: Option<bool>,
}
#[derive(InputObject)]
pub struct UpdatePageInputType {
pub title: Option<String>,
pub slug: Option<String>,
pub description: Option<String>,
pub is_active: Option<bool>,
}
#[derive(InputObject)]
pub struct CreateTextBlockInputType {
pub page_id: Uuid,
pub block_order: i32,
pub title: Option<String>,
pub markdown: String,
pub is_active: Option<bool>,
}
#[derive(InputObject)]
pub struct CreateChartBlockInputType {
pub page_id: Uuid,
pub block_order: i32,
pub title: String,
pub chart_type: String,
pub series: Vec<CreateDataPointInputType>,
pub config: Option<Value>,
pub is_active: Option<bool>,
}
#[derive(InputObject)]
pub struct CreateDataPointInputType {
pub x: f64,
pub y: f64,
pub label: Option<String>,
pub color: Option<String>,
}
#[derive(InputObject)]
pub struct CreateSettingsBlockInputType {
pub page_id: Uuid,
pub block_order: i32,
pub title: Option<String>,
pub category: String,
pub editable: bool,
pub display_mode: String,
pub is_active: Option<bool>,
}
#[derive(InputObject)]
pub struct PageFilterInputType {
pub title: Option<String>,
pub slug: Option<String>,
pub is_active: Option<bool>,
pub search: Option<String>,
}
// Enhanced Settings types for the settings center
#[derive(SimpleObject)]
pub struct SettingCenterType {
pub id: Uuid,
pub key: String,
pub value: Option<String>,
pub value_type: String,
pub is_encrypted: Option<bool>,
pub is_editable: Option<bool>,
pub is_system: Option<bool>,
pub description: Option<String>,
pub updated_at: chrono::DateTime<chrono::Utc>,
}
#[derive(InputObject)]
pub struct UpdateSettingCenterInput {
pub key: String,
pub value: Option<String>,
pub description: Option<String>,
}
#[derive(InputObject)]
pub struct BatchUpdateSettingsInput {
pub updates: Vec<UpdateSettingCenterInput>,
pub reason: Option<String>,
}
/// 配置分类页面类型,包含页面信息和相关配置项
#[derive(SimpleObject)]
pub struct CategoryPageType {
/// 页面信息,如果不存在则为 None
pub page: Option<PageType>,
/// 该分类下的所有配置项
pub settings: Vec<SettingCenterType>,
/// 分类名称
pub category: String,
/// 该分类下的配置项总数
pub settings_count: i32,
/// 该分类下的系统配置项数量
pub system_settings_count: i32,
/// 该分类下的可编辑配置项数量
pub editable_settings_count: i32,
}
/// 权限类型
#[derive(Debug, Clone, SimpleObject)]
pub struct PermissionType {
pub id: Uuid,
pub name: String,
pub description: Option<String>,
pub resource: String,
pub action: String,
pub is_active: bool,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
}
/// 角色权限关联类型
#[derive(Debug, Clone, SimpleObject)]
pub struct RolePermissionType {
pub id: Uuid,
pub role_name: String,
pub permission: PermissionType,
pub granted_by: Option<Uuid>,
pub granted_at: chrono::DateTime<chrono::Utc>,
}
/// 用户角色关联类型
#[derive(Debug, Clone, SimpleObject)]
pub struct UserRoleType {
pub id: Uuid,
pub user_id: Uuid,
pub role_name: String,
pub granted_by: Option<Uuid>,
pub granted_at: chrono::DateTime<chrono::Utc>,
pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
}
/// 权限策略类型
#[derive(Debug, Clone, SimpleObject)]
pub struct PolicyType {
pub role: String,
pub resource: String,
pub action: String,
}
/// 权限检查结果类型
#[derive(Debug, Clone, SimpleObject)]
pub struct PermissionCheckResult {
pub user_id: String,
pub resource: String,
pub action: String,
pub has_permission: bool,
pub roles: Vec<String>,
}
/// 权限管理查询输入类型
#[derive(Debug, InputObject)]
pub struct PermissionFilterInput {
pub resource: Option<String>,
pub action: Option<String>,
pub role_name: Option<String>,
pub is_active: Option<bool>,
}
/// 角色权限分配输入类型
#[derive(Debug, InputObject)]
pub struct AssignRolePermissionInput {
pub role_name: String,
pub permission_id: Uuid,
pub granted_by: Uuid,
}
/// 用户角色分配输入类型
#[derive(Debug, InputObject)]
pub struct AssignUserRoleInput {
pub user_id: Uuid,
pub role_name: String,
pub granted_by: Uuid,
pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
}
#[derive(Debug, Clone, SimpleObject)]
pub struct PermissionPair {
pub resource: String,
pub action: String,
}
// 站点与运营配置相关类型
/// 站点基本信息类型
#[derive(Debug, Clone, SimpleObject)]
pub struct SiteInfoType {
pub name: String,
pub locale_default: String,
pub locales_supported: Vec<String>,
}
/// 品牌配置类型
#[derive(Debug, Clone, SimpleObject)]
pub struct BrandConfigType {
pub logo_url: String,
pub primary_color: String,
pub dark_mode_default: bool,
}
/// 页脚链接类型
#[derive(Debug, Clone, SimpleObject)]
pub struct FooterLinkType {
pub name: String,
pub url: String,
pub visible_to_guest: bool,
}
/// 站点配置类型
#[derive(Debug, Clone, SimpleObject)]
pub struct SiteConfigType {
pub info: SiteInfoType,
pub brand: BrandConfigType,
pub footer_links: Vec<FooterLinkType>,
}
/// 横幅公告类型
#[derive(Debug, Clone, SimpleObject)]
pub struct BannerNoticeType {
pub enabled: bool,
pub text: std::collections::HashMap<String, String>, // 多语言文本
}
/// 维护窗口类型
#[derive(Debug, Clone, SimpleObject)]
pub struct MaintenanceWindowType {
pub enabled: bool,
pub start_time: Option<chrono::DateTime<chrono::Utc>>,
pub end_time: Option<chrono::DateTime<chrono::Utc>>,
pub message: std::collections::HashMap<String, String>, // 多语言消息
}
/// 弹窗公告类型
#[derive(Debug, Clone, SimpleObject)]
pub struct ModalAnnouncementType {
pub id: String,
pub title: std::collections::HashMap<String, String>, // 多语言标题
pub content: std::collections::HashMap<String, String>, // 多语言内容
pub start_time: chrono::DateTime<chrono::Utc>,
pub end_time: chrono::DateTime<chrono::Utc>,
pub audience: Vec<String>,
pub priority: String,
}
/// 公告维护配置类型
#[derive(Debug, Clone, SimpleObject)]
pub struct NoticeMaintenanceType {
pub banner: BannerNoticeType,
pub maintenance_window: MaintenanceWindowType,
pub modal_announcements: Vec<ModalAnnouncementType>,
}
/// 文档链接类型
#[derive(Debug, Clone, SimpleObject)]
pub struct DocLinkType {
pub name: String,
pub url: String,
pub description: String,
}
/// 聊天群组类型
#[derive(Debug, Clone, SimpleObject)]
pub struct ChatGroupType {
pub name: String,
pub url: Option<String>,
pub qr_code: Option<String>,
pub description: String,
}
/// 支持渠道类型
#[derive(Debug, Clone, SimpleObject)]
pub struct SupportChannelsType {
pub email: String,
pub ticket_system: String,
pub chat_groups: Vec<ChatGroupType>,
pub working_hours: std::collections::HashMap<String, String>, // 多语言工作时间
}
/// 文档支持配置类型
#[derive(Debug, Clone, SimpleObject)]
pub struct DocsSupportType {
pub links: Vec<DocLinkType>,
pub channels: SupportChannelsType,
}
/// 功能开关类型
#[derive(Debug, Clone, SimpleObject)]
pub struct FeatureSwitchesType {
pub registration_enabled: bool,
pub invite_code_required: bool,
pub email_verification: bool,
}
/// 限制配置类型
#[derive(Debug, Clone, SimpleObject)]
pub struct LimitsConfigType {
pub max_users: i32,
pub max_invite_codes_per_user: i32,
pub session_timeout_hours: i32,
}
/// 通知配置类型
#[derive(Debug, Clone, SimpleObject)]
pub struct NotificationConfigType {
pub welcome_email: bool,
pub system_announcements: bool,
pub maintenance_alerts: bool,
}
/// 运营配置类型
#[derive(Debug, Clone, SimpleObject)]
pub struct OpsConfigType {
pub features: FeatureSwitchesType,
pub limits: LimitsConfigType,
pub notifications: NotificationConfigType,
}
/// 完整的站点与运营配置类型
#[derive(Debug, Clone, SimpleObject)]
pub struct SiteOpsConfigType {
pub site: SiteConfigType,
pub notice_maintenance: NoticeMaintenanceType,
pub docs_support: DocsSupportType,
pub ops: OpsConfigType,
}
/// 更新站点配置输入类型
#[derive(Debug, InputObject)]
pub struct UpdateSiteConfigInput {
pub name: Option<String>,
pub locale_default: Option<String>,
pub locales_supported: Option<Vec<String>>,
pub logo_url: Option<String>,
pub primary_color: Option<String>,
pub dark_mode_default: Option<bool>,
pub footer_links: Option<Vec<FooterLinkInput>>,
}
/// 页脚链接输入类型
#[derive(Debug, InputObject, Serialize, Deserialize)]
pub struct FooterLinkInput {
pub name: String,
pub url: String,
pub visible_to_guest: bool,
}
/// 更新公告配置输入类型
#[derive(Debug, InputObject)]
pub struct UpdateNoticeConfigInput {
pub banner_enabled: Option<bool>,
pub banner_text: Option<std::collections::HashMap<String, String>>,
pub maintenance_enabled: Option<bool>,
pub maintenance_start_time: Option<chrono::DateTime<chrono::Utc>>,
pub maintenance_end_time: Option<chrono::DateTime<chrono::Utc>>,
pub maintenance_message: Option<std::collections::HashMap<String, String>>,
}
/// 更新弹窗公告输入类型
#[derive(Debug, InputObject)]
pub struct UpdateModalAnnouncementInput {
pub id: String,
pub title: Option<std::collections::HashMap<String, String>>,
pub content: Option<std::collections::HashMap<String, String>>,
pub start_time: Option<chrono::DateTime<chrono::Utc>>,
pub end_time: Option<chrono::DateTime<chrono::Utc>>,
pub audience: Option<Vec<String>>,
pub priority: Option<String>,
}
/// 更新文档支持配置输入类型
#[derive(Debug, InputObject)]
pub struct UpdateDocsSupportInput {
pub links: Option<Vec<DocLinkInput>>,
pub email: Option<String>,
pub ticket_system: Option<String>,
pub working_hours: Option<std::collections::HashMap<String, String>>,
}
/// 文档链接输入类型
#[derive(Debug, InputObject, Serialize, Deserialize)]
pub struct DocLinkInput {
pub name: String,
pub url: String,
pub description: String,
}
/// 更新运营配置输入类型
#[derive(Debug, InputObject)]
pub struct UpdateOpsConfigInput {
pub registration_enabled: Option<bool>,
pub invite_code_required: Option<bool>,
pub email_verification: Option<bool>,
pub max_users: Option<i32>,
pub max_invite_codes_per_user: Option<i32>,
pub session_timeout_hours: Option<i32>,
pub welcome_email: Option<bool>,
pub system_announcements: Option<bool>,
pub maintenance_alerts: Option<bool>,
}
/// 配置更新结果类型
#[derive(Debug, Clone, SimpleObject)]
pub struct ConfigUpdateResultType {
pub success: bool,
pub message: String,
pub updated_settings: Vec<SettingType>,
}
/// 配置验证结果类型
#[derive(Debug, Clone, SimpleObject)]
pub struct ConfigValidationResultType {
pub valid: bool,
pub errors: Vec<String>,
pub warnings: Vec<String>,
}

316
src/graphql/types/blog.rs Normal file
View File

@ -0,0 +1,316 @@
pub use async_graphql::{InputObject, SimpleObject};
pub use chrono::{DateTime, Utc};
pub use serde::{Deserialize, Serialize};
pub use crate::models::blog::{
Blog as ModelBlog, BlogCategory as ModelBlogCategory, BlogTag as ModelBlogTag,
};
pub use input::*;
pub use output::*;
pub mod output {
use super::*;
use crate::from_model;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize, 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>,
}
/// 博客标签 GraphQL 类型
#[derive(Debug, Clone, Serialize, Deserialize, 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>,
}
/// 博客文章 GraphQL 类型
#[derive(Debug, Clone, Serialize, Deserialize, 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>,
}
// 使用宏自动实现 From trait
from_model!(
BlogCategory,
ModelBlogCategory,
id,
name,
slug,
description,
color,
icon,
is_active,
sort_order,
created_at,
updated_at,
created_by,
updated_by
);
from_model!(
BlogTag,
ModelBlogTag,
id,
name,
slug,
description,
color,
is_active,
created_at,
updated_at,
created_by,
updated_by
);
from_model!(
Blog,
ModelBlog,
id,
title,
slug,
excerpt,
content,
category_id,
status,
featured_image,
meta_title,
meta_description,
published_at,
view_count,
is_featured,
is_active,
created_at,
updated_at,
created_by,
updated_by
);
/// 博客文章详情(包含分类和标签信息)
#[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)]
pub struct BlogDetail {
#[graphql(flatten)]
pub blog: Blog,
pub category: Option<BlogCategory>,
pub tags: Vec<BlogTag>,
}
/// 博客标签关联 GraphQL 类型
#[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)]
pub struct BlogTagRelation {
pub id: Uuid,
pub blog_id: Uuid,
pub tag_id: Uuid,
pub created_at: DateTime<Utc>,
}
// 为 BlogTagRelation 实现 From trait
from_model!(
BlogTagRelation,
crate::models::blog::BlogTagRelation,
id,
blog_id,
tag_id,
created_at
);
// 为 BlogStats 实现 From trait
from_model!(
BlogStats,
crate::models::blog::BlogStats,
total_blogs,
published_blogs,
draft_blogs,
archived_blogs,
total_categories,
total_tags,
total_views
);
// 博客统计信息
#[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,
}
// 注意:业务逻辑方法已经在 models/blog.rs 中实现
// 这里的 GraphQL 类型主要用于 API 输出,业务逻辑应该在 model 层处理
impl BlogDetail {
/// 从博客文章创建详情对象
pub fn new(blog: Blog, category: Option<BlogCategory>, tags: Vec<BlogTag>) -> Self {
Self {
blog,
category,
tags,
}
}
}
}
pub mod input {
use super::*;
use uuid::Uuid;
// 创建博客分类输入
#[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"
}
}

169
src/graphql/types/config.rs Normal file
View File

@ -0,0 +1,169 @@
use crate::graphql::guards::*;
use async_graphql::{InputObject, SimpleObject};
use chrono::{DateTime, Utc};
pub use input::*;
pub use output::*;
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)]
pub struct ConfigValue {
pub key: String,
pub value: serde_json::Value,
pub value_type: String,
pub description: Option<String>,
pub category: String,
}
pub mod output {
use serde::de::Error;
use super::*;
#[derive(Debug, Clone, Serialize, Deserialize, FromRow, SimpleObject)]
pub struct Config {
pub id: Uuid,
pub key: String,
pub value: Option<String>,
pub value_type: String,
pub description: Option<String>,
#[graphql(guard = "RequireWritePermission::new(\"config\")")]
pub category: Option<String>,
#[graphql(guard = "RequireWritePermission::new(\"config\")")]
pub is_encrypted: Option<bool>,
#[graphql(guard = "RequireWritePermission::new(\"config\")")]
pub is_system: Option<bool>,
#[graphql(guard = "RequireWritePermission::new(\"config\")")]
pub is_editable: Option<bool>,
#[graphql(guard = "RequireWritePermission::new(\"config\")")]
pub created_at: DateTime<Utc>,
#[graphql(guard = "RequireWritePermission::new(\"config\")")]
pub updated_at: DateTime<Utc>,
#[graphql(guard = "RequireWritePermission::new(\"config\")")]
pub created_by: Option<Uuid>,
#[graphql(guard = "RequireWritePermission::new(\"config\")")]
pub updated_by: Option<Uuid>,
}
impl Config {
/// 获取配置值的类型化版本
pub fn get_typed_value<T>(&self) -> Result<T, serde_json::Error>
where
T: for<'de> Deserialize<'de>,
{
match &self.value {
Some(v) => serde_json::from_str(v),
None => Err(serde_json::Error::custom("No value set")),
}
}
/// 设置配置值
pub fn set_value<T>(&mut self, value: &T) -> Result<(), serde_json::Error>
where
T: Serialize,
{
self.value = Some(serde_json::to_string(value)?);
Ok(())
}
/// 检查是否为特定类型
pub fn is_type(&self, expected_type: &str) -> bool {
self.value_type == expected_type
}
/// 获取布尔值
pub fn get_bool(&self) -> Result<bool, String> {
if self.value_type == "boolean" {
self.value
.as_ref()
.and_then(|v| v.parse::<bool>().ok())
.ok_or_else(|| "Invalid boolean value".to_string())
} else {
Err("Setting is not a boolean type".to_string())
}
}
/// 获取数字值
pub fn get_number(&self) -> Result<f64, String> {
if self.value_type == "number" {
self.value
.as_ref()
.and_then(|v| v.parse::<f64>().ok())
.ok_or_else(|| "Invalid number value".to_string())
} else {
Err("Setting is not a number type".to_string())
}
}
/// 获取JSON值
pub fn get_json(&self) -> Result<serde_json::Value, serde_json::Error> {
if self.value_type == "json" {
match &self.value {
Some(v) => serde_json::from_str(v),
None => Ok(serde_json::Value::Null),
}
} else {
Err(serde_json::Error::custom("Setting is not a JSON type"))
}
}
/// 获取字符串值
pub fn get_string(&self) -> Result<String, String> {
if self.value_type == "string" {
Ok(self.value.clone().unwrap_or_default())
} else {
Err("Setting is not a string type".to_string())
}
}
}
impl Default for Config {
fn default() -> Self {
Self {
id: Uuid::nil(),
key: String::new(),
value: None,
value_type: "string".to_string(),
description: None,
category: Some("general".to_string()),
is_encrypted: Some(false),
is_system: Some(false),
is_editable: Some(true),
created_at: Utc::now(),
updated_at: Utc::now(),
created_by: None,
updated_by: None,
}
}
}
}
pub mod input {
use super::*;
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
pub struct UpdateConfig {
pub key: String,
pub value: Option<String>,
pub description: Option<String>,
pub category: Option<String>,
pub is_editable: Option<bool>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub struct ConfigFilter {
pub category: Option<String>,
pub is_system: Option<bool>,
pub is_editable: Option<bool>,
pub search: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfigHistory {
pub id: Uuid,
pub setting_id: Uuid,
pub old_value: Option<String>,
pub new_value: Option<String>,
pub changed_by: Option<Uuid>,
pub change_reason: Option<String>,
pub created_at: DateTime<Utc>,
}
}

123
src/graphql/types/mod.rs Normal file
View File

@ -0,0 +1,123 @@
use async_graphql::{InputObject, SimpleObject};
use serde::{Deserialize, Serialize};
pub mod blog;
pub mod config;
pub mod permission;
pub mod users;
/// 用于自动实现 From<Model> for GraphQLType 的宏
///
/// 这个宏假设 GraphQL 类型和 Model 类型具有相同的字段名和类型。
///
/// # 用法
/// ```rust
/// from_model!(GraphQLType, ModelType, field1, field2, field3, ...);
/// ```
///
/// # 示例
/// ```rust
/// from_model!(BlogCategory, crate::models::blog::BlogCategory,
/// id, name, slug, description, color, icon, is_active, sort_order,
/// created_at, updated_at, created_by, updated_by
/// );
/// ```
#[macro_export]
macro_rules! from_model {
($graphql_type:ident, $model_type:ty, $( $field:ident ),* $(,)?) => {
impl From<$model_type> for $graphql_type {
fn from(model: $model_type) -> Self {
Self {
$(
$field: model.$field,
)*
}
}
}
};
}
/// 更灵活的宏,支持字段映射和转换
///
/// # 用法
/// ```rust
/// from_model_with_mapping!(
/// GraphQLType,
/// ModelType,
/// field1,
/// field2 => |val| val.map(|v| v.to_string()),
/// field3 => |val| val.into()
/// );
/// ```
#[macro_export]
macro_rules! from_model_with_mapping {
(
$graphql_type:ident,
$model_type:ty,
$( $field:ident $( => $mapping:expr )? ),* $(,)?
) => {
impl From<$model_type> for $graphql_type {
fn from(model: $model_type) -> Self {
Self {
$(
$field: from_model_with_mapping!(@apply_mapping model.$field, $( $mapping )?),
)*
}
}
}
};
// 没有映射,直接使用字段值
(@apply_mapping $value:expr,) => {
$value
};
// 有映射函数,应用映射
(@apply_mapping $value:expr, $mapping:expr) => {
($mapping)($value)
};
}
pub use blog::*;
pub use config::*;
pub use config::*;
pub use permission::*;
pub use users::*;
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
pub struct PaginationInput {
pub page: Option<i32>,
pub per_page: Option<i32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)]
pub struct PaginatedResult<T: async_graphql::OutputType + Send + Sync> {
pub items: Vec<T>,
pub total: i64,
pub page: i32,
pub per_page: i32,
pub total_pages: i32,
}
impl<T: async_graphql::OutputType + Send + Sync> PaginatedResult<T> {
pub fn new(items: Vec<T>, total: i64, page: i32, per_page: i32) -> Self {
let total_pages = ((total as f64) / (per_page as f64)).ceil() as i32;
Self {
items,
total,
page,
per_page,
total_pages,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)]
pub struct PageStats {
pub text_blocks: i32,
pub chart_blocks: i32,
pub settings_blocks: i32,
pub table_blocks: i32,
pub hero_blocks: i32,
pub total_blocks: i32,
}

View File

@ -0,0 +1,102 @@
use async_graphql::{InputObject, SimpleObject};
use std::hash::{Hash, Hasher};
use uuid::Uuid;
/// 权限类型
#[derive(Debug, Clone, SimpleObject)]
pub struct PermissionType {
pub id: Uuid,
pub name: String,
pub description: Option<String>,
pub resource: String,
pub action: String,
pub is_active: bool,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
}
/// 角色权限关联类型
#[derive(Debug, Clone, SimpleObject)]
pub struct RolePermissionType {
pub id: Uuid,
pub role_name: String,
pub permission: PermissionType,
pub granted_by: Option<Uuid>,
pub granted_at: chrono::DateTime<chrono::Utc>,
}
/// 用户角色关联类型
#[derive(Debug, Clone, SimpleObject)]
pub struct UserRoleType {
pub id: Uuid,
pub user_id: Uuid,
pub role_name: String,
pub granted_by: Option<Uuid>,
pub granted_at: chrono::DateTime<chrono::Utc>,
pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
}
/// 权限策略类型
#[derive(Debug, Clone, SimpleObject)]
pub struct PolicyType {
pub role: String,
pub resource: String,
pub action: String,
}
/// 权限检查结果类型
#[derive(Debug, Clone, SimpleObject)]
pub struct PermissionCheckResult {
pub user_id: String,
pub resource: String,
pub action: String,
pub has_permission: bool,
pub roles: Vec<String>,
}
/// 权限管理查询输入类型
#[derive(Debug, InputObject)]
pub struct PermissionFilterInput {
pub resource: Option<String>,
pub action: Option<String>,
pub role_name: Option<String>,
pub is_active: Option<bool>,
}
/// 角色权限分配输入类型
#[derive(Debug, InputObject)]
pub struct AssignRolePermissionInput {
pub role_name: String,
pub permission_id: Uuid,
pub granted_by: Uuid,
}
/// 用户角色分配输入类型
#[derive(Debug, InputObject)]
pub struct AssignUserRoleInput {
pub user_id: Uuid,
pub role_name: String,
pub granted_by: Uuid,
pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
}
#[derive(Debug, Clone, SimpleObject)]
pub struct PermissionPair {
pub resource: String,
pub action: String,
}
impl Hash for PermissionPair {
fn hash<H: Hasher>(&self, state: &mut H) {
self.resource.hash(state);
self.action.hash(state);
}
}
impl PartialEq for PermissionPair {
fn eq(&self, other: &Self) -> bool {
self.resource == other.resource && self.action == other.action
}
}
impl Eq for PermissionPair {}

130
src/graphql/types/users.rs Normal file
View File

@ -0,0 +1,130 @@
use crate::from_model;
use async_graphql::{InputObject, SimpleObject};
pub use input::*;
pub use output::*;
pub mod input {
use super::*;
#[derive(InputObject)]
pub struct RegisterInput {
pub username: String,
pub email: String,
pub password: String,
pub invite_code: String,
}
#[derive(InputObject)]
pub struct CreateUserInput {
pub username: String,
pub email: String,
pub password: String,
}
#[derive(InputObject)]
pub struct LoginInput {
pub username: String,
pub password: String,
}
#[derive(InputObject)]
pub struct CreateInviteCodeInput {
pub expires_in_days: Option<i32>,
}
#[derive(InputObject)]
pub struct ValidateInviteCodeInput {
pub code: String,
}
#[derive(InputObject)]
pub struct InitializeAdminInput {
pub username: String,
pub email: String,
pub password: String,
}
}
pub mod output {
use crate::from_model_with_mapping;
use super::*;
use chrono::{DateTime, Utc};
use uuid::Uuid;
#[derive(SimpleObject)]
pub struct LoginResponse {
pub token: String,
pub user_id: String,
}
#[derive(SimpleObject)]
pub struct InviteCodeResponse {
pub code: String,
pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
}
#[derive(SimpleObject)]
pub struct InitializeAdminResponse {
pub success: bool,
pub message: String,
pub user: Option<User>,
}
#[derive(SimpleObject)]
pub struct UserInfoRespnose {
pub total_users: i64,
pub total_active_users: i64,
pub total_inactive_users: i64,
pub total_admin_users: i64,
pub total_user_users: i64,
pub users: Vec<UserWithGroups>,
}
#[derive(Debug, Clone, SimpleObject)]
pub struct User {
pub id: Uuid,
pub username: String,
pub email: String,
pub is_activate: bool,
pub created_at: Option<DateTime<Utc>>,
pub updated_at: Option<DateTime<Utc>>,
}
/// 包含组信息的用户
#[derive(Debug, Clone, SimpleObject)]
pub struct UserWithGroups {
pub user: User,
pub groups: Vec<String>,
}
from_model!(
User,
crate::models::user::User,
id,
username,
email,
is_activate,
created_at,
updated_at
);
from_model_with_mapping!(
UserInfoRespnose,
crate::models::user::UserInfoRespnose,
total_users,
total_active_users,
total_inactive_users,
total_admin_users,
total_user_users,
users => |val: Vec<crate::models::user::UserWithGroups>| val.into_iter().map(|u| u.into()).collect(),
);
from_model_with_mapping!(
UserWithGroups,
crate::models::user::UserWithGroups,
user => |user: crate::models::user::User| user.into(),
groups ,
);
}

View File

@ -12,9 +12,8 @@ use app::create_router;
use clap::Parser;
use cli::{
AddPolicyArgs, AssignRoleArgs, BlogArgs, BlogCommands, CheckPermissionArgs, Cli, Commands,
CreateBlogArgs, CreateCategoryArgs, CreateTagArgs, DeleteBlogArgs, ListBlogArgs,
ListRolePermissionsArgs, ListUserRolesArgs, MigrateArgs, PermissionsArgs, PermissionsCommands,
RemovePolicyArgs, RemoveRoleArgs, ServeArgs, ShowBlogArgs, UpdateBlogArgs,
RemovePolicyArgs, RemoveRoleArgs, ServeArgs,
};
use config::Config;
use db::{create_pool, run_migrations};
@ -23,6 +22,8 @@ use rustls;
use std::process;
use tokio::task;
use crate::graphql::types::PaginationInput;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli = Cli::parse();
@ -512,8 +513,7 @@ fn print_config_info(config: &Config, args: &ServeArgs) {
}
async fn blog_command(args: BlogArgs) -> Result<(), Box<dyn std::error::Error>> {
use models::blog::*;
use models::page_block::PaginationInput;
use crate::graphql::types::blog::*;
use serde_json;
use services::blog_service::BlogService;
use uuid::Uuid;

View File

@ -1,11 +1,11 @@
use async_graphql::{InputObject, SimpleObject};
pub use crate::graphql::types::blog::input::*;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use uuid::Uuid;
/// 博客分类模型
#[derive(Debug, Clone, Serialize, Deserialize, FromRow, SimpleObject)]
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct BlogCategory {
pub id: Uuid,
pub name: String,
@ -22,7 +22,7 @@ pub struct BlogCategory {
}
/// 博客标签模型
#[derive(Debug, Clone, Serialize, Deserialize, FromRow, SimpleObject)]
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct BlogTag {
pub id: Uuid,
pub name: String,
@ -37,7 +37,7 @@ pub struct BlogTag {
}
/// 博客文章模型
#[derive(Debug, Clone, Serialize, Deserialize, FromRow, SimpleObject)]
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct Blog {
pub id: Uuid,
pub title: String,
@ -60,16 +60,15 @@ pub struct Blog {
}
/// 博客文章详情(包含分类和标签信息)
#[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlogDetail {
#[graphql(flatten)]
pub blog: Blog,
pub category: Option<BlogCategory>,
pub tags: Vec<BlogTag>,
}
/// 博客标签关联模型
#[derive(Debug, Clone, Serialize, Deserialize, FromRow, SimpleObject)]
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct BlogTagRelation {
pub id: Uuid,
pub blog_id: Uuid,
@ -77,126 +76,8 @@ pub struct BlogTagRelation {
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)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlogStats {
pub total_blogs: i64,
pub published_blogs: i64,

1
src/models/config.rs Normal file
View File

@ -0,0 +1 @@
pub use crate::graphql::types::config::*;

View File

@ -1,13 +1,34 @@
pub mod blog;
pub mod config;
pub mod invite_code;
pub mod kafka_message;
pub mod page_block;
pub mod settings;
pub mod user;
pub use blog::*;
pub use config::*;
pub use invite_code::*;
pub use kafka_message::*;
pub use page_block::*;
pub use settings::*;
pub use user::*;
pub struct PaginatedResult<T: Send + Sync> {
pub items: Vec<T>,
pub total: i64,
pub page: i32,
pub per_page: i32,
pub total_pages: i32,
}
impl<T: Send + Sync> PaginatedResult<T> {
pub fn new(items: Vec<T>, total: i64, page: i32, per_page: i32) -> Self {
let total_pages = ((total as f64) / (per_page as f64)).ceil() as i32;
Self {
items,
total,
page,
per_page,
total_pages,
}
}
}

View File

@ -578,44 +578,3 @@ pub struct BlockSortInput {
pub field: String,
pub direction: String, // "asc" or "desc"
}
// 分页输入
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
pub struct PaginationInput {
pub page: Option<i32>,
pub per_page: Option<i32>,
}
// 分页结果
#[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)]
pub struct PaginatedResult<T: async_graphql::OutputType + Send + Sync> {
pub items: Vec<T>,
pub total: i64,
pub page: i32,
pub per_page: i32,
pub total_pages: i32,
}
impl<T: async_graphql::OutputType + Send + Sync> PaginatedResult<T> {
pub fn new(items: Vec<T>, total: i64, page: i32, per_page: i32) -> Self {
let total_pages = ((total as f64) / (per_page as f64)).ceil() as i32;
Self {
items,
total,
page,
per_page,
total_pages,
}
}
}
/// 页面统计信息
#[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)]
pub struct PageStats {
pub text_blocks: i32,
pub chart_blocks: i32,
pub settings_blocks: i32,
pub table_blocks: i32,
pub hero_blocks: i32,
pub total_blocks: i32,
}

View File

@ -1,161 +0,0 @@
use chrono::{DateTime, Utc};
use serde::{de::Error, Deserialize, Serialize};
use sqlx::FromRow;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct Setting {
pub id: Uuid,
pub key: String,
pub value: Option<String>,
pub value_type: String,
pub description: Option<String>,
pub category: Option<String>,
pub is_encrypted: Option<bool>,
pub is_system: Option<bool>,
pub is_editable: Option<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)]
pub struct CreateSetting {
pub key: String,
pub value: Option<String>,
pub value_type: String,
pub description: Option<String>,
pub category: String,
pub is_encrypted: Option<bool>,
pub is_system: Option<bool>,
pub is_editable: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateSetting {
pub value: Option<String>,
pub description: Option<String>,
pub category: Option<String>,
pub is_editable: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SettingValue {
pub key: String,
pub value: serde_json::Value,
pub value_type: String,
pub description: Option<String>,
pub category: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SettingFilter {
pub category: Option<String>,
pub is_system: Option<bool>,
pub is_editable: Option<bool>,
pub search: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SettingHistory {
pub id: Uuid,
pub setting_id: Uuid,
pub old_value: Option<String>,
pub new_value: Option<String>,
pub changed_by: Option<Uuid>,
pub change_reason: Option<String>,
pub created_at: DateTime<Utc>,
}
impl Setting {
/// 获取配置值的类型化版本
pub fn get_typed_value<T>(&self) -> Result<T, serde_json::Error>
where
T: for<'de> Deserialize<'de>,
{
match &self.value {
Some(v) => serde_json::from_str(v),
None => Err(serde_json::Error::custom("No value set")),
}
}
/// 设置配置值
pub fn set_value<T>(&mut self, value: &T) -> Result<(), serde_json::Error>
where
T: Serialize,
{
self.value = Some(serde_json::to_string(value)?);
Ok(())
}
/// 检查是否为特定类型
pub fn is_type(&self, expected_type: &str) -> bool {
self.value_type == expected_type
}
/// 获取布尔值
pub fn get_bool(&self) -> Result<bool, String> {
if self.value_type == "boolean" {
self.value
.as_ref()
.and_then(|v| v.parse::<bool>().ok())
.ok_or_else(|| "Invalid boolean value".to_string())
} else {
Err("Setting is not a boolean type".to_string())
}
}
/// 获取数字值
pub fn get_number(&self) -> Result<f64, String> {
if self.value_type == "number" {
self.value
.as_ref()
.and_then(|v| v.parse::<f64>().ok())
.ok_or_else(|| "Invalid number value".to_string())
} else {
Err("Setting is not a number type".to_string())
}
}
/// 获取JSON值
pub fn get_json(&self) -> Result<serde_json::Value, serde_json::Error> {
if self.value_type == "json" {
match &self.value {
Some(v) => serde_json::from_str(v),
None => Ok(serde_json::Value::Null),
}
} else {
Err(serde_json::Error::custom("Setting is not a JSON type"))
}
}
/// 获取字符串值
pub fn get_string(&self) -> Result<String, String> {
if self.value_type == "string" {
Ok(self.value.clone().unwrap_or_default())
} else {
Err("Setting is not a string type".to_string())
}
}
}
impl Default for Setting {
fn default() -> Self {
Self {
id: Uuid::nil(),
key: String::new(),
value: None,
value_type: "string".to_string(),
description: None,
category: Some("general".to_string()),
is_encrypted: Some(false),
is_system: Some(false),
is_editable: Some(true),
created_at: Utc::now(),
updated_at: Utc::now(),
created_by: None,
updated_by: None,
}
}
}

View File

@ -1,3 +1,4 @@
pub use crate::graphql::types::users::input::*;
use async_graphql::{Enum, SimpleObject};
use chrono::{DateTime, Utc};
use sea_query::Iden;
@ -18,13 +19,12 @@ impl Default for Role {
}
}
#[derive(Debug, Clone, FromRow, SimpleObject)]
#[derive(Debug, Clone, FromRow)]
pub struct User {
pub id: Uuid,
pub username: String,
pub email: String,
#[graphql(skip)]
pub password_hash: String,
pub password_hash: Option<String>,
pub role: Role,
pub invite_code_id: Option<Uuid>,
pub is_activate: bool,
@ -32,27 +32,14 @@ pub struct User {
pub updated_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CreateUserInput {
pub username: String,
pub email: String,
pub password: String,
pub invite_code: String,
pub role: Option<Role>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct LoginInput {
pub username: String,
pub password: String,
}
#[derive(Iden, PartialEq, Eq)]
pub enum Users {
Table,
Id,
Username,
Email,
PasswordHash,
InviteCodeId,
Role,
IsActivate,
CreatedAt,
@ -68,6 +55,7 @@ impl TryFrom<String> for Users {
"username" => Ok(Users::Username),
"email" => Ok(Users::Email),
"role" => Ok(Users::Role),
"invite_code_id" => Ok(Users::InviteCodeId),
"is_activate" => Ok(Users::IsActivate),
"created_at" => Ok(Users::CreatedAt),
"updated_at" => Ok(Users::UpdatedAt),
@ -76,13 +64,19 @@ impl TryFrom<String> for Users {
}
}
#[derive(sqlx::FromRow, Debug, SimpleObject)]
pub struct UserInfoRow {
pub id: Uuid,
pub username: String,
pub email: String,
pub role: Role,
pub is_activate: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
#[derive(Debug, Clone)]
pub struct UserInfoRespnose {
pub total_users: i64,
pub total_active_users: i64,
pub total_inactive_users: i64,
pub total_admin_users: i64,
pub total_user_users: i64,
pub users: Vec<UserWithGroups>,
}
/// 包含组信息的用户
#[derive(Debug, Clone)]
pub struct UserWithGroups {
pub user: User,
pub groups: Vec<String>,
}

View File

@ -1,3 +1,6 @@
use crate::graphql::types::PaginationInput;
use crate::models::{blog::*, PaginatedResult};
use crate::services::query_builder::DynamicQueryBuilder;
use anyhow::{anyhow, Result};
use chrono::Utc;
use sea_query::{extension::postgres::PgExpr, Expr, Iden, Order, PostgresQueryBuilder, Query};
@ -5,10 +8,6 @@ use sea_query_binder::SqlxBinder;
use sqlx::{FromRow, PgPool};
use uuid::Uuid;
use crate::models::blog::*;
use crate::models::page_block::{PaginatedResult, PaginationInput};
use crate::services::query_builder::DynamicQueryBuilder;
/// 博客相关表的列枚举
#[derive(Debug, Clone, Copy)]
pub enum BlogsIden {

View File

@ -198,6 +198,27 @@ impl CasbinService {
.collect())
}
pub async fn get_user_permissions(&self, user_id: &str) -> Result<Vec<(String, String)>> {
let enforcer = self.enforcer.read().await;
let roles = enforcer.get_roles_for_user(user_id, None);
use std::collections::HashSet;
let mut all_permissions = HashSet::new();
for role in roles {
let policies = enforcer.get_filtered_policy(0, vec![role.to_string()]);
for policy in policies {
if policy.len() >= 3 {
all_permissions.insert((policy[1].to_string(), policy[2].to_string()));
}
}
}
Ok(all_permissions.into_iter().collect())
}
/// 重新加载策略
pub async fn reload_policy(&self) -> Result<()> {
let mut enforcer = self.enforcer.write().await;

View File

@ -1,32 +1,89 @@
use crate::models::Setting;
use crate::services::settings_service::SettingsService;
use crate::models::config::Config;
use crate::services::config_service::ConfigsService;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
/// 配置管理器,提供类型安全的配置访问和缓存
pub struct SettingsManager {
settings_service: Arc<SettingsService>,
cache: Arc<RwLock<HashMap<String, Setting>>>,
pub mod keys {
// 应用配置
pub const SITE_NAME: &str = "site.name";
pub const SITE_DESCRIPTION: &str = "site.description";
pub const SITE_KEYWORDS: &str = "site.keywords";
pub const SITE_URL: &str = "site.url";
pub const SITE_LOGO: &str = "site.logo";
pub const SITE_COPYRIGHT: &str = "site.copyright";
pub const SITE_ICP: &str = "site.icp";
pub const SITE_ICP_URL: &str = "site.icp_url";
pub const SITE_COLOR_STYLE: &str = "site.color_style";
// User
pub const USER_DEFAULT_AVATAR: &str = "user.default_avatar";
pub const USER_DEFAULT_ROLE: &str = "user.default_role";
pub const USER_NEED_REGISTER_INVITE_CODE: &str = "user.register_invite_code";
pub const USER_NEED_REGISTER_EMAIL_VERIFICATION: &str = "user.register_email_verification";
pub const OPEN_LOGIN: &str = "user.open_login";
pub const OPEN_RESET_PASSWORD: &str = "user.open_reset_password";
// EMAIL
pub const EMAIL_SMTP_HOST: &str = "email.smtp_host";
pub const EMAIL_SMTP_PORT: &str = "email.smtp_port";
pub const EMAIL_SMTP_USER: &str = "email.smtp_user";
pub const EMAIL_SMTP_PASSWORD: &str = "email.smtp_password";
pub const EMAIL_SMTP_FROM: &str = "email.smtp_from";
pub const EMAIL_SMTP_FROM_NAME: &str = "email.smtp_from_name";
pub const EMAIL_SMTP_FROM_EMAIL: &str = "email.smtp_from_email";
pub const EMAIL_SYSTEM_TEMPLATE: &str = "email.system_template";
// Blog
pub const BLOG_DEFAULT_AUTHOR: &str = "blog.default_author";
pub const BLOG_DEFAULT_CATEGORY: &str = "blog.default_category";
pub const BLOG_DEFAULT_TAG: &str = "blog.default_tag";
pub const BLOG_OPEN_COMMENT: &str = "blog.open_comment";
// 日志配置
pub const LOGGING_LEVEL: &str = "logging.level";
pub const LOGGING_MAX_FILES: &str = "logging.max_files";
pub const LOGGING_MAX_FILE_SIZE: &str = "logging.max_file_size";
// 缓存配置
pub const CACHE_TTL: &str = "cache.ttl";
pub const CACHE_MAX_SIZE: &str = "cache.max_size";
// Switches
pub const SWITCH_OPEN_REGISTER: &str = "switch.open_register";
pub const SWITCH_OPEN_LOGIN: &str = "switch.open_login";
pub const SWITCH_OPEN_RESET_PASSWORD: &str = "switch.open_reset_password";
pub const SWITCH_OPEN_COMMENT: &str = "switch.open_comment";
pub const SWITCH_OPEN_LIKE: &str = "switch.open_like";
pub const SWITCH_OPEN_SHARE: &str = "switch.open_share";
pub const SWITCH_OPEN_VIEW: &str = "switch.open_view";
}
pub struct ConfigsManager {
configs_service: Arc<ConfigsService>,
cache: Arc<RwLock<HashMap<String, Config>>>,
cache_ttl: std::time::Duration,
last_cache_update: Arc<RwLock<std::time::Instant>>,
}
impl SettingsManager {
pub fn new(settings_service: SettingsService) -> Self {
Self {
settings_service: Arc::new(settings_service),
impl ConfigsManager {
pub async fn new(configs_service: ConfigsService) -> Self {
let manager = Self {
configs_service: Arc::new(configs_service),
cache: Arc::new(RwLock::new(HashMap::new())),
cache_ttl: std::time::Duration::from_secs(300), // 5分钟缓存
last_cache_update: Arc::new(RwLock::new(std::time::Instant::now())),
}
};
manager.refresh_cache().await.unwrap();
manager
}
/// 刷新缓存
async fn refresh_cache(&self) -> Result<()> {
let settings = self.settings_service.get_all_settings().await?;
let settings = self.configs_service.get_all_configs().await?;
let mut cache = self.cache.write().await;
cache.clear();
@ -47,7 +104,7 @@ impl SettingsManager {
}
/// 获取配置值(带缓存)
async fn get_cached_setting(&self, key: &str) -> Result<Option<Setting>> {
async fn get_cached_setting(&self, key: &str) -> Result<Option<Config>> {
if self.should_refresh_cache().await {
self.refresh_cache().await?;
}
@ -144,14 +201,15 @@ impl SettingsManager {
T: Serialize,
{
// 更新数据库
if let Some(setting) = self.settings_service.get_setting_by_key(key).await? {
let update_setting = crate::models::UpdateSetting {
if let Some(setting) = self.configs_service.get_config_by_key(key).await? {
let update_setting = crate::models::UpdateConfig {
key: key.to_string(),
value: Some(serde_json::to_string(value)?),
description: None,
category: None,
is_editable: None,
};
self.settings_service
self.configs_service
.update_setting(setting.id, update_setting, uuid::Uuid::nil())
.await?;
}
@ -165,7 +223,7 @@ impl SettingsManager {
/// 批量设置配置值
pub async fn set_values(&self, updates: HashMap<String, serde_json::Value>) -> Result<()> {
let updates: Vec<(String, serde_json::Value)> = updates.into_iter().collect();
self.settings_service
self.configs_service
.batch_update_settings(updates, uuid::Uuid::nil())
.await?;
@ -181,7 +239,7 @@ impl SettingsManager {
}
/// 获取所有配置
pub async fn get_all_settings(&self) -> Result<Vec<Setting>> {
pub async fn get_all_settings(&self) -> Result<Vec<Config>> {
if self.should_refresh_cache().await {
self.refresh_cache().await?;
}
@ -191,7 +249,7 @@ impl SettingsManager {
}
/// 获取分类配置
pub async fn get_settings_by_category(&self, category: &str) -> Result<Vec<Setting>> {
pub async fn get_settings_by_category(&self, category: &str) -> Result<Vec<Config>> {
let all_settings = self.get_all_settings().await?;
Ok(all_settings
.into_iter()
@ -205,106 +263,7 @@ impl SettingsManager {
}
/// 获取配置元数据
pub async fn get_setting_metadata(&self, key: &str) -> Result<Option<Setting>> {
pub async fn get_setting_metadata(&self, key: &str) -> Result<Option<Config>> {
self.get_cached_setting(key).await
}
}
/// 预定义的配置键常量
pub mod keys {
// 应用配置
pub const APP_NAME: &str = "app.name";
pub const APP_VERSION: &str = "app.version";
pub const APP_DEBUG: &str = "app.debug";
pub const APP_TIMEZONE: &str = "app.timezone";
// 数据库配置
pub const DB_MAX_CONNECTIONS: &str = "database.max_connections";
pub const DB_CONNECTION_TIMEOUT: &str = "database.connection_timeout";
// Kafka配置
pub const KAFKA_MAX_RETRIES: &str = "kafka.max_retries";
pub const KAFKA_RETRY_DELAY: &str = "kafka.retry_delay";
// 安全配置
pub const SECURITY_SESSION_TIMEOUT: &str = "security.session_timeout";
pub const SECURITY_MAX_LOGIN_ATTEMPTS: &str = "security.max_login_attempts";
// 日志配置
pub const LOGGING_LEVEL: &str = "logging.level";
pub const LOGGING_MAX_FILES: &str = "logging.max_files";
// 缓存配置
pub const CACHE_TTL: &str = "cache.ttl";
pub const CACHE_MAX_SIZE: &str = "cache.max_size";
}
/// 配置验证器
pub struct SettingsValidator;
impl SettingsValidator {
/// 验证必需的配置项
pub async fn validate_required_settings(manager: &SettingsManager) -> Result<Vec<String>> {
let required_keys = vec![
keys::APP_NAME,
keys::APP_VERSION,
keys::DB_MAX_CONNECTIONS,
keys::KAFKA_MAX_RETRIES,
];
let mut missing = Vec::new();
for key in required_keys {
if !manager.has_setting(key).await? {
missing.push(key.to_string());
}
}
Ok(missing)
}
/// 验证配置值类型
pub async fn validate_setting_types(manager: &SettingsManager) -> Result<Vec<String>> {
let mut errors = Vec::new();
// 验证数字类型配置
let number_keys = vec![
keys::DB_MAX_CONNECTIONS,
keys::DB_CONNECTION_TIMEOUT,
keys::KAFKA_MAX_RETRIES,
keys::KAFKA_RETRY_DELAY,
keys::SECURITY_SESSION_TIMEOUT,
keys::SECURITY_MAX_LOGIN_ATTEMPTS,
keys::LOGGING_MAX_FILES,
keys::CACHE_TTL,
keys::CACHE_MAX_SIZE,
];
for key in number_keys {
if let Some(setting) = manager.get_setting_metadata(key).await? {
if setting.value_type != "number" {
errors.push(format!(
"{} should be number type, got {}",
key, setting.value_type
));
}
}
}
// 验证布尔类型配置
let bool_keys = vec![keys::APP_DEBUG];
for key in bool_keys {
if let Some(setting) = manager.get_setting_metadata(key).await? {
if setting.value_type != "boolean" {
errors.push(format!(
"{} should be boolean type, got {}",
key, setting.value_type
));
}
}
}
Ok(errors)
}
}

View File

@ -1,4 +1,4 @@
use crate::models::{CreateSetting, Setting, SettingFilter, SettingHistory, UpdateSetting};
use crate::models::config::*;
use crate::services::query_builder::DynamicQueryBuilder;
use anyhow::{anyhow, Result};
use chrono::{DateTime, Utc};
@ -11,7 +11,7 @@ use std::collections::HashMap;
use uuid::Uuid;
#[derive(Iden, Clone)]
enum Settings {
enum Configs {
Table,
Id,
Key,
@ -29,10 +29,10 @@ enum Settings {
}
#[derive(Iden, Clone)]
enum SettingsHistory {
enum ConfigsHistory {
Table,
Id,
SettingId,
ConfigId,
OldValue,
NewValue,
ChangedBy,
@ -40,12 +40,12 @@ enum SettingsHistory {
CreatedAt,
}
pub struct SettingsService {
pub struct ConfigsService {
pool: PgPool,
query_builder: DynamicQueryBuilder,
}
impl SettingsService {
impl ConfigsService {
pub fn new(pool: PgPool) -> Self {
let query_builder = DynamicQueryBuilder::new(pool.clone());
Self {
@ -54,97 +54,13 @@ impl SettingsService {
}
}
/// 获取数据库连接池的引用(用于事务)
pub fn get_pool(&self) -> &PgPool {
&self.pool
}
/// 创建新的配置项
pub async fn create_setting(
&self,
create_setting: CreateSetting,
user_id: Uuid,
) -> Result<Setting> {
// 检查key是否已存在
let existing = sqlx::query!("SELECT id FROM settings WHERE key = $1", create_setting.key)
.fetch_optional(&self.pool)
.await?;
if existing.is_some() {
return Err(anyhow!(
"Setting with key '{}' already exists",
create_setting.key
));
}
let setting = sqlx::query_as!(
Setting,
r#"
INSERT INTO settings (key, value, value_type, description, category, is_encrypted, is_system, is_editable, created_by)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING id, key, value, value_type, description, category, is_encrypted, is_system, is_editable,
created_at as "created_at: DateTime<Utc>",
updated_at as "updated_at: DateTime<Utc>",
created_by, updated_by
"#,
create_setting.key,
create_setting.value,
create_setting.value_type,
create_setting.description,
create_setting.category,
create_setting.is_encrypted.unwrap_or(false),
create_setting.is_system.unwrap_or(false),
create_setting.is_editable.unwrap_or(true),
user_id
)
.fetch_one(&self.pool)
.await?;
Ok(setting)
}
/// 根据key获取配置项
pub async fn get_setting_by_key(&self, key: &str) -> Result<Option<Setting>> {
let setting = sqlx::query_as!(
Setting,
r#"
SELECT id, key, value, value_type, description, category, is_encrypted, is_system, is_editable,
created_at as "created_at: DateTime<Utc>",
updated_at as "updated_at: DateTime<Utc>",
created_by, updated_by
FROM settings WHERE key = $1
"#,
key
)
.fetch_optional(&self.pool)
.await?;
Ok(setting)
}
/// 根据ID获取配置项
pub async fn get_setting_by_id(&self, id: Uuid) -> Result<Option<Setting>> {
let setting = sqlx::query_as!(
Setting,
r#"
SELECT id, key, value, value_type, description, category, is_encrypted, is_system, is_editable,
created_at as "created_at: DateTime<Utc>",
updated_at as "updated_at: DateTime<Utc>",
created_by, updated_by
FROM settings WHERE id = $1
"#,
id
)
.fetch_optional(&self.pool)
.await?;
Ok(setting)
}
/// 获取所有配置项
pub async fn get_all_settings(&self) -> Result<Vec<Setting>> {
pub async fn get_all_configs(&self) -> Result<Vec<Config>> {
let settings = sqlx::query_as!(
Setting,
Config,
r#"
SELECT id, key, value, value_type, description, category, is_encrypted, is_system, is_editable,
created_at as "created_at: DateTime<Utc>",
@ -159,53 +75,52 @@ impl SettingsService {
Ok(settings)
}
/// 根据过滤条件获取配置项 (使用 sea-query 构建动态查询)
pub async fn get_settings_with_filter(&self, filter: &SettingFilter) -> Result<Vec<Setting>> {
pub async fn get_settings_with_filter(&self, filter: &ConfigFilter) -> Result<Vec<Config>> {
let mut query = Query::select();
query
.columns([
Settings::Id,
Settings::Key,
Settings::Value,
Settings::ValueType,
Settings::Description,
Settings::Category,
Settings::IsEncrypted,
Settings::IsSystem,
Settings::IsEditable,
Settings::CreatedAt,
Settings::UpdatedAt,
Settings::CreatedBy,
Settings::UpdatedBy,
Configs::Id,
Configs::Key,
Configs::Value,
Configs::ValueType,
Configs::Description,
Configs::Category,
Configs::IsEncrypted,
Configs::IsSystem,
Configs::IsEditable,
Configs::CreatedAt,
Configs::UpdatedAt,
Configs::CreatedBy,
Configs::UpdatedBy,
])
.from(Settings::Table);
.from(Configs::Table);
// 动态添加过滤条件
if let Some(category) = &filter.category {
query.and_where(Expr::col(Settings::Category).eq(category));
query.and_where(Expr::col(Configs::Category).eq(category));
}
if let Some(is_system) = &filter.is_system {
query.and_where(Expr::col(Settings::IsSystem).eq(*is_system));
query.and_where(Expr::col(Configs::IsSystem).eq(*is_system));
}
if let Some(is_editable) = &filter.is_editable {
query.and_where(Expr::col(Settings::IsEditable).eq(*is_editable));
query.and_where(Expr::col(Configs::IsEditable).eq(*is_editable));
}
if let Some(search) = &filter.search {
let search_pattern = format!("%{}%", search);
query.and_where(
Expr::col(Settings::Key)
Expr::col(Configs::Key)
.ilike(&search_pattern)
.or(Expr::col(Settings::Description).ilike(&search_pattern)),
.or(Expr::col(Configs::Description).ilike(&search_pattern)),
);
}
// 添加排序
query
.order_by(Settings::Category, sea_query::Order::Asc)
.order_by(Settings::Key, sea_query::Order::Asc);
.order_by(Configs::Category, sea_query::Order::Asc)
.order_by(Configs::Key, sea_query::Order::Asc);
// 构建并执行查询
let (sql, values) = query.build_sqlx(PostgresQueryBuilder);
@ -213,7 +128,7 @@ impl SettingsService {
let mut settings = Vec::new();
for row in rows {
let setting = Setting {
let setting = Config {
id: row.get("id"),
key: row.get("key"),
value: row.get("value"),
@ -238,18 +153,18 @@ impl SettingsService {
pub async fn update_setting(
&self,
id: Uuid,
update_setting: UpdateSetting,
update_setting: UpdateConfig,
user_id: Uuid,
) -> Result<Setting> {
let setting = self.get_setting_by_id(id).await?;
let setting = setting.ok_or_else(|| anyhow!("Setting not found"))?;
) -> Result<Config> {
let setting = self.get_config_by_id(id).await?;
let setting = setting.ok_or_else(|| anyhow!("Config not found"))?;
if !setting.is_editable.unwrap_or(false) {
return Err(anyhow!("Setting is not editable"));
return Err(anyhow!("Config is not editable"));
}
let updated_setting = sqlx::query_as!(
Setting,
Config,
r#"
UPDATE settings
SET value = COALESCE($1, value),
@ -277,39 +192,58 @@ impl SettingsService {
Ok(updated_setting)
}
/// 删除配置项
pub async fn delete_setting(&self, id: Uuid) -> Result<bool> {
let setting = self.get_setting_by_id(id).await?;
let setting = setting.ok_or_else(|| anyhow!("Setting not found"))?;
if setting.is_system.unwrap_or(false) {
return Err(anyhow!("Cannot delete system settings"));
}
let result = sqlx::query!("DELETE FROM settings WHERE id = $1", id)
.execute(&self.pool)
pub async fn get_config_by_id(&self, id: Uuid) -> Result<Option<Config>> {
let config = sqlx::query_as!(
Config,
r#"
SELECT id, key, value, value_type, description, category, is_encrypted, is_system, is_editable,
created_at as "created_at: DateTime<Utc>",
updated_at as "updated_at: DateTime<Utc>",
created_by, updated_by
FROM settings WHERE id = $1
"#,
id
)
.fetch_optional(&self.pool)
.await?;
Ok(result.rows_affected() > 0)
Ok(config)
}
pub async fn get_config_by_key(&self, key: &str) -> Result<Option<Config>> {
let config = sqlx::query_as!(
Config,
r#"
SELECT id, key, value, value_type, description, category, is_encrypted, is_system, is_editable,
created_at as "created_at: DateTime<Utc>",
updated_at as "updated_at: DateTime<Utc>",
created_by, updated_by
FROM settings WHERE key = $1
"#,
key
)
.fetch_optional(&self.pool)
.await?;
Ok(config)
}
/// 批量更新配置项
pub async fn batch_update_settings(
&self,
updates: Vec<(String, Value)>,
user_id: Uuid,
) -> Result<Vec<Setting>> {
) -> Result<Vec<Config>> {
let mut updated_settings = Vec::new();
for (key, value) in updates {
if let Some(setting) = self.get_setting_by_key(&key).await? {
if let Some(setting) = self.get_config_by_key(&key).await? {
if !setting.is_editable.unwrap_or(false) {
continue; // 跳过不可编辑的配置
}
let value_str = serde_json::to_string(&value)?;
let updated_setting = sqlx::query_as!(
Setting,
Config,
r#"
UPDATE settings
SET value = $1, updated_by = $2, updated_at = CURRENT_TIMESTAMP
@ -334,9 +268,9 @@ impl SettingsService {
}
/// 获取配置历史
pub async fn get_setting_history(&self, setting_id: Uuid) -> Result<Vec<SettingHistory>> {
pub async fn get_setting_history(&self, setting_id: Uuid) -> Result<Vec<ConfigHistory>> {
let history = sqlx::query_as!(
SettingHistory,
ConfigHistory,
r#"
SELECT id, setting_id, old_value, new_value, changed_by, change_reason,
created_at as "created_at: DateTime<Utc>"
@ -401,42 +335,42 @@ impl SettingsService {
}
/// 重置配置到默认值
pub async fn reset_to_defaults(&self, user_id: Uuid) -> Result<Vec<Setting>> {
pub async fn reset_to_defaults(&self, user_id: Uuid) -> Result<Vec<Config>> {
// 这里可以实现重置逻辑,比如从配置文件重新加载默认值
// 暂时返回空列表
Ok(Vec::new())
}
/// 导出配置 (使用 sea-query 构建动态查询)
pub async fn export_settings(&self, category: Option<&str>) -> Result<Vec<Setting>> {
pub async fn export_settings(&self, category: Option<&str>) -> Result<Vec<Config>> {
let mut query = Query::select();
query
.columns([
Settings::Id,
Settings::Key,
Settings::Value,
Settings::ValueType,
Settings::Description,
Settings::Category,
Settings::IsEncrypted,
Settings::IsSystem,
Settings::IsEditable,
Settings::CreatedAt,
Settings::UpdatedAt,
Settings::CreatedBy,
Settings::UpdatedBy,
Configs::Id,
Configs::Key,
Configs::Value,
Configs::ValueType,
Configs::Description,
Configs::Category,
Configs::IsEncrypted,
Configs::IsSystem,
Configs::IsEditable,
Configs::CreatedAt,
Configs::UpdatedAt,
Configs::CreatedBy,
Configs::UpdatedBy,
])
.from(Settings::Table);
.from(Configs::Table);
// 根据分类过滤
if let Some(cat) = category {
query.and_where(Expr::col(Settings::Category).eq(cat));
query.and_where(Expr::col(Configs::Category).eq(cat));
}
// 添加排序
query
.order_by(Settings::Category, sea_query::Order::Asc)
.order_by(Settings::Key, sea_query::Order::Asc);
.order_by(Configs::Category, sea_query::Order::Asc)
.order_by(Configs::Key, sea_query::Order::Asc);
// 构建并执行查询
let (sql, values) = query.build_sqlx(PostgresQueryBuilder);
@ -444,7 +378,7 @@ impl SettingsService {
let mut settings = Vec::new();
for row in rows {
let setting = Setting {
let setting = Config {
id: row.get("id"),
key: row.get("key"),
value: row.get("value"),
@ -465,54 +399,54 @@ impl SettingsService {
Ok(settings)
}
/// 导入配置
pub async fn import_settings(
&self,
settings: Vec<CreateSetting>,
user_id: Uuid,
) -> Result<Vec<Setting>> {
let mut imported_settings = Vec::new();
// /// 导入配置
// pub async fn import_settings(
// &self,
// settings: Vec<CreateConfig>,
// user_id: Uuid,
// ) -> Result<Vec<Config>> {
// let mut imported_settings = Vec::new();
for create_setting in settings {
// 检查是否已存在
if let Some(existing) = self.get_setting_by_key(&create_setting.key).await? {
// 如果存在且可编辑,则更新
if existing.is_editable.unwrap_or(false) {
let update_setting = UpdateSetting {
value: create_setting.value,
description: create_setting.description,
category: Some(create_setting.category),
is_editable: Some(create_setting.is_editable.unwrap_or(true)),
};
let updated = self
.update_setting(existing.id, update_setting, user_id)
.await?;
imported_settings.push(updated);
}
} else {
// 如果不存在,则创建
let new_setting = self.create_setting(create_setting, user_id).await?;
imported_settings.push(new_setting);
}
}
// for create_setting in settings {
// // 检查是否已存在
// if let Some(existing) = self.get_setting_by_key(&create_setting.key).await? {
// // 如果存在且可编辑,则更新
// if existing.is_editable.unwrap_or(false) {
// let update_setting = UpdateConfig {
// value: create_setting.value,
// description: create_setting.description,
// category: Some(create_setting.category),
// is_editable: Some(create_setting.is_editable.unwrap_or(true)),
// };
// let updated = self
// .update_setting(existing.id, update_setting, user_id)
// .await?;
// imported_settings.push(updated);
// }
// } else {
// // 如果不存在,则创建
// let new_setting = self.create_setting(create_setting, user_id).await?;
// imported_settings.push(new_setting);
// }
// }
Ok(imported_settings)
}
// Ok(imported_settings)
// }
/// 分页查询配置项 (sea-query 分页示例)
pub async fn get_settings_paginated(
&self,
filter: &SettingFilter,
filter: &ConfigFilter,
page: u64,
page_size: u64,
) -> Result<(Vec<Setting>, u64)> {
) -> Result<(Vec<Config>, u64)> {
let offset = (page - 1) * page_size;
// 查询总数
let mut count_query = Query::select();
count_query
.expr(Expr::col(Settings::Id).count())
.from(Settings::Table);
.expr(Expr::col(Configs::Id).count())
.from(Configs::Table);
// 应用过滤条件
self.apply_filter_conditions(&mut count_query, filter);
@ -526,27 +460,27 @@ impl SettingsService {
let mut data_query = Query::select();
data_query
.columns([
Settings::Id,
Settings::Key,
Settings::Value,
Settings::ValueType,
Settings::Description,
Settings::Category,
Settings::IsEncrypted,
Settings::IsSystem,
Settings::IsEditable,
Settings::CreatedAt,
Settings::UpdatedAt,
Settings::CreatedBy,
Settings::UpdatedBy,
Configs::Id,
Configs::Key,
Configs::Value,
Configs::ValueType,
Configs::Description,
Configs::Category,
Configs::IsEncrypted,
Configs::IsSystem,
Configs::IsEditable,
Configs::CreatedAt,
Configs::UpdatedAt,
Configs::CreatedBy,
Configs::UpdatedBy,
])
.from(Settings::Table);
.from(Configs::Table);
self.apply_filter_conditions(&mut data_query, filter);
data_query
.order_by(Settings::Category, sea_query::Order::Asc)
.order_by(Settings::Key, sea_query::Order::Asc)
.order_by(Configs::Category, sea_query::Order::Asc)
.order_by(Configs::Key, sea_query::Order::Asc)
.limit(page_size)
.offset(offset);
@ -555,7 +489,7 @@ impl SettingsService {
let mut settings = Vec::new();
for row in rows {
let setting = Setting {
let setting = Config {
id: row.get("id"),
key: row.get("key"),
value: row.get("value"),
@ -584,75 +518,75 @@ impl SettingsService {
value_types: Option<Vec<String>>,
is_system: Option<bool>,
date_range: Option<(chrono::DateTime<chrono::Utc>, chrono::DateTime<chrono::Utc>)>,
) -> Result<Vec<Setting>> {
) -> Result<Vec<Config>> {
let mut query = Query::select();
query
.columns([
Settings::Id,
Settings::Key,
Settings::Value,
Settings::ValueType,
Settings::Description,
Settings::Category,
Settings::IsEncrypted,
Settings::IsSystem,
Settings::IsEditable,
Settings::CreatedAt,
Settings::UpdatedAt,
Settings::CreatedBy,
Settings::UpdatedBy,
Configs::Id,
Configs::Key,
Configs::Value,
Configs::ValueType,
Configs::Description,
Configs::Category,
Configs::IsEncrypted,
Configs::IsSystem,
Configs::IsEditable,
Configs::CreatedAt,
Configs::UpdatedAt,
Configs::CreatedBy,
Configs::UpdatedBy,
])
.from(Settings::Table);
.from(Configs::Table);
// 搜索关键词
if let Some(term) = search_term {
let search_pattern = format!("%{}%", term);
query.and_where(
Expr::col(Settings::Key)
Expr::col(Configs::Key)
.ilike(&search_pattern)
.or(Expr::col(Settings::Description).ilike(&search_pattern))
.or(Expr::col(Settings::Value).ilike(&search_pattern)),
.or(Expr::col(Configs::Description).ilike(&search_pattern))
.or(Expr::col(Configs::Value).ilike(&search_pattern)),
);
}
// 多个分类
if let Some(cats) = categories {
if !cats.is_empty() {
query.and_where(Expr::col(Settings::Category).is_in(cats));
query.and_where(Expr::col(Configs::Category).is_in(cats));
}
}
// 多个值类型
if let Some(types) = value_types {
if !types.is_empty() {
query.and_where(Expr::col(Settings::ValueType).is_in(types));
query.and_where(Expr::col(Configs::ValueType).is_in(types));
}
}
// 系统设置
if let Some(sys) = is_system {
query.and_where(Expr::col(Settings::IsSystem).eq(sys));
query.and_where(Expr::col(Configs::IsSystem).eq(sys));
}
// 日期范围
if let Some((start_date, end_date)) = date_range {
query.and_where(
Expr::col(Settings::CreatedAt)
Expr::col(Configs::CreatedAt)
.gte(start_date)
.and(Expr::col(Settings::CreatedAt).lte(end_date)),
.and(Expr::col(Configs::CreatedAt).lte(end_date)),
);
}
query
.order_by(Settings::Category, sea_query::Order::Asc)
.order_by(Settings::Key, sea_query::Order::Asc);
.order_by(Configs::Category, sea_query::Order::Asc)
.order_by(Configs::Key, sea_query::Order::Asc);
let (sql, values) = query.build_sqlx(PostgresQueryBuilder);
let rows = sqlx::query_with(&sql, values).fetch_all(&self.pool).await?;
let mut settings = Vec::new();
for row in rows {
let setting = Setting {
let setting = Config {
id: row.get("id"),
key: row.get("key"),
value: row.get("value"),
@ -676,37 +610,37 @@ impl SettingsService {
/// 批量操作 (动态更新多个配置项)
pub async fn batch_update_by_conditions(
&self,
conditions: &SettingFilter,
conditions: &ConfigFilter,
updates: HashMap<String, serde_json::Value>,
user_id: Uuid,
) -> Result<u64> {
// 构建更新查询
let mut update_query = Query::update();
update_query.table(Settings::Table);
update_query.table(Configs::Table);
// 添加更新字段
for (field, value) in updates {
match field.as_str() {
"description" => {
if let Some(desc) = value.as_str() {
update_query.value(Settings::Description, desc);
update_query.value(Configs::Description, desc);
}
}
"category" => {
if let Some(cat) = value.as_str() {
update_query.value(Settings::Category, cat);
update_query.value(Configs::Category, cat);
}
}
"is_editable" => {
if let Some(editable) = value.as_bool() {
update_query.value(Settings::IsEditable, editable);
update_query.value(Configs::IsEditable, editable);
}
}
_ => {} // 忽略未知字段
}
}
update_query.value(Settings::UpdatedBy, user_id);
update_query.value(Configs::UpdatedBy, user_id);
// 应用过滤条件
self.apply_filter_conditions(&mut update_query, conditions);
@ -718,67 +652,67 @@ impl SettingsService {
}
/// 辅助方法:应用过滤条件到查询
fn apply_filter_conditions<T>(&self, query: &mut T, filter: &SettingFilter)
fn apply_filter_conditions<T>(&self, query: &mut T, filter: &ConfigFilter)
where
T: sea_query::QueryStatementWriter + sea_query::ConditionalStatement,
{
if let Some(category) = &filter.category {
query.and_where(Expr::col(Settings::Category).eq(category));
query.and_where(Expr::col(Configs::Category).eq(category));
}
if let Some(is_system) = &filter.is_system {
query.and_where(Expr::col(Settings::IsSystem).eq(*is_system));
query.and_where(Expr::col(Configs::IsSystem).eq(*is_system));
}
if let Some(is_editable) = &filter.is_editable {
query.and_where(Expr::col(Settings::IsEditable).eq(*is_editable));
query.and_where(Expr::col(Configs::IsEditable).eq(*is_editable));
}
if let Some(search) = &filter.search {
let search_pattern = format!("%{}%", search);
query.and_where(
Expr::col(Settings::Key)
Expr::col(Configs::Key)
.ilike(&search_pattern)
.or(Expr::col(Settings::Description).ilike(&search_pattern)),
.or(Expr::col(Configs::Description).ilike(&search_pattern)),
);
}
}
/// 使用查询构建器的简化过滤查询示例
pub async fn get_settings_with_builder(&self, filter: &SettingFilter) -> Result<Vec<Setting>> {
pub async fn get_settings_with_builder(&self, filter: &ConfigFilter) -> Result<Vec<Config>> {
let rows = self
.query_builder
.select(Settings::Table)
.select(Configs::Table)
.columns([
Settings::Id,
Settings::Key,
Settings::Value,
Settings::ValueType,
Settings::Description,
Settings::Category,
Settings::IsEncrypted,
Settings::IsSystem,
Settings::IsEditable,
Settings::CreatedAt,
Settings::UpdatedAt,
Settings::CreatedBy,
Settings::UpdatedBy,
Configs::Id,
Configs::Key,
Configs::Value,
Configs::ValueType,
Configs::Description,
Configs::Category,
Configs::IsEncrypted,
Configs::IsSystem,
Configs::IsEditable,
Configs::CreatedAt,
Configs::UpdatedAt,
Configs::CreatedBy,
Configs::UpdatedBy,
])
.condition_option(Settings::Category, filter.category.clone())
.condition_option(Settings::IsSystem, filter.is_system)
.condition_option(Settings::IsEditable, filter.is_editable)
.condition_option(Configs::Category, filter.category.clone())
.condition_option(Configs::IsSystem, filter.is_system)
.condition_option(Configs::IsEditable, filter.is_editable)
.search_like(
vec![Settings::Key, Settings::Description],
vec![Configs::Key, Configs::Description],
filter.search.as_deref(),
)
.order_by(Settings::Category, Order::Asc)
.order_by(Settings::Key, Order::Asc)
.order_by(Configs::Category, Order::Asc)
.order_by(Configs::Key, Order::Asc)
.fetch_all()
.await?;
let mut settings = Vec::new();
for row in rows {
let setting = Setting {
let setting = Config {
id: row.get("id"),
key: row.get("key"),
value: row.get("value"),
@ -802,19 +736,19 @@ impl SettingsService {
/// 使用查询构建器的分页查询示例
pub async fn get_settings_paginated_with_builder(
&self,
filter: &SettingFilter,
filter: &ConfigFilter,
page: u64,
page_size: u64,
) -> Result<(Vec<Setting>, i64)> {
) -> Result<(Vec<Config>, i64)> {
// 获取总数
let total_count = self
.query_builder
.select(Settings::Table)
.condition_option(Settings::Category, filter.category.clone())
.condition_option(Settings::IsSystem, filter.is_system)
.condition_option(Settings::IsEditable, filter.is_editable)
.select(Configs::Table)
.condition_option(Configs::Category, filter.category.clone())
.condition_option(Configs::IsSystem, filter.is_system)
.condition_option(Configs::IsEditable, filter.is_editable)
.search_like(
vec![Settings::Key, Settings::Description],
vec![Configs::Key, Configs::Description],
filter.search.as_deref(),
)
.count()
@ -823,38 +757,38 @@ impl SettingsService {
// 获取分页数据
let rows = self
.query_builder
.select(Settings::Table)
.select(Configs::Table)
.columns([
Settings::Id,
Settings::Key,
Settings::Value,
Settings::ValueType,
Settings::Description,
Settings::Category,
Settings::IsEncrypted,
Settings::IsSystem,
Settings::IsEditable,
Settings::CreatedAt,
Settings::UpdatedAt,
Settings::CreatedBy,
Settings::UpdatedBy,
Configs::Id,
Configs::Key,
Configs::Value,
Configs::ValueType,
Configs::Description,
Configs::Category,
Configs::IsEncrypted,
Configs::IsSystem,
Configs::IsEditable,
Configs::CreatedAt,
Configs::UpdatedAt,
Configs::CreatedBy,
Configs::UpdatedBy,
])
.condition_option(Settings::Category, filter.category.clone())
.condition_option(Settings::IsSystem, filter.is_system)
.condition_option(Settings::IsEditable, filter.is_editable)
.condition_option(Configs::Category, filter.category.clone())
.condition_option(Configs::IsSystem, filter.is_system)
.condition_option(Configs::IsEditable, filter.is_editable)
.search_like(
vec![Settings::Key, Settings::Description],
vec![Configs::Key, Configs::Description],
filter.search.as_deref(),
)
.order_by(Settings::Category, Order::Asc)
.order_by(Settings::Key, Order::Asc)
.order_by(Configs::Category, Order::Asc)
.order_by(Configs::Key, Order::Asc)
.paginate(page, page_size)
.fetch_all()
.await?;
let mut settings = Vec::new();
for row in rows {
let setting = Setting {
let setting = Config {
id: row.get("id"),
key: row.get("key"),
value: row.get("value"),

View File

@ -1,10 +1,9 @@
pub mod blog_service;
pub mod casbin_service;
pub mod config_manager;
pub mod config_service;
pub mod invite_code_service;
pub mod mosaic_service;
pub mod page_block_service;
pub mod query_builder;
pub mod settings_manager;
pub mod settings_service;
pub mod system_config_service;
pub mod user_service;

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,7 @@
use argon2::password_hash::{rand_core::OsRng, SaltString};
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
use argon2::{
password_hash::{rand_core::OsRng, SaltString},
Argon2, PasswordHash, PasswordHasher, PasswordVerifier,
};
use async_graphql::{Error, Result};
use sea_query::{Expr, PostgresQueryBuilder};
use sea_query_binder::SqlxBinder;
@ -8,10 +10,9 @@ use tracing::info;
use uuid::Uuid;
use crate::auth::Claims;
use crate::graphql::types::{
CreateUserInput, LoginInput, LoginResponse, RegisterInput, UserInfoRespnose,
};
use crate::models::user::{Role, User};
use crate::graphql::types::users::LoginResponse;
use crate::models::user::*;
use crate::services::casbin_service::CasbinService;
use crate::services::invite_code_service::InviteCodeService;
use crate::services::system_config_service::SystemConfigService;
@ -19,16 +20,51 @@ use crate::services::system_config_service::SystemConfigService;
pub struct UserService {
pool: PgPool,
jwt_secret: String,
casbin_service: Option<CasbinService>,
}
impl UserService {
pub fn new(pool: PgPool, jwt_secret: String) -> Self {
Self { pool, jwt_secret }
Self {
pool,
jwt_secret,
casbin_service: None,
}
}
pub fn with_casbin(mut self, casbin_service: CasbinService) -> Self {
self.casbin_service = Some(casbin_service);
self
}
/// 获取用户的组(角色)
pub async fn get_user_groups(&self, user_id: &Uuid) -> Result<Vec<String>> {
if let Some(casbin_service) = &self.casbin_service {
casbin_service
.get_user_roles(&user_id.to_string())
.await
.map_err(|e| Error::new(format!("Failed to get user groups: {}", e)))
} else {
Err(Error::new("Casbin service not initialized"))
}
}
/// 获取用户信息(包含组信息)
pub async fn get_user_with_groups(&self, user_id: Uuid) -> Result<Option<UserWithGroups>> {
let user = self.get_user_by_id(user_id).await?;
if let Some(user) = user {
let groups = self.get_user_groups(&user.id).await?;
Ok(Some(UserWithGroups { user, groups }))
} else {
Ok(None)
}
}
pub async fn register(&self, input: RegisterInput) -> Result<User> {
let password_hash = self.hash_password(&input.password)?;
let role = input.role.unwrap_or(Role::User);
let role = Role::User;
// Use transaction to ensure data consistency
let mut tx = self
@ -113,7 +149,7 @@ impl UserService {
pub async fn create_user(&self, input: CreateUserInput) -> Result<User> {
let password_hash = self.hash_password(&input.password)?;
let role = input.role.unwrap_or(Role::User);
let role = Role::User;
// Create user first
let user = sqlx::query_as!(
@ -160,7 +196,10 @@ impl UserService {
.map_err(|e| Error::new(format!("Database error: {}", e)))?
.ok_or_else(|| Error::new("Invalid credentials"))?;
if !self.verify_password(&input.password, &user.password_hash)? {
if !self.verify_password(
&input.password,
&user.password_hash.as_ref().as_ref().unwrap(),
)? {
return Err(Error::new("Invalid credentials"));
}
@ -197,7 +236,7 @@ impl UserService {
sort_by: String,
sort_order: String,
filter: Option<String>,
) -> Result<Vec<crate::models::user::UserInfoRow>> {
) -> Result<Vec<User>> {
use crate::models::user::Users;
use sea_query::{Expr, Order, Query};
let sort_by = Users::try_from(sort_by)?;
@ -213,6 +252,8 @@ impl UserService {
Users::Username,
Users::Email,
Users::Role,
Users::PasswordHash,
Users::InviteCodeId,
Users::IsActivate,
Users::CreatedAt,
Users::UpdatedAt,
@ -224,10 +265,7 @@ impl UserService {
.offset(offset)
.build_sqlx(PostgresQueryBuilder);
info!("sql: {:?}", sql);
info!("values: {:?}", values);
let users = sqlx::query_as_with::<_, crate::models::user::UserInfoRow, _>(&sql, values)
let users = sqlx::query_as_with::<_, crate::models::user::User, _>(&sql, values)
.fetch_all(&self.pool)
.await
.map_err(|e| Error::new(format!("Database error: {}", e)))?;
@ -235,6 +273,27 @@ impl UserService {
Ok(users)
}
pub async fn get_all_users_with_groups(
&self,
offset: u64,
limit: u64,
sort_by: String,
sort_order: String,
filter: Option<String>,
) -> Result<Vec<UserWithGroups>> {
let users = self
.get_all_users(offset, limit, sort_by, sort_order, filter)
.await?;
let mut users_with_groups = Vec::new();
for user in users {
let groups = self.get_user_groups(&user.id).await.unwrap_or_default();
users_with_groups.push(UserWithGroups { user, groups });
}
Ok(users_with_groups)
}
pub async fn initialize_admin(
&self,
username: String,
@ -352,9 +411,8 @@ impl UserService {
.await
.map_err(|e| Error::new(format!("Database error: {}", e)))?;
// 并行获取用户列表
let users = self
.get_all_users(offset, limit, sort_by, sort_order, filter)
let users_with_groups = self
.get_all_users_with_groups(offset, limit, sort_by, sort_order, filter)
.await?;
Ok(UserInfoRespnose {
@ -363,7 +421,7 @@ impl UserService {
total_inactive_users: stats.total_inactive_users.unwrap_or(0),
total_admin_users: stats.total_admin_users.unwrap_or(0),
total_user_users: stats.total_user_users.unwrap_or(0),
users,
users: users_with_groups,
})
}
}