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, + }