# 报名详情页面性能优化

## 问题描述

用户反馈：点击报名详情页面时出现大批量 API 调用，导致页面加载缓慢。

## 原问题分析

### 原实现方式的性能问题

在 [index.vue](d:\workspace\31.比赛项目\project\martial-web\src\views\martial\registration\index.vue) 页面的 `mounted()` 钩子中，同时调用了 4 个数据加载方法：

```javascript
mounted() {
  this.competitionId = this.$route.query.competitionId
  if (this.competitionId) {
    this.loadCompetitionInfo(this.competitionId)
    this.loadRegistrationStats()     // 方法1
    this.loadParticipantsStats()     // 方法2
    this.loadProjectTimeStats()      // 方法3
    this.loadAmountStats()           // 方法4
  }
}
```

**存在的严重性能问题**：

### 1. 重复查询参赛者列表（4 次 API 调用）

四个方法都独立调用 `getParticipantList` API：

- `loadRegistrationStats()` → 调用 1 次 `getParticipantList`
- `loadParticipantsStats()` → 调用 1 次 `getParticipantList`
- `loadProjectTimeStats()` → 调用 1 次 `getParticipantList`
- `loadAmountStats()` → 调用 1 次 `getParticipantList`

**总计：4 次相同的 API 调用**，每次返回几千条数据！

### 2. 循环调用项目详情 API（N × 3 次）

三个方法都需要查询项目详情，每个方法独立循环调用 `getProjectDetail`：

```javascript
// loadRegistrationStats() 中
for (const athlete of participants) {
  const projectId = athlete.projectId || athlete.project_id
  if (projectId && !projectIds.has(projectId)) {
    projectIds.add(projectId)
    const projectRes = await getProjectDetail(projectId)  // 第1轮调用
    // ...
  }
}

// loadProjectTimeStats() 中
for (const [projectId, athleteList] of projectMap) {
  const projectRes = await getProjectDetail(projectId)  // 第2轮调用（重复！）
  // ...
}

// loadAmountStats() 中
if (!stat.projectPrices.has(projectId)) {
  const projectRes = await getProjectDetail(projectId)  // 第3轮调用（重复！）
  // ...
}
```

**假设场景**：一个赛事有 20 个不同项目

- `loadRegistrationStats()` 调用 20 次 `getProjectDetail`
- `loadProjectTimeStats()` 再调用 20 次 `getProjectDetail`
- `loadAmountStats()` 又调用 20 次 `getProjectDetail`

**总计：60 次 `getProjectDetail` API 调用！**

### 3. 总体性能开销

对于一个有 **20 个项目、500 名参赛者** 的赛事：

| API | 调用次数 | 单次数据量 | 总开销 |
|-----|---------|-----------|--------|
| `getParticipantList` | **4 次** | 500 条记录 | **2000 条记录传输** |
| `getProjectDetail` | **60 次** | 1 条记录 | 60 次网络往返 |
| `getCompetitionDetail` | 1 次 | 1 条记录 | 1 次网络往返 |

**总计：65 次 API 调用！**

假设每次 API 调用平均耗时 50ms：
- 总耗时 = 65 × 50ms = **3.25 秒**
- 加上数据处理和渲染 ≈ **4-5 秒**

用户体验极差！

## 优化方案

### 核心思路：缓存 + 批量加载

1. **缓存参赛者列表**：只调用一次 `getParticipantList`，所有方法共享同一份数据
2. **缓存项目信息**：只调用一次每个项目的 `getProjectDetail`，使用 Map 存储
3. **批量并行加载**：一次性并行加载所有项目信息，而不是串行循环

### 实现细节

#### 1. 添加缓存数据结构

```javascript
data() {
  return {
    // ...其他数据
    projectCache: new Map(),  // 项目信息缓存
    participantsCache: null   // 参赛者列表缓存
  }
}
```

#### 2. 统一的参赛者获取方法（带缓存）

```javascript
// 统一获取参赛者列表（带缓存）
async getParticipants() {
  if (this.participantsCache !== null) {
    return this.participantsCache  // 从缓存返回
  }

  try {
    const res = await getParticipantList(this.competitionId, 1, 10000)
    const participants = res.data?.data?.records || res.data?.data || []
    this.participantsCache = participants  // 存入缓存
    return participants
  } catch (err) {
    console.error('查询参赛者列表失败:', err)
    return []
  }
}
```

