From 628659b84949c8419e369496c9c0360cf19541ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Fri, 7 Aug 2026 18:37:26 +0800 Subject: [PATCH] =?UTF-8?q?refactor(api):=20staff-chat-stats=20=E7=A1=AE?= =?UTF-8?q?=E5=AE=9A=E6=80=A7=E6=8E=92=E5=BA=8F=20+=20last=5Fis=5Fsend=3DN?= =?UTF-8?q?one=20=E5=88=86=E6=94=AF=E6=B5=8B=E8=AF=95=20+=20=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E6=A0=87=E6=B3=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude --- .../app/api/routers/staff_chat_stats.py | 3 +- .../tests/test_api_staff_chat_stats.py | 44 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/yunke-server/app/api/routers/staff_chat_stats.py b/yunke-server/app/api/routers/staff_chat_stats.py index 2dbc620..5223258 100644 --- a/yunke-server/app/api/routers/staff_chat_stats.py +++ b/yunke-server/app/api/routers/staff_chat_stats.py @@ -36,7 +36,7 @@ def _private_window(msg, owner: str, start: int, end: int): ] -async def _aggregate(session: AsyncSession, owner: str, start: int, end: int): +async def _aggregate(session: AsyncSession, owner: str, start: int, end: int) -> tuple[list, dict[str, bool | None]]: """返回 [(talker_id, send, recv, interaction_days, last_time)] + last_is_send map。""" # 跨库日期提取:从 ms-epoch 取日期。镜像 repos.py 方言检测。 dialect = session.bind.name # 镜像 app/storage/repos.py(.name 等价 .dialect.name) @@ -56,6 +56,7 @@ async def _aggregate(session: AsyncSession, owner: str, start: int, end: int): ) .where(*base) .group_by(Message.talker_id) + .order_by(Message.talker_id) ) agg_rows = (await session.execute(agg_stmt)).all() diff --git a/yunke-server/tests/test_api_staff_chat_stats.py b/yunke-server/tests/test_api_staff_chat_stats.py index 6ed7c3d..2921e6e 100644 --- a/yunke-server/tests/test_api_staff_chat_stats.py +++ b/yunke-server/tests/test_api_staff_chat_stats.py @@ -135,3 +135,47 @@ async def test_default_window_returns_ok(make_client): r = await client.get("/api/staff-chat-stats", params={"owner_account_id": OWNER}) assert r.status_code == 200 assert "summary" in r.json() and "customers" in r.json() + + +async def test_last_is_send_none_excluded_from_rates(make_client): + """最后一条私聊 is_send IS NULL → last_is_send=None,计入 chat_count/ + interaction_days,但被 ==1/==0 同时排除(两率都不计)。""" + async def seed(session: AsyncSession) -> None: + session.add( + Message( + msg_id="n1", + channel="wechat", + owner_account_id=OWNER, + talker_id="t_n", + room_id=None, + is_send=None, + timestamp=WIN_START, + type="text", + type_raw=1, + text="unknown-dir", + ) + ) + await session.commit() + + client = await make_client(seed) + r = await client.get( + "/api/staff-chat-stats", + params={"owner_account_id": OWNER, "start": WIN_START, "end": WIN_END}, + ) + assert r.status_code == 200 + body = r.json() + customers = body["customers"] + assert len(customers) == 1 + c = customers[0] + assert c["talker_id"] == "t_n" + assert c["last_is_send"] is None + assert c["send_count"] == 0 + assert c["receive_count"] == 0 + assert c["interaction_days"] == 1 + assert body["summary"] == { + "chat_count": 1, + "effective_count": 0, + "follow_count": 0, + "last_reply_count": 0, + "no_reply_count": 0, + }