2026-09-04 18:04:05 +08:00
|
|
|
"""/api/analytics/health + /overview:E0-lite 与 A1 冒烟。"""
|
2026-09-04 15:37:13 +08:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2026-09-04 16:00:38 +08:00
|
|
|
import datetime as dt
|
|
|
|
|
|
2026-09-04 18:04:05 +08:00
|
|
|
from app.analytics import metrics
|
|
|
|
|
from app.db.models import Customer, MsgDailyStats, StatDailyStaff, WechatAccount
|
2026-09-04 16:00:38 +08:00
|
|
|
|
|
|
|
|
# naive:updated_at 列是无时区 DateTime;fromisoformat 构造不触发 DTZ 检查
|
|
|
|
|
_SEEN = "2026-09-04T12:00:00"
|
2026-09-04 15:37:13 +08:00
|
|
|
|
2026-09-04 18:04:05 +08:00
|
|
|
# overview 的 today_key 是真实今天——固定 day 键的 seed 必须 monkeypatch
|
|
|
|
|
_TODAY = 20260904
|
|
|
|
|
|
2026-09-04 15:37:13 +08:00
|
|
|
|
|
|
|
|
async def test_health_empty(make_client):
|
|
|
|
|
async def seed(session):
|
|
|
|
|
...
|
|
|
|
|
|
|
|
|
|
client = await make_client(seed)
|
|
|
|
|
r = await client.get("/api/analytics/health")
|
|
|
|
|
assert r.status_code == 200
|
|
|
|
|
body = r.json()
|
|
|
|
|
assert body["data_as_of"] is None
|
2026-09-04 16:00:38 +08:00
|
|
|
assert len(body["entities"]) == 6
|
|
|
|
|
assert all(e["last_seen"] is None for e in body["entities"])
|
2026-09-04 15:37:13 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_health_with_stats(make_client):
|
|
|
|
|
async def seed(session):
|
|
|
|
|
session.add(
|
|
|
|
|
MsgDailyStats(
|
|
|
|
|
channel="wechat",
|
|
|
|
|
owner_account_id="wx1",
|
|
|
|
|
peer_kind="friend",
|
|
|
|
|
peer_id="t1",
|
|
|
|
|
day=20260904,
|
|
|
|
|
send_cnt=1,
|
|
|
|
|
recv_cnt=2,
|
|
|
|
|
last_ts=1756900000000,
|
|
|
|
|
last_is_send=False,
|
|
|
|
|
)
|
|
|
|
|
)
|
2026-09-04 16:00:38 +08:00
|
|
|
session.add(
|
|
|
|
|
Customer(
|
|
|
|
|
friend_id="f1",
|
|
|
|
|
channel="wechat",
|
|
|
|
|
updated_at=dt.datetime.fromisoformat(_SEEN),
|
|
|
|
|
)
|
|
|
|
|
)
|
2026-09-04 15:37:13 +08:00
|
|
|
await session.commit()
|
|
|
|
|
|
|
|
|
|
client = await make_client(seed)
|
|
|
|
|
r = await client.get("/api/analytics/health")
|
|
|
|
|
assert r.status_code == 200
|
|
|
|
|
assert r.json()["data_as_of"] == 1756900000000
|
2026-09-04 16:00:38 +08:00
|
|
|
entities = {e["table"]: e["last_seen"] for e in r.json()["entities"]}
|
|
|
|
|
# 前端契约:last_seen 必须是 ISO 字符串(fromisoformat 可解析)
|
|
|
|
|
assert dt.datetime.fromisoformat(entities["customer"]) == dt.datetime.fromisoformat(
|
|
|
|
|
_SEEN
|
|
|
|
|
)
|
2026-09-04 18:04:05 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_overview_endpoint(make_client, monkeypatch):
|
|
|
|
|
monkeypatch.setattr(metrics, "today_key", lambda: _TODAY)
|
|
|
|
|
|
|
|
|
|
async def seed(session):
|
|
|
|
|
session.add(MsgDailyStats(channel="wechat", owner_account_id="wx1",
|
|
|
|
|
peer_kind="friend", peer_id="t1", day=_TODAY,
|
|
|
|
|
send_cnt=4, recv_cnt=1, last_ts=1788500000000,
|
|
|
|
|
last_is_send=True))
|
|
|
|
|
await session.commit()
|
|
|
|
|
|
|
|
|
|
client = await make_client(seed)
|
|
|
|
|
r = await client.get(
|
|
|
|
|
"/api/analytics/overview", params={"channel": "wechat", "scope": "today"}
|
|
|
|
|
)
|
|
|
|
|
assert r.status_code == 200
|
|
|
|
|
body = r.json()
|
|
|
|
|
assert body["cards"]["msg_total"] == 5
|
|
|
|
|
assert isinstance(body["trend"], list) and body["trend"]
|
|
|
|
|
# scope 非法 → 422
|
|
|
|
|
r2 = await client.get(
|
|
|
|
|
"/api/analytics/overview", params={"channel": "wechat", "scope": "year"}
|
|
|
|
|
)
|
|
|
|
|
assert r2.status_code == 422
|
2026-09-04 18:16:44 +08:00
|
|
|
# channel 非法 → 422(同一 Literal 校验路径)
|
|
|
|
|
r3 = await client.get(
|
|
|
|
|
"/api/analytics/overview", params={"channel": "sms", "scope": "today"}
|
|
|
|
|
)
|
|
|
|
|
assert r3.status_code == 422
|
2026-09-04 18:04:05 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_overview_unmapped_accounts(make_client, monkeypatch):
|
|
|
|
|
"""控制器追加:cards 带 unmapped_accounts 计数(spec §8「未映射账号按账号计并标注」的标注通道)。"""
|
|
|
|
|
monkeypatch.setattr(metrics, "today_key", lambda: _TODAY)
|
|
|
|
|
|
|
|
|
|
async def seed(session):
|
|
|
|
|
session.add(WechatAccount(wx_id="wx1", user_id="u1"))
|
|
|
|
|
# wx2 无账号表记录 → 未映射
|
|
|
|
|
session.add(MsgDailyStats(channel="wechat", owner_account_id="wx1",
|
|
|
|
|
peer_kind="friend", peer_id="t1", day=_TODAY,
|
|
|
|
|
send_cnt=2, recv_cnt=0, last_ts=1788500000000,
|
|
|
|
|
last_is_send=True))
|
|
|
|
|
session.add(MsgDailyStats(channel="wechat", owner_account_id="wx2",
|
|
|
|
|
peer_kind="friend", peer_id="t2", day=_TODAY,
|
|
|
|
|
send_cnt=1, recv_cnt=0, last_ts=1788501000000,
|
|
|
|
|
last_is_send=True))
|
|
|
|
|
await session.commit()
|
|
|
|
|
|
|
|
|
|
client = await make_client(seed)
|
|
|
|
|
r = await client.get(
|
|
|
|
|
"/api/analytics/overview", params={"channel": "wechat", "scope": "today"}
|
|
|
|
|
)
|
|
|
|
|
body = r.json()
|
|
|
|
|
assert body["cards"]["active_staff"] == 2 # u1 + wx2(按账号计)
|
|
|
|
|
assert body["cards"]["unmapped_accounts"] == 1 # wx2 标注
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_overview_unmapped_rollup(make_client, monkeypatch):
|
|
|
|
|
"""rollup 路径未映射口径与回退路径一致:当窗 user_id 不在映射值集 → 未映射。
|
|
|
|
|
|
|
|
|
|
recompute 以 amap.get(owner, owner) 落表,映射不上的 owner 以原值进
|
|
|
|
|
stat_daily_staff(user_id=wx2);account_map.values() 只含在册员工 user_id,
|
|
|
|
|
wx2 落在值集之外 → 被识别并标注。
|
|
|
|
|
"""
|
|
|
|
|
monkeypatch.setattr(metrics, "today_key", lambda: _TODAY)
|
|
|
|
|
|
|
|
|
|
async def seed(session):
|
|
|
|
|
session.add(WechatAccount(wx_id="wx1", user_id="u1"))
|
|
|
|
|
session.add(StatDailyStaff(day=_TODAY, channel="wechat", user_id="u1",
|
|
|
|
|
msg_sent=2, msg_recv=0,
|
|
|
|
|
active_friends=1, active_rooms=0))
|
|
|
|
|
session.add(StatDailyStaff(day=_TODAY, channel="wechat", user_id="wx2",
|
|
|
|
|
msg_sent=1, msg_recv=0,
|
|
|
|
|
active_friends=1, active_rooms=0))
|
|
|
|
|
session.add(MsgDailyStats(channel="wechat", owner_account_id="wx1",
|
|
|
|
|
peer_kind="friend", peer_id="t1", day=_TODAY,
|
|
|
|
|
send_cnt=2, recv_cnt=0, last_ts=1788500000000,
|
|
|
|
|
last_is_send=True))
|
|
|
|
|
await session.commit()
|
|
|
|
|
|
|
|
|
|
client = await make_client(seed)
|
|
|
|
|
r = await client.get(
|
|
|
|
|
"/api/analytics/overview", params={"channel": "wechat", "scope": "today"}
|
|
|
|
|
)
|
|
|
|
|
body = r.json()
|
|
|
|
|
assert body["cards"]["active_staff"] == 2
|
|
|
|
|
assert body["cards"]["unmapped_accounts"] == 1
|
2026-09-07 14:41:08 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_overview_custom_scope_api(make_client, monkeypatch):
|
|
|
|
|
"""C1 自定义日期:正常窗 200 + customer_assets 恒在;缺参/start>end/
|
|
|
|
|
跨度超 366 天/非法日历 全 422;闰年整 366 天放行。"""
|
|
|
|
|
monkeypatch.setattr(metrics, "today_key", lambda: _TODAY)
|
|
|
|
|
|
|
|
|
|
async def seed(session):
|
|
|
|
|
session.add(MsgDailyStats(channel="wechat", owner_account_id="wx1",
|
|
|
|
|
peer_kind="friend", peer_id="t1", day=20260815,
|
|
|
|
|
send_cnt=10, recv_cnt=0, last_ts=1786000000000,
|
|
|
|
|
last_is_send=True))
|
|
|
|
|
await session.commit()
|
|
|
|
|
|
|
|
|
|
client = await make_client(seed)
|
|
|
|
|
r = await client.get("/api/analytics/overview", params={
|
|
|
|
|
"channel": "wechat", "scope": "custom",
|
|
|
|
|
"start": 20260801, "end": 20260827,
|
|
|
|
|
})
|
|
|
|
|
assert r.status_code == 200
|
|
|
|
|
body = r.json()
|
|
|
|
|
assert body["scope"] == "custom"
|
|
|
|
|
assert len(body["days"]) == 27
|
|
|
|
|
assert body["cards"]["msg_total"] == 10
|
|
|
|
|
# B1 客户资产总览恒在(全库快照,不随 scope)
|
|
|
|
|
assert body["customer_assets"]["total"] >= 0
|
|
|
|
|
|
|
|
|
|
# 缺 start/end → 422(不让静默落到空窗)
|
|
|
|
|
r2 = await client.get("/api/analytics/overview",
|
|
|
|
|
params={"scope": "custom"})
|
|
|
|
|
assert r2.status_code == 422
|
|
|
|
|
# start > end → 422
|
|
|
|
|
r3 = await client.get("/api/analytics/overview", params={
|
|
|
|
|
"scope": "custom", "start": 20260827, "end": 20260801})
|
|
|
|
|
assert r3.status_code == 422
|
|
|
|
|
# 跨度 367 天 → 422(2024-01-01..2025-01-01 含端点 367 天)
|
|
|
|
|
r4 = await client.get("/api/analytics/overview", params={
|
|
|
|
|
"scope": "custom", "start": 20240101, "end": 20250101})
|
|
|
|
|
assert r4.status_code == 422
|
|
|
|
|
# 非法日历(2 月 30)→ 422——Query ge/le 只拦数值范围不拦日历
|
|
|
|
|
r5 = await client.get("/api/analytics/overview", params={
|
|
|
|
|
"scope": "custom", "start": 20260230, "end": 20260301})
|
|
|
|
|
assert r5.status_code == 422
|
|
|
|
|
# 闰年整年 366 天(含端点)→ 放行
|
|
|
|
|
r6 = await client.get("/api/analytics/overview", params={
|
|
|
|
|
"scope": "custom", "start": 20240101, "end": 20241231})
|
|
|
|
|
assert r6.status_code == 200
|
|
|
|
|
assert len(r6.json()["days"]) == 366
|