# 分页使用指南

## 📋 概述

小虾 SaaS 提供统一的分页接口，支持内存分页和数据库分页。

---

## 🔧 基本使用

### 1. API 端点添加分页

```python
from fastapi import APIRouter, Depends
from packages.application.common.pagination import PaginationParams, PaginatedResponse

router = APIRouter()

@router.get("/projects", response_model=PaginatedResponse[ProjectResponse])
async def list_projects(
    pagination: PaginationParams = Depends(),
    user_id: str = Depends(get_current_user_id),
):
    # 获取分页数据
    projects, total = project_repo.find_by_user_paginated(
        user_id=user_id,
        offset=pagination.offset,
        limit=pagination.limit,
    )
    
    # 返回分页响应
    return PaginatedResponse.create(
        data=projects,
        params=pagination,
        total=total,
    )
```

### 2. 请求分页数据

```bash
# 默认第一页，每页 20 条
GET /api/v1/projects

# 指定页码和每页数量
GET /api/v1/projects?page=2&page_size=10

# 获取更多数据
GET /api/v1/projects?page=1&page_size=50
```

### 3. 响应格式

```json
{
  "data": [
    {
      "id": "project-1",
      "name": "项目 1"
    },
    {
      "id": "project-2",
      "name": "项目 2"
    }
  ],
  "pagination": {
    "page": 1,
    "page_size": 20,
    "total": 156,
    "total_pages": 8,
    "has_next": true,
    "has_prev": false
  }
}
```

---

## 🗄️ Repository 实现

### InMemory Repository（内存分页）

```python
from packages.application.common.pagination import paginate, PaginationParams

class InMemoryProjectRepository:
    def find_by_workspace_paginated(
        self,
        workspace_id: str,
        pagination: PaginationParams,
    ):
        # 获取所有项目
        all_projects = [p for p in self._storage.values() 
                       if p.workspace_id == workspace_id]
        
        # 使用内存分页
        return paginate(all_projects, pagination)
```

### PostgreSQL Repository（数据库分页）

```python
class PostgresProjectRepository:
    def find_by_workspace_paginated(
        self,
        workspace_id: str,
        offset: int,
        limit: int,
    ) -> tuple[List[Project], int]:
        conn = self._get_connection()
        try:
            with conn.cursor() as cur:
                # 获取总数
                cur.execute(
                    "SELECT COUNT(*) FROM projects WHERE workspace_id = %s",
                    (workspace_id,)
                )
                total = cur.fetchone()["count"]
                
                # 获取分页数据
                cur.execute("""
                    SELECT * FROM projects 
                    WHERE workspace_id = %s 
                    ORDER BY created_at DESC
                    LIMIT %s OFFSET %s
                """, (workspace_id, limit, offset))
                
                projects = [self._row_to_project(row) for row in cur.fetchall()]
                
                return projects, total
        finally:
            conn.close()
```

---

## 📱 前端使用

### React 示例

```typescript
import { useState, useEffect } from 'react';

function ProjectList() {
  const [projects, setProjects] = useState([]);
  const [pagination, setPagination] = useState(null);
  const [page, setPage] = useState(1);

  useEffect(() => {
    fetch(`/api/v1/projects?page=${page}&page_size=20`)
      .then(res => res.json())
      .then(data => {
        setProjects(data.data);
        setPagination(data.pagination);
      });
  }, [page]);

  return (
    <div>
      <ul>
        {projects.map(p => <li key={p.id}>{p.name}</li>)}
      </ul>
      
      <Pagination
        current={pagination?.page}
        total={pagination?.total}
        pageSize={pagination?.page_size}
        onChange={setPage}
        showPrevNext={true}
        disabled={{
          prev: !pagination?.has_prev,
          next: !pagination?.has_next
        }}
      />
    </div>
  );
}
```

### Vue 示例

```vue
<template>
  <div>
    <ul>
      <li v-for="project in projects" :key="project.id">
        {{ project.name }}
      </li>
    </ul>
    
    <el-pagination
      :current-page="pagination.page"
      :page-size="pagination.page_size"
      :total="pagination.total"
      @current-change="handlePageChange"
    />
  </div>
</template>

<script>
export default {
  data() {
    return {
      projects: [],
      pagination: {
        page: 1,
        page_size: 20,
        total: 0
      }
    };
  },
  methods: {
    async fetchProjects(page = 1) {
      const response = await fetch(
        `/api/v1/projects?page=${page}&page_size=20`
      );
      const data = await response.json();
      this.projects = data.data;
      this.pagination = data.pagination;
    },
    handlePageChange(page) {
      this.fetchProjects(page);
    }
  },
  mounted() {
    this.fetchProjects();
  }
};
</script>
```

---

## 🎯 最佳实践

### 1. 合理的页面大小

```python
# ✅ 推荐
page_size: int = Field(20, ge=1, le=100)  # 默认 20，最大 100

# ❌ 不推荐
page_size: int = Field(1000, ge=1, le=10000)  # 太大，性能差
```

### 2. 默认排序

```python
# ✅ 总是指定排序
ORDER BY created_at DESC

# ❌ 不指定排序（结果不稳定）
SELECT * FROM projects LIMIT 20
```

### 3. 使用游标分页（大数据量）

```python
# 基于 ID 的游标分页（更快）
@router.get("/projects")
async def list_projects(
    cursor: Optional[str] = None,  # 上一页最后一个 ID
    limit: int = 20,
):
    if cursor:
        projects = project_repo.find_after_cursor(cursor, limit)
    else:
        projects = project_repo.find_first_page(limit)
    
    return {
        "data": projects,
        "next_cursor": projects[-1].id if projects else None,
        "has_more": len(projects) == limit
    }
```

### 4. 缓存总数（可选）

```python
# 对于不常变化的列表，缓存总数
from functools import lru_cache

@lru_cache(maxsize=128)
def get_total_projects(workspace_id: str) -> int:
    return project_repo.count_by_workspace(workspace_id)
```

---

## 📊 性能对比

### Offset/Limit 分页

**优点:**
- 简单易用
- 支持跳转到任意页

**缺点:**
- 深分页慢（OFFSET 10000 需要扫描 10000 行）
- 数据变化时可能重复/遗漏

**适用场景:**
- 数据量小（< 10,000 条）
- 用户很少翻到后面

### 游标分页

**优点:**
- 性能稳定（始终快速）
- 不受数据变化影响

**缺点:**
- 不支持跳转
- 只能上一页/下一页

**适用场景:**
- 数据量大（> 100,000 条）
- 无限滚动

---

## 🔗 相关资源

- [PostgreSQL LIMIT/OFFSET 优化](https://www.postgresql.org/docs/current/queries-limit.html)
- [游标分页最佳实践](https://use-the-index-luke.com/sql/partial-results/fetch-next-page)
- [GraphQL Cursor Connections](https://relay.dev/graphql/connections.htm)

---

**最后更新:** 2026-06-17
