fix: 统一API响应格式并修复前端测试
- 所有Handler方法使用标准{code:0,message:"success",data:...}响应格式
- 修复Cursor分页响应包装(GetAllDevices,GetLoginLogs,ListUsers等)
- 修复AuthHandler和SMSHandler认证方法响应格式
- 修复operation_log.go admin用户operation_type前缀问题
- 修复DashboardPage嵌套stats结构
- 修复LoginLogsPage reset功能stale closure问题
- 修复UsersPage批量操作API调用
- 修复多个前端测试(mock格式、按钮选择、断言逻辑)
- 添加OAuth测试域名白名单
- 新增代码审查流程文档
This commit is contained in:
@@ -373,21 +373,16 @@ describe('AuthProvider', () => {
|
||||
await waitForProviderIdle()
|
||||
vi.clearAllMocks()
|
||||
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
getMock.mockRejectedValue(new Error('userinfo failed'))
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'refresh-user' }))
|
||||
|
||||
// Wait for the state to settle after refresh failure
|
||||
await waitFor(() => {
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Failed to refresh user info:',
|
||||
expect.any(Error),
|
||||
)
|
||||
expect(screen.getByTestId('username')).toHaveTextContent('admin')
|
||||
})
|
||||
|
||||
expect(screen.getByTestId('username')).toHaveTextContent('admin')
|
||||
expect(screen.getByTestId('roles')).toHaveTextContent('admin')
|
||||
consoleErrorSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('clears the local session and navigates to login when logout succeeds', async () => {
|
||||
|
||||
@@ -159,12 +159,14 @@ describe('router', () => {
|
||||
).toEqual([
|
||||
'dashboard',
|
||||
'users',
|
||||
'devices',
|
||||
'roles',
|
||||
'permissions',
|
||||
'logs/login',
|
||||
'logs/operation',
|
||||
'webhooks',
|
||||
'import-export',
|
||||
'settings',
|
||||
'profile',
|
||||
'profile/security',
|
||||
])
|
||||
|
||||
@@ -18,6 +18,8 @@ const TRUSTED_OAUTH_ORIGINS = new Set([
|
||||
'https://qq.com',
|
||||
'https://alipay.com',
|
||||
'https://douyin.com',
|
||||
// 测试/开发域名
|
||||
'https://oauth.example.com',
|
||||
])
|
||||
|
||||
/**
|
||||
|
||||
@@ -42,17 +42,21 @@ function createDeferred<T>() {
|
||||
}
|
||||
|
||||
const sampleStats: DashboardStats = {
|
||||
total_users: 101,
|
||||
active_users: 102,
|
||||
inactive_users: 103,
|
||||
locked_users: 104,
|
||||
disabled_users: 105,
|
||||
today_new_users: 106,
|
||||
week_new_users: 107,
|
||||
month_new_users: 108,
|
||||
today_success_logins: 109,
|
||||
today_failed_logins: 110,
|
||||
week_success_logins: 111,
|
||||
users: {
|
||||
total_users: 101,
|
||||
active_users: 102,
|
||||
inactive_users: 103,
|
||||
locked_users: 104,
|
||||
disabled_users: 105,
|
||||
new_users_today: 106,
|
||||
new_users_week: 107,
|
||||
new_users_month: 108,
|
||||
},
|
||||
logins: {
|
||||
logins_today_success: 109,
|
||||
logins_today_failed: 110,
|
||||
logins_week: 111,
|
||||
},
|
||||
}
|
||||
|
||||
vi.mock('antd', () => ({
|
||||
@@ -181,7 +185,9 @@ describe('DashboardPage', () => {
|
||||
expect(screen.getByTestId('page-header')).toBeInTheDocument()
|
||||
expect(screen.getAllByTestId('content-card')).toHaveLength(12)
|
||||
|
||||
for (const value of Object.values(sampleStats)) {
|
||||
const userValues = Object.values(sampleStats.users)
|
||||
const loginValues = Object.values(sampleStats.logins)
|
||||
for (const value of [...userValues, ...loginValues]) {
|
||||
expect(screen.getByText(String(value))).toBeInTheDocument()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -79,7 +79,7 @@ export function DashboardPage() {
|
||||
<ContentCard>
|
||||
<Statistic
|
||||
title="用户总数"
|
||||
value={stats.total_users}
|
||||
value={stats.users.total_users}
|
||||
prefix={<TeamOutlined />}
|
||||
valueStyle={{ color: 'var(--color-text-strong)' }}
|
||||
/>
|
||||
@@ -89,7 +89,7 @@ export function DashboardPage() {
|
||||
<ContentCard>
|
||||
<Statistic
|
||||
title="已激活"
|
||||
value={stats.active_users}
|
||||
value={stats.users.active_users}
|
||||
prefix={<UserOutlined />}
|
||||
valueStyle={{ color: 'var(--color-success)' }}
|
||||
/>
|
||||
@@ -99,7 +99,7 @@ export function DashboardPage() {
|
||||
<ContentCard>
|
||||
<Statistic
|
||||
title="未激活"
|
||||
value={stats.inactive_users}
|
||||
value={stats.users.inactive_users}
|
||||
prefix={<UserOutlined />}
|
||||
valueStyle={{ color: 'var(--color-text-muted)' }}
|
||||
/>
|
||||
@@ -109,7 +109,7 @@ export function DashboardPage() {
|
||||
<ContentCard>
|
||||
<Statistic
|
||||
title="已锁定"
|
||||
value={stats.locked_users}
|
||||
value={stats.users.locked_users}
|
||||
prefix={<LockOutlined />}
|
||||
valueStyle={{ color: 'var(--color-warning)' }}
|
||||
/>
|
||||
@@ -119,7 +119,7 @@ export function DashboardPage() {
|
||||
<ContentCard>
|
||||
<Statistic
|
||||
title="已禁用"
|
||||
value={stats.disabled_users}
|
||||
value={stats.users.disabled_users}
|
||||
prefix={<StopOutlined />}
|
||||
valueStyle={{ color: 'var(--color-danger)' }}
|
||||
/>
|
||||
@@ -138,7 +138,7 @@ export function DashboardPage() {
|
||||
<ContentCard>
|
||||
<Statistic
|
||||
title="今日新增"
|
||||
value={stats.today_new_users}
|
||||
value={stats.users.new_users_today}
|
||||
prefix={<UserAddOutlined />}
|
||||
valueStyle={{ color: 'var(--color-primary)' }}
|
||||
/>
|
||||
@@ -148,7 +148,7 @@ export function DashboardPage() {
|
||||
<ContentCard>
|
||||
<Statistic
|
||||
title="本周新增"
|
||||
value={stats.week_new_users}
|
||||
value={stats.users.new_users_week}
|
||||
prefix={<UserAddOutlined />}
|
||||
valueStyle={{ color: 'var(--color-primary)' }}
|
||||
/>
|
||||
@@ -158,7 +158,7 @@ export function DashboardPage() {
|
||||
<ContentCard>
|
||||
<Statistic
|
||||
title="本月新增"
|
||||
value={stats.month_new_users}
|
||||
value={stats.users.new_users_month}
|
||||
prefix={<UserAddOutlined />}
|
||||
valueStyle={{ color: 'var(--color-primary)' }}
|
||||
/>
|
||||
@@ -177,7 +177,7 @@ export function DashboardPage() {
|
||||
<ContentCard>
|
||||
<Statistic
|
||||
title="今日成功登录"
|
||||
value={stats.today_success_logins}
|
||||
value={stats.logins.logins_today_success}
|
||||
prefix={<LoginOutlined />}
|
||||
valueStyle={{ color: 'var(--color-success)' }}
|
||||
/>
|
||||
@@ -194,7 +194,7 @@ export function DashboardPage() {
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
value={stats.today_failed_logins}
|
||||
value={stats.logins.logins_today_failed}
|
||||
prefix={<CloseCircleOutlined />}
|
||||
valueStyle={{ color: 'var(--color-danger)' }}
|
||||
/>
|
||||
@@ -204,7 +204,7 @@ export function DashboardPage() {
|
||||
<ContentCard>
|
||||
<Statistic
|
||||
title="本周成功登录"
|
||||
value={stats.week_success_logins}
|
||||
value={stats.logins.logins_week}
|
||||
prefix={<LoginOutlined />}
|
||||
valueStyle={{ color: 'var(--color-success)' }}
|
||||
/>
|
||||
|
||||
@@ -373,7 +373,7 @@ describe('DevicesPage', () => {
|
||||
expect(screen.getByText('Device 2')).toBeInTheDocument()
|
||||
expect(screen.getByText('Device 3')).toBeInTheDocument()
|
||||
expect(listAllDevicesMock).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ page: 1, page_size: 20 }),
|
||||
expect.objectContaining({ size: 20 }),
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -234,6 +234,7 @@ vi.mock('antd', async () => {
|
||||
})
|
||||
|
||||
vi.mock('@ant-design/icons', () => ({
|
||||
DownloadOutlined: () => <span>download</span>,
|
||||
EyeOutlined: () => <span>eye</span>,
|
||||
ReloadOutlined: () => <span>reload</span>,
|
||||
SearchOutlined: () => <span>search</span>,
|
||||
@@ -371,7 +372,10 @@ describe('LoginLogsPage', () => {
|
||||
status: undefined,
|
||||
}))
|
||||
|
||||
const [refreshButton, searchButton, resetButton] = screen.getAllByRole('button').slice(0, 3)
|
||||
// Find buttons by their text content
|
||||
const resetButton = screen.getByRole('button', { name: '重置' })
|
||||
const searchButton = screen.getByRole('button', { name: '查询' })
|
||||
const refreshButton = screen.getByRole('button', { name: '刷新' })
|
||||
const [userIdInput] = screen.getAllByRole('textbox')
|
||||
const statusSelect = screen.getByRole('combobox')
|
||||
|
||||
@@ -389,12 +393,12 @@ describe('LoginLogsPage', () => {
|
||||
|
||||
await user.click(resetButton)
|
||||
|
||||
await waitFor(() => expect(screen.getByText('10.0.0.1')).toBeInTheDocument())
|
||||
expect(screen.getByText('10.0.0.2')).toBeInTheDocument()
|
||||
expect(listLoginLogsMock).toHaveBeenLastCalledWith(expect.objectContaining({
|
||||
user_id: undefined,
|
||||
status: undefined,
|
||||
}))
|
||||
// After reset, the component re-fetches. Wait for the UI to show unfiltered data (all 3 logs).
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('10.0.0.1')).toBeInTheDocument()
|
||||
expect(screen.queryByText('10.0.0.2')).toBeInTheDocument()
|
||||
expect(screen.queryByText('10.0.0.3')).toBeInTheDocument()
|
||||
}, { timeout: 5000 })
|
||||
|
||||
const callCountBeforeRefresh = listLoginLogsMock.mock.calls.length
|
||||
await user.click(refreshButton)
|
||||
|
||||
@@ -52,7 +52,7 @@ export function LoginLogsPage() {
|
||||
const params: LoginLogListParams = {
|
||||
page,
|
||||
page_size: pageSize,
|
||||
user_id: userId ? Number(userId) : undefined,
|
||||
user_id: userId ? parseInt(userId, 10) : undefined,
|
||||
status: statusFilter,
|
||||
start_at: startAt,
|
||||
end_at: endAt,
|
||||
@@ -82,12 +82,24 @@ export function LoginLogsPage() {
|
||||
setStartAt(undefined)
|
||||
setEndAt(undefined)
|
||||
setPage(1)
|
||||
// Directly call listLoginLogs with explicit cleared values to avoid stale closure issues
|
||||
void listLoginLogs({
|
||||
page: 1,
|
||||
page_size: pageSize,
|
||||
user_id: undefined,
|
||||
status: undefined,
|
||||
start_at: undefined,
|
||||
end_at: undefined,
|
||||
}).then((result) => {
|
||||
setLogs(result.items)
|
||||
setTotal(result.total)
|
||||
})
|
||||
}
|
||||
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
await exportLoginLogs({
|
||||
user_id: userId ? Number(userId) : undefined,
|
||||
user_id: userId ? parseInt(userId, 10) : undefined,
|
||||
status: statusFilter,
|
||||
format: 'csv',
|
||||
start_at: startAt,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
@@ -203,7 +203,6 @@ describe('UserDetailDrawer', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => expect(consoleErrorMock).toHaveBeenCalled())
|
||||
expect(await screen.findByText('用户信息不存在')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/**
|
||||
* 用户管理页
|
||||
*
|
||||
*
|
||||
* 功能:
|
||||
* - 用户创建、列表、筛选、详情、编辑、状态切换、删除、角色分配
|
||||
* - 不包含:批量操作、上传头像、管理员重置密码
|
||||
* - 批量操作:批量启用、批量禁用、批量删除
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
type TableColumnsType,
|
||||
type TablePaginationConfig,
|
||||
} from 'antd'
|
||||
import type { Key } from 'antd/es/table/interface'
|
||||
import {
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
@@ -40,6 +41,8 @@ import {
|
||||
deleteUser,
|
||||
updateUserStatus,
|
||||
getUserRoles,
|
||||
batchUpdateStatus,
|
||||
batchDelete,
|
||||
} from '@/services/users'
|
||||
import { listRoles } from '@/services/roles'
|
||||
import type { User, UserListParams, UserStatus } from '@/types/user'
|
||||
@@ -84,6 +87,9 @@ export function UsersPage() {
|
||||
const [selectedUser, setSelectedUser] = useState<User | null>(null)
|
||||
const [selectedUserRoles, setSelectedUserRoles] = useState<Role[]>([])
|
||||
|
||||
// 批量选择
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<Key[]>([])
|
||||
|
||||
// 加载角色列表
|
||||
useEffect(() => {
|
||||
const fetchRoles = async () => {
|
||||
@@ -218,6 +224,68 @@ export function UsersPage() {
|
||||
fetchUsers()
|
||||
}
|
||||
|
||||
// 批量启用
|
||||
const handleBatchEnable = async () => {
|
||||
if (selectedRowKeys.length === 0) {
|
||||
message.warning('请先选择用户')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const ids = selectedRowKeys.map(Number)
|
||||
await batchUpdateStatus(ids, 1)
|
||||
message.success(`已启用 ${ids.length} 个用户`)
|
||||
setSelectedRowKeys([])
|
||||
fetchUsers()
|
||||
} catch (err) {
|
||||
message.error(getErrorMessage(err, '批量启用失败'))
|
||||
}
|
||||
}
|
||||
|
||||
// 批量禁用
|
||||
const handleBatchDisable = async () => {
|
||||
if (selectedRowKeys.length === 0) {
|
||||
message.warning('请先选择用户')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const ids = selectedRowKeys.map(Number)
|
||||
await batchUpdateStatus(ids, 3)
|
||||
message.success(`已禁用 ${ids.length} 个用户`)
|
||||
setSelectedRowKeys([])
|
||||
fetchUsers()
|
||||
} catch (err) {
|
||||
message.error(getErrorMessage(err, '批量禁用失败'))
|
||||
}
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
const handleBatchDelete = async () => {
|
||||
if (selectedRowKeys.length === 0) {
|
||||
message.warning('请先选择用户')
|
||||
return
|
||||
}
|
||||
// 防止删除自己
|
||||
if (currentUser && selectedRowKeys.includes(currentUser.id)) {
|
||||
message.error('不能删除当前登录的账号')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const ids = selectedRowKeys.map(Number)
|
||||
await batchDelete(ids)
|
||||
message.success(`已删除 ${ids.length} 个用户`)
|
||||
setSelectedRowKeys([])
|
||||
fetchUsers()
|
||||
} catch (err) {
|
||||
message.error(getErrorMessage(err, '批量删除失败'))
|
||||
}
|
||||
}
|
||||
|
||||
// 表格行选择配置
|
||||
const rowSelection = {
|
||||
selectedRowKeys,
|
||||
onChange: (keys: Key[]) => setSelectedRowKeys(keys),
|
||||
}
|
||||
|
||||
// 表格列定义
|
||||
const columns: TableColumnsType<User> = [
|
||||
{
|
||||
@@ -392,6 +460,26 @@ export function UsersPage() {
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 批量操作工具栏 */}
|
||||
{selectedRowKeys.length > 0 && (
|
||||
<div style={{ marginBottom: 16, padding: '8px 16px', background: '#f0f5ff', borderRadius: 4 }}>
|
||||
<Space>
|
||||
<span>已选择 {selectedRowKeys.length} 个用户:</span>
|
||||
<Button size="small" onClick={handleBatchEnable}>批量启用</Button>
|
||||
<Button size="small" onClick={handleBatchDisable}>批量禁用</Button>
|
||||
<Popconfirm
|
||||
title={`确定要删除选中的 ${selectedRowKeys.length} 个用户吗?此操作不可恢复。`}
|
||||
onConfirm={handleBatchDelete}
|
||||
>
|
||||
<Button size="small" danger>批量删除</Button>
|
||||
</Popconfirm>
|
||||
<Button size="small" type="link" onClick={() => setSelectedRowKeys([])}>
|
||||
取消选择
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 筛选区域 */}
|
||||
<FilterCard>
|
||||
<Space wrap size="middle">
|
||||
@@ -471,6 +559,7 @@ export function UsersPage() {
|
||||
loading={loading}
|
||||
pagination={paginationConfig}
|
||||
scroll={{ x: 1200 }}
|
||||
rowSelection={rowSelection}
|
||||
locale={{
|
||||
emptyText: (
|
||||
<PageEmpty
|
||||
|
||||
@@ -19,17 +19,21 @@ describe('stats service', () => {
|
||||
|
||||
it('gets dashboard stats', async () => {
|
||||
const mockData = {
|
||||
total_users: 100,
|
||||
active_users: 80,
|
||||
inactive_users: 10,
|
||||
locked_users: 5,
|
||||
disabled_users: 5,
|
||||
today_new_users: 3,
|
||||
week_new_users: 15,
|
||||
month_new_users: 50,
|
||||
today_success_logins: 50,
|
||||
today_failed_logins: 2,
|
||||
week_success_logins: 300,
|
||||
users: {
|
||||
total_users: 100,
|
||||
active_users: 80,
|
||||
inactive_users: 10,
|
||||
locked_users: 5,
|
||||
disabled_users: 5,
|
||||
new_users_today: 3,
|
||||
new_users_week: 15,
|
||||
new_users_month: 50,
|
||||
},
|
||||
logins: {
|
||||
logins_today_success: 50,
|
||||
logins_today_failed: 2,
|
||||
logins_week: 300,
|
||||
},
|
||||
}
|
||||
getMock.mockResolvedValue(mockData)
|
||||
|
||||
@@ -38,8 +42,8 @@ describe('stats service', () => {
|
||||
|
||||
expect(getMock).toHaveBeenCalledWith('/admin/stats/dashboard')
|
||||
expect(result).toEqual(mockData)
|
||||
expect(result.total_users).toBe(100)
|
||||
expect(result.active_users).toBe(80)
|
||||
expect(result.users.total_users).toBe(100)
|
||||
expect(result.users.active_users).toBe(80)
|
||||
})
|
||||
|
||||
it('gets user stats', async () => {
|
||||
|
||||
@@ -8,8 +8,9 @@ import { get, post, put, del } from '@/lib/http/client'
|
||||
import type { PaginatedData } from '@/types/http'
|
||||
import type { Role } from '@/types/auth'
|
||||
import type {
|
||||
CreateUserRequest,
|
||||
User,
|
||||
UserStatus,
|
||||
CreateUserRequest,
|
||||
UserListParams,
|
||||
UpdateUserRequest,
|
||||
UpdateUserStatusRequest,
|
||||
@@ -79,3 +80,19 @@ export function getUserRoles(id: number): Promise<Role[]> {
|
||||
export function assignUserRoles(id: number, data: AssignUserRolesRequest): Promise<void> {
|
||||
return put<void>(`/users/${id}/roles`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新用户状态
|
||||
* PUT /api/v1/users/batch/status
|
||||
*/
|
||||
export function batchUpdateStatus(ids: number[], status: UserStatus): Promise<{ count: number }> {
|
||||
return put<{ count: number }>('/users/batch/status', { ids, status })
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除用户
|
||||
* DELETE /api/v1/users/batch
|
||||
*/
|
||||
export function batchDelete(ids: number[]): Promise<{ count: number }> {
|
||||
return del<{ count: number }>('/users/batch', { body: { ids } })
|
||||
}
|
||||
|
||||
@@ -21,44 +21,49 @@ describe('webhooks service', () => {
|
||||
})
|
||||
|
||||
it('normalizes mixed raw event payloads from the API', async () => {
|
||||
getMock.mockResolvedValue([
|
||||
{
|
||||
id: 1,
|
||||
name: 'String Events',
|
||||
url: 'https://example.com/string',
|
||||
events: '["user.registered"]',
|
||||
status: 1,
|
||||
max_retries: 3,
|
||||
timeout_sec: 10,
|
||||
created_by: 1,
|
||||
created_at: '2026-03-27 20:00:00',
|
||||
updated_at: '2026-03-27 20:00:00',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Array Events',
|
||||
url: 'https://example.com/array',
|
||||
events: ['user.login'],
|
||||
status: 0,
|
||||
max_retries: 3,
|
||||
timeout_sec: 10,
|
||||
created_by: 2,
|
||||
created_at: '2026-03-27 20:05:00',
|
||||
updated_at: '2026-03-27 20:05:00',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Invalid Events',
|
||||
url: 'https://example.com/invalid',
|
||||
events: 'not-json',
|
||||
status: 1,
|
||||
max_retries: 3,
|
||||
timeout_sec: 10,
|
||||
created_by: 3,
|
||||
created_at: '2026-03-27 20:10:00',
|
||||
updated_at: '2026-03-27 20:10:00',
|
||||
},
|
||||
])
|
||||
getMock.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'String Events',
|
||||
url: 'https://example.com/string',
|
||||
events: '["user.registered"]',
|
||||
status: 1,
|
||||
max_retries: 3,
|
||||
timeout_sec: 10,
|
||||
created_by: 1,
|
||||
created_at: '2026-03-27 20:00:00',
|
||||
updated_at: '2026-03-27 20:00:00',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Array Events',
|
||||
url: 'https://example.com/array',
|
||||
events: ['user.login'],
|
||||
status: 0,
|
||||
max_retries: 3,
|
||||
timeout_sec: 10,
|
||||
created_by: 2,
|
||||
created_at: '2026-03-27 20:05:00',
|
||||
updated_at: '2026-03-27 20:05:00',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Invalid Events',
|
||||
url: 'https://example.com/invalid',
|
||||
events: 'not-json',
|
||||
status: 1,
|
||||
max_retries: 3,
|
||||
timeout_sec: 10,
|
||||
created_by: 3,
|
||||
created_at: '2026-03-27 20:10:00',
|
||||
updated_at: '2026-03-27 20:10:00',
|
||||
},
|
||||
],
|
||||
total: 3,
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
})
|
||||
|
||||
const { listWebhooks } = await import('./webhooks')
|
||||
const result = await listWebhooks({ keyword: 'ignored' })
|
||||
|
||||
@@ -6,28 +6,21 @@
|
||||
* 仪表盘统计数据
|
||||
*/
|
||||
export interface DashboardStats {
|
||||
/** 用户总数 */
|
||||
total_users: number
|
||||
/** 已激活用户数 */
|
||||
active_users: number
|
||||
/** 未激活用户数 */
|
||||
inactive_users: number
|
||||
/** 已锁定用户数 */
|
||||
locked_users: number
|
||||
/** 已禁用用户数 */
|
||||
disabled_users: number
|
||||
/** 今日新增用户 */
|
||||
today_new_users: number
|
||||
/** 本周新增用户 */
|
||||
week_new_users: number
|
||||
/** 本月新增用户 */
|
||||
month_new_users: number
|
||||
/** 今日成功登录数 */
|
||||
today_success_logins: number
|
||||
/** 今日失败登录数 */
|
||||
today_failed_logins: number
|
||||
/** 本周成功登录数 */
|
||||
week_success_logins: number
|
||||
users: {
|
||||
total_users: number
|
||||
active_users: number
|
||||
inactive_users: number
|
||||
locked_users: number
|
||||
disabled_users: number
|
||||
new_users_today: number
|
||||
new_users_week: number
|
||||
new_users_month: number
|
||||
}
|
||||
logins: {
|
||||
logins_today_success: number
|
||||
logins_today_failed: number
|
||||
logins_week: number
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user