feat(api): sessions加is_del过滤+filehelper置顶标记,accounts加avatar(raw_json提取)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
浅念
2026-08-25 15:44:54 +08:00
parent c21758dfb2
commit d3d7146887
5 changed files with 140 additions and 6 deletions
@@ -103,6 +103,10 @@ async def list_accounts(
* WeChat: account_id=wx_id, nickname=nick_name, user_id, user_name, phone,
contacts_num, group_num, is_on_job, raw_json (verbatim upstream record).
* WhatsApp: account_id=uid (+ same projection).
``avatar`` is extracted from ``raw_json.headImageUrl`` (WeChat wxUser/list
records carry it; WhatsApp businessSalesList records have no avatar key,
so they naturally resolve to ``None`` — same key logic for both channels).
"""
wechat_q = sa.select(
WechatAccount.wx_id.label("account_id"),
@@ -171,6 +175,9 @@ async def list_accounts(
contacts_num=row.contacts_num,
group_num=row.group_num,
is_on_job=row.is_on_job,
# 头像从原始记录提取(对齐云客员工聊天窗口左侧头像,2026-08-25):
# WeChat headImageUrl;WA 原始记录无该键 -> None
avatar=(row.raw_json or {}).get("headImageUrl"),
raw_json=row.raw_json,
)
for row in rows
+24 -6
View File
@@ -64,13 +64,21 @@ async def _group_rows(session: AsyncSession, owner: str, channel: str, today_sta
async def _meta(session: AsyncSession, owner: str, channel: str, friend_ids: list[str], room_ids: list[str]):
friends: dict[str, tuple[str, str | None]] = {}
"""查 customer/chat_group 表合并昵称/头像;好友行额外带 delete_flag(is_del 过滤用)。
friends 值为 ``(name, avatar, delete_flag)``;群无删除标记,groups 值仍为
``(name, avatar)``。
"""
friends: dict[str, tuple[str, str | None, int | None]] = {}
if friend_ids:
for fid, remark, nick, real, avatar in (await session.execute(
sa.select(Customer.friend_id, Customer.friend_remark, Customer.friend_nickname, Customer.real_name, Customer.head_image_url)
for fid, remark, nick, real, avatar, delete_flag in (await session.execute(
sa.select(
Customer.friend_id, Customer.friend_remark, Customer.friend_nickname,
Customer.real_name, Customer.head_image_url, Customer.delete_flag,
)
.where(Customer.channel == channel, Customer.owner_account_id == owner, Customer.friend_id.in_(friend_ids))
)).all():
friends[fid] = (remark or nick or real or fid, avatar)
friends[fid] = (remark or nick or real or fid, avatar, delete_flag)
groups: dict[str, tuple[str, str | None]] = {}
if room_ids:
for rid, name, avatar in (await session.execute(
@@ -86,8 +94,15 @@ async def list_sessions(
session: SessionDep,
owner_account_id: Annotated[str, Query(min_length=1)],
channel: Annotated[str, Query()] = "wechat",
is_del: Annotated[int | None, Query()] = None,
) -> SessionListOut:
"""账号会话列表:好友(talker_id,room_id 空)+ 群(room_id),按 last_chat_time desc。"""
"""账号会话列表:好友(talker_id,room_id 空)+ 群(room_id),按 last_chat_time desc。
``is_del`` 可选过滤(对齐云客员工聊天窗口「删除好友」开关):按 customer.delete_flag
在 Python 侧过滤——0=未删(NULL 视为 0),1=已删;无 customer 行的 talker(如
filehelper 等系统账号)视作未删保留;群会话不受该过滤影响。不传时行为不变。
``pinned``:filehelper 会话置顶标记(对齐云客置顶区),其余好友/群恒 False。
"""
today_start = _today_start_ms()
f_rows = await _friend_rows(session, owner_account_id, channel, today_start)
g_rows = await _group_rows(session, owner_account_id, channel, today_start)
@@ -96,8 +111,11 @@ async def list_sessions(
)
sessions: list[SessionOut] = []
for r in f_rows:
name, avatar = fnames.get(r.talker_id, (r.talker_id, None))
name, avatar, delete_flag = fnames.get(r.talker_id, (r.talker_id, None, None))
if is_del is not None and (delete_flag or 0) != is_del:
continue
sessions.append(SessionOut(type="friend", talker_id=r.talker_id, name=name, avatar=avatar,
pinned=(r.talker_id == "filehelper"),
today_count=int(r.today_count or 0), last_chat_time=r.last_chat_time))
for r in g_rows:
name, avatar = gnames.get(r.room_id, (r.room_id, None))
+5
View File
@@ -112,6 +112,9 @@ class AccountOut(BaseModel):
contacts_num: int | None = None
group_num: int | None = None
is_on_job: bool | None = None
# 头像 URL — 从 raw_json.headImageUrl 提取(WeChat 有该键;WA 无 -> None;
# 对齐云客员工聊天窗口左侧头像,2026-08-25),由路由显式赋值。
avatar: str | None = None
raw_json: dict[str, Any] | None = None
@@ -408,6 +411,8 @@ class SessionOut(BaseModel):
room_id: str | None = None
name: str | None = None
avatar: str | None = None
# filehelper 会话置顶标记(对齐云客置顶区,2026-08-25);其余好友/群恒 False
pinned: bool = False
today_count: int
last_chat_time: int | None = None
@@ -293,3 +293,45 @@ async def test_accounts_pagination(make_client):
b2 = r2.json()
assert len(b2["items"]) == 1
assert b2["items"][0]["account_id"] == "wa_c@s.whatsapp.net"
async def test_accounts_avatar_from_raw_json(make_client):
"""``avatar`` 从 raw_json.headImageUrl 提取(对齐云客员工聊天窗口左侧头像)。
WeChat wxUser/list 原始记录带 ``headImageUrl``;WhatsApp businessSalesList
原始记录无该键(抓包确认,见 wa_accounts_fixture) -> None。NULL raw_json
同样 -> None。
"""
async def seed(s: AsyncSession) -> None:
s.add_all(
[
WechatAccount(
wx_id="wx_av",
nick_name="WxAv",
raw_json={
"wechatId": "wx_av",
"headImageUrl": "https://wx.qlogo.cn/a/0",
},
),
WechatAccount(wx_id="wx_noav", nick_name="WxNoAv", raw_json=None),
WhatsappAccount(
uid="wa_av@s.whatsapp.net",
nick_name="WaAv",
raw_json={"uid": "wa_av@s.whatsapp.net", "nickName": "WaAv"},
),
]
)
await s.commit()
client = await make_client(seed)
r = await client.get("/api/accounts", params={"channel": "wechat"})
assert r.status_code == 200
by_id = {it["account_id"]: it for it in r.json()["items"]}
assert by_id["wx_av"]["avatar"] == "https://wx.qlogo.cn/a/0"
assert by_id["wx_noav"]["avatar"] is None # NULL raw_json
r2 = await client.get("/api/accounts", params={"channel": "whatsapp"})
assert r2.status_code == 200
assert r2.json()["items"][0]["avatar"] is None # WA raw 无头像键
+62
View File
@@ -59,3 +59,65 @@ async def test_sessions_empty(make_client):
r = await client.get("/api/sessions", params={"owner_account_id": OWNER})
assert r.status_code == 200
assert r.json()["sessions"] == []
# ---------------------------------------------------------------------------
# is_del filter + pinned flag (2026-08-25 对齐云客员工聊天窗口)
# ---------------------------------------------------------------------------
def _msg(msg_id: str, talker: str, ts: int = 1) -> Message:
return Message(
msg_id=msg_id, channel=CH, owner_account_id=OWNER,
talker_id=talker, room_id=None, timestamp=ts,
)
async def test_sessions_is_del_filter(make_client):
"""``is_del`` 过滤:delete_flag 1=已删好友;NULL/0=未删;无 customer 行保留。
云客员工聊天窗口「删除好友」开关映射 customer.delete_flag;无 customer
行的 talker(如从未入库的系统账号)视作未删,两个方向都保留。
"""
async def seed(s: AsyncSession):
s.add_all([
Customer(friend_id="f1", channel=CH, owner_account_id=OWNER, delete_flag=0),
Customer(friend_id="f2", channel=CH, owner_account_id=OWNER, delete_flag=1),
Customer(friend_id="filehelper", channel=CH, owner_account_id=OWNER, delete_flag=None),
_msg("m1", "f1", 1),
_msg("m2", "f2", 2),
_msg("m3", "filehelper", 3),
_msg("m4", "gh0st", 4), # 无 customer 行的 talker
])
await s.commit()
client = await make_client(seed)
live = await client.get("/api/sessions", params={"owner_account_id": OWNER, "channel": CH, "is_del": 0})
deleted = await client.get("/api/sessions", params={"owner_account_id": OWNER, "channel": CH, "is_del": 1})
assert live.status_code == 200 and deleted.status_code == 200
# NULL 视为未删;无 customer 行的 talker 保留
assert sorted(s["talker_id"] for s in live.json()["sessions"]) == ["f1", "filehelper", "gh0st"]
assert [s["talker_id"] for s in deleted.json()["sessions"]] == ["f2"]
async def test_sessions_pinned_filehelper(make_client):
"""filehelper 会话 pinned=True(对齐云客置顶区),普通好友/群 False。"""
async def seed(s: AsyncSession):
s.add_all([
Customer(friend_id="f1", channel=CH, owner_account_id=OWNER, delete_flag=0),
ChatGroup(room_id="room1@chatroom", channel=CH, owner_account_id=OWNER, room_name="群一号"),
_msg("p1", "f1", 1),
_msg("p2", "filehelper", 2),
Message(msg_id="p3", channel=CH, owner_account_id=OWNER, talker_id="mx", room_id="room1@chatroom", timestamp=3),
])
await s.commit()
client = await make_client(seed)
r = await client.get("/api/sessions", params={"owner_account_id": OWNER, "channel": CH})
assert r.status_code == 200
by = {s.get("talker_id") or s.get("room_id"): s for s in r.json()["sessions"]}
assert by["filehelper"]["pinned"] is True
assert by["f1"]["pinned"] is False
assert by["room1@chatroom"]["pinned"] is False