feat: paginated list responses, issue sorting + filtering by assignee/tag
This commit is contained in:
11
README.md
11
README.md
@@ -9,8 +9,17 @@ Agent/人类协同任务管理平台 - FastAPI 后端
|
|||||||
- `GET /auth/me` - 获取当前用户信息
|
- `GET /auth/me` - 获取当前用户信息
|
||||||
|
|
||||||
### Issues
|
### 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 决议案类型)
|
- `POST /issues` - 创建 issue(支持 resolution 决议案类型)
|
||||||
- `GET /issues` - 列表(支持按 project/status/type 过滤)
|
- `GET /issues` - 列表(分页、排序、按 assignee/tag 过滤)(支持按 project/status/type 过滤)
|
||||||
- `GET /issues/{id}` - 详情
|
- `GET /issues/{id}` - 详情
|
||||||
- `PATCH /issues/{id}` - 更新
|
- `PATCH /issues/{id}` - 更新
|
||||||
- `DELETE /issues/{id}` - 删除
|
- `DELETE /issues/{id}` - 删除
|
||||||
|
|||||||
76
app/main.py
76
app/main.py
@@ -110,26 +110,64 @@ def create_issue(issue: schemas.IssueCreate, bg: BackgroundTasks, db: Session =
|
|||||||
return db_issue
|
return db_issue
|
||||||
|
|
||||||
|
|
||||||
@app.get("/issues", response_model=List[schemas.IssueResponse])
|
import math
|
||||||
|
|
||||||
|
@app.get("/issues")
|
||||||
def list_issues(
|
def list_issues(
|
||||||
project_id: int = None,
|
project_id: int = None,
|
||||||
issue_status: str = None,
|
issue_status: str = None,
|
||||||
issue_type: str = None,
|
issue_type: str = None,
|
||||||
skip: int = 0,
|
assignee_id: int = None,
|
||||||
limit: int = 100,
|
tag: str = None,
|
||||||
|
sort_by: str = "created_at",
|
||||||
|
sort_order: str = "desc",
|
||||||
|
page: int = 1,
|
||||||
|
page_size: int = 50,
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
|
"""List issues with filtering, sorting, and pagination metadata."""
|
||||||
query = db.query(models.Issue)
|
query = db.query(models.Issue)
|
||||||
|
|
||||||
if project_id:
|
if project_id:
|
||||||
query = query.filter(models.Issue.project_id == project_id)
|
query = query.filter(models.Issue.project_id == project_id)
|
||||||
if issue_status:
|
if issue_status:
|
||||||
query = query.filter(models.Issue.status == issue_status)
|
query = query.filter(models.Issue.status == issue_status)
|
||||||
if issue_type:
|
if issue_type:
|
||||||
query = query.filter(models.Issue.issue_type == issue_type)
|
query = query.filter(models.Issue.issue_type == issue_type)
|
||||||
|
if assignee_id:
|
||||||
issues = query.offset(skip).limit(limit).all()
|
query = query.filter(models.Issue.assignee_id == assignee_id)
|
||||||
return issues
|
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])
|
@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 ============
|
# ============ Search API ============
|
||||||
|
|
||||||
@app.get("/search/issues", response_model=List[schemas.IssueResponse])
|
@app.get("/search/issues")
|
||||||
def search_issues(
|
def search_issues(
|
||||||
q: str,
|
q: str,
|
||||||
project_id: int = None,
|
project_id: int = None,
|
||||||
skip: int = 0,
|
page: int = 1,
|
||||||
limit: int = 50,
|
page_size: int = 50,
|
||||||
db: Session = Depends(get_db)
|
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(
|
query = db.query(models.Issue).filter(
|
||||||
(models.Issue.title.contains(q)) | (models.Issue.description.contains(q))
|
(models.Issue.title.contains(q)) | (models.Issue.description.contains(q))
|
||||||
)
|
)
|
||||||
if project_id:
|
if project_id:
|
||||||
query = query.filter(models.Issue.project_id == 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,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -198,3 +198,15 @@ class MilestoneResponse(MilestoneBase):
|
|||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
from_attributes = True
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user