This commit is contained in:
parent
3e90e2fa9f
commit
d4448c6129
3
.env
3
.env
@ -1,4 +1,5 @@
|
|||||||
DATABASE_URL=postgresql://mmap:yjhcfzXWrzslzl1331@8.217.64.157:5433/mmap
|
# DATABASE_URL=postgresql://mmap:yjhcfzXWrzslzl1331@8.217.64.157:5433/mmap
|
||||||
|
DATABASE_URL=postgresql://mmap:yjhcfzXWrzslzl1331@101.200.43.172:5433/mmap
|
||||||
JWT_SECRET="JvGpWgGWLHAhvhxN7BuOVtUWfMXm6xAqjClaTwOcAnI="
|
JWT_SECRET="JvGpWgGWLHAhvhxN7BuOVtUWfMXm6xAqjClaTwOcAnI="
|
||||||
RUST_LOG=debug
|
RUST_LOG=debug
|
||||||
PORT=3050
|
PORT=3050
|
||||||
|
|||||||
58
.sqlx/query-035b45ec1dafdf984b6e4a0e935a5820278730cfe3253144cb73322fc7e9bff5.json
generated
Normal file
58
.sqlx/query-035b45ec1dafdf984b6e4a0e935a5820278730cfe3253144cb73322fc7e9bff5.json
generated
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n SELECT id, setting_id, old_value, new_value, changed_by, change_reason, \n created_at as \"created_at: DateTime<Utc>\"\n FROM settings_history \n WHERE setting_id = $1 \n ORDER BY created_at DESC\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "id",
|
||||||
|
"type_info": "Uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"name": "setting_id",
|
||||||
|
"type_info": "Uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 2,
|
||||||
|
"name": "old_value",
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 3,
|
||||||
|
"name": "new_value",
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 4,
|
||||||
|
"name": "changed_by",
|
||||||
|
"type_info": "Uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 5,
|
||||||
|
"name": "change_reason",
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 6,
|
||||||
|
"name": "created_at: DateTime<Utc>",
|
||||||
|
"type_info": "Timestamp"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Uuid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "035b45ec1dafdf984b6e4a0e935a5820278730cfe3253144cb73322fc7e9bff5"
|
||||||
|
}
|
||||||
94
.sqlx/query-19c23596ea8098fdf9a13f4a4f033e646282fc1df35afa27dd2b1b91dec607cf.json
generated
Normal file
94
.sqlx/query-19c23596ea8098fdf9a13f4a4f033e646282fc1df35afa27dd2b1b91dec607cf.json
generated
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n SELECT id, key, value, value_type, description, category, is_encrypted, is_system, is_editable, \n created_at as \"created_at: DateTime<Utc>\", \n updated_at as \"updated_at: DateTime<Utc>\", \n created_by, updated_by\n FROM settings WHERE id = $1\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "id",
|
||||||
|
"type_info": "Uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"name": "key",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 2,
|
||||||
|
"name": "value",
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 3,
|
||||||
|
"name": "value_type",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 4,
|
||||||
|
"name": "description",
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 5,
|
||||||
|
"name": "category",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 6,
|
||||||
|
"name": "is_encrypted",
|
||||||
|
"type_info": "Bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 7,
|
||||||
|
"name": "is_system",
|
||||||
|
"type_info": "Bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 8,
|
||||||
|
"name": "is_editable",
|
||||||
|
"type_info": "Bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 9,
|
||||||
|
"name": "created_at: DateTime<Utc>",
|
||||||
|
"type_info": "Timestamp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 10,
|
||||||
|
"name": "updated_at: DateTime<Utc>",
|
||||||
|
"type_info": "Timestamp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 11,
|
||||||
|
"name": "created_by",
|
||||||
|
"type_info": "Uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 12,
|
||||||
|
"name": "updated_by",
|
||||||
|
"type_info": "Uuid"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Uuid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "19c23596ea8098fdf9a13f4a4f033e646282fc1df35afa27dd2b1b91dec607cf"
|
||||||
|
}
|
||||||
94
.sqlx/query-337689745b15c94365b9ab55c4298abfd212f5165628caf6d3549c321bdd8584.json
generated
Normal file
94
.sqlx/query-337689745b15c94365b9ab55c4298abfd212f5165628caf6d3549c321bdd8584.json
generated
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n SELECT id, key, value, value_type, description, category, is_encrypted, is_system, is_editable, \n created_at as \"created_at: DateTime<Utc>\", \n updated_at as \"updated_at: DateTime<Utc>\", \n created_by, updated_by\n FROM settings WHERE key = $1\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "id",
|
||||||
|
"type_info": "Uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"name": "key",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 2,
|
||||||
|
"name": "value",
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 3,
|
||||||
|
"name": "value_type",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 4,
|
||||||
|
"name": "description",
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 5,
|
||||||
|
"name": "category",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 6,
|
||||||
|
"name": "is_encrypted",
|
||||||
|
"type_info": "Bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 7,
|
||||||
|
"name": "is_system",
|
||||||
|
"type_info": "Bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 8,
|
||||||
|
"name": "is_editable",
|
||||||
|
"type_info": "Bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 9,
|
||||||
|
"name": "created_at: DateTime<Utc>",
|
||||||
|
"type_info": "Timestamp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 10,
|
||||||
|
"name": "updated_at: DateTime<Utc>",
|
||||||
|
"type_info": "Timestamp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 11,
|
||||||
|
"name": "created_by",
|
||||||
|
"type_info": "Uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 12,
|
||||||
|
"name": "updated_by",
|
||||||
|
"type_info": "Uuid"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Text"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "337689745b15c94365b9ab55c4298abfd212f5165628caf6d3549c321bdd8584"
|
||||||
|
}
|
||||||
14
.sqlx/query-431c4c6620a596862ed42ca0e402b093bd4c1d13f14f906d00b2b37c832f48ca.json
generated
Normal file
14
.sqlx/query-431c4c6620a596862ed42ca0e402b093bd4c1d13f14f906d00b2b37c832f48ca.json
generated
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "DELETE FROM settings WHERE id = $1",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Uuid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "431c4c6620a596862ed42ca0e402b093bd4c1d13f14f906d00b2b37c832f48ca"
|
||||||
|
}
|
||||||
102
.sqlx/query-6bab42214f2c28a3985afa5ce4bbb5377b96d7b28490db0be5d775a67a8cfcdb.json
generated
Normal file
102
.sqlx/query-6bab42214f2c28a3985afa5ce4bbb5377b96d7b28490db0be5d775a67a8cfcdb.json
generated
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n INSERT INTO settings (key, value, value_type, description, category, is_encrypted, is_system, is_editable, created_by)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n RETURNING id, key, value, value_type, description, category, is_encrypted, is_system, is_editable, \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": "key",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 2,
|
||||||
|
"name": "value",
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 3,
|
||||||
|
"name": "value_type",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 4,
|
||||||
|
"name": "description",
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 5,
|
||||||
|
"name": "category",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 6,
|
||||||
|
"name": "is_encrypted",
|
||||||
|
"type_info": "Bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 7,
|
||||||
|
"name": "is_system",
|
||||||
|
"type_info": "Bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 8,
|
||||||
|
"name": "is_editable",
|
||||||
|
"type_info": "Bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 9,
|
||||||
|
"name": "created_at: DateTime<Utc>",
|
||||||
|
"type_info": "Timestamp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 10,
|
||||||
|
"name": "updated_at: DateTime<Utc>",
|
||||||
|
"type_info": "Timestamp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 11,
|
||||||
|
"name": "created_by",
|
||||||
|
"type_info": "Uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 12,
|
||||||
|
"name": "updated_by",
|
||||||
|
"type_info": "Uuid"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Varchar",
|
||||||
|
"Text",
|
||||||
|
"Varchar",
|
||||||
|
"Text",
|
||||||
|
"Varchar",
|
||||||
|
"Bool",
|
||||||
|
"Bool",
|
||||||
|
"Bool",
|
||||||
|
"Uuid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "6bab42214f2c28a3985afa5ce4bbb5377b96d7b28490db0be5d775a67a8cfcdb"
|
||||||
|
}
|
||||||
96
.sqlx/query-82c99538d54085bc56abe4fb049a5076162d7f505ce08784769b359d8f302938.json
generated
Normal file
96
.sqlx/query-82c99538d54085bc56abe4fb049a5076162d7f505ce08784769b359d8f302938.json
generated
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n UPDATE settings \n SET value = $1, updated_by = $2, updated_at = CURRENT_TIMESTAMP\n WHERE key = $3\n RETURNING id, key, value, value_type, description, category, is_encrypted, is_system, is_editable, \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": "key",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 2,
|
||||||
|
"name": "value",
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 3,
|
||||||
|
"name": "value_type",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 4,
|
||||||
|
"name": "description",
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 5,
|
||||||
|
"name": "category",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 6,
|
||||||
|
"name": "is_encrypted",
|
||||||
|
"type_info": "Bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 7,
|
||||||
|
"name": "is_system",
|
||||||
|
"type_info": "Bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 8,
|
||||||
|
"name": "is_editable",
|
||||||
|
"type_info": "Bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 9,
|
||||||
|
"name": "created_at: DateTime<Utc>",
|
||||||
|
"type_info": "Timestamp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 10,
|
||||||
|
"name": "updated_at: DateTime<Utc>",
|
||||||
|
"type_info": "Timestamp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 11,
|
||||||
|
"name": "created_by",
|
||||||
|
"type_info": "Uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 12,
|
||||||
|
"name": "updated_by",
|
||||||
|
"type_info": "Uuid"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Text",
|
||||||
|
"Uuid",
|
||||||
|
"Text"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "82c99538d54085bc56abe4fb049a5076162d7f505ce08784769b359d8f302938"
|
||||||
|
}
|
||||||
92
.sqlx/query-8fe2f86e9388d6ed22b1b3ac4c595b672dff25aec6fb85120366dd4341a70a3b.json
generated
Normal file
92
.sqlx/query-8fe2f86e9388d6ed22b1b3ac4c595b672dff25aec6fb85120366dd4341a70a3b.json
generated
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n SELECT id, key, value, value_type, description, category, is_encrypted, is_system, is_editable, \n created_at as \"created_at: DateTime<Utc>\", \n updated_at as \"updated_at: DateTime<Utc>\", \n created_by, updated_by\n FROM settings ORDER BY category, key\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "id",
|
||||||
|
"type_info": "Uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"name": "key",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 2,
|
||||||
|
"name": "value",
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 3,
|
||||||
|
"name": "value_type",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 4,
|
||||||
|
"name": "description",
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 5,
|
||||||
|
"name": "category",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 6,
|
||||||
|
"name": "is_encrypted",
|
||||||
|
"type_info": "Bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 7,
|
||||||
|
"name": "is_system",
|
||||||
|
"type_info": "Bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 8,
|
||||||
|
"name": "is_editable",
|
||||||
|
"type_info": "Bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 9,
|
||||||
|
"name": "created_at: DateTime<Utc>",
|
||||||
|
"type_info": "Timestamp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 10,
|
||||||
|
"name": "updated_at: DateTime<Utc>",
|
||||||
|
"type_info": "Timestamp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 11,
|
||||||
|
"name": "created_by",
|
||||||
|
"type_info": "Uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 12,
|
||||||
|
"name": "updated_by",
|
||||||
|
"type_info": "Uuid"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": []
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "8fe2f86e9388d6ed22b1b3ac4c595b672dff25aec6fb85120366dd4341a70a3b"
|
||||||
|
}
|
||||||
99
.sqlx/query-93734fffdd102bda19488e703012bdad22bf3675a9b2dbeaf7943c9d0341eaf2.json
generated
Normal file
99
.sqlx/query-93734fffdd102bda19488e703012bdad22bf3675a9b2dbeaf7943c9d0341eaf2.json
generated
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n UPDATE settings \n SET value = COALESCE($1, value),\n description = COALESCE($2, description),\n category = COALESCE($3, category),\n is_editable = COALESCE($4, is_editable),\n updated_by = $5,\n updated_at = CURRENT_TIMESTAMP\n WHERE id = $6\n RETURNING id, key, value, value_type, description, category, is_encrypted, is_system, is_editable, \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": "key",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 2,
|
||||||
|
"name": "value",
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 3,
|
||||||
|
"name": "value_type",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 4,
|
||||||
|
"name": "description",
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 5,
|
||||||
|
"name": "category",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 6,
|
||||||
|
"name": "is_encrypted",
|
||||||
|
"type_info": "Bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 7,
|
||||||
|
"name": "is_system",
|
||||||
|
"type_info": "Bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 8,
|
||||||
|
"name": "is_editable",
|
||||||
|
"type_info": "Bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 9,
|
||||||
|
"name": "created_at: DateTime<Utc>",
|
||||||
|
"type_info": "Timestamp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 10,
|
||||||
|
"name": "updated_at: DateTime<Utc>",
|
||||||
|
"type_info": "Timestamp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 11,
|
||||||
|
"name": "created_by",
|
||||||
|
"type_info": "Uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 12,
|
||||||
|
"name": "updated_by",
|
||||||
|
"type_info": "Uuid"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Text",
|
||||||
|
"Text",
|
||||||
|
"Varchar",
|
||||||
|
"Bool",
|
||||||
|
"Uuid",
|
||||||
|
"Uuid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "93734fffdd102bda19488e703012bdad22bf3675a9b2dbeaf7943c9d0341eaf2"
|
||||||
|
}
|
||||||
22
.sqlx/query-9daeb02358c339adce84ed11cacc7489ab9ad3c009ca40557d535f5c498704ba.json
generated
Normal file
22
.sqlx/query-9daeb02358c339adce84ed11cacc7489ab9ad3c009ca40557d535f5c498704ba.json
generated
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "SELECT id FROM settings WHERE key = $1",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "id",
|
||||||
|
"type_info": "Uuid"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Text"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "9daeb02358c339adce84ed11cacc7489ab9ad3c009ca40557d535f5c498704ba"
|
||||||
|
}
|
||||||
20
.sqlx/query-a9bf29176e34c2b075611cbe3cecfad3e7132347cfa5df1180bbb2a67e1fbf79.json
generated
Normal file
20
.sqlx/query-a9bf29176e34c2b075611cbe3cecfad3e7132347cfa5df1180bbb2a67e1fbf79.json
generated
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "SELECT DISTINCT category FROM settings ORDER BY category",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "category",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": []
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
true
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "a9bf29176e34c2b075611cbe3cecfad3e7132347cfa5df1180bbb2a67e1fbf79"
|
||||||
|
}
|
||||||
38
.sqlx/query-fbdc7145f7560c576e3e5db15fe014bcff5ce490b18ab012fe6f8cfcff14bac6.json
generated
Normal file
38
.sqlx/query-fbdc7145f7560c576e3e5db15fe014bcff5ce490b18ab012fe6f8cfcff14bac6.json
generated
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n SELECT \n category,\n COUNT(*) as 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 GROUP BY category\n ORDER BY category\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "category",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"name": "count",
|
||||||
|
"type_info": "Int8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 2,
|
||||||
|
"name": "system_count",
|
||||||
|
"type_info": "Int8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 3,
|
||||||
|
"name": "editable_count",
|
||||||
|
"type_info": "Int8"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": []
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
true,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "fbdc7145f7560c576e3e5db15fe014bcff5ce490b18ab012fe6f8cfcff14bac6"
|
||||||
|
}
|
||||||
123
Cargo.lock
generated
123
Cargo.lock
generated
@ -131,6 +131,12 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyhow"
|
||||||
|
version = "1.0.98"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "argon2"
|
name = "argon2"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
@ -881,7 +887,7 @@ version = "0.6.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
|
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"nom",
|
"nom 7.1.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -911,6 +917,16 @@ dependencies = [
|
|||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chumsky"
|
||||||
|
version = "0.9.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9"
|
||||||
|
dependencies = [
|
||||||
|
"hashbrown 0.14.5",
|
||||||
|
"stacker",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clang-sys"
|
name = "clang-sys"
|
||||||
version = "1.8.1"
|
version = "1.8.1"
|
||||||
@ -1217,6 +1233,22 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "email-encoding"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9298e6504d9b9e780ed3f7dfd43a61be8cd0e09eb07f7706a945b0072b6670b6"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "email_address"
|
||||||
|
version = "0.2.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "encoding_rs"
|
name = "encoding_rs"
|
||||||
version = "0.8.35"
|
version = "0.8.35"
|
||||||
@ -1605,6 +1637,10 @@ name = "hashbrown"
|
|||||||
version = "0.14.5"
|
version = "0.14.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||||
|
dependencies = [
|
||||||
|
"ahash 0.8.12",
|
||||||
|
"allocator-api2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
@ -1717,6 +1753,17 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hostname"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "1.3.1"
|
version = "1.3.1"
|
||||||
@ -2122,6 +2169,31 @@ version = "1.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lettre"
|
||||||
|
version = "0.11.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5cb54db6ff7a89efac87dba5baeac57bb9ccd726b49a9b6f21fb92b3966aaf56"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"chumsky",
|
||||||
|
"email-encoding",
|
||||||
|
"email_address",
|
||||||
|
"fastrand 2.3.0",
|
||||||
|
"futures-util",
|
||||||
|
"hostname",
|
||||||
|
"httpdate",
|
||||||
|
"idna",
|
||||||
|
"mime",
|
||||||
|
"native-tls",
|
||||||
|
"nom 8.0.0",
|
||||||
|
"percent-encoding",
|
||||||
|
"quoted_printable",
|
||||||
|
"socket2 0.6.0",
|
||||||
|
"tokio",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.174"
|
version = "0.2.174"
|
||||||
@ -2229,6 +2301,7 @@ dependencies = [
|
|||||||
name = "mapp"
|
name = "mapp"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
"argon2",
|
"argon2",
|
||||||
"async-graphql",
|
"async-graphql",
|
||||||
"async-graphql-axum",
|
"async-graphql-axum",
|
||||||
@ -2242,6 +2315,7 @@ dependencies = [
|
|||||||
"dotenvy",
|
"dotenvy",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"jsonwebtoken",
|
"jsonwebtoken",
|
||||||
|
"lettre",
|
||||||
"rdkafka",
|
"rdkafka",
|
||||||
"rustls",
|
"rustls",
|
||||||
"sea-query",
|
"sea-query",
|
||||||
@ -2383,6 +2457,15 @@ dependencies = [
|
|||||||
"minimal-lexical",
|
"minimal-lexical",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nom"
|
||||||
|
version = "8.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nu-ansi-term"
|
name = "nu-ansi-term"
|
||||||
version = "0.46.0"
|
version = "0.46.0"
|
||||||
@ -2790,6 +2873,15 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "psm"
|
||||||
|
version = "0.1.26"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6e944464ec8536cd1beb0bbfd96987eb5e3b72f2ecdafdc5c769a37f1fa2ae1f"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ptr_meta"
|
name = "ptr_meta"
|
||||||
version = "0.1.4"
|
version = "0.1.4"
|
||||||
@ -2874,6 +2966,12 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quoted_printable"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "r-efi"
|
name = "r-efi"
|
||||||
version = "5.3.0"
|
version = "5.3.0"
|
||||||
@ -3544,6 +3642,16 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "socket2"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "spin"
|
name = "spin"
|
||||||
version = "0.9.8"
|
version = "0.9.8"
|
||||||
@ -3781,6 +3889,19 @@ version = "1.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "stacker"
|
||||||
|
version = "0.1.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cddb07e32ddb770749da91081d8d0ac3a16f1a569a18b20348cd371f5dead06b"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"psm",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "static_assertions_next"
|
name = "static_assertions_next"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
|
|||||||
@ -49,4 +49,6 @@ rustls = { version = "0.23", features = ["aws-lc-rs"] }
|
|||||||
clap = { version = "4.0", features = ["derive"] }
|
clap = { version = "4.0", features = ["derive"] }
|
||||||
rdkafka = "0.38.0"
|
rdkafka = "0.38.0"
|
||||||
axum_gcra = "0.1.1"
|
axum_gcra = "0.1.1"
|
||||||
|
lettre = "0.11.18"
|
||||||
|
anyhow = "1.0.98"
|
||||||
|
|
||||||
|
|||||||
448
docs/SETTINGS_SYSTEM.md
Normal file
448
docs/SETTINGS_SYSTEM.md
Normal file
@ -0,0 +1,448 @@
|
|||||||
|
# Settings 配置管理系统
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
Settings系统是一个灵活的、可扩展的配置管理系统,用于管理网络后台的各种配置项。它使用PostgreSQL数据库存储配置,支持多种数据类型,并提供缓存、验证、历史记录等功能。
|
||||||
|
|
||||||
|
## 特性
|
||||||
|
|
||||||
|
- **灵活的数据类型支持**: 支持string、number、boolean、json等类型
|
||||||
|
- **分类管理**: 按功能模块分类组织配置项
|
||||||
|
- **权限控制**: 区分系统配置和用户配置,支持只读配置
|
||||||
|
- **缓存机制**: 内置缓存,提高配置访问性能
|
||||||
|
- **历史记录**: 自动记录配置变更历史,支持审计
|
||||||
|
- **类型安全**: 提供类型安全的配置访问API
|
||||||
|
- **批量操作**: 支持批量更新和导入导出
|
||||||
|
- **GraphQL接口**: 完整的GraphQL API支持
|
||||||
|
|
||||||
|
## 数据库结构
|
||||||
|
|
||||||
|
### settings表
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE settings (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
key VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
value TEXT,
|
||||||
|
value_type VARCHAR(50) NOT NULL DEFAULT 'string',
|
||||||
|
description TEXT,
|
||||||
|
category VARCHAR(100) DEFAULT 'general',
|
||||||
|
is_encrypted BOOLEAN DEFAULT FALSE,
|
||||||
|
is_system BOOLEAN DEFAULT FALSE,
|
||||||
|
is_editable BOOLEAN DEFAULT TRUE,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
created_by UUID,
|
||||||
|
updated_by UUID
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### settings_history表
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE settings_history (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
setting_id UUID NOT NULL REFERENCES settings(id) ON DELETE CASCADE,
|
||||||
|
old_value TEXT,
|
||||||
|
new_value TEXT,
|
||||||
|
changed_by UUID,
|
||||||
|
change_reason TEXT,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 核心组件
|
||||||
|
|
||||||
|
### 1. SettingsService
|
||||||
|
|
||||||
|
基础的配置服务,提供CRUD操作:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use crate::services::settings_service::SettingsService;
|
||||||
|
|
||||||
|
let settings_service = SettingsService::new(pool);
|
||||||
|
|
||||||
|
// 创建配置
|
||||||
|
let setting = settings_service.create_setting(create_setting, user_id).await?;
|
||||||
|
|
||||||
|
// 获取配置
|
||||||
|
let setting = settings_service.get_setting_by_key("app.name").await?;
|
||||||
|
|
||||||
|
// 更新配置
|
||||||
|
let updated = settings_service.update_setting(id, update_setting, user_id).await?;
|
||||||
|
|
||||||
|
// 删除配置
|
||||||
|
let deleted = settings_service.delete_setting(id).await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. SettingsManager
|
||||||
|
|
||||||
|
高级配置管理器,提供缓存和类型安全的访问:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use crate::services::settings_manager::{SettingsManager, keys};
|
||||||
|
|
||||||
|
let settings_manager = SettingsManager::new(settings_service);
|
||||||
|
|
||||||
|
// 类型安全的配置访问
|
||||||
|
let app_name = settings_manager.get_string_or(keys::APP_NAME, "Default App").await?;
|
||||||
|
let max_connections = settings_manager.get_int_or(keys::DB_MAX_CONNECTIONS, 10).await?;
|
||||||
|
let debug_mode = settings_manager.get_bool_or(keys::APP_DEBUG, false).await?;
|
||||||
|
|
||||||
|
// 动态更新配置
|
||||||
|
settings_manager.set_value(keys::DB_MAX_CONNECTIONS, &20).await?;
|
||||||
|
|
||||||
|
// 批量更新
|
||||||
|
let mut updates = HashMap::new();
|
||||||
|
updates.insert(keys::KAFKA_MAX_RETRIES.to_string(), Value::Number(5));
|
||||||
|
settings_manager.set_values(updates).await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. SettingsValidator
|
||||||
|
|
||||||
|
配置验证器,确保配置的完整性和正确性:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use crate::services::settings_manager::SettingsValidator;
|
||||||
|
|
||||||
|
// 验证必需配置
|
||||||
|
let missing = SettingsValidator::validate_required_settings(&manager).await?;
|
||||||
|
|
||||||
|
// 验证配置类型
|
||||||
|
let type_errors = SettingsValidator::validate_setting_types(&manager).await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 预定义配置键
|
||||||
|
|
||||||
|
系统预定义了一些常用的配置键:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## GraphQL API
|
||||||
|
|
||||||
|
### 查询
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
# 获取所有配置
|
||||||
|
query {
|
||||||
|
settings {
|
||||||
|
id
|
||||||
|
key
|
||||||
|
value
|
||||||
|
valueType
|
||||||
|
description
|
||||||
|
category
|
||||||
|
isSystem
|
||||||
|
isEditable
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 按分类过滤配置
|
||||||
|
query {
|
||||||
|
settings(filter: { category: "app" }) {
|
||||||
|
key
|
||||||
|
value
|
||||||
|
description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 获取配置统计
|
||||||
|
query {
|
||||||
|
settingsStats {
|
||||||
|
categories
|
||||||
|
stats
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 获取配置历史
|
||||||
|
query {
|
||||||
|
settingHistory(settingId: "uuid") {
|
||||||
|
oldValue
|
||||||
|
newValue
|
||||||
|
changedBy
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 变更
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
# 创建配置
|
||||||
|
mutation {
|
||||||
|
createSetting(input: {
|
||||||
|
key: "custom.setting"
|
||||||
|
value: "custom_value"
|
||||||
|
valueType: "string"
|
||||||
|
description: "Custom setting"
|
||||||
|
category: "custom"
|
||||||
|
}) {
|
||||||
|
id
|
||||||
|
key
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 更新配置
|
||||||
|
mutation {
|
||||||
|
updateSetting(
|
||||||
|
id: "uuid"
|
||||||
|
input: { value: "new_value" }
|
||||||
|
) {
|
||||||
|
key
|
||||||
|
value
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 批量更新
|
||||||
|
mutation {
|
||||||
|
batchUpdateSettings(input: {
|
||||||
|
updates: [
|
||||||
|
{ key: "app.name", value: "New App Name" }
|
||||||
|
{ key: "app.debug", value: true }
|
||||||
|
]
|
||||||
|
}) {
|
||||||
|
key
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 删除配置
|
||||||
|
mutation {
|
||||||
|
deleteSetting(id: "uuid")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用示例
|
||||||
|
|
||||||
|
### 基本使用
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use crate::services::settings_manager::{SettingsManager, keys};
|
||||||
|
|
||||||
|
async fn configure_app(settings_manager: &SettingsManager) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// 获取应用配置
|
||||||
|
let app_name = settings_manager.get_string_or(keys::APP_NAME, "Default App").await?;
|
||||||
|
let debug_mode = settings_manager.get_bool_or(keys::APP_DEBUG, false).await?;
|
||||||
|
|
||||||
|
// 根据配置调整应用行为
|
||||||
|
if debug_mode {
|
||||||
|
println!("调试模式已启用");
|
||||||
|
// 设置更详细的日志级别
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("应用名称: {}", app_name);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 动态配置更新
|
||||||
|
|
||||||
|
```rust
|
||||||
|
async fn dynamic_config_update(settings_manager: &SettingsManager) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// 监听配置变化
|
||||||
|
loop {
|
||||||
|
let max_connections = settings_manager.get_int_or(keys::DB_MAX_CONNECTIONS, 10).await?;
|
||||||
|
let connection_timeout = settings_manager.get_int_or(keys::DB_CONNECTION_TIMEOUT, 30).await?;
|
||||||
|
|
||||||
|
// 根据配置调整数据库连接池
|
||||||
|
adjust_database_pool(max_connections, connection_timeout).await?;
|
||||||
|
|
||||||
|
// 等待一段时间后再次检查
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_secs(60)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置迁移
|
||||||
|
|
||||||
|
```rust
|
||||||
|
async fn migrate_old_configs(settings_service: &SettingsService) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let old_configs = vec![
|
||||||
|
("old.setting.1", "value1", "string"),
|
||||||
|
("old.setting.2", "100", "number"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (key, value, value_type) in old_configs {
|
||||||
|
let create_setting = CreateSetting {
|
||||||
|
key: key.to_string(),
|
||||||
|
value: Some(value.to_string()),
|
||||||
|
value_type: value_type.to_string(),
|
||||||
|
description: Some("Migrated from old system".to_string()),
|
||||||
|
category: "migrated".to_string(),
|
||||||
|
is_encrypted: Some(false),
|
||||||
|
is_system: Some(false),
|
||||||
|
is_editable: Some(true),
|
||||||
|
};
|
||||||
|
|
||||||
|
settings_service.create_setting(create_setting, uuid::Uuid::nil()).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
### 1. 配置命名规范
|
||||||
|
|
||||||
|
- 使用点分隔的层次结构:`module.submodule.setting_name`
|
||||||
|
- 使用小写字母和下划线
|
||||||
|
- 保持命名的一致性和可读性
|
||||||
|
|
||||||
|
### 2. 配置分类
|
||||||
|
|
||||||
|
- 按功能模块分类:`app`、`database`、`kafka`、`security`等
|
||||||
|
- 使用`general`分类存放通用配置
|
||||||
|
- 为自定义配置使用专门的分类名
|
||||||
|
|
||||||
|
### 3. 配置类型选择
|
||||||
|
|
||||||
|
- `string`: 文本配置,如应用名称、文件路径等
|
||||||
|
- `number`: 数值配置,如超时时间、连接数等
|
||||||
|
- `boolean`: 开关配置,如调试模式、功能开关等
|
||||||
|
- `json`: 复杂配置,如API配置、映射关系等
|
||||||
|
|
||||||
|
### 4. 安全考虑
|
||||||
|
|
||||||
|
- 敏感配置设置`is_encrypted = true`
|
||||||
|
- 系统关键配置设置`is_system = true`
|
||||||
|
- 只读配置设置`is_editable = false`
|
||||||
|
|
||||||
|
### 5. 性能优化
|
||||||
|
|
||||||
|
- 合理使用缓存,避免频繁数据库查询
|
||||||
|
- 批量操作减少数据库往返
|
||||||
|
- 定期清理历史记录表
|
||||||
|
|
||||||
|
## 扩展性
|
||||||
|
|
||||||
|
### 添加新的配置类型
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 在Settings模型中添加新的类型支持
|
||||||
|
impl Setting {
|
||||||
|
pub fn get_custom_type(&self) -> Result<CustomType, String> {
|
||||||
|
if self.value_type == "custom" {
|
||||||
|
// 自定义类型解析逻辑
|
||||||
|
CustomType::from_str(&self.value.unwrap_or_default())
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
} else {
|
||||||
|
Err("Setting is not a custom type".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 添加新的配置分类
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 在keys模块中添加新的分类常量
|
||||||
|
pub mod keys {
|
||||||
|
// 现有分类...
|
||||||
|
|
||||||
|
// 新功能分类
|
||||||
|
pub const NEW_FEATURE_ENABLED: &str = "new_feature.enabled";
|
||||||
|
pub const NEW_FEATURE_TIMEOUT: &str = "new_feature.timeout";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 自定义验证规则
|
||||||
|
|
||||||
|
```rust
|
||||||
|
impl SettingsValidator {
|
||||||
|
pub async fn validate_custom_rules(manager: &SettingsManager) -> Result<Vec<String>> {
|
||||||
|
let mut errors = Vec::new();
|
||||||
|
|
||||||
|
// 自定义验证逻辑
|
||||||
|
let feature_enabled = manager.get_bool_or(keys::NEW_FEATURE_ENABLED, false).await?;
|
||||||
|
let feature_timeout = manager.get_int_or(keys::NEW_FEATURE_TIMEOUT, 30).await?;
|
||||||
|
|
||||||
|
if feature_enabled && feature_timeout < 10 {
|
||||||
|
errors.push("Feature timeout must be at least 10 seconds when enabled".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(errors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 故障排除
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
1. **配置不生效**
|
||||||
|
- 检查缓存是否过期
|
||||||
|
- 验证配置项是否存在
|
||||||
|
- 确认配置项类型是否正确
|
||||||
|
|
||||||
|
2. **性能问题**
|
||||||
|
- 检查缓存设置
|
||||||
|
- 优化数据库查询
|
||||||
|
- 减少不必要的配置访问
|
||||||
|
|
||||||
|
3. **权限问题**
|
||||||
|
- 确认用户角色
|
||||||
|
- 检查配置项是否可编辑
|
||||||
|
- 验证系统配置保护
|
||||||
|
|
||||||
|
### 调试技巧
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 启用详细日志
|
||||||
|
if settings_manager.get_bool_or(keys::APP_DEBUG, false).await? {
|
||||||
|
println!("当前配置状态:");
|
||||||
|
let all_settings = settings_manager.get_all_settings().await?;
|
||||||
|
for setting in all_settings {
|
||||||
|
println!(" {}: {} ({})", setting.key, setting.value.unwrap_or_default(), setting.value_type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 强制刷新缓存
|
||||||
|
settings_manager.force_refresh_cache().await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
Settings系统提供了一个强大而灵活的配置管理解决方案,支持:
|
||||||
|
|
||||||
|
- 多种数据类型的配置存储
|
||||||
|
- 分类管理和权限控制
|
||||||
|
- 缓存和性能优化
|
||||||
|
- 完整的GraphQL API
|
||||||
|
- 配置历史记录和审计
|
||||||
|
- 类型安全的配置访问
|
||||||
|
- 易于扩展和维护
|
||||||
|
|
||||||
|
通过合理使用这个系统,可以大大简化应用程序的配置管理,提高系统的可维护性和灵活性。
|
||||||
95
migrations/006_create_settings.sql
Normal file
95
migrations/006_create_settings.sql
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
-- 创建settings配置表
|
||||||
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
key VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
value TEXT,
|
||||||
|
value_type VARCHAR(50) NOT NULL DEFAULT 'string', -- string, number, boolean, json
|
||||||
|
description TEXT,
|
||||||
|
category VARCHAR(100) DEFAULT 'general',
|
||||||
|
is_encrypted BOOLEAN DEFAULT FALSE,
|
||||||
|
is_system BOOLEAN DEFAULT FALSE, -- 系统配置,不允许删除
|
||||||
|
is_editable BOOLEAN DEFAULT TRUE, -- 是否允许编辑
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
created_by UUID,
|
||||||
|
updated_by UUID
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 添加字段注释
|
||||||
|
COMMENT ON COLUMN settings.id IS '配置项唯一标识符';
|
||||||
|
COMMENT ON COLUMN settings.key IS '配置键名,全局唯一';
|
||||||
|
COMMENT ON COLUMN settings.value IS '配置值';
|
||||||
|
COMMENT ON COLUMN settings.value_type IS '值类型:string, number, boolean, json';
|
||||||
|
COMMENT ON COLUMN settings.description IS '配置项描述';
|
||||||
|
COMMENT ON COLUMN settings.category IS '配置分类,便于分组管理';
|
||||||
|
COMMENT ON COLUMN settings.is_encrypted IS '是否为加密值';
|
||||||
|
COMMENT ON COLUMN settings.is_system IS '是否为系统配置';
|
||||||
|
COMMENT ON COLUMN settings.is_editable IS '是否允许编辑';
|
||||||
|
COMMENT ON COLUMN settings.created_by IS '创建者ID';
|
||||||
|
COMMENT ON COLUMN settings.updated_by IS '更新者ID';
|
||||||
|
|
||||||
|
-- 创建索引
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_settings_key ON settings(key);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_settings_category ON settings(category);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_settings_is_system ON settings(is_system);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_settings_created_at ON settings(created_at);
|
||||||
|
|
||||||
|
-- 创建复合索引
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_settings_category_key ON settings(category, key);
|
||||||
|
|
||||||
|
-- 为表创建更新时间触发器
|
||||||
|
CREATE TRIGGER update_settings_updated_at
|
||||||
|
BEFORE UPDATE ON settings
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION update_updated_at_column();
|
||||||
|
|
||||||
|
-- 插入一些默认配置
|
||||||
|
INSERT INTO settings (key, value, value_type, description, category, is_system) VALUES
|
||||||
|
('app.name', 'MMAP System', 'string', '应用名称', 'app', true),
|
||||||
|
('app.version', '1.0.0', 'string', '应用版本', 'app', true),
|
||||||
|
('app.debug', 'false', 'boolean', '调试模式', 'app', true),
|
||||||
|
('app.timezone', 'UTC', 'string', '系统时区', 'app', true),
|
||||||
|
('database.max_connections', '10', 'number', '数据库最大连接数', 'database', true),
|
||||||
|
('database.connection_timeout', '30', 'number', '数据库连接超时时间(秒)', 'database', true),
|
||||||
|
('kafka.max_retries', '3', 'number', 'Kafka最大重试次数', 'kafka', true),
|
||||||
|
('kafka.retry_delay', '1000', 'number', 'Kafka重试延迟(毫秒)', 'kafka', true),
|
||||||
|
('security.session_timeout', '3600', 'number', '会话超时时间(秒)', 'security', true),
|
||||||
|
('security.max_login_attempts', '5', 'number', '最大登录尝试次数', 'security', true),
|
||||||
|
('logging.level', 'info', 'string', '日志级别', 'logging', true),
|
||||||
|
('logging.max_files', '10', 'number', '最大日志文件数', 'logging', true),
|
||||||
|
('cache.ttl', '300', 'number', '缓存生存时间(秒)', 'cache', true),
|
||||||
|
('cache.max_size', '1000', 'number', '缓存最大条目数', 'cache', true)
|
||||||
|
ON CONFLICT (key) DO NOTHING;
|
||||||
|
|
||||||
|
-- 创建配置历史表(可选,用于审计)
|
||||||
|
CREATE TABLE IF NOT EXISTS settings_history (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
setting_id UUID NOT NULL REFERENCES settings(id) ON DELETE CASCADE,
|
||||||
|
old_value TEXT,
|
||||||
|
new_value TEXT,
|
||||||
|
changed_by UUID,
|
||||||
|
change_reason TEXT,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 为历史表创建索引
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_settings_history_setting_id ON settings_history(setting_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_settings_history_created_at ON settings_history(created_at);
|
||||||
|
|
||||||
|
-- 创建配置变更触发器
|
||||||
|
CREATE OR REPLACE FUNCTION log_setting_change()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
IF OLD.value IS DISTINCT FROM NEW.value THEN
|
||||||
|
INSERT INTO settings_history (setting_id, old_value, new_value, changed_by)
|
||||||
|
VALUES (NEW.id, OLD.value, NEW.value, NEW.updated_by);
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ language 'plpgsql';
|
||||||
|
|
||||||
|
-- 为settings表创建变更日志触发器
|
||||||
|
CREATE TRIGGER log_settings_changes
|
||||||
|
AFTER UPDATE ON settings
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION log_setting_change();
|
||||||
@ -26,6 +26,7 @@ use crate::{
|
|||||||
services::{
|
services::{
|
||||||
invite_code_service::InviteCodeService, mosaic_service::MosaicService,
|
invite_code_service::InviteCodeService, mosaic_service::MosaicService,
|
||||||
system_config_service::SystemConfigService, user_service::UserService,
|
system_config_service::SystemConfigService, user_service::UserService,
|
||||||
|
settings_service::SettingsService,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -50,6 +51,7 @@ pub fn create_router(
|
|||||||
let invite_code_service = InviteCodeService::new(pool.clone());
|
let invite_code_service = InviteCodeService::new(pool.clone());
|
||||||
let system_config_service = SystemConfigService::new(pool.clone());
|
let system_config_service = SystemConfigService::new(pool.clone());
|
||||||
let mosaic_service = MosaicService::new(pool.clone());
|
let mosaic_service = MosaicService::new(pool.clone());
|
||||||
|
let settings_service = SettingsService::new(pool.clone());
|
||||||
|
|
||||||
let schema = Schema::build(QueryRoot, MutationRoot, SubscriptionRoot)
|
let schema = Schema::build(QueryRoot, MutationRoot, SubscriptionRoot)
|
||||||
.data(pool)
|
.data(pool)
|
||||||
@ -57,6 +59,7 @@ pub fn create_router(
|
|||||||
.data(invite_code_service)
|
.data(invite_code_service)
|
||||||
.data(system_config_service)
|
.data(system_config_service)
|
||||||
.data(mosaic_service)
|
.data(mosaic_service)
|
||||||
|
.data(settings_service)
|
||||||
.data(config.clone())
|
.data(config.clone())
|
||||||
.data(status_sender.clone())
|
.data(status_sender.clone())
|
||||||
.finish();
|
.finish();
|
||||||
|
|||||||
@ -3,12 +3,16 @@ use crate::graphql::guards::RequireRole;
|
|||||||
use crate::graphql::types::{
|
use crate::graphql::types::{
|
||||||
CreateInviteCodeInput, CreateUserInput, InitializeAdminInput, InitializeAdminResponse,
|
CreateInviteCodeInput, CreateUserInput, InitializeAdminInput, InitializeAdminResponse,
|
||||||
InviteCodeResponse, LoginInput, LoginResponse, RegisterInput,
|
InviteCodeResponse, LoginInput, LoginResponse, RegisterInput,
|
||||||
|
CreateSettingInput, UpdateSettingInput, BatchUpdateSettingsInput, SettingType,
|
||||||
};
|
};
|
||||||
use crate::models::user::Role;
|
use crate::models::user::Role;
|
||||||
use crate::models::user::User;
|
use crate::models::user::User;
|
||||||
|
use crate::models::settings::{CreateSetting, UpdateSetting};
|
||||||
use crate::services::invite_code_service::InviteCodeService;
|
use crate::services::invite_code_service::InviteCodeService;
|
||||||
use crate::services::user_service::UserService;
|
use crate::services::user_service::UserService;
|
||||||
|
use crate::services::settings_service::SettingsService;
|
||||||
use async_graphql::{Context, Object, Result};
|
use async_graphql::{Context, Object, Result};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub struct MutationRoot;
|
pub struct MutationRoot;
|
||||||
|
|
||||||
@ -80,4 +84,120 @@ impl MutationRoot {
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Settings mutations
|
||||||
|
#[graphql(guard = "RequireRole(Role::Admin)")]
|
||||||
|
async fn create_setting(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
input: CreateSettingInput,
|
||||||
|
) -> Result<SettingType> {
|
||||||
|
let auth_user = get_auth_user(ctx).await?;
|
||||||
|
let settings_service = ctx.data::<SettingsService>()?;
|
||||||
|
|
||||||
|
let create_setting = CreateSetting {
|
||||||
|
key: input.key,
|
||||||
|
value: input.value,
|
||||||
|
value_type: input.value_type,
|
||||||
|
description: input.description,
|
||||||
|
category: input.category,
|
||||||
|
is_encrypted: input.is_encrypted,
|
||||||
|
is_system: input.is_system,
|
||||||
|
is_editable: input.is_editable,
|
||||||
|
};
|
||||||
|
|
||||||
|
let setting = settings_service.create_setting(create_setting, auth_user.id).await?;
|
||||||
|
|
||||||
|
Ok(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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[graphql(guard = "RequireRole(Role::Admin)")]
|
||||||
|
async fn update_setting(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
id: Uuid,
|
||||||
|
input: UpdateSettingInput,
|
||||||
|
) -> Result<SettingType> {
|
||||||
|
let auth_user = get_auth_user(ctx).await?;
|
||||||
|
let settings_service = ctx.data::<SettingsService>()?;
|
||||||
|
|
||||||
|
let update_setting = UpdateSetting {
|
||||||
|
value: input.value,
|
||||||
|
description: input.description,
|
||||||
|
category: input.category,
|
||||||
|
is_editable: input.is_editable,
|
||||||
|
};
|
||||||
|
|
||||||
|
let setting = settings_service.update_setting(id, update_setting, auth_user.id).await?;
|
||||||
|
|
||||||
|
Ok(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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[graphql(guard = "RequireRole(Role::Admin)")]
|
||||||
|
async fn delete_setting(&self, ctx: &Context<'_>, id: Uuid) -> Result<bool> {
|
||||||
|
let settings_service = ctx.data::<SettingsService>()?;
|
||||||
|
let deleted = settings_service.delete_setting(id).await?;
|
||||||
|
Ok(deleted)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[graphql(guard = "RequireRole(Role::Admin)")]
|
||||||
|
async fn batch_update_settings(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
input: BatchUpdateSettingsInput,
|
||||||
|
) -> Result<Vec<SettingType>> {
|
||||||
|
let auth_user = get_auth_user(ctx).await?;
|
||||||
|
let settings_service = ctx.data::<SettingsService>()?;
|
||||||
|
|
||||||
|
let updates: Vec<(String, serde_json::Value)> = input.updates
|
||||||
|
.into_iter()
|
||||||
|
.map(|item| (item.key, item.value))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let settings = settings_service.batch_update_settings(updates, auth_user.id).await?;
|
||||||
|
|
||||||
|
Ok(settings.into_iter().map(|s| SettingType {
|
||||||
|
id: s.id,
|
||||||
|
key: s.key,
|
||||||
|
value: s.value,
|
||||||
|
value_type: s.value_type,
|
||||||
|
description: s.description,
|
||||||
|
category: s.category,
|
||||||
|
is_encrypted: s.is_encrypted,
|
||||||
|
is_system: s.is_system,
|
||||||
|
is_editable: s.is_editable,
|
||||||
|
created_at: s.created_at,
|
||||||
|
updated_at: s.updated_at,
|
||||||
|
created_by: s.created_by,
|
||||||
|
updated_by: s.updated_by,
|
||||||
|
}).collect())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,17 @@
|
|||||||
use crate::auth::get_auth_user;
|
use crate::auth::get_auth_user;
|
||||||
use crate::graphql::guards::RequireRole;
|
use crate::graphql::guards::RequireRole;
|
||||||
use crate::graphql::types::UserInfoRespnose;
|
use crate::graphql::types::{
|
||||||
|
SettingFilterInput, SettingHistoryType, SettingType, SettingsStatsType, UserInfoRespnose,
|
||||||
|
};
|
||||||
use crate::models::invite_code::InviteCode;
|
use crate::models::invite_code::InviteCode;
|
||||||
|
use crate::models::settings::{SettingFilter, SettingHistory};
|
||||||
use crate::models::user::{Role, User, UserInfoRow};
|
use crate::models::user::{Role, User, UserInfoRow};
|
||||||
use crate::services::invite_code_service::InviteCodeService;
|
use crate::services::invite_code_service::InviteCodeService;
|
||||||
|
use crate::services::settings_service::SettingsService;
|
||||||
use crate::services::user_service::UserService;
|
use crate::services::user_service::UserService;
|
||||||
use async_graphql::{Context, Object, Result};
|
use async_graphql::{Context, Object, Result};
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub struct QueryRoot;
|
pub struct QueryRoot;
|
||||||
|
|
||||||
@ -90,4 +95,128 @@ impl QueryRoot {
|
|||||||
.users_info(offset, limit, sort_by, sort_order, filter)
|
.users_info(offset, limit, sort_by, sort_order, filter)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Settings queries
|
||||||
|
#[graphql(guard = "RequireRole(Role::Admin)")]
|
||||||
|
async fn settings(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
filter: Option<SettingFilterInput>,
|
||||||
|
) -> Result<Vec<SettingType>> {
|
||||||
|
let settings_service = ctx.data::<SettingsService>()?;
|
||||||
|
let filter = filter.map(|f| SettingFilter {
|
||||||
|
category: f.category,
|
||||||
|
is_system: f.is_system,
|
||||||
|
is_editable: f.is_editable,
|
||||||
|
search: f.search,
|
||||||
|
});
|
||||||
|
|
||||||
|
let settings = if let Some(filter) = filter {
|
||||||
|
settings_service.get_settings_with_filter(&filter).await?
|
||||||
|
} else {
|
||||||
|
settings_service.get_all_settings().await?
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(settings
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| SettingType {
|
||||||
|
id: s.id,
|
||||||
|
key: s.key,
|
||||||
|
value: s.value,
|
||||||
|
value_type: s.value_type,
|
||||||
|
description: s.description,
|
||||||
|
category: s.category,
|
||||||
|
is_encrypted: s.is_encrypted,
|
||||||
|
is_system: s.is_system,
|
||||||
|
is_editable: s.is_editable,
|
||||||
|
created_at: s.created_at,
|
||||||
|
updated_at: s.updated_at,
|
||||||
|
created_by: s.created_by,
|
||||||
|
updated_by: s.updated_by,
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[graphql(guard = "RequireRole(Role::Admin)")]
|
||||||
|
async fn setting_by_key(&self, ctx: &Context<'_>, key: String) -> Result<Option<SettingType>> {
|
||||||
|
let settings_service = ctx.data::<SettingsService>()?;
|
||||||
|
let setting = settings_service.get_setting_by_key(&key).await?;
|
||||||
|
|
||||||
|
Ok(setting.map(|s| SettingType {
|
||||||
|
id: s.id,
|
||||||
|
key: s.key,
|
||||||
|
value: s.value,
|
||||||
|
value_type: s.value_type,
|
||||||
|
description: s.description,
|
||||||
|
category: s.category,
|
||||||
|
is_encrypted: s.is_encrypted,
|
||||||
|
is_system: s.is_system,
|
||||||
|
is_editable: s.is_editable,
|
||||||
|
created_at: s.created_at,
|
||||||
|
updated_at: s.updated_at,
|
||||||
|
created_by: s.created_by,
|
||||||
|
updated_by: s.updated_by,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[graphql(guard = "RequireRole(Role::Admin)")]
|
||||||
|
async fn setting_by_id(&self, ctx: &Context<'_>, id: Uuid) -> Result<Option<SettingType>> {
|
||||||
|
let settings_service = ctx.data::<SettingsService>()?;
|
||||||
|
let setting = settings_service.get_setting_by_id(id).await?;
|
||||||
|
|
||||||
|
Ok(setting.map(|s| SettingType {
|
||||||
|
id: s.id,
|
||||||
|
key: s.key,
|
||||||
|
value: s.value,
|
||||||
|
value_type: s.value_type,
|
||||||
|
description: s.description,
|
||||||
|
category: s.category,
|
||||||
|
is_encrypted: s.is_encrypted,
|
||||||
|
is_system: s.is_system,
|
||||||
|
is_editable: s.is_editable,
|
||||||
|
created_at: s.created_at,
|
||||||
|
updated_at: s.updated_at,
|
||||||
|
created_by: s.created_by,
|
||||||
|
updated_by: s.updated_by,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[graphql(guard = "RequireRole(Role::Admin)")]
|
||||||
|
async fn setting_categories(&self, ctx: &Context<'_>) -> Result<Vec<String>> {
|
||||||
|
let settings_service = ctx.data::<SettingsService>()?;
|
||||||
|
let categories = settings_service.get_categories().await?;
|
||||||
|
Ok(categories)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[graphql(guard = "RequireRole(Role::Admin)")]
|
||||||
|
async fn settings_stats(&self, ctx: &Context<'_>) -> Result<SettingsStatsType> {
|
||||||
|
let settings_service = ctx.data::<SettingsService>()?;
|
||||||
|
let categories = settings_service.get_categories().await?;
|
||||||
|
let stats = settings_service.get_settings_stats().await?;
|
||||||
|
|
||||||
|
Ok(SettingsStatsType { categories, stats })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[graphql(guard = "RequireRole(Role::Admin)")]
|
||||||
|
async fn setting_history(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
setting_id: Uuid,
|
||||||
|
) -> Result<Vec<SettingHistoryType>> {
|
||||||
|
let settings_service = ctx.data::<SettingsService>()?;
|
||||||
|
let history = settings_service.get_setting_history(setting_id).await?;
|
||||||
|
|
||||||
|
Ok(history
|
||||||
|
.into_iter()
|
||||||
|
.map(|h| SettingHistoryType {
|
||||||
|
id: h.id,
|
||||||
|
setting_id: h.setting_id,
|
||||||
|
old_value: h.old_value,
|
||||||
|
new_value: h.new_value,
|
||||||
|
changed_by: h.changed_by,
|
||||||
|
change_reason: h.change_reason,
|
||||||
|
created_at: h.created_at,
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
|
use crate::models::settings::{
|
||||||
|
CreateSetting, Setting, SettingFilter, SettingHistory, UpdateSetting,
|
||||||
|
};
|
||||||
use crate::models::user::Role;
|
use crate::models::user::Role;
|
||||||
use async_graphql::{InputObject, SimpleObject};
|
use async_graphql::{InputObject, SimpleObject};
|
||||||
|
use serde_json::Value;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(InputObject)]
|
#[derive(InputObject)]
|
||||||
pub struct RegisterInput {
|
pub struct RegisterInput {
|
||||||
@ -70,3 +75,77 @@ pub struct UserInfoRespnose {
|
|||||||
|
|
||||||
pub users: Vec<crate::models::user::UserInfoRow>,
|
pub users: Vec<crate::models::user::UserInfoRow>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Settings GraphQL types
|
||||||
|
#[derive(SimpleObject)]
|
||||||
|
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>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(InputObject)]
|
||||||
|
pub struct BatchUpdateSettingsInput {
|
||||||
|
pub updates: Vec<BatchUpdateItemInput>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(InputObject)]
|
||||||
|
pub struct BatchUpdateItemInput {
|
||||||
|
pub key: String,
|
||||||
|
pub value: Value,
|
||||||
|
}
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
pub mod invite_code;
|
pub mod invite_code;
|
||||||
pub mod kafka_message;
|
pub mod kafka_message;
|
||||||
|
pub mod settings;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
|
||||||
pub use invite_code::*;
|
pub use invite_code::*;
|
||||||
pub use kafka_message::*;
|
pub use kafka_message::*;
|
||||||
|
pub use settings::*;
|
||||||
pub use user::*;
|
pub use user::*;
|
||||||
|
|||||||
161
src/models/settings.rs
Normal file
161
src/models/settings.rs
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,7 @@
|
|||||||
pub mod invite_code_service;
|
pub mod invite_code_service;
|
||||||
pub mod mosaic_service;
|
pub mod mosaic_service;
|
||||||
|
pub mod query_builder;
|
||||||
|
pub mod settings_manager;
|
||||||
|
pub mod settings_service;
|
||||||
pub mod system_config_service;
|
pub mod system_config_service;
|
||||||
pub mod user_service;
|
pub mod user_service;
|
||||||
|
|||||||
315
src/services/query_builder.rs
Normal file
315
src/services/query_builder.rs
Normal file
@ -0,0 +1,315 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use sea_query::{
|
||||||
|
extension::postgres::PgExpr, DeleteStatement, Expr, Iden, Order, PostgresQueryBuilder, Query,
|
||||||
|
SelectStatement, UpdateStatement,
|
||||||
|
};
|
||||||
|
use sea_query_binder::SqlxBinder;
|
||||||
|
use sqlx::{PgPool, Row};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
/// 通用的动态查询构建器
|
||||||
|
pub struct DynamicQueryBuilder {
|
||||||
|
pool: PgPool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DynamicQueryBuilder {
|
||||||
|
pub fn new(pool: PgPool) -> Self {
|
||||||
|
Self { pool }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 构建动态的选择查询
|
||||||
|
pub fn select<T: Iden + Clone + 'static>(&self, table: T) -> SelectQueryBuilder<T> {
|
||||||
|
SelectQueryBuilder::new(self.pool.clone(), table)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 构建动态的更新查询
|
||||||
|
pub fn update<T: Iden + Clone + 'static>(&self, table: T) -> UpdateQueryBuilder<T> {
|
||||||
|
UpdateQueryBuilder::new(self.pool.clone(), table)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 构建动态的删除查询
|
||||||
|
pub fn delete<T: Iden + Clone + 'static>(&self, table: T) -> DeleteQueryBuilder<T> {
|
||||||
|
DeleteQueryBuilder::new(self.pool.clone(), table)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 选择查询构建器
|
||||||
|
pub struct SelectQueryBuilder<T: Iden + Clone> {
|
||||||
|
pool: PgPool,
|
||||||
|
query: SelectStatement,
|
||||||
|
table: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Iden + Clone + 'static> SelectQueryBuilder<T> {
|
||||||
|
fn new(pool: PgPool, table: T) -> Self {
|
||||||
|
let mut query = Query::select();
|
||||||
|
query.from(table.clone());
|
||||||
|
|
||||||
|
Self { pool, query, table }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 添加选择的列
|
||||||
|
pub fn columns<I: Iden + Clone + 'static>(
|
||||||
|
mut self,
|
||||||
|
columns: impl IntoIterator<Item = I>,
|
||||||
|
) -> Self {
|
||||||
|
self.query.columns(columns);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 添加条件
|
||||||
|
pub fn condition_if<V>(mut self, condition: bool, column: impl Iden + 'static, value: V) -> Self
|
||||||
|
where
|
||||||
|
V: Into<sea_query::Value>,
|
||||||
|
{
|
||||||
|
if condition {
|
||||||
|
self.query.and_where(Expr::col(column).eq(value));
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 添加可选条件
|
||||||
|
pub fn condition_option<V>(mut self, column: impl Iden + 'static, value: Option<V>) -> Self
|
||||||
|
where
|
||||||
|
V: Into<sea_query::Value>,
|
||||||
|
{
|
||||||
|
if let Some(v) = value {
|
||||||
|
self.query.and_where(Expr::col(column).eq(v));
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 添加 LIKE 搜索条件
|
||||||
|
pub fn search_like(
|
||||||
|
mut self,
|
||||||
|
columns: Vec<impl Iden + 'static>,
|
||||||
|
search_term: Option<&str>,
|
||||||
|
) -> Self {
|
||||||
|
if let Some(term) = search_term {
|
||||||
|
let pattern = format!("%{}%", term);
|
||||||
|
let mut conditions = None;
|
||||||
|
|
||||||
|
for column in columns {
|
||||||
|
let condition = Expr::col(column).ilike(&pattern);
|
||||||
|
conditions = match conditions {
|
||||||
|
None => Some(condition),
|
||||||
|
Some(prev) => Some(prev.or(condition)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(cond) = conditions {
|
||||||
|
self.query.and_where(cond);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 添加 IN 条件
|
||||||
|
pub fn in_values<V>(mut self, column: impl Iden + 'static, values: Option<Vec<V>>) -> Self
|
||||||
|
where
|
||||||
|
V: Into<sea_query::Value>,
|
||||||
|
{
|
||||||
|
if let Some(vals) = values {
|
||||||
|
if !vals.is_empty() {
|
||||||
|
self.query.and_where(Expr::col(column).is_in(vals));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 添加日期范围条件
|
||||||
|
pub fn date_range<V>(
|
||||||
|
mut self,
|
||||||
|
column: impl Iden + Copy + 'static,
|
||||||
|
range: Option<(V, V)>,
|
||||||
|
) -> Self
|
||||||
|
where
|
||||||
|
V: Into<sea_query::Value>,
|
||||||
|
{
|
||||||
|
if let Some((start, end)) = range {
|
||||||
|
self.query
|
||||||
|
.and_where(Expr::col(column).gte(start).and(Expr::col(column).lte(end)));
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 添加排序
|
||||||
|
pub fn order_by(mut self, column: impl Iden + 'static, order: Order) -> Self {
|
||||||
|
self.query.order_by(column, order);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 添加分页
|
||||||
|
pub fn paginate(mut self, page: u64, page_size: u64) -> Self {
|
||||||
|
let offset = (page - 1) * page_size;
|
||||||
|
self.query.limit(page_size).offset(offset);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 执行查询并返回所有行
|
||||||
|
pub async fn fetch_all(self) -> Result<Vec<sqlx::postgres::PgRow>> {
|
||||||
|
let (sql, values) = self.query.build_sqlx(PostgresQueryBuilder);
|
||||||
|
let rows = sqlx::query_with(&sql, values).fetch_all(&self.pool).await?;
|
||||||
|
Ok(rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 执行查询并返回单行
|
||||||
|
pub async fn fetch_one(self) -> Result<sqlx::postgres::PgRow> {
|
||||||
|
let (sql, values) = self.query.build_sqlx(PostgresQueryBuilder);
|
||||||
|
let row = sqlx::query_with(&sql, values).fetch_one(&self.pool).await?;
|
||||||
|
Ok(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 执行查询并返回可选的单行
|
||||||
|
pub async fn fetch_optional(self) -> Result<Option<sqlx::postgres::PgRow>> {
|
||||||
|
let (sql, values) = self.query.build_sqlx(PostgresQueryBuilder);
|
||||||
|
let row = sqlx::query_with(&sql, values)
|
||||||
|
.fetch_optional(&self.pool)
|
||||||
|
.await?;
|
||||||
|
Ok(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 执行计数查询
|
||||||
|
pub async fn count(mut self) -> Result<i64> {
|
||||||
|
// 重置选择的列为计数
|
||||||
|
self.query = Query::select();
|
||||||
|
self.query.from(self.table).expr(Expr::col("*").count());
|
||||||
|
|
||||||
|
let (sql, values) = self.query.build_sqlx(PostgresQueryBuilder);
|
||||||
|
let count: i64 = sqlx::query_scalar_with(&sql, values)
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await?;
|
||||||
|
Ok(count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 更新查询构建器
|
||||||
|
pub struct UpdateQueryBuilder<T: Iden + Clone> {
|
||||||
|
pool: PgPool,
|
||||||
|
query: UpdateStatement,
|
||||||
|
_table: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Iden + Clone + 'static> UpdateQueryBuilder<T> {
|
||||||
|
fn new(pool: PgPool, table: T) -> Self {
|
||||||
|
let mut query = Query::update();
|
||||||
|
query.table(table.clone());
|
||||||
|
|
||||||
|
Self {
|
||||||
|
pool,
|
||||||
|
query,
|
||||||
|
_table: table,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 设置要更新的值
|
||||||
|
pub fn set<V>(mut self, column: impl Iden + 'static, value: V) -> Self
|
||||||
|
where
|
||||||
|
V: Into<sea_query::Value>,
|
||||||
|
{
|
||||||
|
self.query.value(column, value);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 条件性设置值
|
||||||
|
pub fn set_if<V>(mut self, condition: bool, column: impl Iden + 'static, value: V) -> Self
|
||||||
|
where
|
||||||
|
V: Into<sea_query::Value>,
|
||||||
|
{
|
||||||
|
if condition {
|
||||||
|
self.query.value(column, value);
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从 HashMap 批量设置值
|
||||||
|
pub fn set_from_map(
|
||||||
|
mut self,
|
||||||
|
updates: HashMap<impl Iden + Copy + 'static, serde_json::Value>,
|
||||||
|
) -> Self {
|
||||||
|
for (key, value) in updates {
|
||||||
|
// 这里需要根据具体的列枚举来映射字段名
|
||||||
|
// 实际使用时应该有一个字段名到列枚举的映射
|
||||||
|
match value {
|
||||||
|
serde_json::Value::String(s) => {
|
||||||
|
self.query.value(key, Expr::value(s));
|
||||||
|
}
|
||||||
|
serde_json::Value::Number(n) => {
|
||||||
|
if let Some(i) = n.as_i64() {
|
||||||
|
self.query.value(key, Expr::value(i));
|
||||||
|
} else if let Some(f) = n.as_f64() {
|
||||||
|
self.query.value(key, Expr::value(f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
serde_json::Value::Bool(b) => {
|
||||||
|
self.query.value(key, Expr::value(b));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 添加 WHERE 条件
|
||||||
|
pub fn where_eq<V>(mut self, column: impl Iden + 'static, value: V) -> Self
|
||||||
|
where
|
||||||
|
V: Into<sea_query::Value>,
|
||||||
|
{
|
||||||
|
self.query.and_where(Expr::col(column).eq(value));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 执行更新查询
|
||||||
|
pub async fn execute(self) -> Result<u64> {
|
||||||
|
let (sql, values) = self.query.build_sqlx(PostgresQueryBuilder);
|
||||||
|
let result = sqlx::query_with(&sql, values).execute(&self.pool).await?;
|
||||||
|
Ok(result.rows_affected())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 删除查询构建器
|
||||||
|
pub struct DeleteQueryBuilder<T: Iden + Clone> {
|
||||||
|
pool: PgPool,
|
||||||
|
query: DeleteStatement,
|
||||||
|
_table: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Iden + Clone + 'static> DeleteQueryBuilder<T> {
|
||||||
|
fn new(pool: PgPool, table: T) -> Self {
|
||||||
|
let mut query = Query::delete();
|
||||||
|
query.from_table(table.clone());
|
||||||
|
|
||||||
|
Self {
|
||||||
|
pool,
|
||||||
|
query,
|
||||||
|
_table: table,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 添加 WHERE 条件
|
||||||
|
pub fn where_eq<V>(mut self, column: impl Iden + 'static, value: V) -> Self
|
||||||
|
where
|
||||||
|
V: Into<sea_query::Value>,
|
||||||
|
{
|
||||||
|
self.query.and_where(Expr::col(column).eq(value));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 添加可选条件
|
||||||
|
pub fn where_option<V>(mut self, column: impl Iden + 'static, value: Option<V>) -> Self
|
||||||
|
where
|
||||||
|
V: Into<sea_query::Value>,
|
||||||
|
{
|
||||||
|
if let Some(v) = value {
|
||||||
|
self.query.and_where(Expr::col(column).eq(v));
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 执行删除查询
|
||||||
|
pub async fn execute(self) -> Result<u64> {
|
||||||
|
let (sql, values) = self.query.build_sqlx(PostgresQueryBuilder);
|
||||||
|
let result = sqlx::query_with(&sql, values).execute(&self.pool).await?;
|
||||||
|
Ok(result.rows_affected())
|
||||||
|
}
|
||||||
|
}
|
||||||
310
src/services/settings_manager.rs
Normal file
310
src/services/settings_manager.rs
Normal file
@ -0,0 +1,310 @@
|
|||||||
|
use crate::models::Setting;
|
||||||
|
use crate::services::settings_service::SettingsService;
|
||||||
|
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>>>,
|
||||||
|
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),
|
||||||
|
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())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 刷新缓存
|
||||||
|
async fn refresh_cache(&self) -> Result<()> {
|
||||||
|
let settings = self.settings_service.get_all_settings().await?;
|
||||||
|
let mut cache = self.cache.write().await;
|
||||||
|
cache.clear();
|
||||||
|
|
||||||
|
for setting in settings {
|
||||||
|
cache.insert(setting.key.clone(), setting);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut last_update = self.last_cache_update.write().await;
|
||||||
|
*last_update = std::time::Instant::now();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 检查缓存是否需要刷新
|
||||||
|
async fn should_refresh_cache(&self) -> bool {
|
||||||
|
let last_update = self.last_cache_update.read().await;
|
||||||
|
last_update.elapsed() > self.cache_ttl
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取配置值(带缓存)
|
||||||
|
async fn get_cached_setting(&self, key: &str) -> Result<Option<Setting>> {
|
||||||
|
if self.should_refresh_cache().await {
|
||||||
|
self.refresh_cache().await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cache = self.cache.read().await;
|
||||||
|
Ok(cache.get(key).cloned())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取字符串配置
|
||||||
|
pub async fn get_string(&self, key: &str) -> Result<Option<String>> {
|
||||||
|
if let Some(setting) = self.get_cached_setting(key).await? {
|
||||||
|
Ok(setting.get_string().ok())
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取字符串配置,带默认值
|
||||||
|
pub async fn get_string_or(&self, key: &str, default: &str) -> Result<String> {
|
||||||
|
Ok(self
|
||||||
|
.get_string(key)
|
||||||
|
.await?
|
||||||
|
.unwrap_or_else(|| default.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取布尔配置
|
||||||
|
pub async fn get_bool(&self, key: &str) -> Result<Option<bool>> {
|
||||||
|
if let Some(setting) = self.get_cached_setting(key).await? {
|
||||||
|
Ok(setting.get_bool().ok())
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取布尔配置,带默认值
|
||||||
|
pub async fn get_bool_or(&self, key: &str, default: bool) -> Result<bool> {
|
||||||
|
Ok(self.get_bool(key).await?.unwrap_or(default))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取数字配置
|
||||||
|
pub async fn get_number(&self, key: &str) -> Result<Option<f64>> {
|
||||||
|
if let Some(setting) = self.get_cached_setting(key).await? {
|
||||||
|
Ok(setting.get_number().ok())
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取数字配置,带默认值
|
||||||
|
pub async fn get_number_or(&self, key: &str, default: f64) -> Result<f64> {
|
||||||
|
Ok(self.get_number(key).await?.unwrap_or(default))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取整数配置
|
||||||
|
pub async fn get_int(&self, key: &str) -> Result<Option<i64>> {
|
||||||
|
if let Some(value) = self.get_number(key).await? {
|
||||||
|
Ok(Some(value as i64))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取整数配置,带默认值
|
||||||
|
pub async fn get_int_or(&self, key: &str, default: i64) -> Result<i64> {
|
||||||
|
Ok(self.get_int(key).await?.unwrap_or(default))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取JSON配置
|
||||||
|
pub async fn get_json<T>(&self, key: &str) -> Result<Option<T>>
|
||||||
|
where
|
||||||
|
T: for<'de> Deserialize<'de>,
|
||||||
|
{
|
||||||
|
if let Some(setting) = self.get_cached_setting(key).await? {
|
||||||
|
setting
|
||||||
|
.get_typed_value()
|
||||||
|
.map(Some)
|
||||||
|
.map_err(|e| anyhow::anyhow!(e))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取JSON配置,带默认值
|
||||||
|
pub async fn get_json_or<T>(&self, key: &str, default: T) -> Result<T>
|
||||||
|
where
|
||||||
|
T: for<'de> Deserialize<'de> + Clone,
|
||||||
|
{
|
||||||
|
Ok(self.get_json(key).await?.unwrap_or(default))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 设置配置值
|
||||||
|
pub async fn set_value<T>(&self, key: &str, value: &T) -> Result<()>
|
||||||
|
where
|
||||||
|
T: Serialize,
|
||||||
|
{
|
||||||
|
// 更新数据库
|
||||||
|
if let Some(setting) = self.settings_service.get_setting_by_key(key).await? {
|
||||||
|
let update_setting = crate::models::UpdateSetting {
|
||||||
|
value: Some(serde_json::to_string(value)?),
|
||||||
|
description: None,
|
||||||
|
category: None,
|
||||||
|
is_editable: None,
|
||||||
|
};
|
||||||
|
self.settings_service
|
||||||
|
.update_setting(setting.id, update_setting, uuid::Uuid::nil())
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新缓存
|
||||||
|
self.refresh_cache().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 批量设置配置值
|
||||||
|
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
|
||||||
|
.batch_update_settings(updates, uuid::Uuid::nil())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// 刷新缓存
|
||||||
|
self.refresh_cache().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 强制刷新缓存
|
||||||
|
pub async fn force_refresh_cache(&self) -> Result<()> {
|
||||||
|
self.refresh_cache().await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取所有配置
|
||||||
|
pub async fn get_all_settings(&self) -> Result<Vec<Setting>> {
|
||||||
|
if self.should_refresh_cache().await {
|
||||||
|
self.refresh_cache().await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cache = self.cache.read().await;
|
||||||
|
Ok(cache.values().cloned().collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取分类配置
|
||||||
|
pub async fn get_settings_by_category(&self, category: &str) -> Result<Vec<Setting>> {
|
||||||
|
let all_settings = self.get_all_settings().await?;
|
||||||
|
Ok(all_settings
|
||||||
|
.into_iter()
|
||||||
|
.filter(|s| s.category.as_ref().map(|c| c == category).unwrap_or(false))
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 检查配置是否存在
|
||||||
|
pub async fn has_setting(&self, key: &str) -> Result<bool> {
|
||||||
|
Ok(self.get_cached_setting(key).await?.is_some())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取配置元数据
|
||||||
|
pub async fn get_setting_metadata(&self, key: &str) -> Result<Option<Setting>> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
872
src/services/settings_service.rs
Normal file
872
src/services/settings_service.rs
Normal file
@ -0,0 +1,872 @@
|
|||||||
|
use crate::models::{CreateSetting, Setting, SettingFilter, SettingHistory, UpdateSetting};
|
||||||
|
use crate::services::query_builder::DynamicQueryBuilder;
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use sea_query::extension::postgres::PgExpr;
|
||||||
|
use sea_query::{Expr, Iden, Order, PostgresQueryBuilder, Query};
|
||||||
|
use sea_query_binder::SqlxBinder;
|
||||||
|
use serde_json::Value;
|
||||||
|
use sqlx::{PgPool, Row};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Iden, Clone)]
|
||||||
|
enum Settings {
|
||||||
|
Table,
|
||||||
|
Id,
|
||||||
|
Key,
|
||||||
|
Value,
|
||||||
|
ValueType,
|
||||||
|
Description,
|
||||||
|
Category,
|
||||||
|
IsEncrypted,
|
||||||
|
IsSystem,
|
||||||
|
IsEditable,
|
||||||
|
CreatedAt,
|
||||||
|
UpdatedAt,
|
||||||
|
CreatedBy,
|
||||||
|
UpdatedBy,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Iden, Clone)]
|
||||||
|
enum SettingsHistory {
|
||||||
|
Table,
|
||||||
|
Id,
|
||||||
|
SettingId,
|
||||||
|
OldValue,
|
||||||
|
NewValue,
|
||||||
|
ChangedBy,
|
||||||
|
ChangeReason,
|
||||||
|
CreatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SettingsService {
|
||||||
|
pool: PgPool,
|
||||||
|
query_builder: DynamicQueryBuilder,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SettingsService {
|
||||||
|
pub fn new(pool: PgPool) -> Self {
|
||||||
|
let query_builder = DynamicQueryBuilder::new(pool.clone());
|
||||||
|
Self {
|
||||||
|
pool,
|
||||||
|
query_builder,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 创建新的配置项
|
||||||
|
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>> {
|
||||||
|
let settings = 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 ORDER BY category, key
|
||||||
|
"#
|
||||||
|
)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 根据过滤条件获取配置项 (使用 sea-query 构建动态查询)
|
||||||
|
pub async fn get_settings_with_filter(&self, filter: &SettingFilter) -> Result<Vec<Setting>> {
|
||||||
|
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,
|
||||||
|
])
|
||||||
|
.from(Settings::Table);
|
||||||
|
|
||||||
|
// 动态添加过滤条件
|
||||||
|
if let Some(category) = &filter.category {
|
||||||
|
query.and_where(Expr::col(Settings::Category).eq(category));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(is_system) = &filter.is_system {
|
||||||
|
query.and_where(Expr::col(Settings::IsSystem).eq(*is_system));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(is_editable) = &filter.is_editable {
|
||||||
|
query.and_where(Expr::col(Settings::IsEditable).eq(*is_editable));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(search) = &filter.search {
|
||||||
|
let search_pattern = format!("%{}%", search);
|
||||||
|
query.and_where(
|
||||||
|
Expr::col(Settings::Key)
|
||||||
|
.ilike(&search_pattern)
|
||||||
|
.or(Expr::col(Settings::Description).ilike(&search_pattern)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加排序
|
||||||
|
query
|
||||||
|
.order_by(Settings::Category, sea_query::Order::Asc)
|
||||||
|
.order_by(Settings::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 {
|
||||||
|
id: row.get("id"),
|
||||||
|
key: row.get("key"),
|
||||||
|
value: row.get("value"),
|
||||||
|
value_type: row.get("value_type"),
|
||||||
|
description: row.get("description"),
|
||||||
|
category: row.get("category"),
|
||||||
|
is_encrypted: row.get("is_encrypted"),
|
||||||
|
is_system: row.get("is_system"),
|
||||||
|
is_editable: row.get("is_editable"),
|
||||||
|
created_at: row.get("created_at"),
|
||||||
|
updated_at: row.get("updated_at"),
|
||||||
|
created_by: row.get("created_by"),
|
||||||
|
updated_by: row.get("updated_by"),
|
||||||
|
};
|
||||||
|
settings.push(setting);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 更新配置项
|
||||||
|
pub async fn update_setting(
|
||||||
|
&self,
|
||||||
|
id: Uuid,
|
||||||
|
update_setting: UpdateSetting,
|
||||||
|
user_id: Uuid,
|
||||||
|
) -> Result<Setting> {
|
||||||
|
let setting = self.get_setting_by_id(id).await?;
|
||||||
|
let setting = setting.ok_or_else(|| anyhow!("Setting not found"))?;
|
||||||
|
|
||||||
|
if !setting.is_editable.unwrap_or(false) {
|
||||||
|
return Err(anyhow!("Setting is not editable"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let updated_setting = sqlx::query_as!(
|
||||||
|
Setting,
|
||||||
|
r#"
|
||||||
|
UPDATE settings
|
||||||
|
SET value = COALESCE($1, value),
|
||||||
|
description = COALESCE($2, description),
|
||||||
|
category = COALESCE($3, category),
|
||||||
|
is_editable = COALESCE($4, is_editable),
|
||||||
|
updated_by = $5,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = $6
|
||||||
|
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
|
||||||
|
"#,
|
||||||
|
update_setting.value,
|
||||||
|
update_setting.description,
|
||||||
|
update_setting.category,
|
||||||
|
update_setting.is_editable,
|
||||||
|
user_id,
|
||||||
|
id
|
||||||
|
)
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
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)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(result.rows_affected() > 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 批量更新配置项
|
||||||
|
pub async fn batch_update_settings(
|
||||||
|
&self,
|
||||||
|
updates: Vec<(String, Value)>,
|
||||||
|
user_id: Uuid,
|
||||||
|
) -> Result<Vec<Setting>> {
|
||||||
|
let mut updated_settings = Vec::new();
|
||||||
|
|
||||||
|
for (key, value) in updates {
|
||||||
|
if let Some(setting) = self.get_setting_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,
|
||||||
|
r#"
|
||||||
|
UPDATE settings
|
||||||
|
SET value = $1, updated_by = $2, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE key = $3
|
||||||
|
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
|
||||||
|
"#,
|
||||||
|
value_str,
|
||||||
|
user_id,
|
||||||
|
key
|
||||||
|
)
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
updated_settings.push(updated_setting);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(updated_settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取配置历史
|
||||||
|
pub async fn get_setting_history(&self, setting_id: Uuid) -> Result<Vec<SettingHistory>> {
|
||||||
|
let history = sqlx::query_as!(
|
||||||
|
SettingHistory,
|
||||||
|
r#"
|
||||||
|
SELECT id, setting_id, old_value, new_value, changed_by, change_reason,
|
||||||
|
created_at as "created_at: DateTime<Utc>"
|
||||||
|
FROM settings_history
|
||||||
|
WHERE setting_id = $1
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
"#,
|
||||||
|
setting_id
|
||||||
|
)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(history)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取配置分类列表
|
||||||
|
pub async fn get_categories(&self) -> Result<Vec<String>> {
|
||||||
|
let categories = sqlx::query!("SELECT DISTINCT category FROM settings ORDER BY category")
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(categories
|
||||||
|
.into_iter()
|
||||||
|
.map(|row| row.category.unwrap_or("".to_string()))
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取配置项数量统计
|
||||||
|
pub async fn get_settings_stats(&self) -> Result<HashMap<String, i64>> {
|
||||||
|
let stats = sqlx::query!(
|
||||||
|
r#"
|
||||||
|
SELECT
|
||||||
|
category,
|
||||||
|
COUNT(*) as count,
|
||||||
|
COUNT(CASE WHEN is_system = true THEN 1 END) as system_count,
|
||||||
|
COUNT(CASE WHEN is_editable = true THEN 1 END) as editable_count
|
||||||
|
FROM settings
|
||||||
|
GROUP BY category
|
||||||
|
ORDER BY category
|
||||||
|
"#
|
||||||
|
)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut result = HashMap::new();
|
||||||
|
for row in stats {
|
||||||
|
result.insert(
|
||||||
|
format!("{}_total", row.category.as_ref().unwrap()),
|
||||||
|
row.count.unwrap_or(0),
|
||||||
|
);
|
||||||
|
result.insert(
|
||||||
|
format!("{}_system", row.category.as_ref().unwrap()),
|
||||||
|
row.system_count.unwrap_or(0),
|
||||||
|
);
|
||||||
|
result.insert(
|
||||||
|
format!("{}_editable", row.category.as_ref().unwrap()),
|
||||||
|
row.editable_count.unwrap_or(0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 重置配置到默认值
|
||||||
|
pub async fn reset_to_defaults(&self, user_id: Uuid) -> Result<Vec<Setting>> {
|
||||||
|
// 这里可以实现重置逻辑,比如从配置文件重新加载默认值
|
||||||
|
// 暂时返回空列表
|
||||||
|
Ok(Vec::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 导出配置 (使用 sea-query 构建动态查询)
|
||||||
|
pub async fn export_settings(&self, category: Option<&str>) -> Result<Vec<Setting>> {
|
||||||
|
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,
|
||||||
|
])
|
||||||
|
.from(Settings::Table);
|
||||||
|
|
||||||
|
// 根据分类过滤
|
||||||
|
if let Some(cat) = category {
|
||||||
|
query.and_where(Expr::col(Settings::Category).eq(cat));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加排序
|
||||||
|
query
|
||||||
|
.order_by(Settings::Category, sea_query::Order::Asc)
|
||||||
|
.order_by(Settings::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 {
|
||||||
|
id: row.get("id"),
|
||||||
|
key: row.get("key"),
|
||||||
|
value: row.get("value"),
|
||||||
|
value_type: row.get("value_type"),
|
||||||
|
description: row.get("description"),
|
||||||
|
category: row.get("category"),
|
||||||
|
is_encrypted: row.get("is_encrypted"),
|
||||||
|
is_system: row.get("is_system"),
|
||||||
|
is_editable: row.get("is_editable"),
|
||||||
|
created_at: row.get("created_at"),
|
||||||
|
updated_at: row.get("updated_at"),
|
||||||
|
created_by: row.get("created_by"),
|
||||||
|
updated_by: row.get("updated_by"),
|
||||||
|
};
|
||||||
|
settings.push(setting);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 导入配置
|
||||||
|
pub async fn import_settings(
|
||||||
|
&self,
|
||||||
|
settings: Vec<CreateSetting>,
|
||||||
|
user_id: Uuid,
|
||||||
|
) -> Result<Vec<Setting>> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(imported_settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 分页查询配置项 (sea-query 分页示例)
|
||||||
|
pub async fn get_settings_paginated(
|
||||||
|
&self,
|
||||||
|
filter: &SettingFilter,
|
||||||
|
page: u64,
|
||||||
|
page_size: u64,
|
||||||
|
) -> Result<(Vec<Setting>, u64)> {
|
||||||
|
let offset = (page - 1) * page_size;
|
||||||
|
|
||||||
|
// 查询总数
|
||||||
|
let mut count_query = Query::select();
|
||||||
|
count_query
|
||||||
|
.expr(Expr::col(Settings::Id).count())
|
||||||
|
.from(Settings::Table);
|
||||||
|
|
||||||
|
// 应用过滤条件
|
||||||
|
self.apply_filter_conditions(&mut count_query, filter);
|
||||||
|
|
||||||
|
let (count_sql, count_values) = count_query.build_sqlx(PostgresQueryBuilder);
|
||||||
|
let total_count: i64 = sqlx::query_scalar_with(&count_sql, count_values)
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// 查询数据
|
||||||
|
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,
|
||||||
|
])
|
||||||
|
.from(Settings::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)
|
||||||
|
.limit(page_size)
|
||||||
|
.offset(offset);
|
||||||
|
|
||||||
|
let (sql, values) = data_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 {
|
||||||
|
id: row.get("id"),
|
||||||
|
key: row.get("key"),
|
||||||
|
value: row.get("value"),
|
||||||
|
value_type: row.get("value_type"),
|
||||||
|
description: row.get("description"),
|
||||||
|
category: row.get("category"),
|
||||||
|
is_encrypted: row.get("is_encrypted"),
|
||||||
|
is_system: row.get("is_system"),
|
||||||
|
is_editable: row.get("is_editable"),
|
||||||
|
created_at: row.get("created_at"),
|
||||||
|
updated_at: row.get("updated_at"),
|
||||||
|
created_by: row.get("created_by"),
|
||||||
|
updated_by: row.get("updated_by"),
|
||||||
|
};
|
||||||
|
settings.push(setting);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((settings, total_count as u64))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 高级搜索配置项 (复杂条件查询示例)
|
||||||
|
pub async fn advanced_search_settings(
|
||||||
|
&self,
|
||||||
|
search_term: Option<&str>,
|
||||||
|
categories: Option<Vec<String>>,
|
||||||
|
value_types: Option<Vec<String>>,
|
||||||
|
is_system: Option<bool>,
|
||||||
|
date_range: Option<(chrono::DateTime<chrono::Utc>, chrono::DateTime<chrono::Utc>)>,
|
||||||
|
) -> Result<Vec<Setting>> {
|
||||||
|
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,
|
||||||
|
])
|
||||||
|
.from(Settings::Table);
|
||||||
|
|
||||||
|
// 搜索关键词
|
||||||
|
if let Some(term) = search_term {
|
||||||
|
let search_pattern = format!("%{}%", term);
|
||||||
|
query.and_where(
|
||||||
|
Expr::col(Settings::Key)
|
||||||
|
.ilike(&search_pattern)
|
||||||
|
.or(Expr::col(Settings::Description).ilike(&search_pattern))
|
||||||
|
.or(Expr::col(Settings::Value).ilike(&search_pattern)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 多个分类
|
||||||
|
if let Some(cats) = categories {
|
||||||
|
if !cats.is_empty() {
|
||||||
|
query.and_where(Expr::col(Settings::Category).is_in(cats));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 多个值类型
|
||||||
|
if let Some(types) = value_types {
|
||||||
|
if !types.is_empty() {
|
||||||
|
query.and_where(Expr::col(Settings::ValueType).is_in(types));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 系统设置
|
||||||
|
if let Some(sys) = is_system {
|
||||||
|
query.and_where(Expr::col(Settings::IsSystem).eq(sys));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 日期范围
|
||||||
|
if let Some((start_date, end_date)) = date_range {
|
||||||
|
query.and_where(
|
||||||
|
Expr::col(Settings::CreatedAt)
|
||||||
|
.gte(start_date)
|
||||||
|
.and(Expr::col(Settings::CreatedAt).lte(end_date)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
query
|
||||||
|
.order_by(Settings::Category, sea_query::Order::Asc)
|
||||||
|
.order_by(Settings::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 {
|
||||||
|
id: row.get("id"),
|
||||||
|
key: row.get("key"),
|
||||||
|
value: row.get("value"),
|
||||||
|
value_type: row.get("value_type"),
|
||||||
|
description: row.get("description"),
|
||||||
|
category: row.get("category"),
|
||||||
|
is_encrypted: row.get("is_encrypted"),
|
||||||
|
is_system: row.get("is_system"),
|
||||||
|
is_editable: row.get("is_editable"),
|
||||||
|
created_at: row.get("created_at"),
|
||||||
|
updated_at: row.get("updated_at"),
|
||||||
|
created_by: row.get("created_by"),
|
||||||
|
updated_by: row.get("updated_by"),
|
||||||
|
};
|
||||||
|
settings.push(setting);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 批量操作 (动态更新多个配置项)
|
||||||
|
pub async fn batch_update_by_conditions(
|
||||||
|
&self,
|
||||||
|
conditions: &SettingFilter,
|
||||||
|
updates: HashMap<String, serde_json::Value>,
|
||||||
|
user_id: Uuid,
|
||||||
|
) -> Result<u64> {
|
||||||
|
// 构建更新查询
|
||||||
|
let mut update_query = Query::update();
|
||||||
|
update_query.table(Settings::Table);
|
||||||
|
|
||||||
|
// 添加更新字段
|
||||||
|
for (field, value) in updates {
|
||||||
|
match field.as_str() {
|
||||||
|
"description" => {
|
||||||
|
if let Some(desc) = value.as_str() {
|
||||||
|
update_query.value(Settings::Description, desc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"category" => {
|
||||||
|
if let Some(cat) = value.as_str() {
|
||||||
|
update_query.value(Settings::Category, cat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"is_editable" => {
|
||||||
|
if let Some(editable) = value.as_bool() {
|
||||||
|
update_query.value(Settings::IsEditable, editable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {} // 忽略未知字段
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update_query.value(Settings::UpdatedBy, user_id);
|
||||||
|
|
||||||
|
// 应用过滤条件
|
||||||
|
self.apply_filter_conditions(&mut update_query, conditions);
|
||||||
|
|
||||||
|
let (sql, values) = update_query.build_sqlx(PostgresQueryBuilder);
|
||||||
|
let result = sqlx::query_with(&sql, values).execute(&self.pool).await?;
|
||||||
|
|
||||||
|
Ok(result.rows_affected())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 辅助方法:应用过滤条件到查询
|
||||||
|
fn apply_filter_conditions<T>(&self, query: &mut T, filter: &SettingFilter)
|
||||||
|
where
|
||||||
|
T: sea_query::QueryStatementWriter + sea_query::ConditionalStatement,
|
||||||
|
{
|
||||||
|
if let Some(category) = &filter.category {
|
||||||
|
query.and_where(Expr::col(Settings::Category).eq(category));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(is_system) = &filter.is_system {
|
||||||
|
query.and_where(Expr::col(Settings::IsSystem).eq(*is_system));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(is_editable) = &filter.is_editable {
|
||||||
|
query.and_where(Expr::col(Settings::IsEditable).eq(*is_editable));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(search) = &filter.search {
|
||||||
|
let search_pattern = format!("%{}%", search);
|
||||||
|
query.and_where(
|
||||||
|
Expr::col(Settings::Key)
|
||||||
|
.ilike(&search_pattern)
|
||||||
|
.or(Expr::col(Settings::Description).ilike(&search_pattern)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 使用查询构建器的简化过滤查询示例
|
||||||
|
pub async fn get_settings_with_builder(&self, filter: &SettingFilter) -> Result<Vec<Setting>> {
|
||||||
|
let rows = self
|
||||||
|
.query_builder
|
||||||
|
.select(Settings::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,
|
||||||
|
])
|
||||||
|
.condition_option(Settings::Category, filter.category.clone())
|
||||||
|
.condition_option(Settings::IsSystem, filter.is_system)
|
||||||
|
.condition_option(Settings::IsEditable, filter.is_editable)
|
||||||
|
.search_like(
|
||||||
|
vec![Settings::Key, Settings::Description],
|
||||||
|
filter.search.as_deref(),
|
||||||
|
)
|
||||||
|
.order_by(Settings::Category, Order::Asc)
|
||||||
|
.order_by(Settings::Key, Order::Asc)
|
||||||
|
.fetch_all()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut settings = Vec::new();
|
||||||
|
for row in rows {
|
||||||
|
let setting = Setting {
|
||||||
|
id: row.get("id"),
|
||||||
|
key: row.get("key"),
|
||||||
|
value: row.get("value"),
|
||||||
|
value_type: row.get("value_type"),
|
||||||
|
description: row.get("description"),
|
||||||
|
category: row.get("category"),
|
||||||
|
is_encrypted: row.get("is_encrypted"),
|
||||||
|
is_system: row.get("is_system"),
|
||||||
|
is_editable: row.get("is_editable"),
|
||||||
|
created_at: row.get("created_at"),
|
||||||
|
updated_at: row.get("updated_at"),
|
||||||
|
created_by: row.get("created_by"),
|
||||||
|
updated_by: row.get("updated_by"),
|
||||||
|
};
|
||||||
|
settings.push(setting);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 使用查询构建器的分页查询示例
|
||||||
|
pub async fn get_settings_paginated_with_builder(
|
||||||
|
&self,
|
||||||
|
filter: &SettingFilter,
|
||||||
|
page: u64,
|
||||||
|
page_size: u64,
|
||||||
|
) -> Result<(Vec<Setting>, 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)
|
||||||
|
.search_like(
|
||||||
|
vec![Settings::Key, Settings::Description],
|
||||||
|
filter.search.as_deref(),
|
||||||
|
)
|
||||||
|
.count()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// 获取分页数据
|
||||||
|
let rows = self
|
||||||
|
.query_builder
|
||||||
|
.select(Settings::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,
|
||||||
|
])
|
||||||
|
.condition_option(Settings::Category, filter.category.clone())
|
||||||
|
.condition_option(Settings::IsSystem, filter.is_system)
|
||||||
|
.condition_option(Settings::IsEditable, filter.is_editable)
|
||||||
|
.search_like(
|
||||||
|
vec![Settings::Key, Settings::Description],
|
||||||
|
filter.search.as_deref(),
|
||||||
|
)
|
||||||
|
.order_by(Settings::Category, Order::Asc)
|
||||||
|
.order_by(Settings::Key, Order::Asc)
|
||||||
|
.paginate(page, page_size)
|
||||||
|
.fetch_all()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut settings = Vec::new();
|
||||||
|
for row in rows {
|
||||||
|
let setting = Setting {
|
||||||
|
id: row.get("id"),
|
||||||
|
key: row.get("key"),
|
||||||
|
value: row.get("value"),
|
||||||
|
value_type: row.get("value_type"),
|
||||||
|
description: row.get("description"),
|
||||||
|
category: row.get("category"),
|
||||||
|
is_encrypted: row.get("is_encrypted"),
|
||||||
|
is_system: row.get("is_system"),
|
||||||
|
is_editable: row.get("is_editable"),
|
||||||
|
created_at: row.get("created_at"),
|
||||||
|
updated_at: row.get("updated_at"),
|
||||||
|
created_by: row.get("created_by"),
|
||||||
|
updated_by: row.get("updated_by"),
|
||||||
|
};
|
||||||
|
settings.push(setting);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((settings, total_count))
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user