2026-08-20 09:21:20 +08:00
|
|
|
|
#!/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")
|
|
|
|
|
|
|
2026-08-27 14:51:05 +08:00
|
|
|
|
# row_number() 窗口取 timestamp 最新一行的 sender_name:MySQL 8 的
|
|
|
|
|
|
# only_full_group_by 拒绝 SQLite 式「max()+裸列」(1055),窗口写法两库通用
|
|
|
|
|
|
# 且语义严格(最新一条的昵称),不依赖方言碰巧行为
|
2026-08-20 09:21:20 +08:00
|
|
|
|
SQL = sa_text("""
|
2026-08-27 14:51:05 +08:00
|
|
|
|
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
|
2026-08-20 09:21:20 +08:00
|
|
|
|
""")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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())
|