feat(customers): 发送/接收列改今日口径(对齐云客;本地消息表Asia/Shanghai今日聚合)
- CustomerOut 加 send_today/receive_today(路由赋值,非ORM列) - /api/customers 每页一次 GROUP BY(owner IN 走复合索引前缀) - 前端列优先今日值,无消息数据回退原raw字段;列名对齐云客「发送条数/接收条数」 - TDD:今日3发2收+昨日不计入+无消息客户0 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -112,5 +112,43 @@ async def list_customers(
|
|||||||
)
|
)
|
||||||
|
|
||||||
rows = (await session.execute(list_stmt)).scalars().all()
|
rows = (await session.execute(list_stmt)).scalars().all()
|
||||||
items = [CustomerOut.model_validate(row) for row in rows]
|
|
||||||
|
# 今日发送/接收条数(对齐云客口径):本地消息表按 Asia/Shanghai 今日聚合,
|
||||||
|
# 一次 GROUP BY 覆盖本页(owner IN 走 ix_message_channel_owner_talker_time 前缀)
|
||||||
|
import datetime as _dt
|
||||||
|
|
||||||
|
from app.db.models import Message
|
||||||
|
|
||||||
|
sh_now = _dt.datetime.now(_dt.UTC) + _dt.timedelta(hours=8)
|
||||||
|
today_start_ms = int(
|
||||||
|
(sh_now.replace(hour=0, minute=0, second=0, microsecond=0) - _dt.timedelta(hours=8)).timestamp() * 1000
|
||||||
|
)
|
||||||
|
owners = sorted({r.owner_account_id for r in rows if r.owner_account_id})
|
||||||
|
today_counts: dict[tuple[str, str], tuple[int, int]] = {}
|
||||||
|
if owners:
|
||||||
|
stat_rows = (
|
||||||
|
await session.execute(
|
||||||
|
sa.select(
|
||||||
|
Message.owner_account_id,
|
||||||
|
Message.talker_id,
|
||||||
|
sa.func.sum(sa.case((Message.is_send.is_(True), 1), else_=0)),
|
||||||
|
sa.func.sum(sa.case((Message.is_send.is_(False), 1), else_=0)),
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
Message.channel == (channel or "wechat"),
|
||||||
|
Message.owner_account_id.in_(owners),
|
||||||
|
Message.timestamp >= today_start_ms,
|
||||||
|
)
|
||||||
|
.group_by(Message.owner_account_id, Message.talker_id)
|
||||||
|
)
|
||||||
|
).all()
|
||||||
|
today_counts = {(o, t): (int(s or 0), int(rc or 0)) for o, t, s, rc in stat_rows}
|
||||||
|
|
||||||
|
items = []
|
||||||
|
for row in rows:
|
||||||
|
item = CustomerOut.model_validate(row)
|
||||||
|
s, rc = today_counts.get((row.owner_account_id or "", row.friend_id or ""), (0, 0))
|
||||||
|
item.send_today = s
|
||||||
|
item.receive_today = rc
|
||||||
|
items.append(item)
|
||||||
return Page(items=items, total=total, page=page, page_size=page_size)
|
return Page(items=items, total=total, page=page, page_size=page_size)
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ class CustomerOut(BaseModel):
|
|||||||
channel: str
|
channel: str
|
||||||
owner_account_id: str | None = None
|
owner_account_id: str | None = None
|
||||||
friend_nickname: str | None = None
|
friend_nickname: str | None = None
|
||||||
|
# 今日发送/接收条数(本地消息表按 Asia/Shanghai 今日聚合,对齐云客口径;
|
||||||
|
# 非 ORM 列,由路由赋值,2026-08-22)
|
||||||
|
send_today: int | None = None
|
||||||
|
receive_today: int | None = None
|
||||||
friend_remark: str | None = None
|
friend_remark: str | None = None
|
||||||
real_name: str | None = None
|
real_name: str | None = None
|
||||||
phone: str | None = None
|
phone: str | None = None
|
||||||
|
|||||||
@@ -35,3 +35,41 @@ async def test_declared_total_sums_contacts_num(make_client):
|
|||||||
assert body["wechat"]["account_count"] == 2 # 只计有值的员工
|
assert body["wechat"]["account_count"] == 2 # 只计有值的员工
|
||||||
assert body["whatsapp"]["declared_total"] == 15
|
assert body["whatsapp"]["declared_total"] == 15
|
||||||
assert body["whatsapp"]["account_count"] == 2
|
assert body["whatsapp"]["account_count"] == 2
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# send_today / receive_today — 今日发送/接收条数(对齐云客口径,2026-08-22)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async def _seed_today_counts(session):
|
||||||
|
import datetime as dt
|
||||||
|
|
||||||
|
from app.db.models import Customer as C, Message as M
|
||||||
|
|
||||||
|
now_ms = int(dt.datetime.now(dt.UTC).timestamp() * 1000)
|
||||||
|
yesterday_ms = now_ms - 86_400_000
|
||||||
|
session.add_all([
|
||||||
|
C(channel="wechat", owner_account_id="o1", friend_id="f1"),
|
||||||
|
C(channel="wechat", owner_account_id="o1", friend_id="f2"),
|
||||||
|
])
|
||||||
|
# f1: 今日 3发2收 + 昨日 5发(不应计入)
|
||||||
|
for i in range(3):
|
||||||
|
session.add(M(msg_id=f"t1s{i}", channel="wechat", owner_account_id="o1", talker_id="f1", is_send=True, timestamp=now_ms - i * 1000))
|
||||||
|
for i in range(2):
|
||||||
|
session.add(M(msg_id=f"t1r{i}", channel="wechat", owner_account_id="o1", talker_id="f1", is_send=False, timestamp=now_ms - i * 1000))
|
||||||
|
for i in range(5):
|
||||||
|
session.add(M(msg_id=f"t0s{i}", channel="wechat", owner_account_id="o1", talker_id="f1", is_send=True, timestamp=yesterday_ms))
|
||||||
|
# f2: 无消息
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_customers_today_send_receive(make_client):
|
||||||
|
"""发送/接收今日列 = 本地消息表按 Asia/Shanghai 今日聚合。"""
|
||||||
|
client = await make_client(_seed_today_counts)
|
||||||
|
r = await client.get("/api/customers", params={"channel": "wechat"})
|
||||||
|
assert r.status_code == 200
|
||||||
|
by_fid = {it["friend_id"]: it for it in r.json()["items"]}
|
||||||
|
assert by_fid["f1"]["send_today"] == 3
|
||||||
|
assert by_fid["f1"]["receive_today"] == 2
|
||||||
|
assert by_fid["f2"]["send_today"] == 0
|
||||||
|
assert by_fid["f2"]["receive_today"] == 0
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ export interface Customer {
|
|||||||
head_image_url?: string | null
|
head_image_url?: string | null
|
||||||
/** ms-epoch of most recent message */
|
/** ms-epoch of most recent message */
|
||||||
last_chat_time?: number | null
|
last_chat_time?: number | null
|
||||||
|
/** 今日发送/接收条数(本地消息聚合,对齐云客今日口径)。 */
|
||||||
|
send_today?: number | null
|
||||||
|
receive_today?: number | null
|
||||||
/** ms epoch of friend add time (from raw_json.addTime). */
|
/** ms epoch of friend add time (from raw_json.addTime). */
|
||||||
add_time?: number | null
|
add_time?: number | null
|
||||||
message_count?: number | null
|
message_count?: number | null
|
||||||
|
|||||||
@@ -450,11 +450,11 @@ onMounted(() => {
|
|||||||
<el-table-column label="好友标签" min-width="120">
|
<el-table-column label="好友标签" min-width="120">
|
||||||
<template #default="{ row }">{{ contactLabelsText(row) }}</template>
|
<template #default="{ row }">{{ contactLabelsText(row) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="发送消息数" width="100" align="right">
|
<el-table-column label="发送条数" width="100" align="right">
|
||||||
<template #default="{ row }">{{ cellNum(row, 'sendNum') }}</template>
|
<template #default="{ row }">{{ row.send_today ?? cellNum(row, 'sendNum') }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="接收消息数" width="100" align="right">
|
<el-table-column label="接收条数" width="100" align="right">
|
||||||
<template #default="{ row }">{{ cellNum(row, 'receiveNum') }}</template>
|
<template #default="{ row }">{{ row.receive_today ?? cellNum(row, 'receiveNum') }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="最近联系时间" width="170">
|
<el-table-column label="最近联系时间" width="170">
|
||||||
<template #default="{ row }">{{ cellTimeText(row, 'lastChatTime') }}</template>
|
<template #default="{ row }">{{ cellTimeText(row, 'lastChatTime') }}</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user