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()
|
||||
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)
|
||||
|
||||
@@ -25,6 +25,10 @@ class CustomerOut(BaseModel):
|
||||
channel: str
|
||||
owner_account_id: 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
|
||||
real_name: 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["whatsapp"]["declared_total"] == 15
|
||||
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
|
||||
/** ms-epoch of most recent message */
|
||||
last_chat_time?: number | null
|
||||
/** 今日发送/接收条数(本地消息聚合,对齐云客今日口径)。 */
|
||||
send_today?: number | null
|
||||
receive_today?: number | null
|
||||
/** ms epoch of friend add time (from raw_json.addTime). */
|
||||
add_time?: number | null
|
||||
message_count?: number | null
|
||||
|
||||
@@ -450,11 +450,11 @@ onMounted(() => {
|
||||
<el-table-column label="好友标签" min-width="120">
|
||||
<template #default="{ row }">{{ contactLabelsText(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="发送消息数" width="100" align="right">
|
||||
<template #default="{ row }">{{ cellNum(row, 'sendNum') }}</template>
|
||||
<el-table-column label="发送条数" width="100" align="right">
|
||||
<template #default="{ row }">{{ row.send_today ?? cellNum(row, 'sendNum') }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="接收消息数" width="100" align="right">
|
||||
<template #default="{ row }">{{ cellNum(row, 'receiveNum') }}</template>
|
||||
<el-table-column label="接收条数" width="100" align="right">
|
||||
<template #default="{ row }">{{ row.receive_today ?? cellNum(row, 'receiveNum') }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最近联系时间" width="170">
|
||||
<template #default="{ row }">{{ cellTimeText(row, 'lastChatTime') }}</template>
|
||||
|
||||
Reference in New Issue
Block a user