#### 3. 统一的项目信息获取方法（带缓存）

```javascript
// 统一的项目信息获取方法（带缓存）
async getProjectInfo(projectId) {
  if (!projectId) return null

  // 先从缓存中查找
  if (this.projectCache.has(projectId)) {
    return this.projectCache.get(projectId)
  }

  // 缓存中没有，则调用API
  try {
    const projectRes = await getProjectDetail(projectId)
    const projectInfo = projectRes.data?.data
    if (projectInfo) {
      this.projectCache.set(projectId, projectInfo)  // 存入缓存
      return projectInfo
    }
  } catch (err) {
    console.error(`查询项目${projectId}详情失败:`, err)
  }
  return null
}
```

#### 4. 批量预加载项目信息

```javascript
// 批量预加载项目信��（一次性并行加载所有需要的项目）
async preloadProjectInfo(participants) {
  const projectIds = new Set()
  participants.forEach(p => {
    const projectId = p.projectId || p.project_id
    if (projectId && !this.projectCache.has(projectId)) {
      projectIds.add(projectId)
    }
  })

  // 并行加载所有项目信息
  if (projectIds.size > 0) {
    const promises = Array.from(projectIds).map(id => this.getProjectInfo(id))
    await Promise.all(promises)  // 并行执行，不是串行！
  }
}
```

#### 5. 修改各个加载方法使用缓存

**loadRegistrationStats()** - 预加载所有项目：

```javascript
async loadRegistrationStats() {
  const participants = await this.getParticipants()  // 使用缓存
  this.competitionInfo.totalParticipants = participants.length

  // 一次性并行加载所有项目信息
  await this.preloadProjectInfo(participants)

  // 从缓存中获取价格
  let totalAmount = 0
  const projectIds = new Set()
  for (const athlete of participants) {
    const projectId = athlete.projectId || athlete.project_id
    if (projectId && !projectIds.has(projectId)) {
      projectIds.add(projectId)
      const project = this.projectCache.get(projectId)  // 从缓存读取
      if (project) {
        totalAmount += parseFloat(project.price || 0)
      }
    }
  }
  this.competitionInfo.totalAmount = totalAmount.toFixed(2)
}
```

**loadParticipantsStats()** - 直接使用缓存：

```javascript
async loadParticipantsStats() {
  const participants = await this.getParticipants()  // 从缓存读取
  // 按单位分组统计...
}
```

**loadProjectTimeStats()** - 从缓存读取项目信息：

```javascript
async loadProjectTimeStats() {
  const participants = await this.getParticipants()  // 从缓存读取

  // 按项目分组
  const projectMap = new Map()
  participants.forEach(athlete => {
    // ...分组逻辑
  })

  // 从缓存中获取项目信息（不再调用API）
  const projectStats = []
  for (const [projectId, athleteList] of projectMap) {
    const project = this.projectCache.get(projectId)  // 从缓存读取
    if (project) {
      projectStats.push({
        projectName: project.projectName || project.project_name || '未知项目',
        // ...其他字段
      })
    }
  }
  this.projectTimeData = projectStats
}
```

**loadAmountStats()** - 从缓存读取价格：

```javascript
async loadAmountStats() {
  const participants = await this.getParticipants()  // 从缓存读取

  const unitMap = new Map()
  for (const athlete of participants) {
    const projectId = athlete.projectId || athlete.project_id
    if (projectId) {
      stat.projectIds.add(projectId)

      // 从缓存中获取价格（不再调用API）
      if (!stat.projectPrices.has(projectId)) {
        const project = this.projectCache.get(projectId)  // 从缓存读取
        const price = project ? (project.price || 0) : 0
        stat.projectPrices.set(projectId, parseFloat(price))
      }
    }
  }
  // ...计算总金额
}
```

## 优化效果

### API 调用次数对比

对于一个有 **20 个项目、500 名参赛者** 的赛事：

| API | 优化前 | 优化后 | 减少 |
|-----|--------|--------|------|
| `getParticipantList` | **4 次** | **1 次** | ↓ 75% |
| `getProjectDetail` | **60 次** | **20 次（并行）** | ↓ 66.7% |
| 总 API 调用 | **65 次** | **21 次** | ↓ 67.7% |

### 性能提升

假设每次 API 调用平均耗时 50ms：

