refactor(frontend): colocate admin components and split service layer

Move route-specific admin components from components/common/admin into
app/(main)/admin/<feature>/components/. Split AdminService god object into
domain services, fix UploadService to use BaseService, add AdminUploadService,
consolidate DB export into DbManageService, and migrate consumers to the
unified services entry.
This commit is contained in:
ryan
2026-06-18 10:55:14 +08:00
parent de8a21a49f
commit beae8d2dd9
56 changed files with 594 additions and 888 deletions
@@ -1,543 +0,0 @@
import {BaseService} from '@/lib/services/core';
import type {
AdminUser,
AppUpdateStatus,
AuthSource,
AuthSourceRequest,
CacheConfig,
CacheStatus,
CreateScheduleRequest,
CreateSystemConfigRequest,
CreateTemplateRequest,
CreateUserRequest,
DatabaseInfo,
DispatchTaskRequest,
ListTaskExecutionsRequest,
ListTaskExecutionsResponse,
ListUsersRequest,
ListUsersResponse,
Schedule,
SystemConfig,
SystemStatus,
TaskExecution,
TaskMeta,
TaskParamType,
TaskTypeResponse,
Template,
ToggleAuthSourceRequest,
UpdateScheduleRequest,
UpdateSystemConfigRequest,
UpdateTemplateRequest,
UpdateUserStatusRequest,
} from './types';
export type { AdminUser, CacheStatus, CacheConfig } from './types';
/**
* 管理员服务
*
* @remarks
* 所有接口都需要管理员权限
*/
export class AdminService extends BaseService {
protected static readonly basePath = '/api/v1/admin';
// ==================== 系统配置管理 ====================
/**
* 创建系统配置
* @param request - 创建系统配置的请求参数
* @returns void
* @throws {UnauthorizedError} 当未登录时
* @throws {ForbiddenError} 当无管理员权限时
* @throws {ValidationError} 当参数验证失败或配置键已存在时
*
* @example
* ```typescript
* await AdminService.createSystemConfig({
* key: 'app.version',
* value: '1.0.0',
* description: '应用版本号'
* });
* ```
*/
static async createSystemConfig(
request: CreateSystemConfigRequest,
): Promise<void> {
return this.post<void>('/system-configs', request);
}
/**
* 获取系统配置列表
* @returns 系统配置列表
* @throws {UnauthorizedError} 当未登录时
* @throws {ForbiddenError} 当无管理员权限时
*
* @example
* ```typescript
* const configs = await AdminService.listSystemConfigs();
* console.log('系统配置数量:', configs.length);
* ```
*/
static async listSystemConfigs(type?: 'system' | 'business'): Promise<SystemConfig[]> {
const query = type ? `?type=${type}` : '';
return this.get<SystemConfig[]>(`/system-configs${query}`);
}
/**
* 获取单个系统配置
* @param key - 配置键
* @returns 系统配置信息
* @throws {UnauthorizedError} 当未登录时
* @throws {ForbiddenError} 当无管理员权限时
* @throws {NotFoundError} 当配置不存在时
*
* @example
* ```typescript
* const config = await AdminService.getSystemConfig('app.version');
* console.log('应用版本:', config.value);
* ```
*/
static async getSystemConfig(key: string): Promise<SystemConfig> {
return this.get<SystemConfig>(`/system-configs/${ key }`);
}
/**
* 更新系统配置
* @param key - 配置键
* @param request - 更新系统配置的请求参数
* @returns void
* @throws {UnauthorizedError} 当未登录时
* @throws {ForbiddenError} 当无管理员权限时
* @throws {NotFoundError} 当配置不存在时
* @throws {ValidationError} 当参数验证失败时
*
* @example
* ```typescript
* await AdminService.updateSystemConfig('app.version', {
* value: '1.1.0',
* description: '更新到新版本'
* });
* ```
*/
static async updateSystemConfig(
key: string,
request: UpdateSystemConfigRequest,
): Promise<void> {
return this.put<void>(`/system-configs/${ key }`, request);
}
static async testSMTP(request: {
smtp_host: string;
smtp_port: number;
smtp_username: string;
smtp_password: string;
to: string;
}): Promise<{ success: boolean; log: string; error: string }> {
return this.post<{ success: boolean; log: string; error: string }>('/system-configs/smtp/test', request);
}
// ==================== 应用更新 ====================
static async getUpdateStatus(): Promise<AppUpdateStatus> {
return this.get<AppUpdateStatus>('/update');
}
static async applyUpdate(): Promise<void> {
return this.post<void>('/update/apply');
}
/**
* 获取所有已存在的文件业务类型及默认内置类型列表
* @returns 业务类型列表
*/
static async listUploadTypes(): Promise<string[]> {
return this.get<string[]>('/uploads/types');
}
// ==================== 认证源管理 ====================
static async listAuthSources(): Promise<AuthSource[]> {
return this.get<AuthSource[]>('/auth-sources');
}
static async createAuthSource(request: AuthSourceRequest): Promise<AuthSource> {
return this.post<AuthSource>('/auth-sources', request);
}
static async updateAuthSource(id: string, request: AuthSourceRequest): Promise<AuthSource> {
return this.put<AuthSource>(`/auth-sources/${ id }`, request);
}
static async toggleAuthSource(id: string, request: ToggleAuthSourceRequest): Promise<void> {
return this.put<void>(`/auth-sources/${ id }/toggle`, request);
}
static async deleteAuthSource(id: string): Promise<void> {
return this.delete<void>(`/auth-sources/${ id }`);
}
// ==================== 任务管理 ====================
/**
* 获取支持的任务类型列表
* @returns 任务类型列表
* @throws {UnauthorizedError} 当未登录时
* @throws {ForbiddenError} 当无管理员权限时
*
* @example
* ```typescript
* const taskTypes = await AdminService.getTaskTypes();
* console.log('可用任务类型:', taskTypes);
* ```
*/
static async getTaskTypes(): Promise<TaskMeta[]> {
const response = await this.get<TaskTypeResponse[]>('/tasks/types');
// Adapt backend PascalCase to frontend snake_case
return response.map(item => ({
type: item.Type || item.type || '',
asynq_task: item.AsynqTask || item.asynq_task || '',
name: item.Name || item.name || '',
description: item.Description || item.description || '',
supports_time: item.SupportsTime ?? item.supports_time ?? false,
max_retry: item.MaxRetry ?? item.max_retry ?? 0,
queue: item.Queue || item.queue || '',
params: (item.Params || item.params || []).map(p => ({
name: p.Name || p.name || '',
label: p.Label || p.label || '',
// Cast to TaskParamType: backend only emits 'string' | 'text' | 'number'.
// Unknown future values fall through safely to the string branch.
type: (p.Type || p.type || 'string') as TaskParamType,
required: p.Required ?? p.required ?? false,
placeholder: p.Placeholder || p.placeholder || '',
description: p.Description || p.description || '',
})),
}));
}
/**
* 下发任务
* @param request - 下发任务请求参数
* @returns void
* @throws {UnauthorizedError} 当未登录时
* @throws {ForbiddenError} 当无管理员权限时
* @throws {ValidationError} 当参数验证失败时
* @remarks
* - 不同任务类型需要不同的参数
* - order_sync 支持 start_time 和 end_time 参数
* - user_gamification 需要 user_id 参数
* - 其他任务无需额外参数
*/
static async dispatchTask(request: DispatchTaskRequest): Promise<string> {
return this.post<string>('/tasks/dispatch', request);
}
/**
* 查询任务执行记录列表
*/
static async listTaskExecutions(
request: ListTaskExecutionsRequest = {},
): Promise<ListTaskExecutionsResponse> {
return this.get<ListTaskExecutionsResponse>(
'/tasks/executions',
request as unknown as Record<string, unknown>,
);
}
/**
* 查询任务执行详情
*/
static async getTaskExecution(id: string): Promise<TaskExecution> {
return this.get<TaskExecution>(`/tasks/executions/${ id }`);
}
/**
* 重试失败任务
*/
static async retryTaskExecution(id: string): Promise<string> {
return this.post<string>(`/tasks/executions/${ id }/retry`);
}
// ==================== 定时任务管理 ====================
/**
* 获取定时任务列表
*/
static async listSchedules(): Promise<Schedule[]> {
return this.get<Schedule[]>('/tasks/schedules');
}
/**
* 创建定时任务
*/
static async createSchedule(request: CreateScheduleRequest): Promise<Schedule> {
return this.post<Schedule>('/tasks/schedules', request);
}
/**
* 更新定时任务
*/
static async updateSchedule(id: string, request: UpdateScheduleRequest): Promise<Schedule> {
return this.put<Schedule>(`/tasks/schedules/${ id }`, request);
}
/**
* 删除定时任务
*/
static async deleteSchedule(id: string): Promise<void> {
return this.delete<void>(`/tasks/schedules/${ id }`);
}
// ==================== 用户管理 ====================
/**
* 获取用户列表
* @param request - 查询参数
* @returns 用户列表及总数
* @throws {UnauthorizedError} 当未登录时
* @throws {ForbiddenError} 当无管理员权限时
* @throws {ValidationError} 当参数验证失败时
*
* @example
* ```typescript
* const result = await AdminService.listUsers({
* page: 1,
* page_size: 20,
* user_id: '10001',
* username: 'test'
* });
* console.log('用户总数:', result.total);
* console.log('用户列表:', result.users);
* ```
*
* @remarks
* - page 从 1 开始
* - page_size 范围 1-100
* - user_id 按用户 ID 精确搜索
* - username 按用户名做前缀搜索
*/
static async listUsers(request: ListUsersRequest): Promise<ListUsersResponse> {
return this.get<ListUsersResponse>('/users', request as unknown as Record<string, unknown>);
}
/**
* 获取用户详情
* @param id - 用户 ID
* @returns 用户完整资料
*/
static async getUser(id: string): Promise<AdminUser> {
return this.get<AdminUser>(`/users/${ id }`);
}
/**
* 更新用户状态
* @param id - 用户 ID
* @param request - 更新状态请求参数
* @returns void
* @throws {UnauthorizedError} 当未登录时
* @throws {ForbiddenError} 当无管理员权限或禁用管理员用户时
* @throws {NotFoundError} 当用户不存在时
*
* @example
* ```typescript
* // 禁用用户
* await AdminService.updateUserStatus(123, { is_active: false });
*
* // 启用用户
* await AdminService.updateUserStatus(123, { is_active: true });
* ```
*
* @remarks
* - 不能禁用管理员用户
*/
static async updateUserStatus(
id: string,
request: UpdateUserStatusRequest
): Promise<void> {
return this.put<void>(`/users/${ id }/status`, request);
}
/**
* 创建用户
* @param request - 创建用户请求参数
* @returns 创建成功的用户信息
*/
static async createUser(request: CreateUserRequest): Promise<AdminUser> {
return this.post<AdminUser>('/users', request);
}
/**
* 删除用户
* @param id - 用户 ID
*/
static async deleteUser(id: string): Promise<void> {
return this.delete<void>(`/users/${ id }`);
}
/**
* 获取系统状态
* @returns 系统状态指标数据
* @throws {UnauthorizedError} 当未登录时
* @throws {ForbiddenError} 当无管理员权限时
*/
static async getSystemStatus(): Promise<SystemStatus> {
return this.get<SystemStatus>('/status');
}
/**
* 获取数据库信息
* @returns 数据库类型、名称、版本信息
* @throws {UnauthorizedError} 当未登录时
* @throws {ForbiddenError} 当无管理员权限时
*/
static async getDatabaseInfo(): Promise<DatabaseInfo> {
return this.get<DatabaseInfo>('/db-info');
}
/**
* 导出数据库
* SQLite 返回 .db 二进制,PostgreSQL 返回 pg_dump .sql 文本
* @returns 包含文件 Blob 和建议文件名的对象
* @throws {UnauthorizedError} 当未登录时
* @throws {ForbiddenError} 当无管理员权限时
*/
static async exportDatabase(): Promise<{ blob: Blob; filename: string }> {
const response = await import('axios').then(({ default: axios }) =>
axios.get<Blob>(this.getFullPath('/db-export'), {
withCredentials: true,
responseType: 'blob',
})
);
// 从 Content-Disposition 提取文件名
const disposition = response.headers['content-disposition'] as string | undefined;
let filename = 'wavelet_export';
if (disposition) {
const match = disposition.match(/filename="?([^";]+)"?/);
if (match) filename = match[1];
}
return { blob: response.data, filename };
}
// ==================== 系统日志 ====================
/**
* 获取系统历史日志
* @param cursor - 日志游标,0=获取最新,>0=获取更早
* @param limit - 每页条数,默认 200
*/
static async getLogs(cursor: number = 0, limit: number = 200): Promise<{
lines: Array<{ index: number; data: string }>;
has_more: boolean;
next_cursor: number;
}> {
return this.get('/logs', { cursor, limit });
}
/**
* 获取 ClickHouse 访问日志列表
*/
static async getAccessLogs(params: {
page: number;
page_size: number;
username?: string;
path?: string;
start_time?: string;
end_time?: string;
}): Promise<{
total: number;
list: Array<{
id: string;
user_id: string;
username: string;
nickname: string;
path: string;
method: string;
ip: string;
user_agent: string;
headers: string;
status: number;
latency: number;
created_at: string;
}>;
}> {
return this.get('/logs/access', params as Record<string, unknown>);
}
/**
* 获取 ClickHouse 访问日志图表聚合指标
*/
static async getLogsAnalytics(): Promise<{
trend: Array<{ date: string; count: number }>;
browsers: Array<{ browser: string; count: number }>;
top_users: Array<{
user_id: string;
username: string;
nickname: string;
count: number;
}>;
}> {
return this.get('/logs/analytics');
}
// ==================== 模板管理 ====================
/**
* 获取模板列表
*/
static async listTemplates(): Promise<Template[]> {
return this.get<Template[]>('/templates');
}
/**
* 获取单个模板
*/
static async getTemplate(key: string): Promise<Template> {
return this.get<Template>(`/templates/${ key }`);
}
/**
* 创建模板
*/
static async createTemplate(request: CreateTemplateRequest): Promise<Template> {
return this.post<Template>('/templates', request);
}
/**
* 更新模板
*/
static async updateTemplate(key: string, request: UpdateTemplateRequest): Promise<Template> {
return this.put<Template>(`/templates/${ key }`, request);
}
/**
* 删除模板
*/
static async deleteTemplate(key: string): Promise<void> {
return this.delete<void>(`/templates/${ key }`);
}
// ==================== 磁盘缓存管理 ====================
/**
* 获取磁盘缓存状态与当前统计数据
*/
static async getCacheStatus(): Promise<CacheStatus> {
return this.get<CacheStatus>('/cache/status');
}
/**
* 更新磁盘缓存策略配置
*/
static async updateCacheConfig(config: CacheConfig): Promise<void> {
return this.post<void>('/cache/config', config);
}
/**
* 一键清空所有磁盘缓存数据
*/
static async clearCache(): Promise<void> {
return this.post<void>('/cache/clear');
}
}
@@ -0,0 +1,26 @@
import {BaseService} from '@/lib/services/core';
import type {AuthSource, AuthSourceRequest, ToggleAuthSourceRequest} from './types';
export class AdminAuthSourceService extends BaseService {
protected static readonly basePath = '/api/v1/admin';
static async listAuthSources(): Promise<AuthSource[]> {
return this.get<AuthSource[]>('/auth-sources');
}
static async createAuthSource(request: AuthSourceRequest): Promise<AuthSource> {
return this.post<AuthSource>('/auth-sources', request);
}
static async updateAuthSource(id: string, request: AuthSourceRequest): Promise<AuthSource> {
return this.put<AuthSource>(`/auth-sources/${id}`, request);
}
static async toggleAuthSource(id: string, request: ToggleAuthSourceRequest): Promise<void> {
return this.put<void>(`/auth-sources/${id}/toggle`, request);
}
static async deleteAuthSource(id: string): Promise<void> {
return this.delete<void>(`/auth-sources/${id}`);
}
}
@@ -0,0 +1,18 @@
import {BaseService} from '@/lib/services/core';
import type {CacheConfig, CacheStatus} from './types';
export class AdminCacheService extends BaseService {
protected static readonly basePath = '/api/v1/admin';
static async getCacheStatus(): Promise<CacheStatus> {
return this.get<CacheStatus>('/cache/status');
}
static async updateCacheConfig(config: CacheConfig): Promise<void> {
return this.post<void>('/cache/config', config);
}
static async clearCache(): Promise<void> {
return this.post<void>('/cache/clear');
}
}
+15 -23
View File
@@ -1,25 +1,12 @@
/**
* 管理员服务模块
*
* @description
* 提供系统配置、用户与任务管理功能,包括:
* - 系统配置管理(创建、查询、更新、删除)
* - 异步任务配置及下发
* - 用户账号状态管理
*
* @remarks
* 所有接口都需要管理员权限
*
* @example
* ```typescript
* import { AdminService } from '@/lib/services';
*
* // 获取系统配置列表
* const configs = await AdminService.listSystemConfigs();
* ```
*/
export { AdminSystemConfigService } from './system-config.service';
export { AdminAuthSourceService } from './auth-source.service';
export { AdminTaskService } from './task.service';
export { AdminUserService } from './user.service';
export { AdminStatusService } from './status.service';
export { AdminLogService } from './log.service';
export { AdminTemplateService } from './template.service';
export { AdminCacheService } from './cache.service';
export { AdminService } from './admin.service';
export type {
SystemConfig,
CreateSystemConfigRequest,
@@ -41,11 +28,16 @@ export type {
ListUsersResponse,
UpdateUserStatusRequest,
SystemStatus,
DatabaseInfo,
AppUpdateStatus,
Schedule,
CreateScheduleRequest,
UpdateScheduleRequest,
CacheStatus,
CacheConfig,
} from './types';
Template,
CreateTemplateRequest,
UpdateTemplateRequest,
StorageDriver,
StorageConfig,
ObjectStorageConfig,
} from './types';
@@ -0,0 +1,53 @@
import {BaseService} from '@/lib/services/core';
export class AdminLogService extends BaseService {
protected static readonly basePath = '/api/v1/admin';
static async getLogs(cursor: number = 0, limit: number = 200): Promise<{
lines: Array<{ index: number; data: string }>;
has_more: boolean;
next_cursor: number;
}> {
return this.get('/logs', { cursor, limit });
}
static async getAccessLogs(params: {
page: number;
page_size: number;
username?: string;
path?: string;
start_time?: string;
end_time?: string;
}): Promise<{
total: number;
list: Array<{
id: string;
user_id: string;
username: string;
nickname: string;
path: string;
method: string;
ip: string;
user_agent: string;
headers: string;
status: number;
latency: number;
created_at: string;
}>;
}> {
return this.get('/logs/access', params as Record<string, unknown>);
}
static async getLogsAnalytics(): Promise<{
trend: Array<{ date: string; count: number }>;
browsers: Array<{ browser: string; count: number }>;
top_users: Array<{
user_id: string;
username: string;
nickname: string;
count: number;
}>;
}> {
return this.get('/logs/analytics');
}
}
@@ -0,0 +1,18 @@
import {BaseService} from '@/lib/services/core';
import type {AppUpdateStatus, SystemStatus} from './types';
export class AdminStatusService extends BaseService {
protected static readonly basePath = '/api/v1/admin';
static async getSystemStatus(): Promise<SystemStatus> {
return this.get<SystemStatus>('/status');
}
static async getUpdateStatus(): Promise<AppUpdateStatus> {
return this.get<AppUpdateStatus>('/update');
}
static async applyUpdate(): Promise<void> {
return this.post<void>('/update/apply');
}
}
@@ -0,0 +1,44 @@
import {BaseService} from '@/lib/services/core';
import type {
CreateSystemConfigRequest,
SystemConfig,
UpdateSystemConfigRequest,
} from './types';
export class AdminSystemConfigService extends BaseService {
protected static readonly basePath = '/api/v1/admin';
static async createSystemConfig(request: CreateSystemConfigRequest): Promise<void> {
return this.post<void>('/system-configs', request);
}
static async listSystemConfigs(type?: 'system' | 'business'): Promise<SystemConfig[]> {
const query = type ? `?type=${type}` : '';
return this.get<SystemConfig[]>(`/system-configs${query}`);
}
static async getSystemConfig(key: string): Promise<SystemConfig> {
return this.get<SystemConfig>(`/system-configs/${key}`);
}
static async updateSystemConfig(key: string, request: UpdateSystemConfigRequest): Promise<void> {
return this.put<void>(`/system-configs/${key}`, request);
}
static async testSMTP(request: {
smtp_host: string;
smtp_port: number;
smtp_username: string;
smtp_password: string;
to: string;
}): Promise<{ success: boolean; log: string; error: string }> {
return this.post<{ success: boolean; log: string; error: string }>(
'/system-configs/smtp/test',
request,
);
}
static async listUploadTypes(): Promise<string[]> {
return this.get<string[]>('/uploads/types');
}
}
@@ -0,0 +1,75 @@
import {BaseService} from '@/lib/services/core';
import type {
CreateScheduleRequest,
DispatchTaskRequest,
ListTaskExecutionsRequest,
ListTaskExecutionsResponse,
Schedule,
TaskExecution,
TaskMeta,
TaskParamType,
TaskTypeResponse,
UpdateScheduleRequest,
} from './types';
export class AdminTaskService extends BaseService {
protected static readonly basePath = '/api/v1/admin';
static async getTaskTypes(): Promise<TaskMeta[]> {
const response = await this.get<TaskTypeResponse[]>('/tasks/types');
return response.map((item) => ({
type: item.Type || item.type || '',
asynq_task: item.AsynqTask || item.asynq_task || '',
name: item.Name || item.name || '',
description: item.Description || item.description || '',
supports_time: item.SupportsTime ?? item.supports_time ?? false,
max_retry: item.MaxRetry ?? item.max_retry ?? 0,
queue: item.Queue || item.queue || '',
params: (item.Params || item.params || []).map((p) => ({
name: p.Name || p.name || '',
label: p.Label || p.label || '',
type: (p.Type || p.type || 'string') as TaskParamType,
required: p.Required ?? p.required ?? false,
placeholder: p.Placeholder || p.placeholder || '',
description: p.Description || p.description || '',
})),
}));
}
static async dispatchTask(request: DispatchTaskRequest): Promise<string> {
return this.post<string>('/tasks/dispatch', request);
}
static async listTaskExecutions(
request: ListTaskExecutionsRequest = {},
): Promise<ListTaskExecutionsResponse> {
return this.get<ListTaskExecutionsResponse>(
'/tasks/executions',
request as unknown as Record<string, unknown>,
);
}
static async getTaskExecution(id: string): Promise<TaskExecution> {
return this.get<TaskExecution>(`/tasks/executions/${id}`);
}
static async retryTaskExecution(id: string): Promise<string> {
return this.post<string>(`/tasks/executions/${id}/retry`);
}
static async listSchedules(): Promise<Schedule[]> {
return this.get<Schedule[]>('/tasks/schedules');
}
static async createSchedule(request: CreateScheduleRequest): Promise<Schedule> {
return this.post<Schedule>('/tasks/schedules', request);
}
static async updateSchedule(id: string, request: UpdateScheduleRequest): Promise<Schedule> {
return this.put<Schedule>(`/tasks/schedules/${id}`, request);
}
static async deleteSchedule(id: string): Promise<void> {
return this.delete<void>(`/tasks/schedules/${id}`);
}
}
@@ -0,0 +1,26 @@
import {BaseService} from '@/lib/services/core';
import type {CreateTemplateRequest, Template, UpdateTemplateRequest} from './types';
export class AdminTemplateService extends BaseService {
protected static readonly basePath = '/api/v1/admin';
static async listTemplates(): Promise<Template[]> {
return this.get<Template[]>('/templates');
}
static async getTemplate(key: string): Promise<Template> {
return this.get<Template>(`/templates/${key}`);
}
static async createTemplate(request: CreateTemplateRequest): Promise<Template> {
return this.post<Template>('/templates', request);
}
static async updateTemplate(key: string, request: UpdateTemplateRequest): Promise<Template> {
return this.put<Template>(`/templates/${key}`, request);
}
static async deleteTemplate(key: string): Promise<void> {
return this.delete<void>(`/templates/${key}`);
}
}
-12
View File
@@ -456,18 +456,6 @@ export interface UpdateTemplateRequest {
description: string;
}
/**
* 数据库信息
*/
export interface DatabaseInfo {
/** 数据库类型:'sqlite' | 'postgres' */
type: 'sqlite' | 'postgres';
/** 数据库名称或文件路径 */
name: string;
/** 数据库版本字符串 */
version: string;
}
/**
* 应用更新状态
*/
@@ -0,0 +1,32 @@
import {BaseService} from '@/lib/services/core';
import type {
AdminUser,
CreateUserRequest,
ListUsersRequest,
ListUsersResponse,
UpdateUserStatusRequest,
} from './types';
export class AdminUserService extends BaseService {
protected static readonly basePath = '/api/v1/admin';
static async listUsers(request: ListUsersRequest): Promise<ListUsersResponse> {
return this.get<ListUsersResponse>('/users', request as unknown as Record<string, unknown>);
}
static async getUser(id: string): Promise<AdminUser> {
return this.get<AdminUser>(`/users/${id}`);
}
static async updateUserStatus(id: string, request: UpdateUserStatusRequest): Promise<void> {
return this.put<void>(`/users/${id}/status`, request);
}
static async createUser(request: CreateUserRequest): Promise<AdminUser> {
return this.post<AdminUser>('/users', request);
}
static async deleteUser(id: string): Promise<void> {
return this.delete<void>(`/users/${id}`);
}
}
@@ -1,3 +1,6 @@
import type {InternalAxiosRequestConfig} from 'axios';
import apiClient from '@/lib/services/core/api-client';
import {BaseService} from '@/lib/services/core';
import type {DBOverview, ExecuteSQLResponse, TableDataResponse} from './types';
@@ -38,4 +41,22 @@ export class DbManageService extends BaseService {
static async executeSQL(sql: string): Promise<ExecuteSQLResponse> {
return this.post<ExecuteSQLResponse>('/query', { sql });
}
/**
* 导出数据库备份(SQLite .db / PostgreSQL .sql)
*/
static async exportDatabase(): Promise<{ blob: Blob; filename: string }> {
const response = await apiClient.get<Blob>('/api/v1/admin/db-export', {
withCredentials: true,
responseType: 'blob',
} as InternalAxiosRequestConfig);
const disposition = response.headers['content-disposition'] as string | undefined;
let filename = 'wavelet_export';
if (disposition) {
const match = disposition.match(/filename="?([^";]+)"?/);
if (match) filename = match[1];
}
return { blob: response.data, filename };
}
}
+47 -42
View File
@@ -1,58 +1,52 @@
/**
* 服务层统一入口
* 提供所有业务服务的访问接口
*
* @example
* ```typescript
* // 推荐:使用统一的 services 对象
* import services from '@/lib/services';
*
* const user = await services.auth.getUserInfo();
* const systemConfig = await services.admin.listSystemConfigs();
* ```
*
* @example
* ```typescript
* // 按需导入:直接导入特定服务
* import { AuthService } from '@/lib/services';
*
* const user = await AuthService.getUserInfo();
* const configs = await services.adminSystemConfig.listSystemConfigs();
* ```
*/
import {
AdminAuthSourceService,
AdminCacheService,
AdminLogService,
AdminStatusService,
AdminSystemConfigService,
AdminTaskService,
AdminTemplateService,
AdminUserService,
} from './admin';
import {AuthService} from './auth';
import {AdminService} from './admin';
import {UserService} from './user';
import {ConfigService} from './config';
import {UploadService} from './upload';
import {DbManageService} from './db-manage';
import {PushService} from './push';
import {AdminUploadService, UploadService} from './upload';
import {UserService} from './user';
/**
* 服务对象
* 集中导出所有业务服务
*
* @description
* 推荐使用此对象访问所有服务,保持代码风格统一
*/
const services = {
/** 认证服务 */
auth: AuthService,
/** 管理员服务 */
admin: AdminService,
/** 用户服务 */
adminSystemConfig: AdminSystemConfigService,
adminAuthSource: AdminAuthSourceService,
adminTask: AdminTaskService,
adminUser: AdminUserService,
adminStatus: AdminStatusService,
adminLog: AdminLogService,
adminTemplate: AdminTemplateService,
adminCache: AdminCacheService,
user: UserService,
/** 配置服务 */
config: ConfigService,
/** 上传服务 */
upload: UploadService,
/** 数据库管理服务 */
adminUpload: AdminUploadService,
dbManage: DbManageService,
/** 通知推送服务 */
push: PushService,
} as const;
export default services;
export { services };
// ==================== 核心模块导出 ====================
@@ -86,20 +80,26 @@ export type {
// ==================== 业务服务导出 ====================
// 认证服务
export { AuthService } from './auth';
export type { User, OAuthLoginUrlResponse, OAuthCallbackRequest, AuthSource, ExternalAccountBinding, ChangePasswordRequest, UpdateProfileRequest } from './auth';
// 配置服务
export { ConfigService } from './config';
export type { PublicConfigResponse } from './config';
// 管理员服务
export { AdminService } from './admin';
export {
AdminSystemConfigService,
AdminAuthSourceService,
AdminTaskService,
AdminUserService,
AdminStatusService,
AdminLogService,
AdminTemplateService,
AdminCacheService,
} from './admin';
export type {
SystemConfig,
CreateSystemConfigRequest,
CreateUserRequest,
UpdateSystemConfigRequest,
TaskMeta,
TaskParam,
@@ -113,28 +113,33 @@ export type {
ListUsersRequest,
ListUsersResponse,
UpdateUserStatusRequest,
CreateUserRequest,
SystemStatus,
DatabaseInfo,
AppUpdateStatus,
Schedule,
CreateScheduleRequest,
UpdateScheduleRequest,
CacheStatus,
CacheConfig,
Template,
CreateTemplateRequest,
UpdateTemplateRequest,
AuthSource as AdminAuthSource,
AuthSourceRequest,
ToggleAuthSourceRequest,
StorageDriver,
StorageConfig,
ObjectStorageConfig,
} from './admin';
// 用户服务
export { UserService } from './user';
export type { AccessToken, CreateTokenResponse } from './user';
// 上传服务
export { UploadService } from './upload';
export type { UploadImageResponse } from './upload';
export { UploadService, AdminUploadService, formatFileSize, getFileUrl } from './upload';
export type { UploadImageResponse, Upload, ListUploadsResponse, FileStatsResponse, ImageQuality } from './upload';
// 数据库管理服务
export { DbManageService } from './db-manage';
export type { DBOverview, TableDataResponse, ExecuteSQLResponse } from './db-manage';
// 通知推送服务
export { PushService } from './push';
export type { PushEvent, PushHistory, PushChannelConfig, ListPushHistoriesRequest, ListPushHistoriesResponse, UpdatePushEventRequest, TestPushRequest } from './push';
export type { PushEvent, PushHistory, PushChannelConfig, ListPushHistoriesRequest, ListPushHistoriesResponse, UpdatePushEventRequest, TestPushRequest } from './push';
@@ -0,0 +1,40 @@
import type {InternalAxiosRequestConfig} from 'axios';
import {BaseService} from '../core/base.service';
import type {FileStatsResponse, ListUploadsResponse} from './types';
export class AdminUploadService extends BaseService {
protected static readonly basePath = '/api/v1/admin/uploads';
static async listUploads(
page = 1,
pageSize = 20,
keyword?: string,
type?: string,
extension?: string,
): Promise<ListUploadsResponse> {
const params: Record<string, string | number> = { page, page_size: pageSize };
if (keyword) params.keyword = keyword;
if (type) params.type = type;
if (extension) params.extension = extension;
return this.get<ListUploadsResponse>('', params);
}
static async getFileStats(): Promise<FileStatsResponse> {
return this.get<FileStatsResponse>('/stats');
}
static async deleteFile(id: string): Promise<void> {
return this.delete<void>(`/${id}`);
}
static getDownloadUrl(id: string): string {
return `${this.basePath}/download/${id}`;
}
static async batchDownload(ids: string[]): Promise<Blob> {
return this.post<Blob>('/download/batch', { ids }, {
responseType: 'blob',
} as InternalAxiosRequestConfig);
}
}
+4 -1
View File
@@ -1,2 +1,5 @@
export { UploadService } from './upload.service';
export type { UploadImageResponse } from './types';
export { AdminUploadService } from './admin-upload.service';
export { formatFileSize, getFileUrl } from './utils';
export type { ImageQuality } from './utils';
export type { UploadImageResponse, Upload, ListUploadsResponse, FileStatsResponse } from './types';
+36 -147
View File
@@ -1,189 +1,78 @@
import {BaseService} from '../core/base.service'
import type {ListUploadsResponse, Upload, UploadImageResponse, FileStatsResponse} from './types'
import type {InternalAxiosRequestConfig} from 'axios'
import apiClient from '../core/api-client'
import type {InternalAxiosRequestConfig} from 'axios';
export type ImageQuality = 'low' | 'medium' | 'high' | 'origin'
import {BaseService} from '../core/base.service';
import type {ListUploadsResponse, Upload, UploadImageResponse} from './types';
/**
* 根据上传ID构造文件访问URL
* @param id - 上传记录ID
* @param quality - 图片质量
* @returns 文件访问URL
*/
export function getFileUrl(
id: string | number | null | undefined,
quality: ImageQuality = 'origin'
): string | null {
if (!id) return null
if (quality === 'origin') return `/f/${id}`
return `/f/${id}?quality=${quality}`
}
/**
* 格式化文件大小
*/
export function formatFileSize(bytes: number): string {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`
}
/**
* 上传服务
* 处理文件上传相关的 API 请求
*/
export class UploadService extends BaseService {
protected static readonly basePath = '/api/v1/admin/uploads'
protected static readonly basePath = '/api/v1/upload';
/**
* 通用文件上传
* @param file - 文件对象
* @param type - 业务分类(如 avatar、attachment、generic)
* @param metadata - 可选额外 JSON 元数据
*/
static async uploadFile(
file: File,
type: string = 'generic',
metadata?: Record<string, unknown>,
accessMode?: number
accessMode?: number,
): Promise<Upload> {
const formData = new FormData()
formData.append('file', file)
formData.append('type', type)
const formData = new FormData();
formData.append('file', file);
formData.append('type', type);
if (metadata) {
formData.append('metadata', JSON.stringify(metadata))
formData.append('metadata', JSON.stringify(metadata));
}
if (accessMode !== undefined) {
formData.append('access_mode', String(accessMode))
formData.append('access_mode', String(accessMode));
}
const response = await apiClient.post<{ data: Upload }>('/api/v1/upload', formData, {
return this.post<Upload>('', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
} as InternalAxiosRequestConfig)
return response.data.data
} as InternalAxiosRequestConfig);
}
/**
* 获取文件列表
* @param page - 页码(1-based)
* @param pageSize - 每页数量
* @param keyword - 搜索关键词(文件名模糊)
* @param type - 业务分类过滤
* @param extension - 扩展名过滤
*/
static async listUploads(
page = 1,
pageSize = 20,
keyword?: string,
type?: string,
extension?: string
): Promise<ListUploadsResponse> {
const params: Record<string, string | number> = { page, page_size: pageSize }
if (keyword) params.keyword = keyword
if (type) params.type = type
if (extension) params.extension = extension
return this.get<ListUploadsResponse>('', params)
}
/**
* 获取当前用户文件统计信息
*/
static async getFileStats(): Promise<FileStatsResponse> {
return this.get<FileStatsResponse>('/stats')
}
/**
* 删除文件
*/
static async deleteFile(id: string): Promise<void> {
return this.delete<void>(`/${id}`)
}
/**
* 获取单文件下载 URL(触发 attachment 下载)
*/
static getDownloadUrl(id: string): string {
return `/api/v1/admin/uploads/download/${id}`
}
/**
* 批量 ZIP 打包下载
* @param ids - 文件 ID 数组
*/
static async batchDownload(ids: string[]): Promise<Blob> {
const response = await this.post<Blob>('/download/batch', { ids }, {
responseType: 'blob',
} as InternalAxiosRequestConfig)
return response
}
/**
* 将 base64 图片转换为 Blob 并上传(兼容旧接口)
*/
static async uploadBase64Image(
base64: string,
type: string = 'generic',
filename: string = 'image.png',
accessMode?: number
accessMode?: number,
): Promise<UploadImageResponse> {
const response = await fetch(base64)
const blob = await response.blob()
const mimeType = base64.match(/data:([^;]+);/)?.[1] || 'image/png'
const file = new File([blob], filename, { type: mimeType })
const result = await this.uploadFile(file, type, undefined, accessMode)
return { id: result.id }
const response = await fetch(base64);
const blob = await response.blob();
const mimeType = base64.match(/data:([^;]+);/)?.[1] || 'image/png';
const file = new File([blob], filename, { type: mimeType });
const result = await this.uploadFile(file, type, undefined, accessMode);
return { id: result.id };
}
/**
* 获取我的文件列表
*/
static async listMyUploads(
page = 1,
pageSize = 20,
keyword?: string,
type?: string,
extension?: string
extension?: string,
): Promise<ListUploadsResponse> {
const params: Record<string, string | number> = { page, page_size: pageSize }
if (keyword) params.keyword = keyword
if (type) params.type = type
if (extension) params.extension = extension
const response = await apiClient.get<{ data: ListUploadsResponse }>('/api/v1/upload/my', {
params
} as InternalAxiosRequestConfig)
return response.data.data
const params: Record<string, string | number> = { page, page_size: pageSize };
if (keyword) params.keyword = keyword;
if (type) params.type = type;
if (extension) params.extension = extension;
return this.get<ListUploadsResponse>('/my', params);
}
/**
* 删除我的文件
*/
static async deleteMyFile(id: string): Promise<void> {
const response = await apiClient.delete<{ data: void }>(`/api/v1/upload/${id}`)
return response.data.data
return this.delete<void>(`/${id}`);
}
/**
* 更新我的文件
*/
static async updateMyFile(id: string, fileName: string, accessMode?: number): Promise<Upload> {
const response = await apiClient.put<{ data: Upload }>(`/api/v1/upload/${id}`, {
return this.put<Upload>(`/${id}`, {
file_name: fileName,
access_mode: accessMode
})
return response.data.data
access_mode: accessMode,
});
}
/**
* 我的文件批量 ZIP 打包下载
*/
static async batchDownloadMyFiles(ids: string[]): Promise<Blob> {
const response = await apiClient.post<Blob>('/api/v1/upload/download/batch', { ids }, {
return this.post<Blob>('/download/batch', { ids }, {
responseType: 'blob',
} as InternalAxiosRequestConfig)
return response.data
} as InternalAxiosRequestConfig);
}
}
static getDownloadUrl(id: string): string {
return `${this.basePath}/download/${id}`;
}
}
+18
View File
@@ -0,0 +1,18 @@
export type ImageQuality = 'low' | 'medium' | 'high' | 'origin';
export function getFileUrl(
id: string | number | null | undefined,
quality: ImageQuality = 'origin',
): string | null {
if (!id) return null;
if (quality === 'origin') return `/f/${id}`;
return `/f/${id}?quality=${quality}`;
}
export function formatFileSize(bytes: number): string {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;
}