Files
2026-01-26 08:04:53 +08:00

25 KiB

前端测试技术文档

1. 测试架构概述

1.1 测试金字塔

        /\
       /  \     E2E测试
      /    \    (Playwright, 最少)
     /------\
    /        \   集成测试
   /          \  (RTL, 适中)
  /____________\
 /              \ 单元测试
/________________\ (Vitest, 最多)

1.2 测试层级

测试类型 覆盖范围 比例 运行速度 工具
单元测试 函数、组件、Hooks 70% 快 (< 1s) Vitest
集成测试 页面、路由、API交互 20% 中 (< 5s) React Testing Library
E2E测试 完整用户流程 10% 慢 (< 30s) Playwright

2. 测试工具栈

2.1 单元测试

Vitest

  • 快速的单元测试框架
  • 与Vite深度集成
  • 兼容Jest API
  • 支持TypeScript

安装

npm install -D vitest @vitest/ui jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event

2.2 集成测试

React Testing Library (RTL)

  • 测试React组件
  • 关注用户行为而非实现细节
  • 可访问性优先

2.3 E2E测试

Playwright

  • 跨浏览器E2E测试
  • 快速、可靠
  • 支持并行执行

安装

npm install -D @playwright/test
npx playwright install

2.4 测试覆盖率

Vitest覆盖率

npm run test:coverage

覆盖率目标

  • 语句覆盖率: > 80%
  • 分支覆盖率: > 75%
  • 函数覆盖率: > 80%
  • 行覆盖率: > 80%

3. 测试配置

3.1 Vitest配置

// vitest.config.js
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import path from 'path';

export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: './tests/setup.js',
    css: true,
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      exclude: [
        'node_modules/',
        'tests/',
        '**/*.config.js',
        '**/*.config.jsx',
        '**/dist/',
        '**/build/',
      ],
      thresholds: {
        lines: 80,
        functions: 80,
        branches: 75,
        statements: 80,
      },
    },
    include: ['src/**/*.{test,spec}.{js,jsx}'],
    testTimeout: 10000,
  },
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
});

3.2 测试环境设置

// tests/setup.js
import '@testing-library/jest-dom';
import { cleanup } from '@testing-library/react';
import { afterEach, vi } from 'vitest';

// 每个测试后清理
afterEach(() => {
  cleanup();
});

// 模拟window.matchMedia
Object.defineProperty(window, 'matchMedia', {
  writable: true,
  value: vi.fn().mockImplementation(query => ({
    matches: false,
    media: query,
    onchange: null,
    addListener: vi.fn(),
    removeListener: vi.fn(),
    addEventListener: vi.fn(),
    removeEventListener: vi.fn(),
    dispatchEvent: vi.fn(),
  })),
});

// 模拟localStorage
const localStorageMock = (() => {
  let store = {};
  return {
    getItem: key => store[key] || null,
    setItem: (key, value) => (store[key] = value.toString()),
    removeItem: key => delete store[key],
    clear: () => (store = {}),
  };
})();

Object.defineProperty(window, 'localStorage', {
  value: localStorageMock,
});

3.3 Playwright配置

// playwright.config.js
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests/e2e',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: [
    ['html'],
    ['json', { outputFile: 'test-results/results.json' }],
  ],
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
  ],
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
    timeout: 120000,
  },
});

4. 单元测试

4.1 测试工具函数

// src/utils/format.test.js
import { describe, it, expect } from 'vitest';
import { formatMoney, formatPercentage, formatDate } from './format';

describe('formatMoney', () => {
  it('应该正确格式化金额', () => {
    expect(formatMoney(1234.56)).toBe('1,234.56');
    expect(formatMoney(10000)).toBe('10,000.00');
  });

  it('应该处理小数', () => {
    expect(formatMoney(100.5)).toBe('100.50');
    expect(formatMoney(100.123)).toBe('100.12');
  });

  it('应该处理零', () => {
    expect(formatMoney(0)).toBe('0.00');
  });

  it('应该处理负数', () => {
    expect(formatMoney(-1234.56)).toBe('-1,234.56');
  });
});

