refactor(account-groups): remove unused members endpoint

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Zyy
2026-08-13 17:28:43 +08:00
co-authored by Codex
parent f2ebb09e34
commit 71159d5633
12 changed files with 21 additions and 350 deletions
@@ -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<String>,
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<AccountGroupMember>,
pub total: u64,
}
/// Create an account group.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CreateAccountGroup {
@@ -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<Revision>;
}
/// 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<AccountGroupPage>;
async fn account_group_members(
&self,
id: &gateway_core::routing::AccountGroupId,
) -> AdminStoreResult<AccountGroupMembers>;
async fn create_account_group(
&self,
command: NewAccountGroup,
@@ -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<AccountGroupPage, AdminError>;
async fn members(&self, id: &AccountGroupId) -> Result<AccountGroupMembers, AdminError>;
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<AccountGroupMembers, AdminError> {
self.store
.account_group_members(id)
.await
.map_err(|error| map_store_error(error, "account group"))
}
async fn create(
&self,
context: &MutationContext,
@@ -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<AccountGroupMembers> {
Err(unavailable("account group members"))
}
async fn create_account_group(
&self,
_: NewAccountGroup,
@@ -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<AccountGroupPage> for AccountGroupPageData {
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct AccountGroupMemberView {
id: String,
name: String,
provider_kind: String,
email: Option<String>,
enabled: bool,
}
impl From<AccountGroupMember> 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<AccountGroupMemberView>,
total: u64,
config_revision: u64,
}
impl From<AccountGroupMembers> 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::<S>))
.route("/api/admin/account-groups/disable", post(disable::<S>))
.route("/api/admin/account-groups/delete", post(delete::<S>))
.route("/api/admin/account-groups/members", get(members::<S>))
}
async fn list<S>(
@@ -302,27 +252,6 @@ where
))
}
async fn members<S>(
_auth: AdminAuth,
State(state): State<S>,
Query(query): Query<AccountGroupIdQuery>,
) -> Result<impl IntoResponse, AdminError>
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<S>(
auth: AdminAuth,
State(state): State<S>,
@@ -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;
+4 -58
View File
@@ -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<gateway_core::routing::AccountGroupId, AccountGroupRecord>,
members: BTreeMap<gateway_core::routing::AccountGroupId, Vec<AccountGroupMember>>,
}
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<AccountGroupMembers> {
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,
@@ -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<AccountGroupMembers> {
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::<StoreResult<Vec<_>>>()
.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<AccountGroupMember> {
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::<String, _>("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> {
u64::try_from(
row.try_get::<i64, _>(field)
@@ -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::<Vec<_>>(),
["openai", "xai"]
);
let all_key = keys
.reveal_client_key(&client_key_id("key_all_accounts"))
.await
-1
View File
@@ -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 }`;成员的
@@ -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<AccountGroupMembersResponse>({
url: '/api/admin/account-groups/members',
method: 'GET',
params: data,
})
}
@@ -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<AccountGroup[]>([])
const membersByGroupId = shallowRef<Record<string, AccountGroupMember[]>>({})
const loading = shallowRef(false)
const loadingMemberIds = shallowRef<ReadonlySet<string>>(new Set())
const memberRequests = new Map<string, Promise<AccountGroupMember[]>>()
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,
}
}