0df53c6b1a
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>
155 lines
5.2 KiB
Python
155 lines
5.2 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 REQUEST is **form-urlencoded** (NOT JSON): the endpoint binds Spring
|
|
``@RequestParam`` from the form body and REQUIRES ``isOnJob`` /
|
|
``departmentId`` (the company-root dept id) / ``startdate`` / ``enddate`` or
|
|
it answers ``code -90000`` ("缺少..."). ``userId`` / ``salesname`` /
|
|
``weChatNickName`` are sent empty to mirror the real client.
|
|
|
|
The RESPONSE 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,
|
|
},
|
|
}
|