压缩历史至 95081aff

This commit is contained in:
ryan
2026-06-08 20:34:27 +08:00
commit 8a782525de
435 changed files with 71146 additions and 0 deletions
+524
View File
@@ -0,0 +1,524 @@
# 服务层架构文档
> **前端服务层** - 统一的 API 交互层,基于 TypeScript 和面向对象设计
## 架构设计
### 设计理念
服务层采用**分层架构**和**继承模式**,遵循以下核心原则:
1. **单一职责** - 每个服务模块负责一个业务领域
2. **类型安全** - 全量 TypeScript 类型定义,杜绝 `any`
3. **统一规范** - 所有服务遵循相同的设计模式
4. **错误分类** - 细粒度错误类型,便于精确处理
5. **可扩展性** - 基于继承的设计,易于添加新服务
---
## 架构分层
```mermaid
graph TB
subgraph UI["业务组件层"]
Components["React Components<br/>Hooks & Contexts"]
end
subgraph Services["服务层 (Services Layer)"]
direction LR
Auth["AuthService<br/><small>认证服务</small>"]
Admin["AdminService<br/><small>管理员服务</small>"]
Merchant["MerchantService<br/><small>商户服务</small>"]
Transaction["TransactionService<br/><small>交易服务</small>"]
Dispute["DisputeService<br/><small>争议服务</small>"]
User["UserService<br/><small>用户服务</small>"]
Config["ConfigService<br/><small>配置服务</small>"]
end
subgraph BaseLayer["服务基类"]
BaseService["BaseService<br/><small>• get() post() put() delete()<br/>• rawGet() rawPost()</small>"]
end
subgraph Core["核心层 (Core Layer)"]
direction LR
ApiClient["api-client.ts<br/><small>• 请求/响应拦截<br/>• 错误处理<br/>• 请求去重<br/>• 401重定向</small>"]
Errors["errors.ts<br/><small>• ApiErrorBase<br/>• NetworkError<br/>• ValidationError<br/>• 等7种错误</small>"]
Types["types.ts<br/><small>• ApiResponse<br/>• PaginationParams<br/>• RequestConfig</small>"]
CoreConfig["config.ts<br/><small>• baseURL<br/>• timeout<br/>• credentials</small>"]
end
Backend["Backend API<br/><small>(Go + Gin)</small>"]
Components -->|调用| Auth
Components -->|调用| Admin
Components -->|调用| Merchant
Components -->|调用| Transaction
Components -->|调用| Dispute
Components -->|调用| User
Components -->|调用| Config
Auth -.->|继承| BaseService
Admin -.->|继承| BaseService
Merchant -.->|继承| BaseService
Transaction -.->|继承| BaseService
Dispute -.->|继承| BaseService
User -.->|继承| BaseService
Config -.->|继承| BaseService
BaseService -->|使用| ApiClient
BaseService -->|使用| Errors
BaseService -->|使用| Types
ApiClient -->|读取| CoreConfig
ApiClient ==>|HTTP 请求| Backend
style UI fill:#e1f5ff
style Services fill:#fff9e6
style BaseLayer fill:#f0f0f0
style Core fill:#e8f5e9
style Backend fill:#ffebee
```
---
## 目录结构
```
lib/services/
├── core/ # 核心基础设施层
│ ├── api-client.ts # HTTP 客户端(请求拦截、错误处理)
│ ├── base.service.ts # 服务基类(封装 CRUD 方法)
│ ├── config.ts # API 配置(环境变量、超时等)
│ ├── errors.ts # 错误类型定义(7种错误类型)
│ ├── types.ts # 核心类型(ApiResponse、分页等)
│ └── index.ts # 核心模块导出
│
├── auth/ # 认证服务模块
│ ├── auth.service.ts # OAuth 认证、登录、登出
│ ├── types.ts # User、OAuthLoginUrlResponse 等
│ └── index.ts # 模块导出 + 文档
│
├── admin/ # 管理员服务模块
│ ├── admin.service.ts # 系统配置、用户积分配置管理
│ ├── types.ts # SystemConfig、UserPayConfig 等
│ └── index.ts # 模块导出 + 文档
│
├── merchant/ # 商户服务模块
│ ├── merchant.service.ts # API Key、支付链接、订单管理
│ ├── types.ts # MerchantAPIKey、PaymentLink 等
│ └── index.ts # 模块导出 + 文档
│
├── transaction/ # 交易服务模块
│ ├── transaction.service.ts # 交易记录
│ ├── types.ts # Order、TransactionQueryParams 等
│ └── index.ts # 模块导出 + 文档
│
├── dispute/ # 争议服务模块
│ ├── dispute.service.ts # 创建争议、退款审核
│ ├── types.ts # Dispute、DisputeWithOrder 等
│ └── index.ts # 模块导出 + 文档
│
├── user/ # 用户服务模块
│ ├── user.service.ts # 用户设置(支付密钥等)
│ ├── types.ts # UpdatePayKeyRequest 等
│ └── index.ts # 模块导出 + 文档
│
├── config/ # 配置服务模块
│ ├── config.service.ts # 获取公共配置
│ ├── types.ts # PublicConfigResponse
│ └── index.ts # 模块导出 + 文档
│
├── index.ts # 统一导出入口
└── README.md # 本文档
```
---
## 核心模块详解
### 1. BaseService - 服务基类
**职责**:为所有业务服务提供统一的 HTTP 方法封装
**关键设计**:
```typescript
export class BaseService {
protected static readonly basePath: string = '';
// 标准 RESTful 方法
protected static async get<T>(path: string, params?: Record<string, unknown>): Promise<T>
protected static async post<T>(path: string, data?: unknown): Promise<T>
protected static async put<T>(path: string, data?: unknown): Promise<T>
protected static async patch<T>(path: string, data?: unknown): Promise<T>
protected static async delete<T>(path: string, params?: Record<string, unknown>): Promise<T>
// 特殊端点支持(不遵循标准响应格式)
protected static async rawGet<T>(url: string, params?: unknown): Promise<T>
protected static async rawPost<T>(url: string, data?: unknown): Promise<T>
}
```
**设计优势**:
- ✅ 子类只需设置 `basePath`,无需重复实现 HTTP 逻辑
- ✅ 统一的响应解包(`response.data.data`)
- ✅ 类型安全的泛型设计
- ✅ 支持特殊端点(如 `/api.php`)
---
### 2. API Client - HTTP 客户端
**职责**:提供全局唯一的 Axios 实例,处理所有 HTTP 请求
**核心功能**:
#### 请求拦截器
- 自动添加 Cancel Token(支持请求取消)
- 请求去重(避免重复请求)
#### 响应拦截器
- **401 自动重定向** - 未授权时自动跳转登录页
- **错误分类** - 将 HTTP 状态码映射为具体错误类型
- **统一响应格式** - 解析 `ApiResponse<T>` 结构
#### 错误处理映射
| HTTP 状态码 | 错误类型 | 说明 |
|------------|---------|------|
| 400 | `ValidationError` | 参数验证失败 |
| 401 | 自动重定向 | 跳转到登录页 |
| 403 | `ForbiddenError` | 权限不足 |
| 404 | `NotFoundError` | 资源不存在 |
| 5xx | `ServerError` | 服务器错误 |
| 超时 | `TimeoutError` | 请求超时 |
| 网络 | `NetworkError` | 网络连接失败 |
---
### 3. 错误类型层级
```
ApiErrorBase (基类)
├── NetworkError (网络连接错误)
├── TimeoutError (请求超时)
├── UnauthorizedError (401 - 未授权)
├── ForbiddenError (403 - 权限不足)
├── NotFoundError (404 - 资源不存在)
├── ValidationError (400 - 参数验证失败)
└── ServerError (5xx - 服务器错误)
```
**设计优势**:
- 支持 `instanceof` 类型判断
- 携带详细错误信息(`error_code`、`details`)
- 便于前端精确处理不同错误场景
---
## 开发规范
### 1. 服务类规范
#### 必须遵循
```typescript
export class SomeService extends BaseService {
// 1. 必须继承 BaseService
// 2. basePath 必须是 protected static readonly
protected static readonly basePath = '/api/v1/resource';
// 3. 方法必须是 static async
// 4. 返回类型必须明确(禁止 any)
static async getAll(): Promise<Resource[]> {
return this.get<Resource[]>('/');
}
// 5. 参数类型必须明确定义在 types.ts
static async create(request: CreateResourceRequest): Promise<Resource> {
return this.post<Resource>('/', request);
}
}
```
#### 方法命名规范
| 操作 | 命名 | 示例 |
|------|------|------|
| 获取列表 | `list*` 或 `getAll` | `listAPIKeys()` |
| 获取单个 | `get*` 或 `getById` | `getAPIKey(id)` |
| 创建 | `create*` | `createAPIKey(request)` |
| 更新 | `update*` | `updateAPIKey(id, request)` |
| 删除 | `delete*` | `deleteAPIKey(id)` |
| 特殊操作 | 动词开头 | `payMerchantOrder()` |
#### 方法排序规范
```typescript
export class SomeService extends BaseService {
protected static readonly basePath = '/api/v1/resource';
// 1. CRUD 操作(按 Create → Read → Update → Delete)
static async create() { }
static async list() { }
static async get() { }
static async update() { }
static async delete() { }
// 2. 其他业务方法(按业务逻辑分组)
static async someAction() { }
}
```
---
### 2. 类型定义规范
#### types.ts 文件结构
```typescript
// 1. 类型别名(Type Aliases)
export type ResourceStatus = 'active' | 'inactive' | 'pending';
// 2. 枚举(Enums) - 使用 const enum 提升性能
export const enum ResourceLevel {
Basic = 1,
Premium = 2,
Enterprise = 3,
}
// 3. 接口(Interfaces) - 按业务逻辑分组
/**
* 资源信息
*/
export interface Resource {
/** 资源 ID */
id: number;
/** 资源名称 */
name: string;
/** 状态 */
status: ResourceStatus;
/** 创建时间 */
created_at: string;
}
/**
* 创建资源请求
*/
export interface CreateResourceRequest {
/** 资源名称(最大 50 字符) */
name: string;
/** 描述(可选,最大 200 字符) */
description?: string;
}
```
#### 注释规范
- ✅ 所有 interface 必须有 JSDoc 描述
- ✅ 所有字段必须有行内注释
- ✅ 包含约束信息(长度、范围、格式等)
- ✅ 可选字段使用 `?` 标记
---
### 3. JSDoc 注释规范
**标准模板**:
```typescript
/**
* [一句话描述方法功能]
*
* @param paramName - 参数说明
* @returns 返回值说明
* @throws {ErrorType} 错误条件说明
*
* @example
* ```typescript
* // 使用示例
* const result = await Service.method({ param: 'value' });
* ```
*
* @remarks [可选]
* - 业务规则或注意事项
*/
```
**示例**:
```typescript
/**
* 创建商户 API Key
*
* @param request - API Key 配置
* @returns 创建的 API Key 信息
* @throws {UnauthorizedError} 当未登录时
* @throws {ValidationError} 当参数验证失败时
*
* @example
* ```typescript
* const apiKey = await MerchantService.createAPIKey({
* app_name: '我的应用',
* app_homepage_url: 'https://example.com'
* });
* ```
*
* @remarks
* - app_name 最大 20 字符
* - 需要登录权限
*/
static async createAPIKey(request: CreateAPIKeyRequest): Promise<MerchantAPIKey>
```
---
### 4. 模块 index.ts 规范
**标准格式**:
```typescript
/**
* [模块名] 服务模块
*
* @description
* 提供 [业务领域] 相关的功能,包括:
* - 功能点1
* - 功能点2
* - 功能点3
*
* @example
* ```typescript
* import { ServiceName } from '@/lib/services';
*
* // 使用示例(展示最常用的1-2个方法)
* const result = await ServiceName.commonMethod();
* ```
*
* @remarks [可选]
* - 特殊说明(如权限要求等)
*/
export { ServiceName } from './service-name.service';
export type * from './types';
// 或明确导出
export type {
Type1,
Type2,
} from './types';
```
---
## 创建新服务指南
### Step 1: 创建目录和文件
```bash
mkdir lib/services/resource
touch lib/services/resource/types.ts
touch lib/services/resource/resource.service.ts
touch lib/services/resource/index.ts
```
### Step 2: 定义类型(types.ts)
```typescript
export interface Resource {
id: number;
name: string;
}
export interface CreateResourceRequest {
name: string;
}
```
### Step 3: 实现服务(resource.service.ts)
```typescript
import { BaseService } from '../core/base.service';
import type { Resource, CreateResourceRequest } from './types';
export class ResourceService extends BaseService {
protected static readonly basePath = '/api/v1/resources';
static async list(): Promise<Resource[]> {
return this.get<Resource[]>('/');
}
static async create(request: CreateResourceRequest): Promise<Resource> {
return this.post<Resource>('/', request);
}
}
```
### Step 4: 导出模块(index.ts)
```typescript
/**
* 资源服务模块
*
* @description
* 提供资源管理相关的功能,包括:
* - 资源列表查询
* - 资源创建
*/
export { ResourceService } from './resource.service';
export type * from './types';
```
### Step 5: 注册到统一入口(services/index.ts)
```typescript
import { ResourceService } from './resource';
const services = {
// ... existing services
resource: ResourceService, // 新增
};
export default services;
export { ResourceService } from './resource';
export type * from './resource';
```
---
## 使用示例
```typescript
import services from '@/lib/services';
// 调用服务
const user = await services.auth.getUserInfo();
const transactions = await services.transaction.getTransactions({
page: 1,
page_size: 20
});
// 错误处理
import { UnauthorizedError, ValidationError } from '@/lib/services';
try {
await services.merchant.createAPIKey(request);
} catch (error) {
if (error instanceof UnauthorizedError) {
router.push('/login');
} else if (error instanceof ValidationError) {
toast.error(error.message);
}
}
```
---
## 注意事项
### 禁止事项
- ❌ 使用 `any` 类型
- ❌ 直接使用 `apiClient`(除非在 `BaseService` 内部)
- ❌ 绕过 BaseService 实现 HTTP 请求
- ❌ 在业务组件中直接导入 axios
### 必须遵循
- ✅ 所有服务继承 `BaseService`
- ✅ 使用 `protected static readonly basePath`
- ✅ 方法必须有完整的 JSDoc 注释
- ✅ 类型定义必须在 `types.ts` 中
- ✅ 通过 `services` 对象调用服务
---
## 相关文档
- [TypeScript 官方文档](https://www.typescriptlang.org/)
- [Axios 文档](https://axios-http.com/)
- [JSDoc 规范](https://jsdoc.app/)
@@ -0,0 +1,372 @@
import { BaseService } from '../core/base.service';
import type {
SystemConfig,
CreateSystemConfigRequest,
UpdateSystemConfigRequest,
UserPayConfig,
CreateUserPayConfigRequest,
UpdateUserPayConfigRequest,
TaskMeta,
TaskTypeResponse,
DispatchTaskRequest,
ListUsersRequest,
ListUsersResponse,
UpdateUserStatusRequest,
} from './types';
export type { AdminUser } from './types';
/**
* 管理员服务
* 处理系统配置和用户积分配置管理相关的 API 请求
*
* @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(): Promise<SystemConfig[]> {
return this.get<SystemConfig[]>('/system-configs');
}
/**
* 获取单个系统配置
* @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);
}
/**
* 删除系统配置
* @param key - 配置键
* @returns void
* @throws {UnauthorizedError} 当未登录时
* @throws {ForbiddenError} 当无管理员权限时
* @throws {NotFoundError} 当配置不存在时
*
* @example
* ```typescript
* await AdminService.deleteSystemConfig('app.version');
* ```
*/
static async deleteSystemConfig(key: string): Promise<void> {
return this.delete<void>(`/system-configs/${ key }`);
}
// ==================== 用户积分配置管理 ====================
/**
* 创建用户积分配置
* @param request - 创建用户积分配置的请求参数
* @returns 创建的用户积分配置
* @throws {UnauthorizedError} 当未登录时
* @throws {ForbiddenError} 当无管理员权限时
* @throws {ValidationError} 当参数验证失败或等级已存在时
*
* @example
* ```typescript
* const config = await AdminService.createUserPayConfig({
* level: PayLevel.Premium,
* min_score: 1000,
* max_score: null,
* daily_limit: 100000,
* fee_rate: 0.01
* });
* console.log('配置ID:', config.id);
* ```
*
* @remarks
* - min_score 必须 >= 0
* - max_score 必须大于 min_score(如果提供)
* - fee_rate 必须在 0-1 之间,最多2位小数
*/
static async createUserPayConfig(
request: CreateUserPayConfigRequest,
): Promise<UserPayConfig> {
return this.post<UserPayConfig>('/user-pay-configs', request);
}
/**
* 获取用户积分配置列表
* @returns 用户积分配置列表(按最低分数升序排序)
* @throws {UnauthorizedError} 当未登录时
* @throws {ForbiddenError} 当无管理员权限时
*
* @example
* ```typescript
* const configs = await AdminService.listUserPayConfigs();
* console.log('积分配置数量:', configs.length);
* ```
*/
static async listUserPayConfigs(): Promise<UserPayConfig[]> {
return this.get<UserPayConfig[]>('/user-pay-configs');
}
/**
* 获取单个用户积分配置
* @param id - 配置ID
* @returns 用户积分配置信息
* @throws {UnauthorizedError} 当未登录时
* @throws {ForbiddenError} 当无管理员权限时
* @throws {NotFoundError} 当配置不存在时
*
* @example
* ```typescript
* const config = await AdminService.getUserPayConfig(123);
* console.log('手续费率:', config.fee_rate);
* ```
*/
static async getUserPayConfig(id: string): Promise<UserPayConfig> {
return this.get<UserPayConfig>(`/user-pay-configs/${ id }`);
}
/**
* 更新用户积分配置
* @param id - 配置ID
* @param request - 更新用户积分配置的请求参数
* @returns void
* @throws {UnauthorizedError} 当未登录时
* @throws {ForbiddenError} 当无管理员权限时
* @throws {NotFoundError} 当配置不存在时
* @throws {ValidationError} 当参数验证失败时
*
* @example
* ```typescript
* await AdminService.updateUserPayConfig(123, {
* min_score: 500,
* max_score: 999,
* daily_limit: 50000,
* fee_rate: 0.02
* });
* ```
*
* @remarks
* - min_score 必须 >= 0
* - max_score 必须大于 min_score(如果提供)
* - fee_rate 必须在 0-1 之间,最多2位小数
*/
static async updateUserPayConfig(
id: string,
request: UpdateUserPayConfigRequest,
): Promise<void> {
return this.put<void>(`/user-pay-configs/${ id }`, request);
}
/**
* 删除用户积分配置
* @param id - 配置ID
* @returns void
* @throws {UnauthorizedError} 当未登录时
* @throws {ForbiddenError} 当无管理员权限时
* @throws {NotFoundError} 当配置不存在时
*
* @example
* ```typescript
* await AdminService.deleteUserPayConfig(123);
* ```
*/
static async deleteUserPayConfig(id: string): Promise<void> {
return this.delete<void>(`/user-pay-configs/${ 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 || '',
}));
}
/**
* 下发任务
* @param request - 下发任务请求参数
* @returns void
* @throws {UnauthorizedError} 当未登录时
* @throws {ForbiddenError} 当无管理员权限时
* @throws {ValidationError} 当参数验证失败时
*
* @example
* ```typescript
* // 下发订单同步任务(带时间范围)
* await AdminService.dispatchTask({
* task_type: 'order_sync',
* start_time: '2025-12-01T00:00:00Z',
* end_time: '2025-12-27T23:59:59Z'
* });
*
* // 下发用户积分更新任务
* await AdminService.dispatchTask({
* task_type: 'user_gamification',
* user_id: 123
* });
*
* // 下发争议自动退款任务
* await AdminService.dispatchTask({
* task_type: 'dispute_auto_refund'
* });
* ```
*
* @remarks
* - 不同任务类型需要不同的参数
* - order_sync 支持 start_time 和 end_time 参数
* - user_gamification 需要 user_id 参数
* - 其他任务无需额外参数
*/
static async dispatchTask(request: DispatchTaskRequest): Promise<void> {
return this.post<void>('/tasks/dispatch', request);
}
// ==================== 用户管理 ====================
/**
* 获取用户列表
* @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
* @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);
}
}
+45
View File
@@ -0,0 +1,45 @@
/**
* 管理员服务模块
*
* @description
* 提供系统配置和用户积分配置管理功能,包括:
* - 系统配置管理(创建、查询、更新、删除)
* - 用户积分配置管理(创建、查询、更新、删除)
*
* @remarks
* 所有接口都需要管理员权限
*
* @example
* ```typescript
* import { AdminService } from '@/lib/services';
*
* // 获取系统配置列表
* const configs = await AdminService.listSystemConfigs();
*
* // 创建用户积分配置
* await AdminService.createUserPayConfig({
* level: 1,
* min_score: 0,
* max_score: 999,
* daily_limit: 10000,
* fee_rate: 0.01
* });
* ```
*/
export { AdminService } from './admin.service';
export type {
SystemConfig,
CreateSystemConfigRequest,
UpdateSystemConfigRequest,
UserPayConfig,
CreateUserPayConfigRequest,
UpdateUserPayConfigRequest,
TaskMeta,
DispatchTaskRequest,
AdminUser,
ListUsersRequest,
ListUsersResponse,
UpdateUserStatusRequest,
} from './types';
+231
View File
@@ -0,0 +1,231 @@
import type { PayLevel } from '@/lib/services/auth/types';
/**
* 系统配置信息
*/
export interface SystemConfig {
/** 配置键 */
key: string;
/** 配置值 */
value: string;
/** 配置描述 */
description: string;
/** 创建时间 */
created_at: string;
/** 更新时间 */
updated_at: string;
}
/**
* 创建系统配置请求参数
*/
export interface CreateSystemConfigRequest {
/** 配置键(最大64字符) */
key: string;
/** 配置值(最大255字符) */
value: string;
/** 配置描述(最大255字符,可选) */
description?: string;
}
/**
* 更新系统配置请求参数
*/
export interface UpdateSystemConfigRequest {
/** 配置值(最大255字符) */
value: string;
/** 配置描述(最大255字符,可选) */
description?: string;
}
/**
* 用户积分配置信息
*/
export interface UserPayConfig {
/** 配置ID */
id: string;
/** 积分等级 */
level: PayLevel;
/** 最低分数 */
min_score: number;
/** 最高分数(可选) */
max_score: number | null;
/** 每日限额(可选) */
daily_limit: number | null;
/** 手续费率(0-1之间的小数,最多2位小数) */
fee_rate: number | string;
/** 积分费率(0-1之间的小数,最多2位小数) */
score_rate: number | string;
/** 分发费率(0-1之间的小数,最多2位小数) */
distribute_rate: number | string;
/** 创建时间 */
created_at: string;
/** 更新时间 */
updated_at: string;
}
/**
* 创建用户积分配置请求参数
*/
export interface CreateUserPayConfigRequest {
/** 积分等级 */
level: PayLevel;
/** 最低分数(必须 >= 0) */
min_score: number;
/** 最高分数(可选,必须大于 min_score) */
max_score?: number | null;
/** 每日限额(可选) */
daily_limit?: number | null;
/** 手续费率(0-1之间的小数,最多2位小数) */
fee_rate: number | string;
/** 积分费率(0-1之间的小数,最多2位小数) */
score_rate: number | string;
/** 分发费率(0-1之间的小数,最多2位小数) */
distribute_rate: number | string;
}
/**
* 更新用户积分配置请求参数
*/
export interface UpdateUserPayConfigRequest {
/** 最低分数(必须 >= 0) */
min_score: number;
/** 最高分数(可选,必须大于 min_score) */
max_score?: number | null;
/** 每日限额(可选) */
daily_limit?: number | null;
/** 手续费率(0-1之间的小数,最多2位小数) */
fee_rate: number | string;
/** 积分费率(0-1之间的小数,最多2位小数) */
score_rate: number | string;
/** 分发费率(0-1之间的小数,最多2位小数) */
distribute_rate: number | string;
}
// ==================== 任务管理 ====================
/**
* 任务类型响应
*/
export interface TaskTypeResponse {
Type?: string;
type?: string;
AsynqTask?: string;
asynq_task?: string;
Name?: string;
name?: string;
Description?: string;
description?: string;
SupportsTime?: boolean;
supports_time?: boolean;
MaxRetry?: number;
max_retry?: number;
Queue?: string;
queue?: string;
}
/**
* 任务元数据
*/
export interface TaskMeta {
/** 任务类型标识 */
type: string;
/** Asynq 任务名称 */
asynq_task: string;
/** 任务名称 */
name: string;
/** 任务描述 */
description: string;
/** 是否支持时间范围参数 */
supports_time: boolean;
/** 最大重试次数 */
max_retry: number;
/** 队列名称 */
queue: string;
}
/**
* 下发任务请求参数
*/
export interface DispatchTaskRequest {
/** 任务类型 */
task_type: string;
/** 开始时间(可选,仅部分任务支持) */
start_time?: string;
/** 结束时间(可选,仅部分任务支持) */
end_time?: string;
/** 用户 ID(可选,仅部分任务需要) */
user_id?: string;
}
// ==================== 用户管理 ====================
/**
* 管理员用户信息
*/
export interface AdminUser {
/** 用户 ID */
id: string;
/** 用户名 */
username: string;
/** 昵称 */
nickname: string;
/** 头像 URL */
avatar_url: string;
/** 信任等级 */
trust_level: number;
/** 支付积分 */
pay_score: number;
/** 累计收入 */
total_receive: string;
/** 累计支出 */
total_payment: string;
/** 累计社区积分 */
total_community: string;
/** 社区余额 */
community_balance: string;
/** 可用余额 */
available_balance: string;
/** 是否激活 */
is_active: boolean;
/** 是否管理员 */
is_admin: boolean;
/** 最后登录时间 */
last_login_at: string;
/** 创建时间 */
created_at: string;
/** 更新时间 */
updated_at: string;
}
/**
* 用户列表查询请求参数
*/
export interface ListUsersRequest {
/** 页码(从 1 开始) */
page: number;
/** 每页数量(1-100) */
page_size: number;
/** 用户 ID 精确过滤(可选) */
user_id?: string;
/** 用户名前缀过滤(可选) */
username?: string;
}
/**
* 用户列表响应
*/
export interface ListUsersResponse {
/** 用户列表 */
users: AdminUser[];
/** 总数 */
total: number;
}
/**
* 更新用户状态请求参数
*/
export interface UpdateUserStatusRequest {
/** 是否激活 */
is_active: boolean;
}
+117
View File
@@ -0,0 +1,117 @@
import { BaseService } from '../core/base.service';
import type {
OAuthLoginUrlResponse,
OAuthCallbackRequest,
User,
} from './types';
/**
* 认证服务
* 处理 OAuth 认证、用户信息获取、登出等
*/
export class AuthService extends BaseService {
protected static readonly basePath = '/api/v1/oauth';
/**
* 获取 OAuth 登录 URL
* @returns OAuth 授权 URL
* @throws {ApiErrorBase} 当获取失败时
*
* @example
* ```typescript
* const url = await AuthService.getLoginUrl();
* window.location.href = url; // 重定向到 OAuth 授权页面
* ```
*/
static async getLoginUrl(): Promise<OAuthLoginUrlResponse> {
return this.get<OAuthLoginUrlResponse>('/login');
}
/**
* 处理 OAuth 回调
* @param request - OAuth 回调参数(state 和 code)
* @throws {ApiErrorBase} 当回调处理失败时
* @throws {ValidationError} 当 state 无效时
*
* @example
* ```typescript
* // 在回调页面获取 URL 参数
* const params = new URLSearchParams(window.location.search);
* const state = params.get('state');
* const code = params.get('code');
*
* if (state && code) {
* await AuthService.handleCallback({ state, code });
* // 登录成功,跳转到首页
* router.push('/home');
* }
* ```
*/
static async handleCallback(request: OAuthCallbackRequest): Promise<void> {
return this.post<void>('/callback', request);
}
/**
* 获取当前登录用户信息
* @returns 用户信息
* @throws {UnauthorizedError} 当未登录时
*
* @example
* ```typescript
* try {
* const user = await AuthService.getUserInfo();
* console.log('当前用户:', user.username);
* } catch (error) {
* if (error instanceof UnauthorizedError) {
* // 跳转到登录页
* router.push('/login');
* }
* }
* ```
*/
static async getUserInfo(): Promise<User> {
return this.get<User>('/user-info');
}
/**
* 用户登出
* @throws {ApiErrorBase} 当登出失败时
*
* @example
* ```typescript
* await AuthService.logout();
* // 清除本地状态
* router.push('/login');
* ```
*/
static async logout(): Promise<void> {
await this.get<void>('/logout');
}
/**
* 发起登录流程
* 直接获取登录 URL 并重定向
*
* @example
* ```typescript
* // 在登录按钮点击时调用
* await AuthService.initiateLogin();
* ```
*/
static async initiateLogin(): Promise<void> {
if (typeof window !== 'undefined') {
const params = new URLSearchParams(window.location.search);
const callbackUrl = params.get('callbackUrl');
if (callbackUrl) {
sessionStorage.setItem('redirect_after_login', callbackUrl);
}
}
const url = await this.getLoginUrl();
if (typeof window !== 'undefined' && url) {
window.location.href = url;
}
}
}
+35
View File
@@ -0,0 +1,35 @@
/**
* 认证服务模块
*
* @description
* 提供 OAuth 认证相关的功能,包括:
* - 获取登录 URL
* - 处理 OAuth 回调
* - 获取用户信息
* - 用户登出
* - 发起登录流程
*
* @example
* ```typescript
* import { AuthService } from '@/lib/services';
*
* // 发起登录
* await AuthService.initiateLogin();
*
* // 获取用户信息
* const user = await AuthService.getUserInfo();
* console.log('当前用户:', user.username);
*
* // 登出
* await AuthService.logout();
* ```
*/
export { AuthService } from './auth.service';
export { TrustLevel, PayLevel } from './types';
export type {
User,
OAuthLoginUrlResponse,
OAuthCallbackRequest,
} from './types';
+85
View File
@@ -0,0 +1,85 @@
/**
* 信任等级
*/
export enum TrustLevel {
/** 新用户 */
New = 0,
/** 基础用户 */
Basic = 1,
/** 成员 */
Member = 2,
/** 常规用户 */
Regular = 3,
/** 领导者 */
Leader = 4,
}
/**
* 支付等级
*/
export enum PayLevel {
/** 普通 */
Ordinary = 0,
/** 黄金 */
Gold = 1,
/** 白金 */
WhiteGold = 2,
/** 黑金 */
BlackGold = 3,
}
/**
* 用户基本信息
*/
export interface User {
/** 用户 ID */
id: string;
/** 账户 */
username: string;
/** 昵称 */
nickname: string;
/** 信任等级 */
trust_level: TrustLevel;
/** 头像 URL */
avatar_url: string;
/** 总接收金额 */
total_receive: string;
/** 总支付金额 */
total_payment: string;
/** 总社区金额 */
total_community: string;
/** 社区余额 */
community_balance: string;
/** 可用余额 */
available_balance: string;
/** 在途资金(延迟到账中) */
pending_balance: string;
/** 支付分数 */
pay_score: number;
/** 是否有支付密钥 */
is_pay_key: boolean;
/** 是否为管理员 */
is_admin: boolean;
/** 当日剩余配额 */
remain_quota: string;
/** 支付等级 */
pay_level: PayLevel;
/** 每日限额 */
daily_limit: number | null;
}
/**
* OAuth 登录 URL 响应
* 后端直接返回字符串 URL
*/
export type OAuthLoginUrlResponse = string;
/**
* OAuth 回调请求参数
*/
export interface OAuthCallbackRequest {
/** 状态码 */
state: string;
/** 授权码 */
code: string;
}
@@ -0,0 +1,44 @@
import { BaseService } from '../core/base.service';
import type { PublicConfigResponse } from './types';
import type { UserPayConfig } from '../admin/types';
/**
* 配置服务
* 处理系统公共配置相关的 API 请求
*/
export class ConfigService extends BaseService {
protected static readonly basePath = '/api/v1/config';
/**
* 获取公共配置
* @returns 公共配置信息
*
* @example
* ```typescript
* const config = await ConfigService.getPublicConfig();
* console.log('争议时间窗口:', config.dispute_time_window_hours, '小时');
* ```
*/
static async getPublicConfig(): Promise<PublicConfigResponse> {
return this.get<PublicConfigResponse>('/public');
}
/**
* 获取用户平台等级配置(公开接口)
* 返回所有平台等级的配置信息,用于向用户展示不同等级的权益
*
* @returns 用户支付配置列表
*
* @example
* ```typescript
* const payConfigs = await ConfigService.getUserPayConfigs();
* payConfigs.forEach(config => {
* console.log(`等级 ${config.level}: 手续费率 ${config.fee_rate}, 每日限额 ${config.daily_limit}`);
* });
* ```
*/
static async getUserPayConfigs(): Promise<UserPayConfig[]> {
return this.get<UserPayConfig[]>('/user-pay');
}
}
+24
View File
@@ -0,0 +1,24 @@
/**
* 配置服务模块
*
* @description
* 提供系统公共配置相关的功能,包括:
* - 获取公共配置(争议时间窗口等)
* - 获取用户平台等级配置(公开接口)
*
* @example
* ```typescript
* import { ConfigService } from '@/lib/services';
*
* // 获取公共配置
* const config = await ConfigService.getPublicConfig();
* console.log('争议时间窗口:', config.dispute_time_window_hours, '小时');
*
* // 获取用户平台等级配置
* const payConfigs = await ConfigService.getUserPayConfigs();
* console.log('平台等级列表:', payConfigs);
* ```
*/
export { ConfigService } from './config.service';
export type * from './types';
+21
View File
@@ -0,0 +1,21 @@
/**
* 公共配置响应
*/
export interface PublicConfigResponse {
/** 争议时间窗口(小时) */
dispute_time_window_hours: number;
/** 红包功能是否启用 */
red_envelope_enabled: boolean;
/** 单个红包的最大积分上限 */
red_envelope_max_amount: string;
/** 每日发红包的个数限制 */
red_envelope_daily_limit: number;
/** 红包手续费率(0-1之间的小数) */
red_envelope_fee_rate: string;
/** 每个红包的最大可领取人数上限 */
red_envelope_max_recipients: number;
/** 到账最短时间 **/
settlement_delay_days_min: number;
/** 到账最长时间 **/
settlement_delay_days_max: number;
}
+413
View File
@@ -0,0 +1,413 @@
import axios, { AxiosError, AxiosResponse, CancelTokenSource, InternalAxiosRequestConfig } from 'axios';
import { toast } from 'sonner';
import { showRiskWarningToast } from '@/components/common/risk/risk-warning-toast';
import { apiConfig } from './config';
import {
ApiErrorBase,
NetworkError,
TimeoutError,
ForbiddenError,
NotFoundError,
ServerError,
ValidationError,
} from './errors';
import { ApiError, ApiResponse } from './types';
/**
* API 客户端实例
* 统一处理请求配置、响应解析和错误处理
*/
const apiClient = axios.create({
baseURL: apiConfig.baseURL,
timeout: apiConfig.timeout,
withCredentials: apiConfig.withCredentials,
headers: {
'Content-Type': 'application/json',
},
});
/**
* 请求取消令牌存储
*/
const cancelTokens = new Map<string, CancelTokenSource>();
/**
* 请求缓存存储
* 存储正在进行的请求 Promise,避免重复请求
*/
const pendingRequests = new Map<string, Promise<AxiosResponse<ApiResponse>>>();
const RISK_LEVEL_HEADER = 'x-credit-risk-level';
const RISK_LABELS_HEADER = 'x-credit-risk-labels';
const RISK_ITEMS_HEADER = 'x-credit-risks';
const RISK_BLOCKED_CODE = 'RISK_BLOCKED';
const RISK_BLOCKED_EVENT = 'credit-risk-blocked';
interface RiskItem {
label: string;
value?: string;
desc?: string;
}
interface RiskInfo {
risk_level: string;
risk_labels: string[];
risks: RiskItem[];
}
function decodeBase64JSON(value?: string): unknown {
if (!value || typeof window === 'undefined') return [];
try {
const binary = window.atob(value);
const bytes = Uint8Array.from(binary, char => char.charCodeAt(0));
const json = new TextDecoder().decode(bytes);
return JSON.parse(json);
} catch {
return [];
}
}
function normalizeRiskItems(value: unknown): RiskItem[] {
if (!Array.isArray(value)) return [];
return value.reduce<RiskItem[]>((items, item) => {
if (!item || typeof item !== 'object') return items;
const label = 'label' in item ? (item as { label?: unknown }).label : undefined;
if (typeof label !== 'string' || !label.trim()) return items;
const value = 'value' in item ? (item as { value?: unknown }).value : undefined;
const desc = 'desc' in item ? (item as { desc?: unknown }).desc : undefined;
items.push({
label: label.trim(),
value: typeof value === 'string' ? value.trim() : undefined,
desc: typeof desc === 'string' ? desc.trim() : undefined,
});
return items;
}, []);
}
function normalizeRiskLabels(value: unknown): string[] {
return Array.isArray(value) ? value.filter((label): label is string => typeof label === 'string' && !!label.trim()).map(label => label.trim()) : [];
}
function riskLabelsFromItems(items: RiskItem[]): string[] {
return items.map(item => item.label).filter(Boolean);
}
function riskInfoFromHeaders(headers: AxiosResponse['headers']): RiskInfo | null {
const riskLevel = headers[RISK_LEVEL_HEADER];
if (typeof riskLevel !== 'string' || !riskLevel) return null;
const riskLabelsHeader = headers[RISK_LABELS_HEADER];
const riskItemsHeader = headers[RISK_ITEMS_HEADER];
const risks = typeof riskItemsHeader === 'string' ? normalizeRiskItems(decodeBase64JSON(riskItemsHeader)) : [];
const labels = typeof riskLabelsHeader === 'string' ? normalizeRiskLabels(decodeBase64JSON(riskLabelsHeader)) : riskLabelsFromItems(risks);
return {
risk_level: riskLevel,
risk_labels: labels,
risks,
};
}
function riskInfoFromDetails(details: unknown): RiskInfo | null {
if (!details || typeof details !== 'object') return null;
const riskLevel = 'risk_level' in details ? (details as { risk_level?: unknown }).risk_level : undefined;
const riskLabels = 'risk_labels' in details ? (details as { risk_labels?: unknown }).risk_labels : undefined;
const riskItems = 'risks' in details ? (details as { risks?: unknown }).risks : undefined;
if (typeof riskLevel !== 'string' || !riskLevel) return null;
const risks = normalizeRiskItems(riskItems);
const labels = normalizeRiskLabels(riskLabels);
return {
risk_level: riskLevel,
risk_labels: labels.length ? labels : riskLabelsFromItems(risks),
risks,
};
}
function showRiskWarning(riskInfo: RiskInfo): void {
showRiskWarningToast(riskInfo);
}
function showRiskBlockedDialog(riskInfo: RiskInfo): void {
if (typeof window === 'undefined') return;
window.dispatchEvent(new CustomEvent<RiskInfo>(RISK_BLOCKED_EVENT, { detail: riskInfo }));
}
/**
* 生成请求的唯一键
* 包含方法、URL 和请求数据的哈希,确保不同参数的请求不会被误取消
*/
function getRequestKey(config: { method?: string; url?: string; data?: unknown }): string {
const baseKey = `${ config.method?.toUpperCase() }_${ config.url }`;
/* 序列化加入键中 */
if (config.data) {
try {
const dataHash = JSON.stringify(config.data);
return `${ baseKey }_${ dataHash }`;
} catch {
// 失败使用基础键
return baseKey;
}
}
return baseKey;
}
/**
* 请求拦截器
* 添加取消令牌和其他配置
*/
apiClient.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const requestKey = getRequestKey(config);
const source = axios.CancelToken.source();
config.cancelToken = source.token;
cancelTokens.set(requestKey, source);
return config;
},
(error: unknown) => Promise.reject(error),
);
/**
* 直接启动登录流程
* @param currentPath - 当前路径,用于登录成功后重定向回来
*/
function initiateLogin(currentPath: string): Promise<never> {
if (!currentPath.startsWith('/login') && !currentPath.startsWith('/callback')) {
if (typeof window !== 'undefined') {
sessionStorage.setItem('redirect_after_login', currentPath);
const loginUrl = new URL('/login', window.location.origin);
loginUrl.searchParams.set('callbackUrl', currentPath);
window.location.href = loginUrl.toString();
}
}
return new Promise<never>(() => { });
}
/**
* 响应拦截器
* 处理 API 响应和统一错误处理
*/
apiClient.interceptors.response.use(
(response: AxiosResponse<ApiResponse>) => {
const requestKey = getRequestKey(response.config);
cancelTokens.delete(requestKey);
pendingRequests.delete(requestKey);
const riskInfo = riskInfoFromHeaders(response.headers);
if (riskInfo) {
showRiskWarning(riskInfo);
}
return response;
},
(error: AxiosError<ApiError>) => {
if (error.config) {
const requestKey = getRequestKey(error.config);
cancelTokens.delete(requestKey);
pendingRequests.delete(requestKey);
}
/* 请求被取消时静默处理 */
if (axios.isCancel(error)) {
const cancelError = new Error(error.message || '请求已被取消') as Error & { __CANCEL__?: boolean };
cancelError.__CANCEL__ = true;
return Promise.reject(cancelError);
}
/* 401 未授权错误 */
if (error.response?.status === 401) {
return initiateLogin(window.location.pathname + window.location.search);
}
/* 403 权限不足错误 */
if (error.response?.status === 403) {
if (error.response.data?.error_code === RISK_BLOCKED_CODE) {
const riskInfo = riskInfoFromDetails(error.response.data.details) || riskInfoFromHeaders(error.response.headers);
if (riskInfo) {
showRiskBlockedDialog(riskInfo);
}
return Promise.reject(
new ForbiddenError(error.response.data?.error_msg || '账号存在风险', RISK_BLOCKED_CODE, error.response.data?.details),
);
}
return Promise.reject(
new ForbiddenError(error.response.data?.error_msg || '权限不足,请过盾后重试', error.response.data?.error_code, error.response.data?.details),
);
}
/* 404 资源未找到错误 */
if (error.response?.status === 404) {
return Promise.reject(
new NotFoundError(error.response.data?.error_msg || '请求的资源不存在'),
);
}
/* 400 验证错误 */
if (error.response?.status === 400) {
return Promise.reject(
new ValidationError(
error.response.data?.error_msg || '请求参数验证失败',
error.response.data?.details,
),
);
}
/* 429 速率限制错误 */
if (error.response?.status === 429) {
const retryAfter = error.response.headers?.['retry-after'];
const message = error.response.data?.error_msg ||
`请求过于频繁,请 ${ retryAfter || '稍后' } 秒后重试`;
toast.error('请求频率限制', {
description: message,
id: 'rate-limit-error',
});
return Promise.reject(
new ApiErrorBase(message, 'RATE_LIMITED', 429),
);
}
/* 5xx 服务器错误 */
if (error.response && error.response.status >= 500) {
return Promise.reject(
new ServerError(
error.response.data?.error_msg || '服务器内部错误,请稍后重试',
error.response.status,
),
);
}
/* 网络超时错误 */
if (error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT') {
return Promise.reject(new TimeoutError());
}
/* 网络连接错误(ECONNREFUSED, ERR_NETWORK 等) */
if (
!error.response ||
error.code === 'ECONNREFUSED' ||
error.code === 'ERR_NETWORK' ||
error.message?.includes('Network Error') ||
error.message?.includes('Failed to fetch')
) {
return Promise.reject(
new NetworkError('无法连接到服务器,请确认后端服务已启动'),
);
}
/* 其他后端返回的错误 */
if (error.response?.data?.error_msg) {
return Promise.reject(
new ApiErrorBase(
error.response.data.error_msg,
error.response.data.error_code,
error.response.status,
error.response.data.details,
),
);
}
/* 兜底错误 */
return Promise.reject(
new ApiErrorBase(error.message || '网络请求失败'),
);
},
);
/**
* 取消指定请求
* @param method - 请求方法
* @param url - 请求 URL
*/
export function cancelRequest(method: string, url: string): void {
const requestKey = `${ method.toUpperCase() }_${ url }`;
const source = cancelTokens.get(requestKey);
if (source) {
source.cancel('请求已被手动取消');
cancelTokens.delete(requestKey);
}
}
/**
* 取消所有请求
*/
export function cancelAllRequests(): void {
cancelTokens.forEach((source) => {
source.cancel('所有请求已被取消');
});
cancelTokens.clear();
}
/**
* 创建带有请求去重功能的请求方法
* @param method HTTP 方法名
* @param hasBody 是否包含请求体
*/
function createRequestMethod(
method: 'get' | 'post' | 'put' | 'patch' | 'delete',
hasBody: boolean
) {
if (hasBody) {
return <T = ApiResponse>(url: string, data?: unknown, config?: InternalAxiosRequestConfig) => {
const requestKey = getRequestKey({ method: method.toUpperCase(), url, data });
if (pendingRequests.has(requestKey)) {
return pendingRequests.get(requestKey) as Promise<AxiosResponse<T>>;
}
const promise = apiClient[method]<T>(url, data, config);
pendingRequests.set(requestKey, promise as Promise<AxiosResponse<ApiResponse>>);
promise.finally(() => {
pendingRequests.delete(requestKey);
});
return promise;
};
}
return <T = ApiResponse>(url: string, config?: InternalAxiosRequestConfig) => {
const requestKey = getRequestKey({ method: method.toUpperCase(), url, data: config?.params });
if (pendingRequests.has(requestKey)) {
return pendingRequests.get(requestKey) as Promise<AxiosResponse<T>>;
}
const promise = apiClient[method]<T>(url, config);
pendingRequests.set(requestKey, promise as Promise<AxiosResponse<ApiResponse>>);
promise.finally(() => {
pendingRequests.delete(requestKey);
});
return promise;
};
}
/**
* 包装的 API 客户端
* 在原有 axios 实例基础上添加请求缓存功能
*/
const wrappedApiClient = {
get: createRequestMethod('get', false) as <T = ApiResponse>(url: string, config?: InternalAxiosRequestConfig) => Promise<AxiosResponse<T>>,
post: createRequestMethod('post', true) as <T = ApiResponse>(url: string, data?: unknown, config?: InternalAxiosRequestConfig) => Promise<AxiosResponse<T>>,
put: createRequestMethod('put', true) as <T = ApiResponse>(url: string, data?: unknown, config?: InternalAxiosRequestConfig) => Promise<AxiosResponse<T>>,
patch: createRequestMethod('patch', true) as <T = ApiResponse>(url: string, data?: unknown, config?: InternalAxiosRequestConfig) => Promise<AxiosResponse<T>>,
delete: createRequestMethod('delete', false) as <T = ApiResponse>(url: string, config?: InternalAxiosRequestConfig) => Promise<AxiosResponse<T>>,
};
export default wrappedApiClient;
+188
View File
@@ -0,0 +1,188 @@
import apiClient from './api-client';
import { ApiResponse } from './types';
import { InternalAxiosRequestConfig } from 'axios';
/**
* 服务基类
* 提供通用的 HTTP 方法封装
*
* @example
* ```typescript
* class UserService extends BaseService {
* protected static readonly basePath = '/api/v1/users';
*
* static async getAll() {
* return this.get<User[]>('/');
* }
* }
* ```
*/
export class BaseService {
/**
* API 基础路径
* 子类必须重写此属性
*/
protected static readonly basePath: string = '';
/**
* 获取完整的 API 路径
* @param path - API 路径
* @returns 完整路径
*/
protected static getFullPath(path: string): string {
return `${ this.basePath }${ path }`;
}
/**
* GET 请求
* @template T - 响应数据类型
* @param path - API 路径
* @param params - 查询参数
* @param config - 额外的请求配置
* @returns 响应数据
*/
protected static async get<T>(
path: string,
params?: Record<string, unknown>,
config?: InternalAxiosRequestConfig,
): Promise<T> {
const requestConfig: InternalAxiosRequestConfig = {
...config,
params,
} as InternalAxiosRequestConfig;
const response = await apiClient.get<ApiResponse<T>>(
this.getFullPath(path),
requestConfig,
);
return response.data.data;
}
/**
* POST 请求
* @template T - 响应数据类型
* @param path - API 路径
* @param data - 请求数据
* @param config - 额外的请求配置
* @returns 响应数据
*/
protected static async post<T>(
path: string,
data?: unknown,
config?: InternalAxiosRequestConfig,
): Promise<T> {
const response = await apiClient.post<ApiResponse<T>>(
this.getFullPath(path),
data,
config,
);
return response.data.data;
}
/**
* PUT 请求
* @template T - 响应数据类型
* @param path - API 路径
* @param data - 请求数据
* @param config - 额外的请求配置
* @returns 响应数据
*/
protected static async put<T>(
path: string,
data?: unknown,
config?: InternalAxiosRequestConfig,
): Promise<T> {
const response = await apiClient.put<ApiResponse<T>>(
this.getFullPath(path),
data,
config,
);
return response.data.data;
}
/**
* PATCH 请求
* @template T - 响应数据类型
* @param path - API 路径
* @param data - 请求数据
* @param config - 额外的请求配置
* @returns 响应数据
*/
protected static async patch<T>(
path: string,
data?: unknown,
config?: InternalAxiosRequestConfig,
): Promise<T> {
const response = await apiClient.patch<ApiResponse<T>>(
this.getFullPath(path),
data,
config,
);
return response.data.data;
}
/**
* DELETE 请求
* @template T - 响应数据类型
* @param path - API 路径
* @param params - 查询参数
* @param config - 额外的请求配置
* @returns 响应数据
*/
protected static async delete<T>(
path: string,
params?: Record<string, unknown>,
config?: InternalAxiosRequestConfig,
): Promise<T> {
const requestConfig: InternalAxiosRequestConfig = {
...config,
params,
} as InternalAxiosRequestConfig;
const response = await apiClient.delete<ApiResponse<T>>(
this.getFullPath(path),
requestConfig,
);
return response.data.data;
}
/**
* 原始 GET 请求(用于特殊 API 端点)
* @template T - 响应数据类型
* @param url - 完整 URL
* @param params - 查询参数
* @returns 响应数据(不经过 response.data.data 解包)
*
* @remarks
* 仅用于不遵循标准响应格式的特殊端点(如 /api.php)
*/
protected static async rawGet<T>(
url: string,
params?: unknown,
): Promise<T> {
const response = await apiClient.get<T>(
url,
{ params } as InternalAxiosRequestConfig,
);
return response.data;
}
/**
* 原始 POST 请求(用于特殊 API 端点)
* @template T - 响应数据类型
* @param url - 完整 URL
* @param data - 请求数据
* @param config - 额外的请求配置
* @returns 响应数据(不经过 response.data.data 解包)
*
* @remarks
* 仅用于不遵循标准响应格式的特殊端点(如 /api.php)
*/
protected static async rawPost<T>(
url: string,
data?: unknown,
config?: InternalAxiosRequestConfig,
): Promise<T> {
const response = await apiClient.post<T>(url, data, config);
return response.data;
}
}
+24
View File
@@ -0,0 +1,24 @@
/**
* API 配置
*/
/**
* 获取 API 基础 URL
* @returns API 基础 URL
*/
export function getApiBaseUrl(): string {
return process.env.NEXT_PUBLIC_LINUX_DO_CREDIT_BACKEND_URL || '';
}
/**
* API 配置选项
*/
export const apiConfig = {
/** Basic URL */
baseURL: getApiBaseUrl(),
/** 超时时间(毫秒) */
timeout: 15000,
/** 携带凭证 */
withCredentials: true,
} as const;
+105
View File
@@ -0,0 +1,105 @@
/**
* API 错误类型定义
*/
/**
* API 错误基类
*/
export class ApiErrorBase extends Error {
constructor(
message: string,
public readonly code?: string,
public readonly statusCode?: number,
public readonly details?: unknown,
) {
super(message);
this.name = 'ApiError';
Object.setPrototypeOf(this, ApiErrorBase.prototype);
}
}
/**
* 网络错误
*/
export class NetworkError extends ApiErrorBase {
constructor(message = '网络连接失败,请检查您的网络') {
super(message);
this.name = 'NetworkError';
Object.setPrototypeOf(this, NetworkError.prototype);
}
}
/**
* 超时错误
*/
export class TimeoutError extends ApiErrorBase {
constructor(message = '请求超时,请稍后重试') {
super(message);
this.name = 'TimeoutError';
Object.setPrototypeOf(this, TimeoutError.prototype);
}
}
/**
* 未授权错误 (401)
*/
export class UnauthorizedError extends ApiErrorBase {
constructor(message = '未授权,请先登录') {
super(message, 'UNAUTHORIZED', 401);
this.name = 'UnauthorizedError';
Object.setPrototypeOf(this, UnauthorizedError.prototype);
}
}
/**
* 权限不足错误 (403)
*/
export class ForbiddenError extends ApiErrorBase {
constructor(message = '权限不足', code = 'FORBIDDEN', details?: unknown) {
super(message, code, 403, details);
this.name = 'ForbiddenError';
Object.setPrototypeOf(this, ForbiddenError.prototype);
}
}
/**
* 资源未找到错误 (404)
*/
export class NotFoundError extends ApiErrorBase {
constructor(message = '请求的资源不存在') {
super(message, 'NOT_FOUND', 404);
this.name = 'NotFoundError';
Object.setPrototypeOf(this, NotFoundError.prototype);
}
}
/**
* 服务器错误 (5xx)
*/
export class ServerError extends ApiErrorBase {
constructor(message = '服务器内部错误,请稍后重试', statusCode = 500) {
super(message, 'SERVER_ERROR', statusCode);
this.name = 'ServerError';
Object.setPrototypeOf(this, ServerError.prototype);
}
}
/**
* 验证错误 (400)
*/
export class ValidationError extends ApiErrorBase {
constructor(message = '请求参数验证失败', details?: unknown) {
super(message, 'VALIDATION_ERROR', 400, details);
this.name = 'ValidationError';
Object.setPrototypeOf(this, ValidationError.prototype);
}
}
/**
* 检查错误是否为请求取消错误
* @param error - 错误对象
* @returns 是否为取消错误
*/
export function isCancelError(error: unknown): boolean {
return error !== null && typeof error === 'object' && ('__CANCEL__' in error && (error as { __CANCEL__?: boolean }).__CANCEL__ === true || ('message' in error && (error as { message?: string }).message === '请求已被取消'));
}
+27
View File
@@ -0,0 +1,27 @@
/**
* 核心服务模块
* 提供 API 请求的基础设施
*/
export { default as apiClient, cancelRequest, cancelAllRequests } from './api-client';
export { BaseService } from './base.service';
export { apiConfig } from './config';
export {
ApiErrorBase,
NetworkError,
TimeoutError,
UnauthorizedError,
ForbiddenError,
NotFoundError,
ServerError,
ValidationError,
isCancelError,
} from './errors';
export type {
ApiResponse,
ApiError,
PaginationParams,
PaginationResponse,
RequestConfig,
} from './types';
+64
View File
@@ -0,0 +1,64 @@
/**
* API 响应通用结构
* @template T - 数据类型
*/
export interface ApiResponse<T = unknown> {
/** 响应数据 */
data: T;
/** 响应消息 */
message?: string;
/** 响应状态码 */
code?: number;
}
/**
* API 错误响应结构
*/
export interface ApiError {
/** 错误消息 */
error_msg: string;
/** 错误代码 */
error_code?: string;
/** 错误详情 */
details?: unknown;
}
/**
* 分页请求参数
*/
export interface PaginationParams {
/** 页码,从 1 开始 */
page: number;
/** 每页数量 */
page_size: number;
}
/**
* 分页响应数据
* @template T - 列表项类型
*/
export interface PaginationResponse<T> {
/** 数据列表 */
items: T[];
/** 总数 */
total: number;
/** 当前页码 */
page: number;
/** 每页数量 */
page_size: number;
/** 总页数 */
total_pages: number;
}
/**
* HTTP 请求配置
*/
export interface RequestConfig {
/** 请求超时时间(毫秒) */
timeout?: number;
/** 请求头 */
headers?: Record<string, string>;
/** 是否携带凭证 */
withCredentials?: boolean;
}
@@ -0,0 +1,69 @@
import { BaseService } from '../core/base.service';
import type {
DailyStatsResponse,
TopCustomersResponse,
} from './types';
/**
* 仪表板服务
* 处理仪表板统计数据相关的 API 请求
*/
export class DashboardService extends BaseService {
protected static readonly basePath = '/api/v1/dashboard';
/**
* 获取每日收支统计
* @param days - 查询天数,最大7天,最小1天
* @returns 每日统计数据列表
* @throws {UnauthorizedError} 当未登录时
* @throws {ValidationError} 当参数验证失败时
*
* @example
* ```typescript
* const stats = await DashboardService.getDailyStats(7);
* console.log('每日统计:', stats);
* // 输出: [
* // { date: '2025-12-22', income: '1000.00', expense: '500.00' },
* // { date: '2025-12-21', income: '800.00', expense: '300.00' },
* // ...
* // ]
* ```
*
* @remarks
* - days 参数必须在 1-7 之间
* - 返回的数据按日期降序排列
* - income 和 expense 为字符串格式的金额
*/
static async getDailyStats(days: number): Promise<DailyStatsResponse> {
return this.get<DailyStatsResponse>('/stats/daily', { days });
}
/**
* 获取 Top 客户统计
* @param days - 查询天数,最大7天,最小1天
* @param limit - 返回数量,最大10条,最小1条
* @returns Top 客户列表
* @throws {UnauthorizedError} 当未登录时
* @throws {ValidationError} 当参数验证失败时
*
* @example
* ```typescript
* const customers = await DashboardService.getTopCustomers(7, 5);
* console.log('Top 5 客户:', customers);
* // 输出: [
* // { user_id: 123, username: 'user1', total_amount: '5000.00', order_count: 10 },
* // { user_id: 456, username: 'user2', total_amount: '3000.00', order_count: 8 },
* // ...
* // ]
* ```
*
* @remarks
* - days 参数必须在 1-7 之间
* - limit 参数必须在 1-10 之间
* - 返回的数据按累计付款金额降序排列
* - total_amount 为字符串格式的金额
*/
static async getTopCustomers(days: number, limit: number): Promise<TopCustomersResponse> {
return this.get<TopCustomersResponse>('/stats/top-customers', { days, limit });
}
}
+29
View File
@@ -0,0 +1,29 @@
/**
* Dashboard 服务模块
*
* @description
* 提供仪表板统计数据相关的服务,包括:
* - 每日收支统计
* - Top 客户统计
*
* @example
* ```typescript
* import { DashboardService } from '@/lib/services/dashboard';
*
* // 获取最近7天的收支统计
* const dailyStats = await DashboardService.getDailyStats(7);
*
* // 获取 Top 5 客户
* const topCustomers = await DashboardService.getTopCustomers(7, 5);
* ```
*/
export { DashboardService } from './dashboard.service';
export type {
DailyStatsItem,
DailyStatsResponse,
GetDailyStatsRequest,
TopCustomer,
TopCustomersResponse,
GetTopCustomersRequest,
} from './types';
+57
View File
@@ -0,0 +1,57 @@
/**
* Dashboard 服务类型定义
*/
/**
* 每日统计项
*/
export interface DailyStatsItem {
/** 日期,格式: YYYY-MM-DD */
date: string;
/** 当日收入金额 */
income: string;
/** 当日支出金额 */
expense: string;
}
/**
* 每日统计响应
*/
export type DailyStatsResponse = DailyStatsItem[];
/**
* 获取每日统计请求参数
*/
export interface GetDailyStatsRequest {
/** 查询天数,最大7天,最小1天 */
days: number;
}
/**
* Top 客户项
*/
export interface TopCustomer {
/** 客户用户ID */
user_id: number;
/** 客户用户名 */
username: string;
/** 累计付款金额 */
total_amount: string;
/** 订单数量 */
order_count: number;
}
/**
* Top 客户响应
*/
export type TopCustomersResponse = TopCustomer[];
/**
* 获取 Top 客户请求参数
*/
export interface GetTopCustomersRequest {
/** 查询天数,最大7天,最小1天 */
days: number;
/** 返回数量,最大10条,最小1条 */
limit: number;
}
@@ -0,0 +1,128 @@
import { BaseService } from '../core/base.service';
import type {
ListDisputesRequest,
ListDisputesResponse,
RefundReviewRequest,
CloseDisputeRequest,
CreateDisputeRequest,
CreateDisputeResponse,
} from './types';
/**
* 争议服务
* 处理争议相关的 API 请求
*/
export class DisputeService extends BaseService {
protected static readonly basePath = '/api/v1/order';
/**
* 创建争议
* @param data - 争议信息
* @returns void
* @throws {UnauthorizedError} 当未登录时
* @throws {NotFoundError} 当订单不存在或不符合争议条件时
* @throws {ValidationError} 当参数验证失败时
*
* @example
* ```typescript
* await DisputeService.createDispute({
* order_id: 123,
* reason: '商品质量问题'
* });
* ```
*/
static async createDispute(data: CreateDisputeRequest): Promise<CreateDisputeResponse> {
return this.post<CreateDisputeResponse>('/dispute', data);
}
/**
* 查询用户发起的争议列表
* @param params - 查询参数
* @returns 争议列表
* @throws {UnauthorizedError} 当未登录时
* @throws {ValidationError} 当参数验证失败时
*
* @example
* ```typescript
* const result = await DisputeService.listDisputes({
* page: 1,
* page_size: 20,
* status: 'disputing'
* });
* console.log('争议数量:', result.total);
* ```
*/
static async listDisputes(
params: ListDisputesRequest
): Promise<ListDisputesResponse> {
return this.post<ListDisputesResponse>('/disputes', params);
}
/**
* 查询商户的争议列表
* @param params - 查询参数
* @returns 争议列表
* @throws {UnauthorizedError} 当未登录时
* @throws {ValidationError} 当参数验证失败时
*
* @example
* ```typescript
* const result = await DisputeService.listMerchantDisputes({
* page: 1,
* page_size: 20
* });
* console.log('商户争议数量:', result.total);
* ```
*/
static async listMerchantDisputes(
params: ListDisputesRequest
): Promise<ListDisputesResponse> {
return this.post<ListDisputesResponse>('/disputes/merchant', params);
}
/**
* 退款审核(商户使用)
* @param data - 审核请求
* @returns void
* @throws {UnauthorizedError} 当未登录时
* @throws {NotFoundError} 当争议不存在时
* @throws {ValidationError} 当参数验证失败时
*
* @example
* ```typescript
* // 同意退款
* await DisputeService.refundReview({
* dispute_id: 123,
* status: 'refund'
* });
*
* // 拒绝退款
* await DisputeService.refundReview({
* dispute_id: 123,
* status: 'closed',
* reason: '不符合退款条件'
* });
* ```
*/
static async refundReview(data: RefundReviewRequest): Promise<void> {
return this.post('/refund-review', data);
}
/**
* 关闭争议(用户主动关闭)
* @param data - 关闭争议请求
* @returns void
* @throws {UnauthorizedError} 当未登录时
* @throws {NotFoundError} 当争议不存在时
*
* @example
* ```typescript
* await DisputeService.closeDispute({
* dispute_id: 123
* });
* ```
*/
static async closeDispute(data: CloseDisputeRequest): Promise<void> {
return this.post('/dispute/close', data);
}
}
+31
View File
@@ -0,0 +1,31 @@
/**
* 争议服务模块
*
* @description
* 提供争议处理相关的功能,包括:
* - 创建争议
* - 查询用户发起的争议列表
* - 查询商户的争议列表
* - 退款审核(商户)
* - 关闭争议(用户)
*
* @example
* ```typescript
* import { DisputeService } from '@/lib/services';
*
* // 创建争议
* await DisputeService.createDispute({
* order_id: 123,
* reason: '商品质量问题'
* });
*
* // 查询争议列表
* const disputes = await DisputeService.listDisputes({
* page: 1,
* page_size: 20
* });
* ```
*/
export { DisputeService } from './dispute.service';
export type * from './types';
+105
View File
@@ -0,0 +1,105 @@
/**
* 争议状态
*/
export type DisputeStatus = 'disputing' | 'refund' | 'closed';
/**
* 争议信息
*/
export interface Dispute {
/** 争议 ID */
id: string;
/** 订单 ID */
order_id: string;
/** 发起者用户 ID */
initiator_user_id: number;
/** 争议原因 */
reason: string;
/** 争议状态 */
status: DisputeStatus;
/** 处理者用户 ID */
handler_user_id?: number;
/** 发起者账户 */
initiator_username: string;
/** 处理者账户 */
handler_username: string;
/** 创建时间 */
created_at: string;
/** 更新时间 */
updated_at: string;
}
/**
* 争议列表项(包含订单信息)
*/
export interface DisputeWithOrder extends Dispute {
/** 订单名称 */
order_name: string;
/** 收款方账户 */
payee_username: string;
/** 订单金额 */
amount: string;
}
/**
* 查询争议列表请求
*/
export interface ListDisputesRequest {
/** 页码,从 1 开始 */
page: number;
/** 每页数量,1-100 */
page_size: number;
/** 状态筛选(可选) */
status?: DisputeStatus;
/** 争议 ID(可选) */
dispute_id?: string;
}
/**
* 争议列表响应
*/
export interface ListDisputesResponse {
/** 总记录数 */
total: number;
/** 当前页码 */
page: number;
/** 每页数量 */
page_size: number;
/** 争议列表 */
disputes: DisputeWithOrder[];
}
/**
* 退款审核请求
*/
export interface RefundReviewRequest {
/** 争议 ID */
dispute_id: string;
/** 审核结果 */
status: 'refund' | 'closed';
/** 拒绝原因(status 为 closed 时必填,最大 100 字符) */
reason?: string;
}
/**
* 关闭争议请求
*/
export interface CloseDisputeRequest {
/** 争议 ID */
dispute_id: string;
}
/**
* 创建争议请求
*/
export interface CreateDisputeRequest {
/** 订单 ID */
order_id: string;
/** 争议原因(最大 100 字符) */
reason: string;
}
/**
* 创建争议响应
*/
export type CreateDisputeResponse = Dispute;
+212
View File
@@ -0,0 +1,212 @@
/**
* 服务层统一入口
* 提供所有业务服务的访问接口
*
* @example
* ```typescript
* // 推荐:使用统一的 services 对象
* import services from '@/lib/services';
*
* const user = await services.auth.getUserInfo();
* const transactions = await services.transaction.getTransactions({ page: 1, page_size: 20 });
* ```
*
* @example
* ```typescript
* // 按需导入:直接导入特定服务
* import { AuthService } from '@/lib/services';
*
* const user = await AuthService.getUserInfo();
* ```
*/
import { AuthService } from './auth';
import { TransactionService } from './transaction';
import { MerchantService } from './merchant';
import { AdminService } from './admin';
import { UserService } from './user';
import { DisputeService } from './dispute';
import { ConfigService } from './config';
import { DashboardService } from './dashboard';
import { LeaderboardService } from './leaderboard';
import { RedEnvelopeService } from './redenvelope';
import { UploadService } from './upload';
/**
* 服务对象
* 集中导出所有业务服务
*
* @description
* 推荐使用此对象访问所有服务,保持代码风格统一
*/
const services = {
/** 认证服务 */
auth: AuthService,
/** 交易服务 */
transaction: TransactionService,
/** 商户服务 */
merchant: MerchantService,
/** 管理员服务 */
admin: AdminService,
/** 用户服务 */
user: UserService,
/** 争议服务 */
dispute: DisputeService,
/** 配置服务 */
config: ConfigService,
/** 仪表板服务 */
dashboard: DashboardService,
/** 排行榜服务 */
leaderboard: LeaderboardService,
/** 红包服务 */
redEnvelope: RedEnvelopeService,
/** 上传服务 */
upload: UploadService,
} as const;
export default services;
// ==================== 核心模块导出 ====================
export {
apiClient,
BaseService,
apiConfig,
cancelRequest,
cancelAllRequests,
} from './core';
export {
ApiErrorBase,
NetworkError,
TimeoutError,
UnauthorizedError,
ForbiddenError,
NotFoundError,
ServerError,
ValidationError,
isCancelError,
} from './core';
export type {
ApiResponse,
ApiError,
PaginationParams,
PaginationResponse,
RequestConfig,
} from './core';
// ==================== 业务服务导出 ====================
// 认证服务
export { AuthService, TrustLevel } from './auth';
export type { User, OAuthLoginUrlResponse, OAuthCallbackRequest } from './auth';
// 交易服务
export { TransactionService } from './transaction';
export { DEFAULT_ORDER_TYPES } from './transaction';
export type {
Order,
OrderType,
OrderStatus,
TransferStatus,
TransactionQueryParams,
TransactionListResponse,
} from './transaction';
// 争议服务
export { DisputeService } from './dispute';
export type {
Dispute,
DisputeStatus,
DisputeWithOrder,
ListDisputesRequest,
ListDisputesResponse,
RefundReviewRequest,
CloseDisputeRequest,
CreateDisputeRequest,
} from './dispute';
// 配置服务
export { ConfigService } from './config';
export type { PublicConfigResponse } from './config';
// 商户服务
export { MerchantService } from './merchant';
export type {
MerchantAPIKey,
CreateAPIKeyRequest,
UpdateAPIKeyRequest,
PayMerchantOrderRequest,
GetMerchantOrderRequest,
GetMerchantOrderResponse,
PaymentLink,
CreatePaymentLinkRequest,
QueryMerchantOrderRequest,
QueryMerchantOrderResponse,
RefundMerchantOrderRequest,
RefundMerchantOrderResponse,
GetPaymentLinkInfoResponse,
} from './merchant';
// 管理员服务
export { AdminService } from './admin';
export type {
SystemConfig,
CreateSystemConfigRequest,
UpdateSystemConfigRequest,
UserPayConfig,
CreateUserPayConfigRequest,
UpdateUserPayConfigRequest,
TaskMeta,
DispatchTaskRequest,
AdminUser,
ListUsersRequest,
ListUsersResponse,
UpdateUserStatusRequest,
} from './admin';
// 用户服务
export { UserService } from './user';
export type { UpdatePayKeyRequest } from './user';
// 仪表板服务
export { DashboardService } from './dashboard';
export type {
DailyStatsItem,
DailyStatsResponse,
GetDailyStatsRequest,
TopCustomer,
TopCustomersResponse,
GetTopCustomersRequest,
} from './dashboard';
// 红包服务
export { RedEnvelopeService } from './redenvelope';
export type {
RedEnvelopeType,
RedEnvelopeStatus,
RedEnvelope,
RedEnvelopeClaim,
CreateRedEnvelopeRequest,
CreateRedEnvelopeResponse,
ClaimRedEnvelopeRequest,
ClaimRedEnvelopeResponse,
RedEnvelopeDetailResponse,
RedEnvelopeListParams,
RedEnvelopeListResponse,
} from './redenvelope';
// 排行榜服务
export { LeaderboardService } from './leaderboard';
export type {
LeaderboardEntry,
LeaderboardListRequest,
LeaderboardListResponse,
UserRankInfo,
UserRankResponse,
} from './leaderboard';
// 上传服务
export { UploadService } from './upload';
export type { UploadImageResponse } from './upload';
@@ -0,0 +1,26 @@
/**
* Leaderboard 服务模块
*
* @description
* 提供排行榜相关的服务,基于 available_balance 排序
*
* @example
* ```typescript
* import { LeaderboardService } from '@/lib/services/leaderboard';
*
* // 获取排行榜
* const list = await LeaderboardService.getList({ page: 1, page_size: 20 });
*
* // 获取当前用户排名
* const myRank = await LeaderboardService.getMyRank();
* ```
*/
export { LeaderboardService } from "./leaderboard.service";
export type {
LeaderboardEntry,
LeaderboardListRequest,
LeaderboardListResponse,
UserRankInfo,
UserRankResponse,
} from "./types";
@@ -0,0 +1,42 @@
import { BaseService } from "../core/base.service";
import type {
LeaderboardListRequest,
LeaderboardListResponse,
UserRankResponse,
} from "./types";
/**
* 排行榜服务
* 基于 available_balance 排序
*/
export class LeaderboardService extends BaseService {
protected static readonly basePath = "/api/v1/leaderboard";
/**
* 获取排行榜列表
* @param params - 分页参数
* @returns 排行榜列表响应
*/
static async getList(
params?: LeaderboardListRequest,
): Promise<LeaderboardListResponse> {
return this.get<LeaderboardListResponse>("", params);
}
/**
* 获取当前用户排名
* @returns 用户排名响应
*/
static async getMyRank(): Promise<UserRankResponse> {
return this.get<UserRankResponse>("/me");
}
/**
* 获取指定用户排名
* @param userId - 用户 ID
* @returns 用户排名响应
*/
static async getUserRankById(userId: number): Promise<UserRankResponse> {
return this.get<UserRankResponse>(`/users/${ userId }`);
}
}
@@ -0,0 +1,68 @@
/**
* Leaderboard 服务类型定义
* 使用 available_balance 排序
*/
/**
* 排行榜条目
*/
export interface LeaderboardEntry {
/** 用户ID */
user_id: number;
/** 用户名 */
username: string;
/** 头像URL */
avatar_url: string;
/** 可用余额 */
available_balance: string;
}
/**
* 排行榜请求参数
*/
export interface LeaderboardListRequest {
/** 页码 */
page: number;
/** 每页数量 */
page_size: number;
/** 索引签名 */
[key: string]: unknown;
}
/**
* 排行榜列表响应
*/
export interface LeaderboardListResponse {
/** 排序字段 */
sort_by: string;
/** 排序方向 */
order: string;
/** 当前页码 */
page: number;
/** 每页数量 */
page_size: number;
/** 总数 */
total: number;
/** 排行榜条目列表 */
items: LeaderboardEntry[];
}
/**
* 用户排名信息
*/
export interface UserRankInfo {
/** 用户ID */
user_id: number;
/** 排名 */
rank: number;
/** 可用余额 */
available_balance: string;
}
/**
* 用户排名响应
*/
export interface UserRankResponse {
/** 用户排名信息 */
user: UserRankInfo;
}
+45
View File
@@ -0,0 +1,45 @@
/**
* 商户服务模块
*
* @description
* 提供商户相关的功能,包括:
* - API Key 管理(创建、查询、更新、删除)
* - 支付链接管理(创建、列表、删除)
* - 商户订单查询和支付
* - 商户订单退款
*
* @example
* ```typescript
* import { MerchantService } from '@/lib/services';
*
* // 创建 API Key
* const apiKey = await MerchantService.createAPIKey({
* app_name: '我的应用',
* app_homepage_url: 'https://example.com',
* redirect_uri: 'https://example.com/callback',
* notify_url: 'https://example.com/notify'
* });
*
* // 获取支付链接
* const links = await MerchantService.listPaymentLinks(apiKey.id);
* ```
*/
export { MerchantService } from './merchant.service';
export type {
MerchantAPIKey,
CreateAPIKeyRequest,
UpdateAPIKeyRequest,
PayMerchantOrderRequest,
GetMerchantOrderRequest,
GetMerchantOrderResponse,
PaymentLink,
CreatePaymentLinkRequest,
PayByLinkRequest,
GetPaymentLinkInfoResponse,
QueryMerchantOrderRequest,
QueryMerchantOrderResponse,
RefundMerchantOrderRequest,
RefundMerchantOrderResponse,
} from './types';
@@ -0,0 +1,491 @@
import { AxiosHeaders, type InternalAxiosRequestConfig } from 'axios';
import { BaseService } from '../core/base.service';
import { encodeBase64 } from '../../utils';
import type {
MerchantAPIKey,
CreateAPIKeyRequest,
UpdateAPIKeyRequest,
PayMerchantOrderRequest,
GetMerchantOrderRequest,
GetMerchantOrderResponse,
PaymentLink,
CreatePaymentLinkRequest,
UpdatePaymentLinkRequest,
PayByLinkRequest,
QueryMerchantOrderRequest,
QueryMerchantOrderResponse,
RefundMerchantOrderRequest,
RefundMerchantOrderResponse,
MerchantDistributeRequest,
MerchantDistributeResponse,
} from './types';
/**
* 商户服务
* 处理商户 API Key 管理和支付订单相关的 API 请求
*/
export class MerchantService extends BaseService {
protected static readonly basePath = '/api/v1/merchant';
// ==================== API Key 管理 ====================
/**
* 创建商户 API Key
* @param request - 创建 API Key 的请求参数
* @returns 创建的 API Key 信息(包含 client_secret)
* @throws {UnauthorizedError} 当未登录时
* @throws {ValidationError} 当参数验证失败时
*
* @example
* ```typescript
* const apiKey = await MerchantService.createAPIKey({
* app_name: '我的应用',
* app_homepage_url: 'https://example.com',
* app_description: '应用描述',
* redirect_uri: 'https://example.com/callback'
* });
* console.log('Client ID:', apiKey.client_id);
* console.log('Client Secret:', apiKey.client_secret); // 请保存,之后无法再次获取
* ```
*/
static async createAPIKey(request: CreateAPIKeyRequest): Promise<MerchantAPIKey> {
return this.post<MerchantAPIKey>('/api-keys', request);
}
/**
* 获取商户 API Key 列表
* @returns API Key 列表
* @throws {UnauthorizedError} 当未登录时
*
* @example
* ```typescript
* const apiKeys = await MerchantService.listAPIKeys();
* console.log('API Keys 数量:', apiKeys.length);
* ```
*/
static async listAPIKeys(): Promise<MerchantAPIKey[]> {
return this.get<MerchantAPIKey[]>('/api-keys');
}
/**
* 获取单个商户 API Key
* @param id - API Key ID
* @returns API Key 详细信息
* @throws {UnauthorizedError} 当未登录时
* @throws {NotFoundError} 当 API Key 不存在时
* @throws {ForbiddenError} 当无权访问该 API Key 时
*
* @example
* ```typescript
* const apiKey = await MerchantService.getAPIKey(123);
* console.log('应用名称:', apiKey.app_name);
* ```
*/
static async getAPIKey(id: string): Promise<MerchantAPIKey> {
return this.get<MerchantAPIKey>(`/api-keys/${ id }`);
}
/**
* 更新商户 API Key
* @param id - API Key ID
* @param request - 更新 API Key 的请求参数
* @returns void
* @throws {UnauthorizedError} 当未登录时
* @throws {NotFoundError} 当 API Key 不存在时
* @throws {ForbiddenError} 当无权访问该 API Key 时
* @throws {ValidationError} 当参数验证失败时
*
* @example
* ```typescript
* await MerchantService.updateAPIKey(123, {
* app_name: '新的应用名称',
* app_description: '新的描述'
* });
* ```
*/
static async updateAPIKey(
id: string,
request: UpdateAPIKeyRequest,
): Promise<void> {
return this.put<void>(`/api-keys/${ id }`, request);
}
/**
* 删除商户 API Key
* @param id - API Key ID
* @returns void
* @throws {UnauthorizedError} 当未登录时
* @throws {NotFoundError} 当 API Key 不存在时
* @throws {ForbiddenError} 当无权访问该 API Key 时
*
* @example
* ```typescript
* await MerchantService.deleteAPIKey(123);
* ```
*/
static async deleteAPIKey(id: string): Promise<void> {
return this.delete<void>(`/api-keys/${ id }`);
}
// ==================== 支付链接管理 ====================
/**
* 创建支付链接
* @param apiKeyId - API Key ID
* @param request - 创建支付链接请求参数
* @returns 创建的支付链接信息
* @throws {UnauthorizedError} 当未登录时
* @throws {NotFoundError} 当 API Key 不存在时
* @throws {ForbiddenError} 当无权访问该 API Key 时
*
* @example
* ```typescript
* const link = await MerchantService.createPaymentLink(123, {
* product_name: '测试商品',
* amount: 100,
* remark: '备注信息'
* });
* console.log('支付链接 Token:', link.token);
* ```
*/
static async createPaymentLink(apiKeyId: string, request: CreatePaymentLinkRequest): Promise<PaymentLink> {
return this.post<PaymentLink>(`/api-keys/${ apiKeyId }/payment-links`, request);
}
/**
* 获取支付链接列表
* @param apiKeyId - API Key ID
* @returns 支付链接列表
* @throws {UnauthorizedError} 当未登录时
* @throws {NotFoundError} 当 API Key 不存在时
* @throws {ForbiddenError} 当无权访问该 API Key 时
*
* @example
* ```typescript
* const links = await MerchantService.listPaymentLinks(123);
* console.log('支付链接数量:', links.length);
* ```
*/
static async listPaymentLinks(apiKeyId: string): Promise<PaymentLink[]> {
return this.get<PaymentLink[]>(`/api-keys/${ apiKeyId }/payment-links`);
}
/**
* 删除支付链接
* @param apiKeyId - API Key ID
* @param linkId - 支付链接 ID
* @returns void
* @throws {UnauthorizedError} 当未登录时
* @throws {NotFoundError} 当 API Key 或支付链接不存在时
* @throws {ForbiddenError} 当无权访问该 API Key 时
*
* @example
* ```typescript
* await MerchantService.deletePaymentLink(123, 456);
* ```
*/
static async deletePaymentLink(apiKeyId: string, linkId: string): Promise<void> {
return this.delete<void>(`/api-keys/${ apiKeyId }/payment-links/${ linkId }`);
}
/**
* 更新支付链接
* @param apiKeyId - API Key ID
* @param linkId - 支付链接 ID
* @param request - 更新支付链接请求参数
* @returns void
* @throws {UnauthorizedError} 当未登录时
* @throws {NotFoundError} 当 API Key 或支付链接不存在时
* @throws {ForbiddenError} 当无权访问该 API Key 时
* @throws {ValidationError} 当参数验证失败时
*
* @example
* ```typescript
* await MerchantService.updatePaymentLink(123, 456, {
* product_name: '更新后的商品',
* amount: 200,
* remark: '新的备注',
* total_limit: 100,
* user_limit: 1
* });
* ```
*/
static async updatePaymentLink(
apiKeyId: string,
linkId: string,
request: UpdatePaymentLinkRequest
): Promise<void> {
return this.put<void>(`/api-keys/${ apiKeyId }/payment-links/${ linkId }`, request);
}
/**
* 通过 Token 获取支付链接信息
*
* @description
* 公开接口,用于支付页面获取支付链接详情。
* 无需登录即可访问。
*
* @param token - 支付链接 Token
* @returns 支付链接信息(包含商品名称、金额等)
* @throws {NotFoundError} 当支付链接不存在时
*
* @example
* ```typescript
* const linkInfo = await MerchantService.getPaymentLinkByToken('abc123');
* console.log('商品名称:', linkInfo.payment_link.product_name);
* console.log('金额:', linkInfo.payment_link.amount);
* ```
*/
static async getPaymentLinkByToken(token: string): Promise<PaymentLink> {
return this.get<PaymentLink>(`/payment-links/${ token }`);
}
/**
* 通过支付链接支付
*
* @description
* 用户使用此接口通过支付链接进行支付。
* 需要用户登录,并且用户余额充足。
*
* @param request - 支付请求参数(token 和 pay_key)
* @returns void
* @throws {UnauthorizedError} 当用户未登录时
* @throws {NotFoundError} 当支付链接不存在时
* @throws {ApiErrorBase} 当余额不足、支付密码错误等业务错误时
*
* @example
* ```typescript
* try {
* await MerchantService.payByLink({
* token: 'abc123',
* pay_key: '123456',
* remark: '备注信息'
* });
* console.log('支付成功');
* } catch (error) {
* console.error('支付失败:', error.message);
* }
* ```
*
* @remarks
* - 用户不能支付自己创建的支付链接
* - 用户余额必须充足
* - 支付成功后会扣除手续费(根据商户的支付等级)
*/
static async payByLink(request: PayByLinkRequest): Promise<void> {
return this.post<void>('/payment-links/pay', request);
}
// ==================== 商户支付订单 ====================
/**
* 查询商户订单信息
*
* @description
* 查询商户创建的订单详细信息,用于支付页面显示订单信息。
* 订单必须处于待支付状态。
*
* @param request - 查询订单请求参数
* @returns 订单信息和用户积分配置
* @throws {UnauthorizedError} 当用户未登录时
* @throws {NotFoundError} 当订单不存在时
* @throws {ValidationError} 当订单号格式错误时
* @throws {ApiErrorBase} 当订单已过期或已支付等业务错误时
*
* @example
* ```typescript
* // 从 URL 获取订单号
* const params = new URLSearchParams(window.location.search);
* const orderNo = params.get('order_no');
*
* if (orderNo) {
* try {
* const orderInfo = await MerchantService.getMerchantOrder({ order_no: orderNo });
* console.log('订单信息:', orderInfo.order);
* console.log('积分配置:', orderInfo.user_pay_config);
* } catch (error) {
* console.error('查询订单失败:', error.message);
* }
* }
* ```
*
* @remarks
* - 订单必须处于待支付状态
* - 返回的用户积分配置包含手续费率等信息
*/
static async getMerchantOrder(request: GetMerchantOrderRequest): Promise<GetMerchantOrderResponse> {
return this.get<GetMerchantOrderResponse>('/payment/order', { order_no: request.order_no });
}
/**
* 支付商户订单
*
* @description
* 用户使用此接口支付商户创建的订单。
* 需要用户登录,并且用户余额充足。
*
* @param request - 支付订单请求参数
* @returns void
* @throws {UnauthorizedError} 当用户未登录时
* @throws {NotFoundError} 当订单不存在或已过期时
* @throws {ValidationError} 当订单号格式错误时
* @throws {ApiErrorBase} 当余额不足、订单已支付等业务错误时
*
* @example
* ```typescript
* // 从 URL 获取订单号
* const params = new URLSearchParams(window.location.search);
* const orderNo = params.get('order_no');
*
* if (orderNo) {
* try {
* await MerchantService.payMerchantOrder({
* order_no: orderNo,
* pay_key: '123456' // 用户的支付密码
* });
* // 支付成功
* console.log('支付成功');
* } catch (error) {
* console.error('支付失败:', error.message);
* }
* }
* ```
*
* @remarks
* - 用户不能支付自己作为商户创建的订单
* - 订单必须在有效期内(5分钟)
* - 用户余额必须充足
* - 支付成功后会扣除手续费(根据用户的积分等级)
*/
static async payMerchantOrder(request: PayMerchantOrderRequest): Promise<void> {
return this.post<void>('/payment', request);
}
/**
* 商户查询订单状态
*
* @description
* 商户使用此接口主动查询订单的支付状态。
* 需要提供商户凭证(Client ID 和 Client Secret)。
*
* @param params - 查询参数
* @returns 订单状态信息
* @throws {ValidationError} 当参数验证失败时
* @throws {ApiErrorBase} 当商户凭证无效或订单不存在时
*
* @example
* ```typescript
* const orderStatus = await MerchantService.queryMerchantOrder({
* trade_no: 12345,
* pid: 'your_client_id',
* key: 'your_client_secret'
* });
*
* if (orderStatus.status === 1) {
* console.log('订单已支付');
* } else {
* console.log('订单未支付');
* }
* ```
*
* @remarks
* - 使用 GET 请求调用 `/api.php` 接口
* - 返回的 status 字段:1 表示已支付,0 表示未支付
*/
static async queryMerchantOrder(
params: QueryMerchantOrderRequest
): Promise<QueryMerchantOrderResponse> {
return this.rawGet<QueryMerchantOrderResponse>('/epay/api.php', params);
}
/**
* 商户退款
*
* @description
* 商户使用此接口对已支付的订单进行退款。
* 需要提供商户凭证(Client ID 和 Client Secret)。
* 退款金额必须与订单金额一致。
*
* @param params - 退款请求参数
* @returns 退款结果
* @throws {ValidationError} 当参数验证失败时
* @throws {ApiErrorBase} 当商户凭证无效、订单不存在或退款失败时
*
* @example
* ```typescript
* const result = await MerchantService.refundMerchantOrder({
* trade_no: 12345,
* money: 99.99,
* pid: 'your_client_id',
* key: 'your_client_secret'
* });
*
* if (result.code === 1) {
* console.log('退款成功');
* } else {
* console.error('退款失败:', result.msg);
* }
* ```
*
* @remarks
* - 使用 POST 请求调用 `/api.php` 接口
* - 退款金额必须与订单金额完全一致
* - 只能对状态为"成功"的订单进行退款
* - 返回的 code 字段:1 表示成功,-1 表示失败
*/
static async refundMerchantOrder(
params: RefundMerchantOrderRequest
): Promise<RefundMerchantOrderResponse> {
return this.rawPost<RefundMerchantOrderResponse>('/epay/api.php', params);
}
// ==================== 商户分发 ====================
/**
* 商户向用户分发积分
*
* @description
* 商户使用此接口向指定用户分发积分。
* 需要商户认证(Basic Auth),分发金额会从商户余额中扣除,
* 收款人获得扣除分发费率后的金额。
*
* @param request - 分发请求参数
* @returns 分发结果(包含订单号)
* @throws {ValidationError} 当参数验证失败时
* @throws {ApiErrorBase} 当余额不足、用户不存在等业务错误时
*
* @example
* ```typescript
* const result = await MerchantService.distribute({
* user_id: 123,
* username: 'alice',
* amount: 100,
* out_trade_no: 'DIST20251231001',
* remark: '新年奖励'
* });
* console.log('订单号:', result.trade_no);
* ```
*
* @remarks
* - 使用 POST 请求调用 `/pay/distribute` 接口(前端通过 `/lpay/distribute` 代理)
* - 需要通过 Basic Auth 提供商户凭证
* - 不能分发给商户自己
* - 商户余额必须充足
* - 分发会扣除分发费率(根据商户的支付等级)
*/
static async distribute(
request: MerchantDistributeRequest,
auth?: { client_id: string; client_secret: string }
): Promise<MerchantDistributeResponse> {
const config: InternalAxiosRequestConfig | undefined = auth
? {
headers: new AxiosHeaders({
Authorization: `Basic ${encodeBase64(`${auth.client_id}:${auth.client_secret}`)}`,
}),
}
: undefined;
return this.rawPost<MerchantDistributeResponse>('/lpay/distribute', request, config);
}
}
+362
View File
@@ -0,0 +1,362 @@
/**
* 商户 API Key 信息
*/
export interface MerchantAPIKey {
/** API Key ID */
id: string;
/** 用户 ID */
user_id: string;
/** 客户端 ID */
client_id: string;
/** 客户端密钥 */
client_secret: string;
/** 应用名称 */
app_name: string;
/** 应用主页 URL */
app_homepage_url: string;
/** 应用描述 */
app_description: string;
/** 重定向 URI */
redirect_uri?: string;
/** 通知 URL */
notify_url: string;
/** 公钥 (Base64) */
public_key?: string;
/** 测试模式 */
test_mode: boolean;
/** 创建时间 */
created_at: string;
/** 更新时间 */
updated_at: string;
/** 删除时间(软删除) */
deleted_at: string | null;
}
/**
* 创建商户 API Key 请求参数
*/
export interface CreateAPIKeyRequest {
/** 应用名称(最大20字符) */
app_name: string;
/** 应用主页 URL(最大100字符,必须是有效的 URL) */
app_homepage_url: string;
/** 应用描述(最大100字符,可选) */
app_description?: string;
/** 重定向 URI(最大100字符,必须是有效的 URL,可选) */
redirect_uri?: string;
/** 通知 URL(最大100字符,必须是有效的 URL) */
notify_url: string;
/** 公钥 (Base64 编码,32字节,可选) */
public_key?: string;
/** 测试模式(可选,默认为 false) */
test_mode?: boolean;
}
/**
* 更新商户 API Key 请求参数
*/
export interface UpdateAPIKeyRequest {
/** 应用名称(最大20字符,可选) */
app_name?: string;
/** 应用主页 URL(最大100字符,必须是有效的 URL,可选) */
app_homepage_url?: string;
/** 应用描述(最大100字符,可选) */
app_description?: string;
/** 重定向 URI(最大100字符,必须是有效的 URL,可选) */
redirect_uri?: string;
/** 通知 URL(最大100字符,必须是有效的 URL,可选) */
notify_url?: string;
/** 公钥 (Base64 编码,32字节,可选) */
public_key?: string;
/** 测试模式(可选) */
test_mode?: boolean;
}
/**
* 支付商户订单请求参数
*/
export interface PayMerchantOrderRequest {
/** 订单号(加密后的订单ID) */
order_no: string;
/** 支付密码(6位数字) */
pay_key: string;
}
/**
* 查询商户订单请求参数
*/
export interface GetMerchantOrderRequest {
/** 订单号(加密后的订单ID) */
order_no: string;
}
/**
* 查询商户订单响应
*/
export interface GetMerchantOrderResponse {
/** 订单信息 */
order: {
/** 订单ID */
id: string;
/** 订单号 */
order_no: string;
/** 订单名称 */
order_name: string;
/** 付款方账户 */
payer_username: string;
/** 收款方账户 */
payee_username: string;
/** 交易金额 */
amount: string;
/** 订单状态 */
status: string;
/** 订单类型 */
type: string;
/** 支付类型 */
payment_type: string;
/** 备注 */
remark: string;
/** 客户端ID */
client_id: string;
/** 同步跳转URL */
return_url: string;
/** 异步通知URL */
notify_url: string;
/** 交易时间 */
trade_time: string | null;
/** 创建时间 */
created_at: string;
/** 更新时间 */
updated_at: string;
};
/** 用户积分配置信息 */
user_pay_config: {
/** 配置ID */
id: string;
/** 积分等级 */
level: number;
/** 最低分数 */
min_score: number;
/** 最高分数 */
max_score: number | null;
/** 每日限额 */
daily_limit: number | null;
/** 手续费率 */
fee_rate: string;
/** 创建时间 */
created_at: string;
/** 更新时间 */
updated_at: string;
};
/** 商户信息 */
merchant: {
/** 应用名称 */
app_name: string;
/** 跳转URI */
redirect_uri: string;
};
}
/**
* 支付链接信息
*/
export interface PaymentLink {
/** 链接 ID */
id: string;
/** 商户 API Key ID */
merchant_api_key_id: string;
/** 支付链接 Token */
token: string;
/** 金额 */
amount: string;
/** 商品名称 */
product_name: string;
/** 备注 */
remark: string;
/** 总支付次数限制(可选) */
total_limit?: number;
/** 单用户支付次数限制(可选) */
user_limit?: number;
/** 创建时间 */
created_at: string;
/** 更新时间 */
updated_at: string;
/** 应用名称 */
app_name: string;
/** 重定向 URI */
redirect_uri: string;
}
/**
* 创建支付链接请求参数
*/
export interface CreatePaymentLinkRequest {
/** 金额 */
amount: number | string;
/** 商品名称 */
product_name: string;
/** 备注(可选) */
remark?: string;
/** 总支付次数限制(可选,最小值为1) */
total_limit?: number;
/** 单用户支付次数限制(可选,最小值为1) */
user_limit?: number;
}
/**
* 更新支付链接请求参数
*/
export interface UpdatePaymentLinkRequest {
/** 金额 */
amount: number | string;
/** 商品名称 */
product_name: string;
/** 备注(可选) */
remark?: string;
/** 总支付次数限制(可选,最小值为1) */
total_limit?: number;
/** 单用户支付次数限制(可选,最小值为1) */
user_limit?: number;
}
/**
* 通过支付链接支付请求参数
*/
export interface PayByLinkRequest {
/** 支付链接 Token */
token: string;
/** 支付密码(6位数字) */
pay_key: string;
/** 备注(可选,最大100字符) */
remark?: string;
}
/**
* 获取支付链接信息响应
* 包含支付链接信息和商户信息
*/
export interface GetPaymentLinkInfoResponse {
/** 支付链接信息 */
payment_link: PaymentLink;
/** 商户信息 */
merchant: {
/** 应用名称 */
app_name: string;
/** 跳转URI */
redirect_uri: string;
};
/** 用户积分配置信息 */
user_pay_config: {
/** 配置ID */
id: string;
/** 积分等级 */
level: number;
/** 最低分数 */
min_score: number;
/** 最高分数 */
max_score: number | null;
/** 每日限额 */
daily_limit: number | null;
/** 手续费率 */
fee_rate: string;
/** 创建时间 */
created_at: string;
/** 更新时间 */
updated_at: string;
};
}
/**
* 商户查询订单请求参数
*/
export interface QueryMerchantOrderRequest {
/** 商户订单号(可选) */
out_trade_no?: string;
/** 平台订单号 */
trade_no: string;
/** 客户端 ID */
pid: string;
/** 客户端密钥 */
key: string;
}
/**
* 商户查询订单响应
*/
export interface QueryMerchantOrderResponse {
/** 状态码(1表示成功,-1表示失败) */
code: number;
/** 响应消息 */
msg: string;
/** 平台订单号 */
trade_no: string;
/** 商户订单号 */
out_trade_no: string;
/** 支付类型 */
type: string;
/** 客户端 ID */
pid: string;
/** 订单创建时间 */
addtime: string;
/** 订单完成时间 */
endtime: string;
/** 订单名称 */
name: string;
/** 订单金额 */
money: string;
/** 订单状态(1表示已支付,0表示未支付) */
status: number;
}
/**
* 商户退款请求参数
*/
export interface RefundMerchantOrderRequest {
/** 客户端 ID */
pid: string;
/** 客户端密钥 */
key: string;
/** 商户订单号(可选) */
out_trade_no?: string;
/** 平台订单号 */
trade_no: string;
/** 退款金额 */
money: number | string;
}
/**
* 商户退款响应
*/
export interface RefundMerchantOrderResponse {
/** 状态码(1表示成功,-1表示失败) */
code: number;
/** 响应消息 */
msg: string;
}
/**
* 商户分发请求参数
*/
export interface MerchantDistributeRequest {
/** 接收用户 ID (必填) */
user_id: number;
/** 接收用户名,用于验证 (必填) */
username: string;
/** 分发金额 (必填) */
amount: number | string;
/** 商户订单号 (可选,最大64字符) */
out_trade_no?: string;
/** 备注 (可选,最大100字符) */
remark?: string;
}
/**
* 商户分发响应
*/
export interface MerchantDistributeResponse {
/** 平台订单号 */
trade_no: string;
/** 商户订单号 */
out_trade_no: string;
}
@@ -0,0 +1,14 @@
export { RedEnvelopeService } from './redenvelope.service';
export type {
RedEnvelopeType,
RedEnvelopeStatus,
RedEnvelope,
RedEnvelopeClaim,
CreateRedEnvelopeRequest,
CreateRedEnvelopeResponse,
ClaimRedEnvelopeRequest,
ClaimRedEnvelopeResponse,
RedEnvelopeDetailResponse,
RedEnvelopeListParams,
RedEnvelopeListResponse,
} from './types';
@@ -0,0 +1,108 @@
import { BaseService } from '../core/base.service';
import type { UploadImageResponse } from '../upload/types';
import type {
CreateRedEnvelopeRequest,
CreateRedEnvelopeResponse,
ClaimRedEnvelopeRequest,
ClaimRedEnvelopeResponse,
RedEnvelopeDetailResponse,
RedEnvelopeListParams,
RedEnvelopeListResponse,
} from './types';
/**
* 红包服务
* 处理红包创建、领取、查询相关的 API 请求
*/
export class RedEnvelopeService extends BaseService {
protected static readonly basePath = '/api/v1/redenvelope';
/**
* 创建红包
* @param data - 创建红包请求参数
* @returns 红包信息(包含分享链接)
* @throws {UnauthorizedError} 当未登录时
* @throws {ValidationError} 当参数验证失败时
* @throws {ApiErrorBase} 当余额不足或支付密码错误时
*
* @example
* ```typescript
* const result = await RedEnvelopeService.create({
* type: 'random',
* total_amount: 100,
* total_count: 10,
* greeting: '恭喜发财',
* pay_key: '123456'
* });
* console.log('分享链接:', result.link);
* ```
*/
static async create(data: CreateRedEnvelopeRequest): Promise<CreateRedEnvelopeResponse> {
return this.post<CreateRedEnvelopeResponse>('/create', data);
}
/**
* 领取红包
* @param data - 领取红包请求参数
* @returns 领取结果(包含领取金额)
* @throws {UnauthorizedError} 当未登录时
* @throws {NotFoundError} 当红包不存在时
* @throws {ApiErrorBase} 当红包已领完、已过期或已领取过时
*
* @example
* ```typescript
* const result = await RedEnvelopeService.claim({ id: '123456' });
* console.log('领取金额:', result.amount);
* ```
*/
static async claim(data: ClaimRedEnvelopeRequest): Promise<ClaimRedEnvelopeResponse> {
return this.post<ClaimRedEnvelopeResponse>('/claim', data);
}
/**
* 获取红包详情
* @param id - 红包ID
* @returns 红包详情(包含领取记录)
* @throws {NotFoundError} 当红包不存在时
*
* @example
* ```typescript
* const detail = await RedEnvelopeService.getDetail('123456');
* console.log('红包状态:', detail.red_envelope.status);
* console.log('已领取人数:', detail.claims.length);
* ```
*/
static async getDetail(id: string): Promise<RedEnvelopeDetailResponse> {
return this.get<RedEnvelopeDetailResponse>(`/${ id }`);
}
/**
* 获取红包列表
* @param params - 查询参数
* @returns 红包列表
* @throws {UnauthorizedError} 当未登录时
*
* @example
* ```typescript
* const result = await RedEnvelopeService.getList({
* page: 1,
* page_size: 20,
* type: 'sent'
* });
* ```
*/
static async getList(params: RedEnvelopeListParams): Promise<RedEnvelopeListResponse> {
return this.post<RedEnvelopeListResponse>('/list', params);
}
/**
* 获取用户历史红包封面
* @param type - 封面类型 (cover: 背景封面, heterotypic: 异形装饰)
* @returns 历史封面列表
*/
static async listCovers(
type: 'cover' | 'heterotypic'
): Promise<UploadImageResponse[]> {
return this.get<UploadImageResponse[]>('/covers', { type });
}
}
+154
View File
@@ -0,0 +1,154 @@
/**
* 红包类型
* - fixed: 固定金额,每个红包金额相同
* - random: 拼手气,随机分配金额
*/
export type RedEnvelopeType = 'fixed' | 'random';
/**
* 红包状态
* - active: 进行中,可领取
* - finished: 已领完
* - expired: 已过期
*/
export type RedEnvelopeStatus = 'active' | 'finished' | 'expired';
/**
* 红包信息
*/
export interface RedEnvelope {
/** 红包 ID */
id: string;
/** 创建者用户 ID */
creator_id: string;
/** 创建者用户名 */
creator_username: string;
/** 创建者头像 URL */
creator_avatar_url?: string;
/** 红包类型 */
type: RedEnvelopeType;
/** 总金额 */
total_amount: string;
/** 剩余金额 */
remaining_amount: string;
/** 红包总个数 */
total_count: number;
/** 剩余个数 */
remaining_count: number;
/** 祝福语 */
greeting: string;
/** 红包状态 */
status: RedEnvelopeStatus;
/** 封面上传记录 ID */
cover_upload_id?: string;
/** 装饰上传记录 ID */
heterotypic_upload_id?: string;
/** 过期时间 */
expires_at: string;
/** 创建时间 */
created_at: string;
}
/**
* 红包领取记录
*/
export interface RedEnvelopeClaim {
/** 记录 ID (作为字符串以避免 JS 精度问题) */
id: string;
/** 红包 ID (作为字符串以避免 JS 精度问题) */
red_envelope_id: string;
/** 领取者用户 ID (作为字符串以避免 JS 精度问题) */
user_id: string;
/** 领取者用户名 */
username: string;
/** 领取者头像 URL */
avatar_url?: string;
/** 领取金额 */
amount: string;
/** 领取时间 */
claimed_at: string;
}
/**
* 创建红包请求参数
*/
export interface CreateRedEnvelopeRequest {
/** 红包类型(fixed: 固定金额, random: 拼手气) */
type: RedEnvelopeType;
/** 总金额(必须大于0,最多2位小数) */
total_amount: number;
/** 红包个数(必须大于0) */
total_count: number;
/** 祝福语(可选,最大100字符) */
greeting?: string;
/** 支付密码(6-10位) */
pay_key: string;
/** 封面上传记录 ID */
cover_upload_id?: string;
/** 异形装饰上传记录 ID */
heterotypic_upload_id?: string;
}
/**
* 创建红包响应
*/
export interface CreateRedEnvelopeResponse {
/** 红包 ID (作为字符串以避免 JS 精度问题) */
id: string;
}
/**
* 领取红包请求参数
*/
export interface ClaimRedEnvelopeRequest {
/** 红包 ID */
id: string;
}
/**
* 领取红包响应
*/
export interface ClaimRedEnvelopeResponse {
/** 领取到的金额 */
amount: string;
/** 红包信息 */
red_envelope: RedEnvelope;
}
/**
* 获取红包详情响应
*/
export interface RedEnvelopeDetailResponse {
/** 红包信息 */
red_envelope: RedEnvelope;
/** 领取记录列表 */
claims: RedEnvelopeClaim[];
/** 当前用户的领取记录(如果已领取) */
user_claimed?: RedEnvelopeClaim;
}
/**
* 红包列表查询参数
*/
export interface RedEnvelopeListParams {
/** 页码,从 1 开始 */
page: number;
/** 每页数量,1-100 */
page_size: number;
/** 查询类型(sent: 发出的, received: 收到的) */
type?: 'sent' | 'received';
}
/**
* 红包列表响应
*/
export interface RedEnvelopeListResponse {
/** 总记录数 */
total: number;
/** 当前页码 */
page: number;
/** 每页数量 */
page_size: number;
/** 红包列表 */
red_envelopes: RedEnvelope[];
}
@@ -0,0 +1,32 @@
/**
* 交易服务模块
*
* @description
* 提供交易相关的功能,包括:
* - 查询交易记录列表(分页)
*
* @example
* ```typescript
* import { TransactionService } from '@/lib/services';
*
* // 查询交易记录
* const result = await TransactionService.getTransactions({
* page: 1,
* page_size: 20,
* types: ['receive'],
* statuses: ['success'],
* payee_transfer_status: 'pending',
* });
* ```
*/
export { TransactionService } from './transaction.service';
export { DEFAULT_ORDER_TYPES } from './types';
export type {
Order,
OrderType,
OrderStatus,
TransferStatus,
TransactionQueryParams,
TransactionListResponse,
} from './types';
@@ -0,0 +1,32 @@
import { BaseService } from '../core/base.service';
import type { TransactionQueryParams, TransactionListResponse } from './types';
/**
* 交易服务
* 处理订单和交易记录相关的 API 请求
*/
export class TransactionService extends BaseService {
protected static readonly basePath = '/api/v1/order';
/**
* 获取交易记录列表(分页)
* @param params - 查询参数
* @returns 交易记录列表
* @throws {UnauthorizedError} 当未登录时
* @throws {ValidationError} 当参数验证失败时
*
* @example
* ```typescript
* const result = await TransactionService.getTransactions({
* page: 1,
* page_size: 20,
* types: ['receive'],
* statuses: ['success'],
* payee_transfer_status: 'pending'
* });
* ```
*/
static async getTransactions(params: TransactionQueryParams): Promise<TransactionListResponse> {
return this.post<TransactionListResponse>('/transactions', params as unknown as Record<string, unknown>);
}
}
+134
View File
@@ -0,0 +1,134 @@
/**
* 订单类型
*/
export type OrderType = 'receive' | 'payment' | 'community' | 'online' | 'test' | 'distribute' | 'red_envelope_send' | 'red_envelope_receive' | 'red_envelope_refund';
/**
* 前端默认展示的订单类型
* 显式排除已移除的积分转移类型
*/
export const DEFAULT_ORDER_TYPES: OrderType[] = [
'receive',
'payment',
'community',
'online',
'test',
'distribute',
'red_envelope_send',
'red_envelope_receive',
'red_envelope_refund',
];
/**
* 订单状态
*/
export type OrderStatus = 'success' | 'pending' | 'failed' | 'expired' | 'disputing' | 'refund' | 'refused';
/**
* 到账状态
*/
export type TransferStatus = 'pending' | 'completed';
/**
* 订单信息
*/
export interface Order {
/** 订单 ID */
id: string;
/** 订单号(18位字符串) */
order_no: string;
/** 订单名称 */
order_name: string;
/** 商户订单号 */
merchant_order_no: string;
/** 付款方用户ID */
payer_user_id: string;
/** 收款方用户ID */
payee_user_id: string;
/** 付款方账户 */
payer_username: string;
/** 付款方头像 */
payer_avatar_url?: string;
/** 收款方账户 */
payee_username: string;
/** 收款方头像 */
payee_avatar_url?: string;
/** 交易金额(decimal字符串) */
amount: string;
/** 订单状态 */
status: OrderStatus;
/** 订单类型 */
type: OrderType;
/** 备注 */
remark: string;
/** 客户端ID */
client_id: string;
/** 交易时间 */
trade_time: string;
/** 过期时间 */
expires_at: string;
/** 创建时间 */
created_at: string;
/** 更新时间 */
updated_at: string;
/** 应用名称(可选) */
app_name?: string;
/** 应用主页 URL(可选) */
app_homepage_url?: string;
/** 应用描述(可选) */
app_description?: string;
/** 重定向 URI(可选) */
redirect_uri?: string;
/** 关联的争议 ID(可选) */
dispute_id?: string;
/** 支付类型 */
payment_type: string;
/** 是否到账 */
payee_transfer_status: TransferStatus;
/** 到账时间 */
payee_transfer_at: string;
}
/**
* 交易查询参数
*/
export interface TransactionQueryParams {
/** 页码,从 1 开始 */
page: number;
/** 每页数量,1-100 */
page_size: number;
/** 订单类型列表(可选) */
types?: OrderType[];
/** 订单状态列表(可选) */
statuses?: OrderStatus[];
/** 收款方结算状态(可选) */
payee_transfer_status?: TransferStatus;
/** 客户端 ID(可选) */
client_id?: string;
/** 开始时间(可选) */
startTime?: string;
/** 结束时间(可选) */
endTime?: string;
/** 订单 ID(可选) */
id?: string;
/** 订单名称,支持前缀模糊查询(可选) */
order_name?: string;
/** 付款方账户,支持前缀模糊查询(可选) */
payer_username?: string;
/** 收款方账户,支持前缀模糊查询(可选) */
payee_username?: string;
}
/**
* 交易列表响应
*/
export interface TransactionListResponse {
/** 总记录数 */
total: number;
/** 当前页码 */
page: number;
/** 每页数量 */
page_size: number;
/** 订单列表 */
orders: Order[];
}
+2
View File
@@ -0,0 +1,2 @@
export { UploadService } from './upload.service';
export type { UploadImageResponse } from './types';
+7
View File
@@ -0,0 +1,7 @@
/**
* 上传图片响应
*/
export interface UploadImageResponse {
/** 上传记录 ID */
id: string;
}
@@ -0,0 +1,88 @@
import { BaseService } from '../core/base.service';
import type { UploadImageResponse } from './types';
import type { InternalAxiosRequestConfig } from 'axios';
/**
* 根据上传ID构造文件访问URL
* @param id - 上传记录ID
* @returns 文件访问URL
*/
export function getFileUrl(id: string | number | null | undefined): string | null {
if (!id) return null;
return `/f/${id}`;
}
/**
* 上传服务
* 处理文件上传相关的 API 请求
*/
export class UploadService extends BaseService {
protected static readonly basePath = '/api/v1/upload';
/**
* 上传红包封面图片
* @param file - 图片文件
* @param type - 封面类型 (cover: 背景封面, heterotypic: 异形装饰)
* @returns 上传后的图片URL
* @throws {ValidationError} 当文件格式或大小不符合要求时
* @throws {UnauthorizedError} 当未登录时
*
* @example
* ```typescript
* const file = e.target.files[0];
* const result = await UploadService.uploadRedEnvelopeCover(file, 'cover');
* console.log('图片URL:', result.url);
* ```
*/
static async uploadRedEnvelopeCover(
file: File,
type: 'cover' | 'heterotypic'
): Promise<UploadImageResponse> {
// 验证文件类型
const allowedTypes = ['image/jpeg', 'image/png', 'image/jpg', 'image/webp'];
if (!allowedTypes.includes(file.type)) {
throw new Error('只支持 JPG、PNG、WEBP 格式的图片');
}
// 验证文件大小 (最大 2MB)
const maxSize = 2 * 1024 * 1024;
if (file.size > maxSize) {
throw new Error('图片大小不能超过 2MB');
}
// 创建 FormData
const formData = new FormData();
formData.append('file', file);
formData.append('type', type);
return this.post<UploadImageResponse>('/redenvelope/cover', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
} as InternalAxiosRequestConfig);
}
/**
* 将 base64 图片转换为 Blob 并上传
* @param base64 - base64 编码的图片
* @param type - 封面类型
* @param filename - 文件名
* @returns 上传后的图片URL
*/
static async uploadBase64Image(
base64: string,
type: 'cover' | 'heterotypic',
filename: string = 'image.png'
): Promise<UploadImageResponse> {
// 将 base64 转换为 Blob
const response = await fetch(base64);
const blob = await response.blob();
// 创建 File 对象,确保正确的 MIME 类型
const mimeType = base64.match(/data:([^;]+);/)?.[1] || 'image/png';
const file = new File([blob], filename, { type: mimeType });
// 上传文件
return this.uploadRedEnvelopeCover(file, type);
}
}
+18
View File
@@ -0,0 +1,18 @@
/**
* 用户服务模块
*
* @description
* 提供用户个人设置相关的功能,包括:
* - 更新支付密钥
*
* @example
* ```typescript
* import { UserService } from '@/lib/services';
*
* // 更新支付密钥
* await UserService.updatePayKey('123456');
* ```
*/
export { UserService } from './user.service';
export type { UpdatePayKeyRequest } from './types';
+8
View File
@@ -0,0 +1,8 @@
/**
* 更新支付密钥请求
*/
export interface UpdatePayKeyRequest {
/** 新的支付密钥(6位数字) */
pay_key: string;
}
@@ -0,0 +1,30 @@
import { BaseService } from '../core/base.service';
/**
* 用户服务
* 处理用户个人设置相关的 API 请求
*/
export class UserService extends BaseService {
protected static readonly basePath = '/api/v1/user';
/**
* 更新用户支付密钥
* @param payKey - 新的支付密钥
* @returns void
* @throws {UnauthorizedError} 当用户未登录时
* @throws {ValidationError} 当支付密钥格式无效时
*
* @example
* ```typescript
* await UserService.updatePayKey('123456');
* ```
*
* @remarks
* - 支付密钥必须为6位数字
* - 只能更新当前登录用户的支付密钥
*/
static async updatePayKey(payKey: string): Promise<void> {
return this.put<void>('/pay-key', { pay_key: payKey });
}
}
+10
View File
@@ -0,0 +1,10 @@
import type { ThemeConfig } from "./types"
/**
* 主题系统配置
* 定义主题文件的存储位置和 localStorage 键名
*/
export const THEME_CONFIG: ThemeConfig = {
styleDir: "public/style",
storageKey: "app-theme-id",
}
+129
View File
@@ -0,0 +1,129 @@
"use client"
import React, { createContext, useContext, useEffect, useState, useCallback } from "react"
import { useTheme as useNextTheme } from "next-themes"
import type { Theme } from "./types"
import { getStoredThemeId, setStoredThemeId } from "./storage"
import { getAvailableThemes } from "./parser"
/**
* 主题上下文类型定义
*/
interface ThemeContextValue {
/** 所有可用主题列表 */
themes: Theme[]
/** 当前激活的主题对象 */
currentTheme: Theme | null
/** 当前激活的主题 ID */
currentThemeId: string | null
/** 设置主题的方法 */
setTheme: (themeId: string) => void
/** 是否正在加载主题 */
isLoading: boolean
}
const ThemeContext = createContext<ThemeContextValue | null>(null)
/**
* 自定义主题 Hook
* 必须在 CustomThemeProvider 内部使用
*/
export function useCustomTheme() {
const context = useContext(ThemeContext)
if (!context) {
throw new Error("useCustomTheme must be used within CustomThemeProvider")
}
return context
}
/**
* 自定义主题提供者
* 管理用户选择的界面外观主题(配色方案),独立于明暗模式
*/
export function CustomThemeProvider({ children }: { children: React.ReactNode }) {
const [themes, setThemes] = useState<Theme[]>([])
const [currentThemeId, setCurrentThemeId] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [mounted, setMounted] = useState(false)
const { resolvedTheme } = useNextTheme()
/* 加载所有可用主题 */
useEffect(() => {
async function loadThemes() {
const availableThemes = await getAvailableThemes()
const storedId = getStoredThemeId()
setThemes(availableThemes)
setCurrentThemeId(storedId)
setIsLoading(false)
}
loadThemes()
}, [])
/* 确保组件已挂载,避免水合不一致 */
useEffect(() => {
setMounted(true)
}, [])
/**
* 将主题颜色应用到 DOM
* @param theme - 要应用的主题
* @param isDark - 是否为暗色模式
*/
const applyThemeColors = useCallback((theme: Theme, isDark: boolean) => {
if (typeof window === "undefined") return
const root = document.documentElement
const colors = isDark ? theme.colors.dark : theme.colors.light
/* 应用所有 CSS 变量到根元素 */
Object.entries(colors).forEach(([key, value]) => {
root.style.setProperty(`--${ key }`, value)
})
}, [])
/* 当主题 ID 或模式改变时,重新应用颜色 */
useEffect(() => {
if (!mounted || !currentThemeId || themes.length === 0) return
const theme = themes.find(t => t.id === currentThemeId)
if (!theme) return
/* 使用 next-themes 的 resolvedTheme,它会自动处理系统偏好 */
const isDark = resolvedTheme === "dark"
applyThemeColors(theme, isDark)
}, [currentThemeId, resolvedTheme, themes, mounted, applyThemeColors])
/**
* 设置主题
* @param themeId - 要设置的主题 ID
*/
const setTheme = useCallback((themeId: string) => {
const theme = themes.find(t => t.id === themeId)
if (!theme) return
setCurrentThemeId(themeId)
setStoredThemeId(themeId)
/* 根据当前解析的主题模式计算是否为暗色 */
const isDark = resolvedTheme === "dark"
applyThemeColors(theme, isDark)
}, [themes, resolvedTheme, applyThemeColors])
const currentTheme = themes.find(t => t.id === currentThemeId) || null
return (
<ThemeContext.Provider
value={{
themes,
currentTheme,
currentThemeId,
setTheme,
isLoading
}}
>
{children}
</ThemeContext.Provider>
)
}
+19
View File
@@ -0,0 +1,19 @@
/**
* 主题系统
* 统一导出整个主题系统的所有功能
*/
/* 类型定义 */
export type { Theme, ThemeColors, ThemeConfig } from "./types"
/* 配置 */
export { THEME_CONFIG } from "./config"
/* 服务端工具 */
export { getAvailableThemes } from "./parser"
/* 客户端工具 */
export { getStoredThemeId, setStoredThemeId, removeStoredThemeId } from "./storage"
/* React 上下文和 Hooks */
export { CustomThemeProvider, useCustomTheme } from "./context"
+79
View File
@@ -0,0 +1,79 @@
"use server"
import fs from "fs/promises"
import path from "path"
import type { Theme } from "./types"
import { THEME_CONFIG } from "./config"
/**
* 从 CSS 片段中解析 CSS 变量
* @param cssSection - CSS 代码片段
* @returns CSS 变量键值对映射
*/
function parseCSSVariables(cssSection: string): Record<string, string> {
const variables: Record<string, string> = {}
const regex = /--([a-z0-9-]+):\s*([^;]+);/g
let match
while ((match = regex.exec(cssSection)) !== null) {
variables[match[1]] = match[2].trim()
}
return variables
}
/**
* 获取所有可用主题
* 通过解析 CSS 文件获取主题列表和颜色配置
* @returns 主题列表,按默认主题优先,其余按名称排序
*/
export async function getAvailableThemes(): Promise<Theme[]> {
try {
const styleDir = path.join(process.cwd(), THEME_CONFIG.styleDir)
const files = await fs.readdir(styleDir)
const themes = await Promise.all(
files
.filter((file) => file.endsWith(".css"))
.map(async (file) => {
/* 生成主题名称 */
const name = file === "default.css"
? "Default"
: file
.replace(".css", "")
.split("-")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ")
const content = await fs.readFile(path.join(styleDir, file), "utf-8")
/* 从 :root 解析明亮模式颜色 */
const rootSection = content.match(/:root\s*\{([^}]+)\}/s)?.[1] || ""
const lightColors = parseCSSVariables(rootSection)
/* 从 .dark 解析暗色模式颜色 */
const darkSection = content.match(/\.dark\s*\{([^}]+)\}/s)?.[1] || ""
const darkColors = parseCSSVariables(darkSection)
return {
id: file,
name,
colors: {
light: lightColors,
dark: darkColors,
}
}
})
)
/* 默认主题排在最前面,其余按名称排序 */
return themes.sort((a, b) => {
if (a.id === "default.css") return -1
if (b.id === "default.css") return 1
return a.name.localeCompare(b.name)
})
} catch (error) {
console.error("Failed to parse themes:", error)
return []
}
}
+32
View File
@@ -0,0 +1,32 @@
"use client"
import { THEME_CONFIG } from "./config"
/**
* 客户端主题存储工具
* 用于在 localStorage 中保存和读取用户的主题偏好
*/
/**
* 获取存储的主题 ID
*/
export function getStoredThemeId(): string | null {
if (typeof window === "undefined") return null
return localStorage.getItem(THEME_CONFIG.storageKey)
}
/**
* 保存主题 ID 到本地存储
*/
export function setStoredThemeId(themeId: string): void {
if (typeof window === "undefined") return
localStorage.setItem(THEME_CONFIG.storageKey, themeId)
}
/**
* 移除存储的主题 ID
*/
export function removeStoredThemeId(): void {
if (typeof window === "undefined") return
localStorage.removeItem(THEME_CONFIG.storageKey)
}
+34
View File
@@ -0,0 +1,34 @@
/**
* 主题系统类型定义
*/
/**
* 主题颜色配置
* 包含明亮和暗色两种模式的 CSS 变量映射
*/
export interface ThemeColors {
light: Record<string, string>
dark: Record<string, string>
}
/**
* 主题定义
*/
export interface Theme {
/** 主题唯一标识符(CSS 文件名) */
id: string
/** 主题显示名称 */
name: string
/** 主题颜色配置 */
colors: ThemeColors
}
/**
* 主题系统配置
*/
export interface ThemeConfig {
/** CSS 样式文件所在目录 */
styleDir: string
/** 主题 ID 在 localStorage 中的存储键名 */
storageKey: string
}
+93
View File
@@ -0,0 +1,93 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
export function formatDateTime(dateStr: string | Date) {
try {
const date = typeof dateStr === 'string' ? new Date(dateStr) : dateStr
return new Intl.DateTimeFormat('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false
}).format(date)
} catch {
return String(dateStr)
}
}
/**
* 格式化日期为本地时间字符串(带时区)
* @param date 要格式化的日期
* @returns 格式化后的日期字符串,如 "2024-01-15T00:00:00+08:00"
*/
export function formatLocalDate(date: Date): string {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${ year }-${ month }-${ day }T${ hours }:${ minutes }:${ seconds }+08:00`
}
type Base64Buffer = {
from: (input: string, encoding: 'utf-8') => { toString: (encoding: 'base64') => string }
}
/**
* Base64 编码
* @param value 待编码字符串
* @returns Base64 编码后的字符串
*/
export function encodeBase64(value: string): string {
if (typeof globalThis.btoa === 'function') {
return globalThis.btoa(value)
}
const bufferConstructor = (globalThis as typeof globalThis & { Buffer?: Base64Buffer }).Buffer
if (bufferConstructor) {
return bufferConstructor.from(value, 'utf-8').toString('base64')
}
throw new Error('当前环境不支持 Base64 编码')
}
/**
* 生成交易缓存的唯一键
* @param params 交易查询参数
* @returns 缓存键字符串
*/
export function generateTransactionCacheKey(params: {
types?: string[]
statuses?: string[]
payee_transfer_status?: string
client_id?: string
page?: number
page_size?: number
startTime?: string
endTime?: string
id?: string
order_name?: string
payer_username?: string
payee_username?: string
}): string {
const typesKey = params.types?.length ? params.types.sort().join(',') : 'all'
const statusesKey = params.statuses?.length ? params.statuses.sort().join(',') : 'all'
const transferStatusKey = params.payee_transfer_status || 'all'
const clientIdKey = params.client_id || 'all'
const startTimeKey = params.startTime || 'no-start'
const endTimeKey = params.endTime || 'no-end'
const idKey = params.id || 'no-id'
const orderNameKey = params.order_name || 'no-name'
const payerKey = params.payer_username || 'no-payer'
const payeeKey = params.payee_username || 'no-payee'
return `${ typesKey }_${ statusesKey }_${ transferStatusKey }_${ clientIdKey }_${ params.page }_${ params.page_size }_${ startTimeKey }_${ endTimeKey }_${ idKey }_${ orderNameKey }_${ payerKey }_${ payeeKey }`
}
+61
View File
@@ -0,0 +1,61 @@
import { isCancelError } from '@/lib/services'
import { toast } from 'sonner'
/**
* 错误处理选项
*/
interface HandleContextErrorOptions {
/** 是否显示 toast 提示 */
showToast?: boolean
/** 是否在控制台记录错误 */
logError?: boolean
}
/**
* 统一处理 Context 中的错误
*
* @param error - 捕获的错误对象
* @param defaultMessage - 默认错误消息
* @param options - 错误处理选项
* @returns 标准化的 Error 对象
*
* @example
* ```typescript
* try {
* await fetchData()
* } catch (error) {
* const errorObject = handleContextError(error, '加载数据失败', {
* showToast: true,
* logError: true
* })
* setError(errorObject)
* }
* ```
*/
export function handleContextError(
error: unknown,
defaultMessage: string,
options: HandleContextErrorOptions = {}
): Error {
const { showToast = false, logError = true } = options
// 取消的请求不算错误
if (isCancelError(error)) {
return new Error('请求已取消')
}
const errorMessage = error instanceof Error ? error.message : defaultMessage
const errorObject = error instanceof Error ? error : new Error(defaultMessage)
if (logError) {
console.error(defaultMessage, error)
}
if (showToast) {
toast.error(defaultMessage, {
description: errorMessage
})
}
return errorObject
}
+36
View File
@@ -0,0 +1,36 @@
import * as React from 'react';
function getStrictContext<T>(
name?: string,
): readonly [
({
value,
children,
}: {
value: T;
children?: React.ReactNode;
}) => React.JSX.Element,
() => T,
] {
const Context = React.createContext<T | undefined>(undefined);
const Provider = ({
value,
children,
}: {
value: T;
children?: React.ReactNode;
}) => <Context.Provider value={value}>{children}</Context.Provider>;
const useSafeContext = () => {
const ctx = React.useContext(Context);
if (ctx === undefined) {
throw new Error(`useContext must be used within ${name ?? 'a Provider'}`);
}
return ctx;
};
return [Provider, useSafeContext] as const;
}
export { getStrictContext };
+536
View File
@@ -0,0 +1,536 @@
import PinyinMatch from 'pinyin-match'
type MatchResult = [number, number] | false;
type MatchFunction = (input: string, keys: string) => MatchResult;
// Handle CJS/ESM interop for pinyin-match
const match = ((): MatchFunction | null => {
const p = PinyinMatch as unknown;
if (!p) return null;
// Check for .default.match (common in some ESM bundles)
const withDefault = p as { default?: { match?: MatchFunction } };
if (typeof withDefault.default?.match === 'function') {
return withDefault.default.match;
}
// Check for .match (defined in its typings)
const withMatch = p as { match?: MatchFunction };
if (typeof withMatch.match === 'function') {
return withMatch.match;
}
// Check if it's the function itself
if (typeof p === 'function') {
return p as MatchFunction;
}
return null;
})();
export interface SearchItem {
id: string
title: string
description: string
url: string
category: 'page' | 'feature' | 'setting' | 'admin'
keywords: string[]
icon?: string
matchRange?: [number, number]
}
/**
* 全局搜索数据源
* 包含所有可搜索的页面和功能
*/
export const searchData: SearchItem[] = [
// ==================== 首页 ====================
{
id: 'home',
title: '首页',
description: '返回首页仪表板',
url: '/home',
category: 'page',
keywords: ['home', '主页', '首页', 'dashboard'],
},
{
id: 'home-disputes',
title: '我的争议',
description: '查看和处理我的争议',
url: '/home',
category: 'feature',
keywords: ['dispute', '争议', '纠纷', 'my'],
},
{
id: 'home-pending-disputes',
title: '待处理的争议',
description: '查看待处理的争议',
url: '/home',
category: 'feature',
keywords: ['dispute', '争议', '待处理', 'pending'],
},
// ==================== 商户中心 ====================
{
id: 'merchant',
title: '集市',
description: '管理集市应用及其相关功能',
url: '/merchant',
category: 'page',
keywords: ['merchant', '商户', '集市', '应用', '功能'],
},
{
id: 'merchant-disputes',
title: '处理争议',
description: '您的争议活动',
url: '/merchant',
category: 'feature',
keywords: ['merchant', '服务方', '争议', 'dispute', '处理'],
},
{
id: 'merchant-orders',
title: '查看所有活动',
description: '查看您的所有活动',
url: '/merchant',
category: 'feature',
keywords: ['merchant', '商户', '订单', 'order', '查看'],
},
{
id: 'merchant-create-online',
title: '创建在线流转',
description: '创建在线流转活动',
url: '/merchant/online-paying',
category: 'feature',
keywords: ['merchant', '服务方', '创建', '在线', 'create', 'online'],
},
{
id: 'merchant-create-app',
title: '创建集市应用',
description: '创建新的集市应用',
url: '/merchant',
category: 'feature',
keywords: ['merchant', '服务方', '创建', '应用', 'create', 'app'],
},
{
id: 'merchant-transactions',
title: '查看活动记录',
description: '查看您的活动记录',
url: '/merchant',
category: 'feature',
keywords: ['merchant', '服务方', '交易', 'transaction', '记录'],
},
{
id: 'merchant-app-info',
title: '查看应用信息',
description: '查看集市应用详细信息',
url: '/merchant',
category: 'feature',
keywords: ['merchant', '服务方', '应用', '信息', 'app', 'info'],
},
{
id: 'merchant-app-config',
title: '应用配置',
description: '查看和编辑应用配置',
url: '/merchant',
category: 'feature',
keywords: ['merchant', '服务方', '配置', 'config', 'app'],
},
{
id: 'merchant-edit-app',
title: '编辑应用信息',
description: '编辑集市应用信息',
url: '/merchant',
category: 'feature',
keywords: ['merchant', '服务方', '编辑', 'edit', 'app'],
},
{
id: 'merchant-delete-app',
title: '删除应用',
description: '删除集市应用',
url: '/merchant',
category: 'feature',
keywords: ['merchant', '服务方', '删除', 'delete', 'app'],
},
{
id: 'merchant-client-id',
title: '查看 Client ID',
description: '查看应用 Client ID',
url: '/merchant',
category: 'feature',
keywords: ['merchant', '服务方', 'client', 'id', '查看'],
},
{
id: 'merchant-client-secret',
title: '查看 Client Secret',
description: '查看应用密钥',
url: '/merchant',
category: 'feature',
keywords: ['merchant', '商户', 'client', 'secret', '密钥', '查看'],
},
// ==================== 在线收款 ====================
{
id: 'online-paying',
title: '在线流转',
description: '管理在线流转活动',
url: '/merchant/online-paying',
category: 'page',
keywords: ['online', '在线', '流转', '活动', 'activity', 'link'],
},
{
id: 'online-products',
title: '活动列表',
description: '查看所有在线活动',
url: '/merchant/online-paying',
category: 'feature',
keywords: ['online', '在线', '商品', 'product', 'list', '列表'],
},
{
id: 'online-create',
title: '创建活动',
description: '创建新的在线流转活动',
url: '/merchant/online-paying',
category: 'feature',
keywords: ['online', '在线', '创建', 'create', 'activity'],
},
{
id: 'online-manage',
title: '管理活动',
description: '管理在线流转活动',
url: '/merchant/online-paying',
category: 'feature',
keywords: ['online', '在线', '管理', 'manage', 'activity'],
},
{
id: 'online-delete',
title: '删除活动',
description: '删除在线流转活动',
url: '/merchant/online-paying',
category: 'feature',
keywords: ['online', '在线', '删除', 'delete', 'activity'],
},
{
id: 'online-preview',
title: '实时预览',
description: '预览在线流转页面效果',
url: '/merchant/online-paying',
category: 'feature',
keywords: ['online', '在线', '预览', 'preview', '实时'],
},
// ==================== 余额管理 ====================
{
id: 'balance',
title: '积分',
description: '查看和管理您的积分余额',
url: '/balance',
category: 'page',
keywords: ['balance', '余额', '积分', '充值', '提现', 'wallet'],
},
{
id: 'balance-view',
title: '查看积分余额',
description: '查看您的积分余额',
url: '/balance',
category: 'feature',
keywords: ['balance', '余额', '查看', 'view'],
},
{
id: 'balance-report',
title: '查看积分报告',
description: '查看积分报告和统计',
url: '/balance',
category: 'feature',
keywords: ['balance', '余额', '报告', 'report', '统计'],
},
{
id: 'balance-activity',
title: '查看交易活动',
description: '查看积分交易活动',
url: '/balance',
category: 'feature',
keywords: ['balance', '余额', '交易', '活动', 'activity'],
},
{
id: 'balance-received',
title: '近期积分收益',
description: '查看近期积分收益记录',
url: '/balance',
category: 'feature',
keywords: ['balance', '余额', '收益', 'receive', '近期'],
},
{
id: 'balance-payment',
title: '近期积分消耗',
description: '查看近期积分消耗记录',
url: '/balance',
category: 'feature',
keywords: ['balance', '余额', '消耗', 'payment', '近期'],
},
{
id: 'balance-community',
title: '近期社区积分划转',
description: '查看社区积分划转记录',
url: '/balance',
category: 'feature',
keywords: ['balance', '余额', '社区', 'community', '划转', '近期'],
},
{
id: 'balance-all',
title: '近期所有活动',
description: '查看所有积分活动',
url: '/balance',
category: 'feature',
keywords: ['balance', '积分', '所有', 'all', '活动', '近期'],
},
// ==================== 交易记录 ====================
{
id: 'trade',
title: '活动',
description: '查看所有交易历史记录',
url: '/trade',
category: 'page',
keywords: ['trade', '交易', '活动', '记录', '历史', 'transaction'],
},
{
id: 'trade-received',
title: '积分收益记录',
description: '查看积分收益记录',
url: '/trade',
category: 'feature',
keywords: ['trade', '交易', '收益', 'receive', '记录'],
},
{
id: 'trade-payment',
title: '积分消耗记录',
description: '查看积分消耗记录',
url: '/trade',
category: 'feature',
keywords: ['trade', '交易', '消耗', 'payment', '记录'],
},
{
id: 'trade-community',
title: '社区积分划转记录',
description: '查看社区积分划转记录',
url: '/trade',
category: 'feature',
keywords: ['trade', '交易', '社区', 'community', '积分', '划转', '记录'],
},
{
id: 'trade-all',
title: '所有积分交易记录',
description: '查看所有积分交易记录',
url: '/trade',
category: 'feature',
keywords: ['trade', '交易', '积分', '所有', 'all', '记录'],
},
{
id: 'trade-official-api',
title: '接入官方积分服务',
description: '集成官方积分服务API',
url: '/trade',
category: 'feature',
keywords: ['trade', '交易', '积分', '接入', 'api', '官方', '服务'],
},
{
id: 'trade-custom-form',
title: '在线流转表单',
description: '创建自定义在线流转表单',
url: '/trade',
category: 'feature',
keywords: ['trade', '交易', '创建', '自定义', 'form', '表单'],
},
{
id: 'trade-overview',
title: '数据概览',
description: '查看积分交易数据统计',
url: '/trade',
category: 'feature',
keywords: ['trade', '交易', '积分', '数据', 'data', '概览', 'overview'],
},
// ==================== 文档 ====================
{
id: 'docs-api',
title: '接口文档',
description: '查看 API 接口文档',
url: '/docs/api',
category: 'page',
keywords: ['api', 'docs', '文档', '接口', 'documentation'],
},
{
id: 'docs-official-api',
title: '官方服务接口',
description: '官方服务接口文档',
url: '/docs/api',
category: 'feature',
keywords: ['api', 'docs', '文档', '官方', '支付', 'payment'],
},
{
id: 'docs-epay-api',
title: '易支付兼容接口',
description: '易支付兼容接口文档',
url: '/docs/api',
category: 'feature',
keywords: ['api', 'docs', '文档', '易支付', 'epay', '兼容'],
},
{
id: 'docs-how-to-use',
title: '使用文档',
description: '查看使用教程和示例',
url: '/docs/how-to-use',
category: 'page',
keywords: ['docs', '文档', '使用', 'how to', 'tutorial', '教程'],
},
// ==================== 设置 ====================
{
id: 'settings',
title: '设置',
description: '应用设置和偏好',
url: '/settings',
category: 'setting',
keywords: ['settings', '设置', '偏好', 'preferences'],
},
{
id: 'settings-all',
title: '所有设置',
description: '查看所有设置选项',
url: '/settings',
category: 'setting',
keywords: ['settings', '设置', '所有', 'all'],
},
{
id: 'settings-profile',
title: '我的资料',
description: '编辑个人信息和头像',
url: '/settings/profile',
category: 'setting',
keywords: ['profile', '资料', '个人', '我的', '信息', '头像'],
},
{
id: 'settings-profile-level',
title: '会员等级',
description: '查看会员等级和权益',
url: '/settings/profile',
category: 'setting',
keywords: ['profile', '资料', '会员', 'level', '等级'],
},
{
id: 'settings-profile-basic',
title: '基本信息',
description: '编辑基本个人信息',
url: '/settings/profile',
category: 'setting',
keywords: ['profile', '资料', '基本', 'basic', '信息'],
},
{
id: 'settings-appearance',
title: '外观设置',
description: '自定义界面主题和显示',
url: '/settings/appearance',
category: 'setting',
keywords: ['appearance', '外观', '主题', 'theme', 'dark', 'light'],
},
{
id: 'settings-theme-switch',
title: '主题切换',
description: '切换亮色/暗色主题',
url: '/settings/appearance',
category: 'setting',
keywords: ['appearance', '外观', '主题', 'theme', '切换', 'switch'],
},
{
id: 'settings-color-theme',
title: '颜色主题',
description: '选择颜色主题',
url: '/settings/appearance',
category: 'setting',
keywords: ['appearance', '外观', '颜色', 'color', '主题'],
},
{
id: 'settings-notification',
title: '通知设置',
description: '管理通知偏好',
url: '/settings',
category: 'setting',
keywords: ['settings', '设置', '通知', 'notification'],
},
{
id: 'settings-security',
title: '安全设置',
description: '账户安全和隐私设置',
url: '/settings',
category: 'setting',
keywords: ['settings', '设置', '安全', 'security', '隐私'],
},
// ==================== 管理员 ====================
{
id: 'admin-system',
title: '系统配置',
description: '系统配置和管理',
url: '/admin/system',
category: 'admin',
keywords: ['admin', '管理', '系统', '配置', 'system'],
},
{
id: 'admin-user-pay',
title: '积分配置',
description: '管理用户支付等级和配置',
url: '/admin/user_pay',
category: 'admin',
keywords: ['admin', '管理', '积分', '配置', 'payment', 'config'],
},
]
/**
* 搜索功能
* @param query 搜索关键词
* @param isAdmin 是否为管理员
* @returns 匹配的搜索结果
*/
export function searchItems(query: string, isAdmin: boolean = false): SearchItem[] {
const trimmedQuery = query.trim()
// 非管理员不能搜索 admin 类别项
const filteredData = isAdmin
? searchData
: searchData.filter(item => item.category !== 'admin')
if (!trimmedQuery) {
return filteredData
}
return filteredData.map(item => {
// 优先匹配标题
const titleMatch = typeof match === 'function' ? match(item.title, trimmedQuery) : null
if (titleMatch) {
return { ...item, matchRange: titleMatch as [number, number] }
}
// 匹配描述
if (typeof match === 'function' && match(item.description, trimmedQuery)) {
return item
}
// 匹配关键词
if (item.keywords.some(keyword => typeof match === 'function' && match(keyword, trimmedQuery))) {
return item
}
return null
}).filter((item): item is SearchItem => item !== null)
.sort((a, b) => {
// 标题匹配优先
if (a.matchRange && !b.matchRange) return -1
if (!a.matchRange && b.matchRange) return 1
// 如果都是标题匹配,按匹配位置排序
if (a.matchRange && b.matchRange) {
return a.matchRange[0] - b.matchRange[0]
}
return 0
})
}