diff --git a/yunke-server/app/api/routers/groups.py b/yunke-server/app/api/routers/groups.py index b176cef..b5c2e69 100644 --- a/yunke-server/app/api/routers/groups.py +++ b/yunke-server/app/api/routers/groups.py @@ -1,8 +1,9 @@ """GET /api/groups, /api/groups/{room_id}/members — chat-group browse endpoints. * ``GET /api/groups`` -> ``Page[GroupOut]`` filtered by ``channel`` / - ``owner_account_id``, ordered by ``last_chat_time desc`` (nulls last) so - recently-active groups surface first. + ``owner_account_id`` / ``user_id`` (the latter mapping to the + ``owner_user_id`` column), ordered by ``last_chat_time desc`` (nulls last) + so recently-active groups surface first. * ``GET /api/groups/{room_id}/members`` -> ``list[GroupMemberOut]`` — the members of a single group (small set, no pagination needed). """ @@ -33,13 +34,22 @@ async def list_groups( page_size: PageSizeParam = 20, channel: Annotated[str | None, Query()] = None, owner_account_id: Annotated[str | None, Query()] = None, + user_id: Annotated[str | None, Query()] = None, ) -> Page[GroupOut]: - """List chat groups with optional ``channel`` / ``owner_account_id`` filters.""" + """List chat groups with optional ``channel`` / ``owner_account_id`` / + ``user_id`` filters. + + ``user_id`` maps to the ``ChatGroup.owner_user_id`` column (the staff + ``userId`` who owns the chatroom account), mirroring the ``user_id`` + filter on ``GET /api/customers``. + """ conditions = [] if channel is not None: conditions.append(ChatGroup.channel == channel) if owner_account_id is not None: conditions.append(ChatGroup.owner_account_id == owner_account_id) + if user_id is not None: + conditions.append(ChatGroup.owner_user_id == user_id) total_stmt = sa.select(sa.func.count()).select_from(ChatGroup) if conditions: diff --git a/yunke-server/tests/test_api_groups.py b/yunke-server/tests/test_api_groups.py index 2c1df6d..5ad1e6e 100644 --- a/yunke-server/tests/test_api_groups.py +++ b/yunke-server/tests/test_api_groups.py @@ -21,6 +21,7 @@ async def _seed(session: AsyncSession) -> None: room_name="Team A", owner_account_id="wx_owner_1", owner_account_nickname="WxOne", + owner_user_id="u_wx_1", member_count=2, message_count=10, last_chat_time=2000, @@ -30,6 +31,7 @@ async def _seed(session: AsyncSession) -> None: channel="wechat", room_name="Team B", owner_account_id="wx_owner_2", + owner_user_id="u_wx_2", member_count=1, last_chat_time=1000, ), @@ -38,6 +40,7 @@ async def _seed(session: AsyncSession) -> None: channel="whatsapp", room_name="WA Team", owner_account_id="wa_owner_1", + owner_user_id="u_wa_1", member_count=3, last_chat_time=None, ), @@ -176,6 +179,26 @@ async def test_filter_by_owner_account_id(make_client): assert body["items"][0]["room_id"] == "room_a@chatroom" +async def test_filter_by_user_id(make_client): + """``?user_id=u_wx_1`` -> only Team A (param maps to owner_user_id column).""" + client = await make_client(_seed) + r = await client.get("/api/groups", params={"user_id": "u_wx_1"}) + assert r.status_code == 200 + body = r.json() + assert body["total"] == 1 + assert body["items"][0]["room_id"] == "room_a@chatroom" + # Sanity: a different user_id matches a different single group. + r2 = await client.get("/api/groups", params={"user_id": "u_wa_1"}) + assert r2.status_code == 200 + body2 = r2.json() + assert body2["total"] == 1 + assert body2["items"][0]["room_id"] == "wa_room_x@s.whatsapp.net" + # Sanity: an unknown user_id matches nothing. + r3 = await client.get("/api/groups", params={"user_id": "nobody"}) + assert r3.status_code == 200 + assert r3.json()["total"] == 0 + + async def test_pagination(make_client): """``page_size=1`` -> 1 item, total 3.""" client = await make_client(_seed) diff --git a/yunkes/src/api/index.ts b/yunkes/src/api/index.ts index ee5a7f4..e78a3de 100644 --- a/yunkes/src/api/index.ts +++ b/yunkes/src/api/index.ts @@ -134,6 +134,7 @@ export function getAccounts(query: AccountQuery = {}): Promise> { export interface GroupQuery { channel?: Channel | 'all' owner_account_id?: string + user_id?: string page?: number page_size?: number } @@ -144,6 +145,7 @@ export function getGroups(query: GroupQuery = {}): Promise> { clean({ channel: query.channel === 'all' ? undefined : query.channel, owner_account_id: query.owner_account_id, + user_id: query.user_id, page: query.page ?? 1, page_size: query.page_size ?? 20, }), diff --git a/yunkes/src/views/CustomerList.vue b/yunkes/src/views/CustomerList.vue index 5c7553e..63de9a8 100644 --- a/yunkes/src/views/CustomerList.vue +++ b/yunkes/src/views/CustomerList.vue @@ -33,6 +33,10 @@ const departmentId = computed(() => route.query.department_id ? String(route.query.department_id) : null, ) +const ownerId = computed(() => + route.query.owner_account_id ? String(route.query.owner_account_id) : null, +) + const departmentName = computed(() => { if (!departmentId.value) return null return org.findNode(departmentId.value)?.label ?? departmentId.value @@ -96,6 +100,7 @@ async function load(): Promise { page: page.value, page_size: pageSize.value, department_id: departmentId.value ?? undefined, + owner_account_id: ownerId.value ?? undefined, }) customers.value = res.items total.value = res.total @@ -118,9 +123,16 @@ function handlePageSizeChange(s: number): void { load() } -// React to top-bar channel + dept tree selection. +/** Drop the ``owner_account_id`` query param while preserving any others. */ +function clearOwnerFilter(): void { + const { owner_account_id: _drop, ...rest } = route.query + void _drop + router.replace({ query: rest }) +} + +// React to top-bar channel + dept tree selection + owner_account_id query. watch( - () => [channelStore.channel, departmentId.value] as const, + () => [channelStore.channel, departmentId.value, ownerId.value] as const, () => { page.value = 1 load() @@ -174,6 +186,7 @@ onMounted(load) 共 {{ total }} 条 + · 渠道:{{ channelLabelCurrent }} @@ -200,6 +213,15 @@ onMounted(load) > 清除部门筛选 + + 清除员工筛选 + diff --git a/yunkes/src/views/GroupList.vue b/yunkes/src/views/GroupList.vue index 9929604..8329496 100644 --- a/yunkes/src/views/GroupList.vue +++ b/yunkes/src/views/GroupList.vue @@ -1,6 +1,6 @@