**优化前**：
- 串行执行：65 × 50ms = **3,250ms（3.25 秒）**
- 加上数据处理 ≈ **4-5 秒**

**优化后**：
- `getParticipantList`: 1 × 50ms = 50ms
- `getProjectDetail`: 20 次并行 ≈ 100ms（并行执行，不是串行！）
- 内存缓存读取：可忽略不计
- **总耗时 ≈ 150-200ms**
- 加上数据处理 ≈ **300-500ms**

**性能提升**：从 **4-5 秒** 降低到 **0.3-0.5 秒**，提升约 **90%**！

### 网络流量优化

**优化前**：
- 传输 500 条参赛者记录 × 4 次 = **2000 条记录**
- 传输 20 条项目记录 × 3 次 = **60 条记录**

**优化后**：
- 传输 500 条参赛者记录 × 1 次 = **500 条记录**
- 传输 20 条项目记录 × 1 次 = **20 条记录**

**流量减少约 75%**

## 优化亮点

1. **缓存机制**：避免重复数据获取
2. **并行加载**：`Promise.all` 并行加载项目信息，而不是串行循环
3. **内存优化**：使用 `Map` 数据结构高效存储和查找
4. **代码复用**：统一的获取方法，避免代码重复

## 相关文件

### 修改的文件
- [src/views/martial/registration/index.vue](d:\workspace\31.比赛项目\project\martial-web\src\views\martial\registration\index.vue)

### 修改内容
1. 添加缓存数据结构（第 189-190 行）
2. 新增 `getParticipants()` 方法（第 206-221 行）
3. 新增 `getProjectInfo()` 方法（第 223-240 行）
4. 新增 `preloadProjectInfo()` 方法（第 242-255 行）
5. 优化 `loadRegistrationStats()` 方法（第 299-331 行）
6. 优化 `loadParticipantsStats()` 方法（第 333-370 行）
7. 优化 `loadProjectTimeStats()` 方法（第 371-420 行）
8. 优化 `loadAmountStats()` 方法（第 422-472 行）

## 测试验证

### 如何测试

1. **打开浏览器开发者工具**（F12）
2. **切换到 Network 标签**
3. **点击报名详情页面**
4. **观察网络请求**

### 预期结果

**优化前**：
- 看到 4 次 `getParticipantList` 请求
- 看到 60 次 `getProjectDetail` 请求
- 总计 65+ 次请求

**优化后**：
- 只看到 1 次 `getParticipantList` 请求
- 只看到 20 次 `getProjectDetail` 请求（并行发起）
- 总计 21 次请求
- 页面加载速度明显提升

## 进一步优化建议

如果还需要继续优化，可以考虑：

### 1. 后端批量查询接口

创建一个后端批量查询接口：

```java
@PostMapping("/projects/batch")
public R<List<MartialProject>> batchGetProjects(@RequestBody List<Long> projectIds) {
    // 一次性查询多个项目
    List<MartialProject> projects = projectService.listByIds(projectIds);
    return R.data(projects);
}
```

这样可以将 20 次 `getProjectDetail` 请求减少到 1 次！

### 2. 后端聚合查询接口

创建一个后端聚合接口，一次性返回所有统计数据：

```java
@GetMapping("/registration/stats")
public R<RegistrationStatsDTO> getRegistrationStats(@RequestParam Long competitionId) {
    // 后端一次性查询所有需要的数据
    RegistrationStatsDTO stats = registrationService.getStats(competitionId);
    return R.data(stats);
}
```

这样前端只需要调用 1 个 API 即可获取所有数据！

### 3. 使用 Vuex 或 Pinia 状态管理

将缓存数据放到全局状态管理中，跨页面共享：

```javascript
// store/modules/competition.js
export default {
  state: {
    participantsCache: {},
    projectCache: {}
  },
  mutations: {
    SET_PARTICIPANTS_CACHE(state, { competitionId, data }) {
      state.participantsCache[competitionId] = data
    }
  }
}
```

## 总结

通过引入缓存机制和并行加载优化，将报名详情页面的 **65 次 API 调用减少到 21 次**，性能提升约 **90%**，页面加载时间从 **4-5 秒降低到 0.3-0.5 秒**，大幅改善了用户体验。

这是前端性能优化的经典案例，核心原则是：
1. **避免重复请求** - 使用缓存
2. **减少串行等待** - 使用并行加载
3. **优化数据流量** - 批量查询而不是循环单次查询
