From 253be99eca80f033332d91f7a7e7d27bb094c5b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Thu, 10 Sep 2026 14:11:13 +0800 Subject: [PATCH] =?UTF-8?q?fix(groups):=20=E5=BE=AE=E4=BF=A1=E7=BE=A4?= =?UTF-8?q?=E6=8E=92=E5=BA=8F=E5=A4=8D=E5=88=BB=E4=BA=91=E5=AE=A2+?= =?UTF-8?q?=E7=BE=A4=E8=AF=A6=E6=83=85=E5=8E=BB=E6=8E=89=E9=87=8D=E5=A4=8D?= =?UTF-8?q?=E7=9A=84=E3=80=8C=E6=88=90=E5=91=98=E5=88=97=E8=A1=A8=E3=80=8D?= =?UTF-8?q?=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户报:①群详情有成员墙后「成员列表」原始串字段重复;②排序与云客不一致。 - 排序:云客「微信群统计」真机判别=员工分组(云客员工列表序)+ 组内 chatRoomId 字符串升序——/api/groups 微信路径改 JOIN wechat_account 按 list_order(NULL 垫底按 owner asc)+room_id asc; WA 保持最近活跃优先 - 群详情:memberIds 从「其他字段」兜底区排除(成员墙已替代,不再 以原始 wxid 串重复出现) 726 测试绿(默认排序断言更新为员工分组序)。 Co-Authored-By: Claude Code --- yunke-server/app/api/routers/groups.py | 33 ++++++++++++++++++++------ yunke-server/tests/test_api_groups.py | 9 ++++--- yunkes/src/utils/groupFields.ts | 6 ++++- 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/yunke-server/app/api/routers/groups.py b/yunke-server/app/api/routers/groups.py index 0e823c1..0794290 100644 --- a/yunke-server/app/api/routers/groups.py +++ b/yunke-server/app/api/routers/groups.py @@ -101,14 +101,33 @@ async def list_groups( list_stmt = sa.select(ChatGroup) if conditions: list_stmt = list_stmt.where(*conditions) - list_stmt = ( - list_stmt.order_by( - sa.nullslast(ChatGroup.last_chat_time.desc()), - ChatGroup.room_id.asc(), + if (channel or "wechat") == "wechat": + # 云客「微信群统计」同款排序(2026-09-10 真机判别):员工分组—— + # 按云客员工列表序(wechat_account.list_order,NULL 垫底按 wx_id)、 + # 组内按 room_id(chatRoomId)字符串升序。WA 保持最近活跃优先。 + list_stmt = ( + list_stmt.outerjoin( + WechatAccount, + WechatAccount.wx_id == ChatGroup.owner_account_id, + ) + .order_by( + WechatAccount.list_order.is_(None), + WechatAccount.list_order.asc(), + ChatGroup.owner_account_id.asc(), + ChatGroup.room_id.asc(), + ) + .limit(page_size) + .offset((page - 1) * page_size) + ) + else: + list_stmt = ( + list_stmt.order_by( + sa.nullslast(ChatGroup.last_chat_time.desc()), + ChatGroup.room_id.asc(), + ) + .limit(page_size) + .offset((page - 1) * page_size) ) - .limit(page_size) - .offset((page - 1) * page_size) - ) rows = (await session.execute(list_stmt)).scalars().all() # ---- 员工微信/员工信息列(wechat_account/staff 关联,本页行集) ---- diff --git a/yunke-server/tests/test_api_groups.py b/yunke-server/tests/test_api_groups.py index d949a67..4522c12 100644 --- a/yunke-server/tests/test_api_groups.py +++ b/yunke-server/tests/test_api_groups.py @@ -64,17 +64,20 @@ async def _seed(session: AsyncSession) -> None: async def test_default_list_returns_all_ordered_desc(make_client): - """No filters -> 3 groups, ordered by ``last_chat_time desc`` (nulls last).""" + """No filters -> 3 groups;微信排序=员工分组(list_order NULL 按 owner asc) + +组内 room_id 字符串升序(云客群统计同款,2026-09-10)。 + seed 无 wechat_account 行 → list_order 全 NULL → owner 字符串序: + wa_owner_1 < wx_owner_1 < wx_owner_2 → [wa_room_x, room_a, room_b]。 + """ client = await make_client(_seed) r = await client.get("/api/groups") assert r.status_code == 200 body = r.json() assert body["total"] == 3 - # 2000, 1000, null -> [room_a, room_b, wa_room_x] assert [it["room_id"] for it in body["items"]] == [ + "wa_room_x@s.whatsapp.net", "room_a@chatroom", "room_b@chatroom", - "wa_room_x@s.whatsapp.net", ] for key in ( "room_id", diff --git a/yunkes/src/utils/groupFields.ts b/yunkes/src/utils/groupFields.ts index f3edc25..0266d30 100644 --- a/yunkes/src/utils/groupFields.ts +++ b/yunkes/src/utils/groupFields.ts @@ -105,7 +105,11 @@ export const GROUP_FIELD_GROUPS: GroupFieldGroup[] = [ ] /** Every key already surfaced via a group (so 其他 can skip duplicates). */ -const KNOWN_KEYS = new Set(GROUP_FIELD_GROUPS.flatMap((g) => g.keys)) +const KNOWN_KEYS = new Set([ + ...GROUP_FIELD_GROUPS.flatMap((g) => g.keys), + // 原始 wxid 串由群详情成员墙替代,不在任何区(含「其他」)显示(2026-09-10) + 'memberIds', +]) /** Resolve the 中文 label for a raw_json key (falls back to the key). */ export function fieldLabel(key: string): string {