Files
yunke/yunke-server/tests/test_api_staff_chat_stats.py
T

182 lines
7.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Tests for GET /api/staff-chat-stats — 微信员工 5 数字 + 客户明细(本地 message 聚合)。"""
from __future__ import annotations
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.models import Customer, Message
OWNER = "wx_owner_1"
WIN_START = 1_700_000_000_000 # 固定 ms-epoch,确定性
WIN_END = WIN_START + 3 * 86_400_000
DAY = 86_400_000
async def _seed(session: AsyncSession) -> None:
session.add_all(
[
# 客户 A:跨 2 天双向 → effective + follow,员工最后发
Message(msg_id="a1", channel="wechat", owner_account_id=OWNER, talker_id="t_a",
room_id=None, is_send=True, timestamp=WIN_START, type="text", type_raw=1, text="x"),
Message(msg_id="a2", channel="wechat", owner_account_id=OWNER, talker_id="t_a",
room_id=None, is_send=False, timestamp=WIN_START + DAY, type="text", type_raw=1, text="y"),
Message(msg_id="a3", channel="wechat", owner_account_id=OWNER, talker_id="t_a",
room_id=None, is_send=True, timestamp=WIN_START + DAY + 1000, type="text", type_raw=1, text="z"),
# 客户 B:仅接收 → 非 effective,客户最后发(未回复)
Message(msg_id="b1", channel="wechat", owner_account_id=OWNER, talker_id="t_b",
room_id=None, is_send=False, timestamp=WIN_START + 2000, type="text", type_raw=1, text="hi"),
# 客户 C:单条员工发 → 员工最后发,1 天
Message(msg_id="c1", channel="wechat", owner_account_id=OWNER, talker_id="t_c",
room_id=None, is_send=True, timestamp=WIN_START + 3000, type="text", type_raw=1, text="c"),
# 群消息(room_id 非空)——必须被全部排除
Message(msg_id="g1", channel="wechat", owner_account_id=OWNER, talker_id="t_grp_member",
room_id="room123@chatroom", is_send=True, timestamp=WIN_START + 4000, type="text", type_raw=1, text="g"),
# 窗口外私聊——必须排除(不污染 t_a 统计)
Message(msg_id="o1", channel="wechat", owner_account_id=OWNER, talker_id="t_a",
room_id=None, is_send=True, timestamp=WIN_END + 1000, type="text", type_raw=1, text="future"),
# 其他 owner——必须排除
Message(msg_id="x1", channel="wechat", owner_account_id="wx_other", talker_id="t_a",
room_id=None, is_send=True, timestamp=WIN_START + 5000, type="text", type_raw=1, text="other"),
Customer(friend_id="t_a", channel="wechat", owner_account_id=OWNER,
friend_nickname="Alice", friend_remark="Alice备注", real_name=None),
Customer(friend_id="t_b", channel="wechat", owner_account_id=OWNER,
friend_nickname="Bob", friend_remark=None, real_name=None),
# t_c 无 customer 行 → 昵称 fallback 为 talker_id "t_c"
]
)
await session.commit()
async def test_summary_five_numbers(make_client):
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
s = r.json()["summary"]
assert s == {
"chat_count": 3, # t_a, t_b, t_c(群/窗外/他owner 全排除)
"effective_count": 1, # 仅 t_a 双向
"follow_count": 1, # 仅 t_a 跨 2 天
"last_reply_count": 2, # t_a, t_c 员工最后发
"no_reply_count": 1, # t_b 客户最后发
}
async def test_customers_fields_and_names(make_client):
client = await make_client(_seed)
r = await client.get(
"/api/staff-chat-stats",
params={"owner_account_id": OWNER, "start": WIN_START, "end": WIN_END},
)
by_id = {c["talker_id"]: c for c in r.json()["customers"]}
assert set(by_id) == {"t_a", "t_b", "t_c"}
# t_a: send=2 recv=1 days=2 last_is_send=1 last_time=a3
assert by_id["t_a"]["friend_name"] == "Alice备注" # remark 优先
assert by_id["t_a"]["send_count"] == 2
assert by_id["t_a"]["receive_count"] == 1
assert by_id["t_a"]["interaction_days"] == 2
assert by_id["t_a"]["last_is_send"] == 1
assert by_id["t_a"]["last_time"] == WIN_START + DAY + 1000
# t_b
assert by_id["t_b"]["friend_name"] == "Bob"
assert by_id["t_b"]["send_count"] == 0
assert by_id["t_b"]["receive_count"] == 1
assert by_id["t_b"]["interaction_days"] == 1
assert by_id["t_b"]["last_is_send"] == 0
# t_c 无 customer 行 → fallback
assert by_id["t_c"]["friend_name"] == "t_c"
assert by_id["t_c"]["last_is_send"] == 1
async def test_group_messages_excluded(make_client):
client = await make_client(_seed)
r = await client.get(
"/api/staff-chat-stats",
params={"owner_account_id": OWNER, "start": WIN_START, "end": WIN_END},
)
ids = {c["talker_id"] for c in r.json()["customers"]}
assert "t_grp_member" not in ids # 群消息被 room_id 过滤
async def test_date_window_and_owner_filter(make_client):
"""窗外消息与他 owner 消息不污染统计(t_a 未被 o1/x1 抬高)。"""
client = await make_client(_seed)
r = await client.get(
"/api/staff-chat-stats",
params={"owner_account_id": OWNER, "start": WIN_START, "end": WIN_END},
)
by_id = {c["talker_id"]: c for c in r.json()["customers"]}
assert by_id["t_a"]["send_count"] == 2 # 非 4(a1+a3,排除 o1+x1)
assert by_id["t_a"]["last_time"] == WIN_START + DAY + 1000 # 非 o1
async def test_empty_when_no_messages(make_client):
async def _empty(session: AsyncSession) -> None:
await session.commit()
client = await make_client(_empty)
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()
assert body["customers"] == []
assert body["summary"] == {
"chat_count": 0, "effective_count": 0, "follow_count": 0,
"last_reply_count": 0, "no_reply_count": 0,
}
async def test_default_window_returns_ok(make_client):
"""省略 start/end 走默认近 3 天;2023 固定种子在默认窗外 → 空集,仅验结构。"""
client = await make_client(_seed)
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,
}