sync: add settings
Some checks are pending
Docker Build and Push / build (push) Waiting to run

This commit is contained in:
Tsuki 2025-08-11 00:05:52 +08:00
parent 3e90e2fa9f
commit d4448c6129
26 changed files with 3393 additions and 3 deletions

3
.env
View File

@ -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="
RUST_LOG=debug
PORT=3050

View 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"
}

View 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"
}

View 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"
}

View File

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

View 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"
}

View 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"
}

View 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"
}

View 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"
}

View 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"
}

View 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"
}

View 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
View File

@ -131,6 +131,12 @@ dependencies = [
"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]]
name = "argon2"
version = "0.5.3"
@ -881,7 +887,7 @@ version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom",
"nom 7.1.3",
]
[[package]]
@ -911,6 +917,16 @@ dependencies = [
"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]]
name = "clang-sys"
version = "1.8.1"
@ -1217,6 +1233,22 @@ dependencies = [
"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]]
name = "encoding_rs"
version = "0.8.35"
@ -1605,6 +1637,10 @@ name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [
"ahash 0.8.12",
"allocator-api2",
]
[[package]]
name = "hashbrown"
@ -1717,6 +1753,17 @@ dependencies = [
"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]]
name = "http"
version = "1.3.1"
@ -2122,6 +2169,31 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "libc"
version = "0.2.174"
@ -2229,6 +2301,7 @@ dependencies = [
name = "mapp"
version = "0.1.0"
dependencies = [
"anyhow",
"argon2",
"async-graphql",
"async-graphql-axum",
@ -2242,6 +2315,7 @@ dependencies = [
"dotenvy",
"futures-util",
"jsonwebtoken",
"lettre",
"rdkafka",
"rustls",
"sea-query",
@ -2383,6 +2457,15 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "nom"
version = "8.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
dependencies = [
"memchr",
]
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
@ -2790,6 +2873,15 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "psm"
version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e944464ec8536cd1beb0bbfd96987eb5e3b72f2ecdafdc5c769a37f1fa2ae1f"
dependencies = [
"cc",
]
[[package]]
name = "ptr_meta"
version = "0.1.4"
@ -2874,6 +2966,12 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "quoted_printable"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73"
[[package]]
name = "r-efi"
version = "5.3.0"
@ -3544,6 +3642,16 @@ dependencies = [
"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]]
name = "spin"
version = "0.9.8"
@ -3781,6 +3889,19 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "static_assertions_next"
version = "1.1.2"

View File

@ -49,4 +49,6 @@ rustls = { version = "0.23", features = ["aws-lc-rs"] }
clap = { version = "4.0", features = ["derive"] }
rdkafka = "0.38.0"
axum_gcra = "0.1.1"
lettre = "0.11.18"
anyhow = "1.0.98"

448
docs/SETTINGS_SYSTEM.md Normal file
View 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
- 配置历史记录和审计
- 类型安全的配置访问
- 易于扩展和维护
通过合理使用这个系统,可以大大简化应用程序的配置管理,提高系统的可维护性和灵活性。

View 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();

View File

@ -26,6 +26,7 @@ use crate::{
services::{
invite_code_service::InviteCodeService, mosaic_service::MosaicService,
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 system_config_service = SystemConfigService::new(pool.clone());
let mosaic_service = MosaicService::new(pool.clone());
let settings_service = SettingsService::new(pool.clone());
let schema = Schema::build(QueryRoot, MutationRoot, SubscriptionRoot)
.data(pool)
@ -57,6 +59,7 @@ pub fn create_router(
.data(invite_code_service)
.data(system_config_service)
.data(mosaic_service)
.data(settings_service)
.data(config.clone())
.data(status_sender.clone())
.finish();

View File

@ -3,12 +3,16 @@ use crate::graphql::guards::RequireRole;
use crate::graphql::types::{
CreateInviteCodeInput, CreateUserInput, InitializeAdminInput, InitializeAdminResponse,
InviteCodeResponse, LoginInput, LoginResponse, RegisterInput,
CreateSettingInput, UpdateSettingInput, BatchUpdateSettingsInput, SettingType,
};
use crate::models::user::Role;
use crate::models::user::User;
use crate::models::settings::{CreateSetting, UpdateSetting};
use crate::services::invite_code_service::InviteCodeService;
use crate::services::user_service::UserService;
use crate::services::settings_service::SettingsService;
use async_graphql::{Context, Object, Result};
use uuid::Uuid;
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())
}
}

View File

@ -1,12 +1,17 @@
use crate::auth::get_auth_user;
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::settings::{SettingFilter, SettingHistory};
use crate::models::user::{Role, User, UserInfoRow};
use crate::services::invite_code_service::InviteCodeService;
use crate::services::settings_service::SettingsService;
use crate::services::user_service::UserService;
use async_graphql::{Context, Object, Result};
use tracing::info;
use uuid::Uuid;
pub struct QueryRoot;
@ -90,4 +95,128 @@ impl QueryRoot {
.users_info(offset, limit, sort_by, sort_order, filter)
.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())
}
}

View File

@ -1,5 +1,10 @@
use crate::models::settings::{
CreateSetting, Setting, SettingFilter, SettingHistory, UpdateSetting,
};
use crate::models::user::Role;
use async_graphql::{InputObject, SimpleObject};
use serde_json::Value;
use uuid::Uuid;
#[derive(InputObject)]
pub struct RegisterInput {
@ -70,3 +75,77 @@ pub struct UserInfoRespnose {
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,
}

View File

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

161
src/models/settings.rs Normal file
View 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,
}
}
}

View File

@ -1,4 +1,7 @@
pub mod invite_code_service;
pub mod mosaic_service;
pub mod query_builder;
pub mod settings_manager;
pub mod settings_service;
pub mod system_config_service;
pub mod user_service;

View 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())
}
}

View 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)
}
}

View 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))
}
}