mirror of
https://github.com/LifeArchiveProject/WeChatDataAnalysis.git
synced 2026-02-19 22:30:49 +08:00
improvement(wrapped): 概览卡片补充年度新增好友统计
- 后端 Card#0 增加 addedFriends 字段(基于系统消息关键字 best-effort 识别新好友) - 前端概览文案在 addedFriends>0 时展示新增好友数 - bump wrapped cache version,避免旧缓存导致字段缺失
This commit is contained in:
@@ -18,6 +18,11 @@
|
|||||||
在与你相伴的
|
在与你相伴的
|
||||||
<span class="wrapped-number text-[#07C160] font-semibold">{{ formatInt(activeDays) }}</span>
|
<span class="wrapped-number text-[#07C160] font-semibold">{{ formatInt(activeDays) }}</span>
|
||||||
天里,
|
天里,
|
||||||
|
<template v-if="addedFriends > 0">
|
||||||
|
你总共加了
|
||||||
|
<span class="wrapped-number text-[#07C160] font-semibold">{{ formatInt(addedFriends) }}</span>
|
||||||
|
位好友,
|
||||||
|
</template>
|
||||||
<template v-if="mostActiveHour !== null && mostActiveWeekdayName">
|
<template v-if="mostActiveHour !== null && mostActiveWeekdayName">
|
||||||
你最常在 {{ mostActiveWeekdayName }} 的
|
你最常在 {{ mostActiveWeekdayName }} 的
|
||||||
<span class="wrapped-number text-[#07C160] font-semibold">{{ mostActiveHour }}</span>
|
<span class="wrapped-number text-[#07C160] font-semibold">{{ mostActiveHour }}</span>
|
||||||
@@ -116,6 +121,7 @@ const formatFloat = (n, digits = 1) => {
|
|||||||
|
|
||||||
const totalMessages = computed(() => Number(props.card?.data?.totalMessages || 0))
|
const totalMessages = computed(() => Number(props.card?.data?.totalMessages || 0))
|
||||||
const activeDays = computed(() => Number(props.card?.data?.activeDays || 0))
|
const activeDays = computed(() => Number(props.card?.data?.activeDays || 0))
|
||||||
|
const addedFriends = computed(() => Number(props.card?.data?.addedFriends || 0))
|
||||||
const messagesPerDay = computed(() => Number(props.card?.data?.messagesPerDay || 0))
|
const messagesPerDay = computed(() => Number(props.card?.data?.messagesPerDay || 0))
|
||||||
|
|
||||||
const mostActiveHour = computed(() => {
|
const mostActiveHour = computed(() => {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from ...chat_helpers import (
|
|||||||
_pick_display_name,
|
_pick_display_name,
|
||||||
_quote_ident,
|
_quote_ident,
|
||||||
_should_keep_session,
|
_should_keep_session,
|
||||||
|
_to_char_token_text,
|
||||||
)
|
)
|
||||||
from ...logging_config import get_logger
|
from ...logging_config import get_logger
|
||||||
|
|
||||||
@@ -28,12 +29,22 @@ logger = get_logger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
_MD5_HEX_RE = re.compile(r"(?i)[0-9a-f]{32}")
|
_MD5_HEX_RE = re.compile(r"(?i)[0-9a-f]{32}")
|
||||||
|
# Best-effort heuristics for "new friends added" detection: WeChat system messages vary by version.
|
||||||
|
_ADDED_FRIEND_PATTERNS: tuple[str, ...] = (
|
||||||
|
"你已添加了",
|
||||||
|
"你添加了",
|
||||||
|
"现在可以开始聊天了",
|
||||||
|
"以上是打招呼的消息",
|
||||||
|
"通过了你的朋友验证",
|
||||||
|
"通过你的朋友验证",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class GlobalOverviewStats:
|
class GlobalOverviewStats:
|
||||||
year: int
|
year: int
|
||||||
active_days: int
|
active_days: int
|
||||||
|
added_friends: int
|
||||||
local_type_counts: dict[int, int]
|
local_type_counts: dict[int, int]
|
||||||
kind_counts: dict[str, int]
|
kind_counts: dict[str, int]
|
||||||
latest_ts: int
|
latest_ts: int
|
||||||
@@ -349,6 +360,37 @@ def compute_global_overview_stats(
|
|||||||
top_group = pick_top(group_counts_i)
|
top_group = pick_top(group_counts_i)
|
||||||
top_phrase = pick_top(phrase_counts_i)
|
top_phrase = pick_top(phrase_counts_i)
|
||||||
|
|
||||||
|
# New friends added in this year (best-effort via WeChat system messages).
|
||||||
|
added_friend_usernames: set[str] = set()
|
||||||
|
try:
|
||||||
|
like_patterns: list[str] = []
|
||||||
|
for pat in _ADDED_FRIEND_PATTERNS:
|
||||||
|
tok = _to_char_token_text(pat)
|
||||||
|
if tok:
|
||||||
|
like_patterns.append(f"%{tok}%")
|
||||||
|
|
||||||
|
if like_patterns:
|
||||||
|
where_added = f"{ts_expr} >= ? AND {ts_expr} < ? AND db_stem NOT LIKE 'biz_message%'"
|
||||||
|
cond_added = " OR ".join(['\"text\" LIKE ?'] * len(like_patterns))
|
||||||
|
rows_added = conn.execute(
|
||||||
|
f"SELECT DISTINCT username FROM message_fts "
|
||||||
|
f"WHERE {where_added} "
|
||||||
|
"AND CAST(local_type AS INTEGER) = 10000 "
|
||||||
|
f"AND ({cond_added})",
|
||||||
|
(start_ts, end_ts, *like_patterns),
|
||||||
|
).fetchall()
|
||||||
|
for rr in rows_added:
|
||||||
|
if not rr or not rr[0]:
|
||||||
|
continue
|
||||||
|
u = str(rr[0] or "").strip()
|
||||||
|
if not u or u.endswith("@chatroom") or (not is_keep_username(u)):
|
||||||
|
continue
|
||||||
|
added_friend_usernames.add(u)
|
||||||
|
except Exception:
|
||||||
|
added_friend_usernames = set()
|
||||||
|
|
||||||
|
added_friends_i = len(added_friend_usernames)
|
||||||
|
|
||||||
total_messages = int(sum(local_type_counts_i.values()))
|
total_messages = int(sum(local_type_counts_i.values()))
|
||||||
logger.info(
|
logger.info(
|
||||||
"Wrapped card#0 overview computed (search index): account=%s year=%s total=%s active_days=%s sender=%s db=%s elapsed=%.2fs",
|
"Wrapped card#0 overview computed (search index): account=%s year=%s total=%s active_days=%s sender=%s db=%s elapsed=%.2fs",
|
||||||
@@ -364,6 +406,7 @@ def compute_global_overview_stats(
|
|||||||
return GlobalOverviewStats(
|
return GlobalOverviewStats(
|
||||||
year=year,
|
year=year,
|
||||||
active_days=active_days_i,
|
active_days=active_days_i,
|
||||||
|
added_friends=added_friends_i,
|
||||||
local_type_counts={int(k): int(v) for k, v in local_type_counts_i.items()},
|
local_type_counts={int(k): int(v) for k, v in local_type_counts_i.items()},
|
||||||
kind_counts={str(k): int(v) for k, v in kind_counts_i.items()},
|
kind_counts={str(k): int(v) for k, v in kind_counts_i.items()},
|
||||||
latest_ts=latest_ts_i,
|
latest_ts=latest_ts_i,
|
||||||
@@ -411,6 +454,8 @@ def compute_global_overview_stats(
|
|||||||
active_days: set[str] = set()
|
active_days: set[str] = set()
|
||||||
per_username_counts: Counter[str] = Counter()
|
per_username_counts: Counter[str] = Counter()
|
||||||
phrase_counts: Counter[str] = Counter()
|
phrase_counts: Counter[str] = Counter()
|
||||||
|
added_friend_usernames: set[str] = set()
|
||||||
|
added_like_patterns = [f"%{p}%" for p in _ADDED_FRIEND_PATTERNS if str(p or "").strip()]
|
||||||
|
|
||||||
latest_ts = 0
|
latest_ts = 0
|
||||||
|
|
||||||
@@ -425,6 +470,7 @@ def compute_global_overview_stats(
|
|||||||
if not tables:
|
if not tables:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
skip_sender_stats = False
|
||||||
sender_rowid: int | None = None
|
sender_rowid: int | None = None
|
||||||
if sender:
|
if sender:
|
||||||
try:
|
try:
|
||||||
@@ -436,13 +482,38 @@ def compute_global_overview_stats(
|
|||||||
sender_rowid = int(r2[0])
|
sender_rowid = int(r2[0])
|
||||||
except Exception:
|
except Exception:
|
||||||
sender_rowid = None
|
sender_rowid = None
|
||||||
# Can't reliably filter by sender for this shard; skip to avoid mixing directions.
|
# Can't reliably filter by sender for this shard; skip sender-only stats to avoid mixing directions.
|
||||||
if sender_rowid is None:
|
if sender_rowid is None:
|
||||||
continue
|
skip_sender_stats = True
|
||||||
|
|
||||||
for table_name in tables:
|
for table_name in tables:
|
||||||
qt = _quote_ident(table_name)
|
qt = _quote_ident(table_name)
|
||||||
username = resolve_username_from_table(table_name)
|
username = resolve_username_from_table(table_name)
|
||||||
|
|
||||||
|
# New friends added: detect common WeChat system messages within this year.
|
||||||
|
if (
|
||||||
|
added_like_patterns
|
||||||
|
and username
|
||||||
|
and (not username.endswith("@chatroom"))
|
||||||
|
and _should_keep_session(username, include_official=False)
|
||||||
|
):
|
||||||
|
cond_added = " OR ".join(["CAST(message_content AS TEXT) LIKE ?"] * len(added_like_patterns))
|
||||||
|
sql_added = (
|
||||||
|
f"SELECT 1 FROM {qt} "
|
||||||
|
f"WHERE local_type = 10000 "
|
||||||
|
f" AND {ts_expr} >= ? AND {ts_expr} < ? "
|
||||||
|
f" AND ({cond_added}) "
|
||||||
|
"LIMIT 1"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
r_added = conn.execute(sql_added, (start_ts, end_ts, *added_like_patterns)).fetchone()
|
||||||
|
except Exception:
|
||||||
|
r_added = None
|
||||||
|
if r_added is not None:
|
||||||
|
added_friend_usernames.add(username)
|
||||||
|
|
||||||
|
if skip_sender_stats:
|
||||||
|
continue
|
||||||
sender_where = " AND real_sender_id = ?" if sender_rowid is not None else ""
|
sender_where = " AND real_sender_id = ?" if sender_rowid is not None else ""
|
||||||
params = (start_ts, end_ts, sender_rowid) if sender_rowid is not None else (start_ts, end_ts)
|
params = (start_ts, end_ts, sender_rowid) if sender_rowid is not None else (start_ts, end_ts)
|
||||||
|
|
||||||
@@ -593,6 +664,7 @@ def compute_global_overview_stats(
|
|||||||
return GlobalOverviewStats(
|
return GlobalOverviewStats(
|
||||||
year=year,
|
year=year,
|
||||||
active_days=len(active_days),
|
active_days=len(active_days),
|
||||||
|
added_friends=len(added_friend_usernames),
|
||||||
local_type_counts={int(k): int(v) for k, v in local_type_counts.items()},
|
local_type_counts={int(k): int(v) for k, v in local_type_counts.items()},
|
||||||
kind_counts={str(k): int(v) for k, v in kind_counts.items()},
|
kind_counts={str(k): int(v) for k, v in kind_counts.items()},
|
||||||
latest_ts=int(latest_ts),
|
latest_ts=int(latest_ts),
|
||||||
@@ -744,6 +816,7 @@ def build_card_00_global_overview(
|
|||||||
"year": int(year),
|
"year": int(year),
|
||||||
"totalMessages": int(heatmap.total_messages),
|
"totalMessages": int(heatmap.total_messages),
|
||||||
"activeDays": int(stats.active_days),
|
"activeDays": int(stats.active_days),
|
||||||
|
"addedFriends": int(stats.added_friends),
|
||||||
"messagesPerDay": messages_per_day,
|
"messagesPerDay": messages_per_day,
|
||||||
"mostActiveHour": most_active_hour,
|
"mostActiveHour": most_active_hour,
|
||||||
"mostActiveWeekday": most_active_weekday,
|
"mostActiveWeekday": most_active_weekday,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ logger = get_logger(__name__)
|
|||||||
# an older partial cache.
|
# an older partial cache.
|
||||||
_IMPLEMENTED_UPTO_ID = 3
|
_IMPLEMENTED_UPTO_ID = 3
|
||||||
# Bump this when we change card payloads/ordering while keeping the same implemented_upto.
|
# Bump this when we change card payloads/ordering while keeping the same implemented_upto.
|
||||||
_CACHE_VERSION = 8
|
_CACHE_VERSION = 9
|
||||||
|
|
||||||
|
|
||||||
# "Manifest" is used by the frontend to render the deck quickly, then lazily fetch each card.
|
# "Manifest" is used by the frontend to render the deck quickly, then lazily fetch each card.
|
||||||
|
|||||||
Reference in New Issue
Block a user