mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-09 00:56:37 +08:00
压缩历史至 95081aff
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 === '请求已被取消'));
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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>);
|
||||
}
|
||||
}
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { UploadService } from './upload.service';
|
||||
export type { UploadImageResponse } from './types';
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { ThemeConfig } from "./types"
|
||||
|
||||
/**
|
||||
* 主题系统配置
|
||||
* 定义主题文件的存储位置和 localStorage 键名
|
||||
*/
|
||||
export const THEME_CONFIG: ThemeConfig = {
|
||||
styleDir: "public/style",
|
||||
storageKey: "app-theme-id",
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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 []
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 }`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user