describe('formatPercentage', () => {
  it('应该正确格式化百分比', () => {
    expect(formatPercentage(0.5)).toBe('50.00%');
    expect(formatPercentage(1)).toBe('100.00%');
  });

  it('应该保留两位小数', () => {
    expect(formatPercentage(0.6666)).toBe('66.66%');
  });
});

describe('formatDate', () => {
  it('应该正确格式化日期', () => {
    expect(formatDate('2026-01-25')).toBe('2026-01-25');
    expect(formatDate(new Date('2026-01-25'))).toBe('2026-01-25');
  });
});

4.2 测试自定义Hooks

// src/hooks/useAuth.test.js
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { renderHook, act, waitFor } from '@testing-library/react';
import { AuthProvider, useAuth } from '../contexts/AuthContext';

describe('useAuth Hook', () => {
  beforeEach(() => {
    // 清理localStorage
    localStorage.clear();
  });

  it('初始状态应该是未认证', () => {
    const { result } = renderHook(() => useAuth(), {
      wrapper: AuthProvider,
    });

    expect(result.current.isAuthenticated).toBe(false);
    expect(result.current.user).toBe(null);
    expect(result.current.token).toBe(null);
  });

  it('应该正确处理登录', async () => {
    const { result } = renderHook(() => useAuth(), {
      wrapper: AuthProvider,
    });

    await act(async () => {
      await result.current.login('admin', 'password123');
    });

    await waitFor(() => {
      expect(result.current.isAuthenticated).toBe(true);
      expect(result.current.user).toBeTruthy();
      expect(result.current.token).toBeTruthy();
    });
  });

  it('应该正确处理登出', async () => {
    const { result } = renderHook(() => useAuth(), {
      wrapper: AuthProvider,
    });

    // 先登录
    await act(async () => {
      await result.current.login('admin', 'password123');
    });

    // 然后登出
    await act(async () => {
      result.current.logout();
    });

    await waitFor(() => {
      expect(result.current.isAuthenticated).toBe(false);
      expect(result.current.user).toBe(null);
      expect(result.current.token).toBe(null);
    });
  });

  it('hasRole应该正确检查角色', async () => {
    const { result } = renderHook(() => useAuth(), {
      wrapper: AuthProvider,
    });

    await act(async () => {
      await result.current.login('admin', 'password123');
    });

    await waitFor(() => {
      expect(result.current.hasRole(['admin'])).toBe(true);
      expect(result.current.hasRole(['market'])).toBe(false);
      expect(result.current.hasRole(['admin', 'market'])).toBe(true);
    });
  });
});

4.3 测试API服务

// src/services/project.test.js
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { projectAPI } from './project';
import api from './api';

vi.mock('./api');

describe('projectAPI', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  describe('getList', () => {
    it('应该调用正确的API', async () => {
      const mockResponse = {
        items: [{ id: 1, name: '项目1' }],
        total: 1,
      };
      api.get.mockResolvedValue(mockResponse);

      const params = { page: 1, page_size: 10 };
      const result = await projectAPI.getList(params);

      expect(api.get).toHaveBeenCalledWith('/projects', { params });
      expect(result).toEqual(mockResponse);
    });
  });

  describe('getDetail', () => {
    it('应该获取项目详情', async () => {
      const mockProject = { id: 1, name: '项目1' };
      api.get.mockResolvedValue(mockProject);

      const result = await projectAPI.getDetail(1);

      expect(api.get).toHaveBeenCalledWith('/projects/1');
      expect(result).toEqual(mockProject);
    });
  });

  describe('create', () => {
    it('应该创建新项目', async () => {
      const mockProject = { id: 2, name: '新项目' };
      api.post.mockResolvedValue(mockProject);

      const data = { name: '新项目', contract_amount: 100 };
      const result = await projectAPI.create(data);

      expect(api.post).toHaveBeenCalledWith('/projects', data);
      expect(result).toEqual(mockProject);
    });
  });

  describe('update', () => {
    it('应该更新项目', async () => {
      const mockProject = { id: 1, name: '更新项目' };
      api.put.mockResolvedValue(mockProject);

      const data = { name: '更新项目' };
      const result = await projectAPI.update(1, data);

      expect(api.put).toHaveBeenCalledWith('/projects/1', data);
      expect(result).toEqual(mockProject);
    });
  });

  describe('delete', () => {
    it('应该删除项目', async () => {
      api.delete.mockResolvedValue({});

      await projectAPI.delete(1);

      expect(api.delete).toHaveBeenCalledWith('/projects/1');
    });
  });
});

