diff --git a/backend/crates/gateway-admin/src/model/account_groups.rs b/backend/crates/gateway-admin/src/model/account_groups.rs index 36814a513..45022b937 100644 --- a/backend/crates/gateway-admin/src/model/account_groups.rs +++ b/backend/crates/gateway-admin/src/model/account_groups.rs @@ -3,7 +3,7 @@ use std::collections::BTreeMap; use chrono::{DateTime, Utc}; -use gateway_core::routing::{AccountGroupId, ProviderKind}; +use gateway_core::routing::AccountGroupId; use super::{PageSize, Revision, observability::DecimalAmount}; @@ -94,25 +94,6 @@ pub struct AccountGroupPage { pub page_size: u16, } -/// Lightweight account shown by the membership editor. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct AccountGroupMember { - pub id: String, - pub name: String, - pub provider_kind: ProviderKind, - pub email: Option, - pub enabled: bool, -} - -/// Complete membership projection for one group. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct AccountGroupMembers { - pub config_revision: Revision, - pub id: AccountGroupId, - pub items: Vec, - pub total: u64, -} - /// Create an account group. #[derive(Debug, Clone, PartialEq, Eq)] pub struct CreateAccountGroup { diff --git a/backend/crates/gateway-admin/src/ports/store.rs b/backend/crates/gateway-admin/src/ports/store.rs index abbe0c2d2..8f2eda3a9 100644 --- a/backend/crates/gateway-admin/src/ports/store.rs +++ b/backend/crates/gateway-admin/src/ports/store.rs @@ -11,8 +11,8 @@ use super::backup::BackupStorePorts; use crate::model::{ MutationContext, Revision, account_groups::{ - AccountGroupListQuery, AccountGroupMembers, AccountGroupMutation, AccountGroupPage, - DeleteAccountGroup, NewAccountGroup, SetAccountGroupEnabled, UpdateAccountGroup, + AccountGroupListQuery, AccountGroupMutation, AccountGroupPage, DeleteAccountGroup, + NewAccountGroup, SetAccountGroupEnabled, UpdateAccountGroup, }, accounts::{ AccountListQuery, AccountPage, AccountPageItem, AccountUpdateResult, AccountUsage, @@ -226,7 +226,7 @@ pub trait ClientKeyStore: Send + Sync { ) -> AdminStoreResult; } -/// Provider-neutral account group management and membership transactions. +/// Provider-neutral account group management transactions. #[async_trait] pub trait AccountGroupStore: Send + Sync { async fn list_account_groups( @@ -234,11 +234,6 @@ pub trait AccountGroupStore: Send + Sync { query: AccountGroupListQuery, ) -> AdminStoreResult; - async fn account_group_members( - &self, - id: &gateway_core::routing::AccountGroupId, - ) -> AdminStoreResult; - async fn create_account_group( &self, command: NewAccountGroup, diff --git a/backend/crates/gateway-admin/src/use_case/account_groups.rs b/backend/crates/gateway-admin/src/use_case/account_groups.rs index c3d15b7fb..07fa093b4 100644 --- a/backend/crates/gateway-admin/src/use_case/account_groups.rs +++ b/backend/crates/gateway-admin/src/use_case/account_groups.rs @@ -10,9 +10,8 @@ use crate::{ model::{ AdminError, MutationContext, account_groups::{ - AccountGroupListQuery, AccountGroupMembers, AccountGroupMutation, AccountGroupPage, - CreateAccountGroup, DeleteAccountGroup, NewAccountGroup, SetAccountGroupEnabled, - UpdateAccountGroup, + AccountGroupListQuery, AccountGroupMutation, AccountGroupPage, CreateAccountGroup, + DeleteAccountGroup, NewAccountGroup, SetAccountGroupEnabled, UpdateAccountGroup, }, }, ports::store::AccountGroupStore, @@ -24,7 +23,6 @@ use super::{map_store_error, publish_committed}; #[async_trait] pub trait AccountGroupService: Send + Sync { async fn list(&self, query: AccountGroupListQuery) -> Result; - async fn members(&self, id: &AccountGroupId) -> Result; async fn create( &self, context: &MutationContext, @@ -80,13 +78,6 @@ impl AccountGroupService for DefaultAccountGroupService { .map_err(|error| map_store_error(error, "account group")) } - async fn members(&self, id: &AccountGroupId) -> Result { - self.store - .account_group_members(id) - .await - .map_err(|error| map_store_error(error, "account group")) - } - async fn create( &self, context: &MutationContext, diff --git a/backend/crates/gateway-admin/tests/use_case/mod.rs b/backend/crates/gateway-admin/tests/use_case/mod.rs index eda5bfd58..7383aacc6 100644 --- a/backend/crates/gateway-admin/tests/use_case/mod.rs +++ b/backend/crates/gateway-admin/tests/use_case/mod.rs @@ -21,8 +21,8 @@ use gateway_admin::{ model::{ MutationContext, Revision, account_groups::{ - AccountGroupListQuery, AccountGroupMembers, AccountGroupMutation, AccountGroupPage, - DeleteAccountGroup, NewAccountGroup, SetAccountGroupEnabled, UpdateAccountGroup, + AccountGroupListQuery, AccountGroupMutation, AccountGroupPage, DeleteAccountGroup, + NewAccountGroup, SetAccountGroupEnabled, UpdateAccountGroup, }, accounts::{ AccountListQuery, AccountPage, AccountUpdateResult, AccountUsage, @@ -252,13 +252,6 @@ impl AccountGroupStore for UnavailableAccountGroupStore { Err(unavailable("account groups")) } - async fn account_group_members( - &self, - _: &gateway_core::routing::AccountGroupId, - ) -> AdminStoreResult { - Err(unavailable("account group members")) - } - async fn create_account_group( &self, _: NewAccountGroup, diff --git a/backend/crates/gateway-api/src/admin/account_groups.rs b/backend/crates/gateway-api/src/admin/account_groups.rs index db365f79b..439586394 100644 --- a/backend/crates/gateway-api/src/admin/account_groups.rs +++ b/backend/crates/gateway-api/src/admin/account_groups.rs @@ -14,9 +14,8 @@ use gateway_admin::model::{ PageSize, account_groups::{ AccountGroupAccountSummary, AccountGroupCapacity, AccountGroupColor, AccountGroupListQuery, - AccountGroupMember, AccountGroupMembers, AccountGroupMutation, AccountGroupPage, - AccountGroupRecord, AccountGroupUsage, CreateAccountGroup, DeleteAccountGroup, - SetAccountGroupEnabled, UpdateAccountGroup, + AccountGroupMutation, AccountGroupPage, AccountGroupRecord, AccountGroupUsage, + CreateAccountGroup, DeleteAccountGroup, SetAccountGroupEnabled, UpdateAccountGroup, }, }; use gateway_core::routing::AccountGroupId; @@ -91,12 +90,6 @@ struct AccountGroupIdRequest { id: String, } -#[derive(Debug, Clone, Deserialize)] -#[serde(deny_unknown_fields)] -struct AccountGroupIdQuery { - id: String, -} - #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] struct AccountGroupView { @@ -207,48 +200,6 @@ impl From for AccountGroupPageData { } } -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -struct AccountGroupMemberView { - id: String, - name: String, - provider_kind: String, - email: Option, - enabled: bool, -} - -impl From for AccountGroupMemberView { - fn from(member: AccountGroupMember) -> Self { - Self { - id: member.id, - name: member.name, - provider_kind: member.provider_kind.to_string(), - email: member.email, - enabled: member.enabled, - } - } -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -struct AccountGroupMembersData { - id: String, - items: Vec, - total: u64, - config_revision: u64, -} - -impl From for AccountGroupMembersData { - fn from(members: AccountGroupMembers) -> Self { - Self { - id: members.id.to_string(), - items: members.items.into_iter().map(Into::into).collect(), - total: members.total, - config_revision: members.config_revision.get(), - } - } -} - #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] struct AccountGroupMutationData { @@ -279,7 +230,6 @@ where .route("/api/admin/account-groups/enable", post(enable::)) .route("/api/admin/account-groups/disable", post(disable::)) .route("/api/admin/account-groups/delete", post(delete::)) - .route("/api/admin/account-groups/members", get(members::)) } async fn list( @@ -302,27 +252,6 @@ where )) } -async fn members( - _auth: AdminAuth, - State(state): State, - Query(query): Query, -) -> Result -where - S: AdminSessionState + Send + Sync, -{ - let id = group_id(query.id)?; - let result = state - .admin_services() - .account_groups() - .members(&id) - .await - .map_err(map_service_error)?; - Ok(AdminResponse::new( - StatusCode::OK, - AdminEnvelope::ok(AccountGroupMembersData::from(result)), - )) -} - async fn create( auth: AdminAuth, State(state): State, diff --git a/backend/crates/gateway-api/tests/admin/account_groups.rs b/backend/crates/gateway-api/tests/admin/account_groups.rs index 3b5a86f58..389636101 100644 --- a/backend/crates/gateway-api/tests/admin/account_groups.rs +++ b/backend/crates/gateway-api/tests/admin/account_groups.rs @@ -44,27 +44,6 @@ async fn list_route_should_keep_camel_case_group_and_page_wire() { assert!(value["data"]["items"][0].get("member_count").is_none()); } -#[tokio::test] -async fn members_route_should_keep_provider_kind_and_member_wire() { - let fixture = authenticated_fixture().await; - let response = request( - router(&fixture), - Method::GET, - &format!("/api/admin/account-groups/members?id={PRIMARY_GROUP_ID}"), - None, - true, - ) - .await; - - assert_eq!(response.status(), StatusCode::OK); - let value = response_json(response).await; - assert_eq!(value["data"]["id"], PRIMARY_GROUP_ID); - assert_eq!(value["data"]["total"], 2); - assert_eq!(value["data"]["items"][0]["providerKind"], "openai"); - assert_eq!(value["data"]["items"][0]["email"], "openai@example.invalid"); - assert!(value["data"]["items"][0].get("provider_kind").is_none()); -} - #[tokio::test] async fn create_route_should_create_an_empty_group() { let fixture = authenticated_fixture().await; diff --git a/backend/crates/gateway-api/tests/admin/mod.rs b/backend/crates/gateway-api/tests/admin/mod.rs index b6dc48b53..e394c35f3 100644 --- a/backend/crates/gateway-api/tests/admin/mod.rs +++ b/backend/crates/gateway-api/tests/admin/mod.rs @@ -15,9 +15,9 @@ use gateway_admin::{ MutationContext, Revision, account_groups::{ AccountGroupAccountSummary, AccountGroupCapacity, AccountGroupColor, - AccountGroupListQuery, AccountGroupMember, AccountGroupMembers, AccountGroupMutation, - AccountGroupPage, AccountGroupRecord, AccountGroupUsage, DeleteAccountGroup, - NewAccountGroup, SetAccountGroupEnabled, UpdateAccountGroup, + AccountGroupListQuery, AccountGroupMutation, AccountGroupPage, AccountGroupRecord, + AccountGroupUsage, DeleteAccountGroup, NewAccountGroup, SetAccountGroupEnabled, + UpdateAccountGroup, }, accounts::{ AccountListQuery, AccountPage, AccountPageItem, AccountUpdateResult, AccountUsage, @@ -361,7 +361,6 @@ pub(super) const SECONDARY_GROUP_ID: &str = "grp_2222222222222222222222222222222 struct MemoryAccountGroupState { revision: Revision, groups: BTreeMap, - members: BTreeMap>, } pub(super) struct MemoryAccountGroupStore { @@ -373,25 +372,11 @@ impl MemoryAccountGroupStore { let now = Utc::now(); let primary_id = group_id(PRIMARY_GROUP_ID); let secondary_id = group_id(SECONDARY_GROUP_ID); - let openai = AccountGroupMember { - id: "acct_openai".to_owned(), - name: "OpenAI primary".to_owned(), - provider_kind: ProviderKind::new("openai").expect("provider kind"), - email: Some("openai@example.invalid".to_owned()), - enabled: true, - }; - let xai = AccountGroupMember { - id: "acct_xai".to_owned(), - name: "xAI primary".to_owned(), - provider_kind: ProviderKind::new("xai").expect("provider kind"), - email: None, - enabled: false, - }; let groups = BTreeMap::from([ ( primary_id.clone(), AccountGroupRecord { - id: primary_id.clone(), + id: primary_id, name: "Alpha routing".to_owned(), description: Some("Primary traffic".to_owned()), color: group_color("#2563ebff"), @@ -432,7 +417,6 @@ impl MemoryAccountGroupStore { state: Mutex::new(MemoryAccountGroupState { revision: Revision::new(7).expect("revision"), groups, - members: BTreeMap::from([(primary_id, vec![openai, xai])]), }), } } @@ -481,23 +465,6 @@ impl AccountGroupStore for MemoryAccountGroupStore { }) } - async fn account_group_members( - &self, - id: &gateway_core::routing::AccountGroupId, - ) -> AdminStoreResult { - let state = self.state.lock().expect("account groups"); - if !state.groups.contains_key(id) { - return Err(not_found("account group")); - } - let items = state.members.get(id).cloned().unwrap_or_default(); - Ok(AccountGroupMembers { - config_revision: state.revision, - id: id.clone(), - total: items.len() as u64, - items, - }) - } - async fn create_account_group( &self, command: NewAccountGroup, @@ -521,8 +488,6 @@ impl AccountGroupStore for MemoryAccountGroupStore { updated_at: now, }; state.groups.insert(command.id.clone(), record); - state.members.insert(command.id.clone(), Vec::new()); - refresh_group_counts(&mut state, &command.id); mutation(&mut state, command.id, true) } @@ -567,7 +532,6 @@ impl AccountGroupStore for MemoryAccountGroupStore { if state.groups.remove(&command.id).is_none() { return Err(not_found("account group")); } - state.members.remove(&command.id); mutation(&mut state, command.id, false) } } @@ -576,24 +540,6 @@ fn group_id(value: &str) -> gateway_core::routing::AccountGroupId { gateway_core::routing::AccountGroupId::new(value).expect("account group ID") } -fn refresh_group_counts( - state: &mut MemoryAccountGroupState, - id: &gateway_core::routing::AccountGroupId, -) { - let members = state.members.get(id).map(Vec::as_slice).unwrap_or_default(); - let provider_counts = members.iter().fold(BTreeMap::new(), |mut counts, member| { - *counts - .entry(member.provider_kind.as_str().to_owned()) - .or_insert(0) += 1; - counts - }); - if let Some(record) = state.groups.get_mut(id) { - record.member_count = members.len() as u64; - record.provider_counts = provider_counts; - record.updated_at = Utc::now(); - } -} - fn mutation( state: &mut MemoryAccountGroupState, id: gateway_core::routing::AccountGroupId, diff --git a/backend/crates/gateway-store/src/postgres/account_groups.rs b/backend/crates/gateway-store/src/postgres/account_groups.rs index 5b6ddf777..6d33f05a4 100644 --- a/backend/crates/gateway-store/src/postgres/account_groups.rs +++ b/backend/crates/gateway-store/src/postgres/account_groups.rs @@ -13,18 +13,17 @@ use gateway_admin::{ MutationContext, account_groups::{ AccountGroupAccountSummary, AccountGroupCapacity, AccountGroupColor, - AccountGroupListQuery, AccountGroupMember, AccountGroupMembers, AccountGroupMutation, - AccountGroupPage, AccountGroupRecord, AccountGroupUsage, DeleteAccountGroup, - NewAccountGroup, SetAccountGroupEnabled, UpdateAccountGroup, + AccountGroupListQuery, AccountGroupMutation, AccountGroupPage, AccountGroupRecord, + AccountGroupUsage, DeleteAccountGroup, NewAccountGroup, SetAccountGroupEnabled, + UpdateAccountGroup, }, observability::DecimalAmount, }, ports::store::{AccountGroupStore, AdminStoreError, AdminStoreResult}, }; use gateway_core::{ - engine::credential::AccountStatus, - provider_ports::ProviderCooldownPort, - routing::{AccountGroupId, ProviderKind}, + engine::credential::AccountStatus, provider_ports::ProviderCooldownPort, + routing::AccountGroupId, }; use sqlx::{PgPool, Postgres, QueryBuilder, Row as _, Transaction}; @@ -178,36 +177,6 @@ impl AccountGroupStore for PgAccountGroupRepository { }) } - async fn account_group_members( - &self, - id: &AccountGroupId, - ) -> AdminStoreResult { - self.required_record(id).await?; - let rows = sqlx::query( - "select a.id, a.name, a.provider_kind, a.email, a.enabled - from account_group_accounts m - join provider_accounts a on a.id = m.provider_account_id - where m.account_group_id = $1 - order by a.provider_kind, lower(a.name), a.id", - ) - .bind(id.as_str()) - .fetch_all(&self.pool) - .await - .map_err(|_| admin_store_error(ENTITY, unavailable("load account group members")))?; - let items = rows - .iter() - .map(group_member) - .collect::>>() - .map_err(|error| admin_store_error(ENTITY, error))?; - Ok(AccountGroupMembers { - config_revision: self.current_revision().await?, - id: id.clone(), - total: u64::try_from(items.len()) - .map_err(|_| invalid_admin("member count overflow"))?, - items, - }) - } - async fn create_account_group( &self, command: NewAccountGroup, @@ -674,28 +643,6 @@ async fn group_costs( .collect() } -fn group_member(row: &sqlx::postgres::PgRow) -> StoreResult { - Ok(AccountGroupMember { - id: row - .try_get("id") - .map_err(|_| invalid("invalid member id"))?, - name: row - .try_get("name") - .map_err(|_| invalid("invalid member name"))?, - provider_kind: ProviderKind::new( - row.try_get::("provider_kind") - .map_err(|_| invalid("invalid member provider"))?, - ) - .map_err(|_| invalid("invalid member provider"))?, - email: row - .try_get("email") - .map_err(|_| invalid("invalid member email"))?, - enabled: row - .try_get("enabled") - .map_err(|_| invalid("invalid member enabled"))?, - }) -} - fn count_value(row: &sqlx::postgres::PgRow, field: &str) -> StoreResult { u64::try_from( row.try_get::(field) diff --git a/backend/crates/gateway-store/tests/postgres/account_groups.rs b/backend/crates/gateway-store/tests/postgres/account_groups.rs index 484b8332b..a2c3865f3 100644 --- a/backend/crates/gateway-store/tests/postgres/account_groups.rs +++ b/backend/crates/gateway-store/tests/postgres/account_groups.rs @@ -122,20 +122,6 @@ async fn groups_aggregate_cross_provider_members_and_key_bindings_without_multip assert!(empty.provider_counts.is_empty()); assert_eq!(empty.client_key_count, 1); - let members = groups - .account_group_members(&mixed_group) - .await - .expect("load mixed group members"); - assert_eq!(members.total, 2); - assert_eq!( - members - .items - .iter() - .map(|member| member.provider_kind.as_str()) - .collect::>(), - ["openai", "xai"] - ); - let all_key = keys .reveal_client_key(&client_key_id("key_all_accounts")) .await diff --git a/docs/api.md b/docs/api.md index 7a9545d89..6387bfb1f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -222,7 +222,6 @@ OAuth start 使用: | `POST` | `/api/admin/account-groups/enable` | `{ id }` | 启用 | | `POST` | `/api/admin/account-groups/disable` | `{ id }` | 禁用;已绑定 Key 保持受限,不回退到全部账号 | | `POST` | `/api/admin/account-groups/delete` | `{ id }` | 删除未被 Client Key 引用的组 | -| `GET` | `/api/admin/account-groups/members` | `id` | 查询跨 Provider 成员 | 列表数据为 `{ items, page, configRevision }`,其中 item 返回 `memberCount`、按 Provider 聚合的 `providerCounts` 和 `clientKeyCount`。成员响应为 `{ id, items, total, configRevision }`;成员的 diff --git a/frontend/src/api/modules/account-groups.ts b/frontend/src/api/modules/account-groups.ts index a0f1b99bb..35d00161e 100644 --- a/frontend/src/api/modules/account-groups.ts +++ b/frontend/src/api/modules/account-groups.ts @@ -48,21 +48,6 @@ export interface AccountGroupListResponse { configRevision: number } -export interface AccountGroupMember { - id: string - name: string - providerKind: string - email: string | null - enabled: boolean -} - -export interface AccountGroupMembersResponse { - id: string - items: AccountGroupMember[] - total: number - configRevision: number -} - export interface AccountGroupMutationResponse { id: string record: AccountGroup | null @@ -140,11 +125,3 @@ export function deleteAccountGroup(data: AccountGroupIdParam) { data, }) } - -export function getAccountGroupMembers(data: AccountGroupIdParam) { - return request({ - url: '/api/admin/account-groups/members', - method: 'GET', - params: data, - }) -} diff --git a/frontend/src/composables/useAccountGroupCatalog.ts b/frontend/src/composables/useAccountGroupCatalog.ts index d766ebed3..0b2011a50 100644 --- a/frontend/src/composables/useAccountGroupCatalog.ts +++ b/frontend/src/composables/useAccountGroupCatalog.ts @@ -1,16 +1,13 @@ -import type { AccountGroup, AccountGroupMember } from '@/api' +import type { AccountGroup } from '@/api' import { onMounted, shallowRef } from 'vue' -import { getAccountGroupMembers, getAccountGroups } from '@/api' +import { getAccountGroups } from '@/api' import { toast } from '@/components/base/BaseToast' import { errorMessage } from '@/utils/async' export function useAccountGroupCatalog(options: { immediate?: boolean } = {}) { const groups = shallowRef([]) - const membersByGroupId = shallowRef>({}) const loading = shallowRef(false) - const loadingMemberIds = shallowRef>(new Set()) - const memberRequests = new Map>() async function loadGroups() { loading.value = true @@ -33,51 +30,6 @@ export function useAccountGroupCatalog(options: { immediate?: boolean } = {}) { } } - async function loadMembers(groupId: string, force = false) { - if (!force && Object.hasOwn(membersByGroupId.value, groupId)) - return membersByGroupId.value[groupId] ?? [] - - const existing = memberRequests.get(groupId) - if (existing) - return existing - - const request = (async () => { - setMemberLoading(groupId, true) - try { - const result = await getAccountGroupMembers({ id: groupId }) - membersByGroupId.value = { - ...membersByGroupId.value, - [groupId]: result.items, - } - return result.items - } - finally { - setMemberLoading(groupId, false) - memberRequests.delete(groupId) - } - })() - memberRequests.set(groupId, request) - return request - } - - async function ensureMembers(groupIds: string[]) { - try { - await Promise.all([...new Set(groupIds)].map(id => loadMembers(id))) - } - catch (error: unknown) { - toast.error(errorMessage(error, '分组成员加载失败')) - } - } - - function setMemberLoading(groupId: string, value: boolean) { - const next = new Set(loadingMemberIds.value) - if (value) - next.add(groupId) - else - next.delete(groupId) - loadingMemberIds.value = next - } - if (options.immediate !== false) { onMounted(() => { void loadGroups() @@ -86,11 +38,7 @@ export function useAccountGroupCatalog(options: { immediate?: boolean } = {}) { return { groups, - membersByGroupId, loading, - loadingMemberIds, loadGroups, - loadMembers, - ensureMembers, } }