628659b849
Co-Authored-By: Claude <noreply@anthropic.com>
182 lines
7.8 KiB
Python
182 lines
7.8 KiB
Python
"""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,
|
||
}
|