From d3d7146887a220c87a0ecc59e2a18587fd922208 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Tue, 25 Aug 2026 15:44:54 +0800 Subject: [PATCH] =?UTF-8?q?feat(api):=20sessions=E5=8A=A0is=5Fdel=E8=BF=87?= =?UTF-8?q?=E6=BB=A4+filehelper=E7=BD=AE=E9=A1=B6=E6=A0=87=E8=AE=B0,accoun?= =?UTF-8?q?ts=E5=8A=A0avatar(raw=5Fjson=E6=8F=90=E5=8F=96)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude --- yunke-server/app/api/routers/organization.py | 7 +++ yunke-server/app/api/routers/sessions.py | 30 ++++++++-- yunke-server/app/api/schemas.py | 5 ++ yunke-server/tests/test_api_organization.py | 42 +++++++++++++ yunke-server/tests/test_api_sessions.py | 62 ++++++++++++++++++++ 5 files changed, 140 insertions(+), 6 deletions(-) diff --git a/yunke-server/app/api/routers/organization.py b/yunke-server/app/api/routers/organization.py index 8c4d31c..635f14a 100644 --- a/yunke-server/app/api/routers/organization.py +++ b/yunke-server/app/api/routers/organization.py @@ -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 diff --git a/yunke-server/app/api/routers/sessions.py b/yunke-server/app/api/routers/sessions.py index 5ee24c5..7954dce 100644 --- a/yunke-server/app/api/routers/sessions.py +++ b/yunke-server/app/api/routers/sessions.py @@ -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)) diff --git a/yunke-server/app/api/schemas.py b/yunke-server/app/api/schemas.py index e3648f4..d7661e5 100644 --- a/yunke-server/app/api/schemas.py +++ b/yunke-server/app/api/schemas.py @@ -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 diff --git a/yunke-server/tests/test_api_organization.py b/yunke-server/tests/test_api_organization.py index 56c7c56..26e9199 100644 --- a/yunke-server/tests/test_api_organization.py +++ b/yunke-server/tests/test_api_organization.py @@ -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 无头像键 diff --git a/yunke-server/tests/test_api_sessions.py b/yunke-server/tests/test_api_sessions.py index 52c53d8..5f3183b 100644 --- a/yunke-server/tests/test_api_sessions.py +++ b/yunke-server/tests/test_api_sessions.py @@ -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