diff --git a/.env b/.env index 86e2ebf..0143182 100644 --- a/.env +++ b/.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=" RUST_LOG=debug PORT=3050 diff --git a/.sqlx/query-035b45ec1dafdf984b6e4a0e935a5820278730cfe3253144cb73322fc7e9bff5.json b/.sqlx/query-035b45ec1dafdf984b6e4a0e935a5820278730cfe3253144cb73322fc7e9bff5.json new file mode 100644 index 0000000..c94f796 --- /dev/null +++ b/.sqlx/query-035b45ec1dafdf984b6e4a0e935a5820278730cfe3253144cb73322fc7e9bff5.json @@ -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\"\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", + "type_info": "Timestamp" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + true, + true, + true, + true, + false + ] + }, + "hash": "035b45ec1dafdf984b6e4a0e935a5820278730cfe3253144cb73322fc7e9bff5" +} diff --git a/.sqlx/query-19c23596ea8098fdf9a13f4a4f033e646282fc1df35afa27dd2b1b91dec607cf.json b/.sqlx/query-19c23596ea8098fdf9a13f4a4f033e646282fc1df35afa27dd2b1b91dec607cf.json new file mode 100644 index 0000000..cd05a0b --- /dev/null +++ b/.sqlx/query-19c23596ea8098fdf9a13f4a4f033e646282fc1df35afa27dd2b1b91dec607cf.json @@ -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\", \n updated_at as \"updated_at: DateTime\", \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", + "type_info": "Timestamp" + }, + { + "ordinal": 10, + "name": "updated_at: DateTime", + "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" +} diff --git a/.sqlx/query-337689745b15c94365b9ab55c4298abfd212f5165628caf6d3549c321bdd8584.json b/.sqlx/query-337689745b15c94365b9ab55c4298abfd212f5165628caf6d3549c321bdd8584.json new file mode 100644 index 0000000..cf1c5c1 --- /dev/null +++ b/.sqlx/query-337689745b15c94365b9ab55c4298abfd212f5165628caf6d3549c321bdd8584.json @@ -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\", \n updated_at as \"updated_at: DateTime\", \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", + "type_info": "Timestamp" + }, + { + "ordinal": 10, + "name": "updated_at: DateTime", + "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" +} diff --git a/.sqlx/query-431c4c6620a596862ed42ca0e402b093bd4c1d13f14f906d00b2b37c832f48ca.json b/.sqlx/query-431c4c6620a596862ed42ca0e402b093bd4c1d13f14f906d00b2b37c832f48ca.json new file mode 100644 index 0000000..ddb27ff --- /dev/null +++ b/.sqlx/query-431c4c6620a596862ed42ca0e402b093bd4c1d13f14f906d00b2b37c832f48ca.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM settings WHERE id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "431c4c6620a596862ed42ca0e402b093bd4c1d13f14f906d00b2b37c832f48ca" +} diff --git a/.sqlx/query-6bab42214f2c28a3985afa5ce4bbb5377b96d7b28490db0be5d775a67a8cfcdb.json b/.sqlx/query-6bab42214f2c28a3985afa5ce4bbb5377b96d7b28490db0be5d775a67a8cfcdb.json new file mode 100644 index 0000000..2cefba2 --- /dev/null +++ b/.sqlx/query-6bab42214f2c28a3985afa5ce4bbb5377b96d7b28490db0be5d775a67a8cfcdb.json @@ -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\", \n updated_at as \"updated_at: DateTime\", \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", + "type_info": "Timestamp" + }, + { + "ordinal": 10, + "name": "updated_at: DateTime", + "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" +} diff --git a/.sqlx/query-82c99538d54085bc56abe4fb049a5076162d7f505ce08784769b359d8f302938.json b/.sqlx/query-82c99538d54085bc56abe4fb049a5076162d7f505ce08784769b359d8f302938.json new file mode 100644 index 0000000..2cc64d5 --- /dev/null +++ b/.sqlx/query-82c99538d54085bc56abe4fb049a5076162d7f505ce08784769b359d8f302938.json @@ -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\", \n updated_at as \"updated_at: DateTime\", \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", + "type_info": "Timestamp" + }, + { + "ordinal": 10, + "name": "updated_at: DateTime", + "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" +} diff --git a/.sqlx/query-8fe2f86e9388d6ed22b1b3ac4c595b672dff25aec6fb85120366dd4341a70a3b.json b/.sqlx/query-8fe2f86e9388d6ed22b1b3ac4c595b672dff25aec6fb85120366dd4341a70a3b.json new file mode 100644 index 0000000..8b1a298 --- /dev/null +++ b/.sqlx/query-8fe2f86e9388d6ed22b1b3ac4c595b672dff25aec6fb85120366dd4341a70a3b.json @@ -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\", \n updated_at as \"updated_at: DateTime\", \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", + "type_info": "Timestamp" + }, + { + "ordinal": 10, + "name": "updated_at: DateTime", + "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" +} diff --git a/.sqlx/query-93734fffdd102bda19488e703012bdad22bf3675a9b2dbeaf7943c9d0341eaf2.json b/.sqlx/query-93734fffdd102bda19488e703012bdad22bf3675a9b2dbeaf7943c9d0341eaf2.json new file mode 100644 index 0000000..ccc10d8 --- /dev/null +++ b/.sqlx/query-93734fffdd102bda19488e703012bdad22bf3675a9b2dbeaf7943c9d0341eaf2.json @@ -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\", \n updated_at as \"updated_at: DateTime\", \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", + "type_info": "Timestamp" + }, + { + "ordinal": 10, + "name": "updated_at: DateTime", + "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" +} diff --git a/.sqlx/query-9daeb02358c339adce84ed11cacc7489ab9ad3c009ca40557d535f5c498704ba.json b/.sqlx/query-9daeb02358c339adce84ed11cacc7489ab9ad3c009ca40557d535f5c498704ba.json new file mode 100644 index 0000000..9ac40cf --- /dev/null +++ b/.sqlx/query-9daeb02358c339adce84ed11cacc7489ab9ad3c009ca40557d535f5c498704ba.json @@ -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" +} diff --git a/.sqlx/query-a9bf29176e34c2b075611cbe3cecfad3e7132347cfa5df1180bbb2a67e1fbf79.json b/.sqlx/query-a9bf29176e34c2b075611cbe3cecfad3e7132347cfa5df1180bbb2a67e1fbf79.json new file mode 100644 index 0000000..c58a24e --- /dev/null +++ b/.sqlx/query-a9bf29176e34c2b075611cbe3cecfad3e7132347cfa5df1180bbb2a67e1fbf79.json @@ -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" +} diff --git a/.sqlx/query-fbdc7145f7560c576e3e5db15fe014bcff5ce490b18ab012fe6f8cfcff14bac6.json b/.sqlx/query-fbdc7145f7560c576e3e5db15fe014bcff5ce490b18ab012fe6f8cfcff14bac6.json new file mode 100644 index 0000000..b0cd99c --- /dev/null +++ b/.sqlx/query-fbdc7145f7560c576e3e5db15fe014bcff5ce490b18ab012fe6f8cfcff14bac6.json @@ -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" +} diff --git a/Cargo.lock b/Cargo.lock index 102694f..33920f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index eab0a8e..d7592b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/docs/SETTINGS_SYSTEM.md b/docs/SETTINGS_SYSTEM.md new file mode 100644 index 0000000..92c2a11 --- /dev/null +++ b/docs/SETTINGS_SYSTEM.md @@ -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> { + // 获取应用配置 + 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> { + // 监听配置变化 + 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> { + 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 { + 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> { + 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 +- 配置历史记录和审计 +- 类型安全的配置访问 +- 易于扩展和维护 + +通过合理使用这个系统,可以大大简化应用程序的配置管理,提高系统的可维护性和灵活性。 \ No newline at end of file diff --git a/migrations/006_create_settings.sql b/migrations/006_create_settings.sql new file mode 100644 index 0000000..fa4b9c5 --- /dev/null +++ b/migrations/006_create_settings.sql @@ -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(); \ No newline at end of file diff --git a/src/app.rs b/src/app.rs index f8bc9c2..3160eae 100644 --- a/src/app.rs +++ b/src/app.rs @@ -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(); diff --git a/src/graphql/mutation.rs b/src/graphql/mutation.rs index f96a0e5..998615b 100644 --- a/src/graphql/mutation.rs +++ b/src/graphql/mutation.rs @@ -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 { + let auth_user = get_auth_user(ctx).await?; + let settings_service = ctx.data::()?; + + 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 { + let auth_user = get_auth_user(ctx).await?; + let settings_service = ctx.data::()?; + + 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 { + let settings_service = ctx.data::()?; + 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> { + let auth_user = get_auth_user(ctx).await?; + let settings_service = ctx.data::()?; + + 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()) + } } diff --git a/src/graphql/query.rs b/src/graphql/query.rs index b3797be..35c434a 100644 --- a/src/graphql/query.rs +++ b/src/graphql/query.rs @@ -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, + ) -> Result> { + let settings_service = ctx.data::()?; + 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> { + let settings_service = ctx.data::()?; + 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> { + let settings_service = ctx.data::()?; + 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> { + let settings_service = ctx.data::()?; + let categories = settings_service.get_categories().await?; + Ok(categories) + } + + #[graphql(guard = "RequireRole(Role::Admin)")] + async fn settings_stats(&self, ctx: &Context<'_>) -> Result { + let settings_service = ctx.data::()?; + 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> { + let settings_service = ctx.data::()?; + 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()) + } } diff --git a/src/graphql/types.rs b/src/graphql/types.rs index 1374b9a..2345d54 100644 --- a/src/graphql/types.rs +++ b/src/graphql/types.rs @@ -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, } + +// Settings GraphQL types +#[derive(SimpleObject)] +pub struct SettingType { + pub id: Uuid, + pub key: String, + pub value: Option, + pub value_type: String, + pub description: Option, + pub category: Option, + pub is_encrypted: Option, + pub is_system: Option, + pub is_editable: Option, + pub created_at: chrono::DateTime, + pub updated_at: chrono::DateTime, + pub created_by: Option, + pub updated_by: Option, +} + +#[derive(InputObject)] +pub struct CreateSettingInput { + pub key: String, + pub value: Option, + pub value_type: String, + pub description: Option, + pub category: String, + pub is_encrypted: Option, + pub is_system: Option, + pub is_editable: Option, +} + +#[derive(InputObject)] +pub struct UpdateSettingInput { + pub value: Option, + pub description: Option, + pub category: Option, + pub is_editable: Option, +} + +#[derive(InputObject)] +pub struct SettingFilterInput { + pub category: Option, + pub is_system: Option, + pub is_editable: Option, + pub search: Option, +} + +#[derive(SimpleObject)] +pub struct SettingHistoryType { + pub id: Uuid, + pub setting_id: Uuid, + pub old_value: Option, + pub new_value: Option, + pub changed_by: Option, + pub change_reason: Option, + pub created_at: chrono::DateTime, +} + +#[derive(SimpleObject)] +pub struct SettingsStatsType { + pub categories: Vec, + pub stats: std::collections::HashMap, +} + +#[derive(InputObject)] +pub struct BatchUpdateSettingsInput { + pub updates: Vec, +} + +#[derive(InputObject)] +pub struct BatchUpdateItemInput { + pub key: String, + pub value: Value, +} diff --git a/src/models/mod.rs b/src/models/mod.rs index f490ce1..18fd4b4 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -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::*; diff --git a/src/models/settings.rs b/src/models/settings.rs new file mode 100644 index 0000000..6750ee0 --- /dev/null +++ b/src/models/settings.rs @@ -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, + pub value_type: String, + pub description: Option, + pub category: Option, + pub is_encrypted: Option, + pub is_system: Option, + pub is_editable: Option, + pub created_at: DateTime, + pub updated_at: DateTime, + pub created_by: Option, + pub updated_by: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CreateSetting { + pub key: String, + pub value: Option, + pub value_type: String, + pub description: Option, + pub category: String, + pub is_encrypted: Option, + pub is_system: Option, + pub is_editable: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UpdateSetting { + pub value: Option, + pub description: Option, + pub category: Option, + pub is_editable: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SettingValue { + pub key: String, + pub value: serde_json::Value, + pub value_type: String, + pub description: Option, + pub category: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SettingFilter { + pub category: Option, + pub is_system: Option, + pub is_editable: Option, + pub search: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SettingHistory { + pub id: Uuid, + pub setting_id: Uuid, + pub old_value: Option, + pub new_value: Option, + pub changed_by: Option, + pub change_reason: Option, + pub created_at: DateTime, +} + +impl Setting { + /// 获取配置值的类型化版本 + pub fn get_typed_value(&self) -> Result + 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(&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 { + if self.value_type == "boolean" { + self.value + .as_ref() + .and_then(|v| v.parse::().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 { + if self.value_type == "number" { + self.value + .as_ref() + .and_then(|v| v.parse::().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 { + 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 { + 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, + } + } +} diff --git a/src/services/mod.rs b/src/services/mod.rs index 31c88d0..9dbd292 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -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; diff --git a/src/services/query_builder.rs b/src/services/query_builder.rs new file mode 100644 index 0000000..0ad1dbe --- /dev/null +++ b/src/services/query_builder.rs @@ -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(&self, table: T) -> SelectQueryBuilder { + SelectQueryBuilder::new(self.pool.clone(), table) + } + + /// 构建动态的更新查询 + pub fn update(&self, table: T) -> UpdateQueryBuilder { + UpdateQueryBuilder::new(self.pool.clone(), table) + } + + /// 构建动态的删除查询 + pub fn delete(&self, table: T) -> DeleteQueryBuilder { + DeleteQueryBuilder::new(self.pool.clone(), table) + } +} + +/// 选择查询构建器 +pub struct SelectQueryBuilder { + pool: PgPool, + query: SelectStatement, + table: T, +} + +impl SelectQueryBuilder { + fn new(pool: PgPool, table: T) -> Self { + let mut query = Query::select(); + query.from(table.clone()); + + Self { pool, query, table } + } + + /// 添加选择的列 + pub fn columns( + mut self, + columns: impl IntoIterator, + ) -> Self { + self.query.columns(columns); + self + } + + /// 添加条件 + pub fn condition_if(mut self, condition: bool, column: impl Iden + 'static, value: V) -> Self + where + V: Into, + { + if condition { + self.query.and_where(Expr::col(column).eq(value)); + } + self + } + + /// 添加可选条件 + pub fn condition_option(mut self, column: impl Iden + 'static, value: Option) -> Self + where + V: Into, + { + if let Some(v) = value { + self.query.and_where(Expr::col(column).eq(v)); + } + self + } + + /// 添加 LIKE 搜索条件 + pub fn search_like( + mut self, + columns: Vec, + 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(mut self, column: impl Iden + 'static, values: Option>) -> Self + where + V: Into, + { + if let Some(vals) = values { + if !vals.is_empty() { + self.query.and_where(Expr::col(column).is_in(vals)); + } + } + self + } + + /// 添加日期范围条件 + pub fn date_range( + mut self, + column: impl Iden + Copy + 'static, + range: Option<(V, V)>, + ) -> Self + where + V: Into, + { + 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> { + 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 { + 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> { + 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 { + // 重置选择的列为计数 + 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 { + pool: PgPool, + query: UpdateStatement, + _table: T, +} + +impl UpdateQueryBuilder { + fn new(pool: PgPool, table: T) -> Self { + let mut query = Query::update(); + query.table(table.clone()); + + Self { + pool, + query, + _table: table, + } + } + + /// 设置要更新的值 + pub fn set(mut self, column: impl Iden + 'static, value: V) -> Self + where + V: Into, + { + self.query.value(column, value); + self + } + + /// 条件性设置值 + pub fn set_if(mut self, condition: bool, column: impl Iden + 'static, value: V) -> Self + where + V: Into, + { + if condition { + self.query.value(column, value); + } + self + } + + /// 从 HashMap 批量设置值 + pub fn set_from_map( + mut self, + updates: HashMap, + ) -> 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(mut self, column: impl Iden + 'static, value: V) -> Self + where + V: Into, + { + self.query.and_where(Expr::col(column).eq(value)); + self + } + + /// 执行更新查询 + pub async fn execute(self) -> Result { + 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 { + pool: PgPool, + query: DeleteStatement, + _table: T, +} + +impl DeleteQueryBuilder { + 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(mut self, column: impl Iden + 'static, value: V) -> Self + where + V: Into, + { + self.query.and_where(Expr::col(column).eq(value)); + self + } + + /// 添加可选条件 + pub fn where_option(mut self, column: impl Iden + 'static, value: Option) -> Self + where + V: Into, + { + if let Some(v) = value { + self.query.and_where(Expr::col(column).eq(v)); + } + self + } + + /// 执行删除查询 + pub async fn execute(self) -> Result { + let (sql, values) = self.query.build_sqlx(PostgresQueryBuilder); + let result = sqlx::query_with(&sql, values).execute(&self.pool).await?; + Ok(result.rows_affected()) + } +} diff --git a/src/services/settings_manager.rs b/src/services/settings_manager.rs new file mode 100644 index 0000000..87700e6 --- /dev/null +++ b/src/services/settings_manager.rs @@ -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, + cache: Arc>>, + cache_ttl: std::time::Duration, + last_cache_update: Arc>, +} + +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> { + 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> { + 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 { + Ok(self + .get_string(key) + .await? + .unwrap_or_else(|| default.to_string())) + } + + /// 获取布尔配置 + pub async fn get_bool(&self, key: &str) -> Result> { + 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 { + Ok(self.get_bool(key).await?.unwrap_or(default)) + } + + /// 获取数字配置 + pub async fn get_number(&self, key: &str) -> Result> { + 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 { + Ok(self.get_number(key).await?.unwrap_or(default)) + } + + /// 获取整数配置 + pub async fn get_int(&self, key: &str) -> Result> { + 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 { + Ok(self.get_int(key).await?.unwrap_or(default)) + } + + /// 获取JSON配置 + pub async fn get_json(&self, key: &str) -> Result> + 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(&self, key: &str, default: T) -> Result + where + T: for<'de> Deserialize<'de> + Clone, + { + Ok(self.get_json(key).await?.unwrap_or(default)) + } + + /// 设置配置值 + pub async fn set_value(&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) -> 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> { + 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> { + 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 { + Ok(self.get_cached_setting(key).await?.is_some()) + } + + /// 获取配置元数据 + pub async fn get_setting_metadata(&self, key: &str) -> Result> { + 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> { + 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> { + 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) + } +} diff --git a/src/services/settings_service.rs b/src/services/settings_service.rs new file mode 100644 index 0000000..e090845 --- /dev/null +++ b/src/services/settings_service.rs @@ -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 { + // 检查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", + updated_at as "updated_at: DateTime", + 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> { + 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", + updated_at as "updated_at: DateTime", + 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> { + 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", + updated_at as "updated_at: DateTime", + 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> { + 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", + updated_at as "updated_at: DateTime", + 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> { + 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 { + 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", + updated_at as "updated_at: DateTime", + 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 { + 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> { + 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", + updated_at as "updated_at: DateTime", + 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> { + 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" + 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> { + 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> { + 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> { + // 这里可以实现重置逻辑,比如从配置文件重新加载默认值 + // 暂时返回空列表 + Ok(Vec::new()) + } + + /// 导出配置 (使用 sea-query 构建动态查询) + pub async fn export_settings(&self, category: Option<&str>) -> Result> { + 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, + user_id: Uuid, + ) -> Result> { + 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, 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>, + value_types: Option>, + is_system: Option, + date_range: Option<(chrono::DateTime, chrono::DateTime)>, + ) -> Result> { + 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, + user_id: Uuid, + ) -> Result { + // 构建更新查询 + 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(&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> { + 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, 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)) + } +}