5. 集成测试

5.1 测试页面组件

// src/pages/Login/Login.test.jsx
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import Login from './Login';

// 模拟authAPI
vi.mock('../../services/auth', () => ({
  authAPI: {
    login: vi.fn(),
  },
}));

// 模拟useAuth Hook
vi.mock('../../hooks/useAuth', () => ({
  useAuth: () => ({
    login: vi.fn(),
    loading: false,
  }),
}));

describe('Login Page', () => {
  const renderLogin = () => {
    return render(
      <BrowserRouter>
        <Login />
      </BrowserRouter>
    );
  };

  it('应该渲染登录表单', () => {
    renderLogin();
    expect(screen.getByText('海洋项目管理系统')).toBeInTheDocument();
    expect(screen.getByLabelText(/用户名/)).toBeInTheDocument();
    expect(screen.getByLabelText(/密码/)).toBeInTheDocument();
    expect(screen.getByRole('button', { name: /登录/ })).toBeInTheDocument();
  });

  it('应该显示必填字段验证错误', async () => {
    renderLogin();

    const submitButton = screen.getByRole('button', { name: /登录/ });
    fireEvent.click(submitButton);

    await waitFor(() => {
      expect(screen.getByText(/请输入用户名/)).toBeInTheDocument();
      expect(screen.getByText(/请输入密码/)).toBeInTheDocument();
    });
  });

  it('应该成功登录', async () => {
    const { login } = useAuth();
    login.mockResolvedValue({
      user: { id: 1, username: 'admin' },
      token: 'mock-token',
    });

    renderLogin();

    const usernameInput = screen.getByLabelText(/用户名/);
    const passwordInput = screen.getByLabelText(/密码/);
    const submitButton = screen.getByRole('button', { name: /登录/ });

    fireEvent.change(usernameInput, { target: { value: 'admin' } });
    fireEvent.change(passwordInput, { target: { value: 'password' } });
    fireEvent.click(submitButton);

    await waitFor(() => {
      expect(login).toHaveBeenCalledWith('admin', 'password');
    });
  });

  it('应该显示登录失败错误', async () => {
    const { login } = useAuth();
    login.mockRejectedValue(new Error('用户名或密码错误'));

    renderLogin();

    const usernameInput = screen.getByLabelText(/用户名/);
    const passwordInput = screen.getByLabelText(/密码/);
    const submitButton = screen.getByRole('button', { name: /登录/ });

    fireEvent.change(usernameInput, { target: { value: 'admin' } });
    fireEvent.change(passwordInput, { target: { value: 'wrong' } });
    fireEvent.click(submitButton);

    await waitFor(() => {
      expect(screen.getByText(/用户名或密码错误/)).toBeInTheDocument();
    });
  });
});

5.2 测试Dashboard页面

// src/pages/Dashboard/Dashboard.test.jsx
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import Dashboard from './Dashboard';

// 模拟API
vi.mock('../../services/project', () => ({
  projectAPI: {
    getList: vi.fn(),
  },
}));

vi.mock('../../services/statistics', () => ({
  statisticsAPI: {
    getBasicStatistics: vi.fn(),
  },
}));

