feat: paginated list responses, issue sorting + filtering by assignee/tag

This commit is contained in:
Zhi
2026-02-23 10:12:07 +00:00
parent 703103af91
commit 107102e775
3 changed files with 86 additions and 13 deletions

View File

@@ -9,8 +9,17 @@ Agent/人类协同任务管理平台 - FastAPI 后端
- `GET /auth/me` - 获取当前用户信息
### Issues
> Issues 和 Search 列表接口返回分页格式:`{items, total, page, page_size, total_pages}`
> Issues 支持排序参数:`sort_by` (created_at/priority/title/due_date/status), `sort_order` (asc/desc)
> Issues 支持额外过滤:`assignee_id`, `tag`
> Issues 和 Search 列表接口返回分页格式:
> Issues 支持排序参数: (created_at/priority/title/due_date/status), (asc/desc)
> Issues 支持额外过滤:,
- `POST /issues` - 创建 issue支持 resolution 决议案类型)
- `GET /issues` - 列表(支持按 project/status/type 过滤)
- `GET /issues` - 列表(分页、排序、按 assignee/tag 过滤)(支持按 project/status/type 过滤)
- `GET /issues/{id}` - 详情
- `PATCH /issues/{id}` - 更新
- `DELETE /issues/{id}` - 删除

View File

@@ -110,26 +110,64 @@ def create_issue(issue: schemas.IssueCreate, bg: BackgroundTasks, db: Session =
return db_issue
@app.get("/issues", response_model=List[schemas.IssueResponse])
import math
@app.get("/issues")
def list_issues(
project_id: int = None,
issue_status: str = None,
issue_type: str = None,
skip: int = 0,
limit: int = 100,
assignee_id: int = None,
tag: str = None,
sort_by: str = "created_at",
sort_order: str = "desc",
page: int = 1,
page_size: int = 50,
db: Session = Depends(get_db)
):
"""List issues with filtering, sorting, and pagination metadata."""
query = db.query(models.Issue)
if project_id:
query = query.filter(models.Issue.project_id == project_id)
if issue_status:
query = query.filter(models.Issue.status == issue_status)
if issue_type:
query = query.filter(models.Issue.issue_type == issue_type)
issues = query.offset(skip).limit(limit).all()
return issues
if assignee_id:
query = query.filter(models.Issue.assignee_id == assignee_id)
if tag:
query = query.filter(models.Issue.tags.contains(tag))
# Sorting
sort_fields = {
"created_at": models.Issue.created_at,
"updated_at": models.Issue.updated_at,
"priority": models.Issue.priority,
"title": models.Issue.title,
"due_date": models.Issue.due_date,
"status": models.Issue.status,
}
sort_col = sort_fields.get(sort_by, models.Issue.created_at)
if sort_order == "asc":
query = query.order_by(sort_col.asc())
else:
query = query.order_by(sort_col.desc())
total = query.count()
page = max(1, page)
page_size = min(max(1, page_size), 200)
total_pages = math.ceil(total / page_size) if total else 1
skip = (page - 1) * page_size
items = query.offset(skip).limit(page_size).all()
return {
"items": [schemas.IssueResponse.model_validate(i) for i in items],
"total": total,
"page": page,
"page_size": page_size,
"total_pages": total_pages,
}
@app.get("/issues/overdue", response_model=List[schemas.IssueResponse])
@@ -462,21 +500,35 @@ def transition_issue(issue_id: int, new_status: str, bg: BackgroundTasks, db: Se
# ============ Search API ============
@app.get("/search/issues", response_model=List[schemas.IssueResponse])
@app.get("/search/issues")
def search_issues(
q: str,
project_id: int = None,
skip: int = 0,
limit: int = 50,
page: int = 1,
page_size: int = 50,
db: Session = Depends(get_db)
):
"""Search issues by title or description keyword."""
"""Search issues by title or description keyword with pagination."""
query = db.query(models.Issue).filter(
(models.Issue.title.contains(q)) | (models.Issue.description.contains(q))
)
if project_id:
query = query.filter(models.Issue.project_id == project_id)
return query.offset(skip).limit(limit).all()
total = query.count()
page = max(1, page)
page_size = min(max(1, page_size), 200)
total_pages = math.ceil(total / page_size) if total else 1
skip = (page - 1) * page_size
items = query.offset(skip).limit(page_size).all()
return {
"items": [schemas.IssueResponse.model_validate(i) for i in items],
"total": total,
"page": page,
"page_size": page_size,
"total_pages": total_pages,
}

View File

@@ -198,3 +198,15 @@ class MilestoneResponse(MilestoneBase):
class Config:
from_attributes = True
# Paginated response
from typing import Generic, TypeVar
T = TypeVar("T")
class PaginatedResponse(BaseModel, Generic[T]):
items: List[T]
total: int
page: int
page_size: int
total_pages: int