diff --git a/backend/app/api/tickets.py b/backend/app/api/tickets.py index aa69475..f64958a 100644 --- a/backend/app/api/tickets.py +++ b/backend/app/api/tickets.py @@ -601,6 +601,7 @@ async def list_tickets( session: SessionDep, role: Annotated[str, Query(description="assigned|requested|all")] = "assigned", status: Annotated[list[str] | None, Query(description="按状态过滤,可重复")] = None, + q: Annotated[str | None, Query(description="搜标题或正文")] = None, limit: Annotated[int, Query(ge=1, le=100)] = 20, cursor: Annotated[str | None, Query()] = None, ) -> dict: @@ -622,6 +623,11 @@ async def list_tickets( if status: stmt = stmt.where(Item.status.in_(status)) + if q and q.strip(): + # 与知识列表同一套模糊匹配:手机上找一张单,标题记不清时正文里的关键词也能命中 + like = f"%{q.strip()}%" + stmt = stmt.where(or_(Item.title.ilike(like), Item.body_md.ilike(like))) + stmt = stmt.order_by(Item.updated_at.desc(), Item.id.desc()).limit(limit + 1) if parsed_cursor is not None: stmt = stmt.where( diff --git a/backend/tests/test_tickets.py b/backend/tests/test_tickets.py index f066954..777ce70 100644 --- a/backend/tests/test_tickets.py +++ b/backend/tests/test_tickets.py @@ -315,6 +315,64 @@ async def test_ticket_list_scopes(client: AsyncClient) -> None: assert empty["tickets"] == [] +@pytest.mark.asyncio +async def test_ticket_list_search(client: AsyncClient) -> None: + """手机端工单列表的搜索框:标题和正文都能命中,不相关的不出现。""" + owner = await login(client) + ws = await create_workspace(client, owner, "工单搜索") + helper = await _member(client, owner, ws) + task = await _task(client, owner, ws["id"], "支付回调重复") + unwrap( + await client.patch( + f"{V1}/workspaces/{ws['id']}/items/{task['id']}", + json={"body_md": "网关在 23:00 之后重复回调了三次", "version": task["version"]}, + headers=owner["headers"], + ) + ) + unwrap( + await client.post( + f"{V1}/workspaces/{ws['id']}/items/{task['id']}/promote", + json={"assignee_id": helper["user_id"]}, + headers=owner["headers"], + ) + ) + other = await _task(client, owner, ws["id"], "登录页白屏") + unwrap( + await client.post( + f"{V1}/workspaces/{ws['id']}/items/{other['id']}/promote", + json={"assignee_id": helper["user_id"]}, + headers=owner["headers"], + ) + ) + + by_title = unwrap( + await client.get( + f"{V1}/workspaces/{ws['id']}/tickets", + params={"role": "all", "q": "支付"}, + headers=helper["headers"], + ) + ) + assert [t["title"] for t in by_title["tickets"]] == ["支付回调重复"] + + by_body = unwrap( + await client.get( + f"{V1}/workspaces/{ws['id']}/tickets", + params={"role": "all", "q": "网关"}, + headers=helper["headers"], + ) + ) + assert [t["title"] for t in by_body["tickets"]] == ["支付回调重复"] + + miss = unwrap( + await client.get( + f"{V1}/workspaces/{ws['id']}/tickets", + params={"role": "all", "q": "不存在的关键词"}, + headers=helper["headers"], + ) + ) + assert miss["tickets"] == [] + + @pytest.mark.asyncio async def test_cross_tenant_ticket_invisible(client: AsyncClient) -> None: alice = await login(client)