feat(api): customer filters user_id / add_time_since / last_chat_since

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
浅念
2026-08-05 18:48:59 +08:00
parent 8d7774b9f5
commit fef176bd90
2 changed files with 100 additions and 3 deletions
+13 -3
View File
@@ -1,9 +1,10 @@
"""GET /api/customers — paginated, filterable customer list.
Read-only query over the mirrored ``customer`` table. Supports filtering by
``channel``, ``owner_account_id`` (direct columns) and ``department_id``
(resolved through ``customer.user_id`` -> ``staff.dept_id``). Results are
ordered by ``last_chat_time desc`` (nulls last) then ``friend_id`` so the
``channel``, ``owner_account_id``, ``user_id``, ``add_time_since`` and
``last_chat_since`` (direct columns) and ``department_id`` (resolved through
``customer.user_id`` -> ``staff.dept_id``). Results are ordered by
``last_chat_time desc`` (nulls last) then ``friend_id`` so the
most-recently-active contacts surface first.
"""
@@ -34,6 +35,9 @@ async def list_customers(
channel: Annotated[str | None, Query()] = None,
department_id: Annotated[str | None, Query()] = None,
owner_account_id: Annotated[str | None, Query()] = None,
user_id: Annotated[str | None, Query()] = None,
add_time_since: Annotated[int | None, Query(ge=0)] = None,
last_chat_since: Annotated[int | None, Query(ge=0)] = None,
) -> Page[CustomerOut]:
"""List customers with optional filters and pagination."""
conditions = []
@@ -47,6 +51,12 @@ async def list_customers(
Staff.dept_id == department_id
)
conditions.append(Customer.user_id.in_(dept_user_ids))
if user_id is not None:
conditions.append(Customer.user_id == user_id)
if add_time_since is not None:
conditions.append(Customer.add_time >= add_time_since)
if last_chat_since is not None:
conditions.append(Customer.last_chat_time >= last_chat_since)
total_stmt = sa.select(sa.func.count()).select_from(Customer)
if conditions:
+87
View File
@@ -255,3 +255,90 @@ async def test_filters_compose_channel_and_owner(client: AsyncClient):
body = r.json()
assert body["total"] == 2
assert {it["friend_id"] for it in body["items"]} == {"wx_a", "wx_b"}
@pytest.fixture
async def filter_client():
"""Independent in-memory client seeded for the new add_time/last_chat filters.
Deliberately separate from the shared ``client`` fixture: that one seeds 3
rows and four existing tests assert ``total == 3`` against it, so adding
rows there would break them. This fixture mirrors the engine + override
pattern of ``client`` but seeds rows that specifically exercise the
``user_id`` / ``add_time_since`` / ``last_chat_since`` filters.
"""
engine = create_async_engine(
"sqlite+aiosqlite:///:memory:",
poolclass=StaticPool,
connect_args={"check_same_thread": False},
)
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
SessionLocal = async_sessionmaker(engine, expire_on_commit=False)
async with SessionLocal() as session:
session.add_all(
[
Customer(
friend_id="u1_a",
channel="wechat",
user_id="U1",
add_time=1000,
last_chat_time=500,
version="v1",
),
Customer(
friend_id="u1_b",
channel="wechat",
user_id="U1",
add_time=2000,
last_chat_time=2000,
version="v1",
),
Customer(
friend_id="u2_a",
channel="wechat",
user_id="U2",
add_time=3000,
last_chat_time=3000,
version="v1",
),
]
)
await session.commit()
async def _override_session():
async with SessionLocal() as s:
yield s
app.dependency_overrides[deps.get_session] = _override_session
try:
async with AsyncClient(
transport=ASGITransport(app=app), base_url="http://test"
) as ac:
yield ac
finally:
app.dependency_overrides.clear()
await engine.dispose()
async def test_filter_by_user_id(filter_client: AsyncClient):
"""``?user_id=U1`` -> only customers owned by U1."""
r = await filter_client.get("/api/customers", params={"user_id": "U1"})
assert r.status_code == 200
assert {c["friend_id"] for c in r.json()["items"]} == {"u1_a", "u1_b"}
async def test_filter_by_add_time_since(filter_client: AsyncClient):
"""``?add_time_since=1500`` -> rows with add_time >= 1500 (u1_b, u2_a)."""
r = await filter_client.get("/api/customers", params={"add_time_since": 1500})
assert r.status_code == 200
ids = [c["friend_id"] for c in r.json()["items"]]
assert "u1_a" not in ids and "u1_b" in ids and "u2_a" in ids
async def test_filter_by_last_chat_since(filter_client: AsyncClient):
"""``?last_chat_since=1500`` -> rows with last_chat_time >= 1500 (u1_a out)."""
r = await filter_client.get("/api/customers", params={"last_chat_since": 1500})
assert r.status_code == 200
assert "u1_a" not in [c["friend_id"] for c in r.json()["items"]]