describe('Dashboard Page', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('应该渲染统计卡片', async () => {
    const mockStatistics = {
      total_count: 100,
      total_contract_amount: 10000,
    };

    const { statisticsAPI } = await import('../../services/statistics');
    statisticsAPI.getBasicStatistics.mockResolvedValue(mockStatistics);

    render(
      <BrowserRouter>
        <Dashboard />
      </BrowserRouter>
    );

    await waitFor(() => {
      expect(screen.getByText(/项目总数/)).toBeInTheDocument();
      expect(screen.getByText(/100/)).toBeInTheDocument();
    });
  });

  it('应该渲染图表', async () => {
    const mockStatistics = {
      total_count: 100,
    };

    const { statisticsAPI } = await import('../../services/statistics');
    statisticsAPI.getBasicStatistics.mockResolvedValue(mockStatistics);

    render(
      <BrowserRouter>
        <Dashboard />
      </BrowserRouter>
    );

    await waitFor(() => {
      expect(screen.getByText(/项目趋势/)).toBeInTheDocument();
    });
  });
});

6. E2E测试

6.1 登录流程测试

// tests/e2e/login.spec.js
import { test, expect } from '@playwright/test';

test.describe('登录流程', () => {
  test('应该成功登录并跳转到仪表盘', async ({ page }) => {
    // 访问登录页
    await page.goto('http://localhost:3000/login');

    // 输入用户名和密码
    await page.fill('input[name="username"]', 'admin');
    await page.fill('input[name="password"]', 'password123');

    // 点击登录按钮
    await page.click('button[type="submit"]');

    // 验证跳转到仪表盘
    await expect(page).toHaveURL('http://localhost:3000/dashboard');

    // 验证用户信息显示
    await expect(page.locator('.user-info')).toContainText('系统管理员');
  });

  test('应该显示登录失败错误', async ({ page }) => {
    await page.goto('http://localhost:3000/login');

    // 输入错误密码
    await page.fill('input[name="username"]', 'admin');
    await page.fill('input[name="password"]', 'wrongpassword');
    await page.click('button[type="submit"]');

    // 验证错误提示
    await expect(page.locator('.error-message')).toContainText('用户名或密码错误');
  });

  test('应该验证必填字段', async ({ page }) => {
    await page.goto('http://localhost:3000/login');

    // 直接点击登录按钮
    await page.click('button[type="submit"]');

    // 验证错误提示
    await expect(page.locator('text=请输入用户名')).toBeVisible();
    await expect(page.locator('text=请输入密码')).toBeVisible();
  });
});

6.2 项目管理测试

// tests/e2e/projects.spec.js
import { test, expect } from '@playwright/test';

