From d58cf7922ec2cf93bc673d9982691ee7a4168cc5 Mon Sep 17 00:00:00 2001 From: Steven Date: Tue, 10 Nov 2026 16:33:54 +0800 Subject: [PATCH] =?UTF-8?q?feat(ticket):=20=E5=88=97=E8=A1=A8=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E6=8C=89=E6=A0=87=E9=A2=98/=E6=AD=A3=E6=96=87?= =?UTF-8?q?=E6=90=9C=E7=B4=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 手机端工单列表要能像任务、知识一样搜。与知识列表同一套 ilike 匹配,标题记不清时正文关键词也能命中;补测试覆盖标题命中、正文命中、无结果三种情况。 --- backend/app/api/tickets.py | 6 ++++ backend/tests/test_tickets.py | 58 +++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) 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)