Files
yunke/yunke-server/scripts/backfill_group_members.py
T
浅念 b2ccbee1ab fix(scripts): backfill_group_members 窗口函数化过 MySQL only_full_group_by(单库收敛)
max(timestamp)+裸 sender_name 的 GROUP BY 是 SQLite 方言行为,MySQL 8 默认
only_full_group_by 直接 1055 拒绝——2026-08-27 单库收敛(本地+调度器同连
Docker MySQL)时在 MySQL 上实跑触发。改 row_number() OVER 取每 (room_id,
talker_id) 最新一行的 sender_name + count() OVER 计数:两库通用,且语义从
「碰巧取 max 行」变为严格最新一条。另 gitignore 补 *.db.retired
(smoke.db 退役改名后 *.db 规则罩不住)。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-27 14:51:05 +08:00

73 lines
2.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(发言者=成员)。
云客 API 不单独给群成员列表(chatroom payload 的 memberIds 我们抓不到,
2026-08-20 确认),但群消息的 talker 就是发言成员的 wxid、sender_name 是其
群昵称——368k 群消息足够派生出高质量成员表(覆盖所有发过言的成员)。
昵称取该成员在群里最新一条消息的 sender_name(SQLite 的 max()+裸列行为,
取 max(timestamp) 同行的值)。幂等可重跑;建议在 group-messages 之后执行。
用法:
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:MySQL 8 的
# only_full_group_by 拒绝 SQLite 式「max()+裸列」(1055),窗口写法两库通用
# 且语义严格(最新一条的昵称),不依赖方言碰巧行为
SQL = sa_text("""
SELECT room_id, talker_id, sender_name, last_ts, msg_count
FROM (
SELECT room_id, talker_id, sender_name, 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
""")
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, last_ts, msg_count in rows:
await _upsert_one(session, GroupMember, {
"channel": "wechat",
"room_id": room_id,
"member_id": talker_id,
"nickname": sender_name,
# raw_json 附加派生统计,抽屉可见
"raw_json": {
"derived_from": "group_messages",
"msg_count": msg_count,
"last_msg_ts": last_ts,
},
})
await session.commit()
log.info(f"DONE: group_member upserted={len(rows)}")
if __name__ == "__main__":
asyncio.run(main())