2b79e7a232
M2 organization scrapers for yunke-server: - scrape_departments: POST /usercenter/dept/tree. Single request (no pagination). Defensively unwraps the envelope (data is node list OR an object wrapping nodes under children), flattens BOTH nested and flat shapes, and reconstructs the materialized path by walking the pId chain (ancestor names joined with /). Maps id/pId/name -> dept_id/parent_id/name. - scrape_wechat_accounts: POST /wechatweb/wxUser/list. Page-number pagination via the existing paginate_by_page helper. Envelope differs from friend_list (no basicResult wrapper): data.data/data.total. Maps wechatId/nickName/alias/phone/userId/userName/contactsNum/groupNum/ sendNum/receiveNum/isOnJob/dbSize/onlineStatus/lastUpdate/version to WechatAccount columns; carries embedded staff pair for the repo layer. Repos: - upsert_department: idempotent upsert into Department (PK dept_id). - upsert_staff: primitive idempotent staff upsert (PK user_id, is_on_job coerced to bool). - upsert_wechat_account: now ALSO upserts the embedded Staff row via upsert_staff_from_account (best-effort; skips when account has no user_id). - upsert_staff_from_account: refactored to delegate to upsert_staff and forward phone/sale_phone/is_on_job when present. Tests (TDD red->green): test_scrape_departments (nested + flat + wrapped-data + empty), test_scrape_wechat_accounts (2-page pagination + body advance + empty first page), test_repos extended (dept idempotency/root parent, direct staff upsert + bool coercion, account-also-upserts-staff, re-upsert updates both, no-user_id skips staff). Cassettes: dept_tree_fixture (same 4 nodes in both nested + flat shapes), wxUser_fixture (4 capture-shaped accounts incl. departed + orphan). Suite: 97 -> 111 passing. ruff check clean. Co-Authored-By: Claude <noreply@anthropic.com>
148 lines
4.9 KiB
Python
148 lines
4.9 KiB
Python
"""Fixture payloads for ``POST /wechatweb/wxUser/list`` (Task 2.1).
|
|
|
|
These records mirror the real field names captured from the live CRM (see the
|
|
data dictionary §2: ``wechatId, nickName, alias, phone, userId,
|
|
userName(员工真名), contactsNum, groupNum, sendNum/receiveNum, isOnJob,
|
|
dbSize, 在线状态, lastUpdate``; total=55).
|
|
|
|
The envelope differs from ``wxFriends/friend_list/es``: there is **no**
|
|
``basicResult`` wrapper. The pagination fields sit directly under ``data``::
|
|
|
|
{"code":"10000","data":{"data":[...], "total":.., "pageNum":.., "pageSize":..}}
|
|
|
|
The cassette ships two pages so a test can drive the page-number paginator
|
|
across the empty-page stop signal:
|
|
|
|
* page 1 -> 2 of the 4 fixture records, ``total=4``
|
|
* page 2 -> the other 2 records, ``total=4``
|
|
* (page 3 -> empty ``data`` array, served inline by the test)
|
|
|
|
Records carry every field with a target ``WechatAccount`` column so the test
|
|
can assert a full mapping end-to-end, plus the embedded ``userId``+``userName``
|
|
staff pair (the owning employee) so the embedded-staff repo upsert can be
|
|
exercised against capture-shaped input.
|
|
"""
|
|
|
|
# The module name uses camelCase deliberately to mirror the upstream endpoint
|
|
# name (``wxUser``); the Task 2.1 spec mandates this exact filename.
|
|
# ruff: noqa: N999
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Capture-shaped WeChat staff-account records (4 total across 2 pages).
|
|
# ---------------------------------------------------------------------------
|
|
|
|
WXUSER_RECORDS: list[dict[str, Any]] = [
|
|
{
|
|
"wechatId": "wxid_staff_alice01",
|
|
"nickName": "Alice WeChat",
|
|
"alias": "alice_wx",
|
|
"phone": "13800000001",
|
|
"userId": "staff_user_001",
|
|
"userName": "Alice Wang",
|
|
"contactsNum": 312,
|
|
"groupNum": 18,
|
|
"sendNum": 1204,
|
|
"receiveNum": 2387,
|
|
"isOnJob": 1,
|
|
"dbSize": 482103,
|
|
"onlineStatus": "online",
|
|
"headImageUrl": "https://wx.qlogo.cn/avatar_alice.png",
|
|
"lastUpdate": 1700000000123,
|
|
"version": "v1.20260731.alice",
|
|
},
|
|
{
|
|
"wechatId": "wxid_staff_bob02",
|
|
"nickName": "Bob WeChat",
|
|
"alias": "bob_wx",
|
|
"phone": "13800000002",
|
|
"userId": "staff_user_002",
|
|
"userName": "Bob Chen",
|
|
"contactsNum": 87,
|
|
"groupNum": 4,
|
|
"sendNum": 211,
|
|
"receiveNum": 402,
|
|
"isOnJob": 1,
|
|
"dbSize": 91205,
|
|
"onlineStatus": "offline",
|
|
"headImageUrl": "https://wx.qlogo.cn/avatar_bob.png",
|
|
"lastUpdate": 1700000000456,
|
|
"version": "v1.20260731.bob",
|
|
},
|
|
{
|
|
"wechatId": "wxid_staff_carol03",
|
|
"nickName": "Carol We Chat",
|
|
"alias": "carol_wx",
|
|
"phone": "13800000003",
|
|
"userId": "staff_user_003",
|
|
"userName": "Carol Liu",
|
|
"contactsNum": 1502,
|
|
"groupNum": 72,
|
|
"sendNum": 9034,
|
|
"receiveNum": 11245,
|
|
"isOnJob": 0, # departed staff — exercises the falsy isOnJob path
|
|
"dbSize": 2_100_445,
|
|
"onlineStatus": "offline",
|
|
"headImageUrl": "https://wx.qlogo.cn/avatar_carol.png",
|
|
"lastUpdate": 1700000000789,
|
|
"version": "v1.20260731.carol",
|
|
},
|
|
{
|
|
# Account with NO embedded staff (userId missing) — the scraper must
|
|
# still yield it; the embedded-staff repo upsert must skip it cleanly.
|
|
"wechatId": "wxid_staff_orphan04",
|
|
"nickName": "Orphan WeChat",
|
|
"alias": "orphan_wx",
|
|
"phone": "13800000004",
|
|
"userId": None,
|
|
"userName": None,
|
|
"contactsNum": 5,
|
|
"groupNum": 0,
|
|
"sendNum": 1,
|
|
"receiveNum": 2,
|
|
"isOnJob": 0,
|
|
"dbSize": 1024,
|
|
"onlineStatus": "offline",
|
|
"headImageUrl": "https://wx.qlogo.cn/avatar_orphan.png",
|
|
"lastUpdate": 1700000001000,
|
|
"version": "v1.20260731.orphan",
|
|
},
|
|
]
|
|
|
|
# Default page size for the fixture (and the scraper). total=4 means the
|
|
# fixture fills exactly 2 pages of size 2 plus a terminating empty page.
|
|
TOTAL = len(WXUSER_RECORDS)
|
|
DEFAULT_PAGE_SIZE = 2
|
|
|
|
|
|
def make_wxuser_list_response(
|
|
records: list[dict[str, Any]],
|
|
*,
|
|
total: int,
|
|
page_num: int = 1,
|
|
page_size: int = DEFAULT_PAGE_SIZE,
|
|
trace_id: str | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Build a capture-shaped ``wxUser/list`` response envelope.
|
|
|
|
Shape (no ``basicResult`` wrapper)::
|
|
|
|
{"message":"调用成功","code":"10000","success":true,"traceId":"...",
|
|
"data":{"data":[...], "total":.., "pageNum":.., "pageSize":..}}
|
|
"""
|
|
return {
|
|
"message": "调用成功",
|
|
"code": "10000",
|
|
"success": True,
|
|
"traceId": trace_id or f"trace-{page_num}",
|
|
"data": {
|
|
"data": list(records),
|
|
"total": total,
|
|
"pageNum": page_num,
|
|
"pageSize": page_size,
|
|
},
|
|
}
|