docs(mobile-upgrade): 添加完整技术文档
新增以下文档: - ARCHITECTURE.md: Clean Architecture 分层架构详解 - API.md: API 端点和数据类型定义 - DEVELOPMENT.md: 开发环境配置和规范指南 - TESTING.md: 单元测试、集成测试、E2E测试方案 - DEPLOYMENT.md: Docker、PM2、Nginx 部署配置 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
6db948099c
commit
08485d361f
|
|
@ -0,0 +1,388 @@
|
||||||
|
# Mobile Upgrade Admin - API 文档
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
本文档描述 Mobile Upgrade Admin 前端与 Admin Service 后端的 API 交互。
|
||||||
|
|
||||||
|
## 基础配置
|
||||||
|
|
||||||
|
### 环境变量
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env.local
|
||||||
|
NEXT_PUBLIC_API_URL=http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
### API 客户端配置
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/infrastructure/http/api-client.ts
|
||||||
|
export const apiClient = axios.create({
|
||||||
|
baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000',
|
||||||
|
timeout: 30000,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## API 端点
|
||||||
|
|
||||||
|
### 版本管理 API
|
||||||
|
|
||||||
|
Base URL: `/api/v1/versions`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1. 获取版本列表
|
||||||
|
|
||||||
|
**端点**: `GET /api/v1/versions`
|
||||||
|
|
||||||
|
**查询参数**:
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| platform | string | 否 | 平台筛选:`android` 或 `ios` |
|
||||||
|
| includeDisabled | boolean | 否 | 是否包含已禁用版本,默认 `true` |
|
||||||
|
|
||||||
|
**请求示例**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 获取所有版本
|
||||||
|
const response = await apiClient.get('/api/v1/versions')
|
||||||
|
|
||||||
|
// 获取 Android 版本
|
||||||
|
const response = await apiClient.get('/api/v1/versions?platform=android')
|
||||||
|
|
||||||
|
// 只获取已启用版本
|
||||||
|
const response = await apiClient.get('/api/v1/versions?includeDisabled=false')
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应示例**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "uuid-1234",
|
||||||
|
"platform": "android",
|
||||||
|
"versionCode": 100,
|
||||||
|
"versionName": "1.0.0",
|
||||||
|
"buildNumber": "100",
|
||||||
|
"downloadUrl": "https://example.com/app-1.0.0.apk",
|
||||||
|
"fileSize": "52428800",
|
||||||
|
"fileSha256": "abc123...",
|
||||||
|
"changelog": "1. 新增功能A\n2. 修复问题B",
|
||||||
|
"isForceUpdate": false,
|
||||||
|
"isEnabled": true,
|
||||||
|
"minOsVersion": "8.0",
|
||||||
|
"releaseDate": "2024-01-01T00:00:00.000Z",
|
||||||
|
"createdAt": "2024-01-01T00:00:00.000Z",
|
||||||
|
"updatedAt": "2024-01-01T00:00:00.000Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 获取单个版本
|
||||||
|
|
||||||
|
**端点**: `GET /api/v1/versions/:id`
|
||||||
|
|
||||||
|
**路径参数**:
|
||||||
|
|
||||||
|
| 参数 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| id | string | 版本 ID (UUID) |
|
||||||
|
|
||||||
|
**请求示例**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const response = await apiClient.get('/api/v1/versions/uuid-1234')
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应**: 单个 `AppVersion` 对象
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. 创建版本
|
||||||
|
|
||||||
|
**端点**: `POST /api/v1/versions`
|
||||||
|
|
||||||
|
**请求体**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface CreateVersionInput {
|
||||||
|
platform: 'android' | 'ios'
|
||||||
|
versionCode: number
|
||||||
|
versionName: string
|
||||||
|
buildNumber: string
|
||||||
|
downloadUrl: string
|
||||||
|
fileSize: string
|
||||||
|
fileSha256: string
|
||||||
|
changelog: string
|
||||||
|
isForceUpdate: boolean
|
||||||
|
minOsVersion?: string
|
||||||
|
releaseDate?: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**请求示例**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const response = await apiClient.post('/api/v1/versions', {
|
||||||
|
platform: 'android',
|
||||||
|
versionCode: 101,
|
||||||
|
versionName: '1.0.1',
|
||||||
|
buildNumber: '101',
|
||||||
|
downloadUrl: 'https://example.com/app-1.0.1.apk',
|
||||||
|
fileSize: '52428800',
|
||||||
|
fileSha256: 'sha256hash...',
|
||||||
|
changelog: '修复已知问题',
|
||||||
|
isForceUpdate: false,
|
||||||
|
minOsVersion: '8.0'
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应**: 创建的 `AppVersion` 对象
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. 更新版本
|
||||||
|
|
||||||
|
**端点**: `PUT /api/v1/versions/:id`
|
||||||
|
|
||||||
|
**请求体**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface UpdateVersionInput {
|
||||||
|
downloadUrl?: string
|
||||||
|
fileSize?: string
|
||||||
|
fileSha256?: string
|
||||||
|
changelog?: string
|
||||||
|
isForceUpdate?: boolean
|
||||||
|
isEnabled?: boolean
|
||||||
|
minOsVersion?: string | null
|
||||||
|
releaseDate?: string | null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**请求示例**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const response = await apiClient.put('/api/v1/versions/uuid-1234', {
|
||||||
|
changelog: '更新的更新日志',
|
||||||
|
isForceUpdate: true
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应**: 更新后的 `AppVersion` 对象
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. 删除版本
|
||||||
|
|
||||||
|
**端点**: `DELETE /api/v1/versions/:id`
|
||||||
|
|
||||||
|
**请求示例**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
await apiClient.delete('/api/v1/versions/uuid-1234')
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应**: 无内容 (204)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. 切换版本状态
|
||||||
|
|
||||||
|
**端点**: `PATCH /api/v1/versions/:id/toggle`
|
||||||
|
|
||||||
|
**请求体**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"isEnabled": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**请求示例**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
await apiClient.patch('/api/v1/versions/uuid-1234/toggle', {
|
||||||
|
isEnabled: false
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应**: 无内容 (204)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. 上传版本文件
|
||||||
|
|
||||||
|
**端点**: `POST /api/v1/versions/upload`
|
||||||
|
|
||||||
|
**请求类型**: `multipart/form-data`
|
||||||
|
|
||||||
|
**表单字段**:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| file | File | 是 | APK 或 IPA 文件 |
|
||||||
|
| platform | string | 是 | `android` 或 `ios` |
|
||||||
|
| versionName | string | 是 | 版本号,如 `1.0.0` |
|
||||||
|
| buildNumber | string | 是 | 构建号,如 `100` |
|
||||||
|
| changelog | string | 否 | 更新日志 |
|
||||||
|
| isForceUpdate | boolean | 否 | 是否强制更新,默认 `false` |
|
||||||
|
| minOsVersion | string | 否 | 最低系统版本 |
|
||||||
|
| releaseDate | string | 否 | 发布日期 (ISO 8601) |
|
||||||
|
|
||||||
|
**请求示例**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
formData.append('platform', 'android')
|
||||||
|
formData.append('versionName', '1.0.0')
|
||||||
|
formData.append('buildNumber', '100')
|
||||||
|
formData.append('changelog', '首次发布')
|
||||||
|
formData.append('isForceUpdate', 'false')
|
||||||
|
|
||||||
|
const response = await apiClient.post('/api/v1/versions/upload', formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
timeout: 300000, // 5分钟超时,适用于大文件
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应**: 创建的 `AppVersion` 对象
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 移动端检查更新 API
|
||||||
|
|
||||||
|
### 检查版本更新
|
||||||
|
|
||||||
|
**端点**: `GET /api/app/version/check`
|
||||||
|
|
||||||
|
> 注意:此端点不使用 `/api/v1` 前缀
|
||||||
|
|
||||||
|
**查询参数**:
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| platform | string | 是 | `android` 或 `ios` |
|
||||||
|
| currentVersion | string | 是 | 当前版本号,如 `1.0.0` |
|
||||||
|
| currentBuild | string | 否 | 当前构建号 |
|
||||||
|
|
||||||
|
**请求示例**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GET /api/app/version/check?platform=android¤tVersion=1.0.0¤tBuild=100
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应示例**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"hasUpdate": true,
|
||||||
|
"isForceUpdate": false,
|
||||||
|
"latestVersion": {
|
||||||
|
"versionName": "1.1.0",
|
||||||
|
"buildNumber": "110",
|
||||||
|
"downloadUrl": "https://example.com/app-1.1.0.apk",
|
||||||
|
"fileSize": "55000000",
|
||||||
|
"changelog": "1. 新功能\n2. 性能优化",
|
||||||
|
"minOsVersion": "8.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 错误处理
|
||||||
|
|
||||||
|
### 错误响应格式
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"statusCode": 400,
|
||||||
|
"message": "错误描述",
|
||||||
|
"error": "Bad Request"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 常见错误码
|
||||||
|
|
||||||
|
| 状态码 | 说明 |
|
||||||
|
|--------|------|
|
||||||
|
| 400 | 请求参数错误 |
|
||||||
|
| 404 | 资源不存在 |
|
||||||
|
| 409 | 资源冲突(如版本已存在) |
|
||||||
|
| 413 | 文件过大 |
|
||||||
|
| 500 | 服务器内部错误 |
|
||||||
|
|
||||||
|
### 前端错误处理
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// API 客户端拦截器
|
||||||
|
apiClient.interceptors.response.use(
|
||||||
|
(response) => response,
|
||||||
|
(error) => {
|
||||||
|
const message = error.response?.data?.message || '请求失败'
|
||||||
|
return Promise.reject(new Error(message))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 组件中使用
|
||||||
|
try {
|
||||||
|
await deleteVersion(id)
|
||||||
|
toast.success('删除成功')
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(err.message || '操作失败')
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 数据类型定义
|
||||||
|
|
||||||
|
### AppVersion
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface AppVersion {
|
||||||
|
id: string // UUID
|
||||||
|
platform: 'android' | 'ios' // 平台
|
||||||
|
versionCode: number // 版本代码(用于比较)
|
||||||
|
versionName: string // 版本名称(显示用)
|
||||||
|
buildNumber: string // 构建号
|
||||||
|
downloadUrl: string // 下载地址
|
||||||
|
fileSize: string // 文件大小(字节)
|
||||||
|
fileSha256: string // 文件 SHA256 校验值
|
||||||
|
changelog: string // 更新日志
|
||||||
|
isForceUpdate: boolean // 是否强制更新
|
||||||
|
isEnabled: boolean // 是否启用
|
||||||
|
minOsVersion: string | null // 最低系统版本要求
|
||||||
|
releaseDate: string | null // 发布日期
|
||||||
|
createdAt: string // 创建时间
|
||||||
|
updatedAt: string // 更新时间
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### VersionListFilter
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface VersionListFilter {
|
||||||
|
platform?: 'android' | 'ios'
|
||||||
|
includeDisabled?: boolean
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 相关文档
|
||||||
|
|
||||||
|
- [架构文档](./ARCHITECTURE.md)
|
||||||
|
- [开发指南](./DEVELOPMENT.md)
|
||||||
|
- [测试指南](./TESTING.md)
|
||||||
|
- [部署指南](./DEPLOYMENT.md)
|
||||||
|
|
@ -0,0 +1,298 @@
|
||||||
|
# Mobile Upgrade Admin - 架构文档
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
Mobile Upgrade Admin 是一个用于管理移动应用版本升级的 Web 管理后台。本项目采用 **Clean Architecture(整洁架构)** 设计模式,实现了关注点分离、可测试性和可维护性。
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
| 分类 | 技术 | 版本 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| 框架 | Next.js | 14.x | React 全栈框架,App Router |
|
||||||
|
| 语言 | TypeScript | 5.x | 类型安全 |
|
||||||
|
| 状态管理 | Zustand | 4.x | 轻量级状态管理 |
|
||||||
|
| HTTP 客户端 | Axios | 1.x | API 请求 |
|
||||||
|
| 样式 | Tailwind CSS | 3.x | 原子化 CSS |
|
||||||
|
| 通知 | react-hot-toast | 2.x | Toast 通知 |
|
||||||
|
|
||||||
|
## 架构设计
|
||||||
|
|
||||||
|
### Clean Architecture 分层
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Presentation Layer │
|
||||||
|
│ (UI Components, Pages) │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ Application Layer │
|
||||||
|
│ (Stores, Hooks, Use Cases) │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ Domain Layer │
|
||||||
|
│ (Entities, Repository Interfaces) │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ Infrastructure Layer │
|
||||||
|
│ (API Client, Repository Implementations) │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── domain/ # 领域层 - 业务核心
|
||||||
|
│ ├── entities/
|
||||||
|
│ │ └── version.ts # 版本实体和类型定义
|
||||||
|
│ ├── repositories/
|
||||||
|
│ │ └── version-repository.ts # Repository 接口定义
|
||||||
|
│ └── index.ts # 统一导出
|
||||||
|
│
|
||||||
|
├── infrastructure/ # 基础设施层 - 外部依赖
|
||||||
|
│ ├── http/
|
||||||
|
│ │ └── api-client.ts # Axios 客户端配置
|
||||||
|
│ ├── repositories/
|
||||||
|
│ │ └── version-repository-impl.ts # Repository 实现
|
||||||
|
│ └── index.ts # 统一导出
|
||||||
|
│
|
||||||
|
├── application/ # 应用层 - 业务逻辑
|
||||||
|
│ ├── stores/
|
||||||
|
│ │ └── version-store.ts # Zustand 状态管理
|
||||||
|
│ ├── hooks/
|
||||||
|
│ │ └── use-versions.ts # React Hooks
|
||||||
|
│ └── index.ts # 统一导出
|
||||||
|
│
|
||||||
|
├── presentation/ # 表示层 - UI 组件
|
||||||
|
│ ├── components/
|
||||||
|
│ │ ├── version-card.tsx # 版本卡片组件
|
||||||
|
│ │ ├── upload-modal.tsx # 上传弹窗组件
|
||||||
|
│ │ └── edit-modal.tsx # 编辑弹窗组件
|
||||||
|
│ └── index.ts # 统一导出
|
||||||
|
│
|
||||||
|
└── app/ # Next.js App Router
|
||||||
|
├── layout.tsx # 根布局
|
||||||
|
├── page.tsx # 首页
|
||||||
|
└── globals.css # 全局样式
|
||||||
|
```
|
||||||
|
|
||||||
|
## 各层职责详解
|
||||||
|
|
||||||
|
### 1. Domain Layer(领域层)
|
||||||
|
|
||||||
|
**职责**:定义业务实体和接口,不依赖任何外部框架。
|
||||||
|
|
||||||
|
#### 实体定义 (`entities/version.ts`)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 平台类型
|
||||||
|
export type Platform = 'android' | 'ios'
|
||||||
|
|
||||||
|
// 应用版本实体
|
||||||
|
export interface AppVersion {
|
||||||
|
id: string
|
||||||
|
platform: Platform
|
||||||
|
versionCode: number
|
||||||
|
versionName: string
|
||||||
|
buildNumber: string
|
||||||
|
downloadUrl: string
|
||||||
|
fileSize: string
|
||||||
|
fileSha256: string
|
||||||
|
changelog: string
|
||||||
|
isForceUpdate: boolean
|
||||||
|
isEnabled: boolean
|
||||||
|
minOsVersion: string | null
|
||||||
|
releaseDate: string | null
|
||||||
|
createdAt: string
|
||||||
|
updatedAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 输入/输出 DTO
|
||||||
|
export interface CreateVersionInput { ... }
|
||||||
|
export interface UpdateVersionInput { ... }
|
||||||
|
export interface UploadVersionInput { ... }
|
||||||
|
export interface VersionListFilter { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Repository 接口 (`repositories/version-repository.ts`)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface IVersionRepository {
|
||||||
|
list(filter?: VersionListFilter): Promise<AppVersion[]>
|
||||||
|
getById(id: string): Promise<AppVersion>
|
||||||
|
create(input: CreateVersionInput): Promise<AppVersion>
|
||||||
|
update(id: string, input: UpdateVersionInput): Promise<AppVersion>
|
||||||
|
delete(id: string): Promise<void>
|
||||||
|
toggle(id: string, isEnabled: boolean): Promise<void>
|
||||||
|
upload(input: UploadVersionInput): Promise<AppVersion>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Infrastructure Layer(基础设施层)
|
||||||
|
|
||||||
|
**职责**:实现与外部系统的交互(API、数据库等)。
|
||||||
|
|
||||||
|
#### API 客户端 (`http/api-client.ts`)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
export const apiClient = axios.create({
|
||||||
|
baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000',
|
||||||
|
timeout: 30000,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 响应拦截器处理错误
|
||||||
|
apiClient.interceptors.response.use(
|
||||||
|
(response) => response,
|
||||||
|
(error) => {
|
||||||
|
const message = error.response?.data?.message || '请求失败'
|
||||||
|
return Promise.reject(new Error(message))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Repository 实现 (`repositories/version-repository-impl.ts`)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export class VersionRepositoryImpl implements IVersionRepository {
|
||||||
|
private client: AxiosInstance
|
||||||
|
|
||||||
|
constructor(client?: AxiosInstance) {
|
||||||
|
this.client = client || apiClient
|
||||||
|
}
|
||||||
|
|
||||||
|
async list(filter?: VersionListFilter): Promise<AppVersion[]> {
|
||||||
|
const response = await this.client.get<AppVersion[]>('/api/v1/versions', { params: filter })
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
async upload(input: UploadVersionInput): Promise<AppVersion> {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', input.file)
|
||||||
|
// ... 其他字段
|
||||||
|
const response = await this.client.post<AppVersion>('/api/v1/versions/upload', formData)
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
// ... 其他方法
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Application Layer(应用层)
|
||||||
|
|
||||||
|
**职责**:协调业务逻辑,管理应用状态。
|
||||||
|
|
||||||
|
#### Zustand Store (`stores/version-store.ts`)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { create } from 'zustand'
|
||||||
|
|
||||||
|
interface VersionState {
|
||||||
|
// 状态
|
||||||
|
versions: AppVersion[]
|
||||||
|
selectedVersion: AppVersion | null
|
||||||
|
isLoading: boolean
|
||||||
|
error: string | null
|
||||||
|
filter: { platform?: Platform; includeDisabled: boolean }
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
fetchVersions: () => Promise<void>
|
||||||
|
createVersion: (input: CreateVersionInput) => Promise<AppVersion>
|
||||||
|
updateVersion: (id: string, input: UpdateVersionInput) => Promise<AppVersion>
|
||||||
|
deleteVersion: (id: string) => Promise<void>
|
||||||
|
toggleVersion: (id: string, isEnabled: boolean) => Promise<void>
|
||||||
|
uploadVersion: (input: UploadVersionInput) => Promise<AppVersion>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useVersionStore = create<VersionState>((set, get) => ({
|
||||||
|
// 实现...
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
|
#### React Hooks (`hooks/use-versions.ts`)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 获取版本列表
|
||||||
|
export function useVersions() {
|
||||||
|
const { versions, isLoading, error, fetchVersions, setFilter } = useVersionStore()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchVersions()
|
||||||
|
}, [filter.platform])
|
||||||
|
|
||||||
|
return { versions, isLoading, error, refetch: fetchVersions, setFilter }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取单个版本
|
||||||
|
export function useVersion(id: string | null) { ... }
|
||||||
|
|
||||||
|
// 版本操作
|
||||||
|
export function useVersionActions() { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Presentation Layer(表示层)
|
||||||
|
|
||||||
|
**职责**:UI 渲染和用户交互。
|
||||||
|
|
||||||
|
#### 组件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
presentation/
|
||||||
|
├── components/
|
||||||
|
│ ├── version-card.tsx # 版本信息卡片
|
||||||
|
│ ├── upload-modal.tsx # 上传新版本弹窗
|
||||||
|
│ └── edit-modal.tsx # 编辑版本弹窗
|
||||||
|
```
|
||||||
|
|
||||||
|
## 数据流
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||||
|
│ User │───▶│ Presentation│───▶│ Application │───▶│Infrastructure│
|
||||||
|
│ Action │ │ Component │ │ Store │ │ Repository │
|
||||||
|
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
|
||||||
|
│ │ │
|
||||||
|
│ │ ▼
|
||||||
|
│ │ ┌─────────────┐
|
||||||
|
│ │ │ Admin API │
|
||||||
|
│ │ └─────────────┘
|
||||||
|
│ │ │
|
||||||
|
│ ◀───────────────────┘
|
||||||
|
│ │
|
||||||
|
◀───────────────────┘
|
||||||
|
│
|
||||||
|
┌─────────────┐
|
||||||
|
│ UI Update │
|
||||||
|
└─────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 依赖关系
|
||||||
|
|
||||||
|
```
|
||||||
|
Presentation ──▶ Application ──▶ Domain ◀── Infrastructure
|
||||||
|
│ │ │ │
|
||||||
|
│ │ │ │
|
||||||
|
▼ ▼ ▼ ▼
|
||||||
|
React Zustand 纯TS接口 Axios
|
||||||
|
Next.js Hooks API实现
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键原则**:
|
||||||
|
- Domain 层不依赖任何外部层
|
||||||
|
- Infrastructure 依赖 Domain(实现接口)
|
||||||
|
- Application 依赖 Domain(使用接口)
|
||||||
|
- Presentation 依赖 Application(使用 Hooks/Store)
|
||||||
|
|
||||||
|
## 设计优势
|
||||||
|
|
||||||
|
1. **可测试性**:每层都可以独立测试,Infrastructure 可以被 Mock
|
||||||
|
2. **可维护性**:关注点分离,修改一层不影响其他层
|
||||||
|
3. **可扩展性**:新增功能只需在相应层添加代码
|
||||||
|
4. **可替换性**:可以轻松替换技术实现(如 Axios → Fetch)
|
||||||
|
|
||||||
|
## 相关文档
|
||||||
|
|
||||||
|
- [API 文档](./API.md)
|
||||||
|
- [开发指南](./DEVELOPMENT.md)
|
||||||
|
- [测试指南](./TESTING.md)
|
||||||
|
- [部署指南](./DEPLOYMENT.md)
|
||||||
|
|
@ -0,0 +1,569 @@
|
||||||
|
# Mobile Upgrade Admin - 部署指南
|
||||||
|
|
||||||
|
## 部署概述
|
||||||
|
|
||||||
|
本文档描述 Mobile Upgrade Admin 前端应用的部署方案,包括本地构建、Docker 容器化和生产环境部署。
|
||||||
|
|
||||||
|
## 部署架构
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Nginx / Caddy │
|
||||||
|
│ (Reverse Proxy) │
|
||||||
|
└─────────────────┬───────────────────────┬───────────────────┘
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
┌─────────────────┐ ┌─────────────────┐
|
||||||
|
│ Mobile Upgrade │ │ Admin Service │
|
||||||
|
│ Frontend (3001)│ │ Backend (3000) │
|
||||||
|
└─────────────────┘ └─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 环境要求
|
||||||
|
|
||||||
|
| 环境 | 要求 |
|
||||||
|
|------|------|
|
||||||
|
| Node.js | >= 18.x |
|
||||||
|
| npm | >= 9.x |
|
||||||
|
| Docker | >= 20.x (可选) |
|
||||||
|
| 内存 | >= 512MB |
|
||||||
|
| 磁盘 | >= 500MB |
|
||||||
|
|
||||||
|
## 环境变量配置
|
||||||
|
|
||||||
|
### 开发环境
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env.local
|
||||||
|
NEXT_PUBLIC_API_URL=http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
### 生产环境
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env.production
|
||||||
|
NEXT_PUBLIC_API_URL=https://api.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
| 变量 | 说明 | 默认值 |
|
||||||
|
|------|------|--------|
|
||||||
|
| NEXT_PUBLIC_API_URL | Admin Service API 地址 | http://localhost:3000 |
|
||||||
|
| PORT | 应用监听端口 | 3000 |
|
||||||
|
|
||||||
|
## 部署方式
|
||||||
|
|
||||||
|
### 方式一:本地构建部署
|
||||||
|
|
||||||
|
#### 1. 构建生产版本
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend/mobile-upgrade
|
||||||
|
|
||||||
|
# 安装依赖
|
||||||
|
npm ci
|
||||||
|
|
||||||
|
# 构建
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
构建产物在 `.next/` 目录。
|
||||||
|
|
||||||
|
#### 2. 启动生产服务
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 使用 Node.js 直接运行
|
||||||
|
npm run start
|
||||||
|
|
||||||
|
# 或指定端口
|
||||||
|
PORT=3001 npm run start
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 使用 PM2 管理
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装 PM2
|
||||||
|
npm install -g pm2
|
||||||
|
|
||||||
|
# 启动应用
|
||||||
|
pm2 start npm --name "mobile-upgrade" -- start
|
||||||
|
|
||||||
|
# 查看状态
|
||||||
|
pm2 status
|
||||||
|
|
||||||
|
# 查看日志
|
||||||
|
pm2 logs mobile-upgrade
|
||||||
|
|
||||||
|
# 重启
|
||||||
|
pm2 restart mobile-upgrade
|
||||||
|
|
||||||
|
# 停止
|
||||||
|
pm2 stop mobile-upgrade
|
||||||
|
```
|
||||||
|
|
||||||
|
PM2 配置文件:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// ecosystem.config.js
|
||||||
|
module.exports = {
|
||||||
|
apps: [{
|
||||||
|
name: 'mobile-upgrade',
|
||||||
|
script: 'npm',
|
||||||
|
args: 'start',
|
||||||
|
cwd: '/path/to/mobile-upgrade',
|
||||||
|
env: {
|
||||||
|
NODE_ENV: 'production',
|
||||||
|
PORT: 3001,
|
||||||
|
},
|
||||||
|
instances: 1,
|
||||||
|
autorestart: true,
|
||||||
|
watch: false,
|
||||||
|
max_memory_restart: '500M',
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方式二:Docker 容器部署
|
||||||
|
|
||||||
|
#### 1. Dockerfile
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
# Dockerfile
|
||||||
|
FROM node:18-alpine AS base
|
||||||
|
|
||||||
|
# 安装依赖阶段
|
||||||
|
FROM base AS deps
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# 构建阶段
|
||||||
|
FROM base AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# 设置环境变量
|
||||||
|
ARG NEXT_PUBLIC_API_URL
|
||||||
|
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
|
||||||
|
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# 运行阶段
|
||||||
|
FROM base AS runner
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV NODE_ENV production
|
||||||
|
|
||||||
|
RUN addgroup --system --gid 1001 nodejs
|
||||||
|
RUN adduser --system --uid 1001 nextjs
|
||||||
|
|
||||||
|
COPY --from=builder /app/public ./public
|
||||||
|
|
||||||
|
# 复制构建产物
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||||
|
|
||||||
|
USER nextjs
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
ENV PORT 3000
|
||||||
|
ENV HOSTNAME "0.0.0.0"
|
||||||
|
|
||||||
|
CMD ["node", "server.js"]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 更新 next.config.js
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// next.config.js
|
||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
output: 'standalone', // 启用独立输出模式
|
||||||
|
// ... 其他配置
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = nextConfig
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 构建 Docker 镜像
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 构建镜像
|
||||||
|
docker build \
|
||||||
|
--build-arg NEXT_PUBLIC_API_URL=https://api.example.com \
|
||||||
|
-t mobile-upgrade:latest \
|
||||||
|
.
|
||||||
|
|
||||||
|
# 运行容器
|
||||||
|
docker run -d \
|
||||||
|
--name mobile-upgrade \
|
||||||
|
-p 3001:3000 \
|
||||||
|
mobile-upgrade:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Docker Compose
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# docker-compose.yml
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
mobile-upgrade:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
args:
|
||||||
|
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://admin-service:3000}
|
||||||
|
image: mobile-upgrade:latest
|
||||||
|
container_name: mobile-upgrade
|
||||||
|
ports:
|
||||||
|
- "3001:3000"
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- rwa-network
|
||||||
|
depends_on:
|
||||||
|
- admin-service
|
||||||
|
|
||||||
|
networks:
|
||||||
|
rwa-network:
|
||||||
|
external: true
|
||||||
|
```
|
||||||
|
|
||||||
|
启动命令:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 启动
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# 查看日志
|
||||||
|
docker-compose logs -f mobile-upgrade
|
||||||
|
|
||||||
|
# 停止
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方式三:静态导出部署
|
||||||
|
|
||||||
|
适用于不需要 SSR 的场景。
|
||||||
|
|
||||||
|
#### 1. 配置静态导出
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// next.config.js
|
||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
output: 'export',
|
||||||
|
trailingSlash: true,
|
||||||
|
images: {
|
||||||
|
unoptimized: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = nextConfig
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 构建静态文件
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
静态文件输出到 `out/` 目录。
|
||||||
|
|
||||||
|
#### 3. 使用 Nginx 部署
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
# nginx.conf
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name upgrade.example.com;
|
||||||
|
root /var/www/mobile-upgrade;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api {
|
||||||
|
proxy_pass http://admin-service:3000;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 反向代理配置
|
||||||
|
|
||||||
|
### Nginx 配置
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
# /etc/nginx/sites-available/mobile-upgrade
|
||||||
|
upstream mobile_upgrade {
|
||||||
|
server 127.0.0.1:3001;
|
||||||
|
}
|
||||||
|
|
||||||
|
upstream admin_service {
|
||||||
|
server 127.0.0.1:3000;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name upgrade.example.com;
|
||||||
|
|
||||||
|
# 前端应用
|
||||||
|
location / {
|
||||||
|
proxy_pass http://mobile_upgrade;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
# API 代理
|
||||||
|
location /api {
|
||||||
|
proxy_pass http://admin_service;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
# 大文件上传支持
|
||||||
|
client_max_body_size 200M;
|
||||||
|
proxy_read_timeout 300s;
|
||||||
|
proxy_connect_timeout 75s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Caddy 配置
|
||||||
|
|
||||||
|
```caddyfile
|
||||||
|
# Caddyfile
|
||||||
|
upgrade.example.com {
|
||||||
|
reverse_proxy /api/* admin-service:3000
|
||||||
|
reverse_proxy /* mobile-upgrade:3001
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 健康检查
|
||||||
|
|
||||||
|
### 应用健康检查
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/app/api/health/route.ts
|
||||||
|
export async function GET() {
|
||||||
|
return Response.json({
|
||||||
|
status: 'healthy',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker 健康检查
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||||
|
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Kubernetes 探针
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /api/health
|
||||||
|
port: 3000
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 30
|
||||||
|
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /api/health
|
||||||
|
port: 3000
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 10
|
||||||
|
```
|
||||||
|
|
||||||
|
## CI/CD 集成
|
||||||
|
|
||||||
|
### GitHub Actions
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# .github/workflows/deploy.yml
|
||||||
|
name: Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'frontend/mobile-upgrade/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 18
|
||||||
|
cache: 'npm'
|
||||||
|
cache-dependency-path: frontend/mobile-upgrade/package-lock.json
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
working-directory: frontend/mobile-upgrade
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
working-directory: frontend/mobile-upgrade
|
||||||
|
run: npm run build
|
||||||
|
env:
|
||||||
|
NEXT_PUBLIC_API_URL: ${{ secrets.API_URL }}
|
||||||
|
|
||||||
|
- name: Build Docker image
|
||||||
|
working-directory: frontend/mobile-upgrade
|
||||||
|
run: |
|
||||||
|
docker build \
|
||||||
|
--build-arg NEXT_PUBLIC_API_URL=${{ secrets.API_URL }} \
|
||||||
|
-t mobile-upgrade:${{ github.sha }} \
|
||||||
|
.
|
||||||
|
|
||||||
|
- name: Push to registry
|
||||||
|
run: |
|
||||||
|
docker tag mobile-upgrade:${{ github.sha }} registry.example.com/mobile-upgrade:${{ github.sha }}
|
||||||
|
docker push registry.example.com/mobile-upgrade:${{ github.sha }}
|
||||||
|
|
||||||
|
- name: Deploy to server
|
||||||
|
uses: appleboy/ssh-action@master
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.SERVER_HOST }}
|
||||||
|
username: ${{ secrets.SERVER_USER }}
|
||||||
|
key: ${{ secrets.SERVER_SSH_KEY }}
|
||||||
|
script: |
|
||||||
|
docker pull registry.example.com/mobile-upgrade:${{ github.sha }}
|
||||||
|
docker stop mobile-upgrade || true
|
||||||
|
docker rm mobile-upgrade || true
|
||||||
|
docker run -d \
|
||||||
|
--name mobile-upgrade \
|
||||||
|
-p 3001:3000 \
|
||||||
|
--restart unless-stopped \
|
||||||
|
registry.example.com/mobile-upgrade:${{ github.sha }}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 监控和日志
|
||||||
|
|
||||||
|
### 日志收集
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Docker 日志
|
||||||
|
docker logs -f mobile-upgrade
|
||||||
|
|
||||||
|
# PM2 日志
|
||||||
|
pm2 logs mobile-upgrade
|
||||||
|
|
||||||
|
# 日志文件位置
|
||||||
|
/var/log/mobile-upgrade/access.log
|
||||||
|
/var/log/mobile-upgrade/error.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### 性能监控
|
||||||
|
|
||||||
|
建议集成:
|
||||||
|
- **Prometheus + Grafana**: 指标监控
|
||||||
|
- **Sentry**: 错误追踪
|
||||||
|
- **Datadog / New Relic**: APM
|
||||||
|
|
||||||
|
### 集成 Sentry
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @sentry/nextjs
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// sentry.client.config.ts
|
||||||
|
import * as Sentry from '@sentry/nextjs'
|
||||||
|
|
||||||
|
Sentry.init({
|
||||||
|
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||||
|
tracesSampleRate: 0.1,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 故障排查
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
| 问题 | 可能原因 | 解决方案 |
|
||||||
|
|------|----------|----------|
|
||||||
|
| 页面空白 | 构建失败 | 检查构建日志 |
|
||||||
|
| API 请求失败 | CORS / 网络 | 检查代理配置 |
|
||||||
|
| 静态资源 404 | 路径配置 | 检查 basePath 配置 |
|
||||||
|
| 内存不足 | 并发过高 | 增加实例或内存 |
|
||||||
|
|
||||||
|
### 调试命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查进程
|
||||||
|
ps aux | grep node
|
||||||
|
|
||||||
|
# 检查端口
|
||||||
|
netstat -tlnp | grep 3001
|
||||||
|
|
||||||
|
# 检查容器
|
||||||
|
docker ps -a
|
||||||
|
docker inspect mobile-upgrade
|
||||||
|
|
||||||
|
# 检查日志
|
||||||
|
docker logs --tail 100 mobile-upgrade
|
||||||
|
```
|
||||||
|
|
||||||
|
## 回滚策略
|
||||||
|
|
||||||
|
### Docker 回滚
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看历史版本
|
||||||
|
docker images mobile-upgrade
|
||||||
|
|
||||||
|
# 回滚到指定版本
|
||||||
|
docker stop mobile-upgrade
|
||||||
|
docker rm mobile-upgrade
|
||||||
|
docker run -d \
|
||||||
|
--name mobile-upgrade \
|
||||||
|
-p 3001:3000 \
|
||||||
|
mobile-upgrade:previous-tag
|
||||||
|
```
|
||||||
|
|
||||||
|
### PM2 回滚
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 保存当前配置
|
||||||
|
pm2 save
|
||||||
|
|
||||||
|
# 回滚
|
||||||
|
pm2 reload ecosystem.config.js --update-env
|
||||||
|
```
|
||||||
|
|
||||||
|
## 安全建议
|
||||||
|
|
||||||
|
1. **环境变量**:不要在代码中硬编码敏感信息
|
||||||
|
2. **HTTPS**:生产环境强制使用 HTTPS
|
||||||
|
3. **CSP**:配置内容安全策略
|
||||||
|
4. **更新依赖**:定期更新依赖修复安全漏洞
|
||||||
|
5. **访问控制**:配置合适的访问权限
|
||||||
|
|
||||||
|
## 相关文档
|
||||||
|
|
||||||
|
- [架构文档](./ARCHITECTURE.md)
|
||||||
|
- [API 文档](./API.md)
|
||||||
|
- [开发指南](./DEVELOPMENT.md)
|
||||||
|
- [测试指南](./TESTING.md)
|
||||||
|
|
@ -0,0 +1,417 @@
|
||||||
|
# Mobile Upgrade Admin - 开发指南
|
||||||
|
|
||||||
|
## 环境要求
|
||||||
|
|
||||||
|
| 工具 | 版本 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| Node.js | >= 18.x | 运行环境 |
|
||||||
|
| npm | >= 9.x | 包管理器 |
|
||||||
|
| Git | >= 2.x | 版本控制 |
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 1. 克隆项目
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://git.gdzx.xyz/hailin/rwadurian.git
|
||||||
|
cd rwadurian/frontend/mobile-upgrade
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 配置环境变量
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 复制环境变量模板
|
||||||
|
cp .env.local.example .env.local
|
||||||
|
|
||||||
|
# 编辑配置
|
||||||
|
# NEXT_PUBLIC_API_URL=http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 启动开发服务器
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
访问 http://localhost:3000 查看应用。
|
||||||
|
|
||||||
|
## 项目脚本
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 开发模式
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# 生产构建
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# 启动生产服务
|
||||||
|
npm run start
|
||||||
|
|
||||||
|
# 代码检查
|
||||||
|
npm run lint
|
||||||
|
|
||||||
|
# 类型检查
|
||||||
|
npm run type-check
|
||||||
|
```
|
||||||
|
|
||||||
|
## 开发规范
|
||||||
|
|
||||||
|
### 目录结构规范
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── domain/ # 领域层:纯 TypeScript,无框架依赖
|
||||||
|
├── infrastructure/ # 基础设施层:外部服务交互
|
||||||
|
├── application/ # 应用层:业务逻辑和状态管理
|
||||||
|
├── presentation/ # 表示层:React 组件
|
||||||
|
└── app/ # Next.js 页面路由
|
||||||
|
```
|
||||||
|
|
||||||
|
### 命名规范
|
||||||
|
|
||||||
|
| 类型 | 规范 | 示例 |
|
||||||
|
|------|------|------|
|
||||||
|
| 文件名 | kebab-case | `version-card.tsx` |
|
||||||
|
| 组件名 | PascalCase | `VersionCard` |
|
||||||
|
| 函数名 | camelCase | `fetchVersions` |
|
||||||
|
| 常量 | UPPER_SNAKE_CASE | `API_BASE_URL` |
|
||||||
|
| 类型/接口 | PascalCase | `AppVersion` |
|
||||||
|
|
||||||
|
### 代码风格
|
||||||
|
|
||||||
|
项目使用 ESLint + Prettier 进行代码规范检查。
|
||||||
|
|
||||||
|
```json
|
||||||
|
// .eslintrc.json
|
||||||
|
{
|
||||||
|
"extends": ["next/core-web-vitals"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 开发流程
|
||||||
|
|
||||||
|
### 1. 新增功能开发流程
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 在 Domain 层定义实体和接口
|
||||||
|
2. 在 Infrastructure 层实现 API 调用
|
||||||
|
3. 在 Application 层添加状态管理逻辑
|
||||||
|
4. 在 Presentation 层创建 UI 组件
|
||||||
|
5. 编写测试用例
|
||||||
|
6. 提交代码
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 示例:添加新的 API 功能
|
||||||
|
|
||||||
|
#### Step 1: Domain 层 - 定义类型
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/domain/entities/version.ts
|
||||||
|
export interface BatchDeleteInput {
|
||||||
|
ids: string[]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 2: Domain 层 - 定义接口
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/domain/repositories/version-repository.ts
|
||||||
|
export interface IVersionRepository {
|
||||||
|
// ... 现有方法
|
||||||
|
batchDelete(input: BatchDeleteInput): Promise<void>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 3: Infrastructure 层 - 实现接口
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/infrastructure/repositories/version-repository-impl.ts
|
||||||
|
export class VersionRepositoryImpl implements IVersionRepository {
|
||||||
|
// ... 现有方法
|
||||||
|
|
||||||
|
async batchDelete(input: BatchDeleteInput): Promise<void> {
|
||||||
|
await this.client.post('/api/v1/versions/batch-delete', input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 4: Application 层 - 添加状态管理
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/application/stores/version-store.ts
|
||||||
|
interface VersionState {
|
||||||
|
// ... 现有状态
|
||||||
|
batchDeleteVersions: (ids: string[]) => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useVersionStore = create<VersionState>((set, get) => ({
|
||||||
|
// ... 现有实现
|
||||||
|
|
||||||
|
batchDeleteVersions: async (ids: string[]) => {
|
||||||
|
set({ isLoading: true, error: null })
|
||||||
|
try {
|
||||||
|
await versionRepository.batchDelete({ ids })
|
||||||
|
const { versions } = get()
|
||||||
|
set({
|
||||||
|
versions: versions.filter((v) => !ids.includes(v.id)),
|
||||||
|
isLoading: false,
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
set({
|
||||||
|
error: err instanceof Error ? err.message : 'Failed to delete',
|
||||||
|
isLoading: false,
|
||||||
|
})
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 5: Application 层 - 添加 Hook
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/application/hooks/use-versions.ts
|
||||||
|
export function useVersionActions() {
|
||||||
|
const { batchDeleteVersions, ... } = useVersionStore()
|
||||||
|
|
||||||
|
return {
|
||||||
|
batchDelete: batchDeleteVersions,
|
||||||
|
// ... 其他方法
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 6: Presentation 层 - 使用
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/presentation/components/batch-actions.tsx
|
||||||
|
export function BatchActions({ selectedIds }: { selectedIds: string[] }) {
|
||||||
|
const { batchDelete } = useVersionActions()
|
||||||
|
|
||||||
|
const handleBatchDelete = async () => {
|
||||||
|
try {
|
||||||
|
await batchDelete(selectedIds)
|
||||||
|
toast.success('批量删除成功')
|
||||||
|
} catch (err) {
|
||||||
|
toast.error('批量删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button onClick={handleBatchDelete}>
|
||||||
|
批量删除 ({selectedIds.length})
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 状态管理
|
||||||
|
|
||||||
|
### Zustand Store 设计
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 状态结构
|
||||||
|
interface VersionState {
|
||||||
|
// 数据状态
|
||||||
|
versions: AppVersion[]
|
||||||
|
selectedVersion: AppVersion | null
|
||||||
|
|
||||||
|
// UI 状态
|
||||||
|
isLoading: boolean
|
||||||
|
error: string | null
|
||||||
|
|
||||||
|
// 筛选状态
|
||||||
|
filter: {
|
||||||
|
platform?: Platform
|
||||||
|
includeDisabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
fetchVersions: () => Promise<void>
|
||||||
|
setFilter: (filter: Partial<VersionState['filter']>) => void
|
||||||
|
clearError: () => void
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 使用 Hooks
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 在组件中使用
|
||||||
|
function VersionList() {
|
||||||
|
const { versions, isLoading, error, refetch, setFilter } = useVersions()
|
||||||
|
const { deleteVersion, toggleVersion } = useVersionActions()
|
||||||
|
|
||||||
|
// 筛选
|
||||||
|
const handleFilterChange = (platform: Platform | 'all') => {
|
||||||
|
setFilter({ platform: platform === 'all' ? undefined : platform })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
const handleDelete = async (id: string) => {
|
||||||
|
await deleteVersion(id)
|
||||||
|
refetch()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (/* JSX */)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 组件开发
|
||||||
|
|
||||||
|
### 组件结构规范
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { useVersions } from '@/application'
|
||||||
|
import { AppVersion } from '@/domain'
|
||||||
|
|
||||||
|
// Props 接口定义
|
||||||
|
interface VersionCardProps {
|
||||||
|
version: AppVersion
|
||||||
|
onEdit: () => void
|
||||||
|
onDelete: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件实现
|
||||||
|
export function VersionCard({ version, onEdit, onDelete }: VersionCardProps) {
|
||||||
|
// Hooks
|
||||||
|
const [isExpanded, setIsExpanded] = useState(false)
|
||||||
|
|
||||||
|
// 事件处理
|
||||||
|
const handleToggle = () => {
|
||||||
|
setIsExpanded(!isExpanded)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
{/* 组件内容 */}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 样式规范
|
||||||
|
|
||||||
|
使用 Tailwind CSS 原子类:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// 推荐
|
||||||
|
<button className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
|
||||||
|
提交
|
||||||
|
</button>
|
||||||
|
|
||||||
|
// 复用样式定义在 globals.css
|
||||||
|
<button className="btn btn-primary">提交</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
全局样式定义:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* src/app/globals.css */
|
||||||
|
@layer components {
|
||||||
|
.btn {
|
||||||
|
@apply px-4 py-2 rounded-lg font-medium transition-colors;
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
@apply bg-blue-600 text-white hover:bg-blue-700;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
@apply bg-white rounded-lg shadow-sm border p-6;
|
||||||
|
}
|
||||||
|
.input {
|
||||||
|
@apply w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 调试技巧
|
||||||
|
|
||||||
|
### 1. React DevTools
|
||||||
|
|
||||||
|
安装浏览器扩展查看组件树和状态。
|
||||||
|
|
||||||
|
### 2. Zustand DevTools
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { devtools } from 'zustand/middleware'
|
||||||
|
|
||||||
|
export const useVersionStore = create<VersionState>()(
|
||||||
|
devtools(
|
||||||
|
(set, get) => ({
|
||||||
|
// store 实现
|
||||||
|
}),
|
||||||
|
{ name: 'version-store' }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. API 请求调试
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 在 api-client.ts 添加请求日志
|
||||||
|
apiClient.interceptors.request.use((config) => {
|
||||||
|
console.log('API Request:', config.method?.toUpperCase(), config.url)
|
||||||
|
return config
|
||||||
|
})
|
||||||
|
|
||||||
|
apiClient.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
console.log('API Response:', response.status, response.config.url)
|
||||||
|
return response
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('API Error:', error.response?.status, error.config.url)
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### 1. 类型错误
|
||||||
|
|
||||||
|
确保导入路径使用别名:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 正确
|
||||||
|
import { AppVersion } from '@/domain'
|
||||||
|
|
||||||
|
// 错误
|
||||||
|
import { AppVersion } from '../../../domain/entities/version'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 环境变量不生效
|
||||||
|
|
||||||
|
Next.js 要求客户端环境变量以 `NEXT_PUBLIC_` 开头:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 正确 - 客户端可访问
|
||||||
|
NEXT_PUBLIC_API_URL=http://localhost:3000
|
||||||
|
|
||||||
|
# 错误 - 仅服务端可访问
|
||||||
|
API_URL=http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 热更新不工作
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 删除 .next 缓存目录
|
||||||
|
rm -rf .next
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## 相关文档
|
||||||
|
|
||||||
|
- [架构文档](./ARCHITECTURE.md)
|
||||||
|
- [API 文档](./API.md)
|
||||||
|
- [测试指南](./TESTING.md)
|
||||||
|
- [部署指南](./DEPLOYMENT.md)
|
||||||
|
|
@ -0,0 +1,706 @@
|
||||||
|
# Mobile Upgrade Admin - 测试指南
|
||||||
|
|
||||||
|
## 测试架构概述
|
||||||
|
|
||||||
|
本项目采用分层测试策略,针对 Clean Architecture 的每一层设计相应的测试方案。
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ E2E Tests │
|
||||||
|
│ (Playwright / Cypress) │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ Integration Tests │
|
||||||
|
│ (React Testing Library) │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ Unit Tests │
|
||||||
|
│ (Jest / Vitest) │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 测试类型
|
||||||
|
|
||||||
|
| 测试类型 | 目标层 | 工具 | 覆盖率目标 |
|
||||||
|
|----------|--------|------|------------|
|
||||||
|
| 单元测试 | Domain, Application | Jest/Vitest | 80% |
|
||||||
|
| 集成测试 | Infrastructure, Presentation | React Testing Library | 70% |
|
||||||
|
| E2E 测试 | 全链路 | Playwright | 关键路径 |
|
||||||
|
|
||||||
|
## 测试环境配置
|
||||||
|
|
||||||
|
### 安装测试依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -D jest @types/jest ts-jest
|
||||||
|
npm install -D @testing-library/react @testing-library/jest-dom
|
||||||
|
npm install -D @testing-library/user-event
|
||||||
|
npm install -D msw # Mock Service Worker
|
||||||
|
npm install -D playwright @playwright/test # E2E 测试
|
||||||
|
```
|
||||||
|
|
||||||
|
### Jest 配置
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// jest.config.js
|
||||||
|
const nextJest = require('next/jest')
|
||||||
|
|
||||||
|
const createJestConfig = nextJest({
|
||||||
|
dir: './',
|
||||||
|
})
|
||||||
|
|
||||||
|
const customJestConfig = {
|
||||||
|
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
|
||||||
|
testEnvironment: 'jest-environment-jsdom',
|
||||||
|
moduleNameMapper: {
|
||||||
|
'^@/(.*)$': '<rootDir>/src/$1',
|
||||||
|
},
|
||||||
|
collectCoverageFrom: [
|
||||||
|
'src/**/*.{ts,tsx}',
|
||||||
|
'!src/**/*.d.ts',
|
||||||
|
'!src/app/**/*',
|
||||||
|
],
|
||||||
|
coverageThreshold: {
|
||||||
|
global: {
|
||||||
|
branches: 70,
|
||||||
|
functions: 70,
|
||||||
|
lines: 70,
|
||||||
|
statements: 70,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = createJestConfig(customJestConfig)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Jest Setup
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// jest.setup.js
|
||||||
|
import '@testing-library/jest-dom'
|
||||||
|
|
||||||
|
// Mock next/navigation
|
||||||
|
jest.mock('next/navigation', () => ({
|
||||||
|
useRouter() {
|
||||||
|
return {
|
||||||
|
push: jest.fn(),
|
||||||
|
replace: jest.fn(),
|
||||||
|
prefetch: jest.fn(),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
usePathname() {
|
||||||
|
return ''
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
|
## 单元测试
|
||||||
|
|
||||||
|
### Domain 层测试
|
||||||
|
|
||||||
|
Domain 层是纯 TypeScript,测试最简单。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/domain/entities/__tests__/version.test.ts
|
||||||
|
import { AppVersion, Platform } from '../version'
|
||||||
|
|
||||||
|
describe('AppVersion Entity', () => {
|
||||||
|
const mockVersion: AppVersion = {
|
||||||
|
id: 'test-id',
|
||||||
|
platform: 'android',
|
||||||
|
versionCode: 100,
|
||||||
|
versionName: '1.0.0',
|
||||||
|
buildNumber: '100',
|
||||||
|
downloadUrl: 'https://example.com/app.apk',
|
||||||
|
fileSize: '52428800',
|
||||||
|
fileSha256: 'abc123',
|
||||||
|
changelog: 'Test changelog',
|
||||||
|
isForceUpdate: false,
|
||||||
|
isEnabled: true,
|
||||||
|
minOsVersion: '8.0',
|
||||||
|
releaseDate: '2024-01-01',
|
||||||
|
createdAt: '2024-01-01T00:00:00Z',
|
||||||
|
updatedAt: '2024-01-01T00:00:00Z',
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should have correct platform type', () => {
|
||||||
|
expect(['android', 'ios']).toContain(mockVersion.platform)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have valid version code', () => {
|
||||||
|
expect(mockVersion.versionCode).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Application 层测试
|
||||||
|
|
||||||
|
测试 Zustand Store 和 Hooks。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/application/stores/__tests__/version-store.test.ts
|
||||||
|
import { act, renderHook } from '@testing-library/react'
|
||||||
|
import { useVersionStore } from '../version-store'
|
||||||
|
|
||||||
|
// Mock repository
|
||||||
|
jest.mock('@/infrastructure', () => ({
|
||||||
|
versionRepository: {
|
||||||
|
list: jest.fn(),
|
||||||
|
getById: jest.fn(),
|
||||||
|
create: jest.fn(),
|
||||||
|
update: jest.fn(),
|
||||||
|
delete: jest.fn(),
|
||||||
|
toggle: jest.fn(),
|
||||||
|
upload: jest.fn(),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
import { versionRepository } from '@/infrastructure'
|
||||||
|
|
||||||
|
const mockVersionRepository = versionRepository as jest.Mocked<typeof versionRepository>
|
||||||
|
|
||||||
|
describe('useVersionStore', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Reset store state
|
||||||
|
useVersionStore.setState({
|
||||||
|
versions: [],
|
||||||
|
selectedVersion: null,
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
filter: { includeDisabled: true },
|
||||||
|
})
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('fetchVersions', () => {
|
||||||
|
it('should fetch versions successfully', async () => {
|
||||||
|
const mockVersions = [
|
||||||
|
{ id: '1', versionName: '1.0.0', platform: 'android' },
|
||||||
|
{ id: '2', versionName: '1.0.1', platform: 'android' },
|
||||||
|
]
|
||||||
|
mockVersionRepository.list.mockResolvedValue(mockVersions as any)
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useVersionStore())
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.fetchVersions()
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.current.versions).toEqual(mockVersions)
|
||||||
|
expect(result.current.isLoading).toBe(false)
|
||||||
|
expect(result.current.error).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle fetch error', async () => {
|
||||||
|
mockVersionRepository.list.mockRejectedValue(new Error('Network error'))
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useVersionStore())
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.fetchVersions()
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.current.versions).toEqual([])
|
||||||
|
expect(result.current.error).toBe('Network error')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('deleteVersion', () => {
|
||||||
|
it('should delete version and update list', async () => {
|
||||||
|
useVersionStore.setState({
|
||||||
|
versions: [
|
||||||
|
{ id: '1', versionName: '1.0.0' },
|
||||||
|
{ id: '2', versionName: '1.0.1' },
|
||||||
|
] as any,
|
||||||
|
})
|
||||||
|
mockVersionRepository.delete.mockResolvedValue()
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useVersionStore())
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.deleteVersion('1')
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.current.versions).toHaveLength(1)
|
||||||
|
expect(result.current.versions[0].id).toBe('2')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('toggleVersion', () => {
|
||||||
|
it('should toggle version enabled status', async () => {
|
||||||
|
useVersionStore.setState({
|
||||||
|
versions: [
|
||||||
|
{ id: '1', versionName: '1.0.0', isEnabled: true },
|
||||||
|
] as any,
|
||||||
|
})
|
||||||
|
mockVersionRepository.toggle.mockResolvedValue()
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useVersionStore())
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.toggleVersion('1', false)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.current.versions[0].isEnabled).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('setFilter', () => {
|
||||||
|
it('should update filter', () => {
|
||||||
|
const { result } = renderHook(() => useVersionStore())
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.setFilter({ platform: 'ios' })
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.current.filter.platform).toBe('ios')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hooks 测试
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/application/hooks/__tests__/use-versions.test.ts
|
||||||
|
import { renderHook, waitFor } from '@testing-library/react'
|
||||||
|
import { useVersions, useVersionActions } from '../use-versions'
|
||||||
|
|
||||||
|
jest.mock('../stores/version-store')
|
||||||
|
|
||||||
|
describe('useVersions', () => {
|
||||||
|
it('should return versions and loading state', async () => {
|
||||||
|
const { result } = renderHook(() => useVersions())
|
||||||
|
|
||||||
|
expect(result.current.versions).toBeDefined()
|
||||||
|
expect(result.current.isLoading).toBeDefined()
|
||||||
|
expect(result.current.error).toBeDefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('useVersionActions', () => {
|
||||||
|
it('should return action functions', () => {
|
||||||
|
const { result } = renderHook(() => useVersionActions())
|
||||||
|
|
||||||
|
expect(result.current.deleteVersion).toBeInstanceOf(Function)
|
||||||
|
expect(result.current.toggleVersion).toBeInstanceOf(Function)
|
||||||
|
expect(result.current.uploadVersion).toBeInstanceOf(Function)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 集成测试
|
||||||
|
|
||||||
|
### Infrastructure 层测试
|
||||||
|
|
||||||
|
使用 MSW (Mock Service Worker) 模拟 API。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/infrastructure/repositories/__tests__/version-repository-impl.test.ts
|
||||||
|
import { rest } from 'msw'
|
||||||
|
import { setupServer } from 'msw/node'
|
||||||
|
import { VersionRepositoryImpl } from '../version-repository-impl'
|
||||||
|
|
||||||
|
const server = setupServer(
|
||||||
|
rest.get('*/api/v1/versions', (req, res, ctx) => {
|
||||||
|
return res(
|
||||||
|
ctx.json([
|
||||||
|
{ id: '1', versionName: '1.0.0', platform: 'android' },
|
||||||
|
])
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
rest.post('*/api/v1/versions', (req, res, ctx) => {
|
||||||
|
return res(ctx.json({ id: 'new-id', ...req.body }))
|
||||||
|
}),
|
||||||
|
rest.delete('*/api/v1/versions/:id', (req, res, ctx) => {
|
||||||
|
return res(ctx.status(204))
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
beforeAll(() => server.listen())
|
||||||
|
afterEach(() => server.resetHandlers())
|
||||||
|
afterAll(() => server.close())
|
||||||
|
|
||||||
|
describe('VersionRepositoryImpl', () => {
|
||||||
|
const repository = new VersionRepositoryImpl()
|
||||||
|
|
||||||
|
describe('list', () => {
|
||||||
|
it('should fetch versions list', async () => {
|
||||||
|
const versions = await repository.list()
|
||||||
|
expect(versions).toHaveLength(1)
|
||||||
|
expect(versions[0].versionName).toBe('1.0.0')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should apply platform filter', async () => {
|
||||||
|
server.use(
|
||||||
|
rest.get('*/api/v1/versions', (req, res, ctx) => {
|
||||||
|
const platform = req.url.searchParams.get('platform')
|
||||||
|
expect(platform).toBe('ios')
|
||||||
|
return res(ctx.json([]))
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
await repository.list({ platform: 'ios' })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('delete', () => {
|
||||||
|
it('should delete version by id', async () => {
|
||||||
|
await expect(repository.delete('1')).resolves.toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Presentation 层测试
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/presentation/components/__tests__/version-card.test.tsx
|
||||||
|
import { render, screen, fireEvent } from '@testing-library/react'
|
||||||
|
import { VersionCard } from '../version-card'
|
||||||
|
|
||||||
|
const mockVersion = {
|
||||||
|
id: '1',
|
||||||
|
platform: 'android' as const,
|
||||||
|
versionCode: 100,
|
||||||
|
versionName: '1.0.0',
|
||||||
|
buildNumber: '100',
|
||||||
|
downloadUrl: 'https://example.com/app.apk',
|
||||||
|
fileSize: '52428800',
|
||||||
|
fileSha256: 'abc123',
|
||||||
|
changelog: 'Test changelog',
|
||||||
|
isForceUpdate: false,
|
||||||
|
isEnabled: true,
|
||||||
|
minOsVersion: '8.0',
|
||||||
|
releaseDate: '2024-01-01',
|
||||||
|
createdAt: '2024-01-01T00:00:00Z',
|
||||||
|
updatedAt: '2024-01-01T00:00:00Z',
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('VersionCard', () => {
|
||||||
|
const mockOnEdit = jest.fn()
|
||||||
|
const mockOnDelete = jest.fn()
|
||||||
|
const mockOnToggle = jest.fn()
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render version information', () => {
|
||||||
|
render(
|
||||||
|
<VersionCard
|
||||||
|
version={mockVersion}
|
||||||
|
onEdit={mockOnEdit}
|
||||||
|
onDelete={mockOnDelete}
|
||||||
|
onToggle={mockOnToggle}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.getByText('v1.0.0')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('android')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('(Build 100)')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show force update badge when isForceUpdate is true', () => {
|
||||||
|
render(
|
||||||
|
<VersionCard
|
||||||
|
version={{ ...mockVersion, isForceUpdate: true }}
|
||||||
|
onEdit={mockOnEdit}
|
||||||
|
onDelete={mockOnDelete}
|
||||||
|
onToggle={mockOnToggle}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.getByText('强制更新')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should call onEdit when edit button clicked', () => {
|
||||||
|
render(
|
||||||
|
<VersionCard
|
||||||
|
version={mockVersion}
|
||||||
|
onEdit={mockOnEdit}
|
||||||
|
onDelete={mockOnDelete}
|
||||||
|
onToggle={mockOnToggle}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('编辑'))
|
||||||
|
expect(mockOnEdit).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should call onDelete when delete button clicked', () => {
|
||||||
|
render(
|
||||||
|
<VersionCard
|
||||||
|
version={mockVersion}
|
||||||
|
onEdit={mockOnEdit}
|
||||||
|
onDelete={mockOnDelete}
|
||||||
|
onToggle={mockOnToggle}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('删除'))
|
||||||
|
expect(mockOnDelete).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should call onToggle when toggle button clicked', () => {
|
||||||
|
render(
|
||||||
|
<VersionCard
|
||||||
|
version={mockVersion}
|
||||||
|
onEdit={mockOnEdit}
|
||||||
|
onDelete={mockOnDelete}
|
||||||
|
onToggle={mockOnToggle}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('禁用'))
|
||||||
|
expect(mockOnToggle).toHaveBeenCalledWith(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Upload Modal 测试
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/presentation/components/__tests__/upload-modal.test.tsx
|
||||||
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
||||||
|
import userEvent from '@testing-library/user-event'
|
||||||
|
import { UploadModal } from '../upload-modal'
|
||||||
|
|
||||||
|
// Mock hooks
|
||||||
|
jest.mock('@/application', () => ({
|
||||||
|
useVersionActions: () => ({
|
||||||
|
uploadVersion: jest.fn().mockResolvedValue({ id: 'new-id' }),
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('UploadModal', () => {
|
||||||
|
const mockOnClose = jest.fn()
|
||||||
|
const mockOnSuccess = jest.fn()
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render upload form', () => {
|
||||||
|
render(<UploadModal onClose={mockOnClose} onSuccess={mockOnSuccess} />)
|
||||||
|
|
||||||
|
expect(screen.getByText('上传新版本')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('点击选择 APK 或 IPA 文件')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should call onClose when close button clicked', () => {
|
||||||
|
render(<UploadModal onClose={mockOnClose} onSuccess={mockOnSuccess} />)
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('取消'))
|
||||||
|
expect(mockOnClose).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show error when submitting without file', async () => {
|
||||||
|
render(<UploadModal onClose={mockOnClose} onSuccess={mockOnSuccess} />)
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('上传'))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('请选择安装包文件')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should auto-detect platform from file extension', async () => {
|
||||||
|
render(<UploadModal onClose={mockOnClose} onSuccess={mockOnSuccess} />)
|
||||||
|
|
||||||
|
const file = new File([''], 'app.apk', { type: 'application/vnd.android.package-archive' })
|
||||||
|
const input = screen.getByRole('textbox', { hidden: true }) || document.querySelector('input[type="file"]')
|
||||||
|
|
||||||
|
// Simulate file selection
|
||||||
|
if (input) {
|
||||||
|
await userEvent.upload(input, file)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Android should be selected
|
||||||
|
const androidRadio = screen.getByLabelText('Android') as HTMLInputElement
|
||||||
|
expect(androidRadio.checked).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## E2E 测试
|
||||||
|
|
||||||
|
### Playwright 配置
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// playwright.config.ts
|
||||||
|
import { defineConfig, devices } from '@playwright/test'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './e2e',
|
||||||
|
fullyParallel: true,
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
workers: process.env.CI ? 1 : undefined,
|
||||||
|
reporter: 'html',
|
||||||
|
use: {
|
||||||
|
baseURL: 'http://localhost:3000',
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
},
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'chromium',
|
||||||
|
use: { ...devices['Desktop Chrome'] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
webServer: {
|
||||||
|
command: 'npm run dev',
|
||||||
|
url: 'http://localhost:3000',
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### E2E 测试用例
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// e2e/version-management.spec.ts
|
||||||
|
import { test, expect } from '@playwright/test'
|
||||||
|
|
||||||
|
test.describe('Version Management', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto('/')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should display version list', async ({ page }) => {
|
||||||
|
await expect(page.getByText('版本管理')).toBeVisible()
|
||||||
|
await expect(page.getByText('管理移动应用的版本更新')).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should filter by platform', async ({ page }) => {
|
||||||
|
// Click Android filter
|
||||||
|
await page.click('button:has-text("Android")')
|
||||||
|
|
||||||
|
// Verify filter is applied
|
||||||
|
const androidButton = page.locator('button:has-text("Android")')
|
||||||
|
await expect(androidButton).toHaveClass(/bg-green-600/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should open upload modal', async ({ page }) => {
|
||||||
|
await page.click('button:has-text("上传新版本")')
|
||||||
|
|
||||||
|
await expect(page.getByText('上传新版本')).toBeVisible()
|
||||||
|
await expect(page.getByText('点击选择 APK 或 IPA 文件')).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should close upload modal', async ({ page }) => {
|
||||||
|
await page.click('button:has-text("上传新版本")')
|
||||||
|
await page.click('button:has-text("取消")')
|
||||||
|
|
||||||
|
await expect(page.locator('.fixed.inset-0')).not.toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should show validation error on empty submit', async ({ page }) => {
|
||||||
|
await page.click('button:has-text("上传新版本")')
|
||||||
|
await page.click('button:has-text("上传"):not(:has-text("新版本"))')
|
||||||
|
|
||||||
|
await expect(page.getByText('请选择安装包文件')).toBeVisible()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test.describe('Version Card Actions', () => {
|
||||||
|
test('should show confirmation on delete', async ({ page }) => {
|
||||||
|
// Assuming there's at least one version
|
||||||
|
await page.goto('/')
|
||||||
|
|
||||||
|
// Mock confirm dialog
|
||||||
|
page.on('dialog', async dialog => {
|
||||||
|
expect(dialog.message()).toContain('确定要删除')
|
||||||
|
await dialog.dismiss()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Click first delete button
|
||||||
|
const deleteButton = page.locator('button:has-text("删除")').first()
|
||||||
|
if (await deleteButton.isVisible()) {
|
||||||
|
await deleteButton.click()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 手动测试清单
|
||||||
|
|
||||||
|
### 功能测试
|
||||||
|
|
||||||
|
| 功能 | 测试步骤 | 预期结果 |
|
||||||
|
|------|----------|----------|
|
||||||
|
| 版本列表 | 打开首页 | 显示所有版本列表 |
|
||||||
|
| 平台筛选 | 点击 Android/iOS 按钮 | 只显示对应平台版本 |
|
||||||
|
| 上传版本 | 点击上传 → 选择文件 → 填写信息 → 提交 | 版本创建成功,列表更新 |
|
||||||
|
| 编辑版本 | 点击编辑 → 修改信息 → 保存 | 版本信息更新 |
|
||||||
|
| 删除版本 | 点击删除 → 确认 | 版本从列表移除 |
|
||||||
|
| 启用/禁用 | 点击启用/禁用按钮 | 状态切换 |
|
||||||
|
| 下载 | 点击下载按钮 | 文件开始下载 |
|
||||||
|
|
||||||
|
### 边界测试
|
||||||
|
|
||||||
|
| 场景 | 测试步骤 | 预期结果 |
|
||||||
|
|------|----------|----------|
|
||||||
|
| 空列表 | 无版本数据 | 显示"暂无版本数据"提示 |
|
||||||
|
| 网络错误 | 断开网络后操作 | 显示错误提示 |
|
||||||
|
| 大文件上传 | 上传 100MB+ 文件 | 上传成功或显示超时提示 |
|
||||||
|
| 并发操作 | 快速连续点击 | 不出现重复操作 |
|
||||||
|
|
||||||
|
### UI/UX 测试
|
||||||
|
|
||||||
|
| 检查项 | 预期 |
|
||||||
|
|--------|------|
|
||||||
|
| 响应式布局 | 移动端/桌面端正常显示 |
|
||||||
|
| 加载状态 | 操作时显示 loading |
|
||||||
|
| 错误提示 | Toast 提示位置正确 |
|
||||||
|
| 弹窗交互 | 点击遮罩可关闭 |
|
||||||
|
|
||||||
|
## 运行测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 运行所有单元测试
|
||||||
|
npm test
|
||||||
|
|
||||||
|
# 运行测试并生成覆盖率报告
|
||||||
|
npm test -- --coverage
|
||||||
|
|
||||||
|
# 运行特定测试文件
|
||||||
|
npm test -- version-store.test.ts
|
||||||
|
|
||||||
|
# 运行 E2E 测试
|
||||||
|
npx playwright test
|
||||||
|
|
||||||
|
# 运行 E2E 测试(带 UI)
|
||||||
|
npx playwright test --ui
|
||||||
|
|
||||||
|
# 查看 E2E 测试报告
|
||||||
|
npx playwright show-report
|
||||||
|
```
|
||||||
|
|
||||||
|
## CI/CD 集成
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# .github/workflows/test.yml
|
||||||
|
name: Test
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 18
|
||||||
|
- run: npm ci
|
||||||
|
- run: npm run lint
|
||||||
|
- run: npm test -- --coverage
|
||||||
|
- run: npx playwright install --with-deps
|
||||||
|
- run: npx playwright test
|
||||||
|
```
|
||||||
|
|
||||||
|
## 相关文档
|
||||||
|
|
||||||
|
- [架构文档](./ARCHITECTURE.md)
|
||||||
|
- [API 文档](./API.md)
|
||||||
|
- [开发指南](./DEVELOPMENT.md)
|
||||||
|
- [部署指南](./DEPLOYMENT.md)
|
||||||
Loading…
Reference in New Issue