be0d6f844e
用户报两点:①微信群列表与云客群统计总数不一致(实测云客 1,864 vs 本地 2,012——friend_list/es 时代残留的 148 行 owner 组合已不在 getChatroomList 返回里);②群详情成员列表全是 wxid 无法识别。 - chatroom-stats 全窗模式加对账:名单取满(>=声明 total)才删本地不在 云客名单里的 chat_group(wechat)行——页脚从此与云客一字不差;短页宁 可不对账防误删 - backfill_group_members 增强:头像列(消息最新一条 sender_head_url)+ customer 兜底 UPDATE(member_id=friend_id 命中本司好友补昵称/头像, 覆盖未发言成员);full_sync 8b 原位生效 - 群详情 drawer 成员墙:头像+昵称网格(群昵称>好友昵称>wxid 回退+wxid 副行),替代原 memberIds 原始串;仅微信 tab(WA 群成员未派生) 722 后端测试绿,前端构建过。 Co-Authored-By: Claude Code <noreply@anthropic.com>
95 lines
3.7 KiB
Python
95 lines
3.7 KiB
Python
#!/usr/bin/env python
|
||
"""从群消息派生 group_member(发言者=成员)+ customer 兜底补全昵称/头像。
|
||
|
||
云客 API 不单独给群成员列表(chatroom payload 的 memberIds 我们抓不到,
|
||
2026-08-20 确认),但群消息的 talker 就是发言成员的 wxid、sender_name/
|
||
sender_head_url 是其群昵称/头像——群消息足够派生出高质量成员表(覆盖
|
||
所有发过言的成员)。昵称/头像取该成员在群里**最新一条**消息的值。
|
||
|
||
未发言的成员(被拉进群从没说话)从 customer 表兜底:member_id=friend_id
|
||
命中本司好友时补好友昵称/头像(2026-09-10 群详情成员墙)。
|
||
|
||
幂等可重跑;建议在 group-messages 之后执行(full_sync_all.sh 8b 已挂)。
|
||
|
||
用法:
|
||
cd yunke-server && DB_URL="sqlite+aiosqlite:///./smoke.db" \
|
||
.venv/Scripts/python.exe -m scripts.backfill_group_members
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import asyncio
|
||
import logging
|
||
|
||
from sqlalchemy import text as sa_text
|
||
|
||
from app.db.base import async_engine, async_session_maker
|
||
from app.db.models import GroupMember
|
||
from app.storage.repos import _upsert_one
|
||
|
||
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
|
||
log = logging.getLogger("backfill_group_members")
|
||
|
||
# row_number() 窗口取 timestamp 最新一行的 sender_name/sender_head_url:
|
||
# MySQL 8 的 only_full_group_by 拒绝 SQLite 式「max()+裸列」(1055),窗口
|
||
# 写法两库通用且语义严格(最新一条的昵称/头像),不依赖方言碰巧行为
|
||
SQL = sa_text("""
|
||
SELECT room_id, talker_id, sender_name, sender_head_url, last_ts, msg_count
|
||
FROM (
|
||
SELECT room_id, talker_id, sender_name, sender_head_url,
|
||
timestamp AS last_ts,
|
||
count(*) OVER (PARTITION BY room_id, talker_id) AS msg_count,
|
||
row_number() OVER (
|
||
PARTITION BY room_id, talker_id ORDER BY timestamp DESC
|
||
) AS rn
|
||
FROM message
|
||
WHERE channel = 'wechat' AND room_id IS NOT NULL AND room_id != ''
|
||
AND talker_id IS NOT NULL AND talker_id != ''
|
||
) t
|
||
WHERE rn = 1
|
||
""")
|
||
|
||
# 未发言成员兜底:member_id 命中 customer(本司好友)时补 NULL 的昵称/头像。
|
||
# customer 全局一人一行(PK=friend_id),join 不放大行数;COALESCE 只补不清。
|
||
CUSTOMER_FALLBACK = sa_text("""
|
||
UPDATE group_member gm
|
||
JOIN customer c ON c.friend_id = gm.member_id AND c.channel = gm.channel
|
||
SET gm.nickname = COALESCE(gm.nickname, c.friend_nickname),
|
||
gm.avatar = COALESCE(gm.avatar, c.head_image_url)
|
||
WHERE gm.channel = 'wechat'
|
||
AND (gm.nickname IS NULL OR gm.avatar IS NULL)
|
||
""")
|
||
|
||
|
||
async def main() -> None:
|
||
async with async_engine.connect() as conn:
|
||
rows = (await conn.execute(SQL)).fetchall()
|
||
log.info(f"派生出 {len(rows)} 个 (群, 成员) 对")
|
||
|
||
async with async_session_maker() as session:
|
||
for room_id, talker_id, sender_name, head_url, last_ts, msg_count in rows:
|
||
await _upsert_one(session, GroupMember, {
|
||
"channel": "wechat",
|
||
"room_id": room_id,
|
||
"member_id": talker_id,
|
||
"nickname": sender_name,
|
||
"avatar": head_url,
|
||
# raw_json 附加派生统计,抽屉可见
|
||
"raw_json": {
|
||
"derived_from": "group_messages",
|
||
"msg_count": msg_count,
|
||
"last_msg_ts": last_ts,
|
||
},
|
||
})
|
||
await session.commit()
|
||
|
||
# 兜底补未发言成员(独立事务,幂等)
|
||
async with async_engine.begin() as conn:
|
||
res = await conn.execute(CUSTOMER_FALLBACK)
|
||
log.info(f"customer 兜底补全 {res.rowcount} 行昵称/头像")
|
||
|
||
log.info("DONE: group_member upsert 完成")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
asyncio.run(main())
|