From fd3cf0003b59ae79fb624113098171a20ff05310 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Thu, 20 Aug 2026 13:44:39 +0800 Subject: [PATCH] =?UTF-8?q?feat(customers):=20=E5=BE=AE=E4=BF=A1=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E9=A1=B5=E8=84=9A=E6=94=B9=E4=BA=91=E5=AE=A2=E5=8F=A3?= =?UTF-8?q?=E5=BE=84(=CE=A3=E5=91=98=E5=B7=A5=E5=A5=BD=E5=8F=8B=E6=95=B0?= =?UTF-8?q?=E5=A3=B0=E6=98=8E=E5=80=BC8590,=E7=94=A8=E6=88=B7=E9=80=89B?= =?UTF-8?q?=E6=96=B9=E6=A1=88)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GET /api/customers/declared-total: Σ wechat_account.contacts_num - 页脚优先显示云客口径(回退真实total);列表内容保持去重真实行 - 云客列表声明值含其ES索引重复doc(连行id都相同无法物理区分),仅对齐计数 Co-Authored-By: Claude --- yunke-server/app/api/routers/customer.py | 24 ++++++++++++++++++++++- yunke-server/tests/test_api_customers.py | 25 ++++++++++++++++++++++++ yunkes/src/api/index.ts | 4 ++++ yunkes/src/views/CustomerList.vue | 17 ++++++++++++++-- 4 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 yunke-server/tests/test_api_customers.py diff --git a/yunke-server/app/api/routers/customer.py b/yunke-server/app/api/routers/customer.py index 6d42e55..6e0e0dd 100644 --- a/yunke-server/app/api/routers/customer.py +++ b/yunke-server/app/api/routers/customer.py @@ -18,7 +18,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.api.deps import get_session from app.api.schemas import CustomerOut, Page -from app.db.models import Customer, Staff +from app.db.models import Customer, Staff, WechatAccount router = APIRouter(prefix="/api", tags=["customers"]) @@ -27,6 +27,28 @@ PageParam = Annotated[int, Query(ge=1)] PageSizeParam = Annotated[int, Query(ge=1, le=100)] +@router.get("/customers/declared-total") +async def customers_declared_total(session: SessionDep) -> dict: + """云客口径的微信客户合计 = Σ wechat_account.contacts_num。 + + 云客列表的页脚数字含 ES 索引重复条目(实测 csf 员工声明 1999 vs 真实 + distinct 1449,连行 id 都相同的重复 doc,2026-08-20);本地表存去重真实 + 客户,此端点把「与云客页脚一字不差」的声明口径给前端展示。 + """ + row = ( + await session.execute( + sa.select( + sa.func.coalesce(sa.func.sum(WechatAccount.contacts_num), 0), + sa.func.count(WechatAccount.contacts_num), + ) + ) + ).one() + return { + "declared_total": int(row[0]), + "account_count": int(row[1]), + } + + @router.get("/customers") async def list_customers( session: SessionDep, diff --git a/yunke-server/tests/test_api_customers.py b/yunke-server/tests/test_api_customers.py new file mode 100644 index 0000000..43e7fc0 --- /dev/null +++ b/yunke-server/tests/test_api_customers.py @@ -0,0 +1,25 @@ + + +# --------------------------------------------------------------------------- +# GET /api/customers/declared-total — 云客口径合计(页脚对齐云客显示,2026-08-20) +# --------------------------------------------------------------------------- + +async def _seed_accounts_for_declared(session): + from app.db.models import WechatAccount + + session.add_all([ + WechatAccount(wx_id="w1", contacts_num=100), + WechatAccount(wx_id="w2", contacts_num=23), + WechatAccount(wx_id="w3", contacts_num=None), + ]) + await session.commit() + + +async def test_declared_total_sums_contacts_num(make_client): + """declared_total = Σ wechat_account.contacts_num(云客声明的含重复口径)。""" + client = await make_client(_seed_accounts_for_declared) + r = await client.get("/api/customers/declared-total") + assert r.status_code == 200 + body = r.json() + assert body["declared_total"] == 123 + assert body["account_count"] == 2 # 只计有值的员工 diff --git a/yunkes/src/api/index.ts b/yunkes/src/api/index.ts index c73f4af..fc160f1 100644 --- a/yunkes/src/api/index.ts +++ b/yunkes/src/api/index.ts @@ -48,6 +48,10 @@ export interface CustomerQuery { last_chat_since?: number } +export function getCustomerDeclaredTotal(): Promise<{ declared_total: number; account_count: number }> { + return getJson<{ declared_total: number; account_count: number }>('/customers/declared-total') +} + export function getCustomers(query: CustomerQuery = {}): Promise> { return getJson>( '/customers', diff --git a/yunkes/src/views/CustomerList.vue b/yunkes/src/views/CustomerList.vue index 7e786f3..32ebf25 100644 --- a/yunkes/src/views/CustomerList.vue +++ b/yunkes/src/views/CustomerList.vue @@ -3,7 +3,7 @@ import { computed, onMounted, reactive, ref, watch } from 'vue' import { useRoute, useRouter } from 'vue-router' import { ElMessage } from 'element-plus' import { Download } from '@element-plus/icons-vue' -import { getCustomers } from '@/api' +import { getCustomerDeclaredTotal, getCustomers } from '@/api' import { downloadBlob } from '@/api/client' import type { Channel, Customer } from '@/api/types' import { useChannelStore } from '@/stores/channel' @@ -39,6 +39,18 @@ function freshTabState(): TabState { return { items: [], total: 0, page: 1, pageSize: 10, loading: false, errorMsg: null, loaded: false } } +/** 云客口径微信客户合计(Σ员工好友数声明值,含云客索引重复;页脚对齐云客)。 */ +const declaredTotal = ref(null) + +async function loadDeclaredTotal(): Promise { + try { + const r = await getCustomerDeclaredTotal() + declaredTotal.value = r.declared_total + } catch { + declaredTotal.value = null // 回退真实 total + } +} + const route = useRoute() const router = useRouter() const channelStore = useChannelStore() @@ -346,6 +358,7 @@ watch( onMounted(() => { loadTab(activeTab.value) + loadDeclaredTotal() }) @@ -355,7 +368,7 @@ onMounted(() => {

{{ activeTab === 'wechat' ? '微信' : 'WhatsApp' }}客户列表

- 共 {{ tabState[activeTab].total }} 条 + 共 {{ activeTab === 'wechat' && declaredTotal !== null ? declaredTotal : tabState[activeTab].total }} 条