From 49abd0fe89045f1665966484183400a2d9dd858f Mon Sep 17 00:00:00 2001 From: iven Date: Tue, 31 Mar 2026 03:21:19 +0800 Subject: [PATCH] feat(saas): wire llm_routing into account CRUD and auth responses - Add llm_routing to all list_accounts/get_account SQL queries and JSON responses - Add llm_routing to UpdateAccountRequest with COALESCE update - Add llm_routing to AccountPublic struct in auth types - Wire llm_routing into register (default 'local'), login, and me handlers - Add llm_routing field to AccountRow, AccountAuthRow, AccountLoginRow model structs Co-Authored-By: Claude Opus 4.6 --- crates/zclaw-saas/src/account/service.rs | 28 +++++++++++++----------- crates/zclaw-saas/src/account/types.rs | 1 + crates/zclaw-saas/src/auth/handlers.rs | 11 ++++++---- crates/zclaw-saas/src/auth/types.rs | 1 + crates/zclaw-saas/src/models/account.rs | 3 +++ 5 files changed, 27 insertions(+), 17 deletions(-) diff --git a/crates/zclaw-saas/src/account/service.rs b/crates/zclaw-saas/src/account/service.rs index ce7a2ad..26ff1dd 100644 --- a/crates/zclaw-saas/src/account/service.rs +++ b/crates/zclaw-saas/src/account/service.rs @@ -23,7 +23,7 @@ pub async fn list_accounts( "SELECT COUNT(*) FROM accounts WHERE role = $1 AND status = $2 AND (username LIKE $3 OR email LIKE $3 OR display_name LIKE $3)" ).bind(role).bind(status).bind(&pattern).fetch_one(db).await?; let rows = sqlx::query_as::<_, AccountRow>( - "SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at + "SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at, llm_routing FROM accounts WHERE role = $1 AND status = $2 AND (username LIKE $3 OR email LIKE $3 OR display_name LIKE $3) ORDER BY created_at DESC LIMIT $4 OFFSET $5" ).bind(role).bind(status).bind(&pattern).bind(page_size as i64).bind(offset as i64).fetch_all(db).await?; @@ -35,7 +35,7 @@ pub async fn list_accounts( "SELECT COUNT(*) FROM accounts WHERE role = $1 AND status = $2" ).bind(role).bind(status).fetch_one(db).await?; let rows = sqlx::query_as::<_, AccountRow>( - "SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at + "SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at, llm_routing FROM accounts WHERE role = $1 AND status = $2 ORDER BY created_at DESC LIMIT $3 OFFSET $4" ).bind(role).bind(status).bind(page_size as i64).bind(offset as i64).fetch_all(db).await?; @@ -48,7 +48,7 @@ pub async fn list_accounts( "SELECT COUNT(*) FROM accounts WHERE role = $1 AND (username LIKE $2 OR email LIKE $2 OR display_name LIKE $2)" ).bind(role).bind(&pattern).fetch_one(db).await?; let rows = sqlx::query_as::<_, AccountRow>( - "SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at + "SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at, llm_routing FROM accounts WHERE role = $1 AND (username LIKE $2 OR email LIKE $2 OR display_name LIKE $2) ORDER BY created_at DESC LIMIT $3 OFFSET $4" ).bind(role).bind(&pattern).bind(page_size as i64).bind(offset as i64).fetch_all(db).await?; @@ -61,7 +61,7 @@ pub async fn list_accounts( "SELECT COUNT(*) FROM accounts WHERE status = $1 AND (username LIKE $2 OR email LIKE $2 OR display_name LIKE $2)" ).bind(status).bind(&pattern).fetch_one(db).await?; let rows = sqlx::query_as::<_, AccountRow>( - "SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at + "SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at, llm_routing FROM accounts WHERE status = $1 AND (username LIKE $2 OR email LIKE $2 OR display_name LIKE $2) ORDER BY created_at DESC LIMIT $3 OFFSET $4" ).bind(status).bind(&pattern).bind(page_size as i64).bind(offset as i64).fetch_all(db).await?; @@ -73,7 +73,7 @@ pub async fn list_accounts( "SELECT COUNT(*) FROM accounts WHERE role = $1" ).bind(role).fetch_one(db).await?; let rows = sqlx::query_as::<_, AccountRow>( - "SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at + "SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at, llm_routing FROM accounts WHERE role = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3" ).bind(role).bind(page_size as i64).bind(offset as i64).fetch_all(db).await?; @@ -85,7 +85,7 @@ pub async fn list_accounts( "SELECT COUNT(*) FROM accounts WHERE status = $1" ).bind(status).fetch_one(db).await?; let rows = sqlx::query_as::<_, AccountRow>( - "SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at + "SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at, llm_routing FROM accounts WHERE status = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3" ).bind(status).bind(page_size as i64).bind(offset as i64).fetch_all(db).await?; @@ -98,7 +98,7 @@ pub async fn list_accounts( "SELECT COUNT(*) FROM accounts WHERE (username LIKE $1 OR email LIKE $1 OR display_name LIKE $1)" ).bind(&pattern).fetch_one(db).await?; let rows = sqlx::query_as::<_, AccountRow>( - "SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at + "SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at, llm_routing FROM accounts WHERE (username LIKE $1 OR email LIKE $1 OR display_name LIKE $1) ORDER BY created_at DESC LIMIT $2 OFFSET $3" ).bind(&pattern).bind(page_size as i64).bind(offset as i64).fetch_all(db).await?; @@ -110,7 +110,7 @@ pub async fn list_accounts( "SELECT COUNT(*) FROM accounts" ).fetch_one(db).await?; let rows = sqlx::query_as::<_, AccountRow>( - "SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at + "SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at, llm_routing FROM accounts ORDER BY created_at DESC LIMIT $1 OFFSET $2" ).bind(page_size as i64).bind(offset as i64).fetch_all(db).await?; (total, rows) @@ -123,7 +123,7 @@ pub async fn list_accounts( serde_json::json!({ "id": r.id, "username": r.username, "email": r.email, "display_name": r.display_name, "role": r.role, "status": r.status, "totp_enabled": r.totp_enabled, - "last_login_at": r.last_login_at, "created_at": r.created_at, + "last_login_at": r.last_login_at, "created_at": r.created_at, "llm_routing": r.llm_routing, }) }) .collect(); @@ -134,7 +134,7 @@ pub async fn list_accounts( pub async fn get_account(db: &PgPool, account_id: &str) -> SaasResult { let row: Option = sqlx::query_as( - "SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at + "SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at, llm_routing FROM accounts WHERE id = $1" ) .bind(account_id) @@ -146,7 +146,7 @@ pub async fn get_account(db: &PgPool, account_id: &str) -> SaasResult, pub role: Option, pub avatar_url: Option, + pub llm_routing: Option, } #[derive(Debug, Deserialize)] diff --git a/crates/zclaw-saas/src/auth/handlers.rs b/crates/zclaw-saas/src/auth/handlers.rs index 7e34315..a838414 100644 --- a/crates/zclaw-saas/src/auth/handlers.rs +++ b/crates/zclaw-saas/src/auth/handlers.rs @@ -111,8 +111,8 @@ pub async fn register( let now = chrono::Utc::now().to_rfc3339(); sqlx::query( - "INSERT INTO accounts (id, username, email, password_hash, display_name, role, status, created_at, updated_at) - VALUES ($1, $2, $3, $4, $5, $6, 'active', $7, $7)" + "INSERT INTO accounts (id, username, email, password_hash, display_name, role, status, created_at, updated_at, llm_routing) + VALUES ($1, $2, $3, $4, $5, $6, 'active', $7, $7, 'local')" ) .bind(&account_id) .bind(&req.username) @@ -159,6 +159,7 @@ pub async fn register( status: "active".into(), totp_enabled: false, created_at: now, + llm_routing: "local".into(), }, }; let jar = set_auth_cookies(jar, &resp.token, &refresh_token); @@ -176,7 +177,7 @@ pub async fn login( let row: Option = sqlx::query_as( "SELECT id, username, email, display_name, role, status, totp_enabled, - password_hash, totp_secret, created_at + password_hash, totp_secret, created_at, llm_routing FROM accounts WHERE username = $1 OR email = $1" ) .bind(&req.username) @@ -245,6 +246,7 @@ pub async fn login( account: AccountPublic { id: r.id, username: r.username, email: r.email, display_name: r.display_name, role: r.role, status: r.status, totp_enabled: r.totp_enabled, created_at: r.created_at, + llm_routing: r.llm_routing, }, }; let jar = set_auth_cookies(jar, &resp.token, &refresh_token); @@ -349,7 +351,7 @@ pub async fn me( ) -> SaasResult> { let row: Option = sqlx::query_as( - "SELECT id, username, email, display_name, role, status, totp_enabled, created_at + "SELECT id, username, email, display_name, role, status, totp_enabled, created_at, llm_routing FROM accounts WHERE id = $1" ) .bind(&ctx.account_id) @@ -361,6 +363,7 @@ pub async fn me( Ok(Json(AccountPublic { id: r.id, username: r.username, email: r.email, display_name: r.display_name, role: r.role, status: r.status, totp_enabled: r.totp_enabled, created_at: r.created_at, + llm_routing: r.llm_routing, })) } diff --git a/crates/zclaw-saas/src/auth/types.rs b/crates/zclaw-saas/src/auth/types.rs index 06e652c..8928d3f 100644 --- a/crates/zclaw-saas/src/auth/types.rs +++ b/crates/zclaw-saas/src/auth/types.rs @@ -45,6 +45,7 @@ pub struct AccountPublic { pub status: String, pub totp_enabled: bool, pub created_at: String, + pub llm_routing: String, } /// 认证上下文 (注入到 request extensions) diff --git a/crates/zclaw-saas/src/models/account.rs b/crates/zclaw-saas/src/models/account.rs index cde1fe1..8c8383c 100644 --- a/crates/zclaw-saas/src/models/account.rs +++ b/crates/zclaw-saas/src/models/account.rs @@ -14,6 +14,7 @@ pub struct AccountRow { pub totp_enabled: bool, pub last_login_at: Option, pub created_at: String, + pub llm_routing: String, } /// accounts 表行 (不含 last_login_at,用于 auth/me 等场景) @@ -27,6 +28,7 @@ pub struct AccountAuthRow { pub status: String, pub totp_enabled: bool, pub created_at: String, + pub llm_routing: String, } /// Login 一次性查询行(合并用户信息 + password_hash + totp_secret) @@ -42,6 +44,7 @@ pub struct AccountLoginRow { pub password_hash: String, pub totp_secret: Option, pub created_at: String, + pub llm_routing: String, } /// operation_logs 表行