test.describe('项目管理', () => {
  test.beforeEach(async ({ page }) => {
    // 登录
    await page.goto('http://localhost:3000/login');
    await page.fill('input[name="username"]', 'admin');
    await page.fill('input[name="password"]', 'password123');
    await page.click('button[type="submit"]');
    await page.waitForURL('**/dashboard');
  });

  test('应该显示项目列表', async ({ page }) => {
    // 导航到项目列表
    await page.click('text=项目管理');
    await page.waitForURL('**/projects');

    // 验证表格显示
    await expect(page.locator('table')).toBeVisible();
    await expect(page.locator('th:has-text("项目编号")')).toBeVisible();
    await expect(page.locator('th:has-text("项目名称")')).toBeVisible();
  });

  test('应该创建新项目', async ({ page }) => {
    await page.click('text=项目管理');
    await page.waitForURL('**/projects');

    // 点击新建项目按钮
    await page.click('button:has-text("新建项目")');

    // 填写表单
    await page.fill('input[name="project_no"]', 'PRJ001');
    await page.fill('input[name="name"]', '测试项目');
    await page.selectOption('select[name="engineering_type"]', '基建');
    await page.fill('input[name="contract_amount"]', '100');

    // 提交表单
    await page.click('button:has-text("保存")');

    // 验证成功提示
    await expect(page.locator('.success-message')).toContainText('项目创建成功');

    // 验证项目出现在列表中
    await expect(page.locator('table')).toContainText('PRJ001');
  });

  test('应该搜索项目', async ({ page }) => {
    await page.click('text=项目管理');
    await page.waitForURL('**/projects');

    // 输入搜索关键词
    await page.fill('input[placeholder*="搜索"]', '测试项目');
    await page.press('input[placeholder*="搜索"]', 'Enter');

    // 验证搜索结果
    await expect(page.locator('table')).toContainText('测试项目');
  });

  test('应该筛选项目', async ({ page }) => {
    await page.click('text=项目管理');
    await page.waitForURL('**/projects');

    // 选择工程类别
    await page.click('select[name="engineering_type"]');
    await page.click('option:has-text("基建")');

    // 验证筛选结果
    await expect(page.locator('table')).toBeVisible();
  });

  test('应该删除项目', async ({ page }) => {
    await page.click('text=项目管理');
    await page.waitForURL('**/projects');

    // 点击删除按钮
    await page.click('button:has-text("删除")');

    // 确认删除
    await page.click('button:has-text("确定")');

    // 验证成功提示
    await expect(page.locator('.success-message')).toContainText('项目删除成功');
  });
});

6.3 用户管理测试

// tests/e2e/users.spec.js
import { test, expect } from '@playwright/test';

test.describe('用户管理', () => {
  test.beforeEach(async ({ page }) => {
    // 管理员登录
    await page.goto('http://localhost:3000/login');
    await page.fill('input[name="username"]', 'admin');
    await page.fill('input[name="password"]', 'password123');
    await page.click('button[type="submit"]');
    await page.waitForURL('**/dashboard');
  });

  test('应该显示用户列表', async ({ page }) => {
    await page.click('text=用户管理');
    await page.waitForURL('**/users');

    await expect(page.locator('table')).toBeVisible();
    await expect(page.locator('th:has-text("用户名")')).toBeVisible();
  });

  test('应该创建新用户', async ({ page }) => {
    await page.click('text=用户管理');
    await page.waitForURL('**/users');

    await page.click('button:has-text("新建用户")');

    // 填写用户信息
    await page.fill('input[name="username"]', 'testuser');
    await page.fill('input[name="password"]', 'password123');
    await page.fill('input[name="real_name"]', '测试用户');
    await page.selectOption('select[name="department"]', '管理部');
    await page.selectOption('select[name="role"]', 'admin');

    await page.click('button:has-text("保存")');

    await expect(page.locator('.success-message')).toContainText('用户创建成功');
  });

  test('应该重置用户密码', async ({ page }) => {
    await page.click('text=用户管理');
    await page.waitForURL('**/users');

    await page.click('button:has-text("重置密码")');

    await page.fill('input[name="new_password"]', 'newpassword123');
    await page.click('button:has-text("确定")');

    await expect(page.locator('.success-message')).toContainText('密码重置成功');
  });
});

7. 测试最佳实践

7.1 单元测试最佳实践

  1. 测试名称应该描述清楚

    // 好的测试名称
    it('应该正确格式化金额为千分位分隔', () => {})
    
    // 不好的测试名称
    it('测试formatMoney函数', () => {})
    
  2. 遵循AAA模式

    it('应该正确格式化金额', () => {
      // Arrange (准备)
      const amount = 1234.56;
    
      // Act (执行)
      const result = formatMoney(amount);
    
      // Assert (断言)
      expect(result).toBe('1,234.56');
    });
    
  3. 测试边界情况

    it('应该处理边界值', () => {
      expect(formatMoney(0)).toBe('0.00');
      expect(formatMoney(0.005)).toBe('0.01');
      expect(formatMoney(9999999999)).toBe('9,999,999,999.00');
    });
    
  4. 避免测试实现细节

    // 不好的测试:测试内部状态
    it('应该设置loading状态为true', () => {
      render(<Login />);
      fireEvent.click(screen.getByText('登录'));
      expect(component.state.loading).toBe(true);
    });
    
    // 好的测试:测试用户可见行为
    it('应该显示加载状态', () => {
      render(<Login />);
      fireEvent.click(screen.getByText('登录'));
      expect(screen.getByRole('button')).toBeDisabled();
    });
    

