fix(staff-stats): 统计tab新客户/删除客户/红包昵称切 link——共享好友不再漏计
全项目同型审计结论的收尾项:统计分析 tab 按 owner 查全局单行档案表, 共享好友(档案挂别的员工名下)不计入—— - _new_customers(新客户):主体 Customer → CustomerAccountLink(add_time 窗+首条消息兜底 JOIN 平移到 link.owner);数字变大=对齐云客按员工口径 - _del_customers(删除客户):_flag_customers 参数化 model,del 走 link (delete_flag per-relation);blocked 保留档案口径(is_block 未镜像进 link,已知偏差=他人员工名下拉黑不计,注释标明) - _red_transfer 昵称:link 优先档案兜底(同 sessions._meta 模式) 审计同时确认无需修:群侧(chat_group 复合主键天然多对多)、搜索/长语音 (按 friend 全局直查)、analytics(全局口径)、WA(与微信共用同批修复, 12,578 行 link 已回填,1,967 共享客户在册;无昵称行=WA 联系人本身没存 名字,与云客一致显示号码)。 644 测试绿(新增共享好友新客户判别测试;存量测试种子补 link 行镜像 生产写路径)。 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,14 @@ import time
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.db.models import ChatGroup, Customer, Message, PhoneCall, WechatAccount
|
||||
from app.db.models import (
|
||||
ChatGroup,
|
||||
Customer,
|
||||
CustomerAccountLink,
|
||||
Message,
|
||||
PhoneCall,
|
||||
WechatAccount,
|
||||
)
|
||||
|
||||
OWNER = "wxid_o"
|
||||
UID = "U32HEX" # 员工 userId(32位HEX)——phone_call.user_id 存的是它,不是 wxId!
|
||||
@@ -39,6 +46,11 @@ async def _seed_counts(s: AsyncSession) -> None:
|
||||
# 删除/拉黑(现存量口径,忽略窗口)
|
||||
s.add(Customer(friend_id="c9", channel="wechat", owner_account_id=OWNER, delete_flag=1))
|
||||
s.add(Customer(friend_id="c8", channel="wechat", owner_account_id=OWNER, is_block=True))
|
||||
# link 行(生产抓取经 upsert_customer 成对写;新客户/删除统计读 link)
|
||||
s.add(CustomerAccountLink(channel="wechat", owner_account_id=OWNER, friend_id="c1", add_time=IN))
|
||||
s.add(CustomerAccountLink(channel="wechat", owner_account_id=OWNER, friend_id="c2"))
|
||||
s.add(CustomerAccountLink(channel="wechat", owner_account_id=OWNER, friend_id="c3", add_time=1_000))
|
||||
s.add(CustomerAccountLink(channel="wechat", owner_account_id=OWNER, friend_id="c9", delete_flag=1))
|
||||
# 语音通话: user_id 是 32HEX 非 wxId!另 seed 一条直存 wxId 的(不计,锁死 join 口径)
|
||||
s.add(PhoneCall(call_id="pc1", channel="voice_call", user_id=UID,
|
||||
start_call_time=IN + 4, call_number="c1"))
|
||||
@@ -78,6 +90,9 @@ async def test_counts_default_window_last_7_days(make_client):
|
||||
s.add(Customer(friend_id="c_now", channel="wechat", owner_account_id=OWNER, add_time=now))
|
||||
s.add(Customer(friend_id="c_old", channel="wechat", owner_account_id=OWNER,
|
||||
add_time=now - 8 * DAY))
|
||||
s.add(CustomerAccountLink(channel="wechat", owner_account_id=OWNER, friend_id="c_now", add_time=now))
|
||||
s.add(CustomerAccountLink(channel="wechat", owner_account_id=OWNER, friend_id="c_old",
|
||||
add_time=now - 8 * DAY))
|
||||
s.add(PhoneCall(call_id="pc_now", channel="voice_call", user_id=UID,
|
||||
start_call_time=now, call_number="c_now"))
|
||||
s.add(PhoneCall(call_id="pc_old", channel="voice_call", user_id=UID,
|
||||
@@ -127,6 +142,12 @@ async def test_detail_new_customers_fields(make_client):
|
||||
s.add(Customer(friend_id="c2", channel="wechat", owner_account_id=OWNER, add_time=None,
|
||||
friend_nickname="兜底客"))
|
||||
s.add(Customer(friend_id="c3", channel="wechat", owner_account_id=OWNER, add_time=1_000))
|
||||
s.add(CustomerAccountLink(channel="wechat", owner_account_id=OWNER, friend_id="c1", add_time=IN,
|
||||
friend_nickname="牛牛", friend_remark="大客户",
|
||||
head_image_url="http://a/1.png"))
|
||||
s.add(CustomerAccountLink(channel="wechat", owner_account_id=OWNER, friend_id="c2",
|
||||
friend_nickname="兜底客"))
|
||||
s.add(CustomerAccountLink(channel="wechat", owner_account_id=OWNER, friend_id="c3", add_time=1_000))
|
||||
s.add(Message(msg_id="m1", channel="wechat", owner_account_id=OWNER, talker_id="c2",
|
||||
timestamp=IN - 5))
|
||||
await s.commit()
|
||||
@@ -238,6 +259,9 @@ async def test_detail_del_and_blocked_fields(make_client):
|
||||
s.add(Customer(friend_id="c_ok", channel="wechat", owner_account_id=OWNER))
|
||||
s.add(Customer(friend_id="c_other", channel="wechat", owner_account_id="wxid_other",
|
||||
delete_flag=1))
|
||||
# 删除读 link:c_del 补 link 行;is_block 保持档案(c_blk 不补)
|
||||
s.add(CustomerAccountLink(channel="wechat", owner_account_id=OWNER, friend_id="c_del",
|
||||
delete_flag=1, friend_nickname="已删客"))
|
||||
s.add(Customer(friend_id="c_fine", channel="wechat", owner_account_id=OWNER,
|
||||
delete_flag=0, is_block=False))
|
||||
await s.commit()
|
||||
@@ -394,3 +418,32 @@ async def test_new_customers_fallback_first_msg_before_window(make_client):
|
||||
assert r_count.status_code == r_detail.status_code == 200
|
||||
assert r_count.json()["new_customers"] == 0
|
||||
assert r_detail.json()["rows"] == []
|
||||
|
||||
|
||||
async def test_new_customers_counts_shared_friend_via_link(make_client):
|
||||
"""共享好友(档案挂别的员工名下)凭 link 行计入新客户(2026-09-09 口径修复)。
|
||||
|
||||
旧档案口径下 c_shared 对 OWNER 不可见(add_time 在窗内也不计);切 link
|
||||
后按 per-owner 关系计数,与云客按员工统计一致。
|
||||
"""
|
||||
|
||||
async def seed(s: AsyncSession) -> None:
|
||||
s.add(WechatAccount(wx_id=OWNER, user_id=UID))
|
||||
# 档案行(last-wins)挂在别的员工名下
|
||||
s.add(Customer(friend_id="c_shared", channel="wechat",
|
||||
owner_account_id="wxid_other", add_time=IN))
|
||||
# OWNER 名下的 link 关系
|
||||
s.add(CustomerAccountLink(channel="wechat", owner_account_id=OWNER,
|
||||
friend_id="c_shared", add_time=IN))
|
||||
await s.commit()
|
||||
|
||||
client = await make_client(seed)
|
||||
r = await client.get(
|
||||
"/api/staff-stats",
|
||||
params={"wechat_id": OWNER, "kind": "new_customers",
|
||||
"start": WIN_START, "end": WIN_END},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert [row["talker_id"] for row in body["rows"]] == ["c_shared"]
|
||||
assert len(body["rows"]) == 1
|
||||
|
||||
Reference in New Issue
Block a user