Files
xinghuoapi/frontend/src/api/admin/users.ts
QTom 9a91815b94 feat(admin): 完整实现管理员修改用户 API Key 分组的功能
## 核心功能
- 添加 AdminUpdateAPIKeyGroupID 服务方法,支持绑定/解绑/保持不变三态语义
- 实现 UserRepository.AddGroupToAllowedGroups 接口,自动同步专属分组权限
- 添加 HTTP PUT /api-keys/:id handler 端点,支持管理员直接修改 API Key 分组

## 事务一致性
- 使用 ent Tx 保证专属分组绑定时「添加权限」和「更新 Key」的原子性
- Repository 方法支持 clientFromContext,兼容事务内调用
- 事务失败时自动回滚,避免权限孤立

## 业务逻辑
- 订阅类型分组阻断,需通过订阅管理流程
- 非活跃分组拒绝绑定
- 负 ID 和非法 ID 验证
- 自动授权响应,告知管理员成功授权的分组

## 代码质量
- 16 个单元测试覆盖所有业务路径和边界用例
- 7 个 handler 集成测试覆盖 HTTP 层
- GroupRepo stub 返回克隆副本,防止测试间数据泄漏
- API 类型安全修复(PaginatedResponse<ApiKey>)
- 前端 ref 回调类型对齐 Vue 规范

## 国际化支持
- 中英文提示信息完整
- 自动授权成功/失败提示
2026-02-28 20:18:14 +08:00

239 lines
5.8 KiB
TypeScript

/**
* Admin Users API endpoints
* Handles user management for administrators
*/
import { apiClient } from '../client'
import type { AdminUser, UpdateUserRequest, PaginatedResponse, ApiKey } from '@/types'
/**
* List all users with pagination
* @param page - Page number (default: 1)
* @param pageSize - Items per page (default: 20)
* @param filters - Optional filters (status, role, search, attributes)
* @param options - Optional request options (signal)
* @returns Paginated list of users
*/
export async function list(
page: number = 1,
pageSize: number = 20,
filters?: {
status?: 'active' | 'disabled'
role?: 'admin' | 'user'
search?: string
attributes?: Record<number, string> // attributeId -> value
},
options?: {
signal?: AbortSignal
}
): Promise<PaginatedResponse<AdminUser>> {
// Build params with attribute filters in attr[id]=value format
const params: Record<string, any> = {
page,
page_size: pageSize,
status: filters?.status,
role: filters?.role,
search: filters?.search
}
// Add attribute filters as attr[id]=value
if (filters?.attributes) {
for (const [attrId, value] of Object.entries(filters.attributes)) {
if (value) {
params[`attr[${attrId}]`] = value
}
}
}
const { data } = await apiClient.get<PaginatedResponse<AdminUser>>('/admin/users', {
params,
signal: options?.signal
})
return data
}
/**
* Get user by ID
* @param id - User ID
* @returns User details
*/
export async function getById(id: number): Promise<AdminUser> {
const { data } = await apiClient.get<AdminUser>(`/admin/users/${id}`)
return data
}
/**
* Create new user
* @param userData - User data (email, password, etc.)
* @returns Created user
*/
export async function create(userData: {
email: string
password: string
balance?: number
concurrency?: number
allowed_groups?: number[] | null
}): Promise<AdminUser> {
const { data } = await apiClient.post<AdminUser>('/admin/users', userData)
return data
}
/**
* Update user
* @param id - User ID
* @param updates - Fields to update
* @returns Updated user
*/
export async function update(id: number, updates: UpdateUserRequest): Promise<AdminUser> {
const { data } = await apiClient.put<AdminUser>(`/admin/users/${id}`, updates)
return data
}
/**
* Delete user
* @param id - User ID
* @returns Success confirmation
*/
export async function deleteUser(id: number): Promise<{ message: string }> {
const { data } = await apiClient.delete<{ message: string }>(`/admin/users/${id}`)
return data
}
/**
* Update user balance
* @param id - User ID
* @param balance - New balance
* @param operation - Operation type ('set', 'add', 'subtract')
* @param notes - Optional notes for the balance adjustment
* @returns Updated user
*/
export async function updateBalance(
id: number,
balance: number,
operation: 'set' | 'add' | 'subtract' = 'set',
notes?: string
): Promise<AdminUser> {
const { data } = await apiClient.post<AdminUser>(`/admin/users/${id}/balance`, {
balance,
operation,
notes: notes || ''
})
return data
}
/**
* Update user concurrency
* @param id - User ID
* @param concurrency - New concurrency limit
* @returns Updated user
*/
export async function updateConcurrency(id: number, concurrency: number): Promise<AdminUser> {
return update(id, { concurrency })
}
/**
* Toggle user status
* @param id - User ID
* @param status - New status
* @returns Updated user
*/
export async function toggleStatus(id: number, status: 'active' | 'disabled'): Promise<AdminUser> {
return update(id, { status })
}
/**
* Get user's API keys
* @param id - User ID
* @returns List of user's API keys
*/
export async function getUserApiKeys(id: number): Promise<PaginatedResponse<ApiKey>> {
const { data } = await apiClient.get<PaginatedResponse<ApiKey>>(`/admin/users/${id}/api-keys`)
return data
}
/**
* Get user's usage statistics
* @param id - User ID
* @param period - Time period
* @returns User usage statistics
*/
export async function getUserUsageStats(
id: number,
period: string = 'month'
): Promise<{
total_requests: number
total_cost: number
total_tokens: number
}> {
const { data } = await apiClient.get<{
total_requests: number
total_cost: number
total_tokens: number
}>(`/admin/users/${id}/usage`, {
params: { period }
})
return data
}
/**
* Balance history item returned from the API
*/
export interface BalanceHistoryItem {
id: number
code: string
type: string
value: number
status: string
used_by: number | null
used_at: string | null
created_at: string
group_id: number | null
validity_days: number
notes: string
user?: { id: number; email: string } | null
group?: { id: number; name: string } | null
}
// Balance history response extends pagination with total_recharged summary
export interface BalanceHistoryResponse extends PaginatedResponse<BalanceHistoryItem> {
total_recharged: number
}
/**
* Get user's balance/concurrency change history
* @param id - User ID
* @param page - Page number
* @param pageSize - Items per page
* @param type - Optional type filter (balance, admin_balance, concurrency, admin_concurrency, subscription)
* @returns Paginated balance history with total_recharged
*/
export async function getUserBalanceHistory(
id: number,
page: number = 1,
pageSize: number = 20,
type?: string
): Promise<BalanceHistoryResponse> {
const params: Record<string, any> = { page, page_size: pageSize }
if (type) params.type = type
const { data } = await apiClient.get<BalanceHistoryResponse>(
`/admin/users/${id}/balance-history`,
{ params }
)
return data
}
export const usersAPI = {
list,
getById,
create,
update,
delete: deleteUser,
updateBalance,
updateConcurrency,
toggleStatus,
getUserApiKeys,
getUserUsageStats,
getUserBalanceHistory
}
export default usersAPI