7.2 集成测试最佳实践

  1. 关注用户行为

    // 好的测试:模拟用户交互
    render(<ProjectForm />);
    fireEvent.change(screen.getByLabelText('项目名称'), {
      target: { value: '新项目' },
    });
    fireEvent.click(screen.getByText('保存'));
    
    // 不好的测试:直接设置props
    render(<ProjectForm value="新项目" />);
    
  2. 使用合适的查询方法

    // 优先级从高到低
    screen.getByRole('button', { name: '保存' });
    screen.getByLabelText('项目名称');
    screen.getByPlaceholderText('请输入项目名称');
    screen.getByText('保存');
    screen.getByTestId('submit-button'); // 最后选择
    
  3. 等待异步操作完成

    it('应该提交表单', async () => {
      render(<ProjectForm />);
      fireEvent.click(screen.getByText('保存'));
    
      await waitFor(() => {
        expect(screen.getByText('保存成功')).toBeInTheDocument();
      });
    });
    

7.3 E2E测试最佳实践

  1. 使用Page Object模式

    // pages/ProjectListPage.js
    class ProjectListPage {
      constructor(page) {
        this.page = page;
      }
    
      async goto() {
        await this.page.goto('/projects');
      }
    
      async createProject(data) {
        await this.page.click('button:has-text("新建项目")');
        await this.page.fill('input[name="name"]', data.name);
        await this.page.click('button:has-text("保存")');
      }
    
      async getProjectRows() {
        return this.page.locator('table tbody tr');
      }
    }
    
    // 测试中使用
    test('应该创建项目', async ({ page }) => {
      const projectPage = new ProjectListPage(page);
      await projectPage.goto();
      await projectPage.createProject({ name: '测试项目' });
    
      const rows = await projectPage.getProjectRows();
      await expect(rows).toHaveCount(1);
    });
    
  2. 避免硬编码选择器

    // 好的
    await page.click('button:has-text("保存")');
    await page.click('[data-testid="save-button"]');
    
    // 不好的
    await page.click('.btn-primary.btn-lg');
    await page.click('#submit-btn-12345');
    
  3. 添加合理的等待

    // 好的:使用自动等待
    await page.waitForSelector('.success-message');
    
    // 不好:固定延迟
    await page.waitForTimeout(1000);
    

8. 测试命令

# 运行所有单元测试
npm run test

# 运行测试并监听文件变化
npm run test:watch

# 运行测试并生成覆盖率报告
npm run test:coverage

# 运行E2E测试
npm run test:e2e

# 运行E2E测试并打开UI
npm run test:e2e:ui

# 运行E2E测试(特定浏览器)
npx playwright test --project=chromium

9. CI/CD集成

9.1 GitHub Actions配置

# .github/workflows/test.yml
name: Tests

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'

      - name: Install dependencies
        run: npm ci

      - name: Run unit tests
        run: npm run test

      - name: Run E2E tests
        run: npm run test:e2e

      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage/coverage-final.json

10. 调试测试

10.1 调试单元测试

// 使用debug()
test('调试测试', () => {
  debug();
  // 这会暂停测试,让你在浏览器中检查DOM
});

10.2 调试E2E测试

// 使用Playwright Inspector
test('调试E2E测试', async ({ page }) => {
  await page.goto('http://localhost:3000');
  await page.pause(); // 暂停执行,打开Inspector

  await page.click('button');
});

文档维护: 前端程序员 文档类型: 前端测试技术文档 最后更新: 2026-01-25