diff --git a/docs/PAGINATION.md b/docs/PAGINATION.md
new file mode 100644
index 000000000..98c9c0441
--- /dev/null
+++ b/docs/PAGINATION.md
@@ -0,0 +1,330 @@
+# 分页使用指南
+
+## 📋 概述
+
+小虾 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 (
+
+
+ {projects.map(p => - {p.name}
)}
+
+
+
+
+ );
+}
+```
+
+### Vue 示例
+
+```vue
+
+
+
+ -
+ {{ project.name }}
+
+
+
+
+
+
+
+
+```
+
+---
+
+## 🎯 最佳实践
+
+### 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
diff --git a/packages/application/common/pagination.py b/packages/application/common/pagination.py
new file mode 100644
index 000000000..f700fb689
--- /dev/null
+++ b/packages/application/common/pagination.py
@@ -0,0 +1,98 @@
+"""
+通用分页器
+"""
+from typing import Generic, TypeVar, List, Optional
+from pydantic import BaseModel, Field
+from math import ceil
+
+
+T = TypeVar("T")
+
+
+class PaginationParams(BaseModel):
+ """分页参数"""
+ page: int = Field(1, ge=1, description="页码(从 1 开始)")
+ page_size: int = Field(20, ge=1, le=100, description="每页数量(最大 100)")
+
+ @property
+ def offset(self) -> int:
+ """计算偏移量"""
+ return (self.page - 1) * self.page_size
+
+ @property
+ def limit(self) -> int:
+ """返回限制数量"""
+ return self.page_size
+
+
+class PaginationMeta(BaseModel):
+ """分页元数据"""
+ page: int = Field(..., description="当前页码")
+ page_size: int = Field(..., description="每页数量")
+ total: int = Field(..., description="总记录数")
+ total_pages: int = Field(..., description="总页数")
+ has_next: bool = Field(..., description="是否有下一页")
+ has_prev: bool = Field(..., description="是否有上一页")
+
+ @classmethod
+ def from_params(
+ cls,
+ params: PaginationParams,
+ total: int,
+ ) -> "PaginationMeta":
+ """从参数和总数创建元数据"""
+ total_pages = ceil(total / params.page_size) if total > 0 else 0
+
+ return cls(
+ page=params.page,
+ page_size=params.page_size,
+ total=total,
+ total_pages=total_pages,
+ has_next=params.page < total_pages,
+ has_prev=params.page > 1,
+ )
+
+
+class PaginatedResponse(BaseModel, Generic[T]):
+ """分页响应"""
+ data: List[T] = Field(..., description="数据列表")
+ pagination: PaginationMeta = Field(..., description="分页信息")
+
+ @classmethod
+ def create(
+ cls,
+ data: List[T],
+ params: PaginationParams,
+ total: int,
+ ) -> "PaginatedResponse[T]":
+ """创建分页响应"""
+ return cls(
+ data=data,
+ pagination=PaginationMeta.from_params(params, total),
+ )
+
+
+def paginate(
+ items: List[T],
+ params: PaginationParams,
+) -> PaginatedResponse[T]:
+ """
+ 内存分页(适用于 InMemory Repository)
+
+ Args:
+ items: 完整列表
+ params: 分页参数
+
+ Returns:
+ 分页响应
+ """
+ total = len(items)
+ start = params.offset
+ end = start + params.limit
+ page_data = items[start:end]
+
+ return PaginatedResponse.create(
+ data=page_data,
+ params=params,
+ total=total,
+ )