Files
yunke/yunke-server/scripts/backfill_group_members.py
T
浅念 be0d6f844e feat(group-members): 群列表对账云客总数+群详情成员墙(头像+昵称)
用户报两点:①微信群列表与云客群统计总数不一致(实测云客 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>
2026-09-10 12:11:42 +08:00

95 lines
3.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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())