fix: wechat account scrape (form-data+dates) + frontend staff page
Backend (scrape_wechat_accounts): - Send wxUser/list as form-urlencoded (not JSON) with the REQUIRED filters isOnJob / departmentId / startdate / enddate + empty userId/salesname/ weChatNickName, matching the live request (empty fails with code -90000). - scrape_wechat_accounts now takes department_id (the company-root dept id) plus optional start/end defaulting to a recent 7-day UTC window. - paginate_by_page gains a form=True mode (data= + form content-type). - Orchestrator sync_wechat_accounts resolves the root dept from the Department table (or accepts one) and skips with a warning when unknown. - seed_accounts.py resolves the root via scrape_departments. Frontend (yunkes): - Add StaffList.vue (/staff) with is_on_job filter + pagination. - Add /staff route and a 员工 nav item between 客户 and 群. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -29,7 +29,7 @@ import sqlalchemy as sa
|
||||
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
||||
|
||||
from app.db.base import Base
|
||||
from app.db.models import SyncRun, SyncState
|
||||
from app.db.models import Department, SyncRun, SyncState
|
||||
from app.sync import orchestrator
|
||||
from app.sync.watermarks import get_last_timestamp
|
||||
|
||||
@@ -477,7 +477,7 @@ async def test_sync_all_wechat_runs_each_entity_then_messages(monkeypatch):
|
||||
try:
|
||||
SessionLocal = async_sessionmaker(engine, expire_on_commit=False)
|
||||
|
||||
async def empty_async_gen(_client=None):
|
||||
async def empty_async_gen(_client=None, **_kwargs):
|
||||
if False: # pragma: no cover
|
||||
yield {}
|
||||
|
||||
@@ -496,6 +496,11 @@ async def test_sync_all_wechat_runs_each_entity_then_messages(monkeypatch):
|
||||
# orchestrator namespace) are never called. No need to patch them.
|
||||
|
||||
async with SessionLocal() as session:
|
||||
# sync_wechat_accounts resolves the company-root dept id required
|
||||
# by the wxUser/list form body from the Department table, so seed
|
||||
# a root (parent_id=None) before running the coordinator.
|
||||
session.add(Department(dept_id="ROOT_DEPT", parent_id=None, name="Root"))
|
||||
await session.flush()
|
||||
results = await orchestrator.sync_all_wechat(
|
||||
session,
|
||||
client=object(),
|
||||
@@ -520,6 +525,92 @@ async def test_sync_all_wechat_runs_each_entity_then_messages(monkeypatch):
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
async def test_sync_wechat_accounts_skips_when_no_root_department(monkeypatch):
|
||||
"""``wxUser/list`` REQUIRES a ``departmentId`` filter. When the org tree is
|
||||
empty (no root resolvable from the Department table) the orchestrator must
|
||||
NOT send a known-bad empty filter; it skips with a logged warning and
|
||||
returns a ``status='skipped'`` result without ever calling the scraper."""
|
||||
engine = await _make_engine()
|
||||
try:
|
||||
SessionLocal = async_sessionmaker(engine, expire_on_commit=False)
|
||||
|
||||
called = False
|
||||
|
||||
async def fake_scrape_accounts(client, *, department_id, **_kwargs):
|
||||
nonlocal called
|
||||
called = True
|
||||
if False: # pragma: no cover
|
||||
yield {}
|
||||
|
||||
monkeypatch.setattr(
|
||||
orchestrator, "scrape_wechat_accounts", fake_scrape_accounts
|
||||
)
|
||||
|
||||
async with SessionLocal() as session:
|
||||
result = await orchestrator.sync_wechat_accounts(
|
||||
session, client=object()
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
assert result.status == "skipped"
|
||||
assert result.fetched == 0
|
||||
assert result.upserted == 0
|
||||
assert not called
|
||||
|
||||
# No SyncRun recorded for a skip (it is not a success or a failure).
|
||||
async with SessionLocal() as session:
|
||||
rows = (
|
||||
await session.execute(
|
||||
sa.select(SyncRun).where(
|
||||
SyncRun.entity_type == "wechat_account"
|
||||
)
|
||||
)
|
||||
).scalars().all()
|
||||
assert rows == []
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
async def test_sync_wechat_accounts_passes_resolved_root_to_scraper(monkeypatch):
|
||||
"""When a root department exists in the DB, the orchestrator resolves its
|
||||
id and passes it as ``department_id`` to the scraper."""
|
||||
engine = await _make_engine()
|
||||
try:
|
||||
SessionLocal = async_sessionmaker(engine, expire_on_commit=False)
|
||||
|
||||
captured: dict = {}
|
||||
|
||||
async def fake_scrape_accounts(client, *, department_id, **_kwargs):
|
||||
captured["department_id"] = department_id
|
||||
if False: # pragma: no cover
|
||||
yield {}
|
||||
|
||||
async def fake_upsert(session, rec):
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(
|
||||
orchestrator, "scrape_wechat_accounts", fake_scrape_accounts
|
||||
)
|
||||
monkeypatch.setattr(orchestrator, "upsert_wechat_account", fake_upsert)
|
||||
|
||||
async with SessionLocal() as session:
|
||||
session.add(
|
||||
Department(
|
||||
dept_id="ROOT_DEPT", parent_id=None, name="Root"
|
||||
)
|
||||
)
|
||||
await session.flush()
|
||||
result = await orchestrator.sync_wechat_accounts(
|
||||
session, client=object()
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
assert result.status == "success"
|
||||
assert captured["department_id"] == "ROOT_DEPT"
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Phone calls + SMS — full re-scrape entities (M4)
|
||||
# ===========================================================================
|
||||
|
||||
Reference in New Issue
Block a user