1use clap::{Args, Subcommand};
10use sz_rust_core::orm::Connection;
11use sz_rust_orm_facade::Value;
12use tabled::{Table, Tabled};
13
14use crate::error::CliError;
15
16#[derive(Subcommand, Debug)]
18pub enum AdminCommand {
19 Migrate(MigrateArgs),
21 ListRoutes,
23 ListCapabilities,
25 Init(InitArgs),
27}
28
29#[derive(Args, Debug)]
31pub struct MigrateArgs {
32 #[arg(long, default_value = "postgres")]
34 db_type: String,
35 #[arg(long)]
37 url: Option<String>,
38 #[arg(long)]
40 show_sql: bool,
41}
42
43#[derive(Args, Debug)]
45pub struct InitArgs {
46 #[arg(long, default_value = "postgres")]
48 db_type: String,
49 #[arg(long)]
51 url: Option<String>,
52 #[arg(long, default_value = "admin")]
54 username: String,
55 #[arg(long, default_value = "admin123")]
57 password: String,
58}
59
60#[derive(Tabled)]
61struct RouteRow {
62 method: &'static str,
63 path: &'static str,
64 description: &'static str,
65}
66
67#[derive(Tabled)]
68struct CapabilityRow {
69 name: &'static str,
70 description: &'static str,
71 tags: &'static str,
72}
73
74const ROUTES: &[(&str, &str, &str)] = &[
75 ("GET", "/api/admin/users", "用户列表"),
76 ("POST", "/api/admin/users", "创建用户"),
77 ("PUT", "/api/admin/users/{user_id}", "更新用户"),
78 ("DELETE", "/api/admin/users/{user_id}", "删除用户"),
79 ("PUT", "/api/admin/users/{user_id}/status", "更新用户状态"),
80 ("PUT", "/api/admin/users/{user_id}/roles", "分配用户角色"),
81 ("GET", "/api/admin/roles", "角色列表"),
82 ("POST", "/api/admin/roles", "创建角色"),
83 ("PUT", "/api/admin/roles/{role_id}", "更新角色"),
84 ("DELETE", "/api/admin/roles/{role_id}", "删除角色"),
85 (
86 "PUT",
87 "/api/admin/roles/{role_id}/permissions",
88 "分配角色权限",
89 ),
90 ("GET", "/api/admin/permissions/tree", "权限树"),
91 ("GET", "/api/admin/menus/tree", "菜单树"),
92 ("POST", "/api/admin/menus", "创建菜单"),
93 ("PUT", "/api/admin/menus/{menu_id}", "更新菜单"),
94 ("DELETE", "/api/admin/menus/{menu_id}", "删除菜单"),
95 ("GET", "/api/admin/configs", "配置列表"),
96 ("PUT", "/api/admin/configs/{config_key}", "更新配置"),
97 ("DELETE", "/api/admin/configs/{config_key}", "删除配置"),
98 ("GET", "/api/admin/operation-logs", "操作日志列表"),
99 ("GET", "/api/admin/dashboard", "仪表盘统计"),
100];
101
102const CAPABILITIES: &[(&str, &str, &str)] = &[
103 ("admin.user_list", "用户列表查询", "user,query"),
104 ("admin.user_create", "创建用户", "user,create"),
105 ("admin.user_update", "更新用户", "user,update"),
106 ("admin.user_delete", "删除用户", "user,delete"),
107 ("admin.role_list", "角色列表查询", "role,query"),
108 ("admin.role_create", "创建角色", "role,create"),
109 ("admin.role_update", "更新角色", "role,update"),
110 ("admin.role_delete", "删除角色", "role,delete"),
111 ("admin.permission_tree", "权限树查询", "permission,query"),
112 ("admin.menu_tree", "菜单树查询", "menu,query"),
113 ("admin.menu_create", "创建菜单", "menu,create"),
114 ("admin.menu_update", "更新菜单", "menu,update"),
115 ("admin.menu_delete", "删除菜单", "menu,delete"),
116 ("admin.config_list", "配置列表查询", "config,query"),
117 ("admin.config_update", "更新配置", "config,update"),
118 ("admin.log_list", "操作日志查询", "log,query"),
119 ("admin.dashboard", "仪表盘统计", "dashboard,query"),
120];
121
122const MIGRATION_SQL: &str = include_str!("../../assets/migrations/001_init_admin.sql");
126
127pub async fn execute(cmd: &AdminCommand) -> Result<i32, CliError> {
129 match cmd {
130 AdminCommand::Migrate(args) => execute_migrate(args).await,
131 AdminCommand::ListRoutes => execute_list_routes(),
132 AdminCommand::ListCapabilities => execute_list_capabilities(),
133 AdminCommand::Init(args) => execute_init(args).await,
134 }
135}
136
137async fn create_connection(url: &str, db_type: &str) -> Result<Box<dyn Connection>, CliError> {
138 use sz_orm_sqlx::any_driver::AnyPool;
139
140 let pool = AnyPool::connect(url)
141 .await
142 .map_err(|e| CliError::Migration(format!("{db_type} 连接失败: {e}")))?;
143 let conn = pool
144 .create()
145 .await
146 .map_err(|e| CliError::Migration(format!("获取连接失败: {e}")))?;
147 Ok(Box::new(conn))
148}
149
150async fn execute_migrate(args: &MigrateArgs) -> Result<i32, CliError> {
151 if args.show_sql || args.url.is_none() {
152 println!("=== sz-rust-addons-admin 迁移 SQL ===\n");
153 println!("{}", MIGRATION_SQL);
154 println!("=== 共 8 张表 + 3 个索引 ===");
155 }
156
157 if let Some(url) = &args.url {
158 println!("\n连接数据库 {} ...", args.db_type);
159 let mut conn = create_connection(url, &args.db_type).await?;
160
161 let sql_statements: Vec<&str> = MIGRATION_SQL
162 .lines()
163 .filter(|l| !l.is_empty() && !l.starts_with("--"))
164 .collect();
165 let joined = sql_statements.join("\n");
166 let statements: Vec<&str> = joined.split(';').filter(|s| !s.trim().is_empty()).collect();
167 println!("待执行 {} 条 SQL 语句", statements.len());
168
169 for (i, stmt) in statements.iter().enumerate() {
170 conn.execute(stmt)
171 .await
172 .map_err(|e| CliError::Migration(format!("第 {} 条 SQL 执行失败: {e}", i + 1)))?;
173 println!(" [{}] OK", i + 1);
174 }
175 println!("\n迁移完成:8 张表 + 3 个索引已创建");
176 } else if !args.show_sql {
177 println!("(离线模式,使用 --url <DATABASE_URL> 执行在线迁移)");
178 }
179
180 Ok(0)
181}
182
183fn execute_list_routes() -> Result<i32, CliError> {
184 let rows: Vec<RouteRow> = ROUTES
185 .iter()
186 .map(|(method, path, desc)| RouteRow {
187 method,
188 path,
189 description: desc,
190 })
191 .collect();
192 let table = Table::new(rows);
193 println!("=== Admin 插件 API 端点(共 {} 个)===\n", ROUTES.len());
194 println!("{table}");
195 Ok(0)
196}
197
198fn execute_list_capabilities() -> Result<i32, CliError> {
199 let rows: Vec<CapabilityRow> = CAPABILITIES
200 .iter()
201 .map(|(name, desc, tags)| CapabilityRow {
202 name,
203 description: desc,
204 tags,
205 })
206 .collect();
207 let table = Table::new(rows);
208 println!(
209 "=== Admin 插件 Capability(共 {} 个)===\n",
210 CAPABILITIES.len()
211 );
212 println!("{table}");
213 Ok(0)
214}
215
216async fn execute_init(args: &InitArgs) -> Result<i32, CliError> {
217 if args.url.is_none() {
218 println!("=== Admin 插件初始化(离线模式)===\n");
219 println!("将创建以下初始数据:");
220 println!(" 1. 内置角色: super_admin(超级管理员)、tenant_admin(租户管理员)");
221 println!(" 2. 内置权限项: admin:* 系列({} 个)", CAPABILITIES.len());
222 println!(" 3. 超级管理员账户: {} (密码: ***)", args.username);
223 println!("\n使用 --url <DATABASE_URL> 执行在线初始化");
224 return Ok(0);
225 }
226
227 let url = args
228 .url
229 .as_deref()
230 .ok_or_else(|| CliError::Clap("admin init 在线模式必须提供 --url".to_string()))?;
231 println!("=== Admin 插件初始化 ===\n");
232 println!("连接数据库...");
233 let mut conn = create_connection(url, &args.db_type).await?;
234
235 println!("创建内置角色...");
236 for (code, name, desc) in [
237 ("super_admin", "超级管理员", "拥有所有权限,不可删除"),
238 ("tenant_admin", "租户管理员", "租户内管理权限,不可删除"),
239 ] {
240 let sql = "INSERT INTO roles (name, code, description, is_builtin, tenant_id) \
241 VALUES (?, ?, ?, TRUE, 0) \
242 ON CONFLICT (code, tenant_id) DO NOTHING";
243 conn.execute_with_params(
244 sql,
245 &[
246 Value::String(name.to_string()),
247 Value::String(code.to_string()),
248 Value::String(desc.to_string()),
249 ],
250 )
251 .await
252 .map_err(|e| CliError::Migration(format!("创建角色 {code} 失败: {e}")))?;
253 println!(" 角色 {} ({}) 已创建", code, name);
254 }
255
256 println!("创建内置权限项...");
257 for (cap_name, cap_desc, _tags) in CAPABILITIES {
258 let module = cap_name.split('.').nth(1).unwrap_or("admin");
259 let sql = "INSERT INTO permissions (code, name, module, description, tenant_id) \
260 VALUES (?, ?, ?, ?, 0) \
261 ON CONFLICT (code, tenant_id) DO NOTHING";
262 conn.execute_with_params(
263 sql,
264 &[
265 Value::String(cap_name.to_string()),
266 Value::String(cap_desc.to_string()),
267 Value::String(module.to_string()),
268 Value::String(cap_desc.to_string()),
269 ],
270 )
271 .await
272 .map_err(|e| CliError::Migration(format!("创建权限 {cap_name} 失败: {e}")))?;
273 }
274 println!(" {} 个权限项已创建", CAPABILITIES.len());
275
276 println!("为 super_admin 分配所有权限...");
277 let sql = "INSERT INTO role_permissions (role_id, permission_code, tenant_id) \
278 SELECT r.id, p.code, 0 FROM roles r, permissions p \
279 WHERE r.code = 'super_admin' AND r.tenant_id = 0 \
280 AND p.tenant_id = 0 \
281 ON CONFLICT (role_id, permission_code, tenant_id) DO NOTHING";
282 conn.execute(sql)
283 .await
284 .map_err(|e| CliError::Migration(format!("分配权限失败: {e}")))?;
285 println!(" super_admin 已分配全部权限");
286
287 println!("创建超级管理员账户...");
288 let hashed = bcrypt::hash(&args.password, 10)
289 .map_err(|e| CliError::Migration(format!("密码哈希失败: {e}")))?;
290 conn.execute_with_params(
291 "INSERT INTO users (username, password, status, tenant_id) \
292 VALUES (?, ?, 'active', 0) \
293 ON CONFLICT (username, tenant_id) DO NOTHING",
294 &[Value::String(args.username.clone()), Value::String(hashed)],
295 )
296 .await
297 .map_err(|e| CliError::Migration(format!("创建管理员账户失败: {e}")))?;
298
299 conn.execute_with_params(
300 "INSERT INTO user_roles (user_id, role_id, tenant_id) \
301 SELECT u.id, r.id, 0 FROM users u, roles r \
302 WHERE u.username = ? AND u.tenant_id = 0 \
303 AND r.code = 'super_admin' AND r.tenant_id = 0 \
304 ON CONFLICT (user_id, role_id, tenant_id) DO NOTHING",
305 &[Value::String(args.username.clone())],
306 )
307 .await
308 .map_err(|e| CliError::Migration(format!("分配管理员角色失败: {e}")))?;
309 println!(" 账户 {} 已创建并分配 super_admin 角色", args.username);
310
311 println!("\n初始化完成!");
312 println!(" 超级管理员: {}", args.username);
313 println!(" 请及时修改默认密码!");
314 Ok(0)
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320 use clap::Parser;
321
322 #[derive(Parser)]
323 struct TestCli {
324 #[command(subcommand)]
325 admin: AdminCommand,
326 }
327
328 #[test]
329 fn test_parse_migrate() {
330 let cli = TestCli::parse_from(["sz-rust", "migrate", "--db-type", "postgres"]);
331 assert!(matches!(cli.admin, AdminCommand::Migrate { .. }));
332 }
333
334 #[test]
335 fn test_parse_migrate_with_url() {
336 let cli = TestCli::parse_from([
337 "sz-rust",
338 "migrate",
339 "--url",
340 "postgres://localhost/db",
341 "--show-sql",
342 ]);
343 match cli.admin {
344 AdminCommand::Migrate(args) => {
345 assert_eq!(args.url.as_deref(), Some("postgres://localhost/db"));
346 assert!(args.show_sql);
347 }
348 _ => panic!("expected Migrate"),
349 }
350 }
351
352 #[test]
353 fn test_parse_list_routes() {
354 let cli = TestCli::parse_from(["sz-rust", "list-routes"]);
355 assert!(matches!(cli.admin, AdminCommand::ListRoutes));
356 }
357
358 #[test]
359 fn test_parse_list_capabilities() {
360 let cli = TestCli::parse_from(["sz-rust", "list-capabilities"]);
361 assert!(matches!(cli.admin, AdminCommand::ListCapabilities));
362 }
363
364 #[test]
365 fn test_parse_init() {
366 let cli = TestCli::parse_from([
367 "sz-rust",
368 "init",
369 "--username",
370 "myadmin",
371 "--password",
372 "mypass",
373 ]);
374 match cli.admin {
375 AdminCommand::Init(args) => {
376 assert_eq!(args.username, "myadmin");
377 assert_eq!(args.password, "mypass");
378 }
379 _ => panic!("expected Init"),
380 }
381 }
382
383 #[test]
384 fn test_routes_count_21() {
385 assert_eq!(ROUTES.len(), 21);
386 }
387
388 #[test]
389 fn test_capabilities_count_17() {
390 assert_eq!(CAPABILITIES.len(), 17);
391 }
392
393 #[test]
394 fn test_all_capabilities_prefixed_admin() {
395 for (name, _, _) in CAPABILITIES {
396 assert!(
397 name.starts_with("admin."),
398 "capability {name} must start with admin."
399 );
400 }
401 }
402
403 #[tokio::test]
404 async fn test_execute_list_routes() {
405 let result = execute(&AdminCommand::ListRoutes).await;
406 assert!(result.is_ok());
407 assert_eq!(result.unwrap(), 0);
408 }
409
410 #[tokio::test]
411 async fn test_execute_list_capabilities() {
412 let result = execute(&AdminCommand::ListCapabilities).await;
413 assert!(result.is_ok());
414 assert_eq!(result.unwrap(), 0);
415 }
416
417 #[tokio::test]
418 async fn test_execute_migrate_offline() {
419 let args = MigrateArgs {
420 db_type: "postgres".to_string(),
421 url: None,
422 show_sql: true,
423 };
424 let result = execute(&AdminCommand::Migrate(args)).await;
425 assert!(result.is_ok());
426 }
427
428 #[tokio::test]
429 async fn test_execute_init_offline() {
430 let args = InitArgs {
431 db_type: "postgres".to_string(),
432 url: None,
433 username: "admin".to_string(),
434 password: "admin123".to_string(),
435 };
436 let result = execute(&AdminCommand::Init(args)).await;
437 assert!(result.is_ok());
438 }
439
440 #[tokio::test]
441 async fn test_execute_migrate_offline_no_show_sql() {
442 let args = MigrateArgs {
443 db_type: "postgres".to_string(),
444 url: None,
445 show_sql: false,
446 };
447 let result = execute(&AdminCommand::Migrate(args)).await;
448 assert!(result.is_ok());
449 }
450
451 #[test]
452 fn test_route_row_tabled() {
453 let row = RouteRow {
454 method: "GET",
455 path: "/api/admin/users",
456 description: "用户列表",
457 };
458 let table = Table::new(vec![row]);
459 let s = table.to_string();
460 assert!(s.contains("GET"));
461 assert!(s.contains("/api/admin/users"));
462 }
463
464 #[test]
465 fn test_capability_row_tabled() {
466 let row = CapabilityRow {
467 name: "admin.user_list",
468 description: "用户列表查询",
469 tags: "user,query",
470 };
471 let table = Table::new(vec![row]);
472 let s = table.to_string();
473 assert!(s.contains("admin.user_list"));
474 }
475
476 #[test]
477 fn test_migration_sql_not_empty() {
478 assert!(!MIGRATION_SQL.is_empty());
479 assert!(MIGRATION_SQL.contains("CREATE TABLE"));
480 }
481}