From c9df6066f5e5c6564df125eae9c664779977c70e Mon Sep 17 00:00:00 2001 From: v6ole Date: Mon, 17 Aug 2026 09:06:36 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BA=AE=E7=81=AF=E8=A1=A8=E9=87=8D?= =?UTF-8?q?=E5=A4=8D=E5=8D=A1=E7=89=87=20=E2=80=94=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=90=8C=E5=AE=A2=E6=88=B7=E7=BB=8F=E7=90=86=E5=90=8C=E6=97=A5?= =?UTF-8?q?=E5=A4=9A=E6=9D=A1=E6=8B=9C=E8=AE=BF=E5=AF=BC=E8=87=B4=E7=9A=84?= =?UTF-8?q?=20fan-out?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 原查询用 outerjoin(Visit, visit_date == MAX(visit_date)) 获取拜访方式, 当同一天有多条拜访(协同拜访)时会扩展成多行,导致亮灯表同一客户显示多张卡片 - 改用 DISTINCT ON (manager_id, customer_id) 子查询,保证每对(经理,客户) 只取最新一条拜访,消除重复 - 移除不再使用的 func 导入 Co-Authored-By: Claude --- backend/app/services/light_board.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/backend/app/services/light_board.py b/backend/app/services/light_board.py index bc78c55..7e0d234 100644 --- a/backend/app/services/light_board.py +++ b/backend/app/services/light_board.py @@ -9,7 +9,7 @@ Status (rolling 30-day window): from datetime import date, timedelta from uuid import UUID -from sqlalchemy import select, func, and_ +from sqlalchemy import select, and_ from sqlalchemy.ext.asyncio import AsyncSession from app.models.user import User from app.models.customer import Customer @@ -86,18 +86,28 @@ async def get_light_board( } # ── All customers with their last visit per manager ── - # Subquery: latest visit date + method per (manager_id, customer_id) + # Subquery: latest visit (date + method) per (manager_id, customer_id). + # DISTINCT ON keeps exactly one row per pair even when multiple visits + # share the same latest date (e.g. companion/collaborative visits), + # preventing fan-out that would duplicate customer cards on the board. latest_visit = ( select( Visit.manager_id, Visit.customer_id, - func.max(Visit.visit_date).label("last_visit_date"), + Visit.visit_date.label("last_visit_date"), + Visit.visit_method.label("last_visit_method"), + ) + .distinct(Visit.manager_id, Visit.customer_id) + .order_by( + Visit.manager_id, + Visit.customer_id, + Visit.visit_date.desc(), + Visit.id.desc(), ) - .group_by(Visit.manager_id, Visit.customer_id) .subquery() ) - # Join: assignments → customers → latest_visit → visits (for method) + # Join: assignments → customers → latest_visit (date + method) rows = await db.execute( select( CustomerAssignment.manager_id, @@ -108,18 +118,13 @@ async def get_light_board( Customer.in_use_services, Customer.monthly_fee, latest_visit.c.last_visit_date, - Visit.visit_method, + latest_visit.c.last_visit_method, ) .join(Customer, Customer.id == CustomerAssignment.customer_id) .outerjoin(latest_visit, and_( latest_visit.c.manager_id == CustomerAssignment.manager_id, latest_visit.c.customer_id == CustomerAssignment.customer_id, )) - .outerjoin(Visit, and_( - Visit.manager_id == CustomerAssignment.manager_id, - Visit.customer_id == CustomerAssignment.customer_id, - Visit.visit_date == latest_visit.c.last_visit_date, - )) .where(CustomerAssignment.role == "primary") .order_by(Customer.name) )