feat(groups): chat_group 补 6 群主/建群列 + 复合PK迁移修复 + 存量回填

对齐云客员工聊天窗口·微信群 tab(2026-08-25 真机验证 raw_json 键):
- ChatGroup 加列 owner_name/owner_wxid/owner_alias/owner_avatar/
  friend_count/create_time(全 nullable)
- scraper _normalize_chat_group 同名提取;createTime 兼容 ms-int 与
  'YYYY-MM-DD HH:mm:ss'(smoke.db 实测 1105 行全为后者),复用
  _parse_add_time 归一 ms-epoch
- 迁移 099ac2097fa2:加 6 列 + 修复 Task1 评审遗留 drift——PK 由单列
  room_id 变更为复合 (room_id, channel, owner_account_id) 且
  owner_account_id NOT NULL(对齐模型 8a99940;smoke.db 1105 行无 NULL,
  未兜底编造数据);SQLite 走 batch 重建(copy_from 显式旧结构),
  MySQL 渲染为直接 ALTER(离线 --sql 已验证)
- 回填脚本 scripts/backfill_chat_group_fields.py(幂等,逐列只补空);
  smoke.db 实跑 backfilled 1105/1105,重跑 0/1105
- GroupOut 加 6 字段(model_validate 自动输出,路由无改)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
浅念
2026-08-25 14:32:12 +08:00
parent 90c28f4c7b
commit 6ff288f1d3
7 changed files with 342 additions and 1 deletions
+18
View File
@@ -889,6 +889,17 @@ def _normalize_chat_group(record: dict[str, Any]) -> dict[str, Any]:
Adds ``channel="wechat"`` and preserves the original record under
``raw_json``. ``None``-safe on every field: missing fields become ``None``
rather than raising, so a partial payload does not break the scrape.
2026-08-25 补充列(对齐云客 员工聊天窗口·微信群 tab,真机验证 raw_json 键):
* ``ownerName`` -> ``owner_name`` (群主昵称,上游脱敏)
* ``ownerWchatId`` -> ``owner_wxid`` (群主微信号,上游脱敏)
* ``ownerAlias`` -> ``owner_alias`` (群主微信号别名)
* ``ownerImage`` -> ``owner_avatar`` (群主头像)
* ``friendCount`` -> ``friend_count`` (群成员中员工好友数)
* ``createTime`` -> ``create_time`` (建群时间;实测 smoke.db 全量为
``'YYYY-MM-DD HH:mm:ss'`` 字符串,另兼容 ms-int/数字串,统一归一为
ms-epoch int,复用 ``_parse_add_time`` 的双形态解析)
"""
return {
"room_id": record.get("friendWxId"),
@@ -908,6 +919,13 @@ def _normalize_chat_group(record: dict[str, Any]) -> dict[str, Any]:
"add_members": None,
"last_chat_time": record.get("lastChatTime"),
"update_time": None,
# 群主/建群补充列(2026-08-25;raw_json 键 -> 同名列)
"owner_name": record.get("ownerName"),
"owner_wxid": record.get("ownerWchatId"),
"owner_alias": record.get("ownerAlias"),
"owner_avatar": record.get("ownerImage"),
"friend_count": _coerce_int_or_none(record.get("friendCount")),
"create_time": _parse_add_time(record.get("createTime")),
"version": record.get("version"),
"raw